@bolloon/bolloon-agent 0.4.24 → 0.4.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/agents/execution-supervisor.js +446 -0
  2. package/dist/agents/external-events.js +162 -0
  3. package/dist/agents/goal-criteria.js +124 -0
  4. package/dist/agents/goal-store.js +526 -0
  5. package/dist/agents/pi-harness.js +263 -0
  6. package/dist/agents/pi-sdk.js +607 -126
  7. package/dist/agents/run-store.js +772 -0
  8. package/dist/agents/runner-resolver.js +225 -0
  9. package/dist/agents/skill-readiness.js +133 -0
  10. package/dist/agents/skill-supervisor-link.js +70 -0
  11. package/dist/agents/skills-manager.js +717 -0
  12. package/dist/agents/supervisor-host.js +249 -0
  13. package/dist/cli/setup-wizard.js +96 -127
  14. package/dist/cron/tick-lock.js +1 -1
  15. package/dist/electron/first-run.js +33 -2
  16. package/dist/electron-build/electron/first-run.js +35 -2
  17. package/dist/electron-build/electron/first-run.js.map +1 -1
  18. package/dist/index.js +549 -26
  19. package/dist/ios/agent-delegate-server.js +58 -12
  20. package/dist/ios/icons/icon-1024x1024.png +0 -0
  21. package/dist/ios/icons/icon-1024x1024.webp +0 -0
  22. package/dist/ios/icons/icon-216x216.png +0 -0
  23. package/dist/ios/icons/icon-216x216.webp +0 -0
  24. package/dist/ios/index.html +21 -1
  25. package/dist/ios/manifest.json +1 -1
  26. package/dist/ios/mobile-agent.js +195 -1
  27. package/dist/ios/mobile-core.js +24876 -24723
  28. package/dist/ios/mobile.css +15 -0
  29. package/dist/ios/mobile.html +21 -1
  30. package/dist/ios/mobile.js +143 -0
  31. package/dist/ios/server.js +51 -4
  32. package/dist/llm/config-store.js +35 -4
  33. package/dist/network/agent-network.js +10 -0
  34. package/dist/network/goal-event-bridge.js +57 -0
  35. package/dist/setup/onboard.js +549 -0
  36. package/dist/setup/setup-store.js +592 -0
  37. package/dist/web/icons/icon-1024x1024.png +0 -0
  38. package/dist/web/icons/icon-1024x1024.webp +0 -0
  39. package/dist/web/icons/icon-216x216.png +0 -0
  40. package/dist/web/icons/icon-216x216.webp +0 -0
  41. package/dist/web/manifest.json +1 -1
  42. package/dist/web/mobile-agent.js +2 -2
  43. package/dist/web/mobile-core.js +24884 -24726
  44. package/dist/web/mobile-privacy.js +185 -0
  45. package/dist/web/mobile.css +15 -0
  46. package/dist/web/mobile.html +21 -1
  47. package/dist/web/mobile.js +179 -6
  48. package/dist/web/server.js +633 -0
  49. package/package.json +2 -2
@@ -1530,6 +1530,11 @@ async function getAgentForChannel(channelId, channelDid, channelName, channelDid
1530
1530
  usePivotLoop: true,
1531
1531
  }, true); // forceNew: true 强制创建新实例
1532
1532
  channelSessions.set(sessionKey, session);
1533
+ // 2026-09-16: 标记运行表面 — 落盘 run 记录里能区分 web 运行 (CLI 默认 cli)
1534
+ try {
1535
+ session.setRunSurface?.('web');
1536
+ }
1537
+ catch { /* 老 session 无此方法则忽略 */ }
1533
1538
  if (channelDid) {
1534
1539
  console.log(`[Agent] 新建频道 ${channelId} session, DID = ${channelDid}, sessionId = ${currentSessionId}`);
1535
1540
  }
