@bolloon/bolloon-agent 0.3.16 → 0.3.17
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/error-classifier.js +118 -0
- package/dist/agents/parse-tool-call.js +196 -1
- package/dist/agents/pi-sdk-tools.js +13 -0
- package/dist/agents/pi-sdk.js +195 -262
- package/dist/cli/loading-tui.js +43 -36
- package/dist/cli-entry.js +15 -8
- package/dist/electron/config.js +9 -14
- package/dist/electron/dialogs.js +16 -53
- package/dist/electron/first-run.js +24 -65
- package/dist/electron/ipc.js +10 -14
- package/dist/electron/logger.js +7 -44
- package/dist/electron/main.js +42 -45
- package/dist/electron/menu.js +13 -18
- package/dist/electron/paths.js +12 -54
- package/dist/electron/server.js +18 -57
- package/dist/electron/tray.js +15 -53
- package/dist/electron/window.js +22 -61
- package/dist/electron-preload.js +16 -19
- package/dist/electron.js +1 -4
- package/dist/external-engines/discovery.js +11 -0
- package/dist/index.js +151 -12
- package/dist/lsp/lsp-manager.js +281 -0
- package/dist/lsp/lsp-tools.js +222 -0
- package/dist/utils/auto-update.js +12 -51
- package/dist/web/client.js +4323 -4812
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +276 -234
- package/dist/web/components/p2p/p2p-modal.js +664 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/index.html +7 -0
- package/dist/web/server.js +48 -7
- package/dist/web/style.css +58 -3
- package/dist/web/ui/message-renderer.js +535 -396
- package/dist/web/ui/step-timeline.js +371 -272
- package/package.json +3 -3
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* P2P Modal - 纯 TypeScript 版本
|
|
3
|
+
*/
|
|
4
|
+
import { safeName as safeAnyName } from '../../util/safe-name.js';
|
|
2
5
|
class P2PModalUI {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
modal = null;
|
|
7
|
+
overlay = null;
|
|
8
|
+
connectedPeers = new Map();
|
|
9
|
+
constructor() {
|
|
10
|
+
this.createModal();
|
|
11
|
+
}
|
|
12
|
+
createModal() {
|
|
13
|
+
// 创建遮罩层
|
|
14
|
+
this.overlay = document.createElement('div');
|
|
15
|
+
this.overlay.className = 'p2p-modal-overlay';
|
|
16
|
+
this.overlay.style.cssText = `
|
|
13
17
|
display: none;
|
|
14
18
|
position: fixed;
|
|
15
19
|
inset: 0;
|
|
@@ -18,9 +22,10 @@ class P2PModalUI {
|
|
|
18
22
|
align-items: center;
|
|
19
23
|
justify-content: center;
|
|
20
24
|
`;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
// 创建弹窗
|
|
26
|
+
this.modal = document.createElement('div');
|
|
27
|
+
this.modal.className = 'p2p-modal';
|
|
28
|
+
this.modal.style.cssText = `
|
|
24
29
|
width: 90%;
|
|
25
30
|
max-width: 650px;
|
|
26
31
|
max-height: 90vh;
|
|
@@ -31,74 +36,87 @@ class P2PModalUI {
|
|
|
31
36
|
display: flex;
|
|
32
37
|
flex-direction: column;
|
|
33
38
|
`;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
39
|
+
this.overlay.appendChild(this.modal);
|
|
40
|
+
document.body.appendChild(this.overlay);
|
|
41
|
+
// 点击遮罩关闭
|
|
42
|
+
this.overlay.addEventListener('click', (e) => {
|
|
43
|
+
if (e.target === this.overlay)
|
|
44
|
+
this.hide();
|
|
45
|
+
});
|
|
46
|
+
this.render();
|
|
47
|
+
}
|
|
48
|
+
async render() {
|
|
49
|
+
if (!this.modal)
|
|
50
|
+
return;
|
|
51
|
+
// 加载连接历史
|
|
52
|
+
await this.loadConnectionHistory();
|
|
53
|
+
// 获取身份信息
|
|
54
|
+
let identityHtml = '<div class="p2p-loading">加载中...</div>';
|
|
55
|
+
let identityData = { name: '未知', did: '未知', cid: '未知' };
|
|
56
|
+
try {
|
|
57
|
+
// 尝试获取 iroh 身份(包含 CID)
|
|
58
|
+
const irohResp = await fetch('/api/iroh/init', { method: 'POST' });
|
|
59
|
+
const irohData = await irohResp.json();
|
|
60
|
+
if (irohData.cid) {
|
|
61
|
+
// 2026-07-06: 防御 irohData.name 是 'undefined'/'null'/空白 的情况,
|
|
62
|
+
// 否则 UI 会渲染字面量 "undefined". (multi-confirmed fix: 避免再现 v0.2.7 的 bug)
|
|
63
|
+
const rawName = (typeof irohData.name === 'string' && irohData.name.trim()
|
|
64
|
+
&& irohData.name.trim() !== 'undefined'
|
|
65
|
+
&& irohData.name.trim() !== 'null') ? irohData.name.trim() : '';
|
|
66
|
+
const tailName = rawName.split('-').slice(-1)[0] || '';
|
|
67
|
+
identityData = {
|
|
68
|
+
name: rawName || tailName || 'Bolloon',
|
|
69
|
+
did: irohData.did || '未知',
|
|
70
|
+
cid: irohData.cid || '未知',
|
|
71
|
+
nodeId: irohData.irohNodeId ? irohData.irohNodeId.substring(0, 20) + '...' : '未知'
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// 回退到 /api/identity
|
|
76
|
+
const resp = await fetch('/api/identity');
|
|
77
|
+
identityData = await resp.json();
|
|
78
|
+
identityData.cid = identityData.cid || '未初始化';
|
|
79
|
+
identityData.nodeId = identityData.nodeId || '未知';
|
|
80
|
+
}
|
|
81
|
+
identityHtml = `
|
|
65
82
|
<div class="p2p-section">
|
|
66
|
-
<h3
|
|
83
|
+
<h3>身份信息</h3>
|
|
67
84
|
<div class="p2p-info-row">
|
|
68
|
-
<span class="label"
|
|
69
|
-
<span class="value">${this.escapeHtml(this.safeName(identityData.name,
|
|
85
|
+
<span class="label">名称:</span>
|
|
86
|
+
<span class="value">${this.escapeHtml(this.safeName(identityData.name, '未知'))}</span>
|
|
70
87
|
</div>
|
|
71
88
|
<div class="p2p-info-row">
|
|
72
89
|
<span class="label">CID:</span>
|
|
73
|
-
<span class="value mono">${this.escapeHtml(identityData.cid ||
|
|
90
|
+
<span class="value mono">${this.escapeHtml(identityData.cid || '未知')}</span>
|
|
74
91
|
</div>
|
|
75
92
|
<div class="p2p-info-row">
|
|
76
93
|
<span class="label">DID:</span>
|
|
77
|
-
<span class="value mono">${this.escapeHtml(identityData.did ||
|
|
94
|
+
<span class="value mono">${this.escapeHtml(identityData.did || '未知')}</span>
|
|
78
95
|
</div>
|
|
79
96
|
<div class="p2p-info-row">
|
|
80
97
|
<span class="label">Node ID:</span>
|
|
81
|
-
<span class="value mono">${this.escapeHtml(identityData.nodeId ||
|
|
98
|
+
<span class="value mono">${this.escapeHtml(identityData.nodeId || '未知')}</span>
|
|
82
99
|
</div>
|
|
83
100
|
<div class="p2p-actions">
|
|
84
|
-
<button class="p2p-btn-secondary" id="p2p-copy-cid-btn"
|
|
85
|
-
<button class="p2p-btn-secondary" id="p2p-copy-did-btn"
|
|
86
|
-
<button class="p2p-btn-secondary" id="p2p-copy-nodeid-btn"
|
|
101
|
+
<button class="p2p-btn-secondary" id="p2p-copy-cid-btn">复制 CID</button>
|
|
102
|
+
<button class="p2p-btn-secondary" id="p2p-copy-did-btn">复制 DID</button>
|
|
103
|
+
<button class="p2p-btn-secondary" id="p2p-copy-nodeid-btn">复制 Node ID</button>
|
|
87
104
|
</div>
|
|
88
105
|
</div>
|
|
89
106
|
`;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
identityHtml = '<div class="p2p-error">无法加载身份信息</div>';
|
|
110
|
+
}
|
|
111
|
+
this.modal.innerHTML = `
|
|
94
112
|
<div class="p2p-header">
|
|
95
|
-
<h2>P2P
|
|
96
|
-
<button class="p2p-close" id="p2p-close-btn"
|
|
113
|
+
<h2>P2P 网络</h2>
|
|
114
|
+
<button class="p2p-close" id="p2p-close-btn">×</button>
|
|
97
115
|
</div>
|
|
98
116
|
<div class="p2p-tabs">
|
|
99
|
-
<button class="p2p-tab active" data-tab="identity"
|
|
100
|
-
<button class="p2p-tab" data-tab="connect"
|
|
101
|
-
<button class="p2p-tab" data-tab="messages"
|
|
117
|
+
<button class="p2p-tab active" data-tab="identity">身份</button>
|
|
118
|
+
<button class="p2p-tab" data-tab="connect">连接</button>
|
|
119
|
+
<button class="p2p-tab" data-tab="messages">消息</button>
|
|
102
120
|
</div>
|
|
103
121
|
<div class="p2p-content">
|
|
104
122
|
<div class="p2p-tab-content active" data-tab="identity">
|
|
@@ -106,192 +124,213 @@ class P2PModalUI {
|
|
|
106
124
|
</div>
|
|
107
125
|
<div class="p2p-tab-content" data-tab="connect">
|
|
108
126
|
<div class="p2p-section">
|
|
109
|
-
<h3
|
|
127
|
+
<h3>连接到节点</h3>
|
|
110
128
|
<div class="p2p-connect-form">
|
|
111
|
-
<input type="text" id="p2p-connect-input" placeholder="
|
|
112
|
-
<button class="p2p-btn-primary" id="p2p-connect-btn"
|
|
129
|
+
<input type="text" id="p2p-connect-input" placeholder="输入节点 ID 或 CID">
|
|
130
|
+
<button class="p2p-btn-primary" id="p2p-connect-btn">连接</button>
|
|
113
131
|
</div>
|
|
114
132
|
<div id="p2p-connect-result" class="p2p-result"></div>
|
|
115
133
|
</div>
|
|
116
134
|
<div class="p2p-section">
|
|
117
|
-
<h3
|
|
135
|
+
<h3>已连接的节点</h3>
|
|
118
136
|
<div id="p2p-connected-peers" class="p2p-connected-peers">
|
|
119
|
-
|
|
137
|
+
暂无连接
|
|
120
138
|
</div>
|
|
121
139
|
</div>
|
|
122
140
|
</div>
|
|
123
141
|
<div class="p2p-tab-content" data-tab="messages">
|
|
124
142
|
<div class="p2p-section">
|
|
125
|
-
<h3
|
|
143
|
+
<h3>收到的消息</h3>
|
|
126
144
|
<div id="p2p-messages-list" class="p2p-messages">
|
|
127
|
-
|
|
145
|
+
暂无消息
|
|
128
146
|
</div>
|
|
129
147
|
</div>
|
|
130
148
|
</div>
|
|
131
149
|
</div>
|
|
132
150
|
`;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
151
|
+
// 绑定事件
|
|
152
|
+
this.bindEvents();
|
|
153
|
+
}
|
|
154
|
+
bindEvents() {
|
|
155
|
+
if (!this.modal)
|
|
156
|
+
return;
|
|
157
|
+
// 关闭按钮
|
|
158
|
+
const closeBtn = this.modal.querySelector('#p2p-close-btn');
|
|
159
|
+
closeBtn?.addEventListener('click', () => this.hide());
|
|
160
|
+
// 标签切换
|
|
161
|
+
this.modal.querySelectorAll('.p2p-tab').forEach(tab => {
|
|
162
|
+
tab.addEventListener('click', (e) => {
|
|
163
|
+
const target = e.target;
|
|
164
|
+
const tabName = target.dataset.tab;
|
|
165
|
+
if (tabName)
|
|
166
|
+
this.switchTab(tabName);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
// 复制 CID
|
|
170
|
+
const copyCidBtn = this.modal.querySelector('#p2p-copy-cid-btn');
|
|
171
|
+
copyCidBtn?.addEventListener('click', async () => {
|
|
172
|
+
try {
|
|
173
|
+
const irohResp = await fetch('/api/iroh/init', { method: 'POST' });
|
|
174
|
+
const data = await irohResp.json();
|
|
175
|
+
if (data.cid) {
|
|
176
|
+
await navigator.clipboard.writeText(data.cid);
|
|
177
|
+
this.showToast('CID 已复制到剪贴板');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
console.error('复制失败:', e);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// 复制 DID
|
|
185
|
+
const copyDidBtn = this.modal.querySelector('#p2p-copy-did-btn');
|
|
186
|
+
copyDidBtn?.addEventListener('click', async () => {
|
|
187
|
+
try {
|
|
188
|
+
const irohResp = await fetch('/api/iroh/init', { method: 'POST' });
|
|
189
|
+
const data = await irohResp.json();
|
|
190
|
+
if (data.did) {
|
|
191
|
+
await navigator.clipboard.writeText(data.did);
|
|
192
|
+
this.showToast('DID 已复制到剪贴板');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
console.error('复制失败:', e);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
// 复制 Node ID
|
|
200
|
+
const copyNodeIdBtn = this.modal.querySelector('#p2p-copy-nodeid-btn');
|
|
201
|
+
copyNodeIdBtn?.addEventListener('click', async () => {
|
|
202
|
+
try {
|
|
203
|
+
const irohResp = await fetch('/api/iroh/init', { method: 'POST' });
|
|
204
|
+
const data = await irohResp.json();
|
|
205
|
+
if (data.irohNodeId) {
|
|
206
|
+
await navigator.clipboard.writeText(data.irohNodeId);
|
|
207
|
+
this.showToast('Node ID 已复制到剪贴板');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
console.error('复制失败:', e);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
// 连接按钮
|
|
215
|
+
const connectBtn = this.modal.querySelector('#p2p-connect-btn');
|
|
216
|
+
connectBtn?.addEventListener('click', () => this.handleConnect());
|
|
217
|
+
}
|
|
218
|
+
switchTab(tabName) {
|
|
219
|
+
if (!this.modal)
|
|
220
|
+
return;
|
|
221
|
+
this.modal.querySelectorAll('.p2p-tab').forEach(tab => {
|
|
222
|
+
tab.classList.toggle('active', tab.getAttribute('data-tab') === tabName);
|
|
223
|
+
});
|
|
224
|
+
this.modal.querySelectorAll('.p2p-tab-content').forEach(content => {
|
|
225
|
+
content.classList.toggle('active', content.getAttribute('data-tab') === tabName);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async handleConnect() {
|
|
229
|
+
const input = document.getElementById('p2p-connect-input');
|
|
230
|
+
const result = document.getElementById('p2p-connect-result');
|
|
231
|
+
if (!input || !result)
|
|
232
|
+
return;
|
|
233
|
+
const cid = input.value.trim();
|
|
234
|
+
if (!cid)
|
|
235
|
+
return;
|
|
236
|
+
result.innerHTML = '<div class="p2p-loading">连接中...</div>';
|
|
237
|
+
result.className = 'p2p-result';
|
|
238
|
+
try {
|
|
239
|
+
const resp = await fetch('/api/iroh/connect', {
|
|
240
|
+
method: 'POST',
|
|
241
|
+
headers: { 'Content-Type': 'application/json' },
|
|
242
|
+
body: JSON.stringify({ cid })
|
|
243
|
+
});
|
|
244
|
+
const data = await resp.json();
|
|
245
|
+
if (data.ok) {
|
|
246
|
+
result.innerHTML = `<div class="p2p-success">连接成功!节点: ${this.escapeHtml(this.safeName(data.nodeName, cid.substring(0, 16)))}...</div>`;
|
|
247
|
+
result.className = 'p2p-result success';
|
|
248
|
+
// 清空输入框
|
|
249
|
+
input.value = '';
|
|
250
|
+
// 保存已连接节点到后端
|
|
251
|
+
try {
|
|
252
|
+
await fetch('/api/p2p/history', {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: { 'Content-Type': 'application/json' },
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
cid: cid, // 输入的 CID 或 Node ID
|
|
257
|
+
irohNodeId: data.targetNodeId,
|
|
258
|
+
did: data.targetNodeId, // 用 Node ID 作为 DID
|
|
259
|
+
name: data.nodeName || cid.substring(0, 16)
|
|
260
|
+
})
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
console.error('保存连接失败:', e);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
result.innerHTML = `<div class="p2p-error">${this.escapeHtml(data.error || '连接失败')}</div>`;
|
|
269
|
+
result.className = 'p2p-result error';
|
|
270
|
+
}
|
|
167
271
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const copyNodeIdBtn = this.modal.querySelector("#p2p-copy-nodeid-btn");
|
|
173
|
-
copyNodeIdBtn?.addEventListener("click", async () => {
|
|
174
|
-
try {
|
|
175
|
-
const irohResp = await fetch("/api/iroh/init", { method: "POST" });
|
|
176
|
-
const data = await irohResp.json();
|
|
177
|
-
if (data.irohNodeId) {
|
|
178
|
-
await navigator.clipboard.writeText(data.irohNodeId);
|
|
179
|
-
this.showToast("Node ID \u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F");
|
|
272
|
+
catch (e) {
|
|
273
|
+
const msg = e instanceof Error ? e.message : '连接失败';
|
|
274
|
+
result.innerHTML = `<div class="p2p-error">${this.escapeHtml(msg)}</div>`;
|
|
275
|
+
result.className = 'p2p-result error';
|
|
180
276
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
const connectBtn = this.modal.querySelector("#p2p-connect-btn");
|
|
186
|
-
connectBtn?.addEventListener("click", () => this.handleConnect());
|
|
187
|
-
}
|
|
188
|
-
switchTab(tabName) {
|
|
189
|
-
if (!this.modal) return;
|
|
190
|
-
this.modal.querySelectorAll(".p2p-tab").forEach((tab) => {
|
|
191
|
-
tab.classList.toggle("active", tab.getAttribute("data-tab") === tabName);
|
|
192
|
-
});
|
|
193
|
-
this.modal.querySelectorAll(".p2p-tab-content").forEach((content) => {
|
|
194
|
-
content.classList.toggle("active", content.getAttribute("data-tab") === tabName);
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
async handleConnect() {
|
|
198
|
-
const input = document.getElementById("p2p-connect-input");
|
|
199
|
-
const result = document.getElementById("p2p-connect-result");
|
|
200
|
-
if (!input || !result) return;
|
|
201
|
-
const cid = input.value.trim();
|
|
202
|
-
if (!cid) return;
|
|
203
|
-
result.innerHTML = '<div class="p2p-loading">\u8FDE\u63A5\u4E2D...</div>';
|
|
204
|
-
result.className = "p2p-result";
|
|
205
|
-
try {
|
|
206
|
-
const resp = await fetch("/api/iroh/connect", {
|
|
207
|
-
method: "POST",
|
|
208
|
-
headers: { "Content-Type": "application/json" },
|
|
209
|
-
body: JSON.stringify({ cid })
|
|
210
|
-
});
|
|
211
|
-
const data = await resp.json();
|
|
212
|
-
if (data.ok) {
|
|
213
|
-
result.innerHTML = `<div class="p2p-success">\u8FDE\u63A5\u6210\u529F\uFF01\u8282\u70B9: ${this.escapeHtml(this.safeName(data.nodeName, cid.substring(0, 16)))}...</div>`;
|
|
214
|
-
result.className = "p2p-result success";
|
|
215
|
-
input.value = "";
|
|
277
|
+
}
|
|
278
|
+
async loadConnectionHistory() {
|
|
216
279
|
try {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
280
|
+
const resp = await fetch('/api/p2p/history');
|
|
281
|
+
const history = await resp.json();
|
|
282
|
+
this.connectedPeers.clear();
|
|
283
|
+
for (const entry of history) {
|
|
284
|
+
this.connectedPeers.set(entry.irohNodeId || entry.did, {
|
|
285
|
+
nodeId: entry.irohNodeId || entry.did,
|
|
286
|
+
name: entry.name || 'Unknown',
|
|
287
|
+
time: entry.lastConnectedAt || Date.now()
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
this.updateConnectedPeersDisplay();
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
console.error('加载连接历史失败:', e);
|
|
231
294
|
}
|
|
232
|
-
} else {
|
|
233
|
-
result.innerHTML = `<div class="p2p-error">${this.escapeHtml(data.error || "\u8FDE\u63A5\u5931\u8D25")}</div>`;
|
|
234
|
-
result.className = "p2p-result error";
|
|
235
|
-
}
|
|
236
|
-
} catch (e) {
|
|
237
|
-
const msg = e instanceof Error ? e.message : "\u8FDE\u63A5\u5931\u8D25";
|
|
238
|
-
result.innerHTML = `<div class="p2p-error">${this.escapeHtml(msg)}</div>`;
|
|
239
|
-
result.className = "p2p-result error";
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
async loadConnectionHistory() {
|
|
243
|
-
try {
|
|
244
|
-
const resp = await fetch("/api/p2p/history");
|
|
245
|
-
const history = await resp.json();
|
|
246
|
-
this.connectedPeers.clear();
|
|
247
|
-
for (const entry of history) {
|
|
248
|
-
this.connectedPeers.set(entry.irohNodeId || entry.did, {
|
|
249
|
-
nodeId: entry.irohNodeId || entry.did,
|
|
250
|
-
name: entry.name || "Unknown",
|
|
251
|
-
time: entry.lastConnectedAt || Date.now()
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
this.updateConnectedPeersDisplay();
|
|
255
|
-
} catch (e) {
|
|
256
|
-
console.error("\u52A0\u8F7D\u8FDE\u63A5\u5386\u53F2\u5931\u8D25:", e);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
updateConnectedPeersDisplay() {
|
|
260
|
-
const container = document.getElementById("p2p-connected-peers");
|
|
261
|
-
if (!container) return;
|
|
262
|
-
if (this.connectedPeers.size === 0) {
|
|
263
|
-
container.innerHTML = '<div class="p2p-empty">\u6682\u65E0\u8FDE\u63A5</div>';
|
|
264
|
-
return;
|
|
265
295
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
296
|
+
updateConnectedPeersDisplay() {
|
|
297
|
+
const container = document.getElementById('p2p-connected-peers');
|
|
298
|
+
if (!container)
|
|
299
|
+
return;
|
|
300
|
+
if (this.connectedPeers.size === 0) {
|
|
301
|
+
container.innerHTML = '<div class="p2p-empty">暂无连接</div>';
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
let html = '';
|
|
305
|
+
for (const [nodeId, info] of this.connectedPeers) {
|
|
306
|
+
const shortId = nodeId.substring(0, 16) + '...';
|
|
307
|
+
const time = new Date(info.time).toLocaleTimeString();
|
|
308
|
+
html += `
|
|
271
309
|
<div class="p2p-peer-item">
|
|
272
310
|
<span class="p2p-peer-name">${this.escapeHtml(this.safeName(info.name, shortId))}</span>
|
|
273
311
|
<span class="p2p-peer-id">${shortId}</span>
|
|
274
312
|
<span class="p2p-peer-time">${time}</span>
|
|
275
313
|
</div>
|
|
276
314
|
`;
|
|
315
|
+
}
|
|
316
|
+
container.innerHTML = html;
|
|
277
317
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
toast.style.cssText = `
|
|
318
|
+
escapeHtml(str) {
|
|
319
|
+
const div = document.createElement('div');
|
|
320
|
+
div.textContent = str;
|
|
321
|
+
return div.innerHTML;
|
|
322
|
+
}
|
|
323
|
+
// 2026-07-06: 通用 name 兜底 — 防止 name=undefined/null/'undefined' 字串
|
|
324
|
+
// 时 UI 渲染字面量 "undefined".
|
|
325
|
+
safeName(input, fallback) {
|
|
326
|
+
// 2026-07-06: 委托共享 safe-name.ts (有单测覆盖); 保持方法签名兼容旧调用.
|
|
327
|
+
return safeAnyName(input, fallback || '未知');
|
|
328
|
+
}
|
|
329
|
+
showToast(message) {
|
|
330
|
+
const toast = document.createElement('div');
|
|
331
|
+
toast.className = 'p2p-toast';
|
|
332
|
+
toast.textContent = message;
|
|
333
|
+
toast.style.cssText = `
|
|
295
334
|
position: fixed;
|
|
296
335
|
bottom: 24px;
|
|
297
336
|
left: 50%;
|
|
@@ -304,22 +343,23 @@ class P2PModalUI {
|
|
|
304
343
|
font-size: 14px;
|
|
305
344
|
z-index: 2000;
|
|
306
345
|
`;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
show() {
|
|
311
|
-
if (this.overlay) {
|
|
312
|
-
this.overlay.style.display = "flex";
|
|
313
|
-
this.render();
|
|
346
|
+
document.body.appendChild(toast);
|
|
347
|
+
setTimeout(() => toast.remove(), 2000);
|
|
314
348
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
349
|
+
show() {
|
|
350
|
+
if (this.overlay) {
|
|
351
|
+
this.overlay.style.display = 'flex';
|
|
352
|
+
this.render();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
hide() {
|
|
356
|
+
if (this.overlay) {
|
|
357
|
+
this.overlay.style.display = 'none';
|
|
358
|
+
}
|
|
319
359
|
}
|
|
320
|
-
}
|
|
321
360
|
}
|
|
322
|
-
|
|
361
|
+
// 样式
|
|
362
|
+
const style = document.createElement('style');
|
|
323
363
|
style.textContent = `
|
|
324
364
|
.p2p-modal-overlay { display: none; }
|
|
325
365
|
.p2p-header {
|
|
@@ -508,8 +548,10 @@ style.textContent = `
|
|
|
508
548
|
}
|
|
509
549
|
`;
|
|
510
550
|
document.head.appendChild(style);
|
|
551
|
+
// 全局实例
|
|
511
552
|
const p2pModal = new P2PModalUI();
|
|
553
|
+
// 导出到 window
|
|
512
554
|
window.p2pModal = p2pModal;
|
|
513
555
|
window.showP2PModal = () => p2pModal.show();
|
|
514
556
|
window.hideP2PModal = () => p2pModal.hide();
|
|
515
|
-
console.log(
|
|
557
|
+
console.log('[P2P Modal] 已初始化');
|