fix: reduce workspace scans in host mode
This commit is contained in:
parent
55ef45e04d
commit
b0941a247b
@ -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))
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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('初始数据加载完成');
|
||||
|
||||
@ -118,14 +118,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="file-tree" @contextmenu.prevent>
|
||||
<FileNode
|
||||
v-for="node in fileTree"
|
||||
:key="node.path"
|
||||
:node="node"
|
||||
:level="0"
|
||||
:expanded-folders="expandedFolders"
|
||||
:icon-style="iconStyle"
|
||||
/>
|
||||
<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"
|
||||
:node="node"
|
||||
:level="0"
|
||||
: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) => {
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"],
|
||||
)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user