@bolloon/bolloon-agent 0.6.5 → 0.6.6

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 (169) hide show
  1. package/dist/agents/chain/chain-settlement.js +0 -1
  2. package/dist/agents/chat-segmenter.js +4 -16
  3. package/dist/agents/contacts/human-directory.js +155 -0
  4. package/dist/agents/contacts/tools.js +79 -0
  5. package/dist/agents/experience-review.js +0 -3
  6. package/dist/agents/goal-flywheel/goal-change.js +3 -4
  7. package/dist/agents/goal-resume.js +0 -4
  8. package/dist/agents/intent-classifier.js +1 -1
  9. package/dist/agents/iteration-budget.js +12 -0
  10. package/dist/agents/knowledge-organizer.js +0 -2
  11. package/dist/agents/lesson-to-skill.js +0 -1
  12. package/dist/agents/loop-review.js +2 -2
  13. package/dist/agents/memory-store.js +359 -0
  14. package/dist/agents/opportunity-web-search.js +39 -0
  15. package/dist/agents/p2p-chat-tools.js +1 -2
  16. package/dist/agents/parse-tool-call.js +9 -22
  17. package/dist/agents/pi-harness.js +1 -2
  18. package/dist/agents/pi-sdk-tools.js +180 -5
  19. package/dist/agents/pi-sdk.js +143 -9
  20. package/dist/agents/process-runner.js +28 -9
  21. package/dist/agents/react-loop.js +9 -17
  22. package/dist/agents/runner-resolver.js +4 -3
  23. package/dist/agents/skill-loader.js +22 -15
  24. package/dist/agents/skills-manager.js +1 -25
  25. package/dist/agents/workflow-pivot-loop.js +152 -61
  26. package/dist/bootstrap/context-collector.js +2 -4
  27. package/dist/bootstrap/context-hierarchy.js +13 -1
  28. package/dist/cli/ink-app.js +44 -17
  29. package/dist/cli/intent-command.js +22 -12
  30. package/dist/cli/loading-tui.js +8 -2
  31. package/dist/cli/log-gate.js +40 -8
  32. package/dist/cli/mention-data.js +1 -0
  33. package/dist/cli/reply-hygiene.js +22 -0
  34. package/dist/cli/trace-line.js +61 -2
  35. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  36. package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -1
  37. package/dist/electron-build/agents/chain/chain-settlement.js +0 -1
  38. package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -1
  39. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  40. package/dist/electron-build/agents/contacts/human-directory.js +204 -0
  41. package/dist/electron-build/agents/contacts/human-directory.js.map +1 -0
  42. package/dist/electron-build/agents/contacts/policy.js.map +1 -1
  43. package/dist/electron-build/agents/contacts/tools.js +112 -0
  44. package/dist/electron-build/agents/contacts/tools.js.map +1 -1
  45. package/dist/electron-build/agents/experience-review.js +0 -3
  46. package/dist/electron-build/agents/experience-review.js.map +1 -1
  47. package/dist/electron-build/agents/external-events.js.map +1 -1
  48. package/dist/electron-build/agents/goal-flywheel/goal-change.js +3 -4
  49. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -1
  50. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -1
  51. package/dist/electron-build/agents/goal-resume.js +0 -4
  52. package/dist/electron-build/agents/goal-resume.js.map +1 -1
  53. package/dist/electron-build/agents/intent-classifier.js +1 -1
  54. package/dist/electron-build/agents/lesson-to-skill.js +0 -1
  55. package/dist/electron-build/agents/lesson-to-skill.js.map +1 -1
  56. package/dist/electron-build/agents/loop-review.js +2 -2
  57. package/dist/electron-build/agents/loop-review.js.map +1 -1
  58. package/dist/electron-build/agents/memory-recall.js.map +1 -1
  59. package/dist/electron-build/agents/memory-store.js +404 -0
  60. package/dist/electron-build/agents/memory-store.js.map +1 -0
  61. package/dist/electron-build/agents/parse-tool-call.js +9 -22
  62. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  63. package/dist/electron-build/agents/pi-harness.js +1 -2
  64. package/dist/electron-build/agents/pi-harness.js.map +1 -1
  65. package/dist/electron-build/agents/pi-sdk-tools.js +180 -5
  66. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  67. package/dist/electron-build/agents/pi-sdk.js +143 -9
  68. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  69. package/dist/electron-build/agents/pre-tool-validator.js.map +1 -1
  70. package/dist/electron-build/agents/process-runner.js +28 -9
  71. package/dist/electron-build/agents/process-runner.js.map +1 -1
  72. package/dist/electron-build/agents/runner-resolver.js +4 -3
  73. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  74. package/dist/electron-build/agents/skill-loader.js +22 -16
  75. package/dist/electron-build/agents/skill-loader.js.map +1 -1
  76. package/dist/electron-build/agents/skill-supervisor-link.js.map +1 -1
  77. package/dist/electron-build/agents/skills-manager.js +0 -24
  78. package/dist/electron-build/agents/skills-manager.js.map +1 -1
  79. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  80. package/dist/electron-build/agents/workflow-engine.js.map +1 -1
  81. package/dist/electron-build/agents/workflow-pivot-loop.js +155 -62
  82. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  83. package/dist/electron-build/agents/x402/goal-run-bridge.js.map +1 -1
  84. package/dist/electron-build/bollharness-integration/gate-state-machine.js.map +1 -1
  85. package/dist/electron-build/bootstrap/context-collector.js +2 -4
  86. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  87. package/dist/electron-build/bootstrap/context-hierarchy.js +13 -1
  88. package/dist/electron-build/bootstrap/context-hierarchy.js.map +1 -1
  89. package/dist/electron-build/electron-preload.js.map +1 -1
  90. package/dist/electron-build/external-engines/delegate.js.map +1 -1
  91. package/dist/electron-build/kernel/channel-actor.js +31 -2
  92. package/dist/electron-build/kernel/channel-actor.js.map +1 -1
  93. package/dist/electron-build/kernel/model-runtime.js +8 -1
  94. package/dist/electron-build/kernel/model-runtime.js.map +1 -1
  95. package/dist/electron-build/llm/config-store.js +20 -0
  96. package/dist/electron-build/llm/config-store.js.map +1 -1
  97. package/dist/electron-build/llm/embedding-adapter.js +75 -0
  98. package/dist/electron-build/llm/embedding-adapter.js.map +1 -0
  99. package/dist/electron-build/llm/pi-ai.js +96 -35
  100. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  101. package/dist/electron-build/network/agent-network.js.map +1 -1
  102. package/dist/electron-build/orbitdb/memory-sync.js +76 -0
  103. package/dist/electron-build/orbitdb/memory-sync.js.map +1 -0
  104. package/dist/electron-build/pi-ecosystem-judgment/adaptive-scan.js.map +1 -1
  105. package/dist/electron-build/security/tool-gate.js +2 -0
  106. package/dist/electron-build/security/tool-gate.js.map +1 -1
  107. package/dist/electron-build/setup/setup-store.js +34 -6
  108. package/dist/electron-build/setup/setup-store.js.map +1 -1
  109. package/dist/electron-build/social/heartbeat.js +18 -5
  110. package/dist/electron-build/social/heartbeat.js.map +1 -1
  111. package/dist/electron-build/utils/update-manager.js.map +1 -1
  112. package/dist/index.js +250 -12
  113. package/dist/ios/bolloon-web.json +2 -2
  114. package/dist/ios/client.js +74 -35
  115. package/dist/ios/icons/icon-1400x1400.png +0 -0
  116. package/dist/ios/icons/icon-1400x1400.webp +0 -0
  117. package/dist/ios/index.html +106 -107
  118. package/dist/ios/mobile-agent.js +346 -45
  119. package/dist/ios/mobile-core.js +7303 -4590
  120. package/dist/ios/mobile-harness.js +1004 -0
  121. package/dist/ios/mobile-orbitdb.js +194 -0
  122. package/dist/ios/mobile-persona.js +80 -0
  123. package/dist/ios/mobile-privacy.js +2 -3
  124. package/dist/ios/mobile.css +286 -21
  125. package/dist/ios/mobile.html +106 -107
  126. package/dist/ios/mobile.js +300 -132
  127. package/dist/ios/routes-world.js +4 -1
  128. package/dist/ios/server.js +3 -1
  129. package/dist/ios/style.css +13 -5
  130. package/dist/ios/ui/message-renderer.js +4 -18
  131. package/dist/ios/ui/step-timeline.js +14 -1
  132. package/dist/kernel/channel-actor.js +30 -2
  133. package/dist/kernel/gate-scan.js +1 -1
  134. package/dist/kernel/index.js +33 -0
  135. package/dist/kernel/model-runtime.js +8 -1
  136. package/dist/kernel/plan.js +3 -3
  137. package/dist/kernel/roster.js +3 -3
  138. package/dist/llm/config-store.js +20 -0
  139. package/dist/llm/embedding-adapter.js +69 -0
  140. package/dist/llm/pi-ai.js +94 -34
  141. package/dist/orbitdb/memory-sync.js +70 -0
  142. package/dist/pi-ecosystem-judgment/adaptive-scan.js +1 -1
  143. package/dist/security/tool-gate.js +2 -0
  144. package/dist/setup/setup-store.js +34 -6
  145. package/dist/social/heartbeat.js +18 -5
  146. package/dist/web/bolloon-web.json +3 -3
  147. package/dist/web/client.js +74 -35
  148. package/dist/web/icons/icon-1400x1400.png +0 -0
  149. package/dist/web/icons/icon-1400x1400.webp +0 -0
  150. package/dist/web/index.html +37 -25
  151. package/dist/web/mobile-agent.js +351 -45
  152. package/dist/web/mobile-contacts.js +0 -4
  153. package/dist/web/mobile-core.js +7430 -4590
  154. package/dist/web/mobile-harness.js +1004 -0
  155. package/dist/web/mobile-orbitdb.js +194 -0
  156. package/dist/web/mobile-persona.js +80 -0
  157. package/dist/web/mobile-privacy.js +2 -3
  158. package/dist/web/mobile.css +286 -21
  159. package/dist/web/mobile.html +121 -107
  160. package/dist/web/mobile.js +409 -136
  161. package/dist/web/routes-contacts.js +94 -1
  162. package/dist/web/routes-world.js +4 -1
  163. package/dist/web/server.js +4 -2
  164. package/dist/web/style.css +13 -5
  165. package/dist/web/sw.js +1 -1
  166. package/dist/web/ui/message-renderer.js +4 -18
  167. package/dist/web/ui/step-timeline.js +14 -1
  168. package/package.json +3 -2
  169. package/scripts/build-web.ts +16 -1
