@yottameta/yotta-memory 0.13.1 → 0.16.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/CHANGELOG.md +64 -0
- package/README.md +65 -21
- package/README.zh-CN.md +70 -26
- package/SKILL.md +129 -30
- package/USER_GUIDE.md +50 -21
- package/assets/view.html +71 -0
- package/bin/yotta-memory.js +1814 -182
- package/package.json +1 -1
- package/references/faq.md +11 -5
- package/references/protocol.md +35 -13
- package/skill-manifest.json +1 -1
package/bin/yotta-memory.js
CHANGED
|
@@ -22,8 +22,12 @@ const os = require('os');
|
|
|
22
22
|
const crypto = require('crypto');
|
|
23
23
|
const http = require('http');
|
|
24
24
|
const child_process = require('child_process');
|
|
25
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
25
26
|
|
|
26
|
-
const VERSION = '0.
|
|
27
|
+
const VERSION = '0.16.0';
|
|
28
|
+
// @generated view-html:start
|
|
29
|
+
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>\r\nbody{font-family:system-ui,-apple-system,\"Microsoft YaHei\",sans-serif;max-width:1000px;margin:24px auto;padding:0 16px;color:#1f2328;background:#fafafa}\r\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)}\r\nbutton{background:#2563eb;color:#fff;border:0;border-radius:6px;padding:7px 14px;cursor:pointer;margin:2px;font-size:14px}\r\nbutton.danger{background:#dc2626} button.ghost{background:#e5e7eb;color:#1f2328}\r\ninput,select{padding:8px;border:1px solid #c9c9c9;border-radius:6px;margin:2px;font-size:14px;box-sizing:border-box}\r\ntable{border-collapse:collapse;width:100%;font-size:13px} td,th{border:1px solid #ececec;padding:6px 8px;text-align:left;vertical-align:top}\r\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}\r\n.entry{border-bottom:1px solid #eee;padding:8px 0} .meta{color:#8a8a8a;font-size:12px}\r\n.err{color:#dc2626;margin-top:8px} .ok{color:#16a34a;margin-top:8px}\r\n#app{display:none} code{background:#f0f0f0;padding:1px 5px;border-radius:4px;font-size:12px}\r\n</style></head><body>\r\n<h1>元忆 · 用户查看平台 <span id=\"ver\" style=\"font-size:14px;color:#888\"></span></h1>\r\n<div id=\"lock\" class=\"card\">\r\n <p><b>输入主口令解锁</b>(口令只在本地内存派生,不落盘、不发送远端)。忘口令可在 CLI 用恢复钥匙重设:<code>yotta-memory reset-password --recovery-key <钥匙></code></p>\r\n <input type=\"password\" id=\"pw\" placeholder=\"主口令\" style=\"width:260px\">\r\n <button onclick=\"unlock()\">解锁</button>\r\n <div class=\"err\" id=\"lockerr\"></div>\r\n</div>\r\n<div id=\"app\">\r\n <div class=\"card\">\r\n <b>AI 列表</b>(✅=已授权可读自己私密,🔒=未授权)\r\n <div class=\"meta\" style=\"margin-top:6px\">「授权」由你(用户)操作:确认后生成只显示一次的 agent_key,请立即单独保存;服务端同时写临时待领取文件 <code>keys/pending/<agent_id>.key</code>,供该 AI 新会话领取,领取成功后自动删除。</div>\r\n <div id=\"owners\" style=\"margin-top:8px\"></div>\r\n </div>\r\n <div class=\"card\">\r\n <b>记忆</b>\r\n <input id=\"q\" placeholder=\"搜索关键词\" style=\"width:220px\" onkeydown=\"if(event.key==='Enter'){off=0;load()}\">\r\n <button onclick=\"off=0;load()\">搜索</button>\r\n <button class=\"ghost\" onclick=\"doExport()\">导出 JSON</button>\r\n <button class=\"ghost\" onclick=\"showRk()\">显示恢复钥匙</button>\r\n <span id=\"rkout\" style=\"font-size:12px;color:#888;margin-left:8px\"></span>\r\n <div id=\"meta\" style=\"margin-top:10px;font-size:12px;color:#666\"></div>\r\n <div id=\"entries\" style=\"margin-top:6px\"></div>\r\n <div id=\"pager\" style=\"margin-top:10px\">\r\n <button class=\"ghost\" id=\"prevb\" onclick=\"prevPage()\">上一页</button>\r\n <span id=\"pageinfo\" style=\"font-size:12px;color:#888;margin:0 8px\"></span>\r\n <button class=\"ghost\" id=\"nextb\" onclick=\"nextPage()\">下一页</button>\r\n </div>\r\n </div>\r\n <div class=\"card\">\r\n <b>重设口令</b><br>\r\n <input type=\"password\" id=\"cur\" placeholder=\"当前口令\">\r\n <input type=\"password\" id=\"np1\" placeholder=\"新口令\">\r\n <input type=\"password\" id=\"np2\" placeholder=\"确认新口令\">\r\n <button onclick=\"resetPw()\">重设</button>\r\n <span id=\"pwout\"></span>\r\n </div>\r\n</div>\r\n<script>\r\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,function(c){return{'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c];});}\r\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)};}}\r\nasync function boot(){const s=await api('/api/status');document.getElementById('ver').textContent='v'+(s.version||'');if(s.unlocked){showApp();}}\r\nfunction showApp(){document.getElementById('lock').style.display='none';document.getElementById('app').style.display='block';loadOwners();load();}\r\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();}\r\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;}\r\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);}\r\n box.querySelectorAll('[data-a]').forEach(function(b){b.onclick=function(){var owner=b.getAttribute('data-a');if(!confirm('确认由你为用户授权 '+owner+' 读取其私密记忆?授权后将生成只显示一次的 agent_key,请立即保存;同时写入待领取文件供该 AI 新会话领取。AI 不应代为执行该授权操作。'))return;b.disabled=true;api('/api/authorize',{owner:owner}).then(function(d){b.disabled=false;if(!d||d.error){alert((d&&d.error)||'授权失败');loadOwners();return;}if(d.agentKey){showKey(d.agentKey);}loadOwners();});};});\r\n box.querySelectorAll('[data-r]').forEach(function(b){b.onclick=function(){if(!confirm('确认吊销 '+b.getAttribute('data-r')+' 的 agent_key?吊销后该智能体立即失去私密读写能力。'))return;api('/api/revoke',{owner:b.getAttribute('data-r')}).then(function(){loadOwners();});};});\r\nfunction showKey(k){var ov=document.createElement('div');ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:99';var box=document.createElement('div');box.className='card';box.style.cssText='max-width:640px;word-break:break-all';var t=document.createElement('div');t.innerHTML='<b>agent_key(只显示一次)</b>';var hint=document.createElement('div');hint.className='meta';hint.textContent='请用户立即单独保存。AI 新会话先执行 yotta-memory key status <agent_id>,有 pending 再执行 key claim <agent_id>;默认写入 AI_HOME/.yotta-memory-agent-key,需要时用 --to 或 --agent-key-file 指定。若 key 丢失,可吊销后重新授权;旧 key 会立即校验失败。';var ta=document.createElement('textarea');ta.readOnly=true;ta.value=k;ta.style.cssText='width:100%;height:72px;margin-top:8px;font-family:monospace;font-size:12px';var close=document.createElement('button');close.textContent='我已保存,关闭';close.onclick=function(){ov.remove();};box.appendChild(t);box.appendChild(hint);box.appendChild(ta);box.appendChild(close);ov.appendChild(box);document.body.appendChild(ov);ta.focus();ta.select();}\r\n}\r\nlet off=0,PS=50;\r\nasync function load(){const d=await api('/api/entries',{query:document.getElementById('q').value,offset:off,limit:PS});const meta=document.getElementById('meta');const pg=document.getElementById('pageinfo');if(meta)meta.textContent='共 '+d.count+' 条';const lim=d.limit||PS;const totalPg=Math.max(1,Math.ceil(d.count/lim));const curPg=Math.floor((d.offset||0)/lim)+1;if(pg)pg.textContent='第 '+curPg+' / '+totalPg+' 页';const box=document.getElementById('entries');box.innerHTML='';if(d.entries)for(const e of d.entries){const div=document.createElement('div');div.className='entry';div.innerHTML='<b>['+esc(e.type)+'] '+esc(e.subject)+'</b><div>'+esc(e.statement)+'</div><div class=\"meta\">'+esc(e.file)+' · owner='+esc(e.owner||'-')+' · '+esc(e.updated||e.created||'')+'</div>';box.appendChild(div);}const pb=document.getElementById('prevb'),nb=document.getElementById('nextb');if(pb)pb.disabled=(d.offset||0)<=0;if(nb)nb.disabled=!d.hasMore;}\r\nfunction prevPage(){if(off>=PS){off-=PS;load();}}\r\nfunction nextPage(){off+=PS;load();}\r\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();}\r\nasync function showRk(){const d=await api('/api/recovery-key');document.getElementById('rkout').textContent=d.recoveryKey?('恢复钥匙: '+d.recoveryKey):(d.error||'');}\r\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;}\r\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>');}\r\nboot();\r\n</script></body></html>\r\n";
|
|
30
|
+
// @generated view-html:end
|
|
27
31
|
// MCP 协议(2026-07-28 无状态 + 2025-11-25 legacy 握手,dual-era)
|
|
28
32
|
const MCP_PROTOCOL_MODERN = '2026-07-28';
|
|
29
33
|
const MCP_PROTOCOL_LEGACY = '2025-11-25';
|
|
@@ -86,13 +90,555 @@ function memoryRoots() {
|
|
|
86
90
|
}
|
|
87
91
|
return out;
|
|
88
92
|
}
|
|
93
|
+
// ---- v0.16.0 M2: runtime stable entry ----
|
|
94
|
+
const RUNTIME_SCHEMA = 1;
|
|
95
|
+
function runtimeRoot() {
|
|
96
|
+
if (process.env.YOTTA_MEMORY_RUNTIME_HOME) return path.resolve(process.env.YOTTA_MEMORY_RUNTIME_HOME);
|
|
97
|
+
return path.join(path.dirname(configPath()), 'runtime');
|
|
98
|
+
}
|
|
99
|
+
function runtimeManifestPath() { return path.join(runtimeRoot(), 'runtime.json'); }
|
|
100
|
+
function runtimeVersionsDir() { return path.join(runtimeRoot(), 'versions'); }
|
|
101
|
+
function runtimeVersionDir(version) { return path.join(runtimeVersionsDir(), String(version)); }
|
|
102
|
+
function runtimeCurrentDir() { return path.join(runtimeRoot(), 'current'); }
|
|
103
|
+
function runtimeCurrentBin() { return path.join(runtimeCurrentDir(), 'bin', 'yotta-memory.js'); }
|
|
104
|
+
function isSafeRuntimeVersion(version) {
|
|
105
|
+
const value = String(version || '').trim();
|
|
106
|
+
if (!value || value === '.' || value === '..') return false;
|
|
107
|
+
if (!/^[0-9A-Za-z][0-9A-Za-z.+-]*$/.test(value)) return false;
|
|
108
|
+
return !/[\\/]/.test(value);
|
|
109
|
+
}
|
|
110
|
+
function runtimePathWithin(parent, child) {
|
|
111
|
+
const p = path.resolve(parent);
|
|
112
|
+
const c = path.resolve(child);
|
|
113
|
+
return c === p || c.indexOf(p + path.sep) === 0;
|
|
114
|
+
}
|
|
115
|
+
function readRuntimeManifest() {
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(fs.readFileSync(runtimeManifestPath(), 'utf8'));
|
|
118
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
119
|
+
return parsed;
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function writeRuntimeManifest(manifest) {
|
|
125
|
+
fs.mkdirSync(runtimeRoot(), { recursive: true });
|
|
126
|
+
const target = runtimeManifestPath();
|
|
127
|
+
const temp = target + '.tmp-' + process.pid + '-' + Date.now();
|
|
128
|
+
fs.writeFileSync(temp, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
129
|
+
fs.renameSync(temp, target);
|
|
130
|
+
}
|
|
131
|
+
function runtimeTreeHash(dir) {
|
|
132
|
+
const hash = crypto.createHash('sha256');
|
|
133
|
+
function walk(rel) {
|
|
134
|
+
const full = path.join(dir, rel);
|
|
135
|
+
const entries = fs.readdirSync(full, { withFileTypes: true })
|
|
136
|
+
.sort(function (a, b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); });
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
if (entry.name === '.git' || entry.name === 'node_modules') continue;
|
|
139
|
+
const childRel = rel ? path.join(rel, entry.name) : entry.name;
|
|
140
|
+
const childFull = path.join(dir, childRel);
|
|
141
|
+
if (entry.isDirectory()) {
|
|
142
|
+
walk(childRel);
|
|
143
|
+
} else if (entry.isFile()) {
|
|
144
|
+
hash.update(childRel.replace(/\\/g, '/') + '\0');
|
|
145
|
+
hash.update(fs.readFileSync(childFull));
|
|
146
|
+
hash.update('\0');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
walk('');
|
|
151
|
+
return hash.digest('hex');
|
|
152
|
+
}
|
|
153
|
+
function runtimePackageVersion(dir) {
|
|
154
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
155
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
156
|
+
const version = String(pkg.version || '').trim();
|
|
157
|
+
if (!isSafeRuntimeVersion(version)) throw new Error('runtime 包版本非法: ' + version);
|
|
158
|
+
return version;
|
|
159
|
+
}
|
|
160
|
+
function runtimeVerifyVersionDir(dir, expected, manifest) {
|
|
161
|
+
if (!fs.existsSync(dir)) return { error: true, text: 'runtime 版本目录不存在: ' + dir };
|
|
162
|
+
let version = '';
|
|
163
|
+
try { version = runtimePackageVersion(dir); } catch (e) { return { error: true, text: e.message }; }
|
|
164
|
+
if (version !== expected) return { error: true, text: 'runtime 版本不一致: 目录为 ' + version + ',请求 ' + expected };
|
|
165
|
+
if (!fs.existsSync(path.join(dir, 'bin', 'yotta-memory.js'))) {
|
|
166
|
+
return { error: true, text: 'runtime 版本缺少 bin/yotta-memory.js: ' + dir };
|
|
167
|
+
}
|
|
168
|
+
const treeHash = runtimeTreeHash(dir);
|
|
169
|
+
const record = manifest && manifest.versions && manifest.versions[expected];
|
|
170
|
+
if (record && record.treeHash && record.treeHash !== treeHash) {
|
|
171
|
+
return { error: true, text: 'runtime 版本内容哈希与 runtime.json 不一致;请重新安装 ' + expected, treeHash: treeHash };
|
|
172
|
+
}
|
|
173
|
+
return { error: false, treeHash: treeHash };
|
|
174
|
+
}
|
|
175
|
+
function compareRuntimeVersions(a, b) {
|
|
176
|
+
const pa = String(a).split('.').map(function (x) { return parseInt(x, 10) || 0; });
|
|
177
|
+
const pb = String(b).split('.').map(function (x) { return parseInt(x, 10) || 0; });
|
|
178
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
179
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
180
|
+
if (d !== 0) return d;
|
|
181
|
+
}
|
|
182
|
+
return String(a).localeCompare(String(b));
|
|
183
|
+
}
|
|
184
|
+
function runtimeListVersions() {
|
|
185
|
+
const dir = runtimeVersionsDir();
|
|
186
|
+
if (!fs.existsSync(dir)) return [];
|
|
187
|
+
return fs.readdirSync(dir).filter(function (name) {
|
|
188
|
+
if (!isSafeRuntimeVersion(name)) return false;
|
|
189
|
+
return fs.existsSync(path.join(dir, name, 'package.json'));
|
|
190
|
+
}).sort(function (a, b) { return compareRuntimeVersions(b, a); });
|
|
191
|
+
}
|
|
192
|
+
function runtimeManagedScript() {
|
|
193
|
+
const current = runtimeCurrentBin();
|
|
194
|
+
return fs.existsSync(current) ? current : __filename;
|
|
195
|
+
}
|
|
196
|
+
function runtimePatternToRegExp(pattern) {
|
|
197
|
+
const escaped = String(pattern).split('*').map(function (part) {
|
|
198
|
+
return part.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
199
|
+
}).join('.*');
|
|
200
|
+
return new RegExp('^' + escaped + '$');
|
|
201
|
+
}
|
|
202
|
+
function runtimeMatchExclude(rel, pattern) {
|
|
203
|
+
const r = String(rel).replace(/\\/g, '/');
|
|
204
|
+
const p = String(pattern).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
205
|
+
if (p.indexOf('*') === -1) return r === p || r.indexOf(p.replace(/\/$/, '') + '/') === 0;
|
|
206
|
+
return runtimePatternToRegExp(p).test(r);
|
|
207
|
+
}
|
|
208
|
+
function runtimeCopyTreeFiltered(src, dst, excludes, relBase) {
|
|
209
|
+
const base = relBase ? relBase + '/' : '';
|
|
210
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
211
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
if (entry.name === '.git' || entry.name === 'node_modules') continue;
|
|
214
|
+
const rel = base + entry.name;
|
|
215
|
+
if (excludes.some(function (pattern) { return runtimeMatchExclude(rel, pattern); })) continue;
|
|
216
|
+
const source = path.join(src, entry.name);
|
|
217
|
+
const target = path.join(dst, entry.name);
|
|
218
|
+
if (entry.isDirectory()) runtimeCopyTreeFiltered(source, target, excludes, rel);
|
|
219
|
+
else if (entry.isFile()) fs.copyFileSync(source, target);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function runtimeCopyTree(src, dst) {
|
|
223
|
+
const st = fs.statSync(src);
|
|
224
|
+
if (st.isDirectory()) {
|
|
225
|
+
runtimeCopyTreeFiltered(src, dst, [], '');
|
|
226
|
+
} else if (st.isFile()) {
|
|
227
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
228
|
+
fs.copyFileSync(src, dst);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function runtimeCopyPackageFromRoot(srcRoot, dstRoot) {
|
|
232
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(srcRoot, 'package.json'), 'utf8'));
|
|
233
|
+
const files = Array.isArray(pkg.files) ? pkg.files : [];
|
|
234
|
+
const includes = ['package.json'].concat(files.filter(function (entry) { return entry && entry[0] !== '!'; }));
|
|
235
|
+
const excludes = files.filter(function (entry) { return entry && entry[0] === '!'; }).map(function (entry) { return entry.slice(1); });
|
|
236
|
+
for (const entry of includes) {
|
|
237
|
+
const rel = String(entry).replace(/^\.\//, '');
|
|
238
|
+
const source = path.join(srcRoot, rel);
|
|
239
|
+
if (!fs.existsSync(source)) continue;
|
|
240
|
+
const target = path.join(dstRoot, rel);
|
|
241
|
+
const st = fs.statSync(source);
|
|
242
|
+
if (st.isDirectory()) {
|
|
243
|
+
runtimeCopyTreeFiltered(source, target, excludes, rel);
|
|
244
|
+
} else if (st.isFile() && !excludes.some(function (pattern) { return runtimeMatchExclude(rel, pattern); })) {
|
|
245
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
246
|
+
fs.copyFileSync(source, target);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function runtimeRemoveVersionDir(version) {
|
|
251
|
+
const target = runtimeVersionDir(version);
|
|
252
|
+
if (!runtimePathWithin(runtimeVersionsDir(), target)) throw new Error('拒绝删除 runtime 目录外的路径: ' + target);
|
|
253
|
+
if (fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
function runtimeSwitchCurrent(version) {
|
|
256
|
+
const dir = runtimeVersionDir(version);
|
|
257
|
+
if (!fs.existsSync(path.join(dir, 'package.json'))) throw new Error('runtime 版本未安装: ' + version);
|
|
258
|
+
fs.mkdirSync(runtimeRoot(), { recursive: true });
|
|
259
|
+
const temp = path.join(runtimeRoot(), '.current-' + process.pid + '-' + Date.now());
|
|
260
|
+
const current = runtimeCurrentDir();
|
|
261
|
+
try {
|
|
262
|
+
fs.symlinkSync(dir, temp, process.platform === 'win32' ? 'junction' : 'dir');
|
|
263
|
+
if (fs.existsSync(current)) {
|
|
264
|
+
const st = fs.lstatSync(current);
|
|
265
|
+
if (st.isSymbolicLink() || st.isFile()) fs.unlinkSync(current);
|
|
266
|
+
else throw new Error('current 已存在且不是可替换的指针: ' + current);
|
|
267
|
+
}
|
|
268
|
+
fs.renameSync(temp, current);
|
|
269
|
+
} catch (e) {
|
|
270
|
+
try { if (fs.existsSync(temp)) fs.unlinkSync(temp); } catch (ignore) {}
|
|
271
|
+
throw e;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function runtimeInstallPrepared(pkgRoot, version, opts) {
|
|
275
|
+
opts = opts || {};
|
|
276
|
+
if (!isSafeRuntimeVersion(version)) return { error: true, text: '非法 runtime 版本: ' + version };
|
|
277
|
+
fs.mkdirSync(runtimeVersionsDir(), { recursive: true });
|
|
278
|
+
const finalDir = runtimeVersionDir(version);
|
|
279
|
+
const staging = path.join(runtimeVersionsDir(), '.' + version + '.tmp-' + process.pid + '-' + Date.now());
|
|
280
|
+
try {
|
|
281
|
+
runtimeCopyTree(pkgRoot, staging);
|
|
282
|
+
if (runtimePackageVersion(staging) !== version) throw new Error('安装包版本与目标版本不一致');
|
|
283
|
+
if (!fs.existsSync(path.join(staging, 'bin', 'yotta-memory.js'))) throw new Error('安装包缺少 bin/yotta-memory.js');
|
|
284
|
+
const treeHash = runtimeTreeHash(staging);
|
|
285
|
+
if (fs.existsSync(finalDir)) {
|
|
286
|
+
const existingHash = runtimeTreeHash(finalDir);
|
|
287
|
+
if (existingHash === treeHash) {
|
|
288
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
289
|
+
return { error: false, installed: false, version: version, treeHash: treeHash, path: finalDir, text: 'runtime ' + version + ' 已安装且内容一致。' };
|
|
290
|
+
}
|
|
291
|
+
if (!opts.force) throw new Error('runtime ' + version + ' 已安装且内容不同;如需覆盖请加 --force。');
|
|
292
|
+
runtimeRemoveVersionDir(version);
|
|
293
|
+
}
|
|
294
|
+
fs.renameSync(staging, finalDir);
|
|
295
|
+
const previousManifest = readRuntimeManifest();
|
|
296
|
+
const manifest = previousManifest || { schema: RUNTIME_SCHEMA, current: '', previous: '', versions: {}, updatedAt: '' };
|
|
297
|
+
manifest.schema = RUNTIME_SCHEMA;
|
|
298
|
+
manifest.versions = manifest.versions || {};
|
|
299
|
+
manifest.versions[version] = Object.assign({}, manifest.versions[version], {
|
|
300
|
+
version: version,
|
|
301
|
+
path: finalDir,
|
|
302
|
+
treeHash: treeHash,
|
|
303
|
+
installedAt: new Date().toISOString(),
|
|
304
|
+
source: opts.source || 'unknown',
|
|
305
|
+
});
|
|
306
|
+
if (!manifest.current) manifest.current = version;
|
|
307
|
+
manifest.updatedAt = new Date().toISOString();
|
|
308
|
+
writeRuntimeManifest(manifest);
|
|
309
|
+
if (manifest.current === version) {
|
|
310
|
+
try {
|
|
311
|
+
runtimeSwitchCurrent(version);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
if (previousManifest) writeRuntimeManifest(previousManifest);
|
|
314
|
+
return { error: true, text: '切换 current 失败: ' + e.message };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { error: false, installed: true, version: version, treeHash: treeHash, path: finalDir, text: '已安装 runtime ' + version + ' 到 ' + finalDir };
|
|
318
|
+
} catch (e) {
|
|
319
|
+
try { if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true }); } catch (ignore) {}
|
|
320
|
+
return { error: true, text: e.message };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function runtimeValidateTarballEntries(entries) {
|
|
324
|
+
const normalized = (entries || []).map(function (entry) {
|
|
325
|
+
return String(entry || '').replace(/\\/g, '/').replace(/\/+$/, '');
|
|
326
|
+
}).filter(Boolean);
|
|
327
|
+
if (!normalized.length) throw new Error('runtime tarball 为空');
|
|
328
|
+
for (const entry of normalized) {
|
|
329
|
+
if (entry[0] === '/' || /^[A-Za-z]:/.test(entry)) throw new Error('runtime tarball 含绝对路径成员: ' + entry);
|
|
330
|
+
const parts = entry.split('/');
|
|
331
|
+
if (parts.indexOf('..') !== -1) throw new Error('runtime tarball 含路径穿越成员: ' + entry);
|
|
332
|
+
if (parts[0] !== 'package') throw new Error('runtime tarball 含 package/ 之外的成员: ' + entry);
|
|
333
|
+
}
|
|
334
|
+
if (normalized.indexOf('package/package.json') === -1) throw new Error('runtime tarball 缺少 package/package.json');
|
|
335
|
+
}
|
|
336
|
+
function runtimeListTarballEntries(tarball) {
|
|
337
|
+
const tarBin = process.env.YOTTA_RUNTIME_TAR || 'tar';
|
|
338
|
+
const listed = child_process.spawnSync(tarBin, ['-tzf', tarball], { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
|
|
339
|
+
if (listed.error) throw new Error('无法执行 tar: ' + listed.error.message);
|
|
340
|
+
if (listed.status !== 0) throw new Error('读取 runtime tarball 失败: ' + String(listed.stderr || listed.stdout || '').trim());
|
|
341
|
+
return String(listed.stdout || '').split(/\r?\n/).filter(Boolean);
|
|
342
|
+
}
|
|
343
|
+
function runtimeExtractTarball(tarball, tempDir) {
|
|
344
|
+
runtimeValidateTarballEntries(runtimeListTarballEntries(tarball));
|
|
345
|
+
const tarBin = process.env.YOTTA_RUNTIME_TAR || 'tar';
|
|
346
|
+
const result = child_process.spawnSync(tarBin, ['-xzf', tarball, '-C', tempDir], { encoding: 'utf8' });
|
|
347
|
+
if (result.error) throw new Error('无法执行 tar: ' + result.error.message);
|
|
348
|
+
if (result.status !== 0) throw new Error('解压 runtime tarball 失败: ' + String(result.stderr || result.stdout || '').trim());
|
|
349
|
+
const packaged = path.join(tempDir, 'package');
|
|
350
|
+
if (fs.existsSync(path.join(packaged, 'package.json'))) return packaged;
|
|
351
|
+
if (fs.existsSync(path.join(tempDir, 'package.json'))) return tempDir;
|
|
352
|
+
throw new Error('runtime tarball 中未找到 package/package.json');
|
|
353
|
+
}
|
|
354
|
+
function runtimeInstallFromCurrent(opts) {
|
|
355
|
+
opts = opts || {};
|
|
356
|
+
const srcRoot = path.join(__dirname, '..');
|
|
357
|
+
const pkgPath = path.join(srcRoot, 'package.json');
|
|
358
|
+
if (!fs.existsSync(pkgPath)) return { error: true, text: '当前运行时不是完整 npm 包(缺少 package.json),无法安装稳定入口。' };
|
|
359
|
+
let version = '';
|
|
360
|
+
try { version = runtimePackageVersion(srcRoot); } catch (e) { return { error: true, text: e.message }; }
|
|
361
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ytm-runtime-current-'));
|
|
362
|
+
try {
|
|
363
|
+
const staged = path.join(temp, 'package');
|
|
364
|
+
runtimeCopyPackageFromRoot(srcRoot, staged);
|
|
365
|
+
return runtimeInstallPrepared(staged, version, { source: 'current', force: !!opts.force });
|
|
366
|
+
} catch (e) {
|
|
367
|
+
return { error: true, text: e.message };
|
|
368
|
+
} finally {
|
|
369
|
+
try { fs.rmSync(temp, { recursive: true, force: true }); } catch (ignore) {}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function runtimeInstallTarball(tarball, requestedVersion, opts) {
|
|
373
|
+
opts = opts || {};
|
|
374
|
+
const abs = path.resolve(String(tarball || ''));
|
|
375
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return { error: true, text: 'runtime tarball 不存在: ' + abs };
|
|
376
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ytm-runtime-tarball-'));
|
|
377
|
+
try {
|
|
378
|
+
const pkgRoot = runtimeExtractTarball(abs, temp);
|
|
379
|
+
const version = runtimePackageVersion(pkgRoot);
|
|
380
|
+
if (requestedVersion && String(requestedVersion) !== version) {
|
|
381
|
+
throw new Error('runtime 包版本 ' + version + ' 与请求版本 ' + requestedVersion + ' 不一致');
|
|
382
|
+
}
|
|
383
|
+
return runtimeInstallPrepared(pkgRoot, version, { source: abs, force: !!opts.force });
|
|
384
|
+
} catch (e) {
|
|
385
|
+
return { error: true, text: e.message };
|
|
386
|
+
} finally {
|
|
387
|
+
try { fs.rmSync(temp, { recursive: true, force: true }); } catch (ignore) {}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function runtimeNpmSpec() {
|
|
391
|
+
const candidates = [];
|
|
392
|
+
if (process.env.npm_execpath) candidates.push(process.env.npm_execpath);
|
|
393
|
+
candidates.push(path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'));
|
|
394
|
+
candidates.push(path.resolve(path.dirname(process.execPath), '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'));
|
|
395
|
+
for (const candidate of candidates) {
|
|
396
|
+
if (candidate && fs.existsSync(candidate)) return { command: process.execPath, args: [path.resolve(candidate)] };
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
function runtimeRunNpm(args, opts) {
|
|
401
|
+
const spec = runtimeNpmSpec();
|
|
402
|
+
if (!spec) throw new Error('无法定位 npm CLI;请改用 runtime install <本地 tarball>。');
|
|
403
|
+
const result = child_process.spawnSync(spec.command, spec.args.concat(args), {
|
|
404
|
+
cwd: (opts && opts.cwd) || process.cwd(),
|
|
405
|
+
encoding: 'utf8',
|
|
406
|
+
timeout: 120000,
|
|
407
|
+
});
|
|
408
|
+
if (result.error) throw new Error('npm 执行失败: ' + result.error.message);
|
|
409
|
+
if (result.status !== 0) throw new Error('npm 执行失败: ' + String(result.stderr || result.stdout || '').trim());
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
function runtimeInstallVersion(version, opts) {
|
|
413
|
+
const target = String(version || '').trim();
|
|
414
|
+
if (!isSafeRuntimeVersion(target)) return { error: true, text: '非法 runtime 版本: ' + target };
|
|
415
|
+
if (target === VERSION) return runtimeInstallFromCurrent(opts);
|
|
416
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ytm-runtime-pack-'));
|
|
417
|
+
try {
|
|
418
|
+
fs.mkdirSync(runtimeRoot(), { recursive: true });
|
|
419
|
+
runtimeRunNpm(['pack', '@yottameta/yotta-memory@' + target, '--pack-destination', temp], { cwd: runtimeRoot() });
|
|
420
|
+
const tarball = fs.readdirSync(temp).filter(function (name) { return /\.tgz$/i.test(name); })[0];
|
|
421
|
+
if (!tarball) throw new Error('npm pack 未生成 tarball');
|
|
422
|
+
return runtimeInstallTarball(path.join(temp, tarball), target, opts);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
return { error: true, text: e.message };
|
|
425
|
+
} finally {
|
|
426
|
+
try { fs.rmSync(temp, { recursive: true, force: true }); } catch (ignore) {}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function runtimeUseCore(version, opts) {
|
|
430
|
+
opts = opts || {};
|
|
431
|
+
const target = String(version || '').trim();
|
|
432
|
+
if (!isSafeRuntimeVersion(target)) return { error: true, text: '非法 runtime 版本: ' + target };
|
|
433
|
+
const manifest = readRuntimeManifest();
|
|
434
|
+
if (!manifest) return { error: true, text: 'runtime 未初始化,请先运行 yotta-memory runtime install --from-current。' };
|
|
435
|
+
const dir = runtimeVersionDir(target);
|
|
436
|
+
if (!fs.existsSync(dir)) return { error: true, text: 'runtime 版本未安装: ' + target };
|
|
437
|
+
const check = runtimeVerifyVersionDir(dir, target, manifest);
|
|
438
|
+
if (check.error) return check;
|
|
439
|
+
const previous = manifest.current && manifest.current !== target ? manifest.current : (manifest.previous && manifest.previous !== target ? manifest.previous : '');
|
|
440
|
+
const previousManifest = JSON.parse(JSON.stringify(manifest));
|
|
441
|
+
try {
|
|
442
|
+
runtimeSwitchCurrent(target);
|
|
443
|
+
} catch (e) {
|
|
444
|
+
return { error: true, text: '切换 current 失败: ' + e.message };
|
|
445
|
+
}
|
|
446
|
+
manifest.schema = RUNTIME_SCHEMA;
|
|
447
|
+
manifest.current = target;
|
|
448
|
+
manifest.previous = previous || '';
|
|
449
|
+
manifest.updatedAt = new Date().toISOString();
|
|
450
|
+
manifest.versions = manifest.versions || {};
|
|
451
|
+
manifest.versions[target] = Object.assign({}, manifest.versions[target], { treeHash: check.treeHash, lastUsedAt: new Date().toISOString() });
|
|
452
|
+
try {
|
|
453
|
+
writeRuntimeManifest(manifest);
|
|
454
|
+
} catch (e) {
|
|
455
|
+
try { if (previous) runtimeSwitchCurrent(previous); } catch (ignore) {}
|
|
456
|
+
return { error: true, text: '写入 runtime.json 失败: ' + e.message };
|
|
457
|
+
}
|
|
458
|
+
let tail = ';如需重启受管 server,请加 --restart';
|
|
459
|
+
if (opts.restart) {
|
|
460
|
+
const restart = opts.restartFn ? opts.restartFn() : runtimeRestartManagedServer();
|
|
461
|
+
if (restart && restart.error) {
|
|
462
|
+
let rollbackText = '';
|
|
463
|
+
if (previous) {
|
|
464
|
+
try {
|
|
465
|
+
runtimeSwitchCurrent(previous);
|
|
466
|
+
const rollback = JSON.parse(JSON.stringify(previousManifest));
|
|
467
|
+
rollback.current = previous;
|
|
468
|
+
rollback.previous = target;
|
|
469
|
+
rollback.updatedAt = new Date().toISOString();
|
|
470
|
+
writeRuntimeManifest(rollback);
|
|
471
|
+
(opts.restartFn ? opts.restartFn() : runtimeRestartManagedServer());
|
|
472
|
+
rollbackText = ';已回滚 current 到 ' + previous;
|
|
473
|
+
} catch (e) {
|
|
474
|
+
rollbackText = ';回滚失败: ' + e.message;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return { error: true, text: '受管 server 重启失败: ' + (restart.text || '') + rollbackText };
|
|
478
|
+
}
|
|
479
|
+
tail = ';受管 server 已重启';
|
|
480
|
+
}
|
|
481
|
+
return { error: false, text: '已切换 current 到 ' + target + tail + '。', version: target, treeHash: check.treeHash };
|
|
482
|
+
}
|
|
483
|
+
function runtimeRollbackCore(opts) {
|
|
484
|
+
const manifest = readRuntimeManifest();
|
|
485
|
+
if (!manifest || !manifest.current) return { error: true, text: 'runtime 未初始化或没有当前版本。' };
|
|
486
|
+
if (!manifest.previous) return { error: true, text: '没有可回滚的 runtime 版本。' };
|
|
487
|
+
const target = manifest.previous;
|
|
488
|
+
const result = runtimeUseCore(target, opts);
|
|
489
|
+
if (result.error) return result;
|
|
490
|
+
return { error: false, text: '已回滚 current 到 ' + target + '。' + result.text, version: target };
|
|
491
|
+
}
|
|
492
|
+
function runtimeStatusCore() {
|
|
493
|
+
const root = runtimeRoot();
|
|
494
|
+
const manifest = readRuntimeManifest();
|
|
495
|
+
const lines = ['runtime root: ' + root];
|
|
496
|
+
const drift = [];
|
|
497
|
+
if (!manifest) {
|
|
498
|
+
lines.push('runtime: 未初始化');
|
|
499
|
+
lines.push('提示: yotta-memory runtime install --from-current');
|
|
500
|
+
return { error: false, text: lines.join('\n'), runtimeRoot: root, drift: ['runtime manifest missing'] };
|
|
501
|
+
}
|
|
502
|
+
lines.push('current: ' + (manifest.current || '(none)'));
|
|
503
|
+
lines.push('current path: ' + runtimeCurrentDir());
|
|
504
|
+
if (manifest.previous) lines.push('previous: ' + manifest.previous);
|
|
505
|
+
if (!manifest.current) drift.push('current version missing in runtime.json');
|
|
506
|
+
else {
|
|
507
|
+
const dir = runtimeVersionDir(manifest.current);
|
|
508
|
+
if (!fs.existsSync(dir)) drift.push('current version dir missing');
|
|
509
|
+
if (!fs.existsSync(runtimeCurrentBin())) drift.push('current launcher missing');
|
|
510
|
+
else {
|
|
511
|
+
try {
|
|
512
|
+
const real = fs.realpathSync(runtimeCurrentDir());
|
|
513
|
+
if (path.resolve(real) !== path.resolve(dir)) drift.push('current pointer mismatch');
|
|
514
|
+
} catch (e) {
|
|
515
|
+
drift.push('current pointer unreadable');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const versions = runtimeListVersions();
|
|
520
|
+
lines.push('versions: ' + (versions.length ? versions.join(', ') : '(none)'));
|
|
521
|
+
lines.push('drift: ' + (drift.length ? drift.join('; ') : 'none'));
|
|
522
|
+
return { error: false, text: lines.join('\n'), runtimeRoot: root, manifest: manifest, drift: drift };
|
|
523
|
+
}
|
|
524
|
+
function runtimeListCore() {
|
|
525
|
+
const manifest = readRuntimeManifest() || { current: '', versions: {} };
|
|
526
|
+
const versions = runtimeListVersions();
|
|
527
|
+
const lines = ['runtime versions:'];
|
|
528
|
+
if (!versions.length) lines.push(' (none)');
|
|
529
|
+
for (const version of versions) {
|
|
530
|
+
const record = (manifest.versions && manifest.versions[version]) || {};
|
|
531
|
+
lines.push(' ' + version + (manifest.current === version ? ' [current]' : '') + (record.treeHash ? ' ' + record.treeHash.slice(0, 12) + '...' : ''));
|
|
532
|
+
}
|
|
533
|
+
return { error: false, text: lines.join('\n'), versions: versions, current: manifest.current || '' };
|
|
534
|
+
}
|
|
535
|
+
function runtimeExecutionPath() {
|
|
536
|
+
const invoked = process.argv && process.argv[1] ? path.resolve(process.argv[1]) : '';
|
|
537
|
+
if (invoked && path.basename(invoked).toLowerCase() === 'yotta-memory.js') return invoked;
|
|
538
|
+
return path.resolve(__filename);
|
|
539
|
+
}
|
|
540
|
+
function runtimeEnsureForManagedTask() {
|
|
541
|
+
const manifest = readRuntimeManifest();
|
|
542
|
+
if (manifest && manifest.current) {
|
|
543
|
+
const check = runtimeVerifyVersionDir(runtimeVersionDir(manifest.current), manifest.current, manifest);
|
|
544
|
+
if (!check.error && fs.existsSync(runtimeCurrentBin())) {
|
|
545
|
+
return { error: false, text: 'runtime current 已就绪: ' + manifest.current, version: manifest.current };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return runtimeInstallFromCurrent({ force: false });
|
|
549
|
+
}
|
|
550
|
+
function runtimeRestartManagedServer() {
|
|
551
|
+
const platform = lanPlatform();
|
|
552
|
+
if (platform === 'win32') {
|
|
553
|
+
const query = child_process.spawnSync('schtasks', ['/query', '/tn', LAN_TASK_NAME, '/fo', 'LIST'], { encoding: 'utf8' });
|
|
554
|
+
if (query.status !== 0) return { error: true, text: '未检测到受管计划任务 ' + LAN_TASK_NAME + '。' };
|
|
555
|
+
const end = child_process.spawnSync('schtasks', ['/end', '/tn', LAN_TASK_NAME], { encoding: 'utf8' });
|
|
556
|
+
const run = child_process.spawnSync('schtasks', ['/run', '/tn', LAN_TASK_NAME], { encoding: 'utf8' });
|
|
557
|
+
if (run.status !== 0) return { error: true, text: '计划任务重启失败: ' + String(run.stderr || run.stdout || '').trim() };
|
|
558
|
+
return { error: false, text: '已重启计划任务 ' + LAN_TASK_NAME + (end.status === 0 ? '' : '(原任务未在运行)') };
|
|
559
|
+
}
|
|
560
|
+
if (platform === 'linux') {
|
|
561
|
+
const unit = lanLinuxUnitPath();
|
|
562
|
+
if (fs.existsSync(unit) && lanFileHasMarker(unit)) {
|
|
563
|
+
const result = lanLinuxSystemctl(['restart', LAN_UNIT_NAME]);
|
|
564
|
+
if (result.status !== 0) return { error: true, text: 'systemd 重启失败: ' + String(result.err || '').trim() };
|
|
565
|
+
return { error: false, text: '已重启 systemd 用户单元 ' + LAN_UNIT_NAME };
|
|
566
|
+
}
|
|
567
|
+
if (lanCrontabHasOurLine(lanCrontabRead())) {
|
|
568
|
+
return { error: true, text: '受管 server 由 crontab @reboot 启动,无法在线重启;请手动重启。' };
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return { error: true, text: '未检测到可管理的 server 自启配置,未执行重启。' };
|
|
572
|
+
}
|
|
89
573
|
function today() {
|
|
90
574
|
const d = new Date();
|
|
91
575
|
const p = (n) => String(n).padStart(2, '0');
|
|
92
576
|
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
|
|
93
577
|
}
|
|
94
|
-
|
|
95
|
-
|
|
578
|
+
let RUNTIME_AGENT = { id: '', agentKey: '' };
|
|
579
|
+
const IDENTITY_CONTEXT = new AsyncLocalStorage();
|
|
580
|
+
const RUNTIME_OWNER_KEYS = new Map();
|
|
581
|
+
function setRuntimeAgent(id, agentKey) {
|
|
582
|
+
RUNTIME_AGENT = { id: String(id || '').trim(), agentKey: String(agentKey || '').trim() };
|
|
583
|
+
RUNTIME_OWNER_KEYS.clear();
|
|
584
|
+
}
|
|
585
|
+
function currentRuntimeIdentity() {
|
|
586
|
+
return IDENTITY_CONTEXT.getStore() || RUNTIME_AGENT;
|
|
587
|
+
}
|
|
588
|
+
function runWithIdentity(identity, fn) {
|
|
589
|
+
const store = {
|
|
590
|
+
id: String((identity && identity.id) || '').trim(),
|
|
591
|
+
agentKey: String((identity && identity.agentKey) || '').trim(),
|
|
592
|
+
};
|
|
593
|
+
return IDENTITY_CONTEXT.run(store, fn);
|
|
594
|
+
}
|
|
595
|
+
function legacyIdentityEnvNames() {
|
|
596
|
+
return ['YOTTA_AGENT_ID', 'AGENT_ID', 'YOTTA_MEMORY_AGENT_KEY', 'YOTTA_MEMORY_TRUST_ENV_AGENT']
|
|
597
|
+
.filter(function (name) { return String(process.env[name] || '').trim() !== ''; });
|
|
598
|
+
}
|
|
599
|
+
function legacyIdentityEnvError() {
|
|
600
|
+
const names = legacyIdentityEnvNames();
|
|
601
|
+
if (!names.length) return '';
|
|
602
|
+
return [
|
|
603
|
+
'[YTM_IDENTITY_ENV_REMOVED] 检测到旧身份环境变量: ' + names.join(', '),
|
|
604
|
+
'0.16.0 已删除身份 env:HTTP / 远程 MCP 只用请求头 Authorization + X-Agent-Id + X-Agent-Key;stdio MCP 只用显式参数 --agent-id <id> + --agent-key-file <path>。',
|
|
605
|
+
'CLI 仍使用 --agent <id> + --agent-key/--agent-key-file。请从宿主 MCP 配置中移除身份 env 后重试。',
|
|
606
|
+
].join('\n');
|
|
607
|
+
}
|
|
608
|
+
function resolveIdentity(opts) {
|
|
609
|
+
opts = opts || {};
|
|
610
|
+
const explicitAgent = String(opts.agent || opts.selfAgent || '').trim();
|
|
611
|
+
const explicitAgentId = String(opts.agentId || '').trim();
|
|
612
|
+
if (explicitAgent && explicitAgentId && explicitAgent !== explicitAgentId) {
|
|
613
|
+
return { id: '', agentKey: '', source: 'conflict', error: '身份冲突:--agent ' + explicitAgent + ' 与 --agent-id ' + explicitAgentId + ' 不一致,请只保留一个身份参数。' };
|
|
614
|
+
}
|
|
615
|
+
const explicit = explicitAgent || explicitAgentId;
|
|
616
|
+
let explicitKey = String(opts.agentKey || '').trim();
|
|
617
|
+
const keyFile = String(opts.agentKeyFile || '').trim();
|
|
618
|
+
if (keyFile) {
|
|
619
|
+
let fileKey = '';
|
|
620
|
+
try { fileKey = fs.readFileSync(path.resolve(keyFile), 'utf8').trim(); } catch (e) {
|
|
621
|
+
return { id: '', agentKey: '', source: 'invalid-key-file', error: '无法读取 agent-key 文件: ' + keyFile };
|
|
622
|
+
}
|
|
623
|
+
if (!fileKey) return { id: '', agentKey: '', source: 'invalid-key-file', error: 'agent-key 文件为空: ' + keyFile };
|
|
624
|
+
if (explicitKey && explicitKey !== fileKey) return { id: '', agentKey: '', source: 'key-conflict', error: '--agent-key 与 --agent-key-file 内容不一致。' };
|
|
625
|
+
explicitKey = fileKey;
|
|
626
|
+
}
|
|
627
|
+
if (explicit && !isSafeAgentId(explicit)) {
|
|
628
|
+
return { id: '', agentKey: '', source: 'invalid', error: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
629
|
+
}
|
|
630
|
+
if (explicit) {
|
|
631
|
+
return { id: explicit, agentKey: explicitKey, source: 'explicit', error: '' };
|
|
632
|
+
}
|
|
633
|
+
return { id: '', agentKey: '', source: 'none', error: '' };
|
|
634
|
+
}
|
|
635
|
+
function isSafeAgentId(id) {
|
|
636
|
+
const value = String(id || '');
|
|
637
|
+
if (!value || value.length > 128) return false;
|
|
638
|
+
if (value === '.' || value === '..') return false;
|
|
639
|
+
if (/[\\/]/.test(value)) return false;
|
|
640
|
+
if (/[\u0000-\u001f]/.test(value)) return false;
|
|
641
|
+
return true;
|
|
96
642
|
}
|
|
97
643
|
function typeDir(type) {
|
|
98
644
|
const t = String(type).toUpperCase();
|
|
@@ -528,8 +1074,12 @@ function nextSeq(dir) {
|
|
|
528
1074
|
const ENC_MAGIC = 'YTMENC1';
|
|
529
1075
|
const IDX_MAGIC = 'YTMIDX1';
|
|
530
1076
|
const KEY_MAGIC = 'YTMKEY1';
|
|
1077
|
+
const AGENT_MAGIC = 'YTMAGENT1';
|
|
531
1078
|
const KEYS_DIR = 'keys';
|
|
532
1079
|
const KEY_CACHE_DIR = 'cache';
|
|
1080
|
+
const AGENT_BINDINGS_DIR = 'bindings';
|
|
1081
|
+
const AGENT_PENDING_DIR = 'pending';
|
|
1082
|
+
const AGENT_KEY_FILENAME = '.yotta-memory-agent-key';
|
|
533
1083
|
const ENC_INDEX_FILE = 'index.enc';
|
|
534
1084
|
const ENC_SUFFIX = '.enc';
|
|
535
1085
|
const PBKDF2_ITER = 600000;
|
|
@@ -544,6 +1094,53 @@ function encOwnerRecPath(root, id) { return path.join(encKeysDir(root), id + '.k
|
|
|
544
1094
|
function encRecoveryEncPath(root) { return path.join(encKeysDir(root), 'recovery.key.enc'); }
|
|
545
1095
|
function encCacheDir(root) { return path.join(encKeysDir(root), KEY_CACHE_DIR); }
|
|
546
1096
|
function encCachePath(root, id) { return path.join(encCacheDir(root), id + '.key'); }
|
|
1097
|
+
function encBindingsDir(root) { return path.join(encKeysDir(root), AGENT_BINDINGS_DIR); }
|
|
1098
|
+
function encAgentBindingPath(root, id) { return path.join(encBindingsDir(root), id + '.key.agent'); }
|
|
1099
|
+
function encPendingDir(root) { return path.join(encKeysDir(root), AGENT_PENDING_DIR); }
|
|
1100
|
+
function encPendingPath(root, id) { return path.join(encPendingDir(root), id + '.key'); }
|
|
1101
|
+
function agentKeyFilePath(agentHome) { return path.join(path.resolve(agentHome), AGENT_KEY_FILENAME); }
|
|
1102
|
+
function detectAgentHost(owner) {
|
|
1103
|
+
const hint = String(process.env.YOTTA_MEMORY_AGENT_HOST || '').trim().toLowerCase();
|
|
1104
|
+
if (hint === 'codex' || hint === 'opencode') return hint;
|
|
1105
|
+
const id = String(owner || '').toLowerCase();
|
|
1106
|
+
if (/(^|[-_.])opencode([-_.]|$)/.test(id)) return 'opencode';
|
|
1107
|
+
if (id === 'codex' || id.indexOf('codex-') === 0) return 'codex';
|
|
1108
|
+
if (process.env.OPENCODE_API_KEY || process.env.OPENCODE_CONFIG) return 'opencode';
|
|
1109
|
+
if (process.env.CODEX_HOME) return 'codex';
|
|
1110
|
+
return 'generic';
|
|
1111
|
+
}
|
|
1112
|
+
function resolveAgentKeyLocation(owner, opts) {
|
|
1113
|
+
opts = opts || {};
|
|
1114
|
+
const explicitHome = String(opts.to || '').trim();
|
|
1115
|
+
const explicitFile = String(opts.agentKeyFile || '').trim();
|
|
1116
|
+
if (explicitHome && explicitFile) {
|
|
1117
|
+
return { ok: false, error: '--to 与 --agent-key-file 不能同时使用。' };
|
|
1118
|
+
}
|
|
1119
|
+
if (explicitFile) {
|
|
1120
|
+
return { ok: true, file: path.resolve(explicitFile), source: 'option:--agent-key-file' };
|
|
1121
|
+
}
|
|
1122
|
+
if (explicitHome) {
|
|
1123
|
+
return { ok: true, file: agentKeyFilePath(explicitHome), source: 'option:--to' };
|
|
1124
|
+
}
|
|
1125
|
+
const envFile = String(process.env.YOTTA_MEMORY_AGENT_KEY_FILE || '').trim();
|
|
1126
|
+
if (envFile) {
|
|
1127
|
+
return { ok: true, file: path.resolve(envFile), source: 'env:YOTTA_MEMORY_AGENT_KEY_FILE' };
|
|
1128
|
+
}
|
|
1129
|
+
const envHome = String(process.env.YOTTA_MEMORY_AGENT_HOME || '').trim();
|
|
1130
|
+
if (envHome) {
|
|
1131
|
+
return { ok: true, file: agentKeyFilePath(envHome), source: 'env:YOTTA_MEMORY_AGENT_HOME' };
|
|
1132
|
+
}
|
|
1133
|
+
const host = detectAgentHost(owner);
|
|
1134
|
+
if (host === 'codex') {
|
|
1135
|
+
const home = process.env.CODEX_HOME ? path.resolve(process.env.CODEX_HOME) : path.join(os.homedir(), '.codex');
|
|
1136
|
+
return { ok: true, file: agentKeyFilePath(home), source: 'host:codex' };
|
|
1137
|
+
}
|
|
1138
|
+
if (host === 'opencode') {
|
|
1139
|
+
const base = process.env.XDG_CONFIG_HOME ? path.resolve(process.env.XDG_CONFIG_HOME) : path.join(os.homedir(), '.config');
|
|
1140
|
+
return { ok: true, file: agentKeyFilePath(path.join(base, 'opencode')), source: 'host:opencode' };
|
|
1141
|
+
}
|
|
1142
|
+
return { ok: true, file: agentKeyFilePath(path.join(os.homedir(), '.' + owner)), source: 'host:generic' };
|
|
1143
|
+
}
|
|
547
1144
|
function isEncrypted(root) { return fs.existsSync(encKeysDir(root)); }
|
|
548
1145
|
function isEncFile(fp) { return String(fp).slice(-ENC_SUFFIX.length) === ENC_SUFFIX; }
|
|
549
1146
|
function ownerFromPrivatePath(root, fp) {
|
|
@@ -637,26 +1234,189 @@ function unwrapOwnerKeyRecovery(root, owner, rk) {
|
|
|
637
1234
|
if (buf.slice(0, KEY_MAGIC.length).toString('utf8') !== KEY_MAGIC) throw new Error('恢复密钥文件格式错误: ' + p);
|
|
638
1235
|
return aesDecryptBytes(rk, buf.slice(KEY_MAGIC.length), Buffer.from('owner:' + owner, 'utf8'));
|
|
639
1236
|
}
|
|
640
|
-
function
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
1237
|
+
function recoverOwnerKeyFromRecovery(root, owner, umk, rk) {
|
|
1238
|
+
if (!isSafeAgentId(owner) || !Buffer.isBuffer(rk)) return null;
|
|
1239
|
+
try {
|
|
1240
|
+
const ownerKey = unwrapOwnerKeyRecovery(root, owner, rk);
|
|
1241
|
+
if (!ownerKey) return null;
|
|
1242
|
+
fs.writeFileSync(encOwnerKeyPath(root, owner), Buffer.concat([
|
|
1243
|
+
Buffer.from(KEY_MAGIC, 'utf8'),
|
|
1244
|
+
aesEncryptBytes(umk, ownerKey, Buffer.from('owner:' + owner, 'utf8')),
|
|
1245
|
+
]));
|
|
1246
|
+
return ownerKey;
|
|
1247
|
+
} catch (e) {
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
645
1250
|
}
|
|
646
|
-
function
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1251
|
+
function privateOwnerHasData(root, owner) {
|
|
1252
|
+
if (!isSafeAgentId(owner)) return false;
|
|
1253
|
+
const dir = path.join(root, PRIVATE_DIR, owner);
|
|
1254
|
+
if (!fs.existsSync(dir)) return false;
|
|
1255
|
+
const stack = [dir];
|
|
1256
|
+
while (stack.length) {
|
|
1257
|
+
const current = stack.pop();
|
|
1258
|
+
let entries = [];
|
|
1259
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch (e) { continue; }
|
|
1260
|
+
for (const entry of entries) {
|
|
1261
|
+
if (entry.isDirectory()) {
|
|
1262
|
+
stack.push(path.join(current, entry.name));
|
|
1263
|
+
} else if (entry.isFile() && entry.name !== ENC_INDEX_FILE) {
|
|
1264
|
+
return true;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return false;
|
|
650
1269
|
}
|
|
651
1270
|
function revokeOwnerKeyCache(root, owner) {
|
|
652
1271
|
const p = encCachePath(root, owner);
|
|
653
1272
|
if (fs.existsSync(p)) { fs.unlinkSync(p); return true; }
|
|
654
1273
|
return false;
|
|
655
1274
|
}
|
|
656
|
-
function
|
|
1275
|
+
function writeAgentBinding(root, owner, ownerKey, agentKey) {
|
|
1276
|
+
if (!isSafeAgentId(owner)) throw new Error('非法 agent ID');
|
|
1277
|
+
if (!Buffer.isBuffer(agentKey) || agentKey.length !== 32) throw new Error('agent_key 必须是 32 字节');
|
|
1278
|
+
fs.mkdirSync(encBindingsDir(root), { recursive: true });
|
|
1279
|
+
const aad = Buffer.from('agent:' + owner, 'utf8');
|
|
1280
|
+
const env = aesEncryptBytes(agentKey, ownerKey, aad);
|
|
1281
|
+
fs.writeFileSync(encAgentBindingPath(root, owner), Buffer.concat([Buffer.from(AGENT_MAGIC, 'utf8'), env]));
|
|
1282
|
+
try { fs.chmodSync(encAgentBindingPath(root, owner), 0o600); } catch (e) {}
|
|
1283
|
+
}
|
|
1284
|
+
function writePendingAgentKey(root, owner, agentKey) {
|
|
1285
|
+
if (!isSafeAgentId(owner)) throw new Error('非法 agent ID');
|
|
1286
|
+
if (!Buffer.isBuffer(agentKey) || agentKey.length !== 32) throw new Error('agent_key 必须是 32 字节');
|
|
1287
|
+
fs.mkdirSync(encPendingDir(root), { recursive: true });
|
|
1288
|
+
const fp = encPendingPath(root, owner);
|
|
1289
|
+
fs.writeFileSync(fp, agentKey.toString('base64') + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
1290
|
+
try { fs.chmodSync(fp, 0o600); } catch (e) {}
|
|
1291
|
+
return fp;
|
|
1292
|
+
}
|
|
1293
|
+
function writeAgentBindingWithPending(root, owner, ownerKey, agentKey) {
|
|
1294
|
+
writeAgentBinding(root, owner, ownerKey, agentKey);
|
|
1295
|
+
try {
|
|
1296
|
+
return writePendingAgentKey(root, owner, agentKey);
|
|
1297
|
+
} catch (e) {
|
|
1298
|
+
try { fs.unlinkSync(encAgentBindingPath(root, owner)); } catch (ignore) {}
|
|
1299
|
+
try { fs.unlinkSync(encPendingPath(root, owner)); } catch (ignore) {}
|
|
1300
|
+
throw e;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function removePendingAgentKey(root, owner) {
|
|
1304
|
+
if (!isSafeAgentId(owner)) return false;
|
|
1305
|
+
const fp = encPendingPath(root, owner);
|
|
1306
|
+
if (!fs.existsSync(fp)) return false;
|
|
1307
|
+
fs.unlinkSync(fp);
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
1310
|
+
function readPendingAgentKey(root, owner) {
|
|
1311
|
+
if (!isSafeAgentId(owner)) return null;
|
|
1312
|
+
const fp = encPendingPath(root, owner);
|
|
1313
|
+
if (!fs.existsSync(fp)) return null;
|
|
1314
|
+
const raw = fs.readFileSync(fp, 'utf8').trim();
|
|
1315
|
+
if (!raw) throw new Error('pending agent_key 文件为空');
|
|
1316
|
+
const key = Buffer.from(raw, 'base64');
|
|
1317
|
+
if (key.length !== 32) throw new Error('pending agent_key 格式错误');
|
|
1318
|
+
return key;
|
|
1319
|
+
}
|
|
1320
|
+
function readAgentKeyFile(fp) {
|
|
1321
|
+
const raw = fs.readFileSync(path.resolve(String(fp)), 'utf8').trim();
|
|
1322
|
+
if (!raw) throw new Error('agent-key 文件为空');
|
|
1323
|
+
const key = Buffer.from(raw, 'base64');
|
|
1324
|
+
if (key.length !== 32) throw new Error('agent-key 文件格式错误');
|
|
1325
|
+
return key;
|
|
1326
|
+
}
|
|
1327
|
+
function unwrapAgentBinding(root, owner, agentKey) {
|
|
1328
|
+
if (!isSafeAgentId(owner)) return null;
|
|
1329
|
+
if (!Buffer.isBuffer(agentKey) || agentKey.length !== 32) return null;
|
|
1330
|
+
const p = encAgentBindingPath(root, owner);
|
|
1331
|
+
if (!fs.existsSync(p)) return null;
|
|
1332
|
+
const buf = fs.readFileSync(p);
|
|
1333
|
+
if (buf.slice(0, AGENT_MAGIC.length).toString('utf8') !== AGENT_MAGIC) throw new Error('agent binding 文件格式错误: ' + p);
|
|
1334
|
+
return aesDecryptBytes(agentKey, buf.slice(AGENT_MAGIC.length), Buffer.from('agent:' + owner, 'utf8'));
|
|
1335
|
+
}
|
|
1336
|
+
function getOwnerKeyFor(root, owner, identity) {
|
|
657
1337
|
if (!owner) return null;
|
|
658
1338
|
if (!isEncrypted(root)) return null;
|
|
659
|
-
|
|
1339
|
+
const current = identity || currentRuntimeIdentity();
|
|
1340
|
+
if (!current.id || current.id !== owner || !current.agentKey) return null;
|
|
1341
|
+
const agentKey = Buffer.from(current.agentKey, 'base64');
|
|
1342
|
+
return unwrapAgentBinding(root, owner, agentKey);
|
|
1343
|
+
}
|
|
1344
|
+
function validatePrivateIdentity(root, ident, owner) {
|
|
1345
|
+
if (!ident || !ident.id) return '私密操作必须先声明显式身份。';
|
|
1346
|
+
if (!isSafeAgentId(owner || ident.id)) return '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。';
|
|
1347
|
+
if (!isEncrypted(root)) return '';
|
|
1348
|
+
const current = currentRuntimeIdentity();
|
|
1349
|
+
const agentKey = ident.agentKey || (current.id === ident.id ? current.agentKey : '');
|
|
1350
|
+
if (!agentKey) {
|
|
1351
|
+
return '记忆库已加密,但缺少 agent_key。CLI 请使用 --agent-key <key> 或 --agent-key-file <path>;stdio MCP 请使用 --agent-id <id> + --agent-key-file <path>;HTTP MCP 请发送 X-Agent-Key 请求头。若该 agent 尚未绑定,请执行 yotta-memory key bind ' + (owner || ident.id) + '。';
|
|
1352
|
+
}
|
|
1353
|
+
const context = IDENTITY_CONTEXT.getStore();
|
|
1354
|
+
if (context) {
|
|
1355
|
+
context.id = ident.id;
|
|
1356
|
+
context.agentKey = agentKey;
|
|
1357
|
+
} else {
|
|
1358
|
+
setRuntimeAgent(ident.id, agentKey);
|
|
1359
|
+
}
|
|
1360
|
+
try {
|
|
1361
|
+
if (!getOwnerKeyFor(root, owner || ident.id, { id: ident.id, agentKey: agentKey })) {
|
|
1362
|
+
return 'agent_key 校验失败:未找到 ' + (owner || ident.id) + ' 的 agent binding(可能已吊销)。请由用户重新授权,或由用户执行 yotta-memory key bind ' + (owner || ident.id) + '。';
|
|
1363
|
+
}
|
|
1364
|
+
} catch (e) {
|
|
1365
|
+
return 'agent_key 无效或 binding 损坏:' + e.message;
|
|
1366
|
+
}
|
|
1367
|
+
return '';
|
|
1368
|
+
}
|
|
1369
|
+
function legacyCacheOwners(root) {
|
|
1370
|
+
if (!isEncrypted(root)) return [];
|
|
1371
|
+
const dir = encCacheDir(root);
|
|
1372
|
+
if (!fs.existsSync(dir)) return [];
|
|
1373
|
+
const found = [];
|
|
1374
|
+
let names = [];
|
|
1375
|
+
try { names = fs.readdirSync(dir); } catch (e) { return found; }
|
|
1376
|
+
for (const f of names) {
|
|
1377
|
+
if (!/\.key$/.test(f)) continue;
|
|
1378
|
+
const owner = f.slice(0, -4);
|
|
1379
|
+
if (!isSafeAgentId(owner)) continue;
|
|
1380
|
+
if (found.indexOf(owner) === -1) found.push(owner);
|
|
1381
|
+
}
|
|
1382
|
+
return found;
|
|
1383
|
+
}
|
|
1384
|
+
function migrationRequiredInfo(root) {
|
|
1385
|
+
if (!isEncrypted(root)) return null;
|
|
1386
|
+
const owners = new Set();
|
|
1387
|
+
for (const owner of collectOwners(root)) owners.add(owner);
|
|
1388
|
+
for (const owner of legacyCacheOwners(root)) owners.add(owner);
|
|
1389
|
+
const actionable = [];
|
|
1390
|
+
const legacyOnly = [];
|
|
1391
|
+
for (const owner of owners) {
|
|
1392
|
+
const hasPrivate = fs.existsSync(path.join(root, PRIVATE_DIR, owner)) || fs.existsSync(path.join(root, PRIVATE_DIR, owner, PROFILE_FILE)) || fs.existsSync(path.join(root, PRIVATE_DIR, owner, PROFILE_FILE + ENC_SUFFIX));
|
|
1393
|
+
const hasLegacy = fs.existsSync(encCachePath(root, owner));
|
|
1394
|
+
const bound = fs.existsSync(encAgentBindingPath(root, owner));
|
|
1395
|
+
if (!hasPrivate) {
|
|
1396
|
+
if (hasLegacy) legacyOnly.push(owner);
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (!bound) actionable.push(owner);
|
|
1400
|
+
}
|
|
1401
|
+
if (!actionable.length) return null;
|
|
1402
|
+
const reasons = [];
|
|
1403
|
+
if (actionable.some(function (owner) { return fs.existsSync(encCachePath(root, owner)); })) reasons.push('legacy_cache');
|
|
1404
|
+
reasons.push('no_agent_binding');
|
|
1405
|
+
return {
|
|
1406
|
+
marker: '[YTM_MIGRATION_REQUIRED] agent=' + actionable.join(',') + ' reason=' + reasons.join(','),
|
|
1407
|
+
owners: actionable,
|
|
1408
|
+
reasons: reasons,
|
|
1409
|
+
legacyOnly: legacyOnly,
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
function migrationReminderText(root) {
|
|
1413
|
+
const info = migrationRequiredInfo(root);
|
|
1414
|
+
if (!info) return '';
|
|
1415
|
+
let text = info.marker + '\nAI 提醒(升级后首次调用元忆 / 重启会话时请主动转达):旧版授权缓存不再加载,需要重新授权。请让用户自己执行 `yotta-memory view` → 浏览器打开平台 → 输入主口令 → 点「授权」并保存只展示一次的 `agent_key`;这是用户侧操作,AI 不得代替用户执行 `migrate` / `key bind` 迁移,只负责提醒和讲解步骤。';
|
|
1416
|
+
if (info.legacyOnly && info.legacyOnly.length) {
|
|
1417
|
+
text += '\n另有 legacy 明文 cache 缺少可迁移私密数据(不属于迁移清单): ' + info.legacyOnly.join(', ') + ';如需清理请单独确认。';
|
|
1418
|
+
}
|
|
1419
|
+
return text;
|
|
660
1420
|
}
|
|
661
1421
|
function encryptMemoryText(text, ownerKey) {
|
|
662
1422
|
const fileKey = crypto.randomBytes(32);
|
|
@@ -1080,7 +1840,7 @@ function hasGrant(userAgent, ownerAgent) {
|
|
|
1080
1840
|
return false;
|
|
1081
1841
|
}
|
|
1082
1842
|
// 三态读取判定:'read' | 'denied'
|
|
1083
|
-
// selfAgent
|
|
1843
|
+
// selfAgent 为调用方显式声明的可信身份(CLI 参数 / stdio 参数 / HTTP 请求头);--owner 仅供授权与展示,不得授予跨智能体私密读取。
|
|
1084
1844
|
function classifyRead(entry, agent, ownerFilter, unsafe, selfAgent) {
|
|
1085
1845
|
if (entry.scope === 'public') return 'read';
|
|
1086
1846
|
const owner = entry.owner || '';
|
|
@@ -1272,7 +2032,7 @@ function backupWindowsTaskXml(opts) {
|
|
|
1272
2032
|
opts = opts || {};
|
|
1273
2033
|
const time = backupTimeParts(opts.time || '03:30');
|
|
1274
2034
|
const nodePath = opts.nodePath || process.execPath;
|
|
1275
|
-
const scriptPath = opts.scriptPath ||
|
|
2035
|
+
const scriptPath = opts.scriptPath || runtimeManagedScript();
|
|
1276
2036
|
const userId = opts.userId || '';
|
|
1277
2037
|
const principal = userId
|
|
1278
2038
|
? '<Principal id="Author"><UserId>' + xmlEscape(userId) + '</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal>'
|
|
@@ -1305,7 +2065,7 @@ function backupWindowsTaskXml(opts) {
|
|
|
1305
2065
|
function backupSystemdServiceContent(opts) {
|
|
1306
2066
|
opts = opts || {};
|
|
1307
2067
|
const nodePath = opts.nodePath || process.execPath;
|
|
1308
|
-
const scriptPath = opts.scriptPath ||
|
|
2068
|
+
const scriptPath = opts.scriptPath || runtimeManagedScript();
|
|
1309
2069
|
return [
|
|
1310
2070
|
'[Unit]',
|
|
1311
2071
|
'Description=Yotta Memory daily backup',
|
|
@@ -1338,7 +2098,7 @@ function backupCronLine(opts) {
|
|
|
1338
2098
|
opts = opts || {};
|
|
1339
2099
|
const time = backupTimeParts(opts.time || '03:30');
|
|
1340
2100
|
const nodePath = opts.nodePath || process.execPath;
|
|
1341
|
-
const scriptPath = opts.scriptPath ||
|
|
2101
|
+
const scriptPath = opts.scriptPath || runtimeManagedScript();
|
|
1342
2102
|
const logPath = opts.logPath || '';
|
|
1343
2103
|
const redirect = logPath ? ' >> ' + shQuote(logPath) + ' 2>&1' : '';
|
|
1344
2104
|
return time.minute + ' ' + time.hour + ' * * * ' + shQuote(nodePath) + ' ' + shQuote(scriptPath) + ' backup ensure-daily' + redirect + ' ' + BACKUP_CRON_MARKER;
|
|
@@ -1347,7 +2107,7 @@ function backupLaunchdPlist(opts) {
|
|
|
1347
2107
|
opts = opts || {};
|
|
1348
2108
|
const time = backupTimeParts(opts.time || '03:30');
|
|
1349
2109
|
const nodePath = opts.nodePath || process.execPath;
|
|
1350
|
-
const scriptPath = opts.scriptPath ||
|
|
2110
|
+
const scriptPath = opts.scriptPath || runtimeManagedScript();
|
|
1351
2111
|
const logPath = opts.logPath || '';
|
|
1352
2112
|
const lines = [
|
|
1353
2113
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
@@ -1398,7 +2158,7 @@ function backupScheduleEnableCore(opts) {
|
|
|
1398
2158
|
const time = opts.time || '03:30';
|
|
1399
2159
|
try { backupTimeParts(time); } catch (error) { return { error: true, text: error.message }; }
|
|
1400
2160
|
const nodePath = opts.nodePath || process.execPath;
|
|
1401
|
-
const scriptPath = opts.scriptPath ||
|
|
2161
|
+
const scriptPath = opts.scriptPath || runtimeManagedScript();
|
|
1402
2162
|
try {
|
|
1403
2163
|
if (platform === 'win32') {
|
|
1404
2164
|
const userId = opts.userId || (process.env.USERDOMAIN && process.env.USERNAME ? process.env.USERDOMAIN + '\\' + process.env.USERNAME : '');
|
|
@@ -1517,7 +2277,7 @@ function backupScheduleStatusCore(opts) {
|
|
|
1517
2277
|
return { error: false, registered: false, scheduler: '', text: '当前平台不支持自动备份调度: ' + platform };
|
|
1518
2278
|
}
|
|
1519
2279
|
function backupFallbackCommand() {
|
|
1520
|
-
return { command: process.execPath, args: [
|
|
2280
|
+
return { command: process.execPath, args: [runtimeManagedScript(), 'backup', 'ensure-daily'] };
|
|
1521
2281
|
}
|
|
1522
2282
|
function startBackupFallback() {
|
|
1523
2283
|
const cfg = loadConfig();
|
|
@@ -1798,6 +2558,348 @@ function backupHealthCore(opts) {
|
|
|
1798
2558
|
}
|
|
1799
2559
|
return { level: 'none', needsSetup: false, text: '' };
|
|
1800
2560
|
}
|
|
2561
|
+
// ---- v0.16.0 M3: runtime drift diagnosis ----
|
|
2562
|
+
function runtimePathList(value) {
|
|
2563
|
+
const pattern = path.delimiter === ':' ? /[;:\n]/ : /[;\n]/;
|
|
2564
|
+
return String(value || '').split(pattern).map(function (item) { return item.trim(); }).filter(Boolean);
|
|
2565
|
+
}
|
|
2566
|
+
function runtimeVersionFromDir(dir) {
|
|
2567
|
+
let current = path.resolve(String(dir || ''));
|
|
2568
|
+
for (let i = 0; i < 8; i++) {
|
|
2569
|
+
const pkgPath = path.join(current, 'package.json');
|
|
2570
|
+
if (fs.existsSync(pkgPath)) {
|
|
2571
|
+
try {
|
|
2572
|
+
const version = String(JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || '').trim();
|
|
2573
|
+
if (version) return version;
|
|
2574
|
+
} catch (e) {}
|
|
2575
|
+
}
|
|
2576
|
+
const skillPath = path.join(current, 'SKILL.md');
|
|
2577
|
+
if (fs.existsSync(skillPath)) {
|
|
2578
|
+
try {
|
|
2579
|
+
const text = fs.readFileSync(skillPath, 'utf8');
|
|
2580
|
+
const match = text.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
2581
|
+
if (match) {
|
|
2582
|
+
const versionLine = match[1].split(/\r?\n/).find(function (line) { return /^version\s*:/.test(line); });
|
|
2583
|
+
if (versionLine) {
|
|
2584
|
+
const version = versionLine.slice(versionLine.indexOf(':') + 1).trim().replace(/^["']|["']$/g, '');
|
|
2585
|
+
if (version) return version;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
} catch (e) {}
|
|
2589
|
+
}
|
|
2590
|
+
const parent = path.dirname(current);
|
|
2591
|
+
if (parent === current) break;
|
|
2592
|
+
current = parent;
|
|
2593
|
+
}
|
|
2594
|
+
return '';
|
|
2595
|
+
}
|
|
2596
|
+
function runtimeVersionFromPath(target) {
|
|
2597
|
+
const value = String(target || '').trim();
|
|
2598
|
+
if (!value) return '';
|
|
2599
|
+
const normalized = value.replace(/\\/g, '/');
|
|
2600
|
+
const versionMatch = normalized.match(/\/versions\/([^/]+)\/bin\/yotta-memory\.js$/i);
|
|
2601
|
+
if (versionMatch) return versionMatch[1];
|
|
2602
|
+
const abs = path.resolve(value);
|
|
2603
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) return runtimeVersionFromDir(abs);
|
|
2604
|
+
const base = path.basename(abs).toLowerCase();
|
|
2605
|
+
const dir = path.dirname(abs);
|
|
2606
|
+
return runtimeVersionFromDir(base === 'yotta-memory.js' ? path.dirname(dir) : dir);
|
|
2607
|
+
}
|
|
2608
|
+
function runtimeExtractLauncherPaths(text) {
|
|
2609
|
+
const found = new Set();
|
|
2610
|
+
const add = function (value) {
|
|
2611
|
+
const raw = String(value || '').trim().replace(/^['"]|['"]$/g, '');
|
|
2612
|
+
if (!raw) return;
|
|
2613
|
+
const parts = splitCommandArgv(raw);
|
|
2614
|
+
for (const part of parts) {
|
|
2615
|
+
const token = part.replace(/^['"]|['"]$/g, '');
|
|
2616
|
+
if (/yotta-memory\.js$/i.test(token.replace(/[;,]$/, ''))) found.add(token.replace(/[;,]$/, ''));
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
const parsed = [];
|
|
2620
|
+
try {
|
|
2621
|
+
const value = JSON.parse(String(text || ''));
|
|
2622
|
+
const walk = function (node) {
|
|
2623
|
+
if (typeof node === 'string') parsed.push(node);
|
|
2624
|
+
else if (Array.isArray(node)) node.forEach(walk);
|
|
2625
|
+
else if (node && typeof node === 'object') Object.keys(node).forEach(function (key) { walk(node[key]); });
|
|
2626
|
+
};
|
|
2627
|
+
walk(value);
|
|
2628
|
+
} catch (e) {}
|
|
2629
|
+
for (const value of parsed) if (/yotta-memory\.js/i.test(value)) add(value);
|
|
2630
|
+
const raw = String(text || '').replace(/\\\\/g, '\\');
|
|
2631
|
+
const patterns = [
|
|
2632
|
+
/[A-Za-z]:[\\/][^"'\r\n|;]+?yotta-memory\.js/gi,
|
|
2633
|
+
/(?:\.{0,2}[\\/])?[^"'\s|;]+?yotta-memory\.js/gi,
|
|
2634
|
+
];
|
|
2635
|
+
for (const pattern of patterns) {
|
|
2636
|
+
let match;
|
|
2637
|
+
while ((match = pattern.exec(raw)) !== null) add(match[0]);
|
|
2638
|
+
}
|
|
2639
|
+
return Array.from(found);
|
|
2640
|
+
}
|
|
2641
|
+
function runtimeConfigIdentityMode(text) {
|
|
2642
|
+
const raw = String(text || '');
|
|
2643
|
+
if (/YOTTA_AGENT_ID|YOTTA_MEMORY_AGENT_KEY|YOTTA_MEMORY_TRUST_ENV_AGENT/.test(raw)) return 'legacy-env';
|
|
2644
|
+
if (/--agent-id/.test(raw) && /--agent-key-file/.test(raw)) return 'stdio-args';
|
|
2645
|
+
if (/X-Agent-Id|Authorization/i.test(raw)) return 'headers';
|
|
2646
|
+
return 'unknown';
|
|
2647
|
+
}
|
|
2648
|
+
function runtimeDiscoverProcesses() {
|
|
2649
|
+
try {
|
|
2650
|
+
if (process.platform === 'win32') {
|
|
2651
|
+
const script = "$p=Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*yotta-memory*' }; if ($p) { $p | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress }";
|
|
2652
|
+
const result = child_process.spawnSync('powershell.exe', ['-NoProfile', '-Command', script], {
|
|
2653
|
+
encoding: 'utf8',
|
|
2654
|
+
timeout: 15000,
|
|
2655
|
+
windowsHide: true,
|
|
2656
|
+
});
|
|
2657
|
+
if (result.status !== 0 || !String(result.stdout || '').trim()) return [];
|
|
2658
|
+
const parsed = JSON.parse(String(result.stdout).trim());
|
|
2659
|
+
const rows = Array.isArray(parsed) ? parsed : [parsed];
|
|
2660
|
+
return rows.map(function (row) {
|
|
2661
|
+
return { pid: row.ProcessId, commandLine: row.CommandLine || '' };
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
const result = child_process.spawnSync('ps', ['-eo', 'pid=,args='], { encoding: 'utf8', timeout: 10000 });
|
|
2665
|
+
if (result.status !== 0) return [];
|
|
2666
|
+
return String(result.stdout || '').split(/\r?\n/).filter(Boolean).map(function (line) {
|
|
2667
|
+
const match = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
2668
|
+
return match ? { pid: parseInt(match[1], 10), commandLine: match[2] } : null;
|
|
2669
|
+
}).filter(Boolean);
|
|
2670
|
+
} catch (e) {
|
|
2671
|
+
return [];
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
function runtimeDiscoverSkillDirs() {
|
|
2675
|
+
const explicit = runtimePathList(process.env.YOTTA_MEMORY_SKILL_DIRS);
|
|
2676
|
+
if (explicit.length) return explicit;
|
|
2677
|
+
const candidates = [];
|
|
2678
|
+
const add = function (base) {
|
|
2679
|
+
if (!base) return;
|
|
2680
|
+
candidates.push(path.join(base, 'skills', 'yotta-memory'));
|
|
2681
|
+
candidates.push(path.join(base, 'yotta-memory'));
|
|
2682
|
+
};
|
|
2683
|
+
add(process.env.CODEX_HOME);
|
|
2684
|
+
if (process.env.XDG_CONFIG_HOME) candidates.push(path.join(process.env.XDG_CONFIG_HOME, 'opencode', 'skills', 'yotta-memory'));
|
|
2685
|
+
add(process.env.HERMES_HOME);
|
|
2686
|
+
add(path.join(os.homedir(), '.codex'));
|
|
2687
|
+
candidates.push(path.join(os.homedir(), '.config', 'opencode', 'skills', 'yotta-memory'));
|
|
2688
|
+
return Array.from(new Set(candidates)).filter(function (dir) { return fs.existsSync(dir); });
|
|
2689
|
+
}
|
|
2690
|
+
function runtimeDoctorCore(opts) {
|
|
2691
|
+
opts = opts || {};
|
|
2692
|
+
const drifts = [];
|
|
2693
|
+
const warnings = [];
|
|
2694
|
+
const mcpConfigPaths = opts.mcpConfigPaths !== undefined
|
|
2695
|
+
? opts.mcpConfigPaths
|
|
2696
|
+
: runtimePathList(process.env.YOTTA_MEMORY_MCP_CONFIGS);
|
|
2697
|
+
const skillDirs = opts.skillDirs !== undefined ? opts.skillDirs : runtimeDiscoverSkillDirs();
|
|
2698
|
+
const processes = opts.processes !== undefined ? opts.processes : runtimeDiscoverProcesses();
|
|
2699
|
+
const manifest = readRuntimeManifest();
|
|
2700
|
+
const currentDir = runtimeCurrentDir();
|
|
2701
|
+
const currentBin = runtimeCurrentBin();
|
|
2702
|
+
const manifestVersion = manifest && manifest.current ? String(manifest.current) : '';
|
|
2703
|
+
const installedCurrentVersion = fs.existsSync(path.join(currentDir, 'package.json'))
|
|
2704
|
+
? runtimeVersionFromDir(currentDir)
|
|
2705
|
+
: '';
|
|
2706
|
+
|
|
2707
|
+
const checks = {
|
|
2708
|
+
cli: { path: runtimeExecutionPath(), version: VERSION },
|
|
2709
|
+
current: {
|
|
2710
|
+
path: currentDir,
|
|
2711
|
+
version: manifestVersion || installedCurrentVersion || '',
|
|
2712
|
+
installedVersion: installedCurrentVersion,
|
|
2713
|
+
manifestVersion: manifestVersion,
|
|
2714
|
+
},
|
|
2715
|
+
runtimeRoot: runtimeRoot(),
|
|
2716
|
+
mcpConfigs: [],
|
|
2717
|
+
runningServers: [],
|
|
2718
|
+
skillCopies: [],
|
|
2719
|
+
identityModes: [],
|
|
2720
|
+
drifts: drifts,
|
|
2721
|
+
};
|
|
2722
|
+
|
|
2723
|
+
if (!manifest || !manifest.current) {
|
|
2724
|
+
drifts.push({
|
|
2725
|
+
kind: 'current-runtime',
|
|
2726
|
+
source: runtimeManifestPath(),
|
|
2727
|
+
actual: 'missing',
|
|
2728
|
+
expected: VERSION,
|
|
2729
|
+
fix: 'yotta-memory runtime install --from-current',
|
|
2730
|
+
blocking: true,
|
|
2731
|
+
});
|
|
2732
|
+
} else {
|
|
2733
|
+
const versionDir = runtimeVersionDir(manifest.current);
|
|
2734
|
+
if (!fs.existsSync(versionDir)) {
|
|
2735
|
+
drifts.push({
|
|
2736
|
+
kind: 'current-runtime',
|
|
2737
|
+
source: runtimeManifestPath(),
|
|
2738
|
+
actual: manifest.current,
|
|
2739
|
+
expected: VERSION,
|
|
2740
|
+
fix: 'yotta-memory runtime install --from-current && yotta-memory runtime use ' + VERSION + ' --restart',
|
|
2741
|
+
blocking: true,
|
|
2742
|
+
});
|
|
2743
|
+
} else if (manifest.current !== VERSION) {
|
|
2744
|
+
drifts.push({
|
|
2745
|
+
kind: 'current-runtime',
|
|
2746
|
+
source: runtimeManifestPath(),
|
|
2747
|
+
actual: manifest.current,
|
|
2748
|
+
expected: VERSION,
|
|
2749
|
+
fix: 'yotta-memory runtime install --from-current && yotta-memory runtime use ' + VERSION + ' --restart',
|
|
2750
|
+
blocking: true,
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
if (fs.existsSync(versionDir)) {
|
|
2754
|
+
if (!fs.existsSync(currentBin)) {
|
|
2755
|
+
drifts.push({
|
|
2756
|
+
kind: 'current-runtime',
|
|
2757
|
+
source: currentDir,
|
|
2758
|
+
actual: 'launcher-missing',
|
|
2759
|
+
expected: VERSION,
|
|
2760
|
+
fix: 'yotta-memory runtime use ' + manifest.current + ' --force',
|
|
2761
|
+
blocking: true,
|
|
2762
|
+
});
|
|
2763
|
+
} else {
|
|
2764
|
+
try {
|
|
2765
|
+
const real = fs.realpathSync(currentDir);
|
|
2766
|
+
if (path.resolve(real) !== path.resolve(versionDir)) {
|
|
2767
|
+
drifts.push({
|
|
2768
|
+
kind: 'current-runtime',
|
|
2769
|
+
source: currentDir,
|
|
2770
|
+
actual: 'pointer-mismatch',
|
|
2771
|
+
expected: manifest.current,
|
|
2772
|
+
fix: 'yotta-memory runtime use ' + manifest.current + ' --restart',
|
|
2773
|
+
blocking: true,
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
} catch (e) {
|
|
2777
|
+
drifts.push({
|
|
2778
|
+
kind: 'current-runtime',
|
|
2779
|
+
source: currentDir,
|
|
2780
|
+
actual: 'pointer-unreadable',
|
|
2781
|
+
expected: manifest.current,
|
|
2782
|
+
fix: 'yotta-memory runtime use ' + manifest.current + ' --restart',
|
|
2783
|
+
blocking: true,
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
const legacyEnv = legacyIdentityEnvNames();
|
|
2791
|
+
if (legacyEnv.length) {
|
|
2792
|
+
drifts.push({
|
|
2793
|
+
kind: 'identity-mode',
|
|
2794
|
+
source: 'process-env',
|
|
2795
|
+
actual: 'legacy-env',
|
|
2796
|
+
expected: 'headers|stdio-args',
|
|
2797
|
+
fix: '移除 ' + legacyEnv.join(', ') + ',HTTP 改用请求头,stdio 改用 --agent-id + --agent-key-file',
|
|
2798
|
+
blocking: true,
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
for (const configPath of mcpConfigPaths) {
|
|
2803
|
+
const abs = path.resolve(String(configPath));
|
|
2804
|
+
let text = '';
|
|
2805
|
+
try {
|
|
2806
|
+
text = fs.readFileSync(abs, 'utf8');
|
|
2807
|
+
} catch (e) {
|
|
2808
|
+
warnings.push('MCP 配置无法读取: ' + abs);
|
|
2809
|
+
checks.mcpConfigs.push({ path: abs, readable: false, version: 'unknown', identityMode: 'unknown' });
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
const launchers = runtimeExtractLauncherPaths(text);
|
|
2813
|
+
const identityMode = runtimeConfigIdentityMode(text);
|
|
2814
|
+
const version = launchers.length ? (runtimeVersionFromPath(launchers[0]) || 'unknown') : 'unknown';
|
|
2815
|
+
checks.mcpConfigs.push({ path: abs, readable: true, launcher: launchers[0] || '', version: version, identityMode: identityMode });
|
|
2816
|
+
checks.identityModes.push({ source: abs, mode: identityMode });
|
|
2817
|
+
if (identityMode === 'legacy-env') {
|
|
2818
|
+
drifts.push({
|
|
2819
|
+
kind: 'identity-mode',
|
|
2820
|
+
source: abs,
|
|
2821
|
+
actual: 'legacy-env',
|
|
2822
|
+
expected: 'headers|stdio-args',
|
|
2823
|
+
fix: '从 MCP 配置移除身份 env,HTTP 改用请求头,stdio 改用 --agent-id + --agent-key-file',
|
|
2824
|
+
blocking: true,
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
if (version !== 'unknown' && version !== VERSION) {
|
|
2828
|
+
drifts.push({
|
|
2829
|
+
kind: 'mcp-config',
|
|
2830
|
+
source: abs,
|
|
2831
|
+
actual: version,
|
|
2832
|
+
expected: VERSION,
|
|
2833
|
+
fix: '把 MCP 配置中的运行时路径改为 ' + runtimeCurrentBin() + ' 后重启该 MCP',
|
|
2834
|
+
blocking: true,
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
for (const processInfo of processes || []) {
|
|
2840
|
+
const row = typeof processInfo === 'string' ? { pid: '', commandLine: processInfo } : (processInfo || {});
|
|
2841
|
+
const commandLine = String(row.commandLine || '');
|
|
2842
|
+
if (!commandLine || commandLine.indexOf('yotta-memory') === -1 || commandLine.indexOf('serve') === -1) continue;
|
|
2843
|
+
const launchers = runtimeExtractLauncherPaths(commandLine);
|
|
2844
|
+
const launcher = launchers[0] || '';
|
|
2845
|
+
const version = launcher ? (runtimeVersionFromPath(launcher) || 'unknown') : 'unknown';
|
|
2846
|
+
checks.runningServers.push({ pid: row.pid || '', launcher: launcher, version: version });
|
|
2847
|
+
if (version !== 'unknown' && version !== VERSION) {
|
|
2848
|
+
drifts.push({
|
|
2849
|
+
kind: 'running-server',
|
|
2850
|
+
source: 'pid=' + String(row.pid || 'unknown'),
|
|
2851
|
+
actual: version,
|
|
2852
|
+
expected: VERSION,
|
|
2853
|
+
fix: 'yotta-memory runtime use ' + VERSION + ' --restart 或重启对应 MCP 客户端',
|
|
2854
|
+
blocking: false,
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
for (const skillDir of skillDirs) {
|
|
2860
|
+
const abs = path.resolve(String(skillDir));
|
|
2861
|
+
if (!fs.existsSync(path.join(abs, 'SKILL.md'))) continue;
|
|
2862
|
+
const version = runtimeVersionFromDir(abs) || 'unknown';
|
|
2863
|
+
checks.skillCopies.push({ path: abs, version: version });
|
|
2864
|
+
if (version !== 'unknown' && version !== VERSION) {
|
|
2865
|
+
drifts.push({
|
|
2866
|
+
kind: 'skill-copy',
|
|
2867
|
+
source: abs,
|
|
2868
|
+
actual: version,
|
|
2869
|
+
expected: VERSION,
|
|
2870
|
+
fix: '用官方安装器更新该 yotta-memory 技能副本',
|
|
2871
|
+
blocking: false,
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
const lines = [
|
|
2877
|
+
'## 运行时一致性',
|
|
2878
|
+
'- CLI: ' + VERSION + ' (' + runtimeExecutionPath() + ')',
|
|
2879
|
+
'- current: ' + (checks.current.version || '(missing)') + ' (' + currentDir + ')',
|
|
2880
|
+
'- runtime.json: ' + runtimeManifestPath(),
|
|
2881
|
+
];
|
|
2882
|
+
if (checks.mcpConfigs.length) {
|
|
2883
|
+
for (const item of checks.mcpConfigs) lines.push('- MCP 配置: ' + item.path + ' -> ' + item.version + ' / ' + item.identityMode);
|
|
2884
|
+
} else lines.push('- MCP 配置: (未提供或未发现)');
|
|
2885
|
+
if (checks.runningServers.length) {
|
|
2886
|
+
for (const item of checks.runningServers) lines.push('- 运行中 server: pid=' + item.pid + ' -> ' + item.version);
|
|
2887
|
+
} else lines.push('- 运行中 server: (未发现)');
|
|
2888
|
+
if (checks.skillCopies.length) {
|
|
2889
|
+
for (const item of checks.skillCopies) lines.push('- 技能副本: ' + item.path + ' -> ' + item.version);
|
|
2890
|
+
} else lines.push('- 技能副本: (未提供或未发现)');
|
|
2891
|
+
if (!drifts.length) lines.push('- 漂移: 无');
|
|
2892
|
+
for (const drift of drifts) {
|
|
2893
|
+
lines.push('- [漂移] ' + drift.kind + ': actual=' + drift.actual + ' expected=' + drift.expected + ' fix=' + drift.fix + ' blocking=' + (drift.blocking ? 'yes' : 'no'));
|
|
2894
|
+
}
|
|
2895
|
+
return {
|
|
2896
|
+
ok: !drifts.some(function (drift) { return drift.blocking; }),
|
|
2897
|
+
checks: checks,
|
|
2898
|
+
drifts: drifts,
|
|
2899
|
+
warnings: warnings,
|
|
2900
|
+
text: lines.join('\n'),
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
1801
2903
|
// 开工可靠性检查:只读检查根目录、密钥库、索引、身份登记与最近备份。
|
|
1802
2904
|
function doctorCore(opts) {
|
|
1803
2905
|
opts = opts || {};
|
|
@@ -1849,6 +2951,14 @@ function doctorCore(opts) {
|
|
|
1849
2951
|
}
|
|
1850
2952
|
}
|
|
1851
2953
|
|
|
2954
|
+
const migration = migrationRequiredInfo(root);
|
|
2955
|
+
if (migration) {
|
|
2956
|
+
checks.agentBindings = { ok: false, pending: migration.owners };
|
|
2957
|
+
warnings.push(migrationReminderText(root));
|
|
2958
|
+
} else if (encrypted) {
|
|
2959
|
+
checks.agentBindings = { ok: true, pending: [] };
|
|
2960
|
+
}
|
|
2961
|
+
|
|
1852
2962
|
const configuredDir = opts.dir || opts.backupDir || cfg.backup_dir;
|
|
1853
2963
|
if (!configuredDir) {
|
|
1854
2964
|
if (cfg.backup_setup_choice === 'manual') {
|
|
@@ -1888,6 +2998,18 @@ function doctorCore(opts) {
|
|
|
1888
2998
|
if (cfg.backup_scheduler_error) warnings.push('每日备份调度异常: ' + cfg.backup_scheduler_error);
|
|
1889
2999
|
}
|
|
1890
3000
|
|
|
3001
|
+
let runtimeReport = null;
|
|
3002
|
+
if (opts.runtime) {
|
|
3003
|
+
runtimeReport = runtimeDoctorCore(opts);
|
|
3004
|
+
checks.runtime = runtimeReport.checks;
|
|
3005
|
+
for (const drift of runtimeReport.drifts) {
|
|
3006
|
+
const message = '运行时漂移 [' + drift.kind + ']: actual=' + drift.actual + ' expected=' + drift.expected + ';修复: ' + drift.fix + ';阻断: ' + (drift.blocking ? '是' : '否');
|
|
3007
|
+
if (drift.blocking) critical.push(message);
|
|
3008
|
+
else warnings.push(message);
|
|
3009
|
+
}
|
|
3010
|
+
for (const warning of runtimeReport.warnings || []) warnings.push(warning);
|
|
3011
|
+
}
|
|
3012
|
+
|
|
1891
3013
|
const level = critical.length ? 'critical' : (warnings.length ? 'warning' : 'ok');
|
|
1892
3014
|
const lines = [
|
|
1893
3015
|
'# yotta-memory doctor(开工可靠性检查)',
|
|
@@ -1900,6 +3022,10 @@ function doctorCore(opts) {
|
|
|
1900
3022
|
for (const message of critical) lines.push('- [严重] ' + message);
|
|
1901
3023
|
for (const message of warnings) lines.push('- [警告] ' + message);
|
|
1902
3024
|
if (!critical.length && !warnings.length) lines.push('- 检查项: 全部通过');
|
|
3025
|
+
if (runtimeReport) {
|
|
3026
|
+
lines.push('');
|
|
3027
|
+
lines.push(runtimeReport.text);
|
|
3028
|
+
}
|
|
1903
3029
|
if (level === 'critical') {
|
|
1904
3030
|
lines.push('');
|
|
1905
3031
|
lines.push('- 破坏性写入已锁定:先运行 yotta-memory doctor 修复严重问题。');
|
|
@@ -2009,7 +3135,10 @@ function isAllowedBackupPath(rel) {
|
|
|
2009
3135
|
const top = normalized.split('/')[0];
|
|
2010
3136
|
if (top === 'facts' || top === 'private' || top === '.archive') return true;
|
|
2011
3137
|
if (top === 'agents.json' || top === 'index.json' || /^index-\d{4}\.json$/.test(top)) return true;
|
|
2012
|
-
if (top === 'keys')
|
|
3138
|
+
if (top === 'keys') {
|
|
3139
|
+
return normalized !== 'keys/cache' && normalized.indexOf('keys/cache/') !== 0 &&
|
|
3140
|
+
normalized !== 'keys/pending' && normalized.indexOf('keys/pending/') !== 0;
|
|
3141
|
+
}
|
|
2013
3142
|
return false;
|
|
2014
3143
|
}
|
|
2015
3144
|
function backupCreateCore(opts) {
|
|
@@ -2196,18 +3325,12 @@ function backupDrillCore(opts) {
|
|
|
2196
3325
|
if (!salt) throw new Error('恢复副本缺少 keys/salt');
|
|
2197
3326
|
ownerKey = unwrapOwnerKey(target, privateFile.owner, deriveUmk(opts.password, salt));
|
|
2198
3327
|
source = 'password';
|
|
2199
|
-
} else {
|
|
2200
|
-
const ownerKeyFile = opts.ownerKeyFile || path.join(opts.sourceRoot || userRoot(), 'keys', 'cache', privateFile.owner + '.key');
|
|
2201
|
-
if (fs.existsSync(ownerKeyFile)) {
|
|
2202
|
-
ownerKey = fs.readFileSync(ownerKeyFile);
|
|
2203
|
-
source = 'owner-cache';
|
|
2204
|
-
}
|
|
2205
3328
|
}
|
|
2206
3329
|
} catch (error) {
|
|
2207
3330
|
ownerKey = null;
|
|
2208
3331
|
}
|
|
2209
3332
|
if (!ownerKey) {
|
|
2210
|
-
return fail('恢复演练失败: 已恢复文件结构,但无法解密测试私密记忆。请提供 --recovery-key 或 --password
|
|
3333
|
+
return fail('恢复演练失败: 已恢复文件结构,但无法解密测试私密记忆。请提供 --recovery-key 或 --password;legacy keys/cache 明文缓存不再作为恢复凭证。', {
|
|
2211
3334
|
manifest: true,
|
|
2212
3335
|
index: true,
|
|
2213
3336
|
private: { checked: true, decrypted: false, source: '', file: privateFile.rel },
|
|
@@ -2253,14 +3376,22 @@ function rememberCore(type, subject, statement, opts) {
|
|
|
2253
3376
|
const subj = String(subject || '').trim();
|
|
2254
3377
|
if (!stmt) return { error: true, text: 'statement 不能为空' };
|
|
2255
3378
|
if (!subj) return { error: true, text: 'subject 不能为空' };
|
|
2256
|
-
const
|
|
3379
|
+
const ident = resolveIdentity(opts);
|
|
3380
|
+
const selfAgent = ident.id;
|
|
2257
3381
|
const owner = opts.owner || selfAgent;
|
|
2258
3382
|
const scope = opts.scope || defaultScope(t);
|
|
3383
|
+
if (scope === 'private' && ident.error) {
|
|
3384
|
+
return { error: true, text: ident.error };
|
|
3385
|
+
}
|
|
3386
|
+
if (scope === 'private') {
|
|
3387
|
+
const keyError = validatePrivateIdentity(root, ident, owner);
|
|
3388
|
+
if (keyError) return { error: true, text: keyError };
|
|
3389
|
+
}
|
|
2259
3390
|
if (scope === 'private' && !owner) {
|
|
2260
|
-
return { error: true, text: '
|
|
3391
|
+
return { error: true, text: '私密记忆必须显式声明归属智能体:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。公共记忆(FACT)不受影响。' };
|
|
2261
3392
|
}
|
|
2262
3393
|
if (scope === 'private' && owner && selfAgent && owner !== selfAgent && !opts.unsafe) {
|
|
2263
|
-
return { error: true, text: '拒绝:
|
|
3394
|
+
return { error: true, text: '拒绝: 当前显式身份 ' + selfAgent + ' 不能写入其它智能体 ' + owner + ' 的私密区。请传正确的 --agent <id>,或加 --unsafe(用户显式授权)。' };
|
|
2264
3395
|
}
|
|
2265
3396
|
const encrypted = isEncrypted(root);
|
|
2266
3397
|
if (scope === 'private' && encrypted && !getOwnerKeyFor(root, owner)) {
|
|
@@ -2366,12 +3497,31 @@ function recallCore(query, opts) {
|
|
|
2366
3497
|
const limit = opts.limit || 50;
|
|
2367
3498
|
const onlyType = opts.type ? String(opts.type).toUpperCase() : null;
|
|
2368
3499
|
const q = query ? String(query).toLowerCase() : '';
|
|
2369
|
-
const
|
|
2370
|
-
|
|
3500
|
+
const ident = resolveIdentity(opts);
|
|
3501
|
+
if (ident.error) return { error: true, exitCode: 3, text: ident.error };
|
|
3502
|
+
const selfAgent = ident.id;
|
|
3503
|
+
const agent = selfAgent;
|
|
2371
3504
|
const ownerFilter = opts.owner || '';
|
|
2372
3505
|
const allSafe = !!opts.unsafe;
|
|
2373
|
-
|
|
2374
|
-
|
|
3506
|
+
if (ownerFilter && !isSafeAgentId(ownerFilter)) {
|
|
3507
|
+
return { error: true, exitCode: 3, text: '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
3508
|
+
}
|
|
3509
|
+
if (!selfAgent && (opts.all || ownerFilter)) {
|
|
3510
|
+
return {
|
|
3511
|
+
error: true,
|
|
3512
|
+
exitCode: 3,
|
|
3513
|
+
text: '显式跨智能体读取必须先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3516
|
+
if (selfAgent && !allSafe) {
|
|
3517
|
+
for (const root of roots) {
|
|
3518
|
+
const keyError = validatePrivateIdentity(root, ident, ownerFilter || selfAgent);
|
|
3519
|
+
if (keyError && isEncrypted(root)) {
|
|
3520
|
+
return { error: true, exitCode: 3, text: keyError };
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
const explicitCross = !!opts.all || (!!ownerFilter && ownerFilter !== agent);
|
|
2375
3525
|
const wantExplain = !!opts.explain;
|
|
2376
3526
|
const useSemantic = opts.semantic !== false;
|
|
2377
3527
|
const prefilter = (q && useSemantic) ? recallPrefilter(q) : null;
|
|
@@ -2483,6 +3633,7 @@ function recallCore(query, opts) {
|
|
|
2483
3633
|
}
|
|
2484
3634
|
}
|
|
2485
3635
|
const lines = ['共 ' + shown.length + ' 条记忆(' + (hits.length > limit ? '前 ' + limit + ' 条' : '全部') + '):'];
|
|
3636
|
+
if (!selfAgent) lines.push('[注意] 未声明身份,本次仅读取公共 FACT;私密 PREF / BOUND / COMMIT 已跳过。');
|
|
2486
3637
|
for (const h of shown) {
|
|
2487
3638
|
lines.push('[' + h.entry.type + '] ' + h.entry.subject + ': ' + h.entry.statement);
|
|
2488
3639
|
lines.push(' ' + path.join(h.root, h.entry.file));
|
|
@@ -2532,7 +3683,7 @@ function appendTrashAudit(root, record) {
|
|
|
2532
3683
|
}
|
|
2533
3684
|
function forgetCore(fileRef, opts) {
|
|
2534
3685
|
opts = opts || {};
|
|
2535
|
-
const selfAgent = opts.
|
|
3686
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
2536
3687
|
const roots = memoryRoots();
|
|
2537
3688
|
const ref = String(fileRef || '').replace(/\\/g, '/');
|
|
2538
3689
|
let target = null, targetRoot = null, targetRel = null;
|
|
@@ -2545,7 +3696,7 @@ function forgetCore(fileRef, opts) {
|
|
|
2545
3696
|
if (seg[0] === 'private') {
|
|
2546
3697
|
const owner = seg[1] || '';
|
|
2547
3698
|
if (!opts.unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
|
|
2548
|
-
return { error: true, text: '拒绝: 不能删除其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用
|
|
3699
|
+
return { error: true, text: '拒绝: 不能删除其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 --agent / --agent-id 声明自己的身份,或加 --unsafe(用户显式授权)。' };
|
|
2549
3700
|
}
|
|
2550
3701
|
}
|
|
2551
3702
|
const trashFile = path.join(
|
|
@@ -2642,7 +3793,7 @@ function checkOwnerWritable(targetRoot, targetRel, selfAgent, unsafe) {
|
|
|
2642
3793
|
if (seg[0] === PRIVATE_DIR) {
|
|
2643
3794
|
const owner = seg[1] || '';
|
|
2644
3795
|
if (!unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
|
|
2645
|
-
return '拒绝: 不能操作其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用
|
|
3796
|
+
return '拒绝: 不能操作其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 --agent / --agent-id 声明自己的身份,或加 --unsafe(用户显式授权)。';
|
|
2646
3797
|
}
|
|
2647
3798
|
}
|
|
2648
3799
|
return null;
|
|
@@ -2650,7 +3801,7 @@ function checkOwnerWritable(targetRoot, targetRel, selfAgent, unsafe) {
|
|
|
2650
3801
|
// 显式使用反馈:useful/useless → weight/confidence/feedback_net 演化(自我学习闭环)
|
|
2651
3802
|
function feedbackCore(ref, opts) {
|
|
2652
3803
|
opts = opts || {};
|
|
2653
|
-
const selfAgent = opts.
|
|
3804
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
2654
3805
|
const target = resolveMemoryTarget(ref);
|
|
2655
3806
|
if (!target) return { error: true, text: '未找到记忆文件: ' + ref };
|
|
2656
3807
|
const deny = checkOwnerWritable(target.root, target.rel, selfAgent, opts.unsafe);
|
|
@@ -2709,7 +3860,7 @@ function feedbackUndoCore(target, owner, meta, selfAgent, opts) {
|
|
|
2709
3860
|
// 解释单条记忆效用分项(为什么靠前/归档/遗忘)
|
|
2710
3861
|
function explainCore(ref, opts) {
|
|
2711
3862
|
opts = opts || {};
|
|
2712
|
-
const selfAgent = opts.
|
|
3863
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
2713
3864
|
const target = resolveMemoryTarget(ref);
|
|
2714
3865
|
if (!target) return { error: true, text: '未找到记忆文件: ' + ref };
|
|
2715
3866
|
const owner = ownerFromPrivatePath(target.root, target.fp);
|
|
@@ -2864,7 +4015,7 @@ function maintainCore(opts) {
|
|
|
2864
4015
|
// 手动合并两条相似记忆:保留高 confidence,低 confidence 移入 .archive
|
|
2865
4016
|
function mergeCore(refA, refB, opts) {
|
|
2866
4017
|
opts = opts || {};
|
|
2867
|
-
const selfAgent = opts.
|
|
4018
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
2868
4019
|
const ta = resolveMemoryTarget(refA);
|
|
2869
4020
|
const tb = resolveMemoryTarget(refB);
|
|
2870
4021
|
if (!ta || !tb) return { error: true, text: '未找到记忆文件: ' + refA + ' / ' + refB };
|
|
@@ -2908,7 +4059,7 @@ function distillCore(opts) {
|
|
|
2908
4059
|
opts = opts || {};
|
|
2909
4060
|
const root = userRoot();
|
|
2910
4061
|
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
|
|
2911
|
-
const selfAgent = opts.
|
|
4062
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
2912
4063
|
const owner = opts.owner || selfAgent;
|
|
2913
4064
|
const entries = [];
|
|
2914
4065
|
for (const r of memoryRoots()) {
|
|
@@ -3059,6 +4210,8 @@ function promptPassword(promptText) {
|
|
|
3059
4210
|
}
|
|
3060
4211
|
|
|
3061
4212
|
function migrateCore(root, password, recoveryKeyIn) {
|
|
4213
|
+
// 第三个参数历史上是 recovery key;兼容旧调用,同时支持 { agent, recoveryKey } 选项对象。
|
|
4214
|
+
const opts = recoveryKeyIn && typeof recoveryKeyIn === 'object' ? recoveryKeyIn : {};
|
|
3062
4215
|
if (!fs.existsSync(root)) return { error: true, text: '记忆库不存在,请先 yotta-memory init。' };
|
|
3063
4216
|
if (isEncrypted(root)) return { error: true, text: '记忆库已启用加密(keys/ 存在),无需迁移。' };
|
|
3064
4217
|
if (!hasPlaintextPrivate(root)) return { error: false, text: '无私密明文可迁移(private/ 下无 .md 文件);如需新建加密库请 init --encrypt。' };
|
|
@@ -3070,10 +4223,9 @@ function migrateCore(root, password, recoveryKeyIn) {
|
|
|
3070
4223
|
fs.writeFileSync(encRecoveryEncPath(root), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(umk, rk, Buffer.from('recovery', 'utf8'))]));
|
|
3071
4224
|
let moved = 0;
|
|
3072
4225
|
const owners = collectOwners(root);
|
|
3073
|
-
const self =
|
|
4226
|
+
const self = resolveIdentity(opts).id;
|
|
3074
4227
|
for (const owner of owners) {
|
|
3075
4228
|
const ok = wrapOwnerKey(root, umk, rk, owner);
|
|
3076
|
-
if (self && owner === self) writeOwnerKeyCache(root, owner, ok);
|
|
3077
4229
|
for (const t of PRIVATE_LEAF) {
|
|
3078
4230
|
const d = path.join(root, PRIVATE_DIR, owner, t);
|
|
3079
4231
|
if (!fs.existsSync(d)) continue;
|
|
@@ -3097,8 +4249,8 @@ function migrateCore(root, password, recoveryKeyIn) {
|
|
|
3097
4249
|
buildIndex(root);
|
|
3098
4250
|
let tail = '已迁移 ' + moved + ' 个私密文件(' + owners.length + ' 个 owner)到密文。';
|
|
3099
4251
|
tail += '\n[恢复钥匙](务必离线保存,仅此一次,泄露=可解全部私密): ' + rk.toString('base64');
|
|
3100
|
-
|
|
3101
|
-
|
|
4252
|
+
tail += '\n迁移不会写明文 owner key cache;请为每个 agent 执行一次 yotta-memory key bind <id>,再注入 agent_key。';
|
|
4253
|
+
if (self && owners.indexOf(self) !== -1) tail += '\n你的身份:' + self + ',请先执行 yotta-memory key bind ' + self + '。';
|
|
3102
4254
|
return { error: false, text: tail };
|
|
3103
4255
|
}
|
|
3104
4256
|
|
|
@@ -3111,7 +4263,7 @@ async function cmdMigrate(opts) {
|
|
|
3111
4263
|
password = await promptPassword('主口令(迁移后私密区加密,勿忘;忘口令可用恢复钥匙重设): ') || '';
|
|
3112
4264
|
if (!password) { console.error('已取消。'); process.exit(2); }
|
|
3113
4265
|
}
|
|
3114
|
-
const r = migrateCore(root, password, null);
|
|
4266
|
+
const r = migrateCore(root, password, { agent: opts.agent, recoveryKey: null });
|
|
3115
4267
|
console.log(r.text);
|
|
3116
4268
|
if (r.error) process.exit(2);
|
|
3117
4269
|
}
|
|
@@ -3177,51 +4329,176 @@ async function cmdResetPassword(opts) {
|
|
|
3177
4329
|
if (r.error) process.exit(2);
|
|
3178
4330
|
}
|
|
3179
4331
|
|
|
3180
|
-
function
|
|
4332
|
+
function keyBindCore(root, owner, opts) {
|
|
4333
|
+
opts = opts || {};
|
|
4334
|
+
if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
3181
4335
|
if (!isEncrypted(root)) return { error: true, text: '记忆库未启用加密。' };
|
|
3182
4336
|
const salt = loadSalt(root);
|
|
3183
4337
|
if (!salt) return { error: true, text: '密钥库损坏(缺 keys/salt)。' };
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
4338
|
+
let ownerKey = null;
|
|
4339
|
+
if (opts.recoveryKey) {
|
|
4340
|
+
const rk = Buffer.from(String(opts.recoveryKey), 'base64');
|
|
4341
|
+
if (rk.length !== 32) return { error: true, text: '恢复钥匙须为 32 字节(base64)。' };
|
|
4342
|
+
try { ownerKey = unwrapOwnerKeyRecovery(root, owner, rk); } catch (e) { ownerKey = null; }
|
|
4343
|
+
if (!ownerKey) return { error: true, text: '恢复钥匙无法解开 ' + owner + ' 的 owner key。' };
|
|
4344
|
+
} else {
|
|
4345
|
+
const password = String(opts.password || '');
|
|
4346
|
+
if (!password) return { error: true, text: '请提供主口令(--password 或交互输入),或用 --recovery-key。' };
|
|
4347
|
+
const umk = deriveUmk(password, salt);
|
|
4348
|
+
try { ownerKey = unwrapOwnerKey(root, owner, umk); } catch (e) { ownerKey = null; }
|
|
4349
|
+
if (!ownerKey) {
|
|
4350
|
+
let rk = null;
|
|
4351
|
+
try { rk = unwrapRecoveryEnc(root, umk); } catch (e) { rk = null; }
|
|
4352
|
+
if (!rk) return { error: true, text: '口令错误,或该 owner 的密钥不存在。' };
|
|
4353
|
+
ownerKey = recoverOwnerKeyFromRecovery(root, owner, umk, rk);
|
|
4354
|
+
if (!ownerKey) {
|
|
4355
|
+
if (privateOwnerHasData(root, owner)) {
|
|
4356
|
+
return { error: true, text: '检测到 private/' + owner + ' 仍有密文,但 keys/' + owner + '.key.enc 与恢复文件均不可用;拒绝新建 owner key(会导致旧数据无法解密)。请从备份恢复原 owner key,或使用恢复钥匙修复后重试。' };
|
|
4357
|
+
}
|
|
4358
|
+
ownerKey = wrapOwnerKey(root, umk, rk, owner);
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
3192
4361
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
4362
|
+
const agentKey = crypto.randomBytes(32);
|
|
4363
|
+
const pendingFile = writeAgentBindingWithPending(root, owner, ownerKey, agentKey);
|
|
4364
|
+
revokeOwnerKeyCache(root, owner);
|
|
4365
|
+
return {
|
|
4366
|
+
error: false,
|
|
4367
|
+
agentKey: agentKey.toString('base64'),
|
|
4368
|
+
pendingFile: pendingFile,
|
|
4369
|
+
text: '已绑定 ' + owner + ' 的 agent_key,并删除旧明文 cache。\nagent_key: ' + agentKey.toString('base64') + '\n待领取文件: ' + pendingFile + '\nAI 在新会话执行 yotta-memory key status ' + owner + ',有 pending 再执行 yotta-memory key claim ' + owner + ';用户请另外保存此 key;此 key 只显示一次。',
|
|
4370
|
+
};
|
|
3195
4371
|
}
|
|
3196
4372
|
|
|
3197
4373
|
function keyRevokeCore(root, owner) {
|
|
3198
|
-
if (!
|
|
3199
|
-
|
|
4374
|
+
if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
4375
|
+
let removed = false;
|
|
4376
|
+
const binding = encAgentBindingPath(root, owner);
|
|
4377
|
+
if (fs.existsSync(binding)) { fs.unlinkSync(binding); removed = true; }
|
|
4378
|
+
if (removePendingAgentKey(root, owner)) removed = true;
|
|
4379
|
+
if (revokeOwnerKeyCache(root, owner)) removed = true;
|
|
4380
|
+
if (!removed) return { error: false, text: owner + ' 无 agent binding(已是未绑定状态)。' };
|
|
4381
|
+
return { error: false, text: '已吊销 ' + owner + ' 的 agent_key binding;该 agent 将无法解密其私密。' };
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
function keyClaimCore(root, owner, opts) {
|
|
4385
|
+
if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
4386
|
+
const location = resolveAgentKeyLocation(owner, opts);
|
|
4387
|
+
if (!location.ok) return { error: true, text: location.error };
|
|
4388
|
+
const pending = encPendingPath(root, owner);
|
|
4389
|
+
if (!fs.existsSync(pending)) {
|
|
4390
|
+
return { error: true, text: '未找到待领取 agent_key:' + pending + '。请先在 yotta-memory view 中授权,或执行 yotta-memory key bind ' + owner + '。' };
|
|
4391
|
+
}
|
|
4392
|
+
let agentKey;
|
|
4393
|
+
try {
|
|
4394
|
+
agentKey = readPendingAgentKey(root, owner);
|
|
4395
|
+
if (!agentKey) throw new Error('pending agent_key 不存在');
|
|
4396
|
+
if (!unwrapAgentBinding(root, owner, agentKey)) {
|
|
4397
|
+
throw new Error('agent binding 不存在或已失效');
|
|
4398
|
+
}
|
|
4399
|
+
} catch (e) {
|
|
4400
|
+
return { error: true, text: 'agent_key 校验失败:' + e.message + '。未写入宿主目录,pending 文件保留。' };
|
|
4401
|
+
}
|
|
4402
|
+
let targetDir;
|
|
4403
|
+
let targetFile;
|
|
4404
|
+
let tempFile;
|
|
4405
|
+
try {
|
|
4406
|
+
targetFile = location.file;
|
|
4407
|
+
targetDir = path.dirname(targetFile);
|
|
4408
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
4409
|
+
tempFile = path.join(targetDir, AGENT_KEY_FILENAME + '.tmp-' + process.pid + '-' + crypto.randomBytes(4).toString('hex'));
|
|
4410
|
+
fs.writeFileSync(tempFile, agentKey.toString('base64') + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
4411
|
+
try { fs.chmodSync(tempFile, 0o600); } catch (e) {}
|
|
4412
|
+
try {
|
|
4413
|
+
fs.renameSync(tempFile, targetFile);
|
|
4414
|
+
} catch (e) {
|
|
4415
|
+
fs.rmSync(targetFile, { force: true });
|
|
4416
|
+
fs.renameSync(tempFile, targetFile);
|
|
4417
|
+
}
|
|
4418
|
+
const readBack = fs.readFileSync(targetFile, 'utf8').trim();
|
|
4419
|
+
if (readBack !== agentKey.toString('base64')) throw new Error('宿主 key 文件回读不一致');
|
|
4420
|
+
} catch (e) {
|
|
4421
|
+
try { if (tempFile) fs.rmSync(tempFile, { force: true }); } catch (ignore) {}
|
|
4422
|
+
return { error: true, text: '写入宿主 key 文件失败:' + e.message + '。pending 文件保留,可重试。' };
|
|
4423
|
+
}
|
|
4424
|
+
removePendingAgentKey(root, owner);
|
|
4425
|
+
return {
|
|
4426
|
+
error: false,
|
|
4427
|
+
targetFile: targetFile,
|
|
4428
|
+
source: location.source,
|
|
4429
|
+
text: '已领取 ' + owner + ' 的 agent_key。\n目标文件: ' + targetFile + '\n发现规则: ' + location.source + '\npending 文件已删除。',
|
|
4430
|
+
};
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
function keyStatusCore(root, owner, opts) {
|
|
4434
|
+
if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
4435
|
+
const location = resolveAgentKeyLocation(owner, opts);
|
|
4436
|
+
if (!location.ok) return { error: true, text: location.error };
|
|
4437
|
+
const binding = fs.existsSync(encAgentBindingPath(root, owner));
|
|
4438
|
+
const pending = fs.existsSync(encPendingPath(root, owner));
|
|
4439
|
+
let hostKey = 'missing';
|
|
4440
|
+
if (fs.existsSync(location.file)) {
|
|
4441
|
+
hostKey = 'invalid';
|
|
4442
|
+
try {
|
|
4443
|
+
const key = readAgentKeyFile(location.file);
|
|
4444
|
+
if (binding && unwrapAgentBinding(root, owner, key)) hostKey = 'valid';
|
|
4445
|
+
} catch (e) {}
|
|
4446
|
+
}
|
|
4447
|
+
return {
|
|
4448
|
+
error: false,
|
|
4449
|
+
binding: binding,
|
|
4450
|
+
pending: pending,
|
|
4451
|
+
hostKey: hostKey,
|
|
4452
|
+
targetFile: location.file,
|
|
4453
|
+
source: location.source,
|
|
4454
|
+
text: 'agent: ' + owner + '\nbinding: ' + (binding ? 'yes' : 'no') + '\npending: ' + (pending ? 'yes' : 'no') + '\nhost_key: ' + hostKey + '\nchecked: ' + location.file + '\ndiscovery: ' + location.source,
|
|
4455
|
+
};
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
function keyListCore(root) {
|
|
4459
|
+
root = root || userRoot();
|
|
4460
|
+
if (!isEncrypted(root)) return { text: '记忆库未启用加密。' };
|
|
4461
|
+
const owners = collectOwners(root);
|
|
4462
|
+
const bound = owners.filter(function (o) { return fs.existsSync(encAgentBindingPath(root, o)); });
|
|
4463
|
+
const pending = owners.filter(function (o) { return fs.existsSync(encPendingPath(root, o)); });
|
|
4464
|
+
const legacy = owners.filter(function (o) { return fs.existsSync(encCachePath(root, o)); });
|
|
4465
|
+
const lines = [
|
|
4466
|
+
'owner: ' + (owners.length ? owners.join(', ') : '(无)'),
|
|
4467
|
+
'已绑定 agent_key: ' + (bound.length ? bound.join(', ') : '(无)'),
|
|
4468
|
+
'待领取 agent_key: ' + (pending.length ? pending.join(', ') : '(无)'),
|
|
4469
|
+
'legacy 明文 cache: ' + (legacy.length ? legacy.join(', ') + '(升级后不再加载)' : '(无)'),
|
|
4470
|
+
];
|
|
4471
|
+
return { text: lines.join('\n'), owners: owners, bound: bound, pending: pending, legacy: legacy };
|
|
3200
4472
|
}
|
|
3201
4473
|
|
|
3202
4474
|
function cmdKeyList() {
|
|
3203
4475
|
const root = userRoot();
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
console.log('
|
|
4476
|
+
const r = keyListCore(root);
|
|
4477
|
+
console.log(r.text);
|
|
4478
|
+
if (!r.owners) return;
|
|
4479
|
+
const reminder = migrationReminderText(root);
|
|
4480
|
+
if (reminder) console.log('\n' + reminder);
|
|
3209
4481
|
}
|
|
3210
4482
|
|
|
3211
|
-
async function
|
|
4483
|
+
async function cmdKeyBind(id, opts) {
|
|
3212
4484
|
const root = userRoot();
|
|
3213
4485
|
if (!isEncrypted(root)) { console.error('记忆库未启用加密。'); process.exit(2); }
|
|
3214
|
-
if (!id) { console.error('请指定 <id>:yotta-memory key
|
|
4486
|
+
if (!id) { console.error('请指定 <id>:yotta-memory key bind <id>'); process.exit(2); }
|
|
3215
4487
|
let password = opts.password || process.env.YOTTA_MEMORY_PASS || '';
|
|
3216
|
-
if (!password) {
|
|
4488
|
+
if (!password && !opts.recoveryKey) {
|
|
3217
4489
|
password = await promptPassword('主口令: ') || '';
|
|
3218
4490
|
if (!password) { console.error('已取消。'); process.exit(2); }
|
|
3219
4491
|
}
|
|
3220
|
-
const r =
|
|
4492
|
+
const r = keyBindCore(root, id, { password: password, recoveryKey: opts.recoveryKey });
|
|
3221
4493
|
console.log(r.text);
|
|
3222
4494
|
if (r.error) process.exit(2);
|
|
3223
4495
|
}
|
|
3224
4496
|
|
|
4497
|
+
async function cmdKeyAuthorize(id, opts) {
|
|
4498
|
+
console.error('提示: key authorize 已由 key bind 替代,本次按 bind 执行。');
|
|
4499
|
+
return cmdKeyBind(id, opts);
|
|
4500
|
+
}
|
|
4501
|
+
|
|
3225
4502
|
function cmdKeyRevoke(id) {
|
|
3226
4503
|
const root = userRoot();
|
|
3227
4504
|
if (!id) { console.error('请指定 <id>:yotta-memory key revoke <id>'); process.exit(2); }
|
|
@@ -3230,6 +4507,22 @@ function cmdKeyRevoke(id) {
|
|
|
3230
4507
|
if (r.error) process.exit(2);
|
|
3231
4508
|
}
|
|
3232
4509
|
|
|
4510
|
+
function cmdKeyClaim(id, opts) {
|
|
4511
|
+
const root = userRoot();
|
|
4512
|
+
if (!id) { console.error('请指定 <id>:yotta-memory key claim <id> [--to <AI_HOME> | --agent-key-file <文件>]'); process.exit(2); }
|
|
4513
|
+
const r = keyClaimCore(root, id, opts);
|
|
4514
|
+
console.log(r.text);
|
|
4515
|
+
if (r.error) process.exit(2);
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
function cmdKeyStatus(id, opts) {
|
|
4519
|
+
const root = userRoot();
|
|
4520
|
+
if (!id) { console.error('请指定 <id>:yotta-memory key status <id> [--to <AI_HOME> | --agent-key-file <文件>]'); process.exit(2); }
|
|
4521
|
+
const r = keyStatusCore(root, id, opts);
|
|
4522
|
+
console.log(r.text);
|
|
4523
|
+
if (r.error) process.exit(2);
|
|
4524
|
+
}
|
|
4525
|
+
|
|
3233
4526
|
// v0.8.1: 查看平台分页——服务端只返回当前页(offset/limit),避免记忆多了一次全量加载/渲染/传输
|
|
3234
4527
|
function viewEntriesCore(root, session, query, offset, limit) {
|
|
3235
4528
|
limit = Math.max(1, parseInt(limit, 10) || 50);
|
|
@@ -3250,13 +4543,50 @@ function viewEntriesCore(root, session, query, offset, limit) {
|
|
|
3250
4543
|
const page = all.slice(offset, offset + limit);
|
|
3251
4544
|
return { count: count, offset: offset, limit: limit, hasMore: offset + limit < count, entries: page };
|
|
3252
4545
|
}
|
|
4546
|
+
function viewHostName(req) {
|
|
4547
|
+
const raw = String(req.headers.host || '').trim();
|
|
4548
|
+
if (!raw) return '';
|
|
4549
|
+
try {
|
|
4550
|
+
return new URL('http://' + raw).hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
4551
|
+
} catch (e) {
|
|
4552
|
+
return '';
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
function viewHostAllowed(req, bindHost) {
|
|
4556
|
+
const name = viewHostName(req);
|
|
4557
|
+
if (!name) return false;
|
|
4558
|
+
const bind = String(bindHost || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
4559
|
+
if (bind !== '127.0.0.1' && bind !== 'localhost' && bind !== '::1') return true;
|
|
4560
|
+
return name === '127.0.0.1' || name === 'localhost' || name === '::1';
|
|
4561
|
+
}
|
|
4562
|
+
function viewOriginAllowed(req) {
|
|
4563
|
+
const origin = req.headers.origin;
|
|
4564
|
+
if (!origin) return true;
|
|
4565
|
+
let originHost = '';
|
|
4566
|
+
try {
|
|
4567
|
+
originHost = new URL(String(origin)).host.toLowerCase();
|
|
4568
|
+
} catch (e) {
|
|
4569
|
+
return false;
|
|
4570
|
+
}
|
|
4571
|
+
const requestHost = String(req.headers.host || '').trim().toLowerCase();
|
|
4572
|
+
return !!requestHost && originHost === requestHost;
|
|
4573
|
+
}
|
|
3253
4574
|
function viewServerCore(root, port, host) {
|
|
3254
4575
|
let session = { umk: null, ownerKeys: {} };
|
|
3255
4576
|
const server = http.createServer(function (req, res) {
|
|
4577
|
+
if (!viewHostAllowed(req, host) || !viewOriginAllowed(req)) {
|
|
4578
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
4579
|
+
res.end('forbidden');
|
|
4580
|
+
return;
|
|
4581
|
+
}
|
|
3256
4582
|
let pathname = '/';
|
|
3257
4583
|
try { pathname = new URL(req.url, 'http://' + (req.headers.host || 'localhost')).pathname; } catch (e) {}
|
|
3258
4584
|
function json(code, obj) {
|
|
3259
|
-
res.writeHead(code, {
|
|
4585
|
+
res.writeHead(code, {
|
|
4586
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
4587
|
+
'Cache-Control': 'no-store',
|
|
4588
|
+
'X-Content-Type-Options': 'nosniff',
|
|
4589
|
+
});
|
|
3260
4590
|
res.end(JSON.stringify(obj));
|
|
3261
4591
|
}
|
|
3262
4592
|
function readBody(cb) {
|
|
@@ -3269,8 +4599,15 @@ function viewServerCore(root, port, host) {
|
|
|
3269
4599
|
});
|
|
3270
4600
|
}
|
|
3271
4601
|
if (req.method === 'GET' && pathname === '/') {
|
|
3272
|
-
res.writeHead(200, {
|
|
3273
|
-
|
|
4602
|
+
res.writeHead(200, {
|
|
4603
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
4604
|
+
'Cache-Control': 'no-store',
|
|
4605
|
+
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'none'",
|
|
4606
|
+
'X-Content-Type-Options': 'nosniff',
|
|
4607
|
+
'X-Frame-Options': 'DENY',
|
|
4608
|
+
'Referrer-Policy': 'no-referrer',
|
|
4609
|
+
});
|
|
4610
|
+
res.end(viewHtml());
|
|
3274
4611
|
return;
|
|
3275
4612
|
}
|
|
3276
4613
|
if (req.method === 'POST' && pathname === '/api/status') {
|
|
@@ -3298,7 +4635,7 @@ function viewServerCore(root, port, host) {
|
|
|
3298
4635
|
const agents = loadAgents(root).agents || {};
|
|
3299
4636
|
const owners = keyOwners(root);
|
|
3300
4637
|
const all = Array.from(new Set(owners.concat(Object.keys(agents))));
|
|
3301
|
-
return json(200, { owners: all.map(function (o) { return { owner: o, registered: !!agents[o], authorized:
|
|
4638
|
+
return json(200, { owners: all.map(function (o) { return { owner: o, registered: !!agents[o], authorized: fs.existsSync(encAgentBindingPath(root, o)) }; }) });
|
|
3302
4639
|
}
|
|
3303
4640
|
if (req.method === 'POST' && pathname === '/api/entries') {
|
|
3304
4641
|
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
@@ -3320,22 +4657,39 @@ function viewServerCore(root, port, host) {
|
|
|
3320
4657
|
return readBody(function (d) {
|
|
3321
4658
|
const owner = String(d.owner || '');
|
|
3322
4659
|
if (!owner) return json(400, { error: '缺少 owner。' });
|
|
4660
|
+
if (!isSafeAgentId(owner)) return json(400, { error: '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' });
|
|
4661
|
+
if (fs.existsSync(encAgentBindingPath(root, owner))) {
|
|
4662
|
+
return json(409, { error: '该智能体已有 agent binding。为避免打断在用的 agent_key,请先“吊销”,再重新授权生成新 key。' });
|
|
4663
|
+
}
|
|
3323
4664
|
let ok = session.ownerKeys[owner];
|
|
3324
4665
|
let rk = null;
|
|
3325
4666
|
try { rk = unwrapRecoveryEnc(root, session.umk); } catch (e) { rk = null; }
|
|
3326
4667
|
if (!ok) {
|
|
3327
4668
|
if (!rk) return json(400, { error: '密钥库缺少恢复钥匙(recovery.key.enc),无法为新 owner 建钥。' });
|
|
3328
|
-
ok =
|
|
4669
|
+
ok = recoverOwnerKeyFromRecovery(root, owner, session.umk, rk);
|
|
4670
|
+
if (!ok) {
|
|
4671
|
+
if (privateOwnerHasData(root, owner)) {
|
|
4672
|
+
return json(400, { error: '检测到 private/' + owner + ' 仍有密文,但 keys/' + owner + '.key.enc 与恢复文件均不可用;拒绝新建 owner key(会导致旧数据无法解密)。请从备份恢复原 owner key,或使用 CLI 恢复钥匙修复后重试。' });
|
|
4673
|
+
}
|
|
4674
|
+
ok = wrapOwnerKey(root, session.umk, rk, owner);
|
|
4675
|
+
}
|
|
3329
4676
|
session.ownerKeys[owner] = ok;
|
|
3330
4677
|
}
|
|
3331
|
-
|
|
3332
|
-
|
|
4678
|
+
const agentKey = crypto.randomBytes(32);
|
|
4679
|
+
const pendingFile = writeAgentBindingWithPending(root, owner, ok, agentKey);
|
|
4680
|
+
revokeOwnerKeyCache(root, owner);
|
|
4681
|
+
json(200, { ok: true, agentKey: agentKey.toString('base64'), pendingFile: pendingFile, text: 'agent_key 只显示一次,请用户自行保存;AI 可由 pending 文件领取并写入宿主目录。' });
|
|
3333
4682
|
});
|
|
3334
4683
|
}
|
|
3335
4684
|
if (req.method === 'POST' && pathname === '/api/revoke') {
|
|
3336
4685
|
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
3337
4686
|
return readBody(function (d) {
|
|
3338
|
-
|
|
4687
|
+
const owner = String(d.owner || '');
|
|
4688
|
+
if (!isSafeAgentId(owner)) return json(400, { error: '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' });
|
|
4689
|
+
const binding = encAgentBindingPath(root, owner);
|
|
4690
|
+
if (fs.existsSync(binding)) fs.unlinkSync(binding);
|
|
4691
|
+
removePendingAgentKey(root, owner);
|
|
4692
|
+
revokeOwnerKeyCache(root, owner);
|
|
3339
4693
|
json(200, { ok: true });
|
|
3340
4694
|
});
|
|
3341
4695
|
}
|
|
@@ -3380,7 +4734,9 @@ function cmdView(opts) {
|
|
|
3380
4734
|
viewServerCore(root, port, host);
|
|
3381
4735
|
}
|
|
3382
4736
|
|
|
3383
|
-
|
|
4737
|
+
function viewHtml() {
|
|
4738
|
+
return VIEW_HTML;
|
|
4739
|
+
}
|
|
3384
4740
|
|
|
3385
4741
|
// ---- 命令包装(CLI 入口)----
|
|
3386
4742
|
async function cmdInit(opts) {
|
|
@@ -3448,8 +4804,13 @@ function cmdDistill(opts) {
|
|
|
3448
4804
|
if (r.error) process.exit(2);
|
|
3449
4805
|
}
|
|
3450
4806
|
function cmdForget(fileRef, opts) {
|
|
4807
|
+
const ident = resolveIdentity(opts);
|
|
4808
|
+
if (ident.error) {
|
|
4809
|
+
console.log(ident.error);
|
|
4810
|
+
process.exit(3);
|
|
4811
|
+
}
|
|
3451
4812
|
const r = forgetCore(fileRef, {
|
|
3452
|
-
selfAgent:
|
|
4813
|
+
selfAgent: ident.id,
|
|
3453
4814
|
unsafe: !!(opts && opts.unsafe),
|
|
3454
4815
|
});
|
|
3455
4816
|
console.log(r.text);
|
|
@@ -3488,7 +4849,15 @@ function cmdBackupEnsureDaily(opts) {
|
|
|
3488
4849
|
}
|
|
3489
4850
|
function cmdBackupSchedule(action, opts) {
|
|
3490
4851
|
let r;
|
|
3491
|
-
if (action === 'enable')
|
|
4852
|
+
if (action === 'enable') {
|
|
4853
|
+
const runtime = runtimeEnsureForManagedTask();
|
|
4854
|
+
if (runtime.error) {
|
|
4855
|
+
console.error('runtime 稳定入口未就绪,拒绝注册备份任务: ' + runtime.text);
|
|
4856
|
+
process.exit(1);
|
|
4857
|
+
}
|
|
4858
|
+
opts = Object.assign({}, opts, { scriptPath: runtimeManagedScript() });
|
|
4859
|
+
r = backupScheduleEnableCore(opts);
|
|
4860
|
+
}
|
|
3492
4861
|
else if (action === 'disable') r = backupScheduleDisableCore(opts);
|
|
3493
4862
|
else if (action === 'status') r = backupScheduleStatusCore(opts);
|
|
3494
4863
|
else {
|
|
@@ -3564,6 +4933,16 @@ function importCore(root, src) {
|
|
|
3564
4933
|
try { data = JSON.parse(fs.readFileSync(fp, 'utf8')); }
|
|
3565
4934
|
catch (e) { return { error: true, text: 'JSON 解析失败: ' + (e && e.message ? e.message : String(e)) }; }
|
|
3566
4935
|
if (!data.memories || !Array.isArray(data.memories)) return { error: true, text: 'JSON 格式不正确(缺少 memories 数组)' };
|
|
4936
|
+
for (const item of data.memories) {
|
|
4937
|
+
const meta = item.meta || {};
|
|
4938
|
+
const t = (meta.type || 'FACT').toUpperCase();
|
|
4939
|
+
if (!TYPE_DIRS[t]) continue;
|
|
4940
|
+
const owner = String(meta.owner || '');
|
|
4941
|
+
const scope = meta.scope || defaultScope(t);
|
|
4942
|
+
if (scope === 'private' && !isSafeAgentId(owner)) {
|
|
4943
|
+
return { error: true, text: '拒绝: 导入的私密条目 owner 非法(只允许单段名称,禁止 /、\\、.. 和控制字符)。' };
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
3567
4946
|
ensureInit(root);
|
|
3568
4947
|
const encrypted = isEncrypted(root);
|
|
3569
4948
|
let m = 0, skippedPriv = 0;
|
|
@@ -3627,6 +5006,7 @@ function cmdTokenNew(agentId, opts) {
|
|
|
3627
5006
|
const root = userRoot();
|
|
3628
5007
|
ensureInit(root);
|
|
3629
5008
|
if (!agentId) { console.error('请指定 --agent <id>'); process.exit(2); }
|
|
5009
|
+
if (!isSafeAgentId(agentId)) { console.error('非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。'); process.exit(2); }
|
|
3630
5010
|
const data = loadTokens(root);
|
|
3631
5011
|
data.version = 1;
|
|
3632
5012
|
data.tokens = data.tokens || {};
|
|
@@ -3652,6 +5032,7 @@ function cmdTokenList() {
|
|
|
3652
5032
|
function cmdTokenRevoke(agentId) {
|
|
3653
5033
|
const root = userRoot();
|
|
3654
5034
|
if (!agentId) { console.error('请指定 --agent <id>'); process.exit(2); }
|
|
5035
|
+
if (!isSafeAgentId(agentId)) { console.error('非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。'); process.exit(2); }
|
|
3655
5036
|
const data = loadTokens(root);
|
|
3656
5037
|
data.tokens = data.tokens || {};
|
|
3657
5038
|
if (data.tokens[agentId]) { delete data.tokens[agentId]; saveTokens(root, data); console.log('已吊销: ' + agentId); }
|
|
@@ -3758,6 +5139,7 @@ function cmdIam(agentId, opts) {
|
|
|
3758
5139
|
const root = userRoot();
|
|
3759
5140
|
ensureInit(root);
|
|
3760
5141
|
if (!agentId) { console.error('请指定 <id>:yotta-memory iam <id> [--name <显示名>] [--user <用户名>] [--relationship <关系>] [--force]'); process.exit(2); }
|
|
5142
|
+
if (!isSafeAgentId(agentId)) { console.error('非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。'); process.exit(2); }
|
|
3761
5143
|
const conflict = identityConflict(root, agentId);
|
|
3762
5144
|
if (conflict && !opts.force) {
|
|
3763
5145
|
console.error("错误: ID '" + agentId + "' " + conflict + '。每个 AI 智能体的 ID 必须唯一。请换一个唯一 ID,或确认是同一智能体后加 --force 覆盖。');
|
|
@@ -3778,13 +5160,18 @@ function cmdIam(agentId, opts) {
|
|
|
3778
5160
|
console.log('已写入自我档案: ' + file);
|
|
3779
5161
|
console.log('本机免 token:以后用 whoami 确认身份;远端接入需 token new --agent ' + agentId);
|
|
3780
5162
|
}
|
|
3781
|
-
function cmdWhoami() {
|
|
5163
|
+
function cmdWhoami(opts) {
|
|
3782
5164
|
const root = userRoot();
|
|
3783
|
-
const
|
|
5165
|
+
const ident = resolveIdentity(opts);
|
|
5166
|
+
if (ident.error) {
|
|
5167
|
+
console.log(ident.error);
|
|
5168
|
+
process.exit(2);
|
|
5169
|
+
}
|
|
5170
|
+
const id = ident.id;
|
|
3784
5171
|
if (!id) {
|
|
3785
|
-
console.log('
|
|
3786
|
-
console.log('
|
|
3787
|
-
console.log('远端:X-Agent-Id
|
|
5172
|
+
console.log('当前未声明显式智能体身份(身份不再从环境变量读取)。');
|
|
5173
|
+
console.log('CLI:请传 --agent <唯一ID>。stdio MCP:使用 --agent-id <唯一ID> + --agent-key-file <path>。HTTP MCP:发送 X-Agent-Id + X-Agent-Key。');
|
|
5174
|
+
console.log('远端:token + X-Agent-Id + X-Agent-Key(token new --agent <id>;agent_key 由 view 授权生成)。');
|
|
3788
5175
|
return;
|
|
3789
5176
|
}
|
|
3790
5177
|
const agents = loadAgents(root).agents || {};
|
|
@@ -3834,9 +5221,20 @@ function profileGroups(root, owner) {
|
|
|
3834
5221
|
function profileCore(opts) {
|
|
3835
5222
|
opts = opts || {};
|
|
3836
5223
|
const root = userRoot();
|
|
3837
|
-
const
|
|
5224
|
+
const ident = resolveIdentity(opts);
|
|
5225
|
+
if (ident.error) return { error: true, exitCode: 3, text: ident.error };
|
|
5226
|
+
if (!ident.id) {
|
|
5227
|
+
return {
|
|
5228
|
+
error: true,
|
|
5229
|
+
exitCode: 3,
|
|
5230
|
+
text: '生成私密画像必须先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
|
|
5231
|
+
};
|
|
5232
|
+
}
|
|
5233
|
+
const selfAgent = ident.id;
|
|
3838
5234
|
const owner = opts.owner || selfAgent;
|
|
3839
|
-
|
|
5235
|
+
const keyError = validatePrivateIdentity(root, ident, owner);
|
|
5236
|
+
if (keyError) return { error: true, exitCode: 3, text: keyError };
|
|
5237
|
+
if (!owner) return { error: true, exitCode: 2, text: '请先声明身份(--agent / --agent-id)或传 --owner <id>,再生成画像。' };
|
|
3840
5238
|
if (selfAgent && owner !== selfAgent && owner !== 'user' && !opts.unsafe && !hasGrant(selfAgent, owner)) {
|
|
3841
5239
|
return { error: true, exitCode: 3, text: '拒绝: 不能生成其它智能体 ' + owner + ' 的画像(private/' + owner + '/ 为私密区)。如需读取请 --owner user 或 --unsafe(用户显式授权)。' };
|
|
3842
5240
|
}
|
|
@@ -3882,14 +5280,41 @@ function selfProfileKv(root, id) {
|
|
|
3882
5280
|
try { text = readMemoryText(root, path.join(selfPrefsDir(root, id), profile), id); } catch (e) { return {}; }
|
|
3883
5281
|
return parseKvBody(parseFrontmatter(text).body);
|
|
3884
5282
|
}
|
|
5283
|
+
// v0.14.0:长期摘要优先 / 近期走廊 / 会话闭环上下文编排。
|
|
5284
|
+
// 摘要只认 consolidate 产物,不新增第二套摘要格式。
|
|
5285
|
+
function isConsolidatedSummary(entry) {
|
|
5286
|
+
if (!entry) return false;
|
|
5287
|
+
if (entry.source === 'consolidate') return true;
|
|
5288
|
+
const tags = Array.isArray(entry.tags) ? entry.tags : [];
|
|
5289
|
+
return tags.indexOf('consolidate') !== -1 && tags.indexOf('summary') !== -1;
|
|
5290
|
+
}
|
|
5291
|
+
function contextRecentKey(entry) {
|
|
5292
|
+
return String((entry && (entry.updated || entry.created)) || '');
|
|
5293
|
+
}
|
|
5294
|
+
function compareContextRecent(a, b) {
|
|
5295
|
+
const byTime = contextRecentKey(b).localeCompare(contextRecentKey(a));
|
|
5296
|
+
if (byTime !== 0) return byTime;
|
|
5297
|
+
return (b.access_count || 0) - (a.access_count || 0);
|
|
5298
|
+
}
|
|
3885
5299
|
function contextCore(opts) {
|
|
3886
5300
|
opts = opts || {};
|
|
3887
5301
|
const roots = memoryRoots();
|
|
3888
5302
|
if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
|
|
3889
5303
|
const root = userRoot();
|
|
3890
|
-
const limit = opts.limit || 10;
|
|
3891
|
-
const
|
|
5304
|
+
const limit = parseInt(opts.limit || '10', 10) || 10;
|
|
5305
|
+
const ident = resolveIdentity(opts);
|
|
5306
|
+
if (ident.error) return { error: true, exitCode: 3, text: ident.error };
|
|
5307
|
+
if (!ident.id) {
|
|
5308
|
+
return {
|
|
5309
|
+
error: true,
|
|
5310
|
+
exitCode: 3,
|
|
5311
|
+
text: '开工上下文包含私密画像 / 边界 / 承诺,必须先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
|
|
5312
|
+
};
|
|
5313
|
+
}
|
|
5314
|
+
const selfAgent = ident.id;
|
|
3892
5315
|
const owner = opts.owner || selfAgent;
|
|
5316
|
+
const keyError = validatePrivateIdentity(root, ident, owner);
|
|
5317
|
+
if (keyError) return { error: true, exitCode: 3, text: keyError };
|
|
3893
5318
|
const unsafe = !!opts.unsafe;
|
|
3894
5319
|
const budget = opts.budget ? parseInt(opts.budget, 10) : 0;
|
|
3895
5320
|
const focus = opts.focus ? String(opts.focus) : '';
|
|
@@ -3898,14 +5323,32 @@ function contextCore(opts) {
|
|
|
3898
5323
|
const embeddingTimeout = effectiveEmbeddingTimeout(opts);
|
|
3899
5324
|
const trace = [];
|
|
3900
5325
|
const lines = [];
|
|
5326
|
+
const shownFiles = new Set();
|
|
3901
5327
|
const FENCE = String.fromCharCode(96, 96, 96);
|
|
3902
5328
|
function usedChars() { return lines.reduce(function (s, x) { return s + String(x).length + 1; }, 0); }
|
|
5329
|
+
function appendContextEntry(entry, reason, options) {
|
|
5330
|
+
options = options || {};
|
|
5331
|
+
if (!entry) return false;
|
|
5332
|
+
if (shownFiles.has(entry.file)) {
|
|
5333
|
+
trace.push('[dropped] ' + entry.file + ' reason: duplicate');
|
|
5334
|
+
return false;
|
|
5335
|
+
}
|
|
5336
|
+
const line = '- [' + entry.type + '] ' + entry.subject + ': ' + entry.statement;
|
|
5337
|
+
if (options.budget !== false && budget > 0 && usedChars() + line.length > budget) {
|
|
5338
|
+
trace.push('[dropped] ' + entry.file + ' reason: budget_exceeded');
|
|
5339
|
+
return false;
|
|
5340
|
+
}
|
|
5341
|
+
lines.push(line);
|
|
5342
|
+
shownFiles.add(entry.file);
|
|
5343
|
+
trace.push('[included] ' + entry.file + ' reason: ' + reason);
|
|
5344
|
+
return true;
|
|
5345
|
+
}
|
|
3903
5346
|
lines.push('# 开工上下文包(yotta-memory context)');
|
|
3904
5347
|
lines.push('');
|
|
3905
5348
|
lines.push('## 1. 身份');
|
|
3906
5349
|
lines.push('');
|
|
3907
5350
|
if (!owner) {
|
|
3908
|
-
lines.push('- 未声明智能体身份(无
|
|
5351
|
+
lines.push('- 未声明智能体身份(无 --agent / --agent-id / X-Agent-Id)。私密记忆与画像不可用;请先 whoami / iam。');
|
|
3909
5352
|
} else {
|
|
3910
5353
|
lines.push('- agent_id: ' + owner);
|
|
3911
5354
|
const kv = selfProfileKv(root, owner);
|
|
@@ -3950,8 +5393,26 @@ function contextCore(opts) {
|
|
|
3950
5393
|
}
|
|
3951
5394
|
lines.push('');
|
|
3952
5395
|
|
|
5396
|
+
const readableEntries = [];
|
|
5397
|
+
for (const r of roots) {
|
|
5398
|
+
for (const e of ensureIndex(r)) {
|
|
5399
|
+
if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
|
|
5400
|
+
readableEntries.push(e);
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
|
|
5404
|
+
lines.push('## 2.5 长期理解摘要');
|
|
5405
|
+
lines.push('');
|
|
5406
|
+
const summaries = readableEntries.filter(isConsolidatedSummary).sort(compareContextRecent);
|
|
5407
|
+
const summaryLimit = Math.min(3, Math.max(0, limit));
|
|
5408
|
+
if (!summaries.length) lines.push('(暂无周期摘要;可用 yotta-memory consolidate --apply 生成)');
|
|
5409
|
+
for (const e of summaries.slice(0, summaryLimit)) {
|
|
5410
|
+
appendContextEntry(e, 'summary_priority', { budget: false });
|
|
5411
|
+
}
|
|
5412
|
+
lines.push('');
|
|
5413
|
+
|
|
3953
5414
|
if (focus) {
|
|
3954
|
-
lines.push('## 2.
|
|
5415
|
+
lines.push('## 2.6 任务相关记忆(--focus)');
|
|
3955
5416
|
lines.push('');
|
|
3956
5417
|
const focused = recallCore(focus, {
|
|
3957
5418
|
limit: limit,
|
|
@@ -3964,70 +5425,51 @@ function contextCore(opts) {
|
|
|
3964
5425
|
const focusedEntries = focused.entries || [];
|
|
3965
5426
|
if (!focusedEntries.length) lines.push('(无匹配记忆)');
|
|
3966
5427
|
for (const e of focusedEntries) {
|
|
3967
|
-
|
|
3968
|
-
if (budget > 0 && usedChars() + line.length > budget) {
|
|
3969
|
-
trace.push('[dropped] ' + e.file + ' reason: budget_exceeded');
|
|
3970
|
-
continue;
|
|
3971
|
-
}
|
|
3972
|
-
lines.push(line);
|
|
3973
|
-
trace.push('[included] ' + e.file + ' reason: focus_match score: ' + round3(e.score));
|
|
5428
|
+
appendContextEntry(e, 'focus_match score: ' + round3(e.score));
|
|
3974
5429
|
}
|
|
3975
5430
|
lines.push('');
|
|
3976
5431
|
}
|
|
3977
5432
|
|
|
3978
|
-
lines.push('## 3.
|
|
5433
|
+
lines.push('## 3. 近期走廊(按时间)');
|
|
3979
5434
|
lines.push('');
|
|
3980
|
-
const
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
if (!recentShown.length) lines.push('(暂无记忆)');
|
|
3991
|
-
for (const h of recentShown) {
|
|
3992
|
-
const line = '- [' + h.e.type + '] ' + h.e.subject + ': ' + h.e.statement;
|
|
3993
|
-
if (budget > 0 && usedChars() + line.length > budget) {
|
|
3994
|
-
trace.push('[dropped] ' + h.e.file + ' reason: budget_exceeded');
|
|
3995
|
-
break;
|
|
3996
|
-
}
|
|
3997
|
-
lines.push(line);
|
|
3998
|
-
trace.push('[included] ' + h.e.file + ' reason: recent_match');
|
|
3999
|
-
}
|
|
5435
|
+
const corridor = readableEntries
|
|
5436
|
+
.filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT'; })
|
|
5437
|
+
.sort(compareContextRecent)
|
|
5438
|
+
.filter(function (e) { return !shownFiles.has(e.file); })
|
|
5439
|
+
.slice(0, limit);
|
|
5440
|
+
if (!corridor.length) lines.push('(暂无近期记忆)');
|
|
5441
|
+
for (const e of corridor) appendContextEntry(e, 'recent_corridor');
|
|
5442
|
+
lines.push('');
|
|
5443
|
+
|
|
5444
|
+
lines.push('## 4. 近期高价值记忆(补位)');
|
|
4000
5445
|
lines.push('');
|
|
4001
|
-
|
|
5446
|
+
const highValue = readableEntries
|
|
5447
|
+
.filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT' && !shownFiles.has(e.file); })
|
|
5448
|
+
.map(function (e) { return { e: e, s: 0.5 * importanceScore(e) + 0.5 * utilityScore(e) }; })
|
|
5449
|
+
.sort(function (a, b) { return b.s - a.s; })
|
|
5450
|
+
.slice(0, limit);
|
|
5451
|
+
if (!highValue.length) lines.push('(暂无补位条目)');
|
|
5452
|
+
for (const h of highValue) appendContextEntry(h.e, 'high_value_backfill');
|
|
4002
5453
|
lines.push('');
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
if (e.type !== 'BOUND') continue;
|
|
4007
|
-
if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
|
|
4008
|
-
bounds.push(e);
|
|
4009
|
-
}
|
|
4010
|
-
}
|
|
5454
|
+
lines.push('## 5. 边界提醒(BOUND)');
|
|
5455
|
+
lines.push('');
|
|
5456
|
+
const bounds = readableEntries.filter(function (e) { return e.type === 'BOUND'; }).sort(compareContextRecent);
|
|
4011
5457
|
if (!bounds.length) lines.push('(无)');
|
|
4012
5458
|
for (const e of bounds) lines.push('- ' + (e.subject || '边界') + ': ' + e.statement);
|
|
4013
5459
|
lines.push('');
|
|
4014
|
-
lines.push('##
|
|
5460
|
+
lines.push('## 6. 承诺 / 锚点(COMMIT)');
|
|
4015
5461
|
lines.push('');
|
|
4016
|
-
const commits =
|
|
4017
|
-
for (const r of roots) {
|
|
4018
|
-
for (const e of ensureIndex(r)) {
|
|
4019
|
-
if (e.type !== 'COMMIT') continue;
|
|
4020
|
-
if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
|
|
4021
|
-
commits.push(e);
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
5462
|
+
const commits = readableEntries.filter(function (e) { return e.type === 'COMMIT'; }).sort(compareContextRecent);
|
|
4024
5463
|
if (!commits.length) lines.push('(无)');
|
|
4025
5464
|
for (const e of commits) lines.push('- ' + (e.subject || '承诺') + ': ' + e.statement);
|
|
4026
5465
|
lines.push('');
|
|
4027
|
-
lines.push('##
|
|
5466
|
+
lines.push('## 7. 本会话闭环契约');
|
|
5467
|
+
lines.push('');
|
|
5468
|
+
lines.push('- 开工:本上下文包已加载;身份、长期摘要、边界与承诺以本包为准。');
|
|
5469
|
+
lines.push('- 进行中:出现事实 / 偏好 / 边界 / 纠正 / 承诺信号时立即 `remember <type> <subject> <statement> --verify`,不攒到收工。');
|
|
5470
|
+
lines.push('- 收工前复盘:检查本轮是否留下 COMMIT / 会话小结;有关键结论但未落盘时补写并 `recall` 回读,无长期价值不硬凑。');
|
|
4028
5471
|
lines.push('');
|
|
4029
|
-
const allEntries =
|
|
4030
|
-
for (const r of roots) for (const e of ensureIndex(r)) allEntries.push(e);
|
|
5472
|
+
const allEntries = readableEntries;
|
|
4031
5473
|
const myCommits = allEntries.filter(function (e) { return e.type === 'COMMIT' && classifyRead(e, owner, '', unsafe, selfAgent) !== 'denied'; });
|
|
4032
5474
|
if (!myCommits.length) {
|
|
4033
5475
|
lines.push('- 最近承诺: 无(收工请补 COMMIT / 交接锚点)');
|
|
@@ -4040,7 +5482,7 @@ function contextCore(opts) {
|
|
|
4040
5482
|
if (oldCount > 0) lines.push('- 归档提醒: ' + oldCount + ' 条超 180 天,建议 archive');
|
|
4041
5483
|
if (explain) {
|
|
4042
5484
|
lines.push('');
|
|
4043
|
-
lines.push('##
|
|
5485
|
+
lines.push('## 8. 选择解释(--explain)');
|
|
4044
5486
|
lines.push('');
|
|
4045
5487
|
if (!trace.length) lines.push('(无选择记录)');
|
|
4046
5488
|
for (const t of trace) lines.push(t);
|
|
@@ -4059,6 +5501,10 @@ function contextCore(opts) {
|
|
|
4059
5501
|
function cmdContext(opts) {
|
|
4060
5502
|
const r = contextCore(opts);
|
|
4061
5503
|
console.log(r.text);
|
|
5504
|
+
if (r.error) {
|
|
5505
|
+
const reminder = migrationReminderText(userRoot());
|
|
5506
|
+
if (reminder) console.log('\n' + reminder);
|
|
5507
|
+
}
|
|
4062
5508
|
if (r.exitCode) process.exit(r.exitCode);
|
|
4063
5509
|
}
|
|
4064
5510
|
|
|
@@ -4103,8 +5549,12 @@ function cmdConfigGet() {
|
|
|
4103
5549
|
}
|
|
4104
5550
|
|
|
4105
5551
|
// ---- MCP serve(局域网 streamable HTTP 记忆引擎,零依赖)----
|
|
4106
|
-
|
|
4107
|
-
|
|
5552
|
+
const MCP_CORE_TOOL_NAMES = ['context', 'recall', 'search', 'remember'];
|
|
5553
|
+
function normalizeMcpToolProfile(profile) {
|
|
5554
|
+
return String(profile || 'full').trim().toLowerCase() === 'core' ? 'core' : 'full';
|
|
5555
|
+
}
|
|
5556
|
+
function mcpTools(profile) {
|
|
5557
|
+
const tools = [
|
|
4108
5558
|
{ name: 'remember', description: '写入一条记忆。参数 type(FACT/PREF/BOUND/COMMIT)、subject、statement 必填;owner 可选,默认当前智能体', inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'FACT/PREF/BOUND/COMMIT' }, subject: { type: 'string' }, statement: { type: 'string' }, owner: { type: 'string' } }, required: ['type', 'subject', 'statement'] } },
|
|
4109
5559
|
{ name: 'recall', description: '检索记忆。query 可选;type 可选;limit 可选(默认 20);explain 可选。只返回当前智能体可读记忆;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' }, embeddingTimeout: { type: 'number' }, explain: { type: 'boolean' } } } },
|
|
4110
5560
|
{ name: 'search', description: '检索记忆(同 recall)。query 可选;type 可选;limit 可选(默认 20);explain 可选;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' }, embeddingTimeout: { type: 'number' }, explain: { type: 'boolean' } } } },
|
|
@@ -4115,16 +5565,33 @@ function mcpTools() {
|
|
|
4115
5565
|
{ name: 'reindex', description: '重建索引(手动改 .md 后校正;扫描 facts/prefs/bounds/commits 四目录)', inputSchema: { type: 'object', properties: {} } },
|
|
4116
5566
|
{ name: 'export', description: '导出全部记忆到记忆库内的 JSON 文件。out 可选(默认 <记忆库>/yottamemory-export-<日期>.json;仅限记忆库内路径)', inputSchema: { type: 'object', properties: { out: { type: 'string' } } } },
|
|
4117
5567
|
{ name: 'import', description: '从记忆库内的 JSON 文件导入记忆。src 为文件路径(相对记忆库目录,或记忆库内绝对路径;仅限记忆库内)', inputSchema: { type: 'object', properties: { src: { type: 'string' } }, required: ['src'] } },
|
|
4118
|
-
{ name: 'agent_info', description: '
|
|
5568
|
+
{ name: 'agent_info', description: '查看当前智能体身份与登记状态(HTTP 读经 token 校验的 X-Agent-Id;stdio 读 --agent-id 显式参数)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
|
|
4119
5569
|
{ name: 'profile', description: '生成当前智能体的用户画像(只读聚合 private/<owner>/ 下 PREF/BOUND/COMMIT 原文,零推断,写入 private/<owner>/profile.md)。owner 默认当前智能体', inputSchema: { type: 'object', properties: { owner: { type: 'string' } } } },
|
|
4120
5570
|
{ 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'] } },
|
|
4121
5571
|
{ 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' } } } },
|
|
4122
5572
|
{ name: 'distill', description: '心理日志蒸馏(自我提升):统计摘要/主题画像/知识地图。owner 默认当前智能体(MCP 不支持 --model 外部命令)', inputSchema: { type: 'object', properties: { owner: { type: 'string' }, subject: { type: 'string' } } } },
|
|
4123
5573
|
{ name: 'explain', description: '解释单条记忆效用分项(为什么靠前/归档/遗忘)。file 为记忆文件路径或文件名', inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] } },
|
|
4124
5574
|
];
|
|
5575
|
+
if (normalizeMcpToolProfile(profile) !== 'core') return tools;
|
|
5576
|
+
return MCP_CORE_TOOL_NAMES
|
|
5577
|
+
.map((name) => tools.find((tool) => tool.name === name))
|
|
5578
|
+
.filter(Boolean);
|
|
4125
5579
|
}
|
|
4126
5580
|
function callTool(name, args, ctx) {
|
|
4127
|
-
|
|
5581
|
+
return runWithIdentity({
|
|
5582
|
+
id: ctx && ctx.agent,
|
|
5583
|
+
agentKey: ctx && ctx.agentKey,
|
|
5584
|
+
}, function () {
|
|
5585
|
+
return callToolInner(name, args, ctx);
|
|
5586
|
+
});
|
|
5587
|
+
}
|
|
5588
|
+
function callToolInner(name, args, ctx) {
|
|
5589
|
+
const agent = (ctx && ctx.agent) || '';
|
|
5590
|
+
const toolProfile = normalizeMcpToolProfile(ctx && ctx.toolProfile);
|
|
5591
|
+
if (toolProfile === 'core' && MCP_CORE_TOOL_NAMES.indexOf(name) === -1) {
|
|
5592
|
+
return { text: '工具 ' + name + ' 属于 full 分组,当前 MCP 以 core 模式启动。需要完整工具集时请使用 --tools full 重新启动。', error: true };
|
|
5593
|
+
}
|
|
5594
|
+
if (agent && !isSafeAgentId(agent)) return { text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。', error: true };
|
|
4128
5595
|
try {
|
|
4129
5596
|
if (name === 'remember') {
|
|
4130
5597
|
const ownerArg = args.owner ? String(args.owner) : '';
|
|
@@ -4193,7 +5660,7 @@ function callTool(name, args, ctx) {
|
|
|
4193
5660
|
if (name === 'agent_info') {
|
|
4194
5661
|
const root = userRoot();
|
|
4195
5662
|
const id = agent || '';
|
|
4196
|
-
if (!id) return { text: '当前未声明智能体身份(无 X-Agent-Id /
|
|
5663
|
+
if (!id) return { text: '当前未声明智能体身份(无 X-Agent-Id / --agent-id)。stdio 请使用 --agent-id <唯一ID> + --agent-key-file <path>;HTTP 请发送 X-Agent-Id + X-Agent-Key。', error: false };
|
|
4197
5664
|
const agents = loadAgents(root).agents || {};
|
|
4198
5665
|
const tokens = loadTokens(root).tokens || {};
|
|
4199
5666
|
let reg = '未登记';
|
|
@@ -4240,11 +5707,26 @@ function reqVersion(params) {
|
|
|
4240
5707
|
const meta = (params && params._meta) || {};
|
|
4241
5708
|
return meta['io.modelcontextprotocol/protocolVersion'] || null;
|
|
4242
5709
|
}
|
|
4243
|
-
function
|
|
5710
|
+
function mcpIdentityMode(ctx) {
|
|
5711
|
+
const mode = String((ctx && ctx.identityMode) || '').trim().toLowerCase();
|
|
5712
|
+
if (mode === 'headers' || mode === 'stdio-args') return mode;
|
|
5713
|
+
if (ctx && ctx.agent) return 'stdio-args';
|
|
5714
|
+
return 'unknown';
|
|
5715
|
+
}
|
|
5716
|
+
function mcpServerInfo(ctx) {
|
|
5717
|
+
return {
|
|
5718
|
+
name: 'yotta-memory',
|
|
5719
|
+
version: VERSION,
|
|
5720
|
+
runtimePath: runtimeExecutionPath(),
|
|
5721
|
+
identityMode: mcpIdentityMode(ctx),
|
|
5722
|
+
toolProfile: normalizeMcpToolProfile(ctx && ctx.toolProfile),
|
|
5723
|
+
};
|
|
5724
|
+
}
|
|
5725
|
+
function modernOk(payload, ttl, ctx) {
|
|
4244
5726
|
const out = { resultType: 'complete' };
|
|
4245
5727
|
Object.assign(out, payload);
|
|
4246
5728
|
if (ttl) { out.ttlMs = ttl[0]; out.cacheScope = ttl[1]; }
|
|
4247
|
-
out._meta = { 'io.modelcontextprotocol/serverInfo':
|
|
5729
|
+
out._meta = { 'io.modelcontextprotocol/serverInfo': mcpServerInfo(ctx) };
|
|
4248
5730
|
return out;
|
|
4249
5731
|
}
|
|
4250
5732
|
function unsupportedVersion(id, pv) {
|
|
@@ -4260,17 +5742,18 @@ function handleMessage(msg, ctx) {
|
|
|
4260
5742
|
if (pv !== null) {
|
|
4261
5743
|
// ---- modern(2026-07-28 无状态)----
|
|
4262
5744
|
if (pv !== MCP_PROTOCOL_MODERN) return unsupportedVersion(id, pv);
|
|
5745
|
+
const toolProfile = normalizeMcpToolProfile(ctx && ctx.toolProfile);
|
|
4263
5746
|
if (method === 'server/discover') {
|
|
4264
5747
|
return { jsonrpc: '2.0', id: id, result: modernOk({
|
|
4265
5748
|
supportedVersions: [MCP_PROTOCOL_MODERN],
|
|
4266
5749
|
capabilities: { tools: {} },
|
|
4267
|
-
instructions: '元忆 MCP(基于 MCP 最新协议 2026-07-28,向后兼容 2025-11-25
|
|
4268
|
-
}, [3600000, 'public']) };
|
|
5750
|
+
instructions: '元忆 MCP(基于 MCP 最新协议 2026-07-28,向后兼容 2025-11-25 及更早握手):当前工具分组 ' + toolProfile + ';文件式智能体记忆 remember/recall/search/context/forget/archive/maintain 等读写检索与维护;私密按 owner 物理隔离,数据不出本机。'
|
|
5751
|
+
}, [3600000, 'public'], ctx) };
|
|
4269
5752
|
}
|
|
4270
|
-
if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: modernOk({ tools: mcpTools() }, [300000, 'public']) };
|
|
5753
|
+
if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: modernOk({ tools: mcpTools(toolProfile) }, [300000, 'public'], ctx) };
|
|
4271
5754
|
if (method === 'tools/call') {
|
|
4272
5755
|
const out = callTool(params.name || '', params.arguments || {}, ctx);
|
|
4273
|
-
return { jsonrpc: '2.0', id: id, result: modernOk({ content: [{ type: 'text', text: out.text }], isError: !!out.error }) };
|
|
5756
|
+
return { jsonrpc: '2.0', id: id, result: modernOk({ content: [{ type: 'text', text: out.text }], isError: !!out.error }, null, ctx) };
|
|
4274
5757
|
}
|
|
4275
5758
|
if (method === 'initialize') {
|
|
4276
5759
|
return { jsonrpc: '2.0', id: id, error: { code: -32601, message: "initialize removed in MCP 2026-07-28; use server/discover. supported: ['2026-07-28']" } };
|
|
@@ -4279,10 +5762,10 @@ function handleMessage(msg, ctx) {
|
|
|
4279
5762
|
}
|
|
4280
5763
|
// ---- legacy(<=2025-11-25,initialize 握手;响应保持旧形状)----
|
|
4281
5764
|
if (method === 'initialize') {
|
|
4282
|
-
return { jsonrpc: '2.0', id: id, result: { protocolVersion: MCP_PROTOCOL_LEGACY, capabilities: { tools: {} }, serverInfo:
|
|
5765
|
+
return { jsonrpc: '2.0', id: id, result: { protocolVersion: MCP_PROTOCOL_LEGACY, capabilities: { tools: {} }, serverInfo: mcpServerInfo(ctx) } };
|
|
4283
5766
|
}
|
|
4284
5767
|
if (method === 'ping') return { jsonrpc: '2.0', id: id, result: {} };
|
|
4285
|
-
if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: { tools: mcpTools() } };
|
|
5768
|
+
if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: { tools: mcpTools(ctx && ctx.toolProfile) } };
|
|
4286
5769
|
if (method === 'tools/call') {
|
|
4287
5770
|
const out = callTool(params.name || '', params.arguments || {}, ctx);
|
|
4288
5771
|
return { jsonrpc: '2.0', id: id, result: { content: [{ type: 'text', text: out.text }], isError: !!out.error } };
|
|
@@ -4290,21 +5773,34 @@ function handleMessage(msg, ctx) {
|
|
|
4290
5773
|
return { jsonrpc: '2.0', id: id, error: { code: -32601, message: 'Method not found: ' + method } };
|
|
4291
5774
|
}
|
|
4292
5775
|
function cmdServe(opts) {
|
|
4293
|
-
if (opts.stdio) { cmdServeStdio(); return; }
|
|
5776
|
+
if (opts.stdio) { cmdServeStdio(opts); return; }
|
|
5777
|
+
const legacyEnvError = legacyIdentityEnvError();
|
|
5778
|
+
if (legacyEnvError) {
|
|
5779
|
+
console.error(legacyEnvError);
|
|
5780
|
+
process.exit(2);
|
|
5781
|
+
}
|
|
5782
|
+
if (opts.agent || opts.agentId) {
|
|
5783
|
+
console.error('HTTP MCP 不接受 --agent/--agent-id 启动参数:身份只从请求头 Authorization + X-Agent-Id + X-Agent-Key 读取。');
|
|
5784
|
+
process.exit(2);
|
|
5785
|
+
}
|
|
4294
5786
|
const host = opts.host || '0.0.0.0';
|
|
4295
5787
|
const port = opts.port || 8787;
|
|
4296
5788
|
const noAuth = !!opts.noAuth;
|
|
4297
5789
|
const root = userRoot();
|
|
4298
5790
|
ensureInit(root);
|
|
4299
5791
|
function authorize(req) {
|
|
4300
|
-
|
|
5792
|
+
const agentId = String(req.headers['x-agent-id'] || '').trim();
|
|
5793
|
+
const agentKey = String(req.headers['x-agent-key'] || '').trim();
|
|
5794
|
+
const toolProfile = normalizeMcpToolProfile(opts.toolProfile);
|
|
5795
|
+
if (noAuth) return { agent: agentId, agentKey: agentKey, toolProfile: toolProfile, identityMode: 'headers' };
|
|
5796
|
+
if (!agentId || !agentKey) return null;
|
|
4301
5797
|
const auth = req.headers['authorization'] || '';
|
|
4302
5798
|
const m = /^Bearer\s+(.+)$/i.exec(auth);
|
|
4303
5799
|
if (!m) return null;
|
|
4304
5800
|
const token = m[1].trim();
|
|
4305
|
-
const agent =
|
|
5801
|
+
const agent = agentId;
|
|
4306
5802
|
const tokenMap = (loadTokens(root).tokens) || {};
|
|
4307
|
-
if (tokenMap[agent] && tokenMap[agent].token === token) return { agent: agent };
|
|
5803
|
+
if (tokenMap[agent] && tokenMap[agent].token === token) return { agent: agent, agentKey: agentKey, toolProfile: toolProfile, identityMode: 'headers' };
|
|
4308
5804
|
return null;
|
|
4309
5805
|
}
|
|
4310
5806
|
function originAllowed(req) {
|
|
@@ -4330,7 +5826,7 @@ function cmdServe(opts) {
|
|
|
4330
5826
|
}
|
|
4331
5827
|
const auth = authorize(req);
|
|
4332
5828
|
if (!auth) {
|
|
4333
|
-
sendJson(res, 401, { jsonrpc: '2.0', error: { code: -32001, message: 'unauthorized: 需要有效 Bearer token 与 X-Agent-Id' } });
|
|
5829
|
+
sendJson(res, 401, { jsonrpc: '2.0', error: { code: -32001, message: 'unauthorized: 需要有效 Bearer token 与 X-Agent-Id;加密私密操作还需 X-Agent-Key' } });
|
|
4334
5830
|
return;
|
|
4335
5831
|
}
|
|
4336
5832
|
if (req.method === 'GET') {
|
|
@@ -4374,17 +5870,41 @@ function cmdServe(opts) {
|
|
|
4374
5870
|
console.log('URL: http://' + host + ':' + port + '/mcp');
|
|
4375
5871
|
console.log('记忆库: ' + root);
|
|
4376
5872
|
console.log('MCP 协议: 2026-07-28(modern,server/discover)/ 2025-11-25(legacy 握手兼容)');
|
|
5873
|
+
console.log('工具分组: ' + normalizeMcpToolProfile(opts.toolProfile));
|
|
4377
5874
|
if (noAuth) console.log('鉴权: 已关闭(--no-auth,仅限可信内网)');
|
|
4378
|
-
else console.log('鉴权: Bearer token + X-Agent-Id(
|
|
5875
|
+
else console.log('鉴权: Bearer token + X-Agent-Id + X-Agent-Key(token 由 token new 生成,agent_key 由 view 授权生成)');
|
|
4379
5876
|
console.log('按 Ctrl+C 停止');
|
|
4380
5877
|
startBackupFallback();
|
|
4381
5878
|
});
|
|
4382
5879
|
}
|
|
4383
5880
|
// ---- stdio 本地零进程模式(客户端按需拉起 CLI)----
|
|
4384
|
-
function cmdServeStdio() {
|
|
5881
|
+
function cmdServeStdio(opts) {
|
|
5882
|
+
const legacyEnvError = legacyIdentityEnvError();
|
|
5883
|
+
if (legacyEnvError) {
|
|
5884
|
+
console.error(legacyEnvError);
|
|
5885
|
+
process.exit(2);
|
|
5886
|
+
}
|
|
5887
|
+
if (opts.agent) {
|
|
5888
|
+
console.error('stdio MCP 不接受 --agent:请使用显式参数 --agent-id <id> + --agent-key-file <path>。');
|
|
5889
|
+
process.exit(2);
|
|
5890
|
+
}
|
|
5891
|
+
if (opts.agentKey) {
|
|
5892
|
+
console.error('stdio MCP 不接受 --agent-key:命令行会暴露 key;请使用 --agent-key-file <path>。');
|
|
5893
|
+
process.exit(2);
|
|
5894
|
+
}
|
|
5895
|
+
const ident = resolveIdentity(opts);
|
|
5896
|
+
if (ident.error) {
|
|
5897
|
+
console.error(ident.error);
|
|
5898
|
+
process.exit(2);
|
|
5899
|
+
}
|
|
4385
5900
|
const root = userRoot();
|
|
4386
5901
|
ensureInit(root);
|
|
4387
|
-
const ctx = {
|
|
5902
|
+
const ctx = {
|
|
5903
|
+
agent: ident.id,
|
|
5904
|
+
agentKey: ident.agentKey,
|
|
5905
|
+
toolProfile: normalizeMcpToolProfile(opts && opts.toolProfile),
|
|
5906
|
+
identityMode: 'stdio-args',
|
|
5907
|
+
};
|
|
4388
5908
|
let buf = '';
|
|
4389
5909
|
process.stdin.setEncoding('utf8');
|
|
4390
5910
|
process.stdin.on('data', function (chunk) {
|
|
@@ -4419,7 +5939,7 @@ function lanTaskRunCmd(opts) {
|
|
|
4419
5939
|
const port = opts.port || 8787;
|
|
4420
5940
|
// schtasks /tr 不接受多余内嵌引号:路径无空格不加引号,含空格/引号用 \" 转义(0.5.2 修复)
|
|
4421
5941
|
const q = (p) => /[\s"]/.test(p) ? '\\"' + p.replace(/"/g, '\\"') + '\\"' : p;
|
|
4422
|
-
return q(process.execPath) + ' ' + q(
|
|
5942
|
+
return q(process.execPath) + ' ' + q(runtimeManagedScript()) + ' serve --host ' + host + ' --port ' + port;
|
|
4423
5943
|
}
|
|
4424
5944
|
|
|
4425
5945
|
function lanServeArgs(opts) {
|
|
@@ -4473,7 +5993,7 @@ function lanAutostartCmdContent(opts) {
|
|
|
4473
5993
|
return '@echo off\r\n'
|
|
4474
5994
|
+ 'chcp 65001 >nul\r\n'
|
|
4475
5995
|
+ 'rem ' + LAN_GEN_MARKER + '; remove with: yotta-memory lan disable (English only, keep ASCII)\r\n'
|
|
4476
|
-
+ q(process.execPath) + ' ' + q(
|
|
5996
|
+
+ q(process.execPath) + ' ' + q(runtimeManagedScript()) + ' serve --host ' + host + ' --port ' + port + ' >> ' + q(lanLogPath(opts)) + ' 2>&1\r\n';
|
|
4477
5997
|
}
|
|
4478
5998
|
function lanVbsContent(opts) {
|
|
4479
5999
|
// v0.6.3 自愈:VBS 内联 autostart.cmd 内容,启动时若 .cmd 缺失/被清理即就地重建,
|
|
@@ -4550,7 +6070,7 @@ function lanLinuxUnitPath() { return path.join(lanLinuxSystemdUserDir(), LAN_UNI
|
|
|
4550
6070
|
function lanLinuxSystemctlBin() { return process.env.YOTTA_LAN_SYSTEMCTL_BIN || 'systemctl'; }
|
|
4551
6071
|
function lanLinuxLoginctlBin() { return process.env.YOTTA_LAN_LOGINCTL_BIN || 'loginctl'; }
|
|
4552
6072
|
function lanLinuxExecStart(opts) {
|
|
4553
|
-
return [systemdEscapeArg(process.execPath), systemdEscapeArg(
|
|
6073
|
+
return [systemdEscapeArg(process.execPath), systemdEscapeArg(runtimeManagedScript())].concat(lanServeArgs(opts)).join(' ');
|
|
4554
6074
|
}
|
|
4555
6075
|
function lanLinuxUnitContent(opts) {
|
|
4556
6076
|
// keep comments ASCII (avoid locale/encoding issues); ExecStart uses systemd's own quote rules
|
|
@@ -4573,7 +6093,7 @@ function lanLinuxUnitContent(opts) {
|
|
|
4573
6093
|
}
|
|
4574
6094
|
function lanCrontabBin() { return process.env.YOTTA_LAN_CRONTAB_BIN || 'crontab'; }
|
|
4575
6095
|
function lanCrontabLine(opts) {
|
|
4576
|
-
const parts = ['@reboot', shQuote(process.execPath), shQuote(
|
|
6096
|
+
const parts = ['@reboot', shQuote(process.execPath), shQuote(runtimeManagedScript())].concat(lanServeArgs(opts));
|
|
4577
6097
|
return parts.join(' ') + ' >> ' + shQuote(lanLogPath(opts)) + ' 2>&1 ' + LAN_CRONTAB_MARKER;
|
|
4578
6098
|
}
|
|
4579
6099
|
function lanCrontabRead() {
|
|
@@ -4814,6 +6334,11 @@ function cmdLanWinStatus() {
|
|
|
4814
6334
|
}
|
|
4815
6335
|
|
|
4816
6336
|
function cmdLanEnable(opts) {
|
|
6337
|
+
const runtime = runtimeEnsureForManagedTask();
|
|
6338
|
+
if (runtime.error) {
|
|
6339
|
+
console.error('runtime 稳定入口未就绪,拒绝注册开机自启: ' + runtime.text);
|
|
6340
|
+
process.exit(1);
|
|
6341
|
+
}
|
|
4817
6342
|
const p = lanPlatform();
|
|
4818
6343
|
if (p === 'win32') return cmdLanWinEnable(opts);
|
|
4819
6344
|
if (p === 'linux') return cmdLanLinuxEnable(opts);
|
|
@@ -5110,7 +6635,7 @@ function consolidateCore(opts) {
|
|
|
5110
6635
|
const root = userRoot();
|
|
5111
6636
|
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
|
|
5112
6637
|
const apply = !!opts.apply;
|
|
5113
|
-
const selfAgent = opts.
|
|
6638
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
5114
6639
|
const unsafe = !!opts.unsafe;
|
|
5115
6640
|
const cfg = loadConfig();
|
|
5116
6641
|
const minAge = (opts.minAge !== undefined && opts.minAge !== null) ? opts.minAge : (parseInt(cfg.consolidate_min_age || '180', 10) || 180);
|
|
@@ -5275,7 +6800,7 @@ function autoMergePairCore(root, keepE, dropE, batch, selfAgent, unsafe) {
|
|
|
5275
6800
|
|
|
5276
6801
|
function appendDedupBlock(lines, root, opts) {
|
|
5277
6802
|
const apply = !!opts.apply;
|
|
5278
|
-
const selfAgent = opts.
|
|
6803
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
5279
6804
|
const unsafe = !!opts.unsafe;
|
|
5280
6805
|
lines.push('');
|
|
5281
6806
|
lines.push('### 重复候选与置信度(--dedup' + (apply ? ' --apply 自动合并' : '') + ')');
|
|
@@ -5335,17 +6860,47 @@ function appendDedupBlock(lines, root, opts) {
|
|
|
5335
6860
|
|
|
5336
6861
|
|
|
5337
6862
|
// ---- usage / main ----
|
|
6863
|
+
function cmdRuntimeInstall(arg, opts) {
|
|
6864
|
+
let result;
|
|
6865
|
+
if (opts.fromCurrent) result = runtimeInstallFromCurrent(opts);
|
|
6866
|
+
else if (!arg) {
|
|
6867
|
+
console.error('runtime install 需要 <tarball|版本>,或使用 --from-current。');
|
|
6868
|
+
process.exit(2);
|
|
6869
|
+
} else if (/\.tgz$/i.test(arg) || fs.existsSync(path.resolve(arg))) {
|
|
6870
|
+
result = runtimeInstallTarball(arg, '', opts);
|
|
6871
|
+
} else {
|
|
6872
|
+
result = runtimeInstallVersion(arg, opts);
|
|
6873
|
+
}
|
|
6874
|
+
console.log(result.text);
|
|
6875
|
+
if (result.error) process.exit(2);
|
|
6876
|
+
}
|
|
6877
|
+
function cmdRuntimeUse(version, opts) {
|
|
6878
|
+
const result = runtimeUseCore(version, opts);
|
|
6879
|
+
console.log(result.text);
|
|
6880
|
+
if (result.error) process.exit(2);
|
|
6881
|
+
}
|
|
6882
|
+
function cmdRuntimeRollback(opts) {
|
|
6883
|
+
const result = runtimeRollbackCore(opts);
|
|
6884
|
+
console.log(result.text);
|
|
6885
|
+
if (result.error) process.exit(2);
|
|
6886
|
+
}
|
|
6887
|
+
function cmdRuntimeStatus() {
|
|
6888
|
+
console.log(runtimeStatusCore().text);
|
|
6889
|
+
}
|
|
6890
|
+
function cmdRuntimeList() {
|
|
6891
|
+
console.log(runtimeListCore().text);
|
|
6892
|
+
}
|
|
5338
6893
|
function usage() {
|
|
5339
6894
|
const banner = 'yotta-memory v' + VERSION + ' — 元忆:有权限边界的文件式智能体记忆';
|
|
5340
6895
|
const sections = [
|
|
5341
6896
|
['核心记忆', [
|
|
5342
6897
|
['init', '初始化记忆库(新建默认加密:需主口令 + 恢复钥匙;--attach 接入已有库;--no-encrypt 降级明文)'],
|
|
5343
|
-
['remember', '写入记忆(--source 来源;--weight 权重;--verify 写后回读校验;--no-hint 关启发)'],
|
|
5344
|
-
['recall', '检索记忆(--type/--limit/--agent/--owner/--all/--unsafe)'],
|
|
6898
|
+
['remember <type> <subject> <statement>', '写入记忆(--source 来源;--weight 权重;--verify 写后回读校验;--no-hint 关启发)'],
|
|
6899
|
+
['recall [关键词]', '检索记忆(--type/--limit/--agent/--owner/--all/--unsafe)'],
|
|
5345
6900
|
['forget', '删除一条记忆'],
|
|
5346
6901
|
['archive', '归档(--days/--threshold 盖棺分+年龄)'],
|
|
5347
6902
|
['backup', '备份记忆库(volumes / setup / status / ensure-daily / schedule / create / list / doctor / restore <id> --to <目录> / drill)'],
|
|
5348
|
-
['doctor', '
|
|
6903
|
+
['doctor', '开工可靠性检查(根目录/密钥库/索引/身份/最近备份;--runtime 加查 CLI/current/MCP/进程/技能副本漂移)'],
|
|
5349
6904
|
['maintain', '记忆自组织(归档/遗忘候选/去重/合并;默认 dry-run;--dedup 查重+置信度,--dedup --apply 自动合并高置信组)'],
|
|
5350
6905
|
['consolidate', '周期摘要压缩(默认 dry-run;--apply 执行;--undo <batch> 回滚;--batches 查批次;候选=超龄+闲置+低效用,immutable/BOUND 豁免)'],
|
|
5351
6906
|
['distill', '心理日志蒸馏(统计摘要/主题画像/知识地图;--model 可选外部模型)'],
|
|
@@ -5366,11 +6921,12 @@ function usage() {
|
|
|
5366
6921
|
['migrate', '把明文私密区迁移为密文(需主口令;迁移后打印恢复钥匙)'],
|
|
5367
6922
|
['view', '启动用户查看平台(--port/--host;口令解锁浏览/授权/吊销 AI)'],
|
|
5368
6923
|
['reset-password', '重设主口令(忘口令用恢复钥匙)'],
|
|
5369
|
-
['key', '管理
|
|
6924
|
+
['key', '管理 agent_key binding(list / bind <id> / rotate <id> / claim <id> [--to <AI_HOME> | --agent-key-file <文件>] / status <id> [--to <AI_HOME> | --agent-key-file <文件>] / revoke <id>;bind/rotate 需主口令或恢复钥匙)'],
|
|
5370
6925
|
['config', '查看/设置配置(get;set memory_home <目录> / backup_dir <目录> / embedding_cmd <命令> / embedding_timeout <毫秒> / maintain_* 阈值与半衰 / consolidate_* 参数)']
|
|
5371
6926
|
]],
|
|
5372
6927
|
['平台与服务', [
|
|
5373
|
-
['serve', '启动 MCP 记忆引擎(streamable HTTP;--stdio
|
|
6928
|
+
['serve', '启动 MCP 记忆引擎(streamable HTTP;--stdio 本地零进程;--tools core|full 控制工具分组)'],
|
|
6929
|
+
['runtime', '运行时稳定入口(list / install <tarball|版本> [--from-current] [--force] / use <版本> [--restart] / rollback [--restart] / status)'],
|
|
5374
6930
|
['lan', '开机自启管理(enable/disable/status;Windows 计划任务 / Linux systemd/crontab)'],
|
|
5375
6931
|
['--version', '版本']
|
|
5376
6932
|
]]
|
|
@@ -5385,9 +6941,9 @@ function usage() {
|
|
|
5385
6941
|
lines.push('');
|
|
5386
6942
|
}
|
|
5387
6943
|
lines.push('类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)');
|
|
5388
|
-
lines.push('环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置;
|
|
6944
|
+
lines.push('环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; 身份不再读取 env;CLI 用 --agent + --agent-key/--agent-key-file,stdio MCP 用 --agent-id + --agent-key-file,HTTP MCP 用请求头');
|
|
5389
6945
|
lines.push('隔离: 公共 FACT 在 facts/;私密 PREF/BOUND/COMMIT 物理分目录 private/<agent_id>/<type>/,禁止 shell 直读写记忆库,一律走本命令');
|
|
5390
|
-
lines.push('远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id>');
|
|
6946
|
+
lines.push('远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id> + X-Agent-Key: <agent_key>');
|
|
5391
6947
|
console.log(lines.join('\n'));
|
|
5392
6948
|
}
|
|
5393
6949
|
async function main() {
|
|
@@ -5395,7 +6951,7 @@ async function main() {
|
|
|
5395
6951
|
if (!args.length) { usage(); return; }
|
|
5396
6952
|
const opts = {};
|
|
5397
6953
|
const positional = [];
|
|
5398
|
-
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', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time']);
|
|
6954
|
+
const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time', '--tools', '--mcp-config', '--skill-dir']);
|
|
5399
6955
|
for (let i = 0; i < args.length; i++) {
|
|
5400
6956
|
const a = args[i];
|
|
5401
6957
|
if (a === '--version' || a === '-v') { console.log(VERSION); return; }
|
|
@@ -5406,6 +6962,8 @@ async function main() {
|
|
|
5406
6962
|
else if (a === '--no-auth') opts.noAuth = true;
|
|
5407
6963
|
else if (a === '--stdio') opts.stdio = true;
|
|
5408
6964
|
else if (a === '--onstart') opts.onstart = true;
|
|
6965
|
+
else if (a === '--from-current') opts.fromCurrent = true;
|
|
6966
|
+
else if (a === '--restart') opts.restart = true;
|
|
5409
6967
|
else if (a === '--force') opts.force = true;
|
|
5410
6968
|
else if (a === '--attach') opts.attach = true;
|
|
5411
6969
|
else if (a === '--allow-same-volume') opts.allowSameVolume = true;
|
|
@@ -5426,6 +6984,7 @@ async function main() {
|
|
|
5426
6984
|
else if (a === '--batches') opts.batches = true;
|
|
5427
6985
|
else if (a === '--explain') opts.explain = true;
|
|
5428
6986
|
else if (a === '--semantic') opts.semantic = true;
|
|
6987
|
+
else if (a === '--runtime') opts.runtime = true;
|
|
5429
6988
|
else if (valueOpts.has(a)) {
|
|
5430
6989
|
const v = args[++i];
|
|
5431
6990
|
if (a === '--type') opts.type = v;
|
|
@@ -5435,6 +6994,9 @@ async function main() {
|
|
|
5435
6994
|
else if (a === '--out') opts.out = v;
|
|
5436
6995
|
else if (a === '--owner') opts.owner = v;
|
|
5437
6996
|
else if (a === '--agent') opts.agent = v;
|
|
6997
|
+
else if (a === '--agent-id') opts.agentId = v;
|
|
6998
|
+
else if (a === '--agent-key') opts.agentKey = v;
|
|
6999
|
+
else if (a === '--agent-key-file') opts.agentKeyFile = v;
|
|
5438
7000
|
else if (a === '--scope') opts.scope = v;
|
|
5439
7001
|
else if (a === '--host') opts.host = v;
|
|
5440
7002
|
else if (a === '--port') opts.port = parseInt(v, 10) || 8787;
|
|
@@ -5463,18 +7025,42 @@ async function main() {
|
|
|
5463
7025
|
else if (a === '--to') opts.to = v;
|
|
5464
7026
|
else if (a === '--id') opts.id = v;
|
|
5465
7027
|
else if (a === '--time') opts.time = v;
|
|
7028
|
+
else if (a === '--tools') opts.toolProfile = v;
|
|
7029
|
+
else if (a === '--mcp-config') opts.mcpConfigPaths = (opts.mcpConfigPaths || []).concat(v);
|
|
7030
|
+
else if (a === '--skill-dir') opts.skillDirs = (opts.skillDirs || []).concat(v);
|
|
5466
7031
|
} else if (a.startsWith('--')) {
|
|
7032
|
+
if (a === '--query') {
|
|
7033
|
+
console.error('未知选项: --query。recall/search 的关键词是位置参数:yotta-memory recall [关键词]');
|
|
7034
|
+
process.exit(2);
|
|
7035
|
+
}
|
|
5467
7036
|
console.error('未知选项: ' + a);
|
|
5468
7037
|
process.exit(2);
|
|
5469
7038
|
} else {
|
|
5470
7039
|
positional.push(a);
|
|
5471
7040
|
}
|
|
5472
7041
|
}
|
|
7042
|
+
if (opts.toolProfile !== undefined) {
|
|
7043
|
+
opts.toolProfile = String(opts.toolProfile).trim().toLowerCase();
|
|
7044
|
+
if (opts.toolProfile !== 'core' && opts.toolProfile !== 'full') {
|
|
7045
|
+
console.error('工具分组必须是 core 或 full(例如:yotta-memory serve --stdio --tools core)。');
|
|
7046
|
+
process.exit(2);
|
|
7047
|
+
}
|
|
7048
|
+
}
|
|
5473
7049
|
const first = positional[0];
|
|
5474
7050
|
const rest = positional.slice(1);
|
|
7051
|
+
if (first === 'remember' && opts.type) {
|
|
7052
|
+
console.error('位置参数用法:yotta-memory remember <type> <subject> <statement>。--type 仅用于 recall/maintain/consolidate 等筛选,不是 remember 的类型参数。');
|
|
7053
|
+
process.exit(2);
|
|
7054
|
+
}
|
|
7055
|
+
if (first === 'remember' && rest.length < 3) {
|
|
7056
|
+
console.error('位置参数不足:yotta-memory remember <type> <subject> <statement>');
|
|
7057
|
+
process.exit(2);
|
|
7058
|
+
}
|
|
7059
|
+
const runtimeIdentity = resolveIdentity(opts);
|
|
7060
|
+
setRuntimeAgent(runtimeIdentity.error ? '' : runtimeIdentity.id, runtimeIdentity.error ? '' : runtimeIdentity.agentKey);
|
|
5475
7061
|
switch (first) {
|
|
5476
7062
|
case 'init': await cmdInit(opts); break;
|
|
5477
|
-
case 'whoami': cmdWhoami(); break;
|
|
7063
|
+
case 'whoami': cmdWhoami(opts); break;
|
|
5478
7064
|
case 'iam': cmdIam(rest[0], opts); break;
|
|
5479
7065
|
case 'config': {
|
|
5480
7066
|
const sub = rest[0];
|
|
@@ -5528,9 +7114,22 @@ async function main() {
|
|
|
5528
7114
|
case 'key': {
|
|
5529
7115
|
const sub = rest[0];
|
|
5530
7116
|
if (sub === 'list') cmdKeyList();
|
|
7117
|
+
else if (sub === 'bind' || sub === 'rotate') await cmdKeyBind(rest[1], opts);
|
|
5531
7118
|
else if (sub === 'authorize') await cmdKeyAuthorize(rest[1], opts);
|
|
7119
|
+
else if (sub === 'claim') cmdKeyClaim(rest[1], opts);
|
|
7120
|
+
else if (sub === 'status') cmdKeyStatus(rest[1], opts);
|
|
5532
7121
|
else if (sub === 'revoke') cmdKeyRevoke(rest[1]);
|
|
5533
|
-
else { console.error('key 子命令: list /
|
|
7122
|
+
else { console.error('key 子命令: list / bind <id> / rotate <id> / claim <id> --to <AI_HOME> / status <id> / revoke <id>'); process.exit(2); }
|
|
7123
|
+
break;
|
|
7124
|
+
}
|
|
7125
|
+
case 'runtime': {
|
|
7126
|
+
const sub = rest[0];
|
|
7127
|
+
if (sub === 'install') cmdRuntimeInstall(rest[1], opts);
|
|
7128
|
+
else if (sub === 'use') cmdRuntimeUse(rest[1], opts);
|
|
7129
|
+
else if (sub === 'rollback') cmdRuntimeRollback(opts);
|
|
7130
|
+
else if (sub === 'status') cmdRuntimeStatus();
|
|
7131
|
+
else if (sub === 'list') cmdRuntimeList();
|
|
7132
|
+
else { console.error('runtime 子命令: list / install <tarball|版本> [--from-current] [--force] / use <版本> [--restart] / rollback [--restart] / status'); process.exit(2); }
|
|
5534
7133
|
break;
|
|
5535
7134
|
}
|
|
5536
7135
|
case 'serve': cmdServe(opts); break;
|
|
@@ -5580,6 +7179,28 @@ module.exports = {
|
|
|
5580
7179
|
collectEntryFiles: collectEntryFiles,
|
|
5581
7180
|
migrateLayout: migrateLayout,
|
|
5582
7181
|
typeSubdir: typeSubdir,
|
|
7182
|
+
runtimeRoot: runtimeRoot,
|
|
7183
|
+
runtimeManifestPath: runtimeManifestPath,
|
|
7184
|
+
runtimeVersionsDir: runtimeVersionsDir,
|
|
7185
|
+
runtimeVersionDir: runtimeVersionDir,
|
|
7186
|
+
runtimeCurrentDir: runtimeCurrentDir,
|
|
7187
|
+
runtimeCurrentBin: runtimeCurrentBin,
|
|
7188
|
+
runtimeExecutionPath: runtimeExecutionPath,
|
|
7189
|
+
runtimeManagedScript: runtimeManagedScript,
|
|
7190
|
+
runtimeTreeHash: runtimeTreeHash,
|
|
7191
|
+
runtimeListVersions: runtimeListVersions,
|
|
7192
|
+
runtimeSwitchCurrent: runtimeSwitchCurrent,
|
|
7193
|
+
runtimeInstallFromCurrent: runtimeInstallFromCurrent,
|
|
7194
|
+
runtimeInstallTarball: runtimeInstallTarball,
|
|
7195
|
+
runtimeInstallVersion: runtimeInstallVersion,
|
|
7196
|
+
runtimeValidateTarballEntries: runtimeValidateTarballEntries,
|
|
7197
|
+
runtimeUseCore: runtimeUseCore,
|
|
7198
|
+
runtimeRollbackCore: runtimeRollbackCore,
|
|
7199
|
+
runtimeStatusCore: runtimeStatusCore,
|
|
7200
|
+
runtimeListCore: runtimeListCore,
|
|
7201
|
+
runtimeDoctorCore: runtimeDoctorCore,
|
|
7202
|
+
runtimeEnsureForManagedTask: runtimeEnsureForManagedTask,
|
|
7203
|
+
runtimeRestartManagedServer: runtimeRestartManagedServer,
|
|
5583
7204
|
lanTaskRunCmd: lanTaskRunCmd,
|
|
5584
7205
|
lanStartupDir: lanStartupDir,
|
|
5585
7206
|
lanAutostartDir: lanAutostartDir,
|
|
@@ -5643,10 +7264,11 @@ module.exports = {
|
|
|
5643
7264
|
unwrapOwnerKey: unwrapOwnerKey,
|
|
5644
7265
|
unwrapOwnerKeyRecovery: unwrapOwnerKeyRecovery,
|
|
5645
7266
|
unwrapRecoveryEnc: unwrapRecoveryEnc,
|
|
5646
|
-
writeOwnerKeyCache: writeOwnerKeyCache,
|
|
5647
7267
|
revokeOwnerKeyCache: revokeOwnerKeyCache,
|
|
5648
|
-
loadOwnerKeyCache: loadOwnerKeyCache,
|
|
5649
7268
|
getOwnerKeyFor: getOwnerKeyFor,
|
|
7269
|
+
keyListCore: keyListCore,
|
|
7270
|
+
migrationRequiredInfo: migrationRequiredInfo,
|
|
7271
|
+
migrationReminderText: migrationReminderText,
|
|
5650
7272
|
encryptMemoryText: encryptMemoryText,
|
|
5651
7273
|
decryptMemoryText: decryptMemoryText,
|
|
5652
7274
|
readMemoryText: readMemoryText,
|
|
@@ -5654,6 +7276,7 @@ module.exports = {
|
|
|
5654
7276
|
loadOwnerIndex: loadOwnerIndex,
|
|
5655
7277
|
saveOwnerIndex: saveOwnerIndex,
|
|
5656
7278
|
viewEntriesCore: viewEntriesCore,
|
|
7279
|
+
viewHtml: viewHtml,
|
|
5657
7280
|
ensureIndex: ensureIndex,
|
|
5658
7281
|
recallPrefilter: recallPrefilter,
|
|
5659
7282
|
tokenize: tokenize,
|
|
@@ -5674,8 +7297,17 @@ module.exports = {
|
|
|
5674
7297
|
writeProfileText: writeProfileText,
|
|
5675
7298
|
migrateCore: migrateCore,
|
|
5676
7299
|
resetPasswordCore: resetPasswordCore,
|
|
5677
|
-
|
|
7300
|
+
keyBindCore: keyBindCore,
|
|
5678
7301
|
keyRevokeCore: keyRevokeCore,
|
|
7302
|
+
keyClaimCore: keyClaimCore,
|
|
7303
|
+
keyStatusCore: keyStatusCore,
|
|
7304
|
+
writeAgentBinding: writeAgentBinding,
|
|
7305
|
+
unwrapAgentBinding: unwrapAgentBinding,
|
|
7306
|
+
writePendingAgentKey: writePendingAgentKey,
|
|
7307
|
+
removePendingAgentKey: removePendingAgentKey,
|
|
7308
|
+
readPendingAgentKey: readPendingAgentKey,
|
|
7309
|
+
agentKeyFilePath: agentKeyFilePath,
|
|
7310
|
+
setRuntimeAgent: setRuntimeAgent,
|
|
5679
7311
|
promptPassword: promptPassword,
|
|
5680
7312
|
initCore: initCore,
|
|
5681
7313
|
backupCreateCore: backupCreateCore,
|