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