@bolloon/bolloon-agent 0.4.31 → 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.
- package/dist/agents/chain/chain-config.js +504 -0
- package/dist/agents/chain/chain-index-query.js +135 -0
- package/dist/agents/chain/chain-indexer.js +1045 -0
- package/dist/agents/chain/chain-settlement.js +284 -0
- package/dist/agents/chain/chain-state-store.js +245 -0
- package/dist/agents/chain/chain-wallet.js +198 -0
- package/dist/agents/chain/escrow-client.js +433 -0
- package/dist/agents/chain/index.js +20 -0
- package/dist/agents/chain/onchain-trade.js +691 -0
- package/dist/agents/network-pulse.js +464 -2
- package/dist/agents/task/task-onchain-runner.js +284 -0
- package/dist/agents/x402/paid-info-store.js +75 -6
- package/dist/cli/commands/chain.js +926 -0
- package/dist/cli/commands/index.js +10 -2
- package/dist/cli/mcp/server.js +4 -1
- package/dist/cli/mcp/tools.js +348 -5
- package/dist/cli/protocol-envelope.js +12 -0
- package/dist/cli-entry.js +5 -0
- package/dist/electron-build/agents/chain/chain-config.js +556 -0
- package/dist/electron-build/agents/chain/chain-config.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-index-query.js +145 -0
- package/dist/electron-build/agents/chain/chain-index-query.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-indexer.js +1095 -0
- package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-settlement.js +291 -0
- package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -0
- package/dist/electron-build/agents/chain/escrow-client.js +443 -0
- package/dist/electron-build/agents/chain/escrow-client.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +478 -3
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/x402/paid-info-store.js +76 -6
- package/dist/electron-build/agents/x402/paid-info-store.js.map +1 -1
- package/dist/web/chain-explorer.js +617 -0
- package/dist/web/explorer.html +163 -0
- package/dist/web/index.html +5 -0
- package/dist/web/routes-chain-index.js +108 -0
- package/dist/web/routes-onchain-trade.js +99 -0
- package/dist/web/server.js +13 -0
- package/package.json +2 -1
- package/scripts/build-web.ts +20 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-onchain-runner.ts — P4: 把链上 escrow **接进任务链路**的任务级入口
|
|
3
|
+
* =========================================================================
|
|
4
|
+
* 与 `runTask()` (x402/local-dev 支付) 并列的一条任务链路 —— 区别只在钱的走法:
|
|
5
|
+
*
|
|
6
|
+
* 预算闸(M1) → 荐资源 → beginTransaction → [createEscrowV2 → 装+真跑技能 → submitProofV2 → releaseV2]
|
|
7
|
+
* ↑ 每一步都走 verifyPaymentOnChain
|
|
8
|
+
* → 验真门(只有 chainSettled=true 才 verified) → 报告卡
|
|
9
|
+
*
|
|
10
|
+
* 借用的都是任务侧既有件: task-budget / resource-advisor / installBundle /
|
|
11
|
+
* executeContractSkill / resource-contract 保真校验 / report-card / transaction-store。
|
|
12
|
+
* 不重写任何支付判定: 链上判定只有 P3 chain-settlement 一份, 钱的状态只有
|
|
13
|
+
* settlement-state 一份, 链上事实只有 chain-state-store 一份。
|
|
14
|
+
*
|
|
15
|
+
* 三条硬规则 (与 runTask 一致):
|
|
16
|
+
* ① local-dev 永远到不了 fully_settled; ② chainSettled!==true 永远不 verified;
|
|
17
|
+
* ③ 非法迁移拒绝并给原因, 不静默修正。
|
|
18
|
+
* 另加一条 (链上特有): 任何一步链上事实不成立 → 原样停下, **不重付/不自动退款**;
|
|
19
|
+
* 重启时先读 chain-state.json 重建 (recoverOnchainTrade) 再决定续跑。
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { resolveTaskBudget } from './task-budget.js';
|
|
24
|
+
import { adviseResource } from './resource-advisor.js';
|
|
25
|
+
import { defaultRequestId, installBundle, deriveSkillInput, extractSources, extractConclusion } from './task-runner.js';
|
|
26
|
+
import { buildReportCard, renderReportCard } from './report-card.js';
|
|
27
|
+
import { loadResourceContract, executeContractSkill, validateResourceOutput, verifyInstallFidelity, } from '../x402/resource-contract.js';
|
|
28
|
+
import { beginTransaction, readTransaction, updateTransaction } from '../x402/transaction-store.js';
|
|
29
|
+
import { writeDeliveryContent } from '../x402/settlement-state.js';
|
|
30
|
+
import { computeContentHash } from '../x402/paid-info-protocol.js';
|
|
31
|
+
import { runOnchainTradeLoop, resumeOnchainTrade, recoverOnchainTrade, sha256Digest, checkOnchainAmount, onchainTaskKey, applyChainSettlementToTransaction, } from '../chain/onchain-trade.js';
|
|
32
|
+
/**
|
|
33
|
+
* 跑一条"链上托管"的任务: 买(托管) → 真执行 → 卖方上链承诺 → 买方释放 → 验真。
|
|
34
|
+
*/
|
|
35
|
+
export async function runTaskOnchain(opts) {
|
|
36
|
+
const stages = [];
|
|
37
|
+
let last = Date.now();
|
|
38
|
+
const mark = (stage, note) => {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
stages.push({ stage, note, ms: now - last });
|
|
41
|
+
last = now;
|
|
42
|
+
opts.onStage?.(stage, note);
|
|
43
|
+
};
|
|
44
|
+
const decimals = opts.tokenDecimals ?? 6;
|
|
45
|
+
const requestId = opts.requestId || defaultRequestId(opts.task, Number(opts.budget ?? 0.05));
|
|
46
|
+
const taskId = requestId; // 任务键 = 幂等键 (重复跑同一任务不会新开托管)
|
|
47
|
+
const taskKey = onchainTaskKey(taskId);
|
|
48
|
+
const outputIssues = [];
|
|
49
|
+
// ① 预算闸 (M1: 单任务 0.05 / 单次 0.02, 所有层取最小值; 非法 → 不记账不花钱)
|
|
50
|
+
const parsed = resolveTaskBudget({ taskBudget: opts.budget, perPurchase: opts.perPurchase });
|
|
51
|
+
if (!parsed.ok || !parsed.plan) {
|
|
52
|
+
const card = buildReportCard({ task: opts.task, executed: false, paid: false, evidenceRef: {}, blocker: `预算不合法: ${parsed.error}` });
|
|
53
|
+
mark('report', '预算非法 → 没有托管, 没有花钱');
|
|
54
|
+
return {
|
|
55
|
+
ok: false, taskKey, requestId, transactionId: '', card, text: renderReportCard(card), budget: null,
|
|
56
|
+
chain: { verified: false, reason: '预算非法', statuses: [] }, outputIssues: [String(parsed.error)],
|
|
57
|
+
recovery: recoverOnchainTrade({ home: opts.home, taskKey }), stages,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const budget = parsed.plan;
|
|
61
|
+
mark('prepare', `任务: ${opts.task} · 任务预算 ${budget.taskBudget} / 单次上限 ${budget.perPurchase} USDC`);
|
|
62
|
+
// ② 荐资源 (用户不点名; 没有可执行资源就不买)
|
|
63
|
+
let chosen = opts.choose ? await opts.choose() : null;
|
|
64
|
+
if (!chosen && !opts.choose) {
|
|
65
|
+
const advised = await adviseResource({ task: opts.task, home: opts.home, cwd: opts.cwd, skillPaths: opts.skillPaths });
|
|
66
|
+
if (advised.needed && advised.chosen) {
|
|
67
|
+
chosen = {
|
|
68
|
+
name: advised.chosen.name, version: advised.chosen.version, dir: advised.chosen.dir,
|
|
69
|
+
contract: advised.chosen.contract, why: advised.chosen.why,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const card = buildReportCard({ task: opts.task, executed: false, paid: false, evidenceRef: {}, blocker: `没有可用的可执行资源: ${advised.reason}` });
|
|
74
|
+
mark('report', '没有可执行资源 → 不买, 如实报告');
|
|
75
|
+
return {
|
|
76
|
+
ok: false, taskKey, requestId, transactionId: '', card, text: renderReportCard(card), budget,
|
|
77
|
+
chain: { verified: false, reason: '没有可执行资源', statuses: [] }, outputIssues: [advised.reason],
|
|
78
|
+
recovery: recoverOnchainTrade({ home: opts.home, taskKey }), stages,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!chosen) {
|
|
83
|
+
const card = buildReportCard({ task: opts.task, executed: false, paid: false, evidenceRef: {}, blocker: '注入的资源选择器没有给出候选' });
|
|
84
|
+
return {
|
|
85
|
+
ok: false, taskKey, requestId, transactionId: '', card, text: renderReportCard(card), budget,
|
|
86
|
+
chain: { verified: false, reason: '没有候选', statuses: [] }, outputIssues: ['没有候选'],
|
|
87
|
+
recovery: recoverOnchainTrade({ home: opts.home, taskKey }), stages,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
mark('acquire', `选中 ${chosen.name} (${(chosen.why || []).slice(0, 2).join(' / ') || '名字匹配'})`);
|
|
91
|
+
// ③ 金额 = 单次购买上限 (与 M1 硬约束一致); 转原子单位
|
|
92
|
+
const amountAtomic = BigInt(Math.round(budget.perPurchase * 10 ** decimals));
|
|
93
|
+
const amountChk = checkOnchainAmount({ amountAtomic, decimals, budget: { taskBudget: budget.taskBudget, perPurchase: budget.perPurchase } });
|
|
94
|
+
if (!amountChk.ok) {
|
|
95
|
+
const card = buildReportCard({ task: opts.task, executed: false, paid: false, evidenceRef: {}, blocker: `预算门拒绝 (${amountChk.layer}): ${amountChk.reason}` });
|
|
96
|
+
return {
|
|
97
|
+
ok: false, taskKey, requestId, transactionId: '', card, text: renderReportCard(card), budget,
|
|
98
|
+
chain: { verified: false, reason: amountChk.reason, statuses: [] }, outputIssues: [amountChk.reason],
|
|
99
|
+
recovery: recoverOnchainTrade({ home: opts.home, taskKey }), stages,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// ④ 交易记录 (任务链路侧) —— 链上事实与它分开记, 最后一起进报告卡
|
|
103
|
+
const { record: created } = await beginTransaction({
|
|
104
|
+
requestId,
|
|
105
|
+
metadata: { itemId: chosen.name, price: amountChk.amountUsdc, currency: 'USDC', network: opts.network || 'unknown', payTo: opts.agentAddress, providerDid: 'did:key:zLocalSeller' },
|
|
106
|
+
buyerDid: opts.buyerDid || 'did:key:zTaskBuyer',
|
|
107
|
+
}, opts.home);
|
|
108
|
+
const transactionId = created.transactionId;
|
|
109
|
+
if (created.status === 'discovered')
|
|
110
|
+
await updateTransaction(transactionId, { status: 'quoted' }, opts.home);
|
|
111
|
+
await updateTransaction(transactionId, {
|
|
112
|
+
status: 'paying', paymentMode: 'escrow', settlementFact: 'payment_submitted', amount: amountChk.amountUsdc,
|
|
113
|
+
event: { kind: 'escrow:intent', detail: `${amountChk.amountUsdc} USDC → escrow ${opts.client.escrowAddress}` },
|
|
114
|
+
}, opts.home);
|
|
115
|
+
const tradeReq = {
|
|
116
|
+
client: opts.client, home: opts.home, taskId, agentAddress: opts.agentAddress,
|
|
117
|
+
amountAtomic, paymentAsset: opts.paymentAsset,
|
|
118
|
+
termsDigest: sha256Digest(`terms:${taskId}`), quoteDigest: sha256Digest(`quote:${taskId}`),
|
|
119
|
+
inputDigest: sha256Digest(`input:${taskId}`), manifestDigest: sha256Digest(`manifest:${taskId}`),
|
|
120
|
+
proofVersion: opts.proofVersion ?? 1,
|
|
121
|
+
deadline: opts.deadline ?? BigInt(Math.floor(Date.now() / 1000) + 3600),
|
|
122
|
+
confirmationWindow: opts.confirmationWindow ?? 3600,
|
|
123
|
+
budget: { taskBudget: budget.taskBudget, perPurchase: budget.perPurchase },
|
|
124
|
+
network: opts.network || 'unknown', tokenDecimals: decimals,
|
|
125
|
+
buyerSigner: opts.buyerSigner, sellerSigner: opts.sellerSigner,
|
|
126
|
+
env: opts.env, persistState: opts.persistState, verifyOnChain: opts.verifyOnChain,
|
|
127
|
+
};
|
|
128
|
+
// ⑤ 幂等/恢复: 已经链上走过的任务**不新开托管** (先读盘重建)
|
|
129
|
+
const before = recoverOnchainTrade({ home: opts.home, taskKey });
|
|
130
|
+
let loopVerified = false, loopReason = '';
|
|
131
|
+
const txHashes = {};
|
|
132
|
+
const statuses = [];
|
|
133
|
+
let execResult;
|
|
134
|
+
let resultDigest;
|
|
135
|
+
let deliveredContent = '';
|
|
136
|
+
let fidelity = { ok: false };
|
|
137
|
+
let outChk = { ok: false };
|
|
138
|
+
let conclusion = '';
|
|
139
|
+
let sources = [];
|
|
140
|
+
let contract = chosen.contract;
|
|
141
|
+
/** 真执行 (装 + 跑 + 契约校验) —— 与 runTask 同一套件, 不下链 */
|
|
142
|
+
const doExecute = async () => {
|
|
143
|
+
const SHARE = await import('../skill-share.js');
|
|
144
|
+
const collected = await SHARE.collectSkillBundle(chosen.dir, { name: chosen.name });
|
|
145
|
+
if (!collected?.ok || !collected?.bundle)
|
|
146
|
+
return { ok: false, reason: `技能打包失败: ${collected?.error || '未知'}` };
|
|
147
|
+
deliveredContent = JSON.stringify(collected.bundle);
|
|
148
|
+
const contentHash = computeContentHash(deliveredContent);
|
|
149
|
+
const installDir = path.join(opts.home, '.bolloon', 'tasks', taskId, 'skills', chosen.name);
|
|
150
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
151
|
+
const installed = installBundle(deliveredContent, installDir);
|
|
152
|
+
fidelity = installed.ok ? await verifyInstallFidelity({ content: deliveredContent, rec: { contentHash }, installDir }) : { ok: false, issues: [installed.error] };
|
|
153
|
+
if (!installed.ok) {
|
|
154
|
+
outputIssues.push(installed.error || '安装失败');
|
|
155
|
+
return { ok: false, reason: installed.error };
|
|
156
|
+
}
|
|
157
|
+
const loaded = await loadResourceContract(installDir);
|
|
158
|
+
if (loaded.ok && loaded.contract)
|
|
159
|
+
contract = loaded.contract;
|
|
160
|
+
const input = opts.input ?? deriveSkillInput(contract, opts.task);
|
|
161
|
+
const exec = await executeContractSkill({ contract, skillDir: installDir, input, allowedTools: ['skill_exec', 'read_file'], allowCodeExecution: true });
|
|
162
|
+
outChk = validateResourceOutput(contract, exec.output);
|
|
163
|
+
sources = extractSources(exec.output);
|
|
164
|
+
conclusion = extractConclusion(exec.output).conclusion || '';
|
|
165
|
+
const execution = { ok: exec.execution.ok === true && outChk.ok === true, reason: exec.execution.reason };
|
|
166
|
+
// 交易记录补执行证据
|
|
167
|
+
await updateTransaction(transactionId, {
|
|
168
|
+
execution: { ...exec.execution, ok: exec.execution.ok === true, schemaOk: outChk.ok === true },
|
|
169
|
+
resourceOutcome: { installed: fidelity.ok, executed: exec.execution.ok === true, outputContract: outChk.ok ? 'pass' : 'fail', criteriaHit: outChk.ok && sources.length > 0 && !!conclusion },
|
|
170
|
+
event: { kind: 'resource_outcome', detail: `installed=${fidelity.ok} executed=${exec.execution.ok === true} contract=${outChk.ok ? 'pass' : 'fail'}` },
|
|
171
|
+
}, opts.home);
|
|
172
|
+
if (!execution.ok)
|
|
173
|
+
outputIssues.push(...(outChk.issues || []), exec.execution.reason || '执行未达标');
|
|
174
|
+
// ★ 链上结果承诺 = 我手上这份交付内容的摘要 (买方可以自己复算)
|
|
175
|
+
return {
|
|
176
|
+
ok: execution.ok,
|
|
177
|
+
resultDigest: sha256Digest(deliveredContent),
|
|
178
|
+
manifestDigest: sha256Digest(`manifest:${taskId}`),
|
|
179
|
+
reason: execution.reason,
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
if (!before.found) {
|
|
183
|
+
const loop = await runOnchainTradeLoop(tradeReq, doExecute);
|
|
184
|
+
txHashes.create = loop.txHashes.create;
|
|
185
|
+
txHashes.proof = loop.txHashes.proof;
|
|
186
|
+
txHashes.release = loop.txHashes.release;
|
|
187
|
+
loopVerified = loop.verified;
|
|
188
|
+
loopReason = loop.verifiedReason;
|
|
189
|
+
execResult = loop.execution ? { ok: loop.execution.ok, reason: loop.execution.reason } : undefined;
|
|
190
|
+
resultDigest = loop.execution?.resultDigest;
|
|
191
|
+
for (const s of loop.steps)
|
|
192
|
+
statuses.push(`${s.method}:${s.chainStatus}`);
|
|
193
|
+
mark('acquire', loop.verified ? '链上闭环成立 (create/proof/release)' : `链上闭环未成立: ${loopReason.slice(0, 60)}`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// 已有链上事实 → 只做安全续跑 (绝不重付)
|
|
197
|
+
mark('acquire', `发现已有链上事实 (nextAction=${before.nextAction}) → 走恢复路径, 不新开托管`);
|
|
198
|
+
const resumed = await resumeOnchainTrade({ ...tradeReq, pendingResultDigest: opts.pendingResultDigest });
|
|
199
|
+
loopVerified = resumed.verified;
|
|
200
|
+
loopReason = resumed.reason;
|
|
201
|
+
for (const s of resumed.steps)
|
|
202
|
+
statuses.push(`${s.method}:${s.chainStatus}`);
|
|
203
|
+
txHashes.create = before.create?.txHash;
|
|
204
|
+
txHashes.proof = before.proof?.txHash;
|
|
205
|
+
txHashes.release = before.release?.txHash;
|
|
206
|
+
}
|
|
207
|
+
// ⑥ 验真门 + 报告卡
|
|
208
|
+
const afterRecovery = recoverOnchainTrade({ home: opts.home, taskKey });
|
|
209
|
+
const releaseRec = afterRecovery.release;
|
|
210
|
+
const payment = {
|
|
211
|
+
mode: 'escrow', chainSettled: loopVerified, trust: loopVerified ? 'verified' : 'self-attested',
|
|
212
|
+
txHash: txHashes.release,
|
|
213
|
+
};
|
|
214
|
+
let gateVerified = false;
|
|
215
|
+
let gateReason = loopReason;
|
|
216
|
+
if (loopVerified && releaseRec?.txHash) {
|
|
217
|
+
// 用**链上判定**重算一遍 (重放判定, 不新写判定逻辑): 释放记录 → 验真门
|
|
218
|
+
const { verifyChainSettlement } = await import('../chain/chain-settlement.js');
|
|
219
|
+
const verdict = await verifyChainSettlement(opts.client, {
|
|
220
|
+
txHash: releaseRec.txHash,
|
|
221
|
+
expect: { kind: 'escrow', taskKey, eventName: 'ReleasedV2', expectEscrowState: 'RELEASED' },
|
|
222
|
+
recorded: { blockNumber: releaseRec.blockNumber, confirmations: releaseRec.confirmations, status: releaseRec.status },
|
|
223
|
+
});
|
|
224
|
+
const recNow = await readTransaction(transactionId, opts.home);
|
|
225
|
+
const delivered = deliveredContent || (() => { try {
|
|
226
|
+
return fs.readFileSync(path.join(opts.home, '.bolloon', 'x402', 'deliveries', `${transactionId}.txt`), 'utf8');
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return '';
|
|
230
|
+
} })();
|
|
231
|
+
if (delivered) {
|
|
232
|
+
const contentHash = computeContentHash(delivered);
|
|
233
|
+
const wr = writeDeliveryContent(transactionId, delivered, opts.home);
|
|
234
|
+
await updateTransaction(transactionId, {
|
|
235
|
+
contentHash, deliveryHash: contentHash, deliveryBytesHash: wr.hash, protocolVerified: true,
|
|
236
|
+
event: { kind: 'delivery_hash', detail: `deliveryHash=${contentHash.slice(0, 14)}… (与链上 resultHash 同源)` },
|
|
237
|
+
}, opts.home);
|
|
238
|
+
}
|
|
239
|
+
const gated = await applyChainSettlementToTransaction({
|
|
240
|
+
rec: (await readTransaction(transactionId, opts.home)),
|
|
241
|
+
home: opts.home, verdict, taskKey,
|
|
242
|
+
execution: recNow?.execution,
|
|
243
|
+
goalCriteriaMet: outChk.ok && sources.length > 0 && !!conclusion,
|
|
244
|
+
receipt: `chain-release:${releaseRec.txHash}`,
|
|
245
|
+
});
|
|
246
|
+
gateVerified = gated.verified;
|
|
247
|
+
gateReason = gated.verified ? gated.reason : `${gated.reason} (${gated.blockedBy.join(' | ')})`;
|
|
248
|
+
}
|
|
249
|
+
else if (!loopVerified) {
|
|
250
|
+
gateReason = `${loopReason} —— 链上没确定结算 → 不标 verified`;
|
|
251
|
+
}
|
|
252
|
+
const evidenceComplete = fidelity.ok && (execResult?.ok === true) && outChk.ok && sources.length > 0 && !!conclusion && gateVerified;
|
|
253
|
+
const card = buildReportCard({
|
|
254
|
+
task: opts.task,
|
|
255
|
+
conclusion: evidenceComplete ? (conclusion || '已完成') : '证据不足',
|
|
256
|
+
conclusionDetail: evidenceComplete ? undefined : ((outChk.issues || [])[0] || loopReason.slice(0, 120)),
|
|
257
|
+
skill: { name: chosen.name, version: chosen.version, dir: chosen.dir },
|
|
258
|
+
cost: { amount: amountChk.amountUsdc, currency: 'USDC', network: opts.network || 'unknown' },
|
|
259
|
+
payment,
|
|
260
|
+
sources,
|
|
261
|
+
outputContract: outChk.ok ? '通过' : '未通过',
|
|
262
|
+
resourceVerified: fidelity.ok ? '通过' : '未通过',
|
|
263
|
+
evidenceComplete,
|
|
264
|
+
executed: execResult?.ok === true,
|
|
265
|
+
paid: !!txHashes.create,
|
|
266
|
+
stage: 'report',
|
|
267
|
+
durationMs: stages.reduce((a, s) => a + s.ms, 0),
|
|
268
|
+
evidenceRef: { transactionId },
|
|
269
|
+
budgetLines: [...budget.why, ...amountChk.why],
|
|
270
|
+
blocker: evidenceComplete ? undefined : (loopVerified ? (outChk.ok ? undefined : `输出不符合资源契约: ${(outChk.issues || [])[0] || '未知'}`) : `链上未成立: ${loopReason.slice(0, 140)}`),
|
|
271
|
+
});
|
|
272
|
+
mark('report', card.status);
|
|
273
|
+
return {
|
|
274
|
+
ok: card.status === '已完成',
|
|
275
|
+
taskKey, requestId, transactionId, card, text: renderReportCard(card),
|
|
276
|
+
skill: { name: chosen.name, version: chosen.version, dir: chosen.dir },
|
|
277
|
+
amountUsdc: amountChk.amountUsdc, budget,
|
|
278
|
+
chain: { verified: gateVerified, reason: gateReason, ...txHashes, statuses },
|
|
279
|
+
execution: execResult,
|
|
280
|
+
outputIssues,
|
|
281
|
+
recovery: afterRecovery,
|
|
282
|
+
stages,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
@@ -244,6 +244,42 @@ function makeTracker(onEvent) {
|
|
|
244
244
|
return async (e) => { if (onEvent)
|
|
245
245
|
await onEvent(e).catch(() => null); };
|
|
246
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* ★ F5 修复点。
|
|
249
|
+
* 老代码: `chainSettled = !!txHash` —— 拿到 txHash 就宣称链上已验证 (从不读 receipt/事件/确认数)。
|
|
250
|
+
* 新逻辑:
|
|
251
|
+
* · 没有 txHash → `not_attempted`, chainSettled=false (没发过链上交易)
|
|
252
|
+
* · 读不到 RPC / receipt → `unknown`, chainSettled=false (不确定 ≠ 没付)
|
|
253
|
+
* · receipt.status == 0 → `reverted`, chainSettled=false
|
|
254
|
+
* · 确认数 < 门槛 → `pending`, chainSettled=false
|
|
255
|
+
* · 事件(taskKey/resultHash)对不上 → `event_mismatch`, chainSettled=false
|
|
256
|
+
* · 全过 → `confirmed`/`finalized`, chainSettled=true
|
|
257
|
+
* 验证器本身抛错也**不会**炸付款路径: 一律降级成 `unknown` + chainSettled=false (fail-closed)。
|
|
258
|
+
*/
|
|
259
|
+
export async function verifyPaymentOnChain(args) {
|
|
260
|
+
const gate = args.chainSettlement?.gate || 'confirmed';
|
|
261
|
+
const txHash = String(args.txHash || '');
|
|
262
|
+
if (!txHash) {
|
|
263
|
+
return {
|
|
264
|
+
chainSettled: false, status: 'not_attempted',
|
|
265
|
+
reason: 'facilitator 说成功但没给 txHash: 没有链上交易可查, 不能认定链上结算完成',
|
|
266
|
+
confirmationsRequired: 0, requiredGate: gate, rpcAvailable: true, checkedAt: Date.now(), evidence: {},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const verifier = args.chainSettlement?.verifier
|
|
271
|
+
?? (await import('../chain/chain-settlement.js')).createDefaultChainSettlementVerifier({ gate });
|
|
272
|
+
return await verifier({ txHash, expect: args.chainSettlement?.expect, gate, recorded: args.chainSettlement?.recorded });
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
return {
|
|
276
|
+
chainSettled: false, status: 'unknown',
|
|
277
|
+
reason: `链上验证器抛错 → 不能判已结算: ${String(e?.message || e).slice(0, 200)}`,
|
|
278
|
+
txHash, confirmationsRequired: 0, requiredGate: gate, rpcAvailable: false,
|
|
279
|
+
checkedAt: Date.now(), evidence: { verifierError: String(e?.message || e).slice(0, 200) },
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
247
283
|
export async function buyInfo(params) {
|
|
248
284
|
const { verifyEnvelope } = await import('./paid-info-protocol.js');
|
|
249
285
|
const doFetch = params.fetchImpl ?? fetch;
|
|
@@ -299,26 +335,59 @@ export async function buyInfo(params) {
|
|
|
299
335
|
const text = await retry.text();
|
|
300
336
|
const parsed = safeJson(text);
|
|
301
337
|
const receipt = retry.headers.get('x-payment-response') || parsed?.payment?.receipt || '';
|
|
338
|
+
// ★ 链上事实需要 txHash: facilitator 说成功但没有 txHash → 不能标 chainSettled (真跑抓到过这类"假结算")
|
|
339
|
+
const txHash = parsed?.payment?.txHash || retry.headers.get('x-payment-txhash') || parsed?.txHash || '';
|
|
302
340
|
if (retry.status < 200 || retry.status >= 300) {
|
|
303
341
|
// 付款这一步已经发出去了 (可能已上链), 只是拿资源失败 → 绝不许当"没付过钱"
|
|
342
|
+
// ★ F5: 有 txHash 就去链上真查一遍, 而不是"有 txHash 就算结算"
|
|
343
|
+
const v = await verifyPaymentOnChain({ txHash, chainSettlement: params.chainSettlement });
|
|
304
344
|
return {
|
|
305
345
|
ok: false, status: retry.status,
|
|
306
|
-
payment: {
|
|
346
|
+
payment: {
|
|
347
|
+
mode: 'facilitator', receipt: receipt || undefined, attempted: true, settled: true,
|
|
348
|
+
settlementUncertain: !receipt && !v.chainSettled,
|
|
349
|
+
chainSettled: v.chainSettled, chainSettlementStatus: v.status, chainSettlementReason: v.reason,
|
|
350
|
+
confirmations: v.confirmations, confirmationsRequired: v.confirmationsRequired, rpcAvailable: v.rpcAvailable,
|
|
351
|
+
...(txHash ? { txHash } : {}),
|
|
352
|
+
},
|
|
307
353
|
error: `付款后重试失败 ${retry.status}: ${text.slice(0, 200)}`,
|
|
308
354
|
};
|
|
309
355
|
}
|
|
310
356
|
mode = 'facilitator';
|
|
311
|
-
// ★
|
|
312
|
-
const
|
|
357
|
+
// ★ F5 修复: 拿到 txHash ≠ 链上已验证。真验证 = receipt(status==1) + 确认数 + 事件(taskKey/resultHash) + 合约 escrow 状态。
|
|
358
|
+
const verdict = await verifyPaymentOnChain({ txHash, chainSettlement: params.chainSettlement });
|
|
313
359
|
await trackEvent({
|
|
314
360
|
kind: 'settled',
|
|
315
|
-
detail: `mode=facilitator receipt=${receipt.slice(0, 24)}… txHash=${txHash ? `${String(txHash).slice(0, 16)}…` : '(缺失: 不能认定链上结算完成)'}
|
|
316
|
-
|
|
361
|
+
detail: `mode=facilitator receipt=${receipt.slice(0, 24)}… txHash=${txHash ? `${String(txHash).slice(0, 16)}…` : '(缺失: 不能认定链上结算完成)'} 链上判定=${verdict.status}` +
|
|
362
|
+
(verdict.confirmations != null ? ` 确认数=${verdict.confirmations}/${verdict.confirmationsRequired}` : ''),
|
|
363
|
+
patch: {
|
|
364
|
+
paymentMode: 'facilitator', paymentReceipt: receipt,
|
|
365
|
+
chainSettled: verdict.chainSettled,
|
|
366
|
+
chainSettlementStatus: verdict.status,
|
|
367
|
+
chainSettlementReason: verdict.reason,
|
|
368
|
+
...(verdict.confirmations != null ? { chainConfirmations: verdict.confirmations } : {}),
|
|
369
|
+
...(verdict.rpcAvailable ? {} : { chainRpcAvailable: false }),
|
|
370
|
+
...(txHash ? { txHash: String(txHash) } : {}),
|
|
371
|
+
},
|
|
317
372
|
});
|
|
318
373
|
const report = parsed?.proof ? await verifyEnvelope(parsed, { resolveDid: params.resolveDid, expectItemId: params.expectItemId }) : undefined;
|
|
319
374
|
if (report)
|
|
320
375
|
await trackEvent({ kind: 'delivered', detail: `trust=${report.trust}`, patch: { verificationTrust: report.trust, contentHash: parsed?.contentHash, protocolVerified: report.trust === 'verified' } });
|
|
321
|
-
return {
|
|
376
|
+
return {
|
|
377
|
+
ok: true, status: retry.status, envelope: parsed, verify: report, metadata, raw: text,
|
|
378
|
+
payment: {
|
|
379
|
+
mode, receipt,
|
|
380
|
+
...(txHash ? { txHash } : {}),
|
|
381
|
+
attempted: true, settled: true,
|
|
382
|
+
settlementUncertain: verdict.status === 'unknown' || verdict.status === 'config_unavailable' || !verdict.rpcAvailable,
|
|
383
|
+
chainSettled: verdict.chainSettled,
|
|
384
|
+
chainSettlementStatus: verdict.status,
|
|
385
|
+
chainSettlementReason: verdict.reason,
|
|
386
|
+
confirmations: verdict.confirmations,
|
|
387
|
+
confirmationsRequired: verdict.confirmationsRequired,
|
|
388
|
+
rpcAvailable: verdict.rpcAvailable,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
322
391
|
}
|
|
323
392
|
if (!params.allowLocalDev) {
|
|
324
393
|
return { ok: false, status: 402, error: '需要钱包私钥才能支付 (未提供 privateKey; 本机联调请显式 allowLocalDev)' };
|