@bolloon/bolloon-agent 0.3.42 → 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/agents/skill-writer.js +70 -14
- package/dist/storage/did-catalog.js +231 -0
- package/dist/web/server.js +75 -0
- package/package.json +1 -1
|
@@ -146,8 +146,26 @@ export async function writeSkillCandidate(c) {
|
|
|
146
146
|
const dir = getCandidateDir();
|
|
147
147
|
await fs.mkdir(dir, { recursive: true });
|
|
148
148
|
const safeName = sanitizeSkillName(c.name);
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
// 2026-08-08: 有 signature 的候选用固定文件名 (合并更新同一个), 无 signature 才带时间戳
|
|
150
|
+
const file = c.signature
|
|
151
|
+
? path.join(dir, `${safeName}.json`)
|
|
152
|
+
: path.join(dir, `${safeName}-${Date.now()}.json`);
|
|
153
|
+
// 追加式合并: 若同 signature 已存在, 累积 runs + 追加 body
|
|
154
|
+
let runs = c.runs ?? 1;
|
|
155
|
+
let body = c.body;
|
|
156
|
+
try {
|
|
157
|
+
const prev = JSON.parse(await fs.readFile(file, 'utf-8'));
|
|
158
|
+
if (prev && prev.runs)
|
|
159
|
+
runs = prev.runs + 1;
|
|
160
|
+
if (prev && prev.body && body !== prev.body && c.signature) {
|
|
161
|
+
// 同一 signature 重复运行 → 追加一条经验 (去重, 避免 body 膨胀)
|
|
162
|
+
const line = `- ${new Date().toISOString().slice(0, 16)} ${c.source}: ${c.description}`;
|
|
163
|
+
body = `${prev.body}\n${line}`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch { /* 新文件 */ }
|
|
167
|
+
const merged = { ...c, runs, body, timestamp: c.timestamp || new Date().toISOString() };
|
|
168
|
+
await fs.writeFile(file, JSON.stringify(merged, null, 2), 'utf-8');
|
|
151
169
|
return file;
|
|
152
170
|
}
|
|
153
171
|
export async function listSkillCandidates(home = os.homedir()) {
|
|
@@ -165,12 +183,37 @@ export async function listSkillCandidates(home = os.homedir()) {
|
|
|
165
183
|
const raw = await fs.readFile(path.join(dir, f), 'utf-8');
|
|
166
184
|
const c = JSON.parse(raw);
|
|
167
185
|
if (c.name && c.body)
|
|
168
|
-
out.push(c);
|
|
186
|
+
out.push({ ...c, file: path.join(dir, f) });
|
|
169
187
|
}
|
|
170
188
|
catch { /* 坏文件跳过 */ }
|
|
171
189
|
}
|
|
172
190
|
return out;
|
|
173
191
|
}
|
|
192
|
+
/** 按名字删除所有同名候选文件 (名可能与文件名前缀不完全一致) */
|
|
193
|
+
async function removeCandidateFiles(name, home) {
|
|
194
|
+
const safe = sanitizeSkillName(name);
|
|
195
|
+
const dir = getCandidateDir(home);
|
|
196
|
+
let files;
|
|
197
|
+
try {
|
|
198
|
+
files = await fs.readdir(dir);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
for (const f of files) {
|
|
204
|
+
if (!f.endsWith('.json'))
|
|
205
|
+
continue;
|
|
206
|
+
try {
|
|
207
|
+
const c = JSON.parse(await fs.readFile(path.join(dir, f), 'utf-8'));
|
|
208
|
+
if (sanitizeSkillName(c.name) === safe)
|
|
209
|
+
await fs.rm(path.join(dir, f), { force: true });
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
if (f.startsWith(safe))
|
|
213
|
+
await fs.rm(path.join(dir, f), { force: true });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
174
217
|
/** 把候选转正为正式 skill (可选: 转正后删除候选文件) */
|
|
175
218
|
export async function promoteCandidate(name, opts = {}, home = os.homedir()) {
|
|
176
219
|
const candidates = await listSkillCandidates(home);
|
|
@@ -179,18 +222,25 @@ export async function promoteCandidate(name, opts = {}, home = os.homedir()) {
|
|
|
179
222
|
return { ok: false, path: '', error: `候选 '${name}' 不存在` };
|
|
180
223
|
const r = await createSkill(c.name, c.description, c.body, opts);
|
|
181
224
|
if (r.ok) {
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
const dir = getCandidateDir(home);
|
|
185
|
-
for (const f of (await fs.readdir(dir))) {
|
|
186
|
-
if (f.startsWith(sanitizeSkillName(c.name) + '-'))
|
|
187
|
-
await fs.rm(path.join(dir, f), { force: true });
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
catch { /* 清理失败不阻塞 */ }
|
|
225
|
+
// 清理已转正的候选文件: 同 name 的所有候选
|
|
226
|
+
await removeCandidateFiles(c.name, home);
|
|
191
227
|
}
|
|
192
228
|
return r;
|
|
193
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* 从一轮成功的工具调用生成稳定签名 — 同一套工具序列 (有序去重, 最多 4 个) 视为同一经验.
|
|
232
|
+
* 用于跨运行合并: 第二次跑同样的工具 → 更新同一个候选, 而不是新建一个.
|
|
233
|
+
*/
|
|
234
|
+
export function toolSignature(okSteps) {
|
|
235
|
+
const seq = [];
|
|
236
|
+
for (const s of (okSteps || [])) {
|
|
237
|
+
if (s.name && !seq.includes(s.name))
|
|
238
|
+
seq.push(s.name);
|
|
239
|
+
if (seq.length >= 4)
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
return seq.join('_');
|
|
243
|
+
}
|
|
194
244
|
export async function writeRunEndSkillCandidates(steps, source, minOk = 2) {
|
|
195
245
|
const okSteps = (steps || []).filter((s) => s.status === 'ok' && s.name && s.name !== 'system' && s.name !== '?');
|
|
196
246
|
if (okSteps.length < minOk) {
|
|
@@ -200,13 +250,19 @@ export async function writeRunEndSkillCandidates(steps, source, minOk = 2) {
|
|
|
200
250
|
const body = `## 背景\n本轮对话连续成功调用了 ${okSteps.length} 个工具: ${toolNames}.\n\n` +
|
|
201
251
|
`## 流程\n${okSteps.map((s) => `1. 调用 ${s.name}${s.output ? ': ' + String(s.output).slice(0, 120) : ''}`).join('\n')}\n\n` +
|
|
202
252
|
`## 注意事项\n- 工具名以 list_skills / get_operation_logs 的实际注册名为准\n- 沉淀为正式 skill 前请人工确认流程可复用\n`;
|
|
203
|
-
|
|
253
|
+
// 2026-08-08: 稳定签名 + 固定文件名 → 同一套工具反复跑时合并更新到同一个候选 (runs++)
|
|
254
|
+
const signature = toolSignature(okSteps);
|
|
255
|
+
const candName = `auto-${signature}`;
|
|
256
|
+
const existing = (await listSkillCandidates()).find((x) => x.signature === signature || sanitizeSkillName(x.name) === sanitizeSkillName(candName));
|
|
204
257
|
const file = await writeSkillCandidate({
|
|
205
258
|
name: candName,
|
|
206
259
|
description: `自动候选: ${okSteps.length} 个工具连续成功 (${toolNames})`,
|
|
207
260
|
body,
|
|
208
261
|
source,
|
|
209
262
|
timestamp: new Date().toISOString(),
|
|
263
|
+
signature,
|
|
210
264
|
});
|
|
211
|
-
|
|
265
|
+
const merged = !!existing;
|
|
266
|
+
const runs = (existing?.runs ?? 0) + 1;
|
|
267
|
+
return { wrote: true, file, count: okSteps.length, names: toolNames, merged, runs };
|
|
212
268
|
}
|
|
@@ -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 {
|