dsh-memory-palace 1.6.0 → 1.6.2-alpha.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.
Files changed (4) hide show
  1. package/README.md +189 -184
  2. package/lib/client.js +433 -433
  3. package/lib/index.js +363 -363
  4. package/package.json +56 -56
package/lib/client.js CHANGED
@@ -1,14 +1,14 @@
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
- // v1.6.0-rc1:移除 require("@deepseek-ai/dsh-client-runtime/client")——该包在
9
- // DSH 0.1.2-alpha.2 已整体删除(原变量赋值后零使用),平台 seed 表的 react 足够。
10
-
11
- const NS = "memory-palace";
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
+ // v1.6.0-rc1:移除 require("@deepseek-ai/dsh-client-runtime/client")——该包在
9
+ // DSH 0.1.2-alpha.2 已整体删除(原变量赋值后零使用),平台 seed 表的 react 足够。
10
+
11
+ const NS = "memory-palace";
12
12
 
13
13
 
14
14
  const zh = {
@@ -215,397 +215,397 @@ window.__ModuleLoader__.load({
215
215
  }
216
216
 
217
217
 
218
- // v1.6.0-rc1:模型枚举改走 ctx.remote.session.modelCatalog()——ApiProxy 包已在
219
- // DSH 0.1.2-alpha.2 删除(旧 conn.api.llm.models 死了)。服务缺席 / 非 ok / 异常
220
- // 一律降级为空列表,下拉只剩「复用当前会话模型」,UX 不变。
221
- async function fetchModelOptions(ctx) {
222
- try {
223
- const remote = ctx && ctx.remote;
224
- if (!remote || !remote.session || typeof remote.session.modelCatalog !== "function") return [];
225
- const response = await remote.session.modelCatalog();
226
- if (!response || !response.ok || !response.value) return [];
227
- const opts = [];
228
- for (const g of response.value.groups || []) {
229
- for (const m of (g && g.models) || []) {
230
- // m.id 已含 provider 前缀(如 nvidia/nemotron-3-ultra-550b-a55b),直接用,避免重复拼接
231
- opts.push({ value: m.id, label: (g.name || g.id) + " / " + (m.name || m.id) });
232
- }
233
- }
234
- return opts;
235
- } catch (e) {
236
- return [];
237
- }
238
- }
239
-
240
- function MemoryPalaceSection(props) {
241
- const { t, fetchModels } = props;
242
- const [remote, setRemote] = react.useState({ value: null, user: {}, revision: undefined, loaded: false });
243
- const [draft, setDraft] = react.useState(() => projectDraft({ value: null }));
244
- const [dirty, setDirty] = react.useState(false);
245
- const [saving, setSaving] = react.useState(false);
246
- const [failed, setFailed] = react.useState(false);
247
- const [validation, setValidation] = react.useState(null);
248
- const [modelOptions, setModelOptions] = react.useState([]);
249
-
250
- // 初始加载(从自有 route 读真实值)。
251
- react.useEffect(() => {
252
- let alive = true;
253
- (async () => {
254
- try {
255
- const full = await apiGetFull();
256
- if (alive) {
257
- setRemote({ value: full.value, user: full.user, revision: full.revision, loaded: true });
258
- setDraft(projectDraft({ value: full.value }));
259
- }
260
- } catch (e) {
261
- if (alive) setRemote((r) => ({ ...r, loaded: true }));
262
- }
263
- })();
264
- return () => { alive = false; };
265
- }, []);
266
-
267
- // 外部变更后校准 draft(仅未编辑时)。
268
- react.useEffect(() => {
269
- if (remote.loaded && remote.value && !dirty) setDraft(projectDraft(remote));
270
- }, [remote, dirty]);
271
-
272
- react.useEffect(() => {
273
- let alive = true;
274
- (async () => {
275
- try {
276
- const opts = await (fetchModels && fetchModels());
277
- if (alive) setModelOptions(Array.isArray(opts) ? opts : []);
278
- } catch (e) {
279
- /* 枚举失败:保持空列表,下拉只剩「复用当前会话模型」 */
280
- }
281
- })();
282
- return () => { alive = false; };
283
- }, []);
284
-
285
- react.useEffect(() => {
286
- if (remote.loaded && remote.value && !dirty) setDraft(projectDraft(remote));
287
- }, [remote, dirty]);
288
-
289
- const disabled = !remote.loaded || saving;
290
-
291
- function edit(field, text) {
292
- setDraft((d) => ({ ...d, [field]: text }));
293
- setDirty(true);
294
- setFailed(false);
295
- if (FIELD_NUMERIC.has(field)) {
296
- const valid = text === "" || (Number.isFinite(Number(text)) && String(Number(text)) === text.trim());
297
- setValidation((v) => (valid ? null : field));
298
- }
299
- }
300
-
301
- function resetField(field) {
302
- setDraft((d) => ({ ...d, [field]: projectDraft(remote)[field] }));
303
- setDirty(true);
304
- setFailed(false);
305
- if (FIELD_NUMERIC.has(field)) setValidation((v) => (v === field ? null : v));
306
- }
307
-
308
- async function save() {
309
- setSaving(true);
310
- setFailed(false);
311
- try {
312
- // 整节替换(replace 语义):空值字段省略 → 自动回退 base/schema 默认。
313
- const section = {};
314
- for (const field of Object.keys(draft)) {
315
- const text = draft[field].trim();
316
- if (FIELD_BOOL.has(field)) {
317
- section[field] = text === "true";
318
- } else if (FIELD_NUMERIC.has(field)) {
319
- if (text !== "") {
320
- const n = Number(text);
321
- section[field] = field === "summaryTimeoutMs" ? n * 1000 : n;
322
- }
323
- } else if (field === "buddyWorkspaceMemoryDirs") {
324
- section[field] = text.split(",").map((s) => s.trim()).filter(Boolean);
325
- } else {
326
- if (text !== "") section[field] = text;
327
- }
328
- }
329
- const full = await apiUpdate(section, remote.revision);
330
- // 保存成功后同步 remote(真实返回值),保留当前 draft(= 用户保存的目标值)。
331
- setRemote({ value: full.value, user: full.user, revision: full.revision, loaded: true });
332
- setDirty(false);
333
- } catch (e) {
334
- setFailed(true);
335
- } finally {
336
- setSaving(false);
337
- }
338
- }
339
-
340
- function discard() {
341
- setDraft(projectDraft(remote));
342
- setDirty(false);
343
- setFailed(false);
344
- setValidation(null);
345
- }
346
-
347
- const h = react.createElement;
348
-
349
- const userLayer = remote.user && typeof remote.user === "object" ? remote.user : {};
350
- const isOverridden = (field) => field in userLayer;
351
-
352
- // 左侧文字容器:min-width:0 + flexShrink:1 让长描述正常换行收缩,不再撑爆 flex 把 checkbox 挤变形。
353
- const toggleLabelBoxStyle = { flexShrink: 1, minWidth: 0 };
354
- const toggle = (label, hint, field, disabledOverride) =>
355
- h("div", { style: toggleStyle }, [
356
- h("div", { style: toggleLabelBoxStyle }, [
357
- h("p", { style: { ...labelStyle, margin: 0 } }, label),
358
- h("p", { style: hintStyle }, hint)
359
- ]),
360
- h("input", {
361
- type: "checkbox",
362
- checked: draft[field] === "true",
363
- disabled: disabled || disabledOverride,
364
- style: checkboxStyle,
365
- onChange: (e) => edit(field, e.target.checked ? "true" : "false")
366
- })
367
- ]);
368
-
369
- const row = (label, hint, control, field) => h("div", { style: fieldStyle }, [
370
- h("div", { style: headStyle }, [
371
- h("label", { style: labelStyle }, label),
372
- dirty && field ? h("span", { style: mutedBadgeStyle }, t("unsaved")) : null,
373
- !dirty && field && isOverridden(field) ? h("span", { style: badgeStyle }, t("overridden")) : null
374
- ]),
375
- control,
376
- hint ? h("p", { style: hintStyle }, hint) : null
377
- ]);
378
-
379
- const inputStyle = {
380
- boxSizing: "border-box",
381
- width: "100%",
382
- border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
383
- background: "var(--dsw-alias-bg-layer-3, #fafafa)",
384
- height: "34px",
385
- font: "inherit",
386
- color: "var(--dsw-alias-label-primary, #1f2329)",
387
- borderRadius: "8px",
388
- padding: "0 12px",
389
- fontSize: "13px",
390
- lineHeight: "1.5"
391
- };
392
- const selectStyle = {
393
- ...inputStyle,
394
- height: "34px",
395
- appearance: "none",
396
- WebkitAppearance: "none",
397
- backgroundImage: "url(data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E)",
398
- backgroundPosition: "right 12px center",
399
- backgroundRepeat: "no-repeat",
400
- backgroundSize: "12px 12px",
401
- paddingRight: "32px"
402
- };
403
- const checkboxStyle = {
404
- width: "18px",
405
- height: "18px",
406
- margin: "0",
407
- accentColor: "var(--dsw-alias-brand-primary, #4b5bff)",
408
- // 锁死:不被 flex 长描述挤压缩(flex-shrink 默认 1 会在布局阶段无视 width/!important)。
409
- flexShrink: 0,
410
- flexGrow: 0,
411
- alignSelf: "center"
412
- };
413
- const toggleStyle = {
414
- display: "flex",
415
- alignItems: "center",
416
- justifyContent: "space-between",
417
- gap: "12px",
418
- padding: "12px 0",
419
- borderTop: "1px solid var(--dsw-alias-border-l2, #e5e7eb)"
420
- };
421
-
422
- const cardStyle = {
423
- border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
424
- borderRadius: "12px",
425
- padding: "14px 16px",
426
- marginBottom: "12px",
427
- background: "var(--dsw-alias-bg-layer-2, #ffffff)"
428
- };
429
- const groupTitleStyle = {
430
- margin: "0 0 4px",
431
- fontSize: "13px",
432
- fontWeight: "500",
433
- color: "var(--dsw-alias-label-primary, #1f2329)"
434
- };
435
- const sectionStyle = {
436
- maxWidth: "760px",
437
- color: "var(--dsw-alias-label-primary, #1f2329)",
438
- display: "flex",
439
- flexDirection: "column",
440
- gap: "12px"
441
- };
442
- const headingStyle = { margin: 0, fontSize: "18px", fontWeight: "600" };
443
- const introStyle = { color: "var(--dsw-alias-label-tertiary, #8a919f)", margin: 0, fontSize: "13px" };
444
- const actionsStyle = { display: "flex", gap: "8px", alignItems: "center" };
445
- const btnPrimary = {
446
- border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
447
- background: "var(--dsw-alias-bg-module-platform, #f2f3f5)",
448
- color: "var(--dsw-alias-label-primary, #1f2329)",
449
- borderRadius: "8px",
450
- padding: "6px 14px",
451
- fontSize: "13px",
452
- cursor: disabled ? "default" : "pointer",
453
- opacity: disabled ? 0.5 : 1
454
- };
455
- const btnAccent = {
456
- border: "1px solid var(--dsw-alias-brand-primary, #4b5bff)",
457
- background: "var(--dsw-alias-brand-primary, #4b5bff)",
458
- color: "#fff",
459
- borderRadius: "8px",
460
- padding: "6px 14px",
461
- fontSize: "13px",
462
- cursor: disabled ? "default" : "pointer",
463
- opacity: disabled ? 0.5 : 1
464
- };
465
- const fieldStyle = {
466
- display: "flex",
467
- flexDirection: "column",
468
- gap: "6px",
469
- padding: "12px 0",
470
- borderTop: "1px solid var(--dsw-alias-border-l2, #e5e7eb)"
471
- };
472
- const headStyle = { display: "flex", alignItems: "center", gap: "8px" };
473
- const labelStyle = {
474
- minWidth: "0",
475
- color: "var(--dsw-alias-label-primary, #1f2329)",
476
- flex: 1,
477
- fontSize: "13px",
478
- fontWeight: "500",
479
- lineHeight: "1.5"
480
- };
481
- const badgeStyle = {
482
- whiteSpace: "nowrap",
483
- background: "var(--dsw-alias-bg-module-platform, #f2f3f5)",
484
- color: "var(--dsw-alias-label-secondary, #6b7280)",
485
- borderRadius: "999px",
486
- padding: "1px 8px",
487
- fontSize: "11px",
488
- fontWeight: "500",
489
- lineHeight: "17px"
490
- };
491
- const mutedBadgeStyle = {
492
- whiteSpace: "nowrap",
493
- color: "var(--dsw-alias-label-tertiary, #8a919f)",
494
- borderRadius: "999px",
495
- padding: "1px 8px",
496
- fontSize: "11px",
497
- lineHeight: "17px"
498
- };
499
- const hintStyle = { color: "var(--dsw-alias-label-tertiary, #8a919f)", margin: 0, fontSize: "12px", lineHeight: "1.5" };
500
- const errStyle = { color: "var(--dsw-alias-label-error, #d54941)", margin: 0, fontSize: "12px", lineHeight: "1.5" };
501
-
502
- const num = (field) => ({
503
- type: "text",
504
- inputMode: "numeric",
505
- value: draft[field],
506
- disabled,
507
- style: { ...inputStyle, borderColor: validation === field ? "var(--dsw-alias-label-error, #d54941)" : undefined },
508
- onChange: (e) => edit(field, e.target.value)
509
- });
510
-
511
- return h("div", { className: "memory-palace-settings", style: sectionStyle }, [
512
- // 强制覆盖宿主 Webview 全局样式对原生 checkbox 尺寸的覆盖(内联 style 可能不敌全局 CSS)。
513
- h("style", null, ".memory-palace-settings input[type=checkbox]{width:18px!important;height:18px!important;margin:0;flex-shrink:0!important;cursor:pointer;}"),
514
- h("div", { style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "12px" } }, [
515
- h("div", null, [
516
- h("h3", { style: headingStyle }, t("title")),
517
- h("p", { style: introStyle }, t("intro"))
518
- ]),
519
- h("div", { style: actionsStyle }, [
520
- h("button", { type: "button", style: btnPrimary, disabled, onClick: discard }, t("discard")),
521
- h("button", { type: "button", style: btnAccent, disabled, onClick: save }, saving ? t("saving") : t("save"))
522
- ])
523
- ]),
524
- failed ? h("p", { style: errStyle }, t("saveFailed")) : null,
525
- h("div", { style: cardStyle }, [
526
- h("p", { style: groupTitleStyle }, t("core")),
527
- h("div", { style: toggleStyle }, [
528
- h("div", { style: { flexShrink: 1, minWidth: 0 } }, [
529
- h("p", { style: { ...labelStyle, margin: 0 } }, t("bridgeLabel")),
530
- h("p", { style: hintStyle }, t("bridgeHint"))
531
- ]),
532
- h("input", {
533
- type: "checkbox",
534
- checked: draft.bridgeBuddyMemory === "true",
535
- disabled,
536
- style: checkboxStyle,
537
- onChange: (e) => edit("bridgeBuddyMemory", e.target.checked ? "true" : "false")
538
- })
539
- ]),
540
- row(t("dirsLabel"), t("dirsHint"),
541
- h("input", {
542
- ...num("buddyWorkspaceMemoryDirs"),
543
- type: "text",
544
- placeholder: ".workbuddy/memory, .codebuddy/memory"
545
- }), "buddyWorkspaceMemoryDirs")
546
- ]),
547
- h("div", { style: cardStyle }, [
548
- h("p", { style: groupTitleStyle }, t("smartSummary")),
549
- // 轮次结束自动记录(summarize):三种记忆模式的共用总闸门,始终显示,置于记忆模式之上、不随模式切换隐藏。
550
- toggle(t("summarizeLabel"), t("summarizeHint"), "summarize"),
551
- row(t("memoryModeLabel"),
552
- draft.memoryMode === "smart"
553
- ? t("memoryModeHintSmart")
554
- : draft.memoryMode === "hybrid"
555
- ? t("memoryModeHintHybrid")
556
- : t("memoryModeHintPlugin"),
557
- h("select", {
558
- value: ["plugin", "smart", "hybrid"].includes(draft.memoryMode) ? draft.memoryMode : "plugin",
559
- disabled,
560
- onChange: (e) => edit("memoryMode", e.target.value),
561
- style: selectStyle
562
- }, [
563
- h("option", { value: "plugin" }, "插件模式"),
564
- h("option", { value: "smart" }, "智能模式"),
565
- h("option", { value: "hybrid" }, "混合模式")
566
- ]), "memoryMode"),
567
- draft.memoryMode === "smart"
568
- ? h("div", null, [
569
- row(t("summaryModelLabel"), t("summaryModelHint"),
570
- h("select", {
571
- // 已存储值不在下拉选项内(如旧版双前缀脏数据)→ 归零为「复用当前会话模型」,
572
- // 避免 select 显示悬空值、保存时又把脏值写回 settings.yaml。
573
- value: modelOptions.some((o) => o.value === draft.summaryModel) ? draft.summaryModel : "",
574
- disabled,
575
- style: selectStyle,
576
- onChange: (e) => edit("summaryModel", e.target.value)
577
- }, [
578
- h("option", { value: "" }, "复用当前会话模型"),
579
- ...modelOptions.map((o) => h("option", { value: o.value }, o.label))
580
- ]), "summaryModel"),
581
- toggle(t("feedbackEnabledLabel"), t("feedbackEnabledHint"), "feedbackEnabled"),
582
- row(t("summaryMaxTokensLabel"), t("summaryMaxTokensHint"), h("input", num("summaryMaxTokens")), "summaryMaxTokens"),
583
- row(t("projectMaxTokensLabel"), t("projectMaxTokensHint"), h("input", num("projectMaxTokens")), "projectMaxTokens")
584
- ])
585
- : draft.memoryMode === "hybrid"
586
- ? h("div", null, [
587
- row(t("reorgCooldownLabel"), t("reorgCooldownHint"), h("input", num("reorgCooldownDays")), "reorgCooldownDays"),
588
- row(t("subagentLogBudgetLabel"), t("subagentLogBudgetHint"), h("input", num("subagentLogBudget")), "subagentLogBudget")
589
- ])
590
- : h("div", null, [
591
- toggle(t("autoCaptureErrorsLabel"), t("autoCaptureErrorsHint"), "autoCaptureErrors")
592
- ])
593
- ]),
594
- h("div", { style: cardStyle }, [
595
- h("p", { style: groupTitleStyle }, t("storage")),
596
- row(t("userPathLabel"), t("userPathHint"), h("input", { ...num("userMemoryPath"), type: "text" }), "userMemoryPath"),
597
- row(t("wsDirLabel"), t("wsDirHint"), h("input", { ...num("workspaceMemoryDir"), type: "text" }), "workspaceMemoryDir"),
598
- row(t("retentionLabel"), t("retentionHint"), h("input", num("dailyLogRetentionDays")), "dailyLogRetentionDays"),
599
- row(t("userBudgetLabel"), t("userBudgetHint"), h("input", num("userBudgetChars")), "userBudgetChars"),
600
- row(t("wsBudgetLabel"), t("wsBudgetHint"), h("input", num("workspaceBudgetChars")), "workspaceBudgetChars")
601
- ]),
602
- h("div", { style: cardStyle }, [
603
- h("p", { style: groupTitleStyle }, t("dev")),
604
- row(t("distillTimeoutLabel"), t("distillTimeoutHint"), h("input", num("summaryTimeoutMs")), "summaryTimeoutMs"),
605
- toggle(t("distillDebugLogLabel"), t("distillDebugLogHint"), "distillDebugLog")
606
- ])
607
- ]);
608
- }
218
+ // v1.6.0-rc1:模型枚举改走 ctx.remote.session.modelCatalog()——ApiProxy 包已在
219
+ // DSH 0.1.2-alpha.2 删除(旧 conn.api.llm.models 死了)。服务缺席 / 非 ok / 异常
220
+ // 一律降级为空列表,下拉只剩「复用当前会话模型」,UX 不变。
221
+ async function fetchModelOptions(ctx) {
222
+ try {
223
+ const remote = ctx && ctx.remote;
224
+ if (!remote || !remote.session || typeof remote.session.modelCatalog !== "function") return [];
225
+ const response = await remote.session.modelCatalog();
226
+ if (!response || !response.ok || !response.value) return [];
227
+ const opts = [];
228
+ for (const g of response.value.groups || []) {
229
+ for (const m of (g && g.models) || []) {
230
+ // m.id 已含 provider 前缀(如 nvidia/nemotron-3-ultra-550b-a55b),直接用,避免重复拼接
231
+ opts.push({ value: m.id, label: (g.name || g.id) + " / " + (m.name || m.id) });
232
+ }
233
+ }
234
+ return opts;
235
+ } catch (e) {
236
+ return [];
237
+ }
238
+ }
239
+
240
+ function MemoryPalaceSection(props) {
241
+ const { t, fetchModels } = props;
242
+ const [remote, setRemote] = react.useState({ value: null, user: {}, revision: undefined, loaded: false });
243
+ const [draft, setDraft] = react.useState(() => projectDraft({ value: null }));
244
+ const [dirty, setDirty] = react.useState(false);
245
+ const [saving, setSaving] = react.useState(false);
246
+ const [failed, setFailed] = react.useState(false);
247
+ const [validation, setValidation] = react.useState(null);
248
+ const [modelOptions, setModelOptions] = react.useState([]);
249
+
250
+ // 初始加载(从自有 route 读真实值)。
251
+ react.useEffect(() => {
252
+ let alive = true;
253
+ (async () => {
254
+ try {
255
+ const full = await apiGetFull();
256
+ if (alive) {
257
+ setRemote({ value: full.value, user: full.user, revision: full.revision, loaded: true });
258
+ setDraft(projectDraft({ value: full.value }));
259
+ }
260
+ } catch (e) {
261
+ if (alive) setRemote((r) => ({ ...r, loaded: true }));
262
+ }
263
+ })();
264
+ return () => { alive = false; };
265
+ }, []);
266
+
267
+ // 外部变更后校准 draft(仅未编辑时)。
268
+ react.useEffect(() => {
269
+ if (remote.loaded && remote.value && !dirty) setDraft(projectDraft(remote));
270
+ }, [remote, dirty]);
271
+
272
+ react.useEffect(() => {
273
+ let alive = true;
274
+ (async () => {
275
+ try {
276
+ const opts = await (fetchModels && fetchModels());
277
+ if (alive) setModelOptions(Array.isArray(opts) ? opts : []);
278
+ } catch (e) {
279
+ /* 枚举失败:保持空列表,下拉只剩「复用当前会话模型」 */
280
+ }
281
+ })();
282
+ return () => { alive = false; };
283
+ }, []);
284
+
285
+ react.useEffect(() => {
286
+ if (remote.loaded && remote.value && !dirty) setDraft(projectDraft(remote));
287
+ }, [remote, dirty]);
288
+
289
+ const disabled = !remote.loaded || saving;
290
+
291
+ function edit(field, text) {
292
+ setDraft((d) => ({ ...d, [field]: text }));
293
+ setDirty(true);
294
+ setFailed(false);
295
+ if (FIELD_NUMERIC.has(field)) {
296
+ const valid = text === "" || (Number.isFinite(Number(text)) && String(Number(text)) === text.trim());
297
+ setValidation((v) => (valid ? null : field));
298
+ }
299
+ }
300
+
301
+ function resetField(field) {
302
+ setDraft((d) => ({ ...d, [field]: projectDraft(remote)[field] }));
303
+ setDirty(true);
304
+ setFailed(false);
305
+ if (FIELD_NUMERIC.has(field)) setValidation((v) => (v === field ? null : v));
306
+ }
307
+
308
+ async function save() {
309
+ setSaving(true);
310
+ setFailed(false);
311
+ try {
312
+ // 整节替换(replace 语义):空值字段省略 → 自动回退 base/schema 默认。
313
+ const section = {};
314
+ for (const field of Object.keys(draft)) {
315
+ const text = draft[field].trim();
316
+ if (FIELD_BOOL.has(field)) {
317
+ section[field] = text === "true";
318
+ } else if (FIELD_NUMERIC.has(field)) {
319
+ if (text !== "") {
320
+ const n = Number(text);
321
+ section[field] = field === "summaryTimeoutMs" ? n * 1000 : n;
322
+ }
323
+ } else if (field === "buddyWorkspaceMemoryDirs") {
324
+ section[field] = text.split(",").map((s) => s.trim()).filter(Boolean);
325
+ } else {
326
+ if (text !== "") section[field] = text;
327
+ }
328
+ }
329
+ const full = await apiUpdate(section, remote.revision);
330
+ // 保存成功后同步 remote(真实返回值),保留当前 draft(= 用户保存的目标值)。
331
+ setRemote({ value: full.value, user: full.user, revision: full.revision, loaded: true });
332
+ setDirty(false);
333
+ } catch (e) {
334
+ setFailed(true);
335
+ } finally {
336
+ setSaving(false);
337
+ }
338
+ }
339
+
340
+ function discard() {
341
+ setDraft(projectDraft(remote));
342
+ setDirty(false);
343
+ setFailed(false);
344
+ setValidation(null);
345
+ }
346
+
347
+ const h = react.createElement;
348
+
349
+ const userLayer = remote.user && typeof remote.user === "object" ? remote.user : {};
350
+ const isOverridden = (field) => field in userLayer;
351
+
352
+ // 左侧文字容器:min-width:0 + flexShrink:1 让长描述正常换行收缩,不再撑爆 flex 把 checkbox 挤变形。
353
+ const toggleLabelBoxStyle = { flexShrink: 1, minWidth: 0 };
354
+ const toggle = (label, hint, field, disabledOverride) =>
355
+ h("div", { style: toggleStyle }, [
356
+ h("div", { style: toggleLabelBoxStyle }, [
357
+ h("p", { style: { ...labelStyle, margin: 0 } }, label),
358
+ h("p", { style: hintStyle }, hint)
359
+ ]),
360
+ h("input", {
361
+ type: "checkbox",
362
+ checked: draft[field] === "true",
363
+ disabled: disabled || disabledOverride,
364
+ style: checkboxStyle,
365
+ onChange: (e) => edit(field, e.target.checked ? "true" : "false")
366
+ })
367
+ ]);
368
+
369
+ const row = (label, hint, control, field) => h("div", { style: fieldStyle }, [
370
+ h("div", { style: headStyle }, [
371
+ h("label", { style: labelStyle }, label),
372
+ dirty && field ? h("span", { style: mutedBadgeStyle }, t("unsaved")) : null,
373
+ !dirty && field && isOverridden(field) ? h("span", { style: badgeStyle }, t("overridden")) : null
374
+ ]),
375
+ control,
376
+ hint ? h("p", { style: hintStyle }, hint) : null
377
+ ]);
378
+
379
+ const inputStyle = {
380
+ boxSizing: "border-box",
381
+ width: "100%",
382
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
383
+ background: "var(--dsw-alias-bg-layer-3, #fafafa)",
384
+ height: "34px",
385
+ font: "inherit",
386
+ color: "var(--dsw-alias-label-primary, #1f2329)",
387
+ borderRadius: "8px",
388
+ padding: "0 12px",
389
+ fontSize: "13px",
390
+ lineHeight: "1.5"
391
+ };
392
+ const selectStyle = {
393
+ ...inputStyle,
394
+ height: "34px",
395
+ appearance: "none",
396
+ WebkitAppearance: "none",
397
+ backgroundImage: "url(data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E)",
398
+ backgroundPosition: "right 12px center",
399
+ backgroundRepeat: "no-repeat",
400
+ backgroundSize: "12px 12px",
401
+ paddingRight: "32px"
402
+ };
403
+ const checkboxStyle = {
404
+ width: "18px",
405
+ height: "18px",
406
+ margin: "0",
407
+ accentColor: "var(--dsw-alias-brand-primary, #4b5bff)",
408
+ // 锁死:不被 flex 长描述挤压缩(flex-shrink 默认 1 会在布局阶段无视 width/!important)。
409
+ flexShrink: 0,
410
+ flexGrow: 0,
411
+ alignSelf: "center"
412
+ };
413
+ const toggleStyle = {
414
+ display: "flex",
415
+ alignItems: "center",
416
+ justifyContent: "space-between",
417
+ gap: "12px",
418
+ padding: "12px 0",
419
+ borderTop: "1px solid var(--dsw-alias-border-l2, #e5e7eb)"
420
+ };
421
+
422
+ const cardStyle = {
423
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
424
+ borderRadius: "12px",
425
+ padding: "14px 16px",
426
+ marginBottom: "12px",
427
+ background: "var(--dsw-alias-bg-layer-2, #ffffff)"
428
+ };
429
+ const groupTitleStyle = {
430
+ margin: "0 0 4px",
431
+ fontSize: "13px",
432
+ fontWeight: "500",
433
+ color: "var(--dsw-alias-label-primary, #1f2329)"
434
+ };
435
+ const sectionStyle = {
436
+ maxWidth: "760px",
437
+ color: "var(--dsw-alias-label-primary, #1f2329)",
438
+ display: "flex",
439
+ flexDirection: "column",
440
+ gap: "12px"
441
+ };
442
+ const headingStyle = { margin: 0, fontSize: "18px", fontWeight: "600" };
443
+ const introStyle = { color: "var(--dsw-alias-label-tertiary, #8a919f)", margin: 0, fontSize: "13px" };
444
+ const actionsStyle = { display: "flex", gap: "8px", alignItems: "center" };
445
+ const btnPrimary = {
446
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
447
+ background: "var(--dsw-alias-bg-module-platform, #f2f3f5)",
448
+ color: "var(--dsw-alias-label-primary, #1f2329)",
449
+ borderRadius: "8px",
450
+ padding: "6px 14px",
451
+ fontSize: "13px",
452
+ cursor: disabled ? "default" : "pointer",
453
+ opacity: disabled ? 0.5 : 1
454
+ };
455
+ const btnAccent = {
456
+ border: "1px solid var(--dsw-alias-brand-primary, #4b5bff)",
457
+ background: "var(--dsw-alias-brand-primary, #4b5bff)",
458
+ color: "#fff",
459
+ borderRadius: "8px",
460
+ padding: "6px 14px",
461
+ fontSize: "13px",
462
+ cursor: disabled ? "default" : "pointer",
463
+ opacity: disabled ? 0.5 : 1
464
+ };
465
+ const fieldStyle = {
466
+ display: "flex",
467
+ flexDirection: "column",
468
+ gap: "6px",
469
+ padding: "12px 0",
470
+ borderTop: "1px solid var(--dsw-alias-border-l2, #e5e7eb)"
471
+ };
472
+ const headStyle = { display: "flex", alignItems: "center", gap: "8px" };
473
+ const labelStyle = {
474
+ minWidth: "0",
475
+ color: "var(--dsw-alias-label-primary, #1f2329)",
476
+ flex: 1,
477
+ fontSize: "13px",
478
+ fontWeight: "500",
479
+ lineHeight: "1.5"
480
+ };
481
+ const badgeStyle = {
482
+ whiteSpace: "nowrap",
483
+ background: "var(--dsw-alias-bg-module-platform, #f2f3f5)",
484
+ color: "var(--dsw-alias-label-secondary, #6b7280)",
485
+ borderRadius: "999px",
486
+ padding: "1px 8px",
487
+ fontSize: "11px",
488
+ fontWeight: "500",
489
+ lineHeight: "17px"
490
+ };
491
+ const mutedBadgeStyle = {
492
+ whiteSpace: "nowrap",
493
+ color: "var(--dsw-alias-label-tertiary, #8a919f)",
494
+ borderRadius: "999px",
495
+ padding: "1px 8px",
496
+ fontSize: "11px",
497
+ lineHeight: "17px"
498
+ };
499
+ const hintStyle = { color: "var(--dsw-alias-label-tertiary, #8a919f)", margin: 0, fontSize: "12px", lineHeight: "1.5" };
500
+ const errStyle = { color: "var(--dsw-alias-label-error, #d54941)", margin: 0, fontSize: "12px", lineHeight: "1.5" };
501
+
502
+ const num = (field) => ({
503
+ type: "text",
504
+ inputMode: "numeric",
505
+ value: draft[field],
506
+ disabled,
507
+ style: { ...inputStyle, borderColor: validation === field ? "var(--dsw-alias-label-error, #d54941)" : undefined },
508
+ onChange: (e) => edit(field, e.target.value)
509
+ });
510
+
511
+ return h("div", { className: "memory-palace-settings", style: sectionStyle }, [
512
+ // 强制覆盖宿主 Webview 全局样式对原生 checkbox 尺寸的覆盖(内联 style 可能不敌全局 CSS)。
513
+ h("style", null, ".memory-palace-settings input[type=checkbox]{width:18px!important;height:18px!important;margin:0;flex-shrink:0!important;cursor:pointer;}"),
514
+ h("div", { style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "12px" } }, [
515
+ h("div", null, [
516
+ h("h3", { style: headingStyle }, t("title")),
517
+ h("p", { style: introStyle }, t("intro"))
518
+ ]),
519
+ h("div", { style: actionsStyle }, [
520
+ h("button", { type: "button", style: btnPrimary, disabled, onClick: discard }, t("discard")),
521
+ h("button", { type: "button", style: btnAccent, disabled, onClick: save }, saving ? t("saving") : t("save"))
522
+ ])
523
+ ]),
524
+ failed ? h("p", { style: errStyle }, t("saveFailed")) : null,
525
+ h("div", { style: cardStyle }, [
526
+ h("p", { style: groupTitleStyle }, t("core")),
527
+ h("div", { style: toggleStyle }, [
528
+ h("div", { style: { flexShrink: 1, minWidth: 0 } }, [
529
+ h("p", { style: { ...labelStyle, margin: 0 } }, t("bridgeLabel")),
530
+ h("p", { style: hintStyle }, t("bridgeHint"))
531
+ ]),
532
+ h("input", {
533
+ type: "checkbox",
534
+ checked: draft.bridgeBuddyMemory === "true",
535
+ disabled,
536
+ style: checkboxStyle,
537
+ onChange: (e) => edit("bridgeBuddyMemory", e.target.checked ? "true" : "false")
538
+ })
539
+ ]),
540
+ row(t("dirsLabel"), t("dirsHint"),
541
+ h("input", {
542
+ ...num("buddyWorkspaceMemoryDirs"),
543
+ type: "text",
544
+ placeholder: ".workbuddy/memory, .codebuddy/memory"
545
+ }), "buddyWorkspaceMemoryDirs")
546
+ ]),
547
+ h("div", { style: cardStyle }, [
548
+ h("p", { style: groupTitleStyle }, t("smartSummary")),
549
+ // 轮次结束自动记录(summarize):三种记忆模式的共用总闸门,始终显示,置于记忆模式之上、不随模式切换隐藏。
550
+ toggle(t("summarizeLabel"), t("summarizeHint"), "summarize"),
551
+ row(t("memoryModeLabel"),
552
+ draft.memoryMode === "smart"
553
+ ? t("memoryModeHintSmart")
554
+ : draft.memoryMode === "hybrid"
555
+ ? t("memoryModeHintHybrid")
556
+ : t("memoryModeHintPlugin"),
557
+ h("select", {
558
+ value: ["plugin", "smart", "hybrid"].includes(draft.memoryMode) ? draft.memoryMode : "plugin",
559
+ disabled,
560
+ onChange: (e) => edit("memoryMode", e.target.value),
561
+ style: selectStyle
562
+ }, [
563
+ h("option", { value: "plugin" }, "插件模式"),
564
+ h("option", { value: "smart" }, "智能模式"),
565
+ h("option", { value: "hybrid" }, "混合模式")
566
+ ]), "memoryMode"),
567
+ draft.memoryMode === "smart"
568
+ ? h("div", null, [
569
+ row(t("summaryModelLabel"), t("summaryModelHint"),
570
+ h("select", {
571
+ // 已存储值不在下拉选项内(如旧版双前缀脏数据)→ 归零为「复用当前会话模型」,
572
+ // 避免 select 显示悬空值、保存时又把脏值写回 settings.yaml。
573
+ value: modelOptions.some((o) => o.value === draft.summaryModel) ? draft.summaryModel : "",
574
+ disabled,
575
+ style: selectStyle,
576
+ onChange: (e) => edit("summaryModel", e.target.value)
577
+ }, [
578
+ h("option", { value: "" }, "复用当前会话模型"),
579
+ ...modelOptions.map((o) => h("option", { value: o.value }, o.label))
580
+ ]), "summaryModel"),
581
+ toggle(t("feedbackEnabledLabel"), t("feedbackEnabledHint"), "feedbackEnabled"),
582
+ row(t("summaryMaxTokensLabel"), t("summaryMaxTokensHint"), h("input", num("summaryMaxTokens")), "summaryMaxTokens"),
583
+ row(t("projectMaxTokensLabel"), t("projectMaxTokensHint"), h("input", num("projectMaxTokens")), "projectMaxTokens")
584
+ ])
585
+ : draft.memoryMode === "hybrid"
586
+ ? h("div", null, [
587
+ row(t("reorgCooldownLabel"), t("reorgCooldownHint"), h("input", num("reorgCooldownDays")), "reorgCooldownDays"),
588
+ row(t("subagentLogBudgetLabel"), t("subagentLogBudgetHint"), h("input", num("subagentLogBudget")), "subagentLogBudget")
589
+ ])
590
+ : h("div", null, [
591
+ toggle(t("autoCaptureErrorsLabel"), t("autoCaptureErrorsHint"), "autoCaptureErrors")
592
+ ])
593
+ ]),
594
+ h("div", { style: cardStyle }, [
595
+ h("p", { style: groupTitleStyle }, t("storage")),
596
+ row(t("userPathLabel"), t("userPathHint"), h("input", { ...num("userMemoryPath"), type: "text" }), "userMemoryPath"),
597
+ row(t("wsDirLabel"), t("wsDirHint"), h("input", { ...num("workspaceMemoryDir"), type: "text" }), "workspaceMemoryDir"),
598
+ row(t("retentionLabel"), t("retentionHint"), h("input", num("dailyLogRetentionDays")), "dailyLogRetentionDays"),
599
+ row(t("userBudgetLabel"), t("userBudgetHint"), h("input", num("userBudgetChars")), "userBudgetChars"),
600
+ row(t("wsBudgetLabel"), t("wsBudgetHint"), h("input", num("workspaceBudgetChars")), "workspaceBudgetChars")
601
+ ]),
602
+ h("div", { style: cardStyle }, [
603
+ h("p", { style: groupTitleStyle }, t("dev")),
604
+ row(t("distillTimeoutLabel"), t("distillTimeoutHint"), h("input", num("summaryTimeoutMs")), "summaryTimeoutMs"),
605
+ toggle(t("distillDebugLogLabel"), t("distillDebugLogHint"), "distillDebugLog")
606
+ ])
607
+ ]);
608
+ }
609
609
 
