@yottameta/yotta-memory 0.22.8 → 0.22.9

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 CHANGED
@@ -1,3 +1,8 @@
1
+ ## v0.22.9 (2026-10-11)
2
+
3
+ - **管理台「高级 / CLI」完整帮助:配置键块与 config 命令相邻化(UX-M-01)**:此前 39 键「配置键(config set)」块统一追加在所有命令分组之后,与「加密与安全」组末尾的 `config` 命令之间隔着整个「平台与服务」组,阅读流断裂;现在键表紧随 `config` 命令(同组相邻),找不到 config 命令时仍回退到末尾(`keysPlaced` 兜底)。
4
+ - 回归门禁:`test/view-cli-help.test.js` 断言放置逻辑标记;`tools/check_memory_view.js` 在 1440 / 390 两档断言「`#cliConfigKeys` 必须是 config 命令 `<details>` 的相邻兄弟节点」。
5
+
1
6
  ## v0.22.8 (2026-10-10)
2
7
 
3
8
  - **管理台「记忆」列表两处展示修复**:① 顶部类型 chips(全部 / FACT / PREF / BOUND / COMMIT)计数跟随当前智能体 / 搜索筛选,不再固定显示全库口径(概览页四类分布保持全局口径);② 列表排序与时间语义修复:命中打点(search / use 信号写回)改为保留文件原时间戳,旧条目被搜到不再顶到「最新」;卡片与详情显示「创建(frontmatter created,文件 btime 兜底)+ 更新(文件 mtime 到秒)」双时间。
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: yotta-memory
3
3
  description: 元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/可回滚:让任何 AI 智能体活过会话,开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档、记忆盘、共享记忆、局域网记忆、画像、开工上下文、长期理解摘要、近期走廊、会话闭环、记忆守则、profile、context、越用越懂、语义检索、反馈、维护、蒸馏、feedback、maintain、distill、explain、自我学习、自我进化、自我提升、查看平台分页、recall 候选预过滤、任务相关记忆、--focus、--embedding、压缩遗忘、consolidate、周期摘要、自动合并、分类型衰减、回滚、备份、backup、防误删、doctor、事务快照、基线探针、恢复演练、记忆库安全扫描、scan、命中打点、容量水位、--capacity、--propose、上下文压缩审计、context --audit
4
- version: 0.22.8
4
+ version: 0.22.9
5
5
  license: MIT
6
6
  ---
7
7
 
package/assets/view.html CHANGED
@@ -839,6 +839,19 @@
839
839
  }
840
840
  function renderFull() {
841
841
  if (!state.help) return;
842
+ var keys = state.help.configKeys || [];
843
+ var keyHelp = state.help.configKeyHelp || {};
844
+ var keysHtml = keys.length ? '<div class="quick-group" id="cliConfigKeys"><h4>配置键(config set)</h4>' +
845
+ '<div class="cmd-when">全部可配置键(共 ' + keys.length + ' 个)。用法:yotta-memory config set &lt;键&gt; &lt;值&gt;,改完用 yotta-memory config get 复查;配置保存在 ~/.yottamemory/config.json。</div>' +
846
+ keys.map(function (k) {
847
+ var h = keyHelp[k] || {};
848
+ return '<div class="opt-row with-copy"><div class="opt-flag">' + esc(k) + '</div><div>' +
849
+ '<div class="opt-what">' + esc(h.what || '') + '</div>' +
850
+ (h.when ? '<div class="opt-when">' + esc(h.when) + '</div>' : '') +
851
+ '</div>' + copyBtn('yotta-memory config set ' + k + ' <值>') + '</div>';
852
+ }).join('') + '</div>' : '';
853
+ // UX-M-01:配置键块紧随 config 命令(同组相邻);找不到 config 时回退末尾。
854
+ var keysPlaced = false;
842
855
  var groupsHtml = (state.help.groups || []).map(function (g) {
843
856
  var cmds = (g.commands || []).map(function (c) {
844
857
  var subs = (c.subcommands || []).map(function (s) {
@@ -847,24 +860,18 @@
847
860
  (s.when ? '<div class="cmd-when">' + esc(s.when) + '</div>' : '') +
848
861
  codeLine('yotta-memory ' + c.name + ' ' + s.usage) + optRows(s.options) + '</div>';
849
862
  }).join('');
850
- return '<details class="help-cmd"><summary><code class="cmd-name">' + esc(c.name) + '</code><span class="cmd-what">' + esc(c.what) + '</span></summary>' +
863
+ var cmdHtml = '<details class="help-cmd"><summary><code class="cmd-name">' + esc(c.name) + '</code><span class="cmd-what">' + esc(c.what) + '</span></summary>' +
851
864
  '<div class="help-body">' + (c.when ? '<div class="cmd-when">' + esc(c.when) + '</div>' : '') +
852
865
  codeLine('yotta-memory ' + c.usage) + optRows(c.options) + subs + '</div></details>';
866
+ if (!keysPlaced && c.name === 'config' && keysHtml) {
867
+ keysPlaced = true;
868
+ return cmdHtml + keysHtml;
869
+ }
870
+ return cmdHtml;
853
871
  }).join('');
854
872
  return '<div class="quick-group"><h4>' + esc(g.group) + '</h4>' + cmds + '</div>';
855
873
  }).join('');
856
- var keys = state.help.configKeys || [];
857
- var keyHelp = state.help.configKeyHelp || {};
858
- var keysHtml = keys.length ? '<div class="quick-group" id="cliConfigKeys"><h4>配置键(config set)</h4>' +
859
- '<div class="cmd-when">全部可配置键(共 ' + keys.length + ' 个)。用法:yotta-memory config set &lt;键&gt; &lt;值&gt;,改完用 yotta-memory config get 复查;配置保存在 ~/.yottamemory/config.json。</div>' +
860
- keys.map(function (k) {
861
- var h = keyHelp[k] || {};
862
- return '<div class="opt-row with-copy"><div class="opt-flag">' + esc(k) + '</div><div>' +
863
- '<div class="opt-what">' + esc(h.what || '') + '</div>' +
864
- (h.when ? '<div class="opt-when">' + esc(h.when) + '</div>' : '') +
865
- '</div>' + copyBtn('yotta-memory config set ' + k + ' <值>') + '</div>';
866
- }).join('') + '</div>' : '';
867
- $('#cliFull').innerHTML = groupsHtml + keysHtml;
874
+ $('#cliFull').innerHTML = groupsHtml + (keysPlaced ? '' : keysHtml);
868
875
  }
