@bolloon/bolloon-agent 0.4.9 → 0.4.11

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.
@@ -5,6 +5,19 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Bolloon Agent</title>
7
7
 
8
+ <!-- 2026-08-12: 手机端 (Capacitor webview / 移动浏览器) 跳转专门的手机端 UI (mobile.html, 微信风格) -->
9
+ <script>
10
+ (function () {
11
+ try {
12
+ var isCapacitor = window.Capacitor && window.Capacitor.isNativePlatform && window.Capacitor.isNativePlatform();
13
+ var isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
14
+ if ((isCapacitor || isMobile) && !location.pathname.endsWith('/mobile.html')) {
15
+ location.replace('./mobile.html');
16
+ }
17
+ } catch (e) { /* 跳转失败静默, 继续桌面 UI */ }
18
+ })();
19
+ </script>
20
+
8
21
  <!-- Favicon -->
9
22
  <link rel="icon" type="image/x-icon" href="./icons/favicon.ico">
10
23
  <link rel="icon" type="image/png" sizes="32x32" href="./icons/favicon-32x32.png">
@@ -0,0 +1,173 @@
1
+ /* 手机端 UI — 微信风格 + bolloon WebUI 主题 (配色不变) */
2
+ :root {
3
+ --bg: #1a1a18;
4
+ --bg-card: #222220;
5
+ --bg-hover: #2a2a26;
6
+ --text: #d8d8c8;
7
+ --text-secondary: #909088;
8
+ --text-muted: #606058;
9
+ --accent: #c4d640;
10
+ --accent-hover: #d4e650;
11
+ --border: #3a3a36;
12
+ --radius: 12px;
13
+ --tabbar-h: 60px;
14
+ --topbar-h: 56px;
15
+ }
16
+
17
+ * { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
18
+ html, body { height: 100%; }
19
+ body {
20
+ background: var(--bg);
21
+ color: var(--text);
22
+ font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
23
+ overflow: hidden;
24
+ touch-action: manipulation;
25
+ }
26
+
27
+ .app {
28
+ display: flex;
29
+ flex-direction: column;
30
+ height: 100%;
31
+ }
32
+
33
+ /* 顶部栏 */
34
+ .topbar {
35
+ height: var(--topbar-h);
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: space-between;
39
+ padding: 0 16px;
40
+ padding-top: env(safe-area-inset-top);
41
+ background: var(--bg-card);
42
+ border-bottom: 1px solid var(--border);
43
+ }
44
+ .topbar-title { font-size: 18px; font-weight: 600; color: var(--text); }
45
+ .topbar-actions { display: flex; gap: 12px; }
46
+ .icon-btn {
47
+ width: 36px; height: 36px;
48
+ border: none; border-radius: 8px;
49
+ background: var(--bg-hover);
50
+ color: var(--accent);
51
+ font-size: 22px;
52
+ display: flex; align-items: center; justify-content: center;
53
+ }
54
+
55
+ /* 页面容器 */
56
+ .page-container { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; }
57
+ .page { min-height: 100%; }
58
+
59
+ /* 列表 */
60
+ .list { padding: 8px 0; }
61
+ .list-item {
62
+ display: flex; align-items: center;
63
+ gap: 12px;
64
+ padding: 14px 16px;
65
+ background: var(--bg-card);
66
+ border-bottom: 1px solid var(--border);
67
+ cursor: pointer;
68
+ }
69
+ .list-item:active { background: var(--bg-hover); }
70
+ .list-icon { font-size: 20px; width: 32px; text-align: center; }
71
+ .list-item span:nth-child(2) { flex: 1; }
72
+ .list-arrow { color: var(--text-muted); }
73
+
74
+ /* 分组标签 */
75
+ .section-label {
76
+ padding: 12px 16px 4px;
77
+ font-size: 12px;
78
+ color: var(--text-muted);
79
+ }
80
+
81
+ /* 会话 / 联系人条目 */
82
+ .conv-item {
83
+ display: flex; align-items: center; gap: 12px;
84
+ padding: 12px 16px;
85
+ background: var(--bg-card);
86
+ border-bottom: 1px solid var(--border);
87
+ cursor: pointer;
88
+ }
89
+ .conv-item:active { background: var(--bg-hover); }
90
+ .conv-avatar {
91
+ width: 44px; height: 44px; border-radius: 8px;
92
+ background: var(--accent);
93
+ color: var(--bg);
94
+ display: flex; align-items: center; justify-content: center;
95
+ font-size: 18px; font-weight: 600;
96
+ }
97
+ .conv-body { flex: 1; min-width: 0; }
98
+ .conv-name { font-size: 16px; color: var(--text); }
99
+ .conv-preview { font-size: 13px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
100
+
101
+ /* 我 tab 卡片 */
102
+ .profile-card {
103
+ display: flex; align-items: center; gap: 14px;
104
+ padding: 20px 16px;
105
+ background: var(--bg-card);
106
+ border-bottom: 1px solid var(--border);
107
+ }
108
+ .avatar {
109
+ width: 56px; height: 56px; border-radius: 12px;
110
+ background: var(--accent); color: var(--bg);
111
+ display: flex; align-items: center; justify-content: center;
112
+ font-size: 24px; font-weight: 700;
113
+ }
114
+ .profile-name { font-size: 18px; font-weight: 600; }
115
+ .profile-did { font-size: 12px; color: var(--text-muted); word-break: break-all; margin-top: 4px; }
116
+
117
+ /* 底部 tab */
118
+ .tabbar {
119
+ height: var(--tabbar-h);
120
+ display: flex;
121
+ padding-bottom: env(safe-area-inset-bottom);
122
+ background: var(--bg-card);
123
+ border-top: 1px solid var(--border);
124
+ }
125
+ .tab {
126
+ flex: 1;
127
+ display: flex; flex-direction: column;
128
+ align-items: center; justify-content: center;
129
+ gap: 2px;
130
+ border: none; background: transparent;
131
+ color: var(--text-secondary);
132
+ font-size: 11px;
133
+ }
134
+ .tab.active { color: var(--accent); }
135
+ .tab-icon { font-size: 20px; }
136
+
137
+ /* 聊天页 */
138
+ .chat-page {
139
+ position: fixed; inset: 0;
140
+ display: flex; flex-direction: column;
141
+ background: var(--bg);
142
+ z-index: 10;
143
+ }
144
+ .chat-topbar {
145
+ height: var(--topbar-h);
146
+ display: flex; align-items: center;
147
+ padding: 0 12px; padding-top: env(safe-area-inset-top);
148
+ background: var(--bg-card);
149
+ border-bottom: 1px solid var(--border);
150
+ gap: 8px;
151
+ }
152
+ .chat-messages { flex: 1; overflow-y: auto; padding: 12px; }
153
+ .chat-input-bar {
154
+ display: flex; gap: 8px; padding: 8px 12px;
155
+ padding-bottom: calc(8px + env(safe-area-inset-bottom));
156
+ background: var(--bg-card);
157
+ border-top: 1px solid var(--border);
158
+ }
159
+ .chat-input-bar input {
160
+ flex: 1; height: 38px; padding: 0 12px;
161
+ border: 1px solid var(--border); border-radius: 8px;
162
+ background: var(--bg-hover); color: var(--text);
163
+ outline: none;
164
+ }
165
+ .chat-input-bar button {
166
+ height: 38px; padding: 0 16px;
167
+ border: none; border-radius: 8px;
168
+ background: var(--accent); color: var(--bg);
169
+ font-weight: 600;
170
+ }
171
+ .bubble { max-width: 75%; padding: 8px 12px; border-radius: 10px; margin: 4px 0; word-break: break-word; }
172
+ .bubble.user { margin-left: auto; background: var(--accent); color: var(--bg); }
173
+ .bubble.ai { background: var(--bg-card); color: var(--text); }
@@ -0,0 +1,76 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
6
+ <title>Bolloon 手机端</title>
7
+ <link rel="stylesheet" href="./mobile.css">
8
+ </head>
9
+ <body>
10
+ <div id="app" class="app">
11
+ <!-- 顶部导航 -->
12
+ <header class="topbar">
13
+ <div class="topbar-title" id="topbar-title">微信</div>
14
+ <div class="topbar-actions">
15
+ <button class="icon-btn" id="btn-add" title="添加好友">+</button>
16
+ </div>
17
+ </header>
18
+
19
+ <!-- 页面容器 (切换 4 个 tab) -->
20
+ <main class="page-container">
21
+ <!-- 微信 tab: 会话列表 -->
22
+ <section class="page" id="page-wechat" data-tab="wechat">
23
+ <div class="list" id="conversation-list"></div>
24
+ </section>
25
+
26
+ <!-- 通讯录 tab -->
27
+ <section class="page" id="page-contacts" data-tab="contacts" hidden>
28
+ <div class="list" id="contacts-list"></div>
29
+ </section>
30
+
31
+ <!-- 发现 tab -->
32
+ <section class="page" id="page-discover" data-tab="discover" hidden>
33
+ <div class="list">
34
+ <div class="list-item" id="item-p2p"><span class="list-icon">🌐</span><span>P2P 好友</span></div>
35
+ <div class="list-item" id="item-p2p-id"><span class="list-icon">🪪</span><span>我的 P2P ID</span></div>
36
+ </div>
37
+ <div class="section-label">MCP 工具 (触控调用)</div>
38
+ <div class="list" id="mcp-tools"></div>
39
+ <div class="section-label">A2UI 动态面板 (智能体生成)</div>
40
+ <div id="a2ui-root"></div>
41
+ </section>
42
+
43
+ <!-- 我 tab -->
44
+ <section class="page" id="page-me" data-tab="me" hidden>
45
+ <div class="profile-card" id="profile-card">
46
+ <div class="avatar" id="me-avatar">?</div>
47
+ <div class="profile-info">
48
+ <div class="profile-name" id="me-name">未登录</div>
49
+ <div class="profile-did" id="me-did"></div>
50
+ </div>
51
+ </div>
52
+ <div class="list">
53
+ <div class="list-item" id="item-settings"><span class="list-icon">⚙️</span><span>设置</span><span class="list-arrow">›</span></div>
54
+ <div class="list-item" id="item-wallet"><span class="list-icon">👛</span><span>钱包</span><span class="list-arrow">›</span></div>
55
+ <div class="list-item" id="item-judgments"><span class="list-icon">🧠</span><span>判断力 API</span><span class="list-arrow">›</span></div>
56
+ <div class="list-item" id="item-did"><span class="list-icon">🪪</span><span>DID 身份</span><span class="list-arrow">›</span></div>
57
+ <div class="list-item" id="item-login"><span class="list-icon">🔐</span><span id="login-label">登录</span><span class="list-arrow">›</span></div>
58
+ <div class="list-item" id="item-logout"><span class="list-icon">🚪</span><span>注销</span></div>
59
+ </div>
60
+ </section>
61
+ </main>
62
+
63
+ <!-- 底部 tab -->
64
+ <nav class="tabbar">
65
+ <button class="tab active" data-tab="wechat"><span class="tab-icon">💬</span><span>微信</span></button>
66
+ <button class="tab" data-tab="contacts"><span class="tab-icon">📇</span><span>通讯录</span></button>
67
+ <button class="tab" data-tab="discover"><span class="tab-icon">🧭</span><span>发现</span></button>
68
+ <button class="tab" data-tab="me"><span class="tab-icon">🙂</span><span>我</span></button>
69
+ </nav>
70
+ </div>
71
+
72
+ <script src="./mobile.js"></script>
73
+ <!-- 2026-08-12: A2UI 渲染器 (智能体生成动态 UI, 挂载 #a2ui-root) -->
74
+ <script src="./a2ui-client.js"></script>
75
+ </body>
76
+ </html>
@@ -0,0 +1,346 @@
1
+ /**
2
+ * mobile.js — 手机端 UI 交互 (微信风格, Capacitor webview)
3
+ * bolloon WebUI 主题配色. 数据来自 bolloon server HTTP API + SSE 流式聊天.
4
+ * 触控组件: tab 切换 / 列表点击 / 聊天输入 / 设置 / MCP 工具调用.
5
+ */
6
+ (function () {
7
+ const $ = (sel) => document.querySelector(sel);
8
+ const $$ = (sel) => Array.from(document.querySelectorAll(sel));
9
+
10
+ const api = {
11
+ async get(path) {
12
+ const r = await fetch(path);
13
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
14
+ return r.json();
15
+ },
16
+ async post(path, body) {
17
+ const r = await fetch(path, {
18
+ method: 'POST',
19
+ headers: { 'Content-Type': 'application/json' },
20
+ body: body ? JSON.stringify(body) : undefined,
21
+ });
22
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
23
+ return r.json();
24
+ },
25
+ };
26
+
27
+ // === 主题 (外观切换, 复用 WebUI 主题变量) ===
28
+ const THEMES = {
29
+ dark: { '--bg': '#1a1a18', '--bg-card': '#222220', '--bg-hover': '#2a2a26', '--text': '#d8d8c8', '--text-secondary': '#909088', '--accent': '#c4d640', '--border': '#3a3a36' },
30
+ light: { '--bg': '#f5f5f0', '--bg-card': '#ffffff', '--bg-hover': '#eeeeea', '--text': '#1a1a18', '--text-secondary': '#606058', '--accent': '#8a9430', '--border': '#d0d0c8' },
31
+ };
32
+ function applyTheme(name) {
33
+ const t = THEMES[name] || THEMES.dark;
34
+ const root = document.documentElement;
35
+ Object.entries(t).forEach(([k, v]) => root.style.setProperty(k, v));
36
+ localStorage.setItem('bolloon_theme', name);
37
+ const btn = $('#theme-toggle');
38
+ if (btn) btn.textContent = name === 'dark' ? '🌙 深色' : '☀️ 浅色';
39
+ }
40
+
41
+ // === tab 切换 ===
42
+ const TITLES = { wechat: '微信', contacts: '通讯录', discover: '发现', me: '我' };
43
+ function switchTab(tab) {
44
+ $$('.page').forEach((p) => { p.hidden = p.dataset.tab !== tab; });
45
+ $$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tab));
46
+ $('#topbar-title').textContent = TITLES[tab] || '微信';
47
+ if (tab === 'discover') loadMcpTools();
48
+ window.__mobileTouch?.('tab', tab);
49
+ }
50
+ $$('.tab').forEach((t) => t.addEventListener('click', () => switchTab(t.dataset.tab)));
51
+
52
+ // === 会话列表 ===
53
+ async function loadConversations() {
54
+ try {
55
+ const channels = await api.get('/channels');
56
+ const list = $('#conversation-list');
57
+ list.innerHTML = '';
58
+ if (!Array.isArray(channels) || channels.length === 0) {
59
+ list.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">暂无会话, 点右上角 + 添加</div>';
60
+ return;
61
+ }
62
+ channels.forEach((ch) => {
63
+ const name = ch.persona?.name || ch.name || ch.agentId || '智能体';
64
+ const el = document.createElement('div');
65
+ el.className = 'conv-item';
66
+ el.innerHTML = `
67
+ <div class="conv-avatar">${escapeHtml(name.charAt(0))}</div>
68
+ <div class="conv-body">
69
+ <div class="conv-name">${escapeHtml(name)}</div>
70
+ <div class="conv-preview">${escapeHtml(ch.preview || '开始对话')}</div>
71
+ </div>`;
72
+ el.addEventListener('click', () => openChat(ch));
73
+ list.appendChild(el);
74
+ });
75
+ } catch (e) {
76
+ $('#conversation-list').innerHTML = `<div style="padding:20px;color:var(--error)">加载失败: ${escapeHtml(e.message)}</div>`;
77
+ }
78
+ }
79
+
80
+ // === 通讯录 ===
81
+ async function loadContacts() {
82
+ try {
83
+ let peers = [];
84
+ try { peers = await api.get('/api/peers'); } catch { peers = []; }
85
+ const list = $('#contacts-list');
86
+ list.innerHTML = '';
87
+ if (!Array.isArray(peers) || peers.length === 0) {
88
+ list.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted)">暂无好友</div>';
89
+ return;
90
+ }
91
+ peers.forEach((p) => {
92
+ const name = p.name || p.publicKey?.slice(0, 12) || '好友';
93
+ const el = document.createElement('div');
94
+ el.className = 'conv-item';
95
+ el.innerHTML = `<div class="conv-avatar">${escapeHtml(name.charAt(0))}</div>
96
+ <div class="conv-body"><div class="conv-name">${escapeHtml(name)}</div></div>`;
97
+ list.appendChild(el);
98
+ });
99
+ } catch (e) { /* 忽略 */ }
100
+ }
101
+
102
+ // === 发现: MCP 工具列表 (MCP 前端支持) ===
103
+ async function loadMcpTools() {
104
+ const box = $('#mcp-tools');
105
+ if (!box) return;
106
+ box.innerHTML = '<div style="padding:12px 16px;color:var(--text-muted)">加载 MCP 工具...</div>';
107
+ try {
108
+ const r = await api.get('/api/mcp/tools').catch(() => null);
109
+ const tools = r?.tools || r || [];
110
+ if (!Array.isArray(tools) || tools.length === 0) {
111
+ box.innerHTML = '<div style="padding:12px 16px;color:var(--text-muted)">暂无可用 MCP 工具</div>';
112
+ return;
113
+ }
114
+ box.innerHTML = '';
115
+ tools.forEach((t) => {
116
+ const name = t.name || t.function?.name || '工具';
117
+ const desc = t.description || t.function?.description || '';
118
+ const el = document.createElement('div');
119
+ el.className = 'conv-item';
120
+ el.innerHTML = `<div class="conv-avatar">🔌</div>
121
+ <div class="conv-body"><div class="conv-name">${escapeHtml(name)}</div>
122
+ <div class="conv-preview">${escapeHtml(desc)}</div></div>`;
123
+ el.addEventListener('click', () => { window.__mobileTouch?.('mcp', name); alert('MCP 工具: ' + name + '\n可在对话中让智能体调用'); });
124
+ box.appendChild(el);
125
+ });
126
+ } catch (e) { box.innerHTML = '<div style="padding:12px 16px;color:var(--text-muted)">MCP 工具加载失败</div>'; }
127
+ }
128
+
129
+ // === 我的页 ===
130
+ async function loadMe() {
131
+ try {
132
+ const s = await api.get('/api/auth/status');
133
+ $('#me-avatar').textContent = (s.name || 'U').charAt(0);
134
+ $('#me-name').textContent = s.name || '未登录';
135
+ $('#me-did').textContent = s.didShort || s.did || '';
136
+ const hasAccount = (s.accounts && s.accounts.length > 0);
137
+ $('#login-label').textContent = hasAccount ? '已登录账号' : '登录';
138
+ $('#settings-did').textContent = 'DID: ' + (s.did || '未生成');
139
+ $('#settings-login-state').textContent = hasAccount ? '已登录 (' + (s.accounts.map(a => a.provider).join(',')) + ')' : '未登录';
140
+ } catch (e) { /* 默认 */ }
141
+ }
142
+
143
+ // === 聊天页 (SSE 流式) ===
144
+ let activeChannel = null;
145
+ let chatEventSource = null;
146
+ let streamingBubble = null;
147
+
148
+ function openChat(ch) {
149
+ activeChannel = ch;
150
+ const page = document.createElement('div');
151
+ page.className = 'chat-page';
152
+ page.id = 'chat-page';
153
+ const name = ch.persona?.name || ch.name || ch.agentId || '智能体';
154
+ page.innerHTML = `
155
+ <div class="chat-topbar">
156
+ <button class="icon-btn" id="chat-back">‹</button>
157
+ <div style="flex:1;font-weight:600">${escapeHtml(name)}</div>
158
+ </div>
159
+ <div class="chat-messages" id="chat-messages"></div>
160
+ <div class="chat-input-bar">
161
+ <input id="chat-input" placeholder="输入消息...">
162
+ <button id="chat-send">发送</button>
163
+ </div>`;
164
+ document.body.appendChild(page);
165
+ $('#chat-back').addEventListener('click', closeChat);
166
+ $('#chat-send').addEventListener('click', sendChat);
167
+ $('#chat-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') sendChat(); });
168
+ loadMessages();
169
+ openChatSse();
170
+ window.__mobileTouch?.('chat', ch.id);
171
+ }
172
+
173
+ function closeChat() {
174
+ if (chatEventSource) { chatEventSource.close(); chatEventSource = null; }
175
+ const p = $('#chat-page');
176
+ if (p) p.remove();
177
+ activeChannel = null;
178
+ loadConversations();
179
+ }
180
+
181
+ function openChatSse() {
182
+ if (chatEventSource) chatEventSource.close();
183
+ if (!activeChannel) return;
184
+ // 订阅全局事件流, 过滤当前 channel 的 AI 流式回复 (step/token/ai)
185
+ try {
186
+ chatEventSource = new EventSource('/events');
187
+ chatEventSource.onmessage = (e) => {
188
+ if (!activeChannel || !e.data) return;
189
+ let msg;
190
+ try { msg = JSON.parse(e.data); } catch { return; }
191
+ const box = $('#chat-messages');
192
+ if (!box) return;
193
+ // AI 消息 / token 流 → 流式气泡
194
+ if (msg.type === 'ai' || msg.type === 'token' || msg.role === 'ai') {
195
+ const content = msg.content || msg.text || '';
196
+ if (msg.channelId && msg.channelId !== activeChannel.id) return;
197
+ if (!streamingBubble || !streamingBubble.isConnected) {
198
+ streamingBubble = document.createElement('div');
199
+ streamingBubble.className = 'bubble ai';
200
+ box.appendChild(streamingBubble);
201
+ }
202
+ streamingBubble.textContent += content;
203
+ box.scrollTop = box.scrollHeight;
204
+ } else if (msg.type === 'done') {
205
+ streamingBubble = null;
206
+ setTimeout(loadMessages, 300);
207
+ }
208
+ };
209
+ chatEventSource.onerror = () => { /* SSE 断线静默重连 */ };
210
+ } catch (e) { /* SSE 不支持则退回轮询 */ }
211
+ }
212
+
213
+ async function loadMessages() {
214
+ if (!activeChannel) return;
215
+ const box = $('#chat-messages');
216
+ try {
217
+ const data = await api.get(`/sessions/${encodeURIComponent(activeChannel.id)}`);
218
+ const msgs = data?.messages || [];
219
+ box.innerHTML = '';
220
+ msgs.slice(-50).forEach((m) => {
221
+ const role = (m.role || m.type || '') === 'user' ? 'user' : 'ai';
222
+ const d = document.createElement('div');
223
+ d.className = 'bubble ' + role;
224
+ d.textContent = m.content || '';
225
+ box.appendChild(d);
226
+ });
227
+ box.scrollTop = box.scrollHeight;
228
+ } catch (e) { box.innerHTML = '<div style="color:var(--text-muted)">暂无历史消息</div>'; }
229
+ }
230
+
231
+ async function sendChat() {
232
+ const input = $('#chat-input');
233
+ const text = input.value.trim();
234
+ if (!text || !activeChannel) return;
235
+ input.value = '';
236
+ const box = $('#chat-messages');
237
+ const userBubble = document.createElement('div');
238
+ userBubble.className = 'bubble user';
239
+ userBubble.textContent = text;
240
+ box.appendChild(userBubble);
241
+ box.scrollTop = box.scrollHeight;
242
+ try {
243
+ await api.post('/message', { text, channelId: activeChannel.id });
244
+ // AI 回复走 SSE 流式 (openChatSse)
245
+ setTimeout(() => { if (!streamingBubble) loadMessages(); }, 1500);
246
+ } catch (e) {
247
+ const ai = document.createElement('div');
248
+ ai.className = 'bubble ai';
249
+ ai.textContent = '发送失败: ' + (e.message || '');
250
+ box.appendChild(ai);
251
+ }
252
+ }
253
+
254
+ // === 设置页 (弹层) ===
255
+ function openSettings() {
256
+ const page = document.createElement('div');
257
+ page.className = 'chat-page';
258
+ page.id = 'settings-page';
259
+ page.innerHTML = `
260
+ <div class="chat-topbar">
261
+ <button class="icon-btn" id="settings-back">‹</button>
262
+ <div style="flex:1;font-weight:600">设置</div>
263
+ </div>
264
+ <div style="padding:12px">
265
+ <div class="conv-item" id="theme-toggle">🌙 深色</div>
266
+ <div class="conv-item" id="settings-api"><span class="list-icon">🔧</span><span>API 配置</span><span class="list-arrow">›</span></div>
267
+ <div class="conv-item" id="settings-wallet"><span class="list-icon">👛</span><span>钱包</span><span class="list-arrow">›</span></div>
268
+ <div class="conv-item" id="settings-judgments"><span class="list-icon">🧠</span><span>判断力 API</span><span class="list-arrow">›</span></div>
269
+ <div class="conv-item" id="settings-did">🪪 DID</div>
270
+ <div class="conv-item" id="settings-login-state">🔐 登录状态</div>
271
+ </div>`;
272
+ document.body.appendChild(page);
273
+ const theme = localStorage.getItem('bolloon_theme') || 'dark';
274
+ applyTheme(theme);
275
+ $('#settings-back').addEventListener('click', () => page.remove());
276
+ $('#theme-toggle').addEventListener('click', () => applyTheme(localStorage.getItem('bolloon_theme') === 'dark' ? 'light' : 'dark'));
277
+ $('#settings-api').addEventListener('click', () => openUrl('/api-config'));
278
+ $('#settings-wallet').addEventListener('click', () => alert('钱包管理 (桌面 Web UI 提供)'));
279
+ $('#settings-judgments').addEventListener('click', () => alert('判断力 API (桌面 Web UI 提供)'));
280
+ $('#settings-did').addEventListener('click', () => { api.get('/api/auth/status').then((s) => alert('DID: ' + (s.did || '未生成'))); });
281
+ }
282
+
283
+ // === 菜单绑定 ===
284
+ function bindMenu() {
285
+ $('#item-settings').addEventListener('click', openSettings);
286
+ $('#item-wallet').addEventListener('click', () => { alert('钱包管理 (桌面 Web UI 提供)'); });
287
+ $('#item-judgments').addEventListener('click', () => { alert('判断力 API (桌面 Web UI 提供)'); });
288
+ $('#item-did').addEventListener('click', () => { api.get('/api/auth/status').then((s) => alert('DID: ' + (s.did || '未生成'))); });
289
+ $('#item-login').addEventListener('click', () => { openUrl('/api-config'); });
290
+ $('#item-logout').addEventListener('click', () => { api.post('/api/auth/logout', { provider: 'github' }).then(() => loadMe()); });
291
+ $('#btn-add').addEventListener('click', () => { alert('添加好友: 在桌面 Web UI 的 P2P 好友中添加'); });
292
+ $('#item-p2p').addEventListener('click', () => { switchTab('contacts'); });
293
+ $('#item-p2p-id').addEventListener('click', () => { alert('我的 P2P ID 见桌面 Web UI'); });
294
+ }
295
+
296
+ function openUrl(url) {
297
+ if (window.Capacitor && window.Capacitor.isNativePlatform?.()) location.href = url;
298
+ else window.open(url, '_blank');
299
+ }
300
+
301
+ function escapeHtml(s) {
302
+ return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
303
+ }
304
+
305
+ // === MCP 触控钩子 ===
306
+ window.__mobileTouch = (type, data) => {
307
+ // 预留: 触控事件上报/控制
308
+ };
309
+
310
+ // === MCP 驱动前端 UI (2026-08-12): 订阅 /events, 收到 {type:'ui'} 指令执行组件动作 ===
311
+ function setupUiControl() {
312
+ try {
313
+ const es = new EventSource('/events');
314
+ es.onmessage = (e) => {
315
+ if (!e.data) return;
316
+ let msg;
317
+ try { msg = JSON.parse(e.data); } catch { return; }
318
+ if (msg.type !== 'ui' || !msg.action) return;
319
+ const d = msg.data || {};
320
+ switch (msg.action) {
321
+ case 'switchTab': if (d.tab && ['wechat', 'contacts', 'discover', 'me'].includes(d.tab)) switchTab(d.tab); break;
322
+ case 'openSettings': openSettings(); break;
323
+ case 'openWallet': alert('钱包管理 (桌面 Web UI 提供)'); break;
324
+ case 'openAddFriend': alert('添加好友: 在桌面 Web UI 的 P2P 好友中添加'); break;
325
+ case 'showToast': alert(d.message || ''); break;
326
+ case 'goBack': { const p = $('#chat-page'); if (p) closeChat(); else openSettings(); break; }
327
+ default: break;
328
+ }
329
+ window.__mobileTouch?.('ui', msg.action);
330
+ };
331
+ es.onerror = () => { /* SSE 断线静默重连 */ };
332
+ } catch (e) { /* 忽略 */ }
333
+ }
334
+
335
+ function init() {
336
+ bindMenu();
337
+ applyTheme(localStorage.getItem('bolloon_theme') || 'dark');
338
+ switchTab('wechat');
339
+ setupUiControl();
340
+ loadConversations();
341
+ loadContacts();
342
+ loadMe();
343
+ }
344
+ document.addEventListener('DOMContentLoaded', init);
345
+ if (document.readyState !== 'loading') init();
346
+ })();
@@ -2785,6 +2785,39 @@ ${goalDesc}
2785
2785
  res.status(400).json({ ok: false, severity: 'block', reason: e?.message ?? 'parse error' });
