@bolloon/bolloon-agent 0.5.0 → 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 (106) hide show
  1. package/dist/agents/execution-supervisor.js +87 -5
  2. package/dist/agents/model-policy.js +651 -0
  3. package/dist/agents/pi-sdk-tools.js +84 -33
  4. package/dist/agents/pi-sdk.js +83 -23
  5. package/dist/agents/run-store.js +53 -0
  6. package/dist/agents/task/task-runner.js +8 -1
  7. package/dist/agents/workflow-pivot-loop.js +19 -12
  8. package/dist/cli/clipboard.js +65 -0
  9. package/dist/cli/ink-app.js +145 -17
  10. package/dist/cli/log-gate.js +386 -0
  11. package/dist/cli/mention-data.js +2 -0
  12. package/dist/cli/model-selector.js +538 -0
  13. package/dist/cli/reply-hygiene.js +54 -0
  14. package/dist/cli/setup-wizard.js +686 -69
  15. package/dist/cli/startup-notice.js +160 -0
  16. package/dist/cli/stores.js +22 -1
  17. package/dist/cli/theme.js +49 -0
  18. package/dist/cli/tui-select.js +970 -0
  19. package/dist/cli-entry.js +125 -18
  20. package/dist/electron-build/agents/execution-supervisor.js +86 -4
  21. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  22. package/dist/electron-build/agents/goal-store.js.map +1 -1
  23. package/dist/electron-build/agents/model-policy.js +706 -0
  24. package/dist/electron-build/agents/model-policy.js.map +1 -0
  25. package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
  26. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  27. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  28. package/dist/electron-build/agents/pi-sdk.js +83 -23
  29. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  30. package/dist/electron-build/agents/run-store.js +55 -1
  31. package/dist/electron-build/agents/run-store.js.map +1 -1
  32. package/dist/electron-build/agents/task/task-runner.js +8 -1
  33. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  34. package/dist/electron-build/agents/workflow-pivot-loop.js +19 -12
  35. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  36. package/dist/electron-build/llm/config-store.js +146 -6
  37. package/dist/electron-build/llm/config-store.js.map +1 -1
  38. package/dist/electron-build/llm/connection-probe.js +764 -0
  39. package/dist/electron-build/llm/connection-probe.js.map +1 -0
  40. package/dist/electron-build/llm/custom-provider-store.js +272 -0
  41. package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
  42. package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
  43. package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
  44. package/dist/electron-build/llm/model-catalog.js +778 -0
  45. package/dist/electron-build/llm/model-catalog.js.map +1 -0
  46. package/dist/electron-build/llm/model-discovery.js +1202 -0
  47. package/dist/electron-build/llm/model-discovery.js.map +1 -0
  48. package/dist/electron-build/llm/model-selection.js +1315 -0
  49. package/dist/electron-build/llm/model-selection.js.map +1 -0
  50. package/dist/electron-build/llm/pi-ai.js +70 -11
  51. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  52. package/dist/electron-build/llm/provider-catalog.js +969 -0
  53. package/dist/electron-build/llm/provider-catalog.js.map +1 -0
  54. package/dist/electron-build/llm/provider-registry.js +950 -0
  55. package/dist/electron-build/llm/provider-registry.js.map +1 -0
  56. package/dist/electron-build/llm/tool-name.js +266 -0
  57. package/dist/electron-build/llm/tool-name.js.map +1 -0
  58. package/dist/electron-build/utils/dual-source-facts.js +236 -0
  59. package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
  60. package/dist/electron-build/utils/dual-source.js +32 -156
  61. package/dist/electron-build/utils/dual-source.js.map +1 -1
  62. package/dist/electron-build/utils/update-manager.js +22 -72
  63. package/dist/electron-build/utils/update-manager.js.map +1 -1
  64. package/dist/electron-build/utils/update-state.js +2 -19
  65. package/dist/electron-build/utils/update-state.js.map +1 -1
  66. package/dist/electron-build/utils/version-identity.js +193 -0
  67. package/dist/electron-build/utils/version-identity.js.map +1 -0
  68. package/dist/electron-build/utils/version-info.js +47 -101
  69. package/dist/electron-build/utils/version-info.js.map +1 -1
  70. package/dist/index.js +282 -96
  71. package/dist/ios/bolloon-web.json +10 -0
  72. package/dist/ios/mobile-contacts.js +44 -11
  73. package/dist/ios/mobile-core.js +1723 -84
  74. package/dist/ios/mobile-tasks.js +339 -0
  75. package/dist/ios/mobile-update.js +1442 -0
  76. package/dist/ios/mobile.js +308 -35
  77. package/dist/ios/routes-llm-config.js +344 -52
  78. package/dist/ios/routes-mobile-tasks.js +353 -0
  79. package/dist/ios/server.js +163 -4
  80. package/dist/ios/sw.js +1 -1
  81. package/dist/llm/config-store.js +146 -6
  82. package/dist/llm/connection-probe.js +744 -0
  83. package/dist/llm/custom-provider-store.js +224 -0
  84. package/dist/llm/data/provider-catalog-baked.js +19 -0
  85. package/dist/llm/model-catalog.js +717 -0
  86. package/dist/llm/model-discovery.js +1137 -0
  87. package/dist/llm/model-selection.js +1241 -0
  88. package/dist/llm/pi-ai.js +70 -11
  89. package/dist/llm/provider-catalog.js +898 -0
  90. package/dist/llm/provider-registry.js +917 -0
  91. package/dist/llm/tool-name.js +220 -0
  92. package/dist/setup/onboard.js +34 -8
  93. package/dist/utils/dual-source-facts.js +223 -0
  94. package/dist/utils/dual-source.js +12 -141
  95. package/dist/utils/update-manager.js +16 -59
  96. package/dist/utils/update-state.js +6 -18
  97. package/dist/utils/version-identity.js +176 -0
  98. package/dist/utils/version-info.js +9 -78
  99. package/dist/web/bolloon-web.json +10 -0
  100. package/dist/web/mobile-core.js +1721 -84
  101. package/dist/web/mobile-update.js +1442 -0
  102. package/dist/web/mobile.js +308 -35
  103. package/dist/web/routes-llm-config.js +344 -52
  104. package/dist/web/sw.js +1 -1
  105. package/package.json +1 -1
  106. 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 };
