@yottameta/yotta-memory 0.6.5 → 0.8.0
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/README.md +23 -13
- package/SKILL.md +59 -21
- package/USER_GUIDE.md +76 -7
- package/bin/yotta-memory.js +1569 -91
- package/package.json +2 -2
- package/references/pinyin-common.json +1 -0
- package/references/protocol.md +162 -121
package/bin/yotta-memory.js
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
// v0.4.2 新增:iam/whoami 智能体身份自注册(agents.json 唯一性)+ 自我档案 PREF + 私密记忆须声明 owner
|
|
6
6
|
// v0.5.3 新增:CLI 选项可前置(--agent 等允许放在子命令前,不再误报"未知命令")/ lan enable 成功补"服务不会立刻启动"提示(v0.5.2 lan 引号修复无回归)
|
|
7
7
|
// v0.5.4 新增:lan enable 在非管理员(schtasks Access denied)时自动降级为用户级 Startup 静默自启(VBS sh.Run 窗口0 + autostart.cmd,node 路径 process.execPath 自动探测);lan disable/status 同时管理计划任务与 Startup 双机制
|
|
8
|
-
// v0.6.0
|
|
9
|
-
// v0.6.1
|
|
8
|
+
// v0.6.0 新增:profile(用户画像聚合,零推断)/ context(开工上下文包)/ iam 扩展(--name/--user/--relationship)/ remember --verify(写后回读)与 --no-hint(关闭类型启发式提示)+ SKILL「记忆守则」
|
|
9
|
+
// v0.6.1 新增:context --budget(token 预算,近记忆按剩余预算放行)/ context 内嵌「多智能体接入铁律」段 / remember --source/--weight(来源 + 重要性权重,去重 weight 取 max)/ 近期记忆排序融合 importance(confidence×recency+updated+weight+immutable)
|
|
10
10
|
// v0.6.3 修复:lan 开机自启 VBS 自愈——VBS 内联 autostart.cmd 内容,启动时自动重建 .cmd(根治 80070002:wscript 找不到被引用启动文件)
|
|
11
11
|
// v0.6.4 新增:lan 命令扩展 Linux——systemd 用户单元(systemctl --user enable/start,登录自启;--onstart 附加 loginctl enable-linger 开机即启)/ systemd 不可用时自动降级用户 crontab @reboot;lanPlatform 测试钩子(YOTTA_LAN_PLATFORM)
|
|
12
12
|
// v0.6.5 修复:recall/context 对同一文件显示 2 条——projectRoot 与 userRoot 指向同一目录(如 cwd=home 或其父)时同一索引被遍历两次;新增 memoryRoots() 唯一化根,hasGrant/recallCore/forgetCore/cmdReindex/contextCore 统一走 memoryRoots()
|
|
13
|
+
// v0.7.0 新增:私密区机制级加密(AES-256-GCM 信封加密 + PBKDF2 主密钥 + 恢复钥匙)/ 每 owner 加密索引 / 用户查看平台 yotta-memory view / migrate 迁移 / reset-password / key 授权 / context 收工纪律 / init 新建默认加密(--no-encrypt 降级)
|
|
14
|
+
// v0.8.0 新增:自我学习/自我进化/自我提升——语义检索(同义词/拼音/字段加权/模糊 + 可选 embedding 插件协议预留)/ feedback 显式反馈闭环(weight/confidence/feedback_net 演化)/ maintain 规则层自组织(统一效用分 utility + 归档/遗忘/去重,默认 dry-run)/ distill 心理日志蒸馏(启发式统计/主题画像/知识地图 + 可选 --model)
|
|
13
15
|
// v0.6.2 修复:remember --verify 写后回读改为直查索引 + 权限判定 + 召回匹配性(不再依赖 recall top-N 排序,消除泛化 subject 下偶发误报「回读未命中」)
|
|
14
16
|
'use strict';
|
|
15
17
|
|
|
@@ -20,7 +22,7 @@ const crypto = require('crypto');
|
|
|
20
22
|
const http = require('http');
|
|
21
23
|
const child_process = require('child_process');
|
|
22
24
|
|
|
23
|
-
const VERSION = '0.
|
|
25
|
+
const VERSION = '0.8.0';
|
|
24
26
|
const TYPES = ['FACT', 'PREF', 'BOUND', 'COMMIT'];
|
|
25
27
|
const TYPE_DIRS = { FACT: 'facts', PREF: 'prefs', BOUND: 'bounds', COMMIT: 'commits' };
|
|
26
28
|
const PUBLIC_DIR = 'facts';
|
|
@@ -29,7 +31,7 @@ const PRIVATE_LEAF = ['prefs', 'bounds', 'commits'];
|
|
|
29
31
|
const ARCHIVE_DIR = '.archive';
|
|
30
32
|
const INDEX_FILE = 'index.json';
|
|
31
33
|
const PRIVATE_TYPES = ['PREF', 'BOUND', 'COMMIT'];
|
|
32
|
-
const FIELD_ORDER = ['type', 'subject', 'statement', 'confidence', 'created', 'updated', 'tags', 'immutable', 'scope', 'owner', 'source', 'weight', 'access_count', 'last_accessed'];
|
|
34
|
+
const FIELD_ORDER = ['type', 'subject', 'statement', 'confidence', 'created', 'updated', 'tags', 'immutable', 'scope', 'owner', 'source', 'weight', 'access_count', 'last_accessed', 'feedback_net'];
|
|
33
35
|
const CONFIG_FILE = 'config.json';
|
|
34
36
|
const SERVER_SUBDIR = '.server';
|
|
35
37
|
const TOKENS_FILE = 'tokens.json';
|
|
@@ -141,11 +143,197 @@ function tokenize(s) {
|
|
|
141
143
|
return toks;
|
|
142
144
|
}
|
|
143
145
|
function buildTokens(subject, statement, tags) {
|
|
144
|
-
const toks = tokenize(subject + ' ' + statement + ' ' + tags.join(' '));
|
|
145
146
|
const m = {};
|
|
146
|
-
|
|
147
|
+
const add = function (tok, w) { if (tok) m[tok] = (m[tok] || 0) + w; };
|
|
148
|
+
const subj = String(subject || '');
|
|
149
|
+
const stmt = String(statement || '');
|
|
150
|
+
const tagArr = Array.isArray(tags) ? tags : parseTags(tags);
|
|
151
|
+
// v0.8.0 字段加权:subject × 3 / tags × 2 / statement × 1
|
|
152
|
+
for (const t of tokenize(subj)) add(t, 3);
|
|
153
|
+
for (const t of tokenize(stmt)) add(t, 1);
|
|
154
|
+
for (const t of tokenize(tagArr.join(' '))) add(t, 2);
|
|
155
|
+
// v0.8.0 拼音 token(py: 全拼 / pyi: 首字母),命中按 0.4 权重(去重防长文本重复字虚高)
|
|
156
|
+
for (const t of new Set(pinyinTokens(subj + ' ' + stmt + ' ' + tagArr.join(' ')))) add(t, 0.4);
|
|
147
157
|
return m;
|
|
148
158
|
}
|
|
159
|
+
// ---- v0.8.0 自我学习:拼音表 + 同义词表 + 语义匹配(零依赖内置数据)----
|
|
160
|
+
function loadPinyinTable() {
|
|
161
|
+
try {
|
|
162
|
+
const p = path.join(__dirname, '..', 'references', 'pinyin-common.json');
|
|
163
|
+
return JSON.parse(fs.readFileSync(p, 'utf8')) || {};
|
|
164
|
+
} catch (e) { return {}; }
|
|
165
|
+
}
|
|
166
|
+
const PINYIN_TABLE = loadPinyinTable();
|
|
167
|
+
// 同义词组:同组内互相视为同义(查询侧扩展,命中按 0.5 权重)
|
|
168
|
+
const SYNONYM_GROUPS = [
|
|
169
|
+
['记', '记住', '记忆', '记录', '铭记'],
|
|
170
|
+
['忘', '忘记', '遗忘', '忘掉'],
|
|
171
|
+
['读', '读取', '检索', '查询', '搜索'],
|
|
172
|
+
['写', '写入', '记录', '保存', '存储'],
|
|
173
|
+
['偏好', '喜好', '喜欢', '偏爱'],
|
|
174
|
+
['承诺', '保证', '约定'],
|
|
175
|
+
['收工', '结束', '完成', '完毕'],
|
|
176
|
+
['归档', '封存', '存档'],
|
|
177
|
+
['删除', '移除', '去掉'],
|
|
178
|
+
['更新', '修改', '刷新'],
|
|
179
|
+
['边界', '红线', '禁区', '界限'],
|
|
180
|
+
['画像', '档案', 'profile'],
|
|
181
|
+
['命令', '指令', 'cli'],
|
|
182
|
+
['记忆', 'memory', '记忆库']
|
|
183
|
+
];
|
|
184
|
+
function synonymSet(token) {
|
|
185
|
+
const out = new Set();
|
|
186
|
+
for (const g of SYNONYM_GROUPS) {
|
|
187
|
+
if (g.indexOf(token) !== -1) { for (const w of g) out.add(w); }
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
function pinyinOf(ch) {
|
|
192
|
+
const arr = PINYIN_TABLE[ch];
|
|
193
|
+
if (!arr || !arr.length) return null;
|
|
194
|
+
return arr[0];
|
|
195
|
+
}
|
|
196
|
+
// 中文字符串 -> 拼音 token:全拼 bigram 'py:yueyong' + 首字母 bigram 'pyi:yy'
|
|
197
|
+
function pinyinTokens(s) {
|
|
198
|
+
const out = [];
|
|
199
|
+
const han = String(s || '').match(/[\u4e00-\u9fa5]+/g);
|
|
200
|
+
if (!han) return out;
|
|
201
|
+
const all = han.join('');
|
|
202
|
+
if (all.length === 1) {
|
|
203
|
+
const p = pinyinOf(all);
|
|
204
|
+
if (p) { out.push('py:' + p); out.push('pyi:' + p[0]); }
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
let initials = '';
|
|
208
|
+
for (let i = 0; i < all.length; i++) { const p = pinyinOf(all[i]); if (p) initials += p[0]; }
|
|
209
|
+
for (let i = 0; i < all.length - 1; i++) {
|
|
210
|
+
const p1 = pinyinOf(all[i]), p2 = pinyinOf(all[i + 1]);
|
|
211
|
+
if (p1 && p2) out.push('py:' + p1 + p2);
|
|
212
|
+
}
|
|
213
|
+
for (let i = 0; i < initials.length - 1; i++) out.push('pyi:' + initials.slice(i, i + 2));
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
function editDistance(a, b) {
|
|
217
|
+
a = String(a); b = String(b);
|
|
218
|
+
if (a === b) return 0;
|
|
219
|
+
const la = a.length, lb = b.length;
|
|
220
|
+
if (!la) return lb; if (!lb) return la;
|
|
221
|
+
if (Math.abs(la - lb) > 2) return 3;
|
|
222
|
+
let prev = [];
|
|
223
|
+
for (let j = 0; j <= lb; j++) prev[j] = j;
|
|
224
|
+
for (let i = 1; i <= la; i++) {
|
|
225
|
+
const cur = [i];
|
|
226
|
+
for (let j = 1; j <= lb; j++) {
|
|
227
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
228
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
|
|
229
|
+
}
|
|
230
|
+
prev = cur;
|
|
231
|
+
}
|
|
232
|
+
return prev[lb];
|
|
233
|
+
}
|
|
234
|
+
// 语义匹配:精确(1.0×,字段加权已入 tokens) / 同义(0.5×) / 拼音(0.4×) / 子串兜底 / 模糊(0.3×)
|
|
235
|
+
function semanticMatch(entry, query, wantDetail) {
|
|
236
|
+
const q = String(query || '').toLowerCase();
|
|
237
|
+
const qtoks = tokenize(q);
|
|
238
|
+
const qpy = pinyinTokens(q);
|
|
239
|
+
const toks = entry.tokens || {};
|
|
240
|
+
const hay = ((entry.subject || '') + ' ' + (entry.statement || '') + ' ' + (entry.tags || []).join(' ')).toLowerCase();
|
|
241
|
+
let score = 0;
|
|
242
|
+
const detail = [];
|
|
243
|
+
const qset = new Set(qtoks);
|
|
244
|
+
for (const tt of qtoks) {
|
|
245
|
+
if (toks[tt]) { score += toks[tt]; if (wantDetail) detail.push('精确:' + tt + '(+' + round3(toks[tt]) + ')'); }
|
|
246
|
+
}
|
|
247
|
+
const ext = new Set();
|
|
248
|
+
for (const tt of qtoks) { const s = synonymSet(tt); for (const w of s) ext.add(w); }
|
|
249
|
+
for (const w of ext) {
|
|
250
|
+
if (toks[w] && !qset.has(w)) { score += 0.5 * toks[w]; if (wantDetail) detail.push('同义:' + w + '(+' + round3(0.5 * toks[w]) + ')'); }
|
|
251
|
+
}
|
|
252
|
+
for (const t of qpy) {
|
|
253
|
+
if (toks[t]) { score += 0.4 * toks[t]; if (wantDetail) detail.push('拼音:' + t + '(+' + round3(0.4 * toks[t]) + ')'); }
|
|
254
|
+
}
|
|
255
|
+
// 查询为拼音(拉丁 token)时,匹配条目侧 py:/pyi: token(去前缀)
|
|
256
|
+
for (const tt of qtoks) {
|
|
257
|
+
if (/^[a-z]+$/.test(tt)) {
|
|
258
|
+
if (toks['py:' + tt]) { score += 0.4 * toks['py:' + tt]; if (wantDetail) detail.push('拼音:' + tt + '(+' + round3(0.4 * toks['py:' + tt]) + ')'); }
|
|
259
|
+
if (toks['pyi:' + tt]) { score += 0.4 * toks['pyi:' + tt]; if (wantDetail) detail.push('拼音首字母:' + tt + '(+' + round3(0.4 * toks['pyi:' + tt]) + ')'); }
|
|
260
|
+
// 首字母串(如 yyyd)拆 bigram 匹配 pyi: token
|
|
261
|
+
if (tt.length >= 3) {
|
|
262
|
+
for (let i = 0; i < tt.length - 1; i++) {
|
|
263
|
+
const bg = tt.slice(i, i + 2);
|
|
264
|
+
if (toks['pyi:' + bg]) { score += 0.4 * toks['pyi:' + bg]; if (wantDetail) detail.push('拼音首字母:' + bg + '(+' + round3(0.4 * toks['pyi:' + bg]) + ')'); }
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (score === 0 && hay.indexOf(q) !== -1) { score = 1; if (wantDetail) detail.push('子串命中'); }
|
|
270
|
+
if (score === 0) {
|
|
271
|
+
const longQtoks = qtoks.filter(function (t) { return t.length >= 4; });
|
|
272
|
+
const keys = Object.keys(toks).filter(function (k) { return k.length >= 4 && k.indexOf(':') === -1; });
|
|
273
|
+
outer:
|
|
274
|
+
for (const t of longQtoks) {
|
|
275
|
+
for (const k of keys) {
|
|
276
|
+
if (Math.abs(k.length - t.length) <= 2 && editDistance(k, t) <= 2) {
|
|
277
|
+
score += 0.3 * toks[k];
|
|
278
|
+
if (wantDetail) detail.push('模糊:' + t + '~' + k + '(+' + round3(0.3 * toks[k]) + ')');
|
|
279
|
+
break outer;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return { score: score, detail: detail };
|
|
285
|
+
}
|
|
286
|
+
function round3(n) { return Math.round(n * 1000) / 1000; }
|
|
287
|
+
// ---- v0.8.0 自我进化:统一效用分(盖棺分)----
|
|
288
|
+
function feedbackNetOf(meta) { return parseInt(meta.feedback_net || '0', 10) || 0; }
|
|
289
|
+
function utilityScore(meta) {
|
|
290
|
+
const conf = Math.min(Math.max(parseFloat(meta.confidence || 1.0) || 1.0, 0), 1);
|
|
291
|
+
const acc = parseInt(meta.access_count || '0', 10) || 0;
|
|
292
|
+
const fb = feedbackNetOf(meta);
|
|
293
|
+
const usageScore = 0.5 * (Math.min(acc, 20) / 20) + 0.5 * (fb / (1 + Math.abs(fb)));
|
|
294
|
+
const last = meta.last_accessed || meta.created || '';
|
|
295
|
+
let recencyScore = 0;
|
|
296
|
+
if (last) {
|
|
297
|
+
const d = daysBetween(last, today());
|
|
298
|
+
recencyScore = Math.max(0, 1 - d / 365);
|
|
299
|
+
}
|
|
300
|
+
const typeScore = ({ BOUND: 1.0, COMMIT: 0.9, PREF: 0.8, FACT: 0.6 })[String(meta.type || 'FACT').toUpperCase()] || 0.6;
|
|
301
|
+
let structureScore = 0;
|
|
302
|
+
if (meta.source) structureScore += 0.2;
|
|
303
|
+
const tags = parseTags(meta.tags);
|
|
304
|
+
if (tags.length) structureScore += 0.3;
|
|
305
|
+
if (meta.subject) structureScore += 0.2;
|
|
306
|
+
if (meta.confidence !== undefined && meta.confidence !== null && meta.confidence !== '') structureScore += 0.1;
|
|
307
|
+
structureScore = Math.min(structureScore, 1.0);
|
|
308
|
+
const w = parseFloat(meta.weight || '1.0');
|
|
309
|
+
const weight = (w > 0 ? w : 1.0);
|
|
310
|
+
return ((0.30 * conf) + (0.25 * usageScore) + (0.20 * recencyScore) + (0.15 * typeScore) + (0.10 * structureScore)) * weight;
|
|
311
|
+
}
|
|
312
|
+
// utility 分项明细(explain / 审计用)
|
|
313
|
+
function utilityBreakdown(meta) {
|
|
314
|
+
const conf = Math.min(Math.max(parseFloat(meta.confidence || 1.0) || 1.0, 0), 1);
|
|
315
|
+
const acc = parseInt(meta.access_count || '0', 10) || 0;
|
|
316
|
+
const fb = feedbackNetOf(meta);
|
|
317
|
+
const usageScore = 0.5 * (Math.min(acc, 20) / 20) + 0.5 * (fb / (1 + Math.abs(fb)));
|
|
318
|
+
const last = meta.last_accessed || meta.created || '';
|
|
319
|
+
let recencyScore = 0;
|
|
320
|
+
if (last) { const d = daysBetween(last, today()); recencyScore = Math.max(0, 1 - d / 365); }
|
|
321
|
+
const typeScore = ({ BOUND: 1.0, COMMIT: 0.9, PREF: 0.8, FACT: 0.6 })[String(meta.type || 'FACT').toUpperCase()] || 0.6;
|
|
322
|
+
let structureScore = 0;
|
|
323
|
+
if (meta.source) structureScore += 0.2;
|
|
324
|
+
const tags = parseTags(meta.tags);
|
|
325
|
+
if (tags.length) structureScore += 0.3;
|
|
326
|
+
if (meta.subject) structureScore += 0.2;
|
|
327
|
+
if (meta.confidence !== undefined && meta.confidence !== null && meta.confidence !== '') structureScore += 0.1;
|
|
328
|
+
structureScore = Math.min(structureScore, 1.0);
|
|
329
|
+
const w = parseFloat(meta.weight || '1.0');
|
|
330
|
+
const weight = (w > 0 ? w : 1.0);
|
|
331
|
+
return {
|
|
332
|
+
confidence: round3(0.30 * conf), usage: round3(0.25 * usageScore), recency: round3(0.20 * recencyScore),
|
|
333
|
+
type: round3(0.15 * typeScore), structure: round3(0.10 * structureScore), weight: round3(weight),
|
|
334
|
+
total: round3(((0.30 * conf) + (0.25 * usageScore) + (0.20 * recencyScore) + (0.15 * typeScore) + (0.10 * structureScore)) * weight)
|
|
335
|
+
};
|
|
336
|
+
}
|
|
149
337
|
function frontmatterToText(meta, body) {
|
|
150
338
|
const lines = ['---'];
|
|
151
339
|
const ordered = FIELD_ORDER.filter(function (k) { return meta[k] !== undefined && meta[k] !== null && meta[k] !== ''; });
|
|
@@ -173,33 +361,249 @@ function nextSeq(dir) {
|
|
|
173
361
|
let max = 0;
|
|
174
362
|
if (fs.existsSync(dir)) {
|
|
175
363
|
for (const f of fs.readdirSync(dir)) {
|
|
176
|
-
const m = f.match(/^\d{4}-\d{2}-\d{2}-(\d{4})\.md
|
|
364
|
+
const m = f.match(/^\d{4}-\d{2}-\d{2}-(\d{4})\.md(\.enc)?$/);
|
|
177
365
|
if (m) max = Math.max(max, parseInt(m[1], 10));
|
|
178
366
|
}
|
|
179
367
|
}
|
|
180
368
|
return String(max + 1).padStart(4, '0');
|
|
181
369
|
}
|
|
182
370
|
|
|
371
|
+
|
|
372
|
+
// ================= v0.7.0 加密(AES-256-GCM 信封加密 + PBKDF2 主密钥 + 恢复钥匙)=================
|
|
373
|
+
const ENC_MAGIC = 'YTMENC1';
|
|
374
|
+
const IDX_MAGIC = 'YTMIDX1';
|
|
375
|
+
const KEY_MAGIC = 'YTMKEY1';
|
|
376
|
+
const KEYS_DIR = 'keys';
|
|
377
|
+
const KEY_CACHE_DIR = 'cache';
|
|
378
|
+
const ENC_INDEX_FILE = 'index.enc';
|
|
379
|
+
const ENC_SUFFIX = '.enc';
|
|
380
|
+
const PBKDF2_ITER = 600000;
|
|
381
|
+
const GCM_NONCE = 12;
|
|
382
|
+
const GCM_TAG = 16;
|
|
383
|
+
const AES_ALGO = 'aes-256-gcm';
|
|
384
|
+
|
|
385
|
+
function encKeysDir(root) { return path.join(root, KEYS_DIR); }
|
|
386
|
+
function encSaltPath(root) { return path.join(encKeysDir(root), 'salt'); }
|
|
387
|
+
function encOwnerKeyPath(root, id) { return path.join(encKeysDir(root), id + '.key.enc'); }
|
|
388
|
+
function encOwnerRecPath(root, id) { return path.join(encKeysDir(root), id + '.key.recovery'); }
|
|
389
|
+
function encRecoveryEncPath(root) { return path.join(encKeysDir(root), 'recovery.key.enc'); }
|
|
390
|
+
function encCacheDir(root) { return path.join(encKeysDir(root), KEY_CACHE_DIR); }
|
|
391
|
+
function encCachePath(root, id) { return path.join(encCacheDir(root), id + '.key'); }
|
|
392
|
+
function isEncrypted(root) { return fs.existsSync(encKeysDir(root)); }
|
|
393
|
+
function isEncFile(fp) { return String(fp).slice(-ENC_SUFFIX.length) === ENC_SUFFIX; }
|
|
394
|
+
function ownerFromPrivatePath(root, fp) {
|
|
395
|
+
const rel = path.relative(root, fp).replace(/\\/g, '/');
|
|
396
|
+
const seg = rel.split('/');
|
|
397
|
+
if (seg[0] === PRIVATE_DIR && seg[1]) return seg[1];
|
|
398
|
+
return '';
|
|
399
|
+
}
|
|
400
|
+
function collectOwners(root) {
|
|
401
|
+
const pdir = path.join(root, PRIVATE_DIR);
|
|
402
|
+
if (!fs.existsSync(pdir)) return [];
|
|
403
|
+
return fs.readdirSync(pdir).filter(function (id) {
|
|
404
|
+
try { return fs.statSync(path.join(pdir, id)).isDirectory(); } catch (e) { return false; }
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
function ensureKeysDir(root) { fs.mkdirSync(encCacheDir(root), { recursive: true }); }
|
|
408
|
+
// owner 来源联合:private/<owner>/ 目录 + keys/*.key.enc(只授权未写记忆时 owner 只在 keys/)
|
|
409
|
+
function keyOwners(root) {
|
|
410
|
+
const set = new Set();
|
|
411
|
+
for (const o of collectOwners(root)) set.add(o);
|
|
412
|
+
const kd = encKeysDir(root);
|
|
413
|
+
if (fs.existsSync(kd)) {
|
|
414
|
+
for (const f of fs.readdirSync(kd)) {
|
|
415
|
+
if (f === 'recovery.key.enc') continue;
|
|
416
|
+
const m = /^(.+)\.key\.enc$/.exec(f);
|
|
417
|
+
if (m) set.add(m[1]);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return Array.from(set);
|
|
421
|
+
}
|
|
422
|
+
function aesEncryptBytes(key, buf, aad) {
|
|
423
|
+
const nonce = crypto.randomBytes(GCM_NONCE);
|
|
424
|
+
const c = crypto.createCipheriv(AES_ALGO, key, nonce);
|
|
425
|
+
if (aad) c.setAAD(aad);
|
|
426
|
+
const ct = Buffer.concat([c.update(buf), c.final()]);
|
|
427
|
+
return Buffer.concat([nonce, c.getAuthTag(), ct]);
|
|
428
|
+
}
|
|
429
|
+
function aesDecryptBytes(key, env, aad) {
|
|
430
|
+
if (!env || env.length < GCM_NONCE + GCM_TAG) throw new Error('密文数据损坏');
|
|
431
|
+
const nonce = env.slice(0, GCM_NONCE);
|
|
432
|
+
const tag = env.slice(GCM_NONCE, GCM_NONCE + GCM_TAG);
|
|
433
|
+
const ct = env.slice(GCM_NONCE + GCM_TAG);
|
|
434
|
+
const d = crypto.createDecipheriv(AES_ALGO, key, nonce);
|
|
435
|
+
if (aad) d.setAAD(aad);
|
|
436
|
+
d.setAuthTag(tag);
|
|
437
|
+
return Buffer.concat([d.update(ct), d.final()]);
|
|
438
|
+
}
|
|
439
|
+
function deriveUmk(password, salt) {
|
|
440
|
+
return crypto.pbkdf2Sync(Buffer.from(String(password), 'utf8'), salt, PBKDF2_ITER, 32, 'sha256');
|
|
441
|
+
}
|
|
442
|
+
function loadSalt(root) {
|
|
443
|
+
const p = encSaltPath(root);
|
|
444
|
+
if (!fs.existsSync(p)) return null;
|
|
445
|
+
return fs.readFileSync(p);
|
|
446
|
+
}
|
|
447
|
+
function initEncryptionCore(root, password, recoveryKeyIn) {
|
|
448
|
+
ensureKeysDir(root);
|
|
449
|
+
const salt = crypto.randomBytes(16);
|
|
450
|
+
fs.writeFileSync(encSaltPath(root), salt);
|
|
451
|
+
const umk = deriveUmk(password, salt);
|
|
452
|
+
const rk = recoveryKeyIn ? Buffer.from(String(recoveryKeyIn), 'base64') : crypto.randomBytes(32);
|
|
453
|
+
if (rk.length !== 32) throw new Error('恢复钥匙须为 32 字节(base64)');
|
|
454
|
+
fs.writeFileSync(encRecoveryEncPath(root), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(umk, rk, Buffer.from('recovery', 'utf8'))]));
|
|
455
|
+
return { umk: umk, rk: rk, salt: salt };
|
|
456
|
+
}
|
|
457
|
+
function unwrapRecoveryEnc(root, umk) {
|
|
458
|
+
const p = encRecoveryEncPath(root);
|
|
459
|
+
if (!fs.existsSync(p)) return null;
|
|
460
|
+
const buf = fs.readFileSync(p);
|
|
461
|
+
if (buf.slice(0, KEY_MAGIC.length).toString('utf8') !== KEY_MAGIC) throw new Error('恢复钥匙文件格式错误');
|
|
462
|
+
return aesDecryptBytes(umk, buf.slice(KEY_MAGIC.length), Buffer.from('recovery', 'utf8'));
|
|
463
|
+
}
|
|
464
|
+
function wrapOwnerKey(root, umk, rk, owner) {
|
|
465
|
+
const ownerKey = crypto.randomBytes(32);
|
|
466
|
+
const aad = Buffer.from('owner:' + owner, 'utf8');
|
|
467
|
+
fs.writeFileSync(encOwnerKeyPath(root, owner), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(umk, ownerKey, aad)]));
|
|
468
|
+
fs.writeFileSync(encOwnerRecPath(root, owner), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(rk, ownerKey, aad)]));
|
|
469
|
+
return ownerKey;
|
|
470
|
+
}
|
|
471
|
+
function unwrapOwnerKey(root, owner, umk) {
|
|
472
|
+
const p = encOwnerKeyPath(root, owner);
|
|
473
|
+
if (!fs.existsSync(p)) return null;
|
|
474
|
+
const buf = fs.readFileSync(p);
|
|
475
|
+
if (buf.slice(0, KEY_MAGIC.length).toString('utf8') !== KEY_MAGIC) throw new Error('密钥文件格式错误: ' + p);
|
|
476
|
+
return aesDecryptBytes(umk, buf.slice(KEY_MAGIC.length), Buffer.from('owner:' + owner, 'utf8'));
|
|
477
|
+
}
|
|
478
|
+
function unwrapOwnerKeyRecovery(root, owner, rk) {
|
|
479
|
+
const p = encOwnerRecPath(root, owner);
|
|
480
|
+
if (!fs.existsSync(p)) return null;
|
|
481
|
+
const buf = fs.readFileSync(p);
|
|
482
|
+
if (buf.slice(0, KEY_MAGIC.length).toString('utf8') !== KEY_MAGIC) throw new Error('恢复密钥文件格式错误: ' + p);
|
|
483
|
+
return aesDecryptBytes(rk, buf.slice(KEY_MAGIC.length), Buffer.from('owner:' + owner, 'utf8'));
|
|
484
|
+
}
|
|
485
|
+
function writeOwnerKeyCache(root, owner, ownerKey) {
|
|
486
|
+
ensureKeysDir(root);
|
|
487
|
+
const p = encCachePath(root, owner);
|
|
488
|
+
fs.writeFileSync(p, ownerKey);
|
|
489
|
+
try { fs.chmodSync(p, 0o600); } catch (e) {}
|
|
490
|
+
}
|
|
491
|
+
function loadOwnerKeyCache(root, owner) {
|
|
492
|
+
const p = encCachePath(root, owner);
|
|
493
|
+
if (!fs.existsSync(p)) return null;
|
|
494
|
+
return fs.readFileSync(p);
|
|
495
|
+
}
|
|
496
|
+
function revokeOwnerKeyCache(root, owner) {
|
|
497
|
+
const p = encCachePath(root, owner);
|
|
498
|
+
if (fs.existsSync(p)) { fs.unlinkSync(p); return true; }
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
function getOwnerKeyFor(root, owner) {
|
|
502
|
+
if (!owner) return null;
|
|
503
|
+
if (!isEncrypted(root)) return null;
|
|
504
|
+
return loadOwnerKeyCache(root, owner);
|
|
505
|
+
}
|
|
506
|
+
function encryptMemoryText(text, ownerKey) {
|
|
507
|
+
const fileKey = crypto.randomBytes(32);
|
|
508
|
+
const wk = aesEncryptBytes(ownerKey, fileKey, Buffer.from('filekey', 'utf8'));
|
|
509
|
+
const data = aesEncryptBytes(fileKey, Buffer.from(text, 'utf8'), Buffer.from('file', 'utf8'));
|
|
510
|
+
return Buffer.concat([Buffer.from(ENC_MAGIC, 'utf8'), wk, data]);
|
|
511
|
+
}
|
|
512
|
+
function decryptMemoryText(buf, ownerKey) {
|
|
513
|
+
if (buf.slice(0, ENC_MAGIC.length).toString('utf8') !== ENC_MAGIC) throw new Error('非密文文件(缺 ' + ENC_MAGIC + ' 头)');
|
|
514
|
+
const env = buf.slice(ENC_MAGIC.length);
|
|
515
|
+
const wkEnv = env.slice(0, GCM_NONCE + GCM_TAG + 32);
|
|
516
|
+
const fileKey = aesDecryptBytes(ownerKey, wkEnv, Buffer.from('filekey', 'utf8'));
|
|
517
|
+
const dataEnv = env.slice(GCM_NONCE + GCM_TAG + 32);
|
|
518
|
+
return aesDecryptBytes(fileKey, dataEnv, Buffer.from('file', 'utf8')).toString('utf8');
|
|
519
|
+
}
|
|
520
|
+
function readMemoryText(root, fp, owner) {
|
|
521
|
+
if (!isEncFile(fp)) return fs.readFileSync(fp, 'utf8');
|
|
522
|
+
const key = getOwnerKeyFor(root, owner);
|
|
523
|
+
if (!key) throw new Error('私密区已加密:当前无 ' + owner + ' 的授权密钥,请在用户平台授权(yotta-memory view → 授权本智能体)。');
|
|
524
|
+
return decryptMemoryText(fs.readFileSync(fp), key);
|
|
525
|
+
}
|
|
526
|
+
function writeMemoryText(root, fp, text, owner) {
|
|
527
|
+
if (!isEncFile(fp)) { fs.writeFileSync(fp, text, 'utf8'); return; }
|
|
528
|
+
const key = getOwnerKeyFor(root, owner);
|
|
529
|
+
if (!key) throw new Error('私密区已加密:当前无 ' + owner + ' 的授权密钥,请在用户平台授权(yotta-memory view → 授权本智能体)。');
|
|
530
|
+
fs.writeFileSync(fp, encryptMemoryText(text, key));
|
|
531
|
+
}
|
|
532
|
+
function ownerIndexPath(root, owner) { return path.join(root, PRIVATE_DIR, owner, ENC_INDEX_FILE); }
|
|
533
|
+
function loadOwnerIndex(root, owner, ownerKey) {
|
|
534
|
+
const p = ownerIndexPath(root, owner);
|
|
535
|
+
if (!fs.existsSync(p)) return [];
|
|
536
|
+
const buf = fs.readFileSync(p);
|
|
537
|
+
if (buf.slice(0, IDX_MAGIC.length).toString('utf8') !== IDX_MAGIC) return [];
|
|
538
|
+
const d = aesDecryptBytes(ownerKey, buf.slice(IDX_MAGIC.length), Buffer.from('index:' + owner, 'utf8'));
|
|
539
|
+
const j = JSON.parse(d.toString('utf8'));
|
|
540
|
+
return Array.isArray(j.entries) ? j.entries : [];
|
|
541
|
+
}
|
|
542
|
+
function saveOwnerIndex(root, owner, ownerKey, entries) {
|
|
543
|
+
const data = JSON.stringify({ version: 1, updated: today(), entries: entries }, null, 2);
|
|
544
|
+
const env = aesEncryptBytes(ownerKey, Buffer.from(data, 'utf8'), Buffer.from('index:' + owner, 'utf8'));
|
|
545
|
+
const dir = path.join(root, PRIVATE_DIR, owner);
|
|
546
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
547
|
+
fs.writeFileSync(path.join(dir, ENC_INDEX_FILE), Buffer.concat([Buffer.from(IDX_MAGIC, 'utf8'), env]));
|
|
548
|
+
}
|
|
549
|
+
function hasPlaintextPrivate(root) {
|
|
550
|
+
for (const owner of collectOwners(root)) {
|
|
551
|
+
for (const t of PRIVATE_LEAF) {
|
|
552
|
+
const d = path.join(root, PRIVATE_DIR, owner, t);
|
|
553
|
+
if (!fs.existsSync(d)) continue;
|
|
554
|
+
for (const f of fs.readdirSync(d)) if (f.endsWith('.md')) return true;
|
|
555
|
+
}
|
|
556
|
+
if (fs.existsSync(path.join(root, PRIVATE_DIR, owner, PROFILE_FILE))) return true;
|
|
557
|
+
}
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
function profilePathFor(root, owner) {
|
|
561
|
+
const base = path.join(root, PRIVATE_DIR, owner, PROFILE_FILE);
|
|
562
|
+
if (fs.existsSync(base)) return base;
|
|
563
|
+
return base + ENC_SUFFIX;
|
|
564
|
+
}
|
|
565
|
+
function readProfileText(root, owner) {
|
|
566
|
+
const p = profilePathFor(root, owner);
|
|
567
|
+
if (!fs.existsSync(p)) return null;
|
|
568
|
+
return readMemoryText(root, p, owner);
|
|
569
|
+
}
|
|
570
|
+
function writeProfileText(root, owner, text) {
|
|
571
|
+
const dir = path.join(root, PRIVATE_DIR, owner);
|
|
572
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
573
|
+
const encrypted = isEncrypted(root);
|
|
574
|
+
const p = encrypted ? path.join(dir, PROFILE_FILE + ENC_SUFFIX) : path.join(dir, PROFILE_FILE);
|
|
575
|
+
writeMemoryText(root, p, text, owner);
|
|
576
|
+
if (encrypted && fs.existsSync(path.join(dir, PROFILE_FILE))) {
|
|
577
|
+
try { fs.unlinkSync(path.join(dir, PROFILE_FILE)); } catch (e) {}
|
|
578
|
+
}
|
|
579
|
+
return p;
|
|
580
|
+
}
|
|
581
|
+
|
|
183
582
|
// ---- 索引(index.json)----
|
|
184
583
|
function indexPath(root) { return path.join(root, INDEX_FILE); }
|
|
584
|
+
const INDEX_VERSION = 3;
|
|
185
585
|
function loadIndex(root) {
|
|
186
586
|
const p = indexPath(root);
|
|
187
587
|
if (!fs.existsSync(p)) return null;
|
|
188
588
|
try {
|
|
189
589
|
const d = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
190
|
-
if (d && Array.isArray(d.entries))
|
|
590
|
+
if (d && Array.isArray(d.entries)) {
|
|
591
|
+
// v0.8.0 语义索引(字段加权 + 拼音 token)version=3;旧版索引返回 null 触发重建
|
|
592
|
+
if (d.version && d.version >= INDEX_VERSION) return d.entries;
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
191
595
|
} catch (e) { /* ignore */ }
|
|
192
596
|
return null;
|
|
193
597
|
}
|
|
194
598
|
function getIndex(root) { return loadIndex(root) || []; }
|
|
195
599
|
function saveIndex(root, entries) {
|
|
196
600
|
const clean = entries.map(function (e) { const c = Object.assign({}, e); delete c.meta; return c; });
|
|
197
|
-
fs.writeFileSync(indexPath(root), JSON.stringify({ version:
|
|
601
|
+
fs.writeFileSync(indexPath(root), JSON.stringify({ version: INDEX_VERSION, updated: today(), entries: clean }, null, 2), 'utf8');
|
|
198
602
|
}
|
|
199
603
|
function collectEntryFiles(root) {
|
|
200
604
|
const dirs = [];
|
|
201
605
|
const facts = path.join(root, PUBLIC_DIR);
|
|
202
|
-
if (fs.existsSync(facts)) dirs.push(
|
|
606
|
+
if (fs.existsSync(facts)) dirs.push(facts);
|
|
203
607
|
const pdir = path.join(root, PRIVATE_DIR);
|
|
204
608
|
if (fs.existsSync(pdir)) {
|
|
205
609
|
for (const id of fs.readdirSync(pdir)) {
|
|
@@ -214,6 +618,7 @@ function collectEntryFiles(root) {
|
|
|
214
618
|
const out = [];
|
|
215
619
|
for (const dir of dirs) {
|
|
216
620
|
for (const f of fs.readdirSync(dir)) {
|
|
621
|
+
if (!/\.md(\.enc)?$/.test(f)) continue;
|
|
217
622
|
const fp = path.join(dir, f);
|
|
218
623
|
if (fs.statSync(fp).isFile()) out.push(fp);
|
|
219
624
|
}
|
|
@@ -242,37 +647,113 @@ function migrateLayout(root) {
|
|
|
242
647
|
}
|
|
243
648
|
function buildIndex(root) {
|
|
244
649
|
migrateLayout(root);
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
650
|
+
const publicEntries = [];
|
|
651
|
+
const privateByOwner = {};
|
|
652
|
+
for (const fp of collectEntryFiles(root)) {
|
|
653
|
+
const rel = path.relative(root, fp).replace(/\\/g, '/');
|
|
654
|
+
if (rel.indexOf(PRIVATE_DIR + '/') === 0) {
|
|
655
|
+
const owner = rel.split('/')[1] || '';
|
|
656
|
+
if (!getOwnerKeyFor(root, owner)) continue;
|
|
657
|
+
let e;
|
|
658
|
+
try { e = readEntry(fp, root); } catch (err) { continue; }
|
|
659
|
+
(privateByOwner[owner] = privateByOwner[owner] || []).push(e);
|
|
660
|
+
} else {
|
|
661
|
+
try { publicEntries.push(readEntry(fp, root)); } catch (err) { /* ignore */ }
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
saveIndex(root, publicEntries);
|
|
665
|
+
for (const owner of Object.keys(privateByOwner)) {
|
|
666
|
+
const key = getOwnerKeyFor(root, owner);
|
|
667
|
+
if (key) saveOwnerIndex(root, owner, key, privateByOwner[owner]);
|
|
668
|
+
}
|
|
669
|
+
const all = publicEntries.slice();
|
|
670
|
+
for (const owner of Object.keys(privateByOwner)) all.push.apply(all, privateByOwner[owner]);
|
|
671
|
+
return all;
|
|
249
672
|
}
|
|
250
673
|
function ensureIndex(root) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
674
|
+
if (!isEncrypted(root)) {
|
|
675
|
+
const idx = loadIndex(root);
|
|
676
|
+
return idx || buildIndex(root);
|
|
677
|
+
}
|
|
678
|
+
const all = [];
|
|
679
|
+
const pub = loadIndex(root);
|
|
680
|
+
if (pub) all.push.apply(all, pub);
|
|
681
|
+
for (const owner of collectOwners(root)) {
|
|
682
|
+
const key = getOwnerKeyFor(root, owner);
|
|
683
|
+
if (!key) continue;
|
|
684
|
+
let idx = [];
|
|
685
|
+
try { idx = loadOwnerIndex(root, owner, key); } catch (err) { idx = []; }
|
|
686
|
+
all.push.apply(all, idx);
|
|
687
|
+
}
|
|
688
|
+
return all;
|
|
254
689
|
}
|
|
255
690
|
function upsertIndexEntry(root, entry) {
|
|
691
|
+
if (isEncrypted(root) && entry.scope !== 'public' && String(entry.file).indexOf(PUBLIC_DIR + '/') !== 0) {
|
|
692
|
+
const owner = entry.owner || String(entry.file).split('/')[1] || '';
|
|
693
|
+
const key = getOwnerKeyFor(root, owner);
|
|
694
|
+
if (!key) return;
|
|
695
|
+
const idx = loadOwnerIndex(root, owner, key);
|
|
696
|
+
const i = idx.findIndex(function (e) { return e.file === entry.file; });
|
|
697
|
+
if (i >= 0) idx[i] = entry; else idx.push(entry);
|
|
698
|
+
saveOwnerIndex(root, owner, key, idx);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
256
701
|
const idx = getIndex(root);
|
|
257
702
|
const i = idx.findIndex(function (e) { return e.file === entry.file; });
|
|
258
703
|
if (i >= 0) idx[i] = entry; else idx.push(entry);
|
|
259
704
|
saveIndex(root, idx);
|
|
260
705
|
}
|
|
261
706
|
function removeIndexEntry(root, file) {
|
|
707
|
+
if (isEncrypted(root) && String(file).indexOf(PRIVATE_DIR + '/') === 0) {
|
|
708
|
+
const owner = String(file).split('/')[1] || '';
|
|
709
|
+
const key = getOwnerKeyFor(root, owner);
|
|
710
|
+
if (!key) return;
|
|
711
|
+
const idx = loadOwnerIndex(root, owner, key);
|
|
712
|
+
const next = idx.filter(function (e) { return e.file !== file; });
|
|
713
|
+
if (next.length !== idx.length) saveOwnerIndex(root, owner, key, next);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
262
716
|
const idx = getIndex(root);
|
|
263
717
|
const i = idx.findIndex(function (e) { return e.file === file; });
|
|
264
718
|
if (i >= 0) { idx.splice(i, 1); saveIndex(root, idx); }
|
|
265
719
|
}
|
|
266
720
|
function removeIndexEntries(root, files) {
|
|
267
721
|
const set = new Set(files);
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
|
|
722
|
+
const byOwner = {};
|
|
723
|
+
const pub = [];
|
|
724
|
+
for (const f of files) {
|
|
725
|
+
if (String(f).indexOf(PRIVATE_DIR + '/') === 0) {
|
|
726
|
+
const o = String(f).split('/')[1] || '';
|
|
727
|
+
(byOwner[o] = byOwner[o] || []).push(f);
|
|
728
|
+
} else pub.push(f);
|
|
729
|
+
}
|
|
730
|
+
if (pub.length) {
|
|
731
|
+
const idx = getIndex(root);
|
|
732
|
+
const next = idx.filter(function (e) { return !set.has(e.file); });
|
|
733
|
+
if (next.length !== idx.length) saveIndex(root, next);
|
|
734
|
+
}
|
|
735
|
+
for (const o of Object.keys(byOwner)) {
|
|
736
|
+
const key = getOwnerKeyFor(root, o);
|
|
737
|
+
if (!key) continue;
|
|
738
|
+
const idx = loadOwnerIndex(root, o, key);
|
|
739
|
+
const oset = new Set(byOwner[o]);
|
|
740
|
+
const next = idx.filter(function (e) { return !oset.has(e.file); });
|
|
741
|
+
if (next.length !== idx.length) saveOwnerIndex(root, o, key, next);
|
|
742
|
+
}
|
|
271
743
|
}
|
|
272
744
|
|
|
273
745
|
// ---- 读取 / 写入记忆文件 ----
|
|
274
746
|
function readEntry(fp, root) {
|
|
275
|
-
const
|
|
747
|
+
const owner = ownerFromPrivatePath(root, fp);
|
|
748
|
+
let text;
|
|
749
|
+
if (isEncFile(fp)) {
|
|
750
|
+
const key = getOwnerKeyFor(root, owner);
|
|
751
|
+
if (!key) throw new Error('私密区已加密:当前无 ' + owner + ' 的授权密钥(yotta-memory view → 授权本智能体)。');
|
|
752
|
+
text = decryptMemoryText(fs.readFileSync(fp), key);
|
|
753
|
+
} else {
|
|
754
|
+
text = fs.readFileSync(fp, 'utf8');
|
|
755
|
+
}
|
|
756
|
+
const parsed = parseFrontmatter(text);
|
|
276
757
|
const meta = parsed.meta;
|
|
277
758
|
const type = (meta.type || 'FACT').toUpperCase();
|
|
278
759
|
const subject = meta.subject || '';
|
|
@@ -294,27 +775,32 @@ function readEntry(fp, root) {
|
|
|
294
775
|
immutable: meta.immutable === 'true',
|
|
295
776
|
source: meta.source || '',
|
|
296
777
|
weight: (parseFloat(meta.weight) > 0 ? parseFloat(meta.weight) : 1.0),
|
|
778
|
+
feedback_net: feedbackNetOf(meta),
|
|
297
779
|
tokens: buildTokens(subject, statement, tags),
|
|
298
780
|
meta: meta,
|
|
299
781
|
};
|
|
300
782
|
}
|
|
301
|
-
function rewriteFrontmatter(fp, patch) {
|
|
302
|
-
|
|
303
|
-
|
|
783
|
+
function rewriteFrontmatter(fp, patch, root, owner) {
|
|
784
|
+
let text;
|
|
785
|
+
try { text = readMemoryText(root || memoryRootForFile(fp), fp, owner || ownerFromPrivatePath(root || memoryRootForFile(fp), fp)); }
|
|
786
|
+
catch (e) { throw e; }
|
|
787
|
+
const parsed = parseFrontmatter(text);
|
|
304
788
|
const meta = Object.assign({}, parsed.meta);
|
|
305
789
|
for (const k of Object.keys(patch)) meta[k] = patch[k];
|
|
306
|
-
|
|
790
|
+
writeMemoryText(root || memoryRootForFile(fp), fp, frontmatterToText(meta, parsed.body), owner || ownerFromPrivatePath(root || memoryRootForFile(fp), fp));
|
|
307
791
|
}
|
|
308
792
|
function bumpReadMeta(root, relFiles) {
|
|
309
793
|
const now = today();
|
|
310
794
|
for (const rel of relFiles) {
|
|
311
795
|
const fp = path.join(root, rel);
|
|
312
796
|
if (!fs.existsSync(fp)) continue;
|
|
313
|
-
const
|
|
797
|
+
const owner = ownerFromPrivatePath(root, fp);
|
|
798
|
+
let parsed;
|
|
799
|
+
try { parsed = parseFrontmatter(readMemoryText(root, fp, owner)); } catch (e) { continue; }
|
|
314
800
|
const meta = parsed.meta;
|
|
315
801
|
if (meta.immutable === 'true') continue;
|
|
316
802
|
const acc = (parseInt(meta.access_count || '0', 10) || 0) + 1;
|
|
317
|
-
rewriteFrontmatter(fp, { access_count: acc, last_accessed: now });
|
|
803
|
+
rewriteFrontmatter(fp, { access_count: acc, last_accessed: now }, root, owner);
|
|
318
804
|
}
|
|
319
805
|
}
|
|
320
806
|
function touchIndex(root, relFiles) {
|
|
@@ -342,7 +828,7 @@ function vitality(meta) {
|
|
|
342
828
|
if (last) { const d = daysBetween(last, today()); recency = Math.max(0, 1 - Math.floor(d / 30) * 0.2); }
|
|
343
829
|
return 0.4 * conf + 0.3 * accScore + 0.3 * recency;
|
|
344
830
|
}
|
|
345
|
-
//
|
|
831
|
+
// importance 融合:confidence × (0.5 + recency) + updated 加分 + weight 乘子 + immutable 加分
|
|
346
832
|
function importanceScore(meta) {
|
|
347
833
|
const conf = parseFloat(meta.confidence || 1.0);
|
|
348
834
|
let ageDays = 999;
|
|
@@ -404,9 +890,20 @@ function verifyWrittenReadable(root, rel, subj, agent) {
|
|
|
404
890
|
|
|
405
891
|
// ---- 命令 core(CLI 与 MCP 共用;返回 { error, exitCode, text },不 process.exit)----
|
|
406
892
|
function initCore(opts) {
|
|
893
|
+
opts = opts || {};
|
|
407
894
|
const root = opts.dir ? path.resolve(String(opts.dir)) : (opts.project ? projectRoot() : userRoot());
|
|
895
|
+
const fresh = !fs.existsSync(path.join(root, PUBLIC_DIR)) && !fs.existsSync(path.join(root, PRIVATE_DIR)) && !fs.existsSync(path.join(root, INDEX_FILE));
|
|
408
896
|
ensureInit(root);
|
|
409
|
-
|
|
897
|
+
const wantEncrypt = opts.encrypt || (!opts.noEncrypt && fresh);
|
|
898
|
+
if (wantEncrypt) {
|
|
899
|
+
if (isEncrypted(root)) return { error: false, text: '已初始化记忆库(已启用加密): ' + root };
|
|
900
|
+
if (hasPlaintextPrivate(root)) return { error: true, text: '检测到明文私密区,请先 yotta-memory migrate 迁移到密文;新建空库可直接 init --encrypt。' };
|
|
901
|
+
const password = String(opts.password || process.env.YOTTA_MEMORY_PASS || '');
|
|
902
|
+
if (!password) return { error: true, text: '启用加密需要主口令:交互输入,或 YOTTA_MEMORY_PASS 环境变量(自动化)。' };
|
|
903
|
+
const r = initEncryptionCore(root, password, null);
|
|
904
|
+
return { error: false, text: '已初始化加密记忆库: ' + root + '\n[恢复钥匙](务必离线保存,仅此一次,泄露=可解全部私密): ' + r.rk.toString('base64') };
|
|
905
|
+
}
|
|
906
|
+
return { error: false, text: '已初始化记忆库: ' + root + (isEncrypted(root) ? '(已启用加密)' : '(明文;建议 init --encrypt 或 migrate 启用加密)') };
|
|
410
907
|
}
|
|
411
908
|
function rememberCore(type, subject, statement, opts) {
|
|
412
909
|
opts = opts || {};
|
|
@@ -427,13 +924,18 @@ function rememberCore(type, subject, statement, opts) {
|
|
|
427
924
|
if (scope === 'private' && owner && selfAgent && owner !== selfAgent && !opts.unsafe) {
|
|
428
925
|
return { error: true, text: '拒绝: 当前声明身份 ' + selfAgent + ' 不能写入其它智能体 ' + owner + ' 的私密区。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。' };
|
|
429
926
|
}
|
|
927
|
+
const encrypted = isEncrypted(root);
|
|
928
|
+
if (scope === 'private' && encrypted && !getOwnerKeyFor(root, owner)) {
|
|
929
|
+
return { error: true, text: '私密区已加密:当前无 ' + owner + ' 的授权密钥,请在用户平台授权(yotta-memory view → 授权本智能体)后再写私密记忆。公共 FACT 不受影响。' };
|
|
930
|
+
}
|
|
430
931
|
const dir = path.join(root, typeSubdir(t, owner));
|
|
431
932
|
fs.mkdirSync(dir, { recursive: true });
|
|
432
933
|
if (fs.existsSync(dir)) {
|
|
433
934
|
for (const f of fs.readdirSync(dir)) {
|
|
434
935
|
const fp = path.join(dir, f);
|
|
435
936
|
if (!fs.statSync(fp).isFile()) continue;
|
|
436
|
-
|
|
937
|
+
let parsed;
|
|
938
|
+
try { parsed = parseFrontmatter(readMemoryText(root, fp, owner)); } catch (err) { continue; }
|
|
437
939
|
const meta = parsed.meta;
|
|
438
940
|
if ((meta.type || '').toUpperCase() === t && meta.subject === subj && meta.statement === stmt) {
|
|
439
941
|
const patch = { updated: today() };
|
|
@@ -441,7 +943,7 @@ function rememberCore(type, subject, statement, opts) {
|
|
|
441
943
|
if (scope && !meta.scope) patch.scope = scope;
|
|
442
944
|
if (opts.source && !meta.source) patch.source = opts.source;
|
|
443
945
|
const w = parseFloat(opts.weight); if (w > 0) patch.weight = Math.max(parseFloat(meta.weight || '1.0') || 1.0, w);
|
|
444
|
-
rewriteFrontmatter(fp, patch);
|
|
946
|
+
rewriteFrontmatter(fp, patch, root, owner);
|
|
445
947
|
upsertIndexEntry(root, readEntry(fp, root));
|
|
446
948
|
let text = '已更新: ' + fp;
|
|
447
949
|
if (opts.hint !== false && t === 'FACT') {
|
|
@@ -458,7 +960,8 @@ function rememberCore(type, subject, statement, opts) {
|
|
|
458
960
|
}
|
|
459
961
|
}
|
|
460
962
|
const seq = nextSeq(dir);
|
|
461
|
-
const
|
|
963
|
+
const suffix = (encrypted && scope === 'private') ? ENC_SUFFIX : '';
|
|
964
|
+
const file = path.join(dir, today() + '-' + seq + '.md' + suffix);
|
|
462
965
|
const rec = {
|
|
463
966
|
type: t, subject: subj, statement: stmt,
|
|
464
967
|
confidence: 1.0, created: today(), updated: today(),
|
|
@@ -468,7 +971,7 @@ function rememberCore(type, subject, statement, opts) {
|
|
|
468
971
|
weight: (parseFloat(opts.weight) > 0 ? parseFloat(opts.weight) : 1.0),
|
|
469
972
|
access_count: 0, last_accessed: '',
|
|
470
973
|
};
|
|
471
|
-
|
|
974
|
+
writeMemoryText(root, file, frontmatterToText(rec, stmt), owner);
|
|
472
975
|
upsertIndexEntry(root, readEntry(file, root));
|
|
473
976
|
let text = '已记录: ' + file;
|
|
474
977
|
if (opts.hint !== false && t === 'FACT') {
|
|
@@ -483,6 +986,7 @@ function rememberCore(type, subject, statement, opts) {
|
|
|
483
986
|
return { error: false, text: text };
|
|
484
987
|
}
|
|
485
988
|
function recallCore(query, opts) {
|
|
989
|
+
opts = opts || {};
|
|
486
990
|
const roots = memoryRoots();
|
|
487
991
|
if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
|
|
488
992
|
const limit = opts.limit || 50;
|
|
@@ -494,6 +998,8 @@ function recallCore(query, opts) {
|
|
|
494
998
|
const allSafe = !!opts.unsafe;
|
|
495
999
|
const impersonation = !!(opts.agent && selfAgent && opts.agent !== selfAgent);
|
|
496
1000
|
const explicitCross = !!opts.all || impersonation || (!!ownerFilter && ownerFilter.toLowerCase() !== 'user' && ownerFilter !== agent);
|
|
1001
|
+
const wantExplain = !!opts.explain;
|
|
1002
|
+
const useSemantic = opts.semantic !== false;
|
|
497
1003
|
const hits = [];
|
|
498
1004
|
let deniedCount = 0;
|
|
499
1005
|
for (const root of roots) {
|
|
@@ -503,27 +1009,60 @@ function recallCore(query, opts) {
|
|
|
503
1009
|
const r = classifyRead(e, agent, ownerFilter, allSafe, selfAgent);
|
|
504
1010
|
if (r === 'denied') { deniedCount++; continue; }
|
|
505
1011
|
let score = 0;
|
|
1012
|
+
let detail = null;
|
|
506
1013
|
if (q) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
1014
|
+
if (useSemantic) {
|
|
1015
|
+
const m = semanticMatch(e, q, wantExplain);
|
|
1016
|
+
score = m.score;
|
|
1017
|
+
if (wantExplain && m.detail && m.detail.length) detail = m.detail;
|
|
1018
|
+
} else {
|
|
1019
|
+
const qtoks = tokenize(q);
|
|
1020
|
+
for (const tt of qtoks) { if (e.tokens && e.tokens[tt]) score += e.tokens[tt]; }
|
|
1021
|
+
if (score === 0) {
|
|
1022
|
+
const hay = ((e.subject || '') + ' ' + (e.statement || '') + ' ' + e.tags.join(' ')).toLowerCase();
|
|
1023
|
+
if (hay.indexOf(q) !== -1) score = 1;
|
|
1024
|
+
}
|
|
512
1025
|
}
|
|
513
1026
|
if (score === 0) continue;
|
|
514
1027
|
} else {
|
|
515
1028
|
score = 1;
|
|
516
1029
|
}
|
|
517
|
-
hits.push({ entry: e, score: score, root: root });
|
|
1030
|
+
hits.push({ entry: e, score: score, root: root, detail: detail });
|
|
518
1031
|
}
|
|
519
1032
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1033
|
+
// v0.8.0 融合排序:0.65 × 语义分归一 + 0.35 × 效用分归一
|
|
1034
|
+
if (hits.length > 1) {
|
|
1035
|
+
let minSem = Infinity, maxSem = -Infinity, minUtil = Infinity, maxUtil = -Infinity;
|
|
1036
|
+
for (const h of hits) {
|
|
1037
|
+
if (h.score < minSem) minSem = h.score;
|
|
1038
|
+
if (h.score > maxSem) maxSem = h.score;
|
|
1039
|
+
const u = utilityScore(h.entry);
|
|
1040
|
+
if (u < minUtil) minUtil = u;
|
|
1041
|
+
if (u > maxUtil) maxUtil = u;
|
|
1042
|
+
}
|
|
1043
|
+
const spanSem = maxSem - minSem;
|
|
1044
|
+
const spanUtil = maxUtil - minUtil;
|
|
1045
|
+
for (const h of hits) {
|
|
1046
|
+
h.semNorm = spanSem > 0 ? (h.score - minSem) / spanSem : 1;
|
|
1047
|
+
h.utilNorm = spanUtil > 0 ? (utilityScore(h.entry) - minUtil) / spanUtil : 1;
|
|
1048
|
+
h.finalScore = 0.65 * h.semNorm + 0.35 * h.utilNorm;
|
|
1049
|
+
}
|
|
1050
|
+
hits.sort(function (a, b) {
|
|
1051
|
+
if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
|
|
1052
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
1053
|
+
const pa = a.root === projectRoot() ? 0 : 1;
|
|
1054
|
+
const pb = b.root === projectRoot() ? 0 : 1;
|
|
1055
|
+
if (pa !== pb) return pa - pb;
|
|
1056
|
+
return String(b.entry.created).localeCompare(String(a.entry.created));
|
|
1057
|
+
});
|
|
1058
|
+
} else {
|
|
1059
|
+
hits.sort(function (a, b) {
|
|
1060
|
+
const pa = a.root === projectRoot() ? 0 : 1;
|
|
1061
|
+
const pb = b.root === projectRoot() ? 0 : 1;
|
|
1062
|
+
if (pa !== pb) return pa - pb;
|
|
1063
|
+
return String(b.entry.created).localeCompare(String(a.entry.created));
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
527
1066
|
const shown = hits.slice(0, limit);
|
|
528
1067
|
if (!shown.length) {
|
|
529
1068
|
if (deniedCount > 0 && explicitCross) {
|
|
@@ -542,6 +1081,11 @@ function recallCore(query, opts) {
|
|
|
542
1081
|
for (const h of shown) {
|
|
543
1082
|
lines.push('[' + h.entry.type + '] ' + h.entry.subject + ': ' + h.entry.statement);
|
|
544
1083
|
lines.push(' ' + path.join(h.root, h.entry.file));
|
|
1084
|
+
if (wantExplain) {
|
|
1085
|
+
if (h.detail && h.detail.length) lines.push(' 命中: ' + h.detail.join('、'));
|
|
1086
|
+
const ub = utilityBreakdown(h.entry);
|
|
1087
|
+
lines.push(' 效用分: ' + round3(ub.total) + '(conf ' + ub.confidence + ' + 使用 ' + ub.usage + ' + 时效 ' + ub.recency + ' + 类型 ' + ub.type + ' + 结构 ' + ub.structure + ')×weight ' + ub.weight);
|
|
1088
|
+
}
|
|
545
1089
|
}
|
|
546
1090
|
if (deniedCount > 0 && explicitCross) {
|
|
547
1091
|
lines.push('\n[警告] 本次检索共拒绝 ' + deniedCount + ' 条越界访问(其它智能体私密记忆,未授权不展示)。如需读取请加 --unsafe 或 --owner user。');
|
|
@@ -585,21 +1129,26 @@ function forgetCore(fileRef, opts) {
|
|
|
585
1129
|
return { error: false, text: '已删除: ' + target };
|
|
586
1130
|
}
|
|
587
1131
|
function archiveCore(opts) {
|
|
588
|
-
|
|
589
|
-
const
|
|
1132
|
+
opts = opts || {};
|
|
1133
|
+
const cfg = loadConfig();
|
|
1134
|
+
const days = opts.days || parseInt(cfg.maintain_archived_age || '180', 10) || 180;
|
|
1135
|
+
const threshold = (opts.threshold !== undefined && opts.threshold !== null) ? opts.threshold : (parseFloat(cfg.maintain_archived_utility || 0.35));
|
|
590
1136
|
const root = userRoot();
|
|
591
1137
|
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
|
|
592
1138
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
593
1139
|
let moved = 0;
|
|
594
1140
|
const movedFiles = [];
|
|
595
1141
|
for (const fp of collectEntryFiles(root)) {
|
|
596
|
-
const
|
|
1142
|
+
const owner = ownerFromPrivatePath(root, fp);
|
|
1143
|
+
let meta;
|
|
1144
|
+
try { meta = parseFrontmatter(readMemoryText(root, fp, owner)).meta; } catch (e) { continue; }
|
|
597
1145
|
if (meta.immutable === 'true') continue;
|
|
598
1146
|
if (!meta.created) continue;
|
|
599
1147
|
const createdTs = new Date(meta.created).getTime();
|
|
600
1148
|
if (isNaN(createdTs)) continue;
|
|
601
1149
|
const t = (meta.type || 'FACT').toUpperCase();
|
|
602
|
-
|
|
1150
|
+
// v0.8.0 统一效用分(盖棺分)替代 vitality
|
|
1151
|
+
if (utilityScore(meta) < threshold && createdTs < cutoff) {
|
|
603
1152
|
const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[t]);
|
|
604
1153
|
fs.mkdirSync(destDir, { recursive: true });
|
|
605
1154
|
fs.renameSync(fp, path.join(destDir, path.basename(fp)));
|
|
@@ -612,10 +1161,771 @@ function archiveCore(opts) {
|
|
|
612
1161
|
return { error: false, text: '已归档 ' + moved + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR) };
|
|
613
1162
|
}
|
|
614
1163
|
|
|
1164
|
+
// ---- v0.8.0 自我学习/自我进化/自我提升:feedback / explain / maintain / distill ----
|
|
1165
|
+
function auditPath(root, kind) {
|
|
1166
|
+
return path.join(root, ARCHIVE_DIR, kind + '-' + today() + '.jsonl');
|
|
1167
|
+
}
|
|
1168
|
+
function appendAudit(root, kind, rec) {
|
|
1169
|
+
try {
|
|
1170
|
+
fs.mkdirSync(path.join(root, ARCHIVE_DIR), { recursive: true });
|
|
1171
|
+
fs.appendFileSync(auditPath(root, kind), JSON.stringify(rec) + '\n', 'utf8');
|
|
1172
|
+
} catch (e) { /* 审计失败不阻断主流程 */ }
|
|
1173
|
+
}
|
|
1174
|
+
function resolveMemoryTarget(ref) {
|
|
1175
|
+
const roots = memoryRoots();
|
|
1176
|
+
const targetRef = String(ref || '').replace(/\\/g, '/');
|
|
1177
|
+
for (const root of roots) {
|
|
1178
|
+
const found = resolveMemoryFile(root, targetRef);
|
|
1179
|
+
if (found) return { fp: found.fp, root: root, rel: found.rel };
|
|
1180
|
+
}
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
function checkOwnerWritable(targetRoot, targetRel, selfAgent, unsafe) {
|
|
1184
|
+
const seg = String(targetRel).split('/');
|
|
1185
|
+
if (seg[0] === PRIVATE_DIR) {
|
|
1186
|
+
const owner = seg[1] || '';
|
|
1187
|
+
if (!unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
|
|
1188
|
+
return '拒绝: 不能操作其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。';
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
// 显式使用反馈:useful/useless → weight/confidence/feedback_net 演化(自我学习闭环)
|
|
1194
|
+
function feedbackCore(ref, opts) {
|
|
1195
|
+
opts = opts || {};
|
|
1196
|
+
const selfAgent = opts.selfAgent || currentAgent();
|
|
1197
|
+
const target = resolveMemoryTarget(ref);
|
|
1198
|
+
if (!target) return { error: true, text: '未找到记忆文件: ' + ref };
|
|
1199
|
+
const deny = checkOwnerWritable(target.root, target.rel, selfAgent, opts.unsafe);
|
|
1200
|
+
if (deny) return { error: true, text: deny };
|
|
1201
|
+
const owner = ownerFromPrivatePath(target.root, target.fp);
|
|
1202
|
+
let meta;
|
|
1203
|
+
try { meta = parseFrontmatter(readMemoryText(target.root, target.fp, owner)).meta; } catch (e) { return { error: true, text: '读取失败: ' + e.message }; }
|
|
1204
|
+
if (opts.undo) {
|
|
1205
|
+
return feedbackUndoCore(target, owner, meta, selfAgent, opts);
|
|
1206
|
+
}
|
|
1207
|
+
const useful = !!opts.useful;
|
|
1208
|
+
const useless = !!opts.useless;
|
|
1209
|
+
if (!useful && !useless) return { error: true, text: '请指定 --useful(有用)或 --useless(没用)。' };
|
|
1210
|
+
const cfg = loadConfig();
|
|
1211
|
+
const stepW = parseFloat(cfg.feedback_weight_step || 0.2);
|
|
1212
|
+
const stepC = parseFloat(cfg.feedback_confidence_step || 0.05);
|
|
1213
|
+
const w = parseFloat(meta.weight || '1.0'); const weight = (w > 0 ? w : 1.0);
|
|
1214
|
+
const conf = parseFloat(meta.confidence || '1.0'); const confidence = (conf > 0 ? conf : 1.0);
|
|
1215
|
+
const fb = feedbackNetOf(meta);
|
|
1216
|
+
let nw = weight, nc = confidence, nfb = fb;
|
|
1217
|
+
if (useful) { nw = Math.min(weight * (1 + stepW), 3.0); nc = Math.min(confidence + stepC, 1.0); nfb = fb + 1; }
|
|
1218
|
+
if (useless) { nw = Math.max(weight * (1 - stepW), 0.2); nc = Math.max(confidence - stepC, 0.3); nfb = fb - 1; }
|
|
1219
|
+
const before = { weight: round3(weight), confidence: round3(confidence), feedback_net: fb };
|
|
1220
|
+
const after = { weight: round3(nw), confidence: round3(nc), feedback_net: nfb };
|
|
1221
|
+
const patch = { weight: after.weight, confidence: after.confidence, feedback_net: after.feedback_net, updated: today() };
|
|
1222
|
+
rewriteFrontmatter(target.fp, patch, target.root, owner);
|
|
1223
|
+
upsertIndexEntry(target.root, readEntry(target.fp, target.root));
|
|
1224
|
+
appendAudit(target.root, 'feedback', { ts: new Date().toISOString(), file: target.rel, action: useful ? 'useful' : 'useless', before: before, after: after, reason: opts.reason || '' });
|
|
1225
|
+
const lines = [(useful ? '已反馈 [有用]' : '已反馈 [没用]') + ': ' + target.rel];
|
|
1226
|
+
lines.push(' weight ' + before.weight + ' -> ' + after.weight + ' / confidence ' + before.confidence + ' -> ' + after.confidence + ' / feedback_net ' + before.feedback_net + ' -> ' + after.feedback_net);
|
|
1227
|
+
if (opts.reason) lines.push(' 原因: ' + opts.reason);
|
|
1228
|
+
lines.push(' 提示: yotta-memory explain ' + target.rel + ' 查看效用分项;feedback --undo 回滚最近一次');
|
|
1229
|
+
return { error: false, text: lines.join('\n') };
|
|
1230
|
+
}
|
|
1231
|
+
function feedbackUndoCore(target, owner, meta, selfAgent, opts) {
|
|
1232
|
+
const cfg = loadConfig();
|
|
1233
|
+
const lines = [];
|
|
1234
|
+
let restored = false;
|
|
1235
|
+
try {
|
|
1236
|
+
const log = auditPath(target.root, 'feedback');
|
|
1237
|
+
if (!fs.existsSync(log)) return { error: true, text: '无可回滚的反馈记录(' + log + ' 不存在)。' };
|
|
1238
|
+
const recs = fs.readFileSync(log, 'utf8').split('\n').filter(Boolean).map(function (l) { try { return JSON.parse(l); } catch (e) { return null; } }).filter(Boolean);
|
|
1239
|
+
const mine = recs.filter(function (r) { return r.file === target.rel && (r.action === 'useful' || r.action === 'useless'); });
|
|
1240
|
+
if (!mine.length) return { error: true, text: '该文件无反馈记录可回滚。' };
|
|
1241
|
+
const last = mine[mine.length - 1];
|
|
1242
|
+
const patch = { weight: last.before.weight, confidence: last.before.confidence, feedback_net: last.before.feedback_net, updated: today() };
|
|
1243
|
+
rewriteFrontmatter(target.fp, patch, target.root, owner);
|
|
1244
|
+
upsertIndexEntry(target.root, readEntry(target.fp, target.root));
|
|
1245
|
+
appendAudit(target.root, 'feedback', { ts: new Date().toISOString(), file: target.rel, action: 'undo', before: last.after, after: last.before, reason: opts.reason || '' });
|
|
1246
|
+
lines.push('已回滚最近一次反馈: ' + target.rel);
|
|
1247
|
+
lines.push(' ' + last.action + ' -> 恢复 weight ' + last.after.weight + ' -> ' + last.before.weight + ' / feedback_net ' + last.after.feedback_net + ' -> ' + last.before.feedback_net);
|
|
1248
|
+
restored = true;
|
|
1249
|
+
} catch (e) { return { error: true, text: '回滚失败: ' + e.message }; }
|
|
1250
|
+
return { error: false, text: lines.join('\n') };
|
|
1251
|
+
}
|
|
1252
|
+
// 解释单条记忆效用分项(为什么靠前/归档/遗忘)
|
|
1253
|
+
function explainCore(ref, opts) {
|
|
1254
|
+
opts = opts || {};
|
|
1255
|
+
const selfAgent = opts.selfAgent || currentAgent();
|
|
1256
|
+
const target = resolveMemoryTarget(ref);
|
|
1257
|
+
if (!target) return { error: true, text: '未找到记忆文件: ' + ref };
|
|
1258
|
+
const owner = ownerFromPrivatePath(target.root, target.fp);
|
|
1259
|
+
let entry;
|
|
1260
|
+
try { entry = readEntry(target.fp, target.root); } catch (e) { return { error: true, text: '读取失败: ' + e.message }; }
|
|
1261
|
+
const deny = checkOwnerWritable(target.root, target.rel, selfAgent, opts.unsafe);
|
|
1262
|
+
const readable = !deny;
|
|
1263
|
+
const ub = utilityBreakdown(entry);
|
|
1264
|
+
const ageDays = entry.created ? daysBetween(entry.created, today()) : 0;
|
|
1265
|
+
const cfg = loadConfig();
|
|
1266
|
+
const archU = parseFloat(cfg.maintain_archived_utility || 0.35);
|
|
1267
|
+
const archAge = parseInt(cfg.maintain_archived_age || '180', 10) || 180;
|
|
1268
|
+
const forgetU = parseFloat(cfg.maintain_forget_utility || 0.12);
|
|
1269
|
+
const forgetAge = parseInt(cfg.maintain_forget_age || '365', 10) || 365;
|
|
1270
|
+
const lines = ['[' + entry.type + '] ' + entry.subject + ': ' + entry.statement];
|
|
1271
|
+
lines.push(' 文件: ' + target.rel + (readable ? '' : '(其它智能体私密,仅元数据)'));
|
|
1272
|
+
lines.push(' 效用分: ' + ub.total + ' = confidence ' + ub.confidence + ' + 使用 ' + ub.usage + ' + 时效 ' + ub.recency + ' + 类型 ' + ub.type + ' + 结构 ' + ub.structure + ')×weight ' + ub.weight);
|
|
1273
|
+
lines.push(' 年龄: ' + ageDays + ' 天 / access_count ' + (entry.access_count || 0) + ' / feedback_net ' + entry.feedback_net + ' / immutable ' + (entry.immutable ? '是' : '否'));
|
|
1274
|
+
const importScore = round3(importanceScore(entry));
|
|
1275
|
+
lines.push(' importance(旧): ' + importScore + ' / vitality(旧): ' + round3(vitality(entry)));
|
|
1276
|
+
if (entry.immutable) lines.push(' 状态: immutable 豁免自动归档/遗忘');
|
|
1277
|
+
else if (ub.total < forgetU && ageDays > forgetAge && entry.type !== 'BOUND') lines.push(' 状态: 遗忘候选(utility < ' + forgetU + ' 且年龄 > ' + forgetAge + ' 天;--purge 才真删)');
|
|
1278
|
+
else if (ub.total < archU && ageDays > archAge) lines.push(' 状态: 归档候选(utility < ' + archU + ' 且年龄 > ' + archAge + ' 天)');
|
|
1279
|
+
else lines.push(' 状态: 保留(未达归档/遗忘阈值)');
|
|
1280
|
+
return { error: false, text: lines.join('\n') };
|
|
1281
|
+
}
|
|
1282
|
+
function jaccardTokens(a, b) {
|
|
1283
|
+
const sa = new Set(Object.keys(a.tokens || {}).filter(function (k) { return k.indexOf(':') === -1; }));
|
|
1284
|
+
const sb = new Set(Object.keys(b.tokens || {}).filter(function (k) { return k.indexOf(':') === -1; }));
|
|
1285
|
+
if (!sa.size || !sb.size) return 0;
|
|
1286
|
+
let inter = 0;
|
|
1287
|
+
for (const k of sa) { if (sb.has(k)) inter++; }
|
|
1288
|
+
const union = sa.size + sb.size - inter;
|
|
1289
|
+
return union ? inter / union : 0;
|
|
1290
|
+
}
|
|
1291
|
+
function subjectsSimilar(a, b) {
|
|
1292
|
+
const sa = String(a.subject || '').toLowerCase();
|
|
1293
|
+
const sb = String(b.subject || '').toLowerCase();
|
|
1294
|
+
if (!sa || !sb) return false;
|
|
1295
|
+
if (sa === sb) return true;
|
|
1296
|
+
return editDistance(sa, sb) <= 3;
|
|
1297
|
+
}
|
|
1298
|
+
// 记忆自组织:规则层自动归档/遗忘/去重(默认 dry-run,--apply 才执行,--purge 才真删)
|
|
1299
|
+
function maintainCore(opts) {
|
|
1300
|
+
opts = opts || {};
|
|
1301
|
+
const apply = !!opts.apply;
|
|
1302
|
+
const purge = !!opts.purge;
|
|
1303
|
+
const root = userRoot();
|
|
1304
|
+
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
|
|
1305
|
+
const cfg = loadConfig();
|
|
1306
|
+
const archU = parseFloat(opts.threshold !== undefined && opts.threshold !== null ? opts.threshold : (cfg.maintain_archived_utility || 0.35));
|
|
1307
|
+
const archAge = parseInt(opts.age || cfg.maintain_archived_age || '180', 10) || 180;
|
|
1308
|
+
const forgetU = parseFloat(cfg.maintain_forget_utility || 0.12);
|
|
1309
|
+
const forgetAge = parseInt(cfg.maintain_forget_age || '365', 10) || 365;
|
|
1310
|
+
const mode = apply ? (purge ? '执行(含遗忘真删 --purge)' : '执行') : '预览(dry-run,未改动;加 --apply 执行,--purge 才真删)';
|
|
1311
|
+
const lines = ['## yotta-memory maintain(记忆自组织)'];
|
|
1312
|
+
lines.push('- 模式: ' + mode);
|
|
1313
|
+
lines.push('- 归档阈值: utility < ' + archU + ' 且年龄 > ' + archAge + ' 天;遗忘阈值: utility < ' + forgetU + ' 且年龄 > ' + forgetAge + ' 天');
|
|
1314
|
+
lines.push('');
|
|
1315
|
+
const toArchive = [], toForget = [], skipped = [];
|
|
1316
|
+
for (const fp of collectEntryFiles(root)) {
|
|
1317
|
+
const owner = ownerFromPrivatePath(root, fp);
|
|
1318
|
+
let meta;
|
|
1319
|
+
try { meta = parseFrontmatter(readMemoryText(root, fp, owner)).meta; } catch (e) { continue; }
|
|
1320
|
+
if (meta.immutable === 'true') { skipped.push({ fp: fp, reason: 'immutable 豁免' }); continue; }
|
|
1321
|
+
if (!meta.created) { skipped.push({ fp: fp, reason: '无 created' }); continue; }
|
|
1322
|
+
const type = (meta.type || 'FACT').toUpperCase();
|
|
1323
|
+
const ageDays = daysBetween(meta.created, today());
|
|
1324
|
+
const u = utilityScore(meta);
|
|
1325
|
+
const rec = { fp: fp, rel: relOf(root, fp), type: type, ageDays: ageDays, utility: u, meta: meta, owner: owner };
|
|
1326
|
+
if (u < forgetU && ageDays > forgetAge) {
|
|
1327
|
+
if (type === 'BOUND') { skipped.push({ fp: fp, reason: 'BOUND 豁免遗忘' }); }
|
|
1328
|
+
else { toForget.push(rec); continue; }
|
|
1329
|
+
}
|
|
1330
|
+
if (u < archU && ageDays > archAge) toArchive.push(rec);
|
|
1331
|
+
}
|
|
1332
|
+
let archived = 0, forgotten = 0;
|
|
1333
|
+
if (toArchive.length) {
|
|
1334
|
+
lines.push('### 归档候选(' + toArchive.length + ' 条)');
|
|
1335
|
+
for (const rec of toArchive) {
|
|
1336
|
+
lines.push('- [' + rec.type + '] ' + rec.rel + '(utility ' + round3(rec.utility) + ',' + rec.ageDays + ' 天)— ' + (rec.meta.subject || '') + ': ' + String(rec.meta.statement || '').slice(0, 40) + (rec.type === 'COMMIT' ? ' ⚠️ COMMIT 收工纪律锚点,请确认' : ''));
|
|
1337
|
+
if (apply) {
|
|
1338
|
+
const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[rec.type]);
|
|
1339
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
1340
|
+
fs.renameSync(rec.fp, path.join(destDir, path.basename(rec.fp)));
|
|
1341
|
+
appendAudit(root, 'audit', { ts: new Date().toISOString(), file: rec.rel, action: 'archive', reason: 'utility<' + round3(rec.utility) + ',age>' + rec.ageDays, utility: round3(rec.utility) });
|
|
1342
|
+
archived++;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
lines.push('');
|
|
1346
|
+
}
|
|
1347
|
+
if (toForget.length) {
|
|
1348
|
+
lines.push('### 遗忘候选(' + toForget.length + ' 条,默认不真删;--purge 才删除)');
|
|
1349
|
+
for (const rec of toForget) {
|
|
1350
|
+
lines.push('- [' + rec.type + '] ' + rec.rel + '(utility ' + round3(rec.utility) + ',' + rec.ageDays + ' 天)');
|
|
1351
|
+
if (apply && purge) {
|
|
1352
|
+
const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[rec.type]);
|
|
1353
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
1354
|
+
const dest = path.join(destDir, path.basename(rec.fp));
|
|
1355
|
+
fs.renameSync(rec.fp, dest);
|
|
1356
|
+
appendAudit(root, 'audit', { ts: new Date().toISOString(), file: rec.rel, action: 'forget', reason: 'utility<' + round3(rec.utility) + ',age>' + rec.ageDays + ',purge', utility: round3(rec.utility) });
|
|
1357
|
+
// 真删前二次确认标记:purge 已显式授权,删除 .archive 内副本前的最终动作 = 记录审计后删除
|
|
1358
|
+
try { fs.unlinkSync(dest); } catch (e) { /* 保留副本 */ }
|
|
1359
|
+
forgotten++;
|
|
1360
|
+
} else if (apply) {
|
|
1361
|
+
lines.push(' (--purge 未开启,跳过真删)');
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
lines.push('');
|
|
1365
|
+
}
|
|
1366
|
+
if (apply) {
|
|
1367
|
+
if (archived || forgotten) {
|
|
1368
|
+
const allMoved = toArchive.concat(toForget).filter(function (r) { return true; });
|
|
1369
|
+
removeIndexEntries(root, allMoved.map(function (r) { return r.rel; }));
|
|
1370
|
+
}
|
|
1371
|
+
lines.push('- 已执行: 归档 ' + archived + ' 条,遗忘 ' + forgotten + ' 条。审计: ' + auditPath(root, 'audit'));
|
|
1372
|
+
} else {
|
|
1373
|
+
lines.push('- 未执行任何变更(dry-run)。加 --apply 执行归档,--purge 才真删遗忘候选。');
|
|
1374
|
+
}
|
|
1375
|
+
if (skipped.length) {
|
|
1376
|
+
lines.push('');
|
|
1377
|
+
lines.push('### 跳过(' + skipped.length + ' 条)');
|
|
1378
|
+
const grouped = {};
|
|
1379
|
+
for (const s of skipped) { grouped[s.reason] = (grouped[s.reason] || 0) + 1; }
|
|
1380
|
+
for (const k of Object.keys(grouped)) lines.push('- ' + k + ': ' + grouped[k] + ' 条');
|
|
1381
|
+
}
|
|
1382
|
+
// 去重
|
|
1383
|
+
if (opts.dedup) {
|
|
1384
|
+
lines.push('');
|
|
1385
|
+
lines.push('### 去重候选(--dedup)');
|
|
1386
|
+
const entries = ensureIndex(root);
|
|
1387
|
+
const groups = [];
|
|
1388
|
+
const used = new Set();
|
|
1389
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1390
|
+
if (used.has(i)) continue;
|
|
1391
|
+
const g = [entries[i]];
|
|
1392
|
+
used.add(i);
|
|
1393
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
1394
|
+
if (used.has(j)) continue;
|
|
1395
|
+
if (subjectsSimilar(entries[i], entries[j]) && jaccardTokens(entries[i], entries[j]) >= 0.7) {
|
|
1396
|
+
g.push(entries[j]); used.add(j);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
if (g.length > 1) groups.push(g);
|
|
1400
|
+
}
|
|
1401
|
+
if (!groups.length) lines.push('(无重复候选)');
|
|
1402
|
+
for (const g of groups) {
|
|
1403
|
+
lines.push('- 组: ' + g.map(function (e) { return e.file; }).join(' | '));
|
|
1404
|
+
lines.push(' ' + g.map(function (e) { return e.subject + ': ' + e.statement; }).join(' / '));
|
|
1405
|
+
}
|
|
1406
|
+
lines.push(' 合并: yotta-memory maintain --merge <fileA> <fileB>');
|
|
1407
|
+
}
|
|
1408
|
+
return { error: false, text: lines.join('\n') };
|
|
1409
|
+
}
|
|
1410
|
+
// 手动合并两条相似记忆:保留高 confidence,低 confidence 移入 .archive
|
|
1411
|
+
function mergeCore(refA, refB, opts) {
|
|
1412
|
+
opts = opts || {};
|
|
1413
|
+
const selfAgent = opts.selfAgent || currentAgent();
|
|
1414
|
+
const ta = resolveMemoryTarget(refA);
|
|
1415
|
+
const tb = resolveMemoryTarget(refB);
|
|
1416
|
+
if (!ta || !tb) return { error: true, text: '未找到记忆文件: ' + refA + ' / ' + refB };
|
|
1417
|
+
for (const t of [ta, tb]) {
|
|
1418
|
+
const deny = checkOwnerWritable(t.root, t.rel, selfAgent, opts.unsafe);
|
|
1419
|
+
if (deny) return { error: true, text: deny };
|
|
1420
|
+
}
|
|
1421
|
+
if (ta.root !== tb.root) return { error: true, text: '暂不支持跨记忆库合并(' + ta.root + ' vs ' + tb.root + ')。' };
|
|
1422
|
+
const root = ta.root;
|
|
1423
|
+
const ea = readEntry(ta.fp, root);
|
|
1424
|
+
const eb = readEntry(tb.fp, root);
|
|
1425
|
+
const keep = ea.confidence >= eb.confidence ? ea : eb;
|
|
1426
|
+
const drop = ea.confidence >= eb.confidence ? eb : ea;
|
|
1427
|
+
const keepFp = ea.confidence >= eb.confidence ? ta.fp : tb.fp;
|
|
1428
|
+
const dropFp = ea.confidence >= eb.confidence ? tb.fp : ta.fp;
|
|
1429
|
+
const owner = ownerFromPrivatePath(root, keepFp);
|
|
1430
|
+
const tags = Array.from(new Set((keep.tags || []).concat(drop.tags || [])));
|
|
1431
|
+
const patch = { updated: today(), tags: JSON.stringify(tags), access_count: (keep.access_count || 0) + (drop.access_count || 0), feedback_net: (keep.feedback_net || 0) + (drop.feedback_net || 0) };
|
|
1432
|
+
rewriteFrontmatter(keepFp, patch, root, owner);
|
|
1433
|
+
upsertIndexEntry(root, readEntry(keepFp, root));
|
|
1434
|
+
const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[drop.type]);
|
|
1435
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
1436
|
+
const dest = path.join(destDir, path.basename(dropFp));
|
|
1437
|
+
fs.renameSync(dropFp, dest);
|
|
1438
|
+
removeIndexEntry(root, drop.file);
|
|
1439
|
+
appendAudit(root, 'audit', { ts: new Date().toISOString(), file: drop.file, action: 'merge', reason: '并入 ' + keep.file, utility: round3(utilityScore(keep)) });
|
|
1440
|
+
return { error: false, text: '已合并: ' + drop.file + ' -> ' + keep.file + '\n 保留: [' + keep.type + '] ' + keep.subject + ': ' + keep.statement + '\n 归档低 confidence: ' + drop.file };
|
|
1441
|
+
}
|
|
1442
|
+
// 心理日志蒸馏:统计摘要 / 主题画像 / 知识地图(启发式默认,可选 --model)
|
|
1443
|
+
function distillCore(opts) {
|
|
1444
|
+
opts = opts || {};
|
|
1445
|
+
const root = userRoot();
|
|
1446
|
+
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
|
|
1447
|
+
const selfAgent = opts.selfAgent || currentAgent();
|
|
1448
|
+
const owner = opts.owner || selfAgent;
|
|
1449
|
+
const entries = [];
|
|
1450
|
+
for (const r of memoryRoots()) {
|
|
1451
|
+
for (const e of ensureIndex(r)) {
|
|
1452
|
+
if (classifyRead(e, owner, '', !!opts.unsafe, selfAgent) === 'denied') continue;
|
|
1453
|
+
entries.push(e);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
const lines = [];
|
|
1457
|
+
lines.push('# 元忆记忆蒸馏报告(v0.8.0)');
|
|
1458
|
+
lines.push('');
|
|
1459
|
+
lines.push('> 生成: yotta-memory distill | 启发式蒸馏(零依赖);--model <cmd> 可选外部模型增强');
|
|
1460
|
+
lines.push('> 时间: ' + new Date().toISOString() + ' / 可读条目: ' + entries.length + ' 条');
|
|
1461
|
+
lines.push('');
|
|
1462
|
+
// 1) 统计摘要
|
|
1463
|
+
lines.push('## 一、统计摘要');
|
|
1464
|
+
lines.push('');
|
|
1465
|
+
if (!entries.length) lines.push('(无记忆)');
|
|
1466
|
+
const byType = {};
|
|
1467
|
+
for (const e of entries) byType[e.type] = (byType[e.type] || 0) + 1;
|
|
1468
|
+
lines.push('- 类型分布: ' + Object.keys(byType).map(function (k) { return k + ' ' + byType[k]; }).join(' / '));
|
|
1469
|
+
let young = 0, mid = 0, old = 0, veryOld = 0;
|
|
1470
|
+
for (const e of entries) {
|
|
1471
|
+
if (!e.created) continue;
|
|
1472
|
+
const d = daysBetween(e.created, today());
|
|
1473
|
+
if (d < 30) young++; else if (d < 90) mid++; else if (d < 180) old++; else veryOld++;
|
|
1474
|
+
}
|
|
1475
|
+
lines.push('- 年龄分布: <30天 ' + young + ' / 30-90 ' + mid + ' / 90-180 ' + old + ' / >180天 ' + veryOld);
|
|
1476
|
+
const hot = entries.slice().sort(function (a, b) { return (b.access_count || 0) - (a.access_count || 0); }).slice(0, 5);
|
|
1477
|
+
lines.push('- 热度 Top5(access_count):');
|
|
1478
|
+
for (const h of hot) lines.push(' - [' + h.type + '] ' + h.subject + '(' + (h.access_count || 0) + ' 次)');
|
|
1479
|
+
let fbSum = 0, usefulN = 0, uselessN = 0;
|
|
1480
|
+
for (const e of entries) { const n = e.feedback_net || 0; fbSum += n; if (n > 0) usefulN++; else if (n < 0) uselessN++; }
|
|
1481
|
+
lines.push('- 反馈统计: feedback_net 总和 ' + fbSum + '(正 ' + usefulN + ' 条 / 负 ' + uselessN + ' 条)');
|
|
1482
|
+
lines.push('');
|
|
1483
|
+
// 2) 主题画像(按 subject 聚类合并)
|
|
1484
|
+
lines.push('## 二、主题画像(按 subject 聚类,启发式合并)');
|
|
1485
|
+
lines.push('');
|
|
1486
|
+
const groups = {};
|
|
1487
|
+
for (const e of entries) {
|
|
1488
|
+
const key = String(e.subject || '(无主题)');
|
|
1489
|
+
(groups[key] = groups[key] || []).push(e);
|
|
1490
|
+
}
|
|
1491
|
+
const sortedKeys = Object.keys(groups).sort(function (a, b) { return groups[b].length - groups[a].length; });
|
|
1492
|
+
for (const k of sortedKeys) {
|
|
1493
|
+
const g = groups[k];
|
|
1494
|
+
const best = g.slice().sort(function (a, b) { return (b.confidence || 0) - (a.confidence || 0); })[0];
|
|
1495
|
+
const tags = Array.from(new Set([].concat.apply([], g.map(function (e) { return e.tags || []; }))));
|
|
1496
|
+
lines.push('### ' + k + '(' + g.length + ' 条)');
|
|
1497
|
+
lines.push('- 代表: ' + best.statement);
|
|
1498
|
+
if (tags.length) lines.push('- 标签: ' + tags.join(', '));
|
|
1499
|
+
lines.push('- 引用: ' + g.map(function (e) { return e.file; }).join(' / '));
|
|
1500
|
+
lines.push('');
|
|
1501
|
+
}
|
|
1502
|
+
// 3) 知识地图(type + tags 层级)
|
|
1503
|
+
lines.push('## 三、知识地图(type → tags)');
|
|
1504
|
+
lines.push('');
|
|
1505
|
+
const map = {};
|
|
1506
|
+
for (const e of entries) {
|
|
1507
|
+
const t = e.type;
|
|
1508
|
+
(map[t] = map[t] || {});
|
|
1509
|
+
const tagKey = (e.tags && e.tags.length) ? e.tags.join(',') : '(无标签)';
|
|
1510
|
+
(map[t][tagKey] = map[t][tagKey] || []).push(e);
|
|
1511
|
+
}
|
|
1512
|
+
for (const t of Object.keys(map).sort()) {
|
|
1513
|
+
lines.push('### ' + t);
|
|
1514
|
+
for (const tk of Object.keys(map[t]).sort()) {
|
|
1515
|
+
lines.push('- ' + tk + ': ' + map[t][tk].map(function (e) { return e.subject; }).join(' / '));
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
let body = lines.join('\n') + '\n';
|
|
1519
|
+
// 可选模型增强:--model <cmd>,stdin 传结构化摘要,stdout 取提炼文本
|
|
1520
|
+
let modelOut = '';
|
|
1521
|
+
if (opts.model) {
|
|
1522
|
+
try {
|
|
1523
|
+
const payload = JSON.stringify({ summary: lines.join('\n'), entries: entries.map(function (e) { return { type: e.type, subject: e.subject, statement: e.statement, tags: e.tags, confidence: e.confidence, access_count: e.access_count, feedback_net: e.feedback_net }; }).slice(0, 200) });
|
|
1524
|
+
const r = child_process.spawnSync(String(opts.model), { input: payload, encoding: 'utf8', maxBuffer: 1024 * 1024, shell: process.platform === 'win32' });
|
|
1525
|
+
if (r.stdout) modelOut = String(r.stdout).trim();
|
|
1526
|
+
if (!modelOut) modelOut = '(模型无输出)';
|
|
1527
|
+
body += '\n## 四、模型提炼(--model ' + opts.model + ')\n\n' + modelOut + '\n';
|
|
1528
|
+
} catch (e) {
|
|
1529
|
+
modelOut = '(模型调用失败: ' + e.message + ')';
|
|
1530
|
+
body += '\n## 四、模型提炼\n\n' + modelOut + '\n';
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
// 落盘:--out 指定 / 私密 distills / 公共 facts/distills
|
|
1534
|
+
let written = '';
|
|
1535
|
+
try {
|
|
1536
|
+
if (opts.out) {
|
|
1537
|
+
const outPath = path.resolve(String(opts.out));
|
|
1538
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
1539
|
+
fs.writeFileSync(outPath, body, 'utf8');
|
|
1540
|
+
written = outPath;
|
|
1541
|
+
} else {
|
|
1542
|
+
const slug = String((opts.subject || 'distill').replace(/[^\w\u4e00-\u9fa5-]+/g, '-').slice(0, 40));
|
|
1543
|
+
const fname = today() + '-' + slug + '.md';
|
|
1544
|
+
if (owner && entries.some(function (e) { return e.scope !== 'public' && e.owner === owner; })) {
|
|
1545
|
+
const dir = path.join(root, PRIVATE_DIR, owner, 'distills');
|
|
1546
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1547
|
+
const fp = path.join(dir, fname);
|
|
1548
|
+
const text = frontmatterToText({ type: 'COMMIT', subject: '蒸馏报告 ' + slug, statement: '记忆蒸馏报告(' + today() + ')', confidence: 1.0, created: today(), updated: today(), tags: ['distill'], immutable: false, scope: 'private', owner: owner, source: 'distill', weight: 1.0, access_count: 0, last_accessed: '', feedback_net: 0 }, body);
|
|
1549
|
+
writeMemoryText(root, fp, text, owner);
|
|
1550
|
+
upsertIndexEntry(root, readEntry(fp, root));
|
|
1551
|
+
written = fp;
|
|
1552
|
+
} else {
|
|
1553
|
+
const dir = path.join(root, PUBLIC_DIR, 'distills');
|
|
1554
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1555
|
+
const fp = path.join(dir, fname);
|
|
1556
|
+
const text = frontmatterToText({ type: 'FACT', subject: '蒸馏报告 ' + slug, statement: '记忆蒸馏报告(' + today() + ')', confidence: 1.0, created: today(), updated: today(), tags: ['distill'], immutable: false, scope: 'public', owner: '', source: 'distill', weight: 1.0, access_count: 0, last_accessed: '', feedback_net: 0 }, body);
|
|
1557
|
+
fs.writeFileSync(fp, text, 'utf8');
|
|
1558
|
+
upsertIndexEntry(root, readEntry(fp, root));
|
|
1559
|
+
written = fp;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
} catch (e) {
|
|
1563
|
+
return { error: false, text: lines.join('\n') + '\n\n[落盘失败] ' + e.message };
|
|
1564
|
+
}
|
|
1565
|
+
return { error: false, text: body + '\n[已写入] ' + written };
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function promptPassword(promptText) {
|
|
1569
|
+
return new Promise(function (resolve) {
|
|
1570
|
+
const stdin = process.stdin;
|
|
1571
|
+
if (!stdin.isTTY) { resolve(null); return; }
|
|
1572
|
+
process.stdout.write(promptText);
|
|
1573
|
+
let buf = '';
|
|
1574
|
+
function done(abort) {
|
|
1575
|
+
try { stdin.setRawMode(false); } catch (e) {}
|
|
1576
|
+
stdin.pause();
|
|
1577
|
+
stdin.removeListener('data', onData);
|
|
1578
|
+
process.stdout.write('\n');
|
|
1579
|
+
resolve(abort ? null : buf);
|
|
1580
|
+
}
|
|
1581
|
+
function onData(chunk) {
|
|
1582
|
+
const s = String(chunk);
|
|
1583
|
+
for (const c of s) {
|
|
1584
|
+
if (c === '\r' || c === '\n') { done(false); return; }
|
|
1585
|
+
if (c === '\u0003') { done(true); return; }
|
|
1586
|
+
if (c === '\u007f' || c === '\b') buf = buf.slice(0, -1);
|
|
1587
|
+
else buf += c;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
try { stdin.setRawMode(true); } catch (e) { resolve(null); return; }
|
|
1591
|
+
stdin.resume();
|
|
1592
|
+
stdin.setEncoding('utf8');
|
|
1593
|
+
stdin.on('data', onData);
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
function migrateCore(root, password, recoveryKeyIn) {
|
|
1598
|
+
if (!fs.existsSync(root)) return { error: true, text: '记忆库不存在,请先 yotta-memory init。' };
|
|
1599
|
+
if (isEncrypted(root)) return { error: true, text: '记忆库已启用加密(keys/ 存在),无需迁移。' };
|
|
1600
|
+
if (!hasPlaintextPrivate(root)) return { error: false, text: '无私密明文可迁移(private/ 下无 .md 文件);如需新建加密库请 init --encrypt。' };
|
|
1601
|
+
const salt = crypto.randomBytes(16);
|
|
1602
|
+
ensureKeysDir(root);
|
|
1603
|
+
fs.writeFileSync(encSaltPath(root), salt);
|
|
1604
|
+
const umk = deriveUmk(password, salt);
|
|
1605
|
+
const rk = crypto.randomBytes(32);
|
|
1606
|
+
fs.writeFileSync(encRecoveryEncPath(root), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(umk, rk, Buffer.from('recovery', 'utf8'))]));
|
|
1607
|
+
let moved = 0;
|
|
1608
|
+
const owners = collectOwners(root);
|
|
1609
|
+
const self = currentAgent();
|
|
1610
|
+
for (const owner of owners) {
|
|
1611
|
+
const ok = wrapOwnerKey(root, umk, rk, owner);
|
|
1612
|
+
if (self && owner === self) writeOwnerKeyCache(root, owner, ok);
|
|
1613
|
+
for (const t of PRIVATE_LEAF) {
|
|
1614
|
+
const d = path.join(root, PRIVATE_DIR, owner, t);
|
|
1615
|
+
if (!fs.existsSync(d)) continue;
|
|
1616
|
+
for (const f of fs.readdirSync(d)) {
|
|
1617
|
+
if (!f.endsWith('.md')) continue;
|
|
1618
|
+
const fp = path.join(d, f);
|
|
1619
|
+
const text = fs.readFileSync(fp, 'utf8');
|
|
1620
|
+
fs.writeFileSync(fp + ENC_SUFFIX, encryptMemoryText(text, ok));
|
|
1621
|
+
fs.unlinkSync(fp);
|
|
1622
|
+
moved++;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
const pf = path.join(root, PRIVATE_DIR, owner, PROFILE_FILE);
|
|
1626
|
+
if (fs.existsSync(pf)) {
|
|
1627
|
+
const text = fs.readFileSync(pf, 'utf8');
|
|
1628
|
+
fs.writeFileSync(pf + ENC_SUFFIX, encryptMemoryText(text, ok));
|
|
1629
|
+
fs.unlinkSync(pf);
|
|
1630
|
+
moved++;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
buildIndex(root);
|
|
1634
|
+
let tail = '已迁移 ' + moved + ' 个私密文件(' + owners.length + ' 个 owner)到密文。';
|
|
1635
|
+
tail += '\n[恢复钥匙](务必离线保存,仅此一次,泄露=可解全部私密): ' + rk.toString('base64');
|
|
1636
|
+
if (self && owners.indexOf(self) !== -1) tail += '\n已为本智能体 ' + self + ' 写入授权缓存,可继续读写自己的私密。';
|
|
1637
|
+
else tail += '\n各智能体需在用户平台授权(yotta-memory view → 授权本智能体)后才能读写自己的私密。';
|
|
1638
|
+
return { error: false, text: tail };
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
async function cmdMigrate(opts) {
|
|
1642
|
+
const root = userRoot();
|
|
1643
|
+
if (isEncrypted(root)) { console.error('记忆库已启用加密,无需迁移。'); process.exit(2); }
|
|
1644
|
+
if (!hasPlaintextPrivate(root)) { console.error('无私密明文可迁移。'); process.exit(2); }
|
|
1645
|
+
let password = opts.password || process.env.YOTTA_MEMORY_PASS || '';
|
|
1646
|
+
if (!password) {
|
|
1647
|
+
password = await promptPassword('主口令(迁移后私密区加密,勿忘;忘口令可用恢复钥匙重设): ') || '';
|
|
1648
|
+
if (!password) { console.error('已取消。'); process.exit(2); }
|
|
1649
|
+
}
|
|
1650
|
+
const r = migrateCore(root, password, null);
|
|
1651
|
+
console.log(r.text);
|
|
1652
|
+
if (r.error) process.exit(2);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function resetPasswordCore(root, opts) {
|
|
1656
|
+
opts = opts || {};
|
|
1657
|
+
if (!isEncrypted(root)) return { error: true, text: '记忆库未启用加密。' };
|
|
1658
|
+
const salt = loadSalt(root);
|
|
1659
|
+
if (!salt) return { error: true, text: '密钥库损坏(缺 keys/salt)。' };
|
|
1660
|
+
const owners = keyOwners(root).filter(function (o) { return fs.existsSync(encOwnerKeyPath(root, o)); });
|
|
1661
|
+
if (!owners.length) return { error: true, text: '没有可重设的 owner 密钥(keys/ 下无 *.key.enc)。' };
|
|
1662
|
+
const newPass = String(opts.newPassword || '');
|
|
1663
|
+
if (!newPass) return { error: true, text: '请提供新口令(--new-password 或交互输入)。' };
|
|
1664
|
+
if (opts.recoveryKey) {
|
|
1665
|
+
const rk = Buffer.from(String(opts.recoveryKey).trim(), 'base64');
|
|
1666
|
+
if (rk.length !== 32) return { error: true, text: '恢复钥匙格式错误(须为 32 字节 base64)。' };
|
|
1667
|
+
let verified = false;
|
|
1668
|
+
for (const o of owners) { try { unwrapOwnerKeyRecovery(root, o, rk); verified = true; break; } catch (e) {} }
|
|
1669
|
+
if (!verified) return { error: true, text: '恢复钥匙无效(无法解开任何 owner 密钥)。' };
|
|
1670
|
+
const newUmk = deriveUmk(newPass, salt);
|
|
1671
|
+
for (const o of owners) {
|
|
1672
|
+
const ok = unwrapOwnerKeyRecovery(root, o, rk);
|
|
1673
|
+
const aad = Buffer.from('owner:' + o, 'utf8');
|
|
1674
|
+
fs.writeFileSync(encOwnerKeyPath(root, o), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(newUmk, ok, aad)]));
|
|
1675
|
+
}
|
|
1676
|
+
fs.writeFileSync(encRecoveryEncPath(root), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(newUmk, rk, Buffer.from('recovery', 'utf8'))]));
|
|
1677
|
+
return { error: false, text: '口令已重设(恢复钥匙验证通过,已重新包裹全部 owner 密钥)。' };
|
|
1678
|
+
}
|
|
1679
|
+
const curPass = String(opts.password || '');
|
|
1680
|
+
if (!curPass) return { error: true, text: '请提供当前口令(--password 或交互输入),或用 --recovery-key。' };
|
|
1681
|
+
const curUmk = deriveUmk(curPass, salt);
|
|
1682
|
+
let verified = false;
|
|
1683
|
+
for (const o of owners) { try { unwrapOwnerKey(root, o, curUmk); verified = true; break; } catch (e) {} }
|
|
1684
|
+
if (!verified) return { error: true, text: '当前口令错误。' };
|
|
1685
|
+
const newUmk = deriveUmk(newPass, salt);
|
|
1686
|
+
for (const o of owners) {
|
|
1687
|
+
const ok = unwrapOwnerKey(root, o, curUmk);
|
|
1688
|
+
const aad = Buffer.from('owner:' + o, 'utf8');
|
|
1689
|
+
fs.writeFileSync(encOwnerKeyPath(root, o), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(newUmk, ok, aad)]));
|
|
1690
|
+
}
|
|
1691
|
+
fs.writeFileSync(encRecoveryEncPath(root), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(newUmk, unwrapRecoveryEnc(root, curUmk), Buffer.from('recovery', 'utf8'))]));
|
|
1692
|
+
return { error: false, text: '口令已重设(当前口令验证通过)。' };
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
async function cmdResetPassword(opts) {
|
|
1696
|
+
const root = userRoot();
|
|
1697
|
+
if (!isEncrypted(root)) { console.error('记忆库未启用加密。'); process.exit(2); }
|
|
1698
|
+
let cur = opts.password || '';
|
|
1699
|
+
let rk = opts.recoveryKey || '';
|
|
1700
|
+
let np = opts.newPassword || process.env.YOTTA_MEMORY_PASS || '';
|
|
1701
|
+
if (!rk && !cur) {
|
|
1702
|
+
cur = await promptPassword('当前口令(或 --recovery-key 用恢复钥匙): ') || '';
|
|
1703
|
+
if (!cur) { console.error('已取消。'); process.exit(2); }
|
|
1704
|
+
}
|
|
1705
|
+
if (!np) {
|
|
1706
|
+
np = await promptPassword('新口令: ') || '';
|
|
1707
|
+
if (!np) { console.error('已取消。'); process.exit(2); }
|
|
1708
|
+
const np2 = await promptPassword('再次输入确认: ') || '';
|
|
1709
|
+
if (np2 !== np) { console.error('两次输入不一致。'); process.exit(2); }
|
|
1710
|
+
}
|
|
1711
|
+
const r = resetPasswordCore(root, { password: cur, recoveryKey: rk, newPassword: np });
|
|
1712
|
+
console.log(r.text);
|
|
1713
|
+
if (r.error) process.exit(2);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function keyAuthorizeCore(root, owner, password) {
|
|
1717
|
+
if (!isEncrypted(root)) return { error: true, text: '记忆库未启用加密。' };
|
|
1718
|
+
const salt = loadSalt(root);
|
|
1719
|
+
if (!salt) return { error: true, text: '密钥库损坏(缺 keys/salt)。' };
|
|
1720
|
+
const umk = deriveUmk(password, salt);
|
|
1721
|
+
let ok = null;
|
|
1722
|
+
try { ok = unwrapOwnerKey(root, owner, umk); } catch (e) { ok = null; }
|
|
1723
|
+
if (!ok) {
|
|
1724
|
+
let rk = null;
|
|
1725
|
+
try { rk = unwrapRecoveryEnc(root, umk); } catch (e) { rk = null; }
|
|
1726
|
+
if (!rk) return { error: true, text: '口令错误。' };
|
|
1727
|
+
ok = wrapOwnerKey(root, umk, rk, owner);
|
|
1728
|
+
}
|
|
1729
|
+
writeOwnerKeyCache(root, owner, ok);
|
|
1730
|
+
return { error: false, text: '已授权 ' + owner + ' 读取其私密(写入授权缓存 keys/cache/' + owner + '.key)。' };
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function keyRevokeCore(root, owner) {
|
|
1734
|
+
if (!revokeOwnerKeyCache(root, owner)) return { error: false, text: owner + ' 无授权缓存(已是未授权状态)。' };
|
|
1735
|
+
return { error: false, text: '已吊销 ' + owner + ' 的授权缓存(该智能体将无法解密其私密)。' };
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
function cmdKeyList() {
|
|
1739
|
+
const root = userRoot();
|
|
1740
|
+
if (!isEncrypted(root)) { console.log('记忆库未启用加密。'); return; }
|
|
1741
|
+
const owners = collectOwners(root);
|
|
1742
|
+
const authorized = owners.filter(function (o) { return fs.existsSync(encCachePath(root, o)); });
|
|
1743
|
+
console.log('owner: ' + (owners.length ? owners.join(', ') : '(无)'));
|
|
1744
|
+
console.log('已授权缓存: ' + (authorized.length ? authorized.join(', ') : '(无)'));
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
async function cmdKeyAuthorize(id, opts) {
|
|
1748
|
+
const root = userRoot();
|
|
1749
|
+
if (!isEncrypted(root)) { console.error('记忆库未启用加密。'); process.exit(2); }
|
|
1750
|
+
if (!id) { console.error('请指定 <id>:yotta-memory key authorize <id>'); process.exit(2); }
|
|
1751
|
+
let password = opts.password || process.env.YOTTA_MEMORY_PASS || '';
|
|
1752
|
+
if (!password) {
|
|
1753
|
+
password = await promptPassword('主口令: ') || '';
|
|
1754
|
+
if (!password) { console.error('已取消。'); process.exit(2); }
|
|
1755
|
+
}
|
|
1756
|
+
const r = keyAuthorizeCore(root, id, password);
|
|
1757
|
+
console.log(r.text);
|
|
1758
|
+
if (r.error) process.exit(2);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
function cmdKeyRevoke(id) {
|
|
1762
|
+
const root = userRoot();
|
|
1763
|
+
if (!id) { console.error('请指定 <id>:yotta-memory key revoke <id>'); process.exit(2); }
|
|
1764
|
+
const r = keyRevokeCore(root, id);
|
|
1765
|
+
console.log(r.text);
|
|
1766
|
+
if (r.error) process.exit(2);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function viewServerCore(root, port, host) {
|
|
1770
|
+
let session = { umk: null, ownerKeys: {} };
|
|
1771
|
+
const server = http.createServer(function (req, res) {
|
|
1772
|
+
let pathname = '/';
|
|
1773
|
+
try { pathname = new URL(req.url, 'http://' + (req.headers.host || 'localhost')).pathname; } catch (e) {}
|
|
1774
|
+
function json(code, obj) {
|
|
1775
|
+
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1776
|
+
res.end(JSON.stringify(obj));
|
|
1777
|
+
}
|
|
1778
|
+
function readBody(cb) {
|
|
1779
|
+
let body = '';
|
|
1780
|
+
req.on('data', function (c) { body += c; });
|
|
1781
|
+
req.on('end', function () {
|
|
1782
|
+
let d = {};
|
|
1783
|
+
try { d = JSON.parse(body || '{}'); } catch (e) {}
|
|
1784
|
+
cb(d);
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
if (req.method === 'GET' && pathname === '/') {
|
|
1788
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1789
|
+
res.end(VIEW_HTML);
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
if (req.method === 'POST' && pathname === '/api/status') {
|
|
1793
|
+
return json(200, { encrypted: isEncrypted(root), unlocked: !!session.umk, version: VERSION });
|
|
1794
|
+
}
|
|
1795
|
+
if (req.method === 'POST' && pathname === '/api/unlock') {
|
|
1796
|
+
return readBody(function (d) {
|
|
1797
|
+
const password = String(d.password || '');
|
|
1798
|
+
const salt = loadSalt(root);
|
|
1799
|
+
if (!salt) return json(400, { error: '记忆库未启用加密。' });
|
|
1800
|
+
const umk = deriveUmk(password, salt);
|
|
1801
|
+
const ownerKeys = {};
|
|
1802
|
+
const owners = keyOwners(root);
|
|
1803
|
+
let verified = false;
|
|
1804
|
+
for (const o of owners) {
|
|
1805
|
+
try { ownerKeys[o] = unwrapOwnerKey(root, o, umk); verified = true; } catch (e) {}
|
|
1806
|
+
}
|
|
1807
|
+
if (!verified) return json(401, { error: '口令错误。' });
|
|
1808
|
+
session = { umk: umk, ownerKeys: ownerKeys };
|
|
1809
|
+
json(200, { ok: true, owners: Object.keys(ownerKeys) });
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
if (req.method === 'POST' && pathname === '/api/owners') {
|
|
1813
|
+
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
1814
|
+
const agents = loadAgents(root).agents || {};
|
|
1815
|
+
const owners = keyOwners(root);
|
|
1816
|
+
const all = Array.from(new Set(owners.concat(Object.keys(agents))));
|
|
1817
|
+
return json(200, { owners: all.map(function (o) { return { owner: o, registered: !!agents[o], authorized: !!session.ownerKeys[o] && fs.existsSync(encCachePath(root, o)) }; }) });
|
|
1818
|
+
}
|
|
1819
|
+
if (req.method === 'POST' && pathname === '/api/entries') {
|
|
1820
|
+
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
1821
|
+
return readBody(function (d) {
|
|
1822
|
+
const q = String(d.query || '').toLowerCase();
|
|
1823
|
+
const entries = [];
|
|
1824
|
+
for (const e of (loadIndex(root) || [])) entries.push(e);
|
|
1825
|
+
for (const o of collectOwners(root)) {
|
|
1826
|
+
const key = session.ownerKeys[o];
|
|
1827
|
+
if (!key) continue;
|
|
1828
|
+
try { for (const e of loadOwnerIndex(root, o, key)) entries.push(e); } catch (err) {}
|
|
1829
|
+
}
|
|
1830
|
+
const out = q ? entries.filter(function (e) { return ((e.subject || '') + ' ' + (e.statement || '') + ' ' + e.tags.join(' ')).toLowerCase().indexOf(q) !== -1; }) : entries;
|
|
1831
|
+
out.sort(function (a, b) { return String(b.updated || b.created || '').localeCompare(String(a.updated || a.created || '')); });
|
|
1832
|
+
json(200, { count: out.length, entries: out });
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
if (req.method === 'POST' && pathname === '/api/export') {
|
|
1836
|
+
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
1837
|
+
const memories = [];
|
|
1838
|
+
for (const e of (loadIndex(root) || [])) memories.push({ file: e.file, meta: e.meta });
|
|
1839
|
+
for (const o of collectOwners(root)) {
|
|
1840
|
+
const key = session.ownerKeys[o];
|
|
1841
|
+
if (!key) continue;
|
|
1842
|
+
try { for (const e of loadOwnerIndex(root, o, key)) memories.push({ file: e.file, meta: e.meta }); } catch (err) {}
|
|
1843
|
+
}
|
|
1844
|
+
return json(200, { format: 'yottamemory', version: 2, exported: today(), memories: memories });
|
|
1845
|
+
}
|
|
1846
|
+
if (req.method === 'POST' && pathname === '/api/authorize') {
|
|
1847
|
+
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
1848
|
+
return readBody(function (d) {
|
|
1849
|
+
const owner = String(d.owner || '');
|
|
1850
|
+
if (!owner) return json(400, { error: '缺少 owner。' });
|
|
1851
|
+
let ok = session.ownerKeys[owner];
|
|
1852
|
+
let rk = null;
|
|
1853
|
+
try { rk = unwrapRecoveryEnc(root, session.umk); } catch (e) { rk = null; }
|
|
1854
|
+
if (!ok) {
|
|
1855
|
+
if (!rk) return json(400, { error: '密钥库缺少恢复钥匙(recovery.key.enc),无法为新 owner 建钥。' });
|
|
1856
|
+
ok = wrapOwnerKey(root, session.umk, rk, owner);
|
|
1857
|
+
session.ownerKeys[owner] = ok;
|
|
1858
|
+
}
|
|
1859
|
+
writeOwnerKeyCache(root, owner, ok);
|
|
1860
|
+
json(200, { ok: true });
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
if (req.method === 'POST' && pathname === '/api/revoke') {
|
|
1864
|
+
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
1865
|
+
return readBody(function (d) {
|
|
1866
|
+
revokeOwnerKeyCache(root, String(d.owner || ''));
|
|
1867
|
+
json(200, { ok: true });
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
if (req.method === 'POST' && pathname === '/api/reset-password') {
|
|
1871
|
+
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
1872
|
+
return readBody(function (d) {
|
|
1873
|
+
const r = resetPasswordCore(root, { password: String(d.currentPassword || ''), recoveryKey: String(d.recoveryKey || ''), newPassword: String(d.newPassword || '') });
|
|
1874
|
+
if (r.error) return json(400, { error: r.text });
|
|
1875
|
+
const salt = loadSalt(root);
|
|
1876
|
+
if (salt) session = { umk: deriveUmk(String(d.newPassword || ''), salt), ownerKeys: {} };
|
|
1877
|
+
json(200, { ok: true, text: r.text });
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
if (req.method === 'POST' && pathname === '/api/recovery-key') {
|
|
1881
|
+
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
1882
|
+
const rk = unwrapRecoveryEnc(root, session.umk);
|
|
1883
|
+
if (!rk) return json(400, { error: '未找到恢复钥匙。' });
|
|
1884
|
+
return json(200, { recoveryKey: rk.toString('base64') });
|
|
1885
|
+
}
|
|
1886
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1887
|
+
res.end('not found');
|
|
1888
|
+
});
|
|
1889
|
+
server.listen(port, host, function () {
|
|
1890
|
+
console.log('yotta-memory 用户查看平台已启动(v' + VERSION + ')');
|
|
1891
|
+
console.log('URL: http://' + host + ':' + port + '(默认仅本机 127.0.0.1;远程需 --host 显式开启)');
|
|
1892
|
+
console.log('记忆库: ' + root);
|
|
1893
|
+
console.log('输入主口令解锁后可浏览 / 搜索 / 导出全部 AI 的记忆(含私密)。');
|
|
1894
|
+
console.log('按 Ctrl+C 停止');
|
|
1895
|
+
});
|
|
1896
|
+
return server;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function cmdView(opts) {
|
|
1900
|
+
const root = userRoot();
|
|
1901
|
+
ensureInit(root);
|
|
1902
|
+
if (!isEncrypted(root)) {
|
|
1903
|
+
console.error('记忆库未启用加密(无 keys/)。yotta-memory view 是加密库的用户查看平台;请先 init --encrypt 或 migrate。');
|
|
1904
|
+
process.exit(2);
|
|
1905
|
+
}
|
|
1906
|
+
const host = opts.host || '127.0.0.1';
|
|
1907
|
+
const port = opts.port || 8788;
|
|
1908
|
+
viewServerCore(root, port, host);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
const VIEW_HTML = "<!doctype html><html lang=\"zh\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>元忆 · 用户查看平台</title><style>\nbody{font-family:system-ui,-apple-system,\"Microsoft YaHei\",sans-serif;max-width:1000px;margin:24px auto;padding:0 16px;color:#1f2328;background:#fafafa}\nh1{font-size:22px} .card{background:#fff;border:1px solid #e2e2e2;border-radius:10px;padding:16px 18px;margin:14px 0;box-shadow:0 1px 2px rgba(0,0,0,.04)}\nbutton{background:#2563eb;color:#fff;border:0;border-radius:6px;padding:7px 14px;cursor:pointer;margin:2px;font-size:14px}\nbutton.danger{background:#dc2626} button.ghost{background:#e5e7eb;color:#1f2328}\ninput,select{padding:8px;border:1px solid #c9c9c9;border-radius:6px;margin:2px;font-size:14px;box-sizing:border-box}\ntable{border-collapse:collapse;width:100%;font-size:13px} td,th{border:1px solid #ececec;padding:6px 8px;text-align:left;vertical-align:top}\n.owner{display:inline-flex;align-items:center;gap:6px;border:1px solid #ddd;border-radius:8px;padding:5px 10px;margin:4px 6px 4px 0;background:#f6f8fa}\n.entry{border-bottom:1px solid #eee;padding:8px 0} .meta{color:#8a8a8a;font-size:12px}\n.err{color:#dc2626;margin-top:8px} .ok{color:#16a34a;margin-top:8px}\n#app{display:none} code{background:#f0f0f0;padding:1px 5px;border-radius:4px;font-size:12px}\n</style></head><body>\n<h1>元忆 · 用户查看平台 <span id=\"ver\" style=\"font-size:14px;color:#888\"></span></h1>\n<div id=\"lock\" class=\"card\">\n <p><b>输入主口令解锁</b>(口令只在本地内存派生,不落盘、不发送远端)。忘口令可在 CLI 用恢复钥匙重设:<code>yotta-memory reset-password --recovery-key <钥匙></code></p>\n <input type=\"password\" id=\"pw\" placeholder=\"主口令\" style=\"width:260px\">\n <button onclick=\"unlock()\">解锁</button>\n <div class=\"err\" id=\"lockerr\"></div>\n</div>\n<div id=\"app\">\n <div class=\"card\">\n <b>AI 列表</b>(✅=已授权可读自己私密,🔒=未授权)\n <div id=\"owners\" style=\"margin-top:8px\"></div>\n </div>\n <div class=\"card\">\n <b>记忆</b>\n <input id=\"q\" placeholder=\"搜索关键词\" style=\"width:220px\" onkeydown=\"if(event.key==='Enter')load()\">\n <button onclick=\"load()\">搜索</button>\n <button class=\"ghost\" onclick=\"doExport()\">导出 JSON</button>\n <button class=\"ghost\" onclick=\"showRk()\">显示恢复钥匙</button>\n <span id=\"rkout\" style=\"font-size:12px;color:#888;margin-left:8px\"></span>\n <div id=\"entries\" style=\"margin-top:10px\"></div>\n </div>\n <div class=\"card\">\n <b>重设口令</b><br>\n <input type=\"password\" id=\"cur\" placeholder=\"当前口令\">\n <input type=\"password\" id=\"np1\" placeholder=\"新口令\">\n <input type=\"password\" id=\"np2\" placeholder=\"确认新口令\">\n <button onclick=\"resetPw()\">重设</button>\n <span id=\"pwout\"></span>\n </div>\n</div>\n<script>\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,function(c){return{'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c];});}\nasync function api(p,b){try{const r=await fetch(p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})});return await r.json();}catch(e){return{error:String(e)};}}\nasync function boot(){const s=await api('/api/status');document.getElementById('ver').textContent='v'+(s.version||'');if(s.unlocked){showApp();}}\nfunction showApp(){document.getElementById('lock').style.display='none';document.getElementById('app').style.display='block';loadOwners();load();}\nasync function unlock(){const d=await api('/api/unlock',{password:document.getElementById('pw').value});if(d.error){document.getElementById('lockerr').textContent=d.error;return;}showApp();}\nasync function loadOwners(){const d=await api('/api/owners');const box=document.getElementById('owners');box.innerHTML='';if(!d.owners||!d.owners.length){box.innerHTML='(无 owner)';return;}\n for(const o of d.owners){const c=document.createElement('span');c.className='owner';c.innerHTML=esc(o.owner)+(o.authorized?' ✅':' 🔒')+' <button class=\"ghost\" data-a=\"'+esc(o.owner)+'\">授权</button><button class=\"danger\" data-r=\"'+esc(o.owner)+'\">吊销</button>';box.appendChild(c);}\n box.querySelectorAll('[data-a]').forEach(function(b){b.onclick=function(){api('/api/authorize',{owner:b.getAttribute('data-a')}).then(function(){loadOwners();});};});\n box.querySelectorAll('[data-r]').forEach(function(b){b.onclick=function(){api('/api/revoke',{owner:b.getAttribute('data-r')}).then(function(){loadOwners();});};});\n}\nasync function load(){const d=await api('/api/entries',{query:document.getElementById('q').value});const box=document.getElementById('entries');box.innerHTML='共 '+d.count+' 条';\n if(!d.entries)return;for(const e of d.entries){const div=document.createElement('div');div.className='entry';div.innerHTML='<b>['+esc(e.type)+'] '+esc(e.subject)+'</b><div>'+esc(e.statement)+'</div><div class=\"meta\">'+esc(e.file)+' · owner='+esc(e.owner||'-')+' · '+esc(e.updated||e.created||'')+'</div>';box.appendChild(div);}}\nasync function doExport(){const d=await api('/api/export');if(d.error){alert(d.error);return;}const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='yottamemory-view-export.json';a.click();}\nasync function showRk(){const d=await api('/api/recovery-key');document.getElementById('rkout').textContent=d.recoveryKey?('恢复钥匙: '+d.recoveryKey):(d.error||'');}\nasync function resetPw(){const np1=document.getElementById('np1').value,np2=document.getElementById('np2').value;if(np1!==np2){document.getElementById('pwout').innerHTML='<span class=\"err\">两次新口令不一致</span>';return;}\n const d=await api('/api/reset-password',{currentPassword:document.getElementById('cur').value,newPassword:np1});document.getElementById('pwout').innerHTML=d.error?('<span class=\"err\">'+esc(d.error)+'</span>'):('<span class=\"ok\">'+esc(d.text||'ok')+'</span>');}\nboot();\n</script></body></html>";
|
|
1912
|
+
|
|
615
1913
|
// ---- 命令包装(CLI 入口)----
|
|
616
|
-
function cmdInit(opts) {
|
|
617
|
-
const
|
|
1914
|
+
async function cmdInit(opts) {
|
|
1915
|
+
const root = opts.dir ? path.resolve(String(opts.dir)) : (opts.project ? projectRoot() : userRoot());
|
|
1916
|
+
const fresh = !fs.existsSync(path.join(root, PUBLIC_DIR)) && !fs.existsSync(path.join(root, PRIVATE_DIR)) && !fs.existsSync(path.join(root, INDEX_FILE));
|
|
1917
|
+
const wantEncrypt = opts.encrypt || (!opts.noEncrypt && fresh);
|
|
1918
|
+
let o = Object.assign({}, opts);
|
|
1919
|
+
if (wantEncrypt && !o.password && !process.env.YOTTA_MEMORY_PASS) {
|
|
1920
|
+
const pw = await promptPassword('主口令(启用加密,勿忘;忘口令可用恢复钥匙重设): ');
|
|
1921
|
+
if (!pw) { console.error('已取消。'); process.exit(2); }
|
|
1922
|
+
const pw2 = await promptPassword('再次输入确认: ');
|
|
1923
|
+
if (pw2 !== pw) { console.error('两次输入不一致。'); process.exit(2); }
|
|
1924
|
+
o.password = pw;
|
|
1925
|
+
}
|
|
1926
|
+
const r = initCore(o);
|
|
618
1927
|
console.log(r.text);
|
|
1928
|
+
if (r.error) process.exit(2);
|
|
619
1929
|
}
|
|
620
1930
|
function cmdRemember(type, subject, statement, opts) {
|
|
621
1931
|
const o = Object.assign({}, opts);
|
|
@@ -629,6 +1939,36 @@ function cmdRecall(query, opts) {
|
|
|
629
1939
|
console.log(r.text);
|
|
630
1940
|
if (r.exitCode) process.exit(r.exitCode);
|
|
631
1941
|
}
|
|
1942
|
+
function cmdFeedback(ref, opts) {
|
|
1943
|
+
const r = feedbackCore(ref, opts);
|
|
1944
|
+
console.log(r.text);
|
|
1945
|
+
if (r.exitCode) process.exit(r.exitCode);
|
|
1946
|
+
if (r.error) process.exit(2);
|
|
1947
|
+
}
|
|
1948
|
+
function cmdExplain(ref, opts) {
|
|
1949
|
+
const r = explainCore(ref, opts);
|
|
1950
|
+
console.log(r.text);
|
|
1951
|
+
if (r.exitCode) process.exit(r.exitCode);
|
|
1952
|
+
if (r.error) process.exit(2);
|
|
1953
|
+
}
|
|
1954
|
+
function cmdMaintain(opts) {
|
|
1955
|
+
if (opts.merge) {
|
|
1956
|
+
const parts = String(opts.merge).split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
|
1957
|
+
if (parts.length < 2) { console.error('--merge 需要两个文件,逗号分隔'); process.exit(2); }
|
|
1958
|
+
const r = mergeCore(parts[0], parts[1], opts);
|
|
1959
|
+
console.log(r.text);
|
|
1960
|
+
if (r.error) process.exit(2);
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
const r = maintainCore(opts);
|
|
1964
|
+
console.log(r.text);
|
|
1965
|
+
if (r.error) process.exit(2);
|
|
1966
|
+
}
|
|
1967
|
+
function cmdDistill(opts) {
|
|
1968
|
+
const r = distillCore(opts);
|
|
1969
|
+
console.log(r.text);
|
|
1970
|
+
if (r.error) process.exit(2);
|
|
1971
|
+
}
|
|
632
1972
|
function cmdForget(fileRef) {
|
|
633
1973
|
const r = forgetCore(fileRef);
|
|
634
1974
|
console.log(r.text);
|
|
@@ -648,17 +1988,23 @@ function cmdReindex() {
|
|
|
648
1988
|
}
|
|
649
1989
|
function collectAll(root) {
|
|
650
1990
|
const out = [];
|
|
1991
|
+
const skipped = {};
|
|
651
1992
|
for (const fp of collectEntryFiles(root)) {
|
|
652
|
-
const
|
|
653
|
-
|
|
1993
|
+
const owner = ownerFromPrivatePath(root, fp);
|
|
1994
|
+
if (isEncFile(fp) && !getOwnerKeyFor(root, owner)) { skipped[owner] = true; continue; }
|
|
1995
|
+
try {
|
|
1996
|
+
const e = readEntry(fp, root);
|
|
1997
|
+
out.push({ file: e.file, meta: e.meta });
|
|
1998
|
+
} catch (err) { if (owner) skipped[owner] = true; }
|
|
654
1999
|
}
|
|
655
|
-
return out;
|
|
2000
|
+
return { memories: out, skipped: Object.keys(skipped) };
|
|
656
2001
|
}
|
|
657
2002
|
function exportCore(root, outPath) {
|
|
658
|
-
const
|
|
2003
|
+
const res = collectAll(root);
|
|
2004
|
+
const data = { format: 'yottamemory', version: 2, exported: today(), memories: res.memories, skippedOwners: res.skipped };
|
|
659
2005
|
const target = outPath || path.join(root, 'yottamemory-export-' + today() + '.json');
|
|
660
2006
|
fs.writeFileSync(target, JSON.stringify(data, null, 2), 'utf8');
|
|
661
|
-
return { error: false, text: '已导出 ' + data.memories.length + ' 条记忆 -> ' + target };
|
|
2007
|
+
return { error: false, text: '已导出 ' + data.memories.length + ' 条记忆 -> ' + target + (res.skipped.length ? '(跳过无授权私密 owner: ' + res.skipped.join(', ') + ')' : '') };
|
|
662
2008
|
}
|
|
663
2009
|
function importCore(root, src) {
|
|
664
2010
|
if (!src) return { error: true, text: '请提供 JSON 文件路径' };
|
|
@@ -673,16 +2019,21 @@ function importCore(root, src) {
|
|
|
673
2019
|
catch (e) { return { error: true, text: 'JSON 解析失败: ' + (e && e.message ? e.message : String(e)) }; }
|
|
674
2020
|
if (!data.memories || !Array.isArray(data.memories)) return { error: true, text: 'JSON 格式不正确(缺少 memories 数组)' };
|
|
675
2021
|
ensureInit(root);
|
|
676
|
-
|
|
2022
|
+
const encrypted = isEncrypted(root);
|
|
2023
|
+
let m = 0, skippedPriv = 0;
|
|
677
2024
|
for (const item of data.memories) {
|
|
678
2025
|
const meta = item.meta || {};
|
|
679
2026
|
const t = (meta.type || 'FACT').toUpperCase();
|
|
680
2027
|
if (!TYPE_DIRS[t]) continue;
|
|
681
|
-
const
|
|
2028
|
+
const owner = meta.owner || '';
|
|
2029
|
+
const scope = meta.scope || defaultScope(t);
|
|
2030
|
+
if (encrypted && scope === 'private' && !getOwnerKeyFor(root, owner)) { skippedPriv++; continue; }
|
|
2031
|
+
const dir = path.join(root, typeSubdir(t, owner));
|
|
682
2032
|
fs.mkdirSync(dir, { recursive: true });
|
|
683
2033
|
let seq = nextSeq(dir);
|
|
684
|
-
|
|
685
|
-
|
|
2034
|
+
const suffix = (encrypted && scope === 'private') ? ENC_SUFFIX : '';
|
|
2035
|
+
let file = path.join(dir, today() + '-' + seq + '.md' + suffix);
|
|
2036
|
+
while (fs.existsSync(file)) { seq = String(parseInt(seq, 10) + 1).padStart(4, '0'); file = path.join(dir, today() + '-' + seq + '.md' + suffix); }
|
|
686
2037
|
const rec = {
|
|
687
2038
|
type: t,
|
|
688
2039
|
subject: meta.subject || '',
|
|
@@ -694,16 +2045,16 @@ function importCore(root, src) {
|
|
|
694
2045
|
immutable: meta.immutable === true || meta.immutable === 'true',
|
|
695
2046
|
source: meta.source || '',
|
|
696
2047
|
weight: (parseFloat(meta.weight) > 0 ? parseFloat(meta.weight) : 1.0),
|
|
697
|
-
scope:
|
|
698
|
-
owner:
|
|
2048
|
+
scope: scope,
|
|
2049
|
+
owner: owner,
|
|
699
2050
|
access_count: parseInt(meta.access_count || '0', 10) || 0,
|
|
700
2051
|
last_accessed: meta.last_accessed || '',
|
|
701
2052
|
};
|
|
702
|
-
|
|
2053
|
+
writeMemoryText(root, file, frontmatterToText(rec, rec.statement), owner);
|
|
703
2054
|
upsertIndexEntry(root, readEntry(file, root));
|
|
704
2055
|
m++;
|
|
705
2056
|
}
|
|
706
|
-
return { error: false, text: '已导入 ' + m + ' 条记忆 -> ' + root };
|
|
2057
|
+
return { error: false, text: '已导入 ' + m + ' 条记忆 -> ' + root + (skippedPriv ? '(跳过 ' + skippedPriv + ' 条无授权私密)' : '') };
|
|
707
2058
|
}
|
|
708
2059
|
function cmdExport(outPath) {
|
|
709
2060
|
const r = exportCore(userRoot(), outPath || 'yottamemory-export-' + today() + '.json');
|
|
@@ -777,9 +2128,12 @@ function findSelfProfile(root, agentId) {
|
|
|
777
2128
|
const dir = selfPrefsDir(root, agentId);
|
|
778
2129
|
if (!fs.existsSync(dir)) return '';
|
|
779
2130
|
for (const f of fs.readdirSync(dir)) {
|
|
2131
|
+
if (!/\.md(\.enc)?$/.test(f)) continue;
|
|
780
2132
|
const fp = path.join(dir, f);
|
|
781
2133
|
if (!fs.statSync(fp).isFile()) continue;
|
|
782
|
-
|
|
2134
|
+
let text;
|
|
2135
|
+
try { text = readMemoryText(root, fp, agentId); } catch (err) { continue; }
|
|
2136
|
+
const meta = parseFrontmatter(text).meta;
|
|
783
2137
|
if (meta.subject === SELF_PROFILE_SUBJECT && (meta.owner || '') === agentId) return f;
|
|
784
2138
|
}
|
|
785
2139
|
return '';
|
|
@@ -809,11 +2163,11 @@ function writeSelfProfile(root, agentId, extra) {
|
|
|
809
2163
|
extra = extra || {};
|
|
810
2164
|
const dir = selfPrefsDir(root, agentId);
|
|
811
2165
|
fs.mkdirSync(dir, { recursive: true });
|
|
2166
|
+
const encrypted = isEncrypted(root);
|
|
812
2167
|
const existing = findSelfProfile(root, agentId);
|
|
813
2168
|
if (existing) {
|
|
814
|
-
// 已有自我档案:合并 kv 字段更新(不重复建文件)
|
|
815
2169
|
const fp = path.join(dir, existing);
|
|
816
|
-
const parsed = parseFrontmatter(
|
|
2170
|
+
const parsed = parseFrontmatter(readMemoryText(root, fp, agentId));
|
|
817
2171
|
const kv = parseKvBody(parsed.body);
|
|
818
2172
|
kv.agent_id = agentId;
|
|
819
2173
|
kv.host = os.hostname();
|
|
@@ -828,12 +2182,12 @@ function writeSelfProfile(root, agentId, extra) {
|
|
|
828
2182
|
const meta = Object.assign({}, parsed.meta);
|
|
829
2183
|
meta.updated = today();
|
|
830
2184
|
meta.statement = body;
|
|
831
|
-
|
|
2185
|
+
writeMemoryText(root, fp, frontmatterToText(meta, body), agentId);
|
|
832
2186
|
upsertIndexEntry(root, readEntry(fp, root));
|
|
833
2187
|
return fp;
|
|
834
2188
|
}
|
|
835
2189
|
const seq = nextSeq(dir);
|
|
836
|
-
const file = path.join(dir, today() + '-' + seq + '.md');
|
|
2190
|
+
const file = path.join(dir, today() + '-' + seq + '.md' + (encrypted ? ENC_SUFFIX : ''));
|
|
837
2191
|
const body = selfProfileBody(agentId, root, extra);
|
|
838
2192
|
const rec = {
|
|
839
2193
|
type: 'PREF', subject: SELF_PROFILE_SUBJECT, statement: body,
|
|
@@ -841,7 +2195,7 @@ function writeSelfProfile(root, agentId, extra) {
|
|
|
841
2195
|
tags: ['自我档案'], immutable: false,
|
|
842
2196
|
scope: 'private', owner: agentId, access_count: 0, last_accessed: '',
|
|
843
2197
|
};
|
|
844
|
-
|
|
2198
|
+
writeMemoryText(root, file, frontmatterToText(rec, body), agentId);
|
|
845
2199
|
upsertIndexEntry(root, readEntry(file, root));
|
|
846
2200
|
return file;
|
|
847
2201
|
}
|
|
@@ -897,7 +2251,7 @@ function cmdWhoami() {
|
|
|
897
2251
|
console.log('登记状态: ' + reg);
|
|
898
2252
|
console.log('自我档案: ' + (profile ? profile : '未写入(请执行 yotta-memory iam ' + id + ')'));
|
|
899
2253
|
if (profile) {
|
|
900
|
-
const kv =
|
|
2254
|
+
const kv = selfProfileKv(root, id);
|
|
901
2255
|
if (kv.agent_name) console.log('显示名: ' + kv.agent_name);
|
|
902
2256
|
if (kv.user_name) console.log('用户: ' + kv.user_name);
|
|
903
2257
|
if (kv.relationship) console.log('关系: ' + kv.relationship);
|
|
@@ -913,9 +2267,11 @@ function profileGroups(root, owner) {
|
|
|
913
2267
|
const dir = path.join(root, PRIVATE_DIR, owner, t);
|
|
914
2268
|
if (!fs.existsSync(dir)) continue;
|
|
915
2269
|
for (const f of fs.readdirSync(dir)) {
|
|
2270
|
+
if (!/\.md(\.enc)?$/.test(f)) continue;
|
|
916
2271
|
const fp = path.join(dir, f);
|
|
917
2272
|
if (!fs.statSync(fp).isFile()) continue;
|
|
918
|
-
|
|
2273
|
+
let e;
|
|
2274
|
+
try { e = readEntry(fp, root); } catch (err) { continue; }
|
|
919
2275
|
const key = e.type + '\u0000' + (e.subject || '') + '\u0000' + e.tags.join(',');
|
|
920
2276
|
if (!map[key]) {
|
|
921
2277
|
map[key] = { type: e.type, subject: e.subject || '', tags: e.tags || [], items: [] };
|
|
@@ -940,6 +2296,9 @@ function profileCore(opts) {
|
|
|
940
2296
|
}
|
|
941
2297
|
const ownerDir = path.join(root, PRIVATE_DIR, owner);
|
|
942
2298
|
if (!fs.existsSync(ownerDir)) return { error: false, text: '该智能体(' + owner + ')暂无画像:private/' + owner + '/ 不存在或为空。' };
|
|
2299
|
+
if (isEncrypted(root) && !getOwnerKeyFor(root, owner)) {
|
|
2300
|
+
return { error: true, exitCode: 3, text: '私密区已加密:当前无 ' + owner + ' 的授权密钥,请在用户平台授权(yotta-memory view → 授权本智能体)后生成画像。' };
|
|
2301
|
+
}
|
|
943
2302
|
const groups = profileGroups(root, owner);
|
|
944
2303
|
if (!groups.length) return { error: false, text: '该智能体(' + owner + ')暂无画像:private/' + owner + '/ 下无 PREF / BOUND / COMMIT 条目。' };
|
|
945
2304
|
const lines = [];
|
|
@@ -959,8 +2318,7 @@ function profileCore(opts) {
|
|
|
959
2318
|
lines.push('');
|
|
960
2319
|
}
|
|
961
2320
|
const text = lines.join('\n');
|
|
962
|
-
const outFile =
|
|
963
|
-
fs.writeFileSync(outFile, text + '\n', 'utf8');
|
|
2321
|
+
const outFile = writeProfileText(root, owner, text + '\n');
|
|
964
2322
|
return { error: false, text: text + '\n\n[已写入] ' + outFile };
|
|
965
2323
|
}
|
|
966
2324
|
function cmdProfile(opts) {
|
|
@@ -974,7 +2332,9 @@ function cmdProfile(opts) {
|
|
|
974
2332
|
function selfProfileKv(root, id) {
|
|
975
2333
|
const profile = findSelfProfile(root, id);
|
|
976
2334
|
if (!profile) return {};
|
|
977
|
-
|
|
2335
|
+
let text;
|
|
2336
|
+
try { text = readMemoryText(root, path.join(selfPrefsDir(root, id), profile), id); } catch (e) { return {}; }
|
|
2337
|
+
return parseKvBody(parseFrontmatter(text).body);
|
|
978
2338
|
}
|
|
979
2339
|
function contextCore(opts) {
|
|
980
2340
|
opts = opts || {};
|
|
@@ -987,6 +2347,7 @@ function contextCore(opts) {
|
|
|
987
2347
|
const unsafe = !!opts.unsafe;
|
|
988
2348
|
const budget = opts.budget ? parseInt(opts.budget, 10) : 0;
|
|
989
2349
|
const lines = [];
|
|
2350
|
+
const FENCE = String.fromCharCode(96, 96, 96);
|
|
990
2351
|
function usedChars() { return lines.reduce(function (s, x) { return s + String(x).length + 1; }, 0); }
|
|
991
2352
|
lines.push('# 开工上下文包(yotta-memory context)');
|
|
992
2353
|
lines.push('');
|
|
@@ -1013,18 +2374,22 @@ function contextCore(opts) {
|
|
|
1013
2374
|
lines.push('## 2. 用户画像摘要');
|
|
1014
2375
|
lines.push('');
|
|
1015
2376
|
if (owner) {
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
lines.push('
|
|
2377
|
+
let pf = profilePathFor(root, owner);
|
|
2378
|
+
let pfText = null;
|
|
2379
|
+
if (fs.existsSync(pf)) { try { pfText = readProfileText(root, owner); } catch (e) { pfText = null; } }
|
|
2380
|
+
if (pfText !== null && pfText !== undefined) {
|
|
2381
|
+
lines.push(FENCE + 'markdown');
|
|
2382
|
+
lines.push(pfText.trim());
|
|
2383
|
+
lines.push(FENCE);
|
|
1021
2384
|
} else {
|
|
1022
2385
|
const pr = profileCore({ owner: owner, unsafe: unsafe, selfAgent: selfAgent });
|
|
2386
|
+
pf = profilePathFor(root, owner);
|
|
1023
2387
|
if (!pr.error && pr.text.indexOf('[已写入]') !== -1 && fs.existsSync(pf)) {
|
|
1024
2388
|
lines.push('(画像不存在,已自动生成一次,见下)');
|
|
1025
|
-
lines.push('
|
|
1026
|
-
|
|
1027
|
-
lines.push('
|
|
2389
|
+
lines.push(FENCE + 'markdown');
|
|
2390
|
+
let t2 = null; try { t2 = readProfileText(root, owner); } catch (e) {}
|
|
2391
|
+
lines.push((t2 === null || t2 === undefined) ? '' : t2.trim());
|
|
2392
|
+
lines.push(FENCE);
|
|
1028
2393
|
} else {
|
|
1029
2394
|
lines.push('(该身份暂无画像;可运行 yotta-memory profile 生成)');
|
|
1030
2395
|
}
|
|
@@ -1039,7 +2404,8 @@ function contextCore(opts) {
|
|
|
1039
2404
|
for (const r of roots) {
|
|
1040
2405
|
for (const e of ensureIndex(r)) {
|
|
1041
2406
|
if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
|
|
1042
|
-
|
|
2407
|
+
// v0.8.0 排序融合:0.5×importance(旧) + 0.5×utility(新盖棺分)
|
|
2408
|
+
recent.push({ e: e, s: 0.5 * importanceScore(e) + 0.5 * utilityScore(e) });
|
|
1043
2409
|
}
|
|
1044
2410
|
}
|
|
1045
2411
|
recent.sort(function (a, b) { return b.s - a.s; });
|
|
@@ -1076,6 +2442,21 @@ function contextCore(opts) {
|
|
|
1076
2442
|
}
|
|
1077
2443
|
if (!commits.length) lines.push('(无)');
|
|
1078
2444
|
for (const e of commits) lines.push('- ' + (e.subject || '承诺') + ': ' + e.statement);
|
|
2445
|
+
lines.push('');
|
|
2446
|
+
lines.push('## 6. 收工纪律');
|
|
2447
|
+
lines.push('');
|
|
2448
|
+
const allEntries = [];
|
|
2449
|
+
for (const r of roots) for (const e of ensureIndex(r)) allEntries.push(e);
|
|
2450
|
+
const myCommits = allEntries.filter(function (e) { return e.type === 'COMMIT' && classifyRead(e, owner, '', unsafe, selfAgent) !== 'denied'; });
|
|
2451
|
+
if (!myCommits.length) {
|
|
2452
|
+
lines.push('- 最近承诺: 无(收工请补 COMMIT / 交接锚点)');
|
|
2453
|
+
} else {
|
|
2454
|
+
const latest = myCommits.map(function (e) { return e.updated || e.created || ''; }).sort().pop();
|
|
2455
|
+
const d = daysBetween(latest, today());
|
|
2456
|
+
lines.push('- 最近承诺: ' + latest + (d > 7 ? '(超期 ' + d + ' 天,请补 COMMIT)' : ''));
|
|
2457
|
+
}
|
|
2458
|
+
const oldCount = allEntries.filter(function (e) { return e.created && daysBetween(e.created, today()) > 180 && classifyRead(e, owner, '', unsafe, selfAgent) !== 'denied'; }).length;
|
|
2459
|
+
if (oldCount > 0) lines.push('- 归档提醒: ' + oldCount + ' 条超 180 天,建议 archive');
|
|
1079
2460
|
return { error: false, exitCode: 0, text: lines.join('\n') };
|
|
1080
2461
|
}
|
|
1081
2462
|
function cmdContext(opts) {
|
|
@@ -1113,6 +2494,10 @@ function mcpTools() {
|
|
|
1113
2494
|
{ name: 'import', description: '从引擎主机上的 JSON 文件导入记忆。src 为文件路径(可绝对路径,或相对记忆库目录)', inputSchema: { type: 'object', properties: { src: { type: 'string' } }, required: ['src'] } },
|
|
1114
2495
|
{ name: 'agent_info', description: '查看当前智能体身份与登记状态(远端读经 token 校验的 X-Agent-Id;本机读 YOTTA_AGENT_ID)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
|
|
1115
2496
|
{ name: 'profile', description: '生成当前智能体的用户画像(只读聚合 private/<owner>/ 下 PREF/BOUND/COMMIT 原文,零推断,写入 private/<owner>/profile.md)。owner 默认当前智能体', inputSchema: { type: 'object', properties: { owner: { type: 'string' } } } },
|
|
2497
|
+
{ name: 'feedback', description: '显式使用反馈(自我学习):useful/useless 调整记忆 weight/confidence/feedback_net。file 为记忆文件路径或文件名', inputSchema: { type: 'object', properties: { file: { type: 'string' }, useful: { type: 'boolean' }, useless: { type: 'boolean' }, reason: { type: 'string' } }, required: ['file'] } },
|
|
2498
|
+
{ name: 'maintain', description: '记忆自组织(自我进化):规则层归档/遗忘/去重预览。默认 dry-run;apply 才执行,purge 才真删', inputSchema: { type: 'object', properties: { apply: { type: 'boolean' }, purge: { type: 'boolean' }, dedup: { type: 'boolean' }, threshold: { type: 'number' }, age: { type: 'number' } } } },
|
|
2499
|
+
{ name: 'distill', description: '心理日志蒸馏(自我提升):统计摘要/主题画像/知识地图。owner 默认当前智能体', inputSchema: { type: 'object', properties: { owner: { type: 'string' }, subject: { type: 'string' }, model: { type: 'string' } } } },
|
|
2500
|
+
{ name: 'explain', description: '解释单条记忆效用分项(为什么靠前/归档/遗忘)。file 为记忆文件路径或文件名', inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] } },
|
|
1116
2501
|
];
|
|
1117
2502
|
}
|
|
1118
2503
|
function callTool(name, args, ctx) {
|
|
@@ -1174,6 +2559,22 @@ function callTool(name, args, ctx) {
|
|
|
1174
2559
|
const r = profileCore({ owner: ownerArg || agent, selfAgent: agent });
|
|
1175
2560
|
return { text: r.text, error: r.error };
|
|
1176
2561
|
}
|
|
2562
|
+
if (name === 'feedback') {
|
|
2563
|
+
const r = feedbackCore(String(args.file || ''), { selfAgent: agent, useful: !!args.useful, useless: !!args.useless, reason: args.reason ? String(args.reason) : '' });
|
|
2564
|
+
return { text: r.text, error: r.error };
|
|
2565
|
+
}
|
|
2566
|
+
if (name === 'maintain') {
|
|
2567
|
+
const r = maintainCore({ selfAgent: agent, apply: !!args.apply, purge: !!args.purge, dedup: !!args.dedup, threshold: args.threshold, age: args.age });
|
|
2568
|
+
return { text: r.text, error: r.error };
|
|
2569
|
+
}
|
|
2570
|
+
if (name === 'distill') {
|
|
2571
|
+
const r = distillCore({ selfAgent: agent, owner: args.owner ? String(args.owner) : agent, subject: args.subject ? String(args.subject) : '', model: args.model ? String(args.model) : '' });
|
|
2572
|
+
return { text: r.text, error: r.error };
|
|
2573
|
+
}
|
|
2574
|
+
if (name === 'explain') {
|
|
2575
|
+
const r = explainCore(String(args.file || ''), { selfAgent: agent });
|
|
2576
|
+
return { text: r.text, error: r.error };
|
|
2577
|
+
}
|
|
1177
2578
|
return { text: '未知工具: ' + name, error: true };
|
|
1178
2579
|
} catch (e) {
|
|
1179
2580
|
return { text: '错误: ' + (e && e.message ? e.message : String(e)), error: true };
|
|
@@ -1260,7 +2661,7 @@ function cmdServe(opts) {
|
|
|
1260
2661
|
});
|
|
1261
2662
|
}
|
|
1262
2663
|
|
|
1263
|
-
// ---- stdio
|
|
2664
|
+
// ---- stdio 本地零进程模式(客户端按需拉起 CLI)----
|
|
1264
2665
|
function cmdServeStdio() {
|
|
1265
2666
|
const root = userRoot();
|
|
1266
2667
|
ensureInit(root);
|
|
@@ -1719,7 +3120,11 @@ function usage() {
|
|
|
1719
3120
|
'yotta-memory v' + VERSION + ' — 元忆:有权限边界的文件式智能体记忆',
|
|
1720
3121
|
'',
|
|
1721
3122
|
'用法:',
|
|
1722
|
-
|
|
3123
|
+
'yotta-memory init [--project] [--dir <目录>] [--encrypt|--no-encrypt] 初始化记忆库(新建默认加密:需主口令 + 恢复钥匙;--no-encrypt 降级明文)',
|
|
3124
|
+
'yotta-memory migrate 把明文私密区迁移为密文(需主口令;迁移后打印恢复钥匙)',
|
|
3125
|
+
'yotta-memory view [--port 8788] [--host 127.0.0.1] 启动用户查看平台(口令解锁浏览/搜索/导出全部 AI 记忆 + 授权/吊销 AI)',
|
|
3126
|
+
'yotta-memory reset-password [--password <当前> | --recovery-key <钥匙>] [--new-password <新>] 重设主口令(忘口令用恢复钥匙)',
|
|
3127
|
+
'yotta-memory key list | authorize <id> | revoke <id> 管理 AI 的私密读取授权缓存(authorize 需主口令)',
|
|
1723
3128
|
' yotta-memory config set memory_home <目录> 持久记住记忆库位置',
|
|
1724
3129
|
' yotta-memory config get 查看当前配置与生效位置',
|
|
1725
3130
|
' yotta-memory remember <type> <subject> <statement> [--owner <id>] [--source <来源>] [--weight <0..>] [--verify] [--no-hint] 写入记忆(--source 记录来源;--weight 重要性权重默认 1.0 去重取 max;--verify 写后回读校验;--no-hint 关闭类型启发式提示)',
|
|
@@ -1747,12 +3152,12 @@ function usage() {
|
|
|
1747
3152
|
'',
|
|
1748
3153
|
].join('\n'));
|
|
1749
3154
|
}
|
|
1750
|
-
function main() {
|
|
3155
|
+
async function main() {
|
|
1751
3156
|
const args = process.argv.slice(2);
|
|
1752
3157
|
if (!args.length) { usage(); return; }
|
|
1753
3158
|
const opts = {};
|
|
1754
3159
|
const positional = [];
|
|
1755
|
-
const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget']);
|
|
3160
|
+
const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--reason', '--merge', '--model', '--subject']);
|
|
1756
3161
|
for (let i = 0; i < args.length; i++) {
|
|
1757
3162
|
const a = args[i];
|
|
1758
3163
|
if (a === '--version' || a === '-v') { console.log(VERSION); return; }
|
|
@@ -1766,6 +3171,17 @@ function main() {
|
|
|
1766
3171
|
else if (a === '--force') opts.force = true;
|
|
1767
3172
|
else if (a === '--verify') opts.verify = true;
|
|
1768
3173
|
else if (a === '--no-hint') opts.noHint = true;
|
|
3174
|
+
else if (a === '--encrypt') opts.encrypt = true;
|
|
3175
|
+
else if (a === '--no-encrypt') opts.noEncrypt = true;
|
|
3176
|
+
else if (a === '--useful') opts.useful = true;
|
|
3177
|
+
else if (a === '--useless') opts.useless = true;
|
|
3178
|
+
else if (a === '--undo') opts.undo = true;
|
|
3179
|
+
else if (a === '--dry-run') opts.dryRun = true;
|
|
3180
|
+
else if (a === '--apply') opts.apply = true;
|
|
3181
|
+
else if (a === '--purge') opts.purge = true;
|
|
3182
|
+
else if (a === '--dedup') opts.dedup = true;
|
|
3183
|
+
else if (a === '--explain') opts.explain = true;
|
|
3184
|
+
else if (a === '--semantic') opts.semantic = true;
|
|
1769
3185
|
else if (valueOpts.has(a)) {
|
|
1770
3186
|
const v = args[++i];
|
|
1771
3187
|
if (a === '--type') opts.type = v;
|
|
@@ -1785,6 +3201,9 @@ function main() {
|
|
|
1785
3201
|
else if (a === '--source') opts.source = v;
|
|
1786
3202
|
else if (a === '--weight') opts.weight = parseFloat(v);
|
|
1787
3203
|
else if (a === '--budget') opts.budget = parseInt(v, 10) || 0;
|
|
3204
|
+
else if (a === '--password') opts.password = v;
|
|
3205
|
+
else if (a === '--new-password') opts.newPassword = v;
|
|
3206
|
+
else if (a === '--recovery-key') opts.recoveryKey = v;
|
|
1788
3207
|
} else if (a.startsWith('--')) {
|
|
1789
3208
|
console.error('未知选项: ' + a);
|
|
1790
3209
|
process.exit(2);
|
|
@@ -1795,7 +3214,7 @@ function main() {
|
|
|
1795
3214
|
const first = positional[0];
|
|
1796
3215
|
const rest = positional.slice(1);
|
|
1797
3216
|
switch (first) {
|
|
1798
|
-
case 'init': cmdInit(opts); break;
|
|
3217
|
+
case 'init': await cmdInit(opts); break;
|
|
1799
3218
|
case 'whoami': cmdWhoami(); break;
|
|
1800
3219
|
case 'iam': cmdIam(rest[0], opts); break;
|
|
1801
3220
|
case 'config': {
|
|
@@ -1807,6 +3226,11 @@ function main() {
|
|
|
1807
3226
|
}
|
|
1808
3227
|
case 'remember': cmdRemember(rest[0], rest[1], rest[2], opts); break;
|
|
1809
3228
|
case 'recall': cmdRecall(rest[0] || null, opts); break;
|
|
3229
|
+
case 'search': cmdRecall(rest[0] || null, opts); break;
|
|
3230
|
+
case 'feedback': cmdFeedback(rest[0], opts); break;
|
|
3231
|
+
case 'explain': cmdExplain(rest[0], opts); break;
|
|
3232
|
+
case 'maintain': cmdMaintain(opts); break;
|
|
3233
|
+
case 'distill': cmdDistill(opts); break;
|
|
1810
3234
|
case 'forget': cmdForget(rest[0]); break;
|
|
1811
3235
|
case 'archive': cmdArchive(opts); break;
|
|
1812
3236
|
case 'reindex': cmdReindex(); break;
|
|
@@ -1822,6 +3246,17 @@ function main() {
|
|
|
1822
3246
|
else { console.error('token 子命令: new --agent <id> / list / revoke --agent <id>'); process.exit(2); }
|
|
1823
3247
|
break;
|
|
1824
3248
|
}
|
|
3249
|
+
case 'migrate': await cmdMigrate(opts); break;
|
|
3250
|
+
case 'view': cmdView(opts); break;
|
|
3251
|
+
case 'reset-password': await cmdResetPassword(opts); break;
|
|
3252
|
+
case 'key': {
|
|
3253
|
+
const sub = rest[0];
|
|
3254
|
+
if (sub === 'list') cmdKeyList();
|
|
3255
|
+
else if (sub === 'authorize') await cmdKeyAuthorize(rest[1], opts);
|
|
3256
|
+
else if (sub === 'revoke') cmdKeyRevoke(rest[1]);
|
|
3257
|
+
else { console.error('key 子命令: list / authorize <id> / revoke <id>'); process.exit(2); }
|
|
3258
|
+
break;
|
|
3259
|
+
}
|
|
1825
3260
|
case 'serve': cmdServe(opts); break;
|
|
1826
3261
|
case 'lan': {
|
|
1827
3262
|
const sub = rest[0];
|
|
@@ -1839,7 +3274,7 @@ function main() {
|
|
|
1839
3274
|
}
|
|
1840
3275
|
}
|
|
1841
3276
|
|
|
1842
|
-
if (require.main === module) main();
|
|
3277
|
+
if (require.main === module) { main().catch(function (e) { console.error('错误: ' + (e && e.message ? e.message : String(e))); process.exit(2); }); }
|
|
1843
3278
|
module.exports = {
|
|
1844
3279
|
VERSION: VERSION,
|
|
1845
3280
|
userRoot: userRoot,
|
|
@@ -1903,5 +3338,48 @@ module.exports = {
|
|
|
1903
3338
|
cmdLanLinuxEnable: cmdLanLinuxEnable,
|
|
1904
3339
|
cmdLanLinuxDisable: cmdLanLinuxDisable,
|
|
1905
3340
|
cmdLanLinuxStatus: cmdLanLinuxStatus,
|
|
3341
|
+
feedbackCore: feedbackCore,
|
|
3342
|
+
explainCore: explainCore,
|
|
3343
|
+
maintainCore: maintainCore,
|
|
3344
|
+
mergeCore: mergeCore,
|
|
3345
|
+
distillCore: distillCore,
|
|
3346
|
+
utilityScore: utilityScore,
|
|
3347
|
+
utilityBreakdown: utilityBreakdown,
|
|
3348
|
+
semanticMatch: semanticMatch,
|
|
3349
|
+
pinyinTokens: pinyinTokens,
|
|
3350
|
+
isEncrypted: isEncrypted,
|
|
3351
|
+
collectOwners: collectOwners,
|
|
3352
|
+
hasPlaintextPrivate: hasPlaintextPrivate,
|
|
3353
|
+
initEncryptionCore: initEncryptionCore,
|
|
3354
|
+
deriveUmk: deriveUmk,
|
|
3355
|
+
loadSalt: loadSalt,
|
|
3356
|
+
wrapOwnerKey: wrapOwnerKey,
|
|
3357
|
+
unwrapOwnerKey: unwrapOwnerKey,
|
|
3358
|
+
unwrapOwnerKeyRecovery: unwrapOwnerKeyRecovery,
|
|
3359
|
+
unwrapRecoveryEnc: unwrapRecoveryEnc,
|
|
3360
|
+
writeOwnerKeyCache: writeOwnerKeyCache,
|
|
3361
|
+
revokeOwnerKeyCache: revokeOwnerKeyCache,
|
|
3362
|
+
loadOwnerKeyCache: loadOwnerKeyCache,
|
|
3363
|
+
getOwnerKeyFor: getOwnerKeyFor,
|
|
3364
|
+
encryptMemoryText: encryptMemoryText,
|
|
3365
|
+
decryptMemoryText: decryptMemoryText,
|
|
3366
|
+
readMemoryText: readMemoryText,
|
|
3367
|
+
writeMemoryText: writeMemoryText,
|
|
3368
|
+
loadOwnerIndex: loadOwnerIndex,
|
|
3369
|
+
saveOwnerIndex: saveOwnerIndex,
|
|
3370
|
+
ensureIndex: ensureIndex,
|
|
3371
|
+
loadIndex: loadIndex,
|
|
3372
|
+
ownerFromPrivatePath: ownerFromPrivatePath,
|
|
3373
|
+
isEncFile: isEncFile,
|
|
3374
|
+
profilePathFor: profilePathFor,
|
|
3375
|
+
readProfileText: readProfileText,
|
|
3376
|
+
writeProfileText: writeProfileText,
|
|
3377
|
+
migrateCore: migrateCore,
|
|
3378
|
+
resetPasswordCore: resetPasswordCore,
|
|
3379
|
+
keyAuthorizeCore: keyAuthorizeCore,
|
|
3380
|
+
keyRevokeCore: keyRevokeCore,
|
|
3381
|
+
promptPassword: promptPassword,
|
|
3382
|
+
initCore: initCore,
|
|
3383
|
+
viewServerCore: viewServerCore,
|
|
1906
3384
|
|
|
1907
3385
|
};
|