@bolloon/bolloon-agent 0.6.6 → 0.6.7

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 (52) hide show
  1. package/dist/agents/ha-daemon.js +85 -0
  2. package/dist/agents/ha-supervisor.js +146 -0
  3. package/dist/agents/memory-store.js +25 -0
  4. package/dist/agents/opportunity-match.js +15 -3
  5. package/dist/agents/pi-sdk-session-manager.js +8 -0
  6. package/dist/agents/pi-sdk.js +67 -494
  7. package/dist/bootstrap/context-collector.js +65 -3
  8. package/dist/bootstrap/persona-loader.js +26 -0
  9. package/dist/electron-build/agents/memory-store.js +26 -0
  10. package/dist/electron-build/agents/memory-store.js.map +1 -1
  11. package/dist/electron-build/agents/pi-sdk-session-manager.js +8 -0
  12. package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
  13. package/dist/electron-build/agents/pi-sdk.js +65 -492
  14. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  15. package/dist/electron-build/bootstrap/context-collector.js +66 -3
  16. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  17. package/dist/electron-build/bootstrap/persona-loader.js +27 -0
  18. package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
  19. package/dist/electron-build/llm/pi-ai.js +16 -3
  20. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  21. package/dist/electron-build/llm/system-prompt/registry.js +8 -6
  22. package/dist/electron-build/llm/system-prompt/registry.js.map +1 -1
  23. package/dist/ios/bolloon-web.json +3 -3
  24. package/dist/ios/index.html +19 -4
  25. package/dist/ios/mobile-agent.js +267 -41
  26. package/dist/ios/mobile-contacts.js +0 -4
  27. package/dist/ios/mobile-core.js +806 -30
  28. package/dist/ios/mobile-memory.js +317 -0
  29. package/dist/ios/mobile.html +19 -4
  30. package/dist/ios/mobile.js +144 -30
  31. package/dist/ios/routes-contacts.js +94 -1
  32. package/dist/ios/routes-world.js +9 -1
  33. package/dist/ios/server.js +51 -40
  34. package/dist/ios/sw.js +1 -1
  35. package/dist/kernel/gate-scan.js +1 -1
  36. package/dist/kernel/ha/ha-arbiter.js +177 -0
  37. package/dist/kernel/ha/ha-guard.js +151 -0
  38. package/dist/kernel/index.js +3 -0
  39. package/dist/kernel/plan.js +5 -5
  40. package/dist/kernel/roster.js +3 -3
  41. package/dist/llm/pi-ai.js +16 -3
  42. package/dist/llm/system-prompt/registry.js +8 -6
  43. package/dist/web/bolloon-web.json +3 -3
  44. package/dist/web/mobile-agent.js +265 -44
  45. package/dist/web/mobile-core.js +681 -32
  46. package/dist/web/mobile-memory.js +317 -0
  47. package/dist/web/mobile.html +4 -4
  48. package/dist/web/mobile.js +36 -27
  49. package/dist/web/routes-world.js +9 -1
  50. package/dist/web/server.js +50 -39
  51. package/dist/web/sw.js +1 -1
  52. package/package.json +1 -1