@@ -15,6 +15,8 @@ import { registerExternalEngineRoutes } from './routes-external-engines.js';
15
15
  import { registerTaskRoutes } from './routes-tasks.js';
16
16
  // 2026-09-19: 联系方式 / 人工批准 / 手机—桌面配对 路由
17
17
  import { registerContactRoutes } from './routes-contacts.js';
18
+ // 2026-09-25: 手机端「任务协作」(入群 / 发公告 / 看飞轮进度) 路由 —— 高风险动作由设备签名把关
19
+ import { registerMobileTaskRoutes } from './routes-mobile-tasks.js';
18
20
  // 2026-09-13: 微支付信息服务 (x402) 路由
19
21
  import { registerX402InfoRoutes } from './routes-x402-info.js';
20
22
  // 2026-09-22: P5 链上索引 (只读: status/stats/timeline/events 增量) 路由
@@ -3108,6 +3110,12 @@ async function act(id, what){
3108
3110
  else if (what === 'abort') { const r = await fetch('/api/runs/'+id+'/abort',{method:'POST'}); log(await r.json()); }
3109
3111
  refresh();
3110
3112
  }
3113
+ async function req(id){
3114
+ const text = prompt('新要求 (原话逐字入档; 只影响后续 Run, 不改写已发生的历史。若说"撤销/别继续做了", 在跑的这一轮会在下一次循环停下):');
3115
+ if (!text) return;
3116
+ const r = await fetch('/api/goals/'+id+'/requirement',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({text})});
3117
+ log(await r.json()); refresh();
3118
+ }
3111
3119
  async function refresh(){
3112
3120
  try {
3113
3121
  const sup = await j('/api/supervisor');
@@ -3115,14 +3123,20 @@ async function refresh(){
3115
3123
  ' <span class="dim">lease='+(sup.leaseTtlMs||'-')+'ms tick='+(sup.tickIntervalMs||'-')+'ms</span>';
3116
3124
  } catch(e){ document.getElementById('sup').textContent = 'supervisor 不可用: '+e; }
3117
3125
  const gs = await j('/api/goals');
3126
+ const labels = gs.visibleLabels || {};
3127
+ const visMap = {}; (gs.goals || []).forEach((g, i) => { visMap[g.goalId] = (gs.visible || [])[i]; });
3118
3128
  const gtb = document.querySelector('#goals tbody'); gtb.innerHTML = '';
3119
3129
  for (const g of (gs.goals || [])) {
3120
3130
  let crit = g.successCriteria?.length ? (g.completedCriteria?.length||0)+'/'+g.successCriteria.length : '<span class="bad">无判据</span>';
3121
3131
  const src = g.criteriaSource ? ' <span class="dim">('+g.criteriaSource+(g.criteriaConfirmed?'✓':'?')+')</span>' : '';
3122
3132
  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">'+
3133
+ const vis = (visMap[g.goalId]) || (g.continuation ? 'executing' : 'executing');
3134
+ const visZh = (labels[vis] && labels[vis].zh) || vis;
3135
+ const pend = (gs.pendingChanges||[])[(gs.goals||[]).indexOf(g)];
3136
+ const pendZh = pend ? ' <span class="bad">('+pend+' 条变更等你决定)</span>' : '';
3137
+ tr.innerHTML = '<td><code>'+g.goalId+'</code></td><td>'+visZh+pendZh+' <span class="dim">('+vis+')</span></td><td>'+crit+src+'</td><td class="dim">'+
3124
3138
  ((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>';
3139
+ '<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><button onclick="req(\\''+g.goalId+'\\')">新要求</button></td>';
3126
3140
  gtb.appendChild(tr);
3127
3141
  }
3128
3142
  const rs = await j('/api/runs');
@@ -3130,7 +3144,7 @@ async function refresh(){
3130
3144
  for (const r of (rs.runs || []).slice(-20).reverse()) {
3131
3145
  const tr = document.createElement('tr');
3132
3146
  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>';
3147
+ '<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
3148
  rtb.appendChild(tr);
3135
3149
  }
3136
3150
  }
@@ -3339,6 +3353,7 @@ fetchState();
3339
3353
  const { getSupervisor } = await import('../agents/execution-supervisor.js');
3340
3354
  const { readSupervisorState, supervisorStatePath } = await import('../agents/supervisor-host.js');
3341
3355
  const { wakeReport, listRunnableGoals } = await import('../agents/goal-store.js');
3356
+ const { goalVisibleState } = await import('../agents/goal-flywheel-wiring.js');
3342
3357
  const { runnable, skipped } = await listRunnableGoals({ now: Date.now() });
3343
3358
  res.json({
3344
3359
  supervisor: getSupervisor().status(),
@@ -3346,6 +3361,15 @@ fetchState();
3346
3361
  host: await readSupervisorState(),
3347
3362
  hostStatePath: supervisorStatePath(),
3348
3363
  wake: await wakeReport(),
3364
+ // 2026-09-25 (飞轮接线 §7): 用户可见态 (六类) —— 界面不得自己造第二套映射
3365
+ wakeVisible: await Promise.all((await wakeReport()).map(async (r) => {
3366
+ try {
3367
+ return { goalId: r.goalId, visible: await goalVisibleState({ goalId: r.goalId }) };
3368
+ }
3369
+ catch {
3370
+ return { goalId: r.goalId, visible: r.visible };
3371
+ }
3372
+ })),
3349
3373
  runnable: runnable.map((g) => ({ goalId: g.goalId, objective: g.objective, status: g.status })),
3350
3374
  skipped,
3351
3375
  });
@@ -3395,6 +3419,111 @@ fetchState();
3395
3419
  res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3396
3420
  }
3397
3421
  });
3422
+ // 2026-09-25 (飞轮接线 P4 / M4 深化): 新要求注入 —— 原话逐字入档 + 分诊 + 版本; 只影响**后续** Run。
3423
+ // 当前 Run 不被历史改写 (规则 4): 本路由只写 Goal 上的变更记录与下一 Run 的指令。
3424
+ // ★ M4: 这里还要回答"**正在跑的 Goal** 会发生什么" —— 撤销 → 在跑的这一轮必须停
3425
+ // (走既有外部控制面原语 `setRunStatus`, 与 /api/runs/:id/abort 同一条路; 执行器只在需要时才注入,
3426
+ // 判定本身在 change 接缝里, 本路由不自己判)。事实 (在跑的 Run / 在跑的子 Agent) 由本路由读出来喂进去。
3427
+ app.post('/api/goals/:goalId/requirement', async (req, res) => {
3428
+ const goalId = String(req.params.goalId);
3429
+ try {
3430
+ const { readGoal } = await import('../agents/goal-store.js');
3431
+ const { readRun } = await import('../agents/run-store.js');
3432
+ const { flywheelSeams } = await import('../agents/goal-flywheel-wiring.js');
3433
+ const { isRefusal } = await import('../agents/goal-flywheel/wiring/index.js');
3434
+ const { USER_VISIBLE_STATE_LABELS } = await import('../agents/goal-flywheel/types.js');
3435
+ const text = String(req.body?.text ?? '').trim();
3436
+ if (!text) {
3437
+ res.status(400).json({ error: '缺少 text (新要求的原话)' });
3438
+ return;
3439
+ }
3440
+ const goal = await readGoal(goalId);
3441
+ if (!goal) {
3442
+ res.status(404).json({ error: `goal 不存在: ${goalId}` });
3443
+ return;
3444
+ }
3445
+ // 在跑的 Run 的事实 (读不到记录 = 没有可停的对象; 绝不因为"读不到"去停一个不存在的 Run)
3446
+ const runRec = goal.currentRunId ? await readRun(goal.currentRunId).catch(() => null) : null;
3447
+ // 在跑的子 Agent = 已签合同还没回报的那些 (规则 5: 它们必须先收到变更版本)
3448
+ const liveWorkIds = (goal.continuation?.pendingReports ?? []).map((p) => p.workId).filter(Boolean);
3449
+ const now = new Date().toISOString();
3450
+ const seam = flywheelSeams().change;
3451
+ const view = await seam.ingestChange({
3452
+ goalId,
3453
+ instruction: text,
3454
+ source: 'user',
3455
+ recordedBy: 'web',
3456
+ now,
3457
+ runningRun: runRec ? { runId: runRec.runId, status: String(runRec.status) } : null,
3458
+ liveWorkIds,
3459
+ caller: 'human',
3460
+ });
3461
+ if (isRefusal(view)) {
3462
+ res.status(409).json({ ok: false, refused: true, rule: view.rule, reason: view.reason, goalId });
3463
+ return;
3464
+ }
3465
+ // 计划说要停, 而接缝自己没执行器 (M0 尚未注入 stopRunningRun) → 入口按计划把停落到真 Run 上:
3466
+ // 执行器 = 既有的 setRunStatus (与 /api/runs/:id/abort 同一个原语, 不新造停止通道)。
3467
+ let boundary = view.runBoundary;
3468
+ if (boundary.action === 'stop_running_run' && !boundary.stopped) {
3469
+ const { setRunStatus } = await import('../agents/run-store.js');
3470
+ boundary = await seam.applyRunBoundary({
3471
+ plan: view.plan,
3472
+ now,
3473
+ stop: async (i) => {
3474
+ const r = await setRunStatus(i.runId, i.runStatus, { error: `变更注入 (${i.goalId}): ${String(i.reason).slice(0, 120)}` });
3475
+ return { ok: !!r.ok, reason: r.reason || '' };
3476
+ },
3477
+ });
3478
+ }
3479
+ const bumped = view.plan.outcome === 'next_run' && view.request.kind === 'success_criteria_change'
3480
+ ? view.plan.criteriaVersion
3481
+ : null;
3482
+ res.json({
3483
+ ok: true,
3484
+ goalId,
3485
+ changeId: view.request.changeId,
3486
+ kind: view.request.kind,
3487
+ outcome: view.plan.outcome,
3488
+ status: view.request.status,
3489
+ /** 只有"用户 + 改判据"才 +1 (规则 3); 其余一律 null */
3490
+ criteriaVersionBumpedTo: bumped,
3491
+ criteriaVersion: view.plan.criteriaVersion,
3492
+ /** 下一 Run 真的会读到的指令 (写进 continuation.nextAction) */
3493
+ nextRunDirective: view.plan.nextRunDirective,
3494
+ /** ★ 正在运行的 Goal: 在跑的这一轮停不停 (停不了也会如实说) */
3495
+ runBoundary: boundary,
3496
+ appliesToFutureRunsOnly: view.plan.appliesToFutureRunsOnly,
3497
+ /** 规则 5: 在跑的子 Agent 各拿一份下发内容, 且必须先 ack */
3498
+ childDelivery: {
3499
+ count: view.childDelivery.directives.length,
3500
+ workIds: view.childDelivery.workIds,
3501
+ mustAck: view.childDelivery.mustAck,
3502
+ scopeMissing: view.childDelivery.scopeMissing,
3503
+ blocked: view.childDelivery.blocked,
3504
+ note: view.childDelivery.note,
3505
+ },
3506
+ /** 用户可见态 (六类之一): 有待拍板的变更 → "需要你决定"; null = 基础态读不到 (不编) */
3507
+ visibleState: view.visibleState,
3508
+ visibleZh: view.visibleState ? (USER_VISIBLE_STATE_LABELS[view.visibleState]?.zh || view.visibleState) : null,
3509
+ visibleReason: view.visibleReason,
3510
+ /** 一个 kind 装不下多个意图 → 已入档但不生效, 需拆开重提 */
3511
+ needsDisambiguation: view.needsDisambiguation,
3512
+ /** 流程留痕 (接收→记录→判影响→摘要→评估当前Run→必要时停→重规划→写入下一Run) */
3513
+ steps: view.plan.steps,
3514
+ changeArchive: {
3515
+ authoritative: 'goal.goalChanges (Goal 记录 = 权威; 界面/下一次 Run 读它)',
3516
+ mirror: '~/.bolloon/goal-changes/<goalId>.json (镜像, 由接线层写)',
3517
+ },
3518
+ note: view.plan.outcome === 'rejected'
3519
+ ? '未生效 (见 outcome): 已如实入档, 不会偷偷生效'
3520
+ : '已生效: 只影响后续 Run, 已发生的 Run 记录不被改写',
3521
+ });
3522
+ }
3523
+ catch (err) {
3524
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3525
+ }
3526
+ });
3398
3527
  // 2026-09-16 (M5): 运行列表 —— Web 与 CLI 读同一份 run 事实 (~/.bolloon/runs)
3399
3528
  app.get('/api/runs', async (req, res) => {
3400
3529
  try {
@@ -3519,7 +3648,34 @@ fetchState();
3519
3648
  const { listGoals, formatGoalLine } = await import('../agents/goal-store.js');
3520
3649
  const status = String(req.query?.status || '').trim();
3521
3650
  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) });
3651
+ // 2026-09-25 (飞轮接线): 界面只认 `toUserVisibleState` 映射的**六类**用户可见态 ——
3652
+ // 内部状态 (retry_wait / stalled / recovering) 不再直接上屏。
3653
+ // ★ M4: 再过一遍**变更驱动**的覆盖 —— 有待拍板的新要求 (`needs_approval`) 时必须是
3654
+ // "需要你决定", 否则界面一直显示"正在执行", 没人知道有东西卡在等人 (只升不降)。
3655
+ const { toUserVisibleState } = await import('../agents/goal-flywheel/work-monitor.js');
3656
+ const { changeVisibleState } = await import('../agents/goal-flywheel/wiring/change.js');
3657
+ const { collectWorkBlocks } = await import('../agents/goal-flywheel-wiring.js');
3658
+ const { USER_VISIBLE_STATE_LABELS } = await import('../agents/goal-flywheel/types.js');
3659
+ const visible = await Promise.all(goals.map(async (g) => {
3660
+ try {
3661
+ const base = toUserVisibleState(g.continuation ? { ...g.continuation } : null, await collectWorkBlocks({ goalId: g.goalId }), null,
3662
+ // ★ M5-⑥ (2026-09-25): 界面认 Goal **本体**的终态 —— 完不成这一步的话, 一份落后的
3663
+ // continuation 就能把"已完成"的目标显示成"正在执行" (界面比系统更乐观)。
3664
+ String(g.status ?? '') || null);
3665
+ // 这里 base 一定读得到 (读不到就是上面的 catch) → 覆盖结果为 null 时回落 base 本身
3666
+ // (不是编一个态: base 是这一步刚刚读出来的真态)
3667
+ return changeVisibleState({ base, changes: g.goalChanges ?? [] }).state ?? base;
3668
+ }
3669
+ catch {
3670
+ return 'executing';
3671
+ }
3672
+ }));
3673
+ res.json({
3674
+ count: goals.length, goals, lines: goals.map(formatGoalLine),
3675
+ visible, visibleZh: visible.map((v) => USER_VISIBLE_STATE_LABELS[v]?.zh || v), visibleLabels: USER_VISIBLE_STATE_LABELS,
3676
+ /** 待拍板的变更 (界面/CLI 用同一份事实说清"你在等什么") */
3677
+ pendingChanges: goals.map((g) => (g.goalChanges ?? []).filter((c) => c.status === 'needs_approval' || c.status === 'triaged').length),
3678
+ });
3523
3679
  }
