@yottameta/yotta-memory 0.13.2 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +51 -18
- package/README.zh-CN.md +54 -21
- package/SKILL.md +62 -21
- package/USER_GUIDE.md +36 -20
- package/assets/view.html +1 -1
- package/bin/yotta-memory.js +1262 -168
- package/package.json +1 -1
- package/references/faq.md +8 -2
- package/references/protocol.md +30 -8
- package/skill-manifest.json +1 -1
package/bin/yotta-memory.js
CHANGED
|
@@ -22,10 +22,11 @@ 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.1';
|
|
27
28
|
// @generated view-html:start
|
|
28
|
-
const VIEW_HTML = "<!doctype html><html lang=\"zh\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>元忆 · 用户查看平台</title><style>\nbody{font-family:system-ui,-apple-system,\"Microsoft YaHei\",sans-serif;max-width:1000px;margin:24px auto;padding:0 16px;color:#1f2328;background:#fafafa}\nh1{font-size:22px} .card{background:#fff;border:1px solid #e2e2e2;border-radius:10px;padding:16px 18px;margin:14px 0;box-shadow:0 1px 2px rgba(0,0,0,.04)}\nbutton{background:#2563eb;color:#fff;border:0;border-radius:6px;padding:7px 14px;cursor:pointer;margin:2px;font-size:14px}\nbutton.danger{background:#dc2626} button.ghost{background:#e5e7eb;color:#1f2328}\ninput,select{padding:8px;border:1px solid #c9c9c9;border-radius:6px;margin:2px;font-size:14px;box-sizing:border-box}\ntable{border-collapse:collapse;width:100%;font-size:13px} td,th{border:1px solid #ececec;padding:6px 8px;text-align:left;vertical-align:top}\n.owner{display:inline-flex;align-items:center;gap:6px;border:1px solid #ddd;border-radius:8px;padding:5px 10px;margin:4px 6px 4px 0;background:#f6f8fa}\n.entry{border-bottom:1px solid #eee;padding:8px 0} .meta{color:#8a8a8a;font-size:12px}\n.err{color:#dc2626;margin-top:8px} .ok{color:#16a34a;margin-top:8px}\n#app{display:none} code{background:#f0f0f0;padding:1px 5px;border-radius:4px;font-size:12px}\n</style></head><body>\n<h1>元忆 · 用户查看平台 <span id=\"ver\" style=\"font-size:14px;color:#888\"></span></h1>\n<div id=\"lock\" class=\"card\">\n <p><b>输入主口令解锁</b>(口令只在本地内存派生,不落盘、不发送远端)。忘口令可在 CLI 用恢复钥匙重设:<code>yotta-memory reset-password --recovery-key <钥匙></code></p>\n <input type=\"password\" id=\"pw\" placeholder=\"主口令\" style=\"width:260px\">\n <button onclick=\"unlock()\">解锁</button>\n <div class=\"err\" id=\"lockerr\"></div>\n</div>\n<div id=\"app\">\n <div class=\"card\">\n <b>AI 列表</b>(✅=已授权可读自己私密,🔒=未授权)\n <div class=\"meta\" style=\"margin-top:6px\">「授权」由你(用户)操作:确认后生成只显示一次的 agent_key,请立即单独保存;服务端同时写临时待领取文件 <code>keys/pending/<agent_id>.key</code>,供该 AI 新会话领取,领取成功后自动删除。</div>\n <div id=\"owners\" style=\"margin-top:8px\"></div>\n </div>\n <div class=\"card\">\n <b>记忆</b>\n <input id=\"q\" placeholder=\"搜索关键词\" style=\"width:220px\" onkeydown=\"if(event.key==='Enter'){off=0;load()}\">\n <button onclick=\"off=0;load()\">搜索</button>\n <button class=\"ghost\" onclick=\"doExport()\">导出 JSON</button>\n <button class=\"ghost\" onclick=\"showRk()\">显示恢复钥匙</button>\n <span id=\"rkout\" style=\"font-size:12px;color:#888;margin-left:8px\"></span>\n <div id=\"meta\" style=\"margin-top:10px;font-size:12px;color:#666\"></div>\n <div id=\"entries\" style=\"margin-top:6px\"></div>\n <div id=\"pager\" style=\"margin-top:10px\">\n <button class=\"ghost\" id=\"prevb\" onclick=\"prevPage()\">上一页</button>\n <span id=\"pageinfo\" style=\"font-size:12px;color:#888;margin:0 8px\"></span>\n <button class=\"ghost\" id=\"nextb\" onclick=\"nextPage()\">下一页</button>\n </div>\n </div>\n <div class=\"card\">\n <b>重设口令</b><br>\n <input type=\"password\" id=\"cur\" placeholder=\"当前口令\">\n <input type=\"password\" id=\"np1\" placeholder=\"新口令\">\n <input type=\"password\" id=\"np2\" placeholder=\"确认新口令\">\n <button onclick=\"resetPw()\">重设</button>\n <span id=\"pwout\"></span>\n </div>\n</div>\n<script>\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,function(c){return{'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c];});}\nasync function api(p,b){try{const r=await fetch(p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})});return await r.json();}catch(e){return{error:String(e)};}}\nasync function boot(){const s=await api('/api/status');document.getElementById('ver').textContent='v'+(s.version||'');if(s.unlocked){showApp();}}\nfunction showApp(){document.getElementById('lock').style.display='none';document.getElementById('app').style.display='block';loadOwners();load();}\nasync function unlock(){const d=await api('/api/unlock',{password:document.getElementById('pw').value});if(d.error){document.getElementById('lockerr').textContent=d.error;return;}showApp();}\nasync function loadOwners(){const d=await api('/api/owners');const box=document.getElementById('owners');box.innerHTML='';if(!d.owners||!d.owners.length){box.innerHTML='(无 owner)';return;}\n for(const o of d.owners){const c=document.createElement('span');c.className='owner';c.innerHTML=esc(o.owner)+(o.authorized?' ✅':' 🔒')+' <button class=\"ghost\" data-a=\"'+esc(o.owner)+'\">授权</button><button class=\"danger\" data-r=\"'+esc(o.owner)+'\">吊销</button>';box.appendChild(c);}\n box.querySelectorAll('[data-a]').forEach(function(b){b.onclick=function(){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();});};});\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();});};});\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
|
|
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";
|
|
29
30
|
// @generated view-html:end
|
|
30
31
|
// MCP 协议(2026-07-28 无状态 + 2025-11-25 legacy 握手,dual-era)
|
|
31
32
|
const MCP_PROTOCOL_MODERN = '2026-07-28';
|
|
@@ -89,39 +90,531 @@ function memoryRoots() {
|
|
|
89
90
|
}
|
|
90
91
|
return out;
|
|
91
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
|
+
}
|
|
92
573
|
function today() {
|
|
93
574
|
const d = new Date();
|
|
94
575
|
const p = (n) => String(n).padStart(2, '0');
|
|
95
576
|
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
|
|
96
577
|
}
|
|
97
|
-
function envAgentTrusted() {
|
|
98
|
-
const v = String(process.env.YOTTA_MEMORY_TRUST_ENV_AGENT || '').trim().toLowerCase();
|
|
99
|
-
return v === '1' || v === 'true' || v === 'yes';
|
|
100
|
-
}
|
|
101
|
-
function rawEnvAgent() {
|
|
102
|
-
return process.env.YOTTA_AGENT_ID || process.env.AGENT_ID || '';
|
|
103
|
-
}
|
|
104
578
|
let RUNTIME_AGENT = { id: '', agentKey: '' };
|
|
579
|
+
const IDENTITY_CONTEXT = new AsyncLocalStorage();
|
|
105
580
|
const RUNTIME_OWNER_KEYS = new Map();
|
|
106
581
|
function setRuntimeAgent(id, agentKey) {
|
|
107
582
|
RUNTIME_AGENT = { id: String(id || '').trim(), agentKey: String(agentKey || '').trim() };
|
|
108
583
|
RUNTIME_OWNER_KEYS.clear();
|
|
109
584
|
}
|
|
110
|
-
function
|
|
111
|
-
return
|
|
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);
|
|
112
594
|
}
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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');
|
|
117
607
|
}
|
|
118
608
|
function resolveIdentity(opts) {
|
|
119
609
|
opts = opts || {};
|
|
120
|
-
const
|
|
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;
|
|
121
616
|
let explicitKey = String(opts.agentKey || '').trim();
|
|
122
617
|
const keyFile = String(opts.agentKeyFile || '').trim();
|
|
123
|
-
const env = String(rawEnvAgent() || '').trim();
|
|
124
|
-
const trusted = envAgentTrusted();
|
|
125
618
|
if (keyFile) {
|
|
126
619
|
let fileKey = '';
|
|
127
620
|
try { fileKey = fs.readFileSync(path.resolve(keyFile), 'utf8').trim(); } catch (e) {
|
|
@@ -134,26 +627,8 @@ function resolveIdentity(opts) {
|
|
|
134
627
|
if (explicit && !isSafeAgentId(explicit)) {
|
|
135
628
|
return { id: '', agentKey: '', source: 'invalid', error: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
136
629
|
}
|
|
137
|
-
if (
|
|
138
|
-
return { id:
|
|
139
|
-
}
|
|
140
|
-
if (explicit && env && explicit !== env) {
|
|
141
|
-
return {
|
|
142
|
-
id: '',
|
|
143
|
-
agentKey: '',
|
|
144
|
-
source: 'conflict',
|
|
145
|
-
error: '身份冲突:显式身份 ' + explicit + ' 与环境身份 ' + env + ' 不一致。请只保留一个明确身份。',
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
if (explicit) return { id: explicit, agentKey: explicitKey, source: 'explicit', error: '' };
|
|
149
|
-
if (env && trusted) return { id: env, agentKey: trustedEnvAgentKey(), source: 'trusted-env', error: '' };
|
|
150
|
-
if (env && !trusted) {
|
|
151
|
-
return {
|
|
152
|
-
id: '',
|
|
153
|
-
agentKey: '',
|
|
154
|
-
source: 'untrusted-env',
|
|
155
|
-
error: '检测到未受信任的环境身份 ' + env + '。请显式传 --agent <id>,或在 MCP 配置中设置 YOTTA_MEMORY_TRUST_ENV_AGENT=1 后重试。',
|
|
156
|
-
};
|
|
630
|
+
if (explicit) {
|
|
631
|
+
return { id: explicit, agentKey: explicitKey, source: 'explicit', error: '' };
|
|
157
632
|
}
|
|
158
633
|
return { id: '', agentKey: '', source: 'none', error: '' };
|
|
159
634
|
}
|
|
@@ -624,6 +1099,48 @@ function encAgentBindingPath(root, id) { return path.join(encBindingsDir(root),
|
|
|
624
1099
|
function encPendingDir(root) { return path.join(encKeysDir(root), AGENT_PENDING_DIR); }
|
|
625
1100
|
function encPendingPath(root, id) { return path.join(encPendingDir(root), id + '.key'); }
|
|
626
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
|
+
}
|
|
627
1144
|
function isEncrypted(root) { return fs.existsSync(encKeysDir(root)); }
|
|
628
1145
|
function isEncFile(fp) { return String(fp).slice(-ENC_SUFFIX.length) === ENC_SUFFIX; }
|
|
629
1146
|
function ownerFromPrivatePath(root, fp) {
|
|
@@ -816,24 +1333,32 @@ function unwrapAgentBinding(root, owner, agentKey) {
|
|
|
816
1333
|
if (buf.slice(0, AGENT_MAGIC.length).toString('utf8') !== AGENT_MAGIC) throw new Error('agent binding 文件格式错误: ' + p);
|
|
817
1334
|
return aesDecryptBytes(agentKey, buf.slice(AGENT_MAGIC.length), Buffer.from('agent:' + owner, 'utf8'));
|
|
818
1335
|
}
|
|
819
|
-
function getOwnerKeyFor(root, owner) {
|
|
1336
|
+
function getOwnerKeyFor(root, owner, identity) {
|
|
820
1337
|
if (!owner) return null;
|
|
821
1338
|
if (!isEncrypted(root)) return null;
|
|
822
|
-
|
|
823
|
-
|
|
1339
|
+
const current = identity || currentRuntimeIdentity();
|
|
1340
|
+
if (!current.id || current.id !== owner || !current.agentKey) return null;
|
|
1341
|
+
const agentKey = Buffer.from(current.agentKey, 'base64');
|
|
824
1342
|
return unwrapAgentBinding(root, owner, agentKey);
|
|
825
1343
|
}
|
|
826
1344
|
function validatePrivateIdentity(root, ident, owner) {
|
|
827
1345
|
if (!ident || !ident.id) return '私密操作必须先声明显式身份。';
|
|
828
1346
|
if (!isSafeAgentId(owner || ident.id)) return '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。';
|
|
829
1347
|
if (!isEncrypted(root)) return '';
|
|
830
|
-
const
|
|
1348
|
+
const current = currentRuntimeIdentity();
|
|
1349
|
+
const agentKey = ident.agentKey || (current.id === ident.id ? current.agentKey : '');
|
|
831
1350
|
if (!agentKey) {
|
|
832
|
-
return '记忆库已加密,但缺少 agent_key
|
|
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);
|
|
833
1359
|
}
|
|
834
|
-
setRuntimeAgent(ident.id, agentKey);
|
|
835
1360
|
try {
|
|
836
|
-
if (!getOwnerKeyFor(root, owner || ident.id)) {
|
|
1361
|
+
if (!getOwnerKeyFor(root, owner || ident.id, { id: ident.id, agentKey: agentKey })) {
|
|
837
1362
|
return 'agent_key 校验失败:未找到 ' + (owner || ident.id) + ' 的 agent binding(可能已吊销)。请由用户重新授权,或由用户执行 yotta-memory key bind ' + (owner || ident.id) + '。';
|
|
838
1363
|
}
|
|
839
1364
|
} catch (e) {
|
|
@@ -1315,7 +1840,7 @@ function hasGrant(userAgent, ownerAgent) {
|
|
|
1315
1840
|
return false;
|
|
1316
1841
|
}
|
|
1317
1842
|
// 三态读取判定:'read' | 'denied'
|
|
1318
|
-
// selfAgent
|
|
1843
|
+
// selfAgent 为调用方显式声明的可信身份(CLI 参数 / stdio 参数 / HTTP 请求头);--owner 仅供授权与展示,不得授予跨智能体私密读取。
|
|
1319
1844
|
function classifyRead(entry, agent, ownerFilter, unsafe, selfAgent) {
|
|
1320
1845
|
if (entry.scope === 'public') return 'read';
|
|
1321
1846
|
const owner = entry.owner || '';
|
|
@@ -1507,7 +2032,7 @@ function backupWindowsTaskXml(opts) {
|
|
|
1507
2032
|
opts = opts || {};
|
|
1508
2033
|
const time = backupTimeParts(opts.time || '03:30');
|
|
1509
2034
|
const nodePath = opts.nodePath || process.execPath;
|
|
1510
|
-
const scriptPath = opts.scriptPath ||
|
|
2035
|
+
const scriptPath = opts.scriptPath || runtimeManagedScript();
|
|
1511
2036
|
const userId = opts.userId || '';
|
|
1512
2037
|
const principal = userId
|
|
1513
2038
|
? '<Principal id="Author"><UserId>' + xmlEscape(userId) + '</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal>'
|
|
@@ -1540,7 +2065,7 @@ function backupWindowsTaskXml(opts) {
|
|
|
1540
2065
|
function backupSystemdServiceContent(opts) {
|
|
1541
2066
|
opts = opts || {};
|
|
1542
2067
|
const nodePath = opts.nodePath || process.execPath;
|
|
1543
|
-
const scriptPath = opts.scriptPath ||
|
|
2068
|
+
const scriptPath = opts.scriptPath || runtimeManagedScript();
|
|
1544
2069
|
return [
|
|
1545
2070
|
'[Unit]',
|
|
1546
2071
|
'Description=Yotta Memory daily backup',
|
|
@@ -1573,7 +2098,7 @@ function backupCronLine(opts) {
|
|
|
1573
2098
|
opts = opts || {};
|
|
1574
2099
|
const time = backupTimeParts(opts.time || '03:30');
|
|
1575
2100
|
const nodePath = opts.nodePath || process.execPath;
|
|
1576
|
-
const scriptPath = opts.scriptPath ||
|
|
2101
|
+
const scriptPath = opts.scriptPath || runtimeManagedScript();
|
|
1577
2102
|
const logPath = opts.logPath || '';
|
|
1578
2103
|
const redirect = logPath ? ' >> ' + shQuote(logPath) + ' 2>&1' : '';
|
|
1579
2104
|
return time.minute + ' ' + time.hour + ' * * * ' + shQuote(nodePath) + ' ' + shQuote(scriptPath) + ' backup ensure-daily' + redirect + ' ' + BACKUP_CRON_MARKER;
|
|
@@ -1582,7 +2107,7 @@ function backupLaunchdPlist(opts) {
|
|
|
1582
2107
|
opts = opts || {};
|
|
1583
2108
|
const time = backupTimeParts(opts.time || '03:30');
|
|
1584
2109
|
const nodePath = opts.nodePath || process.execPath;
|
|
1585
|
-
const scriptPath = opts.scriptPath ||
|
|
2110
|
+
const scriptPath = opts.scriptPath || runtimeManagedScript();
|
|
1586
2111
|
const logPath = opts.logPath || '';
|
|
1587
2112
|
const lines = [
|
|
1588
2113
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
@@ -1633,7 +2158,7 @@ function backupScheduleEnableCore(opts) {
|
|
|
1633
2158
|
const time = opts.time || '03:30';
|
|
1634
2159
|
try { backupTimeParts(time); } catch (error) { return { error: true, text: error.message }; }
|
|
1635
2160
|
const nodePath = opts.nodePath || process.execPath;
|
|
1636
|
-
const scriptPath = opts.scriptPath ||
|
|
2161
|
+
const scriptPath = opts.scriptPath || runtimeManagedScript();
|
|
1637
2162
|
try {
|
|
1638
2163
|
if (platform === 'win32') {
|
|
1639
2164
|
const userId = opts.userId || (process.env.USERDOMAIN && process.env.USERNAME ? process.env.USERDOMAIN + '\\' + process.env.USERNAME : '');
|
|
@@ -1752,7 +2277,7 @@ function backupScheduleStatusCore(opts) {
|
|
|
1752
2277
|
return { error: false, registered: false, scheduler: '', text: '当前平台不支持自动备份调度: ' + platform };
|
|
1753
2278
|
}
|
|
1754
2279
|
function backupFallbackCommand() {
|
|
1755
|
-
return { command: process.execPath, args: [
|
|
2280
|
+
return { command: process.execPath, args: [runtimeManagedScript(), 'backup', 'ensure-daily'] };
|
|
1756
2281
|
}
|
|
1757
2282
|
function startBackupFallback() {
|
|
1758
2283
|
const cfg = loadConfig();
|
|
@@ -2033,6 +2558,348 @@ function backupHealthCore(opts) {
|
|
|
2033
2558
|
}
|
|
2034
2559
|
return { level: 'none', needsSetup: false, text: '' };
|
|
2035
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
|
+
}
|
|
2036
2903
|
// 开工可靠性检查:只读检查根目录、密钥库、索引、身份登记与最近备份。
|
|
2037
2904
|
function doctorCore(opts) {
|
|
2038
2905
|
opts = opts || {};
|
|
@@ -2131,6 +2998,18 @@ function doctorCore(opts) {
|
|
|
2131
2998
|
if (cfg.backup_scheduler_error) warnings.push('每日备份调度异常: ' + cfg.backup_scheduler_error);
|
|
2132
2999
|
}
|
|
2133
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
|
+
|
|
2134
3013
|
const level = critical.length ? 'critical' : (warnings.length ? 'warning' : 'ok');
|
|
2135
3014
|
const lines = [
|
|
2136
3015
|
'# yotta-memory doctor(开工可靠性检查)',
|
|
@@ -2143,6 +3022,10 @@ function doctorCore(opts) {
|
|
|
2143
3022
|
for (const message of critical) lines.push('- [严重] ' + message);
|
|
2144
3023
|
for (const message of warnings) lines.push('- [警告] ' + message);
|
|
2145
3024
|
if (!critical.length && !warnings.length) lines.push('- 检查项: 全部通过');
|
|
3025
|
+
if (runtimeReport) {
|
|
3026
|
+
lines.push('');
|
|
3027
|
+
lines.push(runtimeReport.text);
|
|
3028
|
+
}
|
|
2146
3029
|
if (level === 'critical') {
|
|
2147
3030
|
lines.push('');
|
|
2148
3031
|
lines.push('- 破坏性写入已锁定:先运行 yotta-memory doctor 修复严重问题。');
|
|
@@ -2505,7 +3388,7 @@ function rememberCore(type, subject, statement, opts) {
|
|
|
2505
3388
|
if (keyError) return { error: true, text: keyError };
|
|
2506
3389
|
}
|
|
2507
3390
|
if (scope === 'private' && !owner) {
|
|
2508
|
-
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)不受影响。' };
|
|
2509
3392
|
}
|
|
2510
3393
|
if (scope === 'private' && owner && selfAgent && owner !== selfAgent && !opts.unsafe) {
|
|
2511
3394
|
return { error: true, text: '拒绝: 当前显式身份 ' + selfAgent + ' 不能写入其它智能体 ' + owner + ' 的私密区。请传正确的 --agent <id>,或加 --unsafe(用户显式授权)。' };
|
|
@@ -2627,7 +3510,7 @@ function recallCore(query, opts) {
|
|
|
2627
3510
|
return {
|
|
2628
3511
|
error: true,
|
|
2629
3512
|
exitCode: 3,
|
|
2630
|
-
text: '
|
|
3513
|
+
text: '显式跨智能体读取必须先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
|
|
2631
3514
|
};
|
|
2632
3515
|
}
|
|
2633
3516
|
if (selfAgent && !allSafe) {
|
|
@@ -2813,7 +3696,7 @@ function forgetCore(fileRef, opts) {
|
|
|
2813
3696
|
if (seg[0] === 'private') {
|
|
2814
3697
|
const owner = seg[1] || '';
|
|
2815
3698
|
if (!opts.unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
|
|
2816
|
-
return { error: true, text: '拒绝: 不能删除其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用
|
|
3699
|
+
return { error: true, text: '拒绝: 不能删除其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 --agent / --agent-id 声明自己的身份,或加 --unsafe(用户显式授权)。' };
|
|
2817
3700
|
}
|
|
2818
3701
|
}
|
|
2819
3702
|
const trashFile = path.join(
|
|
@@ -2910,7 +3793,7 @@ function checkOwnerWritable(targetRoot, targetRel, selfAgent, unsafe) {
|
|
|
2910
3793
|
if (seg[0] === PRIVATE_DIR) {
|
|
2911
3794
|
const owner = seg[1] || '';
|
|
2912
3795
|
if (!unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
|
|
2913
|
-
return '拒绝: 不能操作其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用
|
|
3796
|
+
return '拒绝: 不能操作其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 --agent / --agent-id 声明自己的身份,或加 --unsafe(用户显式授权)。';
|
|
2914
3797
|
}
|
|
2915
3798
|
}
|
|
2916
3799
|
return null;
|
|
@@ -3483,7 +4366,7 @@ function keyBindCore(root, owner, opts) {
|
|
|
3483
4366
|
error: false,
|
|
3484
4367
|
agentKey: agentKey.toString('base64'),
|
|
3485
4368
|
pendingFile: pendingFile,
|
|
3486
|
-
text: '已绑定 ' + owner + ' 的 agent_key,并删除旧明文 cache。\nagent_key: ' + agentKey.toString('base64') + '\n待领取文件: ' + pendingFile + '\nAI 在新会话执行 yotta-memory key
|
|
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 只显示一次。',
|
|
3487
4370
|
};
|
|
3488
4371
|
}
|
|
3489
4372
|
|
|
@@ -3498,9 +4381,10 @@ function keyRevokeCore(root, owner) {
|
|
|
3498
4381
|
return { error: false, text: '已吊销 ' + owner + ' 的 agent_key binding;该 agent 将无法解密其私密。' };
|
|
3499
4382
|
}
|
|
3500
4383
|
|
|
3501
|
-
function keyClaimCore(root, owner,
|
|
4384
|
+
function keyClaimCore(root, owner, opts) {
|
|
3502
4385
|
if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
3503
|
-
|
|
4386
|
+
const location = resolveAgentKeyLocation(owner, opts);
|
|
4387
|
+
if (!location.ok) return { error: true, text: location.error };
|
|
3504
4388
|
const pending = encPendingPath(root, owner);
|
|
3505
4389
|
if (!fs.existsSync(pending)) {
|
|
3506
4390
|
return { error: true, text: '未找到待领取 agent_key:' + pending + '。请先在 yotta-memory view 中授权,或执行 yotta-memory key bind ' + owner + '。' };
|
|
@@ -3519,9 +4403,9 @@ function keyClaimCore(root, owner, targetHome) {
|
|
|
3519
4403
|
let targetFile;
|
|
3520
4404
|
let tempFile;
|
|
3521
4405
|
try {
|
|
3522
|
-
|
|
4406
|
+
targetFile = location.file;
|
|
4407
|
+
targetDir = path.dirname(targetFile);
|
|
3523
4408
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
3524
|
-
targetFile = agentKeyFilePath(targetDir);
|
|
3525
4409
|
tempFile = path.join(targetDir, AGENT_KEY_FILENAME + '.tmp-' + process.pid + '-' + crypto.randomBytes(4).toString('hex'));
|
|
3526
4410
|
fs.writeFileSync(tempFile, agentKey.toString('base64') + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
3527
4411
|
try { fs.chmodSync(tempFile, 0o600); } catch (e) {}
|
|
@@ -3538,31 +4422,36 @@ function keyClaimCore(root, owner, targetHome) {
|
|
|
3538
4422
|
return { error: true, text: '写入宿主 key 文件失败:' + e.message + '。pending 文件保留,可重试。' };
|
|
3539
4423
|
}
|
|
3540
4424
|
removePendingAgentKey(root, owner);
|
|
3541
|
-
return {
|
|
4425
|
+
return {
|
|
4426
|
+
error: false,
|
|
4427
|
+
targetFile: targetFile,
|
|
4428
|
+
source: location.source,
|
|
4429
|
+
text: '已领取 ' + owner + ' 的 agent_key。\n目标文件: ' + targetFile + '\n发现规则: ' + location.source + '\npending 文件已删除。',
|
|
4430
|
+
};
|
|
3542
4431
|
}
|
|
3543
4432
|
|
|
3544
|
-
function keyStatusCore(root, owner,
|
|
4433
|
+
function keyStatusCore(root, owner, opts) {
|
|
3545
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 };
|
|
3546
4437
|
const binding = fs.existsSync(encAgentBindingPath(root, owner));
|
|
3547
4438
|
const pending = fs.existsSync(encPendingPath(root, owner));
|
|
3548
4439
|
let hostKey = 'missing';
|
|
3549
|
-
if (
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
if (binding && unwrapAgentBinding(root, owner, key)) hostKey = 'valid';
|
|
3556
|
-
} catch (e) {}
|
|
3557
|
-
}
|
|
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) {}
|
|
3558
4446
|
}
|
|
3559
4447
|
return {
|
|
3560
4448
|
error: false,
|
|
3561
4449
|
binding: binding,
|
|
3562
4450
|
pending: pending,
|
|
3563
4451
|
hostKey: hostKey,
|
|
3564
|
-
targetFile:
|
|
3565
|
-
|
|
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,
|
|
3566
4455
|
};
|
|
3567
4456
|
}
|
|
3568
4457
|
|
|
@@ -3620,16 +4509,16 @@ function cmdKeyRevoke(id) {
|
|
|
3620
4509
|
|
|
3621
4510
|
function cmdKeyClaim(id, opts) {
|
|
3622
4511
|
const root = userRoot();
|
|
3623
|
-
if (!id) { console.error('请指定 <id>:yotta-memory key claim <id> --to <AI_HOME>'); process.exit(2); }
|
|
3624
|
-
const r = keyClaimCore(root, id, opts
|
|
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);
|
|
3625
4514
|
console.log(r.text);
|
|
3626
4515
|
if (r.error) process.exit(2);
|
|
3627
4516
|
}
|
|
3628
4517
|
|
|
3629
4518
|
function cmdKeyStatus(id, opts) {
|
|
3630
4519
|
const root = userRoot();
|
|
3631
|
-
if (!id) { console.error('请指定 <id>:yotta-memory key status <id> [--to <AI_HOME>]'); process.exit(2); }
|
|
3632
|
-
const r = keyStatusCore(root, id, opts
|
|
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);
|
|
3633
4522
|
console.log(r.text);
|
|
3634
4523
|
if (r.error) process.exit(2);
|
|
3635
4524
|
}
|
|
@@ -3960,7 +4849,15 @@ function cmdBackupEnsureDaily(opts) {
|
|
|
3960
4849
|
}
|
|
3961
4850
|
function cmdBackupSchedule(action, opts) {
|
|
3962
4851
|
let r;
|
|
3963
|
-
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
|
+
}
|
|
3964
4861
|
else if (action === 'disable') r = backupScheduleDisableCore(opts);
|
|
3965
4862
|
else if (action === 'status') r = backupScheduleStatusCore(opts);
|
|
3966
4863
|
else {
|
|
@@ -4272,8 +5169,8 @@ function cmdWhoami(opts) {
|
|
|
4272
5169
|
}
|
|
4273
5170
|
const id = ident.id;
|
|
4274
5171
|
if (!id) {
|
|
4275
|
-
console.log('
|
|
4276
|
-
console.log('CLI
|
|
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。');
|
|
4277
5174
|
console.log('远端:token + X-Agent-Id + X-Agent-Key(token new --agent <id>;agent_key 由 view 授权生成)。');
|
|
4278
5175
|
return;
|
|
4279
5176
|
}
|
|
@@ -4330,14 +5227,14 @@ function profileCore(opts) {
|
|
|
4330
5227
|
return {
|
|
4331
5228
|
error: true,
|
|
4332
5229
|
exitCode: 3,
|
|
4333
|
-
text: '
|
|
5230
|
+
text: '生成私密画像必须先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
|
|
4334
5231
|
};
|
|
4335
5232
|
}
|
|
4336
5233
|
const selfAgent = ident.id;
|
|
4337
5234
|
const owner = opts.owner || selfAgent;
|
|
4338
5235
|
const keyError = validatePrivateIdentity(root, ident, owner);
|
|
4339
5236
|
if (keyError) return { error: true, exitCode: 3, text: keyError };
|
|
4340
|
-
if (!owner) return { error: true, exitCode: 2, text: '
|
|
5237
|
+
if (!owner) return { error: true, exitCode: 2, text: '请先声明身份(--agent / --agent-id)或传 --owner <id>,再生成画像。' };
|
|
4341
5238
|
if (selfAgent && owner !== selfAgent && owner !== 'user' && !opts.unsafe && !hasGrant(selfAgent, owner)) {
|
|
4342
5239
|
return { error: true, exitCode: 3, text: '拒绝: 不能生成其它智能体 ' + owner + ' 的画像(private/' + owner + '/ 为私密区)。如需读取请 --owner user 或 --unsafe(用户显式授权)。' };
|
|
4343
5240
|
}
|
|
@@ -4383,19 +5280,35 @@ function selfProfileKv(root, id) {
|
|
|
4383
5280
|
try { text = readMemoryText(root, path.join(selfPrefsDir(root, id), profile), id); } catch (e) { return {}; }
|
|
4384
5281
|
return parseKvBody(parseFrontmatter(text).body);
|
|
4385
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
|
+
}
|
|
4386
5299
|
function contextCore(opts) {
|
|
4387
5300
|
opts = opts || {};
|
|
4388
5301
|
const roots = memoryRoots();
|
|
4389
5302
|
if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
|
|
4390
5303
|
const root = userRoot();
|
|
4391
|
-
const limit = opts.limit || 10;
|
|
5304
|
+
const limit = parseInt(opts.limit || '10', 10) || 10;
|
|
4392
5305
|
const ident = resolveIdentity(opts);
|
|
4393
5306
|
if (ident.error) return { error: true, exitCode: 3, text: ident.error };
|
|
4394
5307
|
if (!ident.id) {
|
|
4395
5308
|
return {
|
|
4396
5309
|
error: true,
|
|
4397
5310
|
exitCode: 3,
|
|
4398
|
-
text: '开工上下文包含私密画像 / 边界 /
|
|
5311
|
+
text: '开工上下文包含私密画像 / 边界 / 承诺,必须先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
|
|
4399
5312
|
};
|
|
4400
5313
|
}
|
|
4401
5314
|
const selfAgent = ident.id;
|
|
@@ -4410,14 +5323,32 @@ function contextCore(opts) {
|
|
|
4410
5323
|
const embeddingTimeout = effectiveEmbeddingTimeout(opts);
|
|
4411
5324
|
const trace = [];
|
|
4412
5325
|
const lines = [];
|
|
5326
|
+
const shownFiles = new Set();
|
|
4413
5327
|
const FENCE = String.fromCharCode(96, 96, 96);
|
|
4414
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
|
+
}
|
|
4415
5346
|
lines.push('# 开工上下文包(yotta-memory context)');
|
|
4416
5347
|
lines.push('');
|
|
4417
5348
|
lines.push('## 1. 身份');
|
|
4418
5349
|
lines.push('');
|
|
4419
5350
|
if (!owner) {
|
|
4420
|
-
lines.push('- 未声明智能体身份(无
|
|
5351
|
+
lines.push('- 未声明智能体身份(无 --agent / --agent-id / X-Agent-Id)。私密记忆与画像不可用;请先 whoami / iam。');
|
|
4421
5352
|
} else {
|
|
4422
5353
|
lines.push('- agent_id: ' + owner);
|
|
4423
5354
|
const kv = selfProfileKv(root, owner);
|
|
@@ -4462,8 +5393,26 @@ function contextCore(opts) {
|
|
|
4462
5393
|
}
|
|
4463
5394
|
lines.push('');
|
|
4464
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
|
+
|
|
4465
5414
|
if (focus) {
|
|
4466
|
-
lines.push('## 2.
|
|
5415
|
+
lines.push('## 2.6 任务相关记忆(--focus)');
|
|
4467
5416
|
lines.push('');
|
|
4468
5417
|
const focused = recallCore(focus, {
|
|
4469
5418
|
limit: limit,
|
|
@@ -4476,70 +5425,51 @@ function contextCore(opts) {
|
|
|
4476
5425
|
const focusedEntries = focused.entries || [];
|
|
4477
5426
|
if (!focusedEntries.length) lines.push('(无匹配记忆)');
|
|
4478
5427
|
for (const e of focusedEntries) {
|
|
4479
|
-
|
|
4480
|
-
if (budget > 0 && usedChars() + line.length > budget) {
|
|
4481
|
-
trace.push('[dropped] ' + e.file + ' reason: budget_exceeded');
|
|
4482
|
-
continue;
|
|
4483
|
-
}
|
|
4484
|
-
lines.push(line);
|
|
4485
|
-
trace.push('[included] ' + e.file + ' reason: focus_match score: ' + round3(e.score));
|
|
5428
|
+
appendContextEntry(e, 'focus_match score: ' + round3(e.score));
|
|
4486
5429
|
}
|
|
4487
5430
|
lines.push('');
|
|
4488
5431
|
}
|
|
4489
5432
|
|
|
4490
|
-
lines.push('## 3.
|
|
5433
|
+
lines.push('## 3. 近期走廊(按时间)');
|
|
4491
5434
|
lines.push('');
|
|
4492
|
-
const
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
}
|
|
4500
|
-
recent.sort(function (a, b) { return b.s - a.s; });
|
|
4501
|
-
const recentShown = recent.slice(0, limit);
|
|
4502
|
-
if (!recentShown.length) lines.push('(暂无记忆)');
|
|
4503
|
-
for (const h of recentShown) {
|
|
4504
|
-
const line = '- [' + h.e.type + '] ' + h.e.subject + ': ' + h.e.statement;
|
|
4505
|
-
if (budget > 0 && usedChars() + line.length > budget) {
|
|
4506
|
-
trace.push('[dropped] ' + h.e.file + ' reason: budget_exceeded');
|
|
4507
|
-
break;
|
|
4508
|
-
}
|
|
4509
|
-
lines.push(line);
|
|
4510
|
-
trace.push('[included] ' + h.e.file + ' reason: recent_match');
|
|
4511
|
-
}
|
|
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');
|
|
4512
5442
|
lines.push('');
|
|
4513
|
-
|
|
5443
|
+
|
|
5444
|
+
lines.push('## 4. 近期高价值记忆(补位)');
|
|
4514
5445
|
lines.push('');
|
|
4515
|
-
const
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
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');
|
|
5453
|
+
lines.push('');
|
|
5454
|
+
lines.push('## 5. 边界提醒(BOUND)');
|
|
5455
|
+
lines.push('');
|
|
5456
|
+
const bounds = readableEntries.filter(function (e) { return e.type === 'BOUND'; }).sort(compareContextRecent);
|
|
4523
5457
|
if (!bounds.length) lines.push('(无)');
|
|
4524
5458
|
for (const e of bounds) lines.push('- ' + (e.subject || '边界') + ': ' + e.statement);
|
|
4525
5459
|
lines.push('');
|
|
4526
|
-
lines.push('##
|
|
5460
|
+
lines.push('## 6. 承诺 / 锚点(COMMIT)');
|
|
4527
5461
|
lines.push('');
|
|
4528
|
-
const commits =
|
|
4529
|
-
for (const r of roots) {
|
|
4530
|
-
for (const e of ensureIndex(r)) {
|
|
4531
|
-
if (e.type !== 'COMMIT') continue;
|
|
4532
|
-
if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
|
|
4533
|
-
commits.push(e);
|
|
4534
|
-
}
|
|
4535
|
-
}
|
|
5462
|
+
const commits = readableEntries.filter(function (e) { return e.type === 'COMMIT'; }).sort(compareContextRecent);
|
|
4536
5463
|
if (!commits.length) lines.push('(无)');
|
|
4537
5464
|
for (const e of commits) lines.push('- ' + (e.subject || '承诺') + ': ' + e.statement);
|
|
4538
5465
|
lines.push('');
|
|
4539
|
-
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` 回读,无长期价值不硬凑。');
|
|
4540
5471
|
lines.push('');
|
|
4541
|
-
const allEntries =
|
|
4542
|
-
for (const r of roots) for (const e of ensureIndex(r)) allEntries.push(e);
|
|
5472
|
+
const allEntries = readableEntries;
|
|
4543
5473
|
const myCommits = allEntries.filter(function (e) { return e.type === 'COMMIT' && classifyRead(e, owner, '', unsafe, selfAgent) !== 'denied'; });
|
|
4544
5474
|
if (!myCommits.length) {
|
|
4545
5475
|
lines.push('- 最近承诺: 无(收工请补 COMMIT / 交接锚点)');
|
|
@@ -4552,7 +5482,7 @@ function contextCore(opts) {
|
|
|
4552
5482
|
if (oldCount > 0) lines.push('- 归档提醒: ' + oldCount + ' 条超 180 天,建议 archive');
|
|
4553
5483
|
if (explain) {
|
|
4554
5484
|
lines.push('');
|
|
4555
|
-
lines.push('##
|
|
5485
|
+
lines.push('## 8. 选择解释(--explain)');
|
|
4556
5486
|
lines.push('');
|
|
4557
5487
|
if (!trace.length) lines.push('(无选择记录)');
|
|
4558
5488
|
for (const t of trace) lines.push(t);
|
|
@@ -4619,8 +5549,12 @@ function cmdConfigGet() {
|
|
|
4619
5549
|
}
|
|
4620
5550
|
|
|
4621
5551
|
// ---- MCP serve(局域网 streamable HTTP 记忆引擎,零依赖)----
|
|
4622
|
-
|
|
4623
|
-
|
|
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 = [
|
|
4624
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'] } },
|
|
4625
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' } } } },
|
|
4626
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' } } } },
|
|
@@ -4631,16 +5565,32 @@ function mcpTools() {
|
|
|
4631
5565
|
{ name: 'reindex', description: '重建索引(手动改 .md 后校正;扫描 facts/prefs/bounds/commits 四目录)', inputSchema: { type: 'object', properties: {} } },
|
|
4632
5566
|
{ name: 'export', description: '导出全部记忆到记忆库内的 JSON 文件。out 可选(默认 <记忆库>/yottamemory-export-<日期>.json;仅限记忆库内路径)', inputSchema: { type: 'object', properties: { out: { type: 'string' } } } },
|
|
4633
5567
|
{ name: 'import', description: '从记忆库内的 JSON 文件导入记忆。src 为文件路径(相对记忆库目录,或记忆库内绝对路径;仅限记忆库内)', inputSchema: { type: 'object', properties: { src: { type: 'string' } }, required: ['src'] } },
|
|
4634
|
-
{ name: 'agent_info', description: '
|
|
5568
|
+
{ name: 'agent_info', description: '查看当前智能体身份与登记状态(HTTP 读经 token 校验的 X-Agent-Id;stdio 读 --agent-id 显式参数)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
|
|
4635
5569
|
{ name: 'profile', description: '生成当前智能体的用户画像(只读聚合 private/<owner>/ 下 PREF/BOUND/COMMIT 原文,零推断,写入 private/<owner>/profile.md)。owner 默认当前智能体', inputSchema: { type: 'object', properties: { owner: { type: 'string' } } } },
|
|
4636
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'] } },
|
|
4637
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' } } } },
|
|
4638
5572
|
{ name: 'distill', description: '心理日志蒸馏(自我提升):统计摘要/主题画像/知识地图。owner 默认当前智能体(MCP 不支持 --model 外部命令)', inputSchema: { type: 'object', properties: { owner: { type: 'string' }, subject: { type: 'string' } } } },
|
|
4639
5573
|
{ name: 'explain', description: '解释单条记忆效用分项(为什么靠前/归档/遗忘)。file 为记忆文件路径或文件名', inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] } },
|
|
4640
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);
|
|
4641
5579
|
}
|
|
4642
5580
|
function callTool(name, args, ctx) {
|
|
4643
|
-
|
|
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
|
+
}
|
|
4644
5594
|
if (agent && !isSafeAgentId(agent)) return { text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。', error: true };
|
|
4645
5595
|
try {
|
|
4646
5596
|
if (name === 'remember') {
|
|
@@ -4710,7 +5660,7 @@ function callTool(name, args, ctx) {
|
|
|
4710
5660
|
if (name === 'agent_info') {
|
|
4711
5661
|
const root = userRoot();
|
|
4712
5662
|
const id = agent || '';
|
|
4713
|
-
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 };
|
|
4714
5664
|
const agents = loadAgents(root).agents || {};
|
|
4715
5665
|
const tokens = loadTokens(root).tokens || {};
|
|
4716
5666
|
let reg = '未登记';
|
|
@@ -4757,18 +5707,32 @@ function reqVersion(params) {
|
|
|
4757
5707
|
const meta = (params && params._meta) || {};
|
|
4758
5708
|
return meta['io.modelcontextprotocol/protocolVersion'] || null;
|
|
4759
5709
|
}
|
|
4760
|
-
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) {
|
|
4761
5726
|
const out = { resultType: 'complete' };
|
|
4762
5727
|
Object.assign(out, payload);
|
|
4763
5728
|
if (ttl) { out.ttlMs = ttl[0]; out.cacheScope = ttl[1]; }
|
|
4764
|
-
out._meta = { 'io.modelcontextprotocol/serverInfo':
|
|
5729
|
+
out._meta = { 'io.modelcontextprotocol/serverInfo': mcpServerInfo(ctx) };
|
|
4765
5730
|
return out;
|
|
4766
5731
|
}
|
|
4767
5732
|
function unsupportedVersion(id, pv) {
|
|
4768
5733
|
return { jsonrpc: '2.0', id: id, error: { code: -32022, message: 'Unsupported protocol version', data: { supported: [MCP_PROTOCOL_MODERN], requested: pv } } };
|
|
4769
5734
|
}
|
|
4770
5735
|
function handleMessage(msg, ctx) {
|
|
4771
|
-
setRuntimeAgent(ctx && ctx.agent, ctx && ctx.agentKey);
|
|
4772
5736
|
if (!msg || msg.jsonrpc !== '2.0') return { jsonrpc: '2.0', id: msg && msg.id, error: { code: -32600, message: 'invalid request' } };
|
|
4773
5737
|
const id = msg.id;
|
|
4774
5738
|
if (id === undefined || id === null) return null;
|
|
@@ -4778,17 +5742,18 @@ function handleMessage(msg, ctx) {
|
|
|
4778
5742
|
if (pv !== null) {
|
|
4779
5743
|
// ---- modern(2026-07-28 无状态)----
|
|
4780
5744
|
if (pv !== MCP_PROTOCOL_MODERN) return unsupportedVersion(id, pv);
|
|
5745
|
+
const toolProfile = normalizeMcpToolProfile(ctx && ctx.toolProfile);
|
|
4781
5746
|
if (method === 'server/discover') {
|
|
4782
5747
|
return { jsonrpc: '2.0', id: id, result: modernOk({
|
|
4783
5748
|
supportedVersions: [MCP_PROTOCOL_MODERN],
|
|
4784
5749
|
capabilities: { tools: {} },
|
|
4785
|
-
instructions: '元忆 MCP(基于 MCP 最新协议 2026-07-28,向后兼容 2025-11-25
|
|
4786
|
-
}, [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) };
|
|
4787
5752
|
}
|
|
4788
|
-
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) };
|
|
4789
5754
|
if (method === 'tools/call') {
|
|
4790
5755
|
const out = callTool(params.name || '', params.arguments || {}, ctx);
|
|
4791
|
-
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) };
|
|
4792
5757
|
}
|
|
4793
5758
|
if (method === 'initialize') {
|
|
4794
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']" } };
|
|
@@ -4797,10 +5762,10 @@ function handleMessage(msg, ctx) {
|
|
|
4797
5762
|
}
|
|
4798
5763
|
// ---- legacy(<=2025-11-25,initialize 握手;响应保持旧形状)----
|
|
4799
5764
|
if (method === 'initialize') {
|
|
4800
|
-
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) } };
|
|
4801
5766
|
}
|
|
4802
5767
|
if (method === 'ping') return { jsonrpc: '2.0', id: id, result: {} };
|
|
4803
|
-
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) } };
|
|
4804
5769
|
if (method === 'tools/call') {
|
|
4805
5770
|
const out = callTool(params.name || '', params.arguments || {}, ctx);
|
|
4806
5771
|
return { jsonrpc: '2.0', id: id, result: { content: [{ type: 'text', text: out.text }], isError: !!out.error } };
|
|
@@ -4808,23 +5773,34 @@ function handleMessage(msg, ctx) {
|
|
|
4808
5773
|
return { jsonrpc: '2.0', id: id, error: { code: -32601, message: 'Method not found: ' + method } };
|
|
4809
5774
|
}
|
|
4810
5775
|
function cmdServe(opts) {
|
|
4811
|
-
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
|
+
}
|
|
4812
5786
|
const host = opts.host || '0.0.0.0';
|
|
4813
5787
|
const port = opts.port || 8787;
|
|
4814
5788
|
const noAuth = !!opts.noAuth;
|
|
4815
5789
|
const root = userRoot();
|
|
4816
5790
|
ensureInit(root);
|
|
4817
5791
|
function authorize(req) {
|
|
4818
|
-
const agentId = String(req.headers['x-agent-id'] || '');
|
|
5792
|
+
const agentId = String(req.headers['x-agent-id'] || '').trim();
|
|
4819
5793
|
const agentKey = String(req.headers['x-agent-key'] || '').trim();
|
|
4820
|
-
|
|
5794
|
+
const toolProfile = normalizeMcpToolProfile(opts.toolProfile);
|
|
5795
|
+
if (noAuth) return { agent: agentId, agentKey: agentKey, toolProfile: toolProfile, identityMode: 'headers' };
|
|
5796
|
+
if (!agentId || !agentKey) return null;
|
|
4821
5797
|
const auth = req.headers['authorization'] || '';
|
|
4822
5798
|
const m = /^Bearer\s+(.+)$/i.exec(auth);
|
|
4823
5799
|
if (!m) return null;
|
|
4824
5800
|
const token = m[1].trim();
|
|
4825
5801
|
const agent = agentId;
|
|
4826
5802
|
const tokenMap = (loadTokens(root).tokens) || {};
|
|
4827
|
-
if (tokenMap[agent] && tokenMap[agent].token === token) return { agent: agent, agentKey: agentKey };
|
|
5803
|
+
if (tokenMap[agent] && tokenMap[agent].token === token) return { agent: agent, agentKey: agentKey, toolProfile: toolProfile, identityMode: 'headers' };
|
|
4828
5804
|
return null;
|
|
4829
5805
|
}
|
|
4830
5806
|
function originAllowed(req) {
|
|
@@ -4894,6 +5870,7 @@ function cmdServe(opts) {
|
|
|
4894
5870
|
console.log('URL: http://' + host + ':' + port + '/mcp');
|
|
4895
5871
|
console.log('记忆库: ' + root);
|
|
4896
5872
|
console.log('MCP 协议: 2026-07-28(modern,server/discover)/ 2025-11-25(legacy 握手兼容)');
|
|
5873
|
+
console.log('工具分组: ' + normalizeMcpToolProfile(opts.toolProfile));
|
|
4897
5874
|
if (noAuth) console.log('鉴权: 已关闭(--no-auth,仅限可信内网)');
|
|
4898
5875
|
else console.log('鉴权: Bearer token + X-Agent-Id + X-Agent-Key(token 由 token new 生成,agent_key 由 view 授权生成)');
|
|
4899
5876
|
console.log('按 Ctrl+C 停止');
|
|
@@ -4901,10 +5878,33 @@ function cmdServe(opts) {
|
|
|
4901
5878
|
});
|
|
4902
5879
|
}
|
|
4903
5880
|
// ---- stdio 本地零进程模式(客户端按需拉起 CLI)----
|
|
4904
|
-
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
|
+
}
|
|
4905
5900
|
const root = userRoot();
|
|
4906
5901
|
ensureInit(root);
|
|
4907
|
-
const ctx = {
|
|
5902
|
+
const ctx = {
|
|
5903
|
+
agent: ident.id,
|
|
5904
|
+
agentKey: ident.agentKey,
|
|
5905
|
+
toolProfile: normalizeMcpToolProfile(opts && opts.toolProfile),
|
|
5906
|
+
identityMode: 'stdio-args',
|
|
5907
|
+
};
|
|
4908
5908
|
let buf = '';
|
|
4909
5909
|
process.stdin.setEncoding('utf8');
|
|
4910
5910
|
process.stdin.on('data', function (chunk) {
|
|
@@ -4939,7 +5939,7 @@ function lanTaskRunCmd(opts) {
|
|
|
4939
5939
|
const port = opts.port || 8787;
|
|
4940
5940
|
// schtasks /tr 不接受多余内嵌引号:路径无空格不加引号,含空格/引号用 \" 转义(0.5.2 修复)
|
|
4941
5941
|
const q = (p) => /[\s"]/.test(p) ? '\\"' + p.replace(/"/g, '\\"') + '\\"' : p;
|
|
4942
|
-
return q(process.execPath) + ' ' + q(
|
|
5942
|
+
return q(process.execPath) + ' ' + q(runtimeManagedScript()) + ' serve --host ' + host + ' --port ' + port;
|
|
4943
5943
|
}
|
|
4944
5944
|
|
|
4945
5945
|
function lanServeArgs(opts) {
|
|
@@ -4993,7 +5993,7 @@ function lanAutostartCmdContent(opts) {
|
|
|
4993
5993
|
return '@echo off\r\n'
|
|
4994
5994
|
+ 'chcp 65001 >nul\r\n'
|
|
4995
5995
|
+ 'rem ' + LAN_GEN_MARKER + '; remove with: yotta-memory lan disable (English only, keep ASCII)\r\n'
|
|
4996
|
-
+ q(process.execPath) + ' ' + q(
|
|
5996
|
+
+ q(process.execPath) + ' ' + q(runtimeManagedScript()) + ' serve --host ' + host + ' --port ' + port + ' >> ' + q(lanLogPath(opts)) + ' 2>&1\r\n';
|
|
4997
5997
|
}
|
|
4998
5998
|
function lanVbsContent(opts) {
|
|
4999
5999
|
// v0.6.3 自愈:VBS 内联 autostart.cmd 内容,启动时若 .cmd 缺失/被清理即就地重建,
|
|
@@ -5070,7 +6070,7 @@ function lanLinuxUnitPath() { return path.join(lanLinuxSystemdUserDir(), LAN_UNI
|
|
|
5070
6070
|
function lanLinuxSystemctlBin() { return process.env.YOTTA_LAN_SYSTEMCTL_BIN || 'systemctl'; }
|
|
5071
6071
|
function lanLinuxLoginctlBin() { return process.env.YOTTA_LAN_LOGINCTL_BIN || 'loginctl'; }
|
|
5072
6072
|
function lanLinuxExecStart(opts) {
|
|
5073
|
-
return [systemdEscapeArg(process.execPath), systemdEscapeArg(
|
|
6073
|
+
return [systemdEscapeArg(process.execPath), systemdEscapeArg(runtimeManagedScript())].concat(lanServeArgs(opts)).join(' ');
|
|
5074
6074
|
}
|
|
5075
6075
|
function lanLinuxUnitContent(opts) {
|
|
5076
6076
|
// keep comments ASCII (avoid locale/encoding issues); ExecStart uses systemd's own quote rules
|
|
@@ -5093,7 +6093,7 @@ function lanLinuxUnitContent(opts) {
|
|
|
5093
6093
|
}
|
|
5094
6094
|
function lanCrontabBin() { return process.env.YOTTA_LAN_CRONTAB_BIN || 'crontab'; }
|
|
5095
6095
|
function lanCrontabLine(opts) {
|
|
5096
|
-
const parts = ['@reboot', shQuote(process.execPath), shQuote(
|
|
6096
|
+
const parts = ['@reboot', shQuote(process.execPath), shQuote(runtimeManagedScript())].concat(lanServeArgs(opts));
|
|
5097
6097
|
return parts.join(' ') + ' >> ' + shQuote(lanLogPath(opts)) + ' 2>&1 ' + LAN_CRONTAB_MARKER;
|
|
5098
6098
|
}
|
|
5099
6099
|
function lanCrontabRead() {
|
|
@@ -5334,6 +6334,11 @@ function cmdLanWinStatus() {
|
|
|
5334
6334
|
}
|
|
5335
6335
|
|
|
5336
6336
|
function cmdLanEnable(opts) {
|
|
6337
|
+
const runtime = runtimeEnsureForManagedTask();
|
|
6338
|
+
if (runtime.error) {
|
|
6339
|
+
console.error('runtime 稳定入口未就绪,拒绝注册开机自启: ' + runtime.text);
|
|
6340
|
+
process.exit(1);
|
|
6341
|
+
}
|
|
5337
6342
|
const p = lanPlatform();
|
|
5338
6343
|
if (p === 'win32') return cmdLanWinEnable(opts);
|
|
5339
6344
|
if (p === 'linux') return cmdLanLinuxEnable(opts);
|
|
@@ -5855,17 +6860,47 @@ function appendDedupBlock(lines, root, opts) {
|
|
|
5855
6860
|
|
|
5856
6861
|
|
|
5857
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
|
+
}
|
|
5858
6893
|
function usage() {
|
|
5859
6894
|
const banner = 'yotta-memory v' + VERSION + ' — 元忆:有权限边界的文件式智能体记忆';
|
|
5860
6895
|
const sections = [
|
|
5861
6896
|
['核心记忆', [
|
|
5862
6897
|
['init', '初始化记忆库(新建默认加密:需主口令 + 恢复钥匙;--attach 接入已有库;--no-encrypt 降级明文)'],
|
|
5863
|
-
['remember', '写入记忆(--source 来源;--weight 权重;--verify 写后回读校验;--no-hint 关启发)'],
|
|
5864
|
-
['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)'],
|
|
5865
6900
|
['forget', '删除一条记忆'],
|
|
5866
6901
|
['archive', '归档(--days/--threshold 盖棺分+年龄)'],
|
|
5867
6902
|
['backup', '备份记忆库(volumes / setup / status / ensure-daily / schedule / create / list / doctor / restore <id> --to <目录> / drill)'],
|
|
5868
|
-
['doctor', '
|
|
6903
|
+
['doctor', '开工可靠性检查(根目录/密钥库/索引/身份/最近备份;--runtime 加查 CLI/current/MCP/进程/技能副本漂移)'],
|
|
5869
6904
|
['maintain', '记忆自组织(归档/遗忘候选/去重/合并;默认 dry-run;--dedup 查重+置信度,--dedup --apply 自动合并高置信组)'],
|
|
5870
6905
|
['consolidate', '周期摘要压缩(默认 dry-run;--apply 执行;--undo <batch> 回滚;--batches 查批次;候选=超龄+闲置+低效用,immutable/BOUND 豁免)'],
|
|
5871
6906
|
['distill', '心理日志蒸馏(统计摘要/主题画像/知识地图;--model 可选外部模型)'],
|
|
@@ -5886,11 +6921,12 @@ function usage() {
|
|
|
5886
6921
|
['migrate', '把明文私密区迁移为密文(需主口令;迁移后打印恢复钥匙)'],
|
|
5887
6922
|
['view', '启动用户查看平台(--port/--host;口令解锁浏览/授权/吊销 AI)'],
|
|
5888
6923
|
['reset-password', '重设主口令(忘口令用恢复钥匙)'],
|
|
5889
|
-
['key', '管理 agent_key binding(list / bind <id> / rotate <id> / claim <id> --to <AI_HOME> / status <id> / revoke <id>;bind/rotate 需主口令或恢复钥匙)'],
|
|
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 需主口令或恢复钥匙)'],
|
|
5890
6925
|
['config', '查看/设置配置(get;set memory_home <目录> / backup_dir <目录> / embedding_cmd <命令> / embedding_timeout <毫秒> / maintain_* 阈值与半衰 / consolidate_* 参数)']
|
|
5891
6926
|
]],
|
|
5892
6927
|
['平台与服务', [
|
|
5893
|
-
['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)'],
|
|
5894
6930
|
['lan', '开机自启管理(enable/disable/status;Windows 计划任务 / Linux systemd/crontab)'],
|
|
5895
6931
|
['--version', '版本']
|
|
5896
6932
|
]]
|
|
@@ -5905,7 +6941,7 @@ function usage() {
|
|
|
5905
6941
|
lines.push('');
|
|
5906
6942
|
}
|
|
5907
6943
|
lines.push('类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)');
|
|
5908
|
-
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 用请求头');
|
|
5909
6945
|
lines.push('隔离: 公共 FACT 在 facts/;私密 PREF/BOUND/COMMIT 物理分目录 private/<agent_id>/<type>/,禁止 shell 直读写记忆库,一律走本命令');
|
|
5910
6946
|
lines.push('远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id> + X-Agent-Key: <agent_key>');
|
|
5911
6947
|
console.log(lines.join('\n'));
|
|
@@ -5915,7 +6951,7 @@ async function main() {
|
|
|
5915
6951
|
if (!args.length) { usage(); return; }
|
|
5916
6952
|
const opts = {};
|
|
5917
6953
|
const positional = [];
|
|
5918
|
-
const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--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']);
|
|
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']);
|
|
5919
6955
|
for (let i = 0; i < args.length; i++) {
|
|
5920
6956
|
const a = args[i];
|
|
5921
6957
|
if (a === '--version' || a === '-v') { console.log(VERSION); return; }
|
|
@@ -5926,6 +6962,8 @@ async function main() {
|
|
|
5926
6962
|
else if (a === '--no-auth') opts.noAuth = true;
|
|
5927
6963
|
else if (a === '--stdio') opts.stdio = true;
|
|
5928
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;
|
|
5929
6967
|
else if (a === '--force') opts.force = true;
|
|
5930
6968
|
else if (a === '--attach') opts.attach = true;
|
|
5931
6969
|
else if (a === '--allow-same-volume') opts.allowSameVolume = true;
|
|
@@ -5946,6 +6984,7 @@ async function main() {
|
|
|
5946
6984
|
else if (a === '--batches') opts.batches = true;
|
|
5947
6985
|
else if (a === '--explain') opts.explain = true;
|
|
5948
6986
|
else if (a === '--semantic') opts.semantic = true;
|
|
6987
|
+
else if (a === '--runtime') opts.runtime = true;
|
|
5949
6988
|
else if (valueOpts.has(a)) {
|
|
5950
6989
|
const v = args[++i];
|
|
5951
6990
|
if (a === '--type') opts.type = v;
|
|
@@ -5955,6 +6994,7 @@ async function main() {
|
|
|
5955
6994
|
else if (a === '--out') opts.out = v;
|
|
5956
6995
|
else if (a === '--owner') opts.owner = v;
|
|
5957
6996
|
else if (a === '--agent') opts.agent = v;
|
|
6997
|
+
else if (a === '--agent-id') opts.agentId = v;
|
|
5958
6998
|
else if (a === '--agent-key') opts.agentKey = v;
|
|
5959
6999
|
else if (a === '--agent-key-file') opts.agentKeyFile = v;
|
|
5960
7000
|
else if (a === '--scope') opts.scope = v;
|
|
@@ -5985,15 +7025,37 @@ async function main() {
|
|
|
5985
7025
|
else if (a === '--to') opts.to = v;
|
|
5986
7026
|
else if (a === '--id') opts.id = v;
|
|
5987
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);
|
|
5988
7031
|
} else if (a.startsWith('--')) {
|
|
7032
|
+
if (a === '--query') {
|
|
7033
|
+
console.error('未知选项: --query。recall/search 的关键词是位置参数:yotta-memory recall [关键词]');
|
|
7034
|
+
process.exit(2);
|
|
7035
|
+
}
|
|
5989
7036
|
console.error('未知选项: ' + a);
|
|
5990
7037
|
process.exit(2);
|
|
5991
7038
|
} else {
|
|
5992
7039
|
positional.push(a);
|
|
5993
7040
|
}
|
|
5994
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
|
+
}
|
|
5995
7049
|
const first = positional[0];
|
|
5996
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
|
+
}
|
|
5997
7059
|
const runtimeIdentity = resolveIdentity(opts);
|
|
5998
7060
|
setRuntimeAgent(runtimeIdentity.error ? '' : runtimeIdentity.id, runtimeIdentity.error ? '' : runtimeIdentity.agentKey);
|
|
5999
7061
|
switch (first) {
|
|
@@ -6060,6 +7122,16 @@ async function main() {
|
|
|
6060
7122
|
else { console.error('key 子命令: list / bind <id> / rotate <id> / claim <id> --to <AI_HOME> / status <id> / revoke <id>'); process.exit(2); }
|
|
6061
7123
|
break;
|
|
6062
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); }
|
|
7133
|
+
break;
|
|
7134
|
+
}
|
|
6063
7135
|
case 'serve': cmdServe(opts); break;
|
|
6064
7136
|
case 'lan': {
|
|
6065
7137
|
const sub = rest[0];
|
|
@@ -6107,6 +7179,28 @@ module.exports = {
|
|
|
6107
7179
|
collectEntryFiles: collectEntryFiles,
|
|
6108
7180
|
migrateLayout: migrateLayout,
|
|
6109
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,
|
|
6110
7204
|
lanTaskRunCmd: lanTaskRunCmd,
|
|
6111
7205
|
lanStartupDir: lanStartupDir,
|
|
6112
7206
|
lanAutostartDir: lanAutostartDir,
|