48 Commits

Author SHA1 Message Date
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
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
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
21 changed files with 1790 additions and 801 deletions

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

@@ -32,8 +32,6 @@ class ASEngineClient:
# ACK相关
self.ack_events: Dict[str, Event] = {} # 存储ack_id和对应的Event对象
# 注册基础事件处理函数
self._register_base_events()
def _register_base_events(self):
@@ -45,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)
@@ -70,31 +72,53 @@ class ASEngineClient:
def on_ack(data):
ack_id = data.get('ack_id')
if ack_id and ack_id in self.ack_events:
print(f"Received ACK for message: {ack_id}")
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,
@@ -109,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}")
@@ -118,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")
@@ -130,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
}
# 注册事件处理器
@@ -139,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:
@@ -180,7 +225,7 @@ class ASEngineClient:
self.sid = None
print("Disconnected from server")
def send_text(self, route: str, text: str, id:str="", wait_for_ack: bool = False, timeout: int = 5) -> 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: 目标路由
@@ -188,7 +233,8 @@ class ASEngineClient:
:param id: 发送者ID
:param wait_for_ack: 是否等待服务器ACK确认
:param timeout: 等待ACK的超时时间
:return: 是否发送成功若wait_for_ack为True则返回是否收到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")
@@ -199,20 +245,13 @@ class ASEngineClient:
try:
# 生成唯一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)
print(f"Sent text to route '{route}': {text[:20]}")
# 如果不需要等待ACK直接返回True
if not wait_for_ack:
return True
@@ -220,6 +259,10 @@ class ASEngineClient:
event = Event()
self.ack_events[ack_id] = event
# 如果需要返回事件对象,直接返回
if return_event:
return event, ack_id
# 等待ACK或超时
received_ack = event.wait(timeout=timeout)
@@ -268,9 +311,6 @@ class ASEngineClient:
},
namespace=self.namespace
)
print(f"Sent voice file to route '{route}'")
# 如果不需要等待ACK直接返回True
if not wait_for_ack:
return True
@@ -336,7 +376,6 @@ class ASEngineClient:
},
namespace=self.namespace
)
print(f"Sent image file to route '{route}': {filename}")
# 如果不需要等待ACK直接返回True
if not wait_for_ack:
@@ -380,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, wait_for_ack=True)
self.send_text('markdown-prompt-in', fmdp, self.id, wait_for_ack=True)
self.send_text('score-prompt-in', fsp, self.id, wait_for_ack=True)
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

@@ -33,7 +33,8 @@ class ProcRecord:
class ChatManager:
_procs: Dict[str, ProcRecord] = {}
_lock = threading.RLock()
def __init__(self):
def __init__(self, restart=False):
self.restart = restart
self.chapter_chain_now = -1
self.ase_client = None
self.app = None
@@ -128,10 +129,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,83 @@
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)
if restart: # 重启时,从-1章进入 0章
ase_client.chatmanager.next_chapter()
ase_client.chatmanager.load_now_chapter(load_code=restart)
print("load_now_chapter with restart:", restart)
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

