diff --git a/core/main_terminal.py b/core/main_terminal.py index 1f12c8f..41f2cda 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -75,17 +75,23 @@ class MainTerminal: self, project_path: str, thinking_mode: bool = False, + run_mode: Optional[str] = None, data_dir: Optional[str] = None, container_session: Optional["ContainerHandle"] = None, usage_tracker: Optional[object] = None, ): self.project_path = project_path - self.thinking_mode = thinking_mode # False=快速模式, True=思考模式 + allowed_modes = {"fast", "thinking", "deep"} + initial_mode = run_mode if run_mode in allowed_modes else ("thinking" if thinking_mode else "fast") + self.run_mode = initial_mode + self.thinking_mode = initial_mode != "fast" # False=快速模式, True=思考模式 + self.deep_thinking_mode = initial_mode == "deep" self.data_dir = Path(data_dir).expanduser().resolve() if data_dir else Path(DATA_DIR).resolve() self.usage_tracker = usage_tracker # 初始化组件 - self.api_client = DeepSeekClient(thinking_mode=thinking_mode) + self.api_client = DeepSeekClient(thinking_mode=self.thinking_mode) + self.api_client.set_deep_thinking_mode(self.deep_thinking_mode) self.context_manager = ContextManager(project_path, data_dir=str(self.data_dir)) self.context_manager.main_terminal = self self.container_mount_path = TERMINAL_SANDBOX_MOUNT_PATH or "/workspace" @@ -232,7 +238,8 @@ class MainTerminal: conversation_id = self.context_manager.start_new_conversation( project_path=self.project_path, - thinking_mode=self.thinking_mode + thinking_mode=self.thinking_mode, + run_mode=self.run_mode ) print(f"{OUTPUT_FORMATS['info']} 新建对话: {conversation_id}") @@ -618,7 +625,7 @@ class MainTerminal: # 如果是思考模式,每个新任务重置状态 # 注意:这里重置的是当前任务的第一次调用标志,确保新用户请求重新思考 if self.thinking_mode: - self.api_client.start_new_task() + self.api_client.start_new_task(force_deep=self.deep_thinking_mode) # 新增:开始新的任务会话 self.current_session_id += 1 @@ -837,7 +844,7 @@ class MainTerminal: # 如果是思考模式,重置状态(下次任务会重新思考) if self.thinking_mode: - self.api_client.start_new_task() + self.api_client.start_new_task(force_deep=self.deep_thinking_mode) self.current_session_id += 1 @@ -859,7 +866,7 @@ class MainTerminal: # 重置相关状态 if self.thinking_mode: - self.api_client.start_new_task() + self.api_client.start_new_task(force_deep=self.deep_thinking_mode) self.current_session_id += 1 @@ -913,7 +920,7 @@ class MainTerminal: terminal_status = self.terminal_manager.list_terminals() # 思考模式状态 - thinking_status = '思考模式' if self.thinking_mode else '快速模式' + thinking_status = self.get_run_mode_label() if self.thinking_mode: thinking_status += f" ({'等待新任务' if self.api_client.current_task_first_call else '任务进行中'})" @@ -2319,7 +2326,11 @@ class MainTerminal: if sub_agent_prompt: messages.append({"role": "system", "content": sub_agent_prompt}) - if self.thinking_mode: + if self.deep_thinking_mode: + deep_prompt = self.load_prompt("deep_thinking_mode_guidelines").strip() + if deep_prompt: + messages.append({"role": "system", "content": deep_prompt}) + elif self.thinking_mode: thinking_prompt = self.load_prompt("thinking_mode_guidelines").strip() if thinking_prompt: messages.append({"role": "system", "content": thinking_prompt}) @@ -2346,6 +2357,9 @@ class MainTerminal: "role": conv["role"], "content": conv["content"] } + reasoning = conv.get("reasoning_content") + if reasoning: + message["reasoning_content"] = reasoning # 如果有工具调用信息,添加到消息中 tool_calls = conv.get("tool_calls") or [] if tool_calls and self._tool_calls_followed_by_tools(conversation, idx, tool_calls): @@ -2534,17 +2548,55 @@ class MainTerminal: print(f"\n📁 项目文件结构:") print(self.context_manager._build_file_tree(structure)) print(f"\n总计: {structure['total_files']} 个文件, {structure['total_size'] / 1024 / 1024:.2f} MB") - - async def toggle_mode(self, args: str = ""): - """切换运行模式(简化版)""" - if self.thinking_mode: - # 当前是思考模式,切换到快速模式 - self.thinking_mode = False - self.api_client.thinking_mode = False - print(f"{OUTPUT_FORMATS['info']} 已切换到: 快速模式(不思考)") - else: - # 当前是快速模式,切换到思考模式 - self.thinking_mode = True - self.api_client.thinking_mode = True + + def set_run_mode(self, mode: str) -> str: + """统一设置运行模式""" + allowed = ["fast", "thinking", "deep"] + normalized = mode.lower() + if normalized not in allowed: + raise ValueError(f"不支持的模式: {mode}") + previous_mode = getattr(self, "run_mode", "fast") + self.run_mode = normalized + self.thinking_mode = normalized != "fast" + self.deep_thinking_mode = normalized == "deep" + self.api_client.thinking_mode = self.thinking_mode + self.api_client.set_deep_thinking_mode(self.deep_thinking_mode) + if self.deep_thinking_mode: + self.api_client.force_thinking_next_call = False + self.api_client.skip_thinking_next_call = False + if not self.thinking_mode: self.api_client.start_new_task() - print(f"{OUTPUT_FORMATS['info']} 已切换到: 思考模式(智能思考)") + elif previous_mode == "deep" and normalized != "deep": + self.api_client.start_new_task() + return self.run_mode + + def get_run_mode_label(self) -> str: + labels = { + "fast": "快速模式(无思考)", + "thinking": "思考模式(首次调用使用思考模型)", + "deep": "深度思考模式(整轮使用思考模型)" + } + return labels.get(self.run_mode, "快速模式(无思考)") + + async def toggle_mode(self, args: str = ""): + """切换运行模式""" + modes = ["fast", "thinking", "deep"] + target_mode = "" + if args: + candidate = args.strip().lower() + if candidate not in modes: + print(f"{OUTPUT_FORMATS['error']} 无效模式: {args}。可选: fast / thinking / deep") + return + target_mode = candidate + else: + current_index = modes.index(self.run_mode) if self.run_mode in modes else 0 + target_mode = modes[(current_index + 1) % len(modes)] + if target_mode == self.run_mode: + print(f"{OUTPUT_FORMATS['info']} 当前已是 {self.get_run_mode_label()}") + return + try: + self.set_run_mode(target_mode) + print(f"{OUTPUT_FORMATS['info']} 已切换到: {self.get_run_mode_label()}") + except ValueError as exc: + print(f"{OUTPUT_FORMATS['error']} {exc}") + diff --git a/modules/file_manager.py b/modules/file_manager.py index ae8bfd8..411ea82 100644 --- a/modules/file_manager.py +++ b/modules/file_manager.py @@ -3,7 +3,8 @@ import os import shutil from pathlib import Path -from typing import Optional, Dict, List, Tuple, TYPE_CHECKING +import re +from typing import Optional, Dict, List, Set, Tuple, TYPE_CHECKING from datetime import datetime try: from config import ( @@ -673,6 +674,316 @@ class FileManager: def append_file(self, path: str, content: str) -> Dict: """追加内容到文件""" return self.write_file(path, content, mode="a") + + def _parse_diff_patch(self, patch_text: str) -> Dict: + """解析统一diff格式的补丁,转换为 apply_modify_blocks 所需的块结构。""" + if not patch_text or "*** Begin Patch" not in patch_text or "*** End Patch" not in patch_text: + return { + "success": False, + "error": "补丁格式错误:缺少 *** Begin Patch / *** End Patch 标记。" + } + + start = patch_text.find("*** Begin Patch") + end = patch_text.rfind("*** End Patch") + if end <= start: + return { + "success": False, + "error": "补丁格式错误:结束标记位置异常。" + } + + body = patch_text[start + len("*** Begin Patch"):end] + lines = body.splitlines(True) # 保留换行符,便于逐字匹配 + + blocks: List[Dict] = [] + current_block: Optional[Dict] = None + auto_index = 1 + + id_pattern = re.compile(r"\[id:\s*(\d+)\]", re.IGNORECASE) + + for raw_line in lines: + stripped = raw_line.strip() + if not stripped and current_block is None: + continue + + if stripped.startswith("@@"): + if current_block: + if not current_block["lines"]: + return { + "success": False, + "error": f"补丁块缺少内容:{current_block.get('header', '').strip()}" + } + blocks.append(current_block) + + header = stripped + id_match = id_pattern.search(header) + block_id: Optional[int] = None + if id_match: + try: + block_id = int(id_match.group(1)) + except ValueError: + return { + "success": False, + "error": f"补丁块编号必须是整数:{header}" + } + + current_block = {"id": block_id, "header": header, "lines": []} + continue + + if current_block is None: + if stripped: + return { + "success": False, + "error": "补丁格式错误:在检测到第一个 @@ 块之前出现内容。" + } + continue + + if raw_line.startswith("\\ No newline at end of file"): + continue + + current_block["lines"].append(raw_line) + + if current_block: + if not current_block["lines"]: + return { + "success": False, + "error": f"补丁块缺少内容:{current_block.get('header', '').strip()}" + } + blocks.append(current_block) + + if not blocks: + return { + "success": False, + "error": "补丁格式错误:未检测到任何 @@ [id:n] 块。" + } + + parsed_blocks: List[Dict] = [] + used_indices: Set[int] = set() + + for block in blocks: + idx = block["id"] + if idx is None: + while auto_index in used_indices: + auto_index += 1 + idx = auto_index + auto_index += 1 + elif idx in used_indices: + while idx in used_indices: + idx += 1 + auto_index = max(auto_index, idx + 1) + + used_indices.add(idx) + + old_lines: List[str] = [] + new_lines: List[str] = [] + has_anchor = False + has_content = False + + for line in block["lines"]: + if not line: + continue + prefix = line[0] + if prefix == ' ': + has_anchor = True + old_lines.append(line[1:]) + new_lines.append(line[1:]) + has_content = True + elif prefix == '-': + has_anchor = True + old_lines.append(line[1:]) + has_content = True + elif prefix == '+': + new_lines.append(line[1:]) + has_content = True + else: + # 容忍空白符或意外格式,直接作为上下文 + old_lines.append(line) + new_lines.append(line) + has_anchor = True + has_content = True + + if not has_content: + return { + "success": False, + "error": f"补丁块 {idx} 未包含任何 + / - / 上下文行。" + } + + append_only = False + if not has_anchor: + append_only = True + + old_text = "".join(old_lines) + new_text = "".join(new_lines) + raw_patch = f"{block['header']}\n{''.join(block['lines'])}" + + parsed_blocks.append({ + "index": idx, + "old": old_text, + "new": new_text, + "append_only": append_only, + "raw_patch": raw_patch + }) + + return { + "success": True, + "blocks": parsed_blocks + } + + def apply_diff_patch(self, path: str, patch_text: str) -> Dict: + """解析统一diff并写入文件,支持多块依次执行。""" + valid, error, full_path = self._validate_path(path) + if not valid: + return {"success": False, "error": error} + + parse_result = self._parse_diff_patch(patch_text) + if not parse_result.get("success"): + return parse_result + + blocks = parse_result.get("blocks") or [] + if not blocks: + return { + "success": False, + "error": "未检测到有效的补丁块。" + } + + relative_path = str(self._relative_path(full_path)) + + parsed_blocks: List[Dict] = blocks + block_lookup: Dict[int, Dict] = {block["index"]: block for block in parsed_blocks} + + def attach_block_context(entries: Optional[List[Dict]]): + if not entries: + return + for entry in entries: + if not isinstance(entry, dict): + continue + idx = entry.get("index") + if idx is None: + continue + block_info = block_lookup.get(idx) + if not block_info: + continue + patch_text = block_info.get("raw_patch") + if patch_text: + entry.setdefault("block_patch", patch_text) + if "old_text" not in entry: + entry["old_text"] = block_info.get("old") + if "new_text" not in entry: + entry["new_text"] = block_info.get("new") + entry.setdefault("append_only", block_info.get("append_only", False)) + + append_only_blocks = [b for b in parsed_blocks if b.get("append_only")] + modify_blocks = [ + {"index": b["index"], "old": b["old"], "new": b["new"]} + for b in parsed_blocks + if not b.get("append_only") + ] + + apply_result = {"results": []} + completed_indices: List[int] = [] + failed_entries: List[Dict] = [] + write_error = None + + if modify_blocks: + modify_result = self.apply_modify_blocks(path, modify_blocks) + apply_result.update(modify_result) + completed_indices.extend(modify_result.get("completed", [])) + failed_entries.extend(modify_result.get("failed", [])) + write_error = modify_result.get("error") + else: + apply_result.update({ + "success": True, + "completed": [], + "failed": [], + "results": [], + "write_performed": False, + "error": None + }) + + results_blocks = apply_result.get("results", []).copy() + append_results: List[Dict] = [] + append_bytes = 0 + append_lines_total = 0 + append_success = True + + if append_only_blocks: + try: + with open(full_path, 'a', encoding='utf-8') as f: + for block in append_only_blocks: + chunk = block.get("new", "") + if not chunk: + append_results.append({ + "index": block["index"], + "status": "failed", + "reason": "追加块为空" + }) + failed_entries.append({ + "index": block["index"], + "reason": "追加块为空" + }) + append_success = False + continue + f.write(chunk) + added_lines = chunk.count('\n') + if chunk and not chunk.endswith('\n'): + added_lines += 1 + append_lines_total += added_lines + append_bytes += len(chunk.encode('utf-8')) + append_results.append({ + "index": block["index"], + "status": "success", + "removed_lines": 0, + "added_lines": added_lines + }) + completed_indices.append(block["index"]) + except Exception as e: + append_success = False + write_error = f"追加写入失败: {e}" + append_results.append({ + "index": append_only_blocks[-1]["index"], + "status": "failed", + "reason": str(e) + }) + failed_entries.append({ + "index": append_only_blocks[-1]["index"], + "reason": str(e) + }) + + attach_block_context(failed_entries) + + total_blocks = len(parsed_blocks) + completed_unique = sorted(set(completed_indices)) + + summary_parts = [ + f"向 {relative_path} 应用 {total_blocks} 个补丁块", + f"成功 {len(completed_unique)} 个", + f"失败 {len(failed_entries)} 个" + ] + if append_only_blocks: + summary_parts.append(f"追加 {len(append_only_blocks)} 块,写入 {append_lines_total} 行({append_bytes} 字节)") + if write_error: + summary_parts.append(write_error) + summary = ",".join(summary_parts) + + results_blocks.extend(append_results) + + apply_result["results"] = results_blocks + apply_result["blocks"] = results_blocks + apply_result["path"] = relative_path + apply_result["total_blocks"] = total_blocks + apply_result["summary"] = summary + apply_result["message"] = summary + apply_result["completed"] = completed_unique + apply_result["failed"] = failed_entries + apply_result["append_bytes"] = append_bytes + apply_result["append_blocks"] = len(append_only_blocks) + apply_result["success"] = ( + append_success + and apply_result.get("success", True) + and not failed_entries + and not write_error + ) + apply_result["error"] = write_error + return apply_result def apply_modify_blocks(self, path: str, blocks: List[Dict]) -> Dict: """ @@ -815,7 +1126,7 @@ class FileManager: return { "success": False, "error": "要替换的文本过长,可能导致性能问题", - "suggestion": "请拆分内容或使用 modify_file 提交结构化补丁" + "suggestion": "请拆分内容或使用 write_file_diff 提交结构化补丁" } if new_text and len(new_text) > 9999999999: diff --git a/sub_agent/utils/context_manager.py b/sub_agent/utils/context_manager.py index 61f2f0a..fdc7560 100644 --- a/sub_agent/utils/context_manager.py +++ b/sub_agent/utils/context_manager.py @@ -487,10 +487,10 @@ class ContextManager: if ("<< str: + if function_name == "read_file" and isinstance(result_data, dict): + return format_read_file_result(result_data) + + if function_name == "write_file_diff" and isinstance(result_data, dict): + path = result_data.get("path", "目标文件") + summary = result_data.get("summary") or result_data.get("message") + completed = result_data.get("completed") or [] + failed_blocks = result_data.get("failed") or [] + lines = [f"[文件补丁] {path}"] + if summary: + lines.append(summary) + if completed: + lines.append(f"✅ 成功块: {', '.join(str(i) for i in completed)}") + if failed_blocks: + fail_descriptions = [] + for item in failed_blocks[:3]: + idx = item.get("index") + reason = item.get("reason") or item.get("error") or "未说明原因" + fail_descriptions.append(f"#{idx}: {reason}") + lines.append("⚠️ 失败块: " + ";".join(fail_descriptions)) + if len(failed_blocks) > 3: + lines.append(f"(其余 {len(failed_blocks) - 3} 个失败块略)") + detail_sections: List[str] = [] + for item in failed_blocks: + idx = item.get("index") + reason = item.get("reason") or item.get("error") or "未说明原因" + block_patch = item.get("block_patch") or item.get("patch") + if not block_patch: + old_text = item.get("old_text") or "" + new_text = item.get("new_text") or "" + synthetic_lines: List[str] = [] + if old_text: + synthetic_lines.extend(f"-{line}" for line in old_text.splitlines()) + if new_text: + synthetic_lines.extend(f"+{line}" for line in new_text.splitlines()) + if synthetic_lines: + block_patch = "\n".join(synthetic_lines) + detail_sections.append(f"- #{idx}: {reason}") + if block_patch: + detail_sections.append("```diff") + detail_sections.append(block_patch.rstrip("\n")) + detail_sections.append("```") + detail_sections.append("") + if detail_sections and detail_sections[-1] == "": + detail_sections.pop() + if detail_sections: + lines.append("⚠️ 失败块详情:") + lines.extend(detail_sections) + if result_data.get("success") is False and result_data.get("error"): + lines.append(f"⚠️ 错误: {result_data.get('error')}") + formatted = "\n".join(line for line in lines if line) + return formatted or raw_text + + if isinstance(result_data, dict): + parts = [] + summary = result_data.get("summary") or result_data.get("message") + if summary: + parts.append(str(summary)) + error_msg = result_data.get("error") + if error_msg: + parts.append(f"⚠️ 错误: {error_msg}") + if parts: + return "\n".join(parts) + return raw_text + def _format_relative_path(path: Optional[str], workspace: Optional[str]) -> str: """将绝对路径转换为相对 workspace 的表示,默认返回原始路径。""" @@ -2508,8 +2574,8 @@ def detect_malformed_tool_call(text): return True # 检测特定的工具名称后跟JSON - tool_names = ['create_file', 'read_file', 'modify_file', 'delete_file', - 'append_to_file', 'terminal_session', 'terminal_input', 'web_search', + tool_names = ['create_file', 'read_file', 'write_file_diff', 'delete_file', + 'terminal_session', 'terminal_input', 'web_search', 'extract_webpage', 'save_webpage', 'run_python', 'run_command', 'focus_file', 'unfocus_file', 'sleep'] for tool in tool_names: @@ -3314,10 +3380,10 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client # 通过文本内容提前检测工具调用意图 if not detected_tools: # 检测常见的工具调用模式 - tool_patterns = [ - (r'(创建|新建|生成).*(文件|file)', 'create_file'), - (r'(读取|查看|打开).*(文件|file)', 'read_file'), - (r'(修改|编辑|更新).*(文件|file)', 'modify_file'), + tool_patterns = [ + (r'(创建|新建|生成).*(文件|file)', 'create_file'), + (r'(读取|查看|打开).*(文件|file)', 'read_file'), + (r'(修改|编辑|更新).*(文件|file)', 'write_file_diff'), (r'(删除|移除).*(文件|file)', 'delete_file'), (r'(搜索|查找|search)', 'web_search'), (r'(执行|运行).*(Python|python|代码)', 'run_python'), @@ -3815,6 +3881,8 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client "content": assistant_content, "tool_calls": tool_calls } + if current_thinking: + assistant_message["reasoning_content"] = current_thinking messages.append(assistant_message) @@ -4003,57 +4071,12 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client action_message = None awaiting_flag = False - if function_name == "append_to_file": - if result_data.get("success") and result_data.get("awaiting_content"): - append_path = result_data.get("path") or arguments.get("path") - pending_append = { - "path": append_path, - "tool_call_id": tool_call_id, - "buffer": "", - "start_marker": f"<<>>", - "end_marker": "<<>>", - "content_start": None, - "end_index": None, - "display_id": tool_display_id - } - append_probe_buffer = "" - awaiting_flag = True - action_status = 'running' - action_message = f"正在向 {append_path} 追加内容..." - text_started = False - text_streaming = False - text_has_content = False - debug_log(f"append_to_file 等待输出: {append_path}") - else: - debug_log("append_to_file 返回完成状态") - elif function_name == "modify_file": - if result_data.get("success") and result_data.get("awaiting_content"): - modify_path = result_data.get("path") or arguments.get("path") - pending_modify = { - "path": modify_path, - "tool_call_id": tool_call_id, - "buffer": "", - "raw_buffer": "", - "start_marker": f"<<>>", - "end_marker": "<<>>", - "start_seen": False, - "end_index": None, - "display_id": tool_display_id, - "detected_blocks": set(), - "probe_buffer": "" - } - modify_probe_buffer = "" - if hasattr(web_terminal, "pending_modify_request"): - web_terminal.pending_modify_request = {"path": modify_path} - awaiting_flag = True - action_status = 'running' - action_message = f"正在修改 {modify_path}..." - text_started = False - text_streaming = False - text_has_content = False - debug_log(f"modify_file 等待输出: {modify_path}") - else: - debug_log("modify_file 返回完成状态") + if function_name == "write_file_diff": + diff_path = result_data.get("path") or arguments.get("path") + summary = result_data.get("summary") or result_data.get("message") + if summary: + action_message = summary + debug_log(f"write_file_diff 执行完成: {summary or '无摘要'}") if function_name == "wait_sub_agent": system_msg = result_data.get("system_message") @@ -4081,7 +4104,7 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client sender('update_action', update_payload) # 更新UI状态 - if function_name in ['focus_file', 'unfocus_file', 'modify_file']: + 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']: @@ -4089,13 +4112,11 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client sender('file_tree_update', structure) # ===== 增量保存:立即保存工具结果 ===== - try: - result_data = json.loads(tool_result) - if function_name == "read_file": - tool_result_content = format_read_file_result(result_data) - else: - tool_result_content = tool_result - except: + metadata_payload = None + if isinstance(result_data, dict): + tool_result_content = format_tool_result_for_context(function_name, result_data, tool_result) + metadata_payload = {"tool_payload": result_data} + else: tool_result_content = tool_result # 立即保存工具结果 @@ -4103,7 +4124,8 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client "tool", tool_result_content, tool_call_id=tool_call_id, - name=function_name + name=function_name, + metadata=metadata_payload ) debug_log(f"💾 增量保存:工具结果 {function_name}") system_message = result_data.get("system_message") if isinstance(result_data, dict) else None @@ -4121,7 +4143,7 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client "content": tool_result_content }) - if function_name not in ['append_to_file', 'modify_file']: + if function_name != 'write_file_diff': await process_sub_agent_updates(messages, inline=True, after_tool_call_id=tool_call_id) await asyncio.sleep(0.2) diff --git a/utils/context_manager.py b/utils/context_manager.py index 1035786..55bfd10 100644 --- a/utils/context_manager.py +++ b/utils/context_manager.py @@ -236,7 +236,7 @@ class ContextManager: # 新增:对话持久化相关方法 # =========================================== - def start_new_conversation(self, project_path: str = None, thinking_mode: bool = False) -> str: + def start_new_conversation(self, project_path: str = None, thinking_mode: bool = False, run_mode: Optional[str] = None) -> str: """ 开始新对话 @@ -258,6 +258,7 @@ class ContextManager: conversation_id = self.conversation_manager.create_conversation( project_path=project_path, thinking_mode=thinking_mode, + run_mode=run_mode or ("thinking" if thinking_mode else "fast"), initial_messages=[] ) @@ -312,6 +313,18 @@ class ContextManager: self.project_path = resolved_project_path + run_mode = metadata.get("run_mode") + if self.main_terminal: + try: + if run_mode: + self.main_terminal.set_run_mode(run_mode) + elif metadata.get("thinking_mode"): + self.main_terminal.set_run_mode("thinking") + else: + self.main_terminal.set_run_mode("fast") + except Exception: + pass + print(f"📖 加载对话: {conversation_id} - {conversation_data.get('title', '未知标题')}") print(f"📊 包含 {len(self.conversation_history)} 条消息") @@ -332,12 +345,14 @@ class ContextManager: return False try: + run_mode = getattr(self.main_terminal, "run_mode", None) if hasattr(self, "main_terminal") else None success = self.conversation_manager.save_conversation( conversation_id=self.current_conversation_id, messages=self.conversation_history, project_path=str(self.project_path), todo_list=self.todo_list, - thinking_mode=getattr(self.main_terminal, "thinking_mode", None) if hasattr(self, "main_terminal") else None + thinking_mode=getattr(self.main_terminal, "thinking_mode", None) if hasattr(self, "main_terminal") else None, + run_mode=run_mode ) if success: @@ -357,11 +372,14 @@ class ContextManager: if not force and not self.conversation_history: return try: + run_mode = getattr(self.main_terminal, "run_mode", None) if hasattr(self, "main_terminal") else None self.conversation_manager.save_conversation( conversation_id=self.current_conversation_id, messages=self.conversation_history, project_path=str(self.project_path), - todo_list=self.todo_list + todo_list=self.todo_list, + thinking_mode=getattr(self.main_terminal, "thinking_mode", None) if hasattr(self, "main_terminal") else None, + run_mode=run_mode ) # 静默保存,不输出日志 except Exception as e: @@ -434,10 +452,10 @@ class ContextManager: if ("<< str: + """将工具结果转成适合写入对话的简洁文本。""" + if function_name == "read_file" and isinstance(result_data, dict): + return format_read_file_result(result_data) + + if function_name == "write_file_diff" and isinstance(result_data, dict): + path = result_data.get("path", "目标文件") + summary = result_data.get("summary") or result_data.get("message") + completed = result_data.get("completed") or [] + failed_blocks = result_data.get("failed") or [] + lines = [f"[文件补丁] {path}"] + if summary: + lines.append(summary) + if completed: + lines.append(f"✅ 成功块: {', '.join(str(i) for i in completed)}") + if failed_blocks: + fail_descriptions = [] + for item in failed_blocks[:3]: + idx = item.get("index") + reason = item.get("reason") or item.get("error") or "未说明原因" + fail_descriptions.append(f"#{idx}: {reason}") + lines.append("⚠️ 失败块: " + ";".join(fail_descriptions)) + if len(failed_blocks) > 3: + lines.append(f"(其余 {len(failed_blocks) - 3} 个失败块略)") + detail_sections: List[str] = [] + for item in failed_blocks: + idx = item.get("index") + reason = item.get("reason") or item.get("error") or "未说明原因" + block_patch = item.get("block_patch") or item.get("patch") + if not block_patch: + old_text = item.get("old_text") or "" + new_text = item.get("new_text") or "" + synthetic_lines: List[str] = [] + if old_text: + synthetic_lines.extend(f"-{line}" for line in old_text.splitlines()) + if new_text: + synthetic_lines.extend(f"+{line}" for line in new_text.splitlines()) + if synthetic_lines: + block_patch = "\n".join(synthetic_lines) + detail_sections.append(f"- #{idx}: {reason}") + if block_patch: + detail_sections.append("```diff") + detail_sections.append(block_patch.rstrip("\n")) + detail_sections.append("```") + detail_sections.append("") + if detail_sections and detail_sections[-1] == "": + detail_sections.pop() + if detail_sections: + lines.append("⚠️ 失败块详情:") + lines.extend(detail_sections) + if result_data.get("success") is False and result_data.get("error"): + lines.append(f"⚠️ 错误: {result_data.get('error')}") + formatted = "\n".join(line for line in lines if line) + return formatted or raw_text + + if isinstance(result_data, dict): + parts = [] + summary = result_data.get("summary") or result_data.get("message") + if summary: + parts.append(str(summary)) + error_msg = result_data.get("error") + if error_msg: + parts.append(f"⚠️ 错误: {error_msg}") + if parts: + return "\n".join(parts) + return raw_text + # 创建调试日志文件 DEBUG_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "debug_stream.log" CHUNK_BACKEND_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "chunk_backend.log" @@ -421,10 +488,15 @@ def get_user_resources(username: Optional[str] = None) -> Tuple[Optional[WebTerm usage_tracker = get_or_create_usage_tracker(username, workspace) terminal = user_terminals.get(username) if not terminal: - thinking_mode = session.get('thinking_mode', False) + run_mode = session.get('run_mode') + thinking_mode_flag = session.get('thinking_mode') + if run_mode not in {"fast", "thinking", "deep"}: + run_mode = "thinking" if thinking_mode_flag else "fast" + thinking_mode = run_mode != "fast" terminal = WebTerminal( project_path=str(workspace.project_path), thinking_mode=thinking_mode, + run_mode=run_mode, message_callback=make_terminal_callback(username), data_dir=str(workspace.data_dir), container_session=container_handle, @@ -525,14 +597,16 @@ def build_upload_error_response(exc: UploadSecurityError): }), status -def ensure_conversation_loaded(terminal: WebTerminal, conversation_id: Optional[str], thinking_mode: bool) -> Tuple[str, bool]: +def ensure_conversation_loaded(terminal: WebTerminal, conversation_id: Optional[str], run_mode: Optional[str]) -> Tuple[str, bool]: """确保终端加载指定对话,若无则创建新的""" created_new = False if not conversation_id: - result = terminal.create_new_conversation(thinking_mode=thinking_mode) + result = terminal.create_new_conversation(run_mode=run_mode) if not result.get("success"): raise RuntimeError(result.get("message", "创建对话失败")) conversation_id = result["conversation_id"] + session['run_mode'] = terminal.run_mode + session['thinking_mode'] = terminal.thinking_mode created_new = True else: conversation_id = conversation_id if conversation_id.startswith('conv_') else f"conv_{conversation_id}" @@ -541,14 +615,23 @@ def ensure_conversation_loaded(terminal: WebTerminal, conversation_id: Optional[ load_result = terminal.load_conversation(conversation_id) if not load_result.get("success"): raise RuntimeError(load_result.get("message", "对话加载失败")) - # 切换到对话记录的思考模式 + # 切换到对话记录的运行模式 try: conv_data = terminal.context_manager.conversation_manager.load_conversation(conversation_id) or {} meta = conv_data.get("metadata", {}) or {} - mode = bool(meta.get("thinking_mode", terminal.thinking_mode)) - terminal.thinking_mode = mode - terminal.api_client.thinking_mode = mode - terminal.api_client.start_new_task() + run_mode_meta = meta.get("run_mode") + if run_mode_meta: + terminal.set_run_mode(run_mode_meta) + elif meta.get("thinking_mode"): + terminal.set_run_mode("thinking") + else: + terminal.set_run_mode("fast") + if terminal.thinking_mode: + terminal.api_client.start_new_task(force_deep=terminal.deep_thinking_mode) + else: + terminal.api_client.start_new_task() + session['run_mode'] = terminal.run_mode + session['thinking_mode'] = terminal.thinking_mode except Exception: pass return conversation_id, created_new @@ -562,7 +645,7 @@ def reset_system_state(terminal: Optional[WebTerminal]): # 1. 重置API客户端状态 if hasattr(terminal, 'api_client') and terminal.api_client: debug_log("重置API客户端状态") - terminal.api_client.start_new_task() # 重置思考模式状态 + terminal.api_client.start_new_task(force_deep=getattr(terminal, "deep_thinking_mode", False)) # 2. 重置主终端会话状态 if hasattr(terminal, 'current_session_id'): @@ -637,6 +720,8 @@ def get_thinking_state(terminal: WebTerminal) -> Dict[str, Any]: def mark_force_thinking(terminal: WebTerminal, reason: str = ""): """标记下一次API调用必须使用思考模型。""" + if getattr(terminal, "deep_thinking_mode", False): + return if not getattr(terminal, "thinking_mode", False): return state = get_thinking_state(terminal) @@ -647,6 +732,8 @@ def mark_force_thinking(terminal: WebTerminal, reason: str = ""): def mark_suppress_thinking(terminal: WebTerminal): """标记下一次API调用必须跳过思考模型(例如写入窗口)。""" + if getattr(terminal, "deep_thinking_mode", False): + return if not getattr(terminal, "thinking_mode", False): return state = get_thinking_state(terminal) @@ -656,6 +743,10 @@ def mark_suppress_thinking(terminal: WebTerminal): def apply_thinking_schedule(terminal: WebTerminal): """根据当前状态配置API客户端的思考/快速模式。""" client = terminal.api_client + if getattr(terminal, "deep_thinking_mode", False): + client.force_thinking_next_call = False + client.skip_thinking_next_call = False + return if not getattr(terminal, "thinking_mode", False): client.force_thinking_next_call = False client.skip_thinking_next_call = False @@ -696,6 +787,10 @@ def apply_thinking_schedule(terminal: WebTerminal): def update_thinking_after_call(terminal: WebTerminal): """一次API调用完成后更新快速计数。""" + if getattr(terminal, "deep_thinking_mode", False): + state = get_thinking_state(terminal) + state["fast_streak"] = 0 + return if not getattr(terminal, "thinking_mode", False): return state = get_thinking_state(terminal) @@ -831,7 +926,9 @@ def login(): session['logged_in'] = True session['username'] = record.username - session['thinking_mode'] = app.config.get('DEFAULT_THINKING_MODE', False) + default_thinking = app.config.get('DEFAULT_THINKING_MODE', False) + session['thinking_mode'] = default_thinking + session['run_mode'] = app.config.get('DEFAULT_RUN_MODE', "thinking" if default_thinking else "fast") session.permanent = True clear_failures("login", identifier=client_ip) workspace = user_manager.ensure_user_workspace(record.username) @@ -1041,11 +1138,20 @@ def update_thinking_mode(terminal: WebTerminal, workspace: UserWorkspace, userna """切换思考模式""" try: data = request.get_json() or {} - desired_mode = bool(data.get('thinking_mode')) - terminal.thinking_mode = desired_mode - terminal.api_client.thinking_mode = desired_mode - terminal.api_client.start_new_task() - session['thinking_mode'] = desired_mode + requested_mode = data.get('mode') + if requested_mode in {"fast", "thinking", "deep"}: + target_mode = requested_mode + elif 'thinking_mode' in data: + target_mode = "thinking" if bool(data.get('thinking_mode')) else "fast" + else: + target_mode = terminal.run_mode + terminal.set_run_mode(target_mode) + if terminal.thinking_mode: + terminal.api_client.start_new_task(force_deep=terminal.deep_thinking_mode) + else: + terminal.api_client.start_new_task() + session['thinking_mode'] = terminal.thinking_mode + session['run_mode'] = terminal.run_mode # 更新当前对话的元数据 ctx = terminal.context_manager if ctx.current_conversation_id: @@ -1055,7 +1161,8 @@ def update_thinking_mode(terminal: WebTerminal, workspace: UserWorkspace, userna messages=ctx.conversation_history, project_path=str(ctx.project_path), todo_list=ctx.todo_list, - thinking_mode=desired_mode + thinking_mode=terminal.thinking_mode, + run_mode=terminal.run_mode ) except Exception as exc: print(f"[API] 保存思考模式到对话失败: {exc}") @@ -1065,7 +1172,10 @@ def update_thinking_mode(terminal: WebTerminal, workspace: UserWorkspace, userna return jsonify({ "success": True, - "data": status.get("thinking_mode") + "data": { + "thinking_mode": terminal.thinking_mode, + "mode": terminal.run_mode + } }) except Exception as exc: print(f"[API] 切换思考模式失败: {exc}") @@ -1909,7 +2019,7 @@ def handle_message(data): requested_conversation_id = data.get('conversation_id') try: - conversation_id, created_new = ensure_conversation_loaded(terminal, requested_conversation_id, terminal.thinking_mode) + conversation_id, created_new = ensure_conversation_loaded(terminal, requested_conversation_id, terminal.run_mode) except RuntimeError as exc: emit('error', {'message': str(exc)}) return @@ -2016,10 +2126,13 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam try: data = request.get_json() or {} thinking_mode = data.get('thinking_mode', terminal.thinking_mode) + run_mode = data.get('mode') - result = terminal.create_new_conversation(thinking_mode=thinking_mode) + result = terminal.create_new_conversation(thinking_mode=thinking_mode, run_mode=run_mode) if result["success"]: + session['run_mode'] = terminal.run_mode + session['thinking_mode'] = terminal.thinking_mode # 广播对话列表更新事件 socketio.emit('conversation_list_update', { 'action': 'created', @@ -2493,8 +2606,8 @@ def detect_malformed_tool_call(text): return True # 检测特定的工具名称后跟JSON - tool_names = ['create_file', 'read_file', 'modify_file', 'delete_file', - 'append_to_file', 'terminal_session', 'terminal_input', 'web_search', + tool_names = ['create_file', 'read_file', 'write_file_diff', 'delete_file', + 'terminal_session', 'terminal_input', 'web_search', 'extract_webpage', 'save_webpage', 'run_python', 'run_command', 'focus_file', 'unfocus_file', 'sleep'] for tool in tool_names: @@ -2511,7 +2624,7 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client # 如果是思考模式,重置状态 if web_terminal.thinking_mode: - web_terminal.api_client.start_new_task() + web_terminal.api_client.start_new_task(force_deep=web_terminal.deep_thinking_mode) state = get_thinking_state(web_terminal) state["fast_streak"] = 0 state["force_next"] = False @@ -3273,7 +3386,7 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client tool_patterns = [ (r'(创建|新建|生成).*(文件|file)', 'create_file'), (r'(读取|查看|打开).*(文件|file)', 'read_file'), - (r'(修改|编辑|更新).*(文件|file)', 'modify_file'), + (r'(修改|编辑|更新).*(文件|file)', 'write_file_diff'), (r'(删除|移除).*(文件|file)', 'delete_file'), (r'(搜索|查找|search)', 'web_search'), (r'(执行|运行).*(Python|python|代码)', 'run_python'), @@ -3791,6 +3904,8 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client "content": assistant_content, "tool_calls": tool_calls } + if current_thinking: + assistant_message["reasoning_content"] = current_thinking messages.append(assistant_message) if assistant_content or current_thinking or tool_calls: @@ -4035,61 +4150,12 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client action_message = None awaiting_flag = False - if function_name == "append_to_file": - if result_data.get("success") and result_data.get("awaiting_content"): - append_path = result_data.get("path") or arguments.get("path") - pending_append = { - "path": append_path, - "tool_call_id": tool_call_id, - "buffer": "", - "start_marker": f"<<>>", - "end_marker": "<<>>", - "content_start": None, - "end_index": None, - "display_id": tool_display_id - } - append_probe_buffer = "" - awaiting_flag = True - action_status = 'running' - action_message = f"正在向 {append_path} 追加内容..." - text_started = False - text_streaming = False - text_has_content = False - if hasattr(web_terminal, "pending_append_request"): - web_terminal.pending_append_request = {"path": append_path} - mark_suppress_thinking(web_terminal) - debug_log(f"append_to_file 等待输出: {append_path}") - else: - debug_log("append_to_file 返回完成状态") - elif function_name == "modify_file": - if result_data.get("success") and result_data.get("awaiting_content"): - modify_path = result_data.get("path") or arguments.get("path") - pending_modify = { - "path": modify_path, - "tool_call_id": tool_call_id, - "buffer": "", - "raw_buffer": "", - "start_marker": f"<<>>", - "end_marker": "<<>>", - "start_seen": False, - "end_index": None, - "display_id": tool_display_id, - "detected_blocks": set(), - "probe_buffer": "" - } - modify_probe_buffer = "" - if hasattr(web_terminal, "pending_modify_request"): - web_terminal.pending_modify_request = {"path": modify_path} - awaiting_flag = True - action_status = 'running' - action_message = f"正在修改 {modify_path}..." - text_started = False - text_streaming = False - text_has_content = False - mark_suppress_thinking(web_terminal) - debug_log(f"modify_file 等待输出: {modify_path}") - else: - debug_log("modify_file 返回完成状态") + if function_name == "write_file_diff": + diff_path = result_data.get("path") or arguments.get("path") + summary = result_data.get("summary") or result_data.get("message") + if summary: + action_message = summary + debug_log(f"write_file_diff 执行完成: {summary or '无摘要'}") if function_name == "wait_sub_agent": system_msg = result_data.get("system_message") @@ -4118,7 +4184,7 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client sender('update_action', update_payload) # 更新UI状态 - if function_name in ['focus_file', 'unfocus_file', 'modify_file']: + 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']: @@ -4126,13 +4192,11 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client sender('file_tree_update', structure) # ===== 增量保存:立即保存工具结果 ===== - try: - result_data = json.loads(tool_result) - if function_name == "read_file": - tool_result_content = format_read_file_result(result_data) - else: - tool_result_content = tool_result - except: + metadata_payload = None + if isinstance(result_data, dict): + tool_result_content = format_tool_result_for_context(function_name, result_data, tool_result) + metadata_payload = {"tool_payload": result_data} + else: tool_result_content = tool_result # 立即保存工具结果 @@ -4140,7 +4204,8 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client "tool", tool_result_content, tool_call_id=tool_call_id, - name=function_name + name=function_name, + metadata=metadata_payload ) debug_log(f"💾 增量保存:工具结果 {function_name}") system_message = result_data.get("system_message") if isinstance(result_data, dict) else None @@ -4160,7 +4225,7 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client "content": tool_result_content }) - if function_name not in ['append_to_file', 'modify_file']: + if function_name != 'write_file_diff': await process_sub_agent_updates(messages, inline=True, after_tool_call_id=tool_call_id) await asyncio.sleep(0.2) @@ -4206,7 +4271,7 @@ def handle_command(data): if cmd == "clear": terminal.context_manager.conversation_history.clear() if terminal.thinking_mode: - terminal.api_client.start_new_task() + terminal.api_client.start_new_task(force_deep=terminal.deep_thinking_mode) emit('command_result', { 'command': cmd, 'success': True, @@ -4313,6 +4378,7 @@ def initialize_system(path: str, thinking_mode: bool = False): print(f"[Init] 调试日志: {DEBUG_LOG_FILE}") app.config['DEFAULT_THINKING_MODE'] = thinking_mode + app.config['DEFAULT_RUN_MODE'] = "thinking" if thinking_mode else "fast" print(f"{OUTPUT_FORMATS['success']} Web系统初始化完成(多用户模式)") def run_server(path: str, thinking_mode: bool = False, port: int = DEFAULT_PORT, debug: bool = False):