dsh-memory-palace 1.2.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/lib/client.js ADDED
@@ -0,0 +1,799 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-memory-palace",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
9
+
10
+ const NS = "memory-palace";
11
+
12
+
13
+ const zh = {
14
+ nav: "记忆",
15
+ title: "记忆",
16
+ intro: "memory-palace 记忆宫殿:跨会话持久化的 Markdown 记忆。",
17
+ core: "核心",
18
+ smartSummary: "自动记录",
19
+ storage: "存储路径与预算",
20
+ bridgeLabel: "桥接 buddy 项目记忆",
21
+ bridgeHint: "项目已存在 .workbuddy / .codebuddy 记忆目录时直接读写,不再建 .deepseek-harness/memory/",
22
+ dirsLabel: "buddy 记忆目录(逗号分隔,按序优先)",
23
+ dirsHint: "例如:.workbuddy/memory, .codebuddy/memory",
24
+ userPathLabel: "用户级记忆文件",
25
+ userPathHint: "跨项目个人偏好记忆,默认 ~/.deepseek-harness/MEMORY.md",
26
+ wsDirLabel: "工作区记忆目录(回退)",
27
+ wsDirHint: "无 buddy 目录时回退到该目录,默认 .deepseek-harness/memory",
28
+ retentionLabel: "日志保留天数",
29
+ retentionHint: "超过该天数的每日日志会迁移进 MEMORY.md 后删除",
30
+ userBudgetLabel: "用户级预算(字符)",
31
+ userBudgetHint: "用户级 MEMORY.md 的最大字符数",
32
+ wsBudgetLabel: "工作区预算(字符)",
33
+ wsBudgetHint: "工作区 MEMORY.md 的最大字符数",
34
+ summarizeLabel: "轮次结束自动记录",
35
+ summarizeHint: "它是「agent 主动记忆」主路径失效时的安全网,保证实质工作不丢,代价是只留原始文本、不做总结。",
36
+ autoCaptureErrorsLabel: "对话出错时自动记录到记忆",
37
+ autoCaptureErrorsHint: "对话中(含代码运行报错、工具执行失败)出错时,自动把「错误现象」写入对应 MEMORY.md(用户级/项目级);「根因/方案」由 agent 按记忆公民指令主动记。默认开,关闭无需重启。",
38
+ memoryModeLabel: "记忆模式",
39
+ memoryModeHintPlugin: "插件模式:记忆公民指令 + 轮次轻量 + 错误捕获。切换需重启 dsh 生效。",
40
+ memoryModeHintSmart: "智能模式:LLM 智能会话摘要(summary→每日日志 + durable→MEMORY.md)。切换需重启 dsh 生效。",
41
+ summaryModelLabel: "摘要模型(智能模式)",
42
+ summaryModelHint: "留空=复用当前会话的 provider/model;也可固定廉价小模型省 token。",
43
+ save: "保存",
44
+ saving: "保存中…",
45
+ discard: "放弃",
46
+ unsaved: "未保存",
47
+ overridden: "已覆盖",
48
+ reset: "恢复默认",
49
+ readOnly: "本部署的设置为只读。",
50
+ saveFailed: "本部署没有接受这些值,已保留供你修改。",
51
+ invalidNumber: "请填数字;留空表示使用默认值。",
52
+ // ---- v1.2.0 会话标题栏「记忆」按钮(蒸馏入口)----
53
+ btnMemory: "记忆",
54
+ distillSession: "蒸馏会话",
55
+ distillProject: "蒸馏项目记忆",
56
+ confirmSessionTitle: "确认蒸馏会话",
57
+ confirmSessionTips: "插件已自动生成会话记忆,手动蒸馏可能会导致记忆重复。",
58
+ confirmProjectTitle: "确认蒸馏项目记忆",
59
+ confirmProjectTips: "只有当项目记忆过大时才建议蒸馏,过度蒸馏可能导致关键信息丢失,当前记忆大小:{size} 字符。",
60
+ cancel: "取消",
61
+ confirm: "确认",
62
+ distilling: "蒸馏中…",
63
+ notifySessionDone: "会话记忆蒸馏完成",
64
+ notifyProjectDone: "项目记忆蒸馏完成",
65
+ notifyFail: "记忆蒸馏失败"
66
+ };
67
+
68
+ const en = {
69
+ nav: "Memory",
70
+ title: "Memory",
71
+ intro: "memory-palace: persistent, human-readable Markdown memory across sessions.",
72
+ core: "Core",
73
+ smartSummary: "Auto-record",
74
+ storage: "Storage & budgets",
75
+ bridgeLabel: "Bridge buddy project memory",
76
+ bridgeHint: "Read/write directly into existing .workbuddy / .codebuddy memory dirs instead of creating .deepseek-harness/memory/",
77
+ dirsLabel: "Buddy memory dirs (comma-separated, first wins)",
78
+ dirsHint: "e.g. .workbuddy/memory, .codebuddy/memory",
79
+ userPathLabel: "User memory file",
80
+ userPathHint: "Cross-project preferences, default ~/.deepseek-harness/MEMORY.md",
81
+ wsDirLabel: "Workspace memory dir (fallback)",
82
+ wsDirHint: "Used when no buddy dir exists, default .deepseek-harness/memory",
83
+ retentionLabel: "Daily log retention (days)",
84
+ retentionHint: "Older logs are migrated into MEMORY.md then removed",
85
+ userBudgetLabel: "User budget (chars)",
86
+ userBudgetHint: "Max chars for the user-level MEMORY.md",
87
+ wsBudgetLabel: "Workspace budget (chars)",
88
+ wsBudgetHint: "Max chars for the workspace MEMORY.md",
89
+ summarizeLabel: "Auto-record at turn end",
90
+ summarizeHint: "It is the safety net when the 'agent proactive memory' main path fails: substantive work is never lost, at the cost of keeping only raw text without summarization.",
91
+ autoCaptureErrorsLabel: "Auto-record conversation errors",
92
+ autoCaptureErrorsHint: "On in-session errors, auto write the 'error' into the corresponding MEMORY.md (user/project); 'root cause / fix' is recorded proactively by the agent. On by default; off takes effect without restart.",
93
+ memoryModeLabel: "Memory mode",
94
+ memoryModeHintPlugin: "Plugin mode: memory-citizen instructions + turn-end light entries + error capture. Switching requires a dsh restart.",
95
+ memoryModeHintSmart: "Smart mode: LLM conversation summarization (summary -> daily log + durable -> MEMORY.md). Switching requires a dsh restart.",
96
+ summaryModelLabel: "Summary model (smart mode)",
97
+ summaryModelHint: "Leave empty to reuse the current session's provider/model, or pin a cheap model (provider/model) to save tokens.",
98
+ save: "Save",
99
+ saving: "Saving…",
100
+ discard: "Discard",
101
+ unsaved: "Unsaved",
102
+ overridden: "Overridden",
103
+ reset: "Reset",
104
+ readOnly: "Settings are read-only in this deployment.",
105
+ saveFailed: "This deployment did not accept these values; they are kept for you to edit.",
106
+ invalidNumber: "Enter a number; leave empty to use the default.",
107
+ // ---- v1.2.0 session-header Memory button (distill entry) ----
108
+ btnMemory: "Memory",
109
+ distillSession: "Distill session",
110
+ distillProject: "Distill project memory",
111
+ confirmSessionTitle: "Confirm session distillation",
112
+ confirmSessionTips: "The plugin already generates session memory automatically; manual distillation may create duplicates.",
113
+ confirmProjectTitle: "Confirm project memory distillation",
114
+ confirmProjectTips: "Distill only when project memory is too large; over-distillation may lose key info. Current size: {size} chars.",
115
+ cancel: "Cancel",
116
+ confirm: "Confirm",
117
+ distilling: "Distilling…",
118
+ notifySessionDone: "Session memory distilled",
119
+ notifyProjectDone: "Project memory distilled",
120
+ notifyFail: "Memory distillation failed"
121
+ };
122
+
123
+
124
+ const FIELD_NUMERIC = new Set(["dailyLogRetentionDays", "userBudgetChars", "workspaceBudgetChars"]);
125
+ const FIELD_BOOL = new Set(["bridgeBuddyMemory", "summarize", "autoCaptureErrors"]);
126
+
127
+ function projectDraft(snap) {
128
+ const v = snap && snap.value ? snap.value : {};
129
+ return {
130
+ bridgeBuddyMemory: v.bridgeBuddyMemory === false ? "false" : "true",
131
+ buddyWorkspaceMemoryDirs: Array.isArray(v.buddyWorkspaceMemoryDirs) ? v.buddyWorkspaceMemoryDirs.join(", ") : ".workbuddy/memory, .codebuddy/memory",
132
+ userMemoryPath: v.userMemoryPath || "~/.deepseek-harness/MEMORY.md",
133
+ workspaceMemoryDir: v.workspaceMemoryDir || ".deepseek-harness/memory",
134
+ dailyLogRetentionDays: v.dailyLogRetentionDays != null ? String(v.dailyLogRetentionDays) : "30",
135
+ userBudgetChars: v.userBudgetChars != null ? String(v.userBudgetChars) : "4000",
136
+ workspaceBudgetChars: v.workspaceBudgetChars != null ? String(v.workspaceBudgetChars) : "3000",
137
+ summarize: v.summarize === false ? "false" : "true",
138
+ autoCaptureErrors: v.autoCaptureErrors === false ? "false" : "true",
139
+ memoryMode: v.memoryMode || "plugin",
140
+ summaryModel: v.summaryModel || ""
141
+ };
142
+ }
143
+
144
+ function MemoryPalaceController(ctx) {
145
+ this.ctx = ctx;
146
+ }
147
+ MemoryPalaceController.prototype.inject = function () {
148
+ return {};
149
+ };
150
+
151
+ // v1.1.3:设置读写改走插件自有 route(/memory-palace/api)——dsh web 端 settingsScope 在
152
+ // 非 loopback 连接下 persistence='memory'(set() no-op),apiproxy 又只暴露 allowlist namespace,
153
+ // 设置页保存永远不落盘。同源 fetch 自己的 route 由服务端直写 settings-file,真正保存。
154
+ async function apiGetFull() {
155
+ const resp = await fetch("/memory-palace/api/settings.get", {
156
+ method: "POST",
157
+ headers: { "content-type": "application/json" },
158
+ body: "{}",
159
+ });
160
+ const parsed = await resp.json().catch(() => null);
161
+ if (!parsed || parsed.ok !== true) throw new Error(parsed?.error?.message ?? `HTTP ${resp.status}`);
162
+ // 服务端 viewOf() 整体作为 value:{ value, user, revision }。
163
+ const v = parsed.value || {};
164
+ return { value: v.value || {}, user: v.user || {}, revision: v.revision };
165
+ }
166
+
167
+ async function apiUpdate(section, expectedRevision) {
168
+ const resp = await fetch("/memory-palace/api/settings.update", {
169
+ method: "POST",
170
+ headers: { "content-type": "application/json" },
171
+ body: JSON.stringify({ section, ...(expectedRevision !== undefined ? { expectedRevision } : {}) }),
172
+ });
173
+ const parsed = await resp.json().catch(() => null);
174
+ if (!parsed || parsed.ok !== true) throw new Error(parsed?.error?.message ?? `HTTP ${resp.status}`);
175
+ const v = parsed.value || {};
176
+ return { value: v.value || {}, user: v.user || {}, revision: v.revision };
177
+ }
178
+
179
+
180
+ function MemoryPalaceSection(props) {
181
+ const { t, getConnection } = props;
182
+ const [remote, setRemote] = react.useState({ value: null, user: {}, revision: undefined, loaded: false });
183
+ const [draft, setDraft] = react.useState(() => projectDraft({ value: null }));
184
+ const [dirty, setDirty] = react.useState(false);
185
+ const [saving, setSaving] = react.useState(false);
186
+ const [failed, setFailed] = react.useState(false);
187
+ const [validation, setValidation] = react.useState(null);
188
+ const [modelOptions, setModelOptions] = react.useState([]);
189
+
190
+ // 初始加载(从自有 route 读真实值,绕过 apiproxy allowlist)。
191
+ react.useEffect(() => {
192
+ let alive = true;
193
+ (async () => {
194
+ try {
195
+ const full = await apiGetFull();
196
+ if (alive) {
197
+ setRemote({ value: full.value, user: full.user, revision: full.revision, loaded: true });
198
+ setDraft(projectDraft({ value: full.value }));
199
+ }
200
+ } catch (e) {
201
+ if (alive) setRemote((r) => ({ ...r, loaded: true }));
202
+ }
203
+ })();
204
+ return () => { alive = false; };
205
+ }, []);
206
+
207
+ // 外部变更后校准 draft(仅未编辑时)。
208
+ react.useEffect(() => {
209
+ if (remote.loaded && remote.value && !dirty) setDraft(projectDraft(remote));
210
+ }, [remote, dirty]);
211
+
212
+ react.useEffect(() => {
213
+ let alive = true;
214
+ (async () => {
215
+ try {
216
+ const conn = getConnection && getConnection();
217
+ if (!conn || !conn.api || !conn.api.llm || typeof conn.api.llm.models !== "function") return;
218
+ const r = await conn.api.llm.models({});
219
+ const groups = (r && r.result && r.result.value && r.result.value.groups) || (r && r.groups) || [];
220
+ const opts = [];
221
+ for (const g of groups || []) {
222
+ for (const m of (g && g.models) || []) {
223
+ opts.push({ value: g.id + "/" + m.id, label: g.name + " / " + m.name });
224
+ }
225
+ }
226
+ if (alive) setModelOptions(opts);
227
+ } catch (e) {
228
+ /* 枚举失败:保持空列表,下拉只剩「复用当前会话模型」 */
229
+ }
230
+ })();
231
+ return () => { alive = false; };
232
+ }, []);
233
+
234
+ react.useEffect(() => {
235
+ if (remote.loaded && remote.value && !dirty) setDraft(projectDraft(remote));
236
+ }, [remote, dirty]);
237
+
238
+ const disabled = !remote.loaded || saving;
239
+
240
+ function edit(field, text) {
241
+ setDraft((d) => ({ ...d, [field]: text }));
242
+ setDirty(true);
243
+ setFailed(false);
244
+ if (FIELD_NUMERIC.has(field)) {
245
+ const valid = text === "" || (Number.isFinite(Number(text)) && String(Number(text)) === text.trim());
246
+ setValidation((v) => (valid ? null : field));
247
+ }
248
+ }
249
+
250
+ function resetField(field) {
251
+ setDraft((d) => ({ ...d, [field]: projectDraft(remote)[field] }));
252
+ setDirty(true);
253
+ setFailed(false);
254
+ if (FIELD_NUMERIC.has(field)) setValidation((v) => (v === field ? null : v));
255
+ }
256
+
257
+ async function save() {
258
+ setSaving(true);
259
+ setFailed(false);
260
+ try {
261
+ // 整节替换(replace 语义):空值字段省略 → 自动回退 base/schema 默认。
262
+ const section = {};
263
+ for (const field of Object.keys(draft)) {
264
+ const text = draft[field].trim();
265
+ if (FIELD_BOOL.has(field)) {
266
+ section[field] = text === "true";
267
+ } else if (FIELD_NUMERIC.has(field)) {
268
+ if (text !== "") section[field] = Number(text);
269
+ } else if (field === "buddyWorkspaceMemoryDirs") {
270
+ section[field] = text.split(",").map((s) => s.trim()).filter(Boolean);
271
+ } else {
272
+ if (text !== "") section[field] = text;
273
+ }
274
+ }
275
+ const full = await apiUpdate(section, remote.revision);
276
+ // 保存成功后同步 remote(真实返回值),保留当前 draft(= 用户保存的目标值)。
277
+ setRemote({ value: full.value, user: full.user, revision: full.revision, loaded: true });
278
+ setDirty(false);
279
+ } catch (e) {
280
+ setFailed(true);
281
+ } finally {
282
+ setSaving(false);
283
+ }
284
+ }
285
+
286
+ function discard() {
287
+ setDraft(projectDraft(remote));
288
+ setDirty(false);
289
+ setFailed(false);
290
+ setValidation(null);
291
+ }
292
+
293
+ const h = react.createElement;
294
+
295
+ const userLayer = remote.user && typeof remote.user === "object" ? remote.user : {};
296
+ const isOverridden = (field) => field in userLayer;
297
+
298
+ const toggle = (label, hint, field, disabledOverride) =>
299
+ h("div", { style: toggleStyle }, [
300
+ h("div", null, [
301
+ h("p", { style: { ...labelStyle, margin: 0 } }, label),
302
+ h("p", { style: hintStyle }, hint)
303
+ ]),
304
+ h("input", {
305
+ type: "checkbox",
306
+ checked: draft[field] === "true",
307
+ disabled: disabled || disabledOverride,
308
+ onChange: (e) => edit(field, e.target.checked ? "true" : "false")
309
+ })
310
+ ]);
311
+
312
+ const row = (label, hint, control, field) => h("div", { style: fieldStyle }, [
313
+ h("div", { style: headStyle }, [
314
+ h("label", { style: labelStyle }, label),
315
+ dirty && field ? h("span", { style: mutedBadgeStyle }, t("unsaved")) : null,
316
+ !dirty && field && isOverridden(field) ? h("span", { style: badgeStyle }, t("overridden")) : null
317
+ ]),
318
+ control,
319
+ hint ? h("p", { style: hintStyle }, hint) : null
320
+ ]);
321
+
322
+ const inputStyle = {
323
+ boxSizing: "border-box",
324
+ width: "100%",
325
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
326
+ background: "var(--dsw-alias-bg-layer-3, #fafafa)",
327
+ height: "34px",
328
+ font: "inherit",
329
+ color: "var(--dsw-alias-label-primary, #1f2329)",
330
+ borderRadius: "8px",
331
+ padding: "0 12px",
332
+ fontSize: "13px",
333
+ lineHeight: "1.5"
334
+ };
335
+ const toggleStyle = {
336
+ display: "flex",
337
+ alignItems: "center",
338
+ justifyContent: "space-between",
339
+ gap: "12px",
340
+ padding: "12px 0",
341
+ borderTop: "1px solid var(--dsw-alias-border-l2, #e5e7eb)"
342
+ };
343
+
344
+ const cardStyle = {
345
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
346
+ borderRadius: "12px",
347
+ padding: "14px 16px",
348
+ marginBottom: "12px",
349
+ background: "var(--dsw-alias-bg-layer-2, #ffffff)"
350
+ };
351
+ const groupTitleStyle = {
352
+ margin: "0 0 4px",
353
+ fontSize: "13px",
354
+ fontWeight: "500",
355
+ color: "var(--dsw-alias-label-primary, #1f2329)"
356
+ };
357
+ const sectionStyle = {
358
+ maxWidth: "760px",
359
+ color: "var(--dsw-alias-label-primary, #1f2329)",
360
+ display: "flex",
361
+ flexDirection: "column",
362
+ gap: "12px"
363
+ };
364
+ const headingStyle = { margin: 0, fontSize: "18px", fontWeight: "600" };
365
+ const introStyle = { color: "var(--dsw-alias-label-tertiary, #8a919f)", margin: 0, fontSize: "13px" };
366
+ const actionsStyle = { display: "flex", gap: "8px", alignItems: "center" };
367
+ const btnPrimary = {
368
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
369
+ background: "var(--dsw-alias-bg-module-platform, #f2f3f5)",
370
+ color: "var(--dsw-alias-label-primary, #1f2329)",
371
+ borderRadius: "8px",
372
+ padding: "6px 14px",
373
+ fontSize: "13px",
374
+ cursor: disabled ? "default" : "pointer",
375
+ opacity: disabled ? 0.5 : 1
376
+ };
377
+ const btnAccent = {
378
+ border: "1px solid var(--dsw-alias-brand-primary, #4b5bff)",
379
+ background: "var(--dsw-alias-brand-primary, #4b5bff)",
380
+ color: "#fff",
381
+ borderRadius: "8px",
382
+ padding: "6px 14px",
383
+ fontSize: "13px",
384
+ cursor: disabled ? "default" : "pointer",
385
+ opacity: disabled ? 0.5 : 1
386
+ };
387
+ const fieldStyle = {
388
+ display: "flex",
389
+ flexDirection: "column",
390
+ gap: "6px",
391
+ padding: "12px 0",
392
+ borderTop: "1px solid var(--dsw-alias-border-l2, #e5e7eb)"
393
+ };
394
+ const headStyle = { display: "flex", alignItems: "center", gap: "8px" };
395
+ const labelStyle = {
396
+ minWidth: "0",
397
+ color: "var(--dsw-alias-label-primary, #1f2329)",
398
+ flex: 1,
399
+ fontSize: "13px",
400
+ fontWeight: "500",
401
+ lineHeight: "1.5"
402
+ };
403
+ const badgeStyle = {
404
+ whiteSpace: "nowrap",
405
+ background: "var(--dsw-alias-bg-module-platform, #f2f3f5)",
406
+ color: "var(--dsw-alias-label-secondary, #6b7280)",
407
+ borderRadius: "999px",
408
+ padding: "1px 8px",
409
+ fontSize: "11px",
410
+ fontWeight: "500",
411
+ lineHeight: "17px"
412
+ };
413
+ const mutedBadgeStyle = {
414
+ whiteSpace: "nowrap",
415
+ color: "var(--dsw-alias-label-tertiary, #8a919f)",
416
+ borderRadius: "999px",
417
+ padding: "1px 8px",
418
+ fontSize: "11px",
419
+ lineHeight: "17px"
420
+ };
421
+ const hintStyle = { color: "var(--dsw-alias-label-tertiary, #8a919f)", margin: 0, fontSize: "12px", lineHeight: "1.5" };
422
+ const errStyle = { color: "var(--dsw-alias-label-error, #d54941)", margin: 0, fontSize: "12px", lineHeight: "1.5" };
423
+
424
+ const num = (field) => ({
425
+ type: "text",
426
+ inputMode: "numeric",
427
+ value: draft[field],
428
+ disabled,
429
+ style: { ...inputStyle, borderColor: validation === field ? "var(--dsw-alias-label-error, #d54941)" : undefined },
430
+ onChange: (e) => edit(field, e.target.value)
431
+ });
432
+
433
+ return h("div", { style: sectionStyle }, [
434
+ h("div", { style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "12px" } }, [
435
+ h("div", null, [
436
+ h("h3", { style: headingStyle }, t("title")),
437
+ h("p", { style: introStyle }, t("intro"))
438
+ ]),
439
+ h("div", { style: actionsStyle }, [
440
+ h("button", { type: "button", style: btnPrimary, disabled, onClick: discard }, t("discard")),
441
+ h("button", { type: "button", style: btnAccent, disabled, onClick: save }, saving ? t("saving") : t("save"))
442
+ ])
443
+ ]),
444
+ failed ? h("p", { style: errStyle }, t("saveFailed")) : null,
445
+ h("div", { style: cardStyle }, [
446
+ h("p", { style: groupTitleStyle }, t("core")),
447
+ h("div", { style: toggleStyle }, [
448
+ h("div", null, [
449
+ h("p", { style: { ...labelStyle, margin: 0 } }, t("bridgeLabel")),
450
+ h("p", { style: hintStyle }, t("bridgeHint"))
451
+ ]),
452
+ h("input", {
453
+ type: "checkbox",
454
+ checked: draft.bridgeBuddyMemory === "true",
455
+ disabled,
456
+ onChange: (e) => edit("bridgeBuddyMemory", e.target.checked ? "true" : "false")
457
+ })
458
+ ]),
459
+ row(t("dirsLabel"), t("dirsHint"),
460
+ h("input", {
461
+ ...num("buddyWorkspaceMemoryDirs"),
462
+ type: "text",
463
+ placeholder: ".workbuddy/memory, .codebuddy/memory"
464
+ }), "buddyWorkspaceMemoryDirs")
465
+ ]),
466
+ h("div", { style: cardStyle }, [
467
+ h("p", { style: groupTitleStyle }, t("smartSummary")),
468
+ row(t("memoryModeLabel"), draft.memoryMode === "smart" ? t("memoryModeHintSmart") : t("memoryModeHintPlugin"),
469
+ h("select", {
470
+ value: draft.memoryMode === "smart" ? "smart" : "plugin",
471
+ disabled,
472
+ onChange: (e) => edit("memoryMode", e.target.value),
473
+ style: { ...inputStyle, height: "34px" }
474
+ }, [
475
+ h("option", { value: "plugin" }, "插件模式"),
476
+ h("option", { value: "smart" }, "智能模式")
477
+ ]), "memoryMode"),
478
+ draft.memoryMode === "smart"
479
+ ? row(t("summaryModelLabel"), t("summaryModelHint"),
480
+ h("select", {
481
+ value: draft.summaryModel || "",
482
+ disabled,
483
+ onChange: (e) => edit("summaryModel", e.target.value)
484
+ }, [
485
+ h("option", { value: "" }, "复用当前会话模型"),
486
+ ...modelOptions.map((o) => h("option", { value: o.value }, o.label))
487
+ ]), "summaryModel")
488
+ : h("div", null, [
489
+ toggle(t("summarizeLabel"), t("summarizeHint"), "summarize"),
490
+ toggle(t("autoCaptureErrorsLabel"), t("autoCaptureErrorsHint"), "autoCaptureErrors")
491
+ ])
492
+ ]),
493
+ h("div", { style: cardStyle }, [
494
+ h("p", { style: groupTitleStyle }, t("storage")),
495
+ row(t("userPathLabel"), t("userPathHint"), h("input", { ...num("userMemoryPath"), type: "text" }), "userMemoryPath"),
496
+ row(t("wsDirLabel"), t("wsDirHint"), h("input", { ...num("workspaceMemoryDir"), type: "text" }), "workspaceMemoryDir"),
497
+ row(t("retentionLabel"), t("retentionHint"), h("input", num("dailyLogRetentionDays")), "dailyLogRetentionDays"),
498
+ row(t("userBudgetLabel"), t("userBudgetHint"), h("input", num("userBudgetChars")), "userBudgetChars"),
499
+ row(t("wsBudgetLabel"), t("wsBudgetHint"), h("input", num("workspaceBudgetChars")), "workspaceBudgetChars")
500
+ ])
501
+ ]);
502
+ }
503
+
504
+
505
+ // ---- v1.2.0 会话标题栏「记忆」按钮(蒸馏入口)----
506
+ // sparkle-twinkle.svg 内联(assets/ 不在 package.json files 数组、不随 npm 分发,不能按路径引用)。
507
+ // 类名/渐变 id 加 mpd- 前缀防全局冲突;twinkle 动画 keyframes 由注入的样式表提供。
508
+ const SPARKLE_SVG =
509
+ '<svg width="14" height="14" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg">' +
510
+ '<defs><linearGradient id="mpd-grad-1" x1="0" y1="3" x2="7" y2="10" gradientUnits="userSpaceOnUse">' +
511
+ '<stop stop-color="#426EFE"></stop><stop offset="1" stop-color="#5979E1" stop-opacity="0.4"></stop></linearGradient>' +
512
+ '<linearGradient id="mpd-grad-2" x1="6" y1="0" x2="10" y2="4" gradientUnits="userSpaceOnUse">' +
513
+ '<stop stop-color="#426EFE"></stop><stop offset="1" stop-color="#4F70DC" stop-opacity="0.4"></stop></linearGradient></defs>' +
514
+ '<path class="mpd-star-big" d="M3.80671 9.79513C3.72888 10.0318 3.39417 10.0318 3.31634 9.79513L2.8653 8.42365C2.66042 7.80074 2.17185 7.31217 1.54894 7.1073L0.177462 6.65626C-0.0591539 6.57843 -0.0591539 6.24371 0.177462 6.16589L1.54894 5.71484C2.17185 5.50997 2.66042 5.0214 2.8653 4.39849L3.31634 3.02701C3.39417 2.79039 3.72888 2.79039 3.80671 3.02701L4.25775 4.39849C4.46262 5.0214 4.9512 5.50997 5.57411 5.71484L6.94558 6.16589C7.1822 6.24371 7.1822 6.57843 6.94558 6.65626L5.57411 7.1073C4.9512 7.31217 4.46262 7.80074 4.25775 8.42365L3.80671 9.79513Z" fill="url(#mpd-grad-1)"></path>' +
515
+ '<path class="mpd-star-small" d="M8.15819 3.90034C8.11449 4.03322 7.92653 4.03322 7.88282 3.90034L7.62954 3.13018C7.51449 2.78038 7.24013 2.50602 6.89033 2.39097L6.12016 2.13769C5.98729 2.09398 5.98729 1.90602 6.12016 1.86231L6.89033 1.60903C7.24013 1.49398 7.51449 1.21962 7.62954 0.869819L7.88282 0.0996549C7.92653 -0.0332183 8.11449 -0.0332183 8.15819 0.0996549L8.41148 0.869819C8.52653 1.21962 8.80089 1.49398 9.15069 1.60903L9.92085 1.86231C10.0537 1.90602 10.0537 2.09398 9.92085 2.13769L9.15069 2.39097C8.80089 2.50602 8.52653 2.78038 8.41148 3.13018L8.15819 3.90034Z" fill="url(#mpd-grad-2)"></path></svg>';
516
+
517
+ // 一次性注入按钮/下拉/弹窗所需样式(含 twinkle keyframes);id 防重复,挂载即注入不随卸载移除。
518
+ function injectDistillStyles() {
519
+ if (document.getElementById("memory-palace-distill-styles")) return;
520
+ const style = document.createElement("style");
521
+ style.id = "memory-palace-distill-styles";
522
+ style.textContent = [
523
+ "@keyframes mpd-tw1{0%,100%{opacity:1}50%{opacity:.3}}",
524
+ "@keyframes mpd-tw2{0%,100%{opacity:1}50%{opacity:.3}}",
525
+ ".mpd-star-big{animation:mpd-tw1 2s ease-in-out infinite}",
526
+ ".mpd-star-small{animation:mpd-tw2 1.5s ease-in-out infinite .5s}",
527
+ ".mpd-item:hover{background:var(--dsw-alias-interactive-bg-hover,#f2f3f5)}"
528
+ ].join("");
529
+ document.head.appendChild(style);
530
+ }
531
+
532
+
533
+ // 会话标题栏「记忆」胶囊按钮:点击开合下拉(蒸馏会话/蒸馏项目记忆)→ 自绘确认弹窗 → fetch route → 浏览器通知。
534
+ function DistillButton(props) {
535
+ const h = react.createElement;
536
+ const { sessionId, t } = props;
537
+ const [open, setOpen] = react.useState(false);
538
+ const [confirming, setConfirming] = react.useState(null); // "session" | "project" | null
539
+ const [busy, setBusy] = react.useState(false);
540
+ const [status, setStatus] = react.useState(""); // Notification 未授权时的内联降级提示
541
+ const [size, setSize] = react.useState(null);
542
+ const [hover, setHover] = react.useState(false);
543
+
544
+ react.useEffect(() => { injectDistillStyles(); }, []);
545
+
546
+ // 点击外部关闭下拉。
547
+ react.useEffect(() => {
548
+ if (!open) return;
549
+ const onDoc = (e) => {
550
+ const el = e && e.target;
551
+ if (el && el.closest && !el.closest("[data-memory-palace-distill]")) setOpen(false);
552
+ };
553
+ document.addEventListener("mousedown", onDoc);
554
+ return () => document.removeEventListener("mousedown", onDoc);
555
+ }, [open]);
556
+
557
+ async function apiCall(method, body) {
558
+ try {
559
+ const resp = await fetch(`/memory-palace/api/${method}`, {
560
+ method: "POST",
561
+ headers: { "content-type": "application/json" },
562
+ body: JSON.stringify(body || {}),
563
+ });
564
+ return await resp.json().catch(() => null);
565
+ } catch (e) {
566
+ return null;
567
+ }
568
+ }
569
+
570
+ // 结果提示:优先浏览器通知(Notification 门控,参考 dsh-web-ui-notify);未授权降级内联文本。
571
+ function notify(title, body) {
572
+ try {
573
+ if (typeof Notification !== "undefined" && Notification.permission === "granted") {
574
+ new Notification(title, { body: body || "" });
575
+ return;
576
+ }
577
+ } catch (e) { /* 通知不可用:降级内联 */ }
578
+ setStatus((body || title || "").toString());
579
+ window.setTimeout(() => setStatus(""), 8000);
580
+ }
581
+
582
+ async function pick(kind) {
583
+ setOpen(false);
584
+ if (kind === "project") {
585
+ const r = await apiCall("distill.project.preview", { sessionId });
586
+ const v = r && r.ok && r.value ? r.value : null;
587
+ setSize(v && typeof v.size === "number" ? v.size : null);
588
+ }
589
+ setConfirming(kind);
590
+ }
591
+
592
+ async function run() {
593
+ const kind = confirming;
594
+ setConfirming(null);
595
+ setBusy(true);
596
+ try {
597
+ const method = kind === "session" ? "distill.session" : "distill.project";
598
+ const r = await apiCall(method, { sessionId });
599
+ const v = r && r.ok ? r.value : null;
600
+ const errMsg = r && r.error && r.error.message ? r.error.message : "";
601
+ if (v && v.ok === true) {
602
+ notify(kind === "session" ? t("notifySessionDone") : t("notifyProjectDone"), (v.summary || v.message || "").slice(0, 120));
603
+ } else {
604
+ notify(t("notifyFail"), errMsg || (v && v.message) || "");
605
+ }
606
+ } catch (e) {
607
+ notify(t("notifyFail"), String((e && e.message) || e));
608
+ } finally {
609
+ setBusy(false);
610
+ }
611
+ }
612
+
613
+ const sizeText = size == null ? "…" : String(size).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
614
+ const caretStyle = {
615
+ width: 0,
616
+ height: 0,
617
+ borderLeft: "3px solid transparent",
618
+ borderRight: "3px solid transparent",
619
+ borderTop: "4px solid currentColor",
620
+ opacity: 0.65
621
+ };
622
+ // 胶囊按钮样式对齐「Session log」下载按钮(HeaderAction.module.css)。
623
+ const pillStyle = {
624
+ display: "inline-flex",
625
+ alignItems: "center",
626
+ justifyContent: "center",
627
+ gap: "5px",
628
+ height: "32px",
629
+ padding: "6px 12px",
630
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
631
+ borderRadius: "18px",
632
+ background: hover ? "var(--dsw-alias-interactive-bg-hover, #f2f3f5)" : "transparent",
633
+ color: "var(--dsw-alias-label-primary, #1f2329)",
634
+ fontFamily: "var(--dsw-font-family, inherit)",
635
+ fontSize: "13px",
636
+ fontWeight: 400,
637
+ lineHeight: "20px",
638
+ cursor: busy ? "wait" : "pointer",
639
+ whiteSpace: "nowrap",
640
+ opacity: busy ? 0.7 : 1
641
+ };
642
+ const menuStyle = {
643
+ position: "absolute",
644
+ top: "calc(100% + 4px)",
645
+ right: "0",
646
+ minWidth: "176px",
647
+ background: "var(--dsw-alias-bg-layer-2, #ffffff)",
648
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
649
+ borderRadius: "12px",
650
+ padding: "5px",
651
+ zIndex: 100
652
+ };
653
+ const itemStyle = {
654
+ display: "flex",
655
+ alignItems: "center",
656
+ justifyContent: "space-between",
657
+ fontSize: "13px",
658
+ color: "var(--dsw-alias-label-primary, #1f2329)",
659
+ padding: "8px 10px",
660
+ borderRadius: "8px",
661
+ cursor: "pointer",
662
+ lineHeight: "1.4"
663
+ };
664
+ const overlayStyle = {
665
+ position: "fixed",
666
+ inset: "0",
667
+ background: "rgba(0,0,0,0.25)",
668
+ display: "flex",
669
+ alignItems: "center",
670
+ justifyContent: "center",
671
+ zIndex: 1000
672
+ };
673
+ const modalStyle = {
674
+ background: "var(--dsw-alias-bg-layer-2, #ffffff)",
675
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
676
+ borderRadius: "12px",
677
+ padding: "16px",
678
+ minWidth: "300px",
679
+ maxWidth: "440px",
680
+ boxSizing: "border-box"
681
+ };
682
+ const modalTitleStyle = { margin: "0 0 8px", fontSize: "14px", fontWeight: 500, lineHeight: "1.5" };
683
+ const modalTipsStyle = {
684
+ margin: "0 0 14px",
685
+ fontSize: "13px",
686
+ color: "var(--dsw-alias-label-secondary, #6b7280)",
687
+ lineHeight: "1.6"
688
+ };
689
+ const modalActionsStyle = { display: "flex", justifyContent: "flex-end", gap: "8px" };
690
+ const btnCancelStyle = {
691
+ color: "var(--dsw-alias-label-secondary, #6b7280)",
692
+ background: "var(--dsw-alias-bg-module-platform, #f2f3f5)",
693
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
694
+ borderRadius: "8px",
695
+ padding: "6px 14px",
696
+ fontSize: "12px",
697
+ fontWeight: 500,
698
+ cursor: "pointer"
699
+ };
700
+ const btnOkStyle = {
701
+ color: "#fff",
702
+ background: "#426EFE",
703
+ border: "1px solid #426EFE",
704
+ borderRadius: "8px",
705
+ padding: "6px 14px",
706
+ fontSize: "12px",
707
+ fontWeight: 500,
708
+ cursor: "pointer"
709
+ };
710
+ const statusStyle = {
711
+ position: "absolute",
712
+ top: "calc(100% + 4px)",
713
+ right: "0",
714
+ maxWidth: "280px",
715
+ background: "var(--dsw-alias-bg-layer-3, #fafafa)",
716
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
717
+ borderRadius: "8px",
718
+ padding: "6px 10px",
719
+ fontSize: "12px",
720
+ color: "var(--dsw-alias-label-secondary, #6b7280)",
721
+ zIndex: 100
722
+ };
723
+
724
+ return h("div", { "data-memory-palace-distill": "", style: { position: "relative", display: "inline-flex" } }, [
725
+ h("button", {
726
+ type: "button",
727
+ style: pillStyle,
728
+ disabled: busy,
729
+ title: t("btnMemory"),
730
+ onClick: () => setOpen(!open),
731
+ onMouseEnter: () => setHover(true),
732
+ onMouseLeave: () => setHover(false)
733
+ }, [
734
+ h("span", { dangerouslySetInnerHTML: { __html: SPARKLE_SVG } }),
735
+ h("span", null, busy ? t("distilling") : t("btnMemory")),
736
+ h("span", { style: caretStyle })
737
+ ]),
738
+ open
739
+ ? h("div", { style: menuStyle }, [
740
+ h("div", { className: "mpd-item", style: itemStyle, onClick: () => pick("session") }, t("distillSession")),
741
+ h("div", { className: "mpd-item", style: itemStyle, onClick: () => pick("project") }, t("distillProject"))
742
+ ])
743
+ : null,
744
+ status ? h("div", { style: statusStyle }, status) : null,
745
+ confirming
746
+ ? h("div", {
747
+ style: overlayStyle,
748
+ onClick: (e) => { if (e && e.target === e.currentTarget) setConfirming(null); }
749
+ }, [
750
+ h("div", { style: modalStyle }, [
751
+ h("p", { style: modalTitleStyle }, confirming === "session" ? t("confirmSessionTitle") : t("confirmProjectTitle")),
752
+ h("p", { style: modalTipsStyle }, confirming === "session" ? t("confirmSessionTips") : t("confirmProjectTips", { size: sizeText })),
753
+ h("div", { style: modalActionsStyle }, [
754
+ h("button", { type: "button", style: btnCancelStyle, onClick: () => setConfirming(null) }, t("cancel")),
755
+ h("button", { type: "button", style: btnOkStyle, onClick: run }, t("confirm"))
756
+ ])
757
+ ])
758
+ ])
759
+ : null
760
+ ]);
761
+ }
762
+
763
+
764
+ function apply(ctx) {
765
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "memory-palace: dictionaries");
766
+ const t = ctx.locale.bind(NS);
767
+ // v1.1.3:设置读写走自有 route(/memory-palace/api),不再 bind settingsScope——
768
+ // 非 loopback 下 settingsScope persistence=memory(set() no-op),保存永不落盘。
769
+ const controller = new MemoryPalaceController(ctx);
770
+ // v1.1.0:枚举已配置模型供「摘要模型」下拉(connection 服务,枚举失败则降级为空列表)。
771
+ const getConnection = () => {
772
+ try {
773
+ return ctx.get("connection");
774
+ } catch {
775
+ return null;
776
+ }
777
+ };
778
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
779
+ name: "settings.section",
780
+ id: "memory-palace",
781
+ order: 20,
782
+ label: () => t("nav"),
783
+ locale: NS,
784
+ inject: () => controller.inject()
785
+ }, (props) => MemoryPalaceSection({ ...props, getConnection })));
786
+ // v1.2.0:会话标题栏「记忆」按钮——headerUtilities 区(右对齐),order:-1 排在 Session log 左边。
787
+ ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
788
+ name: "conversation.session.header.utilities",
789
+ id: "memory-palace-distill",
790
+ order: -1,
791
+ locale: NS
792
+ }, DistillButton));
793
+ }
794
+
795
+ exports.apply = apply;
796
+ exports.inject = ["slots", "locale", "connection"];
797
+ return module.exports;
798
+ }
799
+ });