From 5fcda980fb977625ccda596c5b469955d6e0dfd6 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Thu, 29 Jan 2026 15:05:29 +0800 Subject: [PATCH] fix: cache file tree per conversation --- utils/context_manager.py | 83 ++++++++++++++++++++++++++++++++--- utils/conversation_manager.py | 65 ++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 8 deletions(-) diff --git a/utils/context_manager.py b/utils/context_manager.py index c012bed..b6f299c 100644 --- a/utils/context_manager.py +++ b/utils/context_manager.py @@ -54,6 +54,9 @@ class ContextManager: self.todo_list: Optional[Dict[str, Any]] = None self.has_images: bool = False self.image_compression_mode: str = "original" + # 对话元数据与项目快照缓存 + self.conversation_metadata: Dict[str, Any] = {} + self.project_snapshot: Optional[Dict[str, Any]] = None # 新增:对话持久化管理器 self.conversation_manager = ConversationManager(base_dir=self.data_dir) @@ -329,6 +332,8 @@ class ContextManager: self.conversation_history = [] self.todo_list = None self.has_images = False + self.conversation_metadata = {} + self.project_snapshot = None print(f"📝 开始新对话: {conversation_id}") return conversation_id @@ -358,6 +363,17 @@ class ContextManager: self.conversation_history = conversation_data.get("messages", []) todo_data = conversation_data.get("todo_list") self.todo_list = deepcopy(todo_data) if todo_data else None + self.conversation_metadata = deepcopy(conversation_data.get("metadata", {}) or {}) + # 恢复项目文件树快照(如已存在) + meta = self.conversation_metadata + if meta.get("project_file_tree"): + self.project_snapshot = { + "file_tree": meta.get("project_file_tree"), + "statistics": meta.get("project_statistics"), + "snapshot_at": meta.get("project_snapshot_at") + } + else: + self.project_snapshot = None # 更新项目路径(如果对话中有的话) metadata = conversation_data.get("metadata", {}) @@ -400,6 +416,49 @@ class ContextManager: return True + def _ensure_project_snapshot(self) -> Dict[str, Any]: + """ + 确保当前对话拥有项目文件树快照: + - 若已缓存/存档则直接返回; + - 若不存在,则第一次扫描目录并存入对话文件,后续复用。 + """ + if self.project_snapshot: + return self.project_snapshot + + meta = self.conversation_metadata or {} + stored_tree = meta.get("project_file_tree") + if stored_tree: + self.project_snapshot = { + "file_tree": stored_tree, + "statistics": meta.get("project_statistics"), + "snapshot_at": meta.get("project_snapshot_at") + } + return self.project_snapshot + + # 首次生成并缓存 + structure = self.get_project_structure() + snapshot = { + "file_tree": self._build_file_tree(structure), + "statistics": { + "total_files": structure["total_files"], + "total_size": structure["total_size"] + }, + "snapshot_at": datetime.now().isoformat() + } + self.project_snapshot = snapshot + if self.current_conversation_id: + self.conversation_manager.update_project_snapshot( + self.current_conversation_id, + project_file_tree=snapshot["file_tree"], + project_statistics=snapshot["statistics"], + project_snapshot_at=snapshot["snapshot_at"] + ) + # 同步内存元数据 + self.conversation_metadata["project_file_tree"] = snapshot["file_tree"] + self.conversation_metadata["project_statistics"] = snapshot["statistics"] + self.conversation_metadata["project_snapshot_at"] = snapshot["snapshot_at"] + return snapshot + def save_current_conversation(self) -> bool: """ 保存当前对话 @@ -954,16 +1013,19 @@ class ContextManager: def build_main_context(self, memory_content: str) -> Dict: """构建主终端上下文""" - structure = self.get_project_structure() + snapshot = self._ensure_project_snapshot() + stats = snapshot.get("statistics") or {} + total_files = stats.get("total_files", 0) + total_size_bytes = stats.get("total_size", 0) context = { "project_info": { "path": str(self.project_path), - "file_tree": self._build_file_tree(structure), + "file_tree": snapshot.get("file_tree", ""), "file_annotations": self.file_annotations, "statistics": { - "total_files": structure["total_files"], - "total_size": f"{structure['total_size'] / 1024 / 1024:.2f}MB" + "total_files": total_files, + "total_size": f"{total_size_bytes / 1024 / 1024:.2f}MB" } }, "memory": memory_content, @@ -981,14 +1043,21 @@ class ContextManager: execution_results: List[Dict] = None ) -> Dict: """构建子任务上下文""" - structure = self.get_project_structure() + snapshot = self._ensure_project_snapshot() + stats = snapshot.get("statistics") or {} + total_files = stats.get("total_files", 0) + total_size_bytes = stats.get("total_size", 0) context = { "task_info": task_info, "project_info": { "path": str(self.project_path), - "file_tree": self._build_file_tree(structure), - "file_annotations": self.file_annotations + "file_tree": snapshot.get("file_tree", ""), + "file_annotations": self.file_annotations, + "statistics": { + "total_files": total_files, + "total_size": f"{total_size_bytes / 1024 / 1024:.2f}MB" + } }, "memory": { "main_memory": main_memory, diff --git a/utils/conversation_manager.py b/utils/conversation_manager.py index 78ee561..fac0447 100644 --- a/utils/conversation_manager.py +++ b/utils/conversation_manager.py @@ -347,6 +347,10 @@ class ConversationManager: "run_mode": normalized_mode, "model_key": model_key, "has_images": has_images, + # 首次对话尚未生成文件树快照,待首次用户消息时填充 + "project_file_tree": None, + "project_statistics": None, + "project_snapshot_at": None, "total_messages": len(messages), "total_tools": self._count_tools_in_messages(messages), "status": "active" @@ -454,7 +458,10 @@ class ConversationManager: run_mode: Optional[str] = None, todo_list: Optional[Dict] = None, model_key: Optional[str] = None, - has_images: Optional[bool] = None + has_images: Optional[bool] = None, + project_file_tree: Optional[str] = None, + project_statistics: Optional[Dict] = None, + project_snapshot_at: Optional[str] = None ) -> bool: """ 保存对话(更新现有对话) @@ -522,6 +529,19 @@ class ConversationManager: existing_data["metadata"]["has_images"] = bool(has_images) elif "has_images" not in existing_data["metadata"]: existing_data["metadata"]["has_images"] = False + # 文件树快照(如果有新值则更新,若已有则保持) + if project_file_tree is not None: + existing_data["metadata"]["project_file_tree"] = project_file_tree + elif "project_file_tree" not in existing_data["metadata"]: + existing_data["metadata"]["project_file_tree"] = None + if project_statistics is not None: + existing_data["metadata"]["project_statistics"] = project_statistics + elif "project_statistics" not in existing_data["metadata"]: + existing_data["metadata"]["project_statistics"] = None + if project_snapshot_at is not None: + existing_data["metadata"]["project_snapshot_at"] = project_snapshot_at + elif "project_snapshot_at" not in existing_data["metadata"]: + existing_data["metadata"]["project_snapshot_at"] = None existing_data["metadata"]["total_messages"] = len(messages) existing_data["metadata"]["total_tools"] = self._count_tools_in_messages(messages) @@ -546,6 +566,33 @@ class ConversationManager: print(f"⌘ 保存对话失败 {conversation_id}: {e}") return False + def update_project_snapshot( + self, + conversation_id: str, + project_file_tree: str, + project_statistics: Optional[Dict], + project_snapshot_at: Optional[str] = None + ) -> bool: + """ + 单独更新对话的项目文件树快照,不修改消息内容。 + 便于在首次用户消息时写入固定文件结构。 + """ + try: + data = self.load_conversation(conversation_id) + if not data: + return False + meta = data.get("metadata", {}) or {} + meta["project_file_tree"] = project_file_tree + meta["project_statistics"] = project_statistics + meta["project_snapshot_at"] = project_snapshot_at + data["metadata"] = meta + self._save_conversation_file(conversation_id, data) + self._update_index(conversation_id, data) + return True + except Exception as exc: + print(f"⌘ 更新项目快照失败 {conversation_id}: {exc}") + return False + def load_conversation(self, conversation_id: str) -> Optional[Dict]: """ 加载对话数据 @@ -589,6 +636,22 @@ class ConversationManager: self._save_conversation_file(conversation_id, data) print(f"🔧 为对话 {conversation_id} 添加运行模式字段") + # 确保项目快照字段存在(向后兼容) + changed = False + if "project_file_tree" not in metadata: + metadata["project_file_tree"] = None + changed = True + if "project_statistics" not in metadata: + metadata["project_statistics"] = None + changed = True + if "project_snapshot_at" not in metadata: + metadata["project_snapshot_at"] = None + changed = True + if changed: + data["metadata"] = metadata + self._save_conversation_file(conversation_id, data) + print(f"🔧 为对话 {conversation_id} 补齐项目快照字段") + # 回填缺失的模型字段:从最近的助手消息元数据推断 if metadata.get("model_key") is None: inferred_model = None