@bolloon/bolloon-agent 0.4.30 → 0.4.32

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 (68) 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/chain/chain-config.js +504 -0
  4. package/dist/agents/chain/chain-index-query.js +135 -0
  5. package/dist/agents/chain/chain-indexer.js +1045 -0
  6. package/dist/agents/chain/chain-settlement.js +284 -0
  7. package/dist/agents/chain/chain-state-store.js +245 -0
  8. package/dist/agents/chain/chain-wallet.js +198 -0
  9. package/dist/agents/chain/escrow-client.js +433 -0
  10. package/dist/agents/chain/index.js +20 -0
  11. package/dist/agents/chain/onchain-trade.js +691 -0
  12. package/dist/agents/economic-policy.js +7 -0
  13. package/dist/agents/gateway-network.js +6 -0
  14. package/dist/agents/local-signer.js +59 -0
  15. package/dist/agents/network-pulse.js +912 -0
  16. package/dist/agents/task/task-onchain-runner.js +284 -0
  17. package/dist/agents/task-contract.js +322 -0
  18. package/dist/agents/task-inbox.js +267 -0
  19. package/dist/agents/task-transport.js +280 -0
  20. package/dist/agents/x402/paid-info-store.js +75 -6
  21. package/dist/agents/x402/transaction-store.js +8 -1
  22. package/dist/cli/commands/agent.js +230 -0
  23. package/dist/cli/commands/chain.js +926 -0
  24. package/dist/cli/commands/index.js +69 -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 +188 -0
  33. package/dist/cli/mcp/tools.js +955 -0
  34. package/dist/cli/protocol-envelope.js +358 -0
  35. package/dist/cli-entry.js +146 -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/chain/chain-config.js +556 -0
  41. package/dist/electron-build/agents/chain/chain-config.js.map +1 -0
  42. package/dist/electron-build/agents/chain/chain-index-query.js +145 -0
  43. package/dist/electron-build/agents/chain/chain-index-query.js.map +1 -0
  44. package/dist/electron-build/agents/chain/chain-indexer.js +1095 -0
  45. package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -0
  46. package/dist/electron-build/agents/chain/chain-settlement.js +291 -0
  47. package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -0
  48. package/dist/electron-build/agents/chain/escrow-client.js +443 -0
  49. package/dist/electron-build/agents/chain/escrow-client.js.map +1 -0
  50. package/dist/electron-build/agents/economic-policy.js +7 -0
  51. package/dist/electron-build/agents/economic-policy.js.map +1 -1
  52. package/dist/electron-build/agents/gateway-network.js +6 -0
  53. package/dist/electron-build/agents/gateway-network.js.map +1 -1
  54. package/dist/electron-build/agents/network-pulse.js +979 -0
  55. package/dist/electron-build/agents/network-pulse.js.map +1 -0
  56. package/dist/electron-build/agents/x402/paid-info-store.js +76 -6
  57. package/dist/electron-build/agents/x402/paid-info-store.js.map +1 -1
  58. package/dist/electron-build/agents/x402/transaction-store.js +8 -1
  59. package/dist/electron-build/agents/x402/transaction-store.js.map +1 -1
  60. package/dist/web/chain-explorer.js +617 -0
  61. package/dist/web/explorer.html +163 -0
  62. package/dist/web/index.html +5 -0
  63. package/dist/web/routes-chain-index.js +108 -0
  64. package/dist/web/routes-onchain-trade.js +99 -0
  65. package/dist/web/server.js +42 -0
  66. package/dist/web/task-frame-server.js +211 -0
  67. package/package.json +2 -1
  68. package/scripts/build-web.ts +20 -1