@@ -69,21 +69,66 @@
69
69
  });
70
70
  } catch (e) {}
71
71
 
72
- const TITLES = { main: '首页', friends: '好友', network: '网络', tasks: '任务', me: '我' };
72
+ const TITLES = { main: '首页', friends: '好友', tasks: '任务', me: '我' };
73
73
  let currentTab = 'main';
74
74
  function switchTab(tab) {
75
75
  currentTab = tab;
76
76
  $$('.page, .page-container').forEach((p) => { p.hidden = p.dataset.tab !== tab; });
77
77
  $$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tab));
78
78
  $('#topbar-title').textContent = TITLES[tab] || '会话';
79
- const cs = $('#btn-create-session'); if (cs) cs.hidden = tab !== 'main';
80
79
  const ta = $('#topbar-actions'); if (ta) ta.hidden = tab === 'me'; // 我 页不显示 加号/刷新
81
80
  if (tab === 'friends') { loadContacts(); loadP2PStatus(); }
82
- if (tab === 'network') { loadAgentControl(); loadApprovals(); loadNetMembers(); loadAgentServices(); loadX402Info(); }
81
+ // 2026-10-06: 网络 + 任务合一 —— 任务 tab 同时加载网络与任务数据
82
+ if (tab === 'tasks') { loadAgentControl(); loadApprovals(); loadNetMembers(); loadAgentServices(); loadX402Info(); try { refreshAll(); } catch (e) {} }
83
83
  if (tab === 'main') { loadAgentCovers(); }
84
84
  window.__mobileTouch?.('tab', tab);
85
85
  }
86
86
  $$('.tab').forEach((t) => t.addEventListener('click', () => switchTab(t.dataset.tab)));
87
+ // 2026-10-07: 任务 tab 对话框模式 — 网络收成仪表盘工具 (覆盖层开关)
88
+ function openTaskNetwork() {
89
+ const p = $('#task-network-panel'); if (p) p.hidden = false;
90
+ // 打开时加载网络数据
91
+ try { loadAgentControl(); loadApprovals(); loadNetMembers(); loadAgentServices(); loadX402Info(); } catch (e) {}
92
+ }
93
+ function closeTaskNetwork() {
94
+ const p = $('#task-network-panel'); if (p) p.hidden = true;
95
+ }
96
+ const dashNet = $('#task-dash-network'); if (dashNet) dashNet.addEventListener('click', openTaskNetwork);
97
+ const netBack = $('#task-network-back'); if (netBack) netBack.addEventListener('click', closeTaskNetwork);
98
+
99
+ // 任务对话框: 派活 → runLocalAgent (手机自治循环, 回显到消息流)
100
+ async function sendTask() {
101
+ const input = $('#task-input');
102
+ if (!input) return;
103
+ const text = input.value.trim();
104
+ if (!text) return;
105
+ input.value = '';
106
+ appendTaskMsg('user', text);
107
+ const empty = $('#task-chat-empty'); if (empty) empty.hidden = true;
108
+ try {
109
+ const a = window.BolloonCore && window.BolloonCore.agent;
110
+ let reply;
111
+ if (a && a.runLocal) reply = await a.runLocal(text);
112
+ else if (a && a.runLoop) reply = await a.runLoop(text);
113
+ else reply = '手机 Agent 未就绪';
114
+ appendTaskMsg('ai', String(reply || '(无回复)'));
115
+ } catch (e) {
116
+ appendTaskMsg('ai', '[执行失败] ' + (e.message || e));
117
+ }
118
+ }
119
+ function appendTaskMsg(role, text) {
120
+ const chat = $('#task-chat');
121
+ if (!chat) return;
122
+ const el = document.createElement('div');
123
+ el.className = 'task-msg ' + (role === 'user' ? 'task-msg-user' : 'task-msg-ai');
124
+ el.textContent = text;
125
+ chat.appendChild(el);
126
+ chat.scrollTop = chat.scrollHeight;
127
+ }
128
+ const taskInput = $('#task-input');
129
+ const taskSend = $('#task-input-send');
130
+ if (taskSend) taskSend.addEventListener('click', sendTask);
131
+ if (taskInput) taskInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendTask(); });
87
132
 