@@ -0,0 +1,317 @@
1
+ const memKey = (agentId) => `bolloon_mobile_memory_${agentId}`;
2
+ /** 轻量 hash (记忆指纹/条目 id, 不涉密) */
3
+ export function quickHash(text) {
4
+ let h = 2166136261;
5
+ for (let i = 0; i < String(text || '').length; i++) {
6
+ h ^= String(text || '').charCodeAt(i);
7
+ h = Math.imul(h, 16777619);
8
+ }
9
+ return (h >>> 0).toString(36) + '_' + String(text || '').length.toString(36);
10
+ }
11
+ export function validMemoryContent(content) {
12
+ return String(content || '').trim().length >= 2;
13
+ }
14
+ /** importance 启发式 (与桌面同口径): 长度 + 强语义词 + kind 加权 */
15
+ export function heuristicImportance(content, kind = 'fact') {
16
+ const t = String(content || '');
17
+ let score = 0.3;
18
+ if (t.length > 100)
19
+ score += 0.2;
20
+ if (/喜欢|偏好|讨厌|禁止|必须|永远|从不|重要|注意|记住|约定|家人|朋友|同事/.test(t))
21
+ score += 0.25;
22
+ if (kind === 'preference' || kind === 'relationship')
23
+ score += 0.2;
24
+ if (kind === 'skill-feedback')
25
+ score += 0.15;
26
+ return Math.min(1, score);
27
+ }
28
+ /** 人际关系分类: 从文本/来源推断 relation kind (规则启发式; 未知 = stranger) */
29
+ export function classifyRelation(text, source = '') {
30
+ const t = String(text || '').toLowerCase();
31
+ const src = String(source || '').toLowerCase();
32
+ if (/家人|妈妈|爸爸|母亲|父亲|儿子|女儿|老婆|老公|妻子|丈夫|兄弟|姐妹/.test(t))
33
+ return 'family';
34
+ if (/同事|客户|供应商|老板|领导|下属|同事|合作/.test(t))
35
+ return 'work';
36
+ if (/朋友|好友|哥们|闺蜜|兄弟|认识的人/.test(t))
37
+ return 'friend';
38
+ if (/directory|phone|手机号|通讯录/.test(src))
39
+ return 'acquaintance';
40
+ return 'stranger';
41
+ }
42
+ /**
43
+ * 手机记忆库 (localStorage 持久化; 内存缓存避免每次读盘)。
44
+ * 测试注入 fake localStorage (mobile-core.test 模式) 即可用。
45
+ */
46
+ export class MobileMemory {
47
+ agentId;
48
+ entries = [];
49
+ loaded = false;
50
+ constructor(agentId) {
51
+ this.agentId = String(agentId || '');
52
+ }
53
+ key() { return memKey(this.agentId); }
54
+ readAll() {
55
+ try {
56
+ const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(this.key()) : null;
57
+ if (!raw)
58
+ return [];
59
+ const parsed = JSON.parse(raw);
60
+ return Array.isArray(parsed) ? parsed : [];
61
+ }
62
+ catch {
63
+ return [];
64
+ }
65
+ }
66
+ writeAll(list) {
67
+ try {
68
+ if (typeof localStorage !== 'undefined')
69
+ localStorage.setItem(this.key(), JSON.stringify(list.slice(-200)));
70
+ }
71
+ catch { /* 写失败静默 (内存态仍可用) */ }
72
+ }
73
+ ensureLoaded() {
74
+ if (!this.loaded) {
75
+ this.entries = this.readAll();
76
+ this.loaded = true;
77
+ }
78
+ }
79
+ /** 全部条目 (内部用) */
80
+ all() { this.ensureLoaded(); return [...this.entries]; }
81
+ /** 落一条记忆 (幂等: 同 id 覆盖); 返回条目 */
82
+ remember(input) {
83
+ this.ensureLoaded();
84
+ const content = String(input.content || '').trim();
85
+ const source = input.source || 'session';
86
+ if (!validMemoryContent(content)) {
87
+ // 空内容拒绝存 (与桌面 validContent 同口径)
88
+ const id = quickHash(this.agentId + '|' + content || 'empty');
89
+ const stub = { id, agentId: this.agentId, content: content || '(空)', kind: input.kind || 'fact', importance: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), source, recallCount: 0 };
90
+ return stub;
91
+ }
92
+ const kind = input.kind || 'fact';
93
+ const now = new Date().toISOString();
94
+ const id = quickHash(this.agentId + '|' + content);
95
+ const importance = typeof input.importance === 'number'
96
+ ? Math.min(1, Math.max(0, input.importance))
97
+ : heuristicImportance(content, kind);
98
+ const entry = {
99
+ id, agentId: this.agentId, content, kind,
100
+ importance, createdAt: now, updatedAt: now,
101
+ source,
102
+ ttlMs: input.ttlMs, decayed: false, crystallized: false,
103
+ recallCount: 0,
104
+ ...(kind === 'relationship' && input.relation ? { relation: input.relation } : {}),
105
+ };
106
+ const idx = this.entries.findIndex((e) => e.id === id);
107
+ if (idx >= 0) {
108
+ const prev = this.entries[idx];
109
+ this.entries[idx] = { ...prev, ...entry, createdAt: prev.createdAt, recallCount: prev.recallCount, crystallized: prev.crystallized || entry.importance >= 0.8 };
110
+ if (input.relation && prev.relation) {
111
+ // 关系记忆迭代: 信任度按新信息移动 (平均值), 分类按最新文本重判
112
+ this.entries[idx].relation = {
113
+ did: input.relation.did || prev.relation.did,
114
+ kind: input.relation.kind || prev.relation.kind,
115
+ trust: Math.min(1, Math.max(0, (prev.relation.trust + input.relation.trust) / 2)),
116
+ };
117
+ }
118
+ }
119
+ else {
120
+ this.entries.push(entry);
121
+ }
122
+ this.writeAll(this.entries);
123
+ return this.entries.find((e) => e.id === id) || entry;
124
+ }
125
+ /** 召回: 关键词 + importance 加权; 命中即 +1 recallCount + 刷新 lastRecalledAt (迭代) */
126
+ recall(query, opts = {}) {
127
+ this.ensureLoaded();
128
+ this.applyTtl();
129
+ const q = String(query || '').toLowerCase();
130
+ const limit = opts.limit ?? 3;
131
+ const minScore = opts.minScore ?? 0;
132
+ const scored = [];
133
+ for (const e of this.entries) {
134
+ if (e.decayed)
135
+ continue;
136
+ const c = String(e.content || '').toLowerCase();
137
+ let s = e.importance * 0.5;
138
+ const qTokens = q.split(/\s+/).filter((t) => t.length >= 2);
139
+ const hit = qTokens.filter((t) => c.includes(t)).length;
140
+ if (hit > 0)
141
+ s += 0.3 * (hit / Math.max(qTokens.length, 1));
142
+ if (qTokens.length && qTokens.some((t) => c.includes(t)))
143
+ s += 0.2; // 部分命中
144
+ if (e.relation && e.relation.trust >= 0.7)
145
+ s += 0.1; // 高信任关系记忆加权
146
+ if (s >= minScore)
147
+ scored.push({ e, s });
148
+ }
149
+ scored.sort((a, b) => b.s - a.s);
150
+ const hits = scored.slice(0, limit).map((x) => x.e);
151
+ for (const h of hits) {
152
+ const now = new Date().toISOString();
153
+ h.recallCount += 1;
154
+ h.lastRecalledAt = now;
155
+ if (h.recallCount >= 3)
156
+ h.crystallized = true; // 多次召回 → 固化 (不 decay)
157
+ }
158
+ if (hits.length)
159
+ this.writeAll(this.entries);
160
+ return hits;
161
+ }
162
+ /** TTL 过期 → decayed (不删, 与桌面 applyDecay 同精神) */
163
+ applyTtl(nowIso) {
164
+ this.ensureLoaded();
165
+ const now = Date.parse(nowIso || new Date().toISOString());
166
+ let changed = false;
167
+ for (const e of this.entries) {
168
+ if (e.decayed || e.crystallized || !e.ttlMs)
169
+ continue;
170
+ const base = Date.parse(e.lastRecalledAt || e.createdAt) || now;
171
+ if (now - base > e.ttlMs) {
172
+ e.decayed = true;
173
+ changed = true;
174
+ }
175
+ }
176
+ if (changed)
177
+ this.writeAll(this.entries);
178
+ }
179
+ /**
180
+ * 记忆迭代 (每轮循环后调用): 从「用户目标 + 工具结果 + 最终回复」提炼
181
+ * 新事实 / 偏好 / 关系。规则启发式 (零 LLM 成本): 信号词命中即存,
182
+ * 已经存过的同内容自动合并 (remember 幂等)。
183
+ */
184
+ autoIterate(input) {
185
+ this.ensureLoaded();
186
+ const parts = [String(input.goal || ''), ...(input.toolResults || []), String(input.reply || '')];
187
+ const text = parts.filter(Boolean).join('\n');
188
+ if (!text.trim())
189
+ return 0;
190
+ let added = 0;
191
+ // ① 偏好记忆 — 兼容「我/用户/本人」主语 (agent 转述用户原话时主语可能是「用户」)
192
+ const pref = text.match(/(?:我|用户|本人)(?:喜欢|偏好|热爱|想要|希望).{2,40}?/g);
193
+ if (pref) {
194
+ for (const m of pref.slice(0, 3)) {
195
+ const content = m.replace(/^(?:我|用户|本人)/, '').trim();
196
+ if (validMemoryContent(content)) {
197
+ this.remember({ content: '偏好: ' + content, kind: 'preference', source: input.source || 'iterate' });
198
+ added++;
199
+ }
200
+ }
201
+ }
202
+ // ② 禁忌/约定记忆 — 兼容「不要/不能/别」开头 (用户约束)
203
+ const ban = text.match(/不要(?:再)?.{2,40}?|不要.{2,40}?|不能.{2,40}?|禁止.{2,40}?|记住.{2,40}?/g);
204
+ if (ban) {
205
+ for (const m of ban.slice(0, 2)) {
206
+ if (validMemoryContent(m)) {
207
+ this.remember({ content: '约定: ' + m.trim(), kind: 'insight', source: input.source || 'iterate', importance: 0.6 });
208
+ added++;
209
+ }
210
+ }
211
+ }
212
+ // ③ 关系记忆: 出现「人名/称谓 + 关系词」→ 分类存 (含「同事/合作伙伴」等职场关系)
213
+ const relMatch = text.match(/([一-龥]{2,6})是(?:我的)?(家人|妈妈|爸爸|老婆|老公|朋友|同事|客户|老板|领导|兄弟|姐妹|妻子|丈夫|合作伙伴)/);
214
+ if (relMatch) {
215
+ const name = relMatch[1];
216
+ const kind = classifyRelation(relMatch[0], input.source || '');
217
+ const content = `${name} 是${relMatch[2]}`;
218
+ this.remember({
219
+ content, kind: 'relationship', source: input.source || 'iterate',
220
+ importance: 0.8,
221
+ relation: { did: '', kind, trust: 0.6 },
222
+ });
223
+ added++;
224
+ }
225
+ return added;
226
+ }
227
+ /** 快照 (run 结束/重启恢复用) */
228
+ snapshot() {
229
+ this.ensureLoaded();
230
+ this.applyTtl();
231
+ return { version: 1, agentId: this.agentId, entries: [...this.entries], savedAt: new Date().toISOString() };
232
+ }
233
+ /** 从快照恢复 (bootstrap) — 合并, 不覆盖已有 */
234
+ restore(snap) {
235
+ if (!snap || !Array.isArray(snap.entries))
236
+ return;
237
+ this.ensureLoaded();
238
+ for (const e of snap.entries) {
239
+ const exists = this.entries.some((x) => x.id === e.id);
240
+ if (!exists)
241
+ this.entries.push(e);
242
+ }
243
+ this.writeAll(this.entries);
244
+ }
245
+ /**
246
+ * 稳态记忆块: 固化优先 + 未淡化 + importance 降序, 始终注入 system。
247
+ * 与 recall(query) 互补: recall 是即时相关, stableBlock 是用户确认/多次命中的事实。
248
+ */
249
+ stableBlock(maxEntries = 6) {
250
+ this.ensureLoaded();
251
+ this.applyTtl();
252
+ const picked = this.entries
253
+ .filter((e) => !e.decayed)
254
+ .sort((a, b) => {
255
+ if (a.crystallized !== b.crystallized)
256
+ return a.crystallized ? -1 : 1;
257
+ return b.importance - a.importance;
258
+ })
259
+ .slice(0, maxEntries);
260
+ if (!picked.length)
261
+ return '';
262
+ const lines = picked.map((e) => {
263
+ const tag = `${e.kind}${e.crystallized ? '★' : ''}`;
264
+ return `- (${tag}, ${e.importance.toFixed(2)}) ${e.content.slice(0, 120)}`;
265
+ });
266
+ return `<stable-memory>\n${lines.join('\n')}\n</stable-memory>`;
267
+ }
268
+ /** 记忆指纹: 摘要 hash (参与上下文缓存 key; 记忆变了 → 指纹变 → 缓存 miss) */
269
+ fingerprint() {
270
+ this.ensureLoaded();
271
+ this.applyTtl();
272
+ const active = this.entries.filter((e) => !e.decayed).sort((a, b) => a.id.localeCompare(b.id));
273
+ const digest = active.slice(-30).map((e) => `${e.kind}:${e.content.slice(0, 60)}:${e.importance.toFixed(2)}`).join('|');
274
+ return quickHash(digest);
275
+ }
276
+ }
277
+ /** 独立缓存助手 (mobile-runtime 用): 指纹 → 结果, 带 TTL */
278
+ export class MobileCache {
279
+ prefix;
280
+ constructor(prefix = 'bolloon_mobile_cache') { this.prefix = prefix; }
281
+ get(key, ttlMs = 60_000) {
282
+ try {
283
+ const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(`${this.prefix}_${key}`) : null;
284
+ if (!raw)
285
+ return null;
286
+ const parsed = JSON.parse(raw);
287
+ if (!parsed || !parsed.v || typeof parsed.at !== 'number')
288
+ return null;
289
+ if (Date.now() - parsed.at > ttlMs)
290
+ return null; // TTL 过期 → miss
291
+ return parsed.v;
292
+ }
293
+ catch {
294
+ return null;
295
+ }
296
+ }
297
+ set(key, value) {
298
+ try {
299
+ if (typeof localStorage !== 'undefined')
300
+ localStorage.setItem(`${this.prefix}_${key}`, JSON.stringify({ v: value, at: Date.now() }));
301
+ }
302
+ catch { /* 缓存写失败静默 */ }
303
+ }
304
+ clear() {
305
+ try {
306
+ const keys = [];
307
+ for (let i = 0; i < localStorage.length; i++) {
308
+ const k = localStorage.key(i);
309
+ if (k && k.startsWith(this.prefix))
310
+ keys.push(k);
311
+ }
312
+ for (const k of keys)
313
+ localStorage.removeItem(k);
314
+ }
315
+ catch { /* 忽略 */ }
316
+ }
317
+ }
@@ -76,12 +76,12 @@
76
76
  <div class="list-item" id="item-p2p"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17"/><path d="M12 3.5c2.3 2.3 3.5 5.3 3.5 8.5s-1.2 6.2-3.5 8.5c-2.3-2.3-3.5-5.3-3.5-8.5S9.7 5.8 12 3.5z"/></svg></span><span>群聊</span><span class="list-arrow">›</span></div>
