dsh-vision-router 2.2.0 → 2.2.2
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 +3 -3
- package/README.zh.md +3 -3
- package/docs/releases/v2.2.1.md +22 -0
- package/docs/releases/v2.2.2.md +21 -0
- package/lib/client.js +67 -27
- package/lib/core-primitives.js +11 -0
- package/lib/dsh-contract-compat.js +28 -5
- package/lib/dsh-settings-017-compat.js +416 -0
- package/lib/fetch-wrapper-lifecycle.js +56 -0
- package/lib/legacy-global-proxy-boundary.js +4 -3
- package/lib/pi-ai-bridge-wire-compat.js +4 -3
- package/lib/pixel-diff-stream.js +12 -6
- package/lib/public-entry.js +21 -1
- package/lib/session-message-source-compat.js +92 -0
- package/lib/session-vision-index.js +238 -10
- package/lib/settings-client-017-compat.js +331 -0
- package/lib/settings-client-rc8-lifecycle.js +13 -0
- package/lib/vision-breaker-shadow-health.js +11 -1
- package/lib/web/dsh-settings-017-root-transport.js +299 -0
- package/lib/web/remote-settings-client.js +200 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
|
-
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v2.2.
|
|
20
|
+
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v2.2.1"><img src="https://img.shields.io/badge/release-v2.2.1-5B4CF0?style=flat-square" alt="Release v2.2.1" /></a>
|
|
21
21
|
<a href="tests"><img src="https://img.shields.io/badge/verified-Node%2022%20%2B%2024-2EA44F?style=flat-square" alt="Verified: Node 22 + 24" /></a>
|
|
22
22
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat-square" alt="License: MIT" /></a>
|
|
23
23
|
<a href="package.json"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-339933?style=flat-square&logo=nodedotjs&logoColor=white" alt="Node.js >=22" /></a>
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
<p align="center">💬 <strong>QQ community group: 1105463028</strong></p>
|
|
40
40
|
|
|
41
41
|
> [!WARNING]
|
|
42
|
-
> 📌 **Announcement (v2.2.
|
|
42
|
+
> 📌 **Announcement (v2.2.1)**
|
|
43
43
|
>
|
|
44
|
-
> **v2.2.
|
|
44
|
+
> **v2.2.1:** Stability hotfix for large-session repair, accessor-based fetch composition, non-divisible pixel-diff bounds, and local Ollama answer-budget preservation — without raising the rc.8 Host floor. [What’s new →](docs/releases/v2.2.1.md)
|
|
45
45
|
|
|
46
46
|
<p align="center">
|
|
47
47
|
<img src="assets/vision-demo.gif" width="640" alt="Demo: paste an image, the agent locates the send button with vision_ground / vision_crop / vision_pixel_diff and answers with coordinates" />
|
package/README.zh.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
|
-
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v2.2.
|
|
20
|
+
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v2.2.1"><img src="https://img.shields.io/badge/release-v2.2.1-5B4CF0?style=flat-square" alt="Release v2.2.1" /></a>
|
|
21
21
|
<a href="tests"><img src="https://img.shields.io/badge/verified-Node%2022%20%2B%2024-2EA44F?style=flat-square" alt="已验证 Node 22 + 24" /></a>
|
|
22
22
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat-square" alt="MIT 许可证" /></a>
|
|
23
23
|
<a href="package.json"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-339933?style=flat-square&logo=nodedotjs&logoColor=white" alt="Node.js >=22" /></a>
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
<p align="center">💬 <strong>QQ 用户交流群:1105463028</strong></p>
|
|
40
40
|
|
|
41
41
|
> [!WARNING]
|
|
42
|
-
> 📌 **公告(v2.2.
|
|
42
|
+
> 📌 **公告(v2.2.1)**
|
|
43
43
|
>
|
|
44
|
-
> **v2.2.
|
|
44
|
+
> **v2.2.1:稳定性热修——修复大型 Session repair 卡死、accessor fetch 组合、非整除 pixel diff 边界与本地 Ollama 推理占满答案预算;公开最低 Host 仍保持 `0.1.0-rc.8`。** [查看完整更新 →](docs/releases/v2.2.1.md)
|
|
45
45
|
|
|
46
46
|
<p align="center">
|
|
47
47
|
<img src="assets/vision-demo.gif" width="640" alt="演示:粘贴图片,Agent 用 vision_ground / vision_crop / vision_pixel_diff 定位发送按钮并给出坐标" />
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# v2.2.1
|
|
2
|
+
|
|
3
|
+
DSH Vision Router 2.2.1 is a focused stability and compatibility hotfix for the 2.2.0 line.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
|
|
7
|
+
- Fixes the large-session repair regressions in #524 and #532. On supported DSH Hosts, production tool-result and guard-stop repair now consumes bounded post-commit `session/event` entries and performs zero live Session-history reads: no `sessionQuery.readEvent()`, no `observeSession.events` materialization, and no new `eventAt()` / `snapshotEvents()` dependency. This removes the original full-clone amplification and the later `Unable to deserialize cloned data` / Windows Node 24 native-crash path.
|
|
8
|
+
- Fixes global `fetch` compatibility composition with accessor-based Host/plugin pipelines (#519), preventing wrapper cycles while preserving live Host middleware authority.
|
|
9
|
+
- Fixes `vision_pixel_diff` worst-region bounds on non-divisible image/grid dimensions (#520).
|
|
10
|
+
- Fixes the confirmed Ollama portion of #526 by disabling hidden reasoning for generated local Ollama vision providers on the OpenAI-compatible Chat Completions path, preserving the bounded token budget for visible answer text.
|
|
11
|
+
- Completes the release-channel canary fixture so stable/latest and preview/alpha validate the same Session publication family before Host contract smoke.
|
|
12
|
+
|
|
13
|
+
## Compatibility
|
|
14
|
+
|
|
15
|
+
- Node 22 and Node 24 remain supported.
|
|
16
|
+
- Windows, macOS, and Linux remain gated.
|
|
17
|
+
- The public minimum DSH Host remains `0.1.0-rc.8`.
|
|
18
|
+
- Existing 2.2.0 settings require no migration.
|
|
19
|
+
|
|
20
|
+
## Known follow-ups
|
|
21
|
+
|
|
22
|
+
- #526 remains open for the LM Studio reasoning/budget path; 2.2.1 does not send Ollama-specific `reasoning_effort` to LM Studio without a documented LM Studio wire contract.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# v2.2.2
|
|
2
|
+
|
|
3
|
+
DSH Vision Router 2.2.2 is an urgent browser compatibility hotfix for DSH 0.1.7-rc.1.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
|
|
7
|
+
- Fixes Vision Router being left pending at Web startup on DSH 0.1.7-rc.1 with `waiting for service: settingsScope`.
|
|
8
|
+
- Adapts the legacy `ctx.settingsScope.bind({ namespace })` consumer face to DSH 0.1.7's official `configForms.get(namespace)` service without hard-requiring either service generation.
|
|
9
|
+
- Keeps the real legacy `settingsScope` path preferred on older supported Hosts, so the fix does not raise the existing Host floor.
|
|
10
|
+
- Preserves the existing local settings permission and remote-risk wrappers over both settings backends.
|
|
11
|
+
- Adds loader-level regression coverage for both DSH 0.1.7 `configForms` and legacy `settingsScope` Hosts.
|
|
12
|
+
|
|
13
|
+
## Validation
|
|
14
|
+
|
|
15
|
+
- Node 22 and Node 24 CI passed on the compatibility fix.
|
|
16
|
+
- Architecture Closure, P1 Routing Parity, P3 Compatibility Convergence, DSH Contract, exact-source, browser smoke, adversarial compatibility, fuzz, dependency review, fetch composition, and native multimodal cold-resume gates passed on PR #538.
|
|
17
|
+
- The public minimum DSH Host remains `0.1.0-rc.8`.
|
|
18
|
+
|
|
19
|
+
## Upgrade
|
|
20
|
+
|
|
21
|
+
Users affected by the DSH 0.1.7 startup failure should upgrade Vision Router to 2.2.2. No settings migration is required.
|
package/lib/client.js
CHANGED
|
@@ -865,6 +865,57 @@ window.__ModuleLoader__.load({
|
|
|
865
865
|
: stored && settingsValueEqual(item.key, user[item.key], item.run.value)
|
|
866
866
|
return { ok, stored }
|
|
867
867
|
}
|
|
868
|
+
const finalize = () => {
|
|
869
|
+
const landed = failures.length === 0
|
|
870
|
+
const nextDrafts = landedFields.length === 0 ? drafts : { ...drafts }
|
|
871
|
+
for (const field of landedFields) delete nextDrafts[field]
|
|
872
|
+
return {
|
|
873
|
+
landed,
|
|
874
|
+
failed: !landed,
|
|
875
|
+
landedFields,
|
|
876
|
+
nextDrafts,
|
|
877
|
+
failures,
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
const mismatchFailure = (item, check) => ({
|
|
881
|
+
field: item.key,
|
|
882
|
+
operation: item.run.clear ? 'unset' : 'set',
|
|
883
|
+
reason: 'readback-mismatch',
|
|
884
|
+
detail: item.run.clear
|
|
885
|
+
? 'field remained present in the user layer'
|
|
886
|
+
: check.stored
|
|
887
|
+
? 'stored user-layer value differs from the requested value'
|
|
888
|
+
: 'field is absent from the user layer',
|
|
889
|
+
})
|
|
890
|
+
const batchWrite = scope && typeof scope.__visionRouterWritePlan === 'function'
|
|
891
|
+
? scope.__visionRouterWritePlan.bind(scope)
|
|
892
|
+
: undefined
|
|
893
|
+
// DSH 0.1.7's local compatibility transport is ConfigEditor-backed.
|
|
894
|
+
// ConfigEditor reloads the plugin after a successful edit, so serial POSTs
|
|
895
|
+
// from one Save can cross generations and strand the second mutation.
|
|
896
|
+
// Its private scope seam commits the entire UI plan in one atomic edit.
|
|
897
|
+
if (plan.length > 1 && batchWrite) {
|
|
898
|
+
try {
|
|
899
|
+
await batchWrite(plan)
|
|
900
|
+
} catch (error) {
|
|
901
|
+
for (const item of plan) {
|
|
902
|
+
failures.push({
|
|
903
|
+
field: item.key,
|
|
904
|
+
operation: item.run.clear ? 'unset' : 'set',
|
|
905
|
+
reason: error && error.code === 'settings-conflict' ? 'settings-conflict' : 'write-error',
|
|
906
|
+
detail: settingsSaveErrorMessage(error),
|
|
907
|
+
})
|
|
908
|
+
}
|
|
909
|
+
return finalize()
|
|
910
|
+
}
|
|
911
|
+
for (const item of plan) {
|
|
912
|
+
const check = inspectReadback(item)
|
|
913
|
+
if (check.error) failures.push(check.error)
|
|
914
|
+
else if (check.ok) landedFields.push(item.key)
|
|
915
|
+
else failures.push(mismatchFailure(item, check))
|
|
916
|
+
}
|
|
917
|
+
return finalize()
|
|
918
|
+
}
|
|
868
919
|
const writeItem = async (item) => {
|
|
869
920
|
if (item.run.clear) await scope.unset(item.key)
|
|
870
921
|
else await scope.set(item.key, item.run.value)
|
|
@@ -913,16 +964,7 @@ window.__ModuleLoader__.load({
|
|
|
913
964
|
}
|
|
914
965
|
}
|
|
915
966
|
|
|
916
|
-
terminalFailure =
|
|
917
|
-
field: item.key,
|
|
918
|
-
operation,
|
|
919
|
-
reason: 'readback-mismatch',
|
|
920
|
-
detail: item.run.clear
|
|
921
|
-
? 'field remained present in the user layer'
|
|
922
|
-
: check.stored
|
|
923
|
-
? 'stored user-layer value differs from the requested value'
|
|
924
|
-
: 'field is absent from the user layer',
|
|
925
|
-
}
|
|
967
|
+
terminalFailure = mismatchFailure(item, check)
|
|
926
968
|
}
|
|
927
969
|
if (success) landedFields.push(item.key)
|
|
928
970
|
else failures.push(terminalFailure ?? {
|
|
@@ -932,16 +974,7 @@ window.__ModuleLoader__.load({
|
|
|
932
974
|
detail: 'write did not become visible in the user layer',
|
|
933
975
|
})
|
|
934
976
|
}
|
|
935
|
-
|
|
936
|
-
const nextDrafts = landedFields.length === 0 ? drafts : { ...drafts }
|
|
937
|
-
for (const field of landedFields) delete nextDrafts[field]
|
|
938
|
-
return {
|
|
939
|
-
landed,
|
|
940
|
-
failed: !landed,
|
|
941
|
-
landedFields,
|
|
942
|
-
nextDrafts,
|
|
943
|
-
failures,
|
|
944
|
-
}
|
|
977
|
+
return finalize()
|
|
945
978
|
}
|
|
946
979
|
|
|
947
980
|
const REMOTE_SETTINGS_CHANNEL = '/vision-router-settings'
|
|
@@ -1124,16 +1157,23 @@ window.__ModuleLoader__.load({
|
|
|
1124
1157
|
}
|
|
1125
1158
|
|
|
1126
1159
|
function shouldUseRemoteSettings(getConnection, locationLike) {
|
|
1160
|
+
// Browser page authority is the security boundary. DSH 0.1.7 can report
|
|
1161
|
+
// Connection.isLoopback=false even for a real 127.0.0.1 page, so letting
|
|
1162
|
+
// that hint win would incorrectly force the local Settings page onto the
|
|
1163
|
+
// trusted-host RPC path. Conversely, a non-loopback browser URL must not
|
|
1164
|
+
// become local merely because a stale Connection says true.
|
|
1165
|
+
const location = locationLike ?? (typeof window !== 'undefined' ? window.location : undefined)
|
|
1166
|
+
const hostname = typeof location?.hostname === 'string' ? location.hostname.toLowerCase().replace(/^\[|\]$/g, '') : ''
|
|
1167
|
+
if (hostname !== '') {
|
|
1168
|
+
if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return false
|
|
1169
|
+
if (/^127(?:\.\d{1,3}){3}$/.test(hostname)) return false
|
|
1170
|
+
return true
|
|
1171
|
+
}
|
|
1127
1172
|
try {
|
|
1128
1173
|
const connection = typeof getConnection === 'function' ? getConnection() : undefined
|
|
1129
1174
|
if (connection && typeof connection.isLoopback === 'boolean') return connection.isLoopback === false
|
|
1130
|
-
} catch { /*
|
|
1131
|
-
|
|
1132
|
-
const hostname = typeof location?.hostname === 'string' ? location.hostname.toLowerCase().replace(/^\[|\]$/g, '') : ''
|
|
1133
|
-
if (hostname === '') return false
|
|
1134
|
-
if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return false
|
|
1135
|
-
if (/^127(?:\.\d{1,3}){3}$/.test(hostname)) return false
|
|
1136
|
-
return true
|
|
1175
|
+
} catch { /* no page authority and no usable Connection: stay local-safe */ }
|
|
1176
|
+
return false
|
|
1137
1177
|
}
|
|
1138
1178
|
|
|
1139
1179
|
function reportSettingsSaveFailures(failures) {
|
package/lib/core-primitives.js
CHANGED
|
@@ -1858,6 +1858,10 @@ export function localOllamaProvidersOf(config) {
|
|
|
1858
1858
|
model,
|
|
1859
1859
|
apiKeyEnv: '',
|
|
1860
1860
|
maxTokens: 2048,
|
|
1861
|
+
// Ollama's OpenAI-compatible Chat Completions endpoint supports
|
|
1862
|
+
// reasoning_effort='none'. Vision extraction should spend the bounded
|
|
1863
|
+
// completion budget on observable answer text rather than hidden thought.
|
|
1864
|
+
reasoningEffort: 'none',
|
|
1861
1865
|
// 仅显式选择 anthropic 格式时携带(默认 openai 路径保持字节不变)。
|
|
1862
1866
|
...(local.format === 'anthropic' ? { format: 'anthropic' } : {}),
|
|
1863
1867
|
// 建议值透传:温度/top_p 只在显式配置时携带(callOpenAICompatible
|
|
@@ -1978,6 +1982,9 @@ export async function callLocalBackend(provider, messages, options = {}) {
|
|
|
1978
1982
|
...(typeof options.resolveCredential === 'function'
|
|
1979
1983
|
? { resolveCredential: options.resolveCredential }
|
|
1980
1984
|
: {}),
|
|
1985
|
+
...(typeof provider.reasoningEffort === 'string'
|
|
1986
|
+
? { reasoningEffort: provider.reasoningEffort }
|
|
1987
|
+
: {}),
|
|
1981
1988
|
...sampling,
|
|
1982
1989
|
})
|
|
1983
1990
|
}
|
|
@@ -2121,6 +2128,7 @@ export async function callOpenAICompatible(provider, messages, options = {}) {
|
|
|
2121
2128
|
if (resolvedApiKey === '') throw new Error(`http provider "${provider.name}": ${apiKeyEnv} is not set`)
|
|
2122
2129
|
headers.authorization = `Bearer ${resolvedApiKey}`
|
|
2123
2130
|
}
|
|
2131
|
+
const reasoningEffort = options.reasoningEffort ?? provider.reasoningEffort
|
|
2124
2132
|
const body = {
|
|
2125
2133
|
model: provider.model,
|
|
2126
2134
|
messages,
|
|
@@ -2130,6 +2138,9 @@ export async function callOpenAICompatible(provider, messages, options = {}) {
|
|
|
2130
2138
|
// never pass them, so the wire body stays byte-identical for main paths.
|
|
2131
2139
|
...(typeof options.temperature === 'number' ? { temperature: options.temperature } : {}),
|
|
2132
2140
|
...(typeof options.top_p === 'number' ? { top_p: options.top_p } : {}),
|
|
2141
|
+
...(['none', 'low', 'medium', 'high', 'max'].includes(reasoningEffort)
|
|
2142
|
+
? { reasoning_effort: reasoningEffort }
|
|
2143
|
+
: {}),
|
|
2133
2144
|
}
|
|
2134
2145
|
const url = `${provider.baseURL.replace(/\/$/, '')}/chat/completions`
|
|
2135
2146
|
const request = () =>
|
|
@@ -192,17 +192,40 @@ export function createSessionEventTailReader(ctx) {
|
|
|
192
192
|
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
|
-
* Build the
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
195
|
+
* Build the asynchronous Session-log reader used by cold attachment recovery
|
|
196
|
+
* and batched surface repair. Prefer SessionQuery's observation lease when it
|
|
197
|
+
* exists: one exact live cut materializes at most one immutable event snapshot,
|
|
198
|
+
* rather than cloning the whole live corpus once per requested event (#524).
|
|
199
|
+
* Hosts without that capability retain the released readSession() contract.
|
|
199
200
|
*/
|
|
200
201
|
export function createSessionLogReader(ctx) {
|
|
201
202
|
return async function readSessionLog(session) {
|
|
202
203
|
const sessionId = session?.id
|
|
203
204
|
if (sessionId === undefined || sessionId === null) return { supported: false }
|
|
204
205
|
const query = sessionQueryOf(ctx)
|
|
205
|
-
if (!query
|
|
206
|
+
if (!query) return { supported: false }
|
|
207
|
+
|
|
208
|
+
if (typeof query.observeSession === 'function') {
|
|
209
|
+
let observation
|
|
210
|
+
try {
|
|
211
|
+
observation = await query.observeSession(sessionId, { projectionMode: 'none' })
|
|
212
|
+
if (!observation || !Array.isArray(observation.events)) {
|
|
213
|
+
throw new Error('sessionQuery.observeSession returned no event log')
|
|
214
|
+
}
|
|
215
|
+
const observedId = observation.header?.id
|
|
216
|
+
if (observedId !== undefined && observedId !== sessionId) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`sessionQuery.observeSession returned session ${String(observedId)} for requested ${String(sessionId)}`,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
return { supported: true, events: observation.events }
|
|
222
|
+
} finally {
|
|
223
|
+
const dispose = observation?.[Symbol.dispose]
|
|
224
|
+
if (typeof dispose === 'function') dispose.call(observation)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (typeof query.readSession !== 'function') return { supported: false }
|
|
206
229
|
const snapshot = await query.readSession(sessionId)
|
|
207
230
|
if (!snapshot || !Array.isArray(snapshot.events)) {
|
|
208
231
|
throw new Error('sessionQuery.readSession returned no event log')
|