agent-Specialization/static/src/stores/monitor.ts
JOJO 820ec69354 feat(memory): 新增项目记忆系统,整合总体+项目记忆注入
- 新建 .agents/memory/ 项目记忆目录,支持 YAML frontmatter + Markdown
- 新增 update_project_memory / recall_project_memory 工具(复用 write_file/read_file)
- 新建 memory_system.txt prompt,总体长期记忆 + 项目记忆统一注入
- 记忆从 workspace_system.txt 中移出,独立为 memory_prompt 注入
- 重排 system prompt 注入顺序:主→权限→环境→对话→个性化→工作区→AGENTS.md→skills→记忆→禁用
- 压缩后选择性 invalidate 4 个 frozen keys(skills/workspace/personalization/memory)
- 前端:notebook/notebook-pen 图标、动画、状态文案、展开内容渲染
- 优化三个记忆工具的 description 和 memory_system prompt,引导模型主动调用
2026-06-10 15:14:01 +08:00

678 lines
20 KiB
TypeScript

import { defineStore } from 'pinia';
import type {
MonitorBubbleOptions,
MonitorDriver,
MonitorSceneRuntime
} from '@/components/chat/monitor/types';
import { getSceneProgressLabel } from '@/components/chat/monitor/progressMap';
import { useConnectionStore } from '@/stores/connection';
const MONITOR_DEBUG_LOGS = false;
const monitorDebug = (...args: any[]) => {
if (!MONITOR_DEBUG_LOGS) {
return;
}
console.debug('[Monitor]', ...args);
};
const monitorTrace = (label: string) => {
if (!MONITOR_DEBUG_LOGS) {
return;
}
console.trace(`[Monitor] ${label}`);
};
const MONITOR_PROGRESS_DEBUG = false;
const monitorProgressDebug = (...args: any[]) => {
if (!MONITOR_PROGRESS_DEBUG) {
return;
}
console.debug('[MonitorProgressStore]', ...args);
};
const MONITOR_LIFECYCLE_DEBUG = false;
const monitorLifecycleLog = (...args: any[]) => {
if (!MONITOR_LIFECYCLE_DEBUG) {
return;
}
// 使用 console.info 方便在控制台中筛选
console.info('[MonitorLifecycle]', ...args);
};
type MonitorEvent = {
id: string;
script: string;
payload: Record<string, any>;
};
type PendingResultEntry = {
promise: Promise<any>;
resolve: (value: any) => void;
timeout: ReturnType<typeof setTimeout> | null;
settled: boolean;
};
interface MonitorState {
statusLabel: string;
queue: MonitorEvent[];
playing: boolean;
awaitingTools: Record<string, string>;
lastTreeSnapshot: string[];
lastSpeechAt: number;
thinkingActive: boolean;
pendingResults: Record<string, PendingResultEntry>;
completedResults: Record<string, any>;
driver: MonitorDriver | null;
speechBuffer: string;
bubbleActive: boolean;
pendingProgressScene: string | null;
progressIndicator: {
id: string | null;
label: string;
scene: string | null;
};
}
const DEFAULT_ROOTS = [
'__pycache__',
'config',
'core',
'data',
'doc',
'docker',
'logs',
'modules',
'node_modules',
'project',
'prompts',
'scratch_test',
'scripts',
'static',
'sub_agent',
'test',
'users',
'utils'
];
const COMPLETED_RESULT_CACHE_LIMIT = 32;
const TOOL_SCENE_MAP: Record<string, string> = {
web_search: 'browserSearch',
extract_webpage: 'webExtract',
save_webpage: 'webSave',
read_file: 'reader',
view_video: 'reader',
vlm_analyze: 'ocr',
ocr_image: 'ocr',
create_folder: 'createFolder',
create_file: 'createFile',
rename_file: 'renameFile',
delete_file: 'deleteFile',
write_file: 'modifyFile',
edit_file: 'modifyFile',
run_command: 'runCommand',
terminal_session: 'terminalSession',
terminal_input: 'terminalInput',
terminal_snapshot: 'terminalSnapshot',
sleep: 'terminalSleep',
update_memory: 'memoryUpdate',
recall_project_memory: 'reader',
update_project_memory: 'memoryUpdate',
todo_create: 'todoCreate',
todo_update_task: 'todoUpdate',
todo_delete_task: 'todoDelete',
terminal_run: 'runCommand'
};
const randomId = (prefix = 'evt') =>
`${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
export const useMonitorStore = defineStore('monitor', {
state: (): MonitorState => ({
statusLabel: '待机',
queue: [],
playing: false,
awaitingTools: {},
lastTreeSnapshot: [...DEFAULT_ROOTS],
lastSpeechAt: 0,
thinkingActive: false,
pendingResults: {},
completedResults: {},
driver: null,
speechBuffer: '',
bubbleActive: false,
pendingProgressScene: null,
progressIndicator: {
id: null,
label: '',
scene: null
}
}),
getters: {
isLocked: (state) => state.playing || state.queue.length > 0
},
actions: {
registerDriver(driver: MonitorDriver) {
this.driver = driver;
this.driver.resetScene({
desktopRoots: this.lastTreeSnapshot,
preserveBubble: this.bubbleActive,
preservePointer: false, // 初次挂载重置指针
preserveWindows: false
});
if (this.speechBuffer) {
this.driver.showSpeechBubble(this.speechBuffer, { variant: 'info', duration: 0 });
this.bubbleActive = true;
}
this.driver.setManualInteractionEnabled(!this.isLocked);
if (this.pendingProgressScene) {
monitorProgressDebug('registerDriver:apply-pending', { scene: this.pendingProgressScene });
this.driver.previewSceneProgress(this.pendingProgressScene);
this.pendingProgressScene = null;
}
if (this.queue.length) {
this.processQueue();
}
},
unregisterDriver() {
if (this.driver) {
this.driver.setManualInteractionEnabled(false);
this.driver.hideBubble();
}
this.driver = null;
this.playing = false;
},
syncDesktopFromTree(tree: Array<{ type: string; name: string }> | null) {
if (!Array.isArray(tree) || !tree.length) {
this.lastTreeSnapshot = [...DEFAULT_ROOTS];
this.driver?.setDesktopRoots(this.lastTreeSnapshot, { immediate: true });
return;
}
const folders = tree
.filter((node) => node && node.type === 'folder')
.map((node) => node.name)
.filter(Boolean);
this.lastTreeSnapshot = folders.length ? folders : [...DEFAULT_ROOTS];
this.driver?.setDesktopRoots(this.lastTreeSnapshot);
},
resetVisualState(options?: {
preserveBubble?: boolean;
preservePointer?: boolean;
preserveWindows?: boolean;
preserveQueue?: boolean;
preservePendingResults?: boolean;
preserveAwaitingTools?: boolean;
}) {
const preserveBubble = !!options?.preserveBubble;
const preservePointer = options?.preservePointer !== false;
const preserveWindows = !!options?.preserveWindows;
const preserveQueue = !!options?.preserveQueue;
const preservePendingResults = !!options?.preservePendingResults;
const preserveAwaitingTools = !!options?.preserveAwaitingTools;
this.statusLabel = '待机';
if (!preserveQueue) {
this.queue = [];
this.playing = false;
}
this.lastSpeechAt = 0;
if (!preserveAwaitingTools) {
this.awaitingTools = {};
}
if (!preservePendingResults) {
Object.values(this.pendingResults).forEach((entry) => {
if (entry?.timeout) {
clearTimeout(entry.timeout);
}
});
this.pendingResults = {};
this.completedResults = {};
}
if (!preserveBubble) {
this.speechBuffer = '';
this.bubbleActive = false;
}
this.thinkingActive = false;
this.pendingProgressScene = null;
this.progressIndicator = { id: null, label: '', scene: null };
this.driver?.resetScene({
desktopRoots: this.lastTreeSnapshot,
preserveBubble,
preservePointer,
preserveWindows
});
this.driver?.setManualInteractionEnabled(!this.isLocked);
},
setProgressIndicator(payload: { id: string | null; label: string; scene: string }) {
this.progressIndicator = {
id: payload.id,
label: payload.label,
scene: payload.scene
};
monitorProgressDebug('progress-indicator:set', this.progressIndicator);
},
/**
* 仅用于模型刚检测到工具调用阶段的预览,不入队、不等待结果。
* 使用空 id 以便后续真正的 tool_start/update_action 可以顺利覆盖与清除。
*/
previewToolIntent(payload: Record<string, any>) {
if (this.bubbleActive) {
this.hideBubble(`tool-intent:${payload?.name || 'unknown'}`);
}
this.speechBuffer = '';
const script = TOOL_SCENE_MAP[payload.name] || 'genericTool';
const progressLabel = getSceneProgressLabel(script);
if (!progressLabel) {
return;
}
this.setStatus(progressLabel);
this.progressIndicator = {
id: null,
label: progressLabel,
scene: script
};
monitorProgressDebug('preview-tool-intent', {
tool: payload.name,
script,
label: progressLabel
});
if (this.driver) {
this.driver.previewSceneProgress(script);
} else {
this.pendingProgressScene = script;
}
},
clearProgressIndicator(id?: string | null) {
if (id && this.progressIndicator.id && id !== this.progressIndicator.id) {
return;
}
monitorProgressDebug('progress-indicator:clear', {
id: this.progressIndicator.id,
label: this.progressIndicator.label
});
this.progressIndicator = { id: null, label: '', scene: null };
},
setStatus(label: string) {
this.statusLabel = label;
},
showBubble(text: string, options: MonitorBubbleOptions = {}) {
if (!text) {
return;
}
monitorDebug('showBubble', {
text,
variant: options?.variant,
duration: options?.duration
});
this.driver?.showSpeechBubble(text, options);
this.bubbleActive = true;
},
hideBubble(reason?: string) {
monitorDebug('hideBubble', reason || '');
monitorTrace('hideBubble');
this.driver?.hideBubble();
this.bubbleActive = false;
this.thinkingActive = false;
},
resetSpeechBuffer() {
monitorDebug('resetSpeechBuffer');
this.speechBuffer = '';
},
showPendingReply() {
this.setStatus('待机');
if (this.driver && typeof this.driver.showWaitingBubble === 'function') {
this.driver.showWaitingBubble('等待回复');
this.bubbleActive = true;
return;
}
this.driver?.showSpeechBubble('等待回复...', { variant: 'info', duration: 0 });
this.bubbleActive = true;
},
enqueueModelSpeech(text: string) {
if (!text) {
return;
}
this.setStatus('正在规划');
this.speechBuffer = `${this.speechBuffer}${text}`;
this.lastSpeechAt = Date.now();
monitorDebug('enqueueModelSpeech', {
incoming: text,
combined: this.speechBuffer
});
if (this.playing || !this.driver) {
this.bubbleActive = false;
return;
}
this.driver.showSpeechBubble(this.speechBuffer, { variant: 'info', duration: 0 });
this.bubbleActive = true;
},
enqueueModelThinking() {
const connection = useConnectionStore();
const skipThinking = connection.runMode === 'fast' || connection.thinkingMode === false;
if (skipThinking) {
monitorDebug('enqueueModelThinking skipped');
return;
}
if (this.playing || !this.driver) {
return;
}
if (this.thinkingActive) {
monitorDebug('enqueueModelThinking already active');
return;
}
this.setStatus('思考中');
monitorDebug('enqueueModelThinking show bubble');
this.driver?.showThinkingBubble();
this.bubbleActive = true;
this.thinkingActive = true;
},
endModelOutput() {
this.setStatus('待机');
this.thinkingActive = false;
},
enqueueToolEvent(payload: Record<string, any>) {
const now = Date.now();
const recentSpeechGap = now - this.lastSpeechAt;
const MIN_SPEECH_VISIBLE_MS = 480;
const script = TOOL_SCENE_MAP[payload.name] || 'genericTool';
const progressLabel = getSceneProgressLabel(script);
if (progressLabel) {
monitorProgressDebug('enqueueToolEvent:set-status', { script, progressLabel });
this.setStatus(progressLabel);
}
const id = payload.executionId || payload.id || randomId('tool');
this.ensurePendingResult(id);
this.queue.push({ id, script, payload: { ...payload, executionId: id } });
this.awaitingTools[id] = payload.name;
// 提前为创建类工具标记“待创建”路径,避免动画开始前图标闪现
if (this.driver && typeof this.driver.preparePendingCreation === 'function') {
if (payload.name === 'create_folder' || payload.name === 'create_file') {
const pendingPath =
payload?.arguments?.path ||
payload?.arguments?.target_path ||
payload?.arguments?.targetPath ||
payload?.argumentSnapshot?.path ||
payload?.argumentSnapshot?.target_path;
this.driver.preparePendingCreation(pendingPath);
}
}
monitorLifecycleLog('enqueue', {
id,
tool: payload.name,
script,
queueSize: this.queue.length,
awaiting: Object.keys(this.awaitingTools).length,
status: this.statusLabel
});
if (progressLabel) {
this.setProgressIndicator({ id, label: progressLabel, scene: script });
} else {
this.clearProgressIndicator();
}
const doPreview = () => {
if (this.driver) {
monitorProgressDebug('enqueueToolEvent:preview-now', { tool: payload.name, script, id });
this.driver.previewSceneProgress(script);
} else {
monitorProgressDebug('enqueueToolEvent:pending-preview', {
tool: payload.name,
script,
id
});
this.pendingProgressScene = script;
}
if (this.bubbleActive) {
this.hideBubble(`tool-enqueue:${payload?.name || 'unknown'}`);
}
};
if (this.bubbleActive && recentSpeechGap >= 0 && recentSpeechGap < MIN_SPEECH_VISIBLE_MS) {
const delay = MIN_SPEECH_VISIBLE_MS - recentSpeechGap;
monitorLifecycleLog('enqueue:delay-preview-for-speech', {
delay,
recentSpeechGap,
tool: payload.name
});
setTimeout(doPreview, delay);
} else {
doPreview();
}
this.processQueue();
},
resolveToolResult(payload: {
id?: string;
execution_id?: string;
executionId?: string;
status?: string;
message?: string;
result?: any;
}) {
const key = payload.execution_id || payload.executionId || payload.id;
if (key && this.awaitingTools[key]) {
delete this.awaitingTools[key];
}
monitorLifecycleLog('tool-result', {
id: key,
status: payload.status,
message: payload.message,
hasResult: payload.result !== undefined
});
if (key) {
this.clearProgressIndicator(key);
}
if (payload.status && ['failed', 'error'].includes(payload.status)) {
const message = payload.message || '工具执行失败';
this.driver?.showSpeechBubble(message, { variant: 'error', icon: '✖', duration: 2800 });
this.setStatus('异常');
setTimeout(() => this.hideBubble(), 2600);
}
if (key) {
this.fulfillPendingResult(key, payload);
}
},
ensurePendingResult(id: string) {
if (!id) {
return;
}
const hasCached = Object.prototype.hasOwnProperty.call(this.completedResults, id);
if (this.pendingResults[id]) {
if (hasCached) {
const cached = this.completedResults[id];
delete this.completedResults[id];
this.pendingResults[id].resolve(cached);
}
return;
}
let resolver: (value: any) => void = () => {};
const promise = new Promise<any>((resolve) => {
resolver = resolve;
});
const entry: PendingResultEntry = {
promise,
resolve: (value) => {
if (!entry.settled) {
entry.settled = true;
if (entry.timeout) {
clearTimeout(entry.timeout);
entry.timeout = null;
}
}
resolver(value);
},
timeout: null,
settled: false
};
entry.timeout = setTimeout(() => {
if (!entry.settled) {
entry.resolve(null);
this.cleanupPendingResult(id, entry);
}
}, 12000);
this.pendingResults[id] = entry;
if (hasCached) {
const cached = this.completedResults[id];
delete this.completedResults[id];
entry.resolve(cached);
}
},
waitForResult(id?: string | number | null) {
if (!id) {
return Promise.resolve(null);
}
const key = String(id);
if (Object.prototype.hasOwnProperty.call(this.completedResults, key)) {
const cached = this.completedResults[key];
delete this.completedResults[key];
return Promise.resolve(cached);
}
this.ensurePendingResult(key);
const entry = this.pendingResults[key];
if (!entry) {
return Promise.resolve(null);
}
return entry.promise.finally(() => {
this.cleanupPendingResult(key, entry);
});
},
fulfillPendingResult(id: string, payload: any) {
if (!id) {
return;
}
const entry = this.pendingResults[id];
if (entry) {
entry.resolve(payload);
return;
}
this.completedResults[id] = payload;
const cacheKeys = Object.keys(this.completedResults);
if (cacheKeys.length > COMPLETED_RESULT_CACHE_LIMIT) {
const staleKey = cacheKeys[0];
delete this.completedResults[staleKey];
}
},
cleanupPendingResult(id: string, entry?: PendingResultEntry) {
if (!id) {
return;
}
const target = entry || this.pendingResults[id];
if (!target) {
return;
}
if (target.timeout) {
clearTimeout(target.timeout);
target.timeout = null;
}
if (this.pendingResults[id] === target) {
delete this.pendingResults[id];
}
},
async processQueue() {
if (this.playing || !this.queue.length) {
return;
}
if (!this.driver) {
return;
}
this.playing = true;
this.driver.setManualInteractionEnabled(false);
monitorLifecycleLog('queue-start', { size: this.queue.length });
while (this.queue.length && this.driver) {
const event = this.queue.shift();
if (!event) {
continue;
}
monitorDebug('processQueue start', {
script: event.script,
bubbleActive: this.bubbleActive,
speechBufferLength: this.speechBuffer.length
});
if (this.bubbleActive) {
this.hideBubble(`queue-start:${event.script}`);
}
this.speechBuffer = '';
await this.runScript(event);
}
this.playing = false;
if (this.driver) {
this.driver.setManualInteractionEnabled(true);
}
if (this.speechBuffer && this.driver) {
this.driver.showSpeechBubble(this.speechBuffer, { variant: 'info', duration: 0 });
this.bubbleActive = true;
}
this.setStatus('待机');
monitorLifecycleLog('queue-end');
},
async runScript(event: MonitorEvent) {
if (!this.driver) {
this.queue.unshift(event);
return;
}
const waitKey =
event.payload?.executionId ||
event.payload?.execution_id ||
event.id ||
event.payload?.id ||
null;
monitorLifecycleLog('runScript:start', {
script: event.script,
waitKey,
payloadTool: event.payload?.name,
payloadId: event.payload?.id
});
let playbackResult: any = undefined;
let playbackSettled = false;
const waitForCompletion = async () => {
if (!waitKey) {
playbackSettled = true;
if (playbackResult === undefined) {
playbackResult = null;
}
return playbackResult;
}
if (playbackSettled) {
return playbackResult;
}
try {
playbackResult = await this.waitForResult(waitKey);
} catch (error) {
console.warn('monitor waitForResult error', error);
playbackResult = null;
} finally {
playbackSettled = true;
}
return playbackResult;
};
const transformStatus = (raw?: string) => {
const label = typeof raw === 'string' && raw.trim().length ? raw.trim() : '进行中';
if (label.startsWith('正在')) {
return `回放${label.slice(2)}`;
}
return label;
};
const runtime: MonitorSceneRuntime = {
waitForResult: () => waitForCompletion(),
setStatus: (label: string) => this.setStatus(transformStatus(label)),
statusPhase: 'playback'
};
this.setStatus('工具操作回放');
monitorLifecycleLog('runScript:playback', {
script: event.script,
waitKey,
status: this.statusLabel
});
try {
await this.driver.playScene(event.script, event.payload || {}, runtime);
} catch (error) {
console.warn('Monitor scene error', error);
} finally {
monitorLifecycleLog('runScript:end', {
script: event.script,
waitKey
});
}
}
}
});