2786
2786
  }
2787
2787
  });
2788
+ // 2026-08-12: MCP 工具列表 (MCP 前端支持 — 手机端/桌面 UI 展示可用 MCP 工具)
2789
+ app.get('/api/mcp/tools', async (_req, res) => {
2790
+ try {
2791
+ const mcp = await import('../pi-ecosystem-mcp/index.js');
2792
+ await mcp.initializeMcpAdapter().catch(() => { });
2793
+ const tools = mcp.listTools().map((t) => ({ name: t.name, description: t.description || '' }));
2794
+ res.json({ tools });
2795
+ }
2796
+ catch (e) {
2797
+ res.status(500).json({ error: e?.message });
2798
+ }
2799
+ });
2800
+ // 2026-08-12: MCP 驱动前端 UI — 把 broadcast 注入 ui-tools, agent 调 UI 工具时广播 {type:'ui'} 给前端 SSE
2801
+ (async () => {
2802
+ try {
2803
+ const { setUiBroadcast, registerUiControlTools } = await import('../pi-ecosystem-mcp/ui-tools.js');
2804
+ setUiBroadcast(broadcast);
2805
+ registerUiControlTools();
2806
+ }
2807
+ catch (e) {
2808
+ console.warn('[ui-tools] UI 广播注入失败 (非致命):', e?.message?.slice(0, 120));
2809
+ }
2810
+ })();
2811
+ // 2026-08-12: A2UI (Agent to UI) — 把 broadcast 注入 a2ui 模块, agent 调 a2ui_* 工具时广播 {type:'a2ui'} 给前端 SSE
2812
+ (async () => {
2813
+ try {
2814
+ const { setA2uiBroadcast } = await import('../pi-ecosystem-a2ui/index.js');
2815
+ setA2uiBroadcast(broadcast);
2816
+ }
2817
+ catch (e) {
2818
+ console.warn('[a2ui] 广播注入失败 (非致命):', e?.message?.slice(0, 120));
2819
+ }
2820
+ })();
2788
2821
  app.get('/api/health', (_req, res) => {
2789
2822
  res.json(healthCheck(getPackageVersion()));
2790
2823
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",
@@ -55,6 +55,8 @@
55
55
  "src/constraint-runtime"
56
56
  ],
57
57
  "dependencies": {
58
+ "@a2ui/react": "^0.10.2",
59
+ "@a2ui/web_core": "^0.10.6",
58
60
  "@bolloon/constraint-runtime": "0.1.0",
59
61
  "@capacitor/android": "^8.5.0",
60
62
  "@capacitor/core": "^8.5.0",