@yottameta/yotta-memory 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,7 +25,7 @@ const net = require('net');
25
25
  const child_process = require('child_process');
26
26
  const { AsyncLocalStorage } = require('async_hooks');
27
27
 
28
- const VERSION = '0.20.0';
28
+ const VERSION = '0.21.0';
29
29
  const CLI_VALUE_OPTS = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--plugin-data', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--recovery-key-out', '--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', '--year', '--evalset', '--k', '--seed', '--bootstrap', '--gate', '--against', '--template', '--path', '--from']);
30
30
  const CLI_FLAG_OPTS = new Set(['--project', '--all', '--unsafe', '--no-auth', '--stdio', '--onstart', '--from-current', '--restart', '--force', '--attach', '--allow-same-volume', '--verify', '--no-hint', '--encrypt', '--no-encrypt', '--password-stdin', '--json', '--manual', '--skip-schedule', '--useful', '--useless', '--undo', '--dry-run', '--apply', '--purge', '--dedup', '--batches', '--propose', '--audit', '--capacity', '--rules', '--explain', '--semantic', '--runtime', '--ablate', '--timing', '--baseline', '--probe', '--quarantine', '--restore', '--no-usage', '--yes', '--keep-memories', '--keep-identity']);
31
31
 
@@ -306,7 +306,7 @@ const HELP_MODEL = [
306
306
  ] },
307
307
  ];
308
308
  // @generated view-html:start