77
77
  <div class="list-item" id="item-scan-net"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2"/><path d="M4 12h16"/></svg></span><span>扫码入网 / 加好友</span></div>
78
78
  <div class="list-item" id="item-nearby"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M4 12a8 8 0 0 1 16 0"/><path d="M7.5 12a4.5 4.5 0 0 1 9 0"/><circle cx="12" cy="12" r="1.4"/><path d="M12 13.6V19"/></svg></span><span>附近设备</span><span class="list-arrow">›</span></div>
79
- <div class="list-item" id="item-p2p-id"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="3" y="5.5" width="18" height="13" rx="2.5"/><circle cx="8.5" cy="11" r="2"/><path d="M5.6 15.8a3.2 3.2 0 0 1 5.8 0M13.5 10h5M13.5 13.5h5"/></svg></span><span>我的 P2P ID</span></div>
79
+ <div class="list-item" id="item-p2p-id"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="3" y="5.5" width="18" height="13" rx="2.5"/><circle cx="8.5" cy="11" r="2"/><path d="M5.6 15.8a3.2 3.2 0 0 1 5.8 0M13.5 10h5M13.5 13.5h5"/></svg></span><span>我的连接标识</span></div>
80
80
  </div>
81
81
  <input type="file" id="qr-scan-input" accept="image/*" capture="environment" style="display:none">
