12 Commits

Author SHA1 Message Date
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
11 changed files with 481 additions and 189 deletions

View File

@@ -48,4 +48,5 @@ class Config:
} }
ASE_ENGINE_URL = GLOBAL_CONFIG['ASE_ENGINE']['url'] 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'] ASE_ENGINE_NAMESPACE = GLOBAL_CONFIG['ASE_ENGINE']['namespace']

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

@@ -5,6 +5,7 @@ import os
from datetime import datetime from datetime import datetime
from typing import Dict, Callable, Any, Optional from typing import Dict, Callable, Any, Optional
from functools import partial from functools import partial
from threading import Event
class ASEngineClient: class ASEngineClient:
def __init__(self, server_url: str, namespace: str, id:str): def __init__(self, server_url: str, namespace: str, id:str):
@@ -29,7 +30,8 @@ class ASEngineClient:
self._on_connect_callback_entry=None self._on_connect_callback_entry=None
self._on_connect_callback_initData=None self._on_connect_callback_initData=None
# ACK相关
self.ack_events: Dict[str, Event] = {} # 存储ack_id和对应的Event对象
# 注册基础事件处理函数 # 注册基础事件处理函数
self._register_base_events() self._register_base_events()
@@ -63,6 +65,15 @@ class ASEngineClient:
def on_response(data): def on_response(data):
print(f"Received 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:
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: def register_on_connect_entry(self, callback: Callable, entry: Any, init_data: Any)->bool:
if not callable(callback): if not callable(callback):
return False return False
@@ -169,12 +180,15 @@ class ASEngineClient:
self.sid = None self.sid = None
print("Disconnected from server") 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) -> bool:
""" """
发送文本消息 发送文本消息
:param route: 目标路由 :param route: 目标路由
:param text: 要发送的文本 :param text: 要发送的文本
:return: 是否发送成功 :param id: 发送者ID
:param wait_for_ack: 是否等待服务器ACK确认
:param timeout: 等待ACK的超时时间
:return: 是否发送成功若wait_for_ack为True则返回是否收到ACK
""" """
if not self.connected: if not self.connected:
print("Not connected to server") print("Not connected to server")
@@ -183,19 +197,53 @@ class ASEngineClient:
id = self.id id = self.id
try: try:
self.sio.emit(route, {'type': 'text', 'data': text, 'id': id}, namespace=self.namespace) # 生成唯一ack_id
print(f"Sent text to route '{route}': {text}") 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 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: except Exception as e:
print(f"Failed to send text to route '{route}': {e}") print(f"Failed to send text to route '{route}': {e}")
return False 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 route: 目标路由
:param voice_data: 语音数据字节流 :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: if id is None:
id = self.id id = self.id
@@ -204,8 +252,9 @@ class ASEngineClient:
return False return False
try: try:
# 编码语音数据为base64 # 生成唯一ack_id
# voice_data = 'data:image/jpeg;base64,' +base64.b64encode(voice_data).decode('utf-8') ack_id = f"{id}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}"
# 发送数据 # 发送数据
self.sio.emit( self.sio.emit(
route, route,
@@ -214,24 +263,46 @@ class ASEngineClient:
'data': voice_data, 'data': voice_data,
'timestamp': int(time.time() * 1000), 'timestamp': int(time.time() * 1000),
'sampleRate': 16000, 'sampleRate': 16000,
'id': id 'id': id,
'ack_id': ack_id
}, },
namespace=self.namespace namespace=self.namespace
) )
print(f"Sent voice file to route '{route}'") print(f"Sent voice file to route '{route}'")
# 如果不需要等待ACK直接返回True
if not wait_for_ack:
return True 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: except Exception as e:
print(f"Failed to send voice to route '{route}': {e}") print(f"Failed to send voice to route '{route}': {e}")
return False 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 route: 目标路由
:param file_path: 图片文件路径 :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: if not self.connected:
print("Not connected to server") print("Not connected to server")
@@ -251,18 +322,41 @@ class ASEngineClient:
# 获取文件名 # 获取文件名
filename = os.path.basename(file_path) 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( self.sio.emit(
route, route,
{ {
'type': 'image', 'type': 'image',
'data': image_data, 'data': image_data,
'id': id 'id': id,
'ack_id': ack_id
}, },
namespace=self.namespace namespace=self.namespace
) )
print(f"Sent image file to route '{route}': {filename}") print(f"Sent image file to route '{route}': {filename}")
# 如果不需要等待ACK直接返回True
if not wait_for_ack:
return True 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: except Exception as e:
print(f"Failed to send image to route '{route}': {e}") print(f"Failed to send image to route '{route}': {e}")
return False return False
@@ -286,6 +380,6 @@ class HSAEngineClient(ASEngineClient):
super().__init__(server_url, namespace, id) super().__init__(server_url, namespace, id)
self.chatmanager = chatmanager self.chatmanager = chatmanager
def loadmarkdown(self, fmd, fmdp, fsp): def loadmarkdown(self, fmd, fmdp, fsp):
self.send_text('markdown-in', fmd, self.id) self.send_text('markdown-in', fmd, self.id, wait_for_ack=True)
self.send_text('markdown-prompt-in', fmdp, self.id) self.send_text('markdown-prompt-in', fmdp, self.id, wait_for_ack=True)
self.send_text('score-prompt-in', fsp, self.id) self.send_text('score-prompt-in', fsp, self.id, wait_for_ack=True)

