dsh-vision-router 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/releases/v2.0.1.md +17 -0
- package/entry.js +15 -3
- package/lib/doctor-cli.js +7 -0
- package/lib/doctor-vision-limits.js +72 -0
- package/lib/settings-limit-client-prelude.js +110 -0
- package/lib/settings-number-contract.js +29 -0
- package/lib/vision-limit-diagnostics.js +202 -0
- package/package.json +2 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# v2.0.1
|
|
2
|
+
|
|
3
|
+
这是一个针对长任务识图设置与诊断的补丁版本。
|
|
4
|
+
|
|
5
|
+
## 修复
|
|
6
|
+
|
|
7
|
+
- 修复 `visionTaskTimeoutMs` 在浏览器设置页与 Host schema 之间的校验漂移:客户端现在会在写入前正确检查范围和步进,避免 Host 拒绝后只留下 `readback-mismatch`。
|
|
8
|
+
- 明确区分“单次识图任务超时”和“首次识图后的整轮时间上限”;整轮限制继续沿用既有 wall-clock 语义。
|
|
9
|
+
- 保持 v2 默认 `visionTurnBudgetMs=0`(不限制);不会自动迁移或清除用户已有的显式正数配置。
|
|
10
|
+
- 当整轮视觉时间上限耗尽时,guard 与日志会显示实际生效的限制;Diagnostics 与 Doctor 也会显示有效值及其来源,便于定位旧 profile / composition 覆盖。
|
|
11
|
+
- 增加针对设置边界、预算诊断、Doctor 输出和运行时结构不变量的回归测试。
|
|
12
|
+
|
|
13
|
+
## Compatibility
|
|
14
|
+
|
|
15
|
+
- No change to #220/#295 lazy activation or wall-clock accounting.
|
|
16
|
+
- No change to the v2 default: `visionTurnBudgetMs=0` remains unlimited.
|
|
17
|
+
- Existing explicit positive whole-turn limits remain honored.
|
package/entry.js
CHANGED
|
@@ -34,6 +34,7 @@ import { installLocalMutationRouteBoundary } from './lib/web-capability-boundary
|
|
|
34
34
|
import { installScreenshotSourceBoundary } from './lib/screenshot-source-boundary.js'
|
|
35
35
|
import { installVisionToolRuntimeBoundary } from './lib/vision-tool-runtime-boundary.js'
|
|
36
36
|
import { installVisionRouterRemoteSettingsBridge } from './lib/remote-settings-bridge.js'
|
|
37
|
+
import { installSettingsLimitClientPrelude } from './lib/settings-limit-client-prelude.js'
|
|
37
38
|
import { installSettingsRc8ClientLifecycle } from './lib/settings-client-rc8-lifecycle.js'
|
|
38
39
|
import { installCapabilityShadowRuntime } from './lib/vision-capability-shadow.js'
|
|
39
40
|
import { createCapabilityProfileStore } from './lib/vision-capability-probe.js'
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
installStructuredFlowHardening,
|
|
57
58
|
normalizeGuidanceOverrides,
|
|
58
59
|
} from './lib/structured-flow-hardening.js'
|
|
60
|
+
import { installVisionLimitDiagnostics } from './lib/vision-limit-diagnostics.js'
|
|
59
61
|
import {
|
|
60
62
|
attachmentContextForContract,
|
|
61
63
|
hasBatchAttachmentContract,
|
|
@@ -205,6 +207,9 @@ export function apply(ctx, config = {}) {
|
|
|
205
207
|
}
|
|
206
208
|
const batchAttachmentHost = hasBatchAttachmentContract(stabilizedCtx)
|
|
207
209
|
if (batchAttachmentHost) installVisionAttachmentAdmissionPolicy(stabilizedCtx, logging.logger)
|
|
210
|
+
// Install this before the consolidated Settings IA transform registered by
|
|
211
|
+
// the remote-settings bridge so the numeric fence remains the outer wrapper.
|
|
212
|
+
installSettingsLimitClientPrelude(stabilizedCtx)
|
|
208
213
|
installVisionRouterRemoteSettingsBridge(stabilizedCtx, logging.logger)
|
|
209
214
|
installSettingsRc8ClientLifecycle(stabilizedCtx)
|
|
210
215
|
const ownershipCtx = batchAttachmentHost ? protectHostProviderOwnership(stabilizedCtx) : stabilizedCtx
|
|
@@ -230,8 +235,15 @@ export function apply(ctx, config = {}) {
|
|
|
230
235
|
// actual tool registrations and pre-step listener. It makes bootstrap
|
|
231
236
|
// one-shot, enforces fast/standard/deep/custom quotas, tracks mixed branches,
|
|
232
237
|
// rejects empty/non-evidence results, and applies the optional turn deadline
|
|
233
|
-
// only when the user explicitly configures one.
|
|
234
|
-
|
|
238
|
+
// only when the user explicitly configures one. The diagnostic observer sits
|
|
239
|
+
// immediately inside it so it can inspect the final budget result/guard while
|
|
240
|
+
// leaving #220/#295 wall-clock semantics untouched.
|
|
241
|
+
const limitDiagnosticCtx = installVisionLimitDiagnostics(
|
|
242
|
+
legacyCoreCompat.ctx,
|
|
243
|
+
legacyCoreCompat.config,
|
|
244
|
+
logging.logger,
|
|
245
|
+
)
|
|
246
|
+
const structuredCtx = installStructuredFlowHardening(limitDiagnosticCtx, legacyCoreCompat.config)
|
|
235
247
|
const backgroundProfiling = installBackgroundCapabilityProfiling(
|
|
236
248
|
structuredCtx,
|
|
237
249
|
runtimeConfig,
|
|
@@ -369,4 +381,4 @@ export function apply(ctx, config = {}) {
|
|
|
369
381
|
)
|
|
370
382
|
throw error
|
|
371
383
|
}
|
|
372
|
-
}
|
|
384
|
+
}
|
package/lib/doctor-cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { realpathSync } from 'node:fs'
|
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { doctorProfiles, resolveDshHome } from './doctor.js'
|
|
6
6
|
import { inspectPlatform, probeRuntime, supportReport } from './doctor-runtime.js'
|
|
7
|
+
import { inspectDoctorVisionLimits, formatDoctorVisionLimits } from './doctor-vision-limits.js'
|
|
7
8
|
import { repairLegacySessionLogs } from './legacy-session-repair.js'
|
|
8
9
|
|
|
9
10
|
function usage() {
|
|
@@ -206,6 +207,7 @@ export async function run(argv = process.argv.slice(2), io = console, env = proc
|
|
|
206
207
|
.filter((item) => item.installation?.applicable && item.profileDir)
|
|
207
208
|
.map((item) => item.profileDir)
|
|
208
209
|
const platform = inspectPlatform({ profileDirs: applicableDirs })
|
|
210
|
+
const visionLimits = inspectDoctorVisionLimits(profileReport.log?.file)
|
|
209
211
|
let sessions
|
|
210
212
|
if (options.sessions) sessions = repairLegacySessionLogs({ dshHome, fix: false })
|
|
211
213
|
|
|
@@ -215,6 +217,7 @@ export async function run(argv = process.argv.slice(2), io = console, env = proc
|
|
|
215
217
|
|
|
216
218
|
if (options.json) {
|
|
217
219
|
const report = supportReport({ profileReport, runtime, platform, sessions })
|
|
220
|
+
report.visionLimits = visionLimits
|
|
218
221
|
report.ok = overallOk
|
|
219
222
|
io.log(JSON.stringify(report, null, 2))
|
|
220
223
|
return overallOk ? 0 : 1
|
|
@@ -231,6 +234,10 @@ export async function run(argv = process.argv.slice(2), io = console, env = proc
|
|
|
231
234
|
if (profileReport.log?.historicalSettingsSaveFailures?.length > 0) {
|
|
232
235
|
io.log(`! Historical settings save failures exist (${profileReport.log.historicalSettingsSaveFailures.length}); none are attributed to the latest plugin start.`)
|
|
233
236
|
}
|
|
237
|
+
for (const line of formatDoctorVisionLimits(visionLimits)) {
|
|
238
|
+
if (line.startsWith('WARN:')) io.log(`! ${line.slice('WARN:'.length).trim()}`)
|
|
239
|
+
else io.log(`✓ ${line}`)
|
|
240
|
+
}
|
|
234
241
|
|
|
235
242
|
if (runtime) {
|
|
236
243
|
if (!runtime.reachable) io.log(`– Runtime probe: DSH not reachable at ${runtime.baseUrl}; offline checks still completed.`)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync, openSync, readSync, closeSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
const LIMIT_LINE = /vision-router: effective vision limits\s+taskTimeoutMs=(\d+)\s+taskSource=([^\s]+)\s+turnBudgetMs=(\d+)\s+turnSource=([^\s]+)/i
|
|
4
|
+
const EXHAUSTED_LINE = /vision-router: vision turn deadline exhausted\s+turn=([^\s]+)\s+budgetMs=(\d+)\s+elapsedMs=([^\s]+)/i
|
|
5
|
+
|
|
6
|
+
function readTail(file, maxBytes = 2 * 1024 * 1024) {
|
|
7
|
+
if (!file || !existsSync(file)) return ''
|
|
8
|
+
let fd
|
|
9
|
+
try {
|
|
10
|
+
const stat = statSync(file)
|
|
11
|
+
const size = Math.min(stat.size, maxBytes)
|
|
12
|
+
if (size <= 0) return ''
|
|
13
|
+
const buffer = Buffer.alloc(size)
|
|
14
|
+
fd = openSync(file, 'r')
|
|
15
|
+
readSync(fd, buffer, 0, size, Math.max(0, stat.size - size))
|
|
16
|
+
return buffer.toString('utf8')
|
|
17
|
+
} catch {
|
|
18
|
+
try { return readFileSync(file, 'utf8') } catch { return '' }
|
|
19
|
+
} finally {
|
|
20
|
+
if (fd !== undefined) closeSync(fd)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseVisionLimitLog(text) {
|
|
25
|
+
let latest
|
|
26
|
+
let latestExhaustion
|
|
27
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
28
|
+
const limit = LIMIT_LINE.exec(line)
|
|
29
|
+
if (limit) {
|
|
30
|
+
latest = {
|
|
31
|
+
taskTimeoutMs: Number(limit[1]),
|
|
32
|
+
taskSource: limit[2],
|
|
33
|
+
turnBudgetMs: Number(limit[3]),
|
|
34
|
+
turnSource: limit[4],
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const exhausted = EXHAUSTED_LINE.exec(line)
|
|
38
|
+
if (exhausted) {
|
|
39
|
+
latestExhaustion = {
|
|
40
|
+
turn: exhausted[1],
|
|
41
|
+
budgetMs: Number(exhausted[2]),
|
|
42
|
+
elapsedMs: exhausted[3] === 'unknown' ? undefined : Number(exhausted[3]),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!latest && !latestExhaustion) return undefined
|
|
47
|
+
return {
|
|
48
|
+
...latest,
|
|
49
|
+
latestExhaustion,
|
|
50
|
+
explicitTurnLimit: Number(latest?.turnBudgetMs) > 0,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function inspectDoctorVisionLimits(logFile) {
|
|
55
|
+
return parseVisionLimitLog(readTail(logFile))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatDoctorVisionLimits(limits) {
|
|
59
|
+
if (!limits) return []
|
|
60
|
+
const task = Number.isFinite(limits.taskTimeoutMs) ? `${limits.taskTimeoutMs / 1000}s` : 'unknown'
|
|
61
|
+
const turn = Number.isFinite(limits.turnBudgetMs)
|
|
62
|
+
? limits.turnBudgetMs === 0 ? 'unlimited' : `${limits.turnBudgetMs / 1000}s`
|
|
63
|
+
: 'unknown'
|
|
64
|
+
const lines = [
|
|
65
|
+
`Vision task timeout: ${task}${limits.taskSource ? ` (${limits.taskSource})` : ''}`,
|
|
66
|
+
`Vision turn deadline: ${turn}${limits.turnSource ? ` (${limits.turnSource})` : ''}`,
|
|
67
|
+
]
|
|
68
|
+
if (limits.explicitTurnLimit) {
|
|
69
|
+
lines.push('WARN: an explicit whole-turn vision deadline is active; v2 defaults to unlimited and long multi-image tasks may stop after this deadline.')
|
|
70
|
+
}
|
|
71
|
+
return lines
|
|
72
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { SETTINGS_NUMBER_META } from './settings-number-contract.js'
|
|
2
|
+
|
|
3
|
+
const CLIENT_MARK = 'data-vision-router-settings-limit-hardening'
|
|
4
|
+
const META = JSON.stringify(SETTINGS_NUMBER_META)
|
|
5
|
+
|
|
6
|
+
export const SETTINGS_LIMIT_CLIENT_PRELUDE = String.raw`(function(){
|
|
7
|
+
'use strict';
|
|
8
|
+
var TARGET='dsh-vision-router';
|
|
9
|
+
var SECTION_ID='vision-router';
|
|
10
|
+
var META=${META};
|
|
11
|
+
|
|
12
|
+
function obj(value){return value&&typeof value==='object'&&!Array.isArray(value)?value:{};}
|
|
13
|
+
function own(value,key){return !!value&&typeof value==='object'&&Object.prototype.hasOwnProperty.call(value,key);}
|
|
14
|
+
function validNumber(key,raw){var meta=META[key];if(!meta)return true;var n=Number(raw);if(!Number.isFinite(n)||!Number.isInteger(n)||n<meta.min||n>meta.max)return false;return (n-meta.min)%meta.step===0;}
|
|
15
|
+
function source(snapshot,key){var user=obj(snapshot&&snapshot.user),base=obj(snapshot&&snapshot.base);if(own(user,key))return ['用户设置','User override'];if(own(base,key))return ['Profile / Composition','Profile / Composition'];return ['默认','Default'];}
|
|
16
|
+
function seconds(value,unlimited){var n=Number(value);if(unlimited&&n===0)return ['不限制','Unlimited'];if(!Number.isFinite(n))return ['未知','Unknown'];return [String(n/1000)+' 秒',String(n/1000)+'s'];}
|
|
17
|
+
function zh(){try{return String(document.documentElement.lang||'').toLowerCase().startsWith('zh');}catch(_){return true;}}
|
|
18
|
+
function tx(pair){return zh()?pair[0]:pair[1];}
|
|
19
|
+
|
|
20
|
+
function wrapScope(scope){
|
|
21
|
+
if(!scope||typeof scope!=='object')return scope;
|
|
22
|
+
return new Proxy(scope,{get:function(target,property){
|
|
23
|
+
if(property==='set'){
|
|
24
|
+
var set=Reflect.get(target,property,target);if(typeof set!=='function')return set;
|
|
25
|
+
return function(key,value){
|
|
26
|
+
if(META[key]&&!validNumber(key,value)){
|
|
27
|
+
var meta=META[key],error=new Error('value must be an integer between '+meta.min+' and '+meta.max+' in steps of '+meta.step);
|
|
28
|
+
error.code='settings-client-validation';
|
|
29
|
+
return Promise.reject(error);
|
|
30
|
+
}
|
|
31
|
+
return set.call(target,key,value);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
var value=Reflect.get(target,property,target);return typeof value==='function'?value.bind(target):value;
|
|
35
|
+
}});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function textOf(React,node){
|
|
39
|
+
if(node===null||node===undefined||node===false)return '';
|
|
40
|
+
if(typeof node==='string'||typeof node==='number')return String(node);
|
|
41
|
+
if(!React.isValidElement||!React.isValidElement(node))return '';
|
|
42
|
+
var out='';React.Children.forEach(node.props&&node.props.children,function(child){out+=textOf(React,child)+' ';});return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function replacement(text){
|
|
46
|
+
var map={
|
|
47
|
+
'单个视觉任务':'单次识图任务超时',
|
|
48
|
+
'Single visual task':'Single vision-task timeout',
|
|
49
|
+
'包含该任务内部的重试和备用模型;不是每个后端各自一份。':'包含本次识图内部的重试和备用模型;最长 180 秒。它不会增加本轮视觉时间上限。',
|
|
50
|
+
'Includes retries and fallbacks inside the task; it is not a fresh budget per backend.':'Includes retries and fallbacks for this vision task; maximum 180s. It does not extend the whole-turn vision deadline.',
|
|
51
|
+
'整轮视觉工具上限':'首次识图后的整轮时间上限',
|
|
52
|
+
'Whole-turn vision-tool limit':'Whole-turn limit after first vision call',
|
|
53
|
+
'部分配置没有写入,未写入的修改已保留。':'Host 未接受或未持久化这项设置,修改已保留。请检查允许范围/步进,或确认 DSH 与 Vision Router 版本匹配。',
|
|
54
|
+
'Some settings were not written; unwritten changes were kept.':'The Host did not accept or persist this setting. The edit was kept; check the allowed range/step and DSH/Vision Router version compatibility.'
|
|
55
|
+
};
|
|
56
|
+
return Object.prototype.hasOwnProperty.call(map,text)?map[text]:text;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function transformTree(React,node){
|
|
60
|
+
if(typeof node==='string')return replacement(node);
|
|
61
|
+
if(node===null||node===undefined||node===false||!React.isValidElement||!React.isValidElement(node))return node;
|
|
62
|
+
var originalText=textOf(React,node),props={};
|
|
63
|
+
if(node.props&&node.props.children!==undefined){props.children=React.Children.map(node.props.children,function(child){return transformTree(React,child);});}
|
|
64
|
+
if(node.type==='input'&&node.props&&node.props.type==='number'){
|
|
65
|
+
var min=Number(node.props.min),max=Number(node.props.max),match;
|
|
66
|
+
Object.keys(META).some(function(key){var meta=META[key];if(meta.min===min&&meta.max===max){match=meta;return true;}return false;});
|
|
67
|
+
if(match)props.step=match.step;
|
|
68
|
+
}
|
|
69
|
+
var next=React.cloneElement(node,props);
|
|
70
|
+
if(node.type==='div'&&node.props&&String(node.props.className||'').includes('vr-field')&&originalText.includes('整轮视觉工具上限')){
|
|
71
|
+
var hint=React.createElement('p',{className:'vr-hint',key:'issue307-budget-hint'},zh()
|
|
72
|
+
?'从本轮第一次实际 Vision Router 识图调用开始按墙钟计时;模型思考和两次识图之间的等待也计入。长任务建议保持“不限制”。'
|
|
73
|
+
:'Wall-clock time starts at the first actual Vision Router vision call; model reasoning and waits between calls also count. Keep Unlimited for long tasks.');
|
|
74
|
+
var children=React.Children.toArray(next.props.children);return React.cloneElement(next,{children:children.concat([hint])});
|
|
75
|
+
}
|
|
76
|
+
return next;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function diagnosticsCard(React,snapshot){
|
|
80
|
+
var value=obj(snapshot&&snapshot.value),task=seconds(value.visionTaskTimeoutMs===undefined?120000:value.visionTaskTimeoutMs,false),budget=seconds(value.visionTurnBudgetMs===undefined?0:value.visionTurnBudgetMs,true),taskSource=source(snapshot,'visionTaskTimeoutMs'),budgetSource=source(snapshot,'visionTurnBudgetMs');
|
|
81
|
+
function row(label,valueText){return React.createElement('div',{className:'vr-ia-diag-row'},React.createElement('span',null,label),React.createElement('strong',null,valueText));}
|
|
82
|
+
return React.createElement('div',{className:'vr-card vr-card-open vr-ia-card','data-vr-limit-diagnostics':'1'},React.createElement('div',{className:'vr-body',style:{borderTop:0}},[
|
|
83
|
+
row(zh()?'单次识图任务超时':'Single vision-task timeout',tx(task)+' · '+tx(taskSource)),
|
|
84
|
+
row(zh()?'首次识图后的整轮时间上限':'Whole-turn limit after first vision call',tx(budget)+' · '+tx(budgetSource)),
|
|
85
|
+
Number(value.visionTurnBudgetMs)>0?React.createElement('p',{className:'vr-hint',key:'warning'},zh()?'当前启用了显式整轮视觉时间上限;v2 默认是不限制,长任务可能在该时间后停止继续识图。':'An explicit whole-turn vision limit is active. v2 defaults to Unlimited; long tasks may stop making vision calls after this time.'):null
|
|
86
|
+
]));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function wrapSlots(slots,React){if(!slots||(typeof slots!=='object'&&typeof slots!=='function'))return slots;return new Proxy(slots,{get:function(target,property){if(property==='register'){var register=Reflect.get(target,property,target);if(typeof register!=='function')return register;return function(options,component){var args=Array.prototype.slice.call(arguments);if(options&&options.name==='settings.section'&&options.id===SECTION_ID&&typeof component==='function'){var Original=component;args[1]=function VisionRouterIssue307SettingsBoundary(props){var scope=wrapScope(props&&props.scope),nextProps=Object.assign({},props,{scope:scope}),tree=transformTree(React,Original(nextProps)),text=textOf(React,tree),snapshot;try{snapshot=scope&&scope.getSnapshot?scope.getSnapshot():undefined;}catch(_){snapshot=undefined;}if(text.includes('设置协议')||text.includes('Settings contract'))return React.createElement(React.Fragment,null,tree,diagnosticsCard(React,snapshot));return tree;};}return register.apply(target,args);};}var value=Reflect.get(target,property,target);return typeof value==='function'?value.bind(target):value;}});}
|
|
90
|
+
function wrapContext(ctx,React){if(!ctx||typeof ctx!=='object')return ctx;var slots=wrapSlots(ctx.slots,React);return new Proxy(ctx,{get:function(target,property){if(property==='slots')return slots;var value=Reflect.get(target,property,target);return typeof value==='function'?value.bind(target):value;}});}
|
|
91
|
+
function patchLoader(loader){if(!loader||(typeof loader!=='object'&&typeof loader!=='function'))return;if(typeof loader.load==='function'&&!loader.load.__visionRouterLimitHardening){var original=loader.load;function load(spec){if(spec&&spec.id===TARGET&&typeof spec.factory==='function'){var factory=spec.factory;spec=Object.assign({},spec,{factory:function(require){var exports=factory(require),React;try{React=require('react');}catch(_){React=undefined;}if(React&&exports&&typeof exports.apply==='function'&&!exports.apply.__visionRouterLimitHardening){var apply=exports.apply;var wrappedApply=function(ctx){var rest=Array.prototype.slice.call(arguments,1);return apply.apply(exports,[wrapContext(ctx,React)].concat(rest));};Object.defineProperty(wrappedApply,'__visionRouterLimitHardening',{value:true});exports.apply=wrappedApply;}return exports;}});}return original.call(loader,spec);}Object.defineProperty(load,'__visionRouterLimitHardening',{value:true});loader.load=load;}}
|
|
92
|
+
function install(){if(window.__ModuleLoader__){patchLoader(window.__ModuleLoader__);return;}var descriptor=Object.getOwnPropertyDescriptor(window,'__ModuleLoader__');if(descriptor&&descriptor.configurable===false)return;var stored;Object.defineProperty(window,'__ModuleLoader__',{configurable:true,enumerable:true,get:function(){return stored;},set:function(value){stored=value;patchLoader(value);Object.defineProperty(window,'__ModuleLoader__',{configurable:true,enumerable:true,writable:true,value:stored});}});}
|
|
93
|
+
try{install();}catch(_){}
|
|
94
|
+
})();`
|
|
95
|
+
|
|
96
|
+
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)}`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
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
|
+
)
|
|
109
|
+
})
|
|
110
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const SETTINGS_NUMBER_META = Object.freeze({
|
|
2
|
+
timeoutMs: Object.freeze({ min: 1000, max: 600000, step: 1 }),
|
|
3
|
+
visionTaskTimeoutMs: Object.freeze({ min: 1000, max: 180000, step: 1000 }),
|
|
4
|
+
ocrTimeoutMs: Object.freeze({ min: 1000, max: 120000, step: 1 }),
|
|
5
|
+
downscaleMaxPixels: Object.freeze({ min: 1000, max: 100000000, step: 1 }),
|
|
6
|
+
cacheTtlSeconds: Object.freeze({ min: 0, max: 31536000, step: 1 }),
|
|
7
|
+
cacheMaxEntries: Object.freeze({ min: 1, max: 100000, step: 1 }),
|
|
8
|
+
visionDepthMaxCalls: Object.freeze({ min: 0, max: 100, step: 1 }),
|
|
9
|
+
visionTurnBudgetMs: Object.freeze({ min: 0, max: 600000, step: 1000 }),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export function parseSettingsNumber(key, raw, { allowClear = false } = {}) {
|
|
13
|
+
const meta = SETTINGS_NUMBER_META[key]
|
|
14
|
+
if (!meta) return undefined
|
|
15
|
+
if (raw === '' || raw === null || raw === undefined) return allowClear ? { clear: true } : undefined
|
|
16
|
+
const value = Number(raw)
|
|
17
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) return undefined
|
|
18
|
+
if (value < meta.min || value > meta.max) return undefined
|
|
19
|
+
if ((value - meta.min) % meta.step !== 0) return undefined
|
|
20
|
+
return { value }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatDurationMs(value, { unlimitedZero = false } = {}) {
|
|
24
|
+
const number = Number(value)
|
|
25
|
+
if (!Number.isFinite(number)) return undefined
|
|
26
|
+
if (unlimitedZero && number === 0) return 'unlimited'
|
|
27
|
+
if (number % 1000 === 0) return `${number / 1000}s`
|
|
28
|
+
return `${number}ms`
|
|
29
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
const SETTINGS_NS = 'vision-router'
|
|
2
|
+
const TURN_BUDGET_CODE = 'VISION_TURN_BUDGET_EXCEEDED'
|
|
3
|
+
const GUARD_PREFIX = 'vision-router-structured-guard-stop-'
|
|
4
|
+
|
|
5
|
+
function isObject(value) {
|
|
6
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function hasOwn(value, key) {
|
|
10
|
+
return isObject(value) && Object.prototype.hasOwnProperty.call(value, key)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseResult(value) {
|
|
14
|
+
if (isObject(value)) return value
|
|
15
|
+
if (typeof value !== 'string' || value.trim() === '') return undefined
|
|
16
|
+
try { return JSON.parse(value) } catch { return undefined }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function settingsService(ctx) {
|
|
20
|
+
try {
|
|
21
|
+
const settings = ctx?.get?.('settings')
|
|
22
|
+
return settings && typeof settings === 'object' ? settings : undefined
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sourceFor(descriptor, key) {
|
|
29
|
+
if (hasOwn(descriptor?.user, key)) return 'user'
|
|
30
|
+
if (hasOwn(descriptor?.base, key)) return 'composition'
|
|
31
|
+
return 'default'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveVisionLimitDiagnostics(ctx, fallbackConfig = {}) {
|
|
35
|
+
const settings = settingsService(ctx)
|
|
36
|
+
let value
|
|
37
|
+
let descriptor
|
|
38
|
+
try { value = settings?.get?.(SETTINGS_NS) } catch { value = undefined }
|
|
39
|
+
try {
|
|
40
|
+
const described = settings?.describe?.({ redactSecrets: true })
|
|
41
|
+
descriptor = Array.isArray(described)
|
|
42
|
+
? described.find((entry) => entry?.ns === SETTINGS_NS)
|
|
43
|
+
: undefined
|
|
44
|
+
} catch {
|
|
45
|
+
descriptor = undefined
|
|
46
|
+
}
|
|
47
|
+
const effective = isObject(value) ? value : isObject(descriptor?.value) ? descriptor.value : fallbackConfig
|
|
48
|
+
const taskTimeoutMs = Number.isFinite(Number(effective?.visionTaskTimeoutMs))
|
|
49
|
+
? Number(effective.visionTaskTimeoutMs)
|
|
50
|
+
: 120000
|
|
51
|
+
const turnBudgetMs = Number.isFinite(Number(effective?.visionTurnBudgetMs))
|
|
52
|
+
? Number(effective.visionTurnBudgetMs)
|
|
53
|
+
: 0
|
|
54
|
+
return {
|
|
55
|
+
taskTimeoutMs,
|
|
56
|
+
turnBudgetMs,
|
|
57
|
+
taskSource: descriptor ? sourceFor(descriptor, 'visionTaskTimeoutMs') : 'unknown',
|
|
58
|
+
turnSource: descriptor ? sourceFor(descriptor, 'visionTurnBudgetMs') : 'unknown',
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function formatVisionTurnGuard(turnBudgetMs) {
|
|
63
|
+
const ms = Number(turnBudgetMs)
|
|
64
|
+
const seconds = Number.isFinite(ms) && ms > 0 ? Math.round(ms / 1000) : undefined
|
|
65
|
+
const limit = seconds === undefined ? '' : `(${seconds} 秒)`
|
|
66
|
+
return `Vision Router 本轮视觉时间上限${limit}已耗尽。不要再调用视觉工具;请基于已经获得的证据作答,并明确仍存在的不确定性。默认配置为“不限制”;长任务如需继续识图,请检查“设置 → Vision Router → 高级 → 首次识图后的整轮时间上限”。`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function replaceGuardText(decision, turnBudgetMs) {
|
|
70
|
+
if (!isObject(decision) || !Array.isArray(decision.messages)) return decision
|
|
71
|
+
let anyChanged = false
|
|
72
|
+
const messages = decision.messages.map((message) => {
|
|
73
|
+
if (!isObject(message) || typeof message.id !== 'string' || !message.id.startsWith(GUARD_PREFIX)) return message
|
|
74
|
+
if (!Array.isArray(message.content)) return message
|
|
75
|
+
let messageChanged = false
|
|
76
|
+
const content = message.content.map((block) => {
|
|
77
|
+
if (!isObject(block) || block.type !== 'text' || typeof block.text !== 'string') return block
|
|
78
|
+
if (!block.text.includes('本轮视觉总时间预算已耗尽')) return block
|
|
79
|
+
messageChanged = true
|
|
80
|
+
anyChanged = true
|
|
81
|
+
return { ...block, text: formatVisionTurnGuard(turnBudgetMs) }
|
|
82
|
+
})
|
|
83
|
+
return messageChanged ? { ...message, content } : message
|
|
84
|
+
})
|
|
85
|
+
return anyChanged ? { ...decision, messages } : decision
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function installVisionLimitDiagnostics(ctx, fallbackConfig = {}, logger) {
|
|
89
|
+
if (!ctx || typeof ctx !== 'object') return ctx
|
|
90
|
+
const turnState = new WeakMap()
|
|
91
|
+
let startupLogged = false
|
|
92
|
+
|
|
93
|
+
const snapshot = () => resolveVisionLimitDiagnostics(ctx, fallbackConfig)
|
|
94
|
+
const logStartup = () => {
|
|
95
|
+
if (startupLogged) return
|
|
96
|
+
const limits = snapshot()
|
|
97
|
+
startupLogged = true
|
|
98
|
+
logger?.info?.(
|
|
99
|
+
'vision-router: effective vision limits taskTimeoutMs=%d taskSource=%s turnBudgetMs=%d turnSource=%s',
|
|
100
|
+
limits.taskTimeoutMs,
|
|
101
|
+
limits.taskSource,
|
|
102
|
+
limits.turnBudgetMs,
|
|
103
|
+
limits.turnSource,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
ctx.inject?.(['settings'], (settingsCtx) => {
|
|
109
|
+
settingsCtx.effect?.(() => {
|
|
110
|
+
queueMicrotask(logStartup)
|
|
111
|
+
return () => {}
|
|
112
|
+
}, 'vision-router: report effective vision limits')
|
|
113
|
+
})
|
|
114
|
+
} catch {
|
|
115
|
+
// Diagnostics are best-effort and must never block plugin startup.
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const stateForVisualCall = (session, turnHint) => {
|
|
119
|
+
if (!session) return undefined
|
|
120
|
+
let state = turnState.get(session)
|
|
121
|
+
if (!state || (turnHint !== undefined && state.turn !== turnHint)) {
|
|
122
|
+
state = { turn: turnHint, startedAt: undefined, exhaustionLogged: false }
|
|
123
|
+
turnState.set(session, state)
|
|
124
|
+
}
|
|
125
|
+
if (state.startedAt === undefined) state.startedAt = Date.now()
|
|
126
|
+
return state
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const wrapTool = (def) => {
|
|
130
|
+
if (!def || typeof def.execute !== 'function') return def
|
|
131
|
+
if (def.name !== 'vision_bootstrap' && !String(def.name || '').startsWith('vision_')) return def
|
|
132
|
+
return {
|
|
133
|
+
...def,
|
|
134
|
+
async execute(args, exec) {
|
|
135
|
+
const session = exec?.agent?.session
|
|
136
|
+
const state = stateForVisualCall(session, exec?.agent?.turn ?? exec?.turn)
|
|
137
|
+
const result = await def.execute(args, exec)
|
|
138
|
+
const parsed = parseResult(result)
|
|
139
|
+
if (parsed?.code !== TURN_BUDGET_CODE) return result
|
|
140
|
+
if (state?.exhaustionLogged) return result
|
|
141
|
+
if (state) state.exhaustionLogged = true
|
|
142
|
+
const limits = snapshot()
|
|
143
|
+
const elapsedMs = state?.startedAt === undefined
|
|
144
|
+
? undefined
|
|
145
|
+
: Math.max(0, Date.now() - state.startedAt)
|
|
146
|
+
logger?.warn?.(
|
|
147
|
+
'vision-router: vision turn deadline exhausted turn=%s budgetMs=%d elapsedMs=%s',
|
|
148
|
+
state?.turn === undefined ? 'unknown' : String(state.turn),
|
|
149
|
+
limits.turnBudgetMs,
|
|
150
|
+
elapsedMs === undefined ? 'unknown' : String(elapsedMs),
|
|
151
|
+
)
|
|
152
|
+
return result
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return new Proxy(ctx, {
|
|
158
|
+
get(target, property) {
|
|
159
|
+
if (property === 'tools') {
|
|
160
|
+
const tools = target.tools
|
|
161
|
+
if (!tools || typeof tools !== 'object') return tools
|
|
162
|
+
return new Proxy(tools, {
|
|
163
|
+
get(toolTarget, toolProperty) {
|
|
164
|
+
if (toolProperty !== 'register') {
|
|
165
|
+
const value = Reflect.get(toolTarget, toolProperty, toolTarget)
|
|
166
|
+
return typeof value === 'function' ? value.bind(toolTarget) : value
|
|
167
|
+
}
|
|
168
|
+
const register = Reflect.get(toolTarget, toolProperty, toolTarget)
|
|
169
|
+
return (def) => register.call(toolTarget, wrapTool(def))
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
if (property === 'on') {
|
|
174
|
+
const on = Reflect.get(target, property, target)
|
|
175
|
+
if (typeof on !== 'function') return on
|
|
176
|
+
return (event, handler, ...rest) => {
|
|
177
|
+
if (event !== 'agent/pre-step' || typeof handler !== 'function') {
|
|
178
|
+
return on.call(target, event, handler, ...rest)
|
|
179
|
+
}
|
|
180
|
+
return on.call(target, event, async function issue307GuardDiagnostics(payload, next) {
|
|
181
|
+
const session = payload?.agent?.session
|
|
182
|
+
if (session) {
|
|
183
|
+
const current = turnState.get(session)
|
|
184
|
+
if (!current || current.turn !== payload?.turn) {
|
|
185
|
+
turnState.set(session, {
|
|
186
|
+
turn: payload?.turn,
|
|
187
|
+
startedAt: undefined,
|
|
188
|
+
exhaustionLogged: false,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const decision = await handler.call(this, payload, next)
|
|
193
|
+
const limits = snapshot()
|
|
194
|
+
return replaceGuardText(decision, limits.turnBudgetMs)
|
|
195
|
+
}, ...rest)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const value = Reflect.get(target, property, target)
|
|
199
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vision-router",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain (no key) + pixel-level vision tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots). One-command install, no Python, image turns work like ordinary tool-calling turns.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"sharp": "^0.35.3"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
|
72
|
-
"test": "node --test tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/vision-breaker-readonly.test.js tests/client.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/update-check.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/doctor-v2.test.js tests/doctor-cli-v2.test.js tests/doctor-runtime.test.js tests/session-repair-v2.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/replay-delegation.test.js tests/adversarial-hardening.test.js tests/vision-adversarial-hardening-v2.test.js tests/vision-capability-benchmark-visual-proof.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/local-ollama.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-compat.test.js tests/native-image-coexistence.test.js tests/issue-276-session-image-ownership.test.js tests/issue-276-entry-policy-bridge.test.js tests/issue-276-policy-hardening.test.js tests/issue-289-native-nonintervention.test.js tests/pi-ai-bridge-wire-compat.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/image-resource-governor.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js tests/vision-routing-product.test.js tests/vision-runtime-performance.test.js tests/vision-routing-settings-prelude.test.js tests/v2-settings-ia-integration.test.js tests/vision-capability-router.test.js tests/vision-capability-reference.test.js tests/vision-capability-benchmark.test.js tests/vision-capability-grounding-proof-contract.test.js tests/vision-capability-probe.test.js tests/vision-capability-shadow.test.js tests/vision-capability-axis-freshness.test.js tests/vision-capability-shadow-health.test.js tests/vision-capability-benchmark-service.test.js tests/vision-capability-preflight-hardening.test.js tests/vision-capability-benchmark-contract.test.js tests/vision-capability-benchmark-client.test.js tests/vision-capability-benchmark-client-productization.test.js tests/vision-capability-adapter-route.test.js tests/vision-background-benchmark.test.js tests/vision-background-benchmark-productization.test.js tests/vision-capability-benchmark-budget.test.js tests/vision-image-input-verdict.test.js tests/vision-capability-failure-classification.test.js tests/vision-capability-gate-client.test.js tests/v2-product-review-fixes.test.js tests/qa-top5-reliability.test.js tests/qa-screenshot-runtime.test.js tests/qa-runtime-identity.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js tests/qa-reliability-tail.test.js tests/qa-endpoint-route-alias.test.js tests/vision-toggle-root-hardening.test.js && node --test tests/vision-backend-runtime-policy.test.js"
|
|
72
|
+
"test": "node --test tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/vision-breaker-readonly.test.js tests/client.test.js tests/issue-307-regression.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/update-check.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/doctor-v2.test.js tests/doctor-cli-v2.test.js tests/doctor-runtime.test.js tests/session-repair-v2.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/replay-delegation.test.js tests/adversarial-hardening.test.js tests/vision-adversarial-hardening-v2.test.js tests/vision-capability-benchmark-visual-proof.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/local-ollama.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-compat.test.js tests/native-image-coexistence.test.js tests/issue-276-session-image-ownership.test.js tests/issue-276-entry-policy-bridge.test.js tests/issue-276-policy-hardening.test.js tests/issue-289-native-nonintervention.test.js tests/pi-ai-bridge-wire-compat.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/image-resource-governor.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js tests/vision-routing-product.test.js tests/vision-runtime-performance.test.js tests/vision-routing-settings-prelude.test.js tests/v2-settings-ia-integration.test.js tests/vision-capability-router.test.js tests/vision-capability-reference.test.js tests/vision-capability-benchmark.test.js tests/vision-capability-grounding-proof-contract.test.js tests/vision-capability-probe.test.js tests/vision-capability-shadow.test.js tests/vision-capability-axis-freshness.test.js tests/vision-capability-shadow-health.test.js tests/vision-capability-benchmark-service.test.js tests/vision-capability-preflight-hardening.test.js tests/vision-capability-benchmark-contract.test.js tests/vision-capability-benchmark-client.test.js tests/vision-capability-benchmark-client-productization.test.js tests/vision-capability-adapter-route.test.js tests/vision-background-benchmark.test.js tests/vision-background-benchmark-productization.test.js tests/vision-capability-benchmark-budget.test.js tests/vision-image-input-verdict.test.js tests/vision-capability-failure-classification.test.js tests/vision-capability-gate-client.test.js tests/v2-product-review-fixes.test.js tests/qa-top5-reliability.test.js tests/qa-screenshot-runtime.test.js tests/qa-runtime-identity.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js tests/qa-reliability-tail.test.js tests/qa-endpoint-route-alias.test.js tests/vision-toggle-root-hardening.test.js && node --test tests/vision-backend-runtime-policy.test.js"
|
|
73
73
|
},
|
|
74
74
|
"pnpm": {
|
|
75
75
|
"onlyBuiltDependencies": ["sharp"]
|