@mengruo/dsh-vision-toolkit 0.1.3 → 0.1.5
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 +15 -70
- package/README.zh.md +15 -69
- package/docs/requirements-traceability/README.i18n.yaml +2 -2
- package/docs/requirements-traceability/README.md +1 -1
- package/docs/requirements-traceability/README.zh.md +1 -1
- package/lib/artifact-access.js +20 -2
- package/lib/artifact-access.js.map +1 -1
- package/lib/client.js +128 -12
- package/lib/client.js.map +1 -1
- package/lib/config.js +69 -0
- package/lib/config.js.map +1 -1
- package/lib/errors.js +25 -2
- package/lib/errors.js.map +1 -1
- package/lib/evidence-cache.js +2 -1
- package/lib/evidence-cache.js.map +1 -1
- package/lib/exposure.js +14 -1
- package/lib/exposure.js.map +1 -1
- package/lib/image-input-variants.js +22 -12
- package/lib/image-input-variants.js.map +1 -1
- package/lib/index.js +53 -6
- package/lib/index.js.map +1 -1
- package/lib/object-storage.js +141 -0
- package/lib/object-storage.js.map +1 -0
- package/lib/paste-images.js +67 -19
- package/lib/paste-images.js.map +1 -1
- package/lib/paths.js +214 -28
- package/lib/paths.js.map +1 -1
- package/lib/runtime-manager.js +76 -10
- package/lib/runtime-manager.js.map +1 -1
- package/lib/runtime.js +219 -38
- package/lib/runtime.js.map +1 -1
- package/lib/storage-history.js +154 -0
- package/lib/storage-history.js.map +1 -0
- package/lib/types/artifact-access.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +45 -2
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/paste-images.d.ts +2 -0
- package/lib/types/client/paste-images.d.ts.map +1 -1
- package/lib/types/config.d.ts +55 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/errors.d.ts +18 -2
- package/lib/types/errors.d.ts.map +1 -1
- package/lib/types/evidence-cache.d.ts +1 -1
- package/lib/types/evidence-cache.d.ts.map +1 -1
- package/lib/types/exposure.d.ts.map +1 -1
- package/lib/types/image-input-variants.d.ts +5 -3
- package/lib/types/image-input-variants.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/object-storage.d.ts +54 -0
- package/lib/types/object-storage.d.ts.map +1 -0
- package/lib/types/paste-images.d.ts +12 -4
- package/lib/types/paste-images.d.ts.map +1 -1
- package/lib/types/paths.d.ts +31 -5
- package/lib/types/paths.d.ts.map +1 -1
- package/lib/types/runtime-manager.d.ts +28 -4
- package/lib/types/runtime-manager.d.ts.map +1 -1
- package/lib/types/runtime.d.ts +31 -1
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/storage-history.d.ts +63 -0
- package/lib/types/storage-history.d.ts.map +1 -0
- package/lib/types/upstream.d.ts +1 -0
- package/lib/types/upstream.d.ts.map +1 -1
- package/lib/types/web.d.ts +7 -0
- package/lib/types/web.d.ts.map +1 -1
- package/lib/upstream.js +34 -8
- package/lib/upstream.js.map +1 -1
- package/lib/web.js +44 -9
- package/lib/web.js.map +1 -1
- package/package.json +3 -1
- package/src/artifact-access.ts +22 -2
- package/src/client/index.tsx +169 -9
- package/src/client/paste-images.tsx +14 -4
- package/src/config.ts +128 -0
- package/src/errors.ts +25 -2
- package/src/evidence-cache.ts +2 -1
- package/src/exposure.ts +16 -2
- package/src/image-input-variants.ts +21 -6
- package/src/index.ts +65 -6
- package/src/object-storage.ts +174 -0
- package/src/paste-images.ts +81 -19
- package/src/paths.ts +249 -28
- package/src/runtime-manager.ts +93 -10
- package/src/runtime.ts +215 -36
- package/src/storage-history.ts +172 -0
- package/src/upstream.ts +36 -7
- package/src/web.ts +54 -9
- package/vendor/agent-vision-toolkit/UPSTREAM_MANIFEST.json +11 -11
- package/vendor/agent-vision-toolkit/__pycache__/detect.cpython-314.pyc +0 -0
- package/vendor/agent-vision-toolkit/__pycache__/ground.cpython-314.pyc +0 -0
- package/vendor/agent-vision-toolkit/__pycache__/vision_client.cpython-314.pyc +0 -0
- package/vendor/agent-vision-toolkit/bin/__pycache__/glancecpython-314.pyc +0 -0
- package/vendor/agent-vision-toolkit/bin/glance +8 -1
- package/vendor/agent-vision-toolkit/detect.py +13 -7
- package/vendor/agent-vision-toolkit/ground.py +43 -18
- package/vendor/agent-vision-toolkit/tests/test_vision_client.py +88 -0
- package/vendor/agent-vision-toolkit/vision_client.py +84 -6
- package/assets/community-group-qr.png +0 -0
- package/assets/logo_aihubmix.png +0 -0
- package/assets/logo_eapi_dark.png +0 -0
- package/assets/wechat-reward.png +0 -0
package/src/client/index.tsx
CHANGED
|
@@ -31,6 +31,8 @@ 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
|
+
// Fixed credential reference holding the combined "accessKeyId:secretAccessKey".
|
|
35
|
+
const OBJECT_STORAGE_CREDENTIAL = 'VISION_OBJECT_STORAGE'
|
|
34
36
|
|
|
35
37
|
const en = {
|
|
36
38
|
nav: 'Vision',
|
|
@@ -42,6 +44,7 @@ const en = {
|
|
|
42
44
|
providers: 'Vision providers',
|
|
43
45
|
name: 'Label',
|
|
44
46
|
enabled: 'Enabled',
|
|
47
|
+
disabled: 'Disabled',
|
|
45
48
|
priority: 'Priority {index}',
|
|
46
49
|
attempts: 'Attempts',
|
|
47
50
|
attemptsHint: 'Total attempts against this provider before failing over to the next one.',
|
|
@@ -70,6 +73,10 @@ const en = {
|
|
|
70
73
|
anthropicThinking: 'Anthropic thinking',
|
|
71
74
|
anthropicThinkingHint: 'omit has the broadest compatibility. Use disabled or adaptive only when the selected model documents that mode; restore omit first after HTTP 400.',
|
|
72
75
|
userAgent: 'User-Agent',
|
|
76
|
+
stream: 'Streaming',
|
|
77
|
+
streamHint: 'Request a streamed (SSE) completion instead of one JSON response. Use it for endpoints with weak non-streaming support or that time out on long outputs.',
|
|
78
|
+
uploadViaUrl: 'Transfer images via URL',
|
|
79
|
+
uploadViaUrlHint: 'Upload each image to the configured object storage and send the model a URL instead of base64. Requires object storage (below); region crops fall back to base64.',
|
|
73
80
|
language: 'Output language',
|
|
74
81
|
limits: 'Timeout and concurrency',
|
|
75
82
|
timeout: 'Request timeout (ms)',
|
|
@@ -90,12 +97,29 @@ const en = {
|
|
|
90
97
|
runtimeMode: 'Runtime mode',
|
|
91
98
|
toolkitPath: 'Pinned checkout path',
|
|
92
99
|
python: 'Python override',
|
|
100
|
+
storage: 'Local files',
|
|
101
|
+
storageDir: 'Default save directory',
|
|
102
|
+
storageDirHint: 'Leave blank to keep .dsh-vision-toolkit in each workspace. On POSIX systems, set an absolute shared root such as /tmp/dsh-vision-toolkit to store artifacts, pasted images, and caches in an automatically generated per-user, workspace-specific child directory. Windows shared roots remain disabled until their ACLs can be verified safely.',
|
|
93
103
|
allowedDirs: 'Additional allowed directories',
|
|
94
104
|
allowedDirsHint: 'One path per line. The session workspace is always allowed.',
|
|
105
|
+
objectStorage: 'Object storage',
|
|
106
|
+
objectStorageHint: 'S3-compatible storage used when a provider enables URL image transfer. The access key is stored as a DSH credential and never shown again after saving.',
|
|
107
|
+
storageEndpoint: 'Endpoint',
|
|
108
|
+
storageBucket: 'Bucket',
|
|
109
|
+
storageAccessKeyId: 'Access Key ID',
|
|
110
|
+
storageSecretAccessKey: 'Secret Access Key',
|
|
111
|
+
storagePublicBase: 'Public base URL (optional)',
|
|
112
|
+
storagePublicBaseHint: 'Custom domain or r2.dev URL for permanent public links. Leave blank to use temporary presigned URLs.',
|
|
113
|
+
testStorage: 'Test object storage',
|
|
114
|
+
testingStorage: 'Testing…',
|
|
115
|
+
storageKeyPair: 'Access Key ID and Secret Access Key must both be filled together.',
|
|
95
116
|
save: 'Save and apply',
|
|
96
117
|
saving: 'Validating runtime…',
|
|
97
118
|
reload: 'Reload',
|
|
98
119
|
saved: 'Settings validated and applied.',
|
|
120
|
+
unsavedChanges: 'Changed but not saved',
|
|
121
|
+
discardChanges: 'Discard changes',
|
|
122
|
+
noUnsavedChanges: 'Settings unchanged',
|
|
99
123
|
readOnly: 'Service settings are read-only. A writable API key can still be saved.',
|
|
100
124
|
configured: 'Configured',
|
|
101
125
|
missing: 'Missing',
|
|
@@ -112,7 +136,7 @@ const en = {
|
|
|
112
136
|
connectionHint: 'The API connection test only queries GET /models. The vision model test sends the bundled diagnostic image and verifies one real multimodal request.',
|
|
113
137
|
saveBeforeTesting: 'Save service changes before testing the connection.',
|
|
114
138
|
advanced: 'Advanced settings',
|
|
115
|
-
advancedHint: 'Credential name, provider compatibility, output language, resource limits, runtime source, Python, and additional readable directories.',
|
|
139
|
+
advancedHint: 'Credential name, provider compatibility, output language, resource limits, default save directory, runtime source, Python, and additional readable directories.',
|
|
116
140
|
imageInput: 'Image input',
|
|
117
141
|
hiddenVariants: 'Transparent variant routing',
|
|
118
142
|
hiddenVariantsLabel: 'Keep the original model names and enable images automatically',
|
|
@@ -255,6 +279,7 @@ const zh: Record<LocaleKey, string> = {
|
|
|
255
279
|
providers: '视觉服务列表',
|
|
256
280
|
name: '名称',
|
|
257
281
|
enabled: '启用',
|
|
282
|
+
disabled: '禁用',
|
|
258
283
|
priority: '优先级 {index}',
|
|
259
284
|
attempts: '尝试次数',
|
|
260
285
|
attemptsHint: '该服务在故障转移到下一个之前最多尝试的次数。',
|
|
@@ -283,6 +308,10 @@ const zh: Record<LocaleKey, string> = {
|
|
|
283
308
|
anthropicThinking: 'Anthropic thinking',
|
|
284
309
|
anthropicThinkingHint: 'omit 兼容性最好。仅当所选模型明确支持时使用 disabled 或 adaptive;遇到 HTTP 400 时先恢复 omit。',
|
|
285
310
|
userAgent: 'User-Agent',
|
|
311
|
+
stream: '流式请求',
|
|
312
|
+
streamHint: '以流式(SSE)方式请求补全,而非一次性返回 JSON。适合对非流式支持不佳、长输出易连接超时的端点。',
|
|
313
|
+
uploadViaUrl: '通过 URL 传输图片',
|
|
314
|
+
uploadViaUrlHint: '把每张图片上传到下方配置的对象存储,将 URL 传给模型而不是 base64。需要先配置对象存储;使用 region 裁剪时回退为 base64。',
|
|
286
315
|
language: '结果语言',
|
|
287
316
|
limits: '超时与并发限制',
|
|
288
317
|
timeout: '单次请求超时(毫秒)',
|
|
@@ -303,12 +332,29 @@ const zh: Record<LocaleKey, string> = {
|
|
|
303
332
|
runtimeMode: '环境来源',
|
|
304
333
|
toolkitPath: 'agent-vision-toolkit 目录',
|
|
305
334
|
python: 'Python 解释器(可选)',
|
|
335
|
+
storage: '本地文件',
|
|
336
|
+
storageDir: '默认保存目录',
|
|
337
|
+
storageDirHint: '留空时继续在各工作区创建 .dsh-vision-toolkit。在 POSIX 系统上填写绝对路径共享根目录(例如 /tmp/dsh-vision-toolkit)后,产物、粘贴图片和缓存会保存到自动生成的当前用户、工作区专属子目录中。Windows 共享目录会保持禁用,直到能够安全校验 ACL 权限。',
|
|
306
338
|
allowedDirs: '允许读取的其他目录',
|
|
307
339
|
allowedDirsHint: '每行填写一个目录。当前会话的工作目录始终可以读取,无需重复填写。',
|
|
340
|
+
objectStorage: '对象存储',
|
|
341
|
+
objectStorageHint: '用于「通过 URL 传输图片」的 S3 兼容存储。访问密钥以 DSH 凭据保存,保存后不再显示。',
|
|
342
|
+
storageEndpoint: 'Endpoint',
|
|
343
|
+
storageBucket: 'Bucket',
|
|
344
|
+
storageAccessKeyId: 'Access Key ID',
|
|
345
|
+
storageSecretAccessKey: 'Secret Access Key',
|
|
346
|
+
storagePublicBase: '公网 URL 前缀(可选)',
|
|
347
|
+
storagePublicBaseHint: '自定义域名或 r2.dev 地址,用于生成永久公网链接。留空则使用临时预签名 URL。',
|
|
348
|
+
testStorage: '测试对象存储',
|
|
349
|
+
testingStorage: '测试中…',
|
|
350
|
+
storageKeyPair: 'Access Key ID 与 Secret Access Key 必须同时填写。',
|
|
308
351
|
save: '保存设置',
|
|
309
352
|
saving: '正在检查并应用…',
|
|
310
353
|
reload: '重新加载',
|
|
311
354
|
saved: '设置已保存并生效。',
|
|
355
|
+
unsavedChanges: '已更改但尚未保存',
|
|
356
|
+
discardChanges: '丢弃更改',
|
|
357
|
+
noUnsavedChanges: '设置项未变更',
|
|
312
358
|
readOnly: '服务设置来自只读配置;如果 API 密钥可写,仍可在此保存密钥。',
|
|
313
359
|
configured: '已就绪',
|
|
314
360
|
missing: '未配置',
|
|
@@ -325,7 +371,7 @@ const zh: Record<LocaleKey, string> = {
|
|
|
325
371
|
connectionHint: '“测试 API 连接”只请求 GET /models;“测试视觉模型”会发送插件自带的诊断图片,验证一次真实多模态调用。',
|
|
326
372
|
saveBeforeTesting: '修改服务配置后,请先保存,再执行 API 或视觉模型测试。',
|
|
327
373
|
advanced: '高级设置',
|
|
328
|
-
advancedHint: '
|
|
374
|
+
advancedHint: '凭据名称、服务兼容参数、结果语言、资源限制、默认保存目录、运行环境来源、Python 和额外可读目录。一般无需修改。',
|
|
329
375
|
imageInput: '图片输入',
|
|
330
376
|
hiddenVariants: '透明变体路由',
|
|
331
377
|
hiddenVariantsLabel: '保留原模型名并自动启用图片能力',
|
|
@@ -521,6 +567,8 @@ interface ProviderValue {
|
|
|
521
567
|
protocol?: 'openai' | 'anthropic'
|
|
522
568
|
anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
|
|
523
569
|
userAgent?: string
|
|
570
|
+
stream?: boolean
|
|
571
|
+
uploadViaUrl?: boolean
|
|
524
572
|
t1Seconds?: number
|
|
525
573
|
t2Seconds?: number
|
|
526
574
|
maxImageBytes?: number
|
|
@@ -537,6 +585,8 @@ interface SettingsValue {
|
|
|
537
585
|
protocol?: 'openai' | 'anthropic'
|
|
538
586
|
anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
|
|
539
587
|
userAgent?: string
|
|
588
|
+
stream?: boolean
|
|
589
|
+
uploadViaUrl?: boolean
|
|
540
590
|
}
|
|
541
591
|
providers?: ProviderValue[]
|
|
542
592
|
language?: 'zh' | 'en'
|
|
@@ -546,7 +596,15 @@ interface SettingsValue {
|
|
|
546
596
|
maxImageBytes?: number
|
|
547
597
|
maxImagePixels?: number
|
|
548
598
|
concurrency?: number
|
|
599
|
+
objectStorage?: {
|
|
600
|
+
endpoint?: string
|
|
601
|
+
bucket?: string
|
|
602
|
+
credential?: string
|
|
603
|
+
publicBase?: string
|
|
604
|
+
}
|
|
549
605
|
runtime?: { mode?: 'managed' | 'external'; agentVisionToolkitPath?: string; python?: string }
|
|
606
|
+
storageDir?: string
|
|
607
|
+
storageHistory?: string[]
|
|
550
608
|
allowedDirs?: string[]
|
|
551
609
|
imageInputVariants?: {
|
|
552
610
|
enabled?: boolean
|
|
@@ -603,6 +661,7 @@ interface SettingsSnapshot {
|
|
|
603
661
|
settings: { value: SettingsValue; revision: number; applies: 'live' }
|
|
604
662
|
credential: { ref: string; configured: boolean; source?: string; writable: boolean }
|
|
605
663
|
credentials: Array<{ ref: string; configured: boolean; source?: string; writable: boolean }>
|
|
664
|
+
objectStorageCredential: { ref: string; configured: boolean; writable: boolean }
|
|
606
665
|
runtime: {
|
|
607
666
|
ready: boolean
|
|
608
667
|
generation: number
|
|
@@ -742,6 +801,15 @@ function VisionIcon({ kind = 'scan' }: { kind?: 'scan' | 'target' | 'layers' | '
|
|
|
742
801
|
)
|
|
743
802
|
}
|
|
744
803
|
|
|
804
|
+
function ReloadIcon() {
|
|
805
|
+
return (
|
|
806
|
+
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
|
|
807
|
+
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
|
|
808
|
+
<path d="M21 3v5h-5" />
|
|
809
|
+
</svg>
|
|
810
|
+
)
|
|
811
|
+
}
|
|
812
|
+
|
|
745
813
|
function ToolShell({
|
|
746
814
|
block, title, summary, icon, children, t,
|
|
747
815
|
}: {
|
|
@@ -980,7 +1048,8 @@ interface SettingsState {
|
|
|
980
1048
|
providerHealth?: Record<number, HealthResult> | undefined
|
|
981
1049
|
update?: PluginUpdateCheck | undefined
|
|
982
1050
|
restart?: PluginUpdateResult | undefined
|
|
983
|
-
|
|
1051
|
+
storageTest?: { detail: string } | undefined
|
|
1052
|
+
action?: 'save' | 'health' | 'connection' | 'model' | 'check-update' | 'apply-update' | 'test-storage' | undefined
|
|
984
1053
|
message?: string | undefined
|
|
985
1054
|
error?: string | undefined
|
|
986
1055
|
}
|
|
@@ -1115,6 +1184,20 @@ export class VisionSettingsController {
|
|
|
1115
1184
|
}
|
|
1116
1185
|
}
|
|
1117
1186
|
|
|
1187
|
+
async testStorage(): Promise<void> {
|
|
1188
|
+
this.set({ ...this.state, action: 'test-storage', error: undefined, message: undefined })
|
|
1189
|
+
try {
|
|
1190
|
+
const result = await apiRequest<{ detail: string }>({
|
|
1191
|
+
method: 'POST',
|
|
1192
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1193
|
+
body: JSON.stringify({ action: 'test-storage' }),
|
|
1194
|
+
})
|
|
1195
|
+
this.set({ ...this.state, action: undefined, storageTest: result })
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) })
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1118
1201
|
async checkUpdate(): Promise<void> {
|
|
1119
1202
|
this.set({ ...this.state, action: 'check-update', error: undefined, message: undefined })
|
|
1120
1203
|
try {
|
|
@@ -1163,6 +1246,8 @@ interface ProviderDraft {
|
|
|
1163
1246
|
protocol: 'openai' | 'anthropic'
|
|
1164
1247
|
anthropicThinking: 'omit' | 'disabled' | 'adaptive'
|
|
1165
1248
|
userAgent: string
|
|
1249
|
+
stream: boolean
|
|
1250
|
+
uploadViaUrl: boolean
|
|
1166
1251
|
t1Seconds: string
|
|
1167
1252
|
t2Seconds: string
|
|
1168
1253
|
maxImageBytes: string
|
|
@@ -1180,9 +1265,13 @@ interface Draft {
|
|
|
1180
1265
|
maxImageBytes: string
|
|
1181
1266
|
maxImagePixels: string
|
|
1182
1267
|
concurrency: string
|
|
1268
|
+
storageEndpoint: string
|
|
1269
|
+
storageBucket: string
|
|
1270
|
+
storagePublicBase: string
|
|
1183
1271
|
runtimeMode: 'managed' | 'external'
|
|
1184
1272
|
toolkitPath: string
|
|
1185
1273
|
python: string
|
|
1274
|
+
storageDir: string
|
|
1186
1275
|
allowedDirs: string
|
|
1187
1276
|
hiddenVariants: boolean
|
|
1188
1277
|
variantEnabled: boolean
|
|
@@ -1222,6 +1311,8 @@ function emptyProviderDraft(defaults: ProviderDefaults): ProviderDraft {
|
|
|
1222
1311
|
protocol: 'openai',
|
|
1223
1312
|
anthropicThinking: 'omit',
|
|
1224
1313
|
userAgent: DEFAULT_USER_AGENT,
|
|
1314
|
+
stream: false,
|
|
1315
|
+
uploadViaUrl: false,
|
|
1225
1316
|
t1Seconds: '90',
|
|
1226
1317
|
t2Seconds: '90',
|
|
1227
1318
|
maxImageBytes: String(defaults.maxImageBytes),
|
|
@@ -1244,6 +1335,8 @@ function providerDraftOf(value: ProviderValue | undefined, fallback: ProviderVal
|
|
|
1244
1335
|
protocol: source?.protocol ?? 'openai',
|
|
1245
1336
|
anthropicThinking: source?.anthropicThinking ?? 'omit',
|
|
1246
1337
|
userAgent: source?.userAgent ?? DEFAULT_USER_AGENT,
|
|
1338
|
+
stream: source?.stream === true,
|
|
1339
|
+
uploadViaUrl: source?.uploadViaUrl === true,
|
|
1247
1340
|
t1Seconds: String(source?.t1Seconds ?? 90),
|
|
1248
1341
|
t2Seconds: String(source?.t2Seconds ?? 90),
|
|
1249
1342
|
maxImageBytes: String(source?.maxImageBytes ?? defaults.maxImageBytes),
|
|
@@ -1272,9 +1365,13 @@ function draftOf(value: SettingsValue): Draft {
|
|
|
1272
1365
|
maxImageBytes: String(globalDefaults.maxImageBytes),
|
|
1273
1366
|
maxImagePixels: String(globalDefaults.maxImagePixels),
|
|
1274
1367
|
concurrency: String(globalDefaults.concurrency),
|
|
1368
|
+
storageEndpoint: value.objectStorage?.endpoint ?? '',
|
|
1369
|
+
storageBucket: value.objectStorage?.bucket ?? '',
|
|
1370
|
+
storagePublicBase: value.objectStorage?.publicBase ?? '',
|
|
1275
1371
|
runtimeMode: value.runtime?.mode ?? 'managed',
|
|
1276
1372
|
toolkitPath: value.runtime?.agentVisionToolkitPath ?? '',
|
|
1277
1373
|
python: value.runtime?.python ?? '',
|
|
1374
|
+
storageDir: value.storageDir ?? '',
|
|
1278
1375
|
allowedDirs: (value.allowedDirs ?? []).join('\n'),
|
|
1279
1376
|
hiddenVariants: value.imageInputVariants?.hidden ?? true,
|
|
1280
1377
|
variantEnabled: value.imageInputVariants?.enabled ?? true,
|
|
@@ -1310,6 +1407,8 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
|
|
|
1310
1407
|
protocol: provider.protocol,
|
|
1311
1408
|
anthropicThinking: provider.anthropicThinking,
|
|
1312
1409
|
...(provider.userAgent.trim() === DEFAULT_USER_AGENT ? {} : { userAgent: provider.userAgent.trim() }),
|
|
1410
|
+
...(provider.stream ? { stream: true } : {}),
|
|
1411
|
+
...(provider.uploadViaUrl ? { uploadViaUrl: true } : {}),
|
|
1313
1412
|
...(provider.t1Seconds.trim().length === 0 ? {} : { t1Seconds: positiveInteger(provider.t1Seconds, t('t1'), t) }),
|
|
1314
1413
|
...(provider.t2Seconds.trim().length === 0 ? {} : { t2Seconds: positiveInteger(provider.t2Seconds, t('t2'), t) }),
|
|
1315
1414
|
...(provider.maxImageBytes.trim().length === 0 ? {} : { maxImageBytes: positiveInteger(provider.maxImageBytes, t('maxBytes'), t) }),
|
|
@@ -1326,6 +1425,8 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
|
|
|
1326
1425
|
protocol: primary?.protocol ?? 'openai',
|
|
1327
1426
|
anthropicThinking: primary?.anthropicThinking ?? 'omit',
|
|
1328
1427
|
userAgent: primary?.userAgent.trim() || DEFAULT_USER_AGENT,
|
|
1428
|
+
stream: primary?.stream === true,
|
|
1429
|
+
uploadViaUrl: primary?.uploadViaUrl === true,
|
|
1329
1430
|
},
|
|
1330
1431
|
providers,
|
|
1331
1432
|
language: draft.language,
|
|
@@ -1335,11 +1436,22 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
|
|
|
1335
1436
|
maxImageBytes: positiveInteger(draft.maxImageBytes, t('maxBytes'), t),
|
|
1336
1437
|
maxImagePixels: positiveInteger(draft.maxImagePixels, t('maxPixels'), t),
|
|
1337
1438
|
concurrency: positiveInteger(draft.concurrency, t('concurrency'), t),
|
|
1439
|
+
...(draft.storageEndpoint.trim().length === 0 && draft.storageBucket.trim().length === 0
|
|
1440
|
+
? {}
|
|
1441
|
+
: {
|
|
1442
|
+
objectStorage: {
|
|
1443
|
+
endpoint: draft.storageEndpoint.trim(),
|
|
1444
|
+
bucket: draft.storageBucket.trim(),
|
|
1445
|
+
credential: OBJECT_STORAGE_CREDENTIAL,
|
|
1446
|
+
...(draft.storagePublicBase.trim().length === 0 ? {} : { publicBase: draft.storagePublicBase.trim() }),
|
|
1447
|
+
},
|
|
1448
|
+
}),
|
|
1338
1449
|
runtime: {
|
|
1339
1450
|
mode: draft.runtimeMode,
|
|
1340
1451
|
...(draft.runtimeMode === 'external' ? { agentVisionToolkitPath: draft.toolkitPath.trim() } : {}),
|
|
1341
1452
|
...(draft.python.trim().length === 0 ? {} : { python: draft.python.trim() }),
|
|
1342
1453
|
},
|
|
1454
|
+
...(draft.storageDir.trim().length === 0 ? {} : { storageDir: draft.storageDir.trim() }),
|
|
1343
1455
|
allowedDirs: draft.allowedDirs.split(/\r?\n/).map(entry => entry.trim()).filter(Boolean),
|
|
1344
1456
|
imageInputVariants: {
|
|
1345
1457
|
...(draft.variantEnabled ? {} : { enabled: false }),
|
|
@@ -1463,6 +1575,7 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1463
1575
|
const [draft, setDraft] = useState<Draft | undefined>(undefined)
|
|
1464
1576
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
1465
1577
|
const [keys, setKeys] = useState<Record<string, string>>({})
|
|
1578
|
+
const [storageKeys, setStorageKeys] = useState<{ accessKeyId: string; secretAccessKey: string }>({ accessKeyId: '', secretAccessKey: '' })
|
|
1466
1579
|
const [draftError, setDraftError] = useState<string | undefined>(undefined)
|
|
1467
1580
|
const [copiedCommand, setCopiedCommand] = useState(false)
|
|
1468
1581
|
|
|
@@ -1571,9 +1684,18 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1571
1684
|
return
|
|
1572
1685
|
}
|
|
1573
1686
|
}
|
|
1687
|
+
const accessKeyId = storageKeys.accessKeyId.trim()
|
|
1688
|
+
const secretAccessKey = storageKeys.secretAccessKey.trim()
|
|
1689
|
+
if ((accessKeyId.length === 0) !== (secretAccessKey.length === 0)) {
|
|
1690
|
+
setDraftError(t('storageKeyPair'))
|
|
1691
|
+
return
|
|
1692
|
+
}
|
|
1574
1693
|
const credentials = Object.entries(keys)
|
|
1575
1694
|
.filter(([, value]) => value.trim().length > 0)
|
|
1576
1695
|
.map(([ref, value]) => ({ ref, value: value.trim() }))
|
|
1696
|
+
if (accessKeyId.length > 0 && (draft.storageEndpoint.trim().length > 0 || draft.storageBucket.trim().length > 0)) {
|
|
1697
|
+
credentials.push({ ref: OBJECT_STORAGE_CREDENTIAL, value: `${accessKeyId}:${secretAccessKey}` })
|
|
1698
|
+
}
|
|
1577
1699
|
const savedRefs = new Set(credentials.map(credential => credential.ref))
|
|
1578
1700
|
setDraftError(undefined)
|
|
1579
1701
|
void controller.save(
|
|
@@ -1584,12 +1706,19 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1584
1706
|
).then(saved => {
|
|
1585
1707
|
if (saved) {
|
|
1586
1708
|
setKeys(current => Object.fromEntries(Object.entries(current).filter(([ref]) => !savedRefs.has(ref))))
|
|
1709
|
+
if (savedRefs.has(OBJECT_STORAGE_CREDENTIAL)) setStorageKeys({ accessKeyId: '', secretAccessKey: '' })
|
|
1587
1710
|
}
|
|
1588
1711
|
})
|
|
1589
1712
|
} catch (error) {
|
|
1590
1713
|
setDraftError(error instanceof Error ? error.message : String(error))
|
|
1591
1714
|
}
|
|
1592
1715
|
}
|
|
1716
|
+
const discard = (): void => {
|
|
1717
|
+
setDraft(draftOf(snapshot.settings.value))
|
|
1718
|
+
setKeys({})
|
|
1719
|
+
setStorageKeys({ accessKeyId: '', secretAccessKey: '' })
|
|
1720
|
+
setDraftError(undefined)
|
|
1721
|
+
}
|
|
1593
1722
|
const busy = state.action !== undefined
|
|
1594
1723
|
const selectedProvider = draft.providers[selectedIndex]
|
|
1595
1724
|
const providerCredentials = snapshot.credentials ?? []
|
|
@@ -1597,14 +1726,18 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1597
1726
|
const selectedKey = selectedProvider === undefined ? '' : (keys[selectedProvider.credential] ?? '')
|
|
1598
1727
|
const selectedKeyLocked = selectedCredential !== undefined && !selectedCredential.writable
|
|
1599
1728
|
const selectedBuiltInFree = selectedCredential?.source === 'built-in-free'
|
|
1600
|
-
const
|
|
1729
|
+
const objectStorageCredential = snapshot.objectStorageCredential ?? { ref: '', configured: false, writable: false }
|
|
1730
|
+
const canSave = snapshot.writable || Object.values(keys).some(value => value.trim().length > 0) || storageKeys.accessKeyId.trim().length > 0 || storageKeys.secretAccessKey.trim().length > 0
|
|
1601
1731
|
const runtimeErrorTitle = snapshot.runtime.ready ? t('runtimeCandidateRejected') : t('runtimeUnavailable')
|
|
1602
1732
|
const pluginUpdate = state.update
|
|
1603
1733
|
const updateCapability = pluginUpdate ?? snapshot.release.update
|
|
1604
1734
|
const latestVersion = pluginUpdate?.latestVersion
|
|
1605
1735
|
const updateReason = updateCapability.reason === undefined ? undefined : t(UPDATE_REASON_KEYS[updateCapability.reason])
|
|
1606
1736
|
const updateCheckSupported = updateCapability.checkSupported ?? updateCapability.supported
|
|
1607
|
-
const updateHasUnsavedChanges = Object.values(keys).some(value => value.
|
|
1737
|
+
const updateHasUnsavedChanges = Object.values(keys).some(value => value.length > 0)
|
|
1738
|
+
|| storageKeys.accessKeyId.length > 0
|
|
1739
|
+
|| storageKeys.secretAccessKey.length > 0
|
|
1740
|
+
|| settingsDraftChanged(draft, snapshot.settings.value, t)
|
|
1608
1741
|
const manualUpdateProfile = updateCapability.profile ?? 'web'
|
|
1609
1742
|
const manualUpdateCommand = `dsh plugin --profile ${manualUpdateProfile} add @mengruo/dsh-vision-toolkit@latest --registry=https://registry.npmjs.org/`
|
|
1610
1743
|
const copyManualUpdate = (): void => {
|
|
@@ -1624,6 +1757,18 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1624
1757
|
|
|
1625
1758
|
return (
|
|
1626
1759
|
<div className="dvt-settings">
|
|
1760
|
+
<div className="dvt-save-dock" data-dirty={updateHasUnsavedChanges ? 'true' : undefined}>
|
|
1761
|
+
<button type="button" className="dvt-save-dock-reload" aria-label={t('reload')} title={t('reload')} disabled={busy} onClick={() => { void controller.load() }}><ReloadIcon /></button>
|
|
1762
|
+
{updateHasUnsavedChanges ? (
|
|
1763
|
+
<>
|
|
1764
|
+
<span className="dvt-save-dock-text">{t('unsavedChanges')}</span>
|
|
1765
|
+
<Button size="sm" variant="outline" disabled={busy} onClick={discard}>{t('discardChanges')}</Button>
|
|
1766
|
+
<Button size="sm" variant="primary" disabled={!canSave || busy} onClick={save}>{state.action === 'save' ? t('saving') : t('save')}</Button>
|
|
1767
|
+
</>
|
|
1768
|
+
) : (
|
|
1769
|
+
<span className="dvt-save-dock-text">{t('noUnsavedChanges')}</span>
|
|
1770
|
+
)}
|
|
1771
|
+
</div>
|
|
1627
1772
|
<div className="dvt-alert notice">{t('externalNotice')}</div>
|
|
1628
1773
|
{!snapshot.writable ? <div className="dvt-alert warning">{t('readOnly')}</div> : null}
|
|
1629
1774
|
{draftError === undefined ? null : <div className="dvt-alert error">{draftError}</div>}
|
|
@@ -1656,6 +1801,8 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1656
1801
|
<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>
|
|
1657
1802
|
{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}
|
|
1658
1803
|
<Field label={t('userAgent')}><Input aria-label={t('userAgent')} value={selectedProvider.userAgent} onChange={(event) => { updateProvider(selectedIndex, 'userAgent', event.target.value) }} /></Field>
|
|
1804
|
+
<Field label={t('stream')} hint={t('streamHint')}><select aria-label={t('stream')} disabled={!snapshot.writable || busy} value={selectedProvider.stream ? 'enabled' : 'disabled'} onChange={(event) => { updateProvider(selectedIndex, 'stream', event.target.value === 'enabled') }}><option value="disabled">{t('disabled')}</option><option value="enabled">{t('enabled')}</option></select></Field>
|
|
1805
|
+
<Field label={t('uploadViaUrl')} hint={t('uploadViaUrlHint')}><select aria-label={t('uploadViaUrl')} disabled={!snapshot.writable || busy} value={selectedProvider.uploadViaUrl ? 'enabled' : 'disabled'} onChange={(event) => { updateProvider(selectedIndex, 'uploadViaUrl', event.target.value === 'enabled') }}><option value="disabled">{t('disabled')}</option><option value="enabled">{t('enabled')}</option></select></Field>
|
|
1659
1806
|
<Field label={t('t1')} hint={t('t1Hint')}><Input aria-label={t('t1')} inputMode="numeric" value={selectedProvider.t1Seconds} onChange={(event) => { updateProvider(selectedIndex, 't1Seconds', event.target.value) }} /></Field>
|
|
1660
1807
|
<Field label={t('t2')} hint={t('t2Hint')}><Input aria-label={t('t2')} inputMode="numeric" value={selectedProvider.t2Seconds} onChange={(event) => { updateProvider(selectedIndex, 't2Seconds', event.target.value) }} /></Field>
|
|
1661
1808
|
<Field label={t('maxBytes')}><Input aria-label={t('maxBytes')} inputMode="numeric" value={selectedProvider.maxImageBytes} onChange={(event) => { updateProvider(selectedIndex, 'maxImageBytes', event.target.value) }} /></Field>
|
|
@@ -1677,8 +1824,6 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1677
1824
|
)}
|
|
1678
1825
|
</section>
|
|
1679
1826
|
|
|
1680
|
-
<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>
|
|
1681
|
-
|
|
1682
1827
|
<section className="dvt-panel"><div className="dvt-panel-title"><div><h3>{t('prioritySort')}</h3><p>{t('prioritySortHint')}</p></div></div>
|
|
1683
1828
|
<div className="dvt-sorter">
|
|
1684
1829
|
{draft.providers.map((provider, index) => (
|
|
@@ -1742,11 +1887,25 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1742
1887
|
|
|
1743
1888
|
<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>
|
|
1744
1889
|
|
|
1890
|
+
<section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('storage')}</h3></div><div className="dvt-form-grid">
|
|
1891
|
+
<Field label={t('storageDir')} hint={t('storageDirHint')}><Input aria-label={t('storageDir')} placeholder="/tmp/dsh-vision-toolkit" value={draft.storageDir} onChange={(event) => { update('storageDir', event.target.value) }} /></Field>
|
|
1892
|
+
<Field label={t('allowedDirs')} hint={t('allowedDirsHint')}><textarea rows={3} value={draft.allowedDirs} onChange={(event) => { update('allowedDirs', event.target.value) }} /></Field>
|
|
1893
|
+
</div></section>
|
|
1894
|
+
|
|
1895
|
+
<section className="dvt-panel"><div className="dvt-panel-title"><div><h3>{t('objectStorage')}</h3><p>{t('objectStorageHint')}</p></div><div className="dvt-actions"><Button size="sm" variant="outline" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.testStorage() }}>{state.action === 'test-storage' ? t('testingStorage') : t('testStorage')}</Button></div></div><div className="dvt-form-grid">
|
|
1896
|
+
<Field label={t('storageEndpoint')}><Input aria-label={t('storageEndpoint')} disabled={!snapshot.writable || busy} value={draft.storageEndpoint} onChange={(event) => { update('storageEndpoint', event.target.value) }} placeholder="https://<account>.r2.cloudflarestorage.com" /></Field>
|
|
1897
|
+
<Field label={t('storageBucket')}><Input aria-label={t('storageBucket')} disabled={!snapshot.writable || busy} value={draft.storageBucket} onChange={(event) => { update('storageBucket', event.target.value) }} /></Field>
|
|
1898
|
+
<Field label={t('storageAccessKeyId')}><Input aria-label={t('storageAccessKeyId')} type="password" autoComplete="new-password" disabled={busy || !snapshot.writable} placeholder={objectStorageCredential.configured ? t('apiKeyPlaceholderConfigured') : t('apiKeyPlaceholderMissing')} value={storageKeys.accessKeyId} onChange={(event) => { setStorageKeys(current => ({ ...current, accessKeyId: event.target.value })); setDraftError(undefined) }} /></Field>
|
|
1899
|
+
<Field label={t('storageSecretAccessKey')}><Input aria-label={t('storageSecretAccessKey')} type="password" autoComplete="new-password" disabled={busy || !snapshot.writable} placeholder={objectStorageCredential.configured ? t('apiKeyPlaceholderConfigured') : t('apiKeyPlaceholderMissing')} value={storageKeys.secretAccessKey} onChange={(event) => { setStorageKeys(current => ({ ...current, secretAccessKey: event.target.value })); setDraftError(undefined) }} /></Field>
|
|
1900
|
+
<Field label={t('storagePublicBase')} hint={t('storagePublicBaseHint')}><Input aria-label={t('storagePublicBase')} disabled={!snapshot.writable || busy} value={draft.storagePublicBase} onChange={(event) => { update('storagePublicBase', event.target.value) }} placeholder="https://cdn.example.com" /></Field>
|
|
1901
|
+
</div>
|
|
1902
|
+
{state.storageTest === undefined ? null : <div className="dvt-provider-test-result"><div data-status="ok"><span>{t('objectStorage')}</span><strong>{t('statusOk')}</strong><p>{state.storageTest.detail}</p></div></div>}
|
|
1903
|
+
</section>
|
|
1904
|
+
|
|
1745
1905
|
<section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('runtime')}</h3><span className={`dvt-badge ${snapshot.runtime.ready ? 'ok' : 'error'}`}>{snapshot.runtime.ready ? snapshot.runtime.upstream?.source === 'managed' ? t('runtimeManaged') : snapshot.runtime.upstream?.source === 'external' ? t('runtimeExternal') : t('runtimeReady') : t('runtimeUnavailable')}</span></div><div className="dvt-form-grid">
|
|
1746
1906
|
<Field label={t('runtimeMode')}><select value={draft.runtimeMode} onChange={(event) => { update('runtimeMode', event.target.value as 'managed' | 'external') }}><option value="managed">{t('runtimeManaged')}</option><option value="external">{t('runtimeExternal')}</option></select></Field>
|
|
1747
1907
|
{draft.runtimeMode === 'external' ? <Field label={t('toolkitPath')}><Input value={draft.toolkitPath} onChange={(event) => { update('toolkitPath', event.target.value) }} /></Field> : null}
|
|
1748
1908
|
<Field label={t('python')}><Input placeholder="python3" value={draft.python} onChange={(event) => { update('python', event.target.value) }} /></Field>
|
|
1749
|
-
<Field label={t('allowedDirs')} hint={t('allowedDirsHint')}><textarea rows={3} value={draft.allowedDirs} onChange={(event) => { update('allowedDirs', event.target.value) }} /></Field>
|
|
1750
1909
|
</div>
|
|
1751
1910
|
{snapshot.runtime.upstream === undefined ? null : <div className="dvt-runtime-facts"><code>{snapshot.runtime.upstream.path}</code><code>{snapshot.runtime.upstream.python} · {snapshot.runtime.upstream.pythonVersion}</code><code>{snapshot.runtime.upstream.runtimeHome}</code></div>}
|
|
1752
1911
|
</section>
|
|
@@ -1767,11 +1926,12 @@ const CSS = `
|
|
|
1767
1926
|
.dvt-tool-head{width:100%;min-height:38px;display:flex;align-items:center;gap:7px;padding:8px 10px;border:0;background:transparent;color:inherit;text-align:left;cursor:pointer;font:inherit}.dvt-tool-head:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}.dvt-tool-icon{width:20px;height:20px;display:grid;place-items:center;border-radius:6px;color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);flex:none}.dvt-tool-title{font-size:12px;font-weight:650;white-space:nowrap}.dvt-tool-sep{opacity:.35}.dvt-tool-summary{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:var(--dsw-alias-label-secondary)}.dvt-tool-status{margin-left:auto;font-size:11px;color:var(--dsw-alias-label-secondary);max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvt-tool[data-state=error] .dvt-tool-status{color:var(--dsw-alias-state-error-primary)}.dvt-chevron{margin-left:auto;transition:transform .16s ease;opacity:.55}.dvt-chevron[data-open=true]{transform:rotate(180deg)}.dvt-tool-body{padding:0 10px 10px}.dvt-stack{display:grid;gap:10px}.dvt-muted{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}
|
|
1768
1927
|
.dvt-metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.dvt-metrics>div,.dvt-diff-score{padding:10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);display:grid;gap:4px}.dvt-metrics span,.dvt-diff-score span{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--dsw-alias-label-secondary)}.dvt-metrics strong,.dvt-diff-score strong{font-size:13px}.dvt-list{list-style:none;margin:0;padding:0;display:grid;gap:4px;max-height:160px;overflow:auto}.dvt-list li{display:flex;justify-content:space-between;gap:12px;padding:6px 8px;border-radius:7px;background:var(--dsw-alias-bg-layer-2);font-size:11px}.dvt-list code{color:var(--dsw-alias-state-business-primary)}.dvt-table-wrap{max-height:220px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:9px}.dvt-table{width:100%;border-collapse:collapse;font-size:11px}.dvt-table th,.dvt-table td{padding:7px 8px;text-align:left;border-bottom:1px solid var(--dsw-alias-border-l1)}.dvt-table th{position:sticky;top:0;background:var(--dsw-alias-bg-layer-2);font-size:10px;text-transform:uppercase;letter-spacing:.05em}.dvt-table tr:last-child td{border-bottom:0}
|
|
1769
1928
|
.dvt-artifact{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-1)}.dvt-preview{display:block;width:100%;max-height:360px;object-fit:contain;background:repeating-conic-gradient(var(--dsw-alias-bg-module-platform) 0 25%,var(--dsw-alias-bg-layer-1) 0 50%) 50%/18px 18px;border:0}.dvt-svg{height:280px}.dvt-artifact-meta{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 10px}.dvt-artifact-meta>div:first-child{min-width:0;display:grid;gap:2px}.dvt-artifact-meta strong{font-size:12px;overflow:hidden;text-overflow:ellipsis}.dvt-artifact-meta span,.dvt-artifact-meta small{font-size:10px;color:var(--dsw-alias-label-secondary)}.dvt-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap}.dvt-download{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);text-decoration:none;font-size:12px;font-weight:600}.dvt-download:hover{background:var(--dsw-alias-button-primary-hover)}.dvt-download:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.dvt-artifact>.dvt-muted{padding:0 10px 10px}.dvt-diff-score>div{height:5px;border-radius:99px;background:var(--dsw-alias-border-l2);overflow:hidden}.dvt-diff-score i{display:block;height:100%;min-width:2px;background:linear-gradient(90deg,var(--dsw-alias-state-warn-primary),var(--dsw-alias-state-error-primary));border-radius:99px}.dvt-tool h4{font-size:11px;margin:0 0 6px}.dvt-palette{display:grid;grid-template-columns:repeat(auto-fit,minmax(112px,1fr));gap:7px}.dvt-palette>div{display:flex;align-items:center;gap:8px;padding:7px;border:1px solid var(--dsw-alias-border-l1);border-radius:9px}.dvt-palette i{width:28px;height:28px;border-radius:7px;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2)}.dvt-palette span{display:grid}.dvt-palette strong{font-size:11px}.dvt-palette small{font-size:10px;color:var(--dsw-alias-label-secondary)}
|
|
1770
|
-
.dvt-tutorial-link{margin:0;font-size:12px;line-height:1.5}.dvt-tutorial-link a{color:var(--dsw-alias-state-business-primary);text-decoration:none;font-weight:600}.dvt-tutorial-link a:hover{text-decoration:underline}.dvt-manual-update{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2)}.dvt-manual-update code{flex:1;min-width:0;overflow:auto;white-space:nowrap;font-size:11px;color:var(--dsw-alias-label-primary)}.dvt-settings{display:
|
|
1929
|
+
.dvt-tutorial-link{margin:0;font-size:12px;line-height:1.5}.dvt-tutorial-link a{color:var(--dsw-alias-state-business-primary);text-decoration:none;font-weight:600}.dvt-tutorial-link a:hover{text-decoration:underline}.dvt-manual-update{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2)}.dvt-manual-update code{flex:1;min-width:0;overflow:auto;white-space:nowrap;font-size:11px;color:var(--dsw-alias-label-primary)}.dvt-settings{display:flex;flex-direction:column;width:100%;max-width:900px;min-width:0;box-sizing:border-box;gap:14px;padding:8px 2px 32px;color:var(--dsw-alias-label-primary)}.dvt-settings-footer{display:flex;justify-content:space-between;gap:20px;align-items:flex-start;padding:8px 2px}.dvt-settings-footer h2{font-size:25px;letter-spacing:-.025em;margin:3px 0 6px}.dvt-settings-footer p{max-width:620px;margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.55}.dvt-kicker{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--dsw-alias-state-business-primary);font-weight:700}.dvt-release{display:grid;gap:4px;min-width:170px;padding:9px 11px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);font-size:10px;color:var(--dsw-alias-label-secondary)}.dvt-release span{display:flex;justify-content:space-between;gap:12px}.dvt-release strong{color:var(--dsw-alias-label-primary)}.dvt-alert{padding:10px 12px;border-radius:10px;font-size:12px;line-height:1.5;display:grid;gap:3px}.dvt-alert.notice{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);color:var(--dsw-alias-state-business-primary)}.dvt-alert.warning{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent);color:var(--dsw-alias-state-warn-label)}.dvt-alert.error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}.dvt-alert.success{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary)}.dvt-panel{display:grid;grid-template-columns:minmax(0,1fr);gap:12px;padding:15px;border:1px solid var(--dsw-alias-border-l1);border-radius:14px;background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-shadow-lv1)}.dvt-panel-title{display:flex;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;gap:12px}.dvt-panel-title>div:first-child{flex:1 1 320px;min-width:0}.dvt-panel-title>.dvt-actions{margin-left:auto;justify-content:flex-end}.dvt-panel-title h3{font-size:14px;margin:0}.dvt-panel-title p{font-size:11px;line-height:1.45;color:var(--dsw-alias-label-secondary);margin:4px 0 0;max-width:620px}.dvt-badge{font-size:10px;padding:3px 7px;border-radius:999px;font-weight:650}.dvt-badge.ok{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);color:var(--dsw-alias-state-success-primary)}.dvt-badge.warning{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent);color:var(--dsw-alias-state-warn-label)}.dvt-badge.error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}.dvt-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.dvt-field{display:grid;min-width:0;gap:6px;align-content:start}.dvt-field>span{font-size:11px;font-weight:600}.dvt-field>small{font-size:10px;color:var(--dsw-alias-label-secondary);line-height:1.4}.dvt-field select,.dvt-field textarea{width:100%;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:8px 10px}.dvt-field select{height:36px}.dvt-field textarea{resize:vertical;min-height:76px}.dvt-check{display:grid;gap:6px;cursor:pointer}.dvt-check input{width:auto}.dvt-check>span{font-size:12px;font-weight:600}.dvt-check>small{font-size:10px;color:var(--dsw-alias-label-secondary);line-height:1.4}.dvt-runtime-facts{display:grid;gap:4px;padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);overflow:auto}.dvt-runtime-facts code{font-size:10px;white-space:nowrap;color:var(--dsw-alias-label-secondary)}.dvt-update-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.dvt-update-grid>div{display:grid;gap:3px;padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2)}.dvt-update-grid span{font-size:9px;text-transform:uppercase;letter-spacing:.04em;color:var(--dsw-alias-label-caption)}.dvt-update-grid strong{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1771
1930
|
.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}
|
|
1772
1931
|
.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)}
|
|
1773
1932
|
.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}
|
|
1774
1933
|
.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}
|
|
1934
|
+
.dvt-save-dock{position:sticky;top:8px;align-self:flex-end;z-index:50;display:flex;align-items:center;gap:10px;max-width:calc(100% - 16px);padding:11px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-shadow-lv1)}.dvt-save-dock[data-dirty]{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 45%,var(--dsw-alias-border-l1))}.dvt-save-dock-text{font-size:12px;color:var(--dsw-alias-label-secondary)}.dvt-save-dock[data-dirty] .dvt-save-dock-text{color:var(--dsw-alias-state-warn-label);font-weight:600}.dvt-save-dock-reload{flex:none;display:grid;place-items:center;width:26px;height:26px;border:1px solid transparent;border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:0}.dvt-save-dock-reload:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dvt-save-dock-reload:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}.dvt-save-dock-reload:disabled{opacity:.4;cursor:default}
|
|
1775
1935
|
@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}}
|
|
1776
1936
|
`
|
|
1777
1937
|
|
|
@@ -58,6 +58,8 @@ interface PasteOccurrence {
|
|
|
58
58
|
source: string
|
|
59
59
|
ref: string
|
|
60
60
|
offset: number
|
|
61
|
+
/** DSH rc.8+ stores the full @label text; older releases used one placeholder. */
|
|
62
|
+
length?: number
|
|
61
63
|
label: string
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -166,6 +168,10 @@ function pasteLabel(file: File, index: number): string {
|
|
|
166
168
|
return file.name.trim() || `clipboard-image-${index + 1}`
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
function occurrenceEnd(occurrence: PasteOccurrence): number {
|
|
172
|
+
return occurrence.offset + (occurrence.length ?? 1)
|
|
173
|
+
}
|
|
174
|
+
|
|
169
175
|
/** Owns browser File objects until DSH serializes the corresponding text references. */
|
|
170
176
|
export class PasteImageController {
|
|
171
177
|
private readonly records = new Map<string, PasteRecord>()
|
|
@@ -241,6 +247,7 @@ export class PasteImageController {
|
|
|
241
247
|
if (before !== '' && !/\s$/u.test(before)) cursor = this.insertText(input, ' ', cursor)
|
|
242
248
|
for (const [index, file] of files.entries()) {
|
|
243
249
|
const ref = id()
|
|
250
|
+
const label = pasteLabel(file, index)
|
|
244
251
|
const record: PasteRecord = { ref, file, batch, status: 'ready' }
|
|
245
252
|
batch.records.push(record)
|
|
246
253
|
this.records.set(ref, record)
|
|
@@ -248,11 +255,14 @@ export class PasteImageController {
|
|
|
248
255
|
const accepted = input.insertReference({
|
|
249
256
|
source: SOURCE,
|
|
250
257
|
ref,
|
|
251
|
-
label
|
|
252
|
-
clipboardText: `[pasted image: ${
|
|
258
|
+
label,
|
|
259
|
+
clipboardText: `[pasted image: ${label}]`,
|
|
253
260
|
}, { start: cursor, end: cursor, draftRev: snapshot.draftRev })
|
|
254
261
|
if (!accepted) throw new Error('The composer changed before pasted images could be inserted')
|
|
255
|
-
|
|
262
|
+
const inserted = input.state.getSnapshot().occurrences.find(occurrence =>
|
|
263
|
+
occurrence.source === SOURCE && occurrence.ref === ref)
|
|
264
|
+
if (inserted === undefined) throw new Error('The pasted image reference was not present after insertion')
|
|
265
|
+
cursor = occurrenceEnd(inserted)
|
|
256
266
|
const hasNext = index + 1 < files.length
|
|
257
267
|
const suffix = input.state.getSnapshot().draft.slice(cursor)
|
|
258
268
|
if (hasNext || (suffix !== '' && !/^\s/u.test(suffix))) cursor = this.insertText(input, ' ', cursor)
|
|
@@ -594,7 +604,7 @@ export class PasteImageController {
|
|
|
594
604
|
insertText: (text: string, span: { start: number; end: number; draftRev: number }) => boolean
|
|
595
605
|
}).insertText('', {
|
|
596
606
|
start: current.offset,
|
|
597
|
-
end: current
|
|
607
|
+
end: occurrenceEnd(current),
|
|
598
608
|
draftRev: snapshot.draftRev,
|
|
599
609
|
})
|
|
600
610
|
if (!accepted) return
|