refactor: replace file diff tools, simplify todos, disable typewriter

This commit is contained in:
JOJO 2026-01-29 14:20:01 +08:00
parent e5c2943cb2
commit 453df30f45
21 changed files with 281 additions and 654 deletions

File diff suppressed because one or more lines are too long

View File

@ -31,6 +31,8 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = {
label="文件编辑",
tools=[
"create_file",
"write_file",
"edit_file",
"append_to_file",
"modify_file",
"delete_file",
@ -42,8 +44,6 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = {
label="阅读聚焦",
tools=[
"read_file",
"focus_file",
"unfocus_file",
"vlm_analyze",
"ocr_image",
"view_image",
@ -69,7 +69,7 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = {
),
"todo": ToolCategory(
label="待办事项",
tools=["todo_create", "todo_update_task", "todo_finish", "todo_finish_confirm"],
tools=["todo_create", "todo_update_task"],
),
"sub_agent": ToolCategory(
label="子智能体",

View File

@ -92,7 +92,6 @@ class WebTerminal(MainTerminal):
# 设置token更新回调
if message_callback is not None:
self.context_manager._web_terminal_callback = message_callback
self.context_manager._focused_files = self.focused_files
print(f"[WebTerminal] 实时token统计已启用")
else:
print(f"[WebTerminal] 警告message_callback为None无法启用实时token统计")
@ -302,14 +301,8 @@ class WebTerminal(MainTerminal):
memory_stats = self.memory_manager.get_memory_stats()
structure = self.context_manager.get_project_structure()
# 聚焦文件状态 - 使用与 /api/focused 相同的格式(字典格式)
# 聚焦功能已废弃
focused_files_dict = {}
for path, content in self.focused_files.items():
focused_files_dict[path] = {
"content": content,
"size": len(content),
"lines": content.count('\n') + 1
}
# 终端状态
terminal_status = None
@ -333,8 +326,8 @@ class WebTerminal(MainTerminal):
"total_size": context_status['sizes']['total'],
"conversation_count": len(self.context_manager.conversation_history)
},
"focused_files": focused_files_dict, # 使用字典格式,与 /api/focused 一致
"focused_files_count": len(self.focused_files), # 单独提供计数
"focused_files": focused_files_dict,
"focused_files_count": 0,
"terminals": terminal_status,
"project": {
"total_files": structure['total_files'],
@ -363,18 +356,6 @@ class WebTerminal(MainTerminal):
"""获取思考模式状态描述"""
return "思考模式" if self.thinking_mode else "快速模式"
def get_focused_files_info(self) -> Dict:
"""获取聚焦文件信息用于WebSocket更新- 使用与 /api/focused 一致的格式"""
focused_files_dict = {}
for path, content in self.focused_files.items():
focused_files_dict[path] = {
"content": content,
"size": len(content),
"lines": content.count('\n') + 1
}
return focused_files_dict
def broadcast(self, event_type: str, data: Dict):
"""广播事件到WebSocket"""
if self.message_callback:
@ -426,18 +407,6 @@ class WebTerminal(MainTerminal):
'status': 'deleting',
'detail': f'删除文件: {arguments.get("path", "未知路径")}'
})
elif tool_name == "focus_file":
self.broadcast('tool_status', {
'tool': tool_name,
'status': 'focusing',
'detail': f'聚焦文件: {arguments.get("path", "未知路径")}'
})
elif tool_name == "unfocus_file":
self.broadcast('tool_status', {
'tool': tool_name,
'status': 'unfocusing',
'detail': f'取消聚焦: {arguments.get("path", "未知路径")}'
})
elif tool_name == "web_search":
query = arguments.get("query", "")
filters = []
@ -602,19 +571,6 @@ class WebTerminal(MainTerminal):
except Exception as e:
logger.error(f"广播文件树更新失败: {e}")
# 如果是聚焦操作,广播聚焦文件更新
if tool_name in ['focus_file', 'unfocus_file', 'modify_file']:
try:
focused_files_dict = self.get_focused_files_info()
self.broadcast('focused_files_update', focused_files_dict)
# 聚焦文件变化后更新token统计
self.context_manager.safe_broadcast_token_update()
except Exception as e:
logger.error(f"广播聚焦文件更新失败: {e}")
# 如果是记忆操作,广播记忆状态更新
if tool_name == 'update_memory':
try:

View File

@ -1129,7 +1129,7 @@ class FileManager:
return {
"success": False,
"error": "要替换的文本过长,可能导致性能问题",
"suggestion": "请拆分内容或使用 write_file_diff 提交结构化补丁"
"suggestion": "请拆分内容或使用 edit_file/逐块写入完成修改"
}
if new_text and len(new_text) > 9999999999:

View File

@ -28,7 +28,7 @@ except ImportError: # pragma: no cover
class TodoManager:
"""负责创建、更新和结束 TODO 列表"""
MAX_TASKS = TODO_MAX_TASKS
MAX_TASKS = 8 # 固定为8覆盖配置
MAX_OVERVIEW_LENGTH = TODO_MAX_OVERVIEW_LENGTH
MAX_TASK_LENGTH = TODO_MAX_TASK_LENGTH
@ -59,12 +59,8 @@ class TodoManager:
return normalized
def create_todo_list(self, overview: str, tasks: List[Any]) -> Dict[str, Any]:
current = self._get_current()
if current and current.get("status") == "active":
return {
"success": False,
"error": "已有进行中的 TODO 列表,请先完成或结束后再创建新的列表。"
}
# 若已有列表,直接覆盖
current = None
overview = (overview or "").strip()
if not overview:
@ -109,7 +105,7 @@ class TodoManager:
self._save(todo)
return {
"success": True,
"message": "待办列表已创建。请先完成某项任务,再调用待办工具将其标记完成",
"message": "待办列表已创建(覆盖之前的列表)",
"todo_list": todo
}
@ -127,91 +123,20 @@ class TodoManager:
task = todo["tasks"][task_index - 1]
new_status = "done" if completed else "pending"
if task["status"] == new_status:
return {
"success": True,
"message": "任务状态未发生变化。",
"todo_list": todo
}
task["status"] = new_status
self._save(todo)
return {
"success": True,
"message": f"任务 task{task_index} 已标记为 {'完成' if completed else '未完成'}",
"todo_list": todo
}
def finish_todo(self, reason: Optional[str] = None) -> Dict[str, Any]:
todo = self._get_current()
if not todo:
return {"success": False, "error": "当前没有待办列表。"}
if todo.get("status") in {"completed", "closed"}:
return {
"success": True,
"message": "待办列表已结束,无需重复操作。",
"todo_list": todo
}
all_done = all(task["status"] == "done" for task in todo["tasks"])
all_done = all(t["status"] == "done" for t in todo["tasks"])
if all_done:
todo["status"] = "completed"
todo["forced_finish"] = False
todo["forced_reason"] = None
self._save(todo)
system_note = "✅ TODO 列表中的所有任务已完成,可以整理成果并向用户汇报,如果已经进行过汇报,请忽略这条信息。"
return {
"success": True,
"message": "所有任务已完成,待办列表已结束。",
"todo_list": todo,
"system_note": system_note
}
remaining = [
f"task{task['index']}"
for task in todo["tasks"]
if task["status"] != "done"
]
return {
"success": False,
"requires_confirmation": True,
"message": "仍有未完成的任务,确认要提前结束吗?",
"remaining": remaining,
"todo_list": todo
}
def confirm_finish(self, confirm: bool, reason: Optional[str] = None) -> Dict[str, Any]:
todo = self._get_current()
if not todo:
return {"success": False, "error": "当前没有待办列表。"}
if todo.get("status") in {"completed", "closed"}:
return {
"success": True,
"message": "待办列表已结束,无需重复操作。",
"message": "所有任务已完成。",
"todo_list": todo
}
if not confirm:
return {
"success": True,
"message": "已取消结束待办列表,继续执行剩余任务。",
"todo_list": todo
}
todo["status"] = "closed"
todo["forced_finish"] = True
todo["forced_reason"] = (reason or "").strip() or None
self._save(todo)
system_note = "⚠️ TODO 列表在任务未全部完成的情况下被结束,请在总结中说明原因。"
self.context_manager.add_conversation("system", system_note)
return {
"success": True,
"message": "待办列表已强制结束。",
"todo_list": todo,
"system_note": system_note
"message": f"任务 {task_index}{'完成' if completed else '取消完成'}",
"todo_list": todo
}
def get_snapshot(self) -> Optional[Dict[str, Any]]:

View File

@ -194,12 +194,11 @@ tree -L 2
1. **什么时候用**任务需要2步以上、涉及多个文件或工具时
2. **清单要求**
- 概述用一句话说明任务目标不超过50字
- 任务:最多4条,按执行顺序排列
- 任务:最多8条建议2-6条,按执行顺序排列
- 每条任务要说清楚具体做什么,不要用"优化""处理"这种模糊词
3. **执行方式**
- 完成一项,勾选一项
- 如果计划有变,先告诉用户
- 全部完成后,用 todo_finish 结束
- 完成/撤销一项,立即用 todo_update_task 勾选/取消
- 如果计划有变,先告诉用户,并更新任务后继续勾选
### 示例:整理文档
```

View File

@ -208,12 +208,11 @@ tree -L 2
1. **什么时候用**任务需要2步以上、涉及多个文件或工具时
2. **清单要求**
- 概述用一句话说明任务目标不超过50字
- 任务:最多4条,按执行顺序排列
- 任务:最多8条建议2-6条,按执行顺序排列
- 每条任务要说清楚具体做什么,不要用"优化""处理"这种模糊词
3. **执行方式**
- 完成一项,勾选一项
- 如果计划有变,先告诉用户
- 全部完成后,用 todo_finish 结束
- 完成/撤销一项,立即用 todo_update_task 勾选/取消
- 如果计划有变,先告诉用户,并更新任务后继续勾选
### 示例:整理文档
```

View File

@ -24,8 +24,8 @@
- ✅ "整理家庭照片按年份分类并压缩不超过2GB"
- ❌ "处理照片"(太模糊)
### 2. 任务列表(最多4条)
- **数量**建议2-4条最多不超过4
### 2. 任务列表(最多8条)
- **数量**建议2-6条最多不超过8
- **顺序**:按照实际操作顺序排列
- **要求**:每条任务要说清楚具体做什么
@ -68,9 +68,9 @@
- 完成一项任务后,立即调用 `todo_update_task` 勾选
- 如果发现计划需要调整,先告诉用户,再修改
### 第4步结束清单
- 全部完成:直接调用 `todo_finish`
- 中途需要停止:说明原因,询问是否结束
### 第4步持续更新
- 每完成/撤销一项,调用 `todo_update_task` 勾选/取消勾选
- 如需修改任务描述或顺序,先向用户说明后直接更新清单并同步勾选状态
## 常见场景示例
@ -124,35 +124,12 @@
- 不要跳过步骤(按顺序执行)
- 不要忘记勾选已完成的任务
## 如果任务未完成就要结束
有时候会遇到:
- 缺少必要信息,无法继续
- 发现技术限制,做不了
- 用户改变想法,不做了
**正确做法**
1. 调用 `todo_finish` 尝试结束
2. 系统会提示有未完成任务
3. 调用 `todo_finish_confirm` 并说明原因
4. 告诉用户哪些完成了,哪些没做
**例子**
```
"由于xxx文件找不到任务2无法执行。
已完成任务1读取文件
未完成任务2-4
是否结束当前清单?"
```
## 快速参考
| 工具 | 用途 | 什么时候用 |
|-----|------|---------|
| todo_create | 创建清单 | 开始多步骤任务时 |
| todo_update_task | 勾选任务 | 每完成一项任务后 |
| todo_finish | 结束清单 | 全部任务完成时 |
| todo_finish_confirm | 确认提前结束 | 有未完成任务但需要停止时 |
| todo_create | 创建清单最多8条覆盖旧清单 | 开始多步骤任务时 |
| todo_update_task | 勾选/取消勾选 | 每完成或撤销一项时 |
## 总结
@ -165,4 +142,3 @@
6. **灵活调整**:发现问题及时沟通
记住:清单是给你自己看的,要给自己明确可执行的规划,同时要让用户知道你在做什么、完成到哪一步了。在用户明确给出“好的,请开始”的指令时,才能开始创建待办事项哦!

View File

@ -236,15 +236,8 @@ def get_monitor_snapshot_api():
@api_login_required
@with_terminal
def get_focused_files(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取聚焦文件"""
focused = {}
for path, content in terminal.focused_files.items():
focused[path] = {
"content": content,
"size": len(content),
"lines": content.count('\n') + 1
}
return jsonify(focused)
"""聚焦功能已废弃,返回空列表保持接口兼容。"""
return jsonify({})
@app.route('/api/todo-list')
@api_login_required
@ -553,4 +546,3 @@ def issue_socket_token():
"expires_in": SOCKET_TOKEN_TTL_SECONDS
})

View File

@ -249,15 +249,8 @@ def get_monitor_snapshot_api():
@api_login_required
@with_terminal
def get_focused_files(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取聚焦文件"""
focused = {}
for path, content in terminal.focused_files.items():
focused[path] = {
"content": content,
"size": len(content),
"lines": content.count('\n') + 1
}
return jsonify(focused)
"""聚焦功能已废弃,返回空列表保持接口兼容。"""
return jsonify({})
@app.route('/api/todo-list')
@api_login_required
@ -566,4 +559,3 @@ def issue_socket_token():
"expires_in": SOCKET_TOKEN_TTL_SECONDS
})

View File

@ -627,10 +627,10 @@ def detect_malformed_tool_call(text):
return True
# 检测特定的工具名称后跟JSON
tool_names = ['create_file', 'read_file', 'write_file_diff', 'delete_file',
tool_names = ['create_file', 'read_file', 'write_file', 'edit_file', 'delete_file',
'terminal_session', 'terminal_input', 'web_search',
'extract_webpage', 'save_webpage',
'run_python', 'run_command', 'focus_file', 'unfocus_file', 'sleep']
'run_python', 'run_command', 'sleep']
for tool in tool_names:
if tool in text and '{' in text:
# 可能是工具调用但格式错误
@ -943,13 +943,6 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
'message': summary
})
# 更新聚焦文件内容
if path in web_terminal.focused_files:
refreshed = web_terminal.file_manager.read_file(path)
if refreshed.get("success"):
web_terminal.focused_files[path] = refreshed["content"]
debug_log(f"聚焦文件已刷新: {path}")
debug_log(f"追加写入完成: {summary}")
else:
error_msg = write_result.get("error", "追加写入失败")
@ -1295,12 +1288,6 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
'message': summary_message
})
if path in web_terminal.focused_files and tool_payload.get("success"):
refreshed = web_terminal.file_manager.read_file(path)
if refreshed.get("success"):
web_terminal.focused_files[path] = refreshed["content"]
debug_log(f"聚焦文件已刷新: {path}")
pending_modify = None
modify_probe_buffer = ""
if hasattr(web_terminal, "pending_modify_request"):
@ -2297,12 +2284,12 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
action_message = None
awaiting_flag = False
if function_name == "write_file_diff":
diff_path = result_data.get("path") or arguments.get("path")
if function_name in {"write_file", "edit_file"}:
diff_path = result_data.get("path") or arguments.get("file_path")
summary = result_data.get("summary") or result_data.get("message")
if summary:
action_message = summary
debug_log(f"write_file_diff 执行完成: {summary or '无摘要'}")
debug_log(f"{function_name} 执行完成: {summary or '无摘要'}")
if function_name == "wait_sub_agent":
system_msg = result_data.get("system_message")
@ -2363,10 +2350,6 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
sender('update_action', update_payload)
# 更新UI状态
if function_name in ['focus_file', 'unfocus_file', 'write_file_diff']:
sender('focused_files_update', web_terminal.get_focused_files_info())
if function_name in ['create_file', 'delete_file', 'rename_file', 'create_folder']:
structure = web_terminal.context_manager.get_project_structure()
sender('file_tree_update', structure)
@ -2399,10 +2382,6 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
if system_message:
web_terminal._record_sub_agent_message(system_message, result_data.get("task_id"), inline=False)
maybe_mark_failure_from_message(web_terminal, system_message)
todo_note = result_data.get("system_note") if isinstance(result_data, dict) else None
if todo_note:
web_terminal.context_manager.add_conversation("system", todo_note)
maybe_mark_failure_from_message(web_terminal, todo_note)
# 添加到消息历史用于API继续对话
messages.append({
@ -2412,7 +2391,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
"content": tool_result_content
})
# 处理图片注入:必须紧跟在对应的 tool 消息之后,且工具成功时才插入
# 收集图片注入请求,延后统一追加
if (
function_name == "view_image"
and getattr(web_terminal, "pending_image_view", None)
@ -2421,30 +2400,10 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
):
inj = web_terminal.pending_image_view
web_terminal.pending_image_view = None
injected_text = "这是一条系统控制发送的信息,并非用户主动发送,目的是返回你需要查看的图片。"
# 记录到对话历史
web_terminal.context_manager.add_conversation(
"user",
injected_text,
images=[inj["path"]],
metadata={"system_injected_image": True}
)
# 同步到当前消息列表(直接带多模态 content保证顺序为 tool_call -> tool -> (系统代发)user
content_payload = web_terminal.context_manager._build_content_with_images(
injected_text,
[inj["path"]]
)
messages.append({
"role": "user",
"content": content_payload,
"metadata": {"system_injected_image": True}
})
# 提示前端
sender('system_message', {
'content': f'系统已按模型请求插入图片: {inj.get("path")}'
})
if inj and inj.get("path"):
image_injections.append(inj["path"])
if function_name != 'write_file_diff':
if function_name not in {'write_file', 'edit_file'}:
await process_sub_agent_updates(messages, inline=True, after_tool_call_id=tool_call_id)
await asyncio.sleep(0.2)
@ -2455,6 +2414,29 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
# 标记不再是第一次迭代
is_first_iteration = False
# 统一附加图片消息,保证所有 tool 响应先完成
if image_injections:
for img_path in image_injections:
injected_text = "这是一条系统控制发送的信息,并非用户主动发送,目的是返回你需要查看的图片。"
web_terminal.context_manager.add_conversation(
"user",
injected_text,
images=[img_path],
metadata={"system_injected_image": True}
)
content_payload = web_terminal.context_manager._build_content_with_images(
injected_text,
[img_path]
)
messages.append({
"role": "user",
"content": content_payload,
"metadata": {"system_injected_image": True}
})
sender('system_message', {
'content': f'系统已按模型请求插入图片: {img_path}'
})
# 最终统计
debug_log(f"\n{'='*40}")
debug_log(f"任务完成统计:")

