dsh-vision-router 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +33 -16
  2. package/README.zh.md +34 -17
  3. package/cordis.patch.yml +19 -9
  4. package/docs/architecture/2x-contract-ledger.md +165 -0
  5. package/docs/architecture/compat-inventory.md +121 -0
  6. package/docs/architecture/dsh-compatibility-matrix.md +57 -0
  7. package/docs/architecture/dsh-support-window.md +50 -0
  8. package/docs/architecture/final-closure-audit.md +148 -0
  9. package/docs/architecture/p2-jobs-feasibility.md +94 -0
  10. package/docs/architecture/p3-compat-retirement.md +46 -0
  11. package/docs/architecture/p3-host-native-seams.md +43 -0
  12. package/docs/architecture/p3-native-recovery-evaluation.md +65 -0
  13. package/docs/architecture/runtime-boundaries.md +124 -0
  14. package/docs/releases/v2.0.1.md +17 -0
  15. package/docs/releases/v2.1.0-support-window.md +29 -0
  16. package/docs/releases/v2.1.0.md +83 -0
  17. package/entry.js +13 -307
  18. package/index.js +85 -352
  19. package/lib/abort-signal-compat.js +191 -0
  20. package/lib/adapter-update-coalescer.js +113 -29
  21. package/lib/adversarial-hardening.js +24 -9
  22. package/lib/artifact-boundary.js +16 -175
  23. package/lib/artifact-io.js +220 -0
  24. package/lib/artifact-retention.js +305 -53
  25. package/lib/catalog-corrections.js +6 -1
  26. package/lib/client-host-compat-prelude.js +249 -0
  27. package/lib/client-presentation-boundary.js +61 -3
  28. package/lib/client.js +71 -72
  29. package/lib/core-vision-surface.js +124 -0
  30. package/lib/depth-guidance.js +114 -90
  31. package/lib/doctor-cli-p0.js +168 -0
  32. package/lib/doctor-cli.js +7 -0
  33. package/lib/doctor-vision-limits.js +72 -0
  34. package/lib/dsh-contract-compat.js +207 -39
  35. package/lib/dsh-host-capabilities.js +107 -0
  36. package/lib/dsh-support-window.js +48 -0
  37. package/lib/http-compat.js +22 -6
  38. package/lib/image-resource-governor.js +35 -10
  39. package/lib/legacy-core-vision-policy-bridge.js +25 -226
  40. package/lib/legacy-global-proxy-boundary.js +127 -0
  41. package/lib/live-model-client-prelude.js +0 -12
  42. package/lib/mixed-router.js +81 -81
  43. package/lib/public-entry.js +42 -4
  44. package/lib/runtime-composition.js +348 -0
  45. package/lib/runtime-i18n-boundary.js +683 -0
  46. package/lib/runtime-i18n-core-scope.js +56 -0
  47. package/lib/runtime-i18n-core.js +34 -0
  48. package/lib/runtime-i18n.js +121 -0
  49. package/lib/session-surface-policy.js +97 -0
  50. package/lib/session-vision-index.js +282 -0
  51. package/lib/session-vision-runtime.js +48 -0
  52. package/lib/settings-factory-lifecycle.js +144 -0
  53. package/lib/settings-ia-client-prelude.js +74 -2
  54. package/lib/settings-limit-client-prelude.js +110 -0
  55. package/lib/settings-native-card-layout.js +297 -0
  56. package/lib/settings-number-contract.js +29 -0
  57. package/lib/structured-flow-hardening.js +207 -76
  58. package/lib/tesseract-exec-compat.js +58 -32
  59. package/lib/update-check.js +17 -5
  60. package/lib/v2-settings-ia-integration.js +9 -4
  61. package/lib/vision-artifact-store.js +77 -0
  62. package/lib/vision-backend-runtime-policy.js +6 -2
  63. package/lib/vision-background-benchmark.js +130 -41
  64. package/lib/vision-background-failure-policy.js +70 -0
  65. package/lib/vision-background-stop-store.js +54 -9
  66. package/lib/vision-capability-benchmark-client.js +57 -66
  67. package/lib/vision-capability-benchmark-presentation.js +97 -0
  68. package/lib/vision-capability-benchmark-service.js +3 -3
  69. package/lib/vision-execution-order-apply.js +61 -0
  70. package/lib/vision-execution-order-plan.js +64 -0
  71. package/lib/vision-execution-order.js +38 -0
  72. package/lib/vision-limit-diagnostics.js +202 -0
  73. package/lib/vision-product-presentation.js +284 -0
  74. package/lib/vision-provider-transport.js +197 -0
  75. package/lib/vision-resilience.js +87 -45
  76. package/lib/vision-routing-evidence.js +366 -0
  77. package/lib/vision-routing-runtime.js +278 -0
  78. package/lib/vision-tool-runtime-boundary.js +27 -21
  79. package/lib/vision-turn-budget-client-prelude.js +9 -9
  80. package/lib/web/benchmark-panel.js +6 -0
  81. package/lib/web/diagnostics-panel.js +44 -0
  82. package/lib/web/index.js +60 -0
  83. package/lib/web/model-picker.js +22 -0
  84. package/lib/web/onboarding.js +6 -0
  85. package/lib/web/product-state.js +59 -0
  86. package/lib/web/remote-settings-client.js +8 -0
  87. package/lib/web/routing-section.js +6 -0
  88. package/lib/web/settings-controller.js +6 -0
  89. package/lib/web-capability-boundary.js +65 -9
  90. package/lib/windows-screenshot-dpi-compat.js +148 -0
  91. package/package.json +14 -6
  92. package/lib/vision-capability-shadow.js +0 -576
