agent-Specialization/static/src/stores/file.ts

233 lines
5.8 KiB
TypeScript

import { defineStore } from 'pinia';
const FILE_STORE_DEBUG_LOGS = false;
function fileDebugLog(...args: unknown[]) {
if (!FILE_STORE_DEBUG_LOGS) {
return;
}
console.log(...args);
}
interface FileNode {
type: 'folder' | 'file';
name: string;
path: string;
annotation?: string;
children?: FileNode[];
}
interface TodoTask {
index: number;
title: string;
status: string;
}
interface TodoList {
instruction?: string;
tasks?: TodoTask[];
}
interface ContextMenuState {
visible: boolean;
x: number;
y: number;
node: FileNode | null;
}
interface FileState {
fileTree: FileNode[];
expandedFolders: Record<string, boolean>;
todoList: TodoList | null;
fileTreeUnavailable: boolean;
fileTreeMessage: string;
contextMenu: ContextMenuState;
}
function buildNodes(treeMap: Record<string, any> | undefined): FileNode[] {
if (!treeMap) {
return [];
}
const entries = Object.keys(treeMap).map(name => {
const node = treeMap[name] || {};
if (node.type === 'folder') {
return {
type: 'folder' as const,
name,
path: node.path || name,
children: buildNodes(node.children)
};
}
return {
type: 'file' as const,
name,
path: node.path || name,
annotation: node.annotation || ''
};
});
entries.sort((a, b) => {
if (a.type !== b.type) {
return a.type === 'folder' ? -1 : 1;
}
return a.name.localeCompare(b.name, 'zh-CN');
});
return entries;
}
export const useFileStore = defineStore('file', {
state: (): FileState => ({
fileTree: [],
expandedFolders: {},
todoList: null,
fileTreeUnavailable: false,
fileTreeMessage: '',
contextMenu: {
visible: false,
x: 0,
y: 0,
node: null
}
}),
actions: {
async fetchFileTree() {
try {
const response = await fetch('/api/files');
const data = await response.json();
fileDebugLog('[FileTree] fetch result', data);
this.setFileTreeFromResponse(data);
} catch (error) {
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>();
const ensureExpansion = (list: FileNode[]) => {
list.forEach(item => {
if (item.type === 'folder') {
validFolderPaths.add(item.path);
if (expanded[item.path] === undefined) {
expanded[item.path] = false;
}
ensureExpansion(item.children || []);
}
});
};
ensureExpansion(nodes);
Object.keys(expanded).forEach(path => {
if (!validFolderPaths.has(path)) {
delete expanded[path];
}
});
this.expandedFolders = expanded;
this.fileTree = nodes;
},
toggleFolder(path: string) {
if (!path) {
return;
}
const current = !!this.expandedFolders[path];
fileDebugLog('[FileTree] toggle folder', path, '=>', !current);
this.expandedFolders = {
...this.expandedFolders,
[path]: !current
};
},
async fetchTodoList() {
try {
const response = await fetch('/api/todo-list');
const data = await response.json();
if (data && data.success) {
this.todoList = data.data || null;
}
} catch (error) {
console.error('获取待办列表失败:', error);
}
},
setTodoList(payload: TodoList | null) {
this.todoList = payload;
},
showContextMenu(payload: { node: FileNode; event: MouseEvent }) {
if (!payload || !payload.node) {
return;
}
const { node, event } = payload;
if (!node.path && node.path !== '') {
this.hideContextMenu();
return;
}
if (node.type !== 'file' && node.type !== 'folder') {
this.hideContextMenu();
return;
}
if (event && typeof event.preventDefault === 'function') {
event.preventDefault();
}
if (event && typeof event.stopPropagation === 'function') {
event.stopPropagation();
}
const menuWidth = 200;
const menuHeight = 50;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let x = (event && event.clientX) || 0;
let y = (event && event.clientY) || 0;
if (x + menuWidth > viewportWidth) {
x = viewportWidth - menuWidth - 8;
}
if (y + menuHeight > viewportHeight) {
y = viewportHeight - menuHeight - 8;
}
this.contextMenu = {
visible: true,
x: Math.max(8, x),
y: Math.max(8, y),
node
};
},
hideContextMenu() {
if (!this.contextMenu.visible) {
return;
}
this.contextMenu = {
visible: false,
x: 0,
y: 0,
node: null
};
}
}
});