@yottameta/yotta-memory 0.6.5 → 0.8.1

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,11 +5,13 @@
5
5
  // v0.4.2 新增:iam/whoami 智能体身份自注册(agents.json 唯一性)+ 自我档案 PREF + 私密记忆须声明 owner
6
6
  // v0.5.3 新增:CLI 选项可前置(--agent 等允许放在子命令前,不再误报"未知命令")/ lan enable 成功补"服务不会立刻启动"提示(v0.5.2 lan 引号修复无回归)
7
7
  // v0.5.4 新增:lan enable 在非管理员(schtasks Access denied)时自动降级为用户级 Startup 静默自启(VBS sh.Run 窗口0 + autostart.cmd,node 路径 process.execPath 自动探测);lan disable/status 同时管理计划任务与 Startup 双机制
8
- // v0.6.0 新增:灵魂盘核心——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
12
  // v0.6.5 修复:recall/context 对同一文件显示 2 条——projectRoot 与 userRoot 指向同一目录(如 cwd=home 或其父)时同一索引被遍历两次;新增 memoryRoots() 唯一化根,hasGrant/recallCore/forgetCore/cmdReindex/contextCore 统一走 memoryRoots()
13
+ // v0.7.0 新增:私密区机制级加密(AES-256-GCM 信封加密 + PBKDF2 主密钥 + 恢复钥匙)/ 每 owner 加密索引 / 用户查看平台 yotta-memory view / migrate 迁移 / reset-password / key 授权 / context 收工纪律 / init 新建默认加密(--no-encrypt 降级)
14
+ // v0.8.0 新增:自我学习/自我进化/自我提升——语义检索(同义词/拼音/字段加权/模糊 + 可选 embedding 插件协议预留)/ feedback 显式反馈闭环(weight/confidence/feedback_net 演化)/ maintain 规则层自组织(统一效用分 utility + 归档/遗忘/去重,默认 dry-run)/ distill 心理日志蒸馏(启发式统计/主题画像/知识地图 + 可选 --model)
13
15
  // v0.6.2 修复:remember --verify 写后回读改为直查索引 + 权限判定 + 召回匹配性(不再依赖 recall top-N 排序,消除泛化 subject 下偶发误报「回读未命中」)
14
16
  'use strict';
15
17
 
@@ -20,7 +22,7 @@ const crypto = require('crypto');
20
22
  const http = require('http');
21
23
  const child_process = require('child_process');
22
24
 
23
- const VERSION = '0.6.5';
25
+ const VERSION = '0.8.1';
24
26
  const TYPES = ['FACT', 'PREF', 'BOUND', 'COMMIT'];
25
27
  const TYPE_DIRS = { FACT: 'facts', PREF: 'prefs', BOUND: 'bounds', COMMIT: 'commits' };
26
28
  const PUBLIC_DIR = 'facts';
@@ -29,7 +31,7 @@ const PRIVATE_LEAF = ['prefs', 'bounds', 'commits'];
29
31
  const ARCHIVE_DIR = '.archive';
30
32
  const INDEX_FILE = 'index.json';
31
33
  const PRIVATE_TYPES = ['PREF', 'BOUND', 'COMMIT'];
32
- const FIELD_ORDER = ['type', 'subject', 'statement', 'confidence', 'created', 'updated', 'tags', 'immutable', 'scope', 'owner', 'source', 'weight', 'access_count', 'last_accessed'];
34
+ const FIELD_ORDER = ['type', 'subject', 'statement', 'confidence', 'created', 'updated', 'tags', 'immutable', 'scope', 'owner', 'source', 'weight', 'access_count', 'last_accessed', 'feedback_net'];
33
35
  const CONFIG_FILE = 'config.json';
34
36
  const SERVER_SUBDIR = '.server';
35
37
  const TOKENS_FILE = 'tokens.json';
@@ -141,11 +143,197 @@ function tokenize(s) {
141
143
  return toks;
142
144
  }
143
145
  function buildTokens(subject, statement, tags) {
144
- const toks = tokenize(subject + ' ' + statement + ' ' + tags.join(' '));
145
146
  const m = {};
146
- 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);
147
157
  return m;
148
158
  }
159
+ // ---- v0.8.0 自我学习:拼音表 + 同义词表 + 语义匹配(零依赖内置数据)----
160
+ function loadPinyinTable() {
161
+ try {
162
+ const p = path.join(__dirname, '..', 'references', 'pinyin-common.json');
163
+ return JSON.parse(fs.readFileSync(p, 'utf8')) || {};
164
+ } catch (e) { return {}; }
165
+ }
166
+ const PINYIN_TABLE = loadPinyinTable();
167
+ // 同义词组:同组内互相视为同义(查询侧扩展,命中按 0.5 权重)
168
+ const SYNONYM_GROUPS = [
169
+ ['记', '记住', '记忆', '记录', '铭记'],
170
+ ['忘', '忘记', '遗忘', '忘掉'],
171
+ ['读', '读取', '检索', '查询', '搜索'],
172
+ ['写', '写入', '记录', '保存', '存储'],
173
+ ['偏好', '喜好', '喜欢', '偏爱'],
174
+ ['承诺', '保证', '约定'],
175
+ ['收工', '结束', '完成', '完毕'],
176
+ ['归档', '封存', '存档'],
177
+ ['删除', '移除', '去掉'],
178
+ ['更新', '修改', '刷新'],
179
+ ['边界', '红线', '禁区', '界限'],
180
+ ['画像', '档案', 'profile'],
181
+ ['命令', '指令', 'cli'],
182
+ ['记忆', 'memory', '记忆库']
183
+ ];
184
+ function synonymSet(token) {
185
+ const out = new Set();
186
+ for (const g of SYNONYM_GROUPS) {
187
+ if (g.indexOf(token) !== -1) { for (const w of g) out.add(w); }
188
+ }
189
+ return out;
190
+ }
191
+ function pinyinOf(ch) {
192
+ const arr = PINYIN_TABLE[ch];
193
+ if (!arr || !arr.length) return null;
194
+ return arr[0];
195
+ }
196
+ // 中文字符串 -> 拼音 token:全拼 bigram 'py:yueyong' + 首字母 bigram 'pyi:yy'
197
+ function pinyinTokens(s) {
198
+ const out = [];
199
+ const han = String(s || '').match(/[\u4e00-\u9fa5]+/g);
200
+ if (!han) return out;
201
+ const all = han.join('');
202
+ if (all.length === 1) {
203
+ const p = pinyinOf(all);
204
+ if (p) { out.push('py:' + p); out.push('pyi:' + p[0]); }
205
+ return out;
206
+ }
207
+ let initials = '';
208
+ for (let i = 0; i < all.length; i++) { const p = pinyinOf(all[i]); if (p) initials += p[0]; }
209
+ for (let i = 0; i < all.length - 1; i++) {
210
+ const p1 = pinyinOf(all[i]), p2 = pinyinOf(all[i + 1]);
211
+ if (p1 && p2) out.push('py:' + p1 + p2);
212
+ }
213
+ for (let i = 0; i < initials.length - 1; i++) out.push('pyi:' + initials.slice(i, i + 2));
214
+ return out;
215
+ }
216
+ function editDistance(a, b) {
217
+ a = String(a); b = String(b);
218
+ if (a === b) return 0;
219
+ const la = a.length, lb = b.length;
220
+ if (!la) return lb; if (!lb) return la;
221
+ if (Math.abs(la - lb) > 2) return 3;
222
+ let prev = [];
223
+ for (let j = 0; j <= lb; j++) prev[j] = j;
224
+ for (let i = 1; i <= la; i++) {
225
+ const cur = [i];
226
+ for (let j = 1; j <= lb; j++) {
227
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
228
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
229
+ }
230
+ prev = cur;
231
+ }
232
+ return prev[lb];
233
+ }
234
+ // 语义匹配:精确(1.0×,字段加权已入 tokens) / 同义(0.5×) / 拼音(0.4×) / 子串兜底 / 模糊(0.3×)
235
+ function semanticMatch(entry, query, wantDetail) {
236
+ const q = String(query || '').toLowerCase();
237
+ const qtoks = tokenize(q);
238
+ const qpy = pinyinTokens(q);
239
+ const toks = entry.tokens || {};
240
+ const hay = ((entry.subject || '') + ' ' + (entry.statement || '') + ' ' + (entry.tags || []).join(' ')).toLowerCase();
241
+ let score = 0;
242
+ const detail = [];
243
+ const qset = new Set(qtoks);
244
+ for (const tt of qtoks) {
245
+ if (toks[tt]) { score += toks[tt]; if (wantDetail) detail.push('精确:' + tt + '(+' + round3(toks[tt]) + ')'); }
246
+ }
247
+ const ext = new Set();
248
+ for (const tt of qtoks) { const s = synonymSet(tt); for (const w of s) ext.add(w); }
249
+ for (const w of ext) {
250
+ if (toks[w] && !qset.has(w)) { score += 0.5 * toks[w]; if (wantDetail) detail.push('同义:' + w + '(+' + round3(0.5 * toks[w]) + ')'); }
251
+ }
252
+ for (const t of qpy) {
253
+ if (toks[t]) { score += 0.4 * toks[t]; if (wantDetail) detail.push('拼音:' + t + '(+' + round3(0.4 * toks[t]) + ')'); }
254
+ }
255
+ // 查询为拼音(拉丁 token)时,匹配条目侧 py:/pyi: token(去前缀)
256
+ for (const tt of qtoks) {
257
+ if (/^[a-z]+$/.test(tt)) {
258
+ if (toks['py:' + tt]) { score += 0.4 * toks['py:' + tt]; if (wantDetail) detail.push('拼音:' + tt + '(+' + round3(0.4 * toks['py:' + tt]) + ')'); }
259
+ if (toks['pyi:' + tt]) { score += 0.4 * toks['pyi:' + tt]; if (wantDetail) detail.push('拼音首字母:' + tt + '(+' + round3(0.4 * toks['pyi:' + tt]) + ')'); }
260
+ // 首字母串(如 yyyd)拆 bigram 匹配 pyi: token
261
+ if (tt.length >= 3) {
262
+ for (let i = 0; i < tt.length - 1; i++) {
263
+ const bg = tt.slice(i, i + 2);
264
+ if (toks['pyi:' + bg]) { score += 0.4 * toks['pyi:' + bg]; if (wantDetail) detail.push('拼音首字母:' + bg + '(+' + round3(0.4 * toks['pyi:' + bg]) + ')'); }
265
+ }
266
+ }
267
+ }
268
+ }
269
+ if (score === 0 && hay.indexOf(q) !== -1) { score = 1; if (wantDetail) detail.push('子串命中'); }
270
+ if (score === 0) {
271
+ const longQtoks = qtoks.filter(function (t) { return t.length >= 4; });
272
+ const keys = Object.keys(toks).filter(function (k) { return k.length >= 4 && k.indexOf(':') === -1; });
273
+ outer:
274
+ for (const t of longQtoks) {
275
+ for (const k of keys) {
276
+ if (Math.abs(k.length - t.length) <= 2 && editDistance(k, t) <= 2) {
277
+ score += 0.3 * toks[k];
278
+ if (wantDetail) detail.push('模糊:' + t + '~' + k + '(+' + round3(0.3 * toks[k]) + ')');
279
+ break outer;
280
+ }
281
+ }
282
+ }
283
+ }
284
+ return { score: score, detail: detail };
285
+ }
286
+ function round3(n) { return Math.round(n * 1000) / 1000; }
287
+ // ---- v0.8.0 自我进化:统一效用分(盖棺分)----
288
+ function feedbackNetOf(meta) { return parseInt(meta.feedback_net || '0', 10) || 0; }
289
+ function utilityScore(meta) {
290
+ const conf = Math.min(Math.max(parseFloat(meta.confidence || 1.0) || 1.0, 0), 1);
291
+ const acc = parseInt(meta.access_count || '0', 10) || 0;
292
+ const fb = feedbackNetOf(meta);
293
+ const usageScore = 0.5 * (Math.min(acc, 20) / 20) + 0.5 * (fb / (1 + Math.abs(fb)));
294
+ const last = meta.last_accessed || meta.created || '';
295
+ let recencyScore = 0;
296
+ if (last) {
297
+ const d = daysBetween(last, today());
298
+ recencyScore = Math.max(0, 1 - d / 365);
299
+ }
300
+ const typeScore = ({ BOUND: 1.0, COMMIT: 0.9, PREF: 0.8, FACT: 0.6 })[String(meta.type || 'FACT').toUpperCase()] || 0.6;
301
+ let structureScore = 0;
302
+ if (meta.source) structureScore += 0.2;
303
+ const tags = parseTags(meta.tags);
304
+ if (tags.length) structureScore += 0.3;
305
+ if (meta.subject) structureScore += 0.2;
306
+ if (meta.confidence !== undefined && meta.confidence !== null && meta.confidence !== '') structureScore += 0.1;
307
+ structureScore = Math.min(structureScore, 1.0);
308
+ const w = parseFloat(meta.weight || '1.0');
309
+ const weight = (w > 0 ? w : 1.0);
310
+ return ((0.30 * conf) + (0.25 * usageScore) + (0.20 * recencyScore) + (0.15 * typeScore) + (0.10 * structureScore)) * weight;
311
+ }
312
+ // utility 分项明细(explain / 审计用)
313
+ function utilityBreakdown(meta) {
314
+ const conf = Math.min(Math.max(parseFloat(meta.confidence || 1.0) || 1.0, 0), 1);
315
+ const acc = parseInt(meta.access_count || '0', 10) || 0;
316
+ const fb = feedbackNetOf(meta);
317
+ const usageScore = 0.5 * (Math.min(acc, 20) / 20) + 0.5 * (fb / (1 + Math.abs(fb)));
318
+ const last = meta.last_accessed || meta.created || '';
319
+ let recencyScore = 0;
320
+ if (last) { const d = daysBetween(last, today()); recencyScore = Math.max(0, 1 - d / 365); }
321
+ const typeScore = ({ BOUND: 1.0, COMMIT: 0.9, PREF: 0.8, FACT: 0.6 })[String(meta.type || 'FACT').toUpperCase()] || 0.6;
322
+ let structureScore = 0;
323
+ if (meta.source) structureScore += 0.2;
324
+ const tags = parseTags(meta.tags);
325
+ if (tags.length) structureScore += 0.3;
326
+ if (meta.subject) structureScore += 0.2;
327
+ if (meta.confidence !== undefined && meta.confidence !== null && meta.confidence !== '') structureScore += 0.1;
328
+ structureScore = Math.min(structureScore, 1.0);
329
+ const w = parseFloat(meta.weight || '1.0');
330
+ const weight = (w > 0 ? w : 1.0);
331
+ return {
332
+ confidence: round3(0.30 * conf), usage: round3(0.25 * usageScore), recency: round3(0.20 * recencyScore),
333
+ type: round3(0.15 * typeScore), structure: round3(0.10 * structureScore), weight: round3(weight),
334
+ total: round3(((0.30 * conf) + (0.25 * usageScore) + (0.20 * recencyScore) + (0.15 * typeScore) + (0.10 * structureScore)) * weight)
335
+ };
336
+ }
149
337
  function frontmatterToText(meta, body) {
150
338
  const lines = ['---'];
151
339
  const ordered = FIELD_ORDER.filter(function (k) { return meta[k] !== undefined && meta[k] !== null && meta[k] !== ''; });
@@ -173,33 +361,300 @@ function nextSeq(dir) {
173
361
  let max = 0;
174
362
  if (fs.existsSync(dir)) {
175
363
  for (const f of fs.readdirSync(dir)) {
176
- const m = f.match(/^\d{4}-\d{2}-\d{2}-(\d{4})\.md$/);
364
+ const m = f.match(/^\d{4}-\d{2}-\d{2}-(\d{4})\.md(\.enc)?$/);
177
365
  if (m) max = Math.max(max, parseInt(m[1], 10));
178
366
  }
179
367
  }
180
368
  return String(max + 1).padStart(4, '0');
181
369
  }
182
370
 
371
+
372
+ // ================= v0.7.0 加密(AES-256-GCM 信封加密 + PBKDF2 主密钥 + 恢复钥匙)=================
373
+ const ENC_MAGIC = 'YTMENC1';
374
+ const IDX_MAGIC = 'YTMIDX1';
375
+ const KEY_MAGIC = 'YTMKEY1';
376
+ const KEYS_DIR = 'keys';
377
+ const KEY_CACHE_DIR = 'cache';
378
+ const ENC_INDEX_FILE = 'index.enc';
379
+ const ENC_SUFFIX = '.enc';
380
+ const PBKDF2_ITER = 600000;
381
+ const GCM_NONCE = 12;
382
+ const GCM_TAG = 16;
383
+ const AES_ALGO = 'aes-256-gcm';
384
+
385
+ function encKeysDir(root) { return path.join(root, KEYS_DIR); }
386
+ function encSaltPath(root) { return path.join(encKeysDir(root), 'salt'); }
387
+ function encOwnerKeyPath(root, id) { return path.join(encKeysDir(root), id + '.key.enc'); }
388
+ function encOwnerRecPath(root, id) { return path.join(encKeysDir(root), id + '.key.recovery'); }
389
+ function encRecoveryEncPath(root) { return path.join(encKeysDir(root), 'recovery.key.enc'); }
390
+ function encCacheDir(root) { return path.join(encKeysDir(root), KEY_CACHE_DIR); }
391
+ function encCachePath(root, id) { return path.join(encCacheDir(root), id + '.key'); }
392
+ function isEncrypted(root) { return fs.existsSync(encKeysDir(root)); }
393
+ function isEncFile(fp) { return String(fp).slice(-ENC_SUFFIX.length) === ENC_SUFFIX; }
394
+ function ownerFromPrivatePath(root, fp) {
395
+ const rel = path.relative(root, fp).replace(/\\/g, '/');
396
+ const seg = rel.split('/');
397
+ if (seg[0] === PRIVATE_DIR && seg[1]) return seg[1];
398
+ return '';
399
+ }
400
+ function collectOwners(root) {
401
+ const pdir = path.join(root, PRIVATE_DIR);
402
+ if (!fs.existsSync(pdir)) return [];
403
+ return fs.readdirSync(pdir).filter(function (id) {
404
+ try { return fs.statSync(path.join(pdir, id)).isDirectory(); } catch (e) { return false; }
405
+ });
406
+ }
407
+ function ensureKeysDir(root) { fs.mkdirSync(encCacheDir(root), { recursive: true }); }
408
+ // owner 来源联合:private/<owner>/ 目录 + keys/*.key.enc(只授权未写记忆时 owner 只在 keys/)
409
+ function keyOwners(root) {
410
+ const set = new Set();
411
+ for (const o of collectOwners(root)) set.add(o);
412
+ const kd = encKeysDir(root);
413
+ if (fs.existsSync(kd)) {
414
+ for (const f of fs.readdirSync(kd)) {
415
+ if (f === 'recovery.key.enc') continue;
416
+ const m = /^(.+)\.key\.enc$/.exec(f);
417
+ if (m) set.add(m[1]);
418
+ }
419
+ }
420
+ return Array.from(set);
421
+ }
422
+ function aesEncryptBytes(key, buf, aad) {
423
+ const nonce = crypto.randomBytes(GCM_NONCE);
424
+ const c = crypto.createCipheriv(AES_ALGO, key, nonce);
425
+ if (aad) c.setAAD(aad);
426
+ const ct = Buffer.concat([c.update(buf), c.final()]);
427
+ return Buffer.concat([nonce, c.getAuthTag(), ct]);
428
+ }
429
+ function aesDecryptBytes(key, env, aad) {
430
+ if (!env || env.length < GCM_NONCE + GCM_TAG) throw new Error('密文数据损坏');
431
+ const nonce = env.slice(0, GCM_NONCE);
432
+ const tag = env.slice(GCM_NONCE, GCM_NONCE + GCM_TAG);
433
+ const ct = env.slice(GCM_NONCE + GCM_TAG);
434
+ const d = crypto.createDecipheriv(AES_ALGO, key, nonce);
435
+ if (aad) d.setAAD(aad);
436
+ d.setAuthTag(tag);
437
+ return Buffer.concat([d.update(ct), d.final()]);
438
+ }
439
+ function deriveUmk(password, salt) {
440
+ return crypto.pbkdf2Sync(Buffer.from(String(password), 'utf8'), salt, PBKDF2_ITER, 32, 'sha256');
441
+ }
442
+ function loadSalt(root) {
443
+ const p = encSaltPath(root);
444
+ if (!fs.existsSync(p)) return null;
445
+ return fs.readFileSync(p);
446
+ }
447
+ function initEncryptionCore(root, password, recoveryKeyIn) {
448
+ ensureKeysDir(root);
449
+ const salt = crypto.randomBytes(16);
450
+ fs.writeFileSync(encSaltPath(root), salt);
451
+ const umk = deriveUmk(password, salt);
452
+ const rk = recoveryKeyIn ? Buffer.from(String(recoveryKeyIn), 'base64') : crypto.randomBytes(32);
453
+ if (rk.length !== 32) throw new Error('恢复钥匙须为 32 字节(base64)');
454
+ fs.writeFileSync(encRecoveryEncPath(root), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(umk, rk, Buffer.from('recovery', 'utf8'))]));
455
+ return { umk: umk, rk: rk, salt: salt };
456
+ }
457
+ function unwrapRecoveryEnc(root, umk) {
458
+ const p = encRecoveryEncPath(root);
459
+ if (!fs.existsSync(p)) return null;
460
+ const buf = fs.readFileSync(p);
461
+ if (buf.slice(0, KEY_MAGIC.length).toString('utf8') !== KEY_MAGIC) throw new Error('恢复钥匙文件格式错误');
462
+ return aesDecryptBytes(umk, buf.slice(KEY_MAGIC.length), Buffer.from('recovery', 'utf8'));
463
+ }
464
+ function wrapOwnerKey(root, umk, rk, owner) {
465
+ const ownerKey = crypto.randomBytes(32);
466
+ const aad = Buffer.from('owner:' + owner, 'utf8');
467
+ fs.writeFileSync(encOwnerKeyPath(root, owner), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(umk, ownerKey, aad)]));
468
+ fs.writeFileSync(encOwnerRecPath(root, owner), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(rk, ownerKey, aad)]));
469
+ return ownerKey;
470
+ }
471
+ function unwrapOwnerKey(root, owner, umk) {
472
+ const p = encOwnerKeyPath(root, owner);
473
+ if (!fs.existsSync(p)) return null;
474
+ const buf = fs.readFileSync(p);
475
+ if (buf.slice(0, KEY_MAGIC.length).toString('utf8') !== KEY_MAGIC) throw new Error('密钥文件格式错误: ' + p);
476
+ return aesDecryptBytes(umk, buf.slice(KEY_MAGIC.length), Buffer.from('owner:' + owner, 'utf8'));
477
+ }
478
+ function unwrapOwnerKeyRecovery(root, owner, rk) {
479
+ const p = encOwnerRecPath(root, owner);
480
+ if (!fs.existsSync(p)) return null;
481
+ const buf = fs.readFileSync(p);
482
+ if (buf.slice(0, KEY_MAGIC.length).toString('utf8') !== KEY_MAGIC) throw new Error('恢复密钥文件格式错误: ' + p);
483
+ return aesDecryptBytes(rk, buf.slice(KEY_MAGIC.length), Buffer.from('owner:' + owner, 'utf8'));
484
+ }
485
+ function writeOwnerKeyCache(root, owner, ownerKey) {
486
+ ensureKeysDir(root);
487
+ const p = encCachePath(root, owner);
488
+ fs.writeFileSync(p, ownerKey);
489
+ try { fs.chmodSync(p, 0o600); } catch (e) {}
490
+ }
491
+ function loadOwnerKeyCache(root, owner) {
492
+ const p = encCachePath(root, owner);
493
+ if (!fs.existsSync(p)) return null;
494
+ return fs.readFileSync(p);
495
+ }
496
+ function revokeOwnerKeyCache(root, owner) {
497
+ const p = encCachePath(root, owner);
498
+ if (fs.existsSync(p)) { fs.unlinkSync(p); return true; }
499
+ return false;
500
+ }
501
+ function getOwnerKeyFor(root, owner) {
502
+ if (!owner) return null;
503
+ if (!isEncrypted(root)) return null;
504
+ return loadOwnerKeyCache(root, owner);
505
+ }
506
+ function encryptMemoryText(text, ownerKey) {
507
+ const fileKey = crypto.randomBytes(32);
508
+ const wk = aesEncryptBytes(ownerKey, fileKey, Buffer.from('filekey', 'utf8'));
509
+ const data = aesEncryptBytes(fileKey, Buffer.from(text, 'utf8'), Buffer.from('file', 'utf8'));
510
+ return Buffer.concat([Buffer.from(ENC_MAGIC, 'utf8'), wk, data]);
511
+ }
512
+ function decryptMemoryText(buf, ownerKey) {
513
+ if (buf.slice(0, ENC_MAGIC.length).toString('utf8') !== ENC_MAGIC) throw new Error('非密文文件(缺 ' + ENC_MAGIC + ' 头)');
514
+ const env = buf.slice(ENC_MAGIC.length);
515
+ const wkEnv = env.slice(0, GCM_NONCE + GCM_TAG + 32);
516
+ const fileKey = aesDecryptBytes(ownerKey, wkEnv, Buffer.from('filekey', 'utf8'));
517
+ const dataEnv = env.slice(GCM_NONCE + GCM_TAG + 32);
518
+ return aesDecryptBytes(fileKey, dataEnv, Buffer.from('file', 'utf8')).toString('utf8');
519
+ }
520
+ function readMemoryText(root, fp, owner) {
521
+ if (!isEncFile(fp)) return fs.readFileSync(fp, 'utf8');
522
+ const key = getOwnerKeyFor(root, owner);
523
+ if (!key) throw new Error('私密区已加密:当前无 ' + owner + ' 的授权密钥,请在用户平台授权(yotta-memory view → 授权本智能体)。');
524
+ return decryptMemoryText(fs.readFileSync(fp), key);
525
+ }
526
+ function writeMemoryText(root, fp, text, owner) {
527
+ if (!isEncFile(fp)) { fs.writeFileSync(fp, text, 'utf8'); return; }
528
+ const key = getOwnerKeyFor(root, owner);
529
+ if (!key) throw new Error('私密区已加密:当前无 ' + owner + ' 的授权密钥,请在用户平台授权(yotta-memory view → 授权本智能体)。');
530
+ fs.writeFileSync(fp, encryptMemoryText(text, key));
531
+ }
532
+ function ownerIndexPath(root, owner) { return path.join(root, PRIVATE_DIR, owner, ENC_INDEX_FILE); }
533
+ function loadOwnerIndex(root, owner, ownerKey) {
534
+ const p = ownerIndexPath(root, owner);
535
+ if (!fs.existsSync(p)) return [];
536
+ const buf = fs.readFileSync(p);
537
+ if (buf.slice(0, IDX_MAGIC.length).toString('utf8') !== IDX_MAGIC) return [];
538
+ const d = aesDecryptBytes(ownerKey, buf.slice(IDX_MAGIC.length), Buffer.from('index:' + owner, 'utf8'));
539
+ const j = JSON.parse(d.toString('utf8'));
540
+ return Array.isArray(j.entries) ? j.entries : [];
541
+ }
542
+ function saveOwnerIndex(root, owner, ownerKey, entries) {
543
+ const data = JSON.stringify({ version: 1, updated: today(), entries: entries }, null, 2);
544
+ const env = aesEncryptBytes(ownerKey, Buffer.from(data, 'utf8'), Buffer.from('index:' + owner, 'utf8'));
545
+ const dir = path.join(root, PRIVATE_DIR, owner);
546
+ fs.mkdirSync(dir, { recursive: true });
547
+ fs.writeFileSync(path.join(dir, ENC_INDEX_FILE), Buffer.concat([Buffer.from(IDX_MAGIC, 'utf8'), env]));
548
+ }
549
+ function hasPlaintextPrivate(root) {
550
+ for (const owner of collectOwners(root)) {
551
+ for (const t of PRIVATE_LEAF) {
552
+ const d = path.join(root, PRIVATE_DIR, owner, t);
553
+ if (!fs.existsSync(d)) continue;
554
+ for (const f of fs.readdirSync(d)) if (f.endsWith('.md')) return true;
555
+ }
556
+ if (fs.existsSync(path.join(root, PRIVATE_DIR, owner, PROFILE_FILE))) return true;
557
+ }
558
+ return false;
559
+ }
560
+ function profilePathFor(root, owner) {
561
+ const base = path.join(root, PRIVATE_DIR, owner, PROFILE_FILE);
562
+ if (fs.existsSync(base)) return base;
563
+ return base + ENC_SUFFIX;
564
+ }
565
+ function readProfileText(root, owner) {
566
+ const p = profilePathFor(root, owner);
567
+ if (!fs.existsSync(p)) return null;
568
+ return readMemoryText(root, p, owner);
569
+ }
570
+ function writeProfileText(root, owner, text) {
571
+ const dir = path.join(root, PRIVATE_DIR, owner);
572
+ fs.mkdirSync(dir, { recursive: true });
573
+ const encrypted = isEncrypted(root);
574
+ const p = encrypted ? path.join(dir, PROFILE_FILE + ENC_SUFFIX) : path.join(dir, PROFILE_FILE);
575
+ writeMemoryText(root, p, text, owner);
576
+ if (encrypted && fs.existsSync(path.join(dir, PROFILE_FILE))) {
577
+ try { fs.unlinkSync(path.join(dir, PROFILE_FILE)); } catch (e) {}
578
+ }
579
+ return p;
580
+ }
581
+
183
582
  // ---- 索引(index.json)----
