@bolloon/bolloon-agent 0.3.21 → 0.3.22

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.
@@ -1,15 +1,19 @@
1
- import { safeName as safeAnyName } from "../../util/safe-name.js";
1
+ /**
2
+ * P2P Modal - 纯 TypeScript 版本
3
+ */
4
+ import { safeName as safeAnyName } from '../../util/safe-name.js';
2
5
  class P2PModalUI {
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 = `
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
- this.modal = document.createElement("div");
22
- this.modal.className = "p2p-modal";
23
- this.modal.style.cssText = `
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
- 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 = `
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>\u8EAB\u4EFD\u4FE1\u606F</h3>
83
+ <h3>身份信息</h3>
67
84
  <div class="p2p-info-row">
68
- <span class="label">\u540D\u79F0:</span>
69
- <span class="value">${this.escapeHtml(this.safeName(identityData.name, "\u672A\u77E5"))}</span>
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 || "\u672A\u77E5")}</span>
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 || "\u672A\u77E5")}</span>
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 || "\u672A\u77E5")}</span>
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">\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>
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
- } catch {
91
- identityHtml = '<div class="p2p-error">\u65E0\u6CD5\u52A0\u8F7D\u8EAB\u4EFD\u4FE1\u606F</div>';
92
- }
93
- this.modal.innerHTML = `
107
+ }
108
+ catch {
109
+ identityHtml = '<div class="p2p-error">无法加载身份信息</div>';
110
+ }
111
+ this.modal.innerHTML = `
94
112
  <div class="p2p-header">
95
- <h2>P2P \u7F51\u7EDC</h2>
96
- <button class="p2p-close" id="p2p-close-btn">\xD7</button>
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">\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>
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>\u8FDE\u63A5\u5230\u8282\u70B9</h3>
127
+ <h3>连接到节点</h3>
110
128
  <div class="p2p-connect-form">
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>
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>\u5DF2\u8FDE\u63A5\u7684\u8282\u70B9</h3>
135
+ <h3>已连接的节点</h3>
118
136
  <div id="p2p-connected-peers" class="p2p-connected-peers">
119
- \u6682\u65E0\u8FDE\u63A5
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>\u6536\u5230\u7684\u6D88\u606F</h3>
143
+ <h3>收到的消息</h3>
126
144
  <div id="p2p-messages-list" class="p2p-messages">
127
- \u6682\u65E0\u6D88\u606F
145
+ 暂无消息
128
146
  </div>
129
147
  </div>
130
148
  </div>
131
149
  </div>
132
150
  `;
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");
154
- }
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");
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
- } 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");
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
- } 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 = "";
277
+ }
278
+ async loadConnectionHistory() {
216
279
  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);
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
- 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 += `
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
- 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 = `
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
- 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();
346
+ document.body.appendChild(toast);
347
+ setTimeout(() => toast.remove(), 2000);
314
348
  }
315
- }
316
- hide() {
317
- if (this.overlay) {
318
- this.overlay.style.display = "none";
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
- const style = document.createElement("style");
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("[P2P Modal] \u5DF2\u521D\u59CB\u5316");
557
+ console.log('[P2P Modal] 已初始化');