dsh-vision-router 2.2.7 → 2.2.8

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 (33) hide show
  1. package/README.md +2 -2
  2. package/README.zh.md +2 -2
  3. package/docs/architecture/dsh-020-rc2-adaptation.md +23 -0
  4. package/docs/architecture/dsh-compatibility-matrix.md +1 -1
  5. package/docs/architecture/dsh-support-window.md +4 -2
  6. package/docs/architecture/host-first-proxy-convergence.md +1 -1
  7. package/docs/releases/v2.2.8.md +25 -0
  8. package/lib/client-host-compat-prelude.js +7 -12
  9. package/lib/client-presentation-boundary-main.js +7 -15
  10. package/lib/client-presentation-boundary.js +7 -12
  11. package/lib/client-script-carrier.js +90 -0
  12. package/lib/client.js +2 -2
  13. package/lib/dsh-support-window.js +1 -1
  14. package/lib/guide-vision-toggle-highlight.js +7 -10
  15. package/lib/live-model-client-prelude.js +7 -14
  16. package/lib/local-remote-settings-permission.js +8 -9
  17. package/lib/session-vision-mode-boundary.js +43 -8
  18. package/lib/settings-client-rc8-lifecycle.js +7 -12
  19. package/lib/settings-factory-lifecycle.js +25 -14
  20. package/lib/settings-ia-client-prelude.js +11 -11
  21. package/lib/settings-limit-client-prelude.js +7 -9
  22. package/lib/strict-live-model-client-prelude.js +7 -10
  23. package/lib/v2-settings-ia-integration.js +21 -6
  24. package/lib/vision-background-benchmark.js +3 -5
  25. package/lib/vision-capability-benchmark-client.js +7 -10
  26. package/lib/vision-exact-check-client.js +11 -6
  27. package/lib/vision-model-visibility-boundary-main.js +7 -11
  28. package/lib/vision-model-visibility-boundary.js +7 -12
  29. package/lib/vision-routing-settings-prelude.js +7 -11
  30. package/lib/vision-toggle-root-hardening.js +28 -3
  31. package/lib/vision-turn-budget-client-prelude.js +7 -10
  32. package/lib/web/remote-settings-client.js +7 -12
  33. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { htmlHasScriptMarker } from './html-script-marker.js'
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
2
2
 
3
3
  const SETTINGS_RC8_MARK = 'data-vision-router-settings-rc8-lifecycle'
4
4
 
@@ -384,19 +384,14 @@ export const SETTINGS_RC8_CLIENT_PRELUDE = String.raw`(function(){
384
384
  })();`
385
385
 
