feat: v4.3 生态扩展 — 诊断系统 + Heartbeat + 子代理 + MCP
新增功能: 1. 🩺 一键诊断系统 - 设置面板添加「一键诊断」按钮 - 检查项:Ollama 连接、模型可用性、模型能力(Tools/Vision/Think)、 SQLite 数据库、技能系统、应用版本信息 - 实时渲染诊断结果,✅/⚠️/❌ 状态标识 2. 💓 Heartbeat 后台检查 - 设置面板添加开关 + 间隔选择(10分/30分/1小时/2小时) - 定时检查 Ollama 连接,异常时系统通知 - 配置持久化到 settings,启动时自动恢复 - startHeartbeat/stopHeartbeat/restoreHeartbeat 三件套 3. 🤖 子代理委派(spawn_task) - 新增 spawn_task 工具(26 个工具) - 子 Agent 独立运行(独立消息上下文,5 轮上限,2 分钟超时) - 不支持工具调用,专注于文本推理任务 - 主 Agent 可传入附加上下文 - 结果返回主 Agent Loop 4. 🔌 MCP Client 框架 - mcp-client.ts: MCP 服务器配置管理(增删改查、启用禁用) - 配置存储在 settings.mcp_servers - 工具注册框架就绪,待主进程 MCP bridge 实现协议通信 新增文件:mcp-client.ts, sub-agent.ts 修改文件:settings-modal.ts, index.html, main.ts, tool-registry.ts
This commit is contained in:
@@ -117,6 +117,40 @@ export function initSettingsModal(): void {
|
||||
});
|
||||
});
|
||||
|
||||
// ── v4.3 诊断系统 ──
|
||||
document.querySelector('#btnDoctor')!.addEventListener('click', runDoctor);
|
||||
|
||||
// ── v4.3 Heartbeat 后台检查 ──
|
||||
const toggleHB = document.querySelector<HTMLInputElement>('#toggleHeartbeat')!;
|
||||
const selectHBInterval = document.querySelector<HTMLSelectElement>('#selectHeartbeatInterval')!;
|
||||
|
||||
toggleHB.addEventListener('change', async () => {
|
||||
const enabled = toggleHB.checked;
|
||||
state.set('heartbeatEnabled', enabled);
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) await db.saveSetting('heartbeatEnabled', enabled);
|
||||
if (enabled) {
|
||||
startHeartbeat(parseInt(selectHBInterval.value));
|
||||
showToast('后台检查已启用', 'success');
|
||||
} else {
|
||||
stopHeartbeat();
|
||||
showToast('后台检查已关闭', 'info');
|
||||
}
|
||||
logSetting('后台检查', enabled ? '已启用' : '已关闭');
|
||||
});
|
||||
|
||||
selectHBInterval.addEventListener('change', async () => {
|
||||
const ms = parseInt(selectHBInterval.value);
|
||||
state.set('heartbeatInterval', ms);
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) await db.saveSetting('heartbeatInterval', ms);
|
||||
if (state.get<boolean>('heartbeatEnabled', false)) {
|
||||
stopHeartbeat();
|
||||
startHeartbeat(ms);
|
||||
}
|
||||
logSetting('检查间隔', `${ms / 60000} 分钟`);
|
||||
});
|
||||
|
||||
document.querySelector('#btnReleaseVRAM')!.addEventListener('click', async () => {
|
||||
const api = state.get<OllamaAPI>(KEYS.API);
|
||||
const model = (document.querySelector('#modelSelect') as HTMLSelectElement).value;
|
||||
@@ -402,3 +436,149 @@ async function importSessions(filePath: string): Promise<void> {
|
||||
logError('导入失败', (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// v4.3 诊断系统
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
async function runDoctor(): Promise<void> {
|
||||
const resultsEl = document.querySelector('#doctorResults')!;
|
||||
resultsEl.style.display = '';
|
||||
resultsEl.innerHTML = '<div>🩺 正在诊断...</div>';
|
||||
|
||||
const checks: Array<{ name: string; status: 'ok' | 'warn' | 'error'; detail: string }> = [];
|
||||
|
||||
// 1. Ollama 连接
|
||||
const api = state.get<OllamaAPI>(KEYS.API);
|
||||
try {
|
||||
const version = await api!.getVersion();
|
||||
checks.push({ name: 'Ollama 连接', status: 'ok', detail: `v${version.version}` });
|
||||
} catch (err) {
|
||||
checks.push({ name: 'Ollama 连接', status: 'error', detail: `无法连接: ${(err as Error).message}` });
|
||||
}
|
||||
|
||||
// 2. 模型可用性
|
||||
try {
|
||||
const models = await api!.listModels();
|
||||
const count = models.models?.length || 0;
|
||||
checks.push({ name: '已安装模型', status: count > 0 ? 'ok' : 'warn', detail: `${count} 个模型` });
|
||||
} catch {
|
||||
checks.push({ name: '已安装模型', status: 'error', detail: '无法获取模型列表' });
|
||||
}
|
||||
|
||||
// 3. 当前模型能力
|
||||
const model = (document.querySelector('#modelSelect') as HTMLSelectElement)?.value;
|
||||
if (model) {
|
||||
try {
|
||||
const detail = await api!.showModel(model);
|
||||
const caps = detail.capabilities || [];
|
||||
const hasTools = caps.includes('tools');
|
||||
const hasVision = caps.includes('vision');
|
||||
const hasThink = caps.includes('thinking');
|
||||
checks.push({
|
||||
name: `模型能力 (${model})`,
|
||||
status: hasTools ? 'ok' : 'warn',
|
||||
detail: `Tool Calling: ${hasTools ? '✅' : '❌'} | Vision: ${hasVision ? '✅' : '❌'} | Think: ${hasThink ? '✅' : '❌'}`
|
||||
});
|
||||
} catch {
|
||||
checks.push({ name: '模型能力', status: 'warn', detail: '无法检测模型能力' });
|
||||
}
|
||||
} else {
|
||||
checks.push({ name: '当前模型', status: 'warn', detail: '未选择模型' });
|
||||
}
|
||||
|
||||
// 4. SQLite 数据库
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) {
|
||||
try {
|
||||
const sessions = await db.getAllSessions();
|
||||
checks.push({ name: 'SQLite 数据库', status: 'ok', detail: `${sessions.length} 个会话` });
|
||||
} catch (err) {
|
||||
checks.push({ name: 'SQLite 数据库', status: 'error', detail: `错误: ${(err as Error).message}` });
|
||||
}
|
||||
} else {
|
||||
checks.push({ name: 'SQLite 数据库', status: 'error', detail: '未初始化' });
|
||||
}
|
||||
|
||||
// 5. 技能系统
|
||||
try {
|
||||
if (db && (db as any).getAllSkills) {
|
||||
const skills = await (db as any).getAllSkills();
|
||||
checks.push({ name: '技能系统', status: 'ok', detail: `${skills.length} 个技能` });
|
||||
} else {
|
||||
checks.push({ name: '技能系统', status: 'ok', detail: '已就绪' });
|
||||
}
|
||||
} catch {
|
||||
checks.push({ name: '技能系统', status: 'warn', detail: '无法获取技能列表' });
|
||||
}
|
||||
|
||||
// 6. 版本信息
|
||||
const bridge = (window as any).metonaDesktop;
|
||||
if (bridge?.info) {
|
||||
try {
|
||||
const info = await bridge.info();
|
||||
checks.push({ name: '应用信息', status: 'ok', detail: `v${info.version} | Electron ${info.electronVersion} | ${info.platform} ${info.arch}` });
|
||||
} catch {
|
||||
checks.push({ name: '应用信息', status: 'warn', detail: '无法获取' });
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染结果
|
||||
const statusIcon = (s: string) => s === 'ok' ? '✅' : s === 'warn' ? '⚠️' : '❌';
|
||||
const allOk = checks.every(c => c.status === 'ok');
|
||||
|
||||
resultsEl.innerHTML = `
|
||||
<div style="margin-bottom:6px;font-weight:600;">${allOk ? '✅ 所有检查通过' : '⚠️ 发现问题'}</div>
|
||||
${checks.map(c => `<div style="margin:4px 0;">${statusIcon(c.status)} <strong>${c.name}</strong>: ${c.detail}</div>`).join('')}
|
||||
`;
|
||||
|
||||
logInfo('系统诊断完成', `${checks.filter(c => c.status === 'ok').length}/${checks.length} 项通过`);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// v4.3 Heartbeat 后台检查
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
export function startHeartbeat(intervalMs: number): void {
|
||||
stopHeartbeat();
|
||||
heartbeatTimer = setInterval(async () => {
|
||||
const api = state.get<OllamaAPI>(KEYS.API);
|
||||
if (!api) return;
|
||||
|
||||
try {
|
||||
await api.getVersion();
|
||||
// 连接正常,静默
|
||||
} catch {
|
||||
// 连接异常,通知用户
|
||||
const bridge = (window as any).metonaDesktop;
|
||||
if (bridge?.notify) {
|
||||
bridge.notify('Metona Ollama', '⚠️ Ollama 服务连接异常,请检查是否正在运行');
|
||||
}
|
||||
logWarn('Heartbeat: Ollama 连接异常');
|
||||
}
|
||||
}, intervalMs);
|
||||
logInfo(`Heartbeat 已启动`, `间隔: ${intervalMs / 60000} 分钟`);
|
||||
}
|
||||
|
||||
export function stopHeartbeat(): void {
|
||||
if (heartbeatTimer) {
|
||||
clearInterval(heartbeatTimer);
|
||||
heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 应用启动时恢复 heartbeat */
|
||||
export function restoreHeartbeat(): void {
|
||||
const enabled = state.get<boolean>('heartbeatEnabled', false);
|
||||
const interval = state.get<number>('heartbeatInterval', 3600000);
|
||||
if (enabled) {
|
||||
startHeartbeat(interval);
|
||||
// 同步 UI 状态
|
||||
const toggle = document.querySelector<HTMLInputElement>('#toggleHeartbeat');
|
||||
if (toggle) toggle.checked = true;
|
||||
const select = document.querySelector<HTMLSelectElement>('#selectHeartbeatInterval');
|
||||
if (select) select.value = String(interval);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,6 +322,31 @@
|
||||
</div>
|
||||
<p class="text-muted" style="margin-top:6px;font-size:11px;">导出格式为 JSON,可备份后在其他设备导入恢复。</p>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">💓 后台检查(Heartbeat)</label>
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<label class="toggle-label" style="margin:0;">
|
||||
<input type="checkbox" id="toggleHeartbeat">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<span style="font-size:13px;">启用后台定时检查</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;margin-top:8px;">
|
||||
<label style="font-size:12px;color:var(--text-muted);">检查间隔</label>
|
||||
<select id="selectHeartbeatInterval" class="setting-input" style="width:auto;margin-bottom:0;padding:4px 8px;">
|
||||
<option value="600000">10 分钟</option>
|
||||
<option value="1800000">30 分钟</option>
|
||||
<option value="3600000" selected>1 小时</option>
|
||||
<option value="7200000">2 小时</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="text-muted" style="margin-top:4px;font-size:11px;">定期检查 Ollama 连接状态和模型可用性,异常时通过系统通知提醒。</p>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">🩺 系统诊断</label>
|
||||
<button class="btn btn-outline" id="btnDoctor" style="margin-bottom:8px;">🩺 一键诊断</button>
|
||||
<div id="doctorResults" style="display:none;margin-top:8px;padding:10px;background:var(--bg-layer);border-radius:var(--radius-control);font-size:12px;max-height:200px;overflow-y:auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ import { initHistoryModal, closeHistoryModal } from './components/history-modal.
|
||||
import { initMemoryModal } from './components/memory-modal.js';
|
||||
import { initToolsModal, closeToolsModal } from './components/tools-modal.js';
|
||||
import { initMemoryManager } from './services/memory-manager.js';
|
||||
import { restoreHeartbeat } from './components/settings-modal.js';
|
||||
import { setToolEnabled, setRunCommandMode } from './services/tool-registry.js';
|
||||
import { initToolConfirmModal } from './components/tool-confirm-modal.js';
|
||||
import { initWorkspacePanel } from './components/workspace-panel.js';
|
||||
@@ -378,10 +379,19 @@ async function init(): Promise<void> {
|
||||
}
|
||||
});
|
||||
|
||||
// ── v4.3 Heartbeat 恢复 ──
|
||||
const heartbeatEnabled = await db.getSetting('heartbeatEnabled', false);
|
||||
const heartbeatInterval = await db.getSetting('heartbeatInterval', 3600000);
|
||||
state.set('heartbeatEnabled', heartbeatEnabled);
|
||||
state.set('heartbeatInterval', heartbeatInterval);
|
||||
|
||||
(document.querySelector('#chatInput') as HTMLElement)?.focus();
|
||||
updateTotalTokens();
|
||||
|
||||
logSuccess('应用初始化完成');
|
||||
|
||||
// Heartbeat 在 UI 就绪后启动
|
||||
restoreHeartbeat();
|
||||
} catch (err) {
|
||||
logError('初始化失败', (err as Error).message);
|
||||
if (!api!) {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* MCP Client - Model Context Protocol 客户端 (v4.3)
|
||||
* 轻量级实现:管理 MCP Server 配置,提供工具注册框架
|
||||
*
|
||||
* MCP Server 配置存储在 settings.mcp_servers 中,格式:
|
||||
* [
|
||||
* { "name": "filesystem", "command": "npx", "args": ["-y", "@anthropic/mcp-filesystem"], "enabled": true },
|
||||
* { "name": "sqlite", "command": "npx", "args": ["-y", "@anthropic/mcp-sqlite", "./data.db"], "enabled": false }
|
||||
* ]
|
||||
*/
|
||||
|
||||
import { state, KEYS } from '../state/state.js';
|
||||
import { logInfo, logWarn } from './log-service.js';
|
||||
import type { ToolDefinition } from '../types.js';
|
||||
|
||||
export interface MCPServerConfig {
|
||||
name: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
enabled: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** 获取所有 MCP 服务器配置 */
|
||||
export async function getMCPServers(): Promise<MCPServerConfig[]> {
|
||||
const db = state.get<any>(KEYS.DB);
|
||||
if (!db) return [];
|
||||
try {
|
||||
return await db.getSetting('mcp_servers', []);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存 MCP 服务器配置 */
|
||||
export async function saveMCPServers(servers: MCPServerConfig[]): Promise<void> {
|
||||
const db = state.get<any>(KEYS.DB);
|
||||
if (db) await db.saveSetting('mcp_servers', servers);
|
||||
}
|
||||
|
||||
/** 添加 MCP 服务器 */
|
||||
export async function addMCPServer(config: MCPServerConfig): Promise<void> {
|
||||
const servers = await getMCPServers();
|
||||
const existing = servers.findIndex(s => s.name === config.name);
|
||||
if (existing >= 0) {
|
||||
servers[existing] = config;
|
||||
} else {
|
||||
servers.push(config);
|
||||
}
|
||||
await saveMCPServers(servers);
|
||||
logInfo(`MCP Server 已${existing >= 0 ? '更新' : '添加'}: ${config.name}`);
|
||||
}
|
||||
|
||||
/** 删除 MCP 服务器 */
|
||||
export async function removeMCPServer(name: string): Promise<void> {
|
||||
const servers = await getMCPServers();
|
||||
await saveMCPServers(servers.filter(s => s.name !== name));
|
||||
logInfo(`MCP Server 已删除: ${name}`);
|
||||
}
|
||||
|
||||
/** 切换 MCP 服务器启用状态 */
|
||||
export async function toggleMCPServer(name: string): Promise<boolean> {
|
||||
const servers = await getMCPServers();
|
||||
const server = servers.find(s => s.name === name);
|
||||
if (!server) return false;
|
||||
server.enabled = !server.enabled;
|
||||
await saveMCPServers(servers);
|
||||
logInfo(`MCP Server ${server.enabled ? '已启用' : '已禁用'}: ${name}`);
|
||||
return server.enabled;
|
||||
}
|
||||
|
||||
/** 获取已启用的 MCP 服务器 */
|
||||
export async function getEnabledMCPServers(): Promise<MCPServerConfig[]> {
|
||||
const servers = await getMCPServers();
|
||||
return servers.filter(s => s.enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MCP 工具定义(供 Agent 使用)
|
||||
* 目前返回占位工具定义,实际 MCP 协议通信需通过主进程实现
|
||||
*/
|
||||
export async function getMCPToolDefinitions(): Promise<ToolDefinition[]> {
|
||||
const enabled = await getEnabledMCPServers();
|
||||
// MCP 工具通过主进程的 MCP bridge 动态注册
|
||||
// 这里返回空数组,工具定义由 MCP bridge 在 IPC 层注入
|
||||
if (enabled.length > 0) {
|
||||
logInfo(`MCP: ${enabled.length} 个服务器已启用`, enabled.map(s => s.name).join(', '));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* SubAgent - 子代理委派系统 (v4.3)
|
||||
* 允许主 Agent 将独立任务委派给子 Agent 执行
|
||||
* 子 Agent 独立运行,完成后将结果返回给主 Agent
|
||||
*/
|
||||
|
||||
import { state, KEYS } from '../state/state.js';
|
||||
import { OllamaAPI } from '../api/ollama.js';
|
||||
import { logInfo, logWarn, logError, logAgentLoop } from './log-service.js';
|
||||
import type { ToolResult } from '../types.js';
|
||||
|
||||
const SUB_AGENT_TIMEOUT = 120000; // 2 分钟超时
|
||||
const SUB_AGENT_MAX_LOOPS = 5; // 子 Agent 最多 5 轮
|
||||
|
||||
/**
|
||||
* 执行子 Agent 任务
|
||||
* @param task 子任务描述
|
||||
* @param context 附加上下文(主 Agent 提供的参考信息)
|
||||
* @returns 子 Agent 的执行结果
|
||||
*/
|
||||
export async function executeSubAgent(task: string, context?: string): Promise<ToolResult> {
|
||||
const api = state.get<OllamaAPI>(KEYS.API);
|
||||
const model = state.get<string>('_defaultModel', '');
|
||||
|
||||
if (!api || !model) {
|
||||
return { success: false, error: '未选择模型,无法执行子任务' };
|
||||
}
|
||||
|
||||
logInfo(`子 Agent 启动`, `任务: ${task.slice(0, 80)}`);
|
||||
|
||||
const systemPrompt = `你是一个子任务执行 Agent。你的任务是高效完成指定的单一任务,然后给出简洁的结果报告。
|
||||
不要闲聊,不要说废话。直接执行任务,完成后给出结果。
|
||||
${context ? `\n附加上下文:\n${context}` : ''}`;
|
||||
|
||||
const messages: Array<{ role: string; content: string }> = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: task }
|
||||
];
|
||||
|
||||
let resultContent = '';
|
||||
let loopCount = 0;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
while (loopCount < SUB_AGENT_MAX_LOOPS) {
|
||||
loopCount++;
|
||||
|
||||
if (Date.now() - startTime > SUB_AGENT_TIMEOUT) {
|
||||
logWarn('子 Agent 超时', `${loopCount} 轮`);
|
||||
break;
|
||||
}
|
||||
|
||||
let content = '';
|
||||
let toolCalls: Array<{ name: string; arguments: Record<string, unknown> }> = [];
|
||||
|
||||
await api.chatStream({
|
||||
model,
|
||||
messages,
|
||||
stream: true,
|
||||
think: false,
|
||||
options: { num_ctx: 8192, temperature: 0.3 }
|
||||
} as any, (chunk: any) => {
|
||||
if (chunk.message?.content) content += chunk.message.content;
|
||||
if (chunk.message?.tool_calls?.length) {
|
||||
for (const tc of chunk.message.tool_calls) {
|
||||
if (tc.function?.name) {
|
||||
toolCalls.push({ name: tc.function.name, arguments: tc.function.arguments || {} });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resultContent = content;
|
||||
|
||||
if (toolCalls.length === 0) {
|
||||
// 子 Agent 完成
|
||||
logInfo(`子 Agent 完成`, `${loopCount} 轮, ${resultContent.length} 字`);
|
||||
return {
|
||||
success: true,
|
||||
content: resultContent,
|
||||
loops: loopCount,
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
|
||||
// 子 Agent 调用了工具(不支持,提示直接回答)
|
||||
messages.push({ role: 'assistant', content });
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: '你无法调用工具。请基于已有信息直接给出回答。如果需要工具才能完成,请说明你无法完成此任务及原因。'
|
||||
});
|
||||
}
|
||||
|
||||
// 达到最大轮次
|
||||
logWarn('子 Agent 达到最大轮次', `${loopCount} 轮`);
|
||||
return {
|
||||
success: true,
|
||||
content: resultContent || '(子任务执行超时)',
|
||||
loops: loopCount,
|
||||
duration: Date.now() - startTime,
|
||||
partial: true
|
||||
};
|
||||
} catch (err) {
|
||||
logError('子 Agent 错误', (err as Error).message);
|
||||
return {
|
||||
success: false,
|
||||
error: (err as Error).message,
|
||||
loops: loopCount,
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -415,6 +415,24 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// ══════════════════════════════════════════════
|
||||
// v4.3 新增工具:子代理委派
|
||||
// ══════════════════════════════════════════════
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'spawn_task',
|
||||
description: 'Spawn a sub-agent to independently execute a task. The sub-agent runs in isolation without tool access and returns a result. Use this to parallelize independent sub-tasks or delegate focused work.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: ['task'],
|
||||
properties: {
|
||||
task: { type: 'string', description: 'The task description for the sub-agent to execute.' },
|
||||
context: { type: 'string', description: 'Optional additional context or reference data for the sub-agent.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -446,7 +464,7 @@ let enabledTools: Set<string> = new Set([
|
||||
'move_file', 'copy_file', 'web_fetch', 'web_search', 'append_file', 'edit_file',
|
||||
'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files',
|
||||
'read_multiple_files', 'git', 'compress',
|
||||
'memory_search', 'memory_add', 'session_list', 'session_read'
|
||||
'memory_search', 'memory_add', 'session_list', 'session_read', 'spawn_task'
|
||||
]);
|
||||
|
||||
export function setToolEnabled(toolName: string, enabled: boolean): void {
|
||||
@@ -539,6 +557,18 @@ export async function executeTool(toolName: string, args: Record<string, unknown
|
||||
return { success: true, session: { id: session.id, title: session.title, model: session.model }, messages: msgs, total: messages.length };
|
||||
}
|
||||
|
||||
// v4.3 spawn_task 走子代理
|
||||
if (toolName === 'spawn_task') {
|
||||
const { executeSubAgent } = await import('./sub-agent.js');
|
||||
const task = args.task as string;
|
||||
const context = args.context as string | undefined;
|
||||
if (!task) return { success: false, error: '缺少 task 参数' };
|
||||
logInfo(`子代理委派: ${task.slice(0, 80)}`);
|
||||
const result = await executeSubAgent(task, context);
|
||||
logToolResult('spawn_task', result.success, result.success ? `完成, ${(result as any).loops} 轮` : result.error);
|
||||
return result;
|
||||
}
|
||||
|
||||
// run_command 走 workspace IPC
|
||||
if (toolName === 'run_command') {
|
||||
const command = args.command as string;
|
||||
@@ -581,7 +611,7 @@ export function getToolIcon(name: string): string {
|
||||
edit_file: '✂️', get_file_info: 'ℹ️', tree: '🌳', download_file: '⬇️',
|
||||
diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚',
|
||||
git: '🔖', compress: '🗜️', web_search: '🔍',
|
||||
memory_search: '🧠', memory_add: '💾', session_list: '📋', session_read: '📖'
|
||||
memory_search: '🧠', memory_add: '💾', session_list: '📋', session_read: '📖', spawn_task: '🤖'
|
||||
};
|
||||
return icons[name] || '🔧';
|
||||
}
|
||||
@@ -595,7 +625,7 @@ export function formatToolName(name: string): string {
|
||||
get_file_info: '文件信息', tree: '目录树', download_file: '下载文件',
|
||||
diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取',
|
||||
git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
|
||||
memory_search: '搜索记忆', memory_add: '添加记忆', session_list: '会话列表', session_read: '读取会话'
|
||||
memory_search: '搜索记忆', memory_add: '添加记忆', session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派'
|
||||
};
|
||||
return names[name] || name;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user