@yottameta/yotta-memory 0.6.4 → 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.
@@ -5,10 +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 新增:灵魂盘核心——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)
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
+ // 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)
12
15
  // v0.6.2 修复:remember --verify 写后回读改为直查索引 + 权限判定 + 召回匹配性(不再依赖 recall top-N 排序,消除泛化 subject 下偶发误报「回读未命中」)
13
16
  'use strict';
14
17
 
@@ -19,7 +22,7 @@ const crypto = require('crypto');
19
22
  const http = require('http');
20
23
  const child_process = require('child_process');
21
24
 
22
- const VERSION = '0.6.4';
25
+ const VERSION = '0.8.0';
23
26
  const TYPES = ['FACT', 'PREF', 'BOUND', 'COMMIT'];
24
27
  const TYPE_DIRS = { FACT: 'facts', PREF: 'prefs', BOUND: 'bounds', COMMIT: 'commits' };
25
28
  const PUBLIC_DIR = 'facts';
@@ -28,7 +31,7 @@ const PRIVATE_LEAF = ['prefs', 'bounds', 'commits'];
28
31
  const ARCHIVE_DIR = '.archive';
29
32
  const INDEX_FILE = 'index.json';
30
33
  const PRIVATE_TYPES = ['PREF', 'BOUND', 'COMMIT'];
31
- 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'];
32
35
  const CONFIG_FILE = 'config.json';
33
36
  const SERVER_SUBDIR = '.server';
34
37
  const TOKENS_FILE = 'tokens.json';
@@ -57,6 +60,19 @@ function userRoot() {
57
60
  function projectRoot() {
58
61
  return path.join(process.cwd(), '.yottamemory');
59
62
  }
63
+ // 唯一化记忆库根:projectRoot 与 userRoot 可能指向同一目录(如 cwd=home 或其父时),
64
+ // 若不唯一化,recall/context 等会对同一索引遍历两次 -> 同一条记忆重复展示(v0.6.5 修复)。
65
+ function memoryRoots() {
66
+ const out = [], seen = new Set();
67
+ for (const r of [projectRoot(), userRoot()]) {
68
+ const abs = path.resolve(r);
69
+ if (!fs.existsSync(abs)) continue;
70
+ if (seen.has(abs)) continue;
71
+ seen.add(abs);
72
+ out.push(abs);
73
+ }
74
+ return out;
75
+ }
60
76
  function today() {
61
77
  const d = new Date();
62
78
  const p = (n) => String(n).padStart(2, '0');
@@ -127,11 +143,197 @@ function tokenize(s) {
127
143
  return toks;
128
144
  }
129
145
  function buildTokens(subject, statement, tags) {
130
- const toks = tokenize(subject + ' ' + statement + ' ' + tags.join(' '));
131
146
  const m = {};
132
- for (const t of toks) m[t] = (m[t] || 0) + 1;
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);
133
157
  return m;
134
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
+ }
135
337
  function frontmatterToText(meta, body) {
136
338
  const lines = ['---'];
137
339
  const ordered = FIELD_ORDER.filter(function (k) { return meta[k] !== undefined && meta[k] !== null && meta[k] !== ''; });
@@ -159,33 +361,249 @@ function nextSeq(dir) {
159
361
  let max = 0;
160
362
  if (fs.existsSync(dir)) {
161
363
  for (const f of fs.readdirSync(dir)) {
162
- 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)?$/);
163
365
  if (m) max = Math.max(max, parseInt(m[1], 10));
164
366
  }
165
367
  }
166
368
  return String(max + 1).padStart(4, '0');
167
369
  }
168
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
+
169
582
  // ---- 索引(index.json)----
170
583
  function indexPath(root) { return path.join(root, INDEX_FILE); }
584
+ const INDEX_VERSION = 3;
171
585
  function loadIndex(root) {
172
586
  const p = indexPath(root);
173
587
  if (!fs.existsSync(p)) return null;
174
588
  try {
175
589
  const d = JSON.parse(fs.readFileSync(p, 'utf8'));
176
- if (d && Array.isArray(d.entries)) return 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
+ }
177
595
  } catch (e) { /* ignore */ }
178
596
  return null;
179
597
  }
180
598
  function getIndex(root) { return loadIndex(root) || []; }
181
599
  function saveIndex(root, entries) {
182
600
  const clean = entries.map(function (e) { const c = Object.assign({}, e); delete c.meta; return c; });
183
- fs.writeFileSync(indexPath(root), JSON.stringify({ version: 2, updated: today(), entries: clean }, null, 2), 'utf8');
601
+ fs.writeFileSync(indexPath(root), JSON.stringify({ version: INDEX_VERSION, updated: today(), entries: clean }, null, 2), 'utf8');
184
602
  }
