@bolloon/bolloon-agent 0.4.31 → 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/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/network-pulse.js +464 -2
- package/dist/agents/task/task-onchain-runner.js +284 -0
- package/dist/agents/x402/paid-info-store.js +75 -6
- package/dist/cli/commands/chain.js +926 -0
- package/dist/cli/commands/index.js +10 -2
- package/dist/cli/mcp/server.js +4 -1
- package/dist/cli/mcp/tools.js +348 -5
- package/dist/cli/protocol-envelope.js +12 -0
- package/dist/cli-entry.js +5 -0
- 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/network-pulse.js +478 -3
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- 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/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 +13 -0
- package/package.json +2 -1
- package/scripts/build-web.ts +20 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>链上浏览器 · Chain Explorer — Bolloon</title>
|
|
7
|
+
<meta name="description" content="Bolloon 链上托管事件浏览器 (只读索引: 高度 / 统计 / 时间线 / cursor 增量 / 最终性三档)">
|
|
8
|
+
<link rel="icon" href="./icons/favicon.ico">
|
|
9
|
+
<style>
|
|
10
|
+
/* 本页自带样式 (不动主界面 style.css): 全部 cx- 前缀, 避免污染 */
|
|
11
|
+
:root {
|
|
12
|
+
--cx-bg: #0d1117; --cx-panel: #161b22; --cx-panel2: #1c2230; --cx-fg: #e6edf3;
|
|
13
|
+
--cx-dim: #8b949e; --cx-line: #30363d; --cx-accent: #58a6ff;
|
|
14
|
+
--cx-ok: #3fb950; --cx-warn: #d29922; --cx-bad: #f85149;
|
|
15
|
+
}
|
|
16
|
+
@media (prefers-color-scheme: light) {
|
|
17
|
+
:root { --cx-bg:#f6f8fa; --cx-panel:#ffffff; --cx-panel2:#f0f3f6; --cx-fg:#1f2328; --cx-dim:#59636e; --cx-line:#d0d7de; --cx-accent:#0969da; }
|
|
18
|
+
}
|
|
19
|
+
* { box-sizing: border-box; }
|
|
20
|
+
body { margin: 0; background: var(--cx-bg); color: var(--cx-fg); font: 14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; }
|
|
21
|
+
a { color: var(--cx-accent); }
|
|
22
|
+
.cx-wrap { max-width: 1180px; margin: 0 auto; padding: 18px 16px 60px; }
|
|
23
|
+
.cx-top { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--cx-line); padding-bottom: 12px; }
|
|
24
|
+
.cx-top h1 { font-size: 18px; margin: 0; font-weight: 650; letter-spacing: .2px; }
|
|
25
|
+
.cx-top .cx-sub { color: var(--cx-dim); font-size: 12.5px; margin-top: 2px; }
|
|
26
|
+
.cx-actions { display: flex; gap: 8px; align-items: center; }
|
|
27
|
+
button, .cx-btn { font: inherit; background: var(--cx-panel2); color: var(--cx-fg); border: 1px solid var(--cx-line); border-radius: 7px; padding: 6px 11px; cursor: pointer; }
|
|
28
|
+
button:hover { border-color: var(--cx-accent); }
|
|
29
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
30
|
+
.cx-state { display: flex; align-items: center; gap: 8px; margin: 14px 0 4px; font-size: 13px; color: var(--cx-dim); }
|
|
31
|
+
.cx-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--cx-dim); display: inline-block; }
|
|
32
|
+
.cx-dot-live { background: var(--cx-ok); }
|
|
33
|
+
.cx-dot-degraded { background: var(--cx-bad); }
|
|
34
|
+
.cx-dot-loading { background: var(--cx-warn); animation: cx-pulse 1.2s ease-in-out infinite; }
|
|
35
|
+
@keyframes cx-pulse { 0%,100% { opacity: 1 } 50% { opacity: .3 } }
|
|
36
|
+
.cx-degraded { margin: 10px 0; border: 1px solid var(--cx-bad); background: rgba(248,81,73,.08); border-radius: 8px; padding: 10px 12px; }
|
|
37
|
+
.cx-degraded b { color: var(--cx-bad); }
|
|
38
|
+
.cx-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 10px; margin: 14px 0 6px; }
|
|
39
|
+
.cx-card { background: var(--cx-panel); border: 1px solid var(--cx-line); border-radius: 9px; padding: 10px 12px; }
|
|
40
|
+
.cx-card .cx-k { color: var(--cx-dim); font-size: 12px; display: block; }
|
|
41
|
+
.cx-card .cx-v { font-size: 20px; font-weight: 650; font-variant-numeric: tabular-nums; }
|
|
42
|
+
.cx-card .cx-v-sm { font-size: 13px; font-weight: 500; font-variant-numeric: tabular-nums; word-break: break-all; }
|
|
43
|
+
.cx-cats { display: flex; flex-wrap: wrap; gap: 8px; margin: 6px 0 16px; }
|
|
44
|
+
.cx-cat { background: var(--cx-panel); border: 1px solid var(--cx-line); border-radius: 20px; padding: 4px 11px; font-size: 12.5px; }
|
|
45
|
+
.cx-cat b { font-variant-numeric: tabular-nums; }
|
|
46
|
+
.cx-sec-title { font-size: 14px; font-weight: 650; margin: 20px 0 8px; display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
|
|
47
|
+
.cx-sec-title .cx-hint { color: var(--cx-dim); font-weight: 400; font-size: 12.5px; }
|
|
48
|
+
.cx-legend { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
49
|
+
.cx-final { border: 1px solid var(--cx-line); background: var(--cx-panel); border-radius: 7px; padding: 3px 10px; font-size: 12.5px; }
|
|
50
|
+
.cx-final b { font-variant-numeric: tabular-nums; }
|
|
51
|
+
ol.cx-list { list-style: none; margin: 0; padding: 0; border: 1px solid var(--cx-line); border-radius: 9px; overflow: hidden; background: var(--cx-panel); }
|
|
52
|
+
.cx-row { display: grid; grid-template-columns: 110px 110px 108px 1fr; gap: 4px 10px; padding: 9px 12px; border-bottom: 1px solid var(--cx-line); align-items: center; }
|
|
53
|
+
.cx-row:last-child { border-bottom: none; }
|
|
54
|
+
.cx-row:hover { background: var(--cx-panel2); }
|
|
55
|
+
.cx-block, .cx-tx, .cx-task, .cx-conf { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; color: var(--cx-dim); }
|
|
56
|
+
.cx-event { font-weight: 600; }
|
|
57
|
+
.cx-args { grid-column: 1 / -1; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--cx-dim); word-break: break-all; }
|
|
58
|
+
.cx-badge { justify-self: start; font-size: 11.5px; border-radius: 20px; padding: 1px 9px; border: 1px solid var(--cx-line); }
|
|
59
|
+
.cx-badge-observed { color: var(--cx-warn); border-color: var(--cx-warn); }
|
|
60
|
+
.cx-badge-confirmed { color: var(--cx-accent); border-color: var(--cx-accent); }
|
|
61
|
+
.cx-badge-finalized { color: var(--cx-ok); border-color: var(--cx-ok); }
|
|
62
|
+
.cx-badge-unknown { color: var(--cx-dim); }
|
|
63
|
+
.cx-suspect { color: var(--cx-bad); font-size: 11.5px; }
|
|
64
|
+
.cx-link { padding: 2px 8px; font-size: 12px; border-radius: 20px; }
|
|
65
|
+
.cx-empty { padding: 14px 12px; color: var(--cx-dim); font-size: 13px; }
|
|
66
|
+
.cx-bar { display: flex; gap: 10px; align-items: center; justify-content: space-between; margin: 10px 0; flex-wrap: wrap; }
|
|
67
|
+
.cx-form { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
68
|
+
.cx-form input { font: inherit; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; background: var(--cx-panel); color: var(--cx-fg); border: 1px solid var(--cx-line); border-radius: 7px; padding: 6px 10px; min-width: 320px; }
|
|
69
|
+
.cx-note { color: var(--cx-dim); font-size: 12px; margin: 6px 0 0; }
|
|
70
|
+
@media (max-width: 720px) { .cx-row { grid-template-columns: 1fr 1fr; } .cx-args { grid-column: 1 / -1; } .cx-form input { min-width: 0; width: 100%; } }
|
|
71
|
+
/* 尊重系统设置: 关掉一切过渡/动画 */
|
|
72
|
+
@media (prefers-reduced-motion: reduce) {
|
|
73
|
+
* { animation: none !important; transition: none !important; }
|
|
74
|
+
}
|
|
75
|
+
.cx-reduce * { animation: none !important; transition: none !important; }
|
|
76
|
+
</style>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
<div class="cx-wrap" id="chain-explorer-root" data-api-base="">
|
|
80
|
+
<div class="cx-top">
|
|
81
|
+
<div>
|
|
82
|
+
<h1 data-zh="链上浏览器 (只读)" data-en="Chain Explorer (read-only)">链上浏览器 (只读)</h1>
|
|
83
|
+
<div class="cx-sub" data-zh="数据来自本机链上索引 (P5) — 只读: 不发交易 · 不签名 · 不写盘" data-en="Data comes from the local chain index (P5) — read-only: no transactions, no signing, no writes">数据来自本机链上索引 (P5) — 只读: 不发交易 · 不签名 · 不写盘</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="cx-actions">
|
|
86
|
+
<button type="button" id="cx-reload" data-zh="立即刷新" data-en="Refresh now">立即刷新</button>
|
|
87
|
+
<button type="button" id="cx-lang" data-zh="中文 / EN" data-en="EN / 中文">中文 / EN</button>
|
|
88
|
+
<a class="cx-btn" href="/" data-zh="← 回主界面" data-en="← Back to app">← 回主界面</a>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="cx-state" role="status" aria-live="polite">
|
|
93
|
+
<span class="cx-dot cx-dot-loading" id="cx-state-dot" aria-hidden="true"></span>
|
|
94
|
+
<span id="cx-state" data-zh="加载中…" data-en="loading…">加载中…</span>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="cx-degraded" id="cx-degraded" style="display:none">
|
|
98
|
+
<b data-zh="后端不可用 (降级态)" data-en="backend unavailable (degraded)">后端不可用 (降级态)</b>
|
|
99
|
+
<div class="cx-note" data-zh="数字一律显示为 “—” — 拿不到索引就绝不用 0 顶替 (0 会被误读成真值)。页面会自动按指数退避重试。" data-en="All numbers show “—” — when the index is unreachable we never substitute 0 (0 would read as a real value). The page retries with exponential backoff.">数字一律显示为 “—” — 拿不到索引就绝不用 0 顶替 (0 会被误读成真值)。页面会自动按指数退避重试。</div>
|
|
100
|
+
<div class="cx-note" id="cx-degraded-reason"></div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="cx-grid">
|
|
104
|
+
<div class="cx-card"><span class="cx-k" data-zh="索引高度 (lastSyncedBlock)" data-en="index height (lastSyncedBlock)">索引高度 (lastSyncedBlock)</span><span class="cx-v" id="cx-height">—</span></div>
|
|
105
|
+
<div class="cx-card"><span class="cx-k" data-zh="快照 head 区块" data-en="snapshot head block">快照 head 区块</span><span class="cx-v" id="cx-head">—</span></div>
|
|
106
|
+
<div class="cx-card"><span class="cx-k" data-zh="最后同步时间" data-en="last synced at">最后同步时间</span><span class="cx-v-sm" id="cx-lastsync">—</span></div>
|
|
107
|
+
<div class="cx-card"><span class="cx-k" data-zh="链 / 网络" data-en="chain / network">链 / 网络</span><span class="cx-v-sm" id="cx-chain">—</span></div>
|
|
108
|
+
<div class="cx-card"><span class="cx-k" data-zh="托管合约 (短写)" data-en="escrow contract (short)">托管合约 (短写)</span><span class="cx-v-sm" id="cx-escrow">—</span></div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="cx-sec-title"><span data-zh="事件总数与分类" data-en="total events & categories">事件总数与分类</span></div>
|
|
112
|
+
<div class="cx-grid">
|
|
113
|
+
<div class="cx-card"><span class="cx-k" data-zh="事件总数 (含被回退)" data-en="total events (incl. reverted)">事件总数 (含被回退)</span><span class="cx-v" id="cx-entries">—</span></div>
|
|
114
|
+
<div class="cx-card"><span class="cx-k" data-zh="任务数 (唯一 taskKey)" data-en="tasks (unique taskKey)">任务数 (唯一 taskKey)</span><span class="cx-v" id="cx-tasks">—</span></div>
|
|
115
|
+
<div class="cx-card"><span class="cx-k" data-zh="被回退 (suspect)" data-en="reverted (suspect)">被回退 (suspect)</span><span class="cx-v" id="cx-suspects">—</span></div>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="cx-cats">
|
|
118
|
+
<span class="cx-cat"><span data-zh="托管创建" data-en="escrow created">托管创建</span> <b id="cx-created">—</b></span>
|
|
119
|
+
<span class="cx-cat"><span data-zh="提交证明" data-en="proof submitted">提交证明</span> <b id="cx-proof">—</b></span>
|
|
120
|
+
<span class="cx-cat"><span data-zh="已放款" data-en="released">已放款</span> <b id="cx-released">—</b></span>
|
|
121
|
+
<span class="cx-cat"><span data-zh="已退款" data-en="refunded">已退款</span> <b id="cx-refunded">—</b></span>
|
|
122
|
+
<span class="cx-cat"><span data-zh="争议" data-en="disputed">争议</span> <b id="cx-disputed">—</b></span>
|
|
123
|
+
<span class="cx-cat"><span data-zh="已过期" data-en="expired">已过期</span> <b id="cx-expired">—</b></span>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="cx-sec-title"><span data-zh="最终性三档 (1/12 确认门槛)" data-en="finality tiers (1/12 confirmations gate)">最终性三档 (1/12 确认门槛)</span></div>
|
|
127
|
+
<div class="cx-legend">
|
|
128
|
+
<span class="cx-final"><span data-zh="观测中 observed" data-en="observed">观测中 observed</span> <b id="cx-final-observed">—</b></span>
|
|
129
|
+
<span class="cx-final"><span data-zh="已确认 confirmed" data-en="confirmed">已确认 confirmed</span> <b id="cx-final-confirmed">—</b></span>
|
|
130
|
+
<span class="cx-final"><span data-zh="已最终 finalized" data-en="finalized">已最终 finalized</span> <b id="cx-final-finalized">—</b></span>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div class="cx-sec-title">
|
|
134
|
+
<span data-zh="交易时间线" data-en="transaction timeline">交易时间线</span>
|
|
135
|
+
<span class="cx-hint" data-zh="按 (区块号, logIndex) 升序 · cursor 增量加载" data-en="sorted by (block, logIndex) · cursor incremental loading">按 (区块号, logIndex) 升序 · cursor 增量加载</span>
|
|
136
|
+
<span class="cx-hint" id="cx-timeline-count" role="status" aria-live="polite"></span>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="cx-bar">
|
|
139
|
+
<span class="cx-hint" id="cx-timeline-lede" data-zh="首屏只拉一页; 向下点「加载更多」按 cursor 取下一段 (不整页重拉)" data-en="First paint loads one page; “load more” fetches the next segment by cursor (no full reload)">首屏只拉一页; 向下点「加载更多」按 cursor 取下一段 (不整页重拉)</span>
|
|
140
|
+
<span class="cx-hint" id="cx-remaining"></span>
|
|
141
|
+
</div>
|
|
142
|
+
<ol class="cx-list" id="cx-timeline-body" aria-label="chain timeline"></ol>
|
|
143
|
+
<div class="cx-bar">
|
|
144
|
+
<button type="button" id="cx-more" data-zh="加载更多 (按 cursor 取下一段)" data-en="load more (next cursor)">加载更多 (按 cursor 取下一段)</button>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="cx-sec-title">
|
|
148
|
+
<span data-zh="单个 escrow 时间线" data-en="single escrow timeline">单个 escrow 时间线</span>
|
|
149
|
+
<span class="cx-hint" id="cx-task-title" role="status" aria-live="polite">(未选择)</span>
|
|
150
|
+
</div>
|
|
151
|
+
<form class="cx-form" id="cx-task-form">
|
|
152
|
+
<input type="text" id="cx-task-input" spellcheck="false" autocomplete="off" placeholder="0x… (64 hex taskKey)" aria-label="taskKey">
|
|
153
|
+
<button type="submit" data-zh="查这个 taskKey" data-en="look up this taskKey">查这个 taskKey</button>
|
|
154
|
+
</form>
|
|
155
|
+
<div class="cx-note" data-zh="create → proof → release / refund / expire / dispute (按事件推出的状态)" data-en="create → proof → release / refund / expire / dispute (state derived from events)">create → proof → release / refund / expire / dispute (按事件推出的状态)</div>
|
|
156
|
+
<ol class="cx-list" id="cx-task-body" aria-label="escrow timeline"></ol>
|
|
157
|
+
|
|
158
|
+
<div class="cx-note" data-zh="隐私: 地址/哈希一律短写 (0x12ab…9f0e); 页面不展示完整 DID / peerId / IP / 钱包地址。" data-en="Privacy: addresses/hashes are always shortened (0x12ab…9f0e); the page never shows full DID / peerId / IP / wallet addresses.">隐私: 地址/哈希一律短写 (0x12ab…9f0e); 页面不展示完整 DID / peerId / IP / 钱包地址。</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<script type="module" src="./chain-explorer.js"></script>
|
|
162
|
+
</body>
|
|
163
|
+
</html>
|
package/dist/web/index.html
CHANGED
|
@@ -108,6 +108,11 @@
|
|
|
108
108
|
</div>
|
|
109
109
|
|
|
110
110
|
<div class="sidebar-footer">
|
|
111
|
+
<!-- 2026-09-22 (P7): 链上浏览器入口 (只读索引页面, 独立页 /explorer) -->
|
|
112
|
+
<a class="section-action" id="chain-explorer-link" href="/explorer" title="链上浏览器 (只读) — 索引高度 / 事件统计 / 交易时间线">
|
|
113
|
+
<span>◧</span>
|
|
114
|
+
<span data-zh="链上浏览器" data-en="Chain explorer">链上浏览器</span>
|
|
115
|
+
</a>
|
|
111
116
|
<div class="user-avatar" id="user-avatar" title="点击查看 DID 身份">
|
|
112
117
|
<span class="avatar-letter" id="avatar-letter">?</span>
|
|
113
118
|
</div>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routes-chain-index.ts — 链上索引的**只读**HTTP 路由 (P5, ③)
|
|
3
|
+
* =========================================================================
|
|
4
|
+
* 刻意独立成文件, **不碰 src/web/server.ts**: 挂载方法见文件末尾注释。
|
|
5
|
+
* 本模块只暴露**读**接口 (读已落盘索引), 全都不发 RPC、不写盘、不签名:
|
|
6
|
+
*
|
|
7
|
+
* GET /api/chain/index/status
|
|
8
|
+
* → 当前索引高度 (lastSyncedBlock) / 最后同步时间 / head / 确认数门槛 / 页大小
|
|
9
|
+
* GET /api/chain/index/stats
|
|
10
|
+
* → 全量统计: tasks / created / proofSubmitted / released / refunded / disputed / expired
|
|
11
|
+
* + byFinality (observed|confirmed|finalized) + suspects (被回退的条数)
|
|
12
|
+
* GET /api/chain/index/timeline?taskKey=0x…(64hex)
|
|
13
|
+
* → 该 escrow 的事件时间线 (含块号/块哈希/logIndex/解码 args/确认数/finality/suspect)
|
|
14
|
+
* GET /api/chain/index/events?blockNumber=..&logIndex=..&limit=..
|
|
15
|
+
* → 按 cursor 增量拉取 (严格大于 cursor 的 (blockNumber, logIndex)); 无 cursor = 从头
|
|
16
|
+
*
|
|
17
|
+
* 挂载 (在 src/web/server.ts 里加两行即可, 本文件不改它):
|
|
18
|
+
* import { registerChainIndexRoutes } from './routes-chain-index.js';
|
|
19
|
+
* registerChainIndexRoutes(app); // app: express.Express (或等价 router 宿主)
|
|
20
|
+
*
|
|
21
|
+
* 挂载点可选前缀: registerChainIndexRoutes(app, { basePath: '/api/chain/index' })
|
|
22
|
+
* 读 SQLite/文件直接读 ~/.bolloon/chain/index.json; 换 HOME 用 opts.home (测试隔离)。
|
|
23
|
+
* 若还想在网页上触发同步: registerChainIndexRoutes(app, { allowSync: true })
|
|
24
|
+
* → 多一个 POST /api/chain/index/sync (body: { fromBlock?: number })。默认关 —— 写操作不默认开放。
|
|
25
|
+
*/
|
|
26
|
+
import { getIndexStatus, getIndexStats, getEscrowTimeline, fetchIndexSince, } from '../agents/chain/chain-index-query.js';
|
|
27
|
+
const isTaskKey = (v) => typeof v === 'string' && /^0x[0-9a-fA-F]{64}$/.test(v);
|
|
28
|
+
export function registerChainIndexRoutes(app, opts = {}) {
|
|
29
|
+
const base = opts.basePath || '/api/chain/index';
|
|
30
|
+
const home = opts.home;
|
|
31
|
+
// ① 当前索引高度 + 最后同步时间
|
|
32
|
+
app.get(`${base}/status`, (_req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
res.json({ ok: true, status: getIndexStatus({ home }) });
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
res.status(500).json({ ok: false, error: String(e?.message || e) });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// ② 全量统计
|
|
41
|
+
app.get(`${base}/stats`, (_req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
res.json({ ok: true, stats: getIndexStats({ home }) });
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
res.status(500).json({ ok: false, error: String(e?.message || e) });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// ③ 按 taskKey 查 escrow 时间线
|
|
50
|
+
app.get(`${base}/timeline`, (req, res) => {
|
|
51
|
+
const taskKey = String(req.query?.taskKey || '');
|
|
52
|
+
if (!isTaskKey(taskKey)) {
|
|
53
|
+
return res.status(400).json({ ok: false, error: 'taskKey 必须是 0x + 64 hex' });
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const tl = getEscrowTimeline(taskKey, { home });
|
|
57
|
+
res.json({ ok: true, timeline: tl });
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
res.status(500).json({ ok: false, error: String(e?.message || e) });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
// ④ 按 cursor 增量拉取
|
|
64
|
+
app.get(`${base}/events`, (req, res) => {
|
|
65
|
+
const q = req.query || {};
|
|
66
|
+
const hasCursor = q.blockNumber !== undefined && q.blockNumber !== '';
|
|
67
|
+
let cursor = null;
|
|
68
|
+
if (hasCursor) {
|
|
69
|
+
const bn = Number(q.blockNumber);
|
|
70
|
+
const li = q.logIndex === undefined || q.logIndex === '' ? 0 : Number(q.logIndex);
|
|
71
|
+
if (!Number.isInteger(bn) || bn < 0 || !Number.isInteger(li) || li < 0) {
|
|
72
|
+
return res.status(400).json({ ok: false, error: 'cursor 非法: 需要 blockNumber>=0, logIndex>=0' });
|
|
73
|
+
}
|
|
74
|
+
cursor = { blockNumber: bn, logIndex: li };
|
|
75
|
+
}
|
|
76
|
+
const limit = q.limit === undefined || q.limit === '' ? 200 : Number(q.limit);
|
|
77
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
78
|
+
return res.status(400).json({ ok: false, error: 'limit 必须 >= 1 的整数' });
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
res.json({ ok: true, page: fetchIndexSince(cursor, { home, limit }) });
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
res.status(500).json({ ok: false, error: String(e?.message || e) });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// 可选: 触发一次同步 (显式 opt-in)
|
|
88
|
+
if (opts.allowSync) {
|
|
89
|
+
app.post(`${base}/sync`, async (req, res) => {
|
|
90
|
+
if (!opts.sync)
|
|
91
|
+
return res.status(501).json({ ok: false, error: '未注入 sync 实现' });
|
|
92
|
+
const fromBlockRaw = req.body?.fromBlock;
|
|
93
|
+
let fromBlock;
|
|
94
|
+
if (fromBlockRaw !== undefined && fromBlockRaw !== null && fromBlockRaw !== '') {
|
|
95
|
+
const n = Number(fromBlockRaw);
|
|
96
|
+
if (!Number.isInteger(n) || n < 0)
|
|
97
|
+
return res.status(400).json({ ok: false, error: 'fromBlock 必须 >= 0 的整数' });
|
|
98
|
+
fromBlock = n;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
res.json({ ok: true, result: await opts.sync(fromBlock) });
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
res.status(500).json({ ok: false, error: String(e?.message || e) });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routes-onchain-trade.ts — 链上任务交易闭环的**只读**HTTP 路由 (P4, ②③)
|
|
3
|
+
* =========================================================================
|
|
4
|
+
* 刻意独立成文件, **不碰 src/web/server.ts** (另一个并行任务在动它)。
|
|
5
|
+
* 只暴露**读**接口 —— 全都不发交易、不签名、不改链、不写盘:
|
|
6
|
+
*
|
|
7
|
+
* GET /api/chain/trade/state?taskKey=0x…(64hex) | taskId=…
|
|
8
|
+
* → 该任务在 chain-state.json 里的全部链上记录 (方法/状态/确认数/是否可疑/结果哈希)
|
|
9
|
+
* GET /api/chain/trade/recovery?taskKey=… | taskId=…&pendingResultDigest=sha256:…
|
|
10
|
+
* → ★ 重启恢复决策: nextAction / mustNotRepay / verified / 为什么 (纯读盘, 不联网)
|
|
11
|
+
* GET /api/chain/trade/budget?amount=0.02&taskBudget=0.05
|
|
12
|
+
* → M1 预算硬约束预检 (单任务 0.05 / 单次 0.02): 放行 / 哪一层拦下
|
|
13
|
+
*
|
|
14
|
+
* 挂载 (在 src/web/server.ts 里加两行即可, 本文件不改它):
|
|
15
|
+
* import { registerOnchainTradeRoutes } from './routes-onchain-trade.js';
|
|
16
|
+
* registerOnchainTradeRoutes(app); // app: express.Express
|
|
17
|
+
*
|
|
18
|
+
* 挂载点可选: registerOnchainTradeRoutes(app, { basePath: '/api/chain/trade' })
|
|
19
|
+
* 换 HOME 用 opts.home (测试隔离 / 多 profile)。**没有**任何写接口 ——
|
|
20
|
+
* 链上写操作必须走 CLI/任务链路 (那里有签名放行闸 + 审计), 不通过 HTTP 裸开。
|
|
21
|
+
*/
|
|
22
|
+
import * as os from 'os';
|
|
23
|
+
import { recoverOnchainTrade, checkOnchainAmount, onchainTaskKey, } from '../agents/chain/onchain-trade.js';
|
|
24
|
+
import { loadChainState, chainStatePath } from '../agents/chain/chain-state-store.js';
|
|
25
|
+
const isTaskKey = (v) => typeof v === 'string' && /^0x[0-9a-fA-F]{64}$/.test(v);
|
|
26
|
+
function homeOf(opts) {
|
|
27
|
+
return opts.home || process.env.HOME || os.homedir();
|
|
28
|
+
}
|
|
29
|
+
export function registerOnchainTradeRoutes(app, opts = {}) {
|
|
30
|
+
const base = opts.basePath || '/api/chain/trade';
|
|
31
|
+
// ① 链上记录 (按 taskKey / taskId 过滤; 不过滤 = 全部)
|
|
32
|
+
app.get(`${base}/state`, (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const home = homeOf(opts);
|
|
35
|
+
const taskKey = String(req.query?.taskKey || '');
|
|
36
|
+
const taskId = String(req.query?.taskId || '');
|
|
37
|
+
if (taskKey && !isTaskKey(taskKey))
|
|
38
|
+
return res.status(400).json({ ok: false, error: 'taskKey 必须是 0x + 64 hex' });
|
|
39
|
+
const want = (taskKey || (taskId ? onchainTaskKey(taskId) : '')).toLowerCase();
|
|
40
|
+
const all = Object.values(loadChainState(home).records);
|
|
41
|
+
const records = want ? all.filter((r) => String(r.taskKey).toLowerCase() === want) : all;
|
|
42
|
+
res.json({
|
|
43
|
+
ok: true,
|
|
44
|
+
statePath: chainStatePath(home),
|
|
45
|
+
taskKey: want || null,
|
|
46
|
+
count: records.length,
|
|
47
|
+
records,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
res.status(500).json({ ok: false, error: String(e?.message || e) });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// ② 重启恢复决策 (纯读盘; 不联网 → 不会把"读不到 RPC"当成"没付款")
|
|
55
|
+
app.get(`${base}/recovery`, (req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
const home = homeOf(opts);
|
|
58
|
+
const q = req.query || {};
|
|
59
|
+
const taskKey = String(q.taskKey || '');
|
|
60
|
+
const taskId = String(q.taskId || '');
|
|
61
|
+
if (taskKey && !isTaskKey(taskKey))
|
|
62
|
+
return res.status(400).json({ ok: false, error: 'taskKey 必须是 0x + 64 hex' });
|
|
63
|
+
if (!taskKey && !taskId)
|
|
64
|
+
return res.status(400).json({ ok: false, error: '要 taskKey 或 taskId' });
|
|
65
|
+
const recovery = recoverOnchainTrade({
|
|
66
|
+
home,
|
|
67
|
+
...(taskKey ? { taskKey } : {}),
|
|
68
|
+
...(taskId ? { taskId } : {}),
|
|
69
|
+
...(q.pendingResultDigest ? { pendingResultDigest: String(q.pendingResultDigest) } : {}),
|
|
70
|
+
});
|
|
71
|
+
res.json({ ok: true, recovery });
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
res.status(500).json({ ok: false, error: String(e?.message || e) });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
// ③ M1 预算硬约束预检 (付款前必须能看得到; 纯函数, 不碰钱)
|
|
78
|
+
app.get(`${base}/budget`, (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
const q = req.query || {};
|
|
81
|
+
const amount = Number(q.amount);
|
|
82
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
83
|
+
return res.status(400).json({ ok: false, error: 'amount 必须是正数 (USDC)' });
|
|
84
|
+
const decimals = Number.isFinite(Number(q.decimals)) ? Number(q.decimals) : 6;
|
|
85
|
+
const atomic = BigInt(Math.round(amount * 10 ** decimals));
|
|
86
|
+
const d = checkOnchainAmount({
|
|
87
|
+
amountAtomic: atomic, decimals,
|
|
88
|
+
budget: {
|
|
89
|
+
...(q.taskBudget !== undefined ? { taskBudget: Number(q.taskBudget) } : {}),
|
|
90
|
+
...(q.perPurchase !== undefined ? { perPurchase: Number(q.perPurchase) } : {}),
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
res.json({ ok: true, allowed: d.ok, layer: d.layer ?? null, reason: d.reason, amountUsdc: d.amountUsdc, budget: d.budget, why: d.why });
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
res.status(500).json({ ok: false, error: String(e?.message || e) });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
package/dist/web/server.js
CHANGED
|
@@ -17,8 +17,11 @@ import { registerTaskRoutes } from './routes-tasks.js';
|
|
|
17
17
|
import { registerContactRoutes } from './routes-contacts.js';
|
|
18
18
|
// 2026-09-13: 微支付信息服务 (x402) 路由
|
|
19
19
|
import { registerX402InfoRoutes } from './routes-x402-info.js';
|
|
20
|
+
// 2026-09-22: P5 链上索引 (只读: status/stats/timeline/events 增量) 路由
|
|
21
|
+
import { registerChainIndexRoutes } from './routes-chain-index.js';
|
|
20
22
|
import { loadPeerTier, recordInteraction, tierLabel } from '../social/dunbar-tier.js';
|
|
21
23
|
import { registerHearthRoutes } from './routes-hearth.js';
|
|
24
|
+
import { registerOnchainTradeRoutes } from './routes-onchain-trade.js';
|
|
22
25
|
import { loadOrCreateAgentIdentity } from '../agents/agent-identity.js';
|
|
23
26
|
// 2026-07-06: 类型抽到 ./server-types.ts (channel / session / task / sse client / iroh info / paths)
|
|
24
27
|
import { SESSION_CACHE_PATH, SHARED_SESSION_PATH, IPFS_ENDPOINT, } from './server-types.js';
|
|
@@ -2923,6 +2926,15 @@ ${goalDesc}
|
|
|
2923
2926
|
}
|
|
2924
2927
|
app.get('/', serveStaticHtml('index.html', 'index.html not found; please run `npm run build:web`', 'index'));
|
|
2925
2928
|
app.get('/api-config', serveStaticHtml('api-config.html', 'api-config.html not found; please run `npm run build:web`', 'api-config'));
|
|
2929
|
+
// 2026-09-22 (P7): 链上浏览器页面 (只读索引的网页入口)
|
|
2930
|
+
// /explorer 与 /chain 两个别名都指向同一个静态页 (explorer.html + chain-explorer.js);
|
|
2931
|
+
// 页面只调 /api/chain/index/*, 不碰任何写接口。
|
|
2932
|
+
{
|
|
2933
|
+
const serveExplorer = serveStaticHtml('explorer.html', 'explorer.html not found; please run `npm run build:web`', 'explorer');
|
|
2934
|
+
app.get('/explorer', serveExplorer);
|
|
2935
|
+
app.get('/chain', serveExplorer);
|
|
2936
|
+
registerChainIndexRoutes(app);
|
|
2937
|
+
}
|
|
2926
2938
|
// 2026-07-01 (v0.2.5): 输入验证 + 健康检查 API
|
|
2927
2939
|
// - POST /api/validate-input: 前端发送前预校验, 避免后端 reject
|
|
2928
2940
|
// - GET /api/health: liveness probe + validators 清单
|
|
@@ -6804,6 +6816,7 @@ fetchState();
|
|
|
6804
6816
|
registerContactRoutes(app, {});
|
|
6805
6817
|
// 2026-09-13: 微支付信息服务 (x402) — 发布 / 402 收款 / 买方代付 / 验真
|
|
6806
6818
|
registerX402InfoRoutes(app);
|
|
6819
|
+
registerOnchainTradeRoutes(app);
|
|
6807
6820
|
// 2026-07-06: LLM/Video/Audio 配置路由抽到 ./routes-llm-config.ts
|
|
6808
6821
|
registerLlmConfigRoutes(app);
|
|
6809
6822
|
// 2026-07-22: 外部编码智能体 (codex/claude-code/opencode/openclaw/hermes/实验 API)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bolloon/bolloon-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.32",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
|
|
6
6
|
"main": "dist/cli-entry.js",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"@x402/fetch": "^2.25.0",
|
|
92
92
|
"b4a": "^1.8.1",
|
|
93
93
|
"dotenv": "^17.4.2",
|
|
94
|
+
"ethers": "^6.17.0",
|
|
94
95
|
"express": "^5.2.1",
|
|
95
96
|
"helia": "^7.1.10",
|
|
96
97
|
"ink": "^7.1.1",
|
package/scripts/build-web.ts
CHANGED
|
@@ -14,7 +14,7 @@ async function main() {
|
|
|
14
14
|
|
|
15
15
|
// 重要: 不能 rm -rf 整个 dist/web/, 否则会删掉 build:main 编译出来的
|
|
16
16
|
// dist/web/server.js. 只清理 web 静态资源, 保留 server.js.
|
|
17
|
-
for (const f of ['index.html', 'api-config.html', 'style.css', 'client.js', 'mobile.html', 'mobile.css', 'mobile.js', 'mobile-core.js', 'components']) {
|
|
17
|
+
for (const f of ['index.html', 'api-config.html', 'explorer.html', 'style.css', 'client.js', 'chain-explorer.js', 'mobile.html', 'mobile.css', 'mobile.js', 'mobile-core.js', 'components']) {
|
|
18
18
|
await fs.rm(path.join(DIST_WEB, f), { recursive: true, force: true });
|
|
19
19
|
}
|
|
20
20
|
await fs.mkdir(DIST_WEB, { recursive: true });
|
|
@@ -119,9 +119,28 @@ async function main() {
|
|
|
119
119
|
loader: { '.tsx': 'tsx' },
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
// 2026-09-22 (P7): 编译链上浏览器页面 (chain-explorer.ts → dist/web/chain-explorer.js)
|
|
123
|
+
// 独立入口, 与主界面 client.ts 完全隔离 (只吃 P5 的只读路由);
|
|
124
|
+
// esm + bundle: explorer.html 以 <script type="module"> 加载; 模块内目前不 import 任何东西,
|
|
125
|
+
// 将来真加了 import 也不会踩 client.ts 那个 require 的坑。
|
|
126
|
+
console.log('[build-web] 编译 chain-explorer.ts...');
|
|
127
|
+
await esbuild.build({
|
|
128
|
+
entryPoints: [path.join(ROOT, 'src/web/chain-explorer.ts')],
|
|
129
|
+
outfile: path.join(DIST_WEB, 'chain-explorer.js'),
|
|
130
|
+
format: 'esm',
|
|
131
|
+
target: 'es2022',
|
|
132
|
+
platform: 'browser',
|
|
133
|
+
minify: false,
|
|
134
|
+
bundle: true,
|
|
135
|
+
// charset utf8: 产物里保留中文文案与 '…' 字面量 (默认 ascii 会把非 ASCII 全转义成 \uXXXX,
|
|
136
|
+
// 功能一样但人读不了, 也让"短写省略号存在"这类断言没法从产物里核)
|
|
137
|
+
charset: 'utf8',
|
|
138
|
+
});
|
|
139
|
+
|
|
122
140
|
// 复制静态文件
|
|
123
141
|
console.log('[build-web] 复制静态文件...');
|
|
124
142
|
await fs.copyFile(path.join(ROOT, 'src/web/index.html'), path.join(DIST_WEB, 'index.html'));
|
|
143
|
+
await fs.copyFile(path.join(ROOT, 'src/web/explorer.html'), path.join(DIST_WEB, 'explorer.html'));
|
|
125
144
|
await fs.copyFile(path.join(ROOT, 'src/web/api-config.html'), path.join(DIST_WEB, 'api-config.html'));
|
|
126
145
|
await fs.copyFile(path.join(ROOT, 'src/web/style.css'), path.join(DIST_WEB, 'style.css'));
|
|
127
146
|
// 2026-08-12: 手机端 UI (微信风格, Capacitor webview 加载) — 纯静态复制
|