Commit 2c2cde8542c0d97aaba9ddcc2c5bb8522ba98d16
1 parent
212d022e
feat(company): implement Phase 2 - Hiring Pipeline with training & benchmark
- scaffoldNewAgent: create agent skeleton via AgentScaffold directly - depositTemplate: benchmark gating (≥95) + auto-deposit to templates/ - hireWithTraining: full flow (template lookup → scaffold fallback) - Company route: /hire uses hireWithTraining, new /deposit-template endpoint - E2E test: 69/69 passed (Phase 1 + Phase 2)
Showing
4 changed files
with
300 additions
and
26 deletions
data/templates/_registry.json
| @@ -6,7 +6,7 @@ | @@ -6,7 +6,7 @@ | ||
| 6 | "description": "公司战略规划、团队管理、OKR 制定、任务委派", | 6 | "description": "公司战略规划、团队管理、OKR 制定、任务委派", |
| 7 | "benchmarkScore": 97, | 7 | "benchmarkScore": 97, |
| 8 | "trainedAt": "2026-03-20", | 8 | "trainedAt": "2026-03-20", |
| 9 | - "usedCount": 1 | 9 | + "usedCount": 2 |
| 10 | }, | 10 | }, |
| 11 | { | 11 | { |
| 12 | "id": "qa-judge", | 12 | "id": "qa-judge", |
| @@ -15,6 +15,6 @@ | @@ -15,6 +15,6 @@ | ||
| 15 | "description": "质量验收、OKR 进度审查、标准化仲裁", | 15 | "description": "质量验收、OKR 进度审查、标准化仲裁", |
| 16 | "benchmarkScore": 95, | 16 | "benchmarkScore": 95, |
| 17 | "trainedAt": "2026-03-25", | 17 | "trainedAt": "2026-03-25", |
| 18 | - "usedCount": 1 | 18 | + "usedCount": 2 |
| 19 | } | 19 | } |
| 20 | ] | 20 | ] |
| 21 | \ No newline at end of file | 21 | \ No newline at end of file |
server/routes/company.js
| @@ -68,13 +68,13 @@ router.post('/:companyId/shutdown', (req, res) => { | @@ -68,13 +68,13 @@ router.post('/:companyId/shutdown', (req, res) => { | ||
| 68 | // ═══════════════════════════════════════ | 68 | // ═══════════════════════════════════════ |
| 69 | 69 | ||
| 70 | // POST /api/company/:companyId/hire | 70 | // POST /api/company/:companyId/hire |
| 71 | -router.post('/:companyId/hire', (req, res) => { | 71 | +router.post('/:companyId/hire', async (req, res) => { |
| 72 | try { | 72 | try { |
| 73 | - const { role, customAgent, customSoul, customUser } = req.body; | 73 | + const { role, type, name, description, customAgent, customSoul, customUser } = req.body; |
| 74 | if (!role) return res.status(400).json({ error: 'role 为必填项' }); | 74 | if (!role) return res.status(400).json({ error: 'role 为必填项' }); |
| 75 | 75 | ||
| 76 | - const result = companyService.hire(req.params.companyId, role, { | ||
| 77 | - customAgent, customSoul, customUser, | 76 | + const result = await companyService.hireWithTraining(req.params.companyId, role, { |
| 77 | + type, name: name || role, description, customAgent, customSoul, customUser, | ||
| 78 | }); | 78 | }); |
| 79 | res.json(result); | 79 | res.json(result); |
| 80 | } catch (err) { | 80 | } catch (err) { |
| @@ -92,6 +92,23 @@ router.post('/:companyId/fire', (req, res) => { | @@ -92,6 +92,23 @@ router.post('/:companyId/fire', (req, res) => { | ||
| 92 | res.json(result); | 92 | res.json(result); |
| 93 | }); | 93 | }); |
| 94 | 94 | ||
| 95 | +// POST /api/company/:companyId/deposit-template — 训练合格后沉淀为模板 | ||
| 96 | +router.post('/:companyId/deposit-template', (req, res) => { | ||
| 97 | + try { | ||
| 98 | + const { agentId, benchmarkScore } = req.body; | ||
| 99 | + if (!agentId || benchmarkScore === undefined) { | ||
| 100 | + return res.status(400).json({ error: 'agentId 和 benchmarkScore 为必填项' }); | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + const result = companyService.depositTemplate( | ||
| 104 | + req.params.companyId, agentId, Number(benchmarkScore) | ||
| 105 | + ); | ||
| 106 | + res.json(result); | ||
| 107 | + } catch (err) { | ||
| 108 | + res.status(400).json({ error: err.message }); | ||
| 109 | + } | ||
| 110 | +}); | ||
| 111 | + | ||
| 95 | // GET /api/company/:companyId/agents | 112 | // GET /api/company/:companyId/agents |
| 96 | router.get('/:companyId/agents', (req, res) => { | 113 | router.get('/:companyId/agents', (req, res) => { |
| 97 | const agents = companyService.listAgents(req.params.companyId); | 114 | const agents = companyService.listAgents(req.params.companyId); |
server/scripts/test-company-e2e.js
| @@ -118,26 +118,96 @@ async function main() { | @@ -118,26 +118,96 @@ async function main() { | ||
| 118 | const hireQa = svc.hire(TEST_COMPANY_ID, 'qa-judge'); | 118 | const hireQa = svc.hire(TEST_COMPANY_ID, 'qa-judge'); |
| 119 | assert(hireQa.status === 'hired_from_template', 'qa-judge 从模板招聘成功'); | 119 | assert(hireQa.status === 'hired_from_template', 'qa-judge 从模板招聘成功'); |
| 120 | 120 | ||
| 121 | - // ─── 6. 招聘不存在的角色 (需要培养) ─── | ||
| 122 | - console.log('\n🎓 Phase 6: 招聘不存在的角色 (需培养)'); | ||
| 123 | - const hireNew = svc.hire(TEST_COMPANY_ID, 'hot-topic-hunter'); | ||
| 124 | - assert(hireNew.status === 'needs_training', '返回 needs_training'); | ||
| 125 | - assert(hireNew.needsTraining === true, 'needsTraining 标志为 true'); | ||
| 126 | - | ||
| 127 | - // ─── 7. 重复招聘检测 ─── | ||
| 128 | - console.log('\n🔄 Phase 7: 重复招聘检测'); | 121 | + // ─── 6. Phase 2: hireWithTraining — 模板不存在, 自动创建骨架 ─── |
| 122 | + console.log('\n🎓 Phase 6: hireWithTraining (模板不存在 → 自动创建骨架)'); | ||
| 123 | + const hireNew = await svc.hireWithTraining(TEST_COMPANY_ID, 'hot-topic-hunter', { | ||
| 124 | + type: 'skill', | ||
| 125 | + name: '热点搜索员', | ||
| 126 | + description: '全网热点监控、选题建议', | ||
| 127 | + }); | ||
| 128 | + assert(hireNew.status === 'training', '返回 training 状态'); | ||
| 129 | + | ||
| 130 | + const hthDir = path.join(TEST_COMPANY_DIR, 'agents', 'hot-topic-hunter'); | ||
| 131 | + assert(fs.existsSync(hthDir), '热点搜索员骨架目录已创建'); | ||
| 132 | + assert(fs.existsSync(path.join(hthDir, 'agent.md')), 'agent.md 已生成'); | ||
| 133 | + assert(fs.existsSync(path.join(hthDir, 'soul.md')), 'soul.md 已生成'); | ||
| 134 | + assert(fs.existsSync(path.join(hthDir, 'config.json')), 'config.json 已生成'); | ||
| 135 | + assert(fs.existsSync(path.join(hthDir, 'todo.md')), 'todo.md 已生成'); | ||
| 136 | + assert(fs.existsSync(path.join(hthDir, 'memory', 'memory.md')), 'memory/ 已初始化'); | ||
| 137 | + | ||
| 138 | + const hthConfig = JSON.parse(fs.readFileSync(path.join(hthDir, 'config.json'), 'utf-8')); | ||
| 139 | + assert(hthConfig.status === 'training', 'config 状态为 training'); | ||
| 140 | + assert(hthConfig.companyId === TEST_COMPANY_ID, 'config 含 companyId'); | ||
| 141 | + | ||
| 142 | + // 检查公司 agents 列表已包含 | ||
| 143 | + const companyWithHth = svc.get(TEST_COMPANY_ID); | ||
| 144 | + assert(companyWithHth.agents.includes('hot-topic-hunter'), 'company.agents 包含 hot-topic-hunter'); | ||
| 145 | + | ||
| 146 | + // ─── 7. Phase 2: depositTemplate — Benchmark 门禁 ─── | ||
| 147 | + console.log('\n🚧 Phase 7: depositTemplate — Benchmark 质量门禁'); | ||
| 148 | + | ||
| 149 | + // 7a: 评分不够 → 拒绝 | ||
| 150 | + const rejectResult = svc.depositTemplate(TEST_COMPANY_ID, 'hot-topic-hunter', 80); | ||
| 151 | + assert(rejectResult.status === 'rejected', '评分 80 被拒绝'); | ||
| 152 | + assert(rejectResult.message.includes('80'), '拒绝消息包含分数'); | ||
| 153 | + | ||
| 154 | + // 7b: 评分 ≥ 95 → 通过 | ||
| 155 | + const depositResult = svc.depositTemplate(TEST_COMPANY_ID, 'hot-topic-hunter', 97); | ||
| 156 | + assert(depositResult.status === 'deposited', '评分 97 成功沉淀'); | ||
| 157 | + assert(depositResult.templateId === 'hot-topic-hunter', '模板 ID 正确'); | ||
| 158 | + | ||
| 159 | + // 检查模板已创建 | ||
| 160 | + const tplDir = path.join(DATA_DIR, 'templates', 'hot-topic-hunter'); | ||
| 161 | + assert(fs.existsSync(tplDir), '模板目录已创建'); | ||
| 162 | + assert(fs.existsSync(path.join(tplDir, 'config.json')), '模板 config.json 存在'); | ||
| 163 | + | ||
| 164 | + const tplConfig = JSON.parse(fs.readFileSync(path.join(tplDir, 'config.json'), 'utf-8')); | ||
| 165 | + assert(tplConfig.benchmarkScore === 97, '模板 config 含 benchmarkScore'); | ||
| 166 | + assert(tplConfig.companyId === undefined, '模板 config 不含 companyId (已清除)'); | ||
| 167 | + assert(tplConfig.status === 'ready', '模板 config 状态为 ready'); | ||
| 168 | + | ||
| 169 | + // 检查 Agent 状态从 training → active | ||
| 170 | + const hthConfigAfter = JSON.parse(fs.readFileSync(path.join(hthDir, 'config.json'), 'utf-8')); | ||
| 171 | + assert(hthConfigAfter.status === 'active', 'Agent 状态从 training → active'); | ||
| 172 | + | ||
| 173 | + // 检查模板注册表更新 | ||
| 174 | + const updatedRegistry = svc.listTemplates(); | ||
| 175 | + const hthTemplate = updatedRegistry.find(t => t.id === 'hot-topic-hunter'); | ||
| 176 | + assert(hthTemplate !== undefined, '注册表包含新模板'); | ||
| 177 | + assert(hthTemplate && hthTemplate.benchmarkScore === 97, '注册表分数正确'); | ||
| 178 | + | ||
| 179 | + // ─── 8. Phase 2: 新模板可被第二家公司复用 ─── | ||
| 180 | + console.log('\n🔄 Phase 8: 验证新模板可被第二家公司复用'); | ||
| 181 | + const SECOND_COMPANY = 'test-manga-co'; | ||
| 182 | + const secondDir = path.join(DATA_DIR, SECOND_COMPANY); | ||
| 183 | + try { | ||
| 184 | + svc.create({ id: SECOND_COMPANY, name: '测试漫剧公司', description: '漫剧测试' }); | ||
| 185 | + const hireFromNewTpl = svc.hire(SECOND_COMPANY, 'hot-topic-hunter'); | ||
| 186 | + assert(hireFromNewTpl.status === 'hired_from_template', '第二家公司从新模板招聘成功'); | ||
| 187 | + | ||
| 188 | + const secondHthConfig = JSON.parse(fs.readFileSync( | ||
| 189 | + path.join(secondDir, 'agents', 'hot-topic-hunter', 'config.json'), 'utf-8' | ||
| 190 | + )); | ||
| 191 | + assert(secondHthConfig.companyId === SECOND_COMPANY, '第二家公司 companyId 正确注入'); | ||
| 192 | + } finally { | ||
| 193 | + if (fs.existsSync(secondDir)) fs.rmSync(secondDir, { recursive: true, force: true }); | ||
| 194 | + } | ||
| 195 | + | ||
| 196 | + // ─── 9. 重复招聘检测 ─── | ||
| 197 | + console.log('\n🔁 Phase 9: 重复招聘检测'); | ||
| 129 | const hireDup = svc.hire(TEST_COMPANY_ID, 'ceo'); | 198 | const hireDup = svc.hire(TEST_COMPANY_ID, 'ceo'); |
| 130 | assert(hireDup.status === 'already_exists', '重复招聘返回 already_exists'); | 199 | assert(hireDup.status === 'already_exists', '重复招聘返回 already_exists'); |
| 131 | 200 | ||
| 132 | - // ─── 8. 公司 Agent 列表 ─── | ||
| 133 | - console.log('\n📋 Phase 8: 公司 Agent 列表'); | 201 | + // ─── 10. 公司 Agent 列表 ─── |
| 202 | + console.log('\n📋 Phase 10: 公司 Agent 列表'); | ||
| 134 | const agents = svc.listAgents(TEST_COMPANY_ID); | 203 | const agents = svc.listAgents(TEST_COMPANY_ID); |
| 135 | - assert(agents.length === 2, `Agent 数量 = 2 (实际: ${agents.length})`); | 204 | + assert(agents.length === 3, `Agent 数量 = 3 (实际: ${agents.length})`); |
| 136 | assert(agents.some(a => a.id === 'ceo'), '列表包含 CEO'); | 205 | assert(agents.some(a => a.id === 'ceo'), '列表包含 CEO'); |
| 137 | assert(agents.some(a => a.id === 'qa-judge'), '列表包含 qa-judge'); | 206 | assert(agents.some(a => a.id === 'qa-judge'), '列表包含 qa-judge'); |
| 207 | + assert(agents.some(a => a.id === 'hot-topic-hunter'), '列表包含 hot-topic-hunter'); | ||
| 138 | 208 | ||
| 139 | - // ─── 9. resolveAgentDir ─── | ||
| 140 | - console.log('\n📍 Phase 9: resolveAgentDir 统一寻址'); | 209 | + // ─── 11. resolveAgentDir ─── |
| 210 | + console.log('\n📍 Phase 11: resolveAgentDir 统一寻址'); | ||
| 141 | const companyAgentDir = CompanyService.resolveAgentDir(TEST_COMPANY_ID, 'ceo'); | 211 | const companyAgentDir = CompanyService.resolveAgentDir(TEST_COMPANY_ID, 'ceo'); |
| 142 | assert(companyAgentDir !== null, '公司内 CEO 可定位'); | 212 | assert(companyAgentDir !== null, '公司内 CEO 可定位'); |
| 143 | assert(companyAgentDir.includes(TEST_COMPANY_ID), '路径包含公司 ID'); | 213 | assert(companyAgentDir.includes(TEST_COMPANY_ID), '路径包含公司 ID'); |
| @@ -149,8 +219,8 @@ async function main() { | @@ -149,8 +219,8 @@ async function main() { | ||
| 149 | const notExist = CompanyService.resolveAgentDir(TEST_COMPANY_ID, 'nonexistent-agent'); | 219 | const notExist = CompanyService.resolveAgentDir(TEST_COMPANY_ID, 'nonexistent-agent'); |
| 150 | assert(notExist === null, '不存在的 Agent 返回 null'); | 220 | assert(notExist === null, '不存在的 Agent 返回 null'); |
| 151 | 221 | ||
| 152 | - // ─── 10. 解雇 Agent ─── | ||
| 153 | - console.log('\n🚪 Phase 10: 解雇 Agent'); | 222 | + // ─── 12. 解雇 Agent ─── |
| 223 | + console.log('\n🚪 Phase 12: 解雇 Agent'); | ||
| 154 | const fireResult = svc.fire(TEST_COMPANY_ID, 'qa-judge'); | 224 | const fireResult = svc.fire(TEST_COMPANY_ID, 'qa-judge'); |
| 155 | assert(fireResult.status === 'suspended', '解雇返回 suspended'); | 225 | assert(fireResult.status === 'suspended', '解雇返回 suspended'); |
| 156 | 226 | ||
| @@ -160,8 +230,8 @@ async function main() { | @@ -160,8 +230,8 @@ async function main() { | ||
| 160 | const firedConfig = JSON.parse(fs.readFileSync(path.join(TEST_COMPANY_DIR, 'agents', 'qa-judge', 'config.json'), 'utf-8')); | 230 | const firedConfig = JSON.parse(fs.readFileSync(path.join(TEST_COMPANY_DIR, 'agents', 'qa-judge', 'config.json'), 'utf-8')); |
| 161 | assert(firedConfig.status === 'suspended', 'qa-judge config 状态为 suspended'); | 231 | assert(firedConfig.status === 'suspended', 'qa-judge config 状态为 suspended'); |
| 162 | 232 | ||
| 163 | - // ─── 11. 更新公司 ─── | ||
| 164 | - console.log('\n✏️ Phase 11: 更新公司'); | 233 | + // ─── 13. 更新公司 ─── |
| 234 | + console.log('\n✏️ Phase 13: 更新公司'); | ||
| 165 | const updated = svc.update(TEST_COMPANY_ID, { | 235 | const updated = svc.update(TEST_COMPANY_ID, { |
| 166 | status: 'running', | 236 | status: 'running', |
| 167 | heartbeat: { enabled: true }, | 237 | heartbeat: { enabled: true }, |
| @@ -169,20 +239,30 @@ async function main() { | @@ -169,20 +239,30 @@ async function main() { | ||
| 169 | assert(updated.status === 'running', '状态更新为 running'); | 239 | assert(updated.status === 'running', '状态更新为 running'); |
| 170 | assert(updated.heartbeat.enabled === true, '心跳已开启'); | 240 | assert(updated.heartbeat.enabled === true, '心跳已开启'); |
| 171 | 241 | ||
| 172 | - // ─── 12. 关停公司 ─── | ||
| 173 | - console.log('\n🛑 Phase 12: 关停公司'); | 242 | + // ─── 14. 关停公司 ─── |
| 243 | + console.log('\n🛑 Phase 14: 关停公司'); | ||
| 174 | const shutdown = svc.shutdown(TEST_COMPANY_ID); | 244 | const shutdown = svc.shutdown(TEST_COMPANY_ID); |
| 175 | assert(shutdown.status === 'shutdown', '状态变为 shutdown'); | 245 | assert(shutdown.status === 'shutdown', '状态变为 shutdown'); |
| 176 | assert(shutdown.heartbeat.enabled === false, '心跳已关闭'); | 246 | assert(shutdown.heartbeat.enabled === false, '心跳已关闭'); |
| 177 | 247 | ||
| 178 | // ─── 清理 ─── | 248 | // ─── 清理 ─── |
| 179 | cleanup(); | 249 | cleanup(); |
| 250 | + // 清理测试生成的模板 | ||
| 251 | + const testTplDir = path.join(DATA_DIR, 'templates', 'hot-topic-hunter'); | ||
| 252 | + if (fs.existsSync(testTplDir)) fs.rmSync(testTplDir, { recursive: true, force: true }); | ||
| 253 | + // 恢复 registry (移除测试模板) | ||
| 254 | + const regPath = path.join(DATA_DIR, 'templates', '_registry.json'); | ||
| 255 | + if (fs.existsSync(regPath)) { | ||
| 256 | + const reg = JSON.parse(fs.readFileSync(regPath, 'utf-8')); | ||
| 257 | + const cleaned = reg.filter(t => t.id !== 'hot-topic-hunter'); | ||
| 258 | + fs.writeFileSync(regPath, JSON.stringify(cleaned, null, 2), 'utf-8'); | ||
| 259 | + } | ||
| 180 | 260 | ||
| 181 | // ─── 结果 ─── | 261 | // ─── 结果 ─── |
| 182 | console.log(`\n${'═'.repeat(50)}`); | 262 | console.log(`\n${'═'.repeat(50)}`); |
| 183 | console.log(` 📊 测试结果: ${passed} 通过, ${failed} 失败 (共 ${passed + failed})`); | 263 | console.log(` 📊 测试结果: ${passed} 通过, ${failed} 失败 (共 ${passed + failed})`); |
| 184 | if (failed === 0) { | 264 | if (failed === 0) { |
| 185 | - console.log(' 🎉 ALL PASSED — Phase 1 生产就绪!\n'); | 265 | + console.log(' 🎉 ALL PASSED — Phase 1 + Phase 2 生产就绪!\n'); |
| 186 | } else { | 266 | } else { |
| 187 | console.log(` ⚠️ ${failed} 个测试未通过\n`); | 267 | console.log(` ⚠️ ${failed} 个测试未通过\n`); |
| 188 | process.exit(1); | 268 | process.exit(1); |
| @@ -194,3 +274,4 @@ main().catch(err => { | @@ -194,3 +274,4 @@ main().catch(err => { | ||
| 194 | cleanup(); | 274 | cleanup(); |
| 195 | process.exit(1); | 275 | process.exit(1); |
| 196 | }); | 276 | }); |
| 277 | + |
server/services/company-service.js
| @@ -197,6 +197,182 @@ class CompanyService { | @@ -197,6 +197,182 @@ class CompanyService { | ||
| 197 | } | 197 | } |
| 198 | 198 | ||
| 199 | /** | 199 | /** |
| 200 | + * 用 AgentScaffold 直接创建 Agent 骨架到公司 | ||
| 201 | + * (Phase 2: 不走 HTTP,直接调用脚手架) | ||
| 202 | + * | ||
| 203 | + * @param {string} companyId | ||
| 204 | + * @param {string} roleName | ||
| 205 | + * @param {object} opts - { type, name, description, parentId } | ||
| 206 | + * @returns {Promise<object>} { agentDir, agentId } | ||
| 207 | + */ | ||
| 208 | + async scaffoldNewAgent(companyId, roleName, opts = {}) { | ||
| 209 | + const company = this.get(companyId); | ||
| 210 | + if (!company) throw new Error(`公司 ${companyId} 不存在`); | ||
| 211 | + | ||
| 212 | + const companyAgentsDir = path.join(DATA_DIR, companyId, 'agents'); | ||
| 213 | + if (!fs.existsSync(companyAgentsDir)) { | ||
| 214 | + fs.mkdirSync(companyAgentsDir, { recursive: true }); | ||
| 215 | + } | ||
| 216 | + | ||
| 217 | + // 使用 ESM 导入 AgentScaffold | ||
| 218 | + const { AgentScaffold } = await import('../../packages/agent-core/AgentScaffold.js'); | ||
| 219 | + const scaffold = new AgentScaffold(companyAgentsDir); | ||
| 220 | + | ||
| 221 | + const agentDir = await scaffold.create({ | ||
| 222 | + id: roleName, | ||
| 223 | + type: opts.type || 'skill', | ||
| 224 | + name: opts.name || roleName, | ||
| 225 | + parentId: opts.parentId || company.ceo || 'ceo', | ||
| 226 | + goalId: opts.goalId || null, | ||
| 227 | + }); | ||
| 228 | + | ||
| 229 | + // 注入公司上下文 | ||
| 230 | + this._injectCompanyContext(companyId, roleName, opts); | ||
| 231 | + | ||
| 232 | + // 标记为 training 状态 | ||
| 233 | + const configPath = path.join(agentDir, 'config.json'); | ||
| 234 | + if (fs.existsSync(configPath)) { | ||
| 235 | + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); | ||
| 236 | + config.status = 'training'; | ||
| 237 | + config.companyId = companyId; | ||
| 238 | + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); | ||
| 239 | + } | ||
| 240 | + | ||
| 241 | + // 更新公司 | ||
| 242 | + if (!company.agents.includes(roleName)) { | ||
| 243 | + company.agents.push(roleName); | ||
| 244 | + } | ||
| 245 | + company.updatedAt = new Date().toISOString(); | ||
| 246 | + this._saveCompany(company); | ||
| 247 | + | ||
| 248 | + console.log(`[Company] 🎓 ${companyId} 创建新 Agent 骨架: ${roleName} (training)`); | ||
| 249 | + return { agentDir, agentId: roleName }; | ||
| 250 | + } | ||
| 251 | + | ||
| 252 | + /** | ||
| 253 | + * 将训练合格的 Agent 沉淀为模板 | ||
| 254 | + * (Phase 2: Benchmark ≥ 95 后调用) | ||
| 255 | + * | ||
| 256 | + * @param {string} companyId | ||
| 257 | + * @param {string} agentId | ||
| 258 | + * @param {number} benchmarkScore | ||
| 259 | + * @returns {object} { status, templateId } | ||
| 260 | + */ | ||
| 261 | + depositTemplate(companyId, agentId, benchmarkScore) { | ||
| 262 | + if (benchmarkScore < 95) { | ||
| 263 | + return { | ||
| 264 | + status: 'rejected', | ||
| 265 | + message: `评分 ${benchmarkScore} 未达标 (需 ≥ 95)`, | ||
| 266 | + agentId, | ||
| 267 | + }; | ||
| 268 | + } | ||
| 269 | + | ||
| 270 | + const agentDir = path.join(DATA_DIR, companyId, 'agents', agentId); | ||
| 271 | + if (!fs.existsSync(agentDir)) { | ||
| 272 | + throw new Error(`Agent ${agentId} 在 ${companyId} 中不存在`); | ||
| 273 | + } | ||
| 274 | + | ||
| 275 | + const templateDir = path.join(TEMPLATES_DIR, agentId); | ||
| 276 | + // 如果模板已存在且分数更低,覆盖 | ||
| 277 | + if (fs.existsSync(templateDir)) { | ||
| 278 | + fs.rmSync(templateDir, { recursive: true, force: true }); | ||
| 279 | + } | ||
| 280 | + | ||
| 281 | + // 只复制骨架文件到模板 (不含 memory/workspace/conversations) | ||
| 282 | + fs.mkdirSync(templateDir, { recursive: true }); | ||
| 283 | + for (const file of TEMPLATE_FILES) { | ||
| 284 | + const srcFile = path.join(agentDir, file); | ||
| 285 | + if (fs.existsSync(srcFile)) { | ||
| 286 | + // 复制模板时去掉公司特定信息 (对 config.json 特殊处理) | ||
| 287 | + if (file === 'config.json') { | ||
| 288 | + const config = JSON.parse(fs.readFileSync(srcFile, 'utf-8')); | ||
| 289 | + delete config.companyId; | ||
| 290 | + config.status = 'ready'; | ||
| 291 | + config.benchmarkScore = benchmarkScore; | ||
| 292 | + config.trainedAt = new Date().toISOString().slice(0, 10); | ||
| 293 | + fs.writeFileSync(path.join(templateDir, file), JSON.stringify(config, null, 2), 'utf-8'); | ||
| 294 | + } else { | ||
| 295 | + fs.copyFileSync(srcFile, path.join(templateDir, file)); | ||
| 296 | + } | ||
| 297 | + } | ||
| 298 | + } | ||
| 299 | + | ||
| 300 | + // 复制 skills/ 目录 | ||
| 301 | + const srcSkills = path.join(agentDir, 'skills'); | ||
| 302 | + if (fs.existsSync(srcSkills)) { | ||
| 303 | + this._copyDirRecursive(srcSkills, path.join(templateDir, 'skills')); | ||
| 304 | + } | ||
| 305 | + | ||
| 306 | + // 更新模板注册表 | ||
| 307 | + const registryPath = path.join(TEMPLATES_DIR, '_registry.json'); | ||
| 308 | + let registry = []; | ||
| 309 | + if (fs.existsSync(registryPath)) { | ||
| 310 | + registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8')); | ||
| 311 | + } | ||
| 312 | + // 移除旧条目 (如果存在) | ||
| 313 | + registry = registry.filter(t => t.id !== agentId); | ||
| 314 | + const agentConfig = this._readAgentConfig(templateDir); | ||
| 315 | + registry.push({ | ||
| 316 | + id: agentId, | ||
| 317 | + name: agentConfig?.name || agentId, | ||
| 318 | + type: agentConfig?.type || 'skill', | ||
| 319 | + description: agentConfig?.description || '', | ||
| 320 | + benchmarkScore, | ||
| 321 | + trainedAt: new Date().toISOString().slice(0, 10), | ||
| 322 | + usedCount: 0, | ||
| 323 | + }); | ||
| 324 | + fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf-8'); | ||
| 325 | + | ||
| 326 | + // 将 Agent 状态从 training → active | ||
| 327 | + const configPath = path.join(agentDir, 'config.json'); | ||
| 328 | + if (fs.existsSync(configPath)) { | ||
| 329 | + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); | ||
| 330 | + config.status = 'active'; | ||
| 331 | + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); | ||
| 332 | + } | ||
| 333 | + | ||
| 334 | + console.log(`[Company] 📦 Agent ${agentId} 沉淀为模板 (benchmark: ${benchmarkScore})`); | ||
| 335 | + return { status: 'deposited', templateId: agentId, benchmarkScore }; | ||
| 336 | + } | ||
| 337 | + | ||
| 338 | + /** | ||
| 339 | + * 完整招聘流程 (Phase 2: 模板优先 → 骨架创建兜底) | ||
| 340 | + * 如果模板不存在,会创建骨架并返回 training 状态 | ||
| 341 | + * 上层(路由)负责异步触发 agent-creator + innovator 实际训练 | ||
| 342 | + * | ||
| 343 | + * @param {string} companyId | ||
| 344 | + * @param {string} roleName | ||
| 345 | + * @param {object} opts - { type, name, description, customAgent, customSoul, customUser } | ||
| 346 | + * @returns {Promise<object>} | ||
| 347 | + */ | ||
| 348 | + async hireWithTraining(companyId, roleName, opts = {}) { | ||
| 349 | + // 先走标准招聘流程 | ||
| 350 | + const result = this.hire(companyId, roleName, opts); | ||
| 351 | + | ||
| 352 | + if (result.status !== 'needs_training') { | ||
| 353 | + return result; | ||
| 354 | + } | ||
| 355 | + | ||
| 356 | + // 模板不存在 → 创建骨架 (Phase 2) | ||
| 357 | + const scaffold = await this.scaffoldNewAgent(companyId, roleName, { | ||
| 358 | + type: opts.type || 'skill', | ||
| 359 | + name: opts.name || roleName, | ||
| 360 | + description: opts.description || '', | ||
| 361 | + parentId: opts.parentId, | ||
| 362 | + customAgent: opts.customAgent, | ||
| 363 | + customSoul: opts.customSoul, | ||
| 364 | + customUser: opts.customUser, | ||
| 365 | + }); | ||
| 366 | + | ||
| 367 | + return { | ||
| 368 | + status: 'training', | ||
| 369 | + agentId: roleName, | ||
| 370 | + agentDir: scaffold.agentDir, | ||
| 371 | + message: `Agent "${roleName}" 骨架已创建,状态: training。需要 agent-creator 定义 + innovator 训练至 ≥95 后才可上岗。`, | ||
| 372 | + }; | ||
| 373 | + } | ||
| 374 | + | ||
| 375 | + /** | ||
| 200 | * 解雇 (suspend) 公司内的 Agent | 376 | * 解雇 (suspend) 公司内的 Agent |
| 201 | */ | 377 | */ |
| 202 | fire(companyId, agentId) { | 378 | fire(companyId, agentId) { |