@bolloon/bolloon-agent 0.1.1 → 0.1.2

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 (200) hide show
  1. package/bin/bolloon-cli.cjs +157 -0
  2. package/bin/bolloon-daemon.sh +207 -0
  3. package/bin/bolloon.cmd +11 -0
  4. package/dist/agents/constraint-layer.js +10 -15
  5. package/dist/agents/pi-sdk.js +433 -106
  6. package/dist/agents/protocol.js +82 -1
  7. package/dist/agents/subagent-manager.js +2 -2
  8. package/dist/agents/workflow-engine.js +15 -20
  9. package/dist/agents/workflow-pivot-loop.js +541 -0
  10. package/dist/bollharness/src/index.js +5 -0
  11. package/dist/bollharness/src/scripts/checks/check_adr_plan_numbering.js +6 -0
  12. package/dist/bollharness/src/scripts/checks/check_api_types.js +45 -0
  13. package/dist/bollharness/src/scripts/checks/check_artifact_link.js +146 -0
  14. package/dist/bollharness/src/scripts/checks/check_bridge_deps.js +6 -0
  15. package/dist/bollharness/src/scripts/checks/check_bugfix_binding.js +6 -0
  16. package/dist/bollharness/src/scripts/checks/check_bugfix_binding_ci.js +6 -0
  17. package/dist/bollharness/src/scripts/checks/check_doc_file_references.js +6 -0
  18. package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +135 -0
  19. package/dist/bollharness/src/scripts/checks/check_doc_links.js +31 -0
  20. package/dist/bollharness/src/scripts/checks/check_file_existence_claims.js +6 -0
  21. package/dist/bollharness/src/scripts/checks/check_fragment_integrity.js +34 -0
  22. package/dist/bollharness/src/scripts/checks/check_hook_installed.js +63 -0
  23. package/dist/bollharness/src/scripts/checks/check_issue_closure.js +41 -0
  24. package/dist/bollharness/src/scripts/checks/check_mcp_parity.js +6 -0
  25. package/dist/bollharness/src/scripts/checks/check_security.js +48 -0
  26. package/dist/bollharness/src/scripts/checks/check_skill_parity.js +6 -0
  27. package/dist/bollharness/src/scripts/checks/check_versions.js +6 -0
  28. package/dist/bollharness/src/scripts/checks/finding.js +13 -0
  29. package/dist/bollharness/src/scripts/checks/next_decision_number.js +20 -0
  30. package/dist/bollharness/src/scripts/checks/regenerate_magic_docs.js +6 -0
  31. package/dist/bollharness/src/scripts/ci/detect_rebaseline_triggers.js +8 -0
  32. package/dist/bollharness/src/scripts/ci/scan_subprocess_cfg.js +8 -0
  33. package/dist/bollharness/src/scripts/ci/scan_verify_artifacts.js +8 -0
  34. package/dist/bollharness/src/scripts/ci/scan_yaml_schema.js +8 -0
  35. package/dist/bollharness/src/scripts/context_router.js +67 -0
  36. package/dist/bollharness/src/scripts/deploy-guard.js +157 -0
  37. package/dist/bollharness/src/scripts/guard-feedback.js +192 -0
  38. package/dist/bollharness/src/scripts/guard_router.js +158 -0
  39. package/dist/bollharness/src/scripts/hooks/_hook_output.js +6 -0
  40. package/dist/bollharness/src/scripts/hooks/auto-python3.js +6 -0
  41. package/dist/bollharness/src/scripts/hooks/deploy-progress-on-session-end.js +6 -0
  42. package/dist/bollharness/src/scripts/hooks/failure-analyzer.js +6 -0
  43. package/dist/bollharness/src/scripts/hooks/gate-judgment-inject.js +92 -0
  44. package/dist/bollharness/src/scripts/hooks/gate-transition-judgment.js +63 -0
  45. package/dist/bollharness/src/scripts/hooks/inbox-ack.js +6 -0
  46. package/dist/bollharness/src/scripts/hooks/inbox-inject-on-start.js +6 -0
  47. package/dist/bollharness/src/scripts/hooks/inbox-validate.js +6 -0
  48. package/dist/bollharness/src/scripts/hooks/inbox-write-ledger.js +6 -0
  49. package/dist/bollharness/src/scripts/hooks/initializer-agent.js +6 -0
  50. package/dist/bollharness/src/scripts/hooks/loop-detection.js +73 -0
  51. package/dist/bollharness/src/scripts/hooks/owner-guard.js +6 -0
  52. package/dist/bollharness/src/scripts/hooks/precompact.js +6 -0
  53. package/dist/bollharness/src/scripts/hooks/review-agent-gatekeeper.js +6 -0
  54. package/dist/bollharness/src/scripts/hooks/risk-tracker.js +108 -0
  55. package/dist/bollharness/src/scripts/hooks/sanitize-on-read.js +6 -0
  56. package/dist/bollharness/src/scripts/hooks/session-reflection.js +7 -0
  57. package/dist/bollharness/src/scripts/hooks/session-start-magic-docs.js +7 -0
  58. package/dist/bollharness/src/scripts/hooks/session-start-reset-risk.js +7 -0
  59. package/dist/bollharness/src/scripts/hooks/session-start-toolkit-reminder.js +7 -0
  60. package/dist/bollharness/src/scripts/hooks/stop-evaluator.js +157 -0
  61. package/dist/bollharness/src/scripts/hooks/tool-call-counter.js +6 -0
  62. package/dist/bollharness/src/scripts/hooks/trace-analyzer.js +10 -0
  63. package/dist/bollharness/src/scripts/install/install-trust-token.js +7 -0
  64. package/dist/bollharness/src/scripts/install/multi_project_registry.js +9 -0
  65. package/dist/bollharness/src/scripts/install/phase2_auto.js +21 -0
  66. package/dist/bollharness/src/scripts/install/pre_commit_installer.js +6 -0
  67. package/dist/bollharness/src/scripts/install/tier_selector.js +7 -0
  68. package/dist/bollharness/src/scripts/install/transcript_miner.js +7 -0
  69. package/dist/bollharness/src/scripts/lib/claim_patterns.js +10 -0
  70. package/dist/bollharness/src/scripts/lib/sanitize_patterns.js +12 -0
  71. package/dist/bollharness/src/scripts/sanitize.js +6 -0
  72. package/dist/bollharness-integration/channel-judgment-engine.js +530 -0
  73. package/dist/bollharness-integration/context-chain-router.js +383 -0
  74. package/dist/bollharness-integration/context-router-judgment.js +13 -21
  75. package/dist/bollharness-integration/context-router.js +22 -64
  76. package/dist/bollharness-integration/gate-state-machine.js +14 -19
  77. package/dist/bollharness-integration/gate-transition-hooks.js +16 -61
  78. package/dist/bollharness-integration/guard-checker.js +21 -68
  79. package/dist/bollharness-integration/index.js +14 -124
  80. package/dist/bollharness-integration/integration.js +13 -20
  81. package/dist/bollharness-integration/llm-judgment-engine.js +569 -0
  82. package/dist/bollharness-integration/skill-adapter.js +18 -64
  83. package/dist/cli-entry.js +261 -0
  84. package/dist/constraint-runtime/src/commands.js +17 -7
  85. package/dist/constraint-runtime/src/constraint/budget.js +1 -6
  86. package/dist/constraint-runtime/src/constraint/permission.js +1 -6
  87. package/dist/constraint-runtime/src/models.js +1 -3
  88. package/dist/constraint-runtime/src/tools.js +17 -7
  89. package/dist/constraints/index.js +1 -7
  90. package/dist/documents/reader.js +8 -49
  91. package/dist/heartbeat/DaemonManager.js +242 -0
  92. package/dist/heartbeat/HealthMonitor.js +285 -0
  93. package/dist/heartbeat/StartupVerifier.js +205 -0
  94. package/dist/heartbeat/Watchdog.js +168 -0
  95. package/dist/heartbeat/index.js +84 -0
  96. package/dist/heartbeat/types.js +5 -0
  97. package/dist/index.js +381 -28
  98. package/dist/llm/config-store.js +31 -57
  99. package/dist/llm/llm-judgment-client.js +389 -0
  100. package/dist/llm/pi-ai.js +9 -52
  101. package/dist/network/agent-network.js +46 -90
  102. package/dist/network/hybrid-messenger.js +125 -0
  103. package/dist/network/iroh-bootstrap.js +38 -0
  104. package/dist/network/iroh-discovery.js +145 -0
  105. package/dist/network/iroh-integration.js +9 -16
  106. package/dist/network/iroh-transport.js +10 -48
  107. package/dist/network/p2p.js +23 -62
  108. package/dist/network/storage/adapters/json-adapter.js +4 -42
  109. package/dist/network/storage/index.js +147 -0
  110. package/dist/network/storage/types.js +14 -0
  111. package/dist/pi-ecosystem/index.js +233 -0
  112. package/dist/pi-ecosystem-colony/index.js +29 -90
  113. package/dist/pi-ecosystem-goals/index.js +20 -74
  114. package/dist/pi-ecosystem-judgment/decision.js +29 -47
  115. package/dist/pi-ecosystem-judgment/distillation.js +16 -29
  116. package/dist/pi-ecosystem-judgment/human-value-store.js +13 -60
  117. package/dist/pi-ecosystem-judgment/index.js +21 -74
  118. package/dist/pi-ecosystem-judgment/value-injection.js +26 -72
  119. package/dist/pi-ecosystem-mcp/index.js +24 -78
  120. package/dist/pi-ecosystem-subagents/index.js +20 -69
  121. package/dist/social/ant-colony/AdaptiveHeartbeat.js +3 -8
  122. package/dist/social/ant-colony/PheromoneEngine.js +11 -49
  123. package/dist/social/ant-colony/index.js +6 -0
  124. package/dist/social/ant-colony/types.js +4 -8
  125. package/dist/social/channels/ChannelManager.js +8 -46
  126. package/dist/social/channels/DiapChannelBridge.js +9 -47
  127. package/dist/social/channels/InterestMatcher.js +2 -7
  128. package/dist/social/channels/channel-agent-session.js +309 -0
  129. package/dist/social/channels/channel-heartbeat-agent.js +494 -0
  130. package/dist/social/channels/diap-doc-parser.js +204 -0
  131. package/dist/social/channels/harness-workflow-integrator.js +446 -0
  132. package/dist/social/channels/index.js +9 -0
  133. package/dist/social/channels/types.js +3 -7
  134. package/dist/social/global-shared-context.js +6 -47
  135. package/dist/social/heartbeat.js +29 -72
  136. package/dist/social/persona/enhanced-persona.js +299 -0
  137. package/dist/web/client.js +302 -136
  138. package/dist/web/components/p2p/index.js +159 -9
  139. package/dist/web/components/p2p/p2p-connection.js +136 -0
  140. package/dist/web/components/p2p/p2p-manager.js +24 -0
  141. package/dist/web/components/p2p/p2p-store-memory.js +1 -1
  142. package/dist/web/components/p2p/types.js +7 -0
  143. package/dist/web/index.html +5 -0
  144. package/dist/web/style.css +118 -0
  145. package/package.json +12 -6
  146. package/scripts/build-cli.js +206 -0
  147. package/scripts/postinstall.js +153 -0
  148. package/src/agents/pi-sdk.ts +347 -28
  149. package/src/agents/protocol.ts +95 -1
  150. package/src/agents/workflow-pivot-loop.ts +674 -0
  151. package/src/bollharness/CLAUDE.md +73 -0
  152. package/src/bollharness/README.md +143 -0
  153. package/src/bollharness/README.zh-CN.md +131 -0
  154. package/src/bollharness/reference/boll-reference/scripts/hooks/stop-evaluator.md +57 -0
  155. package/src/bollharness/scripts/context-fragments/artifact-linkage.md +14 -0
  156. package/src/bollharness/scripts/context-fragments/auth-consumers.md +17 -0
  157. package/src/bollharness/scripts/context-fragments/bridge-constitution.md +13 -0
  158. package/src/bollharness/scripts/context-fragments/catalyst-distributed.md +18 -0
  159. package/src/bollharness/scripts/context-fragments/closure-checklist.md +13 -0
  160. package/src/bollharness/scripts/context-fragments/contract-consumers.md +15 -0
  161. package/src/bollharness/scripts/context-fragments/db-shared-structures.md +15 -0
  162. package/src/bollharness/scripts/context-fragments/fixed-three-layers.md +19 -0
  163. package/src/bollharness/scripts/context-fragments/general-dev-principles.md +11 -0
  164. package/src/bollharness/scripts/context-fragments/issue-first.md +8 -0
  165. package/src/bollharness/scripts/context-fragments/mcp-parity.md +16 -0
  166. package/src/bollharness/scripts/context-fragments/pi-agent-operations.md +108 -0
  167. package/src/bollharness/scripts/context-fragments/protocol-consumers.md +15 -0
  168. package/src/bollharness/scripts/context-fragments/run-events-consumers.md +15 -0
  169. package/src/bollharness/scripts/context-fragments/scene-fidelity.md +13 -0
  170. package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +15 -0
  171. package/src/bollharness/scripts/context-fragments/two-language.md +15 -0
  172. package/src/bollharness/scripts/context-fragments/version-sources.md +14 -0
  173. package/src/bollharness/scripts/hooks/stop-evaluator.md +83 -0
  174. package/src/bollharness/templates/scaffold/CLAUDE.md +89 -0
  175. package/src/cli-entry.ts +304 -0
  176. package/src/heartbeat/DaemonManager.ts +283 -0
  177. package/src/heartbeat/HealthMonitor.ts +316 -0
  178. package/src/heartbeat/StartupVerifier.ts +223 -0
  179. package/src/heartbeat/Watchdog.ts +198 -0
  180. package/src/heartbeat/index.ts +108 -0
  181. package/src/heartbeat/types.ts +82 -0
  182. package/src/llm/config-store.ts +23 -5
  183. package/src/network/iroh-transport.ts +3 -3
  184. package/src/web/client.js +302 -136
  185. package/src/web/components/p2p/P2PModal.tsx +91 -3
  186. package/src/web/components/p2p/index.ts +171 -9
  187. package/src/web/components/p2p/p2p-connection.ts +153 -1
  188. package/src/web/components/p2p/p2p-manager.ts +39 -1
  189. package/src/web/components/p2p/p2p-store-memory.ts +1 -1
  190. package/src/web/components/p2p/p2p-tools.ts +315 -0
  191. package/src/web/components/p2p/types.ts +58 -0
  192. package/src/web/design.md +99 -0
  193. package/src/web/index.html +5 -0
  194. package/src/web/server.ts +353 -36
  195. package/src/web/style.css +118 -0
  196. package/tsconfig.cli.json +16 -0
  197. package/tsconfig.electron.json +1 -1
  198. package/tsconfig.json +1 -2
  199. package/dist/web/server.js +0 -1647
  200. package/dist/web/server.js.map +0 -1