184
583
  function indexPath(root) { return path.join(root, INDEX_FILE); }
584
+ const INDEX_VERSION = 4;
585
+ // v0.8.1 超大库索引分片:公共 index.json 超过阈值按年份分片(index-<year>.json),避免单文件膨胀。
586
+ const INDEX_SHARD_THRESHOLD = 5000;
587
+ // 安全校验:分片文件名必须匹配 index-YYYY.json,防 manifest 篡改导致路径穿越。
588
+ const SHARD_RE = /^index-\d{4}\.json$/;
589
+ function isSafeShardName(name) { return typeof name === 'string' && SHARD_RE.test(name); }
590
+ function indexShardName(year) { return 'index-' + year + '.json'; }
591
+ function loadIndexManifest(root) {
592
+ const p = indexPath(root);
593
+ if (!fs.existsSync(p)) return null;
594
+ try {
595
+ const d = JSON.parse(fs.readFileSync(p, 'utf8'));
596
+ if (d && Array.isArray(d.shards)) return d;
597
+ } catch (e) { /* ignore */ }
598
+ return null;
599
+ }
185
600
  function loadIndex(root) {
186
601
  const p = indexPath(root);
187
602
  if (!fs.existsSync(p)) return null;
188
603
  try {
189
604
  const d = JSON.parse(fs.readFileSync(p, 'utf8'));
190
- if (d && Array.isArray(d.entries)) return d.entries;
605
+ if (!d) return null;
606
+ if (Array.isArray(d.entries)) {
607
+ // v0.8.0 语义索引(字段加权 + 拼音 token)version=3;v0.8.1 分片 version=4;旧版索引返回 null 触发重建
608
+ if (d.version && d.version >= INDEX_VERSION) return d.entries;
609
+ return null;
610
+ }
611
+ if (Array.isArray(d.shards)) {
612
+ // v0.8.1 分片 manifest:串联各分片条目
613
+ if (d.version && d.version < INDEX_VERSION) return null;
614
+ const out = [];
615
+ for (const sh of d.shards) {
616
+ if (!isSafeShardName(sh)) continue;
617
+ const sp = path.join(root, sh);
618
+ if (!fs.existsSync(sp)) continue;
619
+ const sd = JSON.parse(fs.readFileSync(sp, 'utf8'));
620
+ if (sd && Array.isArray(sd.entries)) out.push.apply(out, sd.entries);
621
+ }
622
+ return out;
623
+ }
191
624
  } catch (e) { /* ignore */ }
192
625
  return null;
193
626
  }
194
627
  function getIndex(root) { return loadIndex(root) || []; }
195
628
  function saveIndex(root, entries) {
196
629
  const clean = entries.map(function (e) { const c = Object.assign({}, e); delete c.meta; return c; });
197
- fs.writeFileSync(indexPath(root), JSON.stringify({ version: 2, updated: today(), entries: clean }, null, 2), 'utf8');
630
+ const old = loadIndexManifest(root);
631
+ if (clean.length > INDEX_SHARD_THRESHOLD) {
632
+ const byYear = {};
633
+ for (const e of clean) {
634
+ const y = String(e.created || '').slice(0, 4) || String(today()).slice(0, 4);
635
+ (byYear[y] = byYear[y] || []).push(e);
636
+ }
637
+ const newShards = [];
638
+ for (const y of Object.keys(byYear).sort()) {
639
+ const name = indexShardName(y);
640
+ fs.writeFileSync(path.join(root, name), JSON.stringify({ version: INDEX_VERSION, year: (parseInt(y, 10) || 0), entries: byYear[y] }, null, 2), 'utf8');
641
+ newShards.push(name);
642
+ }
643
+ fs.writeFileSync(indexPath(root), JSON.stringify({ version: INDEX_VERSION, updated: today(), shards: newShards, count: clean.length }, null, 2), 'utf8');
644
+ if (old && Array.isArray(old.shards)) {
645
+ for (const sh of old.shards) if (newShards.indexOf(sh) === -1 && isSafeShardName(sh)) { try { fs.unlinkSync(path.join(root, sh)); } catch (e) {} }
646
+ }
647
+ } else {
648
+ if (old && Array.isArray(old.shards)) {
649
+ for (const sh of old.shards) { try { fs.unlinkSync(path.join(root, sh)); } catch (e) {} }
650
+ }
651
+ fs.writeFileSync(indexPath(root), JSON.stringify({ version: INDEX_VERSION, updated: today(), entries: clean }, null, 2), 'utf8');
652
+ }
198
653
  }
