@bolloon/bolloon-agent 0.4.33 → 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 (143) hide show
  1. package/dist/agents/contacts/chain.js +27 -7
  2. package/dist/agents/execution-supervisor.js +493 -35
  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/network-pulse.js +304 -18
  31. package/dist/agents/pi-sdk.js +75 -20
  32. package/dist/agents/run-store.js +18 -5
  33. package/dist/agents/skill-readiness.js +52 -10
  34. package/dist/agents/subagent-manager.js +209 -3
  35. package/dist/agents/task/task-runner.js +9 -3
  36. package/dist/agents/task-group.js +8 -51
  37. package/dist/agents/task-public-text.js +89 -0
  38. package/dist/cli/commands/index.js +1 -0
  39. package/dist/cli/commands/tasks.js +350 -2
  40. package/dist/cli/identity-command.js +177 -0
  41. package/dist/cli/setup-wizard.js +71 -0
  42. package/dist/cli/update-commands.js +150 -24
  43. package/dist/cli-entry.js +12 -2
  44. package/dist/electron-build/agents/contacts/chain.js +26 -6
  45. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  46. package/dist/electron-build/agents/execution-supervisor.js +492 -34
  47. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  48. package/dist/electron-build/agents/external-events.js +34 -5
  49. package/dist/electron-build/agents/external-events.js.map +1 -1
  50. package/dist/electron-build/agents/goal-criteria.js +10 -3
  51. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  52. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  53. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  54. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  55. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  56. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  57. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  58. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  59. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  60. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  61. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  62. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  63. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  64. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  65. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  66. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  67. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  68. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  69. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  70. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  71. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  72. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  73. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  74. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  75. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  76. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  78. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  79. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  80. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  81. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  82. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  83. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  84. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  85. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  86. package/dist/electron-build/agents/goal-store.js +35 -1
  87. package/dist/electron-build/agents/goal-store.js.map +1 -1
  88. package/dist/electron-build/agents/network-pulse.js +307 -19
  89. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  90. package/dist/electron-build/agents/pi-sdk.js +74 -19
  91. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  92. package/dist/electron-build/agents/run-store.js +20 -5
  93. package/dist/electron-build/agents/run-store.js.map +1 -1
  94. package/dist/electron-build/agents/skill-readiness.js +52 -9
  95. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  96. package/dist/electron-build/agents/task/task-runner.js +8 -2
  97. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  98. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  99. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  100. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  101. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  102. package/dist/electron-build/utils/dual-source.js +471 -0
  103. package/dist/electron-build/utils/dual-source.js.map +1 -0
  104. package/dist/electron-build/utils/update-manager.js +331 -57
  105. package/dist/electron-build/utils/update-manager.js.map +1 -1
  106. package/dist/electron-build/utils/update-state.js +16 -1
  107. package/dist/electron-build/utils/update-state.js.map +1 -1
  108. package/dist/electron-build/utils/version-info.js +82 -2
  109. package/dist/electron-build/utils/version-info.js.map +1 -1
  110. package/dist/index.js +103 -12
  111. package/dist/ios/chain-explorer.js +617 -0
  112. package/dist/ios/explorer.html +163 -0
  113. package/dist/ios/index.html +96 -0
  114. package/dist/ios/mobile-agent.js +2 -2
  115. package/dist/ios/mobile-contacts.js +329 -0
  116. package/dist/ios/mobile-core.js +1379 -226
  117. package/dist/ios/mobile-privacy.js +185 -0
  118. package/dist/ios/mobile.css +19 -0
  119. package/dist/ios/mobile.html +96 -0
  120. package/dist/ios/mobile.js +706 -7
  121. package/dist/ios/routes-chain-index.js +108 -0
  122. package/dist/ios/routes-contacts.js +420 -0
  123. package/dist/ios/routes-onchain-trade.js +99 -0
  124. package/dist/ios/routes-x402-info.js +1 -0
  125. package/dist/ios/server.js +762 -0
  126. package/dist/ios/task-frame-server.js +211 -0
  127. package/dist/orbitdb/cid-database.js +81 -16
  128. package/dist/orbitdb/ipfs-node.js +31 -2
  129. package/dist/utils/dual-source.js +426 -0
  130. package/dist/utils/update-cli.js +16 -21
  131. package/dist/utils/update-health.js +35 -5
  132. package/dist/utils/update-manager.js +329 -57
  133. package/dist/utils/update-state.js +16 -1
  134. package/dist/utils/version-info.js +75 -1
  135. package/dist/web/mobile-contacts.js +44 -11
  136. package/dist/web/mobile-core.js +1197 -513
  137. package/dist/web/mobile-tasks.js +339 -0
  138. package/dist/web/mobile.css +19 -0
  139. package/dist/web/mobile.html +64 -0
  140. package/dist/web/mobile.js +531 -1
  141. package/dist/web/routes-mobile-tasks.js +353 -0
  142. package/dist/web/server.js +163 -4
  143. package/package.json +3 -1