82
- <div class="section-label">P2P 连接</div>
82
+ <div class="section-label">网络连接</div>
83
83
  <div class="list" id="p2p-status"></div>
84
- <div class="section-label">好友列表</div>
84
+ <div class="section-label">联系人列表</div>
85
85
  <!-- 通讯录式联系人列表: 按字母分组 + 右侧索引 (由 loadContacts 渲染) -->
86
86
  <div class="contacts-wrap" id="contacts-wrap">
87
87
  <div class="list" id="contacts-list"></div>
@@ -184,7 +184,7 @@
184
184
  <!-- 底部 tab -->
185
185
  <nav class="tabbar" id="tabbar">
186
186
  <button class="tab active" data-tab="main"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="13.5" width="7" height="7" rx="1.6"/></svg></span><span>首页</span></button>
187
- <button class="tab" data-tab="friends"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.2"/><path d="M3.5 19c.8-3 3-4.6 5.5-4.6S13.7 16 14.5 19"/><path d="M17 8v6M14 11h6"/></svg></span><span>好友</span></button>
187
+ <button class="tab" data-tab="friends"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.2"/><path d="M3.5 19c.8-3 3-4.6 5.5-4.6S13.7 16 14.5 19"/><path d="M17 8v6M14 11h6"/></svg></span><span>通讯录</span></button>
188
188
  <!-- 2026-10-06: 网络+任务合一, 任务 tab 内页内子切换 -->
