dsh-dock 0.4.1 → 0.5.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 +93 -8
- package/client.js +1779 -178
- package/features/animation/host.js +516 -0
- package/features/animation/view.jsx +1451 -0
- package/features/balance/view.js +66 -41
- package/features/modelconfig/view.js +7 -4
- package/index.js +5 -9
- package/package.json +2 -2
- package/screenshots/dock-animation.png +0 -0
- package/screenshots/dock-robot.png +0 -0
- package/src/client.jsx +110 -48
- package/src/host-core.js +28 -1
- package/src/shared.js +24 -3
package/features/balance/view.js
CHANGED
|
@@ -90,8 +90,11 @@ function BalanceView(props) {
|
|
|
90
90
|
} else if (providers.length === 0) {
|
|
91
91
|
body.push(react.createElement("div", { key: "empty", className: "dkb-note" }, "没有找到已配置的模型 Provider"));
|
|
92
92
|
} else {
|
|
93
|
+
// Provider 分组:可查余额的排前面(不可查/需登录的往后)
|
|
94
|
+
const rank = (p) => (p.balance && p.balance.status === "ok" ? 0 : p.balance && p.balance.status === "login-required" ? 1 : 2);
|
|
95
|
+
const sorted = providers.slice().sort((a, b) => rank(a) - rank(b));
|
|
93
96
|
body.push(react.createElement("div", { key: "rows", className: "dkb-rows" },
|
|
94
|
-
|
|
97
|
+
sorted.map((p) => {
|
|
95
98
|
const accent = accentOf(p.id);
|
|
96
99
|
const b = p.balance;
|
|
97
100
|
let badge = ["未知", ""];
|
|
@@ -102,51 +105,57 @@ function BalanceView(props) {
|
|
|
102
105
|
else if (b.status === "login-required") badge = ["需登录", "warn"];
|
|
103
106
|
else badge = ["查询失败", "err"];
|
|
104
107
|
}
|
|
105
|
-
|
|
106
|
-
const cells = [];
|
|
108
|
+
// 主体内容按状态分三型:余额(数字大卡)/ 登录跳转 / 说明
|
|
107
109
|
let balBody = null;
|
|
108
110
|
if (b && b.status === "ok" && b.kind === "quota") {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
react.createElement("
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
react.createElement("span", { className: "dkb-
|
|
119
|
-
|
|
120
|
-
react.createElement("span", { className: "dkb-
|
|
121
|
-
d.resetTime
|
|
122
|
-
? react.createElement("span", { className: "dkb-part" }, "重置 " + String(d.resetTime).slice(0, 16))
|
|
123
|
-
: null));
|
|
124
|
-
});
|
|
111
|
+
const dims = Array.isArray(b.dims) ? b.dims : [];
|
|
112
|
+
balBody = react.createElement(react.Fragment, null,
|
|
113
|
+
react.createElement("div", { className: "dkb-main" },
|
|
114
|
+
react.createElement("span", { className: "dkb-main-label" }, "剩余额度"),
|
|
115
|
+
react.createElement("span", { className: "dkb-main-value" }, fmt(b.remaining) + (b.unit ? " " + b.unit : "")),
|
|
116
|
+
react.createElement("span", { className: "dkb-main-part" }, "已用 " + fmt(b.used) + " / 总 " + fmt(b.limit),
|
|
117
|
+
b.resetTime ? react.createElement("span", null, " · 重置 " + String(b.resetTime).slice(0, 10)) : null)),
|
|
118
|
+
dims.map((d, i) => react.createElement("div", { key: "d" + i, className: "dkb-dim" },
|
|
119
|
+
react.createElement("span", { className: "dkb-dim-label" }, d.window === "weekly" ? "周额度" : "小时额度"),
|
|
120
|
+
react.createElement("span", { className: "dkb-dim-bar" },
|
|
121
|
+
react.createElement("span", { className: "dkb-dim-fill", style: { width: (d.limit > 0 ? Math.min(100, Math.round((d.remaining / d.limit) * 100)) : 0) + "%" } })),
|
|
122
|
+
react.createElement("span", { className: "dkb-dim-text" }, "剩 " + fmt(d.remaining) + " / " + fmt(d.limit)))));
|
|
125
123
|
} else if (b && b.status === "ok") {
|
|
126
124
|
const infos = Array.isArray(b.infos) ? b.infos : [];
|
|
127
|
-
|
|
128
|
-
react.createElement("div", { key: idx, className: "dkb-
|
|
129
|
-
react.createElement("span", { className: "dkb-
|
|
130
|
-
react.createElement("span", { className: "dkb-
|
|
131
|
-
|
|
132
|
-
? react.createElement("span", { className: "dkb-part", style: { color: C_GRANTED } }, "赠送 " + fmt(i.grantedBalance))
|
|
133
|
-
: null
|
|
134
|
-
i.toppedUpBalance != null
|
|
135
|
-
? react.createElement("span", { className: "dkb-part", style: { color: C_TOPUP } }, "充值 " + fmt(i.toppedUpBalance))
|
|
136
|
-
: null)));
|
|
125
|
+
balBody = react.createElement("div", { className: "dkb-mains" },
|
|
126
|
+
infos.map((i, idx) => react.createElement("div", { key: idx, className: "dkb-main" },
|
|
127
|
+
react.createElement("span", { className: "dkb-main-label", style: { color: accent } }, i.currency),
|
|
128
|
+
react.createElement("span", { className: "dkb-main-value" }, fmt(i.totalBalance)),
|
|
129
|
+
react.createElement("span", { className: "dkb-main-parts" },
|
|
130
|
+
i.grantedBalance != null ? react.createElement("span", { className: "dkb-main-part", style: { color: C_GRANTED } }, "赠送 " + fmt(i.grantedBalance)) : null,
|
|
131
|
+
i.toppedUpBalance != null ? react.createElement("span", { className: "dkb-main-part", style: { color: C_TOPUP } }, "充值 " + fmt(i.toppedUpBalance)) : null))));
|
|
137
132
|
} else if (b && b.status === "login-required" && b.consoleUrl) {
|
|
138
|
-
balBody = react.createElement("div", { className: "dkb-
|
|
133
|
+
balBody = react.createElement("div", { className: "dkb-main" },
|
|
139
134
|
react.createElement("a", { href: b.consoleUrl, target: "_blank", rel: "noreferrer", className: "dkb-link", style: { color: accent } }, "去控制台查看余额 →"));
|
|
140
135
|
} else {
|
|
141
|
-
balBody =
|
|
136
|
+
balBody = b && b.message
|
|
137
|
+
? react.createElement("div", { className: "dkb-note" }, b.message)
|
|
138
|
+
: react.createElement("div", { className: "dkb-note" }, "该 Provider 没有已知的余额查询接口");
|
|
142
139
|
}
|
|
143
|
-
|
|
140
|
+
// 长连接信息弱化成单行小字(ID · api · baseURL),模型 chips 收敛到折叠区
|
|
141
|
+
const sub = ["ID " + p.id, p.api, p.baseURL ? p.baseURL.replace(/^https?:\/\//, "") : null].filter(Boolean).join(" · ");
|
|
144
142
|
return react.createElement("div", { key: p.id, className: "dkb-row", "data-dock-provider": p.id },
|
|
145
143
|
react.createElement("div", { className: "dkb-row-head" },
|
|
146
144
|
react.createElement("span", { className: "dkb-dot", style: { background: accent } }),
|
|
147
145
|
react.createElement("span", { className: "dkb-name", style: { color: accent } }, p.displayName),
|
|
148
146
|
def === p.id ? react.createElement("span", { className: "dkb-default" }, "默认") : null,
|
|
149
|
-
react.createElement("span", { className: "dkb-badge" + (badge[1] ? " " + badge[1] : ""), style: badge[1] === "err" ? { color: C_ERR, borderColor: C_ERR } : null }, badge[0])
|
|
147
|
+
react.createElement("span", { className: "dkb-badge" + (badge[1] ? " " + badge[1] : ""), style: badge[1] === "err" ? { color: C_ERR, borderColor: C_ERR } : null }, badge[0]),
|
|
148
|
+
// 模型数折叠提示(点开展开 chips)
|
|
149
|
+
p.models && p.models.length > 0
|
|
150
|
+
? react.createElement("button", {
|
|
151
|
+
type: "button", className: "dkb-models-toggle",
|
|
152
|
+
title: p.models.join(", "),
|
|
153
|
+
onClick: (e) => {
|
|
154
|
+
const row = e.currentTarget.closest(".dkb-row");
|
|
155
|
+
if (row) row.classList.toggle("dkb-models-open");
|
|
156
|
+
}
|
|
157
|
+
}, p.models.length + " 个模型 ▾")
|
|
158
|
+
: null),
|
|
150
159
|
sub ? react.createElement("div", { className: "dkb-sub" }, sub) : null,
|
|
151
160
|
p.apiKeyEnv
|
|
152
161
|
? react.createElement("div", { className: "dkb-sub" }, "密钥: " + p.apiKeyEnv + (p.credentialConfigured ? " ✓" : " ✗"))
|
|
@@ -165,7 +174,7 @@ function BalanceView(props) {
|
|
|
165
174
|
return react.createElement("div", { className: "dkb-rows" },
|
|
166
175
|
react.createElement("div", { className: "dkb-note" },
|
|
167
176
|
data && !snap.error && providers.length > 0
|
|
168
|
-
? okCount + "/" + providers.length + " 个 Provider
|
|
177
|
+
? okCount + "/" + providers.length + " 个 Provider 可查余额(可查的排前面)"
|
|
169
178
|
: "模型余额 · Host 半部实时拉取"),
|
|
170
179
|
body,
|
|
171
180
|
react.createElement("div", { className: "dkb-foot" },
|
|
@@ -271,8 +280,9 @@ export const feature = {
|
|
|
271
280
|
css: [
|
|
272
281
|
".dkb-note{color:var(--dsw-alias-label-secondary);font-size:12px;}",
|
|
273
282
|
".dkb-error{color:var(--dsw-alias-state-error-primary);}",
|
|
274
|
-
".dkb-rows{display:flex;flex-direction:column;gap:
|
|
275
|
-
|
|
283
|
+
".dkb-rows{display:flex;flex-direction:column;gap:10px;}",
|
|
284
|
+
// Provider 卡片:行距收窄、留白分层
|
|
285
|
+
".dkb-row{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 14px 12px;display:flex;flex-direction:column;gap:6px;}",
|
|
276
286
|
".dkb-row-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
|
|
277
287
|
".dkb-dot{width:8px;height:8px;border-radius:50%;flex:none;}",
|
|
278
288
|
".dkb-name{font-weight:600;font-size:13px;color:var(--dsw-alias-label-primary);}",
|
|
@@ -281,13 +291,28 @@ export const feature = {
|
|
|
281
291
|
".dkb-badge.ok{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
|
|
282
292
|
".dkb-badge.warn{color:var(--dsw-alias-state-warning-primary);border-color:currentColor;}",
|
|
283
293
|
".dkb-badge.err{color:var(--dsw-alias-state-error-primary);border-color:currentColor;}",
|
|
294
|
+
// 模型数折叠开关(点行头右侧"N 个模型 ▾"展开/收起 chips)
|
|
295
|
+
".dkb-models-toggle{margin-left:auto;cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);font-family:inherit;font-size:11px;padding:1px 6px;border-radius:6px;white-space:nowrap;}",
|
|
296
|
+
".dkb-models-toggle:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
297
|
+
".dkb-row:not(.dkb-models-open) .dkb-chips{display:none;}",
|
|
298
|
+
".dkb-row.dkb-models-open .dkb-models-toggle::after{content:\"\";}",
|
|
299
|
+
// 连接信息弱化单行(省略 URL 协议头、超出省略号)
|
|
284
300
|
".dkb-sub{color:var(--dsw-alias-label-tertiary);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
285
|
-
".dkb-chips{display:flex;flex-wrap:wrap;gap:4px;}",
|
|
301
|
+
".dkb-chips{display:flex;flex-wrap:wrap;gap:4px;padding:2px 0 4px;}",
|
|
286
302
|
".dkb-chip{font-size:10px;border-radius:6px;padding:1px 6px;}",
|
|
287
|
-
|
|
288
|
-
".dkb-
|
|
289
|
-
".dkb-
|
|
290
|
-
".dkb-
|
|
303
|
+
// 余额主数字卡(大号数字 + 小字辅助信息;多币种横排多卡)
|
|
304
|
+
".dkb-mains{display:flex;gap:12px;flex-wrap:wrap;}",
|
|
305
|
+
".dkb-main{display:flex;flex-direction:column;gap:2px;padding:8px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);min-width:150px;}",
|
|
306
|
+
".dkb-main-label{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
|
|
307
|
+
".dkb-main-value{font-size:20px;font-weight:700;color:var(--dsw-alias-state-success-primary);font-variant-numeric:tabular-nums;line-height:1.2;}",
|
|
308
|
+
".dkb-main-parts{display:flex;gap:10px;flex-wrap:wrap;}",
|
|
309
|
+
".dkb-main-part{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
|
|
310
|
+
// 配额维度条(周/小时额度:进度条 + 剩余/总量)
|
|
311
|
+
".dkb-dim{display:flex;align-items:center;gap:10px;font-size:11px;color:var(--dsw-alias-label-secondary);}",
|
|
312
|
+
".dkb-dim-label{flex:none;min-width:44px;color:var(--dsw-alias-label-tertiary);}",
|
|
313
|
+
".dkb-dim-bar{flex:1;max-width:220px;height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);overflow:hidden;}",
|
|
314
|
+
".dkb-dim-fill{display:block;height:100%;border-radius:3px;background:linear-gradient(90deg,var(--dsw-alias-state-success-primary),color-mix(in srgb,var(--dsw-alias-state-success-primary) 60%,#fff));}",
|
|
315
|
+
".dkb-dim-text{flex:none;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;}",
|
|
291
316
|
".dkb-link{font-size:12px;text-decoration:none;}",
|
|
292
317
|
".dkb-link:hover{text-decoration:underline;}",
|
|
293
318
|
".dkb-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;color:var(--dsw-alias-label-tertiary);font-size:12px;}",
|
|
@@ -280,10 +280,12 @@ function ModelsView() {
|
|
|
280
280
|
const vpKey = vpDraft.provider + "/" + vpDraft.model;
|
|
281
281
|
const vpKnown = vpCandidates.some((c) => c.key === vpKey);
|
|
282
282
|
body.push(react.createElement("div", { key: "visionproxy", className: "dkm-prov" },
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
283
|
+
react.createElement("div", { className: "dkm-prov-head" },
|
|
284
|
+
react.createElement("span", { className: "dkm-name" }, "图片理解代理"),
|
|
285
|
+
react.createElement("span", { className: "dkm-badge" }, vpDraft.enabled ? "已启用" : "已停用"),
|
|
286
|
+
react.createElement("span", { className: "dkm-sub" }, "纯文本模型收到图片时自动调用所选视觉模型识别,识别文本替换图片;多模态模型原样自识别")),
|
|
287
|
+
react.createElement("div", { className: "dkm-note dkm-warn" },
|
|
288
|
+
"⚠ 本功能会调用视觉模型识别图片,消耗 token(每次识图按视觉模型计费)。默认关闭;不开启则插件其余功能均不消耗任何 token。"),
|
|
287
289
|
react.createElement("div", { className: "dkm-checks" },
|
|
288
290
|
react.createElement("label", { className: "dkm-check" },
|
|
289
291
|
react.createElement("input", {
|
|
@@ -401,6 +403,7 @@ export const feature = {
|
|
|
401
403
|
description: "编辑各 Provider 模型目录:输入类型(文本/图片 + 标注)与思考强度档位;写回官方配置热生效,会话模型选择器即时可选",
|
|
402
404
|
css: [
|
|
403
405
|
".dkm-note{color:var(--dsw-alias-label-secondary);font-size:12px;}",
|
|
406
|
+
".dkm-warn{color:var(--dsw-alias-state-warning-primary);}",
|
|
404
407
|
".dkm-error{color:var(--dsw-alias-state-error-primary);font-size:12px;}",
|
|
405
408
|
".dkm-chips{display:flex;flex-wrap:wrap;gap:6px;}",
|
|
406
409
|
".dkm-chip{cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);border-radius:999px;padding:3px 12px;font-family:inherit;font-size:12px;}",
|
package/index.js
CHANGED
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
// - visionproxy 图片理解代理(v0.3.1):纯文本模型收图自动走视觉模型识别
|
|
13
13
|
// - balance 模型余额(v0.2.0):各 Provider 账户余额/配额
|
|
14
14
|
// - tokenlog 用量记录(v0.4.0):LLM 调用记账与统计(移植自 @wycto/dsh-token-usage)
|
|
15
|
-
// - animation
|
|
15
|
+
// - animation 任务动画(v0.5.0):会话任务追踪 + 动效/通知配置持久化(参照 @wycto/dsh-task-pulse)
|
|
16
16
|
import { DOCK_NS, DockConfig } from './src/host-core.js'
|
|
17
17
|
import { feature as fModels } from './features/modelconfig/host.js'
|
|
18
18
|
import { feature as fVisionProxy } from './features/visionproxy/host.js'
|
|
19
19
|
import { feature as fBalance } from './features/balance/host.js'
|
|
20
20
|
import { feature as fTokenlog } from './features/tokenlog/host.js'
|
|
21
|
+
import { feature as fAnimation } from './features/animation/host.js'
|
|
21
22
|
|
|
22
23
|
export const name = 'dsh-dock'
|
|
23
24
|
|
|
@@ -36,12 +37,7 @@ export function apply(ctx) {
|
|
|
36
37
|
fVisionProxy,
|
|
37
38
|
fBalance,
|
|
38
39
|
fTokenlog,
|
|
39
|
-
|
|
40
|
-
id: 'animation',
|
|
41
|
-
name: '任务动画',
|
|
42
|
-
roadmap: '0.5.0',
|
|
43
|
-
description: '任务进度动画与通知',
|
|
44
|
-
},
|
|
40
|
+
fAnimation,
|
|
45
41
|
]
|
|
46
42
|
|
|
47
43
|
const state = new Map()
|
|
@@ -80,8 +76,8 @@ export function apply(ctx) {
|
|
|
80
76
|
}
|
|
81
77
|
|
|
82
78
|
// 默认启用已实现的功能(与 Client 半部 defaultEnabled 对齐)。
|
|
83
|
-
//
|
|
84
|
-
// 在此之前 Host 侧开关以本处 defaultEnabled
|
|
79
|
+
// Client 侧功能开关已 localStorage 持久化(v0.5.0);Host↔Client 的开关双向同步仍未打通,
|
|
80
|
+
// 在此之前 Host 侧开关以本处 defaultEnabled 为准(面板停用某功能只影响浏览器侧 UI)。
|
|
85
81
|
for (const f of FEATURES) {
|
|
86
82
|
if (f.defaultEnabled) setEnabled(f.id, true)
|
|
87
83
|
}
|
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.5.0",
|
|
4
|
+
"description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计)、模型设置、图片理解代理、模型余额、任务动画(8 种氛围动效 + 桌面伙伴 3D 场景、完成通知与提示音、钉钉推送,开关独立、配置持久化);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|
|
Binary file
|
|
Binary file
|
package/src/client.jsx
CHANGED
|
@@ -23,19 +23,16 @@ import { feature as fModelconfig } from "../features/modelconfig/view.js";
|
|
|
23
23
|
import { feature as fHeartbeat } from "../features/heartbeat/view.js";
|
|
24
24
|
import { feature as fTheme } from "../features/theme/view.js";
|
|
25
25
|
import { feature as fBalance } from "../features/balance/view.js";
|
|
26
|
+
import { feature as fAnimation } from "../features/animation/view.jsx";
|
|
26
27
|
|
|
27
28
|
const name = "dsh-dock";
|
|
28
|
-
const DOCK_VERSION = "0.
|
|
29
|
+
const DOCK_VERSION = "0.5.0";
|
|
29
30
|
|
|
30
31
|
// ---- 内置功能注册表:新功能 = features/<id>/ 加模块 + 这里 import 一行 ----
|
|
31
|
-
const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance];
|
|
32
|
+
const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation];
|
|
32
33
|
// 规划占位(路线图):接入后移除并建 features/<id>/ 模块
|
|
33
|
-
const PLANNED_FEATURES = [
|
|
34
|
-
|
|
35
|
-
];
|
|
36
|
-
const PLANNED_NOTES = {
|
|
37
|
-
animation: "待接入(路线图 0.5.0):任务进度动画与完成通知。",
|
|
38
|
-
};
|
|
34
|
+
const PLANNED_FEATURES = [];
|
|
35
|
+
const PLANNED_NOTES = {};
|
|
39
36
|
|
|
40
37
|
// ---- 外部功能注册表(dockBridge 回装通道) ----
|
|
41
38
|
const externalDefs = [];
|
|
@@ -45,7 +42,7 @@ function notifyExternal() {
|
|
|
45
42
|
}
|
|
46
43
|
/**
|
|
47
44
|
* 外部功能桥:独立功能包经 require("dsh-dock").dockBridge 注册视图。
|
|
48
|
-
* def: { id, name, order?, accent?, description?, css?, View, HomeStat?, package? }
|
|
45
|
+
* def: { id, name, order?, accent?, description?, css?, View, HomeStat?, Overlay?, package? }
|
|
49
46
|
* 返回注销函数。重复 id 视为更新(HMR/重注册场景)。
|
|
50
47
|
*/
|
|
51
48
|
const dockBridge = {
|
|
@@ -64,6 +61,7 @@ const dockBridge = {
|
|
|
64
61
|
css: typeof def.css === "string" ? def.css : "",
|
|
65
62
|
View: def.View,
|
|
66
63
|
HomeStat: typeof def.HomeStat === "function" ? def.HomeStat : null,
|
|
64
|
+
Overlay: typeof def.Overlay === "function" ? def.Overlay : null,
|
|
67
65
|
external: true,
|
|
68
66
|
package: typeof def.package === "string" ? def.package : "",
|
|
69
67
|
};
|
|
@@ -155,9 +153,22 @@ const SHELL_CSS = [
|
|
|
155
153
|
".dockh-card:hover,.dockh-card:focus-visible{border-color:var(--dsw-alias-accent,#4d9fff);outline:none;}",
|
|
156
154
|
".dockh-head{display:flex;align-items:center;gap:8px;}",
|
|
157
155
|
".dockh-name{font-weight:600;flex:none;}",
|
|
158
|
-
".dockh-badge{
|
|
159
|
-
|
|
160
|
-
".dockh-
|
|
156
|
+
".dockh-badge{flex:none;font-size:11px;border-radius:999px;padding:0 8px;color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l2);}",
|
|
157
|
+
// 状态标识(非交互:圆点+文字,与可点的开关一眼区分)
|
|
158
|
+
".dockh-status{margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;}",
|
|
159
|
+
".dockh-sdot{width:7px;height:7px;border-radius:50%;flex:none;background:var(--dsw-alias-state-success-primary,#34d399);box-shadow:0 0 4px color-mix(in srgb,var(--dsw-alias-state-success-primary,#34d399) 55%,transparent);}",
|
|
160
|
+
".dockh-status.off .dockh-sdot{background:transparent;border:1.5px solid var(--dsw-alias-label-tertiary);box-shadow:none;opacity:.75;}",
|
|
161
|
+
".dockh-status.plan .dockh-sdot{background:transparent;border:1.5px dashed var(--dsw-alias-label-tertiary);box-shadow:none;}",
|
|
162
|
+
// iOS 风滑动开关(首页卡片 / 弹层页脚 / 设置页共用的启停控件)
|
|
163
|
+
".dock-sw{flex:none;position:relative;width:34px;height:19px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);cursor:pointer;padding:0;transition:background .18s var(--ds-ease-in-out),border-color .18s var(--ds-ease-in-out);}",
|
|
164
|
+
".dock-sw::after{content:\"\";position:absolute;top:2px;left:2px;width:13px;height:13px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgb(0 0 0 / .35);transition:transform .18s var(--ds-ease-in-out);}",
|
|
165
|
+
".dock-sw:hover{border-color:var(--dsw-alias-accent,#4d9fff);}",
|
|
166
|
+
".dock-sw.on{background:var(--dsw-alias-state-success-primary,#34d399);border-color:transparent;}",
|
|
167
|
+
".dock-sw.on::after{transform:translateX(15px);}",
|
|
168
|
+
// 弹层页脚:文字标签 + 开关 成组
|
|
169
|
+
".dockm-foot-sw{margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:8px;}",
|
|
170
|
+
".dockm-foot-swlabel{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
|
|
171
|
+
".dockm-foot-swlabel.on{color:var(--dsw-alias-state-success-primary);}",
|
|
161
172
|
".dockh-desc{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5;}",
|
|
162
173
|
".dockh-stat{color:var(--dsw-alias-label-tertiary);font-size:12px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
163
174
|
".dockh-foot{display:flex;align-items:center;gap:8px;}",
|
|
@@ -379,34 +390,46 @@ function DockModal() {
|
|
|
379
390
|
? react.createElement("div", { className: "dockm-note" }, PLANNED_NOTES[mod.id] || "待接入:见 README 路线图")
|
|
380
391
|
: st && st.enabled && viewNode
|
|
381
392
|
? viewNode
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
393
|
+
: st && st.error
|
|
394
|
+
? react.createElement("div", { className: "dockm-note dockm-err" }, "功能出错:" + st.error)
|
|
395
|
+
: react.createElement("div", { className: "dockm-note" }, "该功能当前为停用状态(开关已持久化,重启后保持)"),
|
|
385
396
|
react.createElement("div", { className: "dockm-foot" },
|
|
386
397
|
react.createElement("span", null, isHome
|
|
387
398
|
? "功能坞 v" + DOCK_VERSION + " · 共 " + MODULES.length + " 个功能模块," + enabledCount + " 个已启用"
|
|
388
399
|
: "功能坞 v" + DOCK_VERSION + " · 新功能按路线图追加"),
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
400
|
+
!isHome && mod && !mod.planned && st
|
|
401
|
+
? react.createElement("span", { className: "dockm-foot-sw" },
|
|
402
|
+
react.createElement("span", { className: "dockm-foot-swlabel" + (st.enabled ? " on" : "") }, st.enabled ? "已启用" : "已停用"),
|
|
403
|
+
react.createElement("button", {
|
|
404
|
+
type: "button",
|
|
405
|
+
className: "dock-sw" + (st.enabled ? " on" : ""),
|
|
406
|
+
role: "switch",
|
|
407
|
+
"aria-checked": !!st.enabled,
|
|
408
|
+
"aria-label": (st.enabled ? "停用" : "启用") + mod.name,
|
|
409
|
+
title: st.enabled ? "停用「" + mod.name + "」" : "启用「" + mod.name + "」",
|
|
410
|
+
onClick: () => { toggleFeature(mod.id); force(); }
|
|
411
|
+
}))
|
|
412
|
+
: null,
|
|
413
|
+
!isHome && mod && typeof mod.Chip === "function"
|
|
414
|
+
? react.createElement("span", { className: "dockm-foot-sw" },
|
|
415
|
+
react.createElement("span", { className: "dockm-foot-swlabel" + (chipShown(mod.id) ? " on" : "") }, "会话页小控件"),
|
|
416
|
+
react.createElement("button", {
|
|
417
|
+
type: "button",
|
|
418
|
+
className: "dock-sw" + (chipShown(mod.id) ? " on" : ""),
|
|
419
|
+
role: "switch",
|
|
420
|
+
"aria-checked": chipShown(mod.id),
|
|
421
|
+
"aria-label": (chipShown(mod.id) ? "隐藏" : "显示") + mod.name + "的会话页小控件",
|
|
422
|
+
title: "控制会话输入区(模型选择器左侧)是否显示本功能的随身小控件",
|
|
423
|
+
onClick: () => { setChipShown(mod.id, !chipShown(mod.id)); force(); }
|
|
424
|
+
}))
|
|
425
|
+
: null))),
|
|
404
426
|
win.mode === "normal"
|
|
405
427
|
? react.createElement("div", { className: "dockm-resize", onPointerDown: (e) => beginDrag(e, "size") })
|
|
406
428
|
: null));
|
|
407
429
|
}
|
|
408
430
|
|
|
409
|
-
// ----
|
|
431
|
+
// ---- 首页总揽:每个功能模块一张卡片(状态标识 + 运行概要 + 启停开关),点卡片进对应功能 ----
|
|
432
|
+
// 交互约定:状态是「圆点+文字」纯标识(不可点);启停是 iOS 滑动开关(明显可点);点卡片其余区域跳详情页。
|
|
410
433
|
function HomeView(props) {
|
|
411
434
|
const ctx = props && props.ctx;
|
|
412
435
|
const [, force] = react.useReducer((n) => n + 1, 0);
|
|
@@ -435,15 +458,18 @@ function HomeView(props) {
|
|
|
435
458
|
react.createElement("span", { className: "dockm-dot", style: { background: m.accent } }),
|
|
436
459
|
react.createElement("span", { className: "dockh-name" }, m.name),
|
|
437
460
|
m.external ? react.createElement("span", { className: "dockh-badge", title: m.package || undefined }, "外部") : null,
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
react.createElement("span", { className: "dockh-go" }, "查看详情 →"),
|
|
461
|
+
// 状态标识:圆点 + 文字(纯展示,与开关视觉区分)
|
|
462
|
+
react.createElement("span", { className: "dockh-status" + (m.planned ? " plan" : enabled ? "" : " off") },
|
|
463
|
+
react.createElement("span", { className: "dockh-sdot" }),
|
|
464
|
+
react.createElement("span", null, m.planned ? "规划中" : enabled ? "运行中" : "已停用")),
|
|
465
|
+
// 启停开关(规划中的功能不显示)
|
|
444
466
|
m.planned ? null : react.createElement("button", {
|
|
445
467
|
type: "button",
|
|
446
|
-
className: "
|
|
468
|
+
className: "dock-sw" + (enabled ? " on" : ""),
|
|
469
|
+
role: "switch",
|
|
470
|
+
"aria-checked": enabled,
|
|
471
|
+
"aria-label": (enabled ? "停用" : "启用") + m.name,
|
|
472
|
+
title: enabled ? "停用「" + m.name + "」" : "启用「" + m.name + "」",
|
|
447
473
|
onClick: (e) => {
|
|
448
474
|
e.stopPropagation();
|
|
449
475
|
toggleFeature(m.id);
|
|
@@ -451,7 +477,11 @@ function HomeView(props) {
|
|
|
451
477
|
if (props && typeof props.onToggle === "function") props.onToggle();
|
|
452
478
|
force();
|
|
453
479
|
}
|
|
454
|
-
}
|
|
480
|
+
})),
|
|
481
|
+
react.createElement("div", { className: "dockh-desc" }, m.description),
|
|
482
|
+
react.createElement("div", { className: "dockh-stat" }, statNode),
|
|
483
|
+
react.createElement("div", { className: "dockh-foot" },
|
|
484
|
+
react.createElement("span", { className: "dockh-go" }, "查看详情 →")));
|
|
455
485
|
}));
|
|
456
486
|
}
|
|
457
487
|
|
|
@@ -484,16 +514,27 @@ function DockPanel() {
|
|
|
484
514
|
f.planned
|
|
485
515
|
? react.createElement("span", { className: "dock-badge" }, "规划中")
|
|
486
516
|
: react.createElement("button", {
|
|
487
|
-
|
|
517
|
+
type: "button",
|
|
518
|
+
className: "dock-sw" + (st.enabled ? " on" : ""),
|
|
519
|
+
role: "switch",
|
|
520
|
+
"aria-checked": st.enabled,
|
|
521
|
+
"aria-label": (st.enabled ? "停用" : "启用") + f.name,
|
|
522
|
+
title: st.enabled ? "停用「" + f.name + "」" : "启用「" + f.name + "」",
|
|
488
523
|
onClick: () => toggle(f.id)
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
524
|
+
}),
|
|
525
|
+
!f.planned && typeof f.Chip === "function"
|
|
526
|
+
? react.createElement("span", { className: "dockm-foot-sw" },
|
|
527
|
+
react.createElement("span", { className: "dockm-foot-swlabel" + (chipShown(f.id) ? " on" : "") }, "会话页小控件"),
|
|
528
|
+
react.createElement("button", {
|
|
529
|
+
type: "button",
|
|
530
|
+
className: "dock-sw" + (chipShown(f.id) ? " on" : ""),
|
|
531
|
+
role: "switch",
|
|
532
|
+
"aria-checked": chipShown(f.id),
|
|
533
|
+
"aria-label": (chipShown(f.id) ? "隐藏" : "显示") + f.name + "的会话页小控件",
|
|
534
|
+
title: "控制会话输入区(模型选择器左侧)是否显示本功能的随身小控件",
|
|
535
|
+
onClick: () => { setChipShown(f.id, !chipShown(f.id)); force(); }
|
|
536
|
+
}))
|
|
537
|
+
: null),
|
|
497
538
|
f.planned
|
|
498
539
|
? react.createElement("div", { className: "dock-body" }, PLANNED_NOTES[f.id] || "待接入:见 README 路线图")
|
|
499
540
|
: st.error ? react.createElement("div", { className: "dock-body dockm-err" }, "功能出错:" + st.error) : null,
|
|
@@ -519,6 +560,23 @@ function DockChips(props) {
|
|
|
519
560
|
return react.createElement("div", { className: "dockchip-row" }, items);
|
|
520
561
|
}
|
|
521
562
|
|
|
563
|
+
// ---- 功能全局浮层:模块可在描述符上挂 Overlay 组件(props: { ctx, feature }) ----
|
|
564
|
+
// 与 Chip(会话输入区小控件)不同,Overlay 是常驻整页挂载的全局 UI(如任务动画的动效与通知栈),
|
|
565
|
+
// 只要所属功能启用就渲染(功能停用即卸载,浮层自身负责按数据状态决定显示什么)。
|
|
566
|
+
function FeatureOverlays() {
|
|
567
|
+
const [, force] = react.useReducer((n) => n + 1, 0);
|
|
568
|
+
useExternalVersion();
|
|
569
|
+
react.useEffect(() => subscribeFeatureState(() => force()), []);
|
|
570
|
+
const items = [];
|
|
571
|
+
for (const f of allModules()) {
|
|
572
|
+
if (f.planned || !stateOf(f.id).enabled || typeof f.Overlay !== "function") continue;
|
|
573
|
+
items.push(react.createElement(FeatureBoundary, { key: f.id },
|
|
574
|
+
react.createElement(f.Overlay, { ctx: ctxRef.current, feature: f })));
|
|
575
|
+
}
|
|
576
|
+
if (items.length === 0) return null;
|
|
577
|
+
return react.createElement(react.Fragment, null, items);
|
|
578
|
+
}
|
|
579
|
+
|
|
522
580
|
// ---- 插件应用:注册四处 UI(入口按钮 / 弹层 / 设置页 / 会话区 chips),注入样式 ----
|
|
523
581
|
// ctx 以 ref 供视图组件使用(timer/theme 等服务按需自取;重装/HMR 时更新)
|
|
524
582
|
const ctxRef = { current: null };
|
|
@@ -536,6 +594,10 @@ export function apply(ctx) {
|
|
|
536
594
|
slots.inject("shell.overlay", () => slots.register(
|
|
537
595
|
{ name: "shell.overlay", id: "dsh-dock-panel", order: 21, label: "功能坞面板" },
|
|
538
596
|
() => react.createElement(DockModal, null)));
|
|
597
|
+
// 功能全局浮层(任务动画的动效与通知等):已启用功能的 Overlay 常驻挂载
|
|
598
|
+
slots.inject("shell.overlay", () => slots.register(
|
|
599
|
+
{ name: "shell.overlay", id: "dsh-dock-feature-overlays", order: 22, label: "功能坞全局浮层" },
|
|
600
|
+
() => react.createElement(FeatureOverlays, null)));
|
|
539
601
|
slots.inject("settings.section", () => slots.register(
|
|
540
602
|
{ name: "settings.section", id: "dsh-dock", order: 90, label: "功能坞" },
|
|
541
603
|
() => react.createElement(DockPanel, null)));
|
package/src/host-core.js
CHANGED
|
@@ -5,15 +5,42 @@ import z from '@deepseek-ai/schemastery'
|
|
|
5
5
|
/** dsh-dock 自有 settings 命名空间(插件级配置,如图片理解代理)。 */
|
|
6
6
|
export const DOCK_NS = 'dsh-dock'
|
|
7
7
|
|
|
8
|
-
/** 自有命名空间的 schema
|
|
8
|
+
/** 自有命名空间的 schema(图片理解代理 + 任务动画配置)。 */
|
|
9
9
|
export const DockConfig = z.object({
|
|
10
10
|
visionProxy: z.object({
|
|
11
11
|
enabled: z.boolean().default(false),
|
|
12
12
|
provider: z.string().default(''),
|
|
13
13
|
model: z.string().default(''),
|
|
14
14
|
}).default({}),
|
|
15
|
+
animation: z.object({
|
|
16
|
+
// 动画与通知两个独立开关(可只开其一)
|
|
17
|
+
animationEnabled: z.boolean().default(true),
|
|
18
|
+
effectMode: z.string().default('flow'),
|
|
19
|
+
// 桌面伙伴场景缩放(0.85~2.2;浮层右下角也可直接拖动缩放)
|
|
20
|
+
robotScale: z.number().default(1.35),
|
|
21
|
+
notifyEnabled: z.boolean().default(true),
|
|
22
|
+
notifyOnComplete: z.boolean().default(true),
|
|
23
|
+
notifyOnError: z.boolean().default(true),
|
|
24
|
+
// 通知停留毫秒数(0 = 常驻直到手动关闭)
|
|
25
|
+
notifyStayMs: z.number().default(8000),
|
|
26
|
+
// 浏览器系统通知(页面后台时推送)
|
|
27
|
+
systemNotify: z.boolean().default(false),
|
|
28
|
+
// 任务结束提示音(WebAudio 合成,macOS/Windows 通用,无音频文件依赖)
|
|
29
|
+
soundNotify: z.boolean().default(true),
|
|
30
|
+
// 提示音音效(完成场景音名;异常音由音效包内配套)
|
|
31
|
+
soundEffect: z.string().default('chime'),
|
|
32
|
+
// 钉钉群机器人推送(宿主侧直发,浏览器关着也能推;事件跟随 notifyOnComplete/notifyOnError)
|
|
33
|
+
dingtalkEnabled: z.boolean().default(false),
|
|
34
|
+
dingtalkWebhook: z.string().default(''),
|
|
35
|
+
}).default({}),
|
|
15
36
|
})
|
|
16
37
|
|
|
38
|
+
/** 任务动画 effectMode 合法值(客户端动画模式)。 */
|
|
39
|
+
export const ANIMATION_MODES = ['flow', 'breathe', 'ring', 'orbit', 'robot', 'matrix', 'stars', 'aurora']
|
|
40
|
+
|
|
41
|
+
/** 提示音 soundEffect 合法值(客户端音效库键名)。 */
|
|
42
|
+
export const SOUND_EFFECTS = ['chime', 'ding', 'coin', 'bell', 'pulse', 'arp']
|
|
43
|
+
|
|
17
44
|
/** 沿 settingsPath 走一层对象(user/base/value 都可用)。 */
|
|
18
45
|
export function walkPath(node, path) {
|
|
19
46
|
for (const key of path || []) {
|
package/src/shared.js
CHANGED
|
@@ -23,16 +23,35 @@ export class FeatureBoundary extends react.Component {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// ---- 功能开关状态(外壳面板与会话区 chips
|
|
26
|
+
// ---- 功能开关状态(外壳面板与会话区 chips 共用;每功能开关 localStorage 持久化,重启后保持) ----
|
|
27
27
|
const featureState = new Map();
|
|
28
28
|
const stateListeners = new Set();
|
|
29
29
|
function notifyState() {
|
|
30
30
|
for (const fn of stateListeners) fn();
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
// 开关持久化(dsh-dock/features/v1):id -> boolean;未记录过的功能用模块默认值
|
|
33
|
+
const FEATURE_STORE_KEY = "dsh-dock/features/v1";
|
|
34
|
+
const featurePersist = { map: {} };
|
|
35
|
+
try {
|
|
36
|
+
if (typeof localStorage !== "undefined") {
|
|
37
|
+
const raw = localStorage.getItem(FEATURE_STORE_KEY);
|
|
38
|
+
const obj = raw ? JSON.parse(raw) : null;
|
|
39
|
+
if (obj && typeof obj === "object") featurePersist.map = obj;
|
|
40
|
+
}
|
|
41
|
+
} catch { /* localStorage 不可用时用默认值 */ }
|
|
42
|
+
function persistFeatureEnabled() {
|
|
43
|
+
try {
|
|
44
|
+
if (typeof localStorage !== "undefined") localStorage.setItem(FEATURE_STORE_KEY, JSON.stringify(featurePersist.map));
|
|
45
|
+
} catch { /* 持久化失败静默 */ }
|
|
46
|
+
}
|
|
47
|
+
/** 外壳 apply 时初始化内置功能的默认开关(持久化过的值优先于模块默认)。 */
|
|
33
48
|
export function initFeatureState(defs) {
|
|
34
49
|
for (const f of defs) {
|
|
35
|
-
if (
|
|
50
|
+
if (featureState.has(f.id)) continue;
|
|
51
|
+
let enabled = !f.planned && f.defaultEnabled !== false;
|
|
52
|
+
const saved = featurePersist.map[f.id];
|
|
53
|
+
if (typeof saved === "boolean") enabled = saved;
|
|
54
|
+
featureState.set(f.id, { enabled, error: null });
|
|
36
55
|
}
|
|
37
56
|
}
|
|
38
57
|
/** 取功能开关(外部功能默认启用;未登记的 id 惰性建项)。 */
|
|
@@ -44,6 +63,8 @@ export function stateOf(id) {
|
|
|
44
63
|
export function toggleFeature(id) {
|
|
45
64
|
const st = stateOf(id);
|
|
46
65
|
st.enabled = !st.enabled;
|
|
66
|
+
featurePersist.map[id] = st.enabled;
|
|
67
|
+
persistFeatureEnabled();
|
|
47
68
|
notifyState();
|
|
48
69
|
}
|
|
49
70
|
export function subscribeFeatureState(fn) {
|