dsh-dock 0.1.0 → 0.4.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.
@@ -0,0 +1,446 @@
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 });
16
+ fetch("/dsh-dock/models", { signal: AbortSignal.timeout(20000) })
17
+ .then((res) => (res.ok ? res.json() : Promise.reject(new Error("模型目录接口 HTTP " + res.status))))
18
+ .then((data) => this.set({ data: data, loading: false, error: null }))
19
+ .catch((e) => this.set({ loading: false, error: (e && e.message) || String(e) }));
20
+ }
21
+ };
22
+ function useModels() {
23
+ const [snap, setSnap] = react.useState(modelsStore.snap);
24
+ react.useEffect(() => {
25
+ const off = modelsStore.subscribe(() => setSnap(modelsStore.snap));
26
+ if (!modelsStore.snap.data && !modelsStore.snap.loading) modelsStore.load();
27
+ return off;
28
+ }, []);
29
+ return snap;
30
+ }
31
+
32
+ // ---- 编辑常量与草稿工具 ----
33
+ // 官方 schema 接受的真实输入模态;其余(视频等)仅作为 dockTags 标注随配置持久化
34
+ const DKM_MODALITIES = [["text", "文本"], ["image", "图片"]];
35
+ const DKM_TAGS = [["video", "视频"], ["audio", "音频"], ["document", "文档"]];
36
+ const DKM_PI_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
37
+ const DKM_DS_LEVELS = ["off", "low", "high", "max"];
38
+ const DKM_EFFORT_NAMES = { off: "关闭", minimal: "极低", low: "低", medium: "中", high: "高", xhigh: "超高", max: "最高" };
39
+
40
+ /** 从目录 Provider 视图构建可编辑草稿。 */
41
+ function draftFromProvider(p) {
42
+ return {
43
+ id: p.id,
44
+ kind: p.kind,
45
+ thinking: p.thinking === "disabled" ? "disabled" : "enabled",
46
+ defaultEffort: DKM_DS_LEVELS.indexOf(p.defaultEffort) >= 0 ? p.defaultEffort : "high",
47
+ models: (p.models || []).map((m) => {
48
+ // effortMap: {档位: wire},声明过(wire 字符串或 null)即视为勾选,undefined = 未提供
49
+ const levels = {};
50
+ if (m.efforts === "custom" && m.effortMap && typeof m.effortMap === "object") {
51
+ for (const lv of DKM_PI_LEVELS) levels[lv] = m.effortMap[lv] !== undefined;
52
+ }
53
+ return {
54
+ id: m.id || "",
55
+ name: m.name || "",
56
+ contextWindow: m.contextWindow != null ? String(m.contextWindow) : "",
57
+ maxTokens: m.maxTokens != null ? String(m.maxTokens) : "",
58
+ input: m.input && m.input.length ? m.input.slice() : ["text"],
59
+ tags: (m.tags || []).slice(),
60
+ effortsMode: m.efforts === "custom" ? "custom" : m.efforts === "off" ? "off" : "inherit",
61
+ effortLevels: levels,
62
+ raw: m.raw && typeof m.raw === "object" ? m.raw : null
63
+ };
64
+ })
65
+ };
66
+ }
67
+
68
+ function ModelsView() {
69
+ const snap = useModels();
70
+ const data = snap.data;
71
+ const providers = data && Array.isArray(data.providers) ? data.providers : [];
72
+ const [selId, setSelId] = react.useState(null);
73
+ const [draft, setDraft] = react.useState(null);
74
+ const [builtKey, setBuiltKey] = react.useState(null);
75
+ const [dirty, setDirty] = react.useState(false);
76
+ const [saving, setSaving] = react.useState(false);
77
+ const [msg, setMsg] = react.useState(null);
78
+ // 图片理解代理(全局配置,独立于 Provider 草稿)
79
+ const [vpDraft, setVpDraft] = react.useState(null);
80
+ const [vpBuilt, setVpBuilt] = react.useState(null);
81
+ const [vpDirty, setVpDirty] = react.useState(false);
82
+ const [vpSaving, setVpSaving] = react.useState(false);
83
+ const [vpMsg, setVpMsg] = react.useState(null);
84
+
85
+ const cur = providers.length > 0 ? (providers.find((p) => p.id === selId) || providers[0]) : null;
86
+ // 选中 Provider 或目录刷新时重建草稿(render 期受控重置;msg 只在切 Provider/编辑时清)
87
+ const wantKey = cur ? cur.id + ":" + String(data && data.generatedAt || 0) : null;
88
+ if (cur && builtKey !== wantKey) {
89
+ setBuiltKey(wantKey);
90
+ setDraft(draftFromProvider(cur));
91
+ setDirty(false);
92
+ }
93
+ // 图片代理草稿随目录刷新重建(保存成功 → load() → 新 generatedAt → 重建为已保存态,msg 保留)
94
+ const vpWantKey = data ? String(data.generatedAt) : null;
95
+ if (data && vpBuilt !== vpWantKey) {
96
+ setVpBuilt(vpWantKey);
97
+ const vp = data.visionProxy || {};
98
+ setVpDraft({ enabled: !!vp.enabled, provider: String(vp.provider || ""), model: String(vp.model || "") });
99
+ setVpDirty(false);
100
+ }
101
+ const saveVp = () => {
102
+ if (!vpDraft || vpSaving) return;
103
+ setVpSaving(true);
104
+ fetch("/dsh-dock/models", {
105
+ method: "POST",
106
+ headers: { "content-type": "application/json" },
107
+ body: JSON.stringify({ visionProxy: vpDraft, revisions: (data && data.revisions) || {} }),
108
+ signal: AbortSignal.timeout(20000)
109
+ })
110
+ .then((res) => res.json()
111
+ .then((b2) => ({ status: res.status, body: b2 }))
112
+ .catch(() => ({ status: res.status, body: {} })))
113
+ .then((r2) => {
114
+ if (r2.status === 200 && r2.body && r2.body.ok) {
115
+ setVpMsg({ ok: true, text: "图片代理配置已保存,即时生效" });
116
+ modelsStore.load();
117
+ } else {
118
+ setVpMsg({ ok: false, text: (r2.body && r2.body.error) || ("HTTP " + r2.status) });
119
+ }
120
+ })
121
+ .catch((e) => setVpMsg({ ok: false, text: (e && e.message) || String(e) }))
122
+ .then(() => setVpSaving(false));
123
+ };
124
+
125
+ const patchDraft = (patch) => { setDraft(Object.assign({}, draft, patch)); setDirty(true); setMsg(null); };
126
+ const patchModel = (i, patch) => {
127
+ const models = draft.models.slice();
128
+ models[i] = Object.assign({}, models[i], patch);
129
+ patchDraft({ models: models });
130
+ };
131
+ const toggleIn = (list, v) => list.indexOf(v) >= 0 ? list.filter((x) => x !== v) : list.concat([v]);
132
+ // 一键批量:输入支持(官方模态 + 标注)与强度档。输入留空 = 继承目录默认(Host 写回时删除 input 字段,安全)
133
+ const ALL_INPUT = DKM_MODALITIES.map((x) => x[0]);
134
+ const ALL_TAGS = DKM_TAGS.map((x) => x[0]);
135
+ const allInputs = (on) => ({ input: on ? ALL_INPUT.slice() : [], tags: on ? ALL_TAGS.slice() : [] });
136
+ const allLevels = (on) => {
137
+ const lv = {};
138
+ for (const k of DKM_PI_LEVELS) lv[k] = !!on;
139
+ return { effortsMode: "custom", effortLevels: lv };
140
+ };
141
+ const bulkModels = (fn) => patchDraft({ models: draft.models.map((m) => Object.assign({}, m, fn(m))) });
142
+ const selectProvider = (id) => { setSelId(id); setMsg(null); };
143
+
144
+ const save = () => {
145
+ if (!draft || !cur || !dirty || saving) return;
146
+ const models = draft.models.map((m) => ({
147
+ id: String(m.id || "").trim(),
148
+ name: String(m.name || "").trim(),
149
+ contextWindow: m.contextWindow === "" ? undefined : Number(m.contextWindow),
150
+ maxTokens: m.maxTokens === "" ? undefined : Number(m.maxTokens),
151
+ input: m.input,
152
+ tags: m.tags,
153
+ effortsMode: m.effortsMode,
154
+ effortLevels: m.effortLevels,
155
+ raw: m.raw
156
+ }));
157
+ const payload = { provider: cur.id, revisions: (data && data.revisions) || {}, models: models };
158
+ if (cur.kind === "deepseek") {
159
+ payload.thinking = draft.thinking;
160
+ payload.defaultEffort = draft.defaultEffort;
161
+ }
162
+ setSaving(true);
163
+ fetch("/dsh-dock/models", {
164
+ method: "POST",
165
+ headers: { "content-type": "application/json" },
166
+ body: JSON.stringify(payload),
167
+ signal: AbortSignal.timeout(20000)
168
+ })
169
+ .then((res) => res.json()
170
+ .then((body2) => ({ status: res.status, body: body2 }))
171
+ .catch(() => ({ status: res.status, body: {} })))
172
+ .then((r2) => {
173
+ if (r2.status === 200 && r2.body && r2.body.ok) {
174
+ setMsg({ ok: true, text: "已写回官方配置;重新打开会话模型选择器即可选择新模型/强度档" });
175
+ modelsStore.load();
176
+ } else {
177
+ setMsg({ ok: false, text: (r2.body && r2.body.error) || ("HTTP " + r2.status) });
178
+ }
179
+ })
180
+ .catch((e) => setMsg({ ok: false, text: (e && e.message) || String(e) }))
181
+ .then(() => setSaving(false));
182
+ };
183
+
184
+ const modelRow = (m, i) => react.createElement("div", { key: i, className: "dkm-model" },
185
+ react.createElement("div", { className: "dkm-model-head" },
186
+ react.createElement("span", { className: "dkm-model-idx" }, "#" + (i + 1)),
187
+ react.createElement("button", {
188
+ type: "button",
189
+ className: "dkm-del",
190
+ onClick: () => patchDraft({ models: draft.models.filter((_, j) => j !== i) })
191
+ }, "删除")),
192
+ react.createElement("div", { className: "dkm-grid" },
193
+ react.createElement("input", { className: "dkm-input", placeholder: "模型 id(必填,如 deepseek-v4-pro)", value: m.id, onChange: (e) => patchModel(i, { id: e.target.value }) }),
194
+ react.createElement("input", { className: "dkm-input", placeholder: "显示名称(可选)", value: m.name, onChange: (e) => patchModel(i, { name: e.target.value }) }),
195
+ react.createElement("input", { className: "dkm-input", placeholder: "上下文窗口", inputMode: "numeric", value: m.contextWindow, onChange: (e) => patchModel(i, { contextWindow: e.target.value }) }),
196
+ react.createElement("input", { className: "dkm-input", placeholder: "最大输出", inputMode: "numeric", value: m.maxTokens, onChange: (e) => patchModel(i, { maxTokens: e.target.value }) })),
197
+ react.createElement("div", { className: "dkm-checks" },
198
+ react.createElement("span", { className: "dkm-label" }, "输入类型:"),
199
+ DKM_MODALITIES.map(([v, label]) => react.createElement("label", {
200
+ key: v,
201
+ className: "dkm-check",
202
+ title: v === "image" ? "勾选=端点原生支持图片(原图直发,图片理解代理跳过);纯文本模型勿勾——收图会自动交给图片理解代理识别" : null
203
+ },
204
+ react.createElement("input", {
205
+ type: "checkbox",
206
+ checked: m.input.indexOf(v) >= 0,
207
+ onChange: () => {
208
+ const next = toggleIn(m.input, v);
209
+ patchModel(i, { input: next.length > 0 ? next : ["text"] });
210
+ }
211
+ }),
212
+ label)),
213
+ react.createElement("span", { className: "dkm-label dkm-tag", title: "标注仅在本面板展示与持久化,不参与请求路由(官方 schema 只接受文本/图片)" }, "标注:"),
214
+ DKM_TAGS.map(([v, label]) => react.createElement("label", { key: v, className: "dkm-check dkm-tag" },
215
+ react.createElement("input", { type: "checkbox", checked: m.tags.indexOf(v) >= 0, onChange: () => patchModel(i, { tags: toggleIn(m.tags, v) }) }),
216
+ label)),
217
+ react.createElement("button", { type: "button", className: "dkm-mini", title: "一键勾选全部输入类型与标注", onClick: () => patchModel(i, allInputs(true)) }, "全选"),
218
+ react.createElement("button", { type: "button", className: "dkm-mini", title: "一键取消全部勾选(输入留空 = 继承目录默认)", onClick: () => patchModel(i, allInputs(false)) }, "取消全选")),
219
+ react.createElement("div", { className: "dkm-efforts" },
220
+ react.createElement("span", { className: "dkm-label" }, "思考强度:"),
221
+ cur.kind === "deepseek"
222
+ ? react.createElement("span", { className: "dkm-sub" }, "随 Provider 设置(Off/Low/High/Max,全模型共享)")
223
+ : react.createElement("span", null,
224
+ react.createElement("select", {
225
+ className: "dkm-select",
226
+ value: m.effortsMode,
227
+ onChange: (e) => patchModel(i, {
228
+ effortsMode: e.target.value,
229
+ effortLevels: e.target.value === "custom" && m.effortsMode !== "custom"
230
+ ? { off: true, low: true, high: true }
231
+ : m.effortLevels
232
+ })
233
+ },
234
+ react.createElement("option", { value: "inherit" }, "跟随目录默认"),
235
+ react.createElement("option", { value: "off" }, "不支持思考"),
236
+ react.createElement("option", { value: "custom" }, "自定义档位")),
237
+ m.effortsMode === "custom"
238
+ ? DKM_PI_LEVELS.map((lv) => react.createElement("label", { key: lv, className: "dkm-check" },
239
+ react.createElement("input", {
240
+ type: "checkbox",
241
+ checked: !!m.effortLevels[lv],
242
+ onChange: () => {
243
+ const nl = Object.assign({}, m.effortLevels);
244
+ nl[lv] = !nl[lv];
245
+ patchModel(i, { effortLevels: nl });
246
+ }
247
+ }),
248
+ DKM_EFFORT_NAMES[lv] || lv))
249
+ : null,
250
+ react.createElement("button", { type: "button", className: "dkm-mini", title: "一键勾选全部强度档(切为自定义)", onClick: () => patchModel(i, allLevels(true)) }, "全选"),
251
+ react.createElement("button", { type: "button", className: "dkm-mini", title: "一键取消全部强度档", onClick: () => patchModel(i, allLevels(false)) }, "取消全选"))));
252
+
253
+ const body = [];
254
+ if (snap.error) {
255
+ body.push(react.createElement("div", { key: "err", className: "dkm-error" },
256
+ "模型目录拉取失败:" + snap.error + " ",
257
+ react.createElement("button", { type: "button", className: "dkb-refresh", onClick: () => modelsStore.load() }, "重试")));
258
+ } else if (!data) {
259
+ body.push(react.createElement("div", { key: "loading", className: "dkm-note" }, snap.loading ? "正在拉取模型目录…" : "等待拉取模型目录"));
260
+ }
261
+
262
+ // ---- 图片理解代理(全局配置):纯文本模型收图自动走视觉模型识别 ----
263
+ // 宿主较旧(GET 无 visionProxy 字段)时降级为提示,避免保存撞到旧写回分支
264
+ if (data && data.visionProxy === undefined) {
265
+ body.push(react.createElement("div", { key: "visionproxy-old", className: "dkm-note" },
266
+ "图片理解代理需要新版宿主进程:当前 dsh web 较旧,重启后此面板可用。"));
267
+ }
268
+ if (data && data.visionProxy !== undefined && vpDraft) {
269
+ // 图片能力以运行时视角(runtimeInput,与官方投影同源)为准,无则回退条目 input
270
+ const effInput = (m) => (m.runtimeInput && m.runtimeInput.length > 0 ? m.runtimeInput : m.input) || [];
271
+ const vpDirect = [];
272
+ const vpProxied = [];
273
+ for (const p of providers) {
274
+ 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);
277
+ }
278
+ }
279
+ const vpCandidates = vpDirect.slice();
280
+ const vpKey = vpDraft.provider + "/" + vpDraft.model;
281
+ const vpKnown = vpCandidates.some((c) => c.key === vpKey);
282
+ 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" }, "纯文本模型收到图片时自动调用所选视觉模型识别,识别文本替换图片;多模态模型原样自识别")),
287
+ react.createElement("div", { className: "dkm-checks" },
288
+ react.createElement("label", { className: "dkm-check" },
289
+ react.createElement("input", {
290
+ type: "checkbox",
291
+ checked: vpDraft.enabled,
292
+ onChange: () => { setVpDraft(Object.assign({}, vpDraft, { enabled: !vpDraft.enabled })); setVpDirty(true); setVpMsg(null); }
293
+ }),
294
+ "启用"),
295
+ react.createElement("span", { className: "dkm-label" }, "视觉模型:"),
296
+ react.createElement("select", {
297
+ className: "dkm-select",
298
+ value: vpKey,
299
+ onChange: (e) => {
300
+ const idx = e.target.value.indexOf("/");
301
+ setVpDraft(Object.assign({}, vpDraft, { provider: e.target.value.slice(0, idx), model: e.target.value.slice(idx + 1) }));
302
+ setVpDirty(true);
303
+ setVpMsg(null);
304
+ }
305
+ },
306
+ react.createElement("option", { value: "/" }, "(未选择)"),
307
+ !vpKnown && vpDraft.provider ? react.createElement("option", { value: vpKey }, vpDraft.provider + " / " + vpDraft.model + "(当前)") : null,
308
+ vpCandidates.map((c) => react.createElement("option", { key: c.key, value: c.key }, c.label))),
309
+ vpCandidates.length === 0
310
+ ? react.createElement("span", { className: "dkm-sub" }, "目录里暂无多模态模型——先在下方给真·多模态模型勾选「图片」输入类型")
311
+ : null),
312
+ react.createElement("div", { className: "dkm-sub" },
313
+ "判定与官方运行时同源:「图片」勾选(或目录默认)= 端点原生支持、原图直发;其余模型收图自动走视觉模型。注意:若端点实际不认图却勾了「图片」,模型会看不见图片、转而用工具瞎折腾——不确定就不要勾,交给代理。"),
314
+ 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)))
318
+ : null,
319
+ vpProxied.length > 0
320
+ ? react.createElement("div", { className: "dkm-checks" },
321
+ react.createElement("span", { className: "dkm-label" }, "走视觉代理(纯文本," + vpProxied.length + "):"),
322
+ react.createElement("span", { className: "dkm-sub" }, vpProxied.map((c) => c.model).join("、")))
323
+ : null,
324
+ react.createElement("div", { className: "dkm-savebar" },
325
+ react.createElement("button", { type: "button", className: "dkm-save", disabled: !vpDirty || vpSaving, onClick: saveVp }, vpSaving ? "保存中…" : "保存图片代理配置"),
326
+ vpMsg ? react.createElement("span", { className: "dkm-msg " + (vpMsg.ok ? "ok" : "err") }, vpMsg.text)
327
+ : react.createElement("span", { className: "dkm-msg" }, vpDirty ? "有未保存的修改" : ""))));
328
+ }
329
+
330
+ if (data && providers.length === 0) {
331
+ body.push(react.createElement("div", { key: "empty", className: "dkm-note" }, "没有可编辑的模型 Provider(Host 侧尚未配置)"));
332
+ }
333
+ if (providers.length > 0) {
334
+ body.push(react.createElement("div", { key: "chips", className: "dkm-chips" },
335
+ providers.map((p) => react.createElement("button", {
336
+ type: "button",
337
+ key: p.id,
338
+ className: "dkm-chip" + (cur && p.id === cur.id ? " on" : ""),
339
+ onClick: () => selectProvider(p.id)
340
+ }, p.displayName + "(" + (p.models ? p.models.length : 0) + ")"))));
341
+ }
342
+ if (cur && draft) {
343
+ body.push(react.createElement("div", { key: "prov", className: "dkm-prov" },
344
+ react.createElement("div", { className: "dkm-prov-head" },
345
+ react.createElement("span", { className: "dkm-name" }, cur.displayName),
346
+ react.createElement("span", { className: "dkm-badge" }, cur.kind === "deepseek" ? "官方 DeepSeek" : "自定义路由"),
347
+ react.createElement("span", { className: "dkm-sub" }, [cur.api, cur.baseURL].filter(Boolean).join(" · "))),
348
+ cur.kind === "deepseek"
349
+ ? react.createElement("div", { className: "dkm-controls" },
350
+ react.createElement("span", { className: "dkm-label" }, "思考模式:"),
351
+ react.createElement("select", { className: "dkm-select", value: draft.thinking, onChange: (e) => patchDraft({ thinking: e.target.value }) },
352
+ react.createElement("option", { value: "enabled" }, "开启"),
353
+ react.createElement("option", { value: "disabled" }, "关闭")),
354
+ react.createElement("span", { className: "dkm-label" }, "默认强度:"),
355
+ react.createElement("select", { className: "dkm-select", value: draft.defaultEffort, onChange: (e) => patchDraft({ defaultEffort: e.target.value }) },
356
+ DKM_DS_LEVELS.map((lv) => react.createElement("option", { key: lv, value: lv }, DKM_EFFORT_NAMES[lv] || lv))))
357
+ : null,
358
+ react.createElement("div", { className: "dkm-toolbar" },
359
+ react.createElement("span", { className: "dkm-toolbar-label" }, "批量(全部模型):"),
360
+ react.createElement("button", { type: "button", className: "dkm-mini", title: "全部模型:勾选全部输入类型与标注", onClick: () => bulkModels(() => allInputs(true)) }, "输入全选"),
361
+ react.createElement("button", { type: "button", className: "dkm-mini", title: "全部模型:取消全部输入勾选(留空 = 继承目录默认)", onClick: () => bulkModels(() => allInputs(false)) }, "输入取消全选"),
362
+ cur.kind !== "deepseek"
363
+ ? react.createElement("button", { type: "button", className: "dkm-mini", title: "全部模型:勾选全部强度档(切为自定义)", onClick: () => bulkModels(() => allLevels(true)) }, "强度全选")
364
+ : null,
365
+ cur.kind !== "deepseek"
366
+ ? react.createElement("button", { type: "button", className: "dkm-mini", title: "全部模型:取消全部强度档", onClick: () => bulkModels(() => allLevels(false)) }, "强度取消全选")
367
+ : null),
368
+ react.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "8px" } },
369
+ draft.models.map(modelRow),
370
+ react.createElement("button", {
371
+ type: "button",
372
+ className: "dkm-add",
373
+ onClick: () => patchDraft({
374
+ models: draft.models.concat([{ id: "", name: "", contextWindow: "", maxTokens: "", input: ["text"], tags: [], effortsMode: "inherit", effortLevels: {} }])
375
+ })
376
+ }, "+ 添加模型")),
377
+ react.createElement("div", { className: "dkm-savebar" },
378
+ react.createElement("span", { className: "dkm-msg" }, dirty ? "有未保存的修改" : "无未保存修改"),
379
+ 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(); } }, "重新拉取"),
381
+ msg ? react.createElement("span", { className: "dkm-msg " + (msg.ok ? "ok" : "err") }, msg.text) : null)));
382
+ }
383
+ return react.createElement("div", { className: "dkm-list" }, body);
384
+ }
385
+
386
+ function ModelsStat() {
387
+ const snap = useModels();
388
+ const data = snap.data;
389
+ if (snap.error) return react.createElement("span", { className: "dockm-err" }, "模型目录拉取失败(点击进入查看)");
390
+ if (!data) return react.createElement("span", null, snap.loading ? "正在拉取模型目录…" : "等待拉取模型目录");
391
+ const providers = Array.isArray(data.providers) ? data.providers : [];
392
+ const total = providers.reduce((n, p) => n + (p.models ? p.models.length : 0), 0);
393
+ return react.createElement("span", null, providers.length + " 个 Provider · " + total + " 个模型");
394
+ }
395
+
396
+ export const feature = {
397
+ id: "modelconfig",
398
+ name: "模型设置",
399
+ order: 100,
400
+ accent: "#22d3ee",
401
+ description: "编辑各 Provider 模型目录:输入类型(文本/图片 + 标注)与思考强度档位;写回官方配置热生效,会话模型选择器即时可选",
402
+ css: [
403
+ ".dkm-note{color:var(--dsw-alias-label-secondary);font-size:12px;}",
404
+ ".dkm-error{color:var(--dsw-alias-state-error-primary);font-size:12px;}",
405
+ ".dkm-chips{display:flex;flex-wrap:wrap;gap:6px;}",
406
+ ".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;}",
407
+ ".dkm-chip.on{color:var(--dsw-alias-accent,#4d9fff);border-color:currentColor;}",
408
+ ".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;}",
409
+ ".dkm-prov-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
410
+ ".dkm-name{font-weight:600;}",
411
+ ".dkm-sub{color:var(--dsw-alias-label-tertiary);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;}",
412
+ ".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);}",
413
+ ".dkm-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-top:1px solid var(--dsw-alias-border-l1);padding-top:8px;}",
414
+ ".dkm-label{color:var(--dsw-alias-label-secondary);font-size:12px;flex:none;}",
415
+ ".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;}",
416
+ ".dkm-model{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;display:flex;flex-direction:column;gap:6px;}",
417
+ ".dkm-model-head{display:flex;align-items:center;gap:6px;}",
418
+ ".dkm-model-idx{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none;}",
419
+ ".dkm-grid{display:grid;grid-template-columns:1.3fr 1.3fr .8fr .8fr;gap:6px;align-items:center;}",
420
+ ".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;}",
421
+ ".dkm-checks{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}",
422
+ ".dkm-check{display:inline-flex;align-items:center;gap:5px;cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;}",
423
+ ".dkm-check input{accent-color:var(--dsw-alias-accent,#4d9fff);margin:0;}",
424
+ ".dkm-tag{color:var(--dsw-alias-label-tertiary);}",
425
+ ".dkm-efforts{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}",
426
+ ".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;}",
427
+ ".dkm-del:hover{color:var(--dsw-alias-state-error-primary);border-color:currentColor;}",
428
+ ".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;}",
429
+ ".dkm-add:hover{color:var(--dsw-alias-label-primary);}",
430
+ ".dkm-savebar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px;}",
431
+ ".dkm-save{cursor:pointer;border:1px solid var(--dsw-alias-accent,#4d9fff);color:var(--dsw-alias-accent,#4d9fff);background:transparent;border-radius:8px;padding:4px 14px;font-family:inherit;font-size:12px;flex:none;}",
432
+ ".dkm-save:hover{background:var(--dsw-alias-interactive-bg-hover);}",
433
+ ".dkm-save[disabled]{opacity:.5;cursor:default;}",
434
+ ".dkm-msg{font-size:12px;color:var(--dsw-alias-label-secondary);}",
435
+ ".dkm-msg.ok{color:var(--dsw-alias-state-success-primary);}",
436
+ ".dkm-msg.err{color:var(--dsw-alias-state-error-primary);}",
437
+ ".dkm-list{display:flex;flex-direction:column;gap:8px;}",
438
+ ".dkm-ro{color:var(--dsw-alias-label-secondary);font-size:12px;}",
439
+ ".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
+ ".dkm-mini:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-secondary);}",
441
+ ".dkm-toolbar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;border-top:1px dashed var(--dsw-alias-border-l1);padding-top:8px;}",
442
+ ".dkm-toolbar-label{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none;}"
443
+ ].join("\n"),
444
+ View: ModelsView,
445
+ HomeStat: ModelsStat,
446
+ };
@@ -0,0 +1,35 @@
1
+ // dsh-dock · 功能模块【主题信息】· 客户端视图(纯 Client 功能,无宿主半部)
2
+ import react from "react";
3
+
4
+ function readThemeLabel(ctx) {
5
+ const theme = ctx && ctx.get ? ctx.get("theme") : undefined;
6
+ if (theme === undefined) return { err: "theme 服务不可用" };
7
+ 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 : "未知";
12
+ return { label };
13
+ }
14
+
15
+ function ThemeView(props) {
16
+ const r = readThemeLabel(props && props.ctx);
17
+ if (r.err) return react.createElement("div", null, r.err);
18
+ return react.createElement("div", null, "当前主题:" + r.label);
19
+ }
20
+
21
+ function ThemeStat(props) {
22
+ const r = readThemeLabel(props && props.ctx);
23
+ if (r.err) return react.createElement("span", null, r.err);
24
+ return react.createElement("span", null, "当前主题:" + r.label);
25
+ }
26
+
27
+ export const feature = {
28
+ id: "theme",
29
+ name: "主题信息",
30
+ order: 150,
31
+ accent: "#a78bfa",
32
+ description: "示例功能:读取当前主题快照(纯 Client)",
33
+ View: ThemeView,
34
+ HomeStat: ThemeStat,
35
+ };