@@ -1626,6 +1631,104 @@ export async function createWebServer(port = 3000, options = {}) {
1626
1631
  catch (err) {
1627
1632
  console.warn('[createWebServer] bootstrap 失败 (非致命):', err);
1628
1633
  }
1634
+ // 2026-09-16: 孤儿运行对账 — 上次进程留下的 running 记录改判 interrupted。
1635
+ // 不做这步的话, 刷新/重启后 UI 会显示"还在跑"的幽灵运行 (最典型的假状态)。
1636
+ try {
1637
+ const { reconcileOrphans } = await import('../agents/run-store.js');
1638
+ const rec = await reconcileOrphans();
1639
+ if (rec.interrupted.length) {
1640
+ console.log(`[runs] 对账: ${rec.interrupted.length} 条上次中断的运行已标 interrupted (${rec.interrupted.slice(0, 3).join(', ')}${rec.interrupted.length > 3 ? '…' : ''})`);
1641
+ }
1642
+ if (rec.stillRunning.length)
1643
+ console.log(`[runs] 存活运行: ${rec.stillRunning.length} 条`);
1644
+ }
1645
+ catch (err) {
1646
+ console.warn('[runs] 对账失败 (非致命):', err?.message);
1647
+ }
1648
+ // 2026-09-16 (M2-B): ExecutionSupervisor —— 长期执行层。
1649
+ // 页面关掉、进程重启后由它把 Goal 继续下去; 跨进程排他靠 Goal lease (不是内存状态)。
1650
+ // Harness 管"这一段能不能安全执行", Supervisor 管"这个目标还要不要继续执行"。
1651
+ if (process.env.BOLLOON_SUPERVISOR !== '0') {
1652
+ try {
1653
+ const { getSupervisor } = await import('../agents/execution-supervisor.js');
1654
+ const { runSupervisorHost } = await import('../agents/supervisor-host.js');
1655
+ const tickMs = Number(process.env.BOLLOON_SUPERVISOR_TICK_MS) || 30_000;
1656
+ const leaseMs = Number(process.env.BOLLOON_SUPERVISOR_LEASE_MS) || 90_000;
1657
+ // 执行器**在每次执行前解析** (不写死): web 宿主按 Goal 的 channelId 找 channel agent。
1658
+ // 解析不出来 → 只诊断不执行, 且不写任何 Goal 状态。
1659
+ const resolver = async (req) => {
1660
+ const goal = req.goal;
1661
+ if (!goal?.channelId) {
1662
+ return { ok: false, kind: 'none', reason: 'Goal 没有 channelId: 无法在 web 进程内解析执行器' };
1663
+ }
1664
+ const agent = await getAgentForChannel(goal.channelId, goal.agentId || '', undefined, undefined);
1665
+ if (!agent)
1666
+ return { ok: false, kind: 'none', reason: 'channel agent 不可用' };
1667
+ const runner = async (r) => {
1668
+ if (r.kind === 'resume' && r.prevRunId && typeof agent.resumeRun === 'function') {
1669
+ const res = await agent.resumeRun(r.prevRunId);
1670
+ return { runId: r.prevRunId, status: res?.ok ? 'done' : 'failed', error: res?.ok ? undefined : res?.reason };
1671
+ }
1672
+ // 跨预算继续: 新 Run 必须挂在同一个 Goal 下 (setGoalId), 并带上上一个 Run 的非幂等守卫
1673
+ agent.setGoalId?.(goal.goalId);
1674
+ agent.setContinuationGuards?.(r.guards || []);
1675
+ const reply = await agent.prompt(r.instruction);
1676
+ // 注意: prompt 收尾会清空 currentRunId → 必须读 lastRunId (否则会拿上一条 run 做决策)
1677
+ const runId = agent.getLastRunId?.() || agent.getRunId?.();
1678
+ return { runId, status: 'done', reply: typeof reply === 'string' ? reply.slice(0, 500) : undefined };
1679
+ };
1680
+ return { ok: true, runner, kind: 'web' };
1681
+ };
1682
+ // 2026-09-16 (M4): 初始化未就绪 → Supervisor 只诊断 (不解析执行器, 不执行任何 Goal)
1683
+ let setupReadyForSupervisor = false;
1684
+ try {
1685
+ const { getSetupGateCached } = await import('../setup/setup-store.js');
1686
+ const g = await getSetupGateCached();
1687
+ setupReadyForSupervisor = g.gate === 'ready';
1688
+ if (!setupReadyForSupervisor)
1689
+ console.warn(`[supervisor] 初始化未就绪 (${g.gate}, 阶段 ${g.state.stage}) → 只诊断, 不执行 Goal`);
1690
+ }
1691
+ catch {
1692
+ console.warn('[supervisor] 初始化状态不可读 → 只诊断, 不执行 Goal');
1693
+ }
1694
+ const sup = getSupervisor({
1695
+ tickIntervalMs: tickMs,
1696
+ leaseTtlMs: leaseMs,
1697
+ maxPerTick: Number(process.env.BOLLOON_SUPERVISOR_MAX_PER_TICK) || 1,
1698
+ log: (m) => console.log(m),
1699
+ onEvent: (e) => { try {
1700
+ broadcast({ type: 'supervisor', ...e });
1701
+ }
1702
+ catch { /* UI 广播失败不影响调度 */ } },
1703
+ resolver: setupReadyForSupervisor ? resolver : undefined,
1704
+ });
1705
+ // 宿主层: 跨进程单 tick 互斥 + 宿主身份/心跳落盘 + 优雅停止
1706
+ const host = await runSupervisorHost({
1707
+ supervisor: sup,
1708
+ tickIntervalMs: tickMs,
1709
+ leaseTtlMs: leaseMs,
1710
+ runnerKind: 'web',
1711
+ log: (m) => console.log(m),
1712
+ });
1713
+ global.__bolloonSupervisorHost = host;
1714
+ console.log(`[supervisor] 长期执行层已启动 (owner=${host.state.owner}, worker=${host.state.workerId}, tick=${tickMs}ms, lease=${leaseMs}ms)`);
1715
+ const stopHost = (sig) => { void host.stop(`signal:${sig}`).finally(() => process.exit(0)); };
1716
+ process.once('SIGINT', () => stopHost('SIGINT'));
1717
+ process.once('SIGTERM', () => stopHost('SIGTERM'));
1718
+ }
1719
+ catch (err) {
1720
+ // 启动失败必须可观测 (不许静默降级成长时间不执行)
1721
+ const msg = `[supervisor] 启动失败: ${err?.message || err}`;
1722
+ console.error(msg);
1723
+ try {
1724
+ broadcast({ type: 'supervisor', kind: 'startup_failed', message: msg });
1725
+ }
1726
+ catch { /* ignore */ }
1727
+ }
1728
+ }
1729
+ else {
1730
+ console.log('[supervisor] 长期执行层已关闭 (BOLLOON_SUPERVISOR=0)');
1731
+ }
1629
1732
  // 2026-08-03 (Context OS P5): 初始化资产层 12+3 目录 (幂等, 每层 README 声明职责边界)
1630
1733
  try {
1631
1734
  const { ensureContextOsDirs } = await import('../bootstrap/context-os.js');
@@ -2439,6 +2542,19 @@ export async function createWebServer(port = 3000, options = {}) {
2439
2542
  console.log(`[cron] 调度器已启动 (每 ${Math.round((Number(process.env.BOLLOON_CRON_HEARTBEAT_MS) || 60_000) / 1000)}s 一轮, tick 带锁 + DND)`);
2440
2543
  // 暴露给 /message 使用 (主任务闸门)
2441
2544
  global.__bolloonMainTask = { enterMainTask, exitMainTask };
2545
+ // 2026-09-16: 运行失速巡检 (每 60s) — running 但长时间没新进展的运行标 stalled,
2546
+ // 让 UI/CLI 能说"这个运行卡住了", 而不是永远转圈 (crash 由启动时对账兜底)。
2547
+ setInterval(() => {
2548
+ void (async () => {
2549
+ try {
2550
+ const { superviseRuns } = await import('../agents/run-store.js');
2551
+ const r = await superviseRuns();
2552
+ if (r.stalled.length)
2553
+ console.warn(`[runs] 失速巡检: ${r.stalled.length} 条运行标 stalled (${r.stalled.slice(0, 3).join(', ')})`);
2554
+ }
2555
+ catch { /* 巡检失败不影响主流程 */ }
2556
+ })();
2557
+ }, 60_000);
2442
2558
  }
2443
2559
  catch (cronErr) {
2444
2560
  console.warn('[cron] 调度器启动失败 (non-fatal):', cronErr?.message);
@@ -2832,6 +2948,501 @@ ${goalDesc}
2832
2948
  res.status(400).json({ ok: false, severity: 'block', reason: e?.message ?? 'parse error' });
2833
2949
  }
2834
2950
  });
