dsh-dock 0.15.0 → 0.16.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 +46 -16
- package/client.js +2392 -926
- package/features/animation/host.js +2 -1
- package/features/balance/host.js +2 -1
- package/features/mobile/host.js +9 -0
- package/features/mobile/view.js +20 -0
- package/features/mobile-relay/host.js +2 -1
- package/features/modelconfig/host.js +21 -462
- package/features/modelconfig/view.js +19 -704
- package/features/notify/host.js +2 -1
- package/features/notify/view.jsx +153 -30
- package/features/providerconfig/host.js +235 -0
- package/features/providerconfig/view.js +604 -0
- package/features/runstate/host.js +2 -1
- package/features/tokenlog/host.js +2 -1
- package/features/tts/host.js +164 -0
- package/features/tts/view.jsx +1189 -0
- package/features/visionproxy/host.js +1 -1
- package/index.js +9 -1
- package/package.json +3 -3
- package/screenshots/dock-modelconfig.png +0 -0
- package/src/client.jsx +54 -24
- package/src/host-core.js +84 -0
- package/src/model-directory.js +275 -0
- package/src/model-draft.js +119 -0
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
// dsh-dock · 功能模块【模型设置】·
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// dsh-dock · 功能模块【模型设置】· 客户端视图
|
|
2
|
+
// 经 /dsh-dock/models(GET 目录 / POST 写回)编辑各 Provider **模型条目**的输入类型与
|
|
3
|
+
// 思考强度档位,写回官方 settings 热生效。
|
|
4
|
+
// Provider 自身的增删改、API Key、拉取上游模型列表已拆到【模型提供商】(features/providerconfig/,
|
|
5
|
+
// 独立菜单项、可单独停用);图片理解代理(visionproxy 宿主功能)的全局配置面板仍渲染在本页。
|
|
5
6
|
import react from "react";
|
|
7
|
+
import {
|
|
8
|
+
DKM_DS_LEVELS,
|
|
9
|
+
DKM_EFFORT_NAMES,
|
|
10
|
+
DKM_MODALITIES,
|
|
11
|
+
DKM_PI_LEVELS,
|
|
12
|
+
DKM_TAGS,
|
|
13
|
+
dkmAllInputs,
|
|
14
|
+
dkmAllLevels,
|
|
15
|
+
dkmToggleIn,
|
|
16
|
+
draftFromProvider,
|
|
17
|
+
} from "../../src/model-draft.js";
|
|
6
18
|
|
|
7
19
|
// ---- 模型设置共享快照:视图与首页总揽共用一份目录数据(手动刷新,不轮询) ----
|
|
8
20
|
const modelsStore = {
|
|
@@ -31,121 +43,8 @@ function useModels() {
|
|
|
31
43
|
return snap;
|
|
32
44
|
}
|
|
33
45
|
|
|
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
|
-
// 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
|
-
}
|
|
135
|
-
|
|
136
|
-
/** 从目录 Provider 视图构建可编辑草稿。 */
|
|
137
|
-
function draftFromProvider(p) {
|
|
138
|
-
return {
|
|
139
|
-
id: p.id,
|
|
140
|
-
kind: p.kind,
|
|
141
|
-
thinking: p.thinking === "disabled" ? "disabled" : "enabled",
|
|
142
|
-
defaultEffort: DKM_DS_LEVELS.indexOf(p.defaultEffort) >= 0 ? p.defaultEffort : "high",
|
|
143
|
-
models: (p.models || []).map(draftFromModel)
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
46
|
/**
|
|
148
|
-
*
|
|
47
|
+
* 单个模型编辑行(无 hook,可直接当纯函数调用)。
|
|
149
48
|
* props: { m, i, kind, onPatch(i, patch), onDel(i) }
|
|
150
49
|
*/
|
|
151
50
|
function ModelRow(props) {
|
|
@@ -220,7 +119,6 @@ function ModelsView(props) {
|
|
|
220
119
|
const snap = useModels();
|
|
221
120
|
const data = snap.data;
|
|
222
121
|
const providers = data && Array.isArray(data.providers) ? data.providers : [];
|
|
223
|
-
const params = props && props.params;
|
|
224
122
|
const [selId, setSelId] = react.useState(null);
|
|
225
123
|
const [draft, setDraft] = react.useState(null);
|
|
226
124
|
const [builtKey, setBuiltKey] = react.useState(null);
|
|
@@ -234,10 +132,6 @@ function ModelsView(props) {
|
|
|
234
132
|
const [vpSaving, setVpSaving] = react.useState(false);
|
|
235
133
|
const [vpMsg, setVpMsg] = react.useState(null);
|
|
236
134
|
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);
|
|
241
135
|
|
|
242
136
|
// 每次进入模型设置都重新读取官方目录。modelsStore 是跨页面共享快照,
|
|
243
137
|
// 原生设置刚新增 Provider 时不能要求用户再滚到页面底部手动刷新。
|
|
@@ -293,7 +187,6 @@ function ModelsView(props) {
|
|
|
293
187
|
models[i] = Object.assign({}, models[i], patch);
|
|
294
188
|
patchDraft({ models: models });
|
|
295
189
|
};
|
|
296
|
-
const toggleIn = (list, v) => list.indexOf(v) >= 0 ? list.filter((x) => x !== v) : list.concat([v]);
|
|
297
190
|
// 一键批量:输入支持(官方模态 + 标注)与强度档。输入留空 = 继承目录默认(Host 写回时删除 input 字段,安全)
|
|
298
191
|
const ALL_INPUT = DKM_MODALITIES.map((x) => x[0]);
|
|
299
192
|
const ALL_TAGS = DKM_TAGS.map((x) => x[0]);
|
|
@@ -306,14 +199,6 @@ function ModelsView(props) {
|
|
|
306
199
|
const bulkModels = (fn) => patchDraft({ models: draft.models.map((m) => Object.assign({}, m, fn(m))) });
|
|
307
200
|
const selectProvider = (id) => { setSelId(id); setMsg(null); };
|
|
308
201
|
|
|
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
|
-
|
|
317
202
|
const save = () => {
|
|
318
203
|
if (!draft || !cur || !dirty || saving) return;
|
|
319
204
|
const models = draft.models.map((m) => ({
|
|
@@ -361,22 +246,6 @@ function ModelsView(props) {
|
|
|
361
246
|
onDel: (j) => patchDraft({ models: draft.models.filter((_, k) => k !== j) })
|
|
362
247
|
});
|
|
363
248
|
|
|
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
|
-
}
|
|
379
|
-
|
|
380
249
|
const body = [];
|
|
381
250
|
if (data) {
|
|
382
251
|
body.push(react.createElement("div", { key: "refresh", className: "dkm-topbar" },
|
|
@@ -483,33 +352,6 @@ function ModelsView(props) {
|
|
|
483
352
|
: react.createElement("span", { className: "dkm-msg" }, vpDirty ? "有未保存的修改" : ""))));
|
|
484
353
|
}
|
|
485
354
|
|
|
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
|
-
|
|
513
355
|
if (data && providers.length === 0) {
|
|
514
356
|
body.push(react.createElement("div", { key: "empty", className: "dkm-note" }, "没有可编辑的模型 Provider(Host 侧尚未配置)"));
|
|
515
357
|
}
|
|
@@ -566,441 +408,6 @@ function ModelsView(props) {
|
|
|
566
408
|
return react.createElement("div", { className: "dkm-list" }, body);
|
|
567
409
|
}
|
|
568
410
|
|
|
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
|
-
|
|
1004
411
|
function ModelsStat() {
|
|
1005
412
|
const snap = useModels();
|
|
1006
413
|
const data = snap.data;
|
|
@@ -1016,7 +423,7 @@ export const feature = {
|
|
|
1016
423
|
name: "模型设置",
|
|
1017
424
|
order: 100,
|
|
1018
425
|
accent: "#22d3ee",
|
|
1019
|
-
description: "编辑各 Provider
|
|
426
|
+
description: "编辑各 Provider 模型的输入类型与思考强度档位,写回官方配置热生效;Provider 自身的增删改在「模型提供商」里管理",
|
|
1020
427
|
css: [
|
|
1021
428
|
".dkm-note{color:var(--dsw-alias-label-secondary);font-size:12px;}",
|
|
1022
429
|
".dkm-warn{color:var(--dk-warn);}",
|
|
@@ -1061,99 +468,7 @@ export const feature = {
|
|
|
1061
468
|
".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;}",
|
|
1062
469
|
".dkm-mini:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-secondary);}",
|
|
1063
470
|
".dkm-toolbar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;border-top:1px dashed var(--dsw-alias-border-l1);padding-top:8px;}",
|
|
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}}"
|
|
471
|
+
".dkm-toolbar-label{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none;}"
|
|
1157
472
|
].join("\n"),
|
|
1158
473
|
View: ModelsView,
|
|
1159
474
|
HomeStat: ModelsStat,
|