117 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
496c5be90a code-server-like ssh clash 2025-12-13 23:50:45 +08:00
CakeCN
ec6be8233c 增加code-serverlike cdnback 2025-12-13 22:52:16 +08:00
CakeCN
d0331d9361 navbar adjust 2025-12-13 21:47:10 +08:00
CakeCN
8e79380ba4 progress 检查 2025-12-13 18:56:21 +08:00
CakeCN
6acb1db093 direct no load history when no history 2025-12-13 18:36:33 +08:00
CakeCN
af0ebda419 try fix nextchapter bug 2025-12-13 18:16:34 +08:00
CakeCN
ae9f7b84f4 Merge branch 'test'
修复progress Bar和提示美化
2025-12-12 16:59:58 +08:00
CakeCN
ac74c28f64 提示helpTip的标准化 2025-12-12 16:58:27 +08:00
CakeCN
41c17fcde1 help tips 标准化 2025-12-12 16:46:41 +08:00
CakeCN
5789974d2e 修复一些progressbar bug 2025-12-12 16:34:42 +08:00
CakeCN
221f3faace 美化processbar 2025-12-12 16:26:51 +08:00
CakeCN
560c590067 fix chatmanager 2025-12-12 16:15:48 +08:00
CakeCN
02cf3c0f78 优化前端js代码 2025-12-12 16:11:46 +08:00
CakeCN
9a7571f832 增加function approval button 2025-12-12 00:55:16 +08:00
CakeCN
14dd164545 function 提交 2025-12-12 00:47:09 +08:00
CakeCN
ff3d6e5bf9 fix chatmanager no scores bug 2025-12-12 00:29:03 +08:00
CakeCN
97df0f6407 logger /download 2025-12-12 00:25:39 +08:00
CakeCN
58a25b3e07 fix clear session 2025-12-11 22:40:43 +08:00
CakeCN
c4743bcadd restart with clear user_session 2025-12-11 22:37:58 +08:00
CakeCN
f9b3cf53da 不再使用markdown编译html,而是直接markdown源文件,前端进行渲染 2025-12-11 21:46:04 +08:00
CakeCN
fe1784fa20 前后端next_chapter同步 2025-12-11 21:25:23 +08:00
CakeCN
140ece742e log clear session 2025-12-11 20:46:22 +08:00
CakeCN
97cda4031b clear user session fix 2025-12-11 20:23:21 +08:00
CakeCN
9c3ffce1e2 fix load_history always "false" 2025-12-11 20:20:38 +08:00
CakeCN
23bfc73bcf more log 2025-12-11 20:17:38 +08:00
CakeCN
4a5a34b0aa log restart 2025-12-11 20:16:24 +08:00
CakeCN
2f48f1b6b4 load now chapter with restart 2025-12-11 20:00:15 +08:00
CakeCN
b175407d4b 前端对话历史 2025-12-11 19:53:14 +08:00
CakeCN
ce4a9ac2ca onlogin reduce 2025-12-11 17:47:13 +08:00
CakeCN
be78f480a5 roll back 2025-12-11 17:38:28 +08:00
CakeCN
f804eac811 reduce onlogin 2025-12-11 17:33:36 +08:00
CakeCN
77cd53c241 调整onlogin 代码量 2025-12-11 17:28:20 +08:00
CakeCN
fc7c6708b4 Merge branch 'test' 2025-12-11 16:20:44 +08:00
CakeCN
7300b304e3 修复bug 2025-12-11 16:19:59 +08:00
CakeCN
7aa7bf1690 log receive ack 2025-12-11 15:42:45 +08:00
CakeCN
84a4a578a8 add some logger 2025-12-11 15:32:41 +08:00
CakeCN
5df2931fa1 fix logger 2025-12-11 15:23:50 +08:00
CakeCN
99758ce1f8 add app context log 2025-12-11 15:10:02 +08:00
CakeCN
ae91b62332 logger time 2025-12-11 15:02:33 +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
CakeCN
1bf13aacf6 little change 2025-12-11 14:04:58 +08:00
CakeCN
9dfa0c9738 speed up event wait 2025-12-10 22:47:39 +08:00
CakeCN
f330916ee0 Merge branch 'main' into test 2025-12-10 22:36:00 +08:00
CakeCN
1d4e625b8b reduce some log 2025-12-10 22:35:41 +08:00
CakeCN
dba7914e5d logger reduce 2025-12-10 19:22:13 +08:00
CakeCN
889399b8aa docs 2025-12-10 19:20:17 +08:00
CakeCN
6cc5a23874 upload ['multiagents_dialogs'] fix 2025-12-10 16:47:22 +08:00
CakeCN
38d65bac3d log 2025-12-10 16:31:22 +08:00
CakeCN
abf6f3aa2f 序列化 2025-12-10 15:10:37 +08:00
CakeCN
36662654ba add namespace logger 2025-12-10 14:52:33 +08:00
CakeCN
4d5a8bb23f learn progress 2025-12-09 22:56:51 +08:00
CakeCN
15cbeb93ee nouser require role 2025-12-09 22:08:58 +08:00
CakeCN
ed6075a89c 用user_id save 2025-12-09 22:03:55 +08:00
CakeCN
9b131dfc94 fix 2025-12-09 16:47:14 +08:00
CakeCN
67552dbca9 fix 2025-12-09 16:44:54 +08:00
CakeCN
ec2d1234b2 fix 2025-12-09 16:40:10 +08:00
CakeCN
7159371128 fix 2025-12-09 16:39:01 +08:00
CakeCN
3b4bebba6c aseclient fix 2025-12-09 16:38:18 +08:00
CakeCN
c3ca3f83f6 fix ase bug 2025-12-09 16:24:00 +08:00
CakeCN
d222358ded aseclient initdata 2025-12-09 16:21:14 +08:00
CakeCN
1be048bf06 load history or not 2025-12-09 16:02:11 +08:00
CakeCN
88ae20d596 调整文件格式 2025-12-09 15:47:17 +08:00
CakeCN
c85f8c9539 import fix 2025-12-09 14:29:16 +08:00
CakeCN
f92688cf2c import fix 2025-12-09 14:28:33 +08:00
CakeCN
a509b2efb2 调整course_service 文件长度 2025-12-09 13:25:05 +08:00
CakeCN
c3c76788fe 上传学习进度 2025-12-09 13:16:10 +08:00
CakeCN
c08fb49bcd log his 2025-12-08 23:55:33 +08:00
CakeCN
f8f7a9d104 建立连接OK提示 2025-12-08 13:28:41 +08:00
CakeCN
a210f5bac3 no chapter_dict show 2025-12-08 13:25:46 +08:00
CakeCN
dc9f246841 no app log in client 2025-12-08 12:56:56 +08:00
CakeCN
74f26c72fd url fix 2025-12-08 12:17:14 +08:00
CakeCN
aae10819ca more log 2025-12-08 11:35:09 +08:00
CakeCN
42549bb32a 构建test机制(hsamooc.cn上只跑test) 2025-12-02 22:40:03 +08:00
CakeCN
93cf991779 client: 增加ACK支持 2025-12-02 22:31:38 +08:00
CakeCN
788fbb07da ack-client 2025-12-02 22:31:34 +08:00
CakeCN
6c48d79076 change clear to sync 2025-11-18 23:32:41 +08:00
CakeCN
5e6e040753 fix 2025-11-18 22:42:29 +08:00
CakeCN
ede89cef53 fix load_full_markdown_file import bug 2025-11-18 22:15:45 +08:00
CakeCN
0fd548edcd save_load_learned 2025-11-17 23:14:55 +08:00
553a7724b3 allow clear chat log 2025-11-17 16:35:26 +08:00
Cai
0852e12118 cdn fix more 2025-11-17 14:04:52 +08:00
Cai
7dc4714ae9 index style 2025-11-13 21:08:30 +08:00
Cai
9dc81b1489 Merge branch 'main' into feature/paste_detecte_new 2025-11-13 20:18:39 +08:00
Cai
7862647b30 style fix 2025-11-13 20:15:53 +08:00
Cai
6b9c8e9e0c cdn change to local and use markdown and style change 2025-11-05 19:26:02 +08:00
0cf1896b5b paste detecte new 2025-11-05 19:00:02 +08:00
Cai
33844786a1 use markdown to edit 2025-11-03 16:14:57 +08:00
Cai
ce88ae645e use queue to allow send vscode_ws when it is none 2025-11-03 13:51:15 +08:00
a45e5f6f37 Merge branch 'main' into czh 2025-10-29 20:18:12 +08:00
6c38e9067d change namespace 2025-10-29 20:17:40 +08:00
159 changed files with 46687 additions and 2593 deletions

1
Html/.gitattributes vendored Normal file
View File

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

3164
Html/.log

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

@@ -0,0 +1,124 @@
# ACK机制支持指南
## 概述
客户端已实现基于emit机制的ACK确认功能允许客户端在发送消息后等待服务器确认从而支持同步和异步两种通信模式。
## 客户端改动
### 1. 新增参数
客户端的三个主要发送方法 `send_text``send_voice``send_image` 都新增了以下参数:
- `wait_for_ack: bool = False` - 是否等待服务器ACK确认
- `timeout: int = 5` - 等待ACK的超时时间
### 2. 消息格式变化
客户端发送的消息中新增了 `ack_id` 字段,格式为 `{id}_{timestamp}`,其中:
- `id` 是客户端ID
- `timestamp` 是当前时间的格式化字符串(精确到微秒)
### 3. ACK事件
客户端监听 `ack` 事件,用于接收服务器的确认消息。
## 后端WebSocket服务器需要的支持
### 1. 接收消息处理
当服务器收到客户端发送的消息时,需要:
1. 检查消息中是否包含 `ack_id` 字段
2. 如果包含 `ack_id`立即发送ACK确认消息不需要等待后续业务逻辑执行
3. 继续执行后续的业务逻辑
### 2. ACK消息格式
服务器需要向客户端发送 `ack` 事件,消息格式如下:
```json
{
"ack_id": "客户端发送的ack_id值"
}
```
### 3. 发送ACK的时机
**非常重要**:服务器必须在收到客户端消息后**立即**发送ACK确认而不是在业务逻辑执行完成后发送。这样可以确保客户端能够快速收到确认继续执行后续操作。
### 4. 实现示例Python Socket.IO
```python
# 服务器端示例代码
@sio.on('text_message') # 根据实际事件名调整
def handle_text_message(sid, data):
# 1. 立即发送ACK确认
if 'ack_id' in data:
sio.emit('ack', {'ack_id': data['ack_id']}, room=sid)
# 2. 执行后续业务逻辑(可能耗时较长)
# ... 业务逻辑代码 ...
@sio.on('voice_message') # 根据实际事件名调整
def handle_voice_message(sid, data):
# 1. 立即发送ACK确认
if 'ack_id' in data:
sio.emit('ack', {'ack_id': data['ack_id']}, room=sid)
# 2. 执行后续业务逻辑(可能耗时较长)
# ... 业务逻辑代码 ...
@sio.on('image_message') # 根据实际事件名调整
def handle_image_message(sid, data):
# 1. 立即发送ACK确认
if 'ack_id' in data:
sio.emit('ack', {'ack_id': data['ack_id']}, room=sid)
# 2. 执行后续业务逻辑(可能耗时较长)
# ... 业务逻辑代码 ...
```
## 使用示例
### 客户端同步调用等待ACK
```python
client = ASEngineClient(server_url, namespace, id)
client.connect()
# 同步调用等待ACK确认后再继续执行
result = client.send_text('text_message', 'Hello', wait_for_ack=True, timeout=3)
if result:
print("消息已被服务器确认收到")
# 执行后续操作
else:
print("等待ACK超时")
```
### 客户端异步调用不等待ACK
```python
client = ASEngineClient(server_url, namespace, id)
client.connect()
# 异步调用发送后立即返回不等待ACK
result = client.send_text('text_message', 'Hello', wait_for_ack=False)
if result:
print("消息已发送")
# 执行后续操作不需要等待ACK
```
## 注意事项
1. **向后兼容**:客户端的改动保持了向后兼容性,现有代码无需修改即可继续使用。
2. **超时处理**客户端会在指定的超时时间后停止等待ACK并返回False。
3. **资源清理**客户端会在收到ACK或超时时清理相关的ACK状态避免内存泄漏。
4. **唯一标识**`ack_id` 确保了每个消息的唯一性避免ACK混淆。
5. **性能考虑**等待ACK会阻塞当前线程建议仅在必要时使用同步模式。
## 总结
通过实现基于emit机制的ACK确认功能客户端和服务器之间的通信可以灵活支持同步和异步两种模式。服务器需要做的改动非常简单只需要在收到消息后立即发送ACK确认即可。
这种设计既保证了通信的可靠性,又不会影响业务逻辑的执行效率,是一种高效且可靠的通信方式。

View File