189
189
  <button class="tab" data-tab="tasks"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M4 7.5h10M4 12h7M4 16.5h12"/><path d="M18 5.5l1.6 1.6L22 4.5"/><path d="M18 14.5l1.6 1.6L22 13.5"/></svg></span><span>任务</span></button>
190
190
  <button class="tab" data-tab="me"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="12" cy="8.5" r="4"/><path d="M4.5 20.5a7.5 7.5 0 0 1 15 0"/></svg></span><span>我</span></button>
@@ -69,7 +69,7 @@
69
69
  });
70
70
  } catch (e) {}
71
71
 
72
- const TITLES = { main: '首页', friends: '好友', tasks: '任务', me: '我' };
72
+ const TITLES = { main: '首页', friends: '通讯录', tasks: '任务', me: '我' };
73
73
  let currentTab = 'main';
74
74
  function switchTab(tab) {
75
75
  currentTab = tab;
@@ -166,7 +166,7 @@
166
166
  if (!list) return;
167
167
  list.innerHTML = '';
168
168
  if (!Array.isArray(peers) || peers.length === 0) {
169
- list.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">暂无好友</div>';
169
+ list.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">暂无联系人</div>';
170
170
  const idx = $('#contacts-index'); if (idx) idx.innerHTML = '';
171
171
  return;
172
172
  }
@@ -453,15 +453,15 @@
453
453
  try {
454
454
  const s = await api.get('/api/auth/status');
455
455
  $('#me-name').textContent = s.loggedIn ? (s.name || '未登录') : '未登录';
456
- $('#me-did').textContent = s.did ? ('DID: ' + (s.didShort || s.did)) : '';
456
+ $('#me-did').textContent = s.did ? ('身份证明: ' + (s.didShort || s.did)) : '';
457
457
  if (avatarEl && !av) avatarEl.textContent = (s.loggedIn && s.name ? s.name : 'U').charAt(0);
458
458
  $('#login-label').textContent = s.loggedIn ? '已登录' : '登录';
459
459
  } catch (e) { /* 默认 */ }
460
- // P2P ID = 通信ID, 与 DID 不同 (libp2p nodeId)
460
+ // 身份证明 (连接标识)
461
461
  try {
462
462
  const net = await api.get('/api/network/status');
463
463
  const p2p = net && net.nodeId;
464
- $('#me-p2p').textContent = p2p ? ('P2P: ' + p2p.slice(0, 14) + '…') : '';
464
+ $('#me-p2p').textContent = p2p ? ('连接标识: ' + p2p.slice(0, 14) + '…') : '';
465
465
  } catch (e) { /* 未连接 */ }
466
466
  }