@@ -0,0 +1,249 @@
1
+ import { htmlHasScriptMarker } from './html-script-marker.js'
2
+
3
+ const CLIENT_HOST_COMPAT_MARK = 'data-vision-router-host-compat'
4
+
5
+ /**
6
+ * Client-side Host compatibility shim for the hand-maintained Vision Router
7
+ * settings factory.
8
+ *
9
+ * DSH 0.1.2 moved model-catalog ownership from the legacy
10
+ * `connection.api.llm.models()` facade to `remote.session.modelCatalog()` and
11
+ * renamed the forwarded credential invalidation event. Keep the old client
12
+ * factory unchanged: when (and only when) the new Host capability exists,
13
+ * expose a tiny compatibility view of those two seams to that one factory.
14
+ * Old rc.6/rc.7/rc.8 clients see their original Connection and Remote objects
15
+ * byte-for-byte because the capability probe fails closed.
16
+ */
17
+ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
18
+ 'use strict';
19
+ var TARGET = 'dsh-vision-router';
20
+ var LEGACY_CREDENTIAL_EVENT = 'credentials/updated';
21
+ var HOST_CREDENTIAL_EVENT = 'credentials/reference-updated';
22
+
23
+ function hasHostCatalog(remote) {
24
+ return !!(
25
+ remote && remote.session &&
26
+ typeof remote.session.modelCatalog === 'function'
27
+ );
28
+ }
29
+
30
+ function wrapCatalogResult(value) {
31
+ // The alpha Host returns ConnectionRpcResult<ModelCatalog>. The legacy
32
+ // factory already understands the older { result: RpcResult } envelope, so
33
+ // adapt only that carrier shape and leave future direct values untouched.
34
+ if (value && typeof value === 'object' && typeof value.ok === 'boolean') {
35
+ return { result: value };
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function catalogModels(remote) {
41
+ return function models() {
42
+ return Promise.resolve(remote.session.modelCatalog()).then(wrapCatalogResult);
43
+ };
44
+ }
45
+
46
+ function compatibleApi(originalApi, remote) {
47
+ var originalLlm = originalApi && originalApi.llm;
48
+ var llm = originalLlm && (typeof originalLlm === 'object' || typeof originalLlm === 'function')
49
+ ? new Proxy(originalLlm, {
50
+ get: function(target, property) {
51
+ if (property === 'models') return catalogModels(remote);
52
+ var value = Reflect.get(target, property, target);
53
+ return typeof value === 'function' ? value.bind(target) : value;
54
+ }
55
+ })
56
+ : { models: catalogModels(remote) };
57
+ if (originalApi && (typeof originalApi === 'object' || typeof originalApi === 'function')) {
58
+ return new Proxy(originalApi, {
59
+ get: function(target, property) {
60
+ if (property === 'llm') return llm;
61
+ var value = Reflect.get(target, property, target);
62
+ return typeof value === 'function' ? value.bind(target) : value;
63
+ }
64
+ });
65
+ }
66
+ return { llm: llm };
67
+ }
68
+
69
+ function compatibleConnection(connection, remote) {
70
+ if (!hasHostCatalog(remote)) return connection;
71
+ if (!connection || (typeof connection !== 'object' && typeof connection !== 'function')) return connection;
72
+ var api = compatibleApi(connection.api, remote);
73
+ return new Proxy(connection, {
74
+ get: function(target, property) {
75
+ if (property === 'api') return api;
76
+ var value = Reflect.get(target, property, target);
77
+ return typeof value === 'function' ? value.bind(target) : value;
78
+ }
79
+ });
80
+ }
81
+
82
+ function compatibleRemote(remote) {
83
+ if (!hasHostCatalog(remote)) return remote;
84
+ if (!remote || (typeof remote !== 'object' && typeof remote !== 'function')) return remote;
85
+ return new Proxy(remote, {
86
+ get: function(target, property) {
87
+ if (property === '$on') {
88
+ var subscribe = Reflect.get(target, property, target);
89
+ if (typeof subscribe !== 'function') return subscribe;
90
+ return function(event, listener) {
91
+ var args = Array.prototype.slice.call(arguments);
92
+ if (event === LEGACY_CREDENTIAL_EVENT) args[0] = HOST_CREDENTIAL_EVENT;
93
+ return subscribe.apply(target, args);
94
+ };
95
+ }
96
+ var value = Reflect.get(target, property, target);
97
+ return typeof value === 'function' ? value.bind(target) : value;
98
+ }
99
+ });
100
+ }
101
+
102
+ function patchCanonicalImageCopy(namespace, dictionaries) {
103
+ if (namespace !== 'vision-router' || !dictionaries || typeof dictionaries !== 'object') return dictionaries;
104
+ var next = Object.assign({}, dictionaries);
105
+ if (next.zh && typeof next.zh === 'object') {
106
+ next.zh = Object.assign({}, next.zh, {
107
+ hintInstantDescribe: '会话日志保留 Host 持久化后的图片;像素工具以这份 canonical 图片为坐标与像素事实源。',
108
+ openPresentedImage: '点击查看图片',
109
+ openNamedImage: '查看图片:{name}'
110
+ });
111
+ }
112
+ if (next.en && typeof next.en === 'object') {
113
+ next.en = Object.assign({}, next.en, {
114
+ hintInstantDescribe: 'The session log keeps the Host-persisted image; pixel tools use that canonical image as their coordinate and pixel source of truth.',
115
+ openPresentedImage: 'Open image',
116
+ openNamedImage: 'Open image: {name}'
117
+ });
118
+ }
119
+ return next;
120
+ }
121
+
122
+ function compatibleLocale(locale) {
123
+ if (!locale || (typeof locale !== 'object' && typeof locale !== 'function')) return locale;
124
+ return new Proxy(locale, {
125
+ get: function(target, property) {
126
+ if (property === 'register') {
127
+ var register = Reflect.get(target, property, target);
128
+ if (typeof register !== 'function') return register;
129
+ return function(namespace, dictionaries) {
130
+ var args = Array.prototype.slice.call(arguments);
131
+ args[1] = patchCanonicalImageCopy(namespace, dictionaries);
132
+ return register.apply(target, args);
133
+ };
134
+ }
135
+ var value = Reflect.get(target, property, target);
136
+ return typeof value === 'function' ? value.bind(target) : value;
137
+ }
138
+ });
139
+ }
140
+
141
+ function compatibleContext(ctx) {
142
+ if (!ctx || typeof ctx !== 'object' || typeof Proxy !== 'function') return ctx;
143
+ var remote = compatibleRemote(ctx.remote);
144
+ var locale = compatibleLocale(ctx.locale);
145
+ return new Proxy(ctx, {
146
+ get: function(target, property) {
147
+ if (property === 'remote') return remote;
148
+ if (property === 'locale') return locale;
149
+ if (property === 'get') {
150
+ var get = Reflect.get(target, property, target);
151
+ if (typeof get !== 'function') return get;
152
+ return function(name) {
153
+ var value = get.call(target, name);
154
+ return name === 'connection' ? compatibleConnection(value, remote) : value;
155
+ };
156
+ }
157
+ var value = Reflect.get(target, property, target);
158
+ return typeof value === 'function' ? value.bind(target) : value;
159
+ }
160
+ });
161
+ }
162
+
163
+ function decorate(plugin) {
164
+ if (!plugin || typeof plugin !== 'object' || typeof plugin.apply !== 'function') return plugin;
165
+ if (plugin.apply.__visionRouterHostCompat) return plugin;
166
+ var originalApply = plugin.apply;
167
+ function apply(ctx) {
168
+ var args = Array.prototype.slice.call(arguments);
169
+ args[0] = compatibleContext(ctx);
170
+ return originalApply.apply(this, args);
171
+ }
172
+ try { Object.defineProperty(apply, '__visionRouterHostCompat', { value: true }); } catch (_) {}
173
+ plugin.apply = apply;
174
+ return plugin;
175
+ }
176
+
177
+ function patchLoader(loader) {
178
+ if (!loader || (typeof loader !== 'object' && typeof loader !== 'function')) return;
179
+ if (typeof loader.load === 'function' && !loader.load.__visionRouterHostCompat) {
180
+ var originalLoad = loader.load;
181
+ function load(spec) {
182
+ if (spec && spec.id === TARGET && typeof spec.factory === 'function') {
183
+ var factory = spec.factory;
184
+ spec = Object.assign({}, spec, {
185
+ factory: function(require) { return decorate(factory(require)); }
186
+ });
187
+ }
188
+ return originalLoad.call(this, spec);
189
+ }
190
+ try { Object.defineProperty(load, '__visionRouterHostCompat', { value: true }); } catch (_) {}
191
+ loader.load = load;
192
+ }
193
+ if (typeof loader.create === 'function' && !loader.create.__visionRouterHostCompat) {
194
+ var originalCreate = loader.create;
195
+ function create() {
196
+ var result = originalCreate.apply(this, arguments);
197
+ patchLoader(loader);
198
+ return result;
199
+ }
200
+ try { Object.defineProperty(create, '__visionRouterHostCompat', { value: true }); } catch (_) {}
201
+ loader.create = create;
202
+ }
203
+ }
204
+
205
+ function install() {
206
+ if (window.__ModuleLoader__) {
207
+ patchLoader(window.__ModuleLoader__);
208
+ return;
209
+ }
210
+ var descriptor = Object.getOwnPropertyDescriptor(window, '__ModuleLoader__');
211
+ if (descriptor && descriptor.configurable === false) return;
212
+ var stored;
213
+ Object.defineProperty(window, '__ModuleLoader__', {
214
+ configurable: true,
215
+ enumerable: true,
216
+ get: function(){ return stored; },
217
+ set: function(value) {
218
+ stored = value;
219
+ patchLoader(value);
220
+ Object.defineProperty(window, '__ModuleLoader__', {
221
+ configurable: true,
222
+ enumerable: true,
223
+ writable: true,
224
+ value: stored
225
+ });
226
+ }
227
+ });
228
+ }
229
+
230
+ install();
231
+ })();`
232
+
233
+ export function injectClientHostCompatibility(html) {
234
+ if (typeof html !== 'string' || htmlHasScriptMarker(html, CLIENT_HOST_COMPAT_MARK)) return html
235
+ const safe = CLIENT_HOST_COMPAT_PRELUDE.replace(/<\/script/gi, '<\\/script')
236
+ const script = `<script ${CLIENT_HOST_COMPAT_MARK}>${safe}</script>`
237
+ const closeHead = html.indexOf('</head>')
238
+ if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
239
+ return `${html}${script}`
240
+ }
241
+
242
+ export function installClientHostCompatibility(ctx) {
243
+ ctx?.inject?.(['webServer'], (webCtx) => {
244
+ webCtx.effect(
245
+ () => webCtx.webServer.tapIndex(injectClientHostCompatibility),
246
+ 'vision-router: client Host compatibility boundary',
247
+ )
248
+ })
249
+ }
@@ -1,16 +1,70 @@
1
1
  import { htmlHasScriptMarker } from './html-script-marker.js'
2
2
  import {
3
- CLIENT_PRESENTATION_PRELUDE,
3
+ CLIENT_PRESENTATION_PRELUDE as BASE_CLIENT_PRESENTATION_PRELUDE,
4
4
  resolveVisionModePair,
5
5
  } from './client-presentation-boundary-main.js'
6
+ import {
7
+ installClientHostCompatibility,
8
+ } from './client-host-compat-prelude.js'
6
9
  import { installVisionModelVisibilityBoundary } from './vision-model-visibility-boundary.js'
7
10
 
8
11
  const CLIENT_PRESENTATION_MARK = 'data-vision-router-presentation-boundary'
9
12
 
13
+ const VISION_TOGGLE_TEXT_GLYPH = String.raw` React.createElement('span', { 'aria-hidden': 'true', style: { fontSize: 13, lineHeight: 1 } }, '👁'),
14
+ React.createElement('span', null, t('label')),
15
+ active
16
+ ? React.createElement('span', {
17
+ 'aria-hidden': 'true',
18
+ style: { fontSize: 12, lineHeight: 1, fontWeight: 800, marginLeft: 1 }
19
+ }, '✓')
20
+ : null`
21
+
22
+ const VISION_TOGGLE_FIXED_SVG = String.raw` React.createElement('svg', {
23
+ width: 14,
24
+ height: 14,
25
+ viewBox: '0 0 14 14',
26
+ fill: 'none',
27
+ 'aria-hidden': 'true',
28
+ focusable: 'false',
29
+ style: { display: 'block', flex: '0 0 auto' }
30
+ }, React.createElement('path', {
31
+ fillRule: 'evenodd',
32
+ clipRule: 'evenodd',
33
+ d: 'M7 2.25c-2.84 0-5.04 1.69-6.25 4.25a1.15 1.15 0 0 0 0 1C1.96 10.06 4.16 11.75 7 11.75s5.04-1.69 6.25-4.25a1.15 1.15 0 0 0 0-1C12.04 3.94 9.84 2.25 7 2.25Zm0 1.25c2.16 0 3.96 1.21 5.05 3.5C10.96 9.29 9.16 10.5 7 10.5S3.04 9.29 1.95 7C3.04 4.71 4.84 3.5 7 3.5Zm0 1.25A2.25 2.25 0 1 0 7 9.25a2.25 2.25 0 0 0 0-4.5Zm0 1.25a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z',
34
+ fill: 'currentColor'
35
+ })),
36
+ React.createElement('span', null, t('label')),
37
+ active
38
+ ? React.createElement('svg', {
39
+ width: 12,
40
+ height: 12,
41
+ viewBox: '0 0 14 14',
42
+ fill: 'none',
43
+ 'aria-hidden': 'true',
44
+ focusable: 'false',
45
+ style: { display: 'block', flex: '0 0 auto', marginLeft: 1 }
46
+ }, React.createElement('path', {
47
+ d: 'M2.75 7.15 5.6 10 11.25 4.35',
48
+ stroke: 'currentColor',
49
+ strokeWidth: 1.5,
50
+ strokeLinecap: 'round',
51
+ strokeLinejoin: 'round'
52
+ }))
53
+ : null`
54
+
55
+ export function transformVisionModeToggleIcons(source = BASE_CLIENT_PRESENTATION_PRELUDE) {
56
+ const input = String(source)
57
+ if (!input.includes(VISION_TOGGLE_TEXT_GLYPH)) {
58
+ throw new Error('vision toggle icon transform anchor missing')
59
+ }
60
+ return input.replace(VISION_TOGGLE_TEXT_GLYPH, VISION_TOGGLE_FIXED_SVG)
61
+ }
62
+
63
+ export const CLIENT_PRESENTATION_PRELUDE = transformVisionModeToggleIcons()
64
+
10
65
  export {
11
- CLIENT_PRESENTATION_PRELUDE,
12
66
  resolveVisionModePair,
13
- } from './client-presentation-boundary-main.js'
67
+ }
14
68
 
15
69
  export function injectClientPresentationBoundary(html) {
16
70
  if (typeof html !== 'string' || htmlHasScriptMarker(html, CLIENT_PRESENTATION_MARK)) return html
@@ -28,6 +82,10 @@ export function installClientPresentationBoundary(ctx) {
28
82
  'vision-router: client presentation boundary',
29
83
  )
30
84
  })
85
+ // Keep Host-version compatibility in its own capability-detected prelude.
86
+ // This lets the large legacy client factory stay byte-stable while alpha/new
87
+ // Hosts expose their current Remote/Connection seams to it.
88
+ installClientHostCompatibility(ctx)
31
89
  // #286 keeps real wrapper routes registered while hiding only confidently
32
90
  // owned wrappers from DSH's stock model-selection presentation. Compose that
33
91
  // presentation boundary here so v2 can retain its hardened script-marker
package/lib/client.js CHANGED
@@ -1,6 +1,5 @@
1
- // dsh-vision-router browser half: a first-class 设置 > Vision Router page plus
2
- // the legacy 设置 > 插件 configuration card, both editing the same host-owned
3
- // `vision-router` settings namespace. Self-contained
1
+ // dsh-vision-router browser half: one first-class Settings > Vision Router page
2
+ // backed by the host-owned `vision-router` settings namespace. Self-contained
4
3
  // by hand (no bundler in this repo): the client module system wraps it in a
5
4
  // CJS factory and the kernel adopts { apply, inject } as a client plugin.
6
5
  window.__ModuleLoader__.load({
@@ -56,9 +55,6 @@ window.__ModuleLoader__.load({
56
55
  remoteSettingsProxyBody: '远程设置路由返回 404。若使用 Nginx/Caddy/其他反向代理,请同时转发 /vision-router-settings/* 到 DSH。',
57
56
  remoteSafeScopeHint: '远程页面只允许修改低风险偏好;网络、凭据、本地后端、产物目录、桌面截图和宿主路由等设置只能在 DSH 本机修改。',
58
57
  settingsConflict: '设置已被另一端修改。已刷新最新配置,请确认你的修改后重新保存。',
59
- legacyMovedTitle: 'Vision Router 设置已迁移',
60
- legacyMovedBody: '主设置入口现在位于「设置 → Vision Router」。此处仅保留兼容入口,不再维护第二份可编辑表单。',
61
- legacyOpen: '打开 Vision Router 设置',
62
58
  overridden: '已覆盖',
63
59
  reset: '恢复默认',
64
60
  toggleInstantDescribe: '即时本地识图',
@@ -255,13 +251,14 @@ window.__ModuleLoader__.load({
255
251
  guidanceOverridePlaceholder: '输入自定义引导语(留空 = 使用内置)',
256
252
  addGuidanceOverride: '+ 添加一条自定义引导',
257
253
  selectKind: '选择图片类型…',
258
- visionDepthFast: '快速(最多再细看 1 次)',
259
- visionDepthStandard: '标准(细看 1-2 次,默认)',
260
- visionDepthDeep: '细致(细看 2-4 次)',
261
- visionDepthCustom: '自定义(自己填次数上限)',
262
- hintVisionDepth: '选择看图时的精细度:快速最省(最多再细看 1 次)、标准为默认(细看 1-2 次)、细致看得最细(细看 2-4 次)。档位只限制「再细看几次」,具体看什么由模型按你的问题决定。',
263
- labelVisionDepthMaxCalls: '自定义深挖次数上限',
264
- hintVisionDepthMaxCalls: '选择「自定义」后填写次数上限(1-100);留空或填 0 = 不限制深挖次数(视同标准档)。bootstrap 预识别那遍不计入,失败的调用也不占次数。',
254
+ visionDepthFast: '快速(优先整体判断)',
255
+ visionDepthStandard: '标准(按需查证,默认)',
256
+ visionDepthDeep: '细致(主动交叉验证)',
257
+ hintVisionDepth: '看图深度只决定识图策略,不限制调用次数:快速优先整体判断,标准按问题需要查证,细致会主动做更多局部检查与交叉验证。如需限制调用次数,请启用下方「限制深挖次数」。',
258
+ depthCapTitle: '限制深挖次数',
259
+ depthCapHint: '默认关闭,不限制视觉证据调用次数。启用后可设置本轮最多允许多少次成功的深挖证据调用;bootstrap 预识别不计入,失败或空证据调用不占次数。',
260
+ depthCapValueLabel: '最多深挖次数',
261
+ depthCapInvalid: '请输入 1–100 之间的整数。',
265
262
  numHintTimeoutMs: '单个视觉请求超时;默认 120000。',
266
263
  numHintVisionTaskTimeoutMs: '一次识图任务(含全部 provider、回退与重试)共享的总时限;默认 45000。认证失败/限流会立即熔断对应后端。',
267
264
  numHintOcrTimeoutMs: '一次 OCR 任务的总时限;本地 tesseract 最多用 12 秒,视觉模型回退只用剩余部分;默认 30000。',
@@ -332,9 +329,6 @@ window.__ModuleLoader__.load({
332
329
  remoteSettingsProxyBody: 'The remote-settings route returned 404. If DSH is behind Nginx, Caddy, or another reverse proxy, also forward /vision-router-settings/* to DSH.',
333
330
  remoteSafeScopeHint: 'Remote pages can change low-risk preferences only. Network, credentials, local backends, artifact paths, desktop capture, and host routing remain loopback-only.',
334
331
  settingsConflict: 'Settings changed on another client. The latest values were refreshed; review your draft and save again.',
335
- legacyMovedTitle: 'Vision Router settings moved',
336
- legacyMovedBody: 'The canonical editor is now Settings → Vision Router. This compatibility entry no longer keeps a second editable form.',
337
- legacyOpen: 'Open Vision Router settings',
338
332
  overridden: 'Overridden',
339
333
  reset: 'Reset',
340
334
  toggleInstantDescribe: 'Instant local image recognition',
@@ -534,13 +528,14 @@ window.__ModuleLoader__.load({
534
528
  guidanceOverridePlaceholder: 'Enter custom guidance (empty = built-in)',
535
529
  addGuidanceOverride: '+ Add a custom guidance line',
536
530
  selectKind: 'Select image type…',
537
- visionDepthFast: 'Quick (at most 1 more look)',
538
- visionDepthStandard: 'Standard (1-2 looks, default)',
539
- visionDepthDeep: 'Thorough (2-4 looks)',
540
- visionDepthCustom: 'Custom (set your own cap)',
541
- hintVisionDepth: 'How thoroughly to look after the structured pre-scan: Quick is cheapest (at most 1 more look), Standard is the default (1-2 looks), Thorough looks the closest (2-4 looks). The tier only caps how many extra looks are allowed; what the model looks for is driven by your question.',
542
- labelVisionDepthMaxCalls: 'Custom deep-dive call cap',
543
- hintVisionDepthMaxCalls: 'Pick "Custom" first, then enter a cap (1-100). Empty or 0 = no cap (same as Standard). The bootstrap pre-scan does not count, and failed calls do not consume the quota.',
531
+ visionDepthFast: 'Quick (overall-first)',
532
+ visionDepthStandard: 'Standard (evidence as needed, default)',
533
+ visionDepthDeep: 'Thorough (proactive cross-checking)',
534
+ hintVisionDepth: 'Vision depth chooses the inspection strategy, not a call-count limit: Quick stays overall-first, Standard verifies evidence as needed, and Thorough proactively inspects details and cross-checks important claims. To cap calls, enable “Limit deep-dive calls” below.',
535
+ depthCapTitle: 'Limit deep-dive calls',
536
+ depthCapHint: 'Off by default, so visual evidence calls are unlimited. Enable this to cap successful deep-evidence calls for the turn. The bootstrap pre-scan does not count, and failed or empty-evidence calls do not consume the cap.',
537
+ depthCapValueLabel: 'Maximum deep-dive calls',
538
+ depthCapInvalid: 'Enter an integer from 1 to 100.',
544
539
  numHintTimeoutMs: 'Per vision-call deadline; default 120000.',
545
540
  numHintVisionTaskTimeoutMs: 'One vision task (all providers, fallbacks and retries) shares this wall-clock budget; default 45000. Auth failures and rate limits trip their backend immediately.',
546
541
  numHintOcrTimeoutMs: 'Total budget for one OCR task: tesseract gets at most 12s, the vision fallback only the remainder; default 30000.',
@@ -1196,11 +1191,13 @@ window.__ModuleLoader__.load({
1196
1191
  if (provider === '' || model === '') return undefined
1197
1192
  return { provider, model }
1198
1193
  }
1199
- function parseNumber(text, min) {
1194
+ function parseNumber(text, min, max) {
1200
1195
  const trimmed = String(text ?? '').trim()
1201
1196
  if (trimmed === '') return { clear: true }
1202
1197
  const parsed = Number(trimmed)
1203
- return Number.isInteger(parsed) && parsed >= min ? { value: parsed } : undefined
1198
+ return Number.isInteger(parsed) && parsed >= min && (max === undefined || parsed <= max)
1199
+ ? { value: parsed }
1200
+ : undefined
1204
1201
  }
1205
1202
 
1206
1203
  // ── styles: mirrors the built-in plugin cards (same design tokens) ──────
@@ -1768,8 +1765,7 @@ window.__ModuleLoader__.load({
1768
1765
  return list.find(guideElementUsable)
1769
1766
  }
1770
1767
  // The walkthrough has one canonical destination: the first-class
1771
- // Vision Router settings section. The legacy plugin card remains mounted
1772
- // for compatibility, but onboarding never teaches two competing paths.
1768
+ // Vision Router settings section registered below.
1773
1769
  function guideVisionRouterNav() {
1774
1770
  const panel = guideSettingsPanel()
1775
1771
  if (!panel) return undefined
@@ -2758,7 +2754,8 @@ window.__ModuleLoader__.load({
2758
2754
  }
2759
2755
  if (key === 'localDescribeStyle') return value === 'structured' ? 'structured' : 'plain'
2760
2756
  if (SELECT_KEYS.includes(key)) {
2761
- return value === 'fast' || value === 'standard' || value === 'deep' || value === 'custom' ? value : 'standard'
2757
+ if (value === 'custom') return 'standard'
2758
+ return value === 'fast' || value === 'standard' || value === 'deep' ? value : 'standard'
2762
2759
  }
2763
2760
  return typeof value === 'string' ? value : ''
2764
2761
  }
@@ -2785,7 +2782,9 @@ window.__ModuleLoader__.load({
2785
2782
  const value = parseTextProvider(text)
2786
2783
  return value === undefined ? undefined : { value }
2787
2784
  }
2788
- if (NUMBER_KEYS.includes(key) || DEPTH_NUMBER_KEYS.includes(key)) return parseNumber(text, NUMBER_META[key].min)
2785
+ if (NUMBER_KEYS.includes(key) || DEPTH_NUMBER_KEYS.includes(key)) {
2786
+ return parseNumber(text, NUMBER_META[key].min, NUMBER_META[key].max)
2787
+ }
2789
2788
  if (key === 'guidanceOverrides') {
2790
2789
  const rows = Array.isArray(text) ? text : []
2791
2790
  const cleaned = []
@@ -2815,7 +2814,7 @@ window.__ModuleLoader__.load({
2815
2814
  return text === 'structured' || text === 'plain' ? { value: text } : undefined
2816
2815
  }
2817
2816
  if (SELECT_KEYS.includes(key)) {
2818
- return text === 'fast' || text === 'standard' || text === 'deep' || text === 'custom' ? { value: text } : undefined
2817
+ return text === 'fast' || text === 'standard' || text === 'deep' ? { value: text } : undefined
2819
2818
  }
2820
2819
  if (key === 'proxyHosts') {
2821
2820
  const list = String(text ?? '')
@@ -3136,6 +3135,7 @@ window.__ModuleLoader__.load({
3136
3135
  ),
3137
3136
  h('select', {
3138
3137
  className: 'vr-input vr-select' + (invalidField ? ' vr-input-invalid' : ''),
3138
+ 'data-vr-depth-strategy': key === 'visionDepth' ? '1' : undefined,
3139
3139
  value: format(key), disabled: editBlocked,
3140
3140
  onChange: (event) => setDraft(key, event.target.value),
3141
3141
  },
@@ -3148,6 +3148,44 @@ window.__ModuleLoader__.load({
3148
3148
  : null,
3149
3149
  )
3150
3150
  }
3151
+ const depthCapField = () => {
3152
+ const key = 'visionDepthMaxCalls'
3153
+ const raw = format(key)
3154
+ const parsed = Number(raw)
3155
+ const enabled = Number.isInteger(parsed) && parsed > 0
3156
+ const invalidField = key in drafts && parse(key, drafts[key]) === undefined
3157
+ const savedRaw = Number(readValue(snapshot, key))
3158
+ const savedCap = Number.isInteger(savedRaw) && savedRaw >= 1 && savedRaw <= 100 ? savedRaw : 4
3159
+ return h('div', { className: 'vr-field', key, 'data-vr-depth-cap': '1' },
3160
+ h('div', { className: 'vr-field-head' },
3161
+ h('div', { className: 'vr-toggle' },
3162
+ h('span', { className: 'vr-label' }, t('depthCapTitle')),
3163
+ h('input', {
3164
+ type: 'checkbox', className: 'vr-check', checked: enabled,
3165
+ 'data-vr-depth-cap-toggle': '1', disabled: editBlocked,
3166
+ onChange: (event) => setDraft(key, event.target.checked ? String(savedCap) : '0'),
3167
+ }),
3168
+ ),
3169
+ overriddenBadge(key),
3170
+ ),
3171
+ h('p', { className: 'vr-hint' }, t('depthCapHint')),
3172
+ enabled
3173
+ ? h('div', { className: 'vr-local-row' },
3174
+ h('label', { className: 'vr-label vr-local-label' }, t('depthCapValueLabel')),
3175
+ h('input', {
3176
+ className: 'vr-input' + (invalidField ? ' vr-input-invalid' : ''),
3177
+ type: 'number', min: 1, max: 100, step: 1, value: raw,
3178
+ 'data-vr-depth-cap-value': '1', disabled: editBlocked,
3179
+ onChange: (event) => setDraft(key, event.target.value),
3180
+ }),
3181
+ invalidField
3182
+ ? h('p', { className: 'vr-invalid' }, t('depthCapInvalid'))
3183
+ : null,
3184
+ )
3185
+ : null,
3186
+ )
3187
+ }
3188
+
3151
3189
  // 自定义识图引导编辑器:每行 [图片类型 select] [引导语 input] [移除],+ 添加。
3152
3190
  const GUIDANCE_KIND_OPTIONS = [
3153
3191
  { value: 'code', label: 'code(代码)' },
@@ -3948,14 +3986,11 @@ window.__ModuleLoader__.load({
3948
3986
  { value: 'fast', label: t('visionDepthFast') },
3949
3987
  { value: 'standard', label: t('visionDepthStandard') },
3950
3988
  { value: 'deep', label: t('visionDepthDeep') },
3951
- { value: 'custom', label: t('visionDepthCustom') },
3952
3989
  ])),
3953
- format('visionDepth') === 'custom'
3954
- ? DEPTH_NUMBER_KEYS.map((key) => textField(key, t(LABEL_KEY[key]), t(HINT_KEY[key]), false))
3955
- : null,
3956
3990
  guidanceOverridesEditor(),
3957
3991
  )
3958
3992
  : null,
3993
+ depthCapField(),
3959
3994
  // Local vision is a primary capability. Keep the heavy editors
3960
3995
  // collapsed by default, but make the entry obvious at a glance.
3961
3996
  !remoteMode ? h('div', {
@@ -4214,23 +4249,6 @@ window.__ModuleLoader__.load({
4214
4249
  )
4215
4250
  }
4216
4251
 
4217
- function VisionRouterLegacyEntry(props) {
4218
- const t = props.t
4219
- const h = React.createElement
4220
- return h('li', { className: 'vr-card vr-card-open' },
4221
- h('div', { className: 'vr-body' },
4222
- h('div', { className: 'vr-quickstart' },
4223
- h('div', { className: 'vr-quickstart-title' }, t('legacyMovedTitle')),
4224
- h('p', { className: 'vr-quickstart-body' }, t('legacyMovedBody')),
4225
- h('button', {
4226
- type: 'button', className: 'vr-btn vr-btn-save',
4227
- onClick: () => { guideHostUi.openVisionRouter() },
4228
- }, t('legacyOpen')),
4229
- ),
4230
- ),
4231
- )
4232
- }
4233
-
4234
4252
  function apply(ctx) {
4235
4253
  const localScope = ctx.settingsScope.bind({ namespace: 'vision-router' })
4236
4254
  const presentedImageUrls = new Map()
@@ -4365,11 +4383,9 @@ window.__ModuleLoader__.load({
4365
4383
  // this identity stays fixed across slot renders.
4366
4384
  const subscribeConnectionReset = (listener) => ctx.on('connection/reset', listener)
4367
4385
  const cardInject = { getConnection, t, locale: ctx.locale, remote: ctx.remote, subscribeConnectionReset }
4368
- // One form implementation, two presentation surfaces. Only the primary
4369
- // section switches to the opt-in remote scope on a non-loopback client;
4370
- // the legacy plugin card remains a compatibility shell over DSH's local scope.
4386
+ // Vision Router owns one canonical settings surface. The section switches
4387
+ // to the opt-in remote scope on a non-loopback client.
4371
4388
  const sectionCardInject = Object.freeze({ ...cardInject, scope: primaryScope, surface: 'section' })
4372
- const legacyEntryInject = Object.freeze({ t })
4373
4389
  const VisionRouterSettingsSection = (sectionProps) =>
4374
4390
  React.createElement(
4375
4391
  'ul',
@@ -4392,23 +4408,6 @@ window.__ModuleLoader__.load({
4392
4408
  }),
4393
4409
  'vision-router: primary settings section',
4394
4410
  )
4395
- ctx.effect(
4396
- () =>
4397
- ctx.slots.inject('settings.plugin.item', function* () {
4398
- yield ctx.slots.register(
4399
- {
4400
- name: 'settings.plugin.item',
4401
- key: 'vision-router',
4402
- id: 'vision-router',
4403
- order: 30,
4404
- label: () => t('nav'),
4405
- inject: () => legacyEntryInject,
4406
- },
4407
- VisionRouterLegacyEntry,
4408
- )
4409
- }),
4410
- 'vision-router: legacy plugin settings card',
4411
- )
4412
4411
  ctx.effect(
4413
4412
  () =>
4414
4413
  ctx.slots.inject('tool.call.toolview', function* () {