88
133
  async function loadConversations() {
89
134
  try {
@@ -114,26 +159,58 @@
114
159
  }
115
160
 
116
161
  async function loadContacts() {
117
- try {
118
- let peers = [];
119
- try { peers = await api.get('/api/peers'); } catch { peers = []; }
120
- const list = $('#contacts-list');
121
- if (!list) return;
122
- list.innerHTML = '';
123
- if (!Array.isArray(peers) || peers.length === 0) {
124
- list.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">暂无好友</div>';
125
- return;
126
- }
127
- peers.forEach((p) => {
128
- const name = p.name || p.publicKey?.slice(0, 12) || '好友';
129
- const el = document.createElement('div');
130
- el.className = 'conv-item';
131
- el.innerHTML = `<div class="conv-avatar">${escapeHtml(name.charAt(0))}</div>
132
- <div class="conv-body"><div class="conv-name">${escapeHtml(name)}</div></div>`;
133
- list.appendChild(el);
134
- });
135
- } catch (e) { /* 忽略 */ }
136
- }
162
+ try {
163
+ let peers = [];
164
+ try { peers = await api.get('/api/peers'); } catch { peers = []; }
165
+ const list = $('#contacts-list');
166
+ if (!list) return;
167
+ list.innerHTML = '';
168
+ if (!Array.isArray(peers) || peers.length === 0) {
169
+ list.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">暂无好友</div>';
170
+ const idx = $('#contacts-index'); if (idx) idx.innerHTML = '';
171
+ return;
172
+ }
173
+ // 通讯录式: 按首字母分组 (2026-10-07, 微信通讯录风格)
174
+ const groups = {};
175
+ peers.forEach((p) => {
176
+ const name = p.name || p.publicKey?.slice(0, 12) || '好友';
177
+ const letter = (name.charAt(0) || '#').toUpperCase();
178
+ const g = /[A-Z]/.test(letter) ? letter : '#';
179
+ if (!groups[g]) groups[g] = [];
180
+ groups[g].push({ name, peer: p });
181
+ });
182
+ const letters = Object.keys(groups).sort((a, b) => (a === '#' ? 1 : b === '#' ? -1 : a.localeCompare(b)));
183
+ letters.forEach((letter) => {
184
+ const groupEl = document.createElement('div');
185
+ groupEl.className = 'contacts-group';
186
+ groupEl.setAttribute('data-letter', letter);
187
+ const head = document.createElement('div');
188
+ head.className = 'contacts-group-head';
189
+ head.textContent = letter;
190
+ groupEl.appendChild(head);
191
+ groups[letter].forEach((c) => {
192
+ const el = document.createElement('div');
193
+ el.className = 'conv-item contact-row';
194
+ el.innerHTML = `<div class="conv-avatar contact-avatar">${escapeHtml(c.name.charAt(0))}</div>
195
+ <div class="conv-body"><div class="conv-name">${escapeHtml(c.name)}</div>
196
+ <div class="contact-sub">${c.peer.online ? '在线' : '离线'}${c.peer.publicKey ? ' · ' + escapeHtml(String(c.peer.publicKey).slice(0, 10)) + '…' : ''}</div></div>`;
197
+ groupEl.appendChild(el);
198
+ });
199
+ list.appendChild(groupEl);
200
+ });
201
+ // 右侧字母索引条
202
+ const idx = $('#contacts-index');
203
+ if (idx) {
204
+ idx.innerHTML = letters.map((l) => `<div class="contacts-index-letter" data-letter="${l}">${l === '#' ? '#' : l}</div>`).join('');
205
+ idx.querySelectorAll('.contacts-index-letter').forEach((el) => {
206
+ el.addEventListener('click', () => {
207
+ const target = list.querySelector(`.contacts-group[data-letter="${el.dataset.letter}"]`);
208
+ if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
209
+ });
210
+ });
211
+ }
212
+ } catch (e) { /* 忽略 */ }
213
+ }
137
214
 
138
215
  // 智能体控制 = MCP 控制 + Skills 控制 (不是屏幕触控!)
139
216
  // - MCP 工具: 真正的工具调用 (gateway_status / join / register …)
@@ -515,16 +592,20 @@
515
592
  const cards = $('#world-feed-cards');
516
593
  if (!feed || !cards) return;
