diff --git a/Html/app.py b/Html/app.py index 2d7f9e0..df3bc94 100644 --- a/Html/app.py +++ b/Html/app.py @@ -1,497 +1,20 @@ -from functools import wraps -from flask import Flask, redirect, session, request, jsonify, render_template, send_from_directory, url_for -from flask_cors import CORS -from flask_socketio import SocketIO, join_room, emit, Namespace -import markdown -import os -import uuid -import shutil -import sys -import json -parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, parent_dir) -student_workspace_root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../study')) +# app.py 0.1.1 rise to factory mode +from bootstrap import bootstrap_paths +bootstrap_paths() +from apps import create_app +from apps.extensions import socketio + +# 工厂创建 app,工厂里已经做了:加载配置/注册蓝图/注册 namespaces +app = create_app() + +if __name__ == "__main__": + # 仅本地开发用;生产不要用 werkzeug 自带的开发服务器 + socketio.run( + app, + host="0.0.0.0", + port=5551, + debug=True, # 开发期打开 + allow_unsafe_werkzeug=True # 避免 dev server 的安全限制提示 + ) -current_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '.')) -sys.path.insert(0, current_dir) -from AlgoriAgent.projects.algoriAgent.agent_manager import AgentManager - - -import configparser - -from db.user_list import UserList -from db.user import User, load_user_from_json, create_user_json -from db.course_list import CourseList -from db.course import Course, load_course_from_json -GLOBAL_CONFIG = configparser.ConfigParser() -GLOBAL_CONFIG.read('config.ini') -VSCODE_WEB_URL = GLOBAL_CONFIG['VSCODE_WEB']['url'] -USER_DATA_DIR = GLOBAL_CONFIG['USER_DATA']['dir'] -COURSE_DATA_DIR = GLOBAL_CONFIG['COURSE_DATA']['dir'] - -app = Flask(__name__) -socketio = SocketIO(app, cors_allowed_origins="*",ping_timeout=60, ping_interval=5) -# socketio = SocketIO(app, cors_allowed_origins="http://localhost:9888") # 设置跨域支持 -import logging -app.secret_key = 'cakebaker' -app.logger.setLevel(logging.DEBUG) -# 配置 CORS -CORS(app, resources={r"/*": {"origins": VSCODE_WEB_URL},}, supports_credentials=True) - - -userid_recorder = {} # user_id&path -> session['user_id'] - -''' -Backboard -''' -from backboardManager import BackBoardManager, Backboard - -backboard_manager = BackBoardManager() -uuid2username = {} -username2uuid = {} - - - -# 定义命名空间:用于和 VSCode 插件交流 -class VSCodeNamespace(Namespace): - def on_login(self,data): - print("VSCode client connected") - print(data) - dataconfig = data['config'] - user_id = dataconfig.get('user_id') - path = dataconfig.get('path') - course_id = dataconfig.get('course_id') - lesson_id = dataconfig.get('chapter_id') - print(f"User {user_id} connected with path: {path}") - useruuid = username2uuid[user_id] - join_room(useruuid, namespace='/vscode') - if backboard_manager.get_backboard(useruuid) == None: - backboard_manager.add_backboard(useruuid,user_id, course_id, lesson_id, path) - - - def on_message(self, data): - print(f"Received from VSCode client: {data}") - dataconfig = data['config'] - realtime_response(dataconfig, data) - # emit('response', {'message': 'Config data received and connection established'}) - def on_disconnect(self, data): - print("VSCode client disconnected") - print("Disconnect reason:"+str(data)) - - -def realtime_response(config, realtime_action): - user_id = config['user_id'] - folder_path = config['path'] - useruuid = username2uuid[user_id] - bb = backboard_manager.get_backboard(useruuid) - assert type(bb) == Backboard - - bb.add_history(realtime_action) - - if realtime_action['type'] == 'workspaceFolders': - bb.file_tree = realtime_action['fileTree'] - - if realtime_action['type'] == 'activeFile': - file_path = realtime_action['filePath'] - assert type(file_path) == str - with open(f"{file_path}") as f: - bb.active_file_content = f.read() - - if realtime_action['type'] == 'paste': - file_path = realtime_action['filePath'] - assert type(file_path) == str - bb.pasted_file_path = file_path - bb.pasted_content = realtime_action['content'] - bb.active_file_path = file_path - with open(f"{file_path}") as f: - bb.active_file_content = f.read() - - - if realtime_action['type'] == 'fileEdit': - file_path = realtime_action['filePath'] - assert type(file_path) == str - bb.active_file_path = file_path - with open(f"{file_path}") as f: - bb.active_file_content = f.read() - - - - - print("config"+str(config)) - print("realtime_action"+str(realtime_action)) - pass - - -''' -Agent and Chat -''' -agent_manager = AgentManager(app = app, socketio = socketio) -user_threads = {} -import threading -from concurrent.futures import ThreadPoolExecutor, as_completed - -# 初始化线程池 -executor = ThreadPoolExecutor(max_workers=10) -class AgentNamespace(Namespace): - def on_login(self, data): - data = json.loads(data) - user = data['username'] - course_id = data['course_id'] - chapter_id = data['chapter_id'] - user_uuid = username2uuid[user] - session['user_id'] = user_uuid - print(f'User connected with session user_id: {user_uuid}') - # 从markdown、markdown_prompts、score_prompts中各读取数据folder.md并进行new_agent - with open(f'books/markdown/{course_id}/{chapter_id}.md','r',encoding='UTF-8') as fmd,\ - open(f'books/markdown_prompts/{course_id}/{chapter_id}.md','r',encoding='UTF-8')as fmdp,\ - open(f'books/score_prompts/{course_id}/{chapter_id}.md','r',encoding='UTF-8') as fsp: - markdown = fmd.read() - markdown_prompts = fmdp.read() - score_prompts = fsp.read() - user_uuid, agent = agent_manager.new_agent(course_id, chapter_id, markdown, markdown_prompts, score_prompts, id=user_uuid, - root_path=f'../../study/{user}/{course_id}/{chapter_id}') - print(user_uuid) - join_room(user_uuid, namespace='/agent') # 将该用户加入以user_id为名的room - backboard_manager.add_backboard(user_uuid, user, course_id, chapter_id, root_path=f'../../study/{user}/{course_id}/{chapter_id}') - - def on_language(self, language): - id = session.get('user_id') - agent_manager.change_language(id, language) - - def on_message(self, data): - print(f"Message from client: {data}") - id = session.get('user_id') - if (type(data)==str): - data = json.loads(data) - print(id) - if data['type'] == 'text': - res = agent_manager.invoke(id, data['data'], backboard_manager.get_backboard(id).get_info_prompt()) - print('=*='*20) - print(res.content) - reply = f"{res.content['speak']}" - with app.app_context(): - emit('message',reply, room=id, namespace='/agent') - emit('request_function',res.content['function'], room=id, namespace='/agent') - - if data['type'] == 'function': - agent_manager.function_call(id, data['data']) - - def on_initiative(self,data): - print("User active function call") - user_id = session['user_id'] - if data['name'] == 'sample_judge': - agent_manager.sample_judge(user_id, backboard_manager.get_backboard(user_id)) - if data['name'] == 'judge': - agent_manager.judge(user_id, backboard_manager.get_backboard(user_id)) - - - - def on_disconnect(self,data): - print("VSCode client disconnected") - print("Disconnect reason:"+str(data)) - - - -''' -Markdown to HTML -''' -# 配置文件路径 -MARKDOWN_DIR = 'books/markdown' # 存放 markdown 文件的文件夹 -STATIC_DIR = 'static' # 存放生成的 HTML 和图片资源的文件夹 -IMAGE_DIR = 'image' # 图片资源相对于 markdown 的位置 - - - -@app.route('/--markdown', methods=['GET']) -def convert_md(course_id, filename): - md_file_path = os.path.join(MARKDOWN_DIR,course_id, f'{filename}.md') - - if not os.path.exists(md_file_path): - return jsonify({'error': 'Markdown file not found'}), 404 - - # 读取 markdown 文件内容 - with open(md_file_path, 'r', encoding='utf-8') as file: - md_content = file.read() - - # 将 markdown 转换为 HTML - html_content = markdown.markdown(md_content) - # 插入 CSS 样式,限制图片最大宽度为 100%,高度自动调整 - html_with_styles = f""" - - - - - - - {html_content} - - - """ - # 保存 HTML 文件到 static 文件夹 - html_output_path = os.path.join(STATIC_DIR, f'{filename}.html') - with open(html_output_path, 'w', encoding='utf-8') as html_file: - html_file.write(html_with_styles) - - # 处理图片资源,将 image/xxx 文件夹拷贝到 static/image/xxx - image_source_dir = os.path.join(MARKDOWN_DIR,course_id, IMAGE_DIR, filename) - image_target_dir = os.path.join(STATIC_DIR, IMAGE_DIR, filename) - print(f"Copying image resources from {image_source_dir} to {image_target_dir}") - if os.path.exists(image_source_dir): - # 确保 static/image/xxx 目录存在 - if not os.path.exists(image_target_dir): - os.makedirs(image_target_dir) - - # 拷贝图片资源 - for image_file in os.listdir(image_source_dir): - full_image_file_path = os.path.join(image_source_dir, image_file) - if os.path.isfile(full_image_file_path): - shutil.copy(full_image_file_path, image_target_dir) - - return jsonify({'html_url': f'/static/{filename}.html'}) - -# 静态文件(HTML 和图片资源)的访问 -@app.route('/static/') -def serve_static(filename): - return send_from_directory(STATIC_DIR, filename) - - -''' -Vscode -''' -@app.route('/desktop///') -def desktop(user_id, course_id, chapter_id): - if 'user_id' not in session: - # return redirect(url_for('login')) - session['user_id'] = 'user_' + str(uuid.uuid4()) - print("user "+ user_id + "uuid is"+ session['user_id']) - username2uuid[user_id] = session['user_id'] - uuid2username[session['user_id']] = user_id - userid_recorder[user_id+'&'+course_id] = session['user_id'] - - # 在学习目录下创建一个名为 user_id_path 的文件夹 - path_dir = os.path.join(student_workspace_root_path, user_id, course_id,chapter_id) - os.makedirs(path_dir, exist_ok=True) - # 在此文件夹内部创建一个.config文件,并写入 user_id=user_id\n path=path - config_path = os.path.join(path_dir, '.config') - if GLOBAL_CONFIG['VSCODE_WEB_PATH']['is_wsl']: # 如果是在 WSL 中运行,则将路径转换为 WSL 路径 - path_dir = path_dir.replace('\\', '/') - path_dir = path_dir.replace(GLOBAL_CONFIG['VSCODE_WEB_PATH']['windows_path'] , GLOBAL_CONFIG['VSCODE_WEB_PATH']['wsl_path']) - with open(config_path, 'w', encoding='utf-8') as f: - tmpd = {'user_id': user_id, 'course_id': course_id, 'chapter_id': chapter_id, 'path': path_dir} - json.dump(tmpd, f) - print('---------------------') - print(config_path) - return render_template('desktop.html', vscode_web_url=VSCODE_WEB_URL, user_id=user_id, course_id=course_id, chapter_id=chapter_id, workspace_path=path_dir) - -@app.route('/desktop_nouser//') -def desktop_nouser(course_id, chapter_id): - if 'user_id' not in session: - return redirect(url_for('login')) - user_id = session['user_id'] - username = uuid2username[user_id] - return redirect(url_for('desktop', user_id=username, course_id=course_id, chapter_id=chapter_id)) - -@app.route('/vscode_data', methods=['POST']) -def vscode_data(): - data = request.json - config = data['config'] - realtime_response(config,data) - print(f"Received data from VSCode: {data}") - return jsonify({"status": "success", "received": data}) - -@app.route('/logout') -def logout(): - session.pop('user_id', None) - return redirect(url_for('login')) - -@app.route('/get_session') -def get_session(): - user_session = session.get('user_id', 'default_session') - return jsonify({"session": user_session}) - - - -''' -Login - -''' -users_list = UserList() -course_list = CourseList() - -def require_teacher(func): - @wraps(func) - def wrapper(*args, **kwargs): - if 'user_id' not in session or session['user_id'] not in uuid2username: - return redirect(url_for('login')) - user_id = session['user_id'] - username = uuid2username[user_id] - if users_list.get_user_is_teacher(username) is None or users_list.get_user_is_teacher(username) == False: - return redirect(url_for('login')) - return func(*args, **kwargs) - return wrapper - -def require_user(func): - @wraps(func) - def wrapper(*args, **kwargs): - if 'user_id' not in session or session['user_id'] not in uuid2username: - return redirect(url_for('login')) - return func(*args, **kwargs) - return wrapper - -@app.route('/register') -def register(): - return render_template('register.html') - -@app.route('/register_teacher') -def register_teacher(): - return render_template('register_teacher.html') - -@app.route('/register_teacher_post', methods=['POST']) -def register_teacher_post(): - data = request.get_json() - username = data.get('username') - password = data.get('password') - if users_list.has_user(username) is not None: - users_list.add_user(username, password, teacher=True) - create_user_json(username, USER_DATA_DIR) - return jsonify({'success': True, 'message': '注册成功'}) - else: return jsonify({'success': False, 'message': '用户已存在,请更换用户名'}) - -@app.route('/register_post', methods=['POST']) -def register_post(): - data = request.get_json() - username = data.get('username') - password = data.get('password') - if users_list.has_user(username) is not None: - users_list.add_user(username, password) - create_user_json(username, USER_DATA_DIR) - return jsonify({'success': True, 'message': '注册成功'}) - else: return jsonify({'success': False, 'message': '用户已存在,请更换用户名'}) - - -@app.route('/login') -def login(): - return render_template('login.html') - if 'user_id' not in session: - return render_template('login.html') - else: - return redirect(url_for('/')) - -@app.route('/login_post', methods=['POST']) -def login_post(): - # 获取请求的 JSON 数据 - data = request.get_json() - - username = data.get('username') - password = data.get('password') - - pswd = users_list.get_user_pswd(username) - - if pswd is None: - return jsonify({'success': False, 'message': '用户名或密码错误'}) - if pswd != password: - return jsonify({'success': False, 'message': '用户名或密码错误'}) - - session['user_id'] = 'user_' + str(uuid.uuid4()) - print("user "+ username + "uuid is"+ session['user_id']) - username2uuid[username] = session['user_id'] - uuid2username[session['user_id']] = username - return jsonify({'success': True, 'message': '登录成功'}) - -@app.route('/login_teacher_post', methods=['POST']) -def login_teacher_post(): - data = request.get_json() - username = data.get('username') - password = data.get('password') - pswd = users_list.get_user_pswd(username) - is_teacher = users_list.get_user_is_teacher(username) - if is_teacher is None or is_teacher == False: - return jsonify({'success': False, 'message': '用户名不存在或非教师账号'}) - if pswd is None: - return jsonify({'success': False, 'message': '用户名或密码错误'}) - if pswd != password: - return jsonify({'success': False, 'message': '用户名或密码错误'}) - session['user_id'] = 'user_' + str(uuid.uuid4()) - print("user "+ username + "uuid is"+ session['user_id']) - username2uuid[username] = session['user_id'] - uuid2username[session['user_id']] = username - return jsonify({'success': True, 'message': '登录成功'}) - -@app.route('/teacherboard') -@require_teacher -def teacherboard(): - return render_template('teacherboard.html') - - - -user_id2UserClass = {} -''' -DashBoard -''' -@app.route('/dashboard') -@require_user -def dashboard(): - user_id = session['user_id'] - username = uuid2username[user_id] - if (user_id not in user_id2UserClass): - user_id2UserClass[user_id] = load_user_from_json(username, user_data_dir = USER_DATA_DIR) - user_course_data = [] - for course_id in user_id2UserClass[user_id].select_course: - course_brief_info = course_list.get_course_brief_info(course_id, load_course_from_json(course_id, course_data_dir = COURSE_DATA_DIR)) - user_course_data.append(course_brief_info) - return render_template('dashboard.html',user_data = user_id2UserClass[user_id].to_json_without_dialog(), user_course_data = json.dumps(user_course_data)) - -''' -Course -''' - -@app.route('/course/') -@require_user -def course(course_id): - c = load_course_from_json(course_id, course_data_dir = COURSE_DATA_DIR) - return render_template('course.html', course_id=course_id, course_data=c) - -@app.route('/select_course', methods=['POST']) -@require_user -def select_course(): - user = user_id2UserClass[session['user_id']] - data = request.get_json() - course_id = data.get('course_id') - if course_id: - user.select_new_course(course_id, load_course_from_json(course_id, COURSE_DATA_DIR)) - return jsonify({'success': True, 'message': '课程选择成功'}) - - -@app.route('/') -@require_user -def home_index(): - selected_courses=[] - if ('user_id' in session): - if (session['user_id'] not in user_id2UserClass): return redirect(url_for('login')) - user = user_id2UserClass[session['user_id']] - for course_id in user.select_course: - selected_courses.append(course_id) - - return render_template('index.html', courses_data = course_list, selected_courses =selected_courses) - -# 一些app辅助函数,主要提供给Agent与数据库的交互能力 - -class MyFunction: - def save_chapter_memory(self, id, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal): - print("-=-=-=-=-=-=-=-") - print(id, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal) - username = uuid2username[id] - u = load_user_from_json(username, user_data_dir = USER_DATA_DIR) - u.save_chapter_memory(course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal) - -app.my_function = MyFunction() -if __name__ == '__main__': - socketio.on_namespace(VSCodeNamespace('/vscode')) - socketio.on_namespace(AgentNamespace('/agent')) - socketio.run(app, host='0.0.0.0', port=5551, debug=False,allow_unsafe_werkzeug=True) diff --git a/Html/app_back.py b/Html/app_back.py new file mode 100644 index 0000000..246853c --- /dev/null +++ b/Html/app_back.py @@ -0,0 +1,480 @@ +from bootstrap import bootstrap_paths +bootstrap_paths() +from functools import wraps +from flask import Flask, redirect, session, request, jsonify, render_template, send_from_directory, url_for +from flask_cors import CORS +from flask_socketio import SocketIO, join_room, emit, Namespace +import markdown +import os +import uuid +import shutil +import sys +import json +from apps.auth.decorators import require_role +parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +sys.path.insert(0, parent_dir) +student_workspace_root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../study')) +current_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '.')) +sys.path.insert(0, current_dir) +from AlgoriAgent.projects.algoriAgent.agent_manager import AgentManager + + +import configparser + +from db.user_list import UserList +from db.user import User, load_user_from_json, create_user_json +from db.course_list import CourseList +from db.course import Course, load_course_from_json +GLOBAL_CONFIG = configparser.ConfigParser() +GLOBAL_CONFIG.read('config.ini') +VSCODE_WEB_URL = GLOBAL_CONFIG['VSCODE_WEB']['url'] +USER_DATA_DIR = GLOBAL_CONFIG['USER_DATA']['dir'] +COURSE_DATA_DIR = GLOBAL_CONFIG['COURSE_DATA']['dir'] + +app = Flask(__name__) +socketio = SocketIO(app, cors_allowed_origins="*",ping_timeout=60, ping_interval=5) +# socketio = SocketIO(app, cors_allowed_origins="http://localhost:9888") # 设置跨域支持 +import logging +app.secret_key = 'cakebaker' +app.logger.setLevel(logging.DEBUG) +# 配置 CORS +CORS(app, resources={r"/*": {"origins": VSCODE_WEB_URL},}, supports_credentials=True) + + +userid_recorder = {} # user_id&path -> session['user_id'] + +''' +Backboard +''' +from backboardManager import BackBoardManager, Backboard + +backboard_manager = BackBoardManager() +uuid2username = {} +username2uuid = {} + + + +# 定义命名空间:用于和 VSCode 插件交流 +class VSCodeNamespace(Namespace): + def on_login(self,data): + print("VSCode client connected") + print(data) + dataconfig = data['config'] + user_id = dataconfig.get('user_id') + path = dataconfig.get('path') + course_id = dataconfig.get('course_id') + lesson_id = dataconfig.get('chapter_id') + print(f"User {user_id} connected with path: {path}") + useruuid = username2uuid[user_id] + join_room(useruuid, namespace='/vscode') + if backboard_manager.get_backboard(useruuid) == None: + backboard_manager.add_backboard(useruuid,user_id, course_id, lesson_id, path) + + + def on_message(self, data): + print(f"Received from VSCode client: {data}") + dataconfig = data['config'] + realtime_response(dataconfig, data) + # emit('response', {'message': 'Config data received and connection established'}) + def on_disconnect(self, data): + print("VSCode client disconnected") + print("Disconnect reason:"+str(data)) + + +def realtime_response(config, realtime_action): + user_id = config['user_id'] + folder_path = config['path'] + useruuid = username2uuid[user_id] + bb = backboard_manager.get_backboard(useruuid) + assert type(bb) == Backboard + + bb.add_history(realtime_action) + + if realtime_action['type'] == 'workspaceFolders': + bb.file_tree = realtime_action['fileTree'] + + if realtime_action['type'] == 'activeFile': + file_path = realtime_action['filePath'] + assert type(file_path) == str + with open(f"{file_path}") as f: + bb.active_file_content = f.read() + + if realtime_action['type'] == 'paste': + file_path = realtime_action['filePath'] + assert type(file_path) == str + bb.pasted_file_path = file_path + bb.pasted_content = realtime_action['content'] + bb.active_file_path = file_path + with open(f"{file_path}") as f: + bb.active_file_content = f.read() + + + if realtime_action['type'] == 'fileEdit': + file_path = realtime_action['filePath'] + assert type(file_path) == str + bb.active_file_path = file_path + with open(f"{file_path}") as f: + bb.active_file_content = f.read() + + + + + print("config"+str(config)) + print("realtime_action"+str(realtime_action)) + pass + + +''' +Agent and Chat +''' +agent_manager = AgentManager(app = app, socketio = socketio) +user_threads = {} +import threading +from concurrent.futures import ThreadPoolExecutor, as_completed + +# 初始化线程池 +executor = ThreadPoolExecutor(max_workers=10) +class AgentNamespace(Namespace): + def on_login(self, data): + data = json.loads(data) + user = data['username'] + course_id = data['course_id'] + chapter_id = data['chapter_id'] + user_uuid = username2uuid[user] + session['user_id'] = user_uuid + print(f'User connected with session user_id: {user_uuid}') + # 从markdown、markdown_prompts、score_prompts中各读取数据folder.md并进行new_agent + with open(f'books/markdown/{course_id}/{chapter_id}.md','r',encoding='UTF-8') as fmd,\ + open(f'books/markdown_prompts/{course_id}/{chapter_id}.md','r',encoding='UTF-8')as fmdp,\ + open(f'books/score_prompts/{course_id}/{chapter_id}.md','r',encoding='UTF-8') as fsp: + markdown = fmd.read() + markdown_prompts = fmdp.read() + score_prompts = fsp.read() + user_uuid, agent = agent_manager.new_agent(course_id, chapter_id, markdown, markdown_prompts, score_prompts, id=user_uuid, + root_path=f'../../study/{user}/{course_id}/{chapter_id}') + print(user_uuid) + join_room(user_uuid, namespace='/agent') # 将该用户加入以user_id为名的room + backboard_manager.add_backboard(user_uuid, user, course_id, chapter_id, root_path=f'../../study/{user}/{course_id}/{chapter_id}') + + def on_language(self, language): + id = session.get('user_id') + agent_manager.change_language(id, language) + + def on_message(self, data): + print(f"Message from client: {data}") + id = session.get('user_id') + if (type(data)==str): + data = json.loads(data) + print(id) + if data['type'] == 'text': + res = agent_manager.invoke(id, data['data'], backboard_manager.get_backboard(id).get_info_prompt()) + print('=*='*20) + print(res.content) + reply = f"{res.content['speak']}" + with app.app_context(): + emit('message',reply, room=id, namespace='/agent') + emit('request_function',res.content['function'], room=id, namespace='/agent') + + if data['type'] == 'function': + agent_manager.function_call(id, data['data']) + + def on_initiative(self,data): + print("User active function call") + user_id = session['user_id'] + if data['name'] == 'sample_judge': + agent_manager.sample_judge(user_id, backboard_manager.get_backboard(user_id)) + if data['name'] == 'judge': + agent_manager.judge(user_id, backboard_manager.get_backboard(user_id)) + + + + def on_disconnect(self,data): + print("VSCode client disconnected") + print("Disconnect reason:"+str(data)) + + + +''' +Markdown to HTML +''' +# 配置文件路径 +MARKDOWN_DIR = 'books/markdown' # 存放 markdown 文件的文件夹 +STATIC_DIR = 'static' # 存放生成的 HTML 和图片资源的文件夹 +IMAGE_DIR = 'image' # 图片资源相对于 markdown 的位置 + + + +@app.route('/--markdown', methods=['GET']) +def convert_md(course_id, filename): + md_file_path = os.path.join(MARKDOWN_DIR,course_id, f'{filename}.md') + + if not os.path.exists(md_file_path): + return jsonify({'error': 'Markdown file not found'}), 404 + + # 读取 markdown 文件内容 + with open(md_file_path, 'r', encoding='utf-8') as file: + md_content = file.read() + + # 将 markdown 转换为 HTML + html_content = markdown.markdown(md_content) + # 插入 CSS 样式,限制图片最大宽度为 100%,高度自动调整 + html_with_styles = f""" + + + + + + + {html_content} + + + """ + # 保存 HTML 文件到 static 文件夹 + html_output_path = os.path.join(STATIC_DIR, f'{filename}.html') + with open(html_output_path, 'w', encoding='utf-8') as html_file: + html_file.write(html_with_styles) + + # 处理图片资源,将 image/xxx 文件夹拷贝到 static/image/xxx + image_source_dir = os.path.join(MARKDOWN_DIR,course_id, IMAGE_DIR, filename) + image_target_dir = os.path.join(STATIC_DIR, IMAGE_DIR, filename) + print(f"Copying image resources from {image_source_dir} to {image_target_dir}") + if os.path.exists(image_source_dir): + # 确保 static/image/xxx 目录存在 + if not os.path.exists(image_target_dir): + os.makedirs(image_target_dir) + + # 拷贝图片资源 + for image_file in os.listdir(image_source_dir): + full_image_file_path = os.path.join(image_source_dir, image_file) + if os.path.isfile(full_image_file_path): + shutil.copy(full_image_file_path, image_target_dir) + + return jsonify({'html_url': f'/static/{filename}.html'}) + +# 静态文件(HTML 和图片资源)的访问 +@app.route('/static/') +def serve_static(filename): + return send_from_directory(STATIC_DIR, filename) + + +''' +Vscode +''' +@app.route('/desktop///') +def desktop(user_id, course_id, chapter_id): + if 'user_id' not in session: + # return redirect(url_for('login')) + session['user_id'] = 'user_' + str(uuid.uuid4()) + print("user "+ user_id + "uuid is"+ session['user_id']) + username2uuid[user_id] = session['user_id'] + uuid2username[session['user_id']] = user_id + userid_recorder[user_id+'&'+course_id] = session['user_id'] + + # 在学习目录下创建一个名为 user_id_path 的文件夹 + path_dir = os.path.join(student_workspace_root_path, user_id, course_id,chapter_id) + os.makedirs(path_dir, exist_ok=True) + # 在此文件夹内部创建一个.config文件,并写入 user_id=user_id\n path=path + config_path = os.path.join(path_dir, '.config') + if GLOBAL_CONFIG['VSCODE_WEB_PATH']['is_wsl']: # 如果是在 WSL 中运行,则将路径转换为 WSL 路径 + path_dir = path_dir.replace('\\', '/') + path_dir = path_dir.replace(GLOBAL_CONFIG['VSCODE_WEB_PATH']['windows_path'] , GLOBAL_CONFIG['VSCODE_WEB_PATH']['wsl_path']) + with open(config_path, 'w', encoding='utf-8') as f: + tmpd = {'user_id': user_id, 'course_id': course_id, 'chapter_id': chapter_id, 'path': path_dir} + json.dump(tmpd, f) + print('---------------------') + print(config_path) + return render_template('desktop.html', vscode_web_url=VSCODE_WEB_URL, user_id=user_id, course_id=course_id, chapter_id=chapter_id, workspace_path=path_dir) + +@app.route('/desktop_nouser//') +def desktop_nouser(course_id, chapter_id): + if 'user_id' not in session: + return redirect(url_for('login')) + user_id = session['user_id'] + username = uuid2username[user_id] + return redirect(url_for('desktop', user_id=username, course_id=course_id, chapter_id=chapter_id)) + +@app.route('/vscode_data', methods=['POST']) +def vscode_data(): + data = request.json + config = data['config'] + realtime_response(config,data) + print(f"Received data from VSCode: {data}") + return jsonify({"status": "success", "received": data}) + + + +''' +Login + +''' +users_list = UserList() +course_list = CourseList() + + +@app.route('/register') +def register(): + return render_template('register.html') + +@app.route('/register_teacher') +def register_teacher(): + return render_template('register_teacher.html') + +@app.route('/register_teacher_post', methods=['POST']) +def register_teacher_post(): + data = request.get_json() + username = data.get('username') + password = data.get('password') + if users_list.has_user(username) is not None: + users_list.add_user(username, password, teacher=True) + create_user_json(username, USER_DATA_DIR) + return jsonify({'success': True, 'message': '注册成功'}) + else: return jsonify({'success': False, 'message': '用户已存在,请更换用户名'}) + +@app.route('/register_post', methods=['POST']) +def register_post(): + data = request.get_json() + username = data.get('username') + password = data.get('password') + if users_list.has_user(username) is not None: + users_list.add_user(username, password) + create_user_json(username, USER_DATA_DIR) + return jsonify({'success': True, 'message': '注册成功'}) + else: return jsonify({'success': False, 'message': '用户已存在,请更换用户名'}) + + +@app.route('/login') +def login(): + return render_template('login.html') + if 'user_id' not in session: + return render_template('login.html') + else: + return redirect(url_for('/')) + +@app.route('/login_post', methods=['POST']) +def login_post(): + # 获取请求的 JSON 数据 + data = request.get_json() + + username = data.get('username') + password = data.get('password') + + pswd = users_list.get_user_pswd(username) + + if pswd is None: + return jsonify({'success': False, 'message': '用户名或密码错误'}) + if pswd != password: + return jsonify({'success': False, 'message': '用户名或密码错误'}) + + session['user_id'] = 'user_' + str(uuid.uuid4()) + print("user "+ username + "uuid is"+ session['user_id']) + username2uuid[username] = session['user_id'] + uuid2username[session['user_id']] = username + return jsonify({'success': True, 'message': '登录成功'}) + +@app.route('/login_teacher_post', methods=['POST']) +def login_teacher_post(): + data = request.get_json() + username = data.get('username') + password = data.get('password') + pswd = users_list.get_user_pswd(username) + is_teacher = users_list.get_user_is_teacher(username) + if is_teacher is None or is_teacher == False: + return jsonify({'success': False, 'message': '用户名不存在或非教师账号'}) + if pswd is None: + return jsonify({'success': False, 'message': '用户名或密码错误'}) + if pswd != password: + return jsonify({'success': False, 'message': '用户名或密码错误'}) + session['user_id'] = 'user_' + str(uuid.uuid4()) + print("user "+ username + "uuid is"+ session['user_id']) + username2uuid[username] = session['user_id'] + uuid2username[session['user_id']] = username + return jsonify({'success': True, 'message': '登录成功'}) + +@app.route('/teacherboard') +@require_role(roles="teacher") +def teacherboard(): + return render_template('teacherboard.html') + + +@app.route('/logout') +def logout(): + session.pop('user_id', None) + return redirect(url_for('login')) + +@app.route('/get_session') +def get_session(): + user_session = session.get('user_id', 'default_session') + return jsonify({"session": user_session}) + + +''' +DashBoard +''' +user_id2UserClass = {} +@app.route('/dashboard') +@require_role +def dashboard(): + user_id = session['user_id'] + username = uuid2username[user_id] + if (user_id not in user_id2UserClass): + user_id2UserClass[user_id] = load_user_from_json(username, user_data_dir = USER_DATA_DIR) + user_course_data = [] + for course_id in user_id2UserClass[user_id].select_course: + course_brief_info = course_list.get_course_brief_info(course_id, load_course_from_json(course_id, course_data_dir = COURSE_DATA_DIR)) + user_course_data.append(course_brief_info) + return render_template('dashboard.html',user_data = user_id2UserClass[user_id].to_json_without_dialog(), user_course_data = json.dumps(user_course_data)) + +''' +Course +''' + +@app.route('/course/') +@require_role +def course(course_id): + c = load_course_from_json(course_id, course_data_dir = COURSE_DATA_DIR) + return render_template('course.html', course_id=course_id, course_data=c) + +@app.route('/select_course', methods=['POST']) +@require_role +def select_course(): + user = user_id2UserClass[session['user_id']] + data = request.get_json() + course_id = data.get('course_id') + if course_id: + user.select_new_course(course_id, load_course_from_json(course_id, COURSE_DATA_DIR)) + return jsonify({'success': True, 'message': '课程选择成功'}) + + +@app.route('/') +@require_role +def home_index(): + selected_courses=[] + if ('user_id' in session): + if (session['user_id'] not in user_id2UserClass): return redirect(url_for('login')) + user = user_id2UserClass[session['user_id']] + for course_id in user.select_course: + selected_courses.append(course_id) + + return render_template('index.html', courses_data = course_list, selected_courses =selected_courses) + +# 一些app辅助函数,主要提供给Agent与数据库的交互能力 + +class MyFunction: + def save_chapter_memory(self, id, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal): + print("-=-=-=-=-=-=-=-") + print(id, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal) + username = uuid2username[id] + u = load_user_from_json(username, user_data_dir = USER_DATA_DIR) + u.save_chapter_memory(course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal) + +app.my_function = MyFunction() +if __name__ == '__main__': + socketio.on_namespace(VSCodeNamespace('/vscode')) + socketio.on_namespace(AgentNamespace('/agent')) + socketio.run(app, host='0.0.0.0', port=5551, debug=False,allow_unsafe_werkzeug=True) + diff --git a/Html/apps/__init__.py b/Html/apps/__init__.py new file mode 100644 index 0000000..00ed968 --- /dev/null +++ b/Html/apps/__init__.py @@ -0,0 +1,34 @@ +import logging +from flask import Flask +from .config import Config +from .extensions import init_extensions, register_namespaces +from .views import register_blueprints + + + +def create_app(config_object: type = Config) -> Flask: + app = Flask(__name__, template_folder="templates", static_folder="static") + app.config.from_object(config_object) + + # secret_key 从 config 里来 + app.secret_key = app.config["SECRET_KEY"] + + # logging + app.logger.setLevel(logging.DEBUG) + + + register_namespaces(app) + + # 1) 初始化第三方扩展(db、cache、jwt、cors 等) + init_extensions(app) + + + # 2) 注册蓝图(把子文件暴露的蓝图统一挂到 app 上) + register_blueprints(app) + + # 3) 其他钩子/命令/错误处理 + @app.route("/healthz") + def healthz(): + return {"status": "ok"} + + return app diff --git a/Html/apps/auth/decorators.py b/Html/apps/auth/decorators.py new file mode 100644 index 0000000..85e85e8 --- /dev/null +++ b/Html/apps/auth/decorators.py @@ -0,0 +1,98 @@ +# myapp/auth/decorators.py +from functools import wraps +from typing import Callable, Optional, Union, Iterable +from flask import session, redirect, url_for, current_app + +RoleType = Union[str, Iterable[str]] +RoleChecker = Callable[[str, RoleType], bool] # (username, required_roles) -> bool + +def _get_deps(): + """从 app 注入点拿依赖,避免循环导入。""" + app = current_app + uuid2username = ( + app.config.get("UUID2USERNAME") + or app.extensions.get("uuid2username") + ) + users_list = ( + app.config.get("USERS_LIST") + or app.extensions.get("users_list") + ) + role_checker: Optional[RoleChecker] = ( + app.config.get("ROLE_CHECKER") + or app.extensions.get("role_checker") + ) + return uuid2username, users_list, role_checker + +def require_role( + view=None, + *, + # 未登录时跳转到哪个 endpoint + login_endpoint: str = "auth.login", + # 角色要求:None/空 -> 仅需登录;"teacher" 或 ["teacher", "admin"] -> 需具备其中之一 + roles: Optional[RoleType] = None, + # 可选:自定义角色校验函数(优先级高于内置 users_list 适配) + checker: Optional[RoleChecker] = None, +): + """ + 用法: + 1) 仅需登录(等价于 require_user): + @require_role + 2) 需要 teacher 角色(等价于 require_teacher): + @require_role(roles="teacher") + 3) 需要多个角色之一: + @require_role(roles=["teacher", "admin"]) + 4) 自定义校验器(例如 RBAC/权限码): + @require_role(roles="teacher", checker=my_checker) + + 也支持指定登录端点: + @require_role(login_endpoint="login") + """ + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + uuid2username, users_list, injected_checker = _get_deps() + + user_id = session.get("user_id") + if not user_id or uuid2username is None or user_id not in uuid2username: + return redirect(url_for(login_endpoint)) + + # 仅需登录 + if not roles: + return func(*args, **kwargs) + + username = uuid2username[user_id] + + # 选择校验器优先级:参数 checker > app 注入的 role_checker > 内置 users_list 适配 + effective_checker = checker or injected_checker + + # 内置对 users_list 的向后兼容(你的现有接口) + if effective_checker is None: + def builtin_checker(u: str, required: RoleType) -> bool: + # 支持 teacher 场景(原先的 get_user_is_teacher) + def has_teacher(u_: str) -> bool: + if users_list is None: + return False + getter = getattr(users_list, "get_user_is_teacher", None) + return bool(getter and getter(u_) is True) + + def match_one(required_role: str) -> bool: + if required_role == "teacher": + return has_teacher(u) + # 你也可以在此扩展更多内置角色判断 + # 比如 get_user_is_admin / get_user_roles 等 + # 未知角色默认 False + return False + + if isinstance(required, str): + return match_one(required) + return any(match_one(r) for r in required) + + effective_checker = builtin_checker + + ok = effective_checker(username, roles) + if not ok: + return redirect(url_for(login_endpoint)) + + return func(*args, **kwargs) + return wrapper + return decorator(view) if callable(view) else decorator diff --git a/Html/apps/bootstrap.py b/Html/apps/bootstrap.py new file mode 100644 index 0000000..17dd0e5 --- /dev/null +++ b/Html/apps/bootstrap.py @@ -0,0 +1 @@ +# Html/apps/bootstrap.py diff --git a/Html/apps/config.py b/Html/apps/config.py new file mode 100644 index 0000000..bb285e1 --- /dev/null +++ b/Html/apps/config.py @@ -0,0 +1,37 @@ +# app/config.py +import configparser +import os + +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),'..')) + +GLOBAL_CONFIG = configparser.ConfigParser() +GLOBAL_CONFIG.read(os.getenv("APP_CONFIG_FILE", "config.ini")) + +class Config: + SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret") + SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///dev.db") + SQLALCHEMY_TRACK_MODIFICATIONS = False + SECRET_KEY = os.getenv("SECRET_KEY", "cakebaker") # 覆盖优先 + VSCODE_WEB_URL = GLOBAL_CONFIG['VSCODE_WEB']['url'] + USER_DATA_DIR = GLOBAL_CONFIG['USER_DATA']['dir'] + COURSE_DATA_DIR = GLOBAL_CONFIG['COURSE_DATA']['dir'] + + # socketio / cors 配置也可以放这里 + SOCKETIO_PING_TIMEOUT = 60 + SOCKETIO_PING_INTERVAL = 5 + MARKDOWN_DIR = os.path.join(BASE_DIR, "books", "markdown") + STATIC_DIR = os.path.join(BASE_DIR, "static") + IMAGE_DIR = "image" + + # 你的学生工作区根目录(如无就放到项目 data 目录) + STUDENT_WORKSPACE_ROOT = os.getenv( + "STUDENT_WORKSPACE_ROOT", + os.path.join(BASE_DIR, "data", "workspaces") + ) + + # WSL 相关 + VSCODE_WEB_PATH = { + "is_wsl": GLOBAL_CONFIG.getboolean("VSCODE_WEB_PATH", "is_wsl", fallback=False), + "windows_path": GLOBAL_CONFIG.get("VSCODE_WEB_PATH", "windows_path", fallback=""), + "wsl_path": GLOBAL_CONFIG.get("VSCODE_WEB_PATH", "wsl_path", fallback=""), + } diff --git a/Html/apps/extensions.py b/Html/apps/extensions.py new file mode 100644 index 0000000..a98cfdc --- /dev/null +++ b/Html/apps/extensions.py @@ -0,0 +1,59 @@ +import os, sys +from flask_sqlalchemy import SQLAlchemy +from flask_socketio import SocketIO +from flask_cors import CORS +from db.user_list import UserList +from db.course_list import CourseList +from .services.my_function import MyFunction + +db = SQLAlchemy() + +socketio = SocketIO(cors_allowed_origins="*") # 不直接传 app +cors = CORS() +# ===== 你的全局对象 ===== +# Backboard +from backboardManager import BackBoardManager # 你已有的类 +backboard_manager = BackBoardManager() + +# 用户映射(全局内存结构) +userid_recorder = {} # 你代码里有使用 +users_list = UserList() +uuid2username = {} # 你的映射,或换成数据库访问层 +username2uuid = {} +course_list = CourseList() +user_id2UserClass = {} + +from AlgoriAgent.projects.algoriAgent.agent_manager import AgentManager +agent_manager = AgentManager() +def init_extensions(app): + db.init_app(app) + # 第三方 + socketio.init_app( + app, + cors_allowed_origins='*',#app.config["VSCODE_WEB_URL"], + ping_timeout=app.config["SOCKETIO_PING_TIMEOUT"], + ping_interval=app.config["SOCKETIO_PING_INTERVAL"], + ) + cors.init_app( + app, + resources={r"/*": {"origins": app.config["VSCODE_WEB_URL"]}}, + supports_credentials=True, + ) + + # 把自定义对象挂到 app.extensions,供各处通过 current_app 访问 + app.extensions["users_list"] = users_list + app.extensions["course_list"] = course_list + + app.extensions["backboard_manager"] = backboard_manager + app.extensions["uuid2username"] = uuid2username + app.extensions["username2uuid"] = username2uuid + app.extensions["userid_recorder"] = userid_recorder + app.extensions["user_id2UserClass"] = user_id2UserClass + app.extensions["agent_manager"] = agent_manager + app.extensions["my_function"] = MyFunction() +def register_namespaces(app): + """把所有 Socket.IO namespaces 注册到 socketio""" + # 延迟导入以避免循环 + from .sockets.namespaces import VSCodeNamespace, AgentNamespace + socketio.on_namespace(VSCodeNamespace("/vscode")) + socketio.on_namespace(AgentNamespace("/agent")) \ No newline at end of file diff --git a/Html/image/readme/1728381936172.png b/Html/apps/image/readme/1728381936172.png similarity index 100% rename from Html/image/readme/1728381936172.png rename to Html/apps/image/readme/1728381936172.png diff --git a/Html/apps/services/auth_service.py b/Html/apps/services/auth_service.py new file mode 100644 index 0000000..823d4dd --- /dev/null +++ b/Html/apps/services/auth_service.py @@ -0,0 +1,46 @@ +# myapp/services/auth_service.py +import os, uuid, json +from flask import current_app, session + +def create_user_json(username: str): + user_dir = current_app.config["USER_DATA_DIR"] + os.makedirs(user_dir, exist_ok=True) + path = os.path.join(user_dir, f"{username}.json") + if not os.path.exists(path): + with open(path, "w", encoding="utf-8") as f: + json.dump({"username": username}, f, ensure_ascii=False) + +def register_user(username: str, password: str, *, teacher: bool=False): + users_list = current_app.extensions["users_list"] + # 你原逻辑似乎把 has_user 的判断反了:直觉上「不存在才允许新增」 + exists = users_list.has_user(username) + if exists: + return False, "用户已存在,请更换用户名" + users_list.add_user(username, password, teacher=teacher) + create_user_json(username) + return True, "注册成功" + +def login_user(username: str, password: str, *, require_teacher: bool=False): + users_list = current_app.extensions["users_list"] + pswd = users_list.get_user_pswd(username) + if pswd is None or pswd != password: + return False, "用户名或密码错误" + + if require_teacher: + is_teacher = users_list.get_user_is_teacher(username) + if not is_teacher: + return False, "用户名不存在或非教师账号" + + # 设置会话 + 关联映射 + user_uuid = "user_" + str(uuid.uuid4()) + session["user_id"] = user_uuid + + username2uuid = current_app.extensions["username2uuid"] + uuid2username = current_app.extensions["uuid2username"] + username2uuid[username] = user_uuid + uuid2username[user_uuid] = username + + return True, "登录成功" + +def logout_user(): + session.pop("user_id", None) diff --git a/Html/apps/services/backboard_service.py b/Html/apps/services/backboard_service.py new file mode 100644 index 0000000..8e758cf --- /dev/null +++ b/Html/apps/services/backboard_service.py @@ -0,0 +1,59 @@ +# myapp/services/backboard_service.py +import os +from flask import current_app +from backboardManager import Backboard # 你已有的类 + +def _to_wsl_path_if_needed(path: str) -> str: + cfg = current_app.config["VSCODE_WEB_PATH"] + if cfg.get("is_wsl") and cfg.get("windows_path") and cfg.get("wsl_path"): + return path.replace("\\", "/").replace(cfg["windows_path"], cfg["wsl_path"]) + return path + +def realtime_response(config: dict, realtime_action: dict) -> None: + """ + 业务:根据 VSCode 上报的动作,更新 Backboard(黑板)状态。 + """ + # 依赖从 app.extensions 取,避免循环导入 + username2uuid = current_app.extensions["username2uuid"] + backboard_manager = current_app.extensions["backboard_manager"] + + user_id = config["user_id"] + folder_path = config["path"] # 如需 WSL 转换可在生成 config 时处理 + useruuid = username2uuid[user_id] + bb = backboard_manager.get_backboard(useruuid) + assert isinstance(bb, Backboard) + + # 统一记历史 + bb.add_history(realtime_action) + + rtype = realtime_action.get("type") + + if rtype == "workspaceFolders": + bb.file_tree = realtime_action.get("fileTree") + + elif rtype == "activeFile": + file_path = realtime_action.get("filePath") + assert isinstance(file_path, str) + with open(file_path, "r", encoding="utf-8") as f: + bb.active_file_content = f.read() + bb.active_file_path = file_path + + elif rtype == "paste": + file_path = realtime_action.get("filePath") + assert isinstance(file_path, str) + bb.pasted_file_path = file_path + bb.pasted_content = realtime_action.get("content") + bb.active_file_path = file_path + with open(file_path, "r", encoding="utf-8") as f: + bb.active_file_content = f.read() + + elif rtype == "fileEdit": + file_path = realtime_action.get("filePath") + assert isinstance(file_path, str) + bb.active_file_path = file_path + with open(file_path, "r", encoding="utf-8") as f: + bb.active_file_content = f.read() + + # 如需调试输出,可在这里统一记录日志 + current_app.logger.debug("vscode config: %s", config) + current_app.logger.debug("vscode action: %s", realtime_action) diff --git a/Html/apps/services/course_service.py b/Html/apps/services/course_service.py new file mode 100644 index 0000000..fb4a179 --- /dev/null +++ b/Html/apps/services/course_service.py @@ -0,0 +1,20 @@ +# myapp/services/course_service.py +from flask import current_app + +# 假设你已有这两个 loader +from db.course import load_course_from_json + +def load_course(course_id: str): + return load_course_from_json( + course_id, course_data_dir=current_app.config["COURSE_DATA_DIR"] + ) + +def user_selected_course_briefs(user_obj): + """根据用户对象的选课列表,拼装课程简要信息数组""" + course_list = current_app.extensions["course_list"] + briefs = [] + for cid in getattr(user_obj, "select_course", []): + course_obj = load_course(cid) + brief = course_list.get_course_brief_info(cid, course_obj) + briefs.append(brief) + return briefs diff --git a/Html/apps/services/markdown_service.py b/Html/apps/services/markdown_service.py new file mode 100644 index 0000000..11bb982 --- /dev/null +++ b/Html/apps/services/markdown_service.py @@ -0,0 +1,40 @@ +import os, shutil, markdown + +def convert_markdown_to_html(md_file_path: str) -> str: + """读取 markdown 并转成 HTML 片段""" + with open(md_file_path, 'r', encoding='utf-8') as f: + md_content = f.read() + return markdown.markdown(md_content) + +def wrap_with_styles(html_content: str) -> str: + """加上 CSS 样式和外层 HTML""" + return f""" + + + + + + + {html_content} + + + """ + +def save_html(html_str: str, output_path: str) -> None: + os.makedirs(os.path.dirname(output_path), exist_ok=True) + with open(output_path, "w", encoding="utf-8") as f: + f.write(html_str) + +def copy_images(source_dir: str, target_dir: str) -> None: + if not os.path.exists(source_dir): + return + os.makedirs(target_dir, exist_ok=True) + for filename in os.listdir(source_dir): + src = os.path.join(source_dir, filename) + if os.path.isfile(src): + shutil.copy(src, target_dir) diff --git a/Html/apps/services/memory_service.py b/Html/apps/services/memory_service.py new file mode 100644 index 0000000..7a652ed --- /dev/null +++ b/Html/apps/services/memory_service.py @@ -0,0 +1,26 @@ +# myapp/services/memory_service.py +import os, json +from flask import current_app +from db.user import load_user_from_json # 按你项目实际替换 + +def save_chapter_memory_by_uuid( + user_uuid: str, + course_id: str, + lesson_id: str, + subchapter_title: str, + mem_list, + score, + is_rebuttal: bool, +): + uuid2username = current_app.extensions["uuid2username"] + user_map = current_app.extensions["user_id2UserClass"] # 可复用缓存 + user_data_dir = current_app.config["USER_DATA_DIR"] + + username = uuid2username[user_uuid] + # 若内存里已有用户对象就直接用;否则从 JSON 载入 + user_obj = user_map.get(user_uuid) or load_user_from_json(username, user_data_dir=user_data_dir) + user_map[user_uuid] = user_obj + + user_obj.save_chapter_memory( + course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal + ) diff --git a/Html/apps/services/my_function.py b/Html/apps/services/my_function.py new file mode 100644 index 0000000..0e0fb53 --- /dev/null +++ b/Html/apps/services/my_function.py @@ -0,0 +1,10 @@ +# myapp/services/my_function.py +from flask import current_app +from .memory_service import save_chapter_memory_by_uuid + +class MyFunction: + def save_chapter_memory(self, user_uuid, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal): + current_app.logger.debug("save_chapter_memory %s %s %s", user_uuid, course_id, lesson_id) + save_chapter_memory_by_uuid( + user_uuid, course_id, lesson_id, subchapter_title, mem_list, score, is_rebuttal + ) diff --git a/Html/apps/services/user_service.py b/Html/apps/services/user_service.py new file mode 100644 index 0000000..736e8e2 --- /dev/null +++ b/Html/apps/services/user_service.py @@ -0,0 +1,42 @@ +# myapp/services/user_service.py +import os +from typing import Tuple +from flask import current_app, session + +# 假设你已有这两个 loader +from db.user import load_user_from_json + +def _username_from_session() -> str: + """通过 session['user_id'] -> username""" + user_uuid = session.get("user_id") + if not user_uuid: + return None + uuid2username = current_app.extensions["uuid2username"] + return uuid2username.get(user_uuid) + +def get_or_load_current_user(): + """ + 返回当前用户对象(如 user_id2UserClass 中没有则从 JSON 载入并缓存) + """ + user_uuid = session.get("user_id") + if not user_uuid: + return None + + username = _username_from_session() + if not username: + return None + + user_map = current_app.extensions["user_id2UserClass"] + if user_uuid not in user_map: + user_map[user_uuid] = load_user_from_json( + username, user_data_dir=current_app.config["USER_DATA_DIR"] + ) + return user_map[user_uuid] + +def add_course_for_current_user(course_id: str, course_data): + user_obj = get_or_load_current_user() + if user_obj is None: + return False + # 你的 UserClass 应该有 select_new_course 接口 + user_obj.select_new_course(course_id, course_data) + return True diff --git a/Html/apps/sockets/namespaces.py b/Html/apps/sockets/namespaces.py new file mode 100644 index 0000000..f1d27fa --- /dev/null +++ b/Html/apps/sockets/namespaces.py @@ -0,0 +1,104 @@ +# myapp/sockets/namespaces.py +import json +from flask import current_app, request, session +from flask_socketio import Namespace, join_room, leave_room, emit +from ..services.memory_service import save_chapter_memory_by_uuid +from ..services.backboard_service import realtime_response +class VSCodeNamespace(Namespace): + def on_login(self,data): + ex = current_app.extensions + username2uuid = ex["username2uuid"] + backboard_manager = ex["backboard_manager"] + print("VSCode client connected") + print(data) + dataconfig = data['config'] + user_id = dataconfig.get('user_id') + path = dataconfig.get('path') + course_id = dataconfig.get('course_id') + lesson_id = dataconfig.get('chapter_id') + print(f"User {user_id} connected with path: {path}") + useruuid = username2uuid[user_id] + join_room(useruuid, namespace='/vscode') + if backboard_manager.get_backboard(useruuid) == None: + backboard_manager.add_backboard(useruuid,user_id, course_id, lesson_id, path) + + + def on_message(self, data): + print(f"Received from VSCode client: {data}") + dataconfig = data['config'] + realtime_response(dataconfig, data) + # emit('response', {'message': 'Config data received and connection established'}) + def on_disconnect(self, data): + print("VSCode client disconnected") + print("Disconnect reason:"+str(data)) + + +class AgentNamespace(Namespace): + def on_login(self, data): + ex = current_app.extensions + username2uuid = ex["username2uuid"] + backboard_manager = ex["backboard_manager"] + agent_manager = ex["agent_manager"] + data = json.loads(data) + user = data['username'] + course_id = data['course_id'] + chapter_id = data['chapter_id'] + user_uuid = username2uuid[user] + session['user_id'] = user_uuid + print(f'User connected with session user_id: {user_uuid}') + # 从markdown、markdown_prompts、score_prompts中各读取数据folder.md并进行new_agent + with open(f'books/markdown/{course_id}/{chapter_id}.md','r',encoding='UTF-8') as fmd,\ + open(f'books/markdown_prompts/{course_id}/{chapter_id}.md','r',encoding='UTF-8')as fmdp,\ + open(f'books/score_prompts/{course_id}/{chapter_id}.md','r',encoding='UTF-8') as fsp: + markdown = fmd.read() + markdown_prompts = fmdp.read() + score_prompts = fsp.read() + user_uuid, agent = agent_manager.new_agent(course_id, chapter_id, markdown, markdown_prompts, score_prompts, id=user_uuid, + root_path=f'../../study/{user}/{course_id}/{chapter_id}') + print(user_uuid) + join_room(user_uuid, namespace='/agent') # 将该用户加入以user_id为名的room + backboard_manager.add_backboard(user_uuid, user, course_id, chapter_id, root_path=f'../../study/{user}/{course_id}/{chapter_id}') + + def on_language(self, language): + ex = current_app.extensions + agent_manager = ex["agent_manager"] + id = session.get('user_id') + agent_manager.change_language(id, language) + + def on_message(self, data): + print(f"Message from client: {data}") + ex = current_app.extensions + agent_manager = ex["agent_manager"] + backboard_manager = ex["backboard_manager"] + id = session.get('user_id') + if (type(data)==str): + data = json.loads(data) + print(id) + if data['type'] == 'text': + res = agent_manager.invoke(id, data['data'], backboard_manager.get_backboard(id).get_info_prompt()) + print('=*='*20) + print(res.content) + reply = f"{res.content['speak']}" + with current_app.app_context(): + emit('message',reply, room=id, namespace='/agent') + emit('request_function',res.content['function'], room=id, namespace='/agent') + + if data['type'] == 'function': + agent_manager.function_call(id, data['data']) + + def on_initiative(self,data): + print("User active function call") + ex = current_app.extensions + agent_manager = ex["agent_manager"] + backboard_manager = ex["backboard_manager"] + user_id = session['user_id'] + if data['name'] == 'sample_judge': + agent_manager.sample_judge(user_id, backboard_manager.get_backboard(user_id)) + if data['name'] == 'judge': + agent_manager.judge(user_id, backboard_manager.get_backboard(user_id)) + + + + def on_disconnect(self,data): + print("VSCode client disconnected") + print("Disconnect reason:"+str(data)) \ No newline at end of file diff --git a/Html/apps/static/binary_search.html b/Html/apps/static/binary_search.html new file mode 100644 index 0000000..4f70959 --- /dev/null +++ b/Html/apps/static/binary_search.html @@ -0,0 +1,53 @@ + + + + + + + +

二分查找与二分答案

+

二分查找

+

引入

+

二分是一个很简单基础,但很重要的知识点,为以后许多高级的数据结构与算法铺垫。

+

下面是一个用二分的简单场景:

+

假设小明从0到1000之间选择了一个数字但不告诉你,你可以不断猜测这个数,每次猜测小明会告知你的猜测得过大还是过小,问最多几次就一定能猜中?

+

答案是利用二分查找的原理,猜测11次即可。

+
    +
  1. 对于0到1000的答案备选区,猜测中位数500,假设过小,
  2. +
  3. 则对于501到1000的答案备选区,猜测750,假设过大
  4. +
  5. 则对于501到749的答案备选区,猜测625,假设过小,
  6. +
  7. 则对于626到749区间......
  8. +
  9. (688-749)
  10. +
  11. (718-749)
  12. +
  13. (734-749)
  14. +
  15. (742-749)
  16. +
  17. (746-749)
  18. +
  19. (748-749)
  20. +
  21. (749-749)
  22. +
+

在最差的情况下,第11次的答案备选区就一定长度为1了,也就是必然是答案。

+

因此如果序列是有序的,就可以通过二分查找快速定位所需要的数据。

+

思考题(询问Agent以学习计算方法,或验证你的答案)

+

对于上面那个题目,如果问题区间是1到4000,最差情况下需要猜测几次?

+

练习:二分查找

+

试试对于下面的题目,用代码实现一下二分查找。

+

题目:有序数组寻址

+

给出一个长度为n的有序数组(从小到大),有q次询问,对于每次询问,输出指定数在数组中的下标。如果不存在则输出-1。

+
输入
+

第一行一个整数n。(1<=n<=10^5)

+

第二行n个用空格分开的整数ai。(0<=ai<=10^8)

+

第三行一个整数q,表示询问的次数。(1<=q<=10^4)

+

后q行,每行一个整数b,表示询问的数。(0<=b<=10^8)

+
输出
+

q行,每行一个整数,对应每次询问的返回结果。

+
提示:
+

完成代码后,通知Agent进行评测。

+

如果你还不完全会这个算法,询问Agent获取提示并进行学习。

+ + + \ No newline at end of file diff --git a/Html/static/css/course.css b/Html/apps/static/css/course.css similarity index 100% rename from Html/static/css/course.css rename to Html/apps/static/css/course.css diff --git a/Html/static/css/dashboard.css b/Html/apps/static/css/dashboard.css similarity index 100% rename from Html/static/css/dashboard.css rename to Html/apps/static/css/dashboard.css diff --git a/Html/static/css/desktop.css b/Html/apps/static/css/desktop.css similarity index 100% rename from Html/static/css/desktop.css rename to Html/apps/static/css/desktop.css diff --git a/Html/static/css/index.css b/Html/apps/static/css/index.css similarity index 100% rename from Html/static/css/index.css rename to Html/apps/static/css/index.css diff --git a/Html/static/css/login.css b/Html/apps/static/css/login.css similarity index 100% rename from Html/static/css/login.css rename to Html/apps/static/css/login.css diff --git a/Html/static/css/teacherboard.css b/Html/apps/static/css/teacherboard.css similarity index 65% rename from Html/static/css/teacherboard.css rename to Html/apps/static/css/teacherboard.css index 2d3559c..c375397 100644 --- a/Html/static/css/teacherboard.css +++ b/Html/apps/static/css/teacherboard.css @@ -84,6 +84,75 @@ .details-content { padding-right: 20px; } +.lesson-item { + display: flex; + align-items: center; + justify-content: space-between; /* 让垃圾桶靠右 */ + padding: 5px 0; +} +.lesson-text { + cursor: pointer; /* 鼠标悬停时变成手指 */ + transition: color 0.3s; + flex: 1; /* 占据左侧空间 */ +} + +.lesson-text:hover { + color: #2575fc; /* 这里你可以设置悬停时的颜色 */ +} + +.icon-btn { + background: none; + border: none; + cursor: pointer; + margin-left: 8px; + color: #666; + font-size: 14px; + transition: color 0.2s; +} + +.icon-btn:hover { + color: #2575fc; /* hover 高亮 */ +} + +.delete-btn { + color: #c0392b; /* 红色垃圾桶 */ +} + +.delete-btn:hover { + color: #e74c3c; +} +.new-chapter-input { + width: 200px; + padding: 8px; + margin-top: 10px; + margin-bottom: 10px; + font-size: 14px; + border-radius: 4px; + border: 1px solid #ddd; +} + +.icon-btn { + background: none; + border: none; + cursor: pointer; + margin-left: 8px; + color: #666; + font-size: 14px; + transition: color 0.2s; +} + +.icon-btn:hover { + color: #2575fc; /* hover 高亮 */ +} + +.delete-btn { + color: #c0392b; /* 红色垃圾桶 */ +} + +.delete-btn:hover { + color: #e74c3c; +} + .add-chapter-btn { background-color: #2575fc; diff --git a/Html/static/example.html b/Html/apps/static/example.html similarity index 100% rename from Html/static/example.html rename to Html/apps/static/example.html diff --git a/Html/static/image/CS101/CS101.png b/Html/apps/static/image/CS101/CS101.png similarity index 100% rename from Html/static/image/CS101/CS101.png rename to Html/apps/static/image/CS101/CS101.png diff --git a/Html/static/image/algorithm/book_cover.png b/Html/apps/static/image/algorithm/book_cover.png similarity index 100% rename from Html/static/image/algorithm/book_cover.png rename to Html/apps/static/image/algorithm/book_cover.png diff --git a/Html/static/image/data-structure/book_cover-data-structure.png b/Html/apps/static/image/data-structure/book_cover-data-structure.png similarity index 100% rename from Html/static/image/data-structure/book_cover-data-structure.png rename to Html/apps/static/image/data-structure/book_cover-data-structure.png diff --git a/Html/static/image/example/p1.png b/Html/apps/static/image/example/p1.png similarity index 100% rename from Html/static/image/example/p1.png rename to Html/apps/static/image/example/p1.png diff --git a/Html/static/js/chatbox.js b/Html/apps/static/js/chatbox.js similarity index 98% rename from Html/static/js/chatbox.js rename to Html/apps/static/js/chatbox.js index 35612c4..5da49a5 100644 --- a/Html/static/js/chatbox.js +++ b/Html/apps/static/js/chatbox.js @@ -4,7 +4,12 @@ let system_message_idx=0 document.addEventListener('DOMContentLoaded', function() { data = window.appData; console.log(data); - socket = io('/agent?username='+data.username+'&folder='+data.folder); + socket = io('ws://localhost:5551/agent',{ + query:{ + username:data.username, + folder:data.folder + } + }); socket.on('connect', function() { console.log('Connected to server'); socket.emit('login',JSON.stringify(data)); diff --git a/Html/static/js/dashboard.js b/Html/apps/static/js/dashboard.js similarity index 100% rename from Html/static/js/dashboard.js rename to Html/apps/static/js/dashboard.js diff --git a/Html/static/js/index.js b/Html/apps/static/js/index.js similarity index 100% rename from Html/static/js/index.js rename to Html/apps/static/js/index.js diff --git a/Html/static/js/login.js b/Html/apps/static/js/login.js similarity index 100% rename from Html/static/js/login.js rename to Html/apps/static/js/login.js diff --git a/Html/static/js/register.js b/Html/apps/static/js/register.js similarity index 100% rename from Html/static/js/register.js rename to Html/apps/static/js/register.js diff --git a/Html/static/js/register_teacher.js b/Html/apps/static/js/register_teacher.js similarity index 100% rename from Html/static/js/register_teacher.js rename to Html/apps/static/js/register_teacher.js diff --git a/Html/apps/static/js/teacherboard.js b/Html/apps/static/js/teacherboard.js new file mode 100644 index 0000000..bca619b --- /dev/null +++ b/Html/apps/static/js/teacherboard.js @@ -0,0 +1,189 @@ +function lessonTemplate(lesson) { + return ` +
  • + ${lesson} + + + +
  • + `; +} + +function show_teacher_data(user_data, user_course_data) { + // 这里可以根据user_course_data生成课程卡片 + const courseCardsContainer = document.getElementById('course-cards'); + user_course_data.forEach(course => { + const courseCard = document.createElement('div'); + courseCard.classList.add('course-card'); + courseCard.onclick = function() { openCourseDetails(course.id); }; + courseCard.innerHTML = ` + 课程封面 +
    +

    ${course.name}

    +

    ${course.description}

    +
    + `; + courseCardsContainer.appendChild(courseCard); + }); +} +let isAddingChapter = false; + +function showInputForNewChapter() { + if (isAddingChapter) return; // 防止重复点击 + + isAddingChapter = true; // 标记正在添加章节 + + // 找到新增章节按钮并隐藏 + const addButton = document.querySelector('.add-chapter-btn'); + addButton.style.display = 'none'; + + // 创建一个输入框 + const input = document.createElement('input'); + input.type = 'text'; + input.placeholder = '请输入章节名称...'; + input.className = 'new-chapter-input'; + input.onblur = () => checkAndAddChapter(input); + + // 将输入框插入到页面 + const chapterList = document.getElementById('chapter-list'); + chapterList.appendChild(input); + input.focus(); // 聚焦到输入框 +} + +function checkAndAddChapter(input) { + const chapterName = input.value.trim(); + + // 如果章节名称为空,显示提示并不添加 + if (!chapterName) { + input.remove(); // 删除输入框 + document.querySelector('.add-chapter-btn').style.display = 'block'; // 重新显示新增章节按钮 + isAddingChapter = false; + return; + } + + // 如果章节名称合法,添加到章节列表 + const chapterList = document.getElementById('chapter-list'); + const chapterItem = document.createElement('li'); + chapterItem.innerHTML = ` +
  • + ${chapterName} +
  • +
      +
    + + + `; + chapterList.appendChild(chapterItem); + // 恢复新增按钮的显示 + document.querySelector('.add-chapter-btn').style.display = 'block'; + input.remove(); + isAddingChapter = false; +} +function openCourseDetails(courseId) { + // 打开课程目录 + const courseDetails = document.getElementById('courseDetails'); + const courseTitle = document.getElementById('course-title'); + courseTitle.textContent = "课程名称: " + courseId; // 假设这里显示课程ID,实际情况应该是课程名称 + + courseDetails.classList.add('open'); + + // 获取章节数据并渲染 + renderChapters(courseId); +} +function deleteChapter(deleteButton) { + // 删除章节 + const chapterItem = deleteButton.closest('li'); + chapterItem.remove(); +} +function renderChapters(courseId) { + const chapterList = document.getElementById('chapter-list'); + chapterList.innerHTML = ""; // 清空章节列表 + + // 假设从服务器获取课程章节数据 + const chapters = [ + { title: "第一章:算法基础", lessons: ["算法简介", "算法设计方法"] }, + { title: "第二章:数据结构", lessons: ["数组", "链表"] } + ]; + + chapters.forEach(chapter => { + const chapterItem = document.createElement('li'); + + // 渲染章节标题 + let lessonsHtml = chapter.lessons.map(lesson => lessonTemplate(lesson)).join(''); + + chapterItem.innerHTML = ` + ${chapter.title} +
      + ${lessonsHtml} +
    + + `; + + + chapterList.appendChild(chapterItem); + }); +} + +function showInputForNewLesson(chapterTitle, button) { + // 防止重复点击 + const addButton = button; + addButton.style.display = 'none'; // 隐藏按钮 + + // 创建一个输入框 + const input = document.createElement('input'); + input.type = 'text'; + input.placeholder = '请输入课时名称...'; + input.className = 'new-lesson-input'; + input.onblur = () => checkAndAddLesson(input, chapterTitle, addButton); // 失去焦点时检查输入 + + // 将输入框插入到章节列表中 + const chapterItem = addButton.closest('li'); // 获取到点击按钮的父元素 + chapterItem.appendChild(input); + input.focus(); // 聚焦到输入框 +} +function checkAndAddLesson(input, chapterTitle, addButton) { + const lessonName = input.value.trim(); + + // 如果课时名称为空,显示提示并不添加 + if (!lessonName) { + input.remove(); // 删除输入框 + addButton.style.display = 'block'; // 重新显示新增课时按钮 + return; + } + + // 如果课时名称合法,添加到课时列表 + const chapterList = document.getElementById('chapter-list'); + const chapterItem = Array.from(chapterList.children).find(item => item.querySelector('strong').textContent === chapterTitle); + const lessonsList = chapterItem.querySelector('ul'); + + const lessonItem = document.createElement('span'); + lessonItem.innerHTML = lessonTemplate(lessonName); + + lessonsList.appendChild(lessonItem); // 将新课时添加到章节下 + input.remove(); // 删除输入框 + addButton.style.display = 'block'; // 重新显示新增课时按钮 +} +function deleteLesson(lesson) { + const lessonItems = document.querySelectorAll('.lesson-item'); + lessonItems.forEach(item => { + if (item.querySelector('.lesson-text').textContent === lesson) { + item.remove(); // 删除对应的课时项 + } + }); +} + + +function editLesson(lesson) { + // 编辑课时的逻辑 + alert("编辑课时: " + lesson); +} + +function closeCourseDetails() { + document.getElementById('courseDetails').classList.remove('open'); +} diff --git a/Html/static/twice_split.html b/Html/apps/static/twice_split.html similarity index 100% rename from Html/static/twice_split.html rename to Html/apps/static/twice_split.html diff --git a/Html/templates/course.html b/Html/apps/templates/course.html similarity index 100% rename from Html/templates/course.html rename to Html/apps/templates/course.html diff --git a/Html/templates/dashboard.html b/Html/apps/templates/dashboard.html similarity index 100% rename from Html/templates/dashboard.html rename to Html/apps/templates/dashboard.html diff --git a/Html/templates/desktop.html b/Html/apps/templates/desktop.html similarity index 98% rename from Html/templates/desktop.html rename to Html/apps/templates/desktop.html index bb87bd1..8633354 100644 --- a/Html/templates/desktop.html +++ b/Html/apps/templates/desktop.html @@ -66,7 +66,8 @@ window.appData = { username:"{{user_id}}", course_id:"{{course_id}}", - chapter_id:"{{chapter_id}}" + chapter_id:"{{chapter_id}}", + folder:"{{workspace_path}}" } diff --git a/Html/templates/index.html b/Html/apps/templates/index.html similarity index 97% rename from Html/templates/index.html rename to Html/apps/templates/index.html index 865194a..034cac8 100644 --- a/Html/templates/index.html +++ b/Html/apps/templates/index.html @@ -1,44 +1,44 @@ - - - - - - 课程选择主页 - - - - {% include 'navbar.html' %} -
    -
    -

    课程列表

    -
    - {% for course_id, course in courses_data.items() %} -
    - {{ course.course_name }} -

    {{ course.course_name }}

    -

    {{ course.course_description }}

    - {% if course_id in selected_courses %} - - {% else %} - - {% endif %} -
    - {% endfor %} - -
    -
    -
    - -
    -

    版权所有 © 2024 “华实伴学君”——教学练评一体的虚拟编码助教

    -
    - - - - - + + + + + + 课程选择主页 + + + + {% include 'navbar.html' %} +
    +
    +

    课程列表

    +
    + {% for course_id, course in courses_data.items() %} +
    + {{ course.course_name }} +

    {{ course.course_name }}

    +

    {{ course.course_description }}

    + {% if course_id in selected_courses %} + + {% else %} + + {% endif %} +
    + {% endfor %} + +
    +
    +
    + +
    +

    版权所有 © 2024 “华实伴学君”——教学练评一体的虚拟编码助教

    +
    + + + + + diff --git a/Html/templates/login.html b/Html/apps/templates/login.html similarity index 100% rename from Html/templates/login.html rename to Html/apps/templates/login.html diff --git a/Html/templates/navbar.html b/Html/apps/templates/navbar.html similarity index 100% rename from Html/templates/navbar.html rename to Html/apps/templates/navbar.html diff --git a/Html/templates/register.html b/Html/apps/templates/register.html similarity index 100% rename from Html/templates/register.html rename to Html/apps/templates/register.html diff --git a/Html/templates/register_teacher.html b/Html/apps/templates/register_teacher.html similarity index 100% rename from Html/templates/register_teacher.html rename to Html/apps/templates/register_teacher.html diff --git a/Html/templates/saveindex.html b/Html/apps/templates/saveindex.html similarity index 100% rename from Html/templates/saveindex.html rename to Html/apps/templates/saveindex.html diff --git a/Html/templates/teacherboard.html b/Html/apps/templates/teacherboard.html similarity index 94% rename from Html/templates/teacherboard.html rename to Html/apps/templates/teacherboard.html index 48a4d58..06c2696 100644 --- a/Html/templates/teacherboard.html +++ b/Html/apps/templates/teacherboard.html @@ -7,6 +7,7 @@ + @@ -47,7 +48,7 @@
    - + diff --git a/Html/apps/views/__init__.py b/Html/apps/views/__init__.py new file mode 100644 index 0000000..f9abcc0 --- /dev/null +++ b/Html/apps/views/__init__.py @@ -0,0 +1,10 @@ +from flask import Flask +from .markdown import bp as markdown_bp +from .vscode import bp as vscode_bp +from .auth import bp as auth_bp +from .dashboard import bp as main_bp +def register_blueprints(app: Flask): + app.register_blueprint(markdown_bp) # 默认就是挂在根路径 + app.register_blueprint(vscode_bp) + app.register_blueprint(auth_bp) + app.register_blueprint(main_bp) \ No newline at end of file diff --git a/Html/apps/views/auth.py b/Html/apps/views/auth.py new file mode 100644 index 0000000..65f6fc2 --- /dev/null +++ b/Html/apps/views/auth.py @@ -0,0 +1,57 @@ +# myapp/views/auth.py +from flask import Blueprint, render_template, request, jsonify, redirect, url_for, session, current_app +from ..services.auth_service import register_user, login_user, logout_user +from ..auth.decorators import require_role + +bp = Blueprint("auth", __name__) + +@bp.get("/register") +def register(): + return render_template("register.html") + +@bp.get("/register_teacher") +def register_teacher(): + return render_template("register_teacher.html") + +@bp.post("/register_post") +def register_post(): + data = request.get_json(force=True) or {} + ok, msg = register_user(data.get("username"), data.get("password"), teacher=False) + return jsonify({"success": ok, "message": msg}) + +@bp.post("/register_teacher_post") +def register_teacher_post(): + data = request.get_json(force=True) or {} + ok, msg = register_user(data.get("username"), data.get("password"), teacher=True) + return jsonify({"success": ok, "message": msg}) + +@bp.get("/login") +def login(): + # 你原来 return 后还有逻辑,已被覆盖;这里简化 + return render_template("login.html") + +@bp.post("/login_post") +def login_post(): + data = request.get_json(force=True) or {} + ok, msg = login_user(data.get("username"), data.get("password"), require_teacher=False) + return jsonify({"success": ok, "message": msg}) + +@bp.post("/login_teacher_post") +def login_teacher_post(): + data = request.get_json(force=True) or {} + ok, msg = login_user(data.get("username"), data.get("password"), require_teacher=True) + return jsonify({"success": ok, "message": msg}) + +@bp.get("/teacherboard") +@require_role(roles="teacher") +def teacherboard(): + return render_template("teacherboard.html") + +@bp.get("/logout") +def logout(): + logout_user() + return redirect(url_for("auth.login")) + +@bp.get("/get_session") +def get_session(): + return jsonify({"session": session.get("user_id", "default_session")}) diff --git a/Html/apps/views/dashboard.py b/Html/apps/views/dashboard.py new file mode 100644 index 0000000..9a6d118 --- /dev/null +++ b/Html/apps/views/dashboard.py @@ -0,0 +1,60 @@ +# myapp/views/dashboard.py +import json +from flask import Blueprint, render_template, request, jsonify, redirect, url_for +from ..auth.decorators import require_role +from ..services.user_service import get_or_load_current_user, add_course_for_current_user +from ..services.course_service import load_course, user_selected_course_briefs + +bp = Blueprint("main", __name__) # 根路径蓝图 + +@bp.get("/dashboard") +@require_role +def dashboard(): + user_obj = get_or_load_current_user() + if user_obj is None: + return redirect(url_for("auth.login")) + + user_course_data = user_selected_course_briefs(user_obj) + return render_template( + "dashboard.html", + user_data=user_obj.to_json_without_dialog(), + user_course_data=json.dumps(user_course_data, ensure_ascii=False), + ) + +@bp.get("/course/") +@require_role +def course(course_id): + c = load_course(course_id) + return render_template("course.html", course_id=course_id, course_data=c) + +@bp.post("/select_course") +@require_role +def select_course(): + data = request.get_json(force=True) or {} + course_id = data.get("course_id") + if not course_id: + return jsonify({"success": False, "message": "缺少 course_id"}), 400 + + course_data = load_course(course_id) + ok = add_course_for_current_user(course_id, course_data) + if not ok: + return jsonify({"success": False, "message": "未登录或会话失效"}), 401 + return jsonify({"success": True, "message": "课程选择成功"}) + +@bp.get("/") +@require_role +def home_index(): + user_obj = get_or_load_current_user() + if user_obj is None: + return redirect(url_for("auth.login")) + + selected_courses = list(getattr(user_obj, "select_course", [])) + # 课程目录:依据你的 CourseList 暴露的接口进行传递(这里直接传对象,模板里用) + from flask import current_app + course_list = current_app.extensions["course_list"] + + return render_template( + "index.html", + courses_data=course_list, + selected_courses=selected_courses + ) diff --git a/Html/apps/views/markdown.py b/Html/apps/views/markdown.py new file mode 100644 index 0000000..4dafe03 --- /dev/null +++ b/Html/apps/views/markdown.py @@ -0,0 +1,35 @@ +import os +from flask import Blueprint, jsonify, current_app, send_from_directory +from ..services.markdown_service import ( + convert_markdown_to_html, wrap_with_styles, + save_html, copy_images +) + +bp = Blueprint("markdown", __name__) + +@bp.route("/--markdown", methods=["GET"]) +def convert_md(course_id, filename): + md_file_path = os.path.join(current_app.config["MARKDOWN_DIR"], course_id, f"{filename}.md") + + if not os.path.exists(md_file_path): + return jsonify({"error": "Markdown file not found"}), 404 + + # 转换 markdown -> html + html = convert_markdown_to_html(md_file_path) + html_with_styles = wrap_with_styles(html) + + # 保存 HTML 文件到 static + html_output_path = os.path.join(current_app.config["STATIC_DIR"], f"{filename}.html") + save_html(html_with_styles, html_output_path) + + # 拷贝图片资源 + image_source = os.path.join(current_app.config["MARKDOWN_DIR"], course_id, current_app.config["IMAGE_DIR"], filename) + image_target = os.path.join(current_app.config["STATIC_DIR"], current_app.config["IMAGE_DIR"], filename) + copy_images(image_source, image_target) + + return jsonify({"html_url": f"/static/{filename}.html"}) + +# 提供静态文件访问 +@bp.route("/static/") +def serve_static(filename): + return send_from_directory(current_app.config["STATIC_DIR"], filename) diff --git a/Html/apps/views/vscode.py b/Html/apps/views/vscode.py new file mode 100644 index 0000000..1aafa24 --- /dev/null +++ b/Html/apps/views/vscode.py @@ -0,0 +1,69 @@ +# myapp/views/vscode.py +import os, uuid, json +from flask import Blueprint, current_app, session, redirect, url_for, render_template, request, jsonify +from ..services.backboard_service import realtime_response + +bp = Blueprint("vscode", __name__) + +@bp.route("/desktop///") +def desktop(user_id, course_id, chapter_id): + # session 中放 uuid(访客也能进则给一个) + if "user_id" not in session: + session["user_id"] = "user_" + str(uuid.uuid4()) + + current_app.logger.debug("user %s uuid is %s", user_id, session["user_id"]) + + # 全局映射 + username2uuid = current_app.extensions["username2uuid"] + uuid2username = current_app.extensions["uuid2username"] + userid_recorder = current_app.extensions["userid_recorder"] + + username2uuid[user_id] = session["user_id"] + uuid2username[session["user_id"]] = user_id + userid_recorder[f"{user_id}&{course_id}"] = session["user_id"] + + # 按课程/章节创建工作目录 + base_root = current_app.config["STUDENT_WORKSPACE_ROOT"] + path_dir = os.path.join(base_root, user_id, course_id, chapter_id) + os.makedirs(path_dir, exist_ok=True) + + # 写 .config(并按需要转换为 WSL 路径) + cfg = current_app.config["VSCODE_WEB_PATH"] + path_for_vscode = path_dir + if cfg.get("is_wsl"): + path_for_vscode = path_for_vscode.replace("\\", "/") \ + .replace(cfg.get("windows_path", ""), cfg.get("wsl_path", "")) + + config_path = os.path.join(path_dir, ".config") + tmpd = { + "user_id": user_id, + "course_id": course_id, + "chapter_id": chapter_id, + "path": path_for_vscode + } + with open(config_path, "w", encoding="utf-8") as f: + json.dump(tmpd, f, ensure_ascii=False) + + current_app.logger.debug("config file path: %s", config_path) + + return render_template( + "desktop.html", + vscode_web_url=current_app.config["VSCODE_WEB_URL"], + user_id=user_id, course_id=course_id, chapter_id=chapter_id, + workspace_path=path_for_vscode + ) + +@bp.route("/desktop_nouser//") +def desktop_nouser(course_id, chapter_id): + if "user_id" not in session: + return redirect(url_for("auth.login")) # 如果你有 auth 蓝图 + user_uuid = session["user_id"] + username = current_app.extensions["uuid2username"].get(user_uuid) + return redirect(url_for("vscode.desktop", user_id=username, course_id=course_id, chapter_id=chapter_id)) + +@bp.route("/vscode_data", methods=["POST"]) +def vscode_data(): + data = request.get_json(force=True) + config = data.get("config") or {} + realtime_response(config, data) # 调用服务层逻辑 + return jsonify({"status": "success", "received": data}) diff --git a/Html/bootstrap.py b/Html/bootstrap.py new file mode 100644 index 0000000..bcb15ce --- /dev/null +++ b/Html/bootstrap.py @@ -0,0 +1,31 @@ +from pathlib import Path +import sys, os + +def bootstrap_paths(): + """ + 让 'AlgoriAgent'(位于 repo_root/AlgoriAgent)可被 import; + 并提供 STUDENT_WORKSPACE_ROOT 的默认值(repo_root/study)。 + 适配当前工厂位置:repo_root/Html/apps/__init__.py + """ + here = Path(__file__).resolve() + # repo_root = .../Html/apps/../../ => parents[2] + repo_root = here.parents[2] + + # 兜底:如果目录结构变化,尝试向上搜索包含 AlgoriAgent 的目录作为 repo_root + if not (repo_root / "AlgoriAgent").exists(): + for p in here.parents: + if (p / "AlgoriAgent").is_dir(): + repo_root = p + break + + # 确保仓库根在 sys.path(这样 'AlgoriAgent' 顶层包可被发现) + repo_root_str = str(repo_root) + if repo_root_str not in sys.path: + sys.path.insert(0, repo_root_str) + + # 可选:如果你还有其它顶层包,也可在此处继续插入 + + # 默认学习工作区目录(也可放到 config.ini / 环境变量覆盖) + os.environ.setdefault("STUDENT_WORKSPACE_ROOT", str(repo_root / "study")) + + return repo_root # 方便调试/日志 \ No newline at end of file diff --git a/Html/static/binary_search.html b/Html/static/binary_search.html index 4f70959..ad467d0 100644 --- a/Html/static/binary_search.html +++ b/Html/static/binary_search.html @@ -9,7 +9,7 @@ - +

    二分查找与二分答案

    二分查找

    引入

    diff --git a/Html/static/js/teacherboard.js b/Html/static/js/teacherboard.js deleted file mode 100644 index 9c86c7f..0000000 --- a/Html/static/js/teacherboard.js +++ /dev/null @@ -1,70 +0,0 @@ -function show_teacher_data(user_data, user_course_data) { - // 这里可以根据user_course_data生成课程卡片 - const courseCardsContainer = document.getElementById('course-cards'); - user_course_data.forEach(course => { - const courseCard = document.createElement('div'); - courseCard.classList.add('course-card'); - courseCard.onclick = function() { openCourseDetails(course.id); }; - courseCard.innerHTML = ` - 课程封面 -
    -

    ${course.name}

    -

    ${course.description}

    -
    - `; - courseCardsContainer.appendChild(courseCard); - }); -} - -function openCourseDetails(courseId) { - // 打开课程目录 - const courseDetails = document.getElementById('courseDetails'); - const courseTitle = document.getElementById('course-title'); - courseTitle.textContent = "课程名称: " + courseId; // 假设这里显示课程ID,实际情况应该是课程名称 - - courseDetails.classList.add('open'); - - // 获取章节数据并渲染 - renderChapters(courseId); -} - -function closeCourseDetails() { - document.getElementById('courseDetails').classList.remove('open'); -} - -function renderChapters(courseId) { - const chapterList = document.getElementById('chapter-list'); - chapterList.innerHTML = ""; // 清空章节列表 - // 假设从服务器获取课程章节数据 - const chapters = [ - { title: "第一章:算法基础", lessons: ["算法简介", "算法设计方法"] }, - { title: "第二章:数据结构", lessons: ["数组", "链表"] } - ]; - - chapters.forEach(chapter => { - const chapterItem = document.createElement('li'); - chapterItem.innerHTML = ` - ${chapter.title} -
      - ${chapter.lessons.map(lesson => `
    • ${lesson}
    • `).join('')} -
    - - `; - chapterList.appendChild(chapterItem); - }); -} - -function addChapter() { - // 新增章节的逻辑 - alert("新增章节功能"); -} - -function addLesson(chapterTitle) { - // 新增课时的逻辑 - alert("新增课时功能: " + chapterTitle); -} - -function editLesson(lesson) { - // 编辑课时的逻辑 - alert("编辑课时: " + lesson); -} diff --git a/Html/wsgi.py b/Html/wsgi.py new file mode 100644 index 0000000..bbcbe5f --- /dev/null +++ b/Html/wsgi.py @@ -0,0 +1,6 @@ +# wsgi.py +from bootstrap import bootstrap_paths +bootstrap_paths() +from apps import create_app +# 提供一个可被 WSGI/进程管理器导入的 app 对象 +app = create_app()