@bolloon/bolloon-agent 0.4.29 → 0.4.31

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 (83) hide show
  1. package/dist/agents/agent-gateway.js +5 -0
  2. package/dist/agents/agent-manifest-protocol.js +29 -0
  3. package/dist/agents/contacts/chain.js +579 -0
  4. package/dist/agents/contacts/cli.js +158 -0
  5. package/dist/agents/contacts/consent.js +124 -0
  6. package/dist/agents/contacts/grant-payload.js +45 -0
  7. package/dist/agents/contacts/grants.js +451 -0
  8. package/dist/agents/contacts/policy.js +264 -0
  9. package/dist/agents/contacts/preview-types.js +6 -0
  10. package/dist/agents/contacts/providers.js +341 -0
  11. package/dist/agents/contacts/store.js +326 -0
  12. package/dist/agents/contacts/tools.js +154 -0
  13. package/dist/agents/contacts/types.js +148 -0
  14. package/dist/agents/economic-policy.js +7 -0
  15. package/dist/agents/gateway-network.js +6 -0
  16. package/dist/agents/local-signer.js +59 -0
  17. package/dist/agents/network-pulse.js +450 -0
  18. package/dist/agents/pi-sdk.js +10 -0
  19. package/dist/agents/task-contract.js +322 -0
  20. package/dist/agents/task-inbox.js +267 -0
  21. package/dist/agents/task-transport.js +280 -0
  22. package/dist/agents/x402/transaction-store.js +8 -1
  23. package/dist/cli/commands/agent.js +230 -0
  24. package/dist/cli/commands/index.js +61 -0
  25. package/dist/cli/commands/mcp.js +98 -0
  26. package/dist/cli/commands/network.js +334 -0
  27. package/dist/cli/commands/payment.js +125 -0
  28. package/dist/cli/commands/tasks.js +1236 -0
  29. package/dist/cli/commands/trade.js +229 -0
  30. package/dist/cli/commands/wallet.js +450 -0
  31. package/dist/cli/mcp/bridge.js +105 -0
  32. package/dist/cli/mcp/server.js +185 -0
  33. package/dist/cli/mcp/tools.js +612 -0
  34. package/dist/cli/protocol-envelope.js +346 -0
  35. package/dist/cli-entry.js +141 -17
  36. package/dist/electron-build/agents/agent-gateway.js +5 -0
  37. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  38. package/dist/electron-build/agents/agent-manifest-protocol.js +62 -0
  39. package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
  40. package/dist/electron-build/agents/contacts/chain.js +619 -0
  41. package/dist/electron-build/agents/contacts/chain.js.map +1 -0
  42. package/dist/electron-build/agents/contacts/consent.js +163 -0
  43. package/dist/electron-build/agents/contacts/consent.js.map +1 -0
  44. package/dist/electron-build/agents/contacts/grant-payload.js +52 -0
  45. package/dist/electron-build/agents/contacts/grant-payload.js.map +1 -0
  46. package/dist/electron-build/agents/contacts/grants.js +500 -0
  47. package/dist/electron-build/agents/contacts/grants.js.map +1 -0
  48. package/dist/electron-build/agents/contacts/policy.js +271 -0
  49. package/dist/electron-build/agents/contacts/policy.js.map +1 -0
  50. package/dist/electron-build/agents/contacts/preview-types.js +8 -0
  51. package/dist/electron-build/agents/contacts/preview-types.js.map +1 -0
  52. package/dist/electron-build/agents/contacts/providers.js +384 -0
  53. package/dist/electron-build/agents/contacts/providers.js.map +1 -0
  54. package/dist/electron-build/agents/contacts/store.js +370 -0
  55. package/dist/electron-build/agents/contacts/store.js.map +1 -0
  56. package/dist/electron-build/agents/contacts/tools.js +159 -0
  57. package/dist/electron-build/agents/contacts/tools.js.map +1 -0
  58. package/dist/electron-build/agents/contacts/types.js +166 -0
  59. package/dist/electron-build/agents/contacts/types.js.map +1 -0
  60. package/dist/electron-build/agents/economic-policy.js +7 -0
  61. package/dist/electron-build/agents/economic-policy.js.map +1 -1
  62. package/dist/electron-build/agents/external-events.js.map +1 -1
  63. package/dist/electron-build/agents/gateway-network.js +6 -0
  64. package/dist/electron-build/agents/gateway-network.js.map +1 -1
  65. package/dist/electron-build/agents/goal-store.js.map +1 -1
  66. package/dist/electron-build/agents/network-pulse.js +504 -0
  67. package/dist/electron-build/agents/network-pulse.js.map +1 -0
  68. package/dist/electron-build/agents/pi-sdk.js +10 -0
  69. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  70. package/dist/electron-build/agents/x402/transaction-store.js +8 -1
  71. package/dist/electron-build/agents/x402/transaction-store.js.map +1 -1
  72. package/dist/electron-build/security/tool-gate.js +4 -0
  73. package/dist/electron-build/security/tool-gate.js.map +1 -1
  74. package/dist/index.js +30 -0
  75. package/dist/security/tool-gate.js +4 -0
  76. package/dist/web/mobile-contacts.js +329 -0
  77. package/dist/web/mobile-core.js +481 -15
  78. package/dist/web/mobile.html +32 -0
  79. package/dist/web/mobile.js +139 -0
  80. package/dist/web/routes-contacts.js +420 -0
  81. package/dist/web/server.js +34 -0
  82. package/dist/web/task-frame-server.js +211 -0
  83. package/package.json +1 -1
