@bolloon/bolloon-agent 0.4.32 → 0.5.0

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 (157) hide show
  1. package/dist/agents/chain/explorer.js +52 -0
  2. package/dist/agents/chain/index.js +2 -0
  3. package/dist/agents/chain/onchain-trade.js +15 -1
  4. package/dist/agents/contacts/chain.js +27 -7
  5. package/dist/agents/execution-supervisor.js +493 -35
  6. package/dist/agents/external-events.js +35 -6
  7. package/dist/agents/gateway-group.js +100 -12
  8. package/dist/agents/goal-criteria.js +10 -3
  9. package/dist/agents/goal-flywheel/block-executor.js +346 -0
  10. package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
  11. package/dist/agents/goal-flywheel/goal-change.js +754 -0
  12. package/dist/agents/goal-flywheel/index.js +26 -0
  13. package/dist/agents/goal-flywheel/memory-layers.js +227 -0
  14. package/dist/agents/goal-flywheel/run-closure.js +809 -0
  15. package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
  16. package/dist/agents/goal-flywheel/types.js +329 -0
  17. package/dist/agents/goal-flywheel/wiring/change.js +288 -0
  18. package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
  19. package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
  20. package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
  21. package/dist/agents/goal-flywheel/wiring/index.js +26 -0
  22. package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
  23. package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
  24. package/dist/agents/goal-flywheel/work-contract.js +478 -0
  25. package/dist/agents/goal-flywheel/work-monitor.js +488 -0
  26. package/dist/agents/goal-flywheel-wiring.js +1870 -0
  27. package/dist/agents/goal-state-reducer.js +406 -0
  28. package/dist/agents/goal-store.js +35 -1
  29. package/dist/agents/mobile-flywheel-view.js +144 -0
  30. package/dist/agents/mobile-task-actions-verify.js +74 -0
  31. package/dist/agents/mobile-task-actions.js +178 -0
  32. package/dist/agents/mobile-task-views.js +199 -0
  33. package/dist/agents/network-pulse.js +525 -27
  34. package/dist/agents/pi-sdk.js +75 -20
  35. package/dist/agents/run-store.js +18 -5
  36. package/dist/agents/skill-readiness.js +52 -10
  37. package/dist/agents/subagent-manager.js +209 -3
  38. package/dist/agents/task/task-onchain-runner.js +19 -1
  39. package/dist/agents/task/task-runner.js +36 -6
  40. package/dist/agents/task-board.js +702 -0
  41. package/dist/agents/task-group.js +524 -0
  42. package/dist/agents/task-public-text.js +89 -0
  43. package/dist/cli/commands/index.js +3 -0
  44. package/dist/cli/commands/tasks.js +977 -8
  45. package/dist/cli/identity-command.js +177 -0
  46. package/dist/cli/protocol-envelope.js +4 -0
  47. package/dist/cli/setup-wizard.js +71 -0
  48. package/dist/cli/update-commands.js +150 -24
  49. package/dist/cli-entry.js +12 -2
  50. package/dist/electron-build/agents/chain/explorer.js +59 -0
  51. package/dist/electron-build/agents/chain/explorer.js.map +1 -0
  52. package/dist/electron-build/agents/contacts/chain.js +26 -6
  53. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  54. package/dist/electron-build/agents/execution-supervisor.js +492 -34
  55. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  56. package/dist/electron-build/agents/external-events.js +34 -5
  57. package/dist/electron-build/agents/external-events.js.map +1 -1
  58. package/dist/electron-build/agents/goal-criteria.js +10 -3
  59. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  60. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  61. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  62. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  63. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  64. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  65. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  66. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  67. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  68. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  69. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  70. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  71. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  72. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  73. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  74. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  75. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  76. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  78. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  79. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  80. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  81. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  82. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  83. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  84. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  85. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  86. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  87. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  88. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  89. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  90. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  91. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  92. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  93. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  94. package/dist/electron-build/agents/goal-store.js +35 -1
  95. package/dist/electron-build/agents/goal-store.js.map +1 -1
  96. package/dist/electron-build/agents/local-signer.js +101 -0
  97. package/dist/electron-build/agents/local-signer.js.map +1 -0
  98. package/dist/electron-build/agents/network-pulse.js +531 -28
  99. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  100. package/dist/electron-build/agents/pi-sdk.js +74 -19
  101. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  102. package/dist/electron-build/agents/run-store.js +20 -5
  103. package/dist/electron-build/agents/run-store.js.map +1 -1
  104. package/dist/electron-build/agents/skill-readiness.js +52 -9
  105. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  106. package/dist/electron-build/agents/task/task-runner.js +35 -5
  107. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  108. package/dist/electron-build/agents/task-board.js +761 -0
  109. package/dist/electron-build/agents/task-board.js.map +1 -0
  110. package/dist/electron-build/agents/task-contract.js +382 -0
  111. package/dist/electron-build/agents/task-contract.js.map +1 -0
  112. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  113. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  114. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  115. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  116. package/dist/electron-build/utils/dual-source.js +471 -0
  117. package/dist/electron-build/utils/dual-source.js.map +1 -0
  118. package/dist/electron-build/utils/update-manager.js +331 -57
  119. package/dist/electron-build/utils/update-manager.js.map +1 -1
  120. package/dist/electron-build/utils/update-state.js +16 -1
  121. package/dist/electron-build/utils/update-state.js.map +1 -1
  122. package/dist/electron-build/utils/version-info.js +82 -2
  123. package/dist/electron-build/utils/version-info.js.map +1 -1
  124. package/dist/index.js +103 -12
  125. package/dist/ios/chain-explorer.js +617 -0
  126. package/dist/ios/explorer.html +163 -0
  127. package/dist/ios/index.html +96 -0
  128. package/dist/ios/mobile-agent.js +2 -2
  129. package/dist/ios/mobile-contacts.js +329 -0
  130. package/dist/ios/mobile-core.js +1379 -226
  131. package/dist/ios/mobile-privacy.js +185 -0
  132. package/dist/ios/mobile.css +19 -0
  133. package/dist/ios/mobile.html +96 -0
  134. package/dist/ios/mobile.js +706 -7
  135. package/dist/ios/routes-chain-index.js +108 -0
  136. package/dist/ios/routes-contacts.js +420 -0
  137. package/dist/ios/routes-onchain-trade.js +99 -0
  138. package/dist/ios/routes-x402-info.js +1 -0
  139. package/dist/ios/server.js +762 -0
  140. package/dist/ios/task-frame-server.js +211 -0
  141. package/dist/orbitdb/cid-database.js +81 -16
  142. package/dist/orbitdb/ipfs-node.js +31 -2
  143. package/dist/utils/dual-source.js +426 -0
  144. package/dist/utils/update-cli.js +16 -21
  145. package/dist/utils/update-health.js +35 -5
  146. package/dist/utils/update-manager.js +329 -57
  147. package/dist/utils/update-state.js +16 -1
  148. package/dist/utils/version-info.js +75 -1
  149. package/dist/web/mobile-contacts.js +44 -11
  150. package/dist/web/mobile-core.js +1197 -513
  151. package/dist/web/mobile-tasks.js +339 -0
  152. package/dist/web/mobile.css +19 -0
  153. package/dist/web/mobile.html +64 -0
  154. package/dist/web/mobile.js +531 -1
  155. package/dist/web/routes-mobile-tasks.js +353 -0
  156. package/dist/web/server.js +163 -4
  157. package/package.json +3 -1