309
- 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 &lt;钥匙&gt;</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/&lt;agent_id&gt;.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{'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[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=esc(d.hint||'(无 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><button class=\"danger\" data-d=\"'+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();});};});\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();});};});\n box.querySelectorAll('[data-d]').forEach(function(b){b.onclick=function(){var owner=b.getAttribute('data-d');var typed=prompt('彻底删除 '+owner+' 的身份与私密记忆(不可恢复;公共明文保留,其它 AI 不受影响)。请输入完整 ID 确认:');if(typed===null)return;if(typed!==owner){alert('ID 不匹配,请输入完整 agent ID:'+owner);return;}b.disabled=true;api('/api/identity-remove',{owner:owner,confirm:typed}).then(function(d){b.disabled=false;if(!d||d.error){alert((d&&d.error)||'删除失败');loadOwners();return;}alert(d.text||'已删除');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 新会话先执行 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";
309
+ const VIEW_HTML = "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"robots\" content=\"noindex,nofollow\">\n<title>元忆 · 记忆管理台</title>\n<style>\n :root{--bg:#0f0c12;--bg2:#141019;--panel:#191420;--panel2:#1f1828;--line:rgba(255,217,138,.12);--line2:rgba(255,217,138,.24);\n --text:#f4eee4;--muted:#a79c8e;--faint:#776c60;--amber:#f0a13a;--gold:#ffd98a;--ok:#7fc49a;--danger:#e07070;--shadow:0 10px 30px rgba(0,0,0,.35)}\n body.light{--bg:#f6f1e8;--bg2:#f1eade;--panel:#fffdf8;--panel2:#faf5ec;--line:rgba(56,42,28,.12);--line2:rgba(56,42,28,.24);\n --text:#241c14;--muted:#6f6458;--faint:#96897b;--amber:#c1781c;--gold:#9c6a16;--ok:#3e7d57;--danger:#b23c3c;--shadow:0 8px 24px rgba(56,42,28,.10)}\n *{box-sizing:border-box}[hidden]{display:none!important}\n html,body{margin:0;padding:0}\n body{background:var(--bg);color:var(--text);font:14px/1.6 \"Microsoft YaHei\",\"PingFang SC\",\"Noto Sans CJK SC\",system-ui,sans-serif;letter-spacing:0}\n button{font:inherit;color:inherit;cursor:pointer}\n input,select,textarea{font:inherit;color:var(--text);background:var(--panel2);border:1px solid var(--line2);border-radius:8px;padding:9px 12px;outline:0;width:100%}\n input:focus,select:focus,textarea:focus{border-color:var(--amber)}\n textarea{min-height:96px;resize:vertical}\n .muted{color:var(--muted)}.faint{color:var(--faint);font-size:12px}\n .sv{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;flex:none}\n\n /* lock */\n .lock-wrap{min-height:100vh;display:grid;place-items:center;padding:24px}\n .lock-card{width:min(420px,100%);background:var(--panel);border:1px solid var(--line2);border-radius:12px;padding:26px;box-shadow:var(--shadow)}\n .lock-card h1{margin:14px 0 6px;font-size:22px}\n .lock-card .field{margin-top:16px}\n .err{color:var(--danger);margin-top:10px;font-size:13px;min-height:20px}\n\n /* shell */\n .shell{display:grid;grid-template-columns:236px minmax(0,1fr);min-height:100vh}\n .sidebar{background:var(--bg2);border-right:1px solid var(--line);padding:22px 16px;display:flex;flex-direction:column;gap:22px;position:sticky;top:0;height:100vh;min-width:0}\n .brand{display:flex;align-items:center;gap:10px;padding:0 6px}\n .brand-mark{width:34px;height:34px;border-radius:8px;background:rgba(240,161,58,.14);border:1px solid var(--line2);display:grid;place-items:center;color:var(--gold)}\n .brand-name{font-size:16px;font-weight:600}.brand-sub{font-size:11px;color:var(--faint)}\n .nav{display:flex;flex-direction:column;gap:4px;min-width:0}\n .nav button{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--muted);text-align:left}\n .nav button:hover{background:var(--panel);color:var(--text)}\n .nav button.active{background:var(--panel);color:var(--gold);border-color:var(--line)}\n .side-foot{margin-top:auto;display:flex;flex-direction:column;gap:10px;min-width:0}\n .privacy{display:flex;gap:8px;align-items:flex-start;padding:10px;border:1px solid var(--line);border-radius:8px;color:var(--muted);font-size:12px;background:var(--panel)}\n .privacy .sv{color:var(--ok);margin-top:2px}\n .ghost-btn{display:flex;align-items:center;justify-content:center;gap:8px;border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:6px;padding:8px}\n .ghost-btn:hover{color:var(--text);border-color:var(--line2)}\n\n .main{min-width:0;padding:26px 30px 60px}\n .topbar{display:flex;align-items:center;gap:14px;margin-bottom:22px;flex-wrap:wrap}\n .page-title{font-size:20px;font-weight:600;margin:0}.page-sub{color:var(--faint);font-size:12px;margin-top:2px}\n .spacer{flex:1}\n .search{display:flex;align-items:center;gap:8px;min-width:240px;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:8px 12px;color:var(--muted)}\n .search input{background:transparent;border:0;padding:0;width:100%}\n .chip-user{display:flex;align-items:center;gap:8px;padding:6px 10px;border:1px solid var(--line);border-radius:999px;color:var(--muted);font-size:12px}\n .dot{width:8px;height:8px;border-radius:50%;background:var(--amber)}\n\n .view{display:none}.view.active{display:block}\n .grid{display:grid;gap:14px}.g4{grid-template-columns:repeat(4,minmax(0,1fr))}.g2{grid-template-columns:repeat(2,minmax(0,1fr))}\n .card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:16px;min-width:0}\n .card h3{margin:0 0 4px;font-size:14px}\n .label{color:var(--muted);font-size:12px}.value{font-size:26px;font-weight:600;margin:8px 0 2px}.delta{font-size:12px;color:var(--ok)}\n .section{margin-top:20px}\n .shead{display:flex;align-items:baseline;gap:10px;margin-bottom:10px}.shead h2{font-size:15px;margin:0}\n .bars{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}\n .bar{border:1px solid var(--line);border-radius:8px;padding:12px;background:var(--panel2)}\n .bar b{font-size:18px;display:block;margin-top:6px}\n .bar .dot{display:inline-block;margin-right:6px}\n\n .toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap}\n .chips{display:flex;gap:8px;flex-wrap:wrap}\n .chip{border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:999px;padding:5px 12px;font-size:12px}\n .chip.active{color:var(--gold);border-color:var(--line2);background:rgba(240,161,58,.10)}\n .btn{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;border-radius:6px;border:1px solid var(--line2);background:transparent;color:var(--text)}\n .btn:hover{border-color:var(--gold)}\n .btn.primary{background:var(--amber);border-color:var(--amber);color:#1b1206;font-weight:600}\n .btn.danger{border-color:rgba(224,112,112,.5);color:var(--danger)}\n .btn.small{padding:5px 10px;font-size:12px}\n .btn[disabled]{opacity:.5;cursor:not-allowed}\n\n .list{display:flex;flex-direction:column;gap:8px;margin-top:14px}\n .row{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid var(--line);border-radius:8px;background:var(--panel2)}\n .row:hover{border-color:var(--line2)}\n .row-main{flex:1;min-width:0;cursor:pointer}\n .row-title{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .row-snippet{color:var(--muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .row-meta{color:var(--faint);font-size:11px;margin-top:2px}\n .row-actions{display:flex;gap:6px}\n .ibtn{width:30px;height:30px;display:grid;place-items:center;border:1px solid var(--line);background:transparent;border-radius:6px;color:var(--muted)}\n .ibtn:hover{color:var(--text);border-color:var(--line2)}\n .badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--line2);color:var(--gold);background:rgba(240,161,58,.10);white-space:nowrap}\n .badge.pref{color:#b79cdd;border-color:rgba(183,156,221,.4);background:rgba(183,156,221,.10)}\n .badge.bound{color:#8fb4de;border-color:rgba(143,180,222,.4);background:rgba(143,180,222,.10)}\n .badge.commit{color:var(--ok);border-color:rgba(127,196,154,.4);background:rgba(127,196,154,.10)}\n .badge.off{color:var(--faint);border-color:var(--line);background:transparent}\n .empty{color:var(--faint);text-align:center;padding:26px;border:1px dashed var(--line);border-radius:8px;margin-top:14px}\n .pager{display:flex;align-items:center;gap:10px;margin-top:12px;color:var(--faint);font-size:12px}\n\n table{width:100%;border-collapse:collapse}\n th,td{text-align:left;padding:12px 10px;border-bottom:1px solid var(--line);font-size:13px}\n th{color:var(--faint);font-weight:500;font-size:12px}\n .owner-name{font-weight:600}\n\n .field{margin-bottom:12px}\n .field label{display:block;color:var(--muted);font-size:12px;margin-bottom:6px}\n .row2{display:grid;grid-template-columns:1fr 1fr;gap:12px}\n\n .mask{position:fixed;inset:0;background:rgba(0,0,0,.5);display:none;place-items:center;z-index:50;padding:20px}\n .mask.open{display:grid}\n .modal{width:min(560px,100%);max-height:86vh;overflow:auto;background:var(--panel);border:1px solid var(--line2);border-radius:10px;padding:18px;box-shadow:var(--shadow)}\n .modal h3{margin:0 0 10px;font-size:16px}\n .modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}\n .drawer{position:fixed;top:0;right:0;bottom:0;width:min(440px,92vw);background:var(--panel);border-left:1px solid var(--line2);padding:20px;overflow:auto;transform:translateX(100%);transition:transform .2s;z-index:45}\n .drawer.open{transform:none}\n .kv{display:grid;grid-template-columns:86px 1fr;gap:6px 10px;font-size:13px;margin:14px 0}\n .kv dt{color:var(--faint)}.kv dd{margin:0;word-break:break-all}\n .quote{border-left:2px solid var(--line2);padding-left:12px;color:var(--muted);white-space:pre-wrap;word-break:break-word}\n .keybox{width:100%;min-height:64px;font-family:ui-monospace,Consolas,monospace;font-size:12px}\n\n .toasts{position:fixed;left:50%;bottom:26px;transform:translateX(-50%);display:flex;flex-direction:column;gap:8px;align-items:center;z-index:60}\n .toast{display:flex;align-items:center;gap:10px;background:var(--panel2);border:1px solid var(--line2);border-radius:999px;padding:9px 16px;box-shadow:var(--shadow);font-size:13px}\n .toast button{border:0;background:transparent;color:var(--gold);font-weight:600}\n .list-plain{display:flex;flex-direction:column;gap:8px;max-height:46vh;overflow:auto}\n .list-plain .item{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:8px;padding:9px 10px;font-size:12px;color:var(--muted)}\n .list-plain .item .spacer{flex:1}\n\n @media (max-width:960px){.g4{grid-template-columns:repeat(2,minmax(0,1fr))}.bars{grid-template-columns:repeat(2,minmax(0,1fr))}}\n @media (max-width:760px){\n .shell{grid-template-columns:minmax(0,1fr)}\n .sidebar{position:static;height:auto;padding:14px;gap:12px}\n .nav{flex-direction:row;overflow-x:auto;width:100%}\n .nav button{white-space:nowrap}\n .side-foot{margin-top:0;flex-direction:row}\n .privacy{flex:1}\n .main{padding:18px 16px 48px}\n .search{min-width:100%;order:3}\n .g4{grid-template-columns:repeat(2,minmax(0,1fr))}\n table{display:block;overflow-x:auto;white-space:nowrap}\n .row{flex-wrap:wrap}.row-actions{width:100%;justify-content:flex-end}\n }\n</style>\n</head>\n<body>\n<div id=\"lock\" class=\"lock-wrap\">\n <div class=\"lock-card\">\n <div class=\"brand\">\n <div class=\"brand-mark\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><ellipse cx=\"12\" cy=\"5\" rx=\"8\" ry=\"3\"/><path d=\"M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5\"/><path d=\"M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3\"/></svg></div>\n <div><div class=\"brand-name\">元忆 yotta-memory</div><div class=\"brand-sub\">你的 AI 记忆,由你掌控</div></div>\n </div>\n <h1>解锁记忆管理台 <span id=\"ver\" class=\"faint\"></span></h1>\n <div class=\"field\"><label>主口令(只在本地内存派生,不落盘、不发远端)</label><input type=\"password\" id=\"pw\" placeholder=\"主口令\"></div>\n <button class=\"btn primary\" id=\"unlockBtn\" style=\"width:100%\">解锁</button>\n <div class=\"err\" id=\"lockErr\"></div>\n <div class=\"faint\" style=\"margin-top:8px\">忘记口令可用恢复钥匙在 CLI 重设:<code>yotta-memory reset-password --recovery-key &lt;钥匙&gt;</code></div>\n </div>\n</div>\n\n<div id=\"app\" class=\"shell\" hidden>\n <aside class=\"sidebar\">\n <div class=\"brand\">\n <div class=\"brand-mark\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><ellipse cx=\"12\" cy=\"5\" rx=\"8\" ry=\"3\"/><path d=\"M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5\"/><path d=\"M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3\"/></svg></div>\n <div><div class=\"brand-name\">元忆</div><div class=\"brand-sub\">你的 AI 记忆,由你掌控</div></div>\n </div>\n <nav class=\"nav\" id=\"nav\">\n <button data-view=\"overview\" class=\"active\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M3 10.5 12 3l9 7.5\"/><path d=\"M5 9.5V21h14V9.5\"/></svg>概览</button>\n <button data-view=\"memories\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M4 19.5A2.5 2.5 0 0 1 6.5 17H20\"/><path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z\"/></svg>记忆</button>\n <button data-view=\"access\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/><path d=\"M22 21v-2a4 4 0 0 0-3-3.87\"/></svg>AI 与权限</button>\n <button data-view=\"settings\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M4 21v-7\"/><path d=\"M4 10V3\"/><path d=\"M12 21v-9\"/><path d=\"M12 8V3\"/><path d=\"M20 21v-5\"/><path d=\"M20 12V3\"/><path d=\"M1 14h6\"/><path d=\"M9 8h6\"/><path d=\"M17 16h6\"/></svg>设置</button>\n </nav>\n <div class=\"side-foot\">\n <div class=\"privacy\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/><path d=\"m9 12 2 2 4-4\"/></svg><span>数据不出本机<br>私密区按 AI 隔离加密</span></div>\n <button class=\"ghost-btn\" id=\"themeBtn\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\"/></svg>切换主题</button>\n </div>\n </aside>\n\n <main class=\"main\">\n <div class=\"topbar\">\n <div><h1 class=\"page-title\" id=\"pageTitle\">概览</h1><div class=\"page-sub\" id=\"pageSub\">你的 AI 记住了什么,一眼看清</div></div>\n <div class=\"spacer\"></div>\n <label class=\"search\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-3.5-3.5\"/></svg><input id=\"globalSearch\" placeholder=\"搜索记忆 / 关键词\"></label>\n <div class=\"chip-user\"><span class=\"dot\"></span>本机 · 用户</div>\n </div>\n\n <section class=\"view active\" id=\"view-overview\">\n <div class=\"grid g4\">\n <div class=\"card\"><div class=\"label\">记忆总数</div><div class=\"value\" id=\"ovTotal\">-</div><div class=\"delta\" id=\"ovFiles\">-</div></div>\n <div class=\"card\"><div class=\"label\">私密记忆</div><div class=\"value\" id=\"ovPrivate\">-</div><div class=\"delta\">仅对应 AI 与本人可见</div></div>\n <div class=\"card\"><div class=\"label\">AI 智能体</div><div class=\"value\" id=\"ovOwners\">-</div><div class=\"delta\" id=\"ovAuth\">-</div></div>\n <div class=\"card\"><div class=\"label\">已藏起来(归档)</div><div class=\"value\" id=\"ovArchived\">-</div><div class=\"delta\">可随时放回</div></div>\n </div>\n <div class=\"section\">\n <div class=\"shead\"><h2>四类记忆分布</h2><span class=\"faint\">FACT 共享 · PREF / BOUND / COMMIT 私密</span></div>\n <div class=\"bars\" id=\"typeBars\"></div>\n </div>\n <div class=\"section grid g2\">\n <div class=\"card\">\n <h3>最近更新</h3>\n <div class=\"list\" id=\"recentList\"></div>\n </div>\n <div class=\"card\">\n <h3>备份与安全</h3>\n <div class=\"kv\" style=\"margin-top:10px\">\n <dt>备份目录</dt><dd id=\"ovBackup\">-</dd>\n <dt>存储占用</dt><dd id=\"ovBytes\">-</dd>\n <dt>私密加密</dt><dd>已启用(AES-256-GCM 信封加密)</dd>\n </div>\n <div style=\"display:flex;gap:8px;flex-wrap:wrap\">\n <button class=\"btn small\" id=\"quickArchived\">查看已藏</button>\n <button class=\"btn small\" id=\"quickTrash\">最近删除</button>\n <button class=\"btn small primary\" data-goto=\"memories\">管理记忆</button>\n </div>\n </div>\n </div>\n </section>\n\n <section class=\"view\" id=\"view-memories\">\n <div class=\"card toolbar\">\n <label class=\"search\" style=\"min-width:280px;flex:1\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-3.5-3.5\"/></svg><input id=\"memSearch\" placeholder=\"搜索记忆(标题 / 内容 / 标签)\"></label>\n <div class=\"chips\" id=\"typeChips\">\n <button class=\"chip active\" data-type=\"all\">全部</button>\n <button class=\"chip\" data-type=\"FACT\">FACT</button>\n <button class=\"chip\" data-type=\"PREF\">PREF</button>\n <button class=\"chip\" data-type=\"BOUND\">BOUND</button>\n <button class=\"chip\" data-type=\"COMMIT\">COMMIT</button>\n </div>\n <div class=\"spacer\"></div>\n <button class=\"btn\" id=\"btnArchived\">已藏</button>\n <button class=\"btn\" id=\"btnTrash\">最近删除</button>\n <button class=\"btn primary\" id=\"btnNew\">新建记忆</button>\n </div>\n <div class=\"list\" id=\"memList\"></div>\n <div class=\"empty\" id=\"memEmpty\" hidden>没有匹配的记忆。</div>\n <div class=\"pager\"><button class=\"btn small\" id=\"prevPage\">上一页</button><span id=\"pageInfo\">-</span><button class=\"btn small\" id=\"nextPage\">下一页</button></div>\n </section>\n\n <section class=\"view\" id=\"view-access\">\n <div class=\"card\">\n <h3>谁看得见什么</h3>\n <div class=\"faint\" style=\"margin-bottom:10px\">默认拒绝:每个 AI 只能读自己的私密区;公共 FACT 所有已授权 AI 可见。授权会生成只显示一次的 agent_key,请立即单独保存。</div>\n <table><thead><tr><th>AI 智能体</th><th>FACT 公共</th><th>私密记忆</th><th>状态</th><th></th></tr></thead><tbody id=\"ownerRows\"></tbody></table>\n </div>\n <div class=\"section card\">\n <h3>恢复与备份</h3>\n <div class=\"kv\"><dt>备份目录</dt><dd id=\"accBackup\">-</dd><dt>恢复钥匙</dt><dd>离线保存;泄露等同口令泄露</dd></div>\n <div style=\"display:flex;gap:8px;flex-wrap:wrap\"><button class=\"btn small\" id=\"btnRk\">显示恢复钥匙</button><button class=\"btn small\" id=\"btnExport\">导出全部 JSON</button></div>\n </div>\n </section>\n\n <section class=\"view\" id=\"view-settings\">\n <div class=\"card\">\n <h3>重设主口令</h3>\n <div class=\"row2\" style=\"margin-top:10px\">\n <div class=\"field\"><label>当前口令</label><input type=\"password\" id=\"pwCur\"></div>\n <div class=\"field\"><label>新口令</label><input type=\"password\" id=\"pwNew\"></div>\n <div class=\"field\"><label>确认新口令</label><input type=\"password\" id=\"pwNew2\"></div>\n </div>\n <button class=\"btn primary\" id=\"btnResetPw\">重设口令</button>\n <div class=\"err\" id=\"pwOut\"></div>\n </div>\n <div class=\"section card\">\n <h3>危险操作</h3>\n <div class=\"faint\">删除某个 AI 的身份与其私密记忆(公共 FACT 保留,其它 AI 不受影响)。此操作不可恢复,请谨慎使用。</div>\n <div style=\"margin-top:10px\"><button class=\"btn danger small\" id=\"btnDangerHint\">在哪里执行?</button></div>\n </div>\n </section>\n </main>\n</div>\n\n<div class=\"mask\" id=\"modalMask\"><div class=\"modal\"><h3 id=\"modalTitle\"></h3><div id=\"modalBody\"></div><div class=\"modal-actions\"><button class=\"btn\" id=\"modalCancel\">取消</button><button class=\"btn primary\" id=\"modalOk\">确认</button></div></div></div>\n<div class=\"mask\" id=\"drawerMask\"></div>\n<aside class=\"drawer\" id=\"drawer\">\n <div style=\"display:flex;gap:10px;align-items:flex-start\"><div style=\"flex:1\"><h3 id=\"drTitle\"></h3><div class=\"faint\" id=\"drMeta\"></div></div><button class=\"ibtn\" id=\"drClose\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/></svg></button></div>\n <div class=\"quote\" id=\"drBody\" style=\"margin-top:12px\"></div>\n <dl class=\"kv\" id=\"drKv\"></dl>\n <div style=\"display:flex;gap:8px\"><button class=\"btn\" id=\"drEdit\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z\"/></svg>编辑</button><button class=\"btn danger\" id=\"drDelete\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>删除</button></div>\n</aside>\n<div class=\"toasts\" id=\"toasts\"></div>\n\n<script>\n(function () {\n 'use strict';\n var $ = function (s) { return document.querySelector(s); };\n var state = { view: 'overview', entries: [], count: 0, offset: 0, limit: 20, query: '', type: 'all', owners: [], overview: null, current: null };\n var typeNames = { FACT: 'FACT 事实', PREF: 'PREF 偏好', BOUND: 'BOUND 边界', COMMIT: 'COMMIT 承诺' };\n\n function esc(s) {\n return String(s == null ? '' : s).replace(/[&<>\"']/g, function (c) {\n return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c];\n });\n }\n async function api(pathname, body) {\n try {\n var res = await fetch(pathname, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });\n var text = await res.text();\n var data;\n try { data = JSON.parse(text); } catch (e) { data = { error: text }; }\n if (res.status === 401) showLock();\n return data;\n } catch (e) { return { error: String(e) }; }\n }\n function fmtBytes(n) {\n n = Number(n) || 0;\n if (n < 1024) return n + ' B';\n if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';\n return (n / 1024 / 1024).toFixed(2) + ' MB';\n }\n function toast(msg, actionLabel, fn) {\n var el = document.createElement('div');\n el.className = 'toast';\n el.appendChild(document.createTextNode(msg));\n if (actionLabel && fn) {\n var b = document.createElement('button');\n b.textContent = actionLabel;\n b.addEventListener('click', function () { el.remove(); fn(); });\n el.appendChild(b);\n }\n $('#toasts').appendChild(el);\n setTimeout(function () { el.remove(); }, actionLabel ? 9000 : 3600);\n }\n function showLock() { $('#lock').hidden = false; $('#app').hidden = true; }\n function showApp() { $('#lock').hidden = true; $('#app').hidden = false; refreshAll(); }\n\n /* modal / drawer */\n var modalOkFn = null;\n function openModal(title, bodyHtml, opts) {\n opts = opts || {};\n $('#modalTitle').textContent = title;\n $('#modalBody').innerHTML = bodyHtml;\n $('#modalOk').textContent = opts.okText || '确认';\n $('#modalOk').className = 'btn ' + (opts.danger ? 'danger' : 'primary');\n $('#modalOk').hidden = !opts.onOk;\n modalOkFn = opts.onOk || null;\n $('#modalMask').classList.add('open');\n }\n function closeModal() { $('#modalMask').classList.remove('open'); modalOkFn = null; }\n function confirmModal(title, text, okText, danger, onOk) {\n openModal(title, '<p class=\"muted\">' + esc(text) + '</p>', { okText: okText, danger: danger, onOk: onOk });\n }\n $('#modalCancel').addEventListener('click', closeModal);\n $('#modalMask').addEventListener('click', function (e) { if (e.target === $('#modalMask')) closeModal(); });\n $('#modalOk').addEventListener('click', function () { if (modalOkFn) modalOkFn(); });\n function openDrawer() { $('#drawer').classList.add('open'); $('#drawerMask').classList.add('open'); }\n function closeDrawer() { $('#drawer').classList.remove('open'); $('#drawerMask').classList.remove('open'); }\n $('#drClose').addEventListener('click', closeDrawer);\n $('#drawerMask').addEventListener('click', closeDrawer);\n\n /* nav */\n var titles = { overview: ['概览', '你的 AI 记住了什么,一眼看清'], memories: ['记忆', '看 / 改 / 删 / 藏,全在这里'], access: ['AI 与权限', '默认拒绝,按 AI 隔离'], settings: ['设置', '口令 / 备份 / 导出'] };\n function switchView(name) {\n state.view = name;\n Array.prototype.forEach.call(document.querySelectorAll('#nav button'), function (b) { b.classList.toggle('active', b.dataset.view === name); });\n Array.prototype.forEach.call(document.querySelectorAll('.view'), function (v) { v.classList.toggle('active', v.id === 'view-' + name); });\n $('#pageTitle').textContent = titles[name][0];\n $('#pageSub').textContent = titles[name][1];\n window.scrollTo(0, 0);\n }\n $('#nav').addEventListener('click', function (e) { var b = e.target.closest('button'); if (b) switchView(b.dataset.view); });\n Array.prototype.forEach.call(document.querySelectorAll('[data-goto]'), function (b) { b.addEventListener('click', function () { switchView(b.dataset.goto); }); });\n $('#themeBtn').addEventListener('click', function () { document.body.classList.toggle('light'); });\n\n /* data loaders */\n async function loadOverview() {\n var d = await api('/api/overview');\n if (d.error) return;\n state.overview = d;\n var priv = d.total - (d.byType.FACT || 0);\n $('#ovTotal').textContent = d.total;\n $('#ovFiles').textContent = d.files + ' 个文件';\n $('#ovPrivate').textContent = priv;\n $('#ovOwners').textContent = d.owners.length;\n $('#ovAuth').textContent = d.authorized + ' 已授权';\n $('#ovArchived').textContent = d.archived;\n $('#ovBackup').textContent = d.backupDir || '未配置(CLI: yotta-memory backup setup)';\n $('#ovBytes').textContent = fmtBytes(d.bytes);\n $('#accBackup').textContent = d.backupDir || '未配置';\n var colors = { FACT: 'var(--gold)', PREF: '#b79cdd', BOUND: '#8fb4de', COMMIT: 'var(--ok)' };\n $('#typeBars').innerHTML = ['FACT', 'PREF', 'BOUND', 'COMMIT'].map(function (t) {\n var n = d.byType[t] || 0;\n return '<div class=\"bar\"><span class=\"dot\" style=\"background:' + colors[t] + '\"></span><span class=\"faint\">' + typeNames[t] + '</span><b>' + n + '</b></div>';\n }).join('');\n }\n async function loadOwners() {\n var d = await api('/api/owners');\n state.owners = (d && d.owners) || [];\n var box = $('#ownerRows');\n if (!state.owners.length) { box.innerHTML = '<tr><td colspan=\"5\" class=\"faint\">' + esc((d && d.hint) || '当前没有 owner,请先在 CLI 执行 yotta-memory iam <id>。') + '</td></tr>'; return; }\n box.innerHTML = state.owners.map(function (o) {\n return '<tr><td><span class=\"owner-name\">' + esc(o.owner) + '</span><div class=\"faint\">' + (o.registered ? '已登记' : '未登记') + '</div></td>' +\n '<td><span class=\"badge commit\">可见</span></td>' +\n '<td>' + (o.authorized ? '<span class=\"badge commit\">已授权</span>' : '<span class=\"badge off\">仅自己</span>') + '</td>' +\n '<td>' + (o.authorized ? '可读自己的私密区' : '未绑定 agent_key') + '</td>' +\n '<td><button class=\"btn small\" data-auth=\"' + esc(o.owner) + '\">' + (o.authorized ? '吊销' : '授权') + '</button> <button class=\"btn small danger\" data-remove=\"' + esc(o.owner) + '\">删除</button></td></tr>';\n }).join('');\n }\n async function loadEntries() {\n var d = await api('/api/entries', { query: state.query, offset: state.offset, limit: state.limit, type: state.type === 'all' ? '' : state.type });\n if (d.error) { toast(d.error); return; }\n state.entries = d.entries || [];\n state.count = d.count || 0;\n var totalPg = Math.max(1, Math.ceil(state.count / state.limit));\n var curPg = Math.floor(state.offset / state.limit) + 1;\n $('#pageInfo').textContent = '共 ' + state.count + ' 条 · 第 ' + curPg + ' / ' + totalPg + ' 页';\n $('#prevPage').disabled = state.offset <= 0;\n $('#nextPage').disabled = !d.hasMore;\n renderEntries();\n }\n function renderEntries() {\n var box = $('#memList');\n $('#memEmpty').hidden = state.entries.length > 0;\n box.innerHTML = state.entries.map(function (e) {\n var t = String(e.type || 'FACT').toUpperCase();\n return '<div class=\"row\" data-file=\"' + esc(e.file) + '\"><div class=\"row-main\"><span class=\"badge ' + t.toLowerCase() + '\">' + typeNames[t] + '</span>' +\n '<div class=\"row-title\">' + esc(e.subject) + '</div><div class=\"row-snippet\">' + esc(e.statement) + '</div>' +\n '<div class=\"row-meta\">' + esc(e.owner || '公共') + ' · ' + esc(e.updated || e.created || '') + ' · ' + esc(e.file) + '</div></div>' +\n '<div class=\"row-actions\"><button class=\"ibtn\" data-act=\"edit\" title=\"编辑\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z\"/></svg></button>' +\n '<button class=\"ibtn\" data-act=\"archive\" title=\"藏起来(归档)\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M9.9 4.24A9.1 9.1 0 0 1 12 4c7 0 10 8 10 8a17.5 17.5 0 0 1-2.16 3.19\"/><path d=\"M6.6 6.6A16.8 16.8 0 0 0 2 12s3 8 10 8a9.6 9.6 0 0 0 5.4-1.6\"/><path d=\"m2 2 20 20\"/></svg></button>' +\n '<button class=\"ibtn\" data-act=\"delete\" title=\"删除\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg></button></div></div>';\n }).join('');\n }\n async function refreshAll() {\n await loadOverview();\n await loadOwners();\n await loadEntries();\n var recent = state.entries.slice(0, 4).map(function (e) {\n var t = String(e.type || 'FACT').toUpperCase();\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(e.subject) + '</div><div class=\"row-meta\">' + esc(e.owner || '公共') + ' · ' + esc(e.updated || e.created || '') + '</div></div><span class=\"badge ' + t.toLowerCase() + '\">' + typeNames[t] + '</span></div>';\n }).join('');\n $('#recentList').innerHTML = recent || '<div class=\"faint\">还没有记忆。</div>';\n }\n\n /* entries actions */\n $('#memList').addEventListener('click', async function (e) {\n var row = e.target.closest('.row');\n if (!row) return;\n var file = row.dataset.file;\n var entry = state.entries.filter(function (x) { return x.file === file; })[0];\n var act = e.target.closest('button[data-act]');\n if (!act) { openDetail(entry); return; }\n if (act.dataset.act === 'edit') openEditor(entry);\n else if (act.dataset.act === 'delete') doDelete(entry);\n else if (act.dataset.act === 'archive') doArchive(entry);\n });\n function openDetail(entry) {\n if (!entry) return;\n state.current = entry;\n var t = String(entry.type || 'FACT').toUpperCase();\n $('#drTitle').textContent = entry.subject || '(无标题)';\n $('#drMeta').textContent = typeNames[t] + ' · ' + (entry.owner || '公共') + ' · ' + (entry.updated || entry.created || '');\n $('#drBody').textContent = entry.statement || '';\n $('#drKv').innerHTML = '<dt>文件</dt><dd>' + esc(entry.file) + '</dd><dt>可见性</dt><dd>' + (t === 'FACT' ? '所有已授权 AI' : '仅 ' + esc(entry.owner) + '(私密)') + '</dd><dt>标签</dt><dd>' + esc((entry.tags || []).join(', ') || '-') + '</dd>';\n openDrawer();\n }\n $('#drEdit').addEventListener('click', function () { if (state.current) { closeDrawer(); openEditor(state.current); } });\n $('#drDelete').addEventListener('click', function () { if (state.current) { closeDrawer(); doDelete(state.current); } });\n\n function editorHtml(entry, isNew) {\n var t = entry ? String(entry.type || 'FACT').toUpperCase() : 'FACT';\n return '<div class=\"field\"><label>类型</label><select id=\"fType\"' + (isNew ? '' : ' disabled') + '>' +\n ['FACT', 'PREF', 'BOUND', 'COMMIT'].map(function (x) { return '<option value=\"' + x + '\"' + (x === t ? ' selected' : '') + '>' + typeNames[x] + '</option>'; }).join('') + '</select></div>' +\n '<div class=\"field\" id=\"fOwnerWrap\"><label>归属 AI(私密记忆必填)</label><input id=\"fOwner\" list=\"ownerList\" placeholder=\"如 codex\" value=\"' + esc(entry && entry.owner || '') + '\"><datalist id=\"ownerList\">' + state.owners.map(function (o) { return '<option value=\"' + esc(o.owner) + '\">'; }).join('') + '</datalist></div>' +\n '<div class=\"field\"><label>标题 subject</label><input id=\"fSubject\" value=\"' + esc(entry && entry.subject || '') + '\"></div>' +\n '<div class=\"field\"><label>内容 statement</label><textarea id=\"fStatement\">' + esc(entry && entry.statement || '') + '</textarea></div>' +\n '<div class=\"field\"><label>标签(逗号分隔)</label><input id=\"fTags\" value=\"' + esc(entry && (entry.tags || []).join(', ') || '') + '\"></div>' +\n (isNew ? '' : '<div class=\"faint\">类型与归属不可修改;如需变更请删除后重写。</div>');\n }\n function syncOwnerField() {\n var t = $('#fType') ? $('#fType').value : 'FACT';\n var wrap = $('#fOwnerWrap');\n if (wrap) wrap.hidden = (t === 'FACT');\n }\n function openEditor(entry) {\n openModal('编辑记忆', editorHtml(entry, false), {\n okText: '保存', onOk: async function () {\n var subject = $('#fSubject').value.trim();\n var statement = $('#fStatement').value.trim();\n var tags = $('#fTags').value.split(',').map(function (x) { return x.trim(); }).filter(Boolean);\n if (!subject || !statement) { toast('标题与内容不能为空'); return; }\n var r = await api('/api/memory/update', { file: entry.file, subject: subject, statement: statement, tags: tags });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已更新 1 条记忆', '撤销', async function () {\n var u = await api('/api/memory/restore', { ref: r.undo });\n toast(u.error || '已撤销这次编辑');\n refreshAll();\n });\n refreshAll();\n },\n });\n }\n $('#btnNew').addEventListener('click', function () {\n openModal('新建记忆', editorHtml(null, true), {\n okText: '创建', onOk: async function () {\n var type = $('#fType').value;\n var owner = $('#fOwner') ? $('#fOwner').value.trim() : '';\n var subject = $('#fSubject').value.trim();\n var statement = $('#fStatement').value.trim();\n var tags = $('#fTags').value.split(',').map(function (x) { return x.trim(); }).filter(Boolean);\n if (!subject || !statement) { toast('标题与内容不能为空'); return; }\n var r = await api('/api/memory/create', { type: type, owner: owner, subject: subject, statement: statement, tags: tags });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已创建 1 条记忆');\n refreshAll();\n },\n });\n syncOwnerField();\n $('#fType').addEventListener('change', syncOwnerField);\n });\n function doDelete(entry) {\n confirmModal('删除这条记忆?', '将移入回收区(不物理删除),可从「最近删除」恢复。', '删除', true, async function () {\n var r = await api('/api/memory/delete', { file: entry.file });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已删除 1 条记忆', '撤销', async function () {\n var u = await api('/api/memory/restore', { ref: r.trash });\n toast(u.error || '已恢复');\n refreshAll();\n });\n refreshAll();\n });\n }\n function doArchive(entry) {\n confirmModal('藏起来(归档)?', '归档后不参与召回,但数据保留,可随时放回。', '藏起来', false, async function () {\n var r = await api('/api/memory/archive', { file: entry.file });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已藏起来 1 条', '放回', async function () {\n var u = await api('/api/memory/unarchive', { archive: r.archive });\n toast(u.error || '已放回');\n refreshAll();\n });\n refreshAll();\n });\n }\n\n async function openArchived() {\n var d = await api('/api/archived');\n var items = (d.entries || []).map(function (e) {\n return '<div class=\"item\"><span>' + esc(e.original) + '</span><span class=\"spacer\"></span><button class=\"btn small\" data-unarch=\"' + esc(e.ref) + '\">放回</button></div>';\n }).join('');\n openModal('已藏起来(归档)', items ? '<div class=\"list-plain\">' + items + '</div>' : '<p class=\"faint\">没有归档的记忆。</p>', {});\n $('#modalBody').addEventListener('click', async function (e) {\n var b = e.target.closest('button[data-unarch]');\n if (!b) return;\n var r = await api('/api/memory/unarchive', { archive: b.dataset.unarch });\n toast(r.error || '已放回');\n closeModal();\n refreshAll();\n }, { once: true });\n }\n async function openTrash() {\n var d = await api('/api/trash');\n var items = (d.entries || []).filter(function (e) { return e.kind === 'trash'; }).map(function (e) {\n return '<div class=\"item\"><span>' + esc(e.original) + '</span><span class=\"spacer\"></span><button class=\"btn small\" data-restore=\"' + esc(e.ref) + '\">恢复</button></div>';\n }).join('');\n openModal('最近删除', items ? '<div class=\"list-plain\">' + items + '</div>' : '<p class=\"faint\">回收区是空的。</p>', {});\n $('#modalBody').addEventListener('click', async function (e) {\n var b = e.target.closest('button[data-restore]');\n if (!b) return;\n var r = await api('/api/memory/restore', { ref: b.dataset.restore });\n toast(r.error || '已恢复');\n closeModal();\n refreshAll();\n }, { once: true });\n }\n $('#btnArchived').addEventListener('click', openArchived);\n $('#quickArchived').addEventListener('click', openArchived);\n $('#btnTrash').addEventListener('click', openTrash);\n $('#quickTrash').addEventListener('click', openTrash);\n\n /* search / filter / paging */\n $('#memSearch').addEventListener('keydown', function (e) { if (e.key === 'Enter') { state.query = e.target.value; state.offset = 0; loadEntries(); } });\n $('#globalSearch').addEventListener('keydown', function (e) { if (e.key === 'Enter') { state.query = e.target.value; state.offset = 0; switchView('memories'); loadEntries(); } });\n $('#typeChips').addEventListener('click', function (e) {\n var b = e.target.closest('.chip');\n if (!b) return;\n state.type = b.dataset.type; state.offset = 0;\n Array.prototype.forEach.call(document.querySelectorAll('#typeChips .chip'), function (c) { c.classList.toggle('active', c === b); });\n loadEntries();\n });\n $('#prevPage').addEventListener('click', function () { if (state.offset > 0) { state.offset -= state.limit; loadEntries(); } });\n $('#nextPage').addEventListener('click', function () { state.offset += state.limit; loadEntries(); });\n\n /* owners */\n $('#ownerRows').addEventListener('click', async function (e) {\n var authBtn = e.target.closest('button[data-auth]');\n var rmBtn = e.target.closest('button[data-remove]');\n if (authBtn) {\n var owner = authBtn.dataset.auth;\n var authorized = state.owners.filter(function (o) { return o.owner === owner; })[0].authorized;\n if (authorized) {\n confirmModal('吊销 ' + owner + ' 的授权?', '吊销后该智能体立即失去私密读写能力,旧 key 会立即校验失败。', '吊销', true, async function () {\n var r = await api('/api/revoke', { owner: owner });\n closeModal(); toast(r.error || '已吊销'); loadOwners();\n });\n } else {\n confirmModal('授权 ' + owner + '?', '「授权」由你(用户)操作:确认后生成只显示一次的 agent_key,请立即单独保存;服务端同时写临时待领取文件 keys/pending/<agent_id>.key,供该 AI 新会话执行 key claim 领取,领取成功后自动删除。AI 不应代为执行该授权操作。', '授权', false, async function () {\n var r = await api('/api/authorize', { owner: owner });\n closeModal();\n if (r.error) { toast(r.error); return; }\n openModal('agent_key(只显示一次)', '<p class=\"muted\">请立即单独保存;AI 新会话执行 key status → key claim 领取。</p><textarea class=\"keybox\" readonly>' + esc(r.agentKey) + '</textarea>', { okText: '我已保存', onOk: closeModal });\n var box = $('#modalBody').querySelector('textarea');\n if (box) { box.focus(); box.select(); }\n loadOwners();\n });\n }\n return;\n }\n if (rmBtn) {\n var target = rmBtn.dataset.remove;\n openModal('删除 ' + target + ' 的身份与私密记忆', '<p class=\"muted\">不可恢复;公共 FACT 保留,其它 AI 不受影响。请输入完整 ID 确认:</p><input id=\"confirmOwner\" placeholder=\"' + esc(target) + '\">', {\n okText: '删除', danger: true, onOk: async function () {\n var typed = $('#confirmOwner').value.trim();\n if (typed !== target) { toast('ID 不匹配'); return; }\n var r = await api('/api/identity-remove', { owner: target, confirm: typed });\n closeModal(); toast(r.error || '已删除'); loadOwners(); loadOverview(); loadEntries();\n },\n });\n }\n });\n\n /* settings */\n $('#btnResetPw').addEventListener('click', async function () {\n var a = $('#pwCur').value, b = $('#pwNew').value, c = $('#pwNew2').value;\n if (!b || b !== c) { $('#pwOut').textContent = '两次新口令不一致'; return; }\n var r = await api('/api/reset-password', { currentPassword: a, newPassword: b });\n $('#pwOut').textContent = r.error || (r.text || '已重设');\n if (!r.error) { $('#pwCur').value = $('#pwNew').value = $('#pwNew2').value = ''; }\n });\n $('#btnRk').addEventListener('click', async function () {\n var r = await api('/api/recovery-key');\n if (r.error) { toast(r.error); return; }\n openModal('恢复钥匙(离线保存)', '<p class=\"muted\">泄露等同口令泄露。忘记主口令时可用它重设。</p><textarea class=\"keybox\" readonly>' + esc(r.recoveryKey) + '</textarea>', { okText: '我已保存', onOk: closeModal });\n });\n $('#btnExport').addEventListener('click', async function () {\n var r = await api('/api/export');\n if (r.error) { toast(r.error); return; }\n var blob = new Blob([JSON.stringify(r, null, 2)], { type: 'application/json' });\n var a = document.createElement('a');\n a.href = URL.createObjectURL(blob);\n a.download = 'yottamemory-export.json';\n a.click();\n toast('已导出 JSON');\n });\n $('#btnDangerHint').addEventListener('click', function () {\n toast('在「AI 与权限」列表里,每个 AI 右侧有「删除」按钮');\n });\n\n /* unlock / boot */\n $('#unlockBtn').addEventListener('click', async function () {\n var r = await api('/api/unlock', { password: $('#pw').value });\n if (r.error) { $('#lockErr').textContent = r.error; return; }\n $('#lockErr').textContent = '';\n showApp();\n });\n $('#pw').addEventListener('keydown', function (e) { if (e.key === 'Enter') $('#unlockBtn').click(); });\n (async function boot() {\n var s = await api('/api/status');\n if (s && s.version) $('#ver').textContent = 'v' + s.version;\n if (s && s.unlocked) showApp(); else showLock();\n if (s && s.encrypted === false) $('#lockErr').textContent = '当前记忆库未启用加密;请先在 CLI 执行 init --encrypt / migrate。';\n })();\n})();\n</script>\n</body>\n</html>\n";
310
310
  // @generated view-html:end
311
311
  // MCP 协议(2026-07-28 无状态 + 2025-11-25 legacy 握手,dual-era)
312
312
  const MCP_PROTOCOL_MODERN = '2026-07-28';
@@ -868,6 +868,8 @@ function today(override) {
868
868
  }
869
869
  let RUNTIME_AGENT = { id: '', agentKey: '' };
870
870
  const IDENTITY_CONTEXT = new AsyncLocalStorage();
871
+ // v0.21.0:view 用户会话的 owner 密钥上下文——请求处理期间让读 / 写 / 索引路径复用已解锁的 ownerKeys。
872
+ const VIEW_KEYS_CONTEXT = new AsyncLocalStorage();
871
873
  const RUNTIME_OWNER_KEYS = new Map();
872
874
  function setRuntimeAgent(id, agentKey) {
873
875
  RUNTIME_AGENT = { id: String(id || '').trim(), agentKey: String(agentKey || '').trim() };
@@ -1171,19 +1173,42 @@ function parseFrontmatter(text) {
1171
1173
  if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
1172
1174
  v = v.slice(1, -1);
1173
1175
  }
1174
- meta[k] = v;
1176
+ const arr = parseInlineArray(v);
1177
+ meta[k] = arr === null ? v : arr;
1175
1178
  }
1176
1179
  return { meta: meta, body: text.slice(m[0][0].length + m[0].length) };
1177
1180
  }
1181
+ // 内联数组解析:标准 JSON 数组(v0.21.0+ 写出格式)正常解析;
1182
+ // 历史缺陷留下的逐层 \" 转义污染在这里逐层剥离后解析,使下一次重写自动修复为规范格式。
1183
+ function parseInlineArray(v) {
1184
+ const s = String(v === undefined || v === null ? '' : v).trim();
1185
+ if (!(s.startsWith('[') && s.endsWith(']'))) return null;
1186
+ let candidate = s;
1187
+ for (let i = 0; i < 6; i += 1) {
1188
+ try {
1189
+ const parsed = JSON.parse(candidate);
1190
+ if (Array.isArray(parsed)) return parsed.map(String);
1191
+ } catch (e) { /* 兼容历史转义:继续剥离一层 */ }
1192
+ if (candidate.indexOf('\\"') === -1) break;
1193
+ candidate = candidate.split('\\"').join('"');
1194
+ }
1195
+ const inner = s.slice(1, -1).trim();
1196
+ if (!inner) return [];
1197
+ return inner.split(',').map(function (x) {
1198
+ return x.trim().replace(/^["']+|["']+$/g, '');
1199
+ }).filter(Boolean);
1200
+ }
1178
1201
  function escapeYaml(v) {
1179
1202
  return String(v).replace(/\n/g, ' ').replace(/"/g, '\\"');
1180
1203
  }
1181
1204
  function parseTags(v) {
1182
1205
  if (Array.isArray(v)) return v.map(String);
1183
1206
  if (typeof v === 'string') {
1184
- let s = v.trim();
1185
- if (s.startsWith('[') && s.endsWith(']')) s = s.slice(1, -1);
1186
- return s.split(',').map(function (x) { return x.trim(); }).filter(Boolean);
1207
+ const arr = parseInlineArray(v);
1208
+ if (arr !== null) return arr;
1209
+ return v.split(',').map(function (x) {
1210
+ return x.trim().replace(/^["']+|["']+$/g, '');
1211
+ }).filter(Boolean);
1187
1212
  }
1188
1213
  return [];
1189
1214
  }
@@ -1861,6 +1886,8 @@ function unwrapAgentBinding(root, owner, agentKey) {
1861
1886
  }
1862
1887
  function getOwnerKeyFor(root, owner, identity) {
1863
1888
  if (!owner) return null;
1889
+ const viewKeys = VIEW_KEYS_CONTEXT.getStore();
1890
+ if (viewKeys && viewKeys.keys && viewKeys.keys[owner]) return viewKeys.keys[owner];
1864
1891
  if (!isEncrypted(root)) return null;
1865
1892
  const current = identity || currentRuntimeIdentity();
1866
1893
  if (!current.id || current.id !== owner || !current.agentKey) return null;
@@ -4327,7 +4354,7 @@ function backupDrillCore(opts) {
4327
4354
  }
4328
4355
  function rememberCore(type, subject, statement, opts) {
4329
4356
  opts = opts || {};
4330
- const root = userRoot();
4357
+ const root = opts.root ? path.resolve(String(opts.root)) : userRoot();
4331
4358
  ensureInit(root);
4332
4359
  const t = String(type).toUpperCase();
4333
4360
  if (!TYPE_DIRS[t]) return { error: true, text: '未知记忆类型: ' + type + '(可用: ' + TYPES.join(' / ') + ')' };
@@ -4335,21 +4362,21 @@ function rememberCore(type, subject, statement, opts) {
4335
4362
  const subj = String(subject || '').trim();
4336
4363
  if (!stmt) return { error: true, text: 'statement 不能为空' };
4337
4364
  if (!subj) return { error: true, text: 'subject 不能为空' };
4338
- const ident = resolveIdentity(opts);
4365
+ const ident = opts.userUnlocked ? { id: '', error: '' } : resolveIdentity(opts);
4339
4366
  const selfAgent = ident.id;
4340
4367
  const owner = opts.owner || selfAgent;
4341
4368
  const scope = opts.scope || defaultScope(t);
4342
- if (scope === 'private' && ident.error) {
4369
+ if (scope === 'private' && ident.error && !opts.userUnlocked) {
4343
4370
  return { error: true, text: ident.error };
4344
4371
  }
4345
- if (scope === 'private') {
4372
+ if (scope === 'private' && !opts.userUnlocked) {
4346
4373
  const keyError = validatePrivateIdentity(root, ident, owner);
4347
4374
  if (keyError) return { error: true, text: keyError };
4348
4375
  }
4349
4376
  if (scope === 'private' && !owner) {
4350
4377
  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)不受影响。' };
4351
4378
  }
4352
- if (scope === 'private' && owner && selfAgent && owner !== selfAgent && !opts.unsafe) {
4379
+ if (scope === 'private' && owner && selfAgent && owner !== selfAgent && !opts.unsafe && !opts.userUnlocked) {
4353
4380
  return { error: true, text: '拒绝: 当前显式身份 ' + selfAgent + ' 不能写入其它智能体 ' + owner + ' 的私密区。请传正确的 --agent <id>,或加 --unsafe(用户显式授权)。' };
4354
4381
  }
4355
4382
  const encrypted = isEncrypted(root);
@@ -4395,7 +4422,8 @@ function rememberCore(type, subject, statement, opts) {
4395
4422
  const rec = {
4396
4423
  type: t, subject: subj, statement: stmt,
4397
4424
  confidence: 1.0, created: writeDate, updated: writeDate,
4398
- tags: [], immutable: false,
4425
+ tags: Array.isArray(opts.tags) ? opts.tags.map(function (x) { return String(x).trim(); }).filter(Boolean) : [],
4426
+ immutable: false,
4399
4427
  scope: scope, owner: owner,
4400
4428
  source: opts.source || '',
4401
4429
  weight: (parseFloat(opts.weight) > 0 ? parseFloat(opts.weight) : 1.0),
@@ -4759,8 +4787,8 @@ function renameCore(fileRef, newName, opts) {
4759
4787
 
4760
4788
  function forgetCore(fileRef, opts) {
4761
4789
  opts = opts || {};
4762
- const selfAgent = resolveIdentity(opts).id;
4763
- const roots = memoryRoots();
4790
+ const selfAgent = opts.userUnlocked ? '' : resolveIdentity(opts).id;
4791
+ const roots = opts.root ? [path.resolve(String(opts.root))] : memoryRoots();
4764
4792
  const ref = String(fileRef || '').replace(/\\/g, '/');
4765
4793
  let target = null, targetRoot = null, targetRel = null;
4766
4794
  for (const root of roots) {
@@ -4771,7 +4799,7 @@ function forgetCore(fileRef, opts) {
4771
4799
  const seg = targetRel.replace(/\\/g, '/').split('/');
4772
4800
  if (seg[0] === 'private') {
4773
4801
  const owner = seg[1] || '';
4774
- if (!opts.unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
4802
+ if (!opts.userUnlocked && !opts.unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
4775
4803
  return { error: true, text: '拒绝: 不能删除其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 --agent / --agent-id 声明自己的身份,或加 --unsafe(用户显式授权)。' };
4776
4804
  }
4777
4805
  }
@@ -4798,7 +4826,7 @@ function forgetCore(fileRef, opts) {
4798
4826
  selfAgent,
4799
4827
  });
4800
4828
  }
4801
- return { error: false, text: '已移入回收区: ' + trashFile };
4829
+ return { error: false, text: '已移入回收区: ' + trashFile, trash: relOf(targetRoot, trashFile) };
4802
4830
  }
4803
4831
  // ---- v0.18.0 A3:容量水位 / 淘汰候选 / 晋升建议(只读报告,不自动动数据)----
4804
4832
  const CAPACITY_CONFIG_DEFAULTS = {
@@ -6613,7 +6641,7 @@ function cmdKeyStatus(id, opts) {
6613
6641
  }
6614
6642
 
6615
6643
  // v0.8.1: 查看平台分页——服务端只返回当前页(offset/limit),避免记忆多了一次全量加载/渲染/传输
6616
- function viewEntriesCore(root, session, query, offset, limit) {
6644
+ function viewEntriesCore(root, session, query, offset, limit, type) {
6617
6645
  limit = Math.max(1, parseInt(limit, 10) || 50);
6618
6646
  offset = Math.max(0, parseInt(offset, 10) || 0);
6619
6647
  const q = String(query || '').toLowerCase();
@@ -6624,14 +6652,209 @@ function viewEntriesCore(root, session, query, offset, limit) {
6624
6652
  if (!key) continue;
6625
6653
  try { for (const e of loadOwnerIndex(root, o, key)) entries.push(e); } catch (err) {}
6626
6654
  }
6627
- const all = q ? entries.filter(function (e) {
6655
+ let all = q ? entries.filter(function (e) {
6628
6656
  return ((e.subject || '') + ' ' + (e.statement || '') + ' ' + (e.tags || []).join(' ')).toLowerCase().indexOf(q) !== -1;
6629
6657
  }) : entries;
6658
+ const typeFilter = String(type || '').toUpperCase();
6659
+ if (typeFilter && typeFilter !== 'ALL') {
6660
+ all = all.filter(function (e) { return String(e.type || '').toUpperCase() === typeFilter; });
6661
+ }
6630
6662
  all.sort(function (a, b) { return String(b.updated || b.created || '').localeCompare(String(a.updated || a.created || '')); });
6631
6663
  const count = all.length;
6632
6664
  const page = all.slice(offset, offset + limit);
6633
6665
  return { count: count, offset: offset, limit: limit, hasMore: offset + limit < count, entries: page };
6634
6666
  }
6667
+
6668
+ // ---- v0.21.0 M1:记忆管理台(view)真读写 ----
6669
+ function viewOverviewCore(root, session) {
6670
+ const entries = [];
6671
+ for (const e of (loadIndex(root) || [])) entries.push(e);
6672
+ for (const o of collectOwners(root)) {
6673
+ const key = session && session.ownerKeys ? session.ownerKeys[o] : null;
6674
+ if (!key) continue;
6675
+ try { for (const e of loadOwnerIndex(root, o, key)) entries.push(e); } catch (err) {}
6676
+ }
6677
+ const byType = { FACT: 0, PREF: 0, BOUND: 0, COMMIT: 0 };
6678
+ for (const e of entries) {
6679
+ const t = String(e.type || 'FACT').toUpperCase();
6680
+ if (byType[t] !== undefined) byType[t] += 1;
6681
+ }
6682
+ const owners = keyOwners(root).map(function (o) {
6683
+ return { owner: o, authorized: fs.existsSync(encAgentBindingPath(root, o)) };
6684
+ });
6685
+ let bytes = 0;
6686
+ let files = 0;
6687
+ for (const fp of collectEntryFiles(root)) {
6688
+ files += 1;
6689
+ try { bytes += fs.statSync(fp).size; } catch (e) {}
6690
+ }
6691
+ const archivedFiles = [];
6692
+ const archiveBase = path.join(root, ARCHIVE_DIR);
6693
+ if (fs.existsSync(archiveBase)) walkEntryFiles(archiveBase, archivedFiles);
6694
+ const cfg = loadConfig();
6695
+ return {
6696
+ total: entries.length,
6697
+ files: files,
6698
+ byType: byType,
6699
+ owners: owners,
6700
+ authorized: owners.filter(function (o) { return o.authorized; }).length,
6701
+ bytes: bytes,
6702
+ archived: archivedFiles.length,
6703
+ backupDir: cfg.backup_dir || '',
6704
+ };
6705
+ }
6706
+
6707
+ function viewRememberCore(root, payload) {
6708
+ payload = payload || {};
6709
+ const type = String(payload.type || '').toUpperCase();
6710
+ if (!TYPE_DIRS[type]) return { error: true, text: '未知记忆类型: ' + payload.type };
6711
+ const scope = payload.scope || defaultScope(type);
6712
+ const owner = String(payload.owner || '');
6713
+ if (scope === 'private' && !isSafeAgentId(owner)) return { error: true, text: '私密记忆必须选择归属智能体(owner)。' };
6714
+ return rememberCore(type, payload.subject, payload.statement, {
6715
+ root: root,
6716
+ owner: owner,
6717
+ scope: scope,
6718
+ tags: Array.isArray(payload.tags) ? payload.tags : [],
6719
+ weight: payload.weight,
6720
+ source: 'view',
6721
+ hint: false,
6722
+ userUnlocked: true,
6723
+ });
6724
+ }
6725
+
6726
+ function viewUpdateCore(root, payload) {
6727
+ payload = payload || {};
6728
+ const found = resolveMemoryFile(root, String(payload.file || ''));
6729
+ if (!found) return { error: true, text: '未找到记忆文件: ' + payload.file };
6730
+ const rel = found.rel;
6731
+ const owner = ownerFromPrivatePath(root, found.fp);
6732
+ const patch = {};
6733
+ if (payload.subject !== undefined) {
6734
+ const s = String(payload.subject).trim();
6735
+ if (!s) return { error: true, text: 'subject 不能为空' };
6736
+ patch.subject = s;
6737
+ }
6738
+ if (payload.statement !== undefined) {
6739
+ const s = String(payload.statement).trim();
6740
+ if (!s) return { error: true, text: 'statement 不能为空' };
6741
+ patch.statement = s;
6742
+ }
6743
+ if (payload.tags !== undefined) {
6744
+ patch.tags = Array.isArray(payload.tags) ? payload.tags.map(String).filter(Boolean) : parseTags(String(payload.tags));
6745
+ }
6746
+ if (!Object.keys(patch).length) return { error: true, text: '没有可更新的字段(subject / statement / tags)。' };
6747
+ let original;
6748
+ try { original = parseFrontmatter(readMemoryText(root, found.fp, owner)).meta; } catch (e) { return { error: true, text: '读取失败: ' + e.message }; }
6749
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
6750
+ const undoRel = path.posix.join('.trash', 'view-history', ts, rel);
6751
+ const undoPath = path.join(root, undoRel);
6752
+ try {
6753
+ fs.mkdirSync(path.dirname(undoPath), { recursive: true });
6754
+ fs.copyFileSync(found.fp, undoPath);
6755
+ } catch (e) { return { error: true, text: '编辑前备份失败: ' + e.message }; }
6756
+ patch.updated = today();
6757
+ try { rewriteFrontmatter(found.fp, patch, root, owner); } catch (e) { return { error: true, text: '更新失败: ' + e.message }; }
6758
+ upsertIndexEntry(root, readEntry(found.fp, root));
6759
+ appendAudit(root, 'audit', { action: 'view_update', ts: new Date().toISOString(), file: rel, undo: undoRel, before_updated: original.updated || '' });
6760
+ return { error: false, file: rel, undo: undoRel, text: '已更新: ' + rel };
6761
+ }
6762
+
6763
+ function viewDeleteCore(root, payload) {
6764
+ payload = payload || {};
6765
+ const result = forgetCore(String(payload.file || ''), { root: root, userUnlocked: true });
6766
+ if (result.error) return result;
6767
+ appendAudit(root, 'audit', { action: 'view_delete', ts: new Date().toISOString(), file: payload.file, trash: result.trash || '' });
6768
+ return { error: false, file: payload.file, trash: result.trash || '', text: result.text };
6769
+ }
6770
+
6771
+ function viewRestoreCore(root, payload) {
6772
+ payload = payload || {};
6773
+ const ref = String(payload.ref || '').replace(/\\/g, '/');
6774
+ if (ref.indexOf('.trash/') !== 0) return { error: true, text: '只能从回收区 / 编辑历史恢复。' };
6775
+ const abs = resolveWithinRoot(root, ref);
6776
+ if (!abs || !fs.existsSync(abs) || fs.statSync(abs).isDirectory()) return { error: true, text: '回收区文件不存在: ' + ref };
6777
+ const seg = ref.split('/');
6778
+ const originalRel = seg[1] === 'view-history' ? seg.slice(3).join('/') : seg.slice(2).join('/');
6779
+ if (!originalRel) return { error: true, text: '无法解析原路径。' };
6780
+ const dest = resolveWithinRoot(root, originalRel);
6781
+ if (!dest) return { error: true, text: '原路径非法。' };
6782
+ if (fs.existsSync(dest)) return { error: true, text: '原位置已有文件,拒绝覆盖: ' + originalRel };
6783
+ try {
6784
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
6785
+ fs.renameSync(abs, dest);
6786
+ } catch (e) { return { error: true, text: '恢复失败: ' + e.message }; }
6787
+ upsertIndexEntry(root, readEntry(dest, root));
6788
+ appendAudit(root, 'audit', { action: 'view_restore', ts: new Date().toISOString(), file: originalRel, from: ref });
6789
+ return { error: false, file: originalRel, text: '已恢复: ' + originalRel };
6790
+ }
6791
+
6792
+ function viewArchiveCore(root, payload) {
6793
+ payload = payload || {};
6794
+ const found = resolveMemoryFile(root, String(payload.file || ''));
6795
+ if (!found) return { error: true, text: '未找到记忆文件: ' + payload.file };
6796
+ const rel = found.rel;
6797
+ const owner = ownerFromPrivatePath(root, found.fp);
6798
+ let meta;
6799
+ try { meta = parseFrontmatter(readMemoryText(root, found.fp, owner)).meta; } catch (e) { return { error: true, text: '读取失败: ' + e.message }; }
6800
+ const type = String(meta.type || 'FACT').toUpperCase();
6801
+ const destRel = archiveRelFor(root, rel, type, owner);
6802
+ const dest = resolveWithinRoot(root, destRel);
6803
+ if (!dest) return { error: true, text: '归档路径非法。' };
6804
+ if (fs.existsSync(dest)) return { error: true, text: '归档区已存在同名文件,拒绝覆盖: ' + destRel };
6805
+ try {
6806
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
6807
+ fs.renameSync(found.fp, dest);
6808
+ } catch (e) { return { error: true, text: '归档失败: ' + e.message }; }
6809
+ removeIndexEntry(root, rel);
6810
+ appendAudit(root, 'audit', { action: 'view_archive', ts: new Date().toISOString(), file: rel, archive: destRel });
6811
+ return { error: false, file: rel, archive: destRel, text: '已藏起来(归档): ' + rel };
6812
+ }
6813
+
6814
+ function viewUnarchiveCore(root, payload) {
6815
+ payload = payload || {};
6816
+ const ref = String(payload.archive || payload.file || '').replace(/\\/g, '/');
6817
+ if (ref.indexOf(ARCHIVE_DIR + '/') !== 0) return { error: true, text: '只能从归档区放回。' };
6818
+ const abs = resolveWithinRoot(root, ref);
6819
+ if (!abs || !fs.existsSync(abs) || fs.statSync(abs).isDirectory()) return { error: true, text: '归档文件不存在: ' + ref };
6820
+ const originalRel = ref.slice(ARCHIVE_DIR.length + 1);
6821
+ const dest = resolveWithinRoot(root, originalRel);
6822
+ if (!dest) return { error: true, text: '原路径非法。' };
6823
+ if (fs.existsSync(dest)) return { error: true, text: '原位置已有文件,拒绝覆盖: ' + originalRel };
6824
+ try {
6825
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
6826
+ fs.renameSync(abs, dest);
6827
+ } catch (e) { return { error: true, text: '放回失败: ' + e.message }; }
6828
+ upsertIndexEntry(root, readEntry(dest, root));
6829
+ appendAudit(root, 'audit', { action: 'view_unarchive', ts: new Date().toISOString(), file: originalRel, archive: ref });
6830
+ return { error: false, file: originalRel, text: '已放回: ' + originalRel };
6831
+ }
6832
+
6833
+ function viewArchiveListCore(root) {
6834
+ const files = [];
6835
+ const base = path.join(root, ARCHIVE_DIR);
6836
+ if (fs.existsSync(base)) walkEntryFiles(base, files);
6837
+ return {
6838
+ entries: files.map(function (fp) {
6839
+ return { ref: relOf(root, fp), original: path.relative(base, fp).replace(/\\/g, '/') };
6840
+ }),
6841
+ };
6842
+ }
6843
+
6844
+ function viewTrashListCore(root) {
6845
+ const files = [];
6846
+ const base = path.join(root, '.trash');
6847
+ if (fs.existsSync(base)) walkEntryFiles(base, files);
6848
+ return {
6849
+ entries: files.map(function (fp) {
6850
+ const ref = relOf(root, fp);
6851
+ const seg = ref.split('/');
6852
+ const history = seg[1] === 'view-history';
6853
+ return { ref: ref, kind: history ? 'history' : 'trash', original: history ? seg.slice(3).join('/') : seg.slice(2).join('/') };
6854
+ }),
6855
+ };
6856
+ }
6857
+
6635
6858
  function viewHostName(req) {
6636
6859
  const raw = String(req.headers.host || '').trim();
6637
6860
  if (!raw) return '';
@@ -6739,7 +6962,61 @@ function viewServerCore(root, port, host, opts) {
6739
6962
  }
6740
6963
  if (req.method === 'POST' && pathname === '/api/entries') {
6741
6964
  if (!session.umk) return json(401, { error: '请先解锁。' });
6742
- return readBody(function (d) { json(200, viewEntriesCore(root, session, d.query, d.offset, d.limit)); });
6965
+ return readBody(function (d) { json(200, viewEntriesCore(root, session, d.query, d.offset, d.limit, d.type)); });
6966
+ }
6967
+ if (req.method === 'POST' && pathname === '/api/overview') {
6968
+ if (!session.umk) return json(401, { error: '请先解锁。' });
6969
+ return json(200, VIEW_KEYS_CONTEXT.run({ keys: session.ownerKeys }, function () { return viewOverviewCore(root, session); }));
6970
+ }
6971
+ if (req.method === 'POST' && pathname === '/api/memory/create') {
6972
+ if (!session.umk) return json(401, { error: '请先解锁。' });
6973
+ return readBody(function (d) {
6974
+ const r = VIEW_KEYS_CONTEXT.run({ keys: session.ownerKeys }, function () { return viewRememberCore(root, d); });
6975
+ json(r.error ? 400 : 200, r);
6976
+ });
6977
+ }
6978
+ if (req.method === 'POST' && pathname === '/api/memory/update') {
6979
+ if (!session.umk) return json(401, { error: '请先解锁。' });
6980
+ return readBody(function (d) {
6981
+ const r = VIEW_KEYS_CONTEXT.run({ keys: session.ownerKeys }, function () { return viewUpdateCore(root, d); });
6982
+ json(r.error ? 400 : 200, r);
6983
+ });
6984
+ }
6985
+ if (req.method === 'POST' && pathname === '/api/memory/delete') {
6986
+ if (!session.umk) return json(401, { error: '请先解锁。' });
6987
+ return readBody(function (d) {
6988
+ const r = VIEW_KEYS_CONTEXT.run({ keys: session.ownerKeys }, function () { return viewDeleteCore(root, d); });
6989
+ json(r.error ? 400 : 200, r);
6990
+ });
6991
+ }
6992
+ if (req.method === 'POST' && pathname === '/api/memory/restore') {
6993
+ if (!session.umk) return json(401, { error: '请先解锁。' });
6994
+ return readBody(function (d) {
6995
+ const r = VIEW_KEYS_CONTEXT.run({ keys: session.ownerKeys }, function () { return viewRestoreCore(root, d); });
6996
+ json(r.error ? 400 : 200, r);
6997
+ });
6998
+ }
6999
+ if (req.method === 'POST' && pathname === '/api/memory/archive') {
7000
+ if (!session.umk) return json(401, { error: '请先解锁。' });
7001
+ return readBody(function (d) {
7002
+ const r = VIEW_KEYS_CONTEXT.run({ keys: session.ownerKeys }, function () { return viewArchiveCore(root, d); });
7003
+ json(r.error ? 400 : 200, r);
7004
+ });
7005
+ }
7006
+ if (req.method === 'POST' && pathname === '/api/memory/unarchive') {
7007
+ if (!session.umk) return json(401, { error: '请先解锁。' });
7008
+ return readBody(function (d) {
7009
+ const r = VIEW_KEYS_CONTEXT.run({ keys: session.ownerKeys }, function () { return viewUnarchiveCore(root, d); });
7010
+ json(r.error ? 400 : 200, r);
7011
+ });
7012
+ }
7013
+ if (req.method === 'POST' && pathname === '/api/trash') {
7014
+ if (!session.umk) return json(401, { error: '请先解锁。' });
7015
+ return json(200, viewTrashListCore(root));
7016
+ }
7017
+ if (req.method === 'POST' && pathname === '/api/archived') {
7018
+ if (!session.umk) return json(401, { error: '请先解锁。' });
7019
+ return json(200, viewArchiveListCore(root));
6743
7020
  }
6744
7021
  if (req.method === 'POST' && pathname === '/api/export') {
6745
7022
  if (!session.umk) return json(401, { error: '请先解锁。' });