@@ -8,591 +8,7 @@ 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 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
def save_learning_progress(user_uuid: str, chatmanager, user_id: str) -> bool:
"""
保存用户学习进度到MongoDB
参数:
- user_uuid: 用户UUID
- chatmanager: ChatManager实例包含学习进度数据
- user_id: 用户ID
返回:
- bool: 保存是否成功
"""
try:
# 获取MongoDB连接
mongo = current_app.extensions["mongo"]
# 获取MultiAgents框架中的对话列表
dialog_list = get_multiagents_dialog_list(user_id)
# 构建学习进度数据
progress_data = {
"user_uuid": user_uuid,
"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": dialog_list,
"updated_at": datetime.now()
}
# 保存到MongoDB使用upsert操作如果存在则更新不存在则插入
mongo.db.learning_progress.update_one(
{
"user_uuid": user_uuid,
"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_uuid={user_uuid}, 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
返回:
- List[Dict]: 对话列表
"""
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:
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_uuid: str, material_id: str, chapter_name: str, lesson_name: str) -> Optional[Dict]:
"""
加载用户学习进度
参数:
- user_uuid: 用户UUID
- material_id: 课程ID
- chapter_name: 章节名
- lesson_name: 课时名
返回:
- Optional[Dict]: 学习进度数据如果不存在则返回None
"""
try:
mongo = current_app.extensions["mongo"]
progress = mongo.db.learning_progress.find_one({
"user_uuid": user_uuid,
"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 progress
return None
except Exception as e:
current_app.logger.error(f"加载学习进度失败: {str(e)}")
return None
from .course_services.learning_progess_process import *

View File

@@ -0,0 +1,235 @@
# 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,
"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:
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,
"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,
"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

@@ -10,18 +10,28 @@ 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.course_service import save_learning_progress
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']
@@ -53,8 +63,52 @@ 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: 是否继续学习
Returns:
int: 需要跳过的章节数
"""
need_skip_chapters = 0
if continue_learn: # 尝试加载用户学习进度
progress_result = load_learning_progress(user_id, course_id, chapter_name, lesson_name)
if progress_result['exists']:
progress = progress_result['data']
# 恢复学习进度数据
if 'scores' in progress:
# 根据scores的数量确定需要跳过的章节数
need_skip_chapters = len(progress['scores'])
chatmanager.scores = progress['scores']
# 恢复聊天历史
if 'chat_historys' in progress:
chatmanager.chat_historys = progress['chat_historys']
else:
chatmanager.chat_historys = []
# 恢复当前章节链
if 'chapter_chain_now' in progress:
chatmanager.chapter_chain_now = progress['chapter_chain_now']
upload_learning_progress_to_cloud(progress)
else:
# 使用默认值,确保有完整的结构
chatmanager.chat_historys = progress_result['data']['chat_historys']
chatmanager.scores = progress_result['data']['scores']
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"]
@@ -75,95 +129,79 @@ 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})
# 尝试加载用户学习进度
progress = load_learning_progress(user_uuid, course_id, chapter_name, lesson_name)
need_skip_chapters = 0
if progress:
# 恢复学习进度数据
if 'scores' in progress:
# 根据scores的数量确定需要跳过的章节数
need_skip_chapters = len(progress['scores'])
chatmanager.scores = progress['scores']
print(f"恢复用户学习进度: {user_uuid}, 已完成 {need_skip_chapters} 个章节")
# 恢复聊天历史
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']
else:
chatmanager.chat_historys = []
chatmanager.scores = []
clear_user_session(current_app.config["ASE_ENGINE_URL"], current_app.config["ASE_ENGINE_URL_TOKEN"], user_id)
continue_learn = not chatmanager.restart
need_skip_chapters = self._load_learning_progress(chatmanager, user_id, course_id, chapter_name, lesson_name, continue_learn)
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)
if not progress:
chatmanager.next_chapter()
# 如果有保存的进度,需要跳过已经完成的章节、无需清空代码、对话历史恢复
if continue_learn:
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')
# 如果有保存的进度,需要跳过已经完成的章节
for _ in range(need_skip_chapters):
chatmanager.next_chapter()
self.chatmanager = chatmanager
self.ase_client = user_uuid2ase_client[user_uuid]
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':chatmanager.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}
)
@@ -172,10 +210,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')
@@ -185,14 +221,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'])
@@ -203,81 +236,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')
namespace_entry.emit('message', "Agents服务已连接, 加载教案中", room=init_data['room'], namespace='/agent')
try:
base_url = current_app.config['ASE_ENGINE_URL']
clear_url = f"{base_url}/clear_user_session"
payload = {
'namespace_url': current_app.config['ASE_ENGINE_URL_TOKEN'], # WebSocket命名空间URL
'user_id': init_data['room'], # 用户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"Clear user session successful: {response.json()}")
except Exception as e:
print(f"Error clearing user session: {e}")
ase_client.chatmanager.load_next_chapter()
ase_client.send_text("chapter-start", "")
namespace_entry.emit('message', "教案加载完毕", 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.course_id, chatmanager.chapter_name, chatmanager.lesson_name)
chatmanager.load_next_chapter()
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]
@@ -290,7 +251,7 @@ class AgentNamespace(Namespace):
def on_disconnect(self,data):
print(f'AgentNamespace client disconnected, session user_uuid: {session.get("user_uuid", "unknown")}')
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.get("user_uuid2chatmanager", {})
@@ -300,14 +261,14 @@ class AgentNamespace(Namespace):
# 获取用户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(uuid, chatmanager, user_id)
print(f"保存学习进度结果: {save_result}, user_uuid={uuid}")
save_result = save_learning_progress(chatmanager, user_id)
except Exception as e:
print(f"保存学习进度时发生异常: {str(e)}")
self.app.logger.error(f"保存学习进度时发生异常: {str(e)}")
# 执行原有的断开连接逻辑
if uuid in user_uuid2chatmanager:
@@ -317,5 +278,5 @@ class AgentNamespace(Namespace):
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)}")