@@ -13,10 +13,15 @@ import { registerJudgmentsRoutes } from './routes-judgments.js';
13
13
  import { registerLlmConfigRoutes } from './routes-llm-config.js';
14
14
  import { registerExternalEngineRoutes } from './routes-external-engines.js';
15
15
  import { registerTaskRoutes } from './routes-tasks.js';
16
+ // 2026-09-19: 联系方式 / 人工批准 / 手机—桌面配对 路由
17
+ import { registerContactRoutes } from './routes-contacts.js';
16
18
  // 2026-09-13: 微支付信息服务 (x402) 路由
17
19
  import { registerX402InfoRoutes } from './routes-x402-info.js';
20
+ // 2026-09-22: P5 链上索引 (只读: status/stats/timeline/events 增量) 路由
21
+ import { registerChainIndexRoutes } from './routes-chain-index.js';
18
22
  import { loadPeerTier, recordInteraction, tierLabel } from '../social/dunbar-tier.js';
19
23
  import { registerHearthRoutes } from './routes-hearth.js';
24
+ import { registerOnchainTradeRoutes } from './routes-onchain-trade.js';
20
25
  import { loadOrCreateAgentIdentity } from '../agents/agent-identity.js';
21
26
  // 2026-07-06: 类型抽到 ./server-types.ts (channel / session / task / sse client / iroh info / paths)
22
27
  import { SESSION_CACHE_PATH, SHARED_SESSION_PATH, IPFS_ENDPOINT, } from './server-types.js';
@@ -1530,6 +1535,11 @@ async function getAgentForChannel(channelId, channelDid, channelName, channelDid
1530
1535
  usePivotLoop: true,
1531
1536
  }, true); // forceNew: true 强制创建新实例
1532
1537
  channelSessions.set(sessionKey, session);
1538
+ // 2026-09-16: 标记运行表面 — 落盘 run 记录里能区分 web 运行 (CLI 默认 cli)
1539
+ try {
1540
+ session.setRunSurface?.('web');
1541
+ }
1542
+ catch { /* 老 session 无此方法则忽略 */ }
1533
1543
  if (channelDid) {
1534
1544
  console.log(`[Agent] 新建频道 ${channelId} session, DID = ${channelDid}, sessionId = ${currentSessionId}`);
1535
1545
  }
