@bolloon/bolloon-agent 0.6.1 → 0.6.3

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.
Files changed (78) hide show
  1. package/README.md +128 -111
  2. package/dist/agents/gateway-group.js +231 -6
  3. package/dist/agents/group-autopilot.js +256 -0
  4. package/dist/agents/pi-sdk-tools.js +302 -0
  5. package/dist/agents/pi-sdk.js +89 -140
  6. package/dist/agents/task-group.js +21 -4
  7. package/dist/cli/log-gate.js +68 -4
  8. package/dist/cli/mention-data.js +1 -1
  9. package/dist/cli/model-selector.js +84 -104
  10. package/dist/cli/setup-wizard.js +2 -10
  11. package/dist/cli/tui-select.js +99 -10
  12. package/dist/cli-entry.js +1 -9
  13. package/dist/electron-build/agents/gateway-group.js +814 -0
  14. package/dist/electron-build/agents/gateway-group.js.map +1 -0
  15. package/dist/electron-build/agents/group-autopilot.js +302 -0
  16. package/dist/electron-build/agents/group-autopilot.js.map +1 -0
  17. package/dist/electron-build/agents/pi-sdk-tools.js +302 -0
  18. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  19. package/dist/electron-build/agents/pi-sdk.js +88 -139
  20. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  21. package/dist/electron-build/agents/task-group.js +601 -0
  22. package/dist/electron-build/agents/task-group.js.map +1 -0
  23. package/dist/electron-build/agents/task-public-text.js +97 -0
  24. package/dist/electron-build/agents/task-public-text.js.map +1 -0
  25. package/dist/electron-build/efficode/compress.js +86 -0
  26. package/dist/electron-build/efficode/compress.js.map +1 -0
  27. package/dist/electron-build/efficode/crc32.js +44 -0
  28. package/dist/electron-build/efficode/crc32.js.map +1 -0
  29. package/dist/electron-build/efficode/did.js +74 -0
  30. package/dist/electron-build/efficode/did.js.map +1 -0
  31. package/dist/electron-build/efficode/lz77.js +175 -0
  32. package/dist/electron-build/efficode/lz77.js.map +1 -0
  33. package/dist/electron-build/efficode/negotiate.js +233 -0
  34. package/dist/electron-build/efficode/negotiate.js.map +1 -0
  35. package/dist/electron-build/efficode/ops.js +96 -0
  36. package/dist/electron-build/efficode/ops.js.map +1 -0
  37. package/dist/electron-build/efficode/packet.js +279 -0
  38. package/dist/electron-build/efficode/packet.js.map +1 -0
  39. package/dist/electron-build/efficode/types.js +50 -0
  40. package/dist/electron-build/efficode/types.js.map +1 -0
  41. package/dist/electron-build/kernel/code-write-gate.js +55 -0
  42. package/dist/electron-build/kernel/code-write-gate.js.map +1 -0
  43. package/dist/electron-build/kernel/run-lifecycle.js +23 -0
  44. package/dist/electron-build/kernel/run-lifecycle.js.map +1 -1
  45. package/dist/electron-build/kernel/run-resume.js +62 -0
  46. package/dist/electron-build/kernel/run-resume.js.map +1 -0
  47. package/dist/electron-build/kernel/session-lifecycle.js +223 -0
  48. package/dist/electron-build/kernel/session-lifecycle.js.map +1 -0
  49. package/dist/electron-build/kernel/turn-selfcheck.js +65 -0
  50. package/dist/electron-build/kernel/turn-selfcheck.js.map +1 -0
  51. package/dist/electron-build/llm/config-store.js +2 -2
  52. package/dist/electron-build/llm/config-store.js.map +1 -1
  53. package/dist/electron-build/llm/model-selection.js +16 -2
  54. package/dist/electron-build/llm/model-selection.js.map +1 -1
  55. package/dist/electron-build/llm/pi-ai.js +2 -2
  56. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  57. package/dist/electron-build/orbitdb/cid-database.js +98 -19
  58. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  59. package/dist/electron-build/orbitdb/group-access.js +399 -0
  60. package/dist/electron-build/orbitdb/group-access.js.map +1 -0
  61. package/dist/index.js +143 -10
  62. package/dist/ios/bolloon-web.json +3 -3
  63. package/dist/ios/sw.js +1 -1
  64. package/dist/kernel/code-write-gate.js +47 -0
  65. package/dist/kernel/gate-scan.js +5 -1
  66. package/dist/kernel/plan-harness.js +3 -2
  67. package/dist/kernel/roster.js +6 -6
  68. package/dist/kernel/run-lifecycle.js +21 -0
  69. package/dist/kernel/run-resume.js +58 -0
  70. package/dist/kernel/session-lifecycle.js +210 -0
  71. package/dist/kernel/turn-selfcheck.js +60 -0
  72. package/dist/llm/config-store.js +2 -2
  73. package/dist/llm/model-selection.js +16 -2
  74. package/dist/llm/pi-ai.js +2 -2
  75. package/dist/orbitdb/cid-database.js +95 -19
  76. package/dist/web/bolloon-web.json +3 -3
  77. package/dist/web/sw.js +1 -1
  78. package/package.json +1 -1