View File

@ -6,7 +6,7 @@ terminal_rooms: Dict[str, set] = {}
connection_users: Dict[str, str] = {}
stop_flags: Dict[str, Dict[str, Any]] = {}
MONITOR_FILE_TOOLS = {'append_to_file', 'modify_file', 'write_file_diff'}
MONITOR_FILE_TOOLS = {'append_to_file', 'modify_file', 'write_file', 'edit_file'}
MONITOR_MEMORY_TOOLS = {'update_memory'}
MONITOR_SNAPSHOT_CHAR_LIMIT = 60000
MONITOR_MEMORY_ENTRY_LIMIT = 256
@ -23,4 +23,3 @@ ADMIN_CUSTOM_TOOLS_DIR = (Path(app.static_folder) / 'custom_tools').resolve()
ADMIN_CUSTOM_TOOLS_DIR = (Path(app.static_folder) / 'custom_tools').resolve()
RECENT_UPLOAD_EVENT_LIMIT = 150
RECENT_UPLOAD_FEED_LIMIT = 60

View File

@ -274,15 +274,8 @@ def get_monitor_snapshot_api():
@api_login_required
@with_terminal
def get_focused_files(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取聚焦文件"""
focused = {}
for path, content in terminal.focused_files.items():
focused[path] = {
"content": content,
"size": len(content),
"lines": content.count('\n') + 1
}
return jsonify(focused)
"""聚焦功能已废弃,返回空列表保持接口兼容。"""
return jsonify({})
@chat_bp.route('/api/todo-list')
@api_login_required

View File

@ -448,10 +448,10 @@ def detect_malformed_tool_call(text):
return True
# 检测特定的工具名称后跟JSON
tool_names = ['create_file', 'read_file', 'write_file_diff', 'delete_file',
tool_names = ['create_file', 'read_file', 'write_file', 'edit_file', 'delete_file',
'terminal_session', 'terminal_input', 'web_search',
'extract_webpage', 'save_webpage',
'run_python', 'run_command', 'focus_file', 'unfocus_file', 'sleep']
'run_python', 'run_command', 'sleep']
for tool in tool_names:
if tool in text and '{' in text:
# 可能是工具调用但格式错误
@ -764,13 +764,6 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
'message': summary
})
# 更新聚焦文件内容
if path in web_terminal.focused_files:
refreshed = web_terminal.file_manager.read_file(path)
if refreshed.get("success"):
web_terminal.focused_files[path] = refreshed["content"]
debug_log(f"聚焦文件已刷新: {path}")
debug_log(f"追加写入完成: {summary}")
else:
error_msg = write_result.get("error", "追加写入失败")
@ -1116,12 +1109,6 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
'message': summary_message
})
if path in web_terminal.focused_files and tool_payload.get("success"):
refreshed = web_terminal.file_manager.read_file(path)
if refreshed.get("success"):
web_terminal.focused_files[path] = refreshed["content"]
debug_log(f"聚焦文件已刷新: {path}")
pending_modify = None
modify_probe_buffer = ""
if hasattr(web_terminal, "pending_modify_request"):
@ -1928,6 +1915,8 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
# 更新统计
total_tool_calls += len(tool_calls)
image_injections: list[str] = []
# 执行每个工具
for tool_call in tool_calls:
# 检查停止标志
@ -2118,12 +2107,12 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
action_message = None
awaiting_flag = False
if function_name == "write_file_diff":
diff_path = result_data.get("path") or arguments.get("path")
if function_name in {"write_file", "edit_file"}:
diff_path = result_data.get("path") or arguments.get("file_path")
summary = result_data.get("summary") or result_data.get("message")
if summary:
action_message = summary
debug_log(f"write_file_diff 执行完成: {summary or '无摘要'}")
debug_log(f"{function_name} 执行完成: {summary or '无摘要'}")
if function_name == "wait_sub_agent":
system_msg = result_data.get("system_message")
@ -2184,10 +2173,6 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
sender('update_action', update_payload)
# 更新UI状态
if function_name in ['focus_file', 'unfocus_file', 'write_file_diff']:
sender('focused_files_update', web_terminal.get_focused_files_info())
if function_name in ['create_file', 'delete_file', 'rename_file', 'create_folder']:
structure = web_terminal.context_manager.get_project_structure()
sender('file_tree_update', structure)
@ -2220,10 +2205,6 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
if system_message:
web_terminal._record_sub_agent_message(system_message, result_data.get("task_id"), inline=False)
maybe_mark_failure_from_message(web_terminal, system_message)
todo_note = result_data.get("system_note") if isinstance(result_data, dict) else None
if todo_note:
web_terminal.context_manager.add_conversation("system", todo_note)
maybe_mark_failure_from_message(web_terminal, todo_note)
# 添加到消息历史用于API继续对话
messages.append({
@ -2233,7 +2214,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
"content": tool_result_content
})
# 处理图片注入:必须紧跟在对应的 tool 消息之后,且工具成功时才插入
# 收集图片注入请求,延后统一追加
if (
function_name == "view_image"
and getattr(web_terminal, "pending_image_view", None)
@ -2242,30 +2223,10 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
):
inj = web_terminal.pending_image_view
web_terminal.pending_image_view = None
injected_text = "这是一条系统控制发送的信息,并非用户主动发送,目的是返回你需要查看的图片。"
# 记录到对话历史
web_terminal.context_manager.add_conversation(
"user",
injected_text,
images=[inj["path"]],
metadata={"system_injected_image": True}
)
# 同步到当前消息列表(直接带多模态 content保证顺序为 tool_call -> tool -> (系统代发)user
content_payload = web_terminal.context_manager._build_content_with_images(
injected_text,
[inj["path"]]
)
messages.append({
"role": "user",
"content": content_payload,
"metadata": {"system_injected_image": True}
})
# 提示前端
sender('system_message', {
'content': f'系统已按模型请求插入图片: {inj.get("path")}'
})
if inj and inj.get("path"):
image_injections.append(inj["path"])
if function_name != 'write_file_diff':
if function_name not in {'write_file', 'edit_file'}:
await process_sub_agent_updates(messages, inline=True, after_tool_call_id=tool_call_id)
await asyncio.sleep(0.2)
@ -2275,6 +2236,29 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
# 标记不再是第一次迭代
is_first_iteration = False
# 统一附加图片消息,保证所有 tool 响应先完成
if image_injections:
for img_path in image_injections:
injected_text = "这是一条系统控制发送的信息,并非用户主动发送,目的是返回你需要查看的图片。"
web_terminal.context_manager.add_conversation(
"user",
injected_text,
images=[img_path],
metadata={"system_injected_image": True}
)
content_payload = web_terminal.context_manager._build_content_with_images(
injected_text,
[img_path]
)
messages.append({
"role": "user",
"content": content_payload,
"metadata": {"system_injected_image": True}
})
sender('system_message', {
'content': f'系统已按模型请求插入图片: {img_path}'
})
# 最终统计
debug_log(f"\n{'='*40}")

View File

@ -27,7 +27,7 @@ RECENT_UPLOAD_FEED_LIMIT = 60
stop_flags: Dict[str, Dict[str, Any]] = {}
# 监控/限流/用量
MONITOR_FILE_TOOLS = {'append_to_file', 'modify_file', 'write_file_diff'}
MONITOR_FILE_TOOLS = {'append_to_file', 'modify_file', 'write_file', 'edit_file'}
MONITOR_MEMORY_TOOLS = {'update_memory'}
MONITOR_SNAPSHOT_CHAR_LIMIT = 60000
MONITOR_MEMORY_ENTRY_LIMIT = 256

View File

@ -21,6 +21,8 @@ export async function initializeLegacySocket(ctx: any) {
ctx.socket = createSocketClient('/', socketOptions);
// 可开关的逐字打印功能,默认关闭
const STREAMING_ENABLED = false;
const STREAMING_CHAR_DELAY = 22;
const STREAMING_FINALIZE_DELAY = 1000;
const STREAMING_DEBUG = false;
@ -109,10 +111,11 @@ export async function initializeLegacySocket(ctx: any) {
};
const hasPendingStreamingText = () =>
!!streamingState.buffer.length ||
!!streamingState.pendingCompleteContent ||
streamingState.timer !== null ||
streamingState.completionTimer !== null;
STREAMING_ENABLED &&
( !!streamingState.buffer.length ||
!!streamingState.pendingCompleteContent ||
streamingState.timer !== null ||
streamingState.completionTimer !== null);
const resetPendingToolEvents = () => {
pendingToolEvents.length = 0;
@ -513,6 +516,14 @@ export async function initializeLegacySocket(ctx: any) {
return;
}
stopCompletionTimer();
if (!STREAMING_ENABLED) {
// 关闭逐字模式:整块入缓冲并立即刷新,保持与 chunk 顺序一致
streamingState.pendingCompleteContent = '';
streamingState.buffer.length = 0;
streamingState.apiCompleted = false;
applyTextChunk(text);
return;
}
logStreamingDebug('enqueueStreamingContent', { incomingLength: text.length });
for (const ch of Array.from(text)) {
streamingState.buffer.push(ch);
@ -931,7 +942,12 @@ export async function initializeLegacySocket(ctx: any) {
console.warn('上报chunk日志失败:', error);
}
if (data && typeof data.content === 'string' && data.content.length) {
enqueueStreamingContent(data.content);
if (STREAMING_ENABLED) {
enqueueStreamingContent(data.content);
} else {
// 关闭逐字模式时直接追加当前chunk
applyTextChunk(data.content);
}
const speech = sanitizeBubbleText(data.content);
if (speech) {
ctx.monitorShowSpeech(speech);
@ -946,6 +962,22 @@ export async function initializeLegacySocket(ctx: any) {
finalLength: (data?.full_content || '').length,
snapshot: snapshotStreamingState()
});
if (!STREAMING_ENABLED) {
// 已逐块追加,这里保证动作收尾
const full = data?.full_content || '';
if (full) {
ctx.chatCompleteTextAction(full);
} else {
ctx.chatCompleteTextAction('');
}
streamingState.buffer.length = 0;
streamingState.pendingCompleteContent = '';
streamingState.renderedText = '';
streamingState.apiCompleted = false;
ctx.monitorEndModelOutput();
flushPendingToolEvents();
return;
}
streamingState.apiCompleted = true;
streamingState.pendingCompleteContent = data?.full_content || '';
const hidden = typeof document !== 'undefined' && document.hidden === true;

View File

@ -94,8 +94,6 @@ const TOOL_SCENE_MAP: Record<string, string> = {
extract_webpage: 'webExtract',
save_webpage: 'webSave',
read_file: 'reader',
focus_file: 'focus',
unfocus_file: 'unfocus',
vlm_analyze: 'ocr',
ocr_image: 'ocr',
create_folder: 'createFolder',
@ -104,7 +102,8 @@ const TOOL_SCENE_MAP: Record<string, string> = {
delete_file: 'deleteFile',
append_to_file: 'appendFile',
modify_file: 'modifyFile',
write_file_diff: 'modifyFile',
write_file: 'modifyFile',
edit_file: 'modifyFile',
run_command: 'runCommand',
run_python: 'runPython',
terminal_session: 'terminalSession',
@ -115,8 +114,6 @@ const TOOL_SCENE_MAP: Record<string, string> = {
update_memory: 'memoryUpdate',
todo_create: 'todoCreate',
todo_update_task: 'todoUpdate',
todo_finish: 'todoFinish',
todo_finish_confirm: 'todoFinishConfirm',
todo_delete_task: 'todoDelete',
terminal_run: 'runCommand'
};

View File

@ -7,11 +7,11 @@ const RUNNING_ANIMATIONS: Record<string, string> = {
read_file: 'read-animation',
delete_file: 'file-animation',
rename_file: 'file-animation',
write_file: 'file-animation',
edit_file: 'file-animation',
modify_file: 'file-animation',
append_to_file: 'file-animation',
create_folder: 'file-animation',
focus_file: 'focus-animation',
unfocus_file: 'focus-animation',
web_search: 'search-animation',
extract_webpage: 'search-animation',
save_webpage: 'file-animation',
@ -26,8 +26,6 @@ const RUNNING_ANIMATIONS: Record<string, string> = {
terminal_reset: 'terminal-animation',
todo_create: 'file-animation',
todo_update_task: 'file-animation',
todo_finish: 'file-animation',
todo_finish_confirm: 'file-animation',
create_sub_agent: 'terminal-animation',
wait_sub_agent: 'wait-animation'
};
@ -37,11 +35,11 @@ const RUNNING_STATUS_TEXTS: Record<string, string> = {
sleep: '正在等待...',
delete_file: '正在删除文件...',
rename_file: '正在重命名文件...',
write_file: '正在写入文件...',
edit_file: '正在编辑文件...',
modify_file: '正在修改文件...',
append_to_file: '正在追加文件...',
create_folder: '正在创建文件夹...',
focus_file: '正在聚焦文件...',
unfocus_file: '正在取消聚焦...',
web_search: '正在搜索网络...',
extract_webpage: '正在提取网页...',
save_webpage: '正在保存网页...',
@ -59,11 +57,11 @@ const COMPLETED_STATUS_TEXTS: Record<string, string> = {
delete_file: '文件删除成功',
sleep: '等待完成',
rename_file: '文件重命名成功',
write_file: '文件写入完成',
edit_file: '文件编辑完成',
modify_file: '文件修改成功',
append_to_file: '文件追加完成',
create_folder: '文件夹创建成功',
focus_file: '文件聚焦成功',
unfocus_file: '取消聚焦成功',
web_search: '搜索完成',
extract_webpage: '网页提取完成',
save_webpage: '网页保存完成(纯文本)',

View File

@ -48,9 +48,9 @@ export const TOOL_ICON_MAP = Object.freeze({
create_sub_agent: 'bot',
delete_file: 'trash',
extract_webpage: 'globe',
focus_file: 'eye',
modify_file: 'pencil',
write_file_diff: 'pencil',
write_file: 'pencil',
edit_file: 'pencil',
vlm_analyze: 'camera',
ocr_image: 'camera',
read_file: 'book',
@ -60,8 +60,6 @@ export const TOOL_ICON_MAP = Object.freeze({
save_webpage: 'save',
sleep: 'clock',
todo_create: 'stickyNote',
todo_finish: 'flag',
todo_finish_confirm: 'circleAlert',
todo_update_task: 'check',
terminal_input: 'keyboard',
terminal_reset: 'recycle',

View File

@ -63,7 +63,6 @@ class ContextManager:
# 用于接收Web终端的回调函数
self._web_terminal_callback = None
self._focused_files = {}
self.load_annotations()
@ -121,10 +120,6 @@ class ContextManager:
"""设置Web终端回调函数用于广播事件"""
self._web_terminal_callback = callback
def set_focused_files(self, focused_files: Dict):
"""设置聚焦文件信息用于token计算"""
self._focused_files = focused_files
def load_annotations(self):
"""加载文件备注"""
annotations_file = self.data_dir / "file_annotations.json"
@ -532,10 +527,10 @@ class ContextManager:
if ("<<<APPEND" in content) or metadata.get("append_payload"):
new_msg["content"] = append_placeholder
compressed_types.add("write_file_diff")
compressed_types.add("write_file")
elif ("<<<MODIFY" in content) or metadata.get("modify_payload"):
new_msg["content"] = append_placeholder
compressed_types.add("write_file_diff")
compressed_types.add("edit_file")
elif role == "tool":
tool_name = new_msg.get("name")
@ -543,9 +538,9 @@ class ContextManager:
if tool_name == "read_file":
new_msg["content"] = append_placeholder
compressed_types.add("read_file")
elif tool_name == "write_file_diff":
elif tool_name in {"write_file", "edit_file"}:
new_msg["content"] = append_placeholder
compressed_types.add("write_file_diff")
compressed_types.add(tool_name)
else:
payload = None
@ -580,12 +575,13 @@ class ContextManager:
}
type_labels = {
"write_file_diff": "文件补丁输出",
"write_file": "文件写入内容",
"edit_file": "文件精确替换",
"extract_webpage": "网页提取内容",
"read_file": "文件读取内容"
}
ordered_types = [type_labels[t] for t in ["write_file_diff", "extract_webpage", "read_file"] if t in compressed_types]
ordered_types = [type_labels[t] for t in ["write_file", "edit_file", "extract_webpage", "read_file"] if t in compressed_types]
summary_text = "系统提示:对话已压缩,之前的对话记录中的以下内容被替换为占位文本:" + "".join(ordered_types) + "。其他内容不受影响,如需查看原文,请查看对应文件或重新执行相关工具。"
system_message = {
@ -1318,22 +1314,6 @@ class ContextManager:
"content": content_payload
})
# 添加聚焦文件内容
if self._focused_files:
focused_content = "\n\n=== 🔍 正在聚焦的文件 ===\n"
focused_content += f"(共 {len(self._focused_files)} 个文件处于聚焦状态)\n"
for path, content in self._focused_files.items():
size_kb = len(content) / 1024
focused_content += f"\n--- 文件: {path} ({size_kb:.1f}KB) ---\n"
focused_content += f"```\n{content}\n```\n"
focused_content += "\n=== 聚焦文件结束 ===\n"
messages.append({
"role": "system",
"content": focused_content
})
# 添加终端内容(如果有的话)
# 这里需要从参数传入或获取

View File

@ -196,6 +196,33 @@ def _format_create_file(result_data: Dict[str, Any]) -> str:
return _format_failure("create_file", result_data)
return result_data.get("message") or f"已创建空文件: {result_data.get('path', '未知路径')}"
def _format_write_file(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("write_file", result_data)
path = result_data.get("path") or "未知路径"
mode = str(result_data.get("mode") or "w")
action = "追加" if mode == "a" else "覆盖"
size = result_data.get("size")
message = result_data.get("message")
parts = [f"{action}写入: {path}"]
if size is not None:
parts.append(f"{size} 字节")
if message:
parts.append(str(message))
return "".join(parts)
def _format_edit_file(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("edit_file", result_data)
path = result_data.get("path") or "目标文件"
count = result_data.get("replacements")
msg = result_data.get("message")
replaced_note = f"替换 {count}" if isinstance(count, int) else "已完成替换"
parts = [f"{replaced_note}: {path}"]
if msg:
parts.append(str(msg))
return "".join(parts)
def _classify_diff_block_issue(block: Dict[str, Any], result_data: Dict[str, Any]) -> str:
"""
@ -257,25 +284,6 @@ def _format_create_folder(result_data: Dict[str, Any]) -> str:
return f"已创建文件夹: {result_data.get('path', '未知路径')}"
def _format_focus_file(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("focus_file", result_data)
message = result_data.get("message") or "文件已聚焦"
size = result_data.get("file_size")
size_note = f"{size} 字符)" if isinstance(size, int) else ""
focused = result_data.get("focused_files") or []
focused_note = f"当前聚焦: {', '.join(focused)}" if focused else "当前没有其他聚焦文件"
return f"{message}{size_note}\n{focused_note}"
def _format_unfocus_file(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("unfocus_file", result_data)
message = result_data.get("message") or "已取消聚焦"
remaining = result_data.get("remaining_focused") or []
remain_note = f"剩余聚焦: {', '.join(remaining)}" if remaining else "当前没有聚焦文件"
return f"{message}\n{remain_note}"
def _format_terminal_session(result_data: Dict[str, Any]) -> str:
action = result_data.get("action") or result_data.get("terminal_action") or "未知操作"
@ -417,51 +425,6 @@ def _format_todo_update_task(result_data: Dict[str, Any]) -> str:
return f"{message}{progress_note}".strip("")
def _format_todo_finish(result_data: Dict[str, Any]) -> str:
if result_data.get("success"):
todo = result_data.get("todo_list") or {}
tasks = todo.get("tasks") or []
overview = todo.get("overview") or "未命名任务"
total = len(tasks)
done = sum(1 for t in tasks if t.get("status") == "done")
status_note = "正常结束" if done == total else "已结束"
lines = [
f"TODO 已结束({status_note}),进度 {done}/{total}",
f"概述:{overview}",
]
if tasks:
lines.append(_summarize_todo_tasks(todo))
return "\n".join(lines)
if result_data.get("requires_confirmation"):
remaining = result_data.get("remaining") or []
remain_note = ", ".join(remaining) if remaining else "未知"
return f"仍有未完成任务({remain_note}),需要确认是否提前结束。"
return _format_failure("todo_finish", result_data)
def _format_todo_finish_confirm(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("todo_finish_confirm", result_data)
message = result_data.get("message") or "已处理 TODO 完结确认"
todo = result_data.get("todo_list") or {}
overview = todo.get("overview") or "未命名任务"
tasks = todo.get("tasks") or []
total = len(tasks)
done = sum(1 for t in tasks if t.get("status") == "done")
forced = todo.get("forced_finish")
reason = todo.get("forced_reason")
status_note = "强制结束" if forced else "已结束"
lines = [
f"{message}{status_note},进度 {done}/{total}",
f"概述:{overview}",
]
if forced and reason:
lines.append(f"强制结束原因:{reason}")
if tasks:
lines.append(_summarize_todo_tasks(todo))
return "\n".join(lines)
def _format_update_memory(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("update_memory", result_data)
@ -667,11 +630,11 @@ def _summarize_todo_tasks(todo: Optional[Dict[str, Any]]) -> str:
TOOL_FORMATTERS = {
"create_file": _format_create_file,
"write_file": _format_write_file,
"edit_file": _format_edit_file,
"delete_file": _format_delete_file,
"rename_file": _format_rename_file,
"create_folder": _format_create_folder,
"focus_file": _format_focus_file,
"unfocus_file": _format_unfocus_file,
"terminal_snapshot": _format_terminal_snapshot,
"terminal_session": _format_terminal_session,
"terminal_input": _format_terminal_input,
@ -684,8 +647,6 @@ TOOL_FORMATTERS = {
"trigger_easter_egg": _format_trigger_easter_egg,
"todo_create": _format_todo_create,
"todo_update_task": _format_todo_update_task,
"todo_finish": _format_todo_finish,
"todo_finish_confirm": _format_todo_finish_confirm,
"update_memory": _format_update_memory,
"create_sub_agent": _format_create_sub_agent,
"wait_sub_agent": _format_wait_sub_agent,