Commit 2c2cde8542c0d97aaba9ddcc2c5bb8522ba98d16

Authored by yibo
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)
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) {