@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,1236 @@
1
+ /**
2
+ * tasks.ts — P3 `bolloon task send|list|status|cancel|retry|result|inbox|accept|reject|run|complete`
3
+ *
4
+ * **今天真能包的** (薄包装, 不重实现):
5
+ * list → `x402/transaction-store.listTransactions()` + `goal-store.listGoals()` + 本机任务台账/收件箱
6
+ * status → 收件箱 / 本机台账 (bolloon-task/1) → `readTransaction` / `findByRequestId` / `task-runner.decideTaskRecovery()`
7
+ * retry → `x402/payment-recovery.planTransactionRecovery()` (**只出计划, 绝不代付款**)
8
+ * result → 已存结果 (task-inbox: 内容哈希/CID + 签名验真) 或 `readTransaction` + `settlement-state.verifyDelivery()`
9
+ * run → `task/task-runner.runTask()` (M1 唯一入口的同一条路径)
10
+ *
11
+ * **2026-09-21 (P3 收尾) 真接出来的 5 条** (契约层 + 现成传输, 不再是 C_NOT_IMPLEMENTED):
12
+ * send → 构造 + **签名** TaskRequest (`taskRequestId` 确定性幂等) → `task-transport` 发到目标节点
13
+ * inbox → `~/.bolloon/tasks/inbox/` 待处理请求 (发送方摘要/capability/截止时间/状态; `dedupeInbox` 去重)
14
+ * accept → 校验 (capability 已知 · 预算够 · policy 允许 · requestId 未重复) + 签名接受 + 落盘 + 回执
15
+ * reject → 记原因 + 签名 + 落盘; **不执行、不付款**
16
+ * (result 与上面同一套落盘; complete/cancel 仍如实报未实现)
17
+ *
18
+ * 红线 (P1 §5): local-dev 永远不许被输出成链上结算; 有支付证据不许重付 (计划里 `mustNotRepay` 原样透出);
19
+ * **本文件任何路径都不发起付款** —— `send/accept/reject` 里没有一行付款调用, 付款只能由持钱包的一方
20
+ * 走同一 requestId 的 x402 幂等路径 (计划透出 `paid:false`)。任务正文只在本机落盘 (私有一层)。
21
+ */
22
+ import * as os from 'os';
23
+ import * as fs from 'fs';
24
+ import { okEnvelope, failEnvelope, notImplemented, envelopeForState, line, title, hint, plain, opt, has, failHuman, publicStatusForTask, publicStatusForTrade, settlementLabel, isTradeSuccessful, } from '../protocol-envelope.js';
25
+ export const TASK_USAGE = `
26
+ ${title('bolloon task')}
27
+ bolloon task "<任务>" --budget 0.05 M1 唯一入口: 买能力 + 执行 + 报告卡 (推荐)
28
+ bolloon task run "<任务>" [--budget 0.05] [--request-id <id>] 同上, 显式走信封输出
29
+ bolloon task list [--json] 本地任务/交易一览 (状态 + 支付事实, 不含任务正文)
30
+ bolloon task status <transactionId|requestId|goalId> [--json]
31
+ bolloon task result <transactionId|requestId|taskId> [--json] 已存结果 (内容哈希/CID + 签名 + 验真状态)
32
+ bolloon task retry <id> [--json] 恢复计划 (planTransactionRecovery; 只出计划, 不代付款)
33
+
34
+ ── bolloon-task/1 真收发 (2026-09-21 P3 收尾: 契约层 + 现成传输) ──
35
+ bolloon task send --capability research --instruction "调研 X" \\
36
+ [--budget 0.05] [--network base-sepolia] [--currency USDC] [--deadline +24h] \\
37
+ [--provider <did>] [--endpoint http://host:port] [--reply-to http://me:port] [--peer <iroh nodeId>] [--via gateway]
38
+ bolloon task inbox [--json] [--all] 收件箱: 待处理请求 (发送方摘要 · capability · 截止时间 · 状态)
39
+ bolloon task accept <requestId|taskId> [--deliver <file>] [--reply-to <url>] [--eta <ms>]
40
+ bolloon task reject <requestId|taskId> --reason "<原因>"
41
+ bolloon task complete|cancel 仍未实现 → 如实报 C_NOT_IMPLEMENTED
42
+
43
+ 选项: --json · --quiet · --request-id <id> · --timeout <ms> · --resume <goalId> (同 bolloon task)
44
+ 说明: 收件箱落盘 ~/.bolloon/tasks/inbox/; 本机发出的任务台账 ~/.bolloon/tasks/local/;
45
+ 结果 ~/.bolloon/tasks/results/, 交付正文 ~/.bolloon/tasks/bodies/ (私有层, 不进 stdout)。
46
+ `;
47
+ /** 交易记录判定 (P1 §5.1 红线: local-dev 永不 verified; 成功判据见 §7.1) */
48
+ function judge(rec) {
49
+ const fact = String(rec.settlementFact || '');
50
+ const status = String(rec.status);
51
+ // 红线: local-dev 的记录出现 verified → 就是红线被破坏, 如实报 (绝不顺着说成功)
52
+ if (rec.paymentMode === 'local-dev' && status === 'verified') {
53
+ return { code: 'LOCAL_DEV_NOT_CHAIN', ok: false, next: null };
54
+ }
55
+ if (isTradeSuccessful(rec))
56
+ return { code: 'TASK_VERIFIED', ok: true, next: null };
57
+ switch (status) {
58
+ case 'verified': return { code: 'RESULT_UNVERIFIED', ok: false, next: 'needs_human' }; // verified 了但支付事实不够链上口径
59
+ case 'delivered': return { code: 'TASK_COMPLETED', ok: true, next: 'verify_result' };
60
+ case 'paying': return { code: 'PAYMENT_PENDING', ok: false, next: 'reconcile' };
61
+ case 'payment_required': return { code: 'PAYMENT_REQUIRED', ok: false, next: 'approve_payment' };
62
+ case 'policy_denied': return { code: 'POLICY_DENIED', ok: false, next: 'needs_human' };
63
+ case 'delivery_failed': return { code: 'DELIVERY_FAILED', ok: false, next: 'needs_human' };
64
+ case 'verification_failed': return { code: 'RESULT_UNVERIFIED', ok: false, next: 'needs_human' };
65
+ case 'disputed': return { code: 'DISPUTE_OPEN', ok: false, next: 'needs_human' };
66
+ default:
67
+ if (fact === 'unknown')
68
+ return { code: 'PAYMENT_UNCERTAIN', ok: false, next: 'reconcile' };
69
+ if (status === 'failed')
70
+ return { code: 'TASK_FAILED', ok: false, next: 'needs_human' };
71
+ return { code: 'OK', ok: true, next: 'wait' }; // discovered/quoted/settled: 还在走
72
+ }
73
+ }
74
+ /** 一条记录 → 对外可见摘要 (camelCase; 不含任务正文/私钥) */
75
+ function recSummary(rec) {
76
+ const j = judge(rec);
77
+ return {
78
+ transactionId: rec.transactionId,
79
+ requestId: rec.requestId,
80
+ goalId: rec.goalId || null,
81
+ runId: rec.runId || null,
82
+ lifecycle: String(rec.status),
83
+ settlementFact: String(rec.settlementFact || ''),
84
+ paymentMode: String(rec.paymentMode),
85
+ chainSettled: rec.chainSettled === true,
86
+ settlement: settlementLabel(rec), // 'chain' | 'local-dev' | 'none' —— local-dev 永不冒充链上
87
+ publicStatus: rec.status === 'verified' || rec.status === 'delivered'
88
+ ? publicStatusForTask(String(rec.status), j.next)
89
+ : publicStatusForTrade(String(rec.status), j.next),
90
+ success: isTradeSuccessful(rec),
91
+ txHash: rec.txHash || null,
92
+ contentHash: rec.contentHash || null,
93
+ deliveryHash: rec.deliveryHash || null,
94
+ receiptHash: rec.receiptHash || null,
95
+ amount: rec.amount || null,
96
+ currency: rec.currency || null,
97
+ network: rec.network || null,
98
+ startedAt: rec.startedAt,
99
+ code: j.code,
100
+ next_action: j.next,
101
+ };
102
+ }
103
+ async function loadRecords() {
104
+ const { listTransactions } = await import('../../agents/x402/transaction-store.js');
105
+ return await listTransactions(os.homedir());
106
+ }
107
+ /** 解析一个 id: transactionId → requestId → goalId (goalId 走 decideTaskRecovery 找交易) */
108
+ async function resolveTarget(id) {
109
+ const { readTransaction, findByRequestId } = await import('../../agents/x402/transaction-store.js');
110
+ const home = os.homedir();
111
+ if (/^tx-/.test(id)) {
112
+ return { rec: await readTransaction(id, home) };
113
+ }
114
+ // 任意 requestId (不一定是 treq- 前缀) 也能直接查到交易
115
+ const byReq = await findByRequestId(id, home);
116
+ if (byReq)
117
+ return { rec: byReq };
118
+ // 其它一律当 goalId (M1 的 goal id 形如 g-xxxx)
119
+ const { decideTaskRecovery } = await import('../../agents/task/task-runner.js');
120
+ const d = await decideTaskRecovery({ goalId: id });
121
+ const rec = d.transactionId ? await readTransaction(d.transactionId, home) : null;
122
+ return { rec, goalId: id, plan: { action: d.action, reason: d.reason, mustNotRepay: d.mustNotRepay, transactionId: d.transactionId } };
123
+ }
124
+ export async function taskCommand(flags) {
125
+ const sub = flags.positionals[0];
126
+ switch (sub) {
127
+ case 'list': return taskList(flags);
128
+ case 'status': return taskStatus(flags);
129
+ case 'result': return taskResult(flags);
130
+ case 'retry': return taskRetry(flags);
131
+ case 'run': return taskRun(flags);
132
+ case 'send': return taskSend(flags);
133
+ case 'inbox': return taskInbox(flags);
134
+ case 'accept': return taskAccept(flags);
135
+ case 'reject': return taskReject(flags);
136
+ case 'complete': return taskComplete(flags);
137
+ case 'cancel': return taskCancel(flags);
138
+ default:
139
+ return {
140
+ envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 task 子命令: ${sub}` : '缺少 task 子命令', { usage: plain(TASK_USAGE.trim()) }, [], 'needs_human'),
141
+ human: TASK_USAGE,
142
+ };
143
+ }
144
+ }
145
+ // ── list ────────────────────────────────────────────────────────────────────
146
+ async function taskList(flags) {
147
+ const all = await loadRecords();
148
+ const filtered = flags.requestId ? all.filter((r) => r.requestId === flags.requestId) : all;
149
+ let goals = [];
150
+ try {
151
+ const { listGoals } = await import('../../agents/goal-store.js');
152
+ const gs = await listGoals({ limit: 20 });
153
+ const ids = new Set(all.map((r) => r.goalId).filter(Boolean));
154
+ // 只给 id 与状态: goal.objective 是任务正文, **不进 stdout** (P1 §5.6)
155
+ goals = gs.map((g) => ({ goalId: g.goalId, status: String(g.status), hasTransaction: ids.has(g.goalId) }));
156
+ }
157
+ catch { /* 没有 goal 目录也能列交易 */ }
158
+ const data = {
159
+ count: filtered.length,
160
+ tasks: filtered.map(recSummary),
161
+ goals,
162
+ source: 'transactions (~/.bolloon/transactions) + goals (~/.bolloon/goals)',
163
+ note: 'bolloon-task/1 的收件箱/P2P 任务帧尚未实现 (task-protocol.md §9 Phase 2); 这里列的是本地有交易记录的任务 + 本地 Goal (只给 id/状态, 不含任务正文)',
164
+ };
165
+ const human = [
166
+ title('bolloon task list'),
167
+ line('交易记录', `${filtered.length} 笔${flags.requestId ? ` (requestId=${flags.requestId})` : ''}`),
168
+ ...filtered.slice(0, 20).map((r) => `\n ${r.transactionId} [${r.status}/${r.settlementFact}] ${settlementLabel(r)} ${isTradeSuccessful(r) ? '✓成功' : ''}\n requestId: ${r.requestId}${r.goalId ? `\n goalId: ${r.goalId}` : ''}`),
169
+ line('本地 Goal', `${goals.length} 个 (只给 id/状态, 不含任务正文)`),
170
+ ...goals.slice(0, 10).map((g) => ` · ${g.goalId} [${g.status}]${g.hasTransaction ? ' (已有交易)' : ''}`),
171
+ `\n ${data.note}`,
172
+ ].join('\n');
173
+ return { envelope: okEnvelope('OK', `本地任务/交易: ${filtered.length} 笔交易, ${goals.length} 个 Goal`, data, filtered.map((r) => r.transactionId), null), human };
174
+ }
175
+ // ── bolloon-task/1 层 (收件箱 / 本机台账 / 结果) ──────────────────────────────
176
+ /** InboxState → 任务 14 态 (只用于对外 4 态投影; 内部一个字不改) */
177
+ const INBOX_TO_TASK = {
178
+ pending: 'submitted',
179
+ accepted: 'accepted',
180
+ rejected: 'rejected',
181
+ delivered: 'delivered',
182
+ failed: 'failed',
183
+ };
184
+ /** 任务层 id 解析: 收件箱 requestId/taskId · 本机台账 requestId · 结果 taskId */
185
+ async function findTaskLayer(id) {
186
+ const { readInboxItem, readLocalTask, findResult, listInbox, listLocalTasks } = await import('../../agents/task-inbox.js');
187
+ const home = os.homedir();
188
+ let inbox = readInboxItem(id, home);
189
+ let local = readLocalTask(id, home);
190
+ if (!inbox && !local) {
191
+ // 也可能是 taskId (task-…): 反查
192
+ inbox = listInbox(home).find((i) => i.taskId === id) || null;
193
+ local = listLocalTasks(home).find((t) => t.taskId === id) || null;
194
+ }
195
+ if (!inbox && !local)
196
+ return null;
197
+ const requestId = local?.requestId || inbox?.requestId || null;
198
+ const found = requestId ? findResult(requestId, home) : findResult(id, home);
199
+ return { inbox, local, result: found?.result || null, requestId };
200
+ }
201
+ /** `task status` 的任务层分支 (交易层记录若存在 → 只读衔接一起显示, 绝不改交易) */
202
+ async function taskLayerStatus(id) {
203
+ const l = await findTaskLayer(id);
204
+ if (!l)
205
+ return null;
206
+ const { inboxSummary } = await import('../../agents/task-inbox.js');
207
+ const { findByRequestId } = await import('../../agents/x402/transaction-store.js');
208
+ const home = os.homedir();
209
+ const { inbox, local, result, requestId } = l;
210
+ const rid = String(requestId || id);
211
+ // ★ 交易层衔接: 同一 requestId 若已有交易, 只**读**出来一起显示 (两层不混, 也不绕过)
212
+ const tx = await findByRequestId(rid, home);
213
+ const txView = tx ? recSummary(tx) : null;
214
+ const inboxState = inbox?.state ?? null;
215
+ const taskState = local?.state ?? (inboxState ? INBOX_TO_TASK[inboxState] : null);
216
+ const next = taskState === 'delivered' ? 'verify_result'
217
+ : taskState === 'rejected' ? null
218
+ : inboxState === 'pending' && !local ? 'needs_human' // 收到待处理请求 → 要人来决定接不接
219
+ : taskState === 'submitted' ? null
220
+ : taskState === 'accepted' ? 'wait'
221
+ : 'needs_human';
222
+ const code = taskState === 'verified' ? 'TASK_VERIFIED'
223
+ : taskState === 'delivered' ? 'TASK_COMPLETED'
224
+ : taskState === 'rejected' ? 'TASK_REJECTED'
225
+ : taskState === 'failed' ? 'TASK_FAILED'
226
+ : taskState === 'accepted' ? 'TASK_ACCEPTED'
227
+ : 'TASK_SUBMITTED';
228
+ const publicStatus = publicStatusForTask(String(taskState || 'submitted'), next);
229
+ const rv = l.result?.contentHash || result?.contentHash || null;
230
+ const data = {
231
+ layer: local && inbox ? 'both' : local ? 'local' : 'inbox',
232
+ requestId: rid,
233
+ taskId: local?.taskId || inbox?.taskId || null,
234
+ capability: local?.capability || inbox?.capability || null,
235
+ state: taskState,
236
+ inboxState,
237
+ publicStatus,
238
+ direction: local && inbox ? 'both' : local ? 'sent' : 'received',
239
+ buyerDid: local?.buyerDid || inbox?.buyerDid || null,
240
+ providerDid: local?.providerDid || inbox?.providerDid || null,
241
+ paymentMode: local?.paymentMode || inbox?.paymentMode || null,
242
+ deadline: inbox?.deadline ?? null,
243
+ deadlineInMs: inbox?.deadline ? inbox.deadline - Date.now() : null,
244
+ budget: inbox?.budget ?? null,
245
+ requestSignatureVerified: local ? true : (inbox?.requestSignatureVerified ?? null),
246
+ transport: local ? { kind: local.transportKind, target: local.target } : null,
247
+ sentAt: local?.sentAt ?? null,
248
+ receivedAt: inbox?.receivedAt ?? null,
249
+ receipt: local?.receipt ?? null,
250
+ accept: local?.accept || inbox?.accept ? { providerDid: (local?.accept || inbox?.accept)?.providerDid, etaMs: (local?.accept || inbox?.accept)?.etaMs ?? null, signed: true } : null,
251
+ reject: local?.reject || inbox?.reject ? { reason: (local?.reject || inbox?.reject)?.reason } : null,
252
+ result: (local?.result || inbox?.result) ? {
253
+ ok: (local?.result || inbox?.result)?.ok,
254
+ summary: (local?.result || inbox?.result)?.summary,
255
+ contentHash: rv,
256
+ cid: (local?.result || inbox?.result)?.cid ?? null,
257
+ deliveredAt: (local?.result || inbox?.result)?.deliveredAt,
258
+ signed: !!(local?.result || inbox?.result)?.signature,
259
+ signatureVerified: local?.resultVerified ?? inbox?.resultVerified ?? null,
260
+ } : null,
261
+ transaction: txView,
262
+ origin: inbox ? inboxSummary(inbox) : null,
263
+ /** 本命令**没有**发起付款 (任务层命令从不付款; 付款只走持钱包一方的 x402 幂等路径) */
264
+ paid: false,
265
+ transactionLinked: !!tx,
266
+ taskBodyFile: local?.taskId || inbox?.taskId ? `~/.bolloon/tasks/bodies/${local?.taskId || inbox?.taskId}.txt` : null,
267
+ note: '任务层 (bolloon-task/1) 状态与交易层 (x402) 严格分离: 这里只读交易事实, 不改也不绕',
268
+ };
269
+ const human = [
270
+ title(`bolloon task status ${rid}`),
271
+ line('层', data.layer === 'inbox' ? '收件箱 (收到的请求)' : data.layer === 'local' ? '本机台账 (发出的任务)' : '收件箱 + 本机台账'),
272
+ line('任务状态', `${taskState} (对外: ${publicStatus})${inboxState ? ` · 收件箱态 ${inboxState}` : ''}`),
273
+ line('capability', String(data.capability || '(无)')),
274
+ line('发送方', String(data.buyerDid || '(无)').slice(0, 40)),
275
+ ...(data.deadline ? [line('截止时间', `${new Date(Number(data.deadline)).toISOString()} (${Number(data.deadlineInMs) > 0 ? `还剩 ${Math.round(Number(data.deadlineInMs) / 1000)}s` : '已过期'})`)] : []),
276
+ ...(data.transport ? [line('传输', `${data.transport.kind} → ${data.transport.target}`)] : []),
277
+ ...(data.receipt ? [line('发送回执', data.receipt.ok ? '对端收下了' : `失败: ${data.receipt.error || '未知'}`)] : []),
278
+ ...(data.accept ? [line('接受回执', '已签名 (TaskAccept)')] : []),
279
+ ...(data.reject ? [line('拒绝原因', String(data.reject.reason))] : []),
280
+ ...(data.result ? [line('结果', `${data.result.ok ? 'ok' : 'not-ok'} · contentHash ${String(data.result.contentHash || '').slice(0, 12)}… · 验签 ${data.result.signatureVerified === true ? '通过' : data.result.signatureVerified === false ? '不过' : '未验'}`)] : []),
281
+ line('交易层', txView ? `${txView.transactionId} [${txView.lifecycle}/${txView.settlementFact}] (${txView.settlement})` : '没有同一 requestId 的交易 (本机没为它付过款)'),
282
+ line('本次付款', '没有 (任务层命令不付款)'),
283
+ `\n ${String(data.note)}`,
284
+ ].join('\n');
285
+ return {
286
+ envelope: okEnvelope(code, `任务 ${rid}: ${taskState} (${publicStatus})`, data, [rid, ...(data.taskId ? [String(data.taskId)] : []), ...(tv(tx) || [])], next),
287
+ human,
288
+ };
289
+ }
290
+ function tv(rec) {
291
+ return rec ? rec.transactionId : null;
292
+ }
293
+ // ── status ──────────────────────────────────────────────────────────────────
294
+ async function taskStatus(flags) {
295
+ const id = flags.positionals[1] || flags.requestId;
296
+ if (!id) {
297
+ return {
298
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 id (transactionId / requestId / goalId) 或 --request-id', { usage: plain(TASK_USAGE.trim()) }, [], 'needs_human'),
299
+ human: TASK_USAGE,
300
+ };
301
+ }
302
+ const { rec, goalId, plan } = await resolveTarget(id);
303
+ if (!rec) {
304
+ // ★ 先看任务层 (bolloon-task/1): 收件箱 / 本机台账 —— 真收到的请求与真发出的任务在这里
305
+ const taskLayer = await taskLayerStatus(id);
306
+ if (taskLayer)
307
+ return taskLayer;
308
+ if (goalId && plan) {
309
+ // goalId 路径: 走 decideTaskRecovery (Supervisor 与 CLI --resume 共用的唯一决策入口)
310
+ const data = {
311
+ goalId, hasTransaction: false, recoveryAction: plan.action, mustNotRepay: plan.mustNotRepay,
312
+ reason: plan.reason, decisionSource: 'task-runner.decideTaskRecovery',
313
+ };
314
+ if (plan.action === 'closed') {
315
+ // 目标压根不存在 (decideTaskRecovery 对找不到的 Goal 给 closed)
316
+ return {
317
+ envelope: failEnvelope('NOT_FOUND', `找不到这个 Goal: ${goalId}`, data, [goalId], 'needs_human'),
318
+ human: `${title(`bolloon task status ${id}`)}\n 找不到这个 Goal\n${line('原因', plan.reason)}`,
319
+ };
320
+ }
321
+ if (plan.action === 'retry_payment') {
322
+ // 目标在, 但没有任何交易 → 还没付过钱 (真的没花钱, 不是失败)
323
+ return {
324
+ envelope: okEnvelope('OK', `Goal ${goalId} 还没有过交易 (没付过钱): ${plan.reason}`, data, [goalId], 'retry_same_request'),
325
+ human: `${title(`bolloon task status ${id}`)}\n${line('goalId', goalId)}\n${line('交易', '还没有 (没付过钱)')}\n${line('恢复动作', plan.action)}\n${line('mustNotRepay', plan.mustNotRepay ? 'true' : 'false')}\n\n${hint('下一步: bolloon task --resume ' + goalId + ' (从头跑, 没有钱可重复付)')}`,
326
+ };
327
+ }
328
+ return {
329
+ envelope: failEnvelope('RESULT_UNVERIFIED', `Goal ${goalId} 不能自动继续: ${plan.reason}`, data, [goalId], 'needs_human'),
330
+ human: failHuman(`bolloon task status ${id}`, { code: 'RESULT_UNVERIFIED', message: plan.reason, next_action: 'needs_human' }),
331
+ };
332
+ }
333
+ return {
334
+ envelope: failEnvelope('NOT_FOUND', `找不到这个 id 对应的任务/交易: ${id}`, { id }, [], 'retry_same_request'),
335
+ human: `${title(`bolloon task status ${id}`)}\n 没找到\n\n${hint('下一步: bolloon task list 看本地有哪些交易/目标')}`,
336
+ };
337
+ }
338
+ const s = recSummary(rec);
339
+ const j = judge(rec);
340
+ const human = [
341
+ title(`bolloon task status ${rec.transactionId}`),
342
+ line('对外状态', s.publicStatus),
343
+ line('生命周期', s.lifecycle),
344
+ line('结算事实', s.settlementFact),
345
+ line('结算口径', s.settlement + (s.settlement === 'local-dev' ? ' (链上没动钱: 协议闭环通过, 不是支付成功)' : '')),
346
+ line('链上结算', s.chainSettled ? '是' : '否'),
347
+ line('算不算成功', s.success ? '算 (verified ∧ 链上口径事实)' : '不算'),
348
+ line('requestId', s.requestId),
349
+ ...(s.goalId ? [line('goalId', s.goalId)] : []),
350
+ ...(s.txHash ? [line('txHash', s.txHash)] : []),
351
+ line('事件', `${rec.events?.length || 0} 条 (bolloon trade events ${rec.transactionId})`),
352
+ ...(goalId && plan ? [line('恢复动作', `${plan.action} — ${plan.reason}`)] : []),
353
+ ].join('\n');
354
+ return {
355
+ envelope: envelopeForState(j.ok, j.code, `任务 ${rec.transactionId}: ${s.lifecycle}/${s.settlementFact} (${s.settlement})`, { ...s, goalRecovery: goalId && plan ? plan : null }, [rec.transactionId, rec.requestId, ...(rec.txHash ? [rec.txHash] : [])], j.next),
356
+ human,
357
+ };
358
+ }
359
+ // ── result (任务层: 已存结果 + 真验真) ───────────────────────────────────────
360
+ /**
361
+ * 任务层结果: 把**已收到/已存**的 TaskResult 读出来 —— 内容哈希/CID + 签名 + 验真状态。
362
+ * 真验真 = 用**存下来的提供方公钥**现场再验一次签名, 并核对正文哈希; 验不过就如实说不过。
363
+ */
364
+ async function taskLayerResultEnvelope(id) {
365
+ const { findResult, readBody, readInboxItem, readLocalTask, listInbox, listLocalTasks } = await import('../../agents/task-inbox.js');
366
+ const { verifyTaskEnvelope } = await import('../../agents/task-contract.js');
367
+ const { verifierFor } = await import('../../agents/local-signer.js');
368
+ const home = os.homedir();
369
+ const found = findResult(id, home);
370
+ if (!found)
371
+ return null;
372
+ const { result } = found;
373
+ const inbox = readInboxItem(result.requestId, home) || listInbox(home).find((i) => i.taskId === result.taskId) || null;
374
+ const local = readLocalTask(result.requestId, home) || listLocalTasks(home).find((t) => t.taskId === result.taskId) || null;
375
+ const providerKey = local?.providerPublicKeyHex || inbox?.providerPublicKeyHex || null;
376
+ // ★ 现场重验签名 (存下来的公钥就是当时那个签名者的公钥)
377
+ const v = providerKey ? verifierFor(providerKey) : null;
378
+ const signatureVerified = v ? await verifyTaskEnvelope(result, v) : null;
379
+ const body = readBody(result.taskId, home);
380
+ const trust = signatureVerified === true ? (body.matches === false ? 'signature-ok/body-mismatch' : 'protocol-verified') : signatureVerified === false ? 'signature-invalid' : 'unverified';
381
+ const data = {
382
+ layer: 'task',
383
+ requestId: result.requestId,
384
+ taskId: result.taskId,
385
+ capability: local?.capability || inbox?.capability || null,
386
+ providerDid: local?.providerDid || inbox?.providerDid || null,
387
+ state: local?.state || inbox?.state || 'delivered',
388
+ result: {
389
+ ok: result.ok === true,
390
+ summary: result.summary,
391
+ contentHash: result.contentHash || null,
392
+ cid: result.cid || null,
393
+ deliveredAt: result.deliveredAt,
394
+ signed: !!result.signature,
395
+ signerPublicKeyTrust: providerKey ? 'stored-with-request (收请求/回执时一并存下)' : 'not-on-file (没存到签名者公钥, 无法现场重验)',
396
+ },
397
+ verification: {
398
+ signatureVerified, // true / false / null(没有公钥可验)
399
+ trust, // 分档, 绝不把"没验"说成"验过"
400
+ bodyPresent: body.present,
401
+ bodyBytes: body.present ? body.bytes : null,
402
+ bodyHashMatches: body.matches,
403
+ chainSettled: false,
404
+ success: false, // ★ §5.2: 没有链上结算就永远不是"成功"
405
+ why: '任务层结果只到 delivered: 成功判据是 state=verified ∧ 支付事实 ∈ {fully_settled, payment_verified} (P1 §5.2)',
406
+ },
407
+ bodyPrinted: false,
408
+ bodyFile: body.file,
409
+ paid: false,
410
+ note: '交付正文留在本机私有层 (绝不进 stdout/公开投影); 结果签名只证明"提供方交付了这份内容哈希"',
411
+ };
412
+ const human = [
413
+ title(`bolloon task result ${result.requestId}`),
414
+ line('任务号', result.taskId),
415
+ line('结果', `${result.ok ? 'ok' : 'not-ok'} — ${String(result.summary || '').slice(0, 80)}`),
416
+ line('内容哈希', result.contentHash ? `${result.contentHash.slice(0, 16)}…` : '(无)'),
417
+ ...(result.cid ? [line('CID', result.cid)] : []),
418
+ line('签名', result.signature ? (signatureVerified === true ? '有, 现场重验通过' : signatureVerified === false ? '有, 但**验不过**' : '有, 但没有公钥可验 (如实)') : '没有'),
419
+ line('验真分档', trust),
420
+ line('正文在盘', body.present ? `是 (${body.bytes} 字节, 哈希匹配=${body.matches === null ? '无从比对' : body.matches})` : `否 — ${body.reason || '不在盘上'}`),
421
+ line('正文文件', body.file),
422
+ line('正文输出', '未输出 (私有层)'),
423
+ line('算不算成功', '不算 (没有链上结算; 任务层最高 delivered)'),
424
+ ].join('\n');
425
+ return {
426
+ envelope: okEnvelope('TASK_COMPLETED', `任务 ${result.requestId} 的结果 (已交付; 正文未输出)`, data, [result.requestId, result.taskId, ...(result.contentHash ? [result.contentHash] : []), ...(result.cid ? [result.cid] : [])], 'verify_result'),
427
+ human,
428
+ };
429
+ }
430
+ // ── result ──────────────────────────────────────────────────────────────────
431
+ async function taskResult(flags) {
432
+ const id = flags.positionals[1] || flags.requestId;
433
+ if (!id) {
434
+ return {
435
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 transactionId (或 --request-id)', { usage: plain(TASK_USAGE.trim()) }, [], 'needs_human'),
436
+ human: TASK_USAGE,
437
+ };
438
+ }
439
+ // ★ 先看任务层 (bolloon-task/1): 已存的 TaskResult (内容哈希/CID + 签名 + 现场重验)
440
+ const taskLayer = await taskLayerResultEnvelope(id);
441
+ if (taskLayer)
442
+ return taskLayer;
443
+ const { rec } = await resolveTarget(id);
444
+ if (!rec) {
445
+ return {
446
+ envelope: failEnvelope('NOT_FOUND', `找不到交易或任务结果: ${id}`, { id }, [], 'retry_same_request'),
447
+ human: `${title(`bolloon task result ${id}`)}\n 没找到这笔交易, 也没有已存的任务结果`,
448
+ };
449
+ }
450
+ const { verifyDelivery } = await import('../../agents/x402/settlement-state.js');
451
+ const dv = verifyDelivery(rec, os.homedir());
452
+ const s = recSummary(rec);
453
+ const j = judge(rec);
454
+ // ★ 交付正文**不进 stdout/日志** (P1 §5.6 私有一层): 只给字节数/哈希/在盘证据
455
+ const data = {
456
+ transactionId: rec.transactionId,
457
+ requestId: rec.requestId,
458
+ lifecycle: s.lifecycle,
459
+ settlementFact: s.settlementFact,
460
+ settlement: s.settlement,
461
+ success: s.success,
462
+ verificationTrust: rec.verificationTrust || 'unverified',
463
+ protocolVerified: rec.protocolVerified === true,
464
+ delivery: {
465
+ present: dv.present,
466
+ bytes: dv.bytes ?? null,
467
+ bytesHash: dv.bytesHash || null,
468
+ matchesRecorded: dv.matchesRecorded,
469
+ reason: dv.reason || null,
470
+ contentHash: rec.contentHash || null,
471
+ deliveryHash: rec.deliveryHash || null,
472
+ receiptHash: rec.receiptHash || null,
473
+ storedAs: `~/.bolloon/x402/deliveries/${rec.transactionId}.txt`,
474
+ bodyPrinted: false,
475
+ },
476
+ note: '正文留在本机交付目录 (公开层/日志永不输出正文); 需要正文请在本机直接读该文件',
477
+ };
478
+ const human = [
479
+ title(`bolloon task result ${rec.transactionId}`),
480
+ line('对外状态', s.publicStatus),
481
+ line('结算口径', s.settlement),
482
+ line('算不算成功', s.success ? '算' : '不算'),
483
+ line('验真分档', data.verificationTrust),
484
+ line('正文在盘', dv.present ? `是 (${dv.bytes ?? '?'} 字节, 哈希匹配=${dv.matchesRecorded})` : `否${dv.reason ? ` — ${dv.reason}` : ''}`),
485
+ line('正文文件', data.delivery.storedAs),
486
+ line('正文输出', '未输出 (私有层: 绝不进 stdout/日志)'),
487
+ `\n ${data.note}`,
488
+ ].join('\n');
489
+ return {
490
+ envelope: envelopeForState(j.ok, j.code, `任务 ${rec.transactionId} 的交付证据 (正文未输出)`, data, [rec.transactionId, ...(dv.bytesHash ? [dv.bytesHash] : []), ...(rec.contentHash ? [rec.contentHash] : [])], j.next),
491
+ human,
492
+ };
493
+ }
494
+ // ── retry (只出计划, 绝不代付款) ─────────────────────────────────────────────
495
+ const PLAN_NEXT = {
496
+ retry_payment: 'approve_payment',
497
+ reconcile: 'reconcile',
498
+ deliver: 'needs_human',
499
+ verify: 'needs_human',
500
+ closed: 'needs_human',
501
+ wait: 'wait',
502
+ complete: null,
503
+ };
504
+ async function taskRetry(flags) {
505
+ const id = flags.positionals[1] || flags.requestId;
506
+ if (!id) {
507
+ return {
508
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 transactionId / requestId / goalId', { usage: plain(TASK_USAGE.trim()) }, [], 'needs_human'),
509
+ human: TASK_USAGE,
510
+ };
511
+ }
512
+ const { rec, goalId, plan } = await resolveTarget(id);
513
+ if (!rec) {
514
+ if (goalId && plan) {
515
+ // goalId 路径: 走 decideTaskRecovery (Supervisor 与 CLI --resume 共用的唯一决策入口)
516
+ const next = plan.action === 'retry_payment'
517
+ ? (plan.transactionId ? `x402_payment_retry:${plan.transactionId}` : 'retry_same_request')
518
+ : plan.action === 'already_executed' ? 'needs_human' : (PLAN_NEXT[plan.action] ?? 'needs_human');
519
+ const data = {
520
+ goalId, action: plan.action, reason: plan.reason, mustNotRepay: plan.mustNotRepay,
521
+ transactionId: plan.transactionId ?? null, hasTransaction: !!plan.transactionId,
522
+ decisionSource: 'task-runner.decideTaskRecovery (CLI --resume 与 Supervisor 同一入口)',
523
+ paid: false,
524
+ };
525
+ return {
526
+ envelope: envelopeForState(plan.action === 'complete', 'RECOVERY_PLANNED', `恢复计划: ${plan.action}${plan.mustNotRepay ? ' (绝不重付)' : ''}`, data, plan.transactionId ? [plan.transactionId, goalId] : [goalId], next),
527
+ human: [
528
+ title(`bolloon task retry ${goalId}`),
529
+ line('动作', plan.action),
530
+ line('原因', plan.reason),
531
+ line('绝不重付', plan.mustNotRepay ? 'true (已有支付证据/不确定 → 先对账)' : 'false (确认没付过, 可从同一 requestId 幂等重发)'),
532
+ line('交易', plan.transactionId || '(还没有)'),
533
+ `\n ${hint('这条命令只出计划, 不代付款 (Supervisor/CLI 都不持有钱包)。要继续: bolloon task --resume ' + goalId)}`,
534
+ ].join('\n'),
535
+ };
536
+ }
537
+ return {
538
+ envelope: failEnvelope('NOT_FOUND', `找不到这个 id: ${id}`, { id }, [], 'retry_same_request'),
539
+ human: `${title(`bolloon task retry ${id}`)}\n 没找到`,
540
+ };
541
+ }
542
+ // 交易路径: 唯一决策点 planTransactionRecovery (绝不自己 if-else, 也绝不绕过去付款)
543
+ const { planTransactionRecovery } = await import('../../agents/x402/payment-recovery.js');
544
+ const p = planTransactionRecovery(rec);
545
+ const next = PLAN_NEXT[p.action] === 'approve_payment' && p.mustNotRepay
546
+ ? `x402_payment_retry:${rec.transactionId}`
547
+ : (p.action === 'deliver' || p.action === 'verify' ? `x402_continue:${rec.transactionId}` : PLAN_NEXT[p.action]);
548
+ const data = {
549
+ transactionId: rec.transactionId,
550
+ requestId: rec.requestId,
551
+ action: p.action,
552
+ settlementFact: p.settlementFact,
553
+ mustNotRepay: p.mustNotRepay,
554
+ needsResponsibility: p.needsResponsibility,
555
+ reason: p.reason,
556
+ lifecycle: String(rec.status),
557
+ settlement: settlementLabel(rec),
558
+ decisionSource: 'x402/payment-recovery.planTransactionRecovery',
559
+ paid: false,
560
+ paidNote: 'CLI 只出计划, 不发付款: 付款必须由**持钱包的一方**走同一 requestId 的幂等路径',
561
+ };
562
+ const human = [
563
+ title(`bolloon task retry ${rec.transactionId}`),
564
+ line('动作', p.action),
565
+ line('原因', p.reason),
566
+ line('结算事实', p.settlementFact),
567
+ line('绝不重付', p.mustNotRepay ? 'true' : 'false'),
568
+ line('结算口径', settlementLabel(rec)),
569
+ line('本次付款', '没有 (只出计划)'),
570
+ `\n ${hint('下一步: ' + (p.action === 'reconcile' ? '先对账 (trade reconcile) —— 付款不确定绝不当失败' : String(next)))}`,
571
+ ].join('\n');
572
+ return {
573
+ envelope: envelopeForState(p.action === 'complete', 'RECOVERY_PLANNED', `恢复计划: ${p.action}${p.mustNotRepay ? ' (绝不重付)' : ''}`, data, [rec.transactionId, rep(rec)], next),
574
+ human,
575
+ };
576
+ }
577
+ function rep(rec) { return rec.requestId; }
578
+ // ── run (M1 唯一入口的同一条路径) ────────────────────────────────────────────
579
+ async function taskRun(flags) {
580
+ const { runTask } = await import('../../agents/task/task-runner.js');
581
+ const task = flags.positionals.slice(1).join(' ').trim();
582
+ if (!task) {
583
+ return {
584
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少任务正文 (bolloon task run "<任务>" --budget 0.05)', { usage: plain(TASK_USAGE.trim()) }, [], 'needs_human'),
585
+ human: TASK_USAGE,
586
+ };
587
+ }
588
+ const r = await runTask({
589
+ task,
590
+ budget: opt(flags, '--budget'),
591
+ perPurchase: opt(flags, '--per-purchase'),
592
+ daily: opt(flags, '--daily'),
593
+ requestId: flags.requestId,
594
+ input: parseInput(opt(flags, '--input')),
595
+ allowLocalDev: true,
596
+ });
597
+ const chainSettled = r.payment?.chainSettled === true;
598
+ const settlement = r.payment
599
+ ? (chainSettled ? 'chain' : (r.payment.mode === 'local-dev' ? 'local-dev' : 'none'))
600
+ : 'none';
601
+ // ★ local-dev 最高到 delivered: 不许输出成 TASK_VERIFIED (P1 §5.1)
602
+ const code = r.ok ? (chainSettled ? 'TASK_VERIFIED' : 'TASK_COMPLETED') : (r.card.hardGate === 'bought_not_executed' ? 'DELIVERY_FAILED' : 'RESULT_UNVERIFIED');
603
+ const next = r.ok ? (chainSettled ? null : 'verify_result') : 'needs_human';
604
+ const data = {
605
+ goalId: r.goalId || null,
606
+ runId: r.runId || null,
607
+ transactionId: r.transactionId || null,
608
+ publicStatus: r.card.status,
609
+ conclusion: r.card.conclusion,
610
+ executed: r.card.executed,
611
+ paid: r.card.paid,
612
+ checks: r.card.checks,
613
+ hardGate: r.card.hardGate || null,
614
+ payment: r.payment || null,
615
+ settlement,
616
+ success: !!(r.ok && chainSettled),
617
+ budget: { taskBudget: r.budget.taskBudget, perPurchase: r.budget.perPurchase, daily: r.budget.daily, clamped: r.budget.clamped },
618
+ outputIssues: r.outputIssues,
619
+ stages: r.stages,
620
+ note: settlement === 'chain'
621
+ ? undefined
622
+ : settlement === 'local-dev'
623
+ ? '本机联调 (local-dev): 协议闭环通过, 链上没动钱 —— 不是"支付成功", 也不算任务成功'
624
+ : '这次没有发生付款 (settlement=none): 没有链上也没有联调结算, 也没有花钱',
625
+ };
626
+ const human = [`${title('bolloon task run')}`, r.text, '', line('对外状态', r.card.status), line('结算口径', settlement)].join('\n');
627
+ const env = r.ok
628
+ ? okEnvelope(code, `任务${chainSettled ? '已验真' : '已交付 (local-dev, 链上未结算)'}`, data, [r.goalId, r.runId, r.transactionId].filter(Boolean), next)
629
+ : failEnvelope(code, `任务没走完: ${r.card.blocker || r.card.conclusion}`, data, [r.goalId, r.runId, r.transactionId].filter(Boolean), next);
630
+ return { envelope: env, human };
631
+ }
632
+ function parseInput(raw) {
633
+ if (raw === undefined)
634
+ return undefined;
635
+ try {
636
+ return JSON.parse(raw);
637
+ }
638
+ catch {
639
+ return undefined;
640
+ }
641
+ }
642
+ // ── bolloon-task/1 真收发: send / inbox / accept / reject ────────────────────
643
+ /** 人类单位 → 原子单位正整数串 (拒绝 0 / 负 / 超精度; 浮点模糊一律拒) */
644
+ function toAtomic(human, decimals) {
645
+ const s = String(human ?? '').trim();
646
+ if (!/^[0-9]+(\.[0-9]+)?$/.test(s))
647
+ return null;
648
+ const [i, f = ''] = s.split('.');
649
+ if (f.length > decimals)
650
+ return null;
651
+ const n = BigInt(i + (f + '0'.repeat(decimals)).slice(0, decimals));
652
+ return n > 0n ? n.toString() : null;
653
+ }
654
+ /** `--deadline +2h` / `+30m` / `+45s` / `+1d`, 或未来毫秒时间戳 */
655
+ function parseDeadline(v, now = Date.now()) {
656
+ if (v === undefined)
657
+ return now + 24 * 60 * 60 * 1000;
658
+ const m = /^\+(\d+)(s|m|h|d)$/.exec(String(v).trim());
659
+ if (m) {
660
+ const n = Number(m[1]);
661
+ const mult = m[2] === 's' ? 1000 : m[2] === 'm' ? 60_000 : m[2] === 'h' ? 3_600_000 : 86_400_000;
662
+ return now + n * mult;
663
+ }
664
+ const n = Number(v);
665
+ if (Number.isFinite(n) && n > now)
666
+ return Math.floor(n);
667
+ return `--deadline 不合法: ${v} (要 +30m/+2h/+1d, 或未来的毫秒时间戳)`;
668
+ }
669
+ /** 本机身份 (签名用)。没有 → 结构化失败 (绝不假签名)。 */
670
+ async function requireSigner(head) {
671
+ const { loadLocalSigner } = await import('../../agents/local-signer.js');
672
+ const signer = await loadLocalSigner();
673
+ if (!signer) {
674
+ const msg = '本机没有可签名的身份 (~/.bolloon/identity.json 不存在或不可读)';
675
+ return {
676
+ fail: {
677
+ envelope: failEnvelope('WALLET_UNAVAILABLE', msg, { identityFile: '~/.bolloon/identity.json', privateKeyPrinted: false }, [], 'needs_human'),
678
+ human: `${title(head)}\n ${msg}\n\n${hint('下一步: bolloon setup 生成本机身份')}`,
679
+ },
680
+ };
681
+ }
682
+ return { signer };
683
+ }
684
+ /**
685
+ * 本机对某 capability 的"已知"事实 = 进程内 manifest + 本地注册表 (`~/.bolloon/agent-registry.json`)。
686
+ * 两处都没有 → 未知 (接单会被拒; 绝不假装有能力)。价格取本机为该能力登记的价。
687
+ */
688
+ async function localCapabilityFacts(capability) {
689
+ const sources = [];
690
+ let manifestCaps = [];
691
+ try {
692
+ const { getLocalManifest } = await import('../../agents/agent-manifest-protocol.js');
693
+ manifestCaps = (getLocalManifest().agents || []).flatMap((a) => a.capabilities || []);
694
+ }
695
+ catch { /* 没有 manifest 不影响 */ }
696
+ if (manifestCaps.includes(capability))
697
+ sources.push('local-manifest');
698
+ let services = [];
699
+ try {
700
+ const { getAgentRegistry } = await import('../../agents/agent-registry.js');
701
+ services = await getAgentRegistry().list();
702
+ }
703
+ catch {
704
+ services = [];
705
+ }
706
+ const hits = (services || []).filter((s) => (s?.capabilities || []).includes(capability) || s?.service?.name === capability);
707
+ if (hits.length)
708
+ sources.push(`registry(${hits.length})`);
709
+ const hit = hits.find((s) => s?.service?.price?.amount) || hits[0];
710
+ return {
711
+ known: sources.length > 0,
712
+ sources,
713
+ priceAmount: hit?.service?.price?.amount ?? null,
714
+ currency: hit?.service?.price?.currency ?? null,
715
+ wallet: hit?.wallet ?? null,
716
+ services: services.length,
717
+ };
718
+ }
719
+ /** 把回执帧发回发送方 (有 replyTo 才发; 失败如实记, 不假装送达) */
720
+ async function sendReplyFrames(replyTo, frames, timeoutMs) {
721
+ if (!replyTo) {
722
+ return { attempted: false, ok: false, target: null, error: '发送方没有留下回执端点 (--reply-to)', results: [] };
723
+ }
724
+ const { httpTaskTransport } = await import('../../agents/task-transport.js');
725
+ const t = httpTaskTransport();
726
+ const results = [];
727
+ for (const f of frames) {
728
+ const r = await t.send(replyTo, f, timeoutMs);
729
+ results.push({ frame: String(f.frame), ok: r.ok, error: r.error, elapsedMs: r.elapsedMs });
730
+ }
731
+ return { attempted: true, ok: results.every((r) => r.ok), target: replyTo, results, ...(results.every((r) => r.ok) ? {} : { error: '部分/全部回执没送达' }) };
732
+ }
733
+ /** `--json` 里出现的中转摘要 (不打印私钥/正文全文) */
734
+ function transferView(t) {
735
+ return t ? { kind: t.kind, target: t.target, ok: t.ok, elapsedMs: t.elapsedMs, duplicate: t.duplicate === true, error: t.error || null, detail: t.detail || null } : null;
736
+ }
737
+ // ── send ────────────────────────────────────────────────────────────────────
738
+ async function taskSend(flags) {
739
+ const instruction = opt(flags, '--instruction') || flags.positionals.slice(1).join(' ').trim();
740
+ const capability = opt(flags, '--capability');
741
+ if (!instruction || !capability) {
742
+ return {
743
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少任务正文或 --capability', { usage: plain(TASK_USAGE.trim()), got: { instruction: !!instruction, capability: capability || null } }, [], 'needs_human'),
744
+ human: `${TASK_USAGE}\n${hint('示例: bolloon task send --capability research --instruction "调研日本厨房用品市场" --endpoint http://127.0.0.1:54901')}`,
745
+ };
746
+ }
747
+ const s = await requireSigner('bolloon task send');
748
+ if ('fail' in s)
749
+ return s.fail;
750
+ const signer = s.signer;
751
+ // 预算 / 币种 / 网络 (字段名与 TaskBudget 一致)
752
+ const budgetHuman = opt(flags, '--budget');
753
+ const currency = String(opt(flags, '--currency') || 'USDC').toUpperCase();
754
+ if (currency !== 'USDC' && currency !== 'ETH') {
755
+ return { envelope: failEnvelope('INVALID_ARGUMENT', `--currency 只支持 USDC/ETH (收到 ${currency})`, { currency }, [], 'needs_human'), human: TASK_USAGE };
756
+ }
757
+ const network = opt(flags, '--network') || 'base-sepolia';
758
+ let budget;
759
+ if (budgetHuman !== undefined) {
760
+ const atomic = toAtomic(budgetHuman, currency === 'USDC' ? 6 : 18);
761
+ if (!atomic) {
762
+ return {
763
+ envelope: failEnvelope('INVALID_ARGUMENT', `--budget 必须是正数 (收到 ${budgetHuman}); 原子单位串由本命令换算, 不接受 0/负/超精度`, { budget: budgetHuman, currency, accepted: ['--budget 0.05'] }, [], 'needs_human'),
764
+ human: TASK_USAGE,
765
+ };
766
+ }
767
+ budget = { maxAmount: atomic, currency, network };
768
+ }
769
+ // 支付模式: 缺省 policy (需要人工/策略放行); 本命令**不付款**, 模式只写进信封给对端与后续付款方看
770
+ const modeRaw = opt(flags, '--mode') || 'policy';
771
+ const { isPaymentMode, TASK_PROTOCOL, taskRequestId, newTaskId, validateTaskRequest, signTaskEnvelope } = await import('../../agents/task-contract.js');
772
+ if (!isPaymentMode(modeRaw)) {
773
+ return {
774
+ envelope: failEnvelope('INVALID_ARGUMENT', `--mode 非法: ${modeRaw} (要 manual|policy|autonomous|agent-authorized)`, { mode: modeRaw }, [], 'needs_human'),
775
+ human: TASK_USAGE,
776
+ };
777
+ }
778
+ const dl = parseDeadline(opt(flags, '--deadline'));
779
+ if (typeof dl === 'string') {
780
+ return { envelope: failEnvelope('INVALID_ARGUMENT', dl, { deadline: opt(flags, '--deadline') }, [], 'needs_human'), human: TASK_USAGE };
781
+ }
782
+ // 确定性幂等键 + taskId (同 requestId 重发不发生第二笔付款)
783
+ const requestId = flags.requestId || taskRequestId({ instruction, capability, buyerDid: signer.did, salt: opt(flags, '--salt') });
784
+ // 目标解析: --endpoint > --peer > --via gateway > registry 发现
785
+ const { httpTaskTransport, irohTaskTransport, gatewayTaskTransport, resolveTaskTarget, buildTaskFrame, } = await import('../../agents/task-transport.js');
786
+ const { readLocalTask, upsertLocalTask, patchLocalTask } = await import('../../agents/task-inbox.js');
787
+ const endpoint = opt(flags, '--endpoint') || opt(flags, '--url');
788
+ const peer = opt(flags, '--peer');
789
+ const via = opt(flags, '--via');
790
+ let target = '';
791
+ let targetSource = '';
792
+ let transportKind = 'http';
793
+ let providerDid = opt(flags, '--provider') || '';
794
+ if (endpoint) {
795
+ target = endpoint;
796
+ targetSource = 'CLI 显式 --endpoint';
797
+ transportKind = 'http';
798
+ }
799
+ else if (peer) {
800
+ target = peer;
801
+ targetSource = 'CLI 显式 --peer (iroh nodeId)';
802
+ transportKind = 'iroh';
803
+ }
804
+ else {
805
+ const resolved = await resolveTaskTarget(capability);
806
+ if (resolved.target) {
807
+ target = resolved.target.target;
808
+ targetSource = resolved.target.source;
809
+ transportKind = via === 'gateway' ? 'gateway' : resolved.target.kind;
810
+ providerDid = providerDid || resolved.target.providerDid || '';
811
+ }
812
+ else if (via === 'gateway') {
813
+ return {
814
+ envelope: failEnvelope('CAPABILITY_NOT_FOUND', `注册表里没有能力 '${capability}' 的 provider (registry ${resolved.registryReady ? '就绪' : '未就绪'}, ${resolved.candidates} 条候选)`, { capability, registryReady: resolved.registryReady, candidates: resolved.candidates, via: 'gateway' }, [], 'redefine_capability'),
815
+ human: `${title('bolloon task send')}\n 注册表里没有这个能力的 provider, gateway 传输没有目标`,
816
+ };
817
+ }
818
+ else {
819
+ const joined = resolved.registryReady;
820
+ return {
821
+ envelope: failEnvelope(joined ? 'CAPABILITY_NOT_FOUND' : 'NETWORK_NOT_JOINED', joined
822
+ ? `注册表里没有能力 '${capability}' 的 provider (${resolved.candidates} 条候选, 且都没有 endpoint)`
823
+ : '本机 registry 未就绪 (没有入网/没拉过任何服务声明), 无法解析目标', { capability, registryReady: joined, candidates: resolved.candidates, howTo: '用 --endpoint http://host:port 显式给目标, 或先 bolloon network join / bolloon agent register' }, [], joined ? 'redefine_capability' : 'rejoin_network'),
824
+ human: `${title('bolloon task send')}\n ${joined ? '注册表里没有这个能力' : '本机还没入网, 也没有显式 --endpoint'}\n\n${hint('示例: bolloon task send --capability research --instruction "…" --endpoint http://127.0.0.1:54901')}`,
825
+ };
826
+ }
827
+ }
828
+ if (via === 'gateway')
829
+ transportKind = 'gateway';
830
+ const provider = providerDid || `did:unknown:${String(target).replace(/^https?:\/\//, '').slice(0, 60)}`;
831
+ const existing = readLocalTask(requestId);
832
+ const taskId = existing?.taskId || newTaskId();
833
+ // 构造 + **签名** 请求 (契约层: signTaskEnvelope; 私钥只在本机进程)
834
+ const unsigned = {
835
+ protocol: TASK_PROTOCOL,
836
+ taskId,
837
+ requestId,
838
+ capability,
839
+ instruction,
840
+ ...(budget ? { budget } : {}),
841
+ paymentMode: modeRaw,
842
+ deadline: dl,
843
+ buyerDid: signer.did,
844
+ providerDid: provider,
845
+ signature: '',
846
+ };
847
+ const signed = await signTaskEnvelope(unsigned, signer.keypair);
848
+ const v = validateTaskRequest(signed, { now: Date.now() });
849
+ if (!v.ok) {
850
+ const issues = v.issues;
851
+ const code = issues.some((x) => x.includes('协议版本')) ? 'PROTOCOL_VERSION_UNSUPPORTED'
852
+ : issues.some((x) => x.includes('deadline')) ? 'DEADLINE_EXPIRED'
853
+ : issues.some((x) => x.includes('预算')) ? 'BUDGET_EXCEEDED'
854
+ : issues.some((x) => x.includes('缺签名')) ? 'SIGNATURE_REQUIRED'
855
+ : 'INVALID_ARGUMENT';
856
+ const next = code === 'PROTOCOL_VERSION_UNSUPPORTED' ? 'upgrade_client'
857
+ : code === 'DEADLINE_EXPIRED' ? 'retry_same_request'
858
+ : code === 'BUDGET_EXCEEDED' ? 'raise_budget' : 'needs_human';
859
+ return {
860
+ envelope: failEnvelope(code, `请求没通过契约层校验: ${issues.join('; ')}`, { requestId, taskId, issues, paid: false }, [requestId], next),
861
+ human: `${title('bolloon task send')}\n 请求没通过校验:\n${issues.map((i) => ` · ${i}`).join('\n')}`,
862
+ };
863
+ }
864
+ const replyTo = opt(flags, '--reply-to') || null;
865
+ const frame = buildTaskFrame('task_request', signed, { did: signer.did, publicKeyHex: signer.publicKeyHex }, { replyTo });
866
+ // 传输: 不可用 → 结构化诚实失败 (绝不假装已发出)
867
+ const transport = transportKind === 'iroh' ? irohTaskTransport({ startIfNeeded: true })
868
+ : transportKind === 'gateway' ? gatewayTaskTransport()
869
+ : httpTaskTransport();
870
+ const avail = await transport.available();
871
+ if (!avail.ok) {
872
+ return {
873
+ envelope: failEnvelope('TRANSPORT_UNAVAILABLE', `传输不可用 (${transportKind}): ${avail.reason}`, { requestId, taskId, target, transport: { kind: transportKind, available: false, reason: avail.reason }, state: 'quoted', paid: false }, [requestId, taskId], 'needs_human'),
874
+ human: `${title('bolloon task send')}\n 传输不可用 (${transportKind}): ${avail.reason}\n\n${hint('没有真发出任何东西; 收件箱里不会有它')}`,
875
+ };
876
+ }
877
+ const transfer = await transport.send(target, frame, flags.timeoutMs);
878
+ const receipt = { at: Date.now(), ok: transfer.ok, duplicate: transfer.duplicate === true, error: transfer.error || null };
879
+ // 落本机台账 (发出去了/没发出去都要留痕, 但不覆盖已推进的状态)
880
+ upsertLocalTask({
881
+ protocol: TASK_PROTOCOL,
882
+ requestId, taskId, capability, instruction,
883
+ buyerDid: signer.did, providerDid: provider,
884
+ target, transportKind,
885
+ state: 'submitted',
886
+ sentAt: Date.now(), updatedAt: Date.now(),
887
+ requestSignature: signed.signature,
888
+ receipt,
889
+ notes: [transfer.ok ? `已发给 ${target} (${transportKind}, ${transfer.elapsedMs}ms${transfer.duplicate ? ', 对端说是重复' : ''})` : `发送失败: ${transfer.error}`],
890
+ });
891
+ if (!transfer.ok) {
892
+ return {
893
+ envelope: failEnvelope(/超时/.test(String(transfer.error)) ? 'TIMEOUT' : 'TRANSPORT_FAILED', `任务没发出去: ${transfer.error}`, {
894
+ taskId, requestId, target, targetSource,
895
+ transport: transferView(transfer),
896
+ state: 'submitted', paid: false, localLedger: `~/.bolloon/tasks/local/${requestId}.json`,
897
+ note: '请求已签名但**没有送达**; 收件箱里不会有它 (本命令不重试, 也不假装成功)',
898
+ }, [requestId, taskId], 'retry_same_request'),
899
+ human: `${title('bolloon task send')}\n 没发出去: ${transfer.error}\n${line('目标', target)}\n${line('幂等键', requestId)}\n\n${hint('同一 requestId 重发是幂等的 (不会产生第二笔付款)')}`,
900
+ };
901
+ }
902
+ // 对端若立刻回了帧 (accept/reject/result) → 走**同一套**收件侧语义落盘
903
+ let applied = null;
904
+ if (transfer.reply) {
905
+ const { handleTaskFrame } = await import('../../web/task-frame-server.js');
906
+ const r = await handleTaskFrame(transfer.reply, { home: os.homedir() });
907
+ applied = { frame: transfer.reply.frame, ok: r.ok, note: r.note || null, error: r.error || null, facts: r.facts || null };
908
+ if (r.error)
909
+ patchLocalTask(requestId, { notes: [...(readLocalTask(requestId)?.notes || []), `回执处理失败: ${r.error}`] });
910
+ }
911
+ const data = {
912
+ taskId, requestId, target, targetSource, capability,
913
+ state: 'submitted',
914
+ paymentMode: modeRaw,
915
+ budget: budget || null,
916
+ deadline: dl,
917
+ transport: transferView(transfer),
918
+ replyTo,
919
+ receipt,
920
+ reply: applied,
921
+ /** ★ 本命令**没有**发起任何付款 (付款只能由持钱包的一方走同一 requestId 的 x402 幂等路径) */
922
+ transactionId: null,
923
+ paid: false,
924
+ localLedger: `~/.bolloon/tasks/local/${requestId}.json`,
925
+ note: '任务正文只在信封里走这条直连任务通道 (不用公开投影); 默认 policy 模式: 付款要人工/策略放行, 本命令不付钱',
926
+ };
927
+ return {
928
+ envelope: okEnvelope('TASK_SUBMITTED', `任务已签名发出: ${capability} → ${target}`, data, [requestId, taskId], null),
929
+ human: [
930
+ title('bolloon task send'),
931
+ line('任务号', taskId),
932
+ line('幂等键', requestId),
933
+ line('目标', `${target} (${transportKind})`),
934
+ line('capability', capability),
935
+ line('截止时间', new Date(Number(dl)).toISOString()),
936
+ line('结果', transfer.duplicate ? '对端说这是重复请求 (幂等: 不重复接受)' : '对端收下了 (有回执)'),
937
+ line('本次付款', '没有 (发送任务不付款)'),
938
+ `\n ${hint('下一步: 等对端 bolloon task inbox / accept; 查状态: bolloon task status ' + requestId)}`,
939
+ ].join('\n'),
940
+ };
941
+ }
942
+ // ── inbox ───────────────────────────────────────────────────────────────────
943
+ async function taskInbox(flags) {
944
+ const { listInbox, inboxSummary } = await import('../../agents/task-inbox.js');
945
+ const all = listInbox();
946
+ const showAll = has(flags, '--all');
947
+ const summaries = (showAll ? all : all.filter((i) => i.state === 'pending')).map((i) => inboxSummary(i));
948
+ const pending = all.filter((i) => i.state === 'pending').length;
949
+ const data = {
950
+ count: summaries.length,
951
+ pending,
952
+ total: all.length,
953
+ dir: '~/.bolloon/tasks/inbox',
954
+ filter: showAll ? 'all' : 'pending',
955
+ items: summaries,
956
+ note: '收件箱只落**待处理请求**: 不执行、不付款、不自动接单; 同一 requestId 幂等去重 (dedupeInbox)',
957
+ bodyNote: 'instructionPreview 是本机预览 (60 字); 正文只在本机收件箱文件里, 不进公开投影',
958
+ next: pending > 0 ? '要人决定接不接: bolloon task accept <requestId> / bolloon task reject <requestId> --reason "…"' : null,
959
+ };
960
+ const human = summaries.length
961
+ ? summaries.map((i) => [
962
+ `\n ${i.requestId}`,
963
+ ` 任务号 ${i.taskId}`,
964
+ ` 发送方 ${i.sender || '(无)'}`,
965
+ ` capability ${i.capability}`,
966
+ ` 截止时间 ${i.deadline ? `${new Date(i.deadline).toISOString()} (${(i.deadlineInMs ?? 0) > 0 ? `还剩 ${Math.round((i.deadlineInMs ?? 0) / 1000)}s` : '已过期'})` : '(无)'}`,
967
+ ` 状态 ${i.state}${i.requestSignatureVerified === true ? ' · 请求验签通过' : i.requestSignatureVerified === false ? ' · 请求验签**不过**' : ' · 请求未验签(如实)'}`,
968
+ i.budget ? ` 预算 ≤ ${i.budget.maxAmount} (原子) ${i.budget.currency} @ ${i.budget.network}` : ' 预算 (无)',
969
+ ` 预览 ${i.instructionPreview}`,
970
+ ].join('\n')).join('')
971
+ : '\n (收件箱空: 没有待处理请求)';
972
+ return {
973
+ envelope: okEnvelope('OK', `收件箱: ${summaries.length} 条${showAll ? ' (全部)' : '待处理'} (共 ${all.length} 条)`, data, summaries.map((i) => i.requestId), pending > 0 && !showAll ? 'needs_human' : null),
974
+ human: [`${title('bolloon task inbox')}${human}`, '', ` ${String(data.note)}`, pending > 0 ? `\n ${hint('下一步: bolloon task accept ' + (summaries.find((i) => i.state === 'pending')?.requestId || '<requestId>'))}` : ''].join('\n'),
975
+ };
976
+ }
977
+ // ── accept ──────────────────────────────────────────────────────────────────
978
+ async function taskAccept(flags) {
979
+ const id = flags.positionals[1] || flags.requestId;
980
+ if (!id) {
981
+ return { envelope: failEnvelope('INVALID_ARGUMENT', '缺少 requestId (或 taskId)', { usage: plain(TASK_USAGE.trim()) }, [], 'needs_human'), human: TASK_USAGE };
982
+ }
983
+ const { readInboxItem, listInbox, patchInboxItem, saveBody, saveResult } = await import('../../agents/task-inbox.js');
984
+ const { TASK_PROTOCOL, signTaskEnvelope } = await import('../../agents/task-contract.js');
985
+ const item = readInboxItem(id) || listInbox().find((i) => i.taskId === id) || null;
986
+ if (!item) {
987
+ return {
988
+ envelope: failEnvelope('NOT_FOUND', `收件箱里没有这个请求: ${id}`, { id, dir: '~/.bolloon/tasks/inbox' }, [], 'needs_human'),
989
+ human: `${title('bolloon task accept ' + id)}\n 收件箱里没有它\n\n${hint('下一步: bolloon task inbox 看有哪些待处理的请求')}`,
990
+ };
991
+ }
992
+ // 幂等: 已经处理过 → 返回既有事实, 不重复接受/不重复执行/不重复收费
993
+ if (item.state !== 'pending') {
994
+ return {
995
+ envelope: failEnvelope('DUPLICATE_REQUEST', `这个 requestId 已经处理过了 (state=${item.state}); 幂等: 不重复接受`, { requestId: item.requestId, taskId: item.taskId, state: item.state, accept: item.accept ? { providerDid: item.accept.providerDid } : null, reject: item.reject ? { reason: item.reject.reason } : null, result: item.result ? { contentHash: item.result.contentHash || null } : null, paid: false }, [item.requestId], null),
996
+ human: `${title('bolloon task accept ' + item.requestId)}\n 已经处理过了: state=${item.state}\n\n${hint('幂等: 不重复接受, 不会产生第二条记录/第二笔付款')}`,
997
+ };
998
+ }
999
+ // ① capability 已知 (本机 manifest / 本地注册表声明过)
1000
+ const caps = await localCapabilityFacts(item.capability);
1001
+ if (!caps.known) {
1002
+ return {
1003
+ envelope: failEnvelope('CAPABILITY_NOT_FOUND', `本机没有声明过能力 '${item.capability}' → 不接 (拒, 不假装能做)`, { requestId: item.requestId, capability: item.capability, declaredSources: caps.sources, registryServices: caps.services, howTo: `bolloon agent register --capability ${item.capability} --price 0.01` }, [item.requestId], 'redefine_capability'),
1004
+ human: `${title('bolloon task accept ' + item.requestId)}\n 本机没有声明过能力 '${item.capability}' → 拒绝接单\n\n${hint('先声明能力: bolloon agent register --capability ' + item.capability + ' --price 0.01')}`,
1005
+ };
1006
+ }
1007
+ // ② 预算足够 (本机为该能力登记的价 ≤ 请求预算)
1008
+ if (item.budget && caps.priceAmount) {
1009
+ const priceAtomic = toAtomic(String(caps.priceAmount), String(caps.currency || 'USDC').toUpperCase() === 'ETH' ? 18 : 6);
1010
+ if (priceAtomic && BigInt(priceAtomic) > BigInt(item.budget.maxAmount)) {
1011
+ return {
1012
+ envelope: failEnvelope('BUDGET_EXCEEDED', `本机报价 ${caps.priceAmount} 超过请求预算 ${item.budget.maxAmount} (原子)`, { requestId: item.requestId, priceAtomic, budget: item.budget }, [item.requestId], 'raise_budget'),
1013
+ human: `${title('bolloon task accept ' + item.requestId)}\n 本机报价 ${caps.priceAmount} 超请求预算 (原子 ${item.budget.maxAmount}) → 拒\n\n${hint('对端要提预算 (改预算不换 requestId, 幂等保护仍有效)')}`,
1014
+ };
1015
+ }
1016
+ }
1017
+ // ③ policy 允许 (既有 payment-gate YAML 规则链; deny → 直接拒)
1018
+ let gateView = null;
1019
+ try {
1020
+ const { getPaymentGate } = await import('../../agents/payment-gate.js');
1021
+ const verdict = getPaymentGate().evaluate({ service: item.capability, amount: Number(caps.priceAmount || 0), recipient: caps.wallet || '' });
1022
+ gateView = { decision: verdict.decision, reason: verdict.reason };
1023
+ if (verdict.decision === 'deny') {
1024
+ return {
1025
+ envelope: failEnvelope('POLICY_DENIED', `本地策略拒绝: ${verdict.reason}`, { requestId: item.requestId, policy: gateView, policyFile: '~/.bolloon/../payment-policy.yaml (getPaymentGate)' }, [item.requestId], 'needs_human'),
1026
+ human: `${title('bolloon task accept ' + item.requestId)}\n 本地策略拒绝: ${verdict.reason}`,
1027
+ };
1028
+ }
1029
+ }
1030
+ catch (e) {
1031
+ // 策略读不出来 → 不假装放行 (但也不因此判死; 如实标注)
1032
+ gateView = { decision: 'unavailable', reason: String(e?.message || e).slice(0, 160) };
1033
+ }
1034
+ // ④ requestId 未重复 (收件箱里这条就是唯一一条 —— 幂等去重保证)
1035
+ const dup = listInbox().filter((i) => i.requestId === item.requestId).length;
1036
+ if (dup !== 1) {
1037
+ return {
1038
+ envelope: failEnvelope('DUPLICATE_REQUEST', `收件箱里同一 requestId 有 ${dup} 条 (幂等前提被破坏) → 拒`, { requestId: item.requestId, count: dup }, [item.requestId], 'needs_human'),
1039
+ human: `${title('bolloon task accept ' + item.requestId)}\n 收件箱里同一 requestId 有 ${dup} 条 → 拒`,
1040
+ };
1041
+ }
1042
+ // 交付文件先验证存在 (避免"接受了却没有正文"的半截状态)
1043
+ const deliverFile = opt(flags, '--deliver');
1044
+ let bodyText = null;
1045
+ if (deliverFile) {
1046
+ try {
1047
+ bodyText = fs.readFileSync(deliverFile, 'utf8');
1048
+ }
1049
+ catch (e) {
1050
+ return {
1051
+ envelope: failEnvelope('INVALID_ARGUMENT', `--deliver 文件读不出来: ${String(e?.message || e).slice(0, 160)}`, { file: deliverFile, requestId: item.requestId }, [item.requestId], 'retry_same_request'),
1052
+ human: `${title('bolloon task accept ' + item.requestId)}\n 交付文件读不出来: ${deliverFile}`,
1053
+ };
1054
+ }
1055
+ }
1056
+ const s = await requireSigner('bolloon task accept');
1057
+ if ('fail' in s)
1058
+ return s.fail;
1059
+ const signer = s.signer;
1060
+ const etaMs = opt(flags, '--eta') ? Number(opt(flags, '--eta')) : undefined;
1061
+ const acceptInput = {
1062
+ protocol: TASK_PROTOCOL, taskId: item.taskId, requestId: item.requestId, accepted: true,
1063
+ providerDid: signer.did, ...(Number.isFinite(etaMs) ? { etaMs: etaMs } : {}), signature: '',
1064
+ };
1065
+ const signedAccept = await signTaskEnvelope(acceptInput, signer.keypair);
1066
+ let result = null;
1067
+ let resultVerified = null;
1068
+ let bodyFacts = null;
1069
+ if (bodyText !== null) {
1070
+ const saved = saveBody(item.taskId, bodyText);
1071
+ if (!saved.ok) {
1072
+ return {
1073
+ envelope: failEnvelope('INTERNAL_ERROR', `交付正文落盘失败: ${saved.error}`, { requestId: item.requestId, file: deliverFile }, [item.requestId], 'retry_same_request'),
1074
+ human: `${title('bolloon task accept ' + item.requestId)}\n 交付正文落盘失败: ${saved.error}`,
1075
+ };
1076
+ }
1077
+ const resultInput = {
1078
+ protocol: TASK_PROTOCOL, taskId: item.taskId, requestId: item.requestId, ok: true,
1079
+ summary: `交付正文 ${saved.bytes} 字节 (sha256 ${String(saved.contentHash).slice(0, 12)}…)`,
1080
+ contentHash: saved.contentHash, deliveredAt: Date.now(), signature: '',
1081
+ };
1082
+ const signedResult = await signTaskEnvelope(resultInput, signer.keypair);
1083
+ // ★ 立刻用本机公钥**真验一次**自己刚签的结果 (不是硬编 true)
1084
+ const { verifyTaskEnvelope } = await import('../../agents/task-contract.js');
1085
+ const { verifierFor } = await import('../../agents/local-signer.js');
1086
+ const vf = verifierFor(signer.publicKeyHex);
1087
+ resultVerified = vf ? await verifyTaskEnvelope(signedResult, vf) : null;
1088
+ saveResult(signedResult);
1089
+ result = signedResult;
1090
+ bodyFacts = { contentHash: saved.contentHash, bytes: saved.bytes, file: saved.file, signatureSelfVerified: resultVerified };
1091
+ }
1092
+ const patched = patchInboxItem(item.requestId, {
1093
+ state: result ? 'delivered' : 'accepted',
1094
+ accept: signedAccept,
1095
+ ...(result ? { result, resultVerified } : {}),
1096
+ ...(signer.publicKeyHex ? { providerPublicKeyHex: signer.publicKeyHex } : {}),
1097
+ });
1098
+ if (!patched.ok) {
1099
+ return {
1100
+ envelope: failEnvelope('INTERNAL_ERROR', `接单状态落盘失败: ${patched.error}`, { requestId: item.requestId }, [item.requestId], 'retry_same_request'),
1101
+ human: `${title('bolloon task accept ' + item.requestId)}\n 落盘失败: ${patched.error}`,
1102
+ };
1103
+ }
1104
+ // 回执: 把签名接受 (+ 结果) 发回发送方
1105
+ const replyTo = opt(flags, '--reply-to') || item.replyTo || null;
1106
+ const frames = [];
1107
+ const { buildTaskFrame } = await import('../../agents/task-transport.js');
1108
+ const signerInfo = { did: signer.did, publicKeyHex: signer.publicKeyHex };
1109
+ frames.push(buildTaskFrame('task_accept', signedAccept, signerInfo));
1110
+ if (result) {
1111
+ frames.push(buildTaskFrame('task_result', result, signerInfo, bodyText !== null ? { body: bodyText } : {}));
1112
+ }
1113
+ const reply = await sendReplyFrames(replyTo, frames, flags.timeoutMs);
1114
+ const data = {
1115
+ requestId: item.requestId,
1116
+ taskId: item.taskId,
1117
+ capability: item.capability,
1118
+ providerDid: signer.did,
1119
+ state: result ? 'delivered' : 'accepted',
1120
+ rejected: false,
1121
+ executed: false, // 本命令**不执行**任务 (只签名接受/交付已有的正文)
1122
+ paid: false, // 本命令**不付款**
1123
+ checks: {
1124
+ capabilityKnown: caps.known, capabilitySources: caps.sources,
1125
+ priceAmount: caps.priceAmount, budget: item.budget || null,
1126
+ policy: gateView, requestIdUnique: dup === 1,
1127
+ },
1128
+ accepted: { signed: true, providerDid: signedAccept.providerDid, etaMs: signedAccept.etaMs ?? null },
1129
+ delivered: result ? { signed: true, ...bodyFacts } : null,
1130
+ reply: { attempted: reply.attempted, ok: reply.ok, target: reply.target, error: reply.error || null, results: reply.results },
1131
+ inboxFile: `~/.bolloon/tasks/inbox/${item.requestId}.json`,
1132
+ note: '接受 = 签名声明"我来做"; 交付正文哈希由真实字节算出; 本命令不执行任务、不付款',
1133
+ };
1134
+ return {
1135
+ envelope: okEnvelope('TASK_ACCEPTED', `已接受 ${item.requestId}${result ? ' 并交付结果' : ''}${reply.ok ? ' (回执已送达)' : reply.attempted ? ' (回执没送达)' : ''}`, data, [item.requestId, item.taskId, ...(bodyFacts?.contentHash ? [String(bodyFacts.contentHash)] : [])], result ? null : 'wait'),
1136
+ human: [
1137
+ title('bolloon task accept ' + item.requestId),
1138
+ line('任务号', item.taskId),
1139
+ line('capability', `${item.capability} (已知: ${caps.sources.join('+') || '—'})`),
1140
+ line('报价/预算', `${caps.priceAmount ?? '(未登记价)'} ≤ ${item.budget ? item.budget.maxAmount + ' 原子' : '(无预算)'}`),
1141
+ line('策略', gateView ? `${gateView.decision} — ${gateView.reason}` : '(未读)'),
1142
+ line('已签名接受', '是 (TaskAccept)'),
1143
+ ...(result ? [line('已交付', `contentHash ${String(bodyFacts?.contentHash).slice(0, 16)}… (${bodyFacts?.bytes} 字节)`)] : []),
1144
+ line('回执', reply.attempted ? (reply.ok ? `已送达 ${reply.target}` : `没送达: ${reply.error}`) : `没有回执通道 (${reply.error})`),
1145
+ line('本次付款', '没有 (接单不付款)'),
1146
+ line('本次执行', '没有 (本命令只签名接受/交付已备好的正文)'),
1147
+ ].join('\n'),
1148
+ };
1149
+ }
1150
+ // ── reject ──────────────────────────────────────────────────────────────────
1151
+ async function taskReject(flags) {
1152
+ const id = flags.positionals[1] || flags.requestId;
1153
+ const reason = opt(flags, '--reason');
1154
+ if (!id) {
1155
+ return { envelope: failEnvelope('INVALID_ARGUMENT', '缺少 requestId (或 taskId)', { usage: plain(TASK_USAGE.trim()) }, [], 'needs_human'), human: TASK_USAGE };
1156
+ }
1157
+ if (!reason) {
1158
+ return {
1159
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 --reason (契约要求: 拒绝必须给原因)', { requestId: id, accepted: ['--reason "预算不够"'] }, [], 'needs_human'),
1160
+ human: `${TASK_USAGE}\n${hint('示例: bolloon task reject ' + id + ' --reason "这个能力本机没声明"')}`,
1161
+ };
1162
+ }
1163
+ const { readInboxItem, listInbox, patchInboxItem } = await import('../../agents/task-inbox.js');
1164
+ const { TASK_PROTOCOL, signTaskEnvelope } = await import('../../agents/task-contract.js');
1165
+ const item = readInboxItem(id) || listInbox().find((i) => i.taskId === id) || null;
1166
+ if (!item) {
1167
+ return {
1168
+ envelope: failEnvelope('NOT_FOUND', `收件箱里没有这个请求: ${id}`, { id, dir: '~/.bolloon/tasks/inbox' }, [], 'needs_human'),
1169
+ human: `${title('bolloon task reject ' + id)}\n 收件箱里没有它`,
1170
+ };
1171
+ }
1172
+ if (item.state !== 'pending') {
1173
+ return {
1174
+ envelope: failEnvelope('DUPLICATE_REQUEST', `这个 requestId 已经处理过了 (state=${item.state}); 幂等: 不重复拒绝`, { requestId: item.requestId, state: item.state, reject: item.reject ? { reason: item.reject.reason } : null, paid: false, executed: false }, [item.requestId], null),
1175
+ human: `${title('bolloon task reject ' + item.requestId)}\n 已经处理过了: state=${item.state}`,
1176
+ };
1177
+ }
1178
+ const s = await requireSigner('bolloon task reject');
1179
+ if ('fail' in s)
1180
+ return s.fail;
1181
+ const signer = s.signer;
1182
+ const rejectInput = {
1183
+ protocol: TASK_PROTOCOL, taskId: item.taskId, requestId: item.requestId, accepted: false,
1184
+ reason: String(reason).slice(0, 500), signature: '',
1185
+ };
1186
+ const signedReject = await signTaskEnvelope(rejectInput, signer.keypair);
1187
+ const patched = patchInboxItem(item.requestId, { state: 'rejected', reject: signedReject });
1188
+ if (!patched.ok) {
1189
+ return { envelope: failEnvelope('INTERNAL_ERROR', `拒绝原因落盘失败: ${patched.error}`, { requestId: item.requestId }, [item.requestId], 'retry_same_request'), human: `${title('bolloon task reject')}\n 落盘失败` };
1190
+ }
1191
+ const replyTo = opt(flags, '--reply-to') || item.replyTo || null;
1192
+ const { buildTaskFrame } = await import('../../agents/task-transport.js');
1193
+ const reply = await sendReplyFrames(replyTo, [buildTaskFrame('task_reject', signedReject, { did: signer.did, publicKeyHex: signer.publicKeyHex })], flags.timeoutMs);
1194
+ const data = {
1195
+ requestId: item.requestId,
1196
+ taskId: item.taskId,
1197
+ capability: item.capability,
1198
+ state: 'rejected',
1199
+ reason: String(reason),
1200
+ signed: true,
1201
+ executed: false, // ★ 不执行
1202
+ paid: false, // ★ 不付款
1203
+ reply: { attempted: reply.attempted, ok: reply.ok, target: reply.target, error: reply.error || null },
1204
+ inboxFile: `~/.bolloon/tasks/inbox/${item.requestId}.json`,
1205
+ note: '拒绝只记原因 + 签名 + 落盘: 不执行、不付款 (没有交易被创建)',
1206
+ };
1207
+ return {
1208
+ envelope: okEnvelope('TASK_REJECTED', `已拒绝 ${item.requestId} (原因已记录, 未执行未付款)`, data, [item.requestId, item.taskId], null),
1209
+ human: [
1210
+ title('bolloon task reject ' + item.requestId),
1211
+ line('原因', String(reason)),
1212
+ line('已签名', '是 (TaskReject)'),
1213
+ line('回执', reply.attempted ? (reply.ok ? `已送达 ${reply.target}` : `没送达: ${reply.error}`) : `没有回执通道 (${reply.error})`),
1214
+ line('本次执行', '没有'),
1215
+ line('本次付款', '没有'),
1216
+ ].join('\n'),
1217
+ };
1218
+ }
1219
+ // ── 仍未实现的两个 (任务书没点名; 如实报, 不假装) ────────────────────────────
1220
+ async function taskComplete(_flags) {
1221
+ const env = notImplemented('task complete 还没实现: 没有任务状态存储可写 (交易层有非法迁移保护, 不许绕过)', {
1222
+ plannedCapability: 'task.complete',
1223
+ contractLayer: 'src/agents/task-contract.ts checkTaskMove (:73-80) 非法迁移拒绝, 不静默修正',
1224
+ alternative: '点 M1 闭环的进展: bolloon task status <id> / bolloon task --resume <goalId>',
1225
+ });
1226
+ return { envelope: env, human: `${title('bolloon task complete')}\n 未实现 (没有可写的任务状态存储; 交易状态机有非法迁移保护, 不许绕过)` };
1227
+ }
1228
+ async function taskCancel(_flags) {
1229
+ const env = notImplemented('task cancel 还没实现: 交易层没有 cancelled 态, 也没有取消服务 (不许绕过状态机硬写)', {
1230
+ plannedCapability: 'task.cancel',
1231
+ contractLayer: '任务 14 态有 cancelled (:40) / 交易 11 态没有 cancelled (transaction-protocol.ts:39-51)',
1232
+ alternative: '付款不确定时**先对账**: bolloon trade reconcile <transactionId> (绝不重付)',
1233
+ });
1234
+ return { envelope: env, human: `${title('bolloon task cancel')}\n 未实现 (交易层没有 cancelled 态; 硬写会绕过状态机)\n\n${hint('付款不确定 → bolloon trade reconcile <transactionId> (先对账, 绝不重付)')}` };
1235
+ }
1236
+ export { judge as judgeTransaction, recSummary as transactionSummary };