p3-demo-setup.js 6.73 KB
#!/usr/bin/env node
/**
 * P3 执行控制台 · 手工测试准备脚本
 *
 *   node scripts/p3-demo-setup.js <companyId> [--task "自然语言任务"] [--auto]
 *
 * 做三件事,顺序不能换:
 *   1. 雇齐 agent —— 先 org 节点、再 work graph 节点本体
 *   2. 写 org graph(三节点 + 两条 fail 边)
 *   3. 起一个 run(默认 manual,便于在界面上一步步点)
 *
 * 跑完打开 http://localhost:5173/<companyId>/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 <companyId> [--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); });