@yottameta/yotta-memory 0.21.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/SKILL.md +1 -1
- package/USER_GUIDE.md +1 -1
- package/assets/view.html +150 -3
- package/bin/yotta-memory.js +54 -2
- package/package.json +1 -1
- package/skill-manifest.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## v0.22.0 (2026-10-03)
|
|
2
|
+
|
|
3
|
+
- 记忆管理台新增「高级 / CLI」页:左侧菜单入口 + 高频命令分组速查(备份恢复 / 维护瘦身 / 检索上下文 / 扫描安全 / 导入导出 / 身份权限 / 运行时与局域网),每条 = 人话说明 + 可复制示例 + 危险操作标记;「完整命令与参数」按需加载,直接渲染引擎帮助模型(`HELP_MODEL` 单一真源,随安装版本自动更新)。
|
|
4
|
+
- 页面只展示与复制,不执行任何命令;新增只读 `POST /api/help`(需先解锁),不引入命令执行面。
|
|
5
|
+
- 新增 `test/view-cli-help.test.js`(4 项:未解锁 401 / 帮助模型与速查一致性 / 页面结构与复制入口 / 无执行端点);全量 `npm test` 327/327。
|
|
6
|
+
|
|
1
7
|
## v0.21.1 (2026-10-02)
|
|
2
8
|
|
|
3
9
|
- 管理台「新建 / 编辑记忆」的归属 AI 选择改为平铺选择列表:一次显示全部已登记 AI(带已授权 / 未授权标记),并保留自定义 ID 输入;修复原生 datalist 弹层一次只显示约 4 项、容易被误认为 AI 缺失的问题。
|
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.
|
|
4
|
+
version: 0.22.0
|
|
5
5
|
license: MIT
|
|
6
6
|
---
|
|
7
7
|
|
package/USER_GUIDE.md
CHANGED
|
@@ -170,7 +170,7 @@ yotta-memory recall <关键词> --agent <id> --agent-key-file "<AI_HOME>/.yotta-
|
|
|
170
170
|
|
|
171
171
|
**用户查看平台(看所有 AI 的记忆)**
|
|
172
172
|
|
|
173
|
-
`yotta-memory view` → 浏览器打开 http://127.0.0.1:8788 → 输入主口令解锁 → 浏览 / 搜索 / 导出全部记忆(含各 AI 私密);也可在此**授权 / 吊销**某 AI
|
|
173
|
+
`yotta-memory view` → 浏览器打开 http://127.0.0.1:8788 → 输入主口令解锁 → 浏览 / 搜索 / 导出全部记忆(含各 AI 私密);也可在此**授权 / 吊销**某 AI 读取其私密、**重设口令**、**查看恢复钥匙**。左侧菜单「高级 / CLI」页提供常用命令速查(危险操作已标注、示例可一键复制)与完整命令 / 参数帮助(按需加载,只读展示、不执行命令)。空加密库没有 owner key 时,`view` 会用恢复钥匙校验主口令进入平台;此时先在终端执行 `yotta-memory iam <id>`,页面才会出现可授权的 owner。`view` 启动前会做端口健康检查:已在运行则直接打印 URL 复用;端口被占用会给出明确提示。点击「授权」后会弹窗展示只显示一次的 `agent_key`,请立即单独保存;引擎同时写临时 `keys/pending/<id>.key`,该 AI 新会话用 `key claim` 领取到自己的宿主目录,领取成功后 pending 删除。已绑定的 agent 需先「吊销」再授权,旧 key 随即校验失败。口令只在本地内存派生,不落盘、不发远端;默认仅本机,远程需 `--host` 显式开启。
|
|
174
174
|
|
|
175
175
|
**AI 接入加密库**
|
|
176
176
|
|
package/assets/view.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<meta name="robots" content="noindex,nofollow">
|
|
7
|
+
<link rel="icon" href="data:,">
|
|
7
8
|
<title>元忆 · 记忆管理台</title>
|
|
8
9
|
<style>
|
|
9
10
|
:root{--bg:#0f0c12;--bg2:#141019;--panel:#191420;--panel2:#1f1828;--line:rgba(255,217,138,.12);--line2:rgba(255,217,138,.24);
|
|
@@ -128,7 +129,36 @@
|
|
|
128
129
|
.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)}
|
|
129
130
|
.list-plain .item .spacer{flex:1}
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
/* advanced cli */
|
|
133
|
+
.quick-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
|
|
134
|
+
.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}
|
|
135
|
+
.cmd-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
136
|
+
.cmd-name{font-family:ui-monospace,Consolas,monospace;font-size:12px;color:var(--gold)}
|
|
137
|
+
.cmd-what{font-weight:600}
|
|
138
|
+
.cmd-when{color:var(--muted);font-size:12px}
|
|
139
|
+
.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}
|
|
140
|
+
body.light .code-line{background:rgba(56,42,28,.06)}
|
|
141
|
+
.code-line code{flex:1;min-width:0;font-family:ui-monospace,Consolas,monospace;font-size:12px;overflow-x:auto;white-space:nowrap}
|
|
142
|
+
.danger-badge{color:var(--danger);border-color:rgba(224,112,112,.5);background:rgba(224,112,112,.10)}
|
|
143
|
+
.cmd-note{color:var(--danger);font-size:12px}
|
|
144
|
+
.quick-group{margin-top:14px}
|
|
145
|
+
.quick-group h4{margin:0 0 8px;font-size:13px;color:var(--muted)}
|
|
146
|
+
.help-cmd{border:1px solid var(--line);border-radius:8px;background:var(--panel2);margin-top:10px;min-width:0}
|
|
147
|
+
.help-cmd summary{cursor:pointer;padding:10px 14px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;list-style:none}
|
|
148
|
+
.help-cmd summary::-webkit-details-marker{display:none}
|
|
149
|
+
.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}
|
|
150
|
+
.help-cmd[open] summary::before{transform:rotate(45deg)}
|
|
151
|
+
.help-body{padding:12px 14px;display:flex;flex-direction:column;gap:8px;border-top:1px solid var(--line)}
|
|
152
|
+
.help-sub{border-top:1px solid var(--line);padding-top:8px;margin-top:2px;display:flex;flex-direction:column;gap:6px}
|
|
153
|
+
.help-sub .cmd-name{color:var(--text)}
|
|
154
|
+
.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}
|
|
155
|
+
.opt-row:first-child{border-top:0}
|
|
156
|
+
.opt-flag{font-family:ui-monospace,Consolas,monospace;word-break:break-all}
|
|
157
|
+
.opt-what{color:var(--muted)}
|
|
158
|
+
.opt-when{color:var(--faint)}
|
|
159
|
+
.opt-risk{color:var(--danger)}
|
|
160
|
+
|
|
161
|
+
@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)}}
|
|
132
162
|
@media (max-width:760px){
|
|
133
163
|
.shell{grid-template-columns:minmax(0,1fr)}
|
|
134
164
|
.sidebar{position:static;height:auto;padding:14px;gap:12px}
|
|
@@ -170,6 +200,7 @@
|
|
|
170
200
|
<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>
|
|
171
201
|
<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>
|
|
172
202
|
<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>
|
|
203
|
+
<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>
|
|
173
204
|
</nav>
|
|
174
205
|
<div class="side-foot">
|
|
175
206
|
<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>
|
|
@@ -267,6 +298,20 @@
|
|
|
267
298
|
<div style="margin-top:10px"><button class="btn danger small" id="btnDangerHint">在哪里执行?</button></div>
|
|
268
299
|
</div>
|
|
269
300
|
</section>
|
|
301
|
+
|
|
302
|
+
<section class="view" id="view-cli">
|
|
303
|
+
<div class="card">
|
|
304
|
+
<h3>常用命令速查</h3>
|
|
305
|
+
<div class="faint" style="margin-bottom:12px">只展示与复制;命令都在终端执行,危险操作已标注。</div>
|
|
306
|
+
<div id="cliQuick"><div class="faint">加载中…</div></div>
|
|
307
|
+
</div>
|
|
308
|
+
<div class="section card">
|
|
309
|
+
<h3>完整命令与参数</h3>
|
|
310
|
+
<div class="faint" style="margin-bottom:12px">与当前安装版本一致(随版本自动更新);按需加载,只读展示,不执行任何命令。</div>
|
|
311
|
+
<button class="btn" id="btnFullHelp">加载完整命令与参数</button>
|
|
312
|
+
<div id="cliFull" hidden></div>
|
|
313
|
+
</div>
|
|
314
|
+
</section>
|
|
270
315
|
</main>
|
|
271
316
|
</div>
|
|
272
317
|
|
|
@@ -284,7 +329,7 @@
|
|
|
284
329
|
(function () {
|
|
285
330
|
'use strict';
|
|
286
331
|
var $ = function (s) { return document.querySelector(s); };
|
|
287
|
-
var state = { view: 'overview', entries: [], count: 0, offset: 0, limit: 20, query: '', type: 'all', owners: [], overview: null, current: null };
|
|
332
|
+
var state = { view: 'overview', entries: [], count: 0, offset: 0, limit: 20, query: '', type: 'all', owners: [], overview: null, current: null, help: null };
|
|
288
333
|
var typeNames = { FACT: 'FACT 事实', PREF: 'PREF 偏好', BOUND: 'BOUND 边界', COMMIT: 'COMMIT 承诺' };
|
|
289
334
|
|
|
290
335
|
function esc(s) {
|
|
@@ -359,7 +404,7 @@
|
|
|
359
404
|
$('#drawerMask').addEventListener('click', closeDrawer);
|
|
360
405
|
|
|
361
406
|
/* nav */
|
|
362
|
-
var titles = { overview: ['概览', '你的 AI 记住了什么,一眼看清'], memories: ['记忆', '看 / 改 / 删 / 藏,全在这里'], access: ['AI 与权限', '默认拒绝,按 AI 隔离'], settings: ['设置', '口令 / 备份 / 导出'] };
|
|
407
|
+
var titles = { overview: ['概览', '你的 AI 记住了什么,一眼看清'], memories: ['记忆', '看 / 改 / 删 / 藏,全在这里'], access: ['AI 与权限', '默认拒绝,按 AI 隔离'], settings: ['设置', '口令 / 备份 / 导出'], cli: ['高级 / CLI', '常用命令速查与完整帮助(只展示与复制)'] };
|
|
363
408
|
function switchView(name) {
|
|
364
409
|
state.view = name;
|
|
365
410
|
Array.prototype.forEach.call(document.querySelectorAll('#nav button'), function (b) { b.classList.toggle('active', b.dataset.view === name); });
|
|
@@ -367,6 +412,7 @@
|
|
|
367
412
|
$('#pageTitle').textContent = titles[name][0];
|
|
368
413
|
$('#pageSub').textContent = titles[name][1];
|
|
369
414
|
window.scrollTo(0, 0);
|
|
415
|
+
if (name === 'cli') ensureHelp();
|
|
370
416
|
}
|
|
371
417
|
$('#nav').addEventListener('click', function (e) { var b = e.target.closest('button'); if (b) switchView(b.dataset.view); });
|
|
372
418
|
Array.prototype.forEach.call(document.querySelectorAll('[data-goto]'), function (b) { b.addEventListener('click', function () { switchView(b.dataset.goto); }); });
|
|
@@ -682,6 +728,107 @@
|
|
|
682
728
|
toast('在「AI 与权限」列表里,每个 AI 右侧有「删除」按钮');
|
|
683
729
|
});
|
|
684
730
|
|
|
731
|
+
/* advanced cli */
|
|
732
|
+
function findHelpEntry(groups, ref) {
|
|
733
|
+
var seg = String(ref).split(' ');
|
|
734
|
+
for (var i = 0; i < groups.length; i++) {
|
|
735
|
+
var cmds = groups[i].commands || [];
|
|
736
|
+
for (var j = 0; j < cmds.length; j++) {
|
|
737
|
+
var c = cmds[j];
|
|
738
|
+
if (c.name !== seg[0]) continue;
|
|
739
|
+
if (seg.length === 1) return c;
|
|
740
|
+
var subs = c.subcommands || [];
|
|
741
|
+
for (var k = 0; k < subs.length; k++) if (subs[k].name === seg[1]) return subs[k];
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
function copyText(text) {
|
|
747
|
+
function fallback() {
|
|
748
|
+
var ta = document.createElement('textarea');
|
|
749
|
+
ta.value = text;
|
|
750
|
+
ta.style.position = 'fixed';
|
|
751
|
+
ta.style.top = '-1000px';
|
|
752
|
+
document.body.appendChild(ta);
|
|
753
|
+
ta.select();
|
|
754
|
+
var ok = false;
|
|
755
|
+
try { ok = document.execCommand('copy'); } catch (e) {}
|
|
756
|
+
ta.remove();
|
|
757
|
+
return ok;
|
|
758
|
+
}
|
|
759
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
760
|
+
return navigator.clipboard.writeText(text).then(function () { return true; }, function () { return fallback(); });
|
|
761
|
+
}
|
|
762
|
+
return Promise.resolve(fallback());
|
|
763
|
+
}
|
|
764
|
+
function copyBtn(text) {
|
|
765
|
+
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>';
|
|
766
|
+
}
|
|
767
|
+
function codeLine(text) {
|
|
768
|
+
return '<div class="code-line"><code>' + esc(text) + '</code>' + copyBtn(text) + '</div>';
|
|
769
|
+
}
|
|
770
|
+
function optRows(options) {
|
|
771
|
+
return (options || []).map(function (o) {
|
|
772
|
+
return '<div class="opt-row"><div class="opt-flag">' + esc(o.flag + (o.arg ? ' ' + o.arg : '')) + '</div><div>' +
|
|
773
|
+
'<div class="opt-what">' + esc(o.what) + '</div>' +
|
|
774
|
+
(o.when ? '<div class="opt-when">' + esc(o.when) + '</div>' : '') +
|
|
775
|
+
(o.caution ? '<div class="opt-risk">' + esc(o.caution) + '</div>' : '') +
|
|
776
|
+
'</div></div>';
|
|
777
|
+
}).join('');
|
|
778
|
+
}
|
|
779
|
+
function renderQuick() {
|
|
780
|
+
if (!state.help) return;
|
|
781
|
+
var d = state.help;
|
|
782
|
+
$('#cliQuick').innerHTML = (d.quick || []).map(function (g) {
|
|
783
|
+
var items = (g.items || []).map(function (it) {
|
|
784
|
+
var meta = findHelpEntry(d.groups, it.cmd) || {};
|
|
785
|
+
return '<div class="cmd-item"><div class="cmd-head"><code class="cmd-name">' + esc(it.cmd) + '</code>' +
|
|
786
|
+
(it.danger ? '<span class="badge danger-badge">危险</span>' : '') + '</div>' +
|
|
787
|
+
'<div class="cmd-what">' + esc(meta.what || '') + '</div>' +
|
|
788
|
+
'<div class="cmd-when">' + esc(meta.when || '') + '</div>' +
|
|
789
|
+
codeLine(it.example) +
|
|
790
|
+
(it.note ? '<div class="cmd-note">' + esc(it.note) + '</div>' : '') + '</div>';
|
|
791
|
+
}).join('');
|
|
792
|
+
return '<div class="quick-group"><h4>' + esc(g.group) + '</h4><div class="quick-list">' + items + '</div></div>';
|
|
793
|
+
}).join('');
|
|
794
|
+
}
|
|
795
|
+
function renderFull() {
|
|
796
|
+
if (!state.help) return;
|
|
797
|
+
$('#cliFull').innerHTML = (state.help.groups || []).map(function (g) {
|
|
798
|
+
var cmds = (g.commands || []).map(function (c) {
|
|
799
|
+
var subs = (c.subcommands || []).map(function (s) {
|
|
800
|
+
return '<div class="help-sub"><div class="cmd-head"><code class="cmd-name">' + esc(c.name + ' ' + s.name) + '</code></div>' +
|
|
801
|
+
(s.what ? '<div class="cmd-what">' + esc(s.what) + '</div>' : '') +
|
|
802
|
+
(s.when ? '<div class="cmd-when">' + esc(s.when) + '</div>' : '') +
|
|
803
|
+
codeLine('yotta-memory ' + c.name + ' ' + s.usage) + optRows(s.options) + '</div>';
|
|
804
|
+
}).join('');
|
|
805
|
+
return '<details class="help-cmd"><summary><code class="cmd-name">' + esc(c.name) + '</code><span class="cmd-what">' + esc(c.what) + '</span></summary>' +
|
|
806
|
+
'<div class="help-body">' + (c.when ? '<div class="cmd-when">' + esc(c.when) + '</div>' : '') +
|
|
807
|
+
codeLine('yotta-memory ' + c.usage) + optRows(c.options) + subs + '</div></details>';
|
|
808
|
+
}).join('');
|
|
809
|
+
return '<div class="quick-group"><h4>' + esc(g.group) + '</h4>' + cmds + '</div>';
|
|
810
|
+
}).join('');
|
|
811
|
+
}
|
|
812
|
+
async function ensureHelp() {
|
|
813
|
+
if (state.help) { renderQuick(); return; }
|
|
814
|
+
var d = await api('/api/help');
|
|
815
|
+
if (!d || d.error) { toast((d && d.error) || '帮助加载失败'); return; }
|
|
816
|
+
state.help = d;
|
|
817
|
+
renderQuick();
|
|
818
|
+
}
|
|
819
|
+
$('#btnFullHelp').addEventListener('click', async function () {
|
|
820
|
+
if (!state.help) await ensureHelp();
|
|
821
|
+
if (!state.help) return;
|
|
822
|
+
renderFull();
|
|
823
|
+
$('#cliFull').hidden = false;
|
|
824
|
+
$('#btnFullHelp').hidden = true;
|
|
825
|
+
});
|
|
826
|
+
document.addEventListener('click', function (e) {
|
|
827
|
+
var b = e.target.closest('.copy-btn[data-copy]');
|
|
828
|
+
if (!b) return;
|
|
829
|
+
copyText(b.dataset.copy).then(function (ok) { toast(ok ? '已复制' : '复制失败,请手动选择'); });
|
|
830
|
+
});
|
|
831
|
+
|
|
685
832
|
/* unlock / boot */
|
|
686
833
|
$('#unlockBtn').addEventListener('click', async function () {
|
|
687
834
|
var r = await api('/api/unlock', { password: $('#pw').value });
|
package/bin/yotta-memory.js
CHANGED
|
@@ -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.
|
|
28
|
+
const VERSION = '0.22.0';
|
|
29
29
|
const CLI_VALUE_OPTS = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--plugin-data', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--recovery-key-out', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time', '--tools', '--mcp-config', '--skill-dir', '--year', '--evalset', '--k', '--seed', '--bootstrap', '--gate', '--against', '--template', '--path', '--from']);
|
|
30
30
|
const CLI_FLAG_OPTS = new Set(['--project', '--all', '--unsafe', '--no-auth', '--stdio', '--onstart', '--from-current', '--restart', '--force', '--attach', '--allow-same-volume', '--verify', '--no-hint', '--encrypt', '--no-encrypt', '--password-stdin', '--json', '--manual', '--skip-schedule', '--useful', '--useless', '--undo', '--dry-run', '--apply', '--purge', '--dedup', '--batches', '--propose', '--audit', '--capacity', '--rules', '--explain', '--semantic', '--runtime', '--ablate', '--timing', '--baseline', '--probe', '--quarantine', '--restore', '--no-usage', '--yes', '--keep-memories', '--keep-identity']);
|
|
31
31
|
|
|
@@ -305,8 +305,55 @@ const HELP_MODEL = [
|
|
|
305
305
|
{ name: '--version', usage: '--version;短写法 -v', what: '显示当前版本', when: '确认安装的是哪个版本时', options: [] },
|
|
306
306
|
] },
|
|
307
307
|
];
|
|
308
|
+
// v0.22.0:管理台「高级 / CLI」页速查数据。这里只维护「引用哪条命令 + 可复制示例 + 危险标记」,
|
|
309
|
+
// 说明文案统一从 HELP_MODEL 取(单一真源),管理台不再维护第二份参数表。
|
|
310
|
+
const VIEW_HELP_QUICK = [
|
|
311
|
+
{ group: '备份恢复', items: [
|
|
312
|
+
{ cmd: 'backup setup', example: 'yotta-memory backup setup' },
|
|
313
|
+
{ cmd: 'backup create', example: 'yotta-memory backup create' },
|
|
314
|
+
{ cmd: 'backup list', example: 'yotta-memory backup list' },
|
|
315
|
+
{ cmd: 'backup restore', example: 'yotta-memory backup restore <id> --to <目录>', danger: true, note: '恢复会写入目标目录;覆盖非空目录需显式 --force,先确认目标和备份' },
|
|
316
|
+
{ cmd: 'backup drill', example: 'yotta-memory backup drill <id>', note: '先演练确认备份可用,再正式恢复' },
|
|
317
|
+
] },
|
|
318
|
+
{ group: '维护瘦身', items: [
|
|
319
|
+
{ cmd: 'doctor', example: 'yotta-memory doctor' },
|
|
320
|
+
{ cmd: 'maintain', example: 'yotta-memory maintain --capacity', note: '只读报告,不会自动归档或改权重' },
|
|
321
|
+
{ cmd: 'archive', example: 'yotta-memory archive --dry-run', note: '先预演;--force 会关闭冷却期与常青豁免' },
|
|
322
|
+
{ cmd: 'consolidate', example: 'yotta-memory consolidate --propose' },
|
|
323
|
+
{ cmd: 'forget', example: 'yotta-memory forget <记忆 id>', danger: true, note: '删除前先确认 id;重要库先 backup create 留恢复点' },
|
|
324
|
+
] },
|
|
325
|
+
{ group: '检索上下文', items: [
|
|
326
|
+
{ cmd: 'context', example: 'yotta-memory context' },
|
|
327
|
+
{ cmd: 'recall', example: 'yotta-memory recall 关键词 --type BOUND' },
|
|
328
|
+
{ cmd: 'bench', example: 'yotta-memory bench' },
|
|
329
|
+
{ cmd: 'explain', example: 'yotta-memory explain <记忆 id>' },
|
|
330
|
+
] },
|
|
331
|
+
{ group: '扫描安全', items: [
|
|
332
|
+
{ cmd: 'scan', example: 'yotta-memory scan' },
|
|
333
|
+
{ cmd: 'doctor', example: 'yotta-memory doctor --baseline', note: '恢复 / 迁移后追加六类只读基线探针' },
|
|
334
|
+
] },
|
|
335
|
+
{ group: '导入导出', items: [
|
|
336
|
+
{ cmd: 'export', example: 'yotta-memory export --out 记忆导出.json' },
|
|
337
|
+
{ cmd: 'import', example: 'yotta-memory import 记忆导出.json', danger: true, note: '导入会写入现有记忆库;先确认文件来源与内容' },
|
|
338
|
+
] },
|
|
339
|
+
{ group: '身份权限', items: [
|
|
340
|
+
{ cmd: 'iam', example: 'yotta-memory iam --name <名字> --user <用户> --relationship <关系>' },
|
|
341
|
+
{ cmd: 'whoami', example: 'yotta-memory whoami' },
|
|
342
|
+
{ cmd: 'key status', example: 'yotta-memory key status <id>' },
|
|
343
|
+
{ cmd: 'key claim', example: 'yotta-memory key claim <id>' },
|
|
344
|
+
{ cmd: 'key revoke', example: 'yotta-memory key revoke <id>', danger: true, note: '吊销后该 agent_key 立即失效' },
|
|
345
|
+
{ cmd: 'identity remove', example: 'yotta-memory identity remove <id> --dry-run', danger: true, note: '真删该 AI 的私密记忆(公共 FACT 保留);先 --dry-run 预览' },
|
|
346
|
+
] },
|
|
347
|
+
{ group: '运行时与局域网', items: [
|
|
348
|
+
{ cmd: 'runtime status', example: 'yotta-memory runtime status' },
|
|
349
|
+
{ cmd: 'runtime rollback', example: 'yotta-memory runtime rollback', danger: true, note: '回滚运行时入口;先确认当前版本与目标版本' },
|
|
350
|
+
{ cmd: 'serve', example: 'yotta-memory serve' },
|
|
351
|
+
{ cmd: 'lan status', example: 'yotta-memory lan status' },
|
|
352
|
+
{ cmd: 'lan enable', example: 'yotta-memory lan enable', note: '开机自启;不再需要时用 lan disable 关闭' },
|
|
353
|
+
] },
|
|
354
|
+
];
|
|
308
355
|
// @generated view-html:start
|
|
309
|
-
const VIEW_HTML = "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"robots\" content=\"noindex,nofollow\">\n<title>元忆 · 记忆管理台</title>\n<style>\n :root{--bg:#0f0c12;--bg2:#141019;--panel:#191420;--panel2:#1f1828;--line:rgba(255,217,138,.12);--line2:rgba(255,217,138,.24);\n --text:#f4eee4;--muted:#a79c8e;--faint:#776c60;--amber:#f0a13a;--gold:#ffd98a;--ok:#7fc49a;--danger:#e07070;--shadow:0 10px 30px rgba(0,0,0,.35)}\n body.light{--bg:#f6f1e8;--bg2:#f1eade;--panel:#fffdf8;--panel2:#faf5ec;--line:rgba(56,42,28,.12);--line2:rgba(56,42,28,.24);\n --text:#241c14;--muted:#6f6458;--faint:#96897b;--amber:#c1781c;--gold:#9c6a16;--ok:#3e7d57;--danger:#b23c3c;--shadow:0 8px 24px rgba(56,42,28,.10)}\n *{box-sizing:border-box}[hidden]{display:none!important}\n html,body{margin:0;padding:0}\n body{background:var(--bg);color:var(--text);font:14px/1.6 \"Microsoft YaHei\",\"PingFang SC\",\"Noto Sans CJK SC\",system-ui,sans-serif;letter-spacing:0}\n button{font:inherit;color:inherit;cursor:pointer}\n input,select,textarea{font:inherit;color:var(--text);background:var(--panel2);border:1px solid var(--line2);border-radius:8px;padding:9px 12px;outline:0;width:100%}\n input:focus,select:focus,textarea:focus{border-color:var(--amber)}\n textarea{min-height:96px;resize:vertical}\n .muted{color:var(--muted)}.faint{color:var(--faint);font-size:12px}\n .sv{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;flex:none}\n\n /* lock */\n .lock-wrap{min-height:100vh;display:grid;place-items:center;padding:24px}\n .lock-card{width:min(420px,100%);background:var(--panel);border:1px solid var(--line2);border-radius:12px;padding:26px;box-shadow:var(--shadow)}\n .lock-card h1{margin:14px 0 6px;font-size:22px}\n .lock-card .field{margin-top:16px}\n .err{color:var(--danger);margin-top:10px;font-size:13px;min-height:20px}\n\n /* shell */\n .shell{display:grid;grid-template-columns:236px minmax(0,1fr);min-height:100vh}\n .sidebar{background:var(--bg2);border-right:1px solid var(--line);padding:22px 16px;display:flex;flex-direction:column;gap:22px;position:sticky;top:0;height:100vh;min-width:0}\n .brand{display:flex;align-items:center;gap:10px;padding:0 6px}\n .brand-mark{width:34px;height:34px;border-radius:8px;background:rgba(240,161,58,.14);border:1px solid var(--line2);display:grid;place-items:center;color:var(--gold)}\n .brand-name{font-size:16px;font-weight:600}.brand-sub{font-size:11px;color:var(--faint)}\n .nav{display:flex;flex-direction:column;gap:4px;min-width:0}\n .nav button{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--muted);text-align:left}\n .nav button:hover{background:var(--panel);color:var(--text)}\n .nav button.active{background:var(--panel);color:var(--gold);border-color:var(--line)}\n .side-foot{margin-top:auto;display:flex;flex-direction:column;gap:10px;min-width:0}\n .privacy{display:flex;gap:8px;align-items:flex-start;padding:10px;border:1px solid var(--line);border-radius:8px;color:var(--muted);font-size:12px;background:var(--panel)}\n .privacy .sv{color:var(--ok);margin-top:2px}\n .ghost-btn{display:flex;align-items:center;justify-content:center;gap:8px;border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:6px;padding:8px}\n .ghost-btn:hover{color:var(--text);border-color:var(--line2)}\n\n .main{min-width:0;padding:26px 30px 60px}\n .topbar{display:flex;align-items:center;gap:14px;margin-bottom:22px;flex-wrap:wrap}\n .page-title{font-size:20px;font-weight:600;margin:0}.page-sub{color:var(--faint);font-size:12px;margin-top:2px}\n .spacer{flex:1}\n .search{display:flex;align-items:center;gap:8px;min-width:240px;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:8px 12px;color:var(--muted)}\n .search input{background:transparent;border:0;padding:0;width:100%}\n .chip-user{display:flex;align-items:center;gap:8px;padding:6px 10px;border:1px solid var(--line);border-radius:999px;color:var(--muted);font-size:12px}\n .dot{width:8px;height:8px;border-radius:50%;background:var(--amber)}\n\n .view{display:none}.view.active{display:block}\n .grid{display:grid;gap:14px}.g4{grid-template-columns:repeat(4,minmax(0,1fr))}.g2{grid-template-columns:repeat(2,minmax(0,1fr))}\n .card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:16px;min-width:0}\n .card h3{margin:0 0 4px;font-size:14px}\n .label{color:var(--muted);font-size:12px}.value{font-size:26px;font-weight:600;margin:8px 0 2px}.delta{font-size:12px;color:var(--ok)}\n .section{margin-top:20px}\n .shead{display:flex;align-items:baseline;gap:10px;margin-bottom:10px}.shead h2{font-size:15px;margin:0}\n .bars{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}\n .bar{border:1px solid var(--line);border-radius:8px;padding:12px;background:var(--panel2)}\n .bar b{font-size:18px;display:block;margin-top:6px}\n .bar .dot{display:inline-block;margin-right:6px}\n\n .toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap}\n .chips{display:flex;gap:8px;flex-wrap:wrap}\n .chip{border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:999px;padding:5px 12px;font-size:12px}\n .chip.active{color:var(--gold);border-color:var(--line2);background:rgba(240,161,58,.10)}\n .btn{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;border-radius:6px;border:1px solid var(--line2);background:transparent;color:var(--text)}\n .btn:hover{border-color:var(--gold)}\n .btn.primary{background:var(--amber);border-color:var(--amber);color:#1b1206;font-weight:600}\n .btn.danger{border-color:rgba(224,112,112,.5);color:var(--danger)}\n .btn.small{padding:5px 10px;font-size:12px}\n .btn[disabled]{opacity:.5;cursor:not-allowed}\n\n .list{display:flex;flex-direction:column;gap:8px;margin-top:14px}\n .row{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid var(--line);border-radius:8px;background:var(--panel2)}\n .row:hover{border-color:var(--line2)}\n .row-main{flex:1;min-width:0;cursor:pointer}\n .row-title{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .row-snippet{color:var(--muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .row-meta{color:var(--faint);font-size:11px;margin-top:2px}\n .row-actions{display:flex;gap:6px}\n .ibtn{width:30px;height:30px;display:grid;place-items:center;border:1px solid var(--line);background:transparent;border-radius:6px;color:var(--muted)}\n .ibtn:hover{color:var(--text);border-color:var(--line2)}\n .badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--line2);color:var(--gold);background:rgba(240,161,58,.10);white-space:nowrap}\n .badge.pref{color:#b79cdd;border-color:rgba(183,156,221,.4);background:rgba(183,156,221,.10)}\n .badge.bound{color:#8fb4de;border-color:rgba(143,180,222,.4);background:rgba(143,180,222,.10)}\n .badge.commit{color:var(--ok);border-color:rgba(127,196,154,.4);background:rgba(127,196,154,.10)}\n .badge.off{color:var(--faint);border-color:var(--line);background:transparent}\n .empty{color:var(--faint);text-align:center;padding:26px;border:1px dashed var(--line);border-radius:8px;margin-top:14px}\n .pager{display:flex;align-items:center;gap:10px;margin-top:12px;color:var(--faint);font-size:12px}\n\n table{width:100%;border-collapse:collapse}\n th,td{text-align:left;padding:12px 10px;border-bottom:1px solid var(--line);font-size:13px}\n th{color:var(--faint);font-weight:500;font-size:12px}\n .owner-name{font-weight:600}\n\n .field{margin-bottom:12px}\n .field label{display:block;color:var(--muted);font-size:12px;margin-bottom:6px}\n .row2{display:grid;grid-template-columns:1fr 1fr;gap:12px}\n .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 @media (max-width:960px){.g4{grid-template-columns:repeat(2,minmax(0,1fr))}.bars{grid-template-columns:repeat(2,minmax(0,1fr))}}\n @media (max-width:760px){\n .shell{grid-template-columns:minmax(0,1fr)}\n .sidebar{position:static;height:auto;padding:14px;gap:12px}\n .nav{flex-direction:row;overflow-x:auto;width:100%}\n .nav button{white-space:nowrap}\n .side-foot{margin-top:0;flex-direction:row}\n .privacy{flex:1}\n .main{padding:18px 16px 48px}\n .search{min-width:100%;order:3}\n .g4{grid-template-columns:repeat(2,minmax(0,1fr))}\n table{display:block;overflow-x:auto;white-space:nowrap}\n .row{flex-wrap:wrap}.row-actions{width:100%;justify-content:flex-end}\n }\n</style>\n</head>\n<body>\n<div id=\"lock\" class=\"lock-wrap\">\n <div class=\"lock-card\">\n <div class=\"brand\">\n <div class=\"brand-mark\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><ellipse cx=\"12\" cy=\"5\" rx=\"8\" ry=\"3\"/><path d=\"M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5\"/><path d=\"M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3\"/></svg></div>\n <div><div class=\"brand-name\">元忆 yotta-memory</div><div class=\"brand-sub\">你的 AI 记忆,由你掌控</div></div>\n </div>\n <h1>解锁记忆管理台 <span id=\"ver\" class=\"faint\"></span></h1>\n <div class=\"field\"><label>主口令(只在本地内存派生,不落盘、不发远端)</label><input type=\"password\" id=\"pw\" placeholder=\"主口令\"></div>\n <button class=\"btn primary\" id=\"unlockBtn\" style=\"width:100%\">解锁</button>\n <div class=\"err\" id=\"lockErr\"></div>\n <div class=\"faint\" style=\"margin-top:8px\">忘记口令可用恢复钥匙在 CLI 重设:<code>yotta-memory reset-password --recovery-key <钥匙></code></div>\n </div>\n</div>\n\n<div id=\"app\" class=\"shell\" hidden>\n <aside class=\"sidebar\">\n <div class=\"brand\">\n <div class=\"brand-mark\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><ellipse cx=\"12\" cy=\"5\" rx=\"8\" ry=\"3\"/><path d=\"M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5\"/><path d=\"M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3\"/></svg></div>\n <div><div class=\"brand-name\">元忆</div><div class=\"brand-sub\">你的 AI 记忆,由你掌控</div></div>\n </div>\n <nav class=\"nav\" id=\"nav\">\n <button data-view=\"overview\" class=\"active\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M3 10.5 12 3l9 7.5\"/><path d=\"M5 9.5V21h14V9.5\"/></svg>概览</button>\n <button data-view=\"memories\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M4 19.5A2.5 2.5 0 0 1 6.5 17H20\"/><path d=\"M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z\"/></svg>记忆</button>\n <button data-view=\"access\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/><path d=\"M22 21v-2a4 4 0 0 0-3-3.87\"/></svg>AI 与权限</button>\n <button data-view=\"settings\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M4 21v-7\"/><path d=\"M4 10V3\"/><path d=\"M12 21v-9\"/><path d=\"M12 8V3\"/><path d=\"M20 21v-5\"/><path d=\"M20 12V3\"/><path d=\"M1 14h6\"/><path d=\"M9 8h6\"/><path d=\"M17 16h6\"/></svg>设置</button>\n </nav>\n <div class=\"side-foot\">\n <div class=\"privacy\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/><path d=\"m9 12 2 2 4-4\"/></svg><span>数据不出本机<br>私密区按 AI 隔离加密</span></div>\n <button class=\"ghost-btn\" id=\"themeBtn\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\"/></svg>切换主题</button>\n </div>\n </aside>\n\n <main class=\"main\">\n <div class=\"topbar\">\n <div><h1 class=\"page-title\" id=\"pageTitle\">概览</h1><div class=\"page-sub\" id=\"pageSub\">你的 AI 记住了什么,一眼看清</div></div>\n <div class=\"spacer\"></div>\n <label class=\"search\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-3.5-3.5\"/></svg><input id=\"globalSearch\" placeholder=\"搜索记忆 / 关键词\"></label>\n <div class=\"chip-user\"><span class=\"dot\"></span>本机 · 用户</div>\n </div>\n\n <section class=\"view active\" id=\"view-overview\">\n <div class=\"grid g4\">\n <div class=\"card\"><div class=\"label\">记忆总数</div><div class=\"value\" id=\"ovTotal\">-</div><div class=\"delta\" id=\"ovFiles\">-</div></div>\n <div class=\"card\"><div class=\"label\">私密记忆</div><div class=\"value\" id=\"ovPrivate\">-</div><div class=\"delta\">仅对应 AI 与本人可见</div></div>\n <div class=\"card\"><div class=\"label\">AI 智能体</div><div class=\"value\" id=\"ovOwners\">-</div><div class=\"delta\" id=\"ovAuth\">-</div></div>\n <div class=\"card\"><div class=\"label\">已藏起来(归档)</div><div class=\"value\" id=\"ovArchived\">-</div><div class=\"delta\">可随时放回</div></div>\n </div>\n <div class=\"section\">\n <div class=\"shead\"><h2>四类记忆分布</h2><span class=\"faint\">FACT 共享 · PREF / BOUND / COMMIT 私密</span></div>\n <div class=\"bars\" id=\"typeBars\"></div>\n </div>\n <div class=\"section grid g2\">\n <div class=\"card\">\n <h3>最近更新</h3>\n <div class=\"list\" id=\"recentList\"></div>\n </div>\n <div class=\"card\">\n <h3>备份与安全</h3>\n <div class=\"kv\" style=\"margin-top:10px\">\n <dt>备份目录</dt><dd id=\"ovBackup\">-</dd>\n <dt>存储占用</dt><dd id=\"ovBytes\">-</dd>\n <dt>私密加密</dt><dd>已启用(AES-256-GCM 信封加密)</dd>\n </div>\n <div style=\"display:flex;gap:8px;flex-wrap:wrap\">\n <button class=\"btn small\" id=\"quickArchived\">查看已藏</button>\n <button class=\"btn small\" id=\"quickTrash\">最近删除</button>\n <button class=\"btn small primary\" data-goto=\"memories\">管理记忆</button>\n </div>\n </div>\n </div>\n </section>\n\n <section class=\"view\" id=\"view-memories\">\n <div class=\"card toolbar\">\n <label class=\"search\" style=\"min-width:280px;flex:1\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-3.5-3.5\"/></svg><input id=\"memSearch\" placeholder=\"搜索记忆(标题 / 内容 / 标签)\"></label>\n <div class=\"chips\" id=\"typeChips\">\n <button class=\"chip active\" data-type=\"all\">全部</button>\n <button class=\"chip\" data-type=\"FACT\">FACT</button>\n <button class=\"chip\" data-type=\"PREF\">PREF</button>\n <button class=\"chip\" data-type=\"BOUND\">BOUND</button>\n <button class=\"chip\" data-type=\"COMMIT\">COMMIT</button>\n </div>\n <div class=\"spacer\"></div>\n <button class=\"btn\" id=\"btnArchived\">已藏</button>\n <button class=\"btn\" id=\"btnTrash\">最近删除</button>\n <button class=\"btn primary\" id=\"btnNew\">新建记忆</button>\n </div>\n <div class=\"list\" id=\"memList\"></div>\n <div class=\"empty\" id=\"memEmpty\" hidden>没有匹配的记忆。</div>\n <div class=\"pager\"><button class=\"btn small\" id=\"prevPage\">上一页</button><span id=\"pageInfo\">-</span><button class=\"btn small\" id=\"nextPage\">下一页</button></div>\n </section>\n\n <section class=\"view\" id=\"view-access\">\n <div class=\"card\">\n <h3>谁看得见什么</h3>\n <div class=\"faint\" style=\"margin-bottom:10px\">默认拒绝:每个 AI 只能读自己的私密区;公共 FACT 所有已授权 AI 可见。授权会生成只显示一次的 agent_key,请立即单独保存。</div>\n <table><thead><tr><th>AI 智能体</th><th>FACT 公共</th><th>私密记忆</th><th>状态</th><th></th></tr></thead><tbody id=\"ownerRows\"></tbody></table>\n </div>\n <div class=\"section card\">\n <h3>恢复与备份</h3>\n <div class=\"kv\"><dt>备份目录</dt><dd id=\"accBackup\">-</dd><dt>恢复钥匙</dt><dd>离线保存;泄露等同口令泄露</dd></div>\n <div style=\"display:flex;gap:8px;flex-wrap:wrap\"><button class=\"btn small\" id=\"btnRk\">显示恢复钥匙</button><button class=\"btn small\" id=\"btnExport\">导出全部 JSON</button></div>\n </div>\n </section>\n\n <section class=\"view\" id=\"view-settings\">\n <div class=\"card\">\n <h3>重设主口令</h3>\n <div class=\"row2\" style=\"margin-top:10px\">\n <div class=\"field\"><label>当前口令</label><input type=\"password\" id=\"pwCur\"></div>\n <div class=\"field\"><label>新口令</label><input type=\"password\" id=\"pwNew\"></div>\n <div class=\"field\"><label>确认新口令</label><input type=\"password\" id=\"pwNew2\"></div>\n </div>\n <button class=\"btn primary\" id=\"btnResetPw\">重设口令</button>\n <div class=\"err\" id=\"pwOut\"></div>\n </div>\n <div class=\"section card\">\n <h3>危险操作</h3>\n <div class=\"faint\">删除某个 AI 的身份与其私密记忆(公共 FACT 保留,其它 AI 不受影响)。此操作不可恢复,请谨慎使用。</div>\n <div style=\"margin-top:10px\"><button class=\"btn danger small\" id=\"btnDangerHint\">在哪里执行?</button></div>\n </div>\n </section>\n </main>\n</div>\n\n<div class=\"mask\" id=\"modalMask\"><div class=\"modal\"><h3 id=\"modalTitle\"></h3><div id=\"modalBody\"></div><div class=\"modal-actions\"><button class=\"btn\" id=\"modalCancel\">取消</button><button class=\"btn primary\" id=\"modalOk\">确认</button></div></div></div>\n<div class=\"mask\" id=\"drawerMask\"></div>\n<aside class=\"drawer\" id=\"drawer\">\n <div style=\"display:flex;gap:10px;align-items:flex-start\"><div style=\"flex:1\"><h3 id=\"drTitle\"></h3><div class=\"faint\" id=\"drMeta\"></div></div><button class=\"ibtn\" id=\"drClose\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/></svg></button></div>\n <div class=\"quote\" id=\"drBody\" style=\"margin-top:12px\"></div>\n <dl class=\"kv\" id=\"drKv\"></dl>\n <div style=\"display:flex;gap:8px\"><button class=\"btn\" id=\"drEdit\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z\"/></svg>编辑</button><button class=\"btn danger\" id=\"drDelete\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>删除</button></div>\n</aside>\n<div class=\"toasts\" id=\"toasts\"></div>\n\n<script>\n(function () {\n 'use strict';\n var $ = function (s) { return document.querySelector(s); };\n var state = { view: 'overview', entries: [], count: 0, offset: 0, limit: 20, query: '', type: 'all', owners: [], overview: null, current: null };\n var typeNames = { FACT: 'FACT 事实', PREF: 'PREF 偏好', BOUND: 'BOUND 边界', COMMIT: 'COMMIT 承诺' };\n\n function esc(s) {\n return String(s == null ? '' : s).replace(/[&<>\"']/g, function (c) {\n return { '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[c];\n });\n }\n async function api(pathname, body) {\n try {\n var res = await fetch(pathname, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });\n var text = await res.text();\n var data;\n try { data = JSON.parse(text); } catch (e) { data = { error: text }; }\n if (res.status === 401) showLock();\n return data;\n } catch (e) { return { error: String(e) }; }\n }\n function fmtBytes(n) {\n n = Number(n) || 0;\n if (n < 1024) return n + ' B';\n if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';\n return (n / 1024 / 1024).toFixed(2) + ' MB';\n }\n function 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 function toast(msg, actionLabel, fn) {\n var el = document.createElement('div');\n el.className = 'toast';\n el.appendChild(document.createTextNode(msg));\n if (actionLabel && fn) {\n var b = document.createElement('button');\n b.textContent = actionLabel;\n b.addEventListener('click', function () { el.remove(); fn(); });\n el.appendChild(b);\n }\n $('#toasts').appendChild(el);\n setTimeout(function () { el.remove(); }, actionLabel ? 9000 : 3600);\n }\n function showLock() { $('#lock').hidden = false; $('#app').hidden = true; }\n function showApp() { $('#lock').hidden = true; $('#app').hidden = false; refreshAll(); }\n\n /* modal / drawer */\n var modalOkFn = null;\n function openModal(title, bodyHtml, opts) {\n opts = opts || {};\n $('#modalTitle').textContent = title;\n $('#modalBody').innerHTML = bodyHtml;\n $('#modalOk').textContent = opts.okText || '确认';\n $('#modalOk').className = 'btn ' + (opts.danger ? 'danger' : 'primary');\n $('#modalOk').hidden = !opts.onOk;\n modalOkFn = opts.onOk || null;\n $('#modalMask').classList.add('open');\n }\n function closeModal() { $('#modalMask').classList.remove('open'); modalOkFn = null; }\n function confirmModal(title, text, okText, danger, onOk) {\n openModal(title, '<p class=\"muted\">' + esc(text) + '</p>', { okText: okText, danger: danger, onOk: onOk });\n }\n $('#modalCancel').addEventListener('click', closeModal);\n $('#modalMask').addEventListener('click', function (e) { if (e.target === $('#modalMask')) closeModal(); });\n $('#modalOk').addEventListener('click', function () { if (modalOkFn) modalOkFn(); });\n function openDrawer() { $('#drawer').classList.add('open'); $('#drawerMask').classList.add('open'); }\n function closeDrawer() { $('#drawer').classList.remove('open'); $('#drawerMask').classList.remove('open'); }\n $('#drClose').addEventListener('click', closeDrawer);\n $('#drawerMask').addEventListener('click', closeDrawer);\n\n /* nav */\n var titles = { overview: ['概览', '你的 AI 记住了什么,一眼看清'], memories: ['记忆', '看 / 改 / 删 / 藏,全在这里'], access: ['AI 与权限', '默认拒绝,按 AI 隔离'], settings: ['设置', '口令 / 备份 / 导出'] };\n function switchView(name) {\n state.view = name;\n Array.prototype.forEach.call(document.querySelectorAll('#nav button'), function (b) { b.classList.toggle('active', b.dataset.view === name); });\n Array.prototype.forEach.call(document.querySelectorAll('.view'), function (v) { v.classList.toggle('active', v.id === 'view-' + name); });\n $('#pageTitle').textContent = titles[name][0];\n $('#pageSub').textContent = titles[name][1];\n window.scrollTo(0, 0);\n }\n $('#nav').addEventListener('click', function (e) { var b = e.target.closest('button'); if (b) switchView(b.dataset.view); });\n Array.prototype.forEach.call(document.querySelectorAll('[data-goto]'), function (b) { b.addEventListener('click', function () { switchView(b.dataset.goto); }); });\n $('#themeBtn').addEventListener('click', function () { document.body.classList.toggle('light'); });\n\n /* data loaders */\n async function loadOverview() {\n var d = await api('/api/overview');\n if (d.error) return;\n state.overview = d;\n var priv = d.total - (d.byType.FACT || 0);\n $('#ovTotal').textContent = d.total;\n $('#ovFiles').textContent = d.files + ' 个文件';\n $('#ovPrivate').textContent = priv;\n $('#ovOwners').textContent = d.owners.length;\n $('#ovAuth').textContent = d.authorized + ' 已授权';\n $('#ovArchived').textContent = d.archived;\n $('#ovBackup').textContent = d.backupDir || '未配置(CLI: yotta-memory backup setup)';\n $('#ovBytes').textContent = fmtBytes(d.bytes);\n $('#accBackup').textContent = d.backupDir || '未配置';\n var colors = { FACT: 'var(--gold)', PREF: '#b79cdd', BOUND: '#8fb4de', COMMIT: 'var(--ok)' };\n $('#typeBars').innerHTML = ['FACT', 'PREF', 'BOUND', 'COMMIT'].map(function (t) {\n var n = d.byType[t] || 0;\n return '<div class=\"bar\"><span class=\"dot\" style=\"background:' + colors[t] + '\"></span><span class=\"faint\">' + typeNames[t] + '</span><b>' + n + '</b></div>';\n }).join('');\n }\n async function loadOwners() {\n var d = await api('/api/owners');\n state.owners = (d && d.owners) || [];\n var box = $('#ownerRows');\n if (!state.owners.length) { box.innerHTML = '<tr><td colspan=\"5\" class=\"faint\">' + esc((d && d.hint) || '当前没有 owner,请先在 CLI 执行 yotta-memory iam <id>。') + '</td></tr>'; return; }\n box.innerHTML = state.owners.map(function (o) {\n return '<tr><td><span class=\"owner-name\">' + esc(o.owner) + '</span><div class=\"faint\">' + (o.registered ? '已登记' : '未登记') + '</div></td>' +\n '<td><span class=\"badge commit\">可见</span></td>' +\n '<td>' + (o.authorized ? '<span class=\"badge commit\">已授权</span>' : '<span class=\"badge off\">仅自己</span>') + '</td>' +\n '<td>' + (o.authorized ? '可读自己的私密区' : '未绑定 agent_key') + '</td>' +\n '<td><button class=\"btn small\" data-auth=\"' + esc(o.owner) + '\">' + (o.authorized ? '吊销' : '授权') + '</button> <button class=\"btn small danger\" data-remove=\"' + esc(o.owner) + '\">删除</button></td></tr>';\n }).join('');\n }\n async function loadEntries() {\n var d = await api('/api/entries', { query: state.query, offset: state.offset, limit: state.limit, type: state.type === 'all' ? '' : state.type });\n if (d.error) { toast(d.error); return; }\n state.entries = d.entries || [];\n state.count = d.count || 0;\n var totalPg = Math.max(1, Math.ceil(state.count / state.limit));\n var curPg = Math.floor(state.offset / state.limit) + 1;\n $('#pageInfo').textContent = '共 ' + state.count + ' 条 · 第 ' + curPg + ' / ' + totalPg + ' 页';\n $('#prevPage').disabled = state.offset <= 0;\n $('#nextPage').disabled = !d.hasMore;\n renderEntries();\n }\n function renderEntries() {\n var box = $('#memList');\n $('#memEmpty').hidden = state.entries.length > 0;\n box.innerHTML = state.entries.map(function (e) {\n var t = String(e.type || 'FACT').toUpperCase();\n return '<div class=\"row\" data-file=\"' + esc(e.file) + '\"><div class=\"row-main\"><span class=\"badge ' + t.toLowerCase() + '\">' + typeNames[t] + '</span>' +\n '<div class=\"row-title\">' + esc(e.subject) + '</div><div class=\"row-snippet\">' + esc(e.statement) + '</div>' +\n '<div class=\"row-meta\">' + esc(e.owner || '公共') + ' · ' + esc(fmtTime(e)) + ' · ' + esc(e.file) + '</div></div>' +\n '<div class=\"row-actions\"><button class=\"ibtn\" data-act=\"edit\" title=\"编辑\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z\"/></svg></button>' +\n '<button class=\"ibtn\" data-act=\"archive\" title=\"藏起来(归档)\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M9.9 4.24A9.1 9.1 0 0 1 12 4c7 0 10 8 10 8a17.5 17.5 0 0 1-2.16 3.19\"/><path d=\"M6.6 6.6A16.8 16.8 0 0 0 2 12s3 8 10 8a9.6 9.6 0 0 0 5.4-1.6\"/><path d=\"m2 2 20 20\"/></svg></button>' +\n '<button class=\"ibtn\" data-act=\"delete\" title=\"删除\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg></button></div></div>';\n }).join('');\n }\n async function refreshAll() {\n await loadOverview();\n await loadOwners();\n await loadEntries();\n var recent = state.entries.slice(0, 4).map(function (e) {\n var t = String(e.type || 'FACT').toUpperCase();\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(e.subject) + '</div><div class=\"row-meta\">' + esc(e.owner || '公共') + ' · ' + esc(fmtTime(e)) + '</div></div><span class=\"badge ' + t.toLowerCase() + '\">' + typeNames[t] + '</span></div>';\n }).join('');\n $('#recentList').innerHTML = recent || '<div class=\"faint\">还没有记忆。</div>';\n }\n\n /* entries actions */\n $('#memList').addEventListener('click', async function (e) {\n var row = e.target.closest('.row');\n if (!row) return;\n var file = row.dataset.file;\n var entry = state.entries.filter(function (x) { return x.file === file; })[0];\n var act = e.target.closest('button[data-act]');\n if (!act) { openDetail(entry); return; }\n if (act.dataset.act === 'edit') openEditor(entry);\n else if (act.dataset.act === 'delete') doDelete(entry);\n else if (act.dataset.act === 'archive') doArchive(entry);\n });\n function openDetail(entry) {\n if (!entry) return;\n state.current = entry;\n var t = String(entry.type || 'FACT').toUpperCase();\n $('#drTitle').textContent = entry.subject || '(无标题)';\n $('#drMeta').textContent = typeNames[t] + ' · ' + (entry.owner || '公共') + ' · ' + fmtTime(entry);\n $('#drBody').textContent = entry.statement || '';\n $('#drKv').innerHTML = '<dt>创建</dt><dd>' + esc(entry.btime ? fmtTime({ mtime: entry.btime }) : (entry.created || '-')) + '</dd>' +\n '<dt>最近更新</dt><dd>' + esc(fmtTime(entry) || '-') + '</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 return '<div class=\"field\"><label>类型</label><select id=\"fType\"' + (isNew ? '' : ' disabled') + '>' +\n ['FACT', 'PREF', 'BOUND', 'COMMIT'].map(function (x) { return '<option value=\"' + x + '\"' + (x === t ? ' selected' : '') + '>' + typeNames[x] + '</option>'; }).join('') + '</select></div>' +\n '<div class=\"field\" id=\"fOwnerWrap\"><label>归属 AI(私密记忆必填;点选或输入自定义 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' : '') + '\" 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 || '') + '\"></div>' +\n '<div class=\"field\"><label>标题 subject</label><input id=\"fSubject\" value=\"' + esc(entry && entry.subject || '') + '\"></div>' +\n '<div class=\"field\"><label>内容 statement</label><textarea id=\"fStatement\">' + esc(entry && entry.statement || '') + '</textarea></div>' +\n '<div class=\"field\"><label>标签(逗号分隔)</label><input id=\"fTags\" value=\"' + esc(entry && (entry.tags || []).join(', ') || '') + '\"></div>' +\n (isNew ? '' : '<div class=\"faint\">类型与归属不可修改;如需变更请删除后重写。</div>');\n }\n function syncOwnerField() {\n var t = $('#fType') ? $('#fType').value : 'FACT';\n var wrap = $('#fOwnerWrap');\n if (wrap) wrap.hidden = (t === 'FACT');\n }\n function bindOwnerPicker() {\n var pick = $('#fOwnerPick');\n if (!pick) 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 r = await api('/api/memory/update', { file: entry.file, subject: subject, statement: statement, tags: tags });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已更新 1 条记忆', '撤销', async function () {\n var u = await api('/api/memory/restore', { ref: r.undo });\n toast(u.error || '已撤销这次编辑');\n refreshAll();\n });\n refreshAll();\n },\n });\n 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(r.error); 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(r.error); return; }\n closeModal();\n toast('已删除 1 条记忆', '撤销', async function () {\n var u = await api('/api/memory/restore', { ref: r.trash });\n toast(u.error || '已恢复');\n refreshAll();\n });\n refreshAll();\n });\n }\n function doArchive(entry) {\n confirmModal('藏起来(归档)?', '归档后不参与召回,但数据保留,可随时放回。', '藏起来', false, async function () {\n var r = await api('/api/memory/archive', { file: entry.file });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已藏起来 1 条', '放回', async function () {\n var u = await api('/api/memory/unarchive', { archive: r.archive });\n toast(u.error || '已放回');\n refreshAll();\n });\n refreshAll();\n });\n }\n\n async function openArchived() {\n var d = await api('/api/archived');\n var items = (d.entries || []).map(function (e) {\n return '<div class=\"item\"><span>' + esc(e.original) + '</span><span class=\"spacer\"></span><button class=\"btn small\" data-unarch=\"' + esc(e.ref) + '\">放回</button></div>';\n }).join('');\n openModal('已藏起来(归档)', items ? '<div class=\"list-plain\">' + items + '</div>' : '<p class=\"faint\">没有归档的记忆。</p>', {});\n $('#modalBody').addEventListener('click', async function (e) {\n var b = e.target.closest('button[data-unarch]');\n if (!b) return;\n var r = await api('/api/memory/unarchive', { archive: b.dataset.unarch });\n toast(r.error || '已放回');\n closeModal();\n refreshAll();\n }, { once: true });\n }\n async function openTrash() {\n var d = await api('/api/trash');\n var items = (d.entries || []).filter(function (e) { return e.kind === 'trash'; }).map(function (e) {\n return '<div class=\"item\"><span>' + esc(e.original) + '</span><span class=\"spacer\"></span><button class=\"btn small\" data-restore=\"' + esc(e.ref) + '\">恢复</button></div>';\n }).join('');\n openModal('最近删除', items ? '<div class=\"list-plain\">' + items + '</div>' : '<p class=\"faint\">回收区是空的。</p>', {});\n $('#modalBody').addEventListener('click', async function (e) {\n var b = e.target.closest('button[data-restore]');\n if (!b) return;\n var r = await api('/api/memory/restore', { ref: b.dataset.restore });\n toast(r.error || '已恢复');\n closeModal();\n refreshAll();\n }, { once: true });\n }\n $('#btnArchived').addEventListener('click', openArchived);\n $('#quickArchived').addEventListener('click', openArchived);\n $('#btnTrash').addEventListener('click', openTrash);\n $('#quickTrash').addEventListener('click', openTrash);\n\n /* search / filter / paging */\n $('#memSearch').addEventListener('keydown', function (e) { if (e.key === 'Enter') { state.query = e.target.value; state.offset = 0; loadEntries(); } });\n $('#globalSearch').addEventListener('keydown', function (e) { if (e.key === 'Enter') { state.query = e.target.value; state.offset = 0; switchView('memories'); loadEntries(); } });\n $('#typeChips').addEventListener('click', function (e) {\n var b = e.target.closest('.chip');\n if (!b) return;\n state.type = b.dataset.type; state.offset = 0;\n Array.prototype.forEach.call(document.querySelectorAll('#typeChips .chip'), function (c) { c.classList.toggle('active', c === b); });\n loadEntries();\n });\n $('#prevPage').addEventListener('click', function () { if (state.offset > 0) { state.offset -= state.limit; loadEntries(); } });\n $('#nextPage').addEventListener('click', function () { state.offset += state.limit; loadEntries(); });\n\n /* owners */\n $('#ownerRows').addEventListener('click', async function (e) {\n var authBtn = e.target.closest('button[data-auth]');\n var rmBtn = e.target.closest('button[data-remove]');\n if (authBtn) {\n var owner = authBtn.dataset.auth;\n var authorized = state.owners.filter(function (o) { return o.owner === owner; })[0].authorized;\n if (authorized) {\n confirmModal('吊销 ' + owner + ' 的授权?', '吊销后该智能体立即失去私密读写能力,旧 key 会立即校验失败。', '吊销', true, async function () {\n var r = await api('/api/revoke', { owner: owner });\n closeModal(); toast(r.error || '已吊销'); loadOwners();\n });\n } else {\n confirmModal('授权 ' + owner + '?', '「授权」由你(用户)操作:确认后生成只显示一次的 agent_key,请立即单独保存;服务端同时写临时待领取文件 keys/pending/<agent_id>.key,供该 AI 新会话执行 key claim 领取,领取成功后自动删除。AI 不应代为执行该授权操作。', '授权', false, async function () {\n var r = await api('/api/authorize', { owner: owner });\n closeModal();\n if (r.error) { toast(r.error); return; }\n openModal('agent_key(只显示一次)', '<p class=\"muted\">请立即单独保存;AI 新会话执行 key status → key claim 领取。</p><textarea class=\"keybox\" readonly>' + esc(r.agentKey) + '</textarea>', { okText: '我已保存', onOk: closeModal });\n var box = $('#modalBody').querySelector('textarea');\n if (box) { box.focus(); box.select(); }\n loadOwners();\n });\n }\n return;\n }\n if (rmBtn) {\n var target = rmBtn.dataset.remove;\n openModal('删除 ' + target + ' 的身份与私密记忆', '<p class=\"muted\">不可恢复;公共 FACT 保留,其它 AI 不受影响。请输入完整 ID 确认:</p><input id=\"confirmOwner\" placeholder=\"' + esc(target) + '\">', {\n okText: '删除', danger: true, onOk: async function () {\n var typed = $('#confirmOwner').value.trim();\n if (typed !== target) { toast('ID 不匹配'); return; }\n var r = await api('/api/identity-remove', { owner: target, confirm: typed });\n closeModal(); toast(r.error || '已删除'); loadOwners(); loadOverview(); loadEntries();\n },\n });\n }\n });\n\n /* settings */\n $('#btnResetPw').addEventListener('click', async function () {\n var a = $('#pwCur').value, b = $('#pwNew').value, c = $('#pwNew2').value;\n if (!b || b !== c) { $('#pwOut').textContent = '两次新口令不一致'; return; }\n var r = await api('/api/reset-password', { currentPassword: a, newPassword: b });\n $('#pwOut').textContent = r.error || (r.text || '已重设');\n if (!r.error) { $('#pwCur').value = $('#pwNew').value = $('#pwNew2').value = ''; }\n });\n $('#btnRk').addEventListener('click', async function () {\n var r = await api('/api/recovery-key');\n if (r.error) { toast(r.error); return; }\n openModal('恢复钥匙(离线保存)', '<p class=\"muted\">泄露等同口令泄露。忘记主口令时可用它重设。</p><textarea class=\"keybox\" readonly>' + esc(r.recoveryKey) + '</textarea>', { okText: '我已保存', onOk: closeModal });\n });\n $('#btnExport').addEventListener('click', async function () {\n var r = await api('/api/export');\n if (r.error) { toast(r.error); return; }\n var blob = new Blob([JSON.stringify(r, null, 2)], { type: 'application/json' });\n var a = document.createElement('a');\n a.href = URL.createObjectURL(blob);\n a.download = 'yottamemory-export.json';\n a.click();\n toast('已导出 JSON');\n });\n $('#btnDangerHint').addEventListener('click', function () {\n toast('在「AI 与权限」列表里,每个 AI 右侧有「删除」按钮');\n });\n\n /* unlock / boot */\n $('#unlockBtn').addEventListener('click', async function () {\n var r = await api('/api/unlock', { password: $('#pw').value });\n if (r.error) { $('#lockErr').textContent = r.error; return; }\n $('#lockErr').textContent = '';\n showApp();\n });\n $('#pw').addEventListener('keydown', function (e) { if (e.key === 'Enter') $('#unlockBtn').click(); });\n (async function boot() {\n var s = await api('/api/status');\n if (s && s.version) $('#ver').textContent = 'v' + s.version;\n if (s && s.unlocked) showApp(); else showLock();\n if (s && s.encrypted === false) $('#lockErr').textContent = '当前记忆库未启用加密;请先在 CLI 执行 init --encrypt / migrate。';\n })();\n})();\n</script>\n</body>\n</html>\n";
|
|
356
|
+
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\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 <钥匙></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 <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', 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 { '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[c];\n });\n }\n async function api(pathname, body) {\n try {\n var res = await fetch(pathname, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });\n var text = await res.text();\n var data;\n try { data = JSON.parse(text); } catch (e) { data = { error: text }; }\n if (res.status === 401) showLock();\n return data;\n } catch (e) { return { error: String(e) }; }\n }\n function fmtBytes(n) {\n n = Number(n) || 0;\n if (n < 1024) return n + ' B';\n if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';\n return (n / 1024 / 1024).toFixed(2) + ' MB';\n }\n function 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 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 $('#typeBars').innerHTML = ['FACT', 'PREF', 'BOUND', 'COMMIT'].map(function (t) {\n var n = d.byType[t] || 0;\n return '<div class=\"bar\"><span class=\"dot\" style=\"background:' + colors[t] + '\"></span><span class=\"faint\">' + typeNames[t] + '</span><b>' + n + '</b></div>';\n }).join('');\n }\n async function loadOwners() {\n var d = await api('/api/owners');\n state.owners = (d && d.owners) || [];\n var box = $('#ownerRows');\n if (!state.owners.length) { box.innerHTML = '<tr><td colspan=\"5\" class=\"faint\">' + esc((d && d.hint) || '当前没有 owner,请先在 CLI 执行 yotta-memory iam <id>。') + '</td></tr>'; return; }\n box.innerHTML = state.owners.map(function (o) {\n return '<tr><td><span class=\"owner-name\">' + esc(o.owner) + '</span><div class=\"faint\">' + (o.registered ? '已登记' : '未登记') + '</div></td>' +\n '<td><span class=\"badge commit\">可见</span></td>' +\n '<td>' + (o.authorized ? '<span class=\"badge commit\">已授权</span>' : '<span class=\"badge off\">仅自己</span>') + '</td>' +\n '<td>' + (o.authorized ? '可读自己的私密区' : '未绑定 agent_key') + '</td>' +\n '<td><button class=\"btn small\" data-auth=\"' + esc(o.owner) + '\">' + (o.authorized ? '吊销' : '授权') + '</button> <button class=\"btn small danger\" data-remove=\"' + esc(o.owner) + '\">删除</button></td></tr>';\n }).join('');\n }\n async function loadEntries() {\n var d = await api('/api/entries', { query: state.query, offset: state.offset, limit: state.limit, type: state.type === 'all' ? '' : state.type });\n if (d.error) { toast(d.error); return; }\n state.entries = d.entries || [];\n state.count = d.count || 0;\n var totalPg = Math.max(1, Math.ceil(state.count / state.limit));\n var curPg = Math.floor(state.offset / state.limit) + 1;\n $('#pageInfo').textContent = '共 ' + state.count + ' 条 · 第 ' + curPg + ' / ' + totalPg + ' 页';\n $('#prevPage').disabled = state.offset <= 0;\n $('#nextPage').disabled = !d.hasMore;\n renderEntries();\n }\n function renderEntries() {\n var box = $('#memList');\n $('#memEmpty').hidden = state.entries.length > 0;\n box.innerHTML = state.entries.map(function (e) {\n var t = String(e.type || 'FACT').toUpperCase();\n return '<div class=\"row\" data-file=\"' + esc(e.file) + '\"><div class=\"row-main\"><span class=\"badge ' + t.toLowerCase() + '\">' + typeNames[t] + '</span>' +\n '<div class=\"row-title\">' + esc(e.subject) + '</div><div class=\"row-snippet\">' + esc(e.statement) + '</div>' +\n '<div class=\"row-meta\">' + esc(e.owner || '公共') + ' · ' + esc(fmtTime(e)) + ' · ' + esc(e.file) + '</div></div>' +\n '<div class=\"row-actions\"><button class=\"ibtn\" data-act=\"edit\" title=\"编辑\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z\"/></svg></button>' +\n '<button class=\"ibtn\" data-act=\"archive\" title=\"藏起来(归档)\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M9.9 4.24A9.1 9.1 0 0 1 12 4c7 0 10 8 10 8a17.5 17.5 0 0 1-2.16 3.19\"/><path d=\"M6.6 6.6A16.8 16.8 0 0 0 2 12s3 8 10 8a9.6 9.6 0 0 0 5.4-1.6\"/><path d=\"m2 2 20 20\"/></svg></button>' +\n '<button class=\"ibtn\" data-act=\"delete\" title=\"删除\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg></button></div></div>';\n }).join('');\n }\n async function refreshAll() {\n await loadOverview();\n await loadOwners();\n await loadEntries();\n var recent = state.entries.slice(0, 4).map(function (e) {\n var t = String(e.type || 'FACT').toUpperCase();\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(e.subject) + '</div><div class=\"row-meta\">' + esc(e.owner || '公共') + ' · ' + esc(fmtTime(e)) + '</div></div><span class=\"badge ' + t.toLowerCase() + '\">' + typeNames[t] + '</span></div>';\n }).join('');\n $('#recentList').innerHTML = recent || '<div class=\"faint\">还没有记忆。</div>';\n }\n\n /* entries actions */\n $('#memList').addEventListener('click', async function (e) {\n var row = e.target.closest('.row');\n if (!row) return;\n var file = row.dataset.file;\n var entry = state.entries.filter(function (x) { return x.file === file; })[0];\n var act = e.target.closest('button[data-act]');\n if (!act) { openDetail(entry); return; }\n if (act.dataset.act === 'edit') openEditor(entry);\n else if (act.dataset.act === 'delete') doDelete(entry);\n else if (act.dataset.act === 'archive') doArchive(entry);\n });\n function openDetail(entry) {\n if (!entry) return;\n state.current = entry;\n var t = String(entry.type || 'FACT').toUpperCase();\n $('#drTitle').textContent = entry.subject || '(无标题)';\n $('#drMeta').textContent = typeNames[t] + ' · ' + (entry.owner || '公共') + ' · ' + fmtTime(entry);\n $('#drBody').textContent = entry.statement || '';\n $('#drKv').innerHTML = '<dt>创建</dt><dd>' + esc(entry.btime ? fmtTime({ mtime: entry.btime }) : (entry.created || '-')) + '</dd>' +\n '<dt>最近更新</dt><dd>' + esc(fmtTime(entry) || '-') + '</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 return '<div class=\"field\"><label>类型</label><select id=\"fType\"' + (isNew ? '' : ' disabled') + '>' +\n ['FACT', 'PREF', 'BOUND', 'COMMIT'].map(function (x) { return '<option value=\"' + x + '\"' + (x === t ? ' selected' : '') + '>' + typeNames[x] + '</option>'; }).join('') + '</select></div>' +\n '<div class=\"field\" id=\"fOwnerWrap\"><label>归属 AI(私密记忆必填;点选或输入自定义 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' : '') + '\" 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 || '') + '\"></div>' +\n '<div class=\"field\"><label>标题 subject</label><input id=\"fSubject\" value=\"' + esc(entry && entry.subject || '') + '\"></div>' +\n '<div class=\"field\"><label>内容 statement</label><textarea id=\"fStatement\">' + esc(entry && entry.statement || '') + '</textarea></div>' +\n '<div class=\"field\"><label>标签(逗号分隔)</label><input id=\"fTags\" value=\"' + esc(entry && (entry.tags || []).join(', ') || '') + '\"></div>' +\n (isNew ? '' : '<div class=\"faint\">类型与归属不可修改;如需变更请删除后重写。</div>');\n }\n function syncOwnerField() {\n var t = $('#fType') ? $('#fType').value : 'FACT';\n var wrap = $('#fOwnerWrap');\n if (wrap) wrap.hidden = (t === 'FACT');\n }\n function bindOwnerPicker() {\n var pick = $('#fOwnerPick');\n if (!pick) 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 r = await api('/api/memory/update', { file: entry.file, subject: subject, statement: statement, tags: tags });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已更新 1 条记忆', '撤销', async function () {\n var u = await api('/api/memory/restore', { ref: r.undo });\n toast(u.error || '已撤销这次编辑');\n refreshAll();\n });\n refreshAll();\n },\n });\n 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(r.error); 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(r.error); return; }\n closeModal();\n toast('已删除 1 条记忆', '撤销', async function () {\n var u = await api('/api/memory/restore', { ref: r.trash });\n toast(u.error || '已恢复');\n refreshAll();\n });\n refreshAll();\n });\n }\n function doArchive(entry) {\n confirmModal('藏起来(归档)?', '归档后不参与召回,但数据保留,可随时放回。', '藏起来', false, async function () {\n var r = await api('/api/memory/archive', { file: entry.file });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已藏起来 1 条', '放回', async function () {\n var u = await api('/api/memory/unarchive', { archive: r.archive });\n toast(u.error || '已放回');\n refreshAll();\n });\n refreshAll();\n });\n }\n\n async function openArchived() {\n var d = await api('/api/archived');\n var items = (d.entries || []).map(function (e) {\n return '<div class=\"item\"><span>' + esc(e.original) + '</span><span class=\"spacer\"></span><button class=\"btn small\" data-unarch=\"' + esc(e.ref) + '\">放回</button></div>';\n }).join('');\n openModal('已藏起来(归档)', items ? '<div class=\"list-plain\">' + items + '</div>' : '<p class=\"faint\">没有归档的记忆。</p>', {});\n $('#modalBody').addEventListener('click', async function (e) {\n var b = e.target.closest('button[data-unarch]');\n if (!b) return;\n var r = await api('/api/memory/unarchive', { archive: b.dataset.unarch });\n toast(r.error || '已放回');\n closeModal();\n refreshAll();\n }, { once: true });\n }\n async function openTrash() {\n var d = await api('/api/trash');\n var items = (d.entries || []).filter(function (e) { return e.kind === 'trash'; }).map(function (e) {\n return '<div class=\"item\"><span>' + esc(e.original) + '</span><span class=\"spacer\"></span><button class=\"btn small\" data-restore=\"' + esc(e.ref) + '\">恢复</button></div>';\n }).join('');\n openModal('最近删除', items ? '<div class=\"list-plain\">' + items + '</div>' : '<p class=\"faint\">回收区是空的。</p>', {});\n $('#modalBody').addEventListener('click', async function (e) {\n var b = e.target.closest('button[data-restore]');\n if (!b) return;\n var r = await api('/api/memory/restore', { ref: b.dataset.restore });\n toast(r.error || '已恢复');\n closeModal();\n refreshAll();\n }, { once: true });\n }\n $('#btnArchived').addEventListener('click', openArchived);\n $('#quickArchived').addEventListener('click', openArchived);\n $('#btnTrash').addEventListener('click', openTrash);\n $('#quickTrash').addEventListener('click', openTrash);\n\n /* search / filter / paging */\n $('#memSearch').addEventListener('keydown', function (e) { if (e.key === 'Enter') { state.query = e.target.value; state.offset = 0; loadEntries(); } });\n $('#globalSearch').addEventListener('keydown', function (e) { if (e.key === 'Enter') { state.query = e.target.value; state.offset = 0; switchView('memories'); loadEntries(); } });\n $('#typeChips').addEventListener('click', function (e) {\n var b = e.target.closest('.chip');\n if (!b) return;\n state.type = b.dataset.type; state.offset = 0;\n Array.prototype.forEach.call(document.querySelectorAll('#typeChips .chip'), function (c) { c.classList.toggle('active', c === b); });\n loadEntries();\n });\n $('#prevPage').addEventListener('click', function () { if (state.offset > 0) { state.offset -= state.limit; loadEntries(); } });\n $('#nextPage').addEventListener('click', function () { state.offset += state.limit; loadEntries(); });\n\n /* owners */\n $('#ownerRows').addEventListener('click', async function (e) {\n var authBtn = e.target.closest('button[data-auth]');\n var rmBtn = e.target.closest('button[data-remove]');\n if (authBtn) {\n var owner = authBtn.dataset.auth;\n var authorized = state.owners.filter(function (o) { return o.owner === owner; })[0].authorized;\n if (authorized) {\n confirmModal('吊销 ' + owner + ' 的授权?', '吊销后该智能体立即失去私密读写能力,旧 key 会立即校验失败。', '吊销', true, async function () {\n var r = await api('/api/revoke', { owner: owner });\n closeModal(); toast(r.error || '已吊销'); loadOwners();\n });\n } else {\n confirmModal('授权 ' + owner + '?', '「授权」由你(用户)操作:确认后生成只显示一次的 agent_key,请立即单独保存;服务端同时写临时待领取文件 keys/pending/<agent_id>.key,供该 AI 新会话执行 key claim 领取,领取成功后自动删除。AI 不应代为执行该授权操作。', '授权', false, async function () {\n var r = await api('/api/authorize', { owner: owner });\n closeModal();\n if (r.error) { toast(r.error); return; }\n openModal('agent_key(只显示一次)', '<p class=\"muted\">请立即单独保存;AI 新会话执行 key status → key claim 领取。</p><textarea class=\"keybox\" readonly>' + esc(r.agentKey) + '</textarea>', { okText: '我已保存', onOk: closeModal });\n var box = $('#modalBody').querySelector('textarea');\n if (box) { box.focus(); box.select(); }\n loadOwners();\n });\n }\n return;\n }\n if (rmBtn) {\n var target = rmBtn.dataset.remove;\n openModal('删除 ' + target + ' 的身份与私密记忆', '<p class=\"muted\">不可恢复;公共 FACT 保留,其它 AI 不受影响。请输入完整 ID 确认:</p><input id=\"confirmOwner\" placeholder=\"' + esc(target) + '\">', {\n okText: '删除', danger: true, onOk: async function () {\n var typed = $('#confirmOwner').value.trim();\n if (typed !== target) { toast('ID 不匹配'); return; }\n var r = await api('/api/identity-remove', { owner: target, confirm: typed });\n closeModal(); toast(r.error || '已删除'); loadOwners(); loadOverview(); loadEntries();\n },\n });\n }\n });\n\n /* settings */\n $('#btnResetPw').addEventListener('click', async function () {\n var a = $('#pwCur').value, b = $('#pwNew').value, c = $('#pwNew2').value;\n if (!b || b !== c) { $('#pwOut').textContent = '两次新口令不一致'; return; }\n var r = await api('/api/reset-password', { currentPassword: a, newPassword: b });\n $('#pwOut').textContent = r.error || (r.text || '已重设');\n if (!r.error) { $('#pwCur').value = $('#pwNew').value = $('#pwNew2').value = ''; }\n });\n $('#btnRk').addEventListener('click', async function () {\n var r = await api('/api/recovery-key');\n if (r.error) { toast(r.error); return; }\n openModal('恢复钥匙(离线保存)', '<p class=\"muted\">泄露等同口令泄露。忘记主口令时可用它重设。</p><textarea class=\"keybox\" readonly>' + esc(r.recoveryKey) + '</textarea>', { okText: '我已保存', onOk: closeModal });\n });\n $('#btnExport').addEventListener('click', async function () {\n var r = await api('/api/export');\n if (r.error) { toast(r.error); return; }\n var blob = new Blob([JSON.stringify(r, null, 2)], { type: 'application/json' });\n var a = document.createElement('a');\n a.href = URL.createObjectURL(blob);\n a.download = 'yottamemory-export.json';\n a.click();\n toast('已导出 JSON');\n });\n $('#btnDangerHint').addEventListener('click', function () {\n toast('在「AI 与权限」列表里,每个 AI 右侧有「删除」按钮');\n });\n\n /* 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 $('#cliFull').innerHTML = (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 }\n async function ensureHelp() {\n if (state.help) { renderQuick(); return; }\n var d = await api('/api/help');\n if (!d || d.error) { toast((d && d.error) || '帮助加载失败'); 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";
|
|
310
357
|
// @generated view-html:end
|
|
311
358
|
// MCP 协议(2026-07-28 无状态 + 2025-11-25 legacy 握手,dual-era)
|
|
312
359
|
const MCP_PROTOCOL_MODERN = '2026-07-28';
|
|
@@ -6972,6 +7019,10 @@ function viewServerCore(root, port, host, opts) {
|
|
|
6972
7019
|
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
6973
7020
|
return readBody(function (d) { json(200, viewEntriesCore(root, session, d.query, d.offset, d.limit, d.type)); });
|
|
6974
7021
|
}
|
|
7022
|
+
if (req.method === 'POST' && pathname === '/api/help') {
|
|
7023
|
+
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
7024
|
+
return json(200, { version: VERSION, quick: VIEW_HELP_QUICK, groups: HELP_MODEL });
|
|
7025
|
+
}
|
|
6975
7026
|
if (req.method === 'POST' && pathname === '/api/overview') {
|
|
6976
7027
|
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
6977
7028
|
return json(200, VIEW_KEYS_CONTEXT.run({ keys: session.ownerKeys }, function () { return viewOverviewCore(root, session); }));
|
|
@@ -11747,6 +11798,7 @@ if (require.main === module) { main().catch(function (e) { console.error('错误
|
|
|
11747
11798
|
module.exports = {
|
|
11748
11799
|
VERSION: VERSION,
|
|
11749
11800
|
HELP_MODEL: HELP_MODEL,
|
|
11801
|
+
VIEW_HELP_QUICK: VIEW_HELP_QUICK,
|
|
11750
11802
|
CLI_FLAG_OPTS: CLI_FLAG_OPTS,
|
|
11751
11803
|
CLI_VALUE_OPTS: CLI_VALUE_OPTS,
|
|
11752
11804
|
renderHelp: renderHelp,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yottameta/yotta-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
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": [
|