@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.
- package/dist/agents/ha-daemon.js +85 -0
- package/dist/agents/ha-supervisor.js +146 -0
- package/dist/agents/memory-store.js +25 -0
- package/dist/agents/opportunity-match.js +15 -3
- package/dist/agents/pi-sdk-session-manager.js +8 -0
- package/dist/agents/pi-sdk.js +67 -494
- package/dist/bootstrap/context-collector.js +65 -3
- package/dist/bootstrap/persona-loader.js +26 -0
- package/dist/electron-build/agents/memory-store.js +26 -0
- package/dist/electron-build/agents/memory-store.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-manager.js +8 -0
- package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +65 -492
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/bootstrap/context-collector.js +66 -3
- package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
- package/dist/electron-build/bootstrap/persona-loader.js +27 -0
- package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
- package/dist/electron-build/llm/pi-ai.js +16 -3
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/llm/system-prompt/registry.js +8 -6
- package/dist/electron-build/llm/system-prompt/registry.js.map +1 -1
- package/dist/ios/bolloon-web.json +3 -3
- package/dist/ios/index.html +19 -4
- package/dist/ios/mobile-agent.js +267 -41
- package/dist/ios/mobile-contacts.js +0 -4
- package/dist/ios/mobile-core.js +806 -30
- package/dist/ios/mobile-memory.js +317 -0
- package/dist/ios/mobile.html +19 -4
- package/dist/ios/mobile.js +144 -30
- package/dist/ios/routes-contacts.js +94 -1
- package/dist/ios/routes-world.js +9 -1
- package/dist/ios/server.js +51 -40
- package/dist/ios/sw.js +1 -1
- package/dist/kernel/gate-scan.js +1 -1
- package/dist/kernel/ha/ha-arbiter.js +177 -0
- package/dist/kernel/ha/ha-guard.js +151 -0
- package/dist/kernel/index.js +3 -0
- package/dist/kernel/plan.js +5 -5
- package/dist/kernel/roster.js +3 -3
- package/dist/llm/pi-ai.js +16 -3
- package/dist/llm/system-prompt/registry.js +8 -6
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/mobile-agent.js +265 -44
- package/dist/web/mobile-core.js +681 -32
- package/dist/web/mobile-memory.js +317 -0
- package/dist/web/mobile.html +4 -4
- package/dist/web/mobile.js +36 -27
- package/dist/web/routes-world.js +9 -1
- package/dist/web/server.js +50 -39
- package/dist/web/sw.js +1 -1
- 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
|
+
}
|
package/dist/ios/mobile.html
CHANGED
|
@@ -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
|
|
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"
|
|
82
|
+
<div class="section-label">网络连接</div>
|
|
83
83
|
<div class="list" id="p2p-status"></div>
|
|
84
|
-
<div class="section-label"
|
|
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
|
|
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>
|
|
@@ -224,6 +224,7 @@
|
|
|
224
224
|
<div class="sheet" id="addfriend-sheet" hidden>
|
|
225
225
|
<div class="sheet-inner sheet-inner-choices">
|
|
226
226
|
<div class="sheet-title">连接好友</div>
|
|
227
|
+
<button class="sheet-choice" id="choice-phone"><svg class="ico" viewBox="0 0 24 24"><path d="M5 4h4l1.5 4.5L8 10a12 12 0 0 0 6 6l1.5-2.5L20 15v4a1.5 1.5 0 0 1-1.6 1.5A17 17 0 0 1 3.5 5.6 1.5 1.5 0 0 1 5 4z"/></svg> 手机号搜索添加</button>
|
|
227
228
|
<button class="sheet-choice" id="choice-nearby"><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> 附近的设备</button>
|
|
228
229
|
<button class="sheet-choice" id="choice-scan"><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> 扫码添加</button>
|
|
229
230
|
<button class="sheet-choice" id="choice-requests"><svg class="ico" viewBox="0 0 24 24"><path d="M6 4h9l4 4v12a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z"/><path d="M9 12h6M9 16h4"/></svg> 待处理申请</button>
|
|
@@ -232,6 +233,20 @@
|
|
|
232
233
|
</div>
|
|
233
234
|
</div>
|
|
234
235
|
|
|
236
|
+
<!-- 手机号搜索添加好友: 输入手机号 → 目录查账号 → 发好友申请 (2026-10-15) -->
|
|
237
|
+
<div class="sheet" id="phone-search-sheet" hidden>
|
|
238
|
+
<div class="sheet-inner">
|
|
239
|
+
<div class="sheet-title">手机号搜索添加</div>
|
|
240
|
+
<input id="phone-search-input" type="tel" inputmode="numeric" placeholder="输入对方手机号 (如 13812345678)" style="width:100%;padding:10px 12px;border-radius:8px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:15px;box-sizing:border-box">
|
|
241
|
+
<div id="phone-search-note" style="font-size:12px;color:var(--text-secondary);margin:8px 2px;line-height:1.6">通过手机号目录查对方账号: 对方需已在电脑端发布目录 (手机号 → DID 指针)。查不到 = 对方未公开, 与不存在同义。</div>
|
|
242
|
+
<div id="phone-search-result" style="display:none;margin-top:8px"></div>
|
|
243
|
+
<div style="display:flex;gap:8px;margin-top:12px">
|
|
244
|
+
<button class="sheet-choice" id="phone-search-do" style="flex:1">搜索</button>
|
|
245
|
+
<button class="sheet-choice sheet-cancel" id="phone-search-close" style="flex:1">关闭</button>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
235
250
|
<!-- 加入网络: 点按式 sheet (扫一扫 / 附近设备 / 手动粘贴兜底) -->
|
|
236
251
|
<div class="sheet" id="network-sheet" hidden>
|
|
237
252
|
<div class="sheet-inner sheet-inner-choices">
|