Version 0.1.1 rise to flask app factory
513
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('/<course_id>-<filename>-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>
|
||||
<head>
|
||||
<style>
|
||||
img {{
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
|
||||
</head>
|
||||
<body>
|
||||
{html_content}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
# 保存 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/<path:filename>')
|
||||
def serve_static(filename):
|
||||
return send_from_directory(STATIC_DIR, filename)
|
||||
|
||||
|
||||
'''
|
||||
Vscode
|
||||
'''
|
||||
@app.route('/desktop/<user_id>/<course_id>/<chapter_id>')
|
||||
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/<course_id>/<chapter_id>')
|
||||
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/<course_id>')
|
||||
@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)
|
||||
|
||||
|
||||
480
Html/app_back.py
Normal file
@@ -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('/<course_id>-<filename>-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>
|
||||
<head>
|
||||
<style>
|
||||
img {{
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
|
||||
</head>
|
||||
<body>
|
||||
{html_content}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
# 保存 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/<path:filename>')
|
||||
def serve_static(filename):
|
||||
return send_from_directory(STATIC_DIR, filename)
|
||||
|
||||
|
||||
'''
|
||||
Vscode
|
||||
'''
|
||||
@app.route('/desktop/<user_id>/<course_id>/<chapter_id>')
|
||||
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/<course_id>/<chapter_id>')
|
||||
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/<course_id>')
|
||||
@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)
|
||||
|
||||
34
Html/apps/__init__.py
Normal file
@@ -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
|
||||
98
Html/apps/auth/decorators.py
Normal file
@@ -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
|
||||
1
Html/apps/bootstrap.py
Normal file
@@ -0,0 +1 @@
|
||||
# Html/apps/bootstrap.py
|
||||
37
Html/apps/config.py
Normal file
@@ -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=""),
|
||||
}
|
||||
59
Html/apps/extensions.py
Normal file
@@ -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"))
|
||||
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
46
Html/apps/services/auth_service.py
Normal file
@@ -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)
|
||||
59
Html/apps/services/backboard_service.py
Normal file
@@ -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)
|
||||
20
Html/apps/services/course_service.py
Normal file
@@ -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
|
||||
40
Html/apps/services/markdown_service.py
Normal file
@@ -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>
|
||||
<head>
|
||||
<style>
|
||||
img {{
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
|
||||
</head>
|
||||
<body class="markdown-body">
|
||||
{html_content}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
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)
|
||||
26
Html/apps/services/memory_service.py
Normal file
@@ -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
|
||||
)
|
||||
10
Html/apps/services/my_function.py
Normal file
@@ -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
|
||||
)
|
||||
42
Html/apps/services/user_service.py
Normal file
@@ -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
|
||||
104
Html/apps/sockets/namespaces.py
Normal file
@@ -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))
|
||||
53
Html/apps/static/binary_search.html
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>二分查找与二分答案</h1>
|
||||
<h2>二分查找</h2>
|
||||
<h3>引入</h3>
|
||||
<p>二分是一个很简单基础,但很重要的知识点,为以后许多高级的数据结构与算法铺垫。</p>
|
||||
<p>下面是一个用二分的简单场景:</p>
|
||||
<p>假设小明从0到1000之间选择了一个数字但不告诉你,你可以不断猜测这个数,每次猜测小明会告知你的猜测得过大还是过小,问最多几次就一定能猜中?</p>
|
||||
<p>答案是利用二分查找的原理,猜测11次即可。</p>
|
||||
<ol>
|
||||
<li>对于0到1000的答案备选区,猜测中位数500,假设过小,</li>
|
||||
<li>则对于501到1000的答案备选区,猜测750,假设过大</li>
|
||||
<li>则对于501到749的答案备选区,猜测625,假设过小,</li>
|
||||
<li>则对于626到749区间......</li>
|
||||
<li>(688-749)</li>
|
||||
<li>(718-749)</li>
|
||||
<li>(734-749)</li>
|
||||
<li>(742-749)</li>
|
||||
<li>(746-749)</li>
|
||||
<li>(748-749)</li>
|
||||
<li>(749-749)</li>
|
||||
</ol>
|
||||
<p>在最差的情况下,第11次的答案备选区就一定长度为1了,也就是必然是答案。</p>
|
||||
<p>因此如果序列是有序的,就可以通过二分查找快速定位所需要的数据。</p>
|
||||
<h4>思考题(询问Agent以学习计算方法,或验证你的答案)</h4>
|
||||
<p>对于上面那个题目,如果问题区间是1到4000,最差情况下需要猜测几次?</p>
|
||||
<h3>练习:二分查找</h3>
|
||||
<p>试试对于下面的题目,用代码实现一下二分查找。</p>
|
||||
<h4>题目:有序数组寻址</h4>
|
||||
<p>给出一个长度为n的有序数组(从小到大),有q次询问,对于每次询问,输出指定数在数组中的下标。如果不存在则输出-1。</p>
|
||||
<h5>输入</h5>
|
||||
<p>第一行一个整数n。(1<=n<=10^5)</p>
|
||||
<p>第二行n个用空格分开的整数ai。(0<=ai<=10^8)</p>
|
||||
<p>第三行一个整数q,表示询问的次数。(1<=q<=10^4)</p>
|
||||
<p>后q行,每行一个整数b,表示询问的数。(0<=b<=10^8)</p>
|
||||
<h5>输出</h5>
|
||||
<p>q行,每行一个整数,对应每次询问的返回结果。</p>
|
||||
<h5>提示:</h5>
|
||||
<p>完成代码后,通知Agent进行评测。</p>
|
||||
<p>如果你还不完全会这个算法,询问Agent获取提示并进行学习。</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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;
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 912 KiB After Width: | Height: | Size: 912 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
@@ -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));
|
||||
189
Html/apps/static/js/teacherboard.js
Normal file
@@ -0,0 +1,189 @@
|
||||
function lessonTemplate(lesson) {
|
||||
return `
|
||||
<li class="lesson-item">
|
||||
<span class="lesson-text" onclick="editLesson('${lesson}')">${lesson}
|
||||
<button class="icon-btn edit-btn" onclick="editLesson('${lesson}')">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
</span>
|
||||
<button class="icon-btn delete-btn" onclick="deleteLesson('${lesson}')">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<img src="${course.cover_image}" alt="课程封面" class="course-image">
|
||||
<div class="course-info">
|
||||
<h3 class="course-name">${course.name}</h3>
|
||||
<p class="course-description">${course.description}</p>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<li class="lesson-item">
|
||||
<strong>${chapterName}</strong> <button class="icon-btn delete-btn" onclick="deleteChapter(this)">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</li>
|
||||
<ul>
|
||||
</ul>
|
||||
<button class="add-lesson-btn" onclick="showInputForNewLesson('${chapterName}', this)">新增课时</button>
|
||||
|
||||
`;
|
||||
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 = `
|
||||
<strong>${chapter.title}</strong>
|
||||
<ul>
|
||||
${lessonsHtml}
|
||||
</ul>
|
||||
<button class="add-lesson-btn" onclick="showInputForNewLesson('${chapter.title}', this)">新增课时</button>
|
||||
`;
|
||||
|
||||
|
||||
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');
|
||||
}
|
||||
@@ -66,7 +66,8 @@
|
||||
window.appData = {
|
||||
username:"{{user_id}}",
|
||||
course_id:"{{course_id}}",
|
||||
chapter_id:"{{chapter_id}}"
|
||||
chapter_id:"{{chapter_id}}",
|
||||
folder:"{{workspace_path}}"
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
@@ -1,44 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>课程选择主页</title>
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
</head>
|
||||
<body onload="init_index()" >
|
||||
{% include 'navbar.html' %} <!-- 引入navbar.html -->
|
||||
<main>
|
||||
<div class="course-selection">
|
||||
<h2>课程列表</h2>
|
||||
<div class="course-list">
|
||||
{% for course_id, course in courses_data.items() %}
|
||||
<div class="course-card" onclick="show_course_details('{{ course_id }}')">
|
||||
<img src="{{ course.course_img_path }}" alt="{{ course.course_name }}">
|
||||
<h3>{{ course.course_name }}</h3>
|
||||
<p>{{ course.course_description }}</p>
|
||||
{% if course_id in selected_courses %}
|
||||
<button class="selected-button">已选择</button>
|
||||
{% else %}
|
||||
<button class="select-button" data-course-id="{{course_id}}">选择课程</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>版权所有 © 2024 “华实伴学君”——教学练评一体的虚拟编码助教</p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
<script>
|
||||
window.appData = {
|
||||
user_selected_courses: JSON.parse(`{{selected_courses}}`.replace(/"/g, "\""))
|
||||
}
|
||||
|
||||
</script>
|
||||
<script src="/static/js/index.js"></script>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>课程选择主页</title>
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
</head>
|
||||
<body onload="init_index()" >
|
||||
{% include 'navbar.html' %} <!-- 引入navbar.html -->
|
||||
<main>
|
||||
<div class="course-selection">
|
||||
<h2>课程列表</h2>
|
||||
<div class="course-list">
|
||||
{% for course_id, course in courses_data.items() %}
|
||||
<div class="course-card" onclick="show_course_details('{{ course_id }}')">
|
||||
<img src="{{ course.course_img_path }}" alt="{{ course.course_name }}">
|
||||
<h3>{{ course.course_name }}</h3>
|
||||
<p>{{ course.course_description }}</p>
|
||||
{% if course_id in selected_courses %}
|
||||
<button class="selected-button">已选择</button>
|
||||
{% else %}
|
||||
<button class="select-button" data-course-id="{{course_id}}">选择课程</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>版权所有 © 2024 “华实伴学君”——教学练评一体的虚拟编码助教</p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
<script>
|
||||
window.appData = {
|
||||
user_selected_courses: JSON.parse(`{{selected_courses}}`.replace(/"/g, "\""))
|
||||
}
|
||||
|
||||
</script>
|
||||
<script src="/static/js/index.js"></script>
|
||||
</html>
|
||||
@@ -7,6 +7,7 @@
|
||||
<link rel="stylesheet" href="/static/css/teacherboard.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -47,7 +48,7 @@
|
||||
<ul id="chapter-list">
|
||||
<!-- 章节列表会在点击课程时动态生成 -->
|
||||
</ul>
|
||||
<button class="add-chapter-btn" onclick="addChapter()">新增章节</button>
|
||||
<button class="add-chapter-btn" onclick="showInputForNewChapter()">新增章节</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
10
Html/apps/views/__init__.py
Normal file
@@ -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)
|
||||
57
Html/apps/views/auth.py
Normal file
@@ -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")})
|
||||
60
Html/apps/views/dashboard.py
Normal file
@@ -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/<course_id>")
|
||||
@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
|
||||
)
|
||||
35
Html/apps/views/markdown.py
Normal file
@@ -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("/<course_id>-<filename>-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/<path:filename>")
|
||||
def serve_static(filename):
|
||||
return send_from_directory(current_app.config["STATIC_DIR"], filename)
|
||||
69
Html/apps/views/vscode.py
Normal file
@@ -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/<user_id>/<course_id>/<chapter_id>")
|
||||
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/<course_id>/<chapter_id>")
|
||||
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})
|
||||
31
Html/bootstrap.py
Normal file
@@ -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 # 方便调试/日志
|
||||
@@ -9,7 +9,7 @@
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<body class="markdown-body">
|
||||
<h1>二分查找与二分答案</h1>
|
||||
<h2>二分查找</h2>
|
||||
<h3>引入</h3>
|
||||
|
||||
@@ -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 = `
|
||||
<img src="${course.cover_image}" alt="课程封面" class="course-image">
|
||||
<div class="course-info">
|
||||
<h3 class="course-name">${course.name}</h3>
|
||||
<p class="course-description">${course.description}</p>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<strong>${chapter.title}</strong>
|
||||
<ul>
|
||||
${chapter.lessons.map(lesson => `<li>${lesson} <button onclick="editLesson('${lesson}')">编辑</button></li>`).join('')}
|
||||
</ul>
|
||||
<button onclick="addLesson('${chapter.title}')">新增课时</button>
|
||||
`;
|
||||
chapterList.appendChild(chapterItem);
|
||||
});
|
||||
}
|
||||
|
||||
function addChapter() {
|
||||
// 新增章节的逻辑
|
||||
alert("新增章节功能");
|
||||
}
|
||||
|
||||
function addLesson(chapterTitle) {
|
||||
// 新增课时的逻辑
|
||||
alert("新增课时功能: " + chapterTitle);
|
||||
}
|
||||
|
||||
function editLesson(lesson) {
|
||||
// 编辑课时的逻辑
|
||||
alert("编辑课时: " + lesson);
|
||||
}
|
||||
6
Html/wsgi.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# wsgi.py
|
||||
from bootstrap import bootstrap_paths
|
||||
bootstrap_paths()
|
||||
from apps import create_app
|
||||
# 提供一个可被 WSGI/进程管理器导入的 app 对象
|
||||
app = create_app()
|
||||