@yottameta/yotta-memory 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,6 +21,10 @@
21
21
 
22
22
  > 📖 面向用户的操作手册见 [USER_GUIDE.md](USER_GUIDE.md)。
23
23
 
24
+ > 🆕 **v0.8.2**:发布元数据修复——三次源重发带 `--name 元忆 yotta-memory`,修复 ClawHub 展示名缺失中文(原为裸 `yotta-memory`);无功能变更。
25
+
26
+ > 🆕 **v0.8.1**:`view` 用户查看平台改为服务端分页(记忆多了一次只渲染当前页,页码上下页);`recall` 增加候选预过滤(语义检索前先粗筛候选集,命中集不变,记忆量大时不再逐条全量语义 / 模糊 / 编辑距离);公共 `index.json` 超过 5000 条时按年份分片(`index-<year>.json`),避免单文件膨胀。
27
+
24
28
  ## 核心价值
25
29
 
26
30
  多数记忆方案把「记住」做成了黑盒:数据进了数据库或云端,用户看得到内容却改不动、难以审计,更控制不了「哪个智能体能看到什么」。元忆换了一条路——把记忆还原成看得见、管得着的文件(公共 FACT 明文可审计,私密区机制级加密、用户可口令解锁):
@@ -98,8 +102,9 @@
98
102
 
99
103
  ### 检索:语义检索(v0.8.0)+ 中文分词打分
100
104
 
101
- - `remember` 时自动构建 `index.json` 索引(v0.8.0 起 version=3,含字段加权与拼音 token;旧索引首次 recall 自动重建);recall 默认语义检索——精确(字段加权:subject×3 / tags×2 / statement×1)+ 同义词(内置词表,可扩展)+ 拼音(全拼 / 首字母,内置 3755 常用字表)+ 模糊(编辑距离 ≤ 2)+ 子串兜底,并与效用分融合排序(0.65 × 语义 + 0.35 × 效用),零依赖。
105
+ - `remember` 时自动构建 `index.json` 索引(v0.8.1 起 version=4,含字段加权与拼音 token;公共索引超过 5000 条按年份分片 `index-<year>.json`;旧索引首次 recall 自动重建);recall 默认语义检索——精确(字段加权:subject×3 / tags×2 / statement×1)+ 同义词(内置词表,可扩展)+ 拼音(全拼 / 首字母,内置 3755 常用字表)+ 模糊(编辑距离 ≤ 2)+ 子串兜底,并与效用分融合排序(0.65 × 语义 + 0.35 × 效用),零依赖。
102
106
  - `recall --explain`:展示每条命中理由(精确 / 同义 / 拼音 / 模糊 + 字段)与效用分项。
107
+ - **recall 候选预过滤(v0.8.1)**:语义打分前先用索引 token 粗筛候选集(精确 / 同义 / 拼音 / 子串 / 模糊长度门槛),命中集与 v0.8.0 完全一致;`view` 查看平台按偏移分页返回,一次只取当前页。
103
108
  - 可选 embedding 插件:协议预留(v0.9 实装),无插件自动零依赖降级。
104
109
  - `index.json` 的 `tokens` 字段是中文分词的词频表(TF 打分用),**不是**访问凭证;鉴权令牌在记忆目录下 `.server/tokens.json`。
105
110
  - 支持关键词、`--type` 过滤、`--limit` 截断、项目级优先。
package/SKILL.md CHANGED
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: yotta-memory
3
- description: "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚:让任何 AI 智能体活过会话,开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档、记忆盘、共享记忆、局域网记忆、画像、开工上下文、记忆守则、profile、context、越用越懂、语义检索、反馈、维护、蒸馏、feedback、maintain、distill、explain、自我学习、自我进化、自我提升"
4
- version: 0.8.0
3
+ description: "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚:让任何 AI 智能体活过会话,开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档、记忆盘、共享记忆、局域网记忆、画像、开工上下文、记忆守则、profile、context、越用越懂、语义检索、反馈、维护、蒸馏、feedback、maintain、distill、explain、自我学习、自我进化、自我提升、查看平台分页、recall 候选预过滤"
4
+ version: 0.8.2
5
5
  license: MIT
