47 Commits

Author SHA1 Message Date
CakeCN
a8bccd7c3f 重构了基于websocket的SSH终端连接方案 2026-01-08 00:53:29 +08:00
CakeCN
2e1139d101 防止服务器宕机 : Socket operation on non-socket 错误不会导致服务器崩溃 2026-01-08 00:31:48 +08:00
CakeCN
eba1e8052c 创建了 create_terminal 方法;没有子进程时,自动调用 create_terminal 重新创建 2026-01-08 00:28:39 +08:00
CakeCN
6fba03ea4a 基于room的多租户WebSocket连接管理实现 2026-01-08 00:22:24 +08:00
CakeCN
09103abf38 移除了不存在的属性引用 :删除了导致错误的 eventlet.greenthread.main 相关代码 2026-01-08 00:11:31 +08:00
CakeCN
6ecd0c6735 包装了 eventlet.greenthread.main 函数,捕获所有绿色线程中的异常 2026-01-08 00:10:33 +08:00
CakeCN
a07eee1ffc fix 语音识别重复增加的问题 2026-01-08 00:05:23 +08:00
CakeCN
848b235ddc 移除了不存在的属性引用 :删除了导致错误的代码行 eventlet.debug.hub_prevent_multiple_readers(False) 2026-01-08 00:02:50 +08:00
CakeCN
7f469803df 在捕获到致命错误时,调用 self.close() 彻底关闭连接 2026-01-08 00:01:26 +08:00
CakeCN
4dd2667785 fix module 'eventlet.hubs' has no attribute 'handle_error' 2026-01-07 23:57:25 +08:00
CakeCN
f8e0b461ba fix module 'eventlet.hubs.hub' has no attribute 'Hub' 错误 2026-01-07 23:55:16 +08:00
CakeCN
607c6dfbab 有 eventlet.hubs 相关的错误都会被捕获并打印而不会向外传播导致服务器崩溃 2026-01-07 23:52:01 +08:00
CakeCN
73275cffad 每次 IO 操作之前或期间都会检查文件可用性,并在文件不可用时优雅处理 2026-01-07 20:46:32 +08:00
CakeCN
ee20e8b85b replacing the dynamic selectedItem.path+'/'+selectedItem.name with the filePath 2026-01-07 19:13:38 +08:00
CakeCN
b806da10c4 对 terminal_service.py 文件的全面检查和修复:在子进程中使用 os._exit(1) 替代 disconnect() ,避免在子进程中调用 SocketIO 方法导致的错误;修复了 SSH 硬编码端口问题 等 2026-01-07 18:57:00 +08:00
CakeCN
f818d84208 添加了对 errno 88 (Socket operation on non-socket)的处理 2026-01-07 18:52:17 +08:00
CakeCN
9231c14013 从run 中移除了全局try-except块,增强了eventlet内部错误处理 2026-01-07 01:33:25 +08:00
CakeCN
8aa567cdf2 全局异常捕获,特别是IOClosed错误,避免服务器崩溃 2026-01-07 01:06:13 +08:00
CakeCN
a3b3018de7 客户端断开连接后,文件描述符已经被关闭,但仍有事件在处理中:对 app/sockets/terminal_service.py 文件进行了全面修复 2026-01-07 00:57:27 +08:00
CakeCN
761095d61d 修复了code-server-like服务中的eventlet.hubs.IOClosed错误:当客户端断开连接时,文件描述符被关闭,但后台任务仍在尝试读取,导致eventlet核心代码抛出IOClosed错误。 2026-01-06 21:05:39 +08:00
Cai
643c16fdf7 fix:课时编辑页面步骤处保存冗余保存问题 2026-01-05 17:26:14 +08:00
Cai
5082436289 fix:index.html的课程详情页面跳转缺失 2026-01-05 16:08:55 +08:00
Cai
7193b9a000 fix:修复前端this undefined导致的无法选课问题 2026-01-05 15:50:51 +08:00
Cai
042f6ee99e restore some file 2025-12-31 10:45:42 +08:00
CakeCN
8fd88b0088 add jquery to dashboard 2025-12-31 10:35:25 +08:00
CakeCN
319b111485 pro config 2025-12-29 21:29:54 +08:00
CakeCN
f060a17f6a 生产服 .com 2025-12-29 21:10:49 +08:00
CakeCN
ecdc2e320f Merge branch 'main' of https://hsamooc.com/git/CakeCN/hsa 2025-12-29 20:53:22 +08:00
CakeCN
aa9f39ca18 Merge branch 'main' of https://hsamooc.com/git/CakeCN/hsa 2025-12-29 20:52:54 +08:00
CakeCN
f444e86136 许多更新
Merge branch 'test'
2025-12-29 20:52:03 +08:00
CakeCN
22d9fb39f1 project token change 2025-12-29 20:45:20 +08:00
68493d6a62 add:教师端新增删除课程,增加并更换footer-brief模板 2025-12-25 15:52:29 +08:00
f3c30a80a3 fix:教师端新增课程卡片中选择从课程创建的渲染错误问题(传递的是materials而不是courses) 2025-12-25 15:17:12 +08:00
f09d82571f fix:教师端新增课程的卡片自适应布局 2025-12-25 14:33:52 +08:00
CakeCN
cb435ecade fix chapter now 2025-12-14 14:33:29 +08:00
CakeCN
c8942cedac 初始为0 2025-12-14 14:22:23 +08:00
CakeCN
1f6e75f006 history 重构 2025-12-14 14:07:27 +08:00
CakeCN
5438dc99ba DEBUG 模式 2025-12-14 11:10:54 +08:00
CakeCN
f65b5bf32a try except 2025-12-14 11:05:01 +08:00
CakeCN
da93289799 eventlet green subprocess 2025-12-14 10:58:15 +08:00
CakeCN
1554c85616 event let hub_prevent_multiple_readers(False) 配置,禁用了Eventlet的同时读取检测 2025-12-14 10:56:38 +08:00
CakeCN
70410d2c4b some static bug 2025-12-14 10:32:52 +08:00
CakeCN
3c8821b225 Merge branch 'test' 2025-12-13 23:51:11 +08:00
CakeCN
ae9f7b84f4 Merge branch 'test'
修复progress Bar和提示美化
2025-12-12 16:59:58 +08:00
CakeCN
fc7c6708b4 Merge branch 'test' 2025-12-11 16:20:44 +08:00
CakeCN
a041650a54 删除app back 2025-12-11 15:00:56 +08:00
CakeCN
ad7fd04f19 add .gitattributes 2025-12-11 14:19:14 +08:00
32 changed files with 1743 additions and 1351 deletions

1
Html/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
Html/config.ini merge=ours

File diff suppressed because one or more lines are too long

View File

@@ -1,480 +0,0 @@
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)

View File

@@ -35,7 +35,6 @@ class ChatManager:
_lock = threading.RLock() _lock = threading.RLock()
def __init__(self, restart=False): def __init__(self, restart=False):
self.restart = restart self.restart = restart
self.chapter_chain_now = -1
self.ase_client = None self.ase_client = None
self.app = None self.app = None
self.socketio = None self.socketio = None
@@ -70,6 +69,9 @@ class ChatManager:
self.chapter_chain = load_chapters(raw_markdown, raw_markdown_prompts, raw_score_prompts) self.chapter_chain = load_chapters(raw_markdown, raw_markdown_prompts, raw_score_prompts)
self.bb = None self.bb = None
self.chat_historys = [] self.chat_historys = []
self.chapter_chain_now = 0
self.scores = []
def send_to_vscode(self, event: str, data: dict, room: str = None, namespace: str = '/vscode'): def send_to_vscode(self, event: str, data: dict, room: str = None, namespace: str = '/vscode'):

View File

@@ -33,10 +33,15 @@ def on_connect_to_ase(client, entry, init_data, **kwargs):
namespace_entry.emit('message', "Agents服务已连接, 加载教案中", room=init_data['room'], namespace='/agent') namespace_entry.emit('message', "Agents服务已连接, 加载教案中", room=init_data['room'], namespace='/agent')
restart = init_data.get("restart", False) restart = init_data.get("restart", False)
if restart: # 重启时,从-1章进入 0章 # 不再需要手动next_chapter因为已经在on_login中根据mongo进度恢复了正确的章节
ase_client.chatmanager.next_chapter() # if restart: # 重启时,从-1章进入 0章
# ase_client.chatmanager.next_chapter()
# 根据restart决定是否重新加载代码
ase_client.chatmanager.load_now_chapter(load_code=restart) ase_client.chatmanager.load_now_chapter(load_code=restart)
print("load_now_chapter with restart:", restart) print("load_now_chapter with restart:", restart)
# 只有当需要restart时才发送chapter-start
if restart: if restart:
ase_client.send_text("chapter-start", "") ase_client.send_text("chapter-start", "")
namespace_entry.emit('message', "教案加载完毕", room=init_data['room'], namespace='/agent') namespace_entry.emit('message', "教案加载完毕", room=init_data['room'], namespace='/agent')

View File