@@ -69,7 +69,7 @@
69
69
  });
70
70
  } catch (e) {}
71
71
 
72
- const TITLES = { main: '首页', friends: '好友', network: '网络', me: '我' };
72
+ const TITLES = { main: '首页', friends: '好友', network: '网络', tasks: '任务', me: '我' };
73
73
  let currentTab = 'main';
74
74
  function switchTab(tab) {
75
75
  currentTab = tab;
@@ -3170,3 +3170,533 @@
3170
3170
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bindEvents);
3171
3171
  else bindEvents();
3172
3172
  })();
3173
+
3174
+ // ============ 任务协作: 入群 · 发公告 · 看飞轮进度 (2026-09-25) ============
3175
+ // 独立 IIFE (与联系方式面板同款): 只用 window.BolloonCore.tasks (编译进 mobile-core.js)。
3176
+ // 分工**不变**: 手机 = 输入 · 展示待发内容 · 设备签名; 桌面 = 落盘 · 执行 · 等待回复 · 证据。
3177
+ // 纪律:
3178
+ // · 动态文本一律 textContent (本文件这一段**不用 innerHTML** 拼任何数据)
3179
+ // · 渲染只取白名单字段 (视图对象里多出来的字段进不了 DOM)
3180
+ // · 双语走 data-zh/data-en (切语言只重写文字节点, 不重建结构)
3181
+ // · 相对时间只更新 .rel-time 文字节点 (不重渲染整页)
3182
+ (function () {
3183
+ const $ = (sel) => document.querySelector(sel);
3184
+ const T = () => (window.BolloonCore && window.BolloonCore.tasks) || null;
3185
+
3186
+ // ── 语言 (data-zh/data-en; 默认中文, 显式切 en 才英文) ──────────────────────
3187
+ function lang() {
3188
+ try { return localStorage.getItem('bolloon_lang') === 'en' ? 'en' : 'zh'; } catch (e) { return 'zh'; }
3189
+ }
3190
+ function bilingual(el, zh, en) {
3191
+ if (!el) return el;
3192
+ el.setAttribute('data-zh', String(zh));
3193
+ el.setAttribute('data-en', String(en));
3194
+ el.textContent = lang() === 'en' ? String(en) : String(zh);
3195
+ return el;
3196
+ }
3197
+ function applyLang(root) {
3198
+ const scope = root || document;
3199
+ scope.querySelectorAll('[data-zh][data-en]').forEach((el) => {
3200
+ el.textContent = lang() === 'en' ? el.getAttribute('data-en') : el.getAttribute('data-zh');
3201
+ });
3202
+ }
3203
+ window.__mobileLang = {
3204
+ get: lang,
3205
+ set(v) {
3206
+ try { localStorage.setItem('bolloon_lang', v === 'en' ? 'en' : 'zh'); } catch (e) {}
3207
+ applyLang();
3208
+ // 列表里的空态/描述是用 lang() 现拼的 → 切完语言重渲染一次, 免得留下上一语言的旧句子
3209
+ try {
3210
+ if (window.__mobileTasksUi && typeof window.__mobileTasksUi.refresh === 'function') window.__mobileTasksUi.refresh();
3211
+ } catch (e) {}
3212
+ },
3213
+ };
3214
+
3215
+ // ── DOM 小工具 (全部 textContent) ─────────────────────────────────────────
3216
+ function mk(tag, cls) { const e = document.createElement(tag); if (cls) e.className = cls; return e; }
3217
+ function line(cls, text) { const e = mk('div', cls || 'sheet-text'); if (text !== undefined && text !== null) e.textContent = String(text); return e; }
3218
+ function row(text, sub) {
3219
+ const e = mk('div', 'list-item');
3220
+ const box = mk('div');
3221
+ box.style.flex = '1';
3222
+ box.appendChild(line('', text));
3223
+ if (sub !== undefined && sub !== null && String(sub) !== '') {
3224
+ const s = line('', sub);
3225
+ s.style.fontSize = '12px';
3226
+ s.style.color = 'var(--text-secondary)';
3227
+ box.appendChild(s);
3228
+ }
3229
+ e.appendChild(box);
3230
+ return e;
3231
+ }
3232
+ /** 相对时间节点: 只改这一个文字节点 (data-at 记住基准) */
3233
+ function relNode(atMs) {
3234
+ const s = mk('span', 'rel-time');
3235
+ s.setAttribute('data-at', String(Number(atMs) || 0));
3236
+ s.textContent = relText(Number(atMs) || 0);
3237
+ return s;
3238
+ }
3239
+ /** 相对时间节点 (输入是**毫秒差**, 例如 deadlineInMs / resumeInMs) —— 每 30s 只改这个文字节点 */
3240
+ function relDeltaNode(deltaMs) {
3241
+ const s = mk('span', 'rel-time');
3242
+ s.setAttribute('data-delta', String(Number(deltaMs) || 0));
3243
+ s.textContent = relDeltaText(Number(deltaMs) || 0);
3244
+ return s;
3245
+ }
3246
+ function relDeltaText(deltaMs) {
3247
+ const t = T();
3248
+ if (!t || !t.relative) return '';
3249
+ return t.relative(Number(deltaMs) || 0, lang()) || '';
3250
+ }
3251
+ function relText(atMs) {
3252
+ const t = T();
3253
+ if (!atMs) return '';
3254
+ if (t && t.relative) return t.relative(Date.now() - Number(atMs), lang()) || '';
3255
+ return '';
3256
+ }
3257
+ function shorten(v, n) { const t = T(); return t && t.shorten ? t.shorten(v, n) : String(v == null ? '' : v).slice(0, n || 12); }
3258
+ /** 最后一道自检: 命中红线就不显示原文 (桌面已经挡过, 这里只兜底) */
3259
+ function safeText(v) {
3260
+ const s = String(v == null ? '' : v);
3261
+ const t = T();
3262
+ if (t && t.scanText) {
3263
+ const hits = t.scanText(s);
3264
+ if (hits && hits.length) return '[' + (lang() === 'en' ? 'redacted:' : '已遮蔽:') + hits[0].rule + ']';
3265
+ }
3266
+ return s;
3267
+ }
3268
+ function setText(sel, text) { const el = $(sel); if (el) el.textContent = text == null ? '' : String(text); }
3269
+ /** 动态状态行: 带上 data-zh/data-en, 这样切语言时它也换 (applyLang 只管文字节点) */
3270
+ function dynText(sel, zh, en) {
3271
+ const el = $(sel);
3272
+ if (!el) return;
3273
+ el.setAttribute('data-zh', String(zh));
3274
+ el.setAttribute('data-en', String(en));
3275
+ el.textContent = lang() === 'en' ? String(en) : String(zh);
3276
+ }
3277
+ function clear(node) { if (node) while (node.firstChild) node.removeChild(node.firstChild); }
3278
+ function openSheet() { const el = $('#task-confirm-sheet'); if (el) el.hidden = false; }
3279
+ function closeSheet() { const el = $('#task-confirm-sheet'); if (el) el.hidden = true; }
3280
+ function toast(msg) {
3281
+ if (window.alert) window.alert(String(msg));
3282
+ }
3283
+
3284
+ // ── 状态 (完整 id 只留在这段 JS 内存里, 不进 DOM 文本) ─────────────────────
3285
+ const state = { groups: [], groupsView: [], board: null, flywheel: null, trail: null, selectedGroupRef: '', pending: null };
3286
+
3287
+ // ── 渲染: 群 ──────────────────────────────────────────────────────────────
3288
+ function renderGroups() {
3289
+ const list = $('#tasks-groups');
3290
+ if (!list) return;
3291
+ clear(list);
3292
+ if (!state.groups.length) {
3293
+ list.appendChild(line('sheet-text', lang() === 'en' ? 'No groups joined yet' : '还没有加入任何群'));
3294
+ return;
3295
+ }
3296
+ state.groups.forEach((g, i) => {
3297
+ const view = state.groupsView[i] || {};
3298
+ const r = row(String(view.name || '') + ' · ' + String(view.idShort || ''), '');
3299
+ const when = relNode(Date.parse(String(view.joinedAt || '')) || 0);
3300
+ const sub = line('', '');
3301
+ sub.style.fontSize = '12px';
3302
+ sub.style.color = 'var(--text-secondary)';
3303
+ sub.appendChild(document.createTextNode(lang() === 'en' ? 'joined ' : '加入于 '));
3304
+ sub.appendChild(when);
3305
+ r.firstChild.appendChild(sub);
3306
+ const b = mk('button', 'sheet-choice');
3307
+ b.style.flex = '0 0 auto';
3308
+ bilingual(b, '退群', 'Leave');
3309
+ b.setAttribute('data-group-leave', String(i));
3310
+ b.addEventListener('click', () => askLeave(i));
3311
+ r.appendChild(b);
3312
+ list.appendChild(r);
3313
+ });
3314
+ }
3315
+
3316
+ // ── 渲染: 公告板 ──────────────────────────────────────────────────────────
3317
+ function renderBoard() {
3318
+ const list = $('#tasks-board');
3319
+ if (!list) return;
3320
+ clear(list);
3321
+ const d = state.board || {};
3322
+ const items = d.items || [];
3323
+ if (!items.length) {
3324
+ list.appendChild(line('sheet-text', lang() === 'en' ? 'Board is empty' : '板上暂时没有公告'));
3325
+ }
3326
+ items.forEach((it, i) => {
3327
+ const r = row(
3328
+ String(it.capability || '') + ' · ' + String(it.idShort || '') + ' · ' + String((it.statusLabel || {})[lang()] || ''),
3329
+ '',
3330
+ );
3331
+ const sub = line('', '');
3332
+ sub.style.fontSize = '12px';
3333
+ sub.style.color = 'var(--text-secondary)';
3334
+ sub.appendChild(document.createTextNode((lang() === 'en' ? 'budget ' : '预算 ') + safeText(it.budgetLabel || '—') + ' · '));
3335
+ sub.appendChild(document.createTextNode(lang() === 'en' ? 'closes ' : '截止 '));
3336
+ sub.appendChild(relDeltaNode(it.deadlineInMs));
3337
+ r.firstChild.appendChild(sub);
3338
+ if (it.claimable) {
3339
+ const b = mk('button', 'sheet-choice');
3340
+ b.style.flex = '0 0 auto';
3341
+ bilingual(b, '发进群', 'Post to group');
3342
+ b.setAttribute('data-announce', String(i));
3343
+ b.addEventListener('click', () => askAnnounceToGroup(i));
3344
+ r.appendChild(b);
3345
+ }
3346
+ list.appendChild(r);
3347
+ });
3348
+ const notes = Array.isArray(d.notes) ? d.notes : [];
3349
+ if (notes.length) list.appendChild(line('sheet-text', notes.join(' · ')));
3350
+ }
3351
+
3352
+ // ── 渲染: 飞轮进度 (只读; 状态来自 goal-flywheel 的五类用户可见状态) ────────
3353
+ function renderFlywheel() {
3354
+ const list = $('#tasks-flywheel');
3355
+ if (!list) return;
3356
+ clear(list);
3357
+ const goals = (state.flywheel && state.flywheel.goals) || [];
3358
+ if (!goals.length) {
3359
+ list.appendChild(line('sheet-text', lang() === 'en' ? 'No goals yet' : '还没有目标'));
3360
+ const hint = state.flywheel && state.flywheel.hint;
3361
+ if (hint) list.appendChild(line('sheet-text', hint));
3362
+ return;
3363
+ }
3364
+ const zh = lang() === 'zh';
3365
+ goals.forEach((g) => {
3366
+ const v = g.view || {};
3367
+ const r = row(String(g.objectiveShort || '') + ' · ' + String((v.stateLabel || {})[lang()] || g.status || ''), '');
3368
+ const box = r.firstChild;
3369
+ const rep = v.report || {};
3370
+ if (rep.nextStep) box.appendChild(line('sheet-text', (zh ? '下一步: ' : 'next step: ') + safeText(rep.nextStep)));
3371
+ if (rep.conclusion) box.appendChild(line('sheet-text', (zh ? '结论: ' : 'conclusion: ') + safeText(rep.conclusion)));
3372
+ if (Array.isArray(rep.remaining) && rep.remaining.length) {
3373
+ box.appendChild(line('sheet-text', (zh ? '还剩: ' : 'remaining: ') + rep.remaining.map(safeText).join(' · ')));
3374
+ }
3375
+ if (Array.isArray(rep.blockReasons) && rep.blockReasons.length) {
3376
+ box.appendChild(line('sheet-text', (zh ? '卡住原因: ' : 'blocked by: ') + rep.blockReasons.map(safeText).join(' · ')));
3377
+ }
3378
+ const sub = line('', '');
3379
+ sub.style.fontSize = '12px';
3380
+ sub.style.color = 'var(--text-secondary)';
3381
+ if (v.resumeInMs !== null && v.resumeInMs !== undefined) {
3382
+ sub.appendChild(document.createTextNode(zh ? '下次醒来 ' : 'wakes '));
3383
+ sub.appendChild(relDeltaNode(v.resumeInMs));
3384
+ }
3385
+ sub.appendChild(document.createTextNode((zh ? ' · 会继续: ' : ' · will continue: ') + (rep.willContinue ? (zh ? '是' : 'yes') : (zh ? '否' : 'no'))));
3386
+ box.appendChild(sub);
3387
+ if (v.requiredAgent) box.appendChild(line('sheet-text', safeText(v.requiredAgent)));
3388
+ if (v.riskLevel) box.appendChild(line('sheet-text', (zh ? '风险: ' : 'risk: ') + safeText(v.riskLevel)));
3389
+ const blocks = Array.isArray(v.blocks) ? v.blocks : [];
3390
+ blocks.forEach((b) => {
3391
+ const label = [(b.kindLabel || {})[lang()], (b.ownerLabel || {})[lang()], (b.actionLabel || {})[lang()]].filter(Boolean).join(' · ');
3392
+ const bl = line('sheet-text', safeText(label) + (b.note ? ' — ' + safeText(b.note) : ''));
3393
+ box.appendChild(bl);
3394
+ const bd = line('', '');
3395
+ bd.style.fontSize = '12px';
3396
+ bd.style.color = 'var(--text-secondary)';
3397
+ bd.appendChild(document.createTextNode(zh ? '卡了 ' : 'blocked for '));
3398
+ bd.appendChild(relDeltaNode(b.blockedForMs));
3399
+ box.appendChild(bd);
3400
+ });
3401
+ list.appendChild(r);
3402
+ });
3403
+ const note = state.flywheel && state.flywheel.note;
3404
+ if (note) list.appendChild(line('sheet-text', note));
3405
+ }
3406
+
3407
+ // ── 渲染: 群痕迹 (只读) ───────────────────────────────────────────────────
3408
+ function renderTrail() {
3409
+ const list = $('#tasks-trail');
3410
+ if (!list) return;
3411
+ clear(list);
3412
+ const t = state.trail;
3413
+ if (!t) { list.appendChild(line('sheet-text', lang() === 'en' ? 'Pick a group first' : '先选一个群')); return; }
3414
+ if (!t.entries.length) {
3415
+ list.appendChild(line('sheet-text', lang() === 'en' ? 'No trail entries for this round' : '这一期还没有过程痕迹'));
3416
+ return;
3417
+ }
3418
+ t.entries.forEach((e) => {
3419
+ const label = (e.kindLabel || {})[lang()] || e.kind;
3420
+ const r = row(String(label) + ' · ' + String(e.sender || ''), '');
3421
+ const box = r.firstChild;
3422
+ const sub = line('', '');
3423
+ sub.style.fontSize = '12px';
3424
+ sub.style.color = 'var(--text-secondary)';
3425
+ sub.appendChild(relNode(e.atMs || 0));
3426
+ box.appendChild(sub);
3427
+ (e.facts || []).forEach((f) => box.appendChild(line('sheet-text', safeText(f.k) + '=' + safeText(f.v))));
3428
+ list.appendChild(r);
3429
+ });
3430
+ if (t.privacyHits) list.appendChild(line('sheet-text', lang() === 'en' ? '⚠ some lines hit the privacy rules and were redacted' : '⚠ 有消息命中隐私红线, 已遮蔽'));
3431
+ if (t.ignoredMessages) list.appendChild(line('sheet-text', (lang() === 'en' ? 'ignored messages: ' : '忽略消息: ') + String(t.ignoredMessages)));
3432
+ }
3433
+
3434
+ // ── 确认页 (展示待发内容 → 确认 → 签名) ───────────────────────────────────
3435
+ function renderConfirm() {
3436
+ const t = T();
3437
+ const p = state.pending;
3438
+ if (!t || !p) return;
3439
+ const d = t.describeConfirm(p.kind, p.req) || {};
3440
+ const title = $('#task-confirm-title');
3441
+ if (title) bilingual(title, d.titleZh || '确认这个动作', d.titleEn || 'Confirm this action');
3442
+ const lines = $('#task-confirm-lines');
3443
+ if (lines) {
3444
+ clear(lines);
3445
+ (d.lines || []).forEach((l) => lines.appendChild(line('sheet-text', (lang() === 'en' ? l.kEn : l.k) + ': ' + safeText(l.v))));
3446
+ }
3447
+ setText('#task-confirm-what', lang() === 'en'
3448
+ ? 'This will be signed with this device key. The desktop verifies that signature before doing anything.'
3449
+ : '这个动作会用本机设备密钥签名; 桌面验签通过后才会真正执行。');
3450
+ // 群选择 (群消息类动作必须显式选群)
3451
+ const wrap = $('#task-confirm-group-wrap');
3452
+ const box = $('#task-confirm-group');
3453
+ const needsGroup = p.needsGroup === true;
3454
+ if (wrap) wrap.hidden = !needsGroup;
3455
+ if (box && needsGroup) {
3456
+ clear(box);
3457
+ state.groups.forEach((g, i) => {
3458
+ const view = state.groupsView[i] || {};
3459
+ const b = mk('button', 'sheet-choice');
3460
+ bilingual(b, String(view.name || ''), String(view.name || ''));
3461
+ b.setAttribute('data-pick-group', String(i));
3462
+ if (p.req.groupRef === g.id) b.style.borderColor = 'var(--accent)';
3463
+ b.addEventListener('click', () => { p.req.groupRef = g.id; state.selectedGroupRef = g.id; renderConfirm(); refreshPreview(); });
3464
+ box.appendChild(b);
3465
+ });
3466
+ if (!state.groups.length) box.appendChild(line('sheet-text', lang() === 'en' ? 'no groups' : '还没有群'));
3467
+ }
3468
+ // 群消息类: 预览按钮 + 原文区
3469
+ const pv = $('#task-confirm-preview');
3470
+ if (pv) pv.hidden = !p.showPreview;
3471
+ const ct = $('#task-confirm-content-title');
3472
+ const cc = $('#task-confirm-content');
3473
+ if (ct) ct.hidden = !p.previewText;
3474
+ if (cc) { cc.hidden = !p.previewText; cc.textContent = p.previewText || ''; }
3475
+ const yes = $('#task-confirm-yes');
3476
+ if (yes) {
3477
+ const disabled = needsGroup && !p.req.groupRef;
3478
+ yes.disabled = !!disabled;
3479
+ yes.style.opacity = disabled ? '0.5' : '';
3480
+ }
3481
+ }
3482
+
3483
+ async function refreshPreview() {
3484
+ const t = T();
3485
+ const p = state.pending;
3486
+ if (!t || !p || !p.showPreview) return;
3487
+ if (!p.req.groupRef) { p.previewText = ''; renderConfirm(); return; }
3488
+ const r = await t.previewGroupMessage(p.req);
3489
+ if (r && r.ok) { p.previewText = (r.data && r.data.text) || ''; setText('#tasks-hint', ''); }
3490
+ else { p.previewText = ''; setText('#tasks-hint', (lang() === 'en' ? 'preview failed: ' : '预览失败: ') + String((r && r.code) || '') + ' ' + String((r && r.error) || '')); }
3491
+ renderConfirm();
3492
+ }
3493
+
3494
+ function askConfirm(kind, req, opts) {
3495
+ state.pending = Object.assign({ kind: kind, req: req, showPreview: false, previewText: '' }, opts || {});
3496
+ renderConfirm();
3497
+ openSheet();
3498
+ if (state.pending.showPreview) refreshPreview();
3499
+ }
3500
+
3501
+ function requireSigning() {
3502
+ const t = T();
3503
+ if (!t) { toast(lang() === 'en' ? 'phone kernel not ready' : '手机内核未就绪 (mobile-core 没加载)'); return null; }
3504
+ if (!t.deviceSigning || !t.deviceSigning()) {
3505
+ toast(lang() === 'en'
3506
+ ? 'this WebView cannot do Ed25519 device signing — run it from the desktop instead'
3507
+ : '本机 WebView 不支持 Ed25519 设备签名 —— 这个动作请在桌面端执行 (不降级, 不假装签了)');
3508
+ return null;
3509
+ }
3510
+ return t;
3511
+ }
3512
+
3513
+ // ── 四个能力的入口 ────────────────────────────────────────────────────────
3514
+ function askJoin() {
3515
+ const link = (($('#tasks-group-link') || {}).value || '').trim();
3516
+ if (!link) { toast(lang() === 'en' ? 'paste the group invite link first' : '先粘贴群邀请链接'); return; }
3517
+ if (!requireSigning()) return;
3518
+ const req = T().emptyRequest('group_join');
3519
+ req.groupRef = link;
3520
+ askConfirm('group_join', req, {});
3521
+ }
3522
+ function askCreate() {
3523
+ const name = (($('#tasks-group-name') || {}).value || '').trim();
3524
+ if (!name) { toast(lang() === 'en' ? 'give the group a name first' : '先给群起个名字'); return; }
3525
+ if (!requireSigning()) return;
3526
+ const req = T().emptyRequest('group_create');
3527
+ req.groupRef = name;
3528
+ askConfirm('group_create', req, {});
3529
+ }
3530
+ function askLeave(i) {
3531
+ if (!requireSigning()) return;
3532
+ const g = state.groups[i];
3533
+ if (!g) return;
3534
+ const req = T().emptyRequest('group_leave');
3535
+ req.groupRef = g.id;
3536
+ askConfirm('group_leave', req, {});
3537
+ }
3538
+ function askPublish() {
3539
+ if (!requireSigning()) return;
3540
+ const cap = (($('#tasks-pub-capability') || {}).value || '').trim();
3541
+ const ins = (($('#tasks-pub-instruction') || {}).value || '').trim();
3542
+ const bud = (($('#tasks-pub-budget') || {}).value || '').trim();
3543
+ const dl = (($('#tasks-pub-deadline') || {}).value || '').trim();
3544
+ if (!cap || !ins || !bud) { toast(lang() === 'en' ? 'capability / instruction / budget are required' : '能力 / 任务正文 / 预算 都要填'); return; }
3545
+ const req = T().emptyRequest('announce_publish');
3546
+ req.capability = cap; req.instruction = ins; req.budgetHuman = bud; req.currency = 'USDC';
3547
+ if (dl) req.deadline = dl;
3548
+ askConfirm('announce_publish', req, {});
3549
+ }
3550
+ function askAnnounceToGroup(i) {
3551
+ if (!requireSigning()) return;
3552
+ const it = ((state.board || {}).items || [])[i];
3553
+ if (!it) return;
3554
+ const req = T().emptyRequest('announce_to_group');
3555
+ req.announcementId = it.ref || '';
3556
+ req.groupRef = state.selectedGroupRef || (state.groups[0] && state.groups[0].id) || '';
3557
+ if (!req.announcementId) { toast(lang() === 'en' ? 'desktop did not give a usable announcement id' : '桌面没给出可用的公告号, 先刷新') ; return; }
3558
+ askConfirm('announce_to_group', req, { needsGroup: true, showPreview: true });
3559
+ }
3560
+ function askTrail(kind) {
3561
+ if (!requireSigning()) return;
3562
+ const req = T().emptyRequest('trail_post');
3563
+ req.groupRef = state.selectedGroupRef || (state.groups[0] && state.groups[0].id) || '';
3564
+ req.trailKind = kind;
3565
+ if (!req.groupRef) { toast(lang() === 'en' ? 'pick a group first' : '先选一个群'); return; }
3566
+ askConfirm('trail_post', req, { needsGroup: true, showPreview: true });
3567
+ }
3568
+
3569
+ async function doConfirmed() {
3570
+ const t = T();
3571
+ const p = state.pending;
3572
+ if (!t || !p) return;
3573
+ const yes = $('#task-confirm-yes');
3574
+ if (yes) yes.disabled = true;
3575
+ const r = await t.execute(p.req);
3576
+ if (yes) yes.disabled = false;
3577
+ closeSheet();
3578
+ if (r && r.ok) {
3579
+ const text = (r.data && r.data.text) || (lang() === 'en' ? 'done' : '已执行');
3580
+ t.setHint ? t.setHint(safeText(text)) : null;
3581
+ toast(safeText(text));
3582
+ } else {
3583
+ const code = String((r && r.code) || 'unknown');
3584
+ const err = String((r && r.error) || '');
3585
+ t.setHint ? t.setHint((lang() === 'en' ? 'failed: ' : '失败: ') + code) : null;
3586
+ toast((lang() === 'en' ? 'not executed: ' : '没有执行: ') + code + (err ? '\n' + safeText(err) : ''));
3587
+ }
3588
+ state.pending = null;
3589
+ state.selectedGroupRef = '';
3590
+ await refreshAll();
3591
+ }
3592
+
3593
+ // ── 刷新 ──────────────────────────────────────────────────────────────────
3594
+ async function refreshAll() {
3595
+ const t = T();
3596
+ if (!t) {
3597
+ dynText('#tasks-signing', '手机内核未就绪 (mobile-core 没加载)', 'phone kernel not ready');
3598
+ return;
3599
+ }
3600
+ const canSign = !!t.deviceSigning();
3601
+ const signingEl = $('#tasks-signing');
3602
+ if (signingEl) signingEl.setAttribute('data-signing', canSign ? 'yes' : 'no');
3603
+ setText('#tasks-hint', t.hint ? t.hint() : '');
3604
+ dynText('#tasks-signing', canSign
3605
+ ? '本机可做设备签名 (Ed25519) —— 高风险动作必须由它签名'
3606
+ : '⚠ 本机 WebView 不支持签名 —— 高风险动作请在桌面端执行',
3607
+ canSign
3608
+ ? 'this device can sign (Ed25519) — risky actions need it'
3609
+ : '⚠ this WebView cannot sign — run risky actions on the desktop');
3610
+
3611
+ const g = await t.listGroups();
3612
+ state.groups = (g && g.ok && Array.isArray(g.data)) ? g.data.map((x) => ({ id: x.id, name: x.name })) : [];
3613
+ state.groups.some((x) => x.id === state.selectedGroupRef) || (state.selectedGroupRef = state.groups[0] ? state.groups[0].id : '');
3614
+ state.groupsView = (g && g.ok && Array.isArray(g.data)) ? g.data : [];
3615
+ renderGroups();
3616
+ renderTrailGroupPicker();
3617
+
3618
+ const b = await t.loadBoard();
3619
+ state.board = b && b.ok ? b.data : {};
3620
+ renderBoard();
3621
+
3622
+ const f = await t.loadFlywheel();
3623
+ state.flywheel = f && f.ok ? f.data : {};
3624
+ renderFlywheel();
3625
+
3626
+ if (state.selectedGroupRef) await loadTrailNow();
3627
+ }
3628
+
3629
+ function renderTrailGroupPicker() {
3630
+ const list = $('#tasks-trail-group-picker');
3631
+ if (!list) return;
3632
+ clear(list);
3633
+ if (!state.groups.length) { list.appendChild(line('sheet-text', lang() === 'en' ? 'join a group to see its trail' : '入群后可以看群里这一期的痕迹')); return; }
3634
+ state.groups.forEach((g, i) => {
3635
+ const view = state.groupsView[i] || {};
3636
+ const b = mk('button', 'sheet-choice');
3637
+ bilingual(b, String(view.name || ''), String(view.name || ''));
3638
+ b.setAttribute('data-pick-trail', String(i));
3639
+ if (g.id === state.selectedGroupRef) b.style.borderColor = 'var(--accent)';
3640
+ b.addEventListener('click', async () => {
3641
+ state.selectedGroupRef = g.id;
3642
+ renderTrailGroupPicker();
3643
+ await loadTrailNow();
3644
+ });
3645
+ list.appendChild(b);
3646
+ });
3647
+ }
3648
+
3649
+ async function loadTrailNow() {
3650
+ const t = T();
3651
+ if (!t || !state.selectedGroupRef) return;
3652
+ const r = await t.loadTrail({ groupRef: state.selectedGroupRef });
3653
+ state.trail = (r && r.ok) ? r.data : null;
3654
+ if (!r || !r.ok) setText('#tasks-hint', (lang() === 'en' ? 'trail read failed: ' : '读群痕迹失败: ') + String((r && r.code) || '') + ' ' + String((r && r.error) || ''));
3655
+ renderTrail();
3656
+ }
3657
+
3658
+ function bindEvents() {
3659
+ const tab = document.querySelector('.tab[data-tab="tasks"]');
3660
+ if (tab) tab.addEventListener('click', () => { applyLang(); refreshAll(); });
3661
+ const r = $('#tasks-refresh');
3662
+ if (r) r.addEventListener('click', refreshAll);
3663
+ const j = $('#tasks-group-join');
3664
+ if (j) j.addEventListener('click', askJoin);
3665
+ const c = $('#tasks-group-create');
3666
+ if (c) c.addEventListener('click', askCreate);
3667
+ const p = $('#tasks-pub-submit');
3668
+ if (p) p.addEventListener('click', askPublish);
3669
+ const tr = $('#tasks-trail-refresh');
3670
+ if (tr) tr.addEventListener('click', loadTrailNow);
3671
+ const pv = $('#task-confirm-preview');
3672
+ if (pv) pv.addEventListener('click', refreshPreview);
3673
+ const yes = $('#task-confirm-yes');
3674
+ if (yes) yes.addEventListener('click', doConfirmed);
3675
+ const no = $('#task-confirm-no');
3676
+ if (no) no.addEventListener('click', () => { state.pending = null; closeSheet(); });
3677
+
3678
+ // 相对时间: 每 30s 只改 .rel-time 文字节点 (不重渲染, 不动结构)
3679
+ setInterval(() => {
3680
+ document.querySelectorAll('.rel-time[data-at]').forEach((el) => {
3681
+ const next = relText(Number(el.getAttribute('data-at')) || 0);
3682
+ if (next && el.textContent !== next) el.textContent = next;
3683
+ });
3684
+ document.querySelectorAll('.rel-time[data-delta]').forEach((el) => {
3685
+ const next = relDeltaText(Number(el.getAttribute('data-delta')) || 0);
3686
+ if (next && el.textContent !== next) el.textContent = next;
3687
+ });
3688
+ }, 30000);
3689
+
3690
+ applyLang();
3691
+ }
3692
+
3693
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bindEvents);
3694
+ else bindEvents();
3695
+
3696
+ // 供验收脚本驱动 (真 headless DOM 用): 只暴露行为入口, 不暴露数据
3697
+ window.__mobileTasksUi = {
3698
+ refresh: refreshAll, askJoin: askJoin, askCreate: askCreate, askPublish: askPublish,
3699
+ askTrail: askTrail, askAnnounceToGroup: askAnnounceToGroup, confirm: doConfirmed,
3700
+ preview: refreshPreview, lang: window.__mobileLang, state: state,
3701
+ };
3702
+ })();