@mengruo/dsh-vision-toolkit 0.0.1 → 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/README.i18n.yaml +2 -2
- package/README.md +17 -42
- package/README.zh.md +16 -41
- package/assets/logo_eapi_dark.png +0 -0
- package/docs/aihubmix-gemini-vision.i18n.yaml +2 -2
- package/docs/aihubmix-gemini-vision.md +2 -2
- package/docs/aihubmix-gemini-vision.zh.md +2 -2
- package/lib/client.js +246 -68
- package/lib/client.js.map +1 -1
- package/lib/config.js +143 -24
- package/lib/config.js.map +1 -1
- package/lib/evidence-cache.js +14 -0
- package/lib/evidence-cache.js.map +1 -1
- package/lib/image-input-variants.js +39 -27
- package/lib/image-input-variants.js.map +1 -1
- package/lib/runtime-install.js +204 -19
- package/lib/runtime-install.js.map +1 -1
- package/lib/runtime.js +310 -154
- package/lib/runtime.js.map +1 -1
- package/lib/tools.js +1 -0
- package/lib/tools.js.map +1 -1
- package/lib/types/client/index.d.ts +45 -3
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/config.d.ts +53 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/evidence-cache.d.ts.map +1 -1
- package/lib/types/image-input-variants.d.ts +1 -6
- package/lib/types/image-input-variants.d.ts.map +1 -1
- package/lib/types/runtime-install.d.ts +21 -0
- package/lib/types/runtime-install.d.ts.map +1 -1
- package/lib/types/runtime.d.ts +45 -10
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/tools.d.ts.map +1 -1
- package/lib/types/upstream.d.ts +3 -0
- package/lib/types/upstream.d.ts.map +1 -1
- package/lib/types/web.d.ts +8 -0
- package/lib/types/web.d.ts.map +1 -1
- package/lib/upstream.js +11 -6
- package/lib/upstream.js.map +1 -1
- package/lib/web.js +50 -7
- package/lib/web.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.tsx +331 -88
- package/src/config.ts +210 -28
- package/src/evidence-cache.ts +14 -0
- package/src/image-input-variants.ts +39 -27
- package/src/runtime-install.ts +231 -20
- package/src/runtime.ts +333 -180
- package/src/tools.ts +1 -0
- package/src/upstream.ts +15 -8
- package/src/web.ts +67 -8
package/src/client/index.tsx
CHANGED
|
@@ -31,8 +31,6 @@ const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKi
|
|
|
31
31
|
const BUILT_IN_FREE_VISION_BASE_URL = 'https://vision.anionex.me/v1'
|
|
32
32
|
const BUILT_IN_FREE_VISION_CREDENTIAL = 'ANIONEX_FREE_VISION'
|
|
33
33
|
const BUILT_IN_FREE_VISION_MODEL = 'gemini-3.7-flash'
|
|
34
|
-
const AIHUBMIX_TUTORIAL_URL_EN = 'https://github.com/Anionex/dsh-vision-toolkit/blob/main/docs/aihubmix-gemini-vision.md'
|
|
35
|
-
const AIHUBMIX_TUTORIAL_URL_ZH = 'https://github.com/Anionex/dsh-vision-toolkit/blob/main/docs/aihubmix-gemini-vision.zh.md'
|
|
36
34
|
|
|
37
35
|
const en = {
|
|
38
36
|
nav: 'Vision',
|
|
@@ -40,7 +38,22 @@ const en = {
|
|
|
40
38
|
settingsIntro: 'Configure the pinned visual engineering runtime, its external vision endpoint, and local safety limits.',
|
|
41
39
|
externalNotice: 'Remote tools send the selected image bytes to the configured external vision API. Local crop, trace, pixel diff, palette, foreground extraction, and HTML rendering do not upload images.',
|
|
42
40
|
provider: 'Vision service',
|
|
43
|
-
providerHint: '
|
|
41
|
+
providerHint: 'Add multiple API endpoints and order them by failover priority. A failed or rate-limited request automatically moves to the next provider; when every provider fails, the request reports an error.',
|
|
42
|
+
providers: 'Vision providers',
|
|
43
|
+
name: 'Label',
|
|
44
|
+
enabled: 'Enabled',
|
|
45
|
+
priority: 'Priority {index}',
|
|
46
|
+
attempts: 'Attempts',
|
|
47
|
+
attemptsHint: 'Total attempts against this provider before failing over to the next one.',
|
|
48
|
+
addProvider: 'Add provider',
|
|
49
|
+
moveUp: 'Move up',
|
|
50
|
+
moveDown: 'Move down',
|
|
51
|
+
removeProvider: 'Remove',
|
|
52
|
+
prioritySort: 'Provider priority',
|
|
53
|
+
prioritySortHint: 'Order the providers by failover priority. The top one is tried first; request order here is the priority order.',
|
|
54
|
+
runtimeCheck: 'Runtime check',
|
|
55
|
+
runtimeCheckHint: 'Checks the local Python environment (with its libraries) and the browser used for HTML screenshots.',
|
|
56
|
+
apiKeyBuiltInFree: 'The built-in free provider needs no user key.',
|
|
44
57
|
aihubmixTutorial: 'Need an AIHubMix key for free Gemini 3.7 Flash vision? Follow the signup guide →',
|
|
45
58
|
baseUrl: 'Base URL',
|
|
46
59
|
apiKey: 'API key',
|
|
@@ -228,7 +241,22 @@ const zh: Record<LocaleKey, string> = {
|
|
|
228
241
|
settingsIntro: '在这里设置视觉模型服务、工具运行环境,以及图片和文件的本地访问范围。',
|
|
229
242
|
externalNotice: '使用图像理解、目标定位、界面检测或文字识别等在线功能时,所选图片会发送到下方配置的视觉服务。图片裁剪、轮廓描摹、像素对比、主色提取、前景提取和网页截图均在本机完成,不会上传图片。',
|
|
230
243
|
provider: '在线视觉服务',
|
|
231
|
-
providerHint: '
|
|
244
|
+
providerHint: '添加多个 API 并按故障转移优先级排序;某个 API 失败或限流后自动切换到下一个,全部失败才返回错误。',
|
|
245
|
+
providers: '视觉服务列表',
|
|
246
|
+
name: '名称',
|
|
247
|
+
enabled: '启用',
|
|
248
|
+
priority: '优先级 {index}',
|
|
249
|
+
attempts: '尝试次数',
|
|
250
|
+
attemptsHint: '该服务在故障转移到下一个之前最多尝试的次数。',
|
|
251
|
+
addProvider: '添加服务',
|
|
252
|
+
moveUp: '上移',
|
|
253
|
+
moveDown: '下移',
|
|
254
|
+
removeProvider: '移除',
|
|
255
|
+
prioritySort: '服务优先级',
|
|
256
|
+
prioritySortHint: '按故障转移优先级排列服务;排在最上面的最先尝试,这里的顺序就是优先级顺序。',
|
|
257
|
+
runtimeCheck: '运行检查',
|
|
258
|
+
runtimeCheckHint: '检查本地 Python 环境(含依赖库)和用于网页截图的浏览器。',
|
|
259
|
+
apiKeyBuiltInFree: '内置免费服务无需填写密钥。',
|
|
232
260
|
aihubmixTutorial: '想申请 AIHubMix Key,并用免费 Gemini 3.7 Flash 识图?看这篇图文教程 →',
|
|
233
261
|
baseUrl: 'API 地址',
|
|
234
262
|
apiKey: 'API 密钥',
|
|
@@ -463,6 +491,23 @@ interface HealthResult {
|
|
|
463
491
|
modelTested: boolean
|
|
464
492
|
}
|
|
465
493
|
|
|
494
|
+
interface ProviderValue {
|
|
495
|
+
id?: string
|
|
496
|
+
name?: string
|
|
497
|
+
enabled?: boolean
|
|
498
|
+
baseUrl?: string
|
|
499
|
+
credential?: string
|
|
500
|
+
model?: string
|
|
501
|
+
protocol?: 'openai' | 'anthropic'
|
|
502
|
+
anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
|
|
503
|
+
userAgent?: string
|
|
504
|
+
timeoutMs?: number
|
|
505
|
+
maxImageBytes?: number
|
|
506
|
+
maxImagePixels?: number
|
|
507
|
+
concurrency?: number
|
|
508
|
+
attempts?: number
|
|
509
|
+
}
|
|
510
|
+
|
|
466
511
|
interface SettingsValue {
|
|
467
512
|
provider?: {
|
|
468
513
|
baseUrl?: string
|
|
@@ -472,6 +517,7 @@ interface SettingsValue {
|
|
|
472
517
|
anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
|
|
473
518
|
userAgent?: string
|
|
474
519
|
}
|
|
520
|
+
providers?: ProviderValue[]
|
|
475
521
|
language?: 'zh' | 'en'
|
|
476
522
|
timeoutMs?: number
|
|
477
523
|
maxImageBytes?: number
|
|
@@ -533,6 +579,7 @@ interface SettingsSnapshot {
|
|
|
533
579
|
writable: boolean
|
|
534
580
|
settings: { value: SettingsValue; revision: number; applies: 'live' }
|
|
535
581
|
credential: { ref: string; configured: boolean; source?: string; writable: boolean }
|
|
582
|
+
credentials: Array<{ ref: string; configured: boolean; source?: string; writable: boolean }>
|
|
536
583
|
runtime: {
|
|
537
584
|
ready: boolean
|
|
538
585
|
generation: number
|
|
@@ -907,6 +954,7 @@ interface SettingsState {
|
|
|
907
954
|
status: 'idle' | 'loading' | 'ready' | 'error'
|
|
908
955
|
snapshot?: SettingsSnapshot | undefined
|
|
909
956
|
health?: HealthResult | undefined
|
|
957
|
+
providerHealth?: Record<number, HealthResult> | undefined
|
|
910
958
|
update?: PluginUpdateCheck | undefined
|
|
911
959
|
restart?: PluginUpdateResult | undefined
|
|
912
960
|
action?: 'save' | 'health' | 'connection' | 'model' | 'check-update' | 'apply-update' | undefined
|
|
@@ -959,7 +1007,7 @@ export class VisionSettingsController {
|
|
|
959
1007
|
async save(
|
|
960
1008
|
value: SettingsValue,
|
|
961
1009
|
expectedRevision: number,
|
|
962
|
-
|
|
1010
|
+
credentials: Array<{ ref: string; value: string }>,
|
|
963
1011
|
writeSettings: boolean,
|
|
964
1012
|
): Promise<boolean> {
|
|
965
1013
|
this.set({ ...this.state, action: 'save', error: undefined, message: undefined })
|
|
@@ -973,7 +1021,7 @@ export class VisionSettingsController {
|
|
|
973
1021
|
})
|
|
974
1022
|
}
|
|
975
1023
|
if (snapshot === undefined) throw new Error('Vision Toolkit Settings are unavailable')
|
|
976
|
-
|
|
1024
|
+
for (const credential of credentials) {
|
|
977
1025
|
try {
|
|
978
1026
|
snapshot = await apiRequest<SettingsSnapshot>({
|
|
979
1027
|
method: 'POST',
|
|
@@ -981,8 +1029,8 @@ export class VisionSettingsController {
|
|
|
981
1029
|
body: JSON.stringify({
|
|
982
1030
|
action: 'credential',
|
|
983
1031
|
expectedRevision: snapshot.settings.revision,
|
|
984
|
-
ref:
|
|
985
|
-
value:
|
|
1032
|
+
ref: credential.ref,
|
|
1033
|
+
value: credential.value,
|
|
986
1034
|
}),
|
|
987
1035
|
})
|
|
988
1036
|
} catch (error) {
|
|
@@ -990,6 +1038,7 @@ export class VisionSettingsController {
|
|
|
990
1038
|
status: 'ready',
|
|
991
1039
|
snapshot,
|
|
992
1040
|
health: this.state.health,
|
|
1041
|
+
providerHealth: this.state.providerHealth,
|
|
993
1042
|
update: this.state.update,
|
|
994
1043
|
restart: this.state.restart,
|
|
995
1044
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -1001,6 +1050,7 @@ export class VisionSettingsController {
|
|
|
1001
1050
|
status: 'ready',
|
|
1002
1051
|
snapshot,
|
|
1003
1052
|
health: this.state.health,
|
|
1053
|
+
providerHealth: this.state.providerHealth,
|
|
1004
1054
|
update: this.state.update,
|
|
1005
1055
|
restart: this.state.restart,
|
|
1006
1056
|
message: 'saved',
|
|
@@ -1016,7 +1066,7 @@ export class VisionSettingsController {
|
|
|
1016
1066
|
}
|
|
1017
1067
|
}
|
|
1018
1068
|
|
|
1019
|
-
async runHealth(mode: 'health' | 'connection' | 'model'): Promise<void> {
|
|
1069
|
+
async runHealth(mode: 'health' | 'connection' | 'model', providerIndex?: number): Promise<void> {
|
|
1020
1070
|
const testConnection = mode !== 'health'
|
|
1021
1071
|
const testModel = mode === 'model'
|
|
1022
1072
|
this.set({ ...this.state, action: mode, error: undefined, message: undefined })
|
|
@@ -1024,9 +1074,19 @@ export class VisionSettingsController {
|
|
|
1024
1074
|
const health = await apiRequest<HealthResult>({
|
|
1025
1075
|
method: 'POST',
|
|
1026
1076
|
headers: { 'Content-Type': 'application/json' },
|
|
1027
|
-
body: JSON.stringify({
|
|
1077
|
+
body: JSON.stringify({
|
|
1078
|
+
action: 'health',
|
|
1079
|
+
testConnection,
|
|
1080
|
+
testModel,
|
|
1081
|
+
...(providerIndex === undefined ? {} : { providerIndex }),
|
|
1082
|
+
}),
|
|
1028
1083
|
})
|
|
1029
|
-
|
|
1084
|
+
if (providerIndex === undefined) {
|
|
1085
|
+
this.set({ ...this.state, action: undefined, health })
|
|
1086
|
+
} else {
|
|
1087
|
+
const providerHealth = { ...(this.state.providerHealth ?? {}), [providerIndex]: health }
|
|
1088
|
+
this.set({ ...this.state, action: undefined, providerHealth })
|
|
1089
|
+
}
|
|
1030
1090
|
} catch (error) {
|
|
1031
1091
|
this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) })
|
|
1032
1092
|
}
|
|
@@ -1070,13 +1130,25 @@ export class VisionSettingsController {
|
|
|
1070
1130
|
}
|
|
1071
1131
|
}
|
|
1072
1132
|
|
|
1073
|
-
interface
|
|
1133
|
+
interface ProviderDraft {
|
|
1134
|
+
id: string
|
|
1135
|
+
name: string
|
|
1136
|
+
enabled: boolean
|
|
1074
1137
|
baseUrl: string
|
|
1075
1138
|
credential: string
|
|
1076
1139
|
model: string
|
|
1077
1140
|
protocol: 'openai' | 'anthropic'
|
|
1078
1141
|
anthropicThinking: 'omit' | 'disabled' | 'adaptive'
|
|
1079
1142
|
userAgent: string
|
|
1143
|
+
timeoutMs: string
|
|
1144
|
+
maxImageBytes: string
|
|
1145
|
+
maxImagePixels: string
|
|
1146
|
+
concurrency: string
|
|
1147
|
+
attempts: string
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
interface Draft {
|
|
1151
|
+
providers: ProviderDraft[]
|
|
1080
1152
|
language: 'zh' | 'en'
|
|
1081
1153
|
timeoutMs: string
|
|
1082
1154
|
maxImageBytes: string
|
|
@@ -1092,19 +1164,86 @@ interface Draft {
|
|
|
1092
1164
|
variantAutoSwitch: boolean
|
|
1093
1165
|
}
|
|
1094
1166
|
|
|
1167
|
+
function randomProviderId(): string {
|
|
1168
|
+
const bytes = new Uint8Array(6)
|
|
1169
|
+
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
|
|
1170
|
+
crypto.getRandomValues(bytes)
|
|
1171
|
+
} else {
|
|
1172
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256)
|
|
1173
|
+
}
|
|
1174
|
+
return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('').toUpperCase()
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function autoCredentialName(id: string): string {
|
|
1178
|
+
return `VISION_PROVIDER_${id}`
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
interface ProviderDefaults {
|
|
1182
|
+
timeoutMs: number
|
|
1183
|
+
maxImageBytes: number
|
|
1184
|
+
maxImagePixels: number
|
|
1185
|
+
concurrency: number
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function emptyProviderDraft(defaults: ProviderDefaults): ProviderDraft {
|
|
1189
|
+
const id = randomProviderId()
|
|
1190
|
+
return {
|
|
1191
|
+
id,
|
|
1192
|
+
name: '',
|
|
1193
|
+
enabled: true,
|
|
1194
|
+
baseUrl: '',
|
|
1195
|
+
credential: autoCredentialName(id),
|
|
1196
|
+
model: '',
|
|
1197
|
+
protocol: 'openai',
|
|
1198
|
+
anthropicThinking: 'omit',
|
|
1199
|
+
userAgent: DEFAULT_USER_AGENT,
|
|
1200
|
+
timeoutMs: String(defaults.timeoutMs),
|
|
1201
|
+
maxImageBytes: String(defaults.maxImageBytes),
|
|
1202
|
+
maxImagePixels: String(defaults.maxImagePixels),
|
|
1203
|
+
concurrency: String(defaults.concurrency),
|
|
1204
|
+
attempts: '3',
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function providerDraftOf(value: ProviderValue | undefined, fallback: ProviderValue | undefined, defaults: ProviderDefaults): ProviderDraft {
|
|
1209
|
+
const source = value ?? fallback
|
|
1210
|
+
const id = source?.id ?? ''
|
|
1211
|
+
return {
|
|
1212
|
+
id,
|
|
1213
|
+
name: source?.name ?? '',
|
|
1214
|
+
enabled: source?.enabled !== false,
|
|
1215
|
+
baseUrl: source?.baseUrl ?? '',
|
|
1216
|
+
credential: source?.credential ?? autoCredentialName(id),
|
|
1217
|
+
model: source?.model ?? '',
|
|
1218
|
+
protocol: source?.protocol ?? 'openai',
|
|
1219
|
+
anthropicThinking: source?.anthropicThinking ?? 'omit',
|
|
1220
|
+
userAgent: source?.userAgent ?? DEFAULT_USER_AGENT,
|
|
1221
|
+
timeoutMs: String(source?.timeoutMs ?? defaults.timeoutMs),
|
|
1222
|
+
maxImageBytes: String(source?.maxImageBytes ?? defaults.maxImageBytes),
|
|
1223
|
+
maxImagePixels: String(source?.maxImagePixels ?? defaults.maxImagePixels),
|
|
1224
|
+
concurrency: String(source?.concurrency ?? defaults.concurrency),
|
|
1225
|
+
attempts: String(source?.attempts ?? 3),
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1095
1229
|
function draftOf(value: SettingsValue): Draft {
|
|
1230
|
+
const globalDefaults: ProviderDefaults = {
|
|
1231
|
+
timeoutMs: value.timeoutMs ?? 30000,
|
|
1232
|
+
maxImageBytes: value.maxImageBytes ?? 4194304,
|
|
1233
|
+
maxImagePixels: value.maxImagePixels ?? 20000000,
|
|
1234
|
+
concurrency: value.concurrency ?? 4,
|
|
1235
|
+
}
|
|
1236
|
+
const configured = value.providers ?? []
|
|
1237
|
+
const providers = configured.length > 0
|
|
1238
|
+
? configured.map(entry => providerDraftOf(entry, undefined, globalDefaults))
|
|
1239
|
+
: [providerDraftOf(undefined, value.provider, globalDefaults)]
|
|
1096
1240
|
return {
|
|
1097
|
-
|
|
1098
|
-
credential: value.provider?.credential ?? BUILT_IN_FREE_VISION_CREDENTIAL,
|
|
1099
|
-
model: value.provider?.model ?? BUILT_IN_FREE_VISION_MODEL,
|
|
1100
|
-
protocol: value.provider?.protocol ?? 'openai',
|
|
1101
|
-
anthropicThinking: value.provider?.anthropicThinking ?? 'omit',
|
|
1102
|
-
userAgent: value.provider?.userAgent ?? DEFAULT_USER_AGENT,
|
|
1241
|
+
providers,
|
|
1103
1242
|
language: value.language ?? 'zh',
|
|
1104
|
-
timeoutMs: String(
|
|
1105
|
-
maxImageBytes: String(
|
|
1106
|
-
maxImagePixels: String(
|
|
1107
|
-
concurrency: String(
|
|
1243
|
+
timeoutMs: String(globalDefaults.timeoutMs),
|
|
1244
|
+
maxImageBytes: String(globalDefaults.maxImageBytes),
|
|
1245
|
+
maxImagePixels: String(globalDefaults.maxImagePixels),
|
|
1246
|
+
concurrency: String(globalDefaults.concurrency),
|
|
1108
1247
|
runtimeMode: value.runtime?.mode ?? 'managed',
|
|
1109
1248
|
toolkitPath: value.runtime?.agentVisionToolkitPath ?? '',
|
|
1110
1249
|
python: value.runtime?.python ?? '',
|
|
@@ -1133,15 +1272,33 @@ function apiKeyFailure(value: string, t: Translate): string | undefined {
|
|
|
1133
1272
|
}
|
|
1134
1273
|
|
|
1135
1274
|
function valueOf(draft: Draft, t: Translate): SettingsValue {
|
|
1275
|
+
const providers: ProviderValue[] = draft.providers.map(provider => ({
|
|
1276
|
+
...(provider.id.length === 0 ? {} : { id: provider.id }),
|
|
1277
|
+
...(provider.name.trim().length === 0 ? {} : { name: provider.name.trim() }),
|
|
1278
|
+
...(provider.enabled ? {} : { enabled: false }),
|
|
1279
|
+
...(provider.baseUrl.trim().length === 0 ? {} : { baseUrl: provider.baseUrl.trim() }),
|
|
1280
|
+
...(provider.credential.trim().length === 0 ? {} : { credential: provider.credential.trim() }),
|
|
1281
|
+
...(provider.model.trim().length === 0 ? {} : { model: provider.model.trim() }),
|
|
1282
|
+
protocol: provider.protocol,
|
|
1283
|
+
anthropicThinking: provider.anthropicThinking,
|
|
1284
|
+
...(provider.userAgent.trim() === DEFAULT_USER_AGENT ? {} : { userAgent: provider.userAgent.trim() }),
|
|
1285
|
+
...(provider.timeoutMs.trim().length === 0 ? {} : { timeoutMs: positiveInteger(provider.timeoutMs, t('timeout'), t) }),
|
|
1286
|
+
...(provider.maxImageBytes.trim().length === 0 ? {} : { maxImageBytes: positiveInteger(provider.maxImageBytes, t('maxBytes'), t) }),
|
|
1287
|
+
...(provider.maxImagePixels.trim().length === 0 ? {} : { maxImagePixels: positiveInteger(provider.maxImagePixels, t('maxPixels'), t) }),
|
|
1288
|
+
...(provider.concurrency.trim().length === 0 ? {} : { concurrency: positiveInteger(provider.concurrency, t('concurrency'), t) }),
|
|
1289
|
+
...(provider.attempts.trim().length === 0 ? {} : { attempts: positiveInteger(provider.attempts, t('attempts'), t) }),
|
|
1290
|
+
}))
|
|
1291
|
+
const primary = draft.providers[0]
|
|
1136
1292
|
return {
|
|
1137
1293
|
provider: {
|
|
1138
|
-
baseUrl:
|
|
1139
|
-
credential:
|
|
1140
|
-
model:
|
|
1141
|
-
protocol:
|
|
1142
|
-
anthropicThinking:
|
|
1143
|
-
userAgent:
|
|
1294
|
+
baseUrl: primary?.baseUrl.trim() || BUILT_IN_FREE_VISION_BASE_URL,
|
|
1295
|
+
credential: primary?.credential.trim() || BUILT_IN_FREE_VISION_CREDENTIAL,
|
|
1296
|
+
model: primary?.model.trim() || BUILT_IN_FREE_VISION_MODEL,
|
|
1297
|
+
protocol: primary?.protocol ?? 'openai',
|
|
1298
|
+
anthropicThinking: primary?.anthropicThinking ?? 'omit',
|
|
1299
|
+
userAgent: primary?.userAgent.trim() || DEFAULT_USER_AGENT,
|
|
1144
1300
|
},
|
|
1301
|
+
providers,
|
|
1145
1302
|
language: draft.language,
|
|
1146
1303
|
timeoutMs: positiveInteger(draft.timeoutMs, t('timeout'), t),
|
|
1147
1304
|
maxImageBytes: positiveInteger(draft.maxImageBytes, t('maxBytes'), t),
|
|
@@ -1172,13 +1329,6 @@ function settingsDraftChanged(draft: Draft, saved: SettingsValue, t: Translate):
|
|
|
1172
1329
|
}
|
|
1173
1330
|
}
|
|
1174
1331
|
|
|
1175
|
-
function isBuiltInFreeVisionDraft(draft: Draft): boolean {
|
|
1176
|
-
return draft.baseUrl.trim().replace(/\/+$/, '') === BUILT_IN_FREE_VISION_BASE_URL
|
|
1177
|
-
&& draft.credential.trim() === BUILT_IN_FREE_VISION_CREDENTIAL
|
|
1178
|
-
&& draft.model.trim() === BUILT_IN_FREE_VISION_MODEL
|
|
1179
|
-
&& draft.protocol === 'openai'
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
1332
|
interface SettingsInjected {
|
|
1183
1333
|
controller: VisionSettingsController
|
|
1184
1334
|
t: Translate
|
|
@@ -1261,18 +1411,6 @@ function healthDetail(name: string, detail: string, t: Translate): string {
|
|
|
1261
1411
|
return detail
|
|
1262
1412
|
}
|
|
1263
1413
|
|
|
1264
|
-
function modelTestTag(health: HealthResult, check: HealthCheck): { status: 'ok' | 'warning' | 'error'; label: LocaleKey } {
|
|
1265
|
-
if (!health.modelTested) return { status: 'warning', label: 'modelTestNotRunTag' }
|
|
1266
|
-
if (check.status === 'ok') return { status: 'ok', label: 'modelTestVerifiedTag' }
|
|
1267
|
-
return { status: 'error', label: 'modelTestFailedTag' }
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
function credentialSource(source: string, t: Translate): string {
|
|
1271
|
-
if (source === 'env') return t('sourceEnv')
|
|
1272
|
-
if (source === 'file') return t('sourceFile')
|
|
1273
|
-
return source
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
1414
|
const UPDATE_REASON_KEYS: Record<PluginUpdateUnavailableReason, LocaleKey> = {
|
|
1277
1415
|
'profile-not-found': 'updateReasonProfileNotFound',
|
|
1278
1416
|
'not-direct-dependency': 'updateReasonNotDependency',
|
|
@@ -1292,7 +1430,8 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1292
1430
|
const state = useSyncExternalStore(controller.subscribe, controller.snapshot, controller.snapshot)
|
|
1293
1431
|
const snapshot = state.snapshot
|
|
1294
1432
|
const [draft, setDraft] = useState<Draft | undefined>(undefined)
|
|
1295
|
-
const [
|
|
1433
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
1434
|
+
const [keys, setKeys] = useState<Record<string, string>>({})
|
|
1296
1435
|
const [draftError, setDraftError] = useState<string | undefined>(undefined)
|
|
1297
1436
|
const [copiedCommand, setCopiedCommand] = useState(false)
|
|
1298
1437
|
|
|
@@ -1338,43 +1477,106 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1338
1477
|
}
|
|
1339
1478
|
|
|
1340
1479
|
const update = <K extends keyof Draft>(key: K, value: Draft[K]): void => setDraft(current => current === undefined ? current : { ...current, [key]: value })
|
|
1480
|
+
const updateProvider = <K extends keyof ProviderDraft>(index: number, key: K, value: ProviderDraft[K]): void => {
|
|
1481
|
+
setDraft(current => {
|
|
1482
|
+
if (current === undefined) return current
|
|
1483
|
+
const providers = current.providers.map((provider, i) => i === index ? { ...provider, [key]: value } : provider)
|
|
1484
|
+
return { ...current, providers }
|
|
1485
|
+
})
|
|
1486
|
+
}
|
|
1487
|
+
const addProvider = (): void => {
|
|
1488
|
+
setDraft(current => current === undefined ? current : {
|
|
1489
|
+
...current,
|
|
1490
|
+
providers: [...current.providers, emptyProviderDraft({
|
|
1491
|
+
timeoutMs: Number(current.timeoutMs) || 30000,
|
|
1492
|
+
maxImageBytes: Number(current.maxImageBytes) || 4194304,
|
|
1493
|
+
maxImagePixels: Number(current.maxImagePixels) || 20000000,
|
|
1494
|
+
concurrency: Number(current.concurrency) || 4,
|
|
1495
|
+
})],
|
|
1496
|
+
})
|
|
1497
|
+
setSelectedIndex(draft.providers.length)
|
|
1498
|
+
}
|
|
1499
|
+
const removeProvider = (index: number): void => {
|
|
1500
|
+
const removed = draft.providers[index]
|
|
1501
|
+
if (removed !== undefined) {
|
|
1502
|
+
// Deleting a provider also deletes its stored credential.
|
|
1503
|
+
void apiRequest<SettingsSnapshot>({
|
|
1504
|
+
method: 'POST',
|
|
1505
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1506
|
+
body: JSON.stringify({ action: 'delete-credential', ref: removed.credential }),
|
|
1507
|
+
}).catch(() => {})
|
|
1508
|
+
}
|
|
1509
|
+
setDraft(current => current === undefined ? current : { ...current, providers: current.providers.filter((_, i) => i !== index) })
|
|
1510
|
+
setSelectedIndex(prev => {
|
|
1511
|
+
if (index < prev) return prev - 1
|
|
1512
|
+
if (prev >= draft.providers.length - 1) return Math.max(0, draft.providers.length - 2)
|
|
1513
|
+
return prev
|
|
1514
|
+
})
|
|
1515
|
+
}
|
|
1516
|
+
const moveProvider = (index: number, delta: number): void => {
|
|
1517
|
+
setDraft(current => {
|
|
1518
|
+
if (current === undefined) return current
|
|
1519
|
+
const target = index + delta
|
|
1520
|
+
if (target < 0 || target >= current.providers.length) return current
|
|
1521
|
+
const providers = [...current.providers]
|
|
1522
|
+
const [entry] = providers.splice(index, 1)
|
|
1523
|
+
if (entry === undefined) return current
|
|
1524
|
+
providers.splice(target, 0, entry)
|
|
1525
|
+
return { ...current, providers }
|
|
1526
|
+
})
|
|
1527
|
+
setSelectedIndex(prev => {
|
|
1528
|
+
const target = index + delta
|
|
1529
|
+
if (prev === index) return target
|
|
1530
|
+
if (target < index && prev >= target && prev < index) return prev + 1
|
|
1531
|
+
if (target > index && prev > index && prev <= target) return prev - 1
|
|
1532
|
+
return prev
|
|
1533
|
+
})
|
|
1534
|
+
}
|
|
1341
1535
|
const save = (): void => {
|
|
1342
1536
|
try {
|
|
1343
|
-
const
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1537
|
+
for (const value of Object.values(keys)) {
|
|
1538
|
+
const failure = apiKeyFailure(value, t)
|
|
1539
|
+
if (failure !== undefined) {
|
|
1540
|
+
setDraftError(failure)
|
|
1541
|
+
return
|
|
1542
|
+
}
|
|
1347
1543
|
}
|
|
1348
|
-
const
|
|
1544
|
+
const credentials = Object.entries(keys)
|
|
1545
|
+
.filter(([, value]) => value.trim().length > 0)
|
|
1546
|
+
.map(([ref, value]) => ({ ref, value: value.trim() }))
|
|
1547
|
+
const savedRefs = new Set(credentials.map(credential => credential.ref))
|
|
1349
1548
|
setDraftError(undefined)
|
|
1350
1549
|
void controller.save(
|
|
1351
1550
|
valueOf(draft, t),
|
|
1352
1551
|
snapshot.settings.revision,
|
|
1353
|
-
|
|
1552
|
+
credentials,
|
|
1354
1553
|
snapshot.writable,
|
|
1355
|
-
).then(saved => {
|
|
1554
|
+
).then(saved => {
|
|
1555
|
+
if (saved) {
|
|
1556
|
+
setKeys(current => Object.fromEntries(Object.entries(current).filter(([ref]) => !savedRefs.has(ref))))
|
|
1557
|
+
}
|
|
1558
|
+
})
|
|
1356
1559
|
} catch (error) {
|
|
1357
1560
|
setDraftError(error instanceof Error ? error.message : String(error))
|
|
1358
1561
|
}
|
|
1359
1562
|
}
|
|
1360
1563
|
const busy = state.action !== undefined
|
|
1361
|
-
const
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
const canSave = snapshot.writable || (
|
|
1564
|
+
const selectedProvider = draft.providers[selectedIndex]
|
|
1565
|
+
const providerCredentials = snapshot.credentials ?? []
|
|
1566
|
+
const selectedCredential = providerCredentials[selectedIndex]
|
|
1567
|
+
const selectedKey = selectedProvider === undefined ? '' : (keys[selectedProvider.credential] ?? '')
|
|
1568
|
+
const selectedKeyLocked = selectedCredential !== undefined && !selectedCredential.writable
|
|
1569
|
+
const selectedBuiltInFree = selectedCredential?.source === 'built-in-free'
|
|
1570
|
+
const canSave = snapshot.writable || Object.values(keys).some(value => value.trim().length > 0)
|
|
1368
1571
|
const runtimeErrorTitle = snapshot.runtime.ready ? t('runtimeCandidateRejected') : t('runtimeUnavailable')
|
|
1369
1572
|
const pluginUpdate = state.update
|
|
1370
1573
|
const updateCapability = pluginUpdate ?? snapshot.release.update
|
|
1371
1574
|
const latestVersion = pluginUpdate?.latestVersion
|
|
1372
1575
|
const updateReason = updateCapability.reason === undefined ? undefined : t(UPDATE_REASON_KEYS[updateCapability.reason])
|
|
1373
1576
|
const updateCheckSupported = updateCapability.checkSupported ?? updateCapability.supported
|
|
1374
|
-
const updateHasUnsavedChanges =
|
|
1577
|
+
const updateHasUnsavedChanges = Object.values(keys).some(value => value.trim().length > 0) || settingsDraftChanged(draft, snapshot.settings.value, t)
|
|
1375
1578
|
const manualUpdateProfile = updateCapability.profile ?? 'web'
|
|
1376
1579
|
const manualUpdateCommand = `dsh plugin --profile ${manualUpdateProfile} add @mengruo/dsh-vision-toolkit@latest --registry=https://registry.npmjs.org/`
|
|
1377
|
-
const aihubmixTutorialUrl = draft?.language === 'en' ? AIHUBMIX_TUTORIAL_URL_EN : AIHUBMIX_TUTORIAL_URL_ZH
|
|
1378
1580
|
const copyManualUpdate = (): void => {
|
|
1379
1581
|
void navigator.clipboard?.writeText(manualUpdateCommand)
|
|
1380
1582
|
.then(() => {
|
|
@@ -1388,6 +1590,7 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1388
1590
|
if (!window.confirm(t('updateConfirm', { version: latestVersion }))) return
|
|
1389
1591
|
void controller.applyUpdate(latestVersion)
|
|
1390
1592
|
}
|
|
1593
|
+
const providerHealth = state.providerHealth?.[selectedIndex]
|
|
1391
1594
|
|
|
1392
1595
|
return (
|
|
1393
1596
|
<div className="dvt-settings">
|
|
@@ -1400,27 +1603,76 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1400
1603
|
{state.message === 'manual-restart-required' && state.restart !== undefined ? <div className="dvt-alert success">{t('manualRestartRequired', { version: state.restart.toVersion })}</div> : null}
|
|
1401
1604
|
{snapshot.runtime.lastError === undefined ? null : <div className="dvt-alert error"><strong>{runtimeErrorTitle}</strong><span>{snapshot.runtime.lastError}</span></div>}
|
|
1402
1605
|
|
|
1403
|
-
<section className="dvt-panel dvt-essential"><div className="dvt-panel-title"><div><h3>{t('
|
|
1404
|
-
<
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1606
|
+
<section className="dvt-panel dvt-essential"><div className="dvt-panel-title"><div><h3>{t('providers')}</h3><p>{t('providerHint')}</p></div></div>
|
|
1607
|
+
<div className="dvt-provider-select">
|
|
1608
|
+
<select aria-label={t('providers')} disabled={!snapshot.writable || busy} value={selectedIndex} onChange={(event) => { setSelectedIndex(Number(event.target.value)) }}>
|
|
1609
|
+
{draft.providers.map((provider, index) => (
|
|
1610
|
+
<option key={index} value={index}>{index + 1}. {provider.name.trim() || provider.model.trim() || t('provider')}</option>
|
|
1611
|
+
))}
|
|
1612
|
+
</select>
|
|
1613
|
+
<Button size="sm" variant="outline" disabled={!snapshot.writable || busy || draft.providers.length >= 32} onClick={addProvider}>{t('addProvider')}</Button>
|
|
1614
|
+
<Button size="sm" variant="outline" disabled={!snapshot.writable || busy || draft.providers.length <= 1} onClick={() => { removeProvider(selectedIndex) }}>{t('removeProvider')}</Button>
|
|
1410
1615
|
</div>
|
|
1616
|
+
{selectedProvider === undefined ? null : (
|
|
1617
|
+
<div className="dvt-provider-card">
|
|
1618
|
+
<div className="dvt-provider-head">
|
|
1619
|
+
<label className="dvt-check"><input type="checkbox" checked={selectedProvider.enabled} disabled={!snapshot.writable || busy} onChange={(event) => { updateProvider(selectedIndex, 'enabled', event.target.checked) }} /><span>{t('enabled')}</span></label>
|
|
1620
|
+
<span className="dvt-provider-priority">{t('priority', { index: selectedIndex + 1 })}</span>
|
|
1621
|
+
</div>
|
|
1622
|
+
<div className="dvt-form-grid">
|
|
1623
|
+
<Field label={t('name')}><Input aria-label={t('name')} value={selectedProvider.name} onChange={(event) => { updateProvider(selectedIndex, 'name', event.target.value) }} placeholder={selectedProvider.model || t('model')} /></Field>
|
|
1624
|
+
<Field label={t('model')}><Input aria-label={t('model')} disabled={!snapshot.writable || busy} value={selectedProvider.model} onChange={(event) => { updateProvider(selectedIndex, 'model', event.target.value) }} /></Field>
|
|
1625
|
+
<Field label={t('baseUrl')}><Input aria-label={t('baseUrl')} disabled={!snapshot.writable || busy} value={selectedProvider.baseUrl} onChange={(event) => { updateProvider(selectedIndex, 'baseUrl', event.target.value) }} /></Field>
|
|
1626
|
+
<Field label={t('protocol')}><select aria-label={t('protocol')} disabled={!snapshot.writable || busy} value={selectedProvider.protocol} onChange={(event) => { updateProvider(selectedIndex, 'protocol', event.target.value as 'openai' | 'anthropic') }}><option value="openai">OpenAI Chat Completions</option><option value="anthropic">Anthropic Messages</option></select></Field>
|
|
1627
|
+
{selectedProvider.protocol === 'anthropic' ? <Field label={t('anthropicThinking')} hint={t('anthropicThinkingHint')}><select aria-label={t('anthropicThinking')} value={selectedProvider.anthropicThinking} onChange={(event) => { updateProvider(selectedIndex, 'anthropicThinking', event.target.value as 'omit' | 'disabled' | 'adaptive') }}><option value="omit">omit (widest compatibility)</option><option value="disabled">disabled (model support required)</option><option value="adaptive">adaptive (model support required)</option></select></Field> : null}
|
|
1628
|
+
<Field label={t('userAgent')}><Input aria-label={t('userAgent')} value={selectedProvider.userAgent} onChange={(event) => { updateProvider(selectedIndex, 'userAgent', event.target.value) }} /></Field>
|
|
1629
|
+
<Field label={t('timeout')}><Input aria-label={t('timeout')} inputMode="numeric" value={selectedProvider.timeoutMs} onChange={(event) => { updateProvider(selectedIndex, 'timeoutMs', event.target.value) }} /></Field>
|
|
1630
|
+
<Field label={t('maxBytes')}><Input aria-label={t('maxBytes')} inputMode="numeric" value={selectedProvider.maxImageBytes} onChange={(event) => { updateProvider(selectedIndex, 'maxImageBytes', event.target.value) }} /></Field>
|
|
1631
|
+
<Field label={t('maxPixels')}><Input aria-label={t('maxPixels')} inputMode="numeric" value={selectedProvider.maxImagePixels} onChange={(event) => { updateProvider(selectedIndex, 'maxImagePixels', event.target.value) }} /></Field>
|
|
1632
|
+
<Field label={t('concurrency')}><Input aria-label={t('concurrency')} inputMode="numeric" value={selectedProvider.concurrency} onChange={(event) => { updateProvider(selectedIndex, 'concurrency', event.target.value) }} /></Field>
|
|
1633
|
+
<Field label={t('attempts')} hint={t('attemptsHint')}><Input aria-label={t('attempts')} inputMode="numeric" value={selectedProvider.attempts} onChange={(event) => { updateProvider(selectedIndex, 'attempts', event.target.value) }} /></Field>
|
|
1634
|
+
</div>
|
|
1635
|
+
<Field label={t('apiKey')} hint={selectedBuiltInFree ? t('apiKeyBuiltInFree') : selectedKeyLocked ? t('apiKeyLocked') : t('apiKeyHint')}><Input aria-label={t('apiKey')} type="password" autoComplete="new-password" disabled={busy || selectedKeyLocked || selectedBuiltInFree} placeholder={selectedCredential?.configured ? t('apiKeyPlaceholderConfigured') : t('apiKeyPlaceholderMissing')} value={selectedKey} onChange={(event) => { setKeys(current => ({ ...current, [selectedProvider.credential]: event.target.value })); setDraftError(undefined) }} /></Field>
|
|
1636
|
+
<div className="dvt-provider-tests">
|
|
1637
|
+
<Button size="sm" variant="outline" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runHealth('connection', selectedIndex) }}>{state.action === 'connection' ? t('testing') : t('testConnection')}</Button>
|
|
1638
|
+
<Button size="sm" variant="primary" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runHealth('model', selectedIndex) }}>{state.action === 'model' ? t('testingModel') : t('testModel')}</Button>
|
|
1639
|
+
</div>
|
|
1640
|
+
{providerHealth === undefined ? null : <div className="dvt-provider-test-result">{(['service', 'model'] as const).map(name => {
|
|
1641
|
+
const check = providerHealth.checks[name]
|
|
1642
|
+
if (check === undefined) return null
|
|
1643
|
+
return <div key={name} data-status={check.status}><span>{t(HEALTH_NAME_KEYS[name] ?? 'health')}</span><strong>{t(HEALTH_STATUS_KEYS[check.status])}</strong><p>{healthDetail(name, check.detail, t)}</p></div>
|
|
1644
|
+
})}</div>}
|
|
1645
|
+
</div>
|
|
1646
|
+
)}
|
|
1411
1647
|
</section>
|
|
1412
1648
|
|
|
1413
1649
|
<div className="dvt-save-row"><Button variant="primary" disabled={!canSave || busy} onClick={save}>{state.action === 'save' ? t('saving') : t('save')}</Button><Button variant="outline" disabled={busy} onClick={() => { void controller.load() }}>{t('reload')}</Button></div>
|
|
1414
1650
|
|
|
1415
|
-
<section className="dvt-panel"><div className="dvt-panel-title"><div><h3>{t('
|
|
1416
|
-
<
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1651
|
+
<section className="dvt-panel"><div className="dvt-panel-title"><div><h3>{t('prioritySort')}</h3><p>{t('prioritySortHint')}</p></div></div>
|
|
1652
|
+
<div className="dvt-sorter">
|
|
1653
|
+
{draft.providers.map((provider, index) => (
|
|
1654
|
+
<div key={index} className="dvt-sorter-row">
|
|
1655
|
+
<span className="dvt-sorter-index">{index + 1}</span>
|
|
1656
|
+
<span className="dvt-sorter-name">{provider.name.trim() || provider.model.trim() || t('provider')}</span>
|
|
1657
|
+
<div className="dvt-provider-actions">
|
|
1658
|
+
<button type="button" aria-label={t('moveUp')} title={t('moveUp')} disabled={!snapshot.writable || busy || index === 0} onClick={() => { moveProvider(index, -1) }}>↑</button>
|
|
1659
|
+
<button type="button" aria-label={t('moveDown')} title={t('moveDown')} disabled={!snapshot.writable || busy || index === draft.providers.length - 1} onClick={() => { moveProvider(index, 1) }}>↓</button>
|
|
1660
|
+
</div>
|
|
1661
|
+
</div>
|
|
1662
|
+
))}
|
|
1663
|
+
</div>
|
|
1421
1664
|
</section>
|
|
1422
1665
|
|
|
1423
1666
|
<section className="dvt-panel dvt-update-panel">
|
|
1667
|
+
<div className="dvt-panel-title">
|
|
1668
|
+
<div><h3>{t('runtimeCheck')}</h3><p>{t('runtimeCheckHint')}</p></div>
|
|
1669
|
+
<div className="dvt-actions"><Button size="sm" variant="outline" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runHealth('health') }}>{state.action === 'health' ? t('testing') : t('runHealth')}</Button></div>
|
|
1670
|
+
</div>
|
|
1671
|
+
{state.health === undefined ? <p className="dvt-muted">{t('notTested')}</p> : <div className="dvt-health-grid">{(['python', 'dependencies', 'chrome'] as const).map(name => {
|
|
1672
|
+
const check = state.health?.checks[name]
|
|
1673
|
+
if (check === undefined) return null
|
|
1674
|
+
return <div key={name} data-status={check.status}><span>{t(HEALTH_NAME_KEYS[name] ?? 'health')}</span><strong>{t(HEALTH_STATUS_KEYS[check.status])}</strong><p>{healthDetail(name, check.detail, t)}</p></div>
|
|
1675
|
+
})}</div>}
|
|
1424
1676
|
<div className="dvt-panel-title">
|
|
1425
1677
|
<div><h3>{t('updates')}</h3><p>{t('updatesHint')}</p></div>
|
|
1426
1678
|
<span className={`dvt-badge ${pluginUpdate?.updateAvailable ? 'warning' : pluginUpdate !== undefined && pluginUpdate.supported ? 'ok' : ''}`}>
|
|
@@ -1447,18 +1699,8 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1447
1699
|
<details className="dvt-advanced">
|
|
1448
1700
|
<summary><span><strong>{t('advanced')}</strong><small>{t('advancedHint')}</small></span><span className="dvt-details-chevron" aria-hidden="true">⌄</span></summary>
|
|
1449
1701
|
<div className="dvt-advanced-body">
|
|
1450
|
-
<section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('
|
|
1451
|
-
<Field label={t('credential')} hint={t('credentialHint')}><Input aria-label={t('credential')} disabled={!snapshot.writable || busy} value={draft.credential} onChange={(event) => { update('credential', event.target.value) }} /></Field>
|
|
1452
|
-
{draft.protocol === 'anthropic' ? <Field label={t('anthropicThinking')} hint={t('anthropicThinkingHint')}><select aria-label={t('anthropicThinking')} value={draft.anthropicThinking} onChange={(event) => { update('anthropicThinking', event.target.value as 'omit' | 'disabled' | 'adaptive') }}><option value="omit">omit (widest compatibility)</option><option value="disabled">disabled (model support required)</option><option value="adaptive">adaptive (model support required)</option></select></Field> : null}
|
|
1453
|
-
<Field label={t('userAgent')}><Input value={draft.userAgent} onChange={(event) => { update('userAgent', event.target.value) }} /></Field>
|
|
1454
|
-
</div></section>
|
|
1455
|
-
|
|
1456
|
-
<section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('limits')}</h3></div><div className="dvt-form-grid">
|
|
1702
|
+
<section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('language')}</h3></div><div className="dvt-form-grid">
|
|
1457
1703
|
<Field label={t('language')}><select value={draft.language} onChange={(event) => { update('language', event.target.value as 'zh' | 'en') }}><option value="zh">中文</option><option value="en">English</option></select></Field>
|
|
1458
|
-
<Field label={t('timeout')}><Input inputMode="numeric" value={draft.timeoutMs} onChange={(event) => { update('timeoutMs', event.target.value) }} /></Field>
|
|
1459
|
-
<Field label={t('maxBytes')}><Input inputMode="numeric" value={draft.maxImageBytes} onChange={(event) => { update('maxImageBytes', event.target.value) }} /></Field>
|
|
1460
|
-
<Field label={t('maxPixels')}><Input inputMode="numeric" value={draft.maxImagePixels} onChange={(event) => { update('maxImagePixels', event.target.value) }} /></Field>
|
|
1461
|
-
<Field label={t('concurrency')}><Input inputMode="numeric" value={draft.concurrency} onChange={(event) => { update('concurrency', event.target.value) }} /></Field>
|
|
1462
1704
|
</div></section>
|
|
1463
1705
|
|
|
1464
1706
|
<section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('imageInput')}</h3></div><label className="dvt-check"><input type="checkbox" checked={draft.hiddenVariants} disabled={!snapshot.writable || busy} onChange={(event) => { update('hiddenVariants', event.target.checked) }} /><span>{t('hiddenVariantsLabel')}</span><small>{t('hiddenVariantsHint')}</small></label></section>
|
|
@@ -1492,6 +1734,7 @@ const CSS = `
|
|
|
1492
1734
|
.dvt-settings-footer{margin-top:8px;padding:20px 2px 4px;border-top:1px solid var(--dsw-alias-border-l1);opacity:.82}.dvt-settings-footer h2{font-size:18px;letter-spacing:-.015em;margin:3px 0 5px}.dvt-settings-footer p{font-size:11px;line-height:1.5}.dvt-release{min-width:220px}.dvt-release span{white-space:nowrap}.dvt-essential{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 30%,var(--dsw-alias-border-l1));box-shadow:var(--dsw-shadow-lv1),0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 5%,transparent)}.dvt-advanced{border:1px solid var(--dsw-alias-border-l1);border-radius:14px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dvt-advanced>summary{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:14px 15px;cursor:pointer;list-style:none}.dvt-advanced>summary::-webkit-details-marker{display:none}.dvt-advanced>summary>span:first-child{display:grid;gap:3px}.dvt-advanced>summary strong{font-size:13px}.dvt-advanced>summary small{font-size:10px;line-height:1.45;color:var(--dsw-alias-label-secondary);font-weight:400}.dvt-details-chevron{font-size:15px;opacity:.55;transition:transform .16s ease}.dvt-advanced[open] .dvt-details-chevron{transform:rotate(180deg)}.dvt-advanced-body{display:grid;grid-template-columns:minmax(0,1fr);gap:12px;padding:0 12px 12px}.dvt-advanced-body>.dvt-panel{box-shadow:none}
|
|
1493
1735
|
.dvt-health-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px}.dvt-health-grid>div{padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);border-left:3px solid var(--dsw-alias-border-l4)}.dvt-health-grid>div[data-status=ok]{border-left-color:var(--dsw-alias-state-success-primary)}.dvt-health-grid>div[data-status=warning],.dvt-health-grid>div[data-status=not_tested]{border-left-color:var(--dsw-alias-state-warn-primary)}.dvt-health-grid>div[data-status=error]{border-left-color:var(--dsw-alias-state-error-primary)}.dvt-health-grid span{font-size:10px;text-transform:capitalize}.dvt-health-grid strong{float:right;font-size:9px;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}.dvt-health-test-tag{display:inline-flex;margin-left:6px;padding:1px 6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);font-size:9px;font-style:normal;font-weight:600;color:var(--dsw-alias-label-secondary)}.dvt-health-test-tag[data-status=ok]{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);color:var(--dsw-alias-state-success-primary)}.dvt-health-test-tag[data-status=warning]{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent);color:var(--dsw-alias-state-warn-label)}.dvt-health-test-tag[data-status=error]{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent);color:var(--dsw-alias-state-error-primary)}.dvt-health-grid p{clear:both;margin:5px 0 0;font-size:10px;line-height:1.4;color:var(--dsw-alias-label-secondary)}.dvt-loading{padding:24px;border-radius:12px;background:var(--dsw-alias-bg-layer-2);font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
1494
1736
|
.dvt-paste-dock{box-sizing:border-box;width:calc(100% - 32px);max-width:var(--dsh-composer-card-max-width,960px);margin:0 auto;display:flex;flex-wrap:wrap;gap:6px;padding:0 2px 6px}.dvt-paste-chip{max-width:100%;height:32px;box-sizing:border-box;display:flex;align-items:center;gap:7px;padding:0 6px 0 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-specific-tip);font-size:12px}.dvt-paste-chip[data-status=copying]{border-color:var(--dsw-alias-state-business-primary)}.dvt-paste-chip[data-status=error]{border-color:var(--dsw-alias-state-error-primary)}.dvt-paste-name{max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvt-paste-detail{color:var(--dsw-alias-label-caption);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvt-paste-chip[data-status=error] .dvt-paste-detail{color:var(--dsw-alias-state-error-primary)}.dvt-paste-chip button{width:20px;height:20px;display:grid;place-items:center;border:0;border-radius:50%;padding:0;background:transparent;color:var(--dsw-alias-label-caption);font:inherit;font-size:16px;cursor:pointer}.dvt-paste-chip button:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dvt-paste-chip button:disabled{opacity:.4;cursor:default}
|
|
1737
|
+
.dvt-provider-card{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:9px 10px;display:grid;gap:8px;background:var(--dsw-alias-bg-layer-2)}.dvt-provider-head{display:flex;align-items:center;gap:10px}.dvt-provider-priority{margin-left:auto;font-size:11px;color:var(--dsw-alias-label-secondary);white-space:nowrap}.dvt-provider-actions{display:flex;gap:4px}.dvt-provider-actions button{width:26px;height:26px;display:grid;place-items:center;border:1px solid var(--dsw-alias-border-l1);border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);cursor:pointer;font-size:14px;line-height:1}.dvt-provider-actions button:hover{background:var(--dsw-alias-interactive-bg-hover)}.dvt-provider-actions button:disabled{opacity:.35;cursor:default}.dvt-provider-select{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.dvt-provider-select select{flex:1;min-width:180px;height:36px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;padding:0 10px}.dvt-provider-tests{display:flex;align-items:center;gap:7px;flex-wrap:wrap}.dvt-provider-test-result{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px}.dvt-provider-test-result>div{padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);border-left:3px solid var(--dsw-alias-border-l4)}.dvt-provider-test-result>div[data-status=ok]{border-left-color:var(--dsw-alias-state-success-primary)}.dvt-provider-test-result>div[data-status=warning],.dvt-provider-test-result>div[data-status=not_tested]{border-left-color:var(--dsw-alias-state-warn-primary)}.dvt-provider-test-result>div[data-status=error]{border-left-color:var(--dsw-alias-state-error-primary)}.dvt-provider-test-result span{font-size:10px;text-transform:capitalize}.dvt-provider-test-result strong{float:right;font-size:9px;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}.dvt-provider-test-result p{clear:both;margin:5px 0 0;font-size:10px;line-height:1.4;color:var(--dsw-alias-label-secondary)}.dvt-sorter{display:grid;gap:6px}.dvt-sorter-row{display:flex;align-items:center;gap:10px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}.dvt-sorter-index{flex:none;width:24px;height:24px;display:grid;place-items:center;border-radius:6px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);color:var(--dsw-alias-state-business-primary);font-size:11px;font-weight:700}.dvt-sorter-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}
|
|
1495
1738
|
@media(max-width:720px){.dvt-settings-footer{display:grid}.dvt-release{width:auto}.dvt-form-grid,.dvt-update-grid{grid-template-columns:1fr}.dvt-metrics{grid-template-columns:1fr}.dvt-artifact-meta{align-items:flex-start;flex-direction:column}.dvt-panel-title{flex-direction:column}}
|
|
1496
1739
|
`
|
|
1497
1740
|
|