@bolloon/bolloon-agent 0.3.43 → 0.3.44
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/storage/did-catalog.js +231 -0
- package/dist/web/server.js +75 -0
- package/package.json +1 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* did-catalog.ts — 以用户 DID 为唯一标识的 Postgres 式关系目录 (2026-08-08)
|
|
3
|
+
*
|
|
4
|
+
* 借鉴 Postgres 标准设计:
|
|
5
|
+
* - 表 (table) 由列 (columns) 定义, 每个表有主键 (dsc_key, 默认 = row 的 id 字段)
|
|
6
|
+
* - 每张表 + 每行都以主键 did 分区: 同一用户的全部数据通过 userDid 绑定
|
|
7
|
+
* - 变更写入日志 (WAL: append-only event log), 与目录一起落盘
|
|
8
|
+
* - 多设备同步 = 拉取另一台设备的 WAL → 回放 (replay) → 按 updatedAt LWW 合并
|
|
9
|
+
*
|
|
10
|
+
* 数据模型 (Postgres 式 DDL):
|
|
11
|
+
* TABLE memory (did, dsc_key, content, meta, updatedAt, deviceId)
|
|
12
|
+
* TABLE vocal (did, agentId, persona/name, did, publicKey...)
|
|
13
|
+
* TABLE on_policy (did, policyVersion, policy(JSON), updatedAt)
|
|
14
|
+
* TABLE skills ...
|
|
15
|
+
* TABLE tools / plugins / mcp / context_os ...
|
|
16
|
+
*
|
|
17
|
+
* WAL 事件谱:
|
|
18
|
+
* { seq, did, table, op: upsert|delete, key, dscid, row, ts, deviceId }
|
|
19
|
+
* 回放/合并规则: 同 (did,table,dscid) → 取 updatedAt 较大者 (LWW); 更新更大的 `rev`
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'fs/promises';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import * as os from 'os';
|
|
24
|
+
export const ALL_TABLES = [
|
|
25
|
+
'memory', 'persona', 'on_policy', 'skills', 'tools', 'plugins', 'mcp', 'context_os', 'channels',
|
|
26
|
+
];
|
|
27
|
+
const homeDir = (h) => {
|
|
28
|
+
if (h)
|
|
29
|
+
return h;
|
|
30
|
+
return process.env.HOME || os.homedir() || '/tmp';
|
|
31
|
+
};
|
|
32
|
+
/** 安全 DID → 目录名片段 */
|
|
33
|
+
export function didDirName(did) {
|
|
34
|
+
return did.split(':').pop()?.substring(0, 24) || 'unknown';
|
|
35
|
+
}
|
|
36
|
+
export class DidCatalog {
|
|
37
|
+
did;
|
|
38
|
+
deviceId;
|
|
39
|
+
root;
|
|
40
|
+
tables = {};
|
|
41
|
+
wal = [];
|
|
42
|
+
nextSeq = 0;
|
|
43
|
+
dirty = false;
|
|
44
|
+
constructor(did, opts = {}) {
|
|
45
|
+
this.did = did;
|
|
46
|
+
this.deviceId = opts.deviceId || 'device1';
|
|
47
|
+
const base = path.join(homeDir(opts.home), '.bolloon', 'did-catalog');
|
|
48
|
+
// 以 did 分区: ~/.bolloon/did-catalog/<did>/table.json + wal.jsonl
|
|
49
|
+
this.root = path.join(base, didDirName(did));
|
|
50
|
+
for (const t of ALL_TABLES)
|
|
51
|
+
this.tables[t] = new Map();
|
|
52
|
+
}
|
|
53
|
+
get rootPath() { return this.root; }
|
|
54
|
+
tablePath(t) { return path.join(this.root, `${t}.json`); }
|
|
55
|
+
walPath() { return path.join(this.root, 'wal.jsonl'); }
|
|
56
|
+
/** 加载本地持久化 (表 + WAL)。幂等, 可重复调。 */
|
|
57
|
+
async load() {
|
|
58
|
+
await fs.mkdir(this.root, { recursive: true });
|
|
59
|
+
for (const t of ALL_TABLES) {
|
|
60
|
+
try {
|
|
61
|
+
const raw = JSON.parse(await fs.readFile(this.tablePath(t), 'utf-8'));
|
|
62
|
+
this.tables[t] = new Map(Object.entries(raw));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
this.tables[t] = new Map();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// WAL 回放自身劫
|
|
69
|
+
this.wal = [];
|
|
70
|
+
try {
|
|
71
|
+
const text = await fs.readFile(this.walPath(), 'utf-8');
|
|
72
|
+
for (const line of text.split('\n')) {
|
|
73
|
+
if (!line.trim())
|
|
74
|
+
continue;
|
|
75
|
+
try {
|
|
76
|
+
const e = JSON.parse(line);
|
|
77
|
+
this.wal.push(e);
|
|
78
|
+
if (e.seq >= this.nextSeq)
|
|
79
|
+
this.nextSeq = e.seq + 1;
|
|
80
|
+
}
|
|
81
|
+
catch { /* 坏行跳过 */ }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch { /* 无 WAL */ }
|
|
85
|
+
}
|
|
86
|
+
/** 无参 upsert / write-return. @param keys -> 主键自动填 */
|
|
87
|
+
async upsert(table, key, data) {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const existing = this.tables[table]?.get(key);
|
|
90
|
+
const ev = {
|
|
91
|
+
seq: this.nextSeq++,
|
|
92
|
+
did: this.did,
|
|
93
|
+
table,
|
|
94
|
+
op: 'upsert',
|
|
95
|
+
key,
|
|
96
|
+
row: { ...existing?.data, ...data, dscKey: key, updatedAt: now, deviceId: this.deviceId },
|
|
97
|
+
ts: now,
|
|
98
|
+
deviceId: this.deviceId,
|
|
99
|
+
};
|
|
100
|
+
this.tables[table].set(key, {
|
|
101
|
+
dscKey: key,
|
|
102
|
+
data: ev.row,
|
|
103
|
+
updatedAt: now,
|
|
104
|
+
deviceId: this.deviceId,
|
|
105
|
+
});
|
|
106
|
+
this.wal.push(ev);
|
|
107
|
+
this.dirty = true;
|
|
108
|
+
return ev;
|
|
109
|
+
}
|
|
110
|
+
/** 删除一行 */
|
|
111
|
+
async remove(table, key) {
|
|
112
|
+
if (!this.tables[table]?.has(key))
|
|
113
|
+
return null;
|
|
114
|
+
const ev = {
|
|
115
|
+
seq: this.nextSeq++, did: this.did, table, op: 'delete', key,
|
|
116
|
+
row: null, ts: Date.now(), deviceId: this.deviceId,
|
|
117
|
+
};
|
|
118
|
+
this.tables[table].delete(key);
|
|
119
|
+
this.wal.push(ev);
|
|
120
|
+
this.dirty = true;
|
|
121
|
+
return ev;
|
|
122
|
+
}
|
|
123
|
+
/** 读一行 */
|
|
124
|
+
get(table, key) {
|
|
125
|
+
return this.tables[table]?.get(key);
|
|
126
|
+
}
|
|
127
|
+
/** 全表 */
|
|
128
|
+
all(table) {
|
|
129
|
+
return Array.from((this.tables[table] || new Map()).entries()).map(([k, r]) => ({ key: k, row: r }));
|
|
130
|
+
}
|
|
131
|
+
/** 基础查询: 按字段精确匹配 (Postgres WHERE col = v) */
|
|
132
|
+
where(table, col, value) {
|
|
133
|
+
return this.all(table).filter(({ row }) => {
|
|
134
|
+
const v = row.data[col];
|
|
135
|
+
return String(v) === String(value);
|
|
136
|
+
}).map(({ row }) => row);
|
|
137
|
+
}
|
|
138
|
+
get walEvents() { return Array.from(this.wal); }
|
|
139
|
+
/**
|
|
140
|
+
* 多设备同步入口: 接收一台设备的 WAL 事件流, 按 updatedAt LWW 合并.
|
|
141
|
+
* 返回 {applied, merged} — applied=本机新增, merged=LWW 冲突保留较大者.
|
|
142
|
+
*/
|
|
143
|
+
syncRemote(events) {
|
|
144
|
+
const byKey = new Map();
|
|
145
|
+
for (const e of events) {
|
|
146
|
+
if (!e.table || !e.key)
|
|
147
|
+
continue;
|
|
148
|
+
if (e.did && e.did !== this.did)
|
|
149
|
+
continue; // 只收本用户 DID 的数据
|
|
150
|
+
const signature = `${e.table}::${e.key}`;
|
|
151
|
+
if (e.op === 'delete') {
|
|
152
|
+
byKey.set(signature, e);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const prior = byKey.get(signature);
|
|
156
|
+
if (!prior || (e.ts ?? 0) >= (prior.ts ?? 0))
|
|
157
|
+
byKey.set(signature, e);
|
|
158
|
+
}
|
|
159
|
+
let applied = 0, merged = 0;
|
|
160
|
+
for (const e of byKey.values()) {
|
|
161
|
+
if (e.op === 'delete') {
|
|
162
|
+
const had = this.tables[e.table]?.has(e.key);
|
|
163
|
+
if (had) {
|
|
164
|
+
this.tables[e.table]?.delete(e.key);
|
|
165
|
+
applied++;
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const cur = this.tables[e.table]?.get(e.key);
|
|
170
|
+
if (cur && (cur.updatedAt ?? 0) > (e.ts ?? 0)) {
|
|
171
|
+
merged++;
|
|
172
|
+
continue;
|
|
173
|
+
} // 本地更新 → 保留本地
|
|
174
|
+
this.tables[e.table]?.set(e.key, {
|
|
175
|
+
dscKey: e.key,
|
|
176
|
+
data: e.row,
|
|
177
|
+
updatedAt: e.ts ?? Date.now(),
|
|
178
|
+
deviceId: e.deviceId || 'remote',
|
|
179
|
+
});
|
|
180
|
+
applied++;
|
|
181
|
+
}
|
|
182
|
+
this.dirty = true;
|
|
183
|
+
return { applied, merged };
|
|
184
|
+
}
|
|
185
|
+
/** 持久化表 + WAL 到磁盘 */
|
|
186
|
+
async persist() {
|
|
187
|
+
await fs.mkdir(this.root, { recursive: true });
|
|
188
|
+
for (const t of ALL_TABLES) {
|
|
189
|
+
const obj = {};
|
|
190
|
+
for (const [k, r] of this.tables[t])
|
|
191
|
+
obj[k] = r;
|
|
192
|
+
await fs.writeFile(this.tablePath(t), JSON.stringify(obj, null, 2), 'utf-8');
|
|
193
|
+
}
|
|
194
|
+
const lines = this.wal.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
195
|
+
await fs.writeFile(this.walPath(), lines, 'utf-8');
|
|
196
|
+
this.dirty = false;
|
|
197
|
+
}
|
|
198
|
+
get isDirty() { return this.dirty; }
|
|
199
|
+
}
|
|
200
|
+
/** 轻量单例注册: 同 DID 复用同一目录实例 (跨模块共享) */
|
|
201
|
+
export function createDidCatalogRegistry(opts = {}) {
|
|
202
|
+
const cache = new Map();
|
|
203
|
+
return {
|
|
204
|
+
async open(did) {
|
|
205
|
+
if (!did)
|
|
206
|
+
throw new Error('did 必填');
|
|
207
|
+
if (!cache.has(did)) {
|
|
208
|
+
cache.set(did, (async () => {
|
|
209
|
+
const c = new DidCatalog(did, opts);
|
|
210
|
+
await c.load();
|
|
211
|
+
return c;
|
|
212
|
+
})());
|
|
213
|
+
}
|
|
214
|
+
return cache.get(did);
|
|
215
|
+
},
|
|
216
|
+
async persistAll() {
|
|
217
|
+
for (const p of cache.values()) {
|
|
218
|
+
const c = await p;
|
|
219
|
+
if (c.isDirty)
|
|
220
|
+
await c.persist();
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/** 进程级默认注册表 (server / CLI 共用, 同 DID 复用实例) */
|
|
226
|
+
const _defaultRegistry = createDidCatalogRegistry();
|
|
227
|
+
/** 按 did 打开的目录 (默认注册表单例) — 供各端点快速接入 */
|
|
228
|
+
export async function registryOpen(did, opts) {
|
|
229
|
+
const reg = opts ? createDidCatalogRegistry(opts) : _defaultRegistry;
|
|
230
|
+
return reg.open(did);
|
|
231
|
+
}
|
package/dist/web/server.js
CHANGED
|
@@ -2759,6 +2759,65 @@ ${goalDesc}
|
|
|
2759
2759
|
res.status(500).json({ error: err.message });
|
|
2760
2760
|
}
|
|
2761
2761
|
});
|
|
2762
|
+
// 2026-08-08: DID 目录 (Postgres 式) — 以用户 DID 为主键, 绑定 memory/persona/skills/on_policy/context_os 等表
|
|
2763
|
+
app.get('/api/did-catalog/:table', async (req, res) => {
|
|
2764
|
+
try {
|
|
2765
|
+
const table = String(req.params.table || '');
|
|
2766
|
+
const { registryOpen, ALL_TABLES } = await import('../storage/did-catalog.js');
|
|
2767
|
+
if (!ALL_TABLES.includes(table)) {
|
|
2768
|
+
return res.status(400).json({ error: `未知表: ${table}`, tables: ALL_TABLES });
|
|
2769
|
+
}
|
|
2770
|
+
const identity = await loadOrCreateUserIdentity();
|
|
2771
|
+
if (!identity.did)
|
|
2772
|
+
return res.status(404).json({ error: '未生成用户 DID' });
|
|
2773
|
+
const cat = await registryOpen(identity.did);
|
|
2774
|
+
const rows = cat.all(table).map(({ key, row }) => ({ key, data: row.data, updatedAt: row.updatedAt, deviceId: row.deviceId }));
|
|
2775
|
+
res.json({ did: identity.did, table, rows, deviceId: cat.deviceId });
|
|
2776
|
+
}
|
|
2777
|
+
catch (err) {
|
|
2778
|
+
res.status(500).json({ error: err.message });
|
|
2779
|
+
}
|
|
2780
|
+
});
|
|
2781
|
+
// 写入 DID 目录单行 (以用户 DID 为分区主键) — 供插件/工具把各类数据绑定到用户
|
|
2782
|
+
app.post('/api/did-catalog/:table', async (req, res) => {
|
|
2783
|
+
try {
|
|
2784
|
+
const table = String(req.params.table || '');
|
|
2785
|
+
const { key, data } = req.body || {};
|
|
2786
|
+
const { registryOpen, ALL_TABLES } = await import('../storage/did-catalog.js');
|
|
2787
|
+
if (!ALL_TABLES.includes(table)) {
|
|
2788
|
+
return res.status(400).json({ error: `未知表: ${table}`, tables: ALL_TABLES });
|
|
2789
|
+
}
|
|
2790
|
+
if (!key)
|
|
2791
|
+
return res.status(400).json({ error: 'key 必填' });
|
|
2792
|
+
const identity = await loadOrCreateUserIdentity();
|
|
2793
|
+
if (!identity.did)
|
|
2794
|
+
return res.status(404).json({ error: '未生成用户 DID' });
|
|
2795
|
+
const cat = await registryOpen(identity.did);
|
|
2796
|
+
const ev = await cat.upsert(table, String(key), (data && typeof data === 'object' ? data : {}));
|
|
2797
|
+
res.json({ ok: true, did: identity.did, table, key, seq: ev.seq, deviceId: cat.deviceId });
|
|
2798
|
+
}
|
|
2799
|
+
catch (err) {
|
|
2800
|
+
res.status(500).json({ error: err.message });
|
|
2801
|
+
}
|
|
2802
|
+
});
|
|
2803
|
+
// 拉取某台的 WAL → 多设备同步合并
|
|
2804
|
+
app.post('/api/did-catalog/sync', async (req, res) => {
|
|
2805
|
+
try {
|
|
2806
|
+
const { events } = req.body || {};
|
|
2807
|
+
if (!Array.isArray(events))
|
|
2808
|
+
return res.status(400).json({ error: 'events 必填数组' });
|
|
2809
|
+
const identity = await loadOrCreateUserIdentity();
|
|
2810
|
+
if (!identity.did)
|
|
2811
|
+
return res.status(404).json({ error: '未生成用户 DID' });
|
|
2812
|
+
const { registryOpen } = await import('../storage/did-catalog.js');
|
|
2813
|
+
const cat = await registryOpen(identity.did);
|
|
2814
|
+
const r = cat.syncRemote(events);
|
|
2815
|
+
res.json({ ok: true, did: identity.did, applied: r.applied, merged: r.merged });
|
|
2816
|
+
}
|
|
2817
|
+
catch (err) {
|
|
2818
|
+
res.status(500).json({ error: err.message });
|
|
2819
|
+
}
|
|
2820
|
+
});
|
|
2762
2821
|
// 2026-07-01 (v0.2.6): 前后端分离核心 — 后端切 LLM 输出为结构化 segments
|
|
2763
2822
|
// - POST /api/segment-reply { reply, knownTools }
|
|
2764
2823
|
// - 返回 ChatSegment[] (think / text / env_details / tool_call / final)
|
|
@@ -6567,6 +6626,22 @@ ${goalDesc}
|
|
|
6567
6626
|
const success = writePolicy(newPolicy);
|
|
6568
6627
|
if (success) {
|
|
6569
6628
|
auditShellCall('allowed', 'api:PUT:/api/self-improve/policy', [], `人类用户更新策略`);
|
|
6629
|
+
// 2026-08-08: 策略版本写入 DID 目录 (on-policy 以用户 did 绑定)
|
|
6630
|
+
try {
|
|
6631
|
+
const identity = await loadOrCreateUserIdentity();
|
|
6632
|
+
if (identity.did) {
|
|
6633
|
+
const { registryOpen } = await import('../storage/did-catalog.js');
|
|
6634
|
+
const cat = await registryOpen(identity.did);
|
|
6635
|
+
await cat.upsert('on_policy', `pol-${newPolicy.version || 0}`, {
|
|
6636
|
+
version: newPolicy.version || 0,
|
|
6637
|
+
policy: JSON.parse(JSON.stringify(newPolicy)),
|
|
6638
|
+
updatedAt: Date.now(),
|
|
6639
|
+
});
|
|
6640
|
+
}
|
|
6641
|
+
}
|
|
6642
|
+
catch (e) {
|
|
6643
|
+
console.warn('[did-catalog] 记录 on-policy 失败(非致命):', e?.message);
|
|
6644
|
+
}
|
|
6570
6645
|
res.json({ ok: true, message: '策略已更新, 60 秒内生效' });
|
|
6571
6646
|
}
|
|
6572
6647
|
else {
|