3524
3680
  catch (err) {
3525
3681
  res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
@@ -6814,6 +6970,9 @@ fetchState();
6814
6970
  // 2026-09-19: 社交身份 API (DID + 已验证联系方式 + 联系能力 + 权限 + 证据)
6815
6971
  // 只返回脱敏值; 明文联系方式不进 Run/prompt/Git
6816
6972
  registerContactRoutes(app, {});
6973
+ // 2026-09-25: 手机端「任务协作」API (入群 / 发公告 / 看飞轮进度)
6974
+ // 手机只做 输入·展示待发内容·设备签名; 执行回到桌面, 调用与 CLI 同一批函数 (同一份 store 与协议)
6975
+ registerMobileTaskRoutes(app, {});
6817
6976
  // 2026-09-13: 微支付信息服务 (x402) — 发布 / 402 收款 / 买方代付 / 验真
6818
6977
  registerX402InfoRoutes(app);
6819
6978
  registerOnchainTradeRoutes(app);
package/dist/ios/sw.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * 现在: 代码类资源 (html/js/css/json) 一律 **network-first** (拿不到网才回退缓存),
7
7
  * 缓存名带版本号, activate 时清掉所有旧缓存; 图标等静态资源仍 cache-first。
8
8
  */
9
- const CACHE = 'bolloon-mobile-v1.1';
9
+ const CACHE = 'bolloon-mobile-v0.5.1';
10
10
  const SHELL = [
11
11
  './mobile.html',
12
12
  './mobile.css',