/** * Agent Chat Route — 通用 Agent 对话 SSE 端点 * * 存储架构:conversations/agents/{agentId}/{conversationId}/ * ├── meta.json ← 对话元数据 * ├── {messageId}/messages.log ← 每轮对话日志 * └── history.json ← 有序的消息列表(供前端展示 + 上下文恢复) * * 多轮对话: * - ClaudeCliProtocol 持久进程 → 天然多轮 * - 服务器重启后自动恢复:从 history.json 加载历史,注入 system prompt + 历史摘要 * * 使用 @mas/acp-core 的 ClaudeCliProtocol。 */ const express = require('express'); const path = require('path'); const fs = require('fs'); const { randomUUID } = require('crypto'); const { ClaudeCliProtocol } = require('@mas/acp-core'); const { Parser } = require('../src/core/parser'); const AgentManager = require('../src/core/agent-manager'); const { GoalsService } = require('../services/goals'); const { saveSessionSummary, loadRecentMemory, retrieveRelevantPlans, autoReflect, consolidateMemory, generateDailyReport } = require('../services/memory-service'); const systemConfig = require('../config'); const router = express.Router(); const DATA_DIR = path.resolve(__dirname, '../../data/agents'); function getAgentDir(id) { if (id && id.includes(':')) { const [compId, subId] = id.split(':'); return path.resolve(__dirname, `../../data/${compId}/agents/${subId}`); } return path.resolve(__dirname, `../../data/agents/${id}`); } const goalsService = new GoalsService(); // CONV_ROOT is now dynamically evaluated per-agent based on DATA_DIR. function loadSystemConfig() { const configDir = path.resolve(__dirname, '../config'); const backends = JSON.parse(fs.readFileSync(path.join(configDir, 'backends.json'), 'utf-8')); const { loadConfigFromSkills } = require('../src/core/skill-config-loader'); const skillsDir = systemConfig.skillsCodeDir; return loadConfigFromSkills(skillsDir, backends); } const fullSystemConfig = loadSystemConfig(); // 初始化 AgentManager 用于 Sub-Agent 调用 const agentManager = new AgentManager(fullSystemConfig, path.resolve(__dirname, '../../')); // ═══════════════════════════════════════ // meta_creator 硬编码执行器 // 保证 AgentScaffold 父类一定被调用 // ═══════════════════════════════════════ /** * 从 Innovator 的任务文本中解析 Agent 创建参数,直接调用 AgentScaffold。 * 不依赖 Claude 子进程拼 curl 命令,100% 可靠。 * * @param {string} task - Innovator 传来的任务描述 * @param {string} parentAgentId - 父 Agent ID (如 'innovator-001') * @param {string} convId - 当前对话 ID * @param {function} sendSSE - SSE 回调 * @param {string} callId - 调用 ID * @returns {Promise<{agent: string, output: string, status: string}>} */ async function executeMetaCreator(task, parentAgentId, convId, sendSSE, callId) { sendSSE('sub_agent', { event: 'chunk', callId, agent: 'meta_creator', chunk: '🏗️ [meta_creator] 开始通过 AgentScaffold 父类创建 Agent...\n' }); // Step 1: 从任务文本中解析 Agent 名称 // 支持多种格式: "data/agents/poetry_writer", "Agent: poetry_writer", "名称: xxx", "agent_name" 等 let agentName = null; let displayName = null; // 尝试匹配常见模式 const patterns = [ /data\/agents\/(\w+)/, /[Aa]gent[:\s]+(\w+)/, /名称[:\s]*(\S+)/, /创建[^"]*?[「"'"]([^"'」]+)['"」]/, /(?:id|ID)[:\s]*['"]*(\w+)['"]*/, ]; for (const pat of patterns) { const m = task.match(pat); if (m) { agentName = m[1].replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase(); break; } } // 如果没解析到,从领域关键词生成 if (!agentName) { const domainMatch = task.match(/领域[:\s]*([^\n,,]+)/); if (domainMatch) { agentName = domainMatch[1].trim() .replace(/[\u4e00-\u9fa5]+/g, (m) => { // 简单中文→拼音映射(仅常用词) const map = { '诗词': 'poetry', '小说': 'novel', '古典': 'classical', '创作': 'writer', '写诗': 'poetry', '写作': 'writing' }; return map[m] || m; }) .replace(/[^a-zA-Z0-9]/g, '_') .replace(/_+/g, '_') .replace(/^_|_$/g, '') .toLowerCase(); } } if (!agentName) agentName = `agent_${Date.now().toString(36)}`; // 解析显示名称 const displayMatch = task.match(/显示名[称:\s]*([^\n,,]+)/) || task.match(/名称[:\s]*([^\n,,]+)/); displayName = displayMatch ? displayMatch[1].trim() : agentName; sendSSE('sub_agent', { event: 'chunk', callId, agent: 'meta_creator', chunk: `📝 Agent ID: ${agentName}\n📝 显示名称: ${displayName}\n` }); // Step 2: 读取 knowhow 和 benchmark(如果有) const parentDir = getAgentDir(parentAgentId); const workspaceDir = path.join(parentDir, 'workspace', agentName); let knowhow = ''; let benchmark = ''; // 搜索可能的 knowhow/benchmark 位置 const searchDirs = [workspaceDir, path.join(parentDir, 'workspace'), path.join(parentDir, 'knowledge')]; for (const dir of searchDirs) { if (!knowhow) { const kp = path.join(dir, 'knowhow.md'); if (fs.existsSync(kp)) knowhow = fs.readFileSync(kp, 'utf-8'); } if (!benchmark) { const bp = path.join(dir, 'benchmark.md'); if (fs.existsSync(bp)) benchmark = fs.readFileSync(bp, 'utf-8'); } } sendSSE('sub_agent', { event: 'chunk', callId, agent: 'meta_creator', chunk: `📚 Knowhow: ${knowhow ? `${knowhow.length}字` : '未找到'}\n📊 Benchmark: ${benchmark ? `${benchmark.length}字` : '未找到'}\n` }); // Step 3: 生成基础 SKILL.md (用 knowhow + benchmark 合成) const skillMd = generateSkillMd(agentName, displayName, knowhow, benchmark, task); // Step 4: 调用 AgentScaffold 父类创建完整目录 const { AgentScaffold } = await import('../../packages/agent-core/AgentScaffold.js'); const scaffold = new AgentScaffold(DATA_DIR); const agentMd = `# ${displayName}\n\n你是 **${displayName}**,专业的 AI Agent。\n\n## 执行准则\n你的完整操作手册硬编码在 main_skill/SKILL.md 中,请严格遵照执行。\n`; const agentDir = await scaffold.create({ id: agentName, type: 'skill', // 执行型 Agent name: displayName, parentId: parentAgentId, bootstrap: { agentMd } }); // 写入生成的 SKILL.md const skillPath = path.join(agentDir, '.claude', 'skills', 'main_skill', 'SKILL.md'); fs.writeFileSync(skillPath, skillMd, 'utf-8'); // 如果有 knowhow/benchmark,复制到新 Agent 的 knowledge/ 下 const knowledgeDir = path.join(agentDir, 'knowledge'); if (knowhow) fs.writeFileSync(path.join(knowledgeDir, 'knowhow.md'), knowhow, 'utf-8'); if (benchmark) fs.writeFileSync(path.join(knowledgeDir, 'benchmark.md'), benchmark, 'utf-8'); const output = `✅ Agent "${displayName}" (${agentName}) 已通过 AgentScaffold 父类创建完毕。 目录: data/agents/${agentName}/ 标准结构: ├── agent.md (身份定义) ├── soul.md (灵魂) ├── identity.md (标识) ├── config.json (配置,含 cli.toolPolicy) ├── .claude/skills/main_skill/SKILL.md (核心技能, ${skillMd.length}字) ├── memory/ (记忆) ├── knowledge/ (知识${knowhow ? ', 含 knowhow.md' : ''}${benchmark ? ', 含 benchmark.md' : ''}) ├── workspace/ (工作区) └── logs/ (日志) [RESULT: DONE]`; sendSSE('sub_agent', { event: 'chunk', callId, agent: 'meta_creator', chunk: output }); console.log(`[meta_creator] ✅ Agent "${displayName}" (${agentName}) 创建完毕: ${agentDir}`); return { agent: 'meta_creator', output, status: 'success' }; } /** * 根据 knowhow/benchmark/task 生成 SKILL.md 内容 */ function generateSkillMd(agentName, displayName, knowhow, benchmark, task) { // 从 task 中提取领域信息 const domainMatch = task.match(/领域[:\s]*([^\n,,]+)/); const domain = domainMatch ? domainMatch[1].trim() : displayName; let content = `--- name: ${agentName} description: > ${displayName} — 专业的 ${domain} Agent。 基于深度调研的方法论和标杆案例创建。 version: 1.0 domain: ${domain} --- # ${displayName} 你是 **${displayName}**,专业的 ${domain} 执行 Agent。 ## 核心方法论 `; if (knowhow) { // 提取 knowhow 中的关键部分(不超过 3000 字) content += knowhow.slice(0, 3000); } else { content += `(待补充 — 请通过 meta_evolving 迭代训练补充方法论)\n`; } content += `\n\n## 质量标准\n\n`; if (benchmark) { content += benchmark.slice(0, 1500); } else { content += `(待补充 — 请通过 meta_evolving 迭代训练补充评分标准)\n`; } content += `\n\n## 输出协议\n\n完成后**必须**以 \`[RESULT: DONE]\` 标记收尾。\n`; return content; } // ═══════════════════════════════════════ // Bootstrap 工具 // ═══════════════════════════════════════ function loadFile(agentDir, filename) { try { return fs.readFileSync(path.join(agentDir, filename), 'utf-8'); } catch { return ''; } } // ═══════════════════════════════════════ // 基类模板目录 — 运行时继承 // ═══════════════════════════════════════ const BASE_DIR = path.join(__dirname, '../../packages/agent-core/base'); /** * 构建 system prompt — 三层继承 * * Layer 1: base/common/* (所有 Agent 通用的工具规范、能力说明) * Layer 2: base/{type}/*-base.md (类型专属的角色定义) * Layer 3: data/agents/{id}/* (个体专属的覆盖/扩展) * * 继承规则: * - agent.md: base/type/agent-base.md + agent/agent.md (追加) * - tools.md: base/common/tools-base.md + agent/tools.md (追加) * - 其他文件 (soul.md, identity.md 等): 只从 agent/ 读取(纯个性化) */ function buildSystemPrompt(agentDir, userMessage) { const config = loadAgentConfig(agentDir); const agentType = config?.type || 'skill'; const parts = []; // ── Layer 1: 通用基类 ── const commonFiles = ['tools-base.md', 'capabilities.md']; for (const f of commonFiles) { const fp = path.join(BASE_DIR, 'common', f); if (fs.existsSync(fp)) { const content = fs.readFileSync(fp, 'utf-8').trim(); if (content) parts.push(content); } } // ── Layer 2: 类型基类 ── const typeBaseAgent = path.join(BASE_DIR, agentType, 'agent-base.md'); if (fs.existsSync(typeBaseAgent)) { const content = fs.readFileSync(typeBaseAgent, 'utf-8').trim(); if (content) parts.push(content); } // ── Layer 3: Agent 专属文件 ── const agentListText = Object.entries(fullSystemConfig.agents || {}) .filter(([name]) => name !== 'master' && name !== 'innovator') .map(([name, cfg]) => { const typeLabel = cfg.type === 'orchestrator' ? '子编排' : cfg.type; return `- ${name} (${typeLabel}): ${cfg.description || ''}`; }) .join('\n'); const agentFiles = ['agent.md', 'soul.md', 'identity.md', 'user.md', 'tools.md']; for (const f of agentFiles) { let c = loadFile(agentDir, f); if (c.trim()) { if (c.includes('{{AVAILABLE_AGENTS}}')) { c = c.replace('{{AVAILABLE_AGENTS}}', agentListText); } parts.push(c.trim()); } } // heartbeat.md const heartbeat = loadFile(agentDir, 'heartbeat.md'); if (heartbeat.trim()) parts.push(heartbeat.trim()); // ── Phase 3: 注入 OKR 目标上下文 ── const agentIdForMemory = config?.id || path.basename(agentDir); try { const goalContext = goalsService.getActiveContextForAgent(agentIdForMemory); if (goalContext) { parts.push(goalContext.trim()); } } catch (err) { console.error(`[GoalContext] ⚠️ 读取目标上下文失败: ${err.message}`); } // memory — 核心记忆索引 const memory = loadFile(agentDir, 'memory/memory.md'); if (memory.trim()) parts.push('---\n\n# 我的记忆\n\n' + memory.trim()); // short-term — 近期对话摘要 (Phase 1 MVP) const recentMemory = loadRecentMemory(agentIdForMemory); if (recentMemory) parts.push('---\n\n' + recentMemory); // hippocampus — 海马体检索相关经验 (Phase 2) // 将 userMessage 放在前,避免 todo.md 历史堆积导致当前任务的关键词被截断 const todoContent = loadFile(agentDir, 'todo.md'); const searchText = [userMessage || '', todoContent].join('\n\n'); const hippocampusPlans = retrieveRelevantPlans(agentIdForMemory, searchText); if (hippocampusPlans) parts.push('---\n\n' + hippocampusPlans); // skills let mainSkill = loadFile(agentDir, '.claude/skills/main_skill/SKILL.md'); if (mainSkill.trim()) { if (mainSkill.includes('{{AVAILABLE_AGENTS}}')) { mainSkill = mainSkill.replace('{{AVAILABLE_AGENTS}}', agentListText); } parts.push('---\n\n# 我的核心主技能 (执行准则)\n\n' + mainSkill.trim()); } const skills = loadFile(agentDir, '.claude/skills/skills.md'); if (skills.trim()) parts.push('---\n\n' + skills.trim()); // ── 扫描所有 skill 子目录的 SKILL.md ── const skillsDir = path.join(agentDir, '.claude', 'skills'); if (fs.existsSync(skillsDir)) { const entries = fs.readdirSync(skillsDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory() || entry.name === 'main_skill') continue; const skillMdPath = path.join(skillsDir, entry.name, 'SKILL.md'); if (fs.existsSync(skillMdPath)) { const skillContent = fs.readFileSync(skillMdPath, 'utf-8').trim(); if (skillContent) { parts.push(`---\n\n## 依赖技能: ${entry.name}\n\n${skillContent}`); } } } } return parts.join('\n\n---\n\n'); } /** * 构建 cron 恢复指令 — 合并基类 + Agent 专属 cron * * Layer 1: base/{type}/cron-base.json (类型通用的定时任务) * Layer 2: data/agents/{id}/cron.json (Agent 专属追加) * * 合并策略: 合并两个数组,去重 (相同 schedule+prompt 只保留一个) */ function buildCronInstructions(agentDir) { const config = loadAgentConfig(agentDir); const agentType = config?.type || 'skill'; let allCrons = []; // Layer 1: 基类 cron const baseCronPath = path.join(BASE_DIR, agentType, 'cron-base.json'); if (fs.existsSync(baseCronPath)) { try { const base = JSON.parse(fs.readFileSync(baseCronPath, 'utf-8')); allCrons = allCrons.concat(base); } catch (err) { console.error(`[Cron] ⚠️ 读取 ${baseCronPath} 失败:`, err.message); } } // Layer 2: Agent 专属 cron (追加) const agentCronPath = path.join(agentDir, 'cron.json'); if (fs.existsSync(agentCronPath)) { try { const agent = JSON.parse(fs.readFileSync(agentCronPath, 'utf-8')); allCrons = allCrons.concat(agent); } catch (err) { console.error(`[Cron] ⚠️ 读取 ${agentCronPath} 失败:`, err.message); } } // 过滤已启用 + 按 schedule 去重(后加入的 Agent 专属 cron 覆盖基类) const enabled = allCrons.filter(c => c.enabled !== false); const scheduleMap = new Map(); for (const c of enabled) { // 同一 schedule 下,后者(Agent专属)覆盖前者(基类) scheduleMap.set(c.schedule, c); } const unique = Array.from(scheduleMap.values()); if (unique.length === 0) return ''; const lines = unique.map((c, i) => { const recur = c.recurring !== false ? '循环' : '单次'; return `${i + 1}. 调度: \`${c.schedule}\` (${recur})\n Prompt: "${c.prompt}"`; }).join('\n'); return `\n\n---\n\n## ⏰ 定时任务恢复\n\n**重要**: 请在处理用户消息之前,先用 CronCreate 工具创建以下定时任务(这些是你的持久化调度配置,每次新会话需重建):\n\n${lines}\n\n请依次调用 CronCreate 创建上述任务,然后再回答用户。`; } function loadAgentConfig(agentDir) { try { return JSON.parse(fs.readFileSync(path.join(agentDir, 'config.json'), 'utf-8')); } catch { return null; } } function loadIdentity(agentDir) { const content = loadFile(agentDir, 'identity.md'); const r = { name: 'Agent', emoji: '🤖', greeting: '' }; const nm = content.match(/名称\**[::]\s*(.+)/); if (nm) r.name = nm[1].trim(); // fallback: 如果 identity.md 没有名称,从 config.json 取 name if (r.name === 'Agent') { const cfg = loadAgentConfig(agentDir); if (cfg && cfg.name && cfg.name !== 'Agent') r.name = cfg.name; } const em = content.match(/Emoji\**[::]\s*(.+)/); if (em) r.emoji = em[1].trim(); const gm = content.match(/## 问候语\n"?([^"]*)"?/s); if (gm) r.greeting = gm[1].trim().replace(/^"|"$/g, ''); return r; } function setupSSE(res) { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', }); return (event, data) => res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); } // ═══════════════════════════════════════ // 对话存储 — conversationId/messageId 两层 // ═══════════════════════════════════════ function getConvDir(agentId, convId) { const dir = path.join(getAgentDir(agentId), 'conversations', convId); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); return dir; } /** 读写 meta.json */ function loadMeta(agentId, convId) { try { return JSON.parse(fs.readFileSync(path.join(getAgentDir(agentId), 'conversations', convId, 'meta.json'), 'utf-8')); } catch { return null; } } function saveMeta(agentId, convId, meta) { const dir = getConvDir(agentId, convId); meta.updatedAt = new Date().toISOString(); fs.writeFileSync(path.join(dir, 'meta.json'), JSON.stringify(meta, null, 2)); } /** 读写 history.json — 有序消息列表 */ function loadHistory(agentId, convId) { try { return JSON.parse(fs.readFileSync(path.join(getAgentDir(agentId), 'conversations', convId, 'history.json'), 'utf-8')); } catch { return []; } } function appendHistory(agentId, convId, msg) { const history = loadHistory(agentId, convId); history.push(msg); const dir = getConvDir(agentId, convId); fs.writeFileSync(path.join(dir, 'history.json'), JSON.stringify(history, null, 2)); } /** 写日志(流式或单次) */ function logMessageChunk(agentId, convId, content) { const dir = getConvDir(agentId, convId); fs.appendFileSync(path.join(dir, 'messages.log'), content, 'utf-8'); } /** 传统单次写日志 (为了兼容) */ function logMessage(agentId, convId, role, content) { const ts = new Date().toISOString(); const tag = role === 'user' ? 'USER' : 'AGENT'; const line = `[${ts}] [${tag}]\n${content}\n\n`; logMessageChunk(agentId, convId, line); } function listConversations(agentId) { const agentConvDir = path.join(getAgentDir(agentId), 'conversations'); if (!fs.existsSync(agentConvDir)) return []; try { return fs.readdirSync(agentConvDir, { withFileTypes: true }) .filter(d => d.isDirectory()) .map(d => { const m = loadMeta(agentId, d.name); if (m) m.id = m.id || m.conversationId || d.name; return m; }) .filter(Boolean) .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); } catch { return []; } } // ═══════════════════════════════════════ // ACP Session 池 — 支持多轮 + 上下文恢复 // ═══════════════════════════════════════ // key: `${agentId}:${conversationId}` → { protocol, messageCount } const sessions = new Map(); async function getOrCreateProtocol(agentId, convId) { const key = `${agentId}:${convId}`; let entry = sessions.get(key); if (entry && entry.protocol.isConnected) { return entry; } // 关闭旧的 if (entry) { try { entry.protocol.disconnect(); } catch {} } const agentDir = getAgentDir(agentId); const identity = loadIdentity(agentDir); const agentConfig = loadAgentConfig(agentDir); // 工具隔离策略:从 config.json 的 cli.toolPolicy 读取(由 AgentScaffold 父类生成) // 编排型 Agent 禁用所有原生工具,强制纯文本 [CALL:] 输出 const cliOptions = {}; const toolPolicy = agentConfig?.cli?.toolPolicy; if (toolPolicy?.mode === 'full') { // 执行型 Agent:完整工具权限(不设 allowedTools 限制) console.log(`[AgentChat] 🔓 ${identity.name} 工具策略: full (所有工具可用)`); } else if (toolPolicy && toolPolicy.allowedTools !== undefined) { // 使用 config.json 中声明的策略 (新架构) cliOptions.allowedTools = toolPolicy.allowedTools; if (toolPolicy.disallowedTools) cliOptions.disallowedTools = toolPolicy.disallowedTools; console.log(`[AgentChat] 🔒 ${identity.name} 工具策略 (from config): allowedTools=${JSON.stringify(cliOptions.allowedTools)}`); } else if (agentConfig && (agentConfig.type === 'orchestrator')) { // 兼容旧 config.json (没有 cli.toolPolicy 字段) — 仅 orchestrator 默认禁用 cliOptions.allowedTools = ''; console.log(`[AgentChat] 🔒 ${identity.name} 启用编排模式 (legacy fallback): 原生工具已全部禁用`); } const protocol = new ClaudeCliProtocol({ name: identity.name }, cliOptions); await protocol.connect(null, agentDir); await protocol.initialize(); await protocol.newSession({ cwd: agentDir }); const newEntry = { protocol, messageCount: 0, agentId, convId }; sessions.set(key, newEntry); console.log(`[AgentChat] 🧬 ${identity.name} ACP 会话已创建 (conv: ${convId})`); return newEntry; } /** * 构建 prompt — 根据是否首次消息 & 是否有历史上下文 * * 场景 A: 全新对话的第一条消息 → system prompt + 用户消息 * 场景 B: 同一进程中的后续消息 → 直接发消息(ACP 自带上下文) * 场景 C: 服务重启后恢复对话 → system prompt + 历史摘要 + 用户消息 */ function buildPrompt(agentDir, agentId, convId, userMessage, isNewSession) { if (!isNewSession) { // 场景 B: ACP 进程还在,多轮上下文自动保持 return userMessage; } const systemPrompt = buildSystemPrompt(agentDir, userMessage); const cronInstructions = buildCronInstructions(agentDir); const history = loadHistory(agentId, convId); if (history.length === 0) { // 场景 A: 全新对话 return `${systemPrompt}${cronInstructions}\n\n---\n\n## 用户消息\n\n${userMessage}`; } // 场景 C: 恢复上下文 — 注入历史摘要 const recentHistory = history.slice(-20); // 最近 20 条 const historyText = recentHistory.map(m => { const role = m.role === 'user' ? 'Human' : 'Assistant'; const content = m.content.length > 500 ? m.content.slice(0, 500) + '...(截断)' : m.content; return `${role}: ${content}`; }).join('\n\n'); return `${systemPrompt}${cronInstructions}\n\n---\n\n## 之前的对话历史(供你参考以保持上下文连贯)\n\n${historyText}\n\n---\n\n## 用户新消息\n\n${userMessage}`; } // ═══════════════════════════════════════ // 路由 // ═══════════════════════════════════════ router.get('/list', (req, res) => { try { const allAgents = [] const DATA_ROOT = path.resolve(__dirname, '../../data') // 辅助函数:解析 Agent 目录 const scanAgentDir = (baseDir, companyId) => { if (!fs.existsSync(baseDir)) return const entries = fs.readdirSync(baseDir, { withFileTypes: true }) for (const e of entries) { if (!e.isDirectory() || e.name.startsWith('.')) continue const agentDir = path.join(baseDir, e.name) const config = loadAgentConfig(agentDir) if (!config) continue const identity = loadIdentity(agentDir) let lastActiveAt = null const convDir = path.join(agentDir, 'conversations') if (fs.existsSync(convDir)) { try { const convs = fs.readdirSync(convDir, { withFileTypes: true }).filter(d => d.isDirectory()) let latest = 0 for (const c of convs) { const metaPath = path.join(convDir, c.name, 'meta.json') if (fs.existsSync(metaPath)) { try { const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')) const t = new Date(meta.updatedAt || meta.createdAt).getTime() if (t > latest) latest = t } catch {} } } if (latest > 0) lastActiveAt = new Date(latest).toISOString() } catch {} } // System 级 Agent ID 保持原样,Company 级需要拼装 "companyId:agentName" let agentId = config.id || e.name; if (companyId !== 'system') { agentId = `${companyId}:${agentId.includes(':') ? agentId.split(':')[1] : agentId}`; } allAgents.push({ id: agentId, name: identity.name, emoji: identity.emoji, type: config.type, status: config.status, greeting: identity.greeting, lastActiveAt, companyId // 新增用于前端分组过滤 }) } } // 1. 扫描 System Agents scanAgentDir(path.join(DATA_ROOT, 'agents'), 'system') // 2. 扫描 Company Agents if (fs.existsSync(DATA_ROOT)) { const roots = fs.readdirSync(DATA_ROOT, { withFileTypes: true }) for (const r of roots) { // 如果不是目录,或是公共资源库/系统代理,则跳过 if (!r.isDirectory() || ['agents', 'skills', 'templates'].includes(r.name)) continue scanAgentDir(path.join(DATA_ROOT, r.name, 'agents'), r.name) } } // 排序: master 永远第一,其他按活跃倒序 allAgents.sort((a, b) => { if (a.type === 'master') return -1 if (b.type === 'master') return 1 const ta = a.lastActiveAt ? new Date(a.lastActiveAt).getTime() : 0 const tb = b.lastActiveAt ? new Date(b.lastActiveAt).getTime() : 0 return tb - ta }) res.json(allAgents) } catch (err) { res.status(500).json({ error: err.message }) } }); // ── Phase 2: 小脑方案固化 API ── router.post('/:agentId/memory/cerebellum', (req, res) => { const { saveCerebellumPlan } = require('../services/memory-service'); const { scenario, keywords, steps, conditions, notes, rawContent, confidence } = req.body; if (!scenario) return res.status(400).json({ error: 'scenario is required' }); const result = saveCerebellumPlan(req.params.agentId, { scenario, keywords, steps, conditions, notes, rawContent, confidence: confidence || 0.7, }); if (result) { res.json({ success: true, path: result }); } else { res.status(500).json({ error: 'Failed to save cerebellum plan' }); } }); // ── Phase 3: 记忆管理 API ── router.post('/:agentId/memory/consolidate', (req, res) => { const result = consolidateMemory(req.params.agentId); res.json(result); }); router.post('/:agentId/memory/daily-report', (req, res) => { const reportPath = generateDailyReport(req.params.agentId); res.json({ success: !!reportPath, path: reportPath }); }); router.get('/:agentId/info', (req, res) => { const agentDir = getAgentDir(req.params.agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); const config = loadAgentConfig(agentDir); const identity = loadIdentity(agentDir); res.json({ id: config?.id || req.params.agentId, ...identity, type: config?.type, status: config?.status, bootstrapFiles: ['agent.md','soul.md','identity.md','user.md','tools.md','heartbeat.md'].filter(f => loadFile(agentDir, f).trim().length > 0), }); }); /** 列出对话 */ router.get('/:agentId/conversations', (req, res) => { res.json(listConversations(req.params.agentId)); }); /** 获取对话详情 (meta + history) */ router.get('/:agentId/conversations/:convId', (req, res) => { const { agentId, convId } = req.params; const meta = loadMeta(agentId, convId); if (!meta) return res.status(404).json({ error: 'Not found' }); const messages = loadHistory(agentId, convId); // 同时返回 sub-agent logs 列表 const subLogs = listSubAgentLogs(agentId, convId); res.json({ ...meta, messages, subLogs }); }); /** 获取子 Agent 日志列表 */ router.get('/:agentId/conversations/:convId/sub-logs', (req, res) => { res.json(listSubAgentLogs(req.params.agentId, req.params.convId)); }); /** 获取单个子 Agent 日志内容 */ router.get('/:agentId/conversations/:convId/sub-logs/:logFile', (req, res) => { const { agentId, convId, logFile } = req.params; const logPath = path.join(getConvDir(agentId, convId), logFile); if (!logFile.endsWith('.log') || !fs.existsSync(logPath)) { return res.status(404).json({ error: 'Log not found' }); } const content = fs.readFileSync(logPath, 'utf-8'); res.json({ file: logFile, content }); }); function listSubAgentLogs(agentId, convId) { const convDir = getConvDir(agentId, convId); if (!fs.existsSync(convDir)) return []; return fs.readdirSync(convDir) .filter(f => f.startsWith('sub-') && f.endsWith('.log')) .map(f => { const match = f.match(/^(sub-[a-f0-9]+)-(.+)\.log$/); const content = fs.readFileSync(path.join(convDir, f), 'utf-8'); const status = content.includes('STATUS: SUCCESS') ? 'success' : content.includes('STATUS: ERROR') ? 'error' : 'running'; return { file: f, callId: match ? match[1] : f, agent: match ? match[2] : 'unknown', status, size: content.length, content, // 直接内嵌内容,省一次 API 调用 }; }) .sort((a, b) => a.file.localeCompare(b.file)); } /** 删除对话 */ router.delete('/:agentId/conversations/:convId', (req, res) => { const { agentId, convId } = req.params; try { const dir = path.join(getAgentDir(agentId), 'conversations', convId); if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true }); const key = `${agentId}:${convId}`; const entry = sessions.get(key); if (entry && entry.protocol) entry.protocol.disconnect(); sessions.delete(key); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); /** 停止当前正在生成的任务 */ router.post('/:agentId/chat/:convId/stop', (req, res) => { const { agentId, convId } = req.params; const key = `${agentId}:${convId}`; const entry = sessions.get(key); if (entry && entry.protocol) { entry.protocol.disconnect(); // 关闭底层 claude cli 进程 sessions.delete(key); // 下次从冷启动重新加载 return res.json({ success: true, message: 'Agent 已中断' }); } res.json({ success: false, message: '未找到运行中的 Agent 进程' }); }); /** 读取 bootstrap file */ router.get('/:agentId/bootstrap/:filename', (req, res) => { const allowed = ['agent.md','soul.md','identity.md','user.md','tools.md','heartbeat.md']; if (!allowed.includes(req.params.filename)) return res.status(400).json({ error: 'Invalid' }); res.json({ filename: req.params.filename, content: loadFile(getAgentDir(req.params.agentId), req.params.filename) }); }); /** * v5: Agent 文件树 + 文件预览 + todo/plan */ // GET /api/agent/:agentId/files — 递归列出 Agent 目录文件树 router.get('/:agentId/files', (req, res) => { const agentDir = getAgentDir(req.params.agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); const scanDir = (dir, relativePath = '') => { const entries = fs.readdirSync(dir, { withFileTypes: true }); const result = []; for (const entry of entries) { const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name; if (entry.name.startsWith('.') && entry.name !== '.claude') continue; if (entry.isDirectory()) { const children = scanDir(path.join(dir, entry.name), relPath); result.push({ name: entry.name, path: relPath, type: 'dir', children, count: children.length }); } else { const stat = fs.statSync(path.join(dir, entry.name)); result.push({ name: entry.name, path: relPath, type: 'file', size: stat.size }); } } return result.sort((a, b) => { if (a.type !== b.type) return a.type === 'dir' ? -1 : 1; return a.name.localeCompare(b.name); }); }; try { const tree = scanDir(agentDir); const countFiles = (nodes) => { return nodes.reduce((sum, n) => sum + (n.type === 'file' ? 1 : countFiles(n.children || [])), 0); }; res.json({ tree, totalFiles: countFiles(tree) }); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/agent/:agentId/file/* — 读取 Agent 目录内的文件内容 router.get('/:agentId/file/*', (req, res) => { const filePath = req.params[0]; if (!filePath) return res.status(400).json({ error: 'file path required' }); const fullPath = path.join(getAgentDir(req.params.agentId), filePath); if (!fullPath.startsWith(getAgentDir(req.params.agentId))) { return res.status(403).json({ error: 'Access denied' }); } if (!fs.existsSync(fullPath)) return res.status(404).json({ error: 'File not found' }); try { const stat = fs.statSync(fullPath); if (stat.isDirectory()) return res.status(400).json({ error: 'Path is a directory' }); const ext = path.extname(fullPath).toLowerCase(); const isBinary = ['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf'].includes(ext); if (isBinary) return res.json({ path: filePath, size: stat.size, binary: true, content: '(binary file)' }); const content = fs.readFileSync(fullPath, 'utf-8'); res.json({ path: filePath, size: stat.size, content, ext }); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET/PUT /api/agent/:agentId/todo router.get('/:agentId/todo', (req, res) => { const fp = path.join(getAgentDir(req.params.agentId), 'todo.md'); if (!fs.existsSync(fp)) return res.json({ content: '' }); res.json({ content: fs.readFileSync(fp, 'utf-8') }); }); router.put('/:agentId/todo', (req, res) => { const agentDir = getAgentDir(req.params.agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); fs.writeFileSync(path.join(agentDir, 'todo.md'), req.body.content || '', 'utf-8'); res.json({ success: true }); }); // GET/PUT /api/agent/:agentId/plan router.get('/:agentId/plan', (req, res) => { const fp = path.join(getAgentDir(req.params.agentId), 'plan.md'); if (!fs.existsSync(fp)) return res.json({ content: '' }); res.json({ content: fs.readFileSync(fp, 'utf-8') }); }); router.put('/:agentId/plan', (req, res) => { const agentDir = getAgentDir(req.params.agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); fs.writeFileSync(path.join(agentDir, 'plan.md'), req.body.content || '', 'utf-8'); res.json({ success: true }); }); // GET/PUT /api/agent/:agentId/cron — 定时任务声明 (cron.json) router.get('/:agentId/cron', (req, res) => { const fp = path.join(getAgentDir(req.params.agentId), 'cron.json'); if (!fs.existsSync(fp)) return res.json([]); try { res.json(JSON.parse(fs.readFileSync(fp, 'utf-8'))); } catch { res.json([]); } }); router.put('/:agentId/cron', (req, res) => { const agentDir = getAgentDir(req.params.agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); const crons = req.body; if (!Array.isArray(crons)) return res.status(400).json({ error: 'Body must be an array of cron objects' }); fs.writeFileSync(path.join(agentDir, 'cron.json'), JSON.stringify(crons, null, 2), 'utf-8'); console.log(`[Cron] 📝 ${req.params.agentId} cron.json 已更新 (${crons.length} tasks)`); res.json({ success: true, count: crons.length }); }); /** * POST /api/agent/scaffold — 通过 AgentScaffold 父类创建新 Agent 实例 * * body: { id, type, name, parentId?, bootstrap?: { agentMd?, soulMd?, skillMd? } } * * 这是 meta_creator 子 Agent 调用的核心接口,确保每个新 Agent 都 * 继承了标准目录结构(.claude/skills/main_skill, memory, workspace 等) */ router.post('/scaffold', async (req, res) => { try { const { id, type = 'skill', name, parentId, bootstrap = {} } = req.body; if (!id || !name) return res.status(400).json({ error: 'id and name are required' }); const { AgentScaffold } = await import('../../packages/agent-core/AgentScaffold.js'); const scaffold = new AgentScaffold(DATA_DIR); const agentDir = await scaffold.create({ id, type, name, parentId, bootstrap }); if (bootstrap.skillMd) { const skillPath = path.join(agentDir, '.claude', 'skills', 'main_skill', 'SKILL.md'); fs.writeFileSync(skillPath, bootstrap.skillMd, 'utf-8'); } console.log(`[Scaffold] ✅ 新 Agent "${name}" (${id}) 已通过父类脚手架创建: ${agentDir}`); res.json({ success: true, agentId: id, agentDir, message: `Agent ${name} 已创建 (v5: +todo/plan/hippocampus)`, structure: [ '.claude/skills/main_skill/', 'memory/', 'memory/cerebellum/', 'knowledge/', 'workspace/', 'conversations/', 'logs/', 'agent.md', 'soul.md', 'identity.md', 'tools.md', 'todo.md', 'plan.md', 'cron.json', 'config.json' ] }); } catch (err) { console.error('[Scaffold] Error:', err.message); res.status(500).json({ error: err.message }); } }); // ═══════════════════════════════════════ // v5: Task Dispatch — 跨 Agent 任务分发 // ═══════════════════════════════════════ /** * POST /api/agent/task/dispatch — 向目标 Agent 分发任务 * * body: { * from: 'master-001', // 发送者 Agent ID * to: 'agent-creator', // 目标 Agent ID * task: '创建一个诗人 Agent', // 任务描述 * priority: 'P0', // 优先级 * planId: 'P-001', // 在发送者 plan.md 中的编号 (可选) * autoExecute: true, // 是否自动异步触发执行 * } */ // ── Task 元数据持久化目录 ── const TASK_META_DIR = path.join(DATA_DIR, '.tasks'); if (!fs.existsSync(TASK_META_DIR)) fs.mkdirSync(TASK_META_DIR, { recursive: true }); function saveTaskMeta(taskId, meta) { fs.writeFileSync(path.join(TASK_META_DIR, `${taskId}.json`), JSON.stringify(meta, null, 2), 'utf-8'); } function loadTaskMeta(taskId) { const fp = path.join(TASK_META_DIR, `${taskId}.json`); if (!fs.existsSync(fp)) return null; try { return JSON.parse(fs.readFileSync(fp, 'utf-8')); } catch { return null; } } router.post('/task/dispatch', async (req, res) => { try { const { from, to, task, priority = 'P1', planId, goalId, nodeId, autoExecute = true } = req.body; if (!from || !to || !task) return res.status(400).json({ error: 'from, to, task are required' }); const targetDir = getAgentDir(to); const senderDir = getAgentDir(from); if (!fs.existsSync(targetDir)) return res.status(404).json({ error: `Target agent "${to}" not found` }); if (!fs.existsSync(senderDir)) return res.status(404).json({ error: `Sender agent "${from}" not found` }); const taskId = `T-${Date.now().toString(36)}`; const now = new Date().toISOString().slice(0, 16).replace('T', ' '); // 净化多行文本和竖线,防止破坏 Markdown 表格格式 const safeTask = task.replace(/\n/g, '
').replace(/\|/g, '|'); // 1. 写入目标 Agent 的 todo.md const todoPath = path.join(targetDir, 'todo.md'); let todoContent = fs.existsSync(todoPath) ? fs.readFileSync(todoPath, 'utf-8') : ''; // 移除空占位行 todoContent = todoContent.replace(/\| *\| *(暂无待办).*\n?/g, ''); const newTodoRow = `| ${taskId} | ${safeTask} | ⬚ 未开始 | ${from} | ${priority} | ${now} | - |\n`; todoContent = todoContent.trimEnd() + '\n' + newTodoRow; fs.writeFileSync(todoPath, todoContent, 'utf-8'); // 2. 更新发送者的 plan.md (如果有 planId) if (planId) { const planPath = path.join(senderDir, 'plan.md'); let planContent = fs.existsSync(planPath) ? fs.readFileSync(planPath, 'utf-8') : ''; planContent = planContent.replace(/\| *\| *(暂无计划).*\n?/g, ''); const newPlanRow = `| ${planId} | ${safeTask} | ${to} | 🔄 已分发 | ${now} | taskId: ${taskId} |\n`; planContent = planContent.trimEnd() + '\n' + newPlanRow; fs.writeFileSync(planPath, planContent, 'utf-8'); } // 3. 更新 Goal 节点状态 (如果有 goalId) if (goalId && nodeId) { try { goalsService.updateNode(goalId, nodeId, { status: 'in_progress', taskId, owner: to, }); console.log(`[TaskDispatch] 🎯 Goal ${goalId} 节点 ${nodeId} → in_progress`); } catch (err) { console.warn(`[TaskDispatch] ⚠️ Goal 更新失败: ${err.message}`); } } console.log(`[TaskDispatch] 📨 ${from} → ${to}: "${task}" (${taskId}, ${priority}${goalId ? `, goal: ${goalId}` : ''})`); // 4. 持久化 Task 元数据 (用于 task/report 回调) const executionConvId = autoExecute ? `task-${taskId}` : null; saveTaskMeta(taskId, { taskId, from, to, task: safeTask, priority, goalId: goalId || null, nodeId: nodeId || null, executionConvId, createdAt: new Date().toISOString(), status: 'dispatched', }); // 5. 异步自动触发执行 (发送 chat 消息给目标 Agent) if (autoExecute) { // 异步触发,不阻塞当前响应 setImmediate(async () => { try { const goalInfo = goalId ? `\n目标: ${goalId} → ${nodeId}` : ''; const taskMessage = `[TASK from ${from}]\n任务编号: ${taskId}\n优先级: ${priority}${goalInfo}\n\n${task}\n\n请立即执行此任务。执行完成后,如果你的 agent.md 规定了后续步骤,请在同一轮对话中继续执行,不要停下来等待上级催促!\n全部完成后使用以下命令汇报:\nnode bin/lemon.js task report --taskId ${taskId} --from ${to} --to ${from} --status done --summary "(你的完成摘要)"`; // 内部触发聊天 (通过 HTTP 自调用) const http = require('http'); const postData = JSON.stringify({ message: taskMessage, conversationId: executionConvId }); const options = { hostname: 'localhost', port: 3000, path: `/api/agent/${to}/chat`, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) }, }; const chatReq = http.request(options, (chatRes) => { let body = ''; chatRes.on('data', (chunk) => body += chunk); chatRes.on('end', () => { console.log(`[TaskDispatch] ✅ ${to} 异步执行已启动 (conv: ${executionConvId})`); }); }); chatReq.on('error', (err) => { console.error(`[TaskDispatch] ❌ 异步执行失败: ${err.message}`); }); chatReq.write(postData); chatReq.end(); } catch (err) { console.error(`[TaskDispatch] ❌ 异步触发错误: ${err.message}`); } }); } res.json({ success: true, taskId, from, to, task, priority, autoExecute, executionConvId, message: `任务已分发给 ${to}` + (autoExecute ? ',正在异步执行' : ''), }); } catch (err) { console.error('[TaskDispatch] Error:', err.message); res.status(500).json({ error: err.message }); } }); /** * POST /api/agent/task/report — Agent 汇报任务完成 * * body: { * taskId: 'T-xxx', * from: 'agent-creator', // 执行者 * to: 'master-001', // 上级(任务发送者) * status: 'done' | 'failed' | 'blocked', * summary: '已成功创建 poet Agent', * outputs: ['poet/agent.md', 'poet/soul.md'], // 产出文件 (可选) * } */ router.post('/task/report', (req, res) => { try { const { taskId, from, to, status = 'done', summary = '', outputs = [], planId, goalId, nodeId } = req.body; if (!taskId || !from || !to) return res.status(400).json({ error: 'taskId, from, to are required' }); const now = new Date().toISOString().slice(0, 16).replace('T', ' '); const statusEmoji = status === 'done' ? '✅ 完成' : status === 'failed' ? '❌ 失败' : '⏸ 阻塞'; const safeSummary = (summary || '').replace(/\n/g, '
').replace(/\|/g, '|'); // 1. 更新执行者的 todo.md (标记完成) const executorDir = getAgentDir(from); if (fs.existsSync(executorDir)) { const todoPath = path.join(executorDir, 'todo.md'); if (fs.existsSync(todoPath)) { let content = fs.readFileSync(todoPath, 'utf-8'); // 找到 taskId 所在行,更新状态和检查时间 content = content.replace( new RegExp(`(\\| *${taskId} *\\|[^|]*\\|) *[^|]* *(\\|.*)$`, 'm'), `$1 ${statusEmoji} $2`.replace(/\| *- *\|$/, `| ${now} |`) ); fs.writeFileSync(todoPath, content, 'utf-8'); } } // 2. 更新上级的 plan.md (标记完成) — 使用行级匹配,兼容各种格式 const senderDir = getAgentDir(to); if (fs.existsSync(senderDir)) { const planPath = path.join(senderDir, 'plan.md'); if (fs.existsSync(planPath)) { let content = fs.readFileSync(planPath, 'utf-8'); const lines = content.split('\n'); let updated = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // 匹配包含 taskId 的行(无论格式如何) if (line.includes(taskId) || (planId && line.includes(planId))) { // 替换状态列:找到第4个 | 分隔的列(状态列),替换为新状态 const cells = line.split('|').map(c => c.trim()); if (cells.length >= 5) { // cells: ['', '#', '子任务', '责任人', '状态', '上次检查', '备注', ''] cells[4] = ` ${statusEmoji} `; cells[5] = ` ${now} `; if (safeSummary && cells.length >= 7) { cells[6] = ` ${safeSummary} `; } lines[i] = cells.join(' | ').replace(/\*\*/g, ''); // 去掉粗体 updated = true; console.log(`[TaskReport] 📝 更新 plan.md 第 ${i + 1} 行: ${statusEmoji}`); } } } if (updated) { fs.writeFileSync(planPath, lines.join('\n'), 'utf-8'); } else { console.warn(`[TaskReport] ⚠️ 未在 ${to}/plan.md 中找到 taskId=${taskId} 的行`); } } } // 3. 更新 Goal 节点状态 (如果有 goalId) if (goalId && nodeId) { try { const goalStatus = status === 'done' ? 'done' : status === 'failed' ? 'failed' : 'blocked'; goalsService.updateNode(goalId, nodeId, { status: goalStatus, summary, output: outputs.join(', '), }); console.log(`[TaskReport] 🎯 Goal ${goalId} 节点 ${nodeId} → ${goalStatus}`); } catch (err) { console.warn(`[TaskReport] ⚠️ Goal 更新失败: ${err.message}`); } } console.log(`[TaskReport] 📋 ${from} → ${to}: ${taskId} = ${status} (${summary})`); // 4. Pipeline↔Task 桥接:自动推进 Pipeline 步骤 let pipelineAdvanced = false; try { const DATA_ROOT = path.resolve(__dirname, '../../data'); const { PipelineService } = require('../services/pipeline-service'); // 扫描所有公司的 Pipeline Runs const companyDirs = fs.readdirSync(DATA_ROOT, { withFileTypes: true }) .filter(d => d.isDirectory() && !['agents', 'skills', 'templates'].includes(d.name) && fs.existsSync(path.join(DATA_ROOT, d.name, 'company.json'))); for (const compDir of companyDirs) { const pSvc = new PipelineService(compDir.name); // from 可能是 "companyId:agentId" 格式,Pipeline step.agent 可能只是 "agentId" const agentShortId = from.includes(':') ? from.split(':')[1] : from; const activeStep = pSvc.findActiveRunStep(agentShortId) || pSvc.findActiveRunStep(from); if (activeStep) { const advanceStatus = status === 'done' ? 'done' : 'failed'; pSvc.advanceStep(activeStep.runId, activeStep.stepIndex, { output: { summary, outputs: outputs || [], taskId }, status: advanceStatus, }); pipelineAdvanced = true; console.log(`[TaskReport→Pipeline] 🔗 自动推进 ${compDir.name} Pipeline Run ${activeStep.runId} 步骤 ${activeStep.stepIndex} → ${advanceStatus}`); break; } } } catch (pipeErr) { console.warn(`[TaskReport→Pipeline] ⚠️ Pipeline 桥接失败: ${pipeErr.message}`); } // 5. 自动回调:通知上级 Agent 任务已完成,唤醒其继续执行后续步骤 if (status === 'done' || status === 'failed') { setImmediate(async () => { try { // 读取 task 元数据获取上下文 const taskMeta = loadTaskMeta(taskId); const callbackConvId = `callback-${taskId}`; const statusText = status === 'done' ? '✅ 已完成' : '❌ 失败'; const callbackMessage = `[TASK_CALLBACK]\n任务编号: ${taskId}\n执行者: ${from}\n状态: ${statusText}\n摘要: ${summary || '(无摘要)'}\n产出文件: ${outputs.length > 0 ? outputs.join(', ') : '(无)'}\n${taskMeta ? `原始任务: ${taskMeta.task}` : ''}\n\n请检查上述任务的执行结果,然后根据你的 plan.md 继续执行下一步计划。如有后续任务需要分发,请立即执行 dispatch。`; const http = require('http'); const postData = JSON.stringify({ message: callbackMessage, conversationId: callbackConvId }); const options = { hostname: 'localhost', port: 3000, path: `/api/agent/${to}/chat`, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) }, }; const chatReq = http.request(options, (chatRes) => { let body = ''; chatRes.on('data', (chunk) => body += chunk); chatRes.on('end', () => { console.log(`[TaskCallback] ✅ 已回调 ${to}: ${taskId} ${statusText} (conv: ${callbackConvId})`); }); }); chatReq.on('error', (err) => { console.error(`[TaskCallback] ❌ 回调失败: ${err.message}`); }); chatReq.write(postData); chatReq.end(); // 更新 task 元数据 if (taskMeta) { taskMeta.status = status; taskMeta.completedAt = new Date().toISOString(); taskMeta.summary = summary; saveTaskMeta(taskId, taskMeta); } } catch (err) { console.error(`[TaskCallback] ❌ 回调触发错误: ${err.message}`); } }); } res.json({ success: true, taskId, status, goalId, nodeId, message: `任务 ${taskId} 已汇报为 ${status}` }); } catch (err) { console.error('[TaskReport] Error:', err.message); res.status(500).json({ error: err.message }); } }); // ═══════════════════════════════════════ // POST /api/agent/task/reject — COO 质量门控:驳回任务 // ═══════════════════════════════════════ /** * POST /api/agent/task/reject — 驳回 Pipeline 步骤,要求 Agent 重做 * * body: { * companyId: 'lemonv', * runId: 'R-xxx', * stepIndex: 2, * reason: '文章质量不达标,缺少数据支撑', * reviewedBy: 'coo', * } */ router.post('/task/reject', (req, res) => { try { const { companyId, runId, stepIndex, reason = '', reviewedBy = 'coo' } = req.body; if (!companyId || !runId || stepIndex === undefined) { return res.status(400).json({ error: 'companyId, runId, stepIndex are required' }); } const { PipelineService } = require('../services/pipeline-service'); const pSvc = new PipelineService(companyId); const run = pSvc.rejectStep(runId, Number(stepIndex), { reason, reviewedBy }); const step = run.steps[Number(stepIndex)]; const agentId = step.agent; // 自动触发 Agent 重新执行(如果有 agent) if (agentId) { setImmediate(() => { const rejectMessage = [ `[PIPELINE TASK - REDO]`, `⚠️ 你的上次产出被 ${reviewedBy} 驳回。`, `驳回原因: ${reason}`, `这是第 ${step.rejections?.length || 1} 次重做。`, ``, `请认真阅读以上反馈,改进你的输出质量后重新提交。`, `完成后调用:`, `node bin/lemon.js task report --taskId redo-${runId}-${stepIndex} --from ${agentId} --to ${reviewedBy} --status done --summary "(改进后的完成摘要)"`, ].join('\n'); const http = require('http'); const postData = JSON.stringify({ message: rejectMessage, conversationId: `redo-${runId}-step-${stepIndex}` }); const options = { hostname: 'localhost', port: 3000, path: `/api/agent/${agentId}/chat`, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) }, }; const chatReq = http.request(options, (chatRes) => { let body = ''; chatRes.on('data', (chunk) => body += chunk); chatRes.on('end', () => { console.log(`[TaskReject] 🔄 ${agentId} 重做任务已触发`); }); }); chatReq.on('error', (err) => { console.error(`[TaskReject] ❌ 重做触发失败: ${err.message}`); }); chatReq.write(postData); chatReq.end(); }); } console.log(`[TaskReject] 🔙 ${companyId} Run ${runId} 步骤 ${stepIndex} 被 ${reviewedBy} 驳回: ${reason}`); res.json({ success: true, runId, stepIndex, reason, reviewedBy, rejectionCount: step.rejections?.length || 1, message: `步骤 ${stepIndex} 已驳回,Agent ${agentId} 将重新执行`, }); } catch (err) { console.error('[TaskReject] Error:', err.message); res.status(500).json({ error: err.message }); } }); // ═══════════════════════════════════════ // POST /api/agent/:agentId/chat — SSE 流式对话 // ═══════════════════════════════════════ router.post('/:agentId/chat', async (req, res) => { const { message, conversationId: reqConvId } = req.body; if (!message) return res.status(400).json({ error: 'message is required' }); const agentId = req.params.agentId; const agentDir = getAgentDir(agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); const convId = reqConvId || `conv-${Date.now().toString(36)}`; const msgId = `msg-${Date.now()}-${randomUUID().substring(0, 8)}`; const sendSSE = setupSSE(res); const identity = loadIdentity(agentDir); // 确保 meta.json 存在 let meta = loadMeta(agentId, convId); if (!meta) { meta = { conversationId: convId, agentId, title: req.body.isHeartbeat ? '⏱️ 自主心跳巡检' : message.slice(0, 50), type: req.body.isHeartbeat ? 'heartbeat' : 'chat', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), status: 'running', stats: { messageCount: 0, toolCallCount: 0 }, }; } if (!meta.stats) meta.stats = { messageCount: 0, toolCallCount: 0 }; meta.stats.messageCount = (meta.stats.messageCount || 0) + 1; meta.status = 'running'; saveMeta(agentId, convId, meta); // 写用户消息到两层存储 logMessage(agentId, convId, 'user', message); appendHistory(agentId, convId, { role: 'user', content: message, messageId: msgId, timestamp: new Date().toISOString() }); sendSSE('session', { conversationId: convId, agentId, messageId: msgId }); sendSSE('thinking', { message: `${identity.emoji} ${identity.name} 思考中...` }); let fullOutput = ''; // 记录 Agent 开始回复的标识头 logMessageChunk(agentId, convId, `[${new Date().toISOString()}] [AGENT]\n`); try { // 获取或创建 ACP 会话(per conversation) const entry = await getOrCreateProtocol(agentId, convId); const isNewSession = entry.messageCount === 0; // 构建 prompt(自动处理首次/后续/恢复三种场景) let prompt = buildPrompt(agentDir, agentId, convId, message, isNewSession); entry.messageCount++; let loopCount = 0; while (loopCount < 10) { loopCount++; fullOutput = ''; // Reset fullOutput for this turn // 监听流式事件 let currentToolInput = ''; const onUpdate = (payload) => { const update = payload.update; if (update?.sessionUpdate === 'agent_message_chunk' && update.content?.text) { fullOutput += update.content.text; logMessageChunk(agentId, convId, update.content.text); sendSSE('agent_output', { agent: agentId, message: update.content.text }); } else if (update?.sessionUpdate === 'agent_thought_chunk' && update.content?.text) { logMessageChunk(agentId, convId, update.content.text); sendSSE('thinking', { message: update.content.text }); } }; const onAcpEvent = (event) => { // 透传所有 ACP 事件给前端(完整可观测性) sendSSE('acp_raw', { method: event.method, params: event.params, ts: event.timestamp }); if (event.method === 'claude/tool_use_start') { currentToolInput = ''; const toolName = event.params.name; const toolMsg = `\n🔧 [TOOL_CALL] ${toolName} (ID: ${event.params.tool_use_id || 'unknown'})\n`; logMessageChunk(agentId, convId, toolMsg); sendSSE('tool_call', { tool: toolName, toolId: event.params.tool_use_id, message: `🔧 调用工具: ${toolName}` }); meta.stats.toolCallCount = (meta.stats.toolCallCount || 0) + 1; } else if (event.method === 'claude/stream/content_block_stop') { // 工具调用结束 — 输出积攒的输入参数 if (currentToolInput) { logMessageChunk(agentId, convId, ` 输入: ${currentToolInput.slice(0, 500)}${currentToolInput.length > 500 ? '...' : ''}\n`); sendSSE('tool_input', { input: currentToolInput }); currentToolInput = ''; } } else if (event.method === 'session/result') { if (event.params?.cost_usd) { sendSSE('session_cost', { cost: event.params.cost_usd, duration: event.params.duration_ms }); } if (!fullOutput && event.params?.result) { fullOutput = event.params.result; sendSSE('agent_output', { agent: agentId, message: event.params.result }); } } else if (event.method?.startsWith('claude/')) { // 捕获其他 claude 事件(tool_result 等) const innerEvent = event.params; // input_json_delta — 工具的 JSON 输入增量 if (innerEvent?.delta?.type === 'input_json_delta' && innerEvent.delta.partial_json) { currentToolInput += innerEvent.delta.partial_json; } // tool_result — 工具执行结果 if (innerEvent?.type === 'tool_result' || innerEvent?.content_block?.type === 'tool_result') { const result = innerEvent.content || innerEvent.content_block?.content || ''; const resultStr = typeof result === 'string' ? result : JSON.stringify(result); const truncated = resultStr.slice(0, 1000) + (resultStr.length > 1000 ? `...(${resultStr.length} chars)` : ''); logMessageChunk(agentId, convId, ` 结果: ${truncated}\n`); sendSSE('tool_result', { result: truncated }); } } }; entry.protocol.on('session_update', onUpdate); entry.protocol.on('acp_event', onAcpEvent); // 发送 prompt 并等待完成 await entry.protocol.sendPrompt(prompt); // 清理监听 entry.protocol.off('session_update', onUpdate); entry.protocol.off('acp_event', onAcpEvent); // 持久化当前轮次的回复 if (fullOutput) { logMessageChunk(agentId, convId, '\n\n'); appendHistory(agentId, convId, { role: 'assistant', content: fullOutput, messageId: `msg-${Date.now()}-${loopCount}`, timestamp: new Date().toISOString() }); } // 解析是否带有 [CALL: sub_agent] 标记 const calls = Parser.parseCalls(fullOutput, { allowParallel: true, agents: fullSystemConfig.agents }); if (calls.length === 0) { break; // 没有后续调用,当前回合结束 } // 存在 Sub-Agent 嵌套调用 logMessageChunk(agentId, convId, `\n\n[System] 执行子 Agent 编排调度(并行)...\n`); sendSSE('thinking', { message: `🔄 分析到系统编排指令,正在并行启动 ${calls.length} 个子代理...` }); const resultsText = []; const MAX_CONCURRENCY = 2; // Checkpoint: 保存当前调度计划 const checkpoint = { loopCount, calls: calls.map(c => ({ agent: c.agent, task: c.task })), completedCalls: [], timestamp: new Date().toISOString() }; const checkpointPath = path.join(getConvDir(agentId, convId), 'checkpoint.json'); fs.writeFileSync(checkpointPath, JSON.stringify(checkpoint, null, 2)); // 分批并发执行 (最多同时跑 MAX_CONCURRENCY 个子进程) for (let i = 0; i < calls.length; i += MAX_CONCURRENCY) { const batch = calls.slice(i, i + MAX_CONCURRENCY); await Promise.all(batch.map(async (call) => { const callId = `sub-${randomUUID().substring(0, 6)}`; const subLogPath = path.join(getConvDir(agentId, convId), `${callId}-${call.agent}.log`); // 初始化子 Agent 独立日志文件 const subLogHeader = `[${new Date().toISOString()}] [SUB_AGENT: ${call.agent}] [CALL_ID: ${callId}]\n` + `任务:\n${call.task}\n\n--- 执行输出 ---\n`; fs.writeFileSync(subLogPath, subLogHeader); sendSSE('sub_agent', { event: 'start', callId, agent: call.agent, message: `🚀 开始执行: ${call.agent}...` }); logMessageChunk(agentId, convId, `\n--- 调度 [${callId}]: ${call.agent} ---\n任务: ${call.task}\n\n`); try { let res; // ═══ 硬编码拦截: meta_creator → 直接调用 AgentScaffold 父类 ═══ if (call.agent === 'meta_creator') { res = await executeMetaCreator(call.task, agentId, convId, sendSSE, callId); // 写入子 Agent 日志 fs.appendFileSync(subLogPath, res.output + '\n'); } else { res = await agentManager.callAgent(call.agent, call.task, { metadata: { parentConvId: convId, layer: 'sub' }, onData: (chunk) => { sendSSE('sub_agent', { event: 'chunk', callId, agent: call.agent, chunk }); // 实时追加到子 Agent 独立日志 fs.appendFileSync(subLogPath, chunk); } }); } // 写入最终结果到子 Agent 日志 fs.appendFileSync(subLogPath, `\n\n--- 执行结果 ---\n[${new Date().toISOString()}] STATUS: SUCCESS\n输出长度: ${(res.output || '').length} 字符\n`); resultsText.push(`[Sub Agent ${call.agent} (${callId}) 返回结果]\n${res.output}\n[/Sub Agent ${call.agent} (${callId}) 返回结果]`); logMessageChunk(agentId, convId, `✅ 子 Agent ${call.agent} [${callId}] 执行成功。\n`); sendSSE('sub_agent', { event: 'done', callId, agent: call.agent, status: 'success', message: `✅ [${call.agent}] 执行完毕.` }); // Checkpoint: 标记为完毕 checkpoint.completedCalls.push({ callId, agent: call.agent, status: 'success' }); } catch (subErr) { // 写入错误到子 Agent 日志 fs.appendFileSync(subLogPath, `\n\n--- 执行结果 ---\n[${new Date().toISOString()}] STATUS: ERROR\n错误: ${subErr.message}\n${subErr.stack || ''}\n`); resultsText.push(`[Sub Agent ${call.agent} (${callId}) 执行失败]\n${subErr.message}\n[/Sub Agent ${call.agent} (${callId}) 执行失败]`); logMessageChunk(agentId, convId, `❌ 子 Agent ${call.agent} [${callId}] 失败: ${subErr.message}\n`); sendSSE('sub_agent', { event: 'done', callId, agent: call.agent, status: 'error', message: `❌ [${call.agent}] 抛出异常: ${subErr.message}` }); checkpoint.completedCalls.push({ callId, agent: call.agent, status: 'error', error: subErr.message }); } // 每批次后更新 checkpoint fs.writeFileSync(checkpointPath, JSON.stringify(checkpoint, null, 2)); })); } // 构造下一次反思与后续动作的 Prompt prompt = ` ## Sub Agent 执行结果反馈 ${resultsText.join('\n\n')} --- 请对本次执行结果进行 Reflection 反思,并在确定所有步骤完成时,记得在对话结尾加上 [RESULT: DONE]。 `; // 更新消息计数和记录 User 视角的 system 反馈消息 entry.messageCount++; logMessageChunk(agentId, convId, `\n[${new Date().toISOString()}] [SYSTEM FEEDBACK]\n${prompt}\n\n`); appendHistory(agentId, convId, { role: 'user', content: prompt, messageId: `sys-${Date.now()}`, timestamp: new Date().toISOString() }); // 循环继续,下一次将会把这个反馈 prompt 发给 Innovator logMessageChunk(agentId, convId, `[${new Date().toISOString()}] [AGENT]\n`); } meta.status = 'completed'; saveMeta(agentId, convId, meta); // ── Phase 1 MVP: 自动保存 Session 摘要到记忆 ── try { saveSessionSummary(agentId, convId, meta, fullOutput); } catch (memErr) { console.error('[Memory] Session 摘要保存失败:', memErr.message); } // ── Phase 3: 自动反思 (toolCallCount>=3 或 duration>=60s 时触发) ── try { autoReflect(agentId, convId, meta, fullOutput); } catch (refErr) { console.error('[Memory] 反思失败:', refErr.message); } sendSSE('result', { conversationId: convId, agentId, messageId: msgId, output: fullOutput }); } catch (err) { console.error('[AgentChat] Error:', err.message); logMessageChunk(agentId, convId, `\n\n[ERROR]\n${err.message}\n\n`); meta.status = 'failed'; meta.error = err.message; saveMeta(agentId, convId, meta); // 清理断开的 session const key = `${agentId}:${convId}`; sessions.delete(key); sendSSE('error', { message: err.message }); } finally { res.end(); } }); module.exports = router;