2951
+ // 2026-09-16: 运行记录 (持久化 harness) — 当前 + 历史 agent 运行。跨重载可读。
2952
+ // 2026-09-16 (2-F/2-H): 判据 (criteria) + 长期执行面板 API —— CLI/Web 读同一份 Goal 事实
2953
+ app.get('/api/goals/:id/criteria', async (req, res) => {
2954
+ try {
2955
+ const { longTermStatus } = await import('../agents/goal-criteria.js');
2956
+ const { readGoal } = await import('../agents/goal-store.js');
2957
+ const g = await readGoal(req.params.id);
2958
+ if (!g)
2959
+ return res.status(404).json({ error: 'goal 不存在' });
2960
+ const st = await longTermStatus(req.params.id);
2961
+ res.json({ goalId: g.goalId, objective: g.objective, status: g.status, successCriteria: g.successCriteria, completedCriteria: g.completedCriteria, criteriaSource: g.criteriaSource, criteriaConfirmed: g.criteriaConfirmed === true, criteriaVersion: g.criteriaVersion, proposedCriteria: g.proposedCriteria || null, unresolvedItems: g.unresolvedItems, evidence: (g.evidence || []).slice(-10), longTerm: st });
2962
+ }
2963
+ catch (err) {
2964
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
2965
+ }
2966
+ });
2967
+ app.post('/api/goals/:id/criteria', async (req, res) => {
2968
+ try {
2969
+ const { confirmCriteria, proposeForGoal } = await import('../agents/goal-criteria.js');
2970
+ const { readGoal } = await import('../agents/goal-store.js');
2971
+ const g = await readGoal(req.params.id);
2972
+ if (!g)
2973
+ return res.status(404).json({ error: 'goal 不存在' });
2974
+ const criteria = Array.isArray(req.body?.criteria) ? req.body.criteria.map((c) => String(c)) : undefined;
2975
+ if (req.body?.propose === true) {
2976
+ const p = await proposeForGoal(req.params.id);
2977
+ return res.json({ ok: p.ok, proposed: p.criteria, needsHuman: !!p.needsHuman, reason: p.reason || null });
2978
+ }
2979
+ // 默认: 确认 (可同时改内容)
2980
+ const r = await confirmCriteria(req.params.id, { criteria, by: 'web' });
2981
+ res.status(r.ok ? 200 : 409).json({ ok: r.ok, reason: r.reason || null, criteria: r.goal?.successCriteria, criteriaVersion: r.goal?.criteriaVersion });
2982
+ }
2983
+ catch (err) {
2984
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
2985
+ }
2986
+ });
2987
+ // 长期执行面板 (纯静态页, 直接打已有 API; 未 ready 时 chat 路由本来就 503)
2988
+ app.get('/goals', (_req, res) => {
2989
+ res.type('html').send(`<!doctype html><meta charset="utf-8"><title>Bolloon 长期执行</title>
2990
+ <style>body{font-family:system-ui,-apple-system,"PingFang SC",sans-serif;max-width:1000px;margin:32px auto;padding:0 16px;line-height:1.55;color:#181818}
2991
+ h1{font-size:20px}h2{font-size:15px;margin-top:22px}table{border-collapse:collapse;width:100%;font-size:13px}th,td{border-bottom:1px solid #eee;padding:6px 8px;text-align:left;vertical-align:top}
2992
+ .ok{color:#0a7d32}.bad{color:#b00020}.dim{color:#666}button{font-size:12px;padding:4px 8px;margin-right:4px}code{background:#f6f6f6;padding:1px 4px;border-radius:4px}</style>
2993
+ <h1>长期执行面板</h1>
2994
+ <p class="dim">数据 = <code>/api/goals</code> · <code>/api/runs</code> · <code>/api/goals/:id/criteria</code> · <code>/api/supervisor</code> (与 CLI 同一份事实)</p>
2995
+ <div id="sup"></div>
2996
+ <h2>目标 (Goals)</h2><table id="goals"><thead><tr><th>goalId</th><th>状态</th><th>判据</th><th>下一动作</th><th>Runs</th><th>操作</th></tr></thead><tbody></tbody></table>
2997
+ <h2>运行 (Runs, 最近 20)</h2><table id="runs"><thead><tr><th>runId</th><th>goalId</th><th>状态</th><th>步数</th><th>恢复</th><th>操作</th></tr></thead><tbody></tbody></table>
2998
+ <pre id="log" class="dim"></pre>
2999
+ <script>
3000
+ const log = (m) => { document.getElementById('log').textContent = typeof m === 'string' ? m : JSON.stringify(m, null, 1); };
3001
+ async function j(u){ const r = await fetch(u); return await r.json(); }
3002
+ async function view(id){
3003
+ const c = await j('/api/goals/'+id+'/criteria');
3004
+ log(c);
3005
+ }
3006
+ async function act(id, what){
3007
+ if (what === 'confirm') { const r = await fetch('/api/goals/'+id+'/criteria',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({})}); log(await r.json()); }
3008
+ else if (what === 'propose') { const r = await fetch('/api/goals/'+id+'/criteria',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({propose:true})}); log(await r.json()); }
3009
+ else if (what === 'wake') { const r = await fetch('/api/goals/'+id+'/wake',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({force:true})}); log(await r.json()); }
3010
+ else if (what === 'resume') { const r = await fetch('/api/runs/'+id+'/resume',{method:'POST'}); log(await r.json()); }
3011
+ else if (what === 'pause') { const r = await fetch('/api/runs/'+id+'/pause',{method:'POST'}); log(await r.json()); }
3012
+ else if (what === 'abort') { const r = await fetch('/api/runs/'+id+'/abort',{method:'POST'}); log(await r.json()); }
3013
+ refresh();
3014
+ }
3015
+ async function refresh(){
3016
+ try {
3017
+ const sup = await j('/api/supervisor');
3018
+ document.getElementById('sup').innerHTML = '<b>Supervisor</b> owner='+(sup.state?.owner||'-')+' worker='+(sup.state?.workerId||'-')+' ticks='+(sup.ticks||0)+
3019
+ ' <span class="dim">lease='+(sup.leaseTtlMs||'-')+'ms tick='+(sup.tickIntervalMs||'-')+'ms</span>';
3020
+ } catch(e){ document.getElementById('sup').textContent = 'supervisor 不可用: '+e; }
3021
+ const gs = await j('/api/goals');
3022
+ const gtb = document.querySelector('#goals tbody'); gtb.innerHTML = '';
3023
+ for (const g of (gs.goals || [])) {
3024
+ let crit = g.successCriteria?.length ? (g.completedCriteria?.length||0)+'/'+g.successCriteria.length : '<span class="bad">无判据</span>';
3025
+ const src = g.criteriaSource ? ' <span class="dim">('+g.criteriaSource+(g.criteriaConfirmed?'✓':'?')+')</span>' : '';
3026
+ const tr = document.createElement('tr');
3027
+ tr.innerHTML = '<td><code>'+g.goalId+'</code></td><td>'+g.status+'</td><td>'+crit+src+'</td><td class="dim">'+
3028
+ ((g.continuation&&(g.continuation.nextAction||g.continuation.wakeReason))||'-')+'</td><td>'+((g.runs||[]).length)+'</td>'+
3029
+ '<td><button onclick="view(\''+g.goalId+'\')">详情</button><button onclick="act(\''+g.goalId+'\',\'confirm\')">确认判据</button><button onclick="act(\''+g.goalId+'\',\'propose\')">提候选</button><button onclick="act(\''+g.goalId+'\',\'wake\')">唤醒</button></td>';
3030
+ gtb.appendChild(tr);
3031
+ }
3032
+ const rs = await j('/api/runs');
3033
+ const rtb = document.querySelector('#runs tbody'); rtb.innerHTML = '';
3034
+ for (const r of (rs.runs || []).slice(-20).reverse()) {
3035
+ const tr = document.createElement('tr');
3036
+ tr.innerHTML = '<td><code>'+String(r.runId).slice(0,14)+'</code></td><td class="dim">'+(r.goalId||'-')+'</td><td>'+r.status+'</td><td>'+((r.steps||[]).length)+'</td><td>'+((r.recovery||[]).length)+'</td>'+
3037
+ '<td><button onclick="act(\''+r.runId+'\',\'resume\')">resume</button><button onclick="act(\''+r.runId+'\',\'pause\')">pause</button><button onclick="act(\''+r.runId+'\',\'abort\')">abort</button></td>';
3038
+ rtb.appendChild(tr);
3039
+ }
3040
+ }
3041
+ refresh(); setInterval(refresh, 5000);
3042
+ </script>`);
3043
+ });
3044
+ // 2026-09-16 (Phase 4): Onboard API —— 与 CLI 共用同一条执行器 (src/setup/onboard.ts)
3045
+ // 页面只负责"提交本步输入", 阶段判定/校验/真实验证/原子提交/门禁全在服务端同一份事实里。
3046
+ const setupState = async () => {
3047
+ const { evaluateSetup } = await import('../setup/setup-store.js');
3048
+ const { nextStepInfo } = await import('../setup/onboard.js');
3049
+ const ev = await evaluateSetup();
3050
+ const next = ev.gate === 'ready' ? null : await nextStepInfo(ev.state).catch(() => null);
3051
+ return { gate: ev.gate, stage: ev.state.stage, readiness: ev.state.readiness, readinessWhy: ev.state.readinessWhy,
3052
+ allow: ev.state.allow, completed: ev.state.completed, inputs: ev.state.inputs, checks: ev.state.checks,
3053
+ lastError: ev.state.lastError || null, actions: ev.state.actions, reasons: ev.reasons, nextStep: next };
3054
+ };
3055
+ app.get('/api/setup', async (_req, res) => {
3056
+ try {
3057
+ res.json(await setupState());
3058
+ }
3059
+ catch (err) {
3060
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
3061
+ }
3062
+ });
3063
+ // 跑一步 (answers 按顺序喂给执行器; oneShot: 一步失败就返回, 不阻塞)
3064
+ const runSetupStep = async (req, res, mode) => {
3065
+ try {
3066
+ const { runOnboard, ScriptedIO } = await import('../setup/onboard.js');
3067
+ const answers = Array.isArray(req.body?.answers) ? req.body.answers.map((x) => String(x ?? '')) : [];
3068
+ const io = new ScriptedIO(answers);
3069
+ const r = await runOnboard({ mode, io, oneShot: true, targets: req.body?.targets });
3070
+ res.json({ ...(await setupState()), ok: r.ok, failedStage: r.failedStage || null, errorClass: r.errorClass || null,
3071
+ message: r.message || null, steps: r.steps, stepLog: io.log.slice(-40), summary: r.summary });
3072
+ }
3073
+ catch (err) {
3074
+ res.status(500).json({ ok: false, error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
3075
+ }
3076
+ };
3077
+ app.post('/api/setup/start', async (_req, res) => { try {
3078
+ res.json(await setupState());
3079
+ }
3080
+ catch (err) {
3081
+ res.status(500).json({ error: String(err) });
3082
+ } });
3083
+ app.post('/api/setup/step', async (req, res) => runSetupStep(req, res, 'resume'));
3084
+ app.post('/api/setup/resume', async (req, res) => runSetupStep(req, res, 'resume'));
3085
+ app.post('/api/setup/test', async (req, res) => runSetupStep(req, res, 'test'));
3086
+ app.post('/api/setup/repair', async (req, res) => runSetupStep(req, res, 'repair'));
3087
+ app.post('/api/setup/reconfigure', async (req, res) => runSetupStep(req, res, 'reconfigure'));
3088
+ app.post('/api/setup/commit', async (_req, res) => {
3089
+ try {
3090
+ const { refreshSetupState } = await import('../setup/setup-store.js');
3091
+ const ev = await refreshSetupState({});
3092
+ const { describeSetup } = await import('../setup/setup-store.js');
3093
+ res.json({ ...(await setupState()), ok: ev.gate === 'ready', summary: describeSetup(ev) });
3094
+ }
3095
+ catch (err) {
3096
+ res.status(500).json({ ok: false, error: String(err) });
3097
+ }
3098
+ });
3099
+ app.post('/api/setup/identity', async (req, res) => runSetupStep({ body: { answers: [req.body?.name] } }, res, 'resume'));
3100
+ app.post('/api/setup/provider', async (req, res) => runSetupStep({ body: { answers: [req.body?.provider] } }, res, 'resume'));
3101
+ // 首启页面 (零依赖, 直接打这些 API; 未 ready 时所有对话路由都是 503)
3102
+ app.get('/setup', (_req, res) => {
3103
+ res.type('html').send(`<!doctype html><meta charset="utf-8"><title>Bolloon 初始化</title>
3104
+ <style>body{font-family:system-ui,-apple-system,"PingFang SC",sans-serif;max-width:760px;margin:40px auto;padding:0 16px;line-height:1.6}
3105
+ h1{font-size:20px}pre{background:#f6f6f6;padding:12px;border-radius:8px;white-space:pre-wrap}
3106
+ .ok{color:#0a7d32}.bad{color:#b00020}input,select,button{font-size:15px;padding:8px;margin:4px 0}
3107
+ .row{margin:10px 0}small{color:#666}</style>
3108
+ <h1>Bolloon 初始化</h1>
3109
+ <div id="state">加载中…</div>
3110
+ <div class="row" id="form"></div>
3111
+ <div class="row"><button onclick="run('step')">提交这一步</button>
3112
+ <button onclick="run('test')">重新测试连通性/运行时</button>
3113
+ <button onclick="run('repair')">修复/迁移配置</button>
3114
+ <button onclick="fetchState()">刷新</button></div>
3115
+ <pre id="log"></pre>
3116
+ <script>
3117
+ async function fetchState(){
3118
+ const r = await fetch('/api/setup'); const s = await r.json();
3119
+ const ready = s.gate==='ready';
3120
+ document.getElementById('state').innerHTML =
3121
+ '<b>阶段</b>: '+s.stage+' &nbsp; <b>门禁</b>: <span class="'+(ready?'ok':'bad')+'">'+s.gate+'</span><br>'+
3122
+ '<b>就绪度</b>: basic '+s.readiness.basic+' · agent '+s.readiness.agent+' · durable '+s.readiness.durable+' · network '+s.readiness.network+'<br>'+
3123
+ '<small>已完成: '+(s.completed.join(' → ')||'(无)')+'</small><br>'+
3124
+ '<small>下一步: '+(s.actions[0]||'')+'</small>'+
3125
+ (s.lastError?'<br><small class="bad">上次错误 ['+s.lastError.errorClass+'] '+s.lastError.message+'</small>':'');
3126
+ const f = document.getElementById('form');
3127
+ const n = s.nextStep;
3128
+ if(!n){ f.innerHTML='<b class="ok">✅ 已就绪, 可以进入对话</b>'; window.currentNeeds=null; return; }
3129
+ window.currentNeeds=n.needs;
3130
+ if(n.needs==='provider'){
3131
+ f.innerHTML='<div>'+n.question+'</div><select id="v">'+ (n.choices||[]).map(c=>'<option value="'+c.value+'">'+c.label+(c.hint?' — '+c.hint:'')+'</option>').join('') +'</select>';
3132
+ } else if(n.needs==='credential'){
3133
+ f.innerHTML='<div>'+n.question+'</div><input id="v" type="password" placeholder="API key (不会回显)" autocomplete="off">';
3134
+ } else if(n.needs==='none'){
3135
+ f.innerHTML='<div>'+n.question+' (点上面按钮执行)</div>';
3136
+ } else {
3137
+ f.innerHTML='<div>'+n.question+'</div><input id="v" value="'+(n.defaultValue||'')+'">';
3138
+ }
3139
+ }
3140
+ async function run(ep){
3141
+ const v=document.getElementById('v'); const n=window.currentNeeds;
3142
+ const answers = (n==='credential'||n==='name'||n==='model'||n==='provider') && v ? [v.value] : [];
3143
+ if(v) v.value=''; // key 立即从 DOM 里清掉
3144
+ const r = await fetch('/api/setup/'+ep,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({answers})});
3145
+ const j = await r.json();
3146
+ document.getElementById('log').textContent = (j.log||[]).join('\n') + '\n' + (j.summary||'');
3147
+ fetchState();
3148
+ }
3149
+ fetchState();
3150
+ </script>`);
3151
+ });
3152
+ // 2026-09-16 (M1/M3): 初始化状态 —— CLI 与 Web 读同一份事实 (setup-state.json + 各领域 store)
3153
+ app.get('/api/setup', async (_req, res) => {
3154
+ try {
3155
+ const { evaluateSetup, describeSetup, resolveBolloonHome } = await import('../setup/setup-store.js');
3156
+ const ev = await evaluateSetup({ light: false });
3157
+ res.json({ gate: ev.gate, stage: ev.state.stage, readiness: ev.state.readiness, allow: ev.state.allow, completed: ev.state.completed, inputs: ev.state.inputs, lastError: ev.state.lastError, reasons: ev.reasons, nextActions: ev.nextActions, summary: describeSetup(ev), bolloonHome: resolveBolloonHome() });
3158
+ }
3159
+ catch (err) {
3160
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
3161
+ }
3162
+ });
3163
+ // 2026-09-16 (2-G.1): Skills Manager —— 与 CLI `/skills` `/skill` 读同一份事实 (skills-registry.json + 技能目录)
3164
+ app.get('/api/skills', async (_req, res) => {
3165
+ try {
3166
+ const { getSkillsManager } = await import('../agents/skills-manager.js');
3167
+ const sm = getSkillsManager();
3168
+ const skills = await sm.view();
3169
+ res.json({ count: skills.length, registryPath: (await import('../agents/skills-manager.js')).registryPath(), skills, health: await sm.health() });
3170
+ }
3171
+ catch (err) {
3172
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3173
+ }
3174
+ });
3175
+ app.get('/api/skills/health', async (_req, res) => {
3176
+ try {
3177
+ const { getSkillsManager } = await import('../agents/skills-manager.js');
3178
+ res.json(await getSkillsManager().health());
3179
+ }
3180
+ catch (err) {
3181
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3182
+ }
3183
+ });
3184
+ app.get('/api/skills/:name', async (req, res) => {
3185
+ try {
3186
+ const { getSkillsManager } = await import('../agents/skills-manager.js');
3187
+ const s = await getSkillsManager().inspect(String(req.params.name));
3188
+ if (!s) {
3189
+ res.status(404).json({ error: `没有这个技能: ${req.params.name}` });
3190
+ return;
3191
+ }
3192
+ res.json({ skill: s });
3193
+ }
3194
+ catch (err) {
3195
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3196
+ }
3197
+ });
3198
+ app.post('/api/skills/import', async (req, res) => {
3199
+ try {
3200
+ const ref = String(req.body?.ref || '').trim();
3201
+ if (!ref) {
3202
+ res.status(400).json({ error: '要 body {ref: "bolloon://skill/<cid>" | "ipfs://<cid>" | "<cid>"}' });
3203
+ return;
3204
+ }
3205
+ const { getSkillsManager } = await import('../agents/skills-manager.js');
3206
+ const r = await getSkillsManager().import(ref, { force: !!req.body?.force });
3207
+ if (!r.ok) {
3208
+ res.status(409).json({ error: r.error });
3209
+ return;
3210
+ }
3211
+ res.json({ ok: true, name: r.name, version: r.version, skill: r.skill });
3212
+ }
3213
+ catch (err) {
3214
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3215
+ }
3216
+ });
3217
+ // enable / disable / approve / validate / quarantine (Express 5 不支持内联正则, 显式写开)
3218
+ for (const action of ['enable', 'disable', 'approve', 'validate', 'quarantine']) {
3219
+ app.post(`/api/skills/:name/${action}`, async (req, res) => {
3220
+ try {
3221
+ const { getSkillsManager } = await import('../agents/skills-manager.js');
3222
+ const sm = getSkillsManager();
3223
+ const name = String(req.params.name);
3224
+ const r = action === 'approve' ? await sm.approve(name, 'web')
3225
+ : action === 'quarantine' ? await sm.quarantine(name, String(req.body?.reason || 'web 手动隔离'))
3226
+ : action === 'enable' ? await sm.enable(name)
3227
+ : action === 'disable' ? await sm.disable(name)
3228
+ : await sm.validate(name);
3229
+ if (!r.ok && !r.skill) {
3230
+ res.status(409).json({ error: r.reason || r.issues || '操作未完成' });
3231
+ return;
3232
+ }
3233
+ res.json({ ok: r.ok, name, action, skill: r.skill, issues: r.issues });
3234
+ }
3235
+ catch (err) {
3236
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3237
+ }
3238
+ });
3239
+ }
3240
+ // 2026-09-16 (M2-B): 长期执行层诊断 + 手动推进 —— 与 CLI /supervise 读同一份持久化记录
3241
+ app.get('/api/supervisor', async (_req, res) => {
3242
+ try {
3243
+ const { getSupervisor } = await import('../agents/execution-supervisor.js');
3244
+ const { readSupervisorState, supervisorStatePath } = await import('../agents/supervisor-host.js');
3245
+ const { wakeReport, listRunnableGoals } = await import('../agents/goal-store.js');
3246
+ const { runnable, skipped } = await listRunnableGoals({ now: Date.now() });
3247
+ res.json({
3248
+ supervisor: getSupervisor().status(),
3249
+ /** 宿主身份/心跳 (谁在跑、跑到哪、上次有没有好好停) */
3250
+ host: await readSupervisorState(),
3251
+ hostStatePath: supervisorStatePath(),
3252
+ wake: await wakeReport(),
3253
+ runnable: runnable.map((g) => ({ goalId: g.goalId, objective: g.objective, status: g.status })),
3254
+ skipped,
3255
+ });
3256
+ }
3257
+ catch (err) {
3258
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3259
+ }
3260
+ });
3261
+ // 手动踢一个调度周期 (调试/验收用; 不改变任何"事实来源")
3262
+ app.post('/api/supervisor/tick', async (_req, res) => {
3263
+ if (!(await setupGate(res)))
3264
+ return;
3265
+ try {
3266
+ const { getSupervisor } = await import('../agents/execution-supervisor.js');
3267
+ const report = await getSupervisor().tickOnce();
3268
+ res.json({ ok: true, report });
3269
+ }
3270
+ catch (err) {
3271
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3272
+ }
3273
+ });
3274
+ // 2026-09-16 (2-E 第 4 类): 外部事件到达 → 唤醒在等它的 Goal (不重发已成功的请求)
3275
+ app.post('/api/goals/:goalId/wake', async (req, res) => {
3276
+ const goalId = String(req.params.goalId);
3277
+ try {
3278
+ const { getSupervisor } = await import('../agents/execution-supervisor.js');
3279
+ const { readGoal, setContinuation } = await import('../agents/goal-store.js');
3280
+ const g = await readGoal(goalId);
3281
+ if (!g) {
3282
+ res.status(404).json({ error: `goal 不存在: ${goalId}` });
3283
+ return;
3284
+ }
3285
+ const woke = await getSupervisor().notifyExternal(goalId);
3286
+ if (!woke) {
3287
+ // 没在等外部事件也要如实回答 (可能它其实在等人/在跑), 但允许显式加急
3288
+ if (req.body?.force) {
3289
+ await setContinuation(goalId, { wakeReason: 'active', autoContinue: true, wakeAt: undefined, needsExternal: undefined });
3290
+ res.json({ ok: true, goalId, woke: false, forced: true, note: `原状态 ${g.status} (不在等外部事件), 已按 force 加急` });
3291
+ return;
3292
+ }
3293
+ res.status(409).json({ error: `Goal 不在等外部事件 (status=${g.status}, wakeReason=${g.continuation?.wakeReason || '无'})`, force_hint: '加 body {"force":true} 可强制加急' });
3294
+ return;
3295
+ }
3296
+ res.json({ ok: true, goalId, woke: true, note: '已唤醒: 下一次 Supervisor tick 会推进它' });
3297
+ }
3298
+ catch (err) {
3299
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3300
+ }
3301
+ });
3302
+ // 2026-09-16 (M5): 运行列表 —— Web 与 CLI 读同一份 run 事实 (~/.bolloon/runs)
3303
+ app.get('/api/runs', async (req, res) => {
3304
+ try {
3305
+ const { listRuns, formatRunLine } = await import('../agents/run-store.js');
3306
+ const status = String(req.query?.status || '').trim();
3307
+ const limit = Number(req.query?.limit || 20);
3308
+ const runs = await listRuns({ status: status ? status : undefined, limit });
3309
+ res.json({
3310
+ count: runs.length,
3311
+ runs,
3312
+ lines: runs.map(formatRunLine),
3313
+ });
3314
+ }
3315
+ catch (err) {
3316
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3317
+ }
3318
+ });
3319
+ // 2026-09-16 (M5): 单个 run + 停止原因/checkpoint/最近步骤/recovery/harness 事件 (两端同一份事实)
3320
+ app.get('/api/runs/:runId', async (req, res) => {
3321
+ try {
3322
+ const { readRun } = await import('../agents/run-store.js');
3323
+ const run = await readRun(String(req.params.runId));
3324
+ if (!run) {
3325
+ res.status(404).json({ error: `run 不存在: ${req.params.runId}` });
3326
+ return;
3327
+ }
3328
+ const goal = run.goalId ? await (await import('../agents/goal-store.js')).readGoal(run.goalId) : null;
3329
+ res.json({ run, goal, checkpoint: run.checkpoint, steps: run.steps, recovery: run.recovery, harness: run.harness || [] });
3330
+ }
3331
+ catch (err) {
3332
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3333
+ }
3334
+ });
3335
+ // 2026-09-16 (M5): 继续 interrupted/stalled/paused/needs_human/awaiting_external 的运行 —— 真从 checkpoint 继续
3336
+ app.post('/api/runs/:runId/resume', async (req, res) => {
3337
+ const runId = String(req.params.runId);
3338
+ try {
3339
+ const { readRun, RESUMABLE_STATUSES } = await import('../agents/run-store.js');
3340
+ const run = await readRun(runId);
3341
+ if (!run) {
3342
+ res.status(404).json({ error: `run 不存在: ${runId}` });
3343
+ return;
3344
+ }
3345
+ // 先做只读校验: 不可恢复的状态明确拒绝 (不假装"已开始"), 避免前端以为在跑
3346
+ if (!RESUMABLE_STATUSES.includes(run.status)) {
3347
+ res.status(409).json({ error: `状态 ${run.status} 不可恢复 (可恢复: ${RESUMABLE_STATUSES.join('/')})` });
3348
+ return;
3349
+ }
3350
+ if (!run.channelId) {
3351
+ res.status(400).json({ error: '该 run 没有 channelId, 无法在 web 端恢复 (请在 CLI 用 /resume)' });
3352
+ return;
3353
+ }
3354
+ const agent = await getAgentForChannel(run.channelId);
3355
+ if (!agent?.resumeRun) {
3356
+ res.status(409).json({ error: '该 channel 的 agent 不支持 resumeRun' });
3357
+ return;
3358
+ }
3359
+ // 异步执行: resume 可能跑几分钟; 状态从 /api/runs/:runId 读 (recovering → running → done/failed)
3360
+ void agent.resumeRun(runId).then((r) => {
3361
+ if (!r?.ok)
3362
+ console.warn(`[runs] resume ${runId} 失败:`, r?.reason);
3363
+ }).catch((err) => console.warn(`[runs] resume ${runId} 异常:`, err?.message));
3364
+ res.json({ ok: true, accepted: true, runId, note: '已开始恢复; 状态请读 GET /api/runs/:runId' });
3365
+ }
3366
+ catch (err) {
3367
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3368
+ }
3369
+ });
3370
+ // 2026-09-16 (M5): 人工批准 —— needs_human 的运行经人确认后继续 (= 带批准的 resume)
3371
+ app.post('/api/runs/:runId/approve', async (req, res) => {
3372
+ const runId = String(req.params.runId);
3373
+ try {
3374
+ const { readRun, recordRecovery } = await import('../agents/run-store.js');
3375
+ const run = await readRun(runId);
3376
+ if (!run) {
3377
+ res.status(404).json({ error: `run 不存在: ${runId}` });
3378
+ return;
3379
+ }
3380
+ if (run.status !== 'needs_human') {
3381
+ res.status(409).json({ error: `只有 needs_human 的运行需要批准 (当前 ${run.status})` });
3382
+ return;
3383
+ }
3384
+ await recordRecovery(runId, { errorClass: run.errorClass || 'unknown', message: '人工批准后继续', action: 'resume' });
3385
+ if (!run.channelId) {
3386
+ res.status(400).json({ error: '该 run 没有 channelId, 请在 CLI 用 /approve' });
3387
+ return;
3388
+ }
3389
+ const agent = await getAgentForChannel(run.channelId, run.channelId, run.agentId, {});
3390
+ if (!agent?.resumeRun) {
3391
+ res.status(409).json({ error: '该 channel 的 agent 不支持 resumeRun' });
3392
+ return;
3393
+ }
3394
+ void agent.resumeRun(runId).catch((err) => console.warn(`[runs] approve ${runId} 异常:`, err?.message));
3395
+ res.json({ ok: true, accepted: true, runId, approved: true });
3396
+ }
3397
+ catch (err) {
3398
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3399
+ }
3400
+ });
3401
+ // 2026-09-16 (M5): 暂停 / 中止 —— 只改 run 状态; 运行中的 agent 会在下一次循环检查时如实停下
3402
+ // (Express 5 的 path-to-regexp 不再支持 `:action(a|b)` 内联正则, 所以两条写开)
3403
+ for (const [suffix, to] of [['pause', 'paused'], ['abort', 'aborted']]) {
3404
+ app.post(`/api/runs/:runId/${suffix}`, async (req, res) => {
3405
+ const runId = String(req.params.runId);
3406
+ try {
3407
+ const { setRunStatus } = await import('../agents/run-store.js');
3408
+ const r = await setRunStatus(runId, to, { error: `外部请求 (${suffix})` });
3409
+ if (!r.ok) {
3410
+ res.status(409).json({ error: r.reason || '状态迁移被拒绝' });
3411
+ return;
3412
+ }
3413
+ res.json({ ok: true, runId, status: to, note: '运行中的 agent 会在下一轮循环检查时停下 (轮内不打断)' });
3414
+ }
3415
+ catch (err) {
3416
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3417
+ }
3418
+ });
3419
+ }
3420
+ // 2026-09-16 (M2): 目标 (Goal) —— 目标事实来源; runId → goalId → objective/successCriteria 反查链
3421
+ app.get('/api/goals', async (req, res) => {
3422
+ try {
3423
+ const { listGoals, formatGoalLine } = await import('../agents/goal-store.js');
3424
+ const status = String(req.query?.status || '').trim();
3425
+ const goals = await listGoals({ status: status ? status : undefined, limit: Number(req.query?.limit || 30) });
3426
+ res.json({ count: goals.length, goals, lines: goals.map(formatGoalLine) });
3427
+ }
3428
+ catch (err) {
3429
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3430
+ }
3431
+ });
3432
+ app.get('/api/goals/:goalId', async (req, res) => {
3433
+ try {
3434
+ const { readGoal, evaluateGoalCompletion } = await import('../agents/goal-store.js');
3435
+ const goal = await readGoal(String(req.params.goalId));
3436
+ if (!goal) {
3437
+ res.status(404).json({ error: `goal 不存在: ${req.params.goalId}` });
3438
+ return;
3439
+ }
3440
+ res.json({ goal, completion: evaluateGoalCompletion(goal) });
3441
+ }
3442
+ catch (err) {
3443
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3444
+ }
3445
+ });
2835
3446
  // 2026-08-13 (Phase E1): Agent 服务 Registry — 发现层 (OrbitDB 去中心化 + 本地 fallback)
2836
3447
  app.get('/api/registry', async (_req, res) => {
2837
3448
  try {
@@ -3889,7 +4500,29 @@ ${goalDesc}
3889
4500
  console.log(`[SSE] 客户端断开 channelId=${channelId || '(broadcast)'}, 剩余=${sseClients.size}`);
3890
4501
  });
3891
4502
  });
4503
+ // 2026-09-16 (M4): agent 执行硬门禁 —— 未 ready 时这些路由 503 + 结构化初始化状态 (Web 只显示初始化页)
4504
+ const setupGate = async (res) => {
4505
+ try {
4506
+ const { getSetupGateCached } = await import('../setup/setup-store.js');
4507
+ const { gate, state } = await getSetupGateCached();
4508
+ if (gate === 'ready')
4509
+ return true;
4510
+ res.status(503).json({
4511
+ error: `初始化未就绪 (${gate}, 阶段 ${state.stage}) — 请先完成初始化`,
4512
+ gate, stage: state.stage, readiness: state.readiness,
4513
+ nextActions: state.actions, lastError: state.lastError || null,
4514
+ });
4515
+ return false;
4516
+ }
4517
+ catch (err) {
4518
+ // fail-closed: 连门禁都读不出来 → 不放行
4519
+ res.status(503).json({ error: `初始化状态不可读 (fail-closed): ${String(err?.message || err).slice(0, 160)}`, gate: 'blocked' });
4520
+ return false;
4521
+ }
4522
+ };
3892
4523
  app.post('/message', async (req, res) => {
4524
+ if (!(await setupGate(res)))
4525
+ return;
3893
4526
  const { text, channelId, channelDid, attachments } = req.body;
3894
4527
  if (!text) {
3895
4528
  return res.status(400).json({ error: 'No text provided' });