dsh-dock 0.14.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 +49 -19
- package/client.js +2461 -1008
- package/features/animation/host.js +2 -1
- package/features/balance/host.js +2 -1
- package/features/mobile/host.js +93 -98
- package/features/mobile/view.js +136 -133
- 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 +12 -1
- package/features/tokenlog/host.js +2 -1
- package/features/tokenlog/view.jsx +42 -12
- 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
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
// dsh-dock · 功能模块【模型提供商】· 客户端视图
|
|
2
|
+
//
|
|
3
|
+
// 管理自定义模型 Provider(OpenAI 兼容 / Anthropic 等):新增、编辑、删除、拉取上游
|
|
4
|
+
// 模型列表、写入 API Key。写回官方 settings 与本地凭据库,热生效。
|
|
5
|
+
// 与【模型设置】的分工:那边只管已配置模型的输入模态与思考强度;本页管 Provider 本身。
|
|
6
|
+
// 数据来自 /dsh-dock/providerconfig(GET 列表 / POST action=listModels|create|update|delete)。
|
|
7
|
+
import react from "react";
|
|
8
|
+
import {
|
|
9
|
+
DKM_APIS,
|
|
10
|
+
DKM_EFFORT_NAMES,
|
|
11
|
+
DKM_MODALITIES,
|
|
12
|
+
DKM_PI_LEVELS,
|
|
13
|
+
DKM_ROUTE_KEY_RE,
|
|
14
|
+
DKM_TAGS,
|
|
15
|
+
dkmAllInputs,
|
|
16
|
+
dkmAllLevels,
|
|
17
|
+
dkmEnvFromRoute,
|
|
18
|
+
dkmToggleIn,
|
|
19
|
+
emptyModelDraft,
|
|
20
|
+
emptyProviderForm,
|
|
21
|
+
providerFormFromProvider,
|
|
22
|
+
} from "../../src/model-draft.js";
|
|
23
|
+
|
|
24
|
+
// ---- Provider 列表快照:本模块独占(不与模型目录共享,两个功能可各自启停) ----
|
|
25
|
+
const providersStore = {
|
|
26
|
+
snap: { data: null, loading: false, error: null },
|
|
27
|
+
listeners: new Set(),
|
|
28
|
+
subscribe(fn) { this.listeners.add(fn); return () => { this.listeners.delete(fn); }; },
|
|
29
|
+
set(patch) { this.snap = Object.assign({}, this.snap, patch); for (const fn of this.listeners) fn(); },
|
|
30
|
+
load() {
|
|
31
|
+
if (this.snap.loading) return;
|
|
32
|
+
this.set({ loading: true, error: null });
|
|
33
|
+
// Provider 可以在 DSH 原生模型设置里随时增删;不要复用浏览器缓存。
|
|
34
|
+
fetch("dsh-dock/providerconfig", { cache: "no-store", signal: AbortSignal.timeout(20000) })
|
|
35
|
+
.then((res) => (res.ok ? res.json() : Promise.reject(new Error("模型提供商接口 HTTP " + res.status))))
|
|
36
|
+
.then((data) => this.set({ data: data, loading: false, error: null }))
|
|
37
|
+
.catch((e) => this.set({ loading: false, error: (e && e.message) || String(e) }));
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function useProviders() {
|
|
41
|
+
const [snap, setSnap] = react.useState(providersStore.snap);
|
|
42
|
+
react.useEffect(() => {
|
|
43
|
+
const off = providersStore.subscribe(() => setSnap(providersStore.snap));
|
|
44
|
+
if (!providersStore.snap.data && !providersStore.snap.loading) providersStore.load();
|
|
45
|
+
return off;
|
|
46
|
+
}, []);
|
|
47
|
+
return snap;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Provider 表单里的单个模型行(官方模型行同款:id / 名称 / 容量 一行网格,下方输入类型与思考强度)。
|
|
52
|
+
* props: { m, i, onPatch(i, patch), onDel(i) }
|
|
53
|
+
*/
|
|
54
|
+
function ProviderModelRow(props) {
|
|
55
|
+
const m = props.m;
|
|
56
|
+
const i = props.i;
|
|
57
|
+
const check = (v, list, kind) => react.createElement("label", { key: v[0], className: "dkp-check" },
|
|
58
|
+
react.createElement("input", {
|
|
59
|
+
type: "checkbox",
|
|
60
|
+
checked: list.indexOf(v[0]) >= 0,
|
|
61
|
+
onChange: () => {
|
|
62
|
+
const next = dkmToggleIn(list, v[0]);
|
|
63
|
+
props.onPatch(i, kind === "input" ? { input: next.length > 0 ? next : ["text"] } : { tags: next });
|
|
64
|
+
}
|
|
65
|
+
}),
|
|
66
|
+
v[1]);
|
|
67
|
+
return react.createElement("div", { className: "dkp-model-entry" },
|
|
68
|
+
react.createElement("div", { className: "dkp-model-row" },
|
|
69
|
+
react.createElement("input", { className: "dkp-input", placeholder: "模型 id(必填)", value: m.id, onChange: (e) => props.onPatch(i, { id: e.target.value }) }),
|
|
70
|
+
react.createElement("input", { className: "dkp-input", placeholder: "显示名称(可选)", value: m.name, onChange: (e) => props.onPatch(i, { name: e.target.value }) }),
|
|
71
|
+
react.createElement("input", { className: "dkp-input", placeholder: "上下文窗口", inputMode: "numeric", value: m.contextWindow, onChange: (e) => props.onPatch(i, { contextWindow: e.target.value }) }),
|
|
72
|
+
react.createElement("input", { className: "dkp-input", placeholder: "最大输出", inputMode: "numeric", value: m.maxTokens, onChange: (e) => props.onPatch(i, { maxTokens: e.target.value }) }),
|
|
73
|
+
react.createElement("button", { type: "button", className: "dkp-iconbtn danger", title: "删除该模型", "aria-label": "删除模型", onClick: () => props.onDel(i) }, "✕")),
|
|
74
|
+
react.createElement("div", { className: "dkp-model-opts" },
|
|
75
|
+
react.createElement("div", { className: "dkp-checks" },
|
|
76
|
+
react.createElement("span", { className: "dkp-flabel" }, "输入类型:"),
|
|
77
|
+
DKM_MODALITIES.map((v) => check(v, m.input, "input")),
|
|
78
|
+
react.createElement("span", { className: "dkp-flabel dkp-dim", title: "标注仅在本面板展示与持久化,不参与请求路由(官方 schema 只接受文本/图片)" }, "标注:"),
|
|
79
|
+
DKM_TAGS.map((v) => check(v, m.tags, "tags")),
|
|
80
|
+
react.createElement("button", { type: "button", className: "dkp-mini", title: "一键勾选全部输入类型与标注", onClick: () => props.onPatch(i, dkmAllInputs(true)) }, "全选"),
|
|
81
|
+
react.createElement("button", { type: "button", className: "dkp-mini", title: "一键取消全部勾选", onClick: () => props.onPatch(i, dkmAllInputs(false)) }, "取消全选")),
|
|
82
|
+
react.createElement("div", { className: "dkp-checks" },
|
|
83
|
+
react.createElement("span", { className: "dkp-flabel" }, "思考强度:"),
|
|
84
|
+
react.createElement("select", {
|
|
85
|
+
className: "dkp-input dkp-select dkp-select-sm",
|
|
86
|
+
value: m.effortsMode,
|
|
87
|
+
onChange: (e) => props.onPatch(i, {
|
|
88
|
+
effortsMode: e.target.value,
|
|
89
|
+
effortLevels: e.target.value === "custom" && m.effortsMode !== "custom"
|
|
90
|
+
? { off: true, low: true, high: true }
|
|
91
|
+
: m.effortLevels
|
|
92
|
+
})
|
|
93
|
+
},
|
|
94
|
+
react.createElement("option", { value: "inherit" }, "跟随目录默认"),
|
|
95
|
+
react.createElement("option", { value: "off" }, "不支持思考"),
|
|
96
|
+
react.createElement("option", { value: "custom" }, "自定义档位")),
|
|
97
|
+
m.effortsMode === "custom"
|
|
98
|
+
? DKM_PI_LEVELS.map((lv) => react.createElement("label", { key: lv, className: "dkp-check" },
|
|
99
|
+
react.createElement("input", {
|
|
100
|
+
type: "checkbox",
|
|
101
|
+
checked: !!m.effortLevels[lv],
|
|
102
|
+
onChange: () => {
|
|
103
|
+
const nl = Object.assign({}, m.effortLevels);
|
|
104
|
+
nl[lv] = !nl[lv];
|
|
105
|
+
props.onPatch(i, { effortLevels: nl });
|
|
106
|
+
}
|
|
107
|
+
}),
|
|
108
|
+
DKM_EFFORT_NAMES[lv] || lv))
|
|
109
|
+
: null,
|
|
110
|
+
react.createElement("button", { type: "button", className: "dkp-mini", title: "一键勾选全部强度档(切为自定义)", onClick: () => props.onPatch(i, dkmAllLevels(true)) }, "全选"),
|
|
111
|
+
react.createElement("button", { type: "button", className: "dkp-mini", title: "一键取消全部强度档", onClick: () => props.onPatch(i, dkmAllLevels(false)) }, "取消全选"))));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 模型提供商页:官方「模型」设置页同款布局——
|
|
116
|
+
* 标题 + 说明 + 行卡片列表(每行可展开同一个编辑卡片)+ 底部虚线「新增 Provider」。
|
|
117
|
+
* 只管自定义 pi-ai 路由的增删改(内置 DeepSeek 官方 Provider 在【模型设置】里编辑其模型目录)。
|
|
118
|
+
*/
|
|
119
|
+
function ProvidersView(props) {
|
|
120
|
+
const snap = useProviders();
|
|
121
|
+
const data = snap.data;
|
|
122
|
+
const providers = data && Array.isArray(data.providers) ? data.providers : [];
|
|
123
|
+
const params = props && props.params;
|
|
124
|
+
const [openCreate, setOpenCreate] = react.useState(false);
|
|
125
|
+
const [pfMode, setPfMode] = react.useState(null);
|
|
126
|
+
const [pfDraft, setPfDraft] = react.useState(null);
|
|
127
|
+
const [pfDirty, setPfDirty] = react.useState(false);
|
|
128
|
+
const [pfSaving, setPfSaving] = react.useState(false);
|
|
129
|
+
const [pfMsg, setPfMsg] = react.useState(null);
|
|
130
|
+
const [pfDelArm, setPfDelArm] = react.useState(null);
|
|
131
|
+
const [pfFetching, setPfFetching] = react.useState(false);
|
|
132
|
+
const [pfFetchedList, setPfFetchedList] = react.useState(null);
|
|
133
|
+
|
|
134
|
+
// 每次进入本页都重新读取(原生设置里刚改过 Provider 也要看到最新的)
|
|
135
|
+
react.useEffect(() => { providersStore.load(); }, []);
|
|
136
|
+
// deep-link:params.openProviderForm 直接展开「新增 Provider」表单(渲染回归测试与设置页深链共用)
|
|
137
|
+
react.useEffect(() => {
|
|
138
|
+
if (params && params.openProviderForm) setOpenCreate(true);
|
|
139
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
const reload = () => providersStore.load();
|
|
143
|
+
const openPfCreate = () => {
|
|
144
|
+
setPfMode("create"); setPfDraft(emptyProviderForm()); setPfDirty(false);
|
|
145
|
+
setPfMsg(null); setPfDelArm(null); setPfFetchedList(null);
|
|
146
|
+
};
|
|
147
|
+
const openPfEdit = (p) => {
|
|
148
|
+
setPfMode("edit"); setPfDraft(providerFormFromProvider(p)); setPfDirty(false);
|
|
149
|
+
setPfMsg(null); setPfDelArm(null); setPfFetchedList(null);
|
|
150
|
+
};
|
|
151
|
+
const closePf = () => {
|
|
152
|
+
setPfMode(null); setPfDraft(null); setPfDirty(false);
|
|
153
|
+
setPfMsg(null); setPfDelArm(null); setPfFetchedList(null);
|
|
154
|
+
};
|
|
155
|
+
const patchPf = (patch) => { setPfDraft(Object.assign({}, pfDraft, patch)); setPfDirty(true); setPfMsg(null); };
|
|
156
|
+
const patchPfModel = (i, patch) => {
|
|
157
|
+
const models = pfDraft.models.slice();
|
|
158
|
+
models[i] = Object.assign({}, models[i], patch);
|
|
159
|
+
patchPf({ models: models });
|
|
160
|
+
};
|
|
161
|
+
const delPfModel = (i) => patchPf({ models: pfDraft.models.filter((_, j) => j !== i) });
|
|
162
|
+
const addPfModel = () => patchPf({ models: pfDraft.models.concat([emptyModelDraft()]) });
|
|
163
|
+
// 勾选式选模型:checked 状态直接映射「该 id 是否在草稿模型列表里」
|
|
164
|
+
const togglePfModel = (id, name, on) => {
|
|
165
|
+
const base = pfDraft.models.filter((m) => String(m.id || "").trim());
|
|
166
|
+
if (on) {
|
|
167
|
+
if (base.some((m) => String(m.id).trim() === id)) return;
|
|
168
|
+
base.push(Object.assign(emptyModelDraft(), { id: id, name: name || "", input: pfDraft.defaultInput.slice() }));
|
|
169
|
+
} else {
|
|
170
|
+
const i = base.findIndex((m) => String(m.id).trim() === id);
|
|
171
|
+
if (i >= 0) base.splice(i, 1);
|
|
172
|
+
}
|
|
173
|
+
patchPf({ models: base });
|
|
174
|
+
};
|
|
175
|
+
const setAllPfModels = (on) => {
|
|
176
|
+
const base = [];
|
|
177
|
+
if (on && pfFetchedList) {
|
|
178
|
+
for (const mm of pfFetchedList) {
|
|
179
|
+
const id = String((mm && mm.id) || "").trim();
|
|
180
|
+
if (!id) continue;
|
|
181
|
+
base.push(Object.assign(emptyModelDraft(), { id: id, name: (mm && mm.name) || "", input: pfDraft.defaultInput.slice() }));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
patchPf({ models: base });
|
|
185
|
+
};
|
|
186
|
+
const pfSelectedCount = () => pfDraft ? pfDraft.models.filter((m) => String(m.id || "").trim()).length : 0;
|
|
187
|
+
const pfModelsPayload = () => pfDraft.models.map((m) => ({
|
|
188
|
+
id: String(m.id || "").trim(),
|
|
189
|
+
name: String(m.name || "").trim(),
|
|
190
|
+
contextWindow: m.contextWindow === "" || m.contextWindow == null ? undefined : Number(m.contextWindow),
|
|
191
|
+
maxTokens: m.maxTokens === "" || m.maxTokens == null ? undefined : Number(m.maxTokens),
|
|
192
|
+
input: m.input,
|
|
193
|
+
tags: m.tags,
|
|
194
|
+
effortsMode: m.effortsMode,
|
|
195
|
+
effortLevels: m.effortLevels,
|
|
196
|
+
raw: m.raw
|
|
197
|
+
}));
|
|
198
|
+
// 自动获取模型列表:宿主代打 GET {baseURL}/models(Key 还没保存也能先拉)
|
|
199
|
+
const fetchPfModels = () => {
|
|
200
|
+
if (!pfDraft || pfFetching) return;
|
|
201
|
+
setPfFetching(true); setPfMsg(null);
|
|
202
|
+
fetch("dsh-dock/providerconfig", {
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers: { "content-type": "application/json" },
|
|
205
|
+
body: JSON.stringify({
|
|
206
|
+
action: "listModels",
|
|
207
|
+
baseURL: pfDraft.baseURL,
|
|
208
|
+
api: pfDraft.api,
|
|
209
|
+
apiKeyEnv: pfDraft.apiKeyEnv,
|
|
210
|
+
apiKey: pfDraft.apiKey
|
|
211
|
+
}),
|
|
212
|
+
signal: AbortSignal.timeout(30000)
|
|
213
|
+
})
|
|
214
|
+
.then((res) => res.json()
|
|
215
|
+
.then((b) => ({ status: res.status, body: b }))
|
|
216
|
+
.catch(() => ({ status: res.status, body: {} })))
|
|
217
|
+
.then((r) => {
|
|
218
|
+
if (r.status === 200 && r.body && r.body.ok) {
|
|
219
|
+
setPfFetchedList(Array.isArray(r.body.models) ? r.body.models : []);
|
|
220
|
+
} else {
|
|
221
|
+
setPfMsg({ ok: false, text: (r.body && r.body.error) || ("HTTP " + r.status) });
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
.catch((e) => setPfMsg({ ok: false, text: (e && e.message) || String(e) }))
|
|
225
|
+
.then(() => setPfFetching(false));
|
|
226
|
+
};
|
|
227
|
+
const savePf = () => {
|
|
228
|
+
if (!pfDraft || pfSaving) return;
|
|
229
|
+
const route = pfDraft.route.trim();
|
|
230
|
+
const baseURL = pfDraft.baseURL.trim();
|
|
231
|
+
const apiKey = pfDraft.apiKey.trim();
|
|
232
|
+
// Key 填了但 apiKeyEnv 留空:按路由 key 自动生成凭据库条目名,不再逼用户现编一个
|
|
233
|
+
let apiKeyEnv = pfDraft.apiKeyEnv.trim();
|
|
234
|
+
if (apiKey && !apiKeyEnv) {
|
|
235
|
+
apiKeyEnv = dkmEnvFromRoute(route);
|
|
236
|
+
setPfDraft(Object.assign({}, pfDraft, { apiKeyEnv: apiKeyEnv }));
|
|
237
|
+
}
|
|
238
|
+
const models = pfModelsPayload();
|
|
239
|
+
if (!route) { setPfMsg({ ok: false, text: "路由 key 不能为空(如 modelscope)" }); return; }
|
|
240
|
+
if (!DKM_ROUTE_KEY_RE.test(route)) { setPfMsg({ ok: false, text: "路由 key 非法:只能含字母、数字与 . _ -,且以字母/数字开头" }); return; }
|
|
241
|
+
if (!/^https?:\/\//i.test(baseURL)) { setPfMsg({ ok: false, text: "baseURL 须以 http:// 或 https:// 开头" }); return; }
|
|
242
|
+
const ids = models.map((m) => m.id);
|
|
243
|
+
if (ids.length === 0 || ids.some((x) => !x)) { setPfMsg({ ok: false, text: "每个模型都要有 id(可点「获取模型列表」自动拉取)" }); return; }
|
|
244
|
+
if (new Set(ids).size !== ids.length) { setPfMsg({ ok: false, text: "模型 id 有重复" }); return; }
|
|
245
|
+
const profile = Object.assign({}, pfDraft.extra || {});
|
|
246
|
+
const dn = pfDraft.displayName.trim();
|
|
247
|
+
if (dn) profile.displayName = dn; else delete profile.displayName;
|
|
248
|
+
profile.api = pfDraft.api;
|
|
249
|
+
profile.baseURL = baseURL;
|
|
250
|
+
if (apiKeyEnv) profile.apiKeyEnv = apiKeyEnv; else delete profile.apiKeyEnv;
|
|
251
|
+
profile.defaultInput = pfDraft.defaultInput;
|
|
252
|
+
profile.models = models;
|
|
253
|
+
const payload = {
|
|
254
|
+
action: pfMode === "create" ? "create" : "update",
|
|
255
|
+
route: route,
|
|
256
|
+
profile: profile,
|
|
257
|
+
revisions: (data && data.revisions) || {}
|
|
258
|
+
};
|
|
259
|
+
if (apiKey) payload.apiKey = apiKey;
|
|
260
|
+
setPfSaving(true); setPfMsg(null);
|
|
261
|
+
fetch("dsh-dock/providerconfig", {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: { "content-type": "application/json" },
|
|
264
|
+
body: JSON.stringify(payload),
|
|
265
|
+
signal: AbortSignal.timeout(30000)
|
|
266
|
+
})
|
|
267
|
+
.then((res) => res.json()
|
|
268
|
+
.then((b) => ({ status: res.status, body: b }))
|
|
269
|
+
.catch(() => ({ status: res.status, body: {} })))
|
|
270
|
+
.then((r) => {
|
|
271
|
+
if (r.status === 200 && r.body && r.body.ok) {
|
|
272
|
+
setPfMsg({ ok: true, text: "Provider「" + route + "」已" + (pfMode === "create" ? "新增" : "更新") + ";会话模型选择器即时可选" });
|
|
273
|
+
setPfMode(null); setPfDraft(null); setPfDirty(false);
|
|
274
|
+
reload();
|
|
275
|
+
} else {
|
|
276
|
+
setPfMsg({ ok: false, text: (r.body && r.body.error) || ("HTTP " + r.status) });
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
.catch((e) => setPfMsg({ ok: false, text: (e && e.message) || String(e) }))
|
|
280
|
+
.then(() => setPfSaving(false));
|
|
281
|
+
};
|
|
282
|
+
// 删除:两步确认(先点一次进入待确认态,再点一次真删;不用 window.confirm)
|
|
283
|
+
const deletePf = (p) => {
|
|
284
|
+
const route = p.route || p.id;
|
|
285
|
+
if (pfDelArm !== route) { setPfDelArm(route); setPfMsg(null); return; }
|
|
286
|
+
setPfDelArm(null); setPfSaving(true); setPfMsg(null);
|
|
287
|
+
fetch("dsh-dock/providerconfig", {
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers: { "content-type": "application/json" },
|
|
290
|
+
body: JSON.stringify({ action: "delete", route: route, revisions: (data && data.revisions) || {} }),
|
|
291
|
+
signal: AbortSignal.timeout(20000)
|
|
292
|
+
})
|
|
293
|
+
.then((res) => res.json()
|
|
294
|
+
.then((b) => ({ status: res.status, body: b }))
|
|
295
|
+
.catch(() => ({ status: res.status, body: {} })))
|
|
296
|
+
.then((r) => {
|
|
297
|
+
if (r.status === 200 && r.body && r.body.ok) {
|
|
298
|
+
setPfMsg({ ok: true, text: "Provider「" + route + "」已删除" });
|
|
299
|
+
if (pfMode === "edit" && pfDraft && pfDraft.route === route) { setPfMode(null); setPfDraft(null); }
|
|
300
|
+
reload();
|
|
301
|
+
} else {
|
|
302
|
+
setPfMsg({ ok: false, text: (r.body && r.body.error) || ("HTTP " + r.status) });
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
.catch((e) => setPfMsg({ ok: false, text: (e && e.message) || String(e) }))
|
|
306
|
+
.then(() => setPfSaving(false));
|
|
307
|
+
};
|
|
308
|
+
// deep-link:带 openProviderForm 进入本页时直接展开新增表单(只认第一次,之后由用户自己开关)
|
|
309
|
+
const openedRef = react.useRef(false);
|
|
310
|
+
react.useEffect(() => {
|
|
311
|
+
if (openCreate && !openedRef.current) {
|
|
312
|
+
openedRef.current = true;
|
|
313
|
+
openPfCreate();
|
|
314
|
+
}
|
|
315
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
316
|
+
}, [openCreate]);
|
|
317
|
+
|
|
318
|
+
// 官方 ProviderEditor 同款字段:一行一个字段(标签在上、输入铺满),非必填项收进「自定义设置」
|
|
319
|
+
const input = (p) => react.createElement("input", Object.assign({ className: "dkp-input" }, p));
|
|
320
|
+
const field = (label, node, hint) => react.createElement("div", { className: "dkp-field", key: label },
|
|
321
|
+
react.createElement("label", { className: "dkp-flabel" }, label),
|
|
322
|
+
node,
|
|
323
|
+
hint ? react.createElement("p", { className: "dkp-hint" }, hint) : null);
|
|
324
|
+
|
|
325
|
+
const editor = !pfDraft ? null : react.createElement("div", { className: "dkp-editor" },
|
|
326
|
+
react.createElement("div", { className: "dkp-editor-head" },
|
|
327
|
+
react.createElement("span", { className: "dkp-editor-title" }, pfMode === "create" ? "新增 Provider" : "编辑 Provider"),
|
|
328
|
+
pfMode === "edit" ? react.createElement("span", { className: "dkp-editor-route" }, pfDraft.route) : null),
|
|
329
|
+
field("路由 key", input({
|
|
330
|
+
value: pfDraft.route,
|
|
331
|
+
disabled: pfMode === "edit",
|
|
332
|
+
placeholder: "acme-gateway",
|
|
333
|
+
onChange: (e) => patchPf({ route: e.target.value })
|
|
334
|
+
}), pfMode === "edit" ? "路由 key 是配置键,保存后不建议修改。" : "唯一标识:只能含字母、数字与 . _ -,且以字母/数字开头。"),
|
|
335
|
+
field("显示名称", input({
|
|
336
|
+
value: pfDraft.displayName,
|
|
337
|
+
placeholder: pfDraft.route || "如 魔搭 ModelScope",
|
|
338
|
+
onChange: (e) => patchPf({ displayName: e.target.value })
|
|
339
|
+
})),
|
|
340
|
+
field("baseURL", input({
|
|
341
|
+
value: pfDraft.baseURL,
|
|
342
|
+
placeholder: pfDraft.api === "anthropic-messages" ? "https://gateway.example" : "https://gateway.example/v1",
|
|
343
|
+
onChange: (e) => patchPf({ baseURL: e.target.value })
|
|
344
|
+
}), "OpenAI 兼容端点通常以 /v1 结尾;Anthropic Messages 由 SDK 自行追加 /v1/messages。"),
|
|
345
|
+
field("API 协议", react.createElement("select", {
|
|
346
|
+
className: "dkp-input dkp-select",
|
|
347
|
+
value: pfDraft.api,
|
|
348
|
+
onChange: (e) => patchPf({ api: e.target.value })
|
|
349
|
+
},
|
|
350
|
+
DKM_APIS.map(([v, label]) => react.createElement("option", { key: v, value: v }, label)))),
|
|
351
|
+
field("API Key", react.createElement("input", {
|
|
352
|
+
className: "dkp-input",
|
|
353
|
+
type: "password",
|
|
354
|
+
autoComplete: "new-password",
|
|
355
|
+
placeholder: pfMode === "edit" ? "留空 = 不改动已存密钥" : "sk-…(仅本次写入本机凭据库,不回显)",
|
|
356
|
+
value: pfDraft.apiKey,
|
|
357
|
+
onChange: (e) => patchPf({ apiKey: e.target.value })
|
|
358
|
+
})),
|
|
359
|
+
react.createElement("details", { className: "dkp-customized" },
|
|
360
|
+
react.createElement("summary", { className: "dkp-summary" }, "自定义设置"),
|
|
361
|
+
react.createElement("div", { className: "dkp-custombody" },
|
|
362
|
+
field("apiKeyEnv", input({
|
|
363
|
+
value: pfDraft.apiKeyEnv,
|
|
364
|
+
placeholder: "留空 = 按路由 key 自动生成(" + (dkmEnvFromRoute(pfDraft.route) || "ROUTE_API_KEY") + ")",
|
|
365
|
+
onChange: (e) => patchPf({ apiKeyEnv: e.target.value })
|
|
366
|
+
}), "凭据库条目名;留空时按路由 key 推导,一般无需填写。"),
|
|
367
|
+
react.createElement("div", { className: "dkp-field" },
|
|
368
|
+
react.createElement("span", { className: "dkp-flabel" }, "默认输入类型"),
|
|
369
|
+
react.createElement("div", { className: "dkp-checks" },
|
|
370
|
+
DKM_MODALITIES.map(([v, label]) => react.createElement("label", { key: v, className: "dkp-check" },
|
|
371
|
+
react.createElement("input", {
|
|
372
|
+
type: "checkbox",
|
|
373
|
+
checked: pfDraft.defaultInput.indexOf(v) >= 0,
|
|
374
|
+
onChange: () => {
|
|
375
|
+
const next = dkmToggleIn(pfDraft.defaultInput, v);
|
|
376
|
+
patchPf({ defaultInput: next.length > 0 ? next : ["text"] });
|
|
377
|
+
}
|
|
378
|
+
}),
|
|
379
|
+
label)),
|
|
380
|
+
react.createElement("span", { className: "dkp-hint" }, "模型未单独声明输入类型时继承这里。"))))),
|
|
381
|
+
react.createElement("section", { className: "dkp-catalog", "aria-label": "模型" },
|
|
382
|
+
react.createElement("div", { className: "dkp-catalog-head" },
|
|
383
|
+
react.createElement("div", { className: "dkp-catalog-titlewrap" },
|
|
384
|
+
react.createElement("span", { className: "dkp-catalog-title" }, "模型"),
|
|
385
|
+
react.createElement("span", { className: "dkp-catalog-meta" },
|
|
386
|
+
"已选 " + pfSelectedCount() + " 个" + (pfFetchedList ? " / 上游共 " + pfFetchedList.length + " 个" : ""))),
|
|
387
|
+
react.createElement("div", { className: "dkp-actions" },
|
|
388
|
+
react.createElement("button", {
|
|
389
|
+
type: "button",
|
|
390
|
+
className: "dkp-btn ghost sm",
|
|
391
|
+
disabled: pfFetching || !pfDraft.baseURL,
|
|
392
|
+
title: "调用 baseURL/models 拉取该端点可用模型,勾选后加入下方列表",
|
|
393
|
+
onClick: fetchPfModels
|
|
394
|
+
}, pfFetching ? "拉取中…" : "获取模型列表"),
|
|
395
|
+
pfFetchedList && pfFetchedList.length > 0
|
|
396
|
+
? react.createElement("button", { type: "button", className: "dkp-btn ghost sm", title: "勾选上游返回的全部模型", onClick: () => setAllPfModels(true) }, "全选")
|
|
397
|
+
: null,
|
|
398
|
+
pfFetchedList && pfFetchedList.length > 0
|
|
399
|
+
? react.createElement("button", { type: "button", className: "dkp-btn ghost sm", title: "清空已勾选的模型", onClick: () => setAllPfModels(false) }, "清空")
|
|
400
|
+
: null)),
|
|
401
|
+
pfFetchedList
|
|
402
|
+
? react.createElement("div", { className: "dkp-picker" },
|
|
403
|
+
pfFetchedList.length === 0
|
|
404
|
+
? react.createElement("div", { className: "dkp-hint" }, "上游没有返回任何模型")
|
|
405
|
+
: pfFetchedList.map((mm) => {
|
|
406
|
+
const id = String((mm && mm.id) || "").trim();
|
|
407
|
+
if (!id) return null;
|
|
408
|
+
const checked = pfDraft.models.some((m) => String(m.id || "").trim() === id);
|
|
409
|
+
return react.createElement("label", { key: id },
|
|
410
|
+
react.createElement("input", {
|
|
411
|
+
type: "checkbox",
|
|
412
|
+
checked: checked,
|
|
413
|
+
onChange: () => togglePfModel(id, (mm && mm.name) || "", !checked)
|
|
414
|
+
}),
|
|
415
|
+
react.createElement("span", { className: "dkp-picker-id" }, id),
|
|
416
|
+
(mm && mm.name) ? react.createElement("span", { className: "dkp-picker-name" }, mm.name) : null);
|
|
417
|
+
}))
|
|
418
|
+
: null,
|
|
419
|
+
pfDraft.models.length === 0
|
|
420
|
+
? react.createElement("div", { className: "dkp-empty" }, "还没有模型——点「获取模型列表」勾选上游模型,或「添加模型」手动填写。")
|
|
421
|
+
: react.createElement("div", { className: "dkp-model-list" },
|
|
422
|
+
pfDraft.models.map((m, i) => react.createElement(ProviderModelRow, { key: i, m: m, i: i, onPatch: patchPfModel, onDel: delPfModel }))),
|
|
423
|
+
react.createElement("button", { type: "button", className: "dkp-addmodel", onClick: addPfModel }, "+ 添加模型")),
|
|
424
|
+
pfMsg && !pfMsg.ok ? react.createElement("p", { className: "dkp-error" }, pfMsg.text) : null,
|
|
425
|
+
react.createElement("div", { className: "dkp-editor-actions" },
|
|
426
|
+
react.createElement("span", { className: "dkp-hint" }, pfDirty ? "有未保存的修改" : ""),
|
|
427
|
+
react.createElement("button", { type: "button", className: "dkp-btn ghost", onClick: closePf }, "取消"),
|
|
428
|
+
react.createElement("button", { type: "button", className: "dkp-btn primary", disabled: pfSaving, onClick: savePf },
|
|
429
|
+
pfSaving ? "保存中…" : (pfMode === "create" ? "新增 Provider" : "保存修改"))));
|
|
430
|
+
|
|
431
|
+
const piProviders = providers.filter((p) => p.kind === "pi-ai");
|
|
432
|
+
const body = [
|
|
433
|
+
react.createElement("h2", { key: "title", className: "dkp-title" }, "模型提供商"),
|
|
434
|
+
react.createElement("p", { key: "intro", className: "dkp-intro" },
|
|
435
|
+
"新增 / 编辑 / 删除自定义模型 Provider(OpenAI 兼容、Anthropic 等),写入官方配置并热生效;API Key 存本机凭据库(文件仅 root 可读)。内置 DeepSeek 官方 Provider 在本页只读,其模型目录在「模型设置」里编辑。本功能可单独停用:停用后这些写配置与出站请求的路由一并关闭。")
|
|
436
|
+
];
|
|
437
|
+
if (snap.error) {
|
|
438
|
+
body.push(react.createElement("p", { key: "err", className: "dkp-error" }, "模型目录拉取失败:" + snap.error));
|
|
439
|
+
}
|
|
440
|
+
if (pfMsg && !pfDraft) {
|
|
441
|
+
body.push(react.createElement("p", { key: "msg", className: "dkp-msg " + (pfMsg.ok ? "ok" : "err"), role: "status" }, pfMsg.text));
|
|
442
|
+
}
|
|
443
|
+
if (!data) {
|
|
444
|
+
body.push(react.createElement("p", { key: "loading", className: "dkp-hint" }, snap.loading ? "正在拉取模型目录…" : "等待拉取模型目录"));
|
|
445
|
+
} else if (piProviders.length === 0) {
|
|
446
|
+
body.push(react.createElement("div", { key: "empty", className: "dkp-empty" }, "还没有自定义 Provider——点下方「新增 Provider」添加。"));
|
|
447
|
+
} else {
|
|
448
|
+
body.push(react.createElement("ul", { key: "rows", className: "dkp-rows" },
|
|
449
|
+
piProviders.map((p) => {
|
|
450
|
+
const route = p.route || p.id;
|
|
451
|
+
const editing = pfMode === "edit" && !!pfDraft && pfDraft.route === route;
|
|
452
|
+
const keyMissing = !!(p.apiKeyEnv && !p.apiKeyConfigured);
|
|
453
|
+
return react.createElement("li", { key: p.id, className: "dkp-card" },
|
|
454
|
+
react.createElement("div", { className: "dkp-rowhead" },
|
|
455
|
+
react.createElement("span", { className: "dkp-identity" },
|
|
456
|
+
react.createElement("span", { className: "dkp-name" }, p.displayName || route),
|
|
457
|
+
react.createElement("span", { className: "dkp-tag" }, route),
|
|
458
|
+
react.createElement("span", { className: "dkp-tag" }, (p.models ? p.models.length : 0) + " 模型"),
|
|
459
|
+
react.createElement("span", {
|
|
460
|
+
className: "dkp-dot " + (keyMissing ? "missing" : "ok"),
|
|
461
|
+
role: "img",
|
|
462
|
+
title: p.apiKeyEnv ? (p.apiKeyConfigured ? p.apiKeyEnv + " 已配置" : p.apiKeyEnv + " 未配置") : "未设 apiKeyEnv",
|
|
463
|
+
"aria-label": p.apiKeyEnv ? (p.apiKeyConfigured ? "密钥已配置" : "密钥未配置") : "未设 apiKeyEnv"
|
|
464
|
+
}),
|
|
465
|
+
p.apiKeyEnv
|
|
466
|
+
? react.createElement("span", { className: "dkp-tag" + (keyMissing ? " warn" : " ok") },
|
|
467
|
+
p.apiKeyConfigured ? p.apiKeyEnv + " 已配置" : p.apiKeyEnv + " 未配置")
|
|
468
|
+
: null),
|
|
469
|
+
react.createElement("span", { className: "dkp-actions" },
|
|
470
|
+
react.createElement("button", {
|
|
471
|
+
type: "button",
|
|
472
|
+
className: "dkp-btn ghost sm",
|
|
473
|
+
"aria-expanded": editing,
|
|
474
|
+
onClick: () => (editing ? closePf() : openPfEdit(p))
|
|
475
|
+
}, editing ? "收起" : "编辑"),
|
|
476
|
+
react.createElement("button", {
|
|
477
|
+
type: "button",
|
|
478
|
+
className: "dkp-btn danger sm" + (pfDelArm === route ? " arm" : ""),
|
|
479
|
+
title: pfDelArm === route ? "再点一次确认删除" : "删除该 Provider",
|
|
480
|
+
onClick: () => deletePf(p)
|
|
481
|
+
}, pfDelArm === route ? "确认删除?" : "删除"))),
|
|
482
|
+
react.createElement("div", { className: "dkp-sub" }, [p.api, p.baseURL].filter(Boolean).join(" · ")),
|
|
483
|
+
editing ? editor : null);
|
|
484
|
+
})));
|
|
485
|
+
}
|
|
486
|
+
body.push(react.createElement("div", { key: "add", className: "dkp-addblock" },
|
|
487
|
+
pfMode === "create" && pfDraft
|
|
488
|
+
? react.createElement("div", { className: "dkp-card dkp-addcard" }, editor)
|
|
489
|
+
: react.createElement("button", { type: "button", className: "dkp-addbtn", onClick: openPfCreate }, "+ 新增 Provider")));
|
|
490
|
+
return react.createElement("div", { className: "dkp-page" }, body);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function ProvidersStat() {
|
|
494
|
+
const snap = useProviders();
|
|
495
|
+
const data = snap.data;
|
|
496
|
+
if (snap.error) return react.createElement("span", { className: "dockm-err" }, "模型提供商拉取失败(点击进入查看)");
|
|
497
|
+
if (!data) return react.createElement("span", null, snap.loading ? "正在拉取模型提供商…" : "等待拉取模型提供商");
|
|
498
|
+
const providers = Array.isArray(data.providers) ? data.providers : [];
|
|
499
|
+
const custom = providers.filter((p) => p.kind === "pi-ai");
|
|
500
|
+
const missing = custom.filter((p) => p.apiKeyEnv && !p.apiKeyConfigured).length;
|
|
501
|
+
return react.createElement("span", null, custom.length + " 个自定义 Provider" + (missing > 0 ? " · " + missing + " 个缺密钥" : ""));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export const feature = {
|
|
505
|
+
id: "providerconfig",
|
|
506
|
+
name: "模型提供商",
|
|
507
|
+
order: 101,
|
|
508
|
+
accent: "#8b5cf6",
|
|
509
|
+
description: "管理自定义模型 Provider:新增/编辑/删除、拉取上游模型列表、写入 API Key(可单独停用)",
|
|
510
|
+
css: [
|
|
511
|
+
// ---- 模型提供商页(dkp- 前缀):对齐官方「模型」设置页的排版与尺寸 ----
|
|
512
|
+
// 官方用量参考:section 限宽 720、行卡片 .5px 描边 + radius-xl + 12/14 内距、
|
|
513
|
+
// 输入高 32、按钮高 36(行内小按钮 28)、新增按钮 44 虚线、编辑器 bg-module-platform。
|
|
514
|
+
".dkp-page{width:100%;max-width:720px;display:flex;flex-direction:column;gap:12px;color:var(--dsw-alias-label-primary);}",
|
|
515
|
+
".dkp-title{margin:0;font-size:16px;font-weight:500;line-height:24px;}",
|
|
516
|
+
".dkp-intro{margin:0;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:22px;}",
|
|
517
|
+
".dkp-rows{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-direction:column;gap:8px;}",
|
|
518
|
+
".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;}",
|
|
519
|
+
".dkp-rowhead{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}",
|
|
520
|
+
".dkp-identity{display:inline-flex;align-items:center;gap:6px;min-width:0;flex-wrap:wrap;}",
|
|
521
|
+
".dkp-name{font-size:14px;font-weight:500;line-height:22px;}",
|
|
522
|
+
".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;}",
|
|
523
|
+
".dkp-tag.ok{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
|
|
524
|
+
".dkp-tag.warn{color:var(--dk-warn);border-color:currentColor;}",
|
|
525
|
+
".dkp-dot{box-sizing:border-box;flex:none;width:8px;height:8px;border-radius:50%;display:inline-block;}",
|
|
526
|
+
".dkp-dot.ok{background:var(--dsw-alias-state-success-primary);}",
|
|
527
|
+
".dkp-dot.missing{background:var(--dsw-alias-state-error-primary);}",
|
|
528
|
+
".dkp-dot.warn{background:var(--dk-warn);}",
|
|
529
|
+
".dkp-sub{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
530
|
+
".dkp-hint{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;}",
|
|
531
|
+
".dkp-dim{color:var(--dsw-alias-label-tertiary);}",
|
|
532
|
+
".dkp-error{margin:0;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;}",
|
|
533
|
+
".dkp-msg{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;}",
|
|
534
|
+
".dkp-msg.ok{color:var(--dsw-alias-state-success-primary);}",
|
|
535
|
+
".dkp-msg.err{color:var(--dsw-alias-state-error-primary);}",
|
|
536
|
+
".dkp-actions{flex:none;margin-left:auto;display:inline-flex;align-items:center;gap:4px;}",
|
|
537
|
+
".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;}",
|
|
538
|
+
".dkp-btn.ghost{border:.5px solid var(--dsw-alias-border-l3);}",
|
|
539
|
+
".dkp-btn.ghost:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
540
|
+
".dkp-btn.primary{background:var(--dsw-alias-button-primary-fill,var(--dk-accent));color:var(--dsw-alias-label-primary-foreground,#fff);}",
|
|
541
|
+
".dkp-btn.primary:hover{background:var(--dsw-alias-button-primary-hover,var(--dk-accent));}",
|
|
542
|
+
".dkp-btn.danger{color:var(--dsw-alias-state-error-primary);}",
|
|
543
|
+
".dkp-btn.danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,rgba(220,38,38,.1));}",
|
|
544
|
+
".dkp-btn.arm{color:var(--dsw-alias-state-error-primary);border-color:currentColor;}",
|
|
545
|
+
".dkp-btn.sm{height:28px;padding:0 10px;border-radius:var(--dsw-radius-sm,6px);font-size:12px;line-height:18px;}",
|
|
546
|
+
".dkp-btn[disabled]{opacity:.4;cursor:default;}",
|
|
547
|
+
".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;}",
|
|
548
|
+
".dkp-editor-head{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;}",
|
|
549
|
+
".dkp-editor-title{font-size:14px;font-weight:500;line-height:22px;}",
|
|
550
|
+
".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;}",
|
|
551
|
+
".dkp-field{display:flex;flex-direction:column;gap:6px;min-width:0;}",
|
|
552
|
+
".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;}",
|
|
553
|
+
".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;}",
|
|
554
|
+
".dkp-input:focus{border-color:var(--dsw-alias-state-business-primary,var(--dk-accent));outline:none;}",
|
|
555
|
+
".dkp-input::placeholder{color:var(--dsw-alias-label-dimmed,var(--dsw-alias-label-tertiary));}",
|
|
556
|
+
".dkp-input:disabled{opacity:.6;cursor:default;}",
|
|
557
|
+
"select.dkp-input{appearance:auto;cursor:pointer;max-width:240px;}",
|
|
558
|
+
".dkp-select-sm{height:28px;max-width:160px;font-size:12px;line-height:18px;}",
|
|
559
|
+
".dkp-checks{display:flex;align-items:center;gap:16px;flex-wrap:wrap;min-height:32px;}",
|
|
560
|
+
".dkp-check{display:inline-flex;align-items:center;gap:6px;cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;}",
|
|
561
|
+
".dkp-check input{accent-color:var(--dk-accent);margin:0;}",
|
|
562
|
+
".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;}",
|
|
563
|
+
".dkp-mini:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-secondary);}",
|
|
564
|
+
".dkp-customized{border-top:.5px solid var(--dsw-alias-border-l2);padding-top:10px;}",
|
|
565
|
+
".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;}",
|
|
566
|
+
".dkp-summary::-webkit-details-marker{display:none;}",
|
|
567
|
+
".dkp-summary::before{content:\"\";width:5px;height:5px;border-right:1.5px solid;border-bottom:1.5px solid;transform:rotate(-45deg);transition:transform .12s;}",
|
|
568
|
+
".dkp-customized[open]>.dkp-summary::before{transform:rotate(45deg);}",
|
|
569
|
+
".dkp-summary:hover{color:var(--dsw-alias-label-primary);}",
|
|
570
|
+
".dkp-custombody{display:flex;flex-direction:column;gap:12px;padding-top:12px;}",
|
|
571
|
+
".dkp-catalog{border-top:.5px solid var(--dsw-alias-border-l2);padding-top:12px;display:flex;flex-direction:column;gap:10px;}",
|
|
572
|
+
".dkp-catalog-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;}",
|
|
573
|
+
".dkp-catalog-titlewrap{display:flex;flex-direction:column;gap:2px;}",
|
|
574
|
+
".dkp-catalog-title{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px;}",
|
|
575
|
+
".dkp-catalog-meta{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;}",
|
|
576
|
+
".dkp-model-list{display:flex;flex-direction:column;gap:8px;}",
|
|
577
|
+
".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;}",
|
|
578
|
+
".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;}",
|
|
579
|
+
".dkp-model-row .dkp-input{height:30px;padding:0 8px;font-size:13px;}",
|
|
580
|
+
".dkp-model-opts{display:flex;flex-direction:column;gap:4px;padding:2px 2px 0;}",
|
|
581
|
+
".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;}",
|
|
582
|
+
".dkp-iconbtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
583
|
+
".dkp-iconbtn.danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,rgba(220,38,38,.1));color:var(--dsw-alias-state-error-primary);}",
|
|
584
|
+
".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;}",
|
|
585
|
+
".dkp-addmodel:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
586
|
+
".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;}",
|
|
587
|
+
".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);}",
|
|
588
|
+
".dkp-picker label:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
589
|
+
".dkp-picker input{accent-color:var(--dk-accent);margin:0;flex:none;}",
|
|
590
|
+
".dkp-picker-id{flex:none;font-family:var(--ds-font-family-code,ui-monospace,monospace);font-size:13px;line-height:20px;}",
|
|
591
|
+
".dkp-picker-name{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
592
|
+
".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;}",
|
|
593
|
+
".dkp-addblock{display:flex;align-items:stretch;}",
|
|
594
|
+
".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;}",
|
|
595
|
+
".dkp-addbtn:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
596
|
+
".dkp-addcard{flex:1 1 auto;padding:14px 16px;}",
|
|
597
|
+
".dkp-addcard .dkp-editor{background:transparent;padding:0;}",
|
|
598
|
+
".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;}",
|
|
599
|
+
".dkp-editor-actions .dkp-hint{margin-right:auto;}",
|
|
600
|
+
"@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}}"
|
|
601
|
+
].join("\n"),
|
|
602
|
+
View: ProvidersView,
|
|
603
|
+
HomeStat: ProvidersStat,
|
|
604
|
+
};
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// 数据形状由客户端消费,字段改动要同时看:features/runstate/view.jsx、
|
|
11
11
|
// features/animation/view.jsx、features/notify/view.jsx。
|
|
12
|
-
import { sendJson, readBody } from '../../src/host-core.js'
|
|
12
|
+
import { sendJson, readBody, rejectUntrustedRequest } from '../../src/host-core.js'
|
|
13
13
|
import { acquireTaskTracker } from '../../src/task-track.js'
|
|
14
14
|
|
|
15
15
|
export const feature = {
|
|
@@ -20,6 +20,8 @@ export const feature = {
|
|
|
20
20
|
setup(ctx) {
|
|
21
21
|
const disposers = []
|
|
22
22
|
const dispose = () => {
|
|
23
|
+
// 撤掉「本宿主注册了 runstate 路由」旗标(见下方注册处的说明)
|
|
24
|
+
try { if (typeof window !== 'undefined' && window.__dshDockRunstate) delete window.__dshDockRunstate } catch { /* ignore */ }
|
|
23
25
|
while (disposers.length > 0) {
|
|
24
26
|
const fn = disposers.pop()
|
|
25
27
|
try { if (typeof fn === 'function') fn() } catch { /* 停用清理失败不阻断 */ }
|
|
@@ -33,10 +35,19 @@ export const feature = {
|
|
|
33
35
|
|
|
34
36
|
// ===== RPC 路由 =====
|
|
35
37
|
disposers.push(ctx.inject(['webServer'], (wsCtx) => {
|
|
38
|
+
// 路由注册成功后打旗标:【手机适配】的汉堡角标据此决定要不要轮询 status。
|
|
39
|
+
// 没有它的话,功能停用时角标会先打一次必然失败的请求 → 控制台一条 405/404,
|
|
40
|
+
// 与【用量记录】chip 当年同一个毛病(见 features/tokenlog/view.jsx 的 routeGone 闩锁)。
|
|
41
|
+
// 旗标与 __dshDockMobileDrawer / __dshDockWelcomeAck 同风格:宿主侧唯一事实,客户端只读。
|
|
42
|
+
wsCtx.effect(() => {
|
|
43
|
+
if (typeof window !== 'undefined') window.__dshDockRunstate = true
|
|
44
|
+
return () => { try { if (typeof window !== 'undefined' && window.__dshDockRunstate) delete window.__dshDockRunstate } catch { /* ignore */ } }
|
|
45
|
+
})
|
|
36
46
|
wsCtx.effect(() => wsCtx.webServer.register({
|
|
37
47
|
kind: 'prefix',
|
|
38
48
|
path: '/dsh-dock/runstate',
|
|
39
49
|
async handler(req, res) {
|
|
50
|
+
if (rejectUntrustedRequest(ctx, req, res)) return
|
|
40
51
|
try {
|
|
41
52
|
const url = new URL(req.url || '/', 'http://dsh.internal')
|
|
42
53
|
const method = url.pathname.replace(/^\/dsh-dock\/runstate\/?/, '').split('/')[0] || ''
|