@@ -1626,6 +1636,104 @@ export async function createWebServer(port = 3000, options = {}) {
1626
1636
  catch (err) {
1627
1637
  console.warn('[createWebServer] bootstrap 失败 (非致命):', err);
1628
1638
  }
1639
+ // 2026-09-16: 孤儿运行对账 — 上次进程留下的 running 记录改判 interrupted。
1640
+ // 不做这步的话, 刷新/重启后 UI 会显示"还在跑"的幽灵运行 (最典型的假状态)。
1641
+ try {
1642
+ const { reconcileOrphans } = await import('../agents/run-store.js');
1643
+ const rec = await reconcileOrphans();
1644
+ if (rec.interrupted.length) {
1645
+ console.log(`[runs] 对账: ${rec.interrupted.length} 条上次中断的运行已标 interrupted (${rec.interrupted.slice(0, 3).join(', ')}${rec.interrupted.length > 3 ? '…' : ''})`);
1646
+ }
1647
+ if (rec.stillRunning.length)
1648
+ console.log(`[runs] 存活运行: ${rec.stillRunning.length} 条`);
1649
+ }
1650
+ catch (err) {
1651
+ console.warn('[runs] 对账失败 (非致命):', err?.message);
1652
+ }
1653
+ // 2026-09-16 (M2-B): ExecutionSupervisor —— 长期执行层。
1654
+ // 页面关掉、进程重启后由它把 Goal 继续下去; 跨进程排他靠 Goal lease (不是内存状态)。
1655
+ // Harness 管"这一段能不能安全执行", Supervisor 管"这个目标还要不要继续执行"。
1656
+ if (process.env.BOLLOON_SUPERVISOR !== '0') {
1657
+ try {
1658
+ const { getSupervisor } = await import('../agents/execution-supervisor.js');
1659
+ const { runSupervisorHost } = await import('../agents/supervisor-host.js');
1660
+ const tickMs = Number(process.env.BOLLOON_SUPERVISOR_TICK_MS) || 30_000;
1661
+ const leaseMs = Number(process.env.BOLLOON_SUPERVISOR_LEASE_MS) || 90_000;
1662
+ // 执行器**在每次执行前解析** (不写死): web 宿主按 Goal 的 channelId 找 channel agent。
1663
+ // 解析不出来 → 只诊断不执行, 且不写任何 Goal 状态。
1664
+ const resolver = async (req) => {
1665
+ const goal = req.goal;
1666
+ if (!goal?.channelId) {
1667
+ return { ok: false, kind: 'none', reason: 'Goal 没有 channelId: 无法在 web 进程内解析执行器' };
1668
+ }
1669
+ const agent = await getAgentForChannel(goal.channelId, goal.agentId || '', undefined, undefined);
1670
+ if (!agent)
1671
+ return { ok: false, kind: 'none', reason: 'channel agent 不可用' };
1672
+ const runner = async (r) => {
1673
+ if (r.kind === 'resume' && r.prevRunId && typeof agent.resumeRun === 'function') {
1674
+ const res = await agent.resumeRun(r.prevRunId);
1675
+ return { runId: r.prevRunId, status: res?.ok ? 'done' : 'failed', error: res?.ok ? undefined : res?.reason };
1676
+ }
1677
+ // 跨预算继续: 新 Run 必须挂在同一个 Goal 下 (setGoalId), 并带上上一个 Run 的非幂等守卫
1678
+ agent.setGoalId?.(goal.goalId);
1679
+ agent.setContinuationGuards?.(r.guards || []);
1680
+ const reply = await agent.prompt(r.instruction);
1681
+ // 注意: prompt 收尾会清空 currentRunId → 必须读 lastRunId (否则会拿上一条 run 做决策)
1682
+ const runId = agent.getLastRunId?.() || agent.getRunId?.();
1683
+ return { runId, status: 'done', reply: typeof reply === 'string' ? reply.slice(0, 500) : undefined };
1684
+ };
1685
+ return { ok: true, runner, kind: 'web' };
1686
+ };
1687
+ // 2026-09-16 (M4): 初始化未就绪 → Supervisor 只诊断 (不解析执行器, 不执行任何 Goal)
1688
+ let setupReadyForSupervisor = false;
1689
+ try {
1690
+ const { getSetupGateCached } = await import('../setup/setup-store.js');
1691
+ const g = await getSetupGateCached();
1692
+ setupReadyForSupervisor = g.gate === 'ready';
1693
+ if (!setupReadyForSupervisor)
1694
+ console.warn(`[supervisor] 初始化未就绪 (${g.gate}, 阶段 ${g.state.stage}) → 只诊断, 不执行 Goal`);
1695
+ }
1696
+ catch {
1697
+ console.warn('[supervisor] 初始化状态不可读 → 只诊断, 不执行 Goal');
1698
+ }
1699
+ const sup = getSupervisor({
1700
+ tickIntervalMs: tickMs,
1701
+ leaseTtlMs: leaseMs,
1702
+ maxPerTick: Number(process.env.BOLLOON_SUPERVISOR_MAX_PER_TICK) || 1,
1703
+ log: (m) => console.log(m),
1704
+ onEvent: (e) => { try {
1705
+ broadcast({ type: 'supervisor', ...e });
1706
+ }
1707
+ catch { /* UI 广播失败不影响调度 */ } },
1708
+ resolver: setupReadyForSupervisor ? resolver : undefined,
1709
+ });
1710
+ // 宿主层: 跨进程单 tick 互斥 + 宿主身份/心跳落盘 + 优雅停止
1711
+ const host = await runSupervisorHost({
1712
+ supervisor: sup,
1713
+ tickIntervalMs: tickMs,
1714
+ leaseTtlMs: leaseMs,
1715
+ runnerKind: 'web',
1716
+ log: (m) => console.log(m),
1717
+ });
1718
+ global.__bolloonSupervisorHost = host;
1719
+ console.log(`[supervisor] 长期执行层已启动 (owner=${host.state.owner}, worker=${host.state.workerId}, tick=${tickMs}ms, lease=${leaseMs}ms)`);
1720
+ const stopHost = (sig) => { void host.stop(`signal:${sig}`).finally(() => process.exit(0)); };
1721
+ process.once('SIGINT', () => stopHost('SIGINT'));
1722
+ process.once('SIGTERM', () => stopHost('SIGTERM'));
1723
+ }
1724
+ catch (err) {
1725
+ // 启动失败必须可观测 (不许静默降级成长时间不执行)
1726
+ const msg = `[supervisor] 启动失败: ${err?.message || err}`;
1727
+ console.error(msg);
1728
+ try {
1729
+ broadcast({ type: 'supervisor', kind: 'startup_failed', message: msg });
1730
+ }
1731
+ catch { /* ignore */ }
1732
+ }
1733
+ }
1734
+ else {
1735
+ console.log('[supervisor] 长期执行层已关闭 (BOLLOON_SUPERVISOR=0)');
1736
+ }
1629
1737
  // 2026-08-03 (Context OS P5): 初始化资产层 12+3 目录 (幂等, 每层 README 声明职责边界)
1630
1738
  try {
1631
1739
  const { ensureContextOsDirs } = await import('../bootstrap/context-os.js');
@@ -2439,6 +2547,19 @@ export async function createWebServer(port = 3000, options = {}) {
2439
2547
  console.log(`[cron] 调度器已启动 (每 ${Math.round((Number(process.env.BOLLOON_CRON_HEARTBEAT_MS) || 60_000) / 1000)}s 一轮, tick 带锁 + DND)`);
2440
2548
  // 暴露给 /message 使用 (主任务闸门)
2441
2549
  global.__bolloonMainTask = { enterMainTask, exitMainTask };
2550
+ // 2026-09-16: 运行失速巡检 (每 60s) — running 但长时间没新进展的运行标 stalled,
2551
+ // 让 UI/CLI 能说"这个运行卡住了", 而不是永远转圈 (crash 由启动时对账兜底)。
2552
+ setInterval(() => {
2553
+ void (async () => {
2554
+ try {
2555
+ const { superviseRuns } = await import('../agents/run-store.js');
2556
+ const r = await superviseRuns();
2557
+ if (r.stalled.length)
2558
+ console.warn(`[runs] 失速巡检: ${r.stalled.length} 条运行标 stalled (${r.stalled.slice(0, 3).join(', ')})`);
2559
+ }
2560
+ catch { /* 巡检失败不影响主流程 */ }
2561
+ })();
2562
+ }, 60_000);
2442
2563
  }
2443
2564
  catch (cronErr) {
2444
2565
  console.warn('[cron] 调度器启动失败 (non-fatal):', cronErr?.message);
@@ -2805,6 +2926,15 @@ ${goalDesc}
2805
2926
  }
2806
2927
  app.get('/', serveStaticHtml('index.html', 'index.html not found; please run `npm run build:web`', 'index'));
2807
2928
  app.get('/api-config', serveStaticHtml('api-config.html', 'api-config.html not found; please run `npm run build:web`', 'api-config'));
2929
+ // 2026-09-22 (P7): 链上浏览器页面 (只读索引的网页入口)
2930
+ // /explorer 与 /chain 两个别名都指向同一个静态页 (explorer.html + chain-explorer.js);
2931
+ // 页面只调 /api/chain/index/*, 不碰任何写接口。
2932
+ {
2933
+ const serveExplorer = serveStaticHtml('explorer.html', 'explorer.html not found; please run `npm run build:web`', 'explorer');
2934
+ app.get('/explorer', serveExplorer);
2935
+ app.get('/chain', serveExplorer);
2936
+ registerChainIndexRoutes(app);
2937
+ }
2808
2938
  // 2026-07-01 (v0.2.5): 输入验证 + 健康检查 API
2809
2939
  // - POST /api/validate-input: 前端发送前预校验, 避免后端 reject
2810
2940
  // - GET /api/health: liveness probe + validators 清单
@@ -2832,6 +2962,583 @@ ${goalDesc}
2832
2962
  res.status(400).json({ ok: false, severity: 'block', reason: e?.message ?? 'parse error' });
2833
2963
  }
2834
2964
  });