@@ -0,0 +1,326 @@
# ASEngine 数据同步机制文档
## 1. 概述
本文档介绍了如何通过 `learning_progess_process.py` 实现与 ASEngine大模型框架后端之间的运行时数据下载和上传。该机制为客户端与大模型服务之间提供了可靠的数据同步方案适用于需要在不同服务之间共享和同步用户数据的场景。
## 2. 使用场景
### 2.1 核心场景
- **用户学习进度同步**:将用户在客户端的学习进度同步到大模型框架后端,确保跨设备访问一致性
- **对话历史管理**:从大模型框架获取用户对话历史,用于客户端展示和分析
- **多服务数据共享**:在客户端应用与大模型服务之间建立数据桥接,实现数据互通
- **分布式系统协作**:支持多个服务实例之间的数据同步,确保系统一致性
### 2.2 扩展场景
- **离线数据同步**:支持在网络恢复后进行数据同步
- **数据备份与恢复**:通过上传/下载机制实现数据备份和恢复。
- **多租户数据隔离**:通过 namespace_token 实现不同租户数据隔离
## 3. 实现原理
### 3.1 系统架构
```
┌─────────────────────────┐ ┌─────────────────────────┐
│ 客户端应用 │ │ ASEngine │
│ (learning_progess_process.py) │ (大模型框架后端) │
├─────────────────────────┤ ├─────────────────────────┤
│ 1. get_multiagents_dialog_list │ ←─── 下载数据 ──── │ /api/sync/download │
│ 2. upload_learning_progress_to_cloud │ ──── 上传数据 ────→ │ /api/sync/upload │
└─────────────────────────┘ └─────────────────────────┘
```
### 3.2 数据流向
1. **数据下载流程**
- 客户端调用 `get_multiagents_dialog_list()` 方法
- 发送 POST 请求到 ASEngine 的 `/api/sync/download` 接口
- 获取用户对话历史数据
- 数据用于本地处理或存储
2. **数据上传流程**
- 客户端调用 `upload_learning_progress_to_cloud()` 方法
- 序列化待上传的数据(处理特殊类型如 datetime
- 发送 POST 请求到 ASEngine 的 `/api/sync/upload` 接口
- 接收上传结果并记录日志
## 4. 核心功能说明
### 4.1 数据下载功能
```python
def get_multiagents_dialog_list(user_id: str) -> List[Dict]:
"""
从 MultiAgents 框架获取用户对话列表
参数:
- user_id: 用户ID
返回:
- Dict[List]: 对话列表(key 为 History database)
"""
# 实现细节...
```
**功能特点**
- 从配置中动态获取 ASEngine URL 和认证信息
- 支持超时控制10秒
- 完善的错误处理和日志记录
- 返回结构化数据,便于后续处理
### 4.2 数据上传功能
```python
def upload_learning_progress_to_cloud(progress: Dict) -> Dict:
"""
将学习进度上传到云服务
参数:
- progress: 学习进度数据字典
返回:
- Dict: 上传结果包含success和message字段
"""
# 实现细节...
```
**功能特点**
- 自动处理数据序列化,支持复杂数据类型
- 完善的错误处理机制
- 详细的日志记录,便于问题排查
- 返回标准化结果格式
### 4.3 辅助功能
#### 4.3.1 学习进度保存
```python
def save_learning_progress(chatmanager, user_id: str) -> bool:
"""
保存用户学习进度到MongoDB
"""
# 实现细节...
```
#### 4.3.2 学习进度加载
```python
def load_learning_progress(user_id: str, material_id: str, chapter_name: str, lesson_name: str) -> Dict:
"""
加载用户学习进度
"""
# 实现细节...
```
## 5. 配置方法
### 5.1 环境配置
在 Flask 应用的配置中添加以下配置项:
```python
# ASEngine 配置
ASE_ENGINE_URL = "https://your-ase-engine-url.com" # ASEngine 服务地址
ASE_ENGINE_URL_TOKEN = "your-namespace-token" # 命名空间认证令牌
```
### 5.2 依赖安装
确保项目中已安装以下依赖:
```bash
pip install requests # HTTP 请求库
pip install pymongo # MongoDB 驱动如果使用MongoDB存储
```
## 6. 使用示例
### 6.1 数据下载示例
```python
from apps.services.course_services.learning_progess_process import get_multiagents_dialog_list
# 获取用户对话历史
dialog_list = get_multiagents_dialog_list(user_id="user123")
# 处理对话数据
his_data = dialog_list.get('data', [])
for dialog in his_data:
# 处理单个对话
print(f"对话ID: {dialog.get('id')}, 内容: {dialog.get('content')}")
```
### 6.2 数据上传示例
```python
from apps.services.course_services.learning_progess_process import upload_learning_progress_to_cloud
# 准备待上传的数据
progress_data = {
"user_id": "user123",
"multiagents_dialogs": {
"databases": {...}, # 对话数据库
"global_databases": {...}, # 全局数据库
"session_info_out": {...} # 会话信息
}
}
# 上传数据到 ASEngine
result = upload_learning_progress_to_cloud(progress_data)
# 处理上传结果
if result["success"]:
print("数据上传成功")
else:
print(f"数据上传失败: {result['message']}")
```
### 6.3 完整流程示例
```python
from apps.services.course_services.learning_progess_process import (
get_multiagents_dialog_list,
save_learning_progress,
load_learning_progress,
upload_learning_progress_to_cloud
)
# 1. 从 ASEngine 获取对话历史
dialog_list = get_multiagents_dialog_list(user_id="user123")
# 2. 保存到本地 MongoDB
save_learning_progress(chatmanager, user_id="user123")
# 3. 从本地加载学习进度
progress = load_learning_progress(
user_id="user123",
material_id="material456",
chapter_name="第1章",
lesson_name="第1节"
)
# 4. 将学习进度上传回 ASEngine
if progress["exists"]:
upload_result = upload_learning_progress_to_cloud(progress["data"])
print(f"上传结果: {upload_result}")
```
## 7. 代码结构与扩展
### 7.1 核心模块
| 模块 | 功能描述 | 文件位置 |
| --------------- | -------------------------- | ------------------------------------------------------------- |
| 数据同步服务 | 处理与 ASEngine 的数据交互 | `apps/services/course_services/learning_progess_process.py` |
| JSON 序列化工具 | 处理复杂数据类型序列化 | `apps/utils/json_serializer.py` |
| 配置管理 | 管理 ASEngine 连接配置 | `apps/config.py` |
### 7.2 扩展建议
1. **添加重试机制**
```python
def upload_with_retry(progress: Dict, max_retries: int = 3) -> Dict:
for i in range(max_retries):
result = upload_learning_progress_to_cloud(progress)
if result["success"]:
return result
time.sleep(1) # 指数退避算法可进一步优化
return {"success": False, "message": f"上传失败,已重试{max_retries}次"}
```
2. **添加数据校验**
```python
def validate_progress_data(progress: Dict) -> bool:
"""验证进度数据格式"""
required_fields = ["user_id", "multiagents_dialogs"]
for field in required_fields:
if field not in progress:
return False
return True
```
3. **支持异步操作**
```python
import asyncio
import aiohttp
async def async_upload_learning_progress(progress: Dict) -> Dict:
"""异步上传学习进度"""
# 异步实现
pass
```
## 8. 最佳实践
### 8.1 错误处理
- 始终检查 API 返回结果的 `success` 字段
- 实现适当的日志记录,便于问题排查
- 对网络异常和超时进行合理处理
### 8.2 性能优化
- 避免频繁的小数据上传,考虑批量处理
- 对大文件进行压缩后上传
- 合理设置超时时间,避免长时间阻塞
### 8.3 安全性
- 确保 ASEngine URL 和令牌安全存储,避免硬编码
- 使用 HTTPS 协议进行数据传输
- 对敏感数据进行加密处理
### 8.4 监控与告警
- 添加数据同步成功率监控
- 对同步失败情况进行告警
- 记录详细的操作日志,便于审计
## 9. 后续项目参考建议
### 9.1 架构设计
- **分层设计**:将数据同步逻辑与业务逻辑分离,提高代码可维护性
- **接口标准化**:定义统一的数据格式和接口规范,便于扩展
- **依赖注入**:使用依赖注入模式,提高代码可测试性
### 9.2 代码实现
- **模块化**:将不同功能模块拆分为独立函数或类
- **类型注解**:使用 Type Hints 提高代码可读性和类型安全性
- **文档完善**:为每个函数添加详细的文档字符串
- **测试覆盖**:编写单元测试和集成测试,确保功能正确性
### 9.3 扩展功能
- **支持多种数据格式**:如 JSON、Protobuf 等
- **添加数据版本控制**:支持不同版本数据的兼容处理
- **实现增量同步**:只同步变化的数据,提高同步效率
- **支持断点续传**:对于大文件上传实现断点续传功能
## 10. 总结
本文档介绍了基于 `learning_progess_process.py` 实现的 ASEngine 数据同步机制,包括使用场景、实现原理、核心功能、配置方法和使用示例。该机制为客户端与大模型服务之间提供了可靠的数据同步方案,可作为后续项目的参考模板。
通过遵循本文档中的最佳实践和扩展建议,可以构建更加健壮、高效和可扩展的数据同步系统,满足不同场景下的数据同步需求。
## 11. 版本历史
| 版本 | 日期 | 描述 | 作者 |
| ---- | ---------- | ------------ | ------------ |
| v1.0 | 2025-12-10 | 初始版本 | 系统自动生成 |
| v1.1 | YYYY-MM-DD | 添加异步支持 | XXX |
| v1.2 | YYYY-MM-DD | 实现增量同步 | XXX |
## 12. 参考资料
- [Flask 官方文档](https://flask.palletsprojects.com/)
- [Requests 库文档](https://requests.readthedocs.io/)
- [ASEngine API 文档](https://your-ase-engine-docs.com/)

View File

@@ -0,0 +1,113 @@
# JSON Serializer 用法文档
## 概述
`json_serializer` 是一个通用的JSON序列化工具用于处理Python中常见的非JSON序列化类型`datetime``date``time``ObjectId`等。
## 支持的类型
- `datetime`: 转换为ISO格式字符串
- `date`: 转换为ISO格式字符串
- `time`: 转换为ISO格式字符串
- `ObjectId`: 转换为字符串
- `bytes`: 转换为UTF-8字符串
- `set`/`frozenset`: 转换为列表
- 其他不可序列化类型: 转换为字符串表示
## 安装位置
位于 `apps/utils/json_serializer.py`
## 使用方法
### 导入
```python
from apps.utils.json_serializer import json_serialize, json_serialize_dict, JSONEncoder
```
### 函数说明
1. **json_serialize(obj)**: 将对象序列化为JSON字符串
- 参数: `obj` - 要序列化的对象
- 返回: JSON字符串
2. **json_serialize_dict(obj)**: 将对象转换为可JSON序列化的字典
- 参数: `obj` - 要序列化的对象
- 返回: 可JSON序列化的字典
3. **JSONEncoder**: 自定义JSON编码器类可直接用于`json.dumps()`
### 示例
#### 基本使用
```python
from datetime import datetime
from apps.utils.json_serializer import json_serialize_dict
# 创建包含datetime的字典
data = {
"user_id": "test_user",
"updated_at": datetime.now(),
"scores": [85, 90, 95]
}
# 序列化
serialized = json_serialize_dict(data)
print(serialized)
# 输出: {"user_id": "test_user", "updated_at": "2025-12-10T15:30:45.123456", "scores": [85, 90, 95]}
```
#### 在API请求中使用
```python
import requests
from apps.utils.json_serializer import json_serialize_dict
# 准备请求数据
data = {
"user_id": "test_user",
"updated_at": datetime.now(),
"progress": {"chapter1": "completed", "chapter2": "in_progress"}
}
# 序列化数据
serialized_data = json_serialize_dict(data)
# 发送请求
response = requests.post(
"https://api.example.com/upload",
json=serialized_data,
headers={"Content-Type": "application/json"}
)
```
#### 直接使用JSONEncoder
```python
import json
from datetime import datetime
from apps.utils.json_serializer import JSONEncoder
# 创建数据
data = {"updated_at": datetime.now()}
# 使用自定义编码器
json_str = json.dumps(data, cls=JSONEncoder, ensure_ascii=False)
print(json_str)
# 输出: {"updated_at": "2025-12-10T15:30:45.123456"}
```
## 应用场景
1. API请求中的数据序列化
2. 保存数据到文件或数据库
3. 跨服务数据传输
4. 日志记录中的复杂对象序列化
## 注意事项
- 对于自定义对象,会尝试序列化其`__dict__`属性
- 对于无法序列化的类型,会转换为字符串表示
- 确保导入路径正确,特别是在不同模块中使用时

View File

@@ -0,0 +1,165 @@
# HelpTip 组件文档
## 功能介绍
HelpTip 是一个通用的提示框组件,用于在鼠标悬停时显示提示信息。它具有以下特点:
- 提示框跟随鼠标移动
- 支持多行文本
- 自动防出界
- 平滑的显示/隐藏动画
- 支持在不同页面复用
- 代码标准化,易于维护
## 文件结构
```
/static/
├── css/
│ └── helpTip.css # 样式文件
└── js/
└── helpTip.js # 核心功能文件
```
## 使用方法
### 1. 引入文件
在需要使用 HelpTip 的 HTML 页面中引入以下文件:
```html
<!-- 样式文件 -->
<link rel="stylesheet" href="/static/css/helpTip.css">
<!-- 功能文件 -->
<script src="/static/js/helpTip.js"></script>
```
### 2. 基本用法
```javascript
// 获取需要绑定提示框的元素
const element = document.getElementById('target-element');
// 绑定提示框
attachImmediateTooltip(element, '这是一条提示信息');
```
### 3. 支持换行的提示信息
```javascript
attachImmediateTooltip(element, `这是一条\n多行提示信息\n\n支持换行符`);
```
### 4. 与问号图标配合使用
```html
<!-- HTML -->
<span>这是一个元素<span class="help-icon">?</span></span>
```
```javascript
// JavaScript
const helpIcon = document.querySelector('.help-icon');
attachImmediateTooltip(helpIcon, '这是一条详细的提示信息');
```
## API 说明
### attachImmediateTooltip(anchorEl, text)
绑定提示框到指定元素。
#### 参数
- `anchorEl` (HTMLElement): 触发提示框的元素
- `text` (string): 提示框显示的文本内容,支持换行符 `\n`
#### 返回值
- `Function`: 清理函数,用于移除事件监听器
## 样式说明
### 问号图标样式
| 类名 | 说明 |
|------|------|
| `.help-icon` | 问号图标基础样式 |
| `.help-icon:hover` | 问号图标悬停样式 |
### 提示框样式
| 类名 | 说明 |
|------|------|
| `.tooltip-portal` | 提示框基础样式 |
| `.tooltip-portal.show` | 提示框显示状态 |
| `.tooltip-portal.fade` | 提示框动画效果 |
## 示例场景
### 1. 进度条提示
```javascript
// 为进度条的每个阶段绑定提示框
for (let i = 0; i < progressItems.length; i++) {
const item = progressItems[i];
attachImmediateTooltip(item, `阶段 ${i + 1}: ${item.title}`);
}
```
### 2. 表单字段提示
```javascript
// 为表单字段绑定提示框
const formFields = document.querySelectorAll('.form-field');
formFields.forEach(field => {
const tip = field.getAttribute('data-tip');
if (tip) {
attachImmediateTooltip(field, tip);
}
});
```
### 3. 工具栏图标提示
```javascript
// 为工具栏图标绑定提示框
const toolbarIcons = document.querySelectorAll('.toolbar-icon');
toolbarIcons.forEach(icon => {
const title = icon.getAttribute('title');
if (title) {
attachImmediateTooltip(icon, title);
}
});
```
## 注意事项
1. 确保在 DOM 加载完成后再调用 `attachImmediateTooltip` 函数
2. 如果元素是动态生成的,需要在元素生成后再绑定提示框
3. 提示框会自动添加到 `document.body` 中,不会被父元素的 `overflow` 属性裁切
4. 提示框的 `z-index` 为 999999确保它能覆盖所有元素
5. 提示框默认支持多行文本,使用 `white-space: pre-wrap` 样式
## 浏览器兼容性
- Chrome/Edge (最新版)
- Firefox (最新版)
- Safari (最新版)
## 代码优化建议
1. 对于大量元素需要绑定提示框的情况,建议使用事件委托
2. 如果页面中不再需要使用提示框,可以调用返回的清理函数移除事件监听器
3. 提示文本应简洁明了,避免过长
## 更新日志
- v1.0.0: 初始版本,实现了基本的提示框功能
- v1.0.1: 优化了样式,增加了动画效果
- v1.0.2: 支持多行文本,自动防出界
## 贡献指南
如果您发现问题或有改进建议,欢迎提出 Issue 或 Pull Request。

View File

@@ -48,4 +48,5 @@ class Config:
}
ASE_ENGINE_URL = GLOBAL_CONFIG['ASE_ENGINE']['url']
ASE_ENGINE_URL_TOKEN = GLOBAL_CONFIG['ASE_ENGINE']['url_token']
ASE_ENGINE_NAMESPACE = GLOBAL_CONFIG['ASE_ENGINE']['namespace']

View File

@@ -5,6 +5,7 @@ import os
from datetime import datetime
from typing import Dict, Callable, Any, Optional
from functools import partial
from threading import Event
class ASEngineClient:
def __init__(self, server_url: str, namespace: str, id:str):
@@ -29,9 +30,8 @@ class ASEngineClient:
self._on_connect_callback_entry=None
self._on_connect_callback_initData=None
# 注册基础事件处理函数
# ACK相关
self.ack_events: Dict[str, Event] = {} # 存储ack_id和对应的Event对象
self._register_base_events()
def _register_base_events(self):
@@ -43,7 +43,11 @@ class ASEngineClient:
self.sid = self.sio.get_sid(namespace=self.namespace)
print(f"Connected to server. SID: {self.sid}")
if self._on_connect_callback is not None:
self._on_connect_callback(self._on_connect_callback_entry, self._on_connect_callback_initData)
self._on_connect_callback(
self,
self._on_connect_callback_entry,
init_data=self._on_connect_callback_initData
)
# 断开连接事件
@self.sio.on('disconnect', namespace=self.namespace)
@@ -62,28 +66,59 @@ class ASEngineClient:
@self.sio.on('response', namespace=self.namespace)
def on_response(data):
print(f"Received response: {data}")
# ACK事件处理
@self.sio.on('ack', namespace=self.namespace)
def on_ack(data):
ack_id = data.get('ack_id')
if ack_id and ack_id in self.ack_events:
event = self.ack_events[ack_id]
event.set() # 触发事件,通知等待的线程
def register_on_connect_entry(self, callback: Callable, entry: Any, init_data: Any)->bool:
"""注册连接事件回调函数
参数:
- callback: 连接事件回调函数,接受 **kwargs 参数
- entry: 回调关联的条目对象
- init_data: 初始化数据
回调关键字参数:
- entry: 回调关联的条目对象
- init_data: 初始化数据
"""
if not callable(callback):
return False
self._on_connect_callback = callback
self._on_connect_callback_entry=entry
self._on_connect_callback_initData=init_data
return True
def register_route_with_entry(self, route: str,
callback: Optional[Callable] = None,
entry: Any = None,
init_data: Optional[Any] = None) -> bool:
"""注册带条目的路由回调函数
参数:
- route: 路由名称
- callback: 回调函数,接受 **kwargs 参数
- entry: 回调关联的条目对象
- init_data: 初始化数据
回调关键字参数:
- entry: 回调关联的条目对象
- data: 接收到的消息数据
- init_data: 初始化数据
"""
if not route:
print("Route cannot be empty")
return False
if callback is None:
bound_cb = self._default_callback
else:
bound_cb = partial(callback, entry) # 等价于 lambda data, init: callback(entry, data, init)
else:bound_cb = callback
self.routes[route] = {
'callback': bound_cb,
'entry': entry,
@@ -98,8 +133,9 @@ class ASEngineClient:
return
cb = route_info['callback'] # 已经绑定过 entry
init = route_info.get('init_data')
entry = route_info.get('entry')
try:
cb(data, init) # 现在只传 data / init_data
cb(self, entry, data=data, init_data=init) # 传递关键字参数
except Exception as e:
print(f"Callback error on route '{_route}': {e}")
@@ -107,11 +143,17 @@ class ASEngineClient:
def register_route(self, route: str, callback: Optional[Callable] = None, init_data: Optional[Any] = None):
"""
注册一个路由及其回调函数
:param route: 路由名称
:param callback: 回调函数,接收消息数据作为参数
:return: 是否注册成功
"""注册路由回调函数
参数:
- route: 路由名称
- callback: 回调函数,接受 **kwargs 参数
- init_data: 初始化数据
回调关键字参数:
- entry: 回调关联的条目对象(此处为 None
- data: 接收到的消息数据
- init_data: 初始化数据
"""
if not route:
print("Route cannot be empty")
@@ -119,7 +161,9 @@ class ASEngineClient:
# 存储路由和回调
self.routes[route] = {
'callback': callback if callback else self._default_callback
'callback': callback if callback else self._default_callback,
'entry': None,
'init_data': init_data
}
# 注册事件处理器
@@ -128,15 +172,27 @@ class ASEngineClient:
print(f"Received message on route '{route}': {data}")
if 'callback' in self.routes[route]:
try:
self.routes[route]['callback'](None, data, init_data)
self.routes[route]['callback'](
self,
entry=None,
data=data,
init_data=init_data
)
except Exception as e:
print(f"Error in callback for route '{route}': {e}")
print(f"Route '{route}' registered successfully")
return True
def _default_callback(self, entry: Any, data: Any, init_data: Optional[Any] = None):
"""默认回调函数"""
def _default_callback(self, **kwargs):
"""默认回调函数
关键字参数:
- entry: 回调关联的条目对象
- data: 接收到的消息数据
- init_data: 初始化数据
"""
data = kwargs.get('data', {})
print(f"Default callback received: {data}")
def connect(self, timeout: int = 5) -> bool:
@@ -169,12 +225,16 @@ class ASEngineClient:
self.sid = None
print("Disconnected from server")
def send_text(self, route: str, text: str, id:str="") -> bool:
def send_text(self, route: str, text: str, id:str="", wait_for_ack: bool = False, timeout: int = 5, return_event: bool = False) -> Any:
"""
发送文本消息
:param route: 目标路由
:param text: 要发送的文本
:return: 是否发送成功
:param id: 发送者ID
:param wait_for_ack: 是否等待服务器ACK确认
:param timeout: 等待ACK的超时时间
:param return_event: 是否返回事件对象和ack_id而不是等待ACK
:return: 如果wait_for_ack为True且return_event为False返回是否收到ACK如果return_event为True返回(event, ack_id);否则返回是否发送成功
"""
if not self.connected:
print("Not connected to server")
@@ -183,19 +243,50 @@ class ASEngineClient:
id = self.id
try:
self.sio.emit(route, {'type': 'text', 'data': text, 'id': id}, namespace=self.namespace)
print(f"Sent text to route '{route}': {text}")
return True
# 生成唯一ack_id
ack_id = f"{id}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}"
data = {
'type': 'text',
'data': text,
'id': id,
'ack_id': ack_id
}
self.sio.emit(route, data, namespace=self.namespace)
if not wait_for_ack:
return True
# 初始化ACK事件
event = Event()
self.ack_events[ack_id] = event
# 如果需要返回事件对象,直接返回
if return_event:
return event, ack_id
# 等待ACK或超时
received_ack = event.wait(timeout=timeout)
# 清理ACK事件
del self.ack_events[ack_id]
if received_ack:
return True
else:
print(f"ACK timeout for message: {ack_id}")
return False
except Exception as e:
print(f"Failed to send text to route '{route}': {e}")
return False
def send_voice(self, route: str, voice_data: bytes, id:str=None) -> bool:
def send_voice(self, route: str, voice_data: bytes, id:str=None, wait_for_ack: bool = False, timeout: int = 5) -> bool:
"""
发送语音数据
:param route: 目标路由
:param voice_data: 语音数据字节流
:return: 是否发送成功
:param id: 发送者ID
:param wait_for_ack: 是否等待服务器ACK确认
:param timeout: 等待ACK的超时时间
:return: 是否发送成功若wait_for_ack为True则返回是否收到ACK
"""
if id is None:
id = self.id
@@ -204,8 +295,9 @@ class ASEngineClient:
return False
try:
# 编码语音数据为base64
# voice_data = 'data:image/jpeg;base64,' +base64.b64encode(voice_data).decode('utf-8')
# 生成唯一ack_id
ack_id = f"{id}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}"
# 发送数据
self.sio.emit(
route,
@@ -214,24 +306,43 @@ class ASEngineClient:
'data': voice_data,
'timestamp': int(time.time() * 1000),
'sampleRate': 16000,
'id': id
'id': id,
'ack_id': ack_id
},
namespace=self.namespace
)
# 如果不需要等待ACK直接返回True
if not wait_for_ack:
return True
print(f"Sent voice file to route '{route}'")
return True
# 初始化ACK事件
event = Event()
self.ack_events[ack_id] = event
# 等待ACK或超时
received_ack = event.wait(timeout=timeout)
# 清理ACK事件
del self.ack_events[ack_id]
if received_ack:
return True
else:
print(f"ACK timeout for message: {ack_id}")
return False
except Exception as e:
print(f"Failed to send voice to route '{route}': {e}")
return False
def send_image(self, route: str, file_path: str, id:str) -> bool:
def send_image(self, route: str, file_path: str, id:str, wait_for_ack: bool = False, timeout: int = 5) -> bool:
"""
发送图片文件
:param route: 目标路由
:param file_path: 图片文件路径
:return: 是否发送成功
:param id: 发送者ID
:param wait_for_ack: 是否等待服务器ACK确认
:param timeout: 等待ACK的超时时间
:return: 是否发送成功若wait_for_ack为True则返回是否收到ACK
"""
if not self.connected:
print("Not connected to server")
@@ -251,18 +362,40 @@ class ASEngineClient:
# 获取文件名
filename = os.path.basename(file_path)
# 生成唯一ack_id
ack_id = f"{id}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}"
# 发送数据
self.sio.emit(
route,
{
'type': 'image',
'data': image_data,
'id': id
'id': id,
'ack_id': ack_id
},
namespace=self.namespace
)
print(f"Sent image file to route '{route}': {filename}")
return True
# 如果不需要等待ACK直接返回True
if not wait_for_ack:
return True
# 初始化ACK事件
event = Event()
self.ack_events[ack_id] = event
# 等待ACK或超时
received_ack = event.wait(timeout=timeout)
# 清理ACK事件
del self.ack_events[ack_id]
if received_ack:
return True
else:
print(f"ACK timeout for message: {ack_id}")
return False
except Exception as e:
print(f"Failed to send image to route '{route}': {e}")
return False
@@ -286,6 +419,17 @@ class HSAEngineClient(ASEngineClient):
super().__init__(server_url, namespace, id)
self.chatmanager = chatmanager
def loadmarkdown(self, fmd, fmdp, fsp):
self.send_text('markdown-in', fmd, self.id)
self.send_text('markdown-prompt-in', fmdp, self.id)
self.send_text('score-prompt-in', fsp, self.id)
event1, ack_id1 = self.send_text('markdown-in', fmd, self.id, wait_for_ack=True, return_event=True)
event2, ack_id2 = self.send_text('markdown-prompt-in', fmdp, self.id, wait_for_ack=True, return_event=True)
event3, ack_id3 = self.send_text('score-prompt-in', fsp, self.id, wait_for_ack=True, return_event=True)
timeout = 5 # 超时时间
success1 = event1.wait(timeout=timeout)
success2 = event2.wait(timeout=timeout)
success3 = event3.wait(timeout=timeout)
if ack_id1 in self.ack_events:
del self.ack_events[ack_id1]
if ack_id2 in self.ack_events:
del self.ack_events[ack_id2]
if ack_id3 in self.ack_events:
del self.ack_events[ack_id3]
return success1 and success2 and success3

View File

@@ -14,6 +14,8 @@ from dataclasses import dataclass, field
from typing import Dict, Optional
import subprocess
import threading
from collections import deque
from eventlet.semaphore import Semaphore
from apps.utils import *
EMPTY_CHAPTER_CHAIN = [ Chapter(1, CHAPTER_FOCUS, "本章是未打开某个具体章节时的默认章节。", "处于本章节时不会有任何章节跳转。请作为一名经验丰富的算法教师,回答用户的问题。") ]
@@ -31,8 +33,8 @@ class ProcRecord:
class ChatManager:
_procs: Dict[str, ProcRecord] = {}
_lock = threading.RLock()
def __init__(self):
self.chapter_chain_now = -1
def __init__(self, restart=False):
self.restart = restart
self.ase_client = None
self.app = None
self.socketio = None
@@ -46,8 +48,11 @@ class ChatManager:
self.chapter_chain = None
self.function_manager = FunctionManager()
self.bb = None
self.chat_historys = None
pass
self.chat_historys = []
self.scores = []
self.vscode_ws = None
self._vscode_queue = deque()
self._lock = Semaphore(1)
def init(self,user_uuid,ase_client,raw_markdown,raw_markdown_prompts,raw_score_prompts,material_id,chapter_name, lesson_name, max_iter = 5, app=None, socketio=None):
@@ -64,9 +69,45 @@ 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'):
"""统一发送入口:未就绪则入队,就绪则直接 emit。"""
if room is None:
room = self.user_uuid
with self._lock:
if self.vscode_ws is not None:
# 已就绪,直接发
self.vscode_ws.emit(event, data, room=room, namespace=namespace)
else:
# 未就绪,入队缓存
self._vscode_queue.append((event, data, room, namespace))
def set_vscode_ws(self, ws):
"""在 on_login 时调用,注册 websocket 并立刻 flush 队列。"""
with self._lock:
self.vscode_ws = ws
self._flush_vscode_queue_locked()
def _flush_vscode_queue_locked(self):
"""发送缓存的消息(需在持锁状态下调用)。"""
while self._vscode_queue:
event, data, room, namespace = self._vscode_queue.popleft()
# 这里假设 ws 仍然有效;如果发送失败可以考虑 try/except 并回滚入队
self.vscode_ws.emit(event, data, room=room, namespace=namespace)
def mark_vscode_disconnected(self):
"""可在 on_disconnect 里调用,标记断开(之后会继续缓存)。"""
with self._lock:
self.vscode_ws = None
self._vscode_queue.clear()
def disconnect(self, uuid):
try:
self.mark_vscode_disconnected()
self.stop(uuid)
self.ase_client.disconnect()
except Exception as e:
@@ -77,10 +118,11 @@ class ChatManager:
nowchapter_content = self.chapter_chain[self.chapter_chain_now].chapter
code_texts = extract_code_blocks(nowchapter_content)
for code_text in code_texts:
self.vscode_ws.emit('code-file', {'type':'create', 'data':code_text, 'chapter_title': self.chapter_chain[self.chapter_chain_now].title}, room=self.user_uuid ,namespace='/vscode')
self.send_to_vscode('code-file', {'type':'create', 'data':code_text, 'chapter_title': self.chapter_chain[self.chapter_chain_now].title}, room=self.user_uuid ,namespace='/vscode')
def next_chapter(self):
print(f"now next chapter {self.chapter_chain_now} and len {len(self.chapter_chain)}")
assert self.chapter_chain_now + 1 < len(self.chapter_chain), "chapter_chain_now out of range"
self.chapter_chain_now += 1
self.bb.next_chapter()
@@ -91,10 +133,11 @@ class ChatManager:
self.chapter_chain_now = chapter_index
self.chapter_chain[chapter_index].Focus()
def load_next_chapter(self):
print(f"now load next chapter markdown {self.chapter_chain_now}")
def load_now_chapter(self, load_code = True):
print(f"now load now chapter markdown {self.chapter_chain_now}")
self.ase_client.loadmarkdown(self.chapter_chain[self.chapter_chain_now].chapter, self.chapter_chain[self.chapter_chain_now].chapter_prompt, self.chapter_chain[self.chapter_chain_now].score_prompt)
self.load_code_in_markdown()
if load_code:
self.load_code_in_markdown()
def upload_bb(self):

View File

@@ -0,0 +1,88 @@
import requests
from flask_socketio import Namespace
def clear_user_session(asengine_url, asengine_token, user_id):
'''
快速清理用户会话的函数
'''
try:
base_url = asengine_url
clear_url = f"{base_url}/api/sync/clear_user_session"
payload = {
'namespace_url': asengine_token, # WebSocket命名空间URL
'user_id': user_id, # 用户ID对应session_info.id
'clear_type': 'all' # 清理类型,默认为全部
}
headers = {
'Content-Type': 'application/json'
}
response = requests.post(clear_url, json=payload, headers=headers)
response.raise_for_status() # 检查请求是否成功
print(f"User session {user_id} cleared successfully. Response: {response.json()}")
except Exception as e:
print(f"Error clearing user session: {e}")
def on_connect_to_ase(client, entry, init_data, **kwargs):
'''
此时刚刚建立好aseclient连接并向前端发送打开code-server-like指令。
'''
namespace_entry, ase_client = entry
namespace_entry.emit('open-iframe',"", room=init_data['room'], namespace='/agent')
namespace_entry.emit('message', "Agents服务已连接, 加载教案中", room=init_data['room'], namespace='/agent')
restart = init_data.get("restart", False)
# 不再需要手动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')
#接受消息回来直接让message监听发送
def receive_ase_paste_detected(client_entry, namespace_entry, data, init_data):
print("receive_ase_paste_detected", data)
client_entry.chatmanager.chat_historys.append({'role': 'assistant', 'content': data['data']})
namespace_entry.emit('message', data['data'], room=init_data['room'], namespace='/agent')
def receive_ase_dialog(client_entry, namespace_entry, data, init_data):
print("receive_ase_dialog", data)
client_entry.chatmanager.chat_historys.append({'role': 'assistant', 'content': data['data']})
namespace_entry.emit('message', data['data'], room=init_data['room'], namespace='/agent')
def receive_ase_message_hint(client_entry, namespace_entry, data, init_data):
print("receive_ase_message_hint", data)
namespace_entry.emit('message-hint', data['data'], room=init_data['room'], namespace='/agent')
#接受语音信息
def receive_ase_voice_out(client_entry, namespace_entry, data, init_data):
namespace_entry.emit('voiceMessage', data['data'], room=init_data['room'], namespace='/agent')
def receive_ase_process(client_entry, namespace_entry, data, init_data):
namespace_entry.emit('process', data['data'], room=init_data['room'], namespace='/agent')
def receive_ase_judge(client_entry, namespace_entry, data, init_data):
print("receive_ase_judge", data)
namespace_entry.emit('request_function', data, room=init_data['room'], namespace='/agent') # data 是一个列表 /dict也支持
def receive_ase_next_chapter(client_entry, namespace_entry, data, init_data):
print("receive_ase_next_chapter", data)
namespace_entry.emit('request_function',data, room=init_data['room'], namespace='/agent') # data 是一个列表 /dict也支持
def receive_ase_chapter_score(client_entry, user_and_manager, data, init_data):
print("load_next_chapter")
user, chatmanager = user_and_manager
if len(chatmanager.scores) >= chatmanager.chapter_chain_now:
chatmanager.scores[chatmanager.chapter_chain_now - 1] = data.get('data', None)
else: chatmanager.scores.append(data.get('data', None))
user.set_course_progress(chatmanager.chat_historys, chatmanager.scores, chatmanager.course_id, chatmanager.chapter_name, chatmanager.lesson_name)
chatmanager.load_now_chapter()
chatmanager.ase_client.send_text("chapter-start", "")

View File

@@ -57,9 +57,14 @@ def realtime_response(config: dict, realtime_action: dict) -> None:
# emit('paste_detected', paste_data)
#判断一下,如果粘帖内容过多就发送,后期可加入更多条件判断
if bb.pasted_content and len(bb.pasted_content) > 250:
chatmanager.ase_client.send("pasted_detected_in", {"file_path": file_path, "content": bb.pasted_content})
if bb.pasted_content and len(bb.pasted_content) > 200:
print(f"粘贴内容 : {bb.pasted_content[:100]}...")
print(f"ready to send")
try:
result = chatmanager.ase_client.send_text("pasted_detected_in", bb.pasted_content)
print(f"Send result: {result}")
except Exception as e:
print(f"Error sending: {e}")
elif rtype == "fileEdit":
file_path = realtime_action.get("filePath")

View File

@@ -27,7 +27,7 @@ def get_text_file(file_name):
response = requests.get(file_name)
if response.status_code == 200:
print(file_name)
# print(file_name)
# print(f"get_text_file: {response.text}")
return response.text

View File

@@ -1,424 +1,14 @@
# myapp/services/course_service.py
import re
from typing import Dict, List
import requests
import json
from typing import Dict, List, Optional
from flask import current_app
from urllib.parse import urlparse
from datetime import datetime
from bson import ObjectId
from ..services.cos_service import upload_file, delete_file
from ..models.material import Material
from .course_services.material_process import *
def get_new_edit_materials(num: int)->List[Material]:
mongo = current_app.extensions["mongo"]
materials = mongo.db.materials.find().sort("updated_at", -1).limit(num)
returnCode = []
for material in materials:
returnCode.append(Material(**material))
returnCode[-1]._id = str(material['_id'])
return returnCode
def user_selected_course(user_obj):
briefs = []
for cid in getattr(user_obj, "select_course", []):
course_obj = load_material(cid)
brief={}
brief["_id"] = cid
brief["name"] = course_obj.name
brief["description"] = course_obj.description
brief["image_url"] = course_obj.image_url
brief["created_at"] = course_obj.created_at.strftime("%Y-%m-%d %H:%M:%S")
brief["updated_at"] = course_obj.updated_at.strftime("%Y-%m-%d %H:%M:%S")
brief["chapters"] = [chapter.model_dump() for chapter in course_obj.chapters]
briefs.append(brief)
return briefs
def user_selected_course_briefs(user_obj):
"""根据用户对象的选课列表,拼装课程简要信息数组"""
briefs = []
for cid in getattr(user_obj, "select_course", []):
course_obj = load_material(cid)
brief={}
brief["_id"] = cid
brief["name"] = course_obj.name
brief["description"] = course_obj.description
brief["image_url"] = course_obj.image_url
brief["created_at"] = course_obj.created_at.strftime("%Y-%m-%d %H:%M:%S")
brief["updated_at"] = course_obj.updated_at.strftime("%Y-%m-%d %H:%M:%S")
briefs.append(brief)
return briefs
def create_material(teacher_id, material_name, description, chapters, image_url):
material = Material(
name=material_name,
description=description,
teacher_id=teacher_id,
created_at=datetime.now(),
updated_at=datetime.now(),
chapters=chapters,
image_url=image_url
)
mongo = current_app.extensions["mongo"]
material_id = mongo.db.materials.insert_one(material.model_dump()).inserted_id
return str(material_id)
def load_material(material_id):
mongo = current_app.extensions["mongo"]
material = mongo.db.materials.find_one({"_id": ObjectId(material_id)})
material['_id'] = material_id
return Material(**material)
def update_material(material_id, chapters):
mongo = current_app.extensions["mongo"]
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
if material:
mongo.db.materials.update_one(
{'_id': ObjectId(material_id)},
{'$set': {'chapters': chapters, 'updated_at': datetime.now()}}
)
return True
return False
def add_material_chapter(material_id, chapter_name, teacher_id):
mongo = current_app.extensions["mongo"]
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
if material['teacher_id'] != teacher_id:
return False
if material:
mongo.db.materials.update_one(
{'_id': ObjectId(material_id)},
{'$push': {'chapters': {'chapter_name': chapter_name, 'lessons': []}}})
return True
def rename_material_structure(material_id, is_chapter, chapter_name, lesson_name, new_name):
mongo = current_app.extensions["mongo"]
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
# 先检查是否存在
if is_chapter:
for chapter in material['chapters']:
if chapter['chapter_name'] == chapter_name:
break
return False
for chapter in material['chapters']:
if chapter['chapter_name'] == new_name:
return False
else:
for chapter in material['chapters']:
if chapter['chapter_name'] == chapter_name:
break
return False
for lesson in chapter['lessons']:
if lesson['lesson_name'] == lesson_name:
break
for lesson in chapter['lessons']:
if lesson['lesson_name'] == new_name:
return False
if is_chapter:
mongo.db.materials.update_one(
{'_id': ObjectId(material_id)},
{'$set': {'chapters.$[c].chapter_name': new_name}},
array_filters=[{'c.chapter_name': chapter_name}]
)
else:
mongo.db.materials.update_one(
{'_id': ObjectId(material_id)},
{'$set': {'chapters.$[c].lessons.$[l].lesson_name': new_name}},
array_filters=[{'c.chapter_name': chapter_name}, {'l.lesson_name': lesson_name}]
)
return True
def delete_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id):
"""
删除指定课程的课时。
删除后,更新 MongoDB 中对应的章节中的 lesson 列表。
"""
mongo = current_app.extensions["mongo"]
# 查询 material 是否存在,且确保该 teacher_id 对应的 material
material = mongo.db.materials.find_one({'_id': ObjectId(material_id), 'teacher_id': teacher_id})
for chapter in material['chapters']:
if chapter['chapter_name'] == chapter_name:
for lesson in chapter['lessons']:
if lesson['lesson_name'] == lesson_name:
if lesson['markdown_lesson_file_link']!='':
delete_file(lesson['markdown_lesson_file_link'])
if lesson['markdown_prompt_file_name']!='':
delete_file(lesson['markdown_prompt_file_name'])
if lesson['markdown_score_prompt_file_link']!='':
delete_file(lesson['markdown_score_prompt_file_link'])
if not material:
return False # 找不到对应的 material 或 teacher_id 不匹配
# 删除 lesson 的条件
res = mongo.db.materials.update_one(
{
"_id": ObjectId(material_id),
"teacher_id": teacher_id # 确保是该教师的课程
},
{
"$pull": {
"chapters.$[c].lessons": {
"lesson_name": lesson_name # 根据 lesson_name 来删除
}
},
"$set": {"updated_at": datetime.now()} # 更新修改时间
},
array_filters=[{"c.chapter_name": chapter_name}] # 找到匹配的章节
)
# 如果删除操作成功res.modified_count 应为 1
return res.modified_count == 1
def reorder_material_structure(material_id, order):
mongo = current_app.extensions["mongo"]
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
if not material:
return False
chapter_map = {ch["chapter_name"]: ch for ch in material["chapters"]}
new_chapters = []
for chapter_order in order:
ch_name = chapter_order['chapter_name']
if ch_name not in chapter_map:
continue # 如果前端传了不存在的章节名,可以选择跳过或报错
original_ch = chapter_map[ch_name]
# 建立 lesson_name -> lesson dict
lesson_map = {ls["lesson_name"]: ls for ls in original_ch["lessons"]}
# 按顺序重排 lessons
new_lessons = []
for ls_name in chapter_order['lessons']:
if ls_name in lesson_map:
new_lessons.append(lesson_map[ls_name])
# 如果还有遗漏(前端没传的),可以追加到最后,避免丢数据
for ls_name, ls in lesson_map.items():
if ls not in new_lessons:
new_lessons.append(ls)
# 构造新的 chapter
new_chapter = {**original_ch, "lessons": new_lessons}
new_chapters.append(new_chapter)
# 更新 material
mongo.db.materials.update_one(
{"_id": ObjectId(material_id)},
{
"$set": {
"chapters": new_chapters,
"updated_at": datetime.now()
}
}
)
return True
def save_materials_markdown_cos(
material_id: str,
chapter_name: str,
lesson_name: str,
docs: Dict[str, Dict[str, str]],
version: str | None = None, # 目前不做并发控制,保留参数
actor=None # 可记录操作者到审计日志
) -> Dict[str, str]:
"""
docs 结构:
{
"lesson": {"cdn_link": "...lesson.md", "content": "..."},
"prompt": {"cdn_link": "...prompt.md", "content": "..."},
"score": {"cdn_link": "...score_prompt.md", "content": "..."}
}
约定cdn_link 与对象存储的 key 一一映射(同路径/同文件名),
我们通过解析 URL 的 path 作为 key 进行覆盖写入(不改文件名)。
"""
def _cdn_to_key(cdn_link: str) -> str:
"""
把 CDN URL 映射成对象存储的 key
- 若 CDN 与存储是“同 key 直出”,则直接取 path去掉前导 /
- 若你有自定义映射规则,在此处改写
"""
p = urlparse(cdn_link)
key = p.path.lstrip("/")
if not key:
raise ValueError(f"非法 cdn_link{cdn_link}")
return key
# 1) 覆盖写入三份文件
results: Dict[str, Dict[str, str]] = {}
for kind in ("lesson", "prompt", "score"):
item = docs.get(kind)
if not item or "cdn_link" not in item or "content" not in item:
raise ValueError(f"缺少 {kind} 的 cdn_link 或 content")
key = _cdn_to_key(item["cdn_link"])
content = item["content"]
# 覆盖写入注意upload_file 内部需实现覆盖同 key
try:
# 约定upload_file 返回可访问的 URL覆盖后可与原相同
url = upload_file(content.encode("utf-8"), key)
except Exception as e:
current_app.logger.exception("覆盖写入失败 kind=%s key=%s", kind, key)
raise
# 简单返回:保留原 cdn_link一般与 url 相同),以及 md5 作为伪版本
import hashlib
etag = hashlib.md5(content.encode("utf-8")).hexdigest()
results[kind] = {"cdn_link": url or item["cdn_link"], "version": etag}
# 2) 写库:更新该 lesson 的更新时间(不改文件名/链接)
_touch_material_lesson_updated_at(material_id, chapter_name, lesson_name, actor=actor)
results["updated_at"] = datetime.now().isoformat() + "Z"
return results
def _touch_material_lesson_updated_at(material_id: str, chapter_name: str, lesson_name: str, actor=None) -> None:
"""仅刷新 materials 文档中该课时的更新时间戳(以及顶层 updated_at"""
mongo = current_app.extensions["mongo"]
now = datetime.now()
# 更新顶层 updated_at
mongo.db.materials.update_one(
{"_id": ObjectId(material_id)},
{"$set": {"updated_at": now}}
)
# 更新嵌套 lesson 的更新时间(如果你在 lesson 内有该字段)
# 需要使用 arrayFilters 精确定位到对应章节/课时
mongo.db.materials.update_one(
{"_id": ObjectId(material_id)},
{
"$set": {
"chapters.$[c].lessons.$[l].updated_at": now,
# 可选:记录最近编辑人
"chapters.$[c].lessons.$[l].last_editor": getattr(actor, "id", None) if actor else None
}
},
array_filters=[
{"c.chapter_name": chapter_name},
{"l.lesson_name": lesson_name}
]
)
def add_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id):
"""
1) 生成占位 Markdownlesson_prompt_score_prompt
2) 上传到对象存储(返回 URL
3) 将 lesson 文档(含三份 URLpush 到指定章节
"""
mongo = current_app.extensions["mongo"]
# 组织对象存储的文件路径
chapter_slug = _slugify(chapter_name or "")
lesson_slug = _slugify(lesson_name or "")
timestamp = datetime.now().strftime('%Y%m%dT%H%M%SZ')
base_prefix = f"materials_{material_id}_{chapter_slug}_{lesson_slug}_{timestamp}"
# 占位文本
lesson_md, prompt_md, score_prompt_md = _default_markdowns(lesson_name or "未命名课时")
# 生成文件名(带层级路径)
lesson_key = f"{base_prefix}_lesson.md"
prompt_key = f"{base_prefix}_prompt.md"
score_key = f"{base_prefix}_score_prompt.md"
# 上传(任意一个失败则终止,不写库)
try:
lesson_url = upload_file(lesson_md.encode("utf-8"), lesson_key)
prompt_url = upload_file(prompt_md.encode("utf-8"), prompt_key)
score_url = upload_file(score_prompt_md.encode("utf-8"), score_key)
except Exception as e:
current_app.logger.exception("Upload default markdown failed: %s", e)
return False
# 组装 lesson_doc
lesson_doc = {
"lesson_name": lesson_name or "",
"markdown_lesson_file_link": lesson_url, # lesson 主体
"markdown_prompt_file_name": prompt_url, # 给大模型的 prompt
"markdown_score_prompt_file_link": score_url, # 评分 prompt
}
# 将 lesson push 到指定章节
res = mongo.db.materials.update_one(
{
"_id": ObjectId(material_id),
"teacher_id": teacher_id, # 注意:若存的是 ObjectId请确保传入的是同类型
},
{
"$push": {"chapters.$[c].lessons": lesson_doc},
"$set": {"updated_at": datetime.now()},
},
array_filters=[{"c.chapter_name": chapter_name}],
)
# 可选:如果 res.modified_count != 1说明 push 失败(可能章节名不匹配)
# 这里可以考虑是否回滚 COS 上刚上传的 3 个文件(需实现删除逻辑)。
return res.modified_count == 1
def get_materials_by_teacher(teacher_id)->List[Material]:
mongo = current_app.extensions["mongo"]
materials = mongo.db.materials.find({'teacher_id': teacher_id})
return [Material(**material) for material in materials]
def get_materials_by_teacher_dict(teacher_id)->List[Dict]:
mongo = current_app.extensions["mongo"]
materials = mongo.db.materials.find({'teacher_id': teacher_id})
result = []
for material in materials:
material['_id'] = str(material['_id'])
result.append(material)
return result
def _slugify(name: str) -> str:
"""将章节/课时名转为 URL 友好的 slug。"""
s = name.strip().lower()
s = re.sub(r'\s+', '-', s) # 空白 -> -
s = re.sub(r'[^a-z0-9\-_]+', '', s) # 仅保留安全字符
return s or 'untitled'
def _default_markdowns(lesson_name: str):
"""三份占位 Markdown 文本。可按需调整模板内容。"""
ts = datetime.now().strftime('%Y-%m-%d %H:%M')
lesson_md = f"""# {lesson_name}
## 阶段一:示例阶段
### 步骤A
在这里编写该步骤的教学内容(文字/代码/图片链接等)。
### 步骤B
继续补充该阶段的其它步骤内容。
"""
prompt_md = f"""# {lesson_name}
## 阶段一:示例阶段
### 步骤A
请基于“步骤A”的教学目标与材料生成指导性提示词尽量结构化给出评估标准和示例
### 步骤B
请基于“步骤B”的教学目标与材料生成指导性提示词。
"""
score_prompt_md = f"""# {lesson_name}
## 评分准则
- 正确性、完整性、可读性、效率(按需调整权重)
## 阶段一:示例阶段
### 步骤A
对学员在“步骤A”的输出进行评分与文字反馈分数区间 0-100并输出 JSON```{{"score": <int>, "reasons": "<string>", "advices": "<string>"}}```
### 步骤B
同上对“步骤B”输出进行评分与反馈。
"""
return lesson_md, prompt_md, score_prompt_md
from .course_services.learning_progess_process import *

View File

@@ -0,0 +1,239 @@
# myapp/services/course_service.py
import re
import requests
import json
from typing import Dict, List, Optional
from flask import current_app
from urllib.parse import urlparse
from datetime import datetime
from bson import ObjectId
def save_learning_progress(chatmanager, user_id: str) -> bool:
"""
保存用户学习进度到MongoDB
参数:
- chatmanager: ChatManager实例包含学习进度数据
- user_id: 用户ID
返回:
- bool: 保存是否成功
"""
try:
# 获取MongoDB连接
mongo = current_app.extensions["mongo"]
# 获取MultiAgents框架中的对话列表
dialog_list = get_multiagents_dialog_list(user_id)
his_data = dialog_list['data']
# 构建学习进度数据
progress_data = {
"user_id": user_id,
"material_id": chatmanager.material_id,
"chapter_name": chatmanager.chapter_name,
"lesson_name": chatmanager.lesson_name,
"chapter_chain_now": chatmanager.chapter_chain_now,
"chat_historys": chatmanager.chat_historys,
"scores": chatmanager.scores,
"multiagents_dialogs": his_data,
"chapter_chain_length": len(chatmanager.chapter_chain),
"updated_at": datetime.now()
}
# 保存到MongoDB使用upsert操作如果存在则更新不存在则插入
mongo.db.learning_progress.update_one(
{
"user_id": user_id,
"material_id": chatmanager.material_id,
"chapter_name": chatmanager.chapter_name,
"lesson_name": chatmanager.lesson_name
},
{"$set": progress_data},
upsert=True
)
current_app.logger.info(f"学习进度保存成功: user_id={user_id}, lesson={chatmanager.lesson_name}")
return True
except Exception as e:
current_app.logger.error(f"保存学习进度失败: {str(e)}")
return False
def get_multiagents_dialog_list(user_id: str) -> List[Dict]:
"""
从MultiAgents框架获取用户对话列表
参数:
- user_id: 用户ID
返回:
- Dict[List]: 对话列表(key 为 History database)
"""
try:
# 从配置中获取ASE引擎URL和token
config = current_app.config
url_token = config.get("ASE_ENGINE_URL_TOKEN", "")
# 构建请求URL
base_url = config.get("ASE_ENGINE_URL", "")
api_url = f"{base_url}/api/sync/download"
# 构建请求参数
payload = {
"namespace_url": url_token, # 使用配置中的token作为namespace_url
"user_id": user_id
}
# 发送POST请求
response = requests.post(
api_url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=10
)
# 检查响应
if response.status_code == 200:
current_app.logger.info(f"获取MultiAgents对话列表成功: user_id={user_id}, json={response.json()}")
return response.json()
else:
current_app.logger.error(f"获取MultiAgents对话列表失败: HTTP {response.status_code}")
return []
except Exception as e:
current_app.logger.error(f"获取MultiAgents对话列表异常: {str(e)}")
return []
def load_learning_progress(user_id: str, material_id: str, chapter_name: str, lesson_name: str) -> Dict:
"""
加载用户学习进度
参数:
- user_id: 用户ID
- material_id: 课程ID
- chapter_name: 章节名
- lesson_name: 课时名
返回:
- Dict: 学习进度数据,包含以下字段:
- exists: bool表示学习记录是否存在
- data: Dict学习进度详细数据包含chat_historys、scores、chapter_chain_now等字段
- message: str操作结果描述
"""
try:
mongo = current_app.extensions["mongo"]
progress = mongo.db.learning_progress.find_one({
"user_id": user_id,
"material_id": material_id,
"chapter_name": chapter_name,
"lesson_name": lesson_name
})
if progress:
# 转换ObjectId为字符串
if "_id" in progress:
progress["_id"] = str(progress["_id"])
return {
"exists": True,
"data": progress,
"message": "学习记录加载成功"
}
# 返回默认值,确保即使没有学习记录也有完整的结构
return {
"exists": False,
"data": {
"user_id": user_id,
"material_id": material_id,
"chapter_name": chapter_name,
"lesson_name": lesson_name,
"chat_historys": [],
"scores": [],
"chapter_chain_now": None,
"chapter_chain_length": 0,
"updated_at": datetime.now().isoformat()
},
"message": "未找到学习记录,返回默认值"
}
except Exception as e:
error_msg = f"加载学习进度失败: {str(e)}"
current_app.logger.error(error_msg)
# 发生错误时返回默认结构,确保系统稳定性
return {
"exists": False,
"data": {
"user_id": user_id,
"material_id": material_id,
"chapter_name": chapter_name,
"lesson_name": lesson_name,
"chat_historys": [],
"scores": [],
"chapter_chain_now": None,
"chapter_chain_length": 0,
"updated_at": datetime.now().isoformat()
},
"message": error_msg
}
def upload_learning_progress_to_cloud(progress: Dict) -> Dict:
"""
将学习进度上传到云服务
参数:
- progress: 学习进度数据字典{'databases':{}, 'global_databases':{}, 'session_info_out':{}}
返回:
- Dict: 上传结果包含success和message字段
"""
try:
# 从配置中获取ASE引擎URL和token
config = current_app.config
url_token = config.get("ASE_ENGINE_URL_TOKEN", "")
# 构建请求URL
base_url = config.get("ASE_ENGINE_URL", "")
api_url = f"{base_url}/api/sync/upload"
# 导入JSON序列化工具
from ...utils.json_serializer import json_serialize_dict
# 序列化进度数据处理datetime等特殊类型
serialized_progress = json_serialize_dict(progress['multiagents_dialogs'])
# 构建请求参数
payload = {
"namespace_url": url_token, # 使用配置中的token作为namespace_url
"user_id": progress.get("user_id", ""),
"data": serialized_progress
}
# 发送POST请求
response = requests.post(
api_url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=10
)
# 处理响应
if response.status_code == 200:
result = response.json()
current_app.logger.info(f"学习进度上传成功: user_id={progress.get('user_id')} 内容:{serialized_progress}")
return {
"success": True,
"message": "学习进度上传成功",
"data": result
}
else:
error_msg = f"上传失败HTTP状态码: {response.status_code}"
current_app.logger.error(f"学习进度上传失败: {error_msg}")
return {
"success": False,
"message": error_msg
}
except Exception as e:
error_msg = f"上传异常: {str(e)}"
current_app.logger.error(f"学习进度上传异常: {error_msg}")
return {
"success": False,
"message": error_msg
}

View File

@@ -0,0 +1,473 @@
import re
import requests
import json
from typing import Dict, List, Optional
from flask import current_app
from urllib.parse import urlparse
from datetime import datetime
from bson import ObjectId
from apps.services.cos_service import upload_file, delete_file
from apps.models.material import Material
def get_new_edit_materials(num: int)->List[Material]:
mongo = current_app.extensions["mongo"]
materials = mongo.db.materials.find().sort("updated_at", -1).limit(num)
returnCode = []
for material in materials:
returnCode.append(Material(**material))
returnCode[-1]._id = str(material['_id'])
return returnCode
def user_selected_course(user_obj):
briefs = []
for cid in getattr(user_obj, "select_course", []):
course_obj = load_material(cid)
brief={}
brief["_id"] = cid
brief["name"] = course_obj.name
brief["description"] = course_obj.description
brief["image_url"] = course_obj.image_url
brief["created_at"] = course_obj.created_at.strftime("%Y-%m-%d %H:%M:%S")
brief["updated_at"] = course_obj.updated_at.strftime("%Y-%m-%d %H:%M:%S")
brief["chapters"] = [chapter.model_dump() for chapter in course_obj.chapters]
briefs.append(brief)
return briefs
def user_selected_course_briefs(user_obj):
"""根据用户对象的选课列表,拼装课程简要信息数组"""
briefs = []
for cid in getattr(user_obj, "select_course", []):
course_obj = load_material(cid)
brief={}
brief["_id"] = cid
brief["name"] = course_obj.name
brief["description"] = course_obj.description
brief["image_url"] = course_obj.image_url
brief["created_at"] = course_obj.created_at.strftime("%Y-%m-%d %H:%M:%S")
brief["updated_at"] = course_obj.updated_at.strftime("%Y-%m-%d %H:%M:%S")
briefs.append(brief)
return briefs
def create_material(teacher_id, material_name, description, chapters, image_url):
material = Material(
name=material_name,
description=description,
teacher_id=teacher_id,
created_at=datetime.now(),
updated_at=datetime.now(),
chapters=chapters,
image_url=image_url
)
mongo = current_app.extensions["mongo"]
material_id = mongo.db.materials.insert_one(material.model_dump()).inserted_id
return str(material_id)
def load_material(material_id):
mongo = current_app.extensions["mongo"]
material = mongo.db.materials.find_one({"_id": ObjectId(material_id)})
material['_id'] = material_id
return Material(**material)
def update_material(material_id, chapters):
mongo = current_app.extensions["mongo"]
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
if material:
mongo.db.materials.update_one(
{'_id': ObjectId(material_id)},
{'$set': {'chapters': chapters, 'updated_at': datetime.now()}}
)
return True
return False
def add_material_chapter(material_id, chapter_name, teacher_id):
mongo = current_app.extensions["mongo"]
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
if material['teacher_id'] != teacher_id:
return False
if material:
mongo.db.materials.update_one(
{'_id': ObjectId(material_id)},
{'$push': {'chapters': {'chapter_name': chapter_name, 'lessons': []}}})
return True
def rename_material_structure(material_id, is_chapter, chapter_name, lesson_name, new_name):
mongo = current_app.extensions["mongo"]
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
if not material:
return False # 材料不存在
# 检查名称是否存在于列表中的辅助函数
def name_exists_in_list(item_list, name_key, target_name):
return any(item[name_key] == target_name for item in item_list)
# 查找包含指定章节名的章节
def find_chapter_by_name(chapters, target_name):
for chapter in chapters:
if chapter['chapter_name'] == target_name:
return chapter
return None
# 检查章节是否存在
target_chapter = find_chapter_by_name(material['chapters'], chapter_name)
if not target_chapter:
return False
if is_chapter:
# 检查新章节名是否已存在
if name_exists_in_list(material['chapters'], 'chapter_name', new_name):
return False
# 更新章节名
mongo.db.materials.update_one(
{'_id': ObjectId(material_id)},
{'$set': {'chapters.$[c].chapter_name': new_name}},
array_filters=[{'c.chapter_name': chapter_name}]
)
else:
# 检查课时是否存在
if not name_exists_in_list(target_chapter['lessons'], 'lesson_name', lesson_name):
return False
# 检查新课时名是否已存在
if name_exists_in_list(target_chapter['lessons'], 'lesson_name', new_name):
return False
# 更新课时名
mongo.db.materials.update_one(
{'_id': ObjectId(material_id)},
{'$set': {'chapters.$[c].lessons.$[l].lesson_name': new_name}},
array_filters=[{'c.chapter_name': chapter_name}, {'l.lesson_name': lesson_name}]
)
return True
def delete_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id):
"""
删除指定课程的课时。
删除后,更新 MongoDB 中对应的章节中的 lesson 列表。
"""
mongo = current_app.extensions["mongo"]
# 查询 material 是否存在,且确保该 teacher_id 对应的 material
material = mongo.db.materials.find_one({'_id': ObjectId(material_id), 'teacher_id': teacher_id})
for chapter in material['chapters']:
if chapter['chapter_name'] == chapter_name:
for lesson in chapter['lessons']:
if lesson['lesson_name'] == lesson_name:
if lesson['markdown_lesson_file_link']!='':
delete_file(lesson['markdown_lesson_file_link'])
if lesson['markdown_prompt_file_name']!='':
delete_file(lesson['markdown_prompt_file_name'])
if lesson['markdown_score_prompt_file_link']!='':
delete_file(lesson['markdown_score_prompt_file_link'])
if not material:
return False # 找不到对应的 material 或 teacher_id 不匹配
# 删除 lesson 的条件
res = mongo.db.materials.update_one(
{
"_id": ObjectId(material_id),
"teacher_id": teacher_id # 确保是该教师的课程
},
{
"$pull": {
"chapters.$[c].lessons": {
"lesson_name": lesson_name # 根据 lesson_name 来删除
}
},
"$set": {"updated_at": datetime.now()} # 更新修改时间
},
array_filters=[{"c.chapter_name": chapter_name}] # 找到匹配的章节
)
# 如果删除操作成功res.modified_count 应为 1
return res.modified_count == 1
def reorder_material_structure(material_id, order):
mongo = current_app.extensions["mongo"]
material = mongo.db.materials.find_one({'_id': ObjectId(material_id)})
if not material:
return False
# 1. 构建原章节映射(保留章节元信息)和原章节的课时映射
chapter_map = {ch["chapter_name"]: ch for ch in material["chapters"]}
# 每个章节的课时映射:{章节名: {课时名: 课时对象}}
original_chapter_lessons = {
ch["chapter_name"]: {ls["lesson_name"]: ls for ls in ch["lessons"]}
for ch in material["chapters"]
}
# 2. 收集所有章节“少掉的课时”到全局池(仅包含原章节有但新排序没包含的课时)
# 用列表存储,避免同名课时被覆盖(处理可能的重名场景)
global_extra_lessons = []
for ch_name in chapter_map:
original_lessons = original_chapter_lessons[ch_name] # 原章节的所有课时
# 找到当前章节在order中的配置如果没有默认该章节所有课时都是“少掉的”
chapter_order = next((item for item in order if item["chapter_name"] == ch_name), None)
if not chapter_order:
# 章节未在order中出现所有原课时都是“少掉的”
global_extra_lessons.extend(original_lessons.values())
continue
# 章节在order中出现找出原课时中不在新排序里的即“少掉的”
new_lesson_names = chapter_order["lessons"]
for ls_name, ls in original_lessons.items():
if ls_name not in new_lesson_names:
global_extra_lessons.append(ls)
print("global_extra_lessons:", global_extra_lessons)
new_chapters = []
for chapter_order in order:
ch_name = chapter_order["chapter_name"]
if ch_name not in chapter_map:
continue # 跳过不存在的章节
original_ch = chapter_map[ch_name]
original_lessons = original_chapter_lessons[ch_name] # 本章节原课时
new_lessons = []
# 3. 构建当前章节的新课时列表
for ls_name in chapter_order["lessons"]:
# 优先使用本章节原有的课时
if ls_name in original_lessons:
new_lessons.append(original_lessons[ls_name])
continue
# 本章节没有,从全局池(其他章节少掉的课时)中找
# 处理可能的重名:找到第一个匹配的课时
for i, extra_ls in enumerate(global_extra_lessons):
if extra_ls["lesson_name"] == ls_name:
new_lessons.append(extra_ls)
del global_extra_lessons[i] # 从全局池移除,避免重复
break
# 4. 构造新章节(保留原章节元信息,替换课时列表)
new_chapter = {**original_ch, "lessons": new_lessons}
new_chapters.append(new_chapter)
# 5. 处理全局池中剩余的课时(未被任何章节引用的,追加到最后一个章节)
if global_extra_lessons and new_chapters:
new_chapters[-1]["lessons"].extend(global_extra_lessons)
print("new_chapters:", new_chapters)
# 更新数据库
mongo.db.materials.update_one(
{"_id": ObjectId(material_id)},
{
"$set": {
"chapters": new_chapters,
"updated_at": datetime.now()
}
}
)
return True
def save_materials_markdown_cos(
material_id: str,
chapter_name: str,
lesson_name: str,
docs: Dict[str, Dict[str, str]],
version: str | None = None, # 目前不做并发控制,保留参数
actor=None # 可记录操作者到审计日志
) -> Dict[str, str]:
"""
docs 结构:
{
"lesson": {"cdn_link": "...lesson.md", "content": "..."},
"prompt": {"cdn_link": "...prompt.md", "content": "..."},
"score": {"cdn_link": "...score_prompt.md", "content": "..."}
}
约定cdn_link 与对象存储的 key 一一映射(同路径/同文件名),
我们通过解析 URL 的 path 作为 key 进行覆盖写入(不改文件名)。
"""
def _cdn_to_key(cdn_link: str) -> str:
"""
把 CDN URL 映射成对象存储的 key
- 若 CDN 与存储是“同 key 直出”,则直接取 path去掉前导 /
- 若你有自定义映射规则,在此处改写
"""
p = urlparse(cdn_link)
key = p.path.lstrip("/")
if not key:
raise ValueError(f"非法 cdn_link{cdn_link}")
return key
# 1) 覆盖写入三份文件
results: Dict[str, Dict[str, str]] = {}
for kind in ("lesson", "prompt", "score"):
item = docs.get(kind)
if not item or "cdn_link" not in item or "content" not in item:
raise ValueError(f"缺少 {kind} 的 cdn_link 或 content")
key = _cdn_to_key(item["cdn_link"])
content = item["content"]
# 覆盖写入注意upload_file 内部需实现覆盖同 key
try:
# 约定upload_file 返回可访问的 URL覆盖后可与原相同
url = upload_file(content.encode("utf-8"), key)
except Exception as e:
current_app.logger.exception("覆盖写入失败 kind=%s key=%s", kind, key)
raise
# 简单返回:保留原 cdn_link一般与 url 相同),以及 md5 作为伪版本
import hashlib
etag = hashlib.md5(content.encode("utf-8")).hexdigest()
results[kind] = {"cdn_link": url or item["cdn_link"], "version": etag}
# 2) 写库:更新该 lesson 的更新时间(不改文件名/链接)
_touch_material_lesson_updated_at(material_id, chapter_name, lesson_name, actor=actor)
results["updated_at"] = datetime.now().isoformat() + "Z"
return results
def _touch_material_lesson_updated_at(material_id: str, chapter_name: str, lesson_name: str, actor=None) -> None:
"""仅刷新 materials 文档中该课时的更新时间戳(以及顶层 updated_at"""
mongo = current_app.extensions["mongo"]
now = datetime.now()
# 更新顶层 updated_at
mongo.db.materials.update_one(
{"_id": ObjectId(material_id)},
{"$set": {"updated_at": now}}
)
# 更新嵌套 lesson 的更新时间(如果你在 lesson 内有该字段)
# 需要使用 arrayFilters 精确定位到对应章节/课时
mongo.db.materials.update_one(
{"_id": ObjectId(material_id)},
{
"$set": {
"chapters.$[c].lessons.$[l].updated_at": now,
# 可选:记录最近编辑人
"chapters.$[c].lessons.$[l].last_editor": getattr(actor, "id", None) if actor else None
}
},
array_filters=[
{"c.chapter_name": chapter_name},
{"l.lesson_name": lesson_name}
]
)
def add_material_chapter_lesson(material_id, chapter_name, lesson_name, teacher_id):
"""
1) 生成占位 Markdownlesson_prompt_score_prompt
2) 上传到对象存储(返回 URL
3) 将 lesson 文档(含三份 URLpush 到指定章节
"""
mongo = current_app.extensions["mongo"]
# 组织对象存储的文件路径
chapter_slug = _slugify(chapter_name or "")
lesson_slug = _slugify(lesson_name or "")
timestamp = datetime.now().strftime('%Y%m%dT%H%M%SZ')
base_prefix = f"materials_{material_id}_{chapter_slug}_{lesson_slug}_{timestamp}"
# 占位文本
lesson_md, prompt_md, score_prompt_md = _default_markdowns(lesson_name or "未命名课时")
# 生成文件名(带层级路径)
lesson_key = f"{base_prefix}_lesson.md"
prompt_key = f"{base_prefix}_prompt.md"
score_key = f"{base_prefix}_score_prompt.md"
# 上传(任意一个失败则终止,不写库)
try:
lesson_url = upload_file(lesson_md.encode("utf-8"), lesson_key)
prompt_url = upload_file(prompt_md.encode("utf-8"), prompt_key)
score_url = upload_file(score_prompt_md.encode("utf-8"), score_key)
except Exception as e:
current_app.logger.exception("Upload default markdown failed: %s", e)
return False
# 组装 lesson_doc
lesson_doc = {
"lesson_name": lesson_name or "",
"markdown_lesson_file_link": lesson_url, # lesson 主体
"markdown_prompt_file_name": prompt_url, # 给大模型的 prompt
"markdown_score_prompt_file_link": score_url, # 评分 prompt
}
# 将 lesson push 到指定章节
res = mongo.db.materials.update_one(
{
"_id": ObjectId(material_id),
"teacher_id": teacher_id, # 注意:若存的是 ObjectId请确保传入的是同类型
},
{
"$push": {"chapters.$[c].lessons": lesson_doc},
"$set": {"updated_at": datetime.now()},
},
array_filters=[{"c.chapter_name": chapter_name}],
)
# 可选:如果 res.modified_count != 1说明 push 失败(可能章节名不匹配)
# 这里可以考虑是否回滚 COS 上刚上传的 3 个文件(需实现删除逻辑)。
return res.modified_count == 1
def get_materials_by_teacher(teacher_id)->List[Material]:
mongo = current_app.extensions["mongo"]
materials = mongo.db.materials.find({'teacher_id': teacher_id})
return [Material(**material) for material in materials]
def get_materials_by_teacher_dict(teacher_id)->List[Dict]:
mongo = current_app.extensions["mongo"]
materials = mongo.db.materials.find({'teacher_id': teacher_id})
result = []
for material in materials:
material['_id'] = str(material['_id'])
result.append(material)
return result
def _slugify(name: str) -> str:
"""将章节/课时名转为 URL 友好的 slug。"""
s = name.strip().lower()
s = re.sub(r'\s+', '-', s) # 空白 -> -
s = re.sub(r'[^a-z0-9\-_]+', '', s) # 仅保留安全字符
return s or 'untitled'
def _default_markdowns(lesson_name: str):
"""三份占位 Markdown 文本。可按需调整模板内容。"""
ts = datetime.now().strftime('%Y-%m-%d %H:%M')
lesson_md = f"""# {lesson_name}
## 阶段一:示例阶段
### 步骤A
在这里编写该步骤的教学内容(文字/代码/图片链接等)。
### 步骤B
继续补充该阶段的其它步骤内容。
"""
prompt_md = f"""# {lesson_name}
## 阶段一:示例阶段
### 步骤A
请基于"步骤A"的教学目标与材料,生成指导性提示词(尽量结构化,给出评估标准和示例)。
### 步骤B
请基于"步骤B"的教学目标与材料,生成指导性提示词。
"""
score_prompt_md = f"""# {lesson_name}
## 评分准则
- 正确性、完整性、可读性、效率(按需调整权重)
## 阶段一:示例阶段
### 步骤A
对学员在"步骤A"的输出进行评分与文字反馈,分数区间 0-100并输出 JSON```{{"score": <int>, "reasons": "<string>", "advices": "<string>"}}```
### 步骤B
同上,对"步骤B"输出进行评分与反馈。
"""
return lesson_md, prompt_md, score_prompt_md

View File

@@ -24,7 +24,6 @@ def load_markdown_file(course_id, chapter_name, lesson_name):
break
if file_link is None:
return None
print(f"load_markdown_file: {file_link}")
return get_text_file(file_link)
def convert_markdown_to_html(md_file: str) -> str:

View File

@@ -1,20 +1,37 @@
# myapp/sockets/namespaces.py
import json
import time
from flask import current_app, request, session
import requests
from flask import current_app, request, session, url_for
from flask_socketio import Namespace, join_room, leave_room, emit
from ..config import Config
from ..function_tools import judge
from ..function_tools import next_chapter
from ..services.backboard_service import realtime_response
from ..services.markdown_service import load_full_markdown_file
from ..services.markdown_service import load_full_markdown_file
from ..services.course_service import save_learning_progress, upload_learning_progress_to_cloud
from ..extension_ase.ase_client import HSAEngineClient
from ..services.user_service import get_or_load_current_user
from ..services.course_service import load_learning_progress
from ..services.asectrl_service import (
on_connect_to_ase,
receive_ase_paste_detected,
receive_ase_dialog,
receive_ase_message_hint,
receive_ase_voice_out,
receive_ase_process,
receive_ase_judge,
receive_ase_next_chapter,
receive_ase_chapter_score,
clear_user_session
)
class VSCodeNamespace(Namespace):
def on_login(self,data):
self.app = current_app
ex = current_app.extensions
print("VSCode client connected")
dataconfig = data['config']
@@ -25,8 +42,7 @@ class VSCodeNamespace(Namespace):
print(f"User {user_uuid} connected with path: {path}")
join_room(user_uuid, namespace='/vscode')
chatmanager = ex['user_uuid2chatmanager'][user_uuid]
chatmanager.vscode_ws=self
chatmanager.set_vscode_ws(self)
def on_load_chapter(self, data):
@@ -47,8 +63,53 @@ class VSCodeNamespace(Namespace):
class AgentNamespace(Namespace):
def _load_learning_progress(self, chatmanager, user_id, course_id, chapter_name, lesson_name, continue_learn):
"""加载并恢复用户学习进度
Args:
chatmanager: 聊天管理器实例
user_id: 用户ID
course_id: 课程ID
chapter_name: 章节名称
lesson_name: 课时名称
continue_learn: 是否继续学习现在总是为True保持兼容性
Returns:
int: 需要跳过的章节数
"""
# 总是从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:
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)
# 计算需要跳过的章节数:从初始的-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
def on_login(self, data):
self.current_app = current_app
self.app = current_app
ex = current_app.extensions
uuid2username = ex["uuid2username"]
backboard_manager = ex["backboard_manager"]
@@ -69,67 +130,85 @@ class AgentNamespace(Namespace):
chatmanager = user_uuid2chatmanager[user_uuid]
chatmanager.function_manager.add_function(judge, {'course_id': course_id, 'root_path': f'/home/{user_id}/{course_id}/{chapter_name}/{lesson_name}'})
chatmanager.function_manager.add_function(next_chapter, {'user_uuid': user_uuid})
chatmanager.chat_historys = []
chatmanager.scores = []
clear_user_session(current_app.config["ASE_ENGINE_URL"], current_app.config["ASE_ENGINE_URL_TOKEN"], user_id)
# 总是从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)
chatmanager.init(user_uuid, user_uuid2ase_client[user_uuid], fmd, fmdp, fsp, course_id, chapter_name, lesson_name, max_iter=5, app=current_app, socketio=self)
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.init(user_uuid, user_uuid2ase_client[user_uuid],
fmd, fmdp, fsp, course_id, chapter_name, lesson_name, max_iter=5, app=current_app, socketio=self)
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)
chatmanager.next_chapter()
# 总是根据进度跳过章节,恢复对话历史
for _ in range(need_skip_chapters):
chatmanager.next_chapter()
emit('next_chapter', room=user_uuid, namespace='/agent')
# 恢复对话历史到前端 - 使用批量发送消息列表
if chatmanager.chat_historys:
emit('messages', chatmanager.chat_historys, room=user_uuid, namespace='/agent')
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=self.on_connect_to_ase,
callback=on_connect_to_ase,
entry=(self, user_uuid2ase_client[user_uuid]),
init_data={'room':user_uuid}
init_data={'room':user_uuid, 'restart':restart}
)
user_uuid2ase_client[user_uuid].register_route_with_entry(
route='dialog',
callback=self.receive_ase_dialog,
callback=receive_ase_dialog,
entry=self,
init_data={'room': user_uuid}
)
user_uuid2ase_client[user_uuid].register_route_with_entry(
route='dialog-hint',
callback=self.receive_ase_message_hint,
callback=receive_ase_message_hint,
entry=self,
init_data={'room': user_uuid}
)
user_uuid2ase_client[user_uuid].register_route_with_entry(
route='voice_out',
callback=self.receive_ase_voice_out,
callback=receive_ase_voice_out,
entry=self,
init_data={'room': user_uuid}
)
user_uuid2ase_client[user_uuid].register_route_with_entry(
route='judge',
callback=self.receive_ase_judge,
callback=receive_ase_judge,
entry=self,
init_data={'room': user_uuid}
)
user_uuid2ase_client[user_uuid].register_route_with_entry(
route='next_chapter',
callback=self.receive_ase_next_chapter,
callback=receive_ase_next_chapter,
entry=self,
init_data={'room': user_uuid}
)
user_uuid2ase_client[user_uuid].register_route_with_entry(
route='chapter_score',
callback=self.receive_ase_chapter_score,
callback=receive_ase_chapter_score,
entry=(get_or_load_current_user(),chatmanager),
init_data={'room': user_uuid}
)
user_uuid2ase_client[user_uuid].register_route_with_entry(
route='process',
callback=self.receive_ase_process,
callback=receive_ase_process,
entry=self,
init_data={'room': user_uuid}
)
user_uuid2ase_client[user_uuid].register_route_with_entry(
route='paste_detected',
callback=self.receive_ase_paste_detected,
callback=receive_ase_paste_detected,
entry=self,
init_data={'room': user_uuid}
)
@@ -138,10 +217,8 @@ class AgentNamespace(Namespace):
def on_language(self, language):
ex = current_app.extensions
print(f"Language changed to: {language}")
def on_message(self, data):
print(f"Message from client: {data}")
ex = current_app.extensions
chatmanager = ex["user_uuid2chatmanager"]
uuid = session.get('user_uuid')
@@ -151,14 +228,11 @@ class AgentNamespace(Namespace):
if data['type'] == 'text':
res = user_uuid2ase_client[uuid].send_text('dialog', data['data'])
chatmanager[uuid].chat_historys.append({'role': 'user', 'content': data['data']})
print(f"Send text to route 'dialog' success: {res}")
if data['type'] == 'function': # 在这里 前端会发送允许执行的function与参数
print("function_call", data['data'])
d = data['data']
res = chatmanager[uuid].function_call(d['data']['name'], d['data'].get('args', {}))
d['data'] = res.to_dict()
print("function_call_res", d['data'])
emit(d['route'], d, room=uuid, namespace='/agent')
chatmanager[uuid].chat_historys.append({'role': 'user', 'content': data['data']})
user_uuid2ase_client[uuid].send_text(d['route'], data['data'])
@@ -169,63 +243,9 @@ class AgentNamespace(Namespace):
uuid = session.get('user_uuid')
user_uuid2ase_client = ex["user_uuid2ase_client"]
res = user_uuid2ase_client[uuid].send_voice('voice_in', voice_data['data'])
print(f"Send voicce to route 'voice_in' success: {res}")
def on_connect_to_ase(client_entry: HSAEngineClient, namespace_entry, init_data):
'''
此时刚刚建立好aseclient连接并向前端发送打开code-server-like指令。
'''
print(f"on_connect_to_ase {client_entry}, {namespace_entry}, {init_data}")
namespace_entry, ase_client = namespace_entry
namespace_entry.emit('open-iframe',"", room=init_data['room'], namespace='/agent')
ase_client.chatmanager.load_next_chapter()
ase_client.send_text("chapter-start", "")
namespace_entry.emit('message', "Multi-Agents服务已连接", room=init_data['room'], namespace='/agent')
#接受消息回来直接让message监听发送
def receive_ase_paste_detected(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data):
print("receive_ase_paste_detected", data)
client_entry.chatmanager.chat_historys.append({'role': 'assistant', 'content': data['data']})
namespace_entry.emit('message', data['data'], room=init_data['room'], namespace='/agent')
def receive_ase_dialog(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data):
print("receive_ase_dialog", data)
client_entry.chatmanager.chat_historys.append({'role': 'assistant', 'content': data['data']})
namespace_entry.emit('message', data['data'], room=init_data['room'], namespace='/agent')
def receive_ase_message_hint(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data):
print("receive_ase_message_hint", data)
namespace_entry.emit('message-hint', data['data'], room=init_data['room'], namespace='/agent')
#接受语音信息
def receive_ase_voice_out(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data):
namespace_entry.emit('voiceMessage', data['data'], room=init_data['room'], namespace='/agent')
def receive_ase_process(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data):
namespace_entry.emit('process', data['data'], room=init_data['room'], namespace='/agent')
def receive_ase_judge(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data):
print("receive_ase_judge", data)
namespace_entry.emit('request_function', data, room=init_data['room'], namespace='/agent') # data 是一个列表 /dict也支持
def receive_ase_next_chapter(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data):
print("receive_ase_next_chapter", data)
namespace_entry.emit('request_function',data, room=init_data['room'], namespace='/agent') # data 是一个列表 /dict也支持
def receive_ase_chapter_score(client_entry: HSAEngineClient, user_and_manager, data, init_data):
print("load_next_chapter")
user, chatmanager = user_and_manager
if len(chatmanager.scores) >= chatmanager.chapter_chain_now:
chatmanager.scores[chatmanager.chapter_chain_now - 1] = data.get('data', None)
else: chatmanager.scores.append(data.get('data', None))
user.set_course_progress(chatmanager.chat_historys, chatmanager.scores, chatmanager.material_id, chatmanager.chapter_name, chatmanager.lesson_name)
chatmanager.load_next_chapter()
time.sleep(0.5)
chatmanager.ase_client.send_text("chapter-start", "")
def on_initiative(self,data): # 主动call
print("User active function call")
ex = current_app.extensions
uuid = session.get('user_uuid')
manager = ex["user_uuid2chatmanager"][uuid]
@@ -238,10 +258,32 @@ class AgentNamespace(Namespace):
def on_disconnect(self,data):
self.app.logger.info(f'AgentNamespace client disconnected, session user_uuid: {session.get("user_uuid", "unknown")}')
ex = current_app.extensions
uuid = session.get('user_uuid')
user_uuid2chatmanager = ex["user_uuid2chatmanager"]
user_uuid2chatmanager[uuid].disconnect(uuid)
user_uuid2chatmanager = ex.get("user_uuid2chatmanager", {})
# 保存学习进度到MongoDB
try:
# 获取用户ID
uuid2username = ex.get("uuid2username", {})
user_id = uuid2username.get(uuid, "unknown")
self.app.logger.debug(f"get user's dialog his user_id={user_id}")
# 调用保存学习进度功能
chatmanager = user_uuid2chatmanager.get(uuid)
if chatmanager:
save_result = save_learning_progress(chatmanager, user_id)
except Exception as e:
self.app.logger.error(f"保存学习进度时发生异常: {str(e)}")
# 执行原有的断开连接逻辑
if uuid in user_uuid2chatmanager:
user_uuid2chatmanager[uuid].disconnect(uuid)
# 离开房间
if uuid:
leave_room(uuid, namespace='/agent')
print("VSCode client disconnected")
print("Disconnect reason:"+str(data))
self.app.logger.info("VSCode client disconnected")
self.app.logger.info(f"Disconnect reason: {str(data)}")

View File

@@ -0,0 +1,178 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>效率的重要性与实践验证</h1>
<h2>效率</h2>
<h3>算法是什么</h3>
<h4>算法就是解决某一个问题的做法,其实它在生活中无处不在</h4>
<p>比如从学校宿舍走到食堂:</p>
<ol>
<li>要先宿舍下楼</li>
<li>然后到食堂楼之间可能有3条路
A. 直线方向穿过曲折难走的小路,
B. 先走远路到平坦的道上,
C. 等一会校车,</li>
<li>从3者选择一条走过去最后再上楼。</li>
</ol>
<h4>算法的5大组成</h4>
<ol>
<li>输入</li>
<li>输出</li>
<li>有穷性</li>
<li>确定性</li>
<li>可行性</li>
</ol>
<p>这5大组成其实暗示了一个特性
其实对于所有的可以被算法描述的问题,
一定会有一种算法有解的。
至少有一种方法称为”暴力搜索“”穷举法“,穷尽一切可能。</p>
<h3>效率的重要性与验证</h3>
<h4>任务:分析与决策</h4>
<p>项目组目前有两套备选的交通信号灯同步算法,它们将在不同性能的服务器上运行:</p>
<ul>
<li>
<p><strong>方案A</strong>部署在超级服务器上10^9 次运算/秒采用的是一种较为简单的算法处理n个路口需要 <eq>2n^2</eq> 次计算。</p>
</li>
<li>
<p><strong>方案B</strong>部署在普通服务器上10^7 次运算/秒但采用的是一种更优化的算法处理n个路口需要 <eq>50nlog_2n</eq> 次计算。</p>
</li>
</ul>
<p>你的项目经理右侧的Agent希望你通过分析来判断哪个方案更具前景。请与TA对话逐一回答以下问题。</p>
<h4>问题</h4>
<p>与右侧的Agent对话回答以下问题</p>
<ol>
<li>
<p><strong>概念回顾</strong>:首先,请向你的“经理”解释,根据课程所学,一个合格的“算法”应具备哪些基本特征?</p>
</li>
<li>
<p><strong>小规模测试</strong>对于一个包含100个路口的小型城区n=100计算并说明方案A和方案B分别需要多长时间完成计算在这种情况下你会推荐哪个方案</p>
</li>
<li>
<p><strong>大规模应用</strong>现在我们需要为一座拥有100万个路口的大都市n=1,000,000进行规划。再次计算并说明两个方案的耗时。你的推荐会改变吗为什么</p>
</li>
<li>
<p><strong>总结陈词</strong>:综合以上分析,向你的“经理”总结:为什么一个更优的算法设计,其重要性远超硬件性能的提升? 这验证了课程中提到的哪个核心观点?</p>
</li>
</ol>
<h3>编程实践:验证算法的真实性能</h3>
<h4>任务:编码与分析</h4>
<p>理论分析让你认识到了算法效率的重要性。现在,你需要通过编程来亲身感受不同交通状况对同一算法性能的巨大影响。
我们将以“插入排序”为例,来处理三种典型的交通流量数据,这分别对应算法分析中的<strong>最好</strong><strong>最坏</strong><strong>平均</strong>情况。</p>
<h5>题目:模拟交通流量排序</h5>
<p>实现插入排序算法,并验证其在处理“畅通无阻”(数据有序)、“交通大堵塞”(数据逆序)和“随机车流”(数据随机)三种模式时的效率差异。</p>
<h5>代码框架</h5>
<p>在代码编辑区,完成 <code>insertion_sort(arr)</code> 函数的实现后运行完整代码并与Agent讨论结果。
<strong>请创建任意文件,将下面代码写入到编辑器中</strong></p>
<pre><code class="language-python">import random
import time
def insertion_sort(arr):
&quot;&quot;&quot;
实现插入排序算法
参数arr: 待排序的交通数据数组(整数代表车辆通行次序)
返回: 排序后的数组
&quot;&quot;&quot;
# TODO: 请在此处实现你的插入排序逻辑
def generate_traffic_data(n):
&quot;&quot;&quot;
生成模拟交通数据
参数n: 数据规模(路口数量或监控点数量)
返回: 三种不同交通状况的数据
&quot;&quot;&quot;
random_data = [random.randint(0, 10**6) for _ in range(n)]
# 模拟“畅通无阻”:交通流按次序进入,数据基本有序 (Best Case)
best_case_data = sorted(random_data)
# 模拟“交通大堵塞”:疏散时情况完全反转,数据逆序 (Worst Case)
worst_case_data = sorted(random_data, reverse=True)
# 模拟“随机车流”:正常但无规律的交通状况 (Average Case)
average_case_data = random_data
return best_case_data, worst_case_data, average_case_data
def measure_performance(func, data):
&quot;&quot;&quot;
测量算法性能
参数func: 排序函数
参数data: 交通数据
返回: 执行时间(毫秒)
&quot;&quot;&quot;
start_time = time.perf_counter_ns()
func(data.copy()) # 使用副本避免影响其他测试
end_time = time.perf_counter_ns()
return (end_time - start_time) / 10**6 # 转换为毫秒
#测试不同规模的路口网络
network_sizes = [1000, 5000, 10000]
print(&quot;交通数据处理算法性能测试:&quot;)
for size in network_sizes:
best, worst, avg = generate_traffic_data(size)
time_best = measure_performance(insertion_sort, best)
time_worst = measure_performance(insertion_sort, worst)
time_avg = measure_performance(insertion_sort, avg)
print(f&quot;网络规模 n={size}:&quot;)
print(f&quot; - 畅通无阻 (Best Case): {time_best:.2f} ms&quot;)
print(f&quot; - 交通大堵塞 (Worst Case): {time_worst:.2f} ms&quot;)
print(f&quot; - 随机车流 (Average Case): {time_avg:.2f} ms&quot;)
</code></pre>
<h4>分析与讨论</h4>
<p>完成编程并得到输出后请与右侧Agent讨论以下问题以检验你的理解</p>
<ol>
<li>
<p><strong>结果分析</strong>当网络规模从1000增加到10000时“交通大堵塞”最坏情况的处理时间增长了大约多少倍这更符合O(n)线性还是O(n2)(二次)的增长模式?</p>
</li>
<li>
<p><strong>原因探究</strong>:为什么“畅通无阻”(最好情况)的处理速度如此之快?它的时间复杂度是什么?请结合你的代码逻辑来解释。</p>
</li>
<li>
<p><strong>实践应用</strong>:根据你的实验结果,你认为插入排序是否适合用于需要快速响应大规模交通拥堵的实时预警系统?为什么?</p>
</li>
<li>
<p><strong>融会贯通</strong>:结合第一关的理论分析和第二关的编程实验,你对“算法是解决问题的核心”这句话有了怎样更深的理解?</p>
</li>
</ol>
<h3>规模与增长率</h3>
<h4>算法复杂度定义</h4>
<p>为了更好地描述算法优化的效果,定义为当问题规模趋于无穷大时算法运行时间(算法复杂度,也可以理解为计算机运行的步骤数)</p>
<p>符号:<code>Θ( f(n) )</code> 相对常用,
称为“渐进等于”表示算法复杂度随着问题规模n的增大而增大的速率和函数f(n)在常数倍率上相同。</p>
<h4>算法复杂度的计算</h4>
<p>计算时间复杂度是一件比较重要的技能,我们来用一些例子试着计算:
假设你是一个收银员有n个人排队1分钟你只能收银1个人随着n增大你收银的时间复杂度时长增长和哪一个函数增长“渐进等于”呢</p>
<h5>增加一点难度</h5>
<p>你这个收银员有超能力可以越来越熟练第1个人用时1分钟第2人用时1/2分钟第3人只用1/4分钟随着n增大时间复杂度怎么样呢</p>
<h5>再难一点</h5>
<p>如果你这个超能力是这样的第1个人用时1分钟后面2个人用时1分钟后面4个人用时1分钟后面8个人用时1分钟那么这种情况下随着用户n的增大时间复杂度可以用那个函数描述呢</p>
<h4>另外两个符号</h4>
<p>最后还有两个符号:</p>
<ul>
<li>O 记号:渐近 “小于”f (n)“≤”g (n)</li>
<li>Ω 记号:渐近 “大于”f (n)“≥”g (n)
比如上面Θ( n ) &gt; Θ( log_2(n) ) &gt; Θ( 1 ),就可以写作Θ( 1 )= O ( log_2(n) ) = O ( n ),或者写作Θ( n ) = Ω( log_2(n) ) = Ω( 1 )</li>
</ul>
<p>当然事实上上面的写法比较不常见只是让大家理解一下常函数的增长渐进小于log_2(n)也渐进小于n。</p>
<p>在具体的使用中由于渐进大于没什么意义我们不会去找一个更差的算法我们常混用Θ、O也就是只研究函数上界研究一个算法复杂度渐进小于哪一个函数</p>
</body>
</html>

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>新7周课时</h1>
<h2>阶段一:示例阶段</h2>
<h3>步骤A</h3>
<p>在这里编写该步骤的教学内容(文字/代码/图片链接等)。</p>
<h3>步骤B</h3>
<p>继续补充该阶段的其它步骤内容。</p>
</body>
</html>

View File

@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>分治策略进阶与主方法</h1>
<blockquote>
<p>Auto-generated at 2025-09-22 15:44</p>
</blockquote>
<h2>分治策略进阶与主方法</h2>
<h3>最大子数组问题</h3>
<h4>任务:编码与分析</h4>
<h5>场景介绍</h5>
<p>我们的任务是分析每日交通流量的<strong>变化数组</strong>(正数代表流量增加,负数代表减少),并找到哪一个<strong>连续时段</strong>的流量总增量最大。这在算法上被称为“最大子数组问题” 。</p>
<p>例如,对于流量变化数组<code>[13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]</code>总增量最大的连续时段是从第8天到第11天总增量为 <code>18 + 20 - 7 + 12 = 43</code>
虽然这个问题可以通过O(n2) 的暴力法求解,但我们将使用更高效的分治策略。</p>
<h5>题目:最大交通流量增量</h5>
<p>你需要实现一个分治算法来解决最大子数组问题。
同时,为了对比,你也会实现一个暴力求解算法。</p>
<h5>代码框架</h5>
<p>在下方代码编辑区,完成 <code>find_maximum_subarray</code>(分治法)和 <code>find_max_crossing_subarray</code> 以及 <code>find_maximum_subarray_brute</code>(暴力法)三个函数。</p>
<pre><code class="language-python">import time
import random
def find_maximum_subarray_brute(arr):
&quot;&quot;&quot;
使用暴力法求解最大子数组问题
返回: (最大和, 开始索引, 结束索引)
&quot;&quot;&quot;
#--- 本周任务:请在下方实现分治算法 ---
def find_max_crossing_subarray(arr, low, mid, high):
&quot;&quot;&quot;
找到跨越中点的最大子数组
返回: (最大和, 开始索引, 结束索引)
&quot;&quot;&quot;
# TODO: 实现寻找跨越中点的最大子数组的逻辑
# 提示: 从mid向左和向右分别扫描找到各自的最大和
def find_maximum_subarray(arr, low, high):
&quot;&quot;&quot;
使用分治法求解最大子数组问题
返回: (最大和, 开始索引, 结束索引)
&quot;&quot;&quot;
# TODO: 实现分治递归逻辑
# 提示: 递归基是当数组只有一个元素时
#--- 测试与对比部分 --- 以下内容无需修改
traffic_changes = [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]
print(f&quot;交通流量变化数据: {traffic_changes}&quot;)
#使用分治法
max_sum_dc, start_dc, end_dc = find_maximum_subarray(traffic_changes, 0, len(traffic_changes) - 1)
print(f&quot;分治法结果: 最大增量 = {max_sum_dc}, 时段 = Day {start_dc} to Day {end_dc}&quot;)
#使用暴力法验证
max_sum_brute, start_brute, end_brute = find_maximum_subarray_brute(traffic_changes)
print(f&quot;暴力法结果: 最大增量 = {max_sum_brute}, 时段 = Day {start_brute} to Day {end_brute}&quot;)
#性能测试
large_data = [random.randint(-50, 50) for _ in range(2000)]
start_time = time.perf_counter()
find_maximum_subarray(large_data, 0, len(large_data) - 1)
dc_time = (time.perf_counter() - start_time) * 1000
print(f&quot;\n在 n=2000 的数据集上,分治法耗时: {dc_time:.2f} ms&quot;)
start_time = time.perf_counter()
find_maximum_subarray_brute(large_data)
brute_time = (time.perf_counter() - start_time) * 1000
print(f&quot;在 n=2000 的数据集上,暴力法耗时: {brute_time:.2f} ms&quot;)
</code></pre>
<h3>分治经典算法:快速排序</h3>
<h4>快速排序原理</h4>
<p>快速排序Quick Sort是分治法的经典应用之一。与归并排序不同的是快速排序通过原地划分数组来完成排序无需额外的同规模辅助存储空间。它在平均情况下表现极为高效是实际应用中常用的排序算法之一。</p>
<p>快速排序的分治核心思路非常好理解对于一个未排序的数组希望分解为左区间和一个“枢轴pivot”元素和右区间其中左区间的数都小于等于枢轴右区间的都大于等于枢轴。
我们这里用一种固定枢轴法来将序列变成上述的区间情况:
”选取数组第一个元素作为枢轴,用一个移动变量指向数组末尾元素并不断向前移动,直到找到第一个小于枢轴的元素,将该元素与枢轴位置交换,然后从枢轴原位置的下一位个元素开始往后找第一大于的交替,重复上述过程,最终实现以枢轴为界划分出左小右大的区间,再对左右区间递归执行此操作完成排序“</p>
<h4>任务:编码与实验</h4>
<p>现在你需要亲手实现快速排序算法,并通过实验验证其在不同输入情况下的性能差异。</p>
<p>实现 quick_sort(arr) 函数,使用分治策略对传入的数组进行排序。为简单起见,可选择每次固定使用数组的第一个元素作为枢轴,将比枢轴小的元素放在左侧,比枢轴大的放在右侧,然后递归地排序左右子数组。完成实现后,我们将利用性能测试函数对比有序输入(近似最坏情况)和随机输入(平均情况)下算法的运行时间。
代码框架
请在下方代码编辑区完成 quick_sort(arr) 的实现。</p>
<pre><code>import random
import time
def quick_sort(arr):
&quot;&quot;&quot;
实现快速排序算法(固定枢轴策略)
参数arr: 待排序的数组
返回: 排序后的数组
&quot;&quot;&quot;
# TODO: 实现快速排序的分治逻辑
# 提示:选取第一个元素为枢轴,递归排序左右子数组
def measure_performance(sort_func, data):
start_time = time.perf_counter_ns()
sort_func(data.copy()) # 对数据副本排序,避免影响原数据
end_time = time.perf_counter_ns()
return (end_time - start_time) / 10**6 # 转换为毫秒
#性能测试:对比有序输入和随机输入
sizes = [1000, 5000, 10000]
print(&quot;快速排序性能测试(固定枢轴):&quot;)
for n in sizes:
sorted_data = list(range(n))
random_data = [random.randint(0, 10**6) for _ in range(n)]
time_sorted = measure_performance(quick_sort, sorted_data)
time_random = measure_performance(quick_sort, random_data)
print(f&quot;数据规模 n={n}: 有序输入 {time_sorted:.2f} ms, 随机输入 {time_random:.2f} ms&quot;)
</code></pre>
<p>完成编码并运行测试,报告时间差异。</p>
<h3>快速排序的复杂度分析</h3>
<h4>时间复杂度</h4>
<p>基于 “划分 - 递归” 核心逻辑,时间复杂度由<strong>划分效率(枢轴选择)<strong></strong>递归深度</strong>共同决定
三种典型场景:最坏情况、平均情况、最佳情况</p>
<p>最坏情况:每次划分得规模 n-1 和 0 的子数组
平均情况:输入随机 / 等概率选枢轴
最佳情况:每次划分平分数组</p>
<h4>空间复杂度</h4>
<p>快速排序比归并排序更常用的一点在于,快排是“原地的”。</p>
<h3>注入随机进行优化:随机快排与期望复杂度</h3>
<h4>分析</h4>
<p>为了避免固定枢轴选择导致的最坏情况,我们可以引入随机化策略优化快速排序。
随机快排在每次划分时随机选择枢轴,从概率上保证划分均衡,从而将最坏情况表现转化为极低概率事件。
理论上可以证明,随机快排对任何输入的期望时间复杂度为<eq>O(n \log n)</eq>,且大幅降低了出现<eq>O(n^2)</eq>耗时的概率。</p>
<h4>实践</h4>
<pre><code>import random
import time
def random_quick_sort(arr):
&quot;&quot;&quot;
实现随机快速排序算法(随机选择枢轴)
参数arr: 待排序的数组
返回: 排序后的数组
&quot;&quot;&quot;
def quick_sort(arr):
&quot;&quot;&quot;
你在上一章实现的快排内容
&quot;&quot;&quot;
#验证随机快排在极端有序输入下的性能
n = 10000
sorted_data = list(range(n))
time_fixed = measure_performance(quick_sort, sorted_data)
time_random = measure_performance(random_quick_sort, sorted_data)
print(f&quot;数据规模 n={n}: 固定枢轴快排 {time_fixed:.2f} ms, 随机枢轴快排 {time_random:.2f} ms&quot;)
</code></pre>
</body>
</html>

View File

@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>基于比较的排序</h1>
<h2>基于比较的排序</h2>
<h3>优先队列的原理</h3>
<p><strong>优先队列Priority Queue</strong> 是一种抽象数据结构,它支持以最高(或最低)优先级为先进行元素的插入和取出操作。
<br>
在优先队列中,每次检索得到的都是当前权重最大(或最小)的元素。
<br><br>
为了实现优先队列,最直接的思路是维护一个内部元素有序的数组:每次取数就是第一个数,存数时将元素放入合适位置,并调整序列。
<br><br>
<strong>Heap结构</strong>是实现优先队列的首选。</p>
<h3>数组模拟堆实现的优先队列</h3>
<h4>堆结构与取存原理</h4>
<p>Heap本质上是一个完全二叉树结构
(当然也可以是多叉树,但没有必要)
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b73d7df5-018e-4542-b891-b3711c42c56a" alt="图1大根堆的二叉树结构图示" /></p>
<p>这里用“大根堆”为例,从图中可以看到,每一个节点都比它的子节点更大。
既然“优先队列”可以理解为一种特殊的“队列”,那么我们先用堆实现这个队列的出和入:</p>
<h5>取数</h5>
<p>从优先队列中取数,显然堆顶的数就是要的那个最大者。
但是将这个数取出后还不能结束,因为需要维护堆的性质。
<br>
为了维护堆性质,一般通过将末尾的元素放到堆顶,然后将其不断与左右儿子进行替换,直到他比两个儿子都大或儿子不存在为止,称为“下滤”。
<br></p>
<h5>存数</h5>
<p>与取数类似,重要的是维护堆的性质。将新数放到最后(上图中的第一个黑色节点中)后,将这个数进行“上浮”。</p>
<h4>数组模拟堆</h4>
<p>为了实现堆,其实不需要真的写一个二叉树,用数组就可以做到。
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b4142d35-630c-4d12-a147-d514cca9e0d5" alt="图2左边是堆的数组表示右边是其对应的二叉树" />
如上图,左边是堆的数组表示,右边是其对应的二叉树。</p>
<p>数组下标从1开始任意一个下标i其左右儿子下标刚好就是&quot;i*2&quot;和“i*2+1”。这样在后续代码实现时代码写起来就会简单很多。</p>
<h6>建堆</h6>
<p>用数组模拟堆还有一个好处,就是可以“原地建堆”。
对于一个元素随机的数组只需要O(n)的时间复杂度就可以完成随机数组向堆的转化。</p>
<p>具体做法为“从后向前”遍历,对于每一个非叶子节点,就将其进行“下滤”,这样以它为根的子树就变成一个小堆。往前遍历即可。</p>
<h3>优先队列的算法实现</h3>
<h4>练习:堆操作</h4>
<p>在进行代码实现之前,做一个问题练习:
对于一个随机队列&quot;[3, 32, 6, 43, 5, 8, 0, 9]&quot;,经过一轮反向扫描下滤建大根堆操作,得到的堆的序列是什么?
将答案告知AI教师。</p>
<h4>任务:城市事件优先调度</h4>
<p>现在,让我们通过编程实践来掌握堆排序的实现。假设我们需要对城市中发生的一系列事件按照紧急程度(以数值大小表示优先级)排序,从而依次处理最高优先级的事件。这相当于将一组数字按从大到小排序的过程,与堆排序的机制完全一致。</p>
<p><br><br></p>
<h4>题目:实现堆排序</h4>
<p>请你实现一个堆排序算法 heap_sort(arr),将传入的数组利用堆排序方法排序(从大到小)。
<br>
你可以通过实现max_heapify和build_max_heap等函数来完成这一任务。
<br>
完成编码后,我们将对算法的性能进行测试,比较不同规模输入下堆排序运行时间的增长情况。
<br></p>
<h5>代码框架</h5>
<p>请在下方代码编辑区完成 max_heapify、build_max_heap 和 heap_sort 函数的实现。</p>
<pre><code class="language-python">import random
import time
def max_heapify(arr, n, i):
&quot;&quot;&quot;
维护最大堆性质:假设结点 i 的左右子树已经是最大堆,
调整结点 i 使以其为根的子树成为最大堆
参数:
arr: 存储堆的数组
n: 堆的有效大小(长度)
i: 需要下滤调整的节点索引
&quot;&quot;&quot;
# TODO: 在此处实现 &quot;下滤&quot; 操作,将 arr[i] 下沉到正确位置
def build_max_heap(arr):
&quot;&quot;&quot;
将无序数组原地建成最大堆,从后往前进行下滤
参数:
arr: 待调整的数组
&quot;&quot;&quot;
# TODO: 调用 max_heapify 将 arr 调整为堆
def heap_sort(arr):
&quot;&quot;&quot;
利用堆排序算法排序数组(降序)
参数:
arr: 待排序数组
返回:
排序后的数组(从大到小)
&quot;&quot;&quot;
# TODO: 完成堆排序的实现
n = len(arr)
# 1. 原地建堆
build_max_heap(arr)
# 2. 依次将当前堆顶(最大值)交换到数组末尾,并缩小堆的范围,然后下滤
#性能测试:对比不同规模输入的堆排序用时
def measure(sort_func, data):
start = time.perf_counter_ns()
sort_func(data.copy())
end = time.perf_counter_ns()
return (end - start) / 10**6 # 毫秒
sizes = [1000, 5000, 10000]
print(&quot;堆排序性能测试:&quot;)
for n in sizes:
data = [random.randint(0, 1000000) for _ in range(n)]
t = measure(heap_sort, data)
print(f&quot;数据规模 n={n}: 排序耗时 {t:.2f} ms&quot;)
</code></pre>
<h5>实验结果分析</h5>
<p>请完成并运行上述代码,观察不同输入规模下算法的执行时间。理论上,堆排序的时间复杂度为<eq>O(n \log n)</eq>当输入规模增大时运行时间应呈现近似线性乘以对数的增长趋势。具体来说若将输入规模扩大10倍运行时间将增加约<eq>10 \times \log_2(10) \approx 10 \times 3.3 \approx 33</eq>倍左右。
<br>
相比之下,简单的<eq>O(n^2)</eq>排序算法在相同扩大量级下耗时会增加约100倍。通过与之前插入排序实验的对比你会发现堆排序对规模扩大的响应增长显著缓慢得多。
<br>
这印证了堆排序的效率优势:在最坏情况和平均情况下它都能维持<eq>O(n \log n)</eq>的性能,不会出现如快速排序在极端情况下退化为<eq>O(n^2)</eq>的尴尬局面。
<br>
此外,堆排序是一种原地排序(只需要常数级别的额外空间),这也是相对于归并排序的一个优势。综合来看,利用优先队列实现的堆排序在效率和空间上都表现出色,是一种成熟可靠的排序方法。</p>
<h3>比较排序的决策树模型</h3>
<p>前面的内容介绍了多种基于元素比较的排序算法(比较排序),包括快速排序、堆排序等。接下来,我们讨论一个重要的理论结果:
<br>
在比较模型下,任意排序算法的最优时间复杂度下界为<eq>Ω(n \log n)</eq>
这个结论意味着,无论设计何种巧妙的比较排序算法,都无法突破这一定义上的效率极限。证明这一点的经典工具是决策树模型。</p>
<p>决策树是描述比较排序过程的一种抽象模型。
在排序过程中,每进行一次比较(例如“<eq>A[i] \le A[j]</eq>?”)就相当于根据结果(二叉决策:是/否)将可能的输入情况划分到两个分支。
<br>
整个排序算法的运行过程可以被看作是在这样一棵决策树上从根节点走向某个叶节点的过程。决策树的每个叶节点对应一种可能的输入集合及其确定的输出顺序。
<br>
当有<eq>n</eq>个待排序元素时,假设它们两两各不相同,则可能的输入排列情况共有<eq>n!种(所有元素的全排列)。为了正确地将每种输入排列映射到唯一的输出(即排好序的有序序列),排序算法的决策树必须至少具备</eq>n!个叶节点——每个叶子对应一种输入排列的判别结果。</p>
<p>对于一棵二叉决策树,若包含<eq>L</eq>个叶节点,其高度<eq>h</eq>满足<eq>L \le 2^h</eq>,因此<eq>h \ge \lceil \log_2 L \rceil</eq>。在排序问题中,<eq>L</eq>最少取<eq>n!</eq>,因此最优情况下决策树高度也满足:
<br>
<eq>$h_{\min} \geq \lceil \log_2(n!) \rceil.</eq>$
利用对数运算的性质,可以估计<eq>\log_2(n!)</eq>的数量级。根据斯特林公式近似,<eq>n!</eq>大约为<eq>(n/e)^n</eq>的数量级,那么:
<br>
<eq>$\log_2(n!) \approx \log_2\left((n/e)^n\sqrt{2\pi n}\right) = n\log_2 n - n\log_2 e + O(\log n).</eq>$
<br>
可以看出,当<eq>n</eq>较大时,<eq>\log_2(n!) = Θ(n \log n)</eq>。这意味着决策树的高度下界<eq>h_{\min} = Ω(n \log n)</eq>。换言之,任何基于比较的排序算法在最理想情况下也需要执行与<eq>n \log n</eq>同数量级的比较操作。
<br>
例如,对于$ n=3$的简单情况,<eq>3! = 6</eq>,满足<eq>2^2 < 6 < 2^3</eq>因此判定3个元素的任意排列需要至少3次比较。这与我们已知的事实相符对三个无任何特殊性质的数进行排序最少需要3次比较才能确定它们的正确顺序。</p>
</body>
</html>

View File

@@ -0,0 +1,219 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>非比较型排序</h1>
<h2>非比较型排序</h2>
<h3>基数排序原理</h3>
<h4>排序算法复杂度背景</h4>
<p>基于比较的排序算法,理论上无法突破(Ω(n \log n))时间复杂度下界
特殊场景下,不依赖元素间比较、利用元素结构特征,可实现线性时间排序</p>
<h4>基数排序核心思想</h4>
<p>将元素视为由多个 “位”digit组成
按位进行多轮排序,每轮针对一个关键位
依赖稳定的子排序算法,保证先前排好的顺序不被打乱
关键保障:<strong>排序的稳定性(相同元素相对顺序不变)</strong></p>
<h4>经典示例(十进制整数的基数排序):</h4>
<p>按最低有效位LSB→ 最高有效位MSB 逐轮排序
第一轮按个位数排序0-9 分组)
后续轮次:依次按十位数、百位数等排序</p>
<h4>实操流程:</h4>
<p>初始序列:[329, 457, 657, 839, 436]
第一轮(按个位排序):
按个位 0-9 分组 → [436(6), 457(7), 657(7), 329(9), 839(9)]
457 与 657 个位相同,保持原顺序)
第二轮(按十位排序):
按十位 0-9 分组 → [329(2), 436(3), 839(3), 457(5), 657(5)]
436 与 839 十位相同,保持第一轮后的顺序)
第三轮(按百位排序):
按百位 0-9 分组 → [329(3), 436(4), 457(4), 657(6), 839(8)]
436 与 457 百位相同,保持第二轮后的顺序)</p>
<p>每轮排序通过稳定性保留前序结果,最终实现 “高位决定整体顺序”,完成正确排序。</p>
<h4>时间复杂度:</h4>
<p>每轮扫一遍序列O(n)总共轮数就是基数位数设为k。
时间复杂度为O(kn)其中k一般不太大。</p>
<h3>基数排序的实现</h3>
<p>为了加深对基数排序的理解我们通过一个编程任务来实践其实现。我们将以整数排序为例并采用从最低有效位开始的方法对数字进行排序LSD 基数排序)。</p>
<p>在实现过程中,我们需要编写一个按某个位数进行计数排序的辅助函数,然后在主函数中对每一位循环调用该辅助函数。</p>
<h4>用计数排序稳定地对基数排序</h4>
<p>每选定一个基数后需要用O(n)的时间复杂度将当前序列按照这个基数进行排序;或说<strong>找到按照当前基数下,当前序列的每一个数要前往的位置</strong>
这里用“计数排序”这个算法。
具体做法如下:</p>
<ol>
<li>建一个新数组count记录当前基数的每个不同的数有多少个计数</li>
<li>从前往后遍历序列,取出序列每一个元素在当前基数位置上的值,对应计数值+1count[number]+=1</li>
<li>将count转变为前缀和这样就得到了每一个基数对应的原始数的最后一个在新序列中的位置</li>
<li>从后往前遍历原序列将每一个数基数上的数为number放到对应新序列count[number]-1的位置并让count[number]-=1。</li>
</ol>
<h5>练习:按位计数排序</h5>
<p>作为练习,[170, 45, 75, 90, 802, 24, 2, 66]按个位排序exp=1进行计数排序请写下count数组的计数结果2.步后和前缀和结果。并表述在第4步的利用方式从而得到序列结果。</p>
<h4>任务:实现 LSD 基数排序</h4>
<p>请实现 radix_sort(arr) 函数,对传入的整数列表进行从小到大的排序。你可以假设所有整数为非负且位数相对固定。为了简化问题,我们以十进制为基数(基数=10实现并假定输入整数的最大位数为<eq>d</eq>(例如<eq>10^d</eq>数量级)。算法思路如下:</p>
<p>从最低位个位exp=1开始对数组进行计数排序Counting Sort按该位的数值对元素排序。</p>
<p>然后依次向更高一位exp=10exp=100...)进行排序,直到处理完最高位。</p>
<h4>代码框架</h4>
<p>请在下方代码编辑区完成 counting_sort_by_digit 和 radix_sort 函数的实现。</p>
<pre><code class="language-python">import random
import time
def counting_sort_by_digit(arr, exp):
&quot;&quot;&quot;
对数组按某个数位进行计数排序(稳定的)。
参数:
arr: 要排序的数组
exp: 位标志,表示当前要按照 (exp位) 上的数值进行排序,
例如 exp=1 表示按个位排序exp=10 表示按十位排序。
返回:
按指定位排序后的数组副本
&quot;&quot;&quot;
n = len(arr)
output = [0] * n # 存放排序结果
count = [0] * 10 # 计数数组十进制位范围0-9
# TODO: 计算每个数组元素在该位上的值,并计数
def radix_sort(arr):
&quot;&quot;&quot;
基数排序(按从低位到高位逐位排序)
参数:
arr: 待排序的整数列表
返回:
按非递减顺序排序的列表
&quot;&quot;&quot;
#TODO: 利用 counting_sort_by_digit 按位排序数组
#测试与性能分析
def measure(sort_func, data):
start = time.perf_counter_ns()
sort_func(data)
end = time.perf_counter_ns()
return (end - start) / 10**6 # 转换为毫秒
#生成随机测试数据
sizes = [10000, 50000, 100000]
print(&quot;基数排序性能测试:&quot;)
for n in sizes:
data = [random.randrange(0, 1000000) for _ in range(n)]
t = measure(radix_sort, data)
print(f&quot;数据规模 n={n}: 排序耗时 {t:.2f} ms&quot;)
</code></pre>
<p>性能讨论</p>
<p>在完成代码并运行测试后,可以看到基数排序在不同规模输入下运行时间增长相对温和。理想情况下,对于固定位数<eq>d</eq>的整数,基数排序的时间复杂度为<eq>O(d \times (n + k))</eq>,其中<eq>k</eq>为每位可能的取值数量(对十进制整数而言<eq>k=10</eq>)。
<br>
<eq>d</eq>视作常数时,复杂度近似<eq>O(n)</eq>,因此输入规模扩大倍数时,运行时间应近似线性增长。
<br>
然而需注意基数排序的实际常数因子不小多次稳定排序和额外空间对中等规模数据Python实现未必比C语言内建排序更快。
<br>
更重要的是,基数排序并非万能:如果元素位数<eq>d</eq><eq>n</eq>增长(例如排序<eq>1</eq><eq>N</eq>范围的数,<eq>N</eq>约为<eq>n</eq>量级),则<eq>d = O(\log n)</eq>,此时基数排序复杂度<eq>≈O(n \log n)</eq>,并没有打破比较排序下界。因此,基数排序的线性性能依赖于位数相对较小这一前提。</p>
<h3>桶排序的原理与实现</h3>
<h4>桶排序核心定位</h4>
<p>典型的线性时间排序算法
利用输入数据的<strong>分布特性(假设大致均匀分布在某范围)</strong></p>
<h4>桶排序原理与类比</h4>
<h5>核心逻辑(三步):</h5>
<p>第一步:创建若干 “桶”,将元素按值映射到对应桶中(实现粗分类)
第二步:对每个桶内元素单独排序(可选用任意排序算法)
第三步:按桶的顺序合并所有元素,得到有序序列
<br>
映射:将小数按值投入对应桶,每个桶仅覆盖原始区间 1/10数据量少
桶内排序:因元素少,插入排序等简单算法足够快
合并:按桶序号收集,因 “i&lt;j 时,第 i 号桶元素<第 j 号桶元素”,天然有序
<br></p>
<h5>案例练习</h5>
<p>使用桶排序算法对以下 8 个数字进行排序
[35, 12, 48, 27, 5, 39, 18, 42]
以48为分母将其他所有数划分到0-0.2-0.4-0.6-0.8-1这5个桶中输出每个桶里面的数字是什么。
<br><br></p>
<h4>代码实现</h4>
<p>请实现 bucket_sort(arr) 函数,将传入的[0,1)区间的小数数组排序。思路如下:
<br>
创建若干个空桶列表桶的数量可以根据数据规模n选择这里取<eq>n/10</eq>(为整数)个桶。
<br>
将每个元素按照其值乘以桶数量后的整数部分映射到对应的桶中。例如值为0.23、桶数为10时<eq>0.23 \times 10 = 2.3</eq>放入索引2号桶。
<br>
对每个非空桶内部进行排序。你可以直接使用 Python 内置排序sorted或实现简单排序算法。</p>
<p>按桶的顺序依次合并桶内元素,得到排序后的结果。</p>
<h5>代码框架</h5>
<pre><code class="language-python">import random
import time
def bucket_sort(arr):
&quot;&quot;&quot;
桶排序适用于0到1区间的小数
参数:
arr: 介于[0,1)的浮点数列表
返回:
升序排序后的列表
&quot;&quot;&quot;
# TODO: 按原理实现桶排序
n = len(arr)
if n == 0:
return arr
# 1. 创建桶
# 2. 分配元素到各桶
# 3. 桶内排序
#简单测试与性能评估
sizes = [1000, 5000, 10000]
print(&quot;桶排序性能测试:&quot;)
for n in sizes:
#生成n个[0,1)均匀分布的小数
data = [random.random() for _ in range(n)]
start = time.perf_counter_ns()
sorted_data = bucket_sort(data)
end = time.perf_counter_ns()
elapsed = (end - start) / 10**6
#验证排序正确性
is_correct = &quot;&quot; if sorted_data == sorted(data) else &quot;×&quot;
print(f&quot;数据规模 n={n}: 排序耗时 {elapsed:.2f} ms, 排序正确性 {is_correct}&quot;)
</code></pre>
<h4>桶排序时间复杂度分析</h4>
<h5>平均情况(理想分布):</h5>
<p>前提n 个元素、m 个桶,数据均匀分布 → 每个桶平均含 n/m 个元素
优化:选 m 接近 n 的量级 → 桶内排序成本视为常数
结果整体期望运行时间为O(n)(线性时间)</p>
<h5>最坏情况(分布不均):</h5>
<p>极端场景:所有元素落入同一个桶
复杂度取决于桶内排序算法O (n log n) 或 O (n²)+ 分配桶的 O (n)
结果退化至O (n log n) 甚至更差,失去线性时间优势
结论:理想分布下性能出众,数据分布不均时无优势</p>
<h3>桶排序与哈希</h3>
<h4>核心相通点:映射分散思想</h4>
<p>通过函数映射将元素分散到不同 “容器”(桶排序的 “桶”/ 哈希算法的 “哈希桶”),把原问题拆解为小的子问题
<br>
桶排序:用映射函数(如 index = int(num * bucket_count))划分元素所属桶,依据元素值分配
哈希算法:用哈希函数计算键的哈希桶索引,确定元素存储位置
这种映射也意味着都对<strong>分布均匀性的依赖</strong></p>
<h4>关键区别:映射函数的目标差异</h4>
<p>对比维度
桶排序的映射函数:一般直接做除法:保留元素大小顺序(按值范围划分)
哈希算法的哈希函数:随机映射函数,实现元素均匀分布,不能用于排序(除非特殊设计有序哈希)</p>
<h4>总结</h4>
<p>桶排序可看作哈希思想在排序问题中的应用:均通过 “先分类、后处理” 的范式优化性能,但因目标不同,映射函数设计存在本质差异。</p>
</body>
</html>

View File

@@ -0,0 +1,193 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>算法设计范式:分而治之与归并排序</h1>
<blockquote>
<p>Auto-generated at 2025-09-22 15:38</p>
</blockquote>
<h2>算法设计范式:分而治之与归并排序</h2>
<h3>分治法:分而治之</h3>
<h4>引入</h4>
<p>在算法分析与设计中,有一些设计算法解决问题的通用方法可供大家选取尝试,其中一个很常用的方法就叫做“分治法”,字如其意,
分治法就是将大问题分解为小问题(或说子问题),并将子问题持续分解下去,直到子问题可以用常数时间进行求解。
最基础的问题:何时可以用分治法?</p>
<ol>
<li>找最值问题</li>
<li>回文字符串</li>
</ol>
<h4>分治法的组成</h4>
<p>分治法有3个重要组成分解、解决、合并。</p>
<p>有一些帮助我们进行这些操作的分析与设计原则:</p>
<ol>
<li><strong>子问题相似</strong>:分解出的子问题与原问题类型一致,可复用相同解法,保证递归或分治逻辑的一致性。</li>
<li><strong>子问题互不干扰</strong>:子问题边界清晰、数据独立,避免相互影响,简化问题拆解与求解逻辑。</li>
<li><strong>合并代价可控</strong>:子问题解合并为原问题解的时间 / 空间开销在可接受范围,避免抵消分治带来的效率提升。</li>
</ol>
<h4>分治法的时间复杂度迭代公式</h4>
<p>Tn= 子问题数量 * T 子问题规模 + 合并这些子问题的解的用时</p>
<h3>归并算法理论分析</h3>
<p><strong>案例背景:智慧城市交通优化系统</strong></p>
<p>在上周的工作中你已经实现并分析了插入排序在交通数据处理中的性能。虽然它在数据量较小或基本有序的情况下表现不错但在应对大规模拥堵逆序数据其性能瓶颈非常明显。本周你的项目经理Agent将向你介绍一种更高效的算法设计思想——“分而治之”并要求你掌握其代表算法归并排序。</p>
<h4>任务:理解与分析</h4>
<h5>场景介绍</h5>
<p>归并排序Merge Sort是解决大规模排序问题的利器。在投入编码之前你必须先从理论上理解它为何如此高效。</p>
<h5>思考题</h5>
<p>请与右侧的Agent你的项目经理对话清晰地回答以下问题以证明你已理解核心思想</p>
<ol>
<li>
<p><strong>分治思想</strong>根据课程资料请解释什么是“分而治之”Divide-and-Conquer设计范式它包含哪三个基本步骤</p>
</li>
<li>
<p><strong>递推关系</strong>:归并排序的时间复杂度可以用递推式 T(n)=2T(n/2)+Θ(n) 来表示。请解释这个式子各部分的含义:</p>
<ul>
<li><code>2</code> 代表什么?</li>
<li><code>T(n/2)</code> 代表什么?</li>
<li><code>Θ(n)</code> 代表什么?</li>
</ul>
</li>
<li>
<p><strong>效率对比</strong>你的同事认为插入排序经过极致优化后处理n个数据的耗时为 <eq>0.1n^2</eq>;而归并排序由于递归和合并开销,耗时为 1000nlog_2n。在处理超大规模城市交通数据时即n趋于无穷大时哪个算法最终会胜出请从渐进增长的角度解释你的理由。</p>
</li>
<li>
<p><strong>增长排序</strong>:将下列函数的渐进增长率从低到高排序,并将它们划分到等价类(即 Θ 关系相同的函数)。这有助于你理解不同算法效率的所在层级。</p>
<ul>
<li><eq>n^2</eq> (插入排序最坏情况)</li>
<li>nlogn (归并排序)</li>
<li>n (一种非常低效的蛮力算法)</li>
<li><eq>2^n</eq> (另一种指数级算法)</li>
<li>logn (类似二分查找的效率)</li>
</ul>
</li>
</ol>
<h3>归并排序的编程实现</h3>
<h4>任务:编码与对比</h4>
<h5>场景介绍</h5>
<p>现在你已经从理论上理解了归并排序的威力。是时候亲手实现它,并与上周的插入排序进行一次性能上的正面较量了!</p>
<h5>题目:实现归并排序并对比性能</h5>
<p>你需要实现归并排序的核心函数,并利用上周的测试框架来直观对比它与插入排序在处理不同交通数据时的表现。</p>
<h5>代码框架</h5>
<p>在下方代码编辑区,完成 <code>merge_sort(arr)</code><code>merge(left, right)</code> 两个函数的实现。</p>
<pre><code class="language-python">import random
import time
#上周实现的插入排序(用于对比)
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j &gt;= 0 and arr[j] &gt; key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
#--- 本周任务:请在下方实现归并排序 ---
def merge_sort(arr):
&quot;&quot;&quot;
实现归并排序算法
参数arr: 待排序的交通数据数组
返回: 一个新的、排序好的数组
&quot;&quot;&quot;
# TODO: 实现归并排序的递归逻辑
# 提示当数组元素个数小于等于1时递归终止
def merge(left, right):
&quot;&quot;&quot;
合并两个已排序的子数组
参数left: 左侧已排序数组
参数right: 右侧已排序数组
返回: 合并后的一个有序数组
&quot;&quot;&quot;
#--- 测试与对比部分 ---
def measure_performance(sort_func, data):
start_time = time.perf_counter_ns()
sort_func(data.copy())
end_time = time.perf_counter_ns()
return (end_time - start_time) / 10**6
network_sizes = [1000, 5000, 10000, 50000]
print(&quot;算法性能对比测试:&quot;)
for size in network_sizes:
# 生成逆序数据,这是插入排序的最坏情况
worst_case_data = list(range(size, 0, -1))
time_insertion = measure_performance(insertion_sort, worst_case_data)
time_merge = measure_performance(merge_sort, worst_case_data)
print(f&quot;--- 网络规模 n={size} (最坏情况) ---&quot;)
print(f&quot; - 插入排序: {time_insertion:.2f} ms&quot;)
print(f&quot; - 归并排序: {time_merge:.2f} ms&quot;)
</code></pre>
<h5>分析与讨论</h5>
<p>完成编程并得到输出后请与右侧Agent讨论以下问题</p>
<ol>
<li>
<p><strong>性能验证</strong>:观察“交通大堵塞”(最坏情况)的测试结果,归并排序的性能表现与插入排序有何天壤之别?这是否验证了你在第一关的理论分析?</p>
</li>
<li>
<p><strong>性能稳定性</strong>:如果我们将测试数据换成“畅通无阻”(最好情况)或“随机车流”(平均情况),你认为归并排序的运行时间会发生巨大变化吗?为什么?</p>
</li>
<li>
<p><strong>空间成本</strong>:在实现 <code>merge</code> 函数时,你可能创建了一个新的数组 <code>merged</code> 来存放结果。这在算法分析中被称为“空间复杂度”。与在原数组上进行交换的插入排序相比,归并排序的这个特点是优点还是缺点?</p>
</li>
<li>
<p><strong>最终决策</strong>:作为项目工程师,你会选择哪种排序算法用于我们的大规模交通调度系统?请综合考虑<strong>时间效率</strong><strong>空间成本</strong>来陈述你的理由。</p>
</li>
</ol>
<h3>进一步研究分治问题的时间复杂度分析</h3>
<p>分治问题的两种时间复杂度分析方法:主方法和递归树法。</p>
<h4>主方法</h4>
<p>主方法本质是一种速算公式针对于分治迭代式是这样的形式的T(n)=aT(n/b)+f(n)a&gt;=1b&gt;1。
主方法概念上理解,就是比较分治法的“子问题总数”和“合并总数”,在问题复杂度上,哪个为“主”,就按照这个主部分计算时间复杂度。
(因为时间复杂度公式上,我们只会保留增长最快的那一项,也就是“主项”)</p>
<p>严格定义这里就略了,直接看口诀:</p>
<ol>
<li>算log_b (a)得到n^log_b (a)</li>
<li>比f(n)和n^log_b (a)的增长速度:</li>
</ol>
<ul>
<li>前者慢 → O(n^log_b (a))</li>
<li>差不多 → O(n^log_b (a) · log n)k=0 时);</li>
<li>前者快且满足正则条件 → O(f(n))。</li>
<li>其中正则条件是存在0&lt;c&lt;1使得a·f(n/b)≤ c·n²</li>
</ul>
<p>试着计算一下T(n) = 3T(n/2) + O(n²) 这个迭代式的时间复杂度</p>
<h4>递归树法</h4>
<p>递归树相对更容易理解(可能之前的问题你已经在不知觉中使用递归树法),因为这种方法完全就是对分治方法本身的全流程模拟。</p>
<p>步骤 1画树 —— 把问题拆解成 “节点”,标记每个节点的分解与合并耗时(父节点的解决耗时其实就是自己的所有子节点的分解与合并耗时)
a. 根节点:写原问题的规模 n 和分解 / 合并耗时 f (n)
b. 子节点:根节点拆成几个子问题,就画几个子节点,每个子节点写 “子问题规模 + 子问题的分解 / 合并耗时”
c. 叶子节点:当子问题规模小到能直接解决(比如 n=1就停止拆解叶子节点写 “规模 1耗时 Θ(1)”
步骤 2算层 —— 求每一层的总耗时
同一层的节点,代表 “同一轮拆解的所有问题”,它们的耗时总和就是这一层的总代价
步骤 3求和 —— 总耗时 = 所有层的代价之和
先确定树有多少层,再把每层的总代价加起来</p>
<p>试着计算:无法用主方法解决的问题: T (n)=T (n/3)+T (2n/3)+2*n</p>
</body>
</html>

View File

@@ -0,0 +1,193 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>算法设计范式:分而治之与归并排序</h1>
<blockquote>
<p>Auto-generated at 2025-09-22 15:38</p>
</blockquote>
<h2>算法设计范式:分而治之与归并排序</h2>
<h3>分治法:分而治之</h3>
<h4>引入</h4>
<p>在算法分析与设计中,有一些设计算法解决问题的通用方法可供大家选取尝试,其中一个很常用的方法就叫做“分治法”,字如其意,
分治法就是将大问题分解为小问题(或说子问题),并将子问题持续分解下去,直到子问题可以用常数时间进行求解。
最基础的问题:何时可以用分治法?</p>
<ol>
<li>找最值问题</li>
<li>回文字符串</li>
</ol>
<h4>分治法的组成</h4>
<p>分治法有3个重要组成分解、解决、合并。</p>
<p>有一些帮助我们进行这些操作的分析与设计原则:</p>
<ol>
<li><strong>子问题相似</strong>:分解出的子问题与原问题类型一致,可复用相同解法,保证递归或分治逻辑的一致性。</li>
<li><strong>子问题互不干扰</strong>:子问题边界清晰、数据独立,避免相互影响,简化问题拆解与求解逻辑。</li>
<li><strong>合并代价可控</strong>:子问题解合并为原问题解的时间 / 空间开销在可接受范围,避免抵消分治带来的效率提升。</li>
</ol>
<h4>分治法的时间复杂度迭代公式</h4>
<p>Tn= 子问题数量 * T 子问题规模 + 合并这些子问题的解的用时</p>
<h3>归并算法理论分析</h3>
<p><strong>案例背景:智慧城市交通优化系统</strong></p>
<p>在上周的工作中你已经实现并分析了插入排序在交通数据处理中的性能。虽然它在数据量较小或基本有序的情况下表现不错但在应对大规模拥堵逆序数据其性能瓶颈非常明显。本周你的项目经理Agent将向你介绍一种更高效的算法设计思想——“分而治之”并要求你掌握其代表算法归并排序。</p>
<h4>任务:理解与分析</h4>
<h5>场景介绍</h5>
<p>归并排序Merge Sort是解决大规模排序问题的利器。在投入编码之前你必须先从理论上理解它为何如此高效。</p>
<h5>思考题</h5>
<p>请与右侧的Agent你的项目经理对话清晰地回答以下问题以证明你已理解核心思想</p>
<ol>
<li>
<p><strong>分治思想</strong>根据课程资料请解释什么是“分而治之”Divide-and-Conquer设计范式它包含哪三个基本步骤</p>
</li>
<li>
<p><strong>递推关系</strong>:归并排序的时间复杂度可以用递推式 T(n)=2T(n/2)+Θ(n) 来表示。请解释这个式子各部分的含义:</p>
<ul>
<li><code>2</code> 代表什么?</li>
<li><code>T(n/2)</code> 代表什么?</li>
<li><code>Θ(n)</code> 代表什么?</li>
</ul>
</li>
<li>
<p><strong>效率对比</strong>你的同事认为插入排序经过极致优化后处理n个数据的耗时为 <eq>0.1n^2</eq>;而归并排序由于递归和合并开销,耗时为 1000nlog_2n。在处理超大规模城市交通数据时即n趋于无穷大时哪个算法最终会胜出请从渐进增长的角度解释你的理由。</p>
</li>
<li>
<p><strong>增长排序</strong>:将下列函数的渐进增长率从低到高排序,并将它们划分到等价类(即 Θ 关系相同的函数)。这有助于你理解不同算法效率的所在层级。</p>
<ul>
<li><eq>n^2</eq> (插入排序最坏情况)</li>
<li>nlogn (归并排序)</li>
<li>n (一种非常低效的蛮力算法)</li>
<li><eq>2^n</eq> (另一种指数级算法)</li>
<li>logn (类似二分查找的效率)</li>
</ul>
</li>
</ol>
<h3>归并排序的编程实现</h3>
<h4>任务:编码与对比</h4>
<h5>场景介绍</h5>
<p>现在你已经从理论上理解了归并排序的威力。是时候亲手实现它,并与上周的插入排序进行一次性能上的正面较量了!</p>
<h5>题目:实现归并排序并对比性能</h5>
<p>你需要实现归并排序的核心函数,并利用上周的测试框架来直观对比它与插入排序在处理不同交通数据时的表现。</p>
<h5>代码框架</h5>
<p>在下方代码编辑区,完成 <code>merge_sort(arr)</code><code>merge(left, right)</code> 两个函数的实现。</p>
<pre><code class="language-python">import random
import time
#上周实现的插入排序(用于对比)
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j &gt;= 0 and arr[j] &gt; key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
#--- 本周任务:请在下方实现归并排序 ---
def merge_sort(arr):
&quot;&quot;&quot;
实现归并排序算法
参数arr: 待排序的交通数据数组
返回: 一个新的、排序好的数组
&quot;&quot;&quot;
# TODO: 实现归并排序的递归逻辑
# 提示当数组元素个数小于等于1时递归终止
def merge(left, right):
&quot;&quot;&quot;
合并两个已排序的子数组
参数left: 左侧已排序数组
参数right: 右侧已排序数组
返回: 合并后的一个有序数组
&quot;&quot;&quot;
#--- 测试与对比部分 ---
def measure_performance(sort_func, data):
start_time = time.perf_counter_ns()
sort_func(data.copy())
end_time = time.perf_counter_ns()
return (end_time - start_time) / 10**6
network_sizes = [1000, 5000, 10000, 50000]
print(&quot;算法性能对比测试:&quot;)
for size in network_sizes:
# 生成逆序数据,这是插入排序的最坏情况
worst_case_data = list(range(size, 0, -1))
time_insertion = measure_performance(insertion_sort, worst_case_data)
time_merge = measure_performance(merge_sort, worst_case_data)
print(f&quot;--- 网络规模 n={size} (最坏情况) ---&quot;)
print(f&quot; - 插入排序: {time_insertion:.2f} ms&quot;)
print(f&quot; - 归并排序: {time_merge:.2f} ms&quot;)
</code></pre>
<h5>分析与讨论</h5>
<p>完成编程并得到输出后请与右侧Agent讨论以下问题</p>
<ol>
<li>
<p><strong>性能验证</strong>:观察“交通大堵塞”(最坏情况)的测试结果,归并排序的性能表现与插入排序有何天壤之别?这是否验证了你在第一关的理论分析?</p>
</li>
<li>
<p><strong>性能稳定性</strong>:如果我们将测试数据换成“畅通无阻”(最好情况)或“随机车流”(平均情况),你认为归并排序的运行时间会发生巨大变化吗?为什么?</p>
</li>
<li>
<p><strong>空间成本</strong>:在实现 <code>merge</code> 函数时,你可能创建了一个新的数组 <code>merged</code> 来存放结果。这在算法分析中被称为“空间复杂度”。与在原数组上进行交换的插入排序相比,归并排序的这个特点是优点还是缺点?</p>
</li>
<li>
<p><strong>最终决策</strong>:作为项目工程师,你会选择哪种排序算法用于我们的大规模交通调度系统?请综合考虑<strong>时间效率</strong><strong>空间成本</strong>来陈述你的理由。</p>
</li>
</ol>
<h3>进一步研究分治问题的时间复杂度分析</h3>
<p>分治问题的两种时间复杂度分析方法:主方法和递归树法。</p>
<h4>主方法</h4>
<p>主方法本质是一种速算公式针对于分治迭代式是这样的形式的T(n)=aT(n/b)+f(n)a&gt;=1b&gt;1。
主方法概念上理解,就是比较分治法的“子问题总数”和“合并总数”,在问题复杂度上,哪个为“主”,就按照这个主部分计算时间复杂度。
(因为时间复杂度公式上,我们只会保留增长最快的那一项,也就是“主项”)</p>
<p>严格定义这里就略了,直接看口诀:</p>
<ol>
<li>算log_b (a)得到n^log_b (a)</li>
<li>比f(n)和n^log_b (a)的增长速度:</li>
</ol>
<ul>
<li>前者慢 → O(n^log_b (a))</li>
<li>差不多 → O(n^log_b (a) · log n)k=0 时);</li>
<li>前者快且满足正则条件 → O(f(n))。</li>
<li>其中正则条件是存在0&lt;c&lt;1使得a·f(n/b)≤ c·n²</li>
</ul>
<p>试着计算一下T(n) = 3T(n/2) + O(n²) 这个迭代式的时间复杂度</p>
<h4>递归树法</h4>
<p>递归树相对更容易理解(可能之前的问题你已经在不知觉中使用递归树法),因为这种方法完全就是对分治方法本身的全流程模拟。</p>
<p>步骤 1画树 —— 把问题拆解成 “节点”,标记每个节点的分解与合并耗时(父节点的解决耗时其实就是自己的所有子节点的分解与合并耗时)
a. 根节点:写原问题的规模 n 和分解 / 合并耗时 f (n)
b. 子节点:根节点拆成几个子问题,就画几个子节点,每个子节点写 “子问题规模 + 子问题的分解 / 合并耗时”
c. 叶子节点:当子问题规模小到能直接解决(比如 n=1就停止拆解叶子节点写 “规模 1耗时 Θ(1)”
步骤 2算层 —— 求每一层的总耗时
同一层的节点,代表 “同一轮拆解的所有问题”,它们的耗时总和就是这一层的总代价
步骤 3求和 —— 总耗时 = 所有层的代价之和
先确定树有多少层,再把每层的总代价加起来</p>
<p>试着计算:无法用主方法解决的问题: T (n)=T (n/3)+T (2n/3)+2*n</p>
</body>
</html>

View File

@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>分治策略进阶与主方法</h1>
<blockquote>
<p>Auto-generated at 2025-09-22 15:44</p>
</blockquote>
<h2>分治策略进阶与主方法</h2>
<h3>最大子数组问题</h3>
<h4>任务:编码与分析</h4>
<h5>场景介绍</h5>
<p>我们的任务是分析每日交通流量的<strong>变化数组</strong>(正数代表流量增加,负数代表减少),并找到哪一个<strong>连续时段</strong>的流量总增量最大。这在算法上被称为“最大子数组问题” 。</p>
<p>例如,对于流量变化数组<code>[13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]</code>总增量最大的连续时段是从第8天到第11天总增量为 <code>18 + 20 - 7 + 12 = 43</code>
虽然这个问题可以通过O(n2) 的暴力法求解,但我们将使用更高效的分治策略。</p>
<h5>题目:最大交通流量增量</h5>
<p>你需要实现一个分治算法来解决最大子数组问题。
同时,为了对比,你也会实现一个暴力求解算法。</p>
<h5>代码框架</h5>
<p>在下方代码编辑区,完成 <code>find_maximum_subarray</code>(分治法)和 <code>find_max_crossing_subarray</code> 以及 <code>find_maximum_subarray_brute</code>(暴力法)三个函数。</p>
<pre><code class="language-python">import time
import random
def find_maximum_subarray_brute(arr):
&quot;&quot;&quot;
使用暴力法求解最大子数组问题
返回: (最大和, 开始索引, 结束索引)
&quot;&quot;&quot;
#--- 本周任务:请在下方实现分治算法 ---
def find_max_crossing_subarray(arr, low, mid, high):
&quot;&quot;&quot;
找到跨越中点的最大子数组
返回: (最大和, 开始索引, 结束索引)
&quot;&quot;&quot;
# TODO: 实现寻找跨越中点的最大子数组的逻辑
# 提示: 从mid向左和向右分别扫描找到各自的最大和
def find_maximum_subarray(arr, low, high):
&quot;&quot;&quot;
使用分治法求解最大子数组问题
返回: (最大和, 开始索引, 结束索引)
&quot;&quot;&quot;
# TODO: 实现分治递归逻辑
# 提示: 递归基是当数组只有一个元素时
#--- 测试与对比部分 --- 以下内容无需修改
traffic_changes = [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]
print(f&quot;交通流量变化数据: {traffic_changes}&quot;)
#使用分治法
max_sum_dc, start_dc, end_dc = find_maximum_subarray(traffic_changes, 0, len(traffic_changes) - 1)
print(f&quot;分治法结果: 最大增量 = {max_sum_dc}, 时段 = Day {start_dc} to Day {end_dc}&quot;)
#使用暴力法验证
max_sum_brute, start_brute, end_brute = find_maximum_subarray_brute(traffic_changes)
print(f&quot;暴力法结果: 最大增量 = {max_sum_brute}, 时段 = Day {start_brute} to Day {end_brute}&quot;)
#性能测试
large_data = [random.randint(-50, 50) for _ in range(2000)]
start_time = time.perf_counter()
find_maximum_subarray(large_data, 0, len(large_data) - 1)
dc_time = (time.perf_counter() - start_time) * 1000
print(f&quot;\n在 n=2000 的数据集上,分治法耗时: {dc_time:.2f} ms&quot;)
start_time = time.perf_counter()
find_maximum_subarray_brute(large_data)
brute_time = (time.perf_counter() - start_time) * 1000
print(f&quot;在 n=2000 的数据集上,暴力法耗时: {brute_time:.2f} ms&quot;)
</code></pre>
<h3>分治经典算法:快速排序</h3>
<h4>快速排序原理</h4>
<p>快速排序Quick Sort是分治法的经典应用之一。与归并排序不同的是快速排序通过原地划分数组来完成排序无需额外的同规模辅助存储空间。它在平均情况下表现极为高效是实际应用中常用的排序算法之一。</p>
<p>快速排序的分治核心思路非常好理解对于一个未排序的数组希望分解为左区间和一个“枢轴pivot”元素和右区间其中左区间的数都小于等于枢轴右区间的都大于等于枢轴。
我们这里用一种固定枢轴法来将序列变成上述的区间情况:
”选取数组第一个元素作为枢轴,用一个移动变量指向数组末尾元素并不断向前移动,直到找到第一个小于枢轴的元素,将该元素与枢轴位置交换,然后从枢轴原位置的下一位个元素开始往后找第一大于的交替,重复上述过程,最终实现以枢轴为界划分出左小右大的区间,再对左右区间递归执行此操作完成排序“</p>
<h4>任务:编码与实验</h4>
<p>现在你需要亲手实现快速排序算法,并通过实验验证其在不同输入情况下的性能差异。</p>
<p>实现 quick_sort(arr) 函数,使用分治策略对传入的数组进行排序。为简单起见,可选择每次固定使用数组的第一个元素作为枢轴,将比枢轴小的元素放在左侧,比枢轴大的放在右侧,然后递归地排序左右子数组。完成实现后,我们将利用性能测试函数对比有序输入(近似最坏情况)和随机输入(平均情况)下算法的运行时间。
代码框架
请在下方代码编辑区完成 quick_sort(arr) 的实现。</p>
<pre><code>import random
import time
def quick_sort(arr):
&quot;&quot;&quot;
实现快速排序算法(固定枢轴策略)
参数arr: 待排序的数组
返回: 排序后的数组
&quot;&quot;&quot;
# TODO: 实现快速排序的分治逻辑
# 提示:选取第一个元素为枢轴,递归排序左右子数组
def measure_performance(sort_func, data):
start_time = time.perf_counter_ns()
sort_func(data.copy()) # 对数据副本排序,避免影响原数据
end_time = time.perf_counter_ns()
return (end_time - start_time) / 10**6 # 转换为毫秒
#性能测试:对比有序输入和随机输入
sizes = [1000, 5000, 10000]
print(&quot;快速排序性能测试(固定枢轴):&quot;)
for n in sizes:
sorted_data = list(range(n))
random_data = [random.randint(0, 10**6) for _ in range(n)]
time_sorted = measure_performance(quick_sort, sorted_data)
time_random = measure_performance(quick_sort, random_data)
print(f&quot;数据规模 n={n}: 有序输入 {time_sorted:.2f} ms, 随机输入 {time_random:.2f} ms&quot;)
</code></pre>
<p>完成编码并运行测试,报告时间差异。</p>
<h3>快速排序的复杂度分析</h3>
<h4>时间复杂度</h4>
<p>基于 “划分 - 递归” 核心逻辑,时间复杂度由<strong>划分效率(枢轴选择)<strong></strong>递归深度</strong>共同决定
三种典型场景:最坏情况、平均情况、最佳情况</p>
<p>最坏情况:每次划分得规模 n-1 和 0 的子数组
平均情况:输入随机 / 等概率选枢轴
最佳情况:每次划分平分数组</p>
<h4>空间复杂度</h4>
<p>快速排序比归并排序更常用的一点在于,快排是“原地的”。</p>
<h3>注入随机进行优化:随机快排与期望复杂度</h3>
<h4>分析</h4>
<p>为了避免固定枢轴选择导致的最坏情况,我们可以引入随机化策略优化快速排序。
随机快排在每次划分时随机选择枢轴,从概率上保证划分均衡,从而将最坏情况表现转化为极低概率事件。
理论上可以证明,随机快排对任何输入的期望时间复杂度为<eq>O(n \log n)</eq>,且大幅降低了出现<eq>O(n^2)</eq>耗时的概率。</p>
<h4>实践</h4>
<pre><code>import random
import time
def random_quick_sort(arr):
&quot;&quot;&quot;
实现随机快速排序算法(随机选择枢轴)
参数arr: 待排序的数组
返回: 排序后的数组
&quot;&quot;&quot;
def quick_sort(arr):
&quot;&quot;&quot;
你在上一章实现的快排内容
&quot;&quot;&quot;
#验证随机快排在极端有序输入下的性能
n = 10000
sorted_data = list(range(n))
time_fixed = measure_performance(quick_sort, sorted_data)
time_random = measure_performance(random_quick_sort, sorted_data)
print(f&quot;数据规模 n={n}: 固定枢轴快排 {time_fixed:.2f} ms, 随机枢轴快排 {time_random:.2f} ms&quot;)
</code></pre>
</body>
</html>

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>动态规划例题训练</h1>
<h2>动态规划例题训练</h2>
<h3>步骤A</h3>
<p>在这里编写该步骤的教学内容(文字/代码/图片链接等)。</p>
<h3>步骤B</h3>
<p>继续补充该阶段的其它步骤内容。</p>
</body>
</html>

View File

@@ -0,0 +1,240 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>动态规划原理</h1>
<h2>动态规划原理</h2>
<h3>自顶而下的分治 vs. 自底向上的动态规划</h3>
<p>分治法:面对规模为 <em>n</em> 的问题,从<strong>顶层</strong>出发,将其拆为若干个更小的子问题,分别求解后<strong>合并</strong></p>
<p>动态规划DP<strong>同一子问题重复出现</strong>时,与其一再从顶层“把问题拆碎”,不如记录子问题的答案,并按规模<strong>从小到大</strong>把所有需要的子问题一次性求出来。
<br></p>
<h4>什么才算“同一”子问题</h4>
<p>与AI教师讨论如何区分“相同规模的子问题”和“同一可复用的子问题”</p>
<h3>切绳Rod Cutting问题</h3>
<p>给定一根长度为 <em>n</em> 的绳子,价格表 <code>price[i]</code> 表示长度为 <em>i</em> 的一段可以卖出的价格。允许将绳子切成多段出售,目标是使总收益最大。</p>
<h4>分治法与时间复杂度</h4>
<p><code>R(n)</code> 为长度 <em>n</em> 的最大收益:</p>
<section>
<eqn> R(0) = 0, R(1)=price[1] </eqn>
</section>
<section>
<eqn> R(n) = max_{1 ≤ i ≤ n} ( price[i] + R(n - i) ) </eqn>
</section>
<p><strong>纯递归分治</strong>会对同一规模的子问题多次求解,子问题规模组合数呈指数级,时间复杂度为 <strong>O(n^n)</strong> 量级。</p>
<h4>记忆化(自顶向下)</h4>
<p>思想:用哈希表/数组 <code>memo[n]</code> 记录 <code>R(n)</code>。当再次需要 <code>R(n)</code> 时,直接返回已存结果,避免重复计算。</p>
<ul>
<li><strong>复杂度</strong>:时间 <strong>O(n^2)</strong>(外层 n内层枚举切第一刀 i空间 <strong>O(n)</strong></li>
</ul>
<p><strong>代码任务 A实现记忆化递归Top-Down</strong></p>
<pre><code class="language-python">def rod_cut_topdown(price: dict[int, int], n: int, memo: list[int]) -&gt; int:
&quot;&quot;&quot;
返回长度 n 的最大收益(记忆化递归)。
TODO:
1) 处理 n==02) 命中 memo 直接返回3) 枚举第一刀长度 i4) 写回 memo[n]。
&quot;&quot;&quot;
pass
</code></pre>
<h4>自底向上(反向记忆化)</h4>
<p>将规模从小到大推进:<code>dp[x]</code> 表示长度 <code>x</code> 的最优收益。
<strong>代码任务 B实现自底向上Bottom-Up</strong></p>
<pre><code class="language-python">def rod_cut_bottomup(price: dict[int, int], n: int) -&gt; int:
&quot;&quot;&quot;
返回长度 n 的最大收益(自底向上)。
TODO:
1) 初始化 dp[0]=02) for x in 1..ndp[x] = max_{1..x}( price[i] + dp[x-i] )
&quot;&quot;&quot;
&quot;&quot;&quot;
以下内容无需修改注意将你实现的rod_cut_topdown代码复制过来
&quot;&quot;&quot;
import time
import random
import signal
from functools import wraps
#超时异常定义
class TimeoutError(Exception):
pass
#超时装饰器
def timeout(seconds):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 定义超时处理函数
def handle_timeout(signum, frame):
raise TimeoutError(f&quot;Function {func.__name__} timed out after {seconds} seconds&quot;)
# 设置信号处理
signal.signal(signal.SIGALRM, handle_timeout)
signal.alarm(seconds) # 触发超时
try:
result = func(*args, **kwargs)
return result
finally:
signal.alarm(0) # 取消超时
return wrapper
return decorator
#带超时的纯递归实现(用于对比)
@timeout(1) # 1秒超时
def rod_cut_recursive(price: dict[int, int], n: int) -&gt; int:
if n == 0:
return 0
max_rev = -float('inf')
for i in range(1, n + 1):
if i in price:
max_rev = max(max_rev, price[i] + rod_cut_recursive(price, n - i))
return max_rev
#对比实验
def compare_algorithms():
# 生成测试用的价格表随机生成1到10的价格
max_length = 50
price = {i: random.randint(1, 10) for i in range(1, max_length + 1)}
print(f&quot;{'n':&lt;5} {'递归(ms)':&lt;10} {'记忆化(ms)':&lt;12} {'自底向上(ms)':&lt;15}&quot;)
print(&quot;-&quot; * 50)
# 测试n从10到50的情况
for n in range(10, 51, 5):
# 纯递归(带超时处理)
try:
start = time.time()
recursive_result = rod_cut_recursive(price, n)
recursive_time = (time.time() - start) * 1000
except TimeoutError:
recursive_result = &quot;超时&quot;
recursive_time = &quot;&gt;1000&quot;
# 记忆化递归
memo = [-1] * (n + 1)
start = time.time()
topdown_result = rod_cut_topdown(price, n, memo)
topdown_time = (time.time() - start) * 1000
# 自底向上
start = time.time()
bottomup_result = rod_cut_bottomup(price, n)
bottomup_time = (time.time() - start) * 1000
# 验证结果一致性(仅当递归未超时)
if isinstance(recursive_result, int):
assert recursive_result == topdown_result == bottomup_result, f&quot;结果不一致 for n={n}&quot;
# 输出结果
print(f&quot;{n:&lt;5} {recursive_time:&lt;10} {topdown_time:&lt;12.4f} {bottomup_time:&lt;15.4f}&quot;)
if __name__ == &quot;__main__&quot;:
compare_algorithms()
</code></pre>
<p>完成上面的代码,讨论实验对比结果。</p>
<h3>动态规划的“组成”:如何写出一个 DP</h3>
<h4><strong>四大组成</strong></h4>
<ul>
<li><strong>状态空间</strong>:用最少的下标(或维度)刻画子问题(如 <code>dp[i]</code><code>dp[i][j]</code>)。</li>
<li><strong>状态转移</strong>:写出“从更小状态到当前状态”的递推/转移式。</li>
<li><strong>边界条件</strong>:初始已知的最小规模答案(如 <code>dp[0]=0</code>)。</li>
<li><strong>解的恢复(部分题目可能不需要)</strong>:若需输出方案/路径,记录子问题选择来源(从那个子问题的答案转移而来)(如 <code>choice[i][j]</code>)。</li>
</ul>
<h4><strong>两大性质</strong></h4>
<ul>
<li><strong>最优子结构</strong>:全局最优由若干子问题的最优解组合而成;</li>
<li><strong>重复子问题</strong>:不同路径会遇到同一个(或等价的)子问题。</li>
</ul>
<h3>例题矩阵连乘Matrix-Chain Multiplication, MCM</h3>
<h4>问题描述</h4>
<p>矩阵乘法有严格的维度匹配要求:只有前一个矩阵的列数 = 后一个矩阵的行数,才能相乘。
即:
矩阵 A维度为 m × k共 m 行、k 列)
矩阵 B维度为 k × n共 k 行、n 列)
矩阵 C = A×B维度为 m × n
产生标量乘法次数为 <code>m×n×k</code>
给定矩阵链 <code>A₁A₂…Aₖ</code>,维度数组 <code>p[0..k]</code> 满足 <code>Aᵢ</code> 大小为 <code>p[i-1] × p[i]</code>。目标:只改变乘法<strong>括号化顺序</strong>,最小化标量乘法次数。</p>
<h4>递推与 DP 表</h4>
<p><code>m[i][j]</code> 表示从 <code>Aᵢ…Aⱼ</code> 的最小乘法次数1-index。则</p>
<section>
<eqn> m[i][i] = 0 </eqn>
</section>
<section>
<eqn> m[i][j] = min_{i ≤ k < j} ( 尝试推导一下这里的转移式 ) </eqn>
</section>
<h4>记录断点</h4>
<p><code>s[i][j]</code> 存储从 <code>Aᵢ…Aⱼ</code> 的最优断点。
<eq>$ s[i][j] = k \text{ if } m[i][j] == 与上面的式子一样 $</eq></p>
<h4>代码任务</h4>
<pre><code class="language-python">def matrix_chain_order(p: list[int]) -&gt; tuple[list[list[int]], list[list[int]]]:
&quot;&quot;&quot;
返回 (m, s)m[i][j] 为最小代价s[i][j] 为最优断点。
TODO: 长度 n = len(p)-1按区间长度 L=2..n 填表。
&quot;&quot;&quot;
...
def print_optimal_parens(s: list[list[int]], i: int, j: int) -&gt; str:
&quot;&quot;&quot;根据断点矩阵 s 输出最优括号化方案&quot;&quot;&quot;
if i == j:
return f&quot;A{i}&quot;
else:
return f&quot;({print_optimal_parens(s, i, s[i][j])}&quot; \
f&quot;{print_optimal_parens(s, s[i][j]+1, j)})&quot;
#测试验证部分
if __name__ == &quot;__main__&quot;:
# 经典测试案例
p = [30, 35, 15, 5, 10, 20, 25]
expected_result = 15125
# 计算最优解
m, s = matrix_chain_order(p)
n = len(p) - 1
result = m[1][n]
# 输出测试结果
print(f&quot;矩阵维度数组: {p}&quot;)
print(f&quot;矩阵数量: {n}&quot;)
print(f&quot;计算得到的最小标量乘法次数: {result}&quot;)
print(f&quot;预期的最小标量乘法次数: {expected_result}&quot;)
print(f&quot;测试{'通过' if result == expected_result else '失败'}&quot;)
print(f&quot;最优括号化方案: {print_optimal_parens(s, 1, n)}&quot;)
# 额外测试案例
p2 = [40, 20, 30, 10, 30]
m2, s2 = matrix_chain_order(p2)
print(&quot;\n第二个测试案例:&quot;)
print(f&quot;矩阵维度数组: {p2}&quot;)
print(f&quot;最小标量乘法次数: {m2[1][4]}&quot;) # 预期结果为 26000
print(f&quot;最优括号化方案: {print_optimal_parens(s2, 1, 4)}&quot;)
</code></pre>
<h4>综合讨论</h4>
<ul>
<li>何时选“记忆化 Top-Down”何时选“自底向上表格法”</li>
<li>如何从“纯递归”快速判断是否值得改造成 DP</li>
</ul>
</body>
</html>

View File

@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>基于比较的排序</h1>
<h2>基于比较的排序</h2>
<h3>优先队列的原理</h3>
<p><strong>优先队列Priority Queue</strong> 是一种抽象数据结构,它支持以最高(或最低)优先级为先进行元素的插入和取出操作。
<br>
在优先队列中,每次检索得到的都是当前权重最大(或最小)的元素。
<br><br>
为了实现优先队列,最直接的思路是维护一个内部元素有序的数组:每次取数就是第一个数,存数时将元素放入合适位置,并调整序列。
<br><br>
<strong>Heap结构</strong>是实现优先队列的首选。</p>
<h3>数组模拟堆实现的优先队列</h3>
<h4>堆结构与取存原理</h4>
<p>Heap本质上是一个完全二叉树结构
(当然也可以是多叉树,但没有必要)
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b73d7df5-018e-4542-b891-b3711c42c56a" alt="图1大根堆的二叉树结构图示" /></p>
<p>这里用“大根堆”为例,从图中可以看到,每一个节点都比它的子节点更大。
既然“优先队列”可以理解为一种特殊的“队列”,那么我们先用堆实现这个队列的出和入:</p>
<h5>取数</h5>
<p>从优先队列中取数,显然堆顶的数就是要的那个最大者。
但是将这个数取出后还不能结束,因为需要维护堆的性质。
<br>
为了维护堆性质,一般通过将末尾的元素放到堆顶,然后将其不断与左右儿子进行替换,直到他比两个儿子都大或儿子不存在为止,称为“下滤”。
<br></p>
<h5>存数</h5>
<p>与取数类似,重要的是维护堆的性质。将新数放到最后(上图中的第一个黑色节点中)后,将这个数进行“上浮”。</p>
<h4>数组模拟堆</h4>
<p>为了实现堆,其实不需要真的写一个二叉树,用数组就可以做到。
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_b4142d35-630c-4d12-a147-d514cca9e0d5" alt="图2左边是堆的数组表示右边是其对应的二叉树" />
如上图,左边是堆的数组表示,右边是其对应的二叉树。</p>
<p>数组下标从1开始任意一个下标i其左右儿子下标刚好就是&quot;i*2&quot;和“i*2+1”。这样在后续代码实现时代码写起来就会简单很多。</p>
<h6>建堆</h6>
<p>用数组模拟堆还有一个好处,就是可以“原地建堆”。
对于一个元素随机的数组只需要O(n)的时间复杂度就可以完成随机数组向堆的转化。</p>
<p>具体做法为“从后向前”遍历,对于每一个非叶子节点,就将其进行“下滤”,这样以它为根的子树就变成一个小堆。往前遍历即可。</p>
<h3>优先队列的算法实现</h3>
<h4>练习:堆操作</h4>
<p>在进行代码实现之前,做一个问题练习:
对于一个随机队列&quot;[3, 32, 6, 43, 5, 8, 0, 9]&quot;,经过一轮反向扫描下滤建大根堆操作,得到的堆的序列是什么?
将答案告知AI教师。</p>
<h4>任务:城市事件优先调度</h4>
<p>现在,让我们通过编程实践来掌握堆排序的实现。假设我们需要对城市中发生的一系列事件按照紧急程度(以数值大小表示优先级)排序,从而依次处理最高优先级的事件。这相当于将一组数字按从大到小排序的过程,与堆排序的机制完全一致。</p>
<p><br><br></p>
<h4>题目:实现堆排序</h4>
<p>请你实现一个堆排序算法 heap_sort(arr),将传入的数组利用堆排序方法排序(从大到小)。
<br>
你可以通过实现max_heapify和build_max_heap等函数来完成这一任务。
<br>
完成编码后,我们将对算法的性能进行测试,比较不同规模输入下堆排序运行时间的增长情况。
<br></p>
<h5>代码框架</h5>
<p>请在下方代码编辑区完成 max_heapify、build_max_heap 和 heap_sort 函数的实现。</p>
<pre><code class="language-python">import random
import time
def max_heapify(arr, n, i):
&quot;&quot;&quot;
维护最大堆性质:假设结点 i 的左右子树已经是最大堆,
调整结点 i 使以其为根的子树成为最大堆
参数:
arr: 存储堆的数组
n: 堆的有效大小(长度)
i: 需要下滤调整的节点索引
&quot;&quot;&quot;
# TODO: 在此处实现 &quot;下滤&quot; 操作,将 arr[i] 下沉到正确位置
def build_max_heap(arr):
&quot;&quot;&quot;
将无序数组原地建成最大堆,从后往前进行下滤
参数:
arr: 待调整的数组
&quot;&quot;&quot;
# TODO: 调用 max_heapify 将 arr 调整为堆
def heap_sort(arr):
&quot;&quot;&quot;
利用堆排序算法排序数组(降序)
参数:
arr: 待排序数组
返回:
排序后的数组(从大到小)
&quot;&quot;&quot;
# TODO: 完成堆排序的实现
n = len(arr)
# 1. 原地建堆
build_max_heap(arr)
# 2. 依次将当前堆顶(最大值)交换到数组末尾,并缩小堆的范围,然后下滤
#性能测试:对比不同规模输入的堆排序用时
def measure(sort_func, data):
start = time.perf_counter_ns()
sort_func(data.copy())
end = time.perf_counter_ns()
return (end - start) / 10**6 # 毫秒
sizes = [1000, 5000, 10000]
print(&quot;堆排序性能测试:&quot;)
for n in sizes:
data = [random.randint(0, 1000000) for _ in range(n)]
t = measure(heap_sort, data)
print(f&quot;数据规模 n={n}: 排序耗时 {t:.2f} ms&quot;)
</code></pre>
<h5>实验结果分析</h5>
<p>请完成并运行上述代码,观察不同输入规模下算法的执行时间。理论上,堆排序的时间复杂度为<eq>O(n \log n)</eq>当输入规模增大时运行时间应呈现近似线性乘以对数的增长趋势。具体来说若将输入规模扩大10倍运行时间将增加约<eq>10 \times \log_2(10) \approx 10 \times 3.3 \approx 33</eq>倍左右。
<br>
相比之下,简单的<eq>O(n^2)</eq>排序算法在相同扩大量级下耗时会增加约100倍。通过与之前插入排序实验的对比你会发现堆排序对规模扩大的响应增长显著缓慢得多。
<br>
这印证了堆排序的效率优势:在最坏情况和平均情况下它都能维持<eq>O(n \log n)</eq>的性能,不会出现如快速排序在极端情况下退化为<eq>O(n^2)</eq>的尴尬局面。
<br>
此外,堆排序是一种原地排序(只需要常数级别的额外空间),这也是相对于归并排序的一个优势。综合来看,利用优先队列实现的堆排序在效率和空间上都表现出色,是一种成熟可靠的排序方法。</p>
<h3>比较排序的决策树模型</h3>
<p>前面的内容介绍了多种基于元素比较的排序算法(比较排序),包括快速排序、堆排序等。接下来,我们讨论一个重要的理论结果:
<br>
在比较模型下,任意排序算法的最优时间复杂度下界为<eq>Ω(n \log n)</eq>
这个结论意味着,无论设计何种巧妙的比较排序算法,都无法突破这一定义上的效率极限。证明这一点的经典工具是决策树模型。</p>
<p>决策树是描述比较排序过程的一种抽象模型。
在排序过程中,每进行一次比较(例如“<eq>A[i] \le A[j]</eq>?”)就相当于根据结果(二叉决策:是/否)将可能的输入情况划分到两个分支。
<br>
整个排序算法的运行过程可以被看作是在这样一棵决策树上从根节点走向某个叶节点的过程。决策树的每个叶节点对应一种可能的输入集合及其确定的输出顺序。
<br>
当有<eq>n</eq>个待排序元素时,假设它们两两各不相同,则可能的输入排列情况共有<eq>n!种(所有元素的全排列)。为了正确地将每种输入排列映射到唯一的输出(即排好序的有序序列),排序算法的决策树必须至少具备</eq>n!个叶节点——每个叶子对应一种输入排列的判别结果。</p>
<p>对于一棵二叉决策树,若包含<eq>L</eq>个叶节点,其高度<eq>h</eq>满足<eq>L \le 2^h</eq>,因此<eq>h \ge \lceil \log_2 L \rceil</eq>。在排序问题中,<eq>L</eq>最少取<eq>n!</eq>,因此最优情况下决策树高度也满足:
<br>
<eq>$h_{\min} \geq \lceil \log_2(n!) \rceil.</eq>$
利用对数运算的性质,可以估计<eq>\log_2(n!)</eq>的数量级。根据斯特林公式近似,<eq>n!</eq>大约为<eq>(n/e)^n</eq>的数量级,那么:
<br>
<eq>$\log_2(n!) \approx \log_2\left((n/e)^n\sqrt{2\pi n}\right) = n\log_2 n - n\log_2 e + O(\log n).</eq>$
<br>
可以看出,当<eq>n</eq>较大时,<eq>\log_2(n!) = Θ(n \log n)</eq>。这意味着决策树的高度下界<eq>h_{\min} = Ω(n \log n)</eq>。换言之,任何基于比较的排序算法在最理想情况下也需要执行与<eq>n \log n</eq>同数量级的比较操作。
<br>
例如,对于$ n=3$的简单情况,<eq>3! = 6</eq>,满足<eq>2^2 < 6 < 2^3</eq>因此判定3个元素的任意排列需要至少3次比较。这与我们已知的事实相符对三个无任何特殊性质的数进行排序最少需要3次比较才能确定它们的正确顺序。</p>
</body>
</html>

View File

@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>二分查找</h1>
<h2>二分查找</h2>
<h3>二分查找的原理</h3>
<p>二分查找是一个基础但很重要的知识点,也是一种特殊的分治,为以后许多高级的数据结构与算法铺垫。</p>
<p>下面是一个用二分的简单场景:</p>
<p>假设小明从0到1000之间选择了一个数字但不告诉你你可以不断猜测这个数每次猜测小明会告知你的猜测得过大还是过小问最多几次就一定能猜中</p>
<p>答案是利用二分查找的原理猜测11次即可。</p>
<ol>
<li>对于0到1000的答案备选区猜测中位数500假设过小</li>
<li>则对于501到1000的答案备选区猜测750假设过大</li>
<li>则对于501到749的答案备选区猜测625假设过小</li>
<li>则对于626到749区间......</li>
<li>688-749</li>
<li>718-749</li>
<li>734-749</li>
<li>742-749</li>
<li>746-749</li>
<li>748-749</li>
<li>749-749</li>
</ol>
<p>在最差的情况下第11次的答案备选区就一定长度为1了也就是必然是答案。</p>
<p>因此如果序列是有序的,就可以通过二分查找快速定位所需要的数据。</p>
<h4>例题</h4>
<p>对于上面那个题目如果问题区间是1到4000最差情况下需要猜测几次这个值可以怎么迅速地算出来你可以用时间复杂度的公式建立一下并推导一下么</p>
<h3>练习:二分查找</h3>
<p>试试对于下面的题目,用代码实现一下二分查找。</p>
<h4>题目:有序数组寻址</h4>
<p>给出一个长度为n的有序数组从小到大有q次询问对于每次询问输出指定数在数组中的下标。如果不存在则输出-1。</p>
<h5>输入</h5>
<p>第一行一个整数n。(1&lt;=n&lt;=10^5)</p>
<p>第二行n个用空格分开的整数ai。(0&lt;=ai&lt;=10^8)</p>
<p>第三行一个整数q表示询问的次数。(1&lt;=q&lt;=10^4)</p>
<p>后q行每行一个整数b表示询问的数。(0&lt;=b&lt;=10^8)</p>
<h5>输出</h5>
<p>q行每行一个整数对应每次询问的返回结果</p>
<h5>提示:</h5>
<p>完成代码后通知Agent进行评测。</p>
<p>如果你还不完全会这个算法询问Agent获取提示并进行学习。</p>
<h3>二维二分查找</h3>
<h4>二分查找的直觉</h4>
<p>通过之前的原理和实现,二分查找本质上是通过取中的方式,尽可能排除多(一半)的备选数。
<br>
为进一步理解,除了序列,本节我们来尝试一下在矩阵(二维数组)上进行分治和查找。</p>
<h4>问题建模</h4>
<p>给出一个n*n的矩阵其中每一行都是一个从小到大的序列每一列都是从小到大的序列。从中找到指定的一个数target。</p>
<h5></h5>
<p>[
[1, 2, 4, 5]
[2, 5, 7, 11]
[3, 8, 10, 12]
[4, 10, 17, 20]
]
从中找到&quot;8&quot;这个数。</p>
<h5>线性做法</h5>
<p>这里先介绍线性做法。
一维序列的线性做法就是逐个比对一下,
二维做法最差是逐个扫描n*n所有的数可以聪明一些降低到线性成本称为“楼梯式”查找</p>
<ol>
<li>从右上角看一个元素 x = M[r][c]</li>
<li>查找与排除
a. 若 x &gt; target则这一列里 x 下方都 ≥ x更不可能是 target所以安全地左移排除一整列
b. 若 x &lt; target则这一行里 x 左边都 ≤ x更不可能是 target所以安全地下移排除一整行</li>
<li>持续直到排除所有行和列,从而找到目标元素或告知找不到。</li>
</ol>
<p>做法正确性分析,时间复杂度分析。</p>
<h4>分治做法:二维二分</h4>
<p>接下来试着通过二分的技术,找找复杂度更低的做法。
试着回答这些问题并与AI教师进行讨论</p>
<ol>
<li>分解中点在哪里?</li>
<li>二分后排除掉的部分是哪些?</li>
<li>如何划分子问题?</li>
<li>时间复杂度是多少?</li>
</ol>
<h3>二维二分查找实现</h3>
<h4>题目:有序矩阵查找</h4>
<p>给出一个 n×n 的矩阵,其中每一行的元素都按照从小到大的顺序排列,每一列的元素也都按照从小到大的顺序排列。现需要判断指定的数 target 是否在该矩阵中,若存在则输出其所在的行下标和列下标(行和列均从 0 开始计数);若不存在则输出 - 1 -1。</p>
<h4>输入</h4>
<p>第一行一个整数 n。(1&lt;=n&lt;=10^3)
接下来 n 行,每行 n 个用空格分开的整数,表示矩阵的元素。
最后一行一个整数 target表示需要查找的数。</p>
<h4>输出</h4>
<p>一行两个整数,分别表示 target 所在的行下标和列下标,中间用空格隔开。若不存在则输出 - 1 -1。</p>
<h4>示例</h4>
<p>输入41 2 4 52 5 7 113 8 10 124 10 17 208
输出2 1</p>
</body>
</html>

View File

@@ -0,0 +1,198 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- 你的其他样式 -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']]
},
svg: {
fontCache: 'global'
}
};
</script>
</head>
<body>
<h1>分位统计量</h1>
<h2>分位统计量</h2>
<h3>第K大的数</h3>
<p>在许多算法与数据分析任务中,我们不仅需要排序整个数组,还需要找出某个特定顺序统计量,例如:
• 一个数组中第 K 大的元素;
• 所有元素的中位数K = n/2
• 某个分位点25%、75%等)…
<br>
最直接的办法是先对数组进行排序,然后取第 K 个位置的元素。这种方法的时间复杂度为 <eq>O(n \log n)</eq>,因为我们需要对所有元素进行排序。
<br>
但我们真正关心的,只是一个位置的元素 —— 我们是否有更快的方法?
<br></p>
<h4>快速选择QuickSelect</h4>
<p><strong>快排中的“轴枢”启发</strong>
我们在快速排序中,每次选一个枢轴 pivot通过划分操作把数组分成两部分
• 左边元素 &lt;= pivot
• 右边元素 &gt;= pivot。
最终 pivot 会被放置固定位置上。
<br>
可见:</p>
<blockquote>
<p>若 pivot 恰好是我们要找的第 K 大元素,就不必继续递归。</p>
</blockquote>
<br>
因此可得快速选择的具体做法先令target等于N-K即找第N-K+1小的数其下标为target
1. 随机选一个枢轴;
2. 利用快速排序的 partition 划分函数,找出 pivot 的位置 idx
3. 比较该位置与目标索引:
• 若 idx == target返回 pivot
• 若 idx > target在左半边递归查找
• 否则在右半边递归查找。
<br>
理解做法并分析时间复杂度。
### 快速选择的实现
<p>实现一个 <code>quickselect(arr, k)</code> 函数,返回第 K 大的元素
也就是增序列下标N-K位置的数</p>
<pre><code class="language-python">import random
def partition(arr, low, high):
&quot;&quot;&quot;
对arr序列从low到high下标找到一个枢轴并返回其下标。在arr中比枢轴小的数都在idx左边大的在右边
&quot;&quot;&quot;
def quickselect(arr, k):
&quot;&quot;&quot;
快速选择第K大元素转换为第n-k小
:param arr: 输入数组
:param k: 要找的第K大元素
:return: 该元素值
&quot;&quot;&quot;
n = len(arr)
target = n - k # 第k大转为第n-k小序列下标
low, high = 0, n - 1
while low &lt;= high:
idx = partition(arr, low, high)
if idx == target:
return arr[idx]
elif idx &lt; target:
low = idx + 1
else:
high = idx - 1
if __name__ == &quot;__main__&quot;:
#测试1随机数组
random.seed(42) # 设置随机种子,确保结果可重现
arr1 = [random.randint(1, 100) for _ in range(10)]
k1 = 3
result1 = quickselect(arr1.copy(), k1) # 使用副本避免修改原数组
sorted_arr1 = sorted(arr1)
expected1 = sorted_arr1[-k1]
print(f&quot;测试1 - 随机数组:&quot;)
print(f&quot;原数组: {arr1}&quot;)
print(f&quot;第{k1}大元素: {result1}, 预期值: {expected1}&quot;)
print(f&quot;测试{'通过' if result1 == expected1 else '失败'}\n&quot;)
#测试2包含重复元素的数组
arr2 = [5, 3, 8, 8, 1, 5, 9, 3, 5]
k2 = 2
result2 = quickselect(arr2.copy(), k2)
sorted_arr2 = sorted(arr2)
expected2 = sorted_arr2[-k2]
print(f&quot;测试2 - 包含重复元素:&quot;)
print(f&quot;原数组: {arr2}&quot;)
print(f&quot;第{k2}大元素: {result2}, 预期值: {expected2}&quot;)
print(f&quot;测试{'通过' if result2 == expected2 else '失败'}\n&quot;)
#测试3边界条件 - k=1最大元素
arr3 = [10, 20, 30, 40, 50]
k3 = 1
result3 = quickselect(arr3.copy(), k3)
sorted_arr3 = sorted(arr3)
expected3 = sorted_arr3[-k3]
print(f&quot;测试3 - 最大元素:&quot;)
print(f&quot;原数组: {arr3}&quot;)
print(f&quot;第{k3}大元素: {result3}, 预期值: {expected3}&quot;)
print(f&quot;测试{'通过' if result3 == expected3 else '失败'}\n&quot;)
</code></pre>
<h3>BFPRT 算法:中位数的中位数算法</h3>
<p>快速选择的平均复杂度为 <eq>O(n)</eq>,但可能退化为 <eq>O(n^2)</eq></p>
<h4>BFPRT分治法</h4>
<p>BFPRT算法和二分查找中对“矩阵”进行分治的做法很像也是通过矩阵的中心数来排除一部分数据。
<img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_4d626b28-8e7e-4c19-8238-daaaba079a5f" alt="image" />
<br>
我们通过以下几个步骤确保选出的“轴枢”可以高概率地“削减”足够多的元素,从而保证递归过程每次至少减少 30% 左右的输入规模:
<br></p>
<ol>
<li>将原始数组划分为若干个 5 个元素的组
请看图中所有的竖列(粉红色背景小矩形):每个小矩形内有 5 个点,这就是我们将原始数据分成的小组。
组的个数 ≈ n / 5设 n 是总元素数量)
不足 5 个的组,可以用一个全局最小值(如 -1补全。
<br></li>
<li>对每组内部排序,取出中位数
图中每个竖列的小组中,绿色圆点表示每组的元素。中间处于红框中的数为每组中位数。
每列上两个数小于中位数,下两个大于。
<br></li>
<li>将所有组的中位数组成新数组,对其递归调用本算法找中位数。
图中红色矩形框住的内容,正是“所有组中位数”的集合。这些点来自所有小组的中位点,组成新的长度为 n/5 的数组。
对这个新数组再次使用本算法,递归找出它的中位数,作为我们最终要用的“枢轴”。即图中棕色点。
<br></li>
<li>这个中位数(棕色圆点)就是我们分治的“枢轴”
有如下重要性质:
a. 棕点及左上角(图中蓝框的点)均小于等于枢轴
b. 棕点及右下角(图中黑框的点)均大于等于枢轴
<br></li>
<li>进行排除分解先令x=n-k+1找第k大数就是找第x小数
a. 若x&gt;蓝框数数量则在非蓝框数中找第k大
b. 若k&gt;黑框数数量则在非黑框数中找第k-黑框数数量)大
c. 当k=1或n'n'是当前子问题的数数量或n'&lt;=5均可在不大于O(n')的用时完成找数。
可以证明在c不成立的情况下a,b两点至少成立一个。
<br></li>
</ol>
<p>与AI教师讨论每一步在做的事和最终想要实现的效果。可以联想你之前学习到的二分查找中的矩阵分治。</p>
<h3>BFPRT 复杂度分析</h3>
<p>在理解了“中位数的中位数”算法的流程之后,我们接下来要正式分析它的<strong>时间复杂度</strong>,并证明该算法的整体运行时间为 <eq>O(n)</eq></p>
<hr />
<h4>写出递推式</h4>
<p>根据 BFPRT 算法的执行过程依次与AI教师讨论每步的时间复杂度
<br></p>
<ol>
<li><strong><eq>n</eq> 个元素分组、找出每组中位数</strong>
<ul>
<li>分成 <eq>n/5</eq> 组,每组排序并取中位数。
<br></li>
</ul>
</li>
<li><strong>递归地找所有中位数的中位数(枢轴)</strong>
<ul>
<li>子问题规模为 <eq>n/5</eq>
<br></li>
</ul>
</li>
<li><strong>额外一次线性 partition</strong>
<ul>
<li>对全数组按枢轴划分:蓝框部分、黑框部分,此外递归时是对非蓝框或非黑框的补集部分。
<br></li>
</ul>
</li>
<li><strong>将原始数组按枢轴划分,并在一侧继续递归</strong>
<ul>
<li>对非蓝框或非黑框数进行递归查询。
<br></li>
</ul>
</li>
</ol>
<p><img src="https://hsamooc-cdn-1374354408.cos.ap-guangzhou.myqcloud.com/image_4d626b28-8e7e-4c19-8238-daaaba079a5f" alt="image" /></p>
<p>从而得到最终的递推式。</p>
<hr />
<h4>证明 <eq>T(n) = O(n)</eq></h4>
<p>这里的证明并不复杂,使用高中学习的数学归纳法即可。
提示先假设满足T(n)&lt;=cn然后证明递推式&lt;=d*cn&lt;=cn其中d小于1。</p>
<p>与AI教师探讨证明流程。</p>
</body>
</html>

6
Html/apps/static/cdnback/all.min.css vendored Normal file

File diff suppressed because one or more lines are too long

3
Html/apps/static/cdnback/axios.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,327 @@
/*!
* Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)
* Copyright 2011-2019 The Bootstrap Authors
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
[tabindex="-1"]:focus:not(:focus-visible) {
outline: 0 !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]) {
color: inherit;
text-decoration: none;
}
a:not([href]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg {
overflow: hidden;
vertical-align: middle;
}
table {
border-collapse: collapse;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #6c757d;
text-align: left;
caption-side: bottom;
}
th {
text-align: inherit;
}
label {
display: inline-block;
margin-bottom: 0.5rem;
}
button {
border-radius: 0;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
select {
word-wrap: normal;
}
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button:not(:disabled),
[type="button"]:not(:disabled),
[type="reset"]:not(:disabled),
[type="submit"]:not(:disabled) {
cursor: pointer;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea {
overflow: auto;
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
max-width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
color: inherit;
white-space: normal;
}
progress {
vertical-align: baseline;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
outline-offset: -2px;
-webkit-appearance: none;
}
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
summary {
display: list-item;
cursor: pointer;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)
* Copyright 2011-2019 The Bootstrap Authors
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

10224
Html/apps/static/cdnback/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,218 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/
/**
* Default styles for xterm.js
*/
.xterm {
cursor: text;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 5;
}
.xterm .xterm-helper-textarea {
padding: 0;
border: 0;
margin: 0;
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -5;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm.xterm-cursor-pointer,
.xterm .xterm-cursor-pointer {
cursor: pointer;
}
.xterm.column-select.focus {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility:not(.debug),
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
color: transparent;
pointer-events: none;
}
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
color: transparent;
}
.xterm .xterm-accessibility-tree {
user-select: text;
white-space: pre;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.xterm-dim {
/* Dim should not apply to background, so the opacity of the foreground color is applied
* explicitly in the generated class and reset to 1 here */
opacity: 1 !important;
}
.xterm-underline-1 { text-decoration: underline; }
.xterm-underline-2 { text-decoration: double underline; }
.xterm-underline-3 { text-decoration: wavy underline; }
.xterm-underline-4 { text-decoration: dotted underline; }
.xterm-underline-5 { text-decoration: dashed underline; }
.xterm-overline {
text-decoration: overline;
}
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
.xterm-strikethrough {
text-decoration: line-through;
}
.xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6;
position: absolute;
}
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
z-index: 7;
}
.xterm-decoration-overview-ruler {
z-index: 8;
position: absolute;
top: 0;
right: 0;
pointer-events: none;
}
.xterm-decoration-top {
z-index: 2;
position: relative;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,51 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fit = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function proposeGeometry(term) {
if (!term.element.parentElement) {
return null;
}
var parentElementStyle = window.getComputedStyle(term.element.parentElement);
var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
var elementStyle = window.getComputedStyle(term.element);
var elementPadding = {
top: parseInt(elementStyle.getPropertyValue('padding-top')),
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
right: parseInt(elementStyle.getPropertyValue('padding-right')),
left: parseInt(elementStyle.getPropertyValue('padding-left'))
};
var elementPaddingVer = elementPadding.top + elementPadding.bottom;
var elementPaddingHor = elementPadding.right + elementPadding.left;
var availableHeight = parentElementHeight - elementPaddingVer;
var availableWidth = parentElementWidth - elementPaddingHor - term._core.viewport.scrollBarWidth;
var geometry = {
cols: Math.floor(availableWidth / term._core.renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / term._core.renderer.dimensions.actualCellHeight)
};
return geometry;
}
exports.proposeGeometry = proposeGeometry;
function fit(term) {
var geometry = proposeGeometry(term);
if (geometry) {
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
term._core.renderer.clear();
term.resize(geometry.cols, geometry.rows);
}
}
}
exports.fit = fit;
function apply(terminalConstructor) {
terminalConstructor.prototype.proposeGeometry = function () {
return proposeGeometry(this);
};
terminalConstructor.prototype.fit = function () {
fit(this);
};
}
exports.apply = apply;
},{}]},{},[1])(1)
});
//# sourceMappingURL=fit.js.map

View File

@@ -0,0 +1,27 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fullscreen = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function toggleFullScreen(term, fullscreen) {
var fn;
if (typeof fullscreen === 'undefined') {
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
}
else if (!fullscreen) {
fn = 'remove';
}
else {
fn = 'add';
}
term.element.classList[fn]('fullscreen');
}
exports.toggleFullScreen = toggleFullScreen;
function apply(terminalConstructor) {
terminalConstructor.prototype.toggleFullScreen = function (fullscreen) {
toggleFullScreen(this, fullscreen);
};
}
exports.apply = apply;
},{}]},{},[1])(1)
});
//# sourceMappingURL=fullscreen.js.map

View File

@@ -0,0 +1,126 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.search = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var SearchHelper = (function () {
function SearchHelper(_terminal) {
this._terminal = _terminal;
}
SearchHelper.prototype.findNext = function (term) {
if (!term || term.length === 0) {
return false;
}
var result;
var startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionEnd) {
startRow = this._terminal._core.selectionManager.selectionEnd[1];
}
for (var y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
if (!result) {
for (var y = 0; y < startRow; y++) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
}
return this._selectResult(result);
};
SearchHelper.prototype.findPrevious = function (term) {
if (!term || term.length === 0) {
return false;
}
var result;
var startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionStart) {
startRow = this._terminal._core.selectionManager.selectionStart[1];
}
for (var y = startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
if (!result) {
for (var y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
}
return this._selectResult(result);
};
SearchHelper.prototype._findInLine = function (term, y) {
var lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();
var lowerTerm = term.toLowerCase();
var searchIndex = lowerStringLine.indexOf(lowerTerm);
if (searchIndex >= 0) {
var line = this._terminal._core.buffer.lines.get(y);
for (var i = 0; i < searchIndex; i++) {
var charData = line[i];
var char = charData[1];
if (char.length > 1) {
searchIndex -= char.length - 1;
}
var charWidth = charData[2];
if (charWidth === 0) {
searchIndex++;
}
}
return {
term: term,
col: searchIndex,
row: y
};
}
};
SearchHelper.prototype._selectResult = function (result) {
if (!result) {
return false;
}
this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp);
return true;
};
return SearchHelper;
}());
exports.SearchHelper = SearchHelper;
},{}],2:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var SearchHelper_1 = require("./SearchHelper");
function findNext(terminal, term) {
var addonTerminal = terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper_1.SearchHelper(addonTerminal);
}
return addonTerminal.__searchHelper.findNext(term);
}
exports.findNext = findNext;
function findPrevious(terminal, term) {
var addonTerminal = terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper_1.SearchHelper(addonTerminal);
}
return addonTerminal.__searchHelper.findPrevious(term);
}
exports.findPrevious = findPrevious;
function apply(terminalConstructor) {
terminalConstructor.prototype.findNext = function (term) {
return findNext(this, term);
};
terminalConstructor.prototype.findPrevious = function (term) {
return findPrevious(this, term);
};
}
exports.apply = apply;
},{"./SearchHelper":1}]},{},[2])(2)
});
//# sourceMappingURL=search.js.map

View File

@@ -0,0 +1,41 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.webLinks = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var protocolClause = '(https?:\\/\\/)';
var domainCharacterSet = '[\\da-z\\.-]+';
var negatedDomainCharacterSet = '[^\\da-z\\.-]+';
var domainBodyClause = '(' + domainCharacterSet + ')';
var tldClause = '([a-z\\.]{2,6})';
var ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
var localHostClause = '(localhost)';
var portClause = '(:\\d{1,5})';
var hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
var pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
var queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
var queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
var hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
var negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
var bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
var start = '(?:^|' + negatedDomainCharacterSet + ')(';
var end = ')($|' + negatedPathCharacterSet + ')';
var strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
function handleLink(event, uri) {
window.open(uri, '_blank');
}
function webLinksInit(term, handler, options) {
if (handler === void 0) { handler = handleLink; }
if (options === void 0) { options = {}; }
options.matchIndex = 1;
term.registerLinkMatcher(strictUrlRegex, handler, options);
}
exports.webLinksInit = webLinksInit;
function apply(terminalConstructor) {
terminalConstructor.prototype.webLinksInit = function (handler, options) {
webLinksInit(this, handler, options);
};
}
exports.apply = apply;
},{}]},{},[1])(1)
});
//# sourceMappingURL=webLinks.js.map

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4521
Html/apps/static/cdnback/js/bootstrap.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
Html/apps/static/cdnback/split.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -336,18 +336,6 @@ body {
margin-left: 0.75rem;
}
/* 搜索区域 */
.search-section {
padding: 0;
}
.search-container {
background-color: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: var(--card-shadow);
}
/* 课程卡片样式 */
.course-card {
border: none;

View File

@@ -107,13 +107,7 @@
.btn-group-toggle .btn {
padding: 5px 10px;
}
#leftSidebar, .vscode-web, #rightSidebar {
height: 100%;
overflow: auto;
}
/* 主容器,使用 grid 布局 */
/* 主容器,使用 grid 布局 */
#maxcontainer {
display: grid;
grid-template-columns: 2fr 5px 3fr 5px 2fr; /* 默认宽度比例 */
@@ -122,7 +116,20 @@
/* 各区域基础样式 */
.sidebar {
overflow: auto; /* 确保内容可以滚动 */
display: flex;
flex-direction: column;
overflow: hidden; /* 隐藏溢出内容,让内部元素单独滚动 */
}
/* 左侧边栏样式 */
#leftSidebar {
height: 100%;
}
/* 中间和右侧边栏样式 */
.vscode-web, #rightSidebar {
height: 100%;
overflow: auto;
}
.vscode-web {
@@ -162,55 +169,75 @@
/* Markdown内容区域样式 */
#markdown-content {
flex: 1; /* 占满剩余空间 */
overflow-y: auto; /* 只有内容区域可以垂直滚动 */
overflow-x: hidden; /* 隐藏水平滚动条 */
}
/* 设置进度条容器样式 */
#markdown-content-process {
display: flex;
align-items: flex-end; /* 让进度条靠底部对齐 */
padding: 0 10px 10px 10px; /* 添加底部内边距,调整与底部的距离 */
box-sizing: border-box;
flex-shrink: 0; /* 防止进度条被压缩 */
}
/* 设置每个子框的基本样式 */
.progress-box {
height: 100%; /* 度为 100%,撑满容器 */
width: 100%; /* 度为 100%,撑满容器 */
display: flex;
flex-direction: row; /* 使子框横向排列 */
justify-content: flex-start;
gap: 2px; /* 进度条之间的间距 */
margin-bottom: 0; /* 移除底部边距 */
}
.progress-title {
height: 60px;
text-align: center;
line-height: 60px;
margin-right: 2px;
border: 1px solid #ddd;
transition: background-color 0.3s ease;
height: 8px; /* 更细的进度条 */
flex: 1; /* 均匀分配宽度 */
border-radius: 4px; /* 圆角设计,更扁平化 */
transition: all 0.3s ease; /* 平滑过渡效果 */
position: relative;
/* 新增以下属性实现文字溢出省略 */
white-space: nowrap; /* 防止文字换行 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 溢出部分显示为省略号 */
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.1); /* 轻微阴影,增加层次感 */
overflow: visible; /* 允许文字溢出 */
}
/* 颜色方案:已学习为蓝色,当前学习为绿色,未学习为白色 */
.blue {
background-color: #007bff; /* 蓝色 - 已学习 */
}
.green {
background-color: green;
color: white;
background-color: #28a745; /* 绿色 - 当前学习 */
}
.white {
background-color: white;
color: black;
background-color: #e9ecef; /* 浅灰色 - 未学习,比纯白色更美观 */
}
.progress-box > .progress-title:last-child {
margin-bottom: 0; /* 防止最后一个子框出现多余的间距 */
/* 进度条悬停效果 */
.progress-title:hover {
transform: translateY(-1px); /* 轻微上浮效果 */
box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* 增强阴影 */
}
/* 进度条详细信息的样式 */
#progress-detail {
position: absolute;
top: 0;
left: 0;
display: none;
padding: 10px;
background-color: rgba(0, 0, 0, 0.7);
background-color: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 5px;
border-radius: 6px;
max-width: 200px;
font-size: 14px;
z-index: 1000;
pointer-events: none; /* 防止干扰鼠标事件 */
box-shadow: 0 2px 10px rgba(0,0,0,0.3); /* 增强阴影 */
}
.button {

View File

@@ -0,0 +1,51 @@
/* HelpTip 组件样式 */
/* 问号图标样式 */
.help-icon {
display: inline-block;
margin-left: 8px;
color: #888;
font-weight: bold;
cursor: help;
font-size: 14px;
line-height: 1;
transition: color 0.2s ease;
}
.help-icon:hover {
color: #4CAF50;
}
/* 全局提示框样式(挂在 body 下) */
.tooltip-portal {
position: fixed; /* 不受父级 overflow 影响 */
left: 0;
top: 0;
z-index: 999999; /* 足够高,覆盖所有元素 */
display: none;
opacity: 0;
transition: opacity 0.08s linear;
background: rgba(0, 0, 0, 0.9);
color: #fff;
padding: 6px 10px;
border-radius: 6px;
font-size: 13px;
line-height: 1.4;
pointer-events: none; /* 鼠标穿透,避免闪烁 */
white-space: pre-wrap; /* 支持换行符 */
max-width: 300px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
word-wrap: break-word;
}
/* 提示框显示状态 */
.tooltip-portal.show {
display: block;
opacity: 1;
}
/* 提示框动画效果 */
.tooltip-portal.fade {
transition: opacity 0.2s ease;
}

View File

@@ -19,19 +19,9 @@ body {
}
main {
flex: 1;
padding: 0 20px;
}
/* 搜索区域 */
.search-section {
padding: 0;
}
.search-container {
background-color: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: var(--card-shadow);
}
/* Header styles */
header {
@@ -88,25 +78,9 @@ main {
gap: 20px;
}
.course-card {
background-color: white;
display: flex;
flex-direction: column;
border-radius: 8px;
width: 300px;
height: 400px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: transform 0.3s ease;
margin: 0px;
}
.select-button {
display: block;
width: 100%;
padding: 10px;
background-color: #2575fc;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
@@ -115,11 +89,6 @@ main {
.course-card:hover {
transform: translateY(-10px); /* 提升效果 */
}
.course-card img {
width: 100%;
height: 261px;
object-fit: cover;
}
.course-card h3 {
padding: 15px;
@@ -139,9 +108,6 @@ main {
}
.selected-button {
display: block;
width: 100%;
padding: 10px;
background-color: #8a8b8c;
color: white;
border: none;

View File

@@ -1,35 +1,3 @@
/* 问号图标 */
.help-icon {
display: inline-block;
margin-left: 8px;
color: #888;
font-weight: bold;
cursor: help;
}
.help-icon:hover { color: #4CAF50; }
/* Portal Tooltip挂在 body 下) */
.tooltip-portal {
position: fixed; /* 不受父级 overflow 影响 */
left: 0; top: 0;
z-index: 999999; /* 足够高,覆盖侧栏等 */
display: none;
opacity: 0;
transition: opacity .08s linear;
background: rgba(0,0,0,.9);
color: #fff;
padding: 6px 10px;
border-radius: 6px;
font-size: 13px;
line-height: 1.4;
pointer-events: none; /* 鼠标穿透,避免闪烁 */
white-space: nowrap;
box-shadow: 0 4px 12px rgba(0,0,0,.25);
}
.add-step-btn, .add-phase-btn {
background-color: #4CAF50;
color: white;
@@ -125,7 +93,7 @@
.tab-btn.active { background:#f0f8ff; border-color:#a0c4ff; }
.actions { margin-left:auto; }
.actions button { padding:6px 12px; border:none; border-radius:8px; background:#4CAF50; color:#fff; cursor:pointer; }
.tab-content { flex:1; padding:8px; }
.tab-content { flex:1; padding:8px;display: flex; }
.tab { display:none; height:100%; }
.tab.active { display:block; height:100%; }
textarea {

View File

@@ -0,0 +1,63 @@
/* 让左侧编辑区真正占满并允许内部滚动 */
.tab-content {
height: calc(100vh - 220px);
/* 不要隐藏滚动:*/
overflow: visible; /* 或者干脆删掉 overflow 这行 */
}
/* EasyMDE 容器按列铺开,编辑器本体撑满剩余空间 */
.EasyMDEContainer {
height: 100%;
display: flex;
flex-direction: column;
}
/* 关键:让 CodeMirror 吃满容器高度 */
.EasyMDEContainer .CodeMirror {
height: 100% !important;
}
/* 关键:滚动发生在 CodeMirror-scroll 里 */
.EasyMDEContainer .CodeMirror-scroll {
height: 100% !important;
overflow: auto !important; /* 启用内部滚动条(也支持鼠标滚轮) */
}
/* 右侧预览保持原样独立滚动 */
.md-preview-pane {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
background: #fff;
height: 100%;
overflow: auto;
}
.md-preview-pane h1, .md-preview-pane h2, .md-preview-pane h3 {
border-bottom: 1px solid #f1f5f9;
padding-bottom: .3em;
margin-top: 1.2em;
}
.md-preview-pane img {
max-width: 100%;
height: auto;
}
/* 只影响右侧 Markdown 预览里的表格 */
.md-preview-pane .table {
/* 如果你想完全摆脱 bootstrap.table 的影响 */
all: revert; /* 或者 all: unset; 视需求选择 */
display: table;
border-collapse: collapse;
/* 下面写你自己的表格样式 */
border: 1px solid #e5e7eb;
}
.md-preview-pane .table th,
.md-preview-pane .table td {
border: 1px solid #e5e7eb;
padding: 8px 10px;
}
.md-preview-pane .table thead th {
background: #f8fafc;
font-weight: 600;
}

View File

@@ -3,13 +3,28 @@
background: linear-gradient(135deg, var(--primary-blue), var(--accent-blue));
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 0.8rem 1rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.navbar .logo h1 {
color: rgb(245, 245, 245);
font-size: 24px;
font-size: 18px;
font-weight: bold;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
}
.navbar-links {
display: flex;
justify-content: center;
flex: 1;
margin: 0 20px;
}
.navbar-links a {
@@ -69,4 +84,41 @@
/* 下拉浮框 hover 显示 */
.dropdown:hover .dropdown-content {
display: block;
}
/* 响应式设计 */
@media (max-width: 768px) {
.navbar {
flex-direction: column;
gap: 1rem;
}
.navbar-links {
margin: 0;
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
}
.navbar-links a {
margin: 0 0.75rem;
font-size: 16px;
}
.navbar .logo h1 {
font-size: 16px;
max-width: 250px;
}
}
@media (max-width: 480px) {
.navbar .logo h1 {
font-size: 14px;
max-width: 200px;
}
.navbar-links a {
margin: 0 0.5rem;
font-size: 14px;
}
}

View File

@@ -0,0 +1,12 @@
/* 搜索区域 */
.search-section {
padding: 0;
}
.search-container {
background-color: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: var(--card-shadow);
}

View File

@@ -13,7 +13,39 @@
font-size: 32px;
margin-bottom: 20px;
}
#chapter-list {
/* 设置固定高度,超出范围将出现滚动 */
max-height: 75vh;
/* 超出高度时显示垂直滚动条 */
overflow-y: auto;
/* 隐藏水平滚动条(如果不需要) */
overflow-x: hidden;
/* 可选:添加一些内边距和边框增强视觉效果 */
padding: 10px;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
/* 可选美化滚动条样式仅WebKit浏览器有效 */
#chapter-list::-webkit-scrollbar {
width: 6px; /* 滚动条宽度 */
}
#chapter-list::-webkit-scrollbar-track {
background: #f1f1f1; /* 滚动条轨道背景 */
border-radius: 3px;
}
#chapter-list::-webkit-scrollbar-thumb {
background: #c1c1c1; /* 滚动条滑块颜色 */
border-radius: 3px;
}
#chapter-list::-webkit-scrollbar-thumb:hover {
background: #a8a8a8; /* 鼠标悬停时的滑块颜色 */
}
.course-cards {
display: flex;
flex-wrap: wrap;
@@ -28,6 +60,7 @@
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
cursor: pointer;
position: relative; /* 添加相对定位 */
}
.course-image {
@@ -49,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; /* 行高 */
}
/* 课程目录弹出框 */
@@ -127,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; /* 设置最大高度 */
}
/* 按钮样式 */
@@ -173,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 {
@@ -401,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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

View File

@@ -2,6 +2,57 @@
var socket;
let system_message_idx = 0
const activeApprovals = {};
let chatManager; // 全局聊天管理器实例
// 聊天消息管理器 - 更现代的数据结构
class ChatMessageManager {
constructor() {
this.chatbox = document.getElementById('chatbox');
this.messageTemplate = {
user: `<div class="chat-message user-message"><b>你:</b> {{content}}</div>`,
assistant: `<div><b>华实君:</b></div><div class="chat-message server-message"> {{content}}</div>`
};
}
// 渲染单个消息
renderMessage(message) {
let html;
let content;
// 处理不同的数据格式
if (typeof message === 'string') {
// 旧格式:直接字符串
content = marked.parse(message);
html = this.messageTemplate.assistant.replace('{{content}}', content);
} else if (typeof message === 'object' && message !== null) {
// 新格式:{role, content} 对象
content = marked.parse(message.content);
if (message.role === 'user') {
html = this.messageTemplate.user.replace('{{content}}', content);
} else {
html = this.messageTemplate.assistant.replace('{{content}}', content);
}
}
if (html) {
const messageDiv = document.createElement('div');
messageDiv.innerHTML = html;
this.chatbox.appendChild(messageDiv);
this.scrollToBottom();
}
}
// 渲染多条消息(用于恢复历史)
renderMessages(messages) {
messages.forEach(message => this.renderMessage(message));
}
// 滚动到底部
scrollToBottom() {
this.chatbox.scrollTop = this.chatbox.scrollHeight;
}
}
// 清理过期的批准记录
function cleanupExpiredApprovals() {
const now = Date.now();
@@ -24,6 +75,10 @@ document.addEventListener('DOMContentLoaded', function () {
folder: data.folder
}
});
// 初始化聊天消息管理器
chatManager = new ChatMessageManager();
socket.on('connect', function () {
console.log('Connected to server');
socket.emit('login',JSON.stringify(data));
@@ -38,12 +93,13 @@ document.addEventListener('DOMContentLoaded', function () {
input.value += data;
});
socket.on('message', function (data) {
// 显示服务器的回复消息
const serverMessage = document.createElement('div');
serverMessage.innerHTML = `<div><b>华实君:</b></div><div class="chat-message server-message"> ${marked.parse(data)}</div>`;
document.getElementById('chatbox').appendChild(serverMessage);
// 滚动到最新消息
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
// 显示服务器的回复消息 - 单个消息
chatManager.renderMessage(data);
});
socket.on('messages', function (data) {
// 显示服务器发送的消息列表 - 用于恢复对话历史
chatManager.renderMessages(data);
});
socket.on('message-hint', function(data){
console.log('get message-hint');
@@ -90,7 +146,7 @@ document.addEventListener('DOMContentLoaded', function () {
}
const approveButton = document.createElement('button');
approveButton.innerHTML = '<i class="fas fa-check-circle-check"></i>';
approveButton.innerHTML = '<i class="bi bi-check-circle-fill"></i>';
approveButton.data = element;
approveButton.className = 'approve-button';
approveButton.onclick = function (event) {
@@ -105,29 +161,7 @@ document.addEventListener('DOMContentLoaded', function () {
document.getElementById('chatbox').appendChild(requestMessage);
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
}
// for (let i = 0; i < data.length; i++) {
// const element = data[i]
// const requestMessage = document.createElement('div');
// requestMessage.className = 'chat-message server-message';
// const approveButton = document.createElement('button');
// approveButton.innerText = '批准';
// approveButton.data = element;
// approveButton.style.width = '100%';
// approveButton.style.borderRadius = '4px';
// approveButton.onclick = function(event) {
// console.log(element)
// socket.emit('message', JSON.stringify({type:'function', data:element}));
// event.currentTarget.disabled = true;
// event.currentTarget.style.backgroundColor = 'gray';
// event.currentTarget.style.color = 'white'; // 可选:设置文字颜色为白色,以便更清晰地显示
// event.currentTarget.style.border = 'none'; // 可选:去掉边框
// };
// // 将按钮添加到消息气泡中
// requestMessage.innerHTML = `<b>Function:</b> ${element.name}(${JSON.stringify(element.arguments)})<br>`;
// requestMessage.appendChild(approveButton);
// document.getElementById('chatbox').appendChild(requestMessage);
// document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
// }
} catch (error) {
console.log(error)
}
@@ -232,142 +266,19 @@ function sendMessage() {
const input = document.getElementById('messageInput').value;
if (input.trim() === '') return;
// 使用 marked.js 解析 markdown
const markdownHtml = marked.parse(input);
// 显示用户发送的消息
const userMessage = document.createElement('div');
userMessage.innerHTML = `<div class="chat-message user-message"><b>你:</b> ${markdownHtml}</div>`;
document.getElementById('chatbox').appendChild(userMessage);
// 发送消息到服务器
socket.emit('message', JSON.stringify({ data: input, type: 'text' }));
// 使用聊天管理器显示用户消息
chatManager.renderMessage({ role: 'user', content: input });
// 清空输入框
document.getElementById('messageInput').value = '';
// 滚动到最新消息
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
}
// let voice_stream = null;
// let audio_chunks = [];
// let isRecording = false;
// let recordingInterval = null;
// //鼠标点击终止事件
// function handleMouseClick(event) {
// event.stopPropagation();
// stopRecording();
// }
// //发送语音
// const startAudioRecording = async () => {
// try {
// const stream = await navigator.mediaDevices.getUserMedia({
// audio: {
// sampleRate: 16000,
// channelCount: 1,
// echoCancellation: true,
// noiseSuppression: true
// }
// });
// const mediaRecorder = new MediaRecorder(stream);
// const audioContext = new AudioContext({
// sampleRate: 16000
// });
// // Load the AudioWorklet
// await audioContext.audioWorklet.addModule('/static/js/audio-processor.js');
// const source = audioContext.createMediaStreamSource(stream);
// const workletNode = new AudioWorkletNode(audioContext, 'audio-processor');
// let audioChunks = [];
// workletNode.port.onmessage = (e) => {
// audioChunks.push(new Float32Array(e.data));
// if (audioChunks.length >= 16) {
// // 合并所有音频块
// const totalLength = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0);
// const mergedData = new Float32Array(totalLength);
// let offset = 0;
// for (const chunk of audioChunks) {
// mergedData.set(chunk, offset);
// offset += chunk.length;
// }
// const audioMessage = {
// id: window.data.user_uuid,
// type: 'audio',
// data: mergedData,
// sampleRate: 16000,
// timestamp: Date.now()
// };
// socket.emit('voice', audioMessage);
// console.log("voice sended")
// audioChunks = [];
// }
// };
// source.connect(workletNode);
// workletNode.connect(audioContext.destination);
// mediaRecorder.value = {
// stop: () => {
// source.disconnect();
// workletNode.disconnect();
// stream.getTracks().forEach(track => track.stop());
// audioContext.close();
// }
// }
// isRecording.value = true;
// } catch (error) {
// console.error('Error starting audio recording:', error);
// }
// };
// function sendVoiceMessage() {
// startAudioRecording()
// //变换录音中图标
// document.getElementById('recordIcon').className = 'bi bi-record-circle';
// document.addEventListener('click', handleMouseClick);
// }
// //停止录音
// function stopRecording() {
// if (!isRecording) return;
// isRecording = false;
// //取消监听
// document.removeEventListener('click', handleMouseClick);
// //取消定时器
// if (recordingInterval) {
// clearInterval(recordingInterval);
// recordingInterval = null;
// }
// //变换图标
// document.getElementById('recordIcon').className = 'bi bi-mic';
// //发送剩下的数据
// if (audio_chunks.length > 0) {
// const merged_data = new Uint8Array(audio_chunks.reduce((acc, chunk) => acc + chunk.length, 0));
// let offset = 0;
// for (const chunk of audio_chunks) {
// merged_data.set(new Uint8Array(chunk), offset);
// offset += chunk.length;
// }
// socket.emit('VoiceMessage', merged_data, { binary: true });
// audio_chunks = [];
// }
// overlay.style.display = 'none';
// //关闭音频流
// if (voice_stream) {
// voice_stream.stop_stream();
// voice_stream.close();
// voice_stream = null;
// }
// if (p) {
// p.terminate();
// p = null;
// }
// }
//语音这一块//
@@ -513,14 +424,11 @@ window.addEventListener('beforeunload', () => {
let currentChapterIndex = -1;
function next_chapter(data) {
// 获取所有的 h3 元素
var iframe = document.getElementById('markdown-content-iframe');
// 访问 iframe 的文档对象
var iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
var titles = []
var h3Elements = iframeDocument.querySelectorAll('h3');
console.log("h3Elements next")
var container = document.getElementById('markdown-content-container');
var titles = [];
var h3Elements = container.querySelectorAll('h3');
console.log("h3Elements next");
if (currentChapterIndex >= h3Elements.length) {
return; // 如果已经到了最后一个章节,则不进行任何操作
}
@@ -528,7 +436,7 @@ function next_chapter(data) {
// 隐藏当前章节及其后的内容
for (let i = 0; i < h3Elements.length; i++) {
h3Elements[i].style.display = 'none';
titles.push(h3Elements[i].innerText)
titles.push(h3Elements[i].innerText);
let nextSibling = h3Elements[i].nextElementSibling;
while (nextSibling && nextSibling.tagName !== 'H3') {
nextSibling.style.display = 'none';
@@ -558,48 +466,31 @@ function generateProgressBar(N, idx, titles) {
const progressBox = document.createElement('div');
progressBox.className = 'progress-box';
// 获取详细信息容器
const detailBox = document.getElementById('progress-detail');
// 根据N值动态调整每个进度节点的宽度
const nodeWidth = 100 / N; // 每个节点的宽度为 100% / N
// 根据N值生成子框并设置颜色
for (let i = 0; i < N; i++) {
const titleBox = document.createElement('div');
titleBox.className = 'progress-title';
// 设置进度条的颜色
// 设置进度条的颜色:已学习为蓝色,当前学习为绿色,未学习为白色
if (i < idx) {
titleBox.classList.add('green'); // 已完成部分
titleBox.classList.add('blue'); // 已完成部分 - 蓝色
} else if (i === idx) {
titleBox.classList.add('green'); // 当前正在学习 - 绿色
} else {
titleBox.classList.add('white'); // 未完成部分
titleBox.classList.add('white'); // 未完成部分 - 白色
}
// 设置每个子框的标题
titleBox.innerHTML = titles[i] || `Step ${i + 1}`;
// 移除进度条上的文字,因为现在进度条很细
titleBox.innerHTML = '';
// 设置每个进度节点的宽度
titleBox.style.width = `${nodeWidth}%`;
// 监听鼠标移入事件来显示详情
titleBox.addEventListener('mouseenter', function () {
detailBox.innerHTML = `Step ${i + 1}: ${titles[i] || "No title"}`;// - Additional details here...`;
detailBox.style.display = 'block';
// 根据进度条位置显示详情
const rect = titleBox.getBoundingClientRect();
detailBox.style.top = `${rect.top + window.scrollY - 100}px`; // 微调位置
detailBox.style.left = `${rect.left + window.scrollX + rect.width / 2 - detailBox.offsetWidth / 2}px`;
});
// 监听鼠标移出事件来隐藏详情
titleBox.addEventListener('mouseleave', function () {
detailBox.style.display = 'none';
});
// 使用attachImmediateTooltip函数使提示框跟随鼠标移动
const tooltipText = `Step ${i + 1}: ${titles[i] || "No title"}`;
attachImmediateTooltip(titleBox, tooltipText);
progressBox.appendChild(titleBox);
}
// 将生成的进度条添加到容器中
container.appendChild(progressBox);
}

View File

@@ -0,0 +1,58 @@
// helpTip.js - 全局tooltip功能可在不同页面复用
// 在页面里复用一个全局 tooltip 节点
let __globalTooltip;
// 确保tooltip节点存在
function ensureTooltip() {
if (!__globalTooltip) {
__globalTooltip = document.createElement('div');
__globalTooltip.className = 'tooltip-portal';
document.body.appendChild(__globalTooltip);
}
return __globalTooltip;
}
// 绑定到任意图标:移入→显示;移动→跟随;移出→隐藏
function attachImmediateTooltip(anchorEl, text) {
const tip = ensureTooltip();
let shown = false;
function show(e) {
tip.textContent = text;
tip.style.display = 'block';
tip.style.opacity = '1';
position(e);
shown = true;
}
function position(e) {
// 以鼠标位置为准
const padding = 8;
const tipRect = tip.getBoundingClientRect();
let x = e.clientX + 12;
let y = e.clientY - tipRect.height - 12;
// 简易防出界
const vw = window.innerWidth, vh = window.innerHeight;
if (x + tipRect.width + padding > vw) x = vw - tipRect.width - padding;
if (y < padding) y = e.clientY + 16; // 放到下面
tip.style.left = `${x}px`;
tip.style.top = `${y}px`;
}
function hide() {
tip.style.opacity = '0';
// 用小延迟避免闪烁
setTimeout(() => { if (!shown) return; tip.style.display = 'none'; }, 100);
shown = false;
}
anchorEl.addEventListener('mouseenter', show);
anchorEl.addEventListener('mousemove', position);
anchorEl.addEventListener('mouseleave', hide);
}
// 导出函数,供其他文件使用
window.attachImmediateTooltip = attachImmediateTooltip;

View File

@@ -1,42 +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){
window.location.href = '/course/' + course_id
function show_course_details(course_id) {
window.location.href = '/course/' + course_id
}
document.addEventListener('DOMContentLoaded', function() {
const selectButtons = document.querySelectorAll('.select-button');
selectButtons.forEach(button => {
button.addEventListener('click', function(event) {
event.stopPropagation(); // 阻止事件冒泡
const courseId = this.getAttribute('data-course-id');
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('选择课程失败');
}
})
.catch(error => {
console.error('Error:', error);
alert('选择课程失败');
});
function on_click_select_course(event) {
event.stopPropagation(); // 阻止事件冒泡
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',
},
body: JSON.stringify({ course_id: courseId })
})
.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(error?.message || '选择课程失败');
button.disabled = false;
});
});
});
}
document.addEventListener('DOMContentLoaded', function () {
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,198 @@
const editors = {}; // id -> EasyMDE
(function(){
// ====== 配置:图片上传接口 ======
const UPLOAD_URL = '/api/upload'; // 返回 { url: 'https://...' }
// ====== Markdown 渲染器 ======
const md = window.markdownit({
html: false, // 禁止原生HTML安全些如需支持可设为 true
linkify: true,
typographer: true,
breaks: true
});
const $preview = document.getElementById('md-preview');
// ====== 三个 textarea 的 EasyMDE 实例 ======
const textareas = [
{ id: 'editor-lesson', tabBtnId: 'tab-btn-lesson' },
{ id: 'editor-prompt', tabBtnId: 'tab-btn-prompt' },
{ id: 'editor-score', tabBtnId: 'tab-btn-score' },
];
let activeId = 'editor-lesson'; // 默认激活
// 工具:将 Markdown 渲染到右侧预览
const renderToPreview = (markdownStr) => {
const rawHtml = md.render(markdownStr || '');
const safe = DOMPurify.sanitize(rawHtml);
$preview.innerHTML = safe;
};
// 工具:节流/防抖
function debounce(fn, delay){
let t;
return function(...args){
clearTimeout(t);
t = setTimeout(()=>fn.apply(this,args), delay);
}
}
const debouncedPreview = debounce(renderToPreview, 120);
// 工具:插入文本到当前光标
function insertAtCursor(cm, text){
const doc = cm.getDoc();
const cursor = doc.getCursor();
doc.replaceRange(text, cursor);
}
// 图片上传:给粘贴/拖拽/按钮选择共用
async function uploadFile(file){
const form = new FormData();
form.append('file', file);
const { data } = await axios.post(UPLOAD_URL, form, {
headers: { 'Content-Type': 'multipart/form-data' }
});
if(!data || !data.url){
throw new Error('上传返回缺少 url 字段');
}
return data.url;
}
// 为一个 EasyMDE 实例注入图片粘贴/拖拽/按钮上传
function bindImageHandlers(easy){
const cm = easy.codemirror;
// 1) 粘贴图片
cm.on('paste', async (cmInst, e)=>{
const items = (e.clipboardData || e.originalEvent?.clipboardData)?.items || [];
for(const it of items){
if(it.kind === 'file'){
const file = it.getAsFile();
if(file && file.type.startsWith('image/')){
e.preventDefault();
try{
const url = await uploadFile(file);
insertAtCursor(cmInst, `![image](${url})`);
}catch(err){
console.error(err);
alert('图片上传失败,请重试');
}
}
}
}
});
// 2) 拖拽图片
easy.codemirror.getWrapperElement().addEventListener('drop', async (e)=>{
const files = e.dataTransfer?.files || [];
if(!files.length) return;
const img = Array.from(files).find(f=>f.type.startsWith('image/'));
if(!img) return;
e.preventDefault();
try{
const url = await uploadFile(img);
insertAtCursor(cm, `![image](${url})`);
}catch(err){
console.error(err);
alert('图片上传失败,请重试');
}
});
// 3) 工具栏“上传图片”按钮:使用隐藏 input[type=file]
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
fileInput.addEventListener('change', async ()=>{
const file = fileInput.files?.[0];
if(!file) return;
try{
const url = await uploadFile(file);
insertAtCursor(cm, `![image](${url})`);
}catch(err){
console.error(err);
alert('图片上传失败,请重试');
}finally{
fileInput.value = '';
}
});
document.body.appendChild(fileInput);
// 将 input 触发器挂到 toolbar 按钮上(下面初始化里有 name: 'upload-image'
easy.toolbar?.forEach(btn=>{
if(btn?.name === 'upload-image'){
btn.action = () => fileInput.click();
}
});
}
// 创建并初始化一个 EasyMDE
function createMDEFor(textareaId){
const el = document.getElementById(textareaId);
if(!el) return null;
const mde = new EasyMDE({
element: el,
autoDownloadFontAwesome: false,
spellChecker: false,
status: false,
autofocus: textareaId === activeId,
minHeight: '100%',
renderingConfig: { singleLineBreaks: false },
uploadImage: false, // 我们自定义上传
toolbar: [
'bold','italic','heading','|',
'quote','unordered-list','ordered-list','table','|',
'code','link',
{ name:'upload-image', action:null, className:'fa fa-image', title:'插入图片(上传)' },
'|','preview','side-by-side','fullscreen',
'|','guide'
],
// 保证 textarea 同步(默认就会同步 value保存时你的原逻辑仍可读取
forceSync: true
});
// 内容变化 -> 刷新预览(仅激活编辑器触发)
mde.codemirror.on('change', ()=>{
if(textareaId === activeId){
debouncedPreview(mde.value());
}
});
bindImageHandlers(mde);
return mde;
}
// 初始化三个编辑器
for(const t of textareas){
editors[t.id] = createMDEFor(t.id);
}
// 初次渲染预览
debouncedPreview(editors[activeId]?.value() || '');
// 监听 Tab 切换按钮,让预览跟随当前激活编辑器
function setActiveEditor(id){
activeId = id;
// 确保 EasyMDE 的编辑区获得焦点(可选)
const ed = editors[id];
if(ed){
ed.codemirror.refresh();
ed.codemirror.focus();
renderToPreview(ed.value());
}
}
document.addEventListener('tab:change', (e) => {
const { editorId } = e.detail || {};
if (editorId) setActiveEditor(editorId);
});
// 如果你已有 tab 切换逻辑(例如给 .tab-btn 切换 active 类),保留原逻辑即可;
// 我们只需在切换时调用 setActiveEditor 对齐预览内容。
})();

View File

@@ -0,0 +1,116 @@
// 动态加载 Markdown 文件
function loadMarkdown(course_id, chapter_name, lesson_name) {
fetch(`/${course_id}-${chapter_name}-${lesson_name}-markdown`)
.then(response => response.json())
.then(data => {
if (data.markdown_content) {
// 使用 marked 解析 Markdown 内容
const html = marked.parse(data.markdown_content);
// 创建包含 MathJax 支持的 HTML 结构
const markdownContainer = document.createElement('div');
markdownContainer.id = 'markdown-content-container';
markdownContainer.innerHTML = html;
// 处理代码块,实现缩略显示
const codeBlocks = markdownContainer.querySelectorAll('pre code');
codeBlocks.forEach(codeBlock => {
const preBlock = codeBlock.parentElement;
// 保存原始代码内容
const originalContent = preBlock.innerHTML;
// 创建缩略提示元素
const codeHint = document.createElement('div');
codeHint.className = 'code-hint';
codeHint.innerHTML = '<div style="padding: 10px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; color: #666; font-size: 14px; text-align: center;">💡 代码已放置在代码区中,点击展开查看</div>';
// 创建展开/折叠按钮
const toggleButton = document.createElement('button');
toggleButton.className = 'toggle-code';
toggleButton.innerHTML = '展开代码';
toggleButton.style.cssText = 'margin: 5px; padding: 5px 10px; background-color: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;';
// 添加到提示元素中
codeHint.appendChild(toggleButton);
// 替换原始代码块为提示
preBlock.innerHTML = '';
preBlock.appendChild(codeHint);
// 保存原始代码,用于切换
preBlock.dataset.originalContent = originalContent;
preBlock.dataset.isExpanded = 'false';
// 添加切换事件
toggleButton.addEventListener('click', () => {
if (preBlock.dataset.isExpanded === 'false') {
// 展开代码
preBlock.innerHTML = preBlock.dataset.originalContent;
toggleButton.innerHTML = '折叠代码';
preBlock.appendChild(toggleButton);
preBlock.dataset.isExpanded = 'true';
// 如果MathJax已加载重新排版
if (typeof MathJax !== 'undefined') {
MathJax.typesetPromise([preBlock]);
}
} else {
// 折叠代码
preBlock.innerHTML = '';
preBlock.appendChild(codeHint.cloneNode(true));
preBlock.appendChild(toggleButton.cloneNode(true));
preBlock.dataset.isExpanded = 'false';
// 重新添加事件监听
const newToggleButton = preBlock.querySelector('.toggle-code');
newToggleButton.addEventListener('click', arguments.callee);
}
});
});
// 清空内容容器并添加渲染后的内容
const contentElement = document.getElementById('markdown-content');
contentElement.innerHTML = '';
contentElement.appendChild(markdownContainer);
// 配置并加载 MathJax
if (typeof MathJax === 'undefined') {
// 动态加载 MathJax
const script = document.createElement('script');
script.src = '/static/cdnback/js/tex-mml-chtml.js';
script.async = true;
script.onload = function() {
MathJax.config = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']]
},
svg: {
fontCache: 'global'
}
};
MathJax.typesetPromise([markdownContainer]);
// 初始化课程进度
next_chapter();
};
document.head.appendChild(script);
} else {
// MathJax 已加载,直接排版
MathJax.typesetPromise([markdownContainer]);
// 初始化课程进度
next_chapter();
}
} else {
console.error('Markdown file not found');
}
})
.catch(error => {
console.error('Error loading markdown:', error);
});
}
// 文档加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 从全局变量获取课程信息
if (window.appData) {
loadMarkdown(window.appData.course_id, window.appData.chapter_name, window.appData.lesson_name);
}
});

View File

@@ -450,6 +450,7 @@ async function persistOrderNow(chapters) {
});
if (!res.ok) throw new Error('保存新顺序失败');
console.log('已保存顺序')
console.log(payload);
} catch (err) {
console.error(err);
}

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

Binary file not shown.

Binary file not shown.

View File

@@ -8,15 +8,22 @@
</style>
<link rel="stylesheet" href="/static/css/desktop.css">
<link rel="stylesheet" href="/static/css/chatbox_approve_icon.css">
<link rel="stylesheet" href="/static/css/helpTip.css">
<script>
document.cookie = "vscode-tkn=44edc269-6f88-46ab-9790-dc253f66ac36; path=/; SameSite=None; Secure";
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/1.1.1/marked.min.js"></script>
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css" rel="stylesheet">
<script src="https://unpkg.com/split.js/dist/split.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/1.1.1/"></script> -->
<script src="/static/cdnback/marked.min.js"></script>
<!-- <script src="https://cdn.socket.io/4.0.0/"></script> -->
<script src="/static/cdnback/socket.io.min.js"></script>
<!-- <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/" rel="stylesheet"> -->
<link rel="stylesheet" href="/static/cdnback/bootstrap.min.css">
<!-- <script src="https://unpkg.com/split.js/dist/"></script> -->
<script src="/static/cdnback/split.min.js"></script>
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/"></script> -->
<script src="/static/cdnback/jquery.min.js"></script>
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"> -->
<link rel="stylesheet" href="/static/cdnback/bootstrap-icons.css">
</head>
<body>
<div class="maxcontainer" id="maxcontainer" style="height: 100%;">
@@ -79,6 +86,22 @@
folder:"{{workspace_path}}"
}
</script>
<script>
// 检测页面刷新事件
window.addEventListener('beforeunload', function(e) {
// 这里不弹出提示,只在页面刷新时执行跳转
// 但是beforeunload中不能直接跳转所以我们需要在页面加载时检测是否是刷新
});
// 检测是否是页面刷新
if (performance.navigation.type === 1 || performance.getEntriesByType("navigation")[0].type === "reload") {
// 是刷新操作,跳转到确认页面
const course_id = "{{course_id}}";
const chapter_name = "{{chapter_name}}";
const lesson_name = "{{lesson_name}}";
window.location.href = `/desktop_nouser/${course_id}/${chapter_name}/${lesson_name}`;
}
</script>
</div>
</div>
@@ -118,9 +141,7 @@
</div>
</div>
<script src="/static/js/process_show_floating_window.js"></script>
<script src="/static/js/helpTip.js"></script>
<script src="/static/js/chatbox.js"></script>
<script>
@@ -171,7 +192,6 @@
// 使用 fetch 发送请求到 Flask 服务器,获取 session 信息
fetch('/get_session', {
method: 'GET',
@@ -208,31 +228,6 @@
}
// 动态加载 Markdown 文件
function loadMarkdown(course_id, chapter_name, lesson_name) {
fetch(`/${course_id}-${chapter_name}-${lesson_name}-markdown`)
.then(response => response.json())
.then(data => {
if (data.html_url) {
document.getElementById('markdown-content').innerHTML = `<iframe id="markdown-content-iframe" src="${data.html_url}"style="width: 100%;height: 100%;"></iframe>`;
document.getElementById('markdown-content-iframe').addEventListener('load', function() {
// 在这里执行你想要的操作
console.log('Iframe has finished loading');
next_chapter();
});
} else {
console.error('Markdown file not found');
}
})
.catch(error => {
console.error('Error loading markdown:', error);
});
}
document.addEventListener('DOMContentLoaded', function() {
loadMarkdown('{{course_id}}','{{chapter_name}}','{{lesson_name}}');
});
// 侧边工具栏切换
const sidebarTools = document.getElementById('sidebarTools');
const toggleButton = document.getElementById('toggleButton');
@@ -248,5 +243,7 @@
});
</script>
<!-- 引入 Markdown 渲染脚本 -->
<script src="/static/js/render-desktop-markdown.js"></script>
</body>
</html>

View File

@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>确认加载历史数据</title>
<link rel="stylesheet" href="/static/cdnback/bootstrap.min.css">
<style>
body {
background-color: #f8f9fa;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.confirm-container {
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 30px;
max-width: 500px;
width: 100%;
text-align: center;
}
h1 {
color: #343a40;
margin-bottom: 20px;
}
p {
color: #6c757d;
margin-bottom: 30px;
font-size: 16px;
}
.btn-group {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 20px;
}
.btn {
padding: 10px 30px;
font-size: 16px;
border-radius: 5px;
}
.btn-primary {
background-color: #007bff;
border: none;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
border: none;
}
.btn-secondary:hover {
background-color: #545b62;
}
</style>
</head>
<body>
<div class="confirm-container">
{% if is_course_finished %}
<h1>重新学习确认</h1>
<p>您已经完成了该课程的学习,是否要重新开始学习?</p>
<div class="btn-group">
<a href="{{ url_for('vscode.desktop', user_uuid=user_uuid, course_id=course_id, chapter_name=chapter_name, lesson_name=lesson_name, load_history='false') }}" class="btn btn-primary">
重新开始学习
</a>
</div>
{% else %}
<h1>加载历史数据确认</h1>
<p>您即将进入学习环境,是否需要加载您之前的对话历史记录和学习进度数据?</p>
<div class="btn-group">
<a href="{{ url_for('vscode.desktop', user_uuid=user_uuid, course_id=course_id, chapter_name=chapter_name, lesson_name=lesson_name, load_history='true') }}" class="btn btn-primary">
是,加载历史数据
</a>
<a href="{{ url_for('vscode.desktop', user_uuid=user_uuid, course_id=course_id, chapter_name=chapter_name, lesson_name=lesson_name, load_history='false') }}" class="btn btn-secondary">
否,重新开始
</a>
</div>
{% endif %}
</div>
</body>
</html>

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

@@ -5,9 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>华实学伴 - 专业的在线学习平台</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"> -->
<link href="/static/cdnback/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome 图标 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> -->
<link rel="stylesheet" href="/static/cdnback/all.min.css">
<link rel="stylesheet" href="/static/css/search-section.css">
<link rel="stylesheet" href="/static/css/dashboard.css">
</head>
<body>

View File

@@ -4,17 +4,16 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>华实学伴 - 专业的在线学习平台</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome 图标 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<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">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="/static/css/search-section.css">
<script src="/static/cdnback/js/bootstrap.bundle.min.js"></script>
</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">
@@ -82,7 +81,7 @@
</div>
</div>
{% include 'foot.html' %}
{% include 'footer-brief.html' %}
<script src="/static/js/dashboard.js"></script>
<script>

View File

@@ -12,12 +12,18 @@
<script>
document.cookie = "vscode-tkn=44edc269-6f88-46ab-9790-dc253f66ac36; path=/; SameSite=None; Secure";
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/1.1.1/marked.min.js"></script>
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css" rel="stylesheet">
<script src="https://unpkg.com/split.js/dist/split.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/1.1.1/"></script> -->
<script src="/static/cdnback/marked.min.js"></script>
<!-- <script src="https://cdn.socket.io/4.0.0/"></script> -->
<script src="/static/cdnback/socket.io.min.js"></script>
<!-- <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/" rel="stylesheet"> -->
<link rel="stylesheet" href="/static/cdnback/bootstrap.min.css">
<!-- <script src="https://unpkg.com/split.js/dist/"></script> -->
<script src="/static/cdnback/split.min.js"></script>
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/"></script> -->
<script src="/static/cdnback/jquery.min.js"></script>
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"> -->
<link rel="stylesheet" href="/static/cdnback/bootstrap-icons.css">
</head>
<body>

Some files were not shown because too many files have changed in this diff Show More