diff --git a/core/main_terminal.py b/core/main_terminal.py index 9160e38..a3f5a18 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -15,7 +15,7 @@ try: READ_TOOL_DEFAULT_CONTEXT_BEFORE, READ_TOOL_DEFAULT_CONTEXT_AFTER, READ_TOOL_MAX_CONTEXT_BEFORE, READ_TOOL_MAX_CONTEXT_AFTER, READ_TOOL_DEFAULT_MAX_MATCHES, READ_TOOL_MAX_MATCHES, - READ_TOOL_MAX_FILE_SIZE, MAX_FOCUS_FILE_CHARS, + READ_TOOL_MAX_FILE_SIZE, TERMINAL_SANDBOX_MOUNT_PATH, TERMINAL_SANDBOX_MODE, TERMINAL_SANDBOX_CPUS, @@ -36,7 +36,7 @@ except ImportError: READ_TOOL_DEFAULT_CONTEXT_BEFORE, READ_TOOL_DEFAULT_CONTEXT_AFTER, READ_TOOL_MAX_CONTEXT_BEFORE, READ_TOOL_MAX_CONTEXT_AFTER, READ_TOOL_DEFAULT_MAX_MATCHES, READ_TOOL_MAX_MATCHES, - READ_TOOL_MAX_FILE_SIZE, MAX_FOCUS_FILE_CHARS, + READ_TOOL_MAX_FILE_SIZE, TERMINAL_SANDBOX_MOUNT_PATH, TERMINAL_SANDBOX_MODE, TERMINAL_SANDBOX_CPUS, @@ -140,10 +140,6 @@ class MainTerminal: ) self.easter_egg_manager = EasterEggManager() self._announced_sub_agent_tasks = set() - - # 聚焦文件管理 - self.focused_files = {} # {path: content} 存储聚焦的文件内容 - self.current_session_id = 0 # 用于标识不同的任务会话 # 工具类别(可被管理员动态覆盖) self.tool_categories_map = dict(TOOL_CATEGORIES) @@ -194,7 +190,6 @@ class MainTerminal: "history": self.show_history, "files": self.show_files, "mode": self.toggle_mode, - "focused": self.show_focused_files, "terminals": self.show_terminals, # 新增:对话管理命令 "conversations": self.show_conversations, @@ -203,7 +198,6 @@ class MainTerminal: "save": self.save_conversation_command } #self.context_manager._web_terminal_callback = message_callback - #self.context_manager._focused_files = self.focused_files # 引用传递 def _apply_container_session(self, session: Optional["ContainerHandle"]): self.container_session = session @@ -840,11 +834,6 @@ class MainTerminal: system_message = tool_result.get("system_message") if system_message: self._record_sub_agent_message(system_message, tool_result.get("task_id"), inline=False) - # 补充TODO完成提示,放在tool消息之后保证格式正确 - raw_payload = tool_result.get("raw_result_data") or {} - todo_note = raw_payload.get("system_note") - if todo_note: - self.context_manager.add_conversation("system", todo_note) # 4. 在终端显示执行信息(不保存到历史) if collected_tool_calls: @@ -857,8 +846,10 @@ class MainTerminal: print(f"{OUTPUT_FORMATS['file']} 读取文件") elif tool_name in {"vlm_analyze", "ocr_image"}: print(f"{OUTPUT_FORMATS['file']} VLM 视觉理解") - elif tool_name == "write_file_diff": - print(f"{OUTPUT_FORMATS['file']} 应用补丁") + elif tool_name == "write_file": + print(f"{OUTPUT_FORMATS['file']} 写入文件") + elif tool_name == "edit_file": + print(f"{OUTPUT_FORMATS['file']} 编辑文件") elif tool_name == "delete_file": print(f"{OUTPUT_FORMATS['file']} 删除文件") elif tool_name == "terminal_session": @@ -873,10 +864,6 @@ class MainTerminal: print(f"{OUTPUT_FORMATS['terminal']} 执行系统命令") elif tool_name == "update_memory": print(f"{OUTPUT_FORMATS['memory']} 更新记忆") - elif tool_name == "focus_file": - print(f"🔍 聚焦文件") - elif tool_name == "unfocus_file": - print(f"❌ 取消聚焦") elif tool_name == "sleep": print(f"{OUTPUT_FORMATS['info']} 等待操作") else: @@ -1024,9 +1011,6 @@ class MainTerminal: # 文件结构 structure = self.context_manager.get_project_structure() - # 聚焦文件状态 - focused_size = sum(len(content) for content in self.focused_files.values()) - # 终端会话状态 terminal_status = self.terminal_manager.list_terminals() @@ -1046,7 +1030,6 @@ class MainTerminal: 上下文使用: {context_status['usage_percent']:.1f}% 当前消息: {len(self.context_manager.conversation_history)} 条 - 聚焦文件: {len(self.focused_files)}/3 个 ({focused_size/1024:.1f}KB) 终端会话: {terminal_status['total']}/{terminal_status['max_allowed']} 个 当前会话ID: {self.current_session_id} @@ -1293,7 +1276,7 @@ class MainTerminal: "type": "function", "function": { "name": "create_file", - "description": "创建新文件(仅创建空文件,正文请使用 write_file_diff 提交补丁)", + "description": "创建新文件(仅创建空文件,正文请使用 write_file 或 edit_file 写入/替换)", "parameters": { "type": "object", "properties": self._inject_intent({ @@ -1305,6 +1288,32 @@ class MainTerminal: } } }, + { + "type": "function", + "function": { + "name": "write_file", + "description": "将内容写入本地文件系统;append 为 False 时覆盖原文件,True 时追加到末尾。", + "parameters": { + "type": "object", + "properties": self._inject_intent({ + "file_path": { + "type": "string", + "description": "要写入的相对路径" + }, + "content": { + "type": "string", + "description": "要写入文件的内容" + }, + "append": { + "type": "boolean", + "description": "是否追加到文件而不是覆盖它", + "default": False + } + }), + "required": ["file_path", "content"] + } + } + }, { "type": "function", "function": { @@ -1379,6 +1388,31 @@ class MainTerminal: } } }, + { + "type": "function", + "function": { + "name": "edit_file", + "description": "在文件中执行精确的字符串替换;建议先使用 read_file 获取最新内容以确保精确匹配。", + "parameters": { + "type": "object", + "properties": self._inject_intent({ + "file_path": { + "type": "string", + "description": "要修改文件的相对路径" + }, + "old_string": { + "type": "string", + "description": "要替换的文本(需与文件内容精确匹配,保留缩进)" + }, + "new_string": { + "type": "string", + "description": "用于替换的新文本(必须不同于 old_string)" + } + }), + "required": ["file_path", "old_string", "new_string"] + } + } + }, { "type": "function", "function": { @@ -1423,24 +1457,6 @@ class MainTerminal: } } }, - { - "type": "function", - "function": { - "name": "write_file_diff", - "description": "使用统一 diff(`@@` 块、`-`/`+`/空格行)对单个文件做精确编辑:追加、插入、替换、删除都可以在一次调用里完成。\\n硬性规则:\\n\\n1) 补丁必须被 `*** Begin Patch` 与 `*** End Patch` 包裹。\\n2) 每个修改块必须以 `@@ [id:数字]` 开头。\\n3) 块内每一行只能是三类之一:\\n - 上下文行:以空格开头(` ␠`),表示“文件里必须原样存在”的锚点;\\n - 删除行:以 `-` 开头,表示要从文件中移除的原文;\\n - 新增行:以 `+` 开头,表示要写入的新内容。\\n4) 任何“想新增/想删除/想替换”的内容都必须逐行写 `+` 或 `-`;空行也必须写成单独一行的 `+`(这一行只有 `+` 和换行);如果你把多行新内容直接贴上去却不加 `+`,它会被当成上下文锚点去匹配原文件,极易导致“未找到匹配的原文”。\\n5) 重要语义:一个块里如果完全没有上下文行(空格开头)也没有删除行(`-`),那么它会被视为“仅追加(append-only)”,也就是把所有 `+` 行追加到文件末尾——这对“给空文件写正文”很合适,但对“插入到中间”是错误的。\\n\\n正面案例(至少 5 个,且都包含多行原文/多处修改)\\n\\n1) 给空文件写完整正文(追加到末尾;空文件=正确)\\n目标:新建 README.md 后一次性写入标题、安装、用法、FAQ(多段落、多行)。\\n要点:没有上下文/删除行 → 追加模式;空文件时最常用。\\n\\n*** Begin Patch\\n@@ [id:1]\\n+# 项目名称\\n+\\n+一个简短说明:这个项目用于……\\n+\\n+## 安装\\n+\\n+```bash\\n+pip install -r requirements.txt\\n+```\\n+\\n+## 快速开始\\n+\\n+```bash\\n+python main.py\\n+```\\n+\\n+## 常见问题\\n+\\n+- Q: 为什么会报 xxx?\\n+ A: 先检查 yyy,再确认 zzz。\\n+\\n*** End Patch\\n\\n2) “不删除,直接插入内容”到函数内部(必须用上下文锚定插入位置)\\n目标:在 def build_prompt(...): 里插入日志与参数归一化,但不改动其它行。\\n要点:插入发生在“两个上下文行之间”,上下文必须精确(包含缩进)。\\n\\n*** Begin Patch\\n@@ [id:1]\\n def build_prompt(user_text: str, system_text: str, tags: list):\\n prompt_parts = []\\n+ # 参数归一化:去掉首尾空白,避免模型误判\\n+ user_text = (user_text or \\\"\\\").strip()\\n+ system_text = (system_text or \\\"\\\").strip()\\n+\\n+ logger.debug(\\\"build_prompt: tags=%s, user_len=%d\\\", tags, len(user_text))\\n prompt_parts.append(system_text)\\n prompt_parts.append(user_text)\\n if tags:\\n prompt_parts.append(\\\"TAGS: \\\" + \\\",\\\".join(tags))\\n*** End Patch\\n\\n3) 复杂替换:整段函数重构(多行 old/new + 保留稳定上下文)\\n目标:把旧的 apply_patch()(弱校验)替换成新实现(多分支、异常信息更清晰)。\\n要点:替换不是“改一行”,而是“删一段、加一段”,并用函数签名/相邻代码作锚点。\\n\\n*** Begin Patch\\n@@ [id:1]\\n class FilePatcher:\\n def __init__(self, root: Path):\\n self.root = root\\n\\n def apply_patch(self, path: str, patch_text: str) -> dict:\\n- # old: naive replace\\n- content = (self.root / path).read_text(encoding=\\\"utf-8\\\")\\n- content = content.replace(\\\"foo\\\", \\\"bar\\\")\\n- (self.root / path).write_text(content, encoding=\\\"utf-8\\\")\\n- return {\\\"success\\\": True}\\n+ full_path = (self.root / path).resolve()\\n+ if self.root not in full_path.parents and full_path != self.root:\\n+ return {\\\"success\\\": False, \\\"error\\\": \\\"非法路径:越界访问\\\"}\\n+\\n+ if \\\"*** Begin Patch\\\" not in patch_text or \\\"*** End Patch\\\" not in patch_text:\\n+ return {\\\"success\\\": False, \\\"error\\\": \\\"补丁格式错误:缺少 Begin/End 标记\\\"}\\n+\\n+ try:\\n+ original = full_path.read_text(encoding=\\\"utf-8\\\")\\n+ except Exception as e:\\n+ return {\\\"success\\\": False, \\\"error\\\": f\\\"读取失败: {e}\\\"}\\n+\\n+ # 这里省略:解析 blocks、逐块应用、失败回滚等\\n+ updated = original\\n+ try:\\n+ full_path.write_text(updated, encoding=\\\"utf-8\\\")\\n+ except Exception as e:\\n+ return {\\\"success\\\": False, \\\"error\\\": f\\\"写入失败: {e}\\\"}\\n+\\n+ return {\\\"success\\\": True, \\\"message\\\": \\\"已应用补丁\\\"}\\n*** End Patch\\n\\n4) 复杂多块:同一文件里同时“加 import + 替换逻辑 + 插入新 helper + 删除旧函数”\\n目标:一次调用完成 4 种操作,且每块都有足够上下文,避免误匹配。\\n要点:不同区域用不同 @@ [id:n] 分块,互不干扰。\\n\\n*** Begin Patch\\n@@ [id:1]\\n-import json\\n+import json\\n+import re\\n from pathlib import Path\\n\\n@@ [id:2]\\n def normalize_user_input(text: str) -> str:\\n- return text\\n+ text = (text or \\\"\\\").strip()\\n+ # 压缩多余空白,减少提示词抖动\\n+ text = re.sub(r\\\"\\\\\\\\s+\\\", \\\" \\\", text)\\n+ return text\\n\\n@@ [id:3]\\n def load_config(path: str) -> dict:\\n cfg_path = Path(path)\\n if not cfg_path.exists():\\n return {}\\n data = cfg_path.read_text(encoding=\\\"utf-8\\\")\\n return json.loads(data)\\n+\\n+def safe_get(cfg: dict, key: str, default=None):\\n+ if not isinstance(cfg, dict):\\n+ return default\\n+ return cfg.get(key, default)\\n\\n@@ [id:4]\\n-def legacy_parse_flags(argv):\\n- # deprecated, kept for compatibility\\n- flags = {}\\n- for item in argv:\\n- if item.startswith(\\\"--\\\"):\\n- k, _, v = item[2:].partition(\\\"=\\\")\\n- flags[k] = v or True\\n- return flags\\n-\\n def main():\\n cfg = load_config(\\\"config.json\\\")\\n # ...\\n*** End Patch\\n\\n5) 删除示例:删除一整段“废弃配置块”,并顺手修正周围空行(多行删除 + 上下文)\\n目标:删掉 DEPRECATED_* 配置和旧注释,确保删除位置精确。\\n要点:删除行必须逐行 `-`;保留上下文行确保定位。\\n\\n*** Begin Patch\\n@@ [id:1]\\n # ==============================\\n # Runtime Config\\n # ==============================\\n-DEPRECATED_TIMEOUT = 5\\n-DEPRECATED_RETRIES = 1\\n-# 注意:这些字段将在下个版本移除\\n-# 请迁移到 NEW_TIMEOUT / NEW_RETRIES\\n NEW_TIMEOUT = 30\\n NEW_RETRIES = 3\\n*** End Patch\\n\\n如何写“带上下文”的正确姿势(要点)\\n\\n- 上下文要选“稳定锚点”:函数签名、类名、关键注释、紧邻的两三行缩进代码。\\n- 不要用“容易变的行”当唯一锚点:时间戳、日志序号、随机 id、生成内容片段。\\n- 上下文必须字节级一致(空格/Tab/大小写/标点都算),否则会匹配失败。\\n\\n反面案例(至少 3 个,且都是“真实会踩坑”的类型)\\n\\n反例 A(来自一次常见错误):空文件时只有第一行加了 `+`,后面直接贴正文\\n这会让后面的正文变成“上下文锚点”,工具会去空文件里找这些原文,必然失败(常见报错:未找到匹配的原文)。\\n\\n*** Begin Patch\\n@@ [id:1]\\n+\\n仰望U9X·电驭苍穹\\n银箭破空电光闪\\n三千马力云中藏\\n*** End Patch\\n\\n正确做法:正文每一行都要写 `+`(包括空行也写 `+`),空行写法是一行单独的 `+`。\\n\\n(反例:空行没加 `+`,会被当成上下文,空文件/定位修改时容易失败)\\n\\n*** Begin Patch\\n@@ [id:1]\\n+title = \\\"示例\\\"\\n\\n+[db]\\n+enabled = true\\n*** End Patch\\n\\n(正确:空行也要用 `+` 表示)\\n\\n*** Begin Patch\\n@@ [id:1]\\n+title = \\\"示例\\\"\\n+\\n+[db]\\n+enabled = true\\n*** End Patch\\n\\n(对应的正确 patch 示例:向空文件追加多行)\\n\\n*** Begin Patch\\n@@ [id:1]\\n+\\n+仰望U9X·电驭苍穹\\n+银箭破空电光闪\\n+三千马力云中藏\\n*** End Patch\\n\\n反例 B:想“插入到中间”,却只写 `+`(没有任何上下文/删除行)\\n这种块会被当成“追加到文件末尾”,结果内容跑到文件最后,不会插入到你以为的位置。\\n\\n*** Begin Patch\\n@@ [id:1]\\n+# 我以为会插到某个函数上面\\n+print(\\\"hello\\\")\\n*** End Patch\\n\\n正确做法:用上下文锚定插入点(见正面案例 2)。\\n\\n(对应的正确 patch 示例:用上下文把内容插入到函数内部,而不是追加到文件末尾)\\n\\n*** Begin Patch\\n@@ [id:1]\\n def main():\\n config = load_config(\\\"config.json\\\")\\n+ # 这里插入:启动提示(不会移动到文件末尾)\\n+ print(\\\"hello\\\")\\n run(config)\\n*** End Patch\\n\\n反例 C:补丁在第一个 `@@` 之前出现内容 / 或漏掉 Begin/End 标记\\n解析会直接报格式错误(例如:“在检测到第一个 @@ 块之前出现内容”、“缺少 Begin/End 标记”)。\\n\\n(错误形态示意)\\n这里先写了一段说明文字(没有 @@)\\n@@ [id:1]\\n+...\\n\\n正确做法:确保第一段非空内容必须从 `@@ [id:n]` 开始,并且整体有 Begin/End。\\n\\n(对应的正确 patch 示例:完整结构、第一段内容从 @@ 块开始)\\n\\n*** Begin Patch\\n@@ [id:1]\\n # ==============================\\n # Runtime Config\\n # ==============================\\n+# 说明:此处新增一行注释作为示例\\n NEW_TIMEOUT = 30\\n*** End Patch\\n", - "parameters": { - "type": "object", - "properties": self._inject_intent({ - "path": {"type": "string", "description": "目标文件路径(相对项目根目录)。"}, - "patch": { - "type": "string", - "description": "完整补丁文本,格式类似 unified diff,必须用 *** Begin Patch / *** End Patch 包裹,并用 @@ [id:数字] 划分多个修改块。示例:*** Begin Patch\\n@@ [id:1]\\n def main():\\n- def greet(self):\\n- return \"hi\"\\n+ def greet(self, name: str) -> str:\\n+ message = f\"Hello, {name}!\"\\n+ return message\\n@@ [id:2]\\n+\\n+if __name__ == \"__main__\":\\n+ print(\"hello world\")\\n*** End Patch" - } - }), - "required": ["path", "patch"] - } - } - }, { "type": "function", "function": { @@ -1455,34 +1471,6 @@ class MainTerminal: } } }, - { - "type": "function", - "function": { - "name": "focus_file", - "description": "聚焦 UTF-8 文本文件,将完整内容持续注入上下文。适合频繁查看/修改的核心文件;超过字符限制或非 UTF-8 时会拒绝。", - "parameters": { - "type": "object", - "properties": self._inject_intent({ - "path": {"type": "string", "description": "文件路径"} - }), - "required": ["path"] - } - } - }, - { - "type": "function", - "function": { - "name": "unfocus_file", - "description": "取消聚焦文件,从上下文中移除", - "parameters": { - "type": "object", - "properties": self._inject_intent({ - "path": {"type": "string", "description": "文件路径"} - }), - "required": ["path"] - } - } - }, { "type": "function", "function": { @@ -1705,14 +1693,14 @@ class MainTerminal: "type": "function", "function": { "name": "todo_create", - "description": "创建待办列表,将多步骤任务拆解为最多 8 条可执行项。概述请控制在 50 字以内,直接说明清单目标;任务列表只写 2~4 条明确步骤。", + "description": "创建待办列表,最多 8 条任务;若已有列表将被覆盖。", "parameters": { "type": "object", "properties": self._inject_intent({ "overview": {"type": "string", "description": "一句话概述待办清单要完成的目标,50 字以内。"}, "tasks": { "type": "array", - "description": "任务列表,建议 2~4 条,每条写清“动词+对象+目标”。", + "description": "任务列表,1~8 条,每条写清“动词+对象+目标”。", "items": { "type": "object", "properties": { @@ -1732,7 +1720,7 @@ class MainTerminal: "type": "function", "function": { "name": "todo_update_task", - "description": "勾选或取消指定任务。在调整任务顺序或内容前,请先向用户说明最新理解或变更。", + "description": "勾选或取消指定任务;全部勾选时提示所有任务已完成。", "parameters": { "type": "object", "properties": self._inject_intent({ @@ -1743,34 +1731,6 @@ class MainTerminal: } } }, - { - "type": "function", - "function": { - "name": "todo_finish", - "description": "尝试结束待办列表,需同步汇报每项任务结果。若仍有未完事项,请注明原因与后续建议。", - "parameters": { - "type": "object", - "properties": self._inject_intent({ - "reason": {"type": "string", "description": "可选说明"} - }) - } - } - }, - { - "type": "function", - "function": { - "name": "todo_finish_confirm", - "description": "在任务未完成时确认是否提前结束。若确认结束,请说明后续交付建议或遗留风险。", - "parameters": { - "type": "object", - "properties": self._inject_intent({ - "confirm": {"type": "boolean", "description": "true=确认结束,false=继续执行"}, - "reason": {"type": "string", "description": "确认结束时的说明"} - }), - "required": ["confirm"] - } - } - }, { "type": "function", "function": { @@ -1881,7 +1841,7 @@ class MainTerminal: """处理工具调用(添加参数预检查和改进错误处理)""" # 导入字符限制配置 from config import ( - MAX_READ_FILE_CHARS, MAX_FOCUS_FILE_CHARS, + MAX_READ_FILE_CHARS, MAX_RUN_COMMAND_CHARS, MAX_EXTRACT_WEBPAGE_CHARS ) @@ -1894,24 +1854,23 @@ class MainTerminal: try: # 检查参数总大小 arguments_str = json.dumps(arguments, ensure_ascii=False) - if len(arguments_str) > 50000: # 50KB限制 + if len(arguments_str) > 200000: # 200KB限制 return json.dumps({ "success": False, - "error": f"参数过大({len(arguments_str)}字符),超过50KB限制", + "error": f"参数过大({len(arguments_str)}字符),超过200KB限制", "suggestion": "请分块处理或减少参数内容" }, ensure_ascii=False) # 针对特定工具的内容检查 - if tool_name == "create_file" and "content" in arguments: + if tool_name == "write_file": content = arguments.get("content", "") - if not DISABLE_LENGTH_CHECK and len(content) > 9999999999: # 30KB内容限制 + length_limit = 200000 + if not DISABLE_LENGTH_CHECK and len(content) > length_limit: return json.dumps({ "success": False, - "error": f"文件内容过长({len(content)}字符),建议分块处理", - "suggestion": "请拆分内容或使用 write_file_diff 工具输出结构化补丁" + "error": f"文件内容过长({len(content)}字符),超过{length_limit}字符限制", + "suggestion": "请分块写入,或设置 append=true 多次写入" }, ensure_ascii=False) - - # 检查内容中的特殊字符 if '\\' in content and content.count('\\') > len(content) / 10: print(f"{OUTPUT_FORMATS['warning']} 检测到大量转义字符,可能存在格式问题") @@ -2072,12 +2031,12 @@ class MainTerminal: ) if result.get("success"): result["message"] = ( - f"已创建空文件: {result['path']}。请使用 write_file_diff 提交补丁写入正文。" + f"已创建空文件: {result['path']}。请使用 write_file 写入内容,或使用 edit_file 进行替换。" ) elif tool_name == "delete_file": result = self.file_manager.delete_file(arguments["path"]) - # 如果删除成功,同时删除备注和聚焦 + # 如果删除成功,同时删除备注 if result.get("success") and result.get("action") == "deleted": deleted_path = result.get("path") # 删除备注 @@ -2085,10 +2044,6 @@ class MainTerminal: del self.context_manager.file_annotations[deleted_path] self.context_manager.save_annotations() print(f"🧹 已删除文件备注: {deleted_path}") - # 删除聚焦 - if deleted_path in self.focused_files: - del self.focused_files[deleted_path] - print(f"🔍 已取消文件聚焦: {deleted_path}") elif tool_name == "rename_file": result = self.file_manager.rename_file( @@ -2096,6 +2051,7 @@ class MainTerminal: arguments["new_path"] ) # 如果重命名成功,更新备注和聚焦的key + # 如果重命名成功,更新备注 if result.get("success") and result.get("action") == "renamed": old_path = result.get("old_path") new_path = result.get("new_path") @@ -2106,83 +2062,33 @@ class MainTerminal: self.context_manager.file_annotations[new_path] = annotation self.context_manager.save_annotations() print(f"📝 已更新文件备注: {old_path} -> {new_path}") - # 更新聚焦 - if old_path in self.focused_files: - content = self.focused_files[old_path] - del self.focused_files[old_path] - self.focused_files[new_path] = content - print(f"🔍 已更新文件聚焦: {old_path} -> {new_path}") - - elif tool_name == "write_file_diff": - path = arguments.get("path") - patch_text = arguments.get("patch") - if not path or not patch_text: - result = {"success": False, "error": "缺少必要参数: path/patch"} + + elif tool_name == "write_file": + path = arguments.get("file_path") + content = arguments.get("content", "") + append_flag = bool(arguments.get("append", False)) + if not path: + result = {"success": False, "error": "缺少必要参数: file_path"} else: - length_limit = 30000 - if not DISABLE_LENGTH_CHECK and len(patch_text) > length_limit: - result = { - "success": False, - "error": f"补丁内容过长({len(patch_text)}字符),超过{length_limit}字符上限", - "suggestion": "请拆分补丁后多次调用 write_file_diff。" - } - else: - diff_result = self.file_manager.apply_diff_patch(path, patch_text) - result = diff_result - + mode = "a" if append_flag else "w" + result = self.file_manager.write_file(path, content, mode=mode) + + elif tool_name == "edit_file": + path = arguments.get("file_path") + old_text = arguments.get("old_string") + new_text = arguments.get("new_string") + if not path: + result = {"success": False, "error": "缺少必要参数: file_path"} + elif old_text is None or new_text is None: + result = {"success": False, "error": "缺少必要参数: old_string/new_string"} + elif old_text == new_text: + result = {"success": False, "error": "old_string 与 new_string 相同,无法执行替换"} + elif not old_text: + result = {"success": False, "error": "old_string 不能为空,请从 read_file 内容中精确复制"} + else: + result = self.file_manager.replace_in_file(path, old_text, new_text) elif tool_name == "create_folder": result = self.file_manager.create_folder(arguments["path"]) - - elif tool_name == "focus_file": - path = arguments["path"] - # 检查是否已经聚焦 - if path in self.focused_files: - result = {"success": False, "error": f"文件已经处于聚焦状态: {path}"} - else: - # 检查聚焦文件数量限制 - if len(self.focused_files) >= 3: - result = { - "success": False, - "error": f"已达到最大聚焦文件数量(3个),当前聚焦: {list(self.focused_files.keys())}", - "suggestion": "请先使用 unfocus_file 取消部分文件的聚焦" - } - else: - # 读取文件内容 - read_result = self.file_manager.read_file(path) - if read_result["success"]: - # 字符数检查 - char_count = len(read_result["content"]) - if char_count > MAX_FOCUS_FILE_CHARS: - result = { - "success": False, - "error": f"文件过大,有{char_count}字符,请使用run_command限制字符数返回", - "char_count": char_count, - "limit": MAX_FOCUS_FILE_CHARS - } - else: - self.focused_files[path] = read_result["content"] - result = { - "success": True, - "message": f"文件已聚焦: {path}", - "focused_files": list(self.focused_files.keys()), - "file_size": len(read_result["content"]) - } - print(f"🔍 文件已聚焦: {path} ({len(read_result['content'])} 字节)") - else: - result = read_result - - elif tool_name == "unfocus_file": - path = arguments["path"] - if path in self.focused_files: - del self.focused_files[path] - result = { - "success": True, - "message": f"已取消文件聚焦: {path}", - "remaining_focused": list(self.focused_files.keys()) - } - print(f"✖️ 已取消文件聚焦: {path}") - else: - result = {"success": False, "error": f"文件未处于聚焦状态: {path}"} elif tool_name == "web_search": allowed, quota_info = self.record_search_call() @@ -2409,17 +2315,6 @@ class MainTerminal: completed=arguments.get("completed", True) ) - elif tool_name == "todo_finish": - result = self.todo_manager.finish_todo( - reason=arguments.get("reason") - ) - - elif tool_name == "todo_finish_confirm": - result = self.todo_manager.confirm_finish( - confirm=arguments.get("confirm", False), - reason=arguments.get("reason") - ) - elif tool_name == "create_sub_agent": result = self.sub_agent_manager.create_sub_agent( agent_id=arguments.get("agent_id"), @@ -2634,23 +2529,6 @@ class MainTerminal: "content": todo_message }) - # 在最后注入聚焦文件内容作为系统消息 - 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" - focused_content += "提示:以上文件正在被聚焦,你可以直接看到完整内容并进行修改,禁止再次读取。" - - messages.append({ - "role": "system", - "content": focused_content - }) disabled_notice = self._format_disabled_tool_notice() if disabled_notice: messages.append({ @@ -2667,20 +2545,6 @@ class MainTerminal: return f.read() return "你是一个AI助手。" - async def show_focused_files(self, args: str = ""): - """显示当前聚焦的文件""" - if not self.focused_files: - print(f"{OUTPUT_FORMATS['info']} 当前没有聚焦的文件") - else: - print(f"\n🔍 聚焦文件列表 ({len(self.focused_files)}/3):") - print("="*50) - for path, content in self.focused_files.items(): - size_kb = len(content) / 1024 - lines = content.count('\n') + 1 - print(f" 📄 {path}") - print(f" 大小: {size_kb:.1f}KB | 行数: {lines}") - print("="*50) - async def show_terminals(self, args: str = ""): """显示终端会话列表""" result = self.terminal_manager.list_terminals() diff --git a/core/tool_config.py b/core/tool_config.py index 13dc802..673b45f 100644 --- a/core/tool_config.py +++ b/core/tool_config.py @@ -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="子智能体", diff --git a/core/web_terminal.py b/core/web_terminal.py index d847648..82a04be 100644 --- a/core/web_terminal.py +++ b/core/web_terminal.py @@ -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: diff --git a/modules/file_manager.py b/modules/file_manager.py index 3b9dc10..8537cb0 100644 --- a/modules/file_manager.py +++ b/modules/file_manager.py @@ -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: diff --git a/modules/todo_manager.py b/modules/todo_manager.py index 02aac25..48b267a 100644 --- a/modules/todo_manager.py +++ b/modules/todo_manager.py @@ -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]]: diff --git a/prompts/main_system.txt b/prompts/main_system.txt index d368a67..3331864 100644 --- a/prompts/main_system.txt +++ b/prompts/main_system.txt @@ -194,12 +194,11 @@ tree -L 2 1. **什么时候用**:任务需要2步以上、涉及多个文件或工具时 2. **清单要求**: - 概述:用一句话说明任务目标(不超过50字) - - 任务:最多4条,按执行顺序排列 + - 任务:最多8条(建议2-6条),按执行顺序排列 - 每条任务要说清楚具体做什么,不要用"优化""处理"这种模糊词 3. **执行方式**: - - 完成一项,勾选一项 - - 如果计划有变,先告诉用户 - - 全部完成后,用 todo_finish 结束 + - 完成/撤销一项,立即用 todo_update_task 勾选/取消 + - 如果计划有变,先告诉用户,并更新任务后继续勾选 ### 示例:整理文档 ``` diff --git a/prompts/main_system_qwenvl.txt b/prompts/main_system_qwenvl.txt index 8eb26fb..3161690 100644 --- a/prompts/main_system_qwenvl.txt +++ b/prompts/main_system_qwenvl.txt @@ -208,12 +208,11 @@ tree -L 2 1. **什么时候用**:任务需要2步以上、涉及多个文件或工具时 2. **清单要求**: - 概述:用一句话说明任务目标(不超过50字) - - 任务:最多4条,按执行顺序排列 + - 任务:最多8条(建议2-6条),按执行顺序排列 - 每条任务要说清楚具体做什么,不要用"优化""处理"这种模糊词 3. **执行方式**: - - 完成一项,勾选一项 - - 如果计划有变,先告诉用户 - - 全部完成后,用 todo_finish 结束 + - 完成/撤销一项,立即用 todo_update_task 勾选/取消 + - 如果计划有变,先告诉用户,并更新任务后继续勾选 ### 示例:整理文档 ``` diff --git a/prompts/todo_guidelines.txt b/prompts/todo_guidelines.txt index 00654a1..f487caa 100644 --- a/prompts/todo_guidelines.txt +++ b/prompts/todo_guidelines.txt @@ -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. **灵活调整**:发现问题及时沟通 记住:清单是给你自己看的,要给自己明确可执行的规划,同时要让用户知道你在做什么、完成到哪一步了。在用户明确给出“好的,请开始”的指令时,才能开始创建待办事项哦! - diff --git a/server/_chat_block.py b/server/_chat_block.py index b46d23d..b9d6cbc 100644 --- a/server/_chat_block.py +++ b/server/_chat_block.py @@ -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 }) - diff --git a/server/_chat_usage_segment.py b/server/_chat_usage_segment.py index 0dd5857..85b7ff7 100644 --- a/server/_chat_usage_segment.py +++ b/server/_chat_usage_segment.py @@ -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 }) - diff --git a/server/_conversation_segment.py b/server/_conversation_segment.py index 517b112..75faf1d 100644 --- a/server/_conversation_segment.py +++ b/server/_conversation_segment.py @@ -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"任务完成统计:") diff --git a/server/_globals_snippet.txt b/server/_globals_snippet.txt index b0ca405..f40750d 100644 --- a/server/_globals_snippet.txt +++ b/server/_globals_snippet.txt @@ -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 - diff --git a/server/chat.py b/server/chat.py index 1f87929..df96652 100644 --- a/server/chat.py +++ b/server/chat.py @@ -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 diff --git a/server/chat_flow.py b/server/chat_flow.py index 72ebec7..c8d3909 100644 --- a/server/chat_flow.py +++ b/server/chat_flow.py @@ -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}") diff --git a/server/state.py b/server/state.py index 4535314..dae3c33 100644 --- a/server/state.py +++ b/server/state.py @@ -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 diff --git a/static/src/composables/useLegacySocket.ts b/static/src/composables/useLegacySocket.ts index ace3cbc..1d5b399 100644 --- a/static/src/composables/useLegacySocket.ts +++ b/static/src/composables/useLegacySocket.ts @@ -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; diff --git a/static/src/stores/monitor.ts b/static/src/stores/monitor.ts index 2562d34..6dccaef 100644 --- a/static/src/stores/monitor.ts +++ b/static/src/stores/monitor.ts @@ -94,8 +94,6 @@ const TOOL_SCENE_MAP: Record = { 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 = { 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 = { update_memory: 'memoryUpdate', todo_create: 'todoCreate', todo_update_task: 'todoUpdate', - todo_finish: 'todoFinish', - todo_finish_confirm: 'todoFinishConfirm', todo_delete_task: 'todoDelete', terminal_run: 'runCommand' }; diff --git a/static/src/utils/chatDisplay.ts b/static/src/utils/chatDisplay.ts index a8b852b..1b79bf1 100644 --- a/static/src/utils/chatDisplay.ts +++ b/static/src/utils/chatDisplay.ts @@ -7,11 +7,11 @@ const RUNNING_ANIMATIONS: Record = { 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 = { 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 = { 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 = { 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: '网页保存完成(纯文本)', diff --git a/static/src/utils/icons.ts b/static/src/utils/icons.ts index 89bf4b2..3624f78 100644 --- a/static/src/utils/icons.ts +++ b/static/src/utils/icons.ts @@ -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', diff --git a/utils/context_manager.py b/utils/context_manager.py index 6f0b3a3..c012bed 100644 --- a/utils/context_manager.py +++ b/utils/context_manager.py @@ -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 ("<< 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,