185
603
  function collectEntryFiles(root) {
186
604
  const dirs = [];
187
605
  const facts = path.join(root, PUBLIC_DIR);
188
- if (fs.existsSync(facts)) dirs.push(path.join(root, PUBLIC_DIR));
606
+ if (fs.existsSync(facts)) dirs.push(facts);
189
607
  const pdir = path.join(root, PRIVATE_DIR);
190
608
  if (fs.existsSync(pdir)) {
191
609
  for (const id of fs.readdirSync(pdir)) {
@@ -200,6 +618,7 @@ function collectEntryFiles(root) {
200
618
  const out = [];
201
619
  for (const dir of dirs) {
202
620
  for (const f of fs.readdirSync(dir)) {
621
+ if (!/\.md(\.enc)?$/.test(f)) continue;
203
622
  const fp = path.join(dir, f);
204
623
  if (fs.statSync(fp).isFile()) out.push(fp);
205
624
  }
@@ -228,37 +647,113 @@ function migrateLayout(root) {
228
647
  }
229
648
  function buildIndex(root) {
230
649
  migrateLayout(root);
231
- const entries = [];
232
- for (const fp of collectEntryFiles(root)) entries.push(readEntry(fp, root));
233
- saveIndex(root, entries);
234
- return entries;
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;
235
672
  }
236
673
  function ensureIndex(root) {
237
- const idx = loadIndex(root);
238
- if (idx) return idx;
239
- return buildIndex(root);
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;
240
689
  }
241
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
+ }
242
701
  const idx = getIndex(root);
243
702
  const i = idx.findIndex(function (e) { return e.file === entry.file; });
244
703
  if (i >= 0) idx[i] = entry; else idx.push(entry);
245
704
  saveIndex(root, idx);
246
705
  }
247
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
+ }
248
716
  const idx = getIndex(root);
249
717
  const i = idx.findIndex(function (e) { return e.file === file; });
250
718
  if (i >= 0) { idx.splice(i, 1); saveIndex(root, idx); }
251
719
  }
252
720
  function removeIndexEntries(root, files) {
253
721
  const set = new Set(files);
254
- const idx = getIndex(root);
255
- const next = idx.filter(function (e) { return !set.has(e.file); });
256
- if (next.length !== idx.length) saveIndex(root, next);
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
+ }
257
743
  }
258
744
 
259
745
  // ---- 读取 / 写入记忆文件 ----
260
746
  function readEntry(fp, root) {
261
- const parsed = parseFrontmatter(fs.readFileSync(fp, 'utf8'));
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);
262
757
  const meta = parsed.meta;
263
758
  const type = (meta.type || 'FACT').toUpperCase();
264
759
  const subject = meta.subject || '';
@@ -280,27 +775,32 @@ function readEntry(fp, root) {
280
775
  immutable: meta.immutable === 'true',
281
776
  source: meta.source || '',
282
777
  weight: (parseFloat(meta.weight) > 0 ? parseFloat(meta.weight) : 1.0),
778
+ feedback_net: feedbackNetOf(meta),
283
779
  tokens: buildTokens(subject, statement, tags),
284
780
  meta: meta,
285
781
  };
286
782
  }
287
- function rewriteFrontmatter(fp, patch) {
288
- const txt = fs.readFileSync(fp, 'utf8');
289
- const parsed = parseFrontmatter(txt);
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);
290
788
  const meta = Object.assign({}, parsed.meta);
291
789
  for (const k of Object.keys(patch)) meta[k] = patch[k];
292
- fs.writeFileSync(fp, frontmatterToText(meta, parsed.body), 'utf8');
790
+ writeMemoryText(root || memoryRootForFile(fp), fp, frontmatterToText(meta, parsed.body), owner || ownerFromPrivatePath(root || memoryRootForFile(fp), fp));
293
791
  }
294
792
  function bumpReadMeta(root, relFiles) {
295
793
  const now = today();
296
794
  for (const rel of relFiles) {
297
795
  const fp = path.join(root, rel);
298
796
  if (!fs.existsSync(fp)) continue;
299
- const parsed = parseFrontmatter(fs.readFileSync(fp, 'utf8'));
797
+ const owner = ownerFromPrivatePath(root, fp);
798
+ let parsed;
799
+ try { parsed = parseFrontmatter(readMemoryText(root, fp, owner)); } catch (e) { continue; }
300
800
  const meta = parsed.meta;
301
801
  if (meta.immutable === 'true') continue;
302
802
  const acc = (parseInt(meta.access_count || '0', 10) || 0) + 1;
303
- rewriteFrontmatter(fp, { access_count: acc, last_accessed: now });
803
+ rewriteFrontmatter(fp, { access_count: acc, last_accessed: now }, root, owner);
304
804
  }
305
805
  }
