@bolloon/bolloon-agent 0.5.3 → 0.5.5
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-identity-store.js +41 -5
- package/dist/agents/background-notices.js +124 -0
- package/dist/agents/channel-identity.js +54 -0
- package/dist/agents/code-write-gate.js +47 -0
- package/dist/agents/deny-pipeline.js +11 -2
- package/dist/agents/egress-sanitize.js +69 -0
- package/dist/agents/error-classifier.js +67 -5
- package/dist/agents/experience-review.js +226 -0
- package/dist/agents/gateway-group.js +153 -18
- package/dist/agents/iteration-budget.js +64 -0
- package/dist/agents/lesson-to-skill.js +211 -0
- package/dist/agents/managed-services.js +64 -0
- package/dist/agents/parse-tool-call.js +38 -32
- package/dist/agents/pi-sdk-session-manager.js +72 -3
- package/dist/agents/pi-sdk-tools.js +268 -47
- package/dist/agents/pi-sdk.js +470 -22
- package/dist/agents/plan-store.js +36 -0
- package/dist/agents/process-runner.js +64 -2
- package/dist/agents/session-store.js +144 -3
- package/dist/agents/shell-guard.js +109 -13
- package/dist/agents/skill-health.js +90 -0
- package/dist/agents/skill-ledger.js +129 -0
- package/dist/agents/tool-loop-guard.js +303 -0
- package/dist/agents/tool-path-args.js +53 -0
- package/dist/agents/tool-result-gate.js +50 -0
- package/dist/agents/tool-subset.js +109 -0
- package/dist/agents/tool-telemetry.js +97 -0
- package/dist/agents/wallet-tools.js +63 -0
- package/dist/bootstrap/context-collector.js +37 -7
- package/dist/bootstrap/lifecycle-hooks.js +12 -3
- package/dist/bootstrap/persona-init.js +174 -0
- package/dist/bootstrap/persona-loader.js +5 -1
- package/dist/cli/ink-app.js +231 -32
- package/dist/cli/input-history.js +119 -0
- package/dist/cli/input-paste.js +218 -0
- package/dist/cli/loading-tui.js +69 -7
- package/dist/cli/log-gate.js +46 -1
- package/dist/cli/mention-data.js +30 -0
- package/dist/cli/reasoning-view.js +103 -0
- package/dist/cli/session-summary.js +129 -0
- package/dist/cli/session-tree.js +141 -0
- package/dist/cli/startup-notice.js +25 -0
- package/dist/cli/status-segments.js +155 -0
- package/dist/cli/stores.js +16 -0
- package/dist/cli/trace-line.js +54 -0
- package/dist/cli/wallet-store.js +159 -0
- package/dist/cli/wallet-table.js +46 -0
- package/dist/cli-entry.js +22 -0
- package/dist/context-compaction/auto-compact.js +37 -2
- package/dist/electron-build/agents/agent-identity-store.js +43 -5
- package/dist/electron-build/agents/agent-identity-store.js.map +1 -1
- package/dist/electron-build/agents/background-notices.js +138 -0
- package/dist/electron-build/agents/background-notices.js.map +1 -0
- package/dist/electron-build/agents/channel-identity.js +64 -0
- package/dist/electron-build/agents/channel-identity.js.map +1 -0
- package/dist/electron-build/agents/code-write-gate.js +55 -0
- package/dist/electron-build/agents/code-write-gate.js.map +1 -0
- package/dist/electron-build/agents/deny-pipeline.js +11 -2
- package/dist/electron-build/agents/deny-pipeline.js.map +1 -1
- package/dist/electron-build/agents/egress-sanitize.js +76 -0
- package/dist/electron-build/agents/egress-sanitize.js.map +1 -0
- package/dist/electron-build/agents/error-classifier.js +68 -5
- package/dist/electron-build/agents/error-classifier.js.map +1 -1
- package/dist/electron-build/agents/experience-review.js +243 -0
- package/dist/electron-build/agents/experience-review.js.map +1 -0
- package/dist/electron-build/agents/iteration-budget.js +73 -0
- package/dist/electron-build/agents/iteration-budget.js.map +1 -0
- package/dist/electron-build/agents/lesson-to-skill.js +228 -0
- package/dist/electron-build/agents/lesson-to-skill.js.map +1 -0
- package/dist/electron-build/agents/managed-services.js +69 -0
- package/dist/electron-build/agents/managed-services.js.map +1 -0
- package/dist/electron-build/agents/parse-tool-call.js +38 -32
- package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-manager.js +75 -3
- package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +268 -46
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +471 -22
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/plan-store.js +37 -0
- package/dist/electron-build/agents/plan-store.js.map +1 -1
- package/dist/electron-build/agents/process-runner.js +71 -2
- package/dist/electron-build/agents/process-runner.js.map +1 -1
- package/dist/electron-build/agents/session-store.js +144 -3
- package/dist/electron-build/agents/session-store.js.map +1 -1
- package/dist/electron-build/agents/shell-guard.js +110 -14
- package/dist/electron-build/agents/shell-guard.js.map +1 -1
- package/dist/electron-build/agents/skill-health.js +103 -0
- package/dist/electron-build/agents/skill-health.js.map +1 -0
- package/dist/electron-build/agents/skill-ledger.js +146 -0
- package/dist/electron-build/agents/skill-ledger.js.map +1 -0
- package/dist/electron-build/agents/tool-loop-guard.js +319 -0
- package/dist/electron-build/agents/tool-loop-guard.js.map +1 -0
- package/dist/electron-build/agents/tool-path-args.js +63 -0
- package/dist/electron-build/agents/tool-path-args.js.map +1 -0
- package/dist/electron-build/agents/tool-result-gate.js +59 -0
- package/dist/electron-build/agents/tool-result-gate.js.map +1 -0
- package/dist/electron-build/agents/tool-subset.js +116 -0
- package/dist/electron-build/agents/tool-subset.js.map +1 -0
- package/dist/electron-build/agents/tool-telemetry.js +108 -0
- package/dist/electron-build/agents/tool-telemetry.js.map +1 -0
- package/dist/electron-build/agents/wallet-tools.js +102 -0
- package/dist/electron-build/agents/wallet-tools.js.map +1 -0
- package/dist/electron-build/bootstrap/context-collector.js +38 -7
- package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
- package/dist/electron-build/bootstrap/lifecycle-hooks.js +12 -3
- package/dist/electron-build/bootstrap/lifecycle-hooks.js.map +1 -1
- package/dist/electron-build/bootstrap/persona-init.js +216 -0
- package/dist/electron-build/bootstrap/persona-init.js.map +1 -0
- package/dist/electron-build/bootstrap/persona-loader.js +5 -1
- package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
- package/dist/electron-build/cli/wallet-store.js +200 -0
- package/dist/electron-build/cli/wallet-store.js.map +1 -0
- package/dist/electron-build/context-compaction/auto-compact.js +38 -2
- package/dist/electron-build/context-compaction/auto-compact.js.map +1 -1
- package/dist/electron-build/external-engines/delegate.js +22 -0
- package/dist/electron-build/external-engines/delegate.js.map +1 -1
- package/dist/electron-build/llm/pi-ai.js +25 -0
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/orbitdb/cid-database.js +55 -0
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +36 -11
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/web/server-storage.js +26 -5
- package/dist/electron-build/web/server-storage.js.map +1 -1
- package/dist/external-engines/delegate.js +21 -0
- package/dist/index.js +1324 -173
- package/dist/llm/pi-ai.js +23 -0
- package/dist/orbitdb/cid-database.js +55 -0
- package/dist/orbitdb/event-index.js +370 -0
- package/dist/orbitdb/event-query.js +131 -0
- package/dist/orbitdb/event-shell.js +510 -0
- package/dist/orbitdb/group-access.js +381 -0
- package/dist/orbitdb/group-shards.js +140 -0
- package/dist/orbitdb/ipfs-node.js +36 -11
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +3 -2
- package/dist/web/mobile-core.js +7762 -6289
- package/dist/web/routes-mobile-tasks.js +2 -1
- package/dist/web/server-storage.js +26 -5
- package/dist/web/server.js +2 -0
- package/dist/web/sw.js +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* group-access.ts — 群写入权限的 **DID 门控** (2026-10-01, P1)
|
|
3
|
+
* ==========================================================================
|
|
4
|
+
* 目标: 把群 (OrbitDB events store) 的写入权限从 `write:['*']` 收紧成"DID 白名单":
|
|
5
|
+
* 建群者 + **显式加入**的成员能写; 其余人**写不进 log**; 成员变更本身是一条
|
|
6
|
+
* **可离线验签**的群事件 (成员自签 + 管理员签 ⇒ 不是某个中心节点单方面说了算)。
|
|
7
|
+
*
|
|
8
|
+
* ── 先读源码再动手 (@orbitdb/core 4.0.0, node_modules 里就是权威) ───────────
|
|
9
|
+
*
|
|
10
|
+
* ① 真正的写闸门 = store manifest 里的 access controller (AC)。
|
|
11
|
+
* `IPFSAccessController.canAppend` 逐条判:
|
|
12
|
+
* `write.includes(entry 写者的 identity.id) || write.includes('*')`
|
|
13
|
+
* —— `access-controllers/ipfs.js:78-90`。
|
|
14
|
+
* ⚠️ 比较的是 **OrbitDB 身份 id**, 不是 DID。而 OrbitDB 身份 id =
|
|
15
|
+
* keystore 里该身份的**压缩 secp256k1 公钥 hex** (66 位, 02/03 开头):
|
|
16
|
+
* `key-store.js:213 generateKeyPair('secp256k1')` +
|
|
17
|
+
* `identities/providers/publickey.js:55 base16(publicKey.raw)`。
|
|
18
|
+
* **不是 Ed25519** ⇒ DID 与写身份是**两把钥匙**, 必须显式绑定。
|
|
19
|
+
* 本模块的成员事件就是那张绑定凭证 (`member.did ↔ member.orbitdbId`)。
|
|
20
|
+
*
|
|
21
|
+
* ② 白名单能否在**既有 store** 上增量更新? —— 对 IPFS 型 AC: **不能**。
|
|
22
|
+
* `write` 被打进一个 dag-cbor 的 ACL manifest, 该 ACL 的 **CID** 写进 store
|
|
23
|
+
* manifest 的 `accessController` 字段, 而 store manifest 的 **CID 就是 store 地址**:
|
|
24
|
+
* `access-controllers/ipfs.js:16-25, 62-69` → `manifest-store.js:38-56`
|
|
25
|
+
* → `orbitdb.js:125-134,140-142`。
|
|
26
|
+
* 改白名单 ⇒ 新 ACL 块 ⇒ 新 manifest ⇒ **新地址**。
|
|
27
|
+
* 整个包对 IPFS 型 AC **没有任何"就地改 AC"的 API**
|
|
28
|
+
* (`ipfs.js` 只返回 `{type, address, write, canAppend}`, 没有 grant/revoke/mutate)。
|
|
29
|
+
* ⇒ **白名单变更 = 重建 store (换地址)**; 老地址的写集合永远不变 (历史不可追认)。
|
|
30
|
+
* 代价见 `aclChangePlan()`。
|
|
31
|
+
*
|
|
32
|
+
* ③ 4.0.0 另有一个 `OrbitDBAccessController` (type `'orbitdb'`), 它把白名单搬进一个
|
|
33
|
+
* **独立的 keyvalue db** 并提供 `grant`/`revoke` (`access-controllers/orbitdb.js:152-174`),
|
|
34
|
+
* 所以**那一层是**可增量的。但:
|
|
35
|
+
* · 那个 kv db 自己的**根写名单仍冻结**在它的 IPFS 型 manifest 里 —— 只有根名单里的人
|
|
36
|
+
* 能 grant/revoke (`OrbitDBAccessController({write})` 内部就是这么建的, 见 orbitdb.js:40);
|
|
37
|
+
* · 增量要生效, 必须先把这个 **AC kv db 复制到对端** (等于多同步一个 store);
|
|
38
|
+
* · 2026-10-01 本机真两节点实测 (见报告): 对一个**开 store 前就已 grant** 的新节点,
|
|
39
|
+
* 动态白名单能拿到; 但对一个**已经开着 store** 的节点, 180s 内**没看到** live grant
|
|
40
|
+
* 传播到位。⇒ 不当默认, 只作为可选增量通道记录在此, 不冒充"已实现"。
|
|
41
|
+
* ⇒ 默认策略 = ① 的静态白名单 + ②的重建; 语义确定, 不留"白名单到底传没传过去"的悬念。
|
|
42
|
+
*
|
|
43
|
+
* ── 诚实边界 ──────────────────────────────────────────────────────────────
|
|
44
|
+
* · 本模块**只做**身份/白名单/成员事件的构造与验证 + ACL 选项生成;
|
|
45
|
+
* 它不自己开 store、不自己起节点 (由 gateway-group / 验收门调)。
|
|
46
|
+
* · `verifyMembershipEvent` 是**真** Ed25519 验签 (`ed25519Verify`), 不是摘要比对。
|
|
47
|
+
* · 白名单里的东西是 **OrbitDB 身份 id**, 不是 DID 字符串 —— 别把 DID 塞进 `write`,
|
|
48
|
+
* `canAppend` 永远匹配不上 (这会静默变成"谁都写不了")。
|
|
49
|
+
*/
|
|
50
|
+
import { canonicalize, ed25519Sign, ed25519Verify } from '../agents/x402/paid-info-protocol.js';
|
|
51
|
+
import { didFromEd25519PublicKey } from '../agents/identity/address-binding.js';
|
|
52
|
+
// ─────────────────────────────────────────────────────────── 常量 (冻结)
|
|
53
|
+
/** 成员事件协议名 (冻结; 签名对象里的 `protocol`) */
|
|
54
|
+
export const GROUP_MEMBERSHIP_PROTOCOL = 'bolloon-group-membership/1';
|
|
55
|
+
/** 落在群 store 里的条目种类标记 (带这个 kind 的条目**不是**聊天消息) */
|
|
56
|
+
export const GROUP_MEMBERSHIP_KIND = 'bolloon.group.membership';
|
|
57
|
+
/** OrbitDB 写身份 id 的形状: 压缩 secp256k1 公钥 hex = 33B = 66 位, 02/03 开头 */
|
|
58
|
+
export const ORBITDB_IDENTITY_ID_RE = /^0[23][0-9a-f]{64}$/;
|
|
59
|
+
/** Ed25519 公钥 (raw 32B) hex 形状 */
|
|
60
|
+
export const ED25519_PUBKEY_HEX_RE = /^[0-9a-f]{64}$/;
|
|
61
|
+
/** `did:key:z…` 形状 (与 address-binding 的 BINDING_DID_RE 同构) */
|
|
62
|
+
export const DID_KEY_RE = /^did:key:z[1-9A-HJ-NP-Za-km-z]{20,}$/;
|
|
63
|
+
// ─────────────────────────────────────────────────────────── 形状校验
|
|
64
|
+
function assertMemberRef(m, where) {
|
|
65
|
+
const r = m;
|
|
66
|
+
if (!r || typeof r !== 'object')
|
|
67
|
+
throw new Error(`${where}: 成员对象缺失`);
|
|
68
|
+
if (!DID_KEY_RE.test(String(r.did || ''))) {
|
|
69
|
+
throw new Error(`${where}: did 形状非法 (要 did:key:z…), 实得 ${String(r.did || '').slice(0, 40)}`);
|
|
70
|
+
}
|
|
71
|
+
if (!ED25519_PUBKEY_HEX_RE.test(String(r.publicKeyHex || ''))) {
|
|
72
|
+
throw new Error(`${where}: publicKeyHex 必须是 64 位小写 hex`);
|
|
73
|
+
}
|
|
74
|
+
if (!ORBITDB_IDENTITY_ID_RE.test(String(r.orbitdbId || ''))) {
|
|
75
|
+
throw new Error(`${where}: orbitdbId 形状非法 (要 66 位压缩 secp256k1 公钥 hex, 02/03 开头), 实得 ${String(r.orbitdbId || '').slice(0, 24)}`);
|
|
76
|
+
}
|
|
77
|
+
const derived = didFromEd25519PublicKey(r.publicKeyHex);
|
|
78
|
+
if (derived !== r.did) {
|
|
79
|
+
throw new Error(`${where}: did 与 publicKeyHex 不是同一把钥匙 (期望 ${derived.slice(0, 24)}…, 实得 ${r.did.slice(0, 24)}…)`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 造一条成员变更声明 (纯函数, 不签名)。
|
|
84
|
+
* 校验: op/ts/group 形状 + 成员与发起人的 (did ↔ 公钥) 一致 + orbitdbId 形状。
|
|
85
|
+
*/
|
|
86
|
+
export function buildMembershipStatement(input) {
|
|
87
|
+
const group = String(input.group || '').trim();
|
|
88
|
+
if (!group.startsWith('/orbitdb/'))
|
|
89
|
+
throw new Error(`group 必须是 /orbitdb/… 地址, 实得 ${group.slice(0, 40)}`);
|
|
90
|
+
if (input.op !== 'add' && input.op !== 'remove')
|
|
91
|
+
throw new Error(`op 必须是 add|remove, 实得 ${String(input.op)}`);
|
|
92
|
+
assertMemberRef(input.member, 'member');
|
|
93
|
+
if (!input.by || typeof input.by !== 'object')
|
|
94
|
+
throw new Error('by 缺失');
|
|
95
|
+
if (!DID_KEY_RE.test(String(input.by.did || '')))
|
|
96
|
+
throw new Error('by.did 形状非法 (要 did:key:z…)');
|
|
97
|
+
if (!ED25519_PUBKEY_HEX_RE.test(String(input.by.publicKeyHex || '')))
|
|
98
|
+
throw new Error('by.publicKeyHex 必须是 64 位小写 hex');
|
|
99
|
+
const derivedBy = didFromEd25519PublicKey(input.by.publicKeyHex);
|
|
100
|
+
if (derivedBy !== input.by.did)
|
|
101
|
+
throw new Error('by.did 与 by.publicKeyHex 不是同一把钥匙');
|
|
102
|
+
const ts = input.ts ?? Date.now();
|
|
103
|
+
if (!Number.isInteger(ts) || ts <= 0)
|
|
104
|
+
throw new Error(`ts 必须是正整数毫秒, 实得 ${String(input.ts)}`);
|
|
105
|
+
return {
|
|
106
|
+
protocol: GROUP_MEMBERSHIP_PROTOCOL,
|
|
107
|
+
group,
|
|
108
|
+
op: input.op,
|
|
109
|
+
member: { did: input.member.did, publicKeyHex: input.member.publicKeyHex, orbitdbId: input.member.orbitdbId },
|
|
110
|
+
by: { did: input.by.did, publicKeyHex: input.by.publicKeyHex },
|
|
111
|
+
ts,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** 签名对象 = `canonicalize(statement)` 的字节串 (键递归排序, 无空格) */
|
|
115
|
+
export function membershipSigningPayload(stmt) {
|
|
116
|
+
return canonicalize(stmt);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 从事件里**剥出正文** (丢掉 memberSig/adminSig) —— 验签必须对正文, 不能对整条事件:
|
|
120
|
+
* 签名在事件里, 拿整条事件去 canonicalize 会把签名本身搅进被签字节串, 自己签自己, 永远验不过。
|
|
121
|
+
*/
|
|
122
|
+
export function statementOfEvent(ev) {
|
|
123
|
+
return {
|
|
124
|
+
protocol: ev.protocol,
|
|
125
|
+
group: ev.group,
|
|
126
|
+
op: ev.op,
|
|
127
|
+
member: ev.member,
|
|
128
|
+
by: ev.by,
|
|
129
|
+
ts: ev.ts,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** 成员侧签名 (证明"我认领这个 orbitdbId") */
|
|
133
|
+
export async function signAsMember(stmt, memberPrivateKeyHex) {
|
|
134
|
+
const seed = Buffer.from(String(memberPrivateKeyHex || ''), 'hex');
|
|
135
|
+
if (seed.length !== 32)
|
|
136
|
+
throw new Error(`成员私钥必须是 32 字节 hex (Ed25519 种子), 实得 ${seed.length}B`);
|
|
137
|
+
return ed25519Sign(seed, membershipSigningPayload(stmt));
|
|
138
|
+
}
|
|
139
|
+
/** 管理侧签名 (证明"我同意这次成员变更") */
|
|
140
|
+
export async function signAsAdmin(stmt, adminPrivateKeyHex) {
|
|
141
|
+
const seed = Buffer.from(String(adminPrivateKeyHex || ''), 'hex');
|
|
142
|
+
if (seed.length !== 32)
|
|
143
|
+
throw new Error(`发起人私钥必须是 32 字节 hex (Ed25519 种子), 实得 ${seed.length}B`);
|
|
144
|
+
return ed25519Sign(seed, membershipSigningPayload(stmt));
|
|
145
|
+
}
|
|
146
|
+
/** 把两枚签名与正文组装成事件 (只校形状, 不验签; 验签用 verifyMembershipEvent) */
|
|
147
|
+
export function assembleMembershipEvent(stmt, sigs) {
|
|
148
|
+
const memberSig = String(sigs.memberSig || '');
|
|
149
|
+
const adminSig = String(sigs.adminSig || '');
|
|
150
|
+
if (!memberSig)
|
|
151
|
+
throw new Error('缺 memberSig (成员必须自己签)');
|
|
152
|
+
if (!adminSig)
|
|
153
|
+
throw new Error('缺 adminSig (发起人必须自己签)');
|
|
154
|
+
return { ...stmt, memberSig, adminSig };
|
|
155
|
+
}
|
|
156
|
+
/** 一次性造事件 (成员 + 管理员各自私钥都在手上时用; 门/测试用) */
|
|
157
|
+
export async function createMembershipEvent(input) {
|
|
158
|
+
const memberSig = await signAsMember(input.statement, input.memberPrivateKeyHex);
|
|
159
|
+
const adminSig = await signAsAdmin(input.statement, input.adminPrivateKeyHex);
|
|
160
|
+
return assembleMembershipEvent(input.statement, { memberSig, adminSig });
|
|
161
|
+
}
|
|
162
|
+
/** 事件 → 落库条目 */
|
|
163
|
+
export function membershipEntryOf(ev) {
|
|
164
|
+
return { kind: GROUP_MEMBERSHIP_KIND, event: ev, ts: ev.ts };
|
|
165
|
+
}
|
|
166
|
+
// ─────────────────────────────────────────────────────────── 验签
|
|
167
|
+
function fail(checks, name, detail) {
|
|
168
|
+
checks.push({ name, ok: false, detail });
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 验一条成员事件 —— **真 Ed25519 验签**, 双侧都验 (成员 + 管理员), 少一侧 = 拒。
|
|
173
|
+
*
|
|
174
|
+
* 检查项 (任何一项不过 → `ok:false`, 但 `checks` 全量列出, 便于定位):
|
|
175
|
+
* ① 协议名 / op / ts / group 形状
|
|
176
|
+
* ② member.did 由 member.publicKeyHex 派生、by.did 由 by.publicKeyHex 派生 (两把钥匙一致)
|
|
177
|
+
* ③ member.orbitdbId 形状 (必须真能被当成写身份)
|
|
178
|
+
* ④ adminSig 对 `canonicalize(statement)` 验签通过 (by.publicKeyHex)
|
|
179
|
+
* ⑤ memberSig 对同一份字节串验签通过 (member.publicKeyHex)
|
|
180
|
+
* ⑥ 若给了 opts.group → 事件必须指向同一个群
|
|
181
|
+
*/
|
|
182
|
+
export async function verifyMembershipEvent(ev, opts = {}) {
|
|
183
|
+
const checks = [];
|
|
184
|
+
const e = ev;
|
|
185
|
+
if (!e || typeof e !== 'object')
|
|
186
|
+
return { ok: false, checks: [{ name: 'shape', ok: false, detail: '事件不是对象' }] };
|
|
187
|
+
checks.push({
|
|
188
|
+
name: 'protocol',
|
|
189
|
+
ok: e.protocol === GROUP_MEMBERSHIP_PROTOCOL,
|
|
190
|
+
detail: `protocol=${String(e.protocol)} (要 ${GROUP_MEMBERSHIP_PROTOCOL})`,
|
|
191
|
+
});
|
|
192
|
+
checks.push({ name: 'op', ok: e.op === 'add' || e.op === 'remove', detail: `op=${String(e.op)}` });
|
|
193
|
+
checks.push({
|
|
194
|
+
name: 'group',
|
|
195
|
+
ok: typeof e.group === 'string' && e.group.startsWith('/orbitdb/'),
|
|
196
|
+
detail: `group=${String(e.group).slice(0, 48)}`,
|
|
197
|
+
});
|
|
198
|
+
checks.push({
|
|
199
|
+
name: 'ts',
|
|
200
|
+
ok: Number.isInteger(e.ts) && e.ts > 0,
|
|
201
|
+
detail: `ts=${String(e.ts)}`,
|
|
202
|
+
});
|
|
203
|
+
const memberOk = (() => {
|
|
204
|
+
try {
|
|
205
|
+
assertMemberRef(e.member, 'member');
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
checks.push({ name: 'member-binding', ok: false, detail: String(err.message) });
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
if (memberOk)
|
|
214
|
+
checks.push({ name: 'member-binding', ok: true, detail: 'member.did ↔ member.publicKeyHex 同钥匙 (did:key 由公钥派生)' });
|
|
215
|
+
let byOk = false;
|
|
216
|
+
try {
|
|
217
|
+
if (!e.by || !DID_KEY_RE.test(String(e.by.did || '')) || !ED25519_PUBKEY_HEX_RE.test(String(e.by.publicKeyHex || ''))) {
|
|
218
|
+
fail(checks, 'by-binding', `by 形状非法 (did=${String(e.by?.did || '').slice(0, 20)})`);
|
|
219
|
+
}
|
|
220
|
+
else if (didFromEd25519PublicKey(e.by.publicKeyHex) !== e.by.did) {
|
|
221
|
+
fail(checks, 'by-binding', 'by.did 与 by.publicKeyHex 不是同一把钥匙');
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
byOk = true;
|
|
225
|
+
checks.push({ name: 'by-binding', ok: true, detail: 'by.did ↔ by.publicKeyHex 同钥匙' });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
fail(checks, 'by-binding', String(err.message));
|
|
230
|
+
}
|
|
231
|
+
// ④⑤ 验签 (把不通过的项也列出来; 形如 sig 缺失直接判否)
|
|
232
|
+
let adminSigOk = false;
|
|
233
|
+
let memberSigOk = false;
|
|
234
|
+
try {
|
|
235
|
+
const payload = membershipSigningPayload(statementOfEvent(e));
|
|
236
|
+
adminSigOk = byOk && typeof e.adminSig === 'string' && e.adminSig.length > 0
|
|
237
|
+
? await ed25519Verify(e.by.publicKeyHex, payload, e.adminSig)
|
|
238
|
+
: false;
|
|
239
|
+
checks.push({
|
|
240
|
+
name: 'adminSig',
|
|
241
|
+
ok: adminSigOk,
|
|
242
|
+
detail: adminSigOk ? '发起人 Ed25519 验签通过' : '发起人签名验签**不通过** (或 by 公钥不可用)',
|
|
243
|
+
});
|
|
244
|
+
memberSigOk = memberOk && typeof e.memberSig === 'string' && e.memberSig.length > 0
|
|
245
|
+
? await ed25519Verify(e.member.publicKeyHex, payload, e.memberSig)
|
|
246
|
+
: false;
|
|
247
|
+
checks.push({
|
|
248
|
+
name: 'memberSig',
|
|
249
|
+
ok: memberSigOk,
|
|
250
|
+
detail: memberSigOk ? '成员 Ed25519 验签通过 (成员同意)' : '成员签名验签**不通过** (成员未同意 / 被改过)',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
fail(checks, 'sig-verify', String(err.message));
|
|
255
|
+
}
|
|
256
|
+
if (opts.group !== undefined) {
|
|
257
|
+
checks.push({
|
|
258
|
+
name: 'group-match',
|
|
259
|
+
ok: e.group === opts.group,
|
|
260
|
+
detail: `事件群=${String(e.group).slice(0, 40)} 期望=${String(opts.group).slice(0, 40)}`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return { ok: checks.every((c) => c.ok), checks };
|
|
264
|
+
}
|
|
265
|
+
// ─────────────────────────────────────────────────────────── 白名单派生
|
|
266
|
+
/** 镜像 `access-controllers/ipfs.js:85` 的判定 (`write.includes(id) || write.includes('*')`) */
|
|
267
|
+
export function writeListAllows(orbitdbId, write) {
|
|
268
|
+
if (!Array.isArray(write))
|
|
269
|
+
return false;
|
|
270
|
+
return write.includes('*') || write.includes(String(orbitdbId));
|
|
271
|
+
}
|
|
272
|
+
/** 去重 + 稳定排序 —— 同一成员集合在任何节点派生出的白名单**逐字节一致** (ACL CID 才会一致) */
|
|
273
|
+
export function normalizeWriteList(ids) {
|
|
274
|
+
const uniq = Array.from(new Set(ids.map((x) => String(x).trim()).filter(Boolean)));
|
|
275
|
+
return uniq.sort();
|
|
276
|
+
}
|
|
277
|
+
const normalizeWrite = normalizeWriteList;
|
|
278
|
+
/**
|
|
279
|
+
* 从群主 + 成员事件**派生** ACL 写名单。
|
|
280
|
+
*
|
|
281
|
+
* 规则 (v1, 刻意简单因此可核验):
|
|
282
|
+
* · 群主恒在名单里, 且**不可被 remove**;
|
|
283
|
+
* · 只有 `by.did === owner.did` 的 add/remove 被接受 (v1 只有群主能改成员);
|
|
284
|
+
* · 事件必须先**验签通过** (`verifyMembershipEvent`) 才参与派生 —— 没验过的事件不改白名单;
|
|
285
|
+
* · 事件按 `ts` 升序应用 (同 ts 按输入顺序), `add` 去重、`remove` 移除 (除群主)。
|
|
286
|
+
* · **不受 ts 单调性以外的时序影响**: 同一批事件 → 同一名单 → 同一 ACL。
|
|
287
|
+
*/
|
|
288
|
+
export async function applyMembershipEvents(input) {
|
|
289
|
+
try {
|
|
290
|
+
assertMemberRef(input.owner, 'owner');
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
throw new Error(`群主身份非法: ${err.message}`);
|
|
294
|
+
}
|
|
295
|
+
const order = input.events.map((e, i) => ({ e, i }))
|
|
296
|
+
.sort((a, b) => (Number(a.e?.ts ?? 0) - Number(b.e?.ts ?? 0)) || (a.i - b.i));
|
|
297
|
+
const accepted = [];
|
|
298
|
+
const rejected = [];
|
|
299
|
+
const members = new Map();
|
|
300
|
+
for (const { e } of order) {
|
|
301
|
+
const v = await verifyMembershipEvent(e, input.group !== undefined ? { group: input.group } : {});
|
|
302
|
+
if (!v.ok) {
|
|
303
|
+
rejected.push({ event: e, reason: 'VERIFY_FAILED: ' + v.checks.filter((c) => !c.ok).map((c) => c.name).join(',') });
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (e.by.did !== input.owner.did) {
|
|
307
|
+
rejected.push({ event: e, reason: `UNAUTHORIZED_ISSUER: ${e.by.did.slice(0, 24)}… 不是群主` });
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (e.op === 'add') {
|
|
311
|
+
if (e.member.orbitdbId === input.owner.orbitdbId) {
|
|
312
|
+
rejected.push({ event: e, reason: 'OWNER_ALREADY_MEMBER' });
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
members.set(e.member.orbitdbId, {
|
|
316
|
+
did: e.member.did, publicKeyHex: e.member.publicKeyHex, orbitdbId: e.member.orbitdbId,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
if (e.member.orbitdbId === input.owner.orbitdbId) {
|
|
321
|
+
rejected.push({ event: e, reason: 'CANNOT_REMOVE_OWNER' });
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
members.delete(e.member.orbitdbId);
|
|
325
|
+
}
|
|
326
|
+
accepted.push(e);
|
|
327
|
+
}
|
|
328
|
+
const memberRefs = Array.from(members.values()).sort((a, b) => (a.orbitdbId < b.orbitdbId ? -1 : a.orbitdbId > b.orbitdbId ? 1 : 0));
|
|
329
|
+
return {
|
|
330
|
+
write: normalizeWrite([input.owner.orbitdbId, ...memberRefs.map((m) => m.orbitdbId)]),
|
|
331
|
+
members: memberRefs,
|
|
332
|
+
accepted,
|
|
333
|
+
rejected,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
// ─────────────────────────────────────────────────────────── ACL 选项 / 变更代价
|
|
337
|
+
/**
|
|
338
|
+
* 把白名单接成 `CIDDatabase.openStore` 认的选项。
|
|
339
|
+
* `cid-database.accessControllerOption()` 会把它翻成 OrbitDB v4 真认的大写
|
|
340
|
+
* `AccessController: IPFSAccessController({ write })` (小写会被静默丢弃 —— 2026-09-24 的坑)。
|
|
341
|
+
*/
|
|
342
|
+
export function groupAccessOptions(write) {
|
|
343
|
+
if (!Array.isArray(write) || write.length === 0)
|
|
344
|
+
throw new Error('白名单不能为空 (空名单会被 accessControllerOption 丢掉 → 落回默认策略)');
|
|
345
|
+
return { accessController: { write: normalizeWrite(write) } };
|
|
346
|
+
}
|
|
347
|
+
/** IPFS 型 AC 的"就地更新"能力 —— 源码事实, 不是推测 */
|
|
348
|
+
export const ACL_INPLACE_UPDATE = {
|
|
349
|
+
ipfsType: {
|
|
350
|
+
inPlace: false,
|
|
351
|
+
why: 'write 在内容寻址的 ACL manifest 里, ACL CID → store manifest → store 地址; 改白名单必然换地址; 包里没有 AC 的 mutate/grant API',
|
|
352
|
+
},
|
|
353
|
+
orbitdbType: {
|
|
354
|
+
inPlace: 'partial',
|
|
355
|
+
why: 'grant/revoke 写一个独立的 AC keyvalue db (可复制); 但那 kv db 自己的根写名单仍冻结在它的 IPFS manifest 里 —— 只有根名单里的人能改; 且增量生效依赖该 kv db 复制到对端',
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
/**
|
|
359
|
+
* 成员变更的执行计划 (诚实版): IPFS 型 AC 下**只能重建**。
|
|
360
|
+
* 把代价逐条写出来, 免得下游把"换地址"当免费操作。
|
|
361
|
+
*/
|
|
362
|
+
export function aclChangePlan(input) {
|
|
363
|
+
const before = normalizeWrite(input.before);
|
|
364
|
+
const after = normalizeWrite(input.after);
|
|
365
|
+
const changed = before.join(',') !== after.join(',');
|
|
366
|
+
return {
|
|
367
|
+
mustRebuild: true,
|
|
368
|
+
strategy: 'rebuild',
|
|
369
|
+
why: ACL_INPLACE_UPDATE.ipfsType.why,
|
|
370
|
+
changed,
|
|
371
|
+
before,
|
|
372
|
+
after,
|
|
373
|
+
costs: [
|
|
374
|
+
'新建 store ⇒ **新地址 (新 manifest CID)**: 老成员手里的群链接/群地址全部作废, 必须重新分发',
|
|
375
|
+
'老地址的写名单**不可追认**: 未在重建时列进去的人, 连老地址都写不了 (老地址本身已经是收紧后的名单)',
|
|
376
|
+
'历史消息**不会自动搬过去**: 新 store 只有新地址的 oplog; 要保留历史必须显式回放 (逐条 add) 或让成员两头都读',
|
|
377
|
+
`本机已落盘的旧 store 不会删除 (占用不回收); 消息条数 ${Number(input.messageCount ?? 0)} 条需要回放才不丢`,
|
|
378
|
+
'重建期间**两边都能写**的窗口不存在: 切换只认新地址, 老地址冻结 (没有双写机制, 别假装有)',
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** 每片最多多少条 —— 直接决定"打开一片"的成本上界 (越小越快, 但片数越多) */
|
|
2
|
+
export const SHARD_SIZE = 200;
|
|
3
|
+
const aclOf = (policy) => policy?.writeList && policy.writeList.length ? { accessController: { write: policy.writeList } } : {};
|
|
4
|
+
/**
|
|
5
|
+
* 片级 store 缓存 (与 src/agents/gateway-group.ts 的 storeCache 同一套 idiom)。
|
|
6
|
+
* 没有它, `appendEvent` 每写一条都会再 openStore 一次 —— 1000 条 = 1000 次重复打开,
|
|
7
|
+
* 既慢又可能把同一个库开出多个句柄。
|
|
8
|
+
*/
|
|
9
|
+
const shardCache = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* 片内条数缓存 (2026-10-01 回归修复)。
|
|
12
|
+
* 轮转判据**必须**用这里 (片自己的真实长度), 不能用 manifest 里的 count ——
|
|
13
|
+
* 我为了消掉 manifest 的 O(N) 把它改成"只在轮转时落盘", 结果 count 永远停在旧值 ⇒ 再也不开新片 (实测 1000 条只有 1 片)。
|
|
14
|
+
* 首次碰到某片时用 `all().length` 对齐一次, 之后逐条自增 (不每条都读一遍)。
|
|
15
|
+
*/
|
|
16
|
+
const countCache = new Map();
|
|
17
|
+
function cached(key, make) {
|
|
18
|
+
const hit = shardCache.get(key);
|
|
19
|
+
if (hit)
|
|
20
|
+
return Promise.resolve(hit);
|
|
21
|
+
return make().then((st) => { shardCache.set(key, st); return st; });
|
|
22
|
+
}
|
|
23
|
+
/** manifest store 名 (keyvalue, 极小: 只有一个 key) */
|
|
24
|
+
export const manifestStoreName = (group) => `bolloon-gw-shards-${group}`;
|
|
25
|
+
/** 第 i 片的 store 名 */
|
|
26
|
+
export const shardStoreName = (group, i) => `bolloon-gw-group-${group}-s${String(i).padStart(3, '0')}`;
|
|
27
|
+
/**
|
|
28
|
+
* 有界等待 manifest 到达 (2026-09-30 实测踩的竞态)。
|
|
29
|
+
* OrbitDB 按地址打开 store 是**异步同步**的: open() 很快返回 (实测 174ms), 但内容要靠 pubsub/取块
|
|
30
|
+
* 慢慢到 —— 立刻 `get('manifest')` 会拿到空。任何真实读者都必须等, 且等待要有界、要如实报"等到没有"。
|
|
31
|
+
*/
|
|
32
|
+
export async function waitForManifest(store, group, opts = {}) {
|
|
33
|
+
const timeoutMs = opts.timeoutMs ?? 20000;
|
|
34
|
+
const pollMs = opts.pollMs ?? 250;
|
|
35
|
+
const t0 = Date.now();
|
|
36
|
+
for (;;) {
|
|
37
|
+
const m = await readManifest(store, group);
|
|
38
|
+
if (m.shards.length > 0)
|
|
39
|
+
return { manifest: m, waitedMs: Date.now() - t0, complete: true };
|
|
40
|
+
if (Date.now() - t0 >= timeoutMs)
|
|
41
|
+
return { manifest: m, waitedMs: Date.now() - t0, complete: false };
|
|
42
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** 只读 manifest: 1 个 key ⇒ 与新节点的历史长度无关 */
|
|
46
|
+
export async function readManifest(store, group) {
|
|
47
|
+
const raw = (await store.get('manifest'));
|
|
48
|
+
if (raw && raw.v === 1 && Array.isArray(raw.shards))
|
|
49
|
+
return raw;
|
|
50
|
+
return { v: 1, group, shards: [], updatedAt: 0 };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 追加一条事件: 写进最后一片; 满了就开新片并把 manifest 指过去。
|
|
54
|
+
* 调用方需先把 manifest store 打开 (keyvalue) — 见 openGroupShards()。
|
|
55
|
+
*/
|
|
56
|
+
export async function appendEvent(db, manifestStore, group, event, now = Date.now(), policy) {
|
|
57
|
+
const manifest = await readManifest(manifestStore, group);
|
|
58
|
+
const openShardByName = (name) => cached(name, () => db.openStore(name, 'events', aclOf(policy)));
|
|
59
|
+
/** 片真实长度: 首次对齐后用缓存自增 */
|
|
60
|
+
const lenOf = async (store, name) => {
|
|
61
|
+
if (!countCache.has(name))
|
|
62
|
+
countCache.set(name, (await store.all()).length);
|
|
63
|
+
return countCache.get(name);
|
|
64
|
+
};
|
|
65
|
+
let last = manifest.shards[manifest.shards.length - 1];
|
|
66
|
+
if (!last) {
|
|
67
|
+
const name = shardStoreName(group, 0);
|
|
68
|
+
const store = await openShardByName(name);
|
|
69
|
+
countCache.set(name, 0);
|
|
70
|
+
last = { index: 0, name, address: store.address, count: 0, fromTs: now, toTs: now };
|
|
71
|
+
manifest.shards.push(last);
|
|
72
|
+
manifest.updatedAt = now;
|
|
73
|
+
await manifestStore.put('manifest', JSON.parse(JSON.stringify(manifest)));
|
|
74
|
+
}
|
|
75
|
+
let store = await openShardByName(last.name);
|
|
76
|
+
let len = await lenOf(store, last.name);
|
|
77
|
+
if (len >= SHARD_SIZE) {
|
|
78
|
+
// 轮转: 开新片 + **这时才落盘 manifest** (每 SHARD_SIZE 条一次 ⇒ manifest oplog 是 O(片数))
|
|
79
|
+
const index = last.index + 1;
|
|
80
|
+
const name = shardStoreName(group, index);
|
|
81
|
+
const st = await openShardByName(name);
|
|
82
|
+
countCache.set(name, 0);
|
|
83
|
+
last = { index, name, address: st.address, count: 0, fromTs: now, toTs: now };
|
|
84
|
+
manifest.shards.push(last);
|
|
85
|
+
manifest.updatedAt = now;
|
|
86
|
+
await manifestStore.put('manifest', JSON.parse(JSON.stringify(manifest)));
|
|
87
|
+
store = st;
|
|
88
|
+
len = 0;
|
|
89
|
+
}
|
|
90
|
+
await store.add(event);
|
|
91
|
+
countCache.set(last.name, len + 1);
|
|
92
|
+
last.count = len + 1;
|
|
93
|
+
last.toTs = now;
|
|
94
|
+
return { shard: last, address: last.address, index: len };
|
|
95
|
+
}
|
|
96
|
+
/** 列出各片 (只读 manifest, **不打开任何片**) */
|
|
97
|
+
export async function listShards(manifestStore, group) {
|
|
98
|
+
return (await readManifest(manifestStore, group)).shards;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 读尾部: 只打开**最后一片** (或不含给定片号的最后一片)。
|
|
102
|
+
* 这就是"打开成本不随总历史增长"的落点。
|
|
103
|
+
*/
|
|
104
|
+
export async function readTail(db, manifestStore, group, opts = {}) {
|
|
105
|
+
const shards = await listShards(manifestStore, group);
|
|
106
|
+
const skip = opts.skipShards ?? 0;
|
|
107
|
+
const target = shards[shards.length - 1 - skip] ?? null;
|
|
108
|
+
if (!target)
|
|
109
|
+
return { entries: [], shard: null, openedShards: 0, waitedMs: 0, complete: false };
|
|
110
|
+
const store = await cached(`addr:${target.address}`, async () => {
|
|
111
|
+
const st = await db.openStoreByAddress(target.address, 'events', aclOf(opts.policy));
|
|
112
|
+
if (!st)
|
|
113
|
+
throw new Error(`分片 ${target.index} 打不开 (${target.address})`);
|
|
114
|
+
return st;
|
|
115
|
+
});
|
|
116
|
+
// 同样是有界等待: 只等**这一片** (片大小有上界 ⇒ 等待时间有上界, 与总历史无关)
|
|
117
|
+
const timeoutMs = opts.waitMs ?? 60000;
|
|
118
|
+
const pollMs = opts.pollMs ?? 250;
|
|
119
|
+
const settleMs = opts.settleMs ?? 2000; // 连续这么久没有新条目 ⇒ 认为这片同步完了
|
|
120
|
+
const t0 = Date.now();
|
|
121
|
+
let lastCount = -1, lastGrowthAt = Date.now();
|
|
122
|
+
for (;;) {
|
|
123
|
+
const entries = await store.all();
|
|
124
|
+
if (entries.length !== lastCount) {
|
|
125
|
+
lastCount = entries.length;
|
|
126
|
+
lastGrowthAt = Date.now();
|
|
127
|
+
}
|
|
128
|
+
const settled = lastCount > 0 && Date.now() - lastGrowthAt >= settleMs;
|
|
129
|
+
if (settled)
|
|
130
|
+
return { entries, shard: target, openedShards: 1, waitedMs: Date.now() - t0, complete: true };
|
|
131
|
+
if (Date.now() - t0 >= timeoutMs)
|
|
132
|
+
return { entries, shard: target, openedShards: 1, waitedMs: Date.now() - t0, complete: false };
|
|
133
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** 按需打开某个旧片 (要读历史时才付它的代价) */
|
|
137
|
+
export async function openShard(db, group, index, policy) {
|
|
138
|
+
const name = shardStoreName(group, index);
|
|
139
|
+
return cached(name, () => db.openStore(name, 'events', aclOf(policy)));
|
|
140
|
+
}
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { createHeliaLight } from 'helia';
|
|
30
30
|
import { withLibp2p } from '@helia/libp2p';
|
|
31
|
+
import { withBitswap } from '@helia/bitswap';
|
|
31
32
|
import { FsBlockstore } from 'blockstore-fs';
|
|
32
33
|
import { FsDatastore } from 'datastore-fs';
|
|
33
34
|
import * as dagCbor from '@ipld/dag-cbor';
|
|
@@ -69,7 +70,21 @@ export async function createBolloonIpfs(dataDir) {
|
|
|
69
70
|
// createHeliaLight 无 libp2p → withLibp2p 手动装配 (可传 services)
|
|
70
71
|
// codecs/hashers 照抄 createHelia 默认: OrbitDB 的 log entry 用 dag-cbor (codec 113),
|
|
71
72
|
// 不注册会报 "Could not load codec for 113"
|
|
72
|
-
|
|
73
|
+
// 2026-09-30 (P0b 实验): "轻客户端"开关。P0b 实测到一个**与条数无关的 ~25-30s 固定停顿**
|
|
74
|
+
// (连"只有 1 条的 keyvalue store"新节点都要 25.3s 才看见内容; 历次出现 打开=30002ms),
|
|
75
|
+
// 怀疑是每次取块前的提供者查找 (kadDHT / delegated routing 两个客户端) 在超时。
|
|
76
|
+
// 打开 BOLLOON_ORBITDB_LEAN_ROUTING=1 ⇒ 不带 DHT/delegated/autoTLS, 只留 pubsub+identify+ping+relay。
|
|
77
|
+
// 这既是一个诊断开关, 也是"轻量客户端"该有的形态 (手机/低配节点不需要当 DHT 服务器)。
|
|
78
|
+
const leanRouting = process.env.BOLLOON_ORBITDB_LEAN_ROUTING === '1' || process.env.BOLLOON_ORBITDB_LEAN_ROUTING === 'true';
|
|
79
|
+
// 2026-10-01: 反事实需要"真隔离"。节点默认带 mDNS ⇒ 同机/同网的 peer **不拨号也会被自动发现并连上**
|
|
80
|
+
// ⇒ P0 的 S3(「不拨号必须看不见」)曾经假红/假绿都出现过。做隔离实验时打开这个开关。
|
|
81
|
+
const noMdns = process.env.BOLLOON_ORBITDB_NO_MDNS === '1' || process.env.BOLLOON_ORBITDB_NO_MDNS === 'true';
|
|
82
|
+
// 2026-10-01: "全静默"隔离档。S3 那条反事实(不拨号必须看不见)在只关 mDNS 时**不稳定**
|
|
83
|
+
// (同一份代码一轮 seen=0 一轮 seen=101) ⇒ 说明还有服务在自发建连。这一档只留
|
|
84
|
+
// pubsub + identify + ping, 其余(autoNAT/relay/dcutr/upnp/mdns/dht/delegated/autoTLS)全关。
|
|
85
|
+
// 用途: 反事实隔离实验, 以及"最小可用节点"的形态参考。
|
|
86
|
+
const isolated = process.env.BOLLOON_ORBITDB_ISOLATED === '1' || process.env.BOLLOON_ORBITDB_ISOLATED === 'true';
|
|
87
|
+
const heliaWithLibp2p = withLibp2p(createHeliaLight({
|
|
73
88
|
blockstore,
|
|
74
89
|
datastore,
|
|
75
90
|
codecs: [dagCbor, dagJson, json],
|
|
@@ -78,22 +93,32 @@ export async function createBolloonIpfs(dataDir) {
|
|
|
78
93
|
// 显式列出服务: createLibp2p 浅合并会覆盖默认 services
|
|
79
94
|
services: {
|
|
80
95
|
pubsub: gossipsub({ emitSelf: true }), // OrbitDB 同步必需; emitSelf 让单机也能 publish (否则 NoPeersSubscribedToTopic)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
...(isolated ? {} : {
|
|
97
|
+
autoNAT: autoNAT(),
|
|
98
|
+
dcutr: dcutr(),
|
|
99
|
+
relay: circuitRelayServer(),
|
|
100
|
+
upnp: uPnPNAT(),
|
|
101
|
+
http: http(),
|
|
102
|
+
}),
|
|
103
|
+
...(isolated || leanRouting ? {} : { autoTLS: autoTLS() }),
|
|
104
|
+
...(isolated || leanRouting ? {} : {
|
|
105
|
+
delegatedPeerRouting: delegatedRoutingV1HttpApiClientPeerRouting(delegatedHTTPRoutingDefaults()),
|
|
106
|
+
delegatedContentRouting: delegatedRoutingV1HttpApiClientContentRouting(delegatedHTTPRoutingDefaults()),
|
|
107
|
+
dht: kadDHT(),
|
|
108
|
+
}),
|
|
87
109
|
identify: identify(),
|
|
88
110
|
identifyPush: identifyPush(),
|
|
89
111
|
keychain: keychain({ pass: 'bolloon-orbitdb-keychain-pass-2026' }),
|
|
90
112
|
ping: ping(),
|
|
91
|
-
|
|
92
|
-
upnp: uPnPNAT(),
|
|
93
|
-
mdns: mdns(),
|
|
94
|
-
http: http(),
|
|
113
|
+
...(isolated || noMdns ? {} : { mdns: mdns() }),
|
|
95
114
|
},
|
|
96
115
|
});
|
|
116
|
+
// 2026-09-30 (P0 真两节点验收照出的根因): 在此之前节点**没有任何 block broker** ——
|
|
117
|
+
// 新节点按地址打开群时连 manifest/oplog 的块都拉不到, 报
|
|
118
|
+
// "No block brokers capable of retrieving blocks are configured, the CID bafyrei… cannot be fetched",
|
|
119
|
+
// 也就是「跨机群消息复制」从来就不可能。此前 5 个"多节点"测试都注入 fake CIDDatabase, 照不出来。
|
|
120
|
+
// createHeliaLight 不带 bitswap, withLibp2p 也不加 ⇒ 必须显式 withBitswap (顺序: 在 libp2p 之后)。
|
|
121
|
+
const helia = withBitswap(heliaWithLibp2p);
|
|
97
122
|
await helia.start();
|
|
98
123
|
const peerId = helia.libp2p.peerId.toString();
|
|
99
124
|
return {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "bolloon-mobile-web/1",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"channel": "stable",
|
|
5
5
|
"build": "stable",
|
|
6
|
-
"commit": "
|
|
7
|
-
"builtAt": "2026-
|
|
6
|
+
"commit": "02aa1a6367f30fb4d85f52c360d8cd457f852ddd",
|
|
7
|
+
"builtAt": "2026-10-01T13:11:22.558Z",
|
|
8
8
|
"layer": "web",
|
|
9
9
|
"note": "web 资源层构建戳 (手机端 OTA 的身份第 ③ 级回退; 装机后以 OTA state.json 为准)"
|
|
10
10
|
}
|
package/dist/web/client.js
CHANGED
|
@@ -383,7 +383,7 @@
|
|
|
383
383
|
function parseToolCall(content, ctx) {
|
|
384
384
|
if (!content) return null;
|
|
385
385
|
const strippedContent = content.replace(/<think[\s\S]*?<\/think/g, "");
|
|
386
|
-
try {
|
|
386
|
+
if (PARSE_DIAG_ON) try {
|
|
387
387
|
const result = /* @__PURE__ */ (function _diagProbe() {
|
|
388
388
|
return null;
|
|
389
389
|
})();
|
|
@@ -571,11 +571,12 @@
|
|
|
571
571
|
}
|
|
572
572
|
return null;
|
|
573
573
|
}
|
|
574
|
-
var SHELL_KEYWORDS;
|
|
574
|
+
var SHELL_KEYWORDS, PARSE_DIAG_ON;
|
|
575
575
|
var init_parse_tool_call = __esm({
|
|
576
576
|
"src/agents/parse-tool-call.ts"() {
|
|
577
577
|
"use strict";
|
|
578
578
|
SHELL_KEYWORDS = ["git", "npx", "npm", "tsx", "tsc", "vitest", "node", "mkdir", "touch", "ls", "echo", "cat", "head", "tail", "wc", "pwd", "date"];
|
|
579
|
+
PARSE_DIAG_ON = process.env.BOLLOON_PARSE_DIAG === "1" || process.env.BOLLOON_VERBOSE === "1";
|
|
579
580
|
}
|
|
580
581
|
});
|
|
581
582
|
|