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 | 6 | "description": "公司战略规划、团队管理、OKR 制定、任务委派", |
| 7 | 7 | "benchmarkScore": 97, |
| 8 | 8 | "trainedAt": "2026-03-20", |
| 9 | - "usedCount": 1 | |
| 9 | + "usedCount": 2 | |
| 10 | 10 | }, |
| 11 | 11 | { |
| 12 | 12 | "id": "qa-judge", |
| ... | ... | @@ -15,6 +15,6 @@ |
| 15 | 15 | "description": "质量验收、OKR 进度审查、标准化仲裁", |
| 16 | 16 | "benchmarkScore": 95, |
| 17 | 17 | "trainedAt": "2026-03-25", |
| 18 | - "usedCount": 1 | |
| 18 | + "usedCount": 2 | |
| 19 | 19 | } |
| 20 | 20 | ] |
| 21 | 21 | \ No newline at end of file | ... | ... |
server/routes/company.js
| ... | ... | @@ -68,13 +68,13 @@ router.post('/:companyId/shutdown', (req, res) => { |
| 68 | 68 | // ═══════════════════════════════════════ |
| 69 | 69 | |
| 70 | 70 | // POST /api/company/:companyId/hire |
| 71 | -router.post('/:companyId/hire', (req, res) => { | |
| 71 | +router.post('/:companyId/hire', async (req, res) => { | |
| 72 | 72 | try { |
| 73 | - const { role, customAgent, customSoul, customUser } = req.body; | |
| 73 | + const { role, type, name, description, customAgent, customSoul, customUser } = req.body; | |
| 74 | 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 | 79 | res.json(result); |
| 80 | 80 | } catch (err) { |
| ... | ... | @@ -92,6 +92,23 @@ router.post('/:companyId/fire', (req, res) => { |
| 92 | 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 | 112 | // GET /api/company/:companyId/agents |
| 96 | 113 | router.get('/:companyId/agents', (req, res) => { |
| 97 | 114 | const agents = companyService.listAgents(req.params.companyId); | ... | ... |
server/scripts/test-company-e2e.js
| ... | ... | @@ -118,26 +118,96 @@ async function main() { |
| 118 | 118 | const hireQa = svc.hire(TEST_COMPANY_ID, 'qa-judge'); |
| 119 | 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 | 198 | const hireDup = svc.hire(TEST_COMPANY_ID, 'ceo'); |
| 130 | 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 | 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 | 205 | assert(agents.some(a => a.id === 'ceo'), '列表包含 CEO'); |
| 137 | 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 | 211 | const companyAgentDir = CompanyService.resolveAgentDir(TEST_COMPANY_ID, 'ceo'); |
| 142 | 212 | assert(companyAgentDir !== null, '公司内 CEO 可定位'); |
| 143 | 213 | assert(companyAgentDir.includes(TEST_COMPANY_ID), '路径包含公司 ID'); |
| ... | ... | @@ -149,8 +219,8 @@ async function main() { |
| 149 | 219 | const notExist = CompanyService.resolveAgentDir(TEST_COMPANY_ID, 'nonexistent-agent'); |
| 150 | 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 | 224 | const fireResult = svc.fire(TEST_COMPANY_ID, 'qa-judge'); |
| 155 | 225 | assert(fireResult.status === 'suspended', '解雇返回 suspended'); |
| 156 | 226 | |
| ... | ... | @@ -160,8 +230,8 @@ async function main() { |
| 160 | 230 | const firedConfig = JSON.parse(fs.readFileSync(path.join(TEST_COMPANY_DIR, 'agents', 'qa-judge', 'config.json'), 'utf-8')); |
| 161 | 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 | 235 | const updated = svc.update(TEST_COMPANY_ID, { |
| 166 | 236 | status: 'running', |
| 167 | 237 | heartbeat: { enabled: true }, |
| ... | ... | @@ -169,20 +239,30 @@ async function main() { |
| 169 | 239 | assert(updated.status === 'running', '状态更新为 running'); |
| 170 | 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 | 244 | const shutdown = svc.shutdown(TEST_COMPANY_ID); |
| 175 | 245 | assert(shutdown.status === 'shutdown', '状态变为 shutdown'); |
| 176 | 246 | assert(shutdown.heartbeat.enabled === false, '心跳已关闭'); |
| 177 | 247 | |
| 178 | 248 | // ─── 清理 ─── |
| 179 | 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 | 262 | console.log(`\n${'═'.repeat(50)}`); |
| 183 | 263 | console.log(` 📊 测试结果: ${passed} 通过, ${failed} 失败 (共 ${passed + failed})`); |
| 184 | 264 | if (failed === 0) { |
| 185 | - console.log(' 🎉 ALL PASSED — Phase 1 生产就绪!\n'); | |
| 265 | + console.log(' 🎉 ALL PASSED — Phase 1 + Phase 2 生产就绪!\n'); | |
| 186 | 266 | } else { |
| 187 | 267 | console.log(` ⚠️ ${failed} 个测试未通过\n`); |
| 188 | 268 | process.exit(1); |
| ... | ... | @@ -194,3 +274,4 @@ main().catch(err => { |
| 194 | 274 | cleanup(); |
| 195 | 275 | process.exit(1); |
| 196 | 276 | }); |
| 277 | + | ... | ... |
server/services/company-service.js
| ... | ... | @@ -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 | 376 | * 解雇 (suspend) 公司内的 Agent |
| 201 | 377 | */ |
| 202 | 378 | fire(companyId, agentId) { | ... | ... |