@bolloon/bolloon-agent 0.4.11 → 0.4.13
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/LICENSE +21 -0
- package/README.md +14 -8
- package/dist/agents/agent-registry.js +151 -0
- package/dist/agents/agent-reputation.js +54 -0
- package/dist/agents/agent-service-client.js +147 -0
- package/dist/agents/economic-policy.js +132 -0
- package/dist/agents/payment-approval.js +142 -0
- package/dist/agents/payment-gate.js +99 -0
- package/dist/agents/pi-sdk-tools.js +243 -0
- package/dist/agents/treasury-bridge.js +127 -0
- package/dist/index.js +42 -0
- package/dist/web/mobile.html +2 -0
- package/dist/web/mobile.js +38 -1
- package/dist/web/server.js +97 -0
- package/package.json +2 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* payment-gate.ts — YAML 驱动的支付验证门 (2026-08-13)
|
|
3
|
+
*
|
|
4
|
+
* 用户要求: 智能体支付不能全部交给 AI, 需要 YAML 验证流程.
|
|
5
|
+
* 设计 (参考 Hermes write_approval 配置驱动 + arXiv:2605.30998):
|
|
6
|
+
* - payment-policy.yaml 声明式规则 (allow/confirm/deny)
|
|
7
|
+
* - 支付请求按规则链逐条匹配 → decision
|
|
8
|
+
* - deny 不可覆盖; confirm 返回 pending 等待人工审批 (不自动执行)
|
|
9
|
+
*
|
|
10
|
+
* 加载: ~/.bolloon/payment-policy.yaml (不存在则用内置默认 payment-policy.yaml)
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as yaml from 'js-yaml';
|
|
16
|
+
const home = () => process.env.HOME || os.homedir() || '/tmp';
|
|
17
|
+
/** 支付验证门: 加载 YAML 并评估 */
|
|
18
|
+
export class PaymentGate {
|
|
19
|
+
policy = null;
|
|
20
|
+
policyPath;
|
|
21
|
+
constructor(policyPath = path.join(home(), '.bolloon', 'payment-policy.yaml')) {
|
|
22
|
+
this.policyPath = policyPath;
|
|
23
|
+
}
|
|
24
|
+
/** 加载 YAML (本地文件优先, 否则内置默认) */
|
|
25
|
+
load() {
|
|
26
|
+
if (this.policy)
|
|
27
|
+
return this.policy;
|
|
28
|
+
const candidates = [
|
|
29
|
+
this.policyPath,
|
|
30
|
+
path.resolve(process.cwd(), 'src/agents/payment-policy.yaml'),
|
|
31
|
+
];
|
|
32
|
+
for (const f of candidates) {
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(f)) {
|
|
35
|
+
this.policy = yaml.load(fs.readFileSync(f, 'utf-8'));
|
|
36
|
+
return this.policy;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch { /* 下一个 */ }
|
|
40
|
+
}
|
|
41
|
+
this.policy = { default_action: 'confirm', default_reason: '未命中任何支付规则, 需要人工确认' };
|
|
42
|
+
return this.policy;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 评估支付请求 (按规则链, 顺序匹配第一条命中).
|
|
46
|
+
* 流程: 硬限制 (超限即 deny) → 规则链 (allow/confirm/deny) → 默认.
|
|
47
|
+
*/
|
|
48
|
+
evaluate(intent) {
|
|
49
|
+
const p = this.load();
|
|
50
|
+
const service = String(intent.service || '').toLowerCase();
|
|
51
|
+
const recipient = String(intent.recipient || '').toLowerCase();
|
|
52
|
+
const amount = Number(intent.amount) || 0;
|
|
53
|
+
const limits = {
|
|
54
|
+
maxPerTransaction: p.limits?.max_per_transaction ?? 1,
|
|
55
|
+
maxDailyTotal: p.limits?.max_daily_total ?? 10,
|
|
56
|
+
};
|
|
57
|
+
// 0. 硬限制: 超单笔/日限 → deny (无论规则)
|
|
58
|
+
if (amount > limits.maxPerTransaction) {
|
|
59
|
+
return { decision: 'deny', reason: `单笔超限: ${amount} > ${limits.maxPerTransaction}`, requiresApproval: false, limits };
|
|
60
|
+
}
|
|
61
|
+
// 1. 规则链 (顺序匹配)
|
|
62
|
+
for (const rule of p.rules ?? []) {
|
|
63
|
+
const matchesService = !rule.services || rule.services.some((s) => s.toLowerCase() === service);
|
|
64
|
+
const matchesRecipient = !rule.recipients || rule.recipients.some((r) => r.toLowerCase() === recipient);
|
|
65
|
+
const matchesAmount = rule.max_amount === undefined || amount <= rule.max_amount;
|
|
66
|
+
if (matchesService && matchesRecipient && matchesAmount) {
|
|
67
|
+
return {
|
|
68
|
+
decision: rule.action,
|
|
69
|
+
reason: rule.reason || rule.description || `规则 ${rule.id} 命中`,
|
|
70
|
+
ruleId: rule.id,
|
|
71
|
+
requiresApproval: rule.action === 'confirm',
|
|
72
|
+
limits,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// 2. 默认
|
|
77
|
+
return {
|
|
78
|
+
decision: p.default_action ?? 'confirm',
|
|
79
|
+
reason: p.default_reason || '默认: 需人工确认',
|
|
80
|
+
requiresApproval: (p.default_action ?? 'confirm') === 'confirm',
|
|
81
|
+
limits,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** 快捷: 是否允许 (allow = true; confirm/deny = false 且不自动) */
|
|
85
|
+
isAllowed(intent) {
|
|
86
|
+
const v = this.evaluate(intent);
|
|
87
|
+
return { allowed: v.decision === 'allow', verdict: v };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let _gate = null;
|
|
91
|
+
/** 单例 */
|
|
92
|
+
export function getPaymentGate() {
|
|
93
|
+
if (!_gate)
|
|
94
|
+
_gate = new PaymentGate();
|
|
95
|
+
return _gate;
|
|
96
|
+
}
|
|
97
|
+
export function resetPaymentGate() {
|
|
98
|
+
_gate = null;
|
|
99
|
+
}
|
|
@@ -2262,6 +2262,249 @@ export function registerBuiltinTools(ctx) {
|
|
|
2262
2262
|
}
|
|
2263
2263
|
catch { /* A2UI 工具注册失败静默 */ }
|
|
2264
2264
|
})();
|
|
2265
|
+
// 2026-08-13 (Phase E1): Agent 服务 Registry — 注册/发现服务 (Agent Economic Network Discovery 层)
|
|
2266
|
+
(async () => {
|
|
2267
|
+
try {
|
|
2268
|
+
const registry = await import('../agents/agent-registry.js');
|
|
2269
|
+
// 注册自己为服务提供者
|
|
2270
|
+
ctx.tools.set('registry_register', {
|
|
2271
|
+
name: 'registry_register',
|
|
2272
|
+
description: '在 Agent 服务注册表注册自己的服务 (定价/能力/钱包). 让其他 Agent 能发现并调用你. 参数: service_name(如 research), description, price_amount, price_currency(USDC), price_per(如 query), capabilities(数组), wallet(收款地址)',
|
|
2273
|
+
parameters: {
|
|
2274
|
+
service_name: '服务名 (必填, 如 research/coding/data)',
|
|
2275
|
+
description: '服务描述 (必填)',
|
|
2276
|
+
price_amount: '单价金额 (必填, 如 0.05)',
|
|
2277
|
+
price_currency: '计价币种 (默认 USDC)',
|
|
2278
|
+
price_per: '计价单位 (默认 query)',
|
|
2279
|
+
capabilities: '能力数组 (可选)',
|
|
2280
|
+
wallet: '收款钱包地址 (必填)',
|
|
2281
|
+
},
|
|
2282
|
+
execute: async (args) => {
|
|
2283
|
+
const serviceName = String(args.service_name || '').trim();
|
|
2284
|
+
const wallet = String(args.wallet || '').trim();
|
|
2285
|
+
if (!serviceName || !wallet)
|
|
2286
|
+
return { success: false, error: 'service_name 和 wallet 必填' };
|
|
2287
|
+
const reg = registry.getAgentRegistry();
|
|
2288
|
+
const r = await reg.register({
|
|
2289
|
+
agentId: ctx.identity?.did || `did:local:${ctx.identity?.name || 'agent'}`,
|
|
2290
|
+
name: ctx.identity?.name || 'agent',
|
|
2291
|
+
wallet,
|
|
2292
|
+
service: {
|
|
2293
|
+
name: serviceName,
|
|
2294
|
+
description: String(args.description || `${serviceName} 服务`).trim(),
|
|
2295
|
+
price: {
|
|
2296
|
+
amount: String(args.price_amount || '0'),
|
|
2297
|
+
currency: String(args.price_currency || 'USDC').toUpperCase(),
|
|
2298
|
+
per: String(args.price_per || 'query'),
|
|
2299
|
+
},
|
|
2300
|
+
},
|
|
2301
|
+
capabilities: Array.isArray(args.capabilities) ? args.capabilities.map(String) : [serviceName],
|
|
2302
|
+
});
|
|
2303
|
+
return r.ok
|
|
2304
|
+
? { success: true, output: `✅ 已注册服务: ${serviceName} (${args.price_amount || 0} ${String(args.price_currency || 'USDC').toUpperCase()}/${args.price_per || 'query'}) wallet=${wallet.slice(0, 10)}...` }
|
|
2305
|
+
: { success: false, error: r.error };
|
|
2306
|
+
},
|
|
2307
|
+
});
|
|
2308
|
+
// 发现可用的 Agent 服务
|
|
2309
|
+
ctx.tools.set('registry_discover', {
|
|
2310
|
+
name: 'registry_discover',
|
|
2311
|
+
description: '在 Agent 服务注册表发现服务 (按名称/能力/描述). 找到服务后可用 x402 支付调用. query: 搜索词 (如 research/compute/data).',
|
|
2312
|
+
parameters: { query: '搜索词 (可选, 空=列出全部)' },
|
|
2313
|
+
execute: async (args) => {
|
|
2314
|
+
const q = String(args.query || '').trim();
|
|
2315
|
+
const reg = registry.getAgentRegistry();
|
|
2316
|
+
const services = await reg.discover(q);
|
|
2317
|
+
if (services.length === 0)
|
|
2318
|
+
return { success: true, output: '(注册表无匹配服务)' };
|
|
2319
|
+
const lines = services.slice(0, 20).map((s) => ` [${s.service.name}] ${s.name} (${s.service.price.amount} ${s.service.price.currency}/${s.service.price.per}) wallet=${String(s.wallet).slice(0, 12)}... agent=${s.agentId.slice(0, 24)}...`);
|
|
2320
|
+
return { success: true, output: `发现 ${services.length} 个服务:\n${lines.join('\n')}` };
|
|
2321
|
+
},
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
catch { /* registry 工具注册失败静默 */ }
|
|
2325
|
+
})();
|
|
2326
|
+
// 2026-08-13 (Phase E2): service_call — 调用 Agent 服务 (x402 402 自动支付闭环)
|
|
2327
|
+
ctx.tools.set('service_call', {
|
|
2328
|
+
name: 'service_call',
|
|
2329
|
+
description: '调用注册表里的 Agent 服务 (x402 支付闭环). 从 registry_discover 找到服务名后调用. service_name: 服务名; args: 服务参数; 有钱包私钥时自动支付 402.',
|
|
2330
|
+
parameters: {
|
|
2331
|
+
service_name: '服务名 (必填, registry_discover 查到的)',
|
|
2332
|
+
args: '服务参数 JSON (可选)',
|
|
2333
|
+
max_payment_amount: '最大支付金额 (可选)',
|
|
2334
|
+
},
|
|
2335
|
+
execute: async (args) => {
|
|
2336
|
+
const serviceName = String(args.service_name || '').trim();
|
|
2337
|
+
if (!serviceName)
|
|
2338
|
+
return { success: false, error: 'service_name 必填 (先用 registry_discover 查)' };
|
|
2339
|
+
try {
|
|
2340
|
+
const { serviceCall } = await import('./agent-service-client.js');
|
|
2341
|
+
// 尝试取 channel 钱包私钥 (autoPay)
|
|
2342
|
+
let privateKey;
|
|
2343
|
+
try {
|
|
2344
|
+
const wallet = await ctx.getChannelWallet?.();
|
|
2345
|
+
if (wallet?.encryptedPrivateKey)
|
|
2346
|
+
privateKey = wallet.encryptedPrivateKey;
|
|
2347
|
+
}
|
|
2348
|
+
catch { /* 无钱包 */ }
|
|
2349
|
+
let argsObj = {};
|
|
2350
|
+
try {
|
|
2351
|
+
argsObj = JSON.parse(String(args.args || '{}'));
|
|
2352
|
+
}
|
|
2353
|
+
catch {
|
|
2354
|
+
argsObj = {};
|
|
2355
|
+
}
|
|
2356
|
+
const r = await serviceCall({
|
|
2357
|
+
serviceName,
|
|
2358
|
+
args: argsObj,
|
|
2359
|
+
privateKey,
|
|
2360
|
+
maxPaymentAmount: String(args.max_payment_amount || '').trim() || undefined,
|
|
2361
|
+
});
|
|
2362
|
+
if (!r.success)
|
|
2363
|
+
return { success: false, error: r.error, service: r.service?.service?.name };
|
|
2364
|
+
return { success: true, output: r.output, paid: r.paid, txHash: r.txHash };
|
|
2365
|
+
}
|
|
2366
|
+
catch (e) {
|
|
2367
|
+
return { success: false, error: `service_call 失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
2368
|
+
}
|
|
2369
|
+
},
|
|
2370
|
+
});
|
|
2371
|
+
// 2026-08-13 (Phase E3): Policy Engine — 预算/授权 (安全核心, 私钥不暴露给 LLM)
|
|
2372
|
+
ctx.tools.set('policy_config', {
|
|
2373
|
+
name: 'policy_config',
|
|
2374
|
+
description: '查看/更新支付策略 (预算/白名单). LLM 只能查看预算与授权规则, 私钥由 Policy Engine 隔离保管. 查看: 无参数; 更新: 传要改的字段 (per_transaction_limit/daily_limit/allowed_recipients/allowed_services).',
|
|
2375
|
+
parameters: {
|
|
2376
|
+
per_transaction_limit: '可选: 单笔上限 (数字)',
|
|
2377
|
+
daily_limit: '可选: 每日预算 (数字)',
|
|
2378
|
+
allowed_recipients: '可选: 允许收款方数组',
|
|
2379
|
+
allowed_services: '可选: 允许服务数组',
|
|
2380
|
+
},
|
|
2381
|
+
execute: async (args) => {
|
|
2382
|
+
try {
|
|
2383
|
+
const { getEconomicPolicy } = await import('./economic-policy.js');
|
|
2384
|
+
const policy = getEconomicPolicy();
|
|
2385
|
+
const patch = {};
|
|
2386
|
+
if (args.per_transaction_limit !== undefined)
|
|
2387
|
+
patch.perTransactionLimit = Number(args.per_transaction_limit);
|
|
2388
|
+
if (args.daily_limit !== undefined)
|
|
2389
|
+
patch.dailyLimit = Number(args.daily_limit);
|
|
2390
|
+
if (Array.isArray(args.allowed_recipients))
|
|
2391
|
+
patch.allowedRecipients = args.allowed_recipients.map(String);
|
|
2392
|
+
if (Array.isArray(args.allowed_services))
|
|
2393
|
+
patch.allowedServices = args.allowed_services.map(String);
|
|
2394
|
+
if (Object.keys(patch).length > 0)
|
|
2395
|
+
policy.updateConfig(patch);
|
|
2396
|
+
const spent = await policy.dailySpent();
|
|
2397
|
+
const c = policy.config();
|
|
2398
|
+
return {
|
|
2399
|
+
success: true,
|
|
2400
|
+
output: `支付策略:\n 单笔上限: $${c.perTransactionLimit}\n 每日预算: $${c.dailyLimit} (今日已用 $${spent})\n 允许收款方: ${c.allowedRecipients.length ? c.allowedRecipients.join(', ') : '(全部)'}\n 允许服务: ${c.allowedServices.length ? c.allowedServices.join(', ') : '(全部)'}\n 速率: ${c.rateLimitPerMinute}/min`,
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
catch (e) {
|
|
2404
|
+
return { success: false, error: `policy_config 失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
2405
|
+
}
|
|
2406
|
+
},
|
|
2407
|
+
});
|
|
2408
|
+
// 2026-08-13 (Phase M4): Reputation — 服务结果记录 + 信誉查询 (Agent Economic Protocol §7)
|
|
2409
|
+
ctx.tools.set('reputation_update', {
|
|
2410
|
+
name: 'reputation_update',
|
|
2411
|
+
description: '记录一次服务结果, 更新服务提供者的信誉 (success/failed/disputed → score). 服务完成后调用. agent_id: 服务提供者 DID; service_name: 服务名; outcome: success|failed|disputed.',
|
|
2412
|
+
parameters: { agent_id: '服务提供者 agent_id (必填)', service_name: '服务名 (必填)', outcome: '结果: success|failed|disputed (必填)' },
|
|
2413
|
+
execute: async (args) => {
|
|
2414
|
+
try {
|
|
2415
|
+
const { recordServiceOutcome } = await import('./agent-reputation.js');
|
|
2416
|
+
const agentId = String(args.agent_id || '').trim();
|
|
2417
|
+
const serviceName = String(args.service_name || '').trim();
|
|
2418
|
+
const outcome = String(args.outcome || '').trim();
|
|
2419
|
+
if (!agentId || !serviceName || !['success', 'failed', 'disputed'].includes(outcome)) {
|
|
2420
|
+
return { success: false, error: 'agent_id/service_name/outcome(success|failed|disputed) 必填' };
|
|
2421
|
+
}
|
|
2422
|
+
const r = await recordServiceOutcome(agentId, serviceName, outcome);
|
|
2423
|
+
if (!r.ok)
|
|
2424
|
+
return { success: false, error: r.error };
|
|
2425
|
+
return { success: true, output: `✅ 已记录 ${outcome}: tasks=${r.reputation?.tasks}, score=${r.reputation?.score}` };
|
|
2426
|
+
}
|
|
2427
|
+
catch (e) {
|
|
2428
|
+
return { success: false, error: `reputation_update 失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
2429
|
+
}
|
|
2430
|
+
},
|
|
2431
|
+
});
|
|
2432
|
+
ctx.tools.set('reputation_query', {
|
|
2433
|
+
name: 'reputation_query',
|
|
2434
|
+
description: '查询 Agent 的信誉 (成功率/任务数). agent_id: 服务提供者 DID; service_name: 可选. 选择服务前先查信誉.',
|
|
2435
|
+
parameters: { agent_id: '服务提供者 agent_id (必填)', service_name: '服务名 (可选)' },
|
|
2436
|
+
execute: async (args) => {
|
|
2437
|
+
try {
|
|
2438
|
+
const { queryReputation, formatReputation } = await import('./agent-reputation.js');
|
|
2439
|
+
const r = await queryReputation(String(args.agent_id || '').trim(), String(args.service_name || '').trim() || undefined);
|
|
2440
|
+
if (!r.ok)
|
|
2441
|
+
return { success: true, output: r.error || '(无信誉记录)' };
|
|
2442
|
+
return { success: true, output: r.entries.map((e) => ` [${e.service}] ${formatReputation(e.reputation)}`).join('\n') };
|
|
2443
|
+
}
|
|
2444
|
+
catch (e) {
|
|
2445
|
+
return { success: false, error: `reputation_query 失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
2446
|
+
}
|
|
2447
|
+
},
|
|
2448
|
+
});
|
|
2449
|
+
// 2026-08-13: Treasury 桥 — 链下结算 (Policy/Registry) 驱动链上 Treasury.payAgent
|
|
2450
|
+
// 配置: BOLLOON_TREASURY_RPC / BOLLOON_TREASURY_ADDRESS / BOLLOON_TREASURY_KEY / BOLLOON_TREASURY_TOKEN
|
|
2451
|
+
const treasuryConfigFromEnv = () => {
|
|
2452
|
+
const rpcUrl = process.env.BOLLOON_TREASURY_RPC || '';
|
|
2453
|
+
const treasuryAddress = process.env.BOLLOON_TREASURY_ADDRESS || '';
|
|
2454
|
+
const tokenAddress = process.env.BOLLOON_TREASURY_TOKEN || '';
|
|
2455
|
+
const privateKey = process.env.BOLLOON_TREASURY_KEY || '';
|
|
2456
|
+
return { rpcUrl, treasuryAddress, tokenAddress, privateKey };
|
|
2457
|
+
};
|
|
2458
|
+
ctx.tools.set('treasury_pay', {
|
|
2459
|
+
name: 'treasury_pay',
|
|
2460
|
+
description: '从 Treasury 支付给 Agent (链上 AgentTreasury.payAgent). 自动过 Policy 预算校验 + 信誉门槛 (链上). agent_address: 收款 Agent 地址; amount: 金额 (USDC). 需配置 BOLLOON_TREASURY_* 环境变量 (RPC/合约/私钥).',
|
|
2461
|
+
parameters: { agent_address: '收款 Agent 钱包地址 (必填)', amount: '金额 USDC (必填)', service: '服务名 (可选, Policy 校验用)' },
|
|
2462
|
+
execute: async (args) => {
|
|
2463
|
+
const cfg = treasuryConfigFromEnv();
|
|
2464
|
+
if (!cfg.rpcUrl || !cfg.treasuryAddress || !cfg.privateKey) {
|
|
2465
|
+
return { success: false, error: '未配置 Treasury (BOLLOON_TREASURY_RPC/ADDRESS/KEY/TOKEN)' };
|
|
2466
|
+
}
|
|
2467
|
+
const agentAddress = String(args.agent_address || '').trim();
|
|
2468
|
+
const amount = Number(args.amount);
|
|
2469
|
+
if (!agentAddress || !amount || amount <= 0)
|
|
2470
|
+
return { success: false, error: 'agent_address 和 amount(>0) 必填' };
|
|
2471
|
+
try {
|
|
2472
|
+
const { treasuryPay } = await import('./treasury-bridge.js');
|
|
2473
|
+
const r = await treasuryPay(cfg, {
|
|
2474
|
+
agentAddress,
|
|
2475
|
+
amount,
|
|
2476
|
+
service: String(args.service || '').trim() || undefined,
|
|
2477
|
+
});
|
|
2478
|
+
if (!r.success)
|
|
2479
|
+
return { success: false, error: r.error };
|
|
2480
|
+
return { success: true, output: `✅ Treasury 已支付 ${amount} USDC → ${agentAddress.slice(0, 10)}... tx=${r.txHash}` };
|
|
2481
|
+
}
|
|
2482
|
+
catch (e) {
|
|
2483
|
+
return { success: false, error: `treasury_pay 失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
2484
|
+
}
|
|
2485
|
+
},
|
|
2486
|
+
});
|
|
2487
|
+
ctx.tools.set('treasury_status', {
|
|
2488
|
+
name: 'treasury_status',
|
|
2489
|
+
description: '查询 Treasury 状态 (余额/日限/冻结). 需配置 BOLLOON_TREASURY_* 环境变量.',
|
|
2490
|
+
parameters: {},
|
|
2491
|
+
execute: async () => {
|
|
2492
|
+
const cfg = treasuryConfigFromEnv();
|
|
2493
|
+
if (!cfg.rpcUrl || !cfg.treasuryAddress) {
|
|
2494
|
+
return { success: false, error: '未配置 Treasury (BOLLOON_TREASURY_RPC/ADDRESS)' };
|
|
2495
|
+
}
|
|
2496
|
+
try {
|
|
2497
|
+
const { treasuryStatus } = await import('./treasury-bridge.js');
|
|
2498
|
+
const s = await treasuryStatus(cfg);
|
|
2499
|
+
if (!s.ok)
|
|
2500
|
+
return { success: false, error: s.error };
|
|
2501
|
+
return { success: true, output: `Treasury:\n 余额: ${s.balance} USDC\n 日限: ${s.dailyLimit} USDC\n 冻结: ${s.frozen ? '是' : '否'}` };
|
|
2502
|
+
}
|
|
2503
|
+
catch (e) {
|
|
2504
|
+
return { success: false, error: `treasury_status 失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
2505
|
+
}
|
|
2506
|
+
},
|
|
2507
|
+
});
|
|
2265
2508
|
// ============================================================
|
|
2266
2509
|
// publish_did (2026-08-03) — 把当前 agent 的 DID 发布到 IPFS + IPNS
|
|
2267
2510
|
// 全自动: 自动安装/启动本地 Kubo → 上传 DID 文档 → 发布 IPNS name
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* treasury-bridge.ts — Treasury 合约 × Agent 经济网络桥接 (2026-08-13)
|
|
3
|
+
*
|
|
4
|
+
* 打通: 链下结算逻辑 (Registry 服务/Policy 授权/Reputation 门槛) → 链上 Treasury.payAgent.
|
|
5
|
+
*
|
|
6
|
+
* 流程:
|
|
7
|
+
* registry 服务完成 → reputation_update(success) → treasuryPay(agent, amount)
|
|
8
|
+
* ├─ 链下校验: Policy (预算) + Registry (服务价格) + Reputation (≥60)
|
|
9
|
+
* └─ 链上执行: AgentTreasury.payAgent(agent, amount)
|
|
10
|
+
*
|
|
11
|
+
* 合约 ABI 用最小接口 (viem writeContract), 支持任意部署地址 + RPC.
|
|
12
|
+
* 真实链上需部署 AgentTreasury + 注入 owner 私钥; 测试注入 mock.
|
|
13
|
+
*/
|
|
14
|
+
/** Treasury 合约最小 ABI (payAgent/deposit/balance/dailySpend/frozen) */
|
|
15
|
+
const TREASURY_ABI = [
|
|
16
|
+
{
|
|
17
|
+
name: 'payAgent',
|
|
18
|
+
type: 'function',
|
|
19
|
+
stateMutability: 'nonpayable',
|
|
20
|
+
inputs: [
|
|
21
|
+
{ name: 'agent', type: 'address' },
|
|
22
|
+
{ name: 'amount', type: 'uint256' },
|
|
23
|
+
],
|
|
24
|
+
outputs: [],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'deposit',
|
|
28
|
+
type: 'function',
|
|
29
|
+
stateMutability: 'nonpayable',
|
|
30
|
+
inputs: [{ name: 'amount', type: 'uint256' }],
|
|
31
|
+
outputs: [],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'balance',
|
|
35
|
+
type: 'function',
|
|
36
|
+
stateMutability: 'view',
|
|
37
|
+
inputs: [],
|
|
38
|
+
outputs: [{ name: '', type: 'uint256' }],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'frozen',
|
|
42
|
+
type: 'function',
|
|
43
|
+
stateMutability: 'view',
|
|
44
|
+
inputs: [],
|
|
45
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'dailyLimit',
|
|
49
|
+
type: 'function',
|
|
50
|
+
stateMutability: 'view',
|
|
51
|
+
inputs: [],
|
|
52
|
+
outputs: [{ name: '', type: 'uint256' }],
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* 链下校验 → 链上 Treasury.payAgent.
|
|
57
|
+
* 校验链: Policy (预算/白名单) → 合约调用.
|
|
58
|
+
*/
|
|
59
|
+
export async function treasuryPay(config, opts) {
|
|
60
|
+
const { agentAddress, amount, service } = opts;
|
|
61
|
+
// 1. Policy 授权 (预算/白名单) — 链下, 防无预算支付
|
|
62
|
+
if (opts.policyCheck) {
|
|
63
|
+
const decision = await opts.policyCheck({ payTo: agentAddress, amount, service: service || 'treasury-pay' });
|
|
64
|
+
if (!decision.allowed) {
|
|
65
|
+
return { success: false, error: `[policy] ${decision.reason}`, checks: { policy: decision } };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
try {
|
|
70
|
+
const { getEconomicPolicy } = await import('./economic-policy.js');
|
|
71
|
+
const policy = getEconomicPolicy();
|
|
72
|
+
const decision = await policy.check({ payTo: agentAddress, amount, service: service || 'treasury-pay' });
|
|
73
|
+
if (!decision.allowed) {
|
|
74
|
+
return { success: false, error: `[policy] ${decision.reason}`, checks: { policy: decision } };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch { /* policy 不可用放行 */ }
|
|
78
|
+
}
|
|
79
|
+
// 2. 链上 payAgent (dryRun: 只验证 policy 门, 不真实上链)
|
|
80
|
+
if (opts.dryRun) {
|
|
81
|
+
return { success: true, txHash: 'dry-run:0x0', checks: { policy: { allowed: true } }, dryRun: true };
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const { createWalletClient, http, parseUnits } = await import('viem');
|
|
85
|
+
const { privateKeyToAccount } = await import('viem/accounts');
|
|
86
|
+
const { base, baseSepolia } = await import('viem/chains');
|
|
87
|
+
const decimals = config.decimals ?? 6;
|
|
88
|
+
const account = privateKeyToAccount(config.privateKey);
|
|
89
|
+
const chain = config.rpcUrl.includes('sepolia') ? baseSepolia : base;
|
|
90
|
+
const client = createWalletClient({ account, chain, transport: http(config.rpcUrl) });
|
|
91
|
+
const txHash = await client.writeContract({
|
|
92
|
+
address: config.treasuryAddress,
|
|
93
|
+
abi: TREASURY_ABI,
|
|
94
|
+
functionName: 'payAgent',
|
|
95
|
+
args: [agentAddress, parseUnits(String(amount), decimals)],
|
|
96
|
+
});
|
|
97
|
+
// 记录花费 (链下日预算镜像)
|
|
98
|
+
try {
|
|
99
|
+
const { getEconomicPolicy } = await import('./economic-policy.js');
|
|
100
|
+
await getEconomicPolicy().recordSpend(amount).catch(() => { });
|
|
101
|
+
}
|
|
102
|
+
catch { /* 静默 */ }
|
|
103
|
+
return { success: true, txHash, checks: { policy: { allowed: true } } };
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
return { success: false, error: `Treasury.payAgent 失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** 查询 Treasury 状态 (余额/日限/冻结) */
|
|
110
|
+
export async function treasuryStatus(config) {
|
|
111
|
+
try {
|
|
112
|
+
const { createPublicClient, http, formatUnits } = await import('viem');
|
|
113
|
+
const { base, baseSepolia } = await import('viem/chains');
|
|
114
|
+
const chain = config.rpcUrl.includes('sepolia') ? baseSepolia : base;
|
|
115
|
+
const client = createPublicClient({ chain, transport: http(config.rpcUrl) });
|
|
116
|
+
const decimals = config.decimals ?? 6;
|
|
117
|
+
const [bal, limit, frozen] = await Promise.all([
|
|
118
|
+
client.readContract({ address: config.treasuryAddress, abi: TREASURY_ABI, functionName: 'balance' }),
|
|
119
|
+
client.readContract({ address: config.treasuryAddress, abi: TREASURY_ABI, functionName: 'dailyLimit' }),
|
|
120
|
+
client.readContract({ address: config.treasuryAddress, abi: TREASURY_ABI, functionName: 'frozen' }),
|
|
121
|
+
]);
|
|
122
|
+
return { ok: true, balance: formatUnits(bal, decimals), dailyLimit: formatUnits(limit, decimals), frozen: frozen };
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
return { ok: false, error: `Treasury 状态查询失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1333,6 +1333,48 @@ async function processInput(input, comm) {
|
|
|
1333
1333
|
}
|
|
1334
1334
|
return;
|
|
1335
1335
|
}
|
|
1336
|
+
// /payments [/approve <id> /reject <id>] — 人工支付审批 (2026-08-13)
|
|
1337
|
+
// YAML 验证门判定 confirm 的支付请求 → 这里人工批准/拒绝
|
|
1338
|
+
if (cmd === '/payments' || cmd.startsWith('/payments ') || cmd.startsWith('/approve ') || cmd.startsWith('/reject ')) {
|
|
1339
|
+
try {
|
|
1340
|
+
const { getApprovalStore } = await import('./agents/payment-approval.js');
|
|
1341
|
+
const store = getApprovalStore();
|
|
1342
|
+
if (cmd.startsWith('/approve ')) {
|
|
1343
|
+
const id = cmd.slice('/approve '.length).trim();
|
|
1344
|
+
const r = await store.approve(id);
|
|
1345
|
+
if (!r.ok) {
|
|
1346
|
+
appendLine(`${C_ERROR}${r.error}${RESET}`);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
appendLine(`${C_OK}✓ 已批准 ${id} → ${r.approval?.status}${r.approval?.result ? ` (${String(r.approval.result).slice(0, 80)})` : ''}${RESET}`);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (cmd.startsWith('/reject ')) {
|
|
1353
|
+
const id = cmd.slice('/reject '.length).trim();
|
|
1354
|
+
const r = await store.reject(id);
|
|
1355
|
+
if (!r.ok) {
|
|
1356
|
+
appendLine(`${C_ERROR}${r.error}${RESET}`);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
appendLine(`${C_WARN}✗ 已拒绝 ${id}${RESET}`);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
const approvals = await store.pending();
|
|
1363
|
+
appendLine(`${C_ACCENT}待人工审批支付 (${approvals.length}):${RESET}`);
|
|
1364
|
+
if (approvals.length === 0) {
|
|
1365
|
+
appendLine(` ${C_DIM}无待审批 — YAML 验证门 allow/deny 已自动处理${RESET}`);
|
|
1366
|
+
}
|
|
1367
|
+
for (const a of approvals) {
|
|
1368
|
+
appendLine(` ${C_WARN}⏳${RESET} ${C_ACCENT}${a.id}${RESET} ${a.service} $${a.amount} → ${String(a.recipient).slice(0, 12)}...`);
|
|
1369
|
+
appendLine(` ${C_DIM}${a.reason}${RESET}`);
|
|
1370
|
+
appendLine(` ${C_DIM}/approve ${a.id} | /reject ${a.id}${RESET}`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
catch (e) {
|
|
1374
|
+
appendLine(`${C_ERROR}/payments 失败: ${String(e?.message || e).slice(0, 120)}${RESET}`);
|
|
1375
|
+
}
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1336
1378
|
// /skill — 技能候选 (skill-writer 落盘)
|
|
1337
1379
|
if (cmd === '/skill') {
|
|
1338
1380
|
try {
|
package/dist/web/mobile.html
CHANGED
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
</div>
|
|
37
37
|
<div class="section-label">MCP 工具 (触控调用)</div>
|
|
38
38
|
<div class="list" id="mcp-tools"></div>
|
|
39
|
+
<div class="section-label">待人工审批支付</div>
|
|
40
|
+
<div class="list" id="approval-list"></div>
|
|
39
41
|
<div class="section-label">A2UI 动态面板 (智能体生成)</div>
|
|
40
42
|
<div id="a2ui-root"></div>
|
|
41
43
|
</section>
|
package/dist/web/mobile.js
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
$$('.page').forEach((p) => { p.hidden = p.dataset.tab !== tab; });
|
|
45
45
|
$$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tab));
|
|
46
46
|
$('#topbar-title').textContent = TITLES[tab] || '微信';
|
|
47
|
-
if (tab === 'discover') loadMcpTools();
|
|
47
|
+
if (tab === 'discover') { loadMcpTools(); loadApprovals(); }
|
|
48
48
|
window.__mobileTouch?.('tab', tab);
|
|
49
49
|
}
|
|
50
50
|
$$('.tab').forEach((t) => t.addEventListener('click', () => switchTab(t.dataset.tab)));
|
|
@@ -307,6 +307,43 @@
|
|
|
307
307
|
// 预留: 触控事件上报/控制
|
|
308
308
|
};
|
|
309
309
|
|
|
310
|
+
// === 人工支付审批 (2026-08-13): YAML 验证门 confirm 的支付请求 → 人工批准/拒绝 ===
|
|
311
|
+
async function loadApprovals() {
|
|
312
|
+
const box = $('#approval-list');
|
|
313
|
+
if (!box) return;
|
|
314
|
+
try {
|
|
315
|
+
const r = await api.get('/api/payments/pending');
|
|
316
|
+
const approvals = r.approvals || [];
|
|
317
|
+
box.innerHTML = '';
|
|
318
|
+
if (approvals.length === 0) {
|
|
319
|
+
box.innerHTML = '<div style="padding:10px 16px;color:var(--text-muted)">无待审批支付</div>';
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
approvals.forEach((a) => {
|
|
323
|
+
const el = document.createElement('div');
|
|
324
|
+
el.className = 'conv-item';
|
|
325
|
+
el.style.flexDirection = 'column';
|
|
326
|
+
el.style.alignItems = 'flex-start';
|
|
327
|
+
el.innerHTML = `
|
|
328
|
+
<div style="width:100%"><b>${escapeHtml(a.service)}</b> $${a.amount} → ${escapeHtml(String(a.recipient).slice(0, 14))}...</div>
|
|
329
|
+
<div style="color:var(--text-muted);font-size:12px">${escapeHtml(a.reason)}</div>
|
|
330
|
+
<div style="display:flex;gap:8px;margin-top:6px">
|
|
331
|
+
<button class="approve-btn" data-id="${escapeHtml(a.id)}" style="background:var(--success,#22c55e);border:none;border-radius:6px;padding:4px 14px;color:#fff">批准</button>
|
|
332
|
+
<button class="reject-btn" data-id="${escapeHtml(a.id)}" style="background:var(--error,#ef4444);border:none;border-radius:6px;padding:4px 14px;color:#fff">拒绝</button>
|
|
333
|
+
</div>`;
|
|
334
|
+
el.querySelector('.approve-btn').addEventListener('click', async () => {
|
|
335
|
+
await api.post(`/api/payments/${a.id}/approve`, {}).catch(() => {});
|
|
336
|
+
loadApprovals();
|
|
337
|
+
});
|
|
338
|
+
el.querySelector('.reject-btn').addEventListener('click', async () => {
|
|
339
|
+
await api.post(`/api/payments/${a.id}/reject`, {}).catch(() => {});
|
|
340
|
+
loadApprovals();
|
|
341
|
+
});
|
|
342
|
+
box.appendChild(el);
|
|
343
|
+
});
|
|
344
|
+
} catch (e) { box.innerHTML = '<div style="padding:10px;color:var(--error)">审批加载失败</div>'; }
|
|
345
|
+
}
|
|
346
|
+
|
|
310
347
|
// === MCP 驱动前端 UI (2026-08-12): 订阅 /events, 收到 {type:'ui'} 指令执行组件动作 ===
|
|
311
348
|
function setupUiControl() {
|
|
312
349
|
try {
|