#!/usr/bin/env node /** * P3 执行控制台 · 手工测试准备脚本 * * node scripts/p3-demo-setup.js [--task "自然语言任务"] [--auto] * * 做三件事,顺序不能换: * 1. 雇齐 agent —— 先 org 节点、再 work graph 节点本体 * 2. 写 org graph(三节点 + 两条 fail 边) * 3. 起一个 run(默认 manual,便于在界面上一步步点) * * 跑完打开 http://localhost:5173//graph-console 即可开始点。 * * ── 为什么需要这个脚本(两个已知缺口的绕行)──────────────────────────── * ① work graph 节点本体不会被自动雇:graph-engine.resolveAgentName 只检查 * data/companies/{cid}/agents/{id} 存不存在,**不做按需 hire**(同文件的 * resolvePlanningAgentName 对 workgraph-planning 倒是做了)。本体不在 * system/agents/ 下,节点就会在 2ms 内 fail 且不起任何子进程。 * ② 顺序不能换:company-service.hire() 会把**任何**被雇的 agent upsert 成一行 * org_graph_nodes,而 POST /org-graph 会把不在节点列表里的行标 removed。 * 先写图再雇人 → 左栏 org graph 会混进 prd-planning 这些 work 层本体。 * * 两个缺口修好后本脚本的第 1 步就可以删掉,只留第 2、3 步。 */ const PORT = process.env.PORT || 3010; const BASE = `http://127.0.0.1:${PORT}`; const ORG_NODES = ['org-planner', 'org-coder', 'org-validator']; // 与 fixtures/devloop-mock/workgraph-planning.json 规划出的图对应 const WORK_BODIES = ['prd-planning', 'sdd-planning', 'coder-validator']; const ORG_GRAPH = { nodes: ORG_NODES.map((roleName) => ({ roleName })), edges: [ { from: 'org-planner', to: 'org-coder', condition: 'success' }, { from: 'org-coder', to: 'org-validator', condition: 'success' }, // 归因为 input_underspecified 时退回规划;其余一律走这条无 failReason 的兜底边 // (§三.7 第五项 fail-fallback-edge 要求每个有 fail 出边的节点必须留一条兜底) { from: 'org-validator', to: 'org-planner', condition: 'fail', failReason: 'input_underspecified', edgeKind: 'backward' }, { from: 'org-validator', to: 'org-coder', condition: 'fail', edgeKind: 'backward' }, ], }; async function post(path, body) { const res = await fetch(BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const text = await res.text(); let json = null; try { json = JSON.parse(text); } catch { /* 非 JSON 原样回报 */ } return { status: res.status, json, text }; } async function main() { const args = process.argv.slice(2); const companyId = args.find((a) => !a.startsWith('--')); if (!companyId) { console.error('用法: node scripts/p3-demo-setup.js [--task "..."] [--auto]'); process.exit(1); } const taskIdx = args.indexOf('--task'); const task = taskIdx >= 0 ? args[taskIdx + 1] : '实现一个最小可运行的接口,错误输入要有明确响应'; const mode = args.includes('--auto') ? 'auto' : 'manual'; // 0. server 活着吗 try { const h = await fetch(`${BASE}/api/health`); if (!h.ok) throw new Error(`health ${h.status}`); } catch (e) { console.error(`❌ 连不上 ${BASE} —— 先起服务:`); console.error(' LEMONBOSS_CLAUDE_MODE=mock PORT=3010 node server/index.js'); process.exit(1); } console.log(`\n公司 ${companyId} · 模式 ${mode}\n`); // 1. 雇齐(先 org 节点、再 work 本体;顺序见文件头说明) console.log('① 雇 agent'); for (const role of [...ORG_NODES, ...WORK_BODIES]) { const r = await post(`/api/company/${companyId}/hire`, { role }); const s = r.json?.status || r.json?.error || r.text.slice(0, 60); console.log(` ${role.padEnd(18)} ${s}`); } // 2. 写 org graph(放在雇人之后:hire 会 upsert org_graph_nodes, // 这一步顺带把 work 层本体那几行标 removed,左栏才干净) console.log('\n② 写 org graph'); const g = await post(`/api/company/${companyId}/org-graph`, ORG_GRAPH); if (!g.json?.ok) { console.error(' ❌ 写入失败:', g.json?.errors || g.text.slice(0, 300)); process.exit(1); } console.log(` ✅ ${g.json.nodeCount} 节点 / ${g.json.edgeCount} 边`); // 3. 起 run // §一非目标定的 v1 约束:一家公司同时只有一个 active root run // (idx_graph_runs_one_active_root 按 company 建的部分唯一索引)。 // 所以重复跑本脚本会被拒——除非先把上一个 run 停掉。--fresh 就是干这个的。 console.log('\n③ 起 run'); if (args.includes('--fresh')) { const list = await fetch(`${BASE}/api/company/${companyId}/graph-runs`).then((r) => r.json()).catch(() => null); const active = (list?.runs || []).filter( (r) => !r.parent_run_id && ['ready', 'running', 'paused', 'blocked'].includes(r.run_status) ); for (const r of active) { await post(`/api/graph-runs/${r.id}/stop`, {}); console.log(` ⏹ 已停掉旧 run ${r.id}(原状态 ${r.run_status})`); } } const t = await post(`/api/company/${companyId}/tasks`, { task, mode }); if (!t.json?.runId) { const msg = t.json?.error || t.text.slice(0, 300); console.error(' ❌ 失败:', msg); if (/已有进行中的任务|active root run/.test(msg)) { const list = await fetch(`${BASE}/api/company/${companyId}/graph-runs`).then((r) => r.json()).catch(() => null); const active = (list?.runs || []).find((r) => !r.parent_run_id && ['ready', 'running', 'paused', 'blocked'].includes(r.run_status)); if (active) { console.error(`\n 现有 run: ${active.id}(${active.run_status})`); console.error(' 直接接着点它,或者加 --fresh 重开一个:'); console.error(` node scripts/p3-demo-setup.js ${companyId} --fresh`); console.error(`\n 控制台:http://localhost:5173/${companyId}/graph-console`); } } process.exit(1); } console.log(` ✅ runId ${t.json.runId} · ${t.json.executionMode}`); console.log(`\n打开控制台:http://localhost:5173/${companyId}/graph-console`); if (mode === 'manual') { console.log('\n点法:'); console.log(' 左栏「下一步」→ org-planner 起子图'); console.log(' 点 org-planner 节点 → 下钻,中栏出现它的 work graph'); console.log(' 中栏「下一步」×3 → 第 3 次触发回退,徽标 0/3 → 1/3'); console.log(' 点 wg-check → 右栏「概览」看回退记录与 remediation_note'); console.log(' 中栏「下一步」×2 → work graph 收敛,父节点结算,左栏前移\n'); } } main().catch((e) => { console.error(e); process.exit(1); });