dsh-vision-router 3.0.2 → 3.0.3
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 +8 -9
- package/README.zh.md +6 -7
- package/docs/releases/v3.0.3.md +45 -0
- package/lib/client-presentation-boundary-main.js +20 -10
- package/lib/client.js +10 -5
- package/lib/diagnostic-redaction.js +9 -3
- package/lib/proxy-routing.js +29 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,16 +42,15 @@
|
|
|
42
42
|
> **Data leaves your machine by default.** When a cloud vision model is used, Vision Router sends the image (or a derived crop), the vision prompt and related request metadata to that provider. A fresh install has an anonymous OVHcloud vision fallback enabled, so "free" and "no key" do **not** mean offline. Do not use the cloud chain for confidential, regulated or classified material. See [Data flow and strict local-only use](#data-flow-and-strict-local-only-use).
|
|
43
43
|
|
|
44
44
|
> [!WARNING]
|
|
45
|
-
> 📌 **Announcement (v3.0.
|
|
45
|
+
> 📌 **Announcement (v3.0.3)**
|
|
46
46
|
>
|
|
47
|
-
> **v3.0.
|
|
48
|
-
>
|
|
49
|
-
>
|
|
50
|
-
>
|
|
51
|
-
> `*.
|
|
52
|
-
>
|
|
53
|
-
>
|
|
54
|
-
> `0.1.5-rc.1`, DSH 0.2.x from verified `0.2.0-rc.2`. [What’s new →](docs/releases/v3.0.2.md)
|
|
47
|
+
> **v3.0.3: presentation and proxy-boundary corrections.** Restored history and empty
|
|
48
|
+
> `vision_present` results no longer steal the reader’s scroll position, while fresh presented
|
|
49
|
+
> images still focus correctly on both the newer phase-based Host contract and the supported DSH
|
|
50
|
+
> 0.1.5 lifecycle. Cookie-domain-style `.example.com` entries now follow the same proxy-host
|
|
51
|
+
> policy as `*.example.com`, and the exact DSH 0.1.7 browser smoke again follows both
|
|
52
|
+
> presentation-boundary owners. CI action pins were also refreshed; runtime dependencies and
|
|
53
|
+
> supported Hosts are unchanged. [What’s new →](docs/releases/v3.0.3.md)
|
|
55
54
|
|
|
56
55
|
<p align="center">
|
|
57
56
|
<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
|
@@ -42,14 +42,13 @@
|
|
|
42
42
|
> **默认配置会让数据出网。** 使用云端视觉模型时,Vision Router 会将图片(或由它裁剪出的局部)、识图提示词及相关请求元数据发送给对应供应商。新安装默认开启 OVHcloud 匿名视觉兜底,因此“免费”和“免 Key”不等于离线。涉密、涉案、内部或其他受限材料请勿使用云端链路。详见[数据流向与严格纯本地配置](#数据流向与严格纯本地配置)。
|
|
43
43
|
|
|
44
44
|
> [!WARNING]
|
|
45
|
-
> 📌 **公告(v3.0.
|
|
45
|
+
> 📌 **公告(v3.0.3)**
|
|
46
46
|
>
|
|
47
|
-
> **v3.0.
|
|
48
|
-
>
|
|
49
|
-
>
|
|
50
|
-
>
|
|
51
|
-
>
|
|
52
|
-
> [查看完整更新 →](docs/releases/v3.0.2.md)
|
|
47
|
+
> **v3.0.3:修正图片呈现与代理边界。** 恢复历史会话和空的 `vision_present` 结果不再抢走滚动位置,
|
|
48
|
+
> 新生成的图片仍会在新版 phase 生命周期与受支持的 DSH 0.1.5 旧生命周期上正确聚焦;
|
|
49
|
+
> cookie-domain 风格的 `.example.com` 现在与 `*.example.com` 遵守同一套代理主机规则;
|
|
50
|
+
> DSH 0.1.7 精确浏览器 smoke 也重新跟随两个 presentation-boundary owner。CI Action 固定版本同步更新,
|
|
51
|
+
> 运行时依赖和 Host 支持范围均不变。[查看完整更新 →](docs/releases/v3.0.3.md)
|
|
53
52
|
|
|
54
53
|
<p align="center">
|
|
55
54
|
<img src="assets/vision-demo.gif" width="640" alt="演示:粘贴图片,Agent 用 vision_ground / vision_crop / vision_pixel_diff 定位发送按钮并给出坐标" />
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# v3.0.3
|
|
2
|
+
|
|
3
|
+
DSH Vision Router 3.0.3 is a corrective patch on the 3.0 line. It closes the three residuals
|
|
4
|
+
reported after 3.0.2 and carries two CI-action maintenance updates. There is no runtime dependency,
|
|
5
|
+
settings migration, Session-format change, public-API change or Host-support-window change.
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- **Presented-image focus only reacts to fresh image results (#666).** Restoring a long Session no
|
|
10
|
+
longer jumps to an old image below the viewport, and an empty `vision_present` result cannot
|
|
11
|
+
steer the reader to another card. The helper receives the exact card node instead of querying
|
|
12
|
+
the document for the last presented card.
|
|
13
|
+
- **The focus rule works across the supported Host window.** DSH 0.1.7+ exposes an explicit
|
|
14
|
+
`phase`; the supported DSH 0.1.5 train does not. The client normalizes both contracts by using
|
|
15
|
+
`phase` when present and falling back to the settled `block.kind === 'tool-result'`
|
|
16
|
+
discriminant otherwise. Restored results initialize as already settled, while a live call still
|
|
17
|
+
produces the non-result → result edge.
|
|
18
|
+
- **Leading-dot proxy hosts are no longer inert (#665).** `.example.com` now canonicalizes to the
|
|
19
|
+
same apex-and-subdomain policy as `*.example.com`. Dot-boundary matching remains conservative,
|
|
20
|
+
so lookalikes such as `evilexample.com` stay outside.
|
|
21
|
+
- **The exact DSH 0.1.7 browser smoke follows both presentation owners again (#664).** Changes to
|
|
22
|
+
either presentation-boundary carrier now trigger the exact-Host browser regression layer.
|
|
23
|
+
- **CI action maintenance (#662, #663).** `actions/download-artifact` is pinned to v8.0.1, and the
|
|
24
|
+
HOL scanner Action is pinned to v1.2.763 (plugin-scanner 3.24.2). The scanner version annotation
|
|
25
|
+
in the workflow is corrected to match the actual immutable SHA.
|
|
26
|
+
|
|
27
|
+
## Compatibility
|
|
28
|
+
|
|
29
|
+
Unchanged from 3.0.2:
|
|
30
|
+
|
|
31
|
+
- DSH 0.1.x: `>=0.1.5-rc.1 <0.2.0-0`
|
|
32
|
+
- DSH 0.2.x: `>=0.2.0-rc.2 <0.3.0-0`
|
|
33
|
+
- Node.js: `^22.19.0 || >=24.0.0`
|
|
34
|
+
|
|
35
|
+
## Validation
|
|
36
|
+
|
|
37
|
+
The fixes carry direct regression coverage for restored/empty presentation results, the legacy
|
|
38
|
+
no-`phase` Host lifecycle, leading-dot proxy admission/non-admission, and the DSH 0.1.7 workflow
|
|
39
|
+
ownership paths. The release workflow re-runs the full test, build, reproducibility, type, packed
|
|
40
|
+
API and Host-support gates on the exact release SHA before it creates the immutable tag or
|
|
41
|
+
publishes to npm.
|
|
42
|
+
|
|
43
|
+
## Upgrade
|
|
44
|
+
|
|
45
|
+
Drop-in replacement for 3.0.2. No configuration or data migration is required.
|
|
@@ -996,11 +996,8 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
|
|
|
996
996
|
return function(){ tag.remove(); };
|
|
997
997
|
}
|
|
998
998
|
|
|
999
|
-
// A presented image
|
|
1000
|
-
//
|
|
1001
|
-
// that call thousands of pixels above them. The card calls this on mount; it only moves the
|
|
1002
|
-
// reader for content that arrived below the viewport — a card that is already visible is left
|
|
1003
|
-
// alone, and so is history above the reader when a session is opened.
|
|
999
|
+
// A fresh presented image below the reader is the same as not being shown. Centre only that
|
|
1000
|
+
// card; visible cards and history on either side of a restored viewport stay untouched.
|
|
1004
1001
|
function focusPresentedCard(node, viewportHeight) {
|
|
1005
1002
|
if (!node || typeof node.getBoundingClientRect !== 'function') return false;
|
|
1006
1003
|
var rect = node.getBoundingClientRect();
|
|
@@ -1022,12 +1019,25 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
|
|
|
1022
1019
|
return false;
|
|
1023
1020
|
}
|
|
1024
1021
|
|
|
1022
|
+
// New Hosts expose phase; supported DSH 0.1.5 instead marks settled blocks as
|
|
1023
|
+
// 'tool-result'. Normalize the [phase, block kind] tuple without raising the Host floor.
|
|
1024
|
+
function isPresentedResultState(state) {
|
|
1025
|
+
var phase = state && state[0];
|
|
1026
|
+
return phase !== undefined ? phase === 'result' : !!state && state[1] === 'tool-result';
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Restored history starts settled on both sides of the comparison. Only a live transition into
|
|
1030
|
+
// an image-bearing result is fresh presentation.
|
|
1031
|
+
function shouldFocusPresentedCard(previousState, state, imageCount) {
|
|
1032
|
+
return imageCount > 0 &&
|
|
1033
|
+
isPresentedResultState(state) &&
|
|
1034
|
+
!isPresentedResultState(previousState);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1025
1037
|
try {
|
|
1026
|
-
globalThis.__dvrPresentFocus = function () {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
var last = cards[cards.length - 1];
|
|
1030
|
-
return last ? focusPresentedCard(last, globalThis.innerHeight || 0) : false;
|
|
1038
|
+
globalThis.__dvrPresentFocus = function (node, previousState, state, imageCount) {
|
|
1039
|
+
return shouldFocusPresentedCard(previousState, state, imageCount) &&
|
|
1040
|
+
focusPresentedCard(node, globalThis.innerHeight || 0);
|
|
1031
1041
|
};
|
|
1032
1042
|
} catch (_) {}
|
|
1033
1043
|
|
package/lib/client.js
CHANGED
|
@@ -4756,12 +4756,11 @@ openLogFolder,
|
|
|
4756
4756
|
}
|
|
4757
4757
|
}, ownerToken)
|
|
4758
4758
|
}
|
|
4759
|
-
//
|
|
4760
|
-
// whenever the user switches the locale in Settings → General.
|
|
4759
|
+
// Keep card copy in sync with the app locale.
|
|
4761
4760
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'vision-router: card locale')
|
|
4762
4761
|
const t = ctx.locale.bind(NS)
|
|
4763
4762
|
const VisionPresentCard = (props) => {
|
|
4764
|
-
const { block, sessionId } = props
|
|
4763
|
+
const { block, sessionId, phase } = props
|
|
4765
4764
|
const content = block && Array.isArray(block.content) ? block.content : []
|
|
4766
4765
|
const images = content
|
|
4767
4766
|
.filter((item) => item && item.type === 'image' && item.attachment)
|
|
@@ -4770,7 +4769,13 @@ openLogFolder,
|
|
|
4770
4769
|
const imageKeySignature = JSON.stringify(imageKeys)
|
|
4771
4770
|
const imageOwnerToken = React.useRef({}).current
|
|
4772
4771
|
const ownedImageKeys = React.useRef(new Set())
|
|
4773
|
-
|
|
4772
|
+
const presentNode = React.useRef()
|
|
4773
|
+
const focusState = React.useRef([phase, block?.kind])
|
|
4774
|
+
React.useEffect(() => {
|
|
4775
|
+
const next = [phase, block?.kind]
|
|
4776
|
+
globalThis.__dvrPresentFocus?.(presentNode.current, focusState.current, next, images.length)
|
|
4777
|
+
focusState.current = next
|
|
4778
|
+
}, [phase, block?.kind, imageKeySignature])
|
|
4774
4779
|
React.useEffect(() => {
|
|
4775
4780
|
const current = new Set(imageKeys)
|
|
4776
4781
|
for (const key of ownedImageKeys.current) {
|
|
@@ -4810,7 +4815,7 @@ openLogFolder,
|
|
|
4810
4815
|
}
|
|
4811
4816
|
return React.createElement(
|
|
4812
4817
|
'div',
|
|
4813
|
-
{ 'data-dvr-present': 'true', style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 6, padding: '4px 0' } },
|
|
4818
|
+
{ ref: presentNode, 'data-dvr-present': 'true', style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 6, padding: '4px 0' } },
|
|
4814
4819
|
parsed && typeof parsed.label === 'string' && parsed.label !== 'image'
|
|
4815
4820
|
? React.createElement(
|
|
4816
4821
|
'div',
|
|
@@ -20,9 +20,15 @@ const CREDENTIAL_SHAPE_PATTERNS = [
|
|
|
20
20
|
// (the log sink has always consumed it) while `,` separates what follows.
|
|
21
21
|
[/\b(Authorization\s*[:=]\s*)(?:[A-Za-z][A-Za-z0-9._-]*\s+)?[^\s,]+/gi, `$1${REDACTED}`],
|
|
22
22
|
[/\b(Proxy-Authorization\s*[:=]\s*)(?:[A-Za-z][A-Za-z0-9._-]*\s+)?[^\s,]+/gi, `$1${REDACTED}`],
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
23
|
+
// A real Cookie/Set-Cookie header owns the remainder of its line, so redact the
|
|
24
|
+
// whole value there. The same words also appear inside arbitrary diagnostics,
|
|
25
|
+
// URL query fragments and object dumps; those contexts must keep unrelated text.
|
|
26
|
+
[/^([ \t]*(?:Set-)?Cookie\s*:\s*)[^\r\n]+/gim, `$1${REDACTED}`],
|
|
27
|
+
// Inline colon form: redact one scalar or a semicolon-separated cookie-pair run,
|
|
28
|
+
// then stop at an unrelated comma/word so support diagnostics remain readable.
|
|
29
|
+
[/\b((?:Set-)?Cookie\s*:\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^;\s,&"']+(?:\s*;\s*[^=;\s,&"']+\s*=\s*(?:"[^"\r\n]*"|'[^'\r\n]*'|[^;\s,&"']+))*)/gi, `$1${REDACTED}`],
|
|
30
|
+
// Assignment/query form: bound the value at ordinary diagnostic separators.
|
|
31
|
+
[/\b((?:Set-)?Cookie\s*=\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,&"']+)/gi, `$1${REDACTED}`],
|
|
26
32
|
// URL userinfo (`scheme://user:secret@host`) without needing a URL parser, so
|
|
27
33
|
// the log path — which never parsed URLs — covers it too.
|
|
28
34
|
[/\b([a-z][a-z0-9+.-]*:\/\/)[^/\s@]+@/gi, `$1${REDACTED}@`],
|
package/lib/proxy-routing.js
CHANGED
|
@@ -9,6 +9,21 @@ export function visionProxyOverrideConfigured(config = {}) {
|
|
|
9
9
|
: undefined;
|
|
10
10
|
return typeof value === 'string' && value.trim() !== '';
|
|
11
11
|
}
|
|
12
|
+
function canonicalIpv6Literal(value) {
|
|
13
|
+
if (!value.includes(':'))
|
|
14
|
+
return undefined;
|
|
15
|
+
try {
|
|
16
|
+
const serialized = new URL(`http://[${value}]/`).hostname;
|
|
17
|
+
if (!serialized.startsWith('[') || !serialized.endsWith(']'))
|
|
18
|
+
return undefined;
|
|
19
|
+
return serialized.slice(1, -1).toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Invalid or URL-unrepresentable literals (for example zone-id spellings)
|
|
23
|
+
// keep the existing conservative string semantics instead of broadening admission.
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
12
27
|
/**
|
|
13
28
|
* Canonicalize a configured/request hostname for proxy admission only.
|
|
14
29
|
* Persisted settings stay untouched; this removes DNS presentation differences
|
|
@@ -22,26 +37,30 @@ export function canonicalProxyHost(value) {
|
|
|
22
37
|
host = host.slice(0, -1);
|
|
23
38
|
if (host === '')
|
|
24
39
|
return '';
|
|
25
|
-
//
|
|
26
|
-
// already does for a bare hostname.
|
|
27
|
-
// makes the entry equal no real hostname: the configured host silently proxies nothing.
|
|
40
|
+
// `*.host` and cookie-domain `.host` entries both mean "this host and its subdomains",
|
|
41
|
+
// which is exactly what the matcher already does for a bare hostname. Leaving either prefix
|
|
42
|
+
// in place makes the entry equal no real hostname: the configured host silently proxies nothing.
|
|
28
43
|
if (host.startsWith('*.'))
|
|
29
44
|
host = host.slice(2);
|
|
45
|
+
else if (host.startsWith('.'))
|
|
46
|
+
host = host.slice(1);
|
|
30
47
|
if (host === '')
|
|
31
48
|
return '';
|
|
32
|
-
// WHATWG URL.hostname
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
49
|
+
// WHATWG URL.hostname serializes IPv6 literals to one compressed spelling.
|
|
50
|
+
// Re-serialize configured literals through the same parser before comparing so
|
|
51
|
+
// equivalent legal spellings (expanded/compressed, case, leading zeroes and
|
|
52
|
+
// IPv4-mapped forms) cannot silently bypass a proxy override. A bracketed
|
|
53
|
+
// non-literal keeps its spelling, and invalid/URL-unrepresentable colon forms
|
|
54
|
+
// retain the previous lowercase-only fallback rather than widening admission.
|
|
37
55
|
if (host.startsWith('[') && host.endsWith(']')) {
|
|
38
56
|
const literal = host.slice(1, -1).trim();
|
|
39
57
|
if (literal === '')
|
|
40
58
|
return '';
|
|
41
|
-
return
|
|
59
|
+
return canonicalIpv6Literal(literal)
|
|
60
|
+
?? (literal.includes(':') ? literal.toLowerCase() : host.toLowerCase());
|
|
42
61
|
}
|
|
43
62
|
if (host.includes(':'))
|
|
44
|
-
return host.toLowerCase();
|
|
63
|
+
return canonicalIpv6Literal(host) ?? host.toLowerCase();
|
|
45
64
|
const ascii = domainToASCII(host);
|
|
46
65
|
return (ascii || host).toLowerCase();
|
|
47
66
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vision-router",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain + pixel-level tools. Remote vision providers, including the default free fallback, receive image content unless strict local-only use is configured.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|