dsh-dock 0.4.1 → 0.6.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.
@@ -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
- providers.map((p) => {
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
- const sub = ["ID: " + p.id, p.api ? "api: " + p.api : null, p.baseURL ? p.baseURL : null].filter(Boolean).join(" · ");
106
- const cells = [];
108
+ // 主体内容按状态分三型:余额(数字大卡)/ 登录跳转 / 说明
107
109
  let balBody = null;
108
110
  if (b && b.status === "ok" && b.kind === "quota") {
109
- cells.push(react.createElement("div", { key: "q", className: "dkb-bal" },
110
- react.createElement("span", { className: "dkb-cur", style: { color: accent } }, "总额度"),
111
- react.createElement("span", { className: "dkb-total", style: { color: C_TOTAL } }, "剩余 " + fmt(b.remaining) + (b.unit ? " " + b.unit : "")),
112
- react.createElement("span", { className: "dkb-part" }, "已用 " + fmt(b.used) + " / 总 " + fmt(b.limit)),
113
- b.resetTime
114
- ? react.createElement("span", { className: "dkb-part" }, "重置 " + String(b.resetTime).slice(0, 16))
115
- : null));
116
- (Array.isArray(b.dims) ? b.dims : []).forEach((d, i) => {
117
- cells.push(react.createElement("div", { key: "d" + i, className: "dkb-bal" },
118
- react.createElement("span", { className: "dkb-cur", style: { color: accent } }, d.window === "weekly" ? "周额度" : "小时额度"),
119
- react.createElement("span", { className: "dkb-total", style: { color: C_TOTAL } }, "剩余 " + fmt(d.remaining)),
120
- react.createElement("span", { className: "dkb-part" }, "已用 " + fmt(d.used) + " / " + fmt(d.limit)),
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
- cells.push(infos.map((i, idx) =>
128
- react.createElement("div", { key: idx, className: "dkb-bal" },
129
- react.createElement("span", { className: "dkb-cur", style: { color: accent } }, i.currency),
130
- react.createElement("span", { className: "dkb-total", style: { color: C_TOTAL } }, "总额 " + fmt(i.totalBalance)),
131
- i.grantedBalance != null
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-bal" },
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 = react.createElement("div", { className: "dkb-note" }, (b && b.message) || "未知状态");
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
- if (cells.length > 0) balBody = react.createElement("div", { className: "dkb-rows" }, cells);
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:8px;}",
275
- ".dkb-row{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:8px 10px;display:flex;flex-direction:column;gap:5px;}",
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
- ".dkb-bal{display:flex;gap:10px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-primary);align-items:baseline;}",
288
- ".dkb-cur{font-weight:600;min-width:48px;}",
289
- ".dkb-total{font-weight:600;}",
290
- ".dkb-part{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
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;}",
@@ -12,8 +12,10 @@ const modelsStore = {
12
12
  set(patch) { this.snap = Object.assign({}, this.snap, patch); for (const fn of this.listeners) fn(); },
13
13
  load() {
14
14
  if (this.snap.loading) return;
15
- this.set({ loading: true });
16
- fetch("/dsh-dock/models", { signal: AbortSignal.timeout(20000) })
15
+ this.set({ loading: true, error: null });
16
+ // Provider 可在 DSH 原生模型设置中随时增删;不要复用浏览器缓存,
17
+ // 否则返回本页时仍可能看到旧的共享快照。
18
+ fetch("/dsh-dock/models", { cache: "no-store", signal: AbortSignal.timeout(20000) })
17
19
  .then((res) => (res.ok ? res.json() : Promise.reject(new Error("模型目录接口 HTTP " + res.status))))
18
20
  .then((data) => this.set({ data: data, loading: false, error: null }))
19
21
  .catch((e) => this.set({ loading: false, error: (e && e.message) || String(e) }));
@@ -81,6 +83,15 @@ function ModelsView() {
81
83
  const [vpDirty, setVpDirty] = react.useState(false);
82
84
  const [vpSaving, setVpSaving] = react.useState(false);
83
85
  const [vpMsg, setVpMsg] = react.useState(null);
86
+ const [vpDirectExpanded, setVpDirectExpanded] = react.useState(false);
87
+
88
+ // 每次进入模型设置都重新读取官方目录。modelsStore 是跨页面共享快照,
89
+ // 原生设置刚新增 Provider 时不能要求用户再滚到页面底部手动刷新。
90
+ react.useEffect(() => { modelsStore.load(); }, []);
91
+ const refreshModels = () => {
92
+ setBuiltKey(null);
93
+ modelsStore.load();
94
+ };
84
95
 
85
96
  const cur = providers.length > 0 ? (providers.find((p) => p.id === selId) || providers[0]) : null;
86
97
  // 选中 Provider 或目录刷新时重建草稿(render 期受控重置;msg 只在切 Provider/编辑时清)
@@ -199,7 +210,7 @@ function ModelsView() {
199
210
  DKM_MODALITIES.map(([v, label]) => react.createElement("label", {
200
211
  key: v,
201
212
  className: "dkm-check",
202
- title: v === "image" ? "勾选=端点原生支持图片(原图直发,图片理解代理跳过);纯文本模型勿勾——收图会自动交给图片理解代理识别" : null
213
+ title: v === "image" ? "由你声明该端点支持图片:保存后原图直发,并可作为视觉模型候选。系统不会替你猜测;端点实际不支持时取消勾选即可恢复走图片理解代理" : null
203
214
  },
204
215
  react.createElement("input", {
205
216
  type: "checkbox",
@@ -251,6 +262,11 @@ function ModelsView() {
251
262
  react.createElement("button", { type: "button", className: "dkm-mini", title: "一键取消全部强度档", onClick: () => patchModel(i, allLevels(false)) }, "取消全选"))));
252
263
 
253
264
  const body = [];
265
+ if (data) {
266
+ body.push(react.createElement("div", { key: "refresh", className: "dkm-topbar" },
267
+ react.createElement("span", { className: "dkm-sub" }, "已载入 " + providers.length + " 个 Provider;在 DSH 原生模型设置中新增后,返回本页会自动更新"),
268
+ react.createElement("button", { type: "button", className: "dkb-refresh", disabled: snap.loading || saving, onClick: refreshModels }, snap.loading ? "刷新中…" : "刷新模型目录")));
269
+ }
254
270
  if (snap.error) {
255
271
  body.push(react.createElement("div", { key: "err", className: "dkm-error" },
256
272
  "模型目录拉取失败:" + snap.error + " ",
@@ -266,24 +282,38 @@ function ModelsView() {
266
282
  "图片理解代理需要新版宿主进程:当前 dsh web 较旧,重启后此面板可用。"));
267
283
  }
268
284
  if (data && data.visionProxy !== undefined && vpDraft) {
269
- // 图片能力以运行时视角(runtimeInput,与官方投影同源)为准,无则回退条目 input
270
- const effInput = (m) => (m.runtimeInput && m.runtimeInput.length > 0 ? m.runtimeInput : m.input) || [];
285
+ // 用户在模型设置里勾选「图片」即可决定原图直发和视觉候选;
286
+ // 运行时目录仅作自动提示,不能排除用户明确声明的模型。
287
+ // 无论哪种来源都不等同于联网实测,凭据、额度和上游状态仍以真实调用为准。
288
+ const runtimeImage = (m) => Array.isArray(m.runtimeInput) && m.runtimeInput.indexOf("image") >= 0;
289
+ const declaredImage = (m) => Array.isArray(m.input) && m.input.indexOf("image") >= 0;
271
290
  const vpDirect = [];
272
291
  const vpProxied = [];
273
292
  for (const p of providers) {
274
293
  for (const m of (p.models || [])) {
275
- const item = { key: p.id + "/" + m.id, label: (p.displayName || p.id) + " / " + (m.name || m.id), model: m.name || m.id };
276
- (effInput(m).indexOf("image") >= 0 ? vpDirect : vpProxied).push(item);
294
+ const declared = declaredImage(m);
295
+ const runtime = runtimeImage(m);
296
+ const source = declared ? "你已标记图片" : (runtime ? "运行时识别" : "未标记图片");
297
+ const item = {
298
+ key: p.id + "/" + m.id,
299
+ label: (p.displayName || p.id) + " / " + (m.name || m.id) + "(" + source + ")",
300
+ model: m.name || m.id,
301
+ source: source,
302
+ declared: declared,
303
+ };
304
+ ((declared || runtime) ? vpDirect : vpProxied).push(item);
277
305
  }
278
306
  }
279
307
  const vpCandidates = vpDirect.slice();
280
308
  const vpKey = vpDraft.provider + "/" + vpDraft.model;
281
309
  const vpKnown = vpCandidates.some((c) => c.key === vpKey);
282
310
  body.push(react.createElement("div", { key: "visionproxy", className: "dkm-prov" },
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" }, "纯文本模型收到图片时自动调用所选视觉模型识别,识别文本替换图片;多模态模型原样自识别")),
311
+ react.createElement("div", { className: "dkm-prov-head" },
312
+ react.createElement("span", { className: "dkm-name" }, "图片理解代理"),
313
+ react.createElement("span", { className: "dkm-badge" }, vpDraft.enabled ? "已启用" : "已停用"),
314
+ react.createElement("span", { className: "dkm-sub" }, "纯文本模型收到图片时自动调用所选视觉模型识别,识别文本替换图片;多模态模型原样自识别")),
315
+ react.createElement("div", { className: "dkm-note dkm-warn" },
316
+ "⚠ 本功能会调用视觉模型识别图片,消耗 token(每次识图按视觉模型计费)。默认关闭;不开启则插件其余功能均不消耗任何 token。"),
287
317
  react.createElement("div", { className: "dkm-checks" },
288
318
  react.createElement("label", { className: "dkm-check" },
289
319
  react.createElement("input", {
@@ -307,18 +337,28 @@ function ModelsView() {
307
337
  !vpKnown && vpDraft.provider ? react.createElement("option", { value: vpKey }, vpDraft.provider + " / " + vpDraft.model + "(当前)") : null,
308
338
  vpCandidates.map((c) => react.createElement("option", { key: c.key, value: c.key }, c.label))),
309
339
  vpCandidates.length === 0
310
- ? react.createElement("span", { className: "dkm-sub" }, "目录里暂无多模态模型——先在下方给真·多模态模型勾选「图片」输入类型")
340
+ ? react.createElement("span", { className: "dkm-sub" }, "还没有标记图片能力的模型——在下方给你确认支持图片的模型勾选「图片」并保存")
311
341
  : null),
312
342
  react.createElement("div", { className: "dkm-sub" },
313
- "判定与官方运行时同源:「图片」勾选(或目录默认)= 端点原生支持、原图直发;其余模型收图自动走视觉模型。注意:若端点实际不认图却勾了「图片」,模型会看不见图片、转而用工具瞎折腾——不确定就不要勾,交给代理。"),
343
+ "以你在下方勾选的「图片」为准;运行时识别只是辅助提示。它不代表已联网实测可调用,实际可用性仍取决于 Provider 凭据、额度与上游服务。若端点实际不认图,取消该模型的「图片」勾选即可恢复走图片理解代理。"),
314
344
  vpDirect.length > 0
315
- ? react.createElement("div", { className: "dkm-checks" },
316
- react.createElement("span", { className: "dkm-label" }, "原图直发(多模态," + vpDirect.length + "):"),
317
- vpDirect.map((c) => react.createElement("span", { key: c.key, className: "dkm-chip", title: c.key }, c.model)))
345
+ ? react.createElement("div", { className: "dkm-capabilities" },
346
+ react.createElement("div", { className: "dkm-checks" },
347
+ react.createElement("span", { className: "dkm-label" }, "原图直发候选(你的标记优先," + vpDirect.length + "):"),
348
+ react.createElement("button", {
349
+ type: "button",
350
+ className: "dkm-mini",
351
+ "aria-expanded": vpDirectExpanded,
352
+ onClick: () => setVpDirectExpanded(!vpDirectExpanded)
353
+ }, vpDirectExpanded ? "收起列表" : "展开列表")),
354
+ vpDirectExpanded
355
+ ? react.createElement("div", { className: "dkm-checks dkm-capability-list" },
356
+ vpDirect.map((c) => react.createElement("span", { key: c.key, className: "dkm-chip", title: c.key + " · " + c.source }, c.model)))
357
+ : null)
318
358
  : null,
319
359
  vpProxied.length > 0
320
360
  ? react.createElement("div", { className: "dkm-checks" },
321
- react.createElement("span", { className: "dkm-label" }, "走视觉代理(纯文本," + vpProxied.length + "):"),
361
+ react.createElement("span", { className: "dkm-label" }, "走视觉代理(未标记图片," + vpProxied.length + "):"),
322
362
  react.createElement("span", { className: "dkm-sub" }, vpProxied.map((c) => c.model).join("、")))
323
363
  : null,
324
364
  react.createElement("div", { className: "dkm-savebar" },
@@ -377,7 +417,7 @@ function ModelsView() {
377
417
  react.createElement("div", { className: "dkm-savebar" },
378
418
  react.createElement("span", { className: "dkm-msg" }, dirty ? "有未保存的修改" : "无未保存修改"),
379
419
  react.createElement("button", { type: "button", className: "dkm-save", disabled: !dirty || saving, onClick: save }, saving ? "保存中…" : "保存写回官方配置"),
380
- react.createElement("button", { type: "button", className: "dkb-refresh", disabled: saving, onClick: () => { setBuiltKey(null); modelsStore.load(); } }, "重新拉取"),
420
+ react.createElement("button", { type: "button", className: "dkb-refresh", disabled: saving || snap.loading, onClick: refreshModels }, snap.loading ? "刷新中…" : "重新拉取"),
381
421
  msg ? react.createElement("span", { className: "dkm-msg " + (msg.ok ? "ok" : "err") }, msg.text) : null)));
382
422
  }
383
423
  return react.createElement("div", { className: "dkm-list" }, body);
@@ -401,6 +441,7 @@ export const feature = {
401
441
  description: "编辑各 Provider 模型目录:输入类型(文本/图片 + 标注)与思考强度档位;写回官方配置热生效,会话模型选择器即时可选",
402
442
  css: [
403
443
  ".dkm-note{color:var(--dsw-alias-label-secondary);font-size:12px;}",
444
+ ".dkm-warn{color:var(--dsw-alias-state-warning-primary);}",
404
445
  ".dkm-error{color:var(--dsw-alias-state-error-primary);font-size:12px;}",
405
446
  ".dkm-chips{display:flex;flex-wrap:wrap;gap:6px;}",
406
447
  ".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;}",
@@ -435,6 +476,9 @@ export const feature = {
435
476
  ".dkm-msg.ok{color:var(--dsw-alias-state-success-primary);}",
436
477
  ".dkm-msg.err{color:var(--dsw-alias-state-error-primary);}",
437
478
  ".dkm-list{display:flex;flex-direction:column;gap:8px;}",
479
+ ".dkm-topbar{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;}",
480
+ ".dkm-capabilities{display:flex;flex-direction:column;gap:6px;}",
481
+ ".dkm-capability-list{max-height:168px;overflow:auto;padding:2px 0 2px 2px;}",
438
482
  ".dkm-ro{color:var(--dsw-alias-label-secondary);font-size:12px;}",
439
483
  ".dkm-mini{cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-family:inherit;font-size:11px;flex:none;}",
440
484
  ".dkm-mini:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-secondary);}",
@@ -1,27 +1,52 @@
1
1
  // dsh-dock · 功能模块【主题信息】· 客户端视图(纯 Client 功能,无宿主半部)
2
2
  import react from "react";
3
+ import { useEffect, useState } from "react";
3
4
 
4
- function readThemeLabel(ctx) {
5
+ // dsh 客户端主题服务(@deepseek-ai/dsh-client-ui-theme 的 ctx.provide("theme", ...))快照结构:
6
+ // { preference: 'light'|'dark'|'system', active: { id, colorScheme, tokens }, themes, revision }
7
+ // preference 是用户偏好;active.id 是解析后的实际生效主题(preference 为 system 时落到 light/dark)。
8
+ // 注意:快照顶层没有 id/name 字段——直接读 snap.id 只会得到 undefined(曾导致本页显示"未知")。
9
+ const PREFERENCE_LABELS = { light: "亮色", dark: "暗色", system: "跟随系统" };
10
+ const THEME_LABELS = { light: "亮色", dark: "暗色" };
11
+
12
+ function readThemeInfo(ctx) {
5
13
  const theme = ctx && ctx.get ? ctx.get("theme") : undefined;
6
- if (theme === undefined) return { err: "theme 服务不可用" };
14
+ if (!theme || typeof theme.getTheme !== "function") return { err: "theme 服务不可用" };
7
15
  let snap = null;
8
- try { snap = theme.getTheme(); }
9
- catch (err) { return { err: "读取失败:" + String((err && err.message) || err) }; }
10
- const label = snap && typeof snap.id === "string" ? snap.id
11
- : snap && typeof snap.name === "string" ? snap.name : "未知";
16
+ try { snap = theme.getTheme(); } catch (err) { return { err: "读取失败:" + String((err && err.message) || err) }; }
17
+ const preference = snap && typeof snap.preference === "string" ? snap.preference : "";
18
+ const activeId = snap && snap.active && typeof snap.active.id === "string" ? snap.active.id : "";
19
+ // 偏好 → 中文;system 且实际主题与偏好不同名时补注实际生效值(跟随系统 = 亮色/暗色)
20
+ const label = (PREFERENCE_LABELS[preference] || preference || "未知")
21
+ + (activeId && activeId !== preference && THEME_LABELS[activeId] ? "(实际" + THEME_LABELS[activeId] + ")" : "");
12
22
  return { label };
13
23
  }
14
24
 
25
+ function useThemeInfo(ctx) {
26
+ const [info, setInfo] = useState(() => readThemeInfo(ctx));
27
+ useEffect(() => {
28
+ // 设置页「外观」行切换主题时实时刷新(快照 revision 递增,theme/change 事件广播)
29
+ let off = null;
30
+ try {
31
+ if (ctx && typeof ctx.on === "function") {
32
+ off = ctx.on("theme/change", () => setInfo(readThemeInfo(ctx)));
33
+ }
34
+ } catch { /* 无订阅能力时降级为静态显示 */ }
35
+ return () => { try { if (typeof off === "function") off(); } catch { /* 清理失败静默 */ } };
36
+ }, [ctx]);
37
+ return info;
38
+ }
39
+
15
40
  function ThemeView(props) {
16
- const r = readThemeLabel(props && props.ctx);
41
+ const r = useThemeInfo(props && props.ctx);
17
42
  if (r.err) return react.createElement("div", null, r.err);
18
43
  return react.createElement("div", null, "当前主题:" + r.label);
19
44
  }
20
45
 
21
46
  function ThemeStat(props) {
22
- const r = readThemeLabel(props && props.ctx);
47
+ const r = useThemeInfo(props && props.ctx);
23
48
  if (r.err) return react.createElement("span", null, r.err);
24
- return react.createElement("span", null, "当前主题:" + r.label);
49
+ return react.createElement("span", null, "主题:" + r.label);
25
50
  }
26
51
 
27
52
  export const feature = {
@@ -29,7 +54,7 @@ export const feature = {
29
54
  name: "主题信息",
30
55
  order: 150,
31
56
  accent: "#a78bfa",
32
- description: "示例功能:读取当前主题快照(纯 Client)",
57
+ description: "读取当前主题快照:偏好与实际生效主题(纯 Client,主题切换实时刷新)",
33
58
  View: ThemeView,
34
59
  HomeStat: ThemeStat,
35
60
  };
@@ -540,9 +540,11 @@ export function TokenLogChip(props) {
540
540
  }, [sid]);
541
541
  if (!sid) return null;
542
542
  const t = snap.totals;
543
+ // chip 文案用紧凑金额(2 位小数):fmtCost 对小金额输出 6 位小数,chip 里太长会撑爆输入卡工具行;
544
+ // 完整金额在 title 悬浮提示里。
543
545
  const label = snap.err && !t
544
546
  ? (snap.err.includes("重启") ? "用量·需重启宿主" : "用量·失败")
545
- : (t ? "⛁ " + fmtCompact(t.totalTokens) + (t.cost > 0 ? " · " + fmtCost(t.cost) : "") : "⛁ …");
547
+ : (t ? "⛁ " + fmtCompact(t.totalTokens) + (t.cost > 0 ? " · $" + t.cost.toFixed(2) : "") : "⛁ …");
546
548
  const title = t
547
549
  ? "本会话 " + fmtNum(t.calls) + " 次调用 · " + fmtNum(t.totalTokens) + " Token · " + fmtCost(t.cost) + "(估算)\n点击在功能坞查看用量记录"
548
550
  : (snap.err ? snap.err + "\n" : "") + "点击在功能坞查看用量记录";
@@ -20,6 +20,17 @@ function contentHasImageDeep(content) {
20
20
  return false
21
21
  }
22
22
 
23
+ /**
24
+ * 用户在 dsh-dock 模型设置里勾选「图片」后的声明。
25
+ * 这不是联网探测结果,而是明确让用户决定端点能力:声明后原图直发,
26
+ * 未声明时才继续参考运行时目录或走图片理解代理。
27
+ */
28
+ function hasUserDeclaredImage(directory, provider, model) {
29
+ const p = (directory.providers || []).find((x) => x && x.id === provider)
30
+ const m = p && Array.isArray(p.models) ? p.models.find((x) => x && x.id === model) : undefined
31
+ return !!(m && Array.isArray(m.input) && m.input.includes('image'))
32
+ }
33
+
23
34
  /** 让视觉模型描述一张图片,返回识别文本(失败抛错)。 */
24
35
  async function describeImage(deps, cfg, block, signal) {
25
36
  const prompt = '请用中文详细描述这张图片的内容:主体、文字(若有则逐字转写)、数据/图表要点、与编码任务相关的细节。控制在 300 字以内,直接输出描述,不要客套。'
@@ -127,10 +138,19 @@ async function transformForVisionProxy(deps, options) {
127
138
  if (!options.messages.some((m) => m && contentHasImageDeep(m.content))) return options
128
139
  log(`带图请求 ${options.provider}/${options.model}`)
129
140
 
130
- // 目标模型是否支持图片:优先问运行时(llm.resolveModelInfo,与官方投影同源:
131
- // entry.input → 内置目录 → 路由默认),避免插件目录误判"靠内置目录继承多模态"的
132
- // 模型(如 kimi-k2.7,条目未写 input 但目录里是 [text,image]);
133
- // 旧宿主无此方法或单次解析失败时,退回插件目录视图兜底。
141
+ // 图片能力判定的优先级:用户在模型设置里勾选「图片」优先。
142
+ // 运行时目录只是自动提示,无法识别或识别错误时不能覆盖用户的明确决定。
143
+ // 这让 kimi-k2.7-code 等端点可由用户声明多模态并原图直发;若端点实际不支持,
144
+ // 上游会返回错误,用户取消勾选即可恢复走图片理解代理。
145
+ const directory = deps.directory()
146
+ if (hasUserDeclaredImage(directory, options.provider, options.model)) {
147
+ log(`跳过 ${options.provider}/${options.model}:用户已声明图片能力(原图直发)`)
148
+ return options
149
+ }
150
+
151
+ // 未声明时再问运行时(llm.resolveModelInfo,与官方投影同源:
152
+ // entry.input → 内置目录 → 路由默认);旧宿主无此方法或单次解析失败时,
153
+ // 保守走图片理解代理,避免猜错端点能力。
134
154
  let imageCapable
135
155
  try {
136
156
  const info = deps.resolveModelInfo ? await deps.resolveModelInfo(options.provider, options.model) : undefined
@@ -138,12 +158,6 @@ async function transformForVisionProxy(deps, options) {
138
158
  } catch {
139
159
  // 落到目录回退
140
160
  }
141
- if (imageCapable === undefined) {
142
- const dir = deps.directory()
143
- const p = (dir.providers || []).find((x) => x.id === options.provider)
144
- const m = p && Array.isArray(p.models) ? p.models.find((x) => x.id === options.model) : undefined
145
- imageCapable = !!(m && Array.isArray(m.input) && m.input.includes('image'))
146
- }
147
161
  if (imageCapable) {
148
162
  log(`跳过 ${options.provider}/${options.model}:运行时判定多模态(原图直发)`)
149
163
  return options
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 任务动画(路线图 0.5.0 占位)
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
- // 目前 Host↔Client 的开关同步在 0.5.0(持久化 + 双侧注册表打通)落地,
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.1",
4
- "description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计)、模型设置、图片理解代理、模型余额;侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
3
+ "version": "0.6.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