@bolloon/bolloon-agent 0.1.2 → 0.1.4
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/bin/bolloon-cli.cjs +15 -7
- package/dist/agents/pi-sdk.js +12 -4
- package/dist/llm/config-store.js +8 -5
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +264 -226
- package/dist/web/components/p2p/p2p-modal.js +657 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/server.js +1890 -0
- package/package.json +1 -1
- package/src/web/server.ts +25 -26
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* P2P Modal - 纯 TypeScript 版本
|
|
4
|
+
*/
|
|
1
5
|
class P2PModalUI {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 = `
|
|
12
17
|
display: none;
|
|
13
18
|
position: fixed;
|
|
14
19
|
inset: 0;
|
|
@@ -17,9 +22,10 @@ class P2PModalUI {
|
|
|
17
22
|
align-items: center;
|
|
18
23
|
justify-content: center;
|
|
19
24
|
`;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
// 创建弹窗
|
|
26
|
+
this.modal = document.createElement('div');
|
|
27
|
+
this.modal.className = 'p2p-modal';
|
|
28
|
+
this.modal.style.cssText = `
|
|
23
29
|
width: 90%;
|
|
24
30
|
max-width: 650px;
|
|
25
31
|
max-height: 90vh;
|
|
@@ -30,72 +36,81 @@ class P2PModalUI {
|
|
|
30
36
|
display: flex;
|
|
31
37
|
flex-direction: column;
|
|
32
38
|
`;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
identityData = {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
identityData = {
|
|
62
|
+
name: irohData.name || irohData.name?.split('-').slice(-1)[0] || 'Bolloon',
|
|
63
|
+
did: irohData.did || '未知',
|
|
64
|
+
cid: irohData.cid || '未知',
|
|
65
|
+
nodeId: irohData.irohNodeId ? irohData.irohNodeId.substring(0, 20) + '...' : '未知'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// 回退到 /api/identity
|
|
70
|
+
const resp = await fetch('/api/identity');
|
|
71
|
+
identityData = await resp.json();
|
|
72
|
+
identityData.cid = identityData.cid || '未初始化';
|
|
73
|
+
identityData.nodeId = identityData.nodeId || '未知';
|
|
74
|
+
}
|
|
75
|
+
identityHtml = `
|
|
62
76
|
<div class="p2p-section">
|
|
63
|
-
<h3
|
|
77
|
+
<h3>身份信息</h3>
|
|
64
78
|
<div class="p2p-info-row">
|
|
65
|
-
<span class="label"
|
|
66
|
-
<span class="value">${this.escapeHtml(identityData.name ||
|
|
79
|
+
<span class="label">名称:</span>
|
|
80
|
+
<span class="value">${this.escapeHtml(identityData.name || '未知')}</span>
|
|
67
81
|
</div>
|
|
68
82
|
<div class="p2p-info-row">
|
|
69
83
|
<span class="label">CID:</span>
|
|
70
|
-
<span class="value mono">${this.escapeHtml(identityData.cid ||
|
|
84
|
+
<span class="value mono">${this.escapeHtml(identityData.cid || '未知')}</span>
|
|
71
85
|
</div>
|
|
72
86
|
<div class="p2p-info-row">
|
|
73
87
|
<span class="label">DID:</span>
|
|
74
|
-
<span class="value mono">${this.escapeHtml(identityData.did ||
|
|
88
|
+
<span class="value mono">${this.escapeHtml(identityData.did || '未知')}</span>
|
|
75
89
|
</div>
|
|
76
90
|
<div class="p2p-info-row">
|
|
77
91
|
<span class="label">Node ID:</span>
|
|
78
|
-
<span class="value mono">${this.escapeHtml(identityData.nodeId ||
|
|
92
|
+
<span class="value mono">${this.escapeHtml(identityData.nodeId || '未知')}</span>
|
|
79
93
|
</div>
|
|
80
94
|
<div class="p2p-actions">
|
|
81
|
-
<button class="p2p-btn-secondary" id="p2p-copy-cid-btn"
|
|
82
|
-
<button class="p2p-btn-secondary" id="p2p-copy-did-btn"
|
|
83
|
-
<button class="p2p-btn-secondary" id="p2p-copy-nodeid-btn"
|
|
95
|
+
<button class="p2p-btn-secondary" id="p2p-copy-cid-btn">复制 CID</button>
|
|
96
|
+
<button class="p2p-btn-secondary" id="p2p-copy-did-btn">复制 DID</button>
|
|
97
|
+
<button class="p2p-btn-secondary" id="p2p-copy-nodeid-btn">复制 Node ID</button>
|
|
84
98
|
</div>
|
|
85
99
|
</div>
|
|
86
100
|
`;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
identityHtml = '<div class="p2p-error">无法加载身份信息</div>';
|
|
104
|
+
}
|
|
105
|
+
this.modal.innerHTML = `
|
|
91
106
|
<div class="p2p-header">
|
|
92
|
-
<h2>P2P
|
|
93
|
-
<button class="p2p-close" id="p2p-close-btn"
|
|
107
|
+
<h2>P2P 网络</h2>
|
|
108
|
+
<button class="p2p-close" id="p2p-close-btn">×</button>
|
|
94
109
|
</div>
|
|
95
110
|
<div class="p2p-tabs">
|
|
96
|
-
<button class="p2p-tab active" data-tab="identity"
|
|
97
|
-
<button class="p2p-tab" data-tab="connect"
|
|
98
|
-
<button class="p2p-tab" data-tab="messages"
|
|
111
|
+
<button class="p2p-tab active" data-tab="identity">身份</button>
|
|
112
|
+
<button class="p2p-tab" data-tab="connect">连接</button>
|
|
113
|
+
<button class="p2p-tab" data-tab="messages">消息</button>
|
|
99
114
|
</div>
|
|
100
115
|
<div class="p2p-content">
|
|
101
116
|
<div class="p2p-tab-content active" data-tab="identity">
|
|
@@ -103,187 +118,207 @@ class P2PModalUI {
|
|
|
103
118
|
</div>
|
|
104
119
|
<div class="p2p-tab-content" data-tab="connect">
|
|
105
120
|
<div class="p2p-section">
|
|
106
|
-
<h3
|
|
121
|
+
<h3>连接到节点</h3>
|
|
107
122
|
<div class="p2p-connect-form">
|
|
108
|
-
<input type="text" id="p2p-connect-input" placeholder="
|
|
109
|
-
<button class="p2p-btn-primary" id="p2p-connect-btn"
|
|
123
|
+
<input type="text" id="p2p-connect-input" placeholder="输入节点 ID 或 CID">
|
|
124
|
+
<button class="p2p-btn-primary" id="p2p-connect-btn">连接</button>
|
|
110
125
|
</div>
|
|
111
126
|
<div id="p2p-connect-result" class="p2p-result"></div>
|
|
112
127
|
</div>
|
|
113
128
|
<div class="p2p-section">
|
|
114
|
-
<h3
|
|
129
|
+
<h3>已连接的节点</h3>
|
|
115
130
|
<div id="p2p-connected-peers" class="p2p-connected-peers">
|
|
116
|
-
|
|
131
|
+
暂无连接
|
|
117
132
|
</div>
|
|
118
133
|
</div>
|
|
119
134
|
</div>
|
|
120
135
|
<div class="p2p-tab-content" data-tab="messages">
|
|
121
136
|
<div class="p2p-section">
|
|
122
|
-
<h3
|
|
137
|
+
<h3>收到的消息</h3>
|
|
123
138
|
<div id="p2p-messages-list" class="p2p-messages">
|
|
124
|
-
|
|
139
|
+
暂无消息
|
|
125
140
|
</div>
|
|
126
141
|
</div>
|
|
127
142
|
</div>
|
|
128
143
|
</div>
|
|
129
144
|
`;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
145
|
+
// 绑定事件
|
|
146
|
+
this.bindEvents();
|
|
147
|
+
}
|
|
148
|
+
bindEvents() {
|
|
149
|
+
if (!this.modal)
|
|
150
|
+
return;
|
|
151
|
+
// 关闭按钮
|
|
152
|
+
const closeBtn = this.modal.querySelector('#p2p-close-btn');
|
|
153
|
+
closeBtn?.addEventListener('click', () => this.hide());
|
|
154
|
+
// 标签切换
|
|
155
|
+
this.modal.querySelectorAll('.p2p-tab').forEach(tab => {
|
|
156
|
+
tab.addEventListener('click', (e) => {
|
|
157
|
+
const target = e.target;
|
|
158
|
+
const tabName = target.dataset.tab;
|
|
159
|
+
if (tabName)
|
|
160
|
+
this.switchTab(tabName);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
// 复制 CID
|
|
164
|
+
const copyCidBtn = this.modal.querySelector('#p2p-copy-cid-btn');
|
|
165
|
+
copyCidBtn?.addEventListener('click', async () => {
|
|
166
|
+
try {
|
|
167
|
+
const irohResp = await fetch('/api/iroh/init', { method: 'POST' });
|
|
168
|
+
const data = await irohResp.json();
|
|
169
|
+
if (data.cid) {
|
|
170
|
+
await navigator.clipboard.writeText(data.cid);
|
|
171
|
+
this.showToast('CID 已复制到剪贴板');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
console.error('复制失败:', e);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
// 复制 DID
|
|
179
|
+
const copyDidBtn = this.modal.querySelector('#p2p-copy-did-btn');
|
|
180
|
+
copyDidBtn?.addEventListener('click', async () => {
|
|
181
|
+
try {
|
|
182
|
+
const irohResp = await fetch('/api/iroh/init', { method: 'POST' });
|
|
183
|
+
const data = await irohResp.json();
|
|
184
|
+
if (data.did) {
|
|
185
|
+
await navigator.clipboard.writeText(data.did);
|
|
186
|
+
this.showToast('DID 已复制到剪贴板');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
console.error('复制失败:', e);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
// 复制 Node ID
|
|
194
|
+
const copyNodeIdBtn = this.modal.querySelector('#p2p-copy-nodeid-btn');
|
|
195
|
+
copyNodeIdBtn?.addEventListener('click', async () => {
|
|
196
|
+
try {
|
|
197
|
+
const irohResp = await fetch('/api/iroh/init', { method: 'POST' });
|
|
198
|
+
const data = await irohResp.json();
|
|
199
|
+
if (data.irohNodeId) {
|
|
200
|
+
await navigator.clipboard.writeText(data.irohNodeId);
|
|
201
|
+
this.showToast('Node ID 已复制到剪贴板');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
console.error('复制失败:', e);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
// 连接按钮
|
|
209
|
+
const connectBtn = this.modal.querySelector('#p2p-connect-btn');
|
|
210
|
+
connectBtn?.addEventListener('click', () => this.handleConnect());
|
|
211
|
+
}
|
|
212
|
+
switchTab(tabName) {
|
|
213
|
+
if (!this.modal)
|
|
214
|
+
return;
|
|
215
|
+
this.modal.querySelectorAll('.p2p-tab').forEach(tab => {
|
|
216
|
+
tab.classList.toggle('active', tab.getAttribute('data-tab') === tabName);
|
|
217
|
+
});
|
|
218
|
+
this.modal.querySelectorAll('.p2p-tab-content').forEach(content => {
|
|
219
|
+
content.classList.toggle('active', content.getAttribute('data-tab') === tabName);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async handleConnect() {
|
|
223
|
+
const input = document.getElementById('p2p-connect-input');
|
|
224
|
+
const result = document.getElementById('p2p-connect-result');
|
|
225
|
+
if (!input || !result)
|
|
226
|
+
return;
|
|
227
|
+
const cid = input.value.trim();
|
|
228
|
+
if (!cid)
|
|
229
|
+
return;
|
|
230
|
+
result.innerHTML = '<div class="p2p-loading">连接中...</div>';
|
|
231
|
+
result.className = 'p2p-result';
|
|
232
|
+
try {
|
|
233
|
+
const resp = await fetch('/api/iroh/connect', {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
headers: { 'Content-Type': 'application/json' },
|
|
236
|
+
body: JSON.stringify({ cid })
|
|
237
|
+
});
|
|
238
|
+
const data = await resp.json();
|
|
239
|
+
if (data.ok) {
|
|
240
|
+
result.innerHTML = `<div class="p2p-success">连接成功!节点: ${this.escapeHtml(data.nodeName || cid.substring(0, 16))}...</div>`;
|
|
241
|
+
result.className = 'p2p-result success';
|
|
242
|
+
// 清空输入框
|
|
243
|
+
input.value = '';
|
|
244
|
+
// 保存已连接节点到后端
|
|
245
|
+
try {
|
|
246
|
+
await fetch('/api/p2p/history', {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
headers: { 'Content-Type': 'application/json' },
|
|
249
|
+
body: JSON.stringify({
|
|
250
|
+
cid: cid, // 输入的 CID 或 Node ID
|
|
251
|
+
irohNodeId: data.targetNodeId,
|
|
252
|
+
did: data.targetNodeId, // 用 Node ID 作为 DID
|
|
253
|
+
name: data.nodeName || cid.substring(0, 16)
|
|
254
|
+
})
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
console.error('保存连接失败:', e);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
result.innerHTML = `<div class="p2p-error">${this.escapeHtml(data.error || '连接失败')}</div>`;
|
|
263
|
+
result.className = 'p2p-result error';
|
|
264
|
+
}
|
|
164
265
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const copyNodeIdBtn = this.modal.querySelector("#p2p-copy-nodeid-btn");
|
|
170
|
-
copyNodeIdBtn?.addEventListener("click", async () => {
|
|
171
|
-
try {
|
|
172
|
-
const irohResp = await fetch("/api/iroh/init", { method: "POST" });
|
|
173
|
-
const data = await irohResp.json();
|
|
174
|
-
if (data.irohNodeId) {
|
|
175
|
-
await navigator.clipboard.writeText(data.irohNodeId);
|
|
176
|
-
this.showToast("Node ID \u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F");
|
|
266
|
+
catch (e) {
|
|
267
|
+
const msg = e instanceof Error ? e.message : '连接失败';
|
|
268
|
+
result.innerHTML = `<div class="p2p-error">${this.escapeHtml(msg)}</div>`;
|
|
269
|
+
result.className = 'p2p-result error';
|
|
177
270
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
const connectBtn = this.modal.querySelector("#p2p-connect-btn");
|
|
183
|
-
connectBtn?.addEventListener("click", () => this.handleConnect());
|
|
184
|
-
}
|
|
185
|
-
switchTab(tabName) {
|
|
186
|
-
if (!this.modal) return;
|
|
187
|
-
this.modal.querySelectorAll(".p2p-tab").forEach((tab) => {
|
|
188
|
-
tab.classList.toggle("active", tab.getAttribute("data-tab") === tabName);
|
|
189
|
-
});
|
|
190
|
-
this.modal.querySelectorAll(".p2p-tab-content").forEach((content) => {
|
|
191
|
-
content.classList.toggle("active", content.getAttribute("data-tab") === tabName);
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
async handleConnect() {
|
|
195
|
-
const input = document.getElementById("p2p-connect-input");
|
|
196
|
-
const result = document.getElementById("p2p-connect-result");
|
|
197
|
-
if (!input || !result) return;
|
|
198
|
-
const cid = input.value.trim();
|
|
199
|
-
if (!cid) return;
|
|
200
|
-
result.innerHTML = '<div class="p2p-loading">\u8FDE\u63A5\u4E2D...</div>';
|
|
201
|
-
result.className = "p2p-result";
|
|
202
|
-
try {
|
|
203
|
-
const resp = await fetch("/api/iroh/connect", {
|
|
204
|
-
method: "POST",
|
|
205
|
-
headers: { "Content-Type": "application/json" },
|
|
206
|
-
body: JSON.stringify({ cid })
|
|
207
|
-
});
|
|
208
|
-
const data = await resp.json();
|
|
209
|
-
if (data.ok) {
|
|
210
|
-
result.innerHTML = `<div class="p2p-success">\u8FDE\u63A5\u6210\u529F\uFF01\u8282\u70B9: ${this.escapeHtml(data.nodeName || cid.substring(0, 16))}...</div>`;
|
|
211
|
-
result.className = "p2p-result success";
|
|
212
|
-
input.value = "";
|
|
271
|
+
}
|
|
272
|
+
async loadConnectionHistory() {
|
|
213
273
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
274
|
+
const resp = await fetch('/api/p2p/history');
|
|
275
|
+
const history = await resp.json();
|
|
276
|
+
this.connectedPeers.clear();
|
|
277
|
+
for (const entry of history) {
|
|
278
|
+
this.connectedPeers.set(entry.irohNodeId || entry.did, {
|
|
279
|
+
nodeId: entry.irohNodeId || entry.did,
|
|
280
|
+
name: entry.name || 'Unknown',
|
|
281
|
+
time: entry.lastConnectedAt || Date.now()
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
this.updateConnectedPeersDisplay();
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
console.error('加载连接历史失败:', e);
|
|
228
288
|
}
|
|
229
|
-
} else {
|
|
230
|
-
result.innerHTML = `<div class="p2p-error">${this.escapeHtml(data.error || "\u8FDE\u63A5\u5931\u8D25")}</div>`;
|
|
231
|
-
result.className = "p2p-result error";
|
|
232
|
-
}
|
|
233
|
-
} catch (e) {
|
|
234
|
-
const msg = e instanceof Error ? e.message : "\u8FDE\u63A5\u5931\u8D25";
|
|
235
|
-
result.innerHTML = `<div class="p2p-error">${this.escapeHtml(msg)}</div>`;
|
|
236
|
-
result.className = "p2p-result error";
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
async loadConnectionHistory() {
|
|
240
|
-
try {
|
|
241
|
-
const resp = await fetch("/api/p2p/history");
|
|
242
|
-
const history = await resp.json();
|
|
243
|
-
this.connectedPeers.clear();
|
|
244
|
-
for (const entry of history) {
|
|
245
|
-
this.connectedPeers.set(entry.irohNodeId || entry.did, {
|
|
246
|
-
nodeId: entry.irohNodeId || entry.did,
|
|
247
|
-
name: entry.name || "Unknown",
|
|
248
|
-
time: entry.lastConnectedAt || Date.now()
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
this.updateConnectedPeersDisplay();
|
|
252
|
-
} catch (e) {
|
|
253
|
-
console.error("\u52A0\u8F7D\u8FDE\u63A5\u5386\u53F2\u5931\u8D25:", e);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
updateConnectedPeersDisplay() {
|
|
257
|
-
const container = document.getElementById("p2p-connected-peers");
|
|
258
|
-
if (!container) return;
|
|
259
|
-
if (this.connectedPeers.size === 0) {
|
|
260
|
-
container.innerHTML = '<div class="p2p-empty">\u6682\u65E0\u8FDE\u63A5</div>';
|
|
261
|
-
return;
|
|
262
289
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
290
|
+
updateConnectedPeersDisplay() {
|
|
291
|
+
const container = document.getElementById('p2p-connected-peers');
|
|
292
|
+
if (!container)
|
|
293
|
+
return;
|
|
294
|
+
if (this.connectedPeers.size === 0) {
|
|
295
|
+
container.innerHTML = '<div class="p2p-empty">暂无连接</div>';
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
let html = '';
|
|
299
|
+
for (const [nodeId, info] of this.connectedPeers) {
|
|
300
|
+
const shortId = nodeId.substring(0, 16) + '...';
|
|
301
|
+
const time = new Date(info.time).toLocaleTimeString();
|
|
302
|
+
html += `
|
|
268
303
|
<div class="p2p-peer-item">
|
|
269
304
|
<span class="p2p-peer-name">${this.escapeHtml(info.name || shortId)}</span>
|
|
270
305
|
<span class="p2p-peer-id">${shortId}</span>
|
|
271
306
|
<span class="p2p-peer-time">${time}</span>
|
|
272
307
|
</div>
|
|
273
308
|
`;
|
|
309
|
+
}
|
|
310
|
+
container.innerHTML = html;
|
|
274
311
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
toast.textContent = message;
|
|
286
|
-
toast.style.cssText = `
|
|
312
|
+
escapeHtml(str) {
|
|
313
|
+
const div = document.createElement('div');
|
|
314
|
+
div.textContent = str;
|
|
315
|
+
return div.innerHTML;
|
|
316
|
+
}
|
|
317
|
+
showToast(message) {
|
|
318
|
+
const toast = document.createElement('div');
|
|
319
|
+
toast.className = 'p2p-toast';
|
|
320
|
+
toast.textContent = message;
|
|
321
|
+
toast.style.cssText = `
|
|
287
322
|
position: fixed;
|
|
288
323
|
bottom: 24px;
|
|
289
324
|
left: 50%;
|
|
@@ -296,22 +331,23 @@ class P2PModalUI {
|
|
|
296
331
|
font-size: 14px;
|
|
297
332
|
z-index: 2000;
|
|
298
333
|
`;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
show() {
|
|
303
|
-
if (this.overlay) {
|
|
304
|
-
this.overlay.style.display = "flex";
|
|
305
|
-
this.render();
|
|
334
|
+
document.body.appendChild(toast);
|
|
335
|
+
setTimeout(() => toast.remove(), 2000);
|
|
306
336
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
337
|
+
show() {
|
|
338
|
+
if (this.overlay) {
|
|
339
|
+
this.overlay.style.display = 'flex';
|
|
340
|
+
this.render();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
hide() {
|
|
344
|
+
if (this.overlay) {
|
|
345
|
+
this.overlay.style.display = 'none';
|
|
346
|
+
}
|
|
311
347
|
}
|
|
312
|
-
}
|
|
313
348
|
}
|
|
314
|
-
|
|
349
|
+
// 样式
|
|
350
|
+
const style = document.createElement('style');
|
|
315
351
|
style.textContent = `
|
|
316
352
|
.p2p-modal-overlay { display: none; }
|
|
317
353
|
.p2p-header {
|
|
@@ -500,8 +536,10 @@ style.textContent = `
|
|
|
500
536
|
}
|
|
501
537
|
`;
|
|
502
538
|
document.head.appendChild(style);
|
|
539
|
+
// 全局实例
|
|
503
540
|
const p2pModal = new P2PModalUI();
|
|
541
|
+
// 导出到 window
|
|
504
542
|
window.p2pModal = p2pModal;
|
|
505
543
|
window.showP2PModal = () => p2pModal.show();
|
|
506
544
|
window.hideP2PModal = () => p2pModal.hide();
|
|
507
|
-
console.log(
|
|
545
|
+
console.log('[P2P Modal] 已初始化');
|