dsh-provider-hub 0.1.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/LICENSE +21 -0
- package/README.md +81 -0
- package/cordis.patch.yml +13 -0
- package/core/coverage.js +31 -0
- package/core/profile.js +144 -0
- package/core/providers.js +342 -0
- package/package.json +57 -0
- package/src/client.js +386 -0
- package/src/index.js +320 -0
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-provider-hub",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "只填 Key 的服务商预设:为 DSH 一键写入官方 llm-pi-ai 路由与凭据,内置 20 家 OpenAI 兼容 / Anthropic Messages 预设与自定义端点。核心为纯 JS,可被桌面端等其它外壳复用。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./client": "./src/client.js",
|
|
10
|
+
"./core": "./core/providers.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"core",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test \"test/*.test.js\""
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"deepseek-harness",
|
|
24
|
+
"dsh",
|
|
25
|
+
"dsh-plugin",
|
|
26
|
+
"provider",
|
|
27
|
+
"presets",
|
|
28
|
+
"openai-compatible",
|
|
29
|
+
"anthropic",
|
|
30
|
+
"api-key"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"author": "DDDMUC",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/DDDMUC/dsh-provider-hub.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/DDDMUC/dsh-provider-hub#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/DDDMUC/dsh-provider-hub/issues"
|
|
41
|
+
},
|
|
42
|
+
"dsh": {
|
|
43
|
+
"bundle": {
|
|
44
|
+
"patch": "./cordis.patch.yml"
|
|
45
|
+
},
|
|
46
|
+
"client": {
|
|
47
|
+
"platform": "web",
|
|
48
|
+
"inject": [
|
|
49
|
+
"@deepseek-ai/dsh-client-locale",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-renderer"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"dsh": ">=0.1.6-alpha.2"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// dsh-provider-hub - browser half.
|
|
2
|
+
//
|
|
3
|
+
// One entry point: a card on the official Models settings page footer
|
|
4
|
+
// (`settings.models.footer`) that turns the curated catalog served by the host
|
|
5
|
+
// half into a one-field form per provider - paste the key, press 启用. All
|
|
6
|
+
// writes ride the host's loopback routes; this module only renders and calls
|
|
7
|
+
// fetch, so it needs no build step and no client-module dependencies beyond
|
|
8
|
+
// react and the official primitives.
|
|
9
|
+
//
|
|
10
|
+
// The module is a classic client bundle (client-modules protocol): it
|
|
11
|
+
// registers a factory with window.__ModuleLoader__ and returns apply().
|
|
12
|
+
window.__ModuleLoader__.load({
|
|
13
|
+
id: 'dsh-provider-hub',
|
|
14
|
+
factory: (require) => {
|
|
15
|
+
var module = { exports: {} }
|
|
16
|
+
var exports = module.exports
|
|
17
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
18
|
+
|
|
19
|
+
const react = require('react')
|
|
20
|
+
const jsxRuntime = require('react/jsx-runtime')
|
|
21
|
+
const { jsx } = jsxRuntime
|
|
22
|
+
|
|
23
|
+
const NS = 'dsh-provider-hub'
|
|
24
|
+
const ROUTE_PREFIX = '/dsh-provider-hub'
|
|
25
|
+
|
|
26
|
+
/** Tiny element helper: children ride props.children like the jsx runtime expects. */
|
|
27
|
+
const h = (type, props, ...children) =>
|
|
28
|
+
jsx(type, {
|
|
29
|
+
...(props || {}),
|
|
30
|
+
...(children.length === 0 ? {} : { children: children.length === 1 ? children[0] : children }),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// --- copy -----------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const zh = {
|
|
36
|
+
'title': '服务商预设',
|
|
37
|
+
'hint': '选一个服务商,粘贴 API Key 即可启用。写入官方 llm-pi-ai 设置与凭据服务,立即生效、无需重启。',
|
|
38
|
+
'refresh': '刷新',
|
|
39
|
+
'loading': '读取中…',
|
|
40
|
+
'error.keyRequired': '请先填写 API Key',
|
|
41
|
+
'notice.enabled': '已启用 {name}',
|
|
42
|
+
'notice.removed': '已移除 {route}',
|
|
43
|
+
'configured.title': '已配置的服务商',
|
|
44
|
+
'configured.empty': '还没有通过本面板添加的服务商。',
|
|
45
|
+
'presets.title': '未内置的服务商({visible} 个)',
|
|
46
|
+
'presets.showCovered': '显示 DSH 已内置的 {covered} 个',
|
|
47
|
+
'presets.hideCovered': '收起 DSH 已内置的 {covered} 个',
|
|
48
|
+
'badge.native': 'DSH 已内置',
|
|
49
|
+
'models.count': '{n} 个默认模型',
|
|
50
|
+
'status.live': '运行中',
|
|
51
|
+
'status.stored': '已写入',
|
|
52
|
+
'status.noKey': '缺 Key',
|
|
53
|
+
'key.placeholder': '粘贴 {env}',
|
|
54
|
+
'key.savedHint': '凭据服务里已有 Key;留空则保留现有 Key。',
|
|
55
|
+
'action.enable': '启用',
|
|
56
|
+
'action.remove': '移除',
|
|
57
|
+
'remove.confirm': '移除 {route}?已保存的 Key 会保留在凭据服务里。',
|
|
58
|
+
'custom.title': '自定义服务商(任何 OpenAI 兼容端点)',
|
|
59
|
+
'custom.route': '路由 ID(小写字母/数字/连字符)',
|
|
60
|
+
'custom.name': '显示名称(可空)',
|
|
61
|
+
'custom.baseURL': 'Base URL(到 /v1 为止)',
|
|
62
|
+
'custom.api': '协议',
|
|
63
|
+
'custom.models': '模型 ID(逗号或换行分隔)',
|
|
64
|
+
'custom.add': '添加',
|
|
65
|
+
'docs': '模型列表',
|
|
66
|
+
'getKey': '获取 Key',
|
|
67
|
+
}
|
|
68
|
+
const en = {
|
|
69
|
+
'title': 'Provider presets',
|
|
70
|
+
'hint': 'Pick a provider, paste the API key, press Enable. Writes the official llm-pi-ai settings and credentials service - effective immediately, no restart.',
|
|
71
|
+
'refresh': 'Refresh',
|
|
72
|
+
'loading': 'Loading…',
|
|
73
|
+
'error.keyRequired': 'Enter an API key first',
|
|
74
|
+
'notice.enabled': 'Enabled {name}',
|
|
75
|
+
'notice.removed': 'Removed {route}',
|
|
76
|
+
'configured.title': 'Configured providers',
|
|
77
|
+
'configured.empty': 'No provider has been added through this panel yet.',
|
|
78
|
+
'presets.title': 'Providers not built into DSH ({visible})',
|
|
79
|
+
'presets.showCovered': 'Show {covered} built into DSH',
|
|
80
|
+
'presets.hideCovered': 'Hide {covered} built into DSH',
|
|
81
|
+
'badge.native': 'built into DSH',
|
|
82
|
+
'models.count': '{n} default models',
|
|
83
|
+
'status.live': 'live',
|
|
84
|
+
'status.stored': 'written',
|
|
85
|
+
'status.noKey': 'no key',
|
|
86
|
+
'key.placeholder': 'Paste {env}',
|
|
87
|
+
'key.savedHint': 'A key already exists; leave empty to keep it.',
|
|
88
|
+
'action.enable': 'Enable',
|
|
89
|
+
'action.remove': 'Remove',
|
|
90
|
+
'remove.confirm': 'Remove {route}? The stored key stays in the credentials service.',
|
|
91
|
+
'custom.title': 'Custom provider (any OpenAI-compatible endpoint)',
|
|
92
|
+
'custom.route': 'Route id (lowercase letters/digits/hyphens)',
|
|
93
|
+
'custom.name': 'Display name (optional)',
|
|
94
|
+
'custom.baseURL': 'Base URL (up to /v1)',
|
|
95
|
+
'custom.api': 'Protocol',
|
|
96
|
+
'custom.models': 'Model ids (comma or newline separated)',
|
|
97
|
+
'custom.add': 'Add',
|
|
98
|
+
'docs': 'Models',
|
|
99
|
+
'getKey': 'Get a key',
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const format = (template, vars) =>
|
|
103
|
+
String(template).replace(/\{(\w+)\}/g, (_, key) => (vars && vars[key] !== undefined ? String(vars[key]) : ''))
|
|
104
|
+
|
|
105
|
+
// --- host calls -----------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
async function call(path, init) {
|
|
108
|
+
const response = await fetch(`${ROUTE_PREFIX}${path}`, {
|
|
109
|
+
headers: { 'content-type': 'application/json' },
|
|
110
|
+
...init,
|
|
111
|
+
})
|
|
112
|
+
let payload = null
|
|
113
|
+
try {
|
|
114
|
+
payload = await response.json()
|
|
115
|
+
} catch {
|
|
116
|
+
payload = null
|
|
117
|
+
}
|
|
118
|
+
if (!payload || payload.ok !== true) {
|
|
119
|
+
throw new Error((payload && (payload.error || payload.code)) || `HTTP ${response.status}`)
|
|
120
|
+
}
|
|
121
|
+
return payload
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- styles ---------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
const S = {
|
|
127
|
+
card: { border: '1px solid var(--dsw-alias-border-l2, #e5e7eb)', borderRadius: 12, padding: '14px 16px', marginTop: 16, fontSize: 13 },
|
|
128
|
+
header: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 },
|
|
129
|
+
title: { fontWeight: 600, fontSize: 14 },
|
|
130
|
+
hint: { opacity: 0.7, lineHeight: 1.5, marginBottom: 10 },
|
|
131
|
+
section: { margin: '12px 0 6px', fontWeight: 600, opacity: 0.85 },
|
|
132
|
+
row: { display: 'flex', alignItems: 'center', gap: 8, padding: '8px 0', borderTop: '1px solid var(--dsw-alias-border-l3, #f0f0f0)' },
|
|
133
|
+
name: { minWidth: 150, fontWeight: 500 },
|
|
134
|
+
sub: { opacity: 0.6, fontSize: 12 },
|
|
135
|
+
grow: { flex: 1, minWidth: 0 },
|
|
136
|
+
input: { flex: 1, minWidth: 160, padding: '7px 9px', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2, #d7dbe0)', background: 'transparent', color: 'inherit', fontSize: 13 },
|
|
137
|
+
button: { padding: '7px 14px', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2, #d7dbe0)', background: 'transparent', color: 'inherit', cursor: 'pointer', fontSize: 13 },
|
|
138
|
+
primary: { padding: '7px 16px', borderRadius: 8, border: '1px solid transparent', background: 'var(--dsw-alias-interactive-bg-hover, #2f6fec)', color: '#fff', cursor: 'pointer', fontSize: 13, fontWeight: 600 },
|
|
139
|
+
danger: { padding: '5px 10px', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2, #d7dbe0)', background: 'transparent', color: 'var(--dsw-alias-state-error-primary, #c0392b)', cursor: 'pointer', fontSize: 12 },
|
|
140
|
+
badge: { fontSize: 11, padding: '2px 7px', borderRadius: 999, border: '1px solid var(--dsw-alias-border-l2, #d7dbe0)', opacity: 0.85 },
|
|
141
|
+
linkButton: { background: 'none', border: 'none', color: 'var(--dsw-alias-link, #2f6fec)', cursor: 'pointer', fontSize: 12, padding: 0 },
|
|
142
|
+
link: { color: 'var(--dsw-alias-link, #2f6fec)', textDecoration: 'none', fontSize: 12 },
|
|
143
|
+
error: { color: 'var(--dsw-alias-state-error-primary, #c0392b)', margin: '6px 0' },
|
|
144
|
+
notice: { color: 'var(--dsw-alias-state-success-primary, #1d9a4a)', margin: '6px 0' },
|
|
145
|
+
form: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginTop: 8 },
|
|
146
|
+
field: { display: 'flex', flexDirection: 'column', gap: 4 },
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- component ------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
function PresetsCard({ t }) {
|
|
152
|
+
const tr = (key, vars) => format(t(key), vars)
|
|
153
|
+
const [state, setState] = react.useState(null)
|
|
154
|
+
const [error, setError] = react.useState(null)
|
|
155
|
+
const [notice, setNotice] = react.useState(null)
|
|
156
|
+
const [busy, setBusy] = react.useState(null)
|
|
157
|
+
const [keys, setKeys] = react.useState({})
|
|
158
|
+
const [customOpen, setCustomOpen] = react.useState(false)
|
|
159
|
+
const [showCovered, setShowCovered] = react.useState(false)
|
|
160
|
+
const [custom, setCustom] = react.useState({
|
|
161
|
+
route: '',
|
|
162
|
+
displayName: '',
|
|
163
|
+
baseURL: '',
|
|
164
|
+
api: 'openai-completions',
|
|
165
|
+
modelIds: '',
|
|
166
|
+
key: '',
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const load = react.useCallback(async () => {
|
|
170
|
+
setBusy('__load')
|
|
171
|
+
try {
|
|
172
|
+
const next = await call('/state')
|
|
173
|
+
setState(next)
|
|
174
|
+
setError(null)
|
|
175
|
+
} catch (reason) {
|
|
176
|
+
setError(String((reason && reason.message) || reason))
|
|
177
|
+
} finally {
|
|
178
|
+
setBusy(null)
|
|
179
|
+
}
|
|
180
|
+
}, [])
|
|
181
|
+
|
|
182
|
+
react.useEffect(() => {
|
|
183
|
+
load()
|
|
184
|
+
}, [load])
|
|
185
|
+
|
|
186
|
+
const enablePreset = async (preset) => {
|
|
187
|
+
const key = String(keys[preset.id] || '').trim()
|
|
188
|
+
if (key === '') {
|
|
189
|
+
setError(tr('error.keyRequired'))
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
setBusy(preset.id)
|
|
193
|
+
setError(null)
|
|
194
|
+
setNotice(null)
|
|
195
|
+
try {
|
|
196
|
+
await call('/enable', { method: 'POST', body: JSON.stringify({ presetId: preset.id, key }) })
|
|
197
|
+
setKeys((current) => ({ ...current, [preset.id]: '' }))
|
|
198
|
+
setNotice(tr('notice.enabled', { name: preset.name }))
|
|
199
|
+
await load()
|
|
200
|
+
} catch (reason) {
|
|
201
|
+
setError(String((reason && reason.message) || reason))
|
|
202
|
+
} finally {
|
|
203
|
+
setBusy(null)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const removeRoute = async (route) => {
|
|
208
|
+
if (typeof window !== 'undefined' && typeof window.confirm === 'function' && !window.confirm(tr('remove.confirm', { route }))) {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
setBusy(route)
|
|
212
|
+
setError(null)
|
|
213
|
+
setNotice(null)
|
|
214
|
+
try {
|
|
215
|
+
await call('/remove', { method: 'POST', body: JSON.stringify({ route }) })
|
|
216
|
+
setNotice(tr('notice.removed', { route }))
|
|
217
|
+
await load()
|
|
218
|
+
} catch (reason) {
|
|
219
|
+
setError(String((reason && reason.message) || reason))
|
|
220
|
+
} finally {
|
|
221
|
+
setBusy(null)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const addCustom = async () => {
|
|
226
|
+
const key = String(custom.key || '').trim()
|
|
227
|
+
if (key === '') {
|
|
228
|
+
setError(tr('error.keyRequired'))
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
setBusy('__custom')
|
|
232
|
+
setError(null)
|
|
233
|
+
setNotice(null)
|
|
234
|
+
try {
|
|
235
|
+
await call('/enable', {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
body: JSON.stringify({
|
|
238
|
+
route: custom.route,
|
|
239
|
+
displayName: custom.displayName,
|
|
240
|
+
baseURL: custom.baseURL,
|
|
241
|
+
api: custom.api,
|
|
242
|
+
modelIds: custom.modelIds,
|
|
243
|
+
key,
|
|
244
|
+
}),
|
|
245
|
+
})
|
|
246
|
+
setCustom({ route: '', displayName: '', baseURL: '', api: 'openai-completions', modelIds: '', key: '' })
|
|
247
|
+
setNotice(tr('notice.enabled', { name: custom.route }))
|
|
248
|
+
await load()
|
|
249
|
+
} catch (reason) {
|
|
250
|
+
setError(String((reason && reason.message) || reason))
|
|
251
|
+
} finally {
|
|
252
|
+
setBusy(null)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const configured = state ? Object.entries(state.routes || {}) : []
|
|
257
|
+
const presets = state ? state.presets || [] : []
|
|
258
|
+
const visiblePresets = presets.filter((preset) => preset.covered !== true)
|
|
259
|
+
const coveredPresets = presets.filter((preset) => preset.covered === true)
|
|
260
|
+
|
|
261
|
+
const statusBadge = (route) => {
|
|
262
|
+
const info = state && state.routes ? state.routes[route] : undefined
|
|
263
|
+
if (!info) return null
|
|
264
|
+
const parts = []
|
|
265
|
+
if (info.live) parts.push(tr('status.live'))
|
|
266
|
+
else parts.push(tr('status.stored'))
|
|
267
|
+
if (!info.keyConfigured) parts.push(tr('status.noKey'))
|
|
268
|
+
return h('span', { style: S.badge }, parts.join(' · '))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const rowStyleBusy = (id) => (busy === id ? { opacity: 0.55, pointerEvents: 'none' } : {})
|
|
272
|
+
|
|
273
|
+
const renderPresetRow = (preset) =>
|
|
274
|
+
h('div', { style: { ...S.row, ...rowStyleBusy(preset.id) }, key: `preset-${preset.id}` },
|
|
275
|
+
h('span', { style: { ...S.name, display: 'flex', flexDirection: 'column' } },
|
|
276
|
+
h('span', null,
|
|
277
|
+
preset.name,
|
|
278
|
+
preset.covered === true ? ' ' : null,
|
|
279
|
+
preset.covered === true
|
|
280
|
+
? h('span', { style: S.badge, title: preset.coveredBy ? `${tr('badge.native')}: ${preset.coveredBy}` : tr('badge.native') }, tr('badge.native'))
|
|
281
|
+
: null,
|
|
282
|
+
' ',
|
|
283
|
+
h('a', { style: S.link, href: preset.docs, target: '_blank', rel: 'noreferrer' }, `[${tr('docs')}]`),
|
|
284
|
+
' ',
|
|
285
|
+
h('a', { style: S.link, href: preset.keyUrl, target: '_blank', rel: 'noreferrer' }, `[${tr('getKey')}]`),
|
|
286
|
+
),
|
|
287
|
+
h('span', { style: S.sub }, `${preset.env} · ${tr('models.count', { n: preset.models.length })} · ${preset.models.join(', ')}`),
|
|
288
|
+
),
|
|
289
|
+
preset.configured ? statusBadge(preset.id) : null,
|
|
290
|
+
preset.configured
|
|
291
|
+
? h('span', { style: S.grow })
|
|
292
|
+
: h('input', {
|
|
293
|
+
style: S.input,
|
|
294
|
+
type: 'password',
|
|
295
|
+
autoComplete: 'off',
|
|
296
|
+
placeholder: tr('key.placeholder', { env: preset.env }),
|
|
297
|
+
value: keys[preset.id] || '',
|
|
298
|
+
onChange: (event) => setKeys((current) => ({ ...current, [preset.id]: event.target.value })),
|
|
299
|
+
}),
|
|
300
|
+
preset.configured
|
|
301
|
+
? h('button', { type: 'button', style: S.danger, onClick: () => removeRoute(preset.id) }, tr('action.remove'))
|
|
302
|
+
: h('button', { type: 'button', style: S.primary, onClick: () => enablePreset(preset) }, tr('action.enable')),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return h(
|
|
306
|
+
'section',
|
|
307
|
+
{ style: S.card, 'data-dsh-part': 'provider-hub' },
|
|
308
|
+
h('div', { style: S.header },
|
|
309
|
+
h('span', { style: S.title }, tr('title')),
|
|
310
|
+
h('span', { style: S.grow }),
|
|
311
|
+
busy === '__load'
|
|
312
|
+
? h('span', { style: S.sub }, tr('loading'))
|
|
313
|
+
: h('button', { type: 'button', style: S.button, onClick: load }, tr('refresh')),
|
|
314
|
+
),
|
|
315
|
+
h('div', { style: S.hint }, tr('hint')),
|
|
316
|
+
error ? h('div', { style: S.error }, error) : null,
|
|
317
|
+
notice ? h('div', { style: S.notice }, notice) : null,
|
|
318
|
+
|
|
319
|
+
h('div', { style: S.section }, tr('configured.title')),
|
|
320
|
+
configured.length === 0
|
|
321
|
+
? h('div', { style: S.sub }, tr('configured.empty'))
|
|
322
|
+
: configured.map(([route, info]) =>
|
|
323
|
+
h('div', { style: { ...S.row, ...rowStyleBusy(route) }, key: `cfg-${route}` },
|
|
324
|
+
h('span', { style: S.name }, info.displayName || route),
|
|
325
|
+
h('span', { style: S.sub }, `${route} · ${info.env || '-'} · ${info.modelCount} models`),
|
|
326
|
+
statusBadge(route),
|
|
327
|
+
h('span', { style: S.grow }),
|
|
328
|
+
h('button', { type: 'button', style: S.danger, onClick: () => removeRoute(route) }, tr('action.remove')),
|
|
329
|
+
),
|
|
330
|
+
),
|
|
331
|
+
|
|
332
|
+
h('div', { style: { ...S.section, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
|
|
333
|
+
h('span', null, tr('presets.title', { visible: visiblePresets.length })),
|
|
334
|
+
coveredPresets.length > 0
|
|
335
|
+
? h('button', { type: 'button', style: S.linkButton, onClick: () => setShowCovered((open) => !open) },
|
|
336
|
+
showCovered
|
|
337
|
+
? tr('presets.hideCovered', { covered: coveredPresets.length })
|
|
338
|
+
: tr('presets.showCovered', { covered: coveredPresets.length }))
|
|
339
|
+
: null,
|
|
340
|
+
),
|
|
341
|
+
visiblePresets.map(renderPresetRow),
|
|
342
|
+
showCovered ? coveredPresets.map(renderPresetRow) : null,
|
|
343
|
+
|
|
344
|
+
h('div', { style: S.section },
|
|
345
|
+
h('button', { type: 'button', style: S.button, onClick: () => setCustomOpen((open) => !open) }, tr('custom.title')),
|
|
346
|
+
),
|
|
347
|
+
customOpen
|
|
348
|
+
? h('div', null,
|
|
349
|
+
h('div', { style: S.form },
|
|
350
|
+
h('label', { style: S.field }, h('span', { style: S.sub }, tr('custom.route')), h('input', { style: S.input, value: custom.route, placeholder: 'my-gateway', onChange: (e) => setCustom((c) => ({ ...c, route: e.target.value })) })),
|
|
351
|
+
h('label', { style: S.field }, h('span', { style: S.sub }, tr('custom.name')), h('input', { style: S.input, value: custom.displayName, placeholder: 'My Gateway', onChange: (e) => setCustom((c) => ({ ...c, displayName: e.target.value })) })),
|
|
352
|
+
h('label', { style: S.field }, h('span', { style: S.sub }, tr('custom.baseURL')), h('input', { style: S.input, value: custom.baseURL, placeholder: 'https://api.example.com/v1', onChange: (e) => setCustom((c) => ({ ...c, baseURL: e.target.value })) })),
|
|
353
|
+
h('label', { style: S.field }, h('span', { style: S.sub }, tr('custom.api')), h('select', { style: S.input, value: custom.api, onChange: (e) => setCustom((c) => ({ ...c, api: e.target.value })) }, h('option', { value: 'openai-completions' }, 'openai-completions'), h('option', { value: 'anthropic-messages' }, 'anthropic-messages'))),
|
|
354
|
+
h('label', { style: S.field }, h('span', { style: S.sub }, tr('custom.models')), h('input', { style: S.input, value: custom.modelIds, placeholder: 'model-a, model-b', onChange: (e) => setCustom((c) => ({ ...c, modelIds: e.target.value })) })),
|
|
355
|
+
h('label', { style: S.field }, h('span', { style: S.sub }, 'API Key'), h('input', { style: S.input, type: 'password', autoComplete: 'off', value: custom.key, onChange: (e) => setCustom((c) => ({ ...c, key: e.target.value })) })),
|
|
356
|
+
),
|
|
357
|
+
h('div', { style: { marginTop: 10 } },
|
|
358
|
+
h('button', { type: 'button', style: S.primary, onClick: addCustom }, tr('custom.add')),
|
|
359
|
+
),
|
|
360
|
+
)
|
|
361
|
+
: null,
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// --- plugin ---------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
function apply(ctx) {
|
|
368
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-provider-hub: dictionaries')
|
|
369
|
+
ctx.slots.inject('settings.models.footer', () =>
|
|
370
|
+
ctx.slots.register(
|
|
371
|
+
{
|
|
372
|
+
name: 'settings.models.footer',
|
|
373
|
+
id: 'provider-hub',
|
|
374
|
+
order: 20,
|
|
375
|
+
locale: NS,
|
|
376
|
+
},
|
|
377
|
+
PresetsCard,
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
exports.apply = apply
|
|
383
|
+
exports.inject = ['slots', 'locale']
|
|
384
|
+
return module.exports
|
|
385
|
+
},
|
|
386
|
+
})
|