本次升级基于完整代码审查,修复 Critical/High/Medium/Low 四级共 96 项问题, 并通过返工审计修复 10 项遗留问题,tsc 双端类型检查零错误。 Critical (10/10 完成): - C-4: command.ts 接入 shell-quote 进行 token-level 注入检测,替代原有正则匹配 可防御 r"m" -rf /、$'rm'、$(echo rm) 等字符串拼接绕过 High (11/11 完成): - 竞态保护、Promise.allSettled、AbortController 资源泄漏、IPC 参数校验等 Medium (55/55 完成): - 事务保护、敏感数据脱敏、枚举校验、MUI v9 Stack prop 迁移、 React 组件 cancelled 标志、类型收窄等 Low (20/20 完成): - 辅助方法提取(flushToolCallBuffer/scoreAndPushMemory/tryAddColumn 等) - nanoid 统一替代 Date.now()+Math.random() - confirm() 替换为 MUI Dialog、useMemo 缓存、魔法数字命名化等 返工审计修复 (10/10 完成): - L-11: LogsSettings 残留的原生 confirm()/alert() 全部替换为 MUI Dialog/Alert - M-53: MemoryViewer handleSearch 独立 ref,修复 searching 状态卡死 - M-42: 脱敏短值(length <= 4)泄露修复 - M-47: tasks:update 补全 title/description 类型校验 - L-9: ollama.adapter 非流式路径 nanoid 统一 - M-45: audit:query limit 策略与 memory:listAll 一致化 - SettingsModal handleConfirmRemove 补全 try/catch + loadServers cleanup - L-15: CommandPalette useMemo 补全 sessions 响应式依赖 - useAgentStream 事件类型补全 seq/timestamp 字段 新增依赖: shell-quote + @types/shell-quote 版本号: 0.3.0 -> 0.3.1
308 lines
11 KiB
TypeScript
308 lines
11 KiB
TypeScript
/**
|
||
* Task Orchestrator — 任务编排器
|
||
*
|
||
* 支持父子委派模式:主 Agent 委派子任务给 SubAgent。
|
||
* 每个 SubAgent 运行在**独立的 AgentLoopEngine 实例**中,避免状态污染。
|
||
*
|
||
* 安全保障:
|
||
* 1. 独立引擎实例 — SubAgent 不共享主 Agent 的引擎状态
|
||
* 2. 递归深度限制 — 默认最大 3 层,防止无限递归
|
||
* 3. 工具白名单隔离 — SubAgent 默认不继承 delegate_task(防止递归)
|
||
* 4. 真正的 abort — 通过引擎引用调用 engine.abort()
|
||
* 5. 事件隔离 — SubAgent 的流式事件不直接转发到前端,仅通过 orchestrator 事件通知
|
||
*
|
||
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第五章
|
||
*/
|
||
|
||
import { EventEmitter } from 'events';
|
||
import { nanoid } from 'nanoid';
|
||
import { AgentLoopEngine } from '../agent-loop/engine';
|
||
import type { AgentLoopConfig } from '../agent-loop/types';
|
||
import type { MetonaMessage, MetonaSystemPrompt, MetonaToolDef } from '../types';
|
||
import type { ToolRegistry } from '../tools/registry';
|
||
import type { PreToolHook } from '../hooks/pre-tool';
|
||
import type { PostToolHook } from '../hooks/post-tool';
|
||
import log from 'electron-log';
|
||
|
||
export interface SubAgentResult {
|
||
taskId: string;
|
||
result: string;
|
||
success: boolean;
|
||
durationMs: number;
|
||
iterations: number;
|
||
}
|
||
|
||
interface SubAgentHandle {
|
||
taskId: string;
|
||
description: string;
|
||
status: 'pending' | 'running' | 'completed' | 'error';
|
||
depth: number;
|
||
engine?: AgentLoopEngine;
|
||
result?: SubAgentResult;
|
||
abort: () => void;
|
||
getStatus: () => { taskId: string; status: string; description: string; depth: number };
|
||
}
|
||
|
||
/** 默认递归深度限制 */
|
||
const MAX_DELEGATION_DEPTH = 3;
|
||
|
||
export class TaskOrchestrator extends EventEmitter {
|
||
private activeSubAgents = new Map<string, SubAgentHandle>();
|
||
/** 追踪每个 session 的当前委派深度 */
|
||
private sessionDepth = new Map<string, number>();
|
||
|
||
constructor(
|
||
private mainEngine: AgentLoopEngine,
|
||
private toolRegistry?: ToolRegistry,
|
||
private preToolHooks: PreToolHook[] = [],
|
||
private postToolHooks: PostToolHook[] = [],
|
||
private defaultConfig?: Partial<AgentLoopConfig>,
|
||
) {
|
||
super();
|
||
}
|
||
|
||
/**
|
||
* L-18 修复: 热更新 SubAgent 的默认配置
|
||
*
|
||
* 主 Agent 的配置变更(thinkingEnabled/thinkingEffort/contextLength 等)通过
|
||
* engine.updateConfig() 即时生效;但 SubAgent 在 delegate() 时从 defaultConfig
|
||
* 复制配置,若 defaultConfig 不同步,新创建的 SubAgent 仍使用旧配置。
|
||
*
|
||
* 此方法供 handlers.ts 在 config:set 时同步调用,确保后续 SubAgent 使用最新配置。
|
||
*/
|
||
updateDefaultConfig(partial: Partial<AgentLoopConfig>): void {
|
||
this.defaultConfig = { ...this.defaultConfig, ...partial };
|
||
}
|
||
|
||
/**
|
||
* 委派子任务
|
||
*
|
||
* 创建一个独立的 AgentLoopEngine 实例执行子任务。
|
||
* SubAgent 不共享主 Agent 的引擎状态,安全隔离。
|
||
*/
|
||
async delegate(params: {
|
||
taskId?: string;
|
||
description: string;
|
||
parentSessionId: string;
|
||
maxIterations?: number;
|
||
tools?: string[];
|
||
}): Promise<SubAgentResult> {
|
||
const taskId = params.taskId ?? `sub_${nanoid(8)}`;
|
||
const startMs = Date.now();
|
||
|
||
// ===== 递归深度检查 =====
|
||
const currentDepth = this.sessionDepth.get(params.parentSessionId) ?? 0;
|
||
if (currentDepth >= MAX_DELEGATION_DEPTH) {
|
||
log.warn(`[Orchestrator] Delegation depth limit reached (${currentDepth}) for session ${params.parentSessionId}`);
|
||
return {
|
||
taskId,
|
||
result: `SubAgent delegation depth limit reached (${MAX_DELEGATION_DEPTH}). Cannot delegate further.`,
|
||
success: false,
|
||
durationMs: 0,
|
||
iterations: 0,
|
||
};
|
||
}
|
||
const depth = currentDepth + 1;
|
||
this.sessionDepth.set(params.parentSessionId, depth);
|
||
|
||
this.emit('taskDelegated', { taskId, description: params.description, parentSessionId: params.parentSessionId, depth });
|
||
|
||
// ===== 创建独立的引擎实例 =====
|
||
const subEngine = new AgentLoopEngine(
|
||
{
|
||
maxIterations: params.maxIterations ?? 10,
|
||
totalTimeoutMs: 300_000, // 子任务总超时 5 分钟
|
||
thinkingEnabled: this.defaultConfig?.thinkingEnabled ?? true,
|
||
thinkingEffort: this.defaultConfig?.thinkingEffort ?? 'medium',
|
||
contextLength: this.defaultConfig?.contextLength,
|
||
contextWindow: this.defaultConfig?.contextWindow ?? 128_000,
|
||
},
|
||
this.mainEngine.getAdapter(),
|
||
this.toolRegistry,
|
||
this.preToolHooks,
|
||
this.postToolHooks,
|
||
);
|
||
subEngine.setWorkspacePath(this.mainEngine.getWorkspacePath());
|
||
|
||
// ===== 工具白名单设置 =====
|
||
const allowedTools = this.resolveTools(params.tools);
|
||
subEngine.setTools(allowedTools);
|
||
|
||
const handle: SubAgentHandle = {
|
||
taskId,
|
||
description: params.description,
|
||
status: 'running',
|
||
depth,
|
||
engine: subEngine,
|
||
abort: () => {
|
||
subEngine.abort();
|
||
handle.status = 'error';
|
||
this.activeSubAgents.delete(taskId);
|
||
},
|
||
getStatus: () => ({ taskId, status: handle.status, description: handle.description, depth: handle.depth }),
|
||
};
|
||
|
||
this.activeSubAgents.set(taskId, handle);
|
||
this.emit('taskStarted', { taskId, depth });
|
||
|
||
try {
|
||
// 构建用户消息
|
||
const userMessage: MetonaMessage = {
|
||
role: 'user',
|
||
content: params.description,
|
||
timestamp: Date.now(),
|
||
};
|
||
|
||
// 构建 System Prompt(子 Agent 专用)
|
||
const systemPrompt = this.buildSubAgentPrompt(params.description, depth);
|
||
|
||
// 运行 Agent Loop(同步等待完成)
|
||
const output = await subEngine.runStream(
|
||
userMessage,
|
||
taskId,
|
||
[], // 子 Agent 无历史
|
||
systemPrompt,
|
||
);
|
||
|
||
const durationMs = Date.now() - startMs;
|
||
const success = output.terminationReason === 'completed';
|
||
const result: SubAgentResult = {
|
||
taskId,
|
||
result: output.finalAnswer,
|
||
success,
|
||
durationMs,
|
||
iterations: output.iterations.length,
|
||
};
|
||
|
||
handle.status = success ? 'completed' : 'error';
|
||
handle.result = result;
|
||
this.activeSubAgents.delete(taskId);
|
||
// v0.3.0 修复: 深度恢复为 0 时删除条目,避免 sessionDepth Map 随会话累积
|
||
if (currentDepth === 0) {
|
||
this.sessionDepth.delete(params.parentSessionId);
|
||
} else {
|
||
this.sessionDepth.set(params.parentSessionId, currentDepth);
|
||
}
|
||
this.emit('taskCompleted', result);
|
||
|
||
log.info(`[Orchestrator] SubAgent "${taskId}" (depth=${depth}) ${success ? 'completed' : 'failed'} in ${durationMs}ms, ${output.iterations.length} iterations`);
|
||
|
||
return result;
|
||
} catch (error) {
|
||
const durationMs = Date.now() - startMs;
|
||
const errMsg = (error as Error).message;
|
||
const result: SubAgentResult = {
|
||
taskId,
|
||
result: errMsg,
|
||
success: false,
|
||
durationMs,
|
||
iterations: 0,
|
||
};
|
||
|
||
handle.status = 'error';
|
||
handle.result = result;
|
||
this.activeSubAgents.delete(taskId);
|
||
// v0.3.0 修复: 深度恢复为 0 时删除条目,避免 sessionDepth Map 随会话累积
|
||
if (currentDepth === 0) {
|
||
this.sessionDepth.delete(params.parentSessionId);
|
||
} else {
|
||
this.sessionDepth.set(params.parentSessionId, currentDepth);
|
||
}
|
||
this.emit('taskError', { taskId, error: errMsg });
|
||
|
||
log.error(`[Orchestrator] SubAgent "${taskId}" (depth=${depth}) error: ${errMsg}`);
|
||
|
||
return result;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析工具白名单
|
||
*
|
||
* - 如果指定了 tools,使用白名单(自动排除 delegate_task 防止递归)
|
||
* - 如果未指定,使用所有已启用工具(同样排除 delegate_task)
|
||
*/
|
||
private resolveTools(toolNames?: string[]): MetonaToolDef[] {
|
||
if (!this.toolRegistry) return [];
|
||
|
||
// 始终排除 delegate_task 防止递归(除非深度为 1 且显式要求)
|
||
const EXCLUDE_TOOLS = new Set(['delegate_task']);
|
||
|
||
if (toolNames && toolNames.length > 0) {
|
||
// 使用白名单模式
|
||
const resolved: MetonaToolDef[] = [];
|
||
const notFound: string[] = [];
|
||
|
||
for (const name of toolNames) {
|
||
if (EXCLUDE_TOOLS.has(name)) continue; // 静默排除
|
||
const tool = this.toolRegistry.get(name);
|
||
if (tool) {
|
||
resolved.push(tool.definition);
|
||
} else {
|
||
notFound.push(name);
|
||
}
|
||
}
|
||
|
||
if (notFound.length > 0) {
|
||
log.warn(`[Orchestrator] Tools not found: ${notFound.join(', ')}`);
|
||
}
|
||
|
||
return resolved;
|
||
}
|
||
|
||
// 未指定白名单 — 使用所有已启用工具(排除 delegate_task)
|
||
return this.toolRegistry.listTools().filter((t) => !EXCLUDE_TOOLS.has(t.name));
|
||
}
|
||
|
||
/**
|
||
* 构建 SubAgent 的 System Prompt
|
||
*/
|
||
private buildSubAgentPrompt(description: string, depth: number): MetonaSystemPrompt {
|
||
return {
|
||
roleDefinition: `You are a SubAgent (delegation depth: ${depth}) executing a specific sub-task delegated by the parent Agent.\nYour goal is to complete the assigned task efficiently and return a clear, concise result.\nFocus only on the task at hand. Do not delegate further.`,
|
||
outputConstraints: `Complete the task and provide a clear summary of your findings or actions.\nRespond in the same language as the task description.\nKeep your response focused and relevant — the parent Agent will use your result to continue its work.`,
|
||
safetyGuidelines: `Do not access files outside the workspace.\nDo not execute dangerous commands.\nIf the task cannot be completed, explain why clearly.`,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 中断指定子任务
|
||
*/
|
||
abortTask(taskId: string): boolean {
|
||
const handle = this.activeSubAgents.get(taskId);
|
||
if (handle && handle.status === 'running') {
|
||
handle.abort();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 完成子任务(外部触发,保留接口兼容)
|
||
*/
|
||
completeTask(taskId: string, result: string, success: boolean): void {
|
||
const handle = this.activeSubAgents.get(taskId);
|
||
if (handle && handle.status === 'running') {
|
||
handle.status = success ? 'completed' : 'error';
|
||
handle.result = { taskId, result, success, durationMs: 0, iterations: 0 };
|
||
this.activeSubAgents.delete(taskId);
|
||
this.emit('taskCompleted', handle.result);
|
||
}
|
||
}
|
||
|
||
getActiveAgentsStatus(): Array<{ taskId: string; status: string; description: string; depth: number }> {
|
||
return Array.from(this.activeSubAgents.values()).map((a) => a.getStatus());
|
||
}
|
||
|
||
/**
|
||
* 中断所有子任务
|
||
*/
|
||
abortAll(): void {
|
||
for (const agent of this.activeSubAgents.values()) {
|
||
agent.abort();
|
||
}
|
||
this.activeSubAgents.clear();
|
||
this.sessionDepth.clear();
|
||
}
|
||
}
|