@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.
- package/dist/agents/agent-gateway.js +130 -0
- package/dist/agents/gateway-group.js +261 -0
- package/dist/agents/gateway-network.js +278 -0
- package/dist/agents/pi-sdk-tools.js +117 -0
- package/dist/constraint-runtime/tools/PolymarketSDK/econ-integration.d.ts +55 -0
- package/dist/constraint-runtime/tools/PolymarketSDK/econ-integration.js +59 -0
- package/dist/orbitdb/cid-database.js +39 -10
- package/dist/web/client.js +273 -0
- package/dist/web/index.html +17 -0
- package/dist/web/server.js +183 -0
- package/dist/web/style.css +116 -0
- package/package.json +1 -1
|
@@ -2505,6 +2505,123 @@ export function registerBuiltinTools(ctx) {
|
|
|
2505
2505
|
}
|
|
2506
2506
|
},
|
|
2507
2507
|
});
|
|
2508
|
+
// 2026-08-14: Agent Gateway — 经济协调层统一入口 (注册/调用/加入网络/状态)
|
|
2509
|
+
ctx.tools.set('gateway_register', {
|
|
2510
|
+
name: 'gateway_register',
|
|
2511
|
+
description: '把本 Agent 注册为服务提供者 (Agent Gateway 入口). capability: 能力名 (research/coding/data...); price: 价格; per: 计价单位; wallet: 收款地址. 注册后其他 Agent 能发现并调用你.',
|
|
2512
|
+
parameters: {
|
|
2513
|
+
capability: '能力名 (必填, research/coding/data/compute...)',
|
|
2514
|
+
price: '价格 (必填, 如 0.05)',
|
|
2515
|
+
currency: '币种 (默认 USDC)',
|
|
2516
|
+
per: '计价单位 (默认 query)',
|
|
2517
|
+
description: '服务描述 (可选)',
|
|
2518
|
+
wallet: '收款钱包地址 (可选)',
|
|
2519
|
+
},
|
|
2520
|
+
execute: async (args) => {
|
|
2521
|
+
const { gatewayRegisterAgent } = await import('./agent-gateway.js');
|
|
2522
|
+
const r = await gatewayRegisterAgent({
|
|
2523
|
+
capability: String(args.capability || '').trim(),
|
|
2524
|
+
price: String(args.price || '').trim(),
|
|
2525
|
+
currency: String(args.currency || 'USDC'),
|
|
2526
|
+
per: String(args.per || 'query'),
|
|
2527
|
+
description: String(args.description || '').trim() || undefined,
|
|
2528
|
+
wallet: String(args.wallet || '').trim() || undefined,
|
|
2529
|
+
}, { did: ctx.identity?.did || '', name: ctx.identity?.name || 'agent', wallet: String(args.wallet || '') });
|
|
2530
|
+
return r.ok
|
|
2531
|
+
? { success: true, output: `✅ 已加入 Agent 网络: ${args.capability} @ ${args.price} ${args.currency}/${args.per}` }
|
|
2532
|
+
: { success: false, error: r.error };
|
|
2533
|
+
},
|
|
2534
|
+
});
|
|
2535
|
+
ctx.tools.set('gateway_call', {
|
|
2536
|
+
name: 'gateway_call',
|
|
2537
|
+
description: '通过 Agent Gateway 调用服务 (自动闭环: 发现→预算→验证门→执行→信誉). task: 任务描述; budget: 预算 (USDC); capability: 需要的能力. 需人工确认的支付会返回 approval 待审批.',
|
|
2538
|
+
parameters: {
|
|
2539
|
+
task: '任务描述 (必填)',
|
|
2540
|
+
budget: '预算 USDC (必填)',
|
|
2541
|
+
capability: '需要的能力 (可选, 如 research)',
|
|
2542
|
+
url: '服务端点 URL (可选)',
|
|
2543
|
+
},
|
|
2544
|
+
execute: async (args) => {
|
|
2545
|
+
const task = String(args.task || '').trim();
|
|
2546
|
+
const budget = Number(args.budget);
|
|
2547
|
+
if (!task || !budget || budget <= 0)
|
|
2548
|
+
return { success: false, error: 'task 和 budget(>0) 必填' };
|
|
2549
|
+
try {
|
|
2550
|
+
const { gatewayCallAgent } = await import('./agent-gateway.js');
|
|
2551
|
+
const r = await gatewayCallAgent({
|
|
2552
|
+
task,
|
|
2553
|
+
budget,
|
|
2554
|
+
capability: String(args.capability || '').trim() || undefined,
|
|
2555
|
+
url: String(args.url || '').trim() || undefined,
|
|
2556
|
+
});
|
|
2557
|
+
if (r.success)
|
|
2558
|
+
return { success: true, output: `✅ ${r.provider}: ${r.output}` };
|
|
2559
|
+
return { success: false, error: r.error, requiresApproval: r.requiresApproval, approvalId: r.approvalId, provider: r.provider };
|
|
2560
|
+
}
|
|
2561
|
+
catch (e) {
|
|
2562
|
+
return { success: false, error: `gateway_call 失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
2563
|
+
}
|
|
2564
|
+
},
|
|
2565
|
+
});
|
|
2566
|
+
ctx.tools.set('gateway_join', {
|
|
2567
|
+
name: 'gateway_join',
|
|
2568
|
+
description: '通过链接加入共享 Agent 网络 (拉取远端服务合并到本地 registry). link: ipns://<name> 或 orbitdb://<address> 或 https://.../registry. 加入后可用 gateway_call 调用网络里的服务.',
|
|
2569
|
+
parameters: { link: '网络链接 (必填, ipns:// / orbitdb:// / https://)' },
|
|
2570
|
+
execute: async (args) => {
|
|
2571
|
+
const link = String(args.link || '').trim();
|
|
2572
|
+
if (!link)
|
|
2573
|
+
return { success: false, error: 'link 必填 (ipns:// / orbitdb:// / https://)' };
|
|
2574
|
+
try {
|
|
2575
|
+
const { joinNetwork } = await import('./gateway-network.js');
|
|
2576
|
+
const r = await joinNetwork(link);
|
|
2577
|
+
if (!r.ok)
|
|
2578
|
+
return { success: false, error: r.error };
|
|
2579
|
+
return { success: true, output: `✅ 已加入 Agent 网络: 拉取 ${r.total} 个服务, 新增 ${r.joined} 个 (${r.linkKind})` };
|
|
2580
|
+
}
|
|
2581
|
+
catch (e) {
|
|
2582
|
+
return { success: false, error: `gateway_join 失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
2583
|
+
}
|
|
2584
|
+
},
|
|
2585
|
+
});
|
|
2586
|
+
ctx.tools.set('gateway_status', {
|
|
2587
|
+
name: 'gateway_status',
|
|
2588
|
+
description: '查看 Agent 网络状态 (已注册服务 + 信誉 + 已加入的网络).',
|
|
2589
|
+
parameters: {},
|
|
2590
|
+
execute: async () => {
|
|
2591
|
+
try {
|
|
2592
|
+
const { gatewayStatus } = await import('./agent-gateway.js');
|
|
2593
|
+
const { listJoinedNetworks } = await import('./gateway-network.js');
|
|
2594
|
+
const nets = await listJoinedNetworks();
|
|
2595
|
+
const netLine = nets.length
|
|
2596
|
+
? `\n已加入网络 (${nets.length}):\n` + nets.slice(0, 10).map((n) => ` [${n.kind}] ${n.name || n.link.slice(0, 48)} (${n.serviceCount} 服务, ${n.joinedAt.slice(0, 10)})`).join('\n')
|
|
2597
|
+
: '\n(未加入任何网络, 收到 orbitdb:// 链接会自动加入, 或用 gateway_join)';
|
|
2598
|
+
return { success: true, output: (await gatewayStatus()) + netLine };
|
|
2599
|
+
}
|
|
2600
|
+
catch (e) {
|
|
2601
|
+
return { success: false, error: `gateway_status 失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
2602
|
+
}
|
|
2603
|
+
},
|
|
2604
|
+
});
|
|
2605
|
+
ctx.tools.set('gateway_share', {
|
|
2606
|
+
name: 'gateway_share',
|
|
2607
|
+
description: '生成本 Agent 网络的分享链接 (orbitdb://...). 把链接发给其他 Bolloon/Agent, 对方收到后会自动加入你的网络并发现你注册的服务.',
|
|
2608
|
+
parameters: {
|
|
2609
|
+
name: '网络名 (可选, 默认 registry 名)',
|
|
2610
|
+
},
|
|
2611
|
+
execute: async (args) => {
|
|
2612
|
+
try {
|
|
2613
|
+
const { shareNetworkLink } = await import('./gateway-network.js');
|
|
2614
|
+
const name = String(args.name || '').trim() || undefined;
|
|
2615
|
+
const r = await shareNetworkLink(name ? { name } : undefined);
|
|
2616
|
+
if (!r.ok)
|
|
2617
|
+
return { success: false, error: r.error };
|
|
2618
|
+
return { success: true, output: `🔗 Agent 网络分享链接 (对方收到后自动加入):\n${r.link}` };
|
|
2619
|
+
}
|
|
2620
|
+
catch (e) {
|
|
2621
|
+
return { success: false, error: `gateway_share 失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
2622
|
+
}
|
|
2623
|
+
},
|
|
2624
|
+
});
|
|
2508
2625
|
// ============================================================
|
|
2509
2626
|
// publish_did (2026-08-03) — 把当前 agent 的 DID 发布到 IPFS + IPNS
|
|
2510
2627
|
// 全自动: 自动安装/启动本地 Kubo → 上传 DID 文档 → 发布 IPNS name
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* econ-integration.ts — Polymarket × Agent 经济网络集成 (2026-08-13, Task C4)
|
|
3
|
+
*
|
|
4
|
+
* 打通 Agent Economic Network 与 Polymarket 预测市场:
|
|
5
|
+
* - Agent 可用 Treasury 资金在 Polymarket 下单 (预测/对冲)
|
|
6
|
+
* - 下单前过 Policy Engine (预算/授权)
|
|
7
|
+
* - 订单结果 (成交/收益) 记录回经济层 (审计)
|
|
8
|
+
*
|
|
9
|
+
* Polymarket 合约架构 (Polygon):
|
|
10
|
+
* - CLOB (链上订单簿): createOrder/getOrders/cancelOrder (现有 SDK)
|
|
11
|
+
* - CTF 条件代币框架: 市场 = 条件代币对 (Yes/No)
|
|
12
|
+
* - NegRisk 适配器: 互斥结果市场
|
|
13
|
+
* 收益结算在链上 (USDC), Agent Treasury 可接收.
|
|
14
|
+
*/
|
|
15
|
+
import { type CreateOrderParams, type CreateOrderResult } from './createOrder.js';
|
|
16
|
+
import { type GetOrdersParams, type GetOrdersResult } from './getOrders.js';
|
|
17
|
+
export interface EconOrderOptions {
|
|
18
|
+
/** 预算上限 (USDC) — 经 Policy Engine 检查 */
|
|
19
|
+
maxSpendUsdc?: number;
|
|
20
|
+
/** 服务名 (Policy 白名单用, 默认 polymarket) */
|
|
21
|
+
service?: string;
|
|
22
|
+
/** 是否记录经济审计 */
|
|
23
|
+
audit?: boolean;
|
|
24
|
+
/** 注入 Policy 检查回调 (根包 Agent 经济层注入; 子包不跨包 import) */
|
|
25
|
+
policyCheck?: (intent: {
|
|
26
|
+
payTo: string;
|
|
27
|
+
amount: number;
|
|
28
|
+
service?: string;
|
|
29
|
+
}) => Promise<{
|
|
30
|
+
allowed: boolean;
|
|
31
|
+
reason?: string;
|
|
32
|
+
}>;
|
|
33
|
+
/** 注入花费记录回调 */
|
|
34
|
+
recordSpend?: (amount: number) => Promise<void>;
|
|
35
|
+
/** 注入 createOrder 实现 (测试 mock; 默认真实 CLOB) */
|
|
36
|
+
createOrderImpl?: (params: CreateOrderParams) => Promise<CreateOrderResult>;
|
|
37
|
+
}
|
|
38
|
+
export interface EconOrderResult {
|
|
39
|
+
success: boolean;
|
|
40
|
+
order?: CreateOrderResult;
|
|
41
|
+
policy?: {
|
|
42
|
+
allowed: boolean;
|
|
43
|
+
reason?: string;
|
|
44
|
+
};
|
|
45
|
+
error?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 带 Policy 检查的 Polymarket 下单.
|
|
49
|
+
* 流程: 金额 → Policy 授权 (回调注入) → CLOB 下单.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createOrderEcon(params: CreateOrderParams, opts?: EconOrderOptions): Promise<EconOrderResult>;
|
|
52
|
+
/** 查询订单 (含经济层关联信息) */
|
|
53
|
+
export declare function getOrdersEcon(params?: GetOrdersParams): Promise<GetOrdersResult>;
|
|
54
|
+
/** Polymarket 合约架构摘要 (文档化, 供 Agent 理解) */
|
|
55
|
+
export declare const POLYMARKET_CONTRACT_OVERVIEW = "\nPolymarket \u5408\u7EA6\u67B6\u6784 (Polygon):\n- CLOB: \u94FE\u4E0A\u8BA2\u5355\u7C3F (createOrder/getOrders/cancelOrder)\n- CTF (Conditional Token Framework): \u5E02\u573A = \u6761\u4EF6\u4EE3\u5E01\u5BF9 Yes/No\n- NegRisk \u9002\u914D\u5668: \u4E92\u65A5\u7ED3\u679C\u5E02\u573A (\u5982\"\u54EA\u4E2A\u5019\u9009\u4EBA\u80DC\u51FA\")\n- \u7ED3\u7B97: USDC \u94FE\u4E0A, \u6536\u76CA\u53EF\u56DE\u6536\u5230 Agent Treasury\n- Agent \u96C6\u6210: Treasury \u8D44\u91D1 \u2192 Policy \u6388\u6743 \u2192 CLOB \u4E0B\u5355 \u2192 \u7ED3\u679C \u2192 \u6536\u76CA\u56DE\u6D41\n";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* econ-integration.ts — Polymarket × Agent 经济网络集成 (2026-08-13, Task C4)
|
|
3
|
+
*
|
|
4
|
+
* 打通 Agent Economic Network 与 Polymarket 预测市场:
|
|
5
|
+
* - Agent 可用 Treasury 资金在 Polymarket 下单 (预测/对冲)
|
|
6
|
+
* - 下单前过 Policy Engine (预算/授权)
|
|
7
|
+
* - 订单结果 (成交/收益) 记录回经济层 (审计)
|
|
8
|
+
*
|
|
9
|
+
* Polymarket 合约架构 (Polygon):
|
|
10
|
+
* - CLOB (链上订单簿): createOrder/getOrders/cancelOrder (现有 SDK)
|
|
11
|
+
* - CTF 条件代币框架: 市场 = 条件代币对 (Yes/No)
|
|
12
|
+
* - NegRisk 适配器: 互斥结果市场
|
|
13
|
+
* 收益结算在链上 (USDC), Agent Treasury 可接收.
|
|
14
|
+
*/
|
|
15
|
+
import { createOrder as pmCreateOrder } from './createOrder.js';
|
|
16
|
+
import { getOrders as pmGetOrders } from './getOrders.js';
|
|
17
|
+
/**
|
|
18
|
+
* 带 Policy 检查的 Polymarket 下单.
|
|
19
|
+
* 流程: 金额 → Policy 授权 (回调注入) → CLOB 下单.
|
|
20
|
+
*/
|
|
21
|
+
export async function createOrderEcon(params, opts = {}) {
|
|
22
|
+
const amountUsdc = Number(params.price) * Number(params.size ?? 1);
|
|
23
|
+
// 1. Policy 授权 (预算/白名单) — 回调注入, 不跨包
|
|
24
|
+
if (opts.policyCheck) {
|
|
25
|
+
const decision = await opts.policyCheck({
|
|
26
|
+
payTo: 'polymarket-clob',
|
|
27
|
+
amount: amountUsdc,
|
|
28
|
+
service: opts.service || 'polymarket',
|
|
29
|
+
});
|
|
30
|
+
if (!decision.allowed) {
|
|
31
|
+
return { success: false, policy: { allowed: false, reason: `[policy] ${decision.reason}` } };
|
|
32
|
+
}
|
|
33
|
+
await opts.recordSpend?.(amountUsdc).catch(() => { });
|
|
34
|
+
}
|
|
35
|
+
else if (opts.maxSpendUsdc !== undefined && amountUsdc > opts.maxSpendUsdc) {
|
|
36
|
+
return { success: false, policy: { allowed: false, reason: `Polymarket 下单超预算: ${amountUsdc} > ${opts.maxSpendUsdc}` } };
|
|
37
|
+
}
|
|
38
|
+
// 2. CLOB 下单 (可注入 mock, 默认真实)
|
|
39
|
+
try {
|
|
40
|
+
const order = opts.createOrderImpl ? await opts.createOrderImpl(params) : await pmCreateOrder(params);
|
|
41
|
+
return { success: true, order, policy: { allowed: true } };
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
return { success: false, error: `Polymarket 下单失败: ${String(e?.message || e).slice(0, 200)}` };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** 查询订单 (含经济层关联信息) */
|
|
48
|
+
export async function getOrdersEcon(params = {}) {
|
|
49
|
+
return pmGetOrders(params);
|
|
50
|
+
}
|
|
51
|
+
/** Polymarket 合约架构摘要 (文档化, 供 Agent 理解) */
|
|
52
|
+
export const POLYMARKET_CONTRACT_OVERVIEW = `
|
|
53
|
+
Polymarket 合约架构 (Polygon):
|
|
54
|
+
- CLOB: 链上订单簿 (createOrder/getOrders/cancelOrder)
|
|
55
|
+
- CTF (Conditional Token Framework): 市场 = 条件代币对 Yes/No
|
|
56
|
+
- NegRisk 适配器: 互斥结果市场 (如"哪个候选人胜出")
|
|
57
|
+
- 结算: USDC 链上, 收益可回收到 Agent Treasury
|
|
58
|
+
- Agent 集成: Treasury 资金 → Policy 授权 → CLOB 下单 → 结果 → 收益回流
|
|
59
|
+
`;
|
|
@@ -151,15 +151,9 @@ export class OrbitDBAdapter {
|
|
|
151
151
|
await this.node.helia.blockstore.put(CID.parse(rec.id), dagCbor.encode(rec));
|
|
152
152
|
return `bolloon-cid://${rec.id}`;
|
|
153
153
|
}
|
|
154
|
-
/**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
* 返回的 store 是通用适配 (put=keyvalue / add=events).
|
|
158
|
-
*/
|
|
159
|
-
async openStore(name, type = 'keyvalue') {
|
|
160
|
-
await this.ensure();
|
|
161
|
-
const raw = (await this._orbitdb.open(name, { type }));
|
|
162
|
-
const store = {
|
|
154
|
+
/** 打开 store 的通用适配 (keyvalue/events 统一成 OrbitDBStore) */
|
|
155
|
+
wrapStore(raw) {
|
|
156
|
+
return {
|
|
163
157
|
address: raw.address,
|
|
164
158
|
put: async (key, value) => { await raw.put(key, JSON.parse(JSON.stringify(value))); },
|
|
165
159
|
add: async (value) => { await raw.add(JSON.parse(JSON.stringify(value))); },
|
|
@@ -190,7 +184,42 @@ export class OrbitDBAdapter {
|
|
|
190
184
|
};
|
|
191
185
|
},
|
|
192
186
|
};
|
|
193
|
-
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 2026-08-08: 在同一 OrbitDB 实例上打开附加 store。
|
|
190
|
+
* events 类型 = append-only 事件流 (WAL 复制 / 轨迹流用).
|
|
191
|
+
* opts.accessController: 群组用 write:['*'] (任何人可写).
|
|
192
|
+
*/
|
|
193
|
+
async openStore(name, type = 'keyvalue', opts) {
|
|
194
|
+
await this.ensure();
|
|
195
|
+
const options = { type };
|
|
196
|
+
if (opts?.accessController)
|
|
197
|
+
options.accessController = opts.accessController;
|
|
198
|
+
const raw = (await this._orbitdb.open(name, options));
|
|
199
|
+
return this.wrapStore(raw);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* 2026-08-14: 按地址打开远端 store (Agent Gateway 共享网络 / 群组)。
|
|
203
|
+
* replica=true → 只读副本: 拉取复制但不写回 (自动加入网络不污染他人 registry)。
|
|
204
|
+
* replica=false → 可写 (群组: events store 配 write:'*' 时成员可广播)。
|
|
205
|
+
*/
|
|
206
|
+
async openStoreByAddress(address, type = 'keyvalue', opts) {
|
|
207
|
+
await this.ensure();
|
|
208
|
+
try {
|
|
209
|
+
const options = { type };
|
|
210
|
+
if (opts?.replica === false)
|
|
211
|
+
options.replica = false;
|
|
212
|
+
else
|
|
213
|
+
options.replica = true;
|
|
214
|
+
if (opts?.accessController)
|
|
215
|
+
options.accessController = opts.accessController;
|
|
216
|
+
const raw = (await this._orbitdb.open(address, options));
|
|
217
|
+
return this.wrapStore(raw);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
console.warn(`[orbitdb] openStoreByAddress 失败: ${String(err?.message || err).slice(0, 160)}`);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
194
223
|
}
|
|
195
224
|
async close() {
|
|
196
225
|
try {
|
package/dist/web/client.js
CHANGED
|
@@ -1771,6 +1771,275 @@
|
|
|
1771
1771
|
notice.remove();
|
|
1772
1772
|
}, 15e3);
|
|
1773
1773
|
}
|
|
1774
|
+
var gwGroups = [];
|
|
1775
|
+
var gwNetworks = [];
|
|
1776
|
+
var gwActiveChat = null;
|
|
1777
|
+
var gwMsgCache = /* @__PURE__ */ new Map();
|
|
1778
|
+
function gwToast(text) {
|
|
1779
|
+
try {
|
|
1780
|
+
window.showToast?.(text);
|
|
1781
|
+
} catch {
|
|
1782
|
+
}
|
|
1783
|
+
console.log("[gateway]", text);
|
|
1784
|
+
}
|
|
1785
|
+
function gwEsc(s) {
|
|
1786
|
+
try {
|
|
1787
|
+
return escapeHtml2(String(s ?? ""));
|
|
1788
|
+
} catch {
|
|
1789
|
+
return String(s ?? "");
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
function gwModal(html) {
|
|
1793
|
+
const overlay = document.createElement("div");
|
|
1794
|
+
overlay.className = "gw-modal-overlay";
|
|
1795
|
+
overlay.innerHTML = `<div class="gw-modal">${html}<div class="gw-modal-close" title="\u5173\u95ED">\u2715</div></div>`;
|
|
1796
|
+
document.body.appendChild(overlay);
|
|
1797
|
+
const close = () => overlay.remove();
|
|
1798
|
+
overlay.querySelector(".gw-modal-close")?.addEventListener("click", close);
|
|
1799
|
+
overlay.addEventListener("click", (e) => {
|
|
1800
|
+
if (e.target === overlay) close();
|
|
1801
|
+
});
|
|
1802
|
+
return { root: overlay, close };
|
|
1803
|
+
}
|
|
1804
|
+
async function loadGatewaySidebar() {
|
|
1805
|
+
try {
|
|
1806
|
+
const [g, n] = await Promise.all([
|
|
1807
|
+
fetch("/api/gateway/groups").then((r) => r.json()).catch(() => ({ groups: [] })),
|
|
1808
|
+
fetch("/api/gateway/status").then((r) => r.json()).catch(() => ({ networks: [] }))
|
|
1809
|
+
]);
|
|
1810
|
+
gwGroups = Array.isArray(g.groups) ? g.groups : [];
|
|
1811
|
+
gwNetworks = Array.isArray(n.networks) ? n.networks : [];
|
|
1812
|
+
renderGatewaySidebar();
|
|
1813
|
+
} catch {
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
function renderGatewaySidebar() {
|
|
1817
|
+
const list = document.getElementById("gateway-list");
|
|
1818
|
+
if (!list) return;
|
|
1819
|
+
list.innerHTML = "";
|
|
1820
|
+
for (const grp of gwGroups) {
|
|
1821
|
+
const li = document.createElement("li");
|
|
1822
|
+
li.className = "gateway-item";
|
|
1823
|
+
li.innerHTML = `<span class="gateway-item-icon">\u{1F465}</span>
|
|
1824
|
+
<span class="gateway-item-main">
|
|
1825
|
+
<span class="gateway-item-name">${gwEsc(grp.name || "\u7FA4\u7EC4")}</span>
|
|
1826
|
+
<span class="gateway-item-sub">${grp.memberCount ?? 0} \u6210\u5458 \xB7 ${grp.messageCount ?? 0} \u6761\u6D88\u606F</span>
|
|
1827
|
+
</span>`;
|
|
1828
|
+
li.title = "\u6253\u5F00\u7FA4\u804A";
|
|
1829
|
+
li.onclick = () => openGroupChat(grp.id, grp.name || grp.id);
|
|
1830
|
+
list.appendChild(li);
|
|
1831
|
+
}
|
|
1832
|
+
for (const net of gwNetworks) {
|
|
1833
|
+
const li = document.createElement("li");
|
|
1834
|
+
li.className = "gateway-item gateway-item-net";
|
|
1835
|
+
li.innerHTML = `<span class="gateway-item-icon">\u{1F310}</span>
|
|
1836
|
+
<span class="gateway-item-main">
|
|
1837
|
+
<span class="gateway-item-name">${gwEsc(net.name || String(net.link || "").slice(0, 24) || "\u7F51\u7EDC")}</span>
|
|
1838
|
+
<span class="gateway-item-sub">${net.serviceCount ?? 0} \u4E2A\u670D\u52A1</span>
|
|
1839
|
+
</span>`;
|
|
1840
|
+
li.title = `\u670D\u52A1\u7F51\u7EDC: ${net.link || ""}`;
|
|
1841
|
+
list.appendChild(li);
|
|
1842
|
+
}
|
|
1843
|
+
if (!gwGroups.length && !gwNetworks.length) {
|
|
1844
|
+
list.innerHTML = '<li class="sidebar-empty-hint">(\u6682\u65E0\u7F51\u7EDC, \u70B9 + \u52A0\u5165)</li>';
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
function openGatewayJoinModal() {
|
|
1848
|
+
const { root, close } = gwModal(`
|
|
1849
|
+
<div class="gw-join">
|
|
1850
|
+
<h3>\u52A0\u5165 Agent \u7F51\u7EDC / \u7FA4\u7EC4</h3>
|
|
1851
|
+
<p class="gw-hint">\u7C98\u8D34\u9080\u8BF7\u94FE\u63A5 (orbitdb://...), \u81EA\u52A8\u8BC6\u522B\u670D\u52A1\u7F51\u7EDC\u6216\u7FA4\u7EC4.</p>
|
|
1852
|
+
<input class="gw-join-input" placeholder="orbitdb:///orbitdb/...?type=group&name=..." />
|
|
1853
|
+
<div class="gw-join-actions">
|
|
1854
|
+
<button class="gw-join-go">\u52A0\u5165</button>
|
|
1855
|
+
<button class="gw-join-my">\u{1F517} \u6211\u7684\u7F51\u7EDC\u94FE\u63A5</button>
|
|
1856
|
+
</div>
|
|
1857
|
+
<div class="gw-join-result"></div>
|
|
1858
|
+
</div>`);
|
|
1859
|
+
const input2 = root.querySelector(".gw-join-input");
|
|
1860
|
+
const go = root.querySelector(".gw-join-go");
|
|
1861
|
+
const my = root.querySelector(".gw-join-my");
|
|
1862
|
+
const result = root.querySelector(".gw-join-result");
|
|
1863
|
+
go.onclick = async () => {
|
|
1864
|
+
const link = String(input2.value || "").trim();
|
|
1865
|
+
if (!link) {
|
|
1866
|
+
result.textContent = "\u8BF7\u8F93\u5165\u94FE\u63A5";
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if (link.includes("type=group")) {
|
|
1870
|
+
const r = await fetch("/api/gateway/groups/join", {
|
|
1871
|
+
method: "POST",
|
|
1872
|
+
headers: { "Content-Type": "application/json" },
|
|
1873
|
+
body: JSON.stringify({ link })
|
|
1874
|
+
}).then((x) => x.json()).catch(() => ({ ok: false, error: "\u8BF7\u6C42\u5931\u8D25" }));
|
|
1875
|
+
if (r.ok) {
|
|
1876
|
+
result.textContent = r.already ? "\u5DF2\u5728\u8BE5\u7FA4\u7EC4\u4E2D \u2713" : `\u2705 \u5DF2\u52A0\u5165\u7FA4\u7EC4\u300C${r.group?.name || ""}\u300D`;
|
|
1877
|
+
close();
|
|
1878
|
+
loadGatewaySidebar();
|
|
1879
|
+
} else result.textContent = `\u274C ${r.error || "\u52A0\u5165\u5931\u8D25"}`;
|
|
1880
|
+
} else {
|
|
1881
|
+
const r = await fetch("/api/gateway/join", {
|
|
1882
|
+
method: "POST",
|
|
1883
|
+
headers: { "Content-Type": "application/json" },
|
|
1884
|
+
body: JSON.stringify({ link })
|
|
1885
|
+
}).then((x) => x.json()).catch(() => ({ ok: false, error: "\u8BF7\u6C42\u5931\u8D25" }));
|
|
1886
|
+
if (r.ok) {
|
|
1887
|
+
result.textContent = r.already ? "\u5DF2\u5728\u8BE5\u7F51\u7EDC\u4E2D \u2713" : `\u2705 \u5DF2\u52A0\u5165\u7F51\u7EDC (${r.total} \u670D\u52A1, \u65B0\u589E ${r.joined})`;
|
|
1888
|
+
close();
|
|
1889
|
+
loadGatewaySidebar();
|
|
1890
|
+
} else result.textContent = `\u274C ${r.error || "\u52A0\u5165\u5931\u8D25"}`;
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
my.onclick = async () => {
|
|
1894
|
+
const r = await fetch("/api/gateway/link").then((x) => x.json()).catch(() => null);
|
|
1895
|
+
if (r?.link) {
|
|
1896
|
+
try {
|
|
1897
|
+
await navigator.clipboard.writeText(r.link);
|
|
1898
|
+
} catch {
|
|
1899
|
+
}
|
|
1900
|
+
result.innerHTML = `\u6211\u7684\u7F51\u7EDC\u9080\u8BF7\u94FE\u63A5\u5DF2\u590D\u5236:<br><code>${gwEsc(r.link)}</code>`;
|
|
1901
|
+
} else result.textContent = "\u274C \u751F\u6210\u94FE\u63A5\u5931\u8D25 (OrbitDB \u672A\u5C31\u7EEA)";
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
function openGatewayNewGroupModal() {
|
|
1905
|
+
const { root, close } = gwModal(`
|
|
1906
|
+
<div class="gw-join">
|
|
1907
|
+
<h3>\u521B\u5EFA\u7FA4\u7EC4</h3>
|
|
1908
|
+
<p class="gw-hint">\u7FA4\u7EC4 = \u5FAE\u4FE1\u5F0F\u7FA4\u804A, \u5206\u4EAB\u94FE\u63A5\u9080\u8BF7\u5176\u4ED6 Agent / Bolloon \u52A0\u5165.</p>
|
|
1909
|
+
<input class="gw-join-input" placeholder="\u7FA4\u7EC4\u540D (\u5982: \u7814\u7A76\u534F\u4F5C\u7EC4)" />
|
|
1910
|
+
<div class="gw-join-actions"><button class="gw-join-go">\u521B\u5EFA</button></div>
|
|
1911
|
+
<div class="gw-join-result"></div>
|
|
1912
|
+
</div>`);
|
|
1913
|
+
const input2 = root.querySelector(".gw-join-input");
|
|
1914
|
+
const go = root.querySelector(".gw-join-go");
|
|
1915
|
+
const result = root.querySelector(".gw-join-result");
|
|
1916
|
+
go.onclick = async () => {
|
|
1917
|
+
const name = String(input2.value || "").trim();
|
|
1918
|
+
if (!name) {
|
|
1919
|
+
result.textContent = "\u8BF7\u8F93\u5165\u7FA4\u7EC4\u540D";
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
const r = await fetch("/api/gateway/groups", {
|
|
1923
|
+
method: "POST",
|
|
1924
|
+
headers: { "Content-Type": "application/json" },
|
|
1925
|
+
body: JSON.stringify({ name, from: "web-user" })
|
|
1926
|
+
}).then((x) => x.json()).catch(() => ({ ok: false, error: "\u8BF7\u6C42\u5931\u8D25" }));
|
|
1927
|
+
if (r.ok) {
|
|
1928
|
+
result.innerHTML = `\u2705 \u7FA4\u7EC4\u5DF2\u521B\u5EFA.<br><b>\u9080\u8BF7\u94FE\u63A5:</b><br><code>${gwEsc(r.group?.link)}</code><br>\u590D\u5236\u5206\u4EAB\u7ED9\u5176\u4ED6 Bolloon/Agent.`;
|
|
1929
|
+
loadGatewaySidebar();
|
|
1930
|
+
} else result.textContent = `\u274C ${r.error || "\u521B\u5EFA\u5931\u8D25"}`;
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
function openGroupChat(id, name) {
|
|
1934
|
+
gwActiveChat = { id, name };
|
|
1935
|
+
const { root, close } = gwModal(`
|
|
1936
|
+
<div class="gw-chat">
|
|
1937
|
+
<div class="gw-chat-header">
|
|
1938
|
+
<b>\u{1F465} ${gwEsc(name)}</b>
|
|
1939
|
+
<span class="gw-chat-actions">
|
|
1940
|
+
<button class="gw-chat-invite" title="\u590D\u5236\u7FA4\u9080\u8BF7\u94FE\u63A5">\u{1F517} \u9080\u8BF7</button>
|
|
1941
|
+
</span>
|
|
1942
|
+
</div>
|
|
1943
|
+
<div class="gw-chat-msgs" id="gw-msgs-${id}"><div class="gw-msg-empty">\u52A0\u8F7D\u4E2D...</div></div>
|
|
1944
|
+
<div class="gw-chat-input-row">
|
|
1945
|
+
<input class="gw-chat-input" id="gw-input-${id}" placeholder="\u53D1\u6D88\u606F\u7ED9\u7FA4\u6210\u5458... (Enter \u53D1\u9001)" />
|
|
1946
|
+
<button class="gw-chat-send">\u53D1\u9001</button>
|
|
1947
|
+
</div>
|
|
1948
|
+
</div>`);
|
|
1949
|
+
root.querySelector(".gw-chat-invite")?.addEventListener("click", async () => {
|
|
1950
|
+
const r = await fetch(`/api/gateway/groups/${id}/link`).then((x) => x.json()).catch(() => null);
|
|
1951
|
+
if (r?.link) {
|
|
1952
|
+
try {
|
|
1953
|
+
await navigator.clipboard.writeText(r.link);
|
|
1954
|
+
} catch {
|
|
1955
|
+
}
|
|
1956
|
+
gwToast("\u7FA4\u9080\u8BF7\u94FE\u63A5\u5DF2\u590D\u5236, \u5206\u4EAB\u7ED9\u5176\u4ED6 Agent");
|
|
1957
|
+
} else gwToast("\u83B7\u53D6\u94FE\u63A5\u5931\u8D25");
|
|
1958
|
+
});
|
|
1959
|
+
const sendBtn2 = root.querySelector(".gw-chat-send");
|
|
1960
|
+
const input2 = root.querySelector(".gw-chat-input");
|
|
1961
|
+
sendBtn2.onclick = () => {
|
|
1962
|
+
void sendGroupMessage(id);
|
|
1963
|
+
};
|
|
1964
|
+
input2.onkeydown = (e) => {
|
|
1965
|
+
if (e.key === "Enter") {
|
|
1966
|
+
e.preventDefault();
|
|
1967
|
+
void sendGroupMessage(id);
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
void loadGroupMessages(id);
|
|
1971
|
+
}
|
|
1972
|
+
async function loadGroupMessages(id) {
|
|
1973
|
+
const box = document.getElementById(`gw-msgs-${id}`);
|
|
1974
|
+
if (!box) return;
|
|
1975
|
+
const r = await fetch(`/api/gateway/groups/${id}/messages?limit=50`).then((x) => x.json()).catch(() => ({ messages: [] }));
|
|
1976
|
+
gwMsgCache.set(id, Array.isArray(r.messages) ? r.messages : []);
|
|
1977
|
+
renderGroupMessages(id);
|
|
1978
|
+
}
|
|
1979
|
+
function renderGroupMessages(id) {
|
|
1980
|
+
const box = document.getElementById(`gw-msgs-${id}`);
|
|
1981
|
+
if (!box) return;
|
|
1982
|
+
const msgs = gwMsgCache.get(id) || [];
|
|
1983
|
+
const myName = (document.getElementById("user-name")?.textContent || "web-user").trim();
|
|
1984
|
+
box.innerHTML = msgs.map((m) => {
|
|
1985
|
+
const mine = m.from === myName || m.from === "web-user" || m.from === "me";
|
|
1986
|
+
const t = new Date(m.ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1987
|
+
return `<div class="gw-msg ${mine ? "gw-msg-mine" : ""}">
|
|
1988
|
+
<div class="gw-msg-meta"><b>${gwEsc(String(m.from).slice(0, 24))}</b> \xB7 ${t}</div>
|
|
1989
|
+
<div class="gw-msg-text">${gwEsc(String(m.text))}</div>
|
|
1990
|
+
</div>`;
|
|
1991
|
+
}).join("") || '<div class="gw-msg-empty">\u7FA4\u8FD8\u6CA1\u6709\u6D88\u606F, \u8BF4\u70B9\u4EC0\u4E48\u5427 \u{1F44B}</div>';
|
|
1992
|
+
box.scrollTop = box.scrollHeight;
|
|
1993
|
+
}
|
|
1994
|
+
async function sendGroupMessage(id) {
|
|
1995
|
+
const input2 = document.getElementById(`gw-input-${id}`);
|
|
1996
|
+
const text = input2?.value?.trim();
|
|
1997
|
+
if (!text) return;
|
|
1998
|
+
const myName = (document.getElementById("user-name")?.textContent || "web-user").trim();
|
|
1999
|
+
const r = await fetch(`/api/gateway/groups/${id}/message`, {
|
|
2000
|
+
method: "POST",
|
|
2001
|
+
headers: { "Content-Type": "application/json" },
|
|
2002
|
+
body: JSON.stringify({ text, from: myName })
|
|
2003
|
+
}).then((x) => x.json()).catch(() => ({ ok: false, error: "\u8BF7\u6C42\u5931\u8D25" }));
|
|
2004
|
+
if (r.ok) {
|
|
2005
|
+
if (input2) input2.value = "";
|
|
2006
|
+
void loadGroupMessages(id);
|
|
2007
|
+
} else gwToast(r.error || "\u53D1\u9001\u5931\u8D25");
|
|
2008
|
+
}
|
|
2009
|
+
function handleGroupMessage(msg) {
|
|
2010
|
+
const { groupId, message, groupName } = msg || {};
|
|
2011
|
+
if (!groupId || !message) return;
|
|
2012
|
+
const cur = gwActiveChat;
|
|
2013
|
+
if (cur && cur.id === groupId) {
|
|
2014
|
+
const cache = gwMsgCache.get(groupId) || [];
|
|
2015
|
+
cache.push(message);
|
|
2016
|
+
gwMsgCache.set(groupId, cache.slice(-100));
|
|
2017
|
+
renderGroupMessages(groupId);
|
|
2018
|
+
} else {
|
|
2019
|
+
gwToast(`\u{1F4E2} ${groupName || "\u7FA4\u7EC4"} \u65B0\u6D88\u606F: ${String(message.text || "").slice(0, 40)}`);
|
|
2020
|
+
}
|
|
2021
|
+
void loadGatewaySidebar();
|
|
2022
|
+
}
|
|
2023
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
2024
|
+
const addBtn = document.getElementById("gateway-add-btn");
|
|
2025
|
+
if (addBtn) addBtn.onclick = openGatewayJoinModal;
|
|
2026
|
+
const ngBtn = document.getElementById("gateway-new-group-btn");
|
|
2027
|
+
if (ngBtn) ngBtn.onclick = openGatewayNewGroupModal;
|
|
2028
|
+
const gHeader = document.getElementById("gateway-header");
|
|
2029
|
+
if (gHeader) gHeader.onclick = () => {
|
|
2030
|
+
const t = document.getElementById("gateway-toggle");
|
|
2031
|
+
const list = document.getElementById("gateway-list");
|
|
2032
|
+
if (t && list) {
|
|
2033
|
+
const open = t.textContent === "\u25BC";
|
|
2034
|
+
t.textContent = open ? "\u25B6" : "\u25BC";
|
|
2035
|
+
list.style.display = open ? "none" : "";
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
void loadGatewaySidebar();
|
|
2039
|
+
setInterval(() => {
|
|
2040
|
+
void loadGatewaySidebar();
|
|
2041
|
+
}, 3e4);
|
|
2042
|
+
});
|
|
1774
2043
|
var v3GlobalEventSource = null;
|
|
1775
2044
|
function startV3GlobalSSE() {
|
|
1776
2045
|
if (v3GlobalEventSource) return;
|
|
@@ -1779,6 +2048,10 @@
|
|
|
1779
2048
|
v3GlobalEventSource.onmessage = (e) => {
|
|
1780
2049
|
try {
|
|
1781
2050
|
const msg = JSON.parse(e.data);
|
|
2051
|
+
if (msg.type === "group-message") {
|
|
2052
|
+
handleGroupMessage(msg);
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
1782
2055
|
if (msg.type === "remote-chat-sent") {
|
|
1783
2056
|
console.log("[v3] \u6536\u5230 remote-chat-sent:", msg.channelId, "|", String(msg.text || "").slice(0, 30));
|
|
1784
2057
|
try {
|
package/dist/web/index.html
CHANGED
|
@@ -90,6 +90,23 @@
|
|
|
90
90
|
</ul>
|
|
91
91
|
</div>
|
|
92
92
|
|
|
93
|
+
<!-- 2026-08-14: Agent 网络 (Gateway) — 群组群聊 + 服务网络 -->
|
|
94
|
+
<div class="sidebar-section" id="gateway-section">
|
|
95
|
+
<div class="section-header is-compact is-clickable" id="gateway-header">
|
|
96
|
+
<span class="section-header-row">
|
|
97
|
+
<span id="gateway-toggle">▼</span>
|
|
98
|
+
<span class="section-title">Agent 网络</span>
|
|
99
|
+
</span>
|
|
100
|
+
<span class="section-header-actions">
|
|
101
|
+
<button id="gateway-new-group-btn" class="sidebar-mini-btn" title="创建群组 (微信群聊式)">+ 群组</button>
|
|
102
|
+
<button id="gateway-add-btn" class="sidebar-mini-btn" title="粘贴链接加入网络/群组 (orbitdb://...)">+ 加入</button>
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
<ul class="channel-list" id="gateway-list">
|
|
106
|
+
<li class="sidebar-empty-hint">(暂无网络, 点 + 加入)</li>
|
|
107
|
+
</ul>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
93
110
|
<div class="sidebar-footer">
|
|
94
111
|
<div class="user-avatar" id="user-avatar" title="点击查看 DID 身份">
|
|
95
112
|
<span class="avatar-letter" id="avatar-letter">?</span>
|