@bolloon/bolloon-agent 0.3.22 → 0.3.24

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.
Files changed (37) hide show
  1. package/dist/agents/deny-pipeline.js +116 -0
  2. package/dist/agents/parse-tool-call.js +26 -4
  3. package/dist/agents/pi-sdk.js +247 -50
  4. package/dist/agents/session-store.js +87 -2
  5. package/dist/bootstrap/snip-collapse.js +135 -0
  6. package/dist/cli/ink-app.js +116 -0
  7. package/dist/cli/loading-tui.js +64 -10
  8. package/dist/electron/config.js +14 -9
  9. package/dist/electron/dialogs.js +53 -16
  10. package/dist/electron/first-run.js +65 -24
  11. package/dist/electron/ipc.js +14 -10
  12. package/dist/electron/logger.js +44 -7
  13. package/dist/electron/main.js +45 -42
  14. package/dist/electron/menu.js +18 -13
  15. package/dist/electron/paths.js +54 -12
  16. package/dist/electron/server.js +57 -18
  17. package/dist/electron/tray.js +53 -15
  18. package/dist/electron/window.js +61 -22
  19. package/dist/electron-preload.js +19 -16
  20. package/dist/electron.js +4 -1
  21. package/dist/external-engines/delegate.js +19 -0
  22. package/dist/hooks/hooks-engine.js +329 -0
  23. package/dist/index.js +130 -133
  24. package/dist/llm/pi-ai.js +5 -17
  25. package/dist/security/tool-gate.js +8 -1
  26. package/dist/social/dunbar-tier.js +409 -0
  27. package/dist/utils/auto-update.js +51 -12
  28. package/dist/web/client.js +4833 -4328
  29. package/dist/web/components/p2p/index.js +234 -276
  30. package/dist/web/server.js +17 -3
  31. package/dist/web/style.css +2 -2
  32. package/dist/web/ui/message-renderer.js +396 -535
  33. package/dist/web/ui/step-timeline.js +273 -372
  34. package/package.json +27 -25
  35. package/dist/web/components/p2p/P2PModal.js +0 -188
  36. package/dist/web/components/p2p/p2p-modal.js +0 -664
  37. 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
- 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 = `
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
- this.modal = document.createElement('div');
27
- this.modal.className = 'p2p-modal';
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
- 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 = `
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>身份信息</h3>
66
+ <h3>\u8EAB\u4EFD\u4FE1\u606F</h3>
84
67
  <div class="p2p-info-row">