@@ -1,6 +1,12 @@
1
1
  import { useState, useEffect, useCallback } from 'react';
2
2
  import { p2pManager } from './p2p-manager.js';
3
- import type { P2PIdentity, P2PMessage, ConnectionHistoryEntry, ConnectProgress } from './types.js';
3
+ import type {
4
+ P2PIdentity,
5
+ P2PMessage,
6
+ ConnectionHistoryEntry,
7
+ ConnectProgress,
8
+ PersistentConnection
9
+ } from './types.js';
4
10
 
5
11
  type Tab = 'identity' | 'connect' | 'history' | 'messages';
6
12
  type Status = 'idle' | 'connecting' | 'online' | 'error';
@@ -24,10 +30,16 @@ export function P2PModal({ visible, onClose }: P2PModalProps) {
24
30
  const [messages, setMessages] = useState<P2PMessage[]>([]);
25
31
  const [unreadCount, setUnreadCount] = useState(0);
26
32
  const [peers, setPeers] = useState<{ nodeId: string; info: any }[]>([]);
33
+ const [persistentConnections, setPersistentConnections] = useState<PersistentConnection[]>([]);
27
34
  const [toast, setToast] = useState<string | null>(null);
28
35
 
29
36
  useEffect(() => {
30
37
  if (visible && !initialized) {
38
+ console.log('[P2P Modal] visible=true, initialized=false, calling initP2P');
39
+ initP2P();
40
+ } else if (visible && initialized && !identity) {
41
+ // 已初始化但没有identity,可能是首次未触发
42
+ console.log('[P2P Modal] visible=true, initialized=true, but no identity, calling initP2P again');
31
43
  initP2P();
32
44
  }
33
45
  }, [visible, initialized]);
@@ -64,6 +76,7 @@ export function P2PModal({ visible, onClose }: P2PModalProps) {
64
76
  break;
65
77
  case 'connect':
66
78
  loadPeers();
79
+ loadPersistentConnections();
67
80
  break;
68
81
  }
69
82
  }
