dsh-vision-router 2.2.6 → 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.
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/docs/architecture/dsh-020-rc2-adaptation.md +23 -0
- package/docs/architecture/dsh-compatibility-matrix.md +4 -3
- package/docs/architecture/dsh-support-window.md +7 -4
- package/docs/architecture/host-first-proxy-convergence.md +1 -1
- package/docs/releases/v2.2.7.md +22 -0
- package/docs/releases/v2.2.8.md +25 -0
- package/lib/artifact-io.js +11 -3
- package/lib/client-host-compat-prelude.js +7 -12
- package/lib/client-presentation-boundary-main.js +7 -15
- package/lib/client-presentation-boundary.js +7 -12
- package/lib/client-script-carrier.js +90 -0
- package/lib/client.js +7 -2
- package/lib/dsh-support-window.js +5 -3
- package/lib/guide-vision-toggle-highlight.js +7 -10
- package/lib/live-model-client-prelude.js +7 -14
- package/lib/live-model-discovery.js +2 -2
- package/lib/local-remote-settings-permission.js +8 -9
- package/lib/session-vision-mode-boundary.js +43 -8
- package/lib/settings-client-017-compat.js +4 -0
- package/lib/settings-client-rc8-lifecycle.js +11 -12
- package/lib/settings-factory-lifecycle.js +25 -14
- package/lib/settings-ia-client-prelude.js +12 -12
- package/lib/settings-limit-client-prelude.js +7 -9
- package/lib/strict-live-model-client-prelude.js +7 -10
- package/lib/tesseract-exec-compat.js +28 -6
- package/lib/v2-settings-ia-integration.js +21 -6
- package/lib/vision-background-benchmark.js +3 -5
- package/lib/vision-background-stop-store.js +2 -1
- package/lib/vision-capability-benchmark-client.js +7 -10
- package/lib/vision-capability-probe.js +2 -1
- package/lib/vision-exact-check-client.js +13 -8
- package/lib/vision-image-input-verdict.js +2 -1
- package/lib/vision-model-visibility-boundary-main.js +7 -11
- package/lib/vision-model-visibility-boundary.js +7 -12
- package/lib/vision-routing-settings-prelude.js +7 -11
- package/lib/vision-toggle-root-hardening.js +28 -3
- package/lib/vision-turn-budget-client-prelude.js +7 -10
- package/lib/web/remote-settings-client.js +11 -12
- package/package.json +2 -5
|
@@ -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
|
-
|
|
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)
|
|
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,
|
|
@@ -28,6 +28,10 @@ export const SETTINGS_017_CLIENT_PRELUDE = String.raw`(function(){
|
|
|
28
28
|
var hostname = locationLike && typeof locationLike.hostname === 'string'
|
|
29
29
|
? locationLike.hostname.toLowerCase().replace(/^\[|\]$/g, '')
|
|
30
30
|
: '';
|
|
31
|
+
var protocol = locationLike && typeof locationLike.protocol === 'string'
|
|
32
|
+
? locationLike.protocol.toLowerCase()
|
|
33
|
+
: '';
|
|
34
|
+
if (protocol === 'dsh-app:' && hostname === 'app') return true;
|
|
31
35
|
if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return true;
|
|
32
36
|
return /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
33
37
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
|
@@ -35,6 +35,10 @@ export const SETTINGS_RC8_CLIENT_PRELUDE = String.raw`(function(){
|
|
|
35
35
|
var hostname = locationLike && typeof locationLike.hostname === 'string'
|
|
36
36
|
? locationLike.hostname.toLowerCase().replace(/^\[|\]$/g, '')
|
|
37
37
|
: '';
|
|
38
|
+
var protocol = locationLike && typeof locationLike.protocol === 'string'
|
|
39
|
+
? locationLike.protocol.toLowerCase()
|
|
40
|
+
: '';
|
|
41
|
+
if (protocol === 'dsh-app:' && hostname === 'app') return true;
|
|
38
42
|
if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return true;
|
|
39
43
|
return /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
40
44
|
}
|
|
@@ -380,19 +384,14 @@ export const SETTINGS_RC8_CLIENT_PRELUDE = String.raw`(function(){
|
|
|
380
384
|
})();`
|
|
381
385
|
|
|
382
386
|
export function injectSettingsRc8ClientPrelude(html) {
|
|
383
|
-
|
|
384
|
-
const safe = SETTINGS_RC8_CLIENT_PRELUDE.replace(/<\/script/gi, '<\\/script')
|
|
385
|
-
const script = `<script ${SETTINGS_RC8_MARK}>${safe}</script>`
|
|
386
|
-
const closeHead = html.indexOf('</head>')
|
|
387
|
-
return closeHead === -1 ? `${html}${script}` : `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
|
|
387
|
+
return injectClientCarrierHtml(html, SETTINGS_RC8_MARK, SETTINGS_RC8_CLIENT_PRELUDE)
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
export function installSettingsRc8ClientLifecycle(ctx) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
)
|
|
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,
|
|
397
396
|
})
|
|
398
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
|
|
19
|
-
const
|
|
20
|
-
return
|
|
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
|
|
127
|
-
next = next
|
|
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(){
|
|
@@ -13,7 +14,7 @@ export const SETTINGS_IA_CLIENT_PRELUDE = String.raw`(function(){
|
|
|
13
14
|
function same(a,b){try{return JSON.stringify(a)===JSON.stringify(b);}catch(_){return false;}}
|
|
14
15
|
function bool(value){return value===true||value==='true';}
|
|
15
16
|
function rpcValue(body){if(body&&typeof body==='object'&&body.result&&typeof body.result==='object')return body.result.ok===true?body.result.value:undefined;return body;}
|
|
16
|
-
function localPage(){try{var host=String(
|
|
17
|
+
function localPage(){try{var location=window.location||{},host=String(location.hostname||'').toLowerCase(),protocol=String(location.protocol||'').toLowerCase();return(protocol==='dsh-app:'&&host==='app')||host==='localhost'||host.endsWith('.localhost')||host==='::1'||host==='[::1]'||/^127(?:\.\d{1,3}){3}$/.test(host);}catch(_){return false;}}
|
|
17
18
|
function builtinRow(row){return !!row&&row.provider==='vision-http'&&typeof row.model==='string'&&row.model.indexOf('ovh/')===0;}
|
|
18
19
|
function cloneRow(row,model){return {provider:String(row&&row.provider||''),model:String(model===undefined?(row&&row.model||''):model),fallbacks:[]};}
|
|
19
20
|
function splitChain(value){var builtins=[],users=[];function push(row,model){var next=cloneRow(row,model);(builtinRow(next)?builtins:users).push(next);}arr(value).forEach(function(row){if(!row||typeof row!=='object')return;push(row,row.model);arr(row.fallbacks).forEach(function(fallback){if(typeof fallback==='string'&&fallback.trim())push(row,fallback.trim());});});return {builtins:builtins,users:users};}
|
|
@@ -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
|
|
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
|
-
|
|
286
|
-
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
}
|
|
@@ -187,8 +187,12 @@ function callbackForwarder(execFileImpl, patchedCustom) {
|
|
|
187
187
|
return wrapped
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
function
|
|
191
|
-
|
|
190
|
+
function builtinExportSynchronizer(moduleObject, options) {
|
|
191
|
+
const builtinModule = options.builtinChildProcessModule ?? childProcess
|
|
192
|
+
const synchronize = options.syncBuiltinESMExports ?? syncBuiltinESMExports
|
|
193
|
+
return () => {
|
|
194
|
+
if (moduleObject === builtinModule) synchronize()
|
|
195
|
+
}
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
function warnInstallDegraded(ctx, reason) {
|
|
@@ -217,13 +221,26 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
|
|
|
217
221
|
const execFileImpl = moduleObject.execFile
|
|
218
222
|
let state = installStates.get(execFileImpl)
|
|
219
223
|
if (state === undefined) {
|
|
224
|
+
const synchronizeBuiltinExports = builtinExportSynchronizer(moduleObject, options)
|
|
220
225
|
const originalCustom = execFileImpl[promisify.custom]
|
|
221
226
|
const patchedCustom = createVisionRouterExecFilePromisifyCompat(execFileImpl, originalCustom, options)
|
|
222
227
|
|
|
223
228
|
if (canReplaceCustomPromisify(execFileImpl)) {
|
|
224
229
|
try {
|
|
225
230
|
setCustomPromisify(execFileImpl, patchedCustom)
|
|
231
|
+
// Electron can expose distinct CJS and ESM execFile function objects.
|
|
232
|
+
// Synchronize after mutating the CJS custom hook so existing ESM named
|
|
233
|
+
// imports observe the same promisify.custom compatibility function.
|
|
234
|
+
synchronizeBuiltinExports()
|
|
226
235
|
} catch (error) {
|
|
236
|
+
try {
|
|
237
|
+
if (execFileImpl[promisify.custom] === patchedCustom) {
|
|
238
|
+
restoreCustomPromisify(execFileImpl, originalCustom)
|
|
239
|
+
synchronizeBuiltinExports()
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
/* best effort rollback; the CJS hook is restored before re-sync */
|
|
243
|
+
}
|
|
227
244
|
patchedCustom.deactivate?.()
|
|
228
245
|
warnInstallDegraded(ctx, error && error.message ? error.message : String(error))
|
|
229
246
|
return () => {}
|
|
@@ -236,6 +253,7 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
|
|
|
236
253
|
exposedExecFile: execFileImpl,
|
|
237
254
|
originalCustom,
|
|
238
255
|
patchedCustom,
|
|
256
|
+
synchronizeBuiltinExports,
|
|
239
257
|
}
|
|
240
258
|
installStates.set(execFileImpl, state)
|
|
241
259
|
} else {
|
|
@@ -243,12 +261,12 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
|
|
|
243
261
|
try {
|
|
244
262
|
moduleObject.execFile = exposedExecFile
|
|
245
263
|
if (moduleObject.execFile !== exposedExecFile) throw new Error('child_process.execFile export is not writable')
|
|
246
|
-
|
|
264
|
+
synchronizeBuiltinExports()
|
|
247
265
|
} catch (error) {
|
|
248
266
|
try {
|
|
249
267
|
if (moduleObject.execFile === exposedExecFile) {
|
|
250
268
|
moduleObject.execFile = execFileImpl
|
|
251
|
-
|
|
269
|
+
synchronizeBuiltinExports()
|
|
252
270
|
}
|
|
253
271
|
} catch {
|
|
254
272
|
/* best effort rollback */
|
|
@@ -265,6 +283,7 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
|
|
|
265
283
|
exposedExecFile,
|
|
266
284
|
originalCustom,
|
|
267
285
|
patchedCustom,
|
|
286
|
+
synchronizeBuiltinExports,
|
|
268
287
|
}
|
|
269
288
|
installStates.set(execFileImpl, state)
|
|
270
289
|
installStates.set(exposedExecFile, state)
|
|
@@ -284,7 +303,10 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
|
|
|
284
303
|
|
|
285
304
|
if (state.mode === 'custom-property') {
|
|
286
305
|
if (stillOwnsCustom) {
|
|
287
|
-
try {
|
|
306
|
+
try {
|
|
307
|
+
restoreCustomPromisify(state.execFileImpl, state.originalCustom)
|
|
308
|
+
state.synchronizeBuiltinExports()
|
|
309
|
+
} catch { /* best effort */ }
|
|
288
310
|
}
|
|
289
311
|
} else if (
|
|
290
312
|
state.mode === 'module-wrapper' &&
|
|
@@ -293,7 +315,7 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
|
|
|
293
315
|
) {
|
|
294
316
|
try {
|
|
295
317
|
state.moduleObject.execFile = state.execFileImpl
|
|
296
|
-
|
|
318
|
+
state.synchronizeBuiltinExports()
|
|
297
319
|
} catch {
|
|
298
320
|
/* a later host/plugin mutation stays authoritative */
|
|
299
321
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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'
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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 {
|
|
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,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
1
2
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import process from 'node:process'
|
|
@@ -88,7 +89,7 @@ async function load(file, fsOps, at) {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
async function save(file, records, fsOps, at) {
|
|
91
|
-
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`
|
|
92
|
+
const temporary = `${file}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`
|
|
92
93
|
const stops = selectRetainedStops(records.values(), at)
|
|
93
94
|
await fsOps.mkdir(path.dirname(file), { recursive: true })
|
|
94
95
|
await fsOps.writeFile(temporary, JSON.stringify({ version: CACHE_VERSION, stops }), { encoding: 'utf8', mode: 0o600 })
|