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 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.2)**
45
+ > 📌 **Announcement (v3.0.3)**
46
46
  >
47
- > **v3.0.2: hardening from a full-repository adversarial review.** Instant local describe now
48
- > follows the same `proxy` / `proxyHosts` decision as every other provider leg; credential
49
- > redaction is one shared shape list for the logs and the failure diagnostics; adapter ownership
50
- > no longer accepts a primitive marker; proxy host admission normalizes IPv6 spellings and makes
51
- > `*.host` entries work; the background stop cache can neither go stale nor vanish; and failure
52
- > classification, numeric settings and the retained-image walker are hardened against inherited
53
- > keys, hostile accessors and pathological nesting. Supported Hosts are unchanged: DSH 0.1.x from
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.2)**
45
+ > 📌 **公告(v3.0.3)**
46
46
  >
47
- > **v3.0.2:一轮全仓对抗审查带来的加固。** instant 本地描述现在与其他 provider 腿遵守同一套
48
- > `proxy` / `proxyHosts` 判定;日志与失败诊断的凭据脱敏合并为一份共享清单;适配器归属不再接受
49
- > 基本类型标记;代理主机准入归一了 IPv6 写法并让 `*.host` 条目真正生效;后台停记录缓存既不残留
50
- > 也不会消失;失败分类、数值设置与 retained-image 遍历也分别对继承键、恶意取值器和畸形嵌套做了
51
- > 加固。支持的 Host 不变:DSH 0.1.x 自 `0.1.5-rc.1` 起,DSH 0.2.x 自已验证的 `0.2.0-rc.2` 起。
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 that mounts below the reader is the same as not being shown: the card
1000
- // renders next to its tool call, and a reader who is at the end of a long conversation has
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
- if (typeof document === 'undefined') return false;
1028
- var cards = document.querySelectorAll('[data-dvr-present]');
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
- // Follow the app language: register our dictionaries and re-read them
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
- React.useEffect(() => { if (globalThis.__dvrPresentFocus) globalThis.__dvrPresentFocus() }, [imageKeySignature])
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
- // Cookies are `name=value; name=value` on one line and there is no safe way to
24
- // guess which pair carries the session, so the whole header value goes.
25
- [/\b((?:Set-)?Cookie\s*[:=]\s*)[^\r\n]+/gi, `$1${REDACTED}`],
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}@`],
@@ -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
- // A `*.host` entry means "this host and its subdomains", which is exactly what the matcher
26
- // already does for a bare hostname. `domainToASCII` rejects the star, so leaving it in place
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 keeps IPv6 literals bracketed. domainToASCII does not
33
- // accept an IPv6 literal at all, so case-fold it directly. The brackets are
34
- // pure spelling for the same address: stripping them is what lets a configured
35
- // `::1` match a URL-derived `[::1]`. A bracketed non-literal (say
36
- // `[example.com]`) keeps its spelling, so nothing about DNS is widened.
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 literal.includes(':') ? literal.toLowerCase() : host.toLowerCase();
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.2",
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": {