@yottameta/yotta-memory 0.8.0 → 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.
- package/README.md +4 -1
- package/SKILL.md +3 -3
- package/USER_GUIDE.md +7 -1
- package/bin/yotta-memory.js +192 -55
- package/package.json +1 -1
- package/references/protocol.md +2 -2
package/README.md
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
> 📖 面向用户的操作手册见 [USER_GUIDE.md](USER_GUIDE.md)。
|
|
23
23
|
|
|
24
|
+
> 🆕 **v0.8.1**:`view` 用户查看平台改为服务端分页(记忆多了一次只渲染当前页,页码上下页);`recall` 增加候选预过滤(语义检索前先粗筛候选集,命中集不变,记忆量大时不再逐条全量语义 / 模糊 / 编辑距离);公共 `index.json` 超过 5000 条时按年份分片(`index-<year>.json`),避免单文件膨胀。
|
|
25
|
+
|
|
24
26
|
## 核心价值
|
|
25
27
|
|
|
26
28
|
多数记忆方案把「记住」做成了黑盒:数据进了数据库或云端,用户看得到内容却改不动、难以审计,更控制不了「哪个智能体能看到什么」。元忆换了一条路——把记忆还原成看得见、管得着的文件(公共 FACT 明文可审计,私密区机制级加密、用户可口令解锁):
|
|
@@ -98,8 +100,9 @@
|
|
|
98
100
|
|
|
99
101
|
### 检索:语义检索(v0.8.0)+ 中文分词打分
|
|
100
102
|
|
|
101
|
-
- `remember` 时自动构建 `index.json` 索引(v0.8.
|
|
103
|
+
- `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
104
|
- `recall --explain`:展示每条命中理由(精确 / 同义 / 拼音 / 模糊 + 字段)与效用分项。
|
|
105
|
+
- **recall 候选预过滤(v0.8.1)**:语义打分前先用索引 token 粗筛候选集(精确 / 同义 / 拼音 / 子串 / 模糊长度门槛),命中集与 v0.8.0 完全一致;`view` 查看平台按偏移分页返回,一次只取当前页。
|
|
103
106
|
- 可选 embedding 插件:协议预留(v0.9 实装),无插件自动零依赖降级。
|
|
104
107
|
- `index.json` 的 `tokens` 字段是中文分词的词频表(TF 打分用),**不是**访问凭证;鉴权令牌在记忆目录下 `.server/tokens.json`。
|
|
105
108
|
- 支持关键词、`--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.
|
|
3
|
+
description: "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚:让任何 AI 智能体活过会话,开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档、记忆盘、共享记忆、局域网记忆、画像、开工上下文、记忆守则、profile、context、越用越懂、语义检索、反馈、维护、蒸馏、feedback、maintain、distill、explain、自我学习、自我进化、自我提升、查看平台分页、recall 候选预过滤"
|
|
4
|
+
version: 0.8.1
|
|
5
5
|
license: MIT
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# yotta-memory(元忆)— 有权限边界的文件式智能体记忆
|
|
9
9
|
|
|
10
10
|
> 一句话:元忆 —— 有权限边界的文件式智能体记忆(不注入、可 diff、能回滚;FACT 共享、PREF / BOUND / COMMIT 私密隔离)。
|
|
11
|
-
> 版本:0.8.
|
|
11
|
+
> 版本:0.8.1 | 最后更新: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
|
|
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 智能体。
|
package/bin/yotta-memory.js
CHANGED
|
@@ -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.
|
|
25
|
+
const VERSION = '0.8.1';
|
|
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 =
|
|
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
|
|
591
|
-
|
|
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
|
-
|
|
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 <钥匙></code></p>\n <input type=\"password\" id=\"pw\" placeholder=\"主口令\" style=\"width:260px\">\n <button onclick=\"unlock()\">解锁</button>\n <div class=\"err\" id=\"lockerr\"></div>\n</div>\n<div id=\"app\">\n <div class=\"card\">\n <b>AI 列表</b>(✅=已授权可读自己私密,🔒=未授权)\n <div id=\"owners\" style=\"margin-top:8px\"></div>\n </div>\n <div class=\"card\">\n <b>记忆</b>\n <input id=\"q\" placeholder=\"搜索关键词\" style=\"width:220px\" onkeydown=\"if(event.key==='Enter')load()\">\n <button onclick=\"load()\">搜索</button>\n <button class=\"ghost\" onclick=\"doExport()\">导出 JSON</button>\n <button class=\"ghost\" onclick=\"showRk()\">显示恢复钥匙</button>\n <span id=\"rkout\" style=\"font-size:12px;color:#888;margin-left:8px\"></span>\n <div id=\"entries\" style=\"margin-top:10px\"></div>\n </div>\n <div class=\"card\">\n <b>重设口令</b><br>\n <input type=\"password\" id=\"cur\" placeholder=\"当前口令\">\n <input type=\"password\" id=\"np1\" placeholder=\"新口令\">\n <input type=\"password\" id=\"np2\" placeholder=\"确认新口令\">\n <button onclick=\"resetPw()\">重设</button>\n <span id=\"pwout\"></span>\n </div>\n</div>\n<script>\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,function(c){return{'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c];});}\nasync function api(p,b){try{const r=await fetch(p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})});return await r.json();}catch(e){return{error:String(e)};}}\nasync function boot(){const s=await api('/api/status');document.getElementById('ver').textContent='v'+(s.version||'');if(s.unlocked){showApp();}}\nfunction showApp(){document.getElementById('lock').style.display='none';document.getElementById('app').style.display='block';loadOwners();load();}\nasync function unlock(){const d=await api('/api/unlock',{password:document.getElementById('pw').value});if(d.error){document.getElementById('lockerr').textContent=d.error;return;}showApp();}\nasync function loadOwners(){const d=await api('/api/owners');const box=document.getElementById('owners');box.innerHTML='';if(!d.owners||!d.owners.length){box.innerHTML='(无 owner)';return;}\n for(const o of d.owners){const c=document.createElement('span');c.className='owner';c.innerHTML=esc(o.owner)+(o.authorized?' ✅':' 🔒')+' <button class=\"ghost\" data-a=\"'+esc(o.owner)+'\">授权</button><button class=\"danger\" data-r=\"'+esc(o.owner)+'\">吊销</button>';box.appendChild(c);}\n box.querySelectorAll('[data-a]').forEach(function(b){b.onclick=function(){api('/api/authorize',{owner:b.getAttribute('data-a')}).then(function(){loadOwners();});};});\n box.querySelectorAll('[data-r]').forEach(function(b){b.onclick=function(){api('/api/revoke',{owner:b.getAttribute('data-r')}).then(function(){loadOwners();});};});\n}\nasync function load(){const d=await api('/api/entries',{query:document.getElementById('q').value});const
|
|
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 <钥匙></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{'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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
|
-
|
|
3120
|
-
|
|
3121
|
-
'',
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
'
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
'
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
'
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
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.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/回滚;FACT/PREF/BOUND/COMMIT 四分类(公共共享/私密隔离),用户级+项目级双级存储;v0.8 语义检索 + 使用反馈闭环 + 规则层自组织 + 心理日志蒸馏(自我学习/自我进化/自我提升)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
package/references/protocol.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# yotta-memory 协议规范 v0.8.
|
|
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.
|
|
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
|
|