@bolloon/bolloon-agent 0.4.12 → 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.
@@ -30,12 +30,38 @@ export async function serviceCall(opts) {
30
30
  // 3. 调服务 (x402 自动支付: 402 → 签名 → 重试)
31
31
  try {
32
32
  const { x402Fetch } = await import('./x402/x402Pay.js');
33
+ // 2026-08-13: YAML 验证门 (不全部交给 AI) — 支付前先过 payment-policy.yaml 规则链
34
+ const { getPaymentGate } = await import('./payment-gate.js');
35
+ const gate = getPaymentGate();
36
+ const amount = parseFloat(service.service?.price?.amount || '0');
37
+ const gateVerdict = gate.evaluate({ service: service.service?.name, amount, recipient: service.wallet });
38
+ if (gateVerdict.decision === 'deny') {
39
+ return { success: false, error: `[payment-gate] ${gateVerdict.reason}`, service };
40
+ }
41
+ if (gateVerdict.decision === 'confirm') {
42
+ // 2026-08-13: 人工审批 — 创建 pending 审批请求 (不自动执行), UI/CLI 批准后重试
43
+ const { getApprovalStore } = await import('./payment-approval.js');
44
+ const store = getApprovalStore();
45
+ const approval = await store.create({
46
+ service: service.service?.name || serviceName,
47
+ amount,
48
+ recipient: service.wallet,
49
+ reason: gateVerdict.reason,
50
+ retryPayload: { serviceName, args, privateKey, maxPaymentAmount: maxPaymentAmount || service.service?.price?.amount },
51
+ });
52
+ return {
53
+ success: false,
54
+ error: `[payment-gate] 需人工确认: ${gateVerdict.reason} (approval=${approval.id}, 批准后自动执行)`,
55
+ service,
56
+ requiresApproval: true,
57
+ approvalId: approval.id,
58
+ };
59
+ }
33
60
  // Phase E3: Policy Engine 授权 (预算/白名单) — 通过才允许自动支付
34
61
  let effectiveKey = privateKey;
35
62
  if (privateKey) {
36
63
  const { getEconomicPolicy } = await import('./economic-policy.js');
37
64
  const policy = getEconomicPolicy();
38
- const amount = parseFloat(service.service?.price?.amount || '0');
39
65
  const decision = await policy.check({
40
66
  payTo: service.wallet,
41
67
  amount,
@@ -0,0 +1,142 @@
1
+ /**
2
+ * payment-approval.ts — 人工支付审批 (2026-08-13)
3
+ *
4
+ * YAML 支付验证门 (payment-gate) 判定 confirm 的支付请求 → 进入人工审批流程:
5
+ * - createApproval: 创建 pending 审批请求 (持久化)
6
+ * - approve(id): 人工批准 → 自动重试支付 (executor)
7
+ * - reject(id): 人工拒绝
8
+ * - list/pending: 查询
9
+ *
10
+ * 持久化: ~/.bolloon/payment-approvals.json
11
+ * 审批后执行: 注入 executor (serviceCall 重试 / 链上支付), 由调用方提供.
12
+ */
13
+ import * as fs from 'fs/promises';
14
+ import * as os from 'os';
15
+ import * as path from 'path';
16
+ const home = () => process.env.HOME || os.homedir() || '/tmp';
17
+ let _executor = null;
18
+ /** 注入批准后执行器 (serviceCall 重试 / 链上支付) */
19
+ export function setApprovalExecutor(fn) {
20
+ _executor = fn;
21
+ }
22
+ export class PaymentApprovalStore {
23
+ file;
24
+ approvals = [];
25
+ constructor(file = path.join(home(), '.bolloon', 'payment-approvals.json')) {
26
+ this.file = file;
27
+ }
28
+ async load() {
29
+ try {
30
+ const raw = JSON.parse(await fs.readFile(this.file, 'utf-8'));
31
+ if (Array.isArray(raw))
32
+ this.approvals = raw;
33
+ }
34
+ catch {
35
+ this.approvals = [];
36
+ }
37
+ }
38
+ async persist() {
39
+ try {
40
+ await fs.mkdir(path.dirname(this.file), { recursive: true });
41
+ await fs.writeFile(this.file, JSON.stringify(this.approvals, null, 2), 'utf-8');
42
+ }
43
+ catch { /* 静默 */ }
44
+ }
45
+ /** 创建审批请求 (pending) */
46
+ async create(req) {
47
+ await this.load();
48
+ const approval = {
49
+ id: `pay-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
50
+ service: req.service,
51
+ amount: req.amount,
52
+ recipient: req.recipient,
53
+ reason: req.reason,
54
+ status: 'pending',
55
+ createdAt: Date.now(),
56
+ retryPayload: req.retryPayload,
57
+ };
58
+ this.approvals.push(approval);
59
+ await this.persist();
60
+ return approval;
61
+ }
62
+ /** 待审批列表 */
63
+ async pending() {
64
+ await this.load();
65
+ return this.approvals.filter((a) => a.status === 'pending');
66
+ }
67
+ /** 全部 (含历史) */
68
+ async list() {
69
+ await this.load();
70
+ return [...this.approvals].reverse();
71
+ }
72
+ async get(id) {
73
+ await this.load();
74
+ return this.approvals.find((a) => a.id === id) ?? null;
75
+ }
76
+ /**
77
+ * 人工批准 → 执行支付 (executor) → executed/failed.
78
+ * 未注入 executor 时直接标记 approved (调用方自行处理).
79
+ */
80
+ async approve(id, approver = 'user') {
81
+ await this.load();
82
+ const a = this.approvals.find((x) => x.id === id);
83
+ if (!a)
84
+ return { ok: false, error: `审批 ${id} 不存在` };
85
+ if (a.status !== 'pending')
86
+ return { ok: false, error: `审批 ${id} 状态 ${a.status}, 不可批准` };
87
+ a.status = 'approved';
88
+ a.decidedAt = Date.now();
89
+ await this.persist();
90
+ // 批准后执行支付
91
+ if (_executor) {
92
+ const r = await _executor(a);
93
+ a.status = r.ok ? 'executed' : 'failed';
94
+ a.result = r.ok ? r.result : r.error;
95
+ await this.persist();
96
+ if (!r.ok)
97
+ return { ok: false, approval: a, error: r.error };
98
+ }
99
+ return { ok: true, approval: a };
100
+ }
101
+ /** 人工拒绝 */
102
+ async reject(id, approver = 'user') {
103
+ await this.load();
104
+ const a = this.approvals.find((x) => x.id === id);
105
+ if (!a)
106
+ return { ok: false, error: `审批 ${id} 不存在` };
107
+ if (a.status !== 'pending')
108
+ return { ok: false, error: `审批 ${id} 状态 ${a.status}, 不可拒绝` };
109
+ a.status = 'rejected';
110
+ a.decidedAt = Date.now();
111
+ await this.persist();
112
+ return { ok: true, approval: a };
113
+ }
114
+ /** 清理过期 pending (超时自动标记 rejected) */
115
+ async expireStale(timeoutMs = 60 * 60 * 1000) {
116
+ await this.load();
117
+ const now = Date.now();
118
+ let expired = 0;
119
+ for (const a of this.approvals) {
120
+ if (a.status === 'pending' && now - a.createdAt > timeoutMs) {
121
+ a.status = 'rejected';
122
+ a.decidedAt = now;
123
+ a.result = '审批超时自动拒绝';
124
+ expired++;
125
+ }
126
+ }
127
+ if (expired > 0)
128
+ await this.persist();
129
+ return expired;
130
+ }
131
+ }
132
+ let _store = null;
133
+ /** 单例 */
134
+ export function getApprovalStore() {
135
+ if (!_store)
136
+ _store = new PaymentApprovalStore();
137
+ return _store;
138
+ }
139
+ export function resetApprovalStore() {
140
+ _store = null;
141
+ _executor = null;
142
+ }
@@ -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
+ }
@@ -2446,6 +2446,65 @@ export function registerBuiltinTools(ctx) {
2446
2446
  }
2447
2447
  },
2448
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
+ });
2449
2508
  // ============================================================
2450
2509
  // publish_did (2026-08-03) — 把当前 agent 的 DID 发布到 IPFS + IPNS
2451
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 {
@@ -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>
@@ -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 {
@@ -2811,6 +2811,66 @@ ${goalDesc}
2811
2811
  res.status(500).json({ error: e?.message });
2812
2812
  }
2813
2813
  });
2814
+ // 2026-08-13: 人工支付审批 API — YAML 验证门 confirm 的支付请求
2815
+ app.get('/api/payments/pending', async (_req, res) => {
2816
+ try {
2817
+ const { getApprovalStore } = await import('../agents/payment-approval.js');
2818
+ const approvals = await getApprovalStore().pending();
2819
+ res.json({ approvals });
2820
+ }
2821
+ catch (e) {
2822
+ res.status(500).json({ error: e?.message });
2823
+ }
2824
+ });
2825
+ app.get('/api/payments', async (_req, res) => {
2826
+ try {
2827
+ const { getApprovalStore } = await import('../agents/payment-approval.js');
2828
+ const approvals = await getApprovalStore().list();
2829
+ res.json({ approvals });
2830
+ }
2831
+ catch (e) {
2832
+ res.status(500).json({ error: e?.message });
2833
+ }
2834
+ });
2835
+ app.post('/api/payments/:id/approve', async (req, res) => {
2836
+ try {
2837
+ const { getApprovalStore, setApprovalExecutor } = await import('../agents/payment-approval.js');
2838
+ const store = getApprovalStore();
2839
+ // 批准后执行: 重试 serviceCall (带 retryPayload)
2840
+ setApprovalExecutor(async (approval) => {
2841
+ const payload = approval.retryPayload;
2842
+ if (!payload)
2843
+ return { ok: false, error: '无重试载荷' };
2844
+ const { serviceCall } = await import('../agents/agent-service-client.js');
2845
+ const r = await serviceCall({
2846
+ serviceName: payload.serviceName,
2847
+ args: payload.args,
2848
+ privateKey: payload.privateKey,
2849
+ maxPaymentAmount: payload.maxPaymentAmount,
2850
+ });
2851
+ return { ok: r.success, result: r.output, error: r.error };
2852
+ });
2853
+ const r = await store.approve(String(req.params.id));
2854
+ if (!r.ok)
2855
+ return res.status(400).json({ error: r.error });
2856
+ res.json({ ok: true, approval: r.approval });
2857
+ }
2858
+ catch (e) {
2859
+ res.status(500).json({ error: e?.message });
2860
+ }
2861
+ });
2862
+ app.post('/api/payments/:id/reject', async (req, res) => {
2863
+ try {
2864
+ const { getApprovalStore } = await import('../agents/payment-approval.js');
2865
+ const r = await getApprovalStore().reject(String(req.params.id));
2866
+ if (!r.ok)
2867
+ return res.status(400).json({ error: r.error });
2868
+ res.json({ ok: true, approval: r.approval });
2869
+ }
2870
+ catch (e) {
2871
+ res.status(500).json({ error: e?.message });
2872
+ }
2873
+ });
2814
2874
  // 2026-08-12: MCP 工具列表 (MCP 前端支持 — 手机端/桌面 UI 展示可用 MCP 工具)
2815
2875
  app.get('/api/mcp/tools', async (_req, res) => {
2816
2876
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.4.12",
3
+ "version": "0.4.13",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",
@@ -101,6 +101,7 @@
101
101
  "helia": "^7.1.3",
102
102
  "ink": "^7.1.1",
103
103
  "ink-text-input": "^6.0.0",
104
+ "js-yaml": "^5.2.3",
104
105
  "libp2p": "^3.3.8",
105
106
  "mammoth": "^1.12.1",
106
107
  "pdf-parse": "^2.4.5",