/** * 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 multer = require('multer'); 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(); // Agent 目录由 getAgentDir() 统一路由 (system → system/agents/, company → data/companies/{id}/agents/) function getAgentDir(id) { if (id && id.includes(':')) { const [compId, subId] = id.split(':'); return path.resolve(__dirname, `../../data/companies/${compId}/agents/${subId}`); } return path.resolve(__dirname, `../../system/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(path.resolve(__dirname, '../../system/agents')); 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 0: 公司 workspace 路径注入 ── // 检测是否为公司 Agent (路径格式: data//agents/) const agentDirNorm = agentDir.replace(/\\/g, '/'); const companyMatch = agentDirNorm.match(/data\/companies\/([^/]+)\/agents\/([^/]+)$/); if (companyMatch && companyMatch[1] !== 'agents') { const companyId = companyMatch[1]; const roleName = companyMatch[2]; const companyWorkspace = `data/companies/${companyId}/workspace`; parts.push(`# 🏢 公司环境上下文 你隶属于公司 **${companyId}**,你的角色是 **${roleName}**。 ## ⚠️ 关键路径规范(必须遵守!) - **公司共享工作区**: \`${companyWorkspace}/\` - **你写入的所有文件(战略蓝图、Pipeline JSON、分析报告等)都必须放在 \`${companyWorkspace}/\` 目录下** - 🚫 **绝对禁止**写入全局的 \`workspace/\` 目录!那是旧版遗留路径! - 🚫 **绝对禁止**写入 \`data/workspace/\` 目录! - ✅ 正确路径示例: \`${companyWorkspace}/strategy.md\`、\`${companyWorkspace}/pipeline_design.json\` - 你的同事(CEO/CTO/COO/HR 等)共享同一个 \`${companyWorkspace}/\` 目录,用它来交换文件和数据。 - 你自己的 Agent 目录: \`data/${companyId}/agents/${roleName}/\`(存放记忆、配置等私人文件)`); } // ── Layer 1: 通用基类 ── const commonFiles = ['methodology.md', '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()); } // ── Phase 1 / Micro-Verify: 当前局部任务的试错经验池 `_lessons.md` ── // 当 Agent 遇到棘手的坑或执行报错时,要求它总结并写入 _lessons.md。 // 通过在此处注入,它下次 retry 时便能带着前车之鉴,避免死循环。 const localLessons = loadFile(agentDir, 'workspace/_lessons.md'); if (localLessons.trim()) { parts.push('---\n\n## ⚠️ 本地试错防坑经验 (_lessons.md)\n不要重复踩下面的坑,仔细阅读并绕开:\n\n' + localLessons.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 { const historyPath = path.join(getAgentDir(agentId), 'conversations', convId, 'history.json'); const logPath = path.join(getAgentDir(agentId), 'conversations', convId, 'messages.log'); let history = []; if (fs.existsSync(historyPath)) { history = JSON.parse(fs.readFileSync(historyPath, 'utf-8')); } // 如果 messages.log 存在且比 history.json 更丰富, 从 log 里解析完整消息 if (fs.existsSync(logPath)) { const logContent = fs.readFileSync(logPath, 'utf-8'); const parsed = parseMessagesLog(logContent); // 只有当 log 解析出的消息更丰富时才使用 (例如包含更多内容) if (parsed.length > 0) { const historyTotalLen = history.reduce((sum, m) => sum + (m.content || '').length, 0); const logTotalLen = parsed.reduce((sum, m) => sum + (m.content || '').length, 0); if (logTotalLen > historyTotalLen * 1.5) { return parsed; } } } return history; } catch { return []; } } /** * 解析 messages.log 为消息数组 * 格式: [ISO_TIMESTAMP] [USER|AGENT]\n content...\n\n */ function parseMessagesLog(logContent) { const messages = []; // 按消息头分割: [timestamp] [USER] 或 [timestamp] [AGENT] const pattern = /\[(\d{4}-\d{2}-\d{2}T[\d:.]+Z)\]\s+\[(USER|AGENT)\]\n/g; const matches = []; let match; while ((match = pattern.exec(logContent)) !== null) { matches.push({ index: match.index, timestamp: match[1], role: match[2], headerLen: match[0].length }); } for (let i = 0; i < matches.length; i++) { const start = matches[i].index + matches[i].headerLen; const end = i + 1 < matches.length ? matches[i + 1].index : logContent.length; const content = logContent.substring(start, end).trim(); messages.push({ role: matches[i].role === 'USER' ? 'user' : 'assistant', content, timestamp: matches[i].timestamp, }); } return messages; } 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): 原生工具已全部禁用`); } // effort 级别控制 (low=禁用thinking | medium | high | max) // 优先从 config.json 的 cli.effort 读取,其次环境变量 CLAUDE_EFFORT if (agentConfig?.cli?.effort) { cliOptions.effort = agentConfig.cli.effort; console.log(`[AgentChat] 💡 ${identity.name} effort=${cliOptions.effort}`); } 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 + 历史摘要 + 用户消息 */ // ── 文件上传相关工具 ── const TEXT_EXTENSIONS = new Set([ '.md', '.txt', '.log', '.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.go', '.rs', '.sql', '.sh', '.css', '.html', '.vue', '.svelte', '.json', '.yaml', '.yml', '.toml', '.xml', '.csv', '.conf', '.ini', '.env', '.dockerfile', '.makefile', '.rb', '.php', '.c', '.cpp', '.h', '.hpp', '.swift', '.kt', '.scala', '.r', ]); const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico']); const MAX_INJECT_SIZE = 50 * 1024; // 50KB per file const MAX_TOTAL_INJECT = 500 * 1024; // 500KB total function isTextFile(ext) { return TEXT_EXTENSIONS.has(ext.toLowerCase()); } function isImageFile(ext) { return IMAGE_EXTENSIONS.has(ext.toLowerCase()); } function formatFileSize(bytes) { if (bytes < 1024) return bytes + 'B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB'; return (bytes / (1024 * 1024)).toFixed(1) + 'MB'; } function getLanguageTag(ext) { const map = { '.js': 'javascript', '.ts': 'typescript', '.py': 'python', '.java': 'java', '.go': 'go', '.rs': 'rust', '.sql': 'sql', '.sh': 'bash', '.css': 'css', '.html': 'html', '.vue': 'vue', '.json': 'json', '.yaml': 'yaml', '.yml': 'yaml', '.xml': 'xml', '.csv': 'csv', '.md': 'markdown', '.rb': 'ruby', '.php': 'php', '.c': 'c', '.cpp': 'cpp', }; return map[ext.toLowerCase()] || ''; } function sanitizeFilename(name) { return name.replace(/[\/\\:*?"<>|]/g, '_').replace(/\.\./g, '_').slice(0, 200); } function getUploadDir(agentDir, convId) { return path.join(agentDir, 'uploads', convId); } /** * 扫描 uploads/{convId}/ 目录,构建附件上下文块注入到用户消息前。 */ function buildUploadContext(agentDir, convId) { const uploadDir = getUploadDir(agentDir, convId); if (!fs.existsSync(uploadDir)) return ''; const files = fs.readdirSync(uploadDir).filter(f => !f.startsWith('.')); if (files.length === 0) return ''; let contextBlock = '\n\n---\n📎 **附件上下文** (以下文件已上传到当前会话):\n\n'; let totalInjected = 0; for (const file of files) { const ext = path.extname(file).toLowerCase(); const filePath = path.join(uploadDir, file); let stat; try { stat = fs.statSync(filePath); } catch { continue; } if (isTextFile(ext) && stat.size <= MAX_INJECT_SIZE && totalInjected + stat.size <= MAX_TOTAL_INJECT) { try { const content = fs.readFileSync(filePath, 'utf-8'); const lang = getLanguageTag(ext); contextBlock += `### 📄 ${file} (${formatFileSize(stat.size)})\n\`\`\`${lang}\n${content}\n\`\`\`\n\n`; totalInjected += stat.size; } catch { contextBlock += `### 📄 ${file} (${formatFileSize(stat.size)}) - 读取失败\n\n`; } } else if (isTextFile(ext) && stat.size > MAX_INJECT_SIZE) { // 大文本文件:只注入前 50KB try { const fd = fs.openSync(filePath, 'r'); const buf = Buffer.alloc(MAX_INJECT_SIZE); fs.readSync(fd, buf, 0, MAX_INJECT_SIZE, 0); fs.closeSync(fd); const lang = getLanguageTag(ext); contextBlock += `### 📄 ${file} (${formatFileSize(stat.size)}, 仅显示前50KB)\n\`\`\`${lang}\n${buf.toString('utf-8')}\n...(文件过大已截断,完整路径: ${filePath})\n\`\`\`\n\n`; totalInjected += MAX_INJECT_SIZE; } catch { contextBlock += `### 📄 ${file} (${formatFileSize(stat.size)}) - 路径: ${filePath}\n\n`; } } else if (isImageFile(ext)) { contextBlock += `### 🖼️ ${file} (${formatFileSize(stat.size)})\n(图片文件,路径: ${filePath})\n\n`; } else { contextBlock += `### 📦 ${file} (${formatFileSize(stat.size)})\n(二进制文件,路径: ${filePath})\n\n`; } if (totalInjected >= MAX_TOTAL_INJECT) { contextBlock += '> ⚠️ 已达到注入上限(500KB),剩余文件仅列出路径。\n\n'; break; } } contextBlock += '---\n'; return contextBlock; } function buildPrompt(agentDir, agentId, convId, userMessage, isNewSession) { // 注入上传文件上下文 const uploadContext = buildUploadContext(agentDir, convId); const enrichedMessage = uploadContext ? uploadContext + '\n' + userMessage : userMessage; if (!isNewSession) { // 场景 B: ACP 进程还在,多轮上下文自动保持 return enrichedMessage; } 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${enrichedMessage}`; } // 场景 C: 恢复上下文 — 注入历史摘要 const recentHistory = history.slice(-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${enrichedMessage}`; } // ═══════════════════════════════════════ // 路由 // ═══════════════════════════════════════ router.get('/quota', (req, res) => { try { const DATA_ROOT = path.resolve(__dirname, '../../data'); const records = []; // 读取所有 billing.log const readBilling = (dir) => { const p = path.join(dir, 'billing.log'); if (fs.existsSync(p)) { fs.readFileSync(p, 'utf-8').split('\n').forEach(line => { line = line.trim(); if (line.startsWith('{')) { try { records.push(JSON.parse(line)); } catch(e) {} } else if (line) { // 旧的纯文本兼容: 2026-03-28T18:00:00.000Z | Agent: master | Conv: 123 | Cost: $0.123 | Duration: 123ms try { const parts = line.split('|').map(p => p.trim()); const ts = parts[0]; const ai = parts[1].replace('Agent:', '').trim(); const ci = parts[2].replace('Conv:', '').trim(); const cost = parseFloat(parts[3].replace('Cost: $', '').trim()); const dur = parseInt(parts[4].replace('Duration:', '').replace('ms', '').trim()); records.push({ timestamp: ts, agentId: ai, convId: ci, costUsd: cost || 0, durationMs: dur || 0, usage: { input: 0, output: 0 } }); } catch(e) {} } }); } }; readBilling(path.join(DATA_ROOT, 'system/agents/master/logs')); const compDir = path.join(DATA_ROOT, 'companies'); if (fs.existsSync(compDir)) { const comps = fs.readdirSync(compDir, { withFileTypes: true }); for (const c of comps) { if (c.isDirectory() && !c.name.startsWith('.')) { readBilling(path.join(compDir, c.name, 'logs')); } } } res.json(records); } catch (err) { res.status(500).json({ error: err.message }); } }); 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 let lastMessage = '' 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 let latestConvName = null 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 latestConvName = c.name } } catch {} } } if (latest > 0) { lastActiveAt = new Date(latest).toISOString() if (latestConvName) { const historyPath = path.join(convDir, latestConvName, 'history.json') if (fs.existsSync(historyPath)) { try { const history = JSON.parse(fs.readFileSync(historyPath, 'utf-8')) if (history.length > 0) { const msg = history[history.length - 1] if (Array.isArray(msg.content)) { lastMessage = msg.content.map(c => c.text || '').join(' ') } else { lastMessage = msg.content || '' } if (msg.timestamp) lastActiveAt = msg.timestamp } } catch {} } } } } catch {} } // 截断一行 if (lastMessage) { lastMessage = lastMessage.replace(/[\r\n]+/g, ' ').trim() if (lastMessage.length > 50) lastMessage = lastMessage.substring(0, 50) + '...' } // 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, lastMessage, companyId // 新增用于前端分组过滤 }) } } // 1. 扫描 System Agents (system/agents/) scanAgentDir(path.join(DATA_ROOT, 'system', 'agents'), 'system') // 2. 扫描 Company Agents (data/companies/{companyId}/agents/) const companiesDir = path.join(DATA_ROOT, 'companies') if (fs.existsSync(companiesDir)) { const companies = fs.readdirSync(companiesDir, { withFileTypes: true }) for (const c of companies) { if (!c.isDirectory() || c.name.startsWith('.')) continue scanAgentDir(path.join(companiesDir, c.name, 'agents'), c.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); // Read cron settings let cron = []; try { const cronPath = path.join(agentDir, 'cron.json'); if (fs.existsSync(cronPath)) { cron = JSON.parse(fs.readFileSync(cronPath, 'utf8')); } } catch (e) { console.error('Failed to parse cron.json:', e); } res.json({ id: config?.id || req.params.agentId, ...identity, type: config?.type, status: config?.status, heartbeat: config?.heartbeat || { enabled: false, intervalMinutes: 15 }, cron: cron, bootstrapFiles: ['agent.md','soul.md','identity.md','user.md','tools.md','heartbeat.md', 'cron.json'].filter(f => loadFile(agentDir, f).trim().length > 0), }); }); router.put('/:agentId/heartbeat', (req, res) => { try { const agentDir = getAgentDir(req.params.agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); const configPath = path.join(agentDir, 'config.json'); let config = {}; if (fs.existsSync(configPath)) { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } config.heartbeat = { enabled: req.body.enabled || false, intervalMinutes: req.body.intervalMinutes || 15 }; fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8'); // Update heartbeat service if running const heartbeatService = require('../services/heartbeat-service'); // Using namespacedId logic from UI (`system:master` is skipped, only `company:role` or pure system agent like `master`) const agentId = req.params.agentId; if (config.heartbeat.enabled) { heartbeatService._setupAgentHeartbeat(agentId); } else if (heartbeatService.timers.has(agentId)) { clearInterval(heartbeatService.timers.get(agentId)); heartbeatService.timers.delete(agentId); } res.json({ success: true, heartbeat: config.heartbeat }); } catch (err) { res.status(500).json({ error: err.message }); } }); router.put('/:agentId/cron', (req, res) => { try { const agentDir = getAgentDir(req.params.agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); const cronPath = path.join(agentDir, 'cron.json'); const cronList = Array.isArray(req.body.cron) ? req.body.cron : []; fs.writeFileSync(cronPath, JSON.stringify(cronList, null, 2), 'utf8'); // Check if the heartbeat service picks up the cron changes automatically. // Generally cron jobs execute within the heartbeat mechanism. res.json({ success: true, cron: cronList }); } catch (err) { res.status(500).json({ error: err.message }); } }); /** 列出对话 */ 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 agentDir = getAgentDir(agentId); // 1. 删除对话目录 const convDir = path.join(agentDir, 'conversations', convId); if (fs.existsSync(convDir)) fs.rmSync(convDir, { recursive: true, force: true }); // 2. 删除关联的日志目录/文件 const logsDir = path.join(agentDir, 'logs'); if (fs.existsSync(logsDir)) { for (const f of fs.readdirSync(logsDir)) { if (f.includes(convId)) { const fp = path.join(logsDir, f); fs.rmSync(fp, { recursive: true, force: true }); } } } // 3. 删除上传文件 const uploadDir = getUploadDir(agentDir, convId); if (fs.existsSync(uploadDir)) fs.rmSync(uploadDir, { recursive: true, force: true }); // 4. 断开会话 const key = `${agentId}:${convId}`; const entry = sessions.get(key); if (entry && entry.protocol) entry.protocol.disconnect(); sessions.delete(key); console.log(`[AgentChat] 🗑️ 已删除对话 ${agentId}/${convId} (含uploads)`); 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 }); } }); // PUT /api/agent/:agentId/file/* — 保存文件内容 router.put('/:agentId/file/*', (req, res) => { const filePath = req.params[0]; if (!filePath) return res.status(400).json({ error: 'file path required' }); const agentDir = getAgentDir(req.params.agentId); const fullPath = path.join(agentDir, filePath); if (!fullPath.startsWith(agentDir)) { return res.status(403).json({ error: 'Access denied' }); } try { // 确保目录存在 const dir = path.dirname(fullPath); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(fullPath, req.body.content || '', 'utf-8'); console.log(`[AgentChat] 💾 已保存文件 ${req.params.agentId}/${filePath}`); res.json({ success: true, path: filePath }); } catch (err) { res.status(500).json({ error: err.message }); } }); // DELETE /api/agent/:agentId/file/* — 删除文件或文件夹 router.delete('/:agentId/file/*', (req, res) => { const filePath = req.params[0]; if (!filePath) return res.status(400).json({ error: 'file path required' }); const agentDir = getAgentDir(req.params.agentId); const fullPath = path.join(agentDir, filePath); if (!fullPath.startsWith(agentDir)) { return res.status(403).json({ error: 'Access denied' }); } if (!fs.existsSync(fullPath)) return res.status(404).json({ error: 'Not found' }); try { fs.rmSync(fullPath, { recursive: true, force: true }); console.log(`[AgentChat] 🗑️ 已删除 ${req.params.agentId}/${filePath}`); res.json({ success: true, path: filePath }); } 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(path.resolve(__dirname, '../../system/agents')); 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.resolve(__dirname, '../../system/agents/.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, parentTaskId: explicitParent } = 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, '|'); // ── 自动推断 parentTaskId ── // 如果调用者没有显式传 parentTaskId,则尝试从 .tasks/ 目录下 // 查找 from agent 正在执行的(status=dispatched 且 to=from)的 task let parentTaskId = explicitParent || null; let sourceConvId = null; if (!parentTaskId) { try { const taskFiles = fs.readdirSync(TASK_META_DIR).filter(f => f.endsWith('.json')); // 找到 "to === from" 且尚未完成的任务 → 这就是 from agent 正在执行的父任务 let candidates = []; for (const tf of taskFiles) { try { const tm = JSON.parse(fs.readFileSync(path.join(TASK_META_DIR, tf), 'utf-8')); if (tm.to === from && tm.status === 'dispatched') { candidates.push(tm); } } catch(e) {} } // 如果有多个,取最近创建的 if (candidates.length > 0) { candidates.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); parentTaskId = candidates[0].taskId; sourceConvId = candidates[0].executionConvId || null; console.log(`[TaskDispatch] 🔗 Auto-detected parentTaskId: ${parentTaskId} (${from} is executing it)`); } } catch(e) { /* ignore */ } } // 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}` : ''}${parentTaskId ? `, parent: ${parentTaskId}` : ''})`); // 4. 持久化 Task 元数据 (用于 task/report 回调) const executionConvId = autoExecute ? `task-${taskId}` : null; saveTaskMeta(taskId, { taskId, from, to, task: safeTask, priority, goalId: goalId || null, nodeId: nodeId || null, parentTaskId: parentTaskId || null, sourceConvId: sourceConvId || 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/:agentId/task/transition — 强制流转与记忆断舍离 (Context Flush) // ═══════════════════════════════════════ router.post('/:agentId/task/transition', (req, res) => { try { const { phase, artifact } = req.body; const agentId = req.params.agentId; const agentDir = getAgentDir(agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); console.log(`[TaskTransition] 🔄 ${agentId} 正在跨越阶段至: ${phase}`); // 1. 查找当前的活动会话 ID let activeConvId = null; let sessionKey = null; for (const [k, v] of sessions.entries()) { if (v.agentId === agentId) { activeConvId = v.convId; sessionKey = k; break; } } // 2. Phase 3 核心: 事前阻断阀 (Human-in-the-loop) // 拦截 P0 任务由 plan -> execute 的跳转,强制挂起并申请 Master 审批 if (phase === 'execute' && activeConvId && activeConvId.startsWith('task-')) { const taskId = activeConvId.substring(5); const taskMeta = loadTaskMeta(taskId); if (taskMeta && taskMeta.priority === 'P0') { console.log(`[TaskTransition] 🛑 拦截 P0 任务进入 Execute,挂起等待人类审批 (taskId: ${taskId})`); // 更新 OKR 节点状态为 pending_approval if (taskMeta.goalId && taskMeta.nodeId) { try { goalsService.updateNode(taskMeta.goalId, taskMeta.nodeId, { status: 'pending_approval' }); } catch (err) {} } // 通知 Master 审批 setImmediate(() => { const masterMsg = `[⚠️ 事前审批请求]\n你的下属 ${agentId} 已经完成了 P0 级任务 ${taskId} 的规划阶段。\n现在它正准备加载大威力执行权限 (Execute) 落地方案。\n\n请你阅读其产生的 ${artifact || '_plan.md'},并在确认无误后通过指令手动放行它。`; const http = require('http'); const postData = JSON.stringify({ message: masterMsg, conversationId: `approval-${taskId}` }); const options = { hostname: 'localhost', port: 3000, path: `/api/agent/master/chat`, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) }, }; const chatReq = http.request(options, () => {}); chatReq.on('error', () => {}); chatReq.write(postData); chatReq.end(); }); // 掐断当前进程使其休眠,等待重新唤醒 const v = sessions.get(sessionKey); if (v && v.protocol) v.protocol.disconnect(); sessions.delete(sessionKey); return res.json({ success: true, message: '已拦截。该任务为 P0 级别,必须等待 Master 审批放行后才能进入执行环节!' }); } } // 若未被阻断,更新 OKR 为进行中子状态 (如 researching, executing等) if (activeConvId && activeConvId.startsWith('task-')) { const taskId = activeConvId.substring(5); const taskMeta = loadTaskMeta(taskId); if (taskMeta && taskMeta.goalId && taskMeta.nodeId) { try { const subStatus = phase === 'research' ? 'researching' : phase === 'plan' ? 'planning' : phase === 'execute' ? 'executing' : phase === 'verify' ? 'verifying' : 'in_progress'; goalsService.updateNode(taskMeta.goalId, taskMeta.nodeId, { status: subStatus }); } catch (err) {} } } // 3. Hot Swap 动态换脑: 把 main_skill 替换为 meta-* 技能 const sourceSkill = path.resolve(__dirname, `../../system/skills/meta-${phase}/SKILL.md`); const targetSkill = path.join(agentDir, '.claude', 'skills', 'main_skill', 'SKILL.md'); if (fs.existsSync(sourceSkill)) { if (!fs.existsSync(path.dirname(targetSkill))) fs.mkdirSync(path.dirname(targetSkill), { recursive: true }); fs.copyFileSync(sourceSkill, targetSkill); console.log(`[TaskTransition] 🧠 ${agentId} 动态技能树已切换为 meta-${phase}`); } // 4. Context Flush 记忆断舍离: 掐断当前运行中的 ACP 进程,清空历史消息 if (sessionKey) { const v = sessions.get(sessionKey); if (v && v.protocol) v.protocol.disconnect(); sessions.delete(sessionKey); } if (activeConvId) { const dir = getConvDir(agentId, activeConvId); // 清空长周期的幻觉来源 fs.writeFileSync(path.join(dir, 'history.json'), '[]', 'utf-8'); logMessageChunk(agentId, activeConvId, `\n\n[SYSTEM] 🛑 Context Flush 触发!记忆已断开,阶段切换至 ${phase}\n\n`); // 3. 构建起步上下文: 告诉 Agent 它的前序成就 let artifactContent = ''; if (artifact) { // artifact 可能带有 'workspace/' 前缀,尝试解析 const possiblePaths = [ path.join(agentDir, artifact), path.join(agentDir, path.basename(artifact)), path.join(agentDir, 'workspace', path.basename(artifact)) ]; for (const p of possiblePaths) { if (fs.existsSync(p)) { artifactContent = fs.readFileSync(p, 'utf-8'); break; } } } const flushMsg = `【系统指令:强制状态切换】 你的生命周期已成功切入新阶段:**${phase.toUpperCase()}**。 你之前的对话记忆已被完全抹除防范幻觉,你的系统技能已获得 ${phase} 的授权限制。 ${artifactContent ? `这是上一阶段你或你的前置节点留下的交付物(Artifact),你必须严格参照此文件行动,不再另行发散:\n\n\`\`\`\n${artifactContent}\n\`\`\`` : ''} 请开始你的新阶段工作!`; // 写入一条用户指令开启新的清爽对话 appendHistory(agentId, activeConvId, { role: 'user', content: flushMsg, messageId: `msg-${Date.now()}-flush`, timestamp: new Date().toISOString() }); // 4. 发送唤醒指令让他立刻基于新脑子行动 setImmediate(() => { const http = require('http'); const postData = JSON.stringify({ message: "请确认你已收到系统的新阶段挂载指令,并立即执行你当前阶段本分的任务!", conversationId: activeConvId }); 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, () => {}); chatReq.on('error', () => {}); chatReq.write(postData); chatReq.end(); }); } res.json({ success: true, message: `已成功进行 Context Flush 并切换至 ${phase} 阶段` }); } catch (err) { console.error('[TaskTransition] 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/human-request — Human-in-the-loop: API Key 申请等 // ═══════════════════════════════════════ router.post('/human-request', (req, res) => { try { const { from, type, key, reason } = req.body; if (!from || !type || !key) { return res.status(400).json({ error: 'from, type, key are required' }); } console.log(`[HumanRequest] 🙋 ${from} 遇到需要人类介入的请求: ${type} - ${key} (${reason})`); // 1. 同步写入根目录下的 .env 以备人类填入 const envPath = path.resolve(__dirname, '../../.env'); let envContent = ''; try { envContent = fs.readFileSync(envPath, 'utf-8'); } catch {} // 如果还没包含这个 key,追加进去 if (!envContent.includes(key + '=')) { const appendStr = `\n# [${new Date().toISOString().substring(0, 10)}] Requested by ${from}: ${reason}\n${key}=\n`; fs.appendFileSync(envPath, appendStr, 'utf-8'); console.log(`[HumanRequest] 🔑 已将 ${key} 追加到 .env 等待配置`); } // 2. 写入 master 的 To-Do List const masterWorkspace = path.resolve(__dirname, '../../system/agents/master/workspace'); if (!fs.existsSync(masterWorkspace)) fs.mkdirSync(masterWorkspace, { recursive: true }); const todoPath = path.join(masterWorkspace, 'human_todo.md'); const todoStr = `- [ ] **${type.toUpperCase()}**: Agent \`${from}\` 需要 \`${key}\` (${reason}) — ` + `_请在根目录 \`.env\` 中配置后打钩_\n`; fs.appendFileSync(todoPath, todoStr, 'utf-8'); // 3. 通知 Master (非阻塞) setImmediate(() => { const masterMsg = [ `[🙋 HUMAN INTERVENTION REQUIRED]`, `你的下属 ${from} 遇到了系统级的阻碍。`, `请求类型: ${type}`, `资源名: ${key}`, `原因: ${reason}`, ``, `我已经将其记录到了 workspace/human_todo.md 和根目录的 .env 文件中。`, `请敦促人类 Master 提供该 Key,或者你评估是否可以使用备用方案。` ].join('\n'); const http = require('http'); const postData = JSON.stringify({ message: masterMsg, conversationId: `human-req-${Date.now().toString(36)}` }); const options = { hostname: 'localhost', port: 3000, path: `/api/agent/master/chat`, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) }, }; const chatReq = http.request(options, (chatRes) => { chatRes.on('data', () => {}); }); chatReq.on('error', () => {}); chatReq.write(postData); chatReq.end(); }); res.json({ success: true, message: `已将请求上报给 Master 并记录至 Human To-Do List` }); } catch (err) { console.error('[HumanRequest] Error:', err.message); res.status(500).json({ error: err.message }); } }); // ═══════════════════════════════════════ // 文件上传 API // ═══════════════════════════════════════ const uploadStorage = multer.diskStorage({ destination: (req, file, cb) => { const agentDir = getAgentDir(req.params.agentId); const convId = req.body.conversationId || `conv-${Date.now().toString(36)}`; req._uploadConvId = convId; // 传递给后续逻辑 const uploadDir = getUploadDir(agentDir, convId); fs.mkdirSync(uploadDir, { recursive: true }); cb(null, uploadDir); }, filename: (req, file, cb) => { const safe = sanitizeFilename(file.originalname); cb(null, safe); }, }); const upload = multer({ storage: uploadStorage, limits: { fileSize: 10 * 1024 * 1024, files: 20 }, // 10MB per file, 20 files max }); // POST /api/agent/:agentId/upload — 上传文件 router.post('/:agentId/upload', upload.array('files', 20), (req, res) => { try { const agentId = req.params.agentId; const agentDir = getAgentDir(agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); const convId = req._uploadConvId || req.body.conversationId; const uploaded = (req.files || []).map(f => { const ext = path.extname(f.originalname).toLowerCase(); return { name: f.filename, originalName: f.originalname, size: f.size, type: isTextFile(ext) ? 'text' : isImageFile(ext) ? 'image' : 'binary', path: `uploads/${convId}/${f.filename}`, }; }); console.log(`[Upload] 📎 ${agentId}/${convId}: ${uploaded.length} file(s) uploaded`); res.json({ conversationId: convId, uploaded }); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/agent/:agentId/uploads/:convId — 列出已上传文件 router.get('/:agentId/uploads/:convId', (req, res) => { try { const agentDir = getAgentDir(req.params.agentId); const uploadDir = getUploadDir(agentDir, req.params.convId); if (!fs.existsSync(uploadDir)) return res.json([]); const files = fs.readdirSync(uploadDir).filter(f => !f.startsWith('.')).map(f => { const fp = path.join(uploadDir, f); const stat = fs.statSync(fp); const ext = path.extname(f).toLowerCase(); return { name: f, size: stat.size, type: isTextFile(ext) ? 'text' : isImageFile(ext) ? 'image' : 'binary', uploadedAt: stat.mtime.toISOString(), }; }); res.json(files); } catch (err) { res.status(500).json({ error: err.message }); } }); // DELETE /api/agent/:agentId/uploads/:convId/:filename — 删除单个文件 router.delete('/:agentId/uploads/:convId/:filename', (req, res) => { try { const agentDir = getAgentDir(req.params.agentId); const filePath = path.join(getUploadDir(agentDir, req.params.convId), sanitizeFilename(req.params.filename)); if (fs.existsSync(filePath)) fs.unlinkSync(filePath); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/agent/:agentId/uploads/:convId/:filename — 下载/预览文件 router.get('/:agentId/uploads/:convId/:filename', (req, res) => { try { const agentDir = getAgentDir(req.params.agentId); const filePath = path.join(getUploadDir(agentDir, req.params.convId), sanitizeFilename(req.params.filename)); if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'File not found' }); res.sendFile(filePath); } catch (err) { 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) { console.log(`[SSE-DEBUG] 📤 agent_output (${update.content.text.length} chars)`); 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`; fullOutput += toolMsg; 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) { const inputLog = ` 输入: ${currentToolInput.slice(0, 500)}${currentToolInput.length > 500 ? '...' : ''}\n`; fullOutput += inputLog; logMessageChunk(agentId, convId, inputLog); sendSSE('tool_input', { input: currentToolInput }); currentToolInput = ''; } } else if (event.method === 'session/result') { if (event.params?.duration_ms) { sendSSE('session_cost', { cost: event.params.cost_usd, duration: event.params.duration_ms, usage: event.params.usage }); // Phase 5: Economic Audit - 记录账单 (Token Profiling) JSON lines try { let billingDir; if (agentId && agentId.includes(':')) { const compId = agentId.split(':')[0]; billingDir = path.resolve(__dirname, `../../data/companies/${compId}/logs`); } else { billingDir = path.resolve(__dirname, '../../system/agents/master/logs'); } if (!fs.existsSync(billingDir)) fs.mkdirSync(billingDir, { recursive: true }); const logEntry = { timestamp: new Date().toISOString(), agentId, convId, model: event.params.model || 'unknown', costUsd: event.params.cost_usd || 0, durationMs: event.params.duration_ms, usage: event.params.usage || { input: 0, output: 0 } }; fs.appendFileSync(path.join(billingDir, 'billing.log'), JSON.stringify(logEntry) + '\n', 'utf-8'); } catch (err) {} } 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)` : ''); const resultLog = ` 结果: ${truncated}\n`; fullOutput += `\n` + resultLog; logMessageChunk(agentId, convId, resultLog); sendSSE('tool_result', { result: truncated }); } } }; entry.protocol.on('session_update', onUpdate); entry.protocol.on('acp_event', onAcpEvent); // 监听 stderr / error / disconnect — 将 Claude CLI 错误实时推送到前端 const onStderr = (msg) => { console.warn(`[AgentChat] stderr from ${agentId}:`, msg.substring(0, 300)); sendSSE('acp_raw', { method: 'claude/stderr', params: { message: msg }, ts: Date.now() }); // 如果包含严重错误关键词,额外发一条 error 事件 if (/error|rate.?limit|429|403|timeout|ECONNREFUSED|failed/i.test(msg)) { sendSSE('error', { message: `⚠️ Claude CLI: ${msg.substring(0, 500)}` }); } }; const onDisconnected = ({ code, signal }) => { if (code !== 0) { sendSSE('error', { message: `Claude CLI 异常退出 (code=${code}, signal=${signal})` }); } }; entry.protocol.on('stderr', onStderr); entry.protocol.on('disconnected', onDisconnected); // 发出状态以便前端展示不卡壳 sendSSE('agent_output', { agent: agentId, message: `_[请求已发送,等待底层大模型响应...]_\\\n\\\n` }); // 发送 prompt 并等待完成 await entry.protocol.sendPrompt(prompt); // 清理监听 entry.protocol.off('session_update', onUpdate); entry.protocol.off('acp_event', onAcpEvent); entry.protocol.off('stderr', onStderr); entry.protocol.off('disconnected', onDisconnected); // 持久化当前轮次的回复 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 }); console.log(`[SSE-DEBUG] 🚨 sendSSE('error') called:`, err.message.substring(0, 200)); } finally { res.end(); } }); module.exports = router;