610
610
 
611
611
  // ---- v1.2.0 会话标题栏「记忆」按钮(蒸馏入口)----
@@ -867,34 +867,34 @@ window.__ModuleLoader__.load({
867
867
  }
868
868
 
869
869
 
870
- function apply(ctx) {
871
- ctx.effect(() => ctx.locale.register(NS, { zh, en }), "memory-palace: dictionaries");
872
- const t = ctx.locale.bind(NS);
873
- // v1.1.3:设置读写走自有 route(/memory-palace/api),不再 bind settingsScope——
874
- // 非 loopback 下 settingsScope persistence=memory(set() no-op),保存永不落盘。
875
- const controller = new MemoryPalaceController(ctx);
876
- // v1.6.0-rc1:枚举已配置模型改走 ctx.remote.session.modelCatalog()(ApiProxy 已删),
877
- // 枚举失败则降级为空列表。
878
- ctx.slots.inject("settings.section", () => ctx.slots.register({
879
- name: "settings.section",
880
- id: "memory-palace",
881
- order: 20,
882
- label: () => t("nav"),
883
- locale: NS,
884
- inject: () => controller.inject()
885
- }, (props) => MemoryPalaceSection({ ...props, fetchModels: () => fetchModelOptions(ctx) })));
886
- // v1.2.0:会话标题栏「记忆」按钮——headerUtilities 区(右对齐),order:-1 排在 Session log 左边。
887
- ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
888
- name: "conversation.session.header.utilities",
889
- id: "memory-palace-distill",
890
- order: -1,
891
- locale: NS
892
- }, DistillButton));
893
- }
894
-
895
- exports.apply = apply;
896
- // v1.6.0-rc1:connection → remote + remote.session(模型枚举走 ctx.remote.session.modelCatalog)。
897
- exports.inject = ["slots", "locale", "remote", "remote.session"];
898
- return module.exports;
899
- }
900
- });
870
+ function apply(ctx) {
871
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "memory-palace: dictionaries");
872
+ const t = ctx.locale.bind(NS);
873
+ // v1.1.3:设置读写走自有 route(/memory-palace/api),不再 bind settingsScope——
874
+ // 非 loopback 下 settingsScope persistence=memory(set() no-op),保存永不落盘。
875
+ const controller = new MemoryPalaceController(ctx);
876
+ // v1.6.0-rc1:枚举已配置模型改走 ctx.remote.session.modelCatalog()(ApiProxy 已删),
877
+ // 枚举失败则降级为空列表。
878
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
879
+ name: "settings.section",
880
+ id: "memory-palace",
881
+ order: 20,
882
+ label: () => t("nav"),
883
+ locale: NS,
884
+ inject: () => controller.inject()
885
+ }, (props) => MemoryPalaceSection({ ...props, fetchModels: () => fetchModelOptions(ctx) })));
886
+ // v1.2.0:会话标题栏「记忆」按钮——headerUtilities 区(右对齐),order:-1 排在 Session log 左边。
887
+ ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
888
+ name: "conversation.session.header.utilities",
889
+ id: "memory-palace-distill",
890
+ order: -1,
891
+ locale: NS
892
+ }, DistillButton));
893
+ }
894
+
895
+ exports.apply = apply;
896
+ // v1.6.0-rc1:connection → remote + remote.session(模型枚举走 ctx.remote.session.modelCatalog)。
897
+ exports.inject = ["slots", "locale", "remote", "remote.session"];
898
+ return module.exports;
899
+ }
900
+ });