/**
* 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;