dsh-dock 0.9.5 → 0.9.7

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.
Files changed (35) hide show
  1. package/LICENSE +18 -18
  2. package/README.md +325 -325
  3. package/client.js +42 -8
  4. package/cordis.patch.yml +2 -2
  5. package/features/animation/host.js +660 -660
  6. package/features/animation/view.jsx +2247 -2247
  7. package/features/balance/host.js +494 -494
  8. package/features/balance/view.js +351 -351
  9. package/features/games/games/breakout.jsx +227 -227
  10. package/features/games/games/gomoku.jsx +244 -244
  11. package/features/games/games/racer.jsx +169 -169
  12. package/features/games/games/shared.jsx +118 -118
  13. package/features/games/games/snake.jsx +144 -144
  14. package/features/games/games/sokoban.jsx +180 -180
  15. package/features/games/games/tank.jsx +128 -128
  16. package/features/games/games/tetris.jsx +308 -308
  17. package/features/games/games/xiangqi.jsx +277 -277
  18. package/features/games/tank-core.js +152 -152
  19. package/features/games/view.jsx +639 -639
  20. package/features/games/xiangqi-core.js +299 -299
  21. package/features/heartbeat/view.js +35 -35
  22. package/features/mobile-relay/gateway.js +472 -415
  23. package/features/mobile-relay/host.js +570 -546
  24. package/features/mobile-relay/view.jsx +300 -300
  25. package/features/modelconfig/host.js +403 -403
  26. package/features/modelconfig/view.js +490 -490
  27. package/features/theme/view.js +61 -61
  28. package/features/tokenlog/host.js +678 -678
  29. package/features/tokenlog/view.jsx +561 -561
  30. package/features/visionproxy/host.js +315 -315
  31. package/index.js +175 -118
  32. package/package.json +59 -59
  33. package/src/client.jsx +668 -668
  34. package/src/host-core.js +98 -95
  35. package/src/shared.js +167 -130
