fix: reduce workspace scans in host mode

This commit is contained in:
JOJO 2026-02-06 17:09:19 +08:00
parent 55ef45e04d
commit b0941a247b
10 changed files with 162 additions and 43 deletions

View File

@ -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))

View File

@ -566,6 +566,7 @@ class WebTerminal(MainTerminal):
# 如果是文件操作,广播文件树更新
if tool_name in ['create_file', 'delete_file', 'rename_file', 'create_folder', 'save_webpage']:
if not self.context_manager._is_host_mode_without_safety():
try:
structure = self.context_manager.get_project_structure()
self.broadcast('file_tree_update', structure)

View File

@ -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
@ -71,8 +75,15 @@ class FileManager:
}
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,13 +642,14 @@ class FileManager:
try:
relative_path = self._relative_path(full_path)
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 PROJECT_MAX_STORAGE_BYTES and projected_total > PROJECT_MAX_STORAGE_BYTES:
if projected_total > PROJECT_MAX_STORAGE_BYTES:
return {
"success": False,
"error": "写入失败:超出项目磁盘配额",

View File

@ -2366,6 +2366,7 @@ 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']:
if not web_terminal.context_manager._is_host_mode_without_safety():
structure = web_terminal.context_manager.get_project_structure()
sender('file_tree_update', structure)

View File

@ -2280,6 +2280,7 @@ 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']:
if not web_terminal.context_manager._is_host_mode_without_safety():
structure = web_terminal.context_manager.get_project_structure()
sender('file_tree_update', structure)

View File

@ -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<any> | 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('初始数据加载完成');

View File

@ -118,6 +118,9 @@
</div>
</div>
<div v-else class="file-tree" @contextmenu.prevent>
<div v-if="fileTreeUnavailable" class="file-tree-empty">{{ fileTreeMessage }}</div>
<template v-else>
<div v-if="!fileTree.length" class="file-tree-empty">暂无文件</div>
<FileNode
v-for="node in fileTree"
:key="node.path"
@ -126,6 +129,7 @@
:expanded-folders="expandedFolders"
:icon-style="iconStyle"
/>
</template>
</div>
</div>
</div>
@ -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) => {

View File

@ -38,6 +38,8 @@ interface FileState {
fileTree: FileNode[];
expandedFolders: Record<string, boolean>;
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<string>();

View File

@ -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;

View File

@ -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": {
@ -471,6 +471,63 @@ class ContextManager:
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:
"""
保存当前对话
@ -929,6 +986,11 @@ class ContextManager:
"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"],
)