@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
package/lib/client.js
ADDED
|
@@ -0,0 +1,2099 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "@mengruo/dsh-vision-toolkit", factory: (require) => {
|
|
2
|
+
var __modules = Object.create(null); var __cache = Object.create(null);
|
|
3
|
+
__modules["./display-config.js"] = function(module, exports, require, __load_) {
|
|
4
|
+
"use strict";
|
|
5
|
+
/**
|
|
6
|
+
* Browser-side display-mode flags for transparent variant routing. The paste
|
|
7
|
+
* integration uses a short-lived cache so it does not hammer the same-origin
|
|
8
|
+
* route on every paste. The model selector itself decides purely from DOM
|
|
9
|
+
* display names and does not read this route.
|
|
10
|
+
* @module dsh-vision-toolkit/display-config
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.DISPLAY_CONFIG_ROUTE = void 0;
|
|
14
|
+
exports.readDisplayConfig = readDisplayConfig;
|
|
15
|
+
exports.resetDisplayConfigCache = resetDisplayConfigCache;
|
|
16
|
+
exports.DISPLAY_CONFIG_ROUTE = '/_dsh/vision-toolkit/display-config';
|
|
17
|
+
const CONFIG_TTL_MS = 10_000;
|
|
18
|
+
let cached;
|
|
19
|
+
let cacheEpoch = 0;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the current transparent-routing flag, failing closed to non-hidden
|
|
22
|
+
* (explicit sibling entries) when the route is unreachable or the payload is
|
|
23
|
+
* malformed.
|
|
24
|
+
* @returns the display-mode flags observed from the host.
|
|
25
|
+
*/
|
|
26
|
+
async function readDisplayConfig() {
|
|
27
|
+
for (;;) {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (cached !== undefined && now - cached.at < CONFIG_TTL_MS) {
|
|
30
|
+
return { hidden: cached.hidden };
|
|
31
|
+
}
|
|
32
|
+
const epoch = cacheEpoch;
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(exports.DISPLAY_CONFIG_ROUTE, { cache: 'no-store' });
|
|
35
|
+
if (epoch !== cacheEpoch)
|
|
36
|
+
continue;
|
|
37
|
+
const body = await response.json();
|
|
38
|
+
if (epoch !== cacheEpoch)
|
|
39
|
+
continue;
|
|
40
|
+
if (body.ok !== true || typeof body.value?.hidden !== 'boolean') {
|
|
41
|
+
throw new Error('malformed display-config payload');
|
|
42
|
+
}
|
|
43
|
+
cached = { hidden: body.value.hidden, at: now };
|
|
44
|
+
return { hidden: body.value.hidden };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
if (epoch !== cacheEpoch)
|
|
48
|
+
continue;
|
|
49
|
+
// Transparent routing is an enhancement: an unreachable config must never
|
|
50
|
+
// hide anything or change paste behavior.
|
|
51
|
+
cached = { hidden: false, at: now };
|
|
52
|
+
return { hidden: false };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Drop the cached flag and invalidate in-flight responses (test seams,
|
|
58
|
+
* Settings saves, and connection-reset handling). An older request that
|
|
59
|
+
* resolves afterwards must not repopulate the cache with a stale flag.
|
|
60
|
+
*/
|
|
61
|
+
function resetDisplayConfigCache() {
|
|
62
|
+
cached = undefined;
|
|
63
|
+
cacheEpoch += 1;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
__modules["./index.js"] = function(module, exports, require, __load_) {
|
|
67
|
+
"use strict";
|
|
68
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
69
|
+
exports.inject = exports.VisionSettingsController = void 0;
|
|
70
|
+
exports.decodeVisionResult = decodeVisionResult;
|
|
71
|
+
exports.apply = apply;
|
|
72
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
73
|
+
/**
|
|
74
|
+
* DSH Vision Toolkit browser plugin: dedicated Tool cards plus the Settings,
|
|
75
|
+
* health, connection-test, and safe Artifact preview experience.
|
|
76
|
+
*/
|
|
77
|
+
const react_1 = require("react");
|
|
78
|
+
const dsh_client_ui_primitives_1 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
79
|
+
const paste_images_tsx_1 = __load_("./paste-images.js");
|
|
80
|
+
const model_variants_hider_ts_1 = __load_("./model-variants-hider.js");
|
|
81
|
+
const display_config_ts_1 = __load_("./display-config.js");
|
|
82
|
+
const NS = 'vision-toolkit';
|
|
83
|
+
const SETTINGS_ROUTE = '/_dsh/vision-toolkit/settings';
|
|
84
|
+
const PRESENTATION_META_KEY = '$dshVisionToolkit';
|
|
85
|
+
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';
|
|
86
|
+
// Keep these browser defaults aligned with src/defaults.ts without importing server-side config.
|
|
87
|
+
const BUILT_IN_FREE_VISION_BASE_URL = 'https://vision.anionex.me/v1';
|
|
88
|
+
const BUILT_IN_FREE_VISION_CREDENTIAL = 'ANIONEX_FREE_VISION';
|
|
89
|
+
const BUILT_IN_FREE_VISION_MODEL = 'gemini-3.7-flash';
|
|
90
|
+
const AIHUBMIX_TUTORIAL_URL_EN = 'https://github.com/Anionex/dsh-vision-toolkit/blob/main/docs/aihubmix-gemini-vision.md';
|
|
91
|
+
const AIHUBMIX_TUTORIAL_URL_ZH = 'https://github.com/Anionex/dsh-vision-toolkit/blob/main/docs/aihubmix-gemini-vision.zh.md';
|
|
92
|
+
const en = {
|
|
93
|
+
nav: 'Vision',
|
|
94
|
+
settingsTitle: 'Vision Toolkit',
|
|
95
|
+
settingsIntro: 'Configure the pinned visual engineering runtime, its external vision endpoint, and local safety limits.',
|
|
96
|
+
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.',
|
|
97
|
+
provider: 'Vision service',
|
|
98
|
+
providerHint: 'Choose the API protocol, then provide the service address, model, and API key used by online vision features.',
|
|
99
|
+
aihubmixTutorial: 'Need an AIHubMix key for free Gemini 3.7 Flash vision? Follow the signup guide →',
|
|
100
|
+
baseUrl: 'Base URL',
|
|
101
|
+
apiKey: 'API key',
|
|
102
|
+
apiKeyPlaceholderMissing: 'Paste the API key',
|
|
103
|
+
apiKeyPlaceholderConfigured: 'Saved; leave blank to keep it',
|
|
104
|
+
apiKeyHint: 'The key is stored in DSH Credentials and is never shown again after saving.',
|
|
105
|
+
apiKeyLocked: 'The current key comes from a read-only source and cannot be replaced here.',
|
|
106
|
+
apiKeyBlank: 'The API key cannot contain only spaces.',
|
|
107
|
+
apiKeyInvalid: 'Paste only the key, without a variable name, quotes, spaces, or line breaks.',
|
|
108
|
+
credential: 'Credential name',
|
|
109
|
+
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.',
|
|
110
|
+
model: 'Model',
|
|
111
|
+
protocol: 'API protocol',
|
|
112
|
+
anthropicThinking: 'Anthropic thinking',
|
|
113
|
+
anthropicThinkingHint: 'omit has the broadest compatibility. Use disabled or adaptive only when the selected model documents that mode; restore omit first after HTTP 400.',
|
|
114
|
+
userAgent: 'User-Agent',
|
|
115
|
+
language: 'Output language',
|
|
116
|
+
limits: 'Limits',
|
|
117
|
+
timeout: 'Request timeout (ms)',
|
|
118
|
+
maxBytes: 'Maximum image bytes',
|
|
119
|
+
maxPixels: 'Maximum image pixels',
|
|
120
|
+
concurrency: 'Concurrent calls per session',
|
|
121
|
+
runtime: 'Runtime',
|
|
122
|
+
runtimeMode: 'Runtime mode',
|
|
123
|
+
toolkitPath: 'Pinned checkout path',
|
|
124
|
+
python: 'Python override',
|
|
125
|
+
allowedDirs: 'Additional allowed directories',
|
|
126
|
+
allowedDirsHint: 'One path per line. The session workspace is always allowed.',
|
|
127
|
+
save: 'Save and apply',
|
|
128
|
+
saving: 'Validating runtime…',
|
|
129
|
+
reload: 'Reload',
|
|
130
|
+
saved: 'Settings validated and applied.',
|
|
131
|
+
readOnly: 'Service settings are read-only. A writable API key can still be saved.',
|
|
132
|
+
configured: 'Configured',
|
|
133
|
+
missing: 'Missing',
|
|
134
|
+
source: 'Source',
|
|
135
|
+
sourceHint: '{source}: {value}',
|
|
136
|
+
sourceEnv: 'Environment variable',
|
|
137
|
+
sourceFile: 'Credential file',
|
|
138
|
+
health: 'Health',
|
|
139
|
+
runHealth: 'Run health check',
|
|
140
|
+
testConnection: 'Test API connection',
|
|
141
|
+
testModel: 'Test vision model',
|
|
142
|
+
testing: 'Checking…',
|
|
143
|
+
testingModel: 'Testing model…',
|
|
144
|
+
connectionHint: 'The API connection test only queries GET /models. The vision model test sends the bundled diagnostic image and verifies one real multimodal request.',
|
|
145
|
+
saveBeforeTesting: 'Save service changes before testing the connection.',
|
|
146
|
+
advanced: 'Advanced settings',
|
|
147
|
+
advancedHint: 'Credential name, provider compatibility, output language, resource limits, runtime source, Python, and additional readable directories.',
|
|
148
|
+
imageInput: 'Image input',
|
|
149
|
+
hiddenVariants: 'Transparent variant routing',
|
|
150
|
+
hiddenVariantsLabel: 'Keep the original model names and enable images automatically',
|
|
151
|
+
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.',
|
|
152
|
+
pluginVersion: 'Plugin',
|
|
153
|
+
upstreamVersion: 'Upstream',
|
|
154
|
+
activeGeneration: 'Runtime generation',
|
|
155
|
+
activeGenerationValue: 'Generation {generation}',
|
|
156
|
+
updates: 'Plugin updates',
|
|
157
|
+
updatesHint: 'Check npm for a newer release, install it into this DSH profile, and restart DSH Web automatically.',
|
|
158
|
+
manualUpdate: 'Manual update',
|
|
159
|
+
manualUpdateHint: 'Run this command in your terminal to install the latest release into this DSH profile.',
|
|
160
|
+
copy: 'Copy',
|
|
161
|
+
copied: 'Copied',
|
|
162
|
+
checkUpdate: 'Check for updates',
|
|
163
|
+
checkingUpdate: 'Checking for updates…',
|
|
164
|
+
updateAvailable: 'Update available',
|
|
165
|
+
updateAvailableDetail: 'Version {version} is available. It will restart DSH Web automatically when safe; otherwise you will be asked to restart it manually.',
|
|
166
|
+
upToDate: 'Up to date',
|
|
167
|
+
upToDateDetail: 'Version {version} is the latest release.',
|
|
168
|
+
updateNow: 'Install update',
|
|
169
|
+
updatingPlugin: 'Installing update…',
|
|
170
|
+
updateConfirm: 'Install Vision Toolkit {version} now? DSH Web will restart automatically when supported; otherwise a manual restart will be required.',
|
|
171
|
+
restarting: 'Version {version} was installed. Waiting for DSH Web to restart…',
|
|
172
|
+
manualRestartRequired: 'Version {version} was installed. Restart DSH Web through your usual command or process manager to activate it.',
|
|
173
|
+
updateProfile: 'Profile',
|
|
174
|
+
updateInstalled: 'Installed',
|
|
175
|
+
updateLatest: 'Latest',
|
|
176
|
+
updateUnsupported: 'In-app updates are unavailable for this installation.',
|
|
177
|
+
updateReasonProfileNotFound: 'The running plugin could not be matched to a DSH profile installation.',
|
|
178
|
+
updateReasonNotDependency: 'The plugin is not a direct dependency of this DSH profile.',
|
|
179
|
+
updateReasonLocalSource: 'This profile uses a local, workspace, URL, or git installation; update that source manually so local work is not overwritten.',
|
|
180
|
+
updateReasonReadOnly: 'The profile package manifest is read-only.',
|
|
181
|
+
updateReasonPnpm: 'pnpm is unavailable in the DSH execution environment.',
|
|
182
|
+
updateReasonPlatform: 'Automatic restart is unavailable on this operating system.',
|
|
183
|
+
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.',
|
|
184
|
+
updateReasonRestartAddress: 'Automatic restart is unavailable when DSH Web uses an unknown or dynamically allocated port. Start it with a fixed --port value.',
|
|
185
|
+
updateSaveFirst: 'Save or discard the current Settings and API key changes before updating the plugin.',
|
|
186
|
+
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.',
|
|
187
|
+
restartRolledBack: 'The new plugin did not become ready, so the previous version was restored. Check the restart log before trying again.',
|
|
188
|
+
pluginKind: 'DSH native plugin',
|
|
189
|
+
runtimeUnavailable: 'Runtime unavailable',
|
|
190
|
+
runtimeCandidateRejected: 'Last runtime candidate was rejected; the active generation remains available.',
|
|
191
|
+
runtimeReady: 'Ready',
|
|
192
|
+
runtimeManaged: 'Managed',
|
|
193
|
+
runtimeExternal: 'External checkout',
|
|
194
|
+
retry: 'Retry',
|
|
195
|
+
open: 'Open file',
|
|
196
|
+
download: 'Download',
|
|
197
|
+
previewUnavailable: 'HTTP preview is unavailable in this host; use Open file.',
|
|
198
|
+
running: 'Running…',
|
|
199
|
+
failed: 'Failed',
|
|
200
|
+
matches: 'matches',
|
|
201
|
+
elements: 'elements',
|
|
202
|
+
dimensions: 'Dimensions',
|
|
203
|
+
coordinates: 'Coordinates',
|
|
204
|
+
artifact: 'Artifact',
|
|
205
|
+
artifacts: 'Artifacts',
|
|
206
|
+
difference: 'Overall difference',
|
|
207
|
+
worstRegions: 'Worst regions',
|
|
208
|
+
colors: 'Dominant colors',
|
|
209
|
+
noResult: 'Structured result unavailable; inspect the raw Tool result.',
|
|
210
|
+
healthy: 'Healthy',
|
|
211
|
+
degraded: 'Needs attention',
|
|
212
|
+
notTested: 'Not tested',
|
|
213
|
+
groundTitle: 'Ground',
|
|
214
|
+
detectTitle: 'Detect',
|
|
215
|
+
traceTitle: 'Trace SVG',
|
|
216
|
+
pixelDiffTitle: 'Pixel Diff',
|
|
217
|
+
cropTitle: 'Crop',
|
|
218
|
+
longOcrTitle: 'Long OCR',
|
|
219
|
+
extractForegroundTitle: 'Extract Foreground',
|
|
220
|
+
htmlScreenshotTitle: 'HTML Screenshot',
|
|
221
|
+
artifactTitle: 'Vision Artifact',
|
|
222
|
+
dominantColorsTitle: 'Dominant Colors',
|
|
223
|
+
artifactGroundPreview: 'Grounding bounding-box preview',
|
|
224
|
+
artifactDetectPreview: 'Detected-element bounding-box preview',
|
|
225
|
+
artifactCrop: 'Cropped image region',
|
|
226
|
+
artifactTrace: 'Traced vector geometry',
|
|
227
|
+
artifactDiffHeatmap: 'Pixel-difference heatmap',
|
|
228
|
+
artifactDiffReport: 'Structured pixel-difference report',
|
|
229
|
+
artifactLongManifest: 'Long-screenshot split and merge manifest',
|
|
230
|
+
artifactLongTranscript: 'Merged long-screenshot OCR transcript',
|
|
231
|
+
artifactLongAudit: 'Long-screenshot OCR boundary audit',
|
|
232
|
+
artifactLongChunk: 'Long-screenshot OCR chunk {index}',
|
|
233
|
+
artifactOcrSidecar: 'OCR sidecar for chunk {index}',
|
|
234
|
+
artifactForeground: 'Extracted transparent foreground',
|
|
235
|
+
artifactHtmlScreenshot: 'Headless browser screenshot of local HTML',
|
|
236
|
+
label: 'Label',
|
|
237
|
+
paths: 'paths',
|
|
238
|
+
healthPython: 'Python',
|
|
239
|
+
healthDependencies: 'Dependencies',
|
|
240
|
+
healthChrome: 'Browser',
|
|
241
|
+
healthCredential: 'Credential',
|
|
242
|
+
healthArtifactDirectory: 'Artifact directory',
|
|
243
|
+
healthTempDirectory: 'Temporary directory',
|
|
244
|
+
healthService: 'Vision service',
|
|
245
|
+
healthModel: 'Vision model',
|
|
246
|
+
statusOk: 'OK',
|
|
247
|
+
statusWarning: 'Warning',
|
|
248
|
+
statusError: 'Error',
|
|
249
|
+
statusNotTested: 'Not tested',
|
|
250
|
+
positiveInteger: '{field} must be a positive integer.',
|
|
251
|
+
healthPythonDetail: '{version} via {path}',
|
|
252
|
+
healthChromeMissing: 'Chrome, Chromium, or Edge was not found; HTML Screenshot is unavailable.',
|
|
253
|
+
healthChromeProbeFailed: 'Could not check whether a supported browser is available.',
|
|
254
|
+
healthCredentialMissing: 'Credential {credential} is not configured.',
|
|
255
|
+
healthCredentialReady: 'Credential {credential} is available.',
|
|
256
|
+
healthCredentialFailed: 'Could not read credential {credential}.',
|
|
257
|
+
healthDirectoryWritable: '{directory} is writable: {path}',
|
|
258
|
+
healthDirectoryNotWritable: '{directory} is not writable: {path}',
|
|
259
|
+
healthArtifactDirectoryFailed: 'Could not prepare the artifact directory.',
|
|
260
|
+
healthConnectionNotTested: 'API connection not tested. Use Test API connection to query /models.',
|
|
261
|
+
healthConnectionCredentialMissing: 'Connection test skipped because the credential is unavailable.',
|
|
262
|
+
healthServiceResponded: 'Service responded at {endpoint} (HTTP {status}).',
|
|
263
|
+
healthServiceRejectedCredential: 'Service rejected the configured credential (HTTP {status}).',
|
|
264
|
+
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.',
|
|
265
|
+
healthServiceNoModels: 'Service is reachable but does not support GET /models (HTTP {status}).',
|
|
266
|
+
healthServiceRateLimited: 'Service is reachable, but the connection test was rate-limited (HTTP 429).',
|
|
267
|
+
healthServiceHttpFailed: 'Connection test failed with HTTP {status}.',
|
|
268
|
+
healthServiceUnreachable: 'Could not reach {endpoint}.',
|
|
269
|
+
healthModelNotTested: 'Vision model not tested. Run Test vision model to make one real multimodal request.',
|
|
270
|
+
healthModelCredentialMissing: 'Vision model test skipped because the credential is unavailable.',
|
|
271
|
+
healthModelReady: 'Model {model} completed a real multimodal request.',
|
|
272
|
+
healthModelFailed: 'Real multimodal request failed: {detail}',
|
|
273
|
+
modelTestVerifiedTag: 'Verified',
|
|
274
|
+
modelTestNotRunTag: 'Not tested',
|
|
275
|
+
modelTestFailedTag: 'Test failed',
|
|
276
|
+
};
|
|
277
|
+
const zh = {
|
|
278
|
+
nav: '视觉工具',
|
|
279
|
+
settingsTitle: '视觉工具箱',
|
|
280
|
+
settingsIntro: '在这里设置视觉模型服务、工具运行环境,以及图片和文件的本地访问范围。',
|
|
281
|
+
externalNotice: '使用图像理解、目标定位、界面检测或文字识别等在线功能时,所选图片会发送到下方配置的视觉服务。图片裁剪、轮廓描摹、像素对比、主色提取、前景提取和网页截图均在本机完成,不会上传图片。',
|
|
282
|
+
provider: '在线视觉服务',
|
|
283
|
+
providerHint: '选择接口协议后,填写在线视觉功能使用的 API 地址、模型名称和 API 密钥。',
|
|
284
|
+
aihubmixTutorial: '想申请 AIHubMix Key,并用免费 Gemini 3.7 Flash 识图?看这篇图文教程 →',
|
|
285
|
+
baseUrl: 'API 地址',
|
|
286
|
+
apiKey: 'API 密钥',
|
|
287
|
+
apiKeyPlaceholderMissing: '粘贴 API 密钥',
|
|
288
|
+
apiKeyPlaceholderConfigured: '已保存;留空表示不修改',
|
|
289
|
+
apiKeyHint: '密钥会保存到 DSH 凭据存储,保存后不会在页面中回显。',
|
|
290
|
+
apiKeyLocked: '当前密钥来自只读配置,无法在此替换。',
|
|
291
|
+
apiKeyBlank: 'API 密钥不能只包含空格。',
|
|
292
|
+
apiKeyInvalid: '请只粘贴密钥本身,不要包含变量名、引号、空格或换行。',
|
|
293
|
+
credential: '凭据名称',
|
|
294
|
+
credentialHint: '内置免费视觉服务无需用户密钥;切换到自定义服务时,此处是保存其密钥的 DSH 凭据名称。',
|
|
295
|
+
model: '模型名称',
|
|
296
|
+
protocol: 'API 协议',
|
|
297
|
+
anthropicThinking: 'Anthropic thinking',
|
|
298
|
+
anthropicThinkingHint: 'omit 兼容性最好。仅当所选模型明确支持时使用 disabled 或 adaptive;遇到 HTTP 400 时先恢复 omit。',
|
|
299
|
+
userAgent: 'User-Agent',
|
|
300
|
+
language: '结果语言',
|
|
301
|
+
limits: '资源与并发限制',
|
|
302
|
+
timeout: '单次请求超时(毫秒)',
|
|
303
|
+
maxBytes: '单张图片大小上限(字节)',
|
|
304
|
+
maxPixels: '单张图片最大像素数',
|
|
305
|
+
concurrency: '单个会话最多并发任务数',
|
|
306
|
+
runtime: '工具运行环境',
|
|
307
|
+
runtimeMode: '环境来源',
|
|
308
|
+
toolkitPath: 'agent-vision-toolkit 目录',
|
|
309
|
+
python: 'Python 解释器(可选)',
|
|
310
|
+
allowedDirs: '允许读取的其他目录',
|
|
311
|
+
allowedDirsHint: '每行填写一个目录。当前会话的工作目录始终可以读取,无需重复填写。',
|
|
312
|
+
save: '保存设置',
|
|
313
|
+
saving: '正在检查并应用…',
|
|
314
|
+
reload: '重新加载',
|
|
315
|
+
saved: '设置已保存并生效。',
|
|
316
|
+
readOnly: '服务设置来自只读配置;如果 API 密钥可写,仍可在此保存密钥。',
|
|
317
|
+
configured: '已就绪',
|
|
318
|
+
missing: '未配置',
|
|
319
|
+
source: '配置来源',
|
|
320
|
+
sourceHint: '{source}:{value}',
|
|
321
|
+
sourceEnv: '环境变量',
|
|
322
|
+
sourceFile: '凭据文件',
|
|
323
|
+
health: '运行检查',
|
|
324
|
+
runHealth: '检查本地环境',
|
|
325
|
+
testConnection: '测试 API 连接',
|
|
326
|
+
testModel: '测试视觉模型',
|
|
327
|
+
testing: '检查中…',
|
|
328
|
+
testingModel: '正在测试模型…',
|
|
329
|
+
connectionHint: '“测试 API 连接”只请求 GET /models;“测试视觉模型”会发送插件自带的诊断图片,验证一次真实多模态调用。',
|
|
330
|
+
saveBeforeTesting: '修改服务配置后,请先保存,再执行 API 或视觉模型测试。',
|
|
331
|
+
advanced: '高级设置',
|
|
332
|
+
advancedHint: '凭据名称、服务兼容参数、结果语言、资源限制、运行环境来源、Python 和额外可读目录。一般无需修改。',
|
|
333
|
+
imageInput: '图片输入',
|
|
334
|
+
hiddenVariants: '透明变体路由',
|
|
335
|
+
hiddenVariantsLabel: '保留原模型名并自动启用图片能力',
|
|
336
|
+
hiddenVariantsHint: '文本模型在模型列表中只显示原名称,会话实际运行在支持图片的变体路由上:粘贴图片、历史图片和内置 read_image 工具均可正常使用。关闭后恢复显示显式的(Vision Toolkit)条目。',
|
|
337
|
+
pluginVersion: '插件版本',
|
|
338
|
+
upstreamVersion: '工具包版本',
|
|
339
|
+
activeGeneration: '本次运行已应用',
|
|
340
|
+
activeGenerationValue: '{generation} 次',
|
|
341
|
+
updates: '插件更新',
|
|
342
|
+
updatesHint: '检查 npm 新版本,自动更新当前 DSH Profile 中的插件,然后重启 DSH Web。',
|
|
343
|
+
manualUpdate: '手动更新',
|
|
344
|
+
manualUpdateHint: '在终端运行以下命令,将当前 DSH Profile 更新到最新版本。',
|
|
345
|
+
copy: '复制',
|
|
346
|
+
copied: '已复制',
|
|
347
|
+
checkUpdate: '检查更新',
|
|
348
|
+
checkingUpdate: '正在检查更新…',
|
|
349
|
+
updateAvailable: '发现新版本',
|
|
350
|
+
updateAvailableDetail: '可更新到 {version}。能安全自重启时会自动重启,否则安装完成后会提示你手动重启。',
|
|
351
|
+
upToDate: '已是最新版',
|
|
352
|
+
upToDateDetail: '当前 {version} 已是最新正式版本。',
|
|
353
|
+
updateNow: '安装更新',
|
|
354
|
+
updatingPlugin: '正在安装更新…',
|
|
355
|
+
updateConfirm: '现在安装 Vision Toolkit {version} 吗?支持安全自重启时会自动重启,否则需要你手动重启 DSH Web。',
|
|
356
|
+
restarting: '已安装 {version},正在等待 DSH Web 重启…',
|
|
357
|
+
manualRestartRequired: '已安装 {version}。请按你平时的方式手动重启 DSH Web,重启后新版本生效。',
|
|
358
|
+
updateProfile: 'Profile',
|
|
359
|
+
updateInstalled: '当前版本',
|
|
360
|
+
updateLatest: '最新版本',
|
|
361
|
+
updateUnsupported: '当前安装方式不支持页面内更新。',
|
|
362
|
+
updateReasonProfileNotFound: '无法把正在运行的插件匹配到某个 DSH Profile 安装。',
|
|
363
|
+
updateReasonNotDependency: '该插件不是当前 DSH Profile 的直接依赖。',
|
|
364
|
+
updateReasonLocalSource: '当前使用本地、workspace、URL 或 git 安装;为避免覆盖本地修改,请手动更新对应来源。',
|
|
365
|
+
updateReasonReadOnly: '当前 Profile 的 package.json 不可写。',
|
|
366
|
+
updateReasonPnpm: 'DSH 运行环境中找不到 pnpm。',
|
|
367
|
+
updateReasonPlatform: '当前操作系统不支持安全的自动重启。',
|
|
368
|
+
updateReasonRestartUnmanaged: '默认禁用脱离原进程管理器的自重启。仅对无人监管的 Web 进程明确设置 DSH_VISION_TOOLKIT_ALLOW_DETACHED_RESTART=1 后开放。',
|
|
369
|
+
updateReasonRestartAddress: 'DSH Web 使用未知端口或动态端口时无法安全自动重启。请用固定的 --port 值启动。',
|
|
370
|
+
updateSaveFirst: '更新插件前,请先保存或放弃当前 Settings 和 API 密钥修改。',
|
|
371
|
+
restartTimedOut: 'DSH Web 未能以目标插件版本恢复。请检查重启日志,并通过原进程管理器重启 Web Profile。',
|
|
372
|
+
restartRolledBack: '新插件未能就绪,系统已恢复上一版本。再次尝试前请检查重启日志。',
|
|
373
|
+
pluginKind: 'DSH 原生插件',
|
|
374
|
+
runtimeUnavailable: '运行环境尚未就绪',
|
|
375
|
+
runtimeCandidateRejected: '新设置未能生效,仍在使用上一次可用的设置。',
|
|
376
|
+
runtimeReady: '已就绪',
|
|
377
|
+
runtimeManaged: '自动安装',
|
|
378
|
+
runtimeExternal: '本地源码',
|
|
379
|
+
retry: '重试',
|
|
380
|
+
open: '在工作区中打开',
|
|
381
|
+
download: '下载',
|
|
382
|
+
previewUnavailable: '此页面无法直接预览该文件,请在工作区中打开。',
|
|
383
|
+
running: '运行中…',
|
|
384
|
+
failed: '运行失败',
|
|
385
|
+
matches: '处匹配',
|
|
386
|
+
elements: '个元素',
|
|
387
|
+
dimensions: '图片尺寸',
|
|
388
|
+
coordinates: '坐标',
|
|
389
|
+
artifact: '生成文件',
|
|
390
|
+
artifacts: '个生成文件',
|
|
391
|
+
difference: '像素差异',
|
|
392
|
+
worstRegions: '差异最大的区域',
|
|
393
|
+
colors: '种颜色',
|
|
394
|
+
noResult: '未能读取结果,请查看工具的原始输出。',
|
|
395
|
+
healthy: '一切正常',
|
|
396
|
+
degraded: '有项目需要处理',
|
|
397
|
+
notTested: '尚未检查',
|
|
398
|
+
groundTitle: '目标定位',
|
|
399
|
+
detectTitle: '界面元素识别',
|
|
400
|
+
traceTitle: '描摹为 SVG',
|
|
401
|
+
pixelDiffTitle: '像素对比',
|
|
402
|
+
cropTitle: '裁剪图片',
|
|
403
|
+
longOcrTitle: '长图文字识别',
|
|
404
|
+
extractForegroundTitle: '提取前景',
|
|
405
|
+
htmlScreenshotTitle: '网页截图',
|
|
406
|
+
artifactTitle: '视觉处理结果',
|
|
407
|
+
dominantColorsTitle: '主色提取',
|
|
408
|
+
artifactGroundPreview: '目标定位框预览',
|
|
409
|
+
artifactDetectPreview: '界面元素标注预览',
|
|
410
|
+
artifactCrop: '裁剪后的图片',
|
|
411
|
+
artifactTrace: '描摹得到的矢量图',
|
|
412
|
+
artifactDiffHeatmap: '像素差异热力图',
|
|
413
|
+
artifactDiffReport: '像素差异详细报告',
|
|
414
|
+
artifactLongManifest: '长图切分与合并记录',
|
|
415
|
+
artifactLongTranscript: '长图文字识别结果',
|
|
416
|
+
artifactLongAudit: '长图分块边界检查记录',
|
|
417
|
+
artifactLongChunk: '长图文字识别分块 {index}',
|
|
418
|
+
artifactOcrSidecar: '分块 {index} 的文字识别记录',
|
|
419
|
+
artifactForeground: '提取后的透明背景前景图',
|
|
420
|
+
artifactHtmlScreenshot: '本地网页截图',
|
|
421
|
+
label: '名称',
|
|
422
|
+
paths: '条路径',
|
|
423
|
+
healthPython: 'Python',
|
|
424
|
+
healthDependencies: 'Python 依赖',
|
|
425
|
+
healthChrome: '浏览器',
|
|
426
|
+
healthCredential: 'API 密钥',
|
|
427
|
+
healthArtifactDirectory: '输出目录',
|
|
428
|
+
healthTempDirectory: '临时目录',
|
|
429
|
+
healthService: '视觉服务',
|
|
430
|
+
healthModel: '视觉模型',
|
|
431
|
+
statusOk: '正常',
|
|
432
|
+
statusWarning: '注意',
|
|
433
|
+
statusError: '异常',
|
|
434
|
+
statusNotTested: '未检查',
|
|
435
|
+
positiveInteger: '{field}必须填写正整数。',
|
|
436
|
+
healthPythonDetail: '版本 {version};解释器:{path}',
|
|
437
|
+
healthChromeMissing: '未找到 Chrome、Chromium 或 Edge,网页截图功能暂不可用。',
|
|
438
|
+
healthChromeProbeFailed: '无法检查浏览器是否可用。',
|
|
439
|
+
healthCredentialMissing: '尚未配置凭据 {credential}。',
|
|
440
|
+
healthCredentialReady: '已找到凭据 {credential}。',
|
|
441
|
+
healthCredentialFailed: '无法读取凭据 {credential}。',
|
|
442
|
+
healthDirectoryWritable: '{directory}可写:{path}',
|
|
443
|
+
healthDirectoryNotWritable: '{directory}不可写:{path}',
|
|
444
|
+
healthArtifactDirectoryFailed: '无法准备输出目录。',
|
|
445
|
+
healthConnectionNotTested: '尚未测试 API 连接。点击“测试 API 连接”可请求 /models。',
|
|
446
|
+
healthConnectionCredentialMissing: 'API 密钥不可用,未执行连接测试。',
|
|
447
|
+
healthServiceResponded: '服务已响应:{endpoint}(HTTP {status})。',
|
|
448
|
+
healthServiceRejectedCredential: '服务拒绝了当前 API 密钥(HTTP {status})。',
|
|
449
|
+
healthServiceForbidden: '服务可以访问,但对 GET /models 的访问被限制(HTTP {status})。这通常是账号或模型列表权限限制,不代表密钥无效;若“视觉模型已实测正常”,此警告可忽略。',
|
|
450
|
+
healthServiceNoModels: '服务可以访问,但不支持 GET /models(HTTP {status})。',
|
|
451
|
+
healthServiceRateLimited: '服务可以访问,但本次连接测试触发了限流(HTTP 429)。',
|
|
452
|
+
healthServiceHttpFailed: '连接测试失败(HTTP {status})。',
|
|
453
|
+
healthServiceUnreachable: '无法连接到 {endpoint}。',
|
|
454
|
+
healthModelNotTested: '尚未测试视觉模型。点击“测试视觉模型”可执行一次真实多模态请求。',
|
|
455
|
+
healthModelCredentialMissing: '视觉模型测试已跳过,因为当前 API 密钥不可用。',
|
|
456
|
+
healthModelReady: '模型 {model} 已完成一次真实多模态请求。',
|
|
457
|
+
healthModelFailed: '真实多模态请求失败:{detail}',
|
|
458
|
+
modelTestVerifiedTag: '已实测',
|
|
459
|
+
modelTestNotRunTag: '未测试',
|
|
460
|
+
modelTestFailedTag: '测试失败',
|
|
461
|
+
};
|
|
462
|
+
function isRecord(value) {
|
|
463
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
464
|
+
}
|
|
465
|
+
function textOfContent(block) {
|
|
466
|
+
if (!('kind' in block))
|
|
467
|
+
return '';
|
|
468
|
+
return block.content
|
|
469
|
+
.filter((entry) => entry.type === 'text')
|
|
470
|
+
.map(entry => entry.text)
|
|
471
|
+
.join('\n');
|
|
472
|
+
}
|
|
473
|
+
/** Decode canonical presentation metadata with a JSON-text fallback. */
|
|
474
|
+
function decodeVisionResult(block) {
|
|
475
|
+
if (!('kind' in block) || block.isError)
|
|
476
|
+
return undefined;
|
|
477
|
+
if (isRecord(block.meta))
|
|
478
|
+
return block.meta;
|
|
479
|
+
const text = textOfContent(block).trim();
|
|
480
|
+
if (text.length === 0)
|
|
481
|
+
return undefined;
|
|
482
|
+
try {
|
|
483
|
+
const parsed = JSON.parse(text);
|
|
484
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function accessMap(value) {
|
|
491
|
+
const map = new Map();
|
|
492
|
+
if (value === undefined)
|
|
493
|
+
return map;
|
|
494
|
+
const envelope = value[PRESENTATION_META_KEY];
|
|
495
|
+
if (!isRecord(envelope) || envelope.schemaVersion !== 1 || !Array.isArray(envelope.artifacts))
|
|
496
|
+
return map;
|
|
497
|
+
for (const entry of envelope.artifacts) {
|
|
498
|
+
if (!isRecord(entry) || typeof entry.path !== 'string' || typeof entry.previewUrl !== 'string' || typeof entry.downloadUrl !== 'string')
|
|
499
|
+
continue;
|
|
500
|
+
map.set(entry.path, entry);
|
|
501
|
+
}
|
|
502
|
+
return map;
|
|
503
|
+
}
|
|
504
|
+
function artifactFrom(value) {
|
|
505
|
+
if (!isRecord(value))
|
|
506
|
+
return undefined;
|
|
507
|
+
if (typeof value.path !== 'string'
|
|
508
|
+
|| typeof value.filename !== 'string'
|
|
509
|
+
|| typeof value.mimeType !== 'string'
|
|
510
|
+
|| (value.kind !== 'image' && value.kind !== 'svg' && value.kind !== 'markdown' && value.kind !== 'json')
|
|
511
|
+
|| typeof value.description !== 'string'
|
|
512
|
+
|| typeof value.sourceTool !== 'string'
|
|
513
|
+
|| (value.previewIntent !== 'image' && value.previewIntent !== 'svg' && value.previewIntent !== 'text' && value.previewIntent !== 'download')
|
|
514
|
+
|| typeof value.bytes !== 'number')
|
|
515
|
+
return undefined;
|
|
516
|
+
return value;
|
|
517
|
+
}
|
|
518
|
+
function collectArtifacts(value, found = new Map(), depth = 0) {
|
|
519
|
+
if (depth > 16)
|
|
520
|
+
return [...found.values()];
|
|
521
|
+
const artifact = artifactFrom(value);
|
|
522
|
+
if (artifact !== undefined) {
|
|
523
|
+
found.set(artifact.path, artifact);
|
|
524
|
+
return [...found.values()];
|
|
525
|
+
}
|
|
526
|
+
if (Array.isArray(value)) {
|
|
527
|
+
for (const entry of value)
|
|
528
|
+
collectArtifacts(entry, found, depth + 1);
|
|
529
|
+
}
|
|
530
|
+
else if (isRecord(value)) {
|
|
531
|
+
for (const entry of Object.values(value))
|
|
532
|
+
collectArtifacts(entry, found, depth + 1);
|
|
533
|
+
}
|
|
534
|
+
return [...found.values()];
|
|
535
|
+
}
|
|
536
|
+
function numberOf(value) {
|
|
537
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
538
|
+
}
|
|
539
|
+
function stringOf(value) {
|
|
540
|
+
return typeof value === 'string' ? value : undefined;
|
|
541
|
+
}
|
|
542
|
+
function formatBytes(bytes) {
|
|
543
|
+
if (bytes < 1024)
|
|
544
|
+
return `${bytes} B`;
|
|
545
|
+
if (bytes < 1024 * 1024)
|
|
546
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
547
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
548
|
+
}
|
|
549
|
+
function boxText(value) {
|
|
550
|
+
if (!isRecord(value))
|
|
551
|
+
return '—';
|
|
552
|
+
const parts = ['x1', 'y1', 'x2', 'y2'].map(key => numberOf(value[key]));
|
|
553
|
+
return parts.every(part => part !== undefined) ? parts.join(', ') : '—';
|
|
554
|
+
}
|
|
555
|
+
function statusText(block, t) {
|
|
556
|
+
if (!('kind' in block))
|
|
557
|
+
return t('running');
|
|
558
|
+
if (block.isError)
|
|
559
|
+
return textOfContent(block).split('\n')[0] || t('failed');
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
function VisionIcon({ kind = 'scan' }) {
|
|
563
|
+
const path = kind === 'target'
|
|
564
|
+
? 'M8 2v2m0 8v2M2 8h2m8 0h2M5 5h6v6H5z'
|
|
565
|
+
: kind === 'layers'
|
|
566
|
+
? 'm3 6 5-3 5 3-5 3-5-3Zm0 3 5 3 5-3M3 12l5 3 5-3'
|
|
567
|
+
: kind === 'shape'
|
|
568
|
+
? 'M3 12 6 4l7-1-1 7-9 2Zm3-8 6 6'
|
|
569
|
+
: kind === 'diff'
|
|
570
|
+
? 'M3 3h4v4H3V3Zm6 6h4v4H9V9Zm0-6h4M3 11h4'
|
|
571
|
+
: kind === 'palette'
|
|
572
|
+
? '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'
|
|
573
|
+
: 'M3 5V3h2M11 3h2v2M13 11v2h-2M5 13H3v-2M5 8h6';
|
|
574
|
+
return ((0, jsx_runtime_1.jsx)("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", fill: "none", stroke: "currentColor", strokeWidth: "1.35", strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)("path", { d: path }) }));
|
|
575
|
+
}
|
|
576
|
+
function ToolShell({ block, title, summary, icon, children, t, }) {
|
|
577
|
+
const [open, setOpen] = (0, react_1.useState)(true);
|
|
578
|
+
const status = statusText(block, t);
|
|
579
|
+
const expandable = children !== undefined && children !== null;
|
|
580
|
+
return ((0, jsx_runtime_1.jsxs)("section", { className: "dvt-tool", "data-state": !('kind' in block) ? 'running' : block.isError ? 'error' : 'success', children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "dvt-tool-head", onClick: () => { if (expandable)
|
|
581
|
+
setOpen(value => !value); }, "aria-expanded": expandable ? open : undefined, children: [(0, jsx_runtime_1.jsx)("span", { className: "dvt-tool-icon", children: icon }), (0, jsx_runtime_1.jsx)("span", { className: "dvt-tool-title", children: title }), summary !== undefined && summary.length > 0 ? (0, jsx_runtime_1.jsx)("span", { className: "dvt-tool-sep", "aria-hidden": "true", children: "\u00B7" }) : null, summary !== undefined ? (0, jsx_runtime_1.jsx)("span", { className: "dvt-tool-summary", children: summary }) : null, status !== undefined ? (0, jsx_runtime_1.jsx)("span", { className: "dvt-tool-status", children: status }) : null, expandable ? (0, jsx_runtime_1.jsx)("span", { className: "dvt-chevron", "data-open": open || undefined, children: "\u2304" }) : null] }), expandable && open ? (0, jsx_runtime_1.jsx)("div", { className: "dvt-tool-body", children: children }) : null] }));
|
|
582
|
+
}
|
|
583
|
+
function ArtifactActions({ artifact, grant, openFile, t }) {
|
|
584
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "dvt-actions", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { size: "sm", variant: "outline", onClick: () => { openFile(artifact.path); }, children: t('open') }), grant === undefined ? null : (0, jsx_runtime_1.jsx)("a", { className: "dvt-download", href: grant.downloadUrl, download: artifact.filename, children: t('download') })] }));
|
|
585
|
+
}
|
|
586
|
+
function ArtifactPreview({ artifact, grant, openFile, t }) {
|
|
587
|
+
const canPreview = grant !== undefined && (artifact.kind === 'image' || artifact.kind === 'svg');
|
|
588
|
+
const description = artifactDescription(artifact.description, t);
|
|
589
|
+
return ((0, jsx_runtime_1.jsxs)("article", { className: "dvt-artifact", children: [canPreview
|
|
590
|
+
? artifact.kind === 'svg'
|
|
591
|
+
? (0, jsx_runtime_1.jsx)("iframe", { className: "dvt-preview dvt-svg", sandbox: "", src: grant.previewUrl, title: description })
|
|
592
|
+
: (0, jsx_runtime_1.jsx)("img", { className: "dvt-preview", src: grant.previewUrl, alt: description, loading: "lazy" })
|
|
593
|
+
: null, (0, jsx_runtime_1.jsxs)("div", { className: "dvt-artifact-meta", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: artifact.filename }), (0, jsx_runtime_1.jsx)("span", { children: description }), (0, jsx_runtime_1.jsxs)("small", { children: [artifact.mimeType, " \u00B7 ", formatBytes(artifact.bytes)] })] }), (0, jsx_runtime_1.jsx)(ArtifactActions, { artifact: artifact, grant: grant, openFile: openFile, t: t })] }), !canPreview && grant === undefined ? (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('previewUnavailable') }) : null] }));
|
|
594
|
+
}
|
|
595
|
+
const ARTIFACT_DESCRIPTION_KEYS = {
|
|
596
|
+
'Grounding bounding-box preview': 'artifactGroundPreview',
|
|
597
|
+
'Detected-element bounding-box preview': 'artifactDetectPreview',
|
|
598
|
+
'Cropped image region': 'artifactCrop',
|
|
599
|
+
'Traced vector geometry': 'artifactTrace',
|
|
600
|
+
'Pixel-difference heatmap': 'artifactDiffHeatmap',
|
|
601
|
+
'Structured pixel-difference report': 'artifactDiffReport',
|
|
602
|
+
'Long-screenshot split and merge manifest': 'artifactLongManifest',
|
|
603
|
+
'Merged long-screenshot OCR transcript': 'artifactLongTranscript',
|
|
604
|
+
'Long-screenshot OCR boundary audit': 'artifactLongAudit',
|
|
605
|
+
'Extracted transparent foreground': 'artifactForeground',
|
|
606
|
+
'Headless browser screenshot of local HTML': 'artifactHtmlScreenshot',
|
|
607
|
+
};
|
|
608
|
+
function artifactDescription(description, t) {
|
|
609
|
+
const key = ARTIFACT_DESCRIPTION_KEYS[description];
|
|
610
|
+
if (key !== undefined) {
|
|
611
|
+
const translated = t(key);
|
|
612
|
+
return translated === key ? description : translated;
|
|
613
|
+
}
|
|
614
|
+
let match = /^Long-screenshot OCR chunk (\d+)$/u.exec(description);
|
|
615
|
+
if (match !== null) {
|
|
616
|
+
const translated = t('artifactLongChunk', { index: match[1] });
|
|
617
|
+
return translated === 'artifactLongChunk' ? description : translated;
|
|
618
|
+
}
|
|
619
|
+
match = /^OCR sidecar for chunk (\d+)$/u.exec(description);
|
|
620
|
+
if (match !== null) {
|
|
621
|
+
const translated = t('artifactOcrSidecar', { index: match[1] });
|
|
622
|
+
return translated === 'artifactOcrSidecar' ? description : translated;
|
|
623
|
+
}
|
|
624
|
+
return description;
|
|
625
|
+
}
|
|
626
|
+
function GroundView({ block, openFile, t = key => en[key] }) {
|
|
627
|
+
const value = decodeVisionResult(block);
|
|
628
|
+
const matches = Array.isArray(value?.matches) ? value.matches.filter(isRecord) : [];
|
|
629
|
+
const target = stringOf(value?.target) ?? t('groundTitle');
|
|
630
|
+
const width = numberOf(value?.imageWidth);
|
|
631
|
+
const height = numberOf(value?.imageHeight);
|
|
632
|
+
const preview = artifactFrom(value?.preview);
|
|
633
|
+
const grants = accessMap(value);
|
|
634
|
+
return ((0, jsx_runtime_1.jsx)(ToolShell, { block: block, title: t('groundTitle'), summary: matches.length > 0 ? `${target} · ${matches.length} ${t('matches')}` : target, icon: (0, jsx_runtime_1.jsx)(VisionIcon, { kind: "target" }), t: t, children: value === undefined ? (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('noResult') }) : ((0, jsx_runtime_1.jsxs)("div", { className: "dvt-stack", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-metrics", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { children: t('dimensions') }), (0, jsx_runtime_1.jsxs)("strong", { children: [width ?? '—', " \u00D7 ", height ?? '—'] })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { children: t('coordinates') }), (0, jsx_runtime_1.jsx)("strong", { children: matches[0] === undefined ? '—' : boxText(matches[0].box) })] })] }), matches.length > 1 ? ((0, jsx_runtime_1.jsx)("ol", { className: "dvt-list", children: matches.map((match, index) => (0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("span", { children: stringOf(match.label) ?? `#${index + 1}` }), (0, jsx_runtime_1.jsx)("code", { children: boxText(match.box) })] }, index)) })) : null, preview === undefined ? null : (0, jsx_runtime_1.jsx)(ArtifactPreview, { artifact: preview, grant: grants.get(preview.path), openFile: openFile, t: t })] })) }));
|
|
635
|
+
}
|
|
636
|
+
function DetectView({ block, openFile, t = key => en[key] }) {
|
|
637
|
+
const value = decodeVisionResult(block);
|
|
638
|
+
const elements = Array.isArray(value?.elements) ? value.elements.filter(isRecord) : [];
|
|
639
|
+
const width = numberOf(value?.imageWidth);
|
|
640
|
+
const height = numberOf(value?.imageHeight);
|
|
641
|
+
const preview = artifactFrom(value?.preview);
|
|
642
|
+
const grants = accessMap(value);
|
|
643
|
+
return ((0, jsx_runtime_1.jsx)(ToolShell, { block: block, title: t('detectTitle'), summary: `${elements.length} ${t('elements')}`, icon: (0, jsx_runtime_1.jsx)(VisionIcon, { kind: "layers" }), t: t, children: value === undefined ? (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('noResult') }) : ((0, jsx_runtime_1.jsxs)("div", { className: "dvt-stack", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-metrics", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { children: t('dimensions') }), (0, jsx_runtime_1.jsxs)("strong", { children: [width ?? '—', " \u00D7 ", height ?? '—'] })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { children: t('elements') }), (0, jsx_runtime_1.jsx)("strong", { children: elements.length })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "dvt-table-wrap", children: (0, jsx_runtime_1.jsxs)("table", { className: "dvt-table", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { children: "#" }), (0, jsx_runtime_1.jsx)("th", { children: t('label') }), (0, jsx_runtime_1.jsx)("th", { children: t('coordinates') })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: elements.map((element, index) => (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { children: numberOf(element.index) ?? index + 1 }), (0, jsx_runtime_1.jsx)("td", { children: stringOf(element.label) ?? '—' }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("code", { children: boxText(element.box) }) })] }, index)) })] }) }), preview === undefined ? null : (0, jsx_runtime_1.jsx)(ArtifactPreview, { artifact: preview, grant: grants.get(preview.path), openFile: openFile, t: t })] })) }));
|
|
644
|
+
}
|
|
645
|
+
function TraceView({ block, openFile, t = key => en[key] }) {
|
|
646
|
+
const value = decodeVisionResult(block);
|
|
647
|
+
const artifact = artifactFrom(value?.artifact);
|
|
648
|
+
const geometry = isRecord(value?.geometry) ? value.geometry : undefined;
|
|
649
|
+
const summary = geometry === undefined ? undefined : `${numberOf(geometry.pathCount) ?? 0} ${t('paths')} · ${formatBytes(numberOf(geometry.bytes) ?? 0)}`;
|
|
650
|
+
const grants = accessMap(value);
|
|
651
|
+
return ((0, jsx_runtime_1.jsx)(ToolShell, { block: block, title: t('traceTitle'), summary: summary, icon: (0, jsx_runtime_1.jsx)(VisionIcon, { kind: "shape" }), t: t, children: artifact === undefined ? (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('noResult') }) : (0, jsx_runtime_1.jsx)(ArtifactPreview, { artifact: artifact, grant: grants.get(artifact.path), openFile: openFile, t: t }) }));
|
|
652
|
+
}
|
|
653
|
+
function PixelDiffView({ block, openFile, t = key => en[key] }) {
|
|
654
|
+
const value = decodeVisionResult(block);
|
|
655
|
+
const pct = numberOf(value?.overallDifferencePct);
|
|
656
|
+
const regions = Array.isArray(value?.worstRegions) ? value.worstRegions.filter(isRecord) : [];
|
|
657
|
+
const heatmap = artifactFrom(value?.heatmap);
|
|
658
|
+
const report = artifactFrom(value?.report);
|
|
659
|
+
const grants = accessMap(value);
|
|
660
|
+
return ((0, jsx_runtime_1.jsx)(ToolShell, { block: block, title: t('pixelDiffTitle'), summary: pct === undefined ? undefined : `${pct.toFixed(3)}%`, icon: (0, jsx_runtime_1.jsx)(VisionIcon, { kind: "diff" }), t: t, children: value === undefined ? (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('noResult') }) : ((0, jsx_runtime_1.jsxs)("div", { className: "dvt-stack", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-diff-score", children: [(0, jsx_runtime_1.jsx)("span", { children: t('difference') }), (0, jsx_runtime_1.jsxs)("strong", { children: [pct?.toFixed(4) ?? '—', "%"] }), (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("i", { style: { width: `${Math.min(100, Math.max(0, pct ?? 0))}%` } }) })] }), regions.length === 0 ? null : (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h4", { children: t('worstRegions') }), (0, jsx_runtime_1.jsx)("ol", { className: "dvt-list", children: regions.map((region, index) => (0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsxs)("span", { children: [(numberOf(region.differencePct) ?? 0).toFixed(3), "%"] }), (0, jsx_runtime_1.jsx)("code", { children: boxText(region.box) })] }, index)) })] }), heatmap === undefined ? null : (0, jsx_runtime_1.jsx)(ArtifactPreview, { artifact: heatmap, grant: grants.get(heatmap.path), openFile: openFile, t: t }), report === undefined ? null : (0, jsx_runtime_1.jsx)(ArtifactPreview, { artifact: report, grant: grants.get(report.path), openFile: openFile, t: t })] })) }));
|
|
661
|
+
}
|
|
662
|
+
function ArtifactView({ block, openFile, toolName, t = key => en[key] }) {
|
|
663
|
+
const value = decodeVisionResult(block);
|
|
664
|
+
const artifacts = collectArtifacts(value);
|
|
665
|
+
const grants = accessMap(value);
|
|
666
|
+
const title = toolName === 'vision_crop' ? t('cropTitle')
|
|
667
|
+
: toolName === 'vision_long_screenshot_ocr' ? t('longOcrTitle')
|
|
668
|
+
: toolName === 'vision_extract_foreground' ? t('extractForegroundTitle')
|
|
669
|
+
: toolName === 'vision_html_screenshot' ? t('htmlScreenshotTitle')
|
|
670
|
+
: t('artifactTitle');
|
|
671
|
+
return ((0, jsx_runtime_1.jsx)(ToolShell, { block: block, title: title, summary: artifacts.length > 0 ? `${artifacts.length} ${t('artifacts')}` : undefined, icon: (0, jsx_runtime_1.jsx)(VisionIcon, {}), t: t, children: artifacts.length === 0 ? (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('noResult') }) : (0, jsx_runtime_1.jsx)("div", { className: "dvt-stack", children: artifacts.map(artifact => (0, jsx_runtime_1.jsx)(ArtifactPreview, { artifact: artifact, grant: grants.get(artifact.path), openFile: openFile, t: t }, artifact.path)) }) }));
|
|
672
|
+
}
|
|
673
|
+
function PaletteView({ block, t = key => en[key] }) {
|
|
674
|
+
const value = decodeVisionResult(block);
|
|
675
|
+
const analysis = isRecord(value?.analysis) ? value.analysis : undefined;
|
|
676
|
+
const colors = Array.isArray(analysis?.colors) ? analysis.colors.filter(isRecord) : [];
|
|
677
|
+
return ((0, jsx_runtime_1.jsx)(ToolShell, { block: block, title: t('dominantColorsTitle'), summary: `${colors.length} ${t('colors')}`, icon: (0, jsx_runtime_1.jsx)(VisionIcon, { kind: "palette" }), t: t, children: colors.length === 0 ? (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('noResult') }) : (0, jsx_runtime_1.jsx)("div", { className: "dvt-palette", children: colors.map((color, index) => {
|
|
678
|
+
const hex = stringOf(color.color) ?? '#000000';
|
|
679
|
+
const share = numberOf(color.sharePct);
|
|
680
|
+
return (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("i", { style: { background: hex } }), (0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)("strong", { children: hex }), (0, jsx_runtime_1.jsx)("small", { children: share === undefined ? '' : `${share.toFixed(2)}%` })] })] }, `${hex}-${index}`);
|
|
681
|
+
}) }) }));
|
|
682
|
+
}
|
|
683
|
+
async function apiRequest(init) {
|
|
684
|
+
const response = await fetch(SETTINGS_ROUTE, { credentials: 'same-origin', ...init });
|
|
685
|
+
const body = await response.json();
|
|
686
|
+
if (!response.ok || !body.ok) {
|
|
687
|
+
const failure = body;
|
|
688
|
+
throw new Error(failure.error?.message ?? `Vision Toolkit request failed with HTTP ${response.status}`);
|
|
689
|
+
}
|
|
690
|
+
return body.value;
|
|
691
|
+
}
|
|
692
|
+
/** Small external store shared by the Settings route and pushed invalidations. */
|
|
693
|
+
class VisionSettingsController {
|
|
694
|
+
state = { status: 'idle' };
|
|
695
|
+
listeners = new Set();
|
|
696
|
+
generation = 0;
|
|
697
|
+
subscribe = (listener) => {
|
|
698
|
+
this.listeners.add(listener);
|
|
699
|
+
return () => { this.listeners.delete(listener); };
|
|
700
|
+
};
|
|
701
|
+
snapshot = () => this.state;
|
|
702
|
+
set(next) {
|
|
703
|
+
this.state = next;
|
|
704
|
+
for (const listener of this.listeners)
|
|
705
|
+
listener();
|
|
706
|
+
}
|
|
707
|
+
async load() {
|
|
708
|
+
const generation = ++this.generation;
|
|
709
|
+
this.set({ ...this.state, status: 'loading', error: undefined, message: undefined });
|
|
710
|
+
try {
|
|
711
|
+
const snapshot = await apiRequest();
|
|
712
|
+
if (generation !== this.generation)
|
|
713
|
+
return;
|
|
714
|
+
this.set({
|
|
715
|
+
status: 'ready',
|
|
716
|
+
snapshot,
|
|
717
|
+
health: this.state.health,
|
|
718
|
+
update: this.state.update,
|
|
719
|
+
restart: this.state.restart,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
if (generation !== this.generation)
|
|
724
|
+
return;
|
|
725
|
+
this.set({ ...this.state, status: 'error', error: error instanceof Error ? error.message : String(error) });
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
refreshIfLoaded() {
|
|
729
|
+
if (this.state.status === 'idle' || this.state.action === 'save')
|
|
730
|
+
return;
|
|
731
|
+
void this.load();
|
|
732
|
+
}
|
|
733
|
+
async save(value, expectedRevision, credentialValue, writeSettings) {
|
|
734
|
+
this.set({ ...this.state, action: 'save', error: undefined, message: undefined });
|
|
735
|
+
let snapshot = this.state.snapshot;
|
|
736
|
+
try {
|
|
737
|
+
if (writeSettings) {
|
|
738
|
+
snapshot = await apiRequest({
|
|
739
|
+
method: 'POST',
|
|
740
|
+
headers: { 'Content-Type': 'application/json' },
|
|
741
|
+
body: JSON.stringify({ action: 'save', expectedRevision, value }),
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
if (snapshot === undefined)
|
|
745
|
+
throw new Error('Vision Toolkit Settings are unavailable');
|
|
746
|
+
if (credentialValue !== undefined) {
|
|
747
|
+
try {
|
|
748
|
+
snapshot = await apiRequest({
|
|
749
|
+
method: 'POST',
|
|
750
|
+
headers: { 'Content-Type': 'application/json' },
|
|
751
|
+
body: JSON.stringify({
|
|
752
|
+
action: 'credential',
|
|
753
|
+
expectedRevision: snapshot.settings.revision,
|
|
754
|
+
ref: snapshot.credential.ref,
|
|
755
|
+
value: credentialValue,
|
|
756
|
+
}),
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
this.set({
|
|
761
|
+
status: 'ready',
|
|
762
|
+
snapshot,
|
|
763
|
+
health: this.state.health,
|
|
764
|
+
update: this.state.update,
|
|
765
|
+
restart: this.state.restart,
|
|
766
|
+
error: error instanceof Error ? error.message : String(error),
|
|
767
|
+
});
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
this.set({
|
|
772
|
+
status: 'ready',
|
|
773
|
+
snapshot,
|
|
774
|
+
health: this.state.health,
|
|
775
|
+
update: this.state.update,
|
|
776
|
+
restart: this.state.restart,
|
|
777
|
+
message: 'saved',
|
|
778
|
+
});
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) });
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
finally {
|
|
786
|
+
// The backend commits the generation before the response is readable, so
|
|
787
|
+
// the browser cache must not keep serving the previous hidden flag.
|
|
788
|
+
(0, display_config_ts_1.resetDisplayConfigCache)();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
async runHealth(mode) {
|
|
792
|
+
const testConnection = mode !== 'health';
|
|
793
|
+
const testModel = mode === 'model';
|
|
794
|
+
this.set({ ...this.state, action: mode, error: undefined, message: undefined });
|
|
795
|
+
try {
|
|
796
|
+
const health = await apiRequest({
|
|
797
|
+
method: 'POST',
|
|
798
|
+
headers: { 'Content-Type': 'application/json' },
|
|
799
|
+
body: JSON.stringify({ action: 'health', testConnection, testModel }),
|
|
800
|
+
});
|
|
801
|
+
this.set({ ...this.state, action: undefined, health });
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async checkUpdate() {
|
|
808
|
+
this.set({ ...this.state, action: 'check-update', error: undefined, message: undefined });
|
|
809
|
+
try {
|
|
810
|
+
const update = await apiRequest({
|
|
811
|
+
method: 'POST',
|
|
812
|
+
headers: { 'Content-Type': 'application/json' },
|
|
813
|
+
body: JSON.stringify({ action: 'check-update' }),
|
|
814
|
+
});
|
|
815
|
+
this.set({ ...this.state, action: undefined, update });
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) });
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
async applyUpdate(expectedVersion) {
|
|
822
|
+
this.set({ ...this.state, action: 'apply-update', error: undefined, message: undefined });
|
|
823
|
+
try {
|
|
824
|
+
const result = await apiRequest({
|
|
825
|
+
method: 'POST',
|
|
826
|
+
headers: { 'Content-Type': 'application/json' },
|
|
827
|
+
body: JSON.stringify({ action: 'apply-update', expectedVersion }),
|
|
828
|
+
});
|
|
829
|
+
this.set({
|
|
830
|
+
...this.state,
|
|
831
|
+
action: undefined,
|
|
832
|
+
restart: result,
|
|
833
|
+
message: result.restarting ? 'restarting' : 'manual-restart-required',
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) });
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
reportRestartTimeout(message) {
|
|
841
|
+
this.set({ ...this.state, restart: undefined, message: undefined, error: message });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
exports.VisionSettingsController = VisionSettingsController;
|
|
845
|
+
function draftOf(value) {
|
|
846
|
+
return {
|
|
847
|
+
baseUrl: value.provider?.baseUrl ?? BUILT_IN_FREE_VISION_BASE_URL,
|
|
848
|
+
credential: value.provider?.credential ?? BUILT_IN_FREE_VISION_CREDENTIAL,
|
|
849
|
+
model: value.provider?.model ?? BUILT_IN_FREE_VISION_MODEL,
|
|
850
|
+
protocol: value.provider?.protocol ?? 'openai',
|
|
851
|
+
anthropicThinking: value.provider?.anthropicThinking ?? 'omit',
|
|
852
|
+
userAgent: value.provider?.userAgent ?? DEFAULT_USER_AGENT,
|
|
853
|
+
language: value.language ?? 'zh',
|
|
854
|
+
timeoutMs: String(value.timeoutMs ?? 30000),
|
|
855
|
+
maxImageBytes: String(value.maxImageBytes ?? 4194304),
|
|
856
|
+
maxImagePixels: String(value.maxImagePixels ?? 20000000),
|
|
857
|
+
concurrency: String(value.concurrency ?? 4),
|
|
858
|
+
runtimeMode: value.runtime?.mode ?? 'managed',
|
|
859
|
+
toolkitPath: value.runtime?.agentVisionToolkitPath ?? '',
|
|
860
|
+
python: value.runtime?.python ?? '',
|
|
861
|
+
allowedDirs: (value.allowedDirs ?? []).join('\n'),
|
|
862
|
+
hiddenVariants: value.imageInputVariants?.hidden ?? true,
|
|
863
|
+
variantEnabled: value.imageInputVariants?.enabled ?? true,
|
|
864
|
+
variantProviders: (value.imageInputVariants?.providers ?? []).join('\n'),
|
|
865
|
+
variantAutoSwitch: value.imageInputVariants?.autoSwitch ?? true,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function positiveInteger(raw, label, t) {
|
|
869
|
+
const value = Number(raw);
|
|
870
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
871
|
+
throw new Error(t('positiveInteger', { field: label }));
|
|
872
|
+
return value;
|
|
873
|
+
}
|
|
874
|
+
function apiKeyFailure(value, t) {
|
|
875
|
+
if (value.length === 0)
|
|
876
|
+
return undefined;
|
|
877
|
+
const trimmed = value.trim();
|
|
878
|
+
if (trimmed.length === 0)
|
|
879
|
+
return t('apiKeyBlank');
|
|
880
|
+
const quoted = trimmed.length > 1 && ['"', '\'', '`'].includes(trimmed[0] ?? '') && trimmed.endsWith(trimmed[0] ?? '');
|
|
881
|
+
const environmentLine = /^[A-Z][A-Z0-9_]*=[^=]/u.test(trimmed);
|
|
882
|
+
if (quoted || environmentLine || !/^[\x21-\x7E]+$/u.test(trimmed))
|
|
883
|
+
return t('apiKeyInvalid');
|
|
884
|
+
return undefined;
|
|
885
|
+
}
|
|
886
|
+
function valueOf(draft, t) {
|
|
887
|
+
return {
|
|
888
|
+
provider: {
|
|
889
|
+
baseUrl: draft.baseUrl.trim(),
|
|
890
|
+
credential: draft.credential.trim(),
|
|
891
|
+
model: draft.model.trim(),
|
|
892
|
+
protocol: draft.protocol,
|
|
893
|
+
anthropicThinking: draft.anthropicThinking,
|
|
894
|
+
userAgent: draft.userAgent.trim(),
|
|
895
|
+
},
|
|
896
|
+
language: draft.language,
|
|
897
|
+
timeoutMs: positiveInteger(draft.timeoutMs, t('timeout'), t),
|
|
898
|
+
maxImageBytes: positiveInteger(draft.maxImageBytes, t('maxBytes'), t),
|
|
899
|
+
maxImagePixels: positiveInteger(draft.maxImagePixels, t('maxPixels'), t),
|
|
900
|
+
concurrency: positiveInteger(draft.concurrency, t('concurrency'), t),
|
|
901
|
+
runtime: {
|
|
902
|
+
mode: draft.runtimeMode,
|
|
903
|
+
...(draft.runtimeMode === 'external' ? { agentVisionToolkitPath: draft.toolkitPath.trim() } : {}),
|
|
904
|
+
...(draft.python.trim().length === 0 ? {} : { python: draft.python.trim() }),
|
|
905
|
+
},
|
|
906
|
+
allowedDirs: draft.allowedDirs.split(/\r?\n/).map(entry => entry.trim()).filter(Boolean),
|
|
907
|
+
imageInputVariants: {
|
|
908
|
+
...(draft.variantEnabled ? {} : { enabled: false }),
|
|
909
|
+
...(draft.variantProviders.trim().length === 0 ? {} : {
|
|
910
|
+
providers: draft.variantProviders.split(/\r?\n/).map(entry => entry.trim()).filter(Boolean),
|
|
911
|
+
}),
|
|
912
|
+
...(draft.variantAutoSwitch ? {} : { autoSwitch: false }),
|
|
913
|
+
hidden: draft.hiddenVariants,
|
|
914
|
+
},
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
function settingsDraftChanged(draft, saved, t) {
|
|
918
|
+
try {
|
|
919
|
+
return JSON.stringify(valueOf(draft, t)) !== JSON.stringify(valueOf(draftOf(saved), t));
|
|
920
|
+
}
|
|
921
|
+
catch {
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function isBuiltInFreeVisionDraft(draft) {
|
|
926
|
+
return draft.baseUrl.trim().replace(/\/+$/, '') === BUILT_IN_FREE_VISION_BASE_URL
|
|
927
|
+
&& draft.credential.trim() === BUILT_IN_FREE_VISION_CREDENTIAL
|
|
928
|
+
&& draft.model.trim() === BUILT_IN_FREE_VISION_MODEL
|
|
929
|
+
&& draft.protocol === 'openai';
|
|
930
|
+
}
|
|
931
|
+
function Field({ label, children, hint }) {
|
|
932
|
+
return (0, jsx_runtime_1.jsxs)("label", { className: "dvt-field", children: [(0, jsx_runtime_1.jsx)("span", { children: label }), children, hint === undefined ? null : (0, jsx_runtime_1.jsx)("small", { children: hint })] });
|
|
933
|
+
}
|
|
934
|
+
function SettingsSection({ controller, t }) {
|
|
935
|
+
if (controller === undefined || t === undefined)
|
|
936
|
+
return null;
|
|
937
|
+
return (0, jsx_runtime_1.jsx)(LoadedSettings, { controller: controller, t: t });
|
|
938
|
+
}
|
|
939
|
+
const HEALTH_NAME_KEYS = {
|
|
940
|
+
python: 'healthPython',
|
|
941
|
+
dependencies: 'healthDependencies',
|
|
942
|
+
chrome: 'healthChrome',
|
|
943
|
+
credential: 'healthCredential',
|
|
944
|
+
artifactDirectory: 'healthArtifactDirectory',
|
|
945
|
+
tempDirectory: 'healthTempDirectory',
|
|
946
|
+
service: 'healthService',
|
|
947
|
+
model: 'healthModel',
|
|
948
|
+
};
|
|
949
|
+
const HEALTH_STATUS_KEYS = {
|
|
950
|
+
ok: 'statusOk',
|
|
951
|
+
warning: 'statusWarning',
|
|
952
|
+
error: 'statusError',
|
|
953
|
+
not_tested: 'statusNotTested',
|
|
954
|
+
};
|
|
955
|
+
function healthDetail(name, detail, t) {
|
|
956
|
+
if (name === 'python') {
|
|
957
|
+
const match = /^(.+) via (.+)$/u.exec(detail);
|
|
958
|
+
if (match !== null)
|
|
959
|
+
return t('healthPythonDetail', { version: match[1], path: match[2] });
|
|
960
|
+
}
|
|
961
|
+
if (detail === 'Chrome/Chromium/Edge was not found; vision_html_screenshot is unavailable')
|
|
962
|
+
return t('healthChromeMissing');
|
|
963
|
+
if (detail === 'Chrome availability probe failed')
|
|
964
|
+
return t('healthChromeProbeFailed');
|
|
965
|
+
let match = /^credential (.+) is not configured$/u.exec(detail);
|
|
966
|
+
if (match !== null)
|
|
967
|
+
return t('healthCredentialMissing', { credential: match[1] });
|
|
968
|
+
match = /^credential (.+) is resolvable$/u.exec(detail);
|
|
969
|
+
if (match !== null)
|
|
970
|
+
return t('healthCredentialReady', { credential: match[1] });
|
|
971
|
+
match = /^credential (.+) could not be resolved$/u.exec(detail);
|
|
972
|
+
if (match !== null)
|
|
973
|
+
return t('healthCredentialFailed', { credential: match[1] });
|
|
974
|
+
match = /^(Artifact directory|Runtime temp directory) is writable: (.+)$/u.exec(detail);
|
|
975
|
+
if (match !== null)
|
|
976
|
+
return t('healthDirectoryWritable', {
|
|
977
|
+
directory: match[1] === 'Artifact directory' ? t('healthArtifactDirectory') : t('healthTempDirectory'),
|
|
978
|
+
path: match[2],
|
|
979
|
+
});
|
|
980
|
+
match = /^(Artifact directory|Runtime temp directory) is not writable: (.+)$/u.exec(detail);
|
|
981
|
+
if (match !== null)
|
|
982
|
+
return t('healthDirectoryNotWritable', {
|
|
983
|
+
directory: match[1] === 'Artifact directory' ? t('healthArtifactDirectory') : t('healthTempDirectory'),
|
|
984
|
+
path: match[2],
|
|
985
|
+
});
|
|
986
|
+
if (detail === 'Artifact directory could not be prepared')
|
|
987
|
+
return t('healthArtifactDirectoryFailed');
|
|
988
|
+
if (detail === 'Connection was not tested; pass testConnection=true to query the configured /models endpoint')
|
|
989
|
+
return t('healthConnectionNotTested');
|
|
990
|
+
if (detail === 'Connection test skipped because the configured credential is unavailable')
|
|
991
|
+
return t('healthConnectionCredentialMissing');
|
|
992
|
+
match = /^Service responded at (.+) \(HTTP (\d+)\)$/u.exec(detail);
|
|
993
|
+
if (match !== null)
|
|
994
|
+
return t('healthServiceResponded', { endpoint: match[1], status: match[2] });
|
|
995
|
+
match = /^Service rejected the configured credential \(HTTP (\d+)\)$/u.exec(detail);
|
|
996
|
+
if (match !== null)
|
|
997
|
+
return t('healthServiceRejectedCredential', { status: match[1] });
|
|
998
|
+
match = /^Service is reachable but restricted GET \/models \(HTTP (\d+)\); the credential may still be valid for real vision requests$/u.exec(detail);
|
|
999
|
+
if (match !== null)
|
|
1000
|
+
return t('healthServiceForbidden', { status: match[1] });
|
|
1001
|
+
match = /^Service is reachable but does not expose GET \/models \(HTTP (\d+)\)$/u.exec(detail);
|
|
1002
|
+
if (match !== null)
|
|
1003
|
+
return t('healthServiceNoModels', { status: match[1] });
|
|
1004
|
+
if (detail === 'Service is reachable but rate-limited the connection test (HTTP 429)')
|
|
1005
|
+
return t('healthServiceRateLimited');
|
|
1006
|
+
match = /^Service connection test failed with HTTP (\d+)$/u.exec(detail);
|
|
1007
|
+
if (match !== null)
|
|
1008
|
+
return t('healthServiceHttpFailed', { status: match[1] });
|
|
1009
|
+
match = /^Service could not be reached at (.+)$/u.exec(detail);
|
|
1010
|
+
if (match !== null)
|
|
1011
|
+
return t('healthServiceUnreachable', { endpoint: match[1] });
|
|
1012
|
+
if (detail === 'Vision model was not tested; run an explicit model test to send the bundled diagnostic image')
|
|
1013
|
+
return t('healthModelNotTested');
|
|
1014
|
+
if (detail === 'Vision model test skipped because the configured credential is unavailable')
|
|
1015
|
+
return t('healthModelCredentialMissing');
|
|
1016
|
+
match = /^Vision model (.+) completed a multimodal request$/u.exec(detail);
|
|
1017
|
+
if (match !== null)
|
|
1018
|
+
return t('healthModelReady', { model: match[1] });
|
|
1019
|
+
match = /^Vision model test failed: (.+)$/u.exec(detail);
|
|
1020
|
+
if (match !== null)
|
|
1021
|
+
return t('healthModelFailed', { detail: match[1] });
|
|
1022
|
+
return detail;
|
|
1023
|
+
}
|
|
1024
|
+
function modelTestTag(health, check) {
|
|
1025
|
+
if (!health.modelTested)
|
|
1026
|
+
return { status: 'warning', label: 'modelTestNotRunTag' };
|
|
1027
|
+
if (check.status === 'ok')
|
|
1028
|
+
return { status: 'ok', label: 'modelTestVerifiedTag' };
|
|
1029
|
+
return { status: 'error', label: 'modelTestFailedTag' };
|
|
1030
|
+
}
|
|
1031
|
+
function credentialSource(source, t) {
|
|
1032
|
+
if (source === 'env')
|
|
1033
|
+
return t('sourceEnv');
|
|
1034
|
+
if (source === 'file')
|
|
1035
|
+
return t('sourceFile');
|
|
1036
|
+
return source;
|
|
1037
|
+
}
|
|
1038
|
+
const UPDATE_REASON_KEYS = {
|
|
1039
|
+
'profile-not-found': 'updateReasonProfileNotFound',
|
|
1040
|
+
'not-direct-dependency': 'updateReasonNotDependency',
|
|
1041
|
+
'unsupported-install-source': 'updateReasonLocalSource',
|
|
1042
|
+
'profile-read-only': 'updateReasonReadOnly',
|
|
1043
|
+
'pnpm-unavailable': 'updateReasonPnpm',
|
|
1044
|
+
'unsupported-platform': 'updateReasonPlatform',
|
|
1045
|
+
'restart-unmanaged': 'updateReasonRestartUnmanaged',
|
|
1046
|
+
'restart-address-unavailable': 'updateReasonRestartAddress',
|
|
1047
|
+
};
|
|
1048
|
+
function wait(delayMs) {
|
|
1049
|
+
return new Promise(resolve => { setTimeout(resolve, delayMs); });
|
|
1050
|
+
}
|
|
1051
|
+
function LoadedSettings({ controller, t }) {
|
|
1052
|
+
const state = (0, react_1.useSyncExternalStore)(controller.subscribe, controller.snapshot, controller.snapshot);
|
|
1053
|
+
const snapshot = state.snapshot;
|
|
1054
|
+
const [draft, setDraft] = (0, react_1.useState)(undefined);
|
|
1055
|
+
const [apiKey, setApiKey] = (0, react_1.useState)('');
|
|
1056
|
+
const [draftError, setDraftError] = (0, react_1.useState)(undefined);
|
|
1057
|
+
const [copiedCommand, setCopiedCommand] = (0, react_1.useState)(false);
|
|
1058
|
+
(0, react_1.useEffect)(() => { if (state.status === 'idle')
|
|
1059
|
+
void controller.load(); }, [controller, state.status]);
|
|
1060
|
+
(0, react_1.useEffect)(() => {
|
|
1061
|
+
if (snapshot !== undefined)
|
|
1062
|
+
setDraft(draftOf(snapshot.settings.value));
|
|
1063
|
+
}, [snapshot]);
|
|
1064
|
+
(0, react_1.useEffect)(() => {
|
|
1065
|
+
const restart = state.restart;
|
|
1066
|
+
if (restart === undefined || !restart.restarting)
|
|
1067
|
+
return;
|
|
1068
|
+
let cancelled = false;
|
|
1069
|
+
void (async () => {
|
|
1070
|
+
await wait(restart.retryAfterMs);
|
|
1071
|
+
const deadline = Date.now() + 390_000;
|
|
1072
|
+
let outageSeen = false;
|
|
1073
|
+
while (!cancelled && Date.now() < deadline) {
|
|
1074
|
+
try {
|
|
1075
|
+
const current = await apiRequest();
|
|
1076
|
+
if (current.release.pluginVersion === restart.toVersion) {
|
|
1077
|
+
window.location.reload();
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (outageSeen && current.release.pluginVersion === restart.fromVersion) {
|
|
1081
|
+
controller.reportRestartTimeout(t('restartRolledBack'));
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
// The expected outage while the replacement process starts.
|
|
1087
|
+
outageSeen = true;
|
|
1088
|
+
}
|
|
1089
|
+
await wait(1_000);
|
|
1090
|
+
}
|
|
1091
|
+
if (!cancelled)
|
|
1092
|
+
controller.reportRestartTimeout(t('restartTimedOut'));
|
|
1093
|
+
})();
|
|
1094
|
+
return () => { cancelled = true; };
|
|
1095
|
+
}, [controller, state.restart, t]);
|
|
1096
|
+
if (state.status === 'idle' || (state.status === 'loading' && snapshot === undefined)) {
|
|
1097
|
+
return (0, jsx_runtime_1.jsx)("div", { className: "dvt-settings", children: (0, jsx_runtime_1.jsx)("div", { className: "dvt-loading", children: t('testing') }) });
|
|
1098
|
+
}
|
|
1099
|
+
if (snapshot === undefined || draft === undefined) {
|
|
1100
|
+
return (0, jsx_runtime_1.jsxs)("div", { className: "dvt-settings", children: [(0, jsx_runtime_1.jsx)("div", { className: "dvt-alert error", children: state.error ?? t('runtimeUnavailable') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", onClick: () => { void controller.load(); }, children: t('retry') })] });
|
|
1101
|
+
}
|
|
1102
|
+
const update = (key, value) => setDraft(current => current === undefined ? current : { ...current, [key]: value });
|
|
1103
|
+
const save = () => {
|
|
1104
|
+
try {
|
|
1105
|
+
const keyFailure = apiKeyFailure(apiKey, t);
|
|
1106
|
+
if (keyFailure !== undefined) {
|
|
1107
|
+
setDraftError(keyFailure);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
const credentialValue = apiKey.length === 0 ? undefined : apiKey.trim();
|
|
1111
|
+
setDraftError(undefined);
|
|
1112
|
+
void controller.save(valueOf(draft, t), snapshot.settings.revision, credentialValue, snapshot.writable).then(saved => { if (saved)
|
|
1113
|
+
setApiKey(''); });
|
|
1114
|
+
}
|
|
1115
|
+
catch (error) {
|
|
1116
|
+
setDraftError(error instanceof Error ? error.message : String(error));
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
const busy = state.action !== undefined;
|
|
1120
|
+
const credentialMatchesSnapshot = draft.credential.trim() === snapshot.credential.ref;
|
|
1121
|
+
const builtInCredentialChangedProvider = snapshot.credential.source === 'built-in-free'
|
|
1122
|
+
&& !isBuiltInFreeVisionDraft(draft);
|
|
1123
|
+
const keyLocked = credentialMatchesSnapshot
|
|
1124
|
+
&& !snapshot.credential.writable
|
|
1125
|
+
&& !builtInCredentialChangedProvider;
|
|
1126
|
+
const canSave = snapshot.writable || (apiKey.length > 0 && !keyLocked);
|
|
1127
|
+
const runtimeErrorTitle = snapshot.runtime.ready ? t('runtimeCandidateRejected') : t('runtimeUnavailable');
|
|
1128
|
+
const pluginUpdate = state.update;
|
|
1129
|
+
const updateCapability = pluginUpdate ?? snapshot.release.update;
|
|
1130
|
+
const latestVersion = pluginUpdate?.latestVersion;
|
|
1131
|
+
const updateReason = updateCapability.reason === undefined ? undefined : t(UPDATE_REASON_KEYS[updateCapability.reason]);
|
|
1132
|
+
const updateCheckSupported = updateCapability.checkSupported ?? updateCapability.supported;
|
|
1133
|
+
const updateHasUnsavedChanges = apiKey.length > 0 || settingsDraftChanged(draft, snapshot.settings.value, t);
|
|
1134
|
+
const manualUpdateProfile = updateCapability.profile ?? 'web';
|
|
1135
|
+
const manualUpdateCommand = `dsh plugin --profile ${manualUpdateProfile} add @mengruo/dsh-vision-toolkit@latest --registry=https://registry.npmjs.org/`;
|
|
1136
|
+
const aihubmixTutorialUrl = draft?.language === 'en' ? AIHUBMIX_TUTORIAL_URL_EN : AIHUBMIX_TUTORIAL_URL_ZH;
|
|
1137
|
+
const copyManualUpdate = () => {
|
|
1138
|
+
void navigator.clipboard?.writeText(manualUpdateCommand)
|
|
1139
|
+
.then(() => {
|
|
1140
|
+
setCopiedCommand(true);
|
|
1141
|
+
window.setTimeout(() => setCopiedCommand(false), 2_000);
|
|
1142
|
+
})
|
|
1143
|
+
.catch(() => { });
|
|
1144
|
+
};
|
|
1145
|
+
const applyUpdate = () => {
|
|
1146
|
+
if (latestVersion === undefined)
|
|
1147
|
+
return;
|
|
1148
|
+
if (!window.confirm(t('updateConfirm', { version: latestVersion })))
|
|
1149
|
+
return;
|
|
1150
|
+
void controller.applyUpdate(latestVersion);
|
|
1151
|
+
};
|
|
1152
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "dvt-settings", children: [(0, jsx_runtime_1.jsx)("div", { className: "dvt-alert notice", children: t('externalNotice') }), !snapshot.writable ? (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert warning", children: t('readOnly') }) : null, draftError === undefined ? null : (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert error", children: draftError }), state.error === undefined ? null : (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert error", children: state.error }), state.message === 'saved' ? (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert success", children: t('saved') }) : null, state.message === 'restarting' && state.restart !== undefined ? (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert success", children: t('restarting', { version: state.restart.toVersion }) }) : null, state.message === 'manual-restart-required' && state.restart !== undefined ? (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert success", children: t('manualRestartRequired', { version: state.restart.toVersion }) }) : null, snapshot.runtime.lastError === undefined ? null : (0, jsx_runtime_1.jsxs)("div", { className: "dvt-alert error", children: [(0, jsx_runtime_1.jsx)("strong", { children: runtimeErrorTitle }), (0, jsx_runtime_1.jsx)("span", { children: snapshot.runtime.lastError })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel dvt-essential", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-panel-title", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { children: t('provider') }), (0, jsx_runtime_1.jsx)("p", { children: t('providerHint') })] }), (0, jsx_runtime_1.jsx)("span", { className: `dvt-badge ${snapshot.credential.configured ? 'ok' : 'error'}`, children: snapshot.credential.configured ? t('configured') : t('missing') })] }), (0, jsx_runtime_1.jsx)("p", { className: "dvt-tutorial-link", children: (0, jsx_runtime_1.jsx)("a", { href: aihubmixTutorialUrl, target: "_blank", rel: "noreferrer", children: t('aihubmixTutorial') }) }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-form-grid", children: [(0, jsx_runtime_1.jsx)(Field, { label: t('protocol'), children: (0, jsx_runtime_1.jsxs)("select", { disabled: !snapshot.writable || busy, value: draft.protocol, onChange: (event) => { update('protocol', event.target.value); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "openai", children: "OpenAI Chat Completions" }), (0, jsx_runtime_1.jsx)("option", { value: "anthropic", children: "Anthropic Messages" })] }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('baseUrl'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { disabled: !snapshot.writable || busy, value: draft.baseUrl, onChange: (event) => { update('baseUrl', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('model'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { disabled: !snapshot.writable || busy, value: draft.model, onChange: (event) => { update('model', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(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) })}`, children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.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); } }) })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-save-row", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "primary", disabled: !canSave || busy, onClick: save, children: state.action === 'save' ? t('saving') : t('save') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", disabled: busy, onClick: () => { void controller.load(); }, children: t('reload') })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-panel-title", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { children: t('health') }), (0, jsx_runtime_1.jsx)("p", { children: t('connectionHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-actions", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { size: "sm", variant: "outline", disabled: busy || !snapshot.runtime.ready, onClick: () => { void controller.runHealth('health'); }, children: state.action === 'health' ? t('testing') : t('runHealth') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { size: "sm", variant: "outline", disabled: busy || !snapshot.runtime.ready, onClick: () => { void controller.runHealth('connection'); }, children: state.action === 'connection' ? t('testing') : t('testConnection') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { size: "sm", variant: "primary", disabled: busy || !snapshot.runtime.ready, onClick: () => { void controller.runHealth('model'); }, children: state.action === 'model' ? t('testingModel') : t('testModel') })] })] }), (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('saveBeforeTesting') }), state.health === undefined ? (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('notTested') }) : (0, jsx_runtime_1.jsx)("div", { className: "dvt-health-grid", children: Object.entries(state.health.checks).map(([name, check]) => {
|
|
1153
|
+
const testTag = name === 'model' ? modelTestTag(state.health, check) : undefined;
|
|
1154
|
+
return (0, jsx_runtime_1.jsxs)("div", { "data-status": check.status, children: [(0, jsx_runtime_1.jsx)("span", { children: t(HEALTH_NAME_KEYS[name] ?? 'health') }), testTag === undefined ? null : (0, jsx_runtime_1.jsx)("em", { className: "dvt-health-test-tag", "data-status": testTag.status, children: t(testTag.label) }), (0, jsx_runtime_1.jsx)("strong", { children: t(HEALTH_STATUS_KEYS[check.status]) }), (0, jsx_runtime_1.jsx)("p", { children: healthDetail(name, check.detail, t) })] }, name);
|
|
1155
|
+
}) })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel dvt-update-panel", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-panel-title", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { children: t('updates') }), (0, jsx_runtime_1.jsx)("p", { children: t('updatesHint') })] }), (0, jsx_runtime_1.jsx)("span", { className: `dvt-badge ${pluginUpdate?.updateAvailable ? 'warning' : pluginUpdate !== undefined && pluginUpdate.supported ? 'ok' : ''}`, children: pluginUpdate?.updateAvailable ? t('updateAvailable') : pluginUpdate !== undefined && pluginUpdate.supported ? t('upToDate') : t('pluginVersion') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-update-grid", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { children: t('updateInstalled') }), (0, jsx_runtime_1.jsx)("strong", { children: snapshot.release.pluginVersion })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { children: t('updateLatest') }), (0, jsx_runtime_1.jsx)("strong", { children: latestVersion ?? '—' })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { children: t('updateProfile') }), (0, jsx_runtime_1.jsx)("strong", { children: updateCapability.profile ?? '—' })] })] }), !updateCapability.supported ? (0, jsx_runtime_1.jsxs)("div", { className: "dvt-alert warning", children: [(0, jsx_runtime_1.jsx)("strong", { children: t('updateUnsupported') }), (0, jsx_runtime_1.jsx)("span", { children: updateReason })] }) : null, updateCapability.supported && updateHasUnsavedChanges ? (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert warning", children: t('updateSaveFirst') }) : null, pluginUpdate?.supported && pluginUpdate.updateAvailable && latestVersion !== undefined ? (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('updateAvailableDetail', { version: latestVersion }) }) : null, pluginUpdate?.supported && !pluginUpdate.updateAvailable && latestVersion !== undefined ? (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('upToDateDetail', { version: latestVersion }) }) : null, (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('manualUpdateHint') }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-manual-update", children: [(0, jsx_runtime_1.jsx)("code", { children: manualUpdateCommand }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { size: "sm", variant: "outline", onClick: copyManualUpdate, children: copiedCommand ? t('copied') : t('copy') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-actions", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", disabled: busy || !updateCheckSupported || state.restart !== undefined, onClick: () => { void controller.checkUpdate(); }, children: state.action === 'check-update' ? t('checkingUpdate') : t('checkUpdate') }), pluginUpdate?.supported && pluginUpdate.updateAvailable && latestVersion !== undefined ? (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "primary", disabled: busy || state.restart !== undefined || updateHasUnsavedChanges, onClick: applyUpdate, children: state.action === 'apply-update' ? t('updatingPlugin') : t('updateNow') }) : null] })] }), (0, jsx_runtime_1.jsxs)("details", { className: "dvt-advanced", children: [(0, jsx_runtime_1.jsxs)("summary", { children: [(0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)("strong", { children: t('advanced') }), (0, jsx_runtime_1.jsx)("small", { children: t('advancedHint') })] }), (0, jsx_runtime_1.jsx)("span", { className: "dvt-details-chevron", "aria-hidden": "true", children: "\u2304" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-advanced-body", children: [(0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsx)("div", { className: "dvt-panel-title", children: (0, jsx_runtime_1.jsx)("h3", { children: t('provider') }) }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-form-grid", children: [(0, jsx_runtime_1.jsx)(Field, { label: t('credential'), hint: t('credentialHint'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { "aria-label": t('credential'), disabled: !snapshot.writable || busy, value: draft.credential, onChange: (event) => { update('credential', event.target.value); } }) }), draft.protocol === 'anthropic' ? (0, jsx_runtime_1.jsx)(Field, { label: t('anthropicThinking'), hint: t('anthropicThinkingHint'), children: (0, jsx_runtime_1.jsxs)("select", { "aria-label": t('anthropicThinking'), value: draft.anthropicThinking, onChange: (event) => { update('anthropicThinking', event.target.value); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "omit", children: "omit (widest compatibility)" }), (0, jsx_runtime_1.jsx)("option", { value: "disabled", children: "disabled (model support required)" }), (0, jsx_runtime_1.jsx)("option", { value: "adaptive", children: "adaptive (model support required)" })] }) }) : null, (0, jsx_runtime_1.jsx)(Field, { label: t('userAgent'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { value: draft.userAgent, onChange: (event) => { update('userAgent', event.target.value); } }) })] })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsx)("div", { className: "dvt-panel-title", children: (0, jsx_runtime_1.jsx)("h3", { children: t('limits') }) }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-form-grid", children: [(0, jsx_runtime_1.jsx)(Field, { label: t('language'), children: (0, jsx_runtime_1.jsxs)("select", { value: draft.language, onChange: (event) => { update('language', event.target.value); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "zh", children: "\u4E2D\u6587" }), (0, jsx_runtime_1.jsx)("option", { value: "en", children: "English" })] }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('timeout'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { inputMode: "numeric", value: draft.timeoutMs, onChange: (event) => { update('timeoutMs', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('maxBytes'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { inputMode: "numeric", value: draft.maxImageBytes, onChange: (event) => { update('maxImageBytes', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('maxPixels'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { inputMode: "numeric", value: draft.maxImagePixels, onChange: (event) => { update('maxImagePixels', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('concurrency'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { inputMode: "numeric", value: draft.concurrency, onChange: (event) => { update('concurrency', event.target.value); } }) })] })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsx)("div", { className: "dvt-panel-title", children: (0, jsx_runtime_1.jsx)("h3", { children: t('imageInput') }) }), (0, jsx_runtime_1.jsxs)("label", { className: "dvt-check", children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: draft.hiddenVariants, disabled: !snapshot.writable || busy, onChange: (event) => { update('hiddenVariants', event.target.checked); } }), (0, jsx_runtime_1.jsx)("span", { children: t('hiddenVariantsLabel') }), (0, jsx_runtime_1.jsx)("small", { children: t('hiddenVariantsHint') })] })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-panel-title", children: [(0, jsx_runtime_1.jsx)("h3", { children: t('runtime') }), (0, jsx_runtime_1.jsx)("span", { className: `dvt-badge ${snapshot.runtime.ready ? 'ok' : 'error'}`, children: snapshot.runtime.ready ? snapshot.runtime.upstream?.source === 'managed' ? t('runtimeManaged') : snapshot.runtime.upstream?.source === 'external' ? t('runtimeExternal') : t('runtimeReady') : t('runtimeUnavailable') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-form-grid", children: [(0, jsx_runtime_1.jsx)(Field, { label: t('runtimeMode'), children: (0, jsx_runtime_1.jsxs)("select", { value: draft.runtimeMode, onChange: (event) => { update('runtimeMode', event.target.value); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "managed", children: t('runtimeManaged') }), (0, jsx_runtime_1.jsx)("option", { value: "external", children: t('runtimeExternal') })] }) }), draft.runtimeMode === 'external' ? (0, jsx_runtime_1.jsx)(Field, { label: t('toolkitPath'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { value: draft.toolkitPath, onChange: (event) => { update('toolkitPath', event.target.value); } }) }) : null, (0, jsx_runtime_1.jsx)(Field, { label: t('python'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { placeholder: "python3", value: draft.python, onChange: (event) => { update('python', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('allowedDirs'), hint: t('allowedDirsHint'), children: (0, jsx_runtime_1.jsx)("textarea", { rows: 3, value: draft.allowedDirs, onChange: (event) => { update('allowedDirs', event.target.value); } }) })] }), snapshot.runtime.upstream === undefined ? null : (0, jsx_runtime_1.jsxs)("div", { className: "dvt-runtime-facts", children: [(0, jsx_runtime_1.jsx)("code", { children: snapshot.runtime.upstream.path }), (0, jsx_runtime_1.jsxs)("code", { children: [snapshot.runtime.upstream.python, " \u00B7 ", snapshot.runtime.upstream.pythonVersion] }), (0, jsx_runtime_1.jsx)("code", { children: snapshot.runtime.upstream.runtimeHome })] })] })] })] }), (0, jsx_runtime_1.jsxs)("footer", { className: "dvt-settings-footer", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "dvt-kicker", children: t('pluginKind') }), (0, jsx_runtime_1.jsx)("h2", { children: t('settingsTitle') }), (0, jsx_runtime_1.jsx)("p", { children: t('settingsIntro') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-release", children: [(0, jsx_runtime_1.jsxs)("span", { children: [t('pluginVersion'), " ", (0, jsx_runtime_1.jsx)("strong", { children: snapshot.release.pluginVersion })] }), (0, jsx_runtime_1.jsxs)("span", { children: [t('upstreamVersion'), " ", (0, jsx_runtime_1.jsx)("strong", { children: snapshot.release.upstreamVersion })] }), (0, jsx_runtime_1.jsxs)("span", { children: [t('activeGeneration'), " ", (0, jsx_runtime_1.jsx)("strong", { children: t('activeGenerationValue', { generation: snapshot.runtime.generation }) })] })] })] })] }));
|
|
1156
|
+
}
|
|
1157
|
+
const CSS = `
|
|
1158
|
+
.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)}
|
|
1159
|
+
.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}
|
|
1160
|
+
.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}
|
|
1161
|
+
.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)}
|
|
1162
|
+
.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}
|
|
1163
|
+
.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}
|
|
1164
|
+
.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)}
|
|
1165
|
+
.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}
|
|
1166
|
+
@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}}
|
|
1167
|
+
`;
|
|
1168
|
+
function installStyles() {
|
|
1169
|
+
const id = '@mengruo/dsh-vision-toolkit/client';
|
|
1170
|
+
const existing = document.querySelector(`style[data-plugin-css="${id}"]`);
|
|
1171
|
+
if (existing !== null)
|
|
1172
|
+
return () => { };
|
|
1173
|
+
const style = document.createElement('style');
|
|
1174
|
+
style.dataset.plugin = '@mengruo/dsh-vision-toolkit';
|
|
1175
|
+
style.dataset.pluginCss = id;
|
|
1176
|
+
style.textContent = CSS;
|
|
1177
|
+
document.head.appendChild(style);
|
|
1178
|
+
return () => { style.remove(); };
|
|
1179
|
+
}
|
|
1180
|
+
/** Required client services. The pasted-image codec attaches to either trigger-service generation after load. */
|
|
1181
|
+
exports.inject = ['slots', 'locale', 'remote', 'conversation', 'sessions'];
|
|
1182
|
+
/** Register dedicated Tool views and the Vision Settings section. */
|
|
1183
|
+
function apply(ctx) {
|
|
1184
|
+
ctx.effect(installStyles, 'dsh-vision-toolkit: styles');
|
|
1185
|
+
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-vision-toolkit: locale');
|
|
1186
|
+
(0, paste_images_tsx_1.installPasteImages)(ctx);
|
|
1187
|
+
ctx.effect(model_variants_hider_ts_1.installModelVariantsHider, 'dsh-vision-toolkit: model-selector transparent routing');
|
|
1188
|
+
const t = ctx.locale.bind(NS);
|
|
1189
|
+
const injected = () => ({ t });
|
|
1190
|
+
const entries = [
|
|
1191
|
+
['vision_ground', GroundView],
|
|
1192
|
+
['vision_detect', DetectView],
|
|
1193
|
+
['vision_trace', TraceView],
|
|
1194
|
+
['vision_pixel_diff', PixelDiffView],
|
|
1195
|
+
['vision_crop', ArtifactView],
|
|
1196
|
+
['vision_long_screenshot_ocr', ArtifactView],
|
|
1197
|
+
['vision_extract_foreground', ArtifactView],
|
|
1198
|
+
['vision_html_screenshot', ArtifactView],
|
|
1199
|
+
['vision_dominant_colors', PaletteView],
|
|
1200
|
+
];
|
|
1201
|
+
ctx.slots.inject('tool.call.toolview', function* () {
|
|
1202
|
+
for (const [key, component] of entries) {
|
|
1203
|
+
yield ctx.slots.register({ name: 'tool.call.toolview', key, inject: injected }, component);
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
const controller = new VisionSettingsController();
|
|
1207
|
+
ctx.effect(() => {
|
|
1208
|
+
const refreshSettings = (namespace) => {
|
|
1209
|
+
if (namespace === 'vision-toolkit') {
|
|
1210
|
+
(0, display_config_ts_1.resetDisplayConfigCache)();
|
|
1211
|
+
controller.refreshIfLoaded();
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
const refreshCredential = (ref) => {
|
|
1215
|
+
const current = controller.snapshot().snapshot;
|
|
1216
|
+
if (current?.credential.ref === ref)
|
|
1217
|
+
controller.refreshIfLoaded();
|
|
1218
|
+
};
|
|
1219
|
+
const legacyRemote = ctx.remote;
|
|
1220
|
+
const currentEvents = ctx;
|
|
1221
|
+
const disposers = typeof legacyRemote.$on === 'function'
|
|
1222
|
+
? [
|
|
1223
|
+
legacyRemote.$on('settings/document-updated', refreshSettings),
|
|
1224
|
+
legacyRemote.$on('credentials/updated', refreshCredential),
|
|
1225
|
+
]
|
|
1226
|
+
: [
|
|
1227
|
+
currentEvents.on('settings/changed', (namespace) => {
|
|
1228
|
+
refreshSettings(namespace);
|
|
1229
|
+
}),
|
|
1230
|
+
currentEvents.on('credentials/changed', (ref) => {
|
|
1231
|
+
refreshCredential(ref);
|
|
1232
|
+
}),
|
|
1233
|
+
];
|
|
1234
|
+
disposers.push(ctx.on('connection/reset', () => { controller.refreshIfLoaded(); }));
|
|
1235
|
+
return () => { for (const dispose of disposers)
|
|
1236
|
+
dispose(); };
|
|
1237
|
+
}, 'dsh-vision-toolkit: Settings invalidations');
|
|
1238
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
1239
|
+
name: 'settings.section',
|
|
1240
|
+
id: 'vision-toolkit',
|
|
1241
|
+
order: 30,
|
|
1242
|
+
label: () => t('nav'),
|
|
1243
|
+
inject: () => ({ controller, t }),
|
|
1244
|
+
}, SettingsSection));
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
__modules["./model-variants-hider.js"] = function(module, exports, require, __load_) {
|
|
1248
|
+
"use strict";
|
|
1249
|
+
/**
|
|
1250
|
+
* Transparent routing for the host model selector: when `imageInputVariants.hidden`
|
|
1251
|
+
* is enabled, variant routes keep the upstream provider/model display names and
|
|
1252
|
+
* the browser hides the upstream text-only entries that have a variant twin.
|
|
1253
|
+
* Users then see one entry per model — the original name — while the session
|
|
1254
|
+
* actually runs on the image-capable variant, so pasted images, history with
|
|
1255
|
+
* images, and the built-in `read_image` tool all keep working on text-only
|
|
1256
|
+
* models without exposing `(Vision Toolkit)` routes.
|
|
1257
|
+
*
|
|
1258
|
+
* The host selector renders one `[role=group]` per provider whose group title
|
|
1259
|
+
* id is `:<react-radix>:-<providerId>`, and one `[role=menuitemradio]` per
|
|
1260
|
+
* model. We key groups by that provider id (variant routes carry the
|
|
1261
|
+
* `vision-toolkit-` prefix) and hide every upstream entry whose display name
|
|
1262
|
+
* matches a variant twin, collapsing fully-hidden upstream groups.
|
|
1263
|
+
*
|
|
1264
|
+
* The hiding decision is purely DOM-local: transparent mode is exactly the
|
|
1265
|
+
* case where a variant twin keeps the upstream display name, while explicit
|
|
1266
|
+
* mode appends `(Vision Toolkit)` and therefore never matches. No display-config
|
|
1267
|
+
* round-trip is needed before the selector can be tidied, so the first paint
|
|
1268
|
+
* of an opened menu already shows the merged list instead of flashing the
|
|
1269
|
+
* duplicate upstream group.
|
|
1270
|
+
* @module dsh-vision-toolkit/model-variants-hider
|
|
1271
|
+
*/
|
|
1272
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1273
|
+
exports.tidyModelSelector = tidyModelSelector;
|
|
1274
|
+
exports.installModelVariantsHider = installModelVariantsHider;
|
|
1275
|
+
exports.isModelVariantsHiderActive = isModelVariantsHiderActive;
|
|
1276
|
+
const VARIANT_PROVIDER_PREFIX = 'vision-toolkit-';
|
|
1277
|
+
/** Elements we hid and their original inline display value, for restoration. */
|
|
1278
|
+
const hiddenElements = new Map();
|
|
1279
|
+
let active = false;
|
|
1280
|
+
let observer;
|
|
1281
|
+
let tidyQueued = false;
|
|
1282
|
+
/** Derive the provider id from a group's `aria-labelledby` title id. */
|
|
1283
|
+
function providerIdOf(group) {
|
|
1284
|
+
const labelledBy = group.getAttribute('aria-labelledby');
|
|
1285
|
+
if (labelledBy === null || labelledBy === '')
|
|
1286
|
+
return undefined;
|
|
1287
|
+
const titleId = document.getElementById(labelledBy)?.id ?? labelledBy;
|
|
1288
|
+
const reactPrefixed = /^:[^:]+:-(.+)$/u.exec(titleId);
|
|
1289
|
+
if (reactPrefixed !== null)
|
|
1290
|
+
return reactPrefixed[1];
|
|
1291
|
+
return titleId.replace(/^-/u, '');
|
|
1292
|
+
}
|
|
1293
|
+
function modelNames(group) {
|
|
1294
|
+
return [...group.querySelectorAll('[role="menuitemradio"]')]
|
|
1295
|
+
.map(button => (button.title || (button.textContent ?? '')).trim())
|
|
1296
|
+
.filter(Boolean);
|
|
1297
|
+
}
|
|
1298
|
+
function hideElement(element) {
|
|
1299
|
+
if (!hiddenElements.has(element))
|
|
1300
|
+
hiddenElements.set(element, element.style.display);
|
|
1301
|
+
element.style.display = 'none';
|
|
1302
|
+
}
|
|
1303
|
+
function restoreHidden() {
|
|
1304
|
+
for (const [element, display] of hiddenElements) {
|
|
1305
|
+
element.style.display = display;
|
|
1306
|
+
}
|
|
1307
|
+
hiddenElements.clear();
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Hide upstream text-only entries that have a variant twin. Group keys come
|
|
1311
|
+
* from `aria-labelledby` ids so provider identity is reliable even when the
|
|
1312
|
+
* variant provider name equals the upstream name (transparent mode).
|
|
1313
|
+
*/
|
|
1314
|
+
function tidyModelSelector() {
|
|
1315
|
+
if (document.querySelector('[role="menu"]') === null)
|
|
1316
|
+
return;
|
|
1317
|
+
// The host re-renders selectors while sessions stay open; drop bookkeeping
|
|
1318
|
+
// for entries that already left the DOM so the map cannot grow unboundedly.
|
|
1319
|
+
for (const element of [...hiddenElements.keys()]) {
|
|
1320
|
+
if (!element.isConnected)
|
|
1321
|
+
hiddenElements.delete(element);
|
|
1322
|
+
}
|
|
1323
|
+
const groups = [...document.querySelectorAll('[role="menu"] [role="group"]')];
|
|
1324
|
+
const byProvider = new Map();
|
|
1325
|
+
for (const group of groups) {
|
|
1326
|
+
const provider = providerIdOf(group);
|
|
1327
|
+
if (provider === undefined)
|
|
1328
|
+
continue;
|
|
1329
|
+
const entries = byProvider.get(provider);
|
|
1330
|
+
if (entries === undefined)
|
|
1331
|
+
byProvider.set(provider, [group]);
|
|
1332
|
+
else
|
|
1333
|
+
entries.push(group);
|
|
1334
|
+
}
|
|
1335
|
+
const shouldHide = new Set();
|
|
1336
|
+
for (const [provider, providerGroups] of byProvider) {
|
|
1337
|
+
if (!provider.startsWith(VARIANT_PROVIDER_PREFIX))
|
|
1338
|
+
continue;
|
|
1339
|
+
const upstream = provider.slice(VARIANT_PROVIDER_PREFIX.length);
|
|
1340
|
+
const twinNames = new Set(providerGroups.flatMap(modelNames));
|
|
1341
|
+
if (twinNames.size === 0)
|
|
1342
|
+
continue;
|
|
1343
|
+
for (const upstreamGroup of byProvider.get(upstream) ?? []) {
|
|
1344
|
+
const buttons = [...upstreamGroup.querySelectorAll('[role="menuitemradio"]')];
|
|
1345
|
+
const matched = [];
|
|
1346
|
+
for (const button of buttons) {
|
|
1347
|
+
const name = (button.title || (button.textContent ?? '')).trim();
|
|
1348
|
+
if (twinNames.has(name))
|
|
1349
|
+
matched.push(button);
|
|
1350
|
+
}
|
|
1351
|
+
if (matched.length === 0)
|
|
1352
|
+
continue;
|
|
1353
|
+
for (const button of matched)
|
|
1354
|
+
shouldHide.add(button);
|
|
1355
|
+
// Collapse the whole group only when every model has a variant twin;
|
|
1356
|
+
// otherwise a partially matched group keeps its unmatched entries.
|
|
1357
|
+
if (matched.length === buttons.length)
|
|
1358
|
+
shouldHide.add(upstreamGroup);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
// Restore entries whose twin disappeared (e.g. transparent routing was
|
|
1362
|
+
// disabled and the wrapper rebuilt with explicit `(Vision Toolkit)` names).
|
|
1363
|
+
for (const [element, display] of [...hiddenElements]) {
|
|
1364
|
+
if (!shouldHide.has(element)) {
|
|
1365
|
+
element.style.display = display;
|
|
1366
|
+
hiddenElements.delete(element);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
for (const element of shouldHide)
|
|
1370
|
+
hideElement(element);
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Install the transparent-routing integrator. It watches the document for
|
|
1374
|
+
* model-selector renderings and re-tidies them whenever the host re-renders.
|
|
1375
|
+
* Tidy runs in a microtask (before the browser paints) and is coalesced across
|
|
1376
|
+
* the render batch, so opening the selector never shows the upstream twins.
|
|
1377
|
+
* @returns the disposer that stops observation and restores hidden entries.
|
|
1378
|
+
*/
|
|
1379
|
+
function installModelVariantsHider() {
|
|
1380
|
+
if (observer !== undefined) {
|
|
1381
|
+
// A previous install is still active; a duplicate effect must not tear
|
|
1382
|
+
// down the integrator while its original owner still expects it.
|
|
1383
|
+
return () => { };
|
|
1384
|
+
}
|
|
1385
|
+
let disposed = false;
|
|
1386
|
+
const tidySoon = () => {
|
|
1387
|
+
if (tidyQueued || disposed)
|
|
1388
|
+
return;
|
|
1389
|
+
tidyQueued = true;
|
|
1390
|
+
queueMicrotask(() => {
|
|
1391
|
+
tidyQueued = false;
|
|
1392
|
+
if (disposed)
|
|
1393
|
+
return;
|
|
1394
|
+
active = true;
|
|
1395
|
+
tidyModelSelector();
|
|
1396
|
+
});
|
|
1397
|
+
};
|
|
1398
|
+
observer = new MutationObserver(tidySoon);
|
|
1399
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
1400
|
+
active = true;
|
|
1401
|
+
tidyModelSelector();
|
|
1402
|
+
return () => {
|
|
1403
|
+
disposed = true;
|
|
1404
|
+
observer?.disconnect();
|
|
1405
|
+
observer = undefined;
|
|
1406
|
+
tidyQueued = false;
|
|
1407
|
+
restoreHidden();
|
|
1408
|
+
active = false;
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
/** Test seam: expose whether the integrator is currently installed. */
|
|
1412
|
+
function isModelVariantsHiderActive() {
|
|
1413
|
+
return active;
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
__modules["./paste-images.js"] = function(module, exports, require, __load_) {
|
|
1417
|
+
"use strict";
|
|
1418
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1419
|
+
exports.PasteImageController = exports.PASTE_POLICY_ROUTE = exports.PASTE_IMAGES_ROUTE = void 0;
|
|
1420
|
+
exports.PasteImageDock = PasteImageDock;
|
|
1421
|
+
exports.installPasteImages = installPasteImages;
|
|
1422
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
1423
|
+
/** Clipboard-only multi-image input for DSH Web. */
|
|
1424
|
+
const react_1 = require("react");
|
|
1425
|
+
const display_config_ts_1 = __load_("./display-config.js");
|
|
1426
|
+
const SOURCE = 'vision-toolkit-pasted-image';
|
|
1427
|
+
exports.PASTE_IMAGES_ROUTE = '/_dsh/vision-toolkit/paste-images';
|
|
1428
|
+
exports.PASTE_POLICY_ROUTE = '/_dsh/vision-toolkit/paste-policy';
|
|
1429
|
+
const MAX_IMAGES = 20;
|
|
1430
|
+
/** Hard per-image paste ceiling; must match MAX_PASTE_IMAGE_BYTES on the server. */
|
|
1431
|
+
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
1432
|
+
const MAX_BATCH_BYTES = 80 * 1024 * 1024;
|
|
1433
|
+
/** A confirmed paste verdict older than this is unknown again, even while a refresh is in flight. */
|
|
1434
|
+
const VERDICT_MAX_AGE_MS = 15000;
|
|
1435
|
+
const CORDIS_ORIGINAL = Symbol.for('cordis.original');
|
|
1436
|
+
function registryIdentity(registry) {
|
|
1437
|
+
let current = registry;
|
|
1438
|
+
while (true) {
|
|
1439
|
+
const original = current[CORDIS_ORIGINAL];
|
|
1440
|
+
if ((typeof original !== 'object' && typeof original !== 'function') || original === null || original === current) {
|
|
1441
|
+
return current;
|
|
1442
|
+
}
|
|
1443
|
+
current = original;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
let fallbackId = 0;
|
|
1447
|
+
function id() {
|
|
1448
|
+
if (typeof globalThis.crypto?.randomUUID === 'function')
|
|
1449
|
+
return globalThis.crypto.randomUUID();
|
|
1450
|
+
fallbackId += 1;
|
|
1451
|
+
return `paste-${Date.now()}-${fallbackId}`;
|
|
1452
|
+
}
|
|
1453
|
+
function humanBytes(bytes) {
|
|
1454
|
+
if (bytes < 1024)
|
|
1455
|
+
return `${bytes} B`;
|
|
1456
|
+
if (bytes < 1024 ** 2)
|
|
1457
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1458
|
+
return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
|
1459
|
+
}
|
|
1460
|
+
function message(error) {
|
|
1461
|
+
return error instanceof Error ? error.message : String(error);
|
|
1462
|
+
}
|
|
1463
|
+
function imageFiles(data) {
|
|
1464
|
+
if (data === null)
|
|
1465
|
+
return [];
|
|
1466
|
+
const itemFiles = Array.from(data.items)
|
|
1467
|
+
.filter(item => item.kind === 'file')
|
|
1468
|
+
.map(item => item.getAsFile())
|
|
1469
|
+
.filter((file) => file !== null);
|
|
1470
|
+
const candidates = itemFiles.length > 0 ? itemFiles : Array.from(data.files);
|
|
1471
|
+
return candidates.filter(file => file.type.toLowerCase().startsWith('image/'));
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* The selector label the model picker currently shows, or '' when none is
|
|
1475
|
+
* readable. Matches the host ModelSelect trigger aria-labels ("Select model,
|
|
1476
|
+
* current …" / "选择模型,当前 …"); any other label wording falls back to the
|
|
1477
|
+
* session-header verdict, which is stale until the next request.
|
|
1478
|
+
*/
|
|
1479
|
+
function currentModelLabel() {
|
|
1480
|
+
const buttons = document.querySelectorAll('button[aria-label]');
|
|
1481
|
+
for (const button of buttons) {
|
|
1482
|
+
const label = button.getAttribute('aria-label') ?? '';
|
|
1483
|
+
if (/select model|current model|选择模型/iu.test(label))
|
|
1484
|
+
return label;
|
|
1485
|
+
}
|
|
1486
|
+
return '';
|
|
1487
|
+
}
|
|
1488
|
+
/** Verdict cache key: the model label is part of the answer, so a switch invalidates it. */
|
|
1489
|
+
function verdictKey(sessionId, modelLabel) {
|
|
1490
|
+
return `${sessionId}|${modelLabel}`;
|
|
1491
|
+
}
|
|
1492
|
+
function validateImages(files) {
|
|
1493
|
+
if (files.length > MAX_IMAGES)
|
|
1494
|
+
throw new Error(`Paste at most ${MAX_IMAGES} images at a time`);
|
|
1495
|
+
let total = 0;
|
|
1496
|
+
for (const file of files) {
|
|
1497
|
+
if (!file.type.toLowerCase().startsWith('image/'))
|
|
1498
|
+
throw new Error(`${file.name || 'clipboard item'} is not an image`);
|
|
1499
|
+
if (file.size <= 0)
|
|
1500
|
+
throw new Error(`${file.name || 'clipboard image'} is empty`);
|
|
1501
|
+
if (file.size > MAX_IMAGE_BYTES)
|
|
1502
|
+
throw new Error(`${file.name || 'clipboard image'} exceeds ${humanBytes(MAX_IMAGE_BYTES)}`);
|
|
1503
|
+
total += file.size;
|
|
1504
|
+
}
|
|
1505
|
+
if (total > MAX_BATCH_BYTES)
|
|
1506
|
+
throw new Error(`Pasted images exceed ${humanBytes(MAX_BATCH_BYTES)} in total`);
|
|
1507
|
+
}
|
|
1508
|
+
async function responseJson(response) {
|
|
1509
|
+
const body = await response.json();
|
|
1510
|
+
if (!response.ok || body.ok !== true)
|
|
1511
|
+
throw new Error(body.error?.message ?? `Image copy failed (${response.status})`);
|
|
1512
|
+
return body;
|
|
1513
|
+
}
|
|
1514
|
+
function pasteLabel(file, index) {
|
|
1515
|
+
return file.name.trim() || `clipboard-image-${index + 1}`;
|
|
1516
|
+
}
|
|
1517
|
+
/** Owns browser File objects until DSH serializes the corresponding text references. */
|
|
1518
|
+
class PasteImageController {
|
|
1519
|
+
ctx;
|
|
1520
|
+
records = new Map();
|
|
1521
|
+
listeners = new Set();
|
|
1522
|
+
revision = 0;
|
|
1523
|
+
verdicts = new Map();
|
|
1524
|
+
/** Guards the synthetic replay paste from re-entering capture interception. */
|
|
1525
|
+
replaying = false;
|
|
1526
|
+
constructor(ctx) {
|
|
1527
|
+
this.ctx = ctx;
|
|
1528
|
+
}
|
|
1529
|
+
subscribe = (listener) => {
|
|
1530
|
+
this.listeners.add(listener);
|
|
1531
|
+
return () => { this.listeners.delete(listener); };
|
|
1532
|
+
};
|
|
1533
|
+
snapshot = () => this.revision;
|
|
1534
|
+
changed() {
|
|
1535
|
+
this.revision += 1;
|
|
1536
|
+
for (const listener of this.listeners)
|
|
1537
|
+
listener();
|
|
1538
|
+
}
|
|
1539
|
+
source() {
|
|
1540
|
+
return {
|
|
1541
|
+
trigger: '@',
|
|
1542
|
+
name: SOURCE,
|
|
1543
|
+
order: 1000,
|
|
1544
|
+
candidates: () => Promise.resolve([]),
|
|
1545
|
+
onPick: () => undefined,
|
|
1546
|
+
codec: {
|
|
1547
|
+
clipboardText: ref => `[pasted image: ${this.records.get(ref)?.file.name ?? ref}]`,
|
|
1548
|
+
serialize: (ref, signal) => this.serialize(ref, signal),
|
|
1549
|
+
},
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
recordsFor(occurrences) {
|
|
1553
|
+
return occurrences
|
|
1554
|
+
.filter(occurrence => occurrence.source === SOURCE)
|
|
1555
|
+
.map(occurrence => this.records.get(occurrence.ref))
|
|
1556
|
+
.filter((record) => record !== undefined);
|
|
1557
|
+
}
|
|
1558
|
+
inputFor(sessionId) {
|
|
1559
|
+
const actx = this.ctx.sessions.scope(sessionId);
|
|
1560
|
+
if (actx === undefined)
|
|
1561
|
+
throw new Error('Open a live session before pasting images');
|
|
1562
|
+
return this.ctx.conversation.input.for(actx);
|
|
1563
|
+
}
|
|
1564
|
+
insertText(input, text, start, end = start) {
|
|
1565
|
+
if (text === '')
|
|
1566
|
+
return start;
|
|
1567
|
+
const snapshot = input.state.getSnapshot();
|
|
1568
|
+
input.setDraft(snapshot.draft.slice(0, start) + text + snapshot.draft.slice(end));
|
|
1569
|
+
return start + text.length;
|
|
1570
|
+
}
|
|
1571
|
+
insertRecords(sessionId, input, files, cursor) {
|
|
1572
|
+
const batch = { sessionId, records: [] };
|
|
1573
|
+
const draftBeforeReferences = input.state.getSnapshot().draft;
|
|
1574
|
+
try {
|
|
1575
|
+
const before = input.state.getSnapshot().draft.slice(0, cursor);
|
|
1576
|
+
if (before !== '' && !/\s$/u.test(before))
|
|
1577
|
+
cursor = this.insertText(input, ' ', cursor);
|
|
1578
|
+
for (const [index, file] of files.entries()) {
|
|
1579
|
+
const ref = id();
|
|
1580
|
+
const record = { ref, file, batch, status: 'ready' };
|
|
1581
|
+
batch.records.push(record);
|
|
1582
|
+
this.records.set(ref, record);
|
|
1583
|
+
const snapshot = input.state.getSnapshot();
|
|
1584
|
+
const accepted = input.insertReference({
|
|
1585
|
+
source: SOURCE,
|
|
1586
|
+
ref,
|
|
1587
|
+
label: pasteLabel(file, index),
|
|
1588
|
+
clipboardText: `[pasted image: ${pasteLabel(file, index)}]`,
|
|
1589
|
+
}, { start: cursor, end: cursor, draftRev: snapshot.draftRev });
|
|
1590
|
+
if (!accepted)
|
|
1591
|
+
throw new Error('The composer changed before pasted images could be inserted');
|
|
1592
|
+
cursor += 1;
|
|
1593
|
+
const hasNext = index + 1 < files.length;
|
|
1594
|
+
const suffix = input.state.getSnapshot().draft.slice(cursor);
|
|
1595
|
+
if (hasNext || (suffix !== '' && !/^\s/u.test(suffix)))
|
|
1596
|
+
cursor = this.insertText(input, ' ', cursor);
|
|
1597
|
+
}
|
|
1598
|
+
batch.unsubscribe = input.state.subscribe(() => {
|
|
1599
|
+
const alive = new Set(input.state.getSnapshot().occurrences
|
|
1600
|
+
.filter(occurrence => occurrence.source === SOURCE)
|
|
1601
|
+
.map(occurrence => occurrence.ref));
|
|
1602
|
+
let changed = false;
|
|
1603
|
+
for (const record of batch.records) {
|
|
1604
|
+
if (alive.has(record.ref) || record.batch.inflight !== undefined)
|
|
1605
|
+
continue;
|
|
1606
|
+
changed = this.records.delete(record.ref) || changed;
|
|
1607
|
+
}
|
|
1608
|
+
if (batch.records.every(record => !this.records.has(record.ref)) && batch.inflight === undefined) {
|
|
1609
|
+
batch.unsubscribe?.();
|
|
1610
|
+
batch.unsubscribe = undefined;
|
|
1611
|
+
}
|
|
1612
|
+
if (changed)
|
|
1613
|
+
this.changed();
|
|
1614
|
+
});
|
|
1615
|
+
this.changed();
|
|
1616
|
+
return cursor;
|
|
1617
|
+
}
|
|
1618
|
+
catch (error) {
|
|
1619
|
+
input.setDraft(draftBeforeReferences);
|
|
1620
|
+
for (const record of batch.records)
|
|
1621
|
+
this.records.delete(record.ref);
|
|
1622
|
+
throw error;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* The host's verdict for one Session and selector label, when fresh. The
|
|
1627
|
+
* last CONFIRMED answer is authoritative while a background refresh is in
|
|
1628
|
+
* flight (the paste acts on what the host last said; the refresh only
|
|
1629
|
+
* covers the next paste). A label that changed since the confirmation
|
|
1630
|
+
* answers undefined, so the native attachment flow stays the default.
|
|
1631
|
+
* @param sessionId - the live Session the paste belongs to.
|
|
1632
|
+
* @param modelLabel - the model-selector label currently shown.
|
|
1633
|
+
* @returns the fresh confirmed verdict, or undefined when unconfirmed.
|
|
1634
|
+
*/
|
|
1635
|
+
verdictFor(sessionId, modelLabel) {
|
|
1636
|
+
const entry = this.verdicts.get(verdictKey(sessionId, modelLabel));
|
|
1637
|
+
if (entry === undefined || entry.at === 0)
|
|
1638
|
+
return undefined;
|
|
1639
|
+
if (Date.now() - entry.at > VERDICT_MAX_AGE_MS)
|
|
1640
|
+
return undefined;
|
|
1641
|
+
return { takeOver: entry.takeOver, ...(entry.autoSwitch === undefined ? {} : { autoSwitch: entry.autoSwitch }) };
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* The exact model route the live model catalog reports for one Session.
|
|
1645
|
+
* Unreadable routes answer undefined, so the verdict falls back to the
|
|
1646
|
+
* selector label alone.
|
|
1647
|
+
* @param sessionId - the live Session id.
|
|
1648
|
+
* @returns the current provider/model selection, when readable.
|
|
1649
|
+
*/
|
|
1650
|
+
async readSelection(sessionId) {
|
|
1651
|
+
const connection = this.ctx.get('connection');
|
|
1652
|
+
if (connection === undefined)
|
|
1653
|
+
return undefined;
|
|
1654
|
+
try {
|
|
1655
|
+
const { result } = await connection.api.sessions.models({ sessionId });
|
|
1656
|
+
if (!result.ok)
|
|
1657
|
+
return undefined;
|
|
1658
|
+
const current = result.value.current;
|
|
1659
|
+
if (current === undefined || current === null || current.provider === '' || current.model === '')
|
|
1660
|
+
return undefined;
|
|
1661
|
+
return {
|
|
1662
|
+
provider: current.provider,
|
|
1663
|
+
model: current.model,
|
|
1664
|
+
...(current.reasoningEffort === undefined ? {} : { reasoningEffort: current.reasoningEffort }),
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
catch {
|
|
1668
|
+
return undefined;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Ask the host what to do with a paste for the current model, and cache the
|
|
1673
|
+
* answer per Session and selector label. A model switch changes the label,
|
|
1674
|
+
* which changes the cache key, so a stale verdict never outlives the model
|
|
1675
|
+
* it described. The exact selection rides along when the live model catalog
|
|
1676
|
+
* is readable, so the host can answer with an auto-switch route; a 404
|
|
1677
|
+
* simply leaves the verdict unconfirmed; the next focus or paste retries.
|
|
1678
|
+
* @param sessionId - the live Session to ask about.
|
|
1679
|
+
* @param modelLabel - the model-selector label currently shown.
|
|
1680
|
+
*/
|
|
1681
|
+
refreshVerdict(sessionId, modelLabel) {
|
|
1682
|
+
const key = verdictKey(sessionId, modelLabel);
|
|
1683
|
+
const cached = this.verdicts.get(key);
|
|
1684
|
+
// Dedupe only on an in-flight request, never on freshness: the host's
|
|
1685
|
+
// model route can change under an unchanged Session id.
|
|
1686
|
+
if (cached?.pending)
|
|
1687
|
+
return;
|
|
1688
|
+
const entry = {
|
|
1689
|
+
pending: true,
|
|
1690
|
+
takeOver: cached ? cached.takeOver : false,
|
|
1691
|
+
at: cached ? cached.at : 0,
|
|
1692
|
+
...(cached?.autoSwitch === undefined ? {} : { autoSwitch: cached.autoSwitch }),
|
|
1693
|
+
};
|
|
1694
|
+
this.verdicts.set(key, entry);
|
|
1695
|
+
void (async () => {
|
|
1696
|
+
const selection = await this.readSelection(sessionId);
|
|
1697
|
+
const query = new URLSearchParams({ sessionId });
|
|
1698
|
+
if (modelLabel !== '')
|
|
1699
|
+
query.set('model', modelLabel);
|
|
1700
|
+
if (selection !== undefined) {
|
|
1701
|
+
query.set('provider', selection.provider);
|
|
1702
|
+
query.set('modelId', selection.model);
|
|
1703
|
+
if (selection.reasoningEffort !== undefined)
|
|
1704
|
+
query.set('reasoningEffort', selection.reasoningEffort);
|
|
1705
|
+
}
|
|
1706
|
+
let request;
|
|
1707
|
+
try {
|
|
1708
|
+
request = fetch(`${exports.PASTE_POLICY_ROUTE}?${query.toString()}`);
|
|
1709
|
+
}
|
|
1710
|
+
catch {
|
|
1711
|
+
// No fetch surface (test runtime, pre-fetch bootstrap): leave the
|
|
1712
|
+
// verdict unconfirmed rather than letting the paste listener die.
|
|
1713
|
+
entry.pending = false;
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
request
|
|
1717
|
+
.then((response) => {
|
|
1718
|
+
if (response.status === 404) {
|
|
1719
|
+
// Route not mounted yet (plugin load race, hot reload): forget every
|
|
1720
|
+
// verdict and retry on the next focus or paste instead of standing
|
|
1721
|
+
// down for the page lifetime.
|
|
1722
|
+
this.verdicts.clear();
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
if (!response.ok)
|
|
1726
|
+
throw new Error(`paste policy ${response.status}`);
|
|
1727
|
+
return response.json();
|
|
1728
|
+
})
|
|
1729
|
+
.then((body) => {
|
|
1730
|
+
entry.pending = false;
|
|
1731
|
+
if (body !== null) {
|
|
1732
|
+
entry.takeOver = body.value.takeOver === true;
|
|
1733
|
+
if (body.value.autoSwitch !== undefined)
|
|
1734
|
+
entry.autoSwitch = body.value.autoSwitch;
|
|
1735
|
+
else
|
|
1736
|
+
delete entry.autoSwitch;
|
|
1737
|
+
entry.at = Date.now();
|
|
1738
|
+
}
|
|
1739
|
+
})
|
|
1740
|
+
.catch(() => {
|
|
1741
|
+
entry.pending = false;
|
|
1742
|
+
});
|
|
1743
|
+
})();
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Switch one Session to the route the host validated, through the same
|
|
1747
|
+
* model-directory seat the selector uses when present (so the shared UI
|
|
1748
|
+
* state moves with the session), falling back to the raw RPC.
|
|
1749
|
+
* @param sessionId - the live Session id.
|
|
1750
|
+
* @param route - the validated variant route.
|
|
1751
|
+
*/
|
|
1752
|
+
async switchModel(sessionId, route) {
|
|
1753
|
+
const directories = this.ctx.get('modelDirectories');
|
|
1754
|
+
if (directories !== undefined) {
|
|
1755
|
+
// The label is a display hint; the seat only needs the exact route.
|
|
1756
|
+
await directories.directoryFor(sessionId).select({
|
|
1757
|
+
provider: route.provider,
|
|
1758
|
+
model: route.model,
|
|
1759
|
+
...(route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort }),
|
|
1760
|
+
});
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
const connection = this.ctx.get('connection');
|
|
1764
|
+
if (connection === undefined)
|
|
1765
|
+
throw new Error('No model switch channel is available in this Web application');
|
|
1766
|
+
const { result } = await connection.api.sessions.selectModel({
|
|
1767
|
+
sessionId,
|
|
1768
|
+
provider: route.provider,
|
|
1769
|
+
model: route.model,
|
|
1770
|
+
...(route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort }),
|
|
1771
|
+
});
|
|
1772
|
+
if (!result.ok)
|
|
1773
|
+
throw new Error(`${result.error?.code ?? 'select-model-failed'}: ${result.error?.message ?? 'unknown error'}`);
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Replay a swallowed paste as a synthetic clipboard event so the composer's
|
|
1777
|
+
* own intake (limits, thumbnails, keyboard) runs with the captured files.
|
|
1778
|
+
* @returns false when the environment cannot construct a clipboard payload.
|
|
1779
|
+
*/
|
|
1780
|
+
replayPaste(target, files, text) {
|
|
1781
|
+
let data;
|
|
1782
|
+
try {
|
|
1783
|
+
data = new DataTransfer();
|
|
1784
|
+
for (const file of files)
|
|
1785
|
+
data.items.add(file);
|
|
1786
|
+
if (text !== '')
|
|
1787
|
+
data.setData('text/plain', text);
|
|
1788
|
+
}
|
|
1789
|
+
catch {
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
let event;
|
|
1793
|
+
try {
|
|
1794
|
+
event = new ClipboardEvent('paste', {
|
|
1795
|
+
clipboardData: data,
|
|
1796
|
+
bubbles: true,
|
|
1797
|
+
cancelable: true,
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
catch {
|
|
1801
|
+
return false;
|
|
1802
|
+
}
|
|
1803
|
+
if (event.clipboardData === null || event.clipboardData.files.length === 0)
|
|
1804
|
+
return false;
|
|
1805
|
+
this.replaying = true;
|
|
1806
|
+
try {
|
|
1807
|
+
target.dispatchEvent(event);
|
|
1808
|
+
}
|
|
1809
|
+
finally {
|
|
1810
|
+
this.replaying = false;
|
|
1811
|
+
}
|
|
1812
|
+
return true;
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Auto-switch flow: switch the Session to the image-input variant, announce
|
|
1816
|
+
* it, then replay the paste into the composer's native intake. A failed
|
|
1817
|
+
* switch, or an environment that cannot replay clipboard bytes, degrades to
|
|
1818
|
+
* the path takeover with the same files.
|
|
1819
|
+
* @param sessionId - the live Session id.
|
|
1820
|
+
* @param target - the composer textarea the paste landed on.
|
|
1821
|
+
* @param files - the captured image files.
|
|
1822
|
+
* @param text - same-paste text, replayed alongside the files.
|
|
1823
|
+
* @param route - the validated variant route to switch to.
|
|
1824
|
+
*/
|
|
1825
|
+
async autoSwitchPaste(sessionId, target, files, text, route) {
|
|
1826
|
+
const input = this.inputFor(sessionId);
|
|
1827
|
+
try {
|
|
1828
|
+
await this.switchModel(sessionId, route);
|
|
1829
|
+
const { hidden } = await (0, display_config_ts_1.readDisplayConfig)();
|
|
1830
|
+
input.notify('info', hidden
|
|
1831
|
+
? 'Visual enhancement active: pasted images keep the native attachment flow'
|
|
1832
|
+
: `Switched to ${route.label || `${route.model} (Vision Toolkit)`}; pasted images now keep the native attachment flow`);
|
|
1833
|
+
}
|
|
1834
|
+
catch (error) {
|
|
1835
|
+
input.notify('error', `Model switch failed; images will be sent as workspace paths: ${message(error)}`);
|
|
1836
|
+
this.takeoverPaste(sessionId, target, files, text);
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
// Replaying lets the composer's own intake run (thumbnail, limits,
|
|
1840
|
+
// keyboard); if the environment cannot replay clipboard bytes, the
|
|
1841
|
+
// images still land as workspace paths.
|
|
1842
|
+
const before = input.state.getSnapshot().imageIds.length;
|
|
1843
|
+
const replayed = this.replayPaste(target, files, text);
|
|
1844
|
+
const after = input.state.getSnapshot().imageIds.length;
|
|
1845
|
+
if (!replayed || after <= before) {
|
|
1846
|
+
this.takeoverPaste(sessionId, target, files, text);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Path-takeover flow: insert the same-paste text and every image as a text
|
|
1851
|
+
* reference that serializes to the image's workspace path on send.
|
|
1852
|
+
* @param sessionId - the live Session id.
|
|
1853
|
+
* @param target - the composer textarea the paste landed on.
|
|
1854
|
+
* @param files - the captured image files.
|
|
1855
|
+
* @param text - same-paste text.
|
|
1856
|
+
*/
|
|
1857
|
+
takeoverPaste(sessionId, target, files, text) {
|
|
1858
|
+
const input = this.inputFor(sessionId);
|
|
1859
|
+
const snapshot = input.state.getSnapshot();
|
|
1860
|
+
if (snapshot.phase !== 'plain')
|
|
1861
|
+
return;
|
|
1862
|
+
const start = Math.max(0, Math.min(target.selectionStart ?? snapshot.draft.length, snapshot.draft.length));
|
|
1863
|
+
const end = Math.max(start, Math.min(target.selectionEnd ?? start, snapshot.draft.length));
|
|
1864
|
+
try {
|
|
1865
|
+
let cursor = this.insertText(input, text, start, end);
|
|
1866
|
+
validateImages(files);
|
|
1867
|
+
cursor = this.insertRecords(sessionId, input, files, cursor);
|
|
1868
|
+
requestAnimationFrame(() => {
|
|
1869
|
+
target.focus({ preventScroll: true });
|
|
1870
|
+
target.setSelectionRange(cursor, cursor);
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
catch (error) {
|
|
1874
|
+
input.notify('error', message(error));
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
handlePaste(event) {
|
|
1878
|
+
if (this.replaying)
|
|
1879
|
+
return false;
|
|
1880
|
+
const files = imageFiles(event.clipboardData);
|
|
1881
|
+
if (files.length === 0)
|
|
1882
|
+
return false;
|
|
1883
|
+
const target = event.target;
|
|
1884
|
+
if (!(target instanceof HTMLTextAreaElement) || target.closest('[data-composer-card]') === null)
|
|
1885
|
+
return false;
|
|
1886
|
+
const sessionId = this.ctx.sessions.list.getSnapshot().current;
|
|
1887
|
+
if (sessionId === undefined)
|
|
1888
|
+
return false;
|
|
1889
|
+
const modelLabel = currentModelLabel();
|
|
1890
|
+
this.refreshVerdict(sessionId, modelLabel);
|
|
1891
|
+
// Only a fresh host verdict acts; the native attachment flow stays the
|
|
1892
|
+
// default while the host is unconfirmed.
|
|
1893
|
+
const verdict = this.verdictFor(sessionId, modelLabel);
|
|
1894
|
+
if (verdict === undefined)
|
|
1895
|
+
return false;
|
|
1896
|
+
// An image-capable model (the variant routes included) keeps its native
|
|
1897
|
+
// paste: no switch, no takeover.
|
|
1898
|
+
if (verdict.takeOver === false && verdict.autoSwitch === undefined)
|
|
1899
|
+
return false;
|
|
1900
|
+
event.preventDefault();
|
|
1901
|
+
event.stopPropagation();
|
|
1902
|
+
event.stopImmediatePropagation();
|
|
1903
|
+
const input = this.inputFor(sessionId);
|
|
1904
|
+
if (input.state.getSnapshot().phase !== 'plain')
|
|
1905
|
+
return true;
|
|
1906
|
+
const text = (event.clipboardData?.getData('text/plain') ?? '').replaceAll('\uFFFC', '');
|
|
1907
|
+
if (verdict.autoSwitch !== undefined) {
|
|
1908
|
+
// The text-only model has an image-input variant: switch first, then
|
|
1909
|
+
// let the paste flow natively so the thumbnail and durable session
|
|
1910
|
+
// image are preserved.
|
|
1911
|
+
void this.autoSwitchPaste(sessionId, target, files, text, verdict.autoSwitch);
|
|
1912
|
+
return true;
|
|
1913
|
+
}
|
|
1914
|
+
this.takeoverPaste(sessionId, target, files, text);
|
|
1915
|
+
return true;
|
|
1916
|
+
}
|
|
1917
|
+
remove(sessionId, occurrence) {
|
|
1918
|
+
const record = this.records.get(occurrence.ref);
|
|
1919
|
+
if (record?.batch.inflight !== undefined)
|
|
1920
|
+
return;
|
|
1921
|
+
const input = this.inputFor(sessionId);
|
|
1922
|
+
const snapshot = input.state.getSnapshot();
|
|
1923
|
+
if (snapshot.phase !== 'plain')
|
|
1924
|
+
return;
|
|
1925
|
+
const current = snapshot.occurrences.find(candidate => candidate.source === SOURCE
|
|
1926
|
+
&& candidate.occurrenceId === occurrence.occurrenceId
|
|
1927
|
+
&& candidate.ref === occurrence.ref);
|
|
1928
|
+
if (current === undefined)
|
|
1929
|
+
return;
|
|
1930
|
+
const accepted = input.insertText('', {
|
|
1931
|
+
start: current.offset,
|
|
1932
|
+
end: current.offset + 1,
|
|
1933
|
+
draftRev: snapshot.draftRev,
|
|
1934
|
+
});
|
|
1935
|
+
if (!accepted)
|
|
1936
|
+
return;
|
|
1937
|
+
this.records.delete(occurrence.ref);
|
|
1938
|
+
this.changed();
|
|
1939
|
+
}
|
|
1940
|
+
async upload(batch, signal) {
|
|
1941
|
+
if (batch.inflight !== undefined)
|
|
1942
|
+
return batch.inflight;
|
|
1943
|
+
const active = batch.records.filter(record => this.records.get(record.ref) === record);
|
|
1944
|
+
if (active.length === 0)
|
|
1945
|
+
throw new Error('Pasted images were removed before sending');
|
|
1946
|
+
const pending = active.filter(record => record.absolutePath === undefined);
|
|
1947
|
+
if (pending.length === 0)
|
|
1948
|
+
return;
|
|
1949
|
+
const task = (async () => {
|
|
1950
|
+
for (const record of pending) {
|
|
1951
|
+
record.status = 'copying';
|
|
1952
|
+
record.error = undefined;
|
|
1953
|
+
}
|
|
1954
|
+
this.changed();
|
|
1955
|
+
try {
|
|
1956
|
+
const failures = await Promise.all(pending.map(async (record) => {
|
|
1957
|
+
try {
|
|
1958
|
+
if (signal.aborted)
|
|
1959
|
+
throw signal.reason ?? new DOMException('Aborted', 'AbortError');
|
|
1960
|
+
const query = new URLSearchParams({
|
|
1961
|
+
sessionId: batch.sessionId,
|
|
1962
|
+
name: record.file.name || 'clipboard-image',
|
|
1963
|
+
size: String(record.file.size),
|
|
1964
|
+
});
|
|
1965
|
+
const body = await responseJson(await fetch(`${exports.PASTE_IMAGES_ROUTE}?${query.toString()}`, {
|
|
1966
|
+
method: 'POST',
|
|
1967
|
+
headers: { 'Content-Type': record.file.type },
|
|
1968
|
+
body: record.file,
|
|
1969
|
+
signal,
|
|
1970
|
+
}));
|
|
1971
|
+
const absolutePath = body.value?.absolutePath;
|
|
1972
|
+
if (typeof absolutePath !== 'string' || absolutePath === '') {
|
|
1973
|
+
throw new Error('Image copy response contained an invalid path');
|
|
1974
|
+
}
|
|
1975
|
+
record.absolutePath = absolutePath;
|
|
1976
|
+
record.status = 'copied';
|
|
1977
|
+
record.error = undefined;
|
|
1978
|
+
return undefined;
|
|
1979
|
+
}
|
|
1980
|
+
catch (error) {
|
|
1981
|
+
const failure = error instanceof Error ? error : new Error(message(error));
|
|
1982
|
+
record.status = 'error';
|
|
1983
|
+
record.error = failure.message;
|
|
1984
|
+
return failure;
|
|
1985
|
+
}
|
|
1986
|
+
}));
|
|
1987
|
+
this.changed();
|
|
1988
|
+
const failure = failures.find((error) => error !== undefined);
|
|
1989
|
+
if (failure !== undefined)
|
|
1990
|
+
throw failure;
|
|
1991
|
+
}
|
|
1992
|
+
finally {
|
|
1993
|
+
batch.inflight = undefined;
|
|
1994
|
+
this.changed();
|
|
1995
|
+
}
|
|
1996
|
+
})();
|
|
1997
|
+
batch.inflight = task;
|
|
1998
|
+
return task;
|
|
1999
|
+
}
|
|
2000
|
+
async serialize(ref, signal) {
|
|
2001
|
+
const record = this.records.get(ref);
|
|
2002
|
+
if (record === undefined)
|
|
2003
|
+
throw new Error('Pasted image is no longer available in this browser tab');
|
|
2004
|
+
await this.upload(record.batch, signal);
|
|
2005
|
+
if (record.absolutePath === undefined)
|
|
2006
|
+
throw new Error('Pasted image was not copied into the workspace');
|
|
2007
|
+
return `[Pasted image available at absolute path: ${JSON.stringify(record.absolutePath)}]`;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
exports.PasteImageController = PasteImageController;
|
|
2011
|
+
/** Minimal per-image progress, failure, and removal feedback above the composer. */
|
|
2012
|
+
function PasteImageDock(props) {
|
|
2013
|
+
(0, react_1.useSyncExternalStore)(props.controller.subscribe, props.controller.snapshot);
|
|
2014
|
+
const occurrences = props.input.occurrences.filter(occurrence => occurrence.source === SOURCE);
|
|
2015
|
+
const records = props.controller.recordsFor(occurrences);
|
|
2016
|
+
if (records.length === 0)
|
|
2017
|
+
return null;
|
|
2018
|
+
return (0, jsx_runtime_1.jsx)("div", { className: "dvt-paste-dock", role: "status", "aria-label": "Pasted images", children: occurrences.map((occurrence) => {
|
|
2019
|
+
const record = props.controller.recordsFor([occurrence])[0];
|
|
2020
|
+
if (record === undefined)
|
|
2021
|
+
return null;
|
|
2022
|
+
const detail = record.status === 'copying' ? 'copying…'
|
|
2023
|
+
: record.status === 'copied' ? 'copied'
|
|
2024
|
+
: record.status === 'error' ? record.error ?? 'copy failed'
|
|
2025
|
+
: humanBytes(record.file.size);
|
|
2026
|
+
return (0, jsx_runtime_1.jsxs)("div", { className: "dvt-paste-chip", "data-status": record.status, children: [(0, jsx_runtime_1.jsx)("span", { className: "dvt-paste-name", title: record.file.name, children: record.file.name || 'clipboard image' }), (0, jsx_runtime_1.jsx)("span", { className: "dvt-paste-detail", title: record.error, children: detail }), (0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": `Remove ${record.file.name || 'clipboard image'}`, disabled: props.input.phase !== 'plain' || record.status === 'copying', onClick: () => { props.remove(occurrence); }, children: "\u00D7" })] }, occurrence.occurrenceId);
|
|
2027
|
+
}) });
|
|
2028
|
+
}
|
|
2029
|
+
/** Install capture interception, the text-reference codec, and composer feedback. */
|
|
2030
|
+
function installPasteImages(ctx) {
|
|
2031
|
+
const controller = new PasteImageController(ctx);
|
|
2032
|
+
const registered = new WeakMap();
|
|
2033
|
+
const register = (scope, registry) => {
|
|
2034
|
+
scope.effect(() => {
|
|
2035
|
+
const identity = registryIdentity(registry);
|
|
2036
|
+
let registration = registered.get(identity);
|
|
2037
|
+
if (registration === undefined) {
|
|
2038
|
+
registration = { dispose: registry.registerSource(controller.source()), owners: 0 };
|
|
2039
|
+
registered.set(identity, registration);
|
|
2040
|
+
}
|
|
2041
|
+
registration.owners += 1;
|
|
2042
|
+
return () => {
|
|
2043
|
+
if (registered.get(identity) !== registration)
|
|
2044
|
+
return;
|
|
2045
|
+
registration.owners -= 1;
|
|
2046
|
+
if (registration.owners > 0)
|
|
2047
|
+
return;
|
|
2048
|
+
registered.delete(identity);
|
|
2049
|
+
registration.dispose();
|
|
2050
|
+
};
|
|
2051
|
+
}, 'dsh-vision-toolkit: pasted image reference codec');
|
|
2052
|
+
};
|
|
2053
|
+
ctx.inject(['slash'], (scope) => {
|
|
2054
|
+
register(scope, scope.slash);
|
|
2055
|
+
});
|
|
2056
|
+
ctx.inject(['inputTriggers'], (scope) => {
|
|
2057
|
+
register(scope, scope.inputTriggers);
|
|
2058
|
+
});
|
|
2059
|
+
ctx.effect(() => {
|
|
2060
|
+
const listener = (event) => { controller.handlePaste(event); };
|
|
2061
|
+
// A focus-time prefetch has the verdict ready before the first paste can land.
|
|
2062
|
+
const onFocusIn = () => {
|
|
2063
|
+
const sessionId = ctx.sessions.list.getSnapshot().current;
|
|
2064
|
+
if (sessionId !== undefined)
|
|
2065
|
+
controller.refreshVerdict(String(sessionId), currentModelLabel());
|
|
2066
|
+
};
|
|
2067
|
+
document.addEventListener('paste', listener, true);
|
|
2068
|
+
document.addEventListener('focusin', onFocusIn, true);
|
|
2069
|
+
return () => {
|
|
2070
|
+
document.removeEventListener('paste', listener, true);
|
|
2071
|
+
document.removeEventListener('focusin', onFocusIn, true);
|
|
2072
|
+
};
|
|
2073
|
+
}, 'dsh-vision-toolkit: clipboard image capture');
|
|
2074
|
+
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
2075
|
+
name: 'conversation.input.dock',
|
|
2076
|
+
id: 'vision-toolkit-pasted-images',
|
|
2077
|
+
order: 6,
|
|
2078
|
+
inject: sessionId => ({
|
|
2079
|
+
controller,
|
|
2080
|
+
remove: (occurrence) => { controller.remove(String(sessionId), occurrence); },
|
|
2081
|
+
}),
|
|
2082
|
+
}, PasteImageDock));
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
function __resolve(from, request) {
|
|
2086
|
+
if (!request.startsWith(".")) return request;
|
|
2087
|
+
var parts = from.slice(2).split("/"); parts.pop();
|
|
2088
|
+
for (var part of request.split("/")) { if (part === "." || part === "") continue; if (part === "..") parts.pop(); else parts.push(part); }
|
|
2089
|
+
return "./" + parts.join("/");
|
|
2090
|
+
}
|
|
2091
|
+
function __load(id) {
|
|
2092
|
+
if (__modules[id] === undefined) return require(id);
|
|
2093
|
+
if (__cache[id] !== undefined) return __cache[id].exports;
|
|
2094
|
+
var module = __cache[id] = { exports: {} };
|
|
2095
|
+
__modules[id](module, module.exports, require, function(request) { var resolved = __resolve(id, request); return __modules[resolved] === undefined ? require(request) : __load(resolved); });
|
|
2096
|
+
return module.exports;
|
|
2097
|
+
}
|
|
2098
|
+
return __load("./index.js"); } });
|
|
2099
|
+
//# sourceMappingURL=client.js.map
|