467
467
 
@@ -488,10 +488,10 @@
488
488
  <div class="avatar" id="identity-avatar" style="width:72px;height:72px"></div>
489
489
  <div><div class="profile-name">${escapeHtml(name)}</div><div style="font-size:12px;color:var(--text-muted)">人类认证身份</div></div>
490
490
  </div>
491
- <div class="identity-row"><div class="k">DID 身份 (全局唯一)</div><div class="v">${escapeHtml(did || '未生成')}</div></div>
492
- <div class="identity-row"><div class="k">P2P ID (通信 ID)</div><div class="v">${escapeHtml(p2p || '未连接')}</div></div>
491
+ <div class="identity-row"><div class="k">身份证明 (全局唯一)</div><div class="v">${escapeHtml(did || '未生成')}</div></div>
492
+ <div class="identity-row"><div class="k">连接标识</div><div class="v">${escapeHtml(p2p || '未连接')}</div></div>
493
493
  <div class="identity-row"><div class="k">创建时间</div><div class="v">${escapeHtml(created || '未知')}</div></div>
494
- <div class="identity-row"><div class="k">说明</div><div class="v">DID 是全局唯一的人类认证身份; P2P ID 只是本机 libp2p 通信节点号, 两者不同。</div></div>`;
494
+ <div class="identity-row"><div class="k">说明</div><div class="v">身份证明是全局唯一的人类认证身份; 连接标识是本机的通信编号, 两者不同。</div></div>`;
495
495
  const av = localStorage.getItem('bolloon_avatar');
496
496
  const ia = body.querySelector('#identity-avatar');
497
497
  if (ia) { if (av) { ia.style.backgroundImage = 'url(' + av + ')'; ia.style.backgroundSize = 'cover'; ia.textContent = ''; } else ia.textContent = (name || 'U').charAt(0); }
@@ -592,9 +592,9 @@
592
592
  const cards = $('#world-feed-cards');
593
593
  if (!feed || !cards) return;
594
594
  try {
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');
595
+ // 2026-10-16 (leo: 首页卡片按「附近 + 通讯录」动态传送): 三源聚合 (桌面机会流 + 通讯录卡 + 附近卡)
596
+ // 不再只拉桌面机会流; 来源不上屏 (leo: 主动推送不贴元解释文案)
597
+ const data = await api.get('/api/world/cards');
598
598
  const opps = ((data && data.opportunities) || []).slice(0, 6);
599
599
  if (!opps.length) { feed.hidden = true; return; }
600
600
  feed.hidden = false;
@@ -748,8 +748,8 @@
748
748
  allAgentCards.push({
749
749
  id: p.id || p.publicKey?.slice(0, 12) || `peer-${Date.now()}`,
750
750
  agentId: p.id || p.publicKey?.slice(0, 12),
751
- name: p.name || '未知好友',
752
- desc: 'P2P 好友智能体',
751
+ name: p.name || '未知联系人',
752
+ desc: '通讯录智能体',
753
753
  avatar: null,
754
754
  peer: { name: p.name || '好友', online: p.online || false },
755
755
  status: p.online ? 'online' : 'offline',
@@ -1102,7 +1102,7 @@
1102
1102
  <div style="flex:1;font-weight:600">搜索</div>
1103
1103
  </div>
1104
1104
  <div style="padding:12px;display:flex;flex-direction:column;gap:10px;flex:1;min-height:0">
1105
- <input id="search-input" type="search" autocomplete="off" placeholder="搜智能体 / 好友:名称、DID、节点 ID"
1105
+ <input id="search-input" type="search" autocomplete="off" placeholder="搜智能体 / 通讯录:名称、身份证明、编号"
1106
1106
  style="padding:10px 12px;border:1px solid var(--border);border-radius:10px;background:var(--bg-hover);color:var(--text);font-size:14px">
1107
1107
  <div id="search-hint" style="font-size:12px;color:var(--text-muted)">正在建立索引…</div>
1108
1108
  <div id="search-results" style="flex:1;overflow:auto;-webkit-overflow-scrolling:touch"></div>
@@ -1122,10 +1122,10 @@
1122
1122
  act: () => { page.remove(); openChat(c); },
1123
1123
  })),
