@bolloon/bolloon-agent 0.4.27 → 0.4.28
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 +21 -0
- package/dist/agents/task/local-seller.js +149 -0
- package/dist/agents/task/report-card.js +146 -0
- package/dist/agents/task/resource-advisor.js +182 -0
- package/dist/agents/task/task-budget.js +103 -0
- package/dist/agents/task/task-runner.js +618 -0
- package/dist/agents/x402/goal-run-bridge.js +10 -6
- package/dist/agents/x402/resource-contract.js +12 -1
- package/dist/cli-entry.js +94 -0
- package/package.json +1 -1
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-runner.ts — M1 薄层 ①: Task Runner (M1–M4 收口版)
|
|
3
|
+
*
|
|
4
|
+
* `bolloon task "<任务>" --budget 0.05` → Goal → (顾问) → 购买 → 执行 → 证据 → 报告卡
|
|
5
|
+
* `bolloon task --resume <goalId>` → 统一恢复决策 (与 Supervisor 同一个函数)
|
|
6
|
+
*
|
|
7
|
+
* 收口要点 (leo 2026-09-18 收口计划):
|
|
8
|
+
* · Goal / Run **先建**: 任何一条失败路径都返回报告卡, 不抛栈、不返回空 runId
|
|
9
|
+
* · 证据只走 `bridgeTransactionToRunGoal` (不再自己维护第二套证据写法)
|
|
10
|
+
* · 交易记录写 `resourceOutcome` (安装/执行/契约/判据) + `verificationTrust`
|
|
11
|
+
* · 失败映射: 付了但资源没跑成 → `delivery_failed`; 跑了但输出不合契约 → `verification_failed`
|
|
12
|
+
* · local-dev 永远 self-attested, 永不 `verified`
|
|
13
|
+
* · 非幂等保护: 已有执行证据的任务续跑时**不重复执行**
|
|
14
|
+
* · 故障注入钩子 `BOLLOON_TASK_FAULT` (仅测试用; 不设即无行为差异)
|
|
15
|
+
*/
|
|
16
|
+
import * as os from 'os';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as crypto from 'crypto';
|
|
20
|
+
import { createGoal, attachRun, markCriterion, completeGoalIfEligible, readGoal, setCriteria, } from '../goal-store.js';
|
|
21
|
+
import { startRun, recordStep, finishRun, readRun } from '../run-store.js';
|
|
22
|
+
import { listTransactions, updateTransaction, setTransactionStatus } from '../x402/transaction-store.js';
|
|
23
|
+
import { bridgeTransactionToRunGoal } from '../x402/goal-run-bridge.js';
|
|
24
|
+
import { buyInfoAsTransaction } from '../x402/trade.js';
|
|
25
|
+
import { publishInfo, listInfo } from '../x402/paid-info-store.js';
|
|
26
|
+
import { planTransactionRecovery } from '../x402/payment-recovery.js';
|
|
27
|
+
import { executeContractSkill, validateResourceOutput, verifyInstallFidelity, loadResourceContract } from '../x402/resource-contract.js';
|
|
28
|
+
import { adviseResource } from './resource-advisor.js';
|
|
29
|
+
import { resolveTaskBudget, checkPurchaseAllowed, previewPurchaseImpact } from './task-budget.js';
|
|
30
|
+
import { buildReportCard, renderReportCard } from './report-card.js';
|
|
31
|
+
import { startLocalSeller, ensureSellerIdentity } from './local-seller.js';
|
|
32
|
+
/** M1 默认值 (本地 Registry; 价格必须 ≤ 单次上限 0.02) */
|
|
33
|
+
export const M1_DEFAULTS = {
|
|
34
|
+
localPrice: '0.012',
|
|
35
|
+
currency: 'USDC',
|
|
36
|
+
network: 'base-sepolia',
|
|
37
|
+
localPayTo: '0x1111111111111111111111111111111111111111',
|
|
38
|
+
buyerDid: 'did:key:zTaskBuyer',
|
|
39
|
+
};
|
|
40
|
+
// ── 工具 ────────────────────────────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* 同一个 (任务 + 预算) 派生**同一个 requestId** —— 这是"重跑不重复付款"的关键:
|
|
43
|
+
* 进程被杀后重新执行同样的任务, trade 层的幂等短路会复用已有交易, 不会第二次扣款。
|
|
44
|
+
*/
|
|
45
|
+
export function defaultRequestId(task, taskBudget) {
|
|
46
|
+
const h = crypto.createHash('sha256').update(`${task.trim()}|${taskBudget}`).digest('hex').slice(0, 16);
|
|
47
|
+
return `task-${h}`;
|
|
48
|
+
}
|
|
49
|
+
/** 故障注入 (只给验收用; 未设置 = 完全无行为差异) */
|
|
50
|
+
function faultPoint(name) {
|
|
51
|
+
const want = process.env.BOLLOON_TASK_FAULT;
|
|
52
|
+
if (!want || want !== name)
|
|
53
|
+
return;
|
|
54
|
+
if (process.env.BOLLOON_TASK_FAULT_MODE === 'exit')
|
|
55
|
+
process.exit(97); // 模拟进程被杀
|
|
56
|
+
process.kill(process.pid, 'SIGKILL');
|
|
57
|
+
}
|
|
58
|
+
/** 从任务文本 + 契约 inputSchema 推导输入 (确定性; M1 不做语义抽取, 用 --input 兜底) */
|
|
59
|
+
export function deriveSkillInput(contract, task) {
|
|
60
|
+
const schema = contract?.inputSchema || {};
|
|
61
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
62
|
+
const props = schema.properties || {};
|
|
63
|
+
const MARKET_RE = /(日本|美国|欧盟|东南亚|韩国|德国|英国|法国|加拿大|澳大利亚|印度|中东|拉美|巴西|越南|泰国)/;
|
|
64
|
+
const market = (task.match(MARKET_RE) || [])[0] || '';
|
|
65
|
+
const cleanProduct = (t) => t
|
|
66
|
+
.replace(MARKET_RE, ' ')
|
|
67
|
+
.replace(/判断|这款|这个|是否|适合|不适合|进入|市场|帮我|看下|看一下|能不能|可否|调研|建议/g, ' ')
|
|
68
|
+
.replace(/[,。、,.?!?!::;;]/g, ' ')
|
|
69
|
+
.replace(/\s+/g, ' ')
|
|
70
|
+
.trim();
|
|
71
|
+
const fields = Array.from(new Set([...required, ...Object.keys(props)]));
|
|
72
|
+
const out = {};
|
|
73
|
+
for (const key of fields) {
|
|
74
|
+
const p = props[key] || {};
|
|
75
|
+
const isRequired = required.includes(key);
|
|
76
|
+
const type = String(p.type || 'string');
|
|
77
|
+
if (type === 'number' || type === 'integer') {
|
|
78
|
+
const n = task.match(/\d+(\.\d+)?/);
|
|
79
|
+
if (n)
|
|
80
|
+
out[key] = Number(n[0]); // 任务里真有数字才填 (可选字段也不硬塞)
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (/market|国家|市场|地区|country|region/i.test(key)) {
|
|
84
|
+
if (market)
|
|
85
|
+
out[key] = market;
|
|
86
|
+
else if (isRequired)
|
|
87
|
+
out[key] = task.slice(0, 40);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (/product|商品|品名|item|name/i.test(key)) {
|
|
91
|
+
const prod = cleanProduct(task);
|
|
92
|
+
if (prod.length >= 2)
|
|
93
|
+
out[key] = prod.slice(0, 60);
|
|
94
|
+
else if (isRequired)
|
|
95
|
+
out[key] = task.slice(0, 60);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (isRequired)
|
|
99
|
+
out[key] = task.slice(0, 200);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** 结论抽取: 优先显式字段, 不猜语义 */
|
|
104
|
+
export function extractConclusion(output) {
|
|
105
|
+
if (!output || typeof output !== 'object')
|
|
106
|
+
return {};
|
|
107
|
+
const o = output;
|
|
108
|
+
for (const k of ['conclusion', 'verdict', 'recommendation', 'summary', '结论']) {
|
|
109
|
+
if (typeof o[k] === 'string' && o[k].trim())
|
|
110
|
+
return { conclusion: o[k].trim().slice(0, 160) };
|
|
111
|
+
}
|
|
112
|
+
if (typeof o.suitable === 'boolean')
|
|
113
|
+
return { conclusion: o.suitable ? '适合' : '不适合' };
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
export function extractSources(output) {
|
|
117
|
+
const o = output;
|
|
118
|
+
if (!o || typeof o !== 'object')
|
|
119
|
+
return [];
|
|
120
|
+
const raw = o.sources || o.findings?.map((f) => f?.source).filter(Boolean) || [];
|
|
121
|
+
const list = [];
|
|
122
|
+
for (const s of Array.isArray(raw) ? raw : []) {
|
|
123
|
+
if (typeof s === 'string')
|
|
124
|
+
list.push(s);
|
|
125
|
+
else if (s && typeof s === 'object')
|
|
126
|
+
list.push(String(s.url || s.ref || s.name || JSON.stringify(s).slice(0, 60)));
|
|
127
|
+
}
|
|
128
|
+
return list;
|
|
129
|
+
}
|
|
130
|
+
/** 默认判据 (来自资源契约, 可判定) */
|
|
131
|
+
export function defaultCriteria(contract) {
|
|
132
|
+
const v = contract?.verification || {};
|
|
133
|
+
const req = (v.requiredFields || []).slice(0, 4).join(', ');
|
|
134
|
+
const ev = (v.evidenceFields || []).slice(0, 3).join(', ');
|
|
135
|
+
return [
|
|
136
|
+
`输出包含契约要求字段 (${req || 'requiredFields'})`,
|
|
137
|
+
`输出带可核验来源 (${ev || 'sources'})`,
|
|
138
|
+
'结论可读 (conclusion/verdict)',
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
function safeJoin(dir, rel) {
|
|
142
|
+
if (!rel || path.isAbsolute(rel) || rel.includes('..'))
|
|
143
|
+
return null;
|
|
144
|
+
const full = path.join(dir, rel);
|
|
145
|
+
if (!full.startsWith(dir))
|
|
146
|
+
return null;
|
|
147
|
+
return full;
|
|
148
|
+
}
|
|
149
|
+
export function installBundle(content, installDir) {
|
|
150
|
+
let bundle;
|
|
151
|
+
try {
|
|
152
|
+
bundle = JSON.parse(content);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
return { ok: false, files: 0, error: `交付内容不是合法 JSON 包: ${String(e?.message || e).slice(0, 80)}` };
|
|
156
|
+
}
|
|
157
|
+
const files = bundle?.files || {};
|
|
158
|
+
const rels = Object.keys(files);
|
|
159
|
+
if (rels.length === 0)
|
|
160
|
+
return { ok: false, files: 0, error: '交付包里没有文件' };
|
|
161
|
+
try {
|
|
162
|
+
for (const rel of rels) {
|
|
163
|
+
const dest = safeJoin(installDir, rel);
|
|
164
|
+
if (!dest)
|
|
165
|
+
return { ok: false, files: 0, error: `包内路径非法 (越界): ${rel}` };
|
|
166
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
167
|
+
fs.writeFileSync(dest, String(files[rel]), 'utf8');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
return { ok: false, files: 0, error: `安装失败: ${String(e?.message || e).slice(0, 80)}` };
|
|
172
|
+
}
|
|
173
|
+
return { ok: true, files: rels.length };
|
|
174
|
+
}
|
|
175
|
+
/** 本地 Registry: 若技能没有报价, 就按技能目录发布一条本地报价 (确定性 id = 技能名) */
|
|
176
|
+
export async function ensureLocalListing(opts) {
|
|
177
|
+
const existing = (await listInfo(opts.home)).find((i) => String(i.id) === opts.skillName);
|
|
178
|
+
if (existing)
|
|
179
|
+
return { itemId: existing.id, created: false };
|
|
180
|
+
const SHARE = await import('../skill-share.js');
|
|
181
|
+
const collected = await SHARE.collectSkillBundle(opts.skillDir, { name: opts.skillName });
|
|
182
|
+
if (!collected?.ok || !collected?.bundle)
|
|
183
|
+
return { itemId: '', created: false, error: `打包技能失败: ${collected?.error || '未知'}` };
|
|
184
|
+
const did = (await ensureSellerIdentity(opts.home)).did || 'did:key:zLocalSeller';
|
|
185
|
+
const item = await publishInfo({
|
|
186
|
+
id: opts.skillName,
|
|
187
|
+
title: opts.skillName,
|
|
188
|
+
category: 'skill',
|
|
189
|
+
content: JSON.stringify(collected.bundle),
|
|
190
|
+
description: `本地可执行技能 ${opts.skillName}`,
|
|
191
|
+
price: { amount: String(opts.price ?? M1_DEFAULTS.localPrice), currency: M1_DEFAULTS.currency, network: M1_DEFAULTS.network, payTo: String(opts.payTo ?? M1_DEFAULTS.localPayTo) },
|
|
192
|
+
source: { kind: 'self', refs: [opts.skillDir], note: `skill=${opts.skillName}@${opts.version || ''}` },
|
|
193
|
+
provider: { did, name: '本机卖方' },
|
|
194
|
+
}, { home: opts.home });
|
|
195
|
+
return { itemId: item.id, created: true };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* "只补交付, 不重付" (M2 第 ② 类恢复点):
|
|
199
|
+
* 付款已经发生过 (记录里有凭据), 但内容没拿到 → 拿**同一个支付凭据**再向卖方取一次内容。
|
|
200
|
+
* 卖方会重新校验这个凭据 (幂等), 不会再产生一笔付款。
|
|
201
|
+
*/
|
|
202
|
+
export async function refetchDeliveredContent(opts) {
|
|
203
|
+
if (!opts.receipt)
|
|
204
|
+
return { ok: false, error: '记录里没有支付凭据, 无法补交付 (需要人工/对账)' };
|
|
205
|
+
try {
|
|
206
|
+
const res = await fetch(opts.url, { headers: { 'X-PAYMENT': opts.receipt } });
|
|
207
|
+
if (!res.ok)
|
|
208
|
+
return { ok: false, error: `补交付被卖方拒绝: HTTP ${res.status}` };
|
|
209
|
+
const env = await res.json().catch(() => null);
|
|
210
|
+
const content = String(env?.content ?? '');
|
|
211
|
+
if (!content)
|
|
212
|
+
return { ok: false, error: '卖方返回里没有内容' };
|
|
213
|
+
return { ok: true, content };
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
return { ok: false, error: `补交付请求失败: ${String(e?.message || e).slice(0, 100)}` };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* M4 出口口径 (纯函数, 可单测): 技能**产出了输出**但契约不过 → 验真失败; 压根没产出 → 交付失败。
|
|
221
|
+
*/
|
|
222
|
+
export function mapFailureStatus(opts) {
|
|
223
|
+
return opts.producedOutput && !opts.contractOk ? 'verification_failed' : 'delivery_failed';
|
|
224
|
+
}
|
|
225
|
+
/** 失败阶段 (M4 归责用): 装不上 → install; 没产出 → execute; 产出但不合契约 → output_contract */
|
|
226
|
+
export function failureStageFor(opts) {
|
|
227
|
+
if (!opts.installOk)
|
|
228
|
+
return 'install';
|
|
229
|
+
if (!opts.producedOutput)
|
|
230
|
+
return 'execute';
|
|
231
|
+
if (!opts.contractOk)
|
|
232
|
+
return 'output_contract';
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
/** 判断一笔交易是否已经有"资源被执行过"的证据 (用于非幂等保护) */
|
|
236
|
+
export function hasExecutionEvidence(rec) {
|
|
237
|
+
return rec?.resourceOutcome?.executed === true || rec?.execution?.ok === true;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* 非幂等保护的完整判据: 交易记录**或** Run 轨迹里任一证明"执行过" → 不许再执行一遍。
|
|
241
|
+
* (真跑抓到过: 只查交易记录时, "执行完但还没写回交易就被杀"的任务续跑会重复执行非幂等技能。)
|
|
242
|
+
*/
|
|
243
|
+
export async function goalAlreadyExecuted(goal) {
|
|
244
|
+
for (const runId of (goal?.runs || [])) {
|
|
245
|
+
try {
|
|
246
|
+
const run = await readRun(runId);
|
|
247
|
+
const hit = (run?.steps || []).some((st) => st.tool === 'skill_exec' && st.ok === true);
|
|
248
|
+
if (hit)
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
catch { /* 读不到就当没有, 但交易记录那边还会兜一层 */ }
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
// ── 主流程 ──────────────────────────────────────────────────────────────────
|
|
256
|
+
export async function runTask(opts) {
|
|
257
|
+
const home = opts.home ?? os.homedir();
|
|
258
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
259
|
+
const stages = [];
|
|
260
|
+
const t0 = Date.now();
|
|
261
|
+
let lastMs = t0;
|
|
262
|
+
const mark = (stage, note) => {
|
|
263
|
+
const now = Date.now();
|
|
264
|
+
stages.push({ stage, note, ms: now - lastMs });
|
|
265
|
+
lastMs = now;
|
|
266
|
+
opts.onStage?.(stage, note);
|
|
267
|
+
};
|
|
268
|
+
const blankBudget = (why) => ({ taskBudget: 0, perPurchase: 0, daily: 0, requested: {}, clamped: { task: false, perPurchase: false, daily: false }, why });
|
|
269
|
+
// ① 预算 (唯一可以在没有 Goal 的情况下返回失败的一步: 预算非法 == 记账本身无意义)
|
|
270
|
+
const parsed = resolveTaskBudget({ taskBudget: opts.budget, perPurchase: opts.perPurchase, daily: opts.daily });
|
|
271
|
+
if (!parsed.ok || !parsed.plan) {
|
|
272
|
+
const card = buildReportCard({ task: opts.task, executed: false, paid: false, blocker: `预算不合法: ${parsed.error}`, evidenceRef: {} });
|
|
273
|
+
return { ok: false, card, text: renderReportCard(card), stages, budget: blankBudget([String(parsed.error)]), outputIssues: [] };
|
|
274
|
+
}
|
|
275
|
+
const budget = parsed.plan;
|
|
276
|
+
mark('prepare', `任务: ${opts.task} · 预算 ${budget.taskBudget} USDC`);
|
|
277
|
+
// ② Goal + Run 先建 (此后每条失败路径都带 goalId/runId, 且可续跑)
|
|
278
|
+
const criteria0 = ['任务完成并留下完整证据'];
|
|
279
|
+
const reusedGoal = opts.reuseGoalId ? await readGoal(opts.reuseGoalId) : null;
|
|
280
|
+
let goal = reusedGoal;
|
|
281
|
+
if (!goal) {
|
|
282
|
+
try {
|
|
283
|
+
goal = await createGoal({ objective: opts.task, successCriteria: criteria0, constraints: [`预算 ≤ ${budget.taskBudget} ${M1_DEFAULTS.currency}`], createdBy: 'cli:task' });
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
const msg = String(e?.message || e).slice(0, 200);
|
|
287
|
+
mark('report', '初始化未就绪 → 没有创建 Goal, 也没有花钱');
|
|
288
|
+
const card = buildReportCard({ task: opts.task, executed: false, paid: false, conclusion: '证据不足', blocker: `本机还没初始化好, 无法记账也不会花钱: ${msg}`, evidenceRef: {}, budgetLines: budget.why });
|
|
289
|
+
return { ok: false, card, text: renderReportCard(card), stages, budget, outputIssues: [] };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const run = await startRun({ surface: 'cli', goal: opts.task, goalId: goal.goalId });
|
|
293
|
+
await attachRun(goal.goalId, run.runId);
|
|
294
|
+
const fail = async (stage, note, cardOpts, runStatus = 'needs_human') => {
|
|
295
|
+
mark(stage, note);
|
|
296
|
+
await finishRun(run.runId, { status: runStatus, summary: note.slice(0, 200) });
|
|
297
|
+
const card = buildReportCard({ ...cardOpts, evidenceRef: { goalId: goal.goalId, runId: run.runId, transactionId: cardOpts.evidenceRef?.transactionId }, budgetLines: [...budget.why, ...(cardOpts.budgetLines || [])] });
|
|
298
|
+
return { ok: false, card, text: renderReportCard(card), goalId: goal.goalId, runId: run.runId, transactionId: card.evidenceRef.transactionId, stages, budget, outputIssues: cardOpts.blocker ? [cardOpts.blocker] : [] };
|
|
299
|
+
};
|
|
300
|
+
// ③ 顾问: 自动判断缺什么能力
|
|
301
|
+
const advisor = await adviseResource({ task: opts.task, home, cwd, skillPaths: opts.skillPaths });
|
|
302
|
+
const advisorBrief = { needed: advisor.needed, reason: advisor.reason, skill: advisor.chosen?.name, why: advisor.chosen?.why || [] };
|
|
303
|
+
await recordStep(run.runId, { tool: 'resource_advise', ok: advisor.needed, summary: advisor.chosen ? `选中 ${advisor.chosen.name}@${advisor.chosen.version} (分数 ${advisor.chosen.score})` : advisor.reason, error: advisor.needed ? undefined : advisor.reason });
|
|
304
|
+
if (!advisor.needed || !advisor.chosen) {
|
|
305
|
+
const r = await fail('report', '没有可用的可执行资源 → 不买, 如实报告', {
|
|
306
|
+
task: opts.task, executed: false, paid: false, conclusion: '证据不足',
|
|
307
|
+
blocker: `缺少能完成这个任务的外部能力, 且本地 Registry 里没有匹配的可执行 Skill。${advisor.reason}`,
|
|
308
|
+
evidenceRef: {},
|
|
309
|
+
});
|
|
310
|
+
return { ...r, advisor: advisorBrief };
|
|
311
|
+
}
|
|
312
|
+
const chosen = advisor.chosen;
|
|
313
|
+
mark('prepare', `顾问判断缺能力 → 选 "${chosen.name}" (分数 ${chosen.score}): ${chosen.why.slice(0, 2).join(' / ') || '名字匹配'}`);
|
|
314
|
+
// ④ 报价 (本地 Registry 没报价就发布一条, 如实留痕)
|
|
315
|
+
let listing = chosen.listing;
|
|
316
|
+
let listingCreated = false;
|
|
317
|
+
if (!listing) {
|
|
318
|
+
const pub = await ensureLocalListing({ skillName: chosen.name, skillDir: chosen.dir, home, price: opts.price, payTo: opts.payTo, version: chosen.version });
|
|
319
|
+
if (!pub.itemId) {
|
|
320
|
+
const r = await fail('report', `无法把技能变成可购买资源: ${pub.error}`, {
|
|
321
|
+
task: opts.task, executed: false, paid: false, conclusion: '证据不足',
|
|
322
|
+
blocker: `技能 "${chosen.name}" 无法变成可购买资源: ${pub.error}`,
|
|
323
|
+
skill: { name: chosen.name, version: chosen.version, dir: chosen.dir }, evidenceRef: {},
|
|
324
|
+
});
|
|
325
|
+
return { ...r, advisor: advisorBrief };
|
|
326
|
+
}
|
|
327
|
+
listingCreated = pub.created;
|
|
328
|
+
mark('acquire', `本地 Registry 里没有 "${chosen.name}" 的报价 → 已按技能目录发布本地报价 (itemId=${pub.itemId})`);
|
|
329
|
+
listing = { itemId: pub.itemId, title: chosen.name, amount: String(opts.price ?? M1_DEFAULTS.localPrice), currency: M1_DEFAULTS.currency, network: M1_DEFAULTS.network, payTo: String(opts.payTo ?? M1_DEFAULTS.localPayTo) };
|
|
330
|
+
}
|
|
331
|
+
// ⑤ 预算门 (付款前必须看得到价格与影响; 三层各自会拦)
|
|
332
|
+
const impact = previewPurchaseImpact(budget, listing.amount);
|
|
333
|
+
const decision = checkPurchaseAllowed({ amount: listing.amount, plan: budget });
|
|
334
|
+
if (!decision.allowed) {
|
|
335
|
+
const r = await fail('report', `预算门拒绝 (${decision.layer}): ${decision.reason}`, {
|
|
336
|
+
task: opts.task, executed: false, paid: false, conclusion: '证据不足',
|
|
337
|
+
blocker: `购买被预算门拦下 (${decision.layer}): ${decision.reason}`,
|
|
338
|
+
skill: { name: chosen.name, version: chosen.version },
|
|
339
|
+
cost: { amount: listing.amount, currency: listing.currency, network: listing.network },
|
|
340
|
+
evidenceRef: {}, budgetLines: impact,
|
|
341
|
+
});
|
|
342
|
+
return { ...r, advisor: advisorBrief };
|
|
343
|
+
}
|
|
344
|
+
// ⑤.5 判据 (合同契约生成, 确认后才可能判完成)
|
|
345
|
+
const criteria = defaultCriteria(chosen.contract);
|
|
346
|
+
if (!reusedGoal)
|
|
347
|
+
await setCriteria(goal.goalId, { criteria, source: 'user', confirm: true, by: 'cli:task' });
|
|
348
|
+
// ⑥ 买 (支付逻辑全在 trade.ts; 这里只给本地卖方地址)
|
|
349
|
+
mark('acquire', impact.join(' · '));
|
|
350
|
+
faultPoint('before_payment');
|
|
351
|
+
const seller = await startLocalSeller();
|
|
352
|
+
const requestId = opts.requestId || defaultRequestId(opts.task, budget.taskBudget);
|
|
353
|
+
let trade;
|
|
354
|
+
try {
|
|
355
|
+
trade = await buyInfoAsTransaction({
|
|
356
|
+
url: `${seller.url}/api/x402/info/${listing.itemId}`,
|
|
357
|
+
requestId,
|
|
358
|
+
buyerDid: opts.buyerDid || M1_DEFAULTS.buyerDid,
|
|
359
|
+
privateKey: opts.walletKey,
|
|
360
|
+
allowLocalDev: opts.allowLocalDev ?? true,
|
|
361
|
+
maxPaymentAmount: String(budget.perPurchase),
|
|
362
|
+
taskBudget: String(budget.taskBudget),
|
|
363
|
+
network: listing.network,
|
|
364
|
+
expectItemId: listing.itemId,
|
|
365
|
+
service: 'x402-skill',
|
|
366
|
+
goalId: goal.goalId,
|
|
367
|
+
runId: run.runId,
|
|
368
|
+
home,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
await seller.close();
|
|
373
|
+
}
|
|
374
|
+
faultPoint('after_payment');
|
|
375
|
+
const trust = trade?.record?.chainSettled === true ? 'verified' : 'self-attested';
|
|
376
|
+
const payment = {
|
|
377
|
+
mode: String(trade?.record?.paymentMode || 'none'),
|
|
378
|
+
amount: listing.amount,
|
|
379
|
+
currency: listing.currency,
|
|
380
|
+
network: listing.network,
|
|
381
|
+
txHash: trade?.record?.txHash,
|
|
382
|
+
chainSettled: trade?.record?.chainSettled === true,
|
|
383
|
+
trust,
|
|
384
|
+
reused: trade?.reused === true,
|
|
385
|
+
};
|
|
386
|
+
const paid = payment.mode !== 'none' && (payment.chainSettled || payment.mode === 'local-dev');
|
|
387
|
+
await recordStep(run.runId, {
|
|
388
|
+
tool: 'x402_buy',
|
|
389
|
+
ok: trade?.ok === true,
|
|
390
|
+
summary: `${payment.mode} · ${listing.amount} ${listing.currency} · 结算=${payment.chainSettled}`,
|
|
391
|
+
error: trade?.ok ? undefined : String(trade?.error || trade?.status || '').slice(0, 160),
|
|
392
|
+
});
|
|
393
|
+
// 内容没拿到 (复用的交易不带内容, 或交付阶段被打断) → 补交付, 不重付
|
|
394
|
+
let deliveredContent = String(trade?.envelope?.content ?? '');
|
|
395
|
+
let redelivered = false;
|
|
396
|
+
if (!deliveredContent && trade?.record?.paymentReceipt) {
|
|
397
|
+
const seller2 = await startLocalSeller();
|
|
398
|
+
let ref;
|
|
399
|
+
try {
|
|
400
|
+
ref = await refetchDeliveredContent({ url: `${seller2.url}/api/x402/info/${listing.itemId}`, receipt: String(trade.record.paymentReceipt) });
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
await seller2.close();
|
|
404
|
+
}
|
|
405
|
+
if (ref.ok) {
|
|
406
|
+
deliveredContent = String(ref.content || '');
|
|
407
|
+
redelivered = true;
|
|
408
|
+
trade = { ...trade, ok: true };
|
|
409
|
+
mark('acquire', '已付款但没交付 → 用同一支付凭据补交付 (没有再付款)');
|
|
410
|
+
await recordStep(run.runId, { tool: 'x402_redeliver', ok: true, summary: '补交付成功 (同一凭据, 无第二笔付款)' });
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
await recordStep(run.runId, { tool: 'x402_redeliver', ok: false, summary: ref.error });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!trade?.ok) {
|
|
417
|
+
// 失败映射 (M4): 付款事实不确定 → needs_human, 绝不自动重付
|
|
418
|
+
const rec = trade?.record;
|
|
419
|
+
if (rec) {
|
|
420
|
+
const r2 = await bridgeTransactionToRunGoal(rec, { runId: run.runId, goalId: goal.goalId, executionOk: false, goalCriteriaHit: false, summary: `${chosen.name}@${chosen.version}` });
|
|
421
|
+
void r2;
|
|
422
|
+
}
|
|
423
|
+
const uncertain = String(trade?.status || '') === 'payment_required' && trade?.payment?.settlementUncertain === true;
|
|
424
|
+
return {
|
|
425
|
+
...(await fail('report', `付款/交付失败: ${trade?.error || trade?.status} → 不执行, 不伪装成功`, {
|
|
426
|
+
task: opts.task, executed: false, paid, conclusion: '证据不足',
|
|
427
|
+
blocker: `${uncertain ? '付款状态不确定 (先对账, 不许重付): ' : '卡在购买这一步: '}${String(trade?.error || trade?.status)}`,
|
|
428
|
+
skill: { name: chosen.name, version: chosen.version },
|
|
429
|
+
cost: { amount: listing.amount, currency: listing.currency, network: listing.network },
|
|
430
|
+
lifecycle: String(trade?.status || ''), evidenceRef: { transactionId: trade?.transactionId },
|
|
431
|
+
}, uncertain ? 'needs_human' : 'failed')),
|
|
432
|
+
advisor: advisorBrief, payment,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
// ⑦ 保真 + 安装
|
|
436
|
+
const content = deliveredContent;
|
|
437
|
+
const installDir = path.join(home, '.bolloon', 'tasks', run.runId, 'skills', chosen.name);
|
|
438
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
439
|
+
const installed = installBundle(content, installDir);
|
|
440
|
+
let fidelityScore = '未通过';
|
|
441
|
+
let fidelityIssues = [];
|
|
442
|
+
if (installed.ok) {
|
|
443
|
+
const fidelity = await verifyInstallFidelity({ content, rec: trade?.record, installDir });
|
|
444
|
+
fidelityScore = fidelity.ok ? '通过' : '未通过';
|
|
445
|
+
fidelityIssues = fidelity.issues || [];
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
fidelityIssues = [installed.error || '安装失败'];
|
|
449
|
+
}
|
|
450
|
+
await recordStep(run.runId, { tool: 'resource_install', ok: fidelityScore === '通过', summary: `${installed.files} 个文件 → ${installDir}`, error: fidelityIssues[0] });
|
|
451
|
+
faultPoint('after_install');
|
|
452
|
+
// ⑧ 执行 (真跑技能代码) + 输出契约校验
|
|
453
|
+
mark('execute', `执行 ${chosen.name}`);
|
|
454
|
+
faultPoint('before_execute');
|
|
455
|
+
const loaded = await loadResourceContract(installDir);
|
|
456
|
+
const contract = loaded.ok && loaded.contract ? loaded.contract : chosen.contract;
|
|
457
|
+
const input = opts.input ?? deriveSkillInput(contract, opts.task);
|
|
458
|
+
const exec = await executeContractSkill({
|
|
459
|
+
contract,
|
|
460
|
+
skillDir: installDir,
|
|
461
|
+
input,
|
|
462
|
+
allowedTools: ['skill_exec', 'read_file'],
|
|
463
|
+
allowCodeExecution: true,
|
|
464
|
+
});
|
|
465
|
+
const outChk = validateResourceOutput(contract, exec.output);
|
|
466
|
+
const sources = extractSources(exec.output);
|
|
467
|
+
const concl = extractConclusion(exec.output);
|
|
468
|
+
await recordStep(run.runId, {
|
|
469
|
+
tool: 'skill_exec',
|
|
470
|
+
ok: exec.execution.ok === true && outChk.ok,
|
|
471
|
+
summary: `输出契约=${outChk.ok ? '通过' : '未通过'} · 来源 ${sources.length} 个`,
|
|
472
|
+
error: (outChk.issues || [])[0] || exec.execution.reason,
|
|
473
|
+
args: input,
|
|
474
|
+
});
|
|
475
|
+
faultPoint('after_execute'); // 执行事实已进 Run 轨迹后才允许被杀 (续跑据此判定"已执行过")
|
|
476
|
+
// ⑨ 统一的"资源结果" + 失败映射 + 证据桥
|
|
477
|
+
const executionOk = exec.execution.ok === true && outChk.ok;
|
|
478
|
+
const goalCriteriaHit = outChk.ok && sources.length > 0 && !!concl.conclusion;
|
|
479
|
+
const evidenceOk = fidelityScore === '通过' && executionOk && goalCriteriaHit;
|
|
480
|
+
let rec = (await updateTransaction(trade.transactionId, {
|
|
481
|
+
execution: { ...exec.execution, ok: exec.execution.ok === true, reason: exec.execution.reason },
|
|
482
|
+
verificationTrust: trust,
|
|
483
|
+
resourceOutcome: {
|
|
484
|
+
installed: fidelityScore === '通过',
|
|
485
|
+
executed: exec.execution.ok === true,
|
|
486
|
+
outputContract: outChk.ok ? 'pass' : 'fail',
|
|
487
|
+
criteriaHit: goalCriteriaHit,
|
|
488
|
+
failureStage: evidenceOk ? undefined : failureStageFor({ installOk: fidelityScore === '通过', producedOutput: exec.output !== undefined, contractOk: outChk.ok }),
|
|
489
|
+
},
|
|
490
|
+
event: { kind: 'resource_outcome', detail: `installed=${fidelityScore === '通过'} executed=${exec.execution.ok === true} contract=${outChk.ok ? 'pass' : 'fail'} criteria=${goalCriteriaHit}` },
|
|
491
|
+
}, home)) ?? trade.record;
|
|
492
|
+
if (paid && !evidenceOk) {
|
|
493
|
+
// M4 口径: 技能**产出了输出**但契约不过 → verification_failed; 压根没产出 (崩/超时/没装上) → delivery_failed
|
|
494
|
+
const producedOutput = exec.output !== undefined;
|
|
495
|
+
const target = mapFailureStatus({ producedOutput, contractOk: outChk.ok });
|
|
496
|
+
rec = (await setTransactionStatus(trade.transactionId, target, `资源未达标: install=${fidelityScore} exec=${exec.execution.ok === true} contract=${outChk.ok ? 'pass' : 'fail'}`, home)) ?? rec;
|
|
497
|
+
}
|
|
498
|
+
const bridge = await bridgeTransactionToRunGoal(rec, {
|
|
499
|
+
runId: run.runId,
|
|
500
|
+
goalId: goal.goalId,
|
|
501
|
+
executionOk,
|
|
502
|
+
goalCriteriaHit,
|
|
503
|
+
summary: `${chosen.name}@${chosen.version}`,
|
|
504
|
+
});
|
|
505
|
+
await markCriterion(goal.goalId, 0, outChk.ok, `输出契约 ${outChk.ok ? '通过' : '未通过'}`);
|
|
506
|
+
await markCriterion(goal.goalId, 1, sources.length > 0, `来源 ${sources.length} 个`);
|
|
507
|
+
await markCriterion(goal.goalId, 2, !!concl.conclusion, concl.conclusion || '无结论字段');
|
|
508
|
+
// ⑩ 报告卡
|
|
509
|
+
const card = buildReportCard({
|
|
510
|
+
task: opts.task,
|
|
511
|
+
conclusion: evidenceOk ? concl.conclusion || '已完成' : '证据不足',
|
|
512
|
+
conclusionDetail: evidenceOk ? undefined : (outChk.ok ? fidelityIssues[0] : (outChk.issues || [])[0]),
|
|
513
|
+
skill: { name: chosen.name, version: chosen.version, dir: installDir },
|
|
514
|
+
cost: { amount: listing.amount, currency: listing.currency, network: listing.network },
|
|
515
|
+
payment: { mode: payment.mode, chainSettled: payment.chainSettled, trust: payment.trust, txHash: payment.txHash },
|
|
516
|
+
sources,
|
|
517
|
+
outputContract: outChk.ok ? '通过' : '未通过',
|
|
518
|
+
resourceVerified: fidelityScore,
|
|
519
|
+
evidenceComplete: evidenceOk,
|
|
520
|
+
executed: exec.execution.ok === true,
|
|
521
|
+
paid,
|
|
522
|
+
stage: 'report',
|
|
523
|
+
durationMs: Date.now() - t0,
|
|
524
|
+
evidenceRef: { goalId: goal.goalId, runId: run.runId, transactionId: trade?.transactionId },
|
|
525
|
+
budgetLines: [...budget.why, ...impact],
|
|
526
|
+
blocker: evidenceOk ? undefined : (outChk.ok ? undefined : `输出不符合资源契约: ${(outChk.issues || [])[0] || '未知'}`),
|
|
527
|
+
});
|
|
528
|
+
await finishRun(run.runId, {
|
|
529
|
+
status: evidenceOk ? 'done' : 'needs_human',
|
|
530
|
+
summary: evidenceOk ? `任务完成: ${concl.conclusion || '已产出'}` : '未达标 (证据/契约不完整)',
|
|
531
|
+
evidence: [`bridge: step=${bridge.stepWritten} evidence=${bridge.evidenceWritten} goal=${bridge.goalEvidenceWritten}`],
|
|
532
|
+
});
|
|
533
|
+
if (evidenceOk)
|
|
534
|
+
await completeGoalIfEligible(goal.goalId);
|
|
535
|
+
mark('report', card.status);
|
|
536
|
+
return {
|
|
537
|
+
ok: card.status === '已完成',
|
|
538
|
+
card,
|
|
539
|
+
text: renderReportCard(card),
|
|
540
|
+
goalId: goal.goalId,
|
|
541
|
+
runId: run.runId,
|
|
542
|
+
transactionId: trade?.transactionId,
|
|
543
|
+
stages,
|
|
544
|
+
budget,
|
|
545
|
+
advisor: advisorBrief,
|
|
546
|
+
payment,
|
|
547
|
+
execution: { ok: exec.execution.ok === true, reason: exec.execution.reason, output: exec.output },
|
|
548
|
+
outputIssues: [...(outChk.issues || []), ...fidelityIssues],
|
|
549
|
+
listingCreated,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* 任务的恢复决策 —— **CLI 的 `task --resume` 与 Supervisor 都只能调这一个函数**。
|
|
554
|
+
* 它把交易层 `planTransactionRecovery` 的结论与 Goal/Run 的现状合起来, 给出唯一行动。
|
|
555
|
+
*/
|
|
556
|
+
export async function decideTaskRecovery(opts) {
|
|
557
|
+
const home = opts.home ?? os.homedir();
|
|
558
|
+
const goal = await readGoal(opts.goalId);
|
|
559
|
+
if (!goal)
|
|
560
|
+
return { action: 'closed', reason: `找不到 Goal ${opts.goalId}`, mustNotRepay: true, alreadyExecuted: false };
|
|
561
|
+
const all = await listTransactions(home);
|
|
562
|
+
let txs = all.filter((t) => t.goalId === opts.goalId);
|
|
563
|
+
if (txs.length === 0) {
|
|
564
|
+
// 同 (任务+预算) 派生的 requestId 是幂等的: 目标可能是"复用已有交易"跑出来的,
|
|
565
|
+
// 那笔交易的 goalId 还是最早的 Goal → 按 requestId 找回它 (否则会误判成"没付过"而重复付款)。
|
|
566
|
+
const m = String((goal.constraints || []).join(' ')).match(/预算 ≤ ([0-9.]+)/);
|
|
567
|
+
const budget = m ? Number(m[1]) : 0.05;
|
|
568
|
+
const rid = defaultRequestId(goal.objective, budget);
|
|
569
|
+
txs = all.filter((t) => t.requestId === rid);
|
|
570
|
+
}
|
|
571
|
+
const latest = txs.sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt)))[0];
|
|
572
|
+
if (!latest) {
|
|
573
|
+
return { action: 'retry_payment', reason: '这个目标还没有过交易 → 从头跑 (没有钱可重复付)', mustNotRepay: false, alreadyExecuted: false };
|
|
574
|
+
}
|
|
575
|
+
const plan = planTransactionRecovery(latest);
|
|
576
|
+
const alreadyExecuted = hasExecutionEvidence(latest) || await goalAlreadyExecuted(goal);
|
|
577
|
+
if (alreadyExecuted && plan.action !== 'complete') {
|
|
578
|
+
// 非幂等保护: 已经执行过就别再跑一遍, 只报告现状
|
|
579
|
+
return { action: 'already_executed', reason: `这笔交易的资源已经执行过 (${latest.status}), 不重复执行非幂等操作 — ${plan.reason}`, mustNotRepay: true, alreadyExecuted: true, transactionId: latest.transactionId };
|
|
580
|
+
}
|
|
581
|
+
return { action: plan.action, reason: plan.reason, mustNotRepay: !!plan.mustNotRepay, alreadyExecuted, transactionId: latest.transactionId };
|
|
582
|
+
}
|
|
583
|
+
/** 按 `decideTaskRecovery` 的结论接着做 (CLI 与 Supervisor 共用同一条路径) */
|
|
584
|
+
export async function resumeTask(opts) {
|
|
585
|
+
const home = opts.home ?? os.homedir();
|
|
586
|
+
const goal = await readGoal(opts.goalId);
|
|
587
|
+
if (!goal) {
|
|
588
|
+
const card = buildReportCard({ task: `(goal ${opts.goalId})`, executed: false, paid: false, conclusion: '证据不足', blocker: `找不到这个 Goal: ${opts.goalId}`, evidenceRef: { goalId: opts.goalId } });
|
|
589
|
+
return { ok: false, resumed: false, action: 'closed', reason: 'goal 不存在', mustNotRepay: false, card, text: renderReportCard(card), goalId: opts.goalId };
|
|
590
|
+
}
|
|
591
|
+
const decision = await decideTaskRecovery({ goalId: opts.goalId, home });
|
|
592
|
+
const base = { action: decision.action, reason: decision.reason, mustNotRepay: decision.mustNotRepay, goalId: opts.goalId, transactionId: decision.transactionId };
|
|
593
|
+
const rec = decision.transactionId ? (await listTransactions(home)).find((t) => t.transactionId === decision.transactionId) : undefined;
|
|
594
|
+
if (decision.action === 'complete') {
|
|
595
|
+
const card = buildReportCard({
|
|
596
|
+
task: goal.objective, conclusion: '已完成', executed: true, paid: true, lifecycle: rec?.status,
|
|
597
|
+
outputContract: '通过', resourceVerified: '通过', evidenceComplete: true,
|
|
598
|
+
payment: rec ? { mode: String(rec.paymentMode), chainSettled: rec.chainSettled === true, trust: String(rec.verificationTrust || '') } : undefined,
|
|
599
|
+
evidenceRef: { goalId: opts.goalId, transactionId: decision.transactionId },
|
|
600
|
+
});
|
|
601
|
+
return { ...base, ok: true, resumed: false, card, text: renderReportCard(card) };
|
|
602
|
+
}
|
|
603
|
+
// 可自动继续的三类: 没付过(retry_payment) / 已付未交付(deliver) / 已交付待执行验真(verify)
|
|
604
|
+
const continuable = ['retry_payment', 'deliver', 'verify'].includes(decision.action);
|
|
605
|
+
if (!continuable) {
|
|
606
|
+
const card = buildReportCard({
|
|
607
|
+
task: goal.objective, executed: decision.alreadyExecuted, paid: true, conclusion: '证据不足',
|
|
608
|
+
blocker: decision.alreadyExecuted
|
|
609
|
+
? `资源已经执行过, 但这次结果不达标 (${decision.reason}) → 需要你处理, 我没有重复执行也没有再花钱`
|
|
610
|
+
: `这笔交易不能自动继续 (${decision.reason}) → 需要你处理, 我没有再花钱`,
|
|
611
|
+
lifecycle: rec?.status, evidenceRef: { goalId: opts.goalId, transactionId: decision.transactionId },
|
|
612
|
+
});
|
|
613
|
+
return { ...base, ok: false, resumed: false, card, text: renderReportCard(card) };
|
|
614
|
+
}
|
|
615
|
+
// retry_payment / deliver / verify → 走同一条闭环 (requestId 派生确定 ⇒ 不会重复扣款)
|
|
616
|
+
const r = await runTask({ task: goal.objective, home, cwd: opts.cwd, skillPaths: opts.skillPaths, input: opts.input, allowLocalDev: opts.allowLocalDev, walletKey: opts.walletKey, reuseGoalId: opts.goalId, onStage: opts.onStage });
|
|
617
|
+
return { ...base, ok: r.ok, resumed: true, card: r.card, text: r.text, runId: r.runId, transactionId: r.transactionId || decision.transactionId };
|
|
618
|
+
}
|
|
@@ -32,22 +32,26 @@ export function bridgeEventFor(status) {
|
|
|
32
32
|
}
|
|
33
33
|
/** 一次交易的证据行 (写进 Run evidence / Goal evidence 的同一组字段) */
|
|
34
34
|
export function transactionEvidenceLines(rec) {
|
|
35
|
+
// 2026-09-18: `goal-store.addEvidence` 每行**截断 300 字** —— 所以审计关键字段必须排在**前面**,
|
|
36
|
+
// 哈希类(长且可从交易记录反查)放后面。真跑抓到过: verificationTrust/executionOk 被截掉。
|
|
35
37
|
return [
|
|
36
38
|
`transactionId=${rec.transactionId}`,
|
|
37
39
|
`itemId=${rec.itemId}`,
|
|
40
|
+
`transactionStatus=${rec.status}`,
|
|
38
41
|
`paymentMode=${rec.paymentMode}`,
|
|
39
42
|
`chainSettled=${rec.chainSettled}`,
|
|
40
43
|
`settlementFact=${rec.settlementFact || '(legacy-未记)'}`, // 两层状态: 钱到底动没动, 审计一眼可见
|
|
41
|
-
`protocolVerified=${rec.protocolVerified === true}`,
|
|
42
|
-
`txHash=${rec.txHash || '(none)'}`,
|
|
43
|
-
`receiptHash=${rec.receiptHash || '(none)'}`,
|
|
44
|
-
`contentHash=${rec.contentHash || '(none)'}`,
|
|
45
44
|
`verificationTrust=${rec.verificationTrust || 'unverified'}`,
|
|
46
|
-
`
|
|
45
|
+
`protocolVerified=${rec.protocolVerified === true}`,
|
|
46
|
+
...(rec.execution ? [`executionOk=${rec.execution.ok === true} schemaOk=${rec.execution.schemaOk === true}`] : []),
|
|
47
|
+
...(rec.resourceOutcome ? [`resourceInstalled=${rec.resourceOutcome.installed} resourceExecuted=${rec.resourceOutcome.executed} outputContract=${rec.resourceOutcome.outputContract} criteriaHit=${rec.resourceOutcome.criteriaHit}${rec.resourceOutcome.failureStage ? ` failureStage=${rec.resourceOutcome.failureStage}` : ''}`] : []),
|
|
47
48
|
...(rec.responsibility ? [`responsibility=${rec.responsibility.type}(${rec.responsibility.reason})`] : []),
|
|
48
49
|
...(rec.milestones?.length ? (() => { const a = aggregateMilestones(rec.milestones); return [`milestones=${a.verified}/${a.total}`, `milestoneSettlement=${a.settlementFact}`]; })() : []),
|
|
49
50
|
...(rec.dispute ? [`dispute=opened(${rec.dispute.reason})`, `disputeResolved=${rec.dispute.resolution ? rec.dispute.resolution.decision : 'no'}`] : []),
|
|
50
|
-
|
|
51
|
+
// 长哈希垫底 (可从交易记录反查; 300 字截断时优先牺牲它们, 而不是上面的判定字段)
|
|
52
|
+
`txHash=${rec.txHash || '(none)'}`,
|
|
53
|
+
`receiptHash=${rec.receiptHash || '(none)'}`,
|
|
54
|
+
`contentHash=${rec.contentHash || '(none)'}`,
|
|
51
55
|
];
|
|
52
56
|
}
|
|
53
57
|
/**
|