@bolloon/bolloon-agent 0.4.13 → 0.4.14

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.
@@ -0,0 +1,130 @@
1
+ /**
2
+ * agent-gateway.ts — Agent Gateway 协调层 (2026-08-14)
3
+ *
4
+ * Agent Economy 的"经济路由器": 统一入口, 内部编排所有已有层.
5
+ * 定位: 入口层 + 协调层 + 安全边界 (不是新系统, 是把 Registry/DIAP/x402/Policy/Reputation/Treasury 粘起来).
6
+ *
7
+ * 简单用法 (入口要小):
8
+ * gateway.registerAgent({ capability: "research", price: "0.05 USDC" })
9
+ * gateway.callAgent({ task: "analyze paper", budget: 1, capability: "research" })
10
+ * gateway.joinNetwork(link) // 通过链接自动加入共享网络
11
+ *
12
+ * callAgent 自动闭环:
13
+ * Discovery (registry) → Identity (DIAP) → Negotiation (价格/预算匹配)
14
+ * → Policy (预算/白名单) → Payment (x402/审批) → Execution → Reputation
15
+ */
16
+ import { getAgentRegistry, warmAgentRegistry } from './agent-registry.js';
17
+ import { getEconomicPolicy } from './economic-policy.js';
18
+ import { getPaymentGate } from './payment-gate.js';
19
+ import { getApprovalStore } from './payment-approval.js';
20
+ import { recordServiceOutcome } from './agent-reputation.js';
21
+ // ============ 协调层 ============
22
+ /**
23
+ * 注册本 Agent 为服务提供者 (身份 + Registry + 信誉初始化).
24
+ */
25
+ export async function gatewayRegisterAgent(config, identity, deps) {
26
+ // 2026-08-14: 生产路径先 warm OrbitDB — 否则注册只落本地文件, 分享链接指向的 store 是空的.
27
+ // 离线/测试环境 warm 失败不阻塞 (注册仍落本地 fallback).
28
+ let registry = deps?.registry;
29
+ if (!registry) {
30
+ try {
31
+ await warmAgentRegistry();
32
+ }
33
+ catch { /* 离线模式 */ }
34
+ registry = getAgentRegistry();
35
+ }
36
+ const r = await registry.register({
37
+ agentId: identity.did || `did:local:${identity.name}`,
38
+ name: identity.name,
39
+ wallet: config.wallet || identity.wallet,
40
+ service: {
41
+ name: config.capability,
42
+ description: config.description || `${config.capability} 服务`,
43
+ price: {
44
+ amount: config.price,
45
+ currency: (config.currency || 'USDC').toUpperCase(),
46
+ per: config.per || 'query',
47
+ },
48
+ },
49
+ capabilities: [config.capability],
50
+ });
51
+ return { ok: r.ok, error: r.error };
52
+ }
53
+ /**
54
+ * 调用 Agent 服务 (自动闭环).
55
+ * Discovery → Policy → Payment gate → Approval → Execution → Reputation.
56
+ */
57
+ export async function gatewayCallAgent(opts) {
58
+ const { task, budget, capability, url, privateKey } = opts;
59
+ // 1. Discovery: 从 Registry 找 provider
60
+ const registry = getAgentRegistry();
61
+ const providers = capability ? await registry.discover(capability) : await registry.list();
62
+ if (providers.length === 0) {
63
+ return { success: false, error: `未找到能力为 '${capability || 'any'}' 的 Agent (先 joinNetwork 或注册)` };
64
+ }
65
+ const provider = providers[0]; // 简版: 取第一个 (完整版按信誉/价格排序)
66
+ const price = parseFloat(provider.service?.price?.amount || '0');
67
+ // 2. Negotiation: 预算检查
68
+ if (price > budget) {
69
+ return { success: false, error: `价格 ${price} 超预算 ${budget}`, provider: provider.agentId };
70
+ }
71
+ // 3. Payment gate (YAML 验证门): allow/confirm/deny
72
+ const gate = getPaymentGate();
73
+ const verdict = gate.evaluate({ service: provider.service?.name, amount: price, recipient: provider.wallet });
74
+ if (verdict.decision === 'deny') {
75
+ return { success: false, error: `[payment-gate] ${verdict.reason}`, provider: provider.agentId, decision: 'deny' };
76
+ }
77
+ // 4. Policy (预算/白名单)
78
+ const policy = getEconomicPolicy();
79
+ const pol = await policy.check({ payTo: provider.wallet, amount: price, service: provider.service?.name });
80
+ if (!pol.allowed) {
81
+ return { success: false, error: `[policy] ${pol.reason}`, provider: provider.agentId, decision: 'deny' };
82
+ }
83
+ // 5. confirm → 创建人工审批 (不自动执行)
84
+ if (verdict.decision === 'confirm') {
85
+ const store = getApprovalStore();
86
+ const approval = await store.create({
87
+ service: provider.service?.name || capability || 'service',
88
+ amount: price,
89
+ recipient: provider.wallet,
90
+ reason: verdict.reason,
91
+ retryPayload: { task, url, privateKey, serviceName: provider.service?.name, args: { task } },
92
+ });
93
+ return {
94
+ success: false,
95
+ error: `需人工确认: ${verdict.reason} (approval=${approval.id})`,
96
+ provider: provider.agentId,
97
+ decision: 'confirm',
98
+ requiresApproval: true,
99
+ approvalId: approval.id,
100
+ };
101
+ }
102
+ // 6. Execution (x402 支付 + 调用服务)
103
+ try {
104
+ const { serviceCall } = await import('./agent-service-client.js');
105
+ const r = await serviceCall({
106
+ serviceName: provider.service?.name || capability || 'service',
107
+ args: { task },
108
+ privateKey,
109
+ url,
110
+ registry,
111
+ });
112
+ if (!r.success)
113
+ return { success: false, error: r.error, provider: provider.agentId, decision: 'allow' };
114
+ // 7. Reputation: 记录成功
115
+ await recordServiceOutcome(provider.agentId, provider.service?.name || 'service', 'success', registry).catch(() => { });
116
+ return { success: true, provider: provider.agentId, decision: 'allow', output: r.output };
117
+ }
118
+ catch (e) {
119
+ return { success: false, error: `执行失败: ${String(e?.message || e).slice(0, 200)}`, provider: provider.agentId };
120
+ }
121
+ }
122
+ /**
123
+ * 查询网络状态 (注册表 + 信誉概览).
124
+ */
125
+ export async function gatewayStatus() {
126
+ const registry = getAgentRegistry();
127
+ const services = await registry.list();
128
+ const lines = services.slice(0, 20).map((s) => ` [${s.service?.name}] ${s.name} (${s.service?.price?.amount} ${s.service?.price?.currency}/${s.service?.price?.per}) rep=${s.reputation?.score ?? 'n/a'}`);
129
+ return `Agent 网络 (${services.length} 服务):\n${lines.join('\n') || ' (空, 先 joinNetwork 或注册)'}`;
130
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * gateway-group.ts — Agent Gateway P2P 群组 (2026-08-14)
3
+ *
4
+ * 群组 = OrbitDB events store (accessController write:'*'), 任何成员可广播消息,
5
+ * 全成员通过 OrbitDB pubsub 复制实时同步. 链接: orbitdb://<addr>?type=group&name=<群名>.
6
+ *
7
+ * 符合用户习惯: 群组 = 微信式群聊 — 加入链接即进群, 发消息全网同步.
8
+ * 持久化: ~/.bolloon/gateway-groups.json (重启后仍是群成员, 自动重开 store).
9
+ */
10
+ import * as os from 'os';
11
+ import * as path from 'path';
12
+ import { getCIDDatabase } from '../orbitdb/cid-database.js';
13
+ // ============ 依赖注入 (测试用, 避免单测起真实 OrbitDB 节点) ============
14
+ let _dbOverride = null;
15
+ /** 测试用: 注入 fake CIDDatabase */
16
+ export function setGroupTestDb(db) { _dbOverride = db; }
17
+ function getDb() { return _dbOverride ?? getCIDDatabase(); }
18
+ /** 测试用: 清空 store 缓存 / 订阅 (避免测试间污染) */
19
+ export function resetGroupState() {
20
+ storeCache.clear();
21
+ onChangeCallbacks.clear();
22
+ }
23
+ // ============ 持久化 ============
24
+ const groupsFile = () => path.join(os.homedir() || '/tmp', '.bolloon', 'gateway-groups.json');
25
+ async function loadGroups() {
26
+ try {
27
+ const { readFile } = await import('fs/promises');
28
+ const parsed = JSON.parse(await readFile(groupsFile(), 'utf-8'));
29
+ return Array.isArray(parsed) ? parsed : [];
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ }
35
+ async function saveGroups(list) {
36
+ try {
37
+ const { mkdir, writeFile } = await import('fs/promises');
38
+ await mkdir(path.dirname(groupsFile()), { recursive: true });
39
+ await writeFile(groupsFile(), JSON.stringify(list, null, 2), 'utf-8');
40
+ }
41
+ catch { /* 持久化失败静默 */ }
42
+ }
43
+ // ============ 链接解析 ============
44
+ /** 解析群组链接: orbitdb://<addr>?type=group&name=<群名> (type 非 group 返回 null) */
45
+ export function parseGroupLink(link) {
46
+ const l = String(link || '').trim();
47
+ if (!l.startsWith('orbitdb://'))
48
+ return null;
49
+ const [base, query] = l.split('?');
50
+ let params = null;
51
+ try {
52
+ params = query ? new URLSearchParams(query) : null;
53
+ }
54
+ catch { /* 忽略 */ }
55
+ if (params && params.get('type') === 'group') {
56
+ let address = base.slice('orbitdb://'.length);
57
+ if (!address.startsWith('/'))
58
+ address = `/${address}`;
59
+ return { address, name: params.get('name') || undefined };
60
+ }
61
+ return null;
62
+ }
63
+ /** 从消息文本检测群组链接 */
64
+ export function detectGroupLink(text) {
65
+ const t = String(text || '');
66
+ const re = /(orbitdb:\/\/\/?orbitdb\/[^\s)'"<>,。;]*\?[^\s)'"<>,。;]*type=group[^\s)'"<>,。;]*)/;
67
+ const m = re.exec(t);
68
+ return m ? m[1].trim() : null;
69
+ }
70
+ // ============ store 缓存 (避免重复 open) ============
71
+ const storeCache = new Map();
72
+ /** 订阅回调注册 (server 层挂 SSE 广播) */
73
+ const onChangeCallbacks = new Map();
74
+ function groupIdOf(address) {
75
+ // /orbitdb/zdpu... → zdpu... (地址后 12 位做短 id)
76
+ const m = /\/orbitdb\/(.{8,})/.exec(address);
77
+ return m ? m[1] : address;
78
+ }
79
+ /** 打开群组 store (缓存) — 可写 (replica=false, write:'*') */
80
+ async function openGroupStore(address) {
81
+ const id = groupIdOf(address);
82
+ if (storeCache.has(id))
83
+ return storeCache.get(id);
84
+ const db = getDb();
85
+ const store = await db.openStoreByAddress(address, 'events', {
86
+ replica: false,
87
+ accessController: { write: ['*'] },
88
+ });
89
+ if (!store)
90
+ return null;
91
+ storeCache.set(id, store);
92
+ // 订阅: 新消息 → 通知回调 (server SSE)
93
+ store.onChange(() => {
94
+ groupMessages(id, 5).then((msgs) => {
95
+ if (msgs.length === 0)
96
+ return;
97
+ const cb = onChangeCallbacks.get(id);
98
+ if (cb)
99
+ for (const fn of cb) {
100
+ try {
101
+ fn(msgs[msgs.length - 1]);
102
+ }
103
+ catch { /* 忽略 */ }
104
+ }
105
+ }).catch(() => { });
106
+ });
107
+ return store;
108
+ }
109
+ /** 注册群组消息回调 (返回退订函数) */
110
+ export function onGroupMessage(groupId, fn) {
111
+ if (!onChangeCallbacks.has(groupId))
112
+ onChangeCallbacks.set(groupId, new Set());
113
+ onChangeCallbacks.get(groupId).add(fn);
114
+ return () => { onChangeCallbacks.get(groupId)?.delete(fn); };
115
+ }
116
+ // ============ 群组操作 ============
117
+ /**
118
+ * 创建群组: 新 events store (write:'*') + 持久化 + 生成邀请链接.
119
+ */
120
+ export async function createGroup(name, opts) {
121
+ const groupName = String(name || '').trim() || `group-${Date.now().toString(36).slice(-4)}`;
122
+ try {
123
+ const db = getDb();
124
+ const store = await db.openStore(`bolloon-gw-group-${groupName}`, 'events', {
125
+ accessController: { write: ['*'] },
126
+ });
127
+ const address = store.address;
128
+ const id = groupIdOf(address);
129
+ const link = `orbitdb://${address}?type=group&name=${encodeURIComponent(groupName)}`;
130
+ const info = {
131
+ id, name: groupName, address, link,
132
+ createdAt: new Date().toISOString(),
133
+ lastSyncAt: new Date().toISOString(),
134
+ };
135
+ storeCache.set(id, store);
136
+ // 欢迎消息 (群主自我介绍)
137
+ const from = opts?.from || 'group-owner';
138
+ await store.add({ from, text: opts?.hello || `📢 群主创建了群「${groupName}」, 分享链接邀请成员加入`, ts: Date.now() });
139
+ const groups = await loadGroups();
140
+ await saveGroups([...groups.filter((g) => g.id !== id), info]);
141
+ return { ok: true, group: info };
142
+ }
143
+ catch (e) {
144
+ return { ok: false, error: `创建群组失败: ${String(e?.message || e).slice(0, 160)}` };
145
+ }
146
+ }
147
+ /**
148
+ * 通过链接加入群组: 打开 store (可写) + 持久化 + 幂等.
149
+ * 失败静默返回错误, 不影响本地.
150
+ */
151
+ export async function joinGroup(link) {
152
+ const parsed = parseGroupLink(link);
153
+ if (!parsed) {
154
+ return { ok: false, error: '不是群组链接 (需要 orbitdb://...?type=group&name=...)' };
155
+ }
156
+ // 幂等: 按地址去重
157
+ const existing = await loadGroups();
158
+ const id = groupIdOf(parsed.address);
159
+ if (existing.some((g) => g.id === id)) {
160
+ return { ok: true, already: true, group: existing.find((g) => g.id === id) };
161
+ }
162
+ const store = await openGroupStore(parsed.address);
163
+ if (!store) {
164
+ return { ok: false, error: '群组 store 不可达 (群主节点需在线), 稍后重试或让群主分享最新链接' };
165
+ }
166
+ const name = parsed.name || id.slice(0, 12);
167
+ const info = {
168
+ id, name, address: parsed.address,
169
+ link: `orbitdb://${parsed.address}?type=group&name=${encodeURIComponent(name)}`,
170
+ createdAt: new Date().toISOString(),
171
+ lastSyncAt: new Date().toISOString(),
172
+ };
173
+ await saveGroups([...existing, info]);
174
+ return { ok: true, group: info };
175
+ }
176
+ /** 列出已加入的群组 */
177
+ export async function listGroups() {
178
+ return loadGroups();
179
+ }
180
+ /** 获取群组 store 的最新消息 (ts 升序, 取最后 N 条) */
181
+ export async function groupMessages(groupId, limit = 50) {
182
+ let store = storeCache.get(groupId) ?? null;
183
+ if (!store) {
184
+ const groups = await loadGroups();
185
+ const g = groups.find((x) => x.id === groupId);
186
+ if (!g)
187
+ return [];
188
+ store = await openGroupStore(g.address);
189
+ if (!store)
190
+ return [];
191
+ }
192
+ const all = await store.all().catch(() => []);
193
+ const msgs = [];
194
+ for (const entry of all) {
195
+ const v = entry.value;
196
+ if (v && typeof v.text === 'string' && typeof v.from === 'string') {
197
+ msgs.push({ from: v.from, text: v.text, ts: typeof v.ts === 'number' ? v.ts : 0 });
198
+ }
199
+ }
200
+ msgs.sort((a, b) => a.ts - b.ts);
201
+ return msgs.slice(-limit);
202
+ }
203
+ /** 群成员: 从消息里提取 from 去重 */
204
+ export async function groupMembers(groupId) {
205
+ const msgs = await groupMessages(groupId, 500);
206
+ return Array.from(new Set(msgs.map((m) => m.from)));
207
+ }
208
+ /** 发送群消息 (广播给所有成员) */
209
+ export async function groupSend(groupId, text, from) {
210
+ const msg = String(text || '').trim();
211
+ if (!msg)
212
+ return { ok: false, error: '消息不能为空' };
213
+ let store = storeCache.get(groupId) ?? null;
214
+ if (!store) {
215
+ const groups = await loadGroups();
216
+ const g = groups.find((x) => x.id === groupId);
217
+ if (!g)
218
+ return { ok: false, error: '群组不存在 (先 joinGroup)' };
219
+ store = await openGroupStore(g.address);
220
+ if (!store)
221
+ return { ok: false, error: '群组 store 不可达' };
222
+ }
223
+ try {
224
+ await store.add({ from: String(from || 'anonymous'), text: msg, ts: Date.now() });
225
+ return { ok: true };
226
+ }
227
+ catch (e) {
228
+ return { ok: false, error: `发送失败: ${String(e?.message || e).slice(0, 160)}` };
229
+ }
230
+ }
231
+ /** 群组邀请链接 */
232
+ export async function groupLink(groupId) {
233
+ const groups = await loadGroups();
234
+ return groups.find((g) => g.id === groupId)?.link ?? null;
235
+ }
236
+ /** 群组信息 (含成员数/消息数) */
237
+ export async function groupInfo(groupId) {
238
+ const groups = await loadGroups();
239
+ const g = groups.find((x) => x.id === groupId);
240
+ if (!g)
241
+ return null;
242
+ const msgs = await groupMessages(groupId, 500);
243
+ const members = await groupMembers(groupId);
244
+ return { ...g, messageCount: msgs.length, memberCount: members.length };
245
+ }
246
+ /** 重启恢复: 重开所有已加入群组的 store (失败静默) */
247
+ export async function restoreGroups() {
248
+ const groups = await loadGroups();
249
+ if (groups.length === 0)
250
+ return { restored: 0, failed: 0, total: 0 };
251
+ let restored = 0;
252
+ let failed = 0;
253
+ for (const g of groups) {
254
+ const store = await openGroupStore(g.address).catch(() => null);
255
+ if (store)
256
+ restored++;
257
+ else
258
+ failed++;
259
+ }
260
+ return { restored, failed, total: groups.length };
261
+ }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * gateway-network.ts — Agent 网络加入 (2026-08-14 v2)
3
+ *
4
+ * joinNetwork(link): 通过链接自动加入共享 Agent 网络.
5
+ * 链接形式 (三选一):
6
+ * - orbitdb://<storeAddress> 共享 registry store (主链路, OrbitDB 复制)
7
+ * - ipns://<name> IPNS 标识 (registry 静态快照)
8
+ * - https://.../registry HTTP 端点 (远程 registry JSON)
9
+ *
10
+ * v2 新增:
11
+ * - orbitdb:// 真实可开 (CIDDatabase.openStoreByAddress, replica 只读)
12
+ * - 成员身份持久化 ~/.bolloon/gateway-networks.json (重启后仍是家庭成员)
13
+ * - detectGatewayLink / maybeAutoJoinGateway — 消息里收到链接自动加入 (入口要小)
14
+ * - shareNetworkLink — 生成本机可分享的网络链接 (把 registry 发出去)
15
+ */
16
+ import * as os from 'os';
17
+ import * as path from 'path';
18
+ import { getAgentRegistry, warmAgentRegistry } from './agent-registry.js';
19
+ /** 解析链接字符串 → ParsedLink (剥离 ?name= query) */
20
+ export function parseNetworkLink(link) {
21
+ const l = String(link || '').trim();
22
+ if (!l)
23
+ return null;
24
+ const [base, query] = l.split('?');
25
+ let networkName;
26
+ try {
27
+ networkName = query ? (new URLSearchParams(query).get('name') || undefined) : undefined;
28
+ }
29
+ catch { /* 忽略坏 query */ }
30
+ if (base.startsWith('ipns://'))
31
+ return { kind: 'ipns', name: base.slice('ipns://'.length), networkName };
32
+ if (base.startsWith('orbitdb://')) {
33
+ // orbitdb:///orbitdb/<addr> 或 orbitdb://orbitdb/<addr> → /orbitdb/<addr>
34
+ let address = base.slice('orbitdb://'.length);
35
+ if (!address.startsWith('/'))
36
+ address = `/${address}`;
37
+ return { kind: 'orbitdb', address, networkName };
38
+ }
39
+ if (base.startsWith('http://') || base.startsWith('https://'))
40
+ return { kind: 'http', url: base, networkName };
41
+ return null;
42
+ }
43
+ /** 从消息文本里检测 gateway 链接 (自动加入触发器用) */
44
+ export function detectGatewayLink(text) {
45
+ const t = String(text || '');
46
+ // orbitdb:// 地址含 '/' 必须贪婪匹配到空白/引号; https 允许 /registry 后带 ?query
47
+ const re = /(orbitdb:\/\/\/?orbitdb\/[^\s)'"<>,。;]+|ipns:\/\/[^\s)'"<>,。;]+|https?:\/\/[^\s)'"<>,。;]*\/registry(?:\?[^\s)'"<>,。;]*)?)/;
48
+ const m = re.exec(t);
49
+ return m ? m[1].trim() : null;
50
+ }
51
+ // ============ 远端拉取 ============
52
+ /** 从 HTTP 端点拉取远端 registry */
53
+ async function fetchRemoteRegistry(url) {
54
+ try {
55
+ const r = await fetch(url, { signal: AbortSignal.timeout(15000) });
56
+ if (!r.ok)
57
+ return null;
58
+ const data = await r.json();
59
+ // 兼容: {services:[...]} 或 [...]
60
+ const services = Array.isArray(data) ? data : (data.services ?? null);
61
+ return Array.isArray(services) ? services : null;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ /** 从 IPNS 拉取 registry (需本地 Kubo + 8080 gateway) */
68
+ async function fetchIpnsRegistry(name) {
69
+ try {
70
+ const r = await fetch(`http://127.0.0.1:8080/ipns/${name}/registry.json`, { signal: AbortSignal.timeout(15000) });
71
+ if (!r.ok)
72
+ return null;
73
+ const data = await r.json();
74
+ return Array.isArray(data) ? data : (data.services ?? null);
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ /** 从 OrbitDB 打开共享 registry store (replica 只读) */
81
+ async function fetchOrbitdbRegistry(address) {
82
+ try {
83
+ const { getCIDDatabase } = await import('../orbitdb/cid-database.js');
84
+ const db = getCIDDatabase();
85
+ const store = await db.openStoreByAddress(address, 'keyvalue');
86
+ if (!store)
87
+ return null;
88
+ // 先立即读 (同节点 store 已有数据); 没有再等复制 (owner 在线时 pubsub 复制通常 <1s)
89
+ const first = await store.get('services').catch(() => null);
90
+ if (Array.isArray(first))
91
+ return first;
92
+ await new Promise((resolve) => {
93
+ let done = false;
94
+ const finish = () => { if (!done) {
95
+ done = true;
96
+ try {
97
+ off();
98
+ }
99
+ catch { }
100
+ resolve();
101
+ } };
102
+ const off = store.onChange(finish);
103
+ setTimeout(finish, 4000);
104
+ });
105
+ // 单键 'services' (registry 的 OrbitDB 布局: 整个列表存单键)
106
+ const v = await store.get('services').catch(() => null);
107
+ if (Array.isArray(v))
108
+ return v;
109
+ // 兼容: 遍历 entries 找数组值
110
+ const all = await store.all().catch(() => null);
111
+ if (Array.isArray(all)) {
112
+ for (const entry of all) {
113
+ if (Array.isArray(entry.value))
114
+ return entry.value;
115
+ const val = entry.value;
116
+ if (val?.services && Array.isArray(val.services))
117
+ return val.services;
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ const networksFile = () => path.join(os.homedir() || '/tmp', '.bolloon', 'gateway-networks.json');
127
+ async function loadNetworks() {
128
+ try {
129
+ const { readFile } = await import('fs/promises');
130
+ const parsed = JSON.parse(await readFile(networksFile(), 'utf-8'));
131
+ return Array.isArray(parsed) ? parsed : [];
132
+ }
133
+ catch {
134
+ return [];
135
+ }
136
+ }
137
+ async function saveNetworks(list) {
138
+ try {
139
+ const { mkdir, writeFile } = await import('fs/promises');
140
+ await mkdir(path.dirname(networksFile()), { recursive: true });
141
+ await writeFile(networksFile(), JSON.stringify(list, null, 2), 'utf-8');
142
+ }
143
+ catch { /* 持久化失败静默 */ }
144
+ }
145
+ /** 列出已加入的网络 */
146
+ export async function listJoinedNetworks() {
147
+ return loadNetworks();
148
+ }
149
+ /**
150
+ * 通过链接加入共享网络: 拉取远端服务列表 → 合并到本地 registry (按 agentId+service 去重).
151
+ * 幂等: 已加入的网络直接返回 already=true.
152
+ * deps.registry 可注入 (测试用), 默认单例.
153
+ */
154
+ export async function joinNetwork(link, deps) {
155
+ const parsed = parseNetworkLink(link);
156
+ if (!parsed) {
157
+ return { ok: false, joined: 0, total: 0, error: '无法解析链接 (支持 orbitdb:// / ipns:// / https://)' };
158
+ }
159
+ // 幂等: 已加入 (按解析后的网络身份 key 去重, 忽略 ?name 差异)
160
+ const existing = await loadNetworks();
161
+ const norm = String(link).trim();
162
+ const identityKey = parsed.kind === 'orbitdb' ? `orbitdb:${parsed.address}`
163
+ : parsed.kind === 'ipns' ? `ipns:${parsed.name}`
164
+ : `http:${parsed.url}`;
165
+ if (existing.some((n) => n.linkKey === identityKey || n.link === norm)) {
166
+ return { ok: true, joined: 0, total: 0, already: true, linkKind: parsed.kind, networkName: parsed.networkName };
167
+ }
168
+ // 拉取远端服务
169
+ let remote = null;
170
+ if (parsed.kind === 'http') {
171
+ remote = await fetchRemoteRegistry(parsed.url);
172
+ }
173
+ else if (parsed.kind === 'ipns') {
174
+ remote = await fetchIpnsRegistry(parsed.name);
175
+ }
176
+ else if (parsed.kind === 'orbitdb') {
177
+ remote = await fetchOrbitdbRegistry(parsed.address);
178
+ }
179
+ if (!remote || remote.length === 0) {
180
+ return { ok: false, joined: 0, total: 0, error: '远端网络无服务或不可达 (store owner 需在线)', linkKind: parsed.kind };
181
+ }
182
+ // 合并到本地 registry (按 agentId + service.name 去重)
183
+ const registry = deps?.registry ?? getAgentRegistry();
184
+ const local = await registry.list();
185
+ let joined = 0;
186
+ for (const svc of remote) {
187
+ if (!svc?.agentId || !svc?.service?.name)
188
+ continue;
189
+ const exists = local.some((l) => l.agentId === svc.agentId && l.service?.name === svc.service?.name);
190
+ if (!exists) {
191
+ await registry.register(svc).catch(() => { });
192
+ joined++;
193
+ }
194
+ }
195
+ // 记录成员身份 (持久化 → 重启后自动恢复)
196
+ await saveNetworks([
197
+ ...existing,
198
+ {
199
+ link: norm,
200
+ linkKey: identityKey,
201
+ kind: parsed.kind,
202
+ name: parsed.networkName,
203
+ joinedAt: new Date().toISOString(),
204
+ serviceCount: remote.length,
205
+ lastSyncAt: new Date().toISOString(),
206
+ },
207
+ ]);
208
+ return { ok: true, joined, total: remote.length, linkKind: parsed.kind, networkName: parsed.networkName };
209
+ }
210
+ /** 启动恢复: 重拉所有已加入网络 (失败静默, 保留记录). 返回恢复统计. */
211
+ export async function restoreJoinedNetworks() {
212
+ const nets = await loadNetworks();
213
+ if (nets.length === 0)
214
+ return { restored: 0, failed: 0, total: 0 };
215
+ let restored = 0;
216
+ let failed = 0;
217
+ for (const n of nets) {
218
+ const r = await joinNetwork(n.link).catch(() => null);
219
+ if (r?.ok)
220
+ restored++;
221
+ else
222
+ failed++;
223
+ // 更新 lastSyncAt
224
+ if (r?.ok) {
225
+ const updated = (await loadNetworks()).map((m) => m.link === n.link ? { ...m, lastSyncAt: new Date().toISOString(), serviceCount: r.total || m.serviceCount } : m);
226
+ await saveNetworks(updated);
227
+ }
228
+ }
229
+ return { restored, failed, total: nets.length };
230
+ }
231
+ // ============ 分享链接 ============
232
+ /**
233
+ * 生成本机可分享的网络链接: orbitdb://<registry storeAddress>?name=<网络名>.
234
+ * 对方收到链接 → 自动 joinNetwork → 拉取本机注册的服务.
235
+ * opts.registry 可注入 (测试用), 默认单例.
236
+ */
237
+ export async function shareNetworkLink(opts) {
238
+ try {
239
+ let registry = opts?.registry;
240
+ if (!registry) {
241
+ await warmAgentRegistry();
242
+ registry = getAgentRegistry();
243
+ }
244
+ if (!registry.ready || !registry.storeAddress) {
245
+ return { ok: false, error: 'OrbitDB registry 未就绪 (离线模式). 备选: 把 registry 列表发布成 https://.../registry 端点分享' };
246
+ }
247
+ const name = encodeURIComponent(String(opts?.name || registry.storeName || 'bolloon-network'));
248
+ return { ok: true, link: `orbitdb://${registry.storeAddress}?name=${name}` };
249
+ }
250
+ catch (e) {
251
+ return { ok: false, error: `生成分享链接失败: ${String(e?.message || e).slice(0, 160)}` };
252
+ }
253
+ }
254
+ // ============ 自动加入 (消息触发) ============
255
+ /**
256
+ * 自动加入入口: 文本里检测到 gateway 链接 → 幂等 joinNetwork.
257
+ * 返回给 agent 的通知字符串 (无链接 / 已在网络 → null, 静默).
258
+ * 设计: 加入是自由的 (只拉服务列表, 不花钱), 支付仍走 payment-gate 安全链.
259
+ * deps.registry 可注入 (测试用), 默认单例.
260
+ */
261
+ export async function maybeAutoJoinGateway(text, deps) {
262
+ const link = detectGatewayLink(text);
263
+ if (!link)
264
+ return null;
265
+ try {
266
+ const r = await joinNetwork(link, deps);
267
+ if (r.ok && r.already)
268
+ return null; // 已在网络, 静默
269
+ if (r.ok) {
270
+ const netName = r.networkName ? `「${r.networkName}」` : ''; // URLSearchParams 已 decode
271
+ return `🆕 已自动加入 Agent 网络${netName} (${r.linkKind}): 拉取 ${r.total} 个服务, 新增 ${r.joined} 个。用 gateway_status 查看, gateway_call 调用网络里的服务。`;
272
+ }
273
+ return `⚠️ 检测到 Agent 网络链接 (${r.linkKind || 'unknown'}) 但加入失败: ${r.error}`;
274
+ }
275
+ catch (e) {
276
+ return `⚠️ 自动加入 Agent 网络失败: ${String(e?.message || e).slice(0, 160)}`;
277
+ }
278
+ }