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 = ""):
|
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()
|
structure = self.context_manager.get_project_structure()
|
||||||
print(f"\n📁 项目文件结构:")
|
print(f"\n📁 项目文件结构:")
|
||||||
print(self.context_manager._build_file_tree(structure))
|
print(self.context_manager._build_file_tree(structure))
|
||||||
|
|||||||
@ -566,6 +566,7 @@ class WebTerminal(MainTerminal):
|
|||||||
|
|
||||||
# 如果是文件操作,广播文件树更新
|
# 如果是文件操作,广播文件树更新
|
||||||
if tool_name in ['create_file', 'delete_file', 'rename_file', 'create_folder', 'save_webpage']:
|
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:
|
try:
|
||||||
structure = self.context_manager.get_project_structure()
|
structure = self.context_manager.get_project_structure()
|
||||||
self.broadcast('file_tree_update', structure)
|
self.broadcast('file_tree_update', structure)
|
||||||
|
|||||||
@ -14,6 +14,8 @@ try:
|
|||||||
OUTPUT_FORMATS,
|
OUTPUT_FORMATS,
|
||||||
READ_TOOL_MAX_FILE_SIZE,
|
READ_TOOL_MAX_FILE_SIZE,
|
||||||
PROJECT_MAX_STORAGE_BYTES,
|
PROJECT_MAX_STORAGE_BYTES,
|
||||||
|
TERMINAL_SANDBOX_MODE,
|
||||||
|
LINUX_SAFETY,
|
||||||
)
|
)
|
||||||
except ImportError: # 兼容全局环境中存在同名包的情况
|
except ImportError: # 兼容全局环境中存在同名包的情况
|
||||||
import sys
|
import sys
|
||||||
@ -28,6 +30,8 @@ except ImportError: # 兼容全局环境中存在同名包的情况
|
|||||||
OUTPUT_FORMATS,
|
OUTPUT_FORMATS,
|
||||||
READ_TOOL_MAX_FILE_SIZE,
|
READ_TOOL_MAX_FILE_SIZE,
|
||||||
PROJECT_MAX_STORAGE_BYTES,
|
PROJECT_MAX_STORAGE_BYTES,
|
||||||
|
TERMINAL_SANDBOX_MODE,
|
||||||
|
LINUX_SAFETY,
|
||||||
)
|
)
|
||||||
from modules.container_file_proxy import ContainerFileProxy
|
from modules.container_file_proxy import ContainerFileProxy
|
||||||
from utils.logger import setup_logger
|
from utils.logger import setup_logger
|
||||||
@ -71,8 +75,15 @@ class FileManager:
|
|||||||
}
|
}
|
||||||
return self._container_proxy.run(action, payload)
|
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:
|
def _get_project_size(self) -> int:
|
||||||
"""计算项目目录的总大小(字节),遇到异常时记录并抛出。"""
|
"""计算项目目录的总大小(字节),遇到异常时记录并抛出。"""
|
||||||
|
if not self._is_docker_mode():
|
||||||
|
return 0
|
||||||
total = 0
|
total = 0
|
||||||
if not self.project_path.exists():
|
if not self.project_path.exists():
|
||||||
return 0
|
return 0
|
||||||
@ -631,13 +642,14 @@ class FileManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
relative_path = self._relative_path(full_path)
|
relative_path = self._relative_path(full_path)
|
||||||
|
if PROJECT_MAX_STORAGE_BYTES and self._is_docker_mode():
|
||||||
current_size = self._get_project_size()
|
current_size = self._get_project_size()
|
||||||
existing_size = full_path.stat().st_size if full_path.exists() else 0
|
existing_size = full_path.stat().st_size if full_path.exists() else 0
|
||||||
if mode == "a":
|
if mode == "a":
|
||||||
projected_total = current_size + len(content)
|
projected_total = current_size + len(content)
|
||||||
else:
|
else:
|
||||||
projected_total = current_size - existing_size + len(content)
|
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 {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": "写入失败:超出项目磁盘配额",
|
"error": "写入失败:超出项目磁盘配额",
|
||||||
|
|||||||
@ -2366,6 +2366,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
|
|||||||
sender('update_action', update_payload)
|
sender('update_action', update_payload)
|
||||||
|
|
||||||
if function_name in ['create_file', 'delete_file', 'rename_file', 'create_folder']:
|
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()
|
structure = web_terminal.context_manager.get_project_structure()
|
||||||
sender('file_tree_update', structure)
|
sender('file_tree_update', structure)
|
||||||
|
|
||||||
|
|||||||
@ -2280,6 +2280,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
|
|||||||
sender('update_action', update_payload)
|
sender('update_action', update_payload)
|
||||||
|
|
||||||
if function_name in ['create_file', 'delete_file', 'rename_file', 'create_folder']:
|
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()
|
structure = web_terminal.context_manager.get_project_structure()
|
||||||
sender('file_tree_update', structure)
|
sender('file_tree_update', structure)
|
||||||
|
|
||||||
|
|||||||
@ -869,7 +869,8 @@ const appOptions = {
|
|||||||
fileSetTreeFromResponse: 'setFileTreeFromResponse',
|
fileSetTreeFromResponse: 'setFileTreeFromResponse',
|
||||||
fileFetchTodoList: 'fetchTodoList',
|
fileFetchTodoList: 'fetchTodoList',
|
||||||
fileSetTodoList: 'setTodoList',
|
fileSetTodoList: 'setTodoList',
|
||||||
fileHideContextMenu: 'hideContextMenu'
|
fileHideContextMenu: 'hideContextMenu',
|
||||||
|
fileMarkTreeUnavailable: 'markFileTreeUnavailable'
|
||||||
}),
|
}),
|
||||||
...mapActions(useMonitorStore, {
|
...mapActions(useMonitorStore, {
|
||||||
monitorSyncDesktop: 'syncDesktopFromTree',
|
monitorSyncDesktop: 'syncDesktopFromTree',
|
||||||
@ -1455,11 +1456,6 @@ const appOptions = {
|
|||||||
try {
|
try {
|
||||||
debugLog('加载初始数据...');
|
debugLog('加载初始数据...');
|
||||||
|
|
||||||
// 并行拉取文件/待办,优先获取状态以尽快同步运行模式
|
|
||||||
const treePromise = this.fileFetchTree();
|
|
||||||
const focusPromise = this.focusFetchFiles();
|
|
||||||
const todoPromise = this.fileFetchTodoList();
|
|
||||||
|
|
||||||
const statusResponse = await fetch('/api/status');
|
const statusResponse = await fetch('/api/status');
|
||||||
const statusData = await statusResponse.json();
|
const statusData = await statusResponse.json();
|
||||||
this.projectPath = statusData.project_path || '';
|
this.projectPath = statusData.project_path || '';
|
||||||
@ -1473,6 +1469,16 @@ const appOptions = {
|
|||||||
await policyStore.fetchPolicy();
|
await policyStore.fetchPolicy();
|
||||||
this.applyPolicyUiLocks();
|
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;
|
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
|
||||||
if (statusConversationId && !this.currentConversationId) {
|
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);
|
await this.loadToolSettings(true);
|
||||||
|
|
||||||
debugLog('初始数据加载完成');
|
debugLog('初始数据加载完成');
|
||||||
|
|||||||
@ -118,6 +118,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="file-tree" @contextmenu.prevent>
|
<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
|
<FileNode
|
||||||
v-for="node in fileTree"
|
v-for="node in fileTree"
|
||||||
:key="node.path"
|
:key="node.path"
|
||||||
@ -126,6 +129,7 @@
|
|||||||
:expanded-folders="expandedFolders"
|
:expanded-folders="expandedFolders"
|
||||||
:icon-style="iconStyle"
|
:icon-style="iconStyle"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -218,7 +222,7 @@ const modeIndicatorTitle = computed(() => {
|
|||||||
});
|
});
|
||||||
const fileStore = useFileStore();
|
const fileStore = useFileStore();
|
||||||
const subAgentStore = useSubAgentStore();
|
const subAgentStore = useSubAgentStore();
|
||||||
const { fileTree, expandedFolders, todoList } = storeToRefs(fileStore);
|
const { fileTree, expandedFolders, todoList, fileTreeUnavailable, fileTreeMessage } = storeToRefs(fileStore);
|
||||||
const { subAgents } = storeToRefs(subAgentStore);
|
const { subAgents } = storeToRefs(subAgentStore);
|
||||||
|
|
||||||
const openSubAgent = (agent: any) => {
|
const openSubAgent = (agent: any) => {
|
||||||
|
|||||||
@ -38,6 +38,8 @@ interface FileState {
|
|||||||
fileTree: FileNode[];
|
fileTree: FileNode[];
|
||||||
expandedFolders: Record<string, boolean>;
|
expandedFolders: Record<string, boolean>;
|
||||||
todoList: TodoList | null;
|
todoList: TodoList | null;
|
||||||
|
fileTreeUnavailable: boolean;
|
||||||
|
fileTreeMessage: string;
|
||||||
contextMenu: ContextMenuState;
|
contextMenu: ContextMenuState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,6 +78,8 @@ export const useFileStore = defineStore('file', {
|
|||||||
fileTree: [],
|
fileTree: [],
|
||||||
expandedFolders: {},
|
expandedFolders: {},
|
||||||
todoList: null,
|
todoList: null,
|
||||||
|
fileTreeUnavailable: false,
|
||||||
|
fileTreeMessage: '',
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
visible: false,
|
visible: false,
|
||||||
x: 0,
|
x: 0,
|
||||||
@ -94,16 +98,32 @@ export const useFileStore = defineStore('file', {
|
|||||||
console.error('获取文件树失败:', error);
|
console.error('获取文件树失败:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
markFileTreeUnavailable(message: string) {
|
||||||
|
this.fileTreeUnavailable = true;
|
||||||
|
this.fileTreeMessage = message || '宿主机模式下文件树不可用';
|
||||||
|
this.fileTree = [];
|
||||||
|
this.expandedFolders = {};
|
||||||
|
},
|
||||||
setFileTreeFromResponse(payload: any) {
|
setFileTreeFromResponse(payload: any) {
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
console.warn('[FileTree] 空 payload');
|
console.warn('[FileTree] 空 payload');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (payload.unavailable) {
|
||||||
|
this.markFileTreeUnavailable(payload.message || payload.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const structure = payload.structure || payload;
|
const structure = payload.structure || payload;
|
||||||
|
if (structure && structure.unavailable) {
|
||||||
|
this.markFileTreeUnavailable(structure.message || structure.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!structure || !structure.tree) {
|
if (!structure || !structure.tree) {
|
||||||
console.warn('[FileTree] 缺少 structure.tree', structure);
|
console.warn('[FileTree] 缺少 structure.tree', structure);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.fileTreeUnavailable = false;
|
||||||
|
this.fileTreeMessage = '';
|
||||||
const nodes = buildNodes(structure.tree);
|
const nodes = buildNodes(structure.tree);
|
||||||
const expanded = { ...this.expandedFolders };
|
const expanded = { ...this.expandedFolders };
|
||||||
const validFolderPaths = new Set<string>();
|
const validFolderPaths = new Set<string>();
|
||||||
|
|||||||
@ -442,7 +442,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.todo-empty,
|
.todo-empty,
|
||||||
.sub-agent-empty {
|
.sub-agent-empty,
|
||||||
|
.file-tree-empty {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--claude-text-secondary);
|
color: var(--claude-text-secondary);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|||||||
@ -439,7 +439,7 @@ class ContextManager:
|
|||||||
|
|
||||||
meta = self.conversation_metadata or {}
|
meta = self.conversation_metadata or {}
|
||||||
stored_tree = meta.get("project_file_tree")
|
stored_tree = meta.get("project_file_tree")
|
||||||
if stored_tree:
|
if stored_tree and stored_tree != "宿主机模式下文件树不可用":
|
||||||
self.project_snapshot = {
|
self.project_snapshot = {
|
||||||
"file_tree": stored_tree,
|
"file_tree": stored_tree,
|
||||||
"statistics": meta.get("project_statistics"),
|
"statistics": meta.get("project_statistics"),
|
||||||
@ -448,7 +448,7 @@ class ContextManager:
|
|||||||
return self.project_snapshot
|
return self.project_snapshot
|
||||||
|
|
||||||
# 首次生成并缓存
|
# 首次生成并缓存
|
||||||
structure = self.get_project_structure()
|
structure = self._get_project_structure_for_prompt()
|
||||||
snapshot = {
|
snapshot = {
|
||||||
"file_tree": self._build_file_tree(structure),
|
"file_tree": self._build_file_tree(structure),
|
||||||
"statistics": {
|
"statistics": {
|
||||||
@ -471,6 +471,63 @@ class ContextManager:
|
|||||||
self.conversation_metadata["project_snapshot_at"] = snapshot["snapshot_at"]
|
self.conversation_metadata["project_snapshot_at"] = snapshot["snapshot_at"]
|
||||||
return snapshot
|
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:
|
def save_current_conversation(self) -> bool:
|
||||||
"""
|
"""
|
||||||
保存当前对话
|
保存当前对话
|
||||||
@ -929,6 +986,11 @@ class ContextManager:
|
|||||||
"tree": {} # 新增:树形结构数据
|
"tree": {} # 新增:树形结构数据
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self._is_host_mode_without_safety():
|
||||||
|
structure["unavailable"] = True
|
||||||
|
structure["message"] = "宿主机模式下文件树不可用"
|
||||||
|
return structure
|
||||||
|
|
||||||
# 记录实际存在的文件
|
# 记录实际存在的文件
|
||||||
existing_files = set()
|
existing_files = set()
|
||||||
|
|
||||||
@ -1415,7 +1477,11 @@ class ContextManager:
|
|||||||
container_memory=self.container_memory_limit,
|
container_memory=self.container_memory_limit,
|
||||||
project_storage=self.project_storage_limit,
|
project_storage=self.project_storage_limit,
|
||||||
current_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
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"],
|
memory=context["memory"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user