dsh-dock 0.9.6 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -18
- package/README.md +325 -325
- package/client.js +42 -8
- package/cordis.patch.yml +2 -2
- package/features/animation/host.js +660 -660
- package/features/animation/view.jsx +2247 -2247
- package/features/balance/host.js +494 -494
- package/features/balance/view.js +351 -351
- package/features/games/games/breakout.jsx +227 -227
- package/features/games/games/gomoku.jsx +244 -244
- package/features/games/games/racer.jsx +169 -169
- package/features/games/games/shared.jsx +118 -118
- package/features/games/games/snake.jsx +144 -144
- package/features/games/games/sokoban.jsx +180 -180
- package/features/games/games/tank.jsx +128 -128
- package/features/games/games/tetris.jsx +308 -308
- package/features/games/games/xiangqi.jsx +277 -277
- package/features/games/tank-core.js +152 -152
- package/features/games/view.jsx +639 -639
- package/features/games/xiangqi-core.js +299 -299
- package/features/heartbeat/view.js +35 -35
- package/features/mobile-relay/gateway.js +472 -472
- package/features/mobile-relay/host.js +570 -569
- package/features/mobile-relay/view.jsx +300 -300
- package/features/modelconfig/host.js +403 -403
- package/features/modelconfig/view.js +490 -490
- package/features/theme/view.js +61 -61
- package/features/tokenlog/host.js +690 -678
- package/features/tokenlog/view.jsx +561 -561
- package/features/visionproxy/host.js +315 -315
- package/index.js +175 -118
- package/package.json +59 -59
- package/src/client.jsx +668 -668
- package/src/host-core.js +98 -95
- 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
|
+
};
|