@bolloon/bolloon-agent 0.4.25 → 0.4.27
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/execution-supervisor.js +87 -2
- package/dist/agents/external-events.js +162 -0
- package/dist/agents/goal-criteria.js +124 -0
- package/dist/agents/goal-store.js +79 -4
- package/dist/agents/p2p-info.js +175 -0
- package/dist/agents/pi-sdk.js +39 -0
- package/dist/agents/run-store.js +15 -0
- package/dist/agents/skill-readiness.js +133 -0
- package/dist/agents/skill-supervisor-link.js +70 -0
- package/dist/agents/skills-manager.js +282 -0
- package/dist/agents/trace-export.js +125 -0
- package/dist/agents/write-staging.js +12 -4
- package/dist/agents/x402/goal-run-bridge.js +105 -0
- package/dist/agents/x402/milestone-settlement.js +150 -0
- package/dist/agents/x402/paid-info-store.js +66 -12
- package/dist/agents/x402/payment-recovery.js +290 -0
- package/dist/agents/x402/resource-contract.js +473 -0
- package/dist/agents/x402/settlement-state.js +378 -0
- package/dist/agents/x402/trade.js +257 -0
- package/dist/agents/x402/transaction-protocol.js +99 -0
- package/dist/agents/x402/transaction-store.js +350 -0
- package/dist/cli/setup-wizard.js +96 -127
- package/dist/cli-entry.js +74 -0
- package/dist/electron/first-run.js +33 -2
- package/dist/electron-build/electron/first-run.js +35 -2
- package/dist/electron-build/electron/first-run.js.map +1 -1
- package/dist/index.js +224 -4
- package/dist/llm/config-store.js +35 -4
- package/dist/network/agent-network.js +10 -0
- package/dist/network/goal-event-bridge.js +57 -0
- package/dist/setup/onboard.js +549 -0
- package/dist/setup/setup-store.js +592 -0
- package/dist/web/routes-x402-info.js +1 -0
- package/dist/web/server.js +330 -1
- package/package.json +1 -1
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settlement-state.ts — 交易的两层状态 + 结算事实 + 责任候选 (Phase 0 冻结, 2026-09-18)
|
|
3
|
+
*
|
|
4
|
+
* leo 的规格 (`docs/design-layer2.md` + 2026-09-18 交易闭环完成批次):
|
|
5
|
+
* 生命周期 (这笔交易走到哪一步) 与 **结算事实** (钱到底动没动) 必须**分开记**,
|
|
6
|
+
* 否则表达不了这些真实组合:
|
|
7
|
+
* `paying + payment_submitted` → 发出去了, 还没有回执
|
|
8
|
+
* `settled + fully_settled` → 钱真到了, 但正文还没验
|
|
9
|
+
* `settled + unknown` → facilitator 说成了, 链上还没确认
|
|
10
|
+
* `delivery_failed + fully_settled` → 钱付了, 正文没交 → 不能重付, 只能追责
|
|
11
|
+
*
|
|
12
|
+
* 三条不可混淆的红线:
|
|
13
|
+
* ① `local-dev` 永远不能产生 `fully_settled` (链上没动过钱)
|
|
14
|
+
* ② `chainSettled !== true` 永远不能 `verified`
|
|
15
|
+
* ③ 非法状态迁移**拒绝**并给出原因, **不静默修正**
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { sha256Hex } from './paid-info-protocol.js';
|
|
20
|
+
// ── ① 生命周期 (10 态; 与 transaction-protocol 的 status 同一层) ─────────────
|
|
21
|
+
export const LIFECYCLE_STATUSES = [
|
|
22
|
+
'discovered',
|
|
23
|
+
'quoted',
|
|
24
|
+
'policy_denied',
|
|
25
|
+
'payment_required',
|
|
26
|
+
'paying',
|
|
27
|
+
'settled',
|
|
28
|
+
'delivered',
|
|
29
|
+
'verified',
|
|
30
|
+
'delivery_failed',
|
|
31
|
+
'verification_failed',
|
|
32
|
+
'disputed', // Phase 4: 争议 — 自动化到此为止, 钱的归宿在结算层 (refund_pending/refunded)
|
|
33
|
+
];
|
|
34
|
+
/** 旧记录里可能存在、仍必须可读的状态 (迁移时保留原值, 不假装它从没出现过) */
|
|
35
|
+
export const LEGACY_STATUSES = ['failed'];
|
|
36
|
+
export function isLifecycleStatus(v) {
|
|
37
|
+
return typeof v === 'string' && LIFECYCLE_STATUSES.includes(v);
|
|
38
|
+
}
|
|
39
|
+
export function isKnownStatus(v) {
|
|
40
|
+
return isLifecycleStatus(v) || (typeof v === 'string' && LEGACY_STATUSES.includes(v));
|
|
41
|
+
}
|
|
42
|
+
/** 终态: 走进去就不再自动变化 (delivery_failed 不是"还能重付"的意思) */
|
|
43
|
+
export const TERMINAL_LIFECYCLE = ['verified', 'delivery_failed', 'verification_failed', 'policy_denied', 'disputed'];
|
|
44
|
+
/**
|
|
45
|
+
* 允许的迁移表。没列出来的**一律拒绝**。
|
|
46
|
+
* 特别注意两条容易走错的:
|
|
47
|
+
* - `paying → payment_required` 只在对账确认"没有支付证据"时允许 (回退重试的唯一合法路径)
|
|
48
|
+
* - `delivery_failed`/`verification_failed` 不能回到 `paying` (钱已经付过了)
|
|
49
|
+
*/
|
|
50
|
+
const LIFECYCLE_TRANSITIONS = {
|
|
51
|
+
// 免费资源没有付款环节 → discovered 可直接 delivered (但永远不会 verified)
|
|
52
|
+
discovered: ['quoted', 'delivered', 'failed', 'policy_denied'],
|
|
53
|
+
quoted: ['paying', 'payment_required', 'delivered', 'failed', 'policy_denied'],
|
|
54
|
+
policy_denied: [],
|
|
55
|
+
payment_required: ['paying', 'failed', 'policy_denied'],
|
|
56
|
+
// 付款中 → 结算/交付/验真各结果, 或"对账发现没付过"退回 payment_required
|
|
57
|
+
paying: ['settled', 'delivered', 'verified', 'delivery_failed', 'verification_failed', 'payment_required', 'failed', 'disputed'],
|
|
58
|
+
settled: ['delivered', 'verified', 'delivery_failed', 'verification_failed', 'disputed'],
|
|
59
|
+
delivered: ['verified', 'delivery_failed', 'verification_failed', 'disputed'],
|
|
60
|
+
verified: [],
|
|
61
|
+
delivery_failed: ['disputed'], // 付了钱但没交付 → 可以进争议
|
|
62
|
+
verification_failed: ['disputed'], // 验真不过 → 可以进争议
|
|
63
|
+
disputed: [], // 终态: 收尾只能通过 resolveDispute + 结算层退款状态
|
|
64
|
+
failed: [], // 旧状态: 终态
|
|
65
|
+
};
|
|
66
|
+
export function canTransitionLifecycle(from, to) {
|
|
67
|
+
if (!isKnownStatus(from))
|
|
68
|
+
return { ok: false, reason: `当前状态不可识别: ${from}` };
|
|
69
|
+
if (!isKnownStatus(to))
|
|
70
|
+
return { ok: false, reason: `目标状态不可识别: ${to}` };
|
|
71
|
+
if (from === to)
|
|
72
|
+
return { ok: true, noop: true };
|
|
73
|
+
const allowed = LIFECYCLE_TRANSITIONS[from] || [];
|
|
74
|
+
if (!allowed.includes(to)) {
|
|
75
|
+
const why = TERMINAL_LIFECYCLE.includes(from) ? ` (${from} 是终态, 不许再变)` : '';
|
|
76
|
+
return { ok: false, reason: `非法迁移 ${from} → ${to}${why}; 允许: ${allowed.length ? allowed.join(', ') : '(无, 终态)'}` };
|
|
77
|
+
}
|
|
78
|
+
return { ok: true };
|
|
79
|
+
}
|
|
80
|
+
/** 这笔记录是否**已经有支付证据** (付过钱 / 可能付过钱) */
|
|
81
|
+
export function hasPaymentEvidence(rec) {
|
|
82
|
+
if (rec.chainSettled === true)
|
|
83
|
+
return true;
|
|
84
|
+
if (rec.txHash || rec.paymentReceipt)
|
|
85
|
+
return true;
|
|
86
|
+
const fact = String(rec.settlementFact || '');
|
|
87
|
+
if (isSettlementFact(fact) && fact !== 'unpaid' && fact !== 'unknown')
|
|
88
|
+
return true;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
/** verified 的**硬证据**子集 (完整门见 evaluateVerifiedGate: 还要求正文在盘上 + 执行 + 判据) */
|
|
92
|
+
export function checkVerifiedPreconditions(rec) {
|
|
93
|
+
if (rec.chainSettled !== true)
|
|
94
|
+
return { ok: false, reason: 'chainSettled !== true: 链上没有真实结算, 不能判 verified (联调最高 delivered)' };
|
|
95
|
+
if (rec.protocolVerified !== true)
|
|
96
|
+
return { ok: false, reason: 'protocolVerified !== true: 协议验真未通过' };
|
|
97
|
+
if (!rec.contentHash || !rec.deliveryHash)
|
|
98
|
+
return { ok: false, reason: '缺少内容哈希或交付哈希' };
|
|
99
|
+
if (rec.contentHash !== rec.deliveryHash)
|
|
100
|
+
return { ok: false, reason: '内容哈希与交付哈希不一致 (内容被换过)' };
|
|
101
|
+
if (!rec.receiptHash)
|
|
102
|
+
return { ok: false, reason: '缺少支付回执哈希 (回执与信封未绑定)' };
|
|
103
|
+
const fact = String(rec.settlementFact || '');
|
|
104
|
+
if (fact && fact !== 'fully_settled' && fact !== 'partially_settled') {
|
|
105
|
+
return { ok: false, reason: `结算事实是 ${fact}, 不足以判 verified` };
|
|
106
|
+
}
|
|
107
|
+
return { ok: true };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 争议的三条禁令 (Phase 4, leo 原话): 不能自动重付 · 不能标 verified · **不能静默关闭**。
|
|
111
|
+
* 纯函数 + 无外部依赖, 所以写路径 (`checkLifecycleMove`) 和验收都能直接用同一份判断。
|
|
112
|
+
*/
|
|
113
|
+
export function disputeForbids(rec, target) {
|
|
114
|
+
const d = rec.dispute;
|
|
115
|
+
if (!d)
|
|
116
|
+
return { ok: true };
|
|
117
|
+
if (target === 'paying' || target === 'payment_required') {
|
|
118
|
+
return { ok: false, reason: '争议期间不允许重新付款 (禁令 1: 不能自动重付)' };
|
|
119
|
+
}
|
|
120
|
+
if (target === 'verified') {
|
|
121
|
+
return { ok: false, reason: '争议期间不允许标 verified (禁令 2: 争议不能标成功)' };
|
|
122
|
+
}
|
|
123
|
+
if (!d.resolution && String(rec.status) === 'disputed') {
|
|
124
|
+
return { ok: false, reason: '争议只能通过 resolveDispute 显式收尾 (禁令 3: 不能静默关闭)' };
|
|
125
|
+
}
|
|
126
|
+
return { ok: true };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 记录级的迁移许可 (在 from→to 表之上再加两条安全规则):
|
|
130
|
+
* ① 已经有支付证据的交易**不许**被标成普通 `failed` (钱不能凭空消失) → 该记 delivery_failed / verification_failed, 或回 payment_required 走对账
|
|
131
|
+
* ② 目标 `verified` 必须过硬证据子集 (链上结算 + 协议验真 + 哈希 + 回执绑定)
|
|
132
|
+
*/
|
|
133
|
+
export function checkLifecycleMove(rec, to) {
|
|
134
|
+
const base = canTransitionLifecycle(String(rec.status), to);
|
|
135
|
+
if (!base.ok)
|
|
136
|
+
return base;
|
|
137
|
+
if (base.noop)
|
|
138
|
+
return base;
|
|
139
|
+
const disputeCheck = disputeForbids(rec, to);
|
|
140
|
+
if (!disputeCheck.ok)
|
|
141
|
+
return disputeCheck;
|
|
142
|
+
if (to === 'failed' && hasPaymentEvidence(rec)) {
|
|
143
|
+
return { ok: false, reason: '这笔交易已有支付证据 (txHash/回执/链上结算) → 不许标成普通 failed; 应付交付失败用 delivery_failed, 验真失败用 verification_failed, 状态不明先回 payment_required 对账' };
|
|
144
|
+
}
|
|
145
|
+
if (to === 'verified') {
|
|
146
|
+
const pre = checkVerifiedPreconditions(rec);
|
|
147
|
+
if (!pre.ok)
|
|
148
|
+
return pre;
|
|
149
|
+
}
|
|
150
|
+
return { ok: true };
|
|
151
|
+
}
|
|
152
|
+
// ── ② 结算事实 (钱动没动; 与生命周期分开记) ──────────────────────────────────
|
|
153
|
+
export const SETTLEMENT_FACTS = [
|
|
154
|
+
'unpaid',
|
|
155
|
+
'payment_submitted',
|
|
156
|
+
'payment_verified',
|
|
157
|
+
'partially_settled',
|
|
158
|
+
'fully_settled',
|
|
159
|
+
'refund_pending',
|
|
160
|
+
'refunded',
|
|
161
|
+
'unknown',
|
|
162
|
+
];
|
|
163
|
+
export function isSettlementFact(v) {
|
|
164
|
+
return typeof v === 'string' && SETTLEMENT_FACTS.includes(v);
|
|
165
|
+
}
|
|
166
|
+
const SETTLEMENT_TRANSITIONS = {
|
|
167
|
+
unpaid: ['payment_submitted', 'unknown'],
|
|
168
|
+
payment_submitted: ['payment_verified', 'partially_settled', 'unpaid', 'unknown'],
|
|
169
|
+
payment_verified: ['fully_settled', 'partially_settled', 'refund_pending', 'unknown'],
|
|
170
|
+
partially_settled: ['fully_settled', 'refund_pending', 'unknown'],
|
|
171
|
+
fully_settled: ['refund_pending'],
|
|
172
|
+
refund_pending: ['refunded', 'fully_settled'],
|
|
173
|
+
refunded: [],
|
|
174
|
+
unknown: ['unpaid', 'payment_submitted', 'payment_verified', 'partially_settled', 'fully_settled', 'refund_pending'],
|
|
175
|
+
};
|
|
176
|
+
/** 只有真链上/facilitator 确认过的结算事实 */
|
|
177
|
+
export const CHAIN_BACKED_FACTS = ['payment_verified', 'partially_settled', 'fully_settled'];
|
|
178
|
+
/** local-dev 能到的最高结算事实 (模拟提交 ≠ 链上事实) */
|
|
179
|
+
export const LOCAL_DEV_MAX_FACT = 'payment_submitted';
|
|
180
|
+
export function canTransitionSettlement(from, to, opts = {}) {
|
|
181
|
+
if (!isSettlementFact(from))
|
|
182
|
+
return { ok: false, reason: `当前结算事实不可识别: ${from}` };
|
|
183
|
+
if (!isSettlementFact(to))
|
|
184
|
+
return { ok: false, reason: `目标结算事实不可识别: ${to}` };
|
|
185
|
+
if (from === to)
|
|
186
|
+
return { ok: true, noop: true };
|
|
187
|
+
if (opts.paymentMode === 'local-dev' && CHAIN_BACKED_FACTS.includes(to)) {
|
|
188
|
+
return { ok: false, reason: `本机联调 (local-dev) 不能产生 ${to}: 链上没有真实结算, 联调最高只能是 ${LOCAL_DEV_MAX_FACT}` };
|
|
189
|
+
}
|
|
190
|
+
const allowed = SETTLEMENT_TRANSITIONS[from] || [];
|
|
191
|
+
if (!allowed.includes(to)) {
|
|
192
|
+
// 例外: 对账时拿到**链上事实** (txHash + chainSettled) 可以一步到位 fully_settled, 不必绕 payment_verified
|
|
193
|
+
const chainProof = opts.chainSettled === true && !!opts.txHash;
|
|
194
|
+
// ★ 例外只对"还没到链上口径"的事实生效; 退款状态是终态, 不许被链上证据绕回结算
|
|
195
|
+
// (真跑抓到过: refunded → fully_settled 被这条例外放行 = 钱退出去又算结算)
|
|
196
|
+
const refundTerminal = from === 'refunded' || from === 'refund_pending';
|
|
197
|
+
if (to === 'fully_settled' && chainProof && !refundTerminal && !CHAIN_BACKED_FACTS.includes(from)) {
|
|
198
|
+
return { ok: true };
|
|
199
|
+
}
|
|
200
|
+
return { ok: false, reason: `非法结算迁移 ${from} → ${to}; 允许: ${allowed.length ? allowed.join(', ') : '(无, 终态)'}${to === 'fully_settled' ? ' (一步到 fully_settled 需要链上证据: chainSettled=true + txHash)' : ''}` };
|
|
201
|
+
}
|
|
202
|
+
return { ok: true };
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 从记录既有证据**推导**结算事实 (给老记录迁移用; 不猜没有证据的事)。
|
|
206
|
+
* 推导不出确定事实时给 `unknown` —— 诚实表达"不知道钱动没动"。
|
|
207
|
+
*/
|
|
208
|
+
export function deriveSettlementFact(rec) {
|
|
209
|
+
const paid = ['paying', 'settled', 'delivered', 'verified', 'delivery_failed', 'verification_failed'].includes(String(rec.status));
|
|
210
|
+
if (rec.paymentMode === 'local-dev') {
|
|
211
|
+
// 联调: 有付款动作 → 最高只认"提交过(模拟)"; 没有 → 未付
|
|
212
|
+
return paid && (rec.paymentReceipt || rec.txHash) ? LOCAL_DEV_MAX_FACT : (paid ? 'unknown' : 'unpaid');
|
|
213
|
+
}
|
|
214
|
+
if (rec.chainSettled === true)
|
|
215
|
+
return String(rec.status) === 'verified' ? 'fully_settled' : 'payment_verified';
|
|
216
|
+
if (rec.txHash || rec.paymentReceipt)
|
|
217
|
+
return 'payment_submitted';
|
|
218
|
+
if (String(rec.status) === 'paying')
|
|
219
|
+
return 'unknown'; // 付过没付过不知道 → 先对账
|
|
220
|
+
return 'unpaid';
|
|
221
|
+
}
|
|
222
|
+
// ── ③ 责任候选 (机器只给候选, 不做最终赔偿判决) ─────────────────────────────
|
|
223
|
+
export const RESPONSIBILITY_TYPES = [
|
|
224
|
+
'provider_fault',
|
|
225
|
+
'buyer_fault',
|
|
226
|
+
'agent_fault',
|
|
227
|
+
'platform_fault',
|
|
228
|
+
'payment_infrastructure_fault',
|
|
229
|
+
'undetermined',
|
|
230
|
+
];
|
|
231
|
+
/**
|
|
232
|
+
* 顺序即优先级: 先看"谁给的东西不对"(卖方), 再看"买方/agent 做错了什么",
|
|
233
|
+
* 再看基础设施, 最后证据不足认 `undetermined` —— 不许在证据不足时随便归责。
|
|
234
|
+
*/
|
|
235
|
+
export function deriveResponsibility(ev) {
|
|
236
|
+
const hits = [];
|
|
237
|
+
if (ev.deliveryHashMismatch) {
|
|
238
|
+
hits.push('deliveryHashMismatch');
|
|
239
|
+
}
|
|
240
|
+
if (ev.signatureInvalid) {
|
|
241
|
+
hits.push('signatureInvalid');
|
|
242
|
+
}
|
|
243
|
+
if (ev.outputSchemaViolation) {
|
|
244
|
+
hits.push('outputSchemaViolation');
|
|
245
|
+
}
|
|
246
|
+
if (hits.length) {
|
|
247
|
+
return {
|
|
248
|
+
type: 'provider_fault',
|
|
249
|
+
reason: `卖方交付物与声明不一致 (${hits.join(', ')})`,
|
|
250
|
+
evidence: hits,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (ev.inputSchemaViolation) {
|
|
254
|
+
return { type: 'buyer_fault', reason: '输入不符合资源 inputSchema', evidence: ['inputSchemaViolation'] };
|
|
255
|
+
}
|
|
256
|
+
if (ev.policyBypassed) {
|
|
257
|
+
return { type: 'agent_fault', reason: 'agent 越过 Policy 付款/执行', evidence: ['policyBypassed'] };
|
|
258
|
+
}
|
|
259
|
+
if (ev.receiptMissing) {
|
|
260
|
+
return { type: 'payment_infrastructure_fault', reason: '缺少支付回执 (回执与信封未绑定)', evidence: ['receiptMissing'] };
|
|
261
|
+
}
|
|
262
|
+
if (ev.ledgerLostOrDoubleCharged) {
|
|
263
|
+
return { type: 'platform_fault', reason: '交易记录丢失或重复扣款', evidence: ['ledgerLostOrDoubleCharged'] };
|
|
264
|
+
}
|
|
265
|
+
if (ev.providerInfrastructureUnhealthy) {
|
|
266
|
+
return { type: 'payment_infrastructure_fault', reason: 'facilitator / RPC 状态异常', evidence: ['providerInfrastructureUnhealthy'] };
|
|
267
|
+
}
|
|
268
|
+
if (ev.deliveryMissing) {
|
|
269
|
+
return { type: 'undetermined', reason: '付了钱但没拿到正文: 传输或卖方未交付, 证据不足以定责', evidence: ['deliveryMissing'] };
|
|
270
|
+
}
|
|
271
|
+
if (ev.executionFailed) {
|
|
272
|
+
// 执行失败但交付物本身合契约 → 不足以归责卖方, 给不确定
|
|
273
|
+
return { type: 'undetermined', reason: '资源执行失败, 但证据不足以判归属', evidence: ['executionFailed'] };
|
|
274
|
+
}
|
|
275
|
+
return { type: 'undetermined', reason: '证据不足, 不做归责', evidence: [] };
|
|
276
|
+
}
|
|
277
|
+
// ── ④ 正文实体 (验真要能重新算哈希, 不是信一个布尔) ────────────────────────
|
|
278
|
+
export function deliveriesDir(home) {
|
|
279
|
+
return path.join(home, '.bolloon', 'x402', 'deliveries');
|
|
280
|
+
}
|
|
281
|
+
function deliveryPath(transactionId, home) {
|
|
282
|
+
return path.join(deliveriesDir(home), `${transactionId}.txt`);
|
|
283
|
+
}
|
|
284
|
+
/** 交付正文落盘 (验真时重算哈希; 正文不进交易记录, 只进哈希与路径) */
|
|
285
|
+
export function writeDeliveryContent(transactionId, content, home) {
|
|
286
|
+
const dir = deliveriesDir(home);
|
|
287
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
288
|
+
const p = deliveryPath(transactionId, home);
|
|
289
|
+
const body = String(content ?? '');
|
|
290
|
+
fs.writeFileSync(p, body, 'utf8');
|
|
291
|
+
return { path: p, hash: sha256Hex(body), bytes: Buffer.byteLength(body, 'utf8') };
|
|
292
|
+
}
|
|
293
|
+
export function readDeliveryContent(transactionId, home) {
|
|
294
|
+
try {
|
|
295
|
+
return fs.readFileSync(deliveryPath(transactionId, home), 'utf8');
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* 正文真的在盘上, 且与我们交付时记下的**字节哈希**一致 (验真时重算, 不信记录里的自述)。
|
|
303
|
+
* 注意: 协议层的 contentHash/deliveryHash 是规范化哈希 (computeContentHash), 与字节哈希是两套,
|
|
304
|
+
* 各自独立校验, 不互相冒充。
|
|
305
|
+
*/
|
|
306
|
+
export function verifyDelivery(rec, home) {
|
|
307
|
+
const body = readDeliveryContent(rec.transactionId, home);
|
|
308
|
+
if (body === null)
|
|
309
|
+
return { present: false, matchesRecorded: false, reason: '交付正文不在盘上 (没交付, 或被删)' };
|
|
310
|
+
const bytesHash = sha256Hex(body);
|
|
311
|
+
const bytes = Buffer.byteLength(body, 'utf8');
|
|
312
|
+
const recorded = rec.deliveryBytesHash;
|
|
313
|
+
if (!recorded)
|
|
314
|
+
return { present: true, bytes, bytesHash, matchesRecorded: false, reason: '记录里没有交付字节哈希 (无法证明盘上正文就是当时交付的那份)' };
|
|
315
|
+
if (recorded !== bytesHash)
|
|
316
|
+
return { present: true, bytes, bytesHash, matchesRecorded: false, reason: `盘上正文哈希 ${bytesHash.slice(0, 12)}… ≠ 交付时记录 ${String(recorded).slice(0, 12)}…` };
|
|
317
|
+
return { present: true, bytes, bytesHash, matchesRecorded: true };
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* leo 的最终门: chainSettled + protocolVerified + 正文在 + 哈希对 + 回执绑定
|
|
321
|
+
* + 资源执行成功 + 输出满足契约 + Goal 判据命中
|
|
322
|
+
*/
|
|
323
|
+
export function evaluateVerifiedGate(input) {
|
|
324
|
+
const { rec, home } = input;
|
|
325
|
+
const missing = [];
|
|
326
|
+
if (rec.chainSettled !== true)
|
|
327
|
+
missing.push('chainSettled !== true (链上没有真实结算)');
|
|
328
|
+
if (rec.protocolVerified !== true)
|
|
329
|
+
missing.push('protocolVerified !== true (协议验真没过)');
|
|
330
|
+
const delivery = verifyDelivery(rec, home);
|
|
331
|
+
if (!delivery.present)
|
|
332
|
+
missing.push('交付正文不存在');
|
|
333
|
+
else if (!delivery.matchesRecorded)
|
|
334
|
+
missing.push(`交付正文与交付时记录不一致 (${delivery.reason})`);
|
|
335
|
+
if (!rec.receiptHash)
|
|
336
|
+
missing.push('缺少支付回执哈希 (回执与信封未绑定)');
|
|
337
|
+
if (rec.settlementFact && rec.settlementFact !== 'fully_settled' && rec.settlementFact !== 'partially_settled') {
|
|
338
|
+
missing.push(`结算事实是 ${rec.settlementFact}, 不足以判 verified`);
|
|
339
|
+
}
|
|
340
|
+
// 显式传 null = 调用方明确说"这次没有执行证据"; 传 undefined = 用记录里已有的
|
|
341
|
+
const ex = input.execution !== undefined ? input.execution : (rec.execution ?? null);
|
|
342
|
+
if (!ex)
|
|
343
|
+
missing.push('没有资源执行证据');
|
|
344
|
+
else {
|
|
345
|
+
if (ex.ok !== true)
|
|
346
|
+
missing.push(`资源执行失败${ex.reason ? ` (${ex.reason})` : ''}`);
|
|
347
|
+
if (ex.schemaOk !== true)
|
|
348
|
+
missing.push('资源输出不满足 outputSchema');
|
|
349
|
+
}
|
|
350
|
+
if (input.goalCriteriaMet !== true)
|
|
351
|
+
missing.push('Goal 判据未命中');
|
|
352
|
+
if (missing.length)
|
|
353
|
+
return { verified: false, missing, reason: `未达 verified: ${missing[0]}` };
|
|
354
|
+
return { verified: true, missing: [], reason: '链上结算 + 协议验真 + 正文匹配 + 回执绑定 + 资源执行成功 + 输出合契约 + Goal 判据命中' };
|
|
355
|
+
}
|
|
356
|
+
// ── ⑥ 迁移 (老记录 → 两层; 事件与证据一个不丢) ──────────────────────────────
|
|
357
|
+
export const CURRENT_SCHEMA_VERSION = 2;
|
|
358
|
+
/**
|
|
359
|
+
* 幂等迁移: 补齐 `settlementFact` / `responsibility` / `schemaVersion`,
|
|
360
|
+
* 保留原 `status` 与**全部** events/evidence, 并追加一条 `migrate-v2` 事件说明推导来源。
|
|
361
|
+
* 不覆盖任何已有字段 (除了补默认值)。
|
|
362
|
+
*/
|
|
363
|
+
export function migrateTransactionRecord(input) {
|
|
364
|
+
const rec = { ...input, events: Array.isArray(input.events) ? [...input.events] : [] };
|
|
365
|
+
const before = input.schemaVersion;
|
|
366
|
+
if (before === CURRENT_SCHEMA_VERSION && isSettlementFact(rec.settlementFact)) {
|
|
367
|
+
return { record: rec, changed: false };
|
|
368
|
+
}
|
|
369
|
+
const derived = isSettlementFact(rec.settlementFact) ? rec.settlementFact : deriveSettlementFact(rec);
|
|
370
|
+
rec.settlementFact = derived;
|
|
371
|
+
rec.schemaVersion = CURRENT_SCHEMA_VERSION;
|
|
372
|
+
// 责任候选留空: 没有证据就不写归责 (迁移只补"结算事实"这一层)
|
|
373
|
+
const detail = before === undefined
|
|
374
|
+
? `老记录迁移: 按既有证据推导结算事实 = ${derived} (status=${rec.status}, chainSettled=${rec.chainSettled === true}, mode=${rec.paymentMode})`
|
|
375
|
+
: `schema ${before} → ${CURRENT_SCHEMA_VERSION}: 结算事实 = ${derived}`;
|
|
376
|
+
rec.events.push({ at: new Date().toISOString(), kind: 'migrate-v2', detail });
|
|
377
|
+
return { record: rec, changed: true, note: detail };
|
|
378
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trade.ts — 最小交易闭环编排 (Phase 1/2/4/5, 2026-09-16)
|
|
3
|
+
*
|
|
4
|
+
* 顺序固定 (不可颠倒):
|
|
5
|
+
* 发现报价 → Policy.check → 允许后才解密钱包 → 允许后才签名 → 发起 x402 支付
|
|
6
|
+
* → 交付 → 验真 → 记录
|
|
7
|
+
*
|
|
8
|
+
* 任何一步失败都不许"看起来成功": 策略拒绝 → policy_denied (无签名/无链上交易/无扣预算/无交付);
|
|
9
|
+
* 付了钱但交付不合格 → delivery_failed; 交付了但验真不过 → verification_failed。
|
|
10
|
+
*/
|
|
11
|
+
import * as os from 'os';
|
|
12
|
+
import { validatePaymentRequirements, evaluateTransactionSuccess, computeReceiptHash, } from './transaction-protocol.js';
|
|
13
|
+
import { beginTransaction, updateTransaction, setTransactionStatus, setSettlementFact, readTransaction, findByRequestId, claimPayment, releasePaymentClaim } from './transaction-store.js';
|
|
14
|
+
import { deriveResponsibility, writeDeliveryContent } from './settlement-state.js';
|
|
15
|
+
export async function buyInfoAsTransaction(params) {
|
|
16
|
+
const home = params.home ?? os.homedir();
|
|
17
|
+
const doFetch = params.fetchImpl ?? fetch;
|
|
18
|
+
// 幂等: 同 requestId 已有交易 → 不再付第二次
|
|
19
|
+
const existing = await findByRequestId(params.requestId, home);
|
|
20
|
+
if (existing && ['settled', 'delivered', 'verified', 'delivery_failed', 'verification_failed'].includes(existing.status)) {
|
|
21
|
+
return { ok: existing.status === 'verified' || existing.status === 'delivered', status: existing.status, transactionId: existing.transactionId, record: existing, reused: true, error: existing.failureReason };
|
|
22
|
+
}
|
|
23
|
+
// ① 免费元数据 (不泄露正文; 用来做 itemId/payTo/network/币种 的预期校验)
|
|
24
|
+
let metadata = {};
|
|
25
|
+
let requirements = null;
|
|
26
|
+
let status = 402;
|
|
27
|
+
try {
|
|
28
|
+
const res = await doFetch(params.url, { method: 'GET' });
|
|
29
|
+
status = res.status;
|
|
30
|
+
const body = await res.json().catch(() => null);
|
|
31
|
+
if (res.status === 402) {
|
|
32
|
+
requirements = body?.accepts?.[0] || null;
|
|
33
|
+
// 元数据来源: 显式 metadata/item, 或从 402 要求的 extra 推导 (itemId/providerDid/category)
|
|
34
|
+
metadata = body?.metadata || body?.item || {
|
|
35
|
+
itemId: requirements?.extra?.itemId,
|
|
36
|
+
providerDid: requirements?.extra?.providerDid,
|
|
37
|
+
category: requirements?.extra?.category,
|
|
38
|
+
currency: requirements?.extra?.name,
|
|
39
|
+
network: requirements?.network,
|
|
40
|
+
payTo: requirements?.payTo,
|
|
41
|
+
amount: String(requirements?.amount ?? ''),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
else if (body && (body.item || body.itemId)) {
|
|
45
|
+
metadata = body.item || body;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
const { record } = await beginTransaction({ requestId: params.requestId, metadata: {}, buyerDid: params.buyerDid, goalId: params.goalId, runId: params.runId }, home);
|
|
50
|
+
await setTransactionStatus(record.transactionId, 'failed', `请求失败: ${String(e?.message || e).slice(0, 120)}`, home);
|
|
51
|
+
const rec = await readTransaction(record.transactionId, home);
|
|
52
|
+
return { ok: false, status: 'failed', transactionId: record.transactionId, record: rec, error: `请求失败: ${String(e?.message || e).slice(0, 160)}` };
|
|
53
|
+
}
|
|
54
|
+
const { record, reused } = await beginTransaction({
|
|
55
|
+
requestId: params.requestId, metadata, buyerDid: params.buyerDid,
|
|
56
|
+
providerDid: metadata.providerDid, goalId: params.goalId, runId: params.runId,
|
|
57
|
+
}, home);
|
|
58
|
+
let rec = record;
|
|
59
|
+
// ★ 幂等短路: 这个 requestId 已经有交易走过付款流程 (或已结束) → 直接返回既有事实,
|
|
60
|
+
// 绝不重放"报价→付款"流程 (重放会既重复非幂等操作, 又踩非法迁移)。
|
|
61
|
+
if (reused && ['paying', 'settled', 'delivered', 'verified', 'delivery_failed', 'verification_failed', 'policy_denied', 'failed'].includes(String(record.status))) {
|
|
62
|
+
return {
|
|
63
|
+
ok: record.status === 'verified' || record.status === 'delivered',
|
|
64
|
+
status: record.status,
|
|
65
|
+
transactionId: record.transactionId,
|
|
66
|
+
record,
|
|
67
|
+
reused: true,
|
|
68
|
+
error: `该 requestId 已有交易处于 ${record.status} → 复用既有交易, 不重放付款`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (status !== 402) {
|
|
72
|
+
// 免费资源: 不算交易成功 (没有付款也就没有"结算")
|
|
73
|
+
rec = (await updateTransaction(rec.transactionId, { status: 'delivered', event: { kind: 'free_resource', detail: `HTTP ${status}` } }, home));
|
|
74
|
+
return { ok: true, status: 'delivered', transactionId: rec.transactionId, record: rec, reused };
|
|
75
|
+
}
|
|
76
|
+
// ② 报价自洽校验 (篡改 itemId/amount/payTo/network 一律拒绝)
|
|
77
|
+
const consistency = validatePaymentRequirements({
|
|
78
|
+
requirements,
|
|
79
|
+
metadata,
|
|
80
|
+
expected: { itemId: params.expectItemId, maxAmount: params.maxPaymentAmount, networks: params.expectNetworks },
|
|
81
|
+
});
|
|
82
|
+
if (!consistency.ok) {
|
|
83
|
+
rec = (await setTransactionStatus(rec.transactionId, 'failed', consistency.reason, home));
|
|
84
|
+
return { ok: false, status: 'failed', transactionId: rec.transactionId, record: rec, error: consistency.reason };
|
|
85
|
+
}
|
|
86
|
+
rec = (await updateTransaction(rec.transactionId, {
|
|
87
|
+
status: 'quoted', payTo: requirements.payTo, amount: String(requirements.amount), currency: requirements.currency || metadata.currency,
|
|
88
|
+
network: String(requirements.network), contentHash: metadata.contentHash, price: metadata.price,
|
|
89
|
+
event: { kind: 'quoted', detail: `amount=${requirements.amount} payTo=${String(requirements.payTo).slice(0, 10)}…` },
|
|
90
|
+
}, home));
|
|
91
|
+
// ③ 策略门 (Phase 2): 必须在解密/签名之前
|
|
92
|
+
const amountDecimal = Number(requirements.amount) / 1e6;
|
|
93
|
+
const service = params.service || 'x402-info';
|
|
94
|
+
let decision;
|
|
95
|
+
try {
|
|
96
|
+
const check = params.policyCheck || (async (intent) => {
|
|
97
|
+
const { getEconomicPolicy } = await import('../economic-policy.js');
|
|
98
|
+
return await getEconomicPolicy().check(intent);
|
|
99
|
+
});
|
|
100
|
+
decision = await check({ payTo: String(requirements.payTo), amount: amountDecimal, service });
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
decision = { allowed: false, reason: `策略引擎不可用 (fail-closed): ${String(e?.message || e).slice(0, 120)}` };
|
|
104
|
+
}
|
|
105
|
+
// 任务预算 (独立于 policy 的第二道闸)
|
|
106
|
+
if (decision.allowed && params.taskBudget !== undefined) {
|
|
107
|
+
const budget = Number(params.taskBudget);
|
|
108
|
+
if (Number.isFinite(budget) && amountDecimal > budget) {
|
|
109
|
+
decision = { allowed: false, reason: `支付金额 ${amountDecimal} 大于任务预算 ${budget}` };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
rec = (await updateTransaction(rec.transactionId, { policyDecision: decision, event: { kind: decision.allowed ? 'policy_allowed' : 'policy_denied', detail: decision.reason } }, home));
|
|
113
|
+
if (!decision.allowed) {
|
|
114
|
+
rec = (await setTransactionStatus(rec.transactionId, 'policy_denied', decision.reason, home));
|
|
115
|
+
return { ok: false, status: 'policy_denied', transactionId: rec.transactionId, record: rec, error: decision.reason || '策略拒绝' };
|
|
116
|
+
}
|
|
117
|
+
// ③.5 Phase 1: 抢"这笔 requestId 的付款权" (O_EXCL 独占, 跨进程唯一) —— 并发时只有一个能真付
|
|
118
|
+
const claim = await claimPayment(params.requestId, rec.transactionId, home);
|
|
119
|
+
if (!claim.ok) {
|
|
120
|
+
const prior = claim.record;
|
|
121
|
+
if (prior) {
|
|
122
|
+
rec = prior;
|
|
123
|
+
return { ok: prior.status === 'verified' || prior.status === 'delivered', status: prior.status, transactionId: prior.transactionId, record: prior, reused: true, error: claim.reason };
|
|
124
|
+
}
|
|
125
|
+
rec = (await setTransactionStatus(rec.transactionId, 'failed', claim.reason, home));
|
|
126
|
+
return { ok: false, status: rec.status, transactionId: rec.transactionId, record: rec, error: claim.reason };
|
|
127
|
+
}
|
|
128
|
+
rec = (await updateTransaction(rec.transactionId, { status: 'paying', event: { kind: 'paying', detail: `已取得付款权 (holder=${rec.transactionId}); 结算事实仍是 unpaid (还没真发出付款凭据)` } }, home));
|
|
129
|
+
// ④ 付款 + 交付 (仍然由 buyInfo 执行; 策略门已过才走到这里)
|
|
130
|
+
const { buyInfo } = await import('./paid-info-store.js');
|
|
131
|
+
const res = await buyInfo({
|
|
132
|
+
url: params.url,
|
|
133
|
+
privateKey: params.privateKey,
|
|
134
|
+
maxPaymentAmount: params.maxPaymentAmount,
|
|
135
|
+
network: params.network,
|
|
136
|
+
rpcUrl: params.rpcUrl,
|
|
137
|
+
allowLocalDev: params.allowLocalDev,
|
|
138
|
+
resolveDid: params.resolveDid,
|
|
139
|
+
expectItemId: params.expectItemId,
|
|
140
|
+
fetchImpl: params.fetchImpl,
|
|
141
|
+
prePayGuard: async () => ({ ok: true }), // 已在上一步过门 (这里不再重复打分)
|
|
142
|
+
onEvent: async (e) => {
|
|
143
|
+
const patch = { ...(e.patch || {}) };
|
|
144
|
+
// ★ 两层: 链上真的结算了 → 结算事实 payment_verified; 联调永远停 payment_submitted
|
|
145
|
+
if (patch.chainSettled === true && rec.paymentMode !== 'local-dev')
|
|
146
|
+
patch.settlementFact = 'payment_verified';
|
|
147
|
+
else if (patch.paymentReceipt && patch.paymentMode === 'local-dev')
|
|
148
|
+
patch.settlementFact = 'payment_submitted';
|
|
149
|
+
await updateTransaction(rec.transactionId, { ...patch, event: { kind: e.kind, detail: e.detail } }, home);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
rec = (await readTransaction(rec.transactionId, home));
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
// ★ 钱不能凭空消失: 只要**发起过支付尝试**, 就不许记成普通 failed
|
|
155
|
+
// 结算不确定 → payment_required + unknown (先对账, 不许自动重付)
|
|
156
|
+
// facilitator 明确拒绝 → payment_required + unpaid (钱一定没动, 可安全重试)
|
|
157
|
+
// 完全没付款凭据 → failed (此时确定没付过钱)
|
|
158
|
+
const attempted = res.payment?.attempted === true || !!rec.txHash || !!rec.paymentReceipt;
|
|
159
|
+
const uncertain = res.payment?.settlementUncertain === true;
|
|
160
|
+
const paidDone = res.payment?.settled === true || (!!rec.txHash && !uncertain);
|
|
161
|
+
let next;
|
|
162
|
+
if (res.policyDenied)
|
|
163
|
+
next = 'policy_denied';
|
|
164
|
+
else if (paidDone)
|
|
165
|
+
next = 'delivery_failed'; // 钱付了, 资源没到手
|
|
166
|
+
else if (uncertain)
|
|
167
|
+
next = 'payment_required'; // 可能付过 → 先对账, 不许自动重付
|
|
168
|
+
else if (attempted)
|
|
169
|
+
next = 'payment_required'; // 明确没付成 → 可安全重试
|
|
170
|
+
else
|
|
171
|
+
next = 'failed';
|
|
172
|
+
rec = (await updateTransaction(rec.transactionId, {
|
|
173
|
+
...(uncertain ? { settlementFact: 'unknown' } : {}),
|
|
174
|
+
...(paidDone ? { settlementFact: rec.chainSettled === true ? 'payment_verified' : 'payment_submitted' } : {}),
|
|
175
|
+
event: {
|
|
176
|
+
kind: 'payment_outcome',
|
|
177
|
+
detail: `attempted=${attempted} paidDone=${paidDone} uncertain=${uncertain} verifyRejected=${res.payment?.verifyRejected === true} → 状态 ${next}${uncertain ? ' (先对账, 不许自动重付)' : ''}`,
|
|
178
|
+
},
|
|
179
|
+
}, home));
|
|
180
|
+
rec = (await setTransactionStatus(rec.transactionId, next, res.error, home));
|
|
181
|
+
return { ok: false, status: rec.status, transactionId: rec.transactionId, record: rec, error: res.error };
|
|
182
|
+
}
|
|
183
|
+
await releasePaymentClaim(params.requestId, home).catch(() => null); // 付款完成 → 放锁 (事实已落盘)
|
|
184
|
+
// ⑤ 交付与验真绑定 (支付成功 ≠ 交易成功)
|
|
185
|
+
const envelope = res.envelope || null;
|
|
186
|
+
const content = String(envelope?.content ?? envelope?.body ?? '');
|
|
187
|
+
// 用协议同一套规范化哈希 (与卖方 contentHash 可比), 不是裸 sha256
|
|
188
|
+
const { computeContentHash } = await import('./paid-info-protocol.js');
|
|
189
|
+
const deliveryHash = computeContentHash(content);
|
|
190
|
+
const receiptStr = res.payment?.receipt || '';
|
|
191
|
+
// ★ 正文落盘: 让"资源正文存在 + 哈希正确"可以在事后**重新计算**验证, 而不是信记录里的自述
|
|
192
|
+
let deliveryBytes = null;
|
|
193
|
+
if (content) {
|
|
194
|
+
try {
|
|
195
|
+
const w = writeDeliveryContent(rec.transactionId, content, home);
|
|
196
|
+
deliveryBytes = { hash: w.hash, bytes: w.bytes };
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
deliveryBytes = null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
rec = (await updateTransaction(rec.transactionId, {
|
|
203
|
+
deliveryHash,
|
|
204
|
+
contentHash: envelope?.contentHash || envelope?.proof?.payload?.contentHash || envelope?.proof?.contentHash || rec.contentHash,
|
|
205
|
+
receiptHash: receiptStr ? computeReceiptHash(receiptStr) : undefined,
|
|
206
|
+
verificationTrust: res.verify?.trust || 'unverified',
|
|
207
|
+
protocolVerified: res.verify?.trust === 'verified',
|
|
208
|
+
...(deliveryBytes ? { deliveryBytesHash: deliveryBytes.hash } : {}),
|
|
209
|
+
event: { kind: 'delivery_hash', detail: `deliveryHash=${deliveryHash.slice(0, 12)}… trust=${res.verify?.trust || 'unverified'}` },
|
|
210
|
+
}, home));
|
|
211
|
+
if (!content) {
|
|
212
|
+
const resp = deriveResponsibility({ deliveryMissing: true });
|
|
213
|
+
rec = (await updateTransaction(rec.transactionId, { responsibility: resp, event: { kind: 'responsibility_candidate', detail: `${resp.type}: ${resp.reason}` } }, home));
|
|
214
|
+
rec = (await setTransactionStatus(rec.transactionId, 'delivery_failed', '付了钱但没拿到正文', home));
|
|
215
|
+
return { ok: false, status: 'delivery_failed', transactionId: rec.transactionId, record: rec, verify: res.verify, error: '付了钱但没拿到正文' };
|
|
216
|
+
}
|
|
217
|
+
if (rec.contentHash && deliveryHash !== rec.contentHash) {
|
|
218
|
+
const resp = deriveResponsibility({ deliveryHashMismatch: true });
|
|
219
|
+
rec = (await updateTransaction(rec.transactionId, { responsibility: resp, event: { kind: 'responsibility_candidate', detail: `${resp.type}: ${resp.reason}` } }, home));
|
|
220
|
+
rec = (await setTransactionStatus(rec.transactionId, 'verification_failed', '内容哈希不匹配 (内容被换过)', home));
|
|
221
|
+
return { ok: false, status: 'verification_failed', transactionId: rec.transactionId, record: rec, verify: res.verify, error: '内容哈希不匹配' };
|
|
222
|
+
}
|
|
223
|
+
if (!rec.receiptHash) {
|
|
224
|
+
const resp = deriveResponsibility({ receiptMissing: true });
|
|
225
|
+
rec = (await updateTransaction(rec.transactionId, { responsibility: resp, event: { kind: 'responsibility_candidate', detail: `${resp.type}: ${resp.reason}` } }, home));
|
|
226
|
+
rec = (await setTransactionStatus(rec.transactionId, 'verification_failed', '缺少支付回执 (回执与信封未绑定)', home));
|
|
227
|
+
return { ok: false, status: 'verification_failed', transactionId: rec.transactionId, record: rec, verify: res.verify, error: '缺少支付回执' };
|
|
228
|
+
}
|
|
229
|
+
if ((rec.verificationTrust || 'unverified') === 'unverified') {
|
|
230
|
+
const ev = { signatureInvalid: true };
|
|
231
|
+
const resp = deriveResponsibility(ev);
|
|
232
|
+
rec = (await updateTransaction(rec.transactionId, { responsibility: resp, event: { kind: 'responsibility_candidate', detail: `${resp.type}: ${resp.reason}` } }, home));
|
|
233
|
+
rec = (await setTransactionStatus(rec.transactionId, 'verification_failed', `验真不通过 (${res.verify?.trust})`, home));
|
|
234
|
+
return { ok: false, status: 'verification_failed', transactionId: rec.transactionId, record: rec, verify: res.verify, error: `验真不通过: ${res.verify?.trust}` };
|
|
235
|
+
}
|
|
236
|
+
// ⑥ 终态: 链上结算 + 全部条件 → verified; 本机联调 → delivered (明确不许冒充 verified)
|
|
237
|
+
const verdict = evaluateTransactionSuccess({ ...rec, status: 'delivered' });
|
|
238
|
+
const finalStatus = verdict.success ? 'verified' : 'delivered';
|
|
239
|
+
rec = (await setTransactionStatus(rec.transactionId, finalStatus, verdict.reason, home));
|
|
240
|
+
if (finalStatus === 'verified') {
|
|
241
|
+
rec = (await setSettlementFact(rec.transactionId, 'fully_settled', '链上结算 + 交付与验真全通过', home));
|
|
242
|
+
}
|
|
243
|
+
// Phase 3: 交易证据写进 Run (step + evidence) 与 Goal (仅在 verified + 执行成功 + 命中判据时计入成功证据)
|
|
244
|
+
let bridge = null;
|
|
245
|
+
if (params.runId || params.goalId) {
|
|
246
|
+
try {
|
|
247
|
+
const { bridgeTransactionToRunGoal } = await import('./goal-run-bridge.js');
|
|
248
|
+
bridge = await bridgeTransactionToRunGoal(rec, {
|
|
249
|
+
runId: params.runId, goalId: params.goalId,
|
|
250
|
+
executionOk: params.executionOk, goalCriteriaHit: params.goalCriteriaHit,
|
|
251
|
+
summary: rec.itemId,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (e) { /* 记账失败不改变交易事实 */ }
|
|
255
|
+
}
|
|
256
|
+
return { ok: true, status: finalStatus, transactionId: rec.transactionId, record: rec, verify: res.verify, envelope, reused, bridge };
|
|
257
|
+
}
|