dsh-vision-router 1.7.1 → 1.7.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/cordis.patch.yml CHANGED
@@ -4,9 +4,9 @@
4
4
  # without any manual cordis.patch.yml edits. Later layers (the profile's own
5
5
  # cordis.patch.yml, --patch overlays) override these rows by id.
6
6
 
7
- # 纯增量补丁(issue #34):不碰核心行。DSH rc.6仍保留旧版隐身接管兼容路径;
8
- # DSH rc.7则始终由宿主拥有deepseek-official,插件只提供「+ 自动识图」包装路由。
9
- # 因此这里永远不默认禁用官方llm-deepseek行。
7
+ # 纯增量补丁(issue #34):不碰核心行。最低支持 Host 仍保留旧兼容路径;
8
+ # batch-attachment 合同的 Host 始终拥有 deepseek-official,插件只提供
9
+ # 「+ 自动识图」包装路由,因此这里永远不默认禁用官方 llm-deepseek 行。
10
10
 
11
11
  # 挂载插件行。默认保持完整视觉工具表常驻(issue #81):虽然渐进挂载可以
12
12
  # 少发一小段工具 schema,但图片轮首次扩展工具列表会改变请求前缀,可能让
@@ -18,14 +18,27 @@
18
18
  config:
19
19
  progressiveTools: false
20
20
 
21
- # 放宽附件图片限制(宿主默认 5MB / 4000 万像素 → 20MB / 1 亿像素),
22
- # 超长聊天记录截图(vision_long_screenshot_ocr 主场景)与大尺寸设计稿/
23
- # 扫描图可过宿主准入。rc6 与 rc7 的 @deepseek-ai/dsh-attachment-local
24
- # 均接受这两个可选字段(0.1.0-rc.6 / 0.1.0-rc.7 schema 已核对),且两边
25
- # dsh-base bundle 都存在 `- id: attachment-local` 行,因此该补丁在两个
26
- # 运行时上都有效、不破坏 host-neutral 原则;字段可选,不需要可在 profile
27
- # 补丁层覆写。
21
+ # Vision Router 的附件产品契约:Host attachment store 负责保存用户提交的
22
+ # durable 原件,具体视觉后端负责本次推理所需的 resize/tile/request budget。
23
+ # 因此存储准入保持 20MiB / 1 亿像素,同时在 DSH rc.8 新增的单边限制上
24
+ # 显式选择 10000px:覆盖 4K/手机原图和常见长截图,但不把 Host 的全局
25
+ # 极端长边安全闸无限放开。该值作用于整个 attachment-local(包括原生视觉
26
+ # provider),所以这里刻意不用 32768/无限;更大的部署需求仍可由后续
27
+ # profile/--patch 层按实际 provider 能力覆盖。
28
+ #
29
+ # DSH 的 patch 是“整段 config 替换”而非深度合并。旧版 Vision Router 曾在
30
+ # profile 自己的 cordis.patch.yml 写入同一行的 20MiB/100MP 两个字段;这层
31
+ # 比 bundle 晚,会把这里新加的 maxImageDimension 擦掉,让 rc.8 又落回 2000。
32
+ # 1.7.2 起运行时只对“20MiB + 100MP + rc.8 默认 2000px”这一历史指纹做一次
33
+ # 内存态迁移到 10000px;显式自定义过 dimension 或其他附件策略一律不碰。
34
+ # 这样升级旧 profile 不要求用户手工清理配置,同时后续 profile/--patch 的
35
+ # 明确部署策略仍保持最终所有权。
36
+ #
37
+ # rc.6/rc.7 没有 maxImageDimension;发布合同 CI 会用真实旧包验证额外配置键
38
+ # 不会破坏最低支持 Host。若旧 Host 开始严格拒绝未知键,此处必须改为按能力
39
+ # 生成配置,而不是靠版本字符串分支。
28
40
  - id: attachment-local
29
41
  config:
30
42
  maxImageBytes: 20971520
31
43
  maxImagePixels: 100000000
44
+ maxImageDimension: 10000
package/entry.js CHANGED
@@ -15,6 +15,7 @@ import { contextWithVisionExecutionPolicy } from './lib/vision-execution-policy.
15
15
  import { installLiveModelDiscovery } from './lib/live-model-discovery.js'
16
16
  import { installVisionModelRegistry } from './lib/vision-model-registry.js'
17
17
  import { installLiveModelClientPrelude } from './lib/live-model-client-prelude.js'
18
+ import { installClientPresentationBoundary } from './lib/client-presentation-boundary.js'
18
19
  import { installAdversarialHardening } from './lib/adversarial-hardening.js'
19
20
  import { installOllamaColdStartGuard } from './lib/ollama-cold-start.js'
20
21
  import { installLocalVisionStabilizer } from './lib/local-vision-stabilizer.js'
@@ -31,9 +32,10 @@ import {
31
32
  } from './lib/structured-flow-hardening.js'
32
33
  import {
33
34
  attachmentContextForContract,
34
- installRc7SettingsCompatibility,
35
- isRc7ContractRuntime,
36
- protectRc7ProviderOwnership,
35
+ ensureVisionAttachmentAdmissionPolicy,
36
+ hasBatchAttachmentContract,
37
+ installHostSettingsCompatibility,
38
+ protectHostProviderOwnership,
37
39
  } from './lib/dsh-contract-compat.js'
38
40
 