869
876
  async function ensureHelp() {
870
877
  if (state.help) { renderQuick(); return; }
@@ -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.22.8';
28
+ const VERSION = '0.22.9';
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
 
@@ -387,7 +387,7 @@ const VIEW_HELP_QUICK = [
387
387
  ] },
388
388
  ];
389
389
  // @generated view-html:start
390
- 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<link rel=\"icon\" href=\"data:,\">\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 .owner-pick{display:flex;flex-wrap:wrap;gap:6px;max-height:132px;overflow:auto;margin-bottom:8px}\n .owner-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:999px;padding:5px 10px;font-size:12px}\n .owner-chip:hover{color:var(--text);border-color:var(--line2)}\n .owner-chip.active{color:var(--gold);border-color:var(--line2);background:rgba(240,161,58,.12)}\n .owner-chip .tag{font-size:10px;color:var(--faint);border:1px solid var(--line);border-radius:999px;padding:0 6px}\n .owner-chip.active .tag{color:var(--gold);border-color:var(--line2)}\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 #drawerMask{z-index:40}\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 /* advanced cli */\n .quick-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}\n .cmd-item{border:1px solid var(--line);border-radius:8px;padding:12px 14px;background:var(--panel2);display:flex;flex-direction:column;gap:8px;min-width:0}\n .cmd-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n .cmd-name{font-family:ui-monospace,Consolas,monospace;font-size:12px;color:var(--gold)}\n .cmd-what{font-weight:600}\n .cmd-when{color:var(--muted);font-size:12px}\n .code-line{display:flex;align-items:center;gap:8px;background:rgba(0,0,0,.28);border:1px solid var(--line);border-radius:6px;padding:6px 10px;min-width:0}\n body.light .code-line{background:rgba(56,42,28,.06)}\n .code-line code{flex:1;min-width:0;font-family:ui-monospace,Consolas,monospace;font-size:12px;overflow-x:auto;white-space:nowrap}\n .danger-badge{color:var(--danger);border-color:rgba(224,112,112,.5);background:rgba(224,112,112,.10)}\n .cmd-note{color:var(--danger);font-size:12px}\n .quick-group{margin-top:14px}\n .quick-group h4{margin:0 0 8px;font-size:13px;color:var(--muted)}\n .help-cmd{border:1px solid var(--line);border-radius:8px;background:var(--panel2);margin-top:10px;min-width:0}\n .help-cmd summary{cursor:pointer;padding:10px 14px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;list-style:none}\n .help-cmd summary::-webkit-details-marker{display:none}\n .help-cmd summary::before{content:'';width:6px;height:6px;border-right:1.6px solid var(--muted);border-bottom:1.6px solid var(--muted);transform:rotate(-45deg);transition:transform .15s;flex:none}\n .help-cmd[open] summary::before{transform:rotate(45deg)}\n .help-body{padding:12px 14px;display:flex;flex-direction:column;gap:8px;border-top:1px solid var(--line)}\n .help-sub{border-top:1px solid var(--line);padding-top:8px;margin-top:2px;display:flex;flex-direction:column;gap:6px}\n .help-sub .cmd-name{color:var(--text)}\n .opt-row{display:grid;grid-template-columns:minmax(150px,auto) minmax(0,1fr);gap:2px 12px;padding:7px 0;border-top:1px solid var(--line);font-size:12px}\n .opt-row:first-child{border-top:0}\n .opt-flag{font-family:ui-monospace,Consolas,monospace;word-break:break-all}\n .opt-what{color:var(--muted)}\n .opt-when{color:var(--faint)}\n .opt-risk{color:var(--danger)}\n .opt-row.with-copy{grid-template-columns:minmax(150px,auto) minmax(0,1fr) auto;align-items:start}\n .opt-row.with-copy .ibtn{width:26px;height:26px}\n\n @media (max-width:960px){.g4{grid-template-columns:repeat(2,minmax(0,1fr))}.bars{grid-template-columns:repeat(2,minmax(0,1fr))}.quick-list{grid-template-columns: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 <button data-view=\"cli\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"m4 17 6-6-6-6\"/><path d=\"M12 19h8\"/></svg>高级 / CLI</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 <select id=\"ownerFilter\" title=\"按智能体筛选\" style=\"width:auto;min-width:140px\"><option value=\"\">全部智能体</option></select>\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\n <section class=\"view\" id=\"view-cli\">\n <div class=\"card\">\n <h3>常用命令速查</h3>\n <div class=\"faint\" style=\"margin-bottom:12px\">只展示与复制;命令都在终端执行,危险操作已标注。</div>\n <div id=\"cliQuick\"><div class=\"faint\">加载中…</div></div>\n </div>\n <div class=\"section card\">\n <h3>完整命令与参数</h3>\n <div class=\"faint\" style=\"margin-bottom:12px\">与当前安装版本一致(随版本自动更新);按需加载,只读展示,不执行任何命令。</div>\n <button class=\"btn\" id=\"btnFullHelp\">加载完整命令与参数</button>\n <div id=\"cliFull\" hidden></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', owner: '', owners: [], overview: null, current: null, help: 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 errText(r, fallback) {\n return r && r.error ? ((r.text || r.message || fallback) || '操作失败') : '';\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 pad2(n) { return (n < 10 ? '0' : '') + n; }\n function fmtTime(e) {\n if (e && e.mtime) {\n var d = new Date(e.mtime);\n if (!isNaN(d.getTime())) {\n return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());\n }\n }\n return (e && (e.updated || e.created)) || '';\n }\n // v0.22.8:卡片时间语义显式化——「创建」用 frontmatter created(文件系统 btime 兜底),「更新」用文件 mtime(打点已不再改写它)。\n function metaLine(e, withFile) {\n var parts = [e.owner || '公共'];\n if (e.created) parts.push('创建 ' + e.created);\n var up = fmtTime(e);\n if (up) parts.push('更新 ' + up);\n if (withFile) parts.push(e.file);\n return parts.join(' · ');\n }\n function applyTypeCounts(typeCounts) {\n Array.prototype.forEach.call(document.querySelectorAll('#typeChips .chip'), function (c) {\n c.textContent = (c.dataset.type === 'all' ? '全部' : c.dataset.type) + ' ' + (typeCounts[c.dataset.type] || 0);\n });\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: ['设置', '口令 / 备份 / 导出'], cli: ['高级 / CLI', '常用命令速查与完整帮助(只展示与复制)'] };\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 if (name === 'cli') ensureHelp();\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 var byType = d.byType || {};\n var typeCounts = { all: d.total || 0, FACT: byType.FACT || 0, PREF: byType.PREF || 0, BOUND: byType.BOUND || 0, COMMIT: byType.COMMIT || 0 };\n $('#typeBars').innerHTML = ['FACT', 'PREF', 'BOUND', 'COMMIT'].map(function (t) {\n return '<div class=\"bar\" data-type=\"' + t + '\" style=\"cursor:pointer\" title=\"查看 ' + typeNames[t] + '\"><span class=\"dot\" style=\"background:' + colors[t] + '\"></span><span class=\"faint\">' + typeNames[t] + '</span><b>' + typeCounts[t] + '</b></div>';\n }).join('');\n applyTypeCounts(typeCounts);\n }\n async function loadOwners() {\n var d = await api('/api/owners');\n state.owners = (d && d.owners) || [];\n var sel = $('#ownerFilter');\n if (sel) {\n sel.innerHTML = '<option value=\"\">全部智能体</option>' + state.owners.map(function (o) {\n return '<option value=\"' + esc(o.owner) + '\"' + (o.owner === state.owner ? ' selected' : '') + '>' + esc(o.owner) + '</option>';\n }).join('');\n }\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, owner: state.owner || '' });\n if (d.error) { toast(errText(d, '加载失败')); return; }\n state.entries = d.entries || [];\n state.count = d.count || 0;\n // v0.22.8:chips 计数跟随当前智能体 / 搜索筛选(概览页无筛选时即全局口径)。\n if (d.counts) applyTypeCounts(d.counts);\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(metaLine(e, true)) + '</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(metaLine(e, false)) + '</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] + ' · ' + metaLine(entry, false);\n $('#drBody').textContent = entry.statement || '';\n $('#drKv').innerHTML = '<dt>创建</dt><dd>' + esc(entry.created || (entry.btime ? fmtTime({ mtime: entry.btime }) : '-')) + '</dd>' +\n '<dt>最近更新</dt><dd>' + esc(fmtTime(entry) || entry.updated || '-') + '</dd>' +\n '<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 var ownerLocked = !isNew && t !== '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>' + (t === 'FACT' && !isNew ? '归属 AI(FACT 为公共记忆,归属仅作来源标记,可修改)' : '归属 AI(私密记忆必填;点选或输入自定义 ID)') + '</label>' +\n '<div class=\"owner-pick\" id=\"fOwnerPick\">' + (state.owners.length ? state.owners.map(function (o) {\n return '<button type=\"button\" class=\"owner-chip' + ((entry && entry.owner === o.owner) ? ' active' : '') + '\"' + (ownerLocked ? ' disabled' : '') + ' data-owner=\"' + esc(o.owner) + '\">' + esc(o.owner) + '<span class=\"tag\">' + (o.authorized ? '已授权' : '未授权') + '</span></button>';\n }).join('') : '<span class=\"faint\">还没有已登记的 AI;可直接输入自定义 ID。</span>') + '</div>' +\n '<input id=\"fOwner\" placeholder=\"自定义 AI ID(如 codex)\" value=\"' + esc(entry && entry.owner || '') + '\"' + (ownerLocked ? ' disabled' : '') + '></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 ? '' : (t === 'FACT'\n ? '<div class=\"faint\">类型不可修改;FACT 为公共记忆,归属仅作来源标记,可修改。</div>'\n : '<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 bindOwnerPicker() {\n var pick = $('#fOwnerPick');\n if (!pick || ($('#fOwner') && $('#fOwner').disabled)) return;\n pick.addEventListener('click', function (e) {\n var chip = e.target.closest('.owner-chip');\n if (!chip) return;\n Array.prototype.forEach.call(pick.querySelectorAll('.owner-chip'), function (c) { c.classList.toggle('active', c === chip); });\n $('#fOwner').value = chip.dataset.owner;\n });\n $('#fOwner').addEventListener('input', function () {\n var v = $('#fOwner').value.trim();\n Array.prototype.forEach.call(pick.querySelectorAll('.owner-chip'), function (c) { c.classList.toggle('active', c.dataset.owner === v); });\n });\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 body = { file: entry.file, subject: subject, statement: statement, tags: tags };\n var type = String(entry.type || 'FACT').toUpperCase();\n if (type === 'FACT' && $('#fOwner') && !$('#fOwner').disabled) body.owner = $('#fOwner').value.trim();\n var r = await api('/api/memory/update', body);\n if (r.error) { toast(errText(r, '保存失败')); return; }\n closeModal();\n toast('已更新 1 条记忆' + (r.ownerChange ? '(归属已改为 ' + (r.ownerChange.to || '公共') + ')' : ''), '撤销', async function () {\n var u = await api('/api/memory/restore', { ref: r.undo });\n toast(errText(u, '撤销失败') || '已撤销这次编辑');\n refreshAll();\n });\n refreshAll();\n },\n });\n bindOwnerPicker();\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(errText(r, '创建失败')); return; }\n closeModal();\n toast('已创建 1 条记忆');\n refreshAll();\n },\n });\n bindOwnerPicker();\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(errText(r, '删除失败')); return; }\n closeModal();\n toast('已删除 1 条记忆', '撤销', async function () {\n var u = await api('/api/memory/restore', { ref: r.trash });\n toast(errText(u, '恢复失败') || '已恢复');\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(errText(r, '归档失败')); return; }\n closeModal();\n toast('已藏起来 1 条', '放回', async function () {\n var u = await api('/api/memory/unarchive', { archive: r.archive });\n toast(errText(u, '放回失败') || '已放回');\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(errText(r, '放回失败') || '已放回');\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(errText(r, '恢复失败') || '已恢复');\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 $('#typeBars').addEventListener('click', function (e) {\n var bar = e.target.closest('.bar[data-type]');\n if (!bar) return;\n state.type = bar.dataset.type; state.offset = 0;\n Array.prototype.forEach.call(document.querySelectorAll('#typeChips .chip'), function (c) { c.classList.toggle('active', c.dataset.type === state.type); });\n switchView('memories');\n loadEntries();\n });\n $('#ownerFilter').addEventListener('change', function () { state.owner = this.value; state.offset = 0; loadEntries(); });\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(errText(r, '吊销失败') || '已吊销'); 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(errText(r, '授权失败')); 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(errText(r, '删除失败') || '已删除'); 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 || '重设失败') : (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(errText(r, '恢复钥匙读取失败')); 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(errText(r, '导出失败')); 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 /* advanced cli */\n function findHelpEntry(groups, ref) {\n var seg = String(ref).split(' ');\n for (var i = 0; i < groups.length; i++) {\n var cmds = groups[i].commands || [];\n for (var j = 0; j < cmds.length; j++) {\n var c = cmds[j];\n if (c.name !== seg[0]) continue;\n if (seg.length === 1) return c;\n var subs = c.subcommands || [];\n for (var k = 0; k < subs.length; k++) if (subs[k].name === seg[1]) return subs[k];\n }\n }\n return null;\n }\n function copyText(text) {\n function fallback() {\n var ta = document.createElement('textarea');\n ta.value = text;\n ta.style.position = 'fixed';\n ta.style.top = '-1000px';\n document.body.appendChild(ta);\n ta.select();\n var ok = false;\n try { ok = document.execCommand('copy'); } catch (e) {}\n ta.remove();\n return ok;\n }\n if (navigator.clipboard && navigator.clipboard.writeText) {\n return navigator.clipboard.writeText(text).then(function () { return true; }, function () { return fallback(); });\n }\n return Promise.resolve(fallback());\n }\n function copyBtn(text) {\n return '<button class=\"ibtn copy-btn\" data-copy=\"' + esc(text) + '\" title=\"复制命令\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\"/><path d=\"M5 15V5a2 2 0 0 1 2-2h10\"/></svg></button>';\n }\n function codeLine(text) {\n return '<div class=\"code-line\"><code>' + esc(text) + '</code>' + copyBtn(text) + '</div>';\n }\n function optRows(options) {\n return (options || []).map(function (o) {\n return '<div class=\"opt-row\"><div class=\"opt-flag\">' + esc(o.flag + (o.arg ? ' ' + o.arg : '')) + '</div><div>' +\n '<div class=\"opt-what\">' + esc(o.what) + '</div>' +\n (o.when ? '<div class=\"opt-when\">' + esc(o.when) + '</div>' : '') +\n (o.caution ? '<div class=\"opt-risk\">' + esc(o.caution) + '</div>' : '') +\n '</div></div>';\n }).join('');\n }\n function renderQuick() {\n if (!state.help) return;\n var d = state.help;\n $('#cliQuick').innerHTML = (d.quick || []).map(function (g) {\n var items = (g.items || []).map(function (it) {\n var meta = findHelpEntry(d.groups, it.cmd) || {};\n return '<div class=\"cmd-item\"><div class=\"cmd-head\"><code class=\"cmd-name\">' + esc(it.cmd) + '</code>' +\n (it.danger ? '<span class=\"badge danger-badge\">危险</span>' : '') + '</div>' +\n '<div class=\"cmd-what\">' + esc(meta.what || '') + '</div>' +\n '<div class=\"cmd-when\">' + esc(meta.when || '') + '</div>' +\n codeLine(it.example) +\n (it.note ? '<div class=\"cmd-note\">' + esc(it.note) + '</div>' : '') + '</div>';\n }).join('');\n return '<div class=\"quick-group\"><h4>' + esc(g.group) + '</h4><div class=\"quick-list\">' + items + '</div></div>';\n }).join('');\n }\n function renderFull() {\n if (!state.help) return;\n var groupsHtml = (state.help.groups || []).map(function (g) {\n var cmds = (g.commands || []).map(function (c) {\n var subs = (c.subcommands || []).map(function (s) {\n return '<div class=\"help-sub\"><div class=\"cmd-head\"><code class=\"cmd-name\">' + esc(c.name + ' ' + s.name) + '</code></div>' +\n (s.what ? '<div class=\"cmd-what\">' + esc(s.what) + '</div>' : '') +\n (s.when ? '<div class=\"cmd-when\">' + esc(s.when) + '</div>' : '') +\n codeLine('yotta-memory ' + c.name + ' ' + s.usage) + optRows(s.options) + '</div>';\n }).join('');\n return '<details class=\"help-cmd\"><summary><code class=\"cmd-name\">' + esc(c.name) + '</code><span class=\"cmd-what\">' + esc(c.what) + '</span></summary>' +\n '<div class=\"help-body\">' + (c.when ? '<div class=\"cmd-when\">' + esc(c.when) + '</div>' : '') +\n codeLine('yotta-memory ' + c.usage) + optRows(c.options) + subs + '</div></details>';\n }).join('');\n return '<div class=\"quick-group\"><h4>' + esc(g.group) + '</h4>' + cmds + '</div>';\n }).join('');\n var keys = state.help.configKeys || [];\n var keyHelp = state.help.configKeyHelp || {};\n var keysHtml = keys.length ? '<div class=\"quick-group\" id=\"cliConfigKeys\"><h4>配置键(config set)</h4>' +\n '<div class=\"cmd-when\">全部可配置键(共 ' + keys.length + ' 个)。用法:yotta-memory config set &lt;键&gt; &lt;值&gt;,改完用 yotta-memory config get 复查;配置保存在 ~/.yottamemory/config.json。</div>' +\n keys.map(function (k) {\n var h = keyHelp[k] || {};\n return '<div class=\"opt-row with-copy\"><div class=\"opt-flag\">' + esc(k) + '</div><div>' +\n '<div class=\"opt-what\">' + esc(h.what || '') + '</div>' +\n (h.when ? '<div class=\"opt-when\">' + esc(h.when) + '</div>' : '') +\n '</div>' + copyBtn('yotta-memory config set ' + k + ' <值>') + '</div>';\n }).join('') + '</div>' : '';\n $('#cliFull').innerHTML = groupsHtml + keysHtml;\n }\n async function ensureHelp() {\n if (state.help) { renderQuick(); return; }\n var d = await api('/api/help');\n if (!d || d.error) { toast(errText(d, '帮助加载失败')); return; }\n state.help = d;\n renderQuick();\n }\n $('#btnFullHelp').addEventListener('click', async function () {\n if (!state.help) await ensureHelp();\n if (!state.help) return;\n renderFull();\n $('#cliFull').hidden = false;\n $('#btnFullHelp').hidden = true;\n });\n document.addEventListener('click', function (e) {\n var b = e.target.closest('.copy-btn[data-copy]');\n if (!b) return;\n copyText(b.dataset.copy).then(function (ok) { toast(ok ? '已复制' : '复制失败,请手动选择'); });\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";
390
+ 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<link rel=\"icon\" href=\"data:,\">\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 .owner-pick{display:flex;flex-wrap:wrap;gap:6px;max-height:132px;overflow:auto;margin-bottom:8px}\n .owner-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:999px;padding:5px 10px;font-size:12px}\n .owner-chip:hover{color:var(--text);border-color:var(--line2)}\n .owner-chip.active{color:var(--gold);border-color:var(--line2);background:rgba(240,161,58,.12)}\n .owner-chip .tag{font-size:10px;color:var(--faint);border:1px solid var(--line);border-radius:999px;padding:0 6px}\n .owner-chip.active .tag{color:var(--gold);border-color:var(--line2)}\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 #drawerMask{z-index:40}\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 /* advanced cli */\n .quick-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}\n .cmd-item{border:1px solid var(--line);border-radius:8px;padding:12px 14px;background:var(--panel2);display:flex;flex-direction:column;gap:8px;min-width:0}\n .cmd-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n .cmd-name{font-family:ui-monospace,Consolas,monospace;font-size:12px;color:var(--gold)}\n .cmd-what{font-weight:600}\n .cmd-when{color:var(--muted);font-size:12px}\n .code-line{display:flex;align-items:center;gap:8px;background:rgba(0,0,0,.28);border:1px solid var(--line);border-radius:6px;padding:6px 10px;min-width:0}\n body.light .code-line{background:rgba(56,42,28,.06)}\n .code-line code{flex:1;min-width:0;font-family:ui-monospace,Consolas,monospace;font-size:12px;overflow-x:auto;white-space:nowrap}\n .danger-badge{color:var(--danger);border-color:rgba(224,112,112,.5);background:rgba(224,112,112,.10)}\n .cmd-note{color:var(--danger);font-size:12px}\n .quick-group{margin-top:14px}\n .quick-group h4{margin:0 0 8px;font-size:13px;color:var(--muted)}\n .help-cmd{border:1px solid var(--line);border-radius:8px;background:var(--panel2);margin-top:10px;min-width:0}\n .help-cmd summary{cursor:pointer;padding:10px 14px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;list-style:none}\n .help-cmd summary::-webkit-details-marker{display:none}\n .help-cmd summary::before{content:'';width:6px;height:6px;border-right:1.6px solid var(--muted);border-bottom:1.6px solid var(--muted);transform:rotate(-45deg);transition:transform .15s;flex:none}\n .help-cmd[open] summary::before{transform:rotate(45deg)}\n .help-body{padding:12px 14px;display:flex;flex-direction:column;gap:8px;border-top:1px solid var(--line)}\n .help-sub{border-top:1px solid var(--line);padding-top:8px;margin-top:2px;display:flex;flex-direction:column;gap:6px}\n .help-sub .cmd-name{color:var(--text)}\n .opt-row{display:grid;grid-template-columns:minmax(150px,auto) minmax(0,1fr);gap:2px 12px;padding:7px 0;border-top:1px solid var(--line);font-size:12px}\n .opt-row:first-child{border-top:0}\n .opt-flag{font-family:ui-monospace,Consolas,monospace;word-break:break-all}\n .opt-what{color:var(--muted)}\n .opt-when{color:var(--faint)}\n .opt-risk{color:var(--danger)}\n .opt-row.with-copy{grid-template-columns:minmax(150px,auto) minmax(0,1fr) auto;align-items:start}\n .opt-row.with-copy .ibtn{width:26px;height:26px}\n\n @media (max-width:960px){.g4{grid-template-columns:repeat(2,minmax(0,1fr))}.bars{grid-template-columns:repeat(2,minmax(0,1fr))}.quick-list{grid-template-columns: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 <button data-view=\"cli\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"m4 17 6-6-6-6\"/><path d=\"M12 19h8\"/></svg>高级 / CLI</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 <select id=\"ownerFilter\" title=\"按智能体筛选\" style=\"width:auto;min-width:140px\"><option value=\"\">全部智能体</option></select>\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\n <section class=\"view\" id=\"view-cli\">\n <div class=\"card\">\n <h3>常用命令速查</h3>\n <div class=\"faint\" style=\"margin-bottom:12px\">只展示与复制;命令都在终端执行,危险操作已标注。</div>\n <div id=\"cliQuick\"><div class=\"faint\">加载中…</div></div>\n </div>\n <div class=\"section card\">\n <h3>完整命令与参数</h3>\n <div class=\"faint\" style=\"margin-bottom:12px\">与当前安装版本一致(随版本自动更新);按需加载,只读展示,不执行任何命令。</div>\n <button class=\"btn\" id=\"btnFullHelp\">加载完整命令与参数</button>\n <div id=\"cliFull\" hidden></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', owner: '', owners: [], overview: null, current: null, help: 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 errText(r, fallback) {\n return r && r.error ? ((r.text || r.message || fallback) || '操作失败') : '';\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 pad2(n) { return (n < 10 ? '0' : '') + n; }\n function fmtTime(e) {\n if (e && e.mtime) {\n var d = new Date(e.mtime);\n if (!isNaN(d.getTime())) {\n return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());\n }\n }\n return (e && (e.updated || e.created)) || '';\n }\n // v0.22.8:卡片时间语义显式化——「创建」用 frontmatter created(文件系统 btime 兜底),「更新」用文件 mtime(打点已不再改写它)。\n function metaLine(e, withFile) {\n var parts = [e.owner || '公共'];\n if (e.created) parts.push('创建 ' + e.created);\n var up = fmtTime(e);\n if (up) parts.push('更新 ' + up);\n if (withFile) parts.push(e.file);\n return parts.join(' · ');\n }\n function applyTypeCounts(typeCounts) {\n Array.prototype.forEach.call(document.querySelectorAll('#typeChips .chip'), function (c) {\n c.textContent = (c.dataset.type === 'all' ? '全部' : c.dataset.type) + ' ' + (typeCounts[c.dataset.type] || 0);\n });\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: ['设置', '口令 / 备份 / 导出'], cli: ['高级 / CLI', '常用命令速查与完整帮助(只展示与复制)'] };\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 if (name === 'cli') ensureHelp();\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 var byType = d.byType || {};\n var typeCounts = { all: d.total || 0, FACT: byType.FACT || 0, PREF: byType.PREF || 0, BOUND: byType.BOUND || 0, COMMIT: byType.COMMIT || 0 };\n $('#typeBars').innerHTML = ['FACT', 'PREF', 'BOUND', 'COMMIT'].map(function (t) {\n return '<div class=\"bar\" data-type=\"' + t + '\" style=\"cursor:pointer\" title=\"查看 ' + typeNames[t] + '\"><span class=\"dot\" style=\"background:' + colors[t] + '\"></span><span class=\"faint\">' + typeNames[t] + '</span><b>' + typeCounts[t] + '</b></div>';\n }).join('');\n applyTypeCounts(typeCounts);\n }\n async function loadOwners() {\n var d = await api('/api/owners');\n state.owners = (d && d.owners) || [];\n var sel = $('#ownerFilter');\n if (sel) {\n sel.innerHTML = '<option value=\"\">全部智能体</option>' + state.owners.map(function (o) {\n return '<option value=\"' + esc(o.owner) + '\"' + (o.owner === state.owner ? ' selected' : '') + '>' + esc(o.owner) + '</option>';\n }).join('');\n }\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, owner: state.owner || '' });\n if (d.error) { toast(errText(d, '加载失败')); return; }\n state.entries = d.entries || [];\n state.count = d.count || 0;\n // v0.22.8:chips 计数跟随当前智能体 / 搜索筛选(概览页无筛选时即全局口径)。\n if (d.counts) applyTypeCounts(d.counts);\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(metaLine(e, true)) + '</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(metaLine(e, false)) + '</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] + ' · ' + metaLine(entry, false);\n $('#drBody').textContent = entry.statement || '';\n $('#drKv').innerHTML = '<dt>创建</dt><dd>' + esc(entry.created || (entry.btime ? fmtTime({ mtime: entry.btime }) : '-')) + '</dd>' +\n '<dt>最近更新</dt><dd>' + esc(fmtTime(entry) || entry.updated || '-') + '</dd>' +\n '<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 var ownerLocked = !isNew && t !== '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>' + (t === 'FACT' && !isNew ? '归属 AI(FACT 为公共记忆,归属仅作来源标记,可修改)' : '归属 AI(私密记忆必填;点选或输入自定义 ID)') + '</label>' +\n '<div class=\"owner-pick\" id=\"fOwnerPick\">' + (state.owners.length ? state.owners.map(function (o) {\n return '<button type=\"button\" class=\"owner-chip' + ((entry && entry.owner === o.owner) ? ' active' : '') + '\"' + (ownerLocked ? ' disabled' : '') + ' data-owner=\"' + esc(o.owner) + '\">' + esc(o.owner) + '<span class=\"tag\">' + (o.authorized ? '已授权' : '未授权') + '</span></button>';\n }).join('') : '<span class=\"faint\">还没有已登记的 AI;可直接输入自定义 ID。</span>') + '</div>' +\n '<input id=\"fOwner\" placeholder=\"自定义 AI ID(如 codex)\" value=\"' + esc(entry && entry.owner || '') + '\"' + (ownerLocked ? ' disabled' : '') + '></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 ? '' : (t === 'FACT'\n ? '<div class=\"faint\">类型不可修改;FACT 为公共记忆,归属仅作来源标记,可修改。</div>'\n : '<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 bindOwnerPicker() {\n var pick = $('#fOwnerPick');\n if (!pick || ($('#fOwner') && $('#fOwner').disabled)) return;\n pick.addEventListener('click', function (e) {\n var chip = e.target.closest('.owner-chip');\n if (!chip) return;\n Array.prototype.forEach.call(pick.querySelectorAll('.owner-chip'), function (c) { c.classList.toggle('active', c === chip); });\n $('#fOwner').value = chip.dataset.owner;\n });\n $('#fOwner').addEventListener('input', function () {\n var v = $('#fOwner').value.trim();\n Array.prototype.forEach.call(pick.querySelectorAll('.owner-chip'), function (c) { c.classList.toggle('active', c.dataset.owner === v); });\n });\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 body = { file: entry.file, subject: subject, statement: statement, tags: tags };\n var type = String(entry.type || 'FACT').toUpperCase();\n if (type === 'FACT' && $('#fOwner') && !$('#fOwner').disabled) body.owner = $('#fOwner').value.trim();\n var r = await api('/api/memory/update', body);\n if (r.error) { toast(errText(r, '保存失败')); return; }\n closeModal();\n toast('已更新 1 条记忆' + (r.ownerChange ? '(归属已改为 ' + (r.ownerChange.to || '公共') + ')' : ''), '撤销', async function () {\n var u = await api('/api/memory/restore', { ref: r.undo });\n toast(errText(u, '撤销失败') || '已撤销这次编辑');\n refreshAll();\n });\n refreshAll();\n },\n });\n bindOwnerPicker();\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(errText(r, '创建失败')); return; }\n closeModal();\n toast('已创建 1 条记忆');\n refreshAll();\n },\n });\n bindOwnerPicker();\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(errText(r, '删除失败')); return; }\n closeModal();\n toast('已删除 1 条记忆', '撤销', async function () {\n var u = await api('/api/memory/restore', { ref: r.trash });\n toast(errText(u, '恢复失败') || '已恢复');\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(errText(r, '归档失败')); return; }\n closeModal();\n toast('已藏起来 1 条', '放回', async function () {\n var u = await api('/api/memory/unarchive', { archive: r.archive });\n toast(errText(u, '放回失败') || '已放回');\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(errText(r, '放回失败') || '已放回');\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(errText(r, '恢复失败') || '已恢复');\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 $('#typeBars').addEventListener('click', function (e) {\n var bar = e.target.closest('.bar[data-type]');\n if (!bar) return;\n state.type = bar.dataset.type; state.offset = 0;\n Array.prototype.forEach.call(document.querySelectorAll('#typeChips .chip'), function (c) { c.classList.toggle('active', c.dataset.type === state.type); });\n switchView('memories');\n loadEntries();\n });\n $('#ownerFilter').addEventListener('change', function () { state.owner = this.value; state.offset = 0; loadEntries(); });\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(errText(r, '吊销失败') || '已吊销'); 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(errText(r, '授权失败')); 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(errText(r, '删除失败') || '已删除'); 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 || '重设失败') : (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(errText(r, '恢复钥匙读取失败')); 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(errText(r, '导出失败')); 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 /* advanced cli */\n function findHelpEntry(groups, ref) {\n var seg = String(ref).split(' ');\n for (var i = 0; i < groups.length; i++) {\n var cmds = groups[i].commands || [];\n for (var j = 0; j < cmds.length; j++) {\n var c = cmds[j];\n if (c.name !== seg[0]) continue;\n if (seg.length === 1) return c;\n var subs = c.subcommands || [];\n for (var k = 0; k < subs.length; k++) if (subs[k].name === seg[1]) return subs[k];\n }\n }\n return null;\n }\n function copyText(text) {\n function fallback() {\n var ta = document.createElement('textarea');\n ta.value = text;\n ta.style.position = 'fixed';\n ta.style.top = '-1000px';\n document.body.appendChild(ta);\n ta.select();\n var ok = false;\n try { ok = document.execCommand('copy'); } catch (e) {}\n ta.remove();\n return ok;\n }\n if (navigator.clipboard && navigator.clipboard.writeText) {\n return navigator.clipboard.writeText(text).then(function () { return true; }, function () { return fallback(); });\n }\n return Promise.resolve(fallback());\n }\n function copyBtn(text) {\n return '<button class=\"ibtn copy-btn\" data-copy=\"' + esc(text) + '\" title=\"复制命令\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\"/><path d=\"M5 15V5a2 2 0 0 1 2-2h10\"/></svg></button>';\n }\n function codeLine(text) {\n return '<div class=\"code-line\"><code>' + esc(text) + '</code>' + copyBtn(text) + '</div>';\n }\n function optRows(options) {\n return (options || []).map(function (o) {\n return '<div class=\"opt-row\"><div class=\"opt-flag\">' + esc(o.flag + (o.arg ? ' ' + o.arg : '')) + '</div><div>' +\n '<div class=\"opt-what\">' + esc(o.what) + '</div>' +\n (o.when ? '<div class=\"opt-when\">' + esc(o.when) + '</div>' : '') +\n (o.caution ? '<div class=\"opt-risk\">' + esc(o.caution) + '</div>' : '') +\n '</div></div>';\n }).join('');\n }\n function renderQuick() {\n if (!state.help) return;\n var d = state.help;\n $('#cliQuick').innerHTML = (d.quick || []).map(function (g) {\n var items = (g.items || []).map(function (it) {\n var meta = findHelpEntry(d.groups, it.cmd) || {};\n return '<div class=\"cmd-item\"><div class=\"cmd-head\"><code class=\"cmd-name\">' + esc(it.cmd) + '</code>' +\n (it.danger ? '<span class=\"badge danger-badge\">危险</span>' : '') + '</div>' +\n '<div class=\"cmd-what\">' + esc(meta.what || '') + '</div>' +\n '<div class=\"cmd-when\">' + esc(meta.when || '') + '</div>' +\n codeLine(it.example) +\n (it.note ? '<div class=\"cmd-note\">' + esc(it.note) + '</div>' : '') + '</div>';\n }).join('');\n return '<div class=\"quick-group\"><h4>' + esc(g.group) + '</h4><div class=\"quick-list\">' + items + '</div></div>';\n }).join('');\n }\n function renderFull() {\n if (!state.help) return;\n var keys = state.help.configKeys || [];\n var keyHelp = state.help.configKeyHelp || {};\n var keysHtml = keys.length ? '<div class=\"quick-group\" id=\"cliConfigKeys\"><h4>配置键(config set)</h4>' +\n '<div class=\"cmd-when\">全部可配置键(共 ' + keys.length + ' 个)。用法:yotta-memory config set &lt;键&gt; &lt;值&gt;,改完用 yotta-memory config get 复查;配置保存在 ~/.yottamemory/config.json。</div>' +\n keys.map(function (k) {\n var h = keyHelp[k] || {};\n return '<div class=\"opt-row with-copy\"><div class=\"opt-flag\">' + esc(k) + '</div><div>' +\n '<div class=\"opt-what\">' + esc(h.what || '') + '</div>' +\n (h.when ? '<div class=\"opt-when\">' + esc(h.when) + '</div>' : '') +\n '</div>' + copyBtn('yotta-memory config set ' + k + ' <值>') + '</div>';\n }).join('') + '</div>' : '';\n // UX-M-01:配置键块紧随 config 命令(同组相邻);找不到 config 时回退末尾。\n var keysPlaced = false;\n var groupsHtml = (state.help.groups || []).map(function (g) {\n var cmds = (g.commands || []).map(function (c) {\n var subs = (c.subcommands || []).map(function (s) {\n return '<div class=\"help-sub\"><div class=\"cmd-head\"><code class=\"cmd-name\">' + esc(c.name + ' ' + s.name) + '</code></div>' +\n (s.what ? '<div class=\"cmd-what\">' + esc(s.what) + '</div>' : '') +\n (s.when ? '<div class=\"cmd-when\">' + esc(s.when) + '</div>' : '') +\n codeLine('yotta-memory ' + c.name + ' ' + s.usage) + optRows(s.options) + '</div>';\n }).join('');\n var cmdHtml = '<details class=\"help-cmd\"><summary><code class=\"cmd-name\">' + esc(c.name) + '</code><span class=\"cmd-what\">' + esc(c.what) + '</span></summary>' +\n '<div class=\"help-body\">' + (c.when ? '<div class=\"cmd-when\">' + esc(c.when) + '</div>' : '') +\n codeLine('yotta-memory ' + c.usage) + optRows(c.options) + subs + '</div></details>';\n if (!keysPlaced && c.name === 'config' && keysHtml) {\n keysPlaced = true;\n return cmdHtml + keysHtml;\n }\n return cmdHtml;\n }).join('');\n return '<div class=\"quick-group\"><h4>' + esc(g.group) + '</h4>' + cmds + '</div>';\n }).join('');\n $('#cliFull').innerHTML = groupsHtml + (keysPlaced ? '' : keysHtml);\n }\n async function ensureHelp() {\n if (state.help) { renderQuick(); return; }\n var d = await api('/api/help');\n if (!d || d.error) { toast(errText(d, '帮助加载失败')); return; }\n state.help = d;\n renderQuick();\n }\n $('#btnFullHelp').addEventListener('click', async function () {\n if (!state.help) await ensureHelp();\n if (!state.help) return;\n renderFull();\n $('#cliFull').hidden = false;\n $('#btnFullHelp').hidden = true;\n });\n document.addEventListener('click', function (e) {\n var b = e.target.closest('.copy-btn[data-copy]');\n if (!b) return;\n copyText(b.dataset.copy).then(function (ok) { toast(ok ? '已复制' : '复制失败,请手动选择'); });\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";
391
391
  // @generated view-html:end
392
392
  // MCP 协议(2026-07-28 无状态 + 2025-11-25 legacy 握手,dual-era)
393
393
  const MCP_PROTOCOL_MODERN = '2026-07-28';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-memory",
3
- "version": "0.22.8",
3
+ "version": "0.22.9",
4
4
  "description": "Yuanyi (元忆) — boundary-aware, file-based memory for AI agents. File-based, zero-dependency, diff/rollback-able; FACT/PREF/BOUND/COMMIT types (public shared / private isolated), user-level + project-level storage; v0.12 reliability baseline (init guard, trash-based deletion, independent-volume backup/list/doctor/restore, start-of-work doctor, transactional snapshots before destructive writes), v0.10 consolidation (periodic summaries with provenance, near-duplicate auto-merge, per-type decay, batch audit + rollback), v0.9 recall quality + context focus + optional local embedding plugin, plus v0.8 semantic search, feedback loop, self-organization and distillation.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -3,7 +3,7 @@
3
3
  "slug": "yotta-memory",
4
4
  "name": "元忆",
5
5
  "package": "@yottameta/yotta-memory",
6
- "version": "0.22.8",
6
+ "version": "0.22.9",
7
7
  "trust": "yottameta",
8
8
  "install": {
9
9
  "idempotent": true