@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;
@@ -1712,7 +1712,7 @@
1712
1712
  <div class="conv-item" id="settings-data"><span class="list-icon">${ICONS.chip}</span><span style="flex:1;min-width:0"><span style="display:block">本机数据</span><span class="conv-preview" style="display:block">读取中…</span></span><span class="list-arrow">›</span></div>
1713
1713
  <div class="conv-item" id="settings-desktop"><span class="list-icon">${ICONS.globe}</span><span>电脑端同步</span><span class="list-arrow">›</span></div>
1714
1714
  <div class="conv-item" id="settings-chain"><span class="list-icon">${ICONS.chip}</span><span>链上配置 (RPC/网络)</span><span class="list-arrow">›</span></div>
1715
- <div class="conv-item" id="settings-accessibility"><span class="list-icon">${ICONS.globe}</span><span>无障碍服务 (屏幕触控)</span><span class="list-arrow">›</span></div>
1715
+ <div class="conv-item" id="settings-accessibility"><span class="list-icon">${ICONS.globe}</span><span>完全访问权限<span class="conv-preview" style="display:block" id="access-preview">默认开启</span></span><span class="list-arrow">›</span></div>
1716
1716
  <div class="conv-item" id="settings-ipfs"><span class="list-icon">${ICONS.chip}</span><span>IPFS 存储</span><span class="list-arrow">›</span></div>
1717
1717
  <div class="conv-item" id="settings-helia"><span class="list-icon">${ICONS.globe}</span><span>本机 IPFS 节点</span><span class="list-arrow">›</span></div>
1718
1718
  <div class="conv-item" id="settings-selfcard"><span class="list-icon">${ICONS.chip}</span><span id="selfcard-text">显示本机卡片: 开</span></div>
@@ -1729,16 +1729,46 @@
1729
1729
  const next = currentTheme === 'auto' ? 'light' : (currentTheme === 'light' ? 'dark' : 'auto');
1730
1730
  applyTheme(next, true);
1731
1731
  });
1732
- // 无障碍服务: 屏幕触控 (电脑端 phone.tap / phone.swipe) 的前提 — 与 MCP/Skills 控制无关
1732
+ // 完全访问权限 (屏幕点按/滑动, 电脑端 phone.tap / phone.swipe 的前提) — 默认开启
1733
+ const ACCESS_AUTO_KEY = 'bolloon_access_auto_guided';
1734
+ const refreshAccessPreview = async () => {
1735
+ const el = document.querySelector('#access-preview');
1736
+ if (!el) return;
1737
+ const cap = window.Capacitor;
1738
+ const b = cap && cap.Plugins && cap.Plugins.RokidBridge;
1739
+ if (!b) { el.textContent = '仅手机 App'; return; }
1740
+ let st = {};
1741
+ try { st = (await b.touchStatus()) || {}; } catch (e) { st = {}; }
1742
+ el.textContent = st.ready ? '已开启' : '未开启 · 点这里开';
1743
+ };
1744
+ void refreshAccessPreview();
1745
+ // 默认开启: 首次启动自动把用户带到系统开关页 (只引导一次, 不反复打扰)
1746
+ void (async () => {
1747
+ try {
1748
+ const cap = window.Capacitor;
1749
+ const b = cap && cap.Plugins && cap.Plugins.RokidBridge;
1750
+ if (!b) return;
1751
+ let st = {};
1752
+ try { st = (await b.touchStatus()) || {}; } catch (e) { return; }
1753
+ if (st.ready) return;
1754
+ if (localStorage.getItem(ACCESS_AUTO_KEY) === '1') return;
1755
+ localStorage.setItem(ACCESS_AUTO_KEY, '1');
1756
+ await b.openAccessibilitySettings();
1757
+ } catch (e) {}
1758
+ })();
1733
1759
  $('#settings-accessibility').addEventListener('click', async () => {
1734
1760
  const cap = window.Capacitor;
1735
1761
  const bridge = cap && cap.Plugins && cap.Plugins.RokidBridge;
1736
- if (!bridge) { alert('仅真机可用:App 内可把屏幕触控能力授权给电脑端'); return; }
1762
+ if (!bridge) { alert('完全访问权限只在手机 App 里有:电脑端 / 网页版没有这项。'); return; }
1737
1763
  let st = {};
1738
1764
  try { st = (await bridge.touchStatus()) || {}; } catch (e) { st = {}; }
1739
- if (st.ready) { alert('触控已就绪:电脑端可发 phone.tap / phone.swipe 操作这台手机'); return; }
1740
- try { await bridge.openAccessibilitySettings(); }
1741
- catch (e) { alert('请手动开启:设置 → 辅助功能 → 已安装的服务 → Bolloon Agent'); }
1765
+ if (st.ready) { alert('完全访问权限已开启:智能体可以在这台手机上点按、滑动屏幕(电脑端也能远程帮你点)。\n\n想关掉:系统设置 → 辅助功能 → 已开启的服务 → Bolloon Agent,关掉即可。'); return; }
1766
+ try {
1767
+ alert('马上打开系统设置,把「Bolloon Agent」的完全访问权限打开。\n(这一项在有些手机叫「辅助功能」,有些叫「无障碍」,指同一个开关;打开后一直有效,随时可以关掉。)');
1768
+ await bridge.openAccessibilitySettings();
1769
+ void refreshAccessPreview();
1770
+ }
1771
+ catch (e) { alert('没打开成功,请手动去:系统设置 → 辅助功能 → 已开启的服务 → Bolloon Agent,打开即可。'); }
1742
1772
  });