306
806
  function touchIndex(root, relFiles) {
@@ -328,7 +828,7 @@ function vitality(meta) {
328
828
  if (last) { const d = daysBetween(last, today()); recency = Math.max(0, 1 - Math.floor(d / 30) * 0.2); }
329
829
  return 0.4 * conf + 0.3 * accScore + 0.3 * recency;
330
830
  }
331
- // 灵魂盘 importance 融合:confidence × (0.5 + recency) + updated 加分 + weight 乘子 + immutable 加分
831
+ // importance 融合:confidence × (0.5 + recency) + updated 加分 + weight 乘子 + immutable 加分
332
832
  function importanceScore(meta) {
333
833
  const conf = parseFloat(meta.confidence || 1.0);
334
834
  let ageDays = 999;
@@ -349,7 +849,7 @@ function loadGrants(root) {
349
849
  }
350
850
  function hasGrant(userAgent, ownerAgent) {
351
851
  if (!userAgent || !ownerAgent) return false;
352
- for (const root of [projectRoot(), userRoot()]) {
852
+ for (const root of memoryRoots()) {
353
853
  if (!fs.existsSync(root)) continue;
354
854
  const grants = loadGrants(root);
355
855
  const list = grants[userAgent];
@@ -390,9 +890,20 @@ function verifyWrittenReadable(root, rel, subj, agent) {
390
890
 
391
891
  // ---- 命令 core(CLI 与 MCP 共用;返回 { error, exitCode, text },不 process.exit)----
392
892
  function initCore(opts) {
893
+ opts = opts || {};
393
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));
394
896
  ensureInit(root);
395
- return { error: false, text: '已初始化记忆库: ' + root };
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 启用加密)') };
396
907
  }
397
908
  function rememberCore(type, subject, statement, opts) {
398
909
  opts = opts || {};
@@ -413,13 +924,18 @@ function rememberCore(type, subject, statement, opts) {
413
924
  if (scope === 'private' && owner && selfAgent && owner !== selfAgent && !opts.unsafe) {
414
925
  return { error: true, text: '拒绝: 当前声明身份 ' + selfAgent + ' 不能写入其它智能体 ' + owner + ' 的私密区。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。' };
415
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
+ }
416
931
  const dir = path.join(root, typeSubdir(t, owner));
417
932
  fs.mkdirSync(dir, { recursive: true });
418
933
  if (fs.existsSync(dir)) {
419
934
  for (const f of fs.readdirSync(dir)) {
420
935
  const fp = path.join(dir, f);
421
936
  if (!fs.statSync(fp).isFile()) continue;
422
- const parsed = parseFrontmatter(fs.readFileSync(fp, 'utf8'));
937
+ let parsed;
938
+ try { parsed = parseFrontmatter(readMemoryText(root, fp, owner)); } catch (err) { continue; }
423
939
  const meta = parsed.meta;
424
940
  if ((meta.type || '').toUpperCase() === t && meta.subject === subj && meta.statement === stmt) {
425
941
  const patch = { updated: today() };
@@ -427,7 +943,7 @@ function rememberCore(type, subject, statement, opts) {
427
943
  if (scope && !meta.scope) patch.scope = scope;
428
944
  if (opts.source && !meta.source) patch.source = opts.source;
429
945
  const w = parseFloat(opts.weight); if (w > 0) patch.weight = Math.max(parseFloat(meta.weight || '1.0') || 1.0, w);
430
- rewriteFrontmatter(fp, patch);
946
+ rewriteFrontmatter(fp, patch, root, owner);
431
947
  upsertIndexEntry(root, readEntry(fp, root));
432
948
  let text = '已更新: ' + fp;
433
949
  if (opts.hint !== false && t === 'FACT') {
@@ -444,7 +960,8 @@ function rememberCore(type, subject, statement, opts) {
444
960
  }
445
961
  }
446
962
  const seq = nextSeq(dir);
447
- const file = path.join(dir, today() + '-' + seq + '.md');
963
+ const suffix = (encrypted && scope === 'private') ? ENC_SUFFIX : '';
964
+ const file = path.join(dir, today() + '-' + seq + '.md' + suffix);
448
965
  const rec = {
449
966
  type: t, subject: subj, statement: stmt,
450
967
  confidence: 1.0, created: today(), updated: today(),
@@ -454,7 +971,7 @@ function rememberCore(type, subject, statement, opts) {
454
971
  weight: (parseFloat(opts.weight) > 0 ? parseFloat(opts.weight) : 1.0),
455
972
  access_count: 0, last_accessed: '',
456
973
  };
457
- fs.writeFileSync(file, frontmatterToText(rec, stmt), 'utf8');
974
+ writeMemoryText(root, file, frontmatterToText(rec, stmt), owner);
458
975
  upsertIndexEntry(root, readEntry(file, root));
459
976
  let text = '已记录: ' + file;
460
977
  if (opts.hint !== false && t === 'FACT') {
@@ -469,7 +986,8 @@ function rememberCore(type, subject, statement, opts) {
469
986
  return { error: false, text: text };
470
987
  }
471
988
  function recallCore(query, opts) {
472
- const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
989
+ opts = opts || {};
990
+ const roots = memoryRoots();
473
991
  if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
474
992
  const limit = opts.limit || 50;
475
993
  const onlyType = opts.type ? String(opts.type).toUpperCase() : null;
@@ -480,6 +998,8 @@ function recallCore(query, opts) {
480
998
  const allSafe = !!opts.unsafe;
481
999
  const impersonation = !!(opts.agent && selfAgent && opts.agent !== selfAgent);
482
1000
  const explicitCross = !!opts.all || impersonation || (!!ownerFilter && ownerFilter.toLowerCase() !== 'user' && ownerFilter !== agent);
1001
+ const wantExplain = !!opts.explain;
1002
+ const useSemantic = opts.semantic !== false;
483
1003
  const hits = [];
484
1004
  let deniedCount = 0;
485
1005
  for (const root of roots) {
@@ -489,27 +1009,60 @@ function recallCore(query, opts) {
489
1009
  const r = classifyRead(e, agent, ownerFilter, allSafe, selfAgent);
490
1010
  if (r === 'denied') { deniedCount++; continue; }
491
1011
  let score = 0;
1012
+ let detail = null;
492
1013
  if (q) {
493
- const qtoks = tokenize(q);
494
- for (const tt of qtoks) { if (e.tokens && e.tokens[tt]) score += e.tokens[tt]; }
495
- if (score === 0) {
496
- const hay = ((e.subject || '') + ' ' + (e.statement || '') + ' ' + e.tags.join(' ')).toLowerCase();
497
- if (hay.indexOf(q) !== -1) score = 1;
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
+ }
498
1025
  }
499
1026
  if (score === 0) continue;
500
1027
  } else {
501
1028
  score = 1;
502
1029
  }
503
- hits.push({ entry: e, score: score, root: root });
1030
+ hits.push({ entry: e, score: score, root: root, detail: detail });
504
1031
  }
505
1032
  }
506
- hits.sort(function (a, b) {
507
- if (b.score !== a.score) return b.score - a.score;
508
- const pa = a.root === projectRoot() ? 0 : 1;
509
- const pb = b.root === projectRoot() ? 0 : 1;
510
- if (pa !== pb) return pa - pb;
511
- return String(b.entry.created).localeCompare(String(a.entry.created));
512
- });
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
+ }
513
1066
  const shown = hits.slice(0, limit);
514
1067
  if (!shown.length) {
515
1068
  if (deniedCount > 0 && explicitCross) {
@@ -528,6 +1081,11 @@ function recallCore(query, opts) {
528
1081
  for (const h of shown) {
529
1082
  lines.push('[' + h.entry.type + '] ' + h.entry.subject + ': ' + h.entry.statement);
530
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
+ }
531
1089
  }
532
1090
  if (deniedCount > 0 && explicitCross) {
533
1091
  lines.push('\n[警告] 本次检索共拒绝 ' + deniedCount + ' 条越界访问(其它智能体私密记忆,未授权不展示)。如需读取请加 --unsafe 或 --owner user。');
@@ -551,7 +1109,7 @@ function relOf(root, fp) { return path.relative(root, fp).replace(/\\/g, '/'); }
551
1109
  function forgetCore(fileRef, opts) {
552
1110
  opts = opts || {};
553
1111
  const selfAgent = opts.selfAgent || currentAgent();
554
- const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
1112
+ const roots = memoryRoots();
555
1113
  const ref = String(fileRef || '').replace(/\\/g, '/');
556
1114
  let target = null, targetRoot = null, targetRel = null;
557
1115
  for (const root of roots) {
@@ -571,21 +1129,26 @@ function forgetCore(fileRef, opts) {
571
1129
  return { error: false, text: '已删除: ' + target };
572
1130
  }
573
1131
  function archiveCore(opts) {
574
- const days = opts.days || 180;
575
- const threshold = (opts.threshold !== undefined && opts.threshold !== null) ? opts.threshold : 0.4;
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));
576
1136
  const root = userRoot();
577
1137
  if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
578
1138
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
579
1139
  let moved = 0;
580
1140
  const movedFiles = [];
581
1141
  for (const fp of collectEntryFiles(root)) {
582
- const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
1142
+ const owner = ownerFromPrivatePath(root, fp);
1143
+ let meta;
1144
+ try { meta = parseFrontmatter(readMemoryText(root, fp, owner)).meta; } catch (e) { continue; }
583
1145
  if (meta.immutable === 'true') continue;
584
1146
  if (!meta.created) continue;
585
1147
  const createdTs = new Date(meta.created).getTime();
586
1148
  if (isNaN(createdTs)) continue;
587
1149
  const t = (meta.type || 'FACT').toUpperCase();
588
- if (vitality(meta) < threshold && createdTs < cutoff) {
1150
+ // v0.8.0 统一效用分(盖棺分)替代 vitality
1151
+ if (utilityScore(meta) < threshold && createdTs < cutoff) {
589
1152
  const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[t]);
590
1153
  fs.mkdirSync(destDir, { recursive: true });
591
1154
  fs.renameSync(fp, path.join(destDir, path.basename(fp)));
@@ -598,10 +1161,771 @@ function archiveCore(opts) {
598
1161
  return { error: false, text: '已归档 ' + moved + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR) };
599
1162
  }
600
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 &lt;钥匙&gt;</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{'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[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
+
601
1913
  // ---- 命令包装(CLI 入口)----
602
- function cmdInit(opts) {
603
- const r = initCore(opts);
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);
604
1927
  console.log(r.text);
1928
+ if (r.error) process.exit(2);
605
1929
  }
606
1930
  function cmdRemember(type, subject, statement, opts) {
607
1931
  const o = Object.assign({}, opts);
@@ -615,6 +1939,36 @@ function cmdRecall(query, opts) {
615
1939
  console.log(r.text);
616
1940
  if (r.exitCode) process.exit(r.exitCode);
617
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
+ }
618
1972
  function cmdForget(fileRef) {
619
1973
  const r = forgetCore(fileRef);
620
1974
  console.log(r.text);
@@ -625,7 +1979,7 @@ function cmdArchive(opts) {
625
1979
  console.log(r.text);
626
1980
  }
627
1981
  function cmdReindex() {
628
- const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
1982
+ const roots = memoryRoots();
629
1983
  if (!roots.length) { console.log('记忆库不存在。'); return; }
630
1984
  for (const root of roots) {
631
1985
  const n = buildIndex(root).length;
@@ -634,17 +1988,23 @@ function cmdReindex() {
634
1988
  }
635
1989
  function collectAll(root) {
636
1990
  const out = [];
1991
+ const skipped = {};
637
1992
  for (const fp of collectEntryFiles(root)) {
638
- const e = readEntry(fp, root);
639
- out.push({ file: e.file, meta: e.meta });
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; }
640
1999
  }
641
- return out;
2000
+ return { memories: out, skipped: Object.keys(skipped) };
642
2001
  }
643
2002
  function exportCore(root, outPath) {
644
- const data = { format: 'yottamemory', version: 1, exported: today(), memories: collectAll(root) };
2003
+ const res = collectAll(root);
2004
+ const data = { format: 'yottamemory', version: 2, exported: today(), memories: res.memories, skippedOwners: res.skipped };
645
2005
  const target = outPath || path.join(root, 'yottamemory-export-' + today() + '.json');
646
2006
  fs.writeFileSync(target, JSON.stringify(data, null, 2), 'utf8');
647
- return { error: false, text: '已导出 ' + data.memories.length + ' 条记忆 -> ' + target };
2007
+ return { error: false, text: '已导出 ' + data.memories.length + ' 条记忆 -> ' + target + (res.skipped.length ? '(跳过无授权私密 owner: ' + res.skipped.join(', ') + ')' : '') };
648
2008
  }
649
2009
  function importCore(root, src) {
650
2010
  if (!src) return { error: true, text: '请提供 JSON 文件路径' };
@@ -659,16 +2019,21 @@ function importCore(root, src) {
659
2019
  catch (e) { return { error: true, text: 'JSON 解析失败: ' + (e && e.message ? e.message : String(e)) }; }
660
2020
  if (!data.memories || !Array.isArray(data.memories)) return { error: true, text: 'JSON 格式不正确(缺少 memories 数组)' };
661
2021
  ensureInit(root);
662
- let m = 0;
2022
+ const encrypted = isEncrypted(root);
2023
+ let m = 0, skippedPriv = 0;
663
2024
  for (const item of data.memories) {
664
2025
  const meta = item.meta || {};
665
2026
  const t = (meta.type || 'FACT').toUpperCase();
666
2027
  if (!TYPE_DIRS[t]) continue;
667
- const dir = path.join(root, typeSubdir(t, meta.owner || ''));
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));
668
2032
  fs.mkdirSync(dir, { recursive: true });
669
2033
  let seq = nextSeq(dir);
670
- let file = path.join(dir, today() + '-' + seq + '.md');
671
- while (fs.existsSync(file)) { seq = String(parseInt(seq, 10) + 1).padStart(4, '0'); file = path.join(dir, today() + '-' + seq + '.md'); }
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); }
672
2037
  const rec = {
673
2038
  type: t,
674
2039
  subject: meta.subject || '',
@@ -680,16 +2045,16 @@ function importCore(root, src) {
680
2045
  immutable: meta.immutable === true || meta.immutable === 'true',
681
2046
  source: meta.source || '',
682
2047
  weight: (parseFloat(meta.weight) > 0 ? parseFloat(meta.weight) : 1.0),
683
- scope: meta.scope || defaultScope(t),
684
- owner: meta.owner || '',
2048
+ scope: scope,
2049
+ owner: owner,
685
2050
  access_count: parseInt(meta.access_count || '0', 10) || 0,
686
2051
  last_accessed: meta.last_accessed || '',
687
2052
  };
688
- fs.writeFileSync(file, frontmatterToText(rec, rec.statement), 'utf8');
2053
+ writeMemoryText(root, file, frontmatterToText(rec, rec.statement), owner);
689
2054
  upsertIndexEntry(root, readEntry(file, root));
690
2055
  m++;
691
2056
  }
692
- return { error: false, text: '已导入 ' + m + ' 条记忆 -> ' + root };
2057
+ return { error: false, text: '已导入 ' + m + ' 条记忆 -> ' + root + (skippedPriv ? '(跳过 ' + skippedPriv + ' 条无授权私密)' : '') };
693
2058
  }
694
2059
  function cmdExport(outPath) {
695
2060
  const r = exportCore(userRoot(), outPath || 'yottamemory-export-' + today() + '.json');
@@ -763,9 +2128,12 @@ function findSelfProfile(root, agentId) {
763
2128
  const dir = selfPrefsDir(root, agentId);
764
2129
  if (!fs.existsSync(dir)) return '';
765
2130
  for (const f of fs.readdirSync(dir)) {
2131
+ if (!/\.md(\.enc)?$/.test(f)) continue;
766
2132
  const fp = path.join(dir, f);
767
2133
  if (!fs.statSync(fp).isFile()) continue;
768
- const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
2134
+ let text;
2135
+ try { text = readMemoryText(root, fp, agentId); } catch (err) { continue; }
2136
+ const meta = parseFrontmatter(text).meta;
769
2137
  if (meta.subject === SELF_PROFILE_SUBJECT && (meta.owner || '') === agentId) return f;
770
2138
  }
771
2139
  return '';
@@ -795,11 +2163,11 @@ function writeSelfProfile(root, agentId, extra) {
795
2163
  extra = extra || {};
796
2164
  const dir = selfPrefsDir(root, agentId);
797
2165
  fs.mkdirSync(dir, { recursive: true });
2166
+ const encrypted = isEncrypted(root);
798
2167
  const existing = findSelfProfile(root, agentId);
799
2168
  if (existing) {
800
- // 已有自我档案:合并 kv 字段更新(不重复建文件)
801
2169
  const fp = path.join(dir, existing);
802
- const parsed = parseFrontmatter(fs.readFileSync(fp, 'utf8'));
2170
+ const parsed = parseFrontmatter(readMemoryText(root, fp, agentId));
803
2171
  const kv = parseKvBody(parsed.body);
804
2172
  kv.agent_id = agentId;
805
2173
  kv.host = os.hostname();
@@ -814,12 +2182,12 @@ function writeSelfProfile(root, agentId, extra) {
814
2182
  const meta = Object.assign({}, parsed.meta);
815
2183
  meta.updated = today();
816
2184
  meta.statement = body;
817
- fs.writeFileSync(fp, frontmatterToText(meta, body), 'utf8');
2185
+ writeMemoryText(root, fp, frontmatterToText(meta, body), agentId);
818
2186
  upsertIndexEntry(root, readEntry(fp, root));
819
2187
  return fp;
820
2188
  }
821
2189
  const seq = nextSeq(dir);
822
- const file = path.join(dir, today() + '-' + seq + '.md');
2190
+ const file = path.join(dir, today() + '-' + seq + '.md' + (encrypted ? ENC_SUFFIX : ''));
823
2191
  const body = selfProfileBody(agentId, root, extra);
824
2192
  const rec = {
825
2193
  type: 'PREF', subject: SELF_PROFILE_SUBJECT, statement: body,
@@ -827,7 +2195,7 @@ function writeSelfProfile(root, agentId, extra) {
827
2195
  tags: ['自我档案'], immutable: false,
828
2196
  scope: 'private', owner: agentId, access_count: 0, last_accessed: '',
829
2197
  };
830
- fs.writeFileSync(file, frontmatterToText(rec, body), 'utf8');
2198
+ writeMemoryText(root, file, frontmatterToText(rec, body), agentId);
831
2199
  upsertIndexEntry(root, readEntry(file, root));
832
2200
  return file;
833
2201
  }
@@ -883,7 +2251,7 @@ function cmdWhoami() {
883
2251
  console.log('登记状态: ' + reg);
884
2252
  console.log('自我档案: ' + (profile ? profile : '未写入(请执行 yotta-memory iam ' + id + ')'));
885
2253
  if (profile) {
886
- const kv = parseKvBody(parseFrontmatter(fs.readFileSync(path.join(selfPrefsDir(root, id), profile), 'utf8')).body);
2254
+ const kv = selfProfileKv(root, id);
887
2255
  if (kv.agent_name) console.log('显示名: ' + kv.agent_name);
888
2256
  if (kv.user_name) console.log('用户: ' + kv.user_name);
889
2257
  if (kv.relationship) console.log('关系: ' + kv.relationship);
@@ -899,9 +2267,11 @@ function profileGroups(root, owner) {
899
2267
  const dir = path.join(root, PRIVATE_DIR, owner, t);
900
2268
  if (!fs.existsSync(dir)) continue;
901
2269
  for (const f of fs.readdirSync(dir)) {
2270
+ if (!/\.md(\.enc)?$/.test(f)) continue;
902
2271
  const fp = path.join(dir, f);
903
2272
  if (!fs.statSync(fp).isFile()) continue;
904
- const e = readEntry(fp, root);
2273
+ let e;
2274
+ try { e = readEntry(fp, root); } catch (err) { continue; }
905
2275
  const key = e.type + '\u0000' + (e.subject || '') + '\u0000' + e.tags.join(',');
906
2276
  if (!map[key]) {
907
2277
  map[key] = { type: e.type, subject: e.subject || '', tags: e.tags || [], items: [] };
@@ -926,6 +2296,9 @@ function profileCore(opts) {
926
2296
  }
927
2297
  const ownerDir = path.join(root, PRIVATE_DIR, owner);
928
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
+ }
929
2302
  const groups = profileGroups(root, owner);
930
2303
  if (!groups.length) return { error: false, text: '该智能体(' + owner + ')暂无画像:private/' + owner + '/ 下无 PREF / BOUND / COMMIT 条目。' };
931
2304
  const lines = [];
@@ -945,8 +2318,7 @@ function profileCore(opts) {
945
2318
  lines.push('');
946
2319
  }
947
2320
  const text = lines.join('\n');
948
- const outFile = path.join(ownerDir, PROFILE_FILE);
949
- fs.writeFileSync(outFile, text + '\n', 'utf8');
2321
+ const outFile = writeProfileText(root, owner, text + '\n');
950
2322
  return { error: false, text: text + '\n\n[已写入] ' + outFile };
951
2323
  }
952
2324
  function cmdProfile(opts) {
@@ -960,11 +2332,13 @@ function cmdProfile(opts) {
960
2332
  function selfProfileKv(root, id) {
961
2333
  const profile = findSelfProfile(root, id);
962
2334
  if (!profile) return {};
963
- return parseKvBody(parseFrontmatter(fs.readFileSync(path.join(selfPrefsDir(root, id), profile), 'utf8')).body);
2335
+ let text;
2336
+ try { text = readMemoryText(root, path.join(selfPrefsDir(root, id), profile), id); } catch (e) { return {}; }
2337
+ return parseKvBody(parseFrontmatter(text).body);
964
2338
  }
965
2339
  function contextCore(opts) {
966
2340
  opts = opts || {};
967
- const roots = [projectRoot(), userRoot()].filter(function (r) { return fs.existsSync(r); });
2341
+ const roots = memoryRoots();
968
2342
  if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
969
2343
  const root = userRoot();
970
2344
  const limit = opts.limit || 10;
@@ -973,6 +2347,7 @@ function contextCore(opts) {
973
2347
  const unsafe = !!opts.unsafe;
974
2348
  const budget = opts.budget ? parseInt(opts.budget, 10) : 0;
975
2349
  const lines = [];
2350
+ const FENCE = String.fromCharCode(96, 96, 96);
976
2351
  function usedChars() { return lines.reduce(function (s, x) { return s + String(x).length + 1; }, 0); }
977
2352
  lines.push('# 开工上下文包(yotta-memory context)');
978
2353
  lines.push('');
@@ -999,18 +2374,22 @@ function contextCore(opts) {
999
2374
  lines.push('## 2. 用户画像摘要');
1000
2375
  lines.push('');
1001
2376
  if (owner) {
1002
- const pf = path.join(root, PRIVATE_DIR, owner, PROFILE_FILE);
1003
- if (fs.existsSync(pf)) {
1004
- lines.push('```markdown');
1005
- lines.push(fs.readFileSync(pf, 'utf8').trim());
1006
- 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);
1007
2384
  } else {
1008
2385
  const pr = profileCore({ owner: owner, unsafe: unsafe, selfAgent: selfAgent });
2386
+ pf = profilePathFor(root, owner);
1009
2387
  if (!pr.error && pr.text.indexOf('[已写入]') !== -1 && fs.existsSync(pf)) {
1010
2388
  lines.push('(画像不存在,已自动生成一次,见下)');
1011
- lines.push('```markdown');
1012
- lines.push(fs.readFileSync(pf, 'utf8').trim());
1013
- 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);
1014
2393
  } else {
1015
2394
  lines.push('(该身份暂无画像;可运行 yotta-memory profile 生成)');
1016
2395
  }
@@ -1025,7 +2404,8 @@ function contextCore(opts) {
1025
2404
  for (const r of roots) {
1026
2405
  for (const e of ensureIndex(r)) {
1027
2406
  if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
1028
- recent.push({ e: e, s: importanceScore(e) });
2407
+ // v0.8.0 排序融合:0.5×importance(旧) + 0.5×utility(新盖棺分)
2408
+ recent.push({ e: e, s: 0.5 * importanceScore(e) + 0.5 * utilityScore(e) });
1029
2409
  }
1030
2410
  }
1031
2411
  recent.sort(function (a, b) { return b.s - a.s; });
@@ -1062,6 +2442,21 @@ function contextCore(opts) {
1062
2442
  }
1063
2443
  if (!commits.length) lines.push('(无)');
1064
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');
1065
2460
  return { error: false, exitCode: 0, text: lines.join('\n') };
1066
2461
  }
1067
2462
  function cmdContext(opts) {
@@ -1099,6 +2494,10 @@ function mcpTools() {
1099
2494
  { name: 'import', description: '从引擎主机上的 JSON 文件导入记忆。src 为文件路径(可绝对路径,或相对记忆库目录)', inputSchema: { type: 'object', properties: { src: { type: 'string' } }, required: ['src'] } },
1100
2495
  { name: 'agent_info', description: '查看当前智能体身份与登记状态(远端读经 token 校验的 X-Agent-Id;本机读 YOTTA_AGENT_ID)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
1101
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'] } },
1102
2501
  ];
1103
2502
  }
1104
2503
  function callTool(name, args, ctx) {
@@ -1160,6 +2559,22 @@ function callTool(name, args, ctx) {
1160
2559
  const r = profileCore({ owner: ownerArg || agent, selfAgent: agent });
1161
2560
  return { text: r.text, error: r.error };
1162
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
+ }
1163
2578
  return { text: '未知工具: ' + name, error: true };
1164
2579
  } catch (e) {
1165
2580
  return { text: '错误: ' + (e && e.message ? e.message : String(e)), error: true };
@@ -1246,7 +2661,7 @@ function cmdServe(opts) {
1246
2661
  });
1247
2662
  }
1248
2663
 
1249
- // ---- stdio 本地零进程模式(照灵魂盘:客户端按需拉起 CLI)----
2664
+ // ---- stdio 本地零进程模式(客户端按需拉起 CLI)----
1250
2665
  function cmdServeStdio() {
1251
2666
  const root = userRoot();
1252
2667
  ensureInit(root);
@@ -1705,7 +3120,11 @@ function usage() {
1705
3120
  'yotta-memory v' + VERSION + ' — 元忆:有权限边界的文件式智能体记忆',
1706
3121
  '',
1707
3122
  '用法:',
1708
- ' yotta-memory init [--project] [--dir <目录>] 初始化记忆库(默认用户级;--dir 显式指定位置)',
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 需主口令)',
1709
3128
  ' yotta-memory config set memory_home <目录> 持久记住记忆库位置',
1710
3129
  ' yotta-memory config get 查看当前配置与生效位置',
1711
3130
  ' yotta-memory remember <type> <subject> <statement> [--owner <id>] [--source <来源>] [--weight <0..>] [--verify] [--no-hint] 写入记忆(--source 记录来源;--weight 重要性权重默认 1.0 去重取 max;--verify 写后回读校验;--no-hint 关闭类型启发式提示)',
@@ -1733,12 +3152,12 @@ function usage() {
1733
3152
  '',
1734
3153
  ].join('\n'));
1735
3154
  }
1736
- function main() {
3155
+ async function main() {
1737
3156
  const args = process.argv.slice(2);
1738
3157
  if (!args.length) { usage(); return; }
1739
3158
  const opts = {};
1740
3159
  const positional = [];
1741
- 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']);
1742
3161
  for (let i = 0; i < args.length; i++) {
1743
3162
  const a = args[i];
1744
3163
  if (a === '--version' || a === '-v') { console.log(VERSION); return; }
@@ -1752,6 +3171,17 @@ function main() {
1752
3171
  else if (a === '--force') opts.force = true;
1753
3172
  else if (a === '--verify') opts.verify = true;
1754
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;
1755
3185
  else if (valueOpts.has(a)) {
1756
3186
  const v = args[++i];
1757
3187
  if (a === '--type') opts.type = v;
@@ -1771,6 +3201,9 @@ function main() {
1771
3201
  else if (a === '--source') opts.source = v;
1772
3202
  else if (a === '--weight') opts.weight = parseFloat(v);
1773
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;
1774
3207
  } else if (a.startsWith('--')) {
1775
3208
  console.error('未知选项: ' + a);
1776
3209
  process.exit(2);
@@ -1781,7 +3214,7 @@ function main() {
1781
3214
  const first = positional[0];
1782
3215
  const rest = positional.slice(1);
1783
3216
  switch (first) {
1784
- case 'init': cmdInit(opts); break;
3217
+ case 'init': await cmdInit(opts); break;
1785
3218
  case 'whoami': cmdWhoami(); break;
1786
3219
  case 'iam': cmdIam(rest[0], opts); break;
1787
3220
  case 'config': {
@@ -1793,6 +3226,11 @@ function main() {
1793
3226
  }
1794
3227
  case 'remember': cmdRemember(rest[0], rest[1], rest[2], opts); break;
1795
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;
1796
3234
  case 'forget': cmdForget(rest[0]); break;
1797
3235
  case 'archive': cmdArchive(opts); break;
1798
3236
  case 'reindex': cmdReindex(); break;
@@ -1808,6 +3246,17 @@ function main() {
1808
3246
  else { console.error('token 子命令: new --agent <id> / list / revoke --agent <id>'); process.exit(2); }
1809
3247
  break;
1810
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
+ }
1811
3260
  case 'serve': cmdServe(opts); break;
1812
3261
  case 'lan': {
1813
3262
  const sub = rest[0];
@@ -1825,11 +3274,12 @@ function main() {
1825
3274
  }
1826
3275
  }
1827
3276
 
1828
- 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); }); }
1829
3278
  module.exports = {
1830
3279
  VERSION: VERSION,
1831
3280
  userRoot: userRoot,
1832
3281
  projectRoot: projectRoot,
3282
+ memoryRoots: memoryRoots,
1833
3283
  rememberCore: rememberCore,
1834
3284
  recallCore: recallCore,
1835
3285
  profileCore: profileCore,
@@ -1888,5 +3338,48 @@ module.exports = {
1888
3338
  cmdLanLinuxEnable: cmdLanLinuxEnable,
1889
3339
  cmdLanLinuxDisable: cmdLanLinuxDisable,
1890
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,
1891
3384
 
1892
3385
  };