@bolloon/bolloon-agent 0.3.33 → 0.3.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/agent-identity-store.js +171 -0
- package/dist/agents/pi-sdk-tools.js +43 -4
- package/dist/agents/pi-sdk.js +98 -12
- package/dist/bootstrap/context-manager.js +166 -0
- package/dist/bootstrap/memory-compressor.js +26 -16
- package/dist/bootstrap/snip-collapse.js +39 -35
- package/dist/cli/ink-app.js +16 -3
- package/dist/cli/loading-tui.js +4 -3
- package/dist/index.js +135 -24
- package/dist/orbitdb/agent-tools.js +225 -0
- package/dist/orbitdb/cid-database.js +174 -0
- package/dist/orbitdb/context-store.js +92 -0
- package/dist/orbitdb/ipfs-node.js +82 -0
- package/dist/orbitdb/ui-cid.js +68 -0
- package/dist/security/tool-gate.js +4 -0
- package/dist/web/client.js +51 -5
- package/dist/web/index.html +11 -11
- package/dist/web/server.js +72 -0
- package/package.json +6 -2
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cid-database.ts — 统一 CID 数据库层: CIDDatabase 接口 + OrbitDBAdapter (2026-08-06)
|
|
3
|
+
*
|
|
4
|
+
* 数据模型 (用户设计):
|
|
5
|
+
* {
|
|
6
|
+
* id: CID, // 内容寻址 CID (dag-cbor encode + sha2-256, 内容不变 CID 不变)
|
|
7
|
+
* agentId: string,
|
|
8
|
+
* timestamp: number,
|
|
9
|
+
* type: "memory" | "context" | "state" | "ui" | "knowledge",
|
|
10
|
+
* content: object,
|
|
11
|
+
* metadata: object,
|
|
12
|
+
* version: number, // 版本号 (update 递增)
|
|
13
|
+
* parentId?: string // 上一版本 CID (版本链)
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* 存储:
|
|
17
|
+
* - OrbitDB keyvalue store (持久化 ~/.bolloon/orbitdb/, 数据库名 bolloon-cid-store)
|
|
18
|
+
* - key = record.id (CID), 支持 save/load/update/version/list/share
|
|
19
|
+
* - CID 用 multiformats 本地计算 (不依赖 helia dag API), share() 时才把块放入 helia
|
|
20
|
+
*/
|
|
21
|
+
import { CID } from 'multiformats/cid';
|
|
22
|
+
import * as dagCbor from '@ipld/dag-cbor';
|
|
23
|
+
import { sha256 } from 'multiformats/hashes/sha2';
|
|
24
|
+
import { concat as uint8Concat } from 'uint8arrays/concat';
|
|
25
|
+
import { createOrbitDB } from '@orbitdb/core';
|
|
26
|
+
import { createBolloonIpfs } from './ipfs-node.js';
|
|
27
|
+
import * as path from 'path';
|
|
28
|
+
import * as os from 'os';
|
|
29
|
+
/** 内容 → CID (dag-cbor, sha2-256, codec 0x71); 先 JSON 清洗 (dag-cbor 不支持 undefined) */
|
|
30
|
+
export async function contentToCid(obj) {
|
|
31
|
+
const cleaned = JSON.parse(JSON.stringify(obj)); // 丢弃 undefined 字段
|
|
32
|
+
const bytes = dagCbor.encode(cleaned);
|
|
33
|
+
const hash = await sha256.digest(bytes);
|
|
34
|
+
return CID.createV1(0x71, hash).toString();
|
|
35
|
+
}
|
|
36
|
+
const home = () => process.env.HOME || os.homedir() || '/tmp';
|
|
37
|
+
/**
|
|
38
|
+
* OrbitDB 后端实现。单例: 同一进程只建一个 (helia/OrbitDB 都是重量级节点)。
|
|
39
|
+
*/
|
|
40
|
+
export class OrbitDBAdapter {
|
|
41
|
+
dataDir;
|
|
42
|
+
node = null;
|
|
43
|
+
db = null;
|
|
44
|
+
_orbitdb = null;
|
|
45
|
+
orbitdb;
|
|
46
|
+
constructor(dataDir = path.join(home(), '.bolloon', 'orbitdb')) {
|
|
47
|
+
this.dataDir = dataDir;
|
|
48
|
+
}
|
|
49
|
+
/** 懒初始化: 首次使用时启动 helia + OrbitDB + 打开 keyvalue store */
|
|
50
|
+
async ensure() {
|
|
51
|
+
if (this.db)
|
|
52
|
+
return;
|
|
53
|
+
this.node = await createBolloonIpfs(path.join(this.dataDir, 'ipfs'));
|
|
54
|
+
this._orbitdb = await createOrbitDB({
|
|
55
|
+
ipfs: this.node.helia,
|
|
56
|
+
directory: path.join(this.dataDir, 'stores'),
|
|
57
|
+
});
|
|
58
|
+
this.db = await this._orbitdb.open('bolloon-cid-store', { type: 'keyvalue' });
|
|
59
|
+
// 共享底层实例 (只读暴露)
|
|
60
|
+
this.orbitdb = this._orbitdb;
|
|
61
|
+
}
|
|
62
|
+
async save(data) {
|
|
63
|
+
await this.ensure();
|
|
64
|
+
// 内容寻址: CID 只基于业务内容 (agentId/type/content), 不含时间戳/版本 → 同内容同 CID
|
|
65
|
+
const record = {
|
|
66
|
+
id: await contentToCid({ agentId: data.agentId, type: data.type, content: data.content }),
|
|
67
|
+
agentId: data.agentId,
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
type: data.type,
|
|
70
|
+
content: data.content,
|
|
71
|
+
metadata: data.metadata ?? {},
|
|
72
|
+
version: 1,
|
|
73
|
+
dbAddress: this.db.address,
|
|
74
|
+
};
|
|
75
|
+
// OrbitDB 用 dag-cbor 编码 value, 不支持 undefined 字段 → put 前 JSON 清洗
|
|
76
|
+
await this.db.put(record.id, JSON.parse(JSON.stringify(record)));
|
|
77
|
+
return record;
|
|
78
|
+
}
|
|
79
|
+
async load(cid) {
|
|
80
|
+
await this.ensure();
|
|
81
|
+
const rec = await this.db.get(cid);
|
|
82
|
+
if (rec)
|
|
83
|
+
return rec;
|
|
84
|
+
// KV 无 → 尝试从 helia 拉块 (网络分享的 CID)
|
|
85
|
+
try {
|
|
86
|
+
const stream = this.node.helia.blockstore.get(CID.parse(cid));
|
|
87
|
+
let bytes = new Uint8Array(0);
|
|
88
|
+
for await (const chunk of stream)
|
|
89
|
+
bytes = uint8Concat([bytes, chunk]);
|
|
90
|
+
return dagCbor.decode(bytes);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async update(cid, content, metadata) {
|
|
97
|
+
await this.ensure();
|
|
98
|
+
const old = await this.load(cid);
|
|
99
|
+
if (!old)
|
|
100
|
+
return null;
|
|
101
|
+
const record = {
|
|
102
|
+
id: await contentToCid({ agentId: old.agentId, type: old.type, content }),
|
|
103
|
+
agentId: old.agentId,
|
|
104
|
+
timestamp: Date.now(),
|
|
105
|
+
type: old.type,
|
|
106
|
+
content,
|
|
107
|
+
metadata: metadata ?? old.metadata,
|
|
108
|
+
version: old.version + 1,
|
|
109
|
+
parentId: old.id,
|
|
110
|
+
dbAddress: this.db.address,
|
|
111
|
+
};
|
|
112
|
+
await this.db.put(record.id, JSON.parse(JSON.stringify(record)));
|
|
113
|
+
return record;
|
|
114
|
+
}
|
|
115
|
+
async version(cid) {
|
|
116
|
+
await this.ensure();
|
|
117
|
+
const chain = [];
|
|
118
|
+
let cur = await this.load(cid);
|
|
119
|
+
// 从目标 CID 往回找最老, 再正序返回
|
|
120
|
+
const rev = [];
|
|
121
|
+
let guard = 0;
|
|
122
|
+
while (cur && guard++ < 1000) {
|
|
123
|
+
rev.push(cur);
|
|
124
|
+
cur = cur.parentId ? await this.load(cur.parentId) : null;
|
|
125
|
+
}
|
|
126
|
+
return rev.reverse();
|
|
127
|
+
}
|
|
128
|
+
async list(filter) {
|
|
129
|
+
await this.ensure();
|
|
130
|
+
// OrbitDB keyvalue.all() 返回 [{ key, value, hash }] 数组
|
|
131
|
+
const all = (await this.db.all());
|
|
132
|
+
const records = all.map(e => e.value);
|
|
133
|
+
return records
|
|
134
|
+
.filter(r => {
|
|
135
|
+
if (!r || typeof r !== 'object')
|
|
136
|
+
return false;
|
|
137
|
+
if (filter?.agentId && r.agentId !== filter.agentId)
|
|
138
|
+
return false;
|
|
139
|
+
if (filter?.type && r.type !== filter.type)
|
|
140
|
+
return false;
|
|
141
|
+
return true;
|
|
142
|
+
})
|
|
143
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
144
|
+
}
|
|
145
|
+
async share(cid) {
|
|
146
|
+
await this.ensure();
|
|
147
|
+
const rec = await this.load(cid);
|
|
148
|
+
if (!rec)
|
|
149
|
+
throw new Error(`记录不存在: ${cid}`);
|
|
150
|
+
// 把记录块写入 helia blockstore, 供网络 peers 通过 DHT 拉取
|
|
151
|
+
await this.node.helia.blockstore.put(CID.parse(rec.id), dagCbor.encode(rec));
|
|
152
|
+
return `bolloon-cid://${rec.id}`;
|
|
153
|
+
}
|
|
154
|
+
async close() {
|
|
155
|
+
try {
|
|
156
|
+
await this._orbitdb?.stop();
|
|
157
|
+
}
|
|
158
|
+
catch { /* 忽略 */ }
|
|
159
|
+
try {
|
|
160
|
+
await this.node?.stop();
|
|
161
|
+
}
|
|
162
|
+
catch { /* 忽略 */ }
|
|
163
|
+
this.db = null;
|
|
164
|
+
this._orbitdb = null;
|
|
165
|
+
this.node = null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** 单例访问 (server/CLI 共享) */
|
|
169
|
+
let _adapter = null;
|
|
170
|
+
export function getCIDDatabase() {
|
|
171
|
+
if (!_adapter)
|
|
172
|
+
_adapter = new OrbitDBAdapter();
|
|
173
|
+
return _adapter;
|
|
174
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-store.ts — Context OS 的 CID 化适配层 (2026-08-06)
|
|
3
|
+
*
|
|
4
|
+
* 在现有 Context OS 文件夹体系之上叠加 CID 快照/版本/共享能力 (不动原实现):
|
|
5
|
+
* - saveSnapshot: 抓取当前资产层 (readContextAssets) → 存 CIDDatabase (type: 'context')
|
|
6
|
+
* - restoreContext: 按 agentId 恢复最近快照
|
|
7
|
+
* - contextVersions: 快照版本历史
|
|
8
|
+
* - sharedMemory: 多 agent 共享记忆 (type: 'memory' 记录跨 agent 可见)
|
|
9
|
+
*
|
|
10
|
+
* 架构: Agent → ContextStore → CIDDatabase → OrbitDB → CID → IPFS
|
|
11
|
+
*/
|
|
12
|
+
import { getCIDDatabase, } from './cid-database.js';
|
|
13
|
+
import { readContextAssets } from '../bootstrap/context-os.js';
|
|
14
|
+
/** 快照 → 可用于恢复的上下文文本 (注入 prompt 用) */
|
|
15
|
+
export function formatSnapshot(s) {
|
|
16
|
+
const lines = [`[Context 快照 @${new Date(s.capturedAt).toISOString()}]`];
|
|
17
|
+
for (const [layer, assets] of Object.entries(s.layers)) {
|
|
18
|
+
if (assets.length)
|
|
19
|
+
lines.push(` ${layer}: ${assets.join(', ')}`);
|
|
20
|
+
}
|
|
21
|
+
if (s.memorySummary)
|
|
22
|
+
lines.push(` 记忆: ${s.memorySummary.slice(0, 200)}`);
|
|
23
|
+
if (s.focus)
|
|
24
|
+
lines.push(` focus: ${s.focus}`);
|
|
25
|
+
return lines.join('\n');
|
|
26
|
+
}
|
|
27
|
+
export class ContextStore {
|
|
28
|
+
db;
|
|
29
|
+
constructor(db = getCIDDatabase()) {
|
|
30
|
+
this.db = db;
|
|
31
|
+
}
|
|
32
|
+
/** 抓取当前 Context OS 资产层 → 快照 (与现有 readContextAssets 打通; ctx 可带 identity/channel) */
|
|
33
|
+
async captureCurrentContext(agentId, extra, ctx) {
|
|
34
|
+
const layers = {};
|
|
35
|
+
try {
|
|
36
|
+
const listings = await readContextAssets();
|
|
37
|
+
for (const l of listings) {
|
|
38
|
+
layers[l.layer] = (l.files ?? []).map(a => a.file);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* 资产层读取失败不阻塞快照 */
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
agentId,
|
|
46
|
+
layers,
|
|
47
|
+
memorySummary: extra?.memorySummary,
|
|
48
|
+
focus: extra?.focus,
|
|
49
|
+
capturedAt: Date.now(),
|
|
50
|
+
identity: ctx?.identity,
|
|
51
|
+
channelId: ctx?.channelId,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** 保存快照 → CID 记录 (type: 'context') */
|
|
55
|
+
async saveSnapshot(snapshot) {
|
|
56
|
+
return this.db.save({
|
|
57
|
+
agentId: snapshot.agentId,
|
|
58
|
+
type: 'context',
|
|
59
|
+
content: snapshot,
|
|
60
|
+
metadata: { kind: 'context-snapshot' },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** 恢复: agentId 最近一次快照 */
|
|
64
|
+
async restoreContext(agentId) {
|
|
65
|
+
const snaps = await this.db.list({ agentId, type: 'context' });
|
|
66
|
+
const latest = snaps[snaps.length - 1];
|
|
67
|
+
return latest ? latest.content : null;
|
|
68
|
+
}
|
|
69
|
+
/** 快照版本历史 (全量, 从旧到新) */
|
|
70
|
+
async contextVersions(agentId) {
|
|
71
|
+
return this.db.list({ agentId, type: 'context' });
|
|
72
|
+
}
|
|
73
|
+
/** 多 agent 共享记忆: 全部 memory 记录 (跨 agent 可见), 可指定 agentId */
|
|
74
|
+
async sharedMemory(agentId) {
|
|
75
|
+
return this.db.list(agentId ? { agentId, type: 'memory' } : { type: 'memory' });
|
|
76
|
+
}
|
|
77
|
+
/** 保存一条记忆 (多 agent 共享池) */
|
|
78
|
+
async saveMemory(agentId, content, metadata) {
|
|
79
|
+
return this.db.save({ agentId, type: 'memory', content, metadata: { ...metadata, kind: 'shared-memory' } });
|
|
80
|
+
}
|
|
81
|
+
/** 按 CID 恢复任意记录 (含跨节点分享的) */
|
|
82
|
+
async loadRecord(cid) {
|
|
83
|
+
return this.db.load(cid);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** 单例 */
|
|
87
|
+
let _contextStore = null;
|
|
88
|
+
export function getContextStore() {
|
|
89
|
+
if (!_contextStore)
|
|
90
|
+
_contextStore = new ContextStore();
|
|
91
|
+
return _contextStore;
|
|
92
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ipfs-node.ts — Bolloon 的 OrbitDB 底层 IPFS 节点工厂 (2026-08-06)
|
|
3
|
+
*
|
|
4
|
+
* 基于 helia 7:
|
|
5
|
+
* - createHelia() 内部已 withLibp2p 但**不传 opts** → 无法自定义 services
|
|
6
|
+
* (HeliaInit 没有 libp2p 字段, 传了也被丢弃, 实测服务列表仍是默认 13 个)
|
|
7
|
+
* - 正确姿势: createHeliaLight() (无 libp2p) + 手动 withLibp2p(helia, { services })
|
|
8
|
+
* - OrbitDB 的 P2P 同步依赖 ipfs.libp2p.services.pubsub (sync.js:113) → 必须加 gossipsub
|
|
9
|
+
* - libp2p 的 createLibp2p 是 { ...defaults, ...options } 浅合并: services 整个覆盖,
|
|
10
|
+
* 必须显式列出要保留的默认服务 (dht/identify/keychain/...)
|
|
11
|
+
* - withLibp2p().start() 之后 libp2p getter 才可用 (之前抛 NotStartedError)
|
|
12
|
+
*
|
|
13
|
+
* 跑法: npx tsx scripts/smoke-orbitdb.ts
|
|
14
|
+
*/
|
|
15
|
+
import { createHeliaLight } from 'helia';
|
|
16
|
+
import { withLibp2p } from '@helia/libp2p';
|
|
17
|
+
import * as dagCbor from '@ipld/dag-cbor';
|
|
18
|
+
import * as dagJson from '@ipld/dag-json';
|
|
19
|
+
import * as json from 'multiformats/codecs/json';
|
|
20
|
+
import { sha512 } from 'multiformats/hashes/sha2';
|
|
21
|
+
import { gossipsub } from '@libp2p/gossipsub';
|
|
22
|
+
import { identify, identifyPush } from '@libp2p/identify';
|
|
23
|
+
import { kadDHT } from '@libp2p/kad-dht';
|
|
24
|
+
import { keychain } from '@libp2p/keychain';
|
|
25
|
+
import { autoNAT } from '@libp2p/autonat';
|
|
26
|
+
import { uPnPNAT } from '@libp2p/upnp-nat';
|
|
27
|
+
import { ping } from '@libp2p/ping';
|
|
28
|
+
import { mdns } from '@libp2p/mdns';
|
|
29
|
+
import { circuitRelayServer } from '@libp2p/circuit-relay-v2';
|
|
30
|
+
import { dcutr } from '@libp2p/dcutr';
|
|
31
|
+
import { http } from '@libp2p/http';
|
|
32
|
+
import { delegatedRoutingV1HttpApiClientContentRouting, delegatedRoutingV1HttpApiClientPeerRouting } from '@helia/delegated-routing-v1-http-api-client';
|
|
33
|
+
import { delegatedHTTPRoutingDefaults } from '@helia/delegated-routing-client';
|
|
34
|
+
import { autoTLS } from '@ipshipyard/libp2p-auto-tls';
|
|
35
|
+
import * as path from 'path';
|
|
36
|
+
import * as os from 'os';
|
|
37
|
+
/**
|
|
38
|
+
* 创建 Bolloon 用的 helia 节点 (libp2p 完整默认服务 + gossipsub pubsub)。
|
|
39
|
+
* dataDir 持久化节点身份/数据 (默认 ~/.bolloon/orbitdb-ipfs)。
|
|
40
|
+
*/
|
|
41
|
+
export async function createBolloonIpfs(dataDir) {
|
|
42
|
+
const dir = dataDir ?? path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'orbitdb-ipfs');
|
|
43
|
+
// createHeliaLight 无 libp2p → withLibp2p 手动装配 (可传 services)
|
|
44
|
+
// codecs/hashers 照抄 createHelia 默认: OrbitDB 的 log entry 用 dag-cbor (codec 113),
|
|
45
|
+
// 不注册会报 "Could not load codec for 113"
|
|
46
|
+
const helia = withLibp2p(createHeliaLight({
|
|
47
|
+
codecs: [dagCbor, dagJson, json],
|
|
48
|
+
hashers: [sha512],
|
|
49
|
+
}), {
|
|
50
|
+
// 显式列出服务: createLibp2p 浅合并会覆盖默认 services
|
|
51
|
+
services: {
|
|
52
|
+
pubsub: gossipsub({ emitSelf: true }), // OrbitDB 同步必需; emitSelf 让单机也能 publish (否则 NoPeersSubscribedToTopic)
|
|
53
|
+
autoNAT: autoNAT(),
|
|
54
|
+
autoTLS: autoTLS(),
|
|
55
|
+
dcutr: dcutr(),
|
|
56
|
+
delegatedPeerRouting: delegatedRoutingV1HttpApiClientPeerRouting(delegatedHTTPRoutingDefaults()),
|
|
57
|
+
delegatedContentRouting: delegatedRoutingV1HttpApiClientContentRouting(delegatedHTTPRoutingDefaults()),
|
|
58
|
+
dht: kadDHT(),
|
|
59
|
+
identify: identify(),
|
|
60
|
+
identifyPush: identifyPush(),
|
|
61
|
+
keychain: keychain({ pass: 'bolloon-orbitdb-keychain-pass-2026' }),
|
|
62
|
+
ping: ping(),
|
|
63
|
+
relay: circuitRelayServer(),
|
|
64
|
+
upnp: uPnPNAT(),
|
|
65
|
+
mdns: mdns(),
|
|
66
|
+
http: http(),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
await helia.start();
|
|
70
|
+
const peerId = helia.libp2p.peerId.toString();
|
|
71
|
+
return {
|
|
72
|
+
helia,
|
|
73
|
+
peerId,
|
|
74
|
+
start: async () => { await helia.start(); },
|
|
75
|
+
stop: async () => { await helia.stop(); },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** 从地址字符串解析 OrbitDB 数据库地址的 database name */
|
|
79
|
+
export function dbNameFromAddress(address) {
|
|
80
|
+
const parts = address.split('/');
|
|
81
|
+
return parts[parts.length - 1] || address;
|
|
82
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui-cid.ts — UI 组件 CID 化层 (2026-08-06)
|
|
3
|
+
*
|
|
4
|
+
* 让 UI 组件也可以内容寻址:
|
|
5
|
+
* - saveComponent: 组件定义 (代码 + props schema + theme) → CIDDatabase (type: 'ui')
|
|
6
|
+
* - loadComponent: 按 CID 加载组件定义
|
|
7
|
+
* - versionComponent: 组件版本管理 (复用 CIDDatabase.update 版本链)
|
|
8
|
+
* - loadReactComponent: 从 CID 拉组件代码 → React 组件 (动态渲染)
|
|
9
|
+
*
|
|
10
|
+
* 架构: UI 组件 → UICidStore → CIDDatabase → OrbitDB/CID → IPFS
|
|
11
|
+
* 注: npm 无标准 "UI CID" 库, 按用户减法哲学自研轻量层 (数据层 node 通用,
|
|
12
|
+
* 浏览器渲染集成点留给 Web client)。
|
|
13
|
+
*/
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import { getCIDDatabase, } from './cid-database.js';
|
|
16
|
+
export class UICidStoreImpl {
|
|
17
|
+
db;
|
|
18
|
+
constructor(db = getCIDDatabase()) {
|
|
19
|
+
this.db = db;
|
|
20
|
+
}
|
|
21
|
+
async saveComponent(agentId, def) {
|
|
22
|
+
return this.db.save({
|
|
23
|
+
agentId,
|
|
24
|
+
type: 'ui',
|
|
25
|
+
content: def,
|
|
26
|
+
metadata: { kind: 'ui-component', framework: def.framework },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async loadComponent(cid) {
|
|
30
|
+
const rec = await this.db.load(cid);
|
|
31
|
+
if (!rec)
|
|
32
|
+
return null;
|
|
33
|
+
return rec.content;
|
|
34
|
+
}
|
|
35
|
+
async listComponents(agentId) {
|
|
36
|
+
return this.db.list(agentId ? { agentId, type: 'ui' } : { type: 'ui' });
|
|
37
|
+
}
|
|
38
|
+
async versionComponent(cid, code, extra) {
|
|
39
|
+
const old = await this.loadComponent(cid);
|
|
40
|
+
if (!old)
|
|
41
|
+
return null;
|
|
42
|
+
return this.db.update(cid, {
|
|
43
|
+
...old,
|
|
44
|
+
...extra,
|
|
45
|
+
code,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async loadReactComponent(cid) {
|
|
49
|
+
const def = await this.loadComponent(cid);
|
|
50
|
+
if (!def)
|
|
51
|
+
throw new Error(`组件不存在: ${cid}`);
|
|
52
|
+
if (def.framework !== 'react')
|
|
53
|
+
throw new Error(`不是 React 组件: ${def.framework}`);
|
|
54
|
+
// 动态构造: code 是函数组件源码 → new Function 编译 (受限环境: 无 module 作用域)
|
|
55
|
+
const factory = new Function('React', `return (${def.code})`);
|
|
56
|
+
const component = factory(React);
|
|
57
|
+
if (typeof component !== 'function')
|
|
58
|
+
throw new Error('组件代码必须返回 React 组件函数');
|
|
59
|
+
return { component, def };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** 单例 */
|
|
63
|
+
let _uiStore = null;
|
|
64
|
+
export function getUICidStore() {
|
|
65
|
+
if (!_uiStore)
|
|
66
|
+
_uiStore = new UICidStoreImpl();
|
|
67
|
+
return _uiStore;
|
|
68
|
+
}
|
|
@@ -60,6 +60,10 @@ const TOOL_WHITELIST = new Set([
|
|
|
60
60
|
'fetch_url', 'web_search',
|
|
61
61
|
// 2026-08-02: 远端 channel 工具 (本地智能体 @ 远程交流)
|
|
62
62
|
'list_remote_channels', 'send_to_remote_channel',
|
|
63
|
+
// 2026-08-06: OrbitDB/CID 数据层工具 (src/orbitdb/agent-tools.ts)
|
|
64
|
+
'cid_save', 'cid_load', 'cid_update', 'cid_version', 'cid_list', 'cid_share',
|
|
65
|
+
'context_save_snapshot', 'context_restore',
|
|
66
|
+
'ui_save_component', 'ui_load_component',
|
|
63
67
|
]);
|
|
64
68
|
export const gateWhitelist = { gate: 'whitelist', allowed: true };
|
|
65
69
|
/**
|
package/dist/web/client.js
CHANGED
|
@@ -1546,6 +1546,7 @@
|
|
|
1546
1546
|
var channelNameEl = document.getElementById("channel-name");
|
|
1547
1547
|
var eventSources = /* @__PURE__ */ new Map();
|
|
1548
1548
|
var currentChannelId = null;
|
|
1549
|
+
var activeChannelId = null;
|
|
1549
1550
|
var currentAgentId = "";
|
|
1550
1551
|
var channels = [];
|
|
1551
1552
|
var remoteChannels = [];
|
|
@@ -1671,16 +1672,48 @@
|
|
|
1671
1672
|
async function loadChannels() {
|
|
1672
1673
|
try {
|
|
1673
1674
|
const res = await fetch("/channels");
|
|
1675
|
+
const ct = res.headers.get("content-type") || "";
|
|
1676
|
+
if (!ct.includes("application/json")) {
|
|
1677
|
+
const text = await res.text().catch(() => "");
|
|
1678
|
+
if (!text.trim().startsWith("[") && !text.trim().startsWith("{")) {
|
|
1679
|
+
console.warn("[\u52A0\u8F7D\u9891\u9053] \u68C0\u6D4B\u5230 IPFS \u9759\u6001\u6A21\u5F0F (\u65E0\u540E\u7AEF server), \u529F\u80FD\u53D7\u9650");
|
|
1680
|
+
showStaticModeNotice();
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1674
1684
|
channels = await res.json();
|
|
1675
1685
|
console.log("[\u52A0\u8F7D\u9891\u9053] \u4ECE\u670D\u52A1\u5668\u83B7\u53D6\u5230", channels.length, "\u4E2A\u9891\u9053");
|
|
1676
1686
|
channels.forEach((ch, i) => {
|
|
1677
1687
|
console.log(` [${i}] ${ch.name} - did: "${ch.did}"`);
|
|
1678
1688
|
});
|
|
1689
|
+
try {
|
|
1690
|
+
const ar = await fetch("/active-channel");
|
|
1691
|
+
const a = await ar.json();
|
|
1692
|
+
if (a && a.channelId) {
|
|
1693
|
+
activeChannelId = a.channelId;
|
|
1694
|
+
if (!currentChannelId) currentChannelId = a.channelId;
|
|
1695
|
+
document.title = `Bolloon \xB7 ${a.identity?.name || a.channelId}`;
|
|
1696
|
+
console.log("[\u52A0\u8F7D\u9891\u9053] active channel:", a.channelId, "\u2192", a.identity?.name);
|
|
1697
|
+
}
|
|
1698
|
+
} catch (e) {
|
|
1699
|
+
console.warn("[\u52A0\u8F7D\u9891\u9053] \u8BFB active channel \u5931\u8D25 (\u975E\u81F4\u547D):", e);
|
|
1700
|
+
}
|
|
1679
1701
|
renderChannels();
|
|
1680
1702
|
} catch (err) {
|
|
1681
1703
|
console.error("[\u52A0\u8F7D\u9891\u9053] \u5931\u8D25:", err);
|
|
1682
1704
|
}
|
|
1683
1705
|
}
|
|
1706
|
+
function showStaticModeNotice() {
|
|
1707
|
+
if (document.getElementById("ipfs-static-notice")) return;
|
|
1708
|
+
const notice = document.createElement("div");
|
|
1709
|
+
notice.id = "ipfs-static-notice";
|
|
1710
|
+
notice.style.cssText = "position:fixed;bottom:60px;left:50%;transform:translateX(-50%);z-index:9999;background:#1a1a18;border:1px solid #c4d640;color:#d8d8c8;padding:10px 16px;border-radius:8px;font-size:12px;box-shadow:0 4px 20px rgba(0,0,0,.5);max-width:560px;text-align:center;";
|
|
1711
|
+
notice.innerHTML = `\u{1F4E1} <b style="color:#c4d640">IPFS \u9759\u6001\u6A21\u5F0F</b> \u2014 \u6B64\u9875\u9762\u901A\u8FC7 IPNS \u4ECE\u53BB\u4E2D\u5FC3\u5316\u7F51\u7EDC\u52A0\u8F7D.<br>\u5B8C\u6574\u529F\u80FD (\u5BF9\u8BDD/\u5DE5\u5177/\u5224\u65AD\u529B) \u9700\u8FDE\u63A5\u672C\u5730 Bolloon server: <code style="color:#c4d640">bolloon --web</code>`;
|
|
1712
|
+
document.body.appendChild(notice);
|
|
1713
|
+
setTimeout(() => {
|
|
1714
|
+
notice.remove();
|
|
1715
|
+
}, 15e3);
|
|
1716
|
+
}
|
|
1684
1717
|
var v3GlobalEventSource = null;
|
|
1685
1718
|
function startV3GlobalSSE() {
|
|
1686
1719
|
if (v3GlobalEventSource) return;
|
|
@@ -1924,6 +1957,19 @@ ${msg.text || ""}`, "ai", false, log);
|
|
|
1924
1957
|
console.log(`[v3-friend] \u2705 ack \u6536\u5230: ${name} \u5DF2\u6536\u5230\u597D\u53CB\u7533\u8BF7`);
|
|
1925
1958
|
showSimpleToast(`\u{1F4EC} ${name} \u5DF2\u6536\u5230\u4F60\u7684\u597D\u53CB\u7533\u8BF7, \u7B49\u5BF9\u65B9\u63A5\u53D7`);
|
|
1926
1959
|
}
|
|
1960
|
+
} else if (msg.type === "context_event") {
|
|
1961
|
+
try {
|
|
1962
|
+
const evt = msg.evt || {};
|
|
1963
|
+
if (evt.type === "context.warning") {
|
|
1964
|
+
showSimpleToast(`\u26A0\uFE0F \u4E0A\u4E0B\u6587\u4F7F\u7528\u7387 ${Math.round((evt.usage?.pct || 0) * 100)}%, \u5373\u5C06\u81EA\u52A8\u538B\u7F29`);
|
|
1965
|
+
} else if (evt.type === "context.compress.start") {
|
|
1966
|
+
showSimpleToast(`\u{1F5DC}\uFE0F \u4E0A\u4E0B\u6587\u538B\u7F29\u5F00\u59CB (${(evt.beforeTokens || 0).toLocaleString()} tokens)`);
|
|
1967
|
+
} else if (evt.type === "context.compress.complete") {
|
|
1968
|
+
const s = evt.snapshot || {};
|
|
1969
|
+
showSimpleToast(`\u2713 \u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29: ${((s.beforeTokens || 0) / 1e3).toFixed(0)}k \u2192 ${((s.afterTokens || 0) / 1e3).toFixed(0)}k tokens`);
|
|
1970
|
+
}
|
|
1971
|
+
} catch (ctxErr) {
|
|
1972
|
+
}
|
|
1927
1973
|
}
|
|
1928
1974
|
} catch (err) {
|
|
1929
1975
|
console.error("[v3] \u5168\u5C40 SSE \u89E3\u6790\u5931\u8D25:", err);
|
|
@@ -2182,18 +2228,18 @@ ${msg.text || ""}`, "ai", false, log);
|
|
|
2182
2228
|
}
|
|
2183
2229
|
renderChannels();
|
|
2184
2230
|
}
|
|
2185
|
-
function renderChannelsLite(
|
|
2231
|
+
function renderChannelsLite(activeChannelId2, activeSessionId) {
|
|
2186
2232
|
if (!channelList) return;
|
|
2187
2233
|
channelList.querySelectorAll(".agent-row").forEach((row) => {
|
|
2188
2234
|
const li = row.closest(".agent-group");
|
|
2189
2235
|
const chId = li?.dataset.channelId;
|
|
2190
|
-
row.classList.toggle("active", chId ===
|
|
2236
|
+
row.classList.toggle("active", chId === activeChannelId2);
|
|
2191
2237
|
});
|
|
2192
|
-
if (
|
|
2193
|
-
const activeLi = channelList.querySelector(`.agent-group[data-channel-id="${
|
|
2238
|
+
if (activeChannelId2) expandedAgents.add(activeChannelId2);
|
|
2239
|
+
const activeLi = channelList.querySelector(`.agent-group[data-channel-id="${activeChannelId2}"]`);
|
|
2194
2240
|
if (activeLi) {
|
|
2195
2241
|
activeLi.classList.add("expanded");
|
|
2196
|
-
const ch = channels.find((c) => c.id ===
|
|
2242
|
+
const ch = channels.find((c) => c.id === activeChannelId2);
|
|
2197
2243
|
activeLi.querySelectorAll(".session-item").forEach((sessLi) => {
|
|
2198
2244
|
const sessId = sessLi.dataset.sessionId;
|
|
2199
2245
|
const shouldBeActive = sessId === activeSessionId;
|
package/dist/web/index.html
CHANGED
|
@@ -6,20 +6,20 @@
|
|
|
6
6
|
<title>Bolloon Agent</title>
|
|
7
7
|
|
|
8
8
|
<!-- Favicon -->
|
|
9
|
-
<link rel="icon" type="image/x-icon" href="
|
|
10
|
-
<link rel="icon" type="image/png" sizes="32x32" href="
|
|
11
|
-
<link rel="icon" type="image/png" sizes="16x16" href="
|
|
9
|
+
<link rel="icon" type="image/x-icon" href="./icons/favicon.ico">
|
|
10
|
+
<link rel="icon" type="image/png" sizes="32x32" href="./icons/favicon-32x32.png">
|
|
11
|
+
<link rel="icon" type="image/png" sizes="16x16" href="./icons/favicon-16x16.png">
|
|
12
12
|
|
|
13
13
|
<!-- Apple Touch Icon -->
|
|
14
|
-
<link rel="apple-touch-icon" href="
|
|
14
|
+
<link rel="apple-touch-icon" href="./icons/apple-touch-icon.png">
|
|
15
15
|
|
|
16
16
|
<!-- PWA Manifest -->
|
|
17
|
-
<link rel="manifest" href="
|
|
17
|
+
<link rel="manifest" href="./manifest.json">
|
|
18
18
|
|
|
19
19
|
<!-- 2026-06-11: 移除 Google Fonts + jsdelivr 外部 CDN 阻塞 — 在大陆/跨公网 timeout 拖慢首屏/返回主页
|
|
20
20
|
字体: style.css 只用字面量 'JetBrains Mono', 系统有就用, 没有自动 fall back monospace
|
|
21
21
|
marked/qrcode: 改 async 不阻塞 (下载失败时本地降级到 escape 文本, 不影响主聊天) -->
|
|
22
|
-
<link rel="stylesheet" href="
|
|
22
|
+
<link rel="stylesheet" href="./style.css">
|
|
23
23
|
<script async src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
24
24
|
<script async src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
|
|
25
25
|
</head>
|
|
@@ -414,11 +414,11 @@
|
|
|
414
414
|
</main>
|
|
415
415
|
</div>
|
|
416
416
|
|
|
417
|
-
<script type="module" src="
|
|
418
|
-
<script type="module" src="
|
|
419
|
-
<script type="module" src="
|
|
420
|
-
<script type="module" src="
|
|
417
|
+
<script type="module" src="./components/wallet-viem.mjs"></script>
|
|
418
|
+
<script type="module" src="./components/p2p/index.js"></script>
|
|
419
|
+
<script type="module" src="./ui/step-timeline.js"></script>
|
|
420
|
+
<script type="module" src="./ui/message-renderer.js"></script>
|
|
421
421
|
<!-- 2026-07-06: client.js 含 import 语句 (safeChannelName 兜底), 必须 type="module" -->
|
|
422
|
-
<script type="module" src="
|
|
422
|
+
<script type="module" src="./client.js"></script>
|
|
423
423
|
</body>
|
|
424
424
|
</html>
|
package/dist/web/server.js
CHANGED
|
@@ -1644,6 +1644,20 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1644
1644
|
console.warn('[ipfs] Kubo 自动安装失败 (非致命):', e?.message?.slice(0, 120));
|
|
1645
1645
|
}
|
|
1646
1646
|
})();
|
|
1647
|
+
// 2026-08-06: ContextManager 事件 → SSE broadcast (CLI/Web 状态栏实时同步)
|
|
1648
|
+
try {
|
|
1649
|
+
const { getContextManager } = await import('../bootstrap/context-manager.js');
|
|
1650
|
+
getContextManager().onEvent((evt) => {
|
|
1651
|
+
try {
|
|
1652
|
+
broadcast({ type: 'context_event', evt }, undefined);
|
|
1653
|
+
}
|
|
1654
|
+
catch { /* 广播失败静默 */ }
|
|
1655
|
+
});
|
|
1656
|
+
console.log('[context] ContextManager 事件已接入 SSE (context_event)');
|
|
1657
|
+
}
|
|
1658
|
+
catch (e) {
|
|
1659
|
+
console.warn('[context] ContextManager 事件接入失败 (非致命):', e?.message?.slice(0, 120));
|
|
1660
|
+
}
|
|
1647
1661
|
// 2026-08-03: 初始化 MCP 适配器 (读 ~/.mcp.json, 自动握手发现工具).
|
|
1648
1662
|
// 后台异步: 不阻塞启动, 失败静默 (agent 调 mcp_list_tools 时再触发).
|
|
1649
1663
|
(async () => {
|
|
@@ -4031,6 +4045,52 @@ ${goalDesc}
|
|
|
4031
4045
|
res.status(500).json({ error: err.message });
|
|
4032
4046
|
}
|
|
4033
4047
|
});
|
|
4048
|
+
// 2026-08-06: active channel (统一 Agent Identity) — CLI /channel 与 Web UI 共用
|
|
4049
|
+
// active-channel.json 是唯一持久化点: CLI 切换写, Web 读同一文件 → 两边一致
|
|
4050
|
+
const ACTIVE_CHANNEL_FILE = `${process.env.HOME || '/tmp'}/.bolloon/active-channel.json`;
|
|
4051
|
+
const CHANNELS_JSON = `${process.env.HOME || '/tmp'}/.bolloon/sessions/channels.json`;
|
|
4052
|
+
async function readActiveChannel() {
|
|
4053
|
+
try {
|
|
4054
|
+
const a = JSON.parse(await fs.readFile(ACTIVE_CHANNEL_FILE, 'utf-8'));
|
|
4055
|
+
if (!a || typeof a.channelId !== 'string')
|
|
4056
|
+
return { channelId: null };
|
|
4057
|
+
// 解析 identity name (persona.name 优先)
|
|
4058
|
+
let channels = [];
|
|
4059
|
+
try {
|
|
4060
|
+
channels = JSON.parse(await fs.readFile(CHANNELS_JSON, 'utf-8'));
|
|
4061
|
+
}
|
|
4062
|
+
catch { /* */ }
|
|
4063
|
+
const ch = channels.find((c) => c?.id === a.channelId);
|
|
4064
|
+
const name = ch?.persona?.name?.trim() || ch?.name || ch?.agentId || 'agent';
|
|
4065
|
+
return { channelId: a.channelId, identity: { id: a.channelId, name, channelId: a.channelId } };
|
|
4066
|
+
}
|
|
4067
|
+
catch {
|
|
4068
|
+
return { channelId: null };
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
app.get('/active-channel', async (_req, res) => {
|
|
4072
|
+
try {
|
|
4073
|
+
const a = await readActiveChannel();
|
|
4074
|
+
res.json(a);
|
|
4075
|
+
}
|
|
4076
|
+
catch (err) {
|
|
4077
|
+
res.status(500).json({ error: err.message });
|
|
4078
|
+
}
|
|
4079
|
+
});
|
|
4080
|
+
app.post('/active-channel', async (req, res) => {
|
|
4081
|
+
try {
|
|
4082
|
+
const channelId = String(req.body?.channelId || '').trim();
|
|
4083
|
+
if (!channelId)
|
|
4084
|
+
return res.status(400).json({ error: 'channelId 必填' });
|
|
4085
|
+
await fs.mkdir(path.dirname(ACTIVE_CHANNEL_FILE), { recursive: true });
|
|
4086
|
+
await fs.writeFile(ACTIVE_CHANNEL_FILE, JSON.stringify({ channelId, updatedAt: Date.now() }, null, 2), 'utf-8');
|
|
4087
|
+
const a = await readActiveChannel();
|
|
4088
|
+
res.json(a);
|
|
4089
|
+
}
|
|
4090
|
+
catch (err) {
|
|
4091
|
+
res.status(500).json({ error: err.message });
|
|
4092
|
+
}
|
|
4093
|
+
});
|
|
4034
4094
|
// v3: 列出本节点缓存的远端 channel (按 peerId 分组)
|
|
4035
4095
|
app.get('/api/remote-channels', async (_req, res) => {
|
|
4036
4096
|
try {
|
|
@@ -5699,6 +5759,18 @@ ${goalDesc}
|
|
|
5699
5759
|
res.status(500).json({ error: err.message });
|
|
5700
5760
|
}
|
|
5701
5761
|
});
|
|
5762
|
+
// 2026-08-06: Context OS 资源管理 API — 上下文用量 + 最近一次压缩快照 (Web UI 同步)
|
|
5763
|
+
app.get('/api/context/usage', async (_req, res) => {
|
|
5764
|
+
try {
|
|
5765
|
+
const { getContextManager, loadLatestSnapshot } = await import('../bootstrap/context-manager.js');
|
|
5766
|
+
const usage = getContextManager().getUsage();
|
|
5767
|
+
const latest = await loadLatestSnapshot();
|
|
5768
|
+
res.json({ ok: true, usage, latestSnapshot: latest });
|
|
5769
|
+
}
|
|
5770
|
+
catch (err) {
|
|
5771
|
+
res.status(500).json({ error: err.message });
|
|
5772
|
+
}
|
|
5773
|
+
});
|
|
5702
5774
|
app.get('/api/iroh/info', async (_req, res) => {
|
|
5703
5775
|
if (!irohInitialized || !irohNodeInfo) {
|
|
5704
5776
|
res.json({ initialized: false });
|