2965
+ // 2026-09-16: 运行记录 (持久化 harness) — 当前 + 历史 agent 运行。跨重载可读。
2966
+ // 2026-09-18: 智能体工具执行轨迹 + 本机 P2P 信息 (与小工具/名片交换用, 结构化)
2967
+ // 2026-09-18 (Phase 4): 交易审计 — 里程碑 / 争议 / 责任 / 事件链 一眼可查
2968
+ app.get('/api/x402/transactions', async (_req, res) => {
2969
+ try {
2970
+ const { listTransactions } = await import('../agents/x402/transaction-store.js');
2971
+ const { aggregateMilestones } = await import('../agents/x402/milestone-settlement.js');
2972
+ const txs = await listTransactions();
2973
+ res.json({
2974
+ count: txs.length,
2975
+ transactions: txs.map((t) => ({
2976
+ transactionId: t.transactionId, status: t.status, settlementFact: t.settlementFact,
2977
+ itemId: t.itemId, amount: t.amount, currency: t.currency, network: t.network,
2978
+ paymentMode: t.paymentMode, chainSettled: t.chainSettled === true, txHash: t.txHash,
2979
+ milestones: t.milestones?.length ? aggregateMilestones(t.milestones) : null,
2980
+ disputed: !!t.dispute, disputeResolved: t.dispute?.resolution?.decision || null,
2981
+ responsibility: t.responsibility?.type || null,
2982
+ partial: t.settlementFact === 'partially_settled',
2983
+ })),
2984
+ });
2985
+ }
2986
+ catch (err) {
2987
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
2988
+ }
2989
+ });
2990
+ app.get('/api/x402/transactions/:id', async (req, res) => {
2991
+ try {
2992
+ const { readTransaction, replayTransaction } = await import('../agents/x402/transaction-store.js');
2993
+ const { aggregateMilestones, milestoneGoalEligibility } = await import('../agents/x402/milestone-settlement.js');
2994
+ const rec = await readTransaction(req.params.id);
2995
+ if (!rec)
2996
+ return res.status(404).json({ error: '交易不存在' });
2997
+ res.json({
2998
+ transaction: rec,
2999
+ milestones: rec.milestones?.length ? { list: rec.milestones, aggregate: aggregateMilestones(rec.milestones) } : null,
3000
+ dispute: rec.dispute || null,
3001
+ responsibility: rec.responsibility || null,
3002
+ goalEligibility: milestoneGoalEligibility(rec, { executionOk: rec.execution?.ok === true, goalCriteriaHit: rec.goalCriteriaMet === true }),
3003
+ replay: await replayTransaction(req.params.id),
3004
+ });
3005
+ }
3006
+ catch (err) {
3007
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3008
+ }
3009
+ });
3010
+ app.get('/api/trace', async (req, res) => {
3011
+ try {
3012
+ const { listRuns } = await import('../agents/run-store.js');
3013
+ const { runToTraceJson } = await import('../agents/trace-export.js');
3014
+ const runs = await listRuns({ limit: Number(req.query.limit) || 20 });
3015
+ res.json({ runs: runs.map((r) => runToTraceJson(r)) });
3016
+ }
3017
+ catch (err) {
3018
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3019
+ }
3020
+ });
3021
+ app.get('/api/trace/:runId', async (req, res) => {
3022
+ try {
3023
+ const { readRun } = await import('../agents/run-store.js');
3024
+ const { runToTraceText, runToTraceJson } = await import('../agents/trace-export.js');
3025
+ const run = await readRun(req.params.runId);
3026
+ if (!run)
3027
+ return res.status(404).json({ error: 'run 不存在' });
3028
+ if (String(req.query.format || 'json') === 'text') {
3029
+ res.type('text/plain; charset=utf-8').send(runToTraceText(run));
3030
+ return;
3031
+ }
3032
+ res.json(runToTraceJson(run));
3033
+ }
3034
+ catch (err) {
3035
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3036
+ }
3037
+ });
3038
+ app.get('/api/p2p/info', async (_req, res) => {
3039
+ try {
3040
+ const { getLocalP2pInfo, formatP2pInfoJson } = await import('../agents/p2p-info.js');
3041
+ const info = await getLocalP2pInfo();
3042
+ res.type('application/json').send(formatP2pInfoJson(info));
3043
+ }
3044
+ catch (err) {
3045
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3046
+ }
3047
+ });
3048
+ // 2026-09-16 (2-F/2-H): 判据 (criteria) + 长期执行面板 API —— CLI/Web 读同一份 Goal 事实
3049
+ app.get('/api/goals/:id/criteria', async (req, res) => {
3050
+ try {
3051
+ const { longTermStatus } = await import('../agents/goal-criteria.js');
3052
+ const { readGoal } = await import('../agents/goal-store.js');
3053
+ const g = await readGoal(req.params.id);
3054
+ if (!g)
3055
+ return res.status(404).json({ error: 'goal 不存在' });
3056
+ const st = await longTermStatus(req.params.id);
3057
+ 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 });
3058
+ }
3059
+ catch (err) {
3060
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3061
+ }
3062
+ });
3063
+ app.post('/api/goals/:id/criteria', async (req, res) => {
3064
+ try {
3065
+ const { confirmCriteria, proposeForGoal } = await import('../agents/goal-criteria.js');
3066
+ const { readGoal } = await import('../agents/goal-store.js');
3067
+ const g = await readGoal(req.params.id);
3068
+ if (!g)
3069
+ return res.status(404).json({ error: 'goal 不存在' });
3070
+ const criteria = Array.isArray(req.body?.criteria) ? req.body.criteria.map((c) => String(c)) : undefined;
3071
+ if (req.body?.propose === true) {
3072
+ const p = await proposeForGoal(req.params.id);
3073
+ return res.json({ ok: p.ok, proposed: p.criteria, needsHuman: !!p.needsHuman, reason: p.reason || null });
3074
+ }
3075
+ // 默认: 确认 (可同时改内容)
3076
+ const r = await confirmCriteria(req.params.id, { criteria, by: 'web' });
3077
+ res.status(r.ok ? 200 : 409).json({ ok: r.ok, reason: r.reason || null, criteria: r.goal?.successCriteria, criteriaVersion: r.goal?.criteriaVersion });
3078
+ }
3079
+ catch (err) {
3080
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3081
+ }
3082
+ });
3083
+ // 长期执行面板 (纯静态页, 直接打已有 API; 未 ready 时 chat 路由本来就 503)
3084
+ app.get('/goals', (_req, res) => {
3085
+ res.type('html').send(`<!doctype html><meta charset="utf-8"><title>Bolloon 长期执行</title>
3086
+ <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}
3087
+ 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}
3088
+ .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>
3089
+ <h1>长期执行面板</h1>
3090
+ <p class="dim">数据 = <code>/api/goals</code> · <code>/api/runs</code> · <code>/api/goals/:id/criteria</code> · <code>/api/supervisor</code> (与 CLI 同一份事实)</p>
3091
+ <div id="sup"></div>
3092
+ <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>
3093
+ <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>
3094
+ <pre id="log" class="dim"></pre>
3095
+ <script>
3096
+ const log = (m) => { document.getElementById('log').textContent = typeof m === 'string' ? m : JSON.stringify(m, null, 1); };
3097
+ async function j(u){ const r = await fetch(u); return await r.json(); }
3098
+ async function view(id){
3099
+ const c = await j('/api/goals/'+id+'/criteria');
3100
+ log(c);
3101
+ }
3102
+ async function act(id, what){
3103
+ 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()); }
3104
+ 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()); }
3105
+ 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()); }
3106
+ else if (what === 'resume') { const r = await fetch('/api/runs/'+id+'/resume',{method:'POST'}); log(await r.json()); }
3107
+ else if (what === 'pause') { const r = await fetch('/api/runs/'+id+'/pause',{method:'POST'}); log(await r.json()); }
3108
+ else if (what === 'abort') { const r = await fetch('/api/runs/'+id+'/abort',{method:'POST'}); log(await r.json()); }
3109
+ refresh();
3110
+ }
3111
+ async function refresh(){
3112
+ try {
3113
+ const sup = await j('/api/supervisor');
3114
+ document.getElementById('sup').innerHTML = '<b>Supervisor</b> owner='+(sup.state?.owner||'-')+' worker='+(sup.state?.workerId||'-')+' ticks='+(sup.ticks||0)+
3115
+ ' <span class="dim">lease='+(sup.leaseTtlMs||'-')+'ms tick='+(sup.tickIntervalMs||'-')+'ms</span>';
3116
+ } catch(e){ document.getElementById('sup').textContent = 'supervisor 不可用: '+e; }
3117
+ const gs = await j('/api/goals');
3118
+ const gtb = document.querySelector('#goals tbody'); gtb.innerHTML = '';
3119
+ for (const g of (gs.goals || [])) {
3120
+ let crit = g.successCriteria?.length ? (g.completedCriteria?.length||0)+'/'+g.successCriteria.length : '<span class="bad">无判据</span>';
3121
+ const src = g.criteriaSource ? ' <span class="dim">('+g.criteriaSource+(g.criteriaConfirmed?'✓':'?')+')</span>' : '';
3122
+ const tr = document.createElement('tr');
3123
+ tr.innerHTML = '<td><code>'+g.goalId+'</code></td><td>'+g.status+'</td><td>'+crit+src+'</td><td class="dim">'+
3124
+ ((g.continuation&&(g.continuation.nextAction||g.continuation.wakeReason))||'-')+'</td><td>'+((g.runs||[]).length)+'</td>'+
3125
+ '<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>';
3126
+ gtb.appendChild(tr);
3127
+ }
3128
+ const rs = await j('/api/runs');
3129
+ const rtb = document.querySelector('#runs tbody'); rtb.innerHTML = '';
3130
+ for (const r of (rs.runs || []).slice(-20).reverse()) {
3131
+ const tr = document.createElement('tr');
3132
+ 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>'+
3133
+ '<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>';
3134
+ rtb.appendChild(tr);
3135
+ }
3136
+ }
3137
+ refresh(); setInterval(refresh, 5000);
3138
+ </script>`);
3139
+ });
3140
+ // 2026-09-16 (Phase 4): Onboard API —— 与 CLI 共用同一条执行器 (src/setup/onboard.ts)
3141
+ // 页面只负责"提交本步输入", 阶段判定/校验/真实验证/原子提交/门禁全在服务端同一份事实里。
3142
+ const setupState = async () => {
3143
+ const { evaluateSetup } = await import('../setup/setup-store.js');
3144
+ const { nextStepInfo } = await import('../setup/onboard.js');
3145
+ const ev = await evaluateSetup();
3146
+ const next = ev.gate === 'ready' ? null : await nextStepInfo(ev.state).catch(() => null);
3147
+ return { gate: ev.gate, stage: ev.state.stage, readiness: ev.state.readiness, readinessWhy: ev.state.readinessWhy,
3148
+ allow: ev.state.allow, completed: ev.state.completed, inputs: ev.state.inputs, checks: ev.state.checks,
3149
+ lastError: ev.state.lastError || null, actions: ev.state.actions, reasons: ev.reasons, nextStep: next };
3150
+ };
3151
+ app.get('/api/setup', async (_req, res) => {
3152
+ try {
3153
+ res.json(await setupState());
3154
+ }
3155
+ catch (err) {
3156
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
3157
+ }
3158
+ });
3159
+ // 跑一步 (answers 按顺序喂给执行器; oneShot: 一步失败就返回, 不阻塞)
3160
+ const runSetupStep = async (req, res, mode) => {
3161
+ try {
3162
+ const { runOnboard, ScriptedIO } = await import('../setup/onboard.js');
3163
+ const answers = Array.isArray(req.body?.answers) ? req.body.answers.map((x) => String(x ?? '')) : [];
3164
+ const io = new ScriptedIO(answers);
3165
+ const r = await runOnboard({ mode, io, oneShot: true, targets: req.body?.targets });
3166
+ res.json({ ...(await setupState()), ok: r.ok, failedStage: r.failedStage || null, errorClass: r.errorClass || null,
3167
+ message: r.message || null, steps: r.steps, stepLog: io.log.slice(-40), summary: r.summary });
3168
+ }
3169
+ catch (err) {
3170
+ res.status(500).json({ ok: false, error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
3171
+ }
3172
+ };
3173
+ app.post('/api/setup/start', async (_req, res) => { try {
3174
+ res.json(await setupState());
3175
+ }
3176
+ catch (err) {
3177
+ res.status(500).json({ error: String(err) });
3178
+ } });
3179
+ app.post('/api/setup/step', async (req, res) => runSetupStep(req, res, 'resume'));
3180
+ app.post('/api/setup/resume', async (req, res) => runSetupStep(req, res, 'resume'));
3181
+ app.post('/api/setup/test', async (req, res) => runSetupStep(req, res, 'test'));
3182
+ app.post('/api/setup/repair', async (req, res) => runSetupStep(req, res, 'repair'));
3183
+ app.post('/api/setup/reconfigure', async (req, res) => runSetupStep(req, res, 'reconfigure'));
3184
+ app.post('/api/setup/commit', async (_req, res) => {
3185
+ try {
3186
+ const { refreshSetupState } = await import('../setup/setup-store.js');
3187
+ const ev = await refreshSetupState({});
3188
+ const { describeSetup } = await import('../setup/setup-store.js');
3189
+ res.json({ ...(await setupState()), ok: ev.gate === 'ready', summary: describeSetup(ev) });
3190
+ }
3191
+ catch (err) {
3192
+ res.status(500).json({ ok: false, error: String(err) });
3193
+ }
3194
+ });
3195
+ app.post('/api/setup/identity', async (req, res) => runSetupStep({ body: { answers: [req.body?.name] } }, res, 'resume'));
3196
+ app.post('/api/setup/provider', async (req, res) => runSetupStep({ body: { answers: [req.body?.provider] } }, res, 'resume'));
3197
+ // 首启页面 (零依赖, 直接打这些 API; 未 ready 时所有对话路由都是 503)
3198
+ app.get('/setup', (_req, res) => {
3199
+ res.type('html').send(`<!doctype html><meta charset="utf-8"><title>Bolloon 初始化</title>
3200
+ <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}
3201
+ h1{font-size:20px}pre{background:#f6f6f6;padding:12px;border-radius:8px;white-space:pre-wrap}
3202
+ .ok{color:#0a7d32}.bad{color:#b00020}input,select,button{font-size:15px;padding:8px;margin:4px 0}
3203
+ .row{margin:10px 0}small{color:#666}</style>
3204
+ <h1>Bolloon 初始化</h1>
3205
+ <div id="state">加载中…</div>
3206
+ <div class="row" id="form"></div>
3207
+ <div class="row"><button onclick="run('step')">提交这一步</button>
3208
+ <button onclick="run('test')">重新测试连通性/运行时</button>
3209
+ <button onclick="run('repair')">修复/迁移配置</button>
3210
+ <button onclick="fetchState()">刷新</button></div>
3211
+ <pre id="log"></pre>
3212
+ <script>
3213
+ async function fetchState(){
3214
+ const r = await fetch('/api/setup'); const s = await r.json();
3215
+ const ready = s.gate==='ready';
3216
+ document.getElementById('state').innerHTML =
3217
+ '<b>阶段</b>: '+s.stage+' &nbsp; <b>门禁</b>: <span class="'+(ready?'ok':'bad')+'">'+s.gate+'</span><br>'+
3218
+ '<b>就绪度</b>: basic '+s.readiness.basic+' · agent '+s.readiness.agent+' · durable '+s.readiness.durable+' · network '+s.readiness.network+'<br>'+
3219
+ '<small>已完成: '+(s.completed.join(' → ')||'(无)')+'</small><br>'+
3220
+ '<small>下一步: '+(s.actions[0]||'')+'</small>'+
3221
+ (s.lastError?'<br><small class="bad">上次错误 ['+s.lastError.errorClass+'] '+s.lastError.message+'</small>':'');
3222
+ const f = document.getElementById('form');
3223
+ const n = s.nextStep;
3224
+ if(!n){ f.innerHTML='<b class="ok">✅ 已就绪, 可以进入对话</b>'; window.currentNeeds=null; return; }
3225
+ window.currentNeeds=n.needs;
3226
+ if(n.needs==='provider'){
3227
+ 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>';
3228
+ } else if(n.needs==='credential'){
3229
+ f.innerHTML='<div>'+n.question+'</div><input id="v" type="password" placeholder="API key (不会回显)" autocomplete="off">';
3230
+ } else if(n.needs==='none'){
3231
+ f.innerHTML='<div>'+n.question+' (点上面按钮执行)</div>';
3232
+ } else {
3233
+ f.innerHTML='<div>'+n.question+'</div><input id="v" value="'+(n.defaultValue||'')+'">';
3234
+ }
3235
+ }
3236
+ async function run(ep){
3237
+ const v=document.getElementById('v'); const n=window.currentNeeds;
3238
+ const answers = (n==='credential'||n==='name'||n==='model'||n==='provider') && v ? [v.value] : [];
3239
+ if(v) v.value=''; // key 立即从 DOM 里清掉
3240
+ const r = await fetch('/api/setup/'+ep,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({answers})});
3241
+ const j = await r.json();
3242
+ document.getElementById('log').textContent = (j.log||[]).join('\n') + '\n' + (j.summary||'');
3243
+ fetchState();
3244
+ }
3245
+ fetchState();
3246
+ </script>`);
3247
+ });
3248
+ // 2026-09-16 (M1/M3): 初始化状态 —— CLI 与 Web 读同一份事实 (setup-state.json + 各领域 store)
3249
+ app.get('/api/setup', async (_req, res) => {
3250
+ try {
3251
+ const { evaluateSetup, describeSetup, resolveBolloonHome } = await import('../setup/setup-store.js');
3252
+ const ev = await evaluateSetup({ light: false });
3253
+ 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() });
3254
+ }
3255
+ catch (err) {
3256
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
3257
+ }
3258
+ });
3259
+ // 2026-09-16 (2-G.1): Skills Manager —— 与 CLI `/skills` `/skill` 读同一份事实 (skills-registry.json + 技能目录)
3260
+ app.get('/api/skills', async (_req, res) => {
3261
+ try {
3262
+ const { getSkillsManager } = await import('../agents/skills-manager.js');
3263
+ const sm = getSkillsManager();
3264
+ const skills = await sm.view();
3265
+ res.json({ count: skills.length, registryPath: (await import('../agents/skills-manager.js')).registryPath(), skills, health: await sm.health() });
3266
+ }
3267
+ catch (err) {
3268
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3269
+ }
3270
+ });
3271
+ app.get('/api/skills/health', async (_req, res) => {
3272
+ try {
3273
+ const { getSkillsManager } = await import('../agents/skills-manager.js');
3274
+ res.json(await getSkillsManager().health());
3275
+ }
3276
+ catch (err) {
3277
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3278
+ }
3279
+ });
3280
+ app.get('/api/skills/:name', async (req, res) => {
3281
+ try {
3282
+ const { getSkillsManager } = await import('../agents/skills-manager.js');
3283
+ const s = await getSkillsManager().inspect(String(req.params.name));
3284
+ if (!s) {
3285
+ res.status(404).json({ error: `没有这个技能: ${req.params.name}` });
3286
+ return;
3287
+ }
3288
+ res.json({ skill: s });
3289
+ }
3290
+ catch (err) {
3291
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3292
+ }
3293
+ });
3294
+ app.post('/api/skills/import', async (req, res) => {
3295
+ try {
3296
+ const ref = String(req.body?.ref || '').trim();
3297
+ if (!ref) {
3298
+ res.status(400).json({ error: '要 body {ref: "bolloon://skill/<cid>" | "ipfs://<cid>" | "<cid>"}' });
3299
+ return;
3300
+ }
3301
+ const { getSkillsManager } = await import('../agents/skills-manager.js');
3302
+ const r = await getSkillsManager().import(ref, { force: !!req.body?.force });
3303
+ if (!r.ok) {
3304
+ res.status(409).json({ error: r.error });
3305
+ return;
3306
+ }
3307
+ res.json({ ok: true, name: r.name, version: r.version, skill: r.skill });
3308
+ }
3309
+ catch (err) {
3310
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3311
+ }
3312
+ });
3313
+ // enable / disable / approve / validate / quarantine (Express 5 不支持内联正则, 显式写开)
3314
+ for (const action of ['enable', 'disable', 'approve', 'validate', 'quarantine']) {
3315
+ app.post(`/api/skills/:name/${action}`, async (req, res) => {
3316
+ try {
3317
+ const { getSkillsManager } = await import('../agents/skills-manager.js');
3318
+ const sm = getSkillsManager();
3319
+ const name = String(req.params.name);
3320
+ const r = action === 'approve' ? await sm.approve(name, 'web')
3321
+ : action === 'quarantine' ? await sm.quarantine(name, String(req.body?.reason || 'web 手动隔离'))
3322
+ : action === 'enable' ? await sm.enable(name)
3323
+ : action === 'disable' ? await sm.disable(name)
3324
+ : await sm.validate(name);
3325
+ if (!r.ok && !r.skill) {
3326
+ res.status(409).json({ error: r.reason || r.issues || '操作未完成' });
3327
+ return;
3328
+ }
3329
+ res.json({ ok: r.ok, name, action, skill: r.skill, issues: r.issues });
3330
+ }
3331
+ catch (err) {
3332
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3333
+ }
3334
+ });
3335
+ }
3336
+ // 2026-09-16 (M2-B): 长期执行层诊断 + 手动推进 —— 与 CLI /supervise 读同一份持久化记录
3337
+ app.get('/api/supervisor', async (_req, res) => {
3338
+ try {
3339
+ const { getSupervisor } = await import('../agents/execution-supervisor.js');
3340
+ const { readSupervisorState, supervisorStatePath } = await import('../agents/supervisor-host.js');
3341
+ const { wakeReport, listRunnableGoals } = await import('../agents/goal-store.js');
3342
+ const { runnable, skipped } = await listRunnableGoals({ now: Date.now() });
3343
+ res.json({
3344
+ supervisor: getSupervisor().status(),
3345
+ /** 宿主身份/心跳 (谁在跑、跑到哪、上次有没有好好停) */
3346
+ host: await readSupervisorState(),
3347
+ hostStatePath: supervisorStatePath(),
3348
+ wake: await wakeReport(),
3349
+ runnable: runnable.map((g) => ({ goalId: g.goalId, objective: g.objective, status: g.status })),
3350
+ skipped,
3351
+ });
3352
+ }
3353
+ catch (err) {
3354
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3355
+ }
3356
+ });
3357
+ // 手动踢一个调度周期 (调试/验收用; 不改变任何"事实来源")
3358
+ app.post('/api/supervisor/tick', async (_req, res) => {
3359
+ if (!(await setupGate(res)))
3360
+ return;
3361
+ try {
3362
+ const { getSupervisor } = await import('../agents/execution-supervisor.js');
3363
+ const report = await getSupervisor().tickOnce();
3364
+ res.json({ ok: true, report });
3365
+ }
3366
+ catch (err) {
3367
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3368
+ }
3369
+ });
3370
+ // 2026-09-16 (2-E 第 4 类): 外部事件到达 → 唤醒在等它的 Goal (不重发已成功的请求)
3371
+ app.post('/api/goals/:goalId/wake', async (req, res) => {
3372
+ const goalId = String(req.params.goalId);
3373
+ try {
3374
+ const { getSupervisor } = await import('../agents/execution-supervisor.js');
3375
+ const { readGoal, setContinuation } = await import('../agents/goal-store.js');
3376
+ const g = await readGoal(goalId);
3377
+ if (!g) {
3378
+ res.status(404).json({ error: `goal 不存在: ${goalId}` });
3379
+ return;
3380
+ }
3381
+ const woke = await getSupervisor().notifyExternal(goalId);
3382
+ if (!woke) {
3383
+ // 没在等外部事件也要如实回答 (可能它其实在等人/在跑), 但允许显式加急
3384
+ if (req.body?.force) {
3385
+ await setContinuation(goalId, { wakeReason: 'active', autoContinue: true, wakeAt: undefined, needsExternal: undefined });
3386
+ res.json({ ok: true, goalId, woke: false, forced: true, note: `原状态 ${g.status} (不在等外部事件), 已按 force 加急` });
3387
+ return;
3388
+ }
3389
+ res.status(409).json({ error: `Goal 不在等外部事件 (status=${g.status}, wakeReason=${g.continuation?.wakeReason || '无'})`, force_hint: '加 body {"force":true} 可强制加急' });
3390
+ return;
3391
+ }
3392
+ res.json({ ok: true, goalId, woke: true, note: '已唤醒: 下一次 Supervisor tick 会推进它' });
3393
+ }
3394
+ catch (err) {
3395
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3396
+ }
3397
+ });
3398
+ // 2026-09-16 (M5): 运行列表 —— Web 与 CLI 读同一份 run 事实 (~/.bolloon/runs)
3399
+ app.get('/api/runs', async (req, res) => {
3400
+ try {
3401
+ const { listRuns, formatRunLine } = await import('../agents/run-store.js');
3402
+ const status = String(req.query?.status || '').trim();
3403
+ const limit = Number(req.query?.limit || 20);
3404
+ const runs = await listRuns({ status: status ? status : undefined, limit });
3405
+ res.json({
3406
+ count: runs.length,
3407
+ runs,
3408
+ lines: runs.map(formatRunLine),
3409
+ });
3410
+ }
3411
+ catch (err) {
3412
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3413
+ }
3414
+ });
3415
+ // 2026-09-16 (M5): 单个 run + 停止原因/checkpoint/最近步骤/recovery/harness 事件 (两端同一份事实)
3416
+ app.get('/api/runs/:runId', async (req, res) => {
3417
+ try {
3418
+ const { readRun } = await import('../agents/run-store.js');
3419
+ const run = await readRun(String(req.params.runId));
3420
+ if (!run) {
3421
+ res.status(404).json({ error: `run 不存在: ${req.params.runId}` });
3422
+ return;
3423
+ }
3424
+ const goal = run.goalId ? await (await import('../agents/goal-store.js')).readGoal(run.goalId) : null;
3425
+ res.json({ run, goal, checkpoint: run.checkpoint, steps: run.steps, recovery: run.recovery, harness: run.harness || [] });
3426
+ }
3427
+ catch (err) {
3428
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3429
+ }
3430
+ });
3431
+ // 2026-09-16 (M5): 继续 interrupted/stalled/paused/needs_human/awaiting_external 的运行 —— 真从 checkpoint 继续
3432
+ app.post('/api/runs/:runId/resume', async (req, res) => {
3433
+ const runId = String(req.params.runId);
3434
+ try {
3435
+ const { readRun, RESUMABLE_STATUSES } = await import('../agents/run-store.js');
3436
+ const run = await readRun(runId);
3437
+ if (!run) {
3438
+ res.status(404).json({ error: `run 不存在: ${runId}` });
3439
+ return;
3440
+ }
3441
+ // 先做只读校验: 不可恢复的状态明确拒绝 (不假装"已开始"), 避免前端以为在跑
3442
+ if (!RESUMABLE_STATUSES.includes(run.status)) {
3443
+ res.status(409).json({ error: `状态 ${run.status} 不可恢复 (可恢复: ${RESUMABLE_STATUSES.join('/')})` });
3444
+ return;
3445
+ }
3446
+ if (!run.channelId) {
3447
+ res.status(400).json({ error: '该 run 没有 channelId, 无法在 web 端恢复 (请在 CLI 用 /resume)' });
3448
+ return;
3449
+ }
3450
+ const agent = await getAgentForChannel(run.channelId);
3451
+ if (!agent?.resumeRun) {
3452
+ res.status(409).json({ error: '该 channel 的 agent 不支持 resumeRun' });
3453
+ return;
3454
+ }
3455
+ // 异步执行: resume 可能跑几分钟; 状态从 /api/runs/:runId 读 (recovering → running → done/failed)
3456
+ void agent.resumeRun(runId).then((r) => {
3457
+ if (!r?.ok)
3458
+ console.warn(`[runs] resume ${runId} 失败:`, r?.reason);
3459
+ }).catch((err) => console.warn(`[runs] resume ${runId} 异常:`, err?.message));
3460
+ res.json({ ok: true, accepted: true, runId, note: '已开始恢复; 状态请读 GET /api/runs/:runId' });
3461
+ }
3462
+ catch (err) {
3463
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3464
+ }
3465
+ });
3466
+ // 2026-09-16 (M5): 人工批准 —— needs_human 的运行经人确认后继续 (= 带批准的 resume)
3467
+ app.post('/api/runs/:runId/approve', async (req, res) => {
3468
+ const runId = String(req.params.runId);
3469
+ try {
3470
+ const { readRun, recordRecovery } = await import('../agents/run-store.js');
3471
+ const run = await readRun(runId);
3472
+ if (!run) {
3473
+ res.status(404).json({ error: `run 不存在: ${runId}` });
3474
+ return;
3475
+ }
3476
+ if (run.status !== 'needs_human') {
3477
+ res.status(409).json({ error: `只有 needs_human 的运行需要批准 (当前 ${run.status})` });
3478
+ return;
3479
+ }
3480
+ await recordRecovery(runId, { errorClass: run.errorClass || 'unknown', message: '人工批准后继续', action: 'resume' });
3481
+ if (!run.channelId) {
3482
+ res.status(400).json({ error: '该 run 没有 channelId, 请在 CLI 用 /approve' });
3483
+ return;
3484
+ }
3485
+ const agent = await getAgentForChannel(run.channelId, run.channelId, run.agentId, {});
3486
+ if (!agent?.resumeRun) {
3487
+ res.status(409).json({ error: '该 channel 的 agent 不支持 resumeRun' });
3488
+ return;
3489
+ }
3490
+ void agent.resumeRun(runId).catch((err) => console.warn(`[runs] approve ${runId} 异常:`, err?.message));
3491
+ res.json({ ok: true, accepted: true, runId, approved: true });
3492
+ }
3493
+ catch (err) {
3494
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3495
+ }
3496
+ });
3497
+ // 2026-09-16 (M5): 暂停 / 中止 —— 只改 run 状态; 运行中的 agent 会在下一次循环检查时如实停下
3498
+ // (Express 5 的 path-to-regexp 不再支持 `:action(a|b)` 内联正则, 所以两条写开)
3499
+ for (const [suffix, to] of [['pause', 'paused'], ['abort', 'aborted']]) {
3500
+ app.post(`/api/runs/:runId/${suffix}`, async (req, res) => {
3501
+ const runId = String(req.params.runId);
3502
+ try {
3503
+ const { setRunStatus } = await import('../agents/run-store.js');
3504
+ const r = await setRunStatus(runId, to, { error: `外部请求 (${suffix})` });
3505
+ if (!r.ok) {
3506
+ res.status(409).json({ error: r.reason || '状态迁移被拒绝' });
3507
+ return;
3508
+ }
3509
+ res.json({ ok: true, runId, status: to, note: '运行中的 agent 会在下一轮循环检查时停下 (轮内不打断)' });
3510
+ }
3511
+ catch (err) {
3512
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3513
+ }
3514
+ });
3515
+ }
3516
+ // 2026-09-16 (M2): 目标 (Goal) —— 目标事实来源; runId → goalId → objective/successCriteria 反查链
3517
+ app.get('/api/goals', async (req, res) => {
3518
+ try {
3519
+ const { listGoals, formatGoalLine } = await import('../agents/goal-store.js');
3520
+ const status = String(req.query?.status || '').trim();
3521
+ const goals = await listGoals({ status: status ? status : undefined, limit: Number(req.query?.limit || 30) });
3522
+ res.json({ count: goals.length, goals, lines: goals.map(formatGoalLine) });
3523
+ }
3524
+ catch (err) {
3525
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3526
+ }
3527
+ });
3528
+ app.get('/api/goals/:goalId', async (req, res) => {
3529
+ try {
3530
+ const { readGoal, evaluateGoalCompletion } = await import('../agents/goal-store.js');
3531
+ const goal = await readGoal(String(req.params.goalId));
3532
+ if (!goal) {
3533
+ res.status(404).json({ error: `goal 不存在: ${req.params.goalId}` });
3534
+ return;
3535
+ }
3536
+ res.json({ goal, completion: evaluateGoalCompletion(goal) });
3537
+ }
3538
+ catch (err) {
3539
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3540
+ }
3541
+ });
2835
3542
  // 2026-08-13 (Phase E1): Agent 服务 Registry — 发现层 (OrbitDB 去中心化 + 本地 fallback)
