30 Commits

Author SHA1 Message Date
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
31 changed files with 1170 additions and 1272 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()
def __init__(self, restart=False):
self.restart = restart
self.chapter_chain_now = -1
self.ase_client = None
self.app = None
self.socketio = None
@@ -70,6 +69,9 @@ class ChatManager:
self.chapter_chain = load_chapters(raw_markdown, raw_markdown_prompts, raw_score_prompts)
self.bb = None
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'):

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')
restart = init_data.get("restart", False)
if restart: # 重启时,从-1章进入 0章
ase_client.chatmanager.next_chapter()
# 不再需要手动next_chapter因为已经在on_login中根据mongo进度恢复了正确的章节
# if restart: # 重启时,从-1章进入 0章
# ase_client.chatmanager.next_chapter()
# 根据restart决定是否重新加载代码
ase_client.chatmanager.load_now_chapter(load_code=restart)
print("load_now_chapter with restart:", restart)
# 只有当需要restart时才发送chapter-start
if restart:
ase_client.send_text("chapter-start", "")
namespace_entry.emit('message', "教案加载完毕", room=init_data['room'], namespace='/agent')

View File

@@ -72,22 +72,18 @@ class AgentNamespace(Namespace):
course_id: 课程ID
chapter_name: 章节名称
lesson_name: 课时名称
continue_learn: 是否继续学习
continue_learn: 是否继续学习现在总是为True保持兼容性
Returns:
int: 需要跳过的章节数
"""
need_skip_chapters = 0
if continue_learn: # 尝试加载用户学习进度
# 总是从mongo加载学习进度
progress_result = load_learning_progress(user_id, course_id, chapter_name, lesson_name)
if progress_result['exists']:
progress = progress_result['data']
# 恢复学习进度数据
if 'scores' in progress:
# 根据scores的数量确定需要跳过的章节数
need_skip_chapters = len(progress['scores'])
chatmanager.scores = progress['scores']
# 恢复聊天历史
@@ -100,10 +96,15 @@ class AgentNamespace(Namespace):
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
@@ -131,8 +132,9 @@ class AgentNamespace(Namespace):
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)
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)
user_uuid2ase_client[user_uuid] = HSAEngineClient(current_app.config["ASE_ENGINE_URL"], current_app.config["ASE_ENGINE_NAMESPACE"],id=user_id, chatmanager = chatmanager)
@@ -141,8 +143,8 @@ class AgentNamespace(Namespace):
backboard_manager.add_backboard(user_uuid, user_id, course_id,
lesson_name, root_path=f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}')
chatmanager.bb = backboard_manager.get_backboard(user_uuid)
# 如果有保存的进度,需要跳过已经完成的章节、无需清空代码、对话历史恢复
if continue_learn:
# 总是根据进度跳过章节,恢复对话历史
for _ in range(need_skip_chapters):
chatmanager.next_chapter()
emit('next_chapter', room=user_uuid, namespace='/agent')
@@ -152,10 +154,15 @@ class AgentNamespace(Namespace):
self.chatmanager = chatmanager
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(
callback=on_connect_to_ase,
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(
route='dialog',

View File

@@ -60,6 +60,7 @@
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
cursor: pointer;
position: relative; /* 添加相对定位 */
}
.course-image {
@@ -81,6 +82,27 @@
.course-description {
font-size: 14px;
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 {
background-color: #fff;
margin: 15% auto;
margin: 10% auto; /* 修改为10%以缩小顶部距离 */
padding: 20px;
border-radius: 10px;
width: 40%;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
width: 80%; /* 修改为80%以适应小屏幕 */
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; /* 上方间距 */
}
/* 选择课程或新建课程的下拉框 */
.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 {
@@ -433,6 +485,20 @@ label[for="course-description"] {
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 {
display: none;

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

View File

@@ -1,4 +1,4 @@
window.addEventListener('load', ()=>{
window.addEventListener('load', () => {
console.log(window.material);
console.log(window.lesson);
editLesson(window.material.material_id, window.chapter.chapter_name, window.lesson.lesson_name);
@@ -12,58 +12,58 @@ function parseHeadings(mdText) {
const lines = mdText.split('\n');
const tokens = [];
for (let i=0;i<lines.length;i++) {
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(/^(#{1,3})\s+(.*)$/);
if (m) tokens.push({ level: m[1].length, text: m[2].trim(), line: i });
}
// 确定每个heading的区间到下一个同级或上级前一行
for (let i=0;i<tokens.length;i++) {
let j = i+1;
while (j<tokens.length && tokens[j].level > tokens[i].level) j++;
for (let i = 0; i < tokens.length; i++) {
let j = i + 1;
while (j < tokens.length && tokens[j].level > tokens[i].level) j++;
// 找到第一个 >= 当前level 的 heading 或到文末
let k = i+1;
while (k<tokens.length && tokens[k].level > tokens[i].level) k++;
const nextSameOrHigher = k<tokens.length ? tokens[k].line : lines.length;
let k = i + 1;
while (k < tokens.length && tokens[k].level > tokens[i].level) k++;
const nextSameOrHigher = k < tokens.length ? tokens[k].line : lines.length;
tokens[i].start = tokens[i].line;
tokens[i].end = nextSameOrHigher - 1;
}
return { lines, tokens };
}
}
// 从解析结果中抽取一个 heading 段落文本
function sliceSection(parsed, headingText, level) {
const t = parsed.tokens.find(x => x.level===level && x.text===headingText);
// 从解析结果中抽取一个 heading 段落文本
function sliceSection(parsed, headingText, level) {
const t = parsed.tokens.find(x => x.level === level && x.text === headingText);
if (!t) return '';
return parsed.lines.slice(t.start, t.end+1).join('\n');
}
return parsed.lines.slice(t.start, t.end + 1).join('\n');
}
// 根据三个 md构造统一大纲树一级唯一“主题”其下多个阶段##,其下多个步骤###
function buildUnifiedOutline(parsedLesson, parsedPrompt, parsedScore) {
// 根据三个 md构造统一大纲树一级唯一“主题”其下多个阶段##,其下多个步骤###
function buildUnifiedOutline(parsedLesson, parsedPrompt, parsedScore) {
// 取 lesson 的结构为主(也可做三者交集/并集策略,这里简单以 lesson 为基准)
const root1 = parsedLesson.tokens.find(t => t.level===1);
const phases = parsedLesson.tokens.filter(t => t.level===2);
const root1 = parsedLesson.tokens.find(t => t.level === 1);
const phases = parsedLesson.tokens.filter(t => t.level === 2);
const stepsByPhase = {};
phases.forEach(ph => {
stepsByPhase[ph.text] = parsedLesson.tokens.filter(t => t.level===3 && t.start>ph.start && t.end<=ph.end)
stepsByPhase[ph.text] = parsedLesson.tokens.filter(t => t.level === 3 && t.start > ph.start && t.end <= ph.end)
.map(s => s.text);
});
return { theme: root1 ? root1.text : '主题', phases, stepsByPhase };
}
}
let CURRENT = {
let CURRENT = {
material_id: null, chapter_name: null, lesson_name: null,
links: { lesson:'', prompt:'', score:'' },
raw: { lesson:'', prompt:'', score:'' },
parsed:{ lesson:null, prompt:null, score:null },
active:{ phase:null, step:null, lessontxt:null, prompt:null, score:null },
outline:null
};
function bustCache(url) {
links: { lesson: '', prompt: '', score: '' },
raw: { lesson: '', prompt: '', score: '' },
parsed: { lesson: null, prompt: null, score: null },
active: { phase: null, step: null, lessontxt: null, prompt: null, score: null },
outline: null
};
function bustCache(url) {
const sep = url.includes("?") ? "&" : "?";
return url + sep + "_ts=" + Date.now();
}
// 入口:从你的列表调用
async function editLesson(material_id, chapter_name, lesson_name) {
}
// 入口:从你的列表调用
async function editLesson(material_id, chapter_name, lesson_name) {
CURRENT.material_id = material_id;
CURRENT.chapter_name = chapter_name;
@@ -84,7 +84,7 @@ function parseHeadings(mdText) {
detailsContent.innerHTML = `<h4>描述</h4><p>${meta.description || meta.material_description || '暂无描述'}</p>`;
const chapterList = document.getElementById('chapter-list');
chapterList.innerHTML = '';
(meta.chapters||[]).forEach(ch=>{
(meta.chapters || []).forEach(ch => {
const li = document.createElement('li');
li.textContent = ch.chapter_name;
chapterList.appendChild(li);
@@ -125,10 +125,10 @@ function parseHeadings(mdText) {
document.getElementById('editor-prompt').value = CURRENT.raw.prompt;
document.getElementById('editor-score').value = CURRENT.raw.score;
}
}
}
// ===== 在渲染一级标题时调用 =====
function addThemeWithHelp(tree, themeText) {
// ===== 在渲染一级标题时调用 =====
function addThemeWithHelp(tree, themeText) {
const li = document.createElement('li');
li.className = 'lvl1';
@@ -175,9 +175,9 @@ function parseHeadings(mdText) {
);
tree.appendChild(li);
}
}
function renderOutline(outline) {
function renderOutline(outline) {
const tree = document.getElementById('outline-tree');
tree.innerHTML = ''; // 清空大纲内容
@@ -220,7 +220,7 @@ function parseHeadings(mdText) {
if (window.confirm(confirmMsg)) {
saveUpdatedStep()
}
}else{
} else {
// 已存在新标题:保持原文不变
alert("新标题已存在")
}
@@ -339,21 +339,21 @@ function addStepToPhase(phaseText) {
renderOutline(CURRENT.outline);
loadStepToEditors(phaseText, stepName);
highlightActive(phaseText, stepName);
}
function highlightActive(phase, step) {
document.querySelectorAll('#outline-tree li').forEach(li=>li.classList.remove('active'));
}
function highlightActive(phase, step) {
document.querySelectorAll('#outline-tree li').forEach(li => li.classList.remove('active'));
const target = Array.from(document.querySelectorAll('#outline-tree li.lvl3'))
.find(li => li.dataset.phase===phase && li.dataset.step===step);
.find(li => li.dataset.phase === phase && li.dataset.step === step);
if (target) target.classList.add('active');
CURRENT.active.phase=phase
CURRENT.active.step=step
}
/**
* 在 mdText 中,将 `### stepName` 插入到 `## phaseText` 阶段的末尾。
* 若 phase 不存在,则创建 `## phaseText` 再插入。
* skeleton 为插入文本模板(含### 标题本身及正文)。
*/
function insertStepIntoMarkdown(mdText, phaseText, stepName, skeleton, { allowDuplicate = false } = {}) {
CURRENT.active.phase = phase
CURRENT.active.step = step
}
/**
* 在 mdText 中,将 `### stepName` 插入到 `## phaseText` 阶段的末尾。
* 若 phase 不存在,则创建 `## phaseText` 再插入。
* skeleton 为插入文本模板(含### 标题本身及正文)。
*/
function insertStepIntoMarkdown(mdText, phaseText, stepName, skeleton, { allowDuplicate = false } = {}) {
const parsed = parseHeadings(mdText);
const { lines, tokens } = parsed;
// 找到目标阶段(##
@@ -419,35 +419,35 @@ function addStepToPhase(phaseText) {
.concat(insertChunk)
.concat(lines.slice(insertPos));
return newLines.join('\n');
}
}
const outlineAside = document.querySelector('.outline');
const outlineTree = document.getElementById('outline-tree');
const btnDeleteMode = document.getElementById('toggle-outline-delete');
const outlineAside = document.querySelector('.outline');
const outlineTree = document.getElementById('outline-tree');
const btnDeleteMode = document.getElementById('toggle-outline-delete');
// 切换删除模式
function setDeleteMode(enabled) {
// 切换删除模式
function setDeleteMode(enabled) {
btnDeleteMode.setAttribute('aria-pressed', String(enabled));
outlineAside.classList.toggle('delete-mode', enabled);
}
}
// 点击按钮:进入/退出删除模式
btnDeleteMode.addEventListener('click', (e) => {
// 点击按钮:进入/退出删除模式
btnDeleteMode.addEventListener('click', (e) => {
e.preventDefault();
const enabled = btnDeleteMode.getAttribute('aria-pressed') !== 'true';
setDeleteMode(enabled);
});
});
// 工具:判断是否是 lvl2 / lvl3 节点(允许点到它的内部子元素)
function findDeletableNode(el) {
// 工具:判断是否是 lvl2 / lvl3 节点(允许点到它的内部子元素)
function findDeletableNode(el) {
if (!el) return null;
if (el.classList?.contains('lvl2') || el.classList?.contains('lvl3')) return el;
return el.closest?.('.lvl2, .lvl3') || null;
}
}
// 事件代理:删除模式下,点击二/三级标题进行删除确认
outlineTree.addEventListener('click', (e) => {
// 事件代理:删除模式下,点击二/三级标题进行删除确认
outlineTree.addEventListener('click', (e) => {
if (btnDeleteMode.getAttribute('aria-pressed') !== 'true') return; // 非删除模式,忽略
const targetNode = findDeletableNode(e.target);
if (!targetNode) return;
@@ -472,7 +472,7 @@ function addStepToPhase(phaseText) {
// 可选如果你需要同步删除到正文内容CURRENT.raw.xxx可以在这里调用你的同步逻辑
// 例:把正文中第一处行首 "## 标题" 或 "### 标题" 删除(仅示例,按你项目规则调整):
deleteSectionInRaw(level, text);
});
});
/**
* 删除指定 level(2/3) 与 title 的小节:
* 从 "^## title$" 或 "^### title$" 那一行开始,
@@ -512,8 +512,8 @@ function deleteSectionInRaw(level, title) {
// 解析Markdown并定位章节/步骤
function parseHeadings(mdText) {
// 解析Markdown并定位章节/步骤
function parseHeadings(mdText) {
const lines = mdText.split('\n');
const tokens = [];
for (let i = 0; i < lines.length; i++) {
@@ -531,19 +531,27 @@ function deleteSectionInRaw(level, title) {
tokens[i].end = nextSameOrHigher - 1;
}
return { lines, tokens };
}
}
// 从解析结果中抽取一个 heading 段落文本
function sliceSection(parsed, headingText, level) {
// 从解析结果中抽取一个 heading 段落文本
function sliceSection(parsed, headingText, level) {
const t = parsed.tokens.find(x => x.level === level && x.text === headingText);
if (!t) return '';
return parsed.lines.slice(t.start, t.end + 1).join('\n');
}
function buildOriginSaveUrl() {
return `/api/materials/save/${encodeURIComponent(CURRENT.material_id)}/${encodeURIComponent(CURRENT.chapter_name)}/${encodeURIComponent(CURRENT.lesson_name)}`;
}
}
/**
// 加载某个步骤到编辑器
function buildOriginSaveUrl() {
const mid = typeof CURRENT.material_id === 'string' && CURRENT.material_id
|| (window.material && window.material.material_id) || '';
const ch = typeof CURRENT.chapter_name === 'string' && CURRENT.chapter_name
|| (window.chapter && window.chapter.chapter_name) || '';
const ln = typeof CURRENT.lesson_name === 'string' && CURRENT.lesson_name
|| (window.lesson && window.lesson.lesson_name) || '';
return `/api/materials/save/${encodeURIComponent(mid)}/${encodeURIComponent(ch)}/${encodeURIComponent(ln)}`;
}
/**
* 把三份文档lesson/prompt/score的 "CDN 外链 + 原文内容" 一次性提交给源站。
* @param {Object} payload 形如:
* {
@@ -552,7 +560,7 @@ function deleteSectionInRaw(level, title) {
* score: { cdn_link, content }
* }
*/
async function saveToOrigin(payload) {
async function saveToOrigin(payload) {
const res = await fetch(buildOriginSaveUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -565,11 +573,11 @@ function deleteSectionInRaw(level, title) {
credentials: 'include' // 若需要带 cookie/会话,保留;不需要可移除
});
if (!res.ok) {
const text = await res.text().catch(()=>'');
const text = await res.text().catch(() => '');
throw new Error(`保存失败(${res.status}): ${text}`);
}
return res.json().catch(()=> ({}));
}
return res.json().catch(() => ({}));
}
@@ -642,8 +650,8 @@ function sanitizeHeadingsBeyondFirst(text = "") {
// 保存某个步骤的更新
async function saveUpdatedStep() {
// 保存某个步骤的更新
async function saveUpdatedStep() {
const lEditor = document.getElementById('editor-lesson');
const pEditor = document.getElementById('editor-prompt');
const sEditor = document.getElementById('editor-score');
@@ -651,7 +659,7 @@ function sanitizeHeadingsBeyondFirst(text = "") {
if (!check.ok) {
alert("保存被阻止:" + check.report);
return ;
return;
}
// 获取原始的文档内容
@@ -707,10 +715,10 @@ function sanitizeHeadingsBeyondFirst(text = "") {
btn.innerText = ClearModifiedStat(btn.innerText)
btn = document.getElementById("tab-btn-score");
btn.innerText = ClearModifiedStat(btn.innerText)
}
}
// 更新对应步骤的内容
function updateMarkdownContent(parsed, phaseText, stepText, newContent) {
// 更新对应步骤的内容
function updateMarkdownContent(parsed, phaseText, stepText, newContent) {
const phaseIndex = parsed.tokens.findIndex(token => token.level === 2 && token.text === phaseText);
if (phaseIndex === -1) return parsed.lines.join('\n'); // 找不到阶段
@@ -730,14 +738,14 @@ function sanitizeHeadingsBeyondFirst(text = "") {
lines.splice(startLine, endLine - startLine + 1, newContent); // 用新内容替换
return lines.join('\n'); // 返回更新后的完整内容
}
}
// 获取所有 textarea
function AddModifiedStat(str){
function AddModifiedStat(str) {
return str.charAt(str.length - 1) !== '*' ? str + '*' : str;
}
function ClearModifiedStat(str){
return str.charAt(str.length - 1) !== '*' ? str:str.slice(0, str.length-1);
function ClearModifiedStat(str) {
return str.charAt(str.length - 1) !== '*' ? str : str.slice(0, str.length - 1);
}
function ValidTextareaNoBlankHead(textarea) {
let lines = textarea.value.split("\n");
@@ -753,25 +761,25 @@ const PREFIX = "### ";
// 记录每个 textarea 最新的“标题内容”(不含前缀)
const lastTitleMap = new Map();
// 防止循环触发
let isProgrammaticUpdate = false;
function getFirstLine(text) {
// 防止循环触发
let isProgrammaticUpdate = false;
function getFirstLine(text) {
const idx = text.indexOf("\n");
return idx === -1 ? text : text.slice(0, idx);
}
function setFirstLine(text, newFirstLine) {
}
function setFirstLine(text, newFirstLine) {
const idx = text.indexOf("\n");
return idx === -1 ? newFirstLine : newFirstLine + text.slice(idx);
}
// 工具:规范化首行 -> 始终以 '### ' 开头,并返回 { line, title }
function normalizeFirstLine(line) {
}
// 工具:规范化首行 -> 始终以 '### ' 开头,并返回 { line, title }
function normalizeFirstLine(line) {
// 去掉前导的 # 和空格,保留后面的标题内容
const title = line.replace(/^#+\s*/,''); // 如果用户没写#,也直接当作标题
const title = line.replace(/^#+\s*/, ''); // 如果用户没写#,也直接当作标题
const normalized = PREFIX + title;
return { line: normalized, title };
}
// 初始化 lastTitleMap把当前值规范化一次
editorsDoc.forEach(ed => {
}
// 初始化 lastTitleMap把当前值规范化一次
editorsDoc.forEach(ed => {
const rawFirst = getFirstLine(ed.value || "");
const { line, title } = normalizeFirstLine(rawFirst);
if (line !== rawFirst) {
@@ -780,9 +788,9 @@ const lastTitleMap = new Map();
isProgrammaticUpdate = false;
}
lastTitleMap.set(ed, title);
});
// 同步到其他两个 textarea 的首行
function syncOthers(sourceEditor, newTitle) {
});
// 同步到其他两个 textarea 的首行
function syncOthers(sourceEditor, newTitle) {
isProgrammaticUpdate = true;
try {
editorsDoc.forEach(ed => {
@@ -798,7 +806,7 @@ const lastTitleMap = new Map();
} finally {
isProgrammaticUpdate = false;
}
}
}
// ==== 前置:你的三编辑器实例与 id 映射(按你实际为准)====
const ID_LESSON = 'editor-lesson';
@@ -1018,12 +1026,12 @@ function loadStepToEditors(phaseText, stepText) {
editors['editor-prompt'].value(pText);
editors['editor-score'].value(sText);
CURRENT.active={ //保存打开时的三个文本
phase:phaseText,
step:stepText,
lesson:lText,
prompt:pText,
score:sText
CURRENT.active = { //保存打开时的三个文本
phase: phaseText,
step: stepText,
lesson: lText,
prompt: pText,
score: sText
}
let btn = document.getElementById("tab-btn-lesson");
btn.innerText = ClearModifiedStat(btn.innerText)
@@ -1045,9 +1053,9 @@ document.addEventListener('click', (e) => {
// 广播 tab 变更(告诉别人当前激活的 tab id / 以及要用哪个 editorId
const editorIdMap = {
'tab-lesson' : 'editor-lesson',
'tab-prompt' : 'editor-prompt',
'tab-score' : 'editor-score',
'tab-lesson': 'editor-lesson',
'tab-prompt': 'editor-prompt',
'tab-score': 'editor-score',
};
const activeBtnId = e.target.id; // 如 'tab-btn-lesson'
const editorId =
@@ -1061,8 +1069,8 @@ document.addEventListener('click', (e) => {
}
});
// 保存全部:将三个编辑器中的文本保存回相应的 markdown 文件
document.getElementById('save-all-btn')?.addEventListener('click', async () => {
// 保存全部:将三个编辑器中的文本保存回相应的 markdown 文件
document.getElementById('save-all-btn')?.addEventListener('click', async () => {
try {
// 将各自的步骤更新保存回文件
@@ -1070,10 +1078,10 @@ document.addEventListener('click', (e) => {
} catch (err) {
console.error('保存错误:', err);
}
});
});
const STEP_TEMPLATES = {
const STEP_TEMPLATES = {
lesson: `在这里编写本步骤的课程内容...`,
prompt: `#### 指令
请严格遵循本步骤的教学目标,输出所需内容。
@@ -1093,8 +1101,8 @@ document.addEventListener('click', (e) => {
#### 打分
请输出 JSON
{"score": 0-100, "reasons": ["...","..."]}`
};
const PHASE_TEMPLATES = {
};
const PHASE_TEMPLATES = {
lesson: `### 新步骤
本阶段描述:请在此补充阶段说明。
@@ -1112,7 +1120,7 @@ document.addEventListener('click', (e) => {
- 维度A...
- 维度B...
`
};
};
@@ -1174,7 +1182,7 @@ function buildGlobalStepIndex(lines, tokens) {
const phases = tokens.filter(t => t.level === 2);
for (let i = 0; i < phases.length; i++) {
const ph = phases[i];
const phNextStart = (i + 1 < phases.length) ? phases[i+1].start : lines.length;
const phNextStart = (i + 1 < phases.length) ? phases[i + 1].start : lines.length;
// 该阶段内的所有 ###
const steps = tokens.filter(t => t.level === 3 && t.start > ph.start && t.start < phNextStart);
for (const st of steps) {
@@ -1186,7 +1194,7 @@ function buildGlobalStepIndex(lines, tokens) {
}
// ===== 选择器与小工具 =====
// ===== 选择器与小工具 =====
const treeEl = document.getElementById('outline-tree');
const toggleOutlineBtn = document.getElementById('toggle-outline-reorder');

View File

@@ -292,13 +292,50 @@ function closeAddCourseModal() {
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(){
// 提交表单处理
document.getElementById('add-course-form').addEventListener('submit', function(event) {
event.preventDefault();
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 courseDescription = document.getElementById('course-description').value;
// 构建发送的数据
@@ -312,7 +349,7 @@ document.getElementById('add-course-form').addEventListener('submit', function(e
// 如果选择了已有课程,可以通过 courseSelection 传递课程ID根据需要修改
if (courseSelection !== 'new') {
// 如果是修改已有课程,设置相关的章节信息或其他数据
data.chapters = ["Chapter 1", "Chapter 2"]; // 示例章节信息
data.chapters = ["Chapter 1", "Chapter 2"];
}
// 发送 POST 请求到 /create_material

View File

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

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>华实学伴 - 专业的在线学习平台</title>
<script src="/static/cdnback/jquery.min.js"></script>
<link href="/static/cdnback/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/cdnback/all.min.css">
<link rel="stylesheet" href="/static/css/dashboard.css">
@@ -13,7 +14,6 @@
</head>
<body>
{% include 'navbar.html' %} <!-- 引入navbar.html -->
{% include 'learning-path.html' %} <!-- 引入learning-path.html -->
<section class="search-section">
<div class="container">
<div class="row justify-content-center">
@@ -81,7 +81,7 @@
</div>
</div>
{% include 'foot.html' %}
{% include 'footer-brief.html' %}
<script src="/static/js/dashboard.js"></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,5 +1,6 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>课程选择主页</title>
@@ -12,6 +13,7 @@
<script src="/static/cdnback/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
</head>
<body>
{% include 'navbar.html' %}
@@ -51,8 +53,7 @@
<div class="col-md-6 col-lg-4">
<div class="course-card card h-100" onclick="openCourseProgress('{{course._id}}')">
<img src="{{course.image_url}}"
class="course-img" alt="课程封面">
<img src="{{course.image_url}}" class="course-img" alt="课程封面">
<div class="card-body d-flex flex-column">
<span class="course-category">算法设计</span>
<h5 class="course-title">{{course.name}}</h5>
@@ -62,11 +63,15 @@
<span class="course-rating"><i class="fas fa-star"></i> 4.7</span>
</div>
<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 %}
<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 %}
<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 %}
</div>
@@ -80,7 +85,7 @@
</main>
{% include 'foot.html' %}
{% include 'footer-brief.html' %}
</body>
<script>
window.appData = {
@@ -88,5 +93,6 @@
}
</script>
<script src="/static/js/index.js"></script>
</html>
<script src="/static/js/dashboard.js"></script>
</html>

View File

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

View File

@@ -1,4 +1,5 @@
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 ..auth.decorators import require_role
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)
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")
def get_material(material_id):
def handle_material(material_id):
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
return jsonify(material.model_dump()), 200
@bp.route('/materials/<material_id>', methods=['PUT'])
@require_role(roles="teacher")
def update_existing_material(material_id):
elif request.method == 'PUT':
data = request.get_json()
chapters = data.get('chapters')
if update_material(material_id, chapters):
return jsonify({"message": "教材更新成功"}), 200
return jsonify({"message": "教材未找到"}), 404
else:
return jsonify({"message": "教材更新失败"}), 500
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'])
@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
# 如果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)
# 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
[VSCODE_WEB]
url = https://hsamooc.cn
url = https://hsamooc.com
[CODE_LIKE]
url = /vsc-like
#http://asengine.net:8282
@@ -27,6 +27,6 @@ bucket = hsamooc-cdn-1374354408
region = ap-guangzhou
[ASE_ENGINE]
url = https://test.asengine.net
namespace = /socket.io/7100eebf7a909e1ee3f4e06766f512f0358ecb6ac51d5434c17d31dd70890dc1
url_token = 7100eebf7a909e1ee3f4e06766f512f0358ecb6ac51d5434c17d31dd70890dc1
url = https://asengine.net
namespace = /socket.io/afe58a9e4c03e10fae6fae5985e8adde23a49635
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,32 @@
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 hub 以更优雅地处理 IOClosed 错误
try:
from eventlet.hubs import hub
# 保存原始的 handle_error 方法
original_handle_error = hub.Hub.handle_error
def custom_handle_error(self, context, type, value, tb):
# 忽略 IOClosed 错误
if type.__name__ == 'IOClosed' and 'Operation on closed file' in str(value):
return
# 其他错误调用原始方法
original_handle_error(self, context, type, value, tb)
# 替换原始方法
hub.Hub.handle_error = custom_handle_error
except Exception as e:
# 如果修改失败,忽略错误
pass
from flask import Flask
from .views.routes import main_bp
from .views.file import file_bp

View File

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

View File

@@ -29,8 +29,12 @@ terminal_bp = Blueprint('terminal', __name__, url_prefix='/vsc-like')
def set_winsize(fd, row, col, xpix=0, ypix=0):
try:
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):
@@ -67,21 +71,35 @@ def read_and_forward_pty_output(fd=None, pid=None, room_id=None,namespace=None):
return
if fd:
timeout_sec = 0
try:
# Check if file descriptor is still valid by trying to select on it
(data_ready, _, _) = select.select([fd], [], [], timeout_sec)
if data_ready:
# output = os.read(fd, max_read_bytes).decode('ascii')
timeout=0.1
try:
output = os.read(fd, max_read_bytes).decode()
except Exception as err:
except (OSError, IOError, EOFError) as err:
# File descriptor closed or other IO error, exit gracefully
return
except UnicodeDecodeError as err:
output = """
***AQUI WEB TERM ERR***
{}
Unicode decode error: {}
***********************
""".format(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!
try:
namespace.emit("pty_output", {"output": output}, room=room_id)
except Exception:
# If emit fails, the client is probably disconnected, exit
return
except (OSError, IOError) as err:
# File descriptor closed or invalid, exit gracefully
return
except Exception as e:
# Catch any other unexpected exceptions to prevent server crash
pass
finally:
# Clean up file descriptor if it's open
if fd:
@@ -158,7 +176,13 @@ class VSCLikeNameSpace(Namespace):
if fd:
# print("writing to ptd: %s" % data["input"])
# os.write(fd, data["input"].encode('ascii'))
try:
os.write(fd, data["input"].encode())
except (OSError, IOError):
# File descriptor closed or invalid, clean up
disconnect()
session['terminal_config'] = TERM_INIT_CONFIG
return
def on_resize(self, data):
@@ -174,10 +198,29 @@ class VSCLikeNameSpace(Namespace):
return
fd = session.get('terminal_config').get('fd')
if fd:
# 检查文件描述符是否有效
try:
# 尝试一个简单的操作来检查文件描述符是否有效
os.fstat(fd)
set_winsize(fd, data["rows"], data["cols"])
except (OSError, IOError):
# 文件描述符无效,清理资源
disconnect()
session['terminal_config'] = TERM_INIT_CONFIG
return
def on_disconnect(self):
child_pid = session.get('terminal_config', {}).get('child_pid')
terminal_config = session.get('terminal_config', {})
child_pid = terminal_config.get('child_pid')
fd = terminal_config.get('fd')
# 先关闭文件描述符,避免其他操作尝试使用无效的文件描述符
if fd:
try:
os.close(fd)
except Exception:
pass
if child_pid:
try:
child_process = psutil.Process(child_pid)
@@ -196,21 +239,14 @@ class VSCLikeNameSpace(Namespace):
pass
except psutil.TimeoutExpired:
# If process didn't terminate in time, kill it forcefully
child_process.kill()
try:
child_process.kill()
child_process.wait(timeout=1)
except Exception:
pass
except Exception as 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')
if fd:
try:
os.close(fd)
except Exception:
pass
# Reset session config
session['terminal_config'] = TERM_INIT_CONFIG
current_app.logger.debug('Client disconnected')

View File

@@ -1,5 +1,5 @@
import os
import subprocess
from eventlet.green import subprocess
from flask import Blueprint, request, jsonify, current_app, render_template, session
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]) # 设置目录权限为新用户
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:
# 使用 sudo 执行 mkdir 命令来创建目录
subprocess.run(["sudo", "-u", user_id, "mkdir", "-p", path], check=True)
subprocess.run(["sudo", "-u", user_id, "mkdir", "-p", f'{user_root}/.ssh'], 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=current_app.config['DEBUG'])
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", "chmod", "700", f"{user_root}/.ssh"], check=True)
subprocess.run(["sudo", "chmod", "600", 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=current_app.config['DEBUG'])
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}")
except subprocess.CalledProcessError as e:
print(f"Error creating directory {path} details {str(e)}")
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:
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
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:
print(f"Error adding user {user_id} to shared_group: {e}")
try:
subprocess.run(["sudo", "chown", "-R", f"flask:shared_group_{user_id}", path], check=True)
subprocess.run(["sudo", "chmod", "-R", "755", user_root], check=True)
subprocess.run(["sudo", "chmod", "-R", "775", 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=current_app.config['DEBUG'])
subprocess.run(["sudo", "chmod", "-R", "775", path], check=current_app.config['DEBUG'])
except subprocess.CalledProcessError as e:
print(f"Error changing directory {path} to shared_group_{user_id}: {e}")

View File

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

View File

@@ -1,12 +1,32 @@
from app import create_app
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()
if __name__ == '__main__':
try:
socketio.run(
app,
host="0.0.0.0",
port=5200,
debug=False, # 开发期打开
allow_unsafe_werkzeug=True, # 避免 dev server 的安全限制提示
log_output=False # 禁用详细输出
)
except KeyboardInterrupt:
print("Server stopped by user")
except Exception as e:
# 捕获所有异常,包括 eventlet 内部的 IOClosed 错误
if "IOClosed" in str(type(e).__name__) or "Operation on closed file" in str(e):
# 优雅处理 eventlet 关闭文件的错误
print("Eventlet IOClosed error handled gracefully")
else:
# 其他异常仍然打印
import traceback
traceback.print_exc()

View File

@@ -4,17 +4,17 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cloud IDE</title>
<link rel="stylesheet" href="static/cdnback/css/editor/editor.main.css">
<script src="static/cdnback/socket.io.min.js"></script>
<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/editor/editor.main.css">
<script src="/static/cdnback/socket.io.min.js"></script>
<link rel="stylesheet" href="/static/cdnback/all.min.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="static/cdnback/js/xterm.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/fullscreen/fullscreen.js"></script>
<script src="static/cdnback/js/addons/search/search.js"></script>
<link rel="stylesheet" href="static/cdnback/all.min.css">
<script src="/static/cdnback/js/xterm.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/fullscreen/fullscreen.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="/vsc-like/static/css/index.css">
<link rel="stylesheet" href="/vsc-like/static/css/notification.css">
@@ -61,7 +61,7 @@
</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/file.js"></script>
<script src="/vsc-like/static/js/terminal.js"></script>

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