@@ -1,490 +1,490 @@
1
- // dsh-dock · 功能模块【模型设置】· 客户端视图(v0.3.0 迁自 client.js,行为不变)
2
- // modelconfig 视图经 /dsh-dock/models(GET 目录 / POST 写回)编辑各 Provider 模型的
3
- // 输入类型与思考强度档位,写回官方 settings 热生效。图片理解代理(visionproxy 宿主功能)
4
- // 的全局配置面板也渲染在本页顶部。
5
- import react from "react";
6
-
7
- // ---- 模型设置共享快照:视图与首页总揽共用一份目录数据(手动刷新,不轮询) ----
8
- const modelsStore = {
9
- snap: { data: null, loading: false, error: null },
10
- listeners: new Set(),
11
- subscribe(fn) { this.listeners.add(fn); return () => { this.listeners.delete(fn); }; },
12
- set(patch) { this.snap = Object.assign({}, this.snap, patch); for (const fn of this.listeners) fn(); },
13
- load() {
14
- if (this.snap.loading) return;
15
- this.set({ loading: true, error: null });
16
- // Provider 可在 DSH 原生模型设置中随时增删;不要复用浏览器缓存,
17
- // 否则返回本页时仍可能看到旧的共享快照。
18
- fetch("/dsh-dock/models", { cache: "no-store", signal: AbortSignal.timeout(20000) })
19
- .then((res) => (res.ok ? res.json() : Promise.reject(new Error("模型目录接口 HTTP " + res.status))))
20
- .then((data) => this.set({ data: data, loading: false, error: null }))
21
- .catch((e) => this.set({ loading: false, error: (e && e.message) || String(e) }));
22
- }
23
- };
24
- function useModels() {
25
- const [snap, setSnap] = react.useState(modelsStore.snap);
26
- react.useEffect(() => {
27
- const off = modelsStore.subscribe(() => setSnap(modelsStore.snap));
28
- if (!modelsStore.snap.data && !modelsStore.snap.loading) modelsStore.load();
29
- return off;
30
- }, []);
31
- return snap;
32
- }
33
-
34
- // ---- 编辑常量与草稿工具 ----
35
- // 官方 schema 接受的真实输入模态;其余(视频等)仅作为 dockTags 标注随配置持久化
36
- const DKM_MODALITIES = [["text", "文本"], ["image", "图片"]];
37
- const DKM_TAGS = [["video", "视频"], ["audio", "音频"], ["document", "文档"]];
38
- const DKM_PI_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
39
- const DKM_DS_LEVELS = ["off", "low", "high", "max"];
40
- const DKM_EFFORT_NAMES = { off: "关闭", minimal: "极低", low: "低", medium: "中", high: "高", xhigh: "超高", max: "最高" };
41
-
42
- /** 从目录 Provider 视图构建可编辑草稿。 */
43
- function draftFromProvider(p) {
44
- return {
45
- id: p.id,
46
- kind: p.kind,
47
- thinking: p.thinking === "disabled" ? "disabled" : "enabled",
48
- defaultEffort: DKM_DS_LEVELS.indexOf(p.defaultEffort) >= 0 ? p.defaultEffort : "high",
49
- models: (p.models || []).map((m) => {
50
- // effortMap: {档位: wire},声明过(wire 字符串或 null)即视为勾选,undefined = 未提供
51
- const levels = {};
52
- if (m.efforts === "custom" && m.effortMap && typeof m.effortMap === "object") {
53
- for (const lv of DKM_PI_LEVELS) levels[lv] = m.effortMap[lv] !== undefined;
54
- }
55
- return {
56
- id: m.id || "",
57
- name: m.name || "",
58
- contextWindow: m.contextWindow != null ? String(m.contextWindow) : "",
59
- maxTokens: m.maxTokens != null ? String(m.maxTokens) : "",
60
- input: m.input && m.input.length ? m.input.slice() : ["text"],
61
- tags: (m.tags || []).slice(),
62
- effortsMode: m.efforts === "custom" ? "custom" : m.efforts === "off" ? "off" : "inherit",
63
- effortLevels: levels,
64
- raw: m.raw && typeof m.raw === "object" ? m.raw : null
65
- };
66
- })
67
- };
68
- }
69
-
70
- function ModelsView() {
71
- const snap = useModels();
72
- const data = snap.data;
73
- const providers = data && Array.isArray(data.providers) ? data.providers : [];
74
- const [selId, setSelId] = react.useState(null);
75
- const [draft, setDraft] = react.useState(null);
76
- const [builtKey, setBuiltKey] = react.useState(null);
77
- const [dirty, setDirty] = react.useState(false);
78
- const [saving, setSaving] = react.useState(false);
79
- const [msg, setMsg] = react.useState(null);
80
- // 图片理解代理(全局配置,独立于 Provider 草稿)
81
- const [vpDraft, setVpDraft] = react.useState(null);
82
- const [vpBuilt, setVpBuilt] = react.useState(null);
83
- const [vpDirty, setVpDirty] = react.useState(false);
84
- const [vpSaving, setVpSaving] = react.useState(false);
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
- };
95
-
96
- const cur = providers.length > 0 ? (providers.find((p) => p.id === selId) || providers[0]) : null;
97
- // 选中 Provider 或目录刷新时重建草稿(render 期受控重置;msg 只在切 Provider/编辑时清)
98
- const wantKey = cur ? cur.id + ":" + String(data && data.generatedAt || 0) : null;
99
- if (cur && builtKey !== wantKey) {
100
- setBuiltKey(wantKey);
101
- setDraft(draftFromProvider(cur));
102
- setDirty(false);
103
- }
104
- // 图片代理草稿随目录刷新重建(保存成功 → load() → 新 generatedAt → 重建为已保存态,msg 保留)
105
- const vpWantKey = data ? String(data.generatedAt) : null;
106
- if (data && vpBuilt !== vpWantKey) {
107
- setVpBuilt(vpWantKey);
108
- const vp = data.visionProxy || {};
109
- setVpDraft({ enabled: !!vp.enabled, provider: String(vp.provider || ""), model: String(vp.model || "") });
110
- setVpDirty(false);
111
- }
112
- const saveVp = () => {
113
- if (!vpDraft || vpSaving) return;
114
- setVpSaving(true);
115
- fetch("/dsh-dock/models", {
116
- method: "POST",
117
- headers: { "content-type": "application/json" },
118
- body: JSON.stringify({ visionProxy: vpDraft, revisions: (data && data.revisions) || {} }),
119
- signal: AbortSignal.timeout(20000)
120
- })
121
- .then((res) => res.json()
122
- .then((b2) => ({ status: res.status, body: b2 }))
123
- .catch(() => ({ status: res.status, body: {} })))
124
- .then((r2) => {
125
- if (r2.status === 200 && r2.body && r2.body.ok) {
126
- setVpMsg({ ok: true, text: "图片代理配置已保存,即时生效" });
127
- modelsStore.load();
128
- } else {
129
- setVpMsg({ ok: false, text: (r2.body && r2.body.error) || ("HTTP " + r2.status) });
130
- }
131
- })
132
- .catch((e) => setVpMsg({ ok: false, text: (e && e.message) || String(e) }))
133
- .then(() => setVpSaving(false));
134
- };
135
-
136
- const patchDraft = (patch) => { setDraft(Object.assign({}, draft, patch)); setDirty(true); setMsg(null); };
137
- const patchModel = (i, patch) => {
138
- const models = draft.models.slice();
139
- models[i] = Object.assign({}, models[i], patch);
140
- patchDraft({ models: models });
141
- };
142
- const toggleIn = (list, v) => list.indexOf(v) >= 0 ? list.filter((x) => x !== v) : list.concat([v]);
143
- // 一键批量:输入支持(官方模态 + 标注)与强度档。输入留空 = 继承目录默认(Host 写回时删除 input 字段,安全)
144
- const ALL_INPUT = DKM_MODALITIES.map((x) => x[0]);
145
- const ALL_TAGS = DKM_TAGS.map((x) => x[0]);
146
- const allInputs = (on) => ({ input: on ? ALL_INPUT.slice() : [], tags: on ? ALL_TAGS.slice() : [] });
147
- const allLevels = (on) => {
148
- const lv = {};
149
- for (const k of DKM_PI_LEVELS) lv[k] = !!on;
150
- return { effortsMode: "custom", effortLevels: lv };
151
- };
152
- const bulkModels = (fn) => patchDraft({ models: draft.models.map((m) => Object.assign({}, m, fn(m))) });
153
- const selectProvider = (id) => { setSelId(id); setMsg(null); };
154
-
155
- const save = () => {
156
- if (!draft || !cur || !dirty || saving) return;
157
- const models = draft.models.map((m) => ({
158
- id: String(m.id || "").trim(),
159
- name: String(m.name || "").trim(),
160
- contextWindow: m.contextWindow === "" ? undefined : Number(m.contextWindow),
161
- maxTokens: m.maxTokens === "" ? undefined : Number(m.maxTokens),
162
- input: m.input,
163
- tags: m.tags,
164
- effortsMode: m.effortsMode,
165
- effortLevels: m.effortLevels,
166
- raw: m.raw
167
- }));
168
- const payload = { provider: cur.id, revisions: (data && data.revisions) || {}, models: models };
169
- if (cur.kind === "deepseek") {
170
- payload.thinking = draft.thinking;
171
- payload.defaultEffort = draft.defaultEffort;
172
- }
173
- setSaving(true);
174
- fetch("/dsh-dock/models", {
175
- method: "POST",
176
- headers: { "content-type": "application/json" },
177
- body: JSON.stringify(payload),
178
- signal: AbortSignal.timeout(20000)
179
- })
180
- .then((res) => res.json()
181
- .then((body2) => ({ status: res.status, body: body2 }))
182
- .catch(() => ({ status: res.status, body: {} })))
183
- .then((r2) => {
184
- if (r2.status === 200 && r2.body && r2.body.ok) {
185
- setMsg({ ok: true, text: "已写回官方配置;重新打开会话模型选择器即可选择新模型/强度档" });
186
- modelsStore.load();
187
- } else {
188
- setMsg({ ok: false, text: (r2.body && r2.body.error) || ("HTTP " + r2.status) });
189
- }
190
- })
191
- .catch((e) => setMsg({ ok: false, text: (e && e.message) || String(e) }))
192
- .then(() => setSaving(false));
193
- };
194
-
195
- const modelRow = (m, i) => react.createElement("div", { key: i, className: "dkm-model" },
196
- react.createElement("div", { className: "dkm-model-head" },
197
- react.createElement("span", { className: "dkm-model-idx" }, "#" + (i + 1)),
198
- react.createElement("button", {
199
- type: "button",
200
- className: "dkm-del",
201
- onClick: () => patchDraft({ models: draft.models.filter((_, j) => j !== i) })
202
- }, "删除")),
203
- react.createElement("div", { className: "dkm-grid" },
204
- react.createElement("input", { className: "dkm-input", placeholder: "模型 id(必填,如 deepseek-v4-pro)", value: m.id, onChange: (e) => patchModel(i, { id: e.target.value }) }),
205
- react.createElement("input", { className: "dkm-input", placeholder: "显示名称(可选)", value: m.name, onChange: (e) => patchModel(i, { name: e.target.value }) }),
206
- react.createElement("input", { className: "dkm-input", placeholder: "上下文窗口", inputMode: "numeric", value: m.contextWindow, onChange: (e) => patchModel(i, { contextWindow: e.target.value }) }),
207
- react.createElement("input", { className: "dkm-input", placeholder: "最大输出", inputMode: "numeric", value: m.maxTokens, onChange: (e) => patchModel(i, { maxTokens: e.target.value }) })),
208
- react.createElement("div", { className: "dkm-checks" },
209
- react.createElement("span", { className: "dkm-label" }, "输入类型:"),
210
- DKM_MODALITIES.map(([v, label]) => react.createElement("label", {
211
- key: v,
212
- className: "dkm-check",
213
- title: v === "image" ? "由你声明该端点支持图片:保存后原图直发,并可作为视觉模型候选。系统不会替你猜测;端点实际不支持时取消勾选即可恢复走图片理解代理" : null
214
- },
215
- react.createElement("input", {
216
- type: "checkbox",
217
- checked: m.input.indexOf(v) >= 0,
218
- onChange: () => {
219
- const next = toggleIn(m.input, v);
220
- patchModel(i, { input: next.length > 0 ? next : ["text"] });
221
- }
222
- }),
223
- label)),
224
- react.createElement("span", { className: "dkm-label dkm-tag", title: "标注仅在本面板展示与持久化,不参与请求路由(官方 schema 只接受文本/图片)" }, "标注:"),
225
- DKM_TAGS.map(([v, label]) => react.createElement("label", { key: v, className: "dkm-check dkm-tag" },
226
- react.createElement("input", { type: "checkbox", checked: m.tags.indexOf(v) >= 0, onChange: () => patchModel(i, { tags: toggleIn(m.tags, v) }) }),
227
- label)),
228
- react.createElement("button", { type: "button", className: "dkm-mini", title: "一键勾选全部输入类型与标注", onClick: () => patchModel(i, allInputs(true)) }, "全选"),
229
- react.createElement("button", { type: "button", className: "dkm-mini", title: "一键取消全部勾选(输入留空 = 继承目录默认)", onClick: () => patchModel(i, allInputs(false)) }, "取消全选")),
230
- react.createElement("div", { className: "dkm-efforts" },
231
- react.createElement("span", { className: "dkm-label" }, "思考强度:"),
232
- cur.kind === "deepseek"
233
- ? react.createElement("span", { className: "dkm-sub" }, "随 Provider 设置(Off/Low/High/Max,全模型共享)")
234
- : react.createElement("span", null,
235
- react.createElement("select", {
236
- className: "dkm-select",
237
- value: m.effortsMode,
238
- onChange: (e) => patchModel(i, {
239
- effortsMode: e.target.value,
240
- effortLevels: e.target.value === "custom" && m.effortsMode !== "custom"
241
- ? { off: true, low: true, high: true }
242
- : m.effortLevels
243
- })
244
- },
245
- react.createElement("option", { value: "inherit" }, "跟随目录默认"),
246
- react.createElement("option", { value: "off" }, "不支持思考"),
247
- react.createElement("option", { value: "custom" }, "自定义档位")),
248
- m.effortsMode === "custom"
249
- ? DKM_PI_LEVELS.map((lv) => react.createElement("label", { key: lv, className: "dkm-check" },
250
- react.createElement("input", {
251
- type: "checkbox",
252
- checked: !!m.effortLevels[lv],
253
- onChange: () => {
254
- const nl = Object.assign({}, m.effortLevels);
255
- nl[lv] = !nl[lv];
256
- patchModel(i, { effortLevels: nl });
257
- }
258
- }),
259
- DKM_EFFORT_NAMES[lv] || lv))
260
- : null,
261
- react.createElement("button", { type: "button", className: "dkm-mini", title: "一键勾选全部强度档(切为自定义)", onClick: () => patchModel(i, allLevels(true)) }, "全选"),
262
- react.createElement("button", { type: "button", className: "dkm-mini", title: "一键取消全部强度档", onClick: () => patchModel(i, allLevels(false)) }, "取消全选"))));
263
-
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
- }
270
- if (snap.error) {
271
- body.push(react.createElement("div", { key: "err", className: "dkm-error" },
272
- "模型目录拉取失败:" + snap.error + " ",
273
- react.createElement("button", { type: "button", className: "dkb-refresh", onClick: () => modelsStore.load() }, "重试")));
274
- } else if (!data) {
275
- body.push(react.createElement("div", { key: "loading", className: "dkm-note" }, snap.loading ? "正在拉取模型目录…" : "等待拉取模型目录"));
276
- }
277
-
278
- // ---- 图片理解代理(全局配置):纯文本模型收图自动走视觉模型识别 ----
279
- // 宿主较旧(GET 无 visionProxy 字段)时降级为提示,避免保存撞到旧写回分支
280
- if (data && data.visionProxy === undefined) {
281
- body.push(react.createElement("div", { key: "visionproxy-old", className: "dkm-note" },
282
- "图片理解代理需要新版宿主进程:当前 dsh web 较旧,重启后此面板可用。"));
283
- }
284
- if (data && data.visionProxy !== undefined && vpDraft) {
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;
290
- const vpDirect = [];
291
- const vpProxied = [];
292
- for (const p of providers) {
293
- for (const m of (p.models || [])) {
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);
305
- }
306
- }
307
- const vpCandidates = vpDirect.slice();
308
- const vpKey = vpDraft.provider + "/" + vpDraft.model;
309
- const vpKnown = vpCandidates.some((c) => c.key === vpKey);
310
- body.push(react.createElement("div", { key: "visionproxy", className: "dkm-prov" },
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。"),
317
- react.createElement("div", { className: "dkm-checks" },
318
- react.createElement("label", { className: "dkm-check" },
319
- react.createElement("input", {
320
- type: "checkbox",
321
- checked: vpDraft.enabled,
322
- onChange: () => { setVpDraft(Object.assign({}, vpDraft, { enabled: !vpDraft.enabled })); setVpDirty(true); setVpMsg(null); }
323
- }),
324
- "启用"),
325
- react.createElement("span", { className: "dkm-label" }, "视觉模型:"),
326
- react.createElement("select", {
327
- className: "dkm-select",
328
- value: vpKey,
329
- onChange: (e) => {
330
- const idx = e.target.value.indexOf("/");
331
- setVpDraft(Object.assign({}, vpDraft, { provider: e.target.value.slice(0, idx), model: e.target.value.slice(idx + 1) }));
332
- setVpDirty(true);
333
- setVpMsg(null);
334
- }
335
- },
336
- react.createElement("option", { value: "/" }, "(未选择)"),
337
- !vpKnown && vpDraft.provider ? react.createElement("option", { value: vpKey }, vpDraft.provider + " / " + vpDraft.model + "(当前)") : null,
338
- vpCandidates.map((c) => react.createElement("option", { key: c.key, value: c.key }, c.label))),
339
- vpCandidates.length === 0
340
- ? react.createElement("span", { className: "dkm-sub" }, "还没有标记图片能力的模型——在下方给你确认支持图片的模型勾选「图片」并保存")
341
- : null),
342
- react.createElement("div", { className: "dkm-sub" },
343
- "以你在下方勾选的「图片」为准;运行时识别只是辅助提示。它不代表已联网实测可调用,实际可用性仍取决于 Provider 凭据、额度与上游服务。若端点实际不认图,取消该模型的「图片」勾选即可恢复走图片理解代理。"),
344
- vpDirect.length > 0
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)
358
- : null,
359
- vpProxied.length > 0
360
- ? react.createElement("div", { className: "dkm-checks" },
361
- react.createElement("span", { className: "dkm-label" }, "走视觉代理(未标记图片," + vpProxied.length + "):"),
362
- react.createElement("span", { className: "dkm-sub" }, vpProxied.map((c) => c.model).join("、")))
363
- : null,
364
- react.createElement("div", { className: "dkm-savebar" },
365
- react.createElement("button", { type: "button", className: "dkm-save", disabled: !vpDirty || vpSaving, onClick: saveVp }, vpSaving ? "保存中…" : "保存图片代理配置"),
366
- vpMsg ? react.createElement("span", { className: "dkm-msg " + (vpMsg.ok ? "ok" : "err") }, vpMsg.text)
367
- : react.createElement("span", { className: "dkm-msg" }, vpDirty ? "有未保存的修改" : ""))));
368
- }
369
-
370
- if (data && providers.length === 0) {
371
- body.push(react.createElement("div", { key: "empty", className: "dkm-note" }, "没有可编辑的模型 Provider(Host 侧尚未配置)"));
372
- }
373
- if (providers.length > 0) {
374
- body.push(react.createElement("div", { key: "chips", className: "dkm-chips" },
375
- providers.map((p) => react.createElement("button", {
376
- type: "button",
377
- key: p.id,
378
- className: "dkm-chip" + (cur && p.id === cur.id ? " on" : ""),
379
- onClick: () => selectProvider(p.id)
380
- }, p.displayName + "(" + (p.models ? p.models.length : 0) + ")"))));
381
- }
382
- if (cur && draft) {
383
- body.push(react.createElement("div", { key: "prov", className: "dkm-prov" },
384
- react.createElement("div", { className: "dkm-prov-head" },
385
- react.createElement("span", { className: "dkm-name" }, cur.displayName),
386
- react.createElement("span", { className: "dkm-badge" }, cur.kind === "deepseek" ? "官方 DeepSeek" : "自定义路由"),
387
- react.createElement("span", { className: "dkm-sub" }, [cur.api, cur.baseURL].filter(Boolean).join(" · "))),
388
- cur.kind === "deepseek"
389
- ? react.createElement("div", { className: "dkm-controls" },
390
- react.createElement("span", { className: "dkm-label" }, "思考模式:"),
391
- react.createElement("select", { className: "dkm-select", value: draft.thinking, onChange: (e) => patchDraft({ thinking: e.target.value }) },
392
- react.createElement("option", { value: "enabled" }, "开启"),
393
- react.createElement("option", { value: "disabled" }, "关闭")),
394
- react.createElement("span", { className: "dkm-label" }, "默认强度:"),
395
- react.createElement("select", { className: "dkm-select", value: draft.defaultEffort, onChange: (e) => patchDraft({ defaultEffort: e.target.value }) },
396
- DKM_DS_LEVELS.map((lv) => react.createElement("option", { key: lv, value: lv }, DKM_EFFORT_NAMES[lv] || lv))))
397
- : null,
398
- react.createElement("div", { className: "dkm-toolbar" },
399
- react.createElement("span", { className: "dkm-toolbar-label" }, "批量(全部模型):"),
400
- react.createElement("button", { type: "button", className: "dkm-mini", title: "全部模型:勾选全部输入类型与标注", onClick: () => bulkModels(() => allInputs(true)) }, "输入全选"),
401
- react.createElement("button", { type: "button", className: "dkm-mini", title: "全部模型:取消全部输入勾选(留空 = 继承目录默认)", onClick: () => bulkModels(() => allInputs(false)) }, "输入取消全选"),
402
- cur.kind !== "deepseek"
403
- ? react.createElement("button", { type: "button", className: "dkm-mini", title: "全部模型:勾选全部强度档(切为自定义)", onClick: () => bulkModels(() => allLevels(true)) }, "强度全选")
404
- : null,
405
- cur.kind !== "deepseek"
406
- ? react.createElement("button", { type: "button", className: "dkm-mini", title: "全部模型:取消全部强度档", onClick: () => bulkModels(() => allLevels(false)) }, "强度取消全选")
407
- : null),
408
- react.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "8px" } },
409
- draft.models.map(modelRow),
410
- react.createElement("button", {
411
- type: "button",
412
- className: "dkm-add",
413
- onClick: () => patchDraft({
414
- models: draft.models.concat([{ id: "", name: "", contextWindow: "", maxTokens: "", input: ["text"], tags: [], effortsMode: "inherit", effortLevels: {} }])
415
- })
416
- }, "+ 添加模型")),
417
- react.createElement("div", { className: "dkm-savebar" },
418
- react.createElement("span", { className: "dkm-msg" }, dirty ? "有未保存的修改" : "无未保存修改"),
419
- react.createElement("button", { type: "button", className: "dkm-save", disabled: !dirty || saving, onClick: save }, saving ? "保存中…" : "保存写回官方配置"),
420
- react.createElement("button", { type: "button", className: "dkb-refresh", disabled: saving || snap.loading, onClick: refreshModels }, snap.loading ? "刷新中…" : "重新拉取"),
421
- msg ? react.createElement("span", { className: "dkm-msg " + (msg.ok ? "ok" : "err") }, msg.text) : null)));
422
- }
423
- return react.createElement("div", { className: "dkm-list" }, body);
424
- }
425
-
426
- function ModelsStat() {
427
- const snap = useModels();
428
- const data = snap.data;
429
- if (snap.error) return react.createElement("span", { className: "dockm-err" }, "模型目录拉取失败(点击进入查看)");
430
- if (!data) return react.createElement("span", null, snap.loading ? "正在拉取模型目录…" : "等待拉取模型目录");
431
- const providers = Array.isArray(data.providers) ? data.providers : [];
432
- const total = providers.reduce((n, p) => n + (p.models ? p.models.length : 0), 0);
433
- return react.createElement("span", null, providers.length + " 个 Provider · " + total + " 个模型");
434
- }
435
-
436
- export const feature = {
437
- id: "modelconfig",
438
- name: "模型设置",
439
- order: 100,
440
- accent: "#22d3ee",
441
- description: "编辑各 Provider 模型目录:输入类型(文本/图片 + 标注)与思考强度档位;写回官方配置热生效,会话模型选择器即时可选",
442
- css: [
443
- ".dkm-note{color:var(--dsw-alias-label-secondary);font-size:12px;}",
444
- ".dkm-warn{color:var(--dk-warn);}",
445
- ".dkm-error{color:var(--dsw-alias-state-error-primary);font-size:12px;}",
446
- ".dkm-chips{display:flex;flex-wrap:wrap;gap:6px;}",
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;}",
448
- ".dkm-chip.on{color:var(--dk-accent);border-color:currentColor;}",
449
- ".dkm-prov{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px;}",
450
- ".dkm-prov-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
451
- ".dkm-name{font-weight:600;}",
452
- ".dkm-sub{color:var(--dsw-alias-label-tertiary);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;}",
453
- ".dkm-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);}",
454
- ".dkm-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-top:1px solid var(--dsw-alias-border-l1);padding-top:8px;}",
455
- ".dkm-label{color:var(--dsw-alias-label-secondary);font-size:12px;flex:none;}",
456
- ".dkm-select{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:3px 8px;font-family:inherit;font-size:12px;}",
457
- ".dkm-model{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;display:flex;flex-direction:column;gap:6px;}",
458
- ".dkm-model-head{display:flex;align-items:center;gap:6px;}",
459
- ".dkm-model-idx{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none;}",
460
- ".dkm-grid{display:grid;grid-template-columns:1.3fr 1.3fr .8fr .8fr;gap:6px;align-items:center;}",
461
- ".dkm-input{box-sizing:border-box;width:100%;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px 8px;font-family:inherit;font-size:12px;}",
462
- ".dkm-checks{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}",
463
- ".dkm-check{display:inline-flex;align-items:center;gap:5px;cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;}",
464
- ".dkm-check input{accent-color:var(--dk-accent);margin:0;}",
465
- ".dkm-tag{color:var(--dsw-alias-label-tertiary);}",
466
- ".dkm-efforts{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}",
467
- ".dkm-del{margin-left:auto;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:8px;padding:2px 10px;font-family:inherit;font-size:12px;flex:none;}",
468
- ".dkm-del:hover{color:var(--dsw-alias-state-error-primary);border-color:currentColor;}",
469
- ".dkm-add{cursor:pointer;border:1px dashed var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);border-radius:10px;padding:6px 14px;font-family:inherit;font-size:12px;align-self:flex-start;}",
470
- ".dkm-add:hover{color:var(--dsw-alias-label-primary);}",
471
- ".dkm-savebar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px;}",
472
- ".dkm-save{cursor:pointer;border:1px solid var(--dk-accent);color:var(--dk-accent);background:transparent;border-radius:8px;padding:4px 14px;font-family:inherit;font-size:12px;flex:none;}",
473
- ".dkm-save:hover{background:var(--dsw-alias-interactive-bg-hover);}",
474
- ".dkm-save[disabled]{opacity:.5;cursor:default;}",
475
- ".dkm-msg{font-size:12px;color:var(--dsw-alias-label-secondary);}",
476
- ".dkm-msg.ok{color:var(--dsw-alias-state-success-primary);}",
477
- ".dkm-msg.err{color:var(--dsw-alias-state-error-primary);}",
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;}",
482
- ".dkm-ro{color:var(--dsw-alias-label-secondary);font-size:12px;}",
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;}",
484
- ".dkm-mini:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-secondary);}",
485
- ".dkm-toolbar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;border-top:1px dashed var(--dsw-alias-border-l1);padding-top:8px;}",
486
- ".dkm-toolbar-label{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none;}"
487
- ].join("\n"),
488
- View: ModelsView,
489
- HomeStat: ModelsStat,
490
- };
1
+ // dsh-dock · 功能模块【模型设置】· 客户端视图(v0.3.0 迁自 client.js,行为不变)
2
+ // modelconfig 视图经 /dsh-dock/models(GET 目录 / POST 写回)编辑各 Provider 模型的
3
+ // 输入类型与思考强度档位,写回官方 settings 热生效。图片理解代理(visionproxy 宿主功能)
4
+ // 的全局配置面板也渲染在本页顶部。
5
+ import react from "react";
6
+
7
+ // ---- 模型设置共享快照:视图与首页总揽共用一份目录数据(手动刷新,不轮询) ----
8
+ const modelsStore = {
9
+ snap: { data: null, loading: false, error: null },
10
+ listeners: new Set(),
11
+ subscribe(fn) { this.listeners.add(fn); return () => { this.listeners.delete(fn); }; },
12
+ set(patch) { this.snap = Object.assign({}, this.snap, patch); for (const fn of this.listeners) fn(); },
13
+ load() {
14
+ if (this.snap.loading) return;
15
+ this.set({ loading: true, error: null });
16
+ // Provider 可在 DSH 原生模型设置中随时增删;不要复用浏览器缓存,
17
+ // 否则返回本页时仍可能看到旧的共享快照。
18
+ fetch("/dsh-dock/models", { cache: "no-store", signal: AbortSignal.timeout(20000) })
19
+ .then((res) => (res.ok ? res.json() : Promise.reject(new Error("模型目录接口 HTTP " + res.status))))
20
+ .then((data) => this.set({ data: data, loading: false, error: null }))
21
+ .catch((e) => this.set({ loading: false, error: (e && e.message) || String(e) }));
22
+ }
23
+ };
24
+ function useModels() {
25
+ const [snap, setSnap] = react.useState(modelsStore.snap);
26
+ react.useEffect(() => {
27
+ const off = modelsStore.subscribe(() => setSnap(modelsStore.snap));
28
+ if (!modelsStore.snap.data && !modelsStore.snap.loading) modelsStore.load();
29
+ return off;
30
+ }, []);
31
+ return snap;
32
+ }
33
+
34
+ // ---- 编辑常量与草稿工具 ----
35
+ // 官方 schema 接受的真实输入模态;其余(视频等)仅作为 dockTags 标注随配置持久化
36
+ const DKM_MODALITIES = [["text", "文本"], ["image", "图片"]];
37
+ const DKM_TAGS = [["video", "视频"], ["audio", "音频"], ["document", "文档"]];
38
+ const DKM_PI_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
39
+ const DKM_DS_LEVELS = ["off", "low", "high", "max"];
40
+ const DKM_EFFORT_NAMES = { off: "关闭", minimal: "极低", low: "低", medium: "中", high: "高", xhigh: "超高", max: "最高" };
41
+
42
+ /** 从目录 Provider 视图构建可编辑草稿。 */
43
+ function draftFromProvider(p) {
44
+ return {
45
+ id: p.id,
46
+ kind: p.kind,
47
+ thinking: p.thinking === "disabled" ? "disabled" : "enabled",
48
+ defaultEffort: DKM_DS_LEVELS.indexOf(p.defaultEffort) >= 0 ? p.defaultEffort : "high",
49
+ models: (p.models || []).map((m) => {
50
+ // effortMap: {档位: wire},声明过(wire 字符串或 null)即视为勾选,undefined = 未提供
51
+ const levels = {};
52
+ if (m.efforts === "custom" && m.effortMap && typeof m.effortMap === "object") {
53
+ for (const lv of DKM_PI_LEVELS) levels[lv] = m.effortMap[lv] !== undefined;
54
+ }
55
+ return {
56
+ id: m.id || "",
57
+ name: m.name || "",
58
+ contextWindow: m.contextWindow != null ? String(m.contextWindow) : "",
59
+ maxTokens: m.maxTokens != null ? String(m.maxTokens) : "",
60
+ input: m.input && m.input.length ? m.input.slice() : ["text"],
61
+ tags: (m.tags || []).slice(),
62
+ effortsMode: m.efforts === "custom" ? "custom" : m.efforts === "off" ? "off" : "inherit",
63
+ effortLevels: levels,
64
+ raw: m.raw && typeof m.raw === "object" ? m.raw : null
65
+ };
66
+ })
67
+ };
68
+ }
69
+
70
+ function ModelsView() {
71
+ const snap = useModels();
72
+ const data = snap.data;
73
+ const providers = data && Array.isArray(data.providers) ? data.providers : [];
74
+ const [selId, setSelId] = react.useState(null);
75
+ const [draft, setDraft] = react.useState(null);
76
+ const [builtKey, setBuiltKey] = react.useState(null);
77
+ const [dirty, setDirty] = react.useState(false);
78
+ const [saving, setSaving] = react.useState(false);
79
+ const [msg, setMsg] = react.useState(null);
80
+ // 图片理解代理(全局配置,独立于 Provider 草稿)
81
+ const [vpDraft, setVpDraft] = react.useState(null);
82
+ const [vpBuilt, setVpBuilt] = react.useState(null);
83
+ const [vpDirty, setVpDirty] = react.useState(false);
84
+ const [vpSaving, setVpSaving] = react.useState(false);
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
+ };
95
+
96
+ const cur = providers.length > 0 ? (providers.find((p) => p.id === selId) || providers[0]) : null;
97
+ // 选中 Provider 或目录刷新时重建草稿(render 期受控重置;msg 只在切 Provider/编辑时清)
98
+ const wantKey = cur ? cur.id + ":" + String(data && data.generatedAt || 0) : null;
99
+ if (cur && builtKey !== wantKey) {
100
+ setBuiltKey(wantKey);
101
+ setDraft(draftFromProvider(cur));
102
+ setDirty(false);
103
+ }
104
+ // 图片代理草稿随目录刷新重建(保存成功 → load() → 新 generatedAt → 重建为已保存态,msg 保留)
105
+ const vpWantKey = data ? String(data.generatedAt) : null;
106
+ if (data && vpBuilt !== vpWantKey) {
107
+ setVpBuilt(vpWantKey);
108
+ const vp = data.visionProxy || {};
109
+ setVpDraft({ enabled: !!vp.enabled, provider: String(vp.provider || ""), model: String(vp.model || "") });
110
+ setVpDirty(false);
111
+ }
112
+ const saveVp = () => {
113
+ if (!vpDraft || vpSaving) return;
114
+ setVpSaving(true);
115
+ fetch("/dsh-dock/models", {
116
+ method: "POST",
117
+ headers: { "content-type": "application/json" },
118
+ body: JSON.stringify({ visionProxy: vpDraft, revisions: (data && data.revisions) || {} }),
119
+ signal: AbortSignal.timeout(20000)
120
+ })
121
+ .then((res) => res.json()
122
+ .then((b2) => ({ status: res.status, body: b2 }))
123
+ .catch(() => ({ status: res.status, body: {} })))
124
+ .then((r2) => {
125
+ if (r2.status === 200 && r2.body && r2.body.ok) {
126
+ setVpMsg({ ok: true, text: "图片代理配置已保存,即时生效" });
127
+ modelsStore.load();
128
+ } else {
129
+ setVpMsg({ ok: false, text: (r2.body && r2.body.error) || ("HTTP " + r2.status) });
130
+ }
131
+ })
132
+ .catch((e) => setVpMsg({ ok: false, text: (e && e.message) || String(e) }))
133
+ .then(() => setVpSaving(false));
134
+ };
135
+
136
+ const patchDraft = (patch) => { setDraft(Object.assign({}, draft, patch)); setDirty(true); setMsg(null); };
137
+ const patchModel = (i, patch) => {
138
+ const models = draft.models.slice();
139
+ models[i] = Object.assign({}, models[i], patch);
140
+ patchDraft({ models: models });
141
+ };
142
+ const toggleIn = (list, v) => list.indexOf(v) >= 0 ? list.filter((x) => x !== v) : list.concat([v]);
143
+ // 一键批量:输入支持(官方模态 + 标注)与强度档。输入留空 = 继承目录默认(Host 写回时删除 input 字段,安全)
144
+ const ALL_INPUT = DKM_MODALITIES.map((x) => x[0]);
145
+ const ALL_TAGS = DKM_TAGS.map((x) => x[0]);
146
+ const allInputs = (on) => ({ input: on ? ALL_INPUT.slice() : [], tags: on ? ALL_TAGS.slice() : [] });
147
+ const allLevels = (on) => {
148
+ const lv = {};
149
+ for (const k of DKM_PI_LEVELS) lv[k] = !!on;
150
+ return { effortsMode: "custom", effortLevels: lv };
151
+ };
152
+ const bulkModels = (fn) => patchDraft({ models: draft.models.map((m) => Object.assign({}, m, fn(m))) });
153
+ const selectProvider = (id) => { setSelId(id); setMsg(null); };
154
+
155
+ const save = () => {
156
+ if (!draft || !cur || !dirty || saving) return;
157
+ const models = draft.models.map((m) => ({
158
+ id: String(m.id || "").trim(),
159
+ name: String(m.name || "").trim(),
160
+ contextWindow: m.contextWindow === "" ? undefined : Number(m.contextWindow),
161
+ maxTokens: m.maxTokens === "" ? undefined : Number(m.maxTokens),
162
+ input: m.input,
163
+ tags: m.tags,
164
+ effortsMode: m.effortsMode,
165
+ effortLevels: m.effortLevels,
166
+ raw: m.raw
167
+ }));
168
+ const payload = { provider: cur.id, revisions: (data && data.revisions) || {}, models: models };
169
+ if (cur.kind === "deepseek") {
170
+ payload.thinking = draft.thinking;
171
+ payload.defaultEffort = draft.defaultEffort;
172
+ }
173
+ setSaving(true);
174
+ fetch("/dsh-dock/models", {
175
+ method: "POST",
176
+ headers: { "content-type": "application/json" },
177
+ body: JSON.stringify(payload),
178
+ signal: AbortSignal.timeout(20000)
179
+ })
180
+ .then((res) => res.json()
181
+ .then((body2) => ({ status: res.status, body: body2 }))
182
+ .catch(() => ({ status: res.status, body: {} })))
183
+ .then((r2) => {
184
+ if (r2.status === 200 && r2.body && r2.body.ok) {
185
+ setMsg({ ok: true, text: "已写回官方配置;重新打开会话模型选择器即可选择新模型/强度档" });
186
+ modelsStore.load();
187
+ } else {
188
+ setMsg({ ok: false, text: (r2.body && r2.body.error) || ("HTTP " + r2.status) });
189
+ }
190
+ })
191
+ .catch((e) => setMsg({ ok: false, text: (e && e.message) || String(e) }))
192
+ .then(() => setSaving(false));
193
+ };
194
+
195
+ const modelRow = (m, i) => react.createElement("div", { key: i, className: "dkm-model" },
196
+ react.createElement("div", { className: "dkm-model-head" },
197
+ react.createElement("span", { className: "dkm-model-idx" }, "#" + (i + 1)),
198
+ react.createElement("button", {
199
+ type: "button",
200
+ className: "dkm-del",
201
+ onClick: () => patchDraft({ models: draft.models.filter((_, j) => j !== i) })
202
+ }, "删除")),
203
+ react.createElement("div", { className: "dkm-grid" },
204
+ react.createElement("input", { className: "dkm-input", placeholder: "模型 id(必填,如 deepseek-v4-pro)", value: m.id, onChange: (e) => patchModel(i, { id: e.target.value }) }),
205
+ react.createElement("input", { className: "dkm-input", placeholder: "显示名称(可选)", value: m.name, onChange: (e) => patchModel(i, { name: e.target.value }) }),
206
+ react.createElement("input", { className: "dkm-input", placeholder: "上下文窗口", inputMode: "numeric", value: m.contextWindow, onChange: (e) => patchModel(i, { contextWindow: e.target.value }) }),
207
+ react.createElement("input", { className: "dkm-input", placeholder: "最大输出", inputMode: "numeric", value: m.maxTokens, onChange: (e) => patchModel(i, { maxTokens: e.target.value }) })),
208
+ react.createElement("div", { className: "dkm-checks" },
209
+ react.createElement("span", { className: "dkm-label" }, "输入类型:"),
210
+ DKM_MODALITIES.map(([v, label]) => react.createElement("label", {
211
+ key: v,
212
+ className: "dkm-check",
213
+ title: v === "image" ? "由你声明该端点支持图片:保存后原图直发,并可作为视觉模型候选。系统不会替你猜测;端点实际不支持时取消勾选即可恢复走图片理解代理" : null
214
+ },
215
+ react.createElement("input", {
216
+ type: "checkbox",
217
+ checked: m.input.indexOf(v) >= 0,
218
+ onChange: () => {
219
+ const next = toggleIn(m.input, v);
220
+ patchModel(i, { input: next.length > 0 ? next : ["text"] });
221
+ }
222
+ }),
223
+ label)),
224
+ react.createElement("span", { className: "dkm-label dkm-tag", title: "标注仅在本面板展示与持久化,不参与请求路由(官方 schema 只接受文本/图片)" }, "标注:"),
225
+ DKM_TAGS.map(([v, label]) => react.createElement("label", { key: v, className: "dkm-check dkm-tag" },
226
+ react.createElement("input", { type: "checkbox", checked: m.tags.indexOf(v) >= 0, onChange: () => patchModel(i, { tags: toggleIn(m.tags, v) }) }),
227
+ label)),
228
+ react.createElement("button", { type: "button", className: "dkm-mini", title: "一键勾选全部输入类型与标注", onClick: () => patchModel(i, allInputs(true)) }, "全选"),
229
+ react.createElement("button", { type: "button", className: "dkm-mini", title: "一键取消全部勾选(输入留空 = 继承目录默认)", onClick: () => patchModel(i, allInputs(false)) }, "取消全选")),
230
+ react.createElement("div", { className: "dkm-efforts" },
231
+ react.createElement("span", { className: "dkm-label" }, "思考强度:"),
232
+ cur.kind === "deepseek"
233
+ ? react.createElement("span", { className: "dkm-sub" }, "随 Provider 设置(Off/Low/High/Max,全模型共享)")
234
+ : react.createElement("span", null,
235
+ react.createElement("select", {
236
+ className: "dkm-select",
237
+ value: m.effortsMode,
238
+ onChange: (e) => patchModel(i, {
239
+ effortsMode: e.target.value,
240
+ effortLevels: e.target.value === "custom" && m.effortsMode !== "custom"
241
+ ? { off: true, low: true, high: true }
242
+ : m.effortLevels
243
+ })
244
+ },
245
+ react.createElement("option", { value: "inherit" }, "跟随目录默认"),
246
+ react.createElement("option", { value: "off" }, "不支持思考"),
247
+ react.createElement("option", { value: "custom" }, "自定义档位")),
248
+ m.effortsMode === "custom"
249
+ ? DKM_PI_LEVELS.map((lv) => react.createElement("label", { key: lv, className: "dkm-check" },
250
+ react.createElement("input", {
251
+ type: "checkbox",
252
+ checked: !!m.effortLevels[lv],
253
+ onChange: () => {
254
+ const nl = Object.assign({}, m.effortLevels);
255
+ nl[lv] = !nl[lv];
256
+ patchModel(i, { effortLevels: nl });
257
+ }
258
+ }),
259
+ DKM_EFFORT_NAMES[lv] || lv))
260
+ : null,
261
+ react.createElement("button", { type: "button", className: "dkm-mini", title: "一键勾选全部强度档(切为自定义)", onClick: () => patchModel(i, allLevels(true)) }, "全选"),
262
+ react.createElement("button", { type: "button", className: "dkm-mini", title: "一键取消全部强度档", onClick: () => patchModel(i, allLevels(false)) }, "取消全选"))));
263
+
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
+ }
270
+ if (snap.error) {
271
+ body.push(react.createElement("div", { key: "err", className: "dkm-error" },
272
+ "模型目录拉取失败:" + snap.error + " ",
273
+ react.createElement("button", { type: "button", className: "dkb-refresh", onClick: () => modelsStore.load() }, "重试")));
274
+ } else if (!data) {
275
+ body.push(react.createElement("div", { key: "loading", className: "dkm-note" }, snap.loading ? "正在拉取模型目录…" : "等待拉取模型目录"));
276
+ }
277
+
278
+ // ---- 图片理解代理(全局配置):纯文本模型收图自动走视觉模型识别 ----
279
+ // 宿主较旧(GET 无 visionProxy 字段)时降级为提示,避免保存撞到旧写回分支
280
+ if (data && data.visionProxy === undefined) {
281
+ body.push(react.createElement("div", { key: "visionproxy-old", className: "dkm-note" },
282
+ "图片理解代理需要新版宿主进程:当前 dsh web 较旧,重启后此面板可用。"));
283
+ }
284
+ if (data && data.visionProxy !== undefined && vpDraft) {
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;
290
+ const vpDirect = [];
291
+ const vpProxied = [];
292
+ for (const p of providers) {
293
+ for (const m of (p.models || [])) {
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);
305
+ }
306
+ }
307
+ const vpCandidates = vpDirect.slice();
308
+ const vpKey = vpDraft.provider + "/" + vpDraft.model;
309
+ const vpKnown = vpCandidates.some((c) => c.key === vpKey);
310
+ body.push(react.createElement("div", { key: "visionproxy", className: "dkm-prov" },
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。"),
317
+ react.createElement("div", { className: "dkm-checks" },
318
+ react.createElement("label", { className: "dkm-check" },
319
+ react.createElement("input", {
320
+ type: "checkbox",
321
+ checked: vpDraft.enabled,
322
+ onChange: () => { setVpDraft(Object.assign({}, vpDraft, { enabled: !vpDraft.enabled })); setVpDirty(true); setVpMsg(null); }
323
+ }),
324
+ "启用"),
325
+ react.createElement("span", { className: "dkm-label" }, "视觉模型:"),
326
+ react.createElement("select", {
327
+ className: "dkm-select",
328
+ value: vpKey,
329
+ onChange: (e) => {
330
+ const idx = e.target.value.indexOf("/");
331
+ setVpDraft(Object.assign({}, vpDraft, { provider: e.target.value.slice(0, idx), model: e.target.value.slice(idx + 1) }));
332
+ setVpDirty(true);
333
+ setVpMsg(null);
334
+ }
335
+ },
336
+ react.createElement("option", { value: "/" }, "(未选择)"),
337
+ !vpKnown && vpDraft.provider ? react.createElement("option", { value: vpKey }, vpDraft.provider + " / " + vpDraft.model + "(当前)") : null,
338
+ vpCandidates.map((c) => react.createElement("option", { key: c.key, value: c.key }, c.label))),
339
+ vpCandidates.length === 0
340
+ ? react.createElement("span", { className: "dkm-sub" }, "还没有标记图片能力的模型——在下方给你确认支持图片的模型勾选「图片」并保存")
341
+ : null),
342
+ react.createElement("div", { className: "dkm-sub" },
343
+ "以你在下方勾选的「图片」为准;运行时识别只是辅助提示。它不代表已联网实测可调用,实际可用性仍取决于 Provider 凭据、额度与上游服务。若端点实际不认图,取消该模型的「图片」勾选即可恢复走图片理解代理。"),
344
+ vpDirect.length > 0
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)
358
+ : null,
359
+ vpProxied.length > 0
360
+ ? react.createElement("div", { className: "dkm-checks" },
361
+ react.createElement("span", { className: "dkm-label" }, "走视觉代理(未标记图片," + vpProxied.length + "):"),
362
+ react.createElement("span", { className: "dkm-sub" }, vpProxied.map((c) => c.model).join("、")))
363
+ : null,
364
+ react.createElement("div", { className: "dkm-savebar" },
365
+ react.createElement("button", { type: "button", className: "dkm-save", disabled: !vpDirty || vpSaving, onClick: saveVp }, vpSaving ? "保存中…" : "保存图片代理配置"),
366
+ vpMsg ? react.createElement("span", { className: "dkm-msg " + (vpMsg.ok ? "ok" : "err") }, vpMsg.text)
367
+ : react.createElement("span", { className: "dkm-msg" }, vpDirty ? "有未保存的修改" : ""))));
368
+ }
369
+
370
+ if (data && providers.length === 0) {
371
+ body.push(react.createElement("div", { key: "empty", className: "dkm-note" }, "没有可编辑的模型 Provider(Host 侧尚未配置)"));
372
+ }
373
+ if (providers.length > 0) {
374
+ body.push(react.createElement("div", { key: "chips", className: "dkm-chips" },
375
+ providers.map((p) => react.createElement("button", {
376
+ type: "button",
377
+ key: p.id,
378
+ className: "dkm-chip" + (cur && p.id === cur.id ? " on" : ""),
379
+ onClick: () => selectProvider(p.id)
380
+ }, p.displayName + "(" + (p.models ? p.models.length : 0) + ")"))));
381
+ }
382
+ if (cur && draft) {
383
+ body.push(react.createElement("div", { key: "prov", className: "dkm-prov" },
384
+ react.createElement("div", { className: "dkm-prov-head" },
385
+ react.createElement("span", { className: "dkm-name" }, cur.displayName),
386
+ react.createElement("span", { className: "dkm-badge" }, cur.kind === "deepseek" ? "官方 DeepSeek" : "自定义路由"),
387
+ react.createElement("span", { className: "dkm-sub" }, [cur.api, cur.baseURL].filter(Boolean).join(" · "))),
388
+ cur.kind === "deepseek"
389
+ ? react.createElement("div", { className: "dkm-controls" },
390
+ react.createElement("span", { className: "dkm-label" }, "思考模式:"),
391
+ react.createElement("select", { className: "dkm-select", value: draft.thinking, onChange: (e) => patchDraft({ thinking: e.target.value }) },
392
+ react.createElement("option", { value: "enabled" }, "开启"),
393
+ react.createElement("option", { value: "disabled" }, "关闭")),
394
+ react.createElement("span", { className: "dkm-label" }, "默认强度:"),
395
+ react.createElement("select", { className: "dkm-select", value: draft.defaultEffort, onChange: (e) => patchDraft({ defaultEffort: e.target.value }) },
396
+ DKM_DS_LEVELS.map((lv) => react.createElement("option", { key: lv, value: lv }, DKM_EFFORT_NAMES[lv] || lv))))
397
+ : null,
398
+ react.createElement("div", { className: "dkm-toolbar" },
399
+ react.createElement("span", { className: "dkm-toolbar-label" }, "批量(全部模型):"),
400
+ react.createElement("button", { type: "button", className: "dkm-mini", title: "全部模型:勾选全部输入类型与标注", onClick: () => bulkModels(() => allInputs(true)) }, "输入全选"),
401
+ react.createElement("button", { type: "button", className: "dkm-mini", title: "全部模型:取消全部输入勾选(留空 = 继承目录默认)", onClick: () => bulkModels(() => allInputs(false)) }, "输入取消全选"),
402
+ cur.kind !== "deepseek"
403
+ ? react.createElement("button", { type: "button", className: "dkm-mini", title: "全部模型:勾选全部强度档(切为自定义)", onClick: () => bulkModels(() => allLevels(true)) }, "强度全选")
404
+ : null,
405
+ cur.kind !== "deepseek"
406
+ ? react.createElement("button", { type: "button", className: "dkm-mini", title: "全部模型:取消全部强度档", onClick: () => bulkModels(() => allLevels(false)) }, "强度取消全选")
407
+ : null),
408
+ react.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "8px" } },
409
+ draft.models.map(modelRow),
410
+ react.createElement("button", {
411
+ type: "button",
412
+ className: "dkm-add",
413
+ onClick: () => patchDraft({
414
+ models: draft.models.concat([{ id: "", name: "", contextWindow: "", maxTokens: "", input: ["text"], tags: [], effortsMode: "inherit", effortLevels: {} }])
415
+ })
416
+ }, "+ 添加模型")),
417
+ react.createElement("div", { className: "dkm-savebar" },
418
+ react.createElement("span", { className: "dkm-msg" }, dirty ? "有未保存的修改" : "无未保存修改"),
419
+ react.createElement("button", { type: "button", className: "dkm-save", disabled: !dirty || saving, onClick: save }, saving ? "保存中…" : "保存写回官方配置"),
420
+ react.createElement("button", { type: "button", className: "dkb-refresh", disabled: saving || snap.loading, onClick: refreshModels }, snap.loading ? "刷新中…" : "重新拉取"),
421
+ msg ? react.createElement("span", { className: "dkm-msg " + (msg.ok ? "ok" : "err") }, msg.text) : null)));
422
+ }
423
+ return react.createElement("div", { className: "dkm-list" }, body);
424
+ }
425
+
426
+ function ModelsStat() {
427
+ const snap = useModels();
428
+ const data = snap.data;
429
+ if (snap.error) return react.createElement("span", { className: "dockm-err" }, "模型目录拉取失败(点击进入查看)");
430
+ if (!data) return react.createElement("span", null, snap.loading ? "正在拉取模型目录…" : "等待拉取模型目录");
431
+ const providers = Array.isArray(data.providers) ? data.providers : [];
432
+ const total = providers.reduce((n, p) => n + (p.models ? p.models.length : 0), 0);
433
+ return react.createElement("span", null, providers.length + " 个 Provider · " + total + " 个模型");
434
+ }
435
+
436
+ export const feature = {
437
+ id: "modelconfig",
438
+ name: "模型设置",
439
+ order: 100,
440
+ accent: "#22d3ee",
441
+ description: "编辑各 Provider 模型目录:输入类型(文本/图片 + 标注)与思考强度档位;写回官方配置热生效,会话模型选择器即时可选",
442
+ css: [
443
+ ".dkm-note{color:var(--dsw-alias-label-secondary);font-size:12px;}",
444
+ ".dkm-warn{color:var(--dk-warn);}",
445
+ ".dkm-error{color:var(--dsw-alias-state-error-primary);font-size:12px;}",
446
+ ".dkm-chips{display:flex;flex-wrap:wrap;gap:6px;}",
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;}",
448
+ ".dkm-chip.on{color:var(--dk-accent);border-color:currentColor;}",
449
+ ".dkm-prov{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px;}",
450
+ ".dkm-prov-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
451
+ ".dkm-name{font-weight:600;}",
452
+ ".dkm-sub{color:var(--dsw-alias-label-tertiary);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;}",
453
+ ".dkm-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);}",
454
+ ".dkm-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-top:1px solid var(--dsw-alias-border-l1);padding-top:8px;}",
455
+ ".dkm-label{color:var(--dsw-alias-label-secondary);font-size:12px;flex:none;}",
456
+ ".dkm-select{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:3px 8px;font-family:inherit;font-size:12px;}",
457
+ ".dkm-model{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;display:flex;flex-direction:column;gap:6px;}",
458
+ ".dkm-model-head{display:flex;align-items:center;gap:6px;}",
459
+ ".dkm-model-idx{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none;}",
460
+ ".dkm-grid{display:grid;grid-template-columns:1.3fr 1.3fr .8fr .8fr;gap:6px;align-items:center;}",
461
+ ".dkm-input{box-sizing:border-box;width:100%;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px 8px;font-family:inherit;font-size:12px;}",
462
+ ".dkm-checks{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}",
463
+ ".dkm-check{display:inline-flex;align-items:center;gap:5px;cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;}",
464
+ ".dkm-check input{accent-color:var(--dk-accent);margin:0;}",
465
+ ".dkm-tag{color:var(--dsw-alias-label-tertiary);}",
466
+ ".dkm-efforts{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}",
467
+ ".dkm-del{margin-left:auto;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:8px;padding:2px 10px;font-family:inherit;font-size:12px;flex:none;}",
468
+ ".dkm-del:hover{color:var(--dsw-alias-state-error-primary);border-color:currentColor;}",
469
+ ".dkm-add{cursor:pointer;border:1px dashed var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);border-radius:10px;padding:6px 14px;font-family:inherit;font-size:12px;align-self:flex-start;}",
470
+ ".dkm-add:hover{color:var(--dsw-alias-label-primary);}",
471
+ ".dkm-savebar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px;}",
472
+ ".dkm-save{cursor:pointer;border:1px solid var(--dk-accent);color:var(--dk-accent);background:transparent;border-radius:8px;padding:4px 14px;font-family:inherit;font-size:12px;flex:none;}",
473
+ ".dkm-save:hover{background:var(--dsw-alias-interactive-bg-hover);}",
474
+ ".dkm-save[disabled]{opacity:.5;cursor:default;}",
475
+ ".dkm-msg{font-size:12px;color:var(--dsw-alias-label-secondary);}",
476
+ ".dkm-msg.ok{color:var(--dsw-alias-state-success-primary);}",
477
+ ".dkm-msg.err{color:var(--dsw-alias-state-error-primary);}",
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;}",
482
+ ".dkm-ro{color:var(--dsw-alias-label-secondary);font-size:12px;}",
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;}",
484
+ ".dkm-mini:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-secondary);}",
485
+ ".dkm-toolbar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;border-top:1px dashed var(--dsw-alias-border-l1);padding-top:8px;}",
486
+ ".dkm-toolbar-label{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none;}"
487
+ ].join("\n"),
488
+ View: ModelsView,
489
+ HomeStat: ModelsStat,
490
+ };