@@ -72,38 +72,39 @@ class AgentNamespace(Namespace):
course_id: 课程ID course_id: 课程ID
chapter_name: 章节名称 chapter_name: 章节名称
lesson_name: 课时名称 lesson_name: 课时名称
continue_learn: 是否继续学习 continue_learn: 是否继续学习现在总是为True保持兼容性
Returns: Returns:
int: 需要跳过的章节数 int: 需要跳过的章节数
""" """
need_skip_chapters = 0 # 总是从mongo加载学习进度
progress_result = load_learning_progress(user_id, course_id, chapter_name, lesson_name)
if continue_learn: # 尝试加载用户学习进度 if progress_result['exists']:
progress_result = load_learning_progress(user_id, course_id, chapter_name, lesson_name) progress = progress_result['data']
# 恢复学习进度数据
if 'scores' in progress:
chatmanager.scores = progress['scores']
if progress_result['exists']: # 恢复聊天历史
progress = progress_result['data'] if 'chat_historys' in progress:
# 恢复学习进度数据 chatmanager.chat_historys = progress['chat_historys']
if 'scores' in progress:
# 根据scores的数量确定需要跳过的章节数
need_skip_chapters = len(progress['scores'])
chatmanager.scores = progress['scores']
# 恢复聊天历史
if 'chat_historys' in progress:
chatmanager.chat_historys = progress['chat_historys']
else:
chatmanager.chat_historys = []
# 恢复当前章节链
if 'chapter_chain_now' in progress:
chatmanager.chapter_chain_now = progress['chapter_chain_now']
upload_learning_progress_to_cloud(progress)
else: else:
# 使用默认值,确保有完整的结构 chatmanager.chat_historys = []
chatmanager.chat_historys = progress_result['data']['chat_historys']
chatmanager.scores = progress_result['data']['scores'] # 恢复当前章节链
if 'chapter_chain_now' in progress:
chatmanager.chapter_chain_now = progress['chapter_chain_now']
upload_learning_progress_to_cloud(progress)
# 计算需要跳过的章节数:从初始的-1到当前的chapter_chain_now
need_skip_chapters = chatmanager.chapter_chain_now
else:
# 使用默认值,确保有完整的结构
chatmanager.chat_historys = progress_result['data']['chat_historys']
chatmanager.scores = progress_result['data']['scores']
chatmanager.chapter_chain_now = 0
need_skip_chapters = 0
return need_skip_chapters return need_skip_chapters
@@ -131,8 +132,9 @@ class AgentNamespace(Namespace):
chatmanager.function_manager.add_function(next_chapter, {'user_uuid': user_uuid}) chatmanager.function_manager.add_function(next_chapter, {'user_uuid': user_uuid})
clear_user_session(current_app.config["ASE_ENGINE_URL"], current_app.config["ASE_ENGINE_URL_TOKEN"], user_id) clear_user_session(current_app.config["ASE_ENGINE_URL"], current_app.config["ASE_ENGINE_URL_TOKEN"], user_id)
continue_learn = not chatmanager.restart
need_skip_chapters = self._load_learning_progress(chatmanager, user_id, course_id, chapter_name, lesson_name, continue_learn) # 总是从mongo加载进度不管restart状态
need_skip_chapters = self._load_learning_progress(chatmanager, user_id, course_id, chapter_name, lesson_name, True)
fmd,fmdp,fsp = load_full_markdown_file(course_id, chapter_name, lesson_name) fmd,fmdp,fsp = load_full_markdown_file(course_id, chapter_name, lesson_name)
user_uuid2ase_client[user_uuid] = HSAEngineClient(current_app.config["ASE_ENGINE_URL"], current_app.config["ASE_ENGINE_NAMESPACE"],id=user_id, chatmanager = chatmanager) user_uuid2ase_client[user_uuid] = HSAEngineClient(current_app.config["ASE_ENGINE_URL"], current_app.config["ASE_ENGINE_NAMESPACE"],id=user_id, chatmanager = chatmanager)
@@ -141,21 +143,26 @@ class AgentNamespace(Namespace):
backboard_manager.add_backboard(user_uuid, user_id, course_id, backboard_manager.add_backboard(user_uuid, user_id, course_id,
lesson_name, root_path=f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}') lesson_name, root_path=f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}')
chatmanager.bb = backboard_manager.get_backboard(user_uuid) chatmanager.bb = backboard_manager.get_backboard(user_uuid)
# 如果有保存的进度,需要跳过已经完成的章节、无需清空代码、对话历史恢复
if continue_learn: # 总是根据进度跳过章节,恢复对话历史
for _ in range(need_skip_chapters): for _ in range(need_skip_chapters):
chatmanager.next_chapter() chatmanager.next_chapter()
emit('next_chapter', room=user_uuid, namespace='/agent') emit('next_chapter', room=user_uuid, namespace='/agent')
# 恢复对话历史到前端 - 使用批量发送消息列表 # 恢复对话历史到前端 - 使用批量发送消息列表
if chatmanager.chat_historys: if chatmanager.chat_historys:
emit('messages', chatmanager.chat_historys, room=user_uuid, namespace='/agent') emit('messages', chatmanager.chat_historys, room=user_uuid, namespace='/agent')
self.chatmanager = chatmanager self.chatmanager = chatmanager
self.ase_client = user_uuid2ase_client[user_uuid] self.ase_client = user_uuid2ase_client[user_uuid]
# 将restart设置为False避免后续重复处理
restart = chatmanager.restart
chatmanager.restart = False
user_uuid2ase_client[user_uuid].register_on_connect_entry( user_uuid2ase_client[user_uuid].register_on_connect_entry(
callback=on_connect_to_ase, callback=on_connect_to_ase,
entry=(self, user_uuid2ase_client[user_uuid]), entry=(self, user_uuid2ase_client[user_uuid]),
init_data={'room':user_uuid, 'restart':chatmanager.restart} init_data={'room':user_uuid, 'restart':restart}
) )
user_uuid2ase_client[user_uuid].register_route_with_entry( user_uuid2ase_client[user_uuid].register_route_with_entry(
route='dialog', route='dialog',

View File

@@ -60,6 +60,7 @@
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
cursor: pointer; cursor: pointer;
position: relative; /* 添加相对定位 */
} }
.course-image { .course-image {
@@ -81,6 +82,27 @@
.course-description { .course-description {
font-size: 14px; font-size: 14px;
color: #555; color: #555;
margin-bottom: 10px;
display: -webkit-box; /* 使用-webkit-box实现文本截断 */
-webkit-line-clamp: 3; /* 限制显示3行 */
-webkit-box-orient: vertical; /* 垂直排列 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 用省略号表示截断 */
}
/* 课程日期信息 */
.course-date {
position: absolute; /* 绝对定位到底部 */
bottom: 10px; /* 距离底部10px */
left: 0; /* 左对齐 */
right: 0; /* 右对齐 */
padding: 10px; /* 内边距 */
background-color: white; /* 白色背景 */
border-radius: 5px; /* 圆角 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 阴影 */
font-size: 12px; /* 字体大小 */
color: #666; /* 字体颜色 */
line-height: 1.5; /* 行高 */
} }
/* 课程目录弹出框 */ /* 课程目录弹出框 */
@@ -159,11 +181,14 @@
/* 浮窗内容 */ /* 浮窗内容 */
.modal-content { .modal-content {
background-color: #fff; background-color: #fff;
margin: 15% auto; margin: 10% auto; /* 修改为10%以缩小顶部距离 */
padding: 20px; padding: 20px;
border-radius: 10px; border-radius: 10px;
width: 40%; width: 80%; /* 修改为80%以适应小屏幕 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); max-width: 600px; /* 设置最大宽度 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0 1);
overflow-y: auto; /* 添加垂直滚动条 */
max-height: 80vh; /* 设置最大高度 */
} }
/* 按钮样式 */ /* 按钮样式 */
@@ -205,7 +230,34 @@ label {
margin-top: 20px; /* 上方间距 */ margin-top: 20px; /* 上方间距 */
} }
/* 选择课程或新建课程的下拉框 */
.course-selection-container {
margin: 10px 0;
font-size: 16px;
}
#course-selection {
width: 100%; /* 占满一行 */
padding: 12px; /* 增加内边距 */
font-size: 16px; /* 调整字体大小 */
margin: 10px 0; /* 上下留间距 */
border: 2px solid #ccc; /* 边框 */
border-radius: 8px; /* 圆角边框 */
background-color: #f9f9f9; /* 背景色 */
transition: border 0.3s ease, box-shadow 0.3s ease; /* 添加过渡效果 */
}
/* 鼠标悬停时下拉框的效果 */
#course-selection:hover {
border-color: #4CAF50; /* 悬停时的边框颜色 */
box-shadow: 0 0 10px rgba(76, 175, 80, 0.5); /* 悬停时的阴影效果 */
}
/* 下拉框选中项的样式 */
#course-selection option {
padding: 10px;
font-size: 16px;
}
/* 添加课程按钮样式 */ /* 添加课程按钮样式 */
.add-course-button { .add-course-button {
@@ -433,6 +485,20 @@ label[for="course-description"] {
cursor: pointer; cursor: pointer;
} }
.delete-course-btn {
background-color: #e74c3c;
color: white;
padding: 10px;
border-radius: 5px;
cursor: pointer;
width: 100%;
margin-top: 10px;
}
.delete-course-btn:hover {
background-color: #c0392b;
}
/* 编辑弹窗 */ /* 编辑弹窗 */
.edit-modal { .edit-modal {
display: none; display: none;

View File

@@ -90,7 +90,7 @@ document.addEventListener('DOMContentLoaded', function () {
socket.on('voiceMessage', function (data) { socket.on('voiceMessage', function (data) {
console.log(data); console.log(data);
const input = document.getElementById('messageInput'); const input = document.getElementById('messageInput');
input.value += data; input.value = data;
}); });
socket.on('message', function (data) { socket.on('message', function (data) {
// 显示服务器的回复消息 - 单个消息 // 显示服务器的回复消息 - 单个消息

View File

@@ -1,41 +1,53 @@
function show_books(courses_data, user_selected_courses){ function show_books(courses_data, user_selected_courses) {
console.log(courses_data) console.log(courses_data)
console.log(user_selected_courses) console.log(user_selected_courses)
} }
function show_course_details(course_id){ function show_course_details(course_id) {
window.location.href = '/course/' + course_id window.location.href = '/course/' + course_id
} }
function on_click_select_course(event){ function on_click_select_course(event) {
event.stopPropagation(); // 阻止事件冒泡 event.stopPropagation(); // 阻止事件冒泡
const courseId = event.currentTarget.getAttribute('data-course-id'); const button = event.currentTarget;
if (event.currentTarget.classList.contains("selected-button")) {alert('课程已选择'); return;} const courseId = button.getAttribute('data-course-id');
if (!courseId) { alert('缺少课程ID'); return; }
if (button.classList.contains("selected-button")) { alert('课程已选择'); return; }
if (button.disabled) { return; }
button.disabled = true;
fetch(`/select_course`, { fetch(`/select_course`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}' // 如果使用 CSRF 保护
}, },
body: JSON.stringify({ course_id: courseId }) body: JSON.stringify({ course_id: courseId })
}) })
.then(response => response.json()) .then(async (response) => {
.then(data => { let data = null;
if (data.success) { try {
// 更新按钮样式或显示消息 data = await response.json();
this.classList.remove('select-button'); } catch (_) {
this.classList.add('selected-button'); }
this.textContent = '已选择'; if (!response.ok) {
throw new Error(data?.message || `请求失败(${response.status})`);
}
return data;
})
.then((data) => {
if (!data?.success) {
throw new Error(data?.message || '选择课程失败');
}
button.classList.remove('select-button');
button.classList.add('selected-button');
button.textContent = '已选课';
alert('选择课程成功'); alert('选择课程成功');
} else { })
alert('选择课程失败'); .catch(error => {
} console.error('Error:', error);
}) alert(error?.message || '选择课程失败');
.catch(error => { button.disabled = false;
console.error('Error:', error); });
alert('选择课程失败');
});
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -292,13 +292,50 @@ function closeAddCourseModal() {
document.getElementById('add-course-modal').style.display = 'none'; document.getElementById('add-course-modal').style.display = 'none';
} }
// 显示删除课程确认弹窗
function showDeleteCourseConfirmation() {
if (!material_id) {
alert('无法删除课程未找到课程ID');
return;
}
if (confirm('确定要删除这个课程吗?此操作无法撤销!')) {
deleteCourse(material_id);
}
}
// 删除课程
function deleteCourse(courseId) {
fetch(`/materials/${courseId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.message) {
alert(data.message);
if (data.success) {
// 关闭侧边栏并刷新页面
closeCourseDetails();
location.reload();
}
}
})
.catch(error => {
console.error('删除课程时出错:', error);
alert('删除课程失败,请稍后再试!');
});
}
function onTeacherboardPageLoad(){ function onTeacherboardPageLoad(){
// 提交表单处理 // 提交表单处理
document.getElementById('add-course-form').addEventListener('submit', function(event) { document.getElementById('add-course-form').addEventListener('submit', function(event) {
event.preventDefault(); event.preventDefault();
const courseName = document.getElementById('course-name').value; const courseName = document.getElementById('course-name').value;
const courseSelection = document.getElementById('course-selection').value; const courseSelection = document.getElementById('course-selection').value; // 获取选择值
const coverImage = document.getElementById('cover-preview').src; const coverImage = document.getElementById('cover-preview').src;
const courseDescription = document.getElementById('course-description').value; const courseDescription = document.getElementById('course-description').value;
// 构建发送的数据 // 构建发送的数据
@@ -312,7 +349,7 @@ document.getElementById('add-course-form').addEventListener('submit', function(e
// 如果选择了已有课程,可以通过 courseSelection 传递课程ID根据需要修改 // 如果选择了已有课程,可以通过 courseSelection 传递课程ID根据需要修改
if (courseSelection !== 'new') { if (courseSelection !== 'new') {
// 如果是修改已有课程,设置相关的章节信息或其他数据 // 如果是修改已有课程,设置相关的章节信息或其他数据
data.chapters = ["Chapter 1", "Chapter 2"]; // 示例章节信息 data.chapters = ["Chapter 1", "Chapter 2"];
} }
// 发送 POST 请求到 /create_material // 发送 POST 请求到 /create_material

View File

@@ -46,7 +46,7 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'foot.html' %} {% include 'footer-brief.html' %}
</body> </body>
<script> <script>
var course_id = "{{course_id}}"; var course_id = "{{course_id}}";

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>华实学伴 - 专业的在线学习平台</title> <title>华实学伴 - 专业的在线学习平台</title>
<script src="/static/cdnback/jquery.min.js"></script>
<link href="/static/cdnback/bootstrap.min.css" rel="stylesheet"> <link href="/static/cdnback/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/cdnback/all.min.css"> <link rel="stylesheet" href="/static/cdnback/all.min.css">
<link rel="stylesheet" href="/static/css/dashboard.css"> <link rel="stylesheet" href="/static/css/dashboard.css">
@@ -13,7 +14,6 @@
</head> </head>
<body> <body>
{% include 'navbar.html' %} <!-- 引入navbar.html --> {% include 'navbar.html' %} <!-- 引入navbar.html -->
{% include 'learning-path.html' %} <!-- 引入learning-path.html -->
<section class="search-section"> <section class="search-section">
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
@@ -81,7 +81,7 @@
</div> </div>
</div> </div>
{% include 'foot.html' %} {% include 'footer-brief.html' %}
<script src="/static/js/dashboard.js"></script> <script src="/static/js/dashboard.js"></script>
<script> <script>

View File

@@ -0,0 +1,11 @@
<link rel="stylesheet" href="/static/css/footer.css">
<footer class="hs-footer">
<div class="footer-main">
<div class="footer-bottom">
<div class="footer-bottom-right">
<span>© 2025 华实学伴. 保留所有权利</span>
<a href="https://beian.miit.gov.cn" target="_blank">沪ICP备2025142149号</a>
</div>
</div>
</footer>

View File

@@ -1,8 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>课程选择主页</title> <title>课程选择主页</title>
<script src="/static/cdnback/jquery.min.js"></script> <script src="/static/cdnback/jquery.min.js"></script>
<link href="/static/cdnback/bootstrap.min.css" rel="stylesheet"> <link href="/static/cdnback/bootstrap.min.css" rel="stylesheet">
@@ -12,6 +13,7 @@
<script src="/static/cdnback/js/bootstrap.bundle.min.js"></script> <script src="/static/cdnback/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css"> <link rel="stylesheet" href="/static/css/index.css">
</head> </head>
<body> <body>
{% include 'navbar.html' %} {% include 'navbar.html' %}
@@ -38,7 +40,7 @@
<h2>课程列表</h2> <h2>课程列表</h2>
<div class="course-cards" id="course-cards"> <div class="course-cards" id="course-cards">
{% for course in courses_data %} {% for course in courses_data %}
<!-- <div class="course-card" onclick="show_course_details('{{ course._id }}')"> <!-- <div class="course-card" onclick="show_course_details('{{ course._id }}')">
<img src="{{ course.image_url }}" alt="{{ course.course_name }}"> <img src="{{ course.image_url }}" alt="{{ course.course_name }}">
<h3>{{ course.name }}</h3> <h3>{{ course.name }}</h3>
<p>{{ course.description }}</p> <p>{{ course.description }}</p>
@@ -49,10 +51,9 @@
{% endif %} {% endif %}
</div> --> </div> -->
<div class="col-md-6 col-lg-4"> <div class="col-md-6 col-lg-4">
<div class="course-card card h-100" onclick="openCourseProgress('{{course._id}}')"> <div class="course-card card h-100" onclick="openCourseProgress('{{course._id}}')">
<img src="{{course.image_url}}" <img src="{{course.image_url}}" class="course-img" alt="课程封面">
class="course-img" alt="课程封面">
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
<span class="course-category">算法设计</span> <span class="course-category">算法设计</span>
<h5 class="course-title">{{course.name}}</h5> <h5 class="course-title">{{course.name}}</h5>
@@ -62,11 +63,15 @@
<span class="course-rating"><i class="fas fa-star"></i> 4.7</span> <span class="course-rating"><i class="fas fa-star"></i> 4.7</span>
</div> </div>
<div class="mt-auto d-grid gap-2 d-md-flex"> <div class="mt-auto d-grid gap-2 d-md-flex">
<button class="btn btn-view flex-fill" onclick="btn_view(event,'{{course._id}}')"><i class="far fa-eye me-1"></i> 课程</button> <button class="btn btn-view flex-fill" onclick="btn_view(event,'{{course._id}}')"><i
class="far fa-eye me-1"></i> 课程</button>
{% if course._id in selected_courses %} {% if course._id in selected_courses %}
<button class="btn btn-process flex-fill selected-button" data-course-id="{{course._id}}">已选课</button> <button class="btn btn-process flex-fill selected-button"
data-course-id="{{course._id}}">已选课</button>
{% else %} {% else %}
<button class="btn btn-process flex-fill select-button" onclick="on_click_select_course(event)" data-course-id="{{course._id}}">选择课程</button> <button class="btn btn-process flex-fill select-button"
onclick="on_click_select_course(event)"
data-course-id="{{course._id}}">选择课程</button>
{% endif %} {% endif %}
</div> </div>
@@ -80,7 +85,7 @@
</main> </main>
{% include 'foot.html' %} {% include 'footer-brief.html' %}
</body> </body>
<script> <script>
window.appData = { window.appData = {
@@ -88,5 +93,6 @@
} }
</script> </script>
<script src="/static/js/index.js"></script> <script src="/static/js/index.js"></script>
</html> <script src="/static/js/dashboard.js"></script>
</html>

View File

@@ -65,7 +65,7 @@
</div> </div>
</ul> </ul>
<button class="add-chapter-btn" onclick="showInputForNewChapter()">新增章节</button> <button class="add-chapter-btn" onclick="showInputForNewChapter()">新增章节</button>
<button class="delete-course-btn" onclick="showDeleteCourseConfirmation()">删除课程</button>
</div> </div>
<!-- 编辑章节或课时弹窗 --> <!-- 编辑章节或课时弹窗 -->
@@ -100,8 +100,8 @@
<label for="course-selection">从课程创建或新建课程:</label> <label for="course-selection">从课程创建或新建课程:</label>
<select id="course-selection"> <select id="course-selection">
<option value="new">新建课程</option> <option value="new">新建课程</option>
{% for course in courses %} {% for course in materials %}
<option value="{{ course.id }}">{{ course.name }}</option> <option value="{{ course._id }}">{{ course.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<br><br> <br><br>
@@ -121,6 +121,6 @@
<script src="/static/js/course_current.js"></script> <script src="/static/js/course_current.js"></script>
<script src="/static/js/teacherboard.js"></script> <script src="/static/js/teacherboard.js"></script>
<script src="/static/js/teacher_course_setting.js"></script> <script src="/static/js/teacher_course_setting.js"></script>
{% include 'foot.html' %} {% include 'footer-brief.html' %}
</body> </body>
</html> </html>

View File

@@ -1,4 +1,5 @@
from flask import Blueprint, request, jsonify, current_app, session, render_template, session from flask import Blueprint, request, jsonify, current_app, session, render_template, session
from bson import ObjectId
from ..services.course_service import create_material, update_material, get_materials_by_teacher, load_material, get_materials_by_teacher_dict, add_material_chapter, add_material_chapter_lesson, delete_material_chapter_lesson, reorder_material_structure, rename_material_structure from ..services.course_service import create_material, update_material, get_materials_by_teacher, load_material, get_materials_by_teacher_dict, add_material_chapter, add_material_chapter_lesson, delete_material_chapter_lesson, reorder_material_structure, rename_material_structure
from ..auth.decorators import require_role from ..auth.decorators import require_role
from ..services.cos_service import upload_file from ..services.cos_service import upload_file
@@ -37,24 +38,60 @@ def create_new_material():
material_id = create_material(teacher_id, material_name, description, chapters, image_url) material_id = create_material(teacher_id, material_name, description, chapters, image_url)
return jsonify({"message": "教材创建成功", "material_id": material_id}), 201 return jsonify({"message": "教材创建成功", "material_id": material_id}), 201
@bp.route('/materials/<material_id>', methods=['GET']) @bp.route('/materials/<material_id>', methods=['GET', 'PUT', 'DELETE'])
@require_role(roles="teacher") @require_role(roles="teacher")
def get_material(material_id): def handle_material(material_id):
if material_id is None:
return jsonify({"message": "教材名称不能为空"}), 400
material = load_material(material_id)
print(material)
return jsonify( material.model_dump()), 200
if request.method == 'GET':
if material_id is None:
return jsonify({"message": "教材名称不能为空"}), 400
material = load_material(material_id)
print(material)
return jsonify(material.model_dump()), 200
@bp.route('/materials/<material_id>', methods=['PUT']) elif request.method == 'PUT':
@require_role(roles="teacher") data = request.get_json()
def update_existing_material(material_id): chapters = data.get('chapters')
data = request.get_json() if update_material(material_id, chapters):
chapters = data.get('chapters') return jsonify({"message": "教材更新成功"}), 200
if update_material(material_id, chapters): else:
return jsonify({"message": "教材更新成功"}), 200 return jsonify({"message": "教材更新失败"}), 500
return jsonify({"message": "教材未找到"}), 404
elif request.method == 'DELETE':
# 获取当前教师信息
teacher_uuid = session.get("user_uuid")
teacher_id = current_app.extensions["uuid2username"][teacher_uuid]
# 检查材料是否存在,并且是否为当前教师创建
mongo = current_app.extensions["mongo"]
print(f"尝试删除课程ID: {material_id}, 当前教师ID: {teacher_id}")
try:
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
print(f"找到的材料: {material}")
if not material:
return jsonify({"message": "课程不存在", "success": False}), 404
# 验证是否为当前教师创建的课程
if material.get('teacher_id') != teacher_id:
return jsonify({"message": "权限不足,无法删除他人课程", "success": False}), 403
except Exception as e:
print(f"查询材料时出错: {e}")
return jsonify({"message": f"查询课程时出错: {str(e)}", "success": False}), 500
# 执行删除操作
try:
result = mongo.db.materials.delete_one({'_id': ObjectId(material_id), 'teacher_id': teacher_id})
print(f"删除操作结果: {result.deleted_count}")
except Exception as e:
print(f"删除材料时出错: {e}")
return jsonify({"message": f"删除课程时出错: {str(e)}", "success": False}), 500
if result.deleted_count > 0:
return jsonify({"message": "课程删除成功", "success": True}), 200
else:
return jsonify({"message": "删除失败", "success": False}), 500
@bp.route('/materials/addchapter/<material_id>', methods=['POST']) @bp.route('/materials/addchapter/<material_id>', methods=['POST'])
@require_role(roles="teacher") @require_role(roles="teacher")

View File

@@ -69,8 +69,23 @@ def desktop(user_uuid, course_id, chapter_name, lesson_name):
chatmanager = ChatManager(restart=not load_history) restart = not load_history
chatmanager = ChatManager(restart=restart)
current_app.extensions["user_uuid2chatmanager"][user_uuid] = chatmanager current_app.extensions["user_uuid2chatmanager"][user_uuid] = chatmanager
# 如果restart为True删除mongo中的学习进度
if restart:
try:
mongo = current_app.extensions["mongo"]
mongo.db.learning_progress.delete_one({
"user_id": user_id,
"material_id": course_id,
"chapter_name": chapter_name,
"lesson_name": lesson_name
})
current_app.logger.info(f"删除学习进度成功: user_id={user_id}, course_id={course_id}, chapter={chapter_name}, lesson={lesson_name}")
except Exception as e:
current_app.logger.error(f"删除学习进度失败: {str(e)}")
code_server_port = 10000 + int(uuid.uuid4().int % 10000) code_server_port = 10000 + int(uuid.uuid4().int % 10000)
# chatmanager.start_code_server(user_uuid, user_id, path_dir, code_server_port) # chatmanager.start_code_server(user_uuid, user_id, path_dir, code_server_port)

View File

@@ -5,7 +5,7 @@ api_key = sk-aMpnWklN2IbsK44d1kNpy6YOP9bk1pdPJjFeEmbb0a5ytEFf
model=gpt-4.1-nano model=gpt-4.1-nano
[VSCODE_WEB] [VSCODE_WEB]
url = https://hsamooc.cn url = https://hsamooc.com
[CODE_LIKE] [CODE_LIKE]
url = /vsc-like url = /vsc-like
#http://asengine.net:8282 #http://asengine.net:8282
@@ -27,6 +27,6 @@ bucket = hsamooc-cdn-1374354408
region = ap-guangzhou region = ap-guangzhou
[ASE_ENGINE] [ASE_ENGINE]
url = https://test.asengine.net url = https://asengine.net
namespace = /socket.io/7100eebf7a909e1ee3f4e06766f512f0358ecb6ac51d5434c17d31dd70890dc1 namespace = /socket.io/afe58a9e4c03e10fae6fae5985e8adde23a49635
url_token = 7100eebf7a909e1ee3f4e06766f512f0358ecb6ac51d5434c17d31dd70890dc1 url_token = afe58a9e4c03e10fae6fae5985e8adde23a49635

View File

@@ -0,0 +1 @@
{"username": "_10235101560"}

View File

@@ -0,0 +1 @@
{"username": "shanks"}

View File

@@ -0,0 +1 @@
{"username": "ts88"}

View File

@@ -0,0 +1 @@
{"username": "xuans"}

View File

@@ -0,0 +1 @@
{"username": "xuans_"}

View File

@@ -1,5 +1,210 @@
import eventlet import eventlet
eventlet.monkey_patch() # 进行猴子补丁操作 import logging
# 配置 eventlet 日志,抑制 IOClosed 等错误日志
logging.getLogger('eventlet.hubs').setLevel(logging.CRITICAL)
logging.getLogger('eventlet.greenio').setLevel(logging.CRITICAL)
# 进行猴子补丁操作
eventlet.monkey_patch()
# 配置 eventlet 以更优雅地处理错误,避免疯狂报错
try:
import eventlet
import traceback
# 1. 首先,禁用 eventlet 的调试日志,减少输出
import logging
logging.getLogger('eventlet').setLevel(logging.CRITICAL)
logging.getLogger('socketio').setLevel(logging.CRITICAL)
logging.getLogger('werkzeug').setLevel(logging.CRITICAL)
# 2. 配置 eventlet 相关设置移除了不存在的debug属性
# 3. 避免重复打印相同错误的机制
seen_errors = set()
max_error_logs = 5 # 每个错误最多打印5次
error_counts = {}
def is_duplicate_error(e):
"""检查是否为重复错误"""
error_key = f"{type(e).__name__}: {e}"
if error_key in seen_errors:
error_counts[error_key] = error_counts.get(error_key, 0) + 1
if error_counts[error_key] > max_error_logs:
return True
else:
seen_errors.add(error_key)
error_counts[error_key] = 1
return False
# 4. 只修改关键的 greenio 方法,确保在致命错误时彻底关闭连接
# 增强 socketio 错误处理,确保不会因为单个连接错误而宕机
try:
from flask_socketio import SocketIO
# 获取当前 socketio 实例并配置错误处理
import sys
original_excepthook = sys.excepthook
def custom_excepthook(exc_type, exc_value, exc_traceback):
"""全局异常钩子,捕获所有未处理的异常"""
error_str = str(exc_value)
if exc_type.__name__ in ['OSError', 'IOError'] and any(msg in error_str for msg in ['Bad file descriptor', 'Socket operation on non-socket', 'Operation on closed file']):
# 处理各种socket错误不导致服务器宕机
if not is_duplicate_error(exc_value):
print(f"[Global Error Handler] {exc_type.__name__}: {exc_value}")
print("[Traceback]")
traceback.print_exception(exc_type, exc_value, exc_traceback)
# 不调用原始的 excepthook防止服务器宕机
return
# 其他异常调用原始的 excepthook
original_excepthook(exc_type, exc_value, exc_traceback)
# 设置全局异常钩子
sys.excepthook = custom_excepthook
except Exception as e:
# 如果修改失败,忽略错误
print(f"[Global Excepthook Patch Error] {e}")
from eventlet import greenio
# 保存并替换原始的 greenio 方法
if hasattr(greenio.base.GreenSocket, '_recv_loop'):
original_recv_loop = greenio.base.GreenSocket._recv_loop
def custom_recv_loop(self, recv_func, recv_args, *args, **kwargs):
try:
return original_recv_loop(self, recv_func, recv_args, *args, **kwargs)
except Exception as e:
if not is_duplicate_error(e):
print(f"[Eventlet IO Error] _recv_loop: {type(e).__name__}: {e}")
print("[Traceback]")
traceback.print_exc()
# 彻底关闭连接,不再继续处理
try:
self.close()
except Exception:
pass
return b''
greenio.base.GreenSocket._recv_loop = custom_recv_loop
if hasattr(greenio.base.GreenSocket, 'recv_into'):
original_recv_into = greenio.base.GreenSocket.recv_into
def custom_recv_into(self, buffer, nbytes=0, flags=0):
try:
return original_recv_into(self, buffer, nbytes, flags)
except Exception as e:
if not is_duplicate_error(e):
print(f"[Eventlet IO Error] recv_into: {type(e).__name__}: {e}")
print("[Traceback]")
traceback.print_exc()
# 彻底关闭连接,不再继续处理
try:
self.close()
except Exception:
pass
return 0
greenio.base.GreenSocket.recv_into = custom_recv_into
if hasattr(greenio.base.GreenSocket, '_send_loop'):
original_send_loop = greenio.base.GreenSocket._send_loop
def custom_send_loop(self, send_func, data, *args, **kwargs):
try:
return original_send_loop(self, send_func, data, *args, **kwargs)
except Exception as e:
if not is_duplicate_error(e):
print(f"[Eventlet IO Error] _send_loop: {type(e).__name__}: {e}")
print("[Traceback]")
traceback.print_exc()
# 彻底关闭连接,不再继续处理
try:
self.close()
except Exception:
pass
return 0
greenio.base.GreenSocket._send_loop = custom_send_loop
if hasattr(greenio.base.GreenSocket, 'send'):
original_send = greenio.base.GreenSocket.send
def custom_send(self, data, flags=0):
try:
return original_send(self, data, flags)
except Exception as e:
if not is_duplicate_error(e):
print(f"[Eventlet IO Error] send: {type(e).__name__}: {e}")
print("[Traceback]")
traceback.print_exc()
# 彻底关闭连接,不再继续处理
try:
self.close()
except Exception:
pass
return 0
greenio.base.GreenSocket.send = custom_send
# 5. 处理 WSGI 中的错误,确保彻底清理
try:
import eventlet.wsgi
if hasattr(eventlet.wsgi.HttpProtocol, 'handle_one_request'):
original_wsgi_handle_one_request = eventlet.wsgi.HttpProtocol.handle_one_request
def custom_wsgi_handle_one_request(self):
try:
return original_wsgi_handle_one_request(self)
except Exception as e:
if not is_duplicate_error(e):
print(f"[Eventlet WSGI Request Error] {type(e).__name__}: {e}")
print("[Traceback]")
traceback.print_exc()
# 彻底清理连接,不再继续
try:
self.rfile.close()
self.wfile.close()
self.finish()
except Exception:
pass
return
eventlet.wsgi.HttpProtocol.handle_one_request = custom_wsgi_handle_one_request
if hasattr(eventlet.wsgi.HttpProtocol, 'handle_one_response'):
original_wsgi_handle_one_response = eventlet.wsgi.HttpProtocol.handle_one_response
def custom_wsgi_handle_one_response(self):
try:
return original_wsgi_handle_one_response(self)
except Exception as e:
if not is_duplicate_error(e):
print(f"[Eventlet WSGI Response Error] {type(e).__name__}: {e}")
print("[Traceback]")
traceback.print_exc()
# 彻底清理连接,不再继续
try:
self.rfile.close()
self.wfile.close()
self.finish()
except Exception:
pass
return
eventlet.wsgi.HttpProtocol.handle_one_response = custom_wsgi_handle_one_response
except Exception as e:
# 如果修改失败,忽略错误
print(f"[WSGI Patch Error] {e}")
print("[Eventlet Error Handling] Configured to handle errors gracefully, avoiding duplicate logs")
except Exception as e:
# 如果修改失败,打印错误但继续运行
print(f"[Eventlet Patch Initialization Error] {e}")
traceback.print_exc()
from flask import Flask from flask import Flask
from .views.routes import main_bp from .views.routes import main_bp
from .views.file import file_bp from .views.file import file_bp

View File

@@ -13,4 +13,5 @@ class Config:
# 自定义其他全局配置 # 自定义其他全局配置
ROOT_WORKSPACE_PATH = GLOBAL_CONFIG['Global']['ROOT_WORKSPACE_PATH'] ROOT_WORKSPACE_PATH = GLOBAL_CONFIG['Global']['ROOT_WORKSPACE_PATH']
DEBUG = GLOBAL_CONFIG['Global'].getboolean('DEBUG', False)

View File

@@ -14,6 +14,7 @@ import termios
import struct import struct
import fcntl import fcntl
import psutil import psutil
TERM_INIT_CONFIG = { TERM_INIT_CONFIG = {
# instead of local server runnning this web terminal service # instead of local server runnning this web terminal service
# "domain" is the target that you want to access through local server (with this web terminal) # "domain" is the target that you want to access through local server (with this web terminal)
@@ -24,40 +25,153 @@ TERM_INIT_CONFIG = {
'ssh': '/usr/bin/ssh' 'ssh': '/usr/bin/ssh'
} }
} }
terminal_bp = Blueprint('terminal', __name__, url_prefix='/vsc-like') terminal_bp = Blueprint('terminal', __name__, url_prefix='/vsc-like')
class TerminalSessionManager:
"""终端会话管理器,用于统一管理所有终端会话"""
def __init__(self):
# 存储所有终端会话key为session_idvalue为会话配置
self.sessions = {}
# 线程锁,保证多线程安全
self.lock = threading.Lock()
# 最大终端数量限制
self.max_terminals = 100
# 终端空闲超时时间(秒)
self.idle_timeout = 3600 # 1小时
def create_session(self):
"""创建新的终端会话"""
with self.lock:
# 检查是否达到最大终端数量
if len(self.sessions) >= self.max_terminals:
return None, "Maximum number of terminals reached"
# 生成唯一的会话ID和房间ID
session_id = str(uuid.uuid4())
room_id = f"terminal_{session_id}"
# 创建会话配置
session_config = {
**TERM_INIT_CONFIG.copy(),
'session_id': session_id,
'room_id': room_id,
'created_at': time.time(),
'last_activity': time.time()
}
# 保存会话
self.sessions[session_id] = session_config
return session_config, None
def get_session(self, session_id):
"""获取会话配置"""
with self.lock:
return self.sessions.get(session_id)
def update_session(self, session_id, updates):
"""更新会话配置"""
with self.lock:
if session_id not in self.sessions:
return False
# 更新会话配置
self.sessions[session_id].update(updates)
# 更新最后活动时间
self.sessions[session_id]['last_activity'] = time.time()
return True
def delete_session(self, session_id):
"""删除会话"""
with self.lock:
if session_id in self.sessions:
del self.sessions[session_id]
return True
return False
def get_session_by_room_id(self, room_id):
"""通过房间ID获取会话"""
with self.lock:
for session_config in self.sessions.values():
if session_config.get('room_id') == room_id:
return session_config
return None
def cleanup_inactive_sessions(self):
"""清理不活动的会话"""
with self.lock:
current_time = time.time()
expired_sessions = []
# 找出超时的会话
for session_id, session_config in self.sessions.items():
if current_time - session_config['last_activity'] > self.idle_timeout:
expired_sessions.append(session_id)
# 删除超时的会话
for session_id in expired_sessions:
del self.sessions[session_id]
return expired_sessions
def get_active_session_count(self):
"""获取当前活动会话数量"""
with self.lock:
return len(self.sessions)
# 创建全局终端会话管理器实例
terminal_manager = TerminalSessionManager()
def set_winsize(fd, row, col, xpix=0, ypix=0): def set_winsize(fd, row, col, xpix=0, ypix=0):
winsize = struct.pack("HHHH", row, col, xpix, ypix) try:
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize) winsize = struct.pack("HHHH", row, col, xpix, ypix)
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
except (OSError, IOError):
# File descriptor closed or invalid, do nothing
pass
def read_and_forward_pty_output(fd=None, pid=None, room_id=None,namespace=None): def read_and_forward_pty_output(fd=None, pid=None, room_id=None, namespace=None):
""" """
read data on pty master from the pty slave, and emit to the web terminal visitor read data on pty master from the pty slave, and emit to the web terminal visitor
""" """
max_read_bytes = 1024 * 20 max_read_bytes = 1024 * 20
timeout=0.1 # 初始超时时间设置为较短值,确保响应迅速
timeout = 0.05
# 最大超时时间,避免太频繁的检查
max_timeout = 0.5
# 成功读取数据后的重置超时时间
reset_timeout = 0.05
try: try:
while True: while True:
# 等待一段时间减少CPU占用
socketio.sleep(timeout) socketio.sleep(timeout)
timeout=min(timeout*2, 0.4)
# using flask default web server, or uwsgi production web server # 使用指数退避算法调整超时时间,但不超过最大值
# when the child process is terminated, it will not disappear from linux process list timeout = min(timeout * 1.5, max_timeout)
# and keep staying as a zombie process until the parent exits.
# 检查子进程状态
try: try:
child_process = psutil.Process(pid) child_process = psutil.Process(pid)
except psutil.NoSuchProcess as err: except psutil.NoSuchProcess:
# Process already terminated, clean up any zombie # 进程已终止,清理僵尸进程
try: try:
os.waitpid(pid, os.WNOHANG) os.waitpid(pid, os.WNOHANG)
except Exception: except Exception:
pass pass
return return
# 检查进程状态,如果不是运行或睡眠状态,则退出
if child_process.status() not in ('running', 'sleeping'): if child_process.status() not in ('running', 'sleeping'):
# Process is terminated or in other state, clean up
try: try:
# 等待进程终止
child_process.wait(timeout=1) child_process.wait(timeout=1)
except Exception: except Exception:
try: try:
@@ -65,25 +179,43 @@ def read_and_forward_pty_output(fd=None, pid=None, room_id=None,namespace=None):
except Exception: except Exception:
pass pass
return return
# 如果文件描述符有效,尝试读取数据
if fd: if fd:
timeout_sec = 0 try:
(data_ready, _, _) = select.select([fd], [], [], timeout_sec) # 使用非阻塞select检查是否有数据可读
if data_ready: (data_ready, _, _) = select.select([fd], [], [], 0)
# output = os.read(fd, max_read_bytes).decode('ascii') if data_ready:
timeout=0.1 # 有数据可读,重置超时时间
try: timeout = reset_timeout
output = os.read(fd, max_read_bytes).decode() try:
except Exception as err: # 读取数据
output = """ output = os.read(fd, max_read_bytes).decode()
***AQUI WEB TERM ERR*** except (OSError, IOError, EOFError):
{} # 文件描述符已关闭或其他IO错误优雅退出
*********************** return
""".format(err) except UnicodeDecodeError as err:
# the key for different visitor to get different terminal (instead of mixing up) # 处理编码错误
# is to let the background task push pty response to each one's own (default) ROOM! output = f"""
namespace.emit("pty_output", {"output": output}, room=room_id) ***AQUI WEB TERM ERR***
Unicode decode error: {err}
***********************
"""
# 发送数据到客户端
try:
namespace.emit("pty_output", {"output": output}, room=room_id)
except Exception:
# 如果发送失败,客户端可能已断开连接,退出
return
except (OSError, IOError):
# 文件描述符无效,优雅退出
return
except Exception as e:
# 捕获任何其他未预期的异常,防止服务器崩溃
current_app.logger.error(f"Unexpected error in read_and_forward_pty_output: {e}")
finally: finally:
# Clean up file descriptor if it's open # 清理文件描述符
if fd: if fd:
try: try:
os.close(fd) os.close(fd)
@@ -91,93 +223,327 @@ def read_and_forward_pty_output(fd=None, pid=None, room_id=None,namespace=None):
pass pass
class VSCLikeNameSpace(Namespace): class VSCLikeNameSpace(Namespace):
def on_connect(self): def __init__(self, namespace=None):
"""new client connected""" super().__init__(namespace)
if session.get('terminal_config', {}).get('child_pid', None): # 存储客户端socket_id到session_id的映射
print(session['terminal_config']['child_pid']) self.socket_session_map = {}
# already started child process, don't start another # 线程锁,保证多线程安全
return self.lock = threading.Lock()
def create_terminal(self, session_id):
"""创建新的终端并更新会话配置"""
# 获取会话配置
terminal_config = terminal_manager.get_session(session_id)
if not terminal_config:
current_app.logger.error(f"Session not found: {session_id}")
return None
room_id = terminal_config['room_id']
current_app.logger.debug(f"Creating new terminal for room: {room_id}")
# create child process attached to a pty we can read from and write to # create child process attached to a pty we can read from and write to
(child_pid, fd) = pty.fork() (child_pid, fd) = pty.fork()
lesson_path = session.get('path')
if child_pid == 0: if child_pid == 0:
# this is the child process fork. # this is the child process fork.
# anything printed here will show up in the pty, including the output # anything printed here will show up in the pty, including the output
# of this subprocess # of this subprocess
# subprocess.run('bash') try:
term_type = session.get('terminal_config').get('term_type') # 获取终端配置
path = TERM_INIT_CONFIG.get('client_path', {}).get(term_type, None) term_type = terminal_config.get('term_type')
if not path:
print("Can't locate {} binary, exit".format(term_type))
disconnect()
if term_type == 'telnet':
# switch to the right location of your telnet binary (example comes from OSX which got telnet from brew)
# or you can also make work like auto-detection, or manually but configurable
os.execl(path, 'telnet', '-l', session['terminal_config']['username'],
session['terminal_config']['domain'], '{}'.format(session['terminal_config']['port']))
elif term_type == 'ssh':
os.execl(path,'ssh', '-p','22',
#'{}'.format(session['terminal_config']['port']),
'{}@{}'.format(session['terminal_config']['username'], session['terminal_config']['domain']))
if not term_type:
print("Terminal type not specified, exit")
os._exit(1)
else: path = terminal_config.get('client_path', {}).get(term_type, None)
current_app.logger.debug("wrong term type {}".format(term_type)) if not path or not os.path.exists(path):
disconnect() print(f"Can't locate {term_type} binary at {path}, exit")
session['terminal_config'] = TERM_INIT_CONFIG os._exit(1)
# 获取连接参数
username = terminal_config.get('username')
domain = terminal_config.get('domain')
port = terminal_config.get('port')
if not username or not domain:
print("Missing required connection parameters, exit")
os._exit(1)
if term_type == 'telnet':
# 使用telnet连接
if not port:
print("Port not specified for telnet, exit")
os._exit(1)
os.execl(path, 'telnet', '-l', username, domain, str(port))
elif term_type == 'ssh':
# 使用ssh连接
ssh_port = port if port else 22 # 默认端口22
os.execl(path, 'ssh', '-p', str(ssh_port), f'{username}@{domain}')
else:
print(f"Wrong term type {term_type}, exit")
os._exit(1)
except Exception as e:
print(f"Error in child process: {e}")
os._exit(1)
else: else:
session['terminal_config']['fd'] = fd # 更新会话配置,保存到终端管理器
session['terminal_config']['child_pid'] = child_pid updates = {
session['terminal_config']['room_id'] = rooms()[0] 'fd': fd,
session.modified = True 'child_pid': child_pid
}
terminal_manager.update_session(session_id, updates)
# 设置初始窗口大小
set_winsize(fd, 50, 50) set_winsize(fd, 50, 50)
current_app.logger.debug("child pid = {}".format(child_pid))
current_app.logger.debug("rooms of this session = {}".format(rooms())) # 记录日志
socketio.start_background_task(read_and_forward_pty_output, fd, child_pid, rooms()[0],self) current_app.logger.debug(f"Child process created: {child_pid} for room: {room_id}")
current_app.logger.debug("background task running")
# print(session) # 启动后台任务读取pty输出传入正确的room_id
socketio.start_background_task(read_and_forward_pty_output, fd, child_pid, room_id, self)
current_app.logger.debug("Background task running")
return terminal_config
def on_connect(self):
"""new client connected"""
# 创建新的终端会话
terminal_config, error = terminal_manager.create_session()
if not terminal_config:
current_app.logger.error(f"Failed to create terminal session: {error}")
return
session_id = terminal_config['session_id']
room_id = terminal_config['room_id']
# 加入房间
join_room(room_id)
# 保存socket_id到session_id的映射
socket_id = request.sid
with self.lock:
self.socket_session_map[socket_id] = session_id
current_app.logger.debug(f"Client connected, session_id: {session_id}, room_id: {room_id}, socket_id: {socket_id}")
# 创建终端
self.create_terminal(session_id)
current_app.logger.debug("background task running")
def on_pty_input(self, data): def on_pty_input(self, data):
"""write to the child pty, which now is the ssh process from this machine to the 'domain' configured """write to the child pty, which now is the ssh process from this machine to the 'domain' configured
""" """
print(f"get data {data}") # 获取socket_id对应的session_id
socket_id = request.sid
with self.lock:
session_id = self.socket_session_map.get(socket_id)
if not session_id:
current_app.logger.error(f"No session found for socket: {socket_id}")
return
# 输入验证
if not isinstance(data, dict):
current_app.logger.error("Invalid input format: expected dictionary")
return
input_data = data.get("input")
if not isinstance(input_data, str):
current_app.logger.error("Invalid input data: expected string")
return
# 限制输入长度,防止缓冲区溢出
if len(input_data) > 1024:
input_data = input_data[:1024]
current_app.logger.warning("Input truncated due to length")
try: try:
child_process = psutil.Process(session.get('terminal_config').get('child_pid')) terminal_config = terminal_manager.get_session(session_id)
except psutil.NoSuchProcess as err: if not terminal_config:
disconnect() current_app.logger.error(f"Session not found: {session_id}")
session['terminal_config'] = TERM_INIT_CONFIG return
return
if child_process.status() not in ('running', 'sleeping'): child_pid = terminal_config.get('child_pid')
disconnect()
session['terminal_config'] = TERM_INIT_CONFIG # 如果没有子进程,重新创建终端
return if not child_pid:
# print(session) current_app.logger.debug(f"No child process found for session: {session_id}, creating new terminal")
# print(data, 'from input') terminal_config = self.create_terminal(session_id)
fd = session.get('terminal_config').get('fd') if not terminal_config:
if fd: current_app.logger.error(f"Failed to create new terminal for session: {session_id}")
# print("writing to ptd: %s" % data["input"]) return
# os.write(fd, data["input"].encode('ascii')) child_pid = terminal_config.get('child_pid')
os.write(fd, data["input"].encode()) if not child_pid:
current_app.logger.error(f"Failed to get child_pid for session: {session_id}")
return
# 检查子进程是否存在且运行中
try:
child_process = psutil.Process(child_pid)
if child_process.status() not in ('running', 'sleeping'):
current_app.logger.debug(f"Child process not running for session: {session_id}, creating new terminal")
terminal_config = self.create_terminal(session_id)
if not terminal_config:
current_app.logger.error(f"Failed to create new terminal for session: {session_id}")
return
child_pid = terminal_config.get('child_pid')
if not child_pid:
current_app.logger.error(f"Failed to get child_pid for session: {session_id}")
return
except psutil.NoSuchProcess:
current_app.logger.debug(f"Child process not found for session: {session_id}, creating new terminal")
terminal_config = self.create_terminal(session_id)
if not terminal_config:
current_app.logger.error(f"Failed to create new terminal for session: {session_id}")
return
child_pid = terminal_config.get('child_pid')
if not child_pid:
current_app.logger.error(f"Failed to get child_pid for session: {session_id}")
return
# 获取文件描述符并写入数据
fd = terminal_config.get('fd')
if fd:
try:
os.write(fd, input_data.encode())
except (OSError, IOError) as e:
# File descriptor closed or invalid, create new terminal
current_app.logger.debug(f"Error writing to file descriptor: {e}, creating new terminal for session: {session_id}")
self.create_terminal(session_id)
except Exception as e:
current_app.logger.error(f"Error in pty_input handling: {e}")
# 发生异常时尝试重新创建终端
try:
self.create_terminal(session_id)
except Exception as create_err:
current_app.logger.error(f"Failed to recreate terminal: {create_err}")
def on_resize(self, data): def on_resize(self, data):
# 获取socket_id对应的session_id
socket_id = request.sid
with self.lock:
session_id = self.socket_session_map.get(socket_id)
if not session_id:
current_app.logger.error(f"No session found for socket: {socket_id}")
return
# 输入验证
if not isinstance(data, dict):
current_app.logger.error("Invalid resize data format: expected dictionary")
return
rows = data.get("rows")
cols = data.get("cols")
# 验证行和列的值是否为正整数
if not (isinstance(rows, int) and isinstance(cols, int)):
current_app.logger.error("Invalid resize dimensions: expected integers")
return
# 限制窗口大小范围,防止异常值
if rows <= 0 or rows > 1000 or cols <= 0 or cols > 1000:
current_app.logger.error(f"Invalid resize dimensions: {rows}x{cols} (out of range)")
return
try: try:
child_process = psutil.Process(session.get('terminal_config').get('child_pid')) terminal_config = terminal_manager.get_session(session_id)
except psutil.NoSuchProcess as err: if not terminal_config:
disconnect() current_app.logger.error(f"Session not found: {session_id}")
session['terminal_config'] = TERM_INIT_CONFIG return
return
if child_process.status() not in ('running', 'sleeping'): child_pid = terminal_config.get('child_pid')
disconnect()
session['terminal_config'] = TERM_INIT_CONFIG # 如果没有子进程,重新创建终端
return if not child_pid:
fd = session.get('terminal_config').get('fd') current_app.logger.debug(f"No child process found for resize, creating new terminal for session: {session_id}")
if fd: terminal_config = self.create_terminal(session_id)
set_winsize(fd, data["rows"], data["cols"]) if not terminal_config:
current_app.logger.error(f"Failed to create new terminal for resize, session: {session_id}")
return
child_pid = terminal_config.get('child_pid')
if not child_pid:
current_app.logger.error(f"Failed to get child_pid for resize, session: {session_id}")
return
# 检查子进程是否存在且运行中
try:
child_process = psutil.Process(child_pid)
if child_process.status() not in ('running', 'sleeping'):
current_app.logger.debug(f"Child process not running for resize, creating new terminal for session: {session_id}")
terminal_config = self.create_terminal(session_id)
if not terminal_config:
current_app.logger.error(f"Failed to create new terminal for resize, session: {session_id}")
return
child_pid = terminal_config.get('child_pid')
if not child_pid:
current_app.logger.error(f"Failed to get child_pid for resize, session: {session_id}")
return
except psutil.NoSuchProcess:
current_app.logger.debug(f"Child process not found for resize, creating new terminal for session: {session_id}")
terminal_config = self.create_terminal(session_id)
if not terminal_config:
current_app.logger.error(f"Failed to create new terminal for resize, session: {session_id}")
return
child_pid = terminal_config.get('child_pid')
if not child_pid:
current_app.logger.error(f"Failed to get child_pid for resize, session: {session_id}")
return
# 获取文件描述符并调整窗口大小
fd = terminal_config.get('fd')
if fd:
try:
os.fstat(fd)
set_winsize(fd, rows, cols)
except (OSError, IOError) as e:
# 文件描述符无效,创建新终端
current_app.logger.debug(f"Error resizing terminal: {e}, creating new terminal for session: {session_id}")
self.create_terminal(session_id)
except Exception as e:
current_app.logger.error(f"Error in resize handling: {e}")
# 发生异常时尝试重新创建终端
try:
self.create_terminal(session_id)
except Exception as create_err:
current_app.logger.error(f"Failed to recreate terminal for resize: {create_err}")
def on_disconnect(self): def on_disconnect(self):
child_pid = session.get('terminal_config', {}).get('child_pid') # 获取socket_id对应的session_id
socket_id = request.sid
with self.lock:
session_id = self.socket_session_map.pop(socket_id, None)
if not session_id:
current_app.logger.error(f"No session found for socket: {socket_id}")
return
# 获取会话配置
terminal_config = terminal_manager.get_session(session_id)
if not terminal_config:
current_app.logger.error(f"Session not found: {session_id}")
return
child_pid = terminal_config.get('child_pid')
fd = terminal_config.get('fd')
room_id = terminal_config.get('room_id')
current_app.logger.debug(f"Client disconnecting, session_id: {session_id}, room_id: {room_id}, socket_id: {socket_id}")
# 退出房间
if room_id:
leave_room(room_id)
current_app.logger.debug(f"Client left room: {room_id}")
# 先关闭文件描述符,避免其他操作尝试使用无效的文件描述符
if fd:
try:
os.close(fd)
current_app.logger.debug(f"Closed file descriptor: {fd}")
except Exception as e:
current_app.logger.debug(f"Error closing file descriptor: {e}")
if child_pid: if child_pid:
try: try:
child_process = psutil.Process(child_pid) child_process = psutil.Process(child_pid)
@@ -187,30 +553,28 @@ class VSCLikeNameSpace(Namespace):
child_process.terminate() child_process.terminate()
# Wait for the process to terminate and collect its exit status # Wait for the process to terminate and collect its exit status
child_process.wait(timeout=2) child_process.wait(timeout=2)
current_app.logger.debug('user left the pty alone, terminated and waited') current_app.logger.debug(f'User left pty alone, terminated and waited: {child_pid}')
except psutil.NoSuchProcess as err: except psutil.NoSuchProcess:
# Process already terminated, try to wait anyway to clean up any zombie # Process already terminated, try to wait anyway to clean up any zombie
try: try:
os.waitpid(child_pid, os.WNOHANG) os.waitpid(child_pid, os.WNOHANG)
except Exception: current_app.logger.debug(f'Cleaned up zombie process: {child_pid}')
pass except Exception as e:
current_app.logger.debug(f"Error cleaning up zombie process: {e}")
except psutil.TimeoutExpired: except psutil.TimeoutExpired:
# If process didn't terminate in time, kill it forcefully # If process didn't terminate in time, kill it forcefully
child_process.kill()
try: try:
child_process.kill()
child_process.wait(timeout=1) child_process.wait(timeout=1)
except Exception: current_app.logger.debug(f'Forcefully killed process: {child_pid}')
pass except Exception as e:
current_app.logger.debug(f"Error forcefully killing process: {e}")
except Exception as err: except Exception as err:
current_app.logger.error(f'Error terminating process: {err}') current_app.logger.error(f'Error terminating process: {err}')
finally:
# Close the file descriptor if it's open # 从终端管理器中删除会话
fd = session.get('terminal_config', {}).get('fd') terminal_manager.delete_session(session_id)
if fd: current_app.logger.debug(f"Session deleted: {session_id}")
try:
os.close(fd) current_app.logger.debug(f"Client disconnected, session_id: {session_id}, room_id: {room_id}")
except Exception:
pass
# Reset session config
session['terminal_config'] = TERM_INIT_CONFIG
current_app.logger.debug('Client disconnected') current_app.logger.debug('Client disconnected')

View File

@@ -1,5 +1,5 @@
import os import os
import subprocess from eventlet.green import subprocess
from flask import Blueprint, request, jsonify, current_app, render_template, session from flask import Blueprint, request, jsonify, current_app, render_template, session
from ..services.file_service import get_file_tree, load_config from ..services.file_service import get_file_tree, load_config
@@ -42,32 +42,34 @@ def vsc_like(user_uuid, user_id, course_id, chapter_name, lesson_name, port):
subprocess.run(["sudo", "chown", "-R", user_id, user_root]) # 设置目录权限为新用户 subprocess.run(["sudo", "chown", "-R", user_id, user_root]) # 设置目录权限为新用户
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Error creating user {user_id}: {e}") # 用户可能已经存在,这是正常情况,继续执行
print(f"Useradd command returned non-zero exit status, this is expected if user already exists: {e}")
try: try:
# 使用 sudo 执行 mkdir 命令来创建目录 # 使用 sudo 执行 mkdir 命令来创建目录
subprocess.run(["sudo", "-u", user_id, "mkdir", "-p", path], check=True) subprocess.run(["sudo", "-u", user_id, "mkdir", "-p", path], check=current_app.config['DEBUG'])
subprocess.run(["sudo", "-u", user_id, "mkdir", "-p", f'{user_root}/.ssh'], check=True) subprocess.run(["sudo", "-u", user_id, "mkdir", "-p", f'{user_root}/.ssh'], check=current_app.config['DEBUG'])
flask_pub_key = "/home/flask/.ssh/id_ed25519.pub" flask_pub_key = "/home/flask/.ssh/id_ed25519.pub"
subprocess.run(["sudo", "cp", flask_pub_key, f"{user_root}/.ssh/authorized_keys"], check=True) subprocess.run(["sudo", "cp", flask_pub_key, f"{user_root}/.ssh/authorized_keys"], check=current_app.config['DEBUG'])
subprocess.run(["sudo", "chmod", "700", f"{user_root}/.ssh"], check=True) subprocess.run(["sudo", "chmod", "700", f"{user_root}/.ssh"], check=current_app.config['DEBUG'])
subprocess.run(["sudo", "chmod", "600", f"{user_root}/.ssh/authorized_keys"], check=True) subprocess.run(["sudo", "chmod", "600", f"{user_root}/.ssh/authorized_keys"], check=current_app.config['DEBUG'])
print(f"Directory {path} created successfully for user {user_id}") print(f"Directory {path} created successfully for user {user_id}")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Error creating directory {path} details {str(e)}") print(f"Error creating directory {path} details {str(e)}")
try:#使用sudo创建shared_group try:#使用sudo创建shared_group
subprocess.run(["sudo", "groupadd", f"shared_group_{user_id}"], check=True) subprocess.run(["sudo", "groupadd", f"shared_group_{user_id}"], check=current_app.config['DEBUG'])
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Error creating shared_group: {e}") # 组可能已经存在,这是正常情况,继续执行
print(f"Groupadd command returned non-zero exit status, this is expected if group already exists: {e}")
try:#使用sudo将user_id加入shared_group try:#使用sudo将user_id加入shared_group
subprocess.run(["sudo", "usermod", "-a", "-G", f"shared_group_{user_id}", user_id], check=True) subprocess.run(["sudo", "usermod", "-a", "-G", f"shared_group_{user_id}", user_id], check=current_app.config['DEBUG'])
# 自己也加入 # 自己也加入
subprocess.run(["sudo", "usermod", "-a", "-G", f"shared_group_{user_id}", 'flask'], check=True) subprocess.run(["sudo", "usermod", "-a", "-G", f"shared_group_{user_id}", 'flask'], check=current_app.config['DEBUG'])
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Error adding user {user_id} to shared_group: {e}") print(f"Error adding user {user_id} to shared_group: {e}")
try: try:
subprocess.run(["sudo", "chown", "-R", f"flask:shared_group_{user_id}", path], check=True) subprocess.run(["sudo", "chown", "-R", f"flask:shared_group_{user_id}", path], check=current_app.config['DEBUG'])
subprocess.run(["sudo", "chmod", "-R", "755", user_root], check=True) subprocess.run(["sudo", "chmod", "-R", "755", user_root], check=current_app.config['DEBUG'])
subprocess.run(["sudo", "chmod", "-R", "775", path], check=True) subprocess.run(["sudo", "chmod", "-R", "775", path], check=current_app.config['DEBUG'])
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Error changing directory {path} to shared_group_{user_id}: {e}") print(f"Error changing directory {path} to shared_group_{user_id}: {e}")

View File

@@ -1,3 +1,4 @@
[Global] [Global]
SECRET_KEY = cakebaker SECRET_KEY = cakebaker
ROOT_WORKSPACE_PATH = /home ROOT_WORKSPACE_PATH = /home
DEBUG = False

View File

@@ -1,5 +1,12 @@
from app import create_app from app import create_app
from app.extensions import socketio from app.extensions import socketio
import logging
# 配置日志减少eventlet的调试日志
logging.getLogger('eventlet').setLevel(logging.ERROR)
logging.getLogger('socketio').setLevel(logging.ERROR)
logging.getLogger('werkzeug').setLevel(logging.ERROR)
app = create_app() app = create_app()
if __name__ == '__main__': if __name__ == '__main__':
@@ -9,4 +16,5 @@ if __name__ == '__main__':
port=5200, port=5200,
debug=False, # 开发期打开 debug=False, # 开发期打开
allow_unsafe_werkzeug=True, # 避免 dev server 的安全限制提示 allow_unsafe_werkzeug=True, # 避免 dev server 的安全限制提示
) log_output=False # 禁用详细输出
)

View File

@@ -4,17 +4,17 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cloud IDE</title> <title>Cloud IDE</title>
<link rel="stylesheet" href="static/cdnback/css/editor/editor.main.css"> <link rel="stylesheet" href="/static/cdnback/css/editor/editor.main.css">
<script src="static/cdnback/socket.io.min.js"></script> <script src="/static/cdnback/socket.io.min.js"></script>
<link rel="stylesheet" href="static/cdnback/all.min.css"> <link rel="stylesheet" href="/static/cdnback/all.min.css">
<link rel="stylesheet" href="static/cdnback/css/xterm.css" /> <link rel="stylesheet" href="/static/cdnback/css/xterm.css" />
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.5.0/xterm.js"></script> --> <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/5.5.0/xterm.js"></script> -->
<script src="static/cdnback/js/xterm.js"></script> <script src="/static/cdnback/js/xterm.js"></script>
<script src="static/cdnback/js/addons/fit/fit.js"></script> <script src="/static/cdnback/js/addons/fit/fit.js"></script>
<script src="static/cdnback/js/addons/webLinks/webLinks.js"></script> <script src="/static/cdnback/js/addons/webLinks/webLinks.js"></script>
<script src="static/cdnback/js/addons/fullscreen/fullscreen.js"></script> <script src="/static/cdnback/js/addons/fullscreen/fullscreen.js"></script>
<script src="static/cdnback/js/addons/search/search.js"></script> <script src="/static/cdnback/js/addons/search/search.js"></script>
<link rel="stylesheet" href="static/cdnback/all.min.css"> <link rel="stylesheet" href="/static/cdnback/all.min.css">
<link rel="stylesheet" href="/vsc-like/static/css/index.css"> <link rel="stylesheet" href="/vsc-like/static/css/index.css">
<link rel="stylesheet" href="/vsc-like/static/css/notification.css"> <link rel="stylesheet" href="/vsc-like/static/css/notification.css">
@@ -61,7 +61,7 @@
</div> </div>
<div id="notificationsContainer"></div> <div id="notificationsContainer"></div>
<script src="static/cdnback/js/monaco-editor/0.34.0/min/vs/loader.js"></script> <script src="/static/cdnback/js/monaco-editor/0.34.0/min/vs/loader.js"></script>
<script src="/vsc-like/static/js/code-like-extension.js"></script> <script src="/vsc-like/static/js/code-like-extension.js"></script>
<script src="/vsc-like/static/js/file.js"></script> <script src="/vsc-like/static/js/file.js"></script>
<script src="/vsc-like/static/js/terminal.js"></script> <script src="/vsc-like/static/js/terminal.js"></script>
@@ -161,7 +161,7 @@
// saveFile // saveFile
if (saveTimeout) clearTimeout(saveTimeout); if (saveTimeout) clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => { saveTimeout = setTimeout(() => {
postSaveFile(selectedItem.path+'/'+selectedItem.name, editor.getValue()); postSaveFile(filePath, editor.getValue());
}, 5000); // 5000ms = 5秒 }, 5000); // 5000ms = 5秒
}); });

69
et --hard HEAD@{1} Normal file
View File

@@ -0,0 +1,69 @@
68493d6a (HEAD -> main, origin/main) HEAD@{0}: pull origin main: Fast-forward
0852e121 HEAD@{1}: reset: moving to HEAD
0852e121 HEAD@{2}: commit: cdn fix more
7dc4714a HEAD@{3}: commit: index style
9dc81b14 (origin/feature/paste_detecte_new, feature/paste_detecte_new) HEAD@{4}: merge feature/paste_detecte_new: Fast-forward
7862647b HEAD@{5}: checkout: moving from feature/paste_detecte_new to main
9dc81b14 (origin/feature/paste_detecte_new, feature/paste_detecte_new) HEAD@{6}: commit (merge): Merge branch 'main' into feature/paste_detecte_new
0cf1896b HEAD@{7}: checkout: moving from main to feature/paste_detecte_new
7862647b HEAD@{8}: commit: style fix
6b9c8e9e HEAD@{9}: commit: cdn change to local and use markdown and style change
33844786 HEAD@{10}: commit: use markdown to edit
ce88ae64 HEAD@{11}: commit: use queue to allow send vscode_ws when it is none
6c38e906 HEAD@{12}: pull: Fast-forward
9759c93a HEAD@{13}: commit: fix funciton response
ecf19f49 HEAD@{14}: commit: fix function double promlem
f5d50772 HEAD@{15}: commit: function call can be expire
4a390adb HEAD@{16}: commit: fix style bug
960d1f1d HEAD@{17}: commit: style fix
c73ea447 HEAD@{18}: commit: fix index
33bf569f HEAD@{19}: commit: fix nextchapter bug
b921bda4 HEAD@{20}: commit: auto get domain
9921848d HEAD@{21}: commit: remove prefix of http url
4d81aef8 HEAD@{22}: commit (merge): Merge branch 'feature/paste_deteced'
8fe5ee16 HEAD@{23}: checkout: moving from feature/paste_deteced to main
9ce212e9 (origin/feature/paste_deteced, feature/paste_deteced) HEAD@{24}: checkout: moving from main to feature/paste_deteced
8fe5ee16 HEAD@{25}: commit (merge): 准备合并main
908d5915 HEAD@{26}: checkout: moving from syt to main
57d8ac32 (origin/syt, syt) HEAD@{27}: checkout: moving from main to syt
908d5915 HEAD@{28}: commit: ready
02b12791 HEAD@{29}: checkout: moving from syt to main
57d8ac32 (origin/syt, syt) HEAD@{30}: checkout: moving from main to syt
02b12791 HEAD@{31}: pull: Fast-forward
7d861e34 (origin/zrz) HEAD@{32}: checkout: moving from syt to main
57d8ac32 (origin/syt, syt) HEAD@{33}: commit: first
0196f37e HEAD@{34}: pull: Merge made by the 'ort' strategy.
57eb1197 HEAD@{35}: commit: ok
382f2006 HEAD@{36}: checkout: moving from main to syt
7d861e34 (origin/zrz) HEAD@{37}: commit (merge): Merge branch 'main' of https://gitee.com/CakeCN/code-agent
5f8f6c94 HEAD@{38}: commit: many ok
e24aa12e HEAD@{39}: commit: ready to process
6766ca85 HEAD@{40}: commit (merge): Merge branch 'feature/voice_send'
3c9c7553 HEAD@{41}: checkout: moving from feature/voice_send to main
58a95e9f (origin/feature/voice_send, feature/voice_send) HEAD@{42}: commit: ready merge to main
79125ffc HEAD@{43}: commit: change .cn to .com
2f9bc7df HEAD@{44}: checkout: moving from main to feature/voice_send
3c9c7553 HEAD@{45}: reset: moving to HEAD
3c9c7553 HEAD@{46}: checkout: moving from feature/voice_send to main
2f9bc7df HEAD@{47}: pull: Fast-forward
504c5cae (0v) HEAD@{48}: checkout: moving from main to feature/voice_send
3c9c7553 HEAD@{49}: commit: cn/com allow all
7122c3be HEAD@{50}: commit (merge): fix url
e07c41fb HEAD@{51}: commit: fix code-server reboot problem(close debug)
821e20c9 HEAD@{52}: commit: clear double iframe more safe
053584a7 HEAD@{53}: checkout: moving from feature/voice_send to main
504c5cae (0v) HEAD@{54}: checkout: moving from main to feature/voice_send
053584a7 HEAD@{55}: commit: clear iframe double
10481b0b HEAD@{56}: commit: fix permisson bug
dc0eeaa9 HEAD@{57}: commit: add hint
a24df620 HEAD@{58}: commit: fix some bug
3ad36c29 HEAD@{59}: commit: 25/09/24 10:00~11:30 停机维护
07cf2363 HEAD@{60}: commit: fix many bug
6aac4200 HEAD@{61}: commit: try hello message
271672c9 HEAD@{62}: commit: check to code-server-like
252c426c HEAD@{63}: pull: Merge made by the 'ort' strategy.
588fc3b6 HEAD@{64}: commit: ready to code-like
b1f832a2 (origin/code-like-autosave, code-like-autosave) HEAD@{65}: merge code-like-autosave: Fast-forward
24f191d5 HEAD@{66}: checkout: moving from code-like-autosave to main
b1f832a2 (origin/code-like-autosave, code-like-autosave) HEAD@{67}: checkout: moving from main to code-like-autosave
24f191d5 HEAD@{68}: clone: from https://gitee.com/CakeCN/code-agent.git