85
- <span class="label">名称:</span>
86
- <span class="value">${this.escapeHtml(this.safeName(identityData.name, '未知'))}</span>
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 || '未知')}</span>
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 || '未知')}</span>
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 || '未知')}</span>
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">复制 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>
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
- catch {
109
- identityHtml = '<div class="p2p-error">无法加载身份信息</div>';
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 网络</h2>
114
- <button class="p2p-close" id="p2p-close-btn">×</button>
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">身份</button>
118
- <button class="p2p-tab" data-tab="connect">连接</button>
119
- <button class="p2p-tab" data-tab="messages">消息</button>
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>连接到节点</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="输入节点 ID CID">
130
- <button class="p2p-btn-primary" id="p2p-connect-btn">连接</button>
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>已连接的节点</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>收到的消息</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
- 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
- }
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
- 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';
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
- async loadConnectionHistory() {
279
- try {
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();
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
- catch (e) {
293
- console.error('加载连接历史失败:', e);
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
- 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 += `
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
- // 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 = `
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
- document.body.appendChild(toast);
347
- setTimeout(() => toast.remove(), 2000);
348
- }
349
- show() {
350
- if (this.overlay) {
351
- this.overlay.style.display = 'flex';
352
- this.render();
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
- hide() {
356
- if (this.overlay) {
357
- this.overlay.style.display = 'none';
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('[P2P Modal] 已初始化');
515
+ console.log("[P2P Modal] \u5DF2\u521D\u59CB\u5316");
@@ -9,11 +9,11 @@ import * as os from 'os';
9
9
  import * as crypto from 'crypto';
10
10
  import { validateMessageInput, validateChannelInput, healthCheck, } from './input-validator.js';
11
11
  import { segmentChatReply } from '../agents/chat-segmenter.js';
12
- import { listTools } from '../llm/tool-manifest/index.js';
13
12
  import { registerJudgmentsRoutes } from './routes-judgments.js';
14
13
  import { registerLlmConfigRoutes } from './routes-llm-config.js';
15
14
  import { registerExternalEngineRoutes } from './routes-external-engines.js';
16
15
  import { registerTaskRoutes } from './routes-tasks.js';
16
+ import { loadPeerTier, recordInteraction, tierLabel } from '../social/dunbar-tier.js';
17
17
  import { registerHearthRoutes } from './routes-hearth.js';
18
18
  import { loadOrCreateAgentIdentity } from '../agents/agent-identity.js';
19
19
  // 2026-07-06: 类型抽到 ./server-types.ts (channel / session / task / sse client / iroh info / paths)
@@ -465,7 +465,19 @@ async function handleV3P2PMessage(parsed, conn, comm) {
465
465
  return;
466
466
  }
467
467
  const senderKey = fromPublicKey || peerKey;
468
- console.log(`[v3] 收到 ${senderKey.substring(0, 12)}... channel ${channelId} chat: "${text.substring(0, 40)}..."`);
468
+ // 2026-07-29: Dunbar Tier 检查 远端 agent 不能触发禁区工具
469
+ const tierState = await loadPeerTier(senderKey);
470
+ if (tierState.tier === 'blocked') {
471
+ const reply = JSON.stringify({
472
+ v: 3, op: 'agent.chat.reply',
473
+ payload: { channelId, fromPublicKey: v3P2PRef?.getPublicKey() || '', error: 'blocked', text: '❌ 您已被本地系统加入通信黑名单。如需解除请联系系统管理员。' }
474
+ });
475
+ await comm.sendToConnection(conn.id, reply);
476
+ return;
477
+ }
478
+ // 2026-07-29: 语义分析 — 分析远端聊天内容, 隐式滑动 trustScore
479
+ recordInteraction(senderKey, text).catch(() => { });
480
+ console.log(`[v3] 收到 ${senderKey.substring(0, 12)}... (${tierLabel(tierState.tier)}) 对 channel ${channelId} 的 chat: "${text.substring(0, 40)}..."`);
469
481
  try {
470
482
  // 1. 找到 channel
471
483
  const channels = await loadChannels();
@@ -1486,6 +1498,8 @@ export async function createWebServer(port = 3000, options = {}) {
1486
1498
  // 2026-07-21: 社交心跳 beacon — 远端智能体宣告存活/能力, 更新本地 liveness
1487
1499
  if (parsed.op === 'agent.heartbeat') {
1488
1500
  agentHeartbeat?.handleIncoming('agent.heartbeat', parsed.payload, evt.fromPublicKey);
1501
+ // 2026-07-29: 收到心跳也记录交互 (Dunbar 自动归类)
1502
+ recordInteraction(evt.fromPublicKey).catch(() => { });
1489
1503
  return;
1490
1504
  }
1491
1505
  // v3 新增: B 端收到 A 的 thinking (开始 + 流式 token)
@@ -2218,7 +2232,7 @@ ${goalDesc}
2218
2232
  res.json(healthCheck(getPackageVersion()));
2219
2233
  });
2220
2234
  app.get('/api/tools', (_req, res) => {
2221
- res.json(listTools().map(t => t.id));
2235
+ res.json([]); // tool-manifest 已废弃, 工具列表由 pi-sdk.ts this.tools 维护
2222
2236
  });
2223
2237
  // 2026-07-28: 用户 DID 身份端点 — 静默生成/加载, 持久化到 ~/.bolloon/identity/user.json
2224
2238
  let userIdentityCache = null;