2836
3543
  app.get('/api/registry', async (_req, res) => {
2837
3544
  try {
@@ -3889,7 +4596,29 @@ ${goalDesc}
3889
4596
  console.log(`[SSE] 客户端断开 channelId=${channelId || '(broadcast)'}, 剩余=${sseClients.size}`);
3890
4597
  });
3891
4598
  });
4599
+ // 2026-09-16 (M4): agent 执行硬门禁 —— 未 ready 时这些路由 503 + 结构化初始化状态 (Web 只显示初始化页)
4600
+ const setupGate = async (res) => {
4601
+ try {
4602
+ const { getSetupGateCached } = await import('../setup/setup-store.js');
4603
+ const { gate, state } = await getSetupGateCached();
4604
+ if (gate === 'ready')
4605
+ return true;
4606
+ res.status(503).json({
4607
+ error: `初始化未就绪 (${gate}, 阶段 ${state.stage}) — 请先完成初始化`,
4608
+ gate, stage: state.stage, readiness: state.readiness,
4609
+ nextActions: state.actions, lastError: state.lastError || null,
4610
+ });
4611
+ return false;
4612
+ }
4613
+ catch (err) {
4614
+ // fail-closed: 连门禁都读不出来 → 不放行
4615
+ res.status(503).json({ error: `初始化状态不可读 (fail-closed): ${String(err?.message || err).slice(0, 160)}`, gate: 'blocked' });
4616
+ return false;
4617
+ }
4618
+ };
3892
4619
  app.post('/message', async (req, res) => {
4620
+ if (!(await setupGate(res)))
4621
+ return;
3893
4622
  const { text, channelId, channelDid, attachments } = req.body;
3894
4623
  if (!text) {
3895
4624
  return res.status(400).json({ error: 'No text provided' });
@@ -6082,8 +6811,12 @@ ${goalDesc}
6082
6811
  });
6083
6812
  // 2026-07-06: Task Queue API 抽到 ./routes-tasks.ts
6084
6813
  registerTaskRoutes(app, { broadcast, getAgentForChannel });
6814
+ // 2026-09-19: 社交身份 API (DID + 已验证联系方式 + 联系能力 + 权限 + 证据)
6815
+ // 只返回脱敏值; 明文联系方式不进 Run/prompt/Git
6816
+ registerContactRoutes(app, {});
6085
6817
  // 2026-09-13: 微支付信息服务 (x402) — 发布 / 402 收款 / 买方代付 / 验真
6086
6818
  registerX402InfoRoutes(app);
6819
+ registerOnchainTradeRoutes(app);
6087
6820
  // 2026-07-06: LLM/Video/Audio 配置路由抽到 ./routes-llm-config.ts
6088
6821
  registerLlmConfigRoutes(app);
6089
6822
  // 2026-07-22: 外部编码智能体 (codex/claude-code/opencode/openclaw/hermes/实验 API)
@@ -7501,6 +8234,35 @@ ${goalDesc}
7501
8234
  console.warn('[24h] Failed to load heartbeat modules:', err);
7502
8235
  }