@@ -0,0 +1,358 @@
1
+ /**
2
+ * protocol-envelope.ts — P3 统一输出层 (P1 §2 冻结 JSON 信封 + §3 错误码 + §2.3 next_action 词表)
3
+ *
4
+ * 权威口径: `docs/wiki/access-protocol-v1.md`。本文件**只做四件事**, 不含任何业务逻辑:
5
+ * ① 全局选项解析: `--json` / `--quiet` / `--request-id <id>` / `--timeout <ms>`
6
+ * ② 信封构造: `{ ok, code, message, data, evidence, next_action }` (字段顺序与 §2 一致)
7
+ * ③ 输出: `--json` 信封 · `--quiet` 只出结果 · 默认人类可读 (既有体验是基线, 不许坏)
8
+ * ④ 兜底: 超时/异常也必须是**结构化失败** (§2 硬规则 1); 私钥类字段一律不进 stdout
9
+ *
10
+ * 2026-09-21 (P3): 此前 `src/` 里**没有任何** §3 错误码常量 (P1 §0 表逐条核对过) —— 本文件把它们落成常量。
11
+ *
12
+ * 三条不许破的规矩 (P1 §2 硬规则):
13
+ * 1. 成功和失败都必须结构化 —— 失败也要 `ok:false` + `code` + `next_action`
14
+ * 2. "已付款" ≠ "任务成功" —— `paid`/`delivered` 都不是成功; 成功判据是 §5.2 (任务层: state=verified ∧ 事实∈{fully_settled,payment_verified})
15
+ * 3. 客户端必须忽略未知 `code` —— 所以本文件新增的码是**追加**, 不改任何冻结码的含义
16
+ */
17
+ import { AUDIT_FORBIDDEN_KEYS } from '../agents/task-contract.js';
18
+ // ── §3 冻结错误码 (字面量与 P1 表逐条一致; 改含义 = 必须 bump 协议版本) ──────────
19
+ /** P1 §3 表里的码 (含成功码)。这些的含义**冻结**, 不许在 CLI 层重新解释。 */
20
+ export const FROZEN_CODES = [
21
+ 'TASK_SUBMITTED', 'TASK_ACCEPTED', 'TASK_COMPLETED', 'TASK_VERIFIED',
22
+ 'PAYMENT_REQUIRED', 'PAYMENT_PENDING', 'POLICY_DENIED', 'BUDGET_EXCEEDED',
23
+ 'NETWORK_NOT_JOINED', 'CAPABILITY_NOT_FOUND', 'RESULT_UNVERIFIED',
24
+ 'PROTOCOL_VERSION_UNSUPPORTED', 'PAYMENT_UNCERTAIN', 'DUPLICATE_REQUEST',
25
+ 'TASK_TRANSITION_REJECTED', 'SIGNATURE_REQUIRED', 'SIGNATURE_INVALID',
26
+ 'DEADLINE_EXPIRED', 'WALLET_UNAVAILABLE', 'AGENT_NOT_AUTHORIZED',
27
+ 'LOCAL_DEV_NOT_CHAIN', 'DISPUTE_OPEN', 'DELIVERY_FAILED',
28
+ ];
29
+ /**
30
+ * P3 新增码 (P1 §1.3「新增错误码不 bump 版本」允许; 老客户端按 `ok:false` + `next_action` 处理, 不崩不猜)。
31
+ * 只加**表里确实没有对应语义**的: 命令组自身的成败、参数错、找不到、超时、未实现。
32
+ */
33
+ export const CLI_CODES = [
34
+ // 命令组成功码
35
+ 'OK', 'NETWORK_JOINED', 'NETWORK_NODE_READY', 'AGENT_REGISTERED',
36
+ 'POLICY_UPDATED', 'PAYMENT_APPROVED', 'PAYMENT_REJECTED', 'RECOVERY_PLANNED',
37
+ 'WALLET_SIGNED', 'TASK_DELIVERED',
38
+ // 任务态里 §3 没有单列的终态 (字面量取自冻结的 14 态状态机)
39
+ 'TASK_REJECTED', 'TASK_FAILED', 'TASK_CANCELLED',
40
+ // 传输层 (§1.3「新增错误码不 bump 版本」): 帧没送到 / 没有可用传输
41
+ 'TRANSPORT_UNAVAILABLE', 'TRANSPORT_FAILED',
42
+ // P6 链命令组的新增码 (append-only; 表里确实没有对应语义才加):
43
+ // 配置/可达性/结论未定/链上回滚/找不到 escrow/资金不足/未授权/重组可疑
44
+ 'CHAIN_NOT_CONFIGURED', 'CHAIN_UNAVAILABLE', 'CHAIN_UNCERTAIN', 'CHAIN_TX_REVERTED',
45
+ 'ESCROW_NOT_FOUND', 'INSUFFICIENT_FUNDS', 'NOT_AUTHORIZED', 'REORG_SUSPECTED',
46
+ // 2026-09-22 (P5 修正): **索引身份变更** (换合约部署 / anvil 重启换链实例) —— 这**不是重组**,
47
+ // 混进 REORG_SUSPECTED 会把人引到错的方向 (去查分叉, 而真因是索引属于另一个合约/另一条链)。
48
+ 'INDEX_IDENTITY_CHANGED',
49
+ // 通用失败码
50
+ 'C_NOT_IMPLEMENTED', 'INVALID_ARGUMENT', 'NOT_FOUND', 'TIMEOUT', 'INTERNAL_ERROR',
51
+ ];
52
+ /** §2.3 受控词表。`verify_result` 出现在 §3 表 (TASK_COMPLETED 行) 但 §2.3 表漏列 —— 以 §3 为准。 */
53
+ export const NEXT_ACTIONS = [
54
+ 'reconcile', 'approve_payment', 'needs_human', 'retry_same_request',
55
+ 'rejoin_network', 'redefine_capability', 'raise_budget', 'upgrade_client',
56
+ 'wait', 'verify_result',
57
+ ];
58
+ /** 带参数的词表项 (字面量在 payment-recovery.ts:299-300) */
59
+ export const NEXT_ACTION_PARAM_PREFIXES = ['x402_payment_retry:', 'x402_continue:'];
60
+ /** 带值的选项 (解析时要知道下一个 token 是不是值) */
61
+ const OPTIONS_WITH_VALUE = new Set([
62
+ '--request-id', '--timeout', '--budget', '--per-purchase', '--daily', '--input',
63
+ '--capability', '--name', '--price', '--per', '--description', '--wallet', '--endpoint',
64
+ '--link', '--url', '--instruction', '--provider', '--amount', '--currency', '--network',
65
+ '--per-tx', '--allow-recipient', '--allow-service', '--rate-limit', '--reason',
66
+ // 2026-09-21 (P3 收尾): bolloon-task/1 真收发 + wallet sign 用到的选项
67
+ '--payload', '--message', '--mode', '--task-id', '--deadline', '--salt', '--reply-to',
68
+ '--peer', '--via', '--eta', '--deliver',
69
+ // 2026-09-22 (P6): `bolloon chain` 用的选项 (只加链命令组自己的名字, 不改既有布尔开关的解析)
70
+ '--task-key', '--agent', '--asset', '--result', '--manifest-digest',
71
+ '--confirmation-window', '--proof-version', '--from-block', '--gate',
72
+ // 2026-09-22 (P6b): 链上**写**操作的**授权意图**声明 (MCP 写 tool 必填; 只可能收紧, 不可能放权)
73
+ '--payment-mode',
74
+ ]);
75
+ export function parseFlags(args) {
76
+ const flags = { json: false, quiet: false, positionals: [], options: new Map(), raw: args };
77
+ for (let i = 0; i < args.length; i++) {
78
+ const a = args[i];
79
+ if (a === '--json') {
80
+ flags.json = true;
81
+ continue;
82
+ }
83
+ if (a === '--quiet' || a === '-q') {
84
+ flags.quiet = true;
85
+ continue;
86
+ }
87
+ if (a.startsWith('--')) {
88
+ const eq = a.indexOf('=');
89
+ const key = eq > 0 ? a.slice(0, eq) : a;
90
+ let value = eq > 0 ? a.slice(eq + 1) : undefined;
91
+ if (value === undefined && OPTIONS_WITH_VALUE.has(key) && i + 1 < args.length && !args[i + 1].startsWith('--')) {
92
+ value = args[++i];
93
+ }
94
+ const list = flags.options.get(key) || [];
95
+ list.push(value === undefined ? 'true' : value);
96
+ flags.options.set(key, list);
97
+ continue;
98
+ }
99
+ flags.positionals.push(a);
100
+ }
101
+ const rid = flags.options.get('--request-id')?.[0];
102
+ if (rid && rid !== 'true')
103
+ flags.requestId = rid;
104
+ const t = flags.options.get('--timeout')?.[0];
105
+ if (t && t !== 'true') {
106
+ const n = Number(t);
107
+ if (Number.isFinite(n) && n > 0)
108
+ flags.timeoutMs = Math.floor(n);
109
+ }
110
+ return flags;
111
+ }
112
+ /** 取单个选项值 (取最后一次出现的) */
113
+ export function opt(flags, name) {
114
+ const v = flags.options.get(name)?.slice(-1)[0];
115
+ return v === 'true' ? undefined : v;
116
+ }
117
+ /** 取可重复选项的全部值 */
118
+ export function optAll(flags, name) {
119
+ return (flags.options.get(name) || []).filter((v) => v !== 'true');
120
+ }
121
+ export function has(flags, name) {
122
+ return flags.options.has(name);
123
+ }
124
+ export function okEnvelope(code, message, data = {}, evidence = [], next_action = null) {
125
+ return { ok: true, code, message, data, evidence, next_action };
126
+ }
127
+ export function failEnvelope(code, message, data = {}, evidence = [], next_action = null) {
128
+ return { ok: false, code, message, data, evidence, next_action };
129
+ }
130
+ /**
131
+ * 按"这个码是不是成功码"决定 `ok` (P1 §2.1: `ok` 是唯一分派位; §3 表把成功码与失败码分开)。
132
+ * 状态类命令用它: 例如 `policy_denied` 必须是 `ok:false` —— 不许让客户端把被拒当成完成。
133
+ */
134
+ export function envelopeForState(ok, code, message, data = {}, evidence = [], next_action = null) {
135
+ return ok ? okEnvelope(code, message, data, evidence, next_action) : failEnvelope(code, message, data, evidence, next_action);
136
+ }
137
+ /** `C_NOT_IMPLEMENTED` 的统一说法: 如实说没实现 + 指出现成可用的替代路径 (绝不假装成功) */
138
+ export function notImplemented(message, data = {}) {
139
+ return failEnvelope('C_NOT_IMPLEMENTED', message, { reason: message, ...data }, [], 'upgrade_client');
140
+ }
141
+ /**
142
+ * 私钥类字段兜底剥离 (P1 §5.5): 复用契约层的 `AUDIT_FORBIDDEN_KEYS`
143
+ * (`privateKey`/`secret`/`instruction`/`taskText`/`mnemonic`/`seed`) —— 绝不让它们进 stdout。
144
+ * 命中即替换成 `[redacted]`, 并**不静默**: 顶层补 `redacted_fields` 供审计。
145
+ */
146
+ export function redactSecrets(value, hits = [], path = '$') {
147
+ if (Array.isArray(value))
148
+ return value.map((v, i) => redactSecrets(v, hits, `${path}[${i}]`));
149
+ if (value && typeof value === 'object') {
150
+ const out = {};
151
+ for (const [k, v] of Object.entries(value)) {
152
+ if (AUDIT_FORBIDDEN_KEYS.includes(k)) {
153
+ hits.push(`${path}.${k}`);
154
+ out[k] = '[redacted]';
155
+ continue;
156
+ }
157
+ out[k] = redactSecrets(v, hits, `${path}.${k}`);
158
+ }
159
+ return out;
160
+ }
161
+ return value;
162
+ }
163
+ /** 收敛成冻结字段顺序 + 兜底剥离 (所有对外输出都必须过这里) */
164
+ export function finalizeEnvelope(env, flags) {
165
+ const hits = [];
166
+ const data = redactSecrets(env.data || {}, hits);
167
+ if (hits.length)
168
+ data.redacted_fields = hits;
169
+ // `--request-id`: 幂等键原样透传/回显 (真实用它的命令会把它写进自己的查询路径)
170
+ if (flags?.requestId && data.requestId === undefined)
171
+ data.requestId = flags.requestId;
172
+ return {
173
+ ok: env.ok,
174
+ code: env.code,
175
+ message: env.message,
176
+ data,
177
+ evidence: Array.isArray(env.evidence) ? env.evidence : [],
178
+ next_action: env.next_action ?? null,
179
+ };
180
+ }
181
+ /** 老命令 (task/trace/p2p/x402) 的 `--json`: **保留既有顶层键** (基线兼容), 追加 §2 的 code/message/evidence/next_action */
182
+ export function legacyJson(payload, extra) {
183
+ const hits = [];
184
+ const redacted = redactSecrets({ ...payload, code: extra.code, message: extra.message, evidence: extra.evidence || [], next_action: extra.next_action ?? null }, hits);
185
+ if (hits.length)
186
+ redacted.redacted_fields = hits;
187
+ return redacted;
188
+ }
189
+ // ── 输出 ────────────────────────────────────────────────────────────────────
190
+ const RESET = '\x1b[0m';
191
+ const BOLD = '\x1b[1m';
192
+ // 与 cli-entry.ts 同一套 truecolor (#ef4444 / #c4d640) —— 注意必须写成十进制 RGB, 不能写 0x 字面量
193
+ const RED = '\x1b[38;2;239;68;68m';
194
+ const CYAN = '\x1b[38;2;196;214;64m';
195
+ /**
196
+ * 打印命令结果并给出退出码 (0/1)。
197
+ * · `--json` → 完整信封 (2 空格缩进, 字段顺序与 §2 一致)
198
+ * · `--quiet` → 只出结果 (payload 的 JSON)
199
+ * · 默认 → 人类可读 (命令自带); 失败时补一行 机器可读的 code/next_action, 方便不解析 JSON 也能分派
200
+ */
201
+ export function emit(env, flags, human = '') {
202
+ const e = finalizeEnvelope(env, flags);
203
+ if (flags.json) {
204
+ console.log(JSON.stringify(e, null, 2));
205
+ }
206
+ else if (flags.quiet) {
207
+ console.log(JSON.stringify(e.data));
208
+ }
209
+ else {
210
+ if (human.trim())
211
+ console.log(human.trimEnd());
212
+ if (!e.ok)
213
+ console.log(`\n${RED}✗ ${e.message}${RESET}\n code=${e.code} next_action=${e.next_action ?? 'null'}`);
214
+ }
215
+ return e.ok ? 0 : 1;
216
+ }
217
+ /** 异常兜底: 任何没被命令自己处理的错都变成结构化失败 (§2 硬规则 1) */
218
+ export function errorEnvelope(err) {
219
+ const msg = String(err?.message || err).slice(0, 300);
220
+ return failEnvelope('INTERNAL_ERROR', `命令内部异常: ${msg}`, { error: msg }, [], 'needs_human');
221
+ }
222
+ /**
223
+ * 跑一条命令: 统一套 `--timeout` (§ 超时也是结构化失败) 与异常兜底, 但**不打印**。
224
+ * 2026-09-21 (P4): 从 `runCommand` 抽出 —— MCP 适配层要的是信封本身 (它不能往 stdout 打字,
225
+ * stdout 是 JSON-RPC 流), 而超时/异常兜底必须与 CLI 是同一条实现 (绝不允许两套语义)。
226
+ */
227
+ export async function commandResult(flags, fn) {
228
+ try {
229
+ if (flags.timeoutMs)
230
+ return await raceTimeout(fn(flags), flags.timeoutMs);
231
+ return await fn(flags);
232
+ }
233
+ catch (err) {
234
+ if (err instanceof CommandTimeout) {
235
+ return {
236
+ envelope: failEnvelope('TIMEOUT', `命令超时 (--timeout ${flags.timeoutMs}ms): 已经处理的部分结果没有落盘保证`, { timeoutMs: flags.timeoutMs ?? null }, [], 'retry_same_request'),
237
+ human: '',
238
+ };
239
+ }
240
+ return { envelope: errorEnvelope(err), human: '' };
241
+ }
242
+ }
243
+ /**
244
+ * 跑一条命令: 统一套 `--timeout` (§ 超时也是结构化失败) 与异常兜底, 然后按 `--json`/`--quiet`/默认输出。
245
+ * 返回进程退出码 (0 = ok, 1 = 失败; 但**判据永远是 `ok`/`code`, 不是退出码**)。
246
+ */
247
+ export async function runCommand(flags, fn) {
248
+ const result = await commandResult(flags, fn);
249
+ return emit(result.envelope, flags, result.human);
250
+ }
251
+ class CommandTimeout extends Error {
252
+ constructor() { super('command timeout'); this.name = 'CommandTimeout'; }
253
+ }
254
+ function raceTimeout(work, ms) {
255
+ return new Promise((resolve, reject) => {
256
+ let settled = false;
257
+ const timer = setTimeout(() => { if (!settled) {
258
+ settled = true;
259
+ reject(new CommandTimeout());
260
+ } }, ms);
261
+ work.then((v) => { if (!settled) {
262
+ settled = true;
263
+ clearTimeout(timer);
264
+ resolve(v);
265
+ } })
266
+ .catch((e) => { if (!settled) {
267
+ settled = true;
268
+ clearTimeout(timer);
269
+ reject(e);
270
+ } });
271
+ });
272
+ }
273
+ /** 人类可读输出的小工具 (与 cli-entry.ts 的配色一致) */
274
+ export const tone = { reset: RESET, bold: BOLD, red: RED, cyan: CYAN };
275
+ /** 把带 ANSI 色的帮助文本变成纯文本 (放进 `--json` 的 data 里时必须先过它) */
276
+ export function plain(t) {
277
+ // eslint-disable-next-line no-control-regex
278
+ return String(t).replace(/\u001b\[[0-9;]*m/g, '');
279
+ }
280
+ export function line(label, value) {
281
+ return ` ${String(label).padEnd(14)} ${value === undefined || value === null || value === '' ? '(无)' : String(value)}`;
282
+ }
283
+ export function title(t) {
284
+ return `\n${BOLD}${t}${RESET}`;
285
+ }
286
+ export function hint(t) {
287
+ return `${CYAN}${t}${RESET}`;
288
+ }
289
+ /** 一行内看结论的失败摘要 (人读模式常用) */
290
+ export function failHuman(head, env) {
291
+ return `${head}\n 原因: ${env.message}\n code: ${env.code} · 下一步: ${env.next_action ?? '无需额外动作'}`;
292
+ }
293
+ /** 任务 14 态 (§4.1) → 对外 4 态 */
294
+ export function publicStatusForTask(state, nextAction) {
295
+ switch (state) {
296
+ case 'discovered':
297
+ case 'quoted':
298
+ return '准备中';
299
+ case 'submitted':
300
+ case 'accepted':
301
+ case 'paid':
302
+ return '正在获取能力';
303
+ case 'paying':
304
+ return nextAction === 'reconcile' ? '需要你处理' : '正在获取能力';
305
+ case 'payment_required':
306
+ case 'policy_denied':
307
+ case 'rejected':
308
+ case 'failed':
309
+ case 'cancelled':
310
+ return '需要你处理';
311
+ case 'running':
312
+ return '正在执行';
313
+ case 'delivered':
314
+ return '已完成或需要你处理';
315
+ case 'verified':
316
+ return '已完成';
317
+ default:
318
+ return '需要你处理';
319
+ }
320
+ }
321
+ /** 交易 11 态 (§4.2) → 对外 4 态 */
322
+ export function publicStatusForTrade(status, nextAction) {
323
+ switch (status) {
324
+ case 'discovered':
325
+ case 'quoted':
326
+ return '准备中';
327
+ case 'paying':
328
+ case 'settled':
329
+ return nextAction === 'reconcile' ? '需要你处理' : '正在获取能力';
330
+ case 'delivered':
331
+ return '已完成或需要你处理';
332
+ case 'verified':
333
+ return '已完成';
334
+ case 'payment_required':
335
+ case 'policy_denied':
336
+ case 'delivery_failed':
337
+ case 'verification_failed':
338
+ case 'disputed':
339
+ case 'failed':
340
+ return '需要你处理';
341
+ default:
342
+ return '需要你处理';
343
+ }
344
+ }
345
+ /**
346
+ * 交易记录的对外口径 —— **local-dev 一律标 `local-dev`, 绝不冒充链上** (P1 §5.1 第 3 条)。
347
+ * `success` 用 §7.1 的判据: `status==='verified'` ∧ 事实 ∈ {fully_settled, payment_verified}。
348
+ */
349
+ export function settlementLabel(rec) {
350
+ if (rec.paymentMode === 'local-dev')
351
+ return 'local-dev';
352
+ if (rec.chainSettled === true && ['payment_verified', 'partially_settled', 'fully_settled'].includes(String(rec.settlementFact || '')))
353
+ return 'chain';
354
+ return 'none';
355
+ }
356
+ export function isTradeSuccessful(rec) {
357
+ return String(rec.status) === 'verified' && ['fully_settled', 'payment_verified'].includes(String(rec.settlementFact || ''));
358
+ }
package/dist/cli-entry.js CHANGED
@@ -22,8 +22,15 @@ import { discoverEngines, delegateToEngine } from './external-engines/index.js';
22
22
  import { x402CheckBalance, x402Fetch } from './agents/x402/x402Pay.js';
23
23
  import { runVersionCommand, runUpdateCommand, runDoctorCommand, runRuntimeCommand, UPDATE_HELP } from './cli/update-commands.js';
24
24
  import { collectVersionInfo } from './utils/version-info.js';
25
+ // 2026-09-21 (P3): 统一 JSON 信封 + 命令组 (network/agent/task/wallet/payment/trade)
26
+ import { runServiceGroup, GROUPS_HELP } from './cli/commands/index.js';
27
+ // 2026-09-21 (P4): MCP 适配层 (`bolloon mcp serve` = stdio, 只调 P3 服务层)
28
+ import { runMcpCommand } from './cli/commands/mcp.js';
29
+ import { legacyJson } from './cli/protocol-envelope.js';
25
30
  import { createRequire } from 'module';
26
31
  const _require = createRequire(import.meta.url);
32
+ /** P3 `bolloon task <子命令>` 的已知子命令 —— 其它一律当 M1 任务正文 (既有体验不动) */
33
+ const TASK_SUBCOMMANDS = new Set(['send', 'list', 'status', 'cancel', 'retry', 'result', 'inbox', 'accept', 'reject', 'run', 'complete']);
27
34
  const isWindows = process.platform === 'win32';
28
35
  // ANSI 颜色 — Bolloon Web UI 配色 truecolor
29
36
  function _fg(r, g, b) { return `\x1b[38;2;${r};${g};${b}m`; }
@@ -68,6 +75,10 @@ ${BOLD}命令:${RESET}
68
75
  bolloon engine run <prompt> 委派任务给智能体
69
76
  bolloon x402 fetch <url> x402 自动支付 HTTP 请求
70
77
  bolloon x402 balance <address> 查询 x402 钱包余额
78
+ bolloon mcp serve MCP server (stdio) —— 给 MCP 客户端接本机 Agent 能力
79
+ bolloon mcp tools 列出 MCP 暴露的 tools/resources (只读清单)
80
+
81
+ ${GROUPS_HELP}
71
82
 
72
83
  ${BOLD}示例:${RESET}
73
84
  bolloon # 启动图形界面
@@ -78,6 +89,8 @@ ${BOLD}示例:${RESET}
78
89
  bolloon update plan # 看更新计划与风险检查
79
90
  bolloon update now # 真正执行更新
80
91
  bolloon doctor # 我这台机器的 Bolloon 是否自洽
92
+ bolloon network join --json # 入网 (失败也结构化: ok:false + code + next_action)
93
+ bolloon task --request-id <id> "…" # 用给定幂等键跑 M1 任务 (同一 requestId 不会付两次)
81
94
  ${UPDATE_HELP}
82
95
 
83
96
  ${BOLD}环境变量:${RESET}
@@ -169,6 +182,23 @@ function parseArgs() {
169
182
  // 2026-09-18: M1 唯一入口 —— 一个任务 → 一个 Skill → 一个报告卡
170
183
  case 'task':
171
184
  return { mode: 'task', args: args.slice(1) };
185
+ // 2026-09-21: P3 命令组 (统一信封 + 全局选项; 薄包装现有服务)
186
+ case 'network':
187
+ return { mode: 'network', args: args.slice(1) };
188
+ case 'agent':
189
+ return { mode: 'agent', args: args.slice(1) };
190
+ case 'wallet':
191
+ return { mode: 'wallet', args: args.slice(1) };
192
+ case 'payment':
193
+ return { mode: 'payment', args: args.slice(1) };
194
+ case 'trade':
195
+ return { mode: 'trade', args: args.slice(1) };
196
+ // 2026-09-22 (P6): 链命令组 (`bolloon chain status|escrow|timeline|index|trade`; 复用 P3/P4/P5)
197
+ case 'chain':
198
+ return { mode: 'chain', args: args.slice(1) };
199
+ // 2026-09-21: P4 MCP 适配层 (`bolloon mcp serve` = stdio MCP server; 只调 P3 服务层)
200
+ case 'mcp':
201
+ return { mode: 'mcp', args: args.slice(1) };
172
202
  // 2026-09-13: 初始化向导 (用户身份 + 模型供应商 + API key)
173
203
  case 'setup':
174
204
  case 'init':
@@ -250,7 +280,18 @@ async function handleX402Command(x402Args) {
250
280
  rpcUrl: readOption(rest, '--rpc-url'),
251
281
  });
252
282
  if (hasFlag(rest, '--json')) {
253
- console.log(JSON.stringify(result, null, 2));
283
+ // 2026-09-21 (P3): 保留既有 {success,data,status,error,paymentInfo,...}, 追加 code/message/next_action
284
+ console.log(JSON.stringify(legacyJson(result, {
285
+ code: result.success ? 'OK' : 'INTERNAL_ERROR',
286
+ message: result.success ? `x402 请求完成 (status=${result.status})` : `x402 请求失败: ${result.error || ''}`,
287
+ evidence: [],
288
+ next_action: result.success ? null : 'retry_same_request',
289
+ }), null, 2));
290
+ if (!result.success)
291
+ process.exit(1);
292
+ }
293
+ else if (hasFlag(rest, '--quiet')) {
294
+ console.log(JSON.stringify(result));
254
295
  if (!result.success)
255
296
  process.exit(1);
256
297
  }
@@ -280,7 +321,17 @@ async function handleX402Command(x402Args) {
280
321
  rpcUrl: readOption(rest, '--rpc-url'),
281
322
  });
