@mengruo/dsh-vision-toolkit 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +356 -0
- package/README.zh.md +354 -0
- package/assets/community-group-qr.png +0 -0
- package/assets/dsh-conversation-artifact.png +0 -0
- package/assets/dsh-conversation-image-qa-top.png +0 -0
- package/assets/dsh-conversation-image-qa.png +0 -0
- package/assets/dsh-conversation-pixel-diff.png +0 -0
- package/assets/dsh-conversation-screenshot-debugging-top.png +0 -0
- package/assets/dsh-conversation-screenshot-debugging.png +0 -0
- package/assets/dsh-conversation-tool-call.png +0 -0
- package/assets/dsh-conversation-vision-trace.png +0 -0
- package/assets/dsh-view-example.png +0 -0
- package/assets/hero-v2.png +0 -0
- package/assets/logo_aihubmix.png +0 -0
- package/assets/python-bootstrap.json +48 -0
- package/assets/skill/SKILL.md +329 -0
- package/assets/skill/UPSTREAM.json +71 -0
- package/assets/skill/references/gui.md +88 -0
- package/assets/skill/references/long-screenshot-ocr.md +77 -0
- package/assets/skill/references/restore-graphic.md +84 -0
- package/assets/skill/references/restore-structure.md +45 -0
- package/assets/skill/references/restore-ui.md +202 -0
- package/assets/social-preview.png +0 -0
- package/assets/upstream/README.md +18 -0
- package/assets/upstream/focus-hint-comparison-1.webp +0 -0
- package/assets/upstream/focus-hint-comparison-2.webp +0 -0
- package/assets/upstream/infographic-reference.webp +0 -0
- package/assets/upstream/infographic-result.webp +0 -0
- package/assets/upstream/ui-fast-restore-reference.webp +0 -0
- package/assets/upstream/ui-fast-restore-result.webp +0 -0
- package/assets/upstream/ui-result.webp +0 -0
- package/assets/upstream/ui-sketch.webp +0 -0
- package/assets/vision-model-test.png +0 -0
- package/assets/vision-settings.png +0 -0
- package/assets/wechat-reward.png +0 -0
- package/cordis.patch.yml +6 -0
- package/docs/aihubmix-gemini-vision.i18n.yaml +6 -0
- package/docs/aihubmix-gemini-vision.md +138 -0
- package/docs/aihubmix-gemini-vision.zh.md +138 -0
- package/docs/assets/aihubmix-api-keys.png +0 -0
- package/docs/assets/aihubmix-create-key.png +0 -0
- package/docs/assets/aihubmix-free-vision-model.png +0 -0
- package/docs/assets/aihubmix-home.png +0 -0
- package/docs/assets/aihubmix-sign-up.png +0 -0
- package/docs/assets/groq-console-home.png +0 -0
- package/docs/assets/groq-console-keys-login.png +0 -0
- package/docs/assets/groq-docs-quickstart.png +0 -0
- package/docs/assets/groq-docs-qwen3.6.png +0 -0
- package/docs/assets/groq-docs-vision.png +0 -0
- package/docs/assets/vision-settings.png +0 -0
- package/docs/dsh-desktop-install.md +67 -0
- package/docs/dsh-desktop-install.zh.md +67 -0
- package/docs/groq-qwen3.6-vision.i18n.yaml +6 -0
- package/docs/groq-qwen3.6-vision.md +236 -0
- package/docs/groq-qwen3.6-vision.zh.md +236 -0
- package/docs/python-runtime.i18n.yaml +6 -0
- package/docs/python-runtime.md +89 -0
- package/docs/python-runtime.zh.md +89 -0
- package/docs/requirements-traceability/README.i18n.yaml +6 -0
- package/docs/requirements-traceability/README.md +75 -0
- package/docs/requirements-traceability/README.zh.md +75 -0
- package/examples/ui-restoration/README.i18n.yaml +6 -0
- package/examples/ui-restoration/README.md +70 -0
- package/examples/ui-restoration/README.zh.md +70 -0
- package/examples/ui-restoration/assets/final-heatmap.png +0 -0
- package/examples/ui-restoration/assets/final-report.json +83 -0
- package/examples/ui-restoration/assets/implementation.png +0 -0
- package/examples/ui-restoration/assets/initial-heatmap.png +0 -0
- package/examples/ui-restoration/assets/initial-report.json +83 -0
- package/examples/ui-restoration/assets/initial.png +0 -0
- package/examples/ui-restoration/assets/metrics.json +12 -0
- package/examples/ui-restoration/assets/reference.png +0 -0
- package/examples/ui-restoration/implementation.html +94 -0
- package/examples/ui-restoration/initial.html +57 -0
- package/lib/artifact-access.js +369 -0
- package/lib/artifact-access.js.map +1 -0
- package/lib/artifacts.js +56 -0
- package/lib/artifacts.js.map +1 -0
- package/lib/client.js +2099 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +172 -0
- package/lib/config.js.map +1 -0
- package/lib/defaults.js +6 -0
- package/lib/defaults.js.map +1 -0
- package/lib/errors.js +56 -0
- package/lib/errors.js.map +1 -0
- package/lib/evidence-cache.js +350 -0
- package/lib/evidence-cache.js.map +1 -0
- package/lib/exposure.js +247 -0
- package/lib/exposure.js.map +1 -0
- package/lib/image-input-variants.js +882 -0
- package/lib/image-input-variants.js.map +1 -0
- package/lib/index.js +105 -0
- package/lib/index.js.map +1 -0
- package/lib/paste-images.js +217 -0
- package/lib/paste-images.js.map +1 -0
- package/lib/paths.js +354 -0
- package/lib/paths.js.map +1 -0
- package/lib/plugin-update.js +1003 -0
- package/lib/plugin-update.js.map +1 -0
- package/lib/runtime-install.js +964 -0
- package/lib/runtime-install.js.map +1 -0
- package/lib/runtime-manager.js +133 -0
- package/lib/runtime-manager.js.map +1 -0
- package/lib/runtime.js +1678 -0
- package/lib/runtime.js.map +1 -0
- package/lib/skill.js +25 -0
- package/lib/skill.js.map +1 -0
- package/lib/tools.js +549 -0
- package/lib/tools.js.map +1 -0
- package/lib/types/artifact-access.d.ts +61 -0
- package/lib/types/artifact-access.d.ts.map +1 -0
- package/lib/types/artifacts.d.ts +42 -0
- package/lib/types/artifacts.d.ts.map +1 -0
- package/lib/types/client/display-config.d.ts +24 -0
- package/lib/types/client/display-config.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +350 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/model-variants-hider.d.ts +40 -0
- package/lib/types/client/model-variants-hider.d.ts.map +1 -0
- package/lib/types/client/paste-images.d.ts +126 -0
- package/lib/types/client/paste-images.d.ts.map +1 -0
- package/lib/types/config.d.ts +123 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/defaults.d.ts +6 -0
- package/lib/types/defaults.d.ts.map +1 -0
- package/lib/types/errors.d.ts +35 -0
- package/lib/types/errors.d.ts.map +1 -0
- package/lib/types/evidence-cache.d.ts +91 -0
- package/lib/types/evidence-cache.d.ts.map +1 -0
- package/lib/types/exposure.d.ts +50 -0
- package/lib/types/exposure.d.ts.map +1 -0
- package/lib/types/image-input-variants.d.ts +144 -0
- package/lib/types/image-input-variants.d.ts.map +1 -0
- package/lib/types/index.d.ts +19 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/paste-images.d.ts +73 -0
- package/lib/types/paste-images.d.ts.map +1 -0
- package/lib/types/paths.d.ts +117 -0
- package/lib/types/paths.d.ts.map +1 -0
- package/lib/types/plugin-update.d.ts +119 -0
- package/lib/types/plugin-update.d.ts.map +1 -0
- package/lib/types/runtime-install.d.ts +92 -0
- package/lib/types/runtime-install.d.ts.map +1 -0
- package/lib/types/runtime-manager.d.ts +60 -0
- package/lib/types/runtime-manager.d.ts.map +1 -0
- package/lib/types/runtime.d.ts +412 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/skill.d.ts +14 -0
- package/lib/types/skill.d.ts.map +1 -0
- package/lib/types/tools.d.ts +35 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/upstream.d.ts +233 -0
- package/lib/types/upstream.d.ts.map +1 -0
- package/lib/types/version.d.ts +15 -0
- package/lib/types/version.d.ts.map +1 -0
- package/lib/types/web-request.d.ts +11 -0
- package/lib/types/web-request.d.ts.map +1 -0
- package/lib/types/web.d.ts +118 -0
- package/lib/types/web.d.ts.map +1 -0
- package/lib/upstream.js +860 -0
- package/lib/upstream.js.map +1 -0
- package/lib/version.js +18 -0
- package/lib/version.js.map +1 -0
- package/lib/web-request.js +29 -0
- package/lib/web-request.js.map +1 -0
- package/lib/web.js +462 -0
- package/lib/web.js.map +1 -0
- package/package.json +149 -0
- package/patches/vision-tools-dsh.patch +970 -0
- package/runtime/requirements.lock +3 -0
- package/src/artifact-access.ts +386 -0
- package/src/artifacts.ts +85 -0
- package/src/client/display-config.ts +62 -0
- package/src/client/index.tsx +1580 -0
- package/src/client/model-variants-hider.ts +159 -0
- package/src/client/paste-images.tsx +747 -0
- package/src/config.ts +290 -0
- package/src/defaults.ts +5 -0
- package/src/errors.ts +62 -0
- package/src/evidence-cache.ts +418 -0
- package/src/exposure.ts +265 -0
- package/src/image-input-variants.ts +996 -0
- package/src/index.ts +141 -0
- package/src/paste-images.ts +286 -0
- package/src/paths.ts +383 -0
- package/src/plugin-update.ts +1143 -0
- package/src/runtime-install.ts +1161 -0
- package/src/runtime-manager.ts +174 -0
- package/src/runtime.ts +2189 -0
- package/src/skill.ts +35 -0
- package/src/tools.ts +691 -0
- package/src/upstream.ts +1078 -0
- package/src/version.ts +37 -0
- package/src/web-request.ts +27 -0
- package/src/web.ts +594 -0
- package/vendor/agent-vision-toolkit/CHANGELOG.md +30 -0
- package/vendor/agent-vision-toolkit/LICENSE +21 -0
- package/vendor/agent-vision-toolkit/README.md +405 -0
- package/vendor/agent-vision-toolkit/UPSTREAM_MANIFEST.json +94 -0
- package/vendor/agent-vision-toolkit/bin/crop +90 -0
- package/vendor/agent-vision-toolkit/bin/detect +13 -0
- package/vendor/agent-vision-toolkit/bin/glance +93 -0
- package/vendor/agent-vision-toolkit/bin/ground +13 -0
- package/vendor/agent-vision-toolkit/bin/trace +129 -0
- package/vendor/agent-vision-toolkit/detect.py +60 -0
- package/vendor/agent-vision-toolkit/ground.py +267 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/dominant_colors.py +224 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/extract_fg.py +278 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/html_shot.py +420 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/long_screenshot_ocr.py +1245 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/pixel_diff.py +88 -0
- package/vendor/agent-vision-toolkit/tests/test_vision_client.py +450 -0
- package/vendor/agent-vision-toolkit/vision_client.py +310 -0
|
@@ -0,0 +1,1580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Vision Toolkit browser plugin: dedicated Tool cards plus the Settings,
|
|
3
|
+
* health, connection-test, and safe Artifact preview experience.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
useEffect,
|
|
8
|
+
useState,
|
|
9
|
+
useSyncExternalStore,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from 'react'
|
|
12
|
+
import { Button, Input } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
13
|
+
import type { ClientContext, ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client'
|
|
14
|
+
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
|
15
|
+
import type {} from '@deepseek-ai/dsh-credentials/types'
|
|
16
|
+
import type {} from '@deepseek-ai/dsh-settings/types'
|
|
17
|
+
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
|
19
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
20
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
21
|
+
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
22
|
+
import { installPasteImages } from './paste-images.tsx'
|
|
23
|
+
import { installModelVariantsHider } from './model-variants-hider.ts'
|
|
24
|
+
import { resetDisplayConfigCache } from './display-config.ts'
|
|
25
|
+
|
|
26
|
+
const NS = 'vision-toolkit'
|
|
27
|
+
const SETTINGS_ROUTE = '/_dsh/vision-toolkit/settings'
|
|
28
|
+
const PRESENTATION_META_KEY = '$dshVisionToolkit'
|
|
29
|
+
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
|
|
30
|
+
// Keep these browser defaults aligned with src/defaults.ts without importing server-side config.
|
|
31
|
+
const BUILT_IN_FREE_VISION_BASE_URL = 'https://vision.anionex.me/v1'
|
|
32
|
+
const BUILT_IN_FREE_VISION_CREDENTIAL = 'ANIONEX_FREE_VISION'
|
|
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
|
+
|
|
37
|
+
const en = {
|
|
38
|
+
nav: 'Vision',
|
|
39
|
+
settingsTitle: 'Vision Toolkit',
|
|
40
|
+
settingsIntro: 'Configure the pinned visual engineering runtime, its external vision endpoint, and local safety limits.',
|
|
41
|
+
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
|
+
provider: 'Vision service',
|
|
43
|
+
providerHint: 'Choose the API protocol, then provide the service address, model, and API key used by online vision features.',
|
|
44
|
+
aihubmixTutorial: 'Need an AIHubMix key for free Gemini 3.7 Flash vision? Follow the signup guide →',
|
|
45
|
+
baseUrl: 'Base URL',
|
|
46
|
+
apiKey: 'API key',
|
|
47
|
+
apiKeyPlaceholderMissing: 'Paste the API key',
|
|
48
|
+
apiKeyPlaceholderConfigured: 'Saved; leave blank to keep it',
|
|
49
|
+
apiKeyHint: 'The key is stored in DSH Credentials and is never shown again after saving.',
|
|
50
|
+
apiKeyLocked: 'The current key comes from a read-only source and cannot be replaced here.',
|
|
51
|
+
apiKeyBlank: 'The API key cannot contain only spaces.',
|
|
52
|
+
apiKeyInvalid: 'Paste only the key, without a variable name, quotes, spaces, or line breaks.',
|
|
53
|
+
credential: 'Credential name',
|
|
54
|
+
credentialHint: 'The built-in free provider needs no user key. For a custom provider, this is the DSH credential reference used to store its key.',
|
|
55
|
+
model: 'Model',
|
|
56
|
+
protocol: 'API protocol',
|
|
57
|
+
anthropicThinking: 'Anthropic thinking',
|
|
58
|
+
anthropicThinkingHint: 'omit has the broadest compatibility. Use disabled or adaptive only when the selected model documents that mode; restore omit first after HTTP 400.',
|
|
59
|
+
userAgent: 'User-Agent',
|
|
60
|
+
language: 'Output language',
|
|
61
|
+
limits: 'Limits',
|
|
62
|
+
timeout: 'Request timeout (ms)',
|
|
63
|
+
maxBytes: 'Maximum image bytes',
|
|
64
|
+
maxPixels: 'Maximum image pixels',
|
|
65
|
+
concurrency: 'Concurrent calls per session',
|
|
66
|
+
runtime: 'Runtime',
|
|
67
|
+
runtimeMode: 'Runtime mode',
|
|
68
|
+
toolkitPath: 'Pinned checkout path',
|
|
69
|
+
python: 'Python override',
|
|
70
|
+
allowedDirs: 'Additional allowed directories',
|
|
71
|
+
allowedDirsHint: 'One path per line. The session workspace is always allowed.',
|
|
72
|
+
save: 'Save and apply',
|
|
73
|
+
saving: 'Validating runtime…',
|
|
74
|
+
reload: 'Reload',
|
|
75
|
+
saved: 'Settings validated and applied.',
|
|
76
|
+
readOnly: 'Service settings are read-only. A writable API key can still be saved.',
|
|
77
|
+
configured: 'Configured',
|
|
78
|
+
missing: 'Missing',
|
|
79
|
+
source: 'Source',
|
|
80
|
+
sourceHint: '{source}: {value}',
|
|
81
|
+
sourceEnv: 'Environment variable',
|
|
82
|
+
sourceFile: 'Credential file',
|
|
83
|
+
health: 'Health',
|
|
84
|
+
runHealth: 'Run health check',
|
|
85
|
+
testConnection: 'Test API connection',
|
|
86
|
+
testModel: 'Test vision model',
|
|
87
|
+
testing: 'Checking…',
|
|
88
|
+
testingModel: 'Testing model…',
|
|
89
|
+
connectionHint: 'The API connection test only queries GET /models. The vision model test sends the bundled diagnostic image and verifies one real multimodal request.',
|
|
90
|
+
saveBeforeTesting: 'Save service changes before testing the connection.',
|
|
91
|
+
advanced: 'Advanced settings',
|
|
92
|
+
advancedHint: 'Credential name, provider compatibility, output language, resource limits, runtime source, Python, and additional readable directories.',
|
|
93
|
+
imageInput: 'Image input',
|
|
94
|
+
hiddenVariants: 'Transparent variant routing',
|
|
95
|
+
hiddenVariantsLabel: 'Keep the original model names and enable images automatically',
|
|
96
|
+
hiddenVariantsHint: 'Text-only models keep one model-selector entry with the original name while the session runs on the image-capable variant. Pasted images, image history, and the built-in read_image tool keep working; disable to restore the explicit (Vision Toolkit) entries.',
|
|
97
|
+
pluginVersion: 'Plugin',
|
|
98
|
+
upstreamVersion: 'Upstream',
|
|
99
|
+
activeGeneration: 'Runtime generation',
|
|
100
|
+
activeGenerationValue: 'Generation {generation}',
|
|
101
|
+
updates: 'Plugin updates',
|
|
102
|
+
updatesHint: 'Check npm for a newer release, install it into this DSH profile, and restart DSH Web automatically.',
|
|
103
|
+
manualUpdate: 'Manual update',
|
|
104
|
+
manualUpdateHint: 'Run this command in your terminal to install the latest release into this DSH profile.',
|
|
105
|
+
copy: 'Copy',
|
|
106
|
+
copied: 'Copied',
|
|
107
|
+
checkUpdate: 'Check for updates',
|
|
108
|
+
checkingUpdate: 'Checking for updates…',
|
|
109
|
+
updateAvailable: 'Update available',
|
|
110
|
+
updateAvailableDetail: 'Version {version} is available. It will restart DSH Web automatically when safe; otherwise you will be asked to restart it manually.',
|
|
111
|
+
upToDate: 'Up to date',
|
|
112
|
+
upToDateDetail: 'Version {version} is the latest release.',
|
|
113
|
+
updateNow: 'Install update',
|
|
114
|
+
updatingPlugin: 'Installing update…',
|
|
115
|
+
updateConfirm: 'Install Vision Toolkit {version} now? DSH Web will restart automatically when supported; otherwise a manual restart will be required.',
|
|
116
|
+
restarting: 'Version {version} was installed. Waiting for DSH Web to restart…',
|
|
117
|
+
manualRestartRequired: 'Version {version} was installed. Restart DSH Web through your usual command or process manager to activate it.',
|
|
118
|
+
updateProfile: 'Profile',
|
|
119
|
+
updateInstalled: 'Installed',
|
|
120
|
+
updateLatest: 'Latest',
|
|
121
|
+
updateUnsupported: 'In-app updates are unavailable for this installation.',
|
|
122
|
+
updateReasonProfileNotFound: 'The running plugin could not be matched to a DSH profile installation.',
|
|
123
|
+
updateReasonNotDependency: 'The plugin is not a direct dependency of this DSH profile.',
|
|
124
|
+
updateReasonLocalSource: 'This profile uses a local, workspace, URL, or git installation; update that source manually so local work is not overwritten.',
|
|
125
|
+
updateReasonReadOnly: 'The profile package manifest is read-only.',
|
|
126
|
+
updateReasonPnpm: 'pnpm is unavailable in the DSH execution environment.',
|
|
127
|
+
updateReasonPlatform: 'Automatic restart is unavailable on this operating system.',
|
|
128
|
+
updateReasonRestartUnmanaged: 'Detached self-restart is disabled. Use a supported process manager, or explicitly opt in with DSH_VISION_TOOLKIT_ALLOW_DETACHED_RESTART=1 for an unsupervised Web process.',
|
|
129
|
+
updateReasonRestartAddress: 'Automatic restart is unavailable when DSH Web uses an unknown or dynamically allocated port. Start it with a fixed --port value.',
|
|
130
|
+
updateSaveFirst: 'Save or discard the current Settings and API key changes before updating the plugin.',
|
|
131
|
+
restartTimedOut: 'DSH Web did not return with the target plugin version. Check the restart log and restart the Web profile through its original process manager.',
|
|
132
|
+
restartRolledBack: 'The new plugin did not become ready, so the previous version was restored. Check the restart log before trying again.',
|
|
133
|
+
pluginKind: 'DSH native plugin',
|
|
134
|
+
runtimeUnavailable: 'Runtime unavailable',
|
|
135
|
+
runtimeCandidateRejected: 'Last runtime candidate was rejected; the active generation remains available.',
|
|
136
|
+
runtimeReady: 'Ready',
|
|
137
|
+
runtimeManaged: 'Managed',
|
|
138
|
+
runtimeExternal: 'External checkout',
|
|
139
|
+
retry: 'Retry',
|
|
140
|
+
open: 'Open file',
|
|
141
|
+
download: 'Download',
|
|
142
|
+
previewUnavailable: 'HTTP preview is unavailable in this host; use Open file.',
|
|
143
|
+
running: 'Running…',
|
|
144
|
+
failed: 'Failed',
|
|
145
|
+
matches: 'matches',
|
|
146
|
+
elements: 'elements',
|
|
147
|
+
dimensions: 'Dimensions',
|
|
148
|
+
coordinates: 'Coordinates',
|
|
149
|
+
artifact: 'Artifact',
|
|
150
|
+
artifacts: 'Artifacts',
|
|
151
|
+
difference: 'Overall difference',
|
|
152
|
+
worstRegions: 'Worst regions',
|
|
153
|
+
colors: 'Dominant colors',
|
|
154
|
+
noResult: 'Structured result unavailable; inspect the raw Tool result.',
|
|
155
|
+
healthy: 'Healthy',
|
|
156
|
+
degraded: 'Needs attention',
|
|
157
|
+
notTested: 'Not tested',
|
|
158
|
+
groundTitle: 'Ground',
|
|
159
|
+
detectTitle: 'Detect',
|
|
160
|
+
traceTitle: 'Trace SVG',
|
|
161
|
+
pixelDiffTitle: 'Pixel Diff',
|
|
162
|
+
cropTitle: 'Crop',
|
|
163
|
+
longOcrTitle: 'Long OCR',
|
|
164
|
+
extractForegroundTitle: 'Extract Foreground',
|
|
165
|
+
htmlScreenshotTitle: 'HTML Screenshot',
|
|
166
|
+
artifactTitle: 'Vision Artifact',
|
|
167
|
+
dominantColorsTitle: 'Dominant Colors',
|
|
168
|
+
artifactGroundPreview: 'Grounding bounding-box preview',
|
|
169
|
+
artifactDetectPreview: 'Detected-element bounding-box preview',
|
|
170
|
+
artifactCrop: 'Cropped image region',
|
|
171
|
+
artifactTrace: 'Traced vector geometry',
|
|
172
|
+
artifactDiffHeatmap: 'Pixel-difference heatmap',
|
|
173
|
+
artifactDiffReport: 'Structured pixel-difference report',
|
|
174
|
+
artifactLongManifest: 'Long-screenshot split and merge manifest',
|
|
175
|
+
artifactLongTranscript: 'Merged long-screenshot OCR transcript',
|
|
176
|
+
artifactLongAudit: 'Long-screenshot OCR boundary audit',
|
|
177
|
+
artifactLongChunk: 'Long-screenshot OCR chunk {index}',
|
|
178
|
+
artifactOcrSidecar: 'OCR sidecar for chunk {index}',
|
|
179
|
+
artifactForeground: 'Extracted transparent foreground',
|
|
180
|
+
artifactHtmlScreenshot: 'Headless browser screenshot of local HTML',
|
|
181
|
+
label: 'Label',
|
|
182
|
+
paths: 'paths',
|
|
183
|
+
healthPython: 'Python',
|
|
184
|
+
healthDependencies: 'Dependencies',
|
|
185
|
+
healthChrome: 'Browser',
|
|
186
|
+
healthCredential: 'Credential',
|
|
187
|
+
healthArtifactDirectory: 'Artifact directory',
|
|
188
|
+
healthTempDirectory: 'Temporary directory',
|
|
189
|
+
healthService: 'Vision service',
|
|
190
|
+
healthModel: 'Vision model',
|
|
191
|
+
statusOk: 'OK',
|
|
192
|
+
statusWarning: 'Warning',
|
|
193
|
+
statusError: 'Error',
|
|
194
|
+
statusNotTested: 'Not tested',
|
|
195
|
+
positiveInteger: '{field} must be a positive integer.',
|
|
196
|
+
healthPythonDetail: '{version} via {path}',
|
|
197
|
+
healthChromeMissing: 'Chrome, Chromium, or Edge was not found; HTML Screenshot is unavailable.',
|
|
198
|
+
healthChromeProbeFailed: 'Could not check whether a supported browser is available.',
|
|
199
|
+
healthCredentialMissing: 'Credential {credential} is not configured.',
|
|
200
|
+
healthCredentialReady: 'Credential {credential} is available.',
|
|
201
|
+
healthCredentialFailed: 'Could not read credential {credential}.',
|
|
202
|
+
healthDirectoryWritable: '{directory} is writable: {path}',
|
|
203
|
+
healthDirectoryNotWritable: '{directory} is not writable: {path}',
|
|
204
|
+
healthArtifactDirectoryFailed: 'Could not prepare the artifact directory.',
|
|
205
|
+
healthConnectionNotTested: 'API connection not tested. Use Test API connection to query /models.',
|
|
206
|
+
healthConnectionCredentialMissing: 'Connection test skipped because the credential is unavailable.',
|
|
207
|
+
healthServiceResponded: 'Service responded at {endpoint} (HTTP {status}).',
|
|
208
|
+
healthServiceRejectedCredential: 'Service rejected the configured credential (HTTP {status}).',
|
|
209
|
+
healthServiceForbidden: 'Service is reachable, but GET /models is restricted (HTTP {status}). This is often an account or model-list permission limit, not an invalid key; you can ignore this warning when the vision-model test reports success.',
|
|
210
|
+
healthServiceNoModels: 'Service is reachable but does not support GET /models (HTTP {status}).',
|
|
211
|
+
healthServiceRateLimited: 'Service is reachable, but the connection test was rate-limited (HTTP 429).',
|
|
212
|
+
healthServiceHttpFailed: 'Connection test failed with HTTP {status}.',
|
|
213
|
+
healthServiceUnreachable: 'Could not reach {endpoint}.',
|
|
214
|
+
healthModelNotTested: 'Vision model not tested. Run Test vision model to make one real multimodal request.',
|
|
215
|
+
healthModelCredentialMissing: 'Vision model test skipped because the credential is unavailable.',
|
|
216
|
+
healthModelReady: 'Model {model} completed a real multimodal request.',
|
|
217
|
+
healthModelFailed: 'Real multimodal request failed: {detail}',
|
|
218
|
+
modelTestVerifiedTag: 'Verified',
|
|
219
|
+
modelTestNotRunTag: 'Not tested',
|
|
220
|
+
modelTestFailedTag: 'Test failed',
|
|
221
|
+
} as const
|
|
222
|
+
|
|
223
|
+
type LocaleKey = keyof typeof en
|
|
224
|
+
|
|
225
|
+
const zh: Record<LocaleKey, string> = {
|
|
226
|
+
nav: '视觉工具',
|
|
227
|
+
settingsTitle: '视觉工具箱',
|
|
228
|
+
settingsIntro: '在这里设置视觉模型服务、工具运行环境,以及图片和文件的本地访问范围。',
|
|
229
|
+
externalNotice: '使用图像理解、目标定位、界面检测或文字识别等在线功能时,所选图片会发送到下方配置的视觉服务。图片裁剪、轮廓描摹、像素对比、主色提取、前景提取和网页截图均在本机完成,不会上传图片。',
|
|
230
|
+
provider: '在线视觉服务',
|
|
231
|
+
providerHint: '选择接口协议后,填写在线视觉功能使用的 API 地址、模型名称和 API 密钥。',
|
|
232
|
+
aihubmixTutorial: '想申请 AIHubMix Key,并用免费 Gemini 3.7 Flash 识图?看这篇图文教程 →',
|
|
233
|
+
baseUrl: 'API 地址',
|
|
234
|
+
apiKey: 'API 密钥',
|
|
235
|
+
apiKeyPlaceholderMissing: '粘贴 API 密钥',
|
|
236
|
+
apiKeyPlaceholderConfigured: '已保存;留空表示不修改',
|
|
237
|
+
apiKeyHint: '密钥会保存到 DSH 凭据存储,保存后不会在页面中回显。',
|
|
238
|
+
apiKeyLocked: '当前密钥来自只读配置,无法在此替换。',
|
|
239
|
+
apiKeyBlank: 'API 密钥不能只包含空格。',
|
|
240
|
+
apiKeyInvalid: '请只粘贴密钥本身,不要包含变量名、引号、空格或换行。',
|
|
241
|
+
credential: '凭据名称',
|
|
242
|
+
credentialHint: '内置免费视觉服务无需用户密钥;切换到自定义服务时,此处是保存其密钥的 DSH 凭据名称。',
|
|
243
|
+
model: '模型名称',
|
|
244
|
+
protocol: 'API 协议',
|
|
245
|
+
anthropicThinking: 'Anthropic thinking',
|
|
246
|
+
anthropicThinkingHint: 'omit 兼容性最好。仅当所选模型明确支持时使用 disabled 或 adaptive;遇到 HTTP 400 时先恢复 omit。',
|
|
247
|
+
userAgent: 'User-Agent',
|
|
248
|
+
language: '结果语言',
|
|
249
|
+
limits: '资源与并发限制',
|
|
250
|
+
timeout: '单次请求超时(毫秒)',
|
|
251
|
+
maxBytes: '单张图片大小上限(字节)',
|
|
252
|
+
maxPixels: '单张图片最大像素数',
|
|
253
|
+
concurrency: '单个会话最多并发任务数',
|
|
254
|
+
runtime: '工具运行环境',
|
|
255
|
+
runtimeMode: '环境来源',
|
|
256
|
+
toolkitPath: 'agent-vision-toolkit 目录',
|
|
257
|
+
python: 'Python 解释器(可选)',
|
|
258
|
+
allowedDirs: '允许读取的其他目录',
|
|
259
|
+
allowedDirsHint: '每行填写一个目录。当前会话的工作目录始终可以读取,无需重复填写。',
|
|
260
|
+
save: '保存设置',
|
|
261
|
+
saving: '正在检查并应用…',
|
|
262
|
+
reload: '重新加载',
|
|
263
|
+
saved: '设置已保存并生效。',
|
|
264
|
+
readOnly: '服务设置来自只读配置;如果 API 密钥可写,仍可在此保存密钥。',
|
|
265
|
+
configured: '已就绪',
|
|
266
|
+
missing: '未配置',
|
|
267
|
+
source: '配置来源',
|
|
268
|
+
sourceHint: '{source}:{value}',
|
|
269
|
+
sourceEnv: '环境变量',
|
|
270
|
+
sourceFile: '凭据文件',
|
|
271
|
+
health: '运行检查',
|
|
272
|
+
runHealth: '检查本地环境',
|
|
273
|
+
testConnection: '测试 API 连接',
|
|
274
|
+
testModel: '测试视觉模型',
|
|
275
|
+
testing: '检查中…',
|
|
276
|
+
testingModel: '正在测试模型…',
|
|
277
|
+
connectionHint: '“测试 API 连接”只请求 GET /models;“测试视觉模型”会发送插件自带的诊断图片,验证一次真实多模态调用。',
|
|
278
|
+
saveBeforeTesting: '修改服务配置后,请先保存,再执行 API 或视觉模型测试。',
|
|
279
|
+
advanced: '高级设置',
|
|
280
|
+
advancedHint: '凭据名称、服务兼容参数、结果语言、资源限制、运行环境来源、Python 和额外可读目录。一般无需修改。',
|
|
281
|
+
imageInput: '图片输入',
|
|
282
|
+
hiddenVariants: '透明变体路由',
|
|
283
|
+
hiddenVariantsLabel: '保留原模型名并自动启用图片能力',
|
|
284
|
+
hiddenVariantsHint: '文本模型在模型列表中只显示原名称,会话实际运行在支持图片的变体路由上:粘贴图片、历史图片和内置 read_image 工具均可正常使用。关闭后恢复显示显式的(Vision Toolkit)条目。',
|
|
285
|
+
pluginVersion: '插件版本',
|
|
286
|
+
upstreamVersion: '工具包版本',
|
|
287
|
+
activeGeneration: '本次运行已应用',
|
|
288
|
+
activeGenerationValue: '{generation} 次',
|
|
289
|
+
updates: '插件更新',
|
|
290
|
+
updatesHint: '检查 npm 新版本,自动更新当前 DSH Profile 中的插件,然后重启 DSH Web。',
|
|
291
|
+
manualUpdate: '手动更新',
|
|
292
|
+
manualUpdateHint: '在终端运行以下命令,将当前 DSH Profile 更新到最新版本。',
|
|
293
|
+
copy: '复制',
|
|
294
|
+
copied: '已复制',
|
|
295
|
+
checkUpdate: '检查更新',
|
|
296
|
+
checkingUpdate: '正在检查更新…',
|
|
297
|
+
updateAvailable: '发现新版本',
|
|
298
|
+
updateAvailableDetail: '可更新到 {version}。能安全自重启时会自动重启,否则安装完成后会提示你手动重启。',
|
|
299
|
+
upToDate: '已是最新版',
|
|
300
|
+
upToDateDetail: '当前 {version} 已是最新正式版本。',
|
|
301
|
+
updateNow: '安装更新',
|
|
302
|
+
updatingPlugin: '正在安装更新…',
|
|
303
|
+
updateConfirm: '现在安装 Vision Toolkit {version} 吗?支持安全自重启时会自动重启,否则需要你手动重启 DSH Web。',
|
|
304
|
+
restarting: '已安装 {version},正在等待 DSH Web 重启…',
|
|
305
|
+
manualRestartRequired: '已安装 {version}。请按你平时的方式手动重启 DSH Web,重启后新版本生效。',
|
|
306
|
+
updateProfile: 'Profile',
|
|
307
|
+
updateInstalled: '当前版本',
|
|
308
|
+
updateLatest: '最新版本',
|
|
309
|
+
updateUnsupported: '当前安装方式不支持页面内更新。',
|
|
310
|
+
updateReasonProfileNotFound: '无法把正在运行的插件匹配到某个 DSH Profile 安装。',
|
|
311
|
+
updateReasonNotDependency: '该插件不是当前 DSH Profile 的直接依赖。',
|
|
312
|
+
updateReasonLocalSource: '当前使用本地、workspace、URL 或 git 安装;为避免覆盖本地修改,请手动更新对应来源。',
|
|
313
|
+
updateReasonReadOnly: '当前 Profile 的 package.json 不可写。',
|
|
314
|
+
updateReasonPnpm: 'DSH 运行环境中找不到 pnpm。',
|
|
315
|
+
updateReasonPlatform: '当前操作系统不支持安全的自动重启。',
|
|
316
|
+
updateReasonRestartUnmanaged: '默认禁用脱离原进程管理器的自重启。仅对无人监管的 Web 进程明确设置 DSH_VISION_TOOLKIT_ALLOW_DETACHED_RESTART=1 后开放。',
|
|
317
|
+
updateReasonRestartAddress: 'DSH Web 使用未知端口或动态端口时无法安全自动重启。请用固定的 --port 值启动。',
|
|
318
|
+
updateSaveFirst: '更新插件前,请先保存或放弃当前 Settings 和 API 密钥修改。',
|
|
319
|
+
restartTimedOut: 'DSH Web 未能以目标插件版本恢复。请检查重启日志,并通过原进程管理器重启 Web Profile。',
|
|
320
|
+
restartRolledBack: '新插件未能就绪,系统已恢复上一版本。再次尝试前请检查重启日志。',
|
|
321
|
+
pluginKind: 'DSH 原生插件',
|
|
322
|
+
runtimeUnavailable: '运行环境尚未就绪',
|
|
323
|
+
runtimeCandidateRejected: '新设置未能生效,仍在使用上一次可用的设置。',
|
|
324
|
+
runtimeReady: '已就绪',
|
|
325
|
+
runtimeManaged: '自动安装',
|
|
326
|
+
runtimeExternal: '本地源码',
|
|
327
|
+
retry: '重试',
|
|
328
|
+
open: '在工作区中打开',
|
|
329
|
+
download: '下载',
|
|
330
|
+
previewUnavailable: '此页面无法直接预览该文件,请在工作区中打开。',
|
|
331
|
+
running: '运行中…',
|
|
332
|
+
failed: '运行失败',
|
|
333
|
+
matches: '处匹配',
|
|
334
|
+
elements: '个元素',
|
|
335
|
+
dimensions: '图片尺寸',
|
|
336
|
+
coordinates: '坐标',
|
|
337
|
+
artifact: '生成文件',
|
|
338
|
+
artifacts: '个生成文件',
|
|
339
|
+
difference: '像素差异',
|
|
340
|
+
worstRegions: '差异最大的区域',
|
|
341
|
+
colors: '种颜色',
|
|
342
|
+
noResult: '未能读取结果,请查看工具的原始输出。',
|
|
343
|
+
healthy: '一切正常',
|
|
344
|
+
degraded: '有项目需要处理',
|
|
345
|
+
notTested: '尚未检查',
|
|
346
|
+
groundTitle: '目标定位',
|
|
347
|
+
detectTitle: '界面元素识别',
|
|
348
|
+
traceTitle: '描摹为 SVG',
|
|
349
|
+
pixelDiffTitle: '像素对比',
|
|
350
|
+
cropTitle: '裁剪图片',
|
|
351
|
+
longOcrTitle: '长图文字识别',
|
|
352
|
+
extractForegroundTitle: '提取前景',
|
|
353
|
+
htmlScreenshotTitle: '网页截图',
|
|
354
|
+
artifactTitle: '视觉处理结果',
|
|
355
|
+
dominantColorsTitle: '主色提取',
|
|
356
|
+
artifactGroundPreview: '目标定位框预览',
|
|
357
|
+
artifactDetectPreview: '界面元素标注预览',
|
|
358
|
+
artifactCrop: '裁剪后的图片',
|
|
359
|
+
artifactTrace: '描摹得到的矢量图',
|
|
360
|
+
artifactDiffHeatmap: '像素差异热力图',
|
|
361
|
+
artifactDiffReport: '像素差异详细报告',
|
|
362
|
+
artifactLongManifest: '长图切分与合并记录',
|
|
363
|
+
artifactLongTranscript: '长图文字识别结果',
|
|
364
|
+
artifactLongAudit: '长图分块边界检查记录',
|
|
365
|
+
artifactLongChunk: '长图文字识别分块 {index}',
|
|
366
|
+
artifactOcrSidecar: '分块 {index} 的文字识别记录',
|
|
367
|
+
artifactForeground: '提取后的透明背景前景图',
|
|
368
|
+
artifactHtmlScreenshot: '本地网页截图',
|
|
369
|
+
label: '名称',
|
|
370
|
+
paths: '条路径',
|
|
371
|
+
healthPython: 'Python',
|
|
372
|
+
healthDependencies: 'Python 依赖',
|
|
373
|
+
healthChrome: '浏览器',
|
|
374
|
+
healthCredential: 'API 密钥',
|
|
375
|
+
healthArtifactDirectory: '输出目录',
|
|
376
|
+
healthTempDirectory: '临时目录',
|
|
377
|
+
healthService: '视觉服务',
|
|
378
|
+
healthModel: '视觉模型',
|
|
379
|
+
statusOk: '正常',
|
|
380
|
+
statusWarning: '注意',
|
|
381
|
+
statusError: '异常',
|
|
382
|
+
statusNotTested: '未检查',
|
|
383
|
+
positiveInteger: '{field}必须填写正整数。',
|
|
384
|
+
healthPythonDetail: '版本 {version};解释器:{path}',
|
|
385
|
+
healthChromeMissing: '未找到 Chrome、Chromium 或 Edge,网页截图功能暂不可用。',
|
|
386
|
+
healthChromeProbeFailed: '无法检查浏览器是否可用。',
|
|
387
|
+
healthCredentialMissing: '尚未配置凭据 {credential}。',
|
|
388
|
+
healthCredentialReady: '已找到凭据 {credential}。',
|
|
389
|
+
healthCredentialFailed: '无法读取凭据 {credential}。',
|
|
390
|
+
healthDirectoryWritable: '{directory}可写:{path}',
|
|
391
|
+
healthDirectoryNotWritable: '{directory}不可写:{path}',
|
|
392
|
+
healthArtifactDirectoryFailed: '无法准备输出目录。',
|
|
393
|
+
healthConnectionNotTested: '尚未测试 API 连接。点击“测试 API 连接”可请求 /models。',
|
|
394
|
+
healthConnectionCredentialMissing: 'API 密钥不可用,未执行连接测试。',
|
|
395
|
+
healthServiceResponded: '服务已响应:{endpoint}(HTTP {status})。',
|
|
396
|
+
healthServiceRejectedCredential: '服务拒绝了当前 API 密钥(HTTP {status})。',
|
|
397
|
+
healthServiceForbidden: '服务可以访问,但对 GET /models 的访问被限制(HTTP {status})。这通常是账号或模型列表权限限制,不代表密钥无效;若“视觉模型已实测正常”,此警告可忽略。',
|
|
398
|
+
healthServiceNoModels: '服务可以访问,但不支持 GET /models(HTTP {status})。',
|
|
399
|
+
healthServiceRateLimited: '服务可以访问,但本次连接测试触发了限流(HTTP 429)。',
|
|
400
|
+
healthServiceHttpFailed: '连接测试失败(HTTP {status})。',
|
|
401
|
+
healthServiceUnreachable: '无法连接到 {endpoint}。',
|
|
402
|
+
healthModelNotTested: '尚未测试视觉模型。点击“测试视觉模型”可执行一次真实多模态请求。',
|
|
403
|
+
healthModelCredentialMissing: '视觉模型测试已跳过,因为当前 API 密钥不可用。',
|
|
404
|
+
healthModelReady: '模型 {model} 已完成一次真实多模态请求。',
|
|
405
|
+
healthModelFailed: '真实多模态请求失败:{detail}',
|
|
406
|
+
modelTestVerifiedTag: '已实测',
|
|
407
|
+
modelTestNotRunTag: '未测试',
|
|
408
|
+
modelTestFailedTag: '测试失败',
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
type Translate = (key: LocaleKey, params?: Record<string, unknown>) => string
|
|
412
|
+
|
|
413
|
+
interface ToolCallOwnerProps {
|
|
414
|
+
callId: string
|
|
415
|
+
toolName: string
|
|
416
|
+
block: ToolCallBlock
|
|
417
|
+
cwd?: string | undefined
|
|
418
|
+
openFile: (path: string) => void
|
|
419
|
+
inspect?: (() => void) | undefined
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
423
|
+
interface SlotMap {
|
|
424
|
+
/** Keyed atomic Tool call view, dispatched by wire Tool name. */
|
|
425
|
+
'tool.call.toolview': { kind: 'keyed'; scope: 'session'; owner: ToolCallOwnerProps }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
interface LocaleNamespaceMap {
|
|
429
|
+
/** DSH Vision Toolkit Tool cards and Settings copy. */
|
|
430
|
+
'vision-toolkit': LocaleKey
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
type ToolCallViewProps = PropsRuntime<'tool.call.toolview'>
|
|
435
|
+
|
|
436
|
+
interface ArtifactDescriptor {
|
|
437
|
+
path: string
|
|
438
|
+
filename: string
|
|
439
|
+
mimeType: string
|
|
440
|
+
kind: 'image' | 'svg' | 'markdown' | 'json'
|
|
441
|
+
description: string
|
|
442
|
+
sourceTool: string
|
|
443
|
+
previewIntent: 'image' | 'svg' | 'text' | 'download'
|
|
444
|
+
bytes: number
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
interface ArtifactGrant {
|
|
448
|
+
path: string
|
|
449
|
+
previewUrl: string
|
|
450
|
+
downloadUrl: string
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
interface HealthCheck {
|
|
454
|
+
status: 'ok' | 'warning' | 'error' | 'not_tested'
|
|
455
|
+
detail: string
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
interface HealthResult {
|
|
459
|
+
pluginVersion: string
|
|
460
|
+
checks: Record<string, HealthCheck>
|
|
461
|
+
healthy: boolean
|
|
462
|
+
connectionTested: boolean
|
|
463
|
+
modelTested: boolean
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
interface SettingsValue {
|
|
467
|
+
provider?: {
|
|
468
|
+
baseUrl?: string
|
|
469
|
+
credential?: string
|
|
470
|
+
model?: string
|
|
471
|
+
protocol?: 'openai' | 'anthropic'
|
|
472
|
+
anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
|
|
473
|
+
userAgent?: string
|
|
474
|
+
}
|
|
475
|
+
language?: 'zh' | 'en'
|
|
476
|
+
timeoutMs?: number
|
|
477
|
+
maxImageBytes?: number
|
|
478
|
+
maxImagePixels?: number
|
|
479
|
+
concurrency?: number
|
|
480
|
+
runtime?: { mode?: 'managed' | 'external'; agentVisionToolkitPath?: string; python?: string }
|
|
481
|
+
allowedDirs?: string[]
|
|
482
|
+
imageInputVariants?: {
|
|
483
|
+
enabled?: boolean
|
|
484
|
+
providers?: string[]
|
|
485
|
+
autoSwitch?: boolean
|
|
486
|
+
hidden?: boolean
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
type PluginUpdateUnavailableReason =
|
|
491
|
+
| 'profile-not-found'
|
|
492
|
+
| 'not-direct-dependency'
|
|
493
|
+
| 'unsupported-install-source'
|
|
494
|
+
| 'profile-read-only'
|
|
495
|
+
| 'pnpm-unavailable'
|
|
496
|
+
| 'unsupported-platform'
|
|
497
|
+
| 'restart-unmanaged'
|
|
498
|
+
| 'restart-address-unavailable'
|
|
499
|
+
|
|
500
|
+
interface PluginUpdateCapability {
|
|
501
|
+
supported: boolean
|
|
502
|
+
checkSupported?: boolean
|
|
503
|
+
profile?: string
|
|
504
|
+
dependencySpec?: string
|
|
505
|
+
reason?: PluginUpdateUnavailableReason
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
interface PluginUpdateCheck extends PluginUpdateCapability {
|
|
509
|
+
currentVersion: string
|
|
510
|
+
latestVersion?: string
|
|
511
|
+
updateAvailable: boolean
|
|
512
|
+
checkedAt: string
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
type PluginUpdateResult = {
|
|
516
|
+
fromVersion: string
|
|
517
|
+
toVersion: string
|
|
518
|
+
profile: string
|
|
519
|
+
restarting: true
|
|
520
|
+
retryAfterMs: number
|
|
521
|
+
manualRestartRequired?: false
|
|
522
|
+
} | {
|
|
523
|
+
fromVersion: string
|
|
524
|
+
toVersion: string
|
|
525
|
+
profile: string
|
|
526
|
+
restarting: false
|
|
527
|
+
manualRestartRequired: true
|
|
528
|
+
retryAfterMs?: undefined
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
interface SettingsSnapshot {
|
|
532
|
+
schemaVersion: 1
|
|
533
|
+
writable: boolean
|
|
534
|
+
settings: { value: SettingsValue; revision: number; applies: 'live' }
|
|
535
|
+
credential: { ref: string; configured: boolean; source?: string; writable: boolean }
|
|
536
|
+
runtime: {
|
|
537
|
+
ready: boolean
|
|
538
|
+
generation: number
|
|
539
|
+
activeConfig?: SettingsValue
|
|
540
|
+
upstream?: {
|
|
541
|
+
source: 'managed' | 'external'
|
|
542
|
+
path: string
|
|
543
|
+
runtimeHome: string
|
|
544
|
+
python: string
|
|
545
|
+
pythonVersion: string
|
|
546
|
+
}
|
|
547
|
+
lastError?: string
|
|
548
|
+
}
|
|
549
|
+
release: {
|
|
550
|
+
pluginVersion: string
|
|
551
|
+
upstreamRepository: string
|
|
552
|
+
upstreamVersion: string
|
|
553
|
+
upstreamCommit: string
|
|
554
|
+
update: PluginUpdateCapability
|
|
555
|
+
}
|
|
556
|
+
artifactRouteAvailable: boolean
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
interface ApiSuccess<T> { ok: true; value: T }
|
|
560
|
+
interface ApiFailure { ok: false; error: { code: string; message: string } }
|
|
561
|
+
|
|
562
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
563
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function textOfContent(block: ToolCallBlock): string {
|
|
567
|
+
if (!('kind' in block)) return ''
|
|
568
|
+
return block.content
|
|
569
|
+
.filter((entry): entry is Extract<typeof entry, { type: 'text' }> => entry.type === 'text')
|
|
570
|
+
.map(entry => entry.text)
|
|
571
|
+
.join('\n')
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Decode canonical presentation metadata with a JSON-text fallback. */
|
|
575
|
+
export function decodeVisionResult(block: ToolCallBlock): Record<string, unknown> | undefined {
|
|
576
|
+
if (!('kind' in block) || block.isError) return undefined
|
|
577
|
+
if (isRecord(block.meta)) return block.meta
|
|
578
|
+
const text = textOfContent(block).trim()
|
|
579
|
+
if (text.length === 0) return undefined
|
|
580
|
+
try {
|
|
581
|
+
const parsed = JSON.parse(text) as unknown
|
|
582
|
+
return isRecord(parsed) ? parsed : undefined
|
|
583
|
+
} catch {
|
|
584
|
+
return undefined
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function accessMap(value: Record<string, unknown> | undefined): Map<string, ArtifactGrant> {
|
|
589
|
+
const map = new Map<string, ArtifactGrant>()
|
|
590
|
+
if (value === undefined) return map
|
|
591
|
+
const envelope = value[PRESENTATION_META_KEY]
|
|
592
|
+
if (!isRecord(envelope) || envelope.schemaVersion !== 1 || !Array.isArray(envelope.artifacts)) return map
|
|
593
|
+
for (const entry of envelope.artifacts) {
|
|
594
|
+
if (!isRecord(entry) || typeof entry.path !== 'string' || typeof entry.previewUrl !== 'string' || typeof entry.downloadUrl !== 'string') continue
|
|
595
|
+
map.set(entry.path, entry as unknown as ArtifactGrant)
|
|
596
|
+
}
|
|
597
|
+
return map
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function artifactFrom(value: unknown): ArtifactDescriptor | undefined {
|
|
601
|
+
if (!isRecord(value)) return undefined
|
|
602
|
+
if (
|
|
603
|
+
typeof value.path !== 'string'
|
|
604
|
+
|| typeof value.filename !== 'string'
|
|
605
|
+
|| typeof value.mimeType !== 'string'
|
|
606
|
+
|| (value.kind !== 'image' && value.kind !== 'svg' && value.kind !== 'markdown' && value.kind !== 'json')
|
|
607
|
+
|| typeof value.description !== 'string'
|
|
608
|
+
|| typeof value.sourceTool !== 'string'
|
|
609
|
+
|| (value.previewIntent !== 'image' && value.previewIntent !== 'svg' && value.previewIntent !== 'text' && value.previewIntent !== 'download')
|
|
610
|
+
|| typeof value.bytes !== 'number'
|
|
611
|
+
) return undefined
|
|
612
|
+
return value as unknown as ArtifactDescriptor
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function collectArtifacts(value: unknown, found = new Map<string, ArtifactDescriptor>(), depth = 0): ArtifactDescriptor[] {
|
|
616
|
+
if (depth > 16) return [...found.values()]
|
|
617
|
+
const artifact = artifactFrom(value)
|
|
618
|
+
if (artifact !== undefined) {
|
|
619
|
+
found.set(artifact.path, artifact)
|
|
620
|
+
return [...found.values()]
|
|
621
|
+
}
|
|
622
|
+
if (Array.isArray(value)) {
|
|
623
|
+
for (const entry of value) collectArtifacts(entry, found, depth + 1)
|
|
624
|
+
} else if (isRecord(value)) {
|
|
625
|
+
for (const entry of Object.values(value)) collectArtifacts(entry, found, depth + 1)
|
|
626
|
+
}
|
|
627
|
+
return [...found.values()]
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function numberOf(value: unknown): number | undefined {
|
|
631
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function stringOf(value: unknown): string | undefined {
|
|
635
|
+
return typeof value === 'string' ? value : undefined
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function formatBytes(bytes: number): string {
|
|
639
|
+
if (bytes < 1024) return `${bytes} B`
|
|
640
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
641
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function boxText(value: unknown): string {
|
|
645
|
+
if (!isRecord(value)) return '—'
|
|
646
|
+
const parts = ['x1', 'y1', 'x2', 'y2'].map(key => numberOf(value[key]))
|
|
647
|
+
return parts.every(part => part !== undefined) ? parts.join(', ') : '—'
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function statusText(block: ToolCallBlock, t: Translate): string | undefined {
|
|
651
|
+
if (!('kind' in block)) return t('running')
|
|
652
|
+
if (block.isError) return textOfContent(block).split('\n')[0] || t('failed')
|
|
653
|
+
return undefined
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function VisionIcon({ kind = 'scan' }: { kind?: 'scan' | 'target' | 'layers' | 'shape' | 'diff' | 'palette' }) {
|
|
657
|
+
const path = kind === 'target'
|
|
658
|
+
? 'M8 2v2m0 8v2M2 8h2m8 0h2M5 5h6v6H5z'
|
|
659
|
+
: kind === 'layers'
|
|
660
|
+
? 'm3 6 5-3 5 3-5 3-5-3Zm0 3 5 3 5-3M3 12l5 3 5-3'
|
|
661
|
+
: kind === 'shape'
|
|
662
|
+
? 'M3 12 6 4l7-1-1 7-9 2Zm3-8 6 6'
|
|
663
|
+
: kind === 'diff'
|
|
664
|
+
? 'M3 3h4v4H3V3Zm6 6h4v4H9V9Zm0-6h4M3 11h4'
|
|
665
|
+
: kind === 'palette'
|
|
666
|
+
? 'M8 2a6 6 0 1 0 0 12h1.2a1.3 1.3 0 0 0 0-2.6H8a1.5 1.5 0 0 1 0-3h3.5A2.5 2.5 0 0 0 14 5.9C13.2 3.6 10.9 2 8 2Z'
|
|
667
|
+
: 'M3 5V3h2M11 3h2v2M13 11v2h-2M5 13H3v-2M5 8h6'
|
|
668
|
+
return (
|
|
669
|
+
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.35" strokeLinecap="round" strokeLinejoin="round">
|
|
670
|
+
<path d={path} />
|
|
671
|
+
</svg>
|
|
672
|
+
)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function ToolShell({
|
|
676
|
+
block, title, summary, icon, children, t,
|
|
677
|
+
}: {
|
|
678
|
+
block: ToolCallBlock
|
|
679
|
+
title: string
|
|
680
|
+
summary?: string | undefined
|
|
681
|
+
icon: ReactNode
|
|
682
|
+
children?: ReactNode | undefined
|
|
683
|
+
t: Translate
|
|
684
|
+
}) {
|
|
685
|
+
const [open, setOpen] = useState(true)
|
|
686
|
+
const status = statusText(block, t)
|
|
687
|
+
const expandable = children !== undefined && children !== null
|
|
688
|
+
return (
|
|
689
|
+
<section className="dvt-tool" data-state={!('kind' in block) ? 'running' : block.isError ? 'error' : 'success'}>
|
|
690
|
+
<button type="button" className="dvt-tool-head" onClick={() => { if (expandable) setOpen(value => !value) }} aria-expanded={expandable ? open : undefined}>
|
|
691
|
+
<span className="dvt-tool-icon">{icon}</span>
|
|
692
|
+
<span className="dvt-tool-title">{title}</span>
|
|
693
|
+
{summary !== undefined && summary.length > 0 ? <span className="dvt-tool-sep" aria-hidden="true">·</span> : null}
|
|
694
|
+
{summary !== undefined ? <span className="dvt-tool-summary">{summary}</span> : null}
|
|
695
|
+
{status !== undefined ? <span className="dvt-tool-status">{status}</span> : null}
|
|
696
|
+
{expandable ? <span className="dvt-chevron" data-open={open || undefined}>⌄</span> : null}
|
|
697
|
+
</button>
|
|
698
|
+
{expandable && open ? <div className="dvt-tool-body">{children}</div> : null}
|
|
699
|
+
</section>
|
|
700
|
+
)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function ArtifactActions({ artifact, grant, openFile, t }: {
|
|
704
|
+
artifact: ArtifactDescriptor
|
|
705
|
+
grant?: ArtifactGrant | undefined
|
|
706
|
+
openFile: (path: string) => void
|
|
707
|
+
t: Translate
|
|
708
|
+
}) {
|
|
709
|
+
return (
|
|
710
|
+
<div className="dvt-actions">
|
|
711
|
+
<Button size="sm" variant="outline" onClick={() => { openFile(artifact.path) }}>{t('open')}</Button>
|
|
712
|
+
{grant === undefined ? null : <a className="dvt-download" href={grant.downloadUrl} download={artifact.filename}>{t('download')}</a>}
|
|
713
|
+
</div>
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function ArtifactPreview({ artifact, grant, openFile, t }: {
|
|
718
|
+
artifact: ArtifactDescriptor
|
|
719
|
+
grant?: ArtifactGrant | undefined
|
|
720
|
+
openFile: (path: string) => void
|
|
721
|
+
t: Translate
|
|
722
|
+
}) {
|
|
723
|
+
const canPreview = grant !== undefined && (artifact.kind === 'image' || artifact.kind === 'svg')
|
|
724
|
+
const description = artifactDescription(artifact.description, t)
|
|
725
|
+
return (
|
|
726
|
+
<article className="dvt-artifact">
|
|
727
|
+
{canPreview
|
|
728
|
+
? artifact.kind === 'svg'
|
|
729
|
+
? <iframe className="dvt-preview dvt-svg" sandbox="" src={grant.previewUrl} title={description} />
|
|
730
|
+
: <img className="dvt-preview" src={grant.previewUrl} alt={description} loading="lazy" />
|
|
731
|
+
: null}
|
|
732
|
+
<div className="dvt-artifact-meta">
|
|
733
|
+
<div>
|
|
734
|
+
<strong>{artifact.filename}</strong>
|
|
735
|
+
<span>{description}</span>
|
|
736
|
+
<small>{artifact.mimeType} · {formatBytes(artifact.bytes)}</small>
|
|
737
|
+
</div>
|
|
738
|
+
<ArtifactActions artifact={artifact} grant={grant} openFile={openFile} t={t} />
|
|
739
|
+
</div>
|
|
740
|
+
{!canPreview && grant === undefined ? <p className="dvt-muted">{t('previewUnavailable')}</p> : null}
|
|
741
|
+
</article>
|
|
742
|
+
)
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const ARTIFACT_DESCRIPTION_KEYS: Record<string, LocaleKey> = {
|
|
746
|
+
'Grounding bounding-box preview': 'artifactGroundPreview',
|
|
747
|
+
'Detected-element bounding-box preview': 'artifactDetectPreview',
|
|
748
|
+
'Cropped image region': 'artifactCrop',
|
|
749
|
+
'Traced vector geometry': 'artifactTrace',
|
|
750
|
+
'Pixel-difference heatmap': 'artifactDiffHeatmap',
|
|
751
|
+
'Structured pixel-difference report': 'artifactDiffReport',
|
|
752
|
+
'Long-screenshot split and merge manifest': 'artifactLongManifest',
|
|
753
|
+
'Merged long-screenshot OCR transcript': 'artifactLongTranscript',
|
|
754
|
+
'Long-screenshot OCR boundary audit': 'artifactLongAudit',
|
|
755
|
+
'Extracted transparent foreground': 'artifactForeground',
|
|
756
|
+
'Headless browser screenshot of local HTML': 'artifactHtmlScreenshot',
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function artifactDescription(description: string, t: Translate): string {
|
|
760
|
+
const key = ARTIFACT_DESCRIPTION_KEYS[description]
|
|
761
|
+
if (key !== undefined) {
|
|
762
|
+
const translated = t(key)
|
|
763
|
+
return translated === key ? description : translated
|
|
764
|
+
}
|
|
765
|
+
let match = /^Long-screenshot OCR chunk (\d+)$/u.exec(description)
|
|
766
|
+
if (match !== null) {
|
|
767
|
+
const translated = t('artifactLongChunk', { index: match[1] })
|
|
768
|
+
return translated === 'artifactLongChunk' ? description : translated
|
|
769
|
+
}
|
|
770
|
+
match = /^OCR sidecar for chunk (\d+)$/u.exec(description)
|
|
771
|
+
if (match !== null) {
|
|
772
|
+
const translated = t('artifactOcrSidecar', { index: match[1] })
|
|
773
|
+
return translated === 'artifactOcrSidecar' ? description : translated
|
|
774
|
+
}
|
|
775
|
+
return description
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
type ViewProps = ToolCallViewProps & { t?: Translate }
|
|
779
|
+
|
|
780
|
+
function GroundView({ block, openFile, t = key => en[key] }: ViewProps) {
|
|
781
|
+
const value = decodeVisionResult(block)
|
|
782
|
+
const matches = Array.isArray(value?.matches) ? value.matches.filter(isRecord) : []
|
|
783
|
+
const target = stringOf(value?.target) ?? t('groundTitle')
|
|
784
|
+
const width = numberOf(value?.imageWidth)
|
|
785
|
+
const height = numberOf(value?.imageHeight)
|
|
786
|
+
const preview = artifactFrom(value?.preview)
|
|
787
|
+
const grants = accessMap(value)
|
|
788
|
+
return (
|
|
789
|
+
<ToolShell block={block} title={t('groundTitle')} summary={matches.length > 0 ? `${target} · ${matches.length} ${t('matches')}` : target} icon={<VisionIcon kind="target" />} t={t}>
|
|
790
|
+
{value === undefined ? <p className="dvt-muted">{t('noResult')}</p> : (
|
|
791
|
+
<div className="dvt-stack">
|
|
792
|
+
<div className="dvt-metrics">
|
|
793
|
+
<div><span>{t('dimensions')}</span><strong>{width ?? '—'} × {height ?? '—'}</strong></div>
|
|
794
|
+
<div><span>{t('coordinates')}</span><strong>{matches[0] === undefined ? '—' : boxText(matches[0].box)}</strong></div>
|
|
795
|
+
</div>
|
|
796
|
+
{matches.length > 1 ? (
|
|
797
|
+
<ol className="dvt-list">{matches.map((match, index) => <li key={index}><span>{stringOf(match.label) ?? `#${index + 1}`}</span><code>{boxText(match.box)}</code></li>)}</ol>
|
|
798
|
+
) : null}
|
|
799
|
+
{preview === undefined ? null : <ArtifactPreview artifact={preview} grant={grants.get(preview.path)} openFile={openFile} t={t} />}
|
|
800
|
+
</div>
|
|
801
|
+
)}
|
|
802
|
+
</ToolShell>
|
|
803
|
+
)
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function DetectView({ block, openFile, t = key => en[key] }: ViewProps) {
|
|
807
|
+
const value = decodeVisionResult(block)
|
|
808
|
+
const elements = Array.isArray(value?.elements) ? value.elements.filter(isRecord) : []
|
|
809
|
+
const width = numberOf(value?.imageWidth)
|
|
810
|
+
const height = numberOf(value?.imageHeight)
|
|
811
|
+
const preview = artifactFrom(value?.preview)
|
|
812
|
+
const grants = accessMap(value)
|
|
813
|
+
return (
|
|
814
|
+
<ToolShell block={block} title={t('detectTitle')} summary={`${elements.length} ${t('elements')}`} icon={<VisionIcon kind="layers" />} t={t}>
|
|
815
|
+
{value === undefined ? <p className="dvt-muted">{t('noResult')}</p> : (
|
|
816
|
+
<div className="dvt-stack">
|
|
817
|
+
<div className="dvt-metrics">
|
|
818
|
+
<div><span>{t('dimensions')}</span><strong>{width ?? '—'} × {height ?? '—'}</strong></div>
|
|
819
|
+
<div><span>{t('elements')}</span><strong>{elements.length}</strong></div>
|
|
820
|
+
</div>
|
|
821
|
+
<div className="dvt-table-wrap"><table className="dvt-table"><thead><tr><th>#</th><th>{t('label')}</th><th>{t('coordinates')}</th></tr></thead><tbody>
|
|
822
|
+
{elements.map((element, index) => <tr key={index}><td>{numberOf(element.index) ?? index + 1}</td><td>{stringOf(element.label) ?? '—'}</td><td><code>{boxText(element.box)}</code></td></tr>)}
|
|
823
|
+
</tbody></table></div>
|
|
824
|
+
{preview === undefined ? null : <ArtifactPreview artifact={preview} grant={grants.get(preview.path)} openFile={openFile} t={t} />}
|
|
825
|
+
</div>
|
|
826
|
+
)}
|
|
827
|
+
</ToolShell>
|
|
828
|
+
)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function TraceView({ block, openFile, t = key => en[key] }: ViewProps) {
|
|
832
|
+
const value = decodeVisionResult(block)
|
|
833
|
+
const artifact = artifactFrom(value?.artifact)
|
|
834
|
+
const geometry = isRecord(value?.geometry) ? value.geometry : undefined
|
|
835
|
+
const summary = geometry === undefined ? undefined : `${numberOf(geometry.pathCount) ?? 0} ${t('paths')} · ${formatBytes(numberOf(geometry.bytes) ?? 0)}`
|
|
836
|
+
const grants = accessMap(value)
|
|
837
|
+
return (
|
|
838
|
+
<ToolShell block={block} title={t('traceTitle')} summary={summary} icon={<VisionIcon kind="shape" />} t={t}>
|
|
839
|
+
{artifact === undefined ? <p className="dvt-muted">{t('noResult')}</p> : <ArtifactPreview artifact={artifact} grant={grants.get(artifact.path)} openFile={openFile} t={t} />}
|
|
840
|
+
</ToolShell>
|
|
841
|
+
)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function PixelDiffView({ block, openFile, t = key => en[key] }: ViewProps) {
|
|
845
|
+
const value = decodeVisionResult(block)
|
|
846
|
+
const pct = numberOf(value?.overallDifferencePct)
|
|
847
|
+
const regions = Array.isArray(value?.worstRegions) ? value.worstRegions.filter(isRecord) : []
|
|
848
|
+
const heatmap = artifactFrom(value?.heatmap)
|
|
849
|
+
const report = artifactFrom(value?.report)
|
|
850
|
+
const grants = accessMap(value)
|
|
851
|
+
return (
|
|
852
|
+
<ToolShell block={block} title={t('pixelDiffTitle')} summary={pct === undefined ? undefined : `${pct.toFixed(3)}%`} icon={<VisionIcon kind="diff" />} t={t}>
|
|
853
|
+
{value === undefined ? <p className="dvt-muted">{t('noResult')}</p> : (
|
|
854
|
+
<div className="dvt-stack">
|
|
855
|
+
<div className="dvt-diff-score"><span>{t('difference')}</span><strong>{pct?.toFixed(4) ?? '—'}%</strong><div><i style={{ width: `${Math.min(100, Math.max(0, pct ?? 0))}%` }} /></div></div>
|
|
856
|
+
{regions.length === 0 ? null : <div><h4>{t('worstRegions')}</h4><ol className="dvt-list">{regions.map((region, index) => <li key={index}><span>{(numberOf(region.differencePct) ?? 0).toFixed(3)}%</span><code>{boxText(region.box)}</code></li>)}</ol></div>}
|
|
857
|
+
{heatmap === undefined ? null : <ArtifactPreview artifact={heatmap} grant={grants.get(heatmap.path)} openFile={openFile} t={t} />}
|
|
858
|
+
{report === undefined ? null : <ArtifactPreview artifact={report} grant={grants.get(report.path)} openFile={openFile} t={t} />}
|
|
859
|
+
</div>
|
|
860
|
+
)}
|
|
861
|
+
</ToolShell>
|
|
862
|
+
)
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function ArtifactView({ block, openFile, toolName, t = key => en[key] }: ViewProps) {
|
|
866
|
+
const value = decodeVisionResult(block)
|
|
867
|
+
const artifacts = collectArtifacts(value)
|
|
868
|
+
const grants = accessMap(value)
|
|
869
|
+
const title = toolName === 'vision_crop' ? t('cropTitle')
|
|
870
|
+
: toolName === 'vision_long_screenshot_ocr' ? t('longOcrTitle')
|
|
871
|
+
: toolName === 'vision_extract_foreground' ? t('extractForegroundTitle')
|
|
872
|
+
: toolName === 'vision_html_screenshot' ? t('htmlScreenshotTitle')
|
|
873
|
+
: t('artifactTitle')
|
|
874
|
+
return (
|
|
875
|
+
<ToolShell block={block} title={title} summary={artifacts.length > 0 ? `${artifacts.length} ${t('artifacts')}` : undefined} icon={<VisionIcon />} t={t}>
|
|
876
|
+
{artifacts.length === 0 ? <p className="dvt-muted">{t('noResult')}</p> : <div className="dvt-stack">{artifacts.map(artifact => <ArtifactPreview key={artifact.path} artifact={artifact} grant={grants.get(artifact.path)} openFile={openFile} t={t} />)}</div>}
|
|
877
|
+
</ToolShell>
|
|
878
|
+
)
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function PaletteView({ block, t = key => en[key] }: ViewProps) {
|
|
882
|
+
const value = decodeVisionResult(block)
|
|
883
|
+
const analysis = isRecord(value?.analysis) ? value.analysis : undefined
|
|
884
|
+
const colors = Array.isArray(analysis?.colors) ? analysis.colors.filter(isRecord) : []
|
|
885
|
+
return (
|
|
886
|
+
<ToolShell block={block} title={t('dominantColorsTitle')} summary={`${colors.length} ${t('colors')}`} icon={<VisionIcon kind="palette" />} t={t}>
|
|
887
|
+
{colors.length === 0 ? <p className="dvt-muted">{t('noResult')}</p> : <div className="dvt-palette">{colors.map((color, index) => {
|
|
888
|
+
const hex = stringOf(color.color) ?? '#000000'
|
|
889
|
+
const share = numberOf(color.sharePct)
|
|
890
|
+
return <div key={`${hex}-${index}`}><i style={{ background: hex }} /><span><strong>{hex}</strong><small>{share === undefined ? '' : `${share.toFixed(2)}%`}</small></span></div>
|
|
891
|
+
})}</div>}
|
|
892
|
+
</ToolShell>
|
|
893
|
+
)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async function apiRequest<T>(init?: RequestInit): Promise<T> {
|
|
897
|
+
const response = await fetch(SETTINGS_ROUTE, { credentials: 'same-origin', ...init })
|
|
898
|
+
const body = await response.json() as ApiSuccess<T> | ApiFailure
|
|
899
|
+
if (!response.ok || !body.ok) {
|
|
900
|
+
const failure = body as ApiFailure
|
|
901
|
+
throw new Error(failure.error?.message ?? `Vision Toolkit request failed with HTTP ${response.status}`)
|
|
902
|
+
}
|
|
903
|
+
return body.value
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
interface SettingsState {
|
|
907
|
+
status: 'idle' | 'loading' | 'ready' | 'error'
|
|
908
|
+
snapshot?: SettingsSnapshot | undefined
|
|
909
|
+
health?: HealthResult | undefined
|
|
910
|
+
update?: PluginUpdateCheck | undefined
|
|
911
|
+
restart?: PluginUpdateResult | undefined
|
|
912
|
+
action?: 'save' | 'health' | 'connection' | 'model' | 'check-update' | 'apply-update' | undefined
|
|
913
|
+
message?: string | undefined
|
|
914
|
+
error?: string | undefined
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** Small external store shared by the Settings route and pushed invalidations. */
|
|
918
|
+
export class VisionSettingsController {
|
|
919
|
+
private state: SettingsState = { status: 'idle' }
|
|
920
|
+
private listeners = new Set<() => void>()
|
|
921
|
+
private generation = 0
|
|
922
|
+
|
|
923
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
924
|
+
this.listeners.add(listener)
|
|
925
|
+
return () => { this.listeners.delete(listener) }
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
snapshot = (): SettingsState => this.state
|
|
929
|
+
|
|
930
|
+
private set(next: SettingsState): void {
|
|
931
|
+
this.state = next
|
|
932
|
+
for (const listener of this.listeners) listener()
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
async load(): Promise<void> {
|
|
936
|
+
const generation = ++this.generation
|
|
937
|
+
this.set({ ...this.state, status: 'loading', error: undefined, message: undefined })
|
|
938
|
+
try {
|
|
939
|
+
const snapshot = await apiRequest<SettingsSnapshot>()
|
|
940
|
+
if (generation !== this.generation) return
|
|
941
|
+
this.set({
|
|
942
|
+
status: 'ready',
|
|
943
|
+
snapshot,
|
|
944
|
+
health: this.state.health,
|
|
945
|
+
update: this.state.update,
|
|
946
|
+
restart: this.state.restart,
|
|
947
|
+
})
|
|
948
|
+
} catch (error) {
|
|
949
|
+
if (generation !== this.generation) return
|
|
950
|
+
this.set({ ...this.state, status: 'error', error: error instanceof Error ? error.message : String(error) })
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
refreshIfLoaded(): void {
|
|
955
|
+
if (this.state.status === 'idle' || this.state.action === 'save') return
|
|
956
|
+
void this.load()
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
async save(
|
|
960
|
+
value: SettingsValue,
|
|
961
|
+
expectedRevision: number,
|
|
962
|
+
credentialValue: string | undefined,
|
|
963
|
+
writeSettings: boolean,
|
|
964
|
+
): Promise<boolean> {
|
|
965
|
+
this.set({ ...this.state, action: 'save', error: undefined, message: undefined })
|
|
966
|
+
let snapshot = this.state.snapshot
|
|
967
|
+
try {
|
|
968
|
+
if (writeSettings) {
|
|
969
|
+
snapshot = await apiRequest<SettingsSnapshot>({
|
|
970
|
+
method: 'POST',
|
|
971
|
+
headers: { 'Content-Type': 'application/json' },
|
|
972
|
+
body: JSON.stringify({ action: 'save', expectedRevision, value }),
|
|
973
|
+
})
|
|
974
|
+
}
|
|
975
|
+
if (snapshot === undefined) throw new Error('Vision Toolkit Settings are unavailable')
|
|
976
|
+
if (credentialValue !== undefined) {
|
|
977
|
+
try {
|
|
978
|
+
snapshot = await apiRequest<SettingsSnapshot>({
|
|
979
|
+
method: 'POST',
|
|
980
|
+
headers: { 'Content-Type': 'application/json' },
|
|
981
|
+
body: JSON.stringify({
|
|
982
|
+
action: 'credential',
|
|
983
|
+
expectedRevision: snapshot.settings.revision,
|
|
984
|
+
ref: snapshot.credential.ref,
|
|
985
|
+
value: credentialValue,
|
|
986
|
+
}),
|
|
987
|
+
})
|
|
988
|
+
} catch (error) {
|
|
989
|
+
this.set({
|
|
990
|
+
status: 'ready',
|
|
991
|
+
snapshot,
|
|
992
|
+
health: this.state.health,
|
|
993
|
+
update: this.state.update,
|
|
994
|
+
restart: this.state.restart,
|
|
995
|
+
error: error instanceof Error ? error.message : String(error),
|
|
996
|
+
})
|
|
997
|
+
return false
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
this.set({
|
|
1001
|
+
status: 'ready',
|
|
1002
|
+
snapshot,
|
|
1003
|
+
health: this.state.health,
|
|
1004
|
+
update: this.state.update,
|
|
1005
|
+
restart: this.state.restart,
|
|
1006
|
+
message: 'saved',
|
|
1007
|
+
})
|
|
1008
|
+
return true
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) })
|
|
1011
|
+
return false
|
|
1012
|
+
} finally {
|
|
1013
|
+
// The backend commits the generation before the response is readable, so
|
|
1014
|
+
// the browser cache must not keep serving the previous hidden flag.
|
|
1015
|
+
resetDisplayConfigCache()
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async runHealth(mode: 'health' | 'connection' | 'model'): Promise<void> {
|
|
1020
|
+
const testConnection = mode !== 'health'
|
|
1021
|
+
const testModel = mode === 'model'
|
|
1022
|
+
this.set({ ...this.state, action: mode, error: undefined, message: undefined })
|
|
1023
|
+
try {
|
|
1024
|
+
const health = await apiRequest<HealthResult>({
|
|
1025
|
+
method: 'POST',
|
|
1026
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1027
|
+
body: JSON.stringify({ action: 'health', testConnection, testModel }),
|
|
1028
|
+
})
|
|
1029
|
+
this.set({ ...this.state, action: undefined, health })
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) })
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
async checkUpdate(): Promise<void> {
|
|
1036
|
+
this.set({ ...this.state, action: 'check-update', error: undefined, message: undefined })
|
|
1037
|
+
try {
|
|
1038
|
+
const update = await apiRequest<PluginUpdateCheck>({
|
|
1039
|
+
method: 'POST',
|
|
1040
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1041
|
+
body: JSON.stringify({ action: 'check-update' }),
|
|
1042
|
+
})
|
|
1043
|
+
this.set({ ...this.state, action: undefined, update })
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) })
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async applyUpdate(expectedVersion: string): Promise<void> {
|
|
1050
|
+
this.set({ ...this.state, action: 'apply-update', error: undefined, message: undefined })
|
|
1051
|
+
try {
|
|
1052
|
+
const result = await apiRequest<PluginUpdateResult>({
|
|
1053
|
+
method: 'POST',
|
|
1054
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1055
|
+
body: JSON.stringify({ action: 'apply-update', expectedVersion }),
|
|
1056
|
+
})
|
|
1057
|
+
this.set({
|
|
1058
|
+
...this.state,
|
|
1059
|
+
action: undefined,
|
|
1060
|
+
restart: result,
|
|
1061
|
+
message: result.restarting ? 'restarting' : 'manual-restart-required',
|
|
1062
|
+
})
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) })
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
reportRestartTimeout(message: string): void {
|
|
1069
|
+
this.set({ ...this.state, restart: undefined, message: undefined, error: message })
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
interface Draft {
|
|
1074
|
+
baseUrl: string
|
|
1075
|
+
credential: string
|
|
1076
|
+
model: string
|
|
1077
|
+
protocol: 'openai' | 'anthropic'
|
|
1078
|
+
anthropicThinking: 'omit' | 'disabled' | 'adaptive'
|
|
1079
|
+
userAgent: string
|
|
1080
|
+
language: 'zh' | 'en'
|
|
1081
|
+
timeoutMs: string
|
|
1082
|
+
maxImageBytes: string
|
|
1083
|
+
maxImagePixels: string
|
|
1084
|
+
concurrency: string
|
|
1085
|
+
runtimeMode: 'managed' | 'external'
|
|
1086
|
+
toolkitPath: string
|
|
1087
|
+
python: string
|
|
1088
|
+
allowedDirs: string
|
|
1089
|
+
hiddenVariants: boolean
|
|
1090
|
+
variantEnabled: boolean
|
|
1091
|
+
variantProviders: string
|
|
1092
|
+
variantAutoSwitch: boolean
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function draftOf(value: SettingsValue): Draft {
|
|
1096
|
+
return {
|
|
1097
|
+
baseUrl: value.provider?.baseUrl ?? BUILT_IN_FREE_VISION_BASE_URL,
|
|
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,
|
|
1103
|
+
language: value.language ?? 'zh',
|
|
1104
|
+
timeoutMs: String(value.timeoutMs ?? 30000),
|
|
1105
|
+
maxImageBytes: String(value.maxImageBytes ?? 4194304),
|
|
1106
|
+
maxImagePixels: String(value.maxImagePixels ?? 20000000),
|
|
1107
|
+
concurrency: String(value.concurrency ?? 4),
|
|
1108
|
+
runtimeMode: value.runtime?.mode ?? 'managed',
|
|
1109
|
+
toolkitPath: value.runtime?.agentVisionToolkitPath ?? '',
|
|
1110
|
+
python: value.runtime?.python ?? '',
|
|
1111
|
+
allowedDirs: (value.allowedDirs ?? []).join('\n'),
|
|
1112
|
+
hiddenVariants: value.imageInputVariants?.hidden ?? true,
|
|
1113
|
+
variantEnabled: value.imageInputVariants?.enabled ?? true,
|
|
1114
|
+
variantProviders: (value.imageInputVariants?.providers ?? []).join('\n'),
|
|
1115
|
+
variantAutoSwitch: value.imageInputVariants?.autoSwitch ?? true,
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function positiveInteger(raw: string, label: string, t: Translate): number {
|
|
1120
|
+
const value = Number(raw)
|
|
1121
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(t('positiveInteger', { field: label }))
|
|
1122
|
+
return value
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function apiKeyFailure(value: string, t: Translate): string | undefined {
|
|
1126
|
+
if (value.length === 0) return undefined
|
|
1127
|
+
const trimmed = value.trim()
|
|
1128
|
+
if (trimmed.length === 0) return t('apiKeyBlank')
|
|
1129
|
+
const quoted = trimmed.length > 1 && ['"', '\'', '`'].includes(trimmed[0] ?? '') && trimmed.endsWith(trimmed[0] ?? '')
|
|
1130
|
+
const environmentLine = /^[A-Z][A-Z0-9_]*=[^=]/u.test(trimmed)
|
|
1131
|
+
if (quoted || environmentLine || !/^[\x21-\x7E]+$/u.test(trimmed)) return t('apiKeyInvalid')
|
|
1132
|
+
return undefined
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function valueOf(draft: Draft, t: Translate): SettingsValue {
|
|
1136
|
+
return {
|
|
1137
|
+
provider: {
|
|
1138
|
+
baseUrl: draft.baseUrl.trim(),
|
|
1139
|
+
credential: draft.credential.trim(),
|
|
1140
|
+
model: draft.model.trim(),
|
|
1141
|
+
protocol: draft.protocol,
|
|
1142
|
+
anthropicThinking: draft.anthropicThinking,
|
|
1143
|
+
userAgent: draft.userAgent.trim(),
|
|
1144
|
+
},
|
|
1145
|
+
language: draft.language,
|
|
1146
|
+
timeoutMs: positiveInteger(draft.timeoutMs, t('timeout'), t),
|
|
1147
|
+
maxImageBytes: positiveInteger(draft.maxImageBytes, t('maxBytes'), t),
|
|
1148
|
+
maxImagePixels: positiveInteger(draft.maxImagePixels, t('maxPixels'), t),
|
|
1149
|
+
concurrency: positiveInteger(draft.concurrency, t('concurrency'), t),
|
|
1150
|
+
runtime: {
|
|
1151
|
+
mode: draft.runtimeMode,
|
|
1152
|
+
...(draft.runtimeMode === 'external' ? { agentVisionToolkitPath: draft.toolkitPath.trim() } : {}),
|
|
1153
|
+
...(draft.python.trim().length === 0 ? {} : { python: draft.python.trim() }),
|
|
1154
|
+
},
|
|
1155
|
+
allowedDirs: draft.allowedDirs.split(/\r?\n/).map(entry => entry.trim()).filter(Boolean),
|
|
1156
|
+
imageInputVariants: {
|
|
1157
|
+
...(draft.variantEnabled ? {} : { enabled: false }),
|
|
1158
|
+
...(draft.variantProviders.trim().length === 0 ? {} : {
|
|
1159
|
+
providers: draft.variantProviders.split(/\r?\n/).map(entry => entry.trim()).filter(Boolean),
|
|
1160
|
+
}),
|
|
1161
|
+
...(draft.variantAutoSwitch ? {} : { autoSwitch: false }),
|
|
1162
|
+
hidden: draft.hiddenVariants,
|
|
1163
|
+
},
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function settingsDraftChanged(draft: Draft, saved: SettingsValue, t: Translate): boolean {
|
|
1168
|
+
try {
|
|
1169
|
+
return JSON.stringify(valueOf(draft, t)) !== JSON.stringify(valueOf(draftOf(saved), t))
|
|
1170
|
+
} catch {
|
|
1171
|
+
return true
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
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
|
+
interface SettingsInjected {
|
|
1183
|
+
controller: VisionSettingsController
|
|
1184
|
+
t: Translate
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
type SettingsProps = PropsRuntime<'settings.section'> & Partial<SettingsInjected>
|
|
1188
|
+
|
|
1189
|
+
function Field({ label, children, hint }: { label: string; children: ReactNode; hint?: string | undefined }) {
|
|
1190
|
+
return <label className="dvt-field"><span>{label}</span>{children}{hint === undefined ? null : <small>{hint}</small>}</label>
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function SettingsSection({ controller, t }: SettingsProps) {
|
|
1194
|
+
if (controller === undefined || t === undefined) return null
|
|
1195
|
+
return <LoadedSettings controller={controller} t={t} />
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const HEALTH_NAME_KEYS: Record<string, LocaleKey> = {
|
|
1199
|
+
python: 'healthPython',
|
|
1200
|
+
dependencies: 'healthDependencies',
|
|
1201
|
+
chrome: 'healthChrome',
|
|
1202
|
+
credential: 'healthCredential',
|
|
1203
|
+
artifactDirectory: 'healthArtifactDirectory',
|
|
1204
|
+
tempDirectory: 'healthTempDirectory',
|
|
1205
|
+
service: 'healthService',
|
|
1206
|
+
model: 'healthModel',
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const HEALTH_STATUS_KEYS: Record<HealthCheck['status'], LocaleKey> = {
|
|
1210
|
+
ok: 'statusOk',
|
|
1211
|
+
warning: 'statusWarning',
|
|
1212
|
+
error: 'statusError',
|
|
1213
|
+
not_tested: 'statusNotTested',
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function healthDetail(name: string, detail: string, t: Translate): string {
|
|
1217
|
+
if (name === 'python') {
|
|
1218
|
+
const match = /^(.+) via (.+)$/u.exec(detail)
|
|
1219
|
+
if (match !== null) return t('healthPythonDetail', { version: match[1], path: match[2] })
|
|
1220
|
+
}
|
|
1221
|
+
if (detail === 'Chrome/Chromium/Edge was not found; vision_html_screenshot is unavailable') return t('healthChromeMissing')
|
|
1222
|
+
if (detail === 'Chrome availability probe failed') return t('healthChromeProbeFailed')
|
|
1223
|
+
let match = /^credential (.+) is not configured$/u.exec(detail)
|
|
1224
|
+
if (match !== null) return t('healthCredentialMissing', { credential: match[1] })
|
|
1225
|
+
match = /^credential (.+) is resolvable$/u.exec(detail)
|
|
1226
|
+
if (match !== null) return t('healthCredentialReady', { credential: match[1] })
|
|
1227
|
+
match = /^credential (.+) could not be resolved$/u.exec(detail)
|
|
1228
|
+
if (match !== null) return t('healthCredentialFailed', { credential: match[1] })
|
|
1229
|
+
match = /^(Artifact directory|Runtime temp directory) is writable: (.+)$/u.exec(detail)
|
|
1230
|
+
if (match !== null) return t('healthDirectoryWritable', {
|
|
1231
|
+
directory: match[1] === 'Artifact directory' ? t('healthArtifactDirectory') : t('healthTempDirectory'),
|
|
1232
|
+
path: match[2],
|
|
1233
|
+
})
|
|
1234
|
+
match = /^(Artifact directory|Runtime temp directory) is not writable: (.+)$/u.exec(detail)
|
|
1235
|
+
if (match !== null) return t('healthDirectoryNotWritable', {
|
|
1236
|
+
directory: match[1] === 'Artifact directory' ? t('healthArtifactDirectory') : t('healthTempDirectory'),
|
|
1237
|
+
path: match[2],
|
|
1238
|
+
})
|
|
1239
|
+
if (detail === 'Artifact directory could not be prepared') return t('healthArtifactDirectoryFailed')
|
|
1240
|
+
if (detail === 'Connection was not tested; pass testConnection=true to query the configured /models endpoint') return t('healthConnectionNotTested')
|
|
1241
|
+
if (detail === 'Connection test skipped because the configured credential is unavailable') return t('healthConnectionCredentialMissing')
|
|
1242
|
+
match = /^Service responded at (.+) \(HTTP (\d+)\)$/u.exec(detail)
|
|
1243
|
+
if (match !== null) return t('healthServiceResponded', { endpoint: match[1], status: match[2] })
|
|
1244
|
+
match = /^Service rejected the configured credential \(HTTP (\d+)\)$/u.exec(detail)
|
|
1245
|
+
if (match !== null) return t('healthServiceRejectedCredential', { status: match[1] })
|
|
1246
|
+
match = /^Service is reachable but restricted GET \/models \(HTTP (\d+)\); the credential may still be valid for real vision requests$/u.exec(detail)
|
|
1247
|
+
if (match !== null) return t('healthServiceForbidden', { status: match[1] })
|
|
1248
|
+
match = /^Service is reachable but does not expose GET \/models \(HTTP (\d+)\)$/u.exec(detail)
|
|
1249
|
+
if (match !== null) return t('healthServiceNoModels', { status: match[1] })
|
|
1250
|
+
if (detail === 'Service is reachable but rate-limited the connection test (HTTP 429)') return t('healthServiceRateLimited')
|
|
1251
|
+
match = /^Service connection test failed with HTTP (\d+)$/u.exec(detail)
|
|
1252
|
+
if (match !== null) return t('healthServiceHttpFailed', { status: match[1] })
|
|
1253
|
+
match = /^Service could not be reached at (.+)$/u.exec(detail)
|
|
1254
|
+
if (match !== null) return t('healthServiceUnreachable', { endpoint: match[1] })
|
|
1255
|
+
if (detail === 'Vision model was not tested; run an explicit model test to send the bundled diagnostic image') return t('healthModelNotTested')
|
|
1256
|
+
if (detail === 'Vision model test skipped because the configured credential is unavailable') return t('healthModelCredentialMissing')
|
|
1257
|
+
match = /^Vision model (.+) completed a multimodal request$/u.exec(detail)
|
|
1258
|
+
if (match !== null) return t('healthModelReady', { model: match[1] })
|
|
1259
|
+
match = /^Vision model test failed: (.+)$/u.exec(detail)
|
|
1260
|
+
if (match !== null) return t('healthModelFailed', { detail: match[1] })
|
|
1261
|
+
return detail
|
|
1262
|
+
}
|
|
1263
|
+
|
|
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
|
+
const UPDATE_REASON_KEYS: Record<PluginUpdateUnavailableReason, LocaleKey> = {
|
|
1277
|
+
'profile-not-found': 'updateReasonProfileNotFound',
|
|
1278
|
+
'not-direct-dependency': 'updateReasonNotDependency',
|
|
1279
|
+
'unsupported-install-source': 'updateReasonLocalSource',
|
|
1280
|
+
'profile-read-only': 'updateReasonReadOnly',
|
|
1281
|
+
'pnpm-unavailable': 'updateReasonPnpm',
|
|
1282
|
+
'unsupported-platform': 'updateReasonPlatform',
|
|
1283
|
+
'restart-unmanaged': 'updateReasonRestartUnmanaged',
|
|
1284
|
+
'restart-address-unavailable': 'updateReasonRestartAddress',
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function wait(delayMs: number): Promise<void> {
|
|
1288
|
+
return new Promise(resolve => { setTimeout(resolve, delayMs) })
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
1292
|
+
const state = useSyncExternalStore(controller.subscribe, controller.snapshot, controller.snapshot)
|
|
1293
|
+
const snapshot = state.snapshot
|
|
1294
|
+
const [draft, setDraft] = useState<Draft | undefined>(undefined)
|
|
1295
|
+
const [apiKey, setApiKey] = useState('')
|
|
1296
|
+
const [draftError, setDraftError] = useState<string | undefined>(undefined)
|
|
1297
|
+
const [copiedCommand, setCopiedCommand] = useState(false)
|
|
1298
|
+
|
|
1299
|
+
useEffect(() => { if (state.status === 'idle') void controller.load() }, [controller, state.status])
|
|
1300
|
+
useEffect(() => {
|
|
1301
|
+
if (snapshot !== undefined) setDraft(draftOf(snapshot.settings.value))
|
|
1302
|
+
}, [snapshot])
|
|
1303
|
+
useEffect(() => {
|
|
1304
|
+
const restart = state.restart
|
|
1305
|
+
if (restart === undefined || !restart.restarting) return
|
|
1306
|
+
let cancelled = false
|
|
1307
|
+
void (async () => {
|
|
1308
|
+
await wait(restart.retryAfterMs)
|
|
1309
|
+
const deadline = Date.now() + 390_000
|
|
1310
|
+
let outageSeen = false
|
|
1311
|
+
while (!cancelled && Date.now() < deadline) {
|
|
1312
|
+
try {
|
|
1313
|
+
const current = await apiRequest<SettingsSnapshot>()
|
|
1314
|
+
if (current.release.pluginVersion === restart.toVersion) {
|
|
1315
|
+
window.location.reload()
|
|
1316
|
+
return
|
|
1317
|
+
}
|
|
1318
|
+
if (outageSeen && current.release.pluginVersion === restart.fromVersion) {
|
|
1319
|
+
controller.reportRestartTimeout(t('restartRolledBack'))
|
|
1320
|
+
return
|
|
1321
|
+
}
|
|
1322
|
+
} catch {
|
|
1323
|
+
// The expected outage while the replacement process starts.
|
|
1324
|
+
outageSeen = true
|
|
1325
|
+
}
|
|
1326
|
+
await wait(1_000)
|
|
1327
|
+
}
|
|
1328
|
+
if (!cancelled) controller.reportRestartTimeout(t('restartTimedOut'))
|
|
1329
|
+
})()
|
|
1330
|
+
return () => { cancelled = true }
|
|
1331
|
+
}, [controller, state.restart, t])
|
|
1332
|
+
|
|
1333
|
+
if (state.status === 'idle' || (state.status === 'loading' && snapshot === undefined)) {
|
|
1334
|
+
return <div className="dvt-settings"><div className="dvt-loading">{t('testing')}</div></div>
|
|
1335
|
+
}
|
|
1336
|
+
if (snapshot === undefined || draft === undefined) {
|
|
1337
|
+
return <div className="dvt-settings"><div className="dvt-alert error">{state.error ?? t('runtimeUnavailable')}</div><Button variant="outline" onClick={() => { void controller.load() }}>{t('retry')}</Button></div>
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const update = <K extends keyof Draft>(key: K, value: Draft[K]): void => setDraft(current => current === undefined ? current : { ...current, [key]: value })
|
|
1341
|
+
const save = (): void => {
|
|
1342
|
+
try {
|
|
1343
|
+
const keyFailure = apiKeyFailure(apiKey, t)
|
|
1344
|
+
if (keyFailure !== undefined) {
|
|
1345
|
+
setDraftError(keyFailure)
|
|
1346
|
+
return
|
|
1347
|
+
}
|
|
1348
|
+
const credentialValue = apiKey.length === 0 ? undefined : apiKey.trim()
|
|
1349
|
+
setDraftError(undefined)
|
|
1350
|
+
void controller.save(
|
|
1351
|
+
valueOf(draft, t),
|
|
1352
|
+
snapshot.settings.revision,
|
|
1353
|
+
credentialValue,
|
|
1354
|
+
snapshot.writable,
|
|
1355
|
+
).then(saved => { if (saved) setApiKey('') })
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
setDraftError(error instanceof Error ? error.message : String(error))
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
const busy = state.action !== undefined
|
|
1361
|
+
const credentialMatchesSnapshot = draft.credential.trim() === snapshot.credential.ref
|
|
1362
|
+
const builtInCredentialChangedProvider = snapshot.credential.source === 'built-in-free'
|
|
1363
|
+
&& !isBuiltInFreeVisionDraft(draft)
|
|
1364
|
+
const keyLocked = credentialMatchesSnapshot
|
|
1365
|
+
&& !snapshot.credential.writable
|
|
1366
|
+
&& !builtInCredentialChangedProvider
|
|
1367
|
+
const canSave = snapshot.writable || (apiKey.length > 0 && !keyLocked)
|
|
1368
|
+
const runtimeErrorTitle = snapshot.runtime.ready ? t('runtimeCandidateRejected') : t('runtimeUnavailable')
|
|
1369
|
+
const pluginUpdate = state.update
|
|
1370
|
+
const updateCapability = pluginUpdate ?? snapshot.release.update
|
|
1371
|
+
const latestVersion = pluginUpdate?.latestVersion
|
|
1372
|
+
const updateReason = updateCapability.reason === undefined ? undefined : t(UPDATE_REASON_KEYS[updateCapability.reason])
|
|
1373
|
+
const updateCheckSupported = updateCapability.checkSupported ?? updateCapability.supported
|
|
1374
|
+
const updateHasUnsavedChanges = apiKey.length > 0 || settingsDraftChanged(draft, snapshot.settings.value, t)
|
|
1375
|
+
const manualUpdateProfile = updateCapability.profile ?? 'web'
|
|
1376
|
+
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
|
+
const copyManualUpdate = (): void => {
|
|
1379
|
+
void navigator.clipboard?.writeText(manualUpdateCommand)
|
|
1380
|
+
.then(() => {
|
|
1381
|
+
setCopiedCommand(true)
|
|
1382
|
+
window.setTimeout(() => setCopiedCommand(false), 2_000)
|
|
1383
|
+
})
|
|
1384
|
+
.catch(() => {})
|
|
1385
|
+
}
|
|
1386
|
+
const applyUpdate = (): void => {
|
|
1387
|
+
if (latestVersion === undefined) return
|
|
1388
|
+
if (!window.confirm(t('updateConfirm', { version: latestVersion }))) return
|
|
1389
|
+
void controller.applyUpdate(latestVersion)
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
return (
|
|
1393
|
+
<div className="dvt-settings">
|
|
1394
|
+
<div className="dvt-alert notice">{t('externalNotice')}</div>
|
|
1395
|
+
{!snapshot.writable ? <div className="dvt-alert warning">{t('readOnly')}</div> : null}
|
|
1396
|
+
{draftError === undefined ? null : <div className="dvt-alert error">{draftError}</div>}
|
|
1397
|
+
{state.error === undefined ? null : <div className="dvt-alert error">{state.error}</div>}
|
|
1398
|
+
{state.message === 'saved' ? <div className="dvt-alert success">{t('saved')}</div> : null}
|
|
1399
|
+
{state.message === 'restarting' && state.restart !== undefined ? <div className="dvt-alert success">{t('restarting', { version: state.restart.toVersion })}</div> : null}
|
|
1400
|
+
{state.message === 'manual-restart-required' && state.restart !== undefined ? <div className="dvt-alert success">{t('manualRestartRequired', { version: state.restart.toVersion })}</div> : null}
|
|
1401
|
+
{snapshot.runtime.lastError === undefined ? null : <div className="dvt-alert error"><strong>{runtimeErrorTitle}</strong><span>{snapshot.runtime.lastError}</span></div>}
|
|
1402
|
+
|
|
1403
|
+
<section className="dvt-panel dvt-essential"><div className="dvt-panel-title"><div><h3>{t('provider')}</h3><p>{t('providerHint')}</p></div><span className={`dvt-badge ${snapshot.credential.configured ? 'ok' : 'error'}`}>{snapshot.credential.configured ? t('configured') : t('missing')}</span></div>
|
|
1404
|
+
<p className="dvt-tutorial-link"><a href={aihubmixTutorialUrl} target="_blank" rel="noreferrer">{t('aihubmixTutorial')}</a></p>
|
|
1405
|
+
<div className="dvt-form-grid">
|
|
1406
|
+
<Field label={t('protocol')}><select disabled={!snapshot.writable || busy} value={draft.protocol} onChange={(event) => { update('protocol', event.target.value as 'openai' | 'anthropic') }}><option value="openai">OpenAI Chat Completions</option><option value="anthropic">Anthropic Messages</option></select></Field>
|
|
1407
|
+
<Field label={t('baseUrl')}><Input disabled={!snapshot.writable || busy} value={draft.baseUrl} onChange={(event) => { update('baseUrl', event.target.value) }} /></Field>
|
|
1408
|
+
<Field label={t('model')}><Input disabled={!snapshot.writable || busy} value={draft.model} onChange={(event) => { update('model', event.target.value) }} /></Field>
|
|
1409
|
+
<Field label={t('apiKey')} hint={keyLocked ? t('apiKeyLocked') : snapshot.credential.source === undefined ? t('apiKeyHint') : `${t('apiKeyHint')} ${t('sourceHint', { source: t('source'), value: credentialSource(snapshot.credential.source, t) })}`}><Input aria-label={t('apiKey')} type="password" autoComplete="new-password" disabled={busy || keyLocked} placeholder={snapshot.credential.configured ? t('apiKeyPlaceholderConfigured') : t('apiKeyPlaceholderMissing')} value={apiKey} onChange={(event) => { setApiKey(event.target.value); setDraftError(undefined) }} /></Field>
|
|
1410
|
+
</div>
|
|
1411
|
+
</section>
|
|
1412
|
+
|
|
1413
|
+
<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
|
+
|
|
1415
|
+
<section className="dvt-panel"><div className="dvt-panel-title"><div><h3>{t('health')}</h3><p>{t('connectionHint')}</p></div><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><Button size="sm" variant="outline" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runHealth('connection') }}>{state.action === 'connection' ? t('testing') : t('testConnection')}</Button><Button size="sm" variant="primary" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runHealth('model') }}>{state.action === 'model' ? t('testingModel') : t('testModel')}</Button></div></div>
|
|
1416
|
+
<p className="dvt-muted">{t('saveBeforeTesting')}</p>
|
|
1417
|
+
{state.health === undefined ? <p className="dvt-muted">{t('notTested')}</p> : <div className="dvt-health-grid">{Object.entries(state.health.checks).map(([name, check]) => {
|
|
1418
|
+
const testTag = name === 'model' ? modelTestTag(state.health as HealthResult, check) : undefined
|
|
1419
|
+
return <div key={name} data-status={check.status}><span>{t(HEALTH_NAME_KEYS[name] ?? 'health')}</span>{testTag === undefined ? null : <em className="dvt-health-test-tag" data-status={testTag.status}>{t(testTag.label)}</em>}<strong>{t(HEALTH_STATUS_KEYS[check.status])}</strong><p>{healthDetail(name, check.detail, t)}</p></div>
|
|
1420
|
+
})}</div>}
|
|
1421
|
+
</section>
|
|
1422
|
+
|
|
1423
|
+
<section className="dvt-panel dvt-update-panel">
|
|
1424
|
+
<div className="dvt-panel-title">
|
|
1425
|
+
<div><h3>{t('updates')}</h3><p>{t('updatesHint')}</p></div>
|
|
1426
|
+
<span className={`dvt-badge ${pluginUpdate?.updateAvailable ? 'warning' : pluginUpdate !== undefined && pluginUpdate.supported ? 'ok' : ''}`}>
|
|
1427
|
+
{pluginUpdate?.updateAvailable ? t('updateAvailable') : pluginUpdate !== undefined && pluginUpdate.supported ? t('upToDate') : t('pluginVersion')}
|
|
1428
|
+
</span>
|
|
1429
|
+
</div>
|
|
1430
|
+
<div className="dvt-update-grid">
|
|
1431
|
+
<div><span>{t('updateInstalled')}</span><strong>{snapshot.release.pluginVersion}</strong></div>
|
|
1432
|
+
<div><span>{t('updateLatest')}</span><strong>{latestVersion ?? '—'}</strong></div>
|
|
1433
|
+
<div><span>{t('updateProfile')}</span><strong>{updateCapability.profile ?? '—'}</strong></div>
|
|
1434
|
+
</div>
|
|
1435
|
+
{!updateCapability.supported ? <div className="dvt-alert warning"><strong>{t('updateUnsupported')}</strong><span>{updateReason}</span></div> : null}
|
|
1436
|
+
{updateCapability.supported && updateHasUnsavedChanges ? <div className="dvt-alert warning">{t('updateSaveFirst')}</div> : null}
|
|
1437
|
+
{pluginUpdate?.supported && pluginUpdate.updateAvailable && latestVersion !== undefined ? <p className="dvt-muted">{t('updateAvailableDetail', { version: latestVersion })}</p> : null}
|
|
1438
|
+
{pluginUpdate?.supported && !pluginUpdate.updateAvailable && latestVersion !== undefined ? <p className="dvt-muted">{t('upToDateDetail', { version: latestVersion })}</p> : null}
|
|
1439
|
+
<p className="dvt-muted">{t('manualUpdateHint')}</p>
|
|
1440
|
+
<div className="dvt-manual-update"><code>{manualUpdateCommand}</code><Button size="sm" variant="outline" onClick={copyManualUpdate}>{copiedCommand ? t('copied') : t('copy')}</Button></div>
|
|
1441
|
+
<div className="dvt-actions">
|
|
1442
|
+
<Button variant="outline" disabled={busy || !updateCheckSupported || state.restart !== undefined} onClick={() => { void controller.checkUpdate() }}>{state.action === 'check-update' ? t('checkingUpdate') : t('checkUpdate')}</Button>
|
|
1443
|
+
{pluginUpdate?.supported && pluginUpdate.updateAvailable && latestVersion !== undefined ? <Button variant="primary" disabled={busy || state.restart !== undefined || updateHasUnsavedChanges} onClick={applyUpdate}>{state.action === 'apply-update' ? t('updatingPlugin') : t('updateNow')}</Button> : null}
|
|
1444
|
+
</div>
|
|
1445
|
+
</section>
|
|
1446
|
+
|
|
1447
|
+
<details className="dvt-advanced">
|
|
1448
|
+
<summary><span><strong>{t('advanced')}</strong><small>{t('advancedHint')}</small></span><span className="dvt-details-chevron" aria-hidden="true">⌄</span></summary>
|
|
1449
|
+
<div className="dvt-advanced-body">
|
|
1450
|
+
<section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('provider')}</h3></div><div className="dvt-form-grid">
|
|
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">
|
|
1457
|
+
<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
|
+
</div></section>
|
|
1463
|
+
|
|
1464
|
+
<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>
|
|
1465
|
+
|
|
1466
|
+
<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">
|
|
1467
|
+
<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>
|
|
1468
|
+
{draft.runtimeMode === 'external' ? <Field label={t('toolkitPath')}><Input value={draft.toolkitPath} onChange={(event) => { update('toolkitPath', event.target.value) }} /></Field> : null}
|
|
1469
|
+
<Field label={t('python')}><Input placeholder="python3" value={draft.python} onChange={(event) => { update('python', event.target.value) }} /></Field>
|
|
1470
|
+
<Field label={t('allowedDirs')} hint={t('allowedDirsHint')}><textarea rows={3} value={draft.allowedDirs} onChange={(event) => { update('allowedDirs', event.target.value) }} /></Field>
|
|
1471
|
+
</div>
|
|
1472
|
+
{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>}
|
|
1473
|
+
</section>
|
|
1474
|
+
|
|
1475
|
+
</div>
|
|
1476
|
+
</details>
|
|
1477
|
+
|
|
1478
|
+
<footer className="dvt-settings-footer">
|
|
1479
|
+
<div><span className="dvt-kicker">{t('pluginKind')}</span><h2>{t('settingsTitle')}</h2><p>{t('settingsIntro')}</p></div>
|
|
1480
|
+
<div className="dvt-release"><span>{t('pluginVersion')} <strong>{snapshot.release.pluginVersion}</strong></span><span>{t('upstreamVersion')} <strong>{snapshot.release.upstreamVersion}</strong></span><span>{t('activeGeneration')} <strong>{t('activeGenerationValue', { generation: snapshot.runtime.generation })}</strong></span></div>
|
|
1481
|
+
</footer>
|
|
1482
|
+
</div>
|
|
1483
|
+
)
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const CSS = `
|
|
1487
|
+
.dvt-tool{margin:4px 0;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;box-shadow:var(--dsw-shadow-lv1)}
|
|
1488
|
+
.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}
|
|
1489
|
+
.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}
|
|
1490
|
+
.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)}
|
|
1491
|
+
.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:grid;grid-template-columns:minmax(0,1fr);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-save-row{display:flex;gap:8px;padding:2px 0}.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}
|
|
1492
|
+
.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
|
+
.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
|
+
.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}
|
|
1495
|
+
@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
|
+
`
|
|
1497
|
+
|
|
1498
|
+
function installStyles(): () => void {
|
|
1499
|
+
const id = '@mengruo/dsh-vision-toolkit/client'
|
|
1500
|
+
const existing = document.querySelector<HTMLStyleElement>(`style[data-plugin-css="${id}"]`)
|
|
1501
|
+
if (existing !== null) return () => {}
|
|
1502
|
+
const style = document.createElement('style')
|
|
1503
|
+
style.dataset.plugin = '@mengruo/dsh-vision-toolkit'
|
|
1504
|
+
style.dataset.pluginCss = id
|
|
1505
|
+
style.textContent = CSS
|
|
1506
|
+
document.head.appendChild(style)
|
|
1507
|
+
return () => { style.remove() }
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/** Required client services. The pasted-image codec attaches to either trigger-service generation after load. */
|
|
1511
|
+
export const inject = ['slots', 'locale', 'remote', 'conversation', 'sessions']
|
|
1512
|
+
|
|
1513
|
+
/** Register dedicated Tool views and the Vision Settings section. */
|
|
1514
|
+
export function apply(ctx: ClientContext): void {
|
|
1515
|
+
ctx.effect(installStyles, 'dsh-vision-toolkit: styles')
|
|
1516
|
+
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-vision-toolkit: locale')
|
|
1517
|
+
installPasteImages(ctx)
|
|
1518
|
+
ctx.effect(installModelVariantsHider, 'dsh-vision-toolkit: model-selector transparent routing')
|
|
1519
|
+
const t = ctx.locale.bind(NS)
|
|
1520
|
+
const injected = () => ({ t })
|
|
1521
|
+
const entries: Array<[string, (props: ViewProps) => ReactNode]> = [
|
|
1522
|
+
['vision_ground', GroundView],
|
|
1523
|
+
['vision_detect', DetectView],
|
|
1524
|
+
['vision_trace', TraceView],
|
|
1525
|
+
['vision_pixel_diff', PixelDiffView],
|
|
1526
|
+
['vision_crop', ArtifactView],
|
|
1527
|
+
['vision_long_screenshot_ocr', ArtifactView],
|
|
1528
|
+
['vision_extract_foreground', ArtifactView],
|
|
1529
|
+
['vision_html_screenshot', ArtifactView],
|
|
1530
|
+
['vision_dominant_colors', PaletteView],
|
|
1531
|
+
]
|
|
1532
|
+
ctx.slots.inject('tool.call.toolview', function* () {
|
|
1533
|
+
for (const [key, component] of entries) {
|
|
1534
|
+
yield ctx.slots.register({ name: 'tool.call.toolview', key, inject: injected }, component)
|
|
1535
|
+
}
|
|
1536
|
+
})
|
|
1537
|
+
|
|
1538
|
+
const controller = new VisionSettingsController()
|
|
1539
|
+
ctx.effect(() => {
|
|
1540
|
+
const refreshSettings = (namespace: string): void => {
|
|
1541
|
+
if (namespace === 'vision-toolkit') {
|
|
1542
|
+
resetDisplayConfigCache()
|
|
1543
|
+
controller.refreshIfLoaded()
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
const refreshCredential = (ref: string): void => {
|
|
1547
|
+
const current = controller.snapshot().snapshot
|
|
1548
|
+
if (current?.credential.ref === ref) controller.refreshIfLoaded()
|
|
1549
|
+
}
|
|
1550
|
+
const legacyRemote = ctx.remote as typeof ctx.remote & {
|
|
1551
|
+
$on?: (event: string, listener: (value: string) => void) => () => void
|
|
1552
|
+
}
|
|
1553
|
+
const currentEvents = ctx as unknown as {
|
|
1554
|
+
on(event: 'settings/changed', listener: (namespace: string) => void): () => void
|
|
1555
|
+
on(event: 'credentials/changed', listener: (ref: string) => void): () => void
|
|
1556
|
+
}
|
|
1557
|
+
const disposers = typeof legacyRemote.$on === 'function'
|
|
1558
|
+
? [
|
|
1559
|
+
legacyRemote.$on('settings/document-updated', refreshSettings),
|
|
1560
|
+
legacyRemote.$on('credentials/updated', refreshCredential),
|
|
1561
|
+
]
|
|
1562
|
+
: [
|
|
1563
|
+
currentEvents.on('settings/changed', (namespace) => {
|
|
1564
|
+
refreshSettings(namespace)
|
|
1565
|
+
}),
|
|
1566
|
+
currentEvents.on('credentials/changed', (ref) => {
|
|
1567
|
+
refreshCredential(ref)
|
|
1568
|
+
}),
|
|
1569
|
+
]
|
|
1570
|
+
disposers.push(ctx.on('connection/reset', () => { controller.refreshIfLoaded() }))
|
|
1571
|
+
return () => { for (const dispose of disposers) dispose() }
|
|
1572
|
+
}, 'dsh-vision-toolkit: Settings invalidations')
|
|
1573
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
1574
|
+
name: 'settings.section',
|
|
1575
|
+
id: 'vision-toolkit',
|
|
1576
|
+
order: 30,
|
|
1577
|
+
label: () => t('nav'),
|
|
1578
|
+
inject: () => ({ controller, t }),
|
|
1579
|
+
}, SettingsSection))
|
|
1580
|
+
}
|