@@ -0,0 +1,98 @@
1
+ /**
2
+ * mcp.ts — P4 `bolloon mcp serve` (MCP 适配层入口)
3
+ *
4
+ * 只有三条子命令:
5
+ * serve 启动 stdio MCP server (给 Claude Code / Cursor / 任何 MCP 客户端接)
6
+ * tools 列出本次暴露的 tools / resources 清单 (验收/排查用; 不改任何状态)
7
+ * help 帮助
8
+ *
9
+ * 硬要求: `serve` 期间 **stdout 只走 JSON-RPC** —— 所以这里把 console.log/info/debug/warn
10
+ * 改道到 stderr (任何被包服务顺手打的日志都不许污染协议流)。
11
+ */
12
+ import { serveStdio, inventory } from '../mcp/server.js';
13
+ import { NOT_EXPOSED } from '../mcp/tools.js';
14
+ import { title } from '../protocol-envelope.js';
15
+ export const MCP_USAGE = `
16
+ ${title('bolloon mcp')}
17
+ bolloon mcp serve 启动 MCP server (stdio; 只调 P3 命令/服务层, 不复制业务逻辑)
18
+ bolloon mcp tools [--json] 列出暴露的 tools / resources (只读清单)
19
+ bolloon mcp help 本帮助
20
+
21
+ 接入客户端 (示例, 放进 MCP 配置):
22
+ { "mcpServers": { "bolloon": { "command": "bolloon", "args": ["mcp", "serve"] } } }
23
+
24
+ 判据永远是信封里的 ok/code (失败也返回信封 + isError=true), 不是退出码;
25
+ 不返回私钥/助记词 · 不绕过 payment policy · 不改交易历史 · 不伪造 verified · 无授权不切自主支付。
26
+ `;
27
+ /** 把 console.log 一类的"顺手日志"改道 stderr, 保护 stdout 的 JSON-RPC 流 */
28
+ function protectStdout() {
29
+ const toStderr = (...a) => {
30
+ const s = a.map((x) => {
31
+ if (typeof x === 'string')
32
+ return x;
33
+ try {
34
+ return JSON.stringify(x);
35
+ }
36
+ catch {
37
+ return String(x);
38
+ }
39
+ }).join(' ');
40
+ process.stderr.write(`${s}\n`);
41
+ };
42
+ console.log = toStderr;
43
+ console.info = toStderr;
44
+ console.debug = toStderr;
45
+ console.warn = toStderr;
46
+ }
47
+ export async function runMcpCommand(args) {
48
+ const sub = args[0];
49
+ switch (sub) {
50
+ case 'serve':
51
+ case 'server':
52
+ return mcpServe(args.slice(1));
53
+ case 'tools':
54
+ case 'list':
55
+ return mcpTools(args.slice(1));
56
+ case undefined:
57
+ case 'help':
58
+ case '--help':
59
+ case '-h':
60
+ console.log(MCP_USAGE);
61
+ return sub === undefined ? 1 : 0;
62
+ default:
63
+ console.log(`未知 mcp 子命令: ${sub}`);
64
+ console.log(MCP_USAGE);
65
+ return 1;
66
+ }
67
+ }
68
+ async function mcpServe(rest) {
69
+ if (rest.includes('--help') || rest.includes('-h')) {
70
+ console.log(MCP_USAGE);
71
+ return 0;
72
+ }
73
+ if (rest.length) {
74
+ console.error(`bolloon mcp serve 不接受参数 (收到: ${rest.join(' ')}) —— 所有调用都走 JSON-RPC (stdio)`);
75
+ return 1;
76
+ }
77
+ protectStdout();
78
+ return serveStdio();
79
+ }
80
+ function mcpTools(rest) {
81
+ const inv = inventory();
82
+ if (rest.includes('--json')) {
83
+ console.log(JSON.stringify(inv, null, 2));
84
+ return 0;
85
+ }
86
+ console.log(`\nbolloon mcp (server ${inv.server.name}@${inv.server.version}, MCP ${inv.server.protocolVersions[0]})`);
87
+ console.log(`\n 工具 (${inv.tools.length}, 每个 = 一条 P3 子命令的薄包装):`);
88
+ for (const t of inv.tools)
89
+ console.log(` · ${t.name.padEnd(28)} ${t.title} [${t.params.join(', ')}]`);
90
+ console.log(`\n 资源 (${inv.resources.length}):`);
91
+ for (const r of inv.resources)
92
+ console.log(` · ${r.uri.padEnd(30)} ${r.name}`);
93
+ console.log('\n **刻意不暴露** (暴露就得在 MCP 层假装成功):');
94
+ for (const n of NOT_EXPOSED)
95
+ console.log(` · ${n.command}\n ${n.reason}`);
96
+ console.log('');
97
+ return 0;
98
+ }
@@ -0,0 +1,334 @@
1
+ /**
2
+ * network.ts — P3 `bolloon network init|join|status|leave|peers`
3
+ *
4
+ * 全部是**现有服务的薄包装** (不重实现、不编造):
5
+ * init → `gateway-join.ensureGatewayNode()` (本机 libp2p 节点初始化, 幂等)
6
+ * join → `gateway-network.joinNetwork(link)` (orbitdb:// / ipns:// / https:// 链接)
7
+ * 或 `gateway-join.joinGlobalGateway()` (文档驱动全球入网)
8
+ * status → `p2p-info.getLocalP2pInfo()` + `gateway-network.listJoinedNetworks()`
9
+ * + `gateway-join.getGatewayJoinState()` + `agent-registry` (只读, 不主动 warm)
10
+ * peers → `network/p2p` 的 `getPeers()` (需要本进程里有在跑的节点)
11
+ * leave → **没有实现** (仓库里不存在 leaveNetwork) → 如实报 `C_NOT_IMPLEMENTED`
12
+ *
13
+ * 硬要求 (leo 2026-09-21): `network join` 不许只说"加入成功" —— 必须报
14
+ * DID · 网络名 · manifest 是否注册 · P2P 是否在线 · registry 是否同步 · 是否离线降级 · 下一步。
15
+ */
16
+ import { okEnvelope, failEnvelope, notImplemented, line, title, hint, plain, opt, has, } from '../protocol-envelope.js';
17
+ export const NETWORK_USAGE = `
18
+ ${title('bolloon network')}
19
+ bolloon network status [--json] 本机入网态 (DID / 网络 / P2P / registry)
20
+ bolloon network init [--json] 初始化本机 P2P 节点 (幂等)
21
+ bolloon network join [--json] 文档驱动全球入网 (幂等; 已在网则报 already)
22
+ bolloon network join <link> [--json] 按链接加入共享网络 (orbitdb:// / ipns:// / https://)
23
+ bolloon network peers [--json] 本机节点的已连接 peer
24
+ bolloon network leave 未实现 (仓库没有 leaveNetwork) → 如实报 C_NOT_IMPLEMENTED
25
+
26
+ 选项: --url <doc> · --name <名> · --capabilities a,b · --force · --request-id <id> · --timeout <ms> · --quiet
27
+ `;
28
+ /** 只读地收集本机入网事实 (不 warm OrbitDB, 不启动节点, 不改任何文件) */
29
+ async function collectFacts() {
30
+ const { getLocalP2pInfo } = await import('../../agents/p2p-info.js');
31
+ const { listJoinedNetworks } = await import('../../agents/gateway-network.js');
32
+ const { getGatewayJoinState } = await import('../../agents/gateway-join.js');
33
+ const { getAgentRegistry } = await import('../../agents/agent-registry.js');
34
+ const { getLocalManifest } = await import('../../agents/agent-manifest-protocol.js');
35
+ const { getUserOwnerDid } = await import('../../agents/agent-identity.js');
36
+ const p2p = await getLocalP2pInfo();
37
+ const joinedRaw = await listJoinedNetworks();
38
+ const gate = await getGatewayJoinState().catch(() => null);
39
+ const did = String(gate?.did || p2p.did || getUserOwnerDid() || '');
40
+ const name = String(gate?.name || p2p.name || '');
41
+ // manifest 是**进程内**结构 (agent-manifest-protocol 不落盘) —— 一次性 CLI 里读到空是正常的, 如实说
42
+ const m = getLocalManifest();
43
+ const agentIds = (m.agents || []).map((a) => String(a.id));
44
+ const capabilities = (m.agents || []).flatMap((a) => (a.capabilities || []).map(String));
45
+ const reg = getAgentRegistry(did || 'local');
46
+ let services = null;
47
+ try {
48
+ services = (await reg.loadLocal()).length;
49
+ }
50
+ catch {
51
+ services = null;
52
+ }
53
+ return {
54
+ did,
55
+ name,
56
+ p2p: {
57
+ online: !!p2p.peerId && p2p.source === 'live',
58
+ source: p2p.source,
59
+ peerId: p2p.peerId || '',
60
+ multiaddrs: p2p.multiaddrs || [],
61
+ relayAddrs: p2p.relayAddrs || [],
62
+ isRelay: !!p2p.isRelay,
63
+ note: p2p.note,
64
+ },
65
+ joined: joinedRaw.map((n) => ({
66
+ link: n.link, kind: n.kind, name: n.name, joinedAt: n.joinedAt,
67
+ serviceCount: n.serviceCount, lastSyncAt: n.lastSyncAt, networkId: n.networkId,
68
+ })),
69
+ gatewayJoin: gate ? {
70
+ url: gate.url, did: gate.did, name: gate.name, peerId: gate.peerId,
71
+ networkLink: gate.networkLink, networkId: gate.networkId, joinedAt: gate.joinedAt, capabilities: gate.capabilities,
72
+ } : null,
73
+ manifest: {
74
+ registered: agentIds.length > 0,
75
+ agentIds,
76
+ capabilities: Array.from(new Set(capabilities)),
77
+ note: agentIds.length > 0
78
+ ? '本进程内 manifest 已注册 (刚入网时登记的那份)'
79
+ : 'manifest 只在运行中的 Runtime 进程内 (agent-manifest-protocol 不落盘) —— 一次性 CLI 命令读不到它不等于没注册过',
80
+ },
81
+ registry: {
82
+ ready: reg.ready,
83
+ storeName: reg.storeName,
84
+ services: services ?? 0,
85
+ source: reg.ready ? 'orbitdb' : 'local',
86
+ },
87
+ };
88
+ }
89
+ /** registry 预热 (有界): CLI 一次性命令里不许无限等 OrbitDB/IPFS */
90
+ async function warmRegistryBounded(ms) {
91
+ try {
92
+ const { warmAgentRegistry } = await import('../../agents/agent-registry.js');
93
+ return await Promise.race([
94
+ warmAgentRegistry().then((v) => !!v),
95
+ new Promise((resolve) => setTimeout(() => resolve(false), ms)),
96
+ ]);
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ function factsHuman(f, head) {
103
+ const out = [title(head)];
104
+ out.push(line('DID', f.did));
105
+ out.push(line('名称', f.name));
106
+ out.push(line('已加入网络', `${f.joined.length} 个`));
107
+ for (const n of f.joined)
108
+ out.push(` · ${n.name || n.networkId || n.link.slice(0, 60)} (${n.joinedAt.slice(0, 19)}, 服务 ${n.serviceCount})`);
109
+ if (f.gatewayJoin)
110
+ out.push(line('全球入网', `${f.gatewayJoin.networkId || f.gatewayJoin.networkLink || f.gatewayJoin.url} @ ${f.gatewayJoin.joinedAt.slice(0, 19)}`));
111
+ out.push(line('P2P', f.p2p.online ? `在线 peerId=${f.p2p.peerId.slice(0, 16)}…` : '不在线 (当前进程没有节点在跑)'));
112
+ if (f.p2p.multiaddrs.length)
113
+ out.push(line('可拨入地址', `${f.p2p.multiaddrs.length} 条, 例: ${f.p2p.multiaddrs[0].slice(0, 72)}`));
114
+ if (f.p2p.note)
115
+ out.push(line('说明', f.p2p.note));
116
+ out.push(line('manifest', f.manifest.registered ? `已注册 (${f.manifest.agentIds.join(', ')})` : `本 CLI 进程未读到 (${f.manifest.note})`));
117
+ out.push(line('registry', `${f.registry.ready ? 'OrbitDB 就绪' : '未就绪 (离线/未 warm)'} · ${f.registry.source} · 本地服务 ${f.registry.services} 条`));
118
+ out.push(line('离线降级', f.joined.length > 0 && !f.p2p.online ? '是 (入网记录在, 但当前没有节点在跑)' : '否'));
119
+ return out.join('\n');
120
+ }
121
+ function nextActionForFacts(f) {
122
+ if (!f.did)
123
+ return 'rejoin_network';
124
+ if (f.joined.length === 0 && !f.gatewayJoin)
125
+ return 'rejoin_network';
126
+ if (!f.p2p.online)
127
+ return 'rejoin_network';
128
+ return null;
129
+ }
130
+ export async function networkCommand(flags) {
131
+ const sub = flags.positionals[0];
132
+ switch (sub) {
133
+ case 'status': return networkStatus(flags);
134
+ case 'init': return networkInit(flags);
135
+ case 'join': return networkJoin(flags);
136
+ case 'peers': return networkPeers(flags);
137
+ case 'leave': return networkLeave(flags);
138
+ default:
139
+ return {
140
+ envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 network 子命令: ${sub}` : '缺少 network 子命令', { usage: plain(NETWORK_USAGE.trim()) }, [], 'needs_human'),
141
+ human: NETWORK_USAGE,
142
+ };
143
+ }
144
+ }
145
+ // ── network status ──────────────────────────────────────────────────────────
146
+ async function networkStatus(flags) {
147
+ const f = await collectFacts();
148
+ const next = nextActionForFacts(f);
149
+ if (!f.did && f.joined.length === 0 && !f.gatewayJoin) {
150
+ // 诚实的前置条件失败 (§3 NETWORK_NOT_JOINED: 本机没有任何已加入网络)
151
+ return {
152
+ envelope: failEnvelope('NETWORK_NOT_JOINED', '本机还没有加入任何网络 (也没有 DID 身份)', {
153
+ did: f.did, joinedNetworks: [], registryReady: f.registry.ready, p2p: f.p2p,
154
+ }, [], 'rejoin_network'),
155
+ human: `${factsHuman(f, 'bolloon network status')}\n\n${hint('下一步: bolloon network join (或先 bolloon setup 建立身份)')}`,
156
+ };
157
+ }
158
+ const data = {
159
+ did: f.did,
160
+ name: f.name,
161
+ joinedNetworks: f.joined,
162
+ gatewayJoin: f.gatewayJoin,
163
+ p2pOnline: f.p2p.online,
164
+ p2p: f.p2p,
165
+ manifest: f.manifest,
166
+ registry: f.registry,
167
+ offlineDegraded: !f.p2p.online,
168
+ };
169
+ const env = okEnvelope('OK', `本机入网态: ${f.joined.length} 个网络${f.p2p.online ? ', P2P 在线' : ', P2P 未在线'}`, data, [
170
+ ...(f.did ? [f.did] : []),
171
+ ...(f.p2p.peerId ? [f.p2p.peerId] : []),
172
+ ], next);
173
+ return { envelope: env, human: `${factsHuman(f, 'bolloon network status')}\n\n${hint(next === 'rejoin_network' ? '下一步: 启动一个本机 Runtime (bolloon --web) 或 bolloon network init, 才有节点在跑' : 'P2P 在线, 可直接 bolloon p2p --json 拿到可拨入地址')}` };
174
+ }
175
+ // ── network init ────────────────────────────────────────────────────────────
176
+ async function networkInit(flags) {
177
+ const { ensureGatewayNode } = await import('../../agents/gateway-join.js');
178
+ const node = await ensureGatewayNode({ nodeTimeoutMs: flags.timeoutMs ?? 25_000 });
179
+ if (!node.ok) {
180
+ return {
181
+ envelope: failEnvelope('NETWORK_NOT_JOINED', `本机 P2P 节点没起来: ${node.error || '未知原因'}`, { error: node.error || null, did: (await collectFacts()).did }, [], 'rejoin_network'),
182
+ human: `本机 P2P 节点没起来\n 原因: ${node.error || '未知原因'}\n\n${hint('下一步: bolloon doctor 看运行时; 或 bolloon network join 走文档驱动入网')}`,
183
+ };
184
+ }
185
+ const f = await collectFacts();
186
+ return {
187
+ envelope: okEnvelope('NETWORK_NODE_READY', `本机 P2P 节点就绪 (peerId=${String(node.peerId).slice(0, 16)}…)`, {
188
+ did: f.did, peerId: node.peerId || '', started: !!node.started, multiaddrs: node.multiaddrs || [],
189
+ }, node.peerId ? [String(node.peerId)] : [], f.did ? null : 'rejoin_network'),
190
+ human: `${title('bolloon network init')}\n${line('DID', f.did)}\n${line('peerId', node.peerId || '(未拿到)')}\n${line('本次启动', node.started ? '是' : '否 (已在跑)')}\n${line('可拨入地址', (node.multiaddrs || []).length + ' 条')}\n\n${hint('注意: 这条命令起的节点随进程结束 (一次性命令) —— 要长期在线用 bolloon --web')}`,
191
+ };
192
+ }
193
+ // ── network join ────────────────────────────────────────────────────────────
194
+ async function networkJoin(flags) {
195
+ const link = flags.positionals[1] || opt(flags, '--link');
196
+ const wantGlobal = has(flags, '--global') || !link;
197
+ const steps = [];
198
+ let joined = false;
199
+ let already = false;
200
+ let networkName = '';
201
+ let networkId = '';
202
+ let networkLink = '';
203
+ let error = '';
204
+ let mode = '';
205
+ if (!wantGlobal && link) {
206
+ // 按链接加入 (gateway-network.joinNetwork: 拉远端服务 → 并入本地 registry + 记成员身份)
207
+ const { joinNetwork } = await import('../../agents/gateway-network.js');
208
+ mode = 'link';
209
+ const r = await joinNetwork(link);
210
+ joined = r.ok;
211
+ already = !!r.already;
212
+ networkName = r.networkName || '';
213
+ networkId = r.networkId || '';
214
+ error = r.error || '';
215
+ steps.push({ step: '按链接加入网络', ok: r.ok, note: r.ok ? `${r.already ? '已在网(幂等)' : `拉取远端服务 ${r.total} 条, 新增 ${r.joined} 条`} (kind=${r.linkKind || '?'})` : (r.error || '') });
216
+ }
217
+ else {
218
+ // 文档驱动全球入网 (gateway-join.joinGlobalGateway)
219
+ const { joinGlobalGateway } = await import('../../agents/gateway-join.js');
220
+ mode = 'global';
221
+ const caps = (opt(flags, '--capabilities') || '').split(',').map((s) => s.trim()).filter(Boolean);
222
+ const r = await joinGlobalGateway({
223
+ url: opt(flags, '--url'),
224
+ name: opt(flags, '--name'),
225
+ capabilities: caps.length ? caps : undefined,
226
+ force: has(flags, '--force'),
227
+ });
228
+ joined = r.ok;
229
+ already = !!r.already;
230
+ networkName = r.networkId || '';
231
+ networkId = r.networkId || '';
232
+ networkLink = r.networkLink || '';
233
+ error = r.error || '';
234
+ steps.push(...r.steps.map((s) => ({ step: s.step, ok: s.ok, note: s.note })));
235
+ }
236
+ // 入网后重新采集事实 (join 会写 gateway-join.json / gateway-networks.json)
237
+ const f = await collectFacts();
238
+ const manifestStep = steps.find((s) => s.step.includes('manifest'));
239
+ const serviceStep = steps.find((s) => s.step.includes('服务登记'));
240
+ const registrySynced = mode === 'link'
241
+ ? joined || f.joined.length > 0
242
+ : (serviceStep ? serviceStep.ok : f.joined.length > 0 || !!f.gatewayJoin);
243
+ const manifestRegistered = mode === 'global' ? (manifestStep ? manifestStep.ok : f.manifest.registered) : f.manifest.registered;
244
+ const offlineDegraded = !f.p2p.online;
245
+ const data = {
246
+ mode,
247
+ did: f.did,
248
+ networkName: networkName || f.name,
249
+ networkId,
250
+ networkLink,
251
+ already,
252
+ manifest: {
253
+ registered: manifestRegistered,
254
+ persisted: false,
255
+ agentIds: f.manifest.agentIds,
256
+ note: f.manifest.note,
257
+ },
258
+ p2p: {
259
+ online: f.p2p.online,
260
+ source: f.p2p.source,
261
+ peerId: f.p2p.peerId,
262
+ multiaddrs: f.p2p.multiaddrs.slice(0, 4),
263
+ },
264
+ registry: { ready: f.registry.ready, synced: registrySynced, services: f.registry.services, source: f.registry.source },
265
+ offlineDegraded,
266
+ steps,
267
+ };
268
+ const evidence = [f.did, f.p2p.peerId, networkLink].filter(Boolean);
269
+ const human = [
270
+ title('bolloon network join'),
271
+ line('DID', f.did),
272
+ line('网络名', networkName || f.name || '(未拿到)'),
273
+ line('入网方式', mode === 'global' ? '文档驱动 (joinGlobalGateway)' : `链接 (${link})`),
274
+ line('幂等', already ? '已在网 (already=true, 没有重复建网)' : '本次入网'),
275
+ line('manifest', manifestRegistered ? '已注册 (本进程读回确认)' : `未确认 (${f.manifest.note})`),
276
+ line('P2P', f.p2p.online ? `在线 peerId=${f.p2p.peerId.slice(0, 16)}…` : `不在线 (source=${f.p2p.source || 'none'})`),
277
+ line('registry', `${f.registry.ready ? 'OrbitDB 就绪' : '未就绪'} · 同步=${registrySynced ? '是' : '否'} · 本地服务 ${f.registry.services} 条`),
278
+ line('离线降级', offlineDegraded ? '是 —— 入网记录在, 但当前没有节点在跑 (重启后由 restoreJoinedNetworks 恢复)' : '否'),
279
+ steps.length ? `\n 步骤:` : '',
280
+ ...steps.map((s) => ` ${s.ok ? '✓' : '✗'} ${s.step}: ${s.note.slice(0, 120)}`),
281
+ ].filter(Boolean).join('\n');
282
+ const next = !joined ? 'rejoin_network' : (offlineDegraded ? 'rejoin_network' : null);
283
+ const env = joined
284
+ ? okEnvelope('NETWORK_JOINED', `已加入网络 ${networkName || f.name || networkLink || ''}${already ? ' (幂等命中, 未重复建网)' : ''}`, data, evidence, next)
285
+ : failEnvelope('NETWORK_NOT_JOINED', `入网没成功: ${error || '未知原因'}`, data, evidence, 'rejoin_network');
286
+ return { envelope: env, human: `${human}\n\n${hint(joined ? (offlineDegraded ? '下一步: 启动常驻 Runtime (bolloon --web) 让节点真的在线; 再 bolloon p2p --json 拿可拨入地址' : '下一步: 无需额外动作') : '下一步: 检查网络/文档地址, 或 bolloon network status 看现状')}` };
287
+ }
288
+ // ── network peers ───────────────────────────────────────────────────────────
289
+ async function networkPeers(flags) {
290
+ const peerId = await currentProcessPeerId();
291
+ if (!peerId) {
292
+ // §3 NETWORK_NOT_JOINED: 本机没有 peerId (没有在跑的节点) —— 不编造 peer
293
+ return {
294
+ envelope: failEnvelope('NETWORK_NOT_JOINED', '当前进程没有在跑的 P2P 节点, 拿不到 peer 列表', {
295
+ online: false, peers: [],
296
+ note: 'peer 列表来自运行中的节点 (p2pNetwork.getPeers()); 一次性 CLI 命令里节点没起就没有可列的对端',
297
+ }, [], 'rejoin_network'),
298
+ human: `${title('bolloon network peers')}\n 当前进程没有在跑的 P2P 节点 → 没有 peer 可列 (不是"网络上没人")\n\n${hint('下一步: bolloon network init 起一个节点, 或 bolloon --web 让 Runtime 常驻')}`,
299
+ };
300
+ }
301
+ const mod = await import('../../network/p2p.js');
302
+ const peers = (mod.p2pNetwork?.getPeers?.() || []).map((p) => String(p));
303
+ return {
304
+ envelope: okEnvelope('OK', `本机节点已连接 ${peers.length} 个 peer`, {
305
+ online: true, peerId, peers, count: peers.length,
306
+ }, [peerId], null),
307
+ human: `${title('bolloon network peers')}\n${line('本机 peerId', peerId)}\n${line('连接数', peers.length)}\n${peers.map((p) => ` · ${p}`).join('\n')}`,
308
+ };
309
+ }
310
+ /** 只在**本进程**里有节点时给 peerId (与 p2p-info 的探测同一套 API) */
311
+ async function currentProcessPeerId() {
312
+ try {
313
+ const mod = await import('../../network/p2p.js');
314
+ const net = mod.p2pNetwork;
315
+ if (!net?.getNode?.())
316
+ return '';
317
+ return String(net.getNodePeerId?.() || '');
318
+ }
319
+ catch {
320
+ return '';
321
+ }
322
+ }
323
+ // ── network leave ───────────────────────────────────────────────────────────
324
+ async function networkLeave(_flags) {
325
+ const env = notImplemented('network leave 还没实现: 仓库里不存在 leaveNetwork (没有任何"退出网络"的服务可包)', {
326
+ plannedCapability: 'network.leave',
327
+ contractState: '入网记录在 ~/.bolloon/gateway-networks.json + ~/.bolloon/gateway-join.json (没有删除/退出的服务入口)',
328
+ alternative: '今天只能手工处理入网记录文件 (CLI 不做, 免得绕过 restoreJoinedNetworks 的成员身份语义)',
329
+ });
330
+ return { envelope: env, human: failHumanLeave(env.message) };
331
+ }
332
+ function failHumanLeave(msg) {
333
+ return `${title('bolloon network leave')}\n ${msg}\n\n${hint('下一步: 没有 → 这条能力在路线图上 (upgrade_client)')}`;
334
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * payment.ts — P3 `bolloon payment pending|approve|reject`
3
+ *
4
+ * 薄包装: `agents/payment-approval.ts` 的 `PaymentApprovalStore` (pending/approve/reject, 落 ~/.bolloon/payment-approvals.json)
5
+ *
6
+ * 红线:
7
+ * · CLI **不代付款** —— 批准只改审批状态; 真正发起支付要由注入的 executor / 持钱包的一方做
8
+ * (与 payment-recovery 同源: "Supervisor 不代付款, 它没有私钥")
9
+ * · `approve` 之后**不许**把结果说成"已付款" (§2 硬规则 2)
10
+ */
11
+ import { okEnvelope, failEnvelope, line, title, hint, plain, } from '../protocol-envelope.js';
12
+ export const PAYMENT_USAGE = `
13
+ ${title('bolloon payment')}
14
+ bolloon payment pending [--json] 待人工放行的付款请求
15
+ bolloon payment approve <审批id> [--json] 批准 (只改审批状态; CLI 不发付款)
16
+ bolloon payment reject <审批id> [--json] 拒绝
17
+
18
+ 选项: --json · --quiet · --timeout <ms>
19
+ `;
20
+ export async function paymentCommand(flags) {
21
+ const sub = flags.positionals[0];
22
+ switch (sub) {
23
+ case 'pending': return paymentPending(flags);
24
+ case 'approve': return paymentApprove(flags);
25
+ case 'reject': return paymentReject(flags);
26
+ default:
27
+ return {
28
+ envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 payment 子命令: ${sub}` : '缺少 payment 子命令', { usage: plain(PAYMENT_USAGE.trim()) }, [], 'needs_human'),
29
+ human: PAYMENT_USAGE,
30
+ };
31
+ }
32
+ }
33
+ function view(a) {
34
+ return {
35
+ id: a.id, service: a.service, amount: a.amount, recipient: a.recipient,
36
+ reason: a.reason, status: a.status, createdAt: new Date(a.createdAt).toISOString(),
37
+ decidedAt: a.decidedAt ? new Date(a.decidedAt).toISOString() : null,
38
+ result: a.result || null,
39
+ };
40
+ }
41
+ async function paymentPending(flags) {
42
+ const { getApprovalStore } = await import('../../agents/payment-approval.js');
43
+ const store = getApprovalStore();
44
+ const pending = await store.pending();
45
+ const all = await store.list();
46
+ const data = {
47
+ count: pending.length,
48
+ pending: pending.map(view),
49
+ total: all.length,
50
+ source: '~/.bolloon/payment-approvals.json',
51
+ note: pending.length
52
+ ? '这些付款在等你放行: ok:false + code=PAYMENT_REQUIRED 是**正确表达** (不是错误, 也不是重试信号)'
53
+ : '没有待放行的付款',
54
+ };
55
+ const human = [
56
+ title('bolloon payment pending'),
57
+ line('待放行', `${pending.length} 笔 (历史共 ${all.length})`),
58
+ ...pending.map((a) => `\n ${a.id}\n 服务 ${a.service} · ${a.amount} → ${a.recipient}\n 原因: ${a.reason}`),
59
+ pending.length ? '' : `\n ${hint('没有待放行的付款')}`,
60
+ ].filter(Boolean).join('\n');
61
+ return {
62
+ envelope: okEnvelope('OK', `待放行付款 ${pending.length} 笔`, data, pending.map((a) => a.id), pending.length ? 'approve_payment' : null),
63
+ human,
64
+ };
65
+ }
66
+ async function paymentApprove(flags) {
67
+ const id = flags.positionals[1];
68
+ if (!id) {
69
+ return {
70
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少审批 id (bolloon payment pending 列出)', { usage: plain(PAYMENT_USAGE.trim()) }, [], 'needs_human'),
71
+ human: PAYMENT_USAGE,
72
+ };
73
+ }
74
+ const { getApprovalStore } = await import('../../agents/payment-approval.js');
75
+ const r = await getApprovalStore().approve(id);
76
+ if (!r.ok || !r.approval) {
77
+ const notFound = /不存在/.test(String(r.error || ''));
78
+ return {
79
+ envelope: failEnvelope(notFound ? 'NOT_FOUND' : 'TASK_TRANSITION_REJECTED', `批准失败: ${r.error}`, { approvalId: id }, [], 'needs_human'),
80
+ human: `${title(`bolloon payment approve ${id}`)}\n ✗ ${r.error}`,
81
+ };
82
+ }
83
+ const executed = r.approval.status === 'executed';
84
+ const data = {
85
+ approval: view(r.approval),
86
+ executed,
87
+ paid: executed,
88
+ note: executed
89
+ ? 'executor 已执行'
90
+ : '批准已记录; **没有发起付款** —— CLI 不持有钱包, 真正支付由持钱包的一方 (Runtime/executor) 执行',
91
+ };
92
+ return {
93
+ envelope: okEnvelope('PAYMENT_APPROVED', executed ? `已批准并执行: ${id}` : `已批准: ${id} (未发起付款)`, data, [id], executed ? null : 'needs_human'),
94
+ human: [
95
+ title(`bolloon payment approve ${id}`),
96
+ line('状态', r.approval.status),
97
+ line('金额', `${r.approval.amount} → ${r.approval.recipient}`),
98
+ line('服务', r.approval.service),
99
+ line('是否已付款', executed ? '是 (executor 执行)' : '否 —— 只改了审批状态, CLI 不发付款'),
100
+ `\n ${data.note}`,
101
+ ].join('\n'),
102
+ };
103
+ }
104
+ async function paymentReject(flags) {
105
+ const id = flags.positionals[1];
106
+ if (!id) {
107
+ return {
108
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少审批 id', { usage: plain(PAYMENT_USAGE.trim()) }, [], 'needs_human'),
109
+ human: PAYMENT_USAGE,
110
+ };
111
+ }
112
+ const { getApprovalStore } = await import('../../agents/payment-approval.js');
113
+ const r = await getApprovalStore().reject(id);
114
+ if (!r.ok || !r.approval) {
115
+ const notFound = /不存在/.test(String(r.error || ''));
116
+ return {
117
+ envelope: failEnvelope(notFound ? 'NOT_FOUND' : 'TASK_TRANSITION_REJECTED', `拒绝失败: ${r.error}`, { approvalId: id }, [], 'needs_human'),
118
+ human: `${title(`bolloon payment reject ${id}`)}\n ✗ ${r.error}`,
119
+ };
120
+ }
121
+ return {
122
+ envelope: okEnvelope('PAYMENT_REJECTED', `已拒绝: ${id}`, { approval: view(r.approval), paid: false }, [id], null),
123
+ human: `${title(`bolloon payment reject ${id}`)}\n${line('状态', r.approval.status)}\n${line('金额', `${r.approval.amount} → ${r.approval.recipient}`)}\n\n ${hint('拒绝后不会付款; 同一 requestId 的幂等保护仍在')}`,
124
+ };
125
+ }