@bolloon/bolloon-agent 0.4.30 → 0.4.32
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/agent-gateway.js +5 -0
- package/dist/agents/agent-manifest-protocol.js +29 -0
- package/dist/agents/chain/chain-config.js +504 -0
- package/dist/agents/chain/chain-index-query.js +135 -0
- package/dist/agents/chain/chain-indexer.js +1045 -0
- package/dist/agents/chain/chain-settlement.js +284 -0
- package/dist/agents/chain/chain-state-store.js +245 -0
- package/dist/agents/chain/chain-wallet.js +198 -0
- package/dist/agents/chain/escrow-client.js +433 -0
- package/dist/agents/chain/index.js +20 -0
- package/dist/agents/chain/onchain-trade.js +691 -0
- package/dist/agents/economic-policy.js +7 -0
- package/dist/agents/gateway-network.js +6 -0
- package/dist/agents/local-signer.js +59 -0
- package/dist/agents/network-pulse.js +912 -0
- package/dist/agents/task/task-onchain-runner.js +284 -0
- package/dist/agents/task-contract.js +322 -0
- package/dist/agents/task-inbox.js +267 -0
- package/dist/agents/task-transport.js +280 -0
- package/dist/agents/x402/paid-info-store.js +75 -6
- package/dist/agents/x402/transaction-store.js +8 -1
- package/dist/cli/commands/agent.js +230 -0
- package/dist/cli/commands/chain.js +926 -0
- package/dist/cli/commands/index.js +69 -0
- package/dist/cli/commands/mcp.js +98 -0
- package/dist/cli/commands/network.js +334 -0
- package/dist/cli/commands/payment.js +125 -0
- package/dist/cli/commands/tasks.js +1236 -0
- package/dist/cli/commands/trade.js +229 -0
- package/dist/cli/commands/wallet.js +450 -0
- package/dist/cli/mcp/bridge.js +105 -0
- package/dist/cli/mcp/server.js +188 -0
- package/dist/cli/mcp/tools.js +955 -0
- package/dist/cli/protocol-envelope.js +358 -0
- package/dist/cli-entry.js +146 -17
- package/dist/electron-build/agents/agent-gateway.js +5 -0
- package/dist/electron-build/agents/agent-gateway.js.map +1 -1
- package/dist/electron-build/agents/agent-manifest-protocol.js +62 -0
- package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
- package/dist/electron-build/agents/chain/chain-config.js +556 -0
- package/dist/electron-build/agents/chain/chain-config.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-index-query.js +145 -0
- package/dist/electron-build/agents/chain/chain-index-query.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-indexer.js +1095 -0
- package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-settlement.js +291 -0
- package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -0
- package/dist/electron-build/agents/chain/escrow-client.js +443 -0
- package/dist/electron-build/agents/chain/escrow-client.js.map +1 -0
- package/dist/electron-build/agents/economic-policy.js +7 -0
- package/dist/electron-build/agents/economic-policy.js.map +1 -1
- package/dist/electron-build/agents/gateway-network.js +6 -0
- package/dist/electron-build/agents/gateway-network.js.map +1 -1
- package/dist/electron-build/agents/network-pulse.js +979 -0
- package/dist/electron-build/agents/network-pulse.js.map +1 -0
- package/dist/electron-build/agents/x402/paid-info-store.js +76 -6
- package/dist/electron-build/agents/x402/paid-info-store.js.map +1 -1
- package/dist/electron-build/agents/x402/transaction-store.js +8 -1
- package/dist/electron-build/agents/x402/transaction-store.js.map +1 -1
- package/dist/web/chain-explorer.js +617 -0
- package/dist/web/explorer.html +163 -0
- package/dist/web/index.html +5 -0
- package/dist/web/routes-chain-index.js +108 -0
- package/dist/web/routes-onchain-trade.js +99 -0
- package/dist/web/server.js +42 -0
- package/dist/web/task-frame-server.js +211 -0
- package/package.json +2 -1
- package/scripts/build-web.ts +20 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.ts — P3 命令组调度 (network / agent / task / wallet / payment / trade)
|
|
3
|
+
*
|
|
4
|
+
* 每个命令组只做两件事: 解析全局选项 (`--json`/`--quiet`/`--request-id`/`--timeout`),
|
|
5
|
+
* 然后把活交给对应模块里的**薄包装**。业务逻辑一律不在这里, 也不在命令模块里重实现。
|
|
6
|
+
*/
|
|
7
|
+
import { parseFlags, runCommand } from '../protocol-envelope.js';
|
|
8
|
+
import { networkCommand } from './network.js';
|
|
9
|
+
import { agentCommand } from './agent.js';
|
|
10
|
+
import { taskCommand } from './tasks.js';
|
|
11
|
+
import { walletCommand } from './wallet.js';
|
|
12
|
+
import { paymentCommand } from './payment.js';
|
|
13
|
+
import { tradeCommand } from './trade.js';
|
|
14
|
+
import { chainCommand } from './chain.js';
|
|
15
|
+
/** P3 命令组名 + P6 的 `chain` (cli-entry 的 parseArgs 也认这些) */
|
|
16
|
+
export const SERVICE_GROUPS = ['network', 'agent', 'task', 'wallet', 'payment', 'trade', 'chain'];
|
|
17
|
+
export function isServiceGroup(mode) {
|
|
18
|
+
return SERVICE_GROUPS.includes(mode);
|
|
19
|
+
}
|
|
20
|
+
/** 模式 → 命令函数 (唯一映射表: CLI 与 MCP 适配层都**只**从这里取, 不许各自 if-else) */
|
|
21
|
+
export const GROUP_COMMANDS = {
|
|
22
|
+
network: networkCommand,
|
|
23
|
+
agent: agentCommand,
|
|
24
|
+
task: taskCommand,
|
|
25
|
+
wallet: walletCommand,
|
|
26
|
+
payment: paymentCommand,
|
|
27
|
+
trade: tradeCommand,
|
|
28
|
+
chain: chainCommand,
|
|
29
|
+
};
|
|
30
|
+
export const GROUPS_HELP = `
|
|
31
|
+
命令组 (P3, 统一信封 { ok, code, message, data, evidence, next_action }):
|
|
32
|
+
|
|
33
|
+
bolloon network status | init | join [link] | peers | leave(未实现)
|
|
34
|
+
bolloon agent discover | register | manifest | inspect
|
|
35
|
+
bolloon task list | status | result | retry | run | send|inbox|accept|reject|complete|cancel(未实现)
|
|
36
|
+
bolloon wallet status | policy | set-policy
|
|
37
|
+
bolloon payment pending | approve | reject
|
|
38
|
+
bolloon trade list | show | events | reconcile
|
|
39
|
+
bolloon chain status | escrow show <taskKey> | timeline <taskKey> | index status|stats|sync
|
|
40
|
+
| trade create|submit-proof|release|recover (P6: 真链读写, 复用 P3/P4/P5)
|
|
41
|
+
|
|
42
|
+
全局选项 (全命令组有效):
|
|
43
|
+
--json 输出 §2 冻结信封 (失败也结构化: ok:false + code + next_action)
|
|
44
|
+
--quiet 只输出结果 (payload 的 JSON)
|
|
45
|
+
--request-id <id> 幂等键原样透传 (用在真实做幂等查找/派生的命令上)
|
|
46
|
+
--timeout <ms> 硬超时; 超时给 code=TIMEOUT (不是"打印一行错误就退出")
|
|
47
|
+
|
|
48
|
+
判据永远是 ok / code, **不是**退出码; 也不要把 paid / delivered 读成成功
|
|
49
|
+
(local-dev 永远不是链上结算 —— 见 docs/wiki/access-protocol-v1.md §5)。
|
|
50
|
+
链命令的失败码 (P6 新增, append-only): CHAIN_NOT_CONFIGURED · CHAIN_UNAVAILABLE · CHAIN_UNCERTAIN ·
|
|
51
|
+
CHAIN_TX_REVERTED · ESCROW_NOT_FOUND · INSUFFICIENT_FUNDS · NOT_AUTHORIZED · REORG_SUSPECTED ·
|
|
52
|
+
INDEX_IDENTITY_CHANGED (换合约部署/换链实例 ⇒ 索引身份变了, **不是**重组; 修法 bolloon chain index rebuild)。
|
|
53
|
+
`;
|
|
54
|
+
/** 跑一个命令组, 返回进程退出码 (0=ok, 1=失败) */
|
|
55
|
+
export async function runServiceGroup(mode, args) {
|
|
56
|
+
const flags = parseFlags(args);
|
|
57
|
+
switch (mode) {
|
|
58
|
+
case 'network': return runCommand(flags, networkCommand);
|
|
59
|
+
case 'agent': return runCommand(flags, agentCommand);
|
|
60
|
+
case 'task': return runCommand(flags, taskCommand);
|
|
61
|
+
case 'wallet': return runCommand(flags, walletCommand);
|
|
62
|
+
case 'payment': return runCommand(flags, paymentCommand);
|
|
63
|
+
case 'trade': return runCommand(flags, tradeCommand);
|
|
64
|
+
case 'chain': return runCommand(flags, chainCommand);
|
|
65
|
+
default:
|
|
66
|
+
console.log(GROUPS_HELP);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp.ts — P4 `bolloon mcp serve` (MCP 适配层入口)
|
|
3
|
+
*
|
|
4
|
+
* 只有三条子命令:
|
|
5
|
+
* serve 启动 stdio MCP server (给 Claude Code / Cursor / 任何 MCP 客户端接)
|
|
6
|
+
* tools 列出本次暴露的 tools / resources 清单 (验收/排查用; 不改任何状态)
|
|
7
|
+
* help 帮助
|
|
8
|
+
*
|
|
9
|
+
* 硬要求: `serve` 期间 **stdout 只走 JSON-RPC** —— 所以这里把 console.log/info/debug/warn
|
|
10
|
+
* 改道到 stderr (任何被包服务顺手打的日志都不许污染协议流)。
|
|
11
|
+
*/
|
|
12
|
+
import { serveStdio, inventory } from '../mcp/server.js';
|
|
13
|
+
import { NOT_EXPOSED } from '../mcp/tools.js';
|
|
14
|
+
import { title } from '../protocol-envelope.js';
|
|
15
|
+
export const MCP_USAGE = `
|
|
16
|
+
${title('bolloon mcp')}
|
|
17
|
+
bolloon mcp serve 启动 MCP server (stdio; 只调 P3 命令/服务层, 不复制业务逻辑)
|
|
18
|
+
bolloon mcp tools [--json] 列出暴露的 tools / resources (只读清单)
|
|
19
|
+
bolloon mcp help 本帮助
|
|
20
|
+
|
|
21
|
+
接入客户端 (示例, 放进 MCP 配置):
|
|
22
|
+
{ "mcpServers": { "bolloon": { "command": "bolloon", "args": ["mcp", "serve"] } } }
|
|
23
|
+
|
|
24
|
+
判据永远是信封里的 ok/code (失败也返回信封 + isError=true), 不是退出码;
|
|
25
|
+
不返回私钥/助记词 · 不绕过 payment policy · 不改交易历史 · 不伪造 verified · 无授权不切自主支付。
|
|
26
|
+
`;
|
|
27
|
+
/** 把 console.log 一类的"顺手日志"改道 stderr, 保护 stdout 的 JSON-RPC 流 */
|
|
28
|
+
function protectStdout() {
|
|
29
|
+
const toStderr = (...a) => {
|
|
30
|
+
const s = a.map((x) => {
|
|
31
|
+
if (typeof x === 'string')
|
|
32
|
+
return x;
|
|
33
|
+
try {
|
|
34
|
+
return JSON.stringify(x);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return String(x);
|
|
38
|
+
}
|
|
39
|
+
}).join(' ');
|
|
40
|
+
process.stderr.write(`${s}\n`);
|
|
41
|
+
};
|
|
42
|
+
console.log = toStderr;
|
|
43
|
+
console.info = toStderr;
|
|
44
|
+
console.debug = toStderr;
|
|
45
|
+
console.warn = toStderr;
|
|
46
|
+
}
|
|
47
|
+
export async function runMcpCommand(args) {
|
|
48
|
+
const sub = args[0];
|
|
49
|
+
switch (sub) {
|
|
50
|
+
case 'serve':
|
|
51
|
+
case 'server':
|
|
52
|
+
return mcpServe(args.slice(1));
|
|
53
|
+
case 'tools':
|
|
54
|
+
case 'list':
|
|
55
|
+
return mcpTools(args.slice(1));
|
|
56
|
+
case undefined:
|
|
57
|
+
case 'help':
|
|
58
|
+
case '--help':
|
|
59
|
+
case '-h':
|
|
60
|
+
console.log(MCP_USAGE);
|
|
61
|
+
return sub === undefined ? 1 : 0;
|
|
62
|
+
default:
|
|
63
|
+
console.log(`未知 mcp 子命令: ${sub}`);
|
|
64
|
+
console.log(MCP_USAGE);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function mcpServe(rest) {
|
|
69
|
+
if (rest.includes('--help') || rest.includes('-h')) {
|
|
70
|
+
console.log(MCP_USAGE);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
if (rest.length) {
|
|
74
|
+
console.error(`bolloon mcp serve 不接受参数 (收到: ${rest.join(' ')}) —— 所有调用都走 JSON-RPC (stdio)`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
protectStdout();
|
|
78
|
+
return serveStdio();
|
|
79
|
+
}
|
|
80
|
+
function mcpTools(rest) {
|
|
81
|
+
const inv = inventory();
|
|
82
|
+
if (rest.includes('--json')) {
|
|
83
|
+
console.log(JSON.stringify(inv, null, 2));
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
console.log(`\nbolloon mcp (server ${inv.server.name}@${inv.server.version}, MCP ${inv.server.protocolVersions[0]})`);
|
|
87
|
+
console.log(`\n 工具 (${inv.tools.length}, 每个 = 一条 P3 子命令的薄包装):`);
|
|
88
|
+
for (const t of inv.tools)
|
|
89
|
+
console.log(` · ${t.name.padEnd(28)} ${t.title} [${t.params.join(', ')}]`);
|
|
90
|
+
console.log(`\n 资源 (${inv.resources.length}):`);
|
|
91
|
+
for (const r of inv.resources)
|
|
92
|
+
console.log(` · ${r.uri.padEnd(30)} ${r.name}`);
|
|
93
|
+
console.log('\n **刻意不暴露** (暴露就得在 MCP 层假装成功):');
|
|
94
|
+
for (const n of NOT_EXPOSED)
|
|
95
|
+
console.log(` · ${n.command}\n ${n.reason}`);
|
|
96
|
+
console.log('');
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* network.ts — P3 `bolloon network init|join|status|leave|peers`
|
|
3
|
+
*
|
|
4
|
+
* 全部是**现有服务的薄包装** (不重实现、不编造):
|
|
5
|
+
* init → `gateway-join.ensureGatewayNode()` (本机 libp2p 节点初始化, 幂等)
|
|
6
|
+
* join → `gateway-network.joinNetwork(link)` (orbitdb:// / ipns:// / https:// 链接)
|
|
7
|
+
* 或 `gateway-join.joinGlobalGateway()` (文档驱动全球入网)
|
|
8
|
+
* status → `p2p-info.getLocalP2pInfo()` + `gateway-network.listJoinedNetworks()`
|
|
9
|
+
* + `gateway-join.getGatewayJoinState()` + `agent-registry` (只读, 不主动 warm)
|
|
10
|
+
* peers → `network/p2p` 的 `getPeers()` (需要本进程里有在跑的节点)
|
|
11
|
+
* leave → **没有实现** (仓库里不存在 leaveNetwork) → 如实报 `C_NOT_IMPLEMENTED`
|
|
12
|
+
*
|
|
13
|
+
* 硬要求 (leo 2026-09-21): `network join` 不许只说"加入成功" —— 必须报
|
|
14
|
+
* DID · 网络名 · manifest 是否注册 · P2P 是否在线 · registry 是否同步 · 是否离线降级 · 下一步。
|
|
15
|
+
*/
|
|
16
|
+
import { okEnvelope, failEnvelope, notImplemented, line, title, hint, plain, opt, has, } from '../protocol-envelope.js';
|
|
17
|
+
export const NETWORK_USAGE = `
|
|
18
|
+
${title('bolloon network')}
|
|
19
|
+
bolloon network status [--json] 本机入网态 (DID / 网络 / P2P / registry)
|
|
20
|
+
bolloon network init [--json] 初始化本机 P2P 节点 (幂等)
|
|
21
|
+
bolloon network join [--json] 文档驱动全球入网 (幂等; 已在网则报 already)
|
|
22
|
+
bolloon network join <link> [--json] 按链接加入共享网络 (orbitdb:// / ipns:// / https://)
|
|
23
|
+
bolloon network peers [--json] 本机节点的已连接 peer
|
|
24
|
+
bolloon network leave 未实现 (仓库没有 leaveNetwork) → 如实报 C_NOT_IMPLEMENTED
|
|
25
|
+
|
|
26
|
+
选项: --url <doc> · --name <名> · --capabilities a,b · --force · --request-id <id> · --timeout <ms> · --quiet
|
|
27
|
+
`;
|
|
28
|
+
/** 只读地收集本机入网事实 (不 warm OrbitDB, 不启动节点, 不改任何文件) */
|
|
29
|
+
async function collectFacts() {
|
|
30
|
+
const { getLocalP2pInfo } = await import('../../agents/p2p-info.js');
|
|
31
|
+
const { listJoinedNetworks } = await import('../../agents/gateway-network.js');
|
|
32
|
+
const { getGatewayJoinState } = await import('../../agents/gateway-join.js');
|
|
33
|
+
const { getAgentRegistry } = await import('../../agents/agent-registry.js');
|
|
34
|
+
const { getLocalManifest } = await import('../../agents/agent-manifest-protocol.js');
|
|
35
|
+
const { getUserOwnerDid } = await import('../../agents/agent-identity.js');
|
|
36
|
+
const p2p = await getLocalP2pInfo();
|
|
37
|
+
const joinedRaw = await listJoinedNetworks();
|
|
38
|
+
const gate = await getGatewayJoinState().catch(() => null);
|
|
39
|
+
const did = String(gate?.did || p2p.did || getUserOwnerDid() || '');
|
|
40
|
+
const name = String(gate?.name || p2p.name || '');
|
|
41
|
+
// manifest 是**进程内**结构 (agent-manifest-protocol 不落盘) —— 一次性 CLI 里读到空是正常的, 如实说
|
|
42
|
+
const m = getLocalManifest();
|
|
43
|
+
const agentIds = (m.agents || []).map((a) => String(a.id));
|
|
44
|
+
const capabilities = (m.agents || []).flatMap((a) => (a.capabilities || []).map(String));
|
|
45
|
+
const reg = getAgentRegistry(did || 'local');
|
|
46
|
+
let services = null;
|
|
47
|
+
try {
|
|
48
|
+
services = (await reg.loadLocal()).length;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
services = null;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
did,
|
|
55
|
+
name,
|
|
56
|
+
p2p: {
|
|
57
|
+
online: !!p2p.peerId && p2p.source === 'live',
|
|
58
|
+
source: p2p.source,
|
|
59
|
+
peerId: p2p.peerId || '',
|
|
60
|
+
multiaddrs: p2p.multiaddrs || [],
|
|
61
|
+
relayAddrs: p2p.relayAddrs || [],
|
|
62
|
+
isRelay: !!p2p.isRelay,
|
|
63
|
+
note: p2p.note,
|
|
64
|
+
},
|
|
65
|
+
joined: joinedRaw.map((n) => ({
|
|
66
|
+
link: n.link, kind: n.kind, name: n.name, joinedAt: n.joinedAt,
|
|
67
|
+
serviceCount: n.serviceCount, lastSyncAt: n.lastSyncAt, networkId: n.networkId,
|
|
68
|
+
})),
|
|
69
|
+
gatewayJoin: gate ? {
|
|
70
|
+
url: gate.url, did: gate.did, name: gate.name, peerId: gate.peerId,
|
|
71
|
+
networkLink: gate.networkLink, networkId: gate.networkId, joinedAt: gate.joinedAt, capabilities: gate.capabilities,
|
|
72
|
+
} : null,
|
|
73
|
+
manifest: {
|
|
74
|
+
registered: agentIds.length > 0,
|
|
75
|
+
agentIds,
|
|
76
|
+
capabilities: Array.from(new Set(capabilities)),
|
|
77
|
+
note: agentIds.length > 0
|
|
78
|
+
? '本进程内 manifest 已注册 (刚入网时登记的那份)'
|
|
79
|
+
: 'manifest 只在运行中的 Runtime 进程内 (agent-manifest-protocol 不落盘) —— 一次性 CLI 命令读不到它不等于没注册过',
|
|
80
|
+
},
|
|
81
|
+
registry: {
|
|
82
|
+
ready: reg.ready,
|
|
83
|
+
storeName: reg.storeName,
|
|
84
|
+
services: services ?? 0,
|
|
85
|
+
source: reg.ready ? 'orbitdb' : 'local',
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** registry 预热 (有界): CLI 一次性命令里不许无限等 OrbitDB/IPFS */
|
|
90
|
+
async function warmRegistryBounded(ms) {
|
|
91
|
+
try {
|
|
92
|
+
const { warmAgentRegistry } = await import('../../agents/agent-registry.js');
|
|
93
|
+
return await Promise.race([
|
|
94
|
+
warmAgentRegistry().then((v) => !!v),
|
|
95
|
+
new Promise((resolve) => setTimeout(() => resolve(false), ms)),
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function factsHuman(f, head) {
|
|
103
|
+
const out = [title(head)];
|
|
104
|
+
out.push(line('DID', f.did));
|
|
105
|
+
out.push(line('名称', f.name));
|
|
106
|
+
out.push(line('已加入网络', `${f.joined.length} 个`));
|
|
107
|
+
for (const n of f.joined)
|
|
108
|
+
out.push(` · ${n.name || n.networkId || n.link.slice(0, 60)} (${n.joinedAt.slice(0, 19)}, 服务 ${n.serviceCount})`);
|
|
109
|
+
if (f.gatewayJoin)
|
|
110
|
+
out.push(line('全球入网', `${f.gatewayJoin.networkId || f.gatewayJoin.networkLink || f.gatewayJoin.url} @ ${f.gatewayJoin.joinedAt.slice(0, 19)}`));
|
|
111
|
+
out.push(line('P2P', f.p2p.online ? `在线 peerId=${f.p2p.peerId.slice(0, 16)}…` : '不在线 (当前进程没有节点在跑)'));
|
|
112
|
+
if (f.p2p.multiaddrs.length)
|
|
113
|
+
out.push(line('可拨入地址', `${f.p2p.multiaddrs.length} 条, 例: ${f.p2p.multiaddrs[0].slice(0, 72)}`));
|
|
114
|
+
if (f.p2p.note)
|
|
115
|
+
out.push(line('说明', f.p2p.note));
|
|
116
|
+
out.push(line('manifest', f.manifest.registered ? `已注册 (${f.manifest.agentIds.join(', ')})` : `本 CLI 进程未读到 (${f.manifest.note})`));
|
|
117
|
+
out.push(line('registry', `${f.registry.ready ? 'OrbitDB 就绪' : '未就绪 (离线/未 warm)'} · ${f.registry.source} · 本地服务 ${f.registry.services} 条`));
|
|
118
|
+
out.push(line('离线降级', f.joined.length > 0 && !f.p2p.online ? '是 (入网记录在, 但当前没有节点在跑)' : '否'));
|
|
119
|
+
return out.join('\n');
|
|
120
|
+
}
|
|
121
|
+
function nextActionForFacts(f) {
|
|
122
|
+
if (!f.did)
|
|
123
|
+
return 'rejoin_network';
|
|
124
|
+
if (f.joined.length === 0 && !f.gatewayJoin)
|
|
125
|
+
return 'rejoin_network';
|
|
126
|
+
if (!f.p2p.online)
|
|
127
|
+
return 'rejoin_network';
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
export async function networkCommand(flags) {
|
|
131
|
+
const sub = flags.positionals[0];
|
|
132
|
+
switch (sub) {
|
|
133
|
+
case 'status': return networkStatus(flags);
|
|
134
|
+
case 'init': return networkInit(flags);
|
|
135
|
+
case 'join': return networkJoin(flags);
|
|
136
|
+
case 'peers': return networkPeers(flags);
|
|
137
|
+
case 'leave': return networkLeave(flags);
|
|
138
|
+
default:
|
|
139
|
+
return {
|
|
140
|
+
envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 network 子命令: ${sub}` : '缺少 network 子命令', { usage: plain(NETWORK_USAGE.trim()) }, [], 'needs_human'),
|
|
141
|
+
human: NETWORK_USAGE,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ── network status ──────────────────────────────────────────────────────────
|
|
146
|
+
async function networkStatus(flags) {
|
|
147
|
+
const f = await collectFacts();
|
|
148
|
+
const next = nextActionForFacts(f);
|
|
149
|
+
if (!f.did && f.joined.length === 0 && !f.gatewayJoin) {
|
|
150
|
+
// 诚实的前置条件失败 (§3 NETWORK_NOT_JOINED: 本机没有任何已加入网络)
|
|
151
|
+
return {
|
|
152
|
+
envelope: failEnvelope('NETWORK_NOT_JOINED', '本机还没有加入任何网络 (也没有 DID 身份)', {
|
|
153
|
+
did: f.did, joinedNetworks: [], registryReady: f.registry.ready, p2p: f.p2p,
|
|
154
|
+
}, [], 'rejoin_network'),
|
|
155
|
+
human: `${factsHuman(f, 'bolloon network status')}\n\n${hint('下一步: bolloon network join (或先 bolloon setup 建立身份)')}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const data = {
|
|
159
|
+
did: f.did,
|
|
160
|
+
name: f.name,
|
|
161
|
+
joinedNetworks: f.joined,
|
|
162
|
+
gatewayJoin: f.gatewayJoin,
|
|
163
|
+
p2pOnline: f.p2p.online,
|
|
164
|
+
p2p: f.p2p,
|
|
165
|
+
manifest: f.manifest,
|
|
166
|
+
registry: f.registry,
|
|
167
|
+
offlineDegraded: !f.p2p.online,
|
|
168
|
+
};
|
|
169
|
+
const env = okEnvelope('OK', `本机入网态: ${f.joined.length} 个网络${f.p2p.online ? ', P2P 在线' : ', P2P 未在线'}`, data, [
|
|
170
|
+
...(f.did ? [f.did] : []),
|
|
171
|
+
...(f.p2p.peerId ? [f.p2p.peerId] : []),
|
|
172
|
+
], next);
|
|
173
|
+
return { envelope: env, human: `${factsHuman(f, 'bolloon network status')}\n\n${hint(next === 'rejoin_network' ? '下一步: 启动一个本机 Runtime (bolloon --web) 或 bolloon network init, 才有节点在跑' : 'P2P 在线, 可直接 bolloon p2p --json 拿到可拨入地址')}` };
|
|
174
|
+
}
|
|
175
|
+
// ── network init ────────────────────────────────────────────────────────────
|
|
176
|
+
async function networkInit(flags) {
|
|
177
|
+
const { ensureGatewayNode } = await import('../../agents/gateway-join.js');
|
|
178
|
+
const node = await ensureGatewayNode({ nodeTimeoutMs: flags.timeoutMs ?? 25_000 });
|
|
179
|
+
if (!node.ok) {
|
|
180
|
+
return {
|
|
181
|
+
envelope: failEnvelope('NETWORK_NOT_JOINED', `本机 P2P 节点没起来: ${node.error || '未知原因'}`, { error: node.error || null, did: (await collectFacts()).did }, [], 'rejoin_network'),
|
|
182
|
+
human: `本机 P2P 节点没起来\n 原因: ${node.error || '未知原因'}\n\n${hint('下一步: bolloon doctor 看运行时; 或 bolloon network join 走文档驱动入网')}`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const f = await collectFacts();
|
|
186
|
+
return {
|
|
187
|
+
envelope: okEnvelope('NETWORK_NODE_READY', `本机 P2P 节点就绪 (peerId=${String(node.peerId).slice(0, 16)}…)`, {
|
|
188
|
+
did: f.did, peerId: node.peerId || '', started: !!node.started, multiaddrs: node.multiaddrs || [],
|
|
189
|
+
}, node.peerId ? [String(node.peerId)] : [], f.did ? null : 'rejoin_network'),
|
|
190
|
+
human: `${title('bolloon network init')}\n${line('DID', f.did)}\n${line('peerId', node.peerId || '(未拿到)')}\n${line('本次启动', node.started ? '是' : '否 (已在跑)')}\n${line('可拨入地址', (node.multiaddrs || []).length + ' 条')}\n\n${hint('注意: 这条命令起的节点随进程结束 (一次性命令) —— 要长期在线用 bolloon --web')}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// ── network join ────────────────────────────────────────────────────────────
|
|
194
|
+
async function networkJoin(flags) {
|
|
195
|
+
const link = flags.positionals[1] || opt(flags, '--link');
|
|
196
|
+
const wantGlobal = has(flags, '--global') || !link;
|
|
197
|
+
const steps = [];
|
|
198
|
+
let joined = false;
|
|
199
|
+
let already = false;
|
|
200
|
+
let networkName = '';
|
|
201
|
+
let networkId = '';
|
|
202
|
+
let networkLink = '';
|
|
203
|
+
let error = '';
|
|
204
|
+
let mode = '';
|
|
205
|
+
if (!wantGlobal && link) {
|
|
206
|
+
// 按链接加入 (gateway-network.joinNetwork: 拉远端服务 → 并入本地 registry + 记成员身份)
|
|
207
|
+
const { joinNetwork } = await import('../../agents/gateway-network.js');
|
|
208
|
+
mode = 'link';
|
|
209
|
+
const r = await joinNetwork(link);
|
|
210
|
+
joined = r.ok;
|
|
211
|
+
already = !!r.already;
|
|
212
|
+
networkName = r.networkName || '';
|
|
213
|
+
networkId = r.networkId || '';
|
|
214
|
+
error = r.error || '';
|
|
215
|
+
steps.push({ step: '按链接加入网络', ok: r.ok, note: r.ok ? `${r.already ? '已在网(幂等)' : `拉取远端服务 ${r.total} 条, 新增 ${r.joined} 条`} (kind=${r.linkKind || '?'})` : (r.error || '') });
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// 文档驱动全球入网 (gateway-join.joinGlobalGateway)
|
|
219
|
+
const { joinGlobalGateway } = await import('../../agents/gateway-join.js');
|
|
220
|
+
mode = 'global';
|
|
221
|
+
const caps = (opt(flags, '--capabilities') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
222
|
+
const r = await joinGlobalGateway({
|
|
223
|
+
url: opt(flags, '--url'),
|
|
224
|
+
name: opt(flags, '--name'),
|
|
225
|
+
capabilities: caps.length ? caps : undefined,
|
|
226
|
+
force: has(flags, '--force'),
|
|
227
|
+
});
|
|
228
|
+
joined = r.ok;
|
|
229
|
+
already = !!r.already;
|
|
230
|
+
networkName = r.networkId || '';
|
|
231
|
+
networkId = r.networkId || '';
|
|
232
|
+
networkLink = r.networkLink || '';
|
|
233
|
+
error = r.error || '';
|
|
234
|
+
steps.push(...r.steps.map((s) => ({ step: s.step, ok: s.ok, note: s.note })));
|
|
235
|
+
}
|
|
236
|
+
// 入网后重新采集事实 (join 会写 gateway-join.json / gateway-networks.json)
|
|
237
|
+
const f = await collectFacts();
|
|
238
|
+
const manifestStep = steps.find((s) => s.step.includes('manifest'));
|
|
239
|
+
const serviceStep = steps.find((s) => s.step.includes('服务登记'));
|
|
240
|
+
const registrySynced = mode === 'link'
|
|
241
|
+
? joined || f.joined.length > 0
|
|
242
|
+
: (serviceStep ? serviceStep.ok : f.joined.length > 0 || !!f.gatewayJoin);
|
|
243
|
+
const manifestRegistered = mode === 'global' ? (manifestStep ? manifestStep.ok : f.manifest.registered) : f.manifest.registered;
|
|
244
|
+
const offlineDegraded = !f.p2p.online;
|
|
245
|
+
const data = {
|
|
246
|
+
mode,
|
|
247
|
+
did: f.did,
|
|
248
|
+
networkName: networkName || f.name,
|
|
249
|
+
networkId,
|
|
250
|
+
networkLink,
|
|
251
|
+
already,
|
|
252
|
+
manifest: {
|
|
253
|
+
registered: manifestRegistered,
|
|
254
|
+
persisted: false,
|
|
255
|
+
agentIds: f.manifest.agentIds,
|
|
256
|
+
note: f.manifest.note,
|
|
257
|
+
},
|
|
258
|
+
p2p: {
|
|
259
|
+
online: f.p2p.online,
|
|
260
|
+
source: f.p2p.source,
|
|
261
|
+
peerId: f.p2p.peerId,
|
|
262
|
+
multiaddrs: f.p2p.multiaddrs.slice(0, 4),
|
|
263
|
+
},
|
|
264
|
+
registry: { ready: f.registry.ready, synced: registrySynced, services: f.registry.services, source: f.registry.source },
|
|
265
|
+
offlineDegraded,
|
|
266
|
+
steps,
|
|
267
|
+
};
|
|
268
|
+
const evidence = [f.did, f.p2p.peerId, networkLink].filter(Boolean);
|
|
269
|
+
const human = [
|
|
270
|
+
title('bolloon network join'),
|
|
271
|
+
line('DID', f.did),
|
|
272
|
+
line('网络名', networkName || f.name || '(未拿到)'),
|
|
273
|
+
line('入网方式', mode === 'global' ? '文档驱动 (joinGlobalGateway)' : `链接 (${link})`),
|
|
274
|
+
line('幂等', already ? '已在网 (already=true, 没有重复建网)' : '本次入网'),
|
|
275
|
+
line('manifest', manifestRegistered ? '已注册 (本进程读回确认)' : `未确认 (${f.manifest.note})`),
|
|
276
|
+
line('P2P', f.p2p.online ? `在线 peerId=${f.p2p.peerId.slice(0, 16)}…` : `不在线 (source=${f.p2p.source || 'none'})`),
|
|
277
|
+
line('registry', `${f.registry.ready ? 'OrbitDB 就绪' : '未就绪'} · 同步=${registrySynced ? '是' : '否'} · 本地服务 ${f.registry.services} 条`),
|
|
278
|
+
line('离线降级', offlineDegraded ? '是 —— 入网记录在, 但当前没有节点在跑 (重启后由 restoreJoinedNetworks 恢复)' : '否'),
|
|
279
|
+
steps.length ? `\n 步骤:` : '',
|
|
280
|
+
...steps.map((s) => ` ${s.ok ? '✓' : '✗'} ${s.step}: ${s.note.slice(0, 120)}`),
|
|
281
|
+
].filter(Boolean).join('\n');
|
|
282
|
+
const next = !joined ? 'rejoin_network' : (offlineDegraded ? 'rejoin_network' : null);
|
|
283
|
+
const env = joined
|
|
284
|
+
? okEnvelope('NETWORK_JOINED', `已加入网络 ${networkName || f.name || networkLink || ''}${already ? ' (幂等命中, 未重复建网)' : ''}`, data, evidence, next)
|
|
285
|
+
: failEnvelope('NETWORK_NOT_JOINED', `入网没成功: ${error || '未知原因'}`, data, evidence, 'rejoin_network');
|
|
286
|
+
return { envelope: env, human: `${human}\n\n${hint(joined ? (offlineDegraded ? '下一步: 启动常驻 Runtime (bolloon --web) 让节点真的在线; 再 bolloon p2p --json 拿可拨入地址' : '下一步: 无需额外动作') : '下一步: 检查网络/文档地址, 或 bolloon network status 看现状')}` };
|
|
287
|
+
}
|
|
288
|
+
// ── network peers ───────────────────────────────────────────────────────────
|
|
289
|
+
async function networkPeers(flags) {
|
|
290
|
+
const peerId = await currentProcessPeerId();
|
|
291
|
+
if (!peerId) {
|
|
292
|
+
// §3 NETWORK_NOT_JOINED: 本机没有 peerId (没有在跑的节点) —— 不编造 peer
|
|
293
|
+
return {
|
|
294
|
+
envelope: failEnvelope('NETWORK_NOT_JOINED', '当前进程没有在跑的 P2P 节点, 拿不到 peer 列表', {
|
|
295
|
+
online: false, peers: [],
|
|
296
|
+
note: 'peer 列表来自运行中的节点 (p2pNetwork.getPeers()); 一次性 CLI 命令里节点没起就没有可列的对端',
|
|
297
|
+
}, [], 'rejoin_network'),
|
|
298
|
+
human: `${title('bolloon network peers')}\n 当前进程没有在跑的 P2P 节点 → 没有 peer 可列 (不是"网络上没人")\n\n${hint('下一步: bolloon network init 起一个节点, 或 bolloon --web 让 Runtime 常驻')}`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const mod = await import('../../network/p2p.js');
|
|
302
|
+
const peers = (mod.p2pNetwork?.getPeers?.() || []).map((p) => String(p));
|
|
303
|
+
return {
|
|
304
|
+
envelope: okEnvelope('OK', `本机节点已连接 ${peers.length} 个 peer`, {
|
|
305
|
+
online: true, peerId, peers, count: peers.length,
|
|
306
|
+
}, [peerId], null),
|
|
307
|
+
human: `${title('bolloon network peers')}\n${line('本机 peerId', peerId)}\n${line('连接数', peers.length)}\n${peers.map((p) => ` · ${p}`).join('\n')}`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/** 只在**本进程**里有节点时给 peerId (与 p2p-info 的探测同一套 API) */
|
|
311
|
+
async function currentProcessPeerId() {
|
|
312
|
+
try {
|
|
313
|
+
const mod = await import('../../network/p2p.js');
|
|
314
|
+
const net = mod.p2pNetwork;
|
|
315
|
+
if (!net?.getNode?.())
|
|
316
|
+
return '';
|
|
317
|
+
return String(net.getNodePeerId?.() || '');
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return '';
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// ── network leave ───────────────────────────────────────────────────────────
|
|
324
|
+
async function networkLeave(_flags) {
|
|
325
|
+
const env = notImplemented('network leave 还没实现: 仓库里不存在 leaveNetwork (没有任何"退出网络"的服务可包)', {
|
|
326
|
+
plannedCapability: 'network.leave',
|
|
327
|
+
contractState: '入网记录在 ~/.bolloon/gateway-networks.json + ~/.bolloon/gateway-join.json (没有删除/退出的服务入口)',
|
|
328
|
+
alternative: '今天只能手工处理入网记录文件 (CLI 不做, 免得绕过 restoreJoinedNetworks 的成员身份语义)',
|
|
329
|
+
});
|
|
330
|
+
return { envelope: env, human: failHumanLeave(env.message) };
|
|
331
|
+
}
|
|
332
|
+
function failHumanLeave(msg) {
|
|
333
|
+
return `${title('bolloon network leave')}\n ${msg}\n\n${hint('下一步: 没有 → 这条能力在路线图上 (upgrade_client)')}`;
|
|
334
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* payment.ts — P3 `bolloon payment pending|approve|reject`
|
|
3
|
+
*
|
|
4
|
+
* 薄包装: `agents/payment-approval.ts` 的 `PaymentApprovalStore` (pending/approve/reject, 落 ~/.bolloon/payment-approvals.json)
|
|
5
|
+
*
|
|
6
|
+
* 红线:
|
|
7
|
+
* · CLI **不代付款** —— 批准只改审批状态; 真正发起支付要由注入的 executor / 持钱包的一方做
|
|
8
|
+
* (与 payment-recovery 同源: "Supervisor 不代付款, 它没有私钥")
|
|
9
|
+
* · `approve` 之后**不许**把结果说成"已付款" (§2 硬规则 2)
|
|
10
|
+
*/
|
|
11
|
+
import { okEnvelope, failEnvelope, line, title, hint, plain, } from '../protocol-envelope.js';
|
|
12
|
+
export const PAYMENT_USAGE = `
|
|
13
|
+
${title('bolloon payment')}
|
|
14
|
+
bolloon payment pending [--json] 待人工放行的付款请求
|
|
15
|
+
bolloon payment approve <审批id> [--json] 批准 (只改审批状态; CLI 不发付款)
|
|
16
|
+
bolloon payment reject <审批id> [--json] 拒绝
|
|
17
|
+
|
|
18
|
+
选项: --json · --quiet · --timeout <ms>
|
|
19
|
+
`;
|
|
20
|
+
export async function paymentCommand(flags) {
|
|
21
|
+
const sub = flags.positionals[0];
|
|
22
|
+
switch (sub) {
|
|
23
|
+
case 'pending': return paymentPending(flags);
|
|
24
|
+
case 'approve': return paymentApprove(flags);
|
|
25
|
+
case 'reject': return paymentReject(flags);
|
|
26
|
+
default:
|
|
27
|
+
return {
|
|
28
|
+
envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 payment 子命令: ${sub}` : '缺少 payment 子命令', { usage: plain(PAYMENT_USAGE.trim()) }, [], 'needs_human'),
|
|
29
|
+
human: PAYMENT_USAGE,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function view(a) {
|
|
34
|
+
return {
|
|
35
|
+
id: a.id, service: a.service, amount: a.amount, recipient: a.recipient,
|
|
36
|
+
reason: a.reason, status: a.status, createdAt: new Date(a.createdAt).toISOString(),
|
|
37
|
+
decidedAt: a.decidedAt ? new Date(a.decidedAt).toISOString() : null,
|
|
38
|
+
result: a.result || null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function paymentPending(flags) {
|
|
42
|
+
const { getApprovalStore } = await import('../../agents/payment-approval.js');
|
|
43
|
+
const store = getApprovalStore();
|
|
44
|
+
const pending = await store.pending();
|
|
45
|
+
const all = await store.list();
|
|
46
|
+
const data = {
|
|
47
|
+
count: pending.length,
|
|
48
|
+
pending: pending.map(view),
|
|
49
|
+
total: all.length,
|
|
50
|
+
source: '~/.bolloon/payment-approvals.json',
|
|
51
|
+
note: pending.length
|
|
52
|
+
? '这些付款在等你放行: ok:false + code=PAYMENT_REQUIRED 是**正确表达** (不是错误, 也不是重试信号)'
|
|
53
|
+
: '没有待放行的付款',
|
|
54
|
+
};
|
|
55
|
+
const human = [
|
|
56
|
+
title('bolloon payment pending'),
|
|
57
|
+
line('待放行', `${pending.length} 笔 (历史共 ${all.length})`),
|
|
58
|
+
...pending.map((a) => `\n ${a.id}\n 服务 ${a.service} · ${a.amount} → ${a.recipient}\n 原因: ${a.reason}`),
|
|
59
|
+
pending.length ? '' : `\n ${hint('没有待放行的付款')}`,
|
|
60
|
+
].filter(Boolean).join('\n');
|
|
61
|
+
return {
|
|
62
|
+
envelope: okEnvelope('OK', `待放行付款 ${pending.length} 笔`, data, pending.map((a) => a.id), pending.length ? 'approve_payment' : null),
|
|
63
|
+
human,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function paymentApprove(flags) {
|
|
67
|
+
const id = flags.positionals[1];
|
|
68
|
+
if (!id) {
|
|
69
|
+
return {
|
|
70
|
+
envelope: failEnvelope('INVALID_ARGUMENT', '缺少审批 id (bolloon payment pending 列出)', { usage: plain(PAYMENT_USAGE.trim()) }, [], 'needs_human'),
|
|
71
|
+
human: PAYMENT_USAGE,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const { getApprovalStore } = await import('../../agents/payment-approval.js');
|
|
75
|
+
const r = await getApprovalStore().approve(id);
|
|
76
|
+
if (!r.ok || !r.approval) {
|
|
77
|
+
const notFound = /不存在/.test(String(r.error || ''));
|
|
78
|
+
return {
|
|
79
|
+
envelope: failEnvelope(notFound ? 'NOT_FOUND' : 'TASK_TRANSITION_REJECTED', `批准失败: ${r.error}`, { approvalId: id }, [], 'needs_human'),
|
|
80
|
+
human: `${title(`bolloon payment approve ${id}`)}\n ✗ ${r.error}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const executed = r.approval.status === 'executed';
|
|
84
|
+
const data = {
|
|
85
|
+
approval: view(r.approval),
|
|
86
|
+
executed,
|
|
87
|
+
paid: executed,
|
|
88
|
+
note: executed
|
|
89
|
+
? 'executor 已执行'
|
|
90
|
+
: '批准已记录; **没有发起付款** —— CLI 不持有钱包, 真正支付由持钱包的一方 (Runtime/executor) 执行',
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
envelope: okEnvelope('PAYMENT_APPROVED', executed ? `已批准并执行: ${id}` : `已批准: ${id} (未发起付款)`, data, [id], executed ? null : 'needs_human'),
|
|
94
|
+
human: [
|
|
95
|
+
title(`bolloon payment approve ${id}`),
|
|
96
|
+
line('状态', r.approval.status),
|
|
97
|
+
line('金额', `${r.approval.amount} → ${r.approval.recipient}`),
|
|
98
|
+
line('服务', r.approval.service),
|
|
99
|
+
line('是否已付款', executed ? '是 (executor 执行)' : '否 —— 只改了审批状态, CLI 不发付款'),
|
|
100
|
+
`\n ${data.note}`,
|
|
101
|
+
].join('\n'),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async function paymentReject(flags) {
|
|
105
|
+
const id = flags.positionals[1];
|
|
106
|
+
if (!id) {
|
|
107
|
+
return {
|
|
108
|
+
envelope: failEnvelope('INVALID_ARGUMENT', '缺少审批 id', { usage: plain(PAYMENT_USAGE.trim()) }, [], 'needs_human'),
|
|
109
|
+
human: PAYMENT_USAGE,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const { getApprovalStore } = await import('../../agents/payment-approval.js');
|
|
113
|
+
const r = await getApprovalStore().reject(id);
|
|
114
|
+
if (!r.ok || !r.approval) {
|
|
115
|
+
const notFound = /不存在/.test(String(r.error || ''));
|
|
116
|
+
return {
|
|
117
|
+
envelope: failEnvelope(notFound ? 'NOT_FOUND' : 'TASK_TRANSITION_REJECTED', `拒绝失败: ${r.error}`, { approvalId: id }, [], 'needs_human'),
|
|
118
|
+
human: `${title(`bolloon payment reject ${id}`)}\n ✗ ${r.error}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
envelope: okEnvelope('PAYMENT_REJECTED', `已拒绝: ${id}`, { approval: view(r.approval), paid: false }, [id], null),
|
|
123
|
+
human: `${title(`bolloon payment reject ${id}`)}\n${line('状态', r.approval.status)}\n${line('金额', `${r.approval.amount} → ${r.approval.recipient}`)}\n\n ${hint('拒绝后不会付款; 同一 requestId 的幂等保护仍在')}`,
|
|
124
|
+
};
|
|
125
|
+
}
|