dsh-vision-router 2.2.7 → 2.3.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 (69) hide show
  1. package/README.md +8 -8
  2. package/README.zh.md +8 -8
  3. package/docs/architecture/2.3.0-compat-retirement.md +147 -0
  4. package/docs/architecture/2.3.0-execution-plan.md +410 -0
  5. package/docs/architecture/2.3.0-final-audit.md +279 -0
  6. package/docs/architecture/2.3.0-reopened-plan.md +720 -0
  7. package/docs/architecture/compat-inventory.md +26 -34
  8. package/docs/architecture/dsh-020-rc2-adaptation.md +23 -0
  9. package/docs/architecture/dsh-compatibility-matrix.md +65 -54
  10. package/docs/architecture/dsh-support-window.md +29 -21
  11. package/docs/architecture/final-closure-audit.md +2 -0
  12. package/docs/architecture/host-first-proxy-convergence.md +1 -1
  13. package/docs/releases/v2.2.8.md +25 -0
  14. package/docs/releases/v2.3.0.md +53 -0
  15. package/entry.js +8 -56
  16. package/index.js +161 -691
  17. package/lib/catalog-corrections.js +1 -2
  18. package/lib/client-host-compat-prelude.js +7 -12
  19. package/lib/client-maintenance-actions.js +145 -0
  20. package/lib/client-presentation-boundary-main.js +82 -32
  21. package/lib/client-presentation-boundary.js +60 -40
  22. package/lib/client-script-carrier.js +90 -0
  23. package/lib/client.js +248 -146
  24. package/lib/core-apply-lifecycle.js +47 -0
  25. package/lib/core-primitives.js +88 -135
  26. package/lib/desktop-screenshot-tool.js +262 -0
  27. package/lib/dsh-contract-compat.js +11 -26
  28. package/lib/dsh-support-window.js +4 -4
  29. package/lib/guide-vision-toggle-highlight.js +7 -10
  30. package/lib/http-compat.js +1 -2
  31. package/lib/live-model-client-prelude.js +7 -14
  32. package/lib/local-remote-settings-permission.js +41 -16
  33. package/lib/maintenance-routes.js +136 -0
  34. package/lib/public-config.js +57 -0
  35. package/lib/public-entry.js +2 -7
  36. package/lib/remote-settings-bridge.js +11 -0
  37. package/lib/remote-settings-risk-client-boundary.js +155 -0
  38. package/lib/remote-settings-risk-confirmation.js +17 -105
  39. package/lib/runtime-composition.js +105 -30
  40. package/lib/runtime-i18n-boundary.js +10 -11
  41. package/lib/session-message-source-compat.js +7 -3
  42. package/lib/session-vision-index.js +154 -9
  43. package/lib/session-vision-mode-boundary.js +43 -8
  44. package/lib/settings-client-loader-lifecycle.js +129 -0
  45. package/lib/settings-factory-lifecycle.js +25 -14
  46. package/lib/settings-ia-client-prelude.js +11 -11
  47. package/lib/settings-limit-client-prelude.js +7 -9
  48. package/lib/sharp-runtime.js +0 -7
  49. package/lib/strict-live-model-client-prelude.js +7 -10
  50. package/lib/structured-flow-hardening.js +3 -2
  51. package/lib/v2-settings-ia-integration.js +21 -6
  52. package/lib/vision-backend-runtime-policy.js +3 -2
  53. package/lib/vision-background-benchmark.js +3 -5
  54. package/lib/vision-capability-benchmark-client.js +7 -10
  55. package/lib/vision-capability-benchmark-service.js +1 -0
  56. package/lib/vision-diagnostics-routes.js +216 -0
  57. package/lib/vision-evidence-guidance.js +3 -1
  58. package/lib/vision-exact-check-client.js +11 -6
  59. package/lib/vision-model-visibility-boundary-main.js +7 -11
  60. package/lib/vision-model-visibility-boundary.js +7 -12
  61. package/lib/vision-provider-transport.js +0 -29
  62. package/lib/vision-routing-settings-prelude.js +7 -11
  63. package/lib/vision-toggle-root-hardening.js +50 -8
  64. package/lib/vision-tool-runtime-boundary.js +5 -1
  65. package/lib/vision-turn-budget-client-prelude.js +7 -10
  66. package/lib/web/remote-settings-client.js +9 -14
  67. package/package.json +23 -13
  68. package/lib/android-attachment-compat.js +0 -244
  69. package/lib/settings-client-rc8-lifecycle.js +0 -402
@@ -134,14 +134,26 @@ function disposeHeldRestriction(held) {
134
134
  }
135
135
  }
136
136
 
