@bolloon/bolloon-agent 0.4.22 → 0.4.24

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.
@@ -0,0 +1,193 @@
1
+ /**
2
+ * 手机端 ↔ 电脑端 bolloon 数据同步
3
+ *
4
+ * 登录后 (或设置页手动点「立即同步」) 从电脑端一次拉取全部数据快照:
5
+ * channels (会话) / judgments (判断力) / services (Agent Registry) /
6
+ * resources (数字资源) / networks (已加入网络) / active (活跃身份)
7
+ * 落到本地 → 手机端离线也能读; 重复调用幂等, 不覆盖手机端本机会话.
8
+ *
9
+ * 依赖: 电脑端 server.ts 的 GET /api/mobile/snapshot (已开 CORS).
10
+ * 地址持久化在 localStorage (与 mobile-gateway 的 desktopBaseUrl 同一个 key).
11
+ */
12
+ const URL_KEY = 'bolloon_desktop_base_url';
13
+ const SNAP_KEY = 'bolloon_desktop_snapshot';
14
+ const JUDGE_KEY = 'bolloon_judgments_cache';
15
+ const ORBIT_KEY = 'bolloon_orbit_status';
16
+ function ls() {
17
+ try {
18
+ return typeof localStorage !== 'undefined' ? localStorage : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ /** 电脑端地址 (如 http://192.168.1.5:7788) — 与 mobile-gateway 共用同一 key */
25
+ export function getDesktopUrl() {
26
+ try {
27
+ return ls()?.getItem(URL_KEY) || '';
28
+ }
29
+ catch {
30
+ return '';
31
+ }
32
+ }
33
+ export function setDesktopUrl(url) {
34
+ try {
35
+ ls()?.setItem(URL_KEY, String(url || '').trim().replace(/\/+$/, ''));
36
+ }
37
+ catch { /* 忽略 */ }
38
+ }
39
+ export function getLastSnapshot() {
40
+ try {
41
+ const s = ls()?.getItem(SNAP_KEY);
42
+ return s ? JSON.parse(s) : null;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ /** 电脑端同步下来的判断力库 (判断力 API 页离线读取) */
49
+ export function getCachedJudgments() {
50
+ try {
51
+ const s = ls()?.getItem(JUDGE_KEY);
52
+ const a = s ? JSON.parse(s) : [];
53
+ return Array.isArray(a) ? a : [];
54
+ }
55
+ catch {
56
+ return [];
57
+ }
58
+ }
59
+ export function getSyncStatus() {
60
+ const snap = getLastSnapshot();
61
+ let orbit = null;
62
+ try {
63
+ const s = ls()?.getItem(ORBIT_KEY);
64
+ orbit = s ? JSON.parse(s) : null;
65
+ }
66
+ catch {
67
+ orbit = null;
68
+ }
69
+ return { url: getDesktopUrl(), lastTs: (snap && snap.ts) || 0, counts: (snap && snap.counts) || {}, orbit };
70
+ }
71
+ /** OrbitDB 库级复制: 本地副本 ↔ 电脑端 store (双向 merge). 独立入口, 便于本地写后立即推回. */
72
+ export async function replicateOrbit(opts = {}) {
73
+ try {
74
+ const o = await import('./mobile-orbit.js');
75
+ const r = await o.replicateAll({ baseUrl: getDesktopUrl(), fetchImpl: opts.fetchImpl });
76
+ const out = { ok: r.ok, stores: r.stores, pulled: r.pulled, pushed: r.pushed, entries: r.entries, error: r.error };
77
+ try {
78
+ ls()?.setItem(ORBIT_KEY, JSON.stringify(out));
79
+ }
80
+ catch { /* 忽略 */ }
81
+ return out;
82
+ }
83
+ catch (e) {
84
+ const out = { ok: false, stores: 0, pulled: 0, pushed: 0, entries: 0, error: e?.message || String(e) };
85
+ try {
86
+ ls()?.setItem(ORBIT_KEY, JSON.stringify(out));
87
+ }
88
+ catch { /* 忽略 */ }
89
+ return out;
90
+ }
91
+ }
92
+ /**
93
+ * 从电脑端拉取全部数据快照并落地本地.
94
+ * - 未配置地址 → 明确报错 (不静默)
95
+ * - 网络/解析失败 → 返回 error, 保留上一次快照
96
+ */
97
+ export async function syncFromDesktop(baseUrl, opts = {}) {
98
+ const base = String(baseUrl || getDesktopUrl() || '').trim().replace(/\/+$/, '');
99
+ if (!base)
100
+ return { ok: false, error: '未配置电脑端地址 (设置 → 电脑端同步)' };
101
+ const f = opts.fetchImpl || (typeof fetch !== 'undefined' ? fetch : null);
102
+ if (!f)
103
+ return { ok: false, error: '当前环境无 fetch' };
104
+ try {
105
+ const r = await f(`${base}/api/mobile/snapshot`, { method: 'GET' });
106
+ if (!r.ok)
107
+ return { ok: false, error: `电脑端返回 ${r.status}` };
108
+ const snap = (await r.json());
109
+ if (!snap || snap.ok === false)
110
+ return { ok: false, error: snap?.error || '快照无效' };
111
+ try {
112
+ ls()?.setItem(SNAP_KEY, JSON.stringify(snap));
113
+ }
114
+ catch { /* 存储满 → 忽略 */ }
115
+ if (Array.isArray(snap.judgments)) {
116
+ try {
117
+ ls()?.setItem(JUDGE_KEY, JSON.stringify(snap.judgments));
118
+ }
119
+ catch { /* 忽略 */ }
120
+ }
121
+ // 顺手做 OrbitDB 库级复制 (手机端本地副本 ↔ 电脑端 store, 双向 merge); 失败不影响快照结果
122
+ let orbit;
123
+ try {
124
+ const o = await import('./mobile-orbit.js');
125
+ const r = await o.replicateAll({ baseUrl: base, fetchImpl: opts.fetchImpl });
126
+ orbit = { ok: r.ok, stores: r.stores, pulled: r.pulled, pushed: r.pushed, entries: r.entries, error: r.error };
127
+ try {
128
+ ls()?.setItem(ORBIT_KEY, JSON.stringify(orbit));
129
+ }
130
+ catch { /* 忽略 */ }
131
+ }
132
+ catch (e) {
133
+ orbit = { ok: false, stores: 0, pulled: 0, pushed: 0, entries: 0, error: e?.message || String(e) };
134
+ }
135
+ return { ok: true, counts: snap.counts || {}, ts: snap.ts, orbit };
136
+ }
137
+ catch (e) {
138
+ return { ok: false, error: e?.message || String(e) };
139
+ }
140
+ }
141
+ /**
142
+ * 电脑端可拨的 P2P ws 地址。
143
+ * 手机(WebView)不能 listen → 只能主动拨入桌面节点; 桌面 listen 在 0.0.0.0:<随机端口>/ws,
144
+ * 所以要把 0.0.0.0/127.0.0.1 改写成手机实际访问的桌面主机(端口保持桌面真实端口)。
145
+ */
146
+ export async function desktopP2PAddrs(opts = {}) {
147
+ const base = getDesktopUrl();
148
+ if (!base)
149
+ return { ok: false, addrs: [], error: '未配置电脑端地址 (设置 → 电脑端同步)' };
150
+ const f = opts.fetchImpl || (typeof fetch !== 'undefined' ? fetch : null);
151
+ if (!f)
152
+ return { ok: false, addrs: [], error: '当前环境无 fetch' };
153
+ try {
154
+ let host = '';
155
+ try {
156
+ host = new URL(base).hostname;
157
+ }
158
+ catch { /* 忽略 */ }
159
+ const r = await f(`${base.replace(/\/+$/, '')}/api/p2p/mobile-connect`);
160
+ if (!r.ok)
161
+ return { ok: false, addrs: [], error: `电脑端返回 ${r.status}` };
162
+ const j = await r.json();
163
+ const raw = Array.isArray(j?.wsAddrs) ? j.wsAddrs : [];
164
+ const rewrite = (a) => {
165
+ let out = a;
166
+ if (host)
167
+ out = out.replace(/\/ip4\/(0\.0\.0\.0|127\.0\.0\.1)\//, `/ip4/${host}/`);
168
+ out = out.replace(/\/ip6\/::1\//, `/ip4/${host || '127.0.0.1'}/`);
169
+ return out;
170
+ };
171
+ const keep = (a) => ip4Of(a) && !/\/ip4\/(0\.0\.0\.0|127\.0\.0\.1)\//.test(a);
172
+ const addrs = raw.map(rewrite).filter(keep);
173
+ const relayRaw = Array.isArray(j?.relayAddrs) ? j.relayAddrs : [];
174
+ const relayAddrs = relayRaw.map(rewrite).filter(keep);
175
+ const out = { ok: j?.ok !== false, peerId: j?.peerId || '', addrs: Array.from(new Set(addrs)) };
176
+ if (j?.isRelay === true)
177
+ out.isRelay = true;
178
+ if (relayAddrs.length)
179
+ out.relayAddrs = Array.from(new Set(relayAddrs));
180
+ if (typeof j?.relayProtocol === 'string')
181
+ out.relayProtocol = j.relayProtocol;
182
+ return out;
183
+ }
184
+ catch (e) {
185
+ return { ok: false, addrs: [], error: e?.message || String(e) };
186
+ }
187
+ }
188
+ /** 取多地址里的 ip4 (无则空串) */
189
+ function ip4Of(addr) {
190
+ const m = /\/ip4\/([^/]+)\//.exec(String(addr || ''));
191
+ return m ? m[1] : '';
192
+ }
193
+ export default { getDesktopUrl, setDesktopUrl, getLastSnapshot, getCachedJudgments, getSyncStatus, syncFromDesktop, replicateOrbit, desktopP2PAddrs };
@@ -0,0 +1,492 @@
1
+ /**
2
+ * mobile-trade.ts — 手机端资源交易工作流 (Agent Economic Loop E2/E3/E4)
3
+ *
4
+ * 遵循 docs/wiki/agent-economic-protocol.md:
5
+ * E2 x402 支付闭环 — buyer → POST /agent/service/call → 402 Payment Required(price+wallet)
6
+ * → x402_pay → 200 service result
7
+ * E3 Policy Engine — 单笔限额 / 日累计预算 / 收款方白名单 / 服务白名单 / 信誉阈值
8
+ * (纯函数判定 allow|confirm|deny, 非 AI 决策; 私钥隔离 — LLM 不见私钥)
9
+ * E4 Reputation — 结算后 tasks++ / success|failed|disputed → score = success/tasks
10
+ *
11
+ * 手机端离线可用: 交易记录/信誉落 localStorage (可注入 storage 便于单测).
12
+ * 全部外部依赖可注入 (fetchImpl / payFn / walletForAgent / policy), 单测无需浏览器.
13
+ * 本模块被 mobile-core.ts import → esbuild bundle 进 mobile-core.js.
14
+ */
15
+ // ============ 常量 ============
16
+ export const TRADE_STORE_KEY = 'bolloon_trades';
17
+ export const REP_STORE_KEY = 'bolloon_service_reputation';
18
+ export const DEFAULT_TRADE_POLICY = {
19
+ perTransactionLimit: 1, // 单笔 ≤ 1
20
+ dailyLimit: 10, // 日累计 ≤ 10
21
+ allowedRecipients: [],
22
+ allowedServices: [],
23
+ reputationThreshold: 0,
24
+ };
25
+ const ZERO_REP = { tasks: 0, success: 0, failed: 0, disputed: 0, score: 0 };
26
+ // ============ 基础设施 (storage / 工具) ============
27
+ function ls() {
28
+ try {
29
+ if (typeof localStorage !== 'undefined' && localStorage)
30
+ return localStorage;
31
+ }
32
+ catch { /* 无 localStorage */ }
33
+ return null;
34
+ }
35
+ /** 无 localStorage 时的进程内回退 (Node 测试/SSR) */
36
+ const mem = new Map();
37
+ const MEM_STORAGE = {
38
+ getItem: (k) => (mem.has(k) ? mem.get(k) : null),
39
+ setItem: (k, v) => { mem.set(k, String(v)); },
40
+ };
41
+ function storageOf(injected) {
42
+ return injected || ls() || MEM_STORAGE;
43
+ }
44
+ function nowDefault() { return Date.now(); }
45
+ function ridDefault() {
46
+ return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
47
+ }
48
+ function round2(n) { return Math.round(n * 100) / 100; }
49
+ function numOf(v) {
50
+ const n = typeof v === 'number' ? v : Number(String(v ?? '').trim());
51
+ return Number.isFinite(n) ? n : NaN;
52
+ }
53
+ function sameDay(a, b) {
54
+ return new Date(a).toISOString().slice(0, 10) === new Date(b).toISOString().slice(0, 10);
55
+ }
56
+ function serviceKey(service) {
57
+ return String(service?.agentId || service?.payTo || service?.name || 'unknown');
58
+ }
59
+ // ============ E3: Policy Engine (纯函数) ============
60
+ /**
61
+ * 纯函数策略判定 — 非 AI 决策 (对应 E3 / economic-policy.ts).
62
+ * 顺序: 非法金额 → 单笔限额 → 收款方白名单 → 服务白名单 → 信誉阈值 → 日累计 → confirm 软阈值 → allow.
63
+ */
64
+ export function evaluatePolicy(intent, policy = DEFAULT_TRADE_POLICY, opts = {}) {
65
+ const dailySpent = numOf(opts.dailySpent) || 0;
66
+ const amount = numOf(intent?.amount);
67
+ const payTo = String(intent?.payTo || '').toLowerCase();
68
+ const service = String(intent?.service || '').toLowerCase();
69
+ const base = { dailySpent };
70
+ if (!Number.isFinite(amount) || amount < 0) {
71
+ return { decision: 'deny', reason: `非法金额: ${String(intent?.amount)}`, rule: 'invalid_amount', ...base };
72
+ }
73
+ if (!payTo) {
74
+ return { decision: 'deny', reason: '缺少收款方 (payTo)', rule: 'missing_payto', ...base };
75
+ }
76
+ // 1. 单笔限额
77
+ if (amount > policy.perTransactionLimit) {
78
+ return { decision: 'deny', reason: `单笔超限: ${amount} > ${policy.perTransactionLimit}`, rule: 'per_transaction_limit', ...base };
79
+ }
80
+ // 2. 收款方白名单 (空 = 全部允许)
81
+ if (policy.allowedRecipients.length > 0 && !policy.allowedRecipients.some((r) => String(r).toLowerCase() === payTo)) {
82
+ return { decision: 'deny', reason: `收款方不在白名单: ${payTo}`, rule: 'recipient_whitelist', ...base };
83
+ }
84
+ // 3. 服务白名单 (空 = 全部允许)
85
+ if (policy.allowedServices.length > 0 && service && !policy.allowedServices.some((s) => String(s).toLowerCase() === service)) {
86
+ return { decision: 'deny', reason: `服务不在白名单: ${service}`, rule: 'service_whitelist', ...base };
87
+ }
88
+ // 4. 信誉阈值
89
+ const rep = numOf(intent?.reputation);
90
+ const repVal = Number.isFinite(rep) ? rep : 0;
91
+ if (policy.reputationThreshold > 0 && repVal < policy.reputationThreshold) {
92
+ return { decision: 'deny', reason: `服务方信誉不足: ${repVal} < ${policy.reputationThreshold}`, rule: 'reputation_threshold', ...base };
93
+ }
94
+ // 5. 日累计预算
95
+ if (dailySpent + amount > policy.dailyLimit) {
96
+ return { decision: 'deny', reason: `日累计超限: ${dailySpent} + ${amount} > ${policy.dailyLimit}`, rule: 'daily_limit', ...base };
97
+ }
98
+ // 6. confirm 软阈值 (人工审批, 不自动签名)
99
+ const confirmAbove = opts.requireApprovalAbove ?? policy.confirmAbove;
100
+ if (confirmAbove !== undefined && Number.isFinite(confirmAbove) && amount > confirmAbove) {
101
+ return { decision: 'confirm', reason: `金额 ${amount} > 审批阈值 ${confirmAbove}, 需人工批准`, rule: 'confirm_above', ...base };
102
+ }
103
+ return { decision: 'allow', reason: '策略通过', rule: 'allow', ...base };
104
+ }
105
+ // ============ E2: 报价 / 校验价格结构 ============
106
+ /** 生成报价 + 校验价格结构 (纯函数; 非法价格 → ok:false) */
107
+ export function quoteService(service, request = {}, opts = {}) {
108
+ const now = opts.now || nowDefault;
109
+ const rid = opts.rid || ridDefault;
110
+ const ts = request.timestamp ?? now();
111
+ const requestId = String(request.requestId || '').trim() || rid();
112
+ const name = String(service?.name || '').trim();
113
+ const quantity = Number.isFinite(numOf(request.quantity)) && numOf(request.quantity) > 0 ? numOf(request.quantity) : 1;
114
+ const price = service?.price;
115
+ const currency = String(price?.currency || '').trim().toUpperCase();
116
+ const payTo = String(service?.payTo || '').trim();
117
+ const fail = (error) => ({
118
+ ok: false, error, serviceName: name, amount: NaN, currency, payTo,
119
+ per: price?.per, quantity, requestId, fingerprint: '', ts, endpoint: request.endpoint || service?.endpoint,
120
+ });
121
+ if (!name)
122
+ return fail('服务缺少 name');
123
+ if (!price)
124
+ return fail('服务缺少价格 (price)');
125
+ const unit = numOf(price.amount);
126
+ if (!Number.isFinite(unit) || unit < 0)
127
+ return fail(`价格结构非法: amount=${String(price.amount)}`);
128
+ if (!currency)
129
+ return fail('价格缺少 currency');
130
+ if (!payTo)
131
+ return fail('服务缺少收款钱包 (payTo)');
132
+ const amount = round2(unit * quantity);
133
+ const fingerprint = `${requestId}|${name}|${amount}|${currency}|${ts}`;
134
+ return {
135
+ ok: true, serviceName: name, amount, currency, payTo,
136
+ per: price.per, quantity, requestId, fingerprint, ts,
137
+ endpoint: String(request.endpoint || service?.endpoint || '').trim() || undefined,
138
+ };
139
+ }
140
+ // ============ E2: 交易记录存取 (localStorage) ============
141
+ export function listTrades(storage, opts = {}) {
142
+ const s = storageOf(storage);
143
+ let raw = null;
144
+ try {
145
+ raw = s.getItem(TRADE_STORE_KEY);
146
+ }
147
+ catch {
148
+ raw = null;
149
+ }
150
+ if (!raw)
151
+ return [];
152
+ let arr = [];
153
+ try {
154
+ const p = JSON.parse(raw);
155
+ if (Array.isArray(p))
156
+ arr = p;
157
+ }
158
+ catch {
159
+ arr = [];
160
+ }
161
+ // 倒序: 新的在前 (ts 大者在前, 同 ts 按写入顺序靠后者在前)
162
+ const ordered = arr.map((r, i) => ({ r, i })).sort((a, b) => (b.r.ts - a.r.ts) || (b.i - a.i)).map((x) => x.r);
163
+ return opts.limit && opts.limit > 0 ? ordered.slice(0, opts.limit) : ordered;
164
+ }
165
+ export function appendTrade(rec, storage) {
166
+ const s = storageOf(storage);
167
+ const full = {
168
+ requestId: String(rec.requestId || ''),
169
+ service: String(rec.service || ''),
170
+ amount: numOf(rec.amount) || 0,
171
+ currency: String(rec.currency || 'USDC'),
172
+ txHash: rec.txHash,
173
+ status: rec.status,
174
+ ts: typeof rec.ts === 'number' ? rec.ts : nowDefault(),
175
+ payTo: rec.payTo,
176
+ reason: rec.reason,
177
+ fingerprint: rec.fingerprint,
178
+ };
179
+ const prev = listTrades(s).reverse(); // 还原写入顺序 (旧→新)
180
+ prev.push(full);
181
+ try {
182
+ s.setItem(TRADE_STORE_KEY, JSON.stringify(prev));
183
+ }
184
+ catch { /* 存储满 → 忽略 */ }
185
+ return full;
186
+ }
187
+ export function clearTrades(storage) {
188
+ const s = storageOf(storage);
189
+ try {
190
+ s.setItem(TRADE_STORE_KEY, '[]');
191
+ }
192
+ catch { /* 忽略 */ }
193
+ }
194
+ /** 防重放: 相同 requestId 已存在 → true (请求 ID + 金额 + 服务 + 时间戳 绑定) */
195
+ export function isReplayed(requestId, storage) {
196
+ if (!requestId)
197
+ return false;
198
+ return listTrades(storage).some((t) => t.requestId === requestId);
199
+ }
200
+ // ============ 信誉存储 (E4) ============
201
+ function repStoreOf(injected) {
202
+ if (injected)
203
+ return injected;
204
+ const s = storageOf();
205
+ return {
206
+ get: (k) => {
207
+ try {
208
+ const raw = s.getItem(REP_STORE_KEY);
209
+ const m = raw ? JSON.parse(raw) : {};
210
+ return (m && m[k]) ? { ...ZERO_REP, ...m[k] } : { ...ZERO_REP };
211
+ }
212
+ catch {
213
+ return { ...ZERO_REP };
214
+ }
215
+ },
216
+ set: (k, rep) => {
217
+ try {
218
+ const raw = s.getItem(REP_STORE_KEY);
219
+ const m = raw ? JSON.parse(raw) : {};
220
+ m[k] = rep;
221
+ s.setItem(REP_STORE_KEY, JSON.stringify(m));
222
+ }
223
+ catch { /* 忽略 */ }
224
+ },
225
+ };
226
+ }
227
+ /** E4: 结算后更新服务方信誉 — tasks++ / success|failed|disputed → score = success/tasks */
228
+ export async function settleAndRate(params) {
229
+ const deps = params.deps || {};
230
+ const service = params.service || {};
231
+ const key = serviceKey(service);
232
+ const outcome = typeof params.ok === 'string'
233
+ ? params.ok
234
+ : (params.ok ? 'success' : 'failed');
235
+ if (!key || key === 'unknown') {
236
+ return { ok: false, key, outcome, reputation: { ...ZERO_REP }, score: 0, error: '无法定位服务方 (缺 agentId/payTo/name)' };
237
+ }
238
+ try {
239
+ const store = repStoreOf(deps.reputationStore);
240
+ const cur = { ...ZERO_REP, ...(await store.get(key) || {}) };
241
+ cur.tasks += 1;
242
+ if (outcome === 'success')
243
+ cur.success += 1;
244
+ else if (outcome === 'failed')
245
+ cur.failed += 1;
246
+ else
247
+ cur.disputed += 1;
248
+ cur.score = cur.tasks > 0 ? round2(cur.success / cur.tasks) : 0;
249
+ await store.set(key, cur);
250
+ return { ok: true, key, outcome, reputation: cur, score: cur.score };
251
+ }
252
+ catch (e) {
253
+ return { ok: false, key, outcome, reputation: { ...ZERO_REP }, score: 0, error: `信誉更新失败: ${String(e?.message || e)}` };
254
+ }
255
+ }
256
+ export async function getReputationOf(service, deps = {}) {
257
+ try {
258
+ return { ...ZERO_REP, ...(await repStoreOf(deps.reputationStore).get(serviceKey(service)) || {}) };
259
+ }
260
+ catch {
261
+ return { ...ZERO_REP };
262
+ }
263
+ }
264
+ // ============ E2: 402 解析 ============
265
+ function headerOf(res, name) {
266
+ const h = res?.headers;
267
+ if (!h)
268
+ return undefined;
269
+ const g = h.get;
270
+ if (typeof g === 'function') {
271
+ const v = g.call(h, name);
272
+ return v == null ? undefined : String(v);
273
+ }
274
+ const lower = name.toLowerCase();
275
+ for (const k of Object.keys(h)) {
276
+ if (k.toLowerCase() === lower) {
277
+ const v = h[k];
278
+ return v == null ? undefined : String(v);
279
+ }
280
+ }
281
+ return undefined;
282
+ }
283
+ async function bodyOf(res) {
284
+ const r = res;
285
+ try {
286
+ if (typeof r?.json === 'function')
287
+ return await r.json();
288
+ }
289
+ catch { /* 非 JSON */ }
290
+ try {
291
+ if (typeof r?.text === 'function') {
292
+ const t = await r.text();
293
+ if (!t)
294
+ return null;
295
+ try {
296
+ return JSON.parse(t);
297
+ }
298
+ catch {
299
+ return t;
300
+ }
301
+ }
302
+ }
303
+ catch { /* 无 body */ }
304
+ return r?.body ?? null;
305
+ }
306
+ /** 解析 402 Payment Required 的价格 (header 优先, 其次 body.payment) */
307
+ export function parse402(res, body) {
308
+ const b = (body && typeof body === 'object' ? body : {}) || {};
309
+ const p = (b.payment || {});
310
+ const amountRaw = headerOf(res, 'X-Payment-Amount') ?? p.amount ?? p.price;
311
+ const currencyRaw = headerOf(res, 'X-Payment-Currency') ?? p.currency ?? 'USDC';
312
+ const payToRaw = headerOf(res, 'X-Pay-To') ?? p.payTo ?? p.wallet ?? p.to;
313
+ const networkRaw = headerOf(res, 'X-Payment-Network') ?? p.network;
314
+ const amount = numOf(amountRaw);
315
+ if (!Number.isFinite(amount) || amount < 0)
316
+ return null;
317
+ if (!payToRaw)
318
+ return null;
319
+ const currency = String(currencyRaw || 'USDC').toUpperCase();
320
+ return { amount, currency, payTo: String(payToRaw), network: networkRaw ? String(networkRaw) : undefined };
321
+ }
322
+ // ============ 默认支付执行器 (签名隔离) ============
323
+ async function defaultPayFn(deps, walletId, spec) {
324
+ if (deps.payFn)
325
+ return deps.payFn(spec);
326
+ if (!deps.getPrivateKey)
327
+ return { success: false, error: '未提供 payFn / getPrivateKey (无法签名)' };
328
+ if (!walletId)
329
+ return { success: false, error: 'agent 未被授权任何钱包 (walletForAgent)' };
330
+ const pk = await deps.getPrivateKey(walletId);
331
+ if (!pk)
332
+ return { success: false, error: '钱包已锁定, 请先解锁 (私钥仅在解锁后可取)' };
333
+ if (!deps.x402Pay)
334
+ return { success: false, error: '未注入 x402Pay 适配器 (桌面端 x402 未接线)' };
335
+ try {
336
+ const r = await deps.x402Pay({
337
+ privateKey: pk, amount: String(spec.amount), to: spec.to,
338
+ currency: spec.currency, network: spec.network || deps.network, memo: spec.memo,
339
+ });
340
+ return { success: !!r.success, txHash: r.txHash, error: r.error };
341
+ }
342
+ catch (e) {
343
+ return { success: false, error: `x402 支付异常: ${String(e?.message || e)}` };
344
+ }
345
+ }
346
+ async function resolveWallet(deps, buyer) {
347
+ if (!deps.walletForAgent)
348
+ return {};
349
+ if (!buyer)
350
+ return { error: '缺少 buyer agentId (无法定位钱包)' };
351
+ const info = await deps.walletForAgent(buyer);
352
+ if (!info || !info.exists || !info.wallets?.length)
353
+ return { error: info?.error || `agent ${buyer} 未被授权任何钱包` };
354
+ const w = info.wallets.find((x) => x.unlocked) || info.wallets[0];
355
+ return { id: w.id, address: w.address };
356
+ }
357
+ // ============ E2: 402 → 支付 → 取结果 闭环 ============
358
+ /**
359
+ * 调用付费服务: POST endpoint → 402(price+wallet) → 策略门 → x402_pay → 200 result.
360
+ * 策略 confirm/deny 时**不支付**; 支付失败**不写成功记录**; 相同 requestId 直接判重放.
361
+ */
362
+ export async function callService(params) {
363
+ const deps = params.deps || {};
364
+ const service = params.service;
365
+ const request = params.request || {};
366
+ const now = deps.now || nowDefault;
367
+ const policy = deps.policy || DEFAULT_TRADE_POLICY;
368
+ // 1. 报价 + 价格结构校验
369
+ const quote = quoteService(service, request, { now, rid: deps.rid });
370
+ const requestId = quote.requestId;
371
+ if (!quote.ok) {
372
+ return { ok: false, requestId, status: 'failed', error: quote.error, quote };
373
+ }
374
+ // 2. 防重放 (请求 ID + 金额 + 服务 + 时间戳 绑定)
375
+ if (isReplayed(requestId, deps.storage)) {
376
+ return { ok: false, requestId, status: 'replayed', replayed: true, reason: `重放请求被拒: ${requestId}`, quote };
377
+ }
378
+ // 3. 信誉 (E4) — service.reputation 优先, 否则查信誉库
379
+ let reputation = numOf(service.reputation);
380
+ if (!Number.isFinite(reputation) && deps.getReputation) {
381
+ try {
382
+ reputation = numOf(await deps.getReputation(service));
383
+ }
384
+ catch {
385
+ reputation = 0;
386
+ }
387
+ }
388
+ const repVal = Number.isFinite(reputation) ? reputation : 0;
389
+ // 4. 今日已花费 (日累计预算)
390
+ let dailySpent = 0;
391
+ if (deps.dailySpent) {
392
+ try {
393
+ dailySpent = numOf(await deps.dailySpent()) || 0;
394
+ }
395
+ catch {
396
+ dailySpent = 0;
397
+ }
398
+ }
399
+ else
400
+ dailySpent = listTrades(deps.storage).filter((t) => t.status === 'success' && sameDay(t.ts, now())).reduce((s, t) => s + (numOf(t.amount) || 0), 0);
401
+ // 5. Policy Engine (E3) — 非 AI 决策
402
+ const verdict = evaluatePolicy({ payTo: quote.payTo, amount: quote.amount, currency: quote.currency, service: quote.serviceName, requestId, reputation: repVal, timestamp: quote.ts }, policy, { dailySpent });
403
+ if (verdict.decision === 'deny') {
404
+ appendTrade({ requestId, service: quote.serviceName, amount: quote.amount, currency: quote.currency, status: 'denied', payTo: quote.payTo, reason: verdict.reason, fingerprint: quote.fingerprint, ts: quote.ts }, deps.storage);
405
+ return { ok: false, requestId, status: 'denied', denied: true, reason: verdict.reason, verdict, quote };
406
+ }
407
+ if (verdict.decision === 'confirm') {
408
+ appendTrade({ requestId, service: quote.serviceName, amount: quote.amount, currency: quote.currency, status: 'pending_approval', payTo: quote.payTo, reason: verdict.reason, fingerprint: quote.fingerprint, ts: quote.ts }, deps.storage);
409
+ return { ok: false, requestId, status: 'pending_approval', needsApproval: true, reason: verdict.reason, verdict, quote };
410
+ }
411
+ // 6. 钱包 (buyer)
412
+ const wallet = await resolveWallet(deps, request.buyer);
413
+ if (!deps.payFn && wallet.error) {
414
+ return { ok: false, requestId, status: 'failed', error: wallet.error, verdict, quote };
415
+ }
416
+ const f = deps.fetchImpl || (typeof fetch !== 'undefined' ? fetch : null);
417
+ if (!f)
418
+ return { ok: false, requestId, status: 'failed', error: '当前环境无 fetch', verdict, quote };
419
+ const endpoint = quote.endpoint || String(service?.endpoint || '').trim();
420
+ if (!endpoint)
421
+ return { ok: false, requestId, status: 'failed', error: '服务缺少 endpoint', verdict, quote };
422
+ const reqBody = JSON.stringify(request.body ?? {});
423
+ const baseHeaders = { 'Content-Type': 'application/json', 'X-Request-Id': requestId };
424
+ try {
425
+ // ① 首次请求 → 期望 402
426
+ const first = await f(endpoint, { method: 'POST', headers: baseHeaders, body: reqBody });
427
+ const firstStatus = Number(first?.status ?? 0);
428
+ const firstBody = await bodyOf(first);
429
+ if (firstStatus === 402) {
430
+ const req = parse402(first, firstBody);
431
+ if (!req) {
432
+ appendTrade({ requestId, service: quote.serviceName, amount: quote.amount, currency: quote.currency, status: 'failed', payTo: quote.payTo, reason: '402 响应无法解析', fingerprint: quote.fingerprint, ts: quote.ts }, deps.storage);
433
+ return { ok: false, requestId, status: 'failed', error: '402 响应缺少价格/收款钱包', verdict, quote };
434
+ }
435
+ // 价格完整性: 402 声明金额/币种/收款方必须与报价一致 (防抬价/篡改)
436
+ if (req.amount !== quote.amount || req.currency !== quote.currency || req.payTo.toLowerCase() !== quote.payTo.toLowerCase()) {
437
+ appendTrade({ requestId, service: quote.serviceName, amount: quote.amount, currency: quote.currency, status: 'denied', payTo: quote.payTo, reason: '402 价格与报价不符', fingerprint: quote.fingerprint, ts: quote.ts }, deps.storage);
438
+ return { ok: false, requestId, status: 'denied', denied: true, reason: `402 价格与报价不符 (报价 ${quote.amount} ${quote.currency} → 402 ${req.amount} ${req.currency})`, verdict, quote };
439
+ }
440
+ // ② 支付 (签名隔离: 私钥仅在 defaultPayFn 内可见)
441
+ const pay = await defaultPayFn(deps, wallet.id, { amount: req.amount, currency: req.currency, to: req.payTo, memo: `${quote.serviceName} ${requestId}`, network: req.network });
442
+ if (!pay.success) {
443
+ appendTrade({ requestId, service: quote.serviceName, amount: quote.amount, currency: quote.currency, status: 'failed', payTo: quote.payTo, reason: pay.error || '支付失败', fingerprint: quote.fingerprint, ts: quote.ts }, deps.storage);
444
+ if (deps.reputationStore)
445
+ await settleAndRate({ ok: false, service, deps });
446
+ return { ok: false, requestId, status: 'failed', error: pay.error || '支付失败', verdict, quote };
447
+ }
448
+ // ③ 携带支付凭据重试 → 200 取结果
449
+ const proof = pay.proof || pay.txHash || '';
450
+ const second = await f(endpoint, {
451
+ method: 'POST',
452
+ headers: { ...baseHeaders, 'X-Payment': proof, 'X-Payment-TxHash': pay.txHash || '', 'X-Payment-Request-Id': requestId },
453
+ body: reqBody,
454
+ });
455
+ const secondStatus = Number(second?.status ?? 0);
456
+ const secondOk = second?.ok ?? (secondStatus >= 200 && secondStatus < 300);
457
+ const secondBody = await bodyOf(second);
458
+ if (!secondOk) {
459
+ appendTrade({ requestId, service: quote.serviceName, amount: quote.amount, currency: quote.currency, status: 'failed', txHash: pay.txHash, payTo: quote.payTo, reason: `支付后重试失败 ${secondStatus}`, fingerprint: quote.fingerprint, ts: quote.ts }, deps.storage);
460
+ if (deps.reputationStore)
461
+ await settleAndRate({ ok: false, service, deps });
462
+ return { ok: false, requestId, status: 'failed', txHash: pay.txHash, error: `支付后重试失败 (${secondStatus})`, verdict, quote };
463
+ }
464
+ appendTrade({ requestId, service: quote.serviceName, amount: quote.amount, currency: quote.currency, status: 'success', txHash: pay.txHash, payTo: quote.payTo, fingerprint: quote.fingerprint, ts: quote.ts }, deps.storage);
465
+ if (deps.reputationStore)
466
+ await settleAndRate({ ok: true, service, deps });
467
+ return { ok: true, requestId, status: 'success', result: secondBody, txHash: pay.txHash, paid: quote.amount, currency: quote.currency, verdict, quote };
468
+ }
469
+ // 非 402: 免费服务 (200) 或错误
470
+ const firstOk = first?.ok ?? (firstStatus >= 200 && firstStatus < 300);
471
+ if (firstOk) {
472
+ if (quote.amount > 0) {
473
+ // 付费服务却直接放行 → 保守: 记录但标记异常
474
+ return { ok: false, requestId, status: 'failed', error: `服务声明收费 ${quote.amount} ${quote.currency} 但未返回 402`, verdict, quote };
475
+ }
476
+ appendTrade({ requestId, service: quote.serviceName, amount: 0, currency: quote.currency, status: 'success', payTo: quote.payTo, fingerprint: quote.fingerprint, ts: quote.ts }, deps.storage);
477
+ return { ok: true, requestId, status: 'success', result: firstBody, paid: 0, currency: quote.currency, verdict, quote };
478
+ }
479
+ appendTrade({ requestId, service: quote.serviceName, amount: quote.amount, currency: quote.currency, status: 'failed', payTo: quote.payTo, reason: `服务返回 ${firstStatus}`, fingerprint: quote.fingerprint, ts: quote.ts }, deps.storage);
480
+ return { ok: false, requestId, status: 'failed', error: `服务返回 ${firstStatus}`, verdict, quote };
481
+ }
482
+ catch (e) {
483
+ const msg = String(e?.message || e);
484
+ appendTrade({ requestId, service: quote.serviceName, amount: quote.amount, currency: quote.currency, status: 'failed', payTo: quote.payTo, reason: msg, fingerprint: quote.fingerprint, ts: quote.ts }, deps.storage);
485
+ return { ok: false, requestId, status: 'failed', error: `调用失败: ${msg}`, verdict, quote };
486
+ }
487
+ }
488
+ export default {
489
+ evaluatePolicy, quoteService, callService, settleAndRate, getReputationOf,
490
+ appendTrade, listTrades, clearTrades, isReplayed, parse402,
491
+ DEFAULT_TRADE_POLICY,
492
+ };