@bolloon/bolloon-agent 0.4.22 → 0.4.23
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/gateway-join.js +271 -0
- package/dist/agents/pi-sdk-tools.js +75 -2
- package/dist/agents/pi-sdk.js +6 -3
- package/dist/llm/pi-ai.js +35 -2
- package/dist/network/agent-network.js +78 -6
- package/dist/web/mobile-core.js +17 -0
- package/dist/web/mobile.css +24 -0
- package/dist/web/mobile.html +23 -14
- package/dist/web/mobile.js +439 -45
- package/dist/web/server.js +67 -9
- package/dist/web/sw.js +26 -2
- package/package.json +1 -1
package/dist/web/mobile.js
CHANGED
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
});
|
|
70
70
|
} catch (e) {}
|
|
71
71
|
|
|
72
|
-
const TITLES = { main: '首页', network: '网络', me: '我' };
|
|
72
|
+
const TITLES = { main: '首页', friends: '好友', network: '网络', me: '我' };
|
|
73
73
|
let currentTab = 'main';
|
|
74
74
|
function switchTab(tab) {
|
|
75
75
|
currentTab = tab;
|
|
@@ -78,7 +78,8 @@
|
|
|
78
78
|
$('#topbar-title').textContent = TITLES[tab] || '会话';
|
|
79
79
|
const cs = $('#btn-create-session'); if (cs) cs.hidden = tab !== 'main';
|
|
80
80
|
const ta = $('#topbar-actions'); if (ta) ta.hidden = tab === 'me'; // 我 页不显示 加号/刷新
|
|
81
|
-
if (tab === '
|
|
81
|
+
if (tab === 'friends') { loadContacts(); loadP2PStatus(); }
|
|
82
|
+
if (tab === 'network') { loadAgentControl(); loadApprovals(); loadNetMembers(); loadAgentServices(); loadX402Info(); }
|
|
82
83
|
if (tab === 'main') { loadAgentCovers(); }
|
|
83
84
|
window.__mobileTouch?.('tab', tab);
|
|
84
85
|
}
|
|
@@ -134,30 +135,135 @@
|
|
|
134
135
|
} catch (e) { /* 忽略 */ }
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
// 智能体控制 = MCP 控制 + Skills 控制 (不是屏幕触控!)
|
|
139
|
+
// - MCP 工具: 真正的工具调用 (gateway_status / join / register …)
|
|
140
|
+
// - Skills: 电脑端 ~/.bolloon/skills/ 同步下来的本机技能
|
|
141
|
+
// 屏幕触控(无障碍) 挪到 设置 → 无障碍服务 (屏幕触控)。
|
|
142
|
+
function mkOverlayPage(id, title, inner) {
|
|
143
|
+
const page = document.createElement('div');
|
|
144
|
+
page.className = 'chat-page';
|
|
145
|
+
page.id = id;
|
|
146
|
+
page.style.zIndex = '70';
|
|
147
|
+
page.innerHTML = `<div class="chat-topbar">
|
|
148
|
+
<button class="icon-btn" id="${id}-back">←</button>
|
|
149
|
+
<div style="flex:1;font-weight:600">${escapeHtml(title)}</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div id="${id}-body" style="padding:12px;flex:1;min-height:0;overflow:auto">${inner}</div>`;
|
|
152
|
+
document.body.appendChild(page);
|
|
153
|
+
page.querySelector(`#${id}-back`).addEventListener('click', () => page.remove());
|
|
154
|
+
return page;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function loadAgentControl() {
|
|
158
|
+
const box = $('#agent-control');
|
|
139
159
|
if (!box) return;
|
|
140
|
-
|
|
160
|
+
const iconMcp = '<svg class="ico" viewBox="0 0 24 24"><path d="M14.5 4.5a3.5 3.5 0 0 0-4.9 4.2L4 14.3V20h5.7l5.6-5.6a3.5 3.5 0 0 0 4.2-4.9l-2.4 2.4-2.1-2.1z"/></svg>';
|
|
161
|
+
const iconSkill = '<svg class="ico" viewBox="0 0 24 24"><path d="M5 4h11l3 3v13H5z"/><path d="M9 12h6M9 16h4M9 8h4"/></svg>';
|
|
162
|
+
let tools = []; let skills = [];
|
|
163
|
+
try { tools = (await api.get('/api/mcp/tools')) || []; } catch (e) { tools = []; }
|
|
164
|
+
try { skills = (await api.get('/api/skills')) || []; } catch (e) { skills = []; }
|
|
165
|
+
const skillSub = skills.length
|
|
166
|
+
? skills.slice(0, 3).map((s) => '/' + String(s.name || '')).join(' ')
|
|
167
|
+
: '尚无本机技能 —— 点开后从电脑端同步';
|
|
168
|
+
box.innerHTML = `
|
|
169
|
+
<div class="list-item" id="item-mcp"><span class="list-icon">${iconMcp}</span>
|
|
170
|
+
<span style="flex:1"><span style="display:block">MCP 工具 · ${tools.length} 个</span>
|
|
171
|
+
<span class="conv-preview" style="display:block">点开读工具说明并调用(gateway_status / join / register / call)</span></span>
|
|
172
|
+
<span class="list-arrow">›</span></div>
|
|
173
|
+
<div class="list-item" id="item-skills"><span class="list-icon">${iconSkill}</span>
|
|
174
|
+
<span style="flex:1"><span style="display:block">Skills · ${skills.length} 个</span>
|
|
175
|
+
<span class="conv-preview" style="display:block">${escapeHtml(skillSub)}</span></span>
|
|
176
|
+
<span class="list-arrow">›</span></div>`;
|
|
177
|
+
const mcpEl = $('#item-mcp'); if (mcpEl) mcpEl.addEventListener('click', () => void openMcpPage(tools));
|
|
178
|
+
const skEl = $('#item-skills'); if (skEl) skEl.addEventListener('click', () => void openSkillsPage());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function openMcpPage(toolsIn) {
|
|
182
|
+
if ($('#mcp-page')) return;
|
|
183
|
+
const page = mkOverlayPage('mcp-page', 'MCP 工具', '<div style="font-size:12px;color:var(--text-muted)">读取中…</div>');
|
|
184
|
+
let tools = toolsIn;
|
|
185
|
+
if (!Array.isArray(tools) || !tools.length) {
|
|
186
|
+
try { tools = (await api.get('/api/mcp/tools')) || []; } catch (e) { tools = []; }
|
|
187
|
+
}
|
|
188
|
+
const body = page.querySelector('#mcp-page-body');
|
|
189
|
+
if (!tools.length) { body.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">没有可用的 MCP 工具</div>'; return; }
|
|
190
|
+
body.innerHTML = tools.map((t, i) => `<div class="conv-item" data-ti="${i}">
|
|
191
|
+
<div class="conv-avatar">${escapeHtml(String(t.name || 'M').charAt(0))}</div>
|
|
192
|
+
<div class="conv-body"><div class="conv-name">${escapeHtml(String(t.name || ''))}</div>
|
|
193
|
+
<div class="conv-preview">${escapeHtml(String(t.description || ''))}</div></div></div>`).join('')
|
|
194
|
+
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:12px;line-height:1.7">点一下 = 让本机智能体调用该工具(gateway_* 需要先加入 Agent 网络)</div>';
|
|
195
|
+
body.querySelectorAll('[data-ti]').forEach((el) => el.addEventListener('click', () => void runMcpTool(tools[Number(el.dataset.ti)])));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function runMcpTool(t) {
|
|
199
|
+
const name = String((t && t.name) || '');
|
|
200
|
+
if (!name) return;
|
|
201
|
+
let args = {};
|
|
141
202
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
203
|
+
if (name === 'gateway_join') {
|
|
204
|
+
const link = prompt('gateway_join 需要网络链接:\norbitdb://… 或 ipns://… 或 https://…/registry', '');
|
|
205
|
+
if (!link) return;
|
|
206
|
+
args = { link };
|
|
207
|
+
} else if (name === 'gateway_register') {
|
|
208
|
+
const svc = prompt('注册为服务提供者,填一个服务名(例如 手机端助手):', '');
|
|
209
|
+
if (!svc) return;
|
|
210
|
+
let agentId = '';
|
|
211
|
+
try { const id2 = await api.get('/api/auth/status'); agentId = (id2 && (id2.agentId || id2.did)) || ''; } catch (e) { agentId = ''; }
|
|
212
|
+
if (!agentId) { alert('还没有身份(我 → 登录),无法注册为服务提供者'); return; }
|
|
213
|
+
args = { self: { agentId, name: svc, service: { name: svc } } };
|
|
214
|
+
} else if (name === 'gateway_call') {
|
|
215
|
+
const svc = prompt('要调用的服务(名称或 agentId):', '');
|
|
216
|
+
if (!svc) return;
|
|
217
|
+
args = { service: svc };
|
|
218
|
+
}
|
|
219
|
+
const r = await api.post('/api/mcp/call', { name, args });
|
|
220
|
+
const ok = r && r.ok !== false;
|
|
221
|
+
alert(`${name} ${ok ? '✔' : '✘'}\n\n${(r && r.output) || JSON.stringify(r)}`);
|
|
222
|
+
void loadAgentControl();
|
|
223
|
+
} catch (e) {
|
|
224
|
+
alert(`${name} 调用失败:${(e && e.message) || e}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function openSkillsPage() {
|
|
229
|
+
if ($('#skills-page')) return;
|
|
230
|
+
const page = mkOverlayPage('skills-page', 'Skills', '<div style="font-size:12px;color:var(--text-muted)">读取中…</div>');
|
|
231
|
+
const bar = document.createElement('div');
|
|
232
|
+
bar.style.cssText = 'padding:0 12px 14px';
|
|
233
|
+
bar.innerHTML = '<button id="skills-sync" style="width:100%;padding:11px;border-radius:10px;border:1px solid var(--border);background:var(--bg-hover);color:var(--accent);font-size:14px">从电脑端同步</button>';
|
|
234
|
+
page.appendChild(bar);
|
|
235
|
+
const render = async () => {
|
|
236
|
+
let skills = []; let err = '';
|
|
237
|
+
try { skills = (await api.get('/api/skills')) || []; } catch (e) { skills = []; err = (e && e.message) || ''; }
|
|
238
|
+
const body = page.querySelector('#skills-page-body');
|
|
239
|
+
if (!body) return;
|
|
240
|
+
if (!skills.length) {
|
|
241
|
+
body.innerHTML = `<div style="padding:18px 8px;text-align:center;color:var(--text-muted);line-height:1.8">
|
|
242
|
+
还没有技能列表<br><span style="font-size:12px">电脑端 ~/.bolloon/skills/ 下的 skills,会在「从电脑端同步」后出现在这里</span>
|
|
243
|
+
${err ? `<div style="font-size:11px;margin-top:8px">(读取失败:${escapeHtml(err)})</div>` : ''}</div>`;
|
|
146
244
|
return;
|
|
147
245
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
246
|
+
body.innerHTML = skills.map((s, i) => `<div class="conv-item" data-sk="${i}">
|
|
247
|
+
<div class="conv-avatar">${escapeHtml(String(s.name || 'S').charAt(0).toUpperCase())}</div>
|
|
248
|
+
<div class="conv-body"><div class="conv-name">/${escapeHtml(String(s.name || ''))}</div>
|
|
249
|
+
<div class="conv-preview">${escapeHtml(String(s.description || '(无说明)').slice(0, 60))}</div></div></div>`).join('')
|
|
250
|
+
+ `<div style="font-size:11px;color:var(--text-muted);margin-top:12px">共 ${skills.length} 个 · 来自电脑端 ~/.bolloon/skills/</div>`;
|
|
251
|
+
body.querySelectorAll('[data-sk]').forEach((el) => el.addEventListener('click', () => {
|
|
252
|
+
const s = skills[Number(el.dataset.sk)] || {};
|
|
253
|
+
alert(`/${s.name || ''}\n\n${s.description || '(无说明)'}`);
|
|
254
|
+
}));
|
|
255
|
+
};
|
|
256
|
+
await render();
|
|
257
|
+
const btn = page.querySelector('#skills-sync');
|
|
258
|
+
if (btn) btn.addEventListener('click', async () => {
|
|
259
|
+
btn.disabled = true; const old = btn.textContent; btn.textContent = '同步中…';
|
|
260
|
+
try {
|
|
261
|
+
const r = await api.post('/api/desktop/sync');
|
|
262
|
+
if (r && r.ok === false) { alert('同步失败:' + (r.error || '未知错误')); }
|
|
263
|
+
else { alert('同步完成' + (r && r.counts ? ':' + JSON.stringify(r.counts) : '')); }
|
|
264
|
+
} catch (e) { alert('同步失败:' + ((e && e.message) || e)); }
|
|
265
|
+
finally { btn.disabled = false; btn.textContent = old; await render(); }
|
|
266
|
+
});
|
|
161
267
|
}
|
|
162
268
|
|
|
163
269
|
async function loadApprovals() {
|
|
@@ -686,6 +792,154 @@
|
|
|
686
792
|
let streamingBubble = null;
|
|
687
793
|
let chatLoadPromise = Promise.resolve(); // openChat 的首次历史加载 (供一键入网等自动发消息等它完成, 免被清屏抹掉)
|
|
688
794
|
|
|
795
|
+
function fmtAgo(ts) {
|
|
796
|
+
if (!ts) return '';
|
|
797
|
+
const d = Date.now() - ts;
|
|
798
|
+
if (d < 60e3) return '刚刚';
|
|
799
|
+
if (d < 3600e3) return Math.floor(d / 60e3) + ' 分钟前';
|
|
800
|
+
if (d < 86400e3) return Math.floor(d / 3600e3) + ' 小时前';
|
|
801
|
+
if (d < 7 * 86400e3) return Math.floor(d / 86400e3) + ' 天前';
|
|
802
|
+
return new Date(ts).toLocaleDateString();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// 左上角「索引」: 最近历史会话 (本机 IndexedDB 里的 sessions, 按更新时间倒序)
|
|
806
|
+
async function openIndexPanel() {
|
|
807
|
+
if ($('#index-page')) return;
|
|
808
|
+
const page = document.createElement('div');
|
|
809
|
+
page.className = 'chat-page';
|
|
810
|
+
page.id = 'index-page';
|
|
811
|
+
page.style.zIndex = '70';
|
|
812
|
+
page.innerHTML = `
|
|
813
|
+
<div class="chat-topbar">
|
|
814
|
+
<button class="icon-btn" id="index-back">←</button>
|
|
815
|
+
<div style="flex:1;font-weight:600">索引 · 最近会话</div>
|
|
816
|
+
<button class="icon-btn" id="index-settings" title="设置">⚙</button>
|
|
817
|
+
</div>
|
|
818
|
+
<div id="index-body" style="padding:12px;flex:1;min-height:0;overflow:auto">
|
|
819
|
+
<div style="font-size:12px;color:var(--text-muted)">读取最近会话…</div>
|
|
820
|
+
</div>`;
|
|
821
|
+
document.body.appendChild(page);
|
|
822
|
+
$('#index-back').addEventListener('click', () => page.remove());
|
|
823
|
+
$('#index-settings').addEventListener('click', () => { page.remove(); openSettings(); });
|
|
824
|
+
|
|
825
|
+
let snap = null;
|
|
826
|
+
try { snap = await api.get('/api/data/snapshot'); } catch (e) { snap = null; }
|
|
827
|
+
const channels = (snap && snap.channels) || [];
|
|
828
|
+
const sessions = (snap && snap.sessions) || [];
|
|
829
|
+
const byId = new Map();
|
|
830
|
+
channels.forEach((c) => byId.set(c.id, c));
|
|
831
|
+
const rows = sessions.map((s) => {
|
|
832
|
+
const msgs = s.messages || [];
|
|
833
|
+
const last = msgs[msgs.length - 1];
|
|
834
|
+
return {
|
|
835
|
+
ch: byId.get(s.channelId) || { id: s.channelId, name: '(会话已删除)' },
|
|
836
|
+
updatedAt: s.updatedAt || (last && last.ts) || 0,
|
|
837
|
+
count: msgs.length,
|
|
838
|
+
lastText: (last && last.content) || '(无消息)',
|
|
839
|
+
};
|
|
840
|
+
}).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
841
|
+
|
|
842
|
+
const head = `<div style="font-size:12px;color:var(--text-muted);margin:2px 0 10px">本机智能体 ${channels.length} 个 · 有历史会话 ${rows.length} 个</div>`;
|
|
843
|
+
const list = rows.length
|
|
844
|
+
? rows.map((r, i) => `<div class="conv-item" data-si="${i}">
|
|
845
|
+
<div class="conv-avatar">${escapeHtml(String(r.ch.name || 'A').charAt(0))}</div>
|
|
846
|
+
<div class="conv-body">
|
|
847
|
+
<div class="conv-name">${escapeHtml(String(r.ch.name || r.ch.id))}</div>
|
|
848
|
+
<div class="conv-preview">${escapeHtml(String(r.lastText).slice(0, 40))}</div>
|
|
849
|
+
</div>
|
|
850
|
+
<span style="font-size:11px;color:var(--text-muted);text-align:right;flex:0 0 auto">${escapeHtml(fmtAgo(r.updatedAt))}<br>${r.count} 条</span>
|
|
851
|
+
</div>`).join('')
|
|
852
|
+
: '<div style="padding:24px;text-align:center;color:var(--text-muted)">还没有历史会话<br><span style="font-size:12px">从下面的 + 新建一个智能体开始</span></div>';
|
|
853
|
+
const body = $('#index-body');
|
|
854
|
+
if (body) {
|
|
855
|
+
body.innerHTML = head + list;
|
|
856
|
+
body.querySelectorAll('[data-si]').forEach((el) => el.addEventListener('click', () => {
|
|
857
|
+
const r = rows[Number(el.dataset.si)];
|
|
858
|
+
page.remove();
|
|
859
|
+
openChat(r.ch);
|
|
860
|
+
}));
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// 右上角「搜索」: 本机智能体 + 好友 + 全局智能体 (协议发现) 一起搜
|
|
865
|
+
async function openSearch() {
|
|
866
|
+
if ($('#search-page')) return;
|
|
867
|
+
const page = document.createElement('div');
|
|
868
|
+
page.className = 'chat-page';
|
|
869
|
+
page.id = 'search-page';
|
|
870
|
+
page.style.zIndex = '70';
|
|
871
|
+
page.innerHTML = `
|
|
872
|
+
<div class="chat-topbar">
|
|
873
|
+
<button class="icon-btn" id="search-back">←</button>
|
|
874
|
+
<div style="flex:1;font-weight:600">搜索</div>
|
|
875
|
+
</div>
|
|
876
|
+
<div style="padding:12px;display:flex;flex-direction:column;gap:10px;flex:1;min-height:0">
|
|
877
|
+
<input id="search-input" type="search" autocomplete="off" placeholder="搜智能体 / 好友:名称、DID、节点 ID"
|
|
878
|
+
style="padding:10px 12px;border:1px solid var(--border);border-radius:10px;background:var(--bg-hover);color:var(--text);font-size:14px">
|
|
879
|
+
<div id="search-hint" style="font-size:12px;color:var(--text-muted)">正在建立索引…</div>
|
|
880
|
+
<div id="search-results" style="flex:1;overflow:auto;-webkit-overflow-scrolling:touch"></div>
|
|
881
|
+
</div>`;
|
|
882
|
+
document.body.appendChild(page);
|
|
883
|
+
$('#search-back').addEventListener('click', () => page.remove());
|
|
884
|
+
|
|
885
|
+
let channels = []; let peers = []; let services = [];
|
|
886
|
+
try { channels = (await api.get('/channels')) || []; } catch (e) { channels = []; }
|
|
887
|
+
try { const p = await api.get('/api/peers'); peers = Array.isArray(p) ? p : ((p && p.peers) || []); } catch (e) { peers = []; }
|
|
888
|
+
try { const d = await api.get('/api/social/discover'); services = (d && d.services) || []; } catch (e) { services = []; }
|
|
889
|
+
|
|
890
|
+
const rows = [
|
|
891
|
+
...channels.map((c) => ({
|
|
892
|
+
tag: '本机智能体', name: c.name || c.agentId || c.id, sub: c.id || '',
|
|
893
|
+
hay: [c.name, c.id, c.agentId, c.did, c.description].filter(Boolean).join(' '),
|
|
894
|
+
act: () => { page.remove(); openChat(c); },
|
|
895
|
+
})),
|
|
896
|
+
...peers.map((p) => ({
|
|
897
|
+
tag: '好友', name: p.name || String(p.publicKey || p.id || '好友').slice(0, 12),
|
|
898
|
+
sub: String(p.publicKey || p.id || p.address || '').slice(0, 28),
|
|
899
|
+
hay: [p.name, p.publicKey, p.id, p.address, p.did].filter(Boolean).join(' '),
|
|
900
|
+
act: () => alert(`好友信息\n名称: ${p.name || '(未命名)'}\n节点ID: ${p.publicKey || p.id || '—'}\n地址: ${p.address || '—'}`),
|
|
901
|
+
})),
|
|
902
|
+
...services.map((s) => ({
|
|
903
|
+
tag: '全局智能体',
|
|
904
|
+
name: (s.service && s.service.name) || s.name || s.agentId || 'agent',
|
|
905
|
+
sub: (s.service && s.service.description) || s.description || '',
|
|
906
|
+
hay: [(s.service && s.service.name), s.serviceName, s.name, s.agentId, s.description, s.did].filter(Boolean).join(' '),
|
|
907
|
+
act: () => { page.remove(); openTradeCall(s); },
|
|
908
|
+
})),
|
|
909
|
+
];
|
|
910
|
+
rows.forEach((r) => { r.hay = String(r.hay || '').toLowerCase(); });
|
|
911
|
+
|
|
912
|
+
const hint = $('#search-hint');
|
|
913
|
+
const box = $('#search-results');
|
|
914
|
+
const render = (q) => {
|
|
915
|
+
const k = String(q || '').trim().toLowerCase();
|
|
916
|
+
const hits = rows.filter((r) => !k || r.hay.includes(k) || r.name.toLowerCase().includes(k));
|
|
917
|
+
if (hint) hint.innerHTML = `本机智能体 ${channels.length} · 好友 ${peers.length} · 全局智能体 ${services.length}` + (k ? ` · 命中 ${hits.length}` : '');
|
|
918
|
+
if (!box) return;
|
|
919
|
+
if (!hits.length) {
|
|
920
|
+
box.innerHTML = `<div style="padding:24px;text-align:center;color:var(--text-muted);font-size:13px">没有匹配「${escapeHtml(String(q))}」的智能体或好友</div>`;
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
let html = '';
|
|
924
|
+
let lastTag = '';
|
|
925
|
+
hits.forEach((r, i) => {
|
|
926
|
+
if (r.tag !== lastTag) { html += `<div class="section-label">${escapeHtml(r.tag)}</div>`; lastTag = r.tag; }
|
|
927
|
+
html += `<div class="conv-item" data-ri="${i}">
|
|
928
|
+
<div class="conv-avatar">${escapeHtml(String(r.name || 'A').charAt(0))}</div>
|
|
929
|
+
<div class="conv-body">
|
|
930
|
+
<div class="conv-name">${escapeHtml(String(r.name))}</div>
|
|
931
|
+
<div class="conv-preview">${escapeHtml(String(r.sub).slice(0, 48))}</div>
|
|
932
|
+
</div>
|
|
933
|
+
</div>`;
|
|
934
|
+
});
|
|
935
|
+
box.innerHTML = html;
|
|
936
|
+
box.querySelectorAll('[data-ri]').forEach((el) => el.addEventListener('click', () => hits[Number(el.dataset.ri)].act()));
|
|
937
|
+
};
|
|
938
|
+
render('');
|
|
939
|
+
const input = $('#search-input');
|
|
940
|
+
if (input) { input.addEventListener('input', () => render(input.value)); input.focus(); }
|
|
941
|
+
}
|
|
942
|
+
|
|
689
943
|
function openChat(ch) {
|
|
690
944
|
activeChannel = ch;
|
|
691
945
|
const page = document.createElement('div');
|
|
@@ -1455,9 +1709,10 @@
|
|
|
1455
1709
|
<div style="padding:12px">
|
|
1456
1710
|
<div class="conv-item" id="api-config-item"><span class="list-icon">${ICONS.chip}</span><span>API 配置</span><span class="list-arrow">›</span></div>
|
|
1457
1711
|
<div class="conv-item" id="theme-toggle"><span class="list-icon" id="theme-icon">${ICONS.themeAuto}</span><span id="theme-text">跟随系统</span></div>
|
|
1458
|
-
<div class="conv-item" id="settings-
|
|
1712
|
+
<div class="conv-item" id="settings-data"><span class="list-icon">${ICONS.chip}</span><span style="flex:1;min-width:0"><span style="display:block">本机数据</span><span class="conv-preview" style="display:block">读取中…</span></span><span class="list-arrow">›</span></div>
|
|
1459
1713
|
<div class="conv-item" id="settings-desktop"><span class="list-icon">${ICONS.globe}</span><span>电脑端同步</span><span class="list-arrow">›</span></div>
|
|
1460
1714
|
<div class="conv-item" id="settings-chain"><span class="list-icon">${ICONS.chip}</span><span>链上配置 (RPC/网络)</span><span class="list-arrow">›</span></div>
|
|
1715
|
+
<div class="conv-item" id="settings-accessibility"><span class="list-icon">${ICONS.globe}</span><span>无障碍服务 (屏幕触控)</span><span class="list-arrow">›</span></div>
|
|
1461
1716
|
<div class="conv-item" id="settings-ipfs"><span class="list-icon">${ICONS.chip}</span><span>IPFS 存储</span><span class="list-arrow">›</span></div>
|
|
1462
1717
|
<div class="conv-item" id="settings-helia"><span class="list-icon">${ICONS.globe}</span><span>本机 IPFS 节点</span><span class="list-arrow">›</span></div>
|
|
1463
1718
|
<div class="conv-item" id="settings-selfcard"><span class="list-icon">${ICONS.chip}</span><span id="selfcard-text">显示本机卡片: 开</span></div>
|
|
@@ -1471,7 +1726,36 @@
|
|
|
1471
1726
|
const next = currentTheme === 'auto' ? 'light' : (currentTheme === 'light' ? 'dark' : 'auto');
|
|
1472
1727
|
applyTheme(next, true);
|
|
1473
1728
|
});
|
|
1474
|
-
|
|
1729
|
+
// 无障碍服务: 屏幕触控 (电脑端 phone.tap / phone.swipe) 的前提 — 与 MCP/Skills 控制无关
|
|
1730
|
+
$('#settings-accessibility').addEventListener('click', async () => {
|
|
1731
|
+
const cap = window.Capacitor;
|
|
1732
|
+
const bridge = cap && cap.Plugins && cap.Plugins.RokidBridge;
|
|
1733
|
+
if (!bridge) { alert('仅真机可用:App 内可把屏幕触控能力授权给电脑端'); return; }
|
|
1734
|
+
let st = {};
|
|
1735
|
+
try { st = (await bridge.touchStatus()) || {}; } catch (e) { st = {}; }
|
|
1736
|
+
if (st.ready) { alert('触控已就绪:电脑端可发 phone.tap / phone.swipe 操作这台手机'); return; }
|
|
1737
|
+
try { await bridge.openAccessibilitySettings(); }
|
|
1738
|
+
catch (e) { alert('请手动开启:设置 → 辅助功能 → 已安装的服务 → Bolloon Agent'); }
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
// 本机数据: 直接读 IndexedDB 快照 (智能体/会话/消息都在本机, 重开 App 不会丢)
|
|
1742
|
+
const dataEl = $('#settings-data');
|
|
1743
|
+
if (dataEl) {
|
|
1744
|
+
const subEl = dataEl.querySelector('.conv-preview');
|
|
1745
|
+
void (async () => {
|
|
1746
|
+
try {
|
|
1747
|
+
const snap = await api.get('/api/data/snapshot');
|
|
1748
|
+
const chs = (snap && snap.channels) || [];
|
|
1749
|
+
const ses = (snap && snap.sessions) || [];
|
|
1750
|
+
const msgs = ses.reduce((n, s) => n + ((s.messages || []).length), 0);
|
|
1751
|
+
if (subEl) subEl.textContent = `已保存 ${chs.length} 个智能体 · ${ses.length} 个会话 · ${msgs} 条消息(本机 IndexedDB)`;
|
|
1752
|
+
} catch (e) { if (subEl) subEl.textContent = '读取失败: ' + ((e && e.message) || e); }
|
|
1753
|
+
})();
|
|
1754
|
+
dataEl.addEventListener('click', () => {
|
|
1755
|
+
const sp = $('#settings-page'); if (sp) sp.remove();
|
|
1756
|
+
void openIndexPanel();
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1475
1759
|
$('#settings-desktop').addEventListener('click', openDesktopSync);
|
|
1476
1760
|
// 本机卡片显示开关 (移除后从这里恢复)
|
|
1477
1761
|
const drawSelfCardToggle = () => {
|
|
@@ -1497,14 +1781,25 @@
|
|
|
1497
1781
|
}
|
|
1498
1782
|
|
|
1499
1783
|
// === API 配置 (LLM 供应商) ===
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1784
|
+
// 注意: 手机端 RemoteLlm 走 OpenAI 兼容协议 (baseUrl + /chat/completions) —— 新增 provider 必须是
|
|
1785
|
+
// OpenAI 兼容端点 (gemini 用 /v1beta/openai, 智谱 v4 / dashscope compatible-mode 都兼容)。
|
|
1786
|
+
const LLM_PROVIDERS = [
|
|
1787
|
+
{ id: 'deepseek', label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', model: 'deepseek-chat' },
|
|
1788
|
+
{ id: 'openai', label: 'OpenAI', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o-mini' },
|
|
1789
|
+
{ id: 'anthropic', label: 'Anthropic', baseUrl: 'https://api.anthropic.com/v1', model: 'claude-3-5-sonnet-latest' },
|
|
1790
|
+
{ id: 'gemini', label: 'Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai', model: 'gemini-3.5-flash' },
|
|
1791
|
+
{ id: 'xai', label: 'Grok', baseUrl: 'https://api.x.ai/v1', model: 'grok-2-latest' },
|
|
1792
|
+
{ id: 'qwen', label: '通义千问', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus' },
|
|
1793
|
+
{ id: 'zhipu', label: '智谱 GLM', baseUrl: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-4-plus' },
|
|
1794
|
+
{ id: 'moonshot', label: 'Kimi', baseUrl: 'https://api.moonshot.cn/v1', model: 'moonshot-v1-8k' },
|
|
1795
|
+
{ id: 'minimax', label: 'MiniMax', baseUrl: 'https://api.minimax.chat/v1', model: 'MiniMax-M2.7' },
|
|
1796
|
+
{ id: 'siliconflow', label: '硅基流动', baseUrl: 'https://api.siliconflow.cn/v1', model: 'deepseek-ai/DeepSeek-V3' },
|
|
1797
|
+
{ id: 'groq', label: 'Groq', baseUrl: 'https://api.groq.com/openai/v1', model: 'llama-3.3-70b-versatile' },
|
|
1798
|
+
{ id: 'openrouter', label: 'OpenRouter', baseUrl: 'https://openrouter.ai/api/v1', model: 'openai/gpt-4o-mini' },
|
|
1799
|
+
{ id: 'ollama', label: '本地 Ollama', baseUrl: 'http://localhost:11434/v1', model: 'qwen2.5:7b' },
|
|
1800
|
+
{ id: 'custom', label: '自定义', baseUrl: '', model: '' },
|
|
1801
|
+
];
|
|
1802
|
+
const LLM_BY_ID = LLM_PROVIDERS.reduce((m, p) => (m[p.id] = p, m), {});
|
|
1508
1803
|
async function openApiConfig() {
|
|
1509
1804
|
let cfg;
|
|
1510
1805
|
try { cfg = await api.get('/api/llm-config'); } catch { cfg = null; }
|
|
@@ -1520,10 +1815,11 @@
|
|
|
1520
1815
|
<div style="flex:1;font-weight:600">API 配置</div>
|
|
1521
1816
|
</div>
|
|
1522
1817
|
<div style="padding:12px;display:flex;flex-direction:column;gap:12px">
|
|
1523
|
-
<label style="font-size:13px;color:var(--text-secondary)"
|
|
1524
|
-
<
|
|
1525
|
-
${LLM_PROVIDERS.map((p) => `<
|
|
1526
|
-
</
|
|
1818
|
+
<label style="font-size:13px;color:var(--text-secondary)">供应商 <span id="api-provider-count" style="opacity:.55"></span></label>
|
|
1819
|
+
<div class="provider-chips" id="api-provider-chips">
|
|
1820
|
+
${LLM_PROVIDERS.map((p) => `<button type="button" class="provider-chip${p.id === provider ? ' active' : ''}" data-provider="${p.id}">${p.label}</button>`).join('')}
|
|
1821
|
+
</div>
|
|
1822
|
+
<div id="api-hint" style="font-size:12px;color:var(--text-muted)"></div>
|
|
1527
1823
|
<label style="font-size:13px;color:var(--text-secondary)">Base URL</label>
|
|
1528
1824
|
<input id="api-baseurl" placeholder="https://api.xxx.com/v1" value="${escapeHtml(pc.baseUrl || '')}" style="padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-hover);color:var(--text)">
|
|
1529
1825
|
<label style="font-size:13px;color:var(--text-secondary)">API Key</label>
|
|
@@ -1534,15 +1830,37 @@
|
|
|
1534
1830
|
</div>`;
|
|
1535
1831
|
document.body.appendChild(page);
|
|
1536
1832
|
$('#api-config-back').addEventListener('click', () => page.remove());
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1542
|
-
|
|
1833
|
+
// 供应商选择: 芯片式 (原来是一个原生 select, 小屏上难点、也看不出有哪些可选)
|
|
1834
|
+
let picked = LLM_BY_ID[provider] ? provider : 'deepseek';
|
|
1835
|
+
const fillFrom = (id) => {
|
|
1836
|
+
const saved = (cfg.providers && cfg.providers[id]) || {};
|
|
1837
|
+
const d = LLM_BY_ID[id] || {};
|
|
1838
|
+
$('#api-baseurl').value = saved.baseUrl || d.baseUrl || '';
|
|
1839
|
+
$('#api-model').value = saved.model || d.model || '';
|
|
1840
|
+
$('#api-key').value = saved.apiKey || '';
|
|
1841
|
+
const hint = $('#api-hint');
|
|
1842
|
+
if (hint) {
|
|
1843
|
+
hint.textContent = id === 'custom'
|
|
1844
|
+
? '自定义: 填任意 OpenAI 兼容的 baseUrl (/chat/completions)'
|
|
1845
|
+
: (saved.apiKey ? '该供应商已保存过 key(改完记得再点保存)'
|
|
1846
|
+
: '填官方文档里的 API key; 保存后本机智能体就走它');
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1849
|
+
const paintChips = () => {
|
|
1850
|
+
$$('#api-provider-chips .provider-chip').forEach((c) => c.classList.toggle('active', c.dataset.provider === picked));
|
|
1851
|
+
};
|
|
1852
|
+
const configured = Object.keys(cfg.providers || {}).filter((k) => cfg.providers[k] && cfg.providers[k].apiKey);
|
|
1853
|
+
$('#api-provider-count').textContent = configured.length ? `(已配 ${configured.length} 个: ${configured.join(' / ')})` : '';
|
|
1854
|
+
$('#api-provider-chips').addEventListener('click', (e) => {
|
|
1855
|
+
const b = e.target.closest && e.target.closest('.provider-chip');
|
|
1856
|
+
if (!b) return;
|
|
1857
|
+
picked = b.dataset.provider;
|
|
1858
|
+
paintChips();
|
|
1859
|
+
fillFrom(picked);
|
|
1543
1860
|
});
|
|
1861
|
+
fillFrom(picked);
|
|
1544
1862
|
$('#api-save').addEventListener('click', async () => {
|
|
1545
|
-
const p =
|
|
1863
|
+
const p = picked;
|
|
1546
1864
|
const next = (cfg && cfg.providers) ? cfg : { activeProvider: cfg.activeProvider, providers: {}, updatedAt: Date.now() };
|
|
1547
1865
|
next.activeProvider = p;
|
|
1548
1866
|
next.providers[p] = Object.assign({}, (next.providers[p] || {}), {
|
|
@@ -1876,11 +2194,13 @@
|
|
|
1876
2194
|
try { await api.post('/api/auth/logout', {}); await loadMe(); } catch (e) { alert('注销失败: ' + (e.message || e)); }
|
|
1877
2195
|
});
|
|
1878
2196
|
$('#btn-add').addEventListener('click', addFriend);
|
|
2197
|
+
const bIdx = $('#btn-index'); if (bIdx) bIdx.addEventListener('click', () => void openIndexPanel());
|
|
2198
|
+
const bSearch = $('#btn-search'); if (bSearch) bSearch.addEventListener('click', () => void openSearch());
|
|
1879
2199
|
const cs = $('#btn-create-session'); if (cs) cs.addEventListener('click', createSession);
|
|
1880
2200
|
const csScan = $('#choice-scan'); if (csScan) csScan.addEventListener('click', addFriendScan);
|
|
1881
2201
|
const csMan = $('#choice-manual'); if (csMan) csMan.addEventListener('click', addFriendManual);
|
|
1882
2202
|
const csCan = $('#choice-cancel'); if (csCan) csCan.addEventListener('click', () => hideSheet('#addfriend-sheet'));
|
|
1883
|
-
$('#item-p2p').addEventListener('click', () => { switchTab('
|
|
2203
|
+
$('#item-p2p').addEventListener('click', () => { switchTab('friends'); void loadContacts(); });
|
|
1884
2204
|
const itTrade = $('#item-trade');
|
|
1885
2205
|
if (itTrade) itTrade.addEventListener('click', async () => {
|
|
1886
2206
|
await loadAgentServices();
|
|
@@ -2363,7 +2683,7 @@
|
|
|
2363
2683
|
if (!msg || msg.type !== 'ui' || !msg.action) return;
|
|
2364
2684
|
const d = msg.data || {};
|
|
2365
2685
|
switch (msg.action) {
|
|
2366
|
-
case 'switchTab': if (d.tab && ['main', 'network', 'me'].includes(d.tab)) switchTab(d.tab); break;
|
|
2686
|
+
case 'switchTab': if (d.tab && ['main', 'friends', 'network', 'me'].includes(d.tab)) switchTab(d.tab); break;
|
|
2367
2687
|
case 'openSettings': openSettings(); break;
|
|
2368
2688
|
case 'showToast': alert(d.message || ''); break;
|
|
2369
2689
|
case 'goBack': closeCardDetail(); closeChat(); break;
|
|
@@ -2449,11 +2769,85 @@
|
|
|
2449
2769
|
} catch (e) {}
|
|
2450
2770
|
}
|
|
2451
2771
|
|
|
2772
|
+
// === 手势 (2026-09-14): 滑动切 tab / 点空白关弹窗 / 右滑返回上一层 ===
|
|
2773
|
+
// 优先级: 有浮层 → 右滑 = 返回上一层 (关浮层); 无浮层 → 左右滑 = 依次切 tab
|
|
2774
|
+
function topOverlay() {
|
|
2775
|
+
const all = $$('.crop-modal:not([hidden]), .sheet:not([hidden]), .chat-page:not([hidden]), .card-detail:not([hidden])');
|
|
2776
|
+
if (!all.length) return null;
|
|
2777
|
+
return all
|
|
2778
|
+
.map((el) => ({ el, z: parseFloat(getComputedStyle(el).zIndex) || 0 }))
|
|
2779
|
+
.sort((a, b) => a.z - b.z)
|
|
2780
|
+
.pop().el;
|
|
2781
|
+
}
|
|
2782
|
+
function closeOverlay(el) {
|
|
2783
|
+
if (!el) return false;
|
|
2784
|
+
if (el.classList.contains('sheet')) { el.hidden = true; return true; }
|
|
2785
|
+
if (el.classList.contains('card-detail')) {
|
|
2786
|
+
const b = el.querySelector('#detail-back'); if (b) { b.click(); return true; }
|
|
2787
|
+
el.hidden = true; return true;
|
|
2788
|
+
}
|
|
2789
|
+
if (el.classList.contains('crop-modal')) {
|
|
2790
|
+
const b = el.querySelector('[id$="-cancel"], [id$="-close"], .icon-btn');
|
|
2791
|
+
if (b) { b.click(); return true; }
|
|
2792
|
+
el.hidden = true; return true;
|
|
2793
|
+
}
|
|
2794
|
+
if (el.classList.contains('chat-page')) {
|
|
2795
|
+
const b = el.querySelector('.chat-topbar .icon-btn'); // 各页左上角 ← : 走它自带的清理
|
|
2796
|
+
if (b) { b.click(); return true; }
|
|
2797
|
+
el.remove(); return true;
|
|
2798
|
+
}
|
|
2799
|
+
return false;
|
|
2800
|
+
}
|
|
2801
|
+
function goBack() { return closeOverlay(topOverlay()); }
|
|
2802
|
+
function inHorizontalScroller(node) {
|
|
2803
|
+
for (let n = node; n && n !== document.body && n.nodeType === 1; n = n.parentElement) {
|
|
2804
|
+
if (!n.scrollWidth || n.scrollWidth <= n.clientWidth + 4) continue;
|
|
2805
|
+
const ox = getComputedStyle(n).overflowX;
|
|
2806
|
+
if (ox === 'auto' || ox === 'scroll') return true;
|
|
2807
|
+
}
|
|
2808
|
+
return false;
|
|
2809
|
+
}
|
|
2810
|
+
function setupGestures() {
|
|
2811
|
+
const TAB_ORDER = ['main', 'friends', 'network', 'me'];
|
|
2812
|
+
let sx = 0, sy = 0, st = 0, active = false, startTarget = null;
|
|
2813
|
+
document.addEventListener('touchstart', (e) => {
|
|
2814
|
+
if (e.touches.length !== 1) { active = false; return; }
|
|
2815
|
+
sx = e.touches[0].clientX; sy = e.touches[0].clientY;
|
|
2816
|
+
st = Date.now(); active = true; startTarget = e.target;
|
|
2817
|
+
}, { passive: true });
|
|
2818
|
+
document.addEventListener('touchend', (e) => {
|
|
2819
|
+
if (!active) return;
|
|
2820
|
+
active = false;
|
|
2821
|
+
const t = e.changedTouches[0];
|
|
2822
|
+
const dx = t.clientX - sx, dy = t.clientY - sy;
|
|
2823
|
+
if (Date.now() - st > 900) return; // 慢拖不算滑
|
|
2824
|
+
if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy) * 1.4) return; // 不够横向
|
|
2825
|
+
if (inHorizontalScroller(startTarget)) return; // 横向列表/卡片轨道里不抢手势
|
|
2826
|
+
const ov = topOverlay();
|
|
2827
|
+
if (ov) {
|
|
2828
|
+
if (dx > 0) closeOverlay(ov); // 浮层内右滑 = 返回上一层 (输入框选中文本时不触发: dx 门槛已过滤)
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
const i = TAB_ORDER.indexOf(currentTab);
|
|
2832
|
+
if (dx < 0) { if (i + 1 < TAB_ORDER.length) switchTab(TAB_ORDER[i + 1]); }
|
|
2833
|
+
else { if (i > 0) switchTab(TAB_ORDER[i - 1]); }
|
|
2834
|
+
}, { passive: true });
|
|
2835
|
+
|
|
2836
|
+
// 点空白关弹窗: 点 sheet 的暗背景 (非 .sheet-inner 内容) 即关
|
|
2837
|
+
document.addEventListener('click', (e) => {
|
|
2838
|
+
const t = e.target;
|
|
2839
|
+
if (!t || !t.closest) return;
|
|
2840
|
+
const sheet = t.closest('.sheet');
|
|
2841
|
+
if (sheet && !sheet.hasAttribute('hidden') && !t.closest('.sheet-inner')) sheet.hidden = true;
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2452
2845
|
function init() {
|
|
2453
2846
|
bindMenu();
|
|
2454
2847
|
applyTheme(resolveThemePref(), false);
|
|
2455
2848
|
switchTab('main');
|
|
2456
2849
|
setupUiControl();
|
|
2850
|
+
setupGestures();
|
|
2457
2851
|
installDeepLinkListeners();
|
|
2458
2852
|
loadAgentCovers();
|
|
2459
2853
|
loadMe();
|