1743
1773
 
1744
1774
  // 本机数据: 直接读 IndexedDB 快照 (智能体/会话/消息都在本机, 重开 App 不会丢)
@@ -3001,3 +3031,672 @@
3001
3031
  document.addEventListener('DOMContentLoaded', init);
3002
3032
  if (document.readyState !== 'loading') init();
3003
3033
  })();
3034
+
3035
+ // ============ 联系方式与持久授权 (2026-09-19) ============
3036
+ // 独立 IIFE: 不改动既有逻辑, 只用 window.BolloonCore.contacts (编译进 mobile-core.js)。
3037
+ // 分工: 手机 = 输入/OTP/确认/展示待发内容/批准/签名授权; 桌面 = 落盘/执行/等待回复/证据。
3038
+ (function () {
3039
+ const $ = (sel) => document.querySelector(sel);
3040
+ const C = () => (window.BolloonCore && window.BolloonCore.contacts) || null;
3041
+ const esc = (v) => String(v == null ? '' : v).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
3042
+
3043
+ function openSheet() { const el = $('#contacts-sheet'); if (el) el.hidden = false; }
3044
+ function closeSheet() { const el = $('#contacts-sheet'); if (el) el.hidden = true; }
3045
+ function setText(id, text) { const el = $(id); if (el) el.textContent = text; }
3046
+
3047
+ let _lastChallenge = null;
3048
+
3049
+ async function refresh() {
3050
+ const c = C();
3051
+ setText('#contacts-effective', '加载中...');
3052
+ if (!c) { setText('#contacts-effective', '内核未就绪 (mobile-core 没加载)'); return; }
3053
+ setText('#contacts-signing', c.deviceSigning()
3054
+ ? '本机可做设备签名 (Ed25519) — 授权由这台手机签名后桌面才认'
3055
+ : '⚠ 本机 WebView 不支持 Ed25519 设备签名 — 无法在手机上授权, 请在桌面端授权');
3056
+ // 桌面回来先补同步排队中的授权
3057
+ try { const fl = await c.flushQueue(); if (fl && fl.sent) setText('#contacts-signing', `已补同步 ${fl.sent} 条手机授权到桌面`); } catch (e) {}
3058
+
3059
+ let card = null;
3060
+ try { card = await c.card(); } catch (e) { card = null; }
3061
+ if (!card) { setText('#contacts-effective', '读取失败'); return; }
3062
+ setText('#contacts-effective',
3063
+ `当前授权: ${card.effective}` + (card.deviceSigning ? '' : ' · 本机不能签名'));
3064
+ setText('#contacts-willnot', '不会获得权限: ' + (card.willNotGet || []).join(' · '));
3065
+
3066
+ // 联系方式 (只有脱敏值)
3067
+ const list = $('#contacts-list');
3068
+ if (list) {
3069
+ list.innerHTML = (card.contacts && card.contacts.length)
3070
+ ? card.contacts.map((x) => `<div class="sheet-text">${esc(x.displayValue)} <span style="color:var(--text-secondary)">[${esc(x.kind)}] ${esc(x.state)} · ${esc(x.label)}</span></div>`).join('')
3071
+ : '<div class="sheet-text" style="color:var(--text-secondary)">还没有绑定 — 在下面输入手机号或邮箱</div>';
3072
+ }
3073
+
3074
+ // 待批准 (展示待发内容 → 批准/拒绝)
3075
+ const ap = $('#contacts-approvals');
3076
+ if (ap) {
3077
+ ap.innerHTML = (card.approvals && card.approvals.length)
3078
+ ? `<div class="sheet-title" style="margin-top:10px">待你批准 (${card.approvals.length})</div>` + card.approvals.map((a) => `
3079
+ <div style="border:1px solid var(--border);border-radius:10px;padding:8px;margin-top:6px">
3080
+ <div class="sheet-text" style="font-weight:600">${esc(a.contactName)} · ${esc(a.channel)}${a.reallySent ? '' : ' · 本地落盘(未真实外发)'}</div>
3081
+ <div class="sheet-text" style="font-size:12px">${a.subject ? esc(a.subject) + ' — ' : ''}${esc(a.bodyPreview)}</div>
3082
+ <div class="sheet-text" style="font-size:11px;color:var(--text-secondary)">原因: ${esc(a.reason)}</div>
3083
+ <div style="display:flex;gap:8px;margin-top:6px">
3084
+ <button class="sheet-choice" data-approve="${esc(a.consentId)}" style="flex:1">批准并发送</button>
3085
+ <button class="sheet-choice sheet-cancel" data-reject="${esc(a.consentId)}" style="flex:1">拒绝</button>
3086
+ </div>
3087
+ </div>`).join('')
3088
+ : '<div class="sheet-text" style="color:var(--text-secondary);margin-top:10px">没有待批准的联系</div>';
3089
+ ap.querySelectorAll('[data-approve]').forEach((b) => b.addEventListener('click', async () => {
3090
+ const id = b.getAttribute('data-approve');
3091
+ const r = await C().decide(id, 'approve');
3092
+ alert(r && r.ok ? '已批准并发送' : '批准失败: ' + ((r && r.error) || '未知'));
3093
+ refresh();
3094
+ }));
3095
+ ap.querySelectorAll('[data-reject]').forEach((b) => b.addEventListener('click', async () => {
3096
+ const id = b.getAttribute('data-reject');
3097
+ const r = await C().decide(id, 'reject', { reason: '手机端拒绝' });
3098
+ alert(r && r.ok ? '已拒绝' : '拒绝失败: ' + ((r && r.error) || '未知'));
3099
+ refresh();
3100
+ }));
3101
+ }
3102
+
3103
+ // 三个授权选项
3104
+ const ch = $('#contacts-choices');
3105
+ if (ch && !ch.dataset.bound) {
3106
+ ch.dataset.bound = '1';
3107
+ ch.innerHTML = (card.choices || []).map((x) => `<button class="sheet-choice" data-choice="${esc(x.id)}">${esc(x.label)}</button>`).join('');
3108
+ ch.querySelectorAll('[data-choice]').forEach((b) => b.addEventListener('click', async () => {
3109
+ const choice = b.getAttribute('data-choice');
3110
+ const warn = choice === 'full_contact_access'
3111
+ ? '完全授权联系方式能力: 手机号/邮箱能力交给 Agent, 普通与敏感内容不再逐次确认 (密钥/支付/合同承诺仍然拒绝)。确认吗?'
3112
+ : choice === 'persistent' ? '长期使用: 以后 Agent 联系已验证的人不再打断你 (仍受频率/任务/证据/撤销约束)。确认吗?'
3113
+ : '仅本次任务: 只授权当前任务使用。确认吗?';
3114
+ if (window.confirm && !window.confirm(warn)) return;
3115
+ const r = await C().authorize(choice);
3116
+ if (r && r.ok) alert('已授权: 桌面已验签接受\n' + (r.note || ''));
3117
+ else if (r && r.queued) alert('桌面暂时不在线\n授权已保存在手机本地队列, 桌面一上线自动同步\n(' + (r.error || '') + ')');
3118
+ else alert('授权失败: ' + ((r && r.error) || '未知'));
3119
+ refresh();
3120
+ }));
3121
+ }
3122
+ }
3123
+
3124
+ function bindEvents() {
3125
+ const item = $('#item-contacts');
3126
+ if (item) item.addEventListener('click', () => { openSheet(); refresh(); });
3127
+ const close = $('#contacts-close');
3128
+ if (close) close.addEventListener('click', closeSheet);
3129
+
3130
+ const bind = $('#contacts-bind');
3131
+ if (bind) bind.addEventListener('click', async () => {
3132
+ const v = ($('#contacts-value') || {}).value || '';
3133
+ if (!v.trim()) { alert('先输入手机号或邮箱'); return; }
3134
+ const isEmail = /@/.test(v);
3135
+ const r = await C().bind(isEmail ? 'email' : 'phone', v.trim(), isEmail ? undefined : 'CN');
3136
+ if (!r || !r.ok) { alert('绑定失败: ' + ((r && r.error) || '未知')); return; }
3137
+ _lastChallenge = r.data;
3138
+ alert('已登记 ' + r.data.displayValue + '\n通道: ' + (r.data.channelLabel || '') +
3139
+ (r.data.otpForLocalSink ? '\n本地落盘验证码: ' + r.data.otpForLocalSink : '\n验证码已通过通道发出, 收到后填在下面'));
3140
+ refresh();
3141
+ });
3142
+
3143
+ const verify = $('#contacts-verify');
3144
+ if (verify) verify.addEventListener('click', async () => {
3145
+ const code = ($('#contacts-code') || {}).value || '';
3146
+ if (!_lastChallenge) { alert('先绑定一个手机号或邮箱'); return; }
3147
+ const r = await C().verify(_lastChallenge.contactId, _lastChallenge.challengeId, code.trim());
3148
+ if (!r || !r.ok) { alert('验证失败: ' + ((r && r.error) || '未知')); return; }
3149
+ alert('验证通过: ' + r.data.displayValue + '\n下一步: 选择下面的授权等级 (验证 ≠ 自动长期授权)');
3150
+ refresh();
3151
+ });
3152
+
3153
+ const revoke = $('#contacts-revoke-all');
3154
+ if (revoke) revoke.addEventListener('click', async () => {
3155
+ if (window.confirm && !window.confirm('撤销全部联系方式授权? 之后 Agent 不能再自动联系任何人 (历史证据保留)')) return;
3156
+ const card = await C().card();
3157
+ const grants = (card && card.grants) || [];
3158
+ if (!grants.length) { alert('当前没有生效中的授权'); return; }
3159
+ let done = 0, err = '';
3160
+ for (const g of grants) {
3161
+ if (g.status !== 'active') continue;
3162
+ const r = await C().revoke(g.grantId, '手机端收回');
3163
+ if (r && r.ok) done++; else err = (r && r.error) || '未知';
3164
+ }
3165
+ alert(done ? `已撤销 ${done} 条授权` : ('撤销未完成: ' + err));
3166
+ refresh();
3167
+ });
3168
+ }
3169
+
3170
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bindEvents);
3171
+ else bindEvents();
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
+ })();