File diff suppressed because one or more lines are too long

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
}
});
// 初始化聊天消息管理器
const 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');
@@ -210,22 +266,14 @@ 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;
}
@@ -376,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; // 如果已经到了最后一个章节,则不进行任何操作
}
@@ -391,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';

View File

@@ -85,6 +85,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>
@@ -219,13 +235,46 @@
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');
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 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');
}

View File

@@ -0,0 +1,76 @@
<!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">
<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>
</div>
</body>
</html>

View File

@@ -0,0 +1,58 @@
import json
from datetime import datetime, date, time
from bson import ObjectId
from typing import Any, Dict, List, Union
class JSONEncoder(json.JSONEncoder):
"""
通用JSON编码器处理常见的非JSON序列化类型
支持的特殊类型:
- datetime: 转换为ISO格式字符串
- date: 转换为ISO格式字符串
- time: 转换为ISO格式字符串
- ObjectId: 转换为字符串
- 其他不可序列化类型: 转换为字符串表示
"""
def default(self, obj: Any) -> Any:
if isinstance(obj, (datetime, date, time)):
return obj.isoformat()
elif isinstance(obj, ObjectId):
return str(obj)
elif isinstance(obj, bytes):
return obj.decode('utf-8', errors='replace')
elif isinstance(obj, (set, frozenset)):
return list(obj)
elif hasattr(obj, '__dict__'):
return obj.__dict__
else:
return str(obj)
def json_serialize(obj: Any) -> str:
"""
通用JSON序列化函数处理常见的非JSON序列化类型
参数:
- obj: 要序列化的对象
返回:
- str: JSON字符串
"""
return json.dumps(obj, cls=JSONEncoder, ensure_ascii=False)
def json_serialize_dict(obj: Any) -> Dict:
"""
通用JSON序列化函数将对象转换为可JSON序列化的字典
参数:
- obj: 要序列化的对象
返回:
- Dict: 可JSON序列化的字典
"""
json_str = json_serialize(obj)
return json.loads(json_str)

View File

@@ -14,17 +14,8 @@ bp = Blueprint("markdown", __name__)
def convert_md(course_id, chapter_name, lesson_name):
print(f"convert_md: {course_id}, {chapter_name}, {lesson_name}")
md_file = load_markdown_file(course_id, chapter_name, lesson_name)
# 配置Markdown解析器,启用数学公式支持
md = MarkdownIt()
md.use(texmath.texmath_plugin) # 添加数学公式插件
# 转换markdown为html包含公式标记
html = md.render(md_file)
# 包装样式时添加MathJax支持用于在浏览器中渲染公式
html_with_styles = wrap_with_styles(html)
# 保存HTML文件到static
html_output_path = os.path.join(current_app.config["STATIC_DIR"], f"{course_id}-{chapter_name}-{lesson_name}.html")
save_html(html_with_styles, html_output_path)
return jsonify({"html_url": f"/static/{course_id}-{chapter_name}-{lesson_name}.html"})
# 直接返回Markdown源文件内容,不进行服务器端渲染
return jsonify({"markdown_content": md_file})
# 提供静态文件访问
@bp.route("/static/<path:filename>")

View File