View File

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

View File

@@ -1,6 +1,8 @@
# myapp/services/course_service.py # myapp/services/course_service.py
import re import re
from typing import Dict, List import requests
import json
from typing import Dict, List, Optional
from flask import current_app from flask import current_app
from urllib.parse import urlparse from urllib.parse import urlparse
from datetime import datetime from datetime import datetime
@@ -450,10 +452,10 @@ def _default_markdowns(lesson_name: str):
## 阶段一:示例阶段 ## 阶段一:示例阶段
### 步骤A ### 步骤A
请基于“步骤A”的教学目标与材料,生成指导性提示词(尽量结构化,给出评估标准和示例)。 请基于"步骤A"的教学目标与材料,生成指导性提示词(尽量结构化,给出评估标准和示例)。
### 步骤B ### 步骤B
请基于“步骤B”的教学目标与材料,生成指导性提示词。 请基于"步骤B"的教学目标与材料,生成指导性提示词。
""" """
score_prompt_md = f"""# {lesson_name} score_prompt_md = f"""# {lesson_name}
@@ -463,8 +465,134 @@ def _default_markdowns(lesson_name: str):
## 阶段一:示例阶段 ## 阶段一:示例阶段
### 步骤A ### 步骤A
对学员在“步骤A”的输出进行评分与文字反馈,分数区间 0-100并输出 JSON```{{"score": <int>, "reasons": "<string>", "advices": "<string>"}}``` 对学员在"步骤A"的输出进行评分与文字反馈,分数区间 0-100并输出 JSON```{{"score": <int>, "reasons": "<string>", "advices": "<string>"}}```
### 步骤B ### 步骤B
同上,对“步骤B”输出进行评分与反馈。 同上,对"步骤B"输出进行评分与反馈。
""" """
return lesson_md, prompt_md, score_prompt_md 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

View File