6
6
  ---
7
7
 
8
8
  # yotta-memory(元忆)— 有权限边界的文件式智能体记忆
9
9
 
10
10
  > 一句话:元忆 —— 有权限边界的文件式智能体记忆(不注入、可 diff、能回滚;FACT 共享、PREF / BOUND / COMMIT 私密隔离)。
11
- > 版本:0.8.0 | 最后更新:2026-08-27
11
+ > 版本:0.8.2 | 最后更新:2026-08-27
12
12
 
13
13
  ## 这是什么
14
14
 
package/USER_GUIDE.md CHANGED
@@ -9,6 +9,7 @@
9
9
  3. 本机单机使用
10
10
  3.5 私密区加密(v0.7,推荐)
11
11
  3.6 自我学习 / 自我进化 / 自我提升(v0.8.0)
12
+ 3.7 查看平台分页与检索优化(v0.8.1)
12
13
  4. 便携记忆盘 · 记忆引擎主机篇(Linux / Windows)
13
14
  5. 智能体接入篇(本机 / 局域网其它主机)
14
15
  6. CLI 命令速查
@@ -142,7 +143,7 @@ statement: 本周完成发布
142
143
  - `recall <关键词>` 默认语义检索:同义词(内置词表)、拼音(全拼 / 首字母,内置 3755 常用字表)、字段加权(subject 优先)、模糊匹配(编辑距离 ≤ 2)、子串兜底。
143
144
  - 例:记过「越用越懂」,用 `recall yyyd`(首字母)或 `recall yueyong yuedong`(拼音)都能找回。
144
145
  - `recall --explain`:显示每条命中理由与效用分项。
145
- - 旧索引首次 recall 自动重建(version 3),无需手动 reindex。
146
+ - 旧索引首次 recall 自动重建(version 4),无需手动 reindex。
146
147
 
147
148
  **使用反馈闭环(feedback)**
148
149
 
@@ -169,6 +170,11 @@ statement: 本周完成发布
169
170
 
170
171
  - `explain <文件>`:显示单条记忆的效用分项(confidence / 使用 / 时效 / 类型 / 结构 × weight)与归档 / 遗忘状态判定,帮助理解为什么某条记忆靠前 / 被归档。
171
172
 
173
+ ## 3.7 查看平台分页与检索优化(v0.8.1)
174
+
175
+ - **用户查看平台分页**:yotta-memory view 启动的网页端记忆卡片改为服务端分页——记忆多时不再一次性加载渲染全部,页面显示「共 N 条」「第 x / y 页」,支持上一页 / 下一页。
176
+ - **recall 候选预过滤**:语义检索前先用索引 token 粗筛候选集(精确 / 同义 / 拼音 / 子串 / 模糊长度门槛),命中集与 v0.8.0 一致,记忆量大时显著减少逐条语义 / 模糊 / 编辑距离开销。
177
+
172
178
  ## 4. 便携记忆盘 · 记忆引擎主机篇
173
179
 
174
180
  场景:记忆放在一台主机上(Linux / Windows 均可),本机直接 CLI 读写;局域网内其它主机上的 AI 智能体经 MCP 远程接入。引擎主机只需装 CLI,不需要装任何 AI 智能体。
@@ -22,7 +22,7 @@ const crypto = require('crypto');
22
22
  const http = require('http');
23
23
  const child_process = require('child_process');
24
24
 
25
- const VERSION = '0.8.0';
25
+ const VERSION = '0.8.2';
26
26
  const TYPES = ['FACT', 'PREF', 'BOUND', 'COMMIT'];
27
27
  const TYPE_DIRS = { FACT: 'facts', PREF: 'prefs', BOUND: 'bounds', COMMIT: 'commits' };
28
28
  const PUBLIC_DIR = 'facts';