137
- function releaseAgentToolRestriction(agent, restrictions) {
137
+ function releaseAgentToolRestriction(agent, restrictions, activeRestrictions) {
138
138
  if (!isObject(agent)) return
139
139
  const held = restrictions.get(agent)
140
140
  if (!held) return
141
141
  disposeHeldRestriction(held)
142
+ activeRestrictions?.delete(held)
142
143
  restrictions.delete(agent)
143
144
  }
144
145
 
146
+ function rememberAgentToolRestriction(agent, restrictions, activeRestrictions, held) {
147
+ restrictions.set(agent, held)
148
+ activeRestrictions?.add(held)
149
+ }
150
+
151
+ function releaseAllAgentToolRestrictions(activeRestrictions) {
152
+ if (!(activeRestrictions instanceof Set)) return
153
+ for (const held of activeRestrictions) disposeHeldRestriction(held)
154
+ activeRestrictions.clear()
155
+ }
156
+
145
157
  function restrictionFailureLabel(error) {
146
158
  const name =
147
159
  typeof error?.name === 'string' && error.name !== ''
@@ -165,10 +177,10 @@ function warnRestrictionFailure(agent, error, deny, failed = deny) {
165
177
  } catch {}
166
178
  }
167
179
 
168
- function syncAgentToolRestriction(agent, enabled, restrictions, owned, registryTools) {
180
+ function syncAgentToolRestriction(agent, enabled, restrictions, owned, registryTools, activeRestrictions) {
169
181
  if (!isObject(agent)) return
170
182
  if (enabled) {
171
- releaseAgentToolRestriction(agent, restrictions)
183
+ releaseAgentToolRestriction(agent, restrictions, activeRestrictions)
172
184
  return
173
185
  }
174
186
 
@@ -181,19 +193,19 @@ function syncAgentToolRestriction(agent, enabled, restrictions, owned, registryT
181
193
  const deny = restrictableOwnedVisionToolNames(registryTools, owned)
182
194
  if (typeof restrict !== 'function') return
183
195
  if (deny.length === 0) {
184
- releaseAgentToolRestriction(agent, restrictions)
196
+ releaseAgentToolRestriction(agent, restrictions, activeRestrictions)
185
197
  return
186
198
  }
187
199
 
188
200
  const held = restrictions.get(agent)
189
201
  const signature = deny.join('\u0000')
190
202
  if (held?.signature === signature) return
191
- releaseAgentToolRestriction(agent, restrictions)
203
+ releaseAgentToolRestriction(agent, restrictions, activeRestrictions)
192
204
 
193
205
  try {
194
206
  // Current DSH can mask all inherited plugin tools in one scoped operation.
195
207
  const dispose = restrict.call(tools, { deny })
196
- restrictions.set(agent, {
208
+ rememberAgentToolRestriction(agent, restrictions, activeRestrictions, {
197
209
  signature,
198
210
  disposers: typeof dispose === 'function' ? [dispose] : [],
199
211
  })
@@ -212,7 +224,9 @@ function syncAgentToolRestriction(agent, enabled, restrictions, owned, registryT
212
224
  failed.push(name)
213
225
  }
214
226
  }
215
- if (disposers.length > 0) restrictions.set(agent, { signature, disposers })
227
+ if (disposers.length > 0) {
228
+ rememberAgentToolRestriction(agent, restrictions, activeRestrictions, { signature, disposers })
229
+ }
216
230
  if (failed.length > 0 || disposers.length === 0) {
217
231
  warnRestrictionFailure(agent, combinedError, deny, failed.length > 0 ? failed : deny)
218
232
  }
@@ -311,6 +325,7 @@ function wrapTools(tools, ctx, config, owned) {
311
325
  export function installSessionVisionModeBoundary(ctx, config = {}) {
312
326
  if (!isObject(ctx)) return { ctx, config }
313
327
  const restrictions = new WeakMap()
328
+ const activeRestrictions = new Set()
314
329
  const assemblySnapshots = new WeakMap()
315
330
  const ownedVisionTools = new Map()
316
331
  let toolsView
@@ -335,6 +350,7 @@ export function installSessionVisionModeBoundary(ctx, config = {}) {
335
350
  restrictions,
336
351
  ownedVisionTools,
337
352
  ctx?.tools,
353
+ activeRestrictions,
338
354
  )
339
355
  }
340
356
 
@@ -354,6 +370,7 @@ export function installSessionVisionModeBoundary(ctx, config = {}) {
354
370
  restrictions,
355
371
  ownedVisionTools,
356
372
  ctx?.tools,
373
+ activeRestrictions,
357
374
  )
358
375
  }
359
376
  return authority
@@ -419,13 +436,30 @@ export function installSessionVisionModeBoundary(ctx, config = {}) {
419
436
  })
420
437
  ctx.on?.('agent/disposed', ({ agent }) => {
421
438
  clearAgent(agent)
422
- releaseAgentToolRestriction(agent, restrictions)
439
+ releaseAgentToolRestriction(agent, restrictions, activeRestrictions)
423
440
  })
424
441
  } catch {
425
442
  // Minimum Hosts may not expose every lifecycle event. Prompt assembly and
426
443
  // execution guards preserve the safety boundary without making apply fail.
427
444
  }
428
445
 
446
+ // tools.restrict() is invoked through the long-lived Agent scope so its
447
+ // disposer belongs to that scope, not to this plugin fiber. A Settings/HMR
448
+ // recompose can therefore retire this boundary while the Agent survives. If
449
+ // we do not release those masks here, the next boundary generation cannot
450
+ // discover or dispose the old WeakMap entries and Vision tools stay hidden
451
+ // even after the Session selects a Router-owned route again.
452
+ try {
453
+ if (typeof ctx.effect === 'function') {
454
+ ctx.effect(
455
+ () => () => releaseAllAgentToolRestrictions(activeRestrictions),
456
+ 'vision-router: Session Vision restriction lifecycle',
457
+ )
458
+ }
459
+ } catch {
460
+ // Agent disposal / live enable transitions still release their own masks.
461
+ }
462
+
429
463
  const wrapped = new Proxy(ctx, {
430
464
  get(target, property) {
431
465
  if (property === 'tools') {
@@ -467,6 +501,7 @@ export function installSessionVisionModeBoundary(ctx, config = {}) {
467
501
  restrictions,
468
502
  ownedVisionTools,
469
503
  target?.tools,
504
+ activeRestrictions,
470
505
  )
471
506
  return runWithSessionVisionModeAuthority(
472
507
  authority,
@@ -0,0 +1,129 @@
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
2
+ import {
3
+ LOCAL_PERMISSION_CLIENT_BOUNDARY_SOURCE,
4
+ LOCAL_REMOTE_SETTINGS_PERMISSION_FIELD,
5
+ LOCAL_REMOTE_SETTINGS_PERMISSION_PATH,
6
+ } from './local-remote-settings-permission.js'
7
+ import { REMOTE_SETTINGS_RISK_CLIENT_BOUNDARY_SOURCE } from './remote-settings-risk-client-boundary.js'
8
+
9
+ // Keep the legacy marker value so mixed-generation/HMR pages do not install
10
+ // a second lifecycle wrapper after the source/API rename.
11
+ const SETTINGS_CLIENT_LOADER_LIFECYCLE_MARK = 'data-vision-router-settings-rc8-lifecycle'
12
+
13
+ /**
14
+ * Supported Hosts can replace ModuleLoader.load() inside loader.create(). This
15
+ * compatibility owner only re-attaches the current local-permission and
16
+ * remote-risk product boundaries after that queue-to-live transition; product
17
+ * semantics stay in their capability owners.
18
+ */
19
+ export const SETTINGS_CLIENT_LOADER_LIFECYCLE_PRELUDE = String.raw`(function(){
20
+ 'use strict';
21
+ var TARGET = 'dsh-vision-router';
22
+ ${LOCAL_PERMISSION_CLIENT_BOUNDARY_SOURCE}
23
+ var localPermissionBoundary = createLocalRemoteSettingsPermissionClientBoundary({
24
+ endpoint: '${LOCAL_REMOTE_SETTINGS_PERMISSION_PATH}',
25
+ field: '${LOCAL_REMOTE_SETTINGS_PERMISSION_FIELD}',
26
+ fetchImpl: typeof fetch === 'function'
27
+ ? function(url, options) { return fetch(url, options); }
28
+ : undefined
29
+ });
30
+
31
+ ${REMOTE_SETTINGS_RISK_CLIENT_BOUNDARY_SOURCE}
32
+ var riskBoundary = createRemoteSettingsRiskClientBoundary({
33
+ confirmImpl: function(message) {
34
+ return typeof window.confirm === 'function' && window.confirm(message) === true;
35
+ },
36
+ alertImpl: function(message) {
37
+ try { if (typeof window.alert === 'function') window.alert(message); } catch (_) {}
38
+ },
39
+ locale: function() {
40
+ try {
41
+ return String((document && document.documentElement && document.documentElement.lang)
42
+ || (navigator && navigator.language) || '');
43
+ } catch (_) { return ''; }
44
+ },
45
+ location: function() {
46
+ try { return window && window.location; } catch (_) { return undefined; }
47
+ },
48
+ normalizeLoopback: true
49
+ });
50
+
51
+ function patchLiveLoader(loader) {
52
+ if (!loader || typeof loader.load !== 'function' || loader.load.__visionRouterSettingsRc8Lifecycle) return;
53
+ var original = loader.load;
54
+ function load(spec) {
55
+ if (spec && spec.id === TARGET && typeof spec.factory === 'function') {
56
+ var factory = spec.factory;
57
+ spec = Object.assign({}, spec, {
58
+ factory: function(require) {
59
+ var exports = factory(require);
60
+ if (exports && typeof exports.apply === 'function' && !exports.apply.__visionRouterSettingsRc8Lifecycle) {
61
+ var apply = exports.apply;
62
+ var wrappedApply = function(ctx) {
63
+ var rest = Array.prototype.slice.call(arguments, 1);
64
+ var nextCtx = riskBoundary.wrapContext(localPermissionBoundary.wrapContext(ctx));
65
+ return apply.apply(exports, [nextCtx].concat(rest));
66
+ };
67
+ Object.defineProperty(wrappedApply, '__visionRouterSettingsRc8Lifecycle', { value: true });
68
+ exports.apply = wrappedApply;
69
+ }
70
+ return exports;
71
+ }
72
+ });
73
+ }
74
+ return original.call(this, spec);
75
+ }
76
+ Object.defineProperty(load, '__visionRouterSettingsRc8Lifecycle', { value: true });
77
+ loader.load = load;
78
+ }
79
+
80
+ function patchCreate(loader) {
81
+ if (!loader || typeof loader.create !== 'function' || loader.create.__visionRouterSettingsRc8Lifecycle) return;
82
+ var originalCreate = loader.create;
83
+ function create() {
84
+ var result = originalCreate.apply(this, arguments);
85
+ patchLiveLoader(loader);
86
+ return result;
87
+ }
88
+ Object.defineProperty(create, '__visionRouterSettingsRc8Lifecycle', { value: true });
89
+ loader.create = create;
90
+ if (loader.mode === 'live') patchLiveLoader(loader);
91
+ }
92
+
93
+ function install() {
94
+ if (window.__ModuleLoader__) {
95
+ patchCreate(window.__ModuleLoader__);
96
+ return;
97
+ }
98
+ var descriptor = Object.getOwnPropertyDescriptor(window, '__ModuleLoader__');
99
+ if (descriptor && descriptor.configurable === false) return;
100
+ var previousGet = descriptor && descriptor.get;
101
+ var previousSet = descriptor && descriptor.set;
102
+ var stored = descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value') ? descriptor.value : undefined;
103
+ Object.defineProperty(window, '__ModuleLoader__', {
104
+ configurable: true,
105
+ enumerable: !descriptor || descriptor.enumerable !== false,
106
+ get: function(){ return previousGet ? previousGet.call(window) : stored; },
107
+ set: function(value) {
108
+ if (previousSet) previousSet.call(window, value); else stored = value;
109
+ try { patchCreate(previousGet ? previousGet.call(window) : value); } catch (_) {}
110
+ }
111
+ });
112
+ if (stored) patchCreate(stored);
113
+ }
114
+
115
+ try { install(); } catch (_) {}
116
+ })();`
117
+
118
+ export function injectSettingsClientLoaderLifecyclePrelude(html) {
119
+ return injectClientCarrierHtml(html, SETTINGS_CLIENT_LOADER_LIFECYCLE_MARK, SETTINGS_CLIENT_LOADER_LIFECYCLE_PRELUDE)
120
+ }
121
+
122
+ export function installSettingsClientLoaderLifecycle(ctx) {
123
+ installClientScriptCarriers(ctx, {
124
+ marker: SETTINGS_CLIENT_LOADER_LIFECYCLE_MARK,
125
+ prelude: SETTINGS_CLIENT_LOADER_LIFECYCLE_PRELUDE,
126
+ label: 'vision-router: settings client loader lifecycle',
127
+ injectHtml: injectSettingsClientLoaderLifecyclePrelude,
128
+ })
129
+ }
@@ -8,20 +8,20 @@ import {
8
8
  } from './settings-limit-client-prelude.js'
9
9
  import {
10
10
  SETTINGS_NATIVE_CARD_IA_PRELUDE,
11
+ SETTINGS_NATIVE_CARD_STYLE,
11
12
  injectSettingsNativeCardStyle,
12
13
  } from './settings-native-card-layout.js'
14
+ import { appendStructuredClientCarrier, injectClientCarrierHtml } from './client-script-carrier.js'
13
15
 
14
16
  const CLIENT_MARK = 'data-vision-router-settings-factory-lifecycle'
15
17
  const IA_SCRIPT_MARK = 'data-vision-router-settings-ia'
16
18
  const LIMIT_SCRIPT_MARK = 'data-vision-router-settings-limit-hardening'
17
19
 
18
- function scriptMarkup(mark, source) {
19
- const safe = source.replace(/<\/script/gi, '<\\/script')
20
- return `<script ${mark}>${safe}</script>`
20
+ function removeScriptMarker(source, marker) {
21
+ const pattern = new RegExp(`<script\\b[^>]*\\b${marker}(?:\\s|=|>|/)[^>]*>[\\s\\S]*?<\\/script\\s*>`, 'gi')
22
+ return source.replace(pattern, '')
21
23
  }
22
24
 
23
- const IA_SCRIPT = scriptMarkup(IA_SCRIPT_MARK, SETTINGS_IA_CLIENT_PRELUDE)
24
- const LIMIT_SCRIPT = scriptMarkup(LIMIT_SCRIPT_MARK, SETTINGS_LIMIT_CLIENT_PRELUDE)
25
25
 
26
26
  /**
27
27
  * One lifecycle owner for every decorator that shapes the Vision Router
@@ -123,19 +123,30 @@ export function injectSettingsFactoryLifecycle(html) {
123
123
  let next = injectSettingsLimitClientPrelude(html)
124
124
  next = injectSettingsIaClientPrelude(next)
125
125
  next = injectSettingsNativeCardStyle(next)
126
- next = next.split(LIMIT_SCRIPT).join('')
127
- next = next.split(IA_SCRIPT).join('')
128
-
129
- const safe = SETTINGS_FACTORY_LIFECYCLE_PRELUDE.replace(/<\/script/gi, '<\\/script')
130
- const script = `<script ${CLIENT_MARK}>${safe}</script>`
131
- const closeHead = next.indexOf('</head>')
132
- return closeHead === -1
133
- ? `${next}${script}`
134
- : `${next.slice(0, closeHead)}${script}${next.slice(closeHead)}`
126
+ next = removeScriptMarker(next, LIMIT_SCRIPT_MARK)
127
+ next = removeScriptMarker(next, IA_SCRIPT_MARK)
128
+ return injectClientCarrierHtml(next, CLIENT_MARK, SETTINGS_FACTORY_LIFECYCLE_PRELUDE)
135
129
  }
136
130
 
137
131
  export function installSettingsFactoryLifecycle(ctx) {
138
132
  ctx?.inject?.(['webServer'], (webCtx) => {
133
+ if (typeof webCtx.on === 'function') {
134
+ webCtx.on('webserver/index-inject', (table) => {
135
+ if (!Array.isArray(table)) return
136
+ const historical = [`/* ${IA_SCRIPT_MARK} */`, `/* ${LIMIT_SCRIPT_MARK} */`]
137
+ for (let index = table.length - 1; index >= 0; index -= 1) {
138
+ const row = table[index]
139
+ if (row?.kind === 'script' && typeof row.text === 'string' && historical.some((mark) => row.text.includes(mark))) {
140
+ table.splice(index, 1)
141
+ }
142
+ }
143
+ appendStructuredClientCarrier(table, CLIENT_MARK, SETTINGS_FACTORY_LIFECYCLE_PRELUDE)
144
+ const styleMark = 'data-vision-router-settings-native-cards'
145
+ if (!table.some((row) => row?.kind === 'style' && typeof row.text === 'string' && row.text.includes(styleMark))) {
146
+ table.push({ kind: 'style', placement: 'head', text: `/* ${styleMark} */\n${SETTINGS_NATIVE_CARD_STYLE}` })
147
+ }
148
+ })
149
+ }
139
150
  webCtx.effect(
140
151
  () => webCtx.webServer.tapIndex(injectSettingsFactoryLifecycle),
141
152
  'vision-router: converged settings client factory lifecycle',
@@ -1,3 +1,4 @@
1
+ import { injectClientCarrierHtml, injectClientStyleHtml, installClientScriptCarriers } from './client-script-carrier.js'
1
2
  const CLIENT_MARK = 'data-vision-router-settings-ia'
2
3
 
3
4
  export const SETTINGS_IA_CLIENT_PRELUDE = String.raw`(function(){
@@ -180,7 +181,7 @@ export const SETTINGS_IA_CLIENT_PRELUDE = String.raw`(function(){
180
181
  function modelOptions(provider,current,allowAll){var group=groupById.get(provider),models=arr(group&&group.models),listed=new Set(models.map(function(model){return model&&model.id;}).filter(Boolean)),nodes=[h('option',{value:'',key:'__empty'},allowAll?tx('全部模型','All models'):(provider?tx('选择模型…','Choose model…'):tx('先选供应商','Choose provider first')))];if(current&&!listed.has(current))nodes.push(h('option',{value:current,key:'__stale-'+current},current+' '+tx('(已保存)','(saved)')));models.forEach(function(model){if(model&&typeof model.id==='string'&&model.id)nodes.push(h('option',{value:model.id,key:model.id},modelName(group,model.id)));});return nodes;}
181
182
  function modelRow(row,index,rows,setRows,allowAll){var ready=groups.length>0;function update(next){var copy=rows.map(function(item){return Object.assign({},item);});copy[index]=next;setRows(copy);setSaveState({status:'idle'});}function providerChanged(provider){var next=Object.assign({},row,{provider:provider,model:''});if(!allowAll)next.fallbacks=[];update(next);}function remove(){var copy=rows.filter(function(_,i){return i!==index;});setRows(copy.length?copy:[{provider:'',model:'',fallbacks:[]}]);setSaveState({status:'idle'});}return h('div',{className:'vr-chain-row',key:index},ready?h('select',{className:'vr-input vr-select',value:row.provider||'',disabled:!writable||saving,onChange:function(event){providerChanged(event.target.value);}},providerOptions(row.provider||'')):h('input',{className:'vr-input',placeholder:'provider',value:row.provider||'',disabled:!writable||saving,onChange:function(event){providerChanged(event.target.value);}}),ready?h('select',{className:'vr-input vr-select',value:row.model||'',disabled:!writable||saving||!row.provider,onChange:function(event){update(Object.assign({},row,{model:event.target.value}));}},modelOptions(row.provider||'',row.model||'',allowAll)):h('input',{className:'vr-input',placeholder:allowAll?tx('model(留空=全部)','model (empty = all)'):'model',value:row.model||'',disabled:!writable||saving,onChange:function(event){update(Object.assign({},row,{model:event.target.value}));}}),h('button',{type:'button',className:'vr-reset',disabled:!writable||saving,onClick:remove},tx('移除','Remove')));}
182
183
 
183
- function generalPage(){var rows=chainRows(),configured=rows.filter(function(row){return row&&row.provider&&row.model;}),localOnly=toggleValue('localOnlyVision',false),fallback=!localOnly&&toggleValue('freeFallback',true),status=localOnly?tx('仅本地视觉策略已启用','Local-only vision policy enabled'):configured.length?tx('识图已就绪','Vision ready'):fallback?tx('已可使用内置免费识图','Built-in free vision is ready'):tx('暂无可用识图模型','No vision model configured'),ordered=configured.map(function(row){return row.model;});if(fallback)ordered.push(tx('内置免费兜底','Built-in free fallback'));var stale=catalog.status==='ready'?configured.filter(function(row){var group=groupById.get(row.provider);return !group||!arr(group.models).some(function(model){return model&&model.id===row.model;});}):[];return h(React.Fragment,null,title(tx('常规','General'),tx('这里只设置“谁负责看图”。聊天模型仍在聊天页选择。','This page only decides who reads images; the chat model is still chosen in chat.')),card([toggle('localOnlyVision',tx('仅本地视觉','Local-only vision'),tx('开启后,Vision Router 只允许回环地址上的本地视觉后端(Ollama、LM Studio 或本机自定义 HTTP);所有云端/DSH Provider 和 OVH 免费兜底都会在运行时被硬阻止。已保存的云端配置不会删除,关闭后可恢复。主聊天模型是否联网不受此开关控制。此隐私开关只能在运行 DSH 的本机页面修改。','When enabled, Vision Router only allows visual backends on loopback addresses (Ollama, LM Studio, or custom local HTTP). Cloud/DSH providers and the OVH free fallback are blocked at runtime. Saved cloud settings are preserved for later restoration. This does not control whether the main chat model is remote. This privacy control can only be changed from the local DSH settings page.'),!local),h('div',{className:'vr-ia-status',key:'status'},h('strong',null,(localOnly?'🔒 ':configured.length||fallback?'● ':'⚠ ')+status),h('p',{className:'vr-hint'},tx('需要看图时,在聊天框旁点击「👁 识图」。','Use “👁 Vision” beside the chat box when an image needs inspection.')),h('p',{className:localOnly?'vr-hint':'vr-failed',key:'egress'},localOnly?tx('当前仅本地视觉策略正在生效:云端识图行和 OVH 免费兜底会保留配置但不会执行;只有回环地址上的本地视觉端点可接收图片。注意:当前聊天模型仍可能是云端模型。','Local-only vision is active: cloud vision rows and the OVH fallback remain saved but cannot execute; only visual endpoints on loopback addresses may receive images. The current chat model may still be remote.'):tx('数据流向:使用云端识图模型(包括默认 OVHcloud 匿名兜底)时,图片/裁剪、提示词和相关上下文会离开本机;免费或免 Key 不等于离线。若要阻止 Vision Router 将图片发送到远程视觉端点,请开启上方“仅本地视觉”。','Data flow: cloud vision models, including the default anonymous OVHcloud fallback, receive images/crops, prompts and related context; free or keyless does not mean offline. Enable Local-only vision above to prevent Vision Router from sending image data to remote visual endpoints.')),ordered.length?h('p',{className:'vr-ia-chain-summary'},ordered.join(' → ')):null,stale.length?h('p',{className:'vr-failed'},tx('有已保存的识图模型当前不在可调用目录中;请到“诊断”重新检测。','A saved vision model is not currently in the callable catalog; re-detect it in Diagnostics.')):null)]),card([h('div',{className:'vr-field',id:'vr-vision-backend-chain','data-vr-guide-target':'vision-backend',key:'chain'},fieldHead('providers',tx('识图模型','Vision models')),h('p',{className:'vr-hint'},tx('从上到下依次尝试;前一个失败时自动切换到下一个。','Models are tried from top to bottom and fall through automatically on failure.')),rows.map(function(row,index){return modelRow(row,index,rows,setChainDraft,false);}),h('button',{type:'button',className:'vr-btn',disabled:!writable||saving,onClick:function(){setChainDraft(rows.concat([{provider:'',model:'',fallbacks:[]}]))}},tx('+ 添加备用识图模型','+ Add fallback vision model'))),toggle('freeFallback',tx('内置免费兜底','Built-in free fallback'),localOnly?tx('仅本地视觉开启时,OVH 免费兜底由运行时强制禁用;这里保留原设置,关闭仅本地视觉后可恢复。','The OVH fallback is runtime-blocked while Local-only vision is enabled. Its saved value is preserved and becomes active again when Local-only vision is turned off.'):tx('这是远程 OVHcloud 服务;前面的模型都失败后会上传图片并自动尝试,无需 API Key。','This is a remote OVHcloud service. After earlier models fail, it uploads the image and tries automatically; no API key is required.'),localOnly)]));}
184
+ function generalPage(){var rows=chainRows(),configured=rows.filter(function(row){return row&&row.provider&&row.model;}),localOnly=toggleValue('localOnlyVision',false),fallback=!localOnly&&toggleValue('freeFallback',true),status=localOnly?tx('仅本地视觉策略已启用','Local-only vision policy enabled'):configured.length?tx('识图已就绪','Vision ready'):fallback?tx('已可使用内置免费识图','Built-in free vision is ready'):tx('暂无可用识图模型','No vision model configured'),ordered=configured.map(function(row){return row.model;});if(fallback)ordered.push(tx('内置免费兜底','Built-in free fallback'));var stale=catalog.status==='ready'?configured.filter(function(row){var group=groupById.get(row.provider);return !group||!arr(group.models).some(function(model){return model&&model.id===row.model;});}):[];return h(React.Fragment,null,title(tx('常规','General'),tx('这里只设置“谁负责看图”。聊天模型仍在聊天页选择。','This page only decides who reads images; the chat model is still chosen in chat.')),card([toggle('localOnlyVision',tx('仅本地视觉','Local-only vision'),tx('开启后,Vision Router 只允许回环地址上的本地视觉后端(Ollama、LM Studio 或本机自定义 HTTP);所有云端/DSH Provider 和 OVH 免费兜底都会在运行时被硬阻止。已保存的云端配置不会删除,关闭后可恢复。Vision Router 不会把图片和裁剪发送到云端视觉服务,但视觉后端返回的描述、OCR、坐标等识图结果文本仍会交给当前聊天模型;若聊天模型在云端,图片语义仍可能离开本机。严格纯本地还需同时选择本地聊天模型。此隐私开关只能在运行 DSH 的本机页面修改。','When enabled, Vision Router only allows visual backends on loopback addresses (Ollama, LM Studio, or custom local HTTP). Cloud/DSH providers and the OVH free fallback are blocked at runtime. Saved cloud settings are preserved for later restoration. Vision Router does not send image bytes or crops to cloud vision services, but derived result text—descriptions, OCR, and coordinates—is still passed to the current chat model; if that model is remote, image semantics may still leave this machine. For a strict end-to-end local workflow, select a local chat model too. This privacy control can only be changed from the local DSH settings page.'),!local),h('div',{className:'vr-ia-status',key:'status'},h('strong',null,(localOnly?'🔒 ':configured.length||fallback?'● ':'⚠ ')+status),h('p',{className:'vr-hint'},tx('需要看图时,在聊天框旁点击「👁 识图」。','Use “👁 Vision” beside the chat box when an image needs inspection.')),h('p',{className:localOnly?'vr-hint':'vr-failed',key:'egress'},localOnly?tx('当前仅本地视觉策略正在生效:云端识图行和 OVH 免费兜底会保留配置但不会执行;Vision Router 只会把图片和裁剪发往回环地址上的本地视觉端点。识图结果文本仍会交给当前聊天模型;若聊天模型在云端,图片语义仍可能离开本机。严格纯本地还需同时选择本地聊天模型。','Local-only vision is active: cloud vision rows and the OVH fallback remain saved but cannot execute; Vision Router sends image bytes and crops only to local visual endpoints on loopback addresses. Derived result text is still passed to the current chat model; if that model is remote, image semantics may still leave this machine. For a strict end-to-end local workflow, select a local chat model too.'):tx('数据流向:使用云端识图模型(包括默认 OVHcloud 匿名兜底)时,图片/裁剪、提示词和相关上下文会离开本机;免费或免 Key 不等于离线。若要阻止 Vision Router 将图片发送到远程视觉端点,请开启上方“仅本地视觉”。','Data flow: cloud vision models, including the default anonymous OVHcloud fallback, receive images/crops, prompts and related context; free or keyless does not mean offline. Enable Local-only vision above to prevent Vision Router from sending image data to remote visual endpoints.')),ordered.length?h('p',{className:'vr-ia-chain-summary'},ordered.join(' → ')):null,stale.length?h('p',{className:'vr-failed'},tx('有已保存的识图模型当前不在可调用目录中;请到“诊断”重新检测。','A saved vision model is not currently in the callable catalog; re-detect it in Diagnostics.')):null)]),card([h('div',{className:'vr-field',id:'vr-vision-backend-chain','data-vr-guide-target':'vision-backend',key:'chain'},fieldHead('providers',tx('识图模型','Vision models')),h('p',{className:'vr-hint'},tx('从上到下依次尝试;前一个失败时自动切换到下一个。','Models are tried from top to bottom and fall through automatically on failure.')),rows.map(function(row,index){return modelRow(row,index,rows,setChainDraft,false);}),h('button',{type:'button',className:'vr-btn',disabled:!writable||saving,onClick:function(){setChainDraft(rows.concat([{provider:'',model:'',fallbacks:[]}]))}},tx('+ 添加备用识图模型','+ Add fallback vision model'))),toggle('freeFallback',tx('内置免费兜底','Built-in free fallback'),localOnly?tx('仅本地视觉开启时,OVH 免费兜底由运行时强制禁用;这里保留原设置,关闭仅本地视觉后可恢复。','The OVH fallback is runtime-blocked while Local-only vision is enabled. Its saved value is preserved and becomes active again when Local-only vision is turned off.'):tx('这是远程 OVHcloud 服务;前面的模型都失败后会上传图片并自动尝试,无需 API Key。','This is a remote OVHcloud service. After earlier models fail, it uploads the image and tries automatically; no API key is required.'),localOnly)]));}
184
185
  function strategyPage(){
185
186
  var structured=toggleValue('structuredVisionBootstrap',false);
186
187
  var rawDepth=String(value('visionDepth','standard')||'standard');
@@ -261,7 +262,7 @@ export const SETTINGS_IA_CLIENT_PRELUDE = String.raw`(function(){
261
262
  function advancedPage(){var routing=toggleValue('routing',false),turnBudget=Number(value('visionTurnBudgetMs',0))||0,customBudget=turnBudget>0;return h(React.Fragment,null,title(tx('高级','Advanced'),tx('这些默认值对大多数用户已经合适。','Defaults are suitable for most users.')),card([h('h4',{className:'vr-ia-subtitle',key:'p'},tx('性能与稳定性','Performance & stability')),toggle('downscale',tx('自动缩放','Auto downscale')),numberField('downscaleMaxPixels',tx('图片像素上限','Image pixel limit'),null,1000,100000000),toggle('cache',tx('识图缓存','Vision answer cache')),numberField('cacheTtlSeconds',tx('缓存有效期(秒)','Cache TTL (seconds)'),null,0,31536000),numberField('cacheMaxEntries',tx('最大缓存数量','Maximum cached answers'),null,1,100000),h('h4',{className:'vr-ia-subtitle',key:'t'},tx('超时','Timeouts')),numberField('timeoutMs',tx('单次模型请求','Single model request'),tx('毫秒。','Milliseconds.'),1000,600000),numberField('visionTaskTimeoutMs',tx('单个视觉任务','Single visual task'),tx('包含该任务内部的重试和备用模型;不是每个后端各自一份。','Includes retries and fallbacks inside the task; it is not a fresh budget per backend.'),1000,180000),numberField('ocrTimeoutMs',tx('OCR 任务','OCR task'),null,1000,120000),h('div',{className:'vr-field',key:'ocr-engine'},fieldHead('ocrEngine',tx('OCR 默认引擎','Default OCR engine')),h('select',{className:'vr-input',value:String(value('ocrEngine','auto')||'auto'),disabled:!writable||saving,onChange:function(event){setValue('ocrEngine',event.target.value);}},h('option',{value:'auto'},tx('自动(Tesseract → 视觉模型)','Auto (Tesseract → vision)')),h('option',{value:'tesseract'},tx('仅 Tesseract','Tesseract only')),h('option',{value:'vision'},tx('仅视觉模型','Vision model only'))),h('p',{className:'vr-hint'},tx('vision_ocr 未显式指定 engine 时使用;单次调用的 engine=tesseract/vision 始终优先。','Used when vision_ocr does not explicitly choose an engine; per-call engine=tesseract/vision always wins.'))),h('div',{className:'vr-field',key:'budget'},fieldHead('visionTurnBudgetMs',tx('整轮视觉工具上限','Whole-turn vision-tool limit')),h('select',{className:'vr-input',value:customBudget?'custom':'unlimited',disabled:!writable||saving,onChange:function(event){setValue('visionTurnBudgetMs',event.target.value==='unlimited'?0:(turnBudget>0?turnBudget:180000));}},h('option',{value:'unlimited'},tx('不限制(推荐)','Unlimited (recommended)')),h('option',{value:'custom'},tx('自定义','Custom'))),customBudget?numberField('visionTurnBudgetMs',tx('上限(毫秒)','Limit (ms)'),null,10000,600000):null)]),card([h('h4',{className:'vr-ia-subtitle',key:'cost'},tx('模型顺序与成本','Model order & cost')),toggle('freeCloudFirst',tx('免费云模型优先','Try free cloud models first'))]),card([h('h4',{className:'vr-ia-subtitle',key:'scope'},tx('识图模式范围','Vision mode scope')),toggle('autoWrapProviders',tx('自动允许已启用模型使用识图','Automatically allow enabled models to use Vision mode')),wrappersEditor()]),local?card([h('h4',{className:'vr-ia-subtitle',key:'network'},tx('网络与远程','Network & remote')),h('p',{className:'vr-hint',key:'network-authority'},tx('默认沿用 DSH/Host 的网络出口;只有需要让视觉请求走不同出口时才填写下方覆盖。','By default Vision Router follows the DSH/Host network path. Use the override below only when vision requests need a different egress route.')),toggle('allowRemoteSettings',tx('允许可信 Host 远程修改设置','Allow trusted-host remote settings'),tx('默认关闭;trustedHosts 不是身份认证。','Off by default; trustedHosts is not authentication.')),textField('proxy',tx('视觉专用代理覆盖','Vision-only proxy override'),tx('留空即沿用 DSH/Host;支持 http(s)://、socks5://,并兼容旧 socks5h://。','Leave empty to follow DSH/Host; supports http(s):// and socks5://, with legacy socks5h:// compatibility.')),textareaArray('proxyHosts',tx('视觉代理覆盖域名','Vision proxy override hosts'),tx('仅在上方覆盖非空时生效;每行一个,其余请求继续沿用 DSH/Host。','Used only when the override above is non-empty; one host per line. All other requests keep the DSH/Host path.'))]):null,card([h('h4',{className:'vr-ia-subtitle',key:'compat'},tx('兼容模式','Compatibility')),toggle('rewriteImages',tx('保护纯文本模型','Protect text-only models')),toggle('routing',tx('整轮视觉路由(旧工作流)','Whole-turn vision routing (legacy workflow)')),routing?toggle('reverseRouting',tx('纯文字消息继续使用聊天模型','Keep text-only messages on the chat model')):null,routing?textProviderEditor():null]),card([h('button',{type:'button',className:'vr-btn',key:'dev',onClick:function(){setOpen(Object.assign({},open,{developer:!open.developer}));}},open.developer?tx('隐藏开发者设置','Hide developer settings'):tx('显示开发者设置','Show developer settings')),open.developer?h('div',{className:'vr-ia-dev',key:'body'},toggle('progressiveTools',tx('渐进式工具暴露','Progressive tool exposure')),h('p',{className:'vr-hint',key:'progressive-restart'},tx('保存后需重启 DSH 才生效。','Restart DSH after saving for this change to take effect.')),local?toggle('stealth','Stealth'):null,local?textField('wrapperRoute',tx('包装路由名','Wrapper route name')):null,local?textField('chainRoute',tx('视觉链路由名','Vision chain route name')):null,textareaArray('extraVisionModels',tx('额外视觉能力标记','Extra vision capability labels'),tx('只有诊断发现模型未声明图片能力时才需要。','Needed only when diagnostics show missing image-capability metadata.'))):null]));}
262
263
  function diagnosticValue(label,valueText){return h('div',{className:'vr-ia-diag-row',key:label},h('span',null,label),h('strong',null,valueText));}
263
264
  function updatePanel(){if(!local)return null;var result=updateState.result,auto=result&&result.autoUpdate,current=result&&result.currentVersion?result.currentVersion:tx('检测中','Checking'),latest=result&&result.latestVersion?result.latestVersion:'—',available=result&&result.ok===true&&result.updateAvailable===true,profile=auto&&auto.profile?auto.profile:'web',manualVersion=result&&result.latestVersion?result.latestVersion:'<version>',spec='dsh-vision-router@'+manualVersion,pnpm='pnpm dsh plugin --profile '+profile+' add '+spec,npx='npx @deepseek-ai/dsh plugin --profile '+profile+' add '+spec;return card([h('h4',{className:'vr-ia-subtitle',key:'title'},tx('版本更新','Updates')),h('p',{className:'vr-hint',key:'status'},updateState.status==='running'?tx('正在检查更新…','Checking for updates…'):result&&result.ok===false?tx('更新检查失败:','Update check failed: ')+String(result.error||'unknown'):available?tx('发现新版本:v','Update available: v')+latest+tx('(当前 v',' (current v')+current+')':result&&result.ok===true?tx('已是最新版本 v','Up to date: v')+current:tx('尚未检查','Not checked yet')),h('div',{className:'vr-ia-actions',key:'actions'},h('button',{type:'button',className:'vr-btn',disabled:updateState.status==='running',onClick:function(){void runUpdateCheck(true);}},tx('检查更新','Check for updates')),available&&auto&&auto.supported===true&&auto.token?h('button',{type:'button',className:'vr-btn vr-btn-save',disabled:selfUpdateState.status==='running',onClick:function(){void runSelfUpdate();}},selfUpdateState.status==='running'?tx('更新中…','Updating…'):tx('一键更新','Update now')):null),selfUpdateState.status==='done'&&selfUpdateState.result?h('p',{className:'vr-hint',key:'updated'},tx('更新完成,请重启 DSH。','Update complete. Restart DSH.')):selfUpdateState.error?h('p',{className:'vr-failed',key:'uerr'},String(selfUpdateState.error)):null,available&&(!auto||auto.supported!==true)?h('div',{className:'vr-ia-manual',key:'manual'},h('p',{className:'vr-hint'},tx('当前安装方式不支持安全的一键更新,请使用与你当前 DSH 安装方式一致的命令:','This install method cannot be safely auto-updated. Use the command matching your DSH installation:')),h('code',{className:'vr-ia-code'},pnpm),h('code',{className:'vr-ia-code'},npx)):null]);}
264
- function diagnosticsPage(){var providerCount=groups.length,modelCount=groups.reduce(function(total,group){return total+arr(group&&group.models).length;},0),configured=chainRows().filter(function(row){return row&&row.provider&&row.model;}).length,ollama=obj(value('localOllama',{})),lm=obj(value('localLmStudio',{})),testText=!local?tx('仅本机可见','Local only'):testState.status==='running'?tx('检测中','Checking'):testState.status==='done'&&testState.result&&testState.result.ok===true?tx('连接正常','Connected'):testState.status==='done'?tx('连接失败','Failed'):tx('未检测','Not checked'),capsText=caps.status==='ready'?tx('正常','Ready'):caps.status==='loading'?tx('检测中','Checking'):caps.status==='error'?tx('不可用','Unavailable'):tx('未检测','Not checked');var rows=[[tx('设置协议','Settings contract'),String(value('settingsContractRevision','—'))],[tx('模型目录','Model catalog'),catalog.status==='ready'?tx('正常','Ready'):catalog.status==='loading'?tx('检测中','Checking'):tx('不可用','Unavailable')],[tx('图片能力元数据','Image capability metadata'),capsText],[tx('可选 Provider','Selectable providers'),String(providerCount)],[tx('可选模型','Selectable models'),String(modelCount)],[tx('已配置识图模型','Configured vision models'),String(configured)],[tx('内置免费兜底','Built-in free fallback'),toggleValue('freeFallback',true)?(caps.builtinFallback.length?tx('已启用,','Enabled, ')+caps.builtinFallback.length+tx(' 个模型',' models'):tx('已启用','Enabled')):tx('已关闭','Disabled')],[tx('后端连接','Backend connection'),testText],[tx('Ollama','Ollama'),local?(ollama.enabled===true?tx('已启用','Enabled'):tx('未启用','Disabled')):tx('仅本机可见','Local only')],[tx('LM Studio','LM Studio'),local?(lm.enabled===true?tx('已启用','Enabled'):tx('未启用','Disabled')):tx('仅本机可见','Local only')],[tx('桌面截图','Desktop capture'),local?(toggleValue('desktopScreenshot',false)?tx('已启用','Enabled'):tx('未启用','Disabled')):tx('仅本机可见','Local only')],[tx('网络出口','Network egress'),local?(String(value('proxy','')).trim()?tx('视觉专用覆盖','Vision-only override'):tx('DSH/Host(默认)','DSH/Host (default)')):tx('仅本机可见','Local only')],[tx('远程设置','Remote settings'),local?(toggleValue('allowRemoteSettings',false)?tx('已启用','Enabled'):tx('未启用','Disabled')):tx('当前为远程安全视图','Remote safe view')]];function reportText(){return ['Vision Router diagnostics'].concat(rows.map(function(row){return row[0]+': '+row[1];}),['doctor: dsh-vision-router doctor']).join('\n');}async function copyReport(){try{if(typeof navigator!=='undefined'&&navigator.clipboard&&typeof navigator.clipboard.writeText==='function')await navigator.clipboard.writeText(reportText());setActionState({status:'copied'});}catch(error){setActionState({status:'error',error:error&&error.message?error.message:String(error)});}}return h(React.Fragment,null,title(tx('诊断','Diagnostics'),tx('状态页不会修改你的模型/路由设置;连接测试和更新操作会明确由按钮触发。','The status page does not change model or routing settings; connection tests and updates are explicit actions.')),card(rows.map(function(row){return diagnosticValue(row[0],row[1]);})),card([h('div',{className:'vr-ia-actions',key:'actions'},local?h('button',{type:'button',className:'vr-btn',disabled:testState.status==='running',onClick:function(){void runTestConnection();}},tx('测试连接','Test connection')):null,h('button',{type:'button',className:'vr-btn',onClick:function(){invalidateCatalog();}},tx('重新检测模型','Re-detect models')),local?h('button',{type:'button',className:'vr-btn',onClick:function(){void openLogs();}},tx('打开日志文件夹','Open logs folder')):null,h('button',{type:'button',className:'vr-btn',onClick:function(){void copyReport();}},tx('复制诊断信息','Copy diagnostics'))),h('p',{className:'vr-hint',key:'doctor'},tx('需要完整 DSH 版本、安装、Profile、运行时路由和会话诊断时执行:dsh-vision-router doctor','For the full DSH version, installation, profile, runtime-route, and session report, run: dsh-vision-router doctor')),testState.status==='done'&&testState.result&&testState.result.ok!==true?h('p',{className:'vr-failed',key:'testerr'},String(testState.result.error||tx('连接测试失败','Connection test failed'))):null,actionState.status==='error'?h('p',{className:'vr-failed',key:'aerr'},String(actionState.error||'unknown')):null]),updatePanel());}
265
+ function diagnosticsPage(){var providerCount=groups.length,modelCount=groups.reduce(function(total,group){return total+arr(group&&group.models).length;},0),configured=chainRows().filter(function(row){return row&&row.provider&&row.model;}).length,ollama=obj(value('localOllama',{})),lm=obj(value('localLmStudio',{})),testText=!local?tx('仅本机可见','Local only'):testState.status==='running'?tx('检测中','Checking'):testState.status==='done'&&testState.result&&testState.result.ok===true?tx('连接正常','Connected'):testState.status==='done'?tx('连接失败','Failed'):tx('未检测','Not checked'),capsText=caps.status==='ready'?tx('正常','Ready'):caps.status==='loading'?tx('检测中','Checking'):caps.status==='error'?tx('不可用','Unavailable'):tx('未检测','Not checked');var rows=[[tx('设置协议','Settings contract'),String(value('settingsContractRevision','—'))],[tx('模型目录','Model catalog'),catalog.status==='ready'?tx('正常','Ready'):catalog.status==='loading'?tx('检测中','Checking'):tx('不可用','Unavailable')],[tx('图片能力元数据','Image capability metadata'),capsText],[tx('可选 Provider','Selectable providers'),String(providerCount)],[tx('可选模型','Selectable models'),String(modelCount)],[tx('已配置识图模型','Configured vision models'),String(configured)],[tx('内置免费兜底','Built-in free fallback'),toggleValue('freeFallback',true)?(caps.builtinFallback.length?tx('已启用,','Enabled, ')+caps.builtinFallback.length+tx(' 个模型',' models'):tx('已启用','Enabled')):tx('已关闭','Disabled')],[tx('后端连接','Backend connection'),testText],[tx('Ollama','Ollama'),local?(ollama.enabled===true?tx('已启用','Enabled'):tx('未启用','Disabled')):tx('仅本机可见','Local only')],[tx('LM Studio','LM Studio'),local?(lm.enabled===true?tx('已启用','Enabled'):tx('未启用','Disabled')):tx('仅本机可见','Local only')],[tx('桌面截图','Desktop capture'),local?(toggleValue('desktopScreenshot',false)?tx('已启用','Enabled'):tx('未启用','Disabled')):tx('仅本机可见','Local only')],[tx('网络出口','Network egress'),local?(String(value('proxy','')).trim()?tx('视觉专用覆盖','Vision-only override'):tx('DSH/Host(默认)','DSH/Host (default)')):tx('仅本机可见','Local only')],[tx('识图结果去向','Vision result destination'),tx('当前聊天模型(可能为云端)','Current chat model (may be remote)')],[tx('远程设置','Remote settings'),local?(toggleValue('allowRemoteSettings',false)?tx('已启用','Enabled'):tx('未启用','Disabled')):tx('当前为远程安全视图','Remote safe view')]];function reportText(){return ['Vision Router diagnostics'].concat(rows.map(function(row){return row[0]+': '+row[1];}),['doctor: dsh-vision-router doctor']).join('\n');}async function copyReport(){try{if(typeof navigator!=='undefined'&&navigator.clipboard&&typeof navigator.clipboard.writeText==='function')await navigator.clipboard.writeText(reportText());setActionState({status:'copied'});}catch(error){setActionState({status:'error',error:error&&error.message?error.message:String(error)});}}return h(React.Fragment,null,title(tx('诊断','Diagnostics'),tx('状态页不会修改你的模型/路由设置;连接测试和更新操作会明确由按钮触发。','The status page does not change model or routing settings; connection tests and updates are explicit actions.')),card(rows.map(function(row){return diagnosticValue(row[0],row[1]);})),card([h('div',{className:'vr-ia-actions',key:'actions'},local?h('button',{type:'button',className:'vr-btn',disabled:testState.status==='running',onClick:function(){void runTestConnection();}},tx('测试连接','Test connection')):null,h('button',{type:'button',className:'vr-btn',onClick:function(){invalidateCatalog();}},tx('重新检测模型','Re-detect models')),local?h('button',{type:'button',className:'vr-btn',onClick:function(){void openLogs();}},tx('打开日志文件夹','Open logs folder')):null,h('button',{type:'button',className:'vr-btn',onClick:function(){void copyReport();}},tx('复制诊断信息','Copy diagnostics'))),h('p',{className:'vr-hint',key:'doctor'},tx('需要完整 DSH 版本、安装、Profile、运行时路由和会话诊断时执行:dsh-vision-router doctor','For the full DSH version, installation, profile, runtime-route, and session report, run: dsh-vision-router doctor')),testState.status==='done'&&testState.result&&testState.result.ok!==true?h('p',{className:'vr-failed',key:'testerr'},String(testState.result.error||tx('连接测试失败','Connection test failed'))):null,actionState.status==='error'?h('p',{className:'vr-failed',key:'aerr'},String(actionState.error||'unknown')):null]),updatePanel());}
265
266
 
266
267
  var pages=[['general',tx('常规','General')],['strategy',tx('识图策略','Vision strategy')],['local',tx('本地与设备','Local & device')],['advanced',tx('高级','Advanced')],['diagnostics',tx('诊断','Diagnostics')]];
267
268
  var content=page==='strategy'?strategyPage():page==='local'?localPageContent():page==='advanced'?advancedPage():page==='diagnostics'?diagnosticsPage():generalPage();
@@ -282,17 +283,16 @@ const SETTINGS_IA_STYLE = String.raw`
282
283
  `
283
284
 
284
285
  export function injectSettingsIaClientPrelude(html) {
285
- if (typeof html !== 'string' || html.includes(CLIENT_MARK)) return html
286
- const script = `<style ${CLIENT_MARK}>${SETTINGS_IA_STYLE}</style><script ${CLIENT_MARK}>${SETTINGS_IA_CLIENT_PRELUDE.replace(/<\/script/gi, '<\\/script')}</script>`
287
- const closeHead = html.indexOf('</head>')
288
- return closeHead === -1 ? `${html}${script}` : `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
286
+ const withStyle = injectClientStyleHtml(html, CLIENT_MARK, SETTINGS_IA_STYLE)
287
+ return injectClientCarrierHtml(withStyle, CLIENT_MARK, SETTINGS_IA_CLIENT_PRELUDE)
289
288
  }
290
289
 
291
290
  export function installSettingsIaClientPrelude(ctx) {
292
- ctx?.inject?.(['webServer'], (webCtx) => {
293
- webCtx.effect(
294
- () => webCtx.webServer.tapIndex(injectSettingsIaClientPrelude),
295
- 'vision-router: settings information architecture client prelude',
296
- )
291
+ installClientScriptCarriers(ctx, {
292
+ marker: CLIENT_MARK,
293
+ prelude: SETTINGS_IA_CLIENT_PRELUDE,
294
+ label: 'vision-router: Settings IA client prelude',
295
+ injectHtml: injectSettingsIaClientPrelude,
296
+ style: { marker: CLIENT_MARK, text: SETTINGS_IA_STYLE },
297
297
  })
298
298
  }
@@ -1,3 +1,4 @@
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
1
2
  import { SETTINGS_NUMBER_META } from './settings-number-contract.js'
2
3
 
3
4
  const CLIENT_MARK = 'data-vision-router-settings-limit-hardening'
@@ -94,17 +95,14 @@ export const SETTINGS_LIMIT_CLIENT_PRELUDE = String.raw`(function(){
94
95
  })();`
95
96
 
96
97
  export function injectSettingsLimitClientPrelude(html) {
97
- if (typeof html !== 'string' || html.includes(CLIENT_MARK)) return html
98
- const script = `<script ${CLIENT_MARK}>${SETTINGS_LIMIT_CLIENT_PRELUDE.replace(/<\/script/gi, '<\\/script')}</script>`
99
- const closeHead = html.indexOf('</head>')
100
- return closeHead === -1 ? `${html}${script}` : `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
98
+ return injectClientCarrierHtml(html, CLIENT_MARK, SETTINGS_LIMIT_CLIENT_PRELUDE)
101
99
  }
102
100
 
103
101
  export function installSettingsLimitClientPrelude(ctx) {
104
- ctx?.inject?.(['webServer'], (webCtx) => {
105
- webCtx.effect(
106
- () => webCtx.webServer.tapIndex(injectSettingsLimitClientPrelude),
107
- 'vision-router: settings numeric contract and limit diagnostics prelude',
108
- )
102
+ installClientScriptCarriers(ctx, {
103
+ marker: CLIENT_MARK,
104
+ prelude: SETTINGS_LIMIT_CLIENT_PRELUDE,
105
+ label: 'vision-router: settings numeric limits',
106
+ injectHtml: injectSettingsLimitClientPrelude,
109
107
  })
110
108
  }
@@ -10,7 +10,6 @@ import { Worker } from 'node:worker_threads'
10
10
  import { createRequire } from 'node:module'
11
11
  import { pathToFileURL } from 'node:url'
12
12
  import { promisify } from 'node:util'
13
- import { appendPromptToImageOnlyMessage, fetchWithOpenAICompatibility } from './http-compat.js'
14
13
  import {
15
14
  directSessionAffinityHeaders,
16
15
  isOfficialOpenCodeGoUrl,
@@ -19,12 +18,6 @@ import {
19
18
  sessionIdentityOf,
20
19
  } from './session-affinity.js'
21
20
  import { runWithVisionSessionAffinity, streamWithVisionSessionAffinity } from './session-affinity-runtime.js'
22
- import {
23
- routingCorrectionFor,
24
- toAnthropicMessages,
25
- callAnthropicCompatible,
26
- anthropicMediaType,
27
- } from './catalog-corrections.js'
28
21
  import { createCachedUpdateChecker } from './update-check.js'
29
22
  import { probeLocalBackends } from './local-connection-probe.js'
30
23
  import { detectDshSelfUpdatePlan, runDshPluginUpdate } from './self-update.js'
@@ -1,3 +1,4 @@
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
1
2
  import { LIVE_MODEL_CLIENT_PRELUDE } from './live-model-client-prelude.js'
2
3
 
3
4
  const CLIENT_PRELUDE_MARK = 'data-vision-router-live-models'
@@ -120,18 +121,14 @@ function replaceMergeCatalog(source) {
120
121
  export const STRICT_LIVE_MODEL_CLIENT_PRELUDE = replaceMergeCatalog(LIVE_MODEL_CLIENT_PRELUDE)
121
122
 
122
123
  export function injectStrictLiveModelClientPrelude(html) {
123
- if (typeof html !== 'string' || html.includes(CLIENT_PRELUDE_MARK)) return html
124
- const script = `<script ${CLIENT_PRELUDE_MARK}>${STRICT_LIVE_MODEL_CLIENT_PRELUDE.replace(/<\/script/gi, '<\\/script')}</script>`
125
- const closeHead = html.indexOf('</head>')
126
- if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
127
- return `${html}${script}`
124
+ return injectClientCarrierHtml(html, CLIENT_PRELUDE_MARK, STRICT_LIVE_MODEL_CLIENT_PRELUDE)
128
125
  }
129
126
 
130
127
  export function installStrictLiveModelClientPrelude(ctx) {
131
- ctx?.inject?.(['webServer'], (webCtx) => {
132
- webCtx.effect(
133
- () => webCtx.webServer.tapIndex(injectStrictLiveModelClientPrelude),
134
- 'vision-router: strict live model client prelude',
135
- )
128
+ installClientScriptCarriers(ctx, {
129
+ marker: CLIENT_PRELUDE_MARK,
130
+ prelude: STRICT_LIVE_MODEL_CLIENT_PRELUDE,
131
+ label: 'vision-router: strict live model client prelude',
132
+ injectHtml: injectStrictLiveModelClientPrelude,
136
133
  })
137
134
  }
@@ -4,6 +4,7 @@ import {
4
4
  } from './turn-budget-context.js'
5
5
  import { runWithDepthConfig } from './depth-guidance.js'
6
6
  import { runtimeLanguageFor } from './runtime-i18n.js'
7
+ import { visionRouterMessageSource } from './session-message-source-compat.js'
7
8
 
8
9
  const STRUCTURED_EVIDENCE_TOOLS = new Set([
9
10
  'vision_describe',
@@ -440,7 +441,7 @@ function appendGuardMessage(decision, payload, state, config) {
440
441
  ? '已达到本轮设置的深挖次数上限。不要再调用视觉工具;请基于已经获得的证据作答,并明确仍存在的不确定性。'
441
442
  : '连续多次后续视觉调用都没有产出可用证据。不要继续重复调用视觉工具;请基于已经获得的证据作答,并明确仍存在的不确定性。',
442
443
  }],
443
- source: { kind: 'plugin', plugin: 'dsh-vision-router' },
444
+ source: visionRouterMessageSource(payload?.agent?.session),
444
445
  }
445
446
  return appendSyntheticGuardOnce(decision, baseMessages, state, message, true)
446
447
  }
@@ -461,7 +462,7 @@ function appendGuardMessage(decision, payload, state, config) {
461
462
  ? 'The structured bootstrap is complete, but no usable task-directed visual evidence has been produced yet. Call at least one vision tool that can add or verify evidence, then answer after it succeeds.'
462
463
  : '结构化预识别已经完成,但目前还没有产出可用的任务定向视觉证据。请至少调用一个能新增或验证证据的视觉工具,成功后再作答。',
463
464
  }],
464
- source: { kind: 'plugin', plugin: 'dsh-vision-router' },
465
+ source: visionRouterMessageSource(payload?.agent?.session),
465
466
  }
466
467
  return appendSyntheticGuardOnce(decision, baseMessages, state, message)
467
468
  }
@@ -1,4 +1,4 @@
1
- import { htmlHasScriptMarker } from './html-script-marker.js'
1
+ import { appendStructuredClientCarrier, injectClientCarrierHtml } from './client-script-carrier.js'
2
2
 
3
3
  const V2_SETTINGS_IA_MARK = 'data-vision-router-v2-settings-ia'
4
4
 
@@ -241,16 +241,31 @@ export const V2_SETTINGS_IA_CLIENT = String.raw`(function(){
241
241
  })();`
242
242
 
243
243
  export function injectV2SettingsIaIntegration(html) {
244
- if (typeof html !== 'string' || htmlHasScriptMarker(html, V2_SETTINGS_IA_MARK)) return html
245
- const client = V2_SETTINGS_IA_CLIENT.replace(/<\/script/gi, '<\\/script')
246
- const nodes = `<style>${V2_SETTINGS_IA_STYLE}</style><script ${V2_SETTINGS_IA_MARK}>${client}</script>`
247
- const closeHead = html.indexOf('</head>')
248
- return closeHead === -1 ? `${html}${nodes}` : `${html.slice(0, closeHead)}${nodes}${html.slice(closeHead)}`
244
+ if (typeof html !== 'string') return html
245
+ const styleMark = 'data-vision-router-v2-settings-ia-style'
246
+ let next = html
247
+ if (!next.includes(styleMark)) {
248
+ const style = `<style ${styleMark}>${V2_SETTINGS_IA_STYLE}</style>`
249
+ const closeHead = next.indexOf('</head>')
250
+ next = closeHead === -1 ? `${next}${style}` : `${next.slice(0, closeHead)}${style}${next.slice(closeHead)}`
251
+ }
252
+ return injectClientCarrierHtml(next, V2_SETTINGS_IA_MARK, V2_SETTINGS_IA_CLIENT)
249
253
  }
250
254
 
251
255
  export function installV2SettingsIaIntegration(ctx) {
252
256
  if (!ctx || typeof ctx.inject !== 'function') return
253
257
  ctx.inject(['webServer'], (webCtx) => {
258
+ if (typeof webCtx.on === 'function') {
259
+ webCtx.on('webserver/index-inject', (table) => {
260
+ if (!Array.isArray(table)) return
261
+ appendStructuredClientCarrier(table, V2_SETTINGS_IA_MARK, V2_SETTINGS_IA_CLIENT)
262
+ const styleMark = 'data-vision-router-v2-settings-ia-style'
263
+ if (!table.some((row) => row?.kind === 'style' && typeof row.text === 'string' && row.text.includes(styleMark))) {
264
+ table.push({ kind: 'style', placement: 'head', text: `/* ${styleMark} */
265
+ ${V2_SETTINGS_IA_STYLE}` })
266
+ }
267
+ })
268
+ }
254
269
  webCtx.effect(
255
270
  () => webCtx.webServer.tapIndex(injectV2SettingsIaIntegration),
256
271
  'vision-router: integrate v2 routing controls into settings IA',
@@ -215,7 +215,7 @@ function failureStream(message, code = 'VISION_IMAGE_DELIVERY_UNAVAILABLE') {
215
215
  }
216
216
  }
217
217
 
218
- function directStream(ctx, core, plan, call, logger, source) {
218
+ function directStream(ctx, core, plan, call, logger, source, providerTransport) {
219
219
  return {
220
220
  async *[Symbol.asyncIterator]() {
221
221
  const started = Date.now()
@@ -252,6 +252,7 @@ function directStream(ctx, core, plan, call, logger, source) {
252
252
  maxTokens: call.maxTokens ?? 4096,
253
253
  signal: call.signal,
254
254
  sessionId: call.sessionId,
255
+ providerTransport,
255
256
  resolveCredential: async (ref) => ref === keyRef ? apiKey : undefined,
256
257
  },
257
258
  )
@@ -349,7 +350,7 @@ function llmWithRuntimePolicy(ctx, llm, options) {
349
350
  )
350
351
  return
351
352
  }
352
- yield* directStream(ctx, core, plan, call, logger, source)
353
+ yield* directStream(ctx, core, plan, call, logger, source, options.providerTransport)
353
354
  return
354
355
  }
355
356