1124
1124
  ...peers.map((p) => ({
1125
- tag: '好友', name: p.name || String(p.publicKey || p.id || '好友').slice(0, 12),
1125
+ tag: '通讯录', name: p.name || String(p.publicKey || p.id || '联系人').slice(0, 12),
1126
1126
  sub: String(p.publicKey || p.id || p.address || '').slice(0, 28),
1127
1127
  hay: [p.name, p.publicKey, p.id, p.address, p.did].filter(Boolean).join(' '),
1128
- act: () => alert(`好友信息\n名称: ${p.name || '(未命名)'}\n节点ID: ${p.publicKey || p.id || '—'}\n地址: ${p.address || '—'}`),
1128
+ act: () => alert(`联系人信息\n名称: ${p.name || '(未命名)'}\n编号: ${p.publicKey || p.id || '—'}\n地址: ${p.address || '—'}`),
1129
1129
  })),
1130
1130
  ...services.map((s) => ({
1131
1131
  tag: '全局智能体',
@@ -1142,10 +1142,10 @@
1142
1142
  const render = (q) => {
1143
1143
  const k = String(q || '').trim().toLowerCase();
1144
1144
  const hits = rows.filter((r) => !k || r.hay.includes(k) || r.name.toLowerCase().includes(k));
1145
- if (hint) hint.innerHTML = `本机智能体 ${channels.length} · 好友 ${peers.length} · 全局智能体 ${services.length}` + (k ? ` · 命中 ${hits.length}` : '');
1145
+ if (hint) hint.innerHTML = `本机智能体 ${channels.length} · 通讯录 ${peers.length} · 全局智能体 ${services.length}` + (k ? ` · 命中 ${hits.length}` : '');
1146
1146
  if (!box) return;
1147
1147
  if (!hits.length) {
1148
- box.innerHTML = `<div style="padding:24px;text-align:center;color:var(--text-muted);font-size:13px">没有匹配「${escapeHtml(String(q))}」的智能体或好友</div>`;
1148
+ box.innerHTML = `<div style="padding:24px;text-align:center;color:var(--text-muted);font-size:13px">没有匹配「${escapeHtml(String(q))}」的智能体或联系人</div>`;
1149
1149
  return;
1150
1150
  }
1151
1151
  let html = '';
@@ -2135,11 +2135,12 @@
2135
2135
  $('#settings-persona').addEventListener('click', () => void openPersonaPage());
2136
2136
  }
2137
2137
 
2138
- // === 身份页 (DID / 公钥 / 签名测试 / 邮箱 / 手机号) ===
2138
+ // === 身份页 (身份证明 / 签名 / 邮箱 / 手机号) ===
2139
2139
  // 2026-10-07: 完整身份系统 — DIAP did:key (与桌面同构) + 附带条件 (邮箱/手机号/备注)
2140
+ // 2026-10-16 (leo: 显示端降低认知负担): 不外露 DID/Ed25519 术语 — 「身份证明」「身份密钥」「签名能力」
2140
2141
  async function openIdentityPage() {
2141
2142
  if ($('#identity-page')) return;
2142
- const page = mkOverlayPage('identity-page', 'DID 与身份', '<div style="font-size:12px;color:var(--text-muted)">读取中…</div>');
2143
+ const page = mkOverlayPage('identity-page', '身份与证明', '<div style="font-size:12px;color:var(--text-muted)">读取中…</div>');
2143
2144
  const body = page.querySelector('#identity-page-body');
2144
2145
  try {
2145
2146
  const core = window.BolloonCore || (await import('./mobile-core.js')).core;
@@ -2147,8 +2148,8 @@
2147
2148
  const st = await ag.identityStatus();
2148
2149
  body.innerHTML = `
2149
2150
  <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>
2151
+ <div class="list-item"><span class="list-icon">${ICONS.idcard}</span><span style="flex:1"><span style="display:block">身份证明</span><span class="conv-preview" style="display:block;word-break:break-all">${escapeHtml(st.did || '未生成')}</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;word-break:break-all">${st.publicKey ? escapeHtml(st.publicKey.slice(0, 32) + '…') : '(老设备哈希身份,无签名)'}</span></span></div>
2152
2153
  <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
2154
  </div>
2154
2155
  <div class="section-label">附带身份条件(仅存本机)</div>
@@ -2158,7 +2159,7 @@
2158
2159
  <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
2160
  <button class="sheet-choice" id="id-edit" style="width:100%;margin-top:8px">编辑邮箱 / 手机号 / 备注</button>
2160
2161
  </div>
2161
- <div class="section-label">签名能力(DIAP Ed25519)</div>
2162
+ <div class="section-label">签名能力</div>
2162
2163
  <div class="list">
2163
2164
  <button class="sheet-choice" id="id-sign-test" style="width:100%">测试签名(本地签 + 验,证明身份可签名)</button>
2164
2165
  </div>`;
@@ -2756,7 +2757,7 @@
2756
2757
  if (r && r.found) {
2757
2758
  const name = r.displayName || String(r.did || '').slice(0, 12);
2758
2759
  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
+ <div style="font-size:12px;color:var(--text-secondary);margin-bottom:10px">身份证明: ${escapeHtml(String(r.did || ''))}</div>
2760
2761
  <button class="sheet-choice" id="phone-add-btn" style="width:100%">发送好友申请</button>`;
2761
2762
  const btn = $('#phone-add-btn');
2762
2763
  if (btn) btn.addEventListener('click', async () => {
@@ -2892,7 +2893,7 @@
2892
2893
  bindAvatarInput();
2893
2894
  $('#item-login').addEventListener('click', () => { openLoginPage(); });
2894
2895
  $('#item-logout').addEventListener('click', async () => {
2895
- if (!confirm('确定注销? 将清除本机登录态 (DID 保留, 不影响频道/P2P)。')) return;
2896
+ if (!confirm('确定注销? 将清除本机登录态 (身份证明保留, 不影响频道/连接)。')) return;
2896
2897
  try { await api.post('/api/auth/logout', {}); await loadMe(); } catch (e) { alert('注销失败: ' + (e.message || e)); }
2897
2898
  });
2898
2899
  $('#btn-add').addEventListener('click', addFriend);
@@ -2909,8 +2910,8 @@
2909
2910
  if (first) openTradeCall(first); else openTradeHistory();
2910
2911
  });
2911
2912
  $('#item-p2p-id').addEventListener('click', async () => {
2912
- try { const net = await api.get('/api/network/status'); const p2p = net && net.nodeId; alert('P2P ID (通信ID, ≠ DID):\n' + (p2p || '未连接')); }
2913
- catch (e) { alert('P2P ID: 获取失败'); }
2913
+ try { const net = await api.get('/api/network/status'); const p2p = net && net.nodeId; alert('连接标识 (本机通信编号):\n' + (p2p || '未连接')); }
2914
+ catch (e) { alert('连接标识: 获取失败'); }
2914
2915
  });
2915
2916
  // #1 一键入网 (全球智能体网络): 点一下 → 默认 prompt 交给智能体执行
2916
2917
  const joinGlobalBtn = $('#item-join-global');
@@ -2953,6 +2954,14 @@
2953
2954
  if (nearbyRefresh) nearbyRefresh.addEventListener('click', () => loadNearbyList(_nearbyMode));
2954
2955
  const nearbyClose = $('#nearby-close');
2955
2956
  if (nearbyClose) nearbyClose.addEventListener('click', () => hideSheet('#nearby-sheet'));
2957
+ // 2026-10-16 (leo: 首页卡片动态传送): P2P 收到附近卡 → 即时刷新首页 (不等 45s 轮询)
2958
+ try {
2959
+ window.BolloonCore && window.BolloonCore.events && window.BolloonCore.events.subscribe((m) => {
2960
+ if (m && m.type === 'world-card') loadWorldFeed();
2961
+ });
2962
+ } catch (e) { /* 事件订阅失败不影响其它功能 */ }
2963
+ // 连上对端即要一次卡 (动态传送主动触发; 无 peer 静默)
2964
+ api.get('/api/world/cards/request').then(() => {}).catch(() => {});
2956
2965
  // 微信息 (x402 付费信息): 详情 → 购买并验真 / 只看元数据
2957
2966
  const x402Buy = $('#x402-buy');
2958
2967
  if (x402Buy) x402Buy.addEventListener('click', () => buyX402Info());
@@ -72,7 +72,15 @@ export function registerWorldRoutes(app) {
72
72
  const minScore = Number(minScoreRaw);
73
73
  const limitRaw = typeof req.query.limit === 'string' ? req.query.limit : '50';
74
74
  const limit = Math.max(1, Math.min(200, parseInt(limitRaw, 10) || 50));
75
- const r = await scanOpportunities({ minScore: Number.isFinite(minScore) ? minScore : 0.4, limit });
75
+ // 2026-10-16: 通讯录卡片 — dids=<did1>,<did2> 只返回这些 provider 投递的机会 (手机号通讯录联系人)
76
+ const providerDids = typeof req.query.dids === 'string'
77
+ ? req.query.dids.split(',').map((d) => d.trim()).filter(Boolean)
78
+ : [];
79
+ const r = await scanOpportunities({
80
+ minScore: Number.isFinite(minScore) ? minScore : 0.4,
81
+ limit,
82
+ providerDids: providerDids.length ? providerDids : undefined,
83
+ });
76
84
  if (!r.ok)
77
85
  return json(res, 500, { ok: false, error: r.error });
78
86
  json(res, 200, { ok: true, count: r.results.length, minScore, opportunities: r.results });