@bolloon/bolloon-agent 0.4.32 → 0.5.0
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/explorer.js +52 -0
- package/dist/agents/chain/index.js +2 -0
- package/dist/agents/chain/onchain-trade.js +15 -1
- package/dist/agents/contacts/chain.js +27 -7
- package/dist/agents/execution-supervisor.js +493 -35
- package/dist/agents/external-events.js +35 -6
- package/dist/agents/gateway-group.js +100 -12
- package/dist/agents/goal-criteria.js +10 -3
- package/dist/agents/goal-flywheel/block-executor.js +346 -0
- package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
- package/dist/agents/goal-flywheel/goal-change.js +754 -0
- package/dist/agents/goal-flywheel/index.js +26 -0
- package/dist/agents/goal-flywheel/memory-layers.js +227 -0
- package/dist/agents/goal-flywheel/run-closure.js +809 -0
- package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
- package/dist/agents/goal-flywheel/types.js +329 -0
- package/dist/agents/goal-flywheel/wiring/change.js +288 -0
- package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
- package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
- package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
- package/dist/agents/goal-flywheel/wiring/index.js +26 -0
- package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
- package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
- package/dist/agents/goal-flywheel/work-contract.js +478 -0
- package/dist/agents/goal-flywheel/work-monitor.js +488 -0
- package/dist/agents/goal-flywheel-wiring.js +1870 -0
- package/dist/agents/goal-state-reducer.js +406 -0
- package/dist/agents/goal-store.js +35 -1
- package/dist/agents/mobile-flywheel-view.js +144 -0
- package/dist/agents/mobile-task-actions-verify.js +74 -0
- package/dist/agents/mobile-task-actions.js +178 -0
- package/dist/agents/mobile-task-views.js +199 -0
- package/dist/agents/network-pulse.js +525 -27
- package/dist/agents/pi-sdk.js +75 -20
- package/dist/agents/run-store.js +18 -5
- package/dist/agents/skill-readiness.js +52 -10
- package/dist/agents/subagent-manager.js +209 -3
- package/dist/agents/task/task-onchain-runner.js +19 -1
- package/dist/agents/task/task-runner.js +36 -6
- package/dist/agents/task-board.js +702 -0
- package/dist/agents/task-group.js +524 -0
- package/dist/agents/task-public-text.js +89 -0
- package/dist/cli/commands/index.js +3 -0
- package/dist/cli/commands/tasks.js +977 -8
- package/dist/cli/identity-command.js +177 -0
- package/dist/cli/protocol-envelope.js +4 -0
- package/dist/cli/setup-wizard.js +71 -0
- package/dist/cli/update-commands.js +150 -24
- package/dist/cli-entry.js +12 -2
- package/dist/electron-build/agents/chain/explorer.js +59 -0
- package/dist/electron-build/agents/chain/explorer.js.map +1 -0
- package/dist/electron-build/agents/contacts/chain.js +26 -6
- package/dist/electron-build/agents/contacts/chain.js.map +1 -1
- package/dist/electron-build/agents/execution-supervisor.js +492 -34
- package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
- package/dist/electron-build/agents/external-events.js +34 -5
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-criteria.js +10 -3
- package/dist/electron-build/agents/goal-criteria.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
- package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
- package/dist/electron-build/agents/goal-state-reducer.js +414 -0
- package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
- package/dist/electron-build/agents/goal-store.js +35 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/local-signer.js +101 -0
- package/dist/electron-build/agents/local-signer.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +531 -28
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +74 -19
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +20 -5
- package/dist/electron-build/agents/run-store.js.map +1 -1
- package/dist/electron-build/agents/skill-readiness.js +52 -9
- package/dist/electron-build/agents/skill-readiness.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +35 -5
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/task-board.js +761 -0
- package/dist/electron-build/agents/task-board.js.map +1 -0
- package/dist/electron-build/agents/task-contract.js +382 -0
- package/dist/electron-build/agents/task-contract.js.map +1 -0
- package/dist/electron-build/orbitdb/cid-database.js +83 -17
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/utils/dual-source.js +471 -0
- package/dist/electron-build/utils/dual-source.js.map +1 -0
- package/dist/electron-build/utils/update-manager.js +331 -57
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +16 -1
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-info.js +82 -2
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +103 -12
- package/dist/ios/chain-explorer.js +617 -0
- package/dist/ios/explorer.html +163 -0
- package/dist/ios/index.html +96 -0
- package/dist/ios/mobile-agent.js +2 -2
- package/dist/ios/mobile-contacts.js +329 -0
- package/dist/ios/mobile-core.js +1379 -226
- package/dist/ios/mobile-privacy.js +185 -0
- package/dist/ios/mobile.css +19 -0
- package/dist/ios/mobile.html +96 -0
- package/dist/ios/mobile.js +706 -7
- package/dist/ios/routes-chain-index.js +108 -0
- package/dist/ios/routes-contacts.js +420 -0
- package/dist/ios/routes-onchain-trade.js +99 -0
- package/dist/ios/routes-x402-info.js +1 -0
- package/dist/ios/server.js +762 -0
- package/dist/ios/task-frame-server.js +211 -0
- package/dist/orbitdb/cid-database.js +81 -16
- package/dist/orbitdb/ipfs-node.js +31 -2
- package/dist/utils/dual-source.js +426 -0
- package/dist/utils/update-cli.js +16 -21
- package/dist/utils/update-health.js +35 -5
- package/dist/utils/update-manager.js +329 -57
- package/dist/utils/update-state.js +16 -1
- package/dist/utils/version-info.js +75 -1
- package/dist/web/mobile-contacts.js +44 -11
- package/dist/web/mobile-core.js +1197 -513
- package/dist/web/mobile-tasks.js +339 -0
- package/dist/web/mobile.css +19 -0
- package/dist/web/mobile.html +64 -0
- package/dist/web/mobile.js +531 -1
- package/dist/web/routes-mobile-tasks.js +353 -0
- package/dist/web/server.js +163 -4
- package/package.json +3 -1
|
@@ -11,10 +11,19 @@
|
|
|
11
11
|
* · 快照过期 → `stale` (不许伪装实时); 观察层不可用 → `unavailable`
|
|
12
12
|
* · 类别小于隐私阈值 → 合并进 other
|
|
13
13
|
* · 事件数 / 时间窗 / capability 数都有上限
|
|
14
|
-
* · **冻结形状 `confirmed_activity`** (2026-09-22):
|
|
15
|
-
* 哪个块 · 多少确认」, 而不是只有数字 —— 数据源优先 P5 链上索引,
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* · **冻结形状 `confirmed_activity`** (2026-09-22; 2026-09-23 加链上可核验字段): 公开页面要能列出
|
|
15
|
+
* 「哪个任务 · 什么状态 · 哪个块 · 多少确认」, 而不是只有数字 —— 数据源优先 P5 链上索引, 不可用时
|
|
16
|
+
* 退回脉冲事件并用 `confirmed_activity_source` 标注; 任务标识一律 sha256 短写 (前 8 位), 绝不落
|
|
17
|
+
* taskKey / taskId 原文; 不够确认门槛的行只报 observed, 不冒充 confirmed。
|
|
18
|
+
* 2026-09-23 隐私决定**变更一处** (只放开公开链上事实, 别的一律不变): 交易哈希 (`tx_hash`) 与
|
|
19
|
+
* escrow **合约**地址 (`contract`, 只给索引/诊断) 允许出现; 页面可点的只有**交易**链接 `explorer_tx`
|
|
20
|
+
* (仅当该链有已知公网浏览器)。EOA / 钱包地址 (买方·卖方)、taskKey 原文、taskId、args 里的地址、
|
|
21
|
+
* DID、peer IP/multiaddr、私钥 —— 仍然一个都不许出现; 老字段 `tx` (sha256 短写) 保留不删。
|
|
22
|
+
* · **冻结形状 `open_tasks[]`** (2026-09-23): 公开页要能列「本节点公告板上哪些任务还没被接单」。
|
|
23
|
+
* 数据源 = `~/.bolloon/tasks/board/*.json` 里**未认领且未过期**的公告, 每行只含白名单 7 个字段:
|
|
24
|
+
* `capability` / `budget`(原子单位) / `currency` / `network` / `deadline` / `claimed`(恒 false) /
|
|
25
|
+
* `announcementId`(前 8 位)。**任务正文与它的摘要/预览、买方 DID 与公钥、认领者、公告签名
|
|
26
|
+
* 一律不进公开投影** —— 行由 `buildOpenTaskRow` 逐字段拷贝构造, 不做对象展平。
|
|
18
27
|
*
|
|
19
28
|
* 借鉴 (只借产品与工程思想, 不借 Go/Postgres/API 项目):
|
|
20
29
|
* EigenFlux 的 "注册总量 / 当前活跃 / 最近出现" 时间窗统计、服务端生成匿名活动文本、
|
|
@@ -54,7 +63,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
54
63
|
};
|
|
55
64
|
})();
|
|
56
65
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
-
exports.FORBIDDEN_PUBLIC_KEYS = exports.CHAIN_LABELS = exports.PUBLIC_MAINNET_CHAIN_ID = exports.PUBLIC_TESTNET_CHAIN_ID = exports.PULSE_EVENT_ACTIVITY = exports.CHAIN_EVENT_ACTIVITY = exports.CONFIRMED_ACTIVITY_LIMIT = exports.MAX_AGENT_SITES = exports.pulseDir = exports.NETWORK_EVENT_TYPES = exports.PULSE_LIMITS = void 0;
|
|
66
|
+
exports.PUBLIC_HEX_KEYS = exports.FORBIDDEN_PUBLIC_KEYS = exports.OPEN_TASK_KEYS = exports.TOTALS_FIELD_KEYS = exports.CHAIN_LABELS = exports.PUBLIC_MAINNET_CHAIN_ID = exports.PUBLIC_TESTNET_CHAIN_ID = exports.PULSE_EVENT_ACTIVITY = exports.CHAIN_EVENT_ACTIVITY = exports.CONFIRMED_ACTIVITY_LIMIT = exports.MAX_OPEN_TASKS = exports.MAX_AGENT_SITES = exports.pulseDir = exports.NETWORK_EVENT_TYPES = exports.PULSE_LIMITS = void 0;
|
|
67
|
+
exports.tallySignatureAudit = tallySignatureAudit;
|
|
58
68
|
exports.nodeDigest = nodeDigest;
|
|
59
69
|
exports.capabilityGroup = capabilityGroup;
|
|
60
70
|
exports.bucketOf = bucketOf;
|
|
@@ -62,6 +72,7 @@ exports.recordNetworkEvent = recordNetworkEvent;
|
|
|
62
72
|
exports.renderActivityText = renderActivityText;
|
|
63
73
|
exports.normalizeIpns = normalizeIpns;
|
|
64
74
|
exports.readAgentSites = readAgentSites;
|
|
75
|
+
exports.readOpenTasks = readOpenTasks;
|
|
65
76
|
exports.emitTradePulse = emitTradePulse;
|
|
66
77
|
exports.anonShortRef = anonShortRef;
|
|
67
78
|
exports.isoSeconds = isoSeconds;
|
|
@@ -76,6 +87,8 @@ exports.isPublicChainId = isPublicChainId;
|
|
|
76
87
|
exports.summarizeActivityRows = summarizeActivityRows;
|
|
77
88
|
exports.buildChainIdScope = buildChainIdScope;
|
|
78
89
|
exports.snapshotConsistencyIssues = snapshotConsistencyIssues;
|
|
90
|
+
exports.totalsScopeIssues = totalsScopeIssues;
|
|
91
|
+
exports.openTasksIssues = openTasksIssues;
|
|
79
92
|
exports.computeSnapshot = computeSnapshot;
|
|
80
93
|
exports.getNetworkPulse = getNetworkPulse;
|
|
81
94
|
exports.snapshotStatus = snapshotStatus;
|
|
@@ -84,12 +97,18 @@ exports.lastSnapshotSignError = lastSnapshotSignError;
|
|
|
84
97
|
exports.signSnapshot = signSnapshot;
|
|
85
98
|
exports.verifySnapshotSignature = verifySnapshotSignature;
|
|
86
99
|
exports.assertNoPrivateFields = assertNoPrivateFields;
|
|
100
|
+
exports.auditPublicHexLeaks = auditPublicHexLeaks;
|
|
87
101
|
const fs = __importStar(require("fs"));
|
|
88
102
|
const os = __importStar(require("os"));
|
|
89
103
|
const path = __importStar(require("path"));
|
|
90
104
|
const crypto = __importStar(require("crypto"));
|
|
91
105
|
// 确认数门槛来自 chain-config (单一实现; 默认 confirmed=1 / finalized=12) —— 只借常量, 不借 RPC
|
|
92
106
|
const chain_config_js_1 = require("./chain/chain-config.js");
|
|
107
|
+
// 区块浏览器映射 (chainId → 公网浏览器); 认不出的链没有链接 —— 宁缺勿错, 不编死链
|
|
108
|
+
const explorer_js_1 = require("./chain/explorer.js");
|
|
109
|
+
// 「待接单任务」的数据源 = 本机公告板目录 (~/.bolloon/tasks/board)。只借**路径常量**,
|
|
110
|
+
// 公告的解析/发布/接单一律留在 task-board.ts (这里不做第二套实现)。
|
|
111
|
+
const task_board_js_1 = require("./task-board.js");
|
|
93
112
|
// ── 常量 (上限 + 时间边界) ─────────────────────────────────────────────────
|
|
94
113
|
exports.PULSE_LIMITS = {
|
|
95
114
|
/** 最多留多少条事件 (超出丢最旧) */
|
|
@@ -121,6 +140,8 @@ exports.NETWORK_EVENT_TYPES = [
|
|
|
121
140
|
// 经济事件 (P6 扩展; 原五类保持兼容, 老节点发来的事件仍被接受)
|
|
122
141
|
'task_posted', 'task_accepted', 'task_completed', 'trade_settled', 'trade_verified',
|
|
123
142
|
'wallet_signed',
|
|
143
|
+
// 2026-09-23 (C1): 任务对外发布 (公告板 publish) —— 只记"有节点公告了一个待接单任务", 正文/ID 原文不落盘
|
|
144
|
+
'task_announced',
|
|
124
145
|
];
|
|
125
146
|
// ── 存储 ────────────────────────────────────────────────────────────────────
|
|
126
147
|
const home = (h) => h || process.env.HOME || os.homedir() || '/tmp';
|
|
@@ -153,7 +174,34 @@ function writeEvents(list, h) {
|
|
|
153
174
|
/* 统计失败绝不影响主路径 */
|
|
154
175
|
}
|
|
155
176
|
}
|
|
156
|
-
|
|
177
|
+
function tallySignatureAudit(h, opts = {}) {
|
|
178
|
+
const now = opts.now ?? Date.now();
|
|
179
|
+
const windowMs = opts.windowMs ?? exports.PULSE_LIMITS.windowMs;
|
|
180
|
+
const file = path.join(home(h), '.bolloon', 'wallet-signatures.jsonl');
|
|
181
|
+
if (!fs.existsSync(file))
|
|
182
|
+
return { available: false, count: 0, window_ms: windowMs };
|
|
183
|
+
let count = 0;
|
|
184
|
+
try {
|
|
185
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
186
|
+
if (!line.trim())
|
|
187
|
+
continue;
|
|
188
|
+
let row = null;
|
|
189
|
+
try {
|
|
190
|
+
row = JSON.parse(line);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
continue;
|
|
194
|
+
} // 坏行跳过, 不让一行坏数据毁掉整个计数
|
|
195
|
+
const at = Number(row?.at);
|
|
196
|
+
if (Number.isFinite(at) && at >= now - windowMs)
|
|
197
|
+
count += 1;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return { available: false, count: 0, window_ms: windowMs };
|
|
202
|
+
}
|
|
203
|
+
return { available: true, count, window_ms: windowMs };
|
|
204
|
+
}
|
|
157
205
|
/** 节点摘要: sha256(值) 前 16 位。不可逆, 不暴露原值。 */
|
|
158
206
|
function nodeDigest(value) {
|
|
159
207
|
return crypto.createHash('sha256').update(`bolloon-pulse|${String(value)}`).digest('hex').slice(0, 16);
|
|
@@ -233,6 +281,7 @@ function renderActivityText(kind) {
|
|
|
233
281
|
case 'capability_announced': return { zh: '有节点公开声明了新能力', en: 'A node announced a capability' };
|
|
234
282
|
case 'peer_connected': return { zh: '两个节点建立了连接', en: 'Two nodes connected' };
|
|
235
283
|
case 'delegation_completed': return { zh: '一次能力委派完成', en: 'A delegation completed' };
|
|
284
|
+
case 'task_announced': return { zh: '有节点公告了一个待接单任务', en: 'A node announced an open task' };
|
|
236
285
|
default: return { zh: '网络有活动', en: 'Network activity' };
|
|
237
286
|
}
|
|
238
287
|
}
|
|
@@ -282,6 +331,82 @@ function readAgentSites(h) {
|
|
|
282
331
|
return [];
|
|
283
332
|
}
|
|
284
333
|
}
|
|
334
|
+
/** 上限: 公开页最多列几条待接单任务 (超出按最近截止排序取前 N) */
|
|
335
|
+
exports.MAX_OPEN_TASKS = 25;
|
|
336
|
+
/** 公开页只列这条公告的这 7 个字段; 其余(正文/买方/摘要/签名)一律不出这个函数 */
|
|
337
|
+
function buildOpenTaskRow(a) {
|
|
338
|
+
const capability = String(a?.capability ?? '').trim();
|
|
339
|
+
const id = String(a?.announcementId ?? '').trim();
|
|
340
|
+
const deadline = Number(a?.deadline);
|
|
341
|
+
if (!capability || !id || !Number.isFinite(deadline) || deadline <= 0)
|
|
342
|
+
return null;
|
|
343
|
+
const b = a?.budget && typeof a.budget === 'object' ? a.budget : null;
|
|
344
|
+
const maxAmount = b ? String(b.maxAmount ?? '').trim() : '';
|
|
345
|
+
const currency = b ? String(b.currency ?? '').trim() : '';
|
|
346
|
+
const network = b ? String(b.network ?? '').trim() : '';
|
|
347
|
+
return {
|
|
348
|
+
capability,
|
|
349
|
+
budget: maxAmount || null,
|
|
350
|
+
currency: currency || null,
|
|
351
|
+
network: network || null,
|
|
352
|
+
deadline,
|
|
353
|
+
claimed: (Array.isArray(a?.claims) ? a.claims.length : 0) > 0,
|
|
354
|
+
announcementId: id.slice(0, 8), // 前 8 位 (按字面, 不重新格式化)
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* 读本节点公告板上**未认领且未过期**的公告, 投影成公开「待接单任务」行。
|
|
359
|
+
*
|
|
360
|
+
* 三条筛选 (缺一个都会把不该上的东西露出来):
|
|
361
|
+
* · `protocol= bolloon-task/1` + `kind= task_announcement` (同目录还有 `remote-claims.json`,
|
|
362
|
+
* 它是个数组 —— 不按 kind 判会把认领台账当公告解析)
|
|
363
|
+
* · `status === 'open'` (已认领 / 已取消 → 不是「待接单」)
|
|
364
|
+
* · `deadline > now` (过期只是不能再接单, 文件不删 → 必须在这里判掉)
|
|
365
|
+
*
|
|
366
|
+
* 读不到目录 / 坏 JSON / 缺字段 → 该条跳过 (一条都不编); 整个目录不可读 → 空数组 + 不抛。
|
|
367
|
+
*/
|
|
368
|
+
function readOpenTasks(h, now = Date.now()) {
|
|
369
|
+
const out = [];
|
|
370
|
+
try {
|
|
371
|
+
const dir = (0, task_board_js_1.boardDir)(h);
|
|
372
|
+
let names;
|
|
373
|
+
try {
|
|
374
|
+
names = fs.readdirSync(dir);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
for (const name of names.slice().sort()) {
|
|
380
|
+
if (!name.endsWith('.json') || name === `${task_board_js_1.REMOTE_CLAIMS_FILE}`)
|
|
381
|
+
continue;
|
|
382
|
+
let raw;
|
|
383
|
+
try {
|
|
384
|
+
raw = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf-8'));
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
390
|
+
continue; // 认领台账是数组 → 这里就出局
|
|
391
|
+
if (raw.protocol !== 'bolloon-task/1' || raw.kind !== 'task_announcement')
|
|
392
|
+
continue;
|
|
393
|
+
if (String(raw.status) !== 'open')
|
|
394
|
+
continue;
|
|
395
|
+
const deadline = Number(raw.deadline);
|
|
396
|
+
if (!Number.isFinite(deadline) || deadline <= now)
|
|
397
|
+
continue;
|
|
398
|
+
const row = buildOpenTaskRow(raw);
|
|
399
|
+
if (row)
|
|
400
|
+
out.push(row);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
// 稳定排序: 快到期在前 (读者最需要知道的先看); 同截止按短 id 定序 (排序必须确定, 否则快照每次都"变")
|
|
407
|
+
out.sort((x, y) => (x.deadline - y.deadline) || (x.announcementId < y.announcementId ? -1 : x.announcementId > y.announcementId ? 1 : 0));
|
|
408
|
+
return out.slice(0, exports.MAX_OPEN_TASKS);
|
|
409
|
+
}
|
|
285
410
|
/**
|
|
286
411
|
* 把**真实交易活动**投影成脉冲事件 (由交易写路径调用, fire-and-forget)。
|
|
287
412
|
* 只按事实发: 交付 → task_completed · 真验真 → trade_verified · **链上口径**结算 → trade_settled。
|
|
@@ -314,10 +439,18 @@ async function emitTradePulse(before, after, h) {
|
|
|
314
439
|
// ── 冻结形状: confirmed_activity (真实智能体任务 / 链上活动行) ────────────────
|
|
315
440
|
//
|
|
316
441
|
// 公开网页要能列出「哪个任务 · 什么状态 · 哪个块 · 多少确认」, 而不是只有数字。这块就是那批行。
|
|
317
|
-
// 形状**冻结** (
|
|
318
|
-
// · task = `sha256:` + sha256(域标签|taskKey) 的前 8
|
|
319
|
-
//
|
|
320
|
-
// ·
|
|
442
|
+
// 形状**冻结** (老字段名与取值域不许改), 内容按用途分两类:
|
|
443
|
+
// · **匿名短写** (老口径, 保留不删): task = `sha256:` + sha256(域标签|taskKey) 的前 8 位;
|
|
444
|
+
// tx = `sha256:` + sha256(域标签|txHash) 的前 8 位 —— 绝不落 taskKey / taskId 原文
|
|
445
|
+
// · **公开链上事实** (2026-09-23 新增; 同日按 leo 拍板收窄): tx_hash (真交易哈希) /
|
|
446
|
+
// explorer_tx (交易浏览器链接, 仅当该链有已知公网浏览器) —— 网页上的行因此**可核验地**跳回区块
|
|
447
|
+
// 浏览器; contract (escrow 合约地址) 仍保留在行里**供索引/诊断**, 但**不上页面、不生成合约链接**
|
|
448
|
+
// 隐私红线 (违反即失败, **只放开上面那两种 0x 字符串**):
|
|
449
|
+
// · 允许: 交易哈希 (0x+64 hex) 与 escrow 合约地址 (0x+40 hex), 且只出现在 `tx_hash`/`contract`/`explorer_tx` 这 3 个键下
|
|
450
|
+
// · 禁止: EOA / 钱包地址 (买方/卖方/payTo)、taskKey 原文、taskId、args 里的地址、DID、peerId /
|
|
451
|
+
// IP / multiaddr、私钥 —— `assertNoPrivateFields`(键名) + `auditPublicHexLeaks`(越界 0x) 兜底
|
|
452
|
+
// · 拿不到链配置里的 escrow 地址, 或这条索引记录的 address 不是它 → **不填** contract
|
|
453
|
+
// (宁缺勿错: 宁可少一个字段, 也不把可能是别人的地址写成合约)
|
|
321
454
|
// 数据源优先级: ① P5 链上索引 (真链上事实) → ② 索引不可用 → 退回脉冲事件, 并在快照里用
|
|
322
455
|
// `confirmed_activity_source` 如实标注 (chain-index / pulse-events / none)。
|
|
323
456
|
// 确认数口径走 chain-config 门槛 (默认 confirmed=1 / finalized=12): 够不着门槛的行只报
|
|
@@ -401,6 +534,8 @@ function activityLimit(limit) {
|
|
|
401
534
|
* · 不认识的事件名 / 非法 taskKey·txHash / 缺观察时间 → 跳过 (不猜、不臆造)
|
|
402
535
|
* · confirmations: 有 headBlock → head - block + 1 复算 (无 RPC, 用索引快照里的 head); 否则用记录值
|
|
403
536
|
* · finality: 按 chain-config 门槛复算 (不够 → observed)
|
|
537
|
+
* · 新增: `tx_hash` (真交易哈希) 恒给; `contract` 仅当 opts.escrowAddress 合法且 === 本条 address;
|
|
538
|
+
* `explorer_tx` 仅当该 chainId 有**已知公网浏览器** (否则键整个不存在, 不填 null)
|
|
404
539
|
* · 最新在前 (blockNumber desc, logIndex desc), 上限 25 行
|
|
405
540
|
*/
|
|
406
541
|
function buildConfirmedActivityFromIndex(entries, opts = {}) {
|
|
@@ -408,6 +543,9 @@ function buildConfirmedActivityFromIndex(entries, opts = {}) {
|
|
|
408
543
|
const limit = activityLimit(opts.limit);
|
|
409
544
|
const chainId = Number.isInteger(Number(opts.chainId)) && Number(opts.chainId) >= 0 ? Number(opts.chainId) : 0;
|
|
410
545
|
const head = Number.isInteger(Number(opts.headBlock)) && Number(opts.headBlock) >= 0 ? Number(opts.headBlock) : null;
|
|
546
|
+
// escrow 合约地址白名单 (唯一权威): 只有索引文件自己记的这个地址算数, 别的地址一律不当合约
|
|
547
|
+
const escrow = typeof opts.escrowAddress === 'string' ? opts.escrowAddress.toLowerCase() : '';
|
|
548
|
+
const escrowOk = explorer_js_1.ADDRESS_RE.test(escrow) ? escrow : '';
|
|
411
549
|
const out = [];
|
|
412
550
|
for (const e of Array.isArray(entries) ? entries : []) {
|
|
413
551
|
if (!e || e.suspect === true)
|
|
@@ -428,6 +566,12 @@ function buildConfirmedActivityFromIndex(entries, opts = {}) {
|
|
|
428
566
|
if (!at)
|
|
429
567
|
continue; // 没观察时间 → 不臆造时间, 不成行
|
|
430
568
|
const confirmations = head == null ? Math.max(0, Number(e.confirmations) || 0) : Math.max(0, head - block + 1);
|
|
569
|
+
// 合约地址: 只有「本条记录的 address 就是链配置里的 escrow 地址」才填 (宁缺勿错)
|
|
570
|
+
const entryAddr = String(e.address || '').toLowerCase();
|
|
571
|
+
const contract = escrowOk && entryAddr === escrowOk ? escrowOk : '';
|
|
572
|
+
// 浏览器链接: 该链没有已知公网浏览器 → 返回 null → 键整个不出现 (不编 href="#")
|
|
573
|
+
// 只有**交易**链接 (explorer_tx); 合约地址只在数据里 (contract), 不生成链接 (2026-09-23 收窄)
|
|
574
|
+
const explorerTx = (0, explorer_js_1.explorerTxUrl)(chainId, txHash);
|
|
431
575
|
out.push({
|
|
432
576
|
block, logIndex,
|
|
433
577
|
row: {
|
|
@@ -440,6 +584,10 @@ function buildConfirmedActivityFromIndex(entries, opts = {}) {
|
|
|
440
584
|
confirmations,
|
|
441
585
|
finality: finalityFromConfirmations(confirmations, gates),
|
|
442
586
|
at,
|
|
587
|
+
// 公开链上事实 (白名单键; 老字段 tx 已在上方保留)
|
|
588
|
+
tx_hash: txHash,
|
|
589
|
+
...(contract ? { contract } : {}),
|
|
590
|
+
...(explorerTx ? { explorer_tx: explorerTx } : {}),
|
|
443
591
|
},
|
|
444
592
|
});
|
|
445
593
|
}
|
|
@@ -537,6 +685,7 @@ async function resolveConfirmedActivity(q) {
|
|
|
537
685
|
headBlock: file?.headBlock,
|
|
538
686
|
chainId: file?.chainId,
|
|
539
687
|
limit,
|
|
688
|
+
escrowAddress: file?.escrowAddress, // 合约地址白名单 = 索引文件自己的部署身份
|
|
540
689
|
});
|
|
541
690
|
if (rows.length > 0)
|
|
542
691
|
return { source: 'chain-index', rows, gates };
|
|
@@ -675,6 +824,10 @@ function buildChainIdScope(rows) {
|
|
|
675
824
|
* ④ `activity_totals.source` === `confirmed_activity_source` (两块来源标注必须一致)
|
|
676
825
|
* ⑤ 行里有任务 而 `totals.tasks === 0` 时, 必须有口径说明 note
|
|
677
826
|
* (页面不许出现「0 个任务」与「N 行任务」并存而**不解释**)
|
|
827
|
+
* ⑥ 0x 长 hex 越界 (2026-09-23): 只许出现在 tx_hash/contract/explorer_* 四个白名单键下 ——
|
|
828
|
+
* EOA 地址跑到别的键 (或白名单键形状不对) 就拒绝导出
|
|
829
|
+
* ⑧ ★ **同一概念不许并排两个数** (2026-09-24, 见 `totalsScopeIssues`): 顶部计数与表格行数/
|
|
830
|
+
* 同源计数矛盾, 或者某个数没有来源却不标「未接入」→ 拒绝导出 (这道门在 UI 侧还有一份镜像)
|
|
678
831
|
*/
|
|
679
832
|
function snapshotConsistencyIssues(snap) {
|
|
680
833
|
const issues = [];
|
|
@@ -701,28 +854,258 @@ function snapshotConsistencyIssues(snap) {
|
|
|
701
854
|
if (!explained)
|
|
702
855
|
issues.push(`totals.tasks=0 与 ${rows.length} 行任务并存, 却没有口径说明 note`);
|
|
703
856
|
}
|
|
857
|
+
issues.push(...auditPublicHexLeaks(snap)); // ⑥ 0x 长 hex 越界 → 拒绝导出 (不静默放行)
|
|
858
|
+
issues.push(...openTasksIssues(snap)); // ⑦ 待接单任务的字段白名单与「只含未认领」不变式
|
|
859
|
+
issues.push(...totalsScopeIssues(snap)); // ⑧ ★ 同一概念不得并排两个数 (顶部 vs 表格)
|
|
704
860
|
return issues;
|
|
705
861
|
}
|
|
862
|
+
/** 顶部逐字段口径的键 (与 `TotalsFieldKey` 逐字一致; 少一个 = 那个数没有来源说明) */
|
|
863
|
+
exports.TOTALS_FIELD_KEYS = [
|
|
864
|
+
'nodes', 'agents', 'active_agents', 'seen_last_24h',
|
|
865
|
+
'tasks', 'tasks_completed', 'tasks_verified', 'tasks_settled', 'signatures',
|
|
866
|
+
];
|
|
867
|
+
const TOTALS_FIELD_SOURCES = ['pulse-events', 'chain-index', 'signature-audit', 'none'];
|
|
868
|
+
const TOTALS_FIELD_WINDOWS = ['window-24h', 'full', 'unknown'];
|
|
869
|
+
/** 「未接入」类措辞 (无源时必须出现, 否则等于没说清为什么没有数) */
|
|
870
|
+
const UNAVAILABLE_WORDS = /未接入|not connected|无可用源|no available source/;
|
|
871
|
+
/**
|
|
872
|
+
* ★ 新不变量门 (2026-09-24 leo:「数量怎么对不上」): **同一概念不许并排两个数**。
|
|
873
|
+
*
|
|
874
|
+
* 检查的是「页面会把它们放在同一屏里」的那几个数 —— 它们的口径必须各自挂得住:
|
|
875
|
+
* ① 逐字段口径 `totals_scope.fields` 九个键必须齐 (source/window/short/label 双语都在) ——
|
|
876
|
+
* 缺了就只能靠 notes 辩解, 而那正是当初「顶部 0 个任务 vs 表格 25 行」能上线的路径;
|
|
877
|
+
* ② 值 `null` ⇔ 口径标 `none` + 明说「未接入」; **有源却不给数** 或 **无源却拿 0 冒充** 都算矛盾;
|
|
878
|
+
* ③ 同一概念对账 (表里有 5 个任务而顶部写 0 任务 → 判红):
|
|
879
|
+
* · 链上索引可用时, 顶部 tasks/tasks_completed/tasks_settled **必须逐字等于** activity_totals
|
|
880
|
+
* 的同名字段 (同源即恒等, 任何解释都救不了);
|
|
881
|
+
* · 表格行数/同源计数非零而顶部为 0 或缺值 → 必须有口径说明 (note 里写清) 才放行;
|
|
882
|
+
* · 顶部说有 N 个任务而表格一行都没有 (反向矛盾) → 同样判红。
|
|
883
|
+
* `status === 'unavailable'` 的整份快照不发布任何计数 (页面整块显示「快照读不到」), 故只查形状。
|
|
884
|
+
*/
|
|
885
|
+
function totalsScopeIssues(snap) {
|
|
886
|
+
const out = [];
|
|
887
|
+
const t = snap?.totals || {};
|
|
888
|
+
const ts = snap?.totals_scope;
|
|
889
|
+
const at = snap?.activity_totals || {};
|
|
890
|
+
const rows = Array.isArray(snap?.confirmed_activity) ? snap.confirmed_activity : [];
|
|
891
|
+
const unavailableSnap = String(snap?.status || '') === 'unavailable';
|
|
892
|
+
if (!ts || typeof ts !== 'object')
|
|
893
|
+
return ['totals_scope 缺失 (顶部计数的口径必须随快照一起给)'];
|
|
894
|
+
if (!ts.fields || typeof ts.fields !== 'object') {
|
|
895
|
+
return ['totals_scope.fields 缺失 (顶部每个数都要能就地说明来自哪 —— 不许只靠 notes 辩解)'];
|
|
896
|
+
}
|
|
897
|
+
// ① 形状: 九个键齐 + 每个键的 source/window/short/label 都合法
|
|
898
|
+
for (const k of exports.TOTALS_FIELD_KEYS) {
|
|
899
|
+
const f = ts.fields[k];
|
|
900
|
+
if (!f || typeof f !== 'object') {
|
|
901
|
+
out.push(`totals_scope.fields.${k} 缺失`);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (!TOTALS_FIELD_SOURCES.includes(f.source))
|
|
905
|
+
out.push(`totals_scope.fields.${k}.source=${JSON.stringify(f.source)} 不在白名单`);
|
|
906
|
+
if (!TOTALS_FIELD_WINDOWS.includes(f.window))
|
|
907
|
+
out.push(`totals_scope.fields.${k}.window=${JSON.stringify(f.window)} 不在白名单`);
|
|
908
|
+
if (!f.label?.zh || !f.label?.en)
|
|
909
|
+
out.push(`totals_scope.fields.${k}.label 必须双语都给`);
|
|
910
|
+
if (!f.short?.zh || !f.short?.en)
|
|
911
|
+
out.push(`totals_scope.fields.${k}.short 必须双语都给 (页面要就地贴在数字旁)`);
|
|
912
|
+
}
|
|
913
|
+
if (out.length)
|
|
914
|
+
return out; // 形状都不对就不往下算数
|
|
915
|
+
if (!unavailableSnap) {
|
|
916
|
+
// ② 值 ⇔ 口径 (null = 未接入; 有源必须有数; 无源不许给 0)
|
|
917
|
+
for (const k of exports.TOTALS_FIELD_KEYS) {
|
|
918
|
+
const f = ts.fields[k];
|
|
919
|
+
const v = t[k];
|
|
920
|
+
const isNull = v === null || v === undefined;
|
|
921
|
+
if (isNull) {
|
|
922
|
+
if (f.source !== 'none') {
|
|
923
|
+
out.push(`totals.${k} 没有值 (未接入) 却标了源 ${f.source} —— 有源就必须给真值, 没源就标 none`);
|
|
924
|
+
}
|
|
925
|
+
if (!UNAVAILABLE_WORDS.test(`${f.short?.zh}${f.short?.en}${f.label?.zh}${f.label?.en}`)) {
|
|
926
|
+
out.push(`totals.${k} 没有值却没有一句「未接入」说明 (读者会把空白读成 0)`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v < 0) {
|
|
931
|
+
out.push(`totals.${k}=${JSON.stringify(v)} 不是非负有限数`);
|
|
932
|
+
}
|
|
933
|
+
else if (f.source === 'none') {
|
|
934
|
+
out.push(`totals.${k}=${v} 却标「无源」 —— 不许拿裸 0 冒充「没有接入」(那会被读成"没发生过")`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
// ③ 同一概念对账 (顶部计数 vs 表格行数/同源计数)
|
|
939
|
+
const notes = Array.isArray(snap.notes) ? snap.notes.join(' ') : '';
|
|
940
|
+
const pairs = [
|
|
941
|
+
['tasks', t.tasks, at.tasks],
|
|
942
|
+
['tasks_completed', t.tasks_completed, at.tasks_completed],
|
|
943
|
+
['tasks_settled', t.tasks_settled, at.tasks_settled],
|
|
944
|
+
];
|
|
945
|
+
const tableRows = Number(at.rows) || rows.length;
|
|
946
|
+
const sameSourceChain = String(at.source) === 'chain-index';
|
|
947
|
+
// ⑨ 声明与能力不符: 某字段自称「链上索引」口径, 而这一份快照的链上索引根本不可用 ——
|
|
948
|
+
// 那就是替一个不存在的源背书 (页面会写「链上索引·全量」, 而索引这一份没读出来)。
|
|
949
|
+
// 注: 本块必须放在 pairs / sameSourceChain 之后 (先声明后使用, 否则 TDZ 崩)。
|
|
950
|
+
if (!sameSourceChain) {
|
|
951
|
+
for (const [k] of pairs) {
|
|
952
|
+
const f = ts.fields[k];
|
|
953
|
+
if (f.source === 'chain-index') {
|
|
954
|
+
out.push(`totals_scope.fields.${k}.source='chain-index' 而这一份快照的链上索引不可用 ` +
|
|
955
|
+
`(activity_totals.source=${JSON.stringify(at.source)}) —— 声明与能力不符`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
for (const [k, top, src] of pairs) {
|
|
960
|
+
const n = Number(src) || 0;
|
|
961
|
+
const topN = (top === null || top === undefined) ? null : Number(top);
|
|
962
|
+
const topScope = ts.fields[k];
|
|
963
|
+
if (sameSourceChain && topScope.source === 'chain-index') {
|
|
964
|
+
// 同源: 必须逐字相等, 不需要任何"解释"
|
|
965
|
+
if (topN !== n) {
|
|
966
|
+
out.push(`同一概念两个数: 顶部 totals.${k}=${topN === null ? '未接入' : topN} ≠ 链上索引同源计数 activity_totals.${k}=${n}` +
|
|
967
|
+
` (表格 ${tableRows} 行) —— 同源即恒等, 页面并排出现两个数就是矛盾`);
|
|
968
|
+
}
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
if (n > 0 && (topN === null || topN === 0)) {
|
|
972
|
+
const explained = notes.includes('口径') && notes.includes(String(tableRows)) && notes.includes('脉冲事件');
|
|
973
|
+
if (!explained) {
|
|
974
|
+
out.push(`顶部 totals.${k}=${topN === null ? '未接入' : 0} 与表格里的 ${n} 个同类 (${tableRows} 行) 矛盾, 且没有任何口径说明`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (topN !== null && n === 0 && tableRows === 0 && topN > 0) {
|
|
978
|
+
out.push(`顶部 totals.${k}=${topN} 而表格一行都没有 (${tableRows} 行) —— 反向矛盾: 数不出行却报了数`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
// ④ 明说"数字不一样"时必须有解释 (老规矩保留; 不许只靠一句 label)
|
|
983
|
+
if (ts.differs_from_activity === true) {
|
|
984
|
+
const notes = Array.isArray(snap.notes) ? snap.notes.join(' ') : '';
|
|
985
|
+
if (!notes.includes('口径'))
|
|
986
|
+
out.push('totals_scope.differs_from_activity=true 但 notes 里没有任何口径说明');
|
|
987
|
+
}
|
|
988
|
+
return out;
|
|
989
|
+
}
|
|
990
|
+
/** open_tasks 行的字段白名单 (与 `OpenTaskRow` 逐字一致; 多一个键 = 可能把正文/身份带出去了) */
|
|
991
|
+
exports.OPEN_TASK_KEYS = ['capability', 'budget', 'currency', 'network', 'deadline', 'claimed', 'announcementId'];
|
|
992
|
+
/**
|
|
993
|
+
* 待接单任务行的自检 (空 = 通过)。这不是格式洁癖, 每条都对着一个真泄漏路径:
|
|
994
|
+
* · 键集 ≠ 白名单 → 有人把 instruction / buyerDid / claims 之类**展平**进公开投影了;
|
|
995
|
+
* · `claimed !== false` → 「只导出未认领」这个筛选坏了 (页面会把已接单的当待接单);
|
|
996
|
+
* · `announcementId` 长于 8 位 → 短写丢了, 原始公告 id 上公开页 (页面上的 id 会被读者当完整 id 用);
|
|
997
|
+
* · capability 空 / deadline 非正 → 页面上会出现一条说不出是什么、也不知道何时截止的行。
|
|
998
|
+
*/
|
|
999
|
+
function openTasksIssues(snap) {
|
|
1000
|
+
const out = [];
|
|
1001
|
+
const rows = snap?.open_tasks;
|
|
1002
|
+
if (!Array.isArray(rows))
|
|
1003
|
+
return ['open_tasks 缺失 (公开「待接单任务」必须是数组, 没有就给空数组)'];
|
|
1004
|
+
const allow = exports.OPEN_TASK_KEYS;
|
|
1005
|
+
rows.forEach((r, i) => {
|
|
1006
|
+
const at = `open_tasks[${i}]`;
|
|
1007
|
+
if (!r || typeof r !== 'object' || Array.isArray(r)) {
|
|
1008
|
+
out.push(`${at} 不是对象`);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const extra = Object.keys(r).filter((k) => !allow.includes(k));
|
|
1012
|
+
if (extra.length)
|
|
1013
|
+
out.push(`${at} 出现白名单外的键: ${extra.join(', ')} (公开投影只许 ${allow.join('/')})`);
|
|
1014
|
+
if (typeof r.capability !== 'string' || !r.capability.trim())
|
|
1015
|
+
out.push(`${at}.capability 缺失/为空`);
|
|
1016
|
+
if (r.claimed !== false)
|
|
1017
|
+
out.push(`${at}.claimed=${JSON.stringify(r.claimed)} —— 公开页只许列**未认领**的公告`);
|
|
1018
|
+
if (typeof r.announcementId !== 'string' || r.announcementId.length === 0 || r.announcementId.length > 8) {
|
|
1019
|
+
out.push(`${at}.announcementId 必须是 announcementId 的**前 8 位**(1..8 字符), 实得 ${JSON.stringify(r.announcementId)}`);
|
|
1020
|
+
}
|
|
1021
|
+
if (!Number.isFinite(r.deadline) || r.deadline <= 0)
|
|
1022
|
+
out.push(`${at}.deadline 必须是正的 ms 时间戳`);
|
|
1023
|
+
for (const k of ['budget', 'currency', 'network']) {
|
|
1024
|
+
if (r[k] !== null && typeof r[k] !== 'string')
|
|
1025
|
+
out.push(`${at}.${k} 只许字符串或 null (不替公告猜值)`);
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
return out;
|
|
1029
|
+
}
|
|
1030
|
+
// ── 顶部计数的逐字段口径 (2026-09-24) ────────────────────────────────────────
|
|
1031
|
+
// 页面把 8~9 个数字并排放在**一行**里, 一行一个总口径是解释不了它们的 —— 每个数必须自带来源。
|
|
1032
|
+
const WINDOW_HOURS = Math.round(exports.PULSE_LIMITS.windowMs / 3600000);
|
|
1033
|
+
/** 「本节点 N h 脉冲事件」口径 (节点/智能体类计数, 以及链上索引不可用时的降级口径) */
|
|
1034
|
+
function pulseField(what) {
|
|
1035
|
+
return {
|
|
1036
|
+
source: 'pulse-events',
|
|
1037
|
+
window: 'window-24h',
|
|
1038
|
+
short: { zh: `${WINDOW_HOURS}h 脉冲`, en: `${WINDOW_HOURS}h pulse` },
|
|
1039
|
+
label: {
|
|
1040
|
+
zh: `本节点 ${WINDOW_HOURS}h 观察窗口内收到的脉冲事件 · ${what.zh}`,
|
|
1041
|
+
en: `pulse events received by this node within the ${WINDOW_HOURS}h window · ${what.en}`,
|
|
1042
|
+
},
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
/** 「链上索引 · 全量」口径 (与下方活动表**同源**: 同源即恒等, 页面不许并排两个数) */
|
|
1046
|
+
function chainField(what) {
|
|
1047
|
+
return {
|
|
1048
|
+
source: 'chain-index',
|
|
1049
|
+
window: 'full',
|
|
1050
|
+
short: { zh: '链上索引·全量', en: 'chain index · whole' },
|
|
1051
|
+
label: {
|
|
1052
|
+
zh: `链上索引全量 (与下方活动表同源) · ${what.zh}`,
|
|
1053
|
+
en: `chain index, whole (same source as the activity table below) · ${what.en}`,
|
|
1054
|
+
},
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
/** 「本机签名审计账」口径 (真源: `<home>/.bolloon/wallet-signatures.jsonl` 窗口内条数) */
|
|
1058
|
+
function signatureAuditField() {
|
|
1059
|
+
return {
|
|
1060
|
+
source: 'signature-audit',
|
|
1061
|
+
window: 'window-24h',
|
|
1062
|
+
short: { zh: `${WINDOW_HOURS}h 签名审计`, en: `${WINDOW_HOURS}h signature audit` },
|
|
1063
|
+
label: {
|
|
1064
|
+
zh: `本机签名审计账 (wallet-signatures.jsonl) ${WINDOW_HOURS}h 内条数 —— 只数条数, 不含签名内容`,
|
|
1065
|
+
en: `rows in this node's signature audit log (wallet-signatures.jsonl) within ${WINDOW_HOURS}h — count only, no content`,
|
|
1066
|
+
},
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
/** 「没有可用源」= 未接入 (值必须是 null; 页面必须写「未接入」, 不许拿 0 冒充「没发生过」) */
|
|
1070
|
+
function noneField(why) {
|
|
1071
|
+
return {
|
|
1072
|
+
source: 'none',
|
|
1073
|
+
window: 'unknown',
|
|
1074
|
+
short: { zh: '未接入', en: 'not connected' },
|
|
1075
|
+
label: { zh: `没有可用源 → 报「未接入」而不是 0 · ${why.zh}`, en: `no available source → reported as "not connected", not 0 · ${why.en}` },
|
|
1076
|
+
unavailable: true,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
706
1079
|
function computeSnapshot(events, opts) {
|
|
707
1080
|
const now = opts.now;
|
|
708
1081
|
const fresh_until = now + exports.PULSE_LIMITS.snapshotTtlMs;
|
|
709
1082
|
if (opts.unavailable) {
|
|
710
1083
|
const emptyRows = [];
|
|
711
1084
|
const gates = normalizeActivityGates(chain_config_js_1.DEFAULT_CONFIRMATIONS);
|
|
1085
|
+
// 观察层不可用 = **没有数** (不是 0): 计数一律 null/0 占位由页面整块隐藏, 口径逐字段标「未接入」。
|
|
1086
|
+
const downField = noneField({ zh: '观察层不可用', en: 'observation layer unavailable' });
|
|
712
1087
|
return {
|
|
713
1088
|
status: 'unavailable',
|
|
714
1089
|
generated_at: now,
|
|
715
1090
|
fresh_until,
|
|
716
1091
|
scope: 'observed',
|
|
717
1092
|
scope_label: { zh: '当前节点观察到', en: 'Observed by this node' },
|
|
718
|
-
totals: {
|
|
1093
|
+
totals: {
|
|
1094
|
+
nodes: 0, agents: 0, active_agents: 0, seen_last_24h: 0,
|
|
1095
|
+
tasks: null, tasks_completed: null, tasks_verified: null, signatures: null, tasks_settled: null,
|
|
1096
|
+
},
|
|
719
1097
|
totals_scope: {
|
|
720
1098
|
source: 'pulse-events', window_ms: exports.PULSE_LIMITS.windowMs,
|
|
721
1099
|
label: {
|
|
722
|
-
zh:
|
|
723
|
-
en: `
|
|
1100
|
+
zh: `本节点 ${WINDOW_HOURS}h 观察窗口的脉冲事件层不可用 → 没有数 (不是 0)`,
|
|
1101
|
+
en: `This node's ${WINDOW_HOURS}h pulse layer is unavailable → no counts (not zero)`,
|
|
724
1102
|
},
|
|
725
1103
|
differs_from_activity: false,
|
|
1104
|
+
fields: {
|
|
1105
|
+
nodes: downField, agents: downField, active_agents: downField, seen_last_24h: downField,
|
|
1106
|
+
tasks: downField, tasks_completed: downField, tasks_verified: downField,
|
|
1107
|
+
tasks_settled: downField, signatures: downField,
|
|
1108
|
+
},
|
|
726
1109
|
},
|
|
727
1110
|
capabilities: [],
|
|
728
1111
|
recent_activity: [],
|
|
@@ -730,6 +1113,9 @@ function computeSnapshot(events, opts) {
|
|
|
730
1113
|
confirmed_activity_source: 'none',
|
|
731
1114
|
activity_totals: summarizeActivityRows(emptyRows, { source: 'none', gates }),
|
|
732
1115
|
chain_id_scope: buildChainIdScope(emptyRows),
|
|
1116
|
+
// 观察层不可用 ≠ 公告板读不到: 这一支只说明「脉冲事件层」挂了, 待接单任务照实带出来
|
|
1117
|
+
// (调用方没给就空数组 —— 不拿空数组冒充「板上没有」: 页面空态说的是「暂未观察到」)
|
|
1118
|
+
open_tasks: Array.isArray(opts.openTasks) ? opts.openTasks : [],
|
|
733
1119
|
notes: ['观察层暂不可用 — 这不是"网络为空"'],
|
|
734
1120
|
};
|
|
735
1121
|
}
|
|
@@ -761,10 +1147,13 @@ function computeSnapshot(events, opts) {
|
|
|
761
1147
|
capabilities.push({ key: 'other', count: otherCount.size });
|
|
762
1148
|
capabilities.sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1));
|
|
763
1149
|
}
|
|
764
|
-
//
|
|
1150
|
+
// 经济计数 (**脉冲口径**) —— 注意: 顶部「任务/已完成/已结算」的**权威源是链上索引**
|
|
1151
|
+
// (见下面 chainAuthoritative 那一段), 脉冲口径只在索引不可用时才顶上去。
|
|
1152
|
+
// 按**不同任务**去重 (同一任务重复事件不虚增), 且只给聚合数不给内容
|
|
765
1153
|
const tasks = new Set();
|
|
766
1154
|
const tasksCompleted = new Set();
|
|
767
1155
|
const tasksVerified = new Set();
|
|
1156
|
+
const tasksSettledPulse = new Set();
|
|
768
1157
|
for (const e of window) {
|
|
769
1158
|
const t = e.taskProof;
|
|
770
1159
|
if (!t)
|
|
@@ -773,6 +1162,8 @@ function computeSnapshot(events, opts) {
|
|
|
773
1162
|
tasks.add(t);
|
|
774
1163
|
if (e.type === 'task_completed' || e.type === 'trade_settled')
|
|
775
1164
|
tasksCompleted.add(t);
|
|
1165
|
+
if (e.type === 'trade_settled')
|
|
1166
|
+
tasksSettledPulse.add(t);
|
|
776
1167
|
if (e.type === 'trade_verified')
|
|
777
1168
|
tasksVerified.add(t);
|
|
778
1169
|
}
|
|
@@ -797,15 +1188,62 @@ function computeSnapshot(events, opts) {
|
|
|
797
1188
|
// 同源计数 + 链归属: **输入就是上表那批行**, 所以不可能与行数/链 id 打架
|
|
798
1189
|
const activity_totals = summarizeActivityRows(activity.rows, { source: activity.source, gates: activity.gates });
|
|
799
1190
|
const chain_id_scope = buildChainIdScope(activity.rows);
|
|
1191
|
+
// ★ 2026-09-24 (leo: 「数量怎么对不上, 尤其是后面的任务和钱包」):
|
|
1192
|
+
// 顶部任务类计数的**权威源改成链上索引** —— 它与下面那张表**同源**, 同源即恒等,
|
|
1193
|
+
// 从根上消灭「顶部 0 个任务 / 表格 15 行任务」这种并排矛盾 (不再靠 notes 辩解)。
|
|
1194
|
+
// 链上索引不可用 (降级 pulse-events) 时才退回旧的脉冲口径, 并在 fields 里如实标出降级。
|
|
1195
|
+
const chainAuthoritative = activity.source === 'chain-index';
|
|
1196
|
+
const totalsTasks = chainAuthoritative ? activity_totals.tasks : tasks.size;
|
|
1197
|
+
const totalsCompleted = chainAuthoritative ? activity_totals.tasks_completed : tasksCompleted.size;
|
|
1198
|
+
const totalsSettled = chainAuthoritative ? activity_totals.tasks_settled : tasksSettledPulse.size;
|
|
1199
|
+
// 「验真」在链上索引里**没有**对应事件 → 链上口径下这个数没有源 (null + 未接入), 不拿结算数冒充已验证
|
|
1200
|
+
const totalsVerified = chainAuthoritative ? null : tasksVerified.size;
|
|
1201
|
+
// 钱包签名: 真源 = 本机签名审计账 (窗口内条数, 只计数); 没有审计账 → 退回脉冲事件上报数;
|
|
1202
|
+
// 两个都没有 → null (未接入)。**绝不**在没有源的时候报 0 (那是「没发生过」, 是另一句话)。
|
|
1203
|
+
const sigTally = opts.signatureAudit ?? null;
|
|
1204
|
+
const auditWired = !!sigTally && sigTally.available === true;
|
|
1205
|
+
const totalsSignatures = auditWired
|
|
1206
|
+
? Number(sigTally.count)
|
|
1207
|
+
: (signatureKeys.size > 0 ? signatureKeys.size : null);
|
|
1208
|
+
const fields = {
|
|
1209
|
+
nodes: pulseField({ zh: '不同节点数', en: 'distinct nodes' }),
|
|
1210
|
+
agents: pulseField({ zh: '不同 Agent 数', en: 'distinct agents' }),
|
|
1211
|
+
active_agents: pulseField({ zh: '活跃 Agent 数', en: 'active agents' }),
|
|
1212
|
+
seen_last_24h: pulseField({ zh: '24h 内出现过的不同 Agent', en: 'distinct agents seen within 24h' }),
|
|
1213
|
+
tasks: chainAuthoritative
|
|
1214
|
+
? chainField({ zh: '有链上活动的不同任务', en: 'distinct tasks with on-chain activity' })
|
|
1215
|
+
: pulseField({ zh: '观察到发起的任务', en: 'tasks observed as created' }),
|
|
1216
|
+
tasks_completed: chainAuthoritative
|
|
1217
|
+
? chainField({ zh: '交付完成 (ProofSubmittedV2) 的不同任务', en: 'distinct tasks with ProofSubmittedV2' })
|
|
1218
|
+
: pulseField({ zh: '交付完成的任务', en: 'tasks observed as completed' }),
|
|
1219
|
+
tasks_settled: chainAuthoritative
|
|
1220
|
+
? chainField({ zh: '托管结算 (Released/Refunded/Expired/Disputed) 的不同任务', en: 'distinct tasks with an on-chain escrow settlement' })
|
|
1221
|
+
: pulseField({ zh: '链上口径结算的任务', en: 'tasks with on-chain settlement' }),
|
|
1222
|
+
tasks_verified: chainAuthoritative
|
|
1223
|
+
? noneField({ zh: '链上索引没有「验真」事件', en: 'the chain index has no verification event' })
|
|
1224
|
+
: pulseField({ zh: '真验真 (trade_verified) 的任务', en: 'tasks truly verified (trade_verified)' }),
|
|
1225
|
+
signatures: auditWired
|
|
1226
|
+
? signatureAuditField()
|
|
1227
|
+
: (signatureKeys.size > 0
|
|
1228
|
+
? pulseField({ zh: '上报过的钱包签名 (按来源+时刻去重)', en: 'reported wallet signatures (deduped by source+time)' })
|
|
1229
|
+
: noneField({ zh: '本节点既没有签名审计账, 也没有签名脉冲事件', en: 'this node has neither a signature audit log nor signature pulse events' })),
|
|
1230
|
+
};
|
|
800
1231
|
const totals_scope = {
|
|
801
|
-
source: 'pulse-events',
|
|
1232
|
+
source: chainAuthoritative ? 'mixed' : 'pulse-events',
|
|
802
1233
|
window_ms: exports.PULSE_LIMITS.windowMs,
|
|
803
1234
|
label: {
|
|
804
|
-
zh:
|
|
805
|
-
|
|
1235
|
+
zh: chainAuthoritative
|
|
1236
|
+
? `节点/智能体 = 本节点 ${WINDOW_HOURS}h 脉冲事件; 任务/已完成/已结算 = 链上索引全量 (与下表同源); 钱包签名 = 本机签名审计`
|
|
1237
|
+
: `只统计本节点 ${WINDOW_HOURS}h 观察窗口内收到的脉冲事件 (本节点自己上报的)`,
|
|
1238
|
+
en: chainAuthoritative
|
|
1239
|
+
? `nodes/agents = this node's ${WINDOW_HOURS}h pulse events; tasks/completed/settled = chain index, whole (same source as the table below); wallet signatures = this node's signature audit log`
|
|
1240
|
+
: `Only pulse events received by this node within the ${WINDOW_HOURS}h observation window (reported by this node itself)`,
|
|
806
1241
|
},
|
|
807
1242
|
differs_from_activity: activity_totals.rows > 0 &&
|
|
808
|
-
(
|
|
1243
|
+
(totalsTasks !== activity_totals.tasks ||
|
|
1244
|
+
totalsCompleted !== activity_totals.tasks_completed ||
|
|
1245
|
+
totalsSettled !== activity_totals.tasks_settled),
|
|
1246
|
+
fields,
|
|
809
1247
|
};
|
|
810
1248
|
if (activity.source === 'chain-index') {
|
|
811
1249
|
notes.push(`confirmed_activity 来自链上索引 (chain-index): ${activity_totals.rows} 行 · ${activity_totals.tasks} 个不同任务 · ` +
|
|
@@ -818,10 +1256,21 @@ function computeSnapshot(events, opts) {
|
|
|
818
1256
|
}
|
|
819
1257
|
// 两套计数口径不同时**必须**解释 —— 页面不许出现「0 个任务」与「N 行任务」并存而不解释
|
|
820
1258
|
if (totals_scope.differs_from_activity) {
|
|
821
|
-
notes.push(`口径不同, 不是数据丢失:
|
|
822
|
-
|
|
823
|
-
`
|
|
824
|
-
|
|
1259
|
+
notes.push(`口径不同, 不是数据丢失: 链上索引不可用 → 顶部任务类计数**降级为脉冲口径** (本快照 tasks=${tasks.size} · ` +
|
|
1260
|
+
`tasks_completed=${tasksCompleted.size} · tasks_verified=${tasksVerified.size} · tasks_settled=${tasksSettledPulse.size} · ` +
|
|
1261
|
+
`signatures=${signatureKeys.size}); 上表 ${activity_totals.rows} 行来自脉冲事件 (同源计数见 activity_totals, ` +
|
|
1262
|
+
`不是链上索引全量) —— 每个数的来源见 totals_scope.fields`);
|
|
1263
|
+
}
|
|
1264
|
+
if (chainAuthoritative) {
|
|
1265
|
+
notes.push(`顶部「任务/已完成/已结算」= 链上索引的**同源计数** (= activity_totals, 与下表同源, 同源即恒等): ` +
|
|
1266
|
+
`tasks=${totalsTasks} · tasks_completed=${totalsCompleted} · tasks_settled=${totalsSettled}; ` +
|
|
1267
|
+
`「节点/智能体」= 本节点 ${WINDOW_HOURS}h 脉冲事件; 「已验证」在链上索引里没有对应事件 → ` +
|
|
1268
|
+
`该口径无对应事件源, 不下发该字段 (不是 0); 「钱包签名」= ${auditWired
|
|
1269
|
+
? `本机签名审计账 ${totalsSignatures} 条 (${WINDOW_HOURS}h 窗口, 只计数)`
|
|
1270
|
+
: (totalsSignatures === null
|
|
1271
|
+
? '无可用源 (本节点既没有签名审计账, 也没有签名脉冲事件), 不下发该字段'
|
|
1272
|
+
: `脉冲事件上报 ${totalsSignatures} 条`)} — ` +
|
|
1273
|
+
`每个数的来源见 totals_scope.fields`);
|
|
825
1274
|
}
|
|
826
1275
|
if (activity_totals.rows > 0)
|
|
827
1276
|
notes.push(chain_id_scope.note.zh);
|
|
@@ -838,10 +1287,12 @@ function computeSnapshot(events, opts) {
|
|
|
838
1287
|
agents: agents.size,
|
|
839
1288
|
active_agents: active.size,
|
|
840
1289
|
seen_last_24h: agents.size, // 24h 窗口内的不同 Agent
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1290
|
+
// ★ 任务类计数 = 链上索引同源值 (与下表恒等); 索引不可用才退回脉冲口径 (见 fields.tasks)
|
|
1291
|
+
tasks: totalsTasks,
|
|
1292
|
+
tasks_completed: totalsCompleted,
|
|
1293
|
+
tasks_verified: totalsVerified, // 链上口径下 = null (未接入), 不冒充 0
|
|
1294
|
+
signatures: totalsSignatures, // 真源 = 本机签名审计账; 无源 = null (未接入)
|
|
1295
|
+
tasks_settled: totalsSettled, // 新增字段一律排最后 (老 8 个字段顺序逐字不变)
|
|
845
1296
|
},
|
|
846
1297
|
totals_scope,
|
|
847
1298
|
capabilities,
|
|
@@ -850,6 +1301,7 @@ function computeSnapshot(events, opts) {
|
|
|
850
1301
|
confirmed_activity_source: activity.source,
|
|
851
1302
|
activity_totals,
|
|
852
1303
|
chain_id_scope,
|
|
1304
|
+
open_tasks: Array.isArray(opts.openTasks) ? opts.openTasks : [],
|
|
853
1305
|
notes,
|
|
854
1306
|
};
|
|
855
1307
|
}
|
|
@@ -867,8 +1319,10 @@ async function getNetworkPulse(opts = {}) {
|
|
|
867
1319
|
const shapeOk = Array.isArray(cached?.confirmed_activity)
|
|
868
1320
|
&& typeof cached?.confirmed_activity_source === 'string'
|
|
869
1321
|
&& !!cached?.totals_scope
|
|
1322
|
+
&& !!cached?.totals_scope?.fields // 老缓存没有逐字段口径 → 过期形状, 重算
|
|
870
1323
|
&& !!cached?.activity_totals
|
|
871
|
-
&& !!cached?.chain_id_scope
|
|
1324
|
+
&& !!cached?.chain_id_scope
|
|
1325
|
+
&& Array.isArray(cached?.open_tasks); // 老缓存没有待接单任务 → 过期形状, 重算
|
|
872
1326
|
if (shapeOk && cached && Number.isFinite(cached.generated_at) && cached.generated_at + exports.PULSE_LIMITS.snapshotTtlMs > now)
|
|
873
1327
|
return cached;
|
|
874
1328
|
}
|
|
@@ -883,7 +1337,11 @@ async function getNetworkPulse(opts = {}) {
|
|
|
883
1337
|
}
|
|
884
1338
|
// 活动行优先取 P5 链上索引 (真链上事实); 索引不可用 → 退回脉冲事件, 并在快照里标出来源
|
|
885
1339
|
const confirmedActivity = await resolveConfirmedActivity({ home: opts.home, events, now });
|
|
886
|
-
|
|
1340
|
+
// 钱包签名的**真源**: 本机签名审计账 (窗口内条数)。没有账 → 注入 available:false → 快照标「未接入」
|
|
1341
|
+
// (不拿 0 冒充「没发生过」: 本机明明签过而脉冲事件丢了的那次真事故就是这么来的)
|
|
1342
|
+
const signatureAudit = tallySignatureAudit(opts.home, { now, windowMs: exports.PULSE_LIMITS.windowMs });
|
|
1343
|
+
// 待接单任务: 只读本机公告板目录 (未认领且未过期), 投影成白名单 7 字段 —— 与脉冲事件层无关
|
|
1344
|
+
const snap = computeSnapshot(events, { now, confirmedActivity, openTasks: readOpenTasks(opts.home, now), signatureAudit });
|
|
887
1345
|
try {
|
|
888
1346
|
fs.mkdirSync((0, exports.pulseDir)(opts.home), { recursive: true });
|
|
889
1347
|
fs.writeFileSync(snapshotFile(opts.home), JSON.stringify(snap), 'utf8');
|
|
@@ -976,4 +1434,49 @@ function assertNoPrivateFields(value, at = '$') {
|
|
|
976
1434
|
walk(value, at);
|
|
977
1435
|
return issues;
|
|
978
1436
|
}
|
|
1437
|
+
/**
|
|
1438
|
+
* 允许出现 0x 长 hex 的键 (**唯一白名单**, 2026-09-23 leo 拍板; 同日收窄): 交易哈希 / escrow 合约地址 /
|
|
1439
|
+
* 交易浏览器链接 —— 都是公开链上事实。除了这 3 个键, 别处出现 0x 长 hex (尤其 EOA 钱包地址) 一律算泄露。
|
|
1440
|
+
* 注意 `contract` **只在数据里**(供索引/诊断), 页面不渲染合约地址也不给合约链接 ⇒ 没有 `explorer_contract` 这个键。
|
|
1441
|
+
*/
|
|
1442
|
+
exports.PUBLIC_HEX_KEYS = ['tx_hash', 'contract', 'explorer_tx'];
|
|
1443
|
+
/**
|
|
1444
|
+
* 0x 长 hex 越界审计 (**粒度比 `assertNoPrivateFields` 更细**: 那个只看键名, 这个看值形态)。
|
|
1445
|
+
* · 白名单键下: 形状必须精确 —— `tx_hash` = 0x+64 hex; `contract` = 0x+40 hex;
|
|
1446
|
+
* `explorer_tx` = 我们造的**交易**浏览器链接形状 (`EXPLORER_URL_RE`)
|
|
1447
|
+
* · **其他任何键**的值里出现 `0x` + 40/64 位 hex → 报出来 (买方·卖方 EOA、taskKey 原文、
|
|
1448
|
+
* args 里的地址都属这一类); `explorer_contract` 之类不在白名单的键**同样**按泄露处理
|
|
1449
|
+
* 返回越界说明列表 (空 = 干净)。这条尺子**只收紧不放松**: 老口径下「任何 0x 长 hex 都不许」
|
|
1450
|
+
* 现在等价于「除了这 3 个白名单键, 任何 0x 长 hex 都不许」。
|
|
1451
|
+
*/
|
|
1452
|
+
function auditPublicHexLeaks(value, at = '$') {
|
|
1453
|
+
const issues = [];
|
|
1454
|
+
const allow = exports.PUBLIC_HEX_KEYS;
|
|
1455
|
+
const walk = (v, p) => {
|
|
1456
|
+
if (Array.isArray(v))
|
|
1457
|
+
return v.forEach((x, i) => walk(x, `${p}[${i}]`));
|
|
1458
|
+
if (v && typeof v === 'object') {
|
|
1459
|
+
for (const [k, val] of Object.entries(v))
|
|
1460
|
+
walk(val, `${p}.${k}`);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
if (typeof v !== 'string')
|
|
1464
|
+
return;
|
|
1465
|
+
const s = v;
|
|
1466
|
+
const key = p.slice(p.lastIndexOf('.') + 1);
|
|
1467
|
+
// 白名单键: 形状必须精确 —— 不管值长短都查 (拿短串/怪串冒充同样拒绝)
|
|
1468
|
+
if (allow.includes(key)) {
|
|
1469
|
+
const ok = (key === 'tx_hash' && explorer_js_1.TX_HASH_RE.test(s))
|
|
1470
|
+
|| (key === 'contract' && explorer_js_1.ADDRESS_RE.test(s))
|
|
1471
|
+
|| (key === 'explorer_tx' && explorer_js_1.EXPLORER_URL_RE.test(s));
|
|
1472
|
+
if (!ok)
|
|
1473
|
+
issues.push(`${p} = 白名单键 ${key} 下的形状不对 (只认精确形状, 不认"含 0x 就算")`);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (/0x[0-9a-fA-F]{40}/.test(s))
|
|
1477
|
+
issues.push(`${p} = 0x 长 hex (只许出现在 ${allow.join('/')} 下) → 按泄露处理`);
|
|
1478
|
+
};
|
|
1479
|
+
walk(value, at);
|
|
1480
|
+
return issues;
|
|
1481
|
+
}
|
|
979
1482
|
//# sourceMappingURL=network-pulse.js.map
|