282
323
  if (hasFlag(rest, '--json')) {
283
- console.log(JSON.stringify(result, null, 2));
324
+ console.log(JSON.stringify(legacyJson(result, {
325
+ code: result.success ? 'OK' : 'INTERNAL_ERROR',
326
+ message: result.success ? `余额查询完成 (${result.network})` : `余额查询失败: ${result.error || ''}`,
327
+ evidence: [address],
328
+ next_action: result.success ? null : 'retry_same_request',
329
+ }), null, 2));
330
+ if (!result.success)
331
+ process.exit(1);
332
+ }
333
+ else if (hasFlag(rest, '--quiet')) {
334
+ console.log(JSON.stringify(result));
284
335
  if (!result.success)
285
336
  process.exit(1);
286
337
  }
@@ -317,6 +368,7 @@ async function handleX402Command(x402Args) {
317
368
  async function handleTaskCommand(taskArgs) {
318
369
  const { runTask, resumeTask } = await import('./agents/task/task-runner.js');
319
370
  const wantJson = taskArgs.includes('--json');
371
+ const wantQuiet = taskArgs.includes('--quiet');
320
372
  const flag = (name) => {
321
373
  const i = taskArgs.indexOf(name);
322
374
  return i >= 0 ? taskArgs[i + 1] : undefined;
@@ -326,6 +378,8 @@ async function handleTaskCommand(taskArgs) {
326
378
  const perPurchase = flag('--per-purchase');
327
379
  const daily = flag('--daily');
328
380
  const inputRaw = flag('--input');
381
+ // 2026-09-21 (P3): 幂等键透传 —— 同一 requestId 重发不会产生第二笔付款 (契约层派生逻辑不变)
382
+ const requestId = flag('--request-id');
329
383
  let input;
330
384
  if (inputRaw !== undefined) {
331
385
  try {
@@ -338,14 +392,24 @@ async function handleTaskCommand(taskArgs) {
338
392
  }
339
393
  const STAGE_LABEL = { prepare: '准备中', acquire: '正在获取能力', execute: '正在执行', report: '报告' };
340
394
  const onStage = (stage, note) => {
341
- if (wantJson)
395
+ if (wantJson || wantQuiet)
342
396
  return;
343
397
  console.error(` ${CYAN}${STAGE_LABEL[stage] || stage}${RESET} ${note}`);
344
398
  };
345
399
  if (resumeId) {
346
400
  const r = await resumeTask({ goalId: resumeId, input, allowLocalDev: true });
347
- if (wantJson)
348
- console.log(JSON.stringify({ resumed: r.resumed, action: r.action, reason: r.reason, mustNotRepay: r.mustNotRepay, card: r.card }, null, 2));
401
+ const verdict = legacyTaskVerdict(r);
402
+ const payload = {
403
+ resumed: r.resumed, action: r.action, reason: r.reason, mustNotRepay: r.mustNotRepay, card: r.card,
404
+ goalId: r.goalId, runId: r.runId ?? null, transactionId: r.transactionId ?? null,
405
+ };
406
+ if (wantQuiet)
407
+ console.log(JSON.stringify(payload));
408
+ else if (wantJson)
409
+ console.log(JSON.stringify(legacyJson(payload, {
410
+ code: verdict.code, message: verdict.message, next_action: verdict.next,
411
+ evidence: [r.goalId, r.runId, r.transactionId].filter(Boolean),
412
+ }), null, 2));
349
413
  else {
350
414
  console.log('');
351
415
  console.log(r.text);
@@ -357,7 +421,7 @@ async function handleTaskCommand(taskArgs) {
357
421
  if (a.startsWith('--'))
358
422
  return false;
359
423
  const prev = taskArgs[i - 1];
360
- return !['--budget', '--per-purchase', '--daily', '--input'].includes(prev || '');
424
+ return !['--budget', '--per-purchase', '--daily', '--input', '--request-id', '--timeout'].includes(prev || '');
361
425
  });
362
426
  const task = words.join(' ').trim();
363
427
  if (!task) {
@@ -372,19 +436,27 @@ ${CYAN}bolloon task --resume <goalId>${RESET}
372
436
  --per-purchase <USDC> 单次购买上限 (M1 硬上限 0.02)
373
437
  --daily <USDC> 当日预算 (M1 硬上限 0.10)
374
438
  --input '<json>' 显式给技能输入 (跳过自动推导)
375
- --json 机器可读输出
439
+ --request-id <id> 指定幂等键 (同一 requestId 重发不会产生第二笔付款)
440
+ --json 机器可读输出 (+ code/next_action/evidence; 失败也结构化)
376
441
  `);
377
442
  return;
378
443
  }
379
- const r = await runTask({ task, budget, perPurchase, daily, input, allowLocalDev: true, onStage });
380
- if (wantJson) {
381
- console.log(JSON.stringify({
444
+ const r = await runTask({ task, budget, perPurchase, daily, input, requestId, allowLocalDev: true, onStage });
445
+ const verdict = legacyTaskVerdict(r);
446
+ if (wantQuiet) {
447
+ console.log(JSON.stringify({ ok: r.ok, goalId: r.goalId, runId: r.runId, transactionId: r.transactionId, card: r.card, payment: r.payment }));
448
+ }
449
+ else if (wantJson) {
450
+ console.log(JSON.stringify(legacyJson({
382
451
  ok: r.ok, status: r.card.status, conclusion: r.card.conclusion, card: r.card,
383
452
  goalId: r.goalId, runId: r.runId, transactionId: r.transactionId,
384
453
  advisor: r.advisor, payment: r.payment, outputIssues: r.outputIssues,
385
454
  budget: { taskBudget: r.budget.taskBudget, perPurchase: r.budget.perPurchase, daily: r.budget.daily, clamped: r.budget.clamped },
386
455
  stages: r.stages,
387
- }, null, 2));
456
+ }, {
457
+ code: verdict.code, message: verdict.message, next_action: verdict.next,
458
+ evidence: [r.goalId, r.runId, r.transactionId].filter(Boolean),
459
+ }), null, 2));
388
460
  }
389
461
  else {
390
462
  console.log('');
@@ -394,17 +466,39 @@ ${CYAN}bolloon task --resume <goalId>${RESET}
394
466
  // 一次性命令: 显式收尾 (DIAP/HTTP 句柄不该吊住进程)
395
467
  process.exit(r.ok ? 0 : 1);
396
468
  }
469
+ /**
470
+ * 老 M1 命令 (`bolloon task`) 的 §3 码映射 (P1 §3 + §5.1 红线):
471
+ * local-dev 最高到 `delivered` → 一律 `TASK_COMPLETED` + `verify_result`, **绝不** TASK_VERIFIED。
472
+ */
473
+ function legacyTaskVerdict(r) {
474
+ const chain = r.payment?.chainSettled === true || r.card?.payment?.chainSettled === true;
475
+ if (r.ok) {
476
+ return chain
477
+ ? { code: 'TASK_VERIFIED', message: 'Task verified', next: null }
478
+ : { code: 'TASK_COMPLETED', message: 'Task delivered (local-dev: 链上未结算, 不算成功)', next: 'verify_result' };
479
+ }
480
+ return r.card?.hardGate === 'bought_not_executed'
481
+ ? { code: 'DELIVERY_FAILED', message: '已付款但没有交付 → 绝不重付, 交人处理', next: 'needs_human' }
482
+ : { code: 'RESULT_UNVERIFIED', message: '任务没走完/未过验真门', next: 'needs_human' };
483
+ }
397
484
  async function handleTraceCommand(traceArgs) {
398
485
  const { listRuns, readRun } = await import('./agents/run-store.js');
399
486
  const { runToTraceText, runToTraceJson, summarizeTrace } = await import('./agents/trace-export.js');
400
487
  const wantJson = traceArgs.includes('--json');
488
+ const wantQuiet = traceArgs.includes('--quiet');
401
489
  const lastIdx = traceArgs.indexOf('--last');
402
490
  const last = lastIdx >= 0 ? Number(traceArgs[lastIdx + 1]) : undefined;
403
491
  const runId = traceArgs.find((a) => !a.startsWith('--') && a !== String(last));
404
492
  if (!runId) {
405
493
  const runs = await listRuns({ limit: 20 });
494
+ const payloads = runs.map((r) => runToTraceJson(r));
495
+ if (wantQuiet) {
496
+ console.log(JSON.stringify(payloads));
497
+ return;
498
+ }
406
499
  if (wantJson) {
407
- console.log(JSON.stringify(runs.map((r) => runToTraceJson(r)), null, 2));
500
+ // 2026-09-21 (P3): 列表形式也走统一信封 (此前是裸数组; `data` 里仍是同样的 payload 数组)
501
+ console.log(JSON.stringify({ ok: true, code: 'OK', message: `最近 ${runs.length} 次运行`, data: payloads, evidence: payloads.map((p) => p.runId).filter(Boolean), next_action: null }, null, 2));
408
502
  return;
409
503
  }
410
504
  console.log(`\n${BOLD}最近 ${runs.length} 次运行的执行轨迹${RESET}\n`);
@@ -425,12 +519,19 @@ async function handleTraceCommand(traceArgs) {
425
519
  const run = await readRun(runId);
426
520
  if (!run) {
427
521
  console.error(`${MAGENTA}没有这个运行: ${runId}${RESET}`);
522
+ if (wantJson)
523
+ console.log(JSON.stringify({ ok: false, code: 'NOT_FOUND', message: `没有这个运行: ${runId}`, data: { runId }, evidence: [], next_action: 'retry_same_request' }, null, 2));
428
524
  process.exitCode = 1;
429
525
  return;
430
526
  }
431
- console.log(wantJson ? JSON.stringify(runToTraceJson(run), null, 2) : runToTraceText(run, { limit: last }));
432
- if (!wantJson) {
433
- const j = runToTraceJson(run);
527
+ const j = runToTraceJson(run);
528
+ if (wantQuiet)
529
+ console.log(JSON.stringify(j));
530
+ else if (wantJson)
531
+ console.log(JSON.stringify(legacyJson(j, { code: 'OK', message: `运行 ${runId} 轨迹`, evidence: [runId], next_action: null }), null, 2));
532
+ else
533
+ console.log(runToTraceText(run, { limit: last }));
534
+ if (!wantJson && !wantQuiet) {
434
535
  console.error(`\n${CYAN}(${j.counts.total} 步 · ✓${j.counts.ok} / ✗${j.counts.fail} · ${j.counts.totalMs}ms · 状态 ${j.status})${RESET}`);
435
536
  }
436
537
  }
@@ -442,8 +543,19 @@ async function handleTraceCommand(traceArgs) {
442
543
  async function handleP2pCommand(p2pArgs) {
443
544
  const { getLocalP2pInfo, formatP2pInfoText, formatP2pInfoJson } = await import('./agents/p2p-info.js');
444
545
  const info = await getLocalP2pInfo();
445
- if (p2pArgs.includes('--json'))
446
- console.log(formatP2pInfoJson(info));
546
+ if (p2pArgs.includes('--quiet')) {
547
+ console.log(JSON.stringify(JSON.parse(formatP2pInfoJson(info))));
548
+ }
549
+ else if (p2pArgs.includes('--json')) {
550
+ // 2026-09-21 (P3): 保留 bolloon-p2p-info/1 的全部既有字段, 追加 §2 的 code/message/evidence/next_action
551
+ const payload = JSON.parse(formatP2pInfoJson(info));
552
+ console.log(JSON.stringify(legacyJson(payload, {
553
+ code: 'OK',
554
+ message: info.ok ? `本机 P2P 信息 (peerId=${String(info.peerId).slice(0, 16)}…)` : '还没有本机 peerId',
555
+ evidence: info.peerId ? [String(info.peerId)] : [],
556
+ next_action: info.ok ? null : 'rejoin_network',
557
+ }), null, 2));
558
+ }
447
559
  else
448
560
  console.log(formatP2pInfoText(info));
449
561
  // 一次性信息命令必须自己收尾: 读运行中节点会 import network/p2p (libp2p),
@@ -755,8 +867,25 @@ async function main() {
755
867
  break;
756
868
  // 2026-09-18: M1 任务闭环 (bolloon task "<任务>" --budget 0.05 / --resume <goalId>)
757
869
  case 'task':
870
+ // 2026-09-21 (P3): 已知子命令 → 命令组 (统一信封); 其它一律当 M1 任务正文 (既有体验不动)
871
+ if (args[0] && TASK_SUBCOMMANDS.has(args[0]))
872
+ process.exit(await runServiceGroup('task', args));
758
873
  await handleTaskCommand(args);
759
874
  break;
875
+ // 2026-09-21 (P3): 命令组 (network/agent/wallet/payment/trade) —— 统一信封 + 薄包装现有服务
876
+ // 2026-09-22 (P6): 追加 chain (链上能力: 状态/escrow/时间线/索引/交易)
877
+ case 'network':
878
+ case 'agent':
879
+ case 'wallet':
880
+ case 'payment':
881
+ case 'trade':
882
+ case 'chain':
883
+ process.exit(await runServiceGroup(mode, args));
884
+ break;
885
+ // 2026-09-21 (P4): MCP 适配层 (`bolloon mcp serve` = stdio MCP server; tools/resources 只调 P3 服务层)
886
+ case 'mcp':
887
+ process.exit(await runMcpCommand(args));
888
+ break;
760
889
  // 2026-09-13: bolloon setup — 首次运行初始化向导
761
890
  case 'setup':
762
891
  await handleSetupCommand(args);
@@ -151,6 +151,11 @@ async function gatewayCallAgent(opts) {
151
151
  return { success: false, error: r.error, provider: provider.agentId, decision: 'allow' };
152
152
  // 7. Reputation: 记录成功
153
153
  await (0, agent_reputation_js_1.recordServiceOutcome)(provider.agentId, provider.service?.name || 'service', 'success', registry).catch(() => { });
154
+ // 2026-09-18: 委派完成 → 网络脉冲 (只记"发生过一次成功委派", 不记任务内容/对端地址; 失败静默)
155
+ try {
156
+ void Promise.resolve().then(() => __importStar(require('./network-pulse.js'))).then((np) => np.recordNetworkEvent({ type: 'delegation_completed', did: String(opts.toPublicKey || provider.agentId || 'unknown-node') })).catch(() => { });
157
+ }
158
+ catch { /* ignore */ }
154
159
  return { success: true, provider: provider.agentId, decision: 'allow', output: r.output };
155
160
  }
156
161
  catch (e) {