386
386
  export function injectSettingsRc8ClientPrelude(html) {
387
- if (typeof html !== 'string' || htmlHasScriptMarker(html, SETTINGS_RC8_MARK)) return html
388
- const safe = SETTINGS_RC8_CLIENT_PRELUDE.replace(/<\/script/gi, '<\\/script')
389
- const script = `<script ${SETTINGS_RC8_MARK}>${safe}</script>`
390
- const closeHead = html.indexOf('</head>')
391
- return closeHead === -1 ? `${html}${script}` : `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
387
+ return injectClientCarrierHtml(html, SETTINGS_RC8_MARK, SETTINGS_RC8_CLIENT_PRELUDE)
392
388
  }
393
389
 
394
390
  export function installSettingsRc8ClientLifecycle(ctx) {
395
- if (!ctx || typeof ctx.inject !== 'function') return
396
- ctx.inject(['webServer'], (webCtx) => {
397
- webCtx.effect(
398
- () => webCtx.webServer.tapIndex(injectSettingsRc8ClientPrelude),
399
- 'vision-router: rc8 settings client lifecycle',
400
- )
391
+ installClientScriptCarriers(ctx, {
392
+ marker: SETTINGS_RC8_MARK,
393
+ prelude: SETTINGS_RC8_CLIENT_PRELUDE,
394
+ label: 'vision-router: rc8 settings client lifecycle',
395
+ injectHtml: injectSettingsRc8ClientPrelude,
401
396
  })
402
397
  }
@@ -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
  }
@@ -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
  }
@@ -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',
@@ -8,7 +8,7 @@ import {
8
8
  import { withHardDeadline } from './vision-capability-benchmark-hardening.js'
9
9
  import { resolveVisionRoutingAuthority } from './vision-routing-authority.js'
10
10
  import { redactDiagnosticText } from './diagnostic-redaction.js'
11
- import { injectVisionExactCheckClient } from './vision-exact-check-client.js'
11
+ import { installVisionExactCheckClient } from './vision-exact-check-client.js'
12
12
  import { createImageInputVerdictStore } from './vision-image-input-verdict.js'
13
13
  import { createBackgroundBenchmarkStopStore } from './vision-background-stop-store.js'
14
14
  import { backgroundFailurePolicy } from './vision-background-failure-policy.js'
@@ -1033,12 +1033,10 @@ export function installBackgroundCapabilityProfiling(ctx, config, core, store, o
1033
1033
  // Cleanup registration is best-effort; the profiler remains process-scoped.
1034
1034
  }
1035
1035
 
1036
+ installVisionExactCheckClient(ctx)
1037
+
1036
1038
  try {
1037
1039
  ctx?.inject?.(['webServer'], (webCtx) => {
1038
- webCtx.effect(
1039
- () => webCtx.webServer.tapIndex(injectVisionExactCheckClient),
1040
- 'vision-router: exact image check product control',
1041
- )
1042
1040
  webCtx.effect(() => webCtx.webServer.register({
1043
1041
  kind: 'exact',
1044
1042
  path: VISION_CAPABILITY_RUNTIME_PATH,
@@ -1,4 +1,4 @@
1
- import { htmlHasScriptMarker } from './html-script-marker.js'
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
2
2
 
3
3
  const CLIENT_MARK = 'data-vision-router-capability-benchmark'
4
4
 
@@ -412,17 +412,14 @@ export const CAPABILITY_BENCHMARK_CLIENT = String.raw`(function(){
412
412
  })();`
413
413
 
414
414
  export function injectCapabilityBenchmarkClient(html) {
415
- if (typeof html !== 'string' || htmlHasScriptMarker(html, CLIENT_MARK)) return html
416
- const script = `<script ${CLIENT_MARK}>${CAPABILITY_BENCHMARK_CLIENT.replace(/<\/script/gi, '<\\/script')}</script>`
417
- const head = html.indexOf('<head>')
418
- return head === -1 ? `${script}${html}` : `${html.slice(0, head + 6)}${script}${html.slice(head + 6)}`
415
+ return injectClientCarrierHtml(html, CLIENT_MARK, CAPABILITY_BENCHMARK_CLIENT)
419
416
  }
420
417
 
421
418
  export function installCapabilityBenchmarkClient(ctx) {
422
- ctx?.inject?.(['webServer'], (webCtx) => {
423
- webCtx.effect(
424
- () => webCtx.webServer.tapIndex(injectCapabilityBenchmarkClient),
425
- 'vision-router: capability benchmark Host presentation client controls',
426
- )
419
+ installClientScriptCarriers(ctx, {
420
+ marker: CLIENT_MARK,
421
+ prelude: CAPABILITY_BENCHMARK_CLIENT,
422
+ label: 'vision-router: capability benchmark client',
423
+ injectHtml: injectCapabilityBenchmarkClient,
427
424
  })
428
425
  }
@@ -1,4 +1,4 @@
1
- import { htmlHasScriptMarker } from './html-script-marker.js'
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
2
2
 
3
3
  const CLIENT_MARK = 'data-vision-router-exact-check'
4
4
 
@@ -185,9 +185,14 @@ export const VISION_EXACT_CHECK_CLIENT = String.raw`(function(){
185
185
  })();`
186
186
 
187
187
  export function injectVisionExactCheckClient(html) {
188
- if (typeof html !== 'string' || htmlHasScriptMarker(html, CLIENT_MARK)) return html
189
- const script = `<script ${CLIENT_MARK}>${VISION_EXACT_CHECK_CLIENT.replace(/<\/script/gi, '<\\/script')}</script>`
190
- const closeHead = html.indexOf('</head>')
191
- if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
192
- return `${html}${script}`
188
+ return injectClientCarrierHtml(html, CLIENT_MARK, VISION_EXACT_CHECK_CLIENT)
189
+ }
190
+
191
+ export function installVisionExactCheckClient(ctx) {
192
+ installClientScriptCarriers(ctx, {
193
+ marker: CLIENT_MARK,
194
+ prelude: VISION_EXACT_CHECK_CLIENT,
195
+ label: 'vision-router: exact image check product control',
196
+ injectHtml: injectVisionExactCheckClient,
197
+ })
193
198
  }
@@ -1,3 +1,4 @@
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
1
2
  const VISION_MODE_WRAPPER_SOURCE = 'deepseek-official'
2
3
  const DEFAULT_WRAPPER_ROUTE = 'deepseek-vision'
3
4
  const MODEL_SELECTION_TARGET = '@deepseek-ai/dsh-client-ui-model-selection'
@@ -454,19 +455,14 @@ export const VISION_MODEL_VISIBILITY_PRELUDE = String.raw`(function(){
454
455
  })();`
455
456
 
456
457
  export function injectVisionModelVisibilityBoundary(html) {
457
- if (typeof html !== 'string' || html.includes(VISIBILITY_MARK)) return html
458
- const safe = VISION_MODEL_VISIBILITY_PRELUDE.replace(/<\/script/gi, '<\\/script')
459
- const script = `<script ${VISIBILITY_MARK}>${safe}</script>`
460
- const closeHead = html.indexOf('</head>')
461
- if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
462
- return `${html}${script}`
458
+ return injectClientCarrierHtml(html, VISIBILITY_MARK, VISION_MODEL_VISIBILITY_PRELUDE)
463
459
  }
464
460
 
465
461
  export function installVisionModelVisibilityBoundary(ctx) {
466
- ctx?.inject?.(['webServer'], (webCtx) => {
467
- webCtx.effect(
468
- () => webCtx.webServer.tapIndex(injectVisionModelVisibilityBoundary),
469
- 'vision-router: model visibility boundary',
470
- )
462
+ installClientScriptCarriers(ctx, {
463
+ marker: VISIBILITY_MARK,
464
+ prelude: VISION_MODEL_VISIBILITY_PRELUDE,
465
+ label: 'vision-router: model visibility boundary',
466
+ injectHtml: injectVisionModelVisibilityBoundary,
471
467
  })
472
468
  }
@@ -1,4 +1,4 @@
1
- import { htmlHasScriptMarker } from './html-script-marker.js'
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
2
2
  import { VISION_MODEL_VISIBILITY_PRELUDE } from './vision-model-visibility-boundary-main.js'
3
3
 
4
4
  const VISIBILITY_MARK = 'data-vision-router-model-visibility-boundary'
@@ -10,19 +10,14 @@ export {
10
10
  } from './vision-model-visibility-boundary-main.js'
11
11
 
12
12
  export function injectVisionModelVisibilityBoundary(html) {
13
- if (typeof html !== 'string' || htmlHasScriptMarker(html, VISIBILITY_MARK)) return html
14
- const safe = VISION_MODEL_VISIBILITY_PRELUDE.replace(/<\/script/gi, '<\\/script')
15
- const script = `<script ${VISIBILITY_MARK}>${safe}</script>`
16
- const closeHead = html.indexOf('</head>')
17
- if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
18
- return `${html}${script}`
13
+ return injectClientCarrierHtml(html, VISIBILITY_MARK, VISION_MODEL_VISIBILITY_PRELUDE)
19
14
  }
20
15
 
21
16
  export function installVisionModelVisibilityBoundary(ctx) {
22
- ctx?.inject?.(['webServer'], (webCtx) => {
23
- webCtx.effect(
24
- () => webCtx.webServer.tapIndex(injectVisionModelVisibilityBoundary),
25
- 'vision-router: model visibility boundary',
26
- )
17
+ installClientScriptCarriers(ctx, {
18
+ marker: VISIBILITY_MARK,
19
+ prelude: VISION_MODEL_VISIBILITY_PRELUDE,
20
+ label: 'vision-router: model visibility boundary',
21
+ injectHtml: injectVisionModelVisibilityBoundary,
27
22
  })
28
23
  }
@@ -1,4 +1,4 @@
1
- import { htmlHasScriptMarker } from './html-script-marker.js'
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
2
2
 
3
3
  const ROUTING_SETTINGS_PRELUDE_MARK = 'data-vision-router-routing-settings'
4
4
 
@@ -536,18 +536,14 @@ export const VISION_ROUTING_SETTINGS_PRELUDE = String.raw`(function(){
536
536
  })();`
537
537
 
538
538
  export function injectVisionRoutingSettingsPrelude(html) {
539
- if (typeof html !== 'string' || htmlHasScriptMarker(html, ROUTING_SETTINGS_PRELUDE_MARK)) return html
540
- const script = `<script ${ROUTING_SETTINGS_PRELUDE_MARK}>${VISION_ROUTING_SETTINGS_PRELUDE.replace(/<\/script/gi, '<\\/script')}</script>`
541
- const closeHead = html.indexOf('</head>')
542
- if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
543
- return `${html}${script}`
539
+ return injectClientCarrierHtml(html, ROUTING_SETTINGS_PRELUDE_MARK, VISION_ROUTING_SETTINGS_PRELUDE)
544
540
  }
545
541
 
546
542
  export function installVisionRoutingSettingsPrelude(ctx) {
547
- ctx?.inject?.(['webServer'], (webCtx) => {
548
- webCtx.effect(
549
- () => webCtx.webServer.tapIndex(injectVisionRoutingSettingsPrelude),
550
- 'vision-router: v2 routing settings product panel',
551
- )
543
+ installClientScriptCarriers(ctx, {
544
+ marker: ROUTING_SETTINGS_PRELUDE_MARK,
545
+ prelude: VISION_ROUTING_SETTINGS_PRELUDE,
546
+ label: 'vision-router: Vision routing Settings prelude',
547
+ injectHtml: injectVisionRoutingSettingsPrelude,
552
548
  })
553
549
  }
@@ -331,9 +331,9 @@ function replaceAll(source, before, after) {
331
331
  return source.includes(before) ? source.split(before).join(after) : source
332
332
  }
333
333
 
334
- export function hardenVisionToggleHtml(html) {
335
- if (typeof html !== 'string') return html
336
- let next = html
334
+ export function hardenVisionClientSource(source) {
335
+ if (typeof source !== 'string') return source
336
+ let next = source
337
337
 
338
338
  next = replaceAll(
339
339
  next,
@@ -430,6 +430,13 @@ export function hardenVisionToggleHtml(html) {
430
430
  ` var hardening = window.__dshVisionRouterRootHardening;\n var recovery = hardening ? hardening.recoveryFor(directory) : undefined;\n var store = wrapStore(directory.store, settings, recovery);`,
431
431
  )
432
432
 
433
+ return next
434
+ }
435
+
436
+ export function hardenVisionToggleHtml(html) {
437
+ if (typeof html !== 'string') return html
438
+ let next = hardenVisionClientSource(html)
439
+
433
440
  if (!next.includes(OWNERSHIP_MARK)) {
434
441
  const safe = CLIENT_PRELUDE.replace(/<\/script/gi, '<\\/script')
435
442
  const script = `<script ${OWNERSHIP_MARK}>${safe}</script>`
@@ -474,6 +481,24 @@ export function createVisionToggleRootHardening(ctx, config = {}) {
474
481
  }),
475
482
  'vision-router: route ownership',
476
483
  )
484
+ if (typeof webCtx.on === 'function') {
485
+ webCtx.on('webserver/index-inject', (table) => {
486
+ if (!Array.isArray(table)) return
487
+ for (const row of table) {
488
+ if (row?.kind === 'script' && typeof row.text === 'string') {
489
+ row.text = hardenVisionClientSource(row.text)
490
+ }
491
+ }
492
+ const signature = `/* ${OWNERSHIP_MARK} */`
493
+ if (!table.some((row) => row?.kind === 'script' && typeof row.text === 'string' && row.text.includes(signature))) {
494
+ table.push({
495
+ kind: 'script',
496
+ placement: 'head',
497
+ text: `${signature}\n${CLIENT_PRELUDE}`,
498
+ })
499
+ }
500
+ })
501
+ }
477
502
  webCtx.effect(
478
503
  () => webCtx.webServer.tapIndex(hardenVisionToggleHtml),
479
504
  'vision-router: vision toggle root hardening',
@@ -1,3 +1,4 @@
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
1
2
  const CLIENT_PRELUDE_MARK = 'data-vision-router-turn-budget'
2
3
 
3
4
  export const VISION_TURN_BUDGET_CLIENT_PRELUDE = String.raw`(function(){
@@ -259,18 +260,14 @@ export const VISION_TURN_BUDGET_CLIENT_PRELUDE = String.raw`(function(){
259
260
  })();`
260
261
 
261
262
  export function injectVisionTurnBudgetClientPrelude(html) {
262
- if (typeof html !== 'string' || html.includes(CLIENT_PRELUDE_MARK)) return html
263
- const script = `<script ${CLIENT_PRELUDE_MARK}>${VISION_TURN_BUDGET_CLIENT_PRELUDE.replace(/<\/script/gi, '<\\/script')}</script>`
264
- const closeHead = html.indexOf('</head>')
265
- if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
266
- return `${html}${script}`
263
+ return injectClientCarrierHtml(html, CLIENT_PRELUDE_MARK, VISION_TURN_BUDGET_CLIENT_PRELUDE)
267
264
  }
268
265
 
269
266
  export function installVisionTurnBudgetClientPrelude(ctx) {
270
- ctx?.inject?.(['webServer'], (webCtx) => {
271
- webCtx.effect(
272
- () => webCtx.webServer.tapIndex(injectVisionTurnBudgetClientPrelude),
273
- 'vision-router: turn budget client prelude',
274
- )
267
+ installClientScriptCarriers(ctx, {
268
+ marker: CLIENT_PRELUDE_MARK,
269
+ prelude: VISION_TURN_BUDGET_CLIENT_PRELUDE,
270
+ label: 'vision-router: turn budget client prelude',
271
+ injectHtml: injectVisionTurnBudgetClientPrelude,
275
272
  })
276
273
  }