@@ -17,6 +17,16 @@ def desktop(user_uuid, course_id, chapter_name, lesson_name):
userid_recorder = current_app.extensions["userid_recorder"]
if "user_uuid" not in session:
return redirect(url_for("auth.login"))
# 检查是否有 load_history 参数
load_history_str = request.args.get('load_history')
if load_history_str is None:
# 如果没有参数,跳转到确认页面
return redirect(url_for("vscode.desktop_nouser", course_id=course_id, chapter_name=chapter_name, lesson_name=lesson_name))
# 将字符串转换为布尔值
load_history = load_history_str.lower() == 'true'
# 全局映射
user_id = uuid2username[user_uuid]
userid_recorder[f"{user_id}&{course_id}"] = session["user_uuid"]
@@ -58,8 +68,7 @@ def desktop(user_uuid, course_id, chapter_name, lesson_name):
chatmanager = ChatManager()
print(f"now user uuid {user_uuid}")
chatmanager = ChatManager(restart=not load_history)
current_app.extensions["user_uuid2chatmanager"][user_uuid] = chatmanager
code_server_port = 10000 + int(uuid.uuid4().int % 10000)
# chatmanager.start_code_server(user_uuid, user_id, path_dir, code_server_port)
@@ -136,11 +145,18 @@ def desktop(user_uuid, course_id, chapter_name, lesson_name):
)
@bp.route("/desktop_nouser/<course_id>/<chapter_name>/<lesson_name>")
@require_role
def desktop_nouser(course_id, chapter_name, lesson_name):
if "user_uuid" not in session:
return redirect(url_for("auth.login")) # 如果你有 auth 蓝图
user_uuid = session["user_uuid"]
return redirect(url_for("vscode.desktop", user_uuid=user_uuid, course_id=course_id, chapter_name=chapter_name, lesson_name=lesson_name))
return render_template(
"confirm_load_history.html",
user_uuid=user_uuid,
course_id=course_id,
chapter_name=chapter_name,
lesson_name=lesson_name
)
@bp.route("/vscode_data", methods=["POST"])
def vscode_data():

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
import sys
import os
from datetime import datetime
# Add the project root to the Python path
sys.path.insert(0, '/Users/cakecai/Documents/gitea/hsa/Html')
# Test the JSON serializer
try:
from apps.utils.json_serializer import json_serialize, json_serialize_dict, JSONEncoder
import json
# Create a test dictionary with datetime object
test_data = {
"user_id": "test_user",
"material_id": "test_course",
"chapter_name": "test_chapter",
"lesson_name": "test_lesson",
"chat_historys": [],
"scores": [],
"chapter_chain_now": None,
"updated_at": datetime.now()
}
print("Testing datetime serialization...")
print(f"Original data: {test_data}")
print(f"Type of updated_at: {type(test_data['updated_at'])}")
# Test json_serialize function
json_str = json_serialize(test_data)
print(f"\nSerialized JSON string: {json_str}")
# Test json_serialize_dict function
serialized_dict = json_serialize_dict(test_data)
print(f"\nSerialized dictionary: {serialized_dict}")
print(f"Type of updated_at in serialized dict: {type(serialized_dict['updated_at'])}")
# Test direct json.dumps with custom encoder
json_str2 = json.dumps(test_data, cls=JSONEncoder, ensure_ascii=False)
print(f"\nDirect json.dumps with custom encoder: {json_str2}")
# Verify it can be parsed back
parsed = json.loads(json_str)
print(f"\nParsed back from JSON: {parsed}")
print("\n✅ All tests passed! datetime objects can be correctly serialized to JSON.")
except Exception as e:
print(f"\n❌ Test failed with error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

41
Html/test_progress.py Normal file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python3
import sys
import os
# Add the project root to the Python path
sys.path.insert(0, '/Users/cakecai/Documents/gitea/hsa/Html')
from flask import Flask
from flask_pymongo import PyMongo
# Create a minimal Flask app for testing
app = Flask(__name__)
app.config['MONGO_URI'] = 'mongodb://localhost:27017/test_db'
mongo = PyMongo(app)
# Register the mongo extension with the app
app.extensions = {'mongo': mongo}
# Import the function to test
with app.app_context():
from apps.services.course_services.learning_progess_process import load_learning_progress
# Test with non-existent record
print("Testing with non-existent record:")
result = load_learning_progress("test_user", "test_course", "test_chapter", "test_lesson")
print(f"Result: {result}")
print(f"Exists: {result['exists']}")
print(f"Data keys: {list(result['data'].keys())}")
print(f"Message: {result['message']}")
print("\n")
# Test that default values are correctly set
print("Testing default values:")
print(f"Chat historys: {result['data']['chat_historys']}")
print(f"Scores: {result['data']['scores']}")
print(f"Chapter chain now: {result['data']['chapter_chain_now']}")
print(f"Updated at: {result['data']['updated_at']}")
print("\n")
print("Test completed successfully!")

BIN
profile_stats Normal file

Binary file not shown.

BIN
profile_stats_extract Normal file

Binary file not shown.