39
41
  // Increment whenever the browser-visible settings contract gains a field whose
@@ -56,7 +58,7 @@ core.Config.set('visionTurnBudgetMs', z.number().step(1000).min(10000).max(60000
56
58
  // Both visible entry points — Settings > Vision Router and the legacy
57
59
  // Settings > Plugins compatibility card — edit the same Host-owned namespace.
58
60
  // Keep the depth enum and custom cap on this final public contract so either
59
- // entry serializes exactly the same shape and rc.7 Host persistence accepts it.
61
+ // entry serializes exactly the same shape on every supported Host generation.
60
62
  core.Config.set('visionDepth', z.union(['fast', 'standard', 'deep', 'custom']).default('standard'))
61
63
  core.Config.set('visionDepthMaxCalls', z.number().step(1).min(0).max(100).default(0))
62
64
 
@@ -79,6 +81,12 @@ core.Config.set(
79
81
  export * from './index.js'
80
82
  export {
81
83
  attachmentContextForContract,
84
+ ensureVisionAttachmentAdmissionPolicy,
85
+ hasBatchAttachmentContract,
86
+ installHostSettingsCompatibility,
87
+ protectHostProviderOwnership,
88
+ // Transitional public aliases retained for callers/tests written during the
89
+ // rc.7 compatibility pass. Runtime code below no longer branches on names.
82
90
  installRc7SettingsCompatibility,
83
91
  isRc7ContractRuntime,
84
92
  protectRc7ProviderOwnership,
@@ -106,7 +114,7 @@ export function apply(ctx, config = {}) {
106
114
  : 'deepseek-vision',
107
115
  visionConfig: config,
108
116
  })
109
- // DSH rc.7 pi-ai replay v2 stores the real producer under
117
+ // Newer pi-ai replay envelopes store the real producer under
110
118
  // replayState.response.{provider,model}; the older delegated-replay shim
111
119
  // recognizes the pre-v2 top-level shape. Layer a narrow private compatibility
112
120
  // view so resumed wrapper history keeps provider-native replay metadata rather
@@ -150,21 +158,35 @@ export function apply(ctx, config = {}) {
150
158
  ? Number(bootConfig.visionTurnBudgetMs)
151
159
  : 90000,
152
160
  }
153
- const rc7 = isRc7ContractRuntime(stabilizedCtx)
161
+ // The batch-attachment API is the released, non-incidental discriminator
162
+ // between the minimum Host contract and the newer Host-owned integration
163
+ // generation. Keep the branch named after that observable capability rather
164
+ // than a release number so rc.8+ naturally follows the same public contract.
165
+ const batchAttachmentHost = hasBatchAttachmentContract(stabilizedCtx)
166
+ // DSH profile overlays replace an attachment-local config object wholesale.
167
+ // A stale pre-rc.8 Vision Router profile row can therefore erase the newer
168
+ // bundle's maxImageDimension and silently restore rc.8's 2000px default even
169
+ // after the plugin package itself has updated. Repair only that historical
170
+ // 20MiB/100MP fingerprint; explicit deployment policies remain authoritative.
171
+ if (batchAttachmentHost) {
172
+ ensureVisionAttachmentAdmissionPolicy(stabilizedCtx, logging.logger)
173
+ }
154
174
  // The remote settings bridge uses DSH Connection's trusted-host carrier
155
175
  // fence and its own safe-field capability allow-list. Main's local Web
156
176
  // mutation boundary continues to protect the independent /_dsh write routes.
157
177
  installVisionRouterRemoteSettingsBridge(stabilizedCtx, logging.logger)
158
- const ownershipCtx = rc7 ? protectRc7ProviderOwnership(stabilizedCtx) : stabilizedCtx
159
- const settingsCtx = rc7
160
- ? installRc7SettingsCompatibility(ownershipCtx, { ...runtimeConfig, stealth: false }, {
178
+ const ownershipCtx = batchAttachmentHost
179
+ ? protectHostProviderOwnership(stabilizedCtx)
180
+ : stabilizedCtx
181
+ const settingsCtx = batchAttachmentHost
182
+ ? installHostSettingsCompatibility(ownershipCtx, { ...runtimeConfig, stealth: false }, {
161
183
  namespace: 'vision-router',
162
184
  Config: core.Config,
163
185
  })
164
186
  : ownershipCtx
165
- // rc.6/Termux keeps the narrow process-local fallback that was required by
166
- // the old attachment-local durability walk. rc.7 formalizes AttachmentId as
167
- // store-owned, so never synthesize one there: host persistence errors remain
187
+ // The minimum Host keeps the narrow process-local fallback required by the
188
+ // old attachment-local durability walk. Batch-capable hosts keep AttachmentId
189
+ // store-owned, so never fabricate one there: host persistence errors remain
168
190
  // authoritative and diagnosable instead of creating a false durable ref.
169
191
  const attachmentCompatCtx = attachmentContextForContract(settingsCtx, logging.logger, {
170
192
  installAndroidAttachmentCompat,
@@ -174,7 +196,7 @@ export function apply(ctx, config = {}) {
174
196
  // one-shot, enforces fast/standard/deep/custom quotas, tracks mixed branches,
175
197
  // rejects empty/non-evidence results, and applies one shared visual deadline.
176
198
  const structuredCtx = installStructuredFlowHardening(attachmentCompatCtx, runtimeConfig)
177
- // DSH rc.7 publishes llm/adapters-updated synchronously from inside
199
+ // Newer DSH releases publish llm/adapters-updated synchronously from inside
178
200
  // registerAdapter(). Coalesce only Vision Router's listener: nested events
179
201
  // mark the topology dirty and the outer pass reruns to a fixed point, so we
180
202
  // neither double-register a twin nor lose a provider added mid-pass.
@@ -196,6 +218,12 @@ export function apply(ctx, config = {}) {
196
218
  // source strictly for diagnostics (`known` vs `live`) without changing the
197
219
  // admission decision.
198
220
  installVisionModelRegistry(reconciledCtx, liveDiscovery, { config: runtimeConfig })
221
+ // rc.8 turns ui-attachment into a dynamic presentation plugin and no longer
222
+ // exports its React implementation as package values. Install a narrowly
223
+ // scoped browser boundary that supplies Vision Router's own lightweight
224
+ // gallery to the legacy 1.7.x client factory, so the official package is
225
+ // never value-required at runtime and remains free to evolve independently.
226
+ installClientPresentationBoundary(reconciledCtx)
199
227
  // Keep endpoint-discovered ids private to Vision Router's settings client:
200
228
  // the prelude wraps this package's browser context rather than changing the
201
229
  // global llm.models response (which would expose UNKNOWN_MODEL entries in the
@@ -229,7 +257,7 @@ export function apply(ctx, config = {}) {
229
257
  const lms = c.localLmStudio && typeof c.localLmStudio === 'object' ? c.localLmStudio : {}
230
258
  logging.logger.info(
231
259
  'vision-router: base config summary — contract=%s instantDescribe=%s localDescribeStyle=%s localOllama=%s localLmStudio=%s',
232
- rc7 ? 'rc7' : 'rc6',
260
+ batchAttachmentHost ? 'batch-attachments' : 'single-attachment',
233
261
  c.instantDescribe === true ? 'on' : 'off',
234
262
  c.localDescribeStyle === 'structured' ? 'structured' : 'plain',
235
263
  local.enabled === true ? 'on' : 'off',
@@ -240,11 +268,10 @@ export function apply(ctx, config = {}) {
240
268
  }
241
269
  try {
242
270
  const result = core.apply(executionCtx, runtimeConfig)
243
- // DSH rc.7's Settings -> Models surface is backed by the configurable
244
- // provider directory, not by the live adapter registry alone. Publish the
245
- // main DeepSeek + 自动识图 route as a derived alias of official DeepSeek so
246
- // a reinstall restores the expected model-group row without making an
247
- // arbitrary textProvider look like DeepSeek. On rc.6 the helper is inert.
271
+ // On newer Hosts the Settings -> Models surface is backed by the
272
+ // configurable-provider directory, not by the live adapter registry alone.
273
+ // Publish the main DeepSeek + 自动识图 route as a derived alias of official
274
+ // DeepSeek. On older Hosts the helper feature-detects and stays inert.
248
275
  installWrapperDirectoryAlias(attachmentCompatCtx, runtimeConfig, logging.logger)
249
276
  if (result && typeof result.then === 'function') {
250
277
  return result.catch((error) => {
@@ -0,0 +1,271 @@
1
+ const CLIENT_PRESENTATION_MARK = 'data-vision-router-presentation-boundary'
2
+
3
+ /**
4
+ * Browser prelude for the one legacy value edge left in lib/client.js.
5
+ *
6
+ * DSH rc.8 made @deepseek-ai/dsh-client-ui-attachment a dynamic presentation
7
+ * plugin and deliberately stopped exporting its React implementation as
8
+ * package values. Vision Router must therefore own the presentation of its
9
+ * `vision_present` tool card instead of reaching into that package.
10
+ *
11
+ * The current 1.7.x browser artifact is a hand-maintained self-registering
12
+ * factory. Rewriting the entire artifact just to replace one value edge would
13
+ * create a much larger compatibility diff, so this boundary intercepts that
14
+ * exact legacy require *before it can reach the DSH module table* and supplies
15
+ * a small plugin-owned gallery with the same narrow props the card already
16
+ * passes. No DSH attachment UI code or CSS is copied.
17
+ *
18
+ * This is intentionally a compatibility boundary, not a general module alias:
19
+ * only dsh-vision-router's own factory and only the historical attachment
20
+ * specifier are intercepted. A future modular client can move these components
21
+ * into its source directly and delete this prelude without changing card data
22
+ * loading or Host attachment ownership.
23
+ */
24
+ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
25
+ 'use strict';
26
+ var TARGET = 'dsh-vision-router';
27
+ var LEGACY_ATTACHMENT_VALUE = '@deepseek-ai/dsh-client-ui-attachment';
28
+
29
+ function createPresentation(React) {
30
+ function PresentedImage(props) {
31
+ var attachment = props.attachment;
32
+ var load = props.load;
33
+ var labels = props.labels;
34
+ var tile = props.tile === true;
35
+ var state = React.useState(null);
36
+ var src = state[0];
37
+ var setSrc = state[1];
38
+ var failedState = React.useState(false);
39
+ var failed = failedState[0];
40
+ var setFailed = failedState[1];
41
+ var attemptState = React.useState(0);
42
+ var attempt = attemptState[0];
43
+ var setAttempt = attemptState[1];
44
+ var openState = React.useState(false);
45
+ var open = openState[0];
46
+ var setOpen = openState[1];
47
+
48
+ React.useEffect(function(){
49
+ var live = true;
50
+ setSrc(null);
51
+ setFailed(false);
52
+ Promise.resolve().then(function(){ return load(attachment); }).then(
53
+ function(url){ if (live) setSrc(url); },
54
+ function(){ if (live) setFailed(true); }
55
+ );
56
+ return function(){ live = false; };
57
+ }, [attachment, load, attempt]);
58
+
59
+ React.useEffect(function(){
60
+ if (!open || typeof document === 'undefined') return undefined;
61
+ var onKeyDown = function(event){ if (event && event.key === 'Escape') setOpen(false); };
62
+ document.addEventListener('keydown', onKeyDown);
63
+ return function(){ document.removeEventListener('keydown', onKeyDown); };
64
+ }, [open]);
65
+
66
+ var label = attachment && attachment.name ? attachment.name : labels.image;
67
+ var box = tile
68
+ ? { width: 64, height: 64 }
69
+ : { maxWidth: 240, maxHeight: 240 };
70
+ var frameStyle = Object.assign({
71
+ appearance: 'none',
72
+ border: '1px solid var(--dsw-alias-border-l2)',
73
+ borderRadius: 8,
74
+ background: 'var(--dsw-alias-bg-layer-3)',
75
+ padding: 0,
76
+ overflow: 'hidden',
77
+ display: 'flex',
78
+ alignItems: 'center',
79
+ justifyContent: 'center',
80
+ cursor: src ? 'zoom-in' : failed ? 'pointer' : 'default'
81
+ }, box);
82
+
83
+ if (failed) {
84
+ return React.createElement('button', {
85
+ type: 'button',
86
+ title: labels.loadFailed,
87
+ onClick: function(){ setAttempt(function(value){ return value + 1; }); },
88
+ style: Object.assign({}, frameStyle, {
89
+ minWidth: tile ? 64 : 120,
90
+ minHeight: tile ? 64 : 72,
91
+ color: 'var(--dsw-alias-label-tertiary)',
92
+ font: 'inherit',
93
+ fontSize: 12,
94
+ padding: 8
95
+ })
96
+ }, labels.loadFailed);
97
+ }
98
+
99
+ var thumb = React.createElement('button', {
100
+ type: 'button',
101
+ title: labels.open,
102
+ 'aria-label': labels.openNamed(label),
103
+ disabled: !src,
104
+ onClick: function(){ if (src) setOpen(true); },
105
+ style: frameStyle
106
+ }, src
107
+ ? React.createElement('img', {
108
+ src: src,
109
+ alt: label,
110
+ style: tile
111
+ ? { width: '100%', height: '100%', objectFit: 'cover', display: 'block' }
112
+ : { maxWidth: 240, maxHeight: 240, width: 'auto', height: 'auto', display: 'block' }
113
+ })
114
+ : React.createElement('span', {
115
+ style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12, padding: 10 }
116
+ }, labels.loading)
117
+ );
118
+
119
+ if (!open || !src) return thumb;
120
+ var overlay = React.createElement('div', {
121
+ role: 'dialog',
122
+ 'aria-modal': 'true',
123
+ 'aria-label': labels.lightbox.dialog,
124
+ onClick: function(event){ if (event.target === event.currentTarget) setOpen(false); },
125
+ style: {
126
+ position: 'fixed', inset: 0, zIndex: 11000, background: '#000b',
127
+ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24,
128
+ boxSizing: 'border-box'
129
+ }
130
+ },
131
+ React.createElement('img', {
132
+ src: src,
133
+ alt: label,
134
+ style: { maxWidth: '92vw', maxHeight: '88vh', objectFit: 'contain', borderRadius: 8 }
135
+ }),
136
+ React.createElement('button', {
137
+ type: 'button',
138
+ 'aria-label': labels.lightbox.close,
139
+ title: labels.lightbox.close,
140
+ onClick: function(){ setOpen(false); },
141
+ style: {
142
+ position: 'fixed', top: 16, right: 18, width: 36, height: 36,
143
+ borderRadius: 18, border: '1px solid #ffffff55', background: '#111c',
144
+ color: '#fff', font: 'inherit', fontSize: 22, cursor: 'pointer'
145
+ }
146
+ }, '×')
147
+ );
148
+ return React.createElement(React.Fragment, null, thumb, overlay);
149
+ }
150
+
151
+ function ImageGallery(props) {
152
+ var images = Array.isArray(props.images) ? props.images : [];
153
+ if (images.length === 0) return null;
154
+ var tile = images.length > 1;
155
+ return React.createElement('div', {
156
+ style: {
157
+ display: 'flex', flexWrap: 'wrap', gap: 6,
158
+ justifyContent: props.align === 'end' ? 'flex-end' : 'flex-start',
159
+ alignItems: 'flex-start', maxWidth: '100%'
160
+ }
161
+ }, images.map(function(image, index){
162
+ var attachment = image && image.attachment;
163
+ var key = attachment && attachment.attachmentId
164
+ ? String(attachment.attachmentId) + ':' + index
165
+ : String(index);
166
+ return React.createElement(PresentedImage, {
167
+ key: key,
168
+ attachment: attachment,
169
+ load: props.load,
170
+ labels: props.labels,
171
+ tile: tile
172
+ });
173
+ }));
174
+ }
175
+
176
+ return { ImageGallery: ImageGallery };
177
+ }
178
+
179
+ function patchLoader(loader) {
180
+ if (!loader || (typeof loader !== 'object' && typeof loader !== 'function')) return;
181
+
182
+ if (typeof loader.load === 'function' && !loader.load.__visionRouterPresentationBoundary) {
183
+ var original = loader.load;
184
+ function load(spec) {
185
+ if (spec && spec.id === TARGET && typeof spec.factory === 'function') {
186
+ var factory = spec.factory;
187
+ spec = Object.assign({}, spec, {
188
+ factory: function(require) {
189
+ var React = require('react');
190
+ var presentation = createPresentation(React);
191
+ function scopedRequire(id) {
192
+ if (id === LEGACY_ATTACHMENT_VALUE) return presentation;
193
+ return require(id);
194
+ }
195
+ return factory(scopedRequire);
196
+ }
197
+ });
198
+ }
199
+ return original.call(this, spec);
200
+ }
201
+ Object.defineProperty(load, '__visionRouterPresentationBoundary', { value: true });
202
+ loader.load = load;
203
+ }
204
+
205
+ // rc.8's parser installs a queue-mode facade first. When the Web shell
206
+ // later calls create(), ClientModuleSystem switches that *same object* to
207
+ // live mode by assigning a brand-new loader.load function. That assignment
208
+ // necessarily erases every queue-time wrapper. Wrap create itself so the
209
+ // boundary is re-applied immediately after the official queue -> live
210
+ // transition and before lazy third-party bundles can register.
211
+ if (typeof loader.create === 'function' && !loader.create.__visionRouterPresentationBoundary) {
212
+ var originalCreate = loader.create;
213
+ function create() {
214
+ var result = originalCreate.apply(this, arguments);
215
+ patchLoader(loader);
216
+ return result;
217
+ }
218
+ Object.defineProperty(create, '__visionRouterPresentationBoundary', { value: true });
219
+ loader.create = create;
220
+ }
221
+ }
222
+
223
+ function install() {
224
+ if (window.__ModuleLoader__) {
225
+ patchLoader(window.__ModuleLoader__);
226
+ return;
227
+ }
228
+ var descriptor = Object.getOwnPropertyDescriptor(window, '__ModuleLoader__');
229
+ if (descriptor && descriptor.configurable === false) return;
230
+ var stored;
231
+ Object.defineProperty(window, '__ModuleLoader__', {
232
+ configurable: true,
233
+ enumerable: true,
234
+ get: function(){ return stored; },
235
+ set: function(value) {
236
+ stored = value;
237
+ patchLoader(value);
238
+ Object.defineProperty(window, '__ModuleLoader__', {
239
+ configurable: true,
240
+ enumerable: true,
241
+ writable: true,
242
+ value: stored
243
+ });
244
+ }
245
+ });
246
+ }
247
+
248
+ install();
249
+ })();`
250
+
251
+ export function injectClientPresentationBoundary(html) {
252
+ if (typeof html !== 'string' || html.includes(CLIENT_PRESENTATION_MARK)) return html
253
+ const safe = CLIENT_PRESENTATION_PRELUDE.replace(/<\/script/gi, '<\\/script')
254
+ const script = `<script ${CLIENT_PRESENTATION_MARK}>${safe}</script>`
255
+ // DSH's client-modules index tap prepends the queue-mode loader bootstrap at
256
+ // <head>. Run after that parser bootstrap so we can wrap both load() and the
257
+ // later create() transition. The create wrapper re-applies the boundary when
258
+ // rc.8 replaces loader.load while entering live mode.
259
+ const closeHead = html.indexOf('</head>')
260
+ if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
261
+ return `${html}${script}`
262
+ }
263
+
264
+ export function installClientPresentationBoundary(ctx) {
265
+ ctx?.inject?.(['webServer'], (webCtx) => {
266
+ webCtx.effect(
267
+ () => webCtx.webServer.tapIndex(injectClientPresentationBoundary),
268
+ 'vision-router: client presentation boundary',
269
+ )
270
+ })
271
+ }
@@ -1,4 +1,9 @@
1
- const RC7_OWNED_ROUTES = new Set(['deepseek-official', 'deepseek-official-native'])
1
+ const HOST_OWNED_ROUTES = new Set(['deepseek-official', 'deepseek-official-native'])
2
+
3
+ const LEGACY_VISION_ATTACHMENT_BYTES = 20 * 1024 * 1024
4
+ const LEGACY_VISION_ATTACHMENT_PIXELS = 100_000_000
5
+ const DSH_RC8_DEFAULT_IMAGE_DIMENSION = 2000
6
+ const VISION_ROUTER_IMAGE_DIMENSION = 10_000
2
7
 
3
8
  function attachmentStoreOf(ctx) {
4
9
  if (!ctx || typeof ctx !== 'object') return undefined
@@ -19,33 +24,119 @@ function attachmentStoreOf(ctx) {
19
24
  }
20
25
 
21
26
  /**
22
- * Distinguish the released rc.6/rc.7 attachment contracts, not incidental
23
- * LLM capabilities. rc.6 already shipped registerConfigurableProviders(), so
24
- * that method cannot identify rc.7. rc.7's AttachmentStore adds saveImages()
25
- * while rc.6 exposes only validateImage/saveImage/readImage.
27
+ * Detect the released batch-attachment contract directly. DSH rc.6 exposes
28
+ * validateImage/saveImage/readImage only; rc.7+ adds AttachmentStore.saveImages().
29
+ * Do not infer this from unrelated LLM/settings methods: rc.6 already shipped
30
+ * registerConfigurableProviders(), so those methods cannot prove the attachment
31
+ * ownership generation.
26
32
  */
27
- export function isRc7ContractRuntime(ctx) {
33
+ export function hasBatchAttachmentContract(ctx) {
28
34
  const store = attachmentStoreOf(ctx)
29
35
  return !!store && typeof store.saveImages === 'function'
30
36
  }
31
37
 
32
38
  /**
33
- * Preserve rc.7 provider ownership: Vision Router may wrap/delegate the
34
- * official provider, but cannot synthesize or replace it. Other adapters pass
35
- * through unchanged.
39
+ * Repair one historical Vision Router profile overlay on DSH rc.8.
40
+ *
41
+ * Before rc.8 introduced maxImageDimension, Vision Router wrote a profile-local
42
+ * attachment-local override containing only 20MiB / 100MP. DSH patch rows
43
+ * replace the WHOLE config object instead of deep-merging it, so that later
44
+ * profile layer shadows the newer bundle row and rc.8 silently rematerializes
45
+ * its own 2000px default. Plugin updates cannot delete a user's profile layer.
46
+ *
47
+ * The historical 20MiB + 100MP pair is therefore the migration fingerprint.
48
+ * Only when that exact pair is combined with rc.8's exact 2000px default do we
49
+ * raise the live store policy to Vision Router's 10000px contract. Explicitly
50
+ * customised dimensions or unrelated attachment deployments are never touched.
51
+ * LocalAttachmentStore reads `this.imageLimits` for every validation/save, so
52
+ * replacing this immutable policy object repairs authoritative admission as
53
+ * well as the limits projected to the browser for error copy.
36
54
  */
37
- export function protectRc7ProviderOwnership(ctx) {
55
+ export function ensureVisionAttachmentAdmissionPolicy(ctx, logger, options = {}) {
56
+ const store = attachmentStoreOf(ctx)
57
+ const limits = store && store.imageLimits
58
+ if (!store || !limits || typeof limits !== 'object') {
59
+ return { changed: false, reason: 'attachment-limits-unavailable' }
60
+ }
61
+
62
+ const targetDimension = Number.isInteger(options.maxImageDimension)
63
+ ? Math.max(1, options.maxImageDimension)
64
+ : VISION_ROUTER_IMAGE_DIMENSION
65
+ const legacy =
66
+ limits.maxImageBytes === LEGACY_VISION_ATTACHMENT_BYTES &&
67
+ limits.maxImagePixels === LEGACY_VISION_ATTACHMENT_PIXELS &&
68
+ limits.maxImageDimension === DSH_RC8_DEFAULT_IMAGE_DIMENSION
69
+
70
+ if (!legacy) return { changed: false, reason: 'not-legacy-overlay', limits }
71
+
72
+ const next = Object.freeze({ ...limits, maxImageDimension: targetDimension })
73
+ let changed = false
74
+ try {
75
+ changed = Reflect.set(store, 'imageLimits', next)
76
+ } catch {
77
+ changed = false
78
+ }
79
+ if (!changed || store.imageLimits !== next) {
80
+ try {
81
+ logger?.warn?.(
82
+ 'vision-router: detected the legacy attachment-local 20MiB/100MP overlay but could not repair maxImageDimension; DSH may still enforce 2000px',
83
+ )
84
+ } catch {
85
+ // Diagnostics must never break Host startup.
86
+ }
87
+ return { changed: false, reason: 'limits-readonly', limits }
88
+ }
89
+
90
+ try {
91
+ logger?.info?.(
92
+ 'vision-router: repaired legacy attachment-local overlay — maxImageDimension %d -> %d',
93
+ DSH_RC8_DEFAULT_IMAGE_DIMENSION,
94
+ targetDimension,
95
+ )
96
+ } catch {
97
+ // Diagnostics must never break Host startup.
98
+ }
99
+ return { changed: true, reason: 'legacy-overlay-repaired', limits: next }
100
+ }
101
+
102
+ /**
103
+ * Keep the migration attached to the service lifecycle, not only initial boot.
104
+ * DSH hot-reloads profile/home patch files transactionally; attachment-local can
105
+ * therefore be reconstructed after Vision Router has already started. Cordis'
106
+ * service injection reruns this narrow check for each replacement instance.
107
+ */
108
+ export function installVisionAttachmentAdmissionPolicy(ctx, logger, options = {}) {
109
+ const initial = ensureVisionAttachmentAdmissionPolicy(ctx, logger, options)
110
+ try {
111
+ if (ctx && typeof ctx.inject === 'function') {
112
+ ctx.inject(['attachments'], (attachmentCtx) => {
113
+ ensureVisionAttachmentAdmissionPolicy(attachmentCtx, logger, options)
114
+ })
115
+ }
116
+ } catch {
117
+ // Older/partial contexts may not expose lifecycle injection. The initial
118
+ // repair above is still authoritative for the currently mounted store.
119
+ }
120
+ return initial
121
+ }
122
+
123
+ /**
124
+ * Preserve host ownership of official DeepSeek routes on the batch-attachment
125
+ * contract generation. Vision Router may wrap/delegate those routes, but must
126
+ * not synthesize or replace them. Other adapters pass through unchanged.
127
+ */
128
+ export function protectHostProviderOwnership(ctx) {
38
129
  if (!ctx || typeof ctx !== 'object' || !ctx.llm || typeof ctx.llm !== 'object') return ctx
39
130
  const llm = new Proxy(ctx.llm, {
40
131
  get(target, property) {
41
132
  if (property === 'registerAdapter') {
42
133
  return (routes, adapter) => {
43
134
  const list = Array.isArray(routes) ? routes : [routes]
44
- if (list.some((route) => RC7_OWNED_ROUTES.has(String(route)))) {
135
+ if (list.some((route) => HOST_OWNED_ROUTES.has(String(route)))) {
45
136
  const error = new Error(
46
- 'vision-router: DSH rc.7 owns deepseek-official; use the auto-vision wrapper instead of provider takeover',
137
+ 'vision-router: DSH owns deepseek-official on this host contract; use the auto-vision wrapper instead of provider takeover',
47
138
  )
48
- error.code = 'DSH_RC7_PROVIDER_OWNERSHIP'
139
+ error.code = 'DSH_HOST_PROVIDER_OWNERSHIP'
49
140
  throw error
50
141
  }
51
142
  return target.registerAdapter(routes, adapter)
@@ -65,9 +156,9 @@ export function protectRc7ProviderOwnership(ctx) {
65
156
  }
66
157
 
67
158
  /**
68
- * The common rc.6/rc.7 SettingsProvider service is the stable public seam.
69
- * This helper mirrors installSettingsSection's attach/detach semantics without
70
- * forcing a new package-resolution edge into older rc.6 profiles.
159
+ * The common rc.6+ SettingsProvider service is the stable public seam. This
160
+ * helper mirrors installSettingsSection's attach/detach semantics without
161
+ * forcing a newer package-resolution edge into the minimum-supported host.
71
162
  */
72
163
  export function installSettingsSectionCompat(ctx, namespace, Config, entryConfig, hooks) {
73
164
  hooks.setSource(() => entryConfig)
@@ -82,22 +173,23 @@ export function installSettingsSectionCompat(ctx, namespace, Config, entryConfig
82
173
  hooks.setSource(() => entryConfig)
83
174
  hooks.onChange()
84
175
  },
85
- 'vision-router: rc7 settings compatibility source',
176
+ 'vision-router: host settings compatibility source',
86
177
  )
87
178
  })
88
179
  }
89
180
 
90
181
  /**
91
- * Bridge rc.7's public settings-section semantics to core's rc.6-shaped
92
- * `ctx.inject(['settings']) -> settings.register()` callback. The stored rc.6
93
- * `stealth` flag is masked on rc.7 because provider takeover is not supported.
182
+ * Bridge the newer public settings-section semantics to core's legacy-shaped
183
+ * `ctx.inject(['settings']) -> settings.register()` callback. The old `stealth`
184
+ * flag is masked because host-owned official routes cannot be taken over on
185
+ * this contract generation.
94
186
  */
95
- export function installRc7SettingsCompatibility(ctx, entryConfig, options = {}) {
187
+ export function installHostSettingsCompatibility(ctx, entryConfig, options = {}) {
96
188
  const install = options.installSettingsSection ?? installSettingsSectionCompat
97
189
  const ns = options.namespace
98
190
  const Config = options.Config
99
191
  if (Config === undefined) {
100
- throw new TypeError('vision-router: rc7 settings compatibility requires Config')
192
+ throw new TypeError('vision-router: host settings compatibility requires Config')
101
193
  }
102
194
 
103
195
  let activeSource = () => ({ ...entryConfig, stealth: false })
@@ -172,13 +264,20 @@ export function installRc7SettingsCompatibility(ctx, entryConfig, options = {})
172
264
  }
173
265
 
174
266
  /**
175
- * rc.6 keeps the narrow Termux fallback. rc.7's batch-capable attachment
176
- * service keeps AttachmentId store-owned, so the host context passes through
177
- * untouched there.
267
+ * The minimum-supported host keeps the narrow Termux fallback. A
268
+ * batch-capable attachment service keeps AttachmentId store-owned, so that
269
+ * host context passes through untouched.
178
270
  */
179
271
  export function attachmentContextForContract(ctx, logger, options = {}) {
180
- if (isRc7ContractRuntime(ctx)) return ctx
272
+ if (hasBatchAttachmentContract(ctx)) return ctx
181
273
  const install = options.installAndroidAttachmentCompat
182
274
  if (typeof install !== 'function') return ctx
183
275
  return install(ctx, logger, options.android)
184
276
  }
277
+
278
+ // Transitional aliases for external/tests written against the rc.7-era names.
279
+ // New code must consume the capability-named exports above so future DSH
280
+ // releases do not accrete `if (rc8) / if (rc9)` branches.
281
+ export const isRc7ContractRuntime = hasBatchAttachmentContract
282
+ export const protectRc7ProviderOwnership = protectHostProviderOwnership
283
+ export const installRc7SettingsCompatibility = installHostSettingsCompatibility
@@ -516,31 +516,51 @@ export const LIVE_MODEL_CLIENT_PRELUDE = String.raw`(function(){
516
516
  }
517
517
 
518
518
  function patchLoader(loader) {
519
- if (!loader || typeof loader.load !== 'function' || loader.load.__visionRouterLiveModels) return;
520
- var original = loader.load;
521
- function load(spec) {
522
- if (spec && spec.id === 'dsh-vision-router' && typeof spec.factory === 'function') {
523
- var factory = spec.factory;
524
- spec = Object.assign({}, spec, {
525
- factory: function(require) {
526
- var exports = factory(require);
527
- if (exports && typeof exports.apply === 'function' && !exports.apply.__visionRouterLiveModels) {
528
- var apply = exports.apply;
529
- var wrappedApply = function(ctx) {
530
- var rest = Array.prototype.slice.call(arguments, 1);
531
- return apply.apply(exports, [wrapContext(ctx)].concat(rest));
532
- };
533
- Object.defineProperty(wrappedApply, '__visionRouterLiveModels', { value: true });
534
- exports.apply = wrappedApply;
519
+ if (!loader || (typeof loader !== 'object' && typeof loader !== 'function')) return;
520
+
521
+ if (typeof loader.load === 'function' && !loader.load.__visionRouterLiveModels) {
522
+ var original = loader.load;
523
+ function load(spec) {
524
+ if (spec && spec.id === 'dsh-vision-router' && typeof spec.factory === 'function') {
525
+ var factory = spec.factory;
526
+ spec = Object.assign({}, spec, {
527
+ factory: function(require) {
528
+ var exports = factory(require);
529
+ if (exports && typeof exports.apply === 'function' && !exports.apply.__visionRouterLiveModels) {
530
+ var apply = exports.apply;
531
+ var wrappedApply = function(ctx) {
532
+ var rest = Array.prototype.slice.call(arguments, 1);
533
+ return apply.apply(exports, [wrapContext(ctx)].concat(rest));
534
+ };
535
+ Object.defineProperty(wrappedApply, '__visionRouterLiveModels', { value: true });
536
+ exports.apply = wrappedApply;
537
+ }
538
+ return exports;
535
539
  }
536
- return exports;
537
- }
538
- });
540
+ });
541
+ }
542
+ return original.call(loader, spec);
543
+ }
544
+ Object.defineProperty(load, '__visionRouterLiveModels', { value: true });
545
+ loader.load = load;
546
+ }
547
+
548
+ // rc.8 boot has two loader phases on the same object. The parser first
549
+ // exposes queue-mode load(); later ClientModuleSystem.create() assigns a
550
+ // new live-mode load() before any lazy community bundle arrives. A wrapper
551
+ // installed only during parsing is therefore silently discarded. Wrap the
552
+ // transition itself and immediately re-apply this factory/context boundary
553
+ // to the new live sink.
554
+ if (typeof loader.create === 'function' && !loader.create.__visionRouterLiveModels) {
555
+ var originalCreate = loader.create;
556
+ function create() {
557
+ var result = originalCreate.apply(this, arguments);
558
+ patchLoader(loader);
559
+ return result;
539
560
  }
540
- return original.call(loader, spec);
561
+ Object.defineProperty(create, '__visionRouterLiveModels', { value: true });
562
+ loader.create = create;
541
563
  }
542
- Object.defineProperty(load, '__visionRouterLiveModels', { value: true });
543
- loader.load = load;
544
564
  }
545
565
 
546
566
  function install() {
@@ -574,8 +594,13 @@ export const LIVE_MODEL_CLIENT_PRELUDE = String.raw`(function(){
574
594
  export function injectLiveModelClientPrelude(html) {
575
595
  if (typeof html !== 'string' || html.includes(CLIENT_PRELUDE_MARK)) return html
576
596
  const script = `<script ${CLIENT_PRELUDE_MARK}>${LIVE_MODEL_CLIENT_PRELUDE.replace(/<\/script/gi, '<\\/script')}</script>`
577
- const head = html.indexOf('<head>')
578
- return head === -1 ? `${script}${html}` : `${html.slice(0, head + 6)}${script}${html.slice(head + 6)}`
597
+ // DSH's client-modules index tap prepends its queue-mode facade immediately
598
+ // after <head>. Run after that parser bootstrap so we can wrap both load()
599
+ // and create(); the create wrapper then re-attaches us after rc.8 swaps in
600
+ // its live registration sink, before lazy third-party bundles arrive.
601
+ const closeHead = html.indexOf('</head>')
602
+ if (closeHead !== -1) return `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
603
+ return `${html}${script}`
579
604
  }
580
605
 
581
606
  export function installLiveModelClientPrelude(ctx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vision-router",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain (no key) + pixel-level vision tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots). One-command install, no Python, image turns work like ordinary tool-calling turns.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -75,7 +75,7 @@
75
75
  "sharp": "^0.35.3"
76
76
  },
77
77
  "scripts": {
78
- "test": "node --test tests/core.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/provider-directory-fallback.test.js tests/settings-section-order.test.js tests/vision-resilience.test.js tests/client.test.js tests/remote-settings-bridge.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/update-check.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/replay-delegation.test.js tests/adversarial-hardening.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/local-ollama.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-compat.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/image-resource-governor.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js"
78
+ "test": "node --test tests/core.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/provider-directory-fallback.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/client.test.js tests/client-presentation-boundary.test.js tests/remote-settings-bridge.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/update-check.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/replay-delegation.test.js tests/adversarial-hardening.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/local-ollama.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-compat.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/image-resource-governor.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js"
79
79
  },
80
80
  "pnpm": {
81
81
  "onlyBuiltDependencies": [