dsh-dock 0.11.3 → 0.11.6
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 +14 -1
- package/client.js +528 -194
- 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 +68 -17
- package/features/mobile-relay/view.jsx +103 -23
- 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 +11 -10
- package/features/tokenlog/view.jsx +268 -3
- package/features/visionproxy/host.js +2 -3
- package/index.js +60 -29
- package/package.json +8 -6
- package/screenshots/dock-tokenlog-charts-2.png +0 -0
- package/screenshots/dock-tokenlog-charts-3.png +0 -0
- package/screenshots/dock-tokenlog-charts.png +0 -0
- package/src/client.jsx +16 -6
- package/src/host-core.js +148 -16
package/features/notify/host.js
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
// systemNotify / soundNotify / soundEffect / dingtalkEnabled / dingtalkWebhook /
|
|
15
15
|
// feishuEnabled / feishuWebhook
|
|
16
16
|
// 模块启停由功能坞的 features.notify 开关负责(独立菜单项),本模块不再有第二个总开关。
|
|
17
|
-
import {
|
|
17
|
+
import { SOUND_EFFECTS, sendJson, readBody, readDockRoot, mutateDockSection } from '../../src/host-core.js'
|
|
18
18
|
import { acquireTaskTracker } from '../../src/task-track.js'
|
|
19
19
|
|
|
20
|
-
// 默认配置(schema 默认值一致;settings
|
|
20
|
+
// 默认配置(schema 默认值一致;settings 未挂载时的兜底)
|
|
21
21
|
function defaultConfig() {
|
|
22
22
|
return {
|
|
23
23
|
notifyOnComplete: true,
|
|
@@ -37,12 +37,11 @@ function defaultConfig() {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
//
|
|
40
|
+
// 读本地 Config 镜像里的 notify 配置(resolved 值已含 schema 默认),异常时回退默认
|
|
41
41
|
function readConfig(ctx) {
|
|
42
42
|
const cfg = defaultConfig()
|
|
43
43
|
try {
|
|
44
|
-
const
|
|
45
|
-
const v = settings && typeof settings.get === 'function' ? settings.get(DOCK_NS) : null
|
|
44
|
+
const v = readDockRoot(ctx)
|
|
46
45
|
const n = v && typeof v === 'object' && v.notify && typeof v.notify === 'object' ? v.notify : null
|
|
47
46
|
if (n) {
|
|
48
47
|
for (const key of Object.keys(cfg)) {
|
|
@@ -291,12 +290,8 @@ export const feature = {
|
|
|
291
290
|
cfg.feishuWebhook = hook
|
|
292
291
|
}
|
|
293
292
|
|
|
294
|
-
const settings = ctx.get('settings')
|
|
295
|
-
if (!settings || typeof settings.mutate !== 'function') {
|
|
296
|
-
throw new Error('settings 服务不可用,配置无法持久化')
|
|
297
|
-
}
|
|
298
293
|
try {
|
|
299
|
-
await
|
|
294
|
+
await mutateDockSection(ctx, ['notify'], cfg)
|
|
300
295
|
} catch (e) {
|
|
301
296
|
const err = new Error('保存配置被拒绝:' + ((e && e.message) || String(e)))
|
|
302
297
|
err.statusCode = 400
|
package/features/notify/view.jsx
CHANGED
|
@@ -685,7 +685,7 @@ function NotifyStat(props) {
|
|
|
685
685
|
// ---------- 样式(dknt- 前缀;全部走主题变量,暗/亮色自适应) ----------
|
|
686
686
|
const css = [
|
|
687
687
|
// 通知卡片栈(右上角,不遮 dsh 自身 UI)
|
|
688
|
-
".dknt-toasts{position:fixed;top:16px;right:16px;z-index:
|
|
688
|
+
".dknt-toasts{position:fixed;top:16px;right:16px;z-index:7;display:flex;flex-direction:column;gap:8px;width:min(380px,calc(100vw - 32px));}",
|
|
689
689
|
".dknt-toast{border-radius:12px;padding:12px 14px;pointer-events:auto;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 88%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 10px 36px rgb(0 0 0 / .22);animation:dknt-toast-in .32s var(--ds-ease-in-out);}",
|
|
690
690
|
".dknt-toast.out{animation:dknt-toast-out .24s var(--ds-ease-in-out) forwards;}",
|
|
691
691
|
"@keyframes dknt-toast-in{from{opacity:0;transform:translateX(14px)}to{opacity:1;transform:none}}",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// 定价优先级:用户自定义单价 > 官网抓取刊例价 > 内置默认表 > 兜底单价。
|
|
26
26
|
// 分时段价支持多段(start>end 跨零点、start=end 全天、小数小时表示半点);旧单段 peak 自动归一。
|
|
27
27
|
// 费用在查询/导出时按当前单价即时重算,因此改价无需重扫历史。
|
|
28
|
-
import {
|
|
28
|
+
import { sendJson, readBody, readDockRoot, mutateDockSection } from '../../src/host-core.js'
|
|
29
29
|
|
|
30
30
|
export const feature = {
|
|
31
31
|
id: 'tokenlog',
|
|
@@ -119,8 +119,7 @@ export const feature = {
|
|
|
119
119
|
// 取值做防御性归一(schema 已给默认, 但 settings 未挂载/异常时仍需兜底)。
|
|
120
120
|
function loadPricingConfig() {
|
|
121
121
|
try {
|
|
122
|
-
const
|
|
123
|
-
const root = settings && typeof settings.get === 'function' ? settings.get(DOCK_NS) : null
|
|
122
|
+
const root = readDockRoot(ctx)
|
|
124
123
|
const t = root && typeof root === 'object' && root.tokenlog && typeof root.tokenlog === 'object' ? root.tokenlog : null
|
|
125
124
|
if (!t) return
|
|
126
125
|
if (typeof t.usdCnyRate === 'number' && t.usdCnyRate > 0) usdCnyRate = t.usdCnyRate
|
|
@@ -247,7 +246,7 @@ export const feature = {
|
|
|
247
246
|
// 的字段) → 落盘。保存后由调用方 loadPricingConfig() 刷新内存, 使改价立即影响费用估算。
|
|
248
247
|
async function savePricingConfig(input) {
|
|
249
248
|
const settings = ctx.get('settings')
|
|
250
|
-
if (!settings || typeof settings.
|
|
249
|
+
if (!settings || typeof settings.mutate !== 'function') {
|
|
251
250
|
throw new Error('settings 服务不可用,单价无法持久化')
|
|
252
251
|
}
|
|
253
252
|
const bad = (msg) => { const err = new Error(msg); err.statusCode = 400; return err }
|
|
@@ -256,7 +255,7 @@ export const feature = {
|
|
|
256
255
|
if (!Number.isFinite(n) || n < 0) throw bad(`「${label}」需为不小于 0 的数字`)
|
|
257
256
|
return n
|
|
258
257
|
}
|
|
259
|
-
const root =
|
|
258
|
+
const root = readDockRoot(ctx)
|
|
260
259
|
const current = root && typeof root === 'object' && root.tokenlog && typeof root.tokenlog === 'object' ? root.tokenlog : {}
|
|
261
260
|
const next = Object.assign({}, current)
|
|
262
261
|
if (input.usdCnyRate !== undefined) {
|
|
@@ -314,7 +313,7 @@ export const feature = {
|
|
|
314
313
|
}
|
|
315
314
|
}
|
|
316
315
|
try {
|
|
317
|
-
await
|
|
316
|
+
await mutateDockSection(ctx, ['tokenlog'], next)
|
|
318
317
|
} catch (e) {
|
|
319
318
|
const err = new Error('保存单价被拒绝:' + ((e && e.message) || String(e)))
|
|
320
319
|
err.statusCode = 400
|
|
@@ -481,17 +480,19 @@ export const feature = {
|
|
|
481
480
|
}
|
|
482
481
|
}
|
|
483
482
|
|
|
484
|
-
//
|
|
483
|
+
// 读官方 llm-pi-ai 配置的「现有提供商」路由名 -> [模型 id]。
|
|
485
484
|
// 用途: 前端提供商/模型下拉 = 现有配置 ∪ 历史记录, 去重合并 ——
|
|
486
485
|
// - 没有任何记录时仍可选现有提供商;
|
|
487
486
|
// - 提供商/模型已从配置删除时, 因有历史记录仍可选中(兼容两者)。
|
|
487
|
+
// 0.1.7-alpha.1 起 settings.get 已移除:改走 describe()(SettingsForms 读路径)。
|
|
488
488
|
function readConfiguredProviders() {
|
|
489
489
|
const out = {}
|
|
490
490
|
try {
|
|
491
491
|
const settings = ctx.get('settings')
|
|
492
|
-
if (!settings || typeof settings.
|
|
493
|
-
const
|
|
494
|
-
const
|
|
492
|
+
if (!settings || typeof settings.describe !== 'function') return out
|
|
493
|
+
const views = settings.describe({ redactSecrets: true }) || []
|
|
494
|
+
const hit = views.find((d) => d && d.ns === 'llm-pi-ai' && d.value && typeof d.value === 'object')
|
|
495
|
+
const providers = hit && hit.value.providers ? hit.value.providers : {}
|
|
495
496
|
for (const pid of Object.keys(providers || {})) {
|
|
496
497
|
const p = providers[pid]
|
|
497
498
|
if (!p || typeof p !== 'object') { out[pid] = []; continue }
|
|
@@ -64,6 +64,19 @@ function fmtCostCnyExact(usd, rate) {
|
|
|
64
64
|
const v = (Number(usd) || 0) * (Number(rate) > 0 ? Number(rate) : 7.2);
|
|
65
65
|
return "¥" + v.toFixed(4);
|
|
66
66
|
}
|
|
67
|
+
function fmtDurCompact(ms) {
|
|
68
|
+
if (ms === null || ms === undefined || isNaN(ms)) return "—";
|
|
69
|
+
if (ms < 1000) return Math.round(ms) + "ms";
|
|
70
|
+
if (ms < 60000) return (ms / 1000).toFixed(1) + "s";
|
|
71
|
+
if (ms < 3600000) return Math.round(ms / 60000) + "m";
|
|
72
|
+
return (ms / 3600000).toFixed(1) + "h";
|
|
73
|
+
}
|
|
74
|
+
function startOfToday() {
|
|
75
|
+
const d = new Date();
|
|
76
|
+
d.setHours(0, 0, 0, 0);
|
|
77
|
+
return d.getTime();
|
|
78
|
+
}
|
|
79
|
+
function hourOf(ts) { return new Date(ts).getHours(); }
|
|
67
80
|
|
|
68
81
|
// ---------- CSV 导出 ----------
|
|
69
82
|
// 表头即「调用明细」表的中文列名, 列序/取值格式与页面完全一致(日期时间、千分位、命中%、¥xx.xxxx、
|
|
@@ -152,6 +165,201 @@ function statusInfo(r) {
|
|
|
152
165
|
return { code: 0, label: "…", cls: "pend", title: "进行中" };
|
|
153
166
|
}
|
|
154
167
|
|
|
168
|
+
// ---------- 图表统计(纯 CSS/SVG,无第三方图表库:视图由外壳直接渲染,依赖越少越稳) ----------
|
|
169
|
+
// 调色板按索引取色,循环使用;按值降序分配,颜色与排名绑定而不是与名字绑定。
|
|
170
|
+
const CHART_COLORS = ["#60a5fa", "#fbbf24", "#4ade80", "#f472b6", "#a78bfa", "#fb923c", "#2dd4bf", "#e879f9", "#94a3b8", "#f87171"];
|
|
171
|
+
function chartColor(i) { return CHART_COLORS[i % CHART_COLORS.length]; }
|
|
172
|
+
const CHART_METRICS = [
|
|
173
|
+
["totalTokens", "总 Token", (v) => fmtCompact(v)],
|
|
174
|
+
["calls", "调用次数", (v) => fmtNum(v)],
|
|
175
|
+
["cost", "金额", (v, rate) => fmtCostCny(v, rate)],
|
|
176
|
+
["outputTokens", "输出 Token", (v) => fmtCompact(v)],
|
|
177
|
+
];
|
|
178
|
+
const CHART_DIMS = [
|
|
179
|
+
["model", "模型"],
|
|
180
|
+
["provider", "提供商"],
|
|
181
|
+
["status", "状态"],
|
|
182
|
+
["effort", "推理强度"],
|
|
183
|
+
];
|
|
184
|
+
function emptyAgg() {
|
|
185
|
+
return { calls: 0, totalTokens: 0, outputTokens: 0, inputTokens: 0, cacheReadTokens: 0, reasoningTokens: 0, cost: 0, llmMs: 0, timed: 0 };
|
|
186
|
+
}
|
|
187
|
+
function addToAgg(a, r) {
|
|
188
|
+
a.calls += 1;
|
|
189
|
+
a.totalTokens += Number(r.totalTokens) || 0;
|
|
190
|
+
a.outputTokens += Number(r.outputTokens) || 0;
|
|
191
|
+
a.inputTokens += Number(r.inputTokens) || 0;
|
|
192
|
+
a.cacheReadTokens += Number(r.cacheReadTokens) || 0;
|
|
193
|
+
a.reasoningTokens += Number(r.reasoningTokens) || 0;
|
|
194
|
+
a.cost += Number(r.cost) || 0;
|
|
195
|
+
if (typeof r.llmMs === "number") { a.llmMs += r.llmMs; a.timed += 1; }
|
|
196
|
+
}
|
|
197
|
+
/** records → [{key, ...agg}],按 metric 降序。 */
|
|
198
|
+
function groupByKey(records, keyFn, metric) {
|
|
199
|
+
const map = new Map();
|
|
200
|
+
for (const r of records) {
|
|
201
|
+
const k = keyFn(r);
|
|
202
|
+
let a = map.get(k);
|
|
203
|
+
if (!a) { a = emptyAgg(); a.key = k; map.set(k, a); }
|
|
204
|
+
addToAgg(a, r);
|
|
205
|
+
}
|
|
206
|
+
return [...map.values()].sort((a, b) => (b[metric] || 0) - (a[metric] || 0));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 环形占比图:SVG conic 扇区 + 中央合计 + 图例(最多 8 项,其余并入「其他」)。 */
|
|
210
|
+
function DonutChart({ items, metric, metricFmt, rate, centerLabel, emptyText }) {
|
|
211
|
+
const total = items.reduce((s, it) => s + (Number(it[metric]) || 0), 0);
|
|
212
|
+
if (!items.length || total <= 0) return <div className="dtok-chart-empty">{emptyText || "暂无数据"}</div>;
|
|
213
|
+
const shown = items.slice(0, 8);
|
|
214
|
+
const restSum = items.slice(8).reduce((s, it) => s + (Number(it[metric]) || 0), 0);
|
|
215
|
+
if (restSum > 0) {
|
|
216
|
+
const restAgg = Object.assign(emptyAgg(), { key: "其他(" + (items.length - 8) + "项)" });
|
|
217
|
+
restAgg[metric] = restSum;
|
|
218
|
+
shown.push(restAgg);
|
|
219
|
+
}
|
|
220
|
+
const R = 15.9155; // 周长 100 的半径
|
|
221
|
+
let acc = 0;
|
|
222
|
+
const segs = shown.map((it, i) => {
|
|
223
|
+
const v = Number(it[metric]) || 0;
|
|
224
|
+
const frac = v / total;
|
|
225
|
+
const seg = (
|
|
226
|
+
<circle key={it.key} cx="21" cy="21" r={R} fill="none"
|
|
227
|
+
stroke={chartColor(i)} strokeWidth={frac > 0.02 ? 7 : 5}
|
|
228
|
+
strokeDasharray={(frac * 100) + " " + (100 - frac * 100)}
|
|
229
|
+
strokeDashoffset={String(25 - acc * 100)}>
|
|
230
|
+
<title>{it.key + ":" + metricFmt(v, rate) + "(" + (frac * 100).toFixed(1) + "%)"}</title>
|
|
231
|
+
</circle>
|
|
232
|
+
);
|
|
233
|
+
acc += frac;
|
|
234
|
+
return seg;
|
|
235
|
+
});
|
|
236
|
+
return (
|
|
237
|
+
<div className="dtok-donut-wrap">
|
|
238
|
+
<svg className="dtok-donut" viewBox="0 0 42 42" role="img">
|
|
239
|
+
<circle cx="21" cy="21" r={R} fill="none" stroke="var(--dsw-alias-border-l1)" strokeWidth="7" />
|
|
240
|
+
{segs}
|
|
241
|
+
<text x="21" y="19.5" textAnchor="middle" className="dtok-donut-v">{metricFmt(total, rate)}</text>
|
|
242
|
+
<text x="21" y="26" textAnchor="middle" className="dtok-donut-l">{centerLabel}</text>
|
|
243
|
+
</svg>
|
|
244
|
+
<div className="dtok-legend">
|
|
245
|
+
{shown.map((it, i) => {
|
|
246
|
+
const v = Number(it[metric]) || 0;
|
|
247
|
+
return (
|
|
248
|
+
<div className="dtok-legend-row" key={it.key} title={it.key}>
|
|
249
|
+
<span className="dtok-dot" style={{ background: chartColor(i) }} />
|
|
250
|
+
<span className="dtok-legend-k">{it.key}</span>
|
|
251
|
+
<span className="dtok-legend-v">{metricFmt(v, rate)} · {(v / total * 100).toFixed(1)}%</span>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
})}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 横向条形图:行 = 分组(默认模型),条长 = 指标值(默认耗时)。 */
|
|
261
|
+
function HBarChart({ items, metric, metricFmt, rate, maxRows, emptyText }) {
|
|
262
|
+
if (!items.length) return <div className="dtok-chart-empty">{emptyText || "暂无数据"}</div>;
|
|
263
|
+
const shown = items.slice(0, maxRows || 12);
|
|
264
|
+
const max = Math.max(...shown.map((it) => Number(it[metric]) || 0), 1e-9);
|
|
265
|
+
return (
|
|
266
|
+
<div className="dtok-hbars">
|
|
267
|
+
{shown.map((it, i) => {
|
|
268
|
+
const v = Number(it[metric]) || 0;
|
|
269
|
+
const pct = Math.max(1.5, (v / max) * 100);
|
|
270
|
+
return (
|
|
271
|
+
<div className="dtok-hbar-row" key={it.key} title={it.key + ":" + metricFmt(v, rate)}>
|
|
272
|
+
<span className="dtok-hbar-k">{it.key}</span>
|
|
273
|
+
<span className="dtok-hbar-track">
|
|
274
|
+
<span className="dtok-hbar-fill" style={{ width: pct + "%", background: chartColor(i) }} />
|
|
275
|
+
</span>
|
|
276
|
+
<span className="dtok-hbar-v">{metricFmt(v, rate)}</span>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
})}
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** 24 小时调用分布柱状图:柱高 = 指标值,hover 显示具体数值。 */
|
|
285
|
+
function HourChart({ hours, metric, metricFmt, rate, emptyText }) {
|
|
286
|
+
const total = hours.reduce((s, h) => s + (Number(h[metric]) || 0), 0);
|
|
287
|
+
if (total <= 0) return <div className="dtok-chart-empty">{emptyText || "暂无数据"}</div>;
|
|
288
|
+
const max = Math.max(...hours.map((h) => Number(h[metric]) || 0), 1e-9);
|
|
289
|
+
return (
|
|
290
|
+
<div className="dtok-hours" role="img">
|
|
291
|
+
{hours.map((h) => {
|
|
292
|
+
const v = Number(h[metric]) || 0;
|
|
293
|
+
const pct = v > 0 ? Math.max(2, (v / max) * 100) : 0;
|
|
294
|
+
return (
|
|
295
|
+
<span key={h.hour} className="dtok-hour-col" title={h.hour + " 时:" + metricFmt(v, rate)}>
|
|
296
|
+
{pct > 0 ? <span className="dtok-hour-bar" style={{ height: pct + "%" }} /> : null}
|
|
297
|
+
</span>
|
|
298
|
+
);
|
|
299
|
+
})}
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** 图表统计区:所有图都基于「当前筛选结果 records」客户端聚合,筛选一变图表即时联动。
|
|
305
|
+
* scope = "range"(整个时间范围)| "today"(records 里属于今天的部分)。 */
|
|
306
|
+
function ChartsPanel({ records, rate }) {
|
|
307
|
+
const [metric, setMetric] = useState("totalTokens");
|
|
308
|
+
const [dim, setDimLocal] = useState("model");
|
|
309
|
+
const metricDef = CHART_METRICS.find((m) => m[0] === metric) || CHART_METRICS[0];
|
|
310
|
+
const metricFmt = metricDef[2];
|
|
311
|
+
const todayRecs = useMemo(() => {
|
|
312
|
+
const t0 = startOfToday();
|
|
313
|
+
return records.filter((r) => Number(r.time) >= t0);
|
|
314
|
+
}, [records]);
|
|
315
|
+
const todayByModel = useMemo(() => groupByKey(todayRecs, (r) => r.model || "(未知)", metric), [todayRecs, metric]);
|
|
316
|
+
const byDim = useMemo(() => groupByKey(records, (r) => r[dim] || "(空)", metric), [records, dim, metric]);
|
|
317
|
+
const modelTime = useMemo(() => groupByKey(records, (r) => r.model || "(未知)", "llmMs"), [records]);
|
|
318
|
+
const timedCalls = useMemo(() => records.reduce((s, r) => s + (typeof r.llmMs === "number" ? 1 : 0), 0), [records]);
|
|
319
|
+
const hours = useMemo(() => {
|
|
320
|
+
const arr = [];
|
|
321
|
+
for (let h = 0; h < 24; h++) { const a = emptyAgg(); a.hour = h; arr.push(a); }
|
|
322
|
+
for (const r of records) addToAgg(arr[hourOf(r.time)], r);
|
|
323
|
+
return arr;
|
|
324
|
+
}, [records]);
|
|
325
|
+
return (
|
|
326
|
+
<div className="dtok-charts">
|
|
327
|
+
<div className="dtok-chart-toolbar">
|
|
328
|
+
<span className="dtok-chart-toolbar-label">指标</span>
|
|
329
|
+
{CHART_METRICS.map(([id, label]) => (
|
|
330
|
+
<button key={id} className={"dtok-btn tiny" + (metric === id ? " primary" : "")} onClick={() => setMetric(id)}>{label}</button>
|
|
331
|
+
))}
|
|
332
|
+
<span className="dtok-chart-toolbar-note">图表跟随上方筛选条件(时间 / 会话 / 提供商 / 模型 / 状态 / 强度)实时联动</span>
|
|
333
|
+
</div>
|
|
334
|
+
<div className="dtok-chart-grid">
|
|
335
|
+
<div className="dtok-chart-card">
|
|
336
|
+
<div className="dtok-chart-title">今日模型用量占比<span className="dtok-chart-sub">{metricDef[1]} · {todayRecs.length} 次调用</span></div>
|
|
337
|
+
<DonutChart items={todayByModel} metric={metric} metricFmt={metricFmt} rate={rate} centerLabel={"今日" + metricDef[1]} emptyText="今日暂无调用" />
|
|
338
|
+
</div>
|
|
339
|
+
<div className="dtok-chart-card">
|
|
340
|
+
<div className="dtok-chart-title">24 小时调用分布<span className="dtok-chart-sub">按小时聚合 · {metricDef[1]}</span></div>
|
|
341
|
+
<HourChart hours={hours} metric={metric} metricFmt={metricFmt} rate={rate} />
|
|
342
|
+
</div>
|
|
343
|
+
<div className="dtok-chart-card">
|
|
344
|
+
<div className="dtok-chart-title">模型耗时排行<span className="dtok-chart-sub">累计 LLM 耗时 · 已计时 {timedCalls} 次</span></div>
|
|
345
|
+
<HBarChart items={modelTime} metric="llmMs" metricFmt={(v) => fmtDurCompact(v)} rate={rate} emptyText="暂无计时数据" />
|
|
346
|
+
</div>
|
|
347
|
+
<div className="dtok-chart-card">
|
|
348
|
+
<div className="dtok-chart-title">
|
|
349
|
+
分布统计
|
|
350
|
+
<span className="dtok-chart-sub">
|
|
351
|
+
{CHART_DIMS.map(([id, label]) => (
|
|
352
|
+
<button key={id} className={"dtok-btn tiny" + (dim === id ? " primary" : "")} style={{ marginLeft: 4 }} onClick={() => setDimLocal(id)}>{label}</button>
|
|
353
|
+
))}
|
|
354
|
+
</span>
|
|
355
|
+
</div>
|
|
356
|
+
<HBarChart items={byDim} metric={metric} metricFmt={metricFmt} rate={rate} />
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
155
363
|
// ---------- 样式(dtok- 前缀,dock 面板内自适应:宽度铺满、表格横向滚动) ----------
|
|
156
364
|
const css = `
|
|
157
365
|
.dtok-root{display:flex;flex-direction:column;gap:8px;width:100%;min-width:0;}
|
|
@@ -188,7 +396,7 @@ const css = `
|
|
|
188
396
|
.dtok-code.pend{color:var(--dsw-alias-label-tertiary,#94a3b8);}
|
|
189
397
|
.dtok-detail-link{color:var(--dk-accent);cursor:pointer;font-size:11px;}
|
|
190
398
|
.dtok-detail-link:hover{text-decoration:underline;}
|
|
191
|
-
.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;}
|
|
192
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);}
|
|
193
401
|
.dtok-detail-card h3{margin:0 0 12px;font-size:15px;}
|
|
194
402
|
.dtok-detail-row{display:flex;gap:8px;padding:4px 0;font-size:12px;border-bottom:1px solid var(--dk-tdim);}
|
|
@@ -252,7 +460,7 @@ const css = `
|
|
|
252
460
|
.dtok-btn.primary:not([disabled]):active{transform:none;}
|
|
253
461
|
}
|
|
254
462
|
/* ---- 单价设置(子弹窗) ---- */
|
|
255
|
-
.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;}
|
|
256
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;}
|
|
257
465
|
.dtok-pm.max{border-radius:10px;}
|
|
258
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);}
|
|
@@ -283,6 +491,35 @@ const css = `
|
|
|
283
491
|
.dtok-src{font-size:11px;color:var(--dsw-alias-label-tertiary,#94a3b8);}
|
|
284
492
|
.dtok-src.custom{color:var(--dsw-alias-state-success-primary,#4ade80);}
|
|
285
493
|
.dtok-src.fallback{color:var(--dk-warn);}
|
|
494
|
+
/* ---- 图表统计(纯 CSS/SVG) ---- */
|
|
495
|
+
.dtok-charts{display:flex;flex-direction:column;gap:10px;min-width:0;}
|
|
496
|
+
.dtok-chart-toolbar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:6px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);}
|
|
497
|
+
.dtok-chart-toolbar-label{font-size:12px;color:var(--dsw-alias-label-secondary);flex:none;}
|
|
498
|
+
.dtok-chart-toolbar-note{font-size:11px;color:var(--dsw-alias-label-tertiary,#94a3b8);margin-left:auto;}
|
|
499
|
+
.dtok-chart-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:10px;}
|
|
500
|
+
.dtok-chart-card{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);padding:10px 12px;min-width:0;display:flex;flex-direction:column;gap:8px;}
|
|
501
|
+
.dtok-chart-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
|
|
502
|
+
.dtok-chart-sub{font-size:11px;font-weight:400;color:var(--dsw-alias-label-secondary);}
|
|
503
|
+
.dtok-chart-empty{text-align:center;color:var(--dsw-alias-label-secondary);padding:28px 0;font-size:12px;}
|
|
504
|
+
.dtok-donut-wrap{display:flex;align-items:center;gap:14px;flex-wrap:wrap;}
|
|
505
|
+
.dtok-donut{width:150px;height:150px;flex:none;}
|
|
506
|
+
.dtok-donut-v{font-size:6.5px;font-weight:700;fill:var(--dsw-alias-label-primary);}
|
|
507
|
+
.dtok-donut-l{font-size:3.6px;fill:var(--dsw-alias-label-secondary);}
|
|
508
|
+
.dtok-legend{flex:1;min-width:150px;display:flex;flex-direction:column;gap:3px;max-height:170px;overflow:auto;}
|
|
509
|
+
.dtok-legend-row{display:flex;align-items:center;gap:6px;font-size:11px;min-width:0;}
|
|
510
|
+
.dtok-dot{width:8px;height:8px;border-radius:50%;flex:none;}
|
|
511
|
+
.dtok-legend-k{color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}
|
|
512
|
+
.dtok-legend-v{color:var(--dsw-alias-label-secondary);margin-left:auto;flex:none;}
|
|
513
|
+
.dtok-hbars{display:flex;flex-direction:column;gap:4px;max-height:220px;overflow:auto;}
|
|
514
|
+
.dtok-hbar-row{display:flex;align-items:center;gap:8px;font-size:11px;min-width:0;}
|
|
515
|
+
.dtok-hbar-k{flex:0 1 34%;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
516
|
+
.dtok-hbar-track{flex:1;height:12px;border-radius:4px;background:rgb(127 127 127 / .16);overflow:hidden;min-width:40px;}
|
|
517
|
+
.dtok-hbar-fill{display:block;height:100%;border-radius:4px;transition:width .3s ease;}
|
|
518
|
+
.dtok-hbar-v{flex:none;color:var(--dsw-alias-label-secondary);min-width:52px;text-align:right;}
|
|
519
|
+
.dtok-hours{display:flex;align-items:flex-end;gap:3px;height:130px;padding-top:4px;}
|
|
520
|
+
.dtok-hour-col{flex:1;display:flex;align-items:flex-end;height:100%;min-width:0;}
|
|
521
|
+
.dtok-hour-bar{display:block;width:100%;border-radius:3px 3px 0 0;background:linear-gradient(180deg,var(--dk-accent),rgb(127 127 127 / .25));min-height:2px;transition:height .3s ease;}
|
|
522
|
+
.dtok-hour-col:hover .dtok-hour-bar{filter:brightness(1.3);}
|
|
286
523
|
`;
|
|
287
524
|
|
|
288
525
|
// ---------- 详情弹窗 ----------
|
|
@@ -404,6 +641,11 @@ function PriceModal({ onClose, onSaved }) {
|
|
|
404
641
|
return () => window.removeEventListener("keydown", onKey);
|
|
405
642
|
}, [onClose]);
|
|
406
643
|
|
|
644
|
+
// 遮罩关闭必须「按下也在遮罩上」。
|
|
645
|
+
// 从弹窗内按下、拖选/拖出后在遮罩松开时,click 会派发到共同祖先(遮罩),
|
|
646
|
+
// 若无此标记会把框选误判成点外部关闭(2026-09-23 用户反馈)。
|
|
647
|
+
const backdropDownRef = useRef(false);
|
|
648
|
+
|
|
407
649
|
const style = maxed
|
|
408
650
|
? { position: "fixed", left: 8, top: 8, right: 8, bottom: 8, width: "auto", height: "auto" }
|
|
409
651
|
: geom
|
|
@@ -414,7 +656,15 @@ function PriceModal({ onClose, onSaved }) {
|
|
|
414
656
|
// 无 document(测试沙箱)/无 createPortal 时退回就地渲染,行为不变。
|
|
415
657
|
const portalTarget = typeof document !== "undefined" && document.body ? document.body : null;
|
|
416
658
|
const overlay = (
|
|
417
|
-
<div
|
|
659
|
+
<div
|
|
660
|
+
className="dtok-pm-backdrop"
|
|
661
|
+
onPointerDown={(e) => { backdropDownRef.current = e.target === e.currentTarget; }}
|
|
662
|
+
onClick={(e) => {
|
|
663
|
+
if (!backdropDownRef.current) return;
|
|
664
|
+
if (e.target !== e.currentTarget) return;
|
|
665
|
+
onClose();
|
|
666
|
+
}}
|
|
667
|
+
>
|
|
418
668
|
<div ref={dlgRef} className={"dtok-pm" + (maxed ? " max" : "")} style={style} onClick={(e) => e.stopPropagation()}>
|
|
419
669
|
<div className="dtok-pm-head" onPointerDown={(e) => startDrag(e, "move")} onDoubleClick={() => setMaxed((v) => !v)}>
|
|
420
670
|
<b>单价设置</b>
|
|
@@ -702,6 +952,9 @@ export function TokenLogView(props) {
|
|
|
702
952
|
// 「单价设置」折叠面板:默认收起(费用不准确时才需要展开调整)
|
|
703
953
|
// 「单价设置」子弹窗:默认收起;支持经 params.openPricing 直接打开(深链/测试用)
|
|
704
954
|
const [showPricing, setShowPricing] = useState(() => !!(props && props.params && props.params.openPricing));
|
|
955
|
+
// 明细 | 图表 页签:图表全部基于当前筛选结果客户端聚合,不额外发请求。
|
|
956
|
+
// params.charts 支持深链/测试直接落在图表页签。
|
|
957
|
+
const [tab, setTab] = useState(() => (props && props.params && props.params.charts) ? "charts" : "detail");
|
|
705
958
|
// 查询完成信号:loading 由 true→false 时记一次时间戳,用于「✓ 数据已更新」提示与 KPI 卡回弹。
|
|
706
959
|
// 5 秒静默自动刷新不置 loading,所以轮询不会每 5 秒闪一下。
|
|
707
960
|
const [fetchedAt, setFetchedAt] = useState(0);
|
|
@@ -929,6 +1182,17 @@ export function TokenLogView(props) {
|
|
|
929
1182
|
|
|
930
1183
|
const bodyNodes = [];
|
|
931
1184
|
if (err) bodyNodes.push(<div key="err" className="dtok-err">错误: {err}</div>);
|
|
1185
|
+
bodyNodes.push(
|
|
1186
|
+
<div key="tabs" className="dtok-filter" style={{ padding: "2px 0 0", border: "none", background: "transparent", gap: 4 }}>
|
|
1187
|
+
<button className={"dtok-btn" + (tab === "detail" ? " primary" : "")} onClick={() => setTab("detail")}>明细</button>
|
|
1188
|
+
<button className={"dtok-btn" + (tab === "charts" ? " primary" : "")} onClick={() => setTab("charts")}>图表统计</button>
|
|
1189
|
+
</div>
|
|
1190
|
+
);
|
|
1191
|
+
if (tab === "charts") {
|
|
1192
|
+
bodyNodes.push(
|
|
1193
|
+
<ChartsPanel key={"charts-" + fetchedAt} records={records} rate={rateCny} />
|
|
1194
|
+
);
|
|
1195
|
+
} else {
|
|
932
1196
|
if (cards.length) bodyNodes.push(
|
|
933
1197
|
// key 带 fetchedAt:每次「手动查询 / 首次加载」拿到新数据时重建一次,KPI 卡重播回弹动画
|
|
934
1198
|
<div className={"dtok-cards" + (fetchedAt ? " dtok-pop" : "")} key={"cards-" + fetchedAt}>
|
|
@@ -982,6 +1246,7 @@ export function TokenLogView(props) {
|
|
|
982
1246
|
);
|
|
983
1247
|
bodyNodes.push(pager("pager-bottom", "bottom"));
|
|
984
1248
|
}
|
|
1249
|
+
}
|
|
985
1250
|
|
|
986
1251
|
return (
|
|
987
1252
|
<div className="dtok-root" onClick={(e) => e.stopPropagation()}>
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dock",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
4
4
|
"description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计,支持各模型自定义单价与多段分时价)、模型设置、图片理解代理、模型余额、任务动画(19 种运行动画 + 桌面伙伴 3D 场景)、任务通知(完成/异常/需确认通知、提示音、系统通知、钉钉/飞书推送)、运行状态(进行中任务与最近完成一览);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -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
|
+
}
|