@@ -581,24 +581,75 @@ function writeProfileText(root, owner, text) {
581
581
 
582
582
  // ---- 索引(index.json)----
583
583
  function indexPath(root) { return path.join(root, INDEX_FILE); }
584
- const INDEX_VERSION = 3;
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
+ }
585
600
  function loadIndex(root) {
586
601
  const p = indexPath(root);
587
602
  if (!fs.existsSync(p)) return null;
588
603
  try {
589
604
  const d = JSON.parse(fs.readFileSync(p, 'utf8'));
590
- if (d && Array.isArray(d.entries)) {
591
- // v0.8.0 语义索引(字段加权 + 拼音 token)version=3;旧版索引返回 null 触发重建
605
+ if (!d) return null;
606
+ if (Array.isArray(d.entries)) {
607
+ // v0.8.0 语义索引(字段加权 + 拼音 token)version=3;v0.8.1 分片 version=4;旧版索引返回 null 触发重建
592
608
  if (d.version && d.version >= INDEX_VERSION) return d.entries;
593
609
  return null;
594
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
+ }
595
624
  } catch (e) { /* ignore */ }
596
625
  return null;
597
626
  }
598
627
  function getIndex(root) { return loadIndex(root) || []; }
599
628
  function saveIndex(root, entries) {
600
629
  const clean = entries.map(function (e) { const c = Object.assign({}, e); delete c.meta; return c; });
601
- fs.writeFileSync(indexPath(root), JSON.stringify({ version: INDEX_VERSION, 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
+ }
602
653
  }
603
654
  function collectEntryFiles(root) {
604
655
  const dirs = [];
@@ -805,9 +856,25 @@ function bumpReadMeta(root, relFiles) {
805
856
  }
806
857
  function touchIndex(root, relFiles) {
807
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
+ }
808
876
  const idx = getIndex(root);
809
877
  let dirty = false;
810
- const now = today();
811
878
  for (const e of idx) {
812
879
  if (set.has(e.file)) { e.access_count = (parseInt(e.access_count, 10) || 0) + 1; e.last_accessed = now; dirty = true; }
813
880
  }
@@ -985,6 +1052,42 @@ function rememberCore(type, subject, statement, opts) {
985
1052
  }
986
1053
  return { error: false, text: text };
987
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
+ }
988
1091
  function recallCore(query, opts) {
989
1092
  opts = opts || {};
990
1093
  const roots = memoryRoots();
@@ -1000,6 +1103,7 @@ function recallCore(query, opts) {
1000
1103
  const explicitCross = !!opts.all || impersonation || (!!ownerFilter && ownerFilter.toLowerCase() !== 'user' && ownerFilter !== agent);
1001
1104
  const wantExplain = !!opts.explain;
1002
1105
  const useSemantic = opts.semantic !== false;
1106
+ const prefilter = (q && useSemantic) ? recallPrefilter(q) : null;
1003
1107
  const hits = [];
1004
1108
  let deniedCount = 0;
1005
1109
  for (const root of roots) {
@@ -1012,6 +1116,7 @@ function recallCore(query, opts) {
1012
1116
  let detail = null;
1013
1117
  if (q) {
1014
1118
  if (useSemantic) {
1119
+ if (prefilter && !prefilter(e)) continue; // v0.8.1 候选预过滤:粗筛后再语义打分
1015
1120
  const m = semanticMatch(e, q, wantExplain);
1016
1121
  score = m.score;
1017
1122
  if (wantExplain && m.detail && m.detail.length) detail = m.detail;
@@ -1766,6 +1871,26 @@ function cmdKeyRevoke(id) {
1766
1871
  if (r.error) process.exit(2);
1767
1872
  }
1768
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
+ }
1769
1894
  function viewServerCore(root, port, host) {
1770
1895
  let session = { umk: null, ownerKeys: {} };
1771
1896
  const server = http.createServer(function (req, res) {
@@ -1818,19 +1943,7 @@ function viewServerCore(root, port, host) {
1818
1943
  }
1819
1944
  if (req.method === 'POST' && pathname === '/api/entries') {
1820
1945
  if (!session.umk) return json(401, { error: '请先解锁。' });
1821
- return readBody(function (d) {
1822
- const q = String(d.query || '').toLowerCase();
1823
- const entries = [];
1824
- for (const e of (loadIndex(root) || [])) entries.push(e);
1825
- for (const o of collectOwners(root)) {
1826
- const key = session.ownerKeys[o];
1827
- if (!key) continue;
1828
- try { for (const e of loadOwnerIndex(root, o, key)) entries.push(e); } catch (err) {}
1829
- }
1830
- const out = q ? entries.filter(function (e) { return ((e.subject || '') + ' ' + (e.statement || '') + ' ' + e.tags.join(' ')).toLowerCase().indexOf(q) !== -1; }) : entries;
1831
- out.sort(function (a, b) { return String(b.updated || b.created || '').localeCompare(String(a.updated || a.created || '')); });
1832
- json(200, { count: out.length, entries: out });
1833
- });
1946
+ return readBody(function (d) { json(200, viewEntriesCore(root, session, d.query, d.offset, d.limit)); });
1834
1947
  }
1835
1948
  if (req.method === 'POST' && pathname === '/api/export') {
1836
1949
  if (!session.umk) return json(401, { error: '请先解锁。' });
@@ -1908,7 +2021,7 @@ function cmdView(opts) {
1908
2021
  viewServerCore(root, port, host);
1909
2022
  }
1910
2023
 
1911
- const VIEW_HTML = "<!doctype html><html lang=\"zh\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>元忆 · 用户查看平台</title><style>\nbody{font-family:system-ui,-apple-system,\"Microsoft YaHei\",sans-serif;max-width:1000px;margin:24px auto;padding:0 16px;color:#1f2328;background:#fafafa}\nh1{font-size:22px} .card{background:#fff;border:1px solid #e2e2e2;border-radius:10px;padding:16px 18px;margin:14px 0;box-shadow:0 1px 2px rgba(0,0,0,.04)}\nbutton{background:#2563eb;color:#fff;border:0;border-radius:6px;padding:7px 14px;cursor:pointer;margin:2px;font-size:14px}\nbutton.danger{background:#dc2626} button.ghost{background:#e5e7eb;color:#1f2328}\ninput,select{padding:8px;border:1px solid #c9c9c9;border-radius:6px;margin:2px;font-size:14px;box-sizing:border-box}\ntable{border-collapse:collapse;width:100%;font-size:13px} td,th{border:1px solid #ececec;padding:6px 8px;text-align:left;vertical-align:top}\n.owner{display:inline-flex;align-items:center;gap:6px;border:1px solid #ddd;border-radius:8px;padding:5px 10px;margin:4px 6px 4px 0;background:#f6f8fa}\n.entry{border-bottom:1px solid #eee;padding:8px 0} .meta{color:#8a8a8a;font-size:12px}\n.err{color:#dc2626;margin-top:8px} .ok{color:#16a34a;margin-top:8px}\n#app{display:none} code{background:#f0f0f0;padding:1px 5px;border-radius:4px;font-size:12px}\n</style></head><body>\n<h1>元忆 · 用户查看平台 <span id=\"ver\" style=\"font-size:14px;color:#888\"></span></h1>\n<div id=\"lock\" class=\"card\">\n <p><b>输入主口令解锁</b>(口令只在本地内存派生,不落盘、不发送远端)。忘口令可在 CLI 用恢复钥匙重设:<code>yotta-memory reset-password --recovery-key &lt;钥匙&gt;</code></p>\n <input type=\"password\" id=\"pw\" placeholder=\"主口令\" style=\"width:260px\">\n <button onclick=\"unlock()\">解锁</button>\n <div class=\"err\" id=\"lockerr\"></div>\n</div>\n<div id=\"app\">\n <div class=\"card\">\n <b>AI 列表</b>(✅=已授权可读自己私密,🔒=未授权)\n <div id=\"owners\" style=\"margin-top:8px\"></div>\n </div>\n <div class=\"card\">\n <b>记忆</b>\n <input id=\"q\" placeholder=\"搜索关键词\" style=\"width:220px\" onkeydown=\"if(event.key==='Enter')load()\">\n <button onclick=\"load()\">搜索</button>\n <button class=\"ghost\" onclick=\"doExport()\">导出 JSON</button>\n <button class=\"ghost\" onclick=\"showRk()\">显示恢复钥匙</button>\n <span id=\"rkout\" style=\"font-size:12px;color:#888;margin-left:8px\"></span>\n <div id=\"entries\" style=\"margin-top:10px\"></div>\n </div>\n <div class=\"card\">\n <b>重设口令</b><br>\n <input type=\"password\" id=\"cur\" placeholder=\"当前口令\">\n <input type=\"password\" id=\"np1\" placeholder=\"新口令\">\n <input type=\"password\" id=\"np2\" placeholder=\"确认新口令\">\n <button onclick=\"resetPw()\">重设</button>\n <span id=\"pwout\"></span>\n </div>\n</div>\n<script>\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c];});}\nasync function api(p,b){try{const r=await fetch(p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})});return await r.json();}catch(e){return{error:String(e)};}}\nasync function boot(){const s=await api('/api/status');document.getElementById('ver').textContent='v'+(s.version||'');if(s.unlocked){showApp();}}\nfunction showApp(){document.getElementById('lock').style.display='none';document.getElementById('app').style.display='block';loadOwners();load();}\nasync function unlock(){const d=await api('/api/unlock',{password:document.getElementById('pw').value});if(d.error){document.getElementById('lockerr').textContent=d.error;return;}showApp();}\nasync function loadOwners(){const d=await api('/api/owners');const box=document.getElementById('owners');box.innerHTML='';if(!d.owners||!d.owners.length){box.innerHTML='(无 owner)';return;}\n for(const o of d.owners){const c=document.createElement('span');c.className='owner';c.innerHTML=esc(o.owner)+(o.authorized?' ✅':' 🔒')+' <button class=\"ghost\" data-a=\"'+esc(o.owner)+'\">授权</button><button class=\"danger\" data-r=\"'+esc(o.owner)+'\">吊销</button>';box.appendChild(c);}\n box.querySelectorAll('[data-a]').forEach(function(b){b.onclick=function(){api('/api/authorize',{owner:b.getAttribute('data-a')}).then(function(){loadOwners();});};});\n box.querySelectorAll('[data-r]').forEach(function(b){b.onclick=function(){api('/api/revoke',{owner:b.getAttribute('data-r')}).then(function(){loadOwners();});};});\n}\nasync function load(){const d=await api('/api/entries',{query:document.getElementById('q').value});const box=document.getElementById('entries');box.innerHTML='共 '+d.count+' 条';\n if(!d.entries)return;for(const e of d.entries){const div=document.createElement('div');div.className='entry';div.innerHTML='<b>['+esc(e.type)+'] '+esc(e.subject)+'</b><div>'+esc(e.statement)+'</div><div class=\"meta\">'+esc(e.file)+' · owner='+esc(e.owner||'-')+' · '+esc(e.updated||e.created||'')+'</div>';box.appendChild(div);}}\nasync function doExport(){const d=await api('/api/export');if(d.error){alert(d.error);return;}const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='yottamemory-view-export.json';a.click();}\nasync function showRk(){const d=await api('/api/recovery-key');document.getElementById('rkout').textContent=d.recoveryKey?('恢复钥匙: '+d.recoveryKey):(d.error||'');}\nasync function resetPw(){const np1=document.getElementById('np1').value,np2=document.getElementById('np2').value;if(np1!==np2){document.getElementById('pwout').innerHTML='<span class=\"err\">两次新口令不一致</span>';return;}\n const d=await api('/api/reset-password',{currentPassword:document.getElementById('cur').value,newPassword:np1});document.getElementById('pwout').innerHTML=d.error?('<span class=\"err\">'+esc(d.error)+'</span>'):('<span class=\"ok\">'+esc(d.text||'ok')+'</span>');}\nboot();\n</script></body></html>";
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>";
1912
2025
 
1913
2026
  // ---- 命令包装(CLI 入口)----
1914
2027
  async function cmdInit(opts) {
@@ -3116,41 +3229,52 @@ function cmdLanStatus() {
3116
3229
  }
3117
3230
  // ---- usage / main ----
3118
3231
  function usage() {
3119
- console.log([
3120
- 'yotta-memory v' + VERSION + ' — 元忆:有权限边界的文件式智能体记忆',
3121
- '',
3122
- '用法:',
3123
- 'yotta-memory init [--project] [--dir <目录>] [--encrypt|--no-encrypt] 初始化记忆库(新建默认加密:需主口令 + 恢复钥匙;--no-encrypt 降级明文)',
3124
- 'yotta-memory migrate 把明文私密区迁移为密文(需主口令;迁移后打印恢复钥匙)',
3125
- 'yotta-memory view [--port 8788] [--host 127.0.0.1] 启动用户查看平台(口令解锁浏览/搜索/导出全部 AI 记忆 + 授权/吊销 AI)',
3126
- 'yotta-memory reset-password [--password <当前> | --recovery-key <钥匙>] [--new-password <新>] 重设主口令(忘口令用恢复钥匙)',
3127
- 'yotta-memory key list | authorize <id> | revoke <id> 管理 AI 的私密读取授权缓存(authorize 需主口令)',
3128
- ' yotta-memory config set memory_home <目录> 持久记住记忆库位置',
3129
- ' yotta-memory config get 查看当前配置与生效位置',
3130
- ' yotta-memory remember <type> <subject> <statement> [--owner <id>] [--source <来源>] [--weight <0..>] [--verify] [--no-hint] 写入记忆(--source 记录来源;--weight 重要性权重默认 1.0 去重取 max;--verify 写后回读校验;--no-hint 关闭类型启发式提示)',
3131
- ' yotta-memory profile [--owner <id>] 生成用户画像(聚合 private/<owner>/ 原文,零推断,写 profile.md)',
3132
- ' yotta-memory context [--limit N] [--owner <id>] [--budget N] 生成开工上下文包(身份+铁律+画像+近期记忆+边界+承诺;--budget 近期记忆字符预算,0=不限)',
3133
- ' yotta-memory recall [关键词] [--type T] [--limit N] [--agent <id>] [--owner <id>] [--all] [--unsafe] 检索(索引+TF,读取分区;--agent 仅表示“以该身份检索/模拟”,跨智能体私密读取需 --unsafe / 授权)',
3134
- ' yotta-memory forget <文件或文件名> 删除一条记忆',
3135
- ' yotta-memory archive [--days 180] [--threshold 0.4] 归档(盖棺分+年龄)',
3136
- ' yotta-memory reindex 重建索引',
3137
- ' yotta-memory export [--out 文件.json] 导出全部记忆',
3138
- ' yotta-memory import <文件.json> 导入记忆',
3139
- ' yotta-memory whoami 查看当前智能体身份与登记状态(读 YOTTA_AGENT_ID / X-Agent-Id,不猜不默认)',
3140
- ' yotta-memory iam <id> [--name <显示名>] [--user <用户名>] [--relationship <关系>] [--force] 登记本智能体唯一身份并自动落自我档案(agents.json;ID 必须唯一)',
3141
- ' yotta-memory token new --agent <id> [--force] 生成/重置某智能体访问 token(同 ID 已被其它来源占用需 --force 覆盖)',
3142
- ' yotta-memory token list 列出已登记智能体',
3143
- ' yotta-memory token revoke --agent <id> 吊销某智能体 token',
3144
- ' yotta-memory serve [--host 0.0.0.0] [--port 8787] [--no-auth] [--stdio] 启动 MCP 记忆引擎(streamable HTTP;--stdio 本地零进程模式)',
3145
- ' yotta-memory lan enable [--onstart] | disable | status 开机自启管理(Windows:计划任务,默认 ONLOGON,非管理员自动降级用户级 Startup 静默自启;Linux:systemd 用户单元,不可用时自动降级用户 crontab @reboot)',
3146
- ' yotta-memory --version 版本',
3147
- '',
3148
- '类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)',
3149
- '环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; YOTTA_AGENT_ID/AGENT_ID 当前 agent 标识(本机声明身份用;私密记忆必须有 owner)',
3150
- '隔离: 公共 FACT 在 facts/;私密 PREF/BOUND/COMMIT 物理分目录 private/<agent_id>/<type>/,禁止 shell 直读写记忆库,一律走本命令',
3151
- '远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id>',
3152
- '',
3153
- ].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'));
3154
3278
  }
3155
3279
  async function main() {
3156
3280
  const args = process.argv.slice(2);
@@ -3367,8 +3491,20 @@ module.exports = {
3367
3491
  writeMemoryText: writeMemoryText,
3368
3492
  loadOwnerIndex: loadOwnerIndex,
3369
3493
  saveOwnerIndex: saveOwnerIndex,
3494
+ viewEntriesCore: viewEntriesCore,
3370
3495
  ensureIndex: ensureIndex,
3496
+ recallPrefilter: recallPrefilter,
3497
+ tokenize: tokenize,
3498
+ buildTokens: buildTokens,
3499
+ synonymSet: synonymSet,
3500
+
3371
3501
  loadIndex: loadIndex,
3502
+ saveIndex: saveIndex,
3503
+ getIndex: getIndex,
3504
+ touchIndex: touchIndex,
3505
+ loadIndexManifest: loadIndexManifest,
3506
+ indexShardName: indexShardName,
3507
+ isSafeShardName: isSafeShardName,
3372
3508
  ownerFromPrivatePath: ownerFromPrivatePath,
3373
3509
  isEncFile: isEncFile,
3374
3510
  profilePathFor: profilePathFor,
@@ -3383,3 +3519,4 @@ module.exports = {
3383
3519
  viewServerCore: viewServerCore,
3384
3520
 
3385
3521
  };
3522
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-memory",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚;FACT/PREF/BOUND/COMMIT 四分类(公共共享/私密隔离),用户级+项目级双级存储;v0.8 语义检索 + 使用反馈闭环 + 规则层自组织 + 心理日志蒸馏(自我学习/自我进化/自我提升)。",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -1,4 +1,4 @@
1
- # yotta-memory 协议规范 v0.8.0
1
+ # yotta-memory 协议规范 v0.8.1
2
2
 
3
3
  > 本文件定义 yotta-memory 记忆标准:存储位置、目录结构、文件格式、类型体系与 CLI 命令参考。
4
4
  > 目标:任何支持 Agent Skills 开放标准的智能体,装完即可读写同一份记忆。
@@ -32,7 +32,7 @@
32
32
  └── README.md # 记忆库说明
33
33
  ```
34
34
 
35
- > **`index.json` 说明**:条目内的 `tokens` 字段是中文分词的「词频表」(v0.8.0 起含字段加权与拼音 token),供 recall 的 TF 打分 / 语义检索使用,**不是**访问令牌。真正的访问令牌由 `token` 命令生成,存放在 `<root>/.server/tokens.json`,仅在局域网 `serve` 模式下用于请求鉴权。v0.8.0 起索引 version=3,旧索引首次 recall 自动重建。
35
+ > **`index.json` 说明**:条目内的 `tokens` 字段是中文分词的「词频表」(v0.8.0 起含字段加权与拼音 token),供 recall 的 TF 打分 / 语义检索使用,**不是**访问令牌。真正的访问令牌由 `token` 命令生成,存放在 `<root>/.server/tokens.json`,仅在局域网 `serve` 模式下用于请求鉴权。v0.8.1 起索引 version=4(公共索引超过 5000 条按年份分片 `index-<year>.json`),旧索引首次 recall 自动重建。
36
36
 
37
37
  > **目录结构(v0.5.0)**:私密记忆按 `owner` 物理分目录存放于 `private/<owner>/<type>/`。旧版根下平铺的 `prefs/` `bounds/` `commits/` 会在 `reindex`(或首次 recall 建索引)时按 frontmatter `owner` 自动迁移到 `private/<owner>/<type>/`。**AI 读写红线**:记忆读写一律走 `yotta-memory` CLI / MCP 工具;禁止用 shell(`Get-ChildItem` / `Get-Content` / `cat` / `ls` / `type` 等)直接读改记忆库目录下的文件——否则会绕过 `scope/owner` 权限边界,读到其它智能体的私密内容。
38
38