@bolloon/bolloon-agent 0.4.33 → 0.5.1

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 (214) hide show
  1. package/dist/agents/contacts/chain.js +27 -7
  2. package/dist/agents/execution-supervisor.js +580 -40
  3. package/dist/agents/external-events.js +35 -6
  4. package/dist/agents/gateway-group.js +100 -12
  5. package/dist/agents/goal-criteria.js +10 -3
  6. package/dist/agents/goal-flywheel/block-executor.js +346 -0
  7. package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
  8. package/dist/agents/goal-flywheel/goal-change.js +754 -0
  9. package/dist/agents/goal-flywheel/index.js +26 -0
  10. package/dist/agents/goal-flywheel/memory-layers.js +227 -0
  11. package/dist/agents/goal-flywheel/run-closure.js +809 -0
  12. package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
  13. package/dist/agents/goal-flywheel/types.js +329 -0
  14. package/dist/agents/goal-flywheel/wiring/change.js +288 -0
  15. package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
  16. package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
  17. package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
  18. package/dist/agents/goal-flywheel/wiring/index.js +26 -0
  19. package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
  20. package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
  21. package/dist/agents/goal-flywheel/work-contract.js +478 -0
  22. package/dist/agents/goal-flywheel/work-monitor.js +488 -0
  23. package/dist/agents/goal-flywheel-wiring.js +1870 -0
  24. package/dist/agents/goal-state-reducer.js +406 -0
  25. package/dist/agents/goal-store.js +35 -1
  26. package/dist/agents/mobile-flywheel-view.js +144 -0
  27. package/dist/agents/mobile-task-actions-verify.js +74 -0
  28. package/dist/agents/mobile-task-actions.js +178 -0
  29. package/dist/agents/mobile-task-views.js +199 -0
  30. package/dist/agents/model-policy.js +651 -0
  31. package/dist/agents/network-pulse.js +304 -18
  32. package/dist/agents/pi-sdk-tools.js +84 -33
  33. package/dist/agents/pi-sdk.js +157 -42
  34. package/dist/agents/run-store.js +71 -5
  35. package/dist/agents/skill-readiness.js +52 -10
  36. package/dist/agents/subagent-manager.js +209 -3
  37. package/dist/agents/task/task-runner.js +17 -4
  38. package/dist/agents/task-group.js +8 -51
  39. package/dist/agents/task-public-text.js +89 -0
  40. package/dist/agents/workflow-pivot-loop.js +19 -12
  41. package/dist/cli/clipboard.js +65 -0
  42. package/dist/cli/commands/index.js +1 -0
  43. package/dist/cli/commands/tasks.js +350 -2
  44. package/dist/cli/identity-command.js +177 -0
  45. package/dist/cli/ink-app.js +145 -17
  46. package/dist/cli/log-gate.js +386 -0
  47. package/dist/cli/mention-data.js +2 -0
  48. package/dist/cli/model-selector.js +538 -0
  49. package/dist/cli/reply-hygiene.js +54 -0
  50. package/dist/cli/setup-wizard.js +757 -69
  51. package/dist/cli/startup-notice.js +160 -0
  52. package/dist/cli/stores.js +22 -1
  53. package/dist/cli/theme.js +49 -0
  54. package/dist/cli/tui-select.js +970 -0
  55. package/dist/cli/update-commands.js +150 -24
  56. package/dist/cli-entry.js +137 -20
  57. package/dist/electron-build/agents/contacts/chain.js +26 -6
  58. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  59. package/dist/electron-build/agents/execution-supervisor.js +578 -38
  60. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  61. package/dist/electron-build/agents/external-events.js +34 -5
  62. package/dist/electron-build/agents/external-events.js.map +1 -1
  63. package/dist/electron-build/agents/goal-criteria.js +10 -3
  64. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  65. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  66. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  67. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  68. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  69. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  70. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  71. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  72. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  73. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  74. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  75. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  76. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  78. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  79. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  80. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  81. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  82. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  83. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  84. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  85. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  86. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  87. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  88. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  89. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  90. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  91. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  92. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  93. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  94. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  95. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  96. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  97. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  98. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  99. package/dist/electron-build/agents/goal-store.js +35 -1
  100. package/dist/electron-build/agents/goal-store.js.map +1 -1
  101. package/dist/electron-build/agents/model-policy.js +706 -0
  102. package/dist/electron-build/agents/model-policy.js.map +1 -0
  103. package/dist/electron-build/agents/network-pulse.js +307 -19
  104. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  105. package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
  106. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  107. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  108. package/dist/electron-build/agents/pi-sdk.js +156 -41
  109. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  110. package/dist/electron-build/agents/run-store.js +75 -6
  111. package/dist/electron-build/agents/run-store.js.map +1 -1
  112. package/dist/electron-build/agents/skill-readiness.js +52 -9
  113. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  114. package/dist/electron-build/agents/task/task-runner.js +16 -3
  115. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  116. package/dist/electron-build/agents/workflow-pivot-loop.js +19 -12
  117. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  118. package/dist/electron-build/llm/config-store.js +146 -6
  119. package/dist/electron-build/llm/config-store.js.map +1 -1
  120. package/dist/electron-build/llm/connection-probe.js +764 -0
  121. package/dist/electron-build/llm/connection-probe.js.map +1 -0
  122. package/dist/electron-build/llm/custom-provider-store.js +272 -0
  123. package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
  124. package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
  125. package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
  126. package/dist/electron-build/llm/model-catalog.js +778 -0
  127. package/dist/electron-build/llm/model-catalog.js.map +1 -0
  128. package/dist/electron-build/llm/model-discovery.js +1202 -0
  129. package/dist/electron-build/llm/model-discovery.js.map +1 -0
  130. package/dist/electron-build/llm/model-selection.js +1315 -0
  131. package/dist/electron-build/llm/model-selection.js.map +1 -0
  132. package/dist/electron-build/llm/pi-ai.js +70 -11
  133. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  134. package/dist/electron-build/llm/provider-catalog.js +969 -0
  135. package/dist/electron-build/llm/provider-catalog.js.map +1 -0
  136. package/dist/electron-build/llm/provider-registry.js +950 -0
  137. package/dist/electron-build/llm/provider-registry.js.map +1 -0
  138. package/dist/electron-build/llm/tool-name.js +266 -0
  139. package/dist/electron-build/llm/tool-name.js.map +1 -0
  140. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  141. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  142. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  143. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  144. package/dist/electron-build/utils/dual-source-facts.js +236 -0
  145. package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
  146. package/dist/electron-build/utils/dual-source.js +347 -0
  147. package/dist/electron-build/utils/dual-source.js.map +1 -0
  148. package/dist/electron-build/utils/update-manager.js +328 -104
  149. package/dist/electron-build/utils/update-manager.js.map +1 -1
  150. package/dist/electron-build/utils/update-state.js +9 -11
  151. package/dist/electron-build/utils/update-state.js.map +1 -1
  152. package/dist/electron-build/utils/version-identity.js +193 -0
  153. package/dist/electron-build/utils/version-identity.js.map +1 -0
  154. package/dist/electron-build/utils/version-info.js +57 -31
  155. package/dist/electron-build/utils/version-info.js.map +1 -1
  156. package/dist/index.js +385 -108
  157. package/dist/ios/bolloon-web.json +10 -0
  158. package/dist/ios/chain-explorer.js +617 -0
  159. package/dist/ios/explorer.html +163 -0
  160. package/dist/ios/index.html +96 -0
  161. package/dist/ios/mobile-agent.js +2 -2
  162. package/dist/ios/mobile-contacts.js +362 -0
  163. package/dist/ios/mobile-core.js +3102 -310
  164. package/dist/ios/mobile-privacy.js +185 -0
  165. package/dist/ios/mobile-tasks.js +339 -0
  166. package/dist/ios/mobile-update.js +1442 -0
  167. package/dist/ios/mobile.css +19 -0
  168. package/dist/ios/mobile.html +96 -0
  169. package/dist/ios/mobile.js +1013 -41
  170. package/dist/ios/routes-chain-index.js +108 -0
  171. package/dist/ios/routes-contacts.js +420 -0
  172. package/dist/ios/routes-llm-config.js +344 -52
  173. package/dist/ios/routes-mobile-tasks.js +353 -0
  174. package/dist/ios/routes-onchain-trade.js +99 -0
  175. package/dist/ios/routes-x402-info.js +1 -0
  176. package/dist/ios/server.js +921 -0
  177. package/dist/ios/sw.js +1 -1
  178. package/dist/ios/task-frame-server.js +211 -0
  179. package/dist/llm/config-store.js +146 -6
  180. package/dist/llm/connection-probe.js +744 -0
  181. package/dist/llm/custom-provider-store.js +224 -0
  182. package/dist/llm/data/provider-catalog-baked.js +19 -0
  183. package/dist/llm/model-catalog.js +717 -0
  184. package/dist/llm/model-discovery.js +1137 -0
  185. package/dist/llm/model-selection.js +1241 -0
  186. package/dist/llm/pi-ai.js +70 -11
  187. package/dist/llm/provider-catalog.js +898 -0
  188. package/dist/llm/provider-registry.js +917 -0
  189. package/dist/llm/tool-name.js +220 -0
  190. package/dist/orbitdb/cid-database.js +81 -16
  191. package/dist/orbitdb/ipfs-node.js +31 -2
  192. package/dist/setup/onboard.js +34 -8
  193. package/dist/utils/dual-source-facts.js +223 -0
  194. package/dist/utils/dual-source.js +297 -0
  195. package/dist/utils/update-cli.js +16 -21
  196. package/dist/utils/update-health.js +35 -5
  197. package/dist/utils/update-manager.js +325 -96
  198. package/dist/utils/update-state.js +13 -10
  199. package/dist/utils/version-identity.js +176 -0
  200. package/dist/utils/version-info.js +27 -22
  201. package/dist/web/bolloon-web.json +10 -0
  202. package/dist/web/mobile-contacts.js +44 -11
  203. package/dist/web/mobile-core.js +2901 -580
  204. package/dist/web/mobile-tasks.js +339 -0
  205. package/dist/web/mobile-update.js +1442 -0
  206. package/dist/web/mobile.css +19 -0
  207. package/dist/web/mobile.html +64 -0
  208. package/dist/web/mobile.js +838 -35
  209. package/dist/web/routes-llm-config.js +344 -52
  210. package/dist/web/routes-mobile-tasks.js +353 -0
  211. package/dist/web/server.js +163 -4
  212. package/dist/web/sw.js +1 -1
  213. package/package.json +3 -1
  214. package/scripts/build-web.ts +36 -0
