dsh-dock 0.11.5 → 0.12.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.
@@ -4,7 +4,7 @@
4
4
  // - 秒级时间范围查询 + 会话ID/提供商/模型(联动)/状态/推理强度 筛选,条件本地暂存
5
5
  // - 9 张 KPI 卡 + 分组统计表 + 明细表(点击表头排序、会话ID点击即筛选、100 行/页上下双分页)
6
6
  // - 状态列显示 HTTP 状态码徽章,行内【查看详情】弹窗展示完整信息;CSV 导出(中文表头,列序与明细表一致 + 合计行)
7
- // - 独立的「单价设置」子弹窗:按模型配置单价(支持多段分时价)并持久化,费用按自填单价重算
7
+ // - 独立的「单价设置」子弹窗:按模型配置单价(多段分时价 + 日期类型/法定节假日)并持久化,费用按自填单价重算
8
8
  // - 挂载即扫描历史+按暂存条件查询;挂载期间每 5s 静默自动刷新
9
9
  // - 查询等待动画:点「查询」后按钮转圈扫光 + 数据区顶部「记账小队清点」banner(轮换俏皮文案,
10
10
  // 等久了换语气),旧数据压暗禁点;结果到位时闪一次「✓ 数据已更新」并让 KPI 卡回弹
@@ -396,7 +396,7 @@ const css = `
396
396
  .dtok-code.pend{color:var(--dsw-alias-label-tertiary,#94a3b8);}
397
397
  .dtok-detail-link{color:var(--dk-accent);cursor:pointer;font-size:11px;}
398
398
  .dtok-detail-link:hover{text-decoration:underline;}
399
- .dtok-detail{position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;}
399
+ .dtok-detail{position:fixed;inset:0;z-index:201;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;}
400
400
  .dtok-detail-card{background:var(--dsw-alias-bg-layer-2,#1c212b);border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:12px;padding:20px 24px;max-width:640px;width:92%;max-height:80vh;overflow:auto;color:var(--dsw-alias-label-primary,#e8eaf0);}
401
401
  .dtok-detail-card h3{margin:0 0 12px;font-size:15px;}
402
402
  .dtok-detail-row{display:flex;gap:8px;padding:4px 0;font-size:12px;border-bottom:1px solid var(--dk-tdim);}
@@ -460,7 +460,7 @@ const css = `
460
460
  .dtok-btn.primary:not([disabled]):active{transform:none;}
461
461
  }
462
462
  /* ---- 单价设置(子弹窗) ---- */
463
- .dtok-pm-backdrop{position:fixed;inset:0;z-index:2100;background:rgba(15,17,21,.5);backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;}
463
+ .dtok-pm-backdrop{position:fixed;inset:0;z-index:201;background:rgba(15,17,21,.5);backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;}
464
464
  .dtok-pm{box-sizing:border-box;width:min(980px,calc(100vw - 32px));height:min(760px,calc(100vh - 32px));display:flex;flex-direction:column;border-radius:14px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2,#1c212b);color:var(--dsw-alias-label-primary);box-shadow:0 20px 64px rgb(0 0 0 / .4);overflow:hidden;}
465
465
  .dtok-pm.max{border-radius:10px;}
466
466
  .dtok-pm-head{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);cursor:move;user-select:none;flex:none;background:var(--dsw-alias-bg-layer-1);}
