dsh-dock 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -8
- package/client.js +1523 -486
- package/features/animation/view.jsx +2 -2
- package/features/balance/view.js +1 -1
- package/features/games/view.jsx +1 -1
- package/features/mobile/host.js +320 -0
- package/features/mobile/view.js +527 -0
- package/features/mobile-relay/gateway.js +187 -11
- package/features/mobile-relay/host.js +58 -173
- package/features/mobile-relay/view.jsx +19 -209
- package/features/modelconfig/host.js +195 -1
- package/features/modelconfig/view.js +762 -92
- package/features/notify/view.jsx +2 -2
- package/features/runstate/view.jsx +2 -2
- package/features/tokenlog/view.jsx +2 -2
- package/index.js +20 -2
- package/package.json +9 -3
- package/screenshots/dock-modelconfig-provider-create.png +0 -0
- package/screenshots/dock-modelconfig-provider-edit.png +0 -0
- package/screenshots/dock-modelconfig-providers.png +0 -0
- package/screenshots/dock-modelconfig.png +0 -0
- package/src/client.jsx +3 -2
- package/src/host-core.js +129 -0
- package/src/shared.js +6 -3
- package/src/welcome-ack.js +69 -0
|
@@ -15,7 +15,7 @@ const modelsStore = {
|
|
|
15
15
|
this.set({ loading: true, error: null });
|
|
16
16
|
// Provider 可在 DSH 原生模型设置中随时增删;不要复用浏览器缓存,
|
|
17
17
|
// 否则返回本页时仍可能看到旧的共享快照。
|
|
18
|
-
fetch("
|
|
18
|
+
fetch("dsh-dock/models", { cache: "no-store", signal: AbortSignal.timeout(20000) })
|
|
19
19
|
.then((res) => (res.ok ? res.json() : Promise.reject(new Error("模型目录接口 HTTP " + res.status))))
|
|
20
20
|
.then((data) => this.set({ data: data, loading: false, error: null }))
|
|
21
21
|
.catch((e) => this.set({ loading: false, error: (e && e.message) || String(e) }));
|
|
@@ -38,6 +38,100 @@ const DKM_TAGS = [["video", "视频"], ["audio", "音频"], ["document", "文档
|
|
|
38
38
|
const DKM_PI_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
39
39
|
const DKM_DS_LEVELS = ["off", "low", "high", "max"];
|
|
40
40
|
const DKM_EFFORT_NAMES = { off: "关闭", minimal: "极低", low: "低", medium: "中", high: "高", xhigh: "超高", max: "最高" };
|
|
41
|
+
// Provider 级增删改:api 协议选项(与宿主 PI_AI_APIS 一致)与路由 key 约束(与宿主同规则)
|
|
42
|
+
const DKM_APIS = [
|
|
43
|
+
["openai-completions", "OpenAI 兼容(chat/completions)"],
|
|
44
|
+
["openai-responses", "OpenAI Responses"],
|
|
45
|
+
["anthropic-messages", "Anthropic Messages"]
|
|
46
|
+
];
|
|
47
|
+
const DKM_ROUTE_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
48
|
+
|
|
49
|
+
/** 由路由 key 推导凭据库条目名:jiyuanlvdong -> JIYUANLVDONG_API_KEY。让 apiKeyEnv 可以真的留空。 */
|
|
50
|
+
function dkmEnvFromRoute(route) {
|
|
51
|
+
const s = String(route || "").toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
52
|
+
if (!s) return "";
|
|
53
|
+
return (/^[0-9]/.test(s) ? "KEY_" + s : s) + "_API_KEY";
|
|
54
|
+
}
|
|
55
|
+
const DKM_ALL_INPUT = DKM_MODALITIES.map((x) => x[0]);
|
|
56
|
+
const DKM_ALL_TAGS = DKM_TAGS.map((x) => x[0]);
|
|
57
|
+
|
|
58
|
+
function dkmToggleIn(list, v) {
|
|
59
|
+
return list.indexOf(v) >= 0 ? list.filter((x) => x !== v) : list.concat([v]);
|
|
60
|
+
}
|
|
61
|
+
function dkmAllInputs(on) {
|
|
62
|
+
return { input: on ? DKM_ALL_INPUT.slice() : [], tags: on ? DKM_ALL_TAGS.slice() : [] };
|
|
63
|
+
}
|
|
64
|
+
function dkmAllLevels(on) {
|
|
65
|
+
const lv = {};
|
|
66
|
+
for (const k of DKM_PI_LEVELS) lv[k] = !!on;
|
|
67
|
+
return { effortsMode: "custom", effortLevels: lv };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 单个目录模型条目 → 可编辑草稿(目录编辑器与 Provider 表单共用)。 */
|
|
71
|
+
function draftFromModel(m) {
|
|
72
|
+
// effortMap: {档位: wire},声明过(wire 字符串或 null)即视为勾选,undefined = 未提供
|
|
73
|
+
const levels = {};
|
|
74
|
+
if (m.efforts === "custom" && m.effortMap && typeof m.effortMap === "object") {
|
|
75
|
+
for (const lv of DKM_PI_LEVELS) levels[lv] = m.effortMap[lv] !== undefined;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
id: m.id || "",
|
|
79
|
+
name: m.name || "",
|
|
80
|
+
contextWindow: m.contextWindow != null ? String(m.contextWindow) : "",
|
|
81
|
+
maxTokens: m.maxTokens != null ? String(m.maxTokens) : "",
|
|
82
|
+
input: m.input && m.input.length ? m.input.slice() : ["text"],
|
|
83
|
+
tags: (m.tags || []).slice(),
|
|
84
|
+
effortsMode: m.efforts === "custom" ? "custom" : m.efforts === "off" ? "off" : "inherit",
|
|
85
|
+
effortLevels: levels,
|
|
86
|
+
raw: m.raw && typeof m.raw === "object" ? m.raw : null
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 空白模型行(新增 Provider / 添加模型用)。 */
|
|
91
|
+
function emptyModelDraft() {
|
|
92
|
+
return { id: "", name: "", contextWindow: "", maxTokens: "", input: ["text"], tags: [], effortsMode: "inherit", effortLevels: {}, raw: null };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 新增 Provider 的空表单。 */
|
|
96
|
+
function emptyProviderForm() {
|
|
97
|
+
return {
|
|
98
|
+
route: "",
|
|
99
|
+
displayName: "",
|
|
100
|
+
api: "openai-completions",
|
|
101
|
+
baseURL: "",
|
|
102
|
+
apiKeyEnv: "",
|
|
103
|
+
apiKey: "",
|
|
104
|
+
defaultInput: ["text"],
|
|
105
|
+
models: [],
|
|
106
|
+
extra: {}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 由目录 Provider 视图构建「编辑 Provider」表单。
|
|
112
|
+
* 以用户层原始配置(rawProfile)为准回填;面板不负责的未知字段(自配 compat/description 等)
|
|
113
|
+
* 收进 extra,保存时原样并回,避免把用户手写配置抹掉。API Key 明文永不回显。
|
|
114
|
+
*/
|
|
115
|
+
function providerFormFromProvider(p) {
|
|
116
|
+
const raw = p.rawProfile && typeof p.rawProfile === "object" && !Array.isArray(p.rawProfile) ? p.rawProfile : {};
|
|
117
|
+
const extra = Object.assign({}, raw);
|
|
118
|
+
delete extra.displayName; delete extra.api; delete extra.baseURL;
|
|
119
|
+
delete extra.apiKeyEnv; delete extra.defaultInput; delete extra.models;
|
|
120
|
+
const models = draftFromProvider(p).models;
|
|
121
|
+
return {
|
|
122
|
+
route: p.route || p.id || "",
|
|
123
|
+
displayName: typeof raw.displayName === "string" ? raw.displayName : (p.displayName || ""),
|
|
124
|
+
api: typeof raw.api === "string" && raw.api ? raw.api : (p.api || "openai-completions"),
|
|
125
|
+
baseURL: typeof raw.baseURL === "string" ? raw.baseURL : (p.baseURL || ""),
|
|
126
|
+
apiKeyEnv: typeof raw.apiKeyEnv === "string" ? raw.apiKeyEnv : (p.apiKeyEnv || ""),
|
|
127
|
+
apiKey: "",
|
|
128
|
+
defaultInput: Array.isArray(raw.defaultInput) && raw.defaultInput.length
|
|
129
|
+
? raw.defaultInput.slice()
|
|
130
|
+
: (Array.isArray(p.defaultInput) && p.defaultInput.length ? p.defaultInput.slice() : ["text"]),
|
|
131
|
+
models: models.length ? models : [emptyModelDraft()],
|
|
132
|
+
extra
|
|
133
|
+
};
|
|
134
|
+
}
|
|
41
135
|
|
|
42
136
|
/** 从目录 Provider 视图构建可编辑草稿。 */
|
|
43
137
|
function draftFromProvider(p) {
|
|
@@ -46,31 +140,87 @@ function draftFromProvider(p) {
|
|
|
46
140
|
kind: p.kind,
|
|
47
141
|
thinking: p.thinking === "disabled" ? "disabled" : "enabled",
|
|
48
142
|
defaultEffort: DKM_DS_LEVELS.indexOf(p.defaultEffort) >= 0 ? p.defaultEffort : "high",
|
|
49
|
-
models: (p.models || []).map(
|
|
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
|
-
})
|
|
143
|
+
models: (p.models || []).map(draftFromModel)
|
|
67
144
|
};
|
|
68
145
|
}
|
|
69
146
|
|
|
70
|
-
|
|
147
|
+
/**
|
|
148
|
+
* 单个模型编辑行(Provider 表单与目录模型编辑器共用;无 hook,可直接当纯函数调用)。
|
|
149
|
+
* props: { m, i, kind, onPatch(i, patch), onDel(i) }
|
|
150
|
+
*/
|
|
151
|
+
function ModelRow(props) {
|
|
152
|
+
const m = props.m, i = props.i, kind = props.kind, onPatch = props.onPatch, onDel = props.onDel;
|
|
153
|
+
return react.createElement("div", { key: i, className: "dkm-model" },
|
|
154
|
+
react.createElement("div", { className: "dkm-model-head" },
|
|
155
|
+
react.createElement("span", { className: "dkm-model-idx" }, "#" + (i + 1)),
|
|
156
|
+
react.createElement("button", { type: "button", className: "dkm-del", onClick: () => onDel(i) }, "删除")),
|
|
157
|
+
react.createElement("div", { className: "dkm-grid" },
|
|
158
|
+
react.createElement("input", { className: "dkm-input", placeholder: "模型 id(必填,如 deepseek-v4-pro)", value: m.id, onChange: (e) => onPatch(i, { id: e.target.value }) }),
|
|
159
|
+
react.createElement("input", { className: "dkm-input", placeholder: "显示名称(可选)", value: m.name, onChange: (e) => onPatch(i, { name: e.target.value }) }),
|
|
160
|
+
react.createElement("input", { className: "dkm-input", placeholder: "上下文窗口", inputMode: "numeric", value: m.contextWindow, onChange: (e) => onPatch(i, { contextWindow: e.target.value }) }),
|
|
161
|
+
react.createElement("input", { className: "dkm-input", placeholder: "最大输出", inputMode: "numeric", value: m.maxTokens, onChange: (e) => onPatch(i, { maxTokens: e.target.value }) })),
|
|
162
|
+
react.createElement("div", { className: "dkm-checks" },
|
|
163
|
+
react.createElement("span", { className: "dkm-label" }, "输入类型:"),
|
|
164
|
+
DKM_MODALITIES.map(([v, label]) => react.createElement("label", {
|
|
165
|
+
key: v,
|
|
166
|
+
className: "dkm-check",
|
|
167
|
+
title: v === "image" ? "由你声明该端点支持图片:保存后原图直发,并可作为视觉模型候选。系统不会替你猜测;端点实际不支持时取消勾选即可恢复走图片理解代理" : null
|
|
168
|
+
},
|
|
169
|
+
react.createElement("input", {
|
|
170
|
+
type: "checkbox",
|
|
171
|
+
checked: m.input.indexOf(v) >= 0,
|
|
172
|
+
onChange: () => {
|
|
173
|
+
const next = dkmToggleIn(m.input, v);
|
|
174
|
+
onPatch(i, { input: next.length > 0 ? next : ["text"] });
|
|
175
|
+
}
|
|
176
|
+
}),
|
|
177
|
+
label)),
|
|
178
|
+
react.createElement("span", { className: "dkm-label dkm-tag", title: "标注仅在本面板展示与持久化,不参与请求路由(官方 schema 只接受文本/图片)" }, "标注:"),
|
|
179
|
+
DKM_TAGS.map(([v, label]) => react.createElement("label", { key: v, className: "dkm-check dkm-tag" },
|
|
180
|
+
react.createElement("input", { type: "checkbox", checked: m.tags.indexOf(v) >= 0, onChange: () => onPatch(i, { tags: dkmToggleIn(m.tags, v) }) }),
|
|
181
|
+
label)),
|
|
182
|
+
react.createElement("button", { type: "button", className: "dkm-mini", title: "一键勾选全部输入类型与标注", onClick: () => onPatch(i, dkmAllInputs(true)) }, "全选"),
|
|
183
|
+
react.createElement("button", { type: "button", className: "dkm-mini", title: "一键取消全部勾选(输入留空 = 继承目录默认)", onClick: () => onPatch(i, dkmAllInputs(false)) }, "取消全选")),
|
|
184
|
+
react.createElement("div", { className: "dkm-efforts" },
|
|
185
|
+
react.createElement("span", { className: "dkm-label" }, "思考强度:"),
|
|
186
|
+
kind === "deepseek"
|
|
187
|
+
? react.createElement("span", { className: "dkm-sub" }, "随 Provider 设置(Off/Low/High/Max,全模型共享)")
|
|
188
|
+
: react.createElement("span", null,
|
|
189
|
+
react.createElement("select", {
|
|
190
|
+
className: "dkm-select",
|
|
191
|
+
value: m.effortsMode,
|
|
192
|
+
onChange: (e) => onPatch(i, {
|
|
193
|
+
effortsMode: e.target.value,
|
|
194
|
+
effortLevels: e.target.value === "custom" && m.effortsMode !== "custom"
|
|
195
|
+
? { off: true, low: true, high: true }
|
|
196
|
+
: m.effortLevels
|
|
197
|
+
})
|
|
198
|
+
},
|
|
199
|
+
react.createElement("option", { value: "inherit" }, "跟随目录默认"),
|
|
200
|
+
react.createElement("option", { value: "off" }, "不支持思考"),
|
|
201
|
+
react.createElement("option", { value: "custom" }, "自定义档位")),
|
|
202
|
+
m.effortsMode === "custom"
|
|
203
|
+
? DKM_PI_LEVELS.map((lv) => react.createElement("label", { key: lv, className: "dkm-check" },
|
|
204
|
+
react.createElement("input", {
|
|
205
|
+
type: "checkbox",
|
|
206
|
+
checked: !!m.effortLevels[lv],
|
|
207
|
+
onChange: () => {
|
|
208
|
+
const nl = Object.assign({}, m.effortLevels);
|
|
209
|
+
nl[lv] = !nl[lv];
|
|
210
|
+
onPatch(i, { effortLevels: nl });
|
|
211
|
+
}
|
|
212
|
+
}),
|
|
213
|
+
DKM_EFFORT_NAMES[lv] || lv))
|
|
214
|
+
: null,
|
|
215
|
+
react.createElement("button", { type: "button", className: "dkm-mini", title: "一键勾选全部强度档(切为自定义)", onClick: () => onPatch(i, dkmAllLevels(true)) }, "全选"),
|
|
216
|
+
react.createElement("button", { type: "button", className: "dkm-mini", title: "一键取消全部强度档", onClick: () => onPatch(i, dkmAllLevels(false)) }, "取消全选"))));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function ModelsView(props) {
|
|
71
220
|
const snap = useModels();
|
|
72
221
|
const data = snap.data;
|
|
73
222
|
const providers = data && Array.isArray(data.providers) ? data.providers : [];
|
|
223
|
+
const params = props && props.params;
|
|
74
224
|
const [selId, setSelId] = react.useState(null);
|
|
75
225
|
const [draft, setDraft] = react.useState(null);
|
|
76
226
|
const [builtKey, setBuiltKey] = react.useState(null);
|
|
@@ -84,6 +234,10 @@ function ModelsView() {
|
|
|
84
234
|
const [vpSaving, setVpSaving] = react.useState(false);
|
|
85
235
|
const [vpMsg, setVpMsg] = react.useState(null);
|
|
86
236
|
const [vpDirectExpanded, setVpDirectExpanded] = react.useState(false);
|
|
237
|
+
// 子页面路由:page = "settings"(模型设置主页)| "providers"(模型提供商子页面)
|
|
238
|
+
const [page, setPage] = react.useState("settings");
|
|
239
|
+
// 进入「模型提供商」子页面时是否直接展开新增表单(deep-link:params.openProviderForm)
|
|
240
|
+
const [pageOpenCreate, setPageOpenCreate] = react.useState(false);
|
|
87
241
|
|
|
88
242
|
// 每次进入模型设置都重新读取官方目录。modelsStore 是跨页面共享快照,
|
|
89
243
|
// 原生设置刚新增 Provider 时不能要求用户再滚到页面底部手动刷新。
|
|
@@ -112,7 +266,7 @@ function ModelsView() {
|
|
|
112
266
|
const saveVp = () => {
|
|
113
267
|
if (!vpDraft || vpSaving) return;
|
|
114
268
|
setVpSaving(true);
|
|
115
|
-
fetch("
|
|
269
|
+
fetch("dsh-dock/models", {
|
|
116
270
|
method: "POST",
|
|
117
271
|
headers: { "content-type": "application/json" },
|
|
118
272
|
body: JSON.stringify({ visionProxy: vpDraft, revisions: (data && data.revisions) || {} }),
|
|
@@ -152,6 +306,14 @@ function ModelsView() {
|
|
|
152
306
|
const bulkModels = (fn) => patchDraft({ models: draft.models.map((m) => Object.assign({}, m, fn(m))) });
|
|
153
307
|
const selectProvider = (id) => { setSelId(id); setMsg(null); };
|
|
154
308
|
|
|
309
|
+
// deep-link:params.page === "providers" 直达「模型提供商」子页面;
|
|
310
|
+
// params.openProviderForm 再直接展开新增表单(设置页深链与渲染回归测试共用)。
|
|
311
|
+
react.useEffect(() => {
|
|
312
|
+
if (params && params.page === "providers") setPage("providers");
|
|
313
|
+
if (params && params.openProviderForm) { setPage("providers"); setPageOpenCreate(true); }
|
|
314
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
315
|
+
}, []);
|
|
316
|
+
|
|
155
317
|
const save = () => {
|
|
156
318
|
if (!draft || !cur || !dirty || saving) return;
|
|
157
319
|
const models = draft.models.map((m) => ({
|
|
@@ -171,7 +333,7 @@ function ModelsView() {
|
|
|
171
333
|
payload.defaultEffort = draft.defaultEffort;
|
|
172
334
|
}
|
|
173
335
|
setSaving(true);
|
|
174
|
-
fetch("
|
|
336
|
+
fetch("dsh-dock/models", {
|
|
175
337
|
method: "POST",
|
|
176
338
|
headers: { "content-type": "application/json" },
|
|
177
339
|
body: JSON.stringify(payload),
|
|
@@ -192,74 +354,28 @@ function ModelsView() {
|
|
|
192
354
|
.then(() => setSaving(false));
|
|
193
355
|
};
|
|
194
356
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
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)) }, "取消全选"))));
|
|
357
|
+
// 目录模型编辑行:委托给共用的 ModelRow(kind 用当前 Provider 的类型)
|
|
358
|
+
const modelRow = (m, i) => ModelRow({
|
|
359
|
+
m, i, kind: cur.kind,
|
|
360
|
+
onPatch: (j, patch) => patchModel(j, patch),
|
|
361
|
+
onDel: (j) => patchDraft({ models: draft.models.filter((_, k) => k !== j) })
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ---- 子页面:模型提供商 ----
|
|
365
|
+
// 整块替换模型设置主页内容(官方「模型」设置页同款布局)。这里是独立组件,
|
|
366
|
+
// 因此返回主页时自动卸载——未保存的 Provider 草稿不会悄悄留在后台。
|
|
367
|
+
if (page === "providers") {
|
|
368
|
+
return react.createElement(ProvidersPage, {
|
|
369
|
+
data: data,
|
|
370
|
+
providers: providers,
|
|
371
|
+
loading: snap.loading,
|
|
372
|
+
error: snap.error,
|
|
373
|
+
openCreate: pageOpenCreate,
|
|
374
|
+
onCreateHandled: () => setPageOpenCreate(false),
|
|
375
|
+
// 返回主页时顺手重读目录:子页面里增删改过的 Provider 立即反映到模型编辑器
|
|
376
|
+
onBack: () => { setPage("settings"); refreshModels(); }
|
|
377
|
+
});
|
|
378
|
+
}
|
|
263
379
|
|
|
264
380
|
const body = [];
|
|
265
381
|
if (data) {
|
|
@@ -367,6 +483,33 @@ function ModelsView() {
|
|
|
367
483
|
: react.createElement("span", { className: "dkm-msg" }, vpDirty ? "有未保存的修改" : ""))));
|
|
368
484
|
}
|
|
369
485
|
|
|
486
|
+
// ---- 模型提供商入口卡(子页面入口;卡片外观与官方模型管理页的行卡片一致)----
|
|
487
|
+
if (data) {
|
|
488
|
+
const custom = providers.filter((p) => p.kind === "pi-ai");
|
|
489
|
+
const missingKey = custom.filter((p) => p.apiKeyEnv && !p.apiKeyConfigured).length;
|
|
490
|
+
body.push(react.createElement("div", { key: "pf-entry", className: "dkp-card" },
|
|
491
|
+
react.createElement("div", { className: "dkp-rowhead" },
|
|
492
|
+
react.createElement("span", { className: "dkp-identity" },
|
|
493
|
+
react.createElement("span", { className: "dkp-name" }, "模型提供商"),
|
|
494
|
+
react.createElement("span", { className: "dkp-tag" }, custom.length + " 个自定义路由"),
|
|
495
|
+
custom.length > 0
|
|
496
|
+
? react.createElement("span", {
|
|
497
|
+
className: "dkp-dot " + (missingKey > 0 ? "warn" : "ok"),
|
|
498
|
+
role: "img",
|
|
499
|
+
title: missingKey > 0 ? "有 " + missingKey + " 个 Provider 未配置密钥" : "密钥均已配置",
|
|
500
|
+
"aria-label": missingKey > 0 ? "有 Provider 未配置密钥" : "密钥均已配置"
|
|
501
|
+
})
|
|
502
|
+
: null),
|
|
503
|
+
react.createElement("span", { className: "dkp-actions" },
|
|
504
|
+
react.createElement("button", {
|
|
505
|
+
type: "button",
|
|
506
|
+
className: "dkp-btn ghost sm",
|
|
507
|
+
onClick: () => setPage("providers")
|
|
508
|
+
}, "管理"))),
|
|
509
|
+
react.createElement("p", { className: "dkp-hint" },
|
|
510
|
+
"新增 / 编辑 / 删除自定义模型 Provider(OpenAI 兼容、Anthropic 等),写入官方配置并热生效;API Key 存本机凭据库(文件仅 root 可读)。")));
|
|
511
|
+
}
|
|
512
|
+
|
|
370
513
|
if (data && providers.length === 0) {
|
|
371
514
|
body.push(react.createElement("div", { key: "empty", className: "dkm-note" }, "没有可编辑的模型 Provider(Host 侧尚未配置)"));
|
|
372
515
|
}
|
|
@@ -423,6 +566,441 @@ function ModelsView() {
|
|
|
423
566
|
return react.createElement("div", { className: "dkm-list" }, body);
|
|
424
567
|
}
|
|
425
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Provider 表单里的单个模型行(官方模型行同款:id / 名称 / 容量 一行网格,下方输入类型与思考强度)。
|
|
571
|
+
* props: { m, i, onPatch(i, patch), onDel(i) }
|
|
572
|
+
*/
|
|
573
|
+
function ProviderModelRow(props) {
|
|
574
|
+
const m = props.m;
|
|
575
|
+
const i = props.i;
|
|
576
|
+
const check = (v, list, kind) => react.createElement("label", { key: v[0], className: "dkp-check" },
|
|
577
|
+
react.createElement("input", {
|
|
578
|
+
type: "checkbox",
|
|
579
|
+
checked: list.indexOf(v[0]) >= 0,
|
|
580
|
+
onChange: () => {
|
|
581
|
+
const next = dkmToggleIn(list, v[0]);
|
|
582
|
+
props.onPatch(i, kind === "input" ? { input: next.length > 0 ? next : ["text"] } : { tags: next });
|
|
583
|
+
}
|
|
584
|
+
}),
|
|
585
|
+
v[1]);
|
|
586
|
+
return react.createElement("div", { className: "dkp-model-entry" },
|
|
587
|
+
react.createElement("div", { className: "dkp-model-row" },
|
|
588
|
+
react.createElement("input", { className: "dkp-input", placeholder: "模型 id(必填)", value: m.id, onChange: (e) => props.onPatch(i, { id: e.target.value }) }),
|
|
589
|
+
react.createElement("input", { className: "dkp-input", placeholder: "显示名称(可选)", value: m.name, onChange: (e) => props.onPatch(i, { name: e.target.value }) }),
|
|
590
|
+
react.createElement("input", { className: "dkp-input", placeholder: "上下文窗口", inputMode: "numeric", value: m.contextWindow, onChange: (e) => props.onPatch(i, { contextWindow: e.target.value }) }),
|
|
591
|
+
react.createElement("input", { className: "dkp-input", placeholder: "最大输出", inputMode: "numeric", value: m.maxTokens, onChange: (e) => props.onPatch(i, { maxTokens: e.target.value }) }),
|
|
592
|
+
react.createElement("button", { type: "button", className: "dkp-iconbtn danger", title: "删除该模型", "aria-label": "删除模型", onClick: () => props.onDel(i) }, "✕")),
|
|
593
|
+
react.createElement("div", { className: "dkp-model-opts" },
|
|
594
|
+
react.createElement("div", { className: "dkp-checks" },
|
|
595
|
+
react.createElement("span", { className: "dkp-flabel" }, "输入类型:"),
|
|
596
|
+
DKM_MODALITIES.map((v) => check(v, m.input, "input")),
|
|
597
|
+
react.createElement("span", { className: "dkp-flabel dkp-dim", title: "标注仅在本面板展示与持久化,不参与请求路由(官方 schema 只接受文本/图片)" }, "标注:"),
|
|
598
|
+
DKM_TAGS.map((v) => check(v, m.tags, "tags")),
|
|
599
|
+
react.createElement("button", { type: "button", className: "dkp-mini", title: "一键勾选全部输入类型与标注", onClick: () => props.onPatch(i, dkmAllInputs(true)) }, "全选"),
|
|
600
|
+
react.createElement("button", { type: "button", className: "dkp-mini", title: "一键取消全部勾选", onClick: () => props.onPatch(i, dkmAllInputs(false)) }, "取消全选")),
|
|
601
|
+
react.createElement("div", { className: "dkp-checks" },
|
|
602
|
+
react.createElement("span", { className: "dkp-flabel" }, "思考强度:"),
|
|
603
|
+
react.createElement("select", {
|
|
604
|
+
className: "dkp-input dkp-select dkp-select-sm",
|
|
605
|
+
value: m.effortsMode,
|
|
606
|
+
onChange: (e) => props.onPatch(i, {
|
|
607
|
+
effortsMode: e.target.value,
|
|
608
|
+
effortLevels: e.target.value === "custom" && m.effortsMode !== "custom"
|
|
609
|
+
? { off: true, low: true, high: true }
|
|
610
|
+
: m.effortLevels
|
|
611
|
+
})
|
|
612
|
+
},
|
|
613
|
+
react.createElement("option", { value: "inherit" }, "跟随目录默认"),
|
|
614
|
+
react.createElement("option", { value: "off" }, "不支持思考"),
|
|
615
|
+
react.createElement("option", { value: "custom" }, "自定义档位")),
|
|
616
|
+
m.effortsMode === "custom"
|
|
617
|
+
? DKM_PI_LEVELS.map((lv) => react.createElement("label", { key: lv, className: "dkp-check" },
|
|
618
|
+
react.createElement("input", {
|
|
619
|
+
type: "checkbox",
|
|
620
|
+
checked: !!m.effortLevels[lv],
|
|
621
|
+
onChange: () => {
|
|
622
|
+
const nl = Object.assign({}, m.effortLevels);
|
|
623
|
+
nl[lv] = !nl[lv];
|
|
624
|
+
props.onPatch(i, { effortLevels: nl });
|
|
625
|
+
}
|
|
626
|
+
}),
|
|
627
|
+
DKM_EFFORT_NAMES[lv] || lv))
|
|
628
|
+
: null,
|
|
629
|
+
react.createElement("button", { type: "button", className: "dkp-mini", title: "一键勾选全部强度档(切为自定义)", onClick: () => props.onPatch(i, dkmAllLevels(true)) }, "全选"),
|
|
630
|
+
react.createElement("button", { type: "button", className: "dkp-mini", title: "一键取消全部强度档", onClick: () => props.onPatch(i, dkmAllLevels(false)) }, "取消全选"))));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* 模型提供商子页面:官方「模型」设置页同款布局——
|
|
635
|
+
* 返回入口 + 标题 + 说明 + 行卡片列表(每行可展开同一个编辑卡片)+ 底部虚线「新增 Provider」。
|
|
636
|
+
* 只管自定义 pi-ai 路由的增删改(内置 DeepSeek 官方 Provider 在主页编辑其模型目录)。
|
|
637
|
+
* 由 ModelsView 在 page === "providers" 时整页替换渲染;返回主页即卸载,草稿不会留在后台。
|
|
638
|
+
*/
|
|
639
|
+
function ProvidersPage(props) {
|
|
640
|
+
const data = props.data;
|
|
641
|
+
const providers = props.providers || [];
|
|
642
|
+
const piProviders = providers.filter((p) => p.kind === "pi-ai");
|
|
643
|
+
// pfMode = "create" | "edit" | null;pfDraft 为对应草稿(同一时刻只开一个编辑卡片)
|
|
644
|
+
const [pfMode, setPfMode] = react.useState(null);
|
|
645
|
+
const [pfDraft, setPfDraft] = react.useState(null);
|
|
646
|
+
const [pfDirty, setPfDirty] = react.useState(false);
|
|
647
|
+
const [pfSaving, setPfSaving] = react.useState(false);
|
|
648
|
+
const [pfMsg, setPfMsg] = react.useState(null);
|
|
649
|
+
const [pfDelArm, setPfDelArm] = react.useState(null);
|
|
650
|
+
const [pfFetching, setPfFetching] = react.useState(false);
|
|
651
|
+
const [pfFetchedList, setPfFetchedList] = react.useState(null);
|
|
652
|
+
|
|
653
|
+
const openPfCreate = () => {
|
|
654
|
+
setPfMode("create"); setPfDraft(emptyProviderForm()); setPfDirty(false);
|
|
655
|
+
setPfMsg(null); setPfDelArm(null); setPfFetchedList(null);
|
|
656
|
+
};
|
|
657
|
+
const openPfEdit = (p) => {
|
|
658
|
+
setPfMode("edit"); setPfDraft(providerFormFromProvider(p)); setPfDirty(false);
|
|
659
|
+
setPfMsg(null); setPfDelArm(null); setPfFetchedList(null);
|
|
660
|
+
};
|
|
661
|
+
const closePf = () => {
|
|
662
|
+
setPfMode(null); setPfDraft(null); setPfDirty(false);
|
|
663
|
+
setPfMsg(null); setPfDelArm(null); setPfFetchedList(null);
|
|
664
|
+
};
|
|
665
|
+
const patchPf = (patch) => { setPfDraft(Object.assign({}, pfDraft, patch)); setPfDirty(true); setPfMsg(null); };
|
|
666
|
+
const patchPfModel = (i, patch) => {
|
|
667
|
+
const models = pfDraft.models.slice();
|
|
668
|
+
models[i] = Object.assign({}, models[i], patch);
|
|
669
|
+
patchPf({ models: models });
|
|
670
|
+
};
|
|
671
|
+
const delPfModel = (i) => patchPf({ models: pfDraft.models.filter((_, j) => j !== i) });
|
|
672
|
+
const addPfModel = () => patchPf({ models: pfDraft.models.concat([emptyModelDraft()]) });
|
|
673
|
+
// 勾选式选模型:checked 状态直接映射「该 id 是否在草稿模型列表里」
|
|
674
|
+
const togglePfModel = (id, name, on) => {
|
|
675
|
+
const base = pfDraft.models.filter((m) => String(m.id || "").trim());
|
|
676
|
+
if (on) {
|
|
677
|
+
if (base.some((m) => String(m.id).trim() === id)) return;
|
|
678
|
+
base.push(Object.assign(emptyModelDraft(), { id: id, name: name || "", input: pfDraft.defaultInput.slice() }));
|
|
679
|
+
} else {
|
|
680
|
+
const i = base.findIndex((m) => String(m.id).trim() === id);
|
|
681
|
+
if (i >= 0) base.splice(i, 1);
|
|
682
|
+
}
|
|
683
|
+
patchPf({ models: base });
|
|
684
|
+
};
|
|
685
|
+
const setAllPfModels = (on) => {
|
|
686
|
+
const base = [];
|
|
687
|
+
if (on && pfFetchedList) {
|
|
688
|
+
for (const mm of pfFetchedList) {
|
|
689
|
+
const id = String((mm && mm.id) || "").trim();
|
|
690
|
+
if (!id) continue;
|
|
691
|
+
base.push(Object.assign(emptyModelDraft(), { id: id, name: (mm && mm.name) || "", input: pfDraft.defaultInput.slice() }));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
patchPf({ models: base });
|
|
695
|
+
};
|
|
696
|
+
const pfSelectedCount = () => pfDraft ? pfDraft.models.filter((m) => String(m.id || "").trim()).length : 0;
|
|
697
|
+
const pfModelsPayload = () => pfDraft.models.map((m) => ({
|
|
698
|
+
id: String(m.id || "").trim(),
|
|
699
|
+
name: String(m.name || "").trim(),
|
|
700
|
+
contextWindow: m.contextWindow === "" || m.contextWindow == null ? undefined : Number(m.contextWindow),
|
|
701
|
+
maxTokens: m.maxTokens === "" || m.maxTokens == null ? undefined : Number(m.maxTokens),
|
|
702
|
+
input: m.input,
|
|
703
|
+
tags: m.tags,
|
|
704
|
+
effortsMode: m.effortsMode,
|
|
705
|
+
effortLevels: m.effortLevels,
|
|
706
|
+
raw: m.raw
|
|
707
|
+
}));
|
|
708
|
+
// 自动获取模型列表:宿主代打 GET {baseURL}/models(Key 还没保存也能先拉)
|
|
709
|
+
const fetchPfModels = () => {
|
|
710
|
+
if (!pfDraft || pfFetching) return;
|
|
711
|
+
setPfFetching(true); setPfMsg(null);
|
|
712
|
+
fetch("dsh-dock/models", {
|
|
713
|
+
method: "POST",
|
|
714
|
+
headers: { "content-type": "application/json" },
|
|
715
|
+
body: JSON.stringify({
|
|
716
|
+
action: "listModels",
|
|
717
|
+
baseURL: pfDraft.baseURL,
|
|
718
|
+
api: pfDraft.api,
|
|
719
|
+
apiKeyEnv: pfDraft.apiKeyEnv,
|
|
720
|
+
apiKey: pfDraft.apiKey
|
|
721
|
+
}),
|
|
722
|
+
signal: AbortSignal.timeout(30000)
|
|
723
|
+
})
|
|
724
|
+
.then((res) => res.json()
|
|
725
|
+
.then((b) => ({ status: res.status, body: b }))
|
|
726
|
+
.catch(() => ({ status: res.status, body: {} })))
|
|
727
|
+
.then((r) => {
|
|
728
|
+
if (r.status === 200 && r.body && r.body.ok) {
|
|
729
|
+
setPfFetchedList(Array.isArray(r.body.models) ? r.body.models : []);
|
|
730
|
+
} else {
|
|
731
|
+
setPfMsg({ ok: false, text: (r.body && r.body.error) || ("HTTP " + r.status) });
|
|
732
|
+
}
|
|
733
|
+
})
|
|
734
|
+
.catch((e) => setPfMsg({ ok: false, text: (e && e.message) || String(e) }))
|
|
735
|
+
.then(() => setPfFetching(false));
|
|
736
|
+
};
|
|
737
|
+
const savePf = () => {
|
|
738
|
+
if (!pfDraft || pfSaving) return;
|
|
739
|
+
const route = pfDraft.route.trim();
|
|
740
|
+
const baseURL = pfDraft.baseURL.trim();
|
|
741
|
+
const apiKey = pfDraft.apiKey.trim();
|
|
742
|
+
// Key 填了但 apiKeyEnv 留空:按路由 key 自动生成凭据库条目名,不再逼用户现编一个
|
|
743
|
+
let apiKeyEnv = pfDraft.apiKeyEnv.trim();
|
|
744
|
+
if (apiKey && !apiKeyEnv) {
|
|
745
|
+
apiKeyEnv = dkmEnvFromRoute(route);
|
|
746
|
+
setPfDraft(Object.assign({}, pfDraft, { apiKeyEnv: apiKeyEnv }));
|
|
747
|
+
}
|
|
748
|
+
const models = pfModelsPayload();
|
|
749
|
+
if (!route) { setPfMsg({ ok: false, text: "路由 key 不能为空(如 modelscope)" }); return; }
|
|
750
|
+
if (!DKM_ROUTE_KEY_RE.test(route)) { setPfMsg({ ok: false, text: "路由 key 非法:只能含字母、数字与 . _ -,且以字母/数字开头" }); return; }
|
|
751
|
+
if (!/^https?:\/\//i.test(baseURL)) { setPfMsg({ ok: false, text: "baseURL 须以 http:// 或 https:// 开头" }); return; }
|
|
752
|
+
const ids = models.map((m) => m.id);
|
|
753
|
+
if (ids.length === 0 || ids.some((x) => !x)) { setPfMsg({ ok: false, text: "每个模型都要有 id(可点「获取模型列表」自动拉取)" }); return; }
|
|
754
|
+
if (new Set(ids).size !== ids.length) { setPfMsg({ ok: false, text: "模型 id 有重复" }); return; }
|
|
755
|
+
const profile = Object.assign({}, pfDraft.extra || {});
|
|
756
|
+
const dn = pfDraft.displayName.trim();
|
|
757
|
+
if (dn) profile.displayName = dn; else delete profile.displayName;
|
|
758
|
+
profile.api = pfDraft.api;
|
|
759
|
+
profile.baseURL = baseURL;
|
|
760
|
+
if (apiKeyEnv) profile.apiKeyEnv = apiKeyEnv; else delete profile.apiKeyEnv;
|
|
761
|
+
profile.defaultInput = pfDraft.defaultInput;
|
|
762
|
+
profile.models = models;
|
|
763
|
+
const payload = {
|
|
764
|
+
action: pfMode === "create" ? "create" : "update",
|
|
765
|
+
route: route,
|
|
766
|
+
profile: profile,
|
|
767
|
+
revisions: (data && data.revisions) || {}
|
|
768
|
+
};
|
|
769
|
+
if (apiKey) payload.apiKey = apiKey;
|
|
770
|
+
setPfSaving(true); setPfMsg(null);
|
|
771
|
+
fetch("dsh-dock/models", {
|
|
772
|
+
method: "POST",
|
|
773
|
+
headers: { "content-type": "application/json" },
|
|
774
|
+
body: JSON.stringify(payload),
|
|
775
|
+
signal: AbortSignal.timeout(30000)
|
|
776
|
+
})
|
|
777
|
+
.then((res) => res.json()
|
|
778
|
+
.then((b) => ({ status: res.status, body: b }))
|
|
779
|
+
.catch(() => ({ status: res.status, body: {} })))
|
|
780
|
+
.then((r) => {
|
|
781
|
+
if (r.status === 200 && r.body && r.body.ok) {
|
|
782
|
+
setPfMsg({ ok: true, text: "Provider「" + route + "」已" + (pfMode === "create" ? "新增" : "更新") + ";会话模型选择器即时可选" });
|
|
783
|
+
setPfMode(null); setPfDraft(null); setPfDirty(false);
|
|
784
|
+
modelsStore.load();
|
|
785
|
+
} else {
|
|
786
|
+
setPfMsg({ ok: false, text: (r.body && r.body.error) || ("HTTP " + r.status) });
|
|
787
|
+
}
|
|
788
|
+
})
|
|
789
|
+
.catch((e) => setPfMsg({ ok: false, text: (e && e.message) || String(e) }))
|
|
790
|
+
.then(() => setPfSaving(false));
|
|
791
|
+
};
|
|
792
|
+
// 删除:两步确认(先点一次进入待确认态,再点一次真删;不用 window.confirm)
|
|
793
|
+
const deletePf = (p) => {
|
|
794
|
+
const route = p.route || p.id;
|
|
795
|
+
if (pfDelArm !== route) { setPfDelArm(route); setPfMsg(null); return; }
|
|
796
|
+
setPfDelArm(null); setPfSaving(true); setPfMsg(null);
|
|
797
|
+
fetch("dsh-dock/models", {
|
|
798
|
+
method: "POST",
|
|
799
|
+
headers: { "content-type": "application/json" },
|
|
800
|
+
body: JSON.stringify({ action: "delete", route: route, revisions: (data && data.revisions) || {} }),
|
|
801
|
+
signal: AbortSignal.timeout(20000)
|
|
802
|
+
})
|
|
803
|
+
.then((res) => res.json()
|
|
804
|
+
.then((b) => ({ status: res.status, body: b }))
|
|
805
|
+
.catch(() => ({ status: res.status, body: {} })))
|
|
806
|
+
.then((r) => {
|
|
807
|
+
if (r.status === 200 && r.body && r.body.ok) {
|
|
808
|
+
setPfMsg({ ok: true, text: "Provider「" + route + "」已删除" });
|
|
809
|
+
if (pfMode === "edit" && pfDraft && pfDraft.route === route) { setPfMode(null); setPfDraft(null); }
|
|
810
|
+
modelsStore.load();
|
|
811
|
+
} else {
|
|
812
|
+
setPfMsg({ ok: false, text: (r.body && r.body.error) || ("HTTP " + r.status) });
|
|
813
|
+
}
|
|
814
|
+
})
|
|
815
|
+
.catch((e) => setPfMsg({ ok: false, text: (e && e.message) || String(e) }))
|
|
816
|
+
.then(() => setPfSaving(false));
|
|
817
|
+
};
|
|
818
|
+
// deep-link:带 openProviderForm 进入本页时直接展开新增表单(只认第一次,之后由用户自己开关)
|
|
819
|
+
const openedRef = react.useRef(false);
|
|
820
|
+
react.useEffect(() => {
|
|
821
|
+
if (props.openCreate && !openedRef.current) {
|
|
822
|
+
openedRef.current = true;
|
|
823
|
+
openPfCreate();
|
|
824
|
+
if (typeof props.onCreateHandled === "function") props.onCreateHandled();
|
|
825
|
+
}
|
|
826
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
827
|
+
}, []);
|
|
828
|
+
|
|
829
|
+
// 官方 ProviderEditor 同款字段:一行一个字段(标签在上、输入铺满),非必填项收进「自定义设置」
|
|
830
|
+
const input = (p) => react.createElement("input", Object.assign({ className: "dkp-input" }, p));
|
|
831
|
+
const field = (label, node, hint) => react.createElement("div", { className: "dkp-field", key: label },
|
|
832
|
+
react.createElement("label", { className: "dkp-flabel" }, label),
|
|
833
|
+
node,
|
|
834
|
+
hint ? react.createElement("p", { className: "dkp-hint" }, hint) : null);
|
|
835
|
+
|
|
836
|
+
const editor = !pfDraft ? null : react.createElement("div", { className: "dkp-editor" },
|
|
837
|
+
react.createElement("div", { className: "dkp-editor-head" },
|
|
838
|
+
react.createElement("span", { className: "dkp-editor-title" }, pfMode === "create" ? "新增 Provider" : "编辑 Provider"),
|
|
839
|
+
pfMode === "edit" ? react.createElement("span", { className: "dkp-editor-route" }, pfDraft.route) : null),
|
|
840
|
+
field("路由 key", input({
|
|
841
|
+
value: pfDraft.route,
|
|
842
|
+
disabled: pfMode === "edit",
|
|
843
|
+
placeholder: "acme-gateway",
|
|
844
|
+
onChange: (e) => patchPf({ route: e.target.value })
|
|
845
|
+
}), pfMode === "edit" ? "路由 key 是配置键,保存后不建议修改。" : "唯一标识:只能含字母、数字与 . _ -,且以字母/数字开头。"),
|
|
846
|
+
field("显示名称", input({
|
|
847
|
+
value: pfDraft.displayName,
|
|
848
|
+
placeholder: pfDraft.route || "如 魔搭 ModelScope",
|
|
849
|
+
onChange: (e) => patchPf({ displayName: e.target.value })
|
|
850
|
+
})),
|
|
851
|
+
field("baseURL", input({
|
|
852
|
+
value: pfDraft.baseURL,
|
|
853
|
+
placeholder: pfDraft.api === "anthropic-messages" ? "https://gateway.example" : "https://gateway.example/v1",
|
|
854
|
+
onChange: (e) => patchPf({ baseURL: e.target.value })
|
|
855
|
+
}), "OpenAI 兼容端点通常以 /v1 结尾;Anthropic Messages 由 SDK 自行追加 /v1/messages。"),
|
|
856
|
+
field("API 协议", react.createElement("select", {
|
|
857
|
+
className: "dkp-input dkp-select",
|
|
858
|
+
value: pfDraft.api,
|
|
859
|
+
onChange: (e) => patchPf({ api: e.target.value })
|
|
860
|
+
},
|
|
861
|
+
DKM_APIS.map(([v, label]) => react.createElement("option", { key: v, value: v }, label)))),
|
|
862
|
+
field("API Key", react.createElement("input", {
|
|
863
|
+
className: "dkp-input",
|
|
864
|
+
type: "password",
|
|
865
|
+
autoComplete: "new-password",
|
|
866
|
+
placeholder: pfMode === "edit" ? "留空 = 不改动已存密钥" : "sk-…(仅本次写入本机凭据库,不回显)",
|
|
867
|
+
value: pfDraft.apiKey,
|
|
868
|
+
onChange: (e) => patchPf({ apiKey: e.target.value })
|
|
869
|
+
})),
|
|
870
|
+
react.createElement("details", { className: "dkp-customized" },
|
|
871
|
+
react.createElement("summary", { className: "dkp-summary" }, "自定义设置"),
|
|
872
|
+
react.createElement("div", { className: "dkp-custombody" },
|
|
873
|
+
field("apiKeyEnv", input({
|
|
874
|
+
value: pfDraft.apiKeyEnv,
|
|
875
|
+
placeholder: "留空 = 按路由 key 自动生成(" + (dkmEnvFromRoute(pfDraft.route) || "ROUTE_API_KEY") + ")",
|
|
876
|
+
onChange: (e) => patchPf({ apiKeyEnv: e.target.value })
|
|
877
|
+
}), "凭据库条目名;留空时按路由 key 推导,一般无需填写。"),
|
|
878
|
+
react.createElement("div", { className: "dkp-field" },
|
|
879
|
+
react.createElement("span", { className: "dkp-flabel" }, "默认输入类型"),
|
|
880
|
+
react.createElement("div", { className: "dkp-checks" },
|
|
881
|
+
DKM_MODALITIES.map(([v, label]) => react.createElement("label", { key: v, className: "dkp-check" },
|
|
882
|
+
react.createElement("input", {
|
|
883
|
+
type: "checkbox",
|
|
884
|
+
checked: pfDraft.defaultInput.indexOf(v) >= 0,
|
|
885
|
+
onChange: () => {
|
|
886
|
+
const next = dkmToggleIn(pfDraft.defaultInput, v);
|
|
887
|
+
patchPf({ defaultInput: next.length > 0 ? next : ["text"] });
|
|
888
|
+
}
|
|
889
|
+
}),
|
|
890
|
+
label)),
|
|
891
|
+
react.createElement("span", { className: "dkp-hint" }, "模型未单独声明输入类型时继承这里。"))))),
|
|
892
|
+
react.createElement("section", { className: "dkp-catalog", "aria-label": "模型" },
|
|
893
|
+
react.createElement("div", { className: "dkp-catalog-head" },
|
|
894
|
+
react.createElement("div", { className: "dkp-catalog-titlewrap" },
|
|
895
|
+
react.createElement("span", { className: "dkp-catalog-title" }, "模型"),
|
|
896
|
+
react.createElement("span", { className: "dkp-catalog-meta" },
|
|
897
|
+
"已选 " + pfSelectedCount() + " 个" + (pfFetchedList ? " / 上游共 " + pfFetchedList.length + " 个" : ""))),
|
|
898
|
+
react.createElement("div", { className: "dkp-actions" },
|
|
899
|
+
react.createElement("button", {
|
|
900
|
+
type: "button",
|
|
901
|
+
className: "dkp-btn ghost sm",
|
|
902
|
+
disabled: pfFetching || !pfDraft.baseURL,
|
|
903
|
+
title: "调用 baseURL/models 拉取该端点可用模型,勾选后加入下方列表",
|
|
904
|
+
onClick: fetchPfModels
|
|
905
|
+
}, pfFetching ? "拉取中…" : "获取模型列表"),
|
|
906
|
+
pfFetchedList && pfFetchedList.length > 0
|
|
907
|
+
? react.createElement("button", { type: "button", className: "dkp-btn ghost sm", title: "勾选上游返回的全部模型", onClick: () => setAllPfModels(true) }, "全选")
|
|
908
|
+
: null,
|
|
909
|
+
pfFetchedList && pfFetchedList.length > 0
|
|
910
|
+
? react.createElement("button", { type: "button", className: "dkp-btn ghost sm", title: "清空已勾选的模型", onClick: () => setAllPfModels(false) }, "清空")
|
|
911
|
+
: null)),
|
|
912
|
+
pfFetchedList
|
|
913
|
+
? react.createElement("div", { className: "dkp-picker" },
|
|
914
|
+
pfFetchedList.length === 0
|
|
915
|
+
? react.createElement("div", { className: "dkp-hint" }, "上游没有返回任何模型")
|
|
916
|
+
: pfFetchedList.map((mm) => {
|
|
917
|
+
const id = String((mm && mm.id) || "").trim();
|
|
918
|
+
if (!id) return null;
|
|
919
|
+
const checked = pfDraft.models.some((m) => String(m.id || "").trim() === id);
|
|
920
|
+
return react.createElement("label", { key: id },
|
|
921
|
+
react.createElement("input", {
|
|
922
|
+
type: "checkbox",
|
|
923
|
+
checked: checked,
|
|
924
|
+
onChange: () => togglePfModel(id, (mm && mm.name) || "", !checked)
|
|
925
|
+
}),
|
|
926
|
+
react.createElement("span", { className: "dkp-picker-id" }, id),
|
|
927
|
+
(mm && mm.name) ? react.createElement("span", { className: "dkp-picker-name" }, mm.name) : null);
|
|
928
|
+
}))
|
|
929
|
+
: null,
|
|
930
|
+
pfDraft.models.length === 0
|
|
931
|
+
? react.createElement("div", { className: "dkp-empty" }, "还没有模型——点「获取模型列表」勾选上游模型,或「添加模型」手动填写。")
|
|
932
|
+
: react.createElement("div", { className: "dkp-model-list" },
|
|
933
|
+
pfDraft.models.map((m, i) => react.createElement(ProviderModelRow, { key: i, m: m, i: i, onPatch: patchPfModel, onDel: delPfModel }))),
|
|
934
|
+
react.createElement("button", { type: "button", className: "dkp-addmodel", onClick: addPfModel }, "+ 添加模型")),
|
|
935
|
+
pfMsg && !pfMsg.ok ? react.createElement("p", { className: "dkp-error" }, pfMsg.text) : null,
|
|
936
|
+
react.createElement("div", { className: "dkp-editor-actions" },
|
|
937
|
+
react.createElement("span", { className: "dkp-hint" }, pfDirty ? "有未保存的修改" : ""),
|
|
938
|
+
react.createElement("button", { type: "button", className: "dkp-btn ghost", onClick: closePf }, "取消"),
|
|
939
|
+
react.createElement("button", { type: "button", className: "dkp-btn primary", disabled: pfSaving, onClick: savePf },
|
|
940
|
+
pfSaving ? "保存中…" : (pfMode === "create" ? "新增 Provider" : "保存修改"))));
|
|
941
|
+
|
|
942
|
+
const body = [
|
|
943
|
+
react.createElement("button", { key: "back", type: "button", className: "dkp-back", onClick: props.onBack }, "← 模型设置"),
|
|
944
|
+
react.createElement("h2", { key: "title", className: "dkp-title" }, "模型提供商"),
|
|
945
|
+
react.createElement("p", { key: "intro", className: "dkp-intro" },
|
|
946
|
+
"新增 / 编辑 / 删除自定义模型 Provider(OpenAI 兼容、Anthropic 等),写入官方配置并热生效;API Key 存本机凭据库(文件仅 root 可读)。内置 DeepSeek 官方 Provider 在本页只读,其模型目录请回「模型设置」编辑。")
|
|
947
|
+
];
|
|
948
|
+
if (props.error) {
|
|
949
|
+
body.push(react.createElement("p", { key: "err", className: "dkp-error" }, "模型目录拉取失败:" + props.error));
|
|
950
|
+
}
|
|
951
|
+
if (pfMsg && !pfDraft) {
|
|
952
|
+
body.push(react.createElement("p", { key: "msg", className: "dkp-msg " + (pfMsg.ok ? "ok" : "err"), role: "status" }, pfMsg.text));
|
|
953
|
+
}
|
|
954
|
+
if (!data) {
|
|
955
|
+
body.push(react.createElement("p", { key: "loading", className: "dkp-hint" }, props.loading ? "正在拉取模型目录…" : "等待拉取模型目录"));
|
|
956
|
+
} else if (piProviders.length === 0) {
|
|
957
|
+
body.push(react.createElement("div", { key: "empty", className: "dkp-empty" }, "还没有自定义 Provider——点下方「新增 Provider」添加。"));
|
|
958
|
+
} else {
|
|
959
|
+
body.push(react.createElement("ul", { key: "rows", className: "dkp-rows" },
|
|
960
|
+
piProviders.map((p) => {
|
|
961
|
+
const route = p.route || p.id;
|
|
962
|
+
const editing = pfMode === "edit" && !!pfDraft && pfDraft.route === route;
|
|
963
|
+
const keyMissing = !!(p.apiKeyEnv && !p.apiKeyConfigured);
|
|
964
|
+
return react.createElement("li", { key: p.id, className: "dkp-card" },
|
|
965
|
+
react.createElement("div", { className: "dkp-rowhead" },
|
|
966
|
+
react.createElement("span", { className: "dkp-identity" },
|
|
967
|
+
react.createElement("span", { className: "dkp-name" }, p.displayName || route),
|
|
968
|
+
react.createElement("span", { className: "dkp-tag" }, route),
|
|
969
|
+
react.createElement("span", { className: "dkp-tag" }, (p.models ? p.models.length : 0) + " 模型"),
|
|
970
|
+
react.createElement("span", {
|
|
971
|
+
className: "dkp-dot " + (keyMissing ? "missing" : "ok"),
|
|
972
|
+
role: "img",
|
|
973
|
+
title: p.apiKeyEnv ? (p.apiKeyConfigured ? p.apiKeyEnv + " 已配置" : p.apiKeyEnv + " 未配置") : "未设 apiKeyEnv",
|
|
974
|
+
"aria-label": p.apiKeyEnv ? (p.apiKeyConfigured ? "密钥已配置" : "密钥未配置") : "未设 apiKeyEnv"
|
|
975
|
+
}),
|
|
976
|
+
p.apiKeyEnv
|
|
977
|
+
? react.createElement("span", { className: "dkp-tag" + (keyMissing ? " warn" : " ok") },
|
|
978
|
+
p.apiKeyConfigured ? p.apiKeyEnv + " 已配置" : p.apiKeyEnv + " 未配置")
|
|
979
|
+
: null),
|
|
980
|
+
react.createElement("span", { className: "dkp-actions" },
|
|
981
|
+
react.createElement("button", {
|
|
982
|
+
type: "button",
|
|
983
|
+
className: "dkp-btn ghost sm",
|
|
984
|
+
"aria-expanded": editing,
|
|
985
|
+
onClick: () => (editing ? closePf() : openPfEdit(p))
|
|
986
|
+
}, editing ? "收起" : "编辑"),
|
|
987
|
+
react.createElement("button", {
|
|
988
|
+
type: "button",
|
|
989
|
+
className: "dkp-btn danger sm" + (pfDelArm === route ? " arm" : ""),
|
|
990
|
+
title: pfDelArm === route ? "再点一次确认删除" : "删除该 Provider",
|
|
991
|
+
onClick: () => deletePf(p)
|
|
992
|
+
}, pfDelArm === route ? "确认删除?" : "删除"))),
|
|
993
|
+
react.createElement("div", { className: "dkp-sub" }, [p.api, p.baseURL].filter(Boolean).join(" · ")),
|
|
994
|
+
editing ? editor : null);
|
|
995
|
+
})));
|
|
996
|
+
}
|
|
997
|
+
body.push(react.createElement("div", { key: "add", className: "dkp-addblock" },
|
|
998
|
+
pfMode === "create" && pfDraft
|
|
999
|
+
? react.createElement("div", { className: "dkp-card dkp-addcard" }, editor)
|
|
1000
|
+
: react.createElement("button", { type: "button", className: "dkp-addbtn", onClick: openPfCreate }, "+ 新增 Provider")));
|
|
1001
|
+
return react.createElement("div", { className: "dkp-page" }, body);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
426
1004
|
function ModelsStat() {
|
|
427
1005
|
const snap = useModels();
|
|
428
1006
|
const data = snap.data;
|
|
@@ -438,7 +1016,7 @@ export const feature = {
|
|
|
438
1016
|
name: "模型设置",
|
|
439
1017
|
order: 100,
|
|
440
1018
|
accent: "#22d3ee",
|
|
441
|
-
description: "编辑各 Provider
|
|
1019
|
+
description: "编辑各 Provider 模型目录(输入类型/思考强度档位);自定义 Provider 的增删改在「模型提供商」子页面,写回官方配置热生效,会话模型选择器即时可选",
|
|
442
1020
|
css: [
|
|
443
1021
|
".dkm-note{color:var(--dsw-alias-label-secondary);font-size:12px;}",
|
|
444
1022
|
".dkm-warn{color:var(--dk-warn);}",
|
|
@@ -483,7 +1061,99 @@ export const feature = {
|
|
|
483
1061
|
".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
1062
|
".dkm-mini:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-secondary);}",
|
|
485
1063
|
".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;}"
|
|
1064
|
+
".dkm-toolbar-label{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none;}",
|
|
1065
|
+
// ---- 模型提供商子页面(dkp- 前缀):对齐官方「模型」设置页的排版与尺寸 ----
|
|
1066
|
+
// 官方用量参考:section 限宽 720、行卡片 .5px 描边 + radius-xl + 12/14 内距、
|
|
1067
|
+
// 输入高 32、按钮高 36(行内小按钮 28)、新增按钮 44 虚线、编辑器 bg-module-platform。
|
|
1068
|
+
".dkp-page{width:100%;max-width:720px;display:flex;flex-direction:column;gap:12px;color:var(--dsw-alias-label-primary);}",
|
|
1069
|
+
".dkp-back{box-sizing:border-box;align-self:flex-start;cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:var(--dsw-radius-sm,6px);padding:4px 8px;margin-left:-8px;font-family:inherit;font-size:12px;line-height:18px;}",
|
|
1070
|
+
".dkp-back:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
1071
|
+
".dkp-title{margin:0;font-size:16px;font-weight:500;line-height:24px;}",
|
|
1072
|
+
".dkp-intro{margin:0;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:22px;}",
|
|
1073
|
+
".dkp-rows{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-direction:column;gap:8px;}",
|
|
1074
|
+
".dkp-card{box-sizing:border-box;margin:0;border:.5px solid var(--dsw-alias-settings-card-stroke,var(--dsw-alias-border-l2));background:var(--dsw-alias-settings-card-fill,var(--dsw-alias-bg-layer-1));border-radius:var(--dsw-radius-xl,12px);padding:12px 14px;display:flex;flex-direction:column;gap:10px;}",
|
|
1075
|
+
".dkp-rowhead{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}",
|
|
1076
|
+
".dkp-identity{display:inline-flex;align-items:center;gap:6px;min-width:0;flex-wrap:wrap;}",
|
|
1077
|
+
".dkp-name{font-size:14px;font-weight:500;line-height:22px;}",
|
|
1078
|
+
".dkp-tag{flex:none;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-xs,4px);color:var(--dsw-alias-label-secondary);padding:1px 6px;font-size:11px;line-height:16px;}",
|
|
1079
|
+
".dkp-tag.ok{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
|
|
1080
|
+
".dkp-tag.warn{color:var(--dk-warn);border-color:currentColor;}",
|
|
1081
|
+
".dkp-dot{box-sizing:border-box;flex:none;width:8px;height:8px;border-radius:50%;display:inline-block;}",
|
|
1082
|
+
".dkp-dot.ok{background:var(--dsw-alias-state-success-primary);}",
|
|
1083
|
+
".dkp-dot.missing{background:var(--dsw-alias-state-error-primary);}",
|
|
1084
|
+
".dkp-dot.warn{background:var(--dk-warn);}",
|
|
1085
|
+
".dkp-sub{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
1086
|
+
".dkp-hint{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;}",
|
|
1087
|
+
".dkp-dim{color:var(--dsw-alias-label-tertiary);}",
|
|
1088
|
+
".dkp-error{margin:0;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;}",
|
|
1089
|
+
".dkp-msg{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;}",
|
|
1090
|
+
".dkp-msg.ok{color:var(--dsw-alias-state-success-primary);}",
|
|
1091
|
+
".dkp-msg.err{color:var(--dsw-alias-state-error-primary);}",
|
|
1092
|
+
".dkp-actions{flex:none;margin-left:auto;display:inline-flex;align-items:center;gap:4px;}",
|
|
1093
|
+
".dkp-btn{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:36px;padding:0 14px;border:none;border-radius:var(--dsw-radius-md,8px);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;cursor:pointer;}",
|
|
1094
|
+
".dkp-btn.ghost{border:.5px solid var(--dsw-alias-border-l3);}",
|
|
1095
|
+
".dkp-btn.ghost:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
1096
|
+
".dkp-btn.primary{background:var(--dsw-alias-button-primary-fill,var(--dk-accent));color:var(--dsw-alias-label-primary-foreground,#fff);}",
|
|
1097
|
+
".dkp-btn.primary:hover{background:var(--dsw-alias-button-primary-hover,var(--dk-accent));}",
|
|
1098
|
+
".dkp-btn.danger{color:var(--dsw-alias-state-error-primary);}",
|
|
1099
|
+
".dkp-btn.danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,rgba(220,38,38,.1));}",
|
|
1100
|
+
".dkp-btn.arm{color:var(--dsw-alias-state-error-primary);border-color:currentColor;}",
|
|
1101
|
+
".dkp-btn.sm{height:28px;padding:0 10px;border-radius:var(--dsw-radius-sm,6px);font-size:12px;line-height:18px;}",
|
|
1102
|
+
".dkp-btn[disabled]{opacity:.4;cursor:default;}",
|
|
1103
|
+
".dkp-editor{border-radius:var(--dsw-radius-lg,10px);background:var(--dsw-alias-bg-module-platform,var(--dsw-alias-bg-layer-1));padding:14px 16px;display:flex;flex-direction:column;gap:14px;}",
|
|
1104
|
+
".dkp-editor-head{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;}",
|
|
1105
|
+
".dkp-editor-title{font-size:14px;font-weight:500;line-height:22px;}",
|
|
1106
|
+
".dkp-editor-route{color:var(--dsw-alias-label-tertiary);font-family:var(--ds-font-family-code,ui-monospace,monospace);font-size:12px;line-height:18px;}",
|
|
1107
|
+
".dkp-field{display:flex;flex-direction:column;gap:6px;min-width:0;}",
|
|
1108
|
+
".dkp-flabel{display:inline-flex;align-items:center;gap:10px;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px;}",
|
|
1109
|
+
".dkp-input{box-sizing:border-box;width:100%;height:32px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l4,var(--dsw-alias-border-l2));border-radius:var(--dsw-radius-md,8px);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;}",
|
|
1110
|
+
".dkp-input:focus{border-color:var(--dsw-alias-state-business-primary,var(--dk-accent));outline:none;}",
|
|
1111
|
+
".dkp-input::placeholder{color:var(--dsw-alias-label-dimmed,var(--dsw-alias-label-tertiary));}",
|
|
1112
|
+
".dkp-input:disabled{opacity:.6;cursor:default;}",
|
|
1113
|
+
"select.dkp-input{appearance:auto;cursor:pointer;max-width:240px;}",
|
|
1114
|
+
".dkp-select-sm{height:28px;max-width:160px;font-size:12px;line-height:18px;}",
|
|
1115
|
+
".dkp-checks{display:flex;align-items:center;gap:16px;flex-wrap:wrap;min-height:32px;}",
|
|
1116
|
+
".dkp-check{display:inline-flex;align-items:center;gap:6px;cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;}",
|
|
1117
|
+
".dkp-check input{accent-color:var(--dk-accent);margin:0;}",
|
|
1118
|
+
".dkp-mini{cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:transparent;color:var(--dsw-alias-label-tertiary);padding:1px 8px;font-family:inherit;font-size:11px;line-height:16px;flex:none;}",
|
|
1119
|
+
".dkp-mini:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-secondary);}",
|
|
1120
|
+
".dkp-customized{border-top:.5px solid var(--dsw-alias-border-l2);padding-top:10px;}",
|
|
1121
|
+
".dkp-summary{display:flex;align-items:center;gap:6px;width:fit-content;margin-left:-4px;padding:2px 4px;border-radius:var(--dsw-radius-sm,6px);list-style:none;cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px;}",
|
|
1122
|
+
".dkp-summary::-webkit-details-marker{display:none;}",
|
|
1123
|
+
".dkp-summary::before{content:\"\";width:5px;height:5px;border-right:1.5px solid;border-bottom:1.5px solid;transform:rotate(-45deg);transition:transform .12s;}",
|
|
1124
|
+
".dkp-customized[open]>.dkp-summary::before{transform:rotate(45deg);}",
|
|
1125
|
+
".dkp-summary:hover{color:var(--dsw-alias-label-primary);}",
|
|
1126
|
+
".dkp-custombody{display:flex;flex-direction:column;gap:12px;padding-top:12px;}",
|
|
1127
|
+
".dkp-catalog{border-top:.5px solid var(--dsw-alias-border-l2);padding-top:12px;display:flex;flex-direction:column;gap:10px;}",
|
|
1128
|
+
".dkp-catalog-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;}",
|
|
1129
|
+
".dkp-catalog-titlewrap{display:flex;flex-direction:column;gap:2px;}",
|
|
1130
|
+
".dkp-catalog-title{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px;}",
|
|
1131
|
+
".dkp-catalog-meta{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;}",
|
|
1132
|
+
".dkp-model-list{display:flex;flex-direction:column;gap:8px;}",
|
|
1133
|
+
".dkp-model-entry{border:.5px solid var(--dsw-alias-border-l4,var(--dsw-alias-border-l2));border-radius:var(--dsw-radius-lg,10px);padding:6px;display:flex;flex-direction:column;gap:6px;}",
|
|
1134
|
+
".dkp-model-row{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr) minmax(0,.7fr) minmax(0,.7fr) auto;gap:6px;align-items:center;}",
|
|
1135
|
+
".dkp-model-row .dkp-input{height:30px;padding:0 8px;font-size:13px;}",
|
|
1136
|
+
".dkp-model-opts{display:flex;flex-direction:column;gap:4px;padding:2px 2px 0;}",
|
|
1137
|
+
".dkp-iconbtn{box-sizing:border-box;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:var(--dsw-radius-sm,6px);background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:12px;line-height:1;}",
|
|
1138
|
+
".dkp-iconbtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
1139
|
+
".dkp-iconbtn.danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,rgba(220,38,38,.1));color:var(--dsw-alias-state-error-primary);}",
|
|
1140
|
+
".dkp-addmodel{box-sizing:border-box;align-self:flex-start;display:inline-flex;align-items:center;gap:4px;height:28px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm,6px);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:18px;cursor:pointer;}",
|
|
1141
|
+
".dkp-addmodel:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
1142
|
+
".dkp-picker{max-height:320px;overflow-y:auto;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md,8px);padding:4px 6px;display:flex;flex-direction:column;gap:2px;}",
|
|
1143
|
+
".dkp-picker label{display:flex;align-items:center;gap:8px;min-width:0;cursor:pointer;padding:6px 8px;border-radius:var(--dsw-radius-md,8px);}",
|
|
1144
|
+
".dkp-picker label:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
1145
|
+
".dkp-picker input{accent-color:var(--dk-accent);margin:0;flex:none;}",
|
|
1146
|
+
".dkp-picker-id{flex:none;font-family:var(--ds-font-family-code,ui-monospace,monospace);font-size:13px;line-height:20px;}",
|
|
1147
|
+
".dkp-picker-name{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
1148
|
+
".dkp-empty{border:1px dashed var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-lg,10px);padding:12px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;}",
|
|
1149
|
+
".dkp-addblock{display:flex;align-items:stretch;}",
|
|
1150
|
+
".dkp-addbtn{box-sizing:border-box;flex:1 1 auto;min-width:180px;height:44px;display:inline-flex;align-items:center;justify-content:center;gap:6px;border:1px dashed var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-lg,10px);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;cursor:pointer;}",
|
|
1151
|
+
".dkp-addbtn:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
1152
|
+
".dkp-addcard{flex:1 1 auto;padding:14px 16px;}",
|
|
1153
|
+
".dkp-addcard .dkp-editor{background:transparent;padding:0;}",
|
|
1154
|
+
".dkp-editor-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;border-top:.5px solid var(--dsw-alias-border-l2);padding-top:12px;}",
|
|
1155
|
+
".dkp-editor-actions .dkp-hint{margin-right:auto;}",
|
|
1156
|
+
"@media (max-width:680px){.dkp-page{max-width:none;gap:14px}.dkp-intro{font-size:13px;line-height:20px}.dkp-model-row{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.dkp-model-row .dkp-iconbtn{justify-self:end}.dkp-editor{padding:12px}.dkp-addcard{padding:12px 14px}.dkp-btn{height:40px}.dkp-btn.sm{height:34px}.dkp-addbtn{height:48px}}"
|
|
487
1157
|
].join("\n"),
|
|
488
1158
|
View: ModelsView,
|
|
489
1159
|
HomeStat: ModelsStat,
|