@@ -1,15 +1,19 @@
# myapp/sockets/namespaces.py # myapp/sockets/namespaces.py
import json import json
import time 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 flask_socketio import Namespace, join_room, leave_room, emit
from ..config import Config
from ..function_tools import judge from ..function_tools import judge
from ..function_tools import next_chapter from ..function_tools import next_chapter
from ..services.backboard_service import realtime_response 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
from ..extension_ase.ase_client import HSAEngineClient from ..extension_ase.ase_client import HSAEngineClient
from ..services.user_service import get_or_load_current_user from ..services.user_service import get_or_load_current_user
from ..services.course_service import load_learning_progress
@@ -71,6 +75,29 @@ class AgentNamespace(Namespace):
chatmanager = user_uuid2chatmanager[user_uuid] 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(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.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.chat_historys = []
chatmanager.scores = [] chatmanager.scores = []
@@ -79,6 +106,11 @@ class AgentNamespace(Namespace):
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) 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}') 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.bb = backboard_manager.get_backboard(user_uuid)
if not progress:
chatmanager.next_chapter()
# 如果有保存的进度,需要跳过已经完成的章节
for _ in range(need_skip_chapters):
chatmanager.next_chapter() chatmanager.next_chapter()
self.chatmanager = chatmanager self.chatmanager = chatmanager
self.ase_client = user_uuid2ase_client[user_uuid] self.ase_client = user_uuid2ase_client[user_uuid]
@@ -180,10 +212,28 @@ class AgentNamespace(Namespace):
print(f"on_connect_to_ase {client_entry}, {namespace_entry}, {init_data}") print(f"on_connect_to_ase {client_entry}, {namespace_entry}, {init_data}")
namespace_entry, ase_client = namespace_entry namespace_entry, ase_client = namespace_entry
namespace_entry.emit('open-iframe',"", room=init_data['room'], namespace='/agent') 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.chatmanager.load_next_chapter()
ase_client.send_text("chapter-start", "") ase_client.send_text("chapter-start", "")
ase_client.send_text("clear","") namespace_entry.emit('message', "教案加载完毕", room=init_data['room'], namespace='/agent')
namespace_entry.emit('message', "Multi-Agents服务已连接", room=init_data['room'], namespace='/agent')
#接受消息回来直接让message监听发送 #接受消息回来直接让message监听发送
def receive_ase_paste_detected(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data): def receive_ase_paste_detected(client_entry: HSAEngineClient, namespace_entry: Namespace, data, init_data):
@@ -221,9 +271,8 @@ class AgentNamespace(Namespace):
if len(chatmanager.scores) >= chatmanager.chapter_chain_now: if len(chatmanager.scores) >= chatmanager.chapter_chain_now:
chatmanager.scores[chatmanager.chapter_chain_now - 1] = data.get('data', None) chatmanager.scores[chatmanager.chapter_chain_now - 1] = data.get('data', None)
else: chatmanager.scores.append(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) user.set_course_progress(chatmanager.chat_historys, chatmanager.scores, chatmanager.course_id, chatmanager.chapter_name, chatmanager.lesson_name)
chatmanager.load_next_chapter() chatmanager.load_next_chapter()
time.sleep(0.5)
chatmanager.ase_client.send_text("chapter-start", "") chatmanager.ase_client.send_text("chapter-start", "")
@@ -241,10 +290,32 @@ class AgentNamespace(Namespace):
def on_disconnect(self,data): def on_disconnect(self,data):
print(f'AgentNamespace client disconnected, session user_uuid: {session.get("user_uuid", "unknown")}')
ex = current_app.extensions ex = current_app.extensions
uuid = session.get('user_uuid') uuid = session.get('user_uuid')
user_uuid2chatmanager = ex["user_uuid2chatmanager"] user_uuid2chatmanager = ex.get("user_uuid2chatmanager", {})
# 保存学习进度到MongoDB
try:
# 获取用户ID
uuid2username = ex.get("uuid2username", {})
user_id = uuid2username.get(uuid, "unknown")
# 调用保存学习进度功能
chatmanager = user_uuid2chatmanager.get(uuid)
if chatmanager:
save_result = save_learning_progress(uuid, chatmanager, user_id)
print(f"保存学习进度结果: {save_result}, user_uuid={uuid}")
except Exception as e:
print(f"保存学习进度时发生异常: {str(e)}")
# 执行原有的断开连接逻辑
if uuid in user_uuid2chatmanager:
user_uuid2chatmanager[uuid].disconnect(uuid) user_uuid2chatmanager[uuid].disconnect(uuid)
# 离开房间
if uuid:
leave_room(uuid, namespace='/agent')
print("VSCode client disconnected") print("VSCode client disconnected")
print("Disconnect reason:"+str(data)) print("Disconnect reason:"+str(data))

