sage-mem 0.4.0 → 0.4.2

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/lib/client.js CHANGED
@@ -1,432 +1,432 @@
1
- /**
2
- * sage-mem — 客户端半:DSH 设置页「记忆管理」界面(文件管理器)。
3
- *
4
- * 手写 ModuleLoader bundle(与官方 dsh-client-* 包的构建产物同格式):
5
- * window.__ModuleLoader__.load({ id, factory }),factory 内 require('react')
6
- * 可用,module.exports 导出标准 Cordis 插件(apply/inject)。
7
- *
8
- * 数据通道:TypertRemoteService(host 半 MemoryGateway),通过
9
- * ctx.get("remote.memory") 调用文件列表/读/写/删。remote 返回 {ok, value}
10
- * 信封。浏览器侧无 zod,schema 手写(codec 只要求 mode==="strict" + parse)。
11
- *
12
- * 功能:浏览 memory 目录的 markdown 记忆文件(类型徽章 + 描述预览)、
13
- * 查看/编辑全文、两步确认删除、添加新记忆(选类型 + 描述 + 正文)。
14
- */
15
-
16
- window.__ModuleLoader__.load({
17
- id: "sage-mem",
18
- factory: (require) => {
19
- var module = { exports: {} };
20
- var exports = module.exports;
21
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
22
- let react = require("react");
23
- const h = react.createElement;
24
-
25
- // ── 手写 strict codec(浏览器无 zod,typert 只要求 parse 方法)──
26
- function parseSchema(shape) {
27
- return {
28
- parse(value) {
29
- if (value === null || typeof value !== "object") throw new Error("sage-mem: invalid payload");
30
- for (const [key, check] of Object.entries(shape)) {
31
- if (!(key in value)) throw new Error("sage-mem: missing field " + key);
32
- check(value[key]);
33
- }
34
- return value;
35
- },
36
- };
37
- }
38
- const isStr = (v) => { if (typeof v !== "string") throw new Error("expected string"); };
39
- const isBool = (v) => { if (typeof v !== "boolean") throw new Error("expected boolean"); };
40
- const isNum = (v) => { if (typeof v !== "number") throw new Error("expected number"); };
41
- const strSchema = { parse(v) { isStr(v); return v; } };
42
- const fileSchema = parseSchema({ "file": isStr, "type": isStr, "description": isStr, "size": isNum });
43
- const listSchema = { parse(v) { if (!Array.isArray(v)) throw new Error("expected array"); v.forEach((f) => fileSchema.parse(f)); return v; } };
44
- const readSchema = parseSchema({ "name": isStr, "content": isStr });
45
- const writeSchema = parseSchema({ "ok": isBool, "file": isStr });
46
- const deleteSchema = parseSchema({ "ok": isBool });
47
-
48
- const TYPERT_REMOTE = {
49
- package: "sage-mem",
50
- descriptors: [
51
- {
52
- id: "sage-mem#memory/listFiles",
53
- service: "memory",
54
- namespace: "memory",
55
- method: "listFiles",
56
- invocation: { kind: "direct" },
57
- parameters: [],
58
- result: { mode: "strict", typeSymbol: "sage-mem/types#FileList", schema: listSchema },
59
- sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
60
- },
61
- {
62
- id: "sage-mem#memory/readFile",
63
- service: "memory",
64
- namespace: "memory",
65
- method: "readFile",
66
- invocation: { kind: "direct" },
67
- parameters: [
68
- { name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileName", schema: strSchema } },
69
- ],
70
- result: { mode: "strict", typeSymbol: "sage-mem/types#FileContent", schema: readSchema },
71
- sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
72
- },
73
- {
74
- id: "sage-mem#memory/writeFile",
75
- service: "memory",
76
- namespace: "memory",
77
- method: "writeFile",
78
- invocation: { kind: "direct" },
79
- parameters: [
80
- { name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileName", schema: strSchema } },
81
- { name: "content", wire: "content", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileContent", schema: strSchema } },
82
- ],
83
- result: { mode: "strict", typeSymbol: "sage-mem/types#WriteResult", schema: writeSchema },
84
- sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
85
- },
86
- {
87
- id: "sage-mem#memory/deleteFile",
88
- service: "memory",
89
- namespace: "memory",
90
- method: "deleteFile",
91
- invocation: { kind: "direct" },
92
- parameters: [
93
- { name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileName", schema: strSchema } },
94
- ],
95
- result: { mode: "strict", typeSymbol: "sage-mem/types#DeleteResult", schema: deleteSchema },
96
- sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
97
- },
98
- ],
99
- };
100
-
101
- const CSS = [
102
- ".smem-root{display:flex;flex-direction:column;gap:12px;font-size:13px;line-height:1.55;color:var(--dsw-alias-label-primary);max-width:760px;}",
103
- ".smem-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
104
- ".smem-title{font-size:15px;font-weight:600;}",
105
- ".smem-dot{width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-state-success-primary,#22c55e);flex:none;}",
106
- ".smem-dot--bad{background:var(--dsw-alias-state-error-primary,#ef4444);}",
107
- ".smem-muted{color:var(--dsw-alias-label-secondary);font-size:12px;}",
108
- ".smem-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
109
- ".smem-search{flex:1;min-width:160px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:5px 10px;font-size:12px;outline:none;font-family:inherit;}",
110
- ".smem-search:focus{border-color:var(--dsw-alias-brand-primary,#4d76e6);}",
111
- ".smem-btn{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:8px;padding:4px 12px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;}",
112
- ".smem-btn:hover{border-color:var(--dsw-alias-brand-primary,#4d76e6);color:var(--dsw-alias-brand-primary,#4d76e6);}",
113
- ".smem-btn:disabled{opacity:.5;cursor:default;}",
114
- ".smem-btn-primary{background:var(--dsw-alias-brand-primary,#4d76e6);border-color:var(--dsw-alias-brand-primary,#4d76e6);color:var(--dsw-alias-bg-base,#fff);}",
115
- ".smem-btn-primary:hover{color:var(--dsw-alias-bg-base,#fff);opacity:.88;}",
116
- ".smem-btn-danger-ghost{color:var(--dsw-alias-state-error-primary,#ef4444);border-color:var(--dsw-alias-border-l2);}",
117
- ".smem-btn-danger{background:var(--dsw-alias-state-error-primary,#dc2626);border-color:var(--dsw-alias-state-error-primary,#dc2626);color:#fff;}",
118
- ".smem-btn-danger:hover{color:#fff;opacity:.9;}",
119
- ".smem-list{display:flex;flex-direction:column;gap:10px;}",
120
- ".smem-card{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);padding:10px 12px;display:flex;flex-direction:column;gap:6px;}",
121
- ".smem-card-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
122
- ".smem-card-title{font-weight:600;flex:1;min-width:120px;overflow-wrap:anywhere;text-align:left;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;}",
123
- ".smem-badge{display:inline-flex;align-items:center;gap:4px;font-size:11px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:1px 6px;white-space:nowrap;}",
124
- ".smem-narrative{color:var(--dsw-alias-label-secondary);font-size:12.5px;white-space:pre-wrap;overflow-wrap:anywhere;}",
125
- '.smem-narrative[data-clamp="1"]{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;cursor:pointer;}',
126
- ".smem-card-foot{display:flex;justify-content:flex-end;}",
127
- ".smem-actions{display:flex;gap:6px;}",
128
- ".smem-form{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px;}",
129
- ".smem-form-title{font-weight:600;font-size:13px;}",
130
- ".smem-label{font-size:12px;color:var(--dsw-alias-label-secondary);}",
131
- ".smem-input,.smem-textarea{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-base);color:inherit;padding:6px 10px;font-size:13px;outline:none;font-family:inherit;}",
132
- ".smem-input:focus,.smem-textarea:focus{border-color:var(--dsw-alias-brand-primary,#4d76e6);}",
133
- ".smem-textarea{resize:vertical;min-height:72px;}",
134
- ".smem-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}",
135
- ".smem-chip{border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);border-radius:999px;padding:3px 12px;font-size:12px;cursor:pointer;font-family:inherit;}",
136
- ".smem-chip:hover{border-color:var(--dsw-alias-border-l2);}",
137
- '.smem-chip[data-on="1"]{border-color:var(--dsw-alias-brand-primary,#4d76e6);color:var(--dsw-alias-brand-primary,#4d76e6);}',
138
- ".smem-chips{display:flex;gap:6px;flex-wrap:wrap;}",
139
- ".smem-err{border:1px solid var(--dsw-alias-state-error-primary,#ef4444);color:var(--dsw-alias-state-error-primary,#ef4444);border-radius:8px;padding:6px 10px;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;}",
140
- ".smem-ok{color:var(--dsw-alias-state-success-primary,#16a34a);font-size:12px;}",
141
- ".smem-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;padding:22px 14px;text-align:center;color:var(--dsw-alias-label-secondary);font-size:12.5px;}",
142
- ].join("\n");
143
-
144
- const TYPE_META = {
145
- user: ["👤", "用户"],
146
- feedback: ["📌", "反馈"],
147
- project: ["📁", "项目"],
148
- reference: ["🔗", "参考"],
149
- };
150
- const TYPE_ORDER = ["user", "feedback", "project", "reference"];
151
-
152
- function fmtSize(n) {
153
- if (n < 1024) return n + " B";
154
- if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
155
- return (n / 1024 / 1024).toFixed(1) + " MB";
156
- }
157
-
158
- /** 拆 typert remote 的 {ok, value} 信封。 */
159
- function unwrap(result) {
160
- return result && result.ok ? result.value : result;
161
- }
162
-
163
- function Chip(props) {
164
- return h("button", {
165
- type: "button",
166
- className: "smem-chip",
167
- "data-on": props.on ? "1" : "0",
168
- onClick: props.onClick,
169
- }, props.label);
170
- }
171
-
172
- function Card(props) {
173
- const item = props.item;
174
- const meta = TYPE_META[item.type] || TYPE_META.reference;
175
- const isOpen = !!props.expanded;
176
- return h("div", { className: "smem-card", key: item.file }, [
177
- h("div", { className: "smem-card-head", key: "head" }, [
178
- h("span", { className: "smem-badge", key: "badge", title: "类型 " + String(item.type) }, meta[0] + " " + meta[1]),
179
- h("span", { className: "smem-card-title", key: "title" }, item.file),
180
- h("span", { className: "smem-muted", key: "size" }, fmtSize(item.size)),
181
- ]),
182
- item.description
183
- ? h("div", {
184
- className: "smem-narrative",
185
- "data-clamp": isOpen ? "0" : "1",
186
- key: "body",
187
- onClick: function () { props.onToggle(item.file); },
188
- title: isOpen ? "点击收起" : "点击展开全文",
189
- }, String(item.description))
190
- : null,
191
- h("div", { className: "smem-card-foot", key: "foot" },
192
- h("div", { className: "smem-actions", key: "acts" },
193
- props.deleting
194
- ? [
195
- h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", onClick: function () { props.onConfirmDelete(item.file); } }, "确认删除"),
196
- h("button", { type: "button", className: "smem-btn", key: "no", onClick: function () { props.onCancelDelete(); } }, "取消"),
197
- ]
198
- : [
199
- h("button", { type: "button", className: "smem-btn", key: "edit", onClick: function () { props.onEdit(item.file); } }, "编辑"),
200
- h("button", { type: "button", className: "smem-btn smem-btn-danger-ghost", key: "del", onClick: function () { props.onAskDelete(item.file); } }, "删除"),
201
- ])),
202
- ]);
203
- }
204
-
205
- function Section(props) {
206
- const remote = props.ctx.get("remote.memory");
207
- const sItems = react.useState([]);
208
- const items = sItems[0], setItems = sItems[1];
209
- const sLoading = react.useState(false);
210
- const loading = sLoading[0], setLoading = sLoading[1];
211
- const sError = react.useState(null);
212
- const error = sError[0], setError = sError[1];
213
- const sNotice = react.useState("");
214
- const notice = sNotice[0], setNotice = sNotice[1];
215
- const sQuery = react.useState("");
216
- const query = sQuery[0], setQuery = sQuery[1];
217
- const sExpanded = react.useState({});
218
- const expanded = sExpanded[0], setExpanded = sExpanded[1];
219
- const sDeleting = react.useState(null);
220
- const deleting = sDeleting[0], setDeleting = sDeleting[1];
221
- const sFormOpen = react.useState(false);
222
- const formOpen = sFormOpen[0], setFormOpen = sFormOpen[1];
223
- const sEditingFile = react.useState(null);
224
- const editingFile = sEditingFile[0], setEditingFile = sEditingFile[1];
225
- const sFormType = react.useState("feedback");
226
- const formType = sFormType[0], setFormType = sFormType[1];
227
- const sFormName = react.useState("");
228
- const formName = sFormName[0], setFormName = sFormName[1];
229
- const sFormDesc = react.useState("");
230
- const formDesc = sFormDesc[0], setFormDesc = sFormDesc[1];
231
- const sFormBody = react.useState("");
232
- const formBody = sFormBody[0], setFormBody = sFormBody[1];
233
- const sBusy = react.useState(false);
234
- const busy = sBusy[0], setBusy = sBusy[1];
235
- const sOk = react.useState(false);
236
- const dirOk = sOk[0], setDirOk = sOk[1];
237
-
238
- function load() {
239
- setLoading(true);
240
- setError(null);
241
- remote.listFiles()
242
- .then(function (res) {
243
- if (res && typeof res === "object" && res.ok === false) {
244
- const msg = res.error && res.error.message ? res.error.message : "remote 调用失败";
245
- throw new Error("listFiles RPC 失败: " + msg);
246
- }
247
- const list = unwrap(res);
248
- setItems(Array.isArray(list) ? list : []);
249
- setDirOk(true);
250
- })
251
- .catch(function (e) { setError(String((e && e.message) || e)); setDirOk(false); })
252
- .then(function () { setLoading(false); });
253
- }
254
-
255
- react.useEffect(function () { load(); }, []);
256
-
257
- function closeForm() {
258
- setFormOpen(false); setEditingFile(null); setFormName(""); setFormDesc(""); setFormBody(""); setFormType("feedback");
259
- }
260
-
261
- function openEdit(file) {
262
- setError(null);
263
- setBusy(true);
264
- remote.readFile(file)
265
- .then(function (res) {
266
- const data = unwrap(res);
267
- const content = (data && data.content) || "";
268
- const fm = content.match(/^---\n([\s\S]*?)\n---\n?/);
269
- let type = "reference";
270
- let desc = "";
271
- let body = content;
272
- if (fm) {
273
- body = content.slice(fm[0].length);
274
- const t = fm[1].match(/type:\s*(\S+)/);
275
- if (t) type = t[1];
276
- const d = fm[1].match(/description:\s*"?(.+?)"?\n/);
277
- if (d) desc = d[1].replace(/["']/g, "");
278
- }
279
- setEditingFile(file);
280
- setFormName(file);
281
- setFormType(TYPE_ORDER.indexOf(type) >= 0 ? type : "reference");
282
- setFormDesc(desc);
283
- setFormBody(body);
284
- setFormOpen(true);
285
- })
286
- .catch(function (e) { setError(String((e && e.message) || e)); })
287
- .then(function () { setBusy(false); });
288
- }
289
-
290
- function openAdd() {
291
- setError(null);
292
- setEditingFile(null);
293
- setFormName("");
294
- setFormType("feedback");
295
- setFormDesc("");
296
- setFormBody("");
297
- setFormOpen(true);
298
- }
299
-
300
- function buildFrontmatter(type, desc) {
301
- return "---\nname: \"\"\ndescription: \"" + desc.replace(/"/g, "'") + "\"\nmetadata:\n type: " + type + "\n---\n\n";
302
- }
303
-
304
- function submit() {
305
- if (!formDesc.trim()) { setError("描述不能为空"); return; }
306
- setBusy(true); setError(null); setNotice("");
307
- const type = formType;
308
- const name = (formName.trim() || (type + "_note.md"));
309
- const safeName = name.endsWith(".md") ? name : name + ".md";
310
- const content = buildFrontmatter(type, formDesc.trim()) + formBody;
311
- remote.writeFile(safeName, content)
312
- .then(function () {
313
- closeForm();
314
- setNotice(editingFile ? "已更新 " + safeName : "已保存 " + safeName);
315
- load();
316
- })
317
- .catch(function (e) { setError(String((e && e.message) || e)); })
318
- .then(function () { setBusy(false); });
319
- }
320
-
321
- function confirmDelete(file) {
322
- setBusy(true); setError(null); setNotice("");
323
- remote.deleteFile(file)
324
- .then(function () {
325
- setDeleting(null);
326
- setNotice("已删除 " + file);
327
- load();
328
- })
329
- .catch(function (e) { setError(String((e && e.message) || e)); })
330
- .then(function () { setBusy(false); });
331
- }
332
-
333
- const q = query.trim().toLowerCase();
334
- const filtered = items.filter(function (it) {
335
- if (!q) return true;
336
- const hay = ((it.file || "") + "\n" + (it.description || "") + "\n" + (it.type || "")).toLowerCase();
337
- return hay.indexOf(q) >= 0;
338
- });
339
-
340
- return h("div", { className: "smem-root" }, [
341
- h("div", { className: "smem-head", key: "head" }, [
342
- h("span", { className: "smem-title", key: "t" }, "记忆文件(sage-mem)"),
343
- h("span", { className: "smem-dot" + (dirOk ? "" : " smem-dot--bad"), key: "dot", title: dirOk ? "memory 目录可读" : "memory 目录不可读" }),
344
- h("span", { className: "smem-muted", key: "st" }, dirOk ? "目录可读" : "目录不可读"),
345
- h("span", { className: "smem-badge", key: "cnt" }, "共 " + String(items.length) + " 个文件"),
346
- ]),
347
- h("div", { className: "smem-toolbar", key: "bar" }, [
348
- h("input", { className: "smem-search", key: "q", value: query, placeholder: "搜索文件名 / 描述 / 类型…", onChange: function (e) { setQuery(e.target.value); } }),
349
- h("button", { type: "button", className: "smem-btn", key: "refresh", disabled: busy || loading, onClick: load }, "刷新"),
350
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, "+ 添加记忆"),
351
- ]),
352
- error ? h("div", { className: "smem-err", key: "err" }, "⚠ " + error) : null,
353
- notice && !error ? h("div", { className: "smem-ok", key: "ok" }, "✓ " + notice) : null,
354
- formOpen
355
- ? h("div", { className: "smem-form", key: "form" }, [
356
- h("div", { className: "smem-form-title", key: "ft" }, editingFile ? "编辑 " + editingFile : "添加记忆"),
357
- h("div", { className: "smem-row", key: "cat" }, [
358
- h("span", { className: "smem-label", key: "l" }, "类型:"),
359
- h("div", { className: "smem-chips", key: "chips" }, TYPE_ORDER.map(function (t) {
360
- const meta = TYPE_META[t];
361
- return h(Chip, { key: t, label: meta[0] + " " + meta[1], on: formType === t, onClick: function () { setFormType(t); } });
362
- })),
363
- ]),
364
- h("label", { className: "smem-label", key: "ln" }, "文件名(.md,留空自动 " + formType + "_note.md)"),
365
- h("input", { className: "smem-input", key: "tn", value: formName, onChange: function (e) { setFormName(e.target.value); }, placeholder: "例如 project_traveler.md" }),
366
- h("label", { className: "smem-label", key: "ld" }, "描述(写进 frontmatter,检索与列表都靠它)"),
367
- h("input", { className: "smem-input", key: "td", value: formDesc, onChange: function (e) { setFormDesc(e.target.value); }, placeholder: "一句话说清这条记忆是什么" }),
368
- h("label", { className: "smem-label", key: "lb" }, "正文(可选,frontmatter 自动生成)"),
369
- h("textarea", { className: "smem-textarea", key: "tb", rows: 6, value: formBody, onChange: function (e) { setFormBody(e.target.value); }, placeholder: "要长期记住的细节" }),
370
- h("div", { className: "smem-row", key: "btns" }, [
371
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: busy, onClick: submit }, busy ? "保存中…" : "保存"),
372
- h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: busy, onClick: closeForm }, "取消"),
373
- ]),
374
- ])
375
- : null,
376
- loading && items.length === 0
377
- ? h("div", { className: "smem-muted", key: "loading" }, "加载中…")
378
- : filtered.length === 0
379
- ? h("div", { className: "smem-empty", key: "empty" },
380
- items.length === 0
381
- ? "还没有记忆文件。对话中值得记住的事会写到这里,也可以点「添加记忆」手动写入。"
382
- : "没有符合搜索条件的文件。")
383
- : h("div", { className: "smem-list", key: "list" }, filtered.map(function (it) {
384
- return h(Card, {
385
- key: it.file,
386
- item: it,
387
- expanded: !!expanded[it.file],
388
- deleting: deleting === it.file,
389
- onToggle: function (file) {
390
- const nx = {};
391
- nx[file] = !expanded[file];
392
- setExpanded(Object.assign({}, expanded, nx));
393
- },
394
- onEdit: openEdit,
395
- onAskDelete: function (file) { setDeleting(file); },
396
- onCancelDelete: function () { setDeleting(null); },
397
- onConfirmDelete: confirmDelete,
398
- });
399
- })),
400
- ]);
401
- }
402
-
403
- const inject = ["slots", "remote"];
404
-
405
- /**
406
- * Client 插件:$mount strict remote contribution(第三方 remote 不在
407
- * dsh-api-remotes 的固定列表里,必须自己挂),再注册设置页签。
408
- */
409
- async function apply(ctx) {
410
- await ctx.remote.$mount(TYPERT_REMOTE);
411
- // 样式注入(参照官方 client bundle 的 data-plugin-css 约定,幂等)
412
- const tagId = "sage-mem/settings.css";
413
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
414
- const tag = document.createElement("style");
415
- tag.dataset.plugin = "sage-mem";
416
- tag.dataset.pluginCss = tagId;
417
- tag.textContent = CSS;
418
- document.head.appendChild(tag);
419
- }
420
- ctx.slots.inject("settings.section", () =>
421
- ctx.slots.register(
422
- { name: "settings.section", id: "sage-mem", order: 20, label: "记忆管理" },
423
- (props) => react.createElement(Section, { ...props, ctx }),
424
- ),
425
- );
426
- }
427
-
428
- exports.apply = apply;
429
- exports.inject = inject;
430
- return module.exports;
431
- }
432
- });
1
+ /**
2
+ * sage-mem — 客户端半:DSH 设置页「记忆管理」界面(文件管理器)。
3
+ *
4
+ * 手写 ModuleLoader bundle(与官方 dsh-client-* 包的构建产物同格式):
5
+ * window.__ModuleLoader__.load({ id, factory }),factory 内 require('react')
6
+ * 可用,module.exports 导出标准 Cordis 插件(apply/inject)。
7
+ *
8
+ * 数据通道:TypertRemoteService(host 半 MemoryGateway),通过
9
+ * ctx.get("remote.memory") 调用文件列表/读/写/删。remote 返回 {ok, value}
10
+ * 信封。浏览器侧无 zod,schema 手写(codec 只要求 mode==="strict" + parse)。
11
+ *
12
+ * 功能:浏览 memory 目录的 markdown 记忆文件(类型徽章 + 描述预览)、
13
+ * 查看/编辑全文、两步确认删除、添加新记忆(选类型 + 描述 + 正文)。
14
+ */
15
+
16
+ window.__ModuleLoader__.load({
17
+ id: "sage-mem",
18
+ factory: (require) => {
19
+ var module = { exports: {} };
20
+ var exports = module.exports;
21
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
22
+ let react = require("react");
23
+ const h = react.createElement;
24
+
25
+ // ── 手写 strict codec(浏览器无 zod,typert 只要求 parse 方法)──
26
+ function parseSchema(shape) {
27
+ return {
28
+ parse(value) {
29
+ if (value === null || typeof value !== "object") throw new Error("sage-mem: invalid payload");
30
+ for (const [key, check] of Object.entries(shape)) {
31
+ if (!(key in value)) throw new Error("sage-mem: missing field " + key);
32
+ check(value[key]);
33
+ }
34
+ return value;
35
+ },
36
+ };
37
+ }
38
+ const isStr = (v) => { if (typeof v !== "string") throw new Error("expected string"); };
39
+ const isBool = (v) => { if (typeof v !== "boolean") throw new Error("expected boolean"); };
40
+ const isNum = (v) => { if (typeof v !== "number") throw new Error("expected number"); };
41
+ const strSchema = { parse(v) { isStr(v); return v; } };
42
+ const fileSchema = parseSchema({ "file": isStr, "type": isStr, "description": isStr, "size": isNum });
43
+ const listSchema = { parse(v) { if (!Array.isArray(v)) throw new Error("expected array"); v.forEach((f) => fileSchema.parse(f)); return v; } };
44
+ const readSchema = parseSchema({ "name": isStr, "content": isStr });
45
+ const writeSchema = parseSchema({ "ok": isBool, "file": isStr });
46
+ const deleteSchema = parseSchema({ "ok": isBool });
47
+
48
+ const TYPERT_REMOTE = {
49
+ package: "sage-mem",
50
+ descriptors: [
51
+ {
52
+ id: "sage-mem#memory/listFiles",
53
+ service: "memory",
54
+ namespace: "memory",
55
+ method: "listFiles",
56
+ invocation: { kind: "direct" },
57
+ parameters: [],
58
+ result: { mode: "strict", typeSymbol: "sage-mem/types#FileList", schema: listSchema },
59
+ sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
60
+ },
61
+ {
62
+ id: "sage-mem#memory/readFile",
63
+ service: "memory",
64
+ namespace: "memory",
65
+ method: "readFile",
66
+ invocation: { kind: "direct" },
67
+ parameters: [
68
+ { name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileName", schema: strSchema } },
69
+ ],
70
+ result: { mode: "strict", typeSymbol: "sage-mem/types#FileContent", schema: readSchema },
71
+ sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
72
+ },
73
+ {
74
+ id: "sage-mem#memory/writeFile",
75
+ service: "memory",
76
+ namespace: "memory",
77
+ method: "writeFile",
78
+ invocation: { kind: "direct" },
79
+ parameters: [
80
+ { name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileName", schema: strSchema } },
81
+ { name: "content", wire: "content", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileContent", schema: strSchema } },
82
+ ],
83
+ result: { mode: "strict", typeSymbol: "sage-mem/types#WriteResult", schema: writeSchema },
84
+ sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
85
+ },
86
+ {
87
+ id: "sage-mem#memory/deleteFile",
88
+ service: "memory",
89
+ namespace: "memory",
90
+ method: "deleteFile",
91
+ invocation: { kind: "direct" },
92
+ parameters: [
93
+ { name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileName", schema: strSchema } },
94
+ ],
95
+ result: { mode: "strict", typeSymbol: "sage-mem/types#DeleteResult", schema: deleteSchema },
96
+ sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
97
+ },
98
+ ],
99
+ };
100
+
101
+ const CSS = [
102
+ ".smem-root{display:flex;flex-direction:column;gap:12px;font-size:13px;line-height:1.55;color:var(--dsw-alias-label-primary);max-width:760px;}",
103
+ ".smem-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
104
+ ".smem-title{font-size:15px;font-weight:600;}",
105
+ ".smem-dot{width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-state-success-primary,#22c55e);flex:none;}",
106
+ ".smem-dot--bad{background:var(--dsw-alias-state-error-primary,#ef4444);}",
107
+ ".smem-muted{color:var(--dsw-alias-label-secondary);font-size:12px;}",
108
+ ".smem-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
109
+ ".smem-search{flex:1;min-width:160px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:5px 10px;font-size:12px;outline:none;font-family:inherit;}",
110
+ ".smem-search:focus{border-color:var(--dsw-alias-brand-primary,#4d76e6);}",
111
+ ".smem-btn{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:8px;padding:4px 12px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;}",
112
+ ".smem-btn:hover{border-color:var(--dsw-alias-brand-primary,#4d76e6);color:var(--dsw-alias-brand-primary,#4d76e6);}",
113
+ ".smem-btn:disabled{opacity:.5;cursor:default;}",
114
+ ".smem-btn-primary{background:var(--dsw-alias-brand-primary,#4d76e6);border-color:var(--dsw-alias-brand-primary,#4d76e6);color:var(--dsw-alias-bg-base,#fff);}",
115
+ ".smem-btn-primary:hover{color:var(--dsw-alias-bg-base,#fff);opacity:.88;}",
116
+ ".smem-btn-danger-ghost{color:var(--dsw-alias-state-error-primary,#ef4444);border-color:var(--dsw-alias-border-l2);}",
117
+ ".smem-btn-danger{background:var(--dsw-alias-state-error-primary,#dc2626);border-color:var(--dsw-alias-state-error-primary,#dc2626);color:#fff;}",
118
+ ".smem-btn-danger:hover{color:#fff;opacity:.9;}",
119
+ ".smem-list{display:flex;flex-direction:column;gap:10px;}",
120
+ ".smem-card{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);padding:10px 12px;display:flex;flex-direction:column;gap:6px;}",
121
+ ".smem-card-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
122
+ ".smem-card-title{font-weight:600;flex:1;min-width:120px;overflow-wrap:anywhere;text-align:left;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;}",
123
+ ".smem-badge{display:inline-flex;align-items:center;gap:4px;font-size:11px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:1px 6px;white-space:nowrap;}",
124
+ ".smem-narrative{color:var(--dsw-alias-label-secondary);font-size:12.5px;white-space:pre-wrap;overflow-wrap:anywhere;}",
125
+ '.smem-narrative[data-clamp="1"]{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;cursor:pointer;}',
126
+ ".smem-card-foot{display:flex;justify-content:flex-end;}",
127
+ ".smem-actions{display:flex;gap:6px;}",
128
+ ".smem-form{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px;}",
129
+ ".smem-form-title{font-weight:600;font-size:13px;}",
130
+ ".smem-label{font-size:12px;color:var(--dsw-alias-label-secondary);}",
131
+ ".smem-input,.smem-textarea{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-base);color:inherit;padding:6px 10px;font-size:13px;outline:none;font-family:inherit;}",
132
+ ".smem-input:focus,.smem-textarea:focus{border-color:var(--dsw-alias-brand-primary,#4d76e6);}",
133
+ ".smem-textarea{resize:vertical;min-height:72px;}",
134
+ ".smem-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}",
135
+ ".smem-chip{border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);border-radius:999px;padding:3px 12px;font-size:12px;cursor:pointer;font-family:inherit;}",
136
+ ".smem-chip:hover{border-color:var(--dsw-alias-border-l2);}",
137
+ '.smem-chip[data-on="1"]{border-color:var(--dsw-alias-brand-primary,#4d76e6);color:var(--dsw-alias-brand-primary,#4d76e6);}',
138
+ ".smem-chips{display:flex;gap:6px;flex-wrap:wrap;}",
139
+ ".smem-err{border:1px solid var(--dsw-alias-state-error-primary,#ef4444);color:var(--dsw-alias-state-error-primary,#ef4444);border-radius:8px;padding:6px 10px;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;}",
140
+ ".smem-ok{color:var(--dsw-alias-state-success-primary,#16a34a);font-size:12px;}",
141
+ ".smem-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;padding:22px 14px;text-align:center;color:var(--dsw-alias-label-secondary);font-size:12.5px;}",
142
+ ].join("\n");
143
+
144
+ const TYPE_META = {
145
+ user: ["👤", "用户"],
146
+ feedback: ["📌", "反馈"],
147
+ project: ["📁", "项目"],
148
+ reference: ["🔗", "参考"],
149
+ };
150
+ const TYPE_ORDER = ["user", "feedback", "project", "reference"];
151
+
152
+ function fmtSize(n) {
153
+ if (n < 1024) return n + " B";
154
+ if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
155
+ return (n / 1024 / 1024).toFixed(1) + " MB";
156
+ }
157
+
158
+ /** 拆 typert remote 的 {ok, value} 信封。 */
159
+ function unwrap(result) {
160
+ return result && result.ok ? result.value : result;
161
+ }
162
+
163
+ function Chip(props) {
164
+ return h("button", {
165
+ type: "button",
166
+ className: "smem-chip",
167
+ "data-on": props.on ? "1" : "0",
168
+ onClick: props.onClick,
169
+ }, props.label);
170
+ }
171
+
172
+ function Card(props) {
173
+ const item = props.item;
174
+ const meta = TYPE_META[item.type] || TYPE_META.reference;
175
+ const isOpen = !!props.expanded;
176
+ return h("div", { className: "smem-card", key: item.file }, [
177
+ h("div", { className: "smem-card-head", key: "head" }, [
178
+ h("span", { className: "smem-badge", key: "badge", title: "类型 " + String(item.type) }, meta[0] + " " + meta[1]),
179
+ h("span", { className: "smem-card-title", key: "title" }, item.file),
180
+ h("span", { className: "smem-muted", key: "size" }, fmtSize(item.size)),
181
+ ]),
182
+ item.description
183
+ ? h("div", {
184
+ className: "smem-narrative",
185
+ "data-clamp": isOpen ? "0" : "1",
186
+ key: "body",
187
+ onClick: function () { props.onToggle(item.file); },
188
+ title: isOpen ? "点击收起" : "点击展开全文",
189
+ }, String(item.description))
190
+ : null,
191
+ h("div", { className: "smem-card-foot", key: "foot" },
192
+ h("div", { className: "smem-actions", key: "acts" },
193
+ props.deleting
194
+ ? [
195
+ h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", onClick: function () { props.onConfirmDelete(item.file); } }, "确认删除"),
196
+ h("button", { type: "button", className: "smem-btn", key: "no", onClick: function () { props.onCancelDelete(); } }, "取消"),
197
+ ]
198
+ : [
199
+ h("button", { type: "button", className: "smem-btn", key: "edit", onClick: function () { props.onEdit(item.file); } }, "编辑"),
200
+ h("button", { type: "button", className: "smem-btn smem-btn-danger-ghost", key: "del", onClick: function () { props.onAskDelete(item.file); } }, "删除"),
201
+ ])),
202
+ ]);
203
+ }
204
+
205
+ function Section(props) {
206
+ const remote = props.ctx.get("remote.memory");
207
+ const sItems = react.useState([]);
208
+ const items = sItems[0], setItems = sItems[1];
209
+ const sLoading = react.useState(false);
210
+ const loading = sLoading[0], setLoading = sLoading[1];
211
+ const sError = react.useState(null);
212
+ const error = sError[0], setError = sError[1];
213
+ const sNotice = react.useState("");
214
+ const notice = sNotice[0], setNotice = sNotice[1];
215
+ const sQuery = react.useState("");
216
+ const query = sQuery[0], setQuery = sQuery[1];
217
+ const sExpanded = react.useState({});
218
+ const expanded = sExpanded[0], setExpanded = sExpanded[1];
219
+ const sDeleting = react.useState(null);
220
+ const deleting = sDeleting[0], setDeleting = sDeleting[1];
221
+ const sFormOpen = react.useState(false);
222
+ const formOpen = sFormOpen[0], setFormOpen = sFormOpen[1];
223
+ const sEditingFile = react.useState(null);
224
+ const editingFile = sEditingFile[0], setEditingFile = sEditingFile[1];
225
+ const sFormType = react.useState("feedback");
226
+ const formType = sFormType[0], setFormType = sFormType[1];
227
+ const sFormName = react.useState("");
228
+ const formName = sFormName[0], setFormName = sFormName[1];
229
+ const sFormDesc = react.useState("");
230
+ const formDesc = sFormDesc[0], setFormDesc = sFormDesc[1];
231
+ const sFormBody = react.useState("");
232
+ const formBody = sFormBody[0], setFormBody = sFormBody[1];
233
+ const sBusy = react.useState(false);
234
+ const busy = sBusy[0], setBusy = sBusy[1];
235
+ const sOk = react.useState(false);
236
+ const dirOk = sOk[0], setDirOk = sOk[1];
237
+
238
+ function load() {
239
+ setLoading(true);
240
+ setError(null);
241
+ remote.listFiles()
242
+ .then(function (res) {
243
+ if (res && typeof res === "object" && res.ok === false) {
244
+ const msg = res.error && res.error.message ? res.error.message : "remote 调用失败";
245
+ throw new Error("listFiles RPC 失败: " + msg);
246
+ }
247
+ const list = unwrap(res);
248
+ setItems(Array.isArray(list) ? list : []);
249
+ setDirOk(true);
250
+ })
251
+ .catch(function (e) { setError(String((e && e.message) || e)); setDirOk(false); })
252
+ .then(function () { setLoading(false); });
253
+ }
254
+
255
+ react.useEffect(function () { load(); }, []);
256
+
257
+ function closeForm() {
258
+ setFormOpen(false); setEditingFile(null); setFormName(""); setFormDesc(""); setFormBody(""); setFormType("feedback");
259
+ }
260
+
261
+ function openEdit(file) {
262
+ setError(null);
263
+ setBusy(true);
264
+ remote.readFile(file)
265
+ .then(function (res) {
266
+ const data = unwrap(res);
267
+ const content = (data && data.content) || "";
268
+ const fm = content.match(/^---\n([\s\S]*?)\n---\n?/);
269
+ let type = "reference";
270
+ let desc = "";
271
+ let body = content;
272
+ if (fm) {
273
+ body = content.slice(fm[0].length);
274
+ const t = fm[1].match(/type:\s*(\S+)/);
275
+ if (t) type = t[1];
276
+ const d = fm[1].match(/description:\s*"?(.+?)"?\n/);
277
+ if (d) desc = d[1].replace(/["']/g, "");
278
+ }
279
+ setEditingFile(file);
280
+ setFormName(file);
281
+ setFormType(TYPE_ORDER.indexOf(type) >= 0 ? type : "reference");
282
+ setFormDesc(desc);
283
+ setFormBody(body);
284
+ setFormOpen(true);
285
+ })
286
+ .catch(function (e) { setError(String((e && e.message) || e)); })
287
+ .then(function () { setBusy(false); });
288
+ }
289
+
290
+ function openAdd() {
291
+ setError(null);
292
+ setEditingFile(null);
293
+ setFormName("");
294
+ setFormType("feedback");
295
+ setFormDesc("");
296
+ setFormBody("");
297
+ setFormOpen(true);
298
+ }
299
+
300
+ function buildFrontmatter(type, desc) {
301
+ return "---\nname: \"\"\ndescription: \"" + desc.replace(/"/g, "'") + "\"\nmetadata:\n type: " + type + "\n---\n\n";
302
+ }
303
+
304
+ function submit() {
305
+ if (!formDesc.trim()) { setError("描述不能为空"); return; }
306
+ setBusy(true); setError(null); setNotice("");
307
+ const type = formType;
308
+ const name = (formName.trim() || (type + "_note.md"));
309
+ const safeName = name.endsWith(".md") ? name : name + ".md";
310
+ const content = buildFrontmatter(type, formDesc.trim()) + formBody;
311
+ remote.writeFile(safeName, content)
312
+ .then(function () {
313
+ closeForm();
314
+ setNotice(editingFile ? "已更新 " + safeName : "已保存 " + safeName);
315
+ load();
316
+ })
317
+ .catch(function (e) { setError(String((e && e.message) || e)); })
318
+ .then(function () { setBusy(false); });
319
+ }
320
+
321
+ function confirmDelete(file) {
322
+ setBusy(true); setError(null); setNotice("");
323
+ remote.deleteFile(file)
324
+ .then(function () {
325
+ setDeleting(null);
326
+ setNotice("已删除 " + file);
327
+ load();
328
+ })
329
+ .catch(function (e) { setError(String((e && e.message) || e)); })
330
+ .then(function () { setBusy(false); });
331
+ }
332
+
333
+ const q = query.trim().toLowerCase();
334
+ const filtered = items.filter(function (it) {
335
+ if (!q) return true;
336
+ const hay = ((it.file || "") + "\n" + (it.description || "") + "\n" + (it.type || "")).toLowerCase();
337
+ return hay.indexOf(q) >= 0;
338
+ });
339
+
340
+ return h("div", { className: "smem-root" }, [
341
+ h("div", { className: "smem-head", key: "head" }, [
342
+ h("span", { className: "smem-title", key: "t" }, "记忆文件(sage-mem)"),
343
+ h("span", { className: "smem-dot" + (dirOk ? "" : " smem-dot--bad"), key: "dot", title: dirOk ? "memory 目录可读" : "memory 目录不可读" }),
344
+ h("span", { className: "smem-muted", key: "st" }, dirOk ? "目录可读" : "目录不可读"),
345
+ h("span", { className: "smem-badge", key: "cnt" }, "共 " + String(items.length) + " 个文件"),
346
+ ]),
347
+ h("div", { className: "smem-toolbar", key: "bar" }, [
348
+ h("input", { className: "smem-search", key: "q", value: query, placeholder: "搜索文件名 / 描述 / 类型…", onChange: function (e) { setQuery(e.target.value); } }),
349
+ h("button", { type: "button", className: "smem-btn", key: "refresh", disabled: busy || loading, onClick: load }, "刷新"),
350
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, "+ 添加记忆"),
351
+ ]),
352
+ error ? h("div", { className: "smem-err", key: "err" }, "⚠ " + error) : null,
353
+ notice && !error ? h("div", { className: "smem-ok", key: "ok" }, "✓ " + notice) : null,
354
+ formOpen
355
+ ? h("div", { className: "smem-form", key: "form" }, [
356
+ h("div", { className: "smem-form-title", key: "ft" }, editingFile ? "编辑 " + editingFile : "添加记忆"),
357
+ h("div", { className: "smem-row", key: "cat" }, [
358
+ h("span", { className: "smem-label", key: "l" }, "类型:"),
359
+ h("div", { className: "smem-chips", key: "chips" }, TYPE_ORDER.map(function (t) {
360
+ const meta = TYPE_META[t];
361
+ return h(Chip, { key: t, label: meta[0] + " " + meta[1], on: formType === t, onClick: function () { setFormType(t); } });
362
+ })),
363
+ ]),
364
+ h("label", { className: "smem-label", key: "ln" }, "文件名(.md,留空自动 " + formType + "_note.md)"),
365
+ h("input", { className: "smem-input", key: "tn", value: formName, onChange: function (e) { setFormName(e.target.value); }, placeholder: "例如 project_traveler.md" }),
366
+ h("label", { className: "smem-label", key: "ld" }, "描述(写进 frontmatter,检索与列表都靠它)"),
367
+ h("input", { className: "smem-input", key: "td", value: formDesc, onChange: function (e) { setFormDesc(e.target.value); }, placeholder: "一句话说清这条记忆是什么" }),
368
+ h("label", { className: "smem-label", key: "lb" }, "正文(可选,frontmatter 自动生成)"),
369
+ h("textarea", { className: "smem-textarea", key: "tb", rows: 6, value: formBody, onChange: function (e) { setFormBody(e.target.value); }, placeholder: "要长期记住的细节" }),
370
+ h("div", { className: "smem-row", key: "btns" }, [
371
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: busy, onClick: submit }, busy ? "保存中…" : "保存"),
372
+ h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: busy, onClick: closeForm }, "取消"),
373
+ ]),
374
+ ])
375
+ : null,
376
+ loading && items.length === 0
377
+ ? h("div", { className: "smem-muted", key: "loading" }, "加载中…")
378
+ : filtered.length === 0
379
+ ? h("div", { className: "smem-empty", key: "empty" },
380
+ items.length === 0
381
+ ? "还没有记忆文件。对话中值得记住的事会写到这里,也可以点「添加记忆」手动写入。"
382
+ : "没有符合搜索条件的文件。")
383
+ : h("div", { className: "smem-list", key: "list" }, filtered.map(function (it) {
384
+ return h(Card, {
385
+ key: it.file,
386
+ item: it,
387
+ expanded: !!expanded[it.file],
388
+ deleting: deleting === it.file,
389
+ onToggle: function (file) {
390
+ const nx = {};
391
+ nx[file] = !expanded[file];
392
+ setExpanded(Object.assign({}, expanded, nx));
393
+ },
394
+ onEdit: openEdit,
395
+ onAskDelete: function (file) { setDeleting(file); },
396
+ onCancelDelete: function () { setDeleting(null); },
397
+ onConfirmDelete: confirmDelete,
398
+ });
399
+ })),
400
+ ]);
401
+ }
402
+
403
+ const inject = ["slots", "remote"];
404
+
405
+ /**
406
+ * Client 插件:$mount strict remote contribution(第三方 remote 不在
407
+ * dsh-api-remotes 的固定列表里,必须自己挂),再注册设置页签。
408
+ */
409
+ async function apply(ctx) {
410
+ await ctx.remote.$mount(TYPERT_REMOTE);
411
+ // 样式注入(参照官方 client bundle 的 data-plugin-css 约定,幂等)
412
+ const tagId = "sage-mem/settings.css";
413
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
414
+ const tag = document.createElement("style");
415
+ tag.dataset.plugin = "sage-mem";
416
+ tag.dataset.pluginCss = tagId;
417
+ tag.textContent = CSS;
418
+ document.head.appendChild(tag);
419
+ }
420
+ ctx.slots.inject("settings.section", () =>
421
+ ctx.slots.register(
422
+ { name: "settings.section", id: "sage-mem", order: 20, label: "记忆管理" },
423
+ (props) => react.createElement(Section, { ...props, ctx }),
424
+ ),
425
+ );
426
+ }
427
+
428
+ exports.apply = apply;
429
+ exports.inject = inject;
430
+ return module.exports;
431
+ }
432
+ });