@@ -0,0 +1,814 @@
1
+ "use strict";
2
+ /**
3
+ * gateway-group.ts — Agent Gateway P2P 群组 (2026-08-14)
4
+ *
5
+ * 群组 = OrbitDB events store, 全成员通过 OrbitDB pubsub 复制实时同步.
6
+ * 链接: orbitdb://<addr>?type=group&name=<群名>.
7
+ *
8
+ * 符合用户习惯: 群组 = 微信式群聊 — 加入链接即进群, 发消息全网同步.
9
+ * 持久化: ~/.bolloon/gateway-groups.json (重启后仍是群成员, 自动重开 store).
10
+ *
11
+ * 2026-09-24 跨进程可重开:
12
+ * · 群消息 (OrbitDB events store 的 oplog 条目 + manifest) 现在真落盘 ——
13
+ * `createBolloonIpfs` 用 FsBlockstore/FsDatastore 写 ~/.bolloon/orbitdb/ipfs/{blocks,datastore},
14
+ * 所以"建群的那个进程退出后, 新进程用群链接重开还能读到既有消息"。
15
+ * (修复前: 区块只在内存, 新进程一开就报 "No block brokers capable of retrieving blocks")
16
+ * · 读不到必须报读不到: `groupMessages` 在 store 打不开时**抛 GroupStoreUnreachableError**,
17
+ * 不再返回 `[]`。把"读不到"显示成"群里没有消息"是骗人, 本条不可回退。
18
+ *
19
+ * 2026-10-01 (P1) DID 门控:
20
+ * · `createGroup(name, { gate: { owner, members?, membershipEvents? } })` 建的是**门控群**:
21
+ * 写入白名单 = 群主 OrbitDB 写身份 + 显式成员, 非名单成员 add 会被拒。
22
+ * · 不给 `gate` → 行为**一字不变** (write:['*']); 默认翻成门控需要先迁移全部既有调用点。
23
+ * · 成员变更 = `MembershipEvent` (成员自签 + 群主签, 双侧 Ed25519 验签), 读写入口
24
+ * `recordMembershipEvent` / `groupMembershipEvents` / `groupWriteList`。
25
+ * · **白名单不能就地改** (IPFS 型 AC 的白名单在内容寻址的 ACL 块里, 改它 = 换 store 地址),
26
+ * 所以"改成员"= 重建 store; 代价清单见 `group-access.ts` 的 `aclChangePlan()`。
27
+ */
28
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ var desc = Object.getOwnPropertyDescriptor(m, k);
31
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
32
+ desc = { enumerable: true, get: function() { return m[k]; } };
33
+ }
34
+ Object.defineProperty(o, k2, desc);
35
+ }) : (function(o, m, k, k2) {
36
+ if (k2 === undefined) k2 = k;
37
+ o[k2] = m[k];
38
+ }));
39
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
40
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
41
+ }) : function(o, v) {
42
+ o["default"] = v;
43
+ });
44
+ var __importStar = (this && this.__importStar) || (function () {
45
+ var ownKeys = function(o) {
46
+ ownKeys = Object.getOwnPropertyNames || function (o) {
47
+ var ar = [];
48
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
49
+ return ar;
50
+ };
51
+ return ownKeys(o);
52
+ };
53
+ return function (mod) {
54
+ if (mod && mod.__esModule) return mod;
55
+ var result = {};
56
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
57
+ __setModuleDefault(result, mod);
58
+ return result;
59
+ };
60
+ })();
61
+ Object.defineProperty(exports, "__esModule", { value: true });
62
+ exports.GroupStoreUnreachableError = void 0;
63
+ exports.setGroupTestDb = setGroupTestDb;
64
+ exports.resetGroupState = resetGroupState;
65
+ exports.closeGroupResources = closeGroupResources;
66
+ exports.parseGroupLink = parseGroupLink;
67
+ exports.detectGroupLink = detectGroupLink;
68
+ exports.onGroupMessage = onGroupMessage;
69
+ exports.createGroup = createGroup;
70
+ exports.joinGroup = joinGroup;
71
+ exports.listGroups = listGroups;
72
+ exports.leaveGroup = leaveGroup;
73
+ exports.groupMessages = groupMessages;
74
+ exports.groupMembers = groupMembers;
75
+ exports.groupMembershipEvents = groupMembershipEvents;
76
+ exports.recordMembershipEvent = recordMembershipEvent;
77
+ exports.groupWriteList = groupWriteList;
78
+ exports.groupSend = groupSend;
79
+ exports.groupSendWithLang = groupSendWithLang;
80
+ exports.readGroupMessage = readGroupMessage;
81
+ exports.groupLink = groupLink;
82
+ exports.groupInfo = groupInfo;
83
+ exports.restoreGroups = restoreGroups;
84
+ exports.rebuildGroupWithWriteList = rebuildGroupWithWriteList;
85
+ exports.inviteMember = inviteMember;
86
+ exports.kickMember = kickMember;
87
+ exports.setGroupPrivacy = setGroupPrivacy;
88
+ exports.groupStatus = groupStatus;
89
+ const os = __importStar(require("os"));
90
+ const path = __importStar(require("path"));
91
+ const cid_database_js_1 = require("../orbitdb/cid-database.js");
92
+ // 2026-09-28: 交流语言 (Efficode 是可选项, 不是默认项) —— 只有双方都声明才用
93
+ const negotiate_js_1 = require("../efficode/negotiate.js");
94
+ // 2026-10-01 (P1): **DID 门控** —— 群写入白名单从 write:['*'] 收紧成成员白名单。
95
+ // 白名单里放的是 **OrbitDB 写身份 id** (66 位压缩 secp256k1 公钥 hex), 不是 DID 字符串
96
+ // (canAppend 只比 identity.id; 放 DID 会永远匹配不上 → 谁都写不了)。
97
+ // 成员变更 = 一条**双侧 Ed25519 验签**的群事件 (成员自签 + 群主签), 细节见 group-access.ts。
98
+ const group_access_js_1 = require("../orbitdb/group-access.js");
99
+ // ============ 依赖注入 (测试用, 避免单测起真实 OrbitDB 节点) ============
100
+ let _dbOverride = null;
101
+ /** 测试用: 注入 fake CIDDatabase */
102
+ function setGroupTestDb(db) { _dbOverride = db; }
103
+ function getDb() { return _dbOverride ?? (0, cid_database_js_1.getCIDDatabase)(); }
104
+ /** 测试用: 清空 store 缓存 / 订阅 (避免测试间污染) */
105
+ function resetGroupState() {
106
+ storeCache.clear();
107
+ onChangeCallbacks.clear();
108
+ openFailures.clear();
109
+ }
110
+ /**
111
+ * 关闭群聊使用的 OrbitDB/Helia 资源。
112
+ * 一次性 CLI、验收探针和测试必须在读回后调用它,否则底层 libp2p/Helia 句柄
113
+ * 会继续保持事件循环活跃,表现为“消息已读完但进程不退出”。
114
+ * 测试注入的 fake DB 不关闭,避免破坏测试夹具生命周期。
115
+ */
116
+ async function closeGroupResources() {
117
+ storeCache.clear();
118
+ onChangeCallbacks.clear();
119
+ openFailures.clear();
120
+ if (_dbOverride)
121
+ return;
122
+ await (0, cid_database_js_1.getCIDDatabase)().close();
123
+ }
124
+ /**
125
+ * 群 store 打不开 (区块不在本机 / 地址不可解析)。
126
+ *
127
+ * 2026-09-24: 以前打不开 → `groupMessages` 返回 `[]`, 于是 CLI 把"整个群读不到"报成
128
+ * "群里本期没有过程痕迹 (群里真没有, 不是读失败)" —— 把"读不到"说成了"没有"。
129
+ * 现在读不到就抛: 上游 (task trail / API) 必须如实报 TRANSPORT_FAILED。
130
+ */
131
+ class GroupStoreUnreachableError extends Error {
132
+ groupId;
133
+ address;
134
+ cause;
135
+ code = 'STORE_UNREACHABLE';
136
+ constructor(groupId, address, cause) {
137
+ const why = String(cause?.message ?? cause ?? '未知原因').slice(0, 160);
138
+ super(`群组 store 不可达 (${groupId}): ${why}`);
139
+ this.groupId = groupId;
140
+ this.address = address;
141
+ this.cause = cause;
142
+ this.name = 'GroupStoreUnreachableError';
143
+ }
144
+ }
145
+ exports.GroupStoreUnreachableError = GroupStoreUnreachableError;
146
+ /** 打开失败的一句话原因 (给人类输出用; 没有失败记录时返回通用话术) */
147
+ function unreachableReason(err) {
148
+ const m = String(err?.message ?? err ?? '').trim();
149
+ return m ? m.slice(0, 160) : '区块不在本机且没有可用的 block broker (跨机同步仍是另一回事, 需 peers)';
150
+ }
151
+ // ============ 持久化 ============
152
+ const groupsFile = () => path.join(os.homedir() || '/tmp', '.bolloon', 'gateway-groups.json');
153
+ async function loadGroups() {
154
+ try {
155
+ const { readFile } = await Promise.resolve().then(() => __importStar(require('fs/promises')));
156
+ const parsed = JSON.parse(await readFile(groupsFile(), 'utf-8'));
157
+ return Array.isArray(parsed) ? parsed : [];
158
+ }
159
+ catch {
160
+ return [];
161
+ }
162
+ }
163
+ async function saveGroups(list) {
164
+ try {
165
+ const { mkdir, writeFile } = await Promise.resolve().then(() => __importStar(require('fs/promises')));
166
+ await mkdir(path.dirname(groupsFile()), { recursive: true });
167
+ await writeFile(groupsFile(), JSON.stringify(list, null, 2), 'utf-8');
168
+ }
169
+ catch { /* 持久化失败静默 */ }
170
+ }
171
+ // ============ 链接解析 ============
172
+ /** 解析群组链接: orbitdb://<addr>?type=group&name=<群名> (type 非 group 返回 null) */
173
+ function parseGroupLink(link) {
174
+ const l = String(link || '').trim();
175
+ if (!l.startsWith('orbitdb://'))
176
+ return null;
177
+ const [base, query] = l.split('?');
178
+ let params = null;
179
+ try {
180
+ params = query ? new URLSearchParams(query) : null;
181
+ }
182
+ catch { /* 忽略 */ }
183
+ if (params && params.get('type') === 'group') {
184
+ let address = base.slice('orbitdb://'.length);
185
+ if (!address.startsWith('/'))
186
+ address = `/${address}`;
187
+ return { address, name: params.get('name') || undefined };
188
+ }
189
+ return null;
190
+ }
191
+ /** 从消息文本检测群组链接 */
192
+ function detectGroupLink(text) {
193
+ const t = String(text || '');
194
+ const re = /(orbitdb:\/\/\/?orbitdb\/[^\s)'"<>,。;]*\?[^\s)'"<>,。;]*type=group[^\s)'"<>,。;]*)/;
195
+ const m = re.exec(t);
196
+ return m ? m[1].trim() : null;
197
+ }
198
+ // ============ store 缓存 (避免重复 open) ============
199
+ const storeCache = new Map();
200
+ /** 订阅回调注册 (server 层挂 SSE 广播) */
201
+ const onChangeCallbacks = new Map();
202
+ /** store 打开失败的原始原因 (按 groupId) —— 用来把"打不开"和"没有消息"分开 */
203
+ const openFailures = new Map();
204
+ function groupIdOf(address) {
205
+ // /orbitdb/zdpu... → zdpu... (地址后 12 位做短 id)
206
+ const m = /\/orbitdb\/(.{8,})/.exec(address);
207
+ return m ? m[1] : address;
208
+ }
209
+ /**
210
+ * 打开群组 store (缓存)。
211
+ * 打不开 → 记下原始原因 + 返回 null (不抛: 调用方各自决定用什么话术报)。
212
+ * 2026-09-24: 必须 try/catch —— openStoreByAddress 现在抛 OrbitDBStoreUnreachableError。
213
+ *
214
+ * 2026-10-01 (P1): **不再传 `accessController`**。打开一个**合法已存在地址**时, OrbitDB
215
+ * 从 manifest 里取回 ACL 并覆盖入参 (`@orbitdb/core` src/orbitdb.js:129-131), 所以这里
216
+ * 传 `write:['*']` 是**死代码** —— 之前那行会让人误以为"打开时放开成任何人可写"。
217
+ * 真正的写权限 = 建群时烧进 manifest 的白名单 (DID 门控群 = 成员白名单)。
218
+ * 能不能写由 manifest 决定 + 调用方是否 add/put 决定; 本函数不做权限判定。
219
+ */
220
+ async function openGroupStore(address) {
221
+ const id = groupIdOf(address);
222
+ if (storeCache.has(id))
223
+ return storeCache.get(id);
224
+ const db = getDb();
225
+ let store = null;
226
+ try {
227
+ store = await db.openStoreByAddress(address, 'events', {});
228
+ }
229
+ catch (e) {
230
+ openFailures.set(id, e);
231
+ return null;
232
+ }
233
+ if (!store) {
234
+ openFailures.set(id, new Error('openStoreByAddress 返回 null (没拿到 store, 也没给出原因)'));
235
+ return null;
236
+ }
237
+ openFailures.delete(id);
238
+ storeCache.set(id, store);
239
+ // 订阅: 新消息 → 通知回调 (server SSE)
240
+ store.onChange(() => {
241
+ groupMessages(id, 5).then((msgs) => {
242
+ if (msgs.length === 0)
243
+ return;
244
+ const cb = onChangeCallbacks.get(id);
245
+ if (cb)
246
+ for (const fn of cb) {
247
+ try {
248
+ fn(msgs[msgs.length - 1]);
249
+ }
250
+ catch { /* 忽略 */ }
251
+ }
252
+ }).catch(() => { });
253
+ });
254
+ return store;
255
+ }
256
+ /** 注册群组消息回调 (返回退订函数) */
257
+ function onGroupMessage(groupId, fn) {
258
+ if (!onChangeCallbacks.has(groupId))
259
+ onChangeCallbacks.set(groupId, new Set());
260
+ onChangeCallbacks.get(groupId).add(fn);
261
+ return () => { onChangeCallbacks.get(groupId)?.delete(fn); };
262
+ }
263
+ async function createGroup(name, opts) {
264
+ const groupName = String(name || '').trim() || `group-${Date.now().toString(36).slice(-4)}`;
265
+ try {
266
+ const db = getDb();
267
+ // ---- 门控分支: 先派生白名单 (验签不过就直接拒), 再建 store ----
268
+ // null = 不传 write 列表 ⇒ OrbitDB 默认 (创建者独占). 不再默认 ['*'].
269
+ let writeList = null;
270
+ let gateInfo = {};
271
+ let membershipEntries = [];
272
+ if (opts?.gate) {
273
+ const gate = opts.gate;
274
+ const applied = await (0, group_access_js_1.applyMembershipEvents)({
275
+ owner: gate.owner,
276
+ events: gate.membershipEvents ?? [],
277
+ });
278
+ if (applied.rejected.length > 0) {
279
+ return {
280
+ ok: false,
281
+ error: `拒绝据此建群: ${applied.rejected.length} 条成员事件未通过验签/授权 —— ${applied.rejected[0].reason}`,
282
+ };
283
+ }
284
+ // 白名单 = 群主 ∪ 事件派生成员 ∪ 直接点名成员
285
+ const ids = [
286
+ gate.owner.orbitdbId,
287
+ ...applied.members.map((m) => m.orbitdbId),
288
+ ...(gate.members ?? []).map((m) => m.orbitdbId),
289
+ ];
290
+ writeList = (0, group_access_js_1.normalizeWriteList)(ids);
291
+ gateInfo = { gated: true, ownerDid: gate.owner.did, ownerPublicKeyHex: gate.owner.publicKeyHex, aclWrite: writeList };
292
+ membershipEntries = applied.accepted.map(group_access_js_1.membershipEntryOf);
293
+ }
294
+ else if (opts?.acl === 'open') {
295
+ // 显式声明开放写入 (产品语义: 谁拿到邀请链接都能发言) —— 打一行 warn 留痕
296
+ writeList = ['*'];
297
+ console.warn(`[group] ${groupName}: acl=open ⇒ 任何人可写 (调用方显式声明)`);
298
+ }
299
+ else {
300
+ console.warn(`[group] ${groupName}: 未声明 acl ⇒ 创建者独占写入 (2026-10-01 起的默认)`);
301
+ }
302
+ const store = await db.openStore(`bolloon-gw-group-${groupName}`, 'events',
303
+ // 注意: groupAccessOptions([]) 会抛错 (空名单会被 accessControllerOption 丢掉 ⇒ 落回默认),
304
+ // 所以"创建者独占"这条路只能**不传** AC 选项 ⇒ 传 {}.
305
+ writeList ? (0, group_access_js_1.groupAccessOptions)(writeList) : {});
306
+ const address = store.address;
307
+ const id = groupIdOf(address);
308
+ const link = `orbitdb://${address}?type=group&name=${encodeURIComponent(groupName)}`;
309
+ const info = {
310
+ id, name: groupName, address, link,
311
+ createdAt: new Date().toISOString(),
312
+ lastSyncAt: new Date().toISOString(),
313
+ // 2026-10-05: 隐私群标记 —— 建群默认隐私 (白名单/独占写); acl=open 才是公开
314
+ privacy: opts?.acl !== 'open',
315
+ ...gateInfo,
316
+ };
317
+ storeCache.set(id, store);
318
+ // 成员变更事件先落库 (它们是"谁能写"的凭证; 带 kind 字段, 不会被当成聊天消息)
319
+ for (const entry of membershipEntries)
320
+ await store.add(entry);
321
+ // 欢迎消息 (群主自我介绍)
322
+ const from = opts?.from || gateInfo.ownerDid || 'group-owner';
323
+ await store.add({ from, text: opts?.hello || `📢 群主创建了群「${groupName}」, 分享链接邀请成员加入`, ts: Date.now() });
324
+ const groups = await loadGroups();
325
+ await saveGroups([...groups.filter((g) => g.id !== id), info]);
326
+ return { ok: true, group: info };
327
+ }
328
+ catch (e) {
329
+ return { ok: false, error: `创建群组失败: ${String(e?.message || e).slice(0, 160)}` };
330
+ }
331
+ }
332
+ /**
333
+ * 通过链接加入群组: 打开 store (可写) + 持久化 + 幂等.
334
+ * 失败静默返回错误, 不影响本地.
335
+ */
336
+ async function joinGroup(link) {
337
+ const parsed = parseGroupLink(link);
338
+ if (!parsed) {
339
+ return { ok: false, error: '不是群组链接 (需要 orbitdb://...?type=group&name=...)' };
340
+ }
341
+ // 幂等: 按地址去重
342
+ const existing = await loadGroups();
343
+ const id = groupIdOf(parsed.address);
344
+ if (existing.some((g) => g.id === id)) {
345
+ return { ok: true, already: true, group: existing.find((g) => g.id === id) };
346
+ }
347
+ const store = await openGroupStore(parsed.address);
348
+ if (!store) {
349
+ return {
350
+ ok: false,
351
+ code: 'STORE_UNREACHABLE',
352
+ error: `群组 store 不可达: ${unreachableReason(openFailures.get(id))}`,
353
+ };
354
+ }
355
+ const name = parsed.name || id.slice(0, 12);
356
+ const info = {
357
+ id, name, address: parsed.address,
358
+ link: `orbitdb://${parsed.address}?type=group&name=${encodeURIComponent(name)}`,
359
+ createdAt: new Date().toISOString(),
360
+ lastSyncAt: new Date().toISOString(),
361
+ };
362
+ await saveGroups([...existing, info]);
363
+ return { ok: true, group: info };
364
+ }
365
+ /** 列出已加入的群组 */
366
+ async function listGroups() {
367
+ return loadGroups();
368
+ }
369
+ /**
370
+ * 退群 (2026-09-24): 从本机群列表里摘掉 (撤销"我是成员"这条本地事实)。
371
+ *
372
+ * 只改**本机**记录: 群 store 本身是公共 append-only 的, 别人那边的成员表/消息
373
+ * 不会因为本机退出而改 (本模块没有"踢人/解散"的权限, 也不假装有)。
374
+ * 返回 `removed` = 被摘掉的 groupId (按 id 或群名匹配)。
375
+ *
376
+ * 注意: leave 只摘本地群列表;需要结束进程资源时调用 `closeGroupResources()`。
377
+ */
378
+ async function leaveGroup(idOrName) {
379
+ const raw = String(idOrName || '').trim();
380
+ if (!raw)
381
+ return { ok: false, error: '缺少 groupId (或群名)' };
382
+ const groups = await loadGroups();
383
+ const hit = groups.find((g) => g.id === raw) || groups.find((g) => g.name === raw) || null;
384
+ if (!hit)
385
+ return { ok: false, error: `本机没有这个群: ${raw} (既不是已加入群的 groupId, 也不是群名)` };
386
+ storeCache.delete(hit.id);
387
+ openFailures.delete(hit.id);
388
+ await saveGroups(groups.filter((g) => g.id !== hit.id));
389
+ return { ok: true, removed: hit.id };
390
+ }
391
+ /**
392
+ * 获取群组 store 的最新消息 (ts 升序, 取最后 N 条)。
393
+ *
394
+ * 两个"空"必须分开 (2026-09-24):
395
+ * · 本机没这个群 (不在群列表里) → 返回 `[]` (上游 resolveGroupRef 已单独拦成 NOT_FOUND)
396
+ * · 群在列表里但 store 打不开 → **抛 GroupStoreUnreachableError** (读不到 ≠ 没有消息)
397
+ */
398
+ async function groupMessages(groupId, limit = 50) {
399
+ let store = storeCache.get(groupId) ?? null;
400
+ if (!store) {
401
+ const groups = await loadGroups();
402
+ const g = groups.find((x) => x.id === groupId);
403
+ if (!g)
404
+ return [];
405
+ store = await openGroupStore(g.address);
406
+ if (!store)
407
+ throw new GroupStoreUnreachableError(groupId, g.address, openFailures.get(groupId));
408
+ }
409
+ const all = await store.all();
410
+ const msgs = [];
411
+ for (const entry of all) {
412
+ const v = entry.value;
413
+ if (v && typeof v.text === 'string' && typeof v.from === 'string') {
414
+ const m = { from: v.from, text: v.text, ts: typeof v.ts === 'number' ? v.ts : 0 };
415
+ // 只透传字符串形态的语言声明; 别的形状一律当"没声明"(不让脏值进到解码分派)
416
+ if (typeof v.lang === 'string' && v.lang.trim())
417
+ m.lang = v.lang.trim();
418
+ // 2026-10-02: 新字段一律**先验形状再透传** (脏值当没声明, 不做修补)
419
+ const entryId = String(entry?.hash ?? entry?.id ?? '').trim();
420
+ if (entryId)
421
+ m.id = entryId;
422
+ if (Array.isArray(v.mentions)) {
423
+ const ms = v.mentions.map((x) => String(x ?? '').trim()).filter(Boolean);
424
+ if (ms.length)
425
+ m.mentions = ms;
426
+ }
427
+ if (typeof v.replyTo === 'string' && v.replyTo.trim())
428
+ m.replyTo = v.replyTo.trim();
429
+ if (typeof v.branch === 'string' && v.branch.trim())
430
+ m.branch = v.branch.trim();
431
+ if (typeof v.kind === 'string' && v.kind.trim())
432
+ m.kind = v.kind.trim();
433
+ if (Array.isArray(v.attachments)) {
434
+ const at = v.attachments
435
+ .filter((a) => a && typeof a.cid === 'string' && /^(image|audio|file)$/.test(String(a.kind)))
436
+ .map((a) => ({
437
+ kind: a.kind,
438
+ cid: String(a.cid).trim(),
439
+ ...(typeof a.name === 'string' && a.name.trim() ? { name: a.name.trim().slice(0, 120) } : {}),
440
+ ...(typeof a.bytes === 'number' && Number.isFinite(a.bytes) ? { bytes: a.bytes } : {}),
441
+ }));
442
+ if (at.length)
443
+ m.attachments = at;
444
+ }
445
+ msgs.push(m);
446
+ }
447
+ }
448
+ msgs.sort((a, b) => a.ts - b.ts);
449
+ return msgs.slice(-limit);
450
+ }
451
+ /** 群成员: 从消息里提取 from 去重 */
452
+ async function groupMembers(groupId) {
453
+ const msgs = await groupMessages(groupId, 500);
454
+ return Array.from(new Set(msgs.map((m) => m.from)));
455
+ }
456
+ // ============ 2026-10-01 (P1): DID 门控 —— 成员事件读写 / 白名单自检 ============
457
+ /** 读群 store 的**原始条目** (含非消息条目, 如成员事件)。打不开 → 抛 (与 groupMessages 同规矩) */
458
+ async function allRawEntries(groupId) {
459
+ let store = storeCache.get(groupId) ?? null;
460
+ if (!store) {
461
+ const groups = await loadGroups();
462
+ const g = groups.find((x) => x.id === groupId);
463
+ if (!g)
464
+ return [];
465
+ store = await openGroupStore(g.address);
466
+ if (!store)
467
+ throw new GroupStoreUnreachableError(groupId, g.address, openFailures.get(groupId));
468
+ }
469
+ return store.all();
470
+ }
471
+ /**
472
+ * 读本群的全部成员变更事件, 并**逐条重新验签**。
473
+ * 没验过的事件**不算数** (进 rejected, 不进 accepted) —— 别把"库里有一条"当成"它是真的"。
474
+ */
475
+ async function groupMembershipEvents(groupId) {
476
+ const raw = await allRawEntries(groupId);
477
+ const accepted = [];
478
+ const rejected = [];
479
+ for (const e of raw) {
480
+ const v = e.value;
481
+ if (!v || typeof v !== 'object' || v.kind !== group_access_js_1.GROUP_MEMBERSHIP_KIND)
482
+ continue;
483
+ const ev = v.event;
484
+ const r = await (0, group_access_js_1.verifyMembershipEvent)(ev);
485
+ if (r.ok)
486
+ accepted.push(ev);
487
+ else {
488
+ rejected.push({ event: ev, ok: false, reason: r.checks.filter((c) => !c.ok).map((c) => `${c.name}: ${c.detail}`).join(' | ') });
489
+ }
490
+ }
491
+ accepted.sort((a, b) => a.ts - b.ts);
492
+ return { accepted, rejected };
493
+ }
494
+ /**
495
+ * 落一条成员变更事件 (验签不过 → 拒写, 返回 error)。
496
+ * 注意: 能写这个 store 本身就要求调用者在白名单里 —— 本函数不代替 ACL, 只是入口处的形状/签名闸门。
497
+ */
498
+ async function recordMembershipEvent(groupId, event) {
499
+ const v = await (0, group_access_js_1.verifyMembershipEvent)(event);
500
+ if (!v.ok) {
501
+ return { ok: false, error: `成员事件验签不过: ${v.checks.filter((c) => !c.ok).map((c) => c.name).join(',')}` };
502
+ }
503
+ let store = storeCache.get(groupId) ?? null;
504
+ if (!store) {
505
+ const groups = await loadGroups();
506
+ const g = groups.find((x) => x.id === groupId);
507
+ if (!g)
508
+ return { ok: false, error: '群组不存在 (先 joinGroup/createGroup)' };
509
+ store = await openGroupStore(g.address);
510
+ if (!store)
511
+ return { ok: false, error: `群组 store 不可达: ${unreachableReason(openFailures.get(groupId))}` };
512
+ }
513
+ try {
514
+ await store.add((0, group_access_js_1.membershipEntryOf)(event));
515
+ return { ok: true };
516
+ }
517
+ catch (e) {
518
+ return { ok: false, error: `写成员事件失败: ${String(e?.message || e).slice(0, 160)}` };
519
+ }
520
+ }
521
+ /**
522
+ * 从本群库里的成员事件**派生**写白名单 (群主 + add − remove)。
523
+ * 这就是"当前这条群地址的 ACL 应该长什么样"的**可核验答案**:
524
+ * 拿它去和 manifest 里真正的白名单比对 (或拿它去重建一个新 store)。
525
+ */
526
+ async function groupWriteList(groupId, owner) {
527
+ const groups = await loadGroups();
528
+ const g = groups.find((x) => x.id === groupId);
529
+ const raw = await allRawEntries(groupId);
530
+ const events = [];
531
+ for (const e of raw) {
532
+ const v = e.value;
533
+ if (v && typeof v === 'object' && v.kind === group_access_js_1.GROUP_MEMBERSHIP_KIND && v.event)
534
+ events.push(v.event);
535
+ }
536
+ const applied = await (0, group_access_js_1.applyMembershipEvents)({ owner, events });
537
+ const gated = !!(g?.gated || applied.accepted.length > 0);
538
+ // 非门控的老群 (write:['*']) 派生不出成员白名单 —— 如实标 gated:false, 不硬凑
539
+ return { write: applied.write, members: applied.members, rejectedCount: applied.rejected.length, gated };
540
+ }
541
+ /** 发送群消息 (广播给所有成员) */
542
+ async function groupSend(groupId, text, from, opts) {
543
+ const msg = String(text || '').trim();
544
+ if (!msg)
545
+ return { ok: false, error: '消息不能为空' };
546
+ let store = storeCache.get(groupId) ?? null;
547
+ if (!store) {
548
+ const groups = await loadGroups();
549
+ const g = groups.find((x) => x.id === groupId);
550
+ if (!g)
551
+ return { ok: false, error: '群组不存在 (先 joinGroup)' };
552
+ store = await openGroupStore(g.address);
553
+ if (!store) {
554
+ return { ok: false, error: `群组 store 不可达: ${unreachableReason(openFailures.get(groupId))}` };
555
+ }
556
+ }
557
+ try {
558
+ const record = { from: String(from || 'anonymous'), text: msg, ts: Date.now() };
559
+ // 只有显式给了合法声明才写字段 —— 不给就不写 (老读者读到的东西一字不变)
560
+ const lang = typeof opts?.lang === 'string' ? opts.lang.trim() : '';
561
+ if (lang)
562
+ record.lang = lang;
563
+ // 2026-10-02: 与 lang 同口径 —— **只有显式给了合法形状才写字段** (不给就不写, 老读者读到的一字不变)
564
+ const mentions = Array.isArray(opts?.mentions)
565
+ ? opts.mentions.map((x) => String(x ?? '').trim()).filter(Boolean).slice(0, 20)
566
+ : [];
567
+ if (mentions.length)
568
+ record.mentions = mentions;
569
+ const replyTo = typeof opts?.replyTo === 'string' ? opts.replyTo.trim() : '';
570
+ if (replyTo)
571
+ record.replyTo = replyTo.slice(0, 200);
572
+ const branch = typeof opts?.branch === 'string' ? opts.branch.trim() : '';
573
+ if (branch)
574
+ record.branch = branch.slice(0, 120);
575
+ const attachments = Array.isArray(opts?.attachments)
576
+ ? opts.attachments
577
+ .filter((a) => a && typeof a.cid === 'string' && a.cid.trim() && /^(image|audio|file)$/.test(String(a.kind)))
578
+ .slice(0, 8)
579
+ .map((a) => ({
580
+ kind: a.kind,
581
+ cid: String(a.cid).trim(),
582
+ ...(a.name ? { name: String(a.name).slice(0, 120) } : {}),
583
+ ...(typeof a.bytes === 'number' && Number.isFinite(a.bytes) ? { bytes: a.bytes } : {}),
584
+ }))
585
+ : [];
586
+ if (attachments.length)
587
+ record.attachments = attachments;
588
+ const kind = typeof opts?.kind === 'string' ? opts.kind.trim() : '';
589
+ if (kind)
590
+ record.kind = kind.slice(0, 40);
591
+ // 2026-10-02: 回传本条消息的 id —— 回复/建分支都要锚点 (拿不到就不给字段, 不假造)
592
+ const entry = (await store.add(record));
593
+ const id = String(entry?.hash ?? entry?.id ?? '').trim();
594
+ return id ? { ok: true, id } : { ok: true };
595
+ }
596
+ catch (e) {
597
+ return { ok: false, error: `发送失败: ${String(e?.message || e).slice(0, 160)}` };
598
+ }
599
+ }
600
+ /**
601
+ * 2026-09-28: 按语言裁决发群消息 —— 真可走的那条路。
602
+ *
603
+ * 双方都声明 efficode ⇒ 正文真编成 Efficode 包 (文本模式 Base64), `lang: 'efficode'` 一起落库;
604
+ * 否则**原文直发** + `lang: 'natural'` (或干脆不写字段, 由调用方定), 并记一笔回落原因。
605
+ * 裁决/编码/落库各自失败都有结构化结果, 不静默。
606
+ */
607
+ async function groupSendWithLang(groupId, text, from, opts = { mine: undefined, theirs: undefined }) {
608
+ const enc = (0, negotiate_js_1.encodeForPeer)({ text, mine: opts.mine, theirs: opts.theirs, from, op: opts.op });
609
+ (0, negotiate_js_1.recordLangDecision)(enc.decision, { channel: `group:${groupId}`, peer: from });
610
+ const wantLang = enc.lang === 'efficode' || opts.record === 'always' ? enc.lang : '';
611
+ const r = await groupSend(groupId, enc.text, from, wantLang ? { lang: wantLang } : undefined);
612
+ return {
613
+ ok: r.ok,
614
+ error: r.error,
615
+ lang: enc.lang,
616
+ bytes: enc.bytes,
617
+ fallback: enc.decision.fallback,
618
+ reason: enc.decision.reason,
619
+ symbolic: enc.symbolic,
620
+ };
621
+ }
622
+ /**
623
+ * 2026-09-28: 读一条群消息 —— 严格按它自己声明的 `lang` 分派。
624
+ * 声明不是 efficode (含未知字符串 / 没声明) ⇒ 原样返回, **不进解码器**。
625
+ * 声明 efficode 且本机也声明支持 ⇒ 真解包 (畸形包抛, 由调用方如实报失败)。
626
+ */
627
+ function readGroupMessage(msg, mine) {
628
+ return (0, negotiate_js_1.decodeFromPeer)({ payload: msg.text, declaredLang: msg.lang, mine });
629
+ }
630
+ /** 群组邀请链接 */
631
+ async function groupLink(groupId) {
632
+ const groups = await loadGroups();
633
+ return groups.find((g) => g.id === groupId)?.link ?? null;
634
+ }
635
+ /** 群组信息 (含成员数/消息数) */
636
+ async function groupInfo(groupId) {
637
+ const groups = await loadGroups();
638
+ const g = groups.find((x) => x.id === groupId);
639
+ if (!g)
640
+ return null;
641
+ const msgs = await groupMessages(groupId, 500);
642
+ const members = await groupMembers(groupId);
643
+ return { ...g, messageCount: msgs.length, memberCount: members.length };
644
+ }
645
+ /** 重启恢复: 重开所有已加入群组的 store (失败静默) */
646
+ async function restoreGroups() {
647
+ const groups = await loadGroups();
648
+ if (groups.length === 0)
649
+ return { restored: 0, failed: 0, total: 0 };
650
+ let restored = 0;
651
+ let failed = 0;
652
+ for (const g of groups) {
653
+ const store = await openGroupStore(g.address).catch(() => null);
654
+ if (store)
655
+ restored++;
656
+ else
657
+ failed++;
658
+ }
659
+ return { restored, failed, total: groups.length };
660
+ }
661
+ // ─────────────────────────────────────────────────────────── 群运营能力 (2026-10-05, leo: 邀请/踢出/隐私/链接/状态)
662
+ /**
663
+ * 重建群 store (新白名单) —— 这是"改成员"唯一诚实的路径。
664
+ *
665
+ * IPFS 型 AC 的白名单烧在内容寻址的 ACL manifest 里, 改它必然换 store 地址
666
+ * (见 `ACL_INPLACE_UPDATE.ipfsType` / `aclChangePlan`)。所以:
667
+ * · 邀请 = 新白名单 + 被邀请者 → 重建;
668
+ * · 踢出 = 新白名单 − 被踢者 → 重建。
669
+ * 重建 = 新地址、新链接、**历史消息不回放** (要保留必须显式回放)。
670
+ *
671
+ * 本函数做"换地址重建 + 历史回放 (逐条 add) + 本机链接更新", 返回新 GroupInfo。
672
+ * 迁移代价 (老链接作废/其他成员需重新 join) 由调用方 (CLI) 明示, 这里不假装无痛。
673
+ */
674
+ async function rebuildGroupWithWriteList(groupId, mode, opts) {
675
+ const groups = await loadGroups();
676
+ const g = groups.find((x) => x.id === groupId);
677
+ if (!g)
678
+ return { ok: false, error: '群组不存在 (先 joinGroup/createGroup)' };
679
+ if (!g.gated) {
680
+ return {
681
+ ok: false,
682
+ error: `「${g.name}」不是门控群 (write:[*]), 没有白名单可改 —— 邀请=分享链接, 踢出=不适用 (人人可写)`,
683
+ };
684
+ }
685
+ const owner = {
686
+ did: g.ownerDid || '',
687
+ publicKeyHex: g.ownerPublicKeyHex || '',
688
+ orbitdbId: g.aclWrite?.[0] || '',
689
+ };
690
+ if (!owner.did || !owner.publicKeyHex || !owner.orbitdbId) {
691
+ return { ok: false, error: '缺少群主身份信息 (ownerDid/ownerPublicKeyHex/aclWrite 为空), 无法派生新白名单' };
692
+ }
693
+ // 当前白名单 = manifest 快照 (aclWrite, 上次重建/建群时烧进去的) 优先;
694
+ // 事件派生只在没有快照的老门控群兜底 (两套语义: 事件=成员自签加入, rebuild=群主单方改名单)
695
+ const derived = await groupWriteList(groupId, owner).catch(() => null);
696
+ const before = g.aclWrite && g.aclWrite.length > 0 ? g.aclWrite : (derived?.write ?? []);
697
+ const after = (0, group_access_js_1.normalizeWriteList)(mode.kind === 'add'
698
+ ? [...before, mode.ref.orbitdbId]
699
+ : before.filter((id) => id !== mode.orbitdbId));
700
+ const changed = before.join(',') !== after.join(',');
701
+ if (!changed) {
702
+ return { ok: false, error: mode.kind === 'add' ? '该成员已在白名单里' : '该成员不在白名单里' };
703
+ }
704
+ if (mode.kind === 'remove' && !before.includes(mode.orbitdbId)) {
705
+ return { ok: false, error: `白名单里没有 ${mode.orbitdbId.slice(0, 16)}…, 无需踢出` };
706
+ }
707
+ const plan = (0, group_access_js_1.aclChangePlan)({ before, after, messageCount: opts?.replayHistory ? undefined : 0 });
708
+ const db = getDb();
709
+ // 1) 读历史消息 (回放前) —— 只在显式要求时
710
+ const history = opts?.replayHistory ? await groupMessages(groupId, 100000) : [];
711
+ // 2) 建新 store (新白名单)
712
+ let store = null;
713
+ try {
714
+ store = await db.openStore(`bolloon-gw-group-${g.name}-r${Date.now().toString(36)}`, 'events', (0, group_access_js_1.groupAccessOptions)(after));
715
+ }
716
+ catch (e) {
717
+ return { ok: false, error: `重建 store 失败: ${String(e?.message || e).slice(0, 160)}` };
718
+ }
719
+ if (!store)
720
+ return { ok: false, error: '重建 store 返回空 (openStore 失败, 未改任何东西)' };
721
+ // 3) 迁移成员事件 (原样重放, 双侧签名保留) —— 否则新 store 派生白名单只剩群主, 成员全丢
722
+ try {
723
+ const rawAll = await allRawEntries(groupId);
724
+ for (const { value } of rawAll) {
725
+ const v = value;
726
+ if (v && typeof v === 'object' && v.kind === group_access_js_1.GROUP_MEMBERSHIP_KIND) {
727
+ try {
728
+ await store.add(v);
729
+ }
730
+ catch (e) { /* 单条失败不阻断 */ }
731
+ }
732
+ }
733
+ }
734
+ catch (e) {
735
+ return { ok: false, error: `迁移成员事件失败: ${String(e?.message || e).slice(0, 120)}` };
736
+ }
737
+ // 4) 回放历史消息 (逐条 add, 保持 ts)
738
+ if (opts?.replayHistory) {
739
+ for (const m of history) {
740
+ try {
741
+ await store.add({ ...m });
742
+ }
743
+ catch (e) {
744
+ // 逐条静默跳过, 能回放多少算多少; 回放是尽力而为, 不阻断重建
745
+ }
746
+ }
747
+ }
748
+ // 5) 更新本机记录: 换地址/换链接; 老 store 不删 (占用不回收, 见 plan.costs)
749
+ const address = store.address;
750
+ const link = `orbitdb://${address}?type=group&name=${encodeURIComponent(g.name)}`;
751
+ const info = {
752
+ ...g,
753
+ address,
754
+ link,
755
+ lastSyncAt: new Date().toISOString(),
756
+ gated: true,
757
+ ownerDid: g.ownerDid,
758
+ aclWrite: after,
759
+ privacy: g.privacy ?? true,
760
+ };
761
+ await saveGroups(groups.map((x) => (x.id === groupId ? info : x)));
762
+ storeCache.set(groupId, store);
763
+ return { ok: true, group: info, before, after, costs: plan.costs };
764
+ }
765
+ /** 邀请: 把 orbitdbId 加进白名单 (重建路径) */
766
+ async function inviteMember(groupId, ref) {
767
+ const r = await rebuildGroupWithWriteList(groupId, { kind: 'add', ref });
768
+ return { ok: r.ok, error: r.error, group: r.group, costs: r.costs };
769
+ }
770
+ /** 踢出: 把 orbitdbId 移出白名单 (重建路径; 协议上无法记"被踢者自签的 remove 事件", 不假装) */
771
+ async function kickMember(groupId, orbitdbId) {
772
+ const r = await rebuildGroupWithWriteList(groupId, { kind: 'remove', orbitdbId });
773
+ return { ok: r.ok, error: r.error, group: r.group, costs: r.costs };
774
+ }
775
+ /** 设置隐私群标记 (只影响本机展示/默认 ACL 说明, 不改变 OrbitDB 权限语义) */
776
+ async function setGroupPrivacy(groupId, privacy) {
777
+ const groups = await loadGroups();
778
+ const g = groups.find((x) => x.id === groupId);
779
+ if (!g)
780
+ return { ok: false, error: '群组不存在' };
781
+ const info = { ...g, privacy, lastSyncAt: new Date().toISOString() };
782
+ await saveGroups(groups.map((x) => (x.id === groupId ? info : x)));
783
+ return { ok: true, group: info };
784
+ }
785
+ /** 群状态快照: 成员/消息/白名单/gated/privacy/最后同步 (状态管理用) */
786
+ async function groupStatus(groupId) {
787
+ const g = await groupInfo(groupId);
788
+ if (!g)
789
+ return { ok: false, error: '群组不存在' };
790
+ const members = await groupMembers(groupId);
791
+ const wl = await (async () => {
792
+ if (!g.gated || !g.ownerDid)
793
+ return g.aclWrite ?? [];
794
+ const owner = { did: g.ownerDid, publicKeyHex: g.ownerPublicKeyHex || '', orbitdbId: g.aclWrite?.[0] || '' };
795
+ if (!owner.orbitdbId || !owner.publicKeyHex)
796
+ return g.aclWrite ?? [];
797
+ const d = await groupWriteList(groupId, owner).catch(() => null);
798
+ return d ? d.write : (g.aclWrite ?? []);
799
+ })();
800
+ return {
801
+ ok: true,
802
+ status: {
803
+ group: g,
804
+ memberCount: members.length,
805
+ messageCount: g.messageCount ?? 0,
806
+ writeList: wl,
807
+ gated: !!g.gated,
808
+ privacy: g.privacy ?? !!g.gated,
809
+ ownerDid: g.ownerDid,
810
+ lastSyncAt: g.lastSyncAt,
811
+ },
812
+ };
813
+ }
814
+ //# sourceMappingURL=gateway-group.js.map