7503
8236
  // 健康检查端点
8237
+ // 2026-09-18: 网络脉冲公开只读投影 (给 bolloon-UI 网关页; 无认证, 短缓存, 只有聚合)
8238
+ // 永远不返回原始事件 / registry / DID / peerId / IP / 钱包 / 任务正文。
8239
+ app.get('/api/public/network/progress', async (req, res) => {
8240
+ try {
8241
+ const np = await import('../agents/network-pulse.js');
8242
+ const snap = await np.getNetworkPulse({});
8243
+ // agent_sites = 本节点**显式发布**的公开指针 (IPNS); 没发布就是空数组, 不猜
8244
+ const body = JSON.stringify({ ...snap, status: np.snapshotStatus(snap), agent_sites: np.readAgentSites() });
8245
+ const { createHash } = await import('crypto');
8246
+ const etag = '"' + createHash('sha256').update(body).digest('hex').slice(0, 32) + '"';
8247
+ res.setHeader('Cache-Control', 'public, max-age=15, stale-while-revalidate=15');
8248
+ res.setHeader('ETag', etag);
8249
+ if (req.headers['if-none-match'] === etag) {
8250
+ res.status(304).end();
8251
+ return;
8252
+ }
8253
+ res.type('application/json').send(body);
8254
+ }
8255
+ catch (e) {
8256
+ // 观察层不可用 → 如实 unavailable (不是 500 空体, 也不是"网络为空")
8257
+ try {
8258
+ const np = await import('../agents/network-pulse.js');
8259
+ res.type('application/json').send(JSON.stringify(await np.getNetworkPulse({ unavailable: true })));
8260
+ }
8261
+ catch {
8262
+ res.status(503).json({ status: 'unavailable', error: 'network pulse unavailable' });
8263
+ }
8264
+ }
8265
+ });
7504
8266
  app.get('/api/health', async (req, res) => {
7505
8267
  try {
7506
8268
  if (!healthMonitor) {