dsh-memory-core 0.1.2 → 0.1.4

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/manage.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>遮蔽会话记忆管理 — dsh-memory-core</title>
6
+ <title>遮蔽会话记忆 — dsh-memory-core</title>
7
7
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230ea5e9'/%3E%3Cpath d='M5 4h6v2H5zM5 8h6v2H5zM5 12h4v2H5z' fill='white'/%3E%3C/svg%3E">
8
8
  <style>
9
9
  :root {
@@ -49,16 +49,12 @@
49
49
  header {
50
50
  background: var(--card);
51
51
  border-bottom: 1px solid var(--border);
52
- padding: 14px 20px;
53
- display: flex;
54
- align-items: center;
55
- justify-content: space-between;
56
- flex-wrap: wrap;
57
- gap: 10px;
52
+ padding: 14px 20px 0;
58
53
  position: sticky;
59
54
  top: 0;
60
55
  z-index: 5;
61
56
  }
57
+ .head-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; }
62
58
  header h1 { font-size: 16px; margin: 0; font-weight: 600; }
63
59
  .status-line { display: flex; gap: 8px; flex-wrap: wrap; font-size: 12px; color: var(--muted); }
64
60
  .chip {
@@ -66,148 +62,207 @@
66
62
  border: 1px solid var(--border);
67
63
  border-radius: 999px;
68
64
  padding: 2px 10px;
65
+ font-family: ui-monospace, Consolas, monospace;
66
+ font-size: 12px;
69
67
  }
70
- .chip.badge { background: var(--accent-weak); color: var(--accent); border-color: transparent; }
71
68
  .chip.ok { background: var(--ok-weak); color: var(--ok); border-color: transparent; }
72
69
  .chip.off { background: var(--danger-weak); color: var(--danger); border-color: transparent; }
73
- main { max-width: 1100px; margin: 20px auto; padding: 0 20px; }
74
- .layout { display: grid; grid-template-columns: 200px 1fr; gap: 16px; }
75
- @media (max-width: 720px) { .layout { grid-template-columns: 1fr; } }
76
- nav.tabs {
77
- background: var(--card);
78
- border: 1px solid var(--border);
79
- border-radius: var(--radius);
80
- padding: 8px;
81
- align-self: start;
82
- position: sticky;
83
- top: 76px;
84
- }
70
+ nav.tabs { display: flex; gap: 2px; flex-wrap: wrap; margin-top: 10px; }
85
71
  nav.tabs button {
86
- display: block;
87
- width: 100%;
88
- text-align: left;
89
72
  background: none;
90
73
  border: none;
91
- padding: 9px 12px;
92
- border-radius: 6px;
74
+ border-bottom: 2px solid transparent;
75
+ padding: 8px 12px;
93
76
  cursor: pointer;
94
77
  font-size: 13px;
95
- color: var(--text);
96
- margin-bottom: 2px;
78
+ color: var(--muted);
79
+ white-space: nowrap;
97
80
  }
