/** * CompanyService — 公司实例管理 * * 数据目录: data/{companyId}/ * 模板库: data/templates/ * * 核心职责: * 1. Company CRUD (create/get/list/shutdown) * 2. Agent 招聘 (hire: 模板优先 → 培养兜底) * 3. resolveAgentDir(companyId, agentId) 统一寻址 */ const fs = require('fs'); const path = require('path'); const ROOT_DATA_DIR = path.resolve(__dirname, '../../data'); const DATA_DIR = path.join(ROOT_DATA_DIR, 'companies'); const TEMPLATES_DIR = path.join(ROOT_DATA_DIR, 'system', 'templates'); const GLOBAL_AGENTS_DIR = path.join(ROOT_DATA_DIR, 'system', 'agents'); // 模板骨架文件白名单 (复制这些,不复制 memory/workspace/conversations 等运行态数据) const TEMPLATE_FILES = [ 'agent.md', 'soul.md', 'identity.md', 'user.md', 'tools.md', 'heartbeat.md', 'plan.md', 'todo.md', 'cron.json', 'config.json', ]; class CompanyService { // ═══════════════════════════════════════ // Company CRUD // ═══════════════════════════════════════ /** * 创建一家新公司 * @param {object} opts - { id, name, description } */ create({ id, name, description = '' }) { const companyDir = path.join(DATA_DIR, id); if (fs.existsSync(companyDir)) { throw new Error(`公司 ${id} 已存在`); } // 创建目录结构 const subdirs = ['agents', 'goals', 'pipelines', 'workspace', 'workspace/assets', 'workspace/output', 'docs', 'logs/pipeline_runs', 'logs/audit']; for (const sub of subdirs) { fs.mkdirSync(path.join(companyDir, sub), { recursive: true }); } const now = new Date().toISOString(); const company = { id, name, description, status: 'creating', // creating → running → paused → shutdown createdAt: now, updatedAt: now, createdBy: 'master', ceo: null, sop: null, agents: [], heartbeat: { enabled: false, // CEO 上任后才开启 intervalMinutes: 30, }, config: { timezone: 'Asia/Shanghai', maxAgents: 20, }, }; this._saveCompany(company); // 重新保存(agents 列表已更新) this._saveCompany(company); console.log(`[Company] 🏢 创建公司: ${id} (${name})`); return company; } /** * 获取公司详情 */ get(companyId) { const fp = path.join(DATA_DIR, companyId, 'company.json'); if (!fs.existsSync(fp)) return null; return JSON.parse(fs.readFileSync(fp, 'utf-8')); } /** * 列出所有公司 */ list() { if (!fs.existsSync(DATA_DIR)) return []; return fs.readdirSync(DATA_DIR, { withFileTypes: true }) .filter(d => d.isDirectory() && fs.existsSync(path.join(DATA_DIR, d.name, 'company.json'))) .map(d => { const company = this.get(d.name); if (!company) return null; return { id: company.id, name: company.name, status: company.status, agentCount: company.agents ? company.agents.length : 0, ceo: company.ceo, updatedAt: company.updatedAt, }; }) .filter(Boolean); } /** * 更新公司信息 */ update(companyId, updates) { const company = this.get(companyId); if (!company) return null; if (updates.name !== undefined) company.name = updates.name; if (updates.description !== undefined) company.description = updates.description; if (updates.status !== undefined) { company.status = updates.status; // Track startedAt for uptime calculation if (updates.status === 'running' && !company.startedAt) { company.startedAt = new Date().toISOString(); } else if (updates.status === 'shutdown' || updates.status === 'paused') { company.startedAt = null; } } if (updates.sop !== undefined) company.sop = updates.sop; if (updates.heartbeat !== undefined) company.heartbeat = { ...company.heartbeat, ...updates.heartbeat }; company.updatedAt = new Date().toISOString(); this._saveCompany(company); return company; } /** * 关停公司 (软关停: 标记状态, 停止心跳, 保留数据) */ shutdown(companyId) { const company = this.get(companyId); if (!company) return null; company.status = 'shutdown'; company.heartbeat.enabled = false; company.startedAt = null; company.updatedAt = new Date().toISOString(); this._saveCompany(company); console.log(`[Company] 🛑 关停公司: ${companyId}`); return company; } // ═══════════════════════════════════════ // Agent 招聘系统 (Hiring Pipeline) // ═══════════════════════════════════════ /** * 招聘一个 Agent 到公司 * * 流程: 模板库查找 → 复制 → 注入公司上下文 * 模板不存在 → 返回 { needsTraining: true } (由上层调用 agent-creator + innovator) * * @param {string} companyId * @param {string} roleName - 角色名 (如 "ceo", "content-writer") * @param {object} opts - { customAgent, customSoul, ... } 可选的上下文覆盖 * @returns {object} { status, agentId, agentDir, needsTraining? } */ hire(companyId, roleName, opts = {}) { const company = this.get(companyId); if (!company) throw new Error(`公司 ${companyId} 不存在`); const targetDir = path.join(DATA_DIR, companyId, 'agents', roleName); if (fs.existsSync(targetDir)) { return { status: 'already_exists', agentId: roleName, agentDir: targetDir }; } // 阶段 A: 在模板库中查找 const templateDir = path.join(TEMPLATES_DIR, roleName); if (fs.existsSync(templateDir)) { // ✅ 模板存在 → 复制骨架到公司 this._copyTemplate(templateDir, targetDir); this._injectCompanyContext(companyId, roleName, opts); this._updateTemplateUsedCount(roleName); // 更新公司的 agents 列表 if (!company.agents.includes(roleName)) { company.agents.push(roleName); } // 如果是 CEO,标记到 company.ceo const agentConfig = this._readAgentConfig(targetDir); if (agentConfig && agentConfig.type === 'cxo') { company.ceo = roleName; } company.updatedAt = new Date().toISOString(); this._saveCompany(company); console.log(`[Company] ✅ ${companyId} 招聘 ${roleName} (从模板)`); return { status: 'hired_from_template', agentId: roleName, agentDir: targetDir }; } // 阶段 B: 模板不存在 → 需要培养 console.log(`[Company] ⚠️ 模板库中不存在 "${roleName}",需要 agent-creator + innovator 培养`); return { status: 'needs_training', agentId: roleName, agentDir: targetDir, needsTraining: true, }; } /** * 用 AgentScaffold 直接创建 Agent 骨架到公司 * (Phase 2: 不走 HTTP,直接调用脚手架) * * @param {string} companyId * @param {string} roleName * @param {object} opts - { type, name, description, parentId } * @returns {Promise} { agentDir, agentId } */ async scaffoldNewAgent(companyId, roleName, opts = {}) { const company = this.get(companyId); if (!company) throw new Error(`公司 ${companyId} 不存在`); const companyAgentsDir = path.join(DATA_DIR, companyId, 'agents'); if (!fs.existsSync(companyAgentsDir)) { fs.mkdirSync(companyAgentsDir, { recursive: true }); } // 使用 ESM 导入 AgentScaffold const { AgentScaffold } = await import('../../packages/agent-core/AgentScaffold.js'); const scaffold = new AgentScaffold(companyAgentsDir); const agentDir = await scaffold.create({ id: roleName, type: opts.type || 'skill', name: opts.name || roleName, parentId: opts.parentId || company.ceo || 'ceo', goalId: opts.goalId || null, }); // 注入公司上下文 this._injectCompanyContext(companyId, roleName, opts); // 标记为 training 状态 const configPath = path.join(agentDir, 'config.json'); if (fs.existsSync(configPath)) { const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); config.status = 'training'; config.companyId = companyId; fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); } // 更新公司 if (!company.agents.includes(roleName)) { company.agents.push(roleName); } company.updatedAt = new Date().toISOString(); this._saveCompany(company); console.log(`[Company] 🎓 ${companyId} 创建新 Agent 骨架: ${roleName} (training)`); return { agentDir, agentId: roleName }; } /** * 将训练合格的 Agent 沉淀为模板 * (Phase 2: Benchmark ≥ 95 后调用) * * @param {string} companyId * @param {string} agentId * @param {number} benchmarkScore * @returns {object} { status, templateId } */ depositTemplate(companyId, agentId, benchmarkScore) { if (benchmarkScore < 95) { return { status: 'rejected', message: `评分 ${benchmarkScore} 未达标 (需 ≥ 95)`, agentId, }; } const agentDir = path.join(DATA_DIR, companyId, 'agents', agentId); if (!fs.existsSync(agentDir)) { throw new Error(`Agent ${agentId} 在 ${companyId} 中不存在`); } const templateDir = path.join(TEMPLATES_DIR, agentId); // 如果模板已存在且分数更低,覆盖 if (fs.existsSync(templateDir)) { fs.rmSync(templateDir, { recursive: true, force: true }); } // 只复制骨架文件到模板 (不含 memory/workspace/conversations) fs.mkdirSync(templateDir, { recursive: true }); for (const file of TEMPLATE_FILES) { const srcFile = path.join(agentDir, file); if (fs.existsSync(srcFile)) { // 复制模板时去掉公司特定信息 (对 config.json 特殊处理) if (file === 'config.json') { const config = JSON.parse(fs.readFileSync(srcFile, 'utf-8')); delete config.companyId; config.status = 'ready'; config.benchmarkScore = benchmarkScore; config.trainedAt = new Date().toISOString().slice(0, 10); fs.writeFileSync(path.join(templateDir, file), JSON.stringify(config, null, 2), 'utf-8'); } else { fs.copyFileSync(srcFile, path.join(templateDir, file)); } } } // 复制 skills/ 目录 const srcSkills = path.join(agentDir, 'skills'); if (fs.existsSync(srcSkills)) { this._copyDirRecursive(srcSkills, path.join(templateDir, 'skills')); } // 更新模板注册表 const registryPath = path.join(TEMPLATES_DIR, '_registry.json'); let registry = []; if (fs.existsSync(registryPath)) { registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8')); } // 移除旧条目 (如果存在) registry = registry.filter(t => t.id !== agentId); const agentConfig = this._readAgentConfig(templateDir); registry.push({ id: agentId, name: agentConfig?.name || agentId, type: agentConfig?.type || 'skill', description: agentConfig?.description || '', benchmarkScore, trainedAt: new Date().toISOString().slice(0, 10), usedCount: 0, }); fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf-8'); // 将 Agent 状态从 training → active const configPath = path.join(agentDir, 'config.json'); if (fs.existsSync(configPath)) { const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); config.status = 'active'; fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); } console.log(`[Company] 📦 Agent ${agentId} 沉淀为模板 (benchmark: ${benchmarkScore})`); return { status: 'deposited', templateId: agentId, benchmarkScore }; } /** * 完整招聘流程 (Phase 2: 模板优先 → 骨架创建兜底) * 如果模板不存在,会创建骨架并返回 training 状态 * 上层(路由)负责异步触发 agent-creator + innovator 实际训练 * * @param {string} companyId * @param {string} roleName * @param {object} opts - { type, name, description, customAgent, customSoul, customUser } * @returns {Promise} */ async hireWithTraining(companyId, roleName, opts = {}) { // 先走标准招聘流程 const result = this.hire(companyId, roleName, opts); if (result.status !== 'needs_training') { return result; } // 模板不存在 → 创建骨架 (Phase 2) const scaffold = await this.scaffoldNewAgent(companyId, roleName, { type: opts.type || 'skill', name: opts.name || roleName, description: opts.description || '', parentId: opts.parentId, customAgent: opts.customAgent, customSoul: opts.customSoul, customUser: opts.customUser, }); return { status: 'training', agentId: roleName, agentDir: scaffold.agentDir, message: `Agent "${roleName}" 骨架已创建,状态: training。需要 agent-creator 定义 + innovator 训练至 ≥95 后才可上岗。`, }; } /** * 解雇 (suspend) 公司内的 Agent */ fire(companyId, agentId) { const company = this.get(companyId); if (!company) return null; const agentDir = path.join(DATA_DIR, companyId, 'agents', agentId); if (!fs.existsSync(agentDir)) return null; // 更新 config.json 状态 const configPath = path.join(agentDir, 'config.json'); if (fs.existsSync(configPath)) { const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); config.status = 'suspended'; fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); } // 从公司 agents 列表移除 company.agents = company.agents.filter(a => a !== agentId); company.updatedAt = new Date().toISOString(); this._saveCompany(company); console.log(`[Company] 🚪 ${companyId} 解雇 ${agentId}`); return { status: 'suspended', agentId }; } /** * 列出公司内的所有 Agent (含状态) */ listAgents(companyId) { const agentsDir = path.join(DATA_DIR, companyId, 'agents'); if (!fs.existsSync(agentsDir)) return []; return fs.readdirSync(agentsDir, { withFileTypes: true }) .filter(d => d.isDirectory()) .map(d => { const config = this._readAgentConfig(path.join(agentsDir, d.name)); return { id: d.name, name: config?.name || d.name, type: config?.type || 'unknown', status: config?.status || 'unknown', companyId, }; }); } // ═══════════════════════════════════════ // 模板库管理 // ═══════════════════════════════════════ /** * 列出所有可用模板 */ listTemplates() { // 始终从目录扫描,合并 _registry.json 中的元数据(benchmarkScore, usedCount 等) if (!fs.existsSync(TEMPLATES_DIR)) return []; const registryPath = path.join(TEMPLATES_DIR, '_registry.json'); let registryMap = {}; if (fs.existsSync(registryPath)) { try { const registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8')); for (const entry of registry) registryMap[entry.id] = entry; } catch {} } const templates = fs.readdirSync(TEMPLATES_DIR, { withFileTypes: true }) .filter(d => d.isDirectory() && !d.name.startsWith('_')) .map(d => { const templateDir = path.join(TEMPLATES_DIR, d.name); const config = this._readAgentConfig(templateDir); const regEntry = registryMap[d.name] || {}; let evolvingVersionCount = 0; let benchmarkVersionCount = 0; const evolvingDir = path.join(templateDir, 'memory', 'evolving'); const benchmarkDir = path.join(templateDir, 'memory', 'benchmark'); if (fs.existsSync(evolvingDir)) { evolvingVersionCount = fs.readdirSync(evolvingDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('v')).length; } if (fs.existsSync(benchmarkDir)) { benchmarkVersionCount = fs.readdirSync(benchmarkDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('v')).length; } // Get directory modification time for sorting let updatedAt = null; try { const configStat = fs.statSync(path.join(templateDir, 'config.json')); updatedAt = configStat.mtime.toISOString(); } catch { try { const dirStat = fs.statSync(templateDir); updatedAt = dirStat.mtime.toISOString(); } catch {} } return { id: d.name, name: regEntry.name || config?.name || d.name, type: regEntry.type || config?.type || 'unknown', description: regEntry.description || config?.description || '', benchmarkScore: regEntry.benchmarkScore || config?.benchmarkScore || null, trainedAt: regEntry.trainedAt || config?.trainedAt || null, usedCount: regEntry.usedCount || 0, fullPath: templateDir, evolvingVersionCount, benchmarkVersionCount, updatedAt, }; }); return templates; } /** * 重建模板注册表 */ rebuildTemplateRegistry() { if (!fs.existsSync(TEMPLATES_DIR)) { fs.mkdirSync(TEMPLATES_DIR, { recursive: true }); return []; } const registry = fs.readdirSync(TEMPLATES_DIR, { withFileTypes: true }) .filter(d => d.isDirectory() && !d.name.startsWith('_')) .map(d => { const templateDir = path.join(TEMPLATES_DIR, d.name); const config = this._readAgentConfig(templateDir); let evolvingVersionCount = 0; let benchmarkVersionCount = 0; const evolvingDir = path.join(templateDir, 'memory', 'evolving'); const benchmarkDir = path.join(templateDir, 'memory', 'benchmark'); if (fs.existsSync(evolvingDir)) { evolvingVersionCount = fs.readdirSync(evolvingDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('v')).length; } if (fs.existsSync(benchmarkDir)) { benchmarkVersionCount = fs.readdirSync(benchmarkDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('v')).length; } return { id: d.name, name: config?.name || d.name, type: config?.type || 'unknown', description: config?.description || '', benchmarkScore: config?.benchmarkScore || null, trainedAt: config?.trainedAt || null, usedCount: config?.usedCount || 0, fullPath: templateDir, evolvingVersionCount, benchmarkVersionCount }; }); fs.writeFileSync( path.join(TEMPLATES_DIR, '_registry.json'), JSON.stringify(registry, null, 2), 'utf-8' ); return registry; } // ═══════════════════════════════════════ // Agent 统一寻址 // ═══════════════════════════════════════ /** * 统一定位 Agent 目录 * @param {string|null} companyId - null 表示全局 Agent * @param {string} agentId * @returns {string|null} 绝对路径, 不存在则返回 null */ static resolveAgentDir(companyId, agentId) { if (companyId) { // 公司内 Agent const dir = path.join(DATA_DIR, companyId, 'agents', agentId); if (fs.existsSync(dir)) return dir; } // 全局 Agent const globalDir = path.join(GLOBAL_AGENTS_DIR, agentId); if (fs.existsSync(globalDir)) return globalDir; return null; } // ═══════════════════════════════════════ // 私有方法 // ═══════════════════════════════════════ _saveCompany(company) { const dir = path.join(DATA_DIR, company.id); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, 'company.json'), JSON.stringify(company, null, 2), 'utf-8'); } _readAgentConfig(agentDir) { const fp = path.join(agentDir, 'config.json'); if (!fs.existsSync(fp)) return null; try { return JSON.parse(fs.readFileSync(fp, 'utf-8')); } catch { return null; } } /** * 复制模板骨架到目标目录 (只复制 md/json 骨架文件,不复制运行态数据) */ _copyTemplate(srcDir, destDir) { fs.mkdirSync(destDir, { recursive: true }); // 复制白名单中的骨架文件 for (const file of TEMPLATE_FILES) { const srcFile = path.join(srcDir, file); if (fs.existsSync(srcFile)) { fs.copyFileSync(srcFile, path.join(destDir, file)); } } // 复制 .claude/ 目录 (包含 skills/main_skill/SKILL.md 等) const srcClaude = path.join(srcDir, '.claude'); if (fs.existsSync(srcClaude)) { this._copyDirRecursive(srcClaude, path.join(destDir, '.claude')); } // 复制 skills/ 目录 (旧结构兼容) const srcSkills = path.join(srcDir, 'skills'); if (fs.existsSync(srcSkills)) { this._copyDirRecursive(srcSkills, path.join(destDir, 'skills')); } // 初始化空的 memory 目录 const memoryDir = path.join(destDir, 'memory'); fs.mkdirSync(memoryDir, { recursive: true }); fs.writeFileSync(path.join(memoryDir, 'memory.md'), '# 我的记忆\n\n## 关键事实\n\n_(刚入职,尚无记忆)_\n\n## 已验证的模式\n\n## 失败教训\n', 'utf-8'); } _copyDirRecursive(src, dest) { fs.mkdirSync(dest, { recursive: true }); const entries = fs.readdirSync(src, { withFileTypes: true }); for (const entry of entries) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { this._copyDirRecursive(srcPath, destPath); } else { fs.copyFileSync(srcPath, destPath); } } } /** * 注入公司上下文到新复制的 Agent */ _injectCompanyContext(companyId, agentId, opts = {}) { const company = this.get(companyId); if (!company) return; const agentDir = path.join(DATA_DIR, companyId, 'agents', agentId); // 1. 更新 config.json: 添加 companyId, 设置状态为 active const configPath = path.join(agentDir, 'config.json'); if (fs.existsSync(configPath)) { const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); config.companyId = companyId; config.status = 'active'; if (config.type === 'cxo') { config.parent_id = 'master'; } else { config.parent_id = company.ceo || 'ceo'; } fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); } // 2. 追加公司信息到 agent.md const agentMdPath = path.join(agentDir, 'agent.md'); if (fs.existsSync(agentMdPath)) { const content = fs.readFileSync(agentMdPath, 'utf-8'); const companyContext = [ '', '---', '', '## 所属公司', `- **公司ID**: ${companyId}`, `- **公司名称**: ${company.name}`, `- **公司使命**: ${company.description || '待定义'}`, '', `> 你是 **${company.name}** 的员工。你的所有工作都应围绕公司的使命和目标展开。`, opts.customAgent || '', ].join('\n'); fs.writeFileSync(agentMdPath, content + companyContext, 'utf-8'); } // 3. 重写 user.md: 指明上级关系 const userMdPath = path.join(agentDir, 'user.md'); const userContent = [ '# 与上级的关系', '', '## 所属公司', `- **公司**: ${company.name} (${companyId})`, '', '## 汇报对象', ]; const agentConfig = this._readAgentConfig(agentDir); if (agentConfig && agentConfig.type === 'cxo') { userContent.push('- **直接汇报**: Master (公司创始人/董事会)'); userContent.push('- **管理范围**: 公司内所有 Agent'); } else { userContent.push(`- **直接汇报**: ${company.ceo || 'CEO'} (公司 CEO)`); userContent.push('- **协同**: 公司内其他同事'); } userContent.push('', '## 沟通规则'); userContent.push('1. 完成任务后主动向上级汇报结果'); userContent.push('2. 遇到阻碍时及时上报,附带解决建议'); userContent.push('3. 保持透明,坏消息更要及时同步'); userContent.push(opts.customUser || ''); fs.writeFileSync(userMdPath, userContent.join('\n'), 'utf-8'); // 4. 追加公司文化到 soul.md (如有自定义) if (opts.customSoul) { const soulPath = path.join(agentDir, 'soul.md'); if (fs.existsSync(soulPath)) { const soulContent = fs.readFileSync(soulPath, 'utf-8'); fs.writeFileSync(soulPath, soulContent + '\n\n## 公司文化\n' + opts.customSoul + '\n', 'utf-8'); } } } /** * 更新模板使用次数 */ _updateTemplateUsedCount(templateId) { const registryPath = path.join(TEMPLATES_DIR, '_registry.json'); if (!fs.existsSync(registryPath)) return; try { const registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8')); const entry = registry.find(t => t.id === templateId); if (entry) { entry.usedCount = (entry.usedCount || 0) + 1; fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf-8'); } } catch (err) { console.warn(`[Company] ⚠️ 更新模板使用次数失败: ${err.message}`); } } } module.exports = { CompanyService };