/** * Company Routes — 公司管理 API * * 挂载: /api/company */ const express = require('express'); const router = express.Router(); const { CompanyService } = require('../services/company-service'); const { ScheduleService } = require('../services/schedule-service'); const companyService = new CompanyService(); const scheduleService = new ScheduleService(); // ═══════════════════════════════════════ // Company CRUD // ═══════════════════════════════════════ // POST /api/company/create router.post('/create', async (req, res) => { try { const { id, name, description } = req.body; if (!id || !name) return res.status(400).json({ error: 'id 和 name 为必填项' }); // 校验 id 格式 (小写字母、数字、-) if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(id) && id.length < 2) { return res.status(400).json({ error: 'id 格式无效 (只允许小写字母、数字、-)' }); } const company = companyService.create({ id, name, description }); res.json({ success: true, company, message: '公司已创建,当前为没有员工的空壳公司。请通知 MasterBot 从 Template 库中雇佣 Agent 到你的团队中。' }); } catch (err) { res.status(400).json({ error: err.message }); } }); // GET /api/company/list router.get('/list', (req, res) => { res.json(companyService.list()); }); // GET /api/company/templates router.get('/templates', (req, res) => { res.json(companyService.listTemplates()); }); // POST /api/company/templates — 创建新模板 Agent router.post('/templates', async (req, res) => { try { const { id, name, type, description } = req.body; if (!id || !name) return res.status(400).json({ error: 'id 和 name 为必填项' }); if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) { return res.status(400).json({ error: 'id 格式无效 (只允许小写字母、数字、-)' }); } const path = require('path'); const fs = require('fs'); const TEMPLATES_DIR = path.resolve(__dirname, '../../data/system/templates'); const templateDir = path.join(TEMPLATES_DIR, id); if (fs.existsSync(templateDir)) { return res.status(409).json({ error: `模板 "${id}" 已存在` }); } // 使用 AgentScaffold 创建标准骨架 const { AgentScaffold } = await import('../../packages/agent-core/AgentScaffold.js'); const scaffold = new AgentScaffold(TEMPLATES_DIR); await scaffold.create({ id, type: type || 'skill', name: name, parentId: null, }); // 如果有 description,追加到 config.json if (description) { const configPath = path.join(templateDir, 'config.json'); const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); config.description = description; fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); } console.log(`[Template] ✅ 新模板已创建: ${id} (${type || 'skill'})`); res.status(201).json({ success: true, id, name, type: type || 'skill' }); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/company/templates/:templateId/generate — 调用 Agent Creator 生成定义文件 router.post('/templates/:templateId/generate', async (req, res) => { try { const { templateId } = req.params; const { description } = req.body; const path = require('path'); const fs = require('fs'); const config = require('../config'); const TEMPLATES_DIR = path.resolve(__dirname, '../../data/system/templates'); const templateDir = path.join(TEMPLATES_DIR, templateId); if (!fs.existsSync(templateDir)) { return res.status(404).json({ error: `模板 "${templateId}" 不存在` }); } const PORT = config.port || 3000; const prompt = `请你为模板 Agent "${templateId}" 编写完整的定义文件。 模板位于 \`../../data/system/templates/${templateId}\` 目录。 该 Agent 的描述/用途: ${description || '通用执行型 Agent'} 请使用 Write 工具为其补充完整的 agent.md, identity.md, soul.md, user.md, tools.md 定义文件。 写完后用 ls 验证目录结构。务必使用 Write 工具写入,不要使用 Edit 工具。`; // 异步调用 agent-creator fetch(`http://127.0.0.1:${PORT}/api/agent/agent-creator/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: prompt, conversationId: `tpl-gen-${templateId}-${Date.now().toString(36)}`, }), }).catch(e => console.error('[Template] Agent Creator 请求失败:', e.message)); console.log(`[Template] 🤖 已触发 Agent Creator 为 ${templateId} 生成定义`); res.json({ success: true, message: `已触发 Agent Creator 为 "${templateId}" 生成定义文件`, status: 'generating' }); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/company/templates/:templateId/train — 调用 Innovator 训练模板 router.post('/templates/:templateId/train', async (req, res) => { try { const { templateId } = req.params; const path = require('path'); const fs = require('fs'); const config = require('../config'); const TEMPLATES_DIR = path.resolve(__dirname, '../../data/system/templates'); const templateDir = path.join(TEMPLATES_DIR, templateId); if (!fs.existsSync(templateDir)) { return res.status(404).json({ error: `模板 "${templateId}" 不存在` }); } const PORT = config.port || 3000; const prompt = `请你对模板 Agent "${templateId}" 进行评估和训练。 模板位于 \`../../data/system/templates/${templateId}\` 目录。 请使用 FDE pipeline 对该模板的技能进行评测和进化训练,目标: Benchmark Score ≥ 95。 训练过程包括 Hunt(验证数据), Digest(知识消化), Evolver(技能进化) 等。 请立即开始评估和训练,训练完成后请汇报最终分数。`; // 异步调用 innovator fetch(`http://127.0.0.1:${PORT}/api/agent/innovator/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: prompt, conversationId: `tpl-train-${templateId}-${Date.now().toString(36)}`, }), }).catch(e => console.error('[Template] Innovator 请求失败:', e.message)); console.log(`[Template] 🎓 已触发 Innovator 训练 ${templateId}`); res.json({ success: true, message: `已触发 Innovator 训练 "${templateId}"`, status: 'training' }); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/company/templates/:templateId/logs — 获取模板相关的 agent-creator / innovator 执行日志 router.get('/templates/:templateId/logs', (req, res) => { try { const path = require('path'); const fs = require('fs'); const templateId = req.params.templateId; const AGENTS_DIR = path.resolve(__dirname, '../../data/system/agents'); const logs = []; // 扫描 agent-creator 和 innovator 的对话,匹配 tpl-gen-{id} 和 tpl-train-{id} const scanAgents = [ { agentId: 'agent-creator', prefix: `tpl-gen-${templateId}`, type: 'generate', label: '🤖 Agent Creator' }, { agentId: 'innovator', prefix: `tpl-train-${templateId}`, type: 'train', label: '🎓 Innovator' }, ]; for (const scan of scanAgents) { const convDir = path.join(AGENTS_DIR, scan.agentId, 'conversations'); if (!fs.existsSync(convDir)) continue; const dirs = fs.readdirSync(convDir, { withFileTypes: true }) .filter(d => d.isDirectory() && d.name.startsWith(scan.prefix)); for (const d of dirs) { const metaPath = path.join(convDir, d.name, 'meta.json'); const histPath = path.join(convDir, d.name, 'history.json'); let meta = {}, history = []; try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); } catch {} try { history = JSON.parse(fs.readFileSync(histPath, 'utf-8')); } catch {} // 提取最后一条 assistant 消息作为摘要 const lastAssistant = [...history].reverse().find(m => m.role === 'assistant'); logs.push({ conversationId: d.name, agentId: scan.agentId, type: scan.type, label: scan.label, createdAt: meta.createdAt || null, messageCount: history.length, summary: lastAssistant ? (lastAssistant.content || '').slice(0, 500) : '(执行中...)', status: lastAssistant ? 'done' : 'running', }); } } // 按时间倒序 logs.sort((a, b) => new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime()); res.json(logs); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/company/templates/:templateId/files router.get('/templates/:templateId/files', (req, res) => { try { const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data/system/templates'); const templateDir = path.join(DATA_DIR, req.params.templateId); if (!fs.existsSync(templateDir)) { return res.json({ tree: [], totalFiles: 0 }); } function scanDir(dirPath, relativePath = '') { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); const nodes = []; for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name; if (entry.name.startsWith('.') || entry.name === 'node_modules') continue; if (entry.isDirectory()) { const children = scanDir(fullPath, relPath); nodes.push({ name: entry.name, path: relPath, type: 'directory', children, fileCount: children.reduce((sum, c) => sum + (c.type === 'file' ? 1 : (c.fileCount || 0)), 0), }); } else { const stat = fs.statSync(fullPath); nodes.push({ name: entry.name, path: relPath, type: 'file', size: stat.size, modifiedAt: stat.mtime.toISOString(), }); } } return nodes.sort((a, b) => { if (a.type !== b.type) return a.type === 'directory' ? -1 : 1; return a.name.localeCompare(b.name); }); } const tree = scanDir(templateDir); const countFiles = (nodes) => 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/company/templates/:templateId/files/content router.get('/templates/:templateId/files/content', (req, res) => { try { const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data/system/templates'); const filePath = req.query.path; if (!filePath) return res.status(400).json({ error: 'path 参数为必填' }); const absPath = path.resolve(DATA_DIR, req.params.templateId, filePath); const safeBoundary = path.resolve(DATA_DIR, req.params.templateId); if (!absPath.startsWith(safeBoundary)) { return res.status(403).json({ error: '路径越界' }); } if (!fs.existsSync(absPath)) { return res.status(404).json({ error: '文件不存在' }); } const stat = fs.statSync(absPath); if (stat.size > 512 * 1024) { return res.json({ content: '(文件过大,跳过预览)', truncated: true, size: stat.size }); } const content = fs.readFileSync(absPath, 'utf-8'); res.json({ content, size: stat.size, path: filePath }); } catch (err) { res.status(500).json({ error: err.message }); } }); // PUT /api/company/templates/:templateId/files/content — 保存模板文件内容 router.put('/templates/:templateId/files/content', (req, res) => { try { const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data/system/templates'); const filePath = req.body.path; if (!filePath) return res.status(400).json({ error: 'path 参数为必填' }); const absPath = path.resolve(DATA_DIR, req.params.templateId, filePath); const safeBoundary = path.resolve(DATA_DIR, req.params.templateId); if (!absPath.startsWith(safeBoundary)) { return res.status(403).json({ error: '路径越界' }); } // 确保目录存在 const dir = path.dirname(absPath); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(absPath, req.body.content || '', 'utf-8'); console.log(`[Template] 💾 已保存模板文件 ${req.params.templateId}/${filePath}`); res.json({ success: true, path: filePath }); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/company/:companyId router.get('/:companyId', (req, res) => { const company = companyService.get(req.params.companyId); if (!company) return res.status(404).json({ error: '公司不存在' }); res.json(company); }); // PUT /api/company/:companyId router.put('/:companyId', (req, res) => { const company = companyService.update(req.params.companyId, req.body); if (!company) return res.status(404).json({ error: '公司不存在' }); res.json({ success: true, company }); }); // POST /api/company/:companyId/shutdown router.post('/:companyId/shutdown', (req, res) => { const company = companyService.shutdown(req.params.companyId); if (!company) return res.status(404).json({ error: '公司不存在' }); // 停止该公司下所有 Agent 的心跳 try { const heartbeatService = require('../services/heartbeat-service'); const agents = companyService.listAgents(req.params.companyId); for (const agent of agents) { const namespacedId = `${req.params.companyId}:${agent.id}`; if (heartbeatService.timers.has(namespacedId)) { clearInterval(heartbeatService.timers.get(namespacedId)); heartbeatService.timers.delete(namespacedId); console.log(`[Company] 🛑 已停止 ${namespacedId} 的心跳`); } } } catch (err) { console.warn(`[Company] ⚠️ 停止心跳时出错: ${err.message}`); } res.json({ success: true, company }); }); // POST /api/company/:companyId/start — 启动公司 router.post('/:companyId/start', (req, res) => { try { const company = companyService.get(req.params.companyId); if (!company) return res.status(404).json({ error: '公司不存在' }); // 更新状态 const updated = companyService.update(req.params.companyId, { status: 'running', heartbeat: { enabled: true }, }); // 激活该公司下所有 Agent 的心跳 try { const heartbeatService = require('../services/heartbeat-service'); const agents = companyService.listAgents(req.params.companyId); for (const agent of agents) { const namespacedId = `${req.params.companyId}:${agent.id}`; heartbeatService._setupAgentHeartbeat(namespacedId); } console.log(`[Company] ✅ ${req.params.companyId} 已启动,${agents.length} 个 Agent 心跳已激活`); } catch (err) { console.warn(`[Company] ⚠️ 激活心跳时出错: ${err.message}`); } res.json({ success: true, company: updated }); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/company/:companyId/status — 获取公司运行状态(含 Agent 进程信息) router.get('/:companyId/status', (req, res) => { try { const company = companyService.get(req.params.companyId); if (!company) return res.status(404).json({ error: '公司不存在' }); const agents = companyService.listAgents(req.params.companyId); const heartbeatService = require('../services/heartbeat-service'); const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data'); // 计算运行时长 const uptimeMs = company.status === 'running' && company.startedAt ? Date.now() - new Date(company.startedAt).getTime() : 0; const agentStatuses = agents.map(agent => { const namespacedId = `${req.params.companyId}:${agent.id}`; const hasHeartbeat = heartbeatService.timers.has(namespacedId); // 读取每个 Agent 自己的心跳配置 let heartbeatConfig = { enabled: false, intervalMinutes: 15 }; try { const configPath = path.join(DATA_DIR, req.params.companyId, 'agents', agent.id, 'config.json'); if (fs.existsSync(configPath)) { const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8')); if (cfg.heartbeat) { heartbeatConfig = cfg.heartbeat; } } } catch {} return { id: agent.id, name: agent.name, type: agent.type, status: agent.status, heartbeatActive: hasHeartbeat, heartbeatEnabled: heartbeatConfig.enabled || false, heartbeatInterval: heartbeatConfig.intervalMinutes || 15, }; }); res.json({ id: company.id, name: company.name, description: company.description, status: company.status, createdAt: company.createdAt, updatedAt: company.updatedAt, startedAt: company.startedAt || null, uptimeMs, heartbeat: company.heartbeat, agents: agentStatuses, agentCount: agents.length, activeAgentCount: agentStatuses.filter(a => a.heartbeatActive).length, }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ═══════════════════════════════════════ // Per-Agent 心跳控制 // ═══════════════════════════════════════ // PUT /api/company/:companyId/agents/:agentId/heartbeat — 更新 Agent 心跳配置 router.put('/:companyId/agents/:agentId/heartbeat', (req, res) => { try { const { intervalMinutes, enabled } = req.body; const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data'); const configPath = path.join(DATA_DIR, req.params.companyId, 'agents', req.params.agentId, 'config.json'); if (!fs.existsSync(configPath)) { return res.status(404).json({ error: `Agent ${req.params.agentId} 不存在` }); } const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); if (!config.heartbeat) config.heartbeat = { intervalMinutes: 15, enabled: false }; // 保证有合理的默认值 if (config.heartbeat.intervalMinutes === undefined) config.heartbeat.intervalMinutes = 15; if (config.heartbeat.enabled === undefined) config.heartbeat.enabled = false; if (intervalMinutes !== undefined) config.heartbeat.intervalMinutes = Number(intervalMinutes); if (enabled !== undefined) config.heartbeat.enabled = Boolean(enabled); fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); // 如果正在运行,重新挂载心跳 const heartbeatService = require('../services/heartbeat-service'); const namespacedId = `${req.params.companyId}:${req.params.agentId}`; if (config.heartbeat.enabled) { heartbeatService._setupAgentHeartbeat(namespacedId); } else if (heartbeatService.timers.has(namespacedId)) { clearInterval(heartbeatService.timers.get(namespacedId)); heartbeatService.timers.delete(namespacedId); } console.log(`[Company] ⚙️ ${namespacedId} 心跳配置更新: interval=${config.heartbeat.intervalMinutes}min, enabled=${config.heartbeat.enabled}`); res.json({ success: true, heartbeat: config.heartbeat }); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/company/:companyId/agents/:agentId/heartbeat/start — 启动单个 Agent 心跳 router.post('/:companyId/agents/:agentId/heartbeat/start', (req, res) => { try { const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data'); const configPath = path.join(DATA_DIR, req.params.companyId, 'agents', req.params.agentId, 'config.json'); if (!fs.existsSync(configPath)) { return res.status(404).json({ error: `Agent ${req.params.agentId} 不存在` }); } // 更新 config 中的 enabled const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); if (!config.heartbeat) config.heartbeat = { intervalMinutes: 15 }; config.heartbeat.enabled = true; fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); // 激活心跳 const heartbeatService = require('../services/heartbeat-service'); const namespacedId = `${req.params.companyId}:${req.params.agentId}`; heartbeatService._setupAgentHeartbeat(namespacedId); console.log(`[Company] 💓 ${namespacedId} 心跳已启动`); res.json({ success: true, message: `${namespacedId} 心跳已启动` }); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/company/:companyId/agents/:agentId/heartbeat/stop — 停止单个 Agent 心跳 router.post('/:companyId/agents/:agentId/heartbeat/stop', (req, res) => { try { const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data'); const configPath = path.join(DATA_DIR, req.params.companyId, 'agents', req.params.agentId, 'config.json'); if (!fs.existsSync(configPath)) { return res.status(404).json({ error: `Agent ${req.params.agentId} 不存在` }); } // 更新 config 中的 enabled const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); if (!config.heartbeat) config.heartbeat = {}; config.heartbeat.enabled = false; fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); // 停止心跳 const heartbeatService = require('../services/heartbeat-service'); const namespacedId = `${req.params.companyId}:${req.params.agentId}`; if (heartbeatService.timers.has(namespacedId)) { clearInterval(heartbeatService.timers.get(namespacedId)); heartbeatService.timers.delete(namespacedId); } console.log(`[Company] 🛑 ${namespacedId} 心跳已停止`); res.json({ success: true, message: `${namespacedId} 心跳已停止` }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ═══════════════════════════════════════ // Agent 技能查询 // ═══════════════════════════════════════ // GET /api/company/:companyId/agents/:agentId/skills — 获取 Agent 技能列表 router.get('/:companyId/agents/:agentId/skills', (req, res) => { try { const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data'); const skillsDir = path.join(DATA_DIR, req.params.companyId, 'agents', req.params.agentId, 'skills'); if (!fs.existsSync(skillsDir)) { return res.json([]); } const skills = fs.readdirSync(skillsDir, { withFileTypes: true }) .filter(d => d.isDirectory()) .map(d => d.name); res.json(skills); } catch (err) { res.status(500).json({ error: err.message }); } }); // ═══════════════════════════════════════ // Agent 招聘 // ═══════════════════════════════════════ // POST /api/company/:companyId/hire router.post('/:companyId/hire', async (req, res) => { try { const { role, type, name, description, customAgent, customSoul, customUser } = req.body; if (!role) return res.status(400).json({ error: 'role 为必填项' }); const result = await companyService.hireWithTraining(req.params.companyId, role, { type, name: name || role, description, customAgent, customSoul, customUser, }); // ── 核心逻辑扩展:自动补充定义 + 搜索注入 3 个初始 Skill + Innovator 训练 ── if (result.status === 'training' || result.needsTraining) { (async () => { try { console.log(`[Hire] 🚀 自动流水线启动: 为新员工 ${role} 安排 [定义 → 技能注入 → 训练]`); const fs = require('fs'); const path = require('path'); const config = require('../config'); const skillDb = require('../services/skill-db'); const namespacedId = `${req.params.companyId}:${role}`; const agentDir = result.agentDir || path.resolve(__dirname, '../../data/companies', req.params.companyId, 'agents', role); const PORT = config.port || 3000; // 1. 发起 Agent-Creator 编写骨架定义 (异步不等待) fetch(`http://127.0.0.1:${PORT}/api/agent/agent-creator/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: `新员工 ${namespacedId} 已由脚手架创建空白目录在 \`../${req.params.companyId}/agents/${role}\`。工作需求: ${description || name || role}。\n请你按照标准模板,使用 Write 工具为该员工补充完整的定义文件(agent.md, identity.md, soul.md, user.md等)并使用工具 ls 验证。务必要使用 Write 写文件,不要使用 Edit 工具。写完后请答复。`, conversationId: `create-${req.params.companyId}-${role}-${Date.now().toString(36)}` }) }).catch(e => console.error('[Hire] Agent-Creator 请求错误:', e.message)); // 2. 去网上/Arena搜索并注入 3 个基础 Skill const skills = skillDb.searchSkills(role, { limit: 3 }); const skillsDir = path.join(agentDir, 'skills'); if (skills && skills.length > 0) { if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true }); const copyDir = (src, dest) => { fs.mkdirSync(dest, { recursive: true }); const entries = fs.readdirSync(src, { withFileTypes: true }); for (const e of entries) { if (e.isDirectory()) copyDir(path.join(src, e.name), path.join(dest, e.name)); else fs.copyFileSync(path.join(src, e.name), path.join(dest, e.name)); } }; for (const skill of skills) { if (skill.directory && fs.existsSync(skill.directory)) { const targetPath = path.join(skillsDir, skill.skillName); if (!fs.existsSync(targetPath)) { try { copyDir(skill.directory, targetPath); console.log(`[Hire] 💉 成功为其注入基础技能: ${skill.skillName}`); } catch (e) { console.error(`[Hire] 注入技能 ${skill.skillName} 失败:`, e.message); } } } } } else { console.log(`[Hire] ⚠️ 网上/Arena 未找到该角色的匹配 Skill,将交由 Innovator 凭空生成`); } // 3. 延时 5 秒触发 Innovator 进行评估与训练 (等 creator 写入完毕) setTimeout(() => { fetch(`http://127.0.0.1:${PORT}/api/agent/innovator/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: `公司 ${req.params.companyId} 的员工 ${role} 骨架和基础技能已注入完毕。请你使用 FDE pipeline 对该 Agent ${namespacedId} (特别是它的技能)进行评测和进化训练,直到 Benchmark 达标 (Score ≥ 95)。训练过程包括 Hunt(验证数据), Digest, Evolver 等。请立即开始评估和训练,并保持向我汇报。`, conversationId: `train-${req.params.companyId}-${role}-${Date.now().toString(36)}` }) }).catch(e => console.error('[Hire] Innovator 请求错误:', e.message)); }, 5000); } catch (err) { console.error(`[Hire Orchestration] 自动流水线编排异常: ${err.message}`); } })(); } // ── P0 FIX: 动态心跳挂载 ── // 招聘成功后,立即为这个新 Agent 注册心跳定时器(如果它的 config.json 里 heartbeat.enabled = true) try { const heartbeatService = require('../services/heartbeat-service'); const namespacedId = `${req.params.companyId}:${role}`; heartbeatService._setupAgentHeartbeat(namespacedId); console.log(`[Hire] 💓 已为 ${namespacedId} 动态挂载心跳`); } catch (hbErr) { console.warn(`[Hire] ⚠️ 心跳挂载跳过: ${hbErr.message}`); } res.json(result); } catch (err) { res.status(400).json({ error: err.message }); } }); // POST /api/company/:companyId/fire — 辞退 Agent(招留管退之"退") router.post('/:companyId/fire', (req, res) => { try { const { role, archive } = req.body; if (!role) return res.status(400).json({ error: 'role 为必填项' }); const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data/companies'); const companyId = req.params.companyId; const agentDir = path.join(DATA_DIR, companyId, 'agents', role); if (!fs.existsSync(agentDir)) { return res.status(404).json({ error: `Agent ${role} 不存在于公司 ${companyId}` }); } // 1. 停止心跳 const heartbeatService = require('../services/heartbeat-service'); const namespacedId = `${companyId}:${role}`; if (heartbeatService.timers.has(namespacedId)) { clearInterval(heartbeatService.timers.get(namespacedId)); heartbeatService.timers.delete(namespacedId); } // 2. 归档或直接删除 if (archive !== false) { const archiveDir = path.join(DATA_DIR, companyId, '_archived_agents', role); fs.mkdirSync(path.dirname(archiveDir), { recursive: true }); fs.renameSync(agentDir, archiveDir); console.log(`[Fire] 📦 ${namespacedId} 已归档到 ${archiveDir}`); } else { fs.rmSync(agentDir, { recursive: true, force: true }); console.log(`[Fire] 🗑️ ${namespacedId} 已彻底删除`); } // 3. 从公司 agents 列表中移除 const company = companyService.get(companyId); if (company) { company.agents = (company.agents || []).filter(a => a !== role); company.updatedAt = new Date().toISOString(); companyService._saveCompany(company); } console.log(`[Fire] 🔴 ${namespacedId} 已辞退`); res.json({ success: true, message: `${namespacedId} 已辞退`, archived: archive !== false }); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/company/:companyId/deposit-template — 训练合格后沉淀为模板 router.post('/:companyId/deposit-template', (req, res) => { try { const { agentId, benchmarkScore } = req.body; if (!agentId || benchmarkScore === undefined) { return res.status(400).json({ error: 'agentId 和 benchmarkScore 为必填项' }); } const result = companyService.depositTemplate( req.params.companyId, agentId, Number(benchmarkScore) ); res.json(result); } catch (err) { res.status(400).json({ error: err.message }); } }); // GET /api/company/:companyId/agents router.get('/:companyId/agents', (req, res) => { const agents = companyService.listAgents(req.params.companyId); res.json(agents); }); // ═══════════════════════════════════════ // Company Goals / OKR (公司级 OKR) // ═══════════════════════════════════════ const { GoalsService } = require('../services/goals'); // GET /api/company/:companyId/goals router.get('/:companyId/goals', (req, res) => { const goals = new GoalsService(req.params.companyId); res.json(goals.list()); }); // POST /api/company/:companyId/goals router.post('/:companyId/goals', (req, res) => { try { const { title, description, owner } = req.body; if (!title || !owner) { return res.status(400).json({ error: 'title 和 owner 为必填项' }); } const goals = new GoalsService(req.params.companyId); const goal = goals.create({ title, description, owner }); res.status(201).json(goal); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/company/:companyId/goals/:goalId router.get('/:companyId/goals/:goalId', (req, res) => { const goals = new GoalsService(req.params.companyId); const goal = goals.get(req.params.goalId); if (!goal) return res.status(404).json({ error: 'Goal not found' }); res.json(goal); }); // PUT /api/company/:companyId/goals/:goalId router.put('/:companyId/goals/:goalId', (req, res) => { const goals = new GoalsService(req.params.companyId); const result = goals.updateGoal(req.params.goalId, req.body); if (!result) return res.status(404).json({ error: 'Goal not found' }); res.json(result); }); // POST /api/company/:companyId/goals/:goalId/nodes router.post('/:companyId/goals/:goalId/nodes', (req, res) => { try { const { parentNodeId, title, owner, priority } = req.body; if (!title || !owner) { return res.status(400).json({ error: 'title 和 owner 为必填项' }); } const goals = new GoalsService(req.params.companyId); const result = goals.addNode(req.params.goalId, parentNodeId || null, { title, owner, priority, }); if (!result) return res.status(404).json({ error: 'Goal not found' }); res.status(201).json({ nodeId: result.nodeId, node: result.goal.nodes[result.nodeId] }); } catch (err) { res.status(500).json({ error: err.message }); } }); // PUT /api/company/:companyId/goals/:goalId/nodes/:nodeId router.put('/:companyId/goals/:goalId/nodes/:nodeId', (req, res) => { const goals = new GoalsService(req.params.companyId); const result = goals.updateNode(req.params.goalId, req.params.nodeId, req.body); if (!result) return res.status(404).json({ error: 'Node not found' }); res.json({ nodeId: result.nodeId, node: result.node }); }); // GET /api/company/:companyId/goals/:goalId/progress router.get('/:companyId/goals/:goalId/progress', (req, res) => { const goals = new GoalsService(req.params.companyId); const progress = goals.getProgress(req.params.goalId); if (!progress) return res.status(404).json({ error: 'Goal not found' }); res.json(progress); }); // ═══════════════════════════════════════ // Pipeline / SOP 流水线 // ═══════════════════════════════════════ const { PipelineService } = require('../services/pipeline-service'); // POST /api/company/:companyId/pipelines — 创建流水线 router.post('/:companyId/pipelines', (req, res) => { try { const pSvc = new PipelineService(req.params.companyId); const pipeline = pSvc.createPipeline(req.body); res.status(201).json(pipeline); } catch (err) { res.status(400).json({ error: err.message }); } }); // GET /api/company/:companyId/pipelines — 列出流水线 router.get('/:companyId/pipelines', (req, res) => { const pSvc = new PipelineService(req.params.companyId); res.json(pSvc.listPipelines()); }); // GET /api/company/:companyId/pipelines/:pipelineId — 获取流水线定义 router.get('/:companyId/pipelines/:pipelineId', (req, res) => { const pSvc = new PipelineService(req.params.companyId); const pipeline = pSvc.getPipeline(req.params.pipelineId); if (!pipeline) return res.status(404).json({ error: '流水线不存在' }); res.json(pipeline); }); // PUT /api/company/:companyId/pipelines/:pipelineId — 更新流水线 router.put('/:companyId/pipelines/:pipelineId', (req, res) => { const pSvc = new PipelineService(req.params.companyId); const pipeline = pSvc.updatePipeline(req.params.pipelineId, req.body); if (!pipeline) return res.status(404).json({ error: '流水线不存在' }); res.json(pipeline); }); // POST /api/company/:companyId/pipelines/:pipelineId/run — 启动运行 router.post('/:companyId/pipelines/:pipelineId/run', (req, res) => { try { const pSvc = new PipelineService(req.params.companyId); const run = pSvc.startRun(req.params.pipelineId, req.body); res.status(201).json(run); } catch (err) { res.status(400).json({ error: err.message }); } }); // GET /api/company/:companyId/pipelines/runs/list — 列出运行记录 router.get('/:companyId/pipelines/runs/list', (req, res) => { const pSvc = new PipelineService(req.params.companyId); res.json(pSvc.listRuns(req.query.pipelineId)); }); // GET /api/company/:companyId/pipelines/runs/:runId — 获取运行详情 router.get('/:companyId/pipelines/runs/:runId', (req, res) => { const pSvc = new PipelineService(req.params.companyId); const run = pSvc.getRun(req.params.runId); if (!run) return res.status(404).json({ error: '运行记录不存在' }); res.json(run); }); // POST /api/company/:companyId/pipelines/runs/:runId/advance — 推进步骤 router.post('/:companyId/pipelines/runs/:runId/advance', (req, res) => { try { const pSvc = new PipelineService(req.params.companyId); const { stepIndex, output, status } = req.body; if (stepIndex === undefined) return res.status(400).json({ error: 'stepIndex 为必填项' }); const run = pSvc.advanceStep(req.params.runId, Number(stepIndex), { output, status }); res.json(run); } catch (err) { res.status(400).json({ error: err.message }); } }); // GET /api/company/:companyId/pipelines/runs/:runId/current — 获取当前任务 router.get('/:companyId/pipelines/runs/:runId/current', (req, res) => { const pSvc = new PipelineService(req.params.companyId); const task = pSvc.getCurrentTask(req.params.runId); if (!task) return res.json({ message: '无就绪任务' }); res.json(task); }); // POST /api/company/:companyId/pipelines/runs/:runId/reject — COO 驳回步骤 router.post('/:companyId/pipelines/runs/:runId/reject', (req, res) => { try { const pSvc = new PipelineService(req.params.companyId); const { stepIndex, reason, reviewedBy } = req.body; if (stepIndex === undefined) return res.status(400).json({ error: 'stepIndex 为必填项' }); const run = pSvc.rejectStep(req.params.runId, Number(stepIndex), { reason: reason || '', reviewedBy: reviewedBy || 'coo', }); res.json(run); } catch (err) { res.status(400).json({ error: err.message }); } }); // Schedule (日程管理) // ═══════════════════════════════════════ // GET /api/company/:companyId/schedule — 获取日程 router.get('/:companyId/schedule', (req, res) => { res.json(scheduleService.getSchedule(req.params.companyId)); }); // POST /api/company/:companyId/schedule/slots — 添加日程 router.post('/:companyId/schedule/slots', (req, res) => { try { const slot = scheduleService.addSlot(req.params.companyId, req.body); res.json(slot); } catch (err) { res.status(400).json({ error: err.message }); } }); // PUT /api/company/:companyId/schedule/slots/:slotId — 更新日程 router.put('/:companyId/schedule/slots/:slotId', (req, res) => { const slot = scheduleService.updateSlot(req.params.companyId, req.params.slotId, req.body); if (!slot) return res.status(404).json({ error: '日程不存在' }); res.json(slot); }); // DELETE /api/company/:companyId/schedule/slots/:slotId — 删除日程 router.delete('/:companyId/schedule/slots/:slotId', (req, res) => { scheduleService.removeSlot(req.params.companyId, req.params.slotId); res.json({ success: true }); }); // ═══════════════════════════════════════ // DataSource (运营数据接入) // ═══════════════════════════════════════ const { DataSourceService } = require('../services/datasource-service'); const dataSourceService = new DataSourceService(); // GET /api/company/:companyId/data/metrics — 获取运营指标 router.get('/:companyId/data/metrics', async (req, res) => { try { const { source = 'mock', startDate, endDate } = req.query; const sd = startDate || new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10); const ed = endDate || new Date().toISOString().slice(0, 10); const data = await dataSourceService.fetchMetrics(source, { startDate: sd, endDate: ed }); res.json(data); } catch (err) { res.status(400).json({ error: err.message }); } }); // GET /api/company/:companyId/data/trending — 获取热点趋势 router.get('/:companyId/data/trending', async (req, res) => { try { const { source = 'mock', platform = 'weibo', count = '10' } = req.query; const data = await dataSourceService.fetchTrending(source, { platform, count: parseInt(count, 10) }); res.json(data); } catch (err) { res.status(400).json({ error: err.message }); } }); // GET /api/company/:companyId/data/attribution — 归因分析 router.get('/:companyId/data/attribution', async (req, res) => { try { const { source = 'mock' } = req.query; const now = new Date(); const currentEnd = now.toISOString().slice(0, 10); const currentStart = new Date(now.getTime() - 7 * 86400000).toISOString().slice(0, 10); const previousEnd = new Date(now.getTime() - 7 * 86400000).toISOString().slice(0, 10); const previousStart = new Date(now.getTime() - 14 * 86400000).toISOString().slice(0, 10); const analysis = await dataSourceService.analyzeAttribution( source, { startDate: currentStart, endDate: currentEnd }, { startDate: previousStart, endDate: previousEnd } ); res.json(analysis); } catch (err) { res.status(400).json({ error: err.message }); } }); // ═══════════════════════════════════════ // Company Files — 公司级统一文件树 // ═══════════════════════════════════════ // GET /api/company/:companyId/files — 获取公司下所有 Agent 的文件树 router.get('/:companyId/files', (req, res) => { try { const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data/companies'); const companyDir = path.join(DATA_DIR, req.params.companyId); if (!fs.existsSync(companyDir)) { return res.json({ tree: [], totalFiles: 0 }); } // 递归扫描目录 function scanDir(dirPath, relativePath = '') { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); const nodes = []; for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name; // 跳过隐藏文件和 node_modules if (entry.name.startsWith('.') || entry.name === 'node_modules') continue; // 跳过 conversations 目录(太大) if (entry.name === 'conversations') continue; if (entry.isDirectory()) { const children = scanDir(fullPath, relPath); nodes.push({ name: entry.name, path: relPath, type: 'directory', children, fileCount: children.reduce((sum, c) => sum + (c.type === 'file' ? 1 : (c.fileCount || 0)), 0), }); } else { const stat = fs.statSync(fullPath); nodes.push({ name: entry.name, path: relPath, type: 'file', size: stat.size, modifiedAt: stat.mtime.toISOString(), }); } } // 目录排前面, 然后按名字排序 return nodes.sort((a, b) => { if (a.type !== b.type) return a.type === 'directory' ? -1 : 1; return a.name.localeCompare(b.name); }); } const tree = scanDir(companyDir); const countFiles = (nodes) => 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/company/:companyId/files/content?path=xxx — 读取文件内容 router.get('/:companyId/files/content', (req, res) => { try { const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data/companies'); const filePath = req.query.path; if (!filePath) return res.status(400).json({ error: 'path 参数为必填' }); // 安全校验:防止路径穿越 const absPath = path.resolve(DATA_DIR, req.params.companyId, filePath); const safeBoundary = path.resolve(DATA_DIR, req.params.companyId); if (!absPath.startsWith(safeBoundary)) { return res.status(403).json({ error: '路径越界' }); } if (!fs.existsSync(absPath)) { return res.status(404).json({ error: '文件不存在' }); } const stat = fs.statSync(absPath); if (stat.size > 512 * 1024) { return res.json({ content: '(文件过大,跳过预览)', truncated: true, size: stat.size }); } const content = fs.readFileSync(absPath, 'utf-8'); res.json({ content, size: stat.size, path: filePath }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ═══════════════════════════════════════ // Company Messages — 全局消息时间线 // ═══════════════════════════════════════ // GET /api/company/:companyId/messages?limit=100 — 获取公司所有 Agent 的消息时间线 router.get('/:companyId/messages', (req, res) => { try { const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data/companies'); const agentsDir = path.join(DATA_DIR, req.params.companyId, 'agents'); const limit = Math.min(parseInt(req.query.limit) || 200, 500); if (!fs.existsSync(agentsDir)) { return res.json([]); } const allMessages = []; const agents = fs.readdirSync(agentsDir, { withFileTypes: true }).filter(d => d.isDirectory()); for (const agentEntry of agents) { const agentId = agentEntry.name; const convDir = path.join(agentsDir, agentId, 'conversations'); if (!fs.existsSync(convDir)) continue; const conversations = fs.readdirSync(convDir, { withFileTypes: true }).filter(d => d.isDirectory()); for (const conv of conversations) { const convId = conv.name; // 读 meta let meta = null; try { meta = JSON.parse(fs.readFileSync(path.join(convDir, convId, 'meta.json'), 'utf-8')); } catch { continue; } // 读 history let history = []; try { history = JSON.parse(fs.readFileSync(path.join(convDir, convId, 'history.json'), 'utf-8')); } catch { continue; } for (const msg of history) { allMessages.push({ agentId: `${req.params.companyId}:${agentId}`, agentName: agentId, conversationId: convId, conversationTitle: meta?.title || convId, role: msg.role, content: (msg.content || '').slice(0, 800), timestamp: msg.timestamp || meta?.createdAt, messageId: msg.messageId, taskId: convId.startsWith('task-') ? convId.replace('task-', '') : null, }); } } } // 按时间倒序 allMessages.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); res.json(allMessages.slice(0, limit)); } catch (err) { res.status(500).json({ error: err.message }); } }); // ═══════════════════════════════════════ // Company Stats — 统计 API (skills count, jobs status) // ═══════════════════════════════════════ // GET /api/company/:companyId/stats — 获取公司统计信息 router.get('/:companyId/stats', (req, res) => { try { const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data'); const companyId = req.params.companyId; // 1. Skills 统计 - 统计所有 Agent 的 Skill 数量 let totalSkills = 0; const agentSkills = []; const agentsDir = path.join(DATA_DIR, 'companies', companyId, 'agents'); if (fs.existsSync(agentsDir)) { const agentDirs = fs.readdirSync(agentsDir, { withFileTypes: true }).filter(d => d.isDirectory()); for (const agentDir of agentDirs) { const skillsDir = path.join(agentsDir, agentDir.name, 'skills'); if (fs.existsSync(skillsDir)) { const skillCount = fs.readdirSync(skillsDir, { withFileTypes: true }).filter(d => d.isDirectory()).length; totalSkills += skillCount; agentSkills.push({ agentId: agentDir.name, skillCount }); } } } // 2. Agents 统计 const agents = companyService.listAgents(companyId); const agentCount = agents.length; // 3. Jobs 统计 - 从 jobs 持久化文件读取 let jobsStats = { total: 0, queued: 0, running: 0, completed: 0, failed: 0 }; const jobsFilePath = path.join(DATA_DIR, '.data', 'jobs-openapi.json'); if (fs.existsSync(jobsFilePath)) { try { const jobsData = JSON.parse(fs.readFileSync(jobsFilePath, 'utf-8')); if (Array.isArray(jobsData)) { jobsStats.total = jobsData.length; for (const job of jobsData) { if (jobsStats[job.status] !== undefined) { jobsStats[job.status]++; } } } } catch (e) { console.warn('[Stats] 读取 Jobs 文件失败:', e.message); } } // 4. Pipelines 统计 const pSvc = new PipelineService(companyId); const pipelines = pSvc.listPipelines(); const runs = pSvc.listRuns(); const pipelinesStats = { total: pipelines.length, active: pipelines.filter(p => p.status === 'active').length, runs: { total: runs.length, running: runs.filter(r => r.status === 'running').length, completed: runs.filter(r => r.status === 'completed').length, failed: runs.filter(r => r.status === 'failed').length, } }; res.json({ companyId, timestamp: new Date().toISOString(), skills: { total: totalSkills, byAgent: agentSkills }, agents: { total: agentCount }, jobs: jobsStats, pipelines: pipelinesStats, }); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/company/:companyId/jobs — 获取公司相关的 Job 列表 router.get('/:companyId/jobs', (req, res) => { try { const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data'); const jobsFilePath = path.join(DATA_DIR, '.data', 'jobs-openapi.json'); if (!fs.existsSync(jobsFilePath)) { return res.json([]); } const jobsData = JSON.parse(fs.readFileSync(jobsFilePath, 'utf-8')); if (!Array.isArray(jobsData)) { return res.json([]); } // 过滤与公司相关的 Jobs (通过 domain 或 conversationId 包含 companyId) const limit = Math.min(parseInt(req.query.limit) || 50, 100); const companyId = req.params.companyId; const filtered = jobsData .filter(job => !job.domain || job.domain.includes(companyId)) .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) .slice(0, limit); res.json(filtered); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/company/:companyId/jobs/:jobId — 获取 Job 详情 router.get('/:companyId/jobs/:jobId', (req, res) => { try { const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data'); const jobsFilePath = path.join(DATA_DIR, '.data', 'jobs-openapi.json'); if (!fs.existsSync(jobsFilePath)) { return res.status(404).json({ error: 'Job 不存在' }); } const jobsData = JSON.parse(fs.readFileSync(jobsFilePath, 'utf-8')); if (!Array.isArray(jobsData)) { return res.status(404).json({ error: 'Job 不存在' }); } const job = jobsData.find(j => j.jobId === req.params.jobId); if (!job) { return res.status(404).json({ error: 'Job 不存在' }); } // 返回安全版本 (隐藏 apiKey, webhookUrl) const { apiKey, webhookUrl, ...safeJob } = job; res.json(safeJob); } catch (err) { res.status(500).json({ error: err.message }); } }); module.exports = router;