fix: improve write_file_diff diagnostics
This commit is contained in:
parent
97da631e01
commit
51bb0f1033
@ -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}")
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -487,10 +487,10 @@ class ContextManager:
|
||||
|
||||
if ("<<<APPEND" in content) or metadata.get("append_payload"):
|
||||
new_msg["content"] = append_placeholder
|
||||
compressed_types.add("append_to_file")
|
||||
compressed_types.add("write_file_diff")
|
||||
elif ("<<<MODIFY" in content) or metadata.get("modify_payload"):
|
||||
new_msg["content"] = append_placeholder
|
||||
compressed_types.add("modify_file")
|
||||
compressed_types.add("write_file_diff")
|
||||
|
||||
elif role == "tool":
|
||||
tool_name = new_msg.get("name")
|
||||
@ -498,6 +498,9 @@ class ContextManager:
|
||||
if tool_name == "read_file":
|
||||
new_msg["content"] = append_placeholder
|
||||
compressed_types.add("read_file")
|
||||
elif tool_name == "write_file_diff":
|
||||
new_msg["content"] = append_placeholder
|
||||
compressed_types.add("write_file_diff")
|
||||
else:
|
||||
payload = None
|
||||
|
||||
@ -532,13 +535,12 @@ class ContextManager:
|
||||
}
|
||||
|
||||
type_labels = {
|
||||
"append_to_file": "文件追加输出",
|
||||
"modify_file": "文件修改输出",
|
||||
"write_file_diff": "文件补丁输出",
|
||||
"extract_webpage": "网页提取内容",
|
||||
"read_file": "文件读取内容"
|
||||
}
|
||||
|
||||
ordered_types = [type_labels[t] for t in ["append_to_file", "modify_file", "extract_webpage", "read_file"] if t in compressed_types]
|
||||
ordered_types = [type_labels[t] for t in ["write_file_diff", "extract_webpage", "read_file"] if t in compressed_types]
|
||||
summary_text = "系统提示:对话已压缩,之前的对话记录中的以下内容被替换为占位文本:" + "、".join(ordered_types) + "。其他内容不受影响,如需查看原文,请查看对应文件或重新执行相关工具。"
|
||||
|
||||
system_message = {
|
||||
@ -952,7 +954,15 @@ class ContextManager:
|
||||
|
||||
lines = []
|
||||
project_name = Path(structure['path']).name
|
||||
lines.append(f"📁 {project_name}/")
|
||||
container_root = (self.container_mount_path or "").strip() or "/workspace"
|
||||
container_root = container_root.rstrip("/") or "/"
|
||||
if container_root == "/":
|
||||
root_label = f"/ (项目根)"
|
||||
elif container_root.endswith(project_name):
|
||||
root_label = f"{container_root} (项目根)"
|
||||
else:
|
||||
root_label = f"{container_root} (映射自 {project_name})"
|
||||
lines.append(f"📁 {root_label}/")
|
||||
|
||||
def build_tree_recursive(tree_dict: Dict, prefix: str = ""):
|
||||
"""递归构建树形结构"""
|
||||
@ -1138,6 +1148,9 @@ class ContextManager:
|
||||
"role": conv["role"],
|
||||
"content": conv["content"]
|
||||
}
|
||||
reasoning = conv.get("reasoning_content")
|
||||
if reasoning:
|
||||
message["reasoning_content"] = reasoning
|
||||
if "tool_calls" in conv and conv["tool_calls"]:
|
||||
message["tool_calls"] = conv["tool_calls"]
|
||||
messages.append(message)
|
||||
|
||||
@ -179,6 +179,72 @@ def format_tool_result_notice(tool_name: str, tool_call_id: Optional[str], conte
|
||||
body = "(无附加输出)"
|
||||
return f"{header}\n{body}"
|
||||
|
||||
def format_tool_result_for_context(function_name: str, result_data: Any, raw_text: str) -> 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"<<<APPEND:{append_path}>>>",
|
||||
"end_marker": "<<<END_APPEND>>>",
|
||||
"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"<<<MODIFY:{modify_path}>>>",
|
||||
"end_marker": "<<<END_MODIFY>>>",
|
||||
"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)
|
||||
|
||||
@ -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 ("<<<APPEND" in content) or metadata.get("append_payload"):
|
||||
new_msg["content"] = append_placeholder
|
||||
compressed_types.add("append_to_file")
|
||||
compressed_types.add("write_file_diff")
|
||||
elif ("<<<MODIFY" in content) or metadata.get("modify_payload"):
|
||||
new_msg["content"] = append_placeholder
|
||||
compressed_types.add("modify_file")
|
||||
compressed_types.add("write_file_diff")
|
||||
|
||||
elif role == "tool":
|
||||
tool_name = new_msg.get("name")
|
||||
@ -445,6 +463,9 @@ class ContextManager:
|
||||
if tool_name == "read_file":
|
||||
new_msg["content"] = append_placeholder
|
||||
compressed_types.add("read_file")
|
||||
elif tool_name == "write_file_diff":
|
||||
new_msg["content"] = append_placeholder
|
||||
compressed_types.add("write_file_diff")
|
||||
else:
|
||||
payload = None
|
||||
|
||||
@ -479,13 +500,12 @@ class ContextManager:
|
||||
}
|
||||
|
||||
type_labels = {
|
||||
"append_to_file": "文件追加输出",
|
||||
"modify_file": "文件修改输出",
|
||||
"write_file_diff": "文件补丁输出",
|
||||
"extract_webpage": "网页提取内容",
|
||||
"read_file": "文件读取内容"
|
||||
}
|
||||
|
||||
ordered_types = [type_labels[t] for t in ["append_to_file", "modify_file", "extract_webpage", "read_file"] if t in compressed_types]
|
||||
ordered_types = [type_labels[t] for t in ["write_file_diff", "extract_webpage", "read_file"] if t in compressed_types]
|
||||
summary_text = "系统提示:对话已压缩,之前的对话记录中的以下内容被替换为占位文本:" + "、".join(ordered_types) + "。其他内容不受影响,如需查看原文,请查看对应文件或重新执行相关工具。"
|
||||
|
||||
system_message = {
|
||||
@ -505,10 +525,12 @@ class ContextManager:
|
||||
resolved_project_path = self._resolve_project_path_from_metadata(metadata)
|
||||
project_path = str(resolved_project_path)
|
||||
thinking_mode = metadata.get("thinking_mode", False)
|
||||
run_mode = metadata.get("run_mode") or ("thinking" if thinking_mode else "fast")
|
||||
|
||||
compressed_conversation_id = self.conversation_manager.create_conversation(
|
||||
project_path=project_path,
|
||||
thinking_mode=thinking_mode,
|
||||
run_mode=run_mode,
|
||||
initial_messages=compressed_messages
|
||||
)
|
||||
|
||||
@ -534,10 +556,12 @@ class ContextManager:
|
||||
resolved_project_path = self._resolve_project_path_from_metadata(metadata)
|
||||
project_path = str(resolved_project_path)
|
||||
thinking_mode = metadata.get("thinking_mode", False)
|
||||
run_mode = metadata.get("run_mode") or ("thinking" if thinking_mode else "fast")
|
||||
|
||||
duplicate_conversation_id = self.conversation_manager.create_conversation(
|
||||
project_path=project_path,
|
||||
thinking_mode=thinking_mode,
|
||||
run_mode=run_mode,
|
||||
initial_messages=original_messages
|
||||
)
|
||||
|
||||
@ -620,18 +644,24 @@ class ContextManager:
|
||||
reasoning_content: Optional[str] = None
|
||||
):
|
||||
"""添加对话记录(改进版:集成自动保存 + 智能token统计)"""
|
||||
message = {
|
||||
"role": role,
|
||||
"content": content,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
timestamp = datetime.now().isoformat()
|
||||
if role == "assistant":
|
||||
message = {
|
||||
"role": role,
|
||||
"reasoning_content": reasoning_content if reasoning_content is not None else "",
|
||||
"content": content or "",
|
||||
"timestamp": timestamp
|
||||
}
|
||||
else:
|
||||
message = {
|
||||
"role": role,
|
||||
"content": content,
|
||||
"timestamp": timestamp
|
||||
}
|
||||
|
||||
if metadata:
|
||||
message["metadata"] = metadata
|
||||
|
||||
if reasoning_content:
|
||||
message["reasoning_content"] = reasoning_content
|
||||
|
||||
# 如果是assistant消息且有工具调用,保存完整格式
|
||||
if role == "assistant" and tool_calls:
|
||||
# 确保工具调用格式完整
|
||||
@ -880,7 +910,15 @@ class ContextManager:
|
||||
|
||||
lines = []
|
||||
project_name = Path(structure['path']).name
|
||||
lines.append(f"📁 {project_name}/")
|
||||
container_root = (self.container_mount_path or "").strip() or "/workspace"
|
||||
container_root = container_root.rstrip("/") or "/"
|
||||
if container_root == "/":
|
||||
root_label = f"/ (项目根)"
|
||||
elif container_root.endswith(project_name):
|
||||
root_label = f"{container_root} (项目根)"
|
||||
else:
|
||||
root_label = f"{container_root} (映射自 {project_name})"
|
||||
lines.append(f"📁 {root_label}/")
|
||||
|
||||
def build_tree_recursive(tree_dict: Dict, prefix: str = ""):
|
||||
"""递归构建树形结构"""
|
||||
@ -1066,6 +1104,9 @@ class ContextManager:
|
||||
"role": conv["role"],
|
||||
"content": conv["content"]
|
||||
}
|
||||
reasoning = conv.get("reasoning_content")
|
||||
if reasoning:
|
||||
message["reasoning_content"] = reasoning
|
||||
if "tool_calls" in conv and conv["tool_calls"]:
|
||||
message["tool_calls"] = conv["tool_calls"]
|
||||
messages.append(message)
|
||||
|
||||
244
web_server.py
244
web_server.py
@ -358,6 +358,73 @@ def format_tool_result_notice(tool_name: str, tool_call_id: Optional[str], conte
|
||||
body = "(无附加输出)"
|
||||
return f"{header}\n{body}"
|
||||
|
||||
def format_tool_result_for_context(function_name: str, result_data: Any, raw_text: str) -> 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"<<<APPEND:{append_path}>>>",
|
||||
"end_marker": "<<<END_APPEND>>>",
|
||||
"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"<<<MODIFY:{modify_path}>>>",
|
||||
"end_marker": "<<<END_MODIFY>>>",
|
||||
"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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user