98
- nav.tabs button:hover { background: var(--bg); }
99
- nav.tabs button.active { background: var(--accent-weak); color: var(--accent); font-weight: 600; }
100
- nav.tabs button .count { float: right; color: var(--muted); font-weight: 400; }
101
- section.panel { display: none; }
102
- section.panel.active { display: block; }
103
- .card {
81
+ nav.tabs button:hover { color: var(--text); }
82
+ nav.tabs button.active { color: var(--text); border-bottom-color: var(--text); font-weight: 600; }
83
+ nav.tabs button .count {
84
+ display: inline-block;
85
+ margin-left: 6px;
86
+ padding: 0 6px;
87
+ border-radius: 10px;
88
+ background: var(--accent-weak);
89
+ color: var(--accent);
90
+ font-size: 11px;
91
+ font-weight: 400;
92
+ }
93
+ main { max-width: 1180px; margin: 18px auto 40px; padding: 0 20px; }
94
+ .warn {
95
+ background: var(--warn-weak);
96
+ color: var(--warn);
97
+ border-radius: 6px;
98
+ padding: 8px 12px;
99
+ font-size: 13px;
100
+ margin-bottom: 10px;
101
+ }
102
+ .cwdline { font-size: 13px; color: var(--muted); margin-bottom: 12px; }
103
+ .cwdline code { font-family: ui-monospace, Consolas, monospace; color: var(--text); }
104
+ .cwdline #cwd-input { width: 320px; max-width: 55vw; font-size: 13px; padding: 4px 8px; border: 1px solid var(--border); border-radius: 6px; background: #fff; color: var(--text); margin-right: 6px; }
105
+ .mini { font-size: 12px; padding: 3px 10px; border: 1px solid var(--border); background: #fff; border-radius: 6px; cursor: pointer; color: var(--text); }
106
+ .mini:disabled { opacity: 0.45; cursor: default; }
107
+ .pager { display: flex; gap: 10px; align-items: center; justify-content: center; margin: 14px 0 4px; font-size: 13px; color: var(--muted); }
108
+ .desc { font-size: 13px; color: var(--muted); margin-bottom: 10px; }
109
+ .filebar {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 10px;
113
+ flex-wrap: wrap;
104
114
  background: var(--card);
105
115
  border: 1px solid var(--border);
106
116
  border-radius: var(--radius);
107
- padding: 14px 16px;
108
- margin-bottom: 14px;
117
+ padding: 10px 14px;
118
+ margin-bottom: 10px;
109
119
  }
110
- .card h3 { margin: 0 0 10px; font-size: 14px; font-weight: 600; }
111
- .card h3 .hint { font-weight: 400; font-size: 12px; color: var(--muted); margin-left: 6px; }
112
- .empty { color: var(--muted); font-size: 13px; padding: 8px 0; }
113
- .entry {
120
+ .filebar .name { font-weight: 600; font-size: 14px; }
121
+ .filebar .path {
122
+ font-family: ui-monospace, Consolas, monospace;
123
+ font-size: 12px;
124
+ color: var(--muted);
125
+ word-break: break-all;
126
+ }
127
+ .filebar .spacer { flex: 1; }
128
+ .viewbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
129
+ .seg { display: inline-flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
130
+ .seg button { border: none; background: var(--card); padding: 5px 12px; cursor: pointer; font-size: 13px; color: var(--muted); }
131
+ .seg button.active { background: var(--accent-weak); color: var(--accent); font-weight: 600; }
132
+ #search {
133
+ flex: 1;
134
+ min-width: 220px;
135
+ padding: 6px 10px;
114
136
  border: 1px solid var(--border);
115
137
  border-radius: 6px;
116
- padding: 10px 12px;
117
- margin-bottom: 8px;
118
- position: relative;
138
+ background: var(--card);
139
+ color: var(--text);
140
+ font-size: 13px;
141
+ }
142
+ .card {
143
+ background: var(--card);
144
+ border: 1px solid var(--border);
145
+ border-radius: var(--radius);
146
+ padding: 14px 16px;
147
+ margin-bottom: 12px;
119
148
  }
149
+ .card h3 { font-size: 14px; margin: 0 0 10px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
150
+ .card h3 .hint { font-weight: 400; color: var(--muted); font-size: 12px; }
151
+ .entry { border-top: 1px solid var(--border); padding: 10px 0; }
152
+ .entry:first-of-type { border-top: none; }
120
153
  .entry pre {
121
- margin: 0 0 8px;
122
154
  white-space: pre-wrap;
123
155
  word-break: break-word;
124
- font-family: inherit;
125
- font-size: 13px;
156
+ margin: 0 0 8px;
157
+ font: inherit;
126
158
  color: var(--text);
127
159
  }
128
- .entry .date { font-size: 11px; color: var(--muted); margin-right: 8px; }
129
- .entry .actions { display: flex; gap: 6px; flex-wrap: wrap; }
160
+ .entry .date { display: inline-block; margin-right: 8px; color: var(--muted); font-size: 12px; }
130
161
  .entry textarea {
131
162
  width: 100%;
132
- min-height: 64px;
163
+ min-height: 90px;
164
+ padding: 8px;
133
165
  border: 1px solid var(--border);
134
166
  border-radius: 6px;
135
- padding: 8px;
136
- font: 13px/1.5 inherit;
137
- resize: vertical;
167
+ background: var(--bg);
168
+ color: var(--text);
169
+ font: inherit;
170
+ margin-bottom: 8px;
138
171
  }
172
+ .actions { display: flex; gap: 8px; flex-wrap: wrap; }
139
173
  button {
140
- font: 12px/1.4 inherit;
141
174
  border: 1px solid var(--border);
142
175
  background: var(--card);
143
176
  color: var(--text);
144
177
  border-radius: 6px;
145
178
  padding: 4px 10px;
179
+ font-size: 12px;
146
180
  cursor: pointer;
147
181
  }
148
182
  button:hover { border-color: var(--accent); color: var(--accent); }
149
183
  button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
150
- button.primary:hover { opacity: .9; color: #fff; }
151
- button.danger { color: var(--danger); border-color: #f1c0c6; }
152
- button.danger:hover { background: var(--danger-weak); border-color: var(--danger); color: var(--danger); }
153
- button.ok { color: var(--ok); border-color: #b9dec8; }
154
- button.ok:hover { background: var(--ok-weak); border-color: var(--ok); color: var(--ok); }
155
- button:disabled { opacity: .45; cursor: not-allowed; }
156
- .add-row { display: flex; gap: 8px; margin-top: 4px; }
184
+ button.primary:hover { color: #fff; opacity: .9; }
185
+ button.ok { color: var(--ok); }
186
+ button.danger { color: var(--danger); }
187
+ .empty { color: var(--muted); font-size: 13px; padding: 6px 0; }
188
+ .raw-view {
189
+ background: var(--card);
190
+ border: 1px solid var(--border);
191
+ border-radius: var(--radius);
192
+ padding: 14px;
193
+ white-space: pre-wrap;
194
+ word-break: break-word;
195
+ font-family: ui-monospace, Consolas, monospace;
196
+ font-size: 12px;
197
+ margin: 0;
198
+ }
199
+ .add-row { display: flex; gap: 8px; }
157
200
  .add-row input {
158
201
  flex: 1;
202
+ padding: 8px 10px;
159
203
  border: 1px solid var(--border);
160
204
  border-radius: 6px;
161
- padding: 8px 10px;
162
- font: 13px inherit;
205
+ background: var(--bg);
206
+ color: var(--text);
207
+ font: inherit;
163
208
  }
164
209
  .toast {
165
210
  position: fixed;
166
- bottom: 24px;
167
211
  left: 50%;
168
- transform: translateX(-50%);
169
- background: #23272f;
212
+ bottom: 28px;
213
+ transform: translateX(-50%) translateY(20px);
214
+ background: #1f2430;
170
215
  color: #fff;
171
216
  padding: 8px 16px;
172
- border-radius: 8px;
217
+ border-radius: 6px;
173
218
  font-size: 13px;
174
219
  opacity: 0;
175
- transition: opacity .25s;
176
220
  pointer-events: none;
221
+ transition: opacity .18s, transform .18s;
177
222
  z-index: 20;
178
- max-width: 80vw;
179
- }
180
- .toast.show { opacity: 1; }
181
- .pending-entry { border-left: 3px solid var(--warn); }
182
- .archived-entry { border-left: 3px solid var(--muted); opacity: .88; }
183
- .note { font-size: 12px; color: var(--muted); margin-top: 6px; }
184
- .full-only { display: none; }
185
- body.full .full-only { display: initial; }
223
+ }
224
+ .toast.show { opacity: .95; transform: translateX(-50%) translateY(0); }
225
+ .guide h4 { margin: 14px 0 6px; font-size: 13px; }
226
+ .guide p, .guide li { font-size: 13px; color: var(--text); }
227
+ .guide ul { margin: 4px 0; padding-left: 20px; }
228
+ .guide code { font-family: ui-monospace, Consolas, monospace; background: var(--bg); padding: 1px 5px; border-radius: 4px; font-size: 12px; }
186
229
  </style>
187
230
  </head>
188
231
  <body>
232
+
189
233
  <header>
190
- <h1>遮蔽会话记忆管理</h1>
191
- <div class="status-line" id="status-line">
192
- <span class="chip" id="chip-enabled">加载中…</span>
193
- <span class="chip" id="chip-tracks">—</span>
194
- <span class="chip" id="chip-preset">preset: —</span>
195
- <span class="chip" id="chip-dir">—</span>
234
+ <div class="head-row">
235
+ <h1>遮蔽会话记忆</h1>
236
+ <div class="status-line">
237
+ <span class="chip ok" id="chip-mode"></span>
238
+ <span class="chip" id="chip-tracks"></span>
239
+ <span class="chip" id="chip-preset"></span>
240
+ <span class="chip" id="chip-dir"></span>
241
+ </div>
196
242
  </div>
243
+ <nav class="tabs" id="tabs"></nav>
197
244
  </header>
198
245
 
199
246
  <main>
200
- <div class="card" id="disabled-card" style="display:none">
201
- <h3>当前是纯遮蔽状态(无记忆)</h3>
202
- <p class="empty">本 preset 只遮蔽其他记忆插件,自己没有开记忆。三种状态:纯遮蔽(默认)/ 轻量记忆(三轨)/ 完整记忆(五轨)。</p>
203
- <p class="note">切到轻量或完整记忆:在插件设置里打开「记忆功能」(轻量默认 lite 三轨;想要五轨再设 memoryTracks: full),然后刷新本页。复制下面这句给 agent:<br>
204
- <code>请把 dsh-memory-core 切到「完整记忆」状态:memoryTracks=full,然后告诉我怎么用记忆管理面板。</code></p>
247
+ <div id="notice"></div>
248
+ <div class="desc" id="desc"></div>
249
+ <div class="filebar" id="filebar" style="display:none">
250
+ <span class="name" id="file-title"></span>
251
+ <span class="chip" id="file-mode">结构化</span>
252
+ <span class="chip" id="file-count"></span>
253
+ <span class="path" id="file-path"></span>
254
+ <span class="spacer"></span>
255
+ <button id="open-file">打开文件</button>
256
+ <button id="copy-path">复制路径</button>
205
257
  </div>
206
-
207
- <div class="layout" id="layout" style="display:none">
208
- <nav class="tabs" id="tabs"></nav>
209
- <div id="content"></div>
258
+ <div class="viewbar" id="viewbar" style="display:none">
259
+ <span class="seg">
260
+ <button id="view-pretty" class="active">美观视图</button>
261
+ <button id="view-raw">纯文本视图</button>
262
+ </span>
263
+ <input id="search" placeholder="搜索内容、时间或标签…">
210
264
  </div>
265
+ <div id="content"></div>
211
266
  </main>
212
267
 
213
268
  <div class="toast" id="toast"></div>
@@ -215,258 +270,491 @@
215
270
  <script>
216
271
  (function () {
217
272
  "use strict";
218
- const $ = (sel) => document.querySelector(sel);
219
- let state = { status: null, tracks: [], cache: {} };
273
+ var $ = function (sel) { return document.querySelector(sel); };
274
+ var params = new URLSearchParams(location.search);
275
+ var PAGE_SIZE = 20;
276
+ var state = {
277
+ status: null,
278
+ rows: [],
279
+ tab: null,
280
+ view: "pretty",
281
+ query: "",
282
+ page: 1,
283
+ cwd: params.get("cwd") || localStorage.getItem("pmc-cwd") || "",
284
+ cache: {},
285
+ };
286
+
287
+ // 页签顺序与 evolve 记忆页一致;AGENTS 不属于本包(全局规则由 dsh-agent-instructions 管),不设页签。
288
+ var TABS = [
289
+ { key: "guide", title: "指南" },
290
+ { key: "pending", title: "待确认记忆建议", desc: "写入后需人工确认的条目(如 key 轨的 writeGuard)。确认即写入对应轨,拒绝即丢弃。" },
291
+ { key: "project", title: "项目日志", track: "project", desc: "每回合收尾自动记录本回合进展;不注入上下文,模型按需读取。" },
292
+ { key: "key", title: "项目关键记忆 KEY.md", track: "key", desc: "下次会话不知道就会做错的事实(判据、坑、口径、约定);注入摘要,写入需确认。" },
293
+ { key: "archive:key", title: "项目关键记忆归档 KEY-archive.md", track: "key", archived: true, desc: "已归档的项目关键记忆,不注入任何会话,可移回主轨。" },
294
+ { key: "daily", title: "今日日志", track: "daily", desc: "按天记录当天进展;不注入上下文,模型按需读取。" },
295
+ { key: "user", title: "用户档案 USER.md", track: "user", desc: "关于用户的稳定事实与偏好;每轮注入上下文。" },
296
+ { key: "memory", title: "长期记忆 MEMORY.md", track: "memory", desc: "跨项目通用的事实与约定;每轮注入上下文。" },
297
+ { key: "archive:user", title: "归档用户 USER-archive.md", track: "user", archived: true, desc: "已归档的用户档案,不注入,可移回主轨。" },
298
+ { key: "archive:memory", title: "归档记忆 MEMORY-archive.md", track: "memory", archived: true, desc: "已归档的长期记忆,不注入,可移回主轨。" },
299
+ ];
300
+
301
+ function tabOf(key) {
302
+ for (var i = 0; i < TABS.length; i++) if (TABS[i].key === key) return TABS[i];
303
+ return null;
304
+ }
220
305
 
221
306
  function toast(msg) {
222
- const el = $("#toast");
307
+ var el = $("#toast");
223
308
  el.textContent = msg;
224
309
  el.classList.add("show");
225
310
  clearTimeout(el._t);
226
- el._t = setTimeout(() => el.classList.remove("show"), 2600);
311
+ el._t = setTimeout(function () { el.classList.remove("show"); }, 2600);
227
312
  }
228
313
 
229
314
  async function api(url, opts) {
230
- const res = await fetch(url, opts);
231
- const data = await res.json().catch(() => ({}));
232
- if (!res.ok || data.ok === false) {
233
- throw new Error(data.message || ("请求失败 HTTP " + res.status));
234
- }
315
+ var res = await fetch(url, opts);
316
+ var data = await res.json().catch(function () { return {}; });
317
+ if (!res.ok || data.ok === false) throw new Error(data.why || data.message || ("请求失败 HTTP " + res.status));
235
318
  return data;
236
319
  }
237
320
 
238
321
  function esc(s) {
239
- return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
322
+ return String(s == null ? "" : s)
323
+ .replace(/&/g, "&amp;")
324
+ .replace(/</g, "&lt;")
325
+ .replace(/>/g, "&gt;")
326
+ .replace(/"/g, "&quot;");
240
327
  }
241
328
 
242
- async function loadStatus() {
243
- const data = await api("/api/memory-core/status");
244
- state.status = data.data;
245
- state.tracks = data.data.tracks || [];
246
- renderStatus();
247
- if (data.data.memoryEnabled !== true) {
248
- $("#disabled-card").style.display = "";
249
- $("#layout").style.display = "none";
250
- return;
251
- }
252
- $("#disabled-card").style.display = "none";
253
- $("#layout").style.display = "";
254
- document.body.classList.toggle("full", data.data.memoryTracks === "full");
329
+ function fmtDate(entry) {
330
+ var m = /^\[(\d{4}-\d{2}-\d{2})(?: (\d{2}:\d{2}))?\]/.exec(entry);
331
+ return m ? m[1] + (m[2] ? " " + m[2] : "") : "";
332
+ }
333
+
334
+ function rowsUrl() {
335
+ return "/api/memory-core/files" + (state.cwd ? "?cwd=" + encodeURIComponent(state.cwd) : "");
336
+ }
337
+
338
+ function rowOf(track) {
339
+ for (var i = 0; i < state.rows.length; i++) if (state.rows[i].track === track) return state.rows[i];
340
+ return null;
341
+ }
342
+
343
+ async function boot() {
344
+ var files = await api(rowsUrl());
345
+ state.status = files.data;
346
+ state.rows = files.data.rows || [];
347
+ renderHeader();
255
348
  renderTabs();
256
- await loadTrack(state.tracks[0]);
349
+ var first = tabOf("project");
350
+ var projectRow = rowOf("project");
351
+ if (!projectRow || !projectRow.available) first = tabOf("memory");
352
+ await switchTab(first.key);
257
353
  }
258
354
 
259
- function renderStatus() {
260
- const s = state.status;
261
- const chipEnabled = $("#chip-enabled");
262
- if (s.memoryEnabled === true) {
263
- chipEnabled.textContent = s.memoryTracks === "full" ? "完整记忆(五轨)" : "轻量记忆(三轨)";
264
- chipEnabled.className = "chip ok";
265
- } else {
266
- chipEnabled.textContent = "纯遮蔽状态(无记忆)";
267
- chipEnabled.className = "chip off";
268
- }
269
- $("#chip-tracks").textContent = "tracks: " + (s.tracks || []).join(" / ");
355
+ function renderHeader() {
356
+ var s = state.status;
357
+ var mode = $("#chip-mode");
358
+ mode.textContent = s.memoryTracks === "full" ? "完整记忆(五轨)" : "轻量记忆(三轨)";
359
+ mode.className = "chip ok";
360
+ $("#chip-tracks").textContent = "tracks: " + (s.rows || []).map(function (r) { return r.track; }).join(" / ");
270
361
  $("#chip-preset").textContent = "preset: " + s.presetName;
271
362
  $("#chip-dir").textContent = s.memoryDir || "";
363
+ var html = '<div class="warn">⚠️ 以下文件为 § 分隔的结构化记忆,用系统工具打开后请谨慎编辑,随意修改可能破坏格式、导致记忆读取错乱。</div>';
364
+ if (state.cwd) {
365
+ html += '<div class="cwdline">当前会话工作目录:<code>' + esc(state.cwd) + '</code> <button class="mini" id="cwd-edit">修改</button></div>';
366
+ } else {
367
+ html += '<div class="cwdline">当前会话工作目录:<input id="cwd-input" placeholder="D:\\项目路径" value="' + esc(state.cwd) + '"> <button class="mini primary" id="cwd-set">使用</button> <span class="hint">项目日志与项目关键记忆需要它,填一次会记住</span></div>';
368
+ }
369
+ $("#notice").innerHTML = html;
370
+ var setBtn = $("#cwd-set");
371
+ if (setBtn) {
372
+ setBtn.addEventListener("click", function () {
373
+ var v = ($("#cwd-input").value || "").trim();
374
+ if (!v) { toast("目录为空"); return; }
375
+ state.cwd = v;
376
+ try { localStorage.setItem("pmc-cwd", v); } catch (e) {}
377
+ state.cache = {};
378
+ state.page = 1;
379
+ load();
380
+ });
381
+ }
382
+ var editBtn = $("#cwd-edit");
383
+ if (editBtn) {
384
+ editBtn.addEventListener("click", function () {
385
+ state.cwd = "";
386
+ try { localStorage.removeItem("pmc-cwd"); } catch (e) {}
387
+ state.cache = {};
388
+ state.page = 1;
389
+ load();
390
+ });
391
+ }
272
392
  }
273
393
 
274
394
  function renderTabs() {
275
- const tabs = $("#tabs");
395
+ var tabs = $("#tabs");
276
396
  tabs.innerHTML = "";
277
- const labels = {
278
- memory: "长期记忆", user: "用户档案", key: "关键事实",
279
- project: "项目记忆", daily: "每日日志",
280
- };
281
- for (const track of state.tracks) {
282
- const btn = document.createElement("button");
283
- btn.textContent = labels[track] || track;
284
- btn.dataset.track = track;
285
- btn.addEventListener("click", () => switchTrack(track));
397
+ for (var i = 0; i < TABS.length; i++) {
398
+ var t = TABS[i];
399
+ var n = 0;
400
+ if (t.key === "pending") {
401
+ for (var j = 0; j < state.rows.length; j++) n += state.rows[j].pending || 0;
402
+ } else if (t.archived) {
403
+ var ar = rowOf(t.track);
404
+ n = ar ? ar.archived : 0;
405
+ } else if (t.track) {
406
+ var row = rowOf(t.track);
407
+ n = row ? row.count : 0;
408
+ }
409
+ var btn = document.createElement("button");
410
+ btn.dataset.tab = t.key;
411
+ btn.innerHTML = t.key === "guide" ? esc(t.title) : esc(t.title) + '<span class="count">' + n + "</span>";
412
+ if (t.track) {
413
+ var rr = rowOf(t.track);
414
+ if (rr && !rr.available) btn.title = "需要会话工作目录";
415
+ }
416
+ btn.addEventListener("click", (function (key) {
417
+ return function () { switchTab(key); };
418
+ })(t.key));
286
419
  tabs.appendChild(btn);
287
420
  }
288
421
  }
289
422
 
290
- function setActiveTab(track) {
291
- for (const btn of document.querySelectorAll("#tabs button")) {
292
- btn.classList.toggle("active", btn.dataset.track === track);
293
- }
423
+ function setActiveTab(key) {
424
+ var btns = document.querySelectorAll("#tabs button");
425
+ for (var i = 0; i < btns.length; i++) btns[i].classList.toggle("active", btns[i].dataset.tab === key);
294
426
  }
295
427
 
296
- async function switchTrack(track) {
297
- setActiveTab(track);
298
- await loadTrack(track);
299
- }
428
+ async function switchTab(key) {
429
+ state.tab = key;
430
+ state.query = "";
431
+ state.page = 1;
432
+ $("#search").value = "";
433
+ state.view = "pretty";
434
+ $("#view-pretty").classList.add("active");
435
+ $("#view-raw").classList.remove("active");
436
+ setActiveTab(key);
437
+ var t = tabOf(key);
438
+ $("#desc").textContent = t && t.desc ? t.desc : "";
439
+
440
+ if (key === "guide") {
441
+ $("#filebar").style.display = "none";
442
+ $("#viewbar").style.display = "none";
443
+ renderGuide();
444
+ return;
445
+ }
446
+
447
+ if (key === "pending") {
448
+ $("#filebar").style.display = "none";
449
+ $("#viewbar").style.display = "none";
450
+ await renderPending();
451
+ return;
452
+ }
300
453
 
301
- async function loadTrack(track) {
302
- const data = await api("/api/memory-core/entries?track=" + encodeURIComponent(track));
303
- state.cache[track] = data.data;
304
- renderTrack(track);
454
+ var base = t.archived ? t.track : t.track;
455
+ var q = new URLSearchParams({ track: base });
456
+ if (state.cwd) q.set("cwd", state.cwd);
457
+ var data = await api("/api/memory-core/entries?" + q.toString());
458
+ state.cache[key] = { rows: data.data, tab: t };
459
+ $("#filebar").style.display = "";
460
+ $("#viewbar").style.display = "";
461
+ $("#file-title").textContent = t.title;
462
+ $("#file-count").textContent = (t.archived ? (data.data.archived || []).length : (data.data.entries || []).length) + " 条";
463
+ var row = rowOf(base);
464
+ $("#file-path").textContent = row && row.path ? row.path : "(不可用:需要会话工作目录)";
465
+ $("#open-file").style.display = row && row.path ? "" : "none";
466
+ renderTrack();
305
467
  }
306
468
 
307
- function renderTrack(track) {
308
- const d = state.cache[track];
309
- const content = $("#content");
310
- const labels = {
311
- memory: "长期记忆", user: "用户档案", key: "关键事实",
312
- project: "项目记忆", daily: "每日日志",
313
- };
314
- const html = [];
315
- html.push('<section class="panel active">');
469
+ function renderGuide() {
470
+ $("#content").innerHTML = ''
471
+ + '<div class="card guide">'
472
+ + '<h3>这是什么</h3>'
473
+ + '<p>这是 <code>dsh-memory-core</code> 的管理页:给被 <code>dsh-memory-shield</code> 遮蔽的会话提供一套隔离记忆,与全局 <code>dsh-memory-evolve</code> 的存储互不影响。</p>'
474
+ + '<h4>五条记忆轨</h4>'
475
+ + '<ul>'
476
+ + '<li><code>memory</code> 长期记忆 MEMORY.md:跨项目通用事实,每轮注入。</li>'
477
+ + '<li><code>user</code> 用户档案 USER.md:关于用户的稳定偏好,每轮注入。</li>'
478
+ + '<li><code>project</code> 项目日志:按项目(工作目录哈希)记录每回合进展,不注入、按需读取。</li>'
479
+ + '<li><code>key</code> 项目关键记忆 KEY.md:下次会话不知道就会做错的事实,注入摘要,写入需确认。</li>'
480
+ + '<li><code>daily</code> 今日日志:按天记录,不注入。</li>'
481
+ + '</ul>'
482
+ + '<p>lite 模式只开 memory / user / key 三条;full 模式五条全开(默认)。轨数决定「可写与可见」,不改变注入面。</p>'
483
+ + '<h4>机制:怎么注入</h4>'
484
+ + '<ul>'
485
+ + '<li>每轮把三条注入轨拼成一段快照(section <code>memory-core:snapshot</code>),总预算 <b>2,000 字符</b>;超出时保留最近的条目,从旧条目尾部截断。</li>'
486
+ + '<li>预算分配:<code>memory</code> 1,000 → <code>user</code> 600 → <code>key</code> 400,按此顺序累加,用完即停。</li>'
487
+ + '<li><code>key</code> 轨另有加权:先取最近 8 条,按「不要 / 必须 / 注意 / 重要 / 偏好 / 禁止 / 始终 / 永远」的命中数排序取前 5 条,再补最多 600 字符。</li>'
488
+ + '<li><code>project</code> 与 <code>daily</code> 不进快照,模型要用时通过 <code>memory_core</code> 的 <code>list</code> 现读。</li>'
489
+ + '<li>写入提醒:连续 2 个回合没写记忆,快照末尾追加一句提醒;子代理会话不提醒。</li>'
490
+ + '</ul>'
491
+ + '<h4>机制:怎么写</h4>'
492
+ + '<ul>'
493
+ + '<li>工具是 <code>memory_core</code>,动作 <code>add / list / replace / remove / archive / promote / removeArchive / confirm / reject</code>,target 为五轨之一。</li>'
494
+ + '<li><code>key</code> 轨的 <code>add</code> 先进「待确认记忆建议」,在待确认页签点「允许并写入」才落盘。</li>'
495
+ + '<li>本页可以直接新增、编辑、归档、删除,改动立即写文件;归档可逆,能从归档页签移回主轨。</li>'
496
+ + '</ul>'
497
+ + '<h4>机制:存在哪</h4>'
498
+ + '<ul>'
499
+ + '<li>数据目录与 <code>DSH_HOME</code> 同级:<code>&lt;DSH_HOME 的上级&gt;/.dsh-plugin-data/&lt;presetName&gt;/memory</code>。</li>'
500
+ + '<li><code>memory.md</code>、<code>user.md</code>、<code>daily/&lt;日期&gt;.md</code>;项目相关的落 <code>projects/&lt;工作目录哈希&gt;/memory.md</code> 与 <code>key.md</code>。</li>'
501
+ + '<li>条目以 <code>§</code> 分隔,归档写在同目录的 archive 文件里。</li>'
502
+ + '<li>整套存储与 <code>dsh-memory-evolve</code> 完全隔离,互相看不到对方的条目。</li>'
503
+ + '</ul>'
504
+ + '<h4>为什么没有「全局规则 AGENTS.md」页签</h4>'
505
+ + '<p>AGENTS.md 归 <code>dsh-agent-instructions</code> 管,不在本包的记忆目录里。</p>'
506
+ + '<h4>致谢</h4>'
507
+ + '<p>存储层(<code>lib/store.js</code>)与记忆页的形制参考 <a href="https://github.com/csyangwen/dsh-memory-evolve" target="_blank" rel="noreferrer">dsh-memory-evolve</a>(作者 csyangwen,MIT),按「隔离会话」的需要做了适配。</p>'
508
+ + '</div>';
509
+ }
316
510
 
317
- // 正式条目
318
- html.push('<div class="card"><h3>' + esc(labels[track] || track) + ' · 正式条目 <span class="hint">' + (d.entries || []).length + ' 条</span></h3>');
319
- if (!d.entries || d.entries.length === 0) {
320
- html.push('<div class="empty">暂无记忆条目。</div>');
321
- } else {
322
- for (let i = 0; i < d.entries.length; i++) {
323
- const entry = d.entries[i];
324
- const dateMatch = /^\[(\d{4}-\d{2}-\d{2})/.exec(entry);
325
- const date = dateMatch ? dateMatch[1] : "";
326
- html.push('<div class="entry" data-index="' + i + '">');
327
- html.push('<pre>' + (date ? '<span class="date">' + esc(date) + '</span>' : "") + esc(entry) + '</pre>');
328
- html.push('<div class="actions">');
329
- html.push('<button data-op="edit" data-track="' + esc(track) + '" data-index="' + i + '">编辑</button>');
330
- html.push('<button data-op="archive" data-track="' + esc(track) + '" data-index="' + i + '">归档</button>');
331
- html.push('<button data-op="delete" data-track="' + esc(track) + '" data-index="' + i + '" class="danger">删除</button>');
511
+ async function renderPending() {
512
+ var html = [];
513
+ var total = 0;
514
+ for (var i = 0; i < state.rows.length; i++) {
515
+ var row = state.rows[i];
516
+ if (!row.pending) continue;
517
+ var q = new URLSearchParams({ track: row.track });
518
+ if (state.cwd) q.set("cwd", state.cwd);
519
+ var data;
520
+ try { data = await api("/api/memory-core/entries?" + q.toString()); } catch (e) { continue; }
521
+ var list = data.data.pending || [];
522
+ if (!list.length) continue;
523
+ total += list.length;
524
+ html.push('<div class="card"><h3>' + esc(row.title) + ' <span class="hint">' + list.length + ' 条</span></h3>');
525
+ for (var j = 0; j < list.length; j++) {
526
+ var p = list[j];
527
+ var text = p.content || p.entry || "";
528
+ html.push('<div class="entry pending-entry"><pre>' + esc(text) + '</pre><div class="actions">');
529
+ html.push('<button data-op="confirm" data-track="' + esc(row.track) + '" data-entry="' + esc(text) + '" class="ok">允许并写入</button>');
530
+ html.push('<button data-op="reject" data-track="' + esc(row.track) + '" data-entry="' + esc(text) + '" class="danger">拒绝</button>');
332
531
  html.push('</div></div>');
333
532
  }
533
+ html.push('</div>');
334
534
  }
335
- html.push('</div>');
535
+ if (!total) html.push('<div class="card"><div class="empty">暂无待确认记忆。</div></div>');
536
+ $("#content").innerHTML = html.join("");
537
+ bindActions();
538
+ }
336
539
 
337
- // 待确认
338
- html.push('<div class="card"><h3>待确认 <span class="hint">' + (d.pending || []).length + ' 条 · 写入后需人工确认</span></h3>');
339
- if (!d.pending || d.pending.length === 0) {
340
- html.push('<div class="empty">暂无待确认记忆。</div>');
341
- } else {
342
- for (let i = 0; i < d.pending.length; i++) {
343
- const sug = d.pending[i];
344
- html.push('<div class="entry pending-entry">');
345
- html.push('<pre>' + esc(sug.entry ?? sug.content ?? "") + '</pre>');
346
- html.push('<div class="actions">');
347
- html.push('<button data-op="confirm" data-track="' + esc(track) + '" data-index="' + i + '" class="ok">允许并写入</button>');
348
- html.push('<button data-op="reject" data-track="' + esc(track) + '" data-index="' + i + '" class="danger">拒绝</button>');
349
- html.push('</div></div>');
350
- }
540
+ function filtered(list) {
541
+ if (!state.query) return list;
542
+ var q = state.query.toLowerCase();
543
+ return list.filter(function (e) { return String(e).toLowerCase().indexOf(q) >= 0; });
544
+ }
545
+
546
+ function pagerHtml(pages, total) {
547
+ if (pages <= 1) return "";
548
+ return (
549
+ '<div class="pager"><button class="mini" data-page="prev"' + (state.page <= 1 ? " disabled" : "") + ">上一页</button>" +
550
+ "<span>" + state.page + " / " + pages + " · 共 " + total + " 条</span>" +
551
+ '<button class="mini" data-page="next"' + (state.page >= pages ? " disabled" : "") + ">下一页</button></div>"
552
+ );
553
+ }
554
+
555
+ function bindPager() {
556
+ var btns = document.querySelectorAll("[data-page]");
557
+ for (var i = 0; i < btns.length; i++) {
558
+ btns[i].addEventListener("click", function (ev) {
559
+ var dir = ev.currentTarget.getAttribute("data-page");
560
+ state.page += dir === "next" ? 1 : -1;
561
+ if (state.page < 1) state.page = 1;
562
+ renderTrack();
563
+ });
351
564
  }
352
- html.push('</div>');
565
+ }
353
566
 
354
- // 归档
355
- html.push('<div class="card"><h3>归档 <span class="hint">' + (d.archived || []).length + ' 条 · 不注入任何会话</span></h3>');
356
- if (!d.archived || d.archived.length === 0) {
357
- html.push('<div class="empty">暂无归档条目。</div>');
358
- } else {
359
- for (let i = 0; i < d.archived.length; i++) {
360
- const entry = d.archived[i];
361
- html.push('<div class="entry archived-entry">');
362
- html.push('<pre>' + esc(entry) + '</pre>');
363
- html.push('<div class="actions">');
364
- html.push('<button data-op="promote" data-track="' + esc(track) + '" data-index="' + i + '" class="ok">移回主轨</button>');
365
- html.push('<button data-op="removeArchive" data-track="' + esc(track) + '" data-index="' + i + '" class="danger">删除</button>');
366
- html.push('</div></div>');
567
+ function renderTrack() {
568
+ var t = tabOf(state.tab);
569
+ var d = state.cache[state.tab];
570
+ var content = $("#content");
571
+ if (!d || !t) { content.innerHTML = ""; return; }
572
+ var items = (t.archived ? d.rows.archived : d.rows.entries) || [];
573
+ // 最新在前:文件里是追加写,直接渲染会把最旧的放最上面
574
+ var list = filtered(items).slice().reverse();
575
+ var total = list.length;
576
+ var pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
577
+ if (state.page > pages) state.page = pages;
578
+ var start = (state.page - 1) * PAGE_SIZE;
579
+ var pageList = list.slice(start, start + PAGE_SIZE);
580
+
581
+ if (state.view === "raw") {
582
+ content.innerHTML = '<pre class="raw-view">' + esc(pageList.join("\n§\n")) + "</pre>" + pagerHtml(pages, total);
583
+ bindPager();
584
+ return;
585
+ }
586
+
587
+ var html = [];
588
+
589
+ if (t.archived) {
590
+ html.push('<div class="card"><h3>归档 <span class="hint">' + total + ' 条 · 不注入任何会话,可移回主轨</span></h3>');
591
+ if (!pageList.length) html.push('<div class="empty">' + (state.query ? "没有匹配的归档条目。" : "暂无归档条目。") + "</div>");
592
+ for (var i = 0; i < pageList.length; i++) {
593
+ var entry = pageList[i];
594
+ html.push('<div class="entry archived-entry"><pre>' + esc(entry) + '</pre><div class="actions">');
595
+ html.push('<button data-op="promote" data-track="' + esc(t.track) + '" data-entry="' + esc(entry) + '" class="ok">移回主轨</button>');
596
+ html.push('<button data-op="removeArchive" data-track="' + esc(t.track) + '" data-entry="' + esc(entry) + '" class="danger">删除</button>');
597
+ html.push("</div></div>");
367
598
  }
599
+ html.push(pagerHtml(pages, total));
600
+ html.push("</div>");
601
+ content.innerHTML = html.join("");
602
+ bindActions();
603
+ bindPager();
604
+ return;
368
605
  }
369
- html.push('</div>');
370
606
 
371
- // 新增
372
- html.push('<div class="card"><h3>新增条目</h3>');
373
- html.push('<div class="add-row">');
374
- html.push('<input id="new-entry-input" placeholder="写一条 ' + esc(labels[track] || track) + ' 记忆…">');
375
- html.push('<button id="new-entry-btn" class="primary">写入</button>');
376
- html.push('</div></div>');
607
+ html.push('<div class="card"><h3>' + esc(t.title.replace(/\s+[A-Z-]+\.md$/, "")) + ' · 正式条目 <span class="hint">' + total + ' 条</span></h3>');
608
+ if (!pageList.length) html.push('<div class="empty">' + (state.query ? "没有匹配的条目。" : "暂无记忆条目。") + "</div>");
609
+ for (var k = 0; k < pageList.length; k++) {
610
+ var text = pageList[k];
611
+ var date = fmtDate(text);
612
+ html.push('<div class="entry"><pre>' + (date ? '<span class="date">' + esc(date) + "</span>" : "") + esc(text) + "</pre>");
613
+ html.push('<div class="actions">');
614
+ html.push('<button data-op="edit" data-track="' + esc(t.track) + '" data-entry="' + esc(text) + '">编辑</button>');
615
+ html.push('<button data-op="archive" data-track="' + esc(t.track) + '" data-entry="' + esc(text) + '">归档</button>');
616
+ html.push('<button data-op="delete" data-track="' + esc(t.track) + '" data-entry="' + esc(text) + '" class="danger">删除</button>');
617
+ html.push("</div></div>");
618
+ }
619
+ html.push(pagerHtml(pages, total));
620
+ html.push("</div>");
621
+
622
+ html.push('<div class="card"><h3>新增条目 <span class="hint">写入 ' + esc(t.title) + "</span></h3>");
623
+ html.push('<div class="add-row"><input id="new-entry-input" placeholder="写一条 ' + esc(t.title) + '…"><button id="new-entry-btn" class="primary">写入</button></div></div>');
377
624
 
378
- html.push('</section>');
379
625
  content.innerHTML = html.join("");
626
+ $("#new-entry-btn").addEventListener("click", addEntry);
627
+ $("#new-entry-input").addEventListener("keydown", function (ev) { if (ev.key === "Enter") addEntry(); });
628
+ bindActions();
629
+ bindPager();
630
+ }
631
+
632
+ function currentTrack() {
633
+ var t = tabOf(state.tab);
634
+ return t && t.track ? t.track : "memory";
635
+ }
636
+
637
+ async function addEntry() {
638
+ var input = $("#new-entry-input");
639
+ var text = input.value.trim();
640
+ if (!text) { toast("内容为空"); return; }
641
+ var track = currentTrack();
642
+ try {
643
+ await api("/api/memory-core/entry", {
644
+ method: "POST",
645
+ headers: { "Content-Type": "application/json" },
646
+ body: JSON.stringify({ action: "add", track: track, content: text, cwd: state.cwd || undefined }),
647
+ });
648
+ toast("已写入 " + track);
649
+ await refreshRows();
650
+ await switchTab(state.tab);
651
+ } catch (e) { toast(e.message); }
652
+ }
653
+
654
+ async function refreshRows() {
655
+ var files = await api(rowsUrl());
656
+ state.status = files.data;
657
+ state.rows = files.data.rows || [];
658
+ renderTabs();
659
+ }
660
+
661
+ function bindActions() {
662
+ var btns = document.querySelectorAll("#content button[data-op]");
663
+ for (var i = 0; i < btns.length; i++) {
664
+ btns[i].addEventListener("click", onClickAction);
665
+ }
666
+ }
667
+
668
+ async function onClickAction(ev) {
669
+ var btn = ev.currentTarget;
670
+ var op = btn.dataset.op;
671
+ var track = btn.dataset.track;
672
+ var entry = btn.dataset.entry;
380
673
 
381
- // 新增条目
382
- $("#new-entry-btn").addEventListener("click", async () => {
383
- const input = $("#new-entry-input");
384
- const contentText = input.value.trim();
385
- if (!contentText) { toast("内容为空"); return; }
674
+ if (op === "edit") {
675
+ var div = btn.closest(".entry");
676
+ var pre = div.querySelector("pre");
677
+ var ta = document.createElement("textarea");
678
+ ta.value = entry;
679
+ pre.replaceWith(ta);
680
+ btn.textContent = "保存";
681
+ btn.dataset.op = "saveEdit";
682
+ btn.classList.add("primary");
683
+ var cancel = document.createElement("button");
684
+ cancel.textContent = "取消";
685
+ btn.after(cancel);
686
+ cancel.addEventListener("click", function () { switchTab(state.tab); });
687
+ return;
688
+ }
689
+
690
+ if (op === "saveEdit") {
691
+ var ta2 = btn.closest(".entry").querySelector("textarea");
386
692
  try {
387
693
  await api("/api/memory-core/entry", {
388
694
  method: "POST",
389
695
  headers: { "Content-Type": "application/json" },
390
- body: JSON.stringify({ action: "add", track, content: contentText }),
696
+ body: JSON.stringify({ action: "replace", track: track, entry: entry, content: ta2.value, cwd: state.cwd || undefined }),
391
697
  });
392
- toast("已写入 " + (labels[track] || track));
393
- await loadTrack(track);
698
+ toast("已更新");
699
+ await switchTab(state.tab);
394
700
  } catch (e) { toast(e.message); }
395
- });
396
- $("#new-entry-input").addEventListener("keydown", (ev) => {
397
- if (ev.key === "Enter") $("#new-entry-btn").click();
398
- });
399
-
400
- // 条目操作
401
- content.querySelectorAll("button[data-op]").forEach((btn) => {
402
- btn.addEventListener("click", async () => {
403
- const op = btn.dataset.op;
404
- const index = Number(btn.dataset.index);
405
- const body = { action: op, track };
406
- if (["edit"].includes(op)) {
407
- const entryDiv = btn.closest(".entry");
408
- const pre = entryDiv.querySelector("pre");
409
- const original = state.cache[track].entries[index];
410
- if (!original) return;
411
- const textarea = document.createElement("textarea");
412
- textarea.value = original;
413
- pre.replaceWith(textarea);
414
- btn.textContent = "保存";
415
- btn.dataset.op = "saveEdit";
416
- btn.dataset.index = index;
417
- btn.classList.add("primary");
418
- const cancelBtn = document.createElement("button");
419
- cancelBtn.textContent = "取消";
420
- cancelBtn.dataset.op = "cancelEdit";
421
- cancelBtn.dataset.index = index;
422
- btn.after(cancelBtn);
423
- return;
424
- }
425
- if (op === "saveEdit") {
426
- const entryDiv = btn.closest(".entry");
427
- const textarea = entryDiv.querySelector("textarea");
428
- body.entry = state.cache[track].entries[index];
429
- body.content = textarea.value;
430
- try {
431
- await api("/api/memory-core/entry", {
432
- method: "POST",
433
- headers: { "Content-Type": "application/json" },
434
- body: JSON.stringify(body),
435
- });
436
- toast("已更新");
437
- await loadTrack(track);
438
- } catch (e) { toast(e.message); }
439
- return;
440
- }
441
- if (op === "cancelEdit") {
442
- await loadTrack(track);
443
- return;
444
- }
445
- const source =
446
- op === "delete" || op === "archive" ? state.cache[track].entries[index]
447
- : op === "confirm" || op === "reject" ? state.cache[track].pending[index].content
448
- : op === "promote" || op === "removeArchive" ? state.cache[track].archived[index]
449
- : undefined;
450
- if (source === undefined) return;
451
- if (op === "delete" && !confirm("确认删除这条记忆?此操作不可撤销。")) return;
452
- body.entry = source;
453
- try {
454
- await api("/api/memory-core/entry", {
455
- method: "POST",
456
- headers: { "Content-Type": "application/json" },
457
- body: JSON.stringify(body),
458
- });
459
- const msg = { archive: "已归档", delete: "已删除", confirm: "已允许并写入", reject: "已拒绝", promote: "已移回主轨", removeArchive: "已从归档删除" };
460
- toast(msg[op] || "完成");
461
- await loadTrack(track);
462
- } catch (e) { toast(e.message); }
701
+ return;
702
+ }
703
+
704
+ if (op === "delete" && !confirm("确认删除这条记忆?此操作不可撤销。")) return;
705
+ try {
706
+ await api("/api/memory-core/entry", {
707
+ method: "POST",
708
+ headers: { "Content-Type": "application/json" },
709
+ body: JSON.stringify({ action: op, track: track, entry: entry, cwd: state.cwd || undefined }),
463
710
  });
464
- });
711
+ var msg = { archive: "已归档", delete: "已删除", confirm: "已允许并写入", reject: "已拒绝", promote: "已移回主轨", removeArchive: "已从归档删除" };
712
+ toast(msg[op] || "完成");
713
+ await refreshRows();
714
+ await switchTab(state.tab);
715
+ } catch (e) { toast(e.message); }
465
716
  }
466
717
 
467
- loadStatus().catch((e) => {
468
- toast("初始化失败:" + e.message);
469
- $("#status-line").innerHTML = '<span class="chip off">无法连接插件后端(/api/memory-core)</span>';
718
+ $("#view-pretty").addEventListener("click", function () {
719
+ state.view = "pretty";
720
+ $("#view-pretty").classList.add("active");
721
+ $("#view-raw").classList.remove("active");
722
+ renderTrack();
723
+ });
724
+ $("#view-raw").addEventListener("click", function () {
725
+ state.view = "raw";
726
+ $("#view-raw").classList.add("active");
727
+ $("#view-pretty").classList.remove("active");
728
+ renderTrack();
729
+ });
730
+ $("#search").addEventListener("input", function (ev) {
731
+ state.query = ev.target.value.trim();
732
+ state.page = 1;
733
+ renderTrack();
734
+ });
735
+ $("#copy-path").addEventListener("click", async function () {
736
+ var p = $("#file-path").textContent;
737
+ try {
738
+ await navigator.clipboard.writeText(p);
739
+ toast("路径已复制");
740
+ } catch (e) {
741
+ toast(p);
742
+ }
743
+ });
744
+ $("#open-file").addEventListener("click", async function () {
745
+ var p = $("#file-path").textContent;
746
+ if (!p) return;
747
+ try {
748
+ await api("/api/memory-core/reveal", {
749
+ method: "POST",
750
+ headers: { "Content-Type": "application/json" },
751
+ body: JSON.stringify({ path: p }),
752
+ });
753
+ } catch (e) { toast(e.message); }
754
+ });
755
+
756
+ boot().catch(function (e) {
757
+ $("#notice").innerHTML = '<div class="warn">无法连接插件后端(/api/memory-core):' + esc(e.message) + "</div>";
470
758
  });
471
759
  })();
472
760
  </script>