dsh-vision-router 2.1.2 → 2.1.4
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 +1 -1
- package/README.zh.md +1 -1
- package/docs/architecture/compat-inventory.md +6 -6
- package/docs/architecture/dsh-compatibility-matrix.md +7 -6
- package/docs/architecture/dsh-support-window.md +36 -16
- package/docs/architecture/p3-compat-retirement.md +7 -5
- package/docs/architecture/p3-host-native-seams.md +3 -1
- package/docs/doctor.md +4 -1
- package/docs/releases/v2.1.3.md +34 -0
- package/docs/releases/v2.1.4.md +34 -0
- package/docs/v2-capability-routing.md +20 -11
- package/index.js +156 -129
- package/lib/catalog-corrections.js +2 -0
- package/lib/client-presentation-boundary-main.js +478 -2
- package/lib/client-presentation-boundary.js +121 -1
- package/lib/client.js +4 -4
- package/lib/depth-guidance.js +4 -4
- package/lib/doctor-cli-p0.js +3 -1
- package/lib/doctor-cli.js +4 -1
- package/lib/doctor-runtime.js +8 -3
- package/lib/dsh-support-window.js +15 -7
- package/lib/live-model-discovery.js +20 -13
- package/lib/local-vision-stabilizer.js +1 -1
- package/lib/mixed-router.js +8 -8
- package/lib/pi-ai-bridge-wire-compat.js +69 -10
- package/lib/runtime-i18n-boundary.js +27 -2
- package/lib/runtime-i18n.js +6 -6
- package/lib/session-affinity-runtime.js +104 -0
- package/lib/session-affinity.js +93 -0
- package/lib/settings-ia-client-prelude.js +1 -1
- package/lib/structured-bootstrap.js +2 -2
- package/lib/structured-flow-hardening.js +159 -158
- package/lib/tesseract-exec-compat.js +9 -36
- package/lib/vision-backend-runtime-policy.js +1 -0
- package/lib/vision-background-benchmark.js +79 -52
- package/lib/vision-background-failure-policy.js +1 -24
- package/lib/vision-background-stop-store.js +10 -13
- package/lib/vision-capability-benchmark-service.js +14 -2
- package/lib/vision-tool-runtime-boundary.js +20 -3
- package/lib/windows-desktop-capture.js +247 -0
- package/package.json +5 -5
- package/lib/windows-screenshot-dpi-compat.js +0 -148
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const MAX_SESSION_AFFINITY_ID_LENGTH = 512
|
|
2
|
+
|
|
3
|
+
/** Preserve DSH's in-process session identity exactly; no wire rules belong here. */
|
|
4
|
+
export function rawSessionIdentity(value) {
|
|
5
|
+
if (value === undefined || value === null) return undefined
|
|
6
|
+
try {
|
|
7
|
+
return String(value)
|
|
8
|
+
} catch {
|
|
9
|
+
return undefined
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Read the live DSH session id across released/alpha session shapes without rewriting it. */
|
|
14
|
+
export function sessionIdentityOf(session) {
|
|
15
|
+
if (!session) return undefined
|
|
16
|
+
try {
|
|
17
|
+
const direct = rawSessionIdentity(session.id)
|
|
18
|
+
if (direct !== undefined) return direct
|
|
19
|
+
} catch {
|
|
20
|
+
/* continue to persisted/header shapes */
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const header = rawSessionIdentity(session.header?.id)
|
|
24
|
+
if (header !== undefined) return header
|
|
25
|
+
} catch {
|
|
26
|
+
/* continue to requestHeader() */
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
if (typeof session.requestHeader === 'function') {
|
|
30
|
+
return rawSessionIdentity(session.requestHeader()?.id)
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
/* no usable session identity */
|
|
34
|
+
}
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Admit an opaque identity to an HTTP header without changing it. Fetch/undici
|
|
40
|
+
* requires ByteString-compatible header values; use visible ASCII so behavior
|
|
41
|
+
* is identical across Node 22/24 and HTTP implementations. Leading/trailing
|
|
42
|
+
* whitespace is rejected because Headers would normalize it, violating the
|
|
43
|
+
* requirement to preserve the DSH identity unchanged.
|
|
44
|
+
*/
|
|
45
|
+
export function wireSessionAffinityId(value) {
|
|
46
|
+
const text = rawSessionIdentity(value)
|
|
47
|
+
if (text === undefined || text === '' || text.length > MAX_SESSION_AFFINITY_ID_LENGTH) return undefined
|
|
48
|
+
if (text.trim() !== text) return undefined
|
|
49
|
+
if (!/^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?$/.test(text)) return undefined
|
|
50
|
+
return text
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isOfficialOpenCodeGoUrl(value) {
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(String(value))
|
|
56
|
+
return (
|
|
57
|
+
url.protocol === 'https:' &&
|
|
58
|
+
url.hostname.toLowerCase() === 'opencode.ai' &&
|
|
59
|
+
/^\/zen\/go(?:\/|$)/i.test(url.pathname)
|
|
60
|
+
)
|
|
61
|
+
} catch {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isOpenCodeGoProvider(provider) {
|
|
67
|
+
return isOfficialOpenCodeGoUrl(provider?.baseURL ?? provider?.baseUrl ?? '')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function invalidAffinityError(sessionId) {
|
|
71
|
+
const raw = rawSessionIdentity(sessionId)
|
|
72
|
+
const error = new Error(
|
|
73
|
+
raw === undefined || raw === ''
|
|
74
|
+
? 'OpenCode Go requires a stable session identity; refusing a direct request without one'
|
|
75
|
+
: 'OpenCode Go session identity is not safe for an HTTP header; refusing to rewrite or truncate it',
|
|
76
|
+
)
|
|
77
|
+
error.code = raw === undefined || raw === '' ? 'OPENCODE_SESSION_REQUIRED' : 'OPENCODE_SESSION_INVALID'
|
|
78
|
+
return error
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function directSessionAffinityHeaders(provider, sessionId) {
|
|
82
|
+
if (!isOpenCodeGoProvider(provider)) return {}
|
|
83
|
+
const wire = wireSessionAffinityId(sessionId)
|
|
84
|
+
if (wire === undefined) throw invalidAffinityError(sessionId)
|
|
85
|
+
return { 'x-opencode-session': wire }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function openCodeSessionAffinityHeaderForUrl(url, sessionId) {
|
|
89
|
+
if (!isOfficialOpenCodeGoUrl(url)) return undefined
|
|
90
|
+
const wire = wireSessionAffinityId(sessionId)
|
|
91
|
+
if (wire === undefined) throw invalidAffinityError(sessionId)
|
|
92
|
+
return wire
|
|
93
|
+
}
|
|
@@ -258,7 +258,7 @@ export const SETTINGS_IA_CLIENT_PRELUDE = String.raw`(function(){
|
|
|
258
258
|
function localPageContent(){if(!local)return h(React.Fragment,null,title(tx('本地与设备','Local & device')),card([h('p',{className:'vr-hint',key:'remote'},tx('本地视觉后端和桌面截图只能在运行 DSH 的机器上配置。','Local vision backends and desktop capture can only be configured on the DSH machine.'))]));return h(React.Fragment,null,title(tx('本地与设备','Local & device'),tx('本地运行视觉模型可减少 API 费用和图片上传。','Run vision locally to reduce API cost and image uploads.')),localProviderCard('localOllama','Ollama',{enabled:false,baseURL:'http://127.0.0.1:11434/v1',model:'qwen2.5vl',format:'openai'},'ollama'),localProviderCard('localLmStudio','LM Studio',{enabled:false,baseURL:'http://localhost:1234/v1',model:'',format:'openai'},'lmstudio'),card([toggle('desktopScreenshot',tx('允许 Agent 读取桌面截图','Allow the agent to capture the desktop'),tx('这是独立的隐私权限;macOS 上保存为开启后会立即触发屏幕录制权限检查。','This is a separate privacy permission; on macOS saving it enabled immediately triggers the screen-recording permission check.'))]));}
|
|
259
259
|
function wrappersEditor(){var rows=wrapperRows();return h('div',{className:'vr-field'},fieldHead('wrappedProviders',tx('哪些聊天模型可以开启识图','Which chat models can use Vision mode')),h('p',{className:'vr-hint'},tx('通常无需修改;模型留空表示整个 Provider。','Usually leave this alone; an empty model means the whole provider.')),rows.map(function(row,index){return modelRow(row,index,rows,setWrapperDraft,true);}),h('button',{type:'button',className:'vr-btn',disabled:!writable||saving,onClick:function(){setWrapperDraft(rows.concat([{provider:'',model:''}]))}},tx('+ 添加范围','+ Add scope')));}
|
|
260
260
|
function textProviderEditor(){var current=Object.assign({provider:'',model:''},obj(value('textProvider',{}))),ready=groups.length>0;return h('div',{className:'vr-field'},fieldHead('textProvider',tx('文字回退模型','Text fallback model')),h('div',{className:'vr-chain-row'},ready?h('select',{className:'vr-input',value:current.provider||'',disabled:!writable||saving,onChange:function(event){setValue('textProvider',{provider:event.target.value,model:''});}},providerOptions(current.provider||'')):h('input',{className:'vr-input',value:current.provider||'',disabled:!writable||saving,onChange:function(event){setValue('textProvider',{provider:event.target.value,model:current.model||''});}}),ready?h('select',{className:'vr-input',value:current.model||'',disabled:!writable||saving||!current.provider,onChange:function(event){setValue('textProvider',{provider:current.provider||'',model:event.target.value});}},modelOptions(current.provider||'',current.model||'',false)):h('input',{className:'vr-input',value:current.model||'',disabled:!writable||saving,onChange:function(event){setValue('textProvider',{provider:current.provider||'',model:event.target.value});}})),invalidKeys.includes('textProvider')?h('p',{className:'vr-failed'},tx('Provider 和 model 必须同时填写,或同时留空恢复默认。','Provider and model must both be filled, or both left empty to restore the default.')):null);}
|
|
261
|
-
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:'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')),toggle('allowRemoteSettings',tx('允许可信 Host 远程修改设置','Allow trusted-host remote settings'),tx('默认关闭;trustedHosts 不是身份认证。','Off by default; trustedHosts is not authentication.')),textField('proxy',tx('代理地址','Proxy URL'),tx('留空关闭代理。','Leave empty to disable.')),textareaArray('proxyHosts',tx('走代理的域名','Proxied hosts'),tx('每行一个。','One per line.'))]):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')),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]));}
|
|
261
|
+
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:'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')),toggle('allowRemoteSettings',tx('允许可信 Host 远程修改设置','Allow trusted-host remote settings'),tx('默认关闭;trustedHosts 不是身份认证。','Off by default; trustedHosts is not authentication.')),textField('proxy',tx('代理地址','Proxy URL'),tx('留空关闭代理。','Leave empty to disable.')),textareaArray('proxyHosts',tx('走代理的域名','Proxied hosts'),tx('每行一个。','One per line.'))]):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
262
|
function diagnosticValue(label,valueText){return h('div',{className:'vr-ia-diag-row',key:label},h('span',null,label),h('strong',null,valueText));}
|
|
263
263
|
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
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('代理','Proxy'),local?(String(value('proxy','')).trim()?tx('已配置','Configured'):tx('未配置','Not configured')):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());}
|
|
@@ -32,7 +32,7 @@ export function structuredBootstrapQuestion() {
|
|
|
32
32
|
'The example above must remain valid JSON. For mixed_of: ONLY when visual_kind is "mixed", put 1-2 distinct values chosen from document, ui, code, chat, general; otherwise return an empty array. ' +
|
|
33
33
|
'Preserve high-information evidence for downstream reasoning: layout, reading order, objects/controls, relationships, visible state, and uncertainty. ' +
|
|
34
34
|
'Do not prematurely answer any possible user task and do not invent hidden details. ' +
|
|
35
|
-
'Recommend at least one concrete follow-up evidence
|
|
35
|
+
'Recommend at least one concrete task-independent follow-up evidence candidate; downstream agents must treat these as optional suggestions, not a task plan. Do NOT recommend vision_ocr merely because text is visible: use OCR only when exact verbatim transcription is genuinely uncertain or would materially add evidence; for UI/screenshots prefer vision_detect or a focused vision_describe when semantic verification is enough. ' +
|
|
36
36
|
'OCR is a verbatim transcriber, not a semantic reader: it is systematically unreliable for confusable glyphs (1/l, 0/O), spacing and line breaks, so context-aware semantic reading (vision_describe / vision_detect) usually recovers meaning that raw OCR corrupts. ' +
|
|
37
37
|
'Use vision_ocr ONLY when verbatim fidelity is genuinely required and cannot be recovered from context: executable code, long-form documents needing exact quotation, forms/contracts, table digits, CAPTCHAs, or glyphs with no semantic anchor. ' +
|
|
38
38
|
'When you do call vision_ocr, treat its output as evidence to verify against other tools, never as ground truth. ' +
|
|
@@ -107,7 +107,7 @@ export function normalizeStructuredBootstrapResult(parsed, raw = '') {
|
|
|
107
107
|
recommendedFollowups.push({
|
|
108
108
|
tool: visualKind === 'ui' ? 'vision_detect' : 'vision_describe',
|
|
109
109
|
target: 'the most uncertain or information-dense region from the structured baseline',
|
|
110
|
-
reason: '
|
|
110
|
+
reason: 'Task-independent candidate for the required x>=1 verification/deepening step; the downstream agent must still choose based on the user question and must not default to OCR without a verbatim-text need.',
|
|
111
111
|
})
|
|
112
112
|
}
|
|
113
113
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
@@ -20,9 +20,7 @@ const MAX_TURN_BUDGET_MS = 600_000
|
|
|
20
20
|
const MAX_GUIDANCE_OVERRIDES = 14
|
|
21
21
|
const MAX_GUIDANCE_CHARS = 2_000
|
|
22
22
|
const MAX_NON_PROGRESS_ATTEMPTS = 3
|
|
23
|
-
const MAX_EVIDENCE_SIGNATURE_CHARS = 4_096
|
|
24
23
|
const BUDGETED_TIMEOUT_FIELDS = ['timeoutMs', 'visionTaskTimeoutMs', 'ocrTimeoutMs']
|
|
25
|
-
const EVIDENCE_META_KEYS = new Set(['ok', 'code', 'retryable', 'reason', 'phase', 'next'])
|
|
26
24
|
const GUIDANCE_KINDS = new Set([
|
|
27
25
|
'code',
|
|
28
26
|
'document',
|
|
@@ -40,25 +38,6 @@ const GUIDANCE_KINDS = new Set([
|
|
|
40
38
|
'meme',
|
|
41
39
|
])
|
|
42
40
|
|
|
43
|
-
const BUILTIN_MIXED_GUIDANCE = {
|
|
44
|
-
zh: {
|
|
45
|
-
document: '语义优先;只有逐字引用、合同/表单字段或表格数字确需保真时才使用 OCR。',
|
|
46
|
-
ui: '优先用 vision_detect / vision_ground 验证元素、状态与位置。',
|
|
47
|
-
code: '逐字核对代码;对可执行字符、缩进和符号做交叉验证。',
|
|
48
|
-
chat: '按气泡顺序核对发言者、消息内容与时间/状态信息。',
|
|
49
|
-
general: '聚焦尚未归类的另一部分可见内容,自行选择最能新增证据的视觉工具。',
|
|
50
|
-
fallback: '聚焦这个分支新增或验证可见证据。',
|
|
51
|
-
},
|
|
52
|
-
en: {
|
|
53
|
-
document: 'Prioritize semantics; use OCR only when verbatim quotes, contract/form fields, or table numbers require exact transcription.',
|
|
54
|
-
ui: 'Prefer vision_detect / vision_ground to verify elements, states, and positions.',
|
|
55
|
-
code: 'Verify code verbatim; cross-check executable characters, indentation, and symbols.',
|
|
56
|
-
chat: 'Verify speakers, message text, and time/status information in bubble order.',
|
|
57
|
-
general: 'Focus on the remaining unclassified visible content and choose the vision tool that adds the most useful evidence.',
|
|
58
|
-
fallback: 'Add or verify visible evidence for this branch.',
|
|
59
|
-
},
|
|
60
|
-
}
|
|
61
|
-
|
|
62
41
|
function isObject(value) {
|
|
63
42
|
return value !== null && typeof value === 'object'
|
|
64
43
|
}
|
|
@@ -98,36 +77,143 @@ export function producedStructuredEvidence(value) {
|
|
|
98
77
|
return true
|
|
99
78
|
}
|
|
100
79
|
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
80
|
+
function finiteNumber(value) {
|
|
81
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function positiveInteger(value) {
|
|
85
|
+
return Number.isInteger(value) && value > 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function nonEmptyText(value) {
|
|
89
|
+
return typeof value === 'string' && value.trim() !== ''
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function canonicalBox(value, width, height) {
|
|
93
|
+
if (!isObject(value) || Array.isArray(value)) return false
|
|
94
|
+
const { x1, y1, x2, y2 } = value
|
|
95
|
+
if (![x1, y1, x2, y2].every(Number.isInteger)) return false
|
|
96
|
+
if (x1 < 0 || y1 < 0 || x2 <= x1 || y2 <= y1) return false
|
|
97
|
+
if (positiveInteger(width) && x2 > width) return false
|
|
98
|
+
if (positiveInteger(height) && y2 > height) return false
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function structuredDescribeEvidence(value) {
|
|
103
|
+
if (!isObject(value) || Array.isArray(value)) return false
|
|
104
|
+
if (nonEmptyText(value.summary) || nonEmptyText(value.text)) return true
|
|
105
|
+
if (Array.isArray(value.layout) && value.layout.some((row) => (
|
|
106
|
+
isObject(row) && nonEmptyText(row.region) && nonEmptyText(row.content)
|
|
107
|
+
))) return true
|
|
108
|
+
return Array.isArray(value.entities) && value.entities.some((entity) => (
|
|
109
|
+
isObject(entity) && nonEmptyText(entity.type) && nonEmptyText(entity.label)
|
|
110
|
+
))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function groundEvidence(value) {
|
|
114
|
+
if (!isObject(value) || Array.isArray(value)) return false
|
|
115
|
+
const { width, height } = value
|
|
116
|
+
return positiveInteger(width) && positiveInteger(height) && canonicalBox(value, width, height)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function detectEvidence(value) {
|
|
120
|
+
if (!isObject(value) || Array.isArray(value)) return false
|
|
121
|
+
const { width, height, elements } = value
|
|
122
|
+
if (!positiveInteger(width) || !positiveInteger(height) || !Array.isArray(elements)) return false
|
|
123
|
+
return elements.every((element, index) => (
|
|
124
|
+
isObject(element) &&
|
|
125
|
+
element.number === index + 1 &&
|
|
126
|
+
nonEmptyText(element.label) &&
|
|
127
|
+
canonicalBox(element.box, width, height)
|
|
110
128
|
))
|
|
111
129
|
}
|
|
112
130
|
|
|
131
|
+
function ocrEvidence(value) {
|
|
132
|
+
if (!isObject(value) || Array.isArray(value)) return false
|
|
133
|
+
if (value.engine !== 'tesseract' && value.engine !== 'vision') return false
|
|
134
|
+
return typeof value.text === 'string'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function colorEvidence(value) {
|
|
138
|
+
if (!Array.isArray(value)) return false
|
|
139
|
+
return value.every((entry) => (
|
|
140
|
+
isObject(entry) &&
|
|
141
|
+
typeof entry.hex === 'string' && /^#[0-9a-f]{6}$/i.test(entry.hex) &&
|
|
142
|
+
positiveInteger(entry.count) &&
|
|
143
|
+
finiteNumber(entry.share) && entry.share > 0 && entry.share <= 1
|
|
144
|
+
))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function pixelDiffEvidence(value) {
|
|
148
|
+
if (!isObject(value) || Array.isArray(value)) return false
|
|
149
|
+
const { width, height, differingPixels, totalPixels, diffRatio, worstRegions } = value
|
|
150
|
+
if (!positiveInteger(width) || !positiveInteger(height) || totalPixels !== width * height) return false
|
|
151
|
+
if (!Number.isInteger(differingPixels) || differingPixels < 0 || differingPixels > totalPixels) return false
|
|
152
|
+
if (!finiteNumber(diffRatio) || diffRatio < 0 || diffRatio > 1) return false
|
|
153
|
+
if (Math.abs(diffRatio - differingPixels / totalPixels) > 0.000051) return false
|
|
154
|
+
if (!Array.isArray(worstRegions)) return false
|
|
155
|
+
return worstRegions.every((region) => (
|
|
156
|
+
isObject(region) &&
|
|
157
|
+
canonicalBox(region, width, height) &&
|
|
158
|
+
Number.isInteger(region.differing) && region.differing > 0 &&
|
|
159
|
+
positiveInteger(region.total) && region.differing <= region.total &&
|
|
160
|
+
finiteNumber(region.ratio) && region.ratio > 0 && region.ratio <= 1
|
|
161
|
+
))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function longOcrEvidence(value) {
|
|
165
|
+
if (!isObject(value) || Array.isArray(value)) return false
|
|
166
|
+
if (typeof value.text !== 'string' || !positiveInteger(value.chunks)) return false
|
|
167
|
+
if (!isObject(value.engines) || Array.isArray(value.engines)) return false
|
|
168
|
+
if (![value.markdownPath, value.manifestPath, value.artifactsDir].every(nonEmptyText)) return false
|
|
169
|
+
const engineCounts = Object.values(value.engines)
|
|
170
|
+
if (engineCounts.length === 0 || !engineCounts.every((count) => Number.isInteger(count) && count >= 0)) return false
|
|
171
|
+
if (engineCounts.reduce((sum, count) => sum + count, 0) !== value.chunks) return false
|
|
172
|
+
if (nonEmptyText(value.text)) return true
|
|
173
|
+
const completed = (value.engines.tesseract ?? 0) + (value.engines.vision ?? 0)
|
|
174
|
+
return completed > 0
|
|
175
|
+
}
|
|
176
|
+
|
|
113
177
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
178
|
+
* Decide whether one registered evidence tool completed a real observation.
|
|
179
|
+
*
|
|
180
|
+
* This deliberately follows each tool's output contract instead of treating
|
|
181
|
+
* arbitrary non-empty containers as evidence. Valid negative observations
|
|
182
|
+
* (for example zero detections, empty OCR text, or a 0 pixel diff) still count;
|
|
183
|
+
* transport failures, malformed envelopes, and describe's empty-content
|
|
184
|
+
* sentinel do not.
|
|
118
185
|
*/
|
|
119
|
-
export function
|
|
120
|
-
if (value === undefined || value === null) return false
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
186
|
+
export function producedUsableToolEvidence(toolName, value, args = {}) {
|
|
187
|
+
if (!STRUCTURED_EVIDENCE_TOOLS.has(toolName) || value === undefined || value === null) return false
|
|
188
|
+
const parsed = parseJson(value)
|
|
189
|
+
if (isObject(parsed) && !Array.isArray(parsed) && parsed.ok === false) return false
|
|
190
|
+
|
|
191
|
+
switch (toolName) {
|
|
192
|
+
case 'vision_describe': {
|
|
193
|
+
if (typeof value !== 'string') return false
|
|
194
|
+
const text = value.trim()
|
|
195
|
+
if (text === '' || text === '(the vision model returned empty content)') return false
|
|
196
|
+
if (args?.json === true) {
|
|
197
|
+
if (text.startsWith('vision_describe: the model did not produce valid JSON.')) return false
|
|
198
|
+
return structuredDescribeEvidence(parsed)
|
|
199
|
+
}
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
case 'vision_ground':
|
|
203
|
+
return groundEvidence(parsed)
|
|
204
|
+
case 'vision_detect':
|
|
205
|
+
return detectEvidence(parsed)
|
|
206
|
+
case 'vision_ocr':
|
|
207
|
+
return ocrEvidence(parsed)
|
|
208
|
+
case 'vision_colors':
|
|
209
|
+
return colorEvidence(parsed)
|
|
210
|
+
case 'vision_pixel_diff':
|
|
211
|
+
return pixelDiffEvidence(parsed)
|
|
212
|
+
case 'vision_long_screenshot_ocr':
|
|
213
|
+
return longOcrEvidence(parsed)
|
|
214
|
+
default:
|
|
215
|
+
return false
|
|
127
216
|
}
|
|
128
|
-
if (Array.isArray(value)) return value.length > 0 && value.some(meaningfulEvidenceValue)
|
|
129
|
-
if (!isObject(value) || value.ok === false) return false
|
|
130
|
-
return meaningfulEvidenceValue(value)
|
|
131
217
|
}
|
|
132
218
|
|
|
133
219
|
/**
|
|
@@ -176,15 +262,6 @@ function languageOf(config) {
|
|
|
176
262
|
return runtimeLanguageFor(config?.__visionRouterLocale, 'zh')
|
|
177
263
|
}
|
|
178
264
|
|
|
179
|
-
function guidanceFor(kind, overrides, language = 'zh') {
|
|
180
|
-
for (let i = overrides.length - 1; i >= 0; i--) {
|
|
181
|
-
const entry = overrides[i]
|
|
182
|
-
if (entry.kind === kind) return entry.text
|
|
183
|
-
}
|
|
184
|
-
const copy = BUILTIN_MIXED_GUIDANCE[language] ?? BUILTIN_MIXED_GUIDANCE.zh
|
|
185
|
-
return copy[kind] ?? copy.fallback
|
|
186
|
-
}
|
|
187
|
-
|
|
188
265
|
function turnBudgetMs(config) {
|
|
189
266
|
const value = Number(config?.visionTurnBudgetMs)
|
|
190
267
|
if (!Number.isFinite(value) || value <= 0) return DEFAULT_TURN_BUDGET_MS
|
|
@@ -234,9 +311,7 @@ function freshState(turn, _config) {
|
|
|
234
311
|
bootstrapDone: false,
|
|
235
312
|
bootstrapResult: undefined,
|
|
236
313
|
successfulEvidenceCalls: 0,
|
|
237
|
-
|
|
238
|
-
completedBranches: new Set(),
|
|
239
|
-
mixedAttemptSignatures: new Set(),
|
|
314
|
+
postBootstrapEvidenceCalls: 0,
|
|
240
315
|
consecutiveNonProgressAttempts: 0,
|
|
241
316
|
budgetExhausted: false,
|
|
242
317
|
depthExhausted: false,
|
|
@@ -284,56 +359,13 @@ function bootstrapEvidence(raw) {
|
|
|
284
359
|
return parsed.evidence
|
|
285
360
|
}
|
|
286
361
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (evidence.mixed_of.includes(kind)) unique.push(kind)
|
|
293
|
-
}
|
|
294
|
-
return unique.slice(0, 2)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function evidenceAttemptSignature(name, args) {
|
|
298
|
-
let encoded
|
|
299
|
-
try {
|
|
300
|
-
encoded = JSON.stringify(args ?? null)
|
|
301
|
-
} catch {
|
|
302
|
-
encoded = String(args ?? '')
|
|
303
|
-
}
|
|
304
|
-
if (encoded.length <= MAX_EVIDENCE_SIGNATURE_CHARS) return `${name}:${encoded}`
|
|
305
|
-
const half = Math.floor(MAX_EVIDENCE_SIGNATURE_CHARS / 2)
|
|
306
|
-
return `${name}:${encoded.length}:${encoded.slice(0, half)}:${encoded.slice(-half)}`
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function inferBranchForTool(name, pending) {
|
|
310
|
-
const prefer = (kinds) => kinds.find((kind) => pending.includes(kind))
|
|
311
|
-
if (name === 'vision_detect' || name === 'vision_ground') return prefer(['ui', 'general'])
|
|
312
|
-
if (name === 'vision_long_screenshot_ocr') return prefer(['document', 'chat', 'code', 'general'])
|
|
313
|
-
if (name === 'vision_ocr') return prefer(['document', 'code', 'chat', 'general'])
|
|
314
|
-
if (name === 'vision_colors' || name === 'vision_pixel_diff') return prefer(['general', 'ui'])
|
|
315
|
-
return pending.length === 1 ? pending[0] : undefined
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function recordEvidenceSuccess(state, name, args) {
|
|
319
|
-
state.successfulEvidenceCalls += 1
|
|
320
|
-
if (state.mixedBranches.length === 0) return true
|
|
321
|
-
const pending = state.mixedBranches.filter((kind) => !state.completedBranches.has(kind))
|
|
322
|
-
if (pending.length === 0) return true
|
|
323
|
-
const signature = evidenceAttemptSignature(name, args)
|
|
324
|
-
if (state.mixedAttemptSignatures.has(signature)) return false
|
|
325
|
-
state.mixedAttemptSignatures.add(signature)
|
|
326
|
-
const branch = inferBranchForTool(name, pending)
|
|
327
|
-
if (!branch) return false
|
|
328
|
-
state.completedBranches.add(branch)
|
|
329
|
-
return true
|
|
330
|
-
}
|
|
331
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Structured 1+x has exactly one hard completion condition after bootstrap:
|
|
364
|
+
* at least one usable task-directed evidence observation. Mixed classifications
|
|
365
|
+
* remain model-visible advisory evidence; they do not create branch quotas.
|
|
366
|
+
*/
|
|
332
367
|
function missingEvidence(state) {
|
|
333
|
-
|
|
334
|
-
return state.mixedBranches.filter((kind) => !state.completedBranches.has(kind)).length
|
|
335
|
-
}
|
|
336
|
-
return state.successfulEvidenceCalls >= 1 ? 0 : 1
|
|
368
|
+
return state.postBootstrapEvidenceCalls >= 1 ? 0 : 1
|
|
337
369
|
}
|
|
338
370
|
|
|
339
371
|
function hasCoreFollowup(messages) {
|
|
@@ -347,6 +379,8 @@ function withoutEvidenceFollowups(messages) {
|
|
|
347
379
|
const id = message && typeof message.id === 'string' ? message.id : ''
|
|
348
380
|
return !(
|
|
349
381
|
id.includes('vision-router-structured-followup-') ||
|
|
382
|
+
// Strip historical Round-0 mixed hard guards if one is still present in
|
|
383
|
+
// a persisted/compatibility message surface. Round 1 never emits them.
|
|
350
384
|
id.includes('vision-router-structured-mixed-guard-') ||
|
|
351
385
|
id.includes('vision-router-structured-evidence-guard-')
|
|
352
386
|
)
|
|
@@ -374,30 +408,8 @@ function appendSyntheticGuardOnce(decision, baseMessages, state, message, stripE
|
|
|
374
408
|
}
|
|
375
409
|
|
|
376
410
|
function rearmEvidenceGuardAfterAttempt(state) {
|
|
377
|
-
const mixedPrefix = `vision-router-structured-mixed-guard-${state.turn}-`
|
|
378
411
|
const evidenceId = `vision-router-structured-evidence-guard-${state.turn}`
|
|
379
|
-
|
|
380
|
-
if (id === evidenceId || id.startsWith(mixedPrefix)) state.emittedGuardIds.delete(id)
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function mixedReminder(state, config) {
|
|
385
|
-
const language = languageOf(config)
|
|
386
|
-
const overrides = normalizeGuidanceOverrides(config?.guidanceOverrides)
|
|
387
|
-
const pending = state.mixedBranches.filter((kind) => !state.completedBranches.has(kind))
|
|
388
|
-
const done = state.mixedBranches.filter((kind) => state.completedBranches.has(kind))
|
|
389
|
-
const separator = language === 'en' ? ': ' : ':'
|
|
390
|
-
const lines = pending.map((kind) => `- ${kind}${separator}${guidanceFor(kind, overrides, language)}`)
|
|
391
|
-
if (language === 'en') {
|
|
392
|
-
const doneText = done.length > 0 ? `Verified branches: ${done.join(' + ')}. ` : ''
|
|
393
|
-
return `${doneText}This mixed image still has unverified branches: ${pending.join(' + ')}. ` +
|
|
394
|
-
'Focus each vision call on one unfinished branch; answer only after each branch has produced at least one useful piece of evidence.\n' +
|
|
395
|
-
lines.join('\n')
|
|
396
|
-
}
|
|
397
|
-
const doneText = done.length > 0 ? `已验证分支:${done.join(' + ')}。` : ''
|
|
398
|
-
return `${doneText}混合图片仍有未验证分支:${pending.join(' + ')}。` +
|
|
399
|
-
'每次视觉调用只聚焦一个尚未完成的分支;这些分支分别产生至少一次有效证据后再作答。\n' +
|
|
400
|
-
lines.join('\n')
|
|
412
|
+
state.emittedGuardIds.delete(evidenceId)
|
|
401
413
|
}
|
|
402
414
|
|
|
403
415
|
function appendGuardMessage(decision, payload, state, config) {
|
|
@@ -421,12 +433,12 @@ function appendGuardMessage(decision, payload, state, config) {
|
|
|
421
433
|
? 'The visual turn time budget is exhausted. Do not call more vision tools; answer from the evidence already collected and state any remaining uncertainty.'
|
|
422
434
|
: state.depthExhausted
|
|
423
435
|
? 'The configured deep-dive call cap has been reached. Do not call more vision tools; answer from the evidence already collected and state any remaining uncertainty.'
|
|
424
|
-
: 'Repeated follow-up vision attempts did not produce
|
|
436
|
+
: 'Repeated follow-up vision attempts did not produce usable evidence. Do not keep retrying vision tools; answer from the evidence already collected and state the remaining uncertainty.'
|
|
425
437
|
: state.budgetExhausted
|
|
426
438
|
? '本轮视觉总时间预算已耗尽。不要再调用视觉工具;请基于已经获得的证据作答,并明确仍存在的不确定性。'
|
|
427
439
|
: state.depthExhausted
|
|
428
440
|
? '已达到本轮设置的深挖次数上限。不要再调用视觉工具;请基于已经获得的证据作答,并明确仍存在的不确定性。'
|
|
429
|
-
: '
|
|
441
|
+
: '连续多次后续视觉调用都没有产出可用证据。不要继续重复调用视觉工具;请基于已经获得的证据作答,并明确仍存在的不确定性。',
|
|
430
442
|
}],
|
|
431
443
|
source: { kind: 'plugin', plugin: 'dsh-vision-router' },
|
|
432
444
|
}
|
|
@@ -435,16 +447,10 @@ function appendGuardMessage(decision, payload, state, config) {
|
|
|
435
447
|
|
|
436
448
|
if (!state.bootstrapDone || missingEvidence(state) === 0) return decision
|
|
437
449
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
content: [{ type: 'text', text: mixedReminder(state, config) }],
|
|
443
|
-
source: { kind: 'plugin', plugin: 'dsh-vision-router' },
|
|
444
|
-
}
|
|
445
|
-
return appendSyntheticGuardOnce(decision, baseMessages, state, message, true)
|
|
446
|
-
}
|
|
447
|
-
|
|
450
|
+
// Core owns bootstrap ordering and its one-shot follow-up guidance. This
|
|
451
|
+
// outer boundary owns the hard x>=1 completion rule. If Core's guidance is
|
|
452
|
+
// already present, do not duplicate it; if Core's legacy presentation latch
|
|
453
|
+
// stopped emitting it after an empty/failed attempt, re-assert the hard guard.
|
|
448
454
|
if (hasCoreFollowup(baseMessages)) return decision
|
|
449
455
|
const message = {
|
|
450
456
|
role: 'user',
|
|
@@ -452,8 +458,8 @@ function appendGuardMessage(decision, payload, state, config) {
|
|
|
452
458
|
content: [{
|
|
453
459
|
type: 'text',
|
|
454
460
|
text: language === 'en'
|
|
455
|
-
? 'The structured bootstrap is complete, but
|
|
456
|
-
: '
|
|
461
|
+
? 'The structured bootstrap is complete, but no usable task-directed visual evidence has been produced yet. Call at least one vision tool that can add or verify evidence, then answer after it succeeds.'
|
|
462
|
+
: '结构化预识别已经完成,但目前还没有产出可用的任务定向视觉证据。请至少调用一个能新增或验证证据的视觉工具,成功后再作答。',
|
|
457
463
|
}],
|
|
458
464
|
source: { kind: 'plugin', plugin: 'dsh-vision-router' },
|
|
459
465
|
}
|
|
@@ -542,9 +548,11 @@ function wrapRegisteredTool(def, states, getConfig) {
|
|
|
542
548
|
if (evidence !== undefined) {
|
|
543
549
|
state.bootstrapDone = true
|
|
544
550
|
state.bootstrapResult = result
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
551
|
+
// Structured 1+x is explicitly ordered: x starts only after the
|
|
552
|
+
// bootstrap succeeds. Keep the all-turn successful-evidence count
|
|
553
|
+
// intact for the user's explicit call cap; post-bootstrap progress
|
|
554
|
+
// has its own counter so an early model call cannot satisfy x>=1.
|
|
555
|
+
state.postBootstrapEvidenceCalls = 0
|
|
548
556
|
resetNonProgress(state)
|
|
549
557
|
state.budgetExhausted = false
|
|
550
558
|
refreshDepthExhaustion(state, getConfig())
|
|
@@ -565,7 +573,7 @@ function wrapRegisteredTool(def, states, getConfig) {
|
|
|
565
573
|
if (state.bootstrapDone && missingEvidence(state) > 0 && state.nonProgressExhausted) {
|
|
566
574
|
return failure(
|
|
567
575
|
'VISION_NO_PROGRESS_LIMIT',
|
|
568
|
-
`the structured follow-up stopped after ${MAX_NON_PROGRESS_ATTEMPTS} consecutive attempts without usable evidence
|
|
576
|
+
`the structured follow-up stopped after ${MAX_NON_PROGRESS_ATTEMPTS} consecutive attempts without usable evidence`,
|
|
569
577
|
)
|
|
570
578
|
}
|
|
571
579
|
|
|
@@ -577,18 +585,11 @@ function wrapRegisteredTool(def, states, getConfig) {
|
|
|
577
585
|
)
|
|
578
586
|
const code = resultCode(result)
|
|
579
587
|
if (code === 'VISION_TURN_BUDGET_EXCEEDED') state.budgetExhausted = true
|
|
580
|
-
if (
|
|
581
|
-
|
|
588
|
+
if (producedUsableToolEvidence(def.name, result, args)) {
|
|
589
|
+
state.successfulEvidenceCalls += 1
|
|
590
|
+
if (state.bootstrapDone) state.postBootstrapEvidenceCalls += 1
|
|
582
591
|
refreshDepthExhaustion(state, active)
|
|
583
|
-
|
|
584
|
-
if (progressed) resetNonProgress(state)
|
|
585
|
-
else {
|
|
586
|
-
recordNonProgress(state)
|
|
587
|
-
if (!state.nonProgressExhausted) rearmEvidenceGuardAfterAttempt(state)
|
|
588
|
-
}
|
|
589
|
-
} else {
|
|
590
|
-
resetNonProgress(state)
|
|
591
|
-
}
|
|
592
|
+
resetNonProgress(state)
|
|
592
593
|
} else if (code !== 'VISION_TURN_BUDGET_EXCEEDED' && state.bootstrapDone && missingEvidence(state) > 0) {
|
|
593
594
|
recordNonProgress(state)
|
|
594
595
|
if (!state.nonProgressExhausted) rearmEvidenceGuardAfterAttempt(state)
|