@bolloon/bolloon-agent 0.3.17 → 0.3.18
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/web/client.js +4833 -4328
- package/dist/web/components/p2p/index.js +234 -276
- package/dist/web/ui/message-renderer.js +396 -535
- package/dist/web/ui/step-timeline.js +272 -371
- package/package.json +1 -1
- package/dist/web/components/p2p/P2PModal.js +0 -188
- package/dist/web/components/p2p/p2p-modal.js +0 -664
- package/dist/web/components/p2p/p2p-tools.js +0 -248
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
* P2P Modal - 纯 TypeScript 版本
|
|
3
|
-
*/
|
|
4
|
-
import { safeName as safeAnyName } from '../../util/safe-name.js';
|
|
1
|
+
import { safeName as safeAnyName } from "../../util/safe-name.js";
|
|
5
2
|
class P2PModalUI {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
this.overlay.style.cssText = `
|
|
3
|
+
modal = null;
|
|
4
|
+
overlay = null;
|
|
5
|
+
connectedPeers = /* @__PURE__ */ new Map();
|
|
6
|
+
constructor() {
|
|
7
|
+
this.createModal();
|
|
8
|
+
}
|
|
9
|
+
createModal() {
|
|
10
|
+
this.overlay = document.createElement("div");
|
|
11
|
+
this.overlay.className = "p2p-modal-overlay";
|
|
12
|
+
this.overlay.style.cssText = `
|
|
17
13
|
display: none;
|
|
18
14
|
position: fixed;
|
|
19
15
|
inset: 0;
|
|
@@ -22,10 +18,9 @@ class P2PModalUI {
|
|
|
22
18
|
align-items: center;
|
|
23
19
|
justify-content: center;
|
|
24
20
|
`;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
this.modal.style.cssText = `
|
|
21
|
+
this.modal = document.createElement("div");
|
|
22
|
+
this.modal.className = "p2p-modal";
|
|
23
|
+
this.modal.style.cssText = `
|
|
29
24
|
width: 90%;
|
|
30
25
|
max-width: 650px;
|
|
31
26
|
max-height: 90vh;
|
|
@@ -36,87 +31,74 @@ class P2PModalUI {
|
|
|
36
31
|
display: flex;
|
|
37
32
|
flex-direction: column;
|
|
38
33
|
`;
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 = `
|
|
34
|
+
this.overlay.appendChild(this.modal);
|
|
35
|
+
document.body.appendChild(this.overlay);
|
|
36
|
+
this.overlay.addEventListener("click", (e) => {
|
|
37
|
+
if (e.target === this.overlay) this.hide();
|
|
38
|
+
});
|
|
39
|
+
this.render();
|
|
40
|
+
}
|
|
41
|
+
async render() {
|
|
42
|
+
if (!this.modal) return;
|
|
43
|
+
await this.loadConnectionHistory();
|
|
44
|
+
let identityHtml = '<div class="p2p-loading">\u52A0\u8F7D\u4E2D...</div>';
|
|
45
|
+
let identityData = { name: "\u672A\u77E5", did: "\u672A\u77E5", cid: "\u672A\u77E5" };
|
|
46
|
+
try {
|
|
47
|
+
const irohResp = await fetch("/api/iroh/init", { method: "POST" });
|
|
48
|
+
const irohData = await irohResp.json();
|
|
49
|
+
if (irohData.cid) {
|
|
50
|
+
const rawName = typeof irohData.name === "string" && irohData.name.trim() && irohData.name.trim() !== "undefined" && irohData.name.trim() !== "null" ? irohData.name.trim() : "";
|
|
51
|
+
const tailName = rawName.split("-").slice(-1)[0] || "";
|
|
52
|
+
identityData = {
|
|
53
|
+
name: rawName || tailName || "Bolloon",
|
|
54
|
+
did: irohData.did || "\u672A\u77E5",
|
|
55
|
+
cid: irohData.cid || "\u672A\u77E5",
|
|
56
|
+
nodeId: irohData.irohNodeId ? irohData.irohNodeId.substring(0, 20) + "..." : "\u672A\u77E5"
|
|
57
|
+
};
|
|
58
|
+
} else {
|
|
59
|
+
const resp = await fetch("/api/identity");
|
|
60
|
+
identityData = await resp.json();
|
|
61
|
+
identityData.cid = identityData.cid || "\u672A\u521D\u59CB\u5316";
|
|
62
|
+
identityData.nodeId = identityData.nodeId || "\u672A\u77E5";
|
|
63
|
+
}
|
|
64
|
+
identityHtml = `
|
|
82
65
|
<div class="p2p-section">
|
|
83
|
-
<h3
|
|
66
|
+
<h3>\u8EAB\u4EFD\u4FE1\u606F</h3>
|
|
84
67
|
<div class="p2p-info-row">
|
|
85
|
-
<span class="label"
|
|
86
|
-
<span class="value">${this.escapeHtml(this.safeName(identityData.name,
|
|
68
|
+
<span class="label">\u540D\u79F0:</span>
|
|
69
|
+
<span class="value">${this.escapeHtml(this.safeName(identityData.name, "\u672A\u77E5"))}</span>
|
|
87
70
|
</div>
|
|
88
71
|
<div class="p2p-info-row">
|
|
89
72
|
<span class="label">CID:</span>
|
|
90
|
-
<span class="value mono">${this.escapeHtml(identityData.cid ||
|
|
73
|
+
<span class="value mono">${this.escapeHtml(identityData.cid || "\u672A\u77E5")}</span>
|
|
91
74
|
</div>
|
|
92
75
|
<div class="p2p-info-row">
|
|
93
76
|
<span class="label">DID:</span>
|
|
94
|
-
<span class="value mono">${this.escapeHtml(identityData.did ||
|
|
77
|
+
<span class="value mono">${this.escapeHtml(identityData.did || "\u672A\u77E5")}</span>
|
|
95
78
|
</div>
|
|
96
79
|
<div class="p2p-info-row">
|
|
97
80
|
<span class="label">Node ID:</span>
|
|
98
|
-
<span class="value mono">${this.escapeHtml(identityData.nodeId ||
|
|
81
|
+
<span class="value mono">${this.escapeHtml(identityData.nodeId || "\u672A\u77E5")}</span>
|
|
99
82
|
</div>
|
|
100
83
|
<div class="p2p-actions">
|
|
101
|
-
<button class="p2p-btn-secondary" id="p2p-copy-cid-btn"
|
|
102
|
-
<button class="p2p-btn-secondary" id="p2p-copy-did-btn"
|
|
103
|
-
<button class="p2p-btn-secondary" id="p2p-copy-nodeid-btn"
|
|
84
|
+
<button class="p2p-btn-secondary" id="p2p-copy-cid-btn">\u590D\u5236 CID</button>
|
|
85
|
+
<button class="p2p-btn-secondary" id="p2p-copy-did-btn">\u590D\u5236 DID</button>
|
|
86
|
+
<button class="p2p-btn-secondary" id="p2p-copy-nodeid-btn">\u590D\u5236 Node ID</button>
|
|
104
87
|
</div>
|
|
105
88
|
</div>
|
|
106
89
|
`;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
this.modal.innerHTML = `
|
|
90
|
+
} catch {
|
|
91
|
+
identityHtml = '<div class="p2p-error">\u65E0\u6CD5\u52A0\u8F7D\u8EAB\u4EFD\u4FE1\u606F</div>';
|
|
92
|
+
}
|
|
93
|
+
this.modal.innerHTML = `
|
|
112
94
|
<div class="p2p-header">
|
|
113
|
-
<h2>P2P
|
|
114
|
-
<button class="p2p-close" id="p2p-close-btn"
|
|
95
|
+
<h2>P2P \u7F51\u7EDC</h2>
|
|
96
|
+
<button class="p2p-close" id="p2p-close-btn">\xD7</button>
|
|
115
97
|
</div>
|
|
116
98
|
<div class="p2p-tabs">
|
|
117
|
-
<button class="p2p-tab active" data-tab="identity"
|
|
118
|
-
<button class="p2p-tab" data-tab="connect"
|
|
119
|
-
<button class="p2p-tab" data-tab="messages"
|
|
99
|
+
<button class="p2p-tab active" data-tab="identity">\u8EAB\u4EFD</button>
|
|
100
|
+
<button class="p2p-tab" data-tab="connect">\u8FDE\u63A5</button>
|
|
101
|
+
<button class="p2p-tab" data-tab="messages">\u6D88\u606F</button>
|
|
120
102
|
</div>
|
|
121
103
|
<div class="p2p-content">
|
|
122
104
|
<div class="p2p-tab-content active" data-tab="identity">
|
|
@@ -124,213 +106,192 @@ class P2PModalUI {
|
|
|
124
106
|
</div>
|
|
125
107
|
<div class="p2p-tab-content" data-tab="connect">
|
|
126
108
|
<div class="p2p-section">
|
|
127
|
-
<h3
|
|
109
|
+
<h3>\u8FDE\u63A5\u5230\u8282\u70B9</h3>
|
|
128
110
|
<div class="p2p-connect-form">
|
|
129
|
-
<input type="text" id="p2p-connect-input" placeholder="
|
|
130
|
-
<button class="p2p-btn-primary" id="p2p-connect-btn"
|
|
111
|
+
<input type="text" id="p2p-connect-input" placeholder="\u8F93\u5165\u8282\u70B9 ID \u6216 CID">
|
|
112
|
+
<button class="p2p-btn-primary" id="p2p-connect-btn">\u8FDE\u63A5</button>
|
|
131
113
|
</div>
|
|
132
114
|
<div id="p2p-connect-result" class="p2p-result"></div>
|
|
133
115
|
</div>
|
|
134
116
|
<div class="p2p-section">
|
|
135
|
-
<h3
|
|
117
|
+
<h3>\u5DF2\u8FDE\u63A5\u7684\u8282\u70B9</h3>
|
|
136
118
|
<div id="p2p-connected-peers" class="p2p-connected-peers">
|
|
137
|
-
|
|
119
|
+
\u6682\u65E0\u8FDE\u63A5
|
|
138
120
|
</div>
|
|
139
121
|
</div>
|
|
140
122
|
</div>
|
|
141
123
|
<div class="p2p-tab-content" data-tab="messages">
|
|
142
124
|
<div class="p2p-section">
|
|
143
|
-
<h3
|
|
125
|
+
<h3>\u6536\u5230\u7684\u6D88\u606F</h3>
|
|
144
126
|
<div id="p2p-messages-list" class="p2p-messages">
|
|
145
|
-
|
|
127
|
+
\u6682\u65E0\u6D88\u606F
|
|
146
128
|
</div>
|
|
147
129
|
</div>
|
|
148
130
|
</div>
|
|
149
131
|
</div>
|
|
150
132
|
`;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
this.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
}
|
|
133
|
+
this.bindEvents();
|
|
134
|
+
}
|
|
135
|
+
bindEvents() {
|
|
136
|
+
if (!this.modal) return;
|
|
137
|
+
const closeBtn = this.modal.querySelector("#p2p-close-btn");
|
|
138
|
+
closeBtn?.addEventListener("click", () => this.hide());
|
|
139
|
+
this.modal.querySelectorAll(".p2p-tab").forEach((tab) => {
|
|
140
|
+
tab.addEventListener("click", (e) => {
|
|
141
|
+
const target = e.target;
|
|
142
|
+
const tabName = target.dataset.tab;
|
|
143
|
+
if (tabName) this.switchTab(tabName);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
const copyCidBtn = this.modal.querySelector("#p2p-copy-cid-btn");
|
|
147
|
+
copyCidBtn?.addEventListener("click", async () => {
|
|
148
|
+
try {
|
|
149
|
+
const irohResp = await fetch("/api/iroh/init", { method: "POST" });
|
|
150
|
+
const data = await irohResp.json();
|
|
151
|
+
if (data.cid) {
|
|
152
|
+
await navigator.clipboard.writeText(data.cid);
|
|
153
|
+
this.showToast("CID \u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F");
|
|
271
154
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
155
|
+
} catch (e) {
|
|
156
|
+
console.error("\u590D\u5236\u5931\u8D25:", e);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
const copyDidBtn = this.modal.querySelector("#p2p-copy-did-btn");
|
|
160
|
+
copyDidBtn?.addEventListener("click", async () => {
|
|
161
|
+
try {
|
|
162
|
+
const irohResp = await fetch("/api/iroh/init", { method: "POST" });
|
|
163
|
+
const data = await irohResp.json();
|
|
164
|
+
if (data.did) {
|
|
165
|
+
await navigator.clipboard.writeText(data.did);
|
|
166
|
+
this.showToast("DID \u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F");
|
|
276
167
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
this.updateConnectedPeersDisplay();
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.error("\u590D\u5236\u5931\u8D25:", e);
|
|
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");
|
|
291
180
|
}
|
|
292
|
-
|
|
293
|
-
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.error("\u590D\u5236\u5931\u8D25:", e);
|
|
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 = "";
|
|
216
|
+
try {
|
|
217
|
+
await fetch("/api/p2p/history", {
|
|
218
|
+
method: "POST",
|
|
219
|
+
headers: { "Content-Type": "application/json" },
|
|
220
|
+
body: JSON.stringify({
|
|
221
|
+
cid,
|
|
222
|
+
// 输入的 CID 或 Node ID
|
|
223
|
+
irohNodeId: data.targetNodeId,
|
|
224
|
+
did: data.targetNodeId,
|
|
225
|
+
// 用 Node ID 作为 DID
|
|
226
|
+
name: data.nodeName || cid.substring(0, 16)
|
|
227
|
+
})
|
|
228
|
+
});
|
|
229
|
+
} catch (e) {
|
|
230
|
+
console.error("\u4FDD\u5B58\u8FDE\u63A5\u5931\u8D25:", e);
|
|
294
231
|
}
|
|
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";
|
|
295
240
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
+
}
|
|
266
|
+
let html = "";
|
|
267
|
+
for (const [nodeId, info] of this.connectedPeers) {
|
|
268
|
+
const shortId = nodeId.substring(0, 16) + "...";
|
|
269
|
+
const time = new Date(info.time).toLocaleTimeString();
|
|
270
|
+
html += `
|
|
309
271
|
<div class="p2p-peer-item">
|
|
310
272
|
<span class="p2p-peer-name">${this.escapeHtml(this.safeName(info.name, shortId))}</span>
|
|
311
273
|
<span class="p2p-peer-id">${shortId}</span>
|
|
312
274
|
<span class="p2p-peer-time">${time}</span>
|
|
313
275
|
</div>
|
|
314
276
|
`;
|
|
315
|
-
}
|
|
316
|
-
container.innerHTML = html;
|
|
317
|
-
}
|
|
318
|
-
escapeHtml(str) {
|
|
319
|
-
const div = document.createElement('div');
|
|
320
|
-
div.textContent = str;
|
|
321
|
-
return div.innerHTML;
|
|
322
277
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
278
|
+
container.innerHTML = html;
|
|
279
|
+
}
|
|
280
|
+
escapeHtml(str) {
|
|
281
|
+
const div = document.createElement("div");
|
|
282
|
+
div.textContent = str;
|
|
283
|
+
return div.innerHTML;
|
|
284
|
+
}
|
|
285
|
+
// 2026-07-06: 通用 name 兜底 — 防止 name=undefined/null/'undefined' 字串
|
|
286
|
+
// 时 UI 渲染字面量 "undefined".
|
|
287
|
+
safeName(input, fallback) {
|
|
288
|
+
return safeAnyName(input, fallback || "\u672A\u77E5");
|
|
289
|
+
}
|
|
290
|
+
showToast(message) {
|
|
291
|
+
const toast = document.createElement("div");
|
|
292
|
+
toast.className = "p2p-toast";
|
|
293
|
+
toast.textContent = message;
|
|
294
|
+
toast.style.cssText = `
|
|
334
295
|
position: fixed;
|
|
335
296
|
bottom: 24px;
|
|
336
297
|
left: 50%;
|
|
@@ -343,23 +304,22 @@ class P2PModalUI {
|
|
|
343
304
|
font-size: 14px;
|
|
344
305
|
z-index: 2000;
|
|
345
306
|
`;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
307
|
+
document.body.appendChild(toast);
|
|
308
|
+
setTimeout(() => toast.remove(), 2e3);
|
|
309
|
+
}
|
|
310
|
+
show() {
|
|
311
|
+
if (this.overlay) {
|
|
312
|
+
this.overlay.style.display = "flex";
|
|
313
|
+
this.render();
|
|
354
314
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
315
|
+
}
|
|
316
|
+
hide() {
|
|
317
|
+
if (this.overlay) {
|
|
318
|
+
this.overlay.style.display = "none";
|
|
359
319
|
}
|
|
320
|
+
}
|
|
360
321
|
}
|
|
361
|
-
|
|
362
|
-
const style = document.createElement('style');
|
|
322
|
+
const style = document.createElement("style");
|
|
363
323
|
style.textContent = `
|
|
364
324
|
.p2p-modal-overlay { display: none; }
|
|
365
325
|
.p2p-header {
|
|
@@ -548,10 +508,8 @@ style.textContent = `
|
|
|
548
508
|
}
|
|
549
509
|
`;
|
|
550
510
|
document.head.appendChild(style);
|
|
551
|
-
// 全局实例
|
|
552
511
|
const p2pModal = new P2PModalUI();
|
|
553
|
-
// 导出到 window
|
|
554
512
|
window.p2pModal = p2pModal;
|
|
555
513
|
window.showP2PModal = () => p2pModal.show();
|
|
556
514
|
window.hideP2PModal = () => p2pModal.hide();
|
|
557
|
-
console.log(
|
|
515
|
+
console.log("[P2P Modal] \u5DF2\u521D\u59CB\u5316");
|