@@ -0,0 +1,353 @@
1
+ /**
2
+ * routes-mobile-tasks.ts — 手机端「任务协作」的桌面 API (2026-09-25)
3
+ *
4
+ * 为什么需要这一层: 群的 store (OrbitDB) 与公告板 (落盘 + 注册表) 都在**桌面节点**上,
5
+ * 手机 WebView 跑不了它们。所以手机只做三件事 —— **输入 · 展示待发内容 · 设备签名**,
6
+ * 真正的执行一律回到桌面, 而且**调用与 CLI 完全相同的函数**
7
+ * (`gateway-group.ts` / `task-group.ts` / `task-board.ts`), 因此:
8
+ * 同一份存储 · 同一套协议 · 同一条隐私闸 —— 手机**不是**第二个权威源。
9
+ *
10
+ * 三条硬纪律 (与项目「手机=授权, 桌面=执行」的分工一致):
11
+ * 1. **高风险动作必须带设备签名** (`verdict` 由 `mobile-task-actions-verify.ts` 给出):
12
+ * 入群/退群/建群/发公告/公告入群/过程留痕 一律先验签, 且签名**绑定内容摘要** —— 换类型/换内容都验不过。
13
+ * 2. **读接口只回脱敏事实**: 群里只有短引用, 板上的 buyerDid 不出门 (手机按自己的一份视图投影再挡一道)。
14
+ * 3. **失败如实说**: 群不存在/没入群/桌面没身份/注册表不可达 → 结构化失败 + 人话原因, 不静默降级成"只写本地"。
15
+ */
16
+ import { ContactChain } from '../agents/contacts/chain.js';
17
+ import { verifyTaskAction } from '../agents/mobile-task-actions-verify.js';
18
+ import { shortId } from '../agents/mobile-task-views.js';
19
+ function fail(res, status, code, error, extra = {}) {
20
+ res.status(status).json({ ok: false, code, error, ...extra });
21
+ }
22
+ /** 把任意异常翻成人话 (不吞: 原文截断带上) */
23
+ function reasonOf(e, max = 200) {
24
+ return String(e?.message || e || '未知原因').slice(0, max);
25
+ }
26
+ /** 「本机身份」——公告必须有可追责的买方; 没有身份就拒 (与 CLI publish 一致) */
27
+ async function localSignerOrNull() {
28
+ try {
29
+ const { loadLocalSigner } = await import('../agents/local-signer.js');
30
+ const s = await loadLocalSigner();
31
+ return s ? { did: s.did, publicKeyHex: s.publicKeyHex, keypair: s.keypair } : null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ export function registerMobileTaskRoutes(app, opts = {}) {
38
+ const now = () => (opts.now ? opts.now() : Date.now());
39
+ const chainFor = (ownerDid) => new ContactChain({
40
+ home: opts.home,
41
+ ownerDid: ownerDid || opts.ownerDid || 'did:bolln:local',
42
+ displayName: '本机用户',
43
+ });
44
+ /** 已登记设备公钥 (手机签名只认这些) —— 注入优先 (单测不起真实 store) */
45
+ async function deviceKeyOf(deviceId, ownerDid) {
46
+ if (opts.devices)
47
+ return opts.devices.find((d) => d.deviceId === deviceId) || null;
48
+ try {
49
+ const list = await chainFor(ownerDid).grants.listDevices();
50
+ return list.find((d) => d.deviceId === deviceId) || null;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ /**
57
+ * 高风险动作的统一入口: 形状 → 设备已登记 → 签名 → 时效 → 内容摘要。
58
+ * 任何一项不过都**不执行** (fail-closed)。
59
+ */
60
+ async function authorize(req, res, kind) {
61
+ const request = (req?.body?.request || {});
62
+ const signed = { action: req?.body?.action, signature: req?.body?.signature };
63
+ if (!request || String(request.kind || '') !== String(kind)) {
64
+ fail(res, 400, 'REQUEST_KIND_MISMATCH', `请求类型与路由不一致 (路由要求 ${kind}, 收到 ${String(request?.kind || '无')})`);
65
+ return { ok: false };
66
+ }
67
+ const deviceId = String(signed?.action?.deviceId || '');
68
+ const pub = deviceId ? await deviceKeyOf(deviceId, String(signed?.action?.ownerDid || '')) : null;
69
+ const v = verifyTaskAction(signed, pub, { req: request, now: now() });
70
+ if (!v.ok) {
71
+ fail(res, 403, 'SIGNATURE_REJECTED', `手机签名没通过: ${v.message || v.reason}`, { reason: v.reason, action: shortId(signed?.action?.actionId, 16) });
72
+ return { ok: false };
73
+ }
74
+ return { ok: true, req: request };
75
+ }
76
+ // ── ① 群 (与 CLI `bolloon task group …` 同一份 store) ─────────────────────
77
+ /** 本机已加入的群 (脱敏: 只给 groupId/群名/时间 —— 不放链接与 store 地址) */
78
+ app.get('/api/mobile/tasks/groups', async (_req, res) => {
79
+ try {
80
+ const { listGroups } = await import('../agents/gateway-group.js');
81
+ const groups = await listGroups();
82
+ res.json({
83
+ ok: true,
84
+ groups: groups.map((g) => ({ id: g.id, name: g.name, createdAt: g.createdAt })),
85
+ note: '只给群标识/群名/加入时间; 邀请链接要显式取 (与 CLI task group list 同一纪律)',
86
+ });
87
+ }
88
+ catch (e) {
89
+ fail(res, 500, 'GROUP_LIST_FAILED', `读群列表失败: ${reasonOf(e)}`);
90
+ }
91
+ });
92
+ /** 群列表 (给手机确认页做 ref 校验用; 不放链接) */
93
+ app.get('/api/mobile/tasks/groups/resolve', async (req, res) => {
94
+ try {
95
+ const { resolveGroupRef } = await import('../agents/task-group.js');
96
+ const r = await resolveGroupRef(String(req.query?.group || ''));
97
+ if (!r.ok)
98
+ return fail(res, 404, r.code, r.message, { detail: r.detail });
99
+ res.json({ ok: true, group: { id: r.group.groupId, name: r.group.name, via: r.group.via } });
100
+ }
101
+ catch (e) {
102
+ fail(res, 500, 'GROUP_RESOLVE_FAILED', `解析群失败: ${reasonOf(e)}`);
103
+ }
104
+ });
105
+ // ── ② 公告: 看板 (只读, 与 CLI `task board` 同一份事实) ─────────────────
106
+ app.get('/api/mobile/tasks/board', async (req, res) => {
107
+ try {
108
+ const { listBoard } = await import('../agents/task-board.js');
109
+ const localOnly = String(req.query?.local || '') === '1';
110
+ const openOnly = String(req.query?.open || '') === '1';
111
+ const view = await listBoard({ home: opts.home, now: now(), localOnly, openOnly });
112
+ // 只回手机要用的字段: buyerDid / claimedBy 是身份, **不出门** (手机侧再投影一次)
113
+ res.json({
114
+ ok: true,
115
+ entries: view.entries.map((e) => ({
116
+ announcementId: e.announcementId, capability: e.capability, status: e.status,
117
+ budget: e.budget, deadline: e.deadline, createdAt: e.createdAt, claimCount: e.claimCount,
118
+ instructionDigest: e.instructionDigest, instructionPreview: e.instructionPreview,
119
+ remote: e.remote, source: e.source, signatureVerified: e.signatureVerified, claimable: e.claimable,
120
+ })),
121
+ localCount: view.localCount, remoteCount: view.remoteCount, registryReady: view.registryReady,
122
+ registryError: view.registryError, notes: view.notes,
123
+ });
124
+ }
125
+ catch (e) {
126
+ fail(res, 500, 'BOARD_FAILED', `读看板失败: ${reasonOf(e)}`);
127
+ }
128
+ });
129
+ /** 群里的一期过程痕迹 (只读, 与 CLI `task trail --group …` 同一份汇总) */
130
+ app.get('/api/mobile/tasks/trail', async (req, res) => {
131
+ try {
132
+ const { resolveGroupRef, readTrail } = await import('../agents/task-group.js');
133
+ const ref = await resolveGroupRef(String(req.query?.group || ''));
134
+ if (!ref.ok)
135
+ return fail(res, 404, ref.code, ref.message, { detail: ref.detail });
136
+ const limit = Number(req.query?.limit) || 300;
137
+ const announcementId = req.query?.announcementId ? String(req.query.announcementId) : null;
138
+ const summary = await readTrail(ref.group.groupId, { limit, announcementId });
139
+ res.json({ ok: true, group: { id: ref.group.groupId, name: ref.group.name }, summary });
140
+ }
141
+ catch (e) {
142
+ fail(res, 500, 'TRAIL_FAILED', `读群痕迹失败: ${reasonOf(e)}`);
143
+ }
144
+ });
145
+ /** 预览: 用**将要发送的同一个构造器**生成那一行 (只展示, 不发送, 不需要签名) */
146
+ app.post('/api/mobile/tasks/preview', async (req, res) => {
147
+ try {
148
+ const request = (req?.body?.request || {});
149
+ const built = await buildGroupText(request, opts.home);
150
+ if (!built.ok)
151
+ return fail(res, 400, built.code, built.error);
152
+ res.json({ ok: true, text: built.text, note: '预览由桌面的构造器生成: 与你确认后真正发进群的那一行同源' });
153
+ }
154
+ catch (e) {
155
+ fail(res, 500, 'PREVIEW_FAILED', `生成预览失败: ${reasonOf(e)}`);
156
+ }
157
+ });
158
+ // ── 执行 (验签 → 调用与 CLI 相同的函数) ───────────────────────────────────
159
+ app.post('/api/mobile/tasks/execute', async (req, res) => {
160
+ try {
161
+ const kind = String(req?.body?.request?.kind || '');
162
+ const auth = await authorize(req, res, kind);
163
+ if (!auth.ok)
164
+ return;
165
+ const request = auth.req;
166
+ const ownerDid = String((req?.body?.action?.ownerDid || '') || '');
167
+ if (kind === 'group_create' || kind === 'group_join' || kind === 'group_leave') {
168
+ const { createGroup, joinGroup, leaveGroup } = await import('../agents/gateway-group.js');
169
+ if (kind === 'group_create') {
170
+ const r = await createGroup(String(request.groupRef || '').trim());
171
+ if (!r.ok || !r.group)
172
+ return fail(res, 409, 'GROUP_CREATE_FAILED', `建群失败: ${r.error || '未知原因'}`);
173
+ return res.json({ ok: true, kind, group: { id: r.group.id, name: r.group.name }, text: `已建群「${r.group.name}」` });
174
+ }
175
+ if (kind === 'group_join') {
176
+ const { parseGroupLink } = await import('../agents/gateway-group.js');
177
+ const raw = String(request.groupRef || '').trim();
178
+ if (!/^orbitdb:\/\//i.test(raw))
179
+ return fail(res, 400, 'INVALID_GROUP_LINK', '入群需要群邀请链接 (orbitdb://…?type=group&name=…)');
180
+ if (!parseGroupLink(raw))
181
+ return fail(res, 400, 'INVALID_GROUP_LINK', '邀请链接解析失败 → 拒 (不猜你要进哪个群)');
182
+ const r = await joinGroup(raw);
183
+ if (!r.ok || !r.group)
184
+ return fail(res, 409, 'GROUP_JOIN_FAILED', `入群失败: ${r.error || '未知原因'}`);
185
+ return res.json({ ok: true, kind, group: { id: r.group.id, name: r.group.name }, already: !!r.already, text: r.already ? `本来就在「${r.group.name}」` : `已入群「${r.group.name}」` });
186
+ }
187
+ const r = await leaveGroup(String(request.groupRef || '').trim());
188
+ if (!r.ok)
189
+ return fail(res, 409, 'GROUP_LEAVE_FAILED', `退群失败: ${r.error || '未知原因'}`);
190
+ return res.json({ ok: true, kind, text: `已退群 (只摘本机记录)`, removed: r.removed });
191
+ }
192
+ if (kind === 'announce_publish') {
193
+ const signer = await localSignerOrNull();
194
+ if (!signer)
195
+ return fail(res, 400, 'NO_LOCAL_IDENTITY', '本机没有可签名的身份 (~/.bolloon/identity.json) → 公告没有可追责的买方, 拒发');
196
+ const { toAtomic, parseDeadline } = await import('../cli/commands/tasks.js');
197
+ const currency = String(request.currency || 'USDC').toUpperCase();
198
+ if (currency !== 'USDC' && currency !== 'ETH')
199
+ return fail(res, 400, 'INVALID_CURRENCY', `币种只支持 USDC/ETH (收到 ${currency})`);
200
+ const atomic = toAtomic(String(request.budgetHuman || ''), currency === 'USDC' ? 6 : 18);
201
+ if (!atomic)
202
+ return fail(res, 400, 'INVALID_BUDGET', '预算必须是正数 (原子单位由桌面换算, 不接受 0/负/超精度)');
203
+ const dl = parseDeadline(request.deadline === null || request.deadline === undefined || request.deadline === '' ? undefined : String(request.deadline), now());
204
+ if (typeof dl === 'string')
205
+ return fail(res, 400, 'INVALID_DEADLINE', dl);
206
+ const { publishAnnouncement } = await import('../agents/task-board.js');
207
+ const r = await publishAnnouncement({
208
+ capability: String(request.capability || ''),
209
+ instruction: String(request.instruction || ''),
210
+ buyerDid: signer.did,
211
+ buyerPublicKeyHex: signer.publicKeyHex,
212
+ budget: { maxAmount: atomic, currency, network: 'base-sepolia' },
213
+ deadline: dl,
214
+ paymentMode: 'policy',
215
+ signerKeypair: signer.keypair,
216
+ }, { home: opts.home, now: now() });
217
+ if (!r.ok || !r.announcement)
218
+ return fail(res, 500, 'ANNOUNCE_PUBLISH_FAILED', `公告没发布成功: ${r.error || '未知原因'}`);
219
+ return res.json({
220
+ ok: true, kind, announcementId: r.announcement.announcementId, dup: !!r.dup,
221
+ registryAnnounced: r.registry?.announced === true, registryError: r.registry?.error || null,
222
+ text: r.dup ? '这条公告本来就在板上 (幂等, 没有重复发)' : '公告已发布 (落盘 + 尽力向注册表公告)',
223
+ });
224
+ }
225
+ // announce_to_group / trail_post: 生成那一行 → 隐私闸 → 发进群
226
+ const built = await buildGroupText(request, opts.home);
227
+ if (!built.ok)
228
+ return fail(res, 400, built.code, built.error);
229
+ const { sendTrailMessage, resolveSenderTag } = await import('../agents/task-group.js');
230
+ const tag = await resolveSenderTag(null);
231
+ if (!tag.ok)
232
+ return fail(res, 400, tag.code, tag.message);
233
+ const sent = await sendTrailMessage(built.groupId, built.text, tag.tag);
234
+ if (!sent.sent) {
235
+ return fail(res, 409, 'GROUP_SEND_REFUSED', sent.error || '群消息没发出去', { violations: sent.violations || [] });
236
+ }
237
+ return res.json({ ok: true, kind, text: sent.text, senderTag: tag.tag });
238
+ }
239
+ catch (e) {
240
+ fail(res, 500, 'EXECUTE_FAILED', `执行失败: ${reasonOf(e)}`);
241
+ }
242
+ });
243
+ // ── ③ 飞轮进度 (只读消费 goal-flywheel 冻结类型; 投影在桌面做, 手机只渲染) ──
244
+ app.get('/api/mobile/tasks/flywheel', async (_req, res) => {
245
+ try {
246
+ const { listGoals } = await import('../agents/goal-store.js');
247
+ const { buildMobileFlywheelView, findInternalFieldLeaks } = await import('../agents/mobile-flywheel-view.js');
248
+ const { GOAL_TERMINAL_STATES } = await import('../agents/goal-flywheel/types.js');
249
+ const goals = await listGoals({ limit: 20 });
250
+ const t = now();
251
+ let decisionReadFailures = 0;
252
+ const items = [];
253
+ for (const g of goals) {
254
+ const status = String(g?.status || '');
255
+ // 终态判据来自冻结面 (不在这里重列一遍 completed/failed/abandoned)
256
+ const terminal = GOAL_TERMINAL_STATES.includes(status);
257
+ // 只读消费: goal-store 的 continuation 可整体读作 goal-flywheel 的 GoalContinuationRecord (冻结面)。
258
+ // 终态目标在 store 里没有 continuation (冻结语义), 但 toUserVisibleState 判"已结束"要读 state ——
259
+ // 所以把 **store 里的真实 status** 装进它要的形状, 不新增任何状态、不编造下一步。
260
+ const continuation = (g?.continuation && typeof g.continuation === 'object')
261
+ ? {
262
+ nextAction: String(g.continuation.nextAction || ''),
263
+ wakeAt: g.continuation.wakeAt || null,
264
+ wakeReason: String(g.continuation.wakeReason || ''),
265
+ autoContinue: g.continuation.autoContinue === true,
266
+ requiredAgent: g.continuation.requiredAgent || null,
267
+ pendingReports: [],
268
+ unresolvedItems: Array.isArray(g.unresolvedItems) ? g.unresolvedItems.map((x) => String(x)) : [],
269
+ lastDecisionId: g.continuation.lastDecisionId || null,
270
+ state: status || 'active',
271
+ updatedAt: String(g.continuation.updatedAt || g.updatedAt || new Date(t).toISOString()),
272
+ }
273
+ : (terminal
274
+ ? {
275
+ nextAction: '', wakeAt: null, wakeReason: '', autoContinue: false, requiredAgent: null,
276
+ pendingReports: [], unresolvedItems: [], lastDecisionId: null,
277
+ state: status, updatedAt: String(g?.updatedAt || new Date(t).toISOString()),
278
+ }
279
+ : null);
280
+ // 决策记录: 用飞轮接线自己的读函数 (只读; 没有/还没接线 → null, 不编造)
281
+ let decision = null;
282
+ try {
283
+ const w = await import('../agents/goal-flywheel-wiring.js');
284
+ if (typeof w?.readDecisionRecords === 'function') {
285
+ const recs = await w.readDecisionRecords(String(g?.goalId || g?.id || ''), opts.home);
286
+ const last = Array.isArray(recs) && recs.length ? recs[recs.length - 1] : null;
287
+ decision = last?.decision ?? null;
288
+ }
289
+ }
290
+ catch {
291
+ decisionReadFailures++;
292
+ decision = null;
293
+ }
294
+ // 阻塞清单: 今天的飞轮没有可供手机读的阻塞存储 (monitor 状态归飞轮接线) → 如实空表, 不假装
295
+ const view = buildMobileFlywheelView({ continuation, blocks: [], decision, now: t });
296
+ items.push({
297
+ objectiveShort: String(g?.objective || '').slice(0, 40),
298
+ status,
299
+ terminal,
300
+ view,
301
+ });
302
+ }
303
+ // 自检: 投影里出现内部字段名 → 宁可不给数据 (findInternalFieldLeaks 来自冻结面)
304
+ const leaks = findInternalFieldLeaks(JSON.stringify(items));
305
+ if (leaks.length)
306
+ return fail(res, 500, 'VIEW_LEAK', `飞轮投影里混进了内部字段: ${leaks.join(', ')} → 拒发`);
307
+ res.json({
308
+ ok: true, goals: items,
309
+ hint: items.length ? null : '还没有 Goal 记录: 在桌面/CLI 立一个长期目标后, 这里会显示它在第几类状态 (只读)',
310
+ note: decisionReadFailures
311
+ ? '本页只读消费 goal-flywheel 的五类用户可见状态; 有目标的决策记录这次没读到 (不影响状态判定)'
312
+ : '本页只读消费 goal-flywheel 的五类用户可见状态 (toUserVisibleState); 阻塞清单要等飞轮接线把 detectBlocks 的产物落盘后才有数据',
313
+ });
314
+ }
315
+ catch (e) {
316
+ fail(res, 500, 'FLYWHEEL_FAILED', `读飞轮进度失败: ${reasonOf(e)}`);
317
+ }
318
+ });
319
+ }
320
+ async function buildGroupText(request, home) {
321
+ const TG = await import('../agents/task-group.js');
322
+ const ref = await TG.resolveGroupRef(String(request.groupRef || ''));
323
+ if (!ref.ok)
324
+ return { ok: false, code: ref.code, error: ref.message };
325
+ if (request.kind === 'announce_to_group') {
326
+ const a = TG.pickAnnouncement({
327
+ home,
328
+ announcementId: request.announcementId,
329
+ capability: null,
330
+ });
331
+ if (!a.ok)
332
+ return { ok: false, code: a.code, error: a.message };
333
+ return { ok: true, groupId: ref.group.groupId, text: TG.buildAnnounceMessage(a.announcement, { round: request.round, criteria: request.criteria }) };
334
+ }
335
+ if (request.kind === 'trail_post') {
336
+ const kind = String(request.trailKind || '');
337
+ const built = TG.buildPostMessage({
338
+ kind,
339
+ announcementId: request.announcementId,
340
+ round: request.round,
341
+ price: request.price,
342
+ hash: request.hash,
343
+ bytes: request.bytes,
344
+ checksRaw: request.checks,
345
+ verdict: request.verdict,
346
+ });
347
+ if (!built.ok)
348
+ return { ok: false, code: built.code, error: built.message };
349
+ return { ok: true, groupId: ref.group.groupId, text: built.text };
350
+ }
351
+ return { ok: false, code: 'INVALID_ARGUMENT', error: `这一类动作没有群消息文本: ${String(request.kind)}` };
352
+ }
353
+ export default { registerMobileTaskRoutes };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * routes-onchain-trade.ts — 链上任务交易闭环的**只读**HTTP 路由 (P4, ②③)
3
+ * =========================================================================
4
+ * 刻意独立成文件, **不碰 src/web/server.ts** (另一个并行任务在动它)。
5
+ * 只暴露**读**接口 —— 全都不发交易、不签名、不改链、不写盘:
6
+ *
7
+ * GET /api/chain/trade/state?taskKey=0x…(64hex) | taskId=…
8
+ * → 该任务在 chain-state.json 里的全部链上记录 (方法/状态/确认数/是否可疑/结果哈希)
9
+ * GET /api/chain/trade/recovery?taskKey=… | taskId=…&pendingResultDigest=sha256:…
10
+ * → ★ 重启恢复决策: nextAction / mustNotRepay / verified / 为什么 (纯读盘, 不联网)
11
+ * GET /api/chain/trade/budget?amount=0.02&taskBudget=0.05
12
+ * → M1 预算硬约束预检 (单任务 0.05 / 单次 0.02): 放行 / 哪一层拦下
13
+ *
14
+ * 挂载 (在 src/web/server.ts 里加两行即可, 本文件不改它):
15
+ * import { registerOnchainTradeRoutes } from './routes-onchain-trade.js';
16
+ * registerOnchainTradeRoutes(app); // app: express.Express
17
+ *
18
+ * 挂载点可选: registerOnchainTradeRoutes(app, { basePath: '/api/chain/trade' })
19
+ * 换 HOME 用 opts.home (测试隔离 / 多 profile)。**没有**任何写接口 ——
20
+ * 链上写操作必须走 CLI/任务链路 (那里有签名放行闸 + 审计), 不通过 HTTP 裸开。
21
+ */
22
+ import * as os from 'os';
23
+ import { recoverOnchainTrade, checkOnchainAmount, onchainTaskKey, } from '../agents/chain/onchain-trade.js';
24
+ import { loadChainState, chainStatePath } from '../agents/chain/chain-state-store.js';
25
+ const isTaskKey = (v) => typeof v === 'string' && /^0x[0-9a-fA-F]{64}$/.test(v);
26
+ function homeOf(opts) {
27
+ return opts.home || process.env.HOME || os.homedir();
28
+ }
29
+ export function registerOnchainTradeRoutes(app, opts = {}) {
30
+ const base = opts.basePath || '/api/chain/trade';
31
+ // ① 链上记录 (按 taskKey / taskId 过滤; 不过滤 = 全部)
32
+ app.get(`${base}/state`, (req, res) => {
33
+ try {
34
+ const home = homeOf(opts);
35
+ const taskKey = String(req.query?.taskKey || '');
36
+ const taskId = String(req.query?.taskId || '');
37
+ if (taskKey && !isTaskKey(taskKey))
38
+ return res.status(400).json({ ok: false, error: 'taskKey 必须是 0x + 64 hex' });
39
+ const want = (taskKey || (taskId ? onchainTaskKey(taskId) : '')).toLowerCase();
40
+ const all = Object.values(loadChainState(home).records);
41
+ const records = want ? all.filter((r) => String(r.taskKey).toLowerCase() === want) : all;
42
+ res.json({
43
+ ok: true,
44
+ statePath: chainStatePath(home),
45
+ taskKey: want || null,
46
+ count: records.length,
47
+ records,
48
+ });
49
+ }
50
+ catch (e) {
51
+ res.status(500).json({ ok: false, error: String(e?.message || e) });
52
+ }
53
+ });
54
+ // ② 重启恢复决策 (纯读盘; 不联网 → 不会把"读不到 RPC"当成"没付款")
55
+ app.get(`${base}/recovery`, (req, res) => {
56
+ try {
57
+ const home = homeOf(opts);
58
+ const q = req.query || {};
59
+ const taskKey = String(q.taskKey || '');
60
+ const taskId = String(q.taskId || '');
61
+ if (taskKey && !isTaskKey(taskKey))
62
+ return res.status(400).json({ ok: false, error: 'taskKey 必须是 0x + 64 hex' });
63
+ if (!taskKey && !taskId)
64
+ return res.status(400).json({ ok: false, error: '要 taskKey 或 taskId' });
65
+ const recovery = recoverOnchainTrade({
66
+ home,
67
+ ...(taskKey ? { taskKey } : {}),
68
+ ...(taskId ? { taskId } : {}),
69
+ ...(q.pendingResultDigest ? { pendingResultDigest: String(q.pendingResultDigest) } : {}),
70
+ });
71
+ res.json({ ok: true, recovery });
72
+ }
73
+ catch (e) {
74
+ res.status(500).json({ ok: false, error: String(e?.message || e) });
75
+ }
76
+ });
77
+ // ③ M1 预算硬约束预检 (付款前必须能看得到; 纯函数, 不碰钱)
78
+ app.get(`${base}/budget`, (req, res) => {
79
+ try {
80
+ const q = req.query || {};
81
+ const amount = Number(q.amount);
82
+ if (!Number.isFinite(amount) || amount <= 0)
83
+ return res.status(400).json({ ok: false, error: 'amount 必须是正数 (USDC)' });
84
+ const decimals = Number.isFinite(Number(q.decimals)) ? Number(q.decimals) : 6;
85
+ const atomic = BigInt(Math.round(amount * 10 ** decimals));
86
+ const d = checkOnchainAmount({
87
+ amountAtomic: atomic, decimals,
88
+ budget: {
89
+ ...(q.taskBudget !== undefined ? { taskBudget: Number(q.taskBudget) } : {}),
90
+ ...(q.perPurchase !== undefined ? { perPurchase: Number(q.perPurchase) } : {}),
91
+ },
92
+ });
93
+ res.json({ ok: true, allowed: d.ok, layer: d.layer ?? null, reason: d.reason, amountUsdc: d.amountUsdc, budget: d.budget, why: d.why });
94
+ }
95
+ catch (e) {
96
+ res.status(500).json({ ok: false, error: String(e?.message || e) });
97
+ }
98
+ });
99
+ }
@@ -107,6 +107,7 @@ export function registerX402InfoRoutes(app) {
107
107
  const pay = await checkAndSettlePayment({
108
108
  paymentHeader: String(paymentHeader),
109
109
  requirements,
110
+ expectedItemId: stored.item.id, // 凭据必须绑定这条资源 (防跨资源复用)
110
111
  });
111
112
  if (!pay.ok) {
112
113
  return res.status(402).json({ ...requirements, error: pay.error });