517
594
  try {
518
- const res = await fetch('/api/opportunities?min-score=0.2&limit=10');
519
- if (!res.ok) return;
520
- const data = await res.json();
521
- const opps = (data.opportunities || []).slice(0, 6);
595
+ // 2026-10-05 (leo: 手机端 ↔ 电脑端互联): 走 api 层 → BolloonCore.resolve
596
+ // 转发电脑端 desktopUrl (不再裸 fetch 相对路径 — 手机上相对路径连手机自己, 永远空)
597
+ const data = await api.get('/api/opportunities?min-score=0.2&limit=10');
598
+ const opps = ((data && data.opportunities) || []).slice(0, 6);
522
599
  if (!opps.length) { feed.hidden = true; return; }
523
600
  feed.hidden = false;
524
601
  worldFeedShown = true;
525
602
  cards.innerHTML = opps.map((o) => {
526
603
  const pct = Math.round(o.score * 100);
527
- const tag = o.reason === 'match' ? '与你匹配' : '世界变化';
604
+ // 2026-10-05: 来源标签 (与电脑端同口径) — ✉ 外部投递 / 🔎 AI 搜索 / ○ 世界变化
605
+ let tag = '世界变化';
606
+ if (o.reason === 'inbox') tag = '✉ 外部投递';
607
+ else if (o.reason === 'search') tag = '🔎 AI 搜索';
608
+ else if (o.reason === 'match') tag = '与你匹配';
528
609
  return `<div class="world-feed-card">
529
610
  <div class="world-feed-card-top">
530
611
  <span class="world-feed-title">${escapeHtml(String(o.title || '').slice(0, 34))}</span>
@@ -541,7 +622,7 @@
541
622
  btn.addEventListener('click', async () => {
542
623
  const id = btn.dataset.wfId;
543
624
  const action = btn.dataset.wfAction;
544
- try { await fetch('/api/opportunities/' + id + '/' + action, { method: 'POST' }); } catch (e) {}
625
+ try { await api.post('/api/opportunities/' + id + '/' + action, {}); } catch (e) {}
545
626
  const card = btn.closest('.world-feed-card');
546
627
  if (card) {
547
628
  card.style.opacity = '0.3';
@@ -554,12 +635,41 @@
554
635
  feed.hidden = true; // 拉不到就不显示世界区 (不阻塞首页)
555
636
  }
556
637
  // 持续流入: 世界区可见时每 45s 自动刷新 (AI 持续观察, 不是人刷新)
557
- if (feed && !feed.hidden && worldFeedShown && !worldFeedTimer) {
558
- worldFeedTimer = setInterval(() => { loadWorldFeed(); }, 45000);
559
- }
560
- }
638
+ if (feed && !feed.hidden && worldFeedShown && !worldFeedTimer) {
639
+ worldFeedTimer = setInterval(() => { loadWorldFeed(); }, 45000);
640
+ }
641
+ }
561
642
 
562
- // === 卡片封面 (docs/fig 导出 → src/web/covers, 每个 agent 唯一不重复) ===
643
+ // === 2026-10-07: 手机首页底部输入框 — 告诉世界你在做什么 (对应桌面世界模式) ===
644
+ async function tellWorld() {
645
+ const input = $('#world-input');
646
+ if (!input) return;
647
+ const text = input.value.trim();
648
+ if (!text) return;
649
+ input.value = '';
650
+ try {
651
+ // 声明意图 (与桌面同 API; 桌面不可达时走 BolloonCore.resolve 转发)
652
+ const res = await api.post('/api/intents', { text, priority: 3 });
653
+ if (!res || !res.ok) console.info('[world] 声明意图失败');
654
+ // 输入即主动搜索 (leo: 声明后立刻搜机会)
655
+ try { await api.post('/api/world/search', { topic: text }); } catch (e) {}
656
+ // 刷新机会流
657
+ await loadWorldFeed();
658
+ if (window.showToast) showToast('🌍 已告诉世界,正在为你匹配机会…');
659
+ } catch (e) {
660
+ console.info('[world] 声明失败', e);
661
+ }
662
+ }
663
+ function bindWorldInput() {
664
+ const input = $('#world-input');
665
+ const send = $('#world-input-send');
666
+ if (input) input.addEventListener('keydown', (e) => { if (e.key === 'Enter') tellWorld(); });
667
+ if (send) send.addEventListener('click', tellWorld);
668
+ }
669
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bindWorldInput);
670
+ else bindWorldInput();
671
+
672
+ // === 卡片封面 (docs/fig 导出 → src/web/covers, 每个 agent 唯一不重复) ===
563
673
  let _coverList = null;
564
674
  async function loadCovers() {
565
675
  if (_coverList) return _coverList;
@@ -1935,11 +2045,11 @@
1935
2045
  <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>
1936
2046
  <div class="conv-item" id="settings-desktop"><span class="list-icon">${ICONS.globe}</span><span>电脑端同步</span><span class="list-arrow">›</span></div>
1937
2047
  <div class="conv-item" id="settings-chain"><span class="list-icon">${ICONS.chip}</span><span>链上配置 (RPC/网络)</span><span class="list-arrow">›</span></div>
1938
- <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>
1939
2048
  <div class="conv-item" id="settings-ipfs"><span class="list-icon">${ICONS.chip}</span><span>IPFS 存储</span><span class="list-arrow">›</span></div>
1940
2049
  <div class="conv-item" id="settings-helia"><span class="list-icon">${ICONS.globe}</span><span>本机 IPFS 节点</span><span class="list-arrow">›</span></div>
1941
2050
  <div class="conv-item" id="settings-selfcard"><span class="list-icon">${ICONS.chip}</span><span id="selfcard-text">显示本机卡片: 开</span></div>
1942
- <div class="conv-item" id="settings-did"><span class="list-icon">${ICONS.idcard}</span><span>DID</span></div>
2051
+ <div class="conv-item" id="settings-did"><span class="list-icon">${ICONS.idcard}</span><span>DID 与身份</span><span class="list-arrow">›</span></div>
2052
+ <div class="conv-item" id="settings-persona"><span class="list-icon">${ICONS.chip}</span><span>智能体性格</span><span class="list-arrow">›</span></div>
1943
2053
  <div class="conv-item" id="settings-privacy"><span class="list-icon">${ICONS.chip}</span><span>隐私政策与个人信息</span><span class="list-arrow">›</span></div>
1944
2054
  <div class="conv-item" id="settings-wipe"><span class="list-icon">${ICONS.trash}</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>
1945
2055
  <div class="conv-item" id="settings-filing"><span class="list-icon">${ICONS.idcard}</span><span id="filing-text">APP 备案号:备案办理中</span></div>
@@ -1968,48 +2078,6 @@
1968
2078
  const next = currentTheme === 'auto' ? 'light' : (currentTheme === 'light' ? 'dark' : 'auto');
1969
2079
  applyTheme(next, true);
1970
2080
  });
1971
- // 完全访问权限 (屏幕点按/滑动, 电脑端 phone.tap / phone.swipe 的前提) — 默认开启
1972
- const ACCESS_AUTO_KEY = 'bolloon_access_auto_guided';
1973
- const refreshAccessPreview = async () => {
1974
- const el = document.querySelector('#access-preview');
1975
- if (!el) return;
1976
- const cap = window.Capacitor;
1977
- const b = cap && cap.Plugins && cap.Plugins.RokidBridge;
1978
- if (!b) { el.textContent = '仅手机 App'; return; }
1979
- let st = {};
1980
- try { st = (await b.touchStatus()) || {}; } catch (e) { st = {}; }
1981
- el.textContent = st.ready ? '已开启' : '未开启 · 点这里开';
1982
- };
1983
- void refreshAccessPreview();
1984
- // 默认开启: 首次启动自动把用户带到系统开关页 (只引导一次, 不反复打扰)
1985
- void (async () => {
1986
- try {
1987
- const cap = window.Capacitor;
1988
- const b = cap && cap.Plugins && cap.Plugins.RokidBridge;
1989
- if (!b) return;
1990
- let st = {};
1991
- try { st = (await b.touchStatus()) || {}; } catch (e) { return; }
1992
- if (st.ready) return;
1993
- if (localStorage.getItem(ACCESS_AUTO_KEY) === '1') return;
1994
- localStorage.setItem(ACCESS_AUTO_KEY, '1');
1995
- await b.openAccessibilitySettings();
1996
- } catch (e) {}
1997
- })();
1998
- $('#settings-accessibility').addEventListener('click', async () => {
1999
- const cap = window.Capacitor;
2000
- const bridge = cap && cap.Plugins && cap.Plugins.RokidBridge;
2001
- if (!bridge) { alert('完全访问权限只在手机 App 里有:电脑端 / 网页版没有这项。'); return; }
2002
- let st = {};
2003
- try { st = (await bridge.touchStatus()) || {}; } catch (e) { st = {}; }
2004
- if (st.ready) { alert('完全访问权限已开启:智能体可以在这台手机上点按、滑动屏幕(电脑端也能远程帮你点)。\n\n想关掉:系统设置 → 辅助功能 → 已开启的服务 → Bolloon Agent,关掉即可。'); return; }
2005
- try {
2006
- alert('马上打开系统设置,把「Bolloon Agent」的完全访问权限打开。\n(这一项在有些手机叫「辅助功能」,有些叫「无障碍」,指同一个开关;打开后一直有效,随时可以关掉。)');
2007
- await bridge.openAccessibilitySettings();
2008
- void refreshAccessPreview();
2009
- }
2010
- catch (e) { alert('没打开成功,请手动去:系统设置 → 辅助功能 → 已开启的服务 → Bolloon Agent,打开即可。'); }
2011
- });
2012
-
2013
2081
  // 本机数据: 直接读 IndexedDB 快照 (智能体/会话/消息都在本机, 重开 App 不会丢)
2014
2082
  const dataEl = $('#settings-data');
2015
2083
  if (dataEl) {
@@ -2063,7 +2131,121 @@
2063
2131
  loadAgentCovers();
2064
2132
  showToast(hidden ? '已显示本机卡片' : '已隐藏本机卡片');
2065
2133
  });
2066
- $('#settings-did').addEventListener('click', () => { api.get('/api/auth/status').then((s) => alert('DID: ' + (s.did || '未生成'))); });
2134
+ $('#settings-did').addEventListener('click', () => void openIdentityPage());
2135
+ $('#settings-persona').addEventListener('click', () => void openPersonaPage());
2136
+ }
2137
+
2138
+ // === 身份页 (DID / 公钥 / 签名测试 / 邮箱 / 手机号) ===
2139
+ // 2026-10-07: 完整身份系统 — DIAP did:key (与桌面同构) + 附带条件 (邮箱/手机号/备注)
2140
+ async function openIdentityPage() {
2141
+ if ($('#identity-page')) return;
2142
+ const page = mkOverlayPage('identity-page', 'DID 与身份', '<div style="font-size:12px;color:var(--text-muted)">读取中…</div>');
2143
+ const body = page.querySelector('#identity-page-body');
2144
+ try {
2145
+ const core = window.BolloonCore || (await import('./mobile-core.js')).core;
2146
+ const ag = core && core.agent;
2147
+ const st = await ag.identityStatus();
2148
+ body.innerHTML = `
2149
+ <div class="list">
2150
+ <div class="list-item"><span class="list-icon">${ICONS.idcard}</span><span style="flex:1"><span style="display:block">DID</span><span class="conv-preview" style="display:block;word-break:break-all">${escapeHtml(st.did || '未生成')}</span></span></div>
2151
+ <div class="list-item"><span class="list-icon">${ICONS.chip}</span><span style="flex:1"><span style="display:block">公钥 (Ed25519)</span><span class="conv-preview" style="display:block;word-break:break-all">${st.publicKey ? escapeHtml(st.publicKey.slice(0, 32) + '…') : '(老设备哈希身份,无签名)'}</span></span></div>
2152
+ <div class="list-item"><span class="list-icon">${ICONS.chip}</span><span style="flex:1"><span style="display:block">名字</span><span class="conv-preview" style="display:block">${escapeHtml(st.name || '未登录')}</span></span></div>
2153
+ </div>
2154
+ <div class="section-label">附带身份条件(仅存本机)</div>
2155
+ <div class="list">
2156
+ <div class="list-item"><span class="list-icon">✉️</span><span style="flex:1"><span style="display:block">邮箱</span><span class="conv-preview" id="id-email" style="display:block">${escapeHtml(st.email || '未设置')}</span></span></div>
2157
+ <div class="list-item"><span class="list-icon">📱</span><span style="flex:1"><span style="display:block">手机号</span><span class="conv-preview" id="id-phone" style="display:block">${escapeHtml(st.phone || '未设置')}</span></span></div>
2158
+ <div class="list-item"><span class="list-icon">📝</span><span style="flex:1"><span style="display:block">备注</span><span class="conv-preview" id="id-note" style="display:block">${escapeHtml(st.note || '未设置')}</span></span></div>
2159
+ <button class="sheet-choice" id="id-edit" style="width:100%;margin-top:8px">编辑邮箱 / 手机号 / 备注</button>
2160
+ </div>
2161
+ <div class="section-label">签名能力(DIAP Ed25519)</div>
2162
+ <div class="list">
2163
+ <button class="sheet-choice" id="id-sign-test" style="width:100%">测试签名(本地签 + 验,证明身份可签名)</button>
2164
+ </div>`;
2165
+ const editBtn = page.querySelector('#id-edit');
2166
+ if (editBtn) editBtn.addEventListener('click', () => {
2167
+ const email = prompt('邮箱:', st.email || '');
2168
+ if (email === null) return;
2169
+ const phone = prompt('手机号:', st.phone || '');
2170
+ if (phone === null) return;
2171
+ const note = prompt('备注:', st.note || '');
2172
+ if (note === null) return;
2173
+ void (async () => {
2174
+ const updated = await ag.updateProfile({ email, phone, note });
2175
+ const e = page.querySelector('#id-email'); if (e) e.textContent = updated.email || '未设置';
2176
+ const p = page.querySelector('#id-phone'); if (p) p.textContent = updated.phone || '未设置';
2177
+ const n = page.querySelector('#id-note'); if (n) n.textContent = updated.note || '未设置';
2178
+ showToast('✅ 身份条件已更新');
2179
+ })();
2180
+ });
2181
+ const signBtn = page.querySelector('#id-sign-test');
2182
+ if (signBtn) signBtn.addEventListener('click', () => {
2183
+ void (async () => {
2184
+ signBtn.textContent = '签名中…';
2185
+ try {
2186
+ const s = await ag.sign('bolloon-mobile-sign-test');
2187
+ const ok = await ag.verifySign('bolloon-mobile-sign-test', s.signature, s.publicKey);
2188
+ signBtn.textContent = ok ? `✅ 签名+验签通过 (${s.did.slice(0, 16)}…)` : '❌ 验签失败';
2189
+ } catch (e) {
2190
+ signBtn.textContent = '签名不可用: ' + String((e && e.message) || e).slice(0, 40);
2191
+ }
2192
+ })();
2193
+ });
2194
+ } catch (e) {
2195
+ body.innerHTML = '<div style="padding:20px;color:var(--text-muted)">身份读取失败: ' + escapeHtml(String((e && e.message) || e)) + '</div>';
2196
+ }
2197
+ }
2198
+
2199
+ // === 智能体性格页 (Personal AI) ===
2200
+ // 2026-10-07: mobile-persona 配置文件 — 性格/价值观/兴趣/说话方式, 注入 agent 系统提示
2201
+ async function openPersonaPage() {
2202
+ if ($('#persona-page')) return;
2203
+ const page = mkOverlayPage('persona-page', '智能体性格', '<div style="font-size:12px;color:var(--text-muted)">读取中…</div>');
2204
+ const body = page.querySelector('#persona-page-body');
2205
+ try {
2206
+ const persona = await import('./mobile-persona.js');
2207
+ const p = persona.loadPersona();
2208
+ body.innerHTML = `
2209
+ <div style="padding:14px;font-size:12px;color:var(--text-muted);line-height:1.7">配置手机智能体的「性格文件」—— 会注入到每次对话的系统提示里,决定它怎么说话、看重什么、不做什么。存本机。</div>
2210
+ <div class="section-label">名字</div>
2211
+ <div style="padding:0 14px"><input id="ps-name" value="${escapeHtml(p.name)}" style="width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" /></div>
2212
+ <div class="section-label">性格</div>
2213
+ <div style="padding:0 14px"><input id="ps-personality" value="${escapeHtml(p.personality)}" placeholder="例如:活泼、严谨、毒舌…" style="width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" /></div>
2214
+ <div class="section-label">说话方式</div>
2215
+ <div style="padding:0 14px"><input id="ps-style" value="${escapeHtml(p.style)}" placeholder="例如:简洁中文,先结论后细节" style="width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" /></div>
2216
+ <div class="section-label">价值观(逗号分隔)</div>
2217
+ <div style="padding:0 14px"><input id="ps-values" value="${escapeHtml(p.values.join('、'))}" placeholder="本地优先、隐私优先…" style="width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" /></div>
2218
+ <div class="section-label">兴趣(逗号分隔)</div>
2219
+ <div style="padding:0 14px"><input id="ps-interests" value="${escapeHtml(p.interests.join('、'))}" placeholder="P2P、AI、知识系统…" style="width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" /></div>
2220
+ <div class="section-label">不做的事(逗号分隔)</div>
2221
+ <div style="padding:0 14px"><input id="ps-boundaries" value="${escapeHtml(p.boundaries.join('、'))}" placeholder="不编造数据、不假装能做…" style="width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" /></div>
2222
+ <div style="padding:14px">
2223
+ <button class="sheet-choice" id="ps-save" style="width:100%">保存性格</button>
2224
+ <button class="sheet-choice sheet-cancel" id="ps-reset" style="width:100%;margin-top:8px">恢复默认</button>
2225
+ </div>`;
2226
+ const saveBtn = page.querySelector('#ps-save');
2227
+ if (saveBtn) saveBtn.addEventListener('click', () => {
2228
+ const v = (id) => { const el = page.querySelector(id); return el ? String(el.value || '').trim() : ''; };
2229
+ persona.savePersona({
2230
+ name: v('#ps-name'),
2231
+ personality: v('#ps-personality'),
2232
+ style: v('#ps-style'),
2233
+ values: v('#ps-values').split(/[、,]/).map((s) => s.trim()).filter(Boolean),
2234
+ interests: v('#ps-interests').split(/[、,]/).map((s) => s.trim()).filter(Boolean),
2235
+ boundaries: v('#ps-boundaries').split(/[、,]/).map((s) => s.trim()).filter(Boolean),
2236
+ });
2237
+ showToast('✅ 性格已保存,下次对话生效');
2238
+ });
2239
+ const resetBtn = page.querySelector('#ps-reset');
2240
+ if (resetBtn) resetBtn.addEventListener('click', () => {
2241
+ persona.resetPersona();
2242
+ showToast('已恢复默认性格');
2243
+ page.remove();
2244
+ void openPersonaPage();
2245
+ });
2246
+ } catch (e) {
2247
+ body.innerHTML = '<div style="padding:20px;color:var(--text-muted)">性格读取失败: ' + escapeHtml(String((e && e.message) || e)) + '</div>';
2248
+ }
2067
2249
  }
2068
2250
 
2069
2251
  // === 模型配置 (供应商 / 模型 / 地址) ===
@@ -2525,23 +2707,10 @@
2525
2707
  }
2526
2708
  }
2527
2709
 
2528
- // 创建智能体: 无输入框, 底部滑入加载 sheet, 完成后滑出
2529
- async function createSession() {
2530
- showSheet('#create-sheet');
2531
- try {
2532
- await api.post('/api/channels/create', {});
2533
- await new Promise((r) => setTimeout(r, 700));
2534
- switchTab('main');
2535
- loadAgentCovers();
2536
- } catch (e) {
2537
- alert('创建失败: ' + (e.message || e));
2538
- } finally {
2539
- hideSheet('#create-sheet');
2540
- }
2541
- }
2710
+ // 创建智能体: 已由首页世界流替代 (2026-10-07: createSession 无调用, 删除)
2542
2711
 
2543
- // 添加好友: 弹出选择 sheet (扫码 / 手动)
2544
- function addFriend() { showSheet('#addfriend-sheet'); }
2712
+ // 添加好友: 弹出选择 sheet (扫码 / 手动)
2713
+ function addFriend() { showSheet('#addfriend-sheet'); }
2545
2714
  async function addFriendManual() {
2546
2715
  hideSheet('#addfriend-sheet');
2547
2716
  const addr = prompt('输入好友地址 (multiaddr, 如 /ip4/10.0.2.2/tcp/54188/ws)', '');
@@ -2549,7 +2718,7 @@
2549
2718
  try {
2550
2719
  const r = await api.post('/api/peers/add', { addr });
2551
2720
  alert(r && r.ok ? (r.connected ? '已连接好友' : '已记录好友地址, 连接中...') : ((r && r.error) || '添加失败'));
2552
- if (currentTab === 'network') loadContacts();
2721
+ if (currentTab === 'network' || currentTab === 'tasks') loadContacts();
2553
2722
  } catch (e) {
2554
2723
  alert('添加失败: ' + (e.message || e));
2555
2724
  }
@@ -2564,6 +2733,52 @@
2564
2733
  inp.click();
2565
2734
  }
2566
2735
 
2736
+ // === 手机号搜索添加好友 (2026-10-15, 手机原生手机号功能) ===
2737
+ // 目录数据在电脑端 (human-directory.json), 手机端只转发; 电脑端不可达 → 明确报错不假装。
2738
+ function openPhoneSearch() {
2739
+ hideSheet('#addfriend-sheet');
2740
+ showSheet('#phone-search-sheet');
2741
+ const inp = $('#phone-search-input');
2742
+ if (inp) { inp.value = ''; inp.focus(); }
2743
+ const result = $('#phone-search-result');
2744
+ if (result) { result.style.display = 'none'; result.innerHTML = ''; }
2745
+ }
2746
+ async function doPhoneSearch() {
2747
+ const inp = $('#phone-search-input');
2748
+ const result = $('#phone-search-result');
2749
+ if (!inp || !result) return;
2750
+ const phone = (inp.value || '').replace(/[^\d+]/g, '');
2751
+ if (!/^\+?\d{8,15}$/.test(phone)) { result.style.display = 'block'; result.innerHTML = '<div style="color:var(--danger,#e05555);font-size:13px">请输入正确的手机号 (8-15 位数字)</div>'; return; }
2752
+ result.style.display = 'block';
2753
+ result.innerHTML = '<div style="color:var(--text-secondary);font-size:13px">搜索中…</div>';
2754
+ try {
2755
+ const r = await api.post('/api/phone/directory/lookup', { phone });
2756
+ if (r && r.found) {
2757
+ const name = r.displayName || String(r.did || '').slice(0, 12);
2758
+ result.innerHTML = `<div style="font-size:14px;margin-bottom:6px">✅ 找到账号: <b>${escapeHtml(name)}</b></div>
2759
+ <div style="font-size:12px;color:var(--text-secondary);margin-bottom:10px">DID: ${escapeHtml(String(r.did || ''))}</div>
2760
+ <button class="sheet-choice" id="phone-add-btn" style="width:100%">发送好友申请</button>`;
2761
+ const btn = $('#phone-add-btn');
2762
+ if (btn) btn.addEventListener('click', async () => {
2763
+ btn.disabled = true; btn.textContent = '发送中…';
2764
+ const a = await api.post('/api/phone/directory/add-friend', { phone });
2765
+ btn.disabled = false;
2766
+ if (a && a.ok !== false) {
2767
+ btn.textContent = '✅ 已发送 (对方同意后建立好友)';
2768
+ if (a.friend && a.friend.requestId) result.innerHTML += `<div style="font-size:12px;color:var(--text-secondary);margin-top:6px">申请号: ${escapeHtml(String(a.friend.requestId).slice(0, 12))}… 可到「待处理申请」查看</div>`;
2769
+ } else {
2770
+ btn.textContent = '重试发送';
2771
+ result.innerHTML += `<div style="color:var(--danger,#e05555);font-size:12px;margin-top:6px">发送失败: ${escapeHtml((a && a.error) || '未知原因')}</div>`;
2772
+ }
2773
+ });
2774
+ } else {
2775
+ result.innerHTML = '<div style="color:var(--text-secondary);font-size:13px">未找到该手机号的账号 (对方未发布目录或已撤销)。</div>';
2776
+ }
2777
+ } catch (e) {
2778
+ result.innerHTML = `<div style="color:var(--danger,#e05555);font-size:13px">查询失败: ${escapeHtml((e && e.message) || e || '未知错误')}</div>`;
2779
+ }
2780
+ }
2781
+
2567
2782
  // === 复制 (助记词/私钥/地址 快捷复制; 全局委托 [data-copy]) ===
2568
2783
  async function copyText(t) {
2569
2784
  const txt = String(t || '');
@@ -2586,7 +2801,9 @@
2586
2801
  setTimeout(() => { b.textContent = old; }, 1200);
2587
2802
  });
2588
2803
 
2589
- // === 登录页 (设置本机身份昵称) ===
2804
+ // === 登录页 (手机号验证码登录 + 昵称兜底) ===
2805
+ // 2026-10-15: 手机原生手机号登录 — 输手机号 → 发验证码 → 验证通过 → 身份绑定手机号。
2806
+ // 验证码链路转发电脑端 /api/contacts/bind + verify (与桌面同一 OTP 通道)。
2590
2807
  function openLoginPage() {
2591
2808
  const page = document.createElement('div');
2592
2809
  page.className = 'chat-page';
@@ -2597,13 +2814,61 @@
2597
2814
  <div style="flex:1;font-weight:600">登录</div>
2598
2815
  </div>
2599
2816
  <div style="padding:12px;display:flex;flex-direction:column;gap:12px">
2600
- <div style="font-size:13px;color:var(--text-secondary)">登录 = 在本机创建/启用身份并设置昵称。DID 由本机生成, 不上传服务器。</div>
2817
+ <div style="font-size:13px;color:var(--text-secondary)">手机号登录: 输手机号 → 收验证码 → 登录。身份仍由本机 DID 承载, 手机号只是登录/查找凭证。</div>
2818
+ <input id="login-phone" type="tel" inputmode="numeric" placeholder="手机号 (如 13812345678)" style="${_walletInput}">
2819
+ <div style="display:flex;gap:8px">
2820
+ <input id="login-code" inputmode="numeric" placeholder="验证码" style="${_walletInput};flex:1" disabled>
2821
+ <button id="login-sendcode" style="${_walletBtn};width:110px;flex:none">发送验证码</button>
2822
+ </div>
2823
+ <button id="login-submit" style="${_walletBtn}">手机号登录</button>
2824
+ <div style="border-top:1px solid var(--border);margin-top:4px;padding-top:12px;font-size:12px;color:var(--text-secondary)">或者用昵称登录 (本机身份, 不上传):</div>
2601
2825
  <input id="login-name" placeholder="昵称 (如 觉者)" style="${_walletInput}">
2602
- <button id="login-submit" style="${_walletBtn}">登录</button>
2826
+ <button id="login-name-submit" style="${_walletBtn}">昵称登录</button>
2603
2827
  </div>`;
2604
2828
  document.body.appendChild(page);
2605
2829
  $('#login-back').addEventListener('click', () => page.remove());
2606
- $('#login-submit').addEventListener('click', async () => {
2830
+
2831
+ // 手机号登录: 登录成功后把手机号写进身份 profile (手机号即凭证)
2832
+ const phoneInput = $('#login-phone');
2833
+ const codeInput = $('#login-code');
2834
+ const sendBtn = $('#login-sendcode');
2835
+ const submitBtn = $('#login-submit');
2836
+ let _pending = { contactId: '', challengeId: '' };
2837
+ sendBtn.addEventListener('click', async () => {
2838
+ const phone = (phoneInput.value || '').replace(/[^\d+]/g, '');
2839
+ if (!/^\+?\d{8,15}$/.test(phone)) { alert('请输入正确的手机号 (8-15 位数字)'); return; }
2840
+ sendBtn.disabled = true; sendBtn.textContent = '发送中…';
2841
+ try {
2842
+ const r = await api.post('/api/phone/auth/bind', { phone });
2843
+ if (!r || r.ok === false) { alert((r && r.error) || '发送失败'); sendBtn.disabled = false; sendBtn.textContent = '发送验证码'; return; }
2844
+ _pending = { contactId: String(r.contactId || ''), challengeId: String(r.challengeId || '') };
2845
+ codeInput.disabled = false; codeInput.focus();
2846
+ sendBtn.textContent = '已发送 ✓';
2847
+ // 本地落盘通道会回显验证码 (方便无短信设备的调试; 真 SMS 通道不回显)
2848
+ if (r.otpForLocalSink) { showToast ? showToast('验证码: ' + String(r.otpForLocalSink)) : alert('验证码: ' + String(r.otpForLocalSink)); }
2849
+ setTimeout(() => { sendBtn.disabled = false; sendBtn.textContent = '重新发送'; }, 30000);
2850
+ } catch (e) { alert('发送失败: ' + (e.message || e)); sendBtn.disabled = false; sendBtn.textContent = '发送验证码'; }
2851
+ });
2852
+ submitBtn.addEventListener('click', async () => {
2853
+ const code = (codeInput.value || '').trim();
2854
+ if (!_pending.contactId || !code) { alert('先发送验证码并输入验证码'); return; }
2855
+ submitBtn.disabled = true; submitBtn.textContent = '验证中…';
2856
+ try {
2857
+ const v = await api.post('/api/phone/auth/verify', { contactId: _pending.contactId, challengeId: _pending.challengeId, code });
2858
+ if (!v || v.ok === false) { alert((v && v.error) || '验证失败'); submitBtn.disabled = false; submitBtn.textContent = '手机号登录'; return; }
2859
+ // 验证通过 → 昵称默认取手机号, 登录本机身份, 手机号写进 profile
2860
+ const phone = (phoneInput.value || '').replace(/[^\d+]/g, '');
2861
+ await api.post('/api/auth/login', { name: '用户' + phone.slice(-4) });
2862
+ try { await api.post('/api/phone/profile/phone', { phone }); } catch { /* 本地 profile 更新失败不阻塞登录 */ }
2863
+ page.remove();
2864
+ await loadMe();
2865
+ switchTab('me');
2866
+ await autoSyncDesktop();
2867
+ } catch (e) { alert('验证失败: ' + (e.message || e)); submitBtn.disabled = false; submitBtn.textContent = '手机号登录'; }
2868
+ });
2869
+ // 手机号 → 本机身份 profile (只存本机; 电脑端目录发布仍需用户在电脑端做)
2870
+ // 昵称登录兜底
2871
+ $('#login-name-submit').addEventListener('click', async () => {
2607
2872
  const name = (($('#login-name') || {}).value || '').trim();
2608
2873
  try {
2609
2874
  await api.post('/api/auth/login', { name });
@@ -2633,7 +2898,6 @@
2633
2898
  $('#btn-add').addEventListener('click', addFriend);
2634
2899
  const bIdx = $('#btn-index'); if (bIdx) bIdx.addEventListener('click', () => void openIndexPanel());
2635
2900
  const bSearch = $('#btn-search'); if (bSearch) bSearch.addEventListener('click', () => void openSearch());
2636
- const cs = $('#btn-create-session'); if (cs) cs.addEventListener('click', createSession);
2637
2901
  const csScan = $('#choice-scan'); if (csScan) csScan.addEventListener('click', addFriendScan);
2638
2902
  const csMan = $('#choice-manual'); if (csMan) csMan.addEventListener('click', addFriendManual);
2639
2903
  const csCan = $('#choice-cancel'); if (csCan) csCan.addEventListener('click', () => hideSheet('#addfriend-sheet'));
@@ -2670,10 +2934,19 @@
2670
2934
  // #2b 连接好友 (点按式) + 附近设备 + 待处理申请
2671
2935
  const addFriendBtn = $('#item-add-friend');
2672
2936
  if (addFriendBtn) addFriendBtn.addEventListener('click', () => showSheet('#addfriend-sheet'));
2937
+ const cPhone = $('#choice-phone');
2938
+ if (cPhone) cPhone.addEventListener('click', () => openPhoneSearch());
2673
2939
  const cNearby = $('#choice-nearby');
2674
2940
  if (cNearby) cNearby.addEventListener('click', () => { hideSheet('#addfriend-sheet'); openNearbySheet('friend'); });
2675
2941
  const cRequests = $('#choice-requests');
2676
2942
  if (cRequests) cRequests.addEventListener('click', () => { hideSheet('#addfriend-sheet'); openRequestsSheet(); });
2943
+ // #2c 手机号搜索添加好友 sheet 的按键绑定
2944
+ const psDo = $('#phone-search-do');
2945
+ if (psDo) psDo.addEventListener('click', () => { void doPhoneSearch(); });
2946
+ const psClose = $('#phone-search-close');
2947
+ if (psClose) psClose.addEventListener('click', () => hideSheet('#phone-search-sheet'));
2948
+ const psInput = $('#phone-search-input');
2949
+ if (psInput) psInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); void doPhoneSearch(); } });
2677
2950
  const nearbyBtn = $('#item-nearby');
2678
2951
  if (nearbyBtn) nearbyBtn.addEventListener('click', () => openNearbySheet('friend'));
2679
2952
  const nearbyRefresh = $('#nearby-refresh');
@@ -2719,7 +2992,7 @@
2719
2992
  const r2 = await (window.BolloonCore && window.BolloonCore.gateway && window.BolloonCore.gateway.join(text));
2720
2993
  alert(r2 ? (r2.output || '已处理') : '这个二维码不是好友地址 (也不是入网链接)');
2721
2994
  }
2722
- if (currentTab === 'network') { loadContacts(); loadP2PStatus(); }
2995
+ if (currentTab === 'network' || currentTab === 'tasks') { loadContacts(); loadP2PStatus(); }
2723
2996
  return;
2724
2997
  }
2725
2998
  const r = await (window.BolloonCore && window.BolloonCore.gateway && window.BolloonCore.gateway.join(text));
@@ -3120,7 +3393,7 @@
3120
3393
  if (!msg || msg.type !== 'ui' || !msg.action) return;
3121
3394
  const d = msg.data || {};
3122
3395
  switch (msg.action) {
3123
- case 'switchTab': if (d.tab && ['main', 'friends', 'network', 'me'].includes(d.tab)) switchTab(d.tab); break;
3396
+ case 'switchTab': if (d.tab && ['main', 'friends', 'tasks', 'me'].includes(d.tab)) switchTab(d.tab); break;
3124
3397
  case 'openSettings': openSettings(); break;
3125
3398
  case 'showToast': alert(d.message || ''); break;
3126
3399
  case 'goBack': closeCardDetail(); closeChat(); break;
@@ -3245,7 +3518,7 @@
3245
3518
  return false;
3246
3519
  }
3247
3520
  function setupGestures() {
3248
- const TAB_ORDER = ['main', 'friends', 'network', 'me'];
3521
+ const TAB_ORDER = ['main', 'friends', 'tasks', 'me'];
3249
3522
  let sx = 0, sy = 0, st = 0, active = false, startTarget = null;
3250
3523
  document.addEventListener('touchstart', (e) => {
3251
3524
  if (e.touches.length !== 1) { active = false; return; }
@@ -3705,39 +3978,39 @@
3705
3978
 
3706
3979
  // ── 渲染: 公告板 ──────────────────────────────────────────────────────────
3707
3980
  function renderBoard() {
3708
- const list = $('#tasks-board');
3709
- if (!list) return;
3710
- clear(list);
3711
- const d = state.board || {};
3712
- const items = d.items || [];
3713
- if (!items.length) {
3714
- list.appendChild(line('sheet-text', lang() === 'en' ? 'Board is empty' : '板上暂时没有公告'));
3715
- }
3716
- items.forEach((it, i) => {
3717
- const r = row(
3718
- String(it.capability || '') + ' · ' + String(it.idShort || '') + ' · ' + String((it.statusLabel || {})[lang()] || ''),
3719
- '',
3720
- );
3721
- const sub = line('', '');
3722
- sub.style.fontSize = '12px';
3723
- sub.style.color = 'var(--text-secondary)';
3724
- sub.appendChild(document.createTextNode((lang() === 'en' ? 'budget ' : '预算 ') + safeText(it.budgetLabel || '—') + ' · '));
3725
- sub.appendChild(document.createTextNode(lang() === 'en' ? 'closes ' : '截止 '));
3726
- sub.appendChild(relDeltaNode(it.deadlineInMs));
3727
- r.firstChild.appendChild(sub);
3728
- if (it.claimable) {
3729
- const b = mk('button', 'sheet-choice');
3730
- b.style.flex = '0 0 auto';
3731
- bilingual(b, '发进群', 'Post to group');
3732
- b.setAttribute('data-announce', String(i));
3733
- b.addEventListener('click', () => askAnnounceToGroup(i));
3734
- r.appendChild(b);
3981
+ const list = $('#tasks-board');
3982
+ if (!list) return;
3983
+ clear(list);
3984
+ const d = state.board || {};
3985
+ const items = d.items || [];
3986
+ if (!items.length) {
3987
+ list.appendChild(line('sheet-text', lang() === 'en' ? 'Board is empty' : '板上暂时没有公告'));
3735
3988
  }
3736
- list.appendChild(r);
3737
- });
3738
- const notes = Array.isArray(d.notes) ? d.notes : [];
3739
- if (notes.length) list.appendChild(line('sheet-text', notes.join(' · ')));
3740
- }
3989
+ // 2026-10-07: 双栏瀑布流卡片 (发现页风格)
3990
+ items.forEach((it, i) => {
3991
+ const card = mk('div', 'board-card');
3992
+ const cap = mk('div', 'board-card-cap');
3993
+ cap.textContent = String(it.capability || it.idShort || '任务');
3994
+ card.appendChild(cap);
3995
+ const status = mk('div', 'board-card-status');
3996
+ status.textContent = String((it.statusLabel || {})[lang()] || '');
3997
+ card.appendChild(status);
3998
+ const sub = mk('div', 'board-card-sub');
3999
+ sub.textContent = (lang() === 'en' ? 'budget ' : '预算 ') + safeText(it.budgetLabel || '—')
4000
+ + ' · ' + (lang() === 'en' ? 'closes ' : '截止 ') + relDeltaText(it.deadlineInMs);
4001
+ card.appendChild(sub);
4002
+ if (it.claimable) {
4003
+ const b = mk('button', 'board-card-btn');
4004
+ b.textContent = lang() === 'en' ? 'Post to group' : '发进群';
4005
+ b.setAttribute('data-announce', String(i));
4006
+ b.addEventListener('click', () => askAnnounceToGroup(i));
4007
+ card.appendChild(b);
4008
+ }
4009
+ list.appendChild(card);
4010
+ });
4011
+ const notes = Array.isArray(d.notes) ? d.notes : [];
4012
+ if (notes.length) list.appendChild(line('sheet-text', notes.join(' · ')));
4013
+ }
3741
4014
 
3742
4015
  // ── 渲染: 飞轮进度 (只读; 状态来自 goal-flywheel 的五类用户可见状态) ────────
3743
4016
  function renderFlywheel() {