@@ -93,6 +106,38 @@ export function P2PModal({ visible, onClose }: P2PModalProps) {
93
106
  setPeers(connectedPeers);
94
107
  }
95
108
 
109
+ async function loadPersistentConnections() {
110
+ try {
111
+ const connections = await p2pManager.getPersistentConnections();
112
+ setPersistentConnections(connections);
113
+ } catch (e) {
114
+ console.error('[P2P Modal] 加载持久连接失败:', e);
115
+ }
116
+ }
117
+
118
+ async function handleToggleConnection(connection: PersistentConnection) {
119
+ const newStatus = connection.status === 'connected' ? 'disconnected' : 'connected';
120
+ const enable = newStatus === 'connected';
121
+
122
+ try {
123
+ const success = await p2pManager.toggleConnection(connection, enable);
124
+ if (success) {
125
+ await loadPersistentConnections();
126
+ loadPeers();
127
+ showToast(enable ? '正在连接...' : '已断开');
128
+ } else {
129
+ showToast('操作失败');
130
+ }
131
+ } catch (e) {
132
+ showToast('操作失败');
133
+ }
134
+ }
135
+
136
+ async function handleOpenChannel(channelId: string) {
137
+ showToast(`打开通道: ${channelId}`);
138
+ onClose();
139
+ }
140
+
96
141
  async function handleConnect() {
97
142
  if (!connectInput.trim()) return;
98
143
 
@@ -101,13 +146,14 @@ export function P2PModal({ visible, onClose }: P2PModalProps) {
101
146
  setConnectResult(null);
102
147
 
103
148
  try {
104
- const result = await p2pManager.connect(connectInput, (p) => setProgress(p));
149
+ const result = await p2pManager.connectAndCreateChannel(connectInput, (p) => setProgress(p));
105
150
 
106
151
  if (result.success) {
107
152
  setConnectResult({ type: 'success', text: `已连接到 ${result.name || '节点'}` });
108
153
  setConnectInput('');
109
154
  await loadHistory();
110
155
  loadPeers();
156
+ loadPersistentConnections();
111
157
  } else {
112
158
  setConnectResult({ type: 'error', text: result.error || '连接失败' });
113
159
  }
@@ -248,8 +294,50 @@ export function P2PModal({ visible, onClose }: P2PModalProps) {
248
294
  </div>
249
295
  )}
250
296
 
297
+ {/* 持久连接列表 */}
298
+ <div className="persistent-peers-section">
299
+ <h4>持久连接 ({persistentConnections.length})</h4>
300
+ {persistentConnections.length === 0 ? (
301
+ <div className="empty-hint">暂无持久连接</div>
302
+ ) : (
303
+ persistentConnections.map((conn) => (
304
+ <div key={conn.id} className={`persistent-peer-item ${conn.status}`}>
305
+ <div className="peer-status-indicator">
306
+ <span className={`dot ${conn.status === 'connected' ? 'online' : 'offline'}`}></span>
307
+ </div>
308
+ <div className="peer-info">
309
+ <div className="peer-name">
310
+ {conn.peerName || 'Unknown'}
311
+ {conn.isAutoConnect && <span className="auto-badge">自动</span>}
312
+ </div>
313
+ <div className="peer-meta">
314
+ <span>DID: {conn.peerDid?.substring(0, 16)}...</span>
315
+ <span>状态: {conn.status === 'connected' ? '已连接' : '未连接'}</span>
316
+ </div>
317
+ </div>
318
+ <div className="peer-actions">
319
+ <button
320
+ className={`btn-sm ${conn.status === 'connected' ? 'btn-danger' : 'btn-primary'}`}
321
+ onClick={() => handleToggleConnection(conn)}
322
+ >
323
+ {conn.status === 'connected' ? '断开' : '连接'}
324
+ </button>
325
+ {conn.channelId && (
326
+ <button
327
+ className="btn-sm btn-secondary"
328
+ onClick={() => handleOpenChannel(conn.channelId!)}
329
+ >
330
+ 对话
331
+ </button>
332
+ )}
333
+ </div>
334
+ </div>
335
+ ))
336
+ )}
337
+ </div>
338
+
251
339
  <div className="peers-section">
252
- <h4>已连接节点 ({peers.length})</h4>
340
+ <h4>当前连接 ({peers.length})</h4>
253
341
  <div id="p2p-peers-list">
254
342
  {peers.length === 0 && <div className="empty-hint">暂无连接</div>}
255
343
  {peers.map((peer, i) => (
@@ -5,6 +5,7 @@
5
5
  class P2PModalUI {
6
6
  private modal: HTMLElement | null = null;
7
7
  private overlay: HTMLElement | null = null;
8
+ private connectedPeers: Map<string, { nodeId: string; name: string; time: number }> = new Map();
8
9
 
9
10
  constructor() {
10
11
  this.createModal();
@@ -53,23 +54,54 @@ class P2PModalUI {
53
54
  private async render(): Promise<void> {
54
55
  if (!this.modal) return;
55
56
 
57
+ // 加载连接历史
58
+ await this.loadConnectionHistory();
59
+
56
60
  // 获取身份信息
57
61
  let identityHtml = '<div class="p2p-loading">加载中...</div>';
62
+ let identityData: any = { name: '未知', did: '未知', cid: '未知' };
58
63
  try {
59
- const resp = await fetch('/api/identity');
60
- const data = await resp.json();
64
+ // 尝试获取 iroh 身份(包含 CID)
65
+ const irohResp = await fetch('/api/iroh/init', { method: 'POST' });
66
+ const irohData = await irohResp.json();
67
+ if (irohData.cid) {
68
+ identityData = {
69
+ name: irohData.name || irohData.name?.split('-').slice(-1)[0] || 'Bolloon',
70
+ did: irohData.did || '未知',
71
+ cid: irohData.cid || '未知',
72
+ nodeId: irohData.irohNodeId ? irohData.irohNodeId.substring(0, 20) + '...' : '未知'
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
+ }
61
81
  identityHtml = `
62
82
  <div class="p2p-section">
63
83
  <h3>身份信息</h3>
64
84
  <div class="p2p-info-row">
65
85
  <span class="label">名称:</span>
66
- <span class="value">${this.escapeHtml(data.name || '未知')}</span>
86
+ <span class="value">${this.escapeHtml(identityData.name || '未知')}</span>
87
+ </div>
88
+ <div class="p2p-info-row">
89
+ <span class="label">CID:</span>
90
+ <span class="value mono">${this.escapeHtml(identityData.cid || '未知')}</span>
67
91
  </div>
68
92
  <div class="p2p-info-row">
69
93
  <span class="label">DID:</span>
70
- <span class="value mono">${this.escapeHtml(data.did || '未知')}</span>
94
+ <span class="value mono">${this.escapeHtml(identityData.did || '未知')}</span>
95
+ </div>
96
+ <div class="p2p-info-row">
97
+ <span class="label">Node ID:</span>
98
+ <span class="value mono">${this.escapeHtml(identityData.nodeId || '未知')}</span>
99
+ </div>
100
+ <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>
71
104
  </div>
72
- <button class="p2p-btn-primary" id="p2p-copy-btn">复制 DID</button>
73
105
  </div>
74
106
  `;
75
107
  } catch {
@@ -99,6 +131,12 @@ class P2PModalUI {
99
131
  </div>
100
132
  <div id="p2p-connect-result" class="p2p-result"></div>
101
133
  </div>
134
+ <div class="p2p-section">
135
+ <h3>已连接的节点</h3>
136
+ <div id="p2p-connected-peers" class="p2p-connected-peers">
137
+ 暂无连接
138
+ </div>
139
+ </div>
102
140
  </div>
103
141
  <div class="p2p-tab-content" data-tab="messages">
104
142
  <div class="p2p-section">
@@ -131,12 +169,27 @@ class P2PModalUI {
131
169
  });
132
170
  });
133
171
 
172
+ // 复制 CID
173
+ const copyCidBtn = this.modal.querySelector('#p2p-copy-cid-btn');
174
+ copyCidBtn?.addEventListener('click', async () => {
175
+ try {
176
+ const irohResp = await fetch('/api/iroh/init', { method: 'POST' });
177
+ const data = await irohResp.json();
178
+ if (data.cid) {
179
+ await navigator.clipboard.writeText(data.cid);
180
+ this.showToast('CID 已复制到剪贴板');
181
+ }
182
+ } catch (e) {
183
+ console.error('复制失败:', e);
184
+ }
185
+ });
186
+
134
187
  // 复制 DID
135
- const copyBtn = this.modal.querySelector('#p2p-copy-btn');
136
- copyBtn?.addEventListener('click', async () => {
188
+ const copyDidBtn = this.modal.querySelector('#p2p-copy-did-btn');
189
+ copyDidBtn?.addEventListener('click', async () => {
137
190
  try {
138
- const resp = await fetch('/api/identity');
139
- const data = await resp.json();
191
+ const irohResp = await fetch('/api/iroh/init', { method: 'POST' });
192
+ const data = await irohResp.json();
140
193
  if (data.did) {
141
194
  await navigator.clipboard.writeText(data.did);
142
195
  this.showToast('DID 已复制到剪贴板');
@@ -146,6 +199,21 @@ class P2PModalUI {
146
199
  }
147
200
  });
148
201
 
202
+ // 复制 Node ID
203
+ const copyNodeIdBtn = this.modal.querySelector('#p2p-copy-nodeid-btn');
204
+ copyNodeIdBtn?.addEventListener('click', async () => {
205
+ try {
206
+ const irohResp = await fetch('/api/iroh/init', { method: 'POST' });
207
+ const data = await irohResp.json();
208
+ if (data.irohNodeId) {
209
+ await navigator.clipboard.writeText(data.irohNodeId);
210
+ this.showToast('Node ID 已复制到剪贴板');
211
+ }
212
+ } catch (e) {
213
+ console.error('复制失败:', e);
214
+ }
215
+ });
216
+
149
217
  // 连接按钮
150
218
  const connectBtn = this.modal.querySelector('#p2p-connect-btn');
151
219
  connectBtn?.addEventListener('click', () => this.handleConnect());
@@ -185,6 +253,23 @@ class P2PModalUI {
185
253
  if (data.ok) {
186
254
  result.innerHTML = `<div class="p2p-success">连接成功!节点: ${this.escapeHtml(data.nodeName || cid.substring(0, 16))}...</div>`;
187
255
  result.className = 'p2p-result success';
256
+ // 清空输入框
257
+ input.value = '';
258
+ // 保存已连接节点到后端
259
+ try {
260
+ await fetch('/api/p2p/history', {
261
+ method: 'POST',
262
+ headers: { 'Content-Type': 'application/json' },
263
+ body: JSON.stringify({
264
+ cid: cid, // 输入的 CID 或 Node ID
265
+ irohNodeId: data.targetNodeId,
266
+ did: data.targetNodeId, // 用 Node ID 作为 DID
267
+ name: data.nodeName || cid.substring(0, 16)
268
+ })
269
+ });
270
+ } catch (e) {
271
+ console.error('保存连接失败:', e);
272
+ }
188
273
  } else {
189
274
  result.innerHTML = `<div class="p2p-error">${this.escapeHtml(data.error || '连接失败')}</div>`;
190
275
  result.className = 'p2p-result error';
@@ -196,6 +281,48 @@ class P2PModalUI {
196
281
  }
197
282
  }
198
283
 
284
+ private async loadConnectionHistory(): Promise<void> {
285
+ try {
286
+ const resp = await fetch('/api/p2p/history');
287
+ const history = await resp.json();
288
+ this.connectedPeers.clear();
289
+ for (const entry of history) {
290
+ this.connectedPeers.set(entry.irohNodeId || entry.did, {
291
+ nodeId: entry.irohNodeId || entry.did,
292
+ name: entry.name || 'Unknown',
293
+ time: entry.lastConnectedAt || Date.now()
294
+ });
295
+ }
296
+ this.updateConnectedPeersDisplay();
297
+ } catch (e) {
298
+ console.error('加载连接历史失败:', e);
299
+ }
300
+ }
301
+
302
+ private updateConnectedPeersDisplay(): void {
303
+ const container = document.getElementById('p2p-connected-peers');
304
+ if (!container) return;
305
+
306
+ if (this.connectedPeers.size === 0) {
307
+ container.innerHTML = '<div class="p2p-empty">暂无连接</div>';
308
+ return;
309
+ }
310
+
311
+ let html = '';
312
+ for (const [nodeId, info] of this.connectedPeers) {
313
+ const shortId = nodeId.substring(0, 16) + '...';
314
+ const time = new Date(info.time).toLocaleTimeString();
315
+ html += `
316
+ <div class="p2p-peer-item">
317
+ <span class="p2p-peer-name">${this.escapeHtml(info.name || shortId)}</span>
318
+ <span class="p2p-peer-id">${shortId}</span>
319
+ <span class="p2p-peer-time">${time}</span>
320
+ </div>
321
+ `;
322
+ }
323
+ container.innerHTML = html;
324
+ }
325
+
199
326
  private escapeHtml(str: string): string {
200
327
  const div = document.createElement('div');
201
328
  div.textContent = str;
@@ -390,6 +517,41 @@ style.textContent = `
390
517
  color: var(--text-muted, #606058);
391
518
  }
392
519
  .p2p-error { color: var(--error, #ef4444); }
520
+ .p2p-connected-peers {
521
+ background: var(--bg, #1a1a18);
522
+ border: 1px solid var(--border, #3a3a36);
523
+ border-radius: 8px;
524
+ overflow: hidden;
525
+ }
526
+ .p2p-connected-peers .p2p-empty {
527
+ padding: 16px;
528
+ text-align: center;
529
+ color: var(--text-muted, #606058);
530
+ font-size: 14px;
531
+ }
532
+ .p2p-peer-item {
533
+ display: flex;
534
+ align-items: center;
535
+ gap: 8px;
536
+ padding: 12px 16px;
537
+ border-bottom: 1px solid var(--border, #3a3a36);
538
+ }
539
+ .p2p-peer-item:last-child { border-bottom: none; }
540
+ .p2p-peer-name {
541
+ font-size: 14px;
542
+ color: var(--text, #d8d8c8);
543
+ min-width: 80px;
544
+ }
545
+ .p2p-peer-id {
546
+ flex: 1;
547
+ font-family: 'JetBrains Mono', monospace;
548
+ font-size: 12px;
549
+ color: var(--text-secondary, #909088);
550
+ }
551
+ .p2p-peer-time {
552
+ font-size: 12px;
553
+ color: var(--text-muted, #606058);
554
+ }
393
555
  `;
394
556
  document.head.appendChild(style);
395
557
 
@@ -2,7 +2,15 @@
2
2
  * P2P 连接管理
3
3
  */
4
4
 
5
- import type { ConnectResult, ConnectProgress, ParsedInput, ConnectedPeer } from './types.js';
5
+ import type {
6
+ ConnectResult,
7
+ ConnectProgress,
8
+ ParsedInput,
9
+ ConnectedPeer,
10
+ PersistentConnection,
11
+ CIDResolveResult,
12
+ PersistentConnectionStatus
13
+ } from './types.js';
6
14
  import { ConnectionStatus } from './types.js';
7
15
  import { p2pStore } from './p2p-store-memory.js';
8
16
 
@@ -181,6 +189,150 @@ export class P2PConnectionManager {
181
189
  this.reconnectTimers.clear();
182
190
  this.peers.clear();
183
191
  }
192
+
193
+ // 从 CID 解析身份
194
+ async resolveFromCID(cid: string): Promise<CIDResolveResult> {
195
+ try {
196
+ const res = await fetch('/api/p2p/resolve-cid', {
197
+ method: 'POST',
198
+ headers: { 'Content-Type': 'application/json' },
199
+ body: JSON.stringify({ cid })
200
+ });
201
+
202
+ if (res.ok) {
203
+ const data = await res.json();
204
+ return {
205
+ success: true,
206
+ did: data.doc?.id || data.doc?.did,
207
+ cid: cid,
208
+ name: data.doc?.name,
209
+ peerId: data.doc?.peerId
210
+ };
211
+ } else {
212
+ return { success: false, error: 'CID 解析失败' };
213
+ }
214
+ } catch (e) {
215
+ return { success: false, error: (e as Error).message };
216
+ }
217
+ }
218
+
219
+ // 获取所有持久连接
220
+ async getPersistentConnections(): Promise<PersistentConnection[]> {
221
+ try {
222
+ const res = await fetch('/api/p2p/persistent-connections');
223
+ if (res.ok) {
224
+ return await res.json();
225
+ }
226
+ return [];
227
+ } catch {
228
+ return [];
229
+ }
230
+ }
231
+
232
+ // 切换连接状态
233
+ async toggleConnection(connection: PersistentConnection, enable: boolean): Promise<boolean> {
234
+ try {
235
+ if (enable) {
236
+ // 建立连接
237
+ const result = await this.connect(connection.cid);
238
+ if (result.success) {
239
+ await fetch('/api/p2p/connection-status', {
240
+ method: 'POST',
241
+ headers: { 'Content-Type': 'application/json' },
242
+ body: JSON.stringify({
243
+ id: connection.id,
244
+ status: 'connected',
245
+ channelId: connection.channelId
246
+ })
247
+ });
248
+ return true;
249
+ }
250
+ return false;
251
+ } else {
252
+ // 断开连接
253
+ if (connection.peerId) {
254
+ await this.disconnect(connection.peerId);
255
+ }
256
+ await fetch('/api/p2p/connection-status', {
257
+ method: 'POST',
258
+ headers: { 'Content-Type': 'application/json' },
259
+ body: JSON.stringify({
260
+ id: connection.id,
261
+ status: 'disconnected'
262
+ })
263
+ });
264
+ return true;
265
+ }
266
+ } catch {
267
+ return false;
268
+ }
269
+ }
270
+
271
+ // 连接并创建对话通道
272
+ async connectAndCreateChannel(
273
+ input: string,
274
+ onProgress?: (progress: ConnectProgress) => void
275
+ ): Promise<ConnectResult> {
276
+ const parsed = this.parseInput(input);
277
+ if (parsed.type === 'invalid') {
278
+ return { success: false, error: parsed.error };
279
+ }
280
+
281
+ onProgress?.({ stage: 'validating', percent: 10, message: '验证输入格式...' });
282
+
283
+ let cid = '';
284
+ let did = '';
285
+ const value = parsed.value;
286
+
287
+ if (parsed.type === 'link' && typeof value !== 'string') {
288
+ cid = (value as { did?: string; cid?: string }).cid || '';
289
+ did = (value as { did?: string; cid?: string }).did || '';
290
+ } else if (parsed.type === 'cid' && typeof value === 'string') {
291
+ cid = value;
292
+ } else if (parsed.type === 'diapDoc' && typeof value !== 'string') {
293
+ cid = (value as { id?: string; did?: string; cid?: string }).cid || '';
294
+ did = (value as { id?: string; did?: string }).id || (value as { id?: string; did?: string }).did || '';
295
+ }
296
+
297
+ if (!cid) {
298
+ return { success: false, error: '未找到 CID' };
299
+ }
300
+
301
+ // 如果没有 DID,从 CID 解析
302
+ if (!did) {
303
+ onProgress?.({ stage: 'resolving', percent: 30, message: '从 IPFS 解析身份...' });
304
+ const resolveResult = await this.resolveFromCID(cid);
305
+ if (resolveResult.success) {
306
+ did = resolveResult.did || '';
307
+ onProgress?.({ stage: 'identity_resolved', percent: 50, message: `身份解析完成: ${resolveResult.name}` });
308
+ }
309
+ }
310
+
311
+ // 创建对话通道
312
+ onProgress?.({ stage: 'creating_channel', percent: 60, message: '创建对话通道...' });
313
+ try {
314
+ const channelRes = await fetch('/api/p2p/create-channel', {
315
+ method: 'POST',
316
+ headers: { 'Content-Type': 'application/json' },
317
+ body: JSON.stringify({
318
+ peerDid: did,
319
+ peerName: did.substring(0, 16),
320
+ cid: cid,
321
+ peerId: ''
322
+ })
323
+ });
324
+
325
+ if (!channelRes.ok) {
326
+ return { success: false, error: '创建对话通道失败' };
327
+ }
328
+ } catch (e) {
329
+ return { success: false, error: (e as Error).message };
330
+ }
331
+
332
+ // 执行连接
333
+ onProgress?.({ stage: 'connecting', percent: 80, message: '建立 P2P 连接...' });
334
+ return this.connect(input, onProgress);
335
+ }
184
336
  }
185
337
 
186
338
  export const p2pConnection = new P2PConnectionManager();
@@ -11,7 +11,9 @@ import type {
11
11
  ConnectResult,
12
12
  ConnectProgress,
13
13
  P2PMessage,
14
- ConnectionHistoryEntry
14
+ ConnectionHistoryEntry,
15
+ PersistentConnection,
16
+ CIDResolveResult
15
17
  } from './types.js';
16
18
 
17
19
  export class P2PManager {
@@ -96,6 +98,42 @@ export class P2PManager {
96
98
  this.connection.destroy();
97
99
  this.messages.destroy();
98
100
  }
101
+
102
+ // 获取持久连接列表
103
+ async getPersistentConnections(): Promise<PersistentConnection[]> {
104
+ return this.connection.getPersistentConnections();
105
+ }
106
+
107
+ // 更新连接状态
108
+ async updateConnectionStatus(
109
+ id: string,
110
+ status: string,
111
+ channelId?: string
112
+ ): Promise<void> {
113
+ await fetch('/api/p2p/connection-status', {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json' },
116
+ body: JSON.stringify({ id, status, channelId })
117
+ });
118
+ }
119
+
120
+ // 切换连接状态
121
+ async toggleConnection(connection: PersistentConnection, enable: boolean): Promise<boolean> {
122
+ return this.connection.toggleConnection(connection, enable);
123
+ }
124
+
125
+ // CID 解析
126
+ async resolveFromCID(cid: string): Promise<CIDResolveResult> {
127
+ return this.connection.resolveFromCID(cid);
128
+ }
129
+
130
+ // 连接并创建对话通道
131
+ async connectAndCreateChannel(
132
+ input: string,
133
+ onProgress?: (progress: ConnectProgress) => void
134
+ ): Promise<ConnectResult> {
135
+ return this.connection.connectAndCreateChannel(input, onProgress);
136
+ }
99
137
  }
100
138
 
101
139
  // 全局单例
@@ -27,7 +27,7 @@ export class P2PStoreMemory {
27
27
  // ==================== 连接历史 ====================
28
28
 
29
29
  async addToHistory(entry: Omit<ConnectionHistoryEntry, 'id'>): Promise<string> {
30
- const existingIndex = this.history.findIndex(h => h.did === entry.did);
30
+ const existingIndex = this.history.findIndex(h => h.cid === entry.cid);
31
31
 
32
32
  if (existingIndex >= 0) {
33
33
  this.history[existingIndex] = {