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.
- package/README.md +3 -3
- package/client.js +154 -121
- package/features/animation/host.js +5 -10
- package/features/animation/view.jsx +10 -10
- package/features/games/view.jsx +2 -2
- package/features/mobile-relay/host.js +13 -14
- package/features/modelconfig/host.js +4 -4
- package/features/notify/host.js +5 -10
- package/features/notify/view.jsx +1 -1
- package/features/tokenlog/host.js +103 -23
- package/features/tokenlog/view.jsx +71 -24
- package/features/visionproxy/host.js +2 -3
- package/index.js +60 -29
- package/package.json +9 -7
- package/src/client.jsx +16 -6
- package/src/host-core.js +155 -17
|
@@ -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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
769
|
-
|
|
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>end 表示跨零点,如 23~7;start=end
|
|
854
|
+
分时段价(可多段,命中哪段用哪段;时段外回落到基准价。start>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 }}
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
// 读宿主侧功能开关表(本地 Config 镜像优先;settings 未挂载时回退 defaultEnabled)。
|
|
81
|
+
// 兼容别名:宿主侧【模型设置】功能 id 曾叫 models(与客户端 modelconfig 对不上,
|
|
82
|
+
// 开关永远推不到宿主);迁移落盘前(异步)在这里读时兜底换算,保证启动开关表
|
|
83
|
+
// 与 /dsh-dock/features 响应第一时间就是新口径。
|
|
75
84
|
function persistedFeatureMap() {
|
|
76
85
|
try {
|
|
77
|
-
const
|
|
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
|
-
// 自有
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
156
|
-
if (settings && typeof settings.mutate === 'function') {
|
|
185
|
+
try {
|
|
157
186
|
const features = Object.assign({}, persistedFeatureMap() || {}, { [id]: enabled })
|
|
158
|
-
await
|
|
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.
|
|
4
|
-
"description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
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;
|