@@ -488,6 +488,9 @@ const css = `
488
488
  .dtok-pseg{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:4px 0;}
489
489
  .dtok-pseg-name{font-size:11px;color:var(--dsw-alias-label-secondary);min-width:112px;}
490
490
  .dtok-pseg-tilde{font-size:11px;color:var(--dsw-alias-label-secondary);}
491
+ .dtok-price select{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 6px;font-size:12px;font-family:inherit;}
492
+ .dtok-price textarea{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:6px 8px;font-size:12px;font-family:inherit;min-height:64px;resize:vertical;width:100%;box-sizing:border-box;}
493
+ .dtok-days{font-size:11px;}
491
494
  .dtok-src{font-size:11px;color:var(--dsw-alias-label-tertiary,#94a3b8);}
492
495
  .dtok-src.custom{color:var(--dsw-alias-state-success-primary,#4ade80);}
493
496
  .dtok-src.fallback{color:var(--dk-warn);}
@@ -571,10 +574,12 @@ function Detail({ rec, onClose, rate }) {
571
574
  // 独立子弹窗(可最大化 / 拖动 / 缩放),与功能坞面板解耦,方便配置较长时段表。
572
575
  function numStr(v) { return v === undefined || v === null ? "" : String(v); }
573
576
 
574
- /** 时段草稿:一个 { start,end,input,output,cacheRead,cacheWrite } 的可编辑副本。 */
577
+ /** 时段草稿:一个 { start,end,days,input,output,cacheRead,cacheWrite } 的可编辑副本。 */
575
578
  function segToDraft(s) {
579
+ const d = s && s.days;
576
580
  return {
577
581
  start: numStr(s && s.start), end: numStr(s && s.end),
582
+ days: d === "workday" || d === "nonworkday" ? d : "all",
578
583
  input: numStr(s && s.input), output: numStr(s && s.output),
579
584
  cacheRead: numStr(s && s.cacheRead), cacheWrite: numStr(s && s.cacheWrite),
580
585
  };
@@ -641,6 +646,11 @@ function PriceModal({ onClose, onSaved }) {
641
646
  return () => window.removeEventListener("keydown", onKey);
642
647
  }, [onClose]);
643
648
 
649
+ // 遮罩关闭必须「按下也在遮罩上」。
650
+ // 从弹窗内按下、拖选/拖出后在遮罩松开时,click 会派发到共同祖先(遮罩),
651
+ // 若无此标记会把框选误判成点外部关闭(2026-09-23 用户反馈)。
652
+ const backdropDownRef = useRef(false);
653
+
644
654
  const style = maxed
645
655
  ? { position: "fixed", left: 8, top: 8, right: 8, bottom: 8, width: "auto", height: "auto" }
646
656
  : geom
@@ -651,7 +661,15 @@ function PriceModal({ onClose, onSaved }) {
651
661
  // 无 document(测试沙箱)/无 createPortal 时退回就地渲染,行为不变。
652
662
  const portalTarget = typeof document !== "undefined" && document.body ? document.body : null;
653
663
  const overlay = (
654
- <div className="dtok-pm-backdrop" onClick={onClose}>
664
+ <div
665
+ className="dtok-pm-backdrop"
666
+ onPointerDown={(e) => { backdropDownRef.current = e.target === e.currentTarget; }}
667
+ onClick={(e) => {
668
+ if (!backdropDownRef.current) return;
669
+ if (e.target !== e.currentTarget) return;
670
+ onClose();
671
+ }}
672
+ >
655
673
  <div ref={dlgRef} className={"dtok-pm" + (maxed ? " max" : "")} style={style} onClick={(e) => e.stopPropagation()}>
656
674
  <div className="dtok-pm-head" onPointerDown={(e) => startDrag(e, "move")} onDoubleClick={() => setMaxed((v) => !v)}>
657
675
  <b>单价设置</b>
@@ -679,24 +697,27 @@ function PricingEditor({ onClose, onSaved, embedded }) {
679
697
  const [rate, setRate] = useState("7.2");
680
698
  const [fetchOn, setFetchOn] = useState(true);
681
699
  const [fb, setFb] = useState({ input: "", output: "", cacheRead: "", cacheWrite: "" });
700
+ const [holidaysText, setHolidaysText] = useState("");
682
701
  const [newMatch, setNewMatch] = useState("");
683
702
  const [msg, setMsg] = useState(null);
684
703
  const [saving, setSaving] = useState(false);
685
704
 
705
+ const applyCfg = (d) => {
706
+ setCfg(d);
707
+ setRate(numStr(d.usdCnyRate));
708
+ setFetchOn(!!d.fetchOfficial);
709
+ setFb({
710
+ input: numStr(d.fallback && d.fallback.input), output: numStr(d.fallback && d.fallback.output),
711
+ cacheRead: numStr(d.fallback && d.fallback.cacheRead), cacheWrite: numStr(d.fallback && d.fallback.cacheWrite),
712
+ });
713
+ setHolidaysText((d.holidays || []).join("\n"));
714
+ setRows((d.pricing || []).map(rowToDraft));
715
+ };
716
+
686
717
  useEffect(() => {
687
718
  let cancel = false;
688
719
  rpcCall("pricing", {})
689
- .then((d) => {
690
- if (cancel) return;
691
- setCfg(d);
692
- setRate(numStr(d.usdCnyRate));
693
- setFetchOn(!!d.fetchOfficial);
694
- setFb({
695
- input: numStr(d.fallback && d.fallback.input), output: numStr(d.fallback && d.fallback.output),
696
- cacheRead: numStr(d.fallback && d.fallback.cacheRead), cacheWrite: numStr(d.fallback && d.fallback.cacheWrite),
697
- });
698
- setRows((d.pricing || []).map(rowToDraft));
699
- })
720
+ .then((d) => { if (!cancel) applyCfg(d); })
700
721
  .catch((e) => { if (!cancel) setMsg({ ok: false, text: String((e && e.message) || e) }); });
701
722
  return () => { cancel = true; };
702
723
  }, []);
@@ -728,12 +749,14 @@ function PricingEditor({ onClose, onSaved, embedded }) {
728
749
  };
729
750
  const peaks = (r.peaks || []).map((s) => ({
730
751
  start: Number(s.start || 0), end: Number(s.end || 0),
752
+ days: s.days === "workday" || s.days === "nonworkday" ? s.days : "all",
731
753
  input: Number(s.input || 0), output: Number(s.output || 0),
732
754
  cacheRead: Number(s.cacheRead || 0), cacheWrite: Number(s.cacheWrite || 0),
733
755
  }));
734
756
  if (peaks.length) out.peaks = peaks;
735
757
  return out;
736
758
  });
759
+ payload.holidays = holidaysText.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
737
760
  payload.fallback = {
738
761
  input: Number(fb.input || 0), output: Number(fb.output || 0),
739
762
  cacheRead: Number(fb.cacheRead || 0), cacheWrite: Number(fb.cacheWrite || 0),
@@ -741,8 +764,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
741
764
  setSaving(true); setMsg(null);
742
765
  rpcCall("setpricing", payload)
743
766
  .then((d) => {
744
- setCfg(d);
745
- setRows((d.pricing || []).map(rowToDraft));
767
+ applyCfg(d);
746
768
  setMsg({ ok: true, text: "单价已保存,费用已按新价即时重算" });
747
769
  if (onSaved) onSaved();
748
770
  })
@@ -765,9 +787,18 @@ function PricingEditor({ onClose, onSaved, embedded }) {
765
787
  );
766
788
  const segLabel = (s) => {
767
789
  const a = numStr(s.start), b = numStr(s.end);
768
- if (a === "" || b === "") return "时段";
769
- return a + "~" + b + (Number(a) === Number(b) ? "(全天)" : Number(a) > Number(b) ? "(跨零点)" : "");
790
+ const dayTag = s.days === "workday" ? "工作日" : s.days === "nonworkday" ? "周末/节假日" : "";
791
+ const time = a === "" || b === "" ? "时段"
792
+ : a + "~" + b + (Number(a) === Number(b) ? "(全天)" : Number(a) > Number(b) ? "(跨零点)" : "");
793
+ return dayTag ? dayTag + " " + time : time;
770
794
  };
795
+ const daysSelect = (value, onChange) => (
796
+ <select className="dtok-days" value={value || "all"} onChange={(e) => onChange(e.target.value)} title="该时段在哪类日期生效">
797
+ <option value="all">每天</option>
798
+ <option value="workday">仅工作日</option>
799
+ <option value="nonworkday">仅周末/节假日</option>
800
+ </select>
801
+ );
771
802
 
772
803
  return (
773
804
  <div className={embedded ? "dtok-price emb" : "dtok-price"} onClick={(e) => e.stopPropagation()}>
@@ -782,6 +813,20 @@ function PricingEditor({ onClose, onSaved, embedded }) {
782
813
  </label>
783
814
  </div>
784
815
 
816
+ <div>
817
+ <div className="dtok-section-title" style={{ margin: "0 0 4px" }}>法定节假日(供「仅工作日」分时段判定)</div>
818
+ <div className="dtok-price-field" style={{ maxWidth: 420 }}>
819
+ <label>每行一条:单日 2026-10-01,或区间 2026-10-01~2026-10-07(含两端)</label>
820
+ <textarea
821
+ value={holidaysText}
822
+ placeholder={"2026-10-01~2026-10-07\n2027-01-01"}
823
+ onChange={(e) => setHolidaysText(e.target.value)}
824
+ spellCheck={false}
825
+ />
826
+ </div>
827
+ <div className="dtok-price-hint">周末(周六/周日)与这些节假日视为非工作日;标了「仅工作日」的高峰段在这些日期回落基准价(谷价)。</div>
828
+ </div>
829
+
785
830
  <div>
786
831
  <div className="dtok-section-title" style={{ margin: "0 0 4px" }}>按模型配置单价({rows.length} 条)</div>
787
832
  {onFallback.length > 0 ? (
@@ -806,12 +851,14 @@ function PricingEditor({ onClose, onSaved, embedded }) {
806
851
  缓存写入=未命中缓存、本次新建缓存的那部分输入(Claude 等按溢价单独收建缓存费);DeepSeek 不收此项,填 0 即可。
807
852
  </div>
808
853
  <div className="dtok-price-hint" style={{ marginTop: 2 }}>
809
- 分时段价(可多段,命中哪段用哪段;时段外回落到基准价。start&gt;end 表示跨零点,如 23~7;start=end 表示全天)
854
+ 分时段价(可多段,命中哪段用哪段;时段外回落到基准价。start&gt;end 表示跨零点,如 23~7;start=end 表示全天。
855
+ 每段可选日期类型:每天 / 仅工作日(非周末且非下方节假日)/ 仅周末或节假日——如 DeepSeek「工作日 9~12、14~18 双倍,周末与节假日全天谷价」)
810
856
  </div>
811
857
  {r.peaks.length === 0 ? <div className="dtok-price-hint">(未设置分时段,按基准价计费)</div> : null}
812
858
  {r.peaks.map((s, k) => (
813
859
  <div className="dtok-pseg" key={k}>
814
860
  <span className="dtok-pseg-name">{segLabel(s)}</span>
861
+ {daysSelect(s.days, (v) => patchSeg(i, k, { days: v }))}
815
862
  {num(s.start, (v) => patchSeg(i, k, { start: v }), { width: 56 })}
816
863
  <span className="dtok-pseg-tilde">~</span>
817
864
  {num(s.end, (v) => patchSeg(i, k, { end: v }), { width: 56 })}
@@ -823,7 +870,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
823
870
  <button className="dtok-btn tiny" onClick={() => delSeg(i, k)}>删除时段</button>
824
871
  </div>
825
872
  ))}
826
- <div className="dtok-price-hint" style={{ opacity: .85 }}>时段价从左到右:输入(命中)/ 输入(未命中)/ 输出 / 缓存写入</div>
873
+ <div className="dtok-price-hint" style={{ opacity: .85 }}>时段价从左到右:日期类型 / 起~止时 / 输入(命中)/ 输入(未命中)/ 输出 / 缓存写入</div>
827
874
  <button className="dtok-btn tiny" onClick={() => addSeg(i)}>+ 添加时段</button>
828
875
  </div>
829
876
  ))}
@@ -858,7 +905,7 @@ function PricingEditor({ onClose, onSaved, embedded }) {
858
905
  {cfg.builtin.map((b) => (
859
906
  <tr key={b.match}>
860
907
  <td>{b.match}</td><td>{b.cacheRead}</td><td>{b.input}</td><td>{b.output}</td><td>{b.cacheWrite}</td>
861
- <td>{b.peaks && b.peaks.length ? b.peaks.map((s) => s.start + "~" + s.end).join("、") + "时" : "—"}</td>
908
+ <td>{b.peaks && b.peaks.length ? b.peaks.map((s) => (s.days === "workday" ? "工作日" : s.days === "nonworkday" ? "周末/节假" : "") + s.start + "~" + s.end).join("、") + "时" : "—"}</td>
862
909
  </tr>
863
910
  ))}
864
911
  </tbody>
@@ -1361,7 +1408,7 @@ export const feature = {
1361
1408
  name: "用量记录",
1362
1409
  order: 110,
1363
1410
  accent: "#fbbf24",
1364
- description: "记录全部 LLM API 调用:秒级时间筛选、Token/费用统计(可配置各模型单价,持久保存并按自填单价计费;内置峰谷计价+官网价目自动同步作兜底)、分组汇总、明细检索与 CSV 导出",
1411
+ description: "记录全部 LLM API 调用:秒级时间筛选、Token/费用统计(可配置各模型单价,持久保存并按自填单价计费;多段分时价可限定工作日/周末节假日,内置峰谷计价+官网价目自动同步作兜底)、分组汇总、明细检索与 CSV 导出",
1365
1412
  css,
1366
1413
  View: TokenLogView,
1367
1414
  HomeStat: TokenLogHomeStat,
@@ -7,7 +7,7 @@
7
7
  // 配置存自有 settings 命名空间 dsh-dock(visionProxy: enabled/provider/model),
8
8
  // 其编辑 UI 在模型设置页内(见 features/modelconfig/view.js)。
9
9
  import { BlockAssembler } from '@deepseek-ai/dsh-llm'
10
- import { DOCK_NS } from '../../src/host-core.js'
10
+ import { readDockRoot } from '../../src/host-core.js'
11
11
  import { readModelDirectory } from '../modelconfig/host.js'
12
12
 
13
13
  /** 递归判断内容块里是否有图片(含 tool-result 嵌套,与官方 contentHasImage 同构)。 */
@@ -223,8 +223,7 @@ function installVisionProxy(ctx, directory) {
223
223
  const deps = {
224
224
  config: () => {
225
225
  try {
226
- const settings = ctx.get('settings')
227
- const v = settings && typeof settings.get === 'function' ? settings.get(DOCK_NS) : undefined
226
+ const v = readDockRoot(ctx)
228
227
  return v && v.visionProxy ? v.visionProxy : { enabled: false, provider: '', model: '' }
229
228
  } catch {
230
229
  return { enabled: false, provider: '', model: '' }
package/index.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // - notify 任务通知:完成/异常/需确认通知 + 提示音/系统通知/钉钉飞书推送(从任务动画拆出)
17
17
  // - runstate 运行状态:进行中任务与最近完成一览(从任务动画拆出;只读,无配置段)
18
18
  // - mobile-relay 手机接力(未发布):扫码反向代理接力 + 局域网电脑直连(0.0.0.0)
19
- import { DOCK_NS, DockConfig, sendJson, readBody, migrateNotifyConfig, migrateModelsFeatureId } from './src/host-core.js'
19
+ import { DockConfig, sendJson, readBody, migrateNotifyConfig, migrateModelsFeatureId, migrateImportedFeatures, bindDockConfig, readDockRoot, mutateDockSection } from './src/host-core.js'
20
20
  import { feature as fModels } from './features/modelconfig/host.js'
21
21
  import { feature as fVisionProxy } from './features/visionproxy/host.js'
22
22
  import { feature as fBalance } from './features/balance/host.js'
@@ -28,7 +28,10 @@ import { feature as fMobileRelay } from './features/mobile-relay/host.js'
28
28
 
29
29
  export const name = 'dsh-dock'
30
30
 
31
- // 硬依赖 webServer(路由注册)+ llm(图片理解代理包装)+ settings(自有命名空间读写)。
31
+ /** Config schema:dsh ≥ 0.1.7-alpha.1 SettingsForms 要求插件导出同名 Config,否则 mutate 报 No configurable plugin entry。 */
32
+ export const Config = DockConfig
33
+
34
+ // 硬依赖 webServer(路由注册)+ llm(图片理解代理包装)+ settings(SettingsForms 写回)。
32
35
  export const inject = ['webServer', 'llm', 'settings']
33
36
 
34
37
  /** 测试钩子:清空识别缓存(冒烟在用例间隔离;实现在 visionproxy 模块)。 */
@@ -50,7 +53,7 @@ if (typeof process !== 'undefined' && typeof process.on === 'function' && !proce
50
53
  })
51
54
  }
52
55
 
53
- export function apply(ctx) {
56
+ export function apply(ctx, config) {
54
57
  // ---- 功能注册表(Host 侧):每个条目来自对应功能模块的 feature 描述符 ----
55
58
  // defaultEnabled:与 Client 半部保持一致。已接入的功能默认打开,
56
59
  // 规划中的功能缺省 false,等实现后移除 roadmap 并打开。
@@ -65,17 +68,22 @@ export function apply(ctx) {
65
68
  fMobileRelay,
66
69
  ]
67
70
 
71
+ // 绑定 Config 镜像(替代已移除的 settings.get);volatile 字段热更时重绑。
72
+ bindDockConfig(config)
73
+ ctx.on('loader/volatile-update', () => {
74
+ try { bindDockConfig(config) } catch { /* Config 形态异常时保持旧镜像 */ }
75
+ })
76
+
68
77
  const state = new Map()
69
78
  for (const f of FEATURES) state.set(f.id, { enabled: false, dispose: null, error: null })
70
79
 
71
- /** 读宿主侧功能开关表(settings 持久化;settings 未挂载时回退 defaultEnabled)。
72
- * 兼容别名:宿主侧【模型设置】功能 id 曾叫 models(与客户端 modelconfig 对不上,
73
- * 开关永远推不到宿主);迁移落盘前(异步)在这里读时兜底换算,保证启动开关表
74
- * 与 /dsh-dock/features 响应第一时间就是新口径。 */
80
+ // 读宿主侧功能开关表(本地 Config 镜像优先;settings 未挂载时回退 defaultEnabled)。
81
+ // 兼容别名:宿主侧【模型设置】功能 id 曾叫 models(与客户端 modelconfig 对不上,
82
+ // 开关永远推不到宿主);迁移落盘前(异步)在这里读时兜底换算,保证启动开关表
83
+ // 与 /dsh-dock/features 响应第一时间就是新口径。
75
84
  function persistedFeatureMap() {
76
85
  try {
77
- const settings = ctx.get('settings')
78
- const v = settings && typeof settings.get === 'function' ? settings.get(DOCK_NS) : null
86
+ const v = readDockRoot(ctx)
79
87
  if (v && typeof v === 'object' && v.features && typeof v.features === 'object') {
80
88
  const map = v.features
81
89
  if (typeof map.models === 'boolean' && typeof map.modelconfig !== 'boolean') {
@@ -115,25 +123,47 @@ export function apply(ctx) {
115
123
  }
116
124
  }
117
125
 
118
- // 自有 settings 命名空间(dsh-dock):功能开关与各功能配置的持久化;
119
- // 读取走 settings.get(内存 resolved 值,写入经 settings.mutate,均热生效)。
120
- // 宿主侧初始开关也在这里应用:settings 命名空间注册完成后才能读到持久化开关表
121
- // (apply() 同步执行时注册尚未生效,读到的永远是空值)。
126
+ // 自有 Config 段(profile 条目 id dsh-dock):功能开关与各功能配置的持久化。
127
+ // 读走本地镜像(apply 的 config / loader/volatile-update),写经 settings.mutate。
128
+ // 关键顺序:先跑完一次性迁移(可能改写 features),再应用初始开关——
129
+ // 否则首次启动时 bind 的还是空默认值,migrate 尚未落盘就 setEnabled,
130
+ // 面板显示 persisted=true 却全部「未启用」、路由 404/405(2026-09-23 事故)。
122
131
  let initialTogglesApplied = false
123
132
  ctx.inject(['settings'], (sctx) => {
124
- sctx.settings.register(DOCK_NS, DockConfig, {})
125
- // 一次性迁移:通知配置从 animation 段搬到 notify 段(【任务通知】独立成模块)。
126
- // 必须在任何面板保存动作之前跑——animation 模块保存时整段写回,旧字段会被覆盖丢失。
127
- migrateNotifyConfig(sctx)
128
- // 一次性迁移:宿主侧【模型设置】功能 id models → modelconfig(开关表键名对齐客户端)。
129
- migrateModelsFeatureId(sctx)
130
- if (initialTogglesApplied) return
131
- initialTogglesApplied = true
132
- const persisted = persistedFeatureMap()
133
- for (const f of FEATURES) {
134
- const saved = persisted ? persisted[f.id] : undefined
135
- setEnabled(f.id, typeof saved === 'boolean' ? saved : !!f.defaultEnabled)
136
- }
133
+ // 0.1.7-alpha.1 起 settings.register 已移除——不再调用;导出 Config 即注册。
134
+ Promise.resolve()
135
+ // 通知配置从 animation 段搬到 notify 段(【任务通知】独立成模块)。
136
+ // 必须在任何面板保存动作之前跑——animation 模块保存时整段写回,旧字段会被覆盖丢失。
137
+ .then(() => migrateNotifyConfig(sctx))
138
+ // 宿主侧【模型设置】功能 id models → modelconfig(开关表键名对齐客户端)。
139
+ .then(() => migrateModelsFeatureId(sctx))
140
+ // settings.yaml.imported 里停住的功能开关表(Config 导出前 import 失败的那段)。
141
+ .then(() => migrateImportedFeatures(sctx))
142
+ .then(() => {
143
+ // 迁移可能 mutate 过 Config:刷新镜像后再读开关表。
144
+ try { bindDockConfig(config) } catch { /* 保持旧镜像 */ }
145
+ if (initialTogglesApplied) return
146
+ initialTogglesApplied = true
147
+ const persisted = persistedFeatureMap()
148
+ for (const f of FEATURES) {
149
+ const saved = persisted ? persisted[f.id] : undefined
150
+ setEnabled(f.id, typeof saved === 'boolean' ? saved : !!f.defaultEnabled)
151
+ }
152
+ console.log('[dsh-dock] initial toggles applied:', FEATURES
153
+ .filter((f) => state.get(f.id).enabled)
154
+ .map((f) => f.id).join(', ') || '(none)')
155
+ })
156
+ .catch((e) => {
157
+ console.error('[dsh-dock] settings init/migration failed:', (e && e.message) || e)
158
+ // 迁移失败也要尝试按当前镜像启用,避免整坞空白。
159
+ if (initialTogglesApplied) return
160
+ initialTogglesApplied = true
161
+ const persisted = persistedFeatureMap()
162
+ for (const f of FEATURES) {
163
+ const saved = persisted ? persisted[f.id] : undefined
164
+ setEnabled(f.id, typeof saved === 'boolean' ? saved : !!f.defaultEnabled)
165
+ }
166
+ })
137
167
  })
138
168
 
139
169
  // 功能开关管理路由(无条件注册):GET 查询各功能宿主侧状态;POST 同步开关——
@@ -152,10 +182,11 @@ export function apply(ctx) {
152
182
  const enabled = !!(body && body.enabled)
153
183
  if (!state.has(id)) return sendJson(res, 404, { ok: false, error: { message: `未知功能:${id}` } })
154
184
  setEnabled(id, enabled)
155
- const settings = ctx.get('settings')
156
- if (settings && typeof settings.mutate === 'function') {
185
+ try {
157
186
  const features = Object.assign({}, persistedFeatureMap() || {}, { [id]: enabled })
158
- await settings.mutate(DOCK_NS, [{ op: 'set', path: ['features'], value: features }])
187
+ await mutateDockSection(ctx, ['features'], features)
188
+ } catch (e) {
189
+ console.error('[dsh-dock] feature toggle persist failed:', (e && e.message) || e)
159
190
  }
160
191
  }
161
192
  const persisted = persistedFeatureMap() || {}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-dock",
3
- "version": "0.11.5",
4
- "description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计,支持各模型自定义单价与多段分时价)、模型设置、图片理解代理、模型余额、任务动画(19 种运行动画 + 桌面伙伴 3D 场景)、任务通知(完成/异常/需确认通知、提示音、系统通知、钉钉/飞书推送)、运行状态(进行中任务与最近完成一览);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
3
+ "version": "0.12.0",
4
+ "description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计,支持各模型自定义单价、多段分时价与周末/节假日谷价)、模型设置、图片理解代理、模型余额、任务动画(19 种运行动画 + 桌面伙伴 3D 场景)、任务通知(完成/异常/需确认通知、提示音、系统通知、钉钉/飞书推送)、运行状态(进行中任务与最近完成一览);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -57,10 +57,12 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "js-yaml": "^4.1.0",
60
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
61
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
62
- "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
63
- "@deepseek-ai/schemastery": "^3.18.1",
64
60
  "qrcode": "^1.5.4"
61
+ },
62
+ "peerDependencies": {
63
+ "@deepseek-ai/dsh-credentials": "*",
64
+ "@deepseek-ai/dsh-llm": "*",
65
+ "@deepseek-ai/dsh-settings": "*",
66
+ "@deepseek-ai/schemastery": "*"
65
67
  }
66
- }
68
+ }
package/src/client.jsx CHANGED
@@ -30,7 +30,7 @@ import { feature as fGames } from "../features/games/view.jsx";
30
30
  import { feature as fMobileRelay } from "../features/mobile-relay/view.jsx";
31
31
 
32
32
  const name = "dsh-dock";
33
- const DOCK_VERSION = "0.11.3";
33
+ const DOCK_VERSION = "0.12.0";
34
34
 
35
35
  // ---- 内置功能注册表:新功能 = features/<id>/ 加模块 + 这里 import 一行 ----
36
36
  const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation, fNotify, fRunState, fGames, fMobileRelay];
@@ -238,6 +238,11 @@ function fullDockCss() {
238
238
  // 样式全局注入一次(入口按钮与右下角浮层挂在设置页之外,不能依赖页内 <style>)。
239
239
  // 这里用 SHELL_CSS 唯一命名,并做防御:注入永不抛错、失败只降级不打断启动;
240
240
  // 外部功能注册携带样式时可重算更新(外部样式后到)。
241
+ //
242
+ // ⚠️ 必须同时打 data-plugin="dsh-dock"(官方 claimStyles/removeOwnedStyles 归属协议):
243
+ // 只有 data-plugin-css 的无主 <style> 会被任意后 materialize 的插件 claim 走,
244
+ // 对方卸载时 removeOwnedStyles 会连带删掉功能坞样式——表现为「开关别的插件后样式坏了、
245
+ // 要手动刷新才正常」。见 packages/client/modules claimStyles。
241
246
  function ensureCss() {
242
247
  if (typeof document === "undefined") return;
243
248
  cancelInitialCssSchedule();
@@ -247,19 +252,21 @@ function ensureCss() {
247
252
  dockCssTag = document.querySelector('style[data-plugin-css="dsh-dock"]');
248
253
  } else {
249
254
  dockCssTag = document.createElement("style");
250
- dockCssTag.dataset.pluginCss = "dsh-dock";
251
255
  document.head.appendChild(dockCssTag);
252
256
  }
253
257
  }
258
+ // 归属与标识:缺 data-plugin 的旧标签/被 claim 走的标签一律改回本插件
259
+ dockCssTag.dataset.plugin = "dsh-dock";
260
+ dockCssTag.dataset.pluginCss = "dsh-dock";
254
261
  dockCssTag.textContent = fullDockCss();
255
262
  } catch (e) {
256
263
  console.error("[dsh-dock] ensureCss failed:", e && e.message ? e.message : String(e));
257
264
  }
258
265
  }
259
- // 刷新页时宿主内容优先绘制:功能坞样式很大,首帧同步解析会让整页短暂停顿。
260
- // 首次注入延后到浏览器空闲期;之后的外部模块注册仍走 ensureCss() 立即更新。
266
+ // 首次注入延后到浏览器空闲期:功能坞样式很大,首帧同步解析会让整页短暂停顿。
267
+ // 之后的外部模块注册仍走 ensureCss() 立即更新;apply() 另有同步一击(见下)。
261
268
  function scheduleInitialCss() {
262
- if (typeof document === "undefined" || initialCssSchedule || (dockCssTag && dockCssTag.isConnected)) return;
269
+ if (typeof document === "undefined" || initialCssSchedule || (dockCssTag && dockCssTag.isConnected && dockCssTag.dataset.plugin === "dsh-dock")) return;
263
270
  const run = () => {
264
271
  initialCssSchedule = null;
265
272
  ensureCss();
@@ -649,7 +656,10 @@ const ctxRef = { current: null };
649
656
 
650
657
  export function apply(ctx) {
651
658
  ctxRef.current = ctx;
652
- scheduleInitialCss();
659
+ // 同步注入归属完整的样式:插件开关/HMR 重进 apply 时若只靠 idle,
660
+ // 窗口期内其它插件 materialize 会 claim 走无主标签(或标签已被对方删掉)。
661
+ // 首屏成本可接受(textContent 写入),换「开合插件样式不坏」。
662
+ ensureCss();
653
663
  initFeatureState(BUILTIN_FEATURES.concat(PLANNED_FEATURES));
654
664
  const slots = ctx.get("slots");
655
665
  if (slots === undefined) return;