From b0941a247bfe0620c41b8cffe358b8abdbbe36aa Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Fri, 6 Feb 2026 17:09:19 +0800 Subject: [PATCH] fix: reduce workspace scans in host mode --- core/main_terminal.py | 3 + core/web_terminal.py | 11 +-- modules/file_manager.py | 40 +++++++---- server/_conversation_segment.py | 5 +- server/chat_flow.py | 5 +- static/src/app.ts | 24 +++++-- static/src/components/panels/LeftPanel.vue | 22 +++--- static/src/stores/file.ts | 20 ++++++ .../styles/components/panels/_left-panel.scss | 3 +- utils/context_manager.py | 72 ++++++++++++++++++- 10 files changed, 162 insertions(+), 43 deletions(-) diff --git a/core/main_terminal.py b/core/main_terminal.py index ad69330..b8f69a6 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -2734,6 +2734,9 @@ class MainTerminal: async def show_files(self, args: str = ""): """显示项目文件""" + if self.context_manager._is_host_mode_without_safety(): + print("\n⚠️ 宿主机模式下文件树不可用") + return structure = self.context_manager.get_project_structure() print(f"\n📁 项目文件结构:") print(self.context_manager._build_file_tree(structure)) diff --git a/core/web_terminal.py b/core/web_terminal.py index 003a43a..d2d1b04 100644 --- a/core/web_terminal.py +++ b/core/web_terminal.py @@ -566,11 +566,12 @@ class WebTerminal(MainTerminal): # 如果是文件操作,广播文件树更新 if tool_name in ['create_file', 'delete_file', 'rename_file', 'create_folder', 'save_webpage']: - try: - structure = self.context_manager.get_project_structure() - self.broadcast('file_tree_update', structure) - except Exception as e: - logger.error(f"广播文件树更新失败: {e}") + if not self.context_manager._is_host_mode_without_safety(): + try: + structure = self.context_manager.get_project_structure() + self.broadcast('file_tree_update', structure) + except Exception as e: + logger.error(f"广播文件树更新失败: {e}") # 如果是记忆操作,广播记忆状态更新 if tool_name == 'update_memory': diff --git a/modules/file_manager.py b/modules/file_manager.py index 8537cb0..3b12ebc 100644 --- a/modules/file_manager.py +++ b/modules/file_manager.py @@ -14,6 +14,8 @@ try: OUTPUT_FORMATS, READ_TOOL_MAX_FILE_SIZE, PROJECT_MAX_STORAGE_BYTES, + TERMINAL_SANDBOX_MODE, + LINUX_SAFETY, ) except ImportError: # 兼容全局环境中存在同名包的情况 import sys @@ -28,6 +30,8 @@ except ImportError: # 兼容全局环境中存在同名包的情况 OUTPUT_FORMATS, READ_TOOL_MAX_FILE_SIZE, PROJECT_MAX_STORAGE_BYTES, + TERMINAL_SANDBOX_MODE, + LINUX_SAFETY, ) from modules.container_file_proxy import ContainerFileProxy from utils.logger import setup_logger @@ -70,9 +74,16 @@ class FileManager: "error": "容器未就绪,无法执行文件操作" } return self._container_proxy.run(action, payload) + + def _is_docker_mode(self) -> bool: + if self.container_session and getattr(self.container_session, "mode", None) is not None: + return getattr(self.container_session, "mode", None) == "docker" + return (TERMINAL_SANDBOX_MODE or "").lower() == "docker" or bool(LINUX_SAFETY) def _get_project_size(self) -> int: """计算项目目录的总大小(字节),遇到异常时记录并抛出。""" + if not self._is_docker_mode(): + return 0 total = 0 if not self.project_path.exists(): return 0 @@ -631,20 +642,21 @@ class FileManager: try: relative_path = self._relative_path(full_path) - current_size = self._get_project_size() - existing_size = full_path.stat().st_size if full_path.exists() else 0 - if mode == "a": - projected_total = current_size + len(content) - else: - projected_total = current_size - existing_size + len(content) - if PROJECT_MAX_STORAGE_BYTES and projected_total > PROJECT_MAX_STORAGE_BYTES: - return { - "success": False, - "error": "写入失败:超出项目磁盘配额", - "limit_bytes": PROJECT_MAX_STORAGE_BYTES, - "project_size_bytes": current_size, - "attempt_size_bytes": len(content) - } + if PROJECT_MAX_STORAGE_BYTES and self._is_docker_mode(): + current_size = self._get_project_size() + existing_size = full_path.stat().st_size if full_path.exists() else 0 + if mode == "a": + projected_total = current_size + len(content) + else: + projected_total = current_size - existing_size + len(content) + if projected_total > PROJECT_MAX_STORAGE_BYTES: + return { + "success": False, + "error": "写入失败:超出项目磁盘配额", + "limit_bytes": PROJECT_MAX_STORAGE_BYTES, + "project_size_bytes": current_size, + "attempt_size_bytes": len(content) + } if self._use_container(): result = self._container_call("write_file", { "path": relative_path, diff --git a/server/_conversation_segment.py b/server/_conversation_segment.py index 1e57663..aff5920 100644 --- a/server/_conversation_segment.py +++ b/server/_conversation_segment.py @@ -2366,8 +2366,9 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac sender('update_action', update_payload) if function_name in ['create_file', 'delete_file', 'rename_file', 'create_folder']: - structure = web_terminal.context_manager.get_project_structure() - sender('file_tree_update', structure) + if not web_terminal.context_manager._is_host_mode_without_safety(): + structure = web_terminal.context_manager.get_project_structure() + sender('file_tree_update', structure) # ===== 增量保存:立即保存工具结果 ===== metadata_payload = None diff --git a/server/chat_flow.py b/server/chat_flow.py index bfdbcfa..a2288ae 100644 --- a/server/chat_flow.py +++ b/server/chat_flow.py @@ -2280,8 +2280,9 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac sender('update_action', update_payload) if function_name in ['create_file', 'delete_file', 'rename_file', 'create_folder']: - structure = web_terminal.context_manager.get_project_structure() - sender('file_tree_update', structure) + if not web_terminal.context_manager._is_host_mode_without_safety(): + structure = web_terminal.context_manager.get_project_structure() + sender('file_tree_update', structure) # ===== 增量保存:立即保存工具结果 ===== metadata_payload = None diff --git a/static/src/app.ts b/static/src/app.ts index 89fc661..b038477 100644 --- a/static/src/app.ts +++ b/static/src/app.ts @@ -869,7 +869,8 @@ const appOptions = { fileSetTreeFromResponse: 'setFileTreeFromResponse', fileFetchTodoList: 'fetchTodoList', fileSetTodoList: 'setTodoList', - fileHideContextMenu: 'hideContextMenu' + fileHideContextMenu: 'hideContextMenu', + fileMarkTreeUnavailable: 'markFileTreeUnavailable' }), ...mapActions(useMonitorStore, { monitorSyncDesktop: 'syncDesktopFromTree', @@ -1455,11 +1456,6 @@ const appOptions = { try { debugLog('加载初始数据...'); - // 并行拉取文件/待办,优先获取状态以尽快同步运行模式 - const treePromise = this.fileFetchTree(); - const focusPromise = this.focusFetchFiles(); - const todoPromise = this.fileFetchTodoList(); - const statusResponse = await fetch('/api/status'); const statusData = await statusResponse.json(); this.projectPath = statusData.project_path || ''; @@ -1473,6 +1469,16 @@ const appOptions = { await policyStore.fetchPolicy(); this.applyPolicyUiLocks(); + const focusPromise = this.focusFetchFiles(); + const todoPromise = this.fileFetchTodoList(); + let treePromise: Promise | null = null; + const isHostMode = statusData?.container?.mode === 'host'; + if (isHostMode) { + this.fileMarkTreeUnavailable('宿主机模式下文件树不可用'); + } else { + treePromise = this.fileFetchTree(); + } + // 获取当前对话信息 const statusConversationId = statusData.conversation && statusData.conversation.current_id; if (statusConversationId && !this.currentConversationId) { @@ -1521,7 +1527,11 @@ const appOptions = { } // 等待其他加载项完成(允许部分失败不阻塞模式切换) - await Promise.allSettled([treePromise, focusPromise, todoPromise]); + const pendingPromises = [focusPromise, todoPromise]; + if (treePromise) { + pendingPromises.push(treePromise); + } + await Promise.allSettled(pendingPromises); await this.loadToolSettings(true); debugLog('初始数据加载完成'); diff --git a/static/src/components/panels/LeftPanel.vue b/static/src/components/panels/LeftPanel.vue index b8f3c4b..d1d2f6d 100644 --- a/static/src/components/panels/LeftPanel.vue +++ b/static/src/components/panels/LeftPanel.vue @@ -118,14 +118,18 @@
- +
{{ fileTreeMessage }}
+
@@ -218,7 +222,7 @@ const modeIndicatorTitle = computed(() => { }); const fileStore = useFileStore(); const subAgentStore = useSubAgentStore(); -const { fileTree, expandedFolders, todoList } = storeToRefs(fileStore); +const { fileTree, expandedFolders, todoList, fileTreeUnavailable, fileTreeMessage } = storeToRefs(fileStore); const { subAgents } = storeToRefs(subAgentStore); const openSubAgent = (agent: any) => { diff --git a/static/src/stores/file.ts b/static/src/stores/file.ts index d696c56..9d828d9 100644 --- a/static/src/stores/file.ts +++ b/static/src/stores/file.ts @@ -38,6 +38,8 @@ interface FileState { fileTree: FileNode[]; expandedFolders: Record; todoList: TodoList | null; + fileTreeUnavailable: boolean; + fileTreeMessage: string; contextMenu: ContextMenuState; } @@ -76,6 +78,8 @@ export const useFileStore = defineStore('file', { fileTree: [], expandedFolders: {}, todoList: null, + fileTreeUnavailable: false, + fileTreeMessage: '', contextMenu: { visible: false, x: 0, @@ -94,16 +98,32 @@ export const useFileStore = defineStore('file', { console.error('获取文件树失败:', error); } }, + markFileTreeUnavailable(message: string) { + this.fileTreeUnavailable = true; + this.fileTreeMessage = message || '宿主机模式下文件树不可用'; + this.fileTree = []; + this.expandedFolders = {}; + }, setFileTreeFromResponse(payload: any) { if (!payload) { console.warn('[FileTree] 空 payload'); return; } + if (payload.unavailable) { + this.markFileTreeUnavailable(payload.message || payload.error); + return; + } const structure = payload.structure || payload; + if (structure && structure.unavailable) { + this.markFileTreeUnavailable(structure.message || structure.error); + return; + } if (!structure || !structure.tree) { console.warn('[FileTree] 缺少 structure.tree', structure); return; } + this.fileTreeUnavailable = false; + this.fileTreeMessage = ''; const nodes = buildNodes(structure.tree); const expanded = { ...this.expandedFolders }; const validFolderPaths = new Set(); diff --git a/static/src/styles/components/panels/_left-panel.scss b/static/src/styles/components/panels/_left-panel.scss index 9cebb78..ecc47de 100644 --- a/static/src/styles/components/panels/_left-panel.scss +++ b/static/src/styles/components/panels/_left-panel.scss @@ -442,7 +442,8 @@ } .todo-empty, -.sub-agent-empty { +.sub-agent-empty, +.file-tree-empty { font-size: 14px; color: var(--claude-text-secondary); padding: 12px; diff --git a/utils/context_manager.py b/utils/context_manager.py index 2c2f441..2f5b829 100644 --- a/utils/context_manager.py +++ b/utils/context_manager.py @@ -439,7 +439,7 @@ class ContextManager: meta = self.conversation_metadata or {} stored_tree = meta.get("project_file_tree") - if stored_tree: + if stored_tree and stored_tree != "宿主机模式下文件树不可用": self.project_snapshot = { "file_tree": stored_tree, "statistics": meta.get("project_statistics"), @@ -448,7 +448,7 @@ class ContextManager: return self.project_snapshot # 首次生成并缓存 - structure = self.get_project_structure() + structure = self._get_project_structure_for_prompt() snapshot = { "file_tree": self._build_file_tree(structure), "statistics": { @@ -470,6 +470,63 @@ class ContextManager: self.conversation_metadata["project_statistics"] = snapshot["statistics"] self.conversation_metadata["project_snapshot_at"] = snapshot["snapshot_at"] return snapshot + + def _get_project_structure_for_prompt(self, limit: int = 20) -> Dict: + """获取用于 prompt 的浅层文件结构(仅根目录,优先文件夹)。""" + structure = { + "path": str(self.project_path), + "files": [], + "folders": [], + "total_files": 0, + "total_size": 0, + "tree": {} + } + if not self.project_path.exists(): + return structure + try: + entries = [p for p in self.project_path.iterdir() if not p.name.startswith('.')] + except PermissionError: + return structure + + folders = [p for p in entries if p.is_dir()] + files = [p for p in entries if p.is_file()] + folders.sort(key=lambda p: p.name.lower()) + files.sort(key=lambda p: p.name.lower()) + selected = (folders + files)[:max(0, limit)] + + for entry in selected: + relative_path = str(entry.relative_to(self.project_path)) + if entry.is_dir(): + structure["folders"].append({ + "name": entry.name, + "path": relative_path + }) + structure["tree"][entry.name] = { + "type": "folder", + "path": relative_path, + "children": {} + } + else: + try: + size = entry.stat().st_size + except OSError: + size = 0 + file_info = { + "name": entry.name, + "path": relative_path, + "size": size, + "annotation": self.file_annotations.get(relative_path, "") + } + structure["files"].append(file_info) + structure["total_files"] += 1 + structure["total_size"] += size + structure["tree"][entry.name] = { + "type": "file", + "path": relative_path, + "size": size, + "annotation": file_info["annotation"] + } + return structure def save_current_conversation(self) -> bool: """ @@ -928,6 +985,11 @@ class ContextManager: "total_size": 0, "tree": {} # 新增:树形结构数据 } + + if self._is_host_mode_without_safety(): + structure["unavailable"] = True + structure["message"] = "宿主机模式下文件树不可用" + return structure # 记录实际存在的文件 existing_files = set() @@ -1415,7 +1477,11 @@ class ContextManager: container_memory=self.container_memory_limit, project_storage=self.project_storage_limit, current_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - file_tree=context["project_info"]["file_tree"], + file_tree=( + "(以下为工作区根目录的部分文件和文件夹)\n" + context["project_info"]["file_tree"] + if context["project_info"].get("file_tree") + else "" + ), memory=context["memory"], )