View File

@@ -105,29 +105,7 @@ document.addEventListener('DOMContentLoaded', function () {
document.getElementById('chatbox').appendChild(requestMessage); document.getElementById('chatbox').appendChild(requestMessage);
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; 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) { } catch (error) {
console.log(error) console.log(error)
} }
@@ -250,124 +228,9 @@ function sendMessage() {
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight; 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;
// }
// }
//语音这一块// //语音这一块//

View File

@@ -156,7 +156,6 @@ def load_chapters(markdown, markdown_prompt, score_prompt):
chapter_chain = [] chapter_chain = []
No = 1 No = 1
print (chapter_dict)
for chapter_name in chapter_sequence: for chapter_name in chapter_sequence:
chapter_chain.append(Chapter( No, CHAPTER_LATTER, chapter_name, chapter_dict[chapter_name]["markdown"], chapter_dict[chapter_name]["markdown_prompt"], chapter_dict[chapter_name]["score_prompt"],chapter_dict[chapter_name]["require_tools"])) chapter_chain.append(Chapter( No, CHAPTER_LATTER, chapter_name, chapter_dict[chapter_name]["markdown"], chapter_dict[chapter_name]["markdown_prompt"], chapter_dict[chapter_name]["score_prompt"],chapter_dict[chapter_name]["require_tools"]))
No+=1 No+=1

View File

@@ -43,13 +43,22 @@ class Backboard:
f"- File tree: {self.file_tree}{endl}") f"- File tree: {self.file_tree}{endl}")
# f"- Activated file content (current editing 10 lines): {endl}" # f"- Activated file content (current editing 10 lines): {endl}"
def add_history(self, realtime_action): def add_history(self, realtime_action):
if(realtime_action['type']=='config'):return # 检查必要的键是否存在避免KeyError
if len(self.history)!=0: if realtime_action.get('type') == 'config':
if (self.history[-1]['type']=='fileEdit'): return
if(self.history[-1]['filePath'] == realtime_action['filePath']): if len(self.history) > 0 and realtime_action.get('type') == 'fileEdit':
# 安全地获取filePath并进行比较
current_file_path = realtime_action.get('filePath')
# 检查最近的历史记录是否包含filePath
if current_file_path and self.history[-1].get('filePath') == current_file_path:
# 安全地更新content只有当content存在时才更新
if 'content' in realtime_action:
self.history[-1]['content'] = realtime_action['content'] self.history[-1]['content'] = realtime_action['content']
return return
self.history.append(realtime_action) self.history.append(realtime_action)
# 限制历史记录的数量
if len(self.history) > 500: # 假设最多保留500条记录
self.history.pop(0) # 移除最旧的记录
def get_deltatime_mmss(self, start_time, end_time): def get_deltatime_mmss(self, start_time, end_time):

View File

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

View File

@@ -148,12 +148,14 @@
}); });
// 防抖处理非粘贴的编辑操作 // 防抖处理非粘贴的编辑操作
const content = changes.text; // 确保始终使用完整的编辑器内容而非仅变更部分
const fullContent = editor.getValue();
debounceSendToServer({ debounceSendToServer({
type: 'fileEdit', type: 'fileEdit',
filePath: filePath, filePath: filePath,
content: content, content: fullContent,
config: code_like_config, config: code_like_config,
timestamp: new Date().getTime() // 添加时间戳以便追踪
}, 5000 }, 5000
); );
// saveFile // saveFile