dsh-vision-router 1.7.0 → 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/README.md +3 -3
- package/README.zh.md +4 -4
- package/cordis.patch.yml +23 -10
- package/docs/remote-settings.md +3 -1
- package/entry.js +46 -19
- package/lib/client-presentation-boundary.js +271 -0
- package/lib/dsh-contract-compat.js +125 -26
- package/lib/live-model-client-prelude.js +49 -24
- package/lib/remote-settings-bridge.js +51 -0
- package/lib/remote-settings-risk-confirmation.js +195 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.7.
|
|
19
|
+
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.7.1"><img src="https://img.shields.io/badge/release-v1.7.1-5B4CF0?style=flat-square" alt="Release v1.7.1" /></a>
|
|
20
20
|
<a href="tests"><img src="https://img.shields.io/badge/verified-657%20tests-2EA44F?style=flat-square" alt="Verified: 657 tests" /></a>
|
|
21
21
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat-square" alt="License: MIT" /></a>
|
|
22
22
|
<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>
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
<p align="center">💬 <strong>QQ community group: 1105463028</strong></p>
|
|
30
30
|
|
|
31
31
|
> [!WARNING]
|
|
32
|
-
> 📌 **Announcement (v1.7.
|
|
32
|
+
> 📌 **Announcement (v1.7.1)**
|
|
33
33
|
>
|
|
34
|
-
> **v1.7.
|
|
34
|
+
> **v1.7.1:** Remote settings can now be enabled after an explicit risk confirmation.
|
|
35
35
|
|
|
36
36
|
<p align="center">
|
|
37
37
|
<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
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.7.
|
|
19
|
+
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.7.1"><img src="https://img.shields.io/badge/release-v1.7.1-5B4CF0?style=flat-square" alt="Release v1.7.1" /></a>
|
|
20
20
|
<a href="tests"><img src="https://img.shields.io/badge/verified-657%20tests-2EA44F?style=flat-square" alt="Verified: 657 tests" /></a>
|
|
21
21
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat-square" alt="License: MIT" /></a>
|
|
22
22
|
<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>
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
<p align="center">💬 <strong>QQ 用户交流群:1105463028</strong></p>
|
|
30
30
|
|
|
31
31
|
> [!WARNING]
|
|
32
|
-
> 📌 **公告(v1.7.
|
|
32
|
+
> 📌 **公告(v1.7.1)**
|
|
33
33
|
>
|
|
34
|
-
> **v1.7.
|
|
34
|
+
> **v1.7.1:远程设置现可在风险确认后开启。**
|
|
35
35
|
|
|
36
36
|
<p align="center">
|
|
37
37
|
<img src="assets/vision-demo.gif" width="640" alt="演示:粘贴图片,Agent 用 vision_ground / vision_crop / vision_pixel_diff 定位发送按钮并给出坐标" />
|
|
@@ -178,7 +178,7 @@ opencode-go + 自动识图 ← 发图片时选这个
|
|
|
178
178
|
| Intern AI(上海AI实验室) | `internvl-latest` · `internvl3.5-latest` | 30 RPM,**9000 万 token/月** | ✅ | chat.intern-ai.org.cn |
|
|
179
179
|
| Groq | `meta-llama/llama-4-scout-17b-16e-instruct`(原生多模态,最多 5 张图) | 30 RPM / 14,400 次/天,免卡 | ❌ 需代理 | console.groq.com |
|
|
180
180
|
| Google AI Studio | `gemini-2.5-flash` · `gemini-2.5-flash-lite` | 10–30 RPM / 500–1,500 次/天 | ❌ 需代理 | aistudio.google.com |
|
|
181
|
-
| NVIDIA NIM | `meta/llama-3.2-11b-vision-instruct` · `nvidia/nemotron-nano-12b-v2-vl` | 40 RPM,免卡 | ⚠️ | build.nvidia.com |
|
|
181
|
+
| NVIDIA NIM | `meta-llama/llama-3.2-11b-vision-instruct` · `nvidia/nemotron-nano-12b-v2-vl` | 40 RPM,免卡 | ⚠️ | build.nvidia.com |
|
|
182
182
|
| OpenCode Zen | `mimo-v2.5-free`(视觉 + 代码) | 30 RPM / 500 次/天 | ⚠️ | opencode.ai/zen |
|
|
183
183
|
| OpenRouter | `google/gemma-4-26b-a4b-it:free` · `google/gemma-4-31b-it:free` | 未充值账户 50 次/天 | ❌ 需代理 | openrouter.ai |
|
|
184
184
|
|
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
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
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/docs/remote-settings.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Vision Router uses a dedicated DSH Connection RPC channel at `/vision-router-settings/*` because the global DSH settings/credentials plane is intentionally loopback-only.
|
|
4
4
|
|
|
5
|
-
Remote editing is off by default.
|
|
5
|
+
Remote editing is off by default. When a Vision Router settings page is opened through a DSH trusted host while the permission is still disabled, the page shows an explicit risk confirmation. Cancelling leaves remote editing disabled and writes nothing. Confirming enables only `allowRemoteSettings`, then reloads the scoped Vision Router settings immediately. The same permission can still be enabled or revoked from the loopback settings page.
|
|
6
|
+
|
|
7
|
+
DSH `trustedHosts` protects the browser carrier against Host/Origin/DNS-rebinding attacks; it is **not user authentication**. Enable remote editing only when you trust the clients that can reach the DSH instance. Ordinary remote mutations remain restricted to the explicit Vision Router allow-list. Credential-bearing HTTP providers, local Ollama/LM Studio configuration, artifact paths and every other field not present in that allow-list stay unavailable remotely. `allowRemoteSettings` itself is not added to the ordinary mutation allow-list: it can be enabled only through the explicit risk-confirmation flow (or from the loopback settings page).
|
|
6
8
|
|
|
7
9
|
If DSH is behind Nginx, Caddy or another reverse proxy, forward `/vision-router-settings/*` to the same DSH origin in addition to the normal DSH routes. The UI detects a 404 on this channel and shows this requirement directly.
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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 =
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
//
|
|
166
|
-
//
|
|
167
|
-
// store-owned, so never
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
244
|
-
// provider directory, not by the live adapter registry alone.
|
|
245
|
-
// main DeepSeek + 自动识图 route as a derived alias of official
|
|
246
|
-
//
|
|
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
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
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) =>
|
|
135
|
+
if (list.some((route) => HOST_OWNED_ROUTES.has(String(route)))) {
|
|
45
136
|
const error = new Error(
|
|
46
|
-
'vision-router: DSH
|
|
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 = '
|
|
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
|
|
69
|
-
*
|
|
70
|
-
* forcing a
|
|
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:
|
|
176
|
+
'vision-router: host settings compatibility source',
|
|
86
177
|
)
|
|
87
178
|
})
|
|
88
179
|
}
|
|
89
180
|
|
|
90
181
|
/**
|
|
91
|
-
* Bridge
|
|
92
|
-
* `ctx.inject(['settings']) -> settings.register()` callback. The
|
|
93
|
-
*
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
176
|
-
* service keeps AttachmentId store-owned, so
|
|
177
|
-
* untouched
|
|
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 (
|
|
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
|
|
520
|
-
|
|
521
|
-
function load
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
spec
|
|
525
|
-
factory
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
var
|
|
529
|
-
|
|
530
|
-
var
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
578
|
-
|
|
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) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { installLocalRemoteSettingsPermissionBridge } from './local-remote-settings-permission.js'
|
|
2
|
+
import { installRemoteSettingsRiskConfirmationBridge } from './remote-settings-risk-confirmation.js'
|
|
2
3
|
|
|
3
4
|
const SETTINGS_NS = 'vision-router'
|
|
4
5
|
|
|
5
6
|
export const REMOTE_SETTINGS_CHANNEL = '/vision-router-settings'
|
|
6
7
|
export const REMOTE_SETTINGS_PERMISSION = 'allowRemoteSettings'
|
|
8
|
+
export const REMOTE_SETTINGS_AUTHORIZE_ENDPOINT = 'authorize'
|
|
7
9
|
|
|
8
10
|
// Remote DSH pages are deliberately less privileged than the loopback settings UI.
|
|
9
11
|
// This is a capability allow-list, not a blacklist: a future Config field is local-only
|
|
@@ -117,6 +119,51 @@ function conflictResult(error) {
|
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
export async function authorizeRemoteSettingsAfterRiskConfirmation(settings, payload, logger) {
|
|
123
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || payload.acceptedRisk !== true) {
|
|
124
|
+
return badRequest('acceptedRisk must be true')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let descriptor
|
|
128
|
+
try {
|
|
129
|
+
descriptor = namespaceDescriptor(settings)
|
|
130
|
+
} catch (error) {
|
|
131
|
+
logger?.warn?.('vision-router: remote settings authorization describe failed: %s', error?.message ?? String(error))
|
|
132
|
+
return settingsRejected('Vision Router settings could not be read')
|
|
133
|
+
}
|
|
134
|
+
if (!descriptor) return settingsRejected('Vision Router settings namespace is unavailable')
|
|
135
|
+
if (settings.writable !== true) return settingsRejected('Vision Router settings provider is read-only')
|
|
136
|
+
|
|
137
|
+
const value = descriptor.value
|
|
138
|
+
if (value && typeof value === 'object' && value[REMOTE_SETTINGS_PERMISSION] === true) {
|
|
139
|
+
return { ok: true, value: accessSnapshot(settings) }
|
|
140
|
+
}
|
|
141
|
+
if (!Number.isInteger(descriptor.revision) || descriptor.revision < 0) {
|
|
142
|
+
return settingsRejected('Vision Router settings revision is unavailable')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
await settings.mutate(
|
|
147
|
+
SETTINGS_NS,
|
|
148
|
+
[{ op: 'set', path: [REMOTE_SETTINGS_PERMISSION], value: true }],
|
|
149
|
+
descriptor.revision,
|
|
150
|
+
)
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const conflict = conflictResult(error)
|
|
153
|
+
if (conflict) return conflict
|
|
154
|
+
logger?.warn?.('vision-router: remote settings authorization rejected: %s', error?.message ?? String(error))
|
|
155
|
+
return settingsRejected(error?.message ?? 'Vision Router remote settings authorization was rejected')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const access = accessSnapshot(settings)
|
|
160
|
+
if (access.enabled !== true) return settingsRejected('Vision Router remote settings authorization did not persist')
|
|
161
|
+
return { ok: true, value: access }
|
|
162
|
+
} catch {
|
|
163
|
+
return settingsRejected('Vision Router remote settings were enabled but could not be read back')
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
120
167
|
export function createVisionRouterRemoteSettingsHandler(settings, logger) {
|
|
121
168
|
return async (endpoint, payload) => {
|
|
122
169
|
if (endpoint === 'describe') {
|
|
@@ -126,6 +173,9 @@ export function createVisionRouterRemoteSettingsHandler(settings, logger) {
|
|
|
126
173
|
return settingsRejected('Vision Router settings could not be read')
|
|
127
174
|
}
|
|
128
175
|
}
|
|
176
|
+
if (endpoint === REMOTE_SETTINGS_AUTHORIZE_ENDPOINT) {
|
|
177
|
+
return authorizeRemoteSettingsAfterRiskConfirmation(settings, payload, logger)
|
|
178
|
+
}
|
|
129
179
|
if (endpoint !== 'mutate') return badRequest('unknown Vision Router remote settings endpoint ' + JSON.stringify(endpoint))
|
|
130
180
|
|
|
131
181
|
let descriptor
|
|
@@ -163,5 +213,6 @@ export function installVisionRouterRemoteSettingsBridge(ctx, logger) {
|
|
|
163
213
|
'vision-router: opt-in remote settings channel',
|
|
164
214
|
)
|
|
165
215
|
})
|
|
216
|
+
installRemoteSettingsRiskConfirmationBridge(ctx)
|
|
166
217
|
installLocalRemoteSettingsPermissionBridge(ctx, logger)
|
|
167
218
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const RISK_PRELUDE_MARK = 'data-vision-router-remote-settings-risk-confirmation'
|
|
2
|
+
|
|
3
|
+
export const REMOTE_SETTINGS_RISK_CLIENT_PRELUDE = String.raw`(function(){
|
|
4
|
+
'use strict';
|
|
5
|
+
var CHANNEL = '/vision-router-settings';
|
|
6
|
+
var AUTHORIZE_ENDPOINT = 'authorize';
|
|
7
|
+
var connectionCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
|
|
8
|
+
var rpcCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
|
|
9
|
+
var contextCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
|
|
10
|
+
var authorizationPromise;
|
|
11
|
+
|
|
12
|
+
function chineseLocale() {
|
|
13
|
+
var lang = '';
|
|
14
|
+
try {
|
|
15
|
+
lang = String((document && document.documentElement && document.documentElement.lang) || (navigator && navigator.language) || '').toLowerCase();
|
|
16
|
+
} catch (_) {}
|
|
17
|
+
return lang === '' || lang.indexOf('zh') === 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function riskMessage() {
|
|
21
|
+
return chineseLocale()
|
|
22
|
+
? '启用远程设置?\n\n当前页面通过远程地址访问 DSH。启用后,能够访问此 trusted host 的客户端可以修改 Vision Router 已开放的设置。\n\nDSH trustedHosts 不是身份认证,请仅在你信任当前网络和访问者时继续。API Key、HTTP Provider 凭据、本地 Ollama / LM Studio、产物路径等未列入远程白名单的敏感配置仍不会开放。\n\n确定要允许远程修改设置吗?'
|
|
23
|
+
: 'Enable remote settings?\n\nThis page is accessing DSH through a remote address. After enabling this, clients that can reach this trusted host can change the Vision Router settings exposed by the remote allow-list.\n\nDSH trustedHosts is not authentication. Continue only if you trust the current network and its users. Sensitive settings outside the remote allow-list, including API keys, HTTP-provider credentials, local Ollama / LM Studio, and artifact paths, remain unavailable remotely.\n\nAllow remote settings?';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isPermissionDisabledDescribe(channel, endpoint, result) {
|
|
27
|
+
return channel === CHANNEL
|
|
28
|
+
&& endpoint === 'describe'
|
|
29
|
+
&& result && result.ok === true
|
|
30
|
+
&& result.value && result.value.enabled !== true
|
|
31
|
+
&& result.value.reason === 'permission-disabled';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function authorizeAndRefresh(target, call) {
|
|
35
|
+
if (authorizationPromise) return authorizationPromise;
|
|
36
|
+
authorizationPromise = (async function(){
|
|
37
|
+
if (typeof window.confirm !== 'function' || window.confirm(riskMessage()) !== true) return undefined;
|
|
38
|
+
var authorized = await call.call(target, CHANNEL, AUTHORIZE_ENDPOINT, { acceptedRisk: true });
|
|
39
|
+
if (!authorized || authorized.ok !== true) {
|
|
40
|
+
var message = authorized && authorized.error && authorized.error.message
|
|
41
|
+
? authorized.error.message
|
|
42
|
+
: 'Vision Router could not enable remote settings.';
|
|
43
|
+
try { if (typeof window.alert === 'function') window.alert(message); } catch (_) {}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return call.call(target, CHANNEL, 'describe', {});
|
|
47
|
+
})();
|
|
48
|
+
try { return await authorizationPromise; }
|
|
49
|
+
finally { authorizationPromise = undefined; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function wrapRpc(rpc) {
|
|
53
|
+
if (!rpc || (typeof rpc !== 'object' && typeof rpc !== 'function')) return rpc;
|
|
54
|
+
if (rpcCache && rpcCache.has(rpc)) return rpcCache.get(rpc);
|
|
55
|
+
var wrapped = new Proxy(rpc, {
|
|
56
|
+
get: function(target, property) {
|
|
57
|
+
if (property === 'call') {
|
|
58
|
+
var call = Reflect.get(target, property, target);
|
|
59
|
+
if (typeof call !== 'function') return call;
|
|
60
|
+
return async function(channel, endpoint) {
|
|
61
|
+
var result = await call.apply(target, arguments);
|
|
62
|
+
if (!isPermissionDisabledDescribe(channel, endpoint, result)) return result;
|
|
63
|
+
var refreshed = await authorizeAndRefresh(target, call);
|
|
64
|
+
return refreshed || result;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
var value = Reflect.get(target, property, target);
|
|
68
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (rpcCache) rpcCache.set(rpc, wrapped);
|
|
72
|
+
return wrapped;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function wrapConnection(connection) {
|
|
76
|
+
if (!connection || (typeof connection !== 'object' && typeof connection !== 'function')) return connection;
|
|
77
|
+
if (connectionCache && connectionCache.has(connection)) return connectionCache.get(connection);
|
|
78
|
+
var wrapped = new Proxy(connection, {
|
|
79
|
+
get: function(target, property) {
|
|
80
|
+
if (property === 'rpc') return wrapRpc(Reflect.get(target, property, target));
|
|
81
|
+
var value = Reflect.get(target, property, target);
|
|
82
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
if (connectionCache) connectionCache.set(connection, wrapped);
|
|
86
|
+
return wrapped;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function wrapContext(ctx) {
|
|
90
|
+
if (!ctx || typeof ctx !== 'object') return ctx;
|
|
91
|
+
if (contextCache && contextCache.has(ctx)) return contextCache.get(ctx);
|
|
92
|
+
var wrapped = new Proxy(ctx, {
|
|
93
|
+
get: function(target, property) {
|
|
94
|
+
if (property === 'get') {
|
|
95
|
+
var get = Reflect.get(target, property, target);
|
|
96
|
+
if (typeof get !== 'function') return get;
|
|
97
|
+
return function(name) {
|
|
98
|
+
var value = get.apply(target, arguments);
|
|
99
|
+
return name === 'connection' ? wrapConnection(value) : value;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
var value = Reflect.get(target, property, target);
|
|
103
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
if (contextCache) contextCache.set(ctx, wrapped);
|
|
107
|
+
return wrapped;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function patchLoader(loader) {
|
|
111
|
+
if (!loader || typeof loader.load !== 'function' || loader.load.__visionRouterRemoteRiskConfirmation) return;
|
|
112
|
+
var original = loader.load;
|
|
113
|
+
function load(spec) {
|
|
114
|
+
if (spec && spec.id === 'dsh-vision-router' && typeof spec.factory === 'function') {
|
|
115
|
+
var factory = spec.factory;
|
|
116
|
+
spec = Object.assign({}, spec, {
|
|
117
|
+
factory: function(require) {
|
|
118
|
+
var exports = factory(require);
|
|
119
|
+
if (exports && typeof exports.apply === 'function' && !exports.apply.__visionRouterRemoteRiskConfirmation) {
|
|
120
|
+
var apply = exports.apply;
|
|
121
|
+
var wrappedApply = function(ctx) {
|
|
122
|
+
var rest = Array.prototype.slice.call(arguments, 1);
|
|
123
|
+
return apply.apply(exports, [wrapContext(ctx)].concat(rest));
|
|
124
|
+
};
|
|
125
|
+
Object.defineProperty(wrappedApply, '__visionRouterRemoteRiskConfirmation', { value: true });
|
|
126
|
+
exports.apply = wrappedApply;
|
|
127
|
+
}
|
|
128
|
+
return exports;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return original.call(loader, spec);
|
|
133
|
+
}
|
|
134
|
+
Object.defineProperty(load, '__visionRouterRemoteRiskConfirmation', { value: true });
|
|
135
|
+
loader.load = load;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function install() {
|
|
139
|
+
if (window.__ModuleLoader__) {
|
|
140
|
+
patchLoader(window.__ModuleLoader__);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
var descriptor = Object.getOwnPropertyDescriptor(window, '__ModuleLoader__');
|
|
144
|
+
if (descriptor && descriptor.configurable === false) return;
|
|
145
|
+
if (descriptor && typeof descriptor.set === 'function') {
|
|
146
|
+
var previousGet = descriptor.get;
|
|
147
|
+
var previousSet = descriptor.set;
|
|
148
|
+
Object.defineProperty(window, '__ModuleLoader__', {
|
|
149
|
+
configurable: true,
|
|
150
|
+
enumerable: descriptor.enumerable !== false,
|
|
151
|
+
get: function(){ return previousGet ? previousGet.call(window) : undefined; },
|
|
152
|
+
set: function(value) {
|
|
153
|
+
previousSet.call(window, value);
|
|
154
|
+
try { patchLoader(window.__ModuleLoader__ || value); } catch (_) {}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
var stored;
|
|
160
|
+
Object.defineProperty(window, '__ModuleLoader__', {
|
|
161
|
+
configurable: true,
|
|
162
|
+
enumerable: true,
|
|
163
|
+
get: function(){ return stored; },
|
|
164
|
+
set: function(value) {
|
|
165
|
+
stored = value;
|
|
166
|
+
patchLoader(value);
|
|
167
|
+
Object.defineProperty(window, '__ModuleLoader__', {
|
|
168
|
+
configurable: true,
|
|
169
|
+
enumerable: true,
|
|
170
|
+
writable: true,
|
|
171
|
+
value: stored
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try { install(); } catch (_) {}
|
|
178
|
+
})();`
|
|
179
|
+
|
|
180
|
+
export function injectRemoteSettingsRiskConfirmationPrelude(html) {
|
|
181
|
+
if (typeof html !== 'string' || html.includes(RISK_PRELUDE_MARK)) return html
|
|
182
|
+
const script = `<script ${RISK_PRELUDE_MARK}>${REMOTE_SETTINGS_RISK_CLIENT_PRELUDE.replace(/<\/script/gi, '<\\/script')}</script>`
|
|
183
|
+
const endHead = html.indexOf('</head>')
|
|
184
|
+
return endHead === -1 ? `${html}${script}` : `${html.slice(0, endHead)}${script}${html.slice(endHead)}`
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function installRemoteSettingsRiskConfirmationBridge(ctx) {
|
|
188
|
+
if (!ctx || typeof ctx.inject !== 'function') return
|
|
189
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
190
|
+
webCtx.effect(
|
|
191
|
+
() => webCtx.webServer.tapIndex(injectRemoteSettingsRiskConfirmationPrelude),
|
|
192
|
+
'vision-router: remote settings risk confirmation client shim',
|
|
193
|
+
)
|
|
194
|
+
})
|
|
195
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vision-router",
|
|
3
|
-
"version": "1.7.
|
|
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/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": [
|