199
654
  function collectEntryFiles(root) {
200
655
  const dirs = [];
201
656
  const facts = path.join(root, PUBLIC_DIR);
202
- if (fs.existsSync(facts)) dirs.push(path.join(root, PUBLIC_DIR));
657
+ if (fs.existsSync(facts)) dirs.push(facts);
203
658
  const pdir = path.join(root, PRIVATE_DIR);
204
659
  if (fs.existsSync(pdir)) {
205
660
  for (const id of fs.readdirSync(pdir)) {
@@ -214,6 +669,7 @@ function collectEntryFiles(root) {
214
669
  const out = [];
215
670
  for (const dir of dirs) {
216
671
  for (const f of fs.readdirSync(dir)) {
672
+ if (!/\.md(\.enc)?$/.test(f)) continue;
217
673
  const fp = path.join(dir, f);
218
674
  if (fs.statSync(fp).isFile()) out.push(fp);
219
675
  }
@@ -242,37 +698,113 @@ function migrateLayout(root) {
242
698
  }
243
699
  function buildIndex(root) {
244
700
  migrateLayout(root);
245
- const entries = [];
246
- for (const fp of collectEntryFiles(root)) entries.push(readEntry(fp, root));
247
- saveIndex(root, entries);
248
- return entries;
701
+ const publicEntries = [];
702
+ const privateByOwner = {};
703
+ for (const fp of collectEntryFiles(root)) {
704
+ const rel = path.relative(root, fp).replace(/\\/g, '/');
705
+ if (rel.indexOf(PRIVATE_DIR + '/') === 0) {
706
+ const owner = rel.split('/')[1] || '';
707
+ if (!getOwnerKeyFor(root, owner)) continue;
708
+ let e;
709
+ try { e = readEntry(fp, root); } catch (err) { continue; }
710
+ (privateByOwner[owner] = privateByOwner[owner] || []).push(e);
711
+ } else {
712
+ try { publicEntries.push(readEntry(fp, root)); } catch (err) { /* ignore */ }
713
+ }
714
+ }
715
+ saveIndex(root, publicEntries);
716
+ for (const owner of Object.keys(privateByOwner)) {
717
+ const key = getOwnerKeyFor(root, owner);
718
+ if (key) saveOwnerIndex(root, owner, key, privateByOwner[owner]);
719
+ }
720
+ const all = publicEntries.slice();
721
+ for (const owner of Object.keys(privateByOwner)) all.push.apply(all, privateByOwner[owner]);
722
+ return all;
249
723
  }
250
724
  function ensureIndex(root) {
251
- const idx = loadIndex(root);
252
- if (idx) return idx;
253
- return buildIndex(root);
725
+ if (!isEncrypted(root)) {
726
+ const idx = loadIndex(root);
727
+ return idx || buildIndex(root);
728
+ }
729
+ const all = [];
730
+ const pub = loadIndex(root);
731
+ if (pub) all.push.apply(all, pub);
732
+ for (const owner of collectOwners(root)) {
733
+ const key = getOwnerKeyFor(root, owner);
734
+ if (!key) continue;
735
+ let idx = [];
736
+ try { idx = loadOwnerIndex(root, owner, key); } catch (err) { idx = []; }
737
+ all.push.apply(all, idx);
738
+ }
739
+ return all;
254
740
  }
255
741
  function upsertIndexEntry(root, entry) {
742
+ if (isEncrypted(root) && entry.scope !== 'public' && String(entry.file).indexOf(PUBLIC_DIR + '/') !== 0) {
743
+ const owner = entry.owner || String(entry.file).split('/')[1] || '';
744
+ const key = getOwnerKeyFor(root, owner);
745
+ if (!key) return;
746
+ const idx = loadOwnerIndex(root, owner, key);
747
+ const i = idx.findIndex(function (e) { return e.file === entry.file; });
748
+ if (i >= 0) idx[i] = entry; else idx.push(entry);
749
+ saveOwnerIndex(root, owner, key, idx);
750
+ return;
751
+ }
256
752
  const idx = getIndex(root);
257
753
  const i = idx.findIndex(function (e) { return e.file === entry.file; });
258
754
  if (i >= 0) idx[i] = entry; else idx.push(entry);
259
755
  saveIndex(root, idx);
260
756
  }
261
757
  function removeIndexEntry(root, file) {
758
+ if (isEncrypted(root) && String(file).indexOf(PRIVATE_DIR + '/') === 0) {
759
+ const owner = String(file).split('/')[1] || '';
760
+ const key = getOwnerKeyFor(root, owner);
761
+ if (!key) return;
762
+ const idx = loadOwnerIndex(root, owner, key);
763
+ const next = idx.filter(function (e) { return e.file !== file; });
764
+ if (next.length !== idx.length) saveOwnerIndex(root, owner, key, next);
765
+ return;
766
+ }
262
767
  const idx = getIndex(root);
263
768
  const i = idx.findIndex(function (e) { return e.file === file; });
264
769
  if (i >= 0) { idx.splice(i, 1); saveIndex(root, idx); }
265
770
  }
266
771
  function removeIndexEntries(root, files) {
267
772
  const set = new Set(files);
268
- const idx = getIndex(root);
269
- const next = idx.filter(function (e) { return !set.has(e.file); });
270
- if (next.length !== idx.length) saveIndex(root, next);
773
+ const byOwner = {};
774
+ const pub = [];
775
+ for (const f of files) {
776
+ if (String(f).indexOf(PRIVATE_DIR + '/') === 0) {
777
+ const o = String(f).split('/')[1] || '';
778
+ (byOwner[o] = byOwner[o] || []).push(f);
779
+ } else pub.push(f);
780
+ }
781
+ if (pub.length) {
782
+ const idx = getIndex(root);
783
+ const next = idx.filter(function (e) { return !set.has(e.file); });
784
+ if (next.length !== idx.length) saveIndex(root, next);
785
+ }
786
+ for (const o of Object.keys(byOwner)) {
787
+ const key = getOwnerKeyFor(root, o);
788
+ if (!key) continue;
789
+ const idx = loadOwnerIndex(root, o, key);
790
+ const oset = new Set(byOwner[o]);
791
+ const next = idx.filter(function (e) { return !oset.has(e.file); });
792
+ if (next.length !== idx.length) saveOwnerIndex(root, o, key, next);
793
+ }
271
794
  }
272
795
 
273
796
  // ---- 读取 / 写入记忆文件 ----
274
797
  function readEntry(fp, root) {
275
- const parsed = parseFrontmatter(fs.readFileSync(fp, 'utf8'));
798
+ const owner = ownerFromPrivatePath(root, fp);
799
+ let text;
800
+ if (isEncFile(fp)) {
801
+ const key = getOwnerKeyFor(root, owner);
802
+ if (!key) throw new Error('私密区已加密:当前无 ' + owner + ' 的授权密钥(yotta-memory view → 授权本智能体)。');
803
+ text = decryptMemoryText(fs.readFileSync(fp), key);
804
+ } else {
805
+ text = fs.readFileSync(fp, 'utf8');
806
+ }
807
+ const parsed = parseFrontmatter(text);
276
808
  const meta = parsed.meta;
277
809
  const type = (meta.type || 'FACT').toUpperCase();
278
810
  const subject = meta.subject || '';
@@ -294,34 +826,55 @@ function readEntry(fp, root) {
294
826
  immutable: meta.immutable === 'true',
295
827
  source: meta.source || '',
296
828
  weight: (parseFloat(meta.weight) > 0 ? parseFloat(meta.weight) : 1.0),
829
+ feedback_net: feedbackNetOf(meta),
297
830
  tokens: buildTokens(subject, statement, tags),
298
831
  meta: meta,
299
832
  };
300
833
  }
301
- function rewriteFrontmatter(fp, patch) {
302
- const txt = fs.readFileSync(fp, 'utf8');
303
- const parsed = parseFrontmatter(txt);
834
+ function rewriteFrontmatter(fp, patch, root, owner) {
835
+ let text;
836
+ try { text = readMemoryText(root || memoryRootForFile(fp), fp, owner || ownerFromPrivatePath(root || memoryRootForFile(fp), fp)); }
837
+ catch (e) { throw e; }
838
+ const parsed = parseFrontmatter(text);
304
839
  const meta = Object.assign({}, parsed.meta);
305
840
  for (const k of Object.keys(patch)) meta[k] = patch[k];
306
- fs.writeFileSync(fp, frontmatterToText(meta, parsed.body), 'utf8');
841
+ writeMemoryText(root || memoryRootForFile(fp), fp, frontmatterToText(meta, parsed.body), owner || ownerFromPrivatePath(root || memoryRootForFile(fp), fp));
307
842
  }
308
843
  function bumpReadMeta(root, relFiles) {
309
844
  const now = today();
310
845
  for (const rel of relFiles) {
311
846
  const fp = path.join(root, rel);
312
847
  if (!fs.existsSync(fp)) continue;
313
- const parsed = parseFrontmatter(fs.readFileSync(fp, 'utf8'));
848
+ const owner = ownerFromPrivatePath(root, fp);
849
+ let parsed;
850
+ try { parsed = parseFrontmatter(readMemoryText(root, fp, owner)); } catch (e) { continue; }
314
851
  const meta = parsed.meta;
315
852
  if (meta.immutable === 'true') continue;
316
853
  const acc = (parseInt(meta.access_count || '0', 10) || 0) + 1;
317
- rewriteFrontmatter(fp, { access_count: acc, last_accessed: now });
854
+ rewriteFrontmatter(fp, { access_count: acc, last_accessed: now }, root, owner);
318
855
  }
319
856
  }
320
857
  function touchIndex(root, relFiles) {
321
858
  const set = new Set(relFiles);
859
+ const now = today();
860
+ const manifest = loadIndexManifest(root);
861
+ if (manifest && Array.isArray(manifest.shards)) {
862
+ for (const sh of manifest.shards) {
863
+ if (!isSafeShardName(sh)) continue;
864
+ const sp = path.join(root, sh);
865
+ if (!fs.existsSync(sp)) continue;
866
+ let sd = null; try { sd = JSON.parse(fs.readFileSync(sp, 'utf8')); } catch (e) { continue; }
867
+ if (!sd || !Array.isArray(sd.entries)) continue;
868
+ let dirty = false;
869
+ for (const e of sd.entries) {
870
+ if (set.has(e.file)) { e.access_count = (parseInt(e.access_count, 10) || 0) + 1; e.last_accessed = now; dirty = true; }
871
+ }
872
+ if (dirty) fs.writeFileSync(sp, JSON.stringify(sd, null, 2), 'utf8');
873
+ }
874
+ return;
875
+ }
322
876
  const idx = getIndex(root);
323
877
  let dirty = false;
324
- const now = today();
325
878
  for (const e of idx) {
326
879
  if (set.has(e.file)) { e.access_count = (parseInt(e.access_count, 10) || 0) + 1; e.last_accessed = now; dirty = true; }
327
880
  }
@@ -342,7 +895,7 @@ function vitality(meta) {
342
895
  if (last) { const d = daysBetween(last, today()); recency = Math.max(0, 1 - Math.floor(d / 30) * 0.2); }
343
896
  return 0.4 * conf + 0.3 * accScore + 0.3 * recency;
344
897
  }
345
- // 灵魂盘 importance 融合:confidence × (0.5 + recency) + updated 加分 + weight 乘子 + immutable 加分
898
+ // importance 融合:confidence × (0.5 + recency) + updated 加分 + weight 乘子 + immutable 加分
346
899
  function importanceScore(meta) {
347
900
  const conf = parseFloat(meta.confidence || 1.0);
348
901
  let ageDays = 999;
@@ -404,9 +957,20 @@ function verifyWrittenReadable(root, rel, subj, agent) {
404
957
 
405
958
  // ---- 命令 core(CLI 与 MCP 共用;返回 { error, exitCode, text },不 process.exit)----
406
959
  function initCore(opts) {
960
+ opts = opts || {};
407
961
  const root = opts.dir ? path.resolve(String(opts.dir)) : (opts.project ? projectRoot() : userRoot());
962
+ const fresh = !fs.existsSync(path.join(root, PUBLIC_DIR)) && !fs.existsSync(path.join(root, PRIVATE_DIR)) && !fs.existsSync(path.join(root, INDEX_FILE));
408
963
  ensureInit(root);
409
- return { error: false, text: '已初始化记忆库: ' + root };
964
+ const wantEncrypt = opts.encrypt || (!opts.noEncrypt && fresh);
965
+ if (wantEncrypt) {
966
+ if (isEncrypted(root)) return { error: false, text: '已初始化记忆库(已启用加密): ' + root };
967
+ if (hasPlaintextPrivate(root)) return { error: true, text: '检测到明文私密区,请先 yotta-memory migrate 迁移到密文;新建空库可直接 init --encrypt。' };
968
+ const password = String(opts.password || process.env.YOTTA_MEMORY_PASS || '');
969
+ if (!password) return { error: true, text: '启用加密需要主口令:交互输入,或 YOTTA_MEMORY_PASS 环境变量(自动化)。' };
970
+ const r = initEncryptionCore(root, password, null);
971
+ return { error: false, text: '已初始化加密记忆库: ' + root + '\n[恢复钥匙](务必离线保存,仅此一次,泄露=可解全部私密): ' + r.rk.toString('base64') };
972
+ }
973
+ return { error: false, text: '已初始化记忆库: ' + root + (isEncrypted(root) ? '(已启用加密)' : '(明文;建议 init --encrypt 或 migrate 启用加密)') };
410
974
  }
411
975
  function rememberCore(type, subject, statement, opts) {
412
976
  opts = opts || {};
@@ -427,13 +991,18 @@ function rememberCore(type, subject, statement, opts) {
427
991
  if (scope === 'private' && owner && selfAgent && owner !== selfAgent && !opts.unsafe) {
428
992
  return { error: true, text: '拒绝: 当前声明身份 ' + selfAgent + ' 不能写入其它智能体 ' + owner + ' 的私密区。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。' };
429
993
  }
994
+ const encrypted = isEncrypted(root);
995
+ if (scope === 'private' && encrypted && !getOwnerKeyFor(root, owner)) {
996
+ return { error: true, text: '私密区已加密:当前无 ' + owner + ' 的授权密钥,请在用户平台授权(yotta-memory view → 授权本智能体)后再写私密记忆。公共 FACT 不受影响。' };
997
+ }
430
998
  const dir = path.join(root, typeSubdir(t, owner));
431
999
  fs.mkdirSync(dir, { recursive: true });
432
1000
  if (fs.existsSync(dir)) {
433
1001
  for (const f of fs.readdirSync(dir)) {
434
1002
  const fp = path.join(dir, f);
435
1003
  if (!fs.statSync(fp).isFile()) continue;
436
- const parsed = parseFrontmatter(fs.readFileSync(fp, 'utf8'));
1004
+ let parsed;
1005
+ try { parsed = parseFrontmatter(readMemoryText(root, fp, owner)); } catch (err) { continue; }
437
1006
  const meta = parsed.meta;
438
1007
  if ((meta.type || '').toUpperCase() === t && meta.subject === subj && meta.statement === stmt) {
439
1008
  const patch = { updated: today() };
@@ -441,7 +1010,7 @@ function rememberCore(type, subject, statement, opts) {
441
1010
  if (scope && !meta.scope) patch.scope = scope;
442
1011
  if (opts.source && !meta.source) patch.source = opts.source;
443
1012
  const w = parseFloat(opts.weight); if (w > 0) patch.weight = Math.max(parseFloat(meta.weight || '1.0') || 1.0, w);
444
- rewriteFrontmatter(fp, patch);
1013
+ rewriteFrontmatter(fp, patch, root, owner);
445
1014
  upsertIndexEntry(root, readEntry(fp, root));
446
1015
  let text = '已更新: ' + fp;
447
1016
  if (opts.hint !== false && t === 'FACT') {
@@ -458,7 +1027,8 @@ function rememberCore(type, subject, statement, opts) {
458
1027
  }
459
1028
  }
460
1029
  const seq = nextSeq(dir);
461
- const file = path.join(dir, today() + '-' + seq + '.md');
1030
+ const suffix = (encrypted && scope === 'private') ? ENC_SUFFIX : '';
1031
+ const file = path.join(dir, today() + '-' + seq + '.md' + suffix);
462
1032
  const rec = {
463
1033
  type: t, subject: subj, statement: stmt,
464
1034
  confidence: 1.0, created: today(), updated: today(),
@@ -468,7 +1038,7 @@ function rememberCore(type, subject, statement, opts) {
468
1038
  weight: (parseFloat(opts.weight) > 0 ? parseFloat(opts.weight) : 1.0),
469
1039
  access_count: 0, last_accessed: '',
470
1040
  };
471
- fs.writeFileSync(file, frontmatterToText(rec, stmt), 'utf8');
1041
+ writeMemoryText(root, file, frontmatterToText(rec, stmt), owner);
472
1042
  upsertIndexEntry(root, readEntry(file, root));
473
1043
  let text = '已记录: ' + file;
474
1044
  if (opts.hint !== false && t === 'FACT') {
@@ -482,7 +1052,44 @@ function rememberCore(type, subject, statement, opts) {
482
1052
  }
483
1053
  return { error: false, text: text };
484
1054
  }
1055
+ // v0.8.1: recall 候选预过滤——查询侧一次构建候选 token 集(精确/同义/拼音/拉丁 py),
1056
+ // 逐条用 token 交集 + 子串 + 模糊长度门槛做粗筛,命中候选才进语义打分,避免 N 大时逐条全量语义/模糊/编辑距离。
1057
+ // 该闸门是 semanticMatch 命中集的超集,绝不漏掉同义词/拼音/模糊命中,仅做性能粗筛不改语义。
1058
+ function recallPrefilter(q) {
1059
+ const qtoks = tokenize(q);
1060
+ const qset = new Set(qtoks);
1061
+ const qpy = pinyinTokens(q);
1062
+ const ext = new Set();
1063
+ for (const tt of qtoks) { const s = synonymSet(tt); for (const w of s) ext.add(w); }
1064
+ const qkeys = new Set();
1065
+ for (const tt of qtoks) qkeys.add(tt);
1066
+ for (const w of ext) qkeys.add(w);
1067
+ for (const t of qpy) qkeys.add(t);
1068
+ const longQtoks = qtoks.filter(function (t) { return t.length >= 4; });
1069
+ for (const tt of qtoks) {
1070
+ if (/^[a-z]+$/.test(tt)) {
1071
+ qkeys.add('py:' + tt); qkeys.add('pyi:' + tt);
1072
+ if (tt.length >= 3) { for (let i = 0; i < tt.length - 1; i++) qkeys.add('pyi:' + tt.slice(i, i + 2)); }
1073
+ }
1074
+ }
1075
+ return function (e) {
1076
+ const toks = e.tokens || {};
1077
+ const keys = Object.keys(toks);
1078
+ for (const k of keys) {
1079
+ if (qkeys.has(k)) return true;
1080
+ if (longQtoks.length && k.length >= 4 && k.indexOf(':') === -1) {
1081
+ for (const t of longQtoks) {
1082
+ if (Math.abs(k.length - t.length) <= 2) return true;
1083
+ }
1084
+ }
1085
+ }
1086
+ const hay = ((e.subject || '') + ' ' + (e.statement || '') + ' ' + (e.tags || []).join(' ')).toLowerCase();
1087
+ if (hay.indexOf(q) !== -1) return true;
1088
+ return false;
1089
+ };
1090
+ }
485
1091
  function recallCore(query, opts) {
1092
+ opts = opts || {};
486
1093
  const roots = memoryRoots();
487
1094
  if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
488
1095
  const limit = opts.limit || 50;
@@ -494,6 +1101,9 @@ function recallCore(query, opts) {
494
1101
  const allSafe = !!opts.unsafe;
495
1102
  const impersonation = !!(opts.agent && selfAgent && opts.agent !== selfAgent);
496
1103
  const explicitCross = !!opts.all || impersonation || (!!ownerFilter && ownerFilter.toLowerCase() !== 'user' && ownerFilter !== agent);
1104
+ const wantExplain = !!opts.explain;
1105
+ const useSemantic = opts.semantic !== false;
1106
+ const prefilter = (q && useSemantic) ? recallPrefilter(q) : null;
497
1107
  const hits = [];
498
1108
  let deniedCount = 0;
499
1109
  for (const root of roots) {
@@ -503,27 +1113,61 @@ function recallCore(query, opts) {
503
1113
  const r = classifyRead(e, agent, ownerFilter, allSafe, selfAgent);
504
1114
  if (r === 'denied') { deniedCount++; continue; }
505
1115
  let score = 0;
1116
+ let detail = null;
506
1117
  if (q) {
507
- const qtoks = tokenize(q);
508
- for (const tt of qtoks) { if (e.tokens && e.tokens[tt]) score += e.tokens[tt]; }
509
- if (score === 0) {
510
- const hay = ((e.subject || '') + ' ' + (e.statement || '') + ' ' + e.tags.join(' ')).toLowerCase();
511
- if (hay.indexOf(q) !== -1) score = 1;
1118
+ if (useSemantic) {
1119
+ if (prefilter && !prefilter(e)) continue; // v0.8.1 候选预过滤:粗筛后再语义打分
1120
+ const m = semanticMatch(e, q, wantExplain);
1121
+ score = m.score;
1122
+ if (wantExplain && m.detail && m.detail.length) detail = m.detail;
1123
+ } else {
1124
+ const qtoks = tokenize(q);
1125
+ for (const tt of qtoks) { if (e.tokens && e.tokens[tt]) score += e.tokens[tt]; }
1126
+ if (score === 0) {
1127
+ const hay = ((e.subject || '') + ' ' + (e.statement || '') + ' ' + e.tags.join(' ')).toLowerCase();
1128
+ if (hay.indexOf(q) !== -1) score = 1;
1129
+ }
512
1130
  }
513
1131
  if (score === 0) continue;
514
1132
  } else {
515
1133
  score = 1;
516
1134
  }
517
- hits.push({ entry: e, score: score, root: root });
1135
+ hits.push({ entry: e, score: score, root: root, detail: detail });
518
1136
  }
519
1137
  }
520
- hits.sort(function (a, b) {
521
- if (b.score !== a.score) return b.score - a.score;
522
- const pa = a.root === projectRoot() ? 0 : 1;
523
- const pb = b.root === projectRoot() ? 0 : 1;
524
- if (pa !== pb) return pa - pb;
525
- return String(b.entry.created).localeCompare(String(a.entry.created));
526
- });
1138
+ // v0.8.0 融合排序:0.65 × 语义分归一 + 0.35 × 效用分归一
1139
+ if (hits.length > 1) {
1140
+ let minSem = Infinity, maxSem = -Infinity, minUtil = Infinity, maxUtil = -Infinity;
1141
+ for (const h of hits) {
1142
+ if (h.score < minSem) minSem = h.score;
1143
+ if (h.score > maxSem) maxSem = h.score;
1144
+ const u = utilityScore(h.entry);
1145
+ if (u < minUtil) minUtil = u;
1146
+ if (u > maxUtil) maxUtil = u;
1147
+ }
1148
+ const spanSem = maxSem - minSem;
1149
+ const spanUtil = maxUtil - minUtil;
1150
+ for (const h of hits) {
1151
+ h.semNorm = spanSem > 0 ? (h.score - minSem) / spanSem : 1;
1152
+ h.utilNorm = spanUtil > 0 ? (utilityScore(h.entry) - minUtil) / spanUtil : 1;
1153
+ h.finalScore = 0.65 * h.semNorm + 0.35 * h.utilNorm;
1154
+ }
1155
+ hits.sort(function (a, b) {
1156
+ if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
1157
+ if (b.score !== a.score) return b.score - a.score;
1158
+ const pa = a.root === projectRoot() ? 0 : 1;
1159
+ const pb = b.root === projectRoot() ? 0 : 1;
1160
+ if (pa !== pb) return pa - pb;
1161
+ return String(b.entry.created).localeCompare(String(a.entry.created));
1162
+ });
1163
+ } else {
1164
+ hits.sort(function (a, b) {
1165
+ const pa = a.root === projectRoot() ? 0 : 1;
1166
+ const pb = b.root === projectRoot() ? 0 : 1;
1167
+ if (pa !== pb) return pa - pb;
1168
+ return String(b.entry.created).localeCompare(String(a.entry.created));
1169
+ });
1170
+ }
527
1171
  const shown = hits.slice(0, limit);
528
1172
  if (!shown.length) {
529
1173
  if (deniedCount > 0 && explicitCross) {
@@ -542,6 +1186,11 @@ function recallCore(query, opts) {
542
1186
  for (const h of shown) {
543
1187
  lines.push('[' + h.entry.type + '] ' + h.entry.subject + ': ' + h.entry.statement);
544
1188
  lines.push(' ' + path.join(h.root, h.entry.file));
1189
+ if (wantExplain) {
1190
+ if (h.detail && h.detail.length) lines.push(' 命中: ' + h.detail.join('、'));
1191
+ const ub = utilityBreakdown(h.entry);
1192
+ lines.push(' 效用分: ' + round3(ub.total) + '(conf ' + ub.confidence + ' + 使用 ' + ub.usage + ' + 时效 ' + ub.recency + ' + 类型 ' + ub.type + ' + 结构 ' + ub.structure + ')×weight ' + ub.weight);
1193
+ }
545
1194
  }
546
1195
  if (deniedCount > 0 && explicitCross) {
547
1196
  lines.push('\n[警告] 本次检索共拒绝 ' + deniedCount + ' 条越界访问(其它智能体私密记忆,未授权不展示)。如需读取请加 --unsafe 或 --owner user。');
@@ -585,21 +1234,26 @@ function forgetCore(fileRef, opts) {
585
1234
  return { error: false, text: '已删除: ' + target };
586
1235
  }
587
1236
  function archiveCore(opts) {
588
- const days = opts.days || 180;
589
- const threshold = (opts.threshold !== undefined && opts.threshold !== null) ? opts.threshold : 0.4;
1237
+ opts = opts || {};
1238
+ const cfg = loadConfig();
1239
+ const days = opts.days || parseInt(cfg.maintain_archived_age || '180', 10) || 180;
1240
+ const threshold = (opts.threshold !== undefined && opts.threshold !== null) ? opts.threshold : (parseFloat(cfg.maintain_archived_utility || 0.35));
590
1241
  const root = userRoot();
591
1242
  if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
592
1243
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
593
1244
  let moved = 0;
594
1245
  const movedFiles = [];
595
1246
  for (const fp of collectEntryFiles(root)) {
596
- const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
1247
+ const owner = ownerFromPrivatePath(root, fp);
1248
+ let meta;
1249
+ try { meta = parseFrontmatter(readMemoryText(root, fp, owner)).meta; } catch (e) { continue; }
597
1250
  if (meta.immutable === 'true') continue;
598
1251
  if (!meta.created) continue;
599
1252
  const createdTs = new Date(meta.created).getTime();
600
1253
  if (isNaN(createdTs)) continue;
601
1254
  const t = (meta.type || 'FACT').toUpperCase();
602
- if (vitality(meta) < threshold && createdTs < cutoff) {
1255
+ // v0.8.0 统一效用分(盖棺分)替代 vitality
1256
+ if (utilityScore(meta) < threshold && createdTs < cutoff) {
603
1257
  const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[t]);
604
1258
  fs.mkdirSync(destDir, { recursive: true });
605
1259
  fs.renameSync(fp, path.join(destDir, path.basename(fp)));
@@ -612,10 +1266,779 @@ function archiveCore(opts) {
612
1266
  return { error: false, text: '已归档 ' + moved + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR) };
613
1267
  }
614
1268
 
1269
+ // ---- v0.8.0 自我学习/自我进化/自我提升:feedback / explain / maintain / distill ----
1270
+ function auditPath(root, kind) {
1271
+ return path.join(root, ARCHIVE_DIR, kind + '-' + today() + '.jsonl');
1272
+ }
1273
+ function appendAudit(root, kind, rec) {
1274
+ try {
1275
+ fs.mkdirSync(path.join(root, ARCHIVE_DIR), { recursive: true });
1276
+ fs.appendFileSync(auditPath(root, kind), JSON.stringify(rec) + '\n', 'utf8');
1277
+ } catch (e) { /* 审计失败不阻断主流程 */ }
1278
+ }
1279
+ function resolveMemoryTarget(ref) {
1280
+ const roots = memoryRoots();
1281
+ const targetRef = String(ref || '').replace(/\\/g, '/');
1282
+ for (const root of roots) {
1283
+ const found = resolveMemoryFile(root, targetRef);
1284
+ if (found) return { fp: found.fp, root: root, rel: found.rel };
1285
+ }
1286
+ return null;
1287
+ }
1288
+ function checkOwnerWritable(targetRoot, targetRel, selfAgent, unsafe) {
1289
+ const seg = String(targetRel).split('/');
1290
+ if (seg[0] === PRIVATE_DIR) {
1291
+ const owner = seg[1] || '';
1292
+ if (!unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
1293
+ return '拒绝: 不能操作其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。';
1294
+ }
1295
+ }
1296
+ return null;
1297
+ }
1298
+ // 显式使用反馈:useful/useless → weight/confidence/feedback_net 演化(自我学习闭环)
1299
+ function feedbackCore(ref, opts) {
1300
+ opts = opts || {};
1301
+ const selfAgent = opts.selfAgent || currentAgent();
1302
+ const target = resolveMemoryTarget(ref);
1303
+ if (!target) return { error: true, text: '未找到记忆文件: ' + ref };
1304
+ const deny = checkOwnerWritable(target.root, target.rel, selfAgent, opts.unsafe);
1305
+ if (deny) return { error: true, text: deny };
1306
+ const owner = ownerFromPrivatePath(target.root, target.fp);
1307
+ let meta;
1308
+ try { meta = parseFrontmatter(readMemoryText(target.root, target.fp, owner)).meta; } catch (e) { return { error: true, text: '读取失败: ' + e.message }; }
1309
+ if (opts.undo) {
1310
+ return feedbackUndoCore(target, owner, meta, selfAgent, opts);
1311
+ }
1312
+ const useful = !!opts.useful;
1313
+ const useless = !!opts.useless;
1314
+ if (!useful && !useless) return { error: true, text: '请指定 --useful(有用)或 --useless(没用)。' };
1315
+ const cfg = loadConfig();
1316
+ const stepW = parseFloat(cfg.feedback_weight_step || 0.2);
1317
+ const stepC = parseFloat(cfg.feedback_confidence_step || 0.05);
1318
+ const w = parseFloat(meta.weight || '1.0'); const weight = (w > 0 ? w : 1.0);
1319
+ const conf = parseFloat(meta.confidence || '1.0'); const confidence = (conf > 0 ? conf : 1.0);
1320
+ const fb = feedbackNetOf(meta);
1321
+ let nw = weight, nc = confidence, nfb = fb;
1322
+ if (useful) { nw = Math.min(weight * (1 + stepW), 3.0); nc = Math.min(confidence + stepC, 1.0); nfb = fb + 1; }
1323
+ if (useless) { nw = Math.max(weight * (1 - stepW), 0.2); nc = Math.max(confidence - stepC, 0.3); nfb = fb - 1; }
1324
+ const before = { weight: round3(weight), confidence: round3(confidence), feedback_net: fb };
1325
+ const after = { weight: round3(nw), confidence: round3(nc), feedback_net: nfb };
1326
+ const patch = { weight: after.weight, confidence: after.confidence, feedback_net: after.feedback_net, updated: today() };
1327
+ rewriteFrontmatter(target.fp, patch, target.root, owner);
1328
+ upsertIndexEntry(target.root, readEntry(target.fp, target.root));
1329
+ appendAudit(target.root, 'feedback', { ts: new Date().toISOString(), file: target.rel, action: useful ? 'useful' : 'useless', before: before, after: after, reason: opts.reason || '' });
1330
+ const lines = [(useful ? '已反馈 [有用]' : '已反馈 [没用]') + ': ' + target.rel];
1331
+ lines.push(' weight ' + before.weight + ' -> ' + after.weight + ' / confidence ' + before.confidence + ' -> ' + after.confidence + ' / feedback_net ' + before.feedback_net + ' -> ' + after.feedback_net);
1332
+ if (opts.reason) lines.push(' 原因: ' + opts.reason);
1333
+ lines.push(' 提示: yotta-memory explain ' + target.rel + ' 查看效用分项;feedback --undo 回滚最近一次');
1334
+ return { error: false, text: lines.join('\n') };
1335
+ }
1336
+ function feedbackUndoCore(target, owner, meta, selfAgent, opts) {
1337
+ const cfg = loadConfig();
1338
+ const lines = [];
1339
+ let restored = false;
1340
+ try {
1341
+ const log = auditPath(target.root, 'feedback');
1342
+ if (!fs.existsSync(log)) return { error: true, text: '无可回滚的反馈记录(' + log + ' 不存在)。' };
1343
+ const recs = fs.readFileSync(log, 'utf8').split('\n').filter(Boolean).map(function (l) { try { return JSON.parse(l); } catch (e) { return null; } }).filter(Boolean);
1344
+ const mine = recs.filter(function (r) { return r.file === target.rel && (r.action === 'useful' || r.action === 'useless'); });
1345
+ if (!mine.length) return { error: true, text: '该文件无反馈记录可回滚。' };
1346
+ const last = mine[mine.length - 1];
1347
+ const patch = { weight: last.before.weight, confidence: last.before.confidence, feedback_net: last.before.feedback_net, updated: today() };
1348
+ rewriteFrontmatter(target.fp, patch, target.root, owner);
1349
+ upsertIndexEntry(target.root, readEntry(target.fp, target.root));
1350
+ appendAudit(target.root, 'feedback', { ts: new Date().toISOString(), file: target.rel, action: 'undo', before: last.after, after: last.before, reason: opts.reason || '' });
1351
+ lines.push('已回滚最近一次反馈: ' + target.rel);
1352
+ lines.push(' ' + last.action + ' -> 恢复 weight ' + last.after.weight + ' -> ' + last.before.weight + ' / feedback_net ' + last.after.feedback_net + ' -> ' + last.before.feedback_net);
1353
+ restored = true;
1354
+ } catch (e) { return { error: true, text: '回滚失败: ' + e.message }; }
1355
+ return { error: false, text: lines.join('\n') };
1356
+ }
1357
+ // 解释单条记忆效用分项(为什么靠前/归档/遗忘)
1358
+ function explainCore(ref, opts) {
1359
+ opts = opts || {};
1360
+ const selfAgent = opts.selfAgent || currentAgent();
1361
+ const target = resolveMemoryTarget(ref);
1362
+ if (!target) return { error: true, text: '未找到记忆文件: ' + ref };
1363
+ const owner = ownerFromPrivatePath(target.root, target.fp);
1364
+ let entry;
1365
+ try { entry = readEntry(target.fp, target.root); } catch (e) { return { error: true, text: '读取失败: ' + e.message }; }
1366
+ const deny = checkOwnerWritable(target.root, target.rel, selfAgent, opts.unsafe);
1367
+ const readable = !deny;
1368
+ const ub = utilityBreakdown(entry);
1369
+ const ageDays = entry.created ? daysBetween(entry.created, today()) : 0;
1370
+ const cfg = loadConfig();
1371
+ const archU = parseFloat(cfg.maintain_archived_utility || 0.35);
1372
+ const archAge = parseInt(cfg.maintain_archived_age || '180', 10) || 180;
1373
+ const forgetU = parseFloat(cfg.maintain_forget_utility || 0.12);
1374
+ const forgetAge = parseInt(cfg.maintain_forget_age || '365', 10) || 365;
1375
+ const lines = ['[' + entry.type + '] ' + entry.subject + ': ' + entry.statement];
1376
+ lines.push(' 文件: ' + target.rel + (readable ? '' : '(其它智能体私密,仅元数据)'));
1377
+ lines.push(' 效用分: ' + ub.total + ' = confidence ' + ub.confidence + ' + 使用 ' + ub.usage + ' + 时效 ' + ub.recency + ' + 类型 ' + ub.type + ' + 结构 ' + ub.structure + ')×weight ' + ub.weight);
1378
+ lines.push(' 年龄: ' + ageDays + ' 天 / access_count ' + (entry.access_count || 0) + ' / feedback_net ' + entry.feedback_net + ' / immutable ' + (entry.immutable ? '是' : '否'));
1379
+ const importScore = round3(importanceScore(entry));
1380
+ lines.push(' importance(旧): ' + importScore + ' / vitality(旧): ' + round3(vitality(entry)));
1381
+ if (entry.immutable) lines.push(' 状态: immutable 豁免自动归档/遗忘');
1382
+ else if (ub.total < forgetU && ageDays > forgetAge && entry.type !== 'BOUND') lines.push(' 状态: 遗忘候选(utility < ' + forgetU + ' 且年龄 > ' + forgetAge + ' 天;--purge 才真删)');
1383
+ else if (ub.total < archU && ageDays > archAge) lines.push(' 状态: 归档候选(utility < ' + archU + ' 且年龄 > ' + archAge + ' 天)');
1384
+ else lines.push(' 状态: 保留(未达归档/遗忘阈值)');
1385
+ return { error: false, text: lines.join('\n') };
1386
+ }
1387
+ function jaccardTokens(a, b) {
1388
+ const sa = new Set(Object.keys(a.tokens || {}).filter(function (k) { return k.indexOf(':') === -1; }));
1389
+ const sb = new Set(Object.keys(b.tokens || {}).filter(function (k) { return k.indexOf(':') === -1; }));
1390
+ if (!sa.size || !sb.size) return 0;
1391
+ let inter = 0;
1392
+ for (const k of sa) { if (sb.has(k)) inter++; }
1393
+ const union = sa.size + sb.size - inter;
1394
+ return union ? inter / union : 0;
1395
+ }
1396
+ function subjectsSimilar(a, b) {
1397
+ const sa = String(a.subject || '').toLowerCase();
1398
+ const sb = String(b.subject || '').toLowerCase();
1399
+ if (!sa || !sb) return false;
1400
+ if (sa === sb) return true;
1401
+ return editDistance(sa, sb) <= 3;
1402
+ }
1403
+ // 记忆自组织:规则层自动归档/遗忘/去重(默认 dry-run,--apply 才执行,--purge 才真删)
1404
+ function maintainCore(opts) {
1405
+ opts = opts || {};
1406
+ const apply = !!opts.apply;
1407
+ const purge = !!opts.purge;
1408
+ const root = userRoot();
1409
+ if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
1410
+ const cfg = loadConfig();
1411
+ const archU = parseFloat(opts.threshold !== undefined && opts.threshold !== null ? opts.threshold : (cfg.maintain_archived_utility || 0.35));
1412
+ const archAge = parseInt(opts.age || cfg.maintain_archived_age || '180', 10) || 180;
1413
+ const forgetU = parseFloat(cfg.maintain_forget_utility || 0.12);
1414
+ const forgetAge = parseInt(cfg.maintain_forget_age || '365', 10) || 365;
1415
+ const mode = apply ? (purge ? '执行(含遗忘真删 --purge)' : '执行') : '预览(dry-run,未改动;加 --apply 执行,--purge 才真删)';
1416
+ const lines = ['## yotta-memory maintain(记忆自组织)'];
1417
+ lines.push('- 模式: ' + mode);
1418
+ lines.push('- 归档阈值: utility < ' + archU + ' 且年龄 > ' + archAge + ' 天;遗忘阈值: utility < ' + forgetU + ' 且年龄 > ' + forgetAge + ' 天');
1419
+ lines.push('');
1420
+ const toArchive = [], toForget = [], skipped = [];
1421
+ for (const fp of collectEntryFiles(root)) {
1422
+ const owner = ownerFromPrivatePath(root, fp);
1423
+ let meta;
1424
+ try { meta = parseFrontmatter(readMemoryText(root, fp, owner)).meta; } catch (e) { continue; }
1425
+ if (meta.immutable === 'true') { skipped.push({ fp: fp, reason: 'immutable 豁免' }); continue; }
1426
+ if (!meta.created) { skipped.push({ fp: fp, reason: '无 created' }); continue; }
1427
+ const type = (meta.type || 'FACT').toUpperCase();
1428
+ const ageDays = daysBetween(meta.created, today());
1429
+ const u = utilityScore(meta);
1430
+ const rec = { fp: fp, rel: relOf(root, fp), type: type, ageDays: ageDays, utility: u, meta: meta, owner: owner };
1431
+ if (u < forgetU && ageDays > forgetAge) {
1432
+ if (type === 'BOUND') { skipped.push({ fp: fp, reason: 'BOUND 豁免遗忘' }); }
1433
+ else { toForget.push(rec); continue; }
1434
+ }
1435
+ if (u < archU && ageDays > archAge) toArchive.push(rec);
1436
+ }
1437
+ let archived = 0, forgotten = 0;
1438
+ if (toArchive.length) {
1439
+ lines.push('### 归档候选(' + toArchive.length + ' 条)');
1440
+ for (const rec of toArchive) {
1441
+ 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 收工纪律锚点,请确认' : ''));
1442
+ if (apply) {
1443
+ const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[rec.type]);
1444
+ fs.mkdirSync(destDir, { recursive: true });
1445
+ fs.renameSync(rec.fp, path.join(destDir, path.basename(rec.fp)));
1446
+ appendAudit(root, 'audit', { ts: new Date().toISOString(), file: rec.rel, action: 'archive', reason: 'utility<' + round3(rec.utility) + ',age>' + rec.ageDays, utility: round3(rec.utility) });
1447
+ archived++;
1448
+ }
1449
+ }
1450
+ lines.push('');
1451
+ }
1452
+ if (toForget.length) {
1453
+ lines.push('### 遗忘候选(' + toForget.length + ' 条,默认不真删;--purge 才删除)');
1454
+ for (const rec of toForget) {
1455
+ lines.push('- [' + rec.type + '] ' + rec.rel + '(utility ' + round3(rec.utility) + ',' + rec.ageDays + ' 天)');
1456
+ if (apply && purge) {
1457
+ const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[rec.type]);
1458
+ fs.mkdirSync(destDir, { recursive: true });
1459
+ const dest = path.join(destDir, path.basename(rec.fp));
1460
+ fs.renameSync(rec.fp, dest);
1461
+ 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) });
1462
+ // 真删前二次确认标记:purge 已显式授权,删除 .archive 内副本前的最终动作 = 记录审计后删除
1463
+ try { fs.unlinkSync(dest); } catch (e) { /* 保留副本 */ }
1464
+ forgotten++;
1465
+ } else if (apply) {
1466
+ lines.push(' (--purge 未开启,跳过真删)');
1467
+ }
1468
+ }
1469
+ lines.push('');
1470
+ }
1471
+ if (apply) {
1472
+ if (archived || forgotten) {
1473
+ const allMoved = toArchive.concat(toForget).filter(function (r) { return true; });
1474
+ removeIndexEntries(root, allMoved.map(function (r) { return r.rel; }));
1475
+ }
1476
+ lines.push('- 已执行: 归档 ' + archived + ' 条,遗忘 ' + forgotten + ' 条。审计: ' + auditPath(root, 'audit'));
1477
+ } else {
1478
+ lines.push('- 未执行任何变更(dry-run)。加 --apply 执行归档,--purge 才真删遗忘候选。');
1479
+ }
1480
+ if (skipped.length) {
1481
+ lines.push('');
1482
+ lines.push('### 跳过(' + skipped.length + ' 条)');
1483
+ const grouped = {};
1484
+ for (const s of skipped) { grouped[s.reason] = (grouped[s.reason] || 0) + 1; }
1485
+ for (const k of Object.keys(grouped)) lines.push('- ' + k + ': ' + grouped[k] + ' 条');
1486
+ }
1487
+ // 去重
1488
+ if (opts.dedup) {
1489
+ lines.push('');
1490
+ lines.push('### 去重候选(--dedup)');
1491
+ const entries = ensureIndex(root);
1492
+ const groups = [];
1493
+ const used = new Set();
1494
+ for (let i = 0; i < entries.length; i++) {
1495
+ if (used.has(i)) continue;
1496
+ const g = [entries[i]];
1497
+ used.add(i);
1498
+ for (let j = i + 1; j < entries.length; j++) {
1499
+ if (used.has(j)) continue;
1500
+ if (subjectsSimilar(entries[i], entries[j]) && jaccardTokens(entries[i], entries[j]) >= 0.7) {
1501
+ g.push(entries[j]); used.add(j);
1502
+ }
1503
+ }
1504
+ if (g.length > 1) groups.push(g);
1505
+ }
1506
+ if (!groups.length) lines.push('(无重复候选)');
1507
+ for (const g of groups) {
1508
+ lines.push('- 组: ' + g.map(function (e) { return e.file; }).join(' | '));
1509
+ lines.push(' ' + g.map(function (e) { return e.subject + ': ' + e.statement; }).join(' / '));
1510
+ }
1511
+ lines.push(' 合并: yotta-memory maintain --merge <fileA> <fileB>');
1512
+ }
1513
+ return { error: false, text: lines.join('\n') };
1514
+ }
1515
+ // 手动合并两条相似记忆:保留高 confidence,低 confidence 移入 .archive
1516
+ function mergeCore(refA, refB, opts) {
1517
+ opts = opts || {};
1518
+ const selfAgent = opts.selfAgent || currentAgent();
1519
+ const ta = resolveMemoryTarget(refA);
1520
+ const tb = resolveMemoryTarget(refB);
1521
+ if (!ta || !tb) return { error: true, text: '未找到记忆文件: ' + refA + ' / ' + refB };
1522
+ for (const t of [ta, tb]) {
1523
+ const deny = checkOwnerWritable(t.root, t.rel, selfAgent, opts.unsafe);
1524
+ if (deny) return { error: true, text: deny };
1525
+ }
1526
+ if (ta.root !== tb.root) return { error: true, text: '暂不支持跨记忆库合并(' + ta.root + ' vs ' + tb.root + ')。' };
1527
+ const root = ta.root;
1528
+ const ea = readEntry(ta.fp, root);
1529
+ const eb = readEntry(tb.fp, root);
1530
+ const keep = ea.confidence >= eb.confidence ? ea : eb;
1531
+ const drop = ea.confidence >= eb.confidence ? eb : ea;
1532
+ const keepFp = ea.confidence >= eb.confidence ? ta.fp : tb.fp;
1533
+ const dropFp = ea.confidence >= eb.confidence ? tb.fp : ta.fp;
1534
+ const owner = ownerFromPrivatePath(root, keepFp);
1535
+ const tags = Array.from(new Set((keep.tags || []).concat(drop.tags || [])));
1536
+ 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) };
1537
+ rewriteFrontmatter(keepFp, patch, root, owner);
1538
+ upsertIndexEntry(root, readEntry(keepFp, root));
1539
+ const destDir = path.join(root, ARCHIVE_DIR, TYPE_DIRS[drop.type]);
1540
+ fs.mkdirSync(destDir, { recursive: true });
1541
+ const dest = path.join(destDir, path.basename(dropFp));
1542
+ fs.renameSync(dropFp, dest);
1543
+ removeIndexEntry(root, drop.file);
1544
+ appendAudit(root, 'audit', { ts: new Date().toISOString(), file: drop.file, action: 'merge', reason: '并入 ' + keep.file, utility: round3(utilityScore(keep)) });
1545
+ return { error: false, text: '已合并: ' + drop.file + ' -> ' + keep.file + '\n 保留: [' + keep.type + '] ' + keep.subject + ': ' + keep.statement + '\n 归档低 confidence: ' + drop.file };
1546
+ }
1547
+ // 心理日志蒸馏:统计摘要 / 主题画像 / 知识地图(启发式默认,可选 --model)
1548
+ function distillCore(opts) {
1549
+ opts = opts || {};
1550
+ const root = userRoot();
1551
+ if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
1552
+ const selfAgent = opts.selfAgent || currentAgent();
1553
+ const owner = opts.owner || selfAgent;
1554
+ const entries = [];
1555
+ for (const r of memoryRoots()) {
1556
+ for (const e of ensureIndex(r)) {
1557
+ if (classifyRead(e, owner, '', !!opts.unsafe, selfAgent) === 'denied') continue;
1558
+ entries.push(e);
1559
+ }
1560
+ }
1561
+ const lines = [];
1562
+ lines.push('# 元忆记忆蒸馏报告(v0.8.0)');
1563
+ lines.push('');
1564
+ lines.push('> 生成: yotta-memory distill | 启发式蒸馏(零依赖);--model <cmd> 可选外部模型增强');
1565
+ lines.push('> 时间: ' + new Date().toISOString() + ' / 可读条目: ' + entries.length + ' 条');
1566
+ lines.push('');
1567
+ // 1) 统计摘要
1568
+ lines.push('## 一、统计摘要');
1569
+ lines.push('');
1570
+ if (!entries.length) lines.push('(无记忆)');
1571
+ const byType = {};
1572
+ for (const e of entries) byType[e.type] = (byType[e.type] || 0) + 1;
1573
+ lines.push('- 类型分布: ' + Object.keys(byType).map(function (k) { return k + ' ' + byType[k]; }).join(' / '));
1574
+ let young = 0, mid = 0, old = 0, veryOld = 0;
1575
+ for (const e of entries) {
1576
+ if (!e.created) continue;
1577
+ const d = daysBetween(e.created, today());
1578
+ if (d < 30) young++; else if (d < 90) mid++; else if (d < 180) old++; else veryOld++;
1579
+ }
1580
+ lines.push('- 年龄分布: <30天 ' + young + ' / 30-90 ' + mid + ' / 90-180 ' + old + ' / >180天 ' + veryOld);
1581
+ const hot = entries.slice().sort(function (a, b) { return (b.access_count || 0) - (a.access_count || 0); }).slice(0, 5);
1582
+ lines.push('- 热度 Top5(access_count):');
1583
+ for (const h of hot) lines.push(' - [' + h.type + '] ' + h.subject + '(' + (h.access_count || 0) + ' 次)');
1584
+ let fbSum = 0, usefulN = 0, uselessN = 0;
1585
+ for (const e of entries) { const n = e.feedback_net || 0; fbSum += n; if (n > 0) usefulN++; else if (n < 0) uselessN++; }
1586
+ lines.push('- 反馈统计: feedback_net 总和 ' + fbSum + '(正 ' + usefulN + ' 条 / 负 ' + uselessN + ' 条)');
1587
+ lines.push('');
1588
+ // 2) 主题画像(按 subject 聚类合并)
1589
+ lines.push('## 二、主题画像(按 subject 聚类,启发式合并)');
1590
+ lines.push('');
1591
+ const groups = {};
1592
+ for (const e of entries) {
1593
+ const key = String(e.subject || '(无主题)');
1594
+ (groups[key] = groups[key] || []).push(e);
1595
+ }
1596
+ const sortedKeys = Object.keys(groups).sort(function (a, b) { return groups[b].length - groups[a].length; });
1597
+ for (const k of sortedKeys) {
1598
+ const g = groups[k];
1599
+ const best = g.slice().sort(function (a, b) { return (b.confidence || 0) - (a.confidence || 0); })[0];
1600
+ const tags = Array.from(new Set([].concat.apply([], g.map(function (e) { return e.tags || []; }))));
1601
+ lines.push('### ' + k + '(' + g.length + ' 条)');
1602
+ lines.push('- 代表: ' + best.statement);
1603
+ if (tags.length) lines.push('- 标签: ' + tags.join(', '));
1604
+ lines.push('- 引用: ' + g.map(function (e) { return e.file; }).join(' / '));
1605
+ lines.push('');
1606
+ }
1607
+ // 3) 知识地图(type + tags 层级)
1608
+ lines.push('## 三、知识地图(type → tags)');
1609
+ lines.push('');
1610
+ const map = {};
1611
+ for (const e of entries) {
1612
+ const t = e.type;
1613
+ (map[t] = map[t] || {});
1614
+ const tagKey = (e.tags && e.tags.length) ? e.tags.join(',') : '(无标签)';
1615
+ (map[t][tagKey] = map[t][tagKey] || []).push(e);
1616
+ }
1617
+ for (const t of Object.keys(map).sort()) {
1618
+ lines.push('### ' + t);
1619
+ for (const tk of Object.keys(map[t]).sort()) {
1620
+ lines.push('- ' + tk + ': ' + map[t][tk].map(function (e) { return e.subject; }).join(' / '));
1621
+ }
1622
+ }
1623
+ let body = lines.join('\n') + '\n';
1624
+ // 可选模型增强:--model <cmd>,stdin 传结构化摘要,stdout 取提炼文本
1625
+ let modelOut = '';
1626
+ if (opts.model) {
1627
+ try {
1628
+ 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) });
1629
+ const r = child_process.spawnSync(String(opts.model), { input: payload, encoding: 'utf8', maxBuffer: 1024 * 1024, shell: process.platform === 'win32' });
1630
+ if (r.stdout) modelOut = String(r.stdout).trim();
1631
+ if (!modelOut) modelOut = '(模型无输出)';
1632
+ body += '\n## 四、模型提炼(--model ' + opts.model + ')\n\n' + modelOut + '\n';
1633
+ } catch (e) {
1634
+ modelOut = '(模型调用失败: ' + e.message + ')';
1635
+ body += '\n## 四、模型提炼\n\n' + modelOut + '\n';
1636
+ }
1637
+ }
1638
+ // 落盘:--out 指定 / 私密 distills / 公共 facts/distills
1639
+ let written = '';
1640
+ try {
1641
+ if (opts.out) {
1642
+ const outPath = path.resolve(String(opts.out));
1643
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
1644
+ fs.writeFileSync(outPath, body, 'utf8');
1645
+ written = outPath;
1646
+ } else {
1647
+ const slug = String((opts.subject || 'distill').replace(/[^\w\u4e00-\u9fa5-]+/g, '-').slice(0, 40));
1648
+ const fname = today() + '-' + slug + '.md';
1649
+ if (owner && entries.some(function (e) { return e.scope !== 'public' && e.owner === owner; })) {
1650
+ const dir = path.join(root, PRIVATE_DIR, owner, 'distills');
1651
+ fs.mkdirSync(dir, { recursive: true });
1652
+ const fp = path.join(dir, fname);
1653
+ 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);
1654
+ writeMemoryText(root, fp, text, owner);
1655
+ upsertIndexEntry(root, readEntry(fp, root));
1656
+ written = fp;
1657
+ } else {
1658
+ const dir = path.join(root, PUBLIC_DIR, 'distills');
1659
+ fs.mkdirSync(dir, { recursive: true });
1660
+ const fp = path.join(dir, fname);
1661
+ 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);
1662
+ fs.writeFileSync(fp, text, 'utf8');
1663
+ upsertIndexEntry(root, readEntry(fp, root));
1664
+ written = fp;
1665
+ }
1666
+ }
1667
+ } catch (e) {
1668
+ return { error: false, text: lines.join('\n') + '\n\n[落盘失败] ' + e.message };
1669
+ }
1670
+ return { error: false, text: body + '\n[已写入] ' + written };
1671
+ }
1672
+
1673
+ function promptPassword(promptText) {
1674
+ return new Promise(function (resolve) {
1675
+ const stdin = process.stdin;
1676
+ if (!stdin.isTTY) { resolve(null); return; }
1677
+ process.stdout.write(promptText);
1678
+ let buf = '';
1679
+ function done(abort) {
1680
+ try { stdin.setRawMode(false); } catch (e) {}
1681
+ stdin.pause();
1682
+ stdin.removeListener('data', onData);
1683
+ process.stdout.write('\n');
1684
+ resolve(abort ? null : buf);
1685
+ }
1686
+ function onData(chunk) {
1687
+ const s = String(chunk);
1688
+ for (const c of s) {
1689
+ if (c === '\r' || c === '\n') { done(false); return; }
1690
+ if (c === '\u0003') { done(true); return; }
1691
+ if (c === '\u007f' || c === '\b') buf = buf.slice(0, -1);
1692
+ else buf += c;
1693
+ }
1694
+ }
1695
+ try { stdin.setRawMode(true); } catch (e) { resolve(null); return; }
1696
+ stdin.resume();
1697
+ stdin.setEncoding('utf8');
1698
+ stdin.on('data', onData);
1699
+ });
1700
+ }
1701
+
1702
+ function migrateCore(root, password, recoveryKeyIn) {
1703
+ if (!fs.existsSync(root)) return { error: true, text: '记忆库不存在,请先 yotta-memory init。' };
1704
+ if (isEncrypted(root)) return { error: true, text: '记忆库已启用加密(keys/ 存在),无需迁移。' };
1705
+ if (!hasPlaintextPrivate(root)) return { error: false, text: '无私密明文可迁移(private/ 下无 .md 文件);如需新建加密库请 init --encrypt。' };
1706
+ const salt = crypto.randomBytes(16);
1707
+ ensureKeysDir(root);
1708
+ fs.writeFileSync(encSaltPath(root), salt);
1709
+ const umk = deriveUmk(password, salt);
1710
+ const rk = crypto.randomBytes(32);
1711
+ fs.writeFileSync(encRecoveryEncPath(root), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(umk, rk, Buffer.from('recovery', 'utf8'))]));
1712
+ let moved = 0;
1713
+ const owners = collectOwners(root);
1714
+ const self = currentAgent();
1715
+ for (const owner of owners) {
1716
+ const ok = wrapOwnerKey(root, umk, rk, owner);
1717
+ if (self && owner === self) writeOwnerKeyCache(root, owner, ok);
1718
+ for (const t of PRIVATE_LEAF) {
1719
+ const d = path.join(root, PRIVATE_DIR, owner, t);
1720
+ if (!fs.existsSync(d)) continue;
1721
+ for (const f of fs.readdirSync(d)) {
1722
+ if (!f.endsWith('.md')) continue;
1723
+ const fp = path.join(d, f);
1724
+ const text = fs.readFileSync(fp, 'utf8');
1725
+ fs.writeFileSync(fp + ENC_SUFFIX, encryptMemoryText(text, ok));
1726
+ fs.unlinkSync(fp);
1727
+ moved++;
1728
+ }
1729
+ }
1730
+ const pf = path.join(root, PRIVATE_DIR, owner, PROFILE_FILE);
1731
+ if (fs.existsSync(pf)) {
1732
+ const text = fs.readFileSync(pf, 'utf8');
1733
+ fs.writeFileSync(pf + ENC_SUFFIX, encryptMemoryText(text, ok));
1734
+ fs.unlinkSync(pf);
1735
+ moved++;
1736
+ }
1737
+ }
1738
+ buildIndex(root);
1739
+ let tail = '已迁移 ' + moved + ' 个私密文件(' + owners.length + ' 个 owner)到密文。';
1740
+ tail += '\n[恢复钥匙](务必离线保存,仅此一次,泄露=可解全部私密): ' + rk.toString('base64');
1741
+ if (self && owners.indexOf(self) !== -1) tail += '\n已为本智能体 ' + self + ' 写入授权缓存,可继续读写自己的私密。';
1742
+ else tail += '\n各智能体需在用户平台授权(yotta-memory view → 授权本智能体)后才能读写自己的私密。';
1743
+ return { error: false, text: tail };
1744
+ }
1745
+
1746
+ async function cmdMigrate(opts) {
1747
+ const root = userRoot();
1748
+ if (isEncrypted(root)) { console.error('记忆库已启用加密,无需迁移。'); process.exit(2); }
1749
+ if (!hasPlaintextPrivate(root)) { console.error('无私密明文可迁移。'); process.exit(2); }
1750
+ let password = opts.password || process.env.YOTTA_MEMORY_PASS || '';
1751
+ if (!password) {
1752
+ password = await promptPassword('主口令(迁移后私密区加密,勿忘;忘口令可用恢复钥匙重设): ') || '';
1753
+ if (!password) { console.error('已取消。'); process.exit(2); }
1754
+ }
1755
+ const r = migrateCore(root, password, null);
1756
+ console.log(r.text);
1757
+ if (r.error) process.exit(2);
1758
+ }
1759
+
1760
+ function resetPasswordCore(root, opts) {
1761
+ opts = opts || {};
1762
+ if (!isEncrypted(root)) return { error: true, text: '记忆库未启用加密。' };
1763
+ const salt = loadSalt(root);
1764
+ if (!salt) return { error: true, text: '密钥库损坏(缺 keys/salt)。' };
1765
+ const owners = keyOwners(root).filter(function (o) { return fs.existsSync(encOwnerKeyPath(root, o)); });
1766
+ if (!owners.length) return { error: true, text: '没有可重设的 owner 密钥(keys/ 下无 *.key.enc)。' };
1767
+ const newPass = String(opts.newPassword || '');
1768
+ if (!newPass) return { error: true, text: '请提供新口令(--new-password 或交互输入)。' };
1769
+ if (opts.recoveryKey) {
1770
+ const rk = Buffer.from(String(opts.recoveryKey).trim(), 'base64');
1771
+ if (rk.length !== 32) return { error: true, text: '恢复钥匙格式错误(须为 32 字节 base64)。' };
1772
+ let verified = false;
1773
+ for (const o of owners) { try { unwrapOwnerKeyRecovery(root, o, rk); verified = true; break; } catch (e) {} }
1774
+ if (!verified) return { error: true, text: '恢复钥匙无效(无法解开任何 owner 密钥)。' };
1775
+ const newUmk = deriveUmk(newPass, salt);
1776
+ for (const o of owners) {
1777
+ const ok = unwrapOwnerKeyRecovery(root, o, rk);
1778
+ const aad = Buffer.from('owner:' + o, 'utf8');
1779
+ fs.writeFileSync(encOwnerKeyPath(root, o), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(newUmk, ok, aad)]));
1780
+ }
1781
+ fs.writeFileSync(encRecoveryEncPath(root), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(newUmk, rk, Buffer.from('recovery', 'utf8'))]));
1782
+ return { error: false, text: '口令已重设(恢复钥匙验证通过,已重新包裹全部 owner 密钥)。' };
1783
+ }
1784
+ const curPass = String(opts.password || '');
1785
+ if (!curPass) return { error: true, text: '请提供当前口令(--password 或交互输入),或用 --recovery-key。' };
1786
+ const curUmk = deriveUmk(curPass, salt);
1787
+ let verified = false;
1788
+ for (const o of owners) { try { unwrapOwnerKey(root, o, curUmk); verified = true; break; } catch (e) {} }
1789
+ if (!verified) return { error: true, text: '当前口令错误。' };
1790
+ const newUmk = deriveUmk(newPass, salt);
1791
+ for (const o of owners) {
1792
+ const ok = unwrapOwnerKey(root, o, curUmk);
1793
+ const aad = Buffer.from('owner:' + o, 'utf8');
1794
+ fs.writeFileSync(encOwnerKeyPath(root, o), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(newUmk, ok, aad)]));
1795
+ }
1796
+ fs.writeFileSync(encRecoveryEncPath(root), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(newUmk, unwrapRecoveryEnc(root, curUmk), Buffer.from('recovery', 'utf8'))]));
1797
+ return { error: false, text: '口令已重设(当前口令验证通过)。' };
1798
+ }
1799
+
1800
+ async function cmdResetPassword(opts) {
1801
+ const root = userRoot();
1802
+ if (!isEncrypted(root)) { console.error('记忆库未启用加密。'); process.exit(2); }
1803
+ let cur = opts.password || '';
1804
+ let rk = opts.recoveryKey || '';
1805
+ let np = opts.newPassword || process.env.YOTTA_MEMORY_PASS || '';
1806
+ if (!rk && !cur) {
1807
+ cur = await promptPassword('当前口令(或 --recovery-key 用恢复钥匙): ') || '';
1808
+ if (!cur) { console.error('已取消。'); process.exit(2); }
1809
+ }
1810
+ if (!np) {
1811
+ np = await promptPassword('新口令: ') || '';
1812
+ if (!np) { console.error('已取消。'); process.exit(2); }
1813
+ const np2 = await promptPassword('再次输入确认: ') || '';
1814
+ if (np2 !== np) { console.error('两次输入不一致。'); process.exit(2); }
1815
+ }
1816
+ const r = resetPasswordCore(root, { password: cur, recoveryKey: rk, newPassword: np });
1817
+ console.log(r.text);
1818
+ if (r.error) process.exit(2);
1819
+ }
1820
+
1821
+ function keyAuthorizeCore(root, owner, password) {
1822
+ if (!isEncrypted(root)) return { error: true, text: '记忆库未启用加密。' };
1823
+ const salt = loadSalt(root);
1824
+ if (!salt) return { error: true, text: '密钥库损坏(缺 keys/salt)。' };
1825
+ const umk = deriveUmk(password, salt);
1826
+ let ok = null;
1827
+ try { ok = unwrapOwnerKey(root, owner, umk); } catch (e) { ok = null; }
1828
+ if (!ok) {
1829
+ let rk = null;
1830
+ try { rk = unwrapRecoveryEnc(root, umk); } catch (e) { rk = null; }
1831
+ if (!rk) return { error: true, text: '口令错误。' };
1832
+ ok = wrapOwnerKey(root, umk, rk, owner);
1833
+ }
1834
+ writeOwnerKeyCache(root, owner, ok);
1835
+ return { error: false, text: '已授权 ' + owner + ' 读取其私密(写入授权缓存 keys/cache/' + owner + '.key)。' };
1836
+ }
1837
+
1838
+ function keyRevokeCore(root, owner) {
1839
+ if (!revokeOwnerKeyCache(root, owner)) return { error: false, text: owner + ' 无授权缓存(已是未授权状态)。' };
1840
+ return { error: false, text: '已吊销 ' + owner + ' 的授权缓存(该智能体将无法解密其私密)。' };
1841
+ }
1842
+
1843
+ function cmdKeyList() {
1844
+ const root = userRoot();
1845
+ if (!isEncrypted(root)) { console.log('记忆库未启用加密。'); return; }
1846
+ const owners = collectOwners(root);
1847
+ const authorized = owners.filter(function (o) { return fs.existsSync(encCachePath(root, o)); });
1848
+ console.log('owner: ' + (owners.length ? owners.join(', ') : '(无)'));
1849
+ console.log('已授权缓存: ' + (authorized.length ? authorized.join(', ') : '(无)'));
1850
+ }
1851
+
1852
+ async function cmdKeyAuthorize(id, opts) {
1853
+ const root = userRoot();
1854
+ if (!isEncrypted(root)) { console.error('记忆库未启用加密。'); process.exit(2); }
1855
+ if (!id) { console.error('请指定 <id>:yotta-memory key authorize <id>'); process.exit(2); }
1856
+ let password = opts.password || process.env.YOTTA_MEMORY_PASS || '';
1857
+ if (!password) {
1858
+ password = await promptPassword('主口令: ') || '';
1859
+ if (!password) { console.error('已取消。'); process.exit(2); }
1860
+ }
1861
+ const r = keyAuthorizeCore(root, id, password);
1862
+ console.log(r.text);
1863
+ if (r.error) process.exit(2);
1864
+ }
1865
+
1866
+ function cmdKeyRevoke(id) {
1867
+ const root = userRoot();
1868
+ if (!id) { console.error('请指定 <id>:yotta-memory key revoke <id>'); process.exit(2); }
1869
+ const r = keyRevokeCore(root, id);
1870
+ console.log(r.text);
1871
+ if (r.error) process.exit(2);
1872
+ }
1873
+
1874
+ // v0.8.1: 查看平台分页——服务端只返回当前页(offset/limit),避免记忆多了一次全量加载/渲染/传输
1875
+ function viewEntriesCore(root, session, query, offset, limit) {
1876
+ limit = Math.max(1, parseInt(limit, 10) || 50);
1877
+ offset = Math.max(0, parseInt(offset, 10) || 0);
1878
+ const q = String(query || '').toLowerCase();
1879
+ const entries = [];
1880
+ for (const e of (loadIndex(root) || [])) entries.push(e);
1881
+ for (const o of collectOwners(root)) {
1882
+ const key = session.ownerKeys[o];
1883
+ if (!key) continue;
1884
+ try { for (const e of loadOwnerIndex(root, o, key)) entries.push(e); } catch (err) {}
1885
+ }
1886
+ const all = q ? entries.filter(function (e) {
1887
+ return ((e.subject || '') + ' ' + (e.statement || '') + ' ' + (e.tags || []).join(' ')).toLowerCase().indexOf(q) !== -1;
1888
+ }) : entries;
1889
+ all.sort(function (a, b) { return String(b.updated || b.created || '').localeCompare(String(a.updated || a.created || '')); });
1890
+ const count = all.length;
1891
+ const page = all.slice(offset, offset + limit);
1892
+ return { count: count, offset: offset, limit: limit, hasMore: offset + limit < count, entries: page };
1893
+ }
1894
+ function viewServerCore(root, port, host) {
1895
+ let session = { umk: null, ownerKeys: {} };
1896
+ const server = http.createServer(function (req, res) {
1897
+ let pathname = '/';
1898
+ try { pathname = new URL(req.url, 'http://' + (req.headers.host || 'localhost')).pathname; } catch (e) {}
1899
+ function json(code, obj) {
1900
+ res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
1901
+ res.end(JSON.stringify(obj));
1902
+ }
1903
+ function readBody(cb) {
1904
+ let body = '';
1905
+ req.on('data', function (c) { body += c; });
1906
+ req.on('end', function () {
1907
+ let d = {};
1908
+ try { d = JSON.parse(body || '{}'); } catch (e) {}
1909
+ cb(d);
1910
+ });
1911
+ }
1912
+ if (req.method === 'GET' && pathname === '/') {
1913
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1914
+ res.end(VIEW_HTML);
1915
+ return;
1916
+ }
1917
+ if (req.method === 'POST' && pathname === '/api/status') {
1918
+ return json(200, { encrypted: isEncrypted(root), unlocked: !!session.umk, version: VERSION });
1919
+ }
1920
+ if (req.method === 'POST' && pathname === '/api/unlock') {
1921
+ return readBody(function (d) {
1922
+ const password = String(d.password || '');
1923
+ const salt = loadSalt(root);
1924
+ if (!salt) return json(400, { error: '记忆库未启用加密。' });
1925
+ const umk = deriveUmk(password, salt);
1926
+ const ownerKeys = {};
1927
+ const owners = keyOwners(root);
1928
+ let verified = false;
1929
+ for (const o of owners) {
1930
+ try { ownerKeys[o] = unwrapOwnerKey(root, o, umk); verified = true; } catch (e) {}
1931
+ }
1932
+ if (!verified) return json(401, { error: '口令错误。' });
1933
+ session = { umk: umk, ownerKeys: ownerKeys };
1934
+ json(200, { ok: true, owners: Object.keys(ownerKeys) });
1935
+ });
1936
+ }
1937
+ if (req.method === 'POST' && pathname === '/api/owners') {
1938
+ if (!session.umk) return json(401, { error: '请先解锁。' });
1939
+ const agents = loadAgents(root).agents || {};
1940
+ const owners = keyOwners(root);
1941
+ const all = Array.from(new Set(owners.concat(Object.keys(agents))));
1942
+ return json(200, { owners: all.map(function (o) { return { owner: o, registered: !!agents[o], authorized: !!session.ownerKeys[o] && fs.existsSync(encCachePath(root, o)) }; }) });
1943
+ }
1944
+ if (req.method === 'POST' && pathname === '/api/entries') {
1945
+ if (!session.umk) return json(401, { error: '请先解锁。' });
1946
+ return readBody(function (d) { json(200, viewEntriesCore(root, session, d.query, d.offset, d.limit)); });
1947
+ }
1948
+ if (req.method === 'POST' && pathname === '/api/export') {
1949
+ if (!session.umk) return json(401, { error: '请先解锁。' });
1950
+ const memories = [];
1951
+ for (const e of (loadIndex(root) || [])) memories.push({ file: e.file, meta: e.meta });
1952
+ for (const o of collectOwners(root)) {
1953
+ const key = session.ownerKeys[o];
1954
+ if (!key) continue;
1955
+ try { for (const e of loadOwnerIndex(root, o, key)) memories.push({ file: e.file, meta: e.meta }); } catch (err) {}
1956
+ }
1957
+ return json(200, { format: 'yottamemory', version: 2, exported: today(), memories: memories });
1958
+ }
1959
+ if (req.method === 'POST' && pathname === '/api/authorize') {
1960
+ if (!session.umk) return json(401, { error: '请先解锁。' });
1961
+ return readBody(function (d) {
1962
+ const owner = String(d.owner || '');
1963
+ if (!owner) return json(400, { error: '缺少 owner。' });
1964
+ let ok = session.ownerKeys[owner];
1965
+ let rk = null;
1966
+ try { rk = unwrapRecoveryEnc(root, session.umk); } catch (e) { rk = null; }
1967
+ if (!ok) {
1968
+ if (!rk) return json(400, { error: '密钥库缺少恢复钥匙(recovery.key.enc),无法为新 owner 建钥。' });
1969
+ ok = wrapOwnerKey(root, session.umk, rk, owner);
1970
+ session.ownerKeys[owner] = ok;
1971
+ }
1972
+ writeOwnerKeyCache(root, owner, ok);
1973
+ json(200, { ok: true });
1974
+ });
1975
+ }
1976
+ if (req.method === 'POST' && pathname === '/api/revoke') {
1977
+ if (!session.umk) return json(401, { error: '请先解锁。' });
1978
+ return readBody(function (d) {
1979
+ revokeOwnerKeyCache(root, String(d.owner || ''));
1980
+ json(200, { ok: true });
1981
+ });
1982
+ }
1983
+ if (req.method === 'POST' && pathname === '/api/reset-password') {
1984
+ if (!session.umk) return json(401, { error: '请先解锁。' });
1985
+ return readBody(function (d) {
1986
+ const r = resetPasswordCore(root, { password: String(d.currentPassword || ''), recoveryKey: String(d.recoveryKey || ''), newPassword: String(d.newPassword || '') });
1987
+ if (r.error) return json(400, { error: r.text });
1988
+ const salt = loadSalt(root);
1989
+ if (salt) session = { umk: deriveUmk(String(d.newPassword || ''), salt), ownerKeys: {} };
1990
+ json(200, { ok: true, text: r.text });
1991
+ });
1992
+ }
1993
+ if (req.method === 'POST' && pathname === '/api/recovery-key') {
1994
+ if (!session.umk) return json(401, { error: '请先解锁。' });
1995
+ const rk = unwrapRecoveryEnc(root, session.umk);
1996
+ if (!rk) return json(400, { error: '未找到恢复钥匙。' });
1997
+ return json(200, { recoveryKey: rk.toString('base64') });
1998
+ }
1999
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
2000
+ res.end('not found');
2001
+ });
2002
+ server.listen(port, host, function () {
2003
+ console.log('yotta-memory 用户查看平台已启动(v' + VERSION + ')');
2004
+ console.log('URL: http://' + host + ':' + port + '(默认仅本机 127.0.0.1;远程需 --host 显式开启)');
2005
+ console.log('记忆库: ' + root);
2006
+ console.log('输入主口令解锁后可浏览 / 搜索 / 导出全部 AI 的记忆(含私密)。');
2007
+ console.log('按 Ctrl+C 停止');
2008
+ });
2009
+ return server;
2010
+ }
2011
+
2012
+ function cmdView(opts) {
2013
+ const root = userRoot();
2014
+ ensureInit(root);
2015
+ if (!isEncrypted(root)) {
2016
+ console.error('记忆库未启用加密(无 keys/)。yotta-memory view 是加密库的用户查看平台;请先 init --encrypt 或 migrate。');
2017
+ process.exit(2);
2018
+ }
2019
+ const host = opts.host || '127.0.0.1';
2020
+ const port = opts.port || 8788;
2021
+ viewServerCore(root, port, host);
2022
+ }
2023
+
2024
+ 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'){off=0;load()}\">\n <button onclick=\"off=0;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=\"meta\" style=\"margin-top:10px;font-size:12px;color:#666\"></div>\n <div id=\"entries\" style=\"margin-top:6px\"></div>\n <div id=\"pager\" style=\"margin-top:10px\">\n <button class=\"ghost\" id=\"prevb\" onclick=\"prevPage()\">上一页</button>\n <span id=\"pageinfo\" style=\"font-size:12px;color:#888;margin:0 8px\"></span>\n <button class=\"ghost\" id=\"nextb\" onclick=\"nextPage()\">下一页</button>\n </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}\nlet off=0,PS=50;\nasync function load(){const d=await api('/api/entries',{query:document.getElementById('q').value,offset:off,limit:PS});const meta=document.getElementById('meta');const pg=document.getElementById('pageinfo');if(meta)meta.textContent='共 '+d.count+' 条';const lim=d.limit||PS;const totalPg=Math.max(1,Math.ceil(d.count/lim));const curPg=Math.floor((d.offset||0)/lim)+1;if(pg)pg.textContent='第 '+curPg+' / '+totalPg+' 页';const box=document.getElementById('entries');box.innerHTML='';if(d.entries)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);}const pb=document.getElementById('prevb'),nb=document.getElementById('nextb');if(pb)pb.disabled=(d.offset||0)<=0;if(nb)nb.disabled=!d.hasMore;}\nfunction prevPage(){if(off>=PS){off-=PS;load();}}\nfunction nextPage(){off+=PS;load();}\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>";
2025
+
615
2026
  // ---- 命令包装(CLI 入口)----
616
- function cmdInit(opts) {
617
- const r = initCore(opts);
2027
+ async function cmdInit(opts) {
2028
+ const root = opts.dir ? path.resolve(String(opts.dir)) : (opts.project ? projectRoot() : userRoot());
2029
+ const fresh = !fs.existsSync(path.join(root, PUBLIC_DIR)) && !fs.existsSync(path.join(root, PRIVATE_DIR)) && !fs.existsSync(path.join(root, INDEX_FILE));
2030
+ const wantEncrypt = opts.encrypt || (!opts.noEncrypt && fresh);
2031
+ let o = Object.assign({}, opts);
2032
+ if (wantEncrypt && !o.password && !process.env.YOTTA_MEMORY_PASS) {
2033
+ const pw = await promptPassword('主口令(启用加密,勿忘;忘口令可用恢复钥匙重设): ');
2034
+ if (!pw) { console.error('已取消。'); process.exit(2); }
2035
+ const pw2 = await promptPassword('再次输入确认: ');
2036
+ if (pw2 !== pw) { console.error('两次输入不一致。'); process.exit(2); }
2037
+ o.password = pw;
2038
+ }
2039
+ const r = initCore(o);
618
2040
  console.log(r.text);
2041
+ if (r.error) process.exit(2);
619
2042
  }
620
2043
  function cmdRemember(type, subject, statement, opts) {
621
2044
  const o = Object.assign({}, opts);
@@ -629,6 +2052,36 @@ function cmdRecall(query, opts) {
629
2052
  console.log(r.text);
630
2053
  if (r.exitCode) process.exit(r.exitCode);
631
2054
  }
2055
+ function cmdFeedback(ref, opts) {
2056
+ const r = feedbackCore(ref, opts);
2057
+ console.log(r.text);
2058
+ if (r.exitCode) process.exit(r.exitCode);
2059
+ if (r.error) process.exit(2);
2060
+ }
2061
+ function cmdExplain(ref, opts) {
2062
+ const r = explainCore(ref, opts);
2063
+ console.log(r.text);
2064
+ if (r.exitCode) process.exit(r.exitCode);
2065
+ if (r.error) process.exit(2);
2066
+ }
2067
+ function cmdMaintain(opts) {
2068
+ if (opts.merge) {
2069
+ const parts = String(opts.merge).split(',').map(function (s) { return s.trim(); }).filter(Boolean);
2070
+ if (parts.length < 2) { console.error('--merge 需要两个文件,逗号分隔'); process.exit(2); }
2071
+ const r = mergeCore(parts[0], parts[1], opts);
2072
+ console.log(r.text);
2073
+ if (r.error) process.exit(2);
2074
+ return;
2075
+ }
2076
+ const r = maintainCore(opts);
2077
+ console.log(r.text);
2078
+ if (r.error) process.exit(2);
2079
+ }
2080
+ function cmdDistill(opts) {
2081
+ const r = distillCore(opts);
2082
+ console.log(r.text);
2083
+ if (r.error) process.exit(2);
2084
+ }
632
2085
  function cmdForget(fileRef) {
633
2086
  const r = forgetCore(fileRef);
634
2087
  console.log(r.text);
@@ -648,17 +2101,23 @@ function cmdReindex() {
648
2101
  }
649
2102
  function collectAll(root) {
650
2103
  const out = [];
2104
+ const skipped = {};
651
2105
  for (const fp of collectEntryFiles(root)) {
652
- const e = readEntry(fp, root);
653
- out.push({ file: e.file, meta: e.meta });
2106
+ const owner = ownerFromPrivatePath(root, fp);
2107
+ if (isEncFile(fp) && !getOwnerKeyFor(root, owner)) { skipped[owner] = true; continue; }
2108
+ try {
2109
+ const e = readEntry(fp, root);
2110
+ out.push({ file: e.file, meta: e.meta });
2111
+ } catch (err) { if (owner) skipped[owner] = true; }
654
2112
  }
655
- return out;
2113
+ return { memories: out, skipped: Object.keys(skipped) };
656
2114
  }
657
2115
  function exportCore(root, outPath) {
658
- const data = { format: 'yottamemory', version: 1, exported: today(), memories: collectAll(root) };
2116
+ const res = collectAll(root);
2117
+ const data = { format: 'yottamemory', version: 2, exported: today(), memories: res.memories, skippedOwners: res.skipped };
659
2118
  const target = outPath || path.join(root, 'yottamemory-export-' + today() + '.json');
660
2119
  fs.writeFileSync(target, JSON.stringify(data, null, 2), 'utf8');
661
- return { error: false, text: '已导出 ' + data.memories.length + ' 条记忆 -> ' + target };
2120
+ return { error: false, text: '已导出 ' + data.memories.length + ' 条记忆 -> ' + target + (res.skipped.length ? '(跳过无授权私密 owner: ' + res.skipped.join(', ') + ')' : '') };
662
2121
  }
663
2122
  function importCore(root, src) {
664
2123
  if (!src) return { error: true, text: '请提供 JSON 文件路径' };
@@ -673,16 +2132,21 @@ function importCore(root, src) {
673
2132
  catch (e) { return { error: true, text: 'JSON 解析失败: ' + (e && e.message ? e.message : String(e)) }; }
674
2133
  if (!data.memories || !Array.isArray(data.memories)) return { error: true, text: 'JSON 格式不正确(缺少 memories 数组)' };
675
2134
  ensureInit(root);
676
- let m = 0;
2135
+ const encrypted = isEncrypted(root);
2136
+ let m = 0, skippedPriv = 0;
677
2137
  for (const item of data.memories) {
678
2138
  const meta = item.meta || {};
679
2139
  const t = (meta.type || 'FACT').toUpperCase();
680
2140
  if (!TYPE_DIRS[t]) continue;
681
- const dir = path.join(root, typeSubdir(t, meta.owner || ''));
2141
+ const owner = meta.owner || '';
2142
+ const scope = meta.scope || defaultScope(t);
2143
+ if (encrypted && scope === 'private' && !getOwnerKeyFor(root, owner)) { skippedPriv++; continue; }
2144
+ const dir = path.join(root, typeSubdir(t, owner));
682
2145
  fs.mkdirSync(dir, { recursive: true });
683
2146
  let seq = nextSeq(dir);
684
- let file = path.join(dir, today() + '-' + seq + '.md');
685
- while (fs.existsSync(file)) { seq = String(parseInt(seq, 10) + 1).padStart(4, '0'); file = path.join(dir, today() + '-' + seq + '.md'); }
2147
+ const suffix = (encrypted && scope === 'private') ? ENC_SUFFIX : '';
2148
+ let file = path.join(dir, today() + '-' + seq + '.md' + suffix);
2149
+ while (fs.existsSync(file)) { seq = String(parseInt(seq, 10) + 1).padStart(4, '0'); file = path.join(dir, today() + '-' + seq + '.md' + suffix); }
686
2150
  const rec = {
687
2151
  type: t,
688
2152
  subject: meta.subject || '',
@@ -694,16 +2158,16 @@ function importCore(root, src) {
694
2158
  immutable: meta.immutable === true || meta.immutable === 'true',
695
2159
  source: meta.source || '',
696
2160
  weight: (parseFloat(meta.weight) > 0 ? parseFloat(meta.weight) : 1.0),
697
- scope: meta.scope || defaultScope(t),
698
- owner: meta.owner || '',
2161
+ scope: scope,
2162
+ owner: owner,
699
2163
  access_count: parseInt(meta.access_count || '0', 10) || 0,
700
2164
  last_accessed: meta.last_accessed || '',
701
2165
  };
702
- fs.writeFileSync(file, frontmatterToText(rec, rec.statement), 'utf8');
2166
+ writeMemoryText(root, file, frontmatterToText(rec, rec.statement), owner);
703
2167
  upsertIndexEntry(root, readEntry(file, root));
704
2168
  m++;
705
2169
  }
706
- return { error: false, text: '已导入 ' + m + ' 条记忆 -> ' + root };
2170
+ return { error: false, text: '已导入 ' + m + ' 条记忆 -> ' + root + (skippedPriv ? '(跳过 ' + skippedPriv + ' 条无授权私密)' : '') };
707
2171
  }
708
2172
  function cmdExport(outPath) {
709
2173
  const r = exportCore(userRoot(), outPath || 'yottamemory-export-' + today() + '.json');
@@ -777,9 +2241,12 @@ function findSelfProfile(root, agentId) {
777
2241
  const dir = selfPrefsDir(root, agentId);
778
2242
  if (!fs.existsSync(dir)) return '';
779
2243
  for (const f of fs.readdirSync(dir)) {
2244
+ if (!/\.md(\.enc)?$/.test(f)) continue;
780
2245
  const fp = path.join(dir, f);
781
2246
  if (!fs.statSync(fp).isFile()) continue;
782
- const meta = parseFrontmatter(fs.readFileSync(fp, 'utf8')).meta;
2247
+ let text;
2248
+ try { text = readMemoryText(root, fp, agentId); } catch (err) { continue; }
2249
+ const meta = parseFrontmatter(text).meta;
783
2250
  if (meta.subject === SELF_PROFILE_SUBJECT && (meta.owner || '') === agentId) return f;
784
2251
  }
785
2252
  return '';
@@ -809,11 +2276,11 @@ function writeSelfProfile(root, agentId, extra) {
809
2276
  extra = extra || {};
810
2277
  const dir = selfPrefsDir(root, agentId);
811
2278
  fs.mkdirSync(dir, { recursive: true });
2279
+ const encrypted = isEncrypted(root);
812
2280
  const existing = findSelfProfile(root, agentId);
813
2281
  if (existing) {
814
- // 已有自我档案:合并 kv 字段更新(不重复建文件)
815
2282
  const fp = path.join(dir, existing);
816
- const parsed = parseFrontmatter(fs.readFileSync(fp, 'utf8'));
2283
+ const parsed = parseFrontmatter(readMemoryText(root, fp, agentId));
817
2284
  const kv = parseKvBody(parsed.body);
818
2285
  kv.agent_id = agentId;
819
2286
  kv.host = os.hostname();
@@ -828,12 +2295,12 @@ function writeSelfProfile(root, agentId, extra) {
828
2295
  const meta = Object.assign({}, parsed.meta);
829
2296
  meta.updated = today();
830
2297
  meta.statement = body;
831
- fs.writeFileSync(fp, frontmatterToText(meta, body), 'utf8');
2298
+ writeMemoryText(root, fp, frontmatterToText(meta, body), agentId);
832
2299
  upsertIndexEntry(root, readEntry(fp, root));
833
2300
  return fp;
834
2301
  }
835
2302
  const seq = nextSeq(dir);
836
- const file = path.join(dir, today() + '-' + seq + '.md');
2303
+ const file = path.join(dir, today() + '-' + seq + '.md' + (encrypted ? ENC_SUFFIX : ''));
837
2304
  const body = selfProfileBody(agentId, root, extra);
838
2305
  const rec = {
839
2306
  type: 'PREF', subject: SELF_PROFILE_SUBJECT, statement: body,
@@ -841,7 +2308,7 @@ function writeSelfProfile(root, agentId, extra) {
841
2308
  tags: ['自我档案'], immutable: false,
842
2309
  scope: 'private', owner: agentId, access_count: 0, last_accessed: '',
843
2310
  };
844
- fs.writeFileSync(file, frontmatterToText(rec, body), 'utf8');
2311
+ writeMemoryText(root, file, frontmatterToText(rec, body), agentId);
845
2312
  upsertIndexEntry(root, readEntry(file, root));
846
2313
  return file;
847
2314
  }
@@ -897,7 +2364,7 @@ function cmdWhoami() {
897
2364
  console.log('登记状态: ' + reg);
898
2365
  console.log('自我档案: ' + (profile ? profile : '未写入(请执行 yotta-memory iam ' + id + ')'));
899
2366
  if (profile) {
900
- const kv = parseKvBody(parseFrontmatter(fs.readFileSync(path.join(selfPrefsDir(root, id), profile), 'utf8')).body);
2367
+ const kv = selfProfileKv(root, id);
901
2368
  if (kv.agent_name) console.log('显示名: ' + kv.agent_name);
902
2369
  if (kv.user_name) console.log('用户: ' + kv.user_name);
903
2370
  if (kv.relationship) console.log('关系: ' + kv.relationship);
@@ -913,9 +2380,11 @@ function profileGroups(root, owner) {
913
2380
  const dir = path.join(root, PRIVATE_DIR, owner, t);
914
2381
  if (!fs.existsSync(dir)) continue;
915
2382
  for (const f of fs.readdirSync(dir)) {
2383
+ if (!/\.md(\.enc)?$/.test(f)) continue;
916
2384
  const fp = path.join(dir, f);
917
2385
  if (!fs.statSync(fp).isFile()) continue;
918
- const e = readEntry(fp, root);
2386
+ let e;
2387
+ try { e = readEntry(fp, root); } catch (err) { continue; }
919
2388
  const key = e.type + '\u0000' + (e.subject || '') + '\u0000' + e.tags.join(',');
920
2389
  if (!map[key]) {
921
2390
  map[key] = { type: e.type, subject: e.subject || '', tags: e.tags || [], items: [] };
@@ -940,6 +2409,9 @@ function profileCore(opts) {
940
2409
  }
941
2410
  const ownerDir = path.join(root, PRIVATE_DIR, owner);
942
2411
  if (!fs.existsSync(ownerDir)) return { error: false, text: '该智能体(' + owner + ')暂无画像:private/' + owner + '/ 不存在或为空。' };
2412
+ if (isEncrypted(root) && !getOwnerKeyFor(root, owner)) {
2413
+ return { error: true, exitCode: 3, text: '私密区已加密:当前无 ' + owner + ' 的授权密钥,请在用户平台授权(yotta-memory view → 授权本智能体)后生成画像。' };
2414
+ }
943
2415
  const groups = profileGroups(root, owner);
944
2416
  if (!groups.length) return { error: false, text: '该智能体(' + owner + ')暂无画像:private/' + owner + '/ 下无 PREF / BOUND / COMMIT 条目。' };
945
2417
  const lines = [];
@@ -959,8 +2431,7 @@ function profileCore(opts) {
959
2431
  lines.push('');
960
2432
  }
961
2433
  const text = lines.join('\n');
962
- const outFile = path.join(ownerDir, PROFILE_FILE);
963
- fs.writeFileSync(outFile, text + '\n', 'utf8');
2434
+ const outFile = writeProfileText(root, owner, text + '\n');
964
2435
  return { error: false, text: text + '\n\n[已写入] ' + outFile };
965
2436
  }
966
2437
  function cmdProfile(opts) {
@@ -974,7 +2445,9 @@ function cmdProfile(opts) {
974
2445
  function selfProfileKv(root, id) {
975
2446
  const profile = findSelfProfile(root, id);
976
2447
  if (!profile) return {};
977
- return parseKvBody(parseFrontmatter(fs.readFileSync(path.join(selfPrefsDir(root, id), profile), 'utf8')).body);
2448
+ let text;
2449
+ try { text = readMemoryText(root, path.join(selfPrefsDir(root, id), profile), id); } catch (e) { return {}; }
2450
+ return parseKvBody(parseFrontmatter(text).body);
978
2451
  }
979
2452
  function contextCore(opts) {
980
2453
  opts = opts || {};
@@ -987,6 +2460,7 @@ function contextCore(opts) {
987
2460
  const unsafe = !!opts.unsafe;
988
2461
  const budget = opts.budget ? parseInt(opts.budget, 10) : 0;
989
2462
  const lines = [];
2463
+ const FENCE = String.fromCharCode(96, 96, 96);
990
2464
  function usedChars() { return lines.reduce(function (s, x) { return s + String(x).length + 1; }, 0); }
991
2465
  lines.push('# 开工上下文包(yotta-memory context)');
992
2466
  lines.push('');
@@ -1013,18 +2487,22 @@ function contextCore(opts) {
1013
2487
  lines.push('## 2. 用户画像摘要');
1014
2488
  lines.push('');
1015
2489
  if (owner) {
1016
- const pf = path.join(root, PRIVATE_DIR, owner, PROFILE_FILE);
1017
- if (fs.existsSync(pf)) {
1018
- lines.push('```markdown');
1019
- lines.push(fs.readFileSync(pf, 'utf8').trim());
1020
- lines.push('```');
2490
+ let pf = profilePathFor(root, owner);
2491
+ let pfText = null;
2492
+ if (fs.existsSync(pf)) { try { pfText = readProfileText(root, owner); } catch (e) { pfText = null; } }
2493
+ if (pfText !== null && pfText !== undefined) {
2494
+ lines.push(FENCE + 'markdown');
2495
+ lines.push(pfText.trim());
2496
+ lines.push(FENCE);
1021
2497
  } else {
1022
2498
  const pr = profileCore({ owner: owner, unsafe: unsafe, selfAgent: selfAgent });
2499
+ pf = profilePathFor(root, owner);
1023
2500
  if (!pr.error && pr.text.indexOf('[已写入]') !== -1 && fs.existsSync(pf)) {
1024
2501
  lines.push('(画像不存在,已自动生成一次,见下)');
1025
- lines.push('```markdown');
1026
- lines.push(fs.readFileSync(pf, 'utf8').trim());
1027
- lines.push('```');
2502
+ lines.push(FENCE + 'markdown');
2503
+ let t2 = null; try { t2 = readProfileText(root, owner); } catch (e) {}
2504
+ lines.push((t2 === null || t2 === undefined) ? '' : t2.trim());
2505
+ lines.push(FENCE);
1028
2506
  } else {
1029
2507
  lines.push('(该身份暂无画像;可运行 yotta-memory profile 生成)');
1030
2508
  }
@@ -1039,7 +2517,8 @@ function contextCore(opts) {
1039
2517
  for (const r of roots) {
1040
2518
  for (const e of ensureIndex(r)) {
1041
2519
  if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
1042
- recent.push({ e: e, s: importanceScore(e) });
2520
+ // v0.8.0 排序融合:0.5×importance(旧) + 0.5×utility(新盖棺分)
2521
+ recent.push({ e: e, s: 0.5 * importanceScore(e) + 0.5 * utilityScore(e) });
1043
2522
  }
1044
2523
  }
1045
2524
  recent.sort(function (a, b) { return b.s - a.s; });
@@ -1076,6 +2555,21 @@ function contextCore(opts) {
1076
2555
  }
1077
2556
  if (!commits.length) lines.push('(无)');
1078
2557
  for (const e of commits) lines.push('- ' + (e.subject || '承诺') + ': ' + e.statement);
2558
+ lines.push('');
2559
+ lines.push('## 6. 收工纪律');
2560
+ lines.push('');
2561
+ const allEntries = [];
2562
+ for (const r of roots) for (const e of ensureIndex(r)) allEntries.push(e);
2563
+ const myCommits = allEntries.filter(function (e) { return e.type === 'COMMIT' && classifyRead(e, owner, '', unsafe, selfAgent) !== 'denied'; });
2564
+ if (!myCommits.length) {
2565
+ lines.push('- 最近承诺: 无(收工请补 COMMIT / 交接锚点)');
2566
+ } else {
2567
+ const latest = myCommits.map(function (e) { return e.updated || e.created || ''; }).sort().pop();
2568
+ const d = daysBetween(latest, today());
2569
+ lines.push('- 最近承诺: ' + latest + (d > 7 ? '(超期 ' + d + ' 天,请补 COMMIT)' : ''));
2570
+ }
2571
+ const oldCount = allEntries.filter(function (e) { return e.created && daysBetween(e.created, today()) > 180 && classifyRead(e, owner, '', unsafe, selfAgent) !== 'denied'; }).length;
2572
+ if (oldCount > 0) lines.push('- 归档提醒: ' + oldCount + ' 条超 180 天,建议 archive');
1079
2573
  return { error: false, exitCode: 0, text: lines.join('\n') };
1080
2574
  }
1081
2575
  function cmdContext(opts) {
@@ -1113,6 +2607,10 @@ function mcpTools() {
1113
2607
  { name: 'import', description: '从引擎主机上的 JSON 文件导入记忆。src 为文件路径(可绝对路径,或相对记忆库目录)', inputSchema: { type: 'object', properties: { src: { type: 'string' } }, required: ['src'] } },
1114
2608
  { name: 'agent_info', description: '查看当前智能体身份与登记状态(远端读经 token 校验的 X-Agent-Id;本机读 YOTTA_AGENT_ID)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
1115
2609
  { name: 'profile', description: '生成当前智能体的用户画像(只读聚合 private/<owner>/ 下 PREF/BOUND/COMMIT 原文,零推断,写入 private/<owner>/profile.md)。owner 默认当前智能体', inputSchema: { type: 'object', properties: { owner: { type: 'string' } } } },
2610
+ { 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'] } },
2611
+ { 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' } } } },
2612
+ { name: 'distill', description: '心理日志蒸馏(自我提升):统计摘要/主题画像/知识地图。owner 默认当前智能体', inputSchema: { type: 'object', properties: { owner: { type: 'string' }, subject: { type: 'string' }, model: { type: 'string' } } } },
2613
+ { name: 'explain', description: '解释单条记忆效用分项(为什么靠前/归档/遗忘)。file 为记忆文件路径或文件名', inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] } },
1116
2614
  ];
1117
2615
  }
1118
2616
  function callTool(name, args, ctx) {
@@ -1174,6 +2672,22 @@ function callTool(name, args, ctx) {
1174
2672
  const r = profileCore({ owner: ownerArg || agent, selfAgent: agent });
1175
2673
  return { text: r.text, error: r.error };
1176
2674
  }
2675
+ if (name === 'feedback') {
2676
+ const r = feedbackCore(String(args.file || ''), { selfAgent: agent, useful: !!args.useful, useless: !!args.useless, reason: args.reason ? String(args.reason) : '' });
2677
+ return { text: r.text, error: r.error };
2678
+ }
2679
+ if (name === 'maintain') {
2680
+ const r = maintainCore({ selfAgent: agent, apply: !!args.apply, purge: !!args.purge, dedup: !!args.dedup, threshold: args.threshold, age: args.age });
2681
+ return { text: r.text, error: r.error };
2682
+ }
2683
+ if (name === 'distill') {
2684
+ const r = distillCore({ selfAgent: agent, owner: args.owner ? String(args.owner) : agent, subject: args.subject ? String(args.subject) : '', model: args.model ? String(args.model) : '' });
2685
+ return { text: r.text, error: r.error };
2686
+ }
2687
+ if (name === 'explain') {
2688
+ const r = explainCore(String(args.file || ''), { selfAgent: agent });
2689
+ return { text: r.text, error: r.error };
2690
+ }
1177
2691
  return { text: '未知工具: ' + name, error: true };
1178
2692
  } catch (e) {
1179
2693
  return { text: '错误: ' + (e && e.message ? e.message : String(e)), error: true };
@@ -1260,7 +2774,7 @@ function cmdServe(opts) {
1260
2774
  });
1261
2775
  }
1262
2776
 
1263
- // ---- stdio 本地零进程模式(照灵魂盘:客户端按需拉起 CLI)----
2777
+ // ---- stdio 本地零进程模式(客户端按需拉起 CLI)----
1264
2778
  function cmdServeStdio() {
1265
2779
  const root = userRoot();
1266
2780
  ensureInit(root);
@@ -1715,44 +3229,59 @@ function cmdLanStatus() {
1715
3229
  }
1716
3230
  // ---- usage / main ----
1717
3231
  function usage() {
1718
- console.log([
1719
- 'yotta-memory v' + VERSION + ' — 元忆:有权限边界的文件式智能体记忆',
1720
- '',
1721
- '用法:',
1722
- ' yotta-memory init [--project] [--dir <目录>] 初始化记忆库(默认用户级;--dir 显式指定位置)',
1723
- ' yotta-memory config set memory_home <目录> 持久记住记忆库位置',
1724
- ' yotta-memory config get 查看当前配置与生效位置',
1725
- ' yotta-memory remember <type> <subject> <statement> [--owner <id>] [--source <来源>] [--weight <0..>] [--verify] [--no-hint] 写入记忆(--source 记录来源;--weight 重要性权重默认 1.0 去重取 max;--verify 写后回读校验;--no-hint 关闭类型启发式提示)',
1726
- ' yotta-memory profile [--owner <id>] 生成用户画像(聚合 private/<owner>/ 原文,零推断,写 profile.md)',
1727
- ' yotta-memory context [--limit N] [--owner <id>] [--budget N] 生成开工上下文包(身份+铁律+画像+近期记忆+边界+承诺;--budget 近期记忆字符预算,0=不限)',
1728
- ' yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe] 检索(索引+TF,读取分区;--agent 仅表示“以该身份检索/模拟”,跨智能体私密读取需 --unsafe / 授权)',
1729
- ' yotta-memory forget <文件或文件名> 删除一条记忆',
1730
- ' yotta-memory archive [--days 180] [--threshold 0.4] 归档(盖棺分+年龄)',
1731
- ' yotta-memory reindex 重建索引',
1732
- ' yotta-memory export [--out 文件.json] 导出全部记忆',
1733
- ' yotta-memory import <文件.json> 导入记忆',
1734
- ' yotta-memory whoami 查看当前智能体身份与登记状态(读 YOTTA_AGENT_ID / X-Agent-Id,不猜不默认)',
1735
- ' yotta-memory iam <id> [--name <显示名>] [--user <用户名>] [--relationship <关系>] [--force] 登记本智能体唯一身份并自动落自我档案(agents.json;ID 必须唯一)',
1736
- ' yotta-memory token new --agent <id> [--force] 生成/重置某智能体访问 token(同 ID 已被其它来源占用需 --force 覆盖)',
1737
- ' yotta-memory token list 列出已登记智能体',
1738
- ' yotta-memory token revoke --agent <id> 吊销某智能体 token',
1739
- ' yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio] 启动 MCP 记忆引擎(streamable HTTP;--stdio 本地零进程模式)',
1740
- ' yotta-memory lan enable [--onstart] | disable | status 开机自启管理(Windows:计划任务,默认 ONLOGON,非管理员自动降级用户级 Startup 静默自启;Linux:systemd 用户单元,不可用时自动降级用户 crontab @reboot)',
1741
- ' yotta-memory --version 版本',
1742
- '',
1743
- '类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)',
1744
- '环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; YOTTA_AGENT_ID/AGENT_ID 当前 agent 标识(本机声明身份用;私密记忆必须有 owner)',
1745
- '隔离: 公共 FACT 在 facts/;私密 PREF/BOUND/COMMIT 物理分目录 private/<agent_id>/<type>/,禁止 shell 直读写记忆库,一律走本命令',
1746
- '远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id>',
1747
- '',
1748
- ].join('\n'));
3232
+ const banner = 'yotta-memory v' + VERSION + ' — 元忆:有权限边界的文件式智能体记忆';
3233
+ const sections = [
3234
+ ['核心记忆', [
3235
+ ['init', '初始化记忆库(新建默认加密:需主口令 + 恢复钥匙;--no-encrypt 降级明文)'],
3236
+ ['remember', '写入记忆(--source 来源;--weight 权重;--verify 写后回读校验;--no-hint 关启发)'],
3237
+ ['recall', '检索记忆(--type/--limit/--agent/--owner/--all/--unsafe)'],
3238
+ ['forget', '删除一条记忆'],
3239
+ ['archive', '归档(--days/--threshold 盖棺分+年龄)'],
3240
+ ['reindex', '重建索引'],
3241
+ ['export', '导出全部记忆(--out 文件.json)'],
3242
+ ['import', '导入记忆(<文件.json>)']
3243
+ ]],
3244
+ ['身份与画像', [
3245
+ ['iam', '登记本智能体唯一身份(--name/--user/--relationship;agents.json)'],
3246
+ ['whoami', '查看当前智能体身份与登记状态'],
3247
+ ['profile', '生成用户画像(聚合 private/<owner>/ 原文,零推断)'],
3248
+ ['context', '生成开工上下文包(--limit/--owner/--budget)'],
3249
+ ['token', '生成/列出/吊销访问 token(new --agent / list / revoke --agent)']
3250
+ ]],
3251
+ ['加密与安全', [
3252
+ ['migrate', '把明文私密区迁移为密文(需主口令;迁移后打印恢复钥匙)'],
3253
+ ['view', '启动用户查看平台(--port/--host;口令解锁浏览/授权/吊销 AI)'],
3254
+ ['reset-password', '重设主口令(忘口令用恢复钥匙)'],
3255
+ ['key', '管理 AI 私密读取授权缓存(list / authorize <id> / revoke <id>)'],
3256
+ ['config', '查看/记住记忆库位置(get / set memory_home <目录>)']
3257
+ ]],
3258
+ ['平台与服务', [
3259
+ ['serve', '启动 MCP 记忆引擎(streamable HTTP;--stdio 本地零进程)'],
3260
+ ['lan', '开机自启管理(enable/disable/status;Windows 计划任务 / Linux systemd/crontab)'],
3261
+ ['--version', '版本']
3262
+ ]]
3263
+ ];
3264
+ let col = 0;
3265
+ for (const s of sections) for (const r of s[1]) if (r[0].length > col) col = r[0].length;
3266
+ col += 2;
3267
+ const lines = [banner, '', '用法:', ' yotta-memory <命令> [选项]', ''];
3268
+ for (const s of sections) {
3269
+ lines.push(s[0] + ':');
3270
+ for (const r of s[1]) lines.push(' ' + r[0].padEnd(col) + r[1]);
3271
+ lines.push('');
3272
+ }
3273
+ lines.push('类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)');
3274
+ lines.push('环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; YOTTA_AGENT_ID/AGENT_ID 当前 agent 标识(本机声明身份用;私密记忆必须有 owner)');
3275
+ lines.push('隔离: 公共 FACT 在 facts/;私密 PREF/BOUND/COMMIT 物理分目录 private/<agent_id>/<type>/,禁止 shell 直读写记忆库,一律走本命令');
3276
+ lines.push('远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id>');
3277
+ console.log(lines.join('\n'));
1749
3278
  }
1750
- function main() {
3279
+ async function main() {
1751
3280
  const args = process.argv.slice(2);
1752
3281
  if (!args.length) { usage(); return; }
1753
3282
  const opts = {};
1754
3283
  const positional = [];
1755
- const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget']);
3284
+ const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--reason', '--merge', '--model', '--subject']);
1756
3285
  for (let i = 0; i < args.length; i++) {
1757
3286
  const a = args[i];
1758
3287
  if (a === '--version' || a === '-v') { console.log(VERSION); return; }
@@ -1766,6 +3295,17 @@ function main() {
1766
3295
  else if (a === '--force') opts.force = true;
1767
3296
  else if (a === '--verify') opts.verify = true;
1768
3297
  else if (a === '--no-hint') opts.noHint = true;
3298
+ else if (a === '--encrypt') opts.encrypt = true;
3299
+ else if (a === '--no-encrypt') opts.noEncrypt = true;
3300
+ else if (a === '--useful') opts.useful = true;
3301
+ else if (a === '--useless') opts.useless = true;
3302
+ else if (a === '--undo') opts.undo = true;
3303
+ else if (a === '--dry-run') opts.dryRun = true;
3304
+ else if (a === '--apply') opts.apply = true;
3305
+ else if (a === '--purge') opts.purge = true;
3306
+ else if (a === '--dedup') opts.dedup = true;
3307
+ else if (a === '--explain') opts.explain = true;
3308
+ else if (a === '--semantic') opts.semantic = true;
1769
3309
  else if (valueOpts.has(a)) {
1770
3310
  const v = args[++i];
1771
3311
  if (a === '--type') opts.type = v;
@@ -1785,6 +3325,9 @@ function main() {
1785
3325
  else if (a === '--source') opts.source = v;
1786
3326
  else if (a === '--weight') opts.weight = parseFloat(v);
1787
3327
  else if (a === '--budget') opts.budget = parseInt(v, 10) || 0;
3328
+ else if (a === '--password') opts.password = v;
3329
+ else if (a === '--new-password') opts.newPassword = v;
3330
+ else if (a === '--recovery-key') opts.recoveryKey = v;
1788
3331
  } else if (a.startsWith('--')) {
1789
3332
  console.error('未知选项: ' + a);
1790
3333
  process.exit(2);
@@ -1795,7 +3338,7 @@ function main() {
1795
3338
  const first = positional[0];
1796
3339
  const rest = positional.slice(1);
1797
3340
  switch (first) {
1798
- case 'init': cmdInit(opts); break;
3341
+ case 'init': await cmdInit(opts); break;
1799
3342
  case 'whoami': cmdWhoami(); break;
1800
3343
  case 'iam': cmdIam(rest[0], opts); break;
1801
3344
  case 'config': {
@@ -1807,6 +3350,11 @@ function main() {
1807
3350
  }
1808
3351
  case 'remember': cmdRemember(rest[0], rest[1], rest[2], opts); break;
1809
3352
  case 'recall': cmdRecall(rest[0] || null, opts); break;
3353
+ case 'search': cmdRecall(rest[0] || null, opts); break;
3354
+ case 'feedback': cmdFeedback(rest[0], opts); break;
3355
+ case 'explain': cmdExplain(rest[0], opts); break;
3356
+ case 'maintain': cmdMaintain(opts); break;
3357
+ case 'distill': cmdDistill(opts); break;
1810
3358
  case 'forget': cmdForget(rest[0]); break;
1811
3359
  case 'archive': cmdArchive(opts); break;
1812
3360
  case 'reindex': cmdReindex(); break;
@@ -1822,6 +3370,17 @@ function main() {
1822
3370
  else { console.error('token 子命令: new --agent <id> / list / revoke --agent <id>'); process.exit(2); }
1823
3371
  break;
1824
3372
  }
3373
+ case 'migrate': await cmdMigrate(opts); break;
3374
+ case 'view': cmdView(opts); break;
3375
+ case 'reset-password': await cmdResetPassword(opts); break;
3376
+ case 'key': {
3377
+ const sub = rest[0];
3378
+ if (sub === 'list') cmdKeyList();
3379
+ else if (sub === 'authorize') await cmdKeyAuthorize(rest[1], opts);
3380
+ else if (sub === 'revoke') cmdKeyRevoke(rest[1]);
3381
+ else { console.error('key 子命令: list / authorize <id> / revoke <id>'); process.exit(2); }
3382
+ break;
3383
+ }
1825
3384
  case 'serve': cmdServe(opts); break;
1826
3385
  case 'lan': {
1827
3386
  const sub = rest[0];
@@ -1839,7 +3398,7 @@ function main() {
1839
3398
  }
1840
3399
  }
1841
3400
 
1842
- if (require.main === module) main();
3401
+ if (require.main === module) { main().catch(function (e) { console.error('错误: ' + (e && e.message ? e.message : String(e))); process.exit(2); }); }
1843
3402
  module.exports = {
1844
3403
  VERSION: VERSION,
1845
3404
  userRoot: userRoot,
@@ -1903,5 +3462,61 @@ module.exports = {
1903
3462
  cmdLanLinuxEnable: cmdLanLinuxEnable,
1904
3463
  cmdLanLinuxDisable: cmdLanLinuxDisable,
1905
3464
  cmdLanLinuxStatus: cmdLanLinuxStatus,
3465
+ feedbackCore: feedbackCore,
3466
+ explainCore: explainCore,
3467
+ maintainCore: maintainCore,
3468
+ mergeCore: mergeCore,
3469
+ distillCore: distillCore,
3470
+ utilityScore: utilityScore,
3471
+ utilityBreakdown: utilityBreakdown,
3472
+ semanticMatch: semanticMatch,
3473
+ pinyinTokens: pinyinTokens,
3474
+ isEncrypted: isEncrypted,
3475
+ collectOwners: collectOwners,
3476
+ hasPlaintextPrivate: hasPlaintextPrivate,
3477
+ initEncryptionCore: initEncryptionCore,
3478
+ deriveUmk: deriveUmk,
3479
+ loadSalt: loadSalt,
3480
+ wrapOwnerKey: wrapOwnerKey,
3481
+ unwrapOwnerKey: unwrapOwnerKey,
3482
+ unwrapOwnerKeyRecovery: unwrapOwnerKeyRecovery,
3483
+ unwrapRecoveryEnc: unwrapRecoveryEnc,
3484
+ writeOwnerKeyCache: writeOwnerKeyCache,
3485
+ revokeOwnerKeyCache: revokeOwnerKeyCache,
3486
+ loadOwnerKeyCache: loadOwnerKeyCache,
3487
+ getOwnerKeyFor: getOwnerKeyFor,
3488
+ encryptMemoryText: encryptMemoryText,
3489
+ decryptMemoryText: decryptMemoryText,
3490
+ readMemoryText: readMemoryText,
3491
+ writeMemoryText: writeMemoryText,
3492
+ loadOwnerIndex: loadOwnerIndex,
3493
+ saveOwnerIndex: saveOwnerIndex,
3494
+ viewEntriesCore: viewEntriesCore,
3495
+ ensureIndex: ensureIndex,
3496
+ recallPrefilter: recallPrefilter,
3497
+ tokenize: tokenize,
3498
+ buildTokens: buildTokens,
3499
+ synonymSet: synonymSet,
3500
+
3501
+ loadIndex: loadIndex,
3502
+ saveIndex: saveIndex,
3503
+ getIndex: getIndex,
3504
+ touchIndex: touchIndex,
3505
+ loadIndexManifest: loadIndexManifest,
3506
+ indexShardName: indexShardName,
3507
+ isSafeShardName: isSafeShardName,
3508
+ ownerFromPrivatePath: ownerFromPrivatePath,
3509
+ isEncFile: isEncFile,
3510
+ profilePathFor: profilePathFor,
3511
+ readProfileText: readProfileText,
3512
+ writeProfileText: writeProfileText,
3513
+ migrateCore: migrateCore,
3514
+ resetPasswordCore: resetPasswordCore,
3515
+ keyAuthorizeCore: keyAuthorizeCore,
3516
+ keyRevokeCore: keyRevokeCore,
3517
+ promptPassword: promptPassword,
3518
+ initCore: initCore,
3519
+ viewServerCore: viewServerCore,
1906
3520
 
1907
3521
  };
3522
+