dsh-vision-router 2.2.8 → 3.0.0
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 +16 -7
- package/README.zh.md +16 -7
- package/docs/architecture/2.3.0-compat-retirement.md +147 -0
- package/docs/architecture/2.3.0-execution-plan.md +410 -0
- package/docs/architecture/2.3.0-final-audit.md +279 -0
- package/docs/architecture/2.3.0-reopened-plan.md +720 -0
- package/docs/architecture/3.0-architecture-plan.md +252 -0
- package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
- package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
- package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
- package/docs/architecture/3.0-r11-adversarial-review.md +441 -0
- package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
- package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
- package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
- package/docs/architecture/3.0-r7-public-api.md +151 -0
- package/docs/architecture/3.0-r8-ci-release.md +248 -0
- package/docs/architecture/3.0-r9-final-audit.md +370 -0
- package/docs/architecture/compat-inventory.md +26 -34
- package/docs/architecture/dsh-compatibility-matrix.md +65 -54
- package/docs/architecture/dsh-support-window.md +30 -22
- package/docs/architecture/final-closure-audit.md +2 -0
- package/docs/releases/v2.3.0.md +53 -0
- package/docs/releases/v3.0.0.md +85 -0
- package/entry.js +9 -57
- package/index.js +268 -761
- package/lib/agent-request-route-authority.js +1 -20
- package/lib/catalog-corrections.js +1 -2
- package/lib/client-maintenance-actions.js +145 -0
- package/lib/client-presentation-boundary-main.js +75 -17
- package/lib/client-presentation-boundary.js +53 -28
- package/lib/client.js +246 -144
- package/lib/core-apply-lifecycle.js +41 -0
- package/lib/core-cache.js +87 -0
- package/lib/core-primitives.js +91 -278
- package/lib/core-vision-surface-domain.js +63 -0
- package/lib/core-vision-surface.js +11 -77
- package/lib/delegated-call-config.js +15 -24
- package/lib/desktop-screenshot-tool.js +262 -0
- package/lib/doctor-cli.js +0 -0
- package/lib/doctor-runtime.js +4 -1
- package/lib/dsh-contract-compat.js +37 -256
- package/lib/dsh-host-capabilities.js +3 -9
- package/lib/dsh-settings-017-compat.js +17 -10
- package/lib/dsh-support-window.js +3 -3
- package/lib/file-logger.js +8 -3
- package/lib/grounding-coordinate-runtime.js +1 -2
- package/lib/http-body-limit.js +97 -79
- package/lib/http-compat.js +1 -2
- package/lib/image-offload-compat.js +30 -21
- package/lib/image-resource-governor.js +210 -228
- package/lib/legacy-global-proxy-boundary.js +46 -3
- package/lib/live-model-client-prelude.js +12 -4
- package/lib/live-model-discovery.js +134 -43
- package/lib/local-remote-settings-permission.js +33 -7
- package/lib/local-vision-stabilizer.js +31 -17
- package/lib/maintenance-routes.js +134 -0
- package/lib/native-image-coexistence.js +15 -3
- package/lib/ollama-cold-start.js +87 -23
- package/lib/pi-ai-bridge-wire-compat.js +3 -2
- package/lib/proxy-dispatcher-pool.js +151 -92
- package/lib/proxy-routing.js +75 -51
- package/lib/proxy-url-compat.js +3 -10
- package/lib/public-config.js +36 -0
- package/lib/public-entry.d.ts +148 -0
- package/lib/public-entry.js +19 -11
- package/lib/remote-settings-bridge.js +11 -0
- package/lib/remote-settings-risk-client-boundary.js +155 -0
- package/lib/remote-settings-risk-confirmation.js +17 -105
- package/lib/replay-delegation.js +12 -7
- package/lib/replay-envelope-v2-compat.js +12 -7
- package/lib/runtime-composition.js +127 -34
- package/lib/runtime-i18n-boundary.js +10 -11
- package/lib/screenshot-source-boundary.js +2 -2
- package/lib/session-affinity-runtime.js +113 -97
- package/lib/session-affinity.js +89 -76
- package/lib/session-event-history-compat.js +30 -0
- package/lib/session-message-source-compat.js +7 -3
- package/lib/session-query-readers.js +263 -0
- package/lib/session-surface-compat.js +30 -31
- package/lib/session-surface-policy-domain.js +85 -0
- package/lib/session-surface-policy.js +2 -111
- package/lib/session-turn-resolver.js +2 -6
- package/lib/session-vision-attachment-index.js +143 -0
- package/lib/session-vision-delegation-boundary.js +34 -0
- package/lib/session-vision-delegation.js +22 -0
- package/lib/session-vision-index.js +179 -116
- package/lib/session-vision-mode-authority.js +229 -192
- package/lib/session-vision-mode-boundary.js +65 -22
- package/lib/session-vision-policy-domain.js +304 -0
- package/lib/session-vision-policy.js +104 -0
- package/lib/session-vision-state.js +368 -378
- package/lib/settings-client-017-compat.js +9 -7
- package/lib/settings-client-loader-lifecycle.js +129 -0
- package/lib/sharp-runtime.js +0 -7
- package/lib/structured-flow-hardening.js +25 -6
- package/lib/tesseract-exec-compat.js +9 -1
- package/lib/turn-budget-context.js +47 -41
- package/lib/update-check.js +11 -2
- package/lib/vision-attachment-handle-runtime.js +176 -156
- package/lib/vision-backend-runtime-policy.js +19 -2
- package/lib/vision-background-benchmark.js +41 -4
- package/lib/vision-background-failure-policy.js +41 -41
- package/lib/vision-background-stop-store-core.js +217 -0
- package/lib/vision-background-stop-store.js +8 -179
- package/lib/vision-capability-benchmark-service.js +1 -0
- package/lib/vision-capability-benchmark.js +11 -61
- package/lib/vision-capability-evidence.js +35 -0
- package/lib/vision-capability-fingerprint.js +72 -0
- package/lib/vision-capability-identity.js +116 -61
- package/lib/vision-capability-router.js +13 -33
- package/lib/vision-chunk-assembler.js +95 -0
- package/lib/vision-diagnostics-routes.js +246 -0
- package/lib/vision-evidence-guidance.js +3 -1
- package/lib/vision-execution-order-apply.js +45 -55
- package/lib/vision-execution-order.js +19 -19
- package/lib/vision-execution-policy.js +12 -7
- package/lib/vision-model-registry.js +30 -17
- package/lib/vision-provider-transport.js +137 -176
- package/lib/vision-routing-authority.js +47 -48
- package/lib/vision-routing-product.js +26 -29
- package/lib/vision-routing-settings-prelude.js +8 -3
- package/lib/vision-runtime-performance-store.js +166 -0
- package/lib/vision-runtime-performance.js +13 -142
- package/lib/vision-toggle-root-hardening.js +22 -5
- package/lib/vision-tool-runtime-boundary.js +104 -31
- package/lib/web/dsh-settings-017-root-transport.js +27 -12
- package/lib/web/remote-settings-client.js +6 -9
- package/lib/web-capability-boundary.js +17 -7
- package/lib/wrapper-directory.js +14 -23
- package/package.json +56 -20
- package/lib/android-attachment-compat.js +0 -244
- package/lib/settings-client-rc8-lifecycle.js +0 -397
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
|
|
3
|
-
const wrappedContexts = new WeakMap()
|
|
4
|
-
const MIB = 1024 * 1024
|
|
5
|
-
|
|
6
|
-
export const DEFAULT_ANDROID_TRANSIENT_LIMITS = Object.freeze({
|
|
7
|
-
maxEntries: 64,
|
|
8
|
-
maxBytes: 128 * MIB,
|
|
9
|
-
maxSingleBytes: 64 * MIB,
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
function nonEmptyString(value) {
|
|
13
|
-
return typeof value === 'string' && value.length > 0
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function positiveInteger(value, fallback) {
|
|
17
|
-
const n = Number(value)
|
|
18
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function isAndroidTermuxRuntime(options = {}) {
|
|
22
|
-
const platform = options.platform ?? process.platform
|
|
23
|
-
const env = options.env ?? process.env
|
|
24
|
-
if (platform === 'android') return true
|
|
25
|
-
if (nonEmptyString(env?.TERMUX_VERSION)) return true
|
|
26
|
-
const prefix = nonEmptyString(env?.PREFIX) ? env.PREFIX : ''
|
|
27
|
-
return prefix.includes('/com.termux/') || prefix.includes('\\com.termux\\')
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function isPermissionBoundaryError(error) {
|
|
31
|
-
let current = error
|
|
32
|
-
const seen = new Set()
|
|
33
|
-
for (let depth = 0; current && depth < 8; depth += 1) {
|
|
34
|
-
if ((typeof current === 'object' || typeof current === 'function') && seen.has(current)) break
|
|
35
|
-
if (typeof current === 'object' || typeof current === 'function') seen.add(current)
|
|
36
|
-
const code = current && typeof current === 'object' ? current.code : undefined
|
|
37
|
-
if (code === 'EACCES' || code === 'EPERM') return true
|
|
38
|
-
current = current && typeof current === 'object' ? current.cause : undefined
|
|
39
|
-
}
|
|
40
|
-
return false
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function displayName(value) {
|
|
44
|
-
if (!nonEmptyString(value)) return undefined
|
|
45
|
-
const leaf = value.slice(Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\')) + 1)
|
|
46
|
-
const clean = leaf.replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, 255)
|
|
47
|
-
return clean === '' ? undefined : clean
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function probeImage(data, injectedProbe) {
|
|
51
|
-
if (typeof injectedProbe === 'function') return injectedProbe(data)
|
|
52
|
-
const mod = await import('sharp')
|
|
53
|
-
const sharp = mod.default ?? mod
|
|
54
|
-
const metadata = await sharp(data, { failOn: 'none' }).metadata()
|
|
55
|
-
const width = Number(metadata.width ?? 0)
|
|
56
|
-
const height = Number(metadata.height ?? 0)
|
|
57
|
-
if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
|
|
58
|
-
throw new Error('vision-router: Android attachment fallback could not read image dimensions')
|
|
59
|
-
}
|
|
60
|
-
return { width, height }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function transientTooLargeError(bytes, limit) {
|
|
64
|
-
const error = new Error(
|
|
65
|
-
`vision-router: Android attachment fallback refuses to keep a ${bytes}-byte image in process memory (limit ${limit} bytes)`,
|
|
66
|
-
)
|
|
67
|
-
error.code = 'ANDROID_TRANSIENT_ATTACHMENT_TOO_LARGE'
|
|
68
|
-
return error
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function transientStoredImage(input, injectedProbe, maxSingleBytes) {
|
|
72
|
-
const data = input?.data
|
|
73
|
-
if (!(data instanceof Uint8Array) || data.byteLength === 0) {
|
|
74
|
-
throw new Error('vision-router: Android attachment fallback received empty image bytes')
|
|
75
|
-
}
|
|
76
|
-
// Enforce the byte boundary BEFORE probing with sharp. Metadata decode can
|
|
77
|
-
// itself allocate native memory, so rejecting after probeImage() would still
|
|
78
|
-
// allow a single pathological input to cross the mobile memory boundary.
|
|
79
|
-
if (data.byteLength > maxSingleBytes) {
|
|
80
|
-
throw transientTooLargeError(data.byteLength, maxSingleBytes)
|
|
81
|
-
}
|
|
82
|
-
const mediaType = input?.mediaType
|
|
83
|
-
if (!nonEmptyString(mediaType)) {
|
|
84
|
-
throw new Error('vision-router: Android attachment fallback received no media type')
|
|
85
|
-
}
|
|
86
|
-
const { width, height } = await probeImage(data, injectedProbe)
|
|
87
|
-
const sha256 = createHash('sha256').update(data).digest('hex')
|
|
88
|
-
const name = displayName(input?.name)
|
|
89
|
-
const ref = {
|
|
90
|
-
attachmentId: `sha256:${sha256}`,
|
|
91
|
-
mediaType,
|
|
92
|
-
bytes: data.byteLength,
|
|
93
|
-
width,
|
|
94
|
-
height,
|
|
95
|
-
...(name !== undefined ? { name } : {}),
|
|
96
|
-
}
|
|
97
|
-
return { ref, data }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Byte-weighted LRU for the Android process-local durability fallback. */
|
|
101
|
-
export function createTransientAttachmentCache(options = {}) {
|
|
102
|
-
const maxEntries = positiveInteger(options.maxEntries, DEFAULT_ANDROID_TRANSIENT_LIMITS.maxEntries)
|
|
103
|
-
const maxBytes = positiveInteger(options.maxBytes, DEFAULT_ANDROID_TRANSIENT_LIMITS.maxBytes)
|
|
104
|
-
const map = new Map()
|
|
105
|
-
let bytes = 0
|
|
106
|
-
|
|
107
|
-
const remove = (id) => {
|
|
108
|
-
const old = map.get(id)
|
|
109
|
-
if (old === undefined) return false
|
|
110
|
-
bytes = Math.max(0, bytes - Number(old.data?.byteLength ?? 0))
|
|
111
|
-
return map.delete(id)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const trim = () => {
|
|
115
|
-
while (map.size > maxEntries || bytes > maxBytes) {
|
|
116
|
-
const oldest = map.keys().next().value
|
|
117
|
-
if (oldest === undefined) break
|
|
118
|
-
remove(oldest)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
get(id) {
|
|
124
|
-
const stored = map.get(id)
|
|
125
|
-
if (stored === undefined) return undefined
|
|
126
|
-
// Refresh recency so an actively used image survives pressure from newer
|
|
127
|
-
// one-shot attachments.
|
|
128
|
-
map.delete(id)
|
|
129
|
-
map.set(id, stored)
|
|
130
|
-
return stored
|
|
131
|
-
},
|
|
132
|
-
set(stored) {
|
|
133
|
-
const id = String(stored?.ref?.attachmentId ?? '')
|
|
134
|
-
const weight = Number(stored?.data?.byteLength ?? 0)
|
|
135
|
-
if (id === '' || !Number.isFinite(weight) || weight <= 0 || weight > maxBytes) return false
|
|
136
|
-
remove(id)
|
|
137
|
-
map.set(id, stored)
|
|
138
|
-
bytes += weight
|
|
139
|
-
trim()
|
|
140
|
-
return map.has(id)
|
|
141
|
-
},
|
|
142
|
-
stats() {
|
|
143
|
-
return { entries: map.size, bytes, maxEntries, maxBytes }
|
|
144
|
-
},
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Android/Termux compatibility shim for deepseek-harness attachment-local.
|
|
150
|
-
*
|
|
151
|
-
* Current DSH attachment-local proves directory durability by fsyncing every
|
|
152
|
-
* ancestor from DSH_HOME to the filesystem root. Android SELinux lets Termux
|
|
153
|
-
* access its own /data/data/com.termux subtree but intentionally denies opening
|
|
154
|
-
* /data/data itself, so saveImage() is wrapped as ATTACHMENT_WRITE_FAILED even
|
|
155
|
-
* though the image bytes are otherwise readable.
|
|
156
|
-
*
|
|
157
|
-
* Keep the host service authoritative. Only when an Android runtime reaches an
|
|
158
|
-
* EACCES/EPERM boundary do Vision Router's own calls fall back to a byte- and
|
|
159
|
-
* count-bounded, process-local content-addressed attachment. The paired
|
|
160
|
-
* readImage() wrapper makes the direct vision-http path and plugin-side bridge
|
|
161
|
-
* code consume those bytes normally. Once upstream accepts the save, this path
|
|
162
|
-
* is never used.
|
|
163
|
-
*/
|
|
164
|
-
export function installAndroidAttachmentCompat(ctx, logger, options = {}) {
|
|
165
|
-
if (!ctx || typeof ctx !== 'object' || !isAndroidTermuxRuntime(options.runtime)) return ctx
|
|
166
|
-
const cached = wrappedContexts.get(ctx)
|
|
167
|
-
if (cached) return cached
|
|
168
|
-
|
|
169
|
-
let attachments
|
|
170
|
-
try {
|
|
171
|
-
attachments = typeof ctx.get === 'function' ? ctx.get('attachments') : undefined
|
|
172
|
-
} catch {
|
|
173
|
-
return ctx
|
|
174
|
-
}
|
|
175
|
-
if (!attachments || typeof attachments.saveImage !== 'function' || typeof attachments.readImage !== 'function') {
|
|
176
|
-
return ctx
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const maxTransientBytes = positiveInteger(
|
|
180
|
-
options.maxTransientBytes,
|
|
181
|
-
DEFAULT_ANDROID_TRANSIENT_LIMITS.maxBytes,
|
|
182
|
-
)
|
|
183
|
-
const maxTransientAttachments = positiveInteger(
|
|
184
|
-
options.maxTransientAttachments,
|
|
185
|
-
DEFAULT_ANDROID_TRANSIENT_LIMITS.maxEntries,
|
|
186
|
-
)
|
|
187
|
-
const maxTransientSingleBytes = Math.min(
|
|
188
|
-
maxTransientBytes,
|
|
189
|
-
positiveInteger(options.maxTransientSingleBytes, DEFAULT_ANDROID_TRANSIENT_LIMITS.maxSingleBytes),
|
|
190
|
-
)
|
|
191
|
-
const transient = createTransientAttachmentCache({
|
|
192
|
-
maxEntries: maxTransientAttachments,
|
|
193
|
-
maxBytes: maxTransientBytes,
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
const wrappedAttachments = new Proxy(attachments, {
|
|
197
|
-
get(target, property) {
|
|
198
|
-
if (property === 'saveImage') {
|
|
199
|
-
return async (input) => {
|
|
200
|
-
try {
|
|
201
|
-
return await target.saveImage(input)
|
|
202
|
-
} catch (error) {
|
|
203
|
-
if (!isPermissionBoundaryError(error)) throw error
|
|
204
|
-
const stored = await transientStoredImage(input, options.probeImage, maxTransientSingleBytes)
|
|
205
|
-
if (!transient.set(stored)) {
|
|
206
|
-
throw transientTooLargeError(stored.data.byteLength, maxTransientBytes)
|
|
207
|
-
}
|
|
208
|
-
try {
|
|
209
|
-
logger?.warn?.(
|
|
210
|
-
'vision-router: Android/Termux attachment save hit an inaccessible ancestor; using a bounded process-local image ref until DSH attachment-local is fixed upstream',
|
|
211
|
-
)
|
|
212
|
-
} catch {
|
|
213
|
-
/* diagnostics must never block the fallback */
|
|
214
|
-
}
|
|
215
|
-
return stored.ref
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (property === 'readImage') {
|
|
220
|
-
return async (ref, signal) => {
|
|
221
|
-
signal?.throwIfAborted?.()
|
|
222
|
-
const id = String(ref?.attachmentId ?? '')
|
|
223
|
-
const stored = transient.get(id)
|
|
224
|
-
if (stored !== undefined) return stored
|
|
225
|
-
return target.readImage(ref, signal)
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
const value = Reflect.get(target, property, target)
|
|
229
|
-
return typeof value === 'function' ? value.bind(target) : value
|
|
230
|
-
},
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
const wrapped = new Proxy(ctx, {
|
|
234
|
-
get(target, property) {
|
|
235
|
-
if (property === 'get') {
|
|
236
|
-
return (name) => (name === 'attachments' ? wrappedAttachments : target.get(name))
|
|
237
|
-
}
|
|
238
|
-
const value = Reflect.get(target, property, target)
|
|
239
|
-
return typeof value === 'function' ? value.bind(target) : value
|
|
240
|
-
},
|
|
241
|
-
})
|
|
242
|
-
wrappedContexts.set(ctx, wrapped)
|
|
243
|
-
return wrapped
|
|
244
|
-
}
|
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
|
|
2
|
-
|
|
3
|
-
const SETTINGS_RC8_MARK = 'data-vision-router-settings-rc8-lifecycle'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* rc.8 replaces ModuleLoader.load() inside loader.create(). The original local
|
|
7
|
-
* permission and remote-risk shims predate that lifecycle and are intentionally
|
|
8
|
-
* left untouched for rc.6/rc.7. This narrow safety net activates only when a
|
|
9
|
-
* create() transition exists, then re-installs both settings context wrappers
|
|
10
|
-
* on the new live loader.
|
|
11
|
-
*/
|
|
12
|
-
export const SETTINGS_RC8_CLIENT_PRELUDE = String.raw`(function(){
|
|
13
|
-
'use strict';
|
|
14
|
-
var TARGET = 'dsh-vision-router';
|
|
15
|
-
var FIELD = 'allowRemoteSettings';
|
|
16
|
-
var ENDPOINT = '/_dsh/vision-router/remote-settings-permission';
|
|
17
|
-
var CHANNEL = '/vision-router-settings';
|
|
18
|
-
var AUTHORIZE_ENDPOINT = 'authorize';
|
|
19
|
-
var scopeCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
|
|
20
|
-
var binderCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
|
|
21
|
-
var localContextCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
|
|
22
|
-
var connectionCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
|
|
23
|
-
var rpcCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
|
|
24
|
-
var riskContextCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
|
|
25
|
-
var authorizationPromise;
|
|
26
|
-
|
|
27
|
-
function normalizePermissionValue(value) {
|
|
28
|
-
if (typeof value === 'boolean') return value;
|
|
29
|
-
if (value === 'true') return true;
|
|
30
|
-
if (value === 'false') return false;
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function isLoopbackLocation(locationLike) {
|
|
35
|
-
var hostname = locationLike && typeof locationLike.hostname === 'string'
|
|
36
|
-
? locationLike.hostname.toLowerCase().replace(/^\[|\]$/g, '')
|
|
37
|
-
: '';
|
|
38
|
-
var protocol = locationLike && typeof locationLike.protocol === 'string'
|
|
39
|
-
? locationLike.protocol.toLowerCase()
|
|
40
|
-
: '';
|
|
41
|
-
if (protocol === 'dsh-app:' && hostname === 'app') return true;
|
|
42
|
-
if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return true;
|
|
43
|
-
return /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function writePermission(operation, value, revision) {
|
|
47
|
-
if (typeof fetch !== 'function') throw new Error('Vision Router local settings transport is unavailable');
|
|
48
|
-
var payload = { operation: operation };
|
|
49
|
-
if (operation === 'set') payload.value = value;
|
|
50
|
-
if (Number.isInteger(revision) && revision >= 0) payload.expectedRevision = revision;
|
|
51
|
-
var response = await fetch(ENDPOINT, {
|
|
52
|
-
method: 'POST',
|
|
53
|
-
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
54
|
-
body: JSON.stringify(payload)
|
|
55
|
-
});
|
|
56
|
-
var body;
|
|
57
|
-
try { body = await response.json(); } catch (_) { body = undefined; }
|
|
58
|
-
if (!response.ok || !body || body.ok !== true) {
|
|
59
|
-
var message = body && body.error && body.error.message ? body.error.message : ('HTTP ' + response.status);
|
|
60
|
-
var error = new Error(message);
|
|
61
|
-
if (body && body.error && body.error.code) error.code = body.error.code;
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
return body.value;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function wrapScope(scope) {
|
|
68
|
-
if (!scope || (typeof scope !== 'object' && typeof scope !== 'function')) return scope;
|
|
69
|
-
if (scopeCache && scopeCache.has(scope)) return scopeCache.get(scope);
|
|
70
|
-
var overlay;
|
|
71
|
-
var cachedRawSnapshot;
|
|
72
|
-
var cachedOverlayKey;
|
|
73
|
-
var cachedClientSnapshot;
|
|
74
|
-
|
|
75
|
-
function overlayKey() {
|
|
76
|
-
if (!overlay) return 'none';
|
|
77
|
-
return (overlay.present ? '1' : '0') + ':' + String(overlay.value) + ':' + String(overlay.revision);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function rawSnapshot() {
|
|
81
|
-
try { return typeof scope.getSnapshot === 'function' ? scope.getSnapshot() : undefined; } catch (_) { return undefined; }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function snapshotWithOverlay(snapshot) {
|
|
85
|
-
if (!overlay || !snapshot || typeof snapshot !== 'object') return snapshot;
|
|
86
|
-
var user = snapshot.user && typeof snapshot.user === 'object' && !Array.isArray(snapshot.user) ? snapshot.user : {};
|
|
87
|
-
var present = Object.prototype.hasOwnProperty.call(user, FIELD);
|
|
88
|
-
var matches = overlay.present ? present && user[FIELD] === overlay.value : !present;
|
|
89
|
-
if (matches && (!Number.isInteger(overlay.revision) || !Number.isInteger(snapshot.revision) || snapshot.revision >= overlay.revision)) {
|
|
90
|
-
overlay = undefined;
|
|
91
|
-
return snapshot;
|
|
92
|
-
}
|
|
93
|
-
var nextUser = Object.assign({}, user);
|
|
94
|
-
if (overlay.present) nextUser[FIELD] = overlay.value;
|
|
95
|
-
else delete nextUser[FIELD];
|
|
96
|
-
var nextValue = snapshot.value && typeof snapshot.value === 'object' && !Array.isArray(snapshot.value)
|
|
97
|
-
? Object.assign({}, snapshot.value)
|
|
98
|
-
: snapshot.value;
|
|
99
|
-
if (nextValue && typeof nextValue === 'object') nextValue[FIELD] = overlay.present ? overlay.value : false;
|
|
100
|
-
return Object.assign({}, snapshot, {
|
|
101
|
-
user: nextUser,
|
|
102
|
-
value: nextValue,
|
|
103
|
-
revision: Number.isInteger(overlay.revision) ? overlay.revision : snapshot.revision
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function snapshotForClient(snapshot) {
|
|
108
|
-
var keyBefore = overlayKey();
|
|
109
|
-
if (snapshot === cachedRawSnapshot && keyBefore === cachedOverlayKey) return cachedClientSnapshot;
|
|
110
|
-
var effective = snapshotWithOverlay(snapshot);
|
|
111
|
-
var keyAfter = overlayKey();
|
|
112
|
-
if (snapshot === cachedRawSnapshot && keyAfter === cachedOverlayKey) return cachedClientSnapshot;
|
|
113
|
-
if (!effective || typeof effective !== 'object' || effective.mode !== 'host') {
|
|
114
|
-
cachedRawSnapshot = snapshot;
|
|
115
|
-
cachedOverlayKey = keyAfter;
|
|
116
|
-
cachedClientSnapshot = effective;
|
|
117
|
-
return effective;
|
|
118
|
-
}
|
|
119
|
-
var next = effective;
|
|
120
|
-
var value = effective.value;
|
|
121
|
-
if (value && typeof value === 'object' && !Array.isArray(value)
|
|
122
|
-
&& Object.prototype.hasOwnProperty.call(value, FIELD)
|
|
123
|
-
&& typeof value[FIELD] === 'boolean') {
|
|
124
|
-
value = Object.assign({}, value, { [FIELD]: value[FIELD] ? 'true' : '' });
|
|
125
|
-
next = Object.assign({}, next, { value: value });
|
|
126
|
-
}
|
|
127
|
-
var user = effective.user;
|
|
128
|
-
if (user && typeof user === 'object' && !Array.isArray(user)
|
|
129
|
-
&& Object.prototype.hasOwnProperty.call(user, FIELD)
|
|
130
|
-
&& typeof user[FIELD] === 'boolean') {
|
|
131
|
-
user = Object.assign({}, user, { [FIELD]: user[FIELD] ? 'true' : 'false' });
|
|
132
|
-
next = Object.assign({}, next, { user: user });
|
|
133
|
-
}
|
|
134
|
-
cachedRawSnapshot = snapshot;
|
|
135
|
-
cachedOverlayKey = keyAfter;
|
|
136
|
-
cachedClientSnapshot = next;
|
|
137
|
-
return next;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
var wrapped = new Proxy(scope, {
|
|
141
|
-
get: function(target, property) {
|
|
142
|
-
if (property === 'getSnapshot') return function(){ return snapshotForClient(rawSnapshot()); };
|
|
143
|
-
if (property === 'set') {
|
|
144
|
-
var set = Reflect.get(target, property, target);
|
|
145
|
-
return async function(field, value) {
|
|
146
|
-
var snapshot = rawSnapshot();
|
|
147
|
-
if (field === FIELD && snapshot && snapshot.mode === 'host') {
|
|
148
|
-
var normalized = normalizePermissionValue(value);
|
|
149
|
-
if (normalized === undefined) throw new TypeError('allowRemoteSettings must be boolean');
|
|
150
|
-
var result = await writePermission('set', normalized, snapshot.revision);
|
|
151
|
-
overlay = { present: true, value: normalized, revision: result && result.revision };
|
|
152
|
-
cachedRawSnapshot = undefined;
|
|
153
|
-
if (typeof target.load === 'function') await target.load();
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
return typeof set === 'function' ? set.apply(target, arguments) : undefined;
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
if (property === 'unset') {
|
|
160
|
-
var unset = Reflect.get(target, property, target);
|
|
161
|
-
return async function(field) {
|
|
162
|
-
var snapshot = rawSnapshot();
|
|
163
|
-
if (field === FIELD && snapshot && snapshot.mode === 'host') {
|
|
164
|
-
var result = await writePermission('unset', undefined, snapshot.revision);
|
|
165
|
-
overlay = { present: false, revision: result && result.revision };
|
|
166
|
-
cachedRawSnapshot = undefined;
|
|
167
|
-
if (typeof target.load === 'function') await target.load();
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
return typeof unset === 'function' ? unset.apply(target, arguments) : undefined;
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
var value = Reflect.get(target, property, target);
|
|
174
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
if (scopeCache) scopeCache.set(scope, wrapped);
|
|
178
|
-
return wrapped;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function wrapBinder(binder) {
|
|
182
|
-
if (!binder || (typeof binder !== 'object' && typeof binder !== 'function')) return binder;
|
|
183
|
-
if (binderCache && binderCache.has(binder)) return binderCache.get(binder);
|
|
184
|
-
var wrapped = new Proxy(binder, {
|
|
185
|
-
get: function(target, property) {
|
|
186
|
-
if (property === 'bind') {
|
|
187
|
-
var bind = Reflect.get(target, property, target);
|
|
188
|
-
if (typeof bind !== 'function') return bind;
|
|
189
|
-
return function(spec) {
|
|
190
|
-
var scope = bind.apply(target, arguments);
|
|
191
|
-
return spec && spec.namespace === 'vision-router' ? wrapScope(scope) : scope;
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
var value = Reflect.get(target, property, target);
|
|
195
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
if (binderCache) binderCache.set(binder, wrapped);
|
|
199
|
-
return wrapped;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function wrapLocalContext(ctx) {
|
|
203
|
-
if (!ctx || typeof ctx !== 'object') return ctx;
|
|
204
|
-
if (localContextCache && localContextCache.has(ctx)) return localContextCache.get(ctx);
|
|
205
|
-
var wrapped = new Proxy(ctx, {
|
|
206
|
-
get: function(target, property) {
|
|
207
|
-
if (property === 'settingsScope') return wrapBinder(Reflect.get(target, property, target));
|
|
208
|
-
var value = Reflect.get(target, property, target);
|
|
209
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
if (localContextCache) localContextCache.set(ctx, wrapped);
|
|
213
|
-
return wrapped;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function chineseLocale() {
|
|
217
|
-
var lang = '';
|
|
218
|
-
try {
|
|
219
|
-
lang = String((document && document.documentElement && document.documentElement.lang) || (navigator && navigator.language) || '').toLowerCase();
|
|
220
|
-
} catch (_) {}
|
|
221
|
-
return lang === '' || lang.indexOf('zh') === 0;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function riskMessage() {
|
|
225
|
-
return chineseLocale()
|
|
226
|
-
? '启用远程设置?\n\n当前页面通过远程地址访问 DSH。启用后,能够访问此 trusted host 的客户端可以修改 Vision Router 已开放的设置。\n\nDSH trustedHosts 不是身份认证,请仅在你信任当前网络和访问者时继续。API Key、HTTP Provider 凭据、本地 Ollama / LM Studio、产物路径等未列入远程白名单的敏感配置仍不会开放。\n\n确定要允许远程修改设置吗?'
|
|
227
|
-
: '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?';
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function isPermissionDisabledDescribe(channel, endpoint, result) {
|
|
231
|
-
return channel === CHANNEL
|
|
232
|
-
&& endpoint === 'describe'
|
|
233
|
-
&& result && result.ok === true
|
|
234
|
-
&& result.value && result.value.enabled !== true
|
|
235
|
-
&& result.value.reason === 'permission-disabled';
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async function authorizeAndRefresh(target, call) {
|
|
239
|
-
if (authorizationPromise) return authorizationPromise;
|
|
240
|
-
authorizationPromise = (async function(){
|
|
241
|
-
if (typeof window.confirm !== 'function' || window.confirm(riskMessage()) !== true) return undefined;
|
|
242
|
-
var authorized = await call.call(target, CHANNEL, AUTHORIZE_ENDPOINT, { acceptedRisk: true });
|
|
243
|
-
if (!authorized || authorized.ok !== true) {
|
|
244
|
-
var message = authorized && authorized.error && authorized.error.message
|
|
245
|
-
? authorized.error.message
|
|
246
|
-
: 'Vision Router could not enable remote settings.';
|
|
247
|
-
try { if (typeof window.alert === 'function') window.alert(message); } catch (_) {}
|
|
248
|
-
return undefined;
|
|
249
|
-
}
|
|
250
|
-
return call.call(target, CHANNEL, 'describe', {});
|
|
251
|
-
})();
|
|
252
|
-
try { return await authorizationPromise; }
|
|
253
|
-
finally { authorizationPromise = undefined; }
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function wrapRpc(rpc) {
|
|
257
|
-
if (!rpc || (typeof rpc !== 'object' && typeof rpc !== 'function')) return rpc;
|
|
258
|
-
if (rpcCache && rpcCache.has(rpc)) return rpcCache.get(rpc);
|
|
259
|
-
var wrapped = new Proxy(rpc, {
|
|
260
|
-
get: function(target, property) {
|
|
261
|
-
if (property === 'call') {
|
|
262
|
-
var call = Reflect.get(target, property, target);
|
|
263
|
-
if (typeof call !== 'function') return call;
|
|
264
|
-
return async function(channel, endpoint) {
|
|
265
|
-
var result = await call.apply(target, arguments);
|
|
266
|
-
if (!isPermissionDisabledDescribe(channel, endpoint, result)) return result;
|
|
267
|
-
var refreshed = await authorizeAndRefresh(target, call);
|
|
268
|
-
return refreshed || result;
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
var value = Reflect.get(target, property, target);
|
|
272
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
if (rpcCache) rpcCache.set(rpc, wrapped);
|
|
276
|
-
return wrapped;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function wrapConnection(connection) {
|
|
280
|
-
if (!connection || (typeof connection !== 'object' && typeof connection !== 'function')) return connection;
|
|
281
|
-
if (connectionCache && connectionCache.has(connection)) return connectionCache.get(connection);
|
|
282
|
-
var wrapped = new Proxy(connection, {
|
|
283
|
-
get: function(target, property) {
|
|
284
|
-
if (property === 'isLoopback') {
|
|
285
|
-
var locationLike;
|
|
286
|
-
try { locationLike = window && window.location; } catch (_) { locationLike = undefined; }
|
|
287
|
-
if (isLoopbackLocation(locationLike)) return true;
|
|
288
|
-
}
|
|
289
|
-
if (property === 'rpc') return wrapRpc(Reflect.get(target, property, target));
|
|
290
|
-
var value = Reflect.get(target, property, target);
|
|
291
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
if (connectionCache) connectionCache.set(connection, wrapped);
|
|
295
|
-
return wrapped;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function wrapRiskContext(ctx) {
|
|
299
|
-
if (!ctx || typeof ctx !== 'object') return ctx;
|
|
300
|
-
if (riskContextCache && riskContextCache.has(ctx)) return riskContextCache.get(ctx);
|
|
301
|
-
var wrapped = new Proxy(ctx, {
|
|
302
|
-
get: function(target, property) {
|
|
303
|
-
if (property === 'get') {
|
|
304
|
-
var get = Reflect.get(target, property, target);
|
|
305
|
-
if (typeof get !== 'function') return get;
|
|
306
|
-
return function(name) {
|
|
307
|
-
var value = get.apply(target, arguments);
|
|
308
|
-
return name === 'connection' ? wrapConnection(value) : value;
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
var value = Reflect.get(target, property, target);
|
|
312
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
if (riskContextCache) riskContextCache.set(ctx, wrapped);
|
|
316
|
-
return wrapped;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function patchLiveLoader(loader) {
|
|
320
|
-
if (!loader || typeof loader.load !== 'function' || loader.load.__visionRouterSettingsRc8Lifecycle) return;
|
|
321
|
-
var original = loader.load;
|
|
322
|
-
function load(spec) {
|
|
323
|
-
if (spec && spec.id === TARGET && typeof spec.factory === 'function') {
|
|
324
|
-
var factory = spec.factory;
|
|
325
|
-
spec = Object.assign({}, spec, {
|
|
326
|
-
factory: function(require) {
|
|
327
|
-
var exports = factory(require);
|
|
328
|
-
if (exports && typeof exports.apply === 'function' && !exports.apply.__visionRouterSettingsRc8Lifecycle) {
|
|
329
|
-
var apply = exports.apply;
|
|
330
|
-
var wrappedApply = function(ctx) {
|
|
331
|
-
var rest = Array.prototype.slice.call(arguments, 1);
|
|
332
|
-
var nextCtx = wrapRiskContext(wrapLocalContext(ctx));
|
|
333
|
-
return apply.apply(exports, [nextCtx].concat(rest));
|
|
334
|
-
};
|
|
335
|
-
Object.defineProperty(wrappedApply, '__visionRouterSettingsRc8Lifecycle', { value: true });
|
|
336
|
-
exports.apply = wrappedApply;
|
|
337
|
-
}
|
|
338
|
-
return exports;
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
return original.call(this, spec);
|
|
343
|
-
}
|
|
344
|
-
Object.defineProperty(load, '__visionRouterSettingsRc8Lifecycle', { value: true });
|
|
345
|
-
loader.load = load;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function patchCreate(loader) {
|
|
349
|
-
if (!loader || typeof loader.create !== 'function' || loader.create.__visionRouterSettingsRc8Lifecycle) return;
|
|
350
|
-
var originalCreate = loader.create;
|
|
351
|
-
function create() {
|
|
352
|
-
var result = originalCreate.apply(this, arguments);
|
|
353
|
-
patchLiveLoader(loader);
|
|
354
|
-
return result;
|
|
355
|
-
}
|
|
356
|
-
Object.defineProperty(create, '__visionRouterSettingsRc8Lifecycle', { value: true });
|
|
357
|
-
loader.create = create;
|
|
358
|
-
if (loader.mode === 'live') patchLiveLoader(loader);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function install() {
|
|
362
|
-
if (window.__ModuleLoader__) {
|
|
363
|
-
patchCreate(window.__ModuleLoader__);
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
var descriptor = Object.getOwnPropertyDescriptor(window, '__ModuleLoader__');
|
|
367
|
-
if (descriptor && descriptor.configurable === false) return;
|
|
368
|
-
var previousGet = descriptor && descriptor.get;
|
|
369
|
-
var previousSet = descriptor && descriptor.set;
|
|
370
|
-
var stored = descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value') ? descriptor.value : undefined;
|
|
371
|
-
Object.defineProperty(window, '__ModuleLoader__', {
|
|
372
|
-
configurable: true,
|
|
373
|
-
enumerable: !descriptor || descriptor.enumerable !== false,
|
|
374
|
-
get: function(){ return previousGet ? previousGet.call(window) : stored; },
|
|
375
|
-
set: function(value) {
|
|
376
|
-
if (previousSet) previousSet.call(window, value); else stored = value;
|
|
377
|
-
try { patchCreate(previousGet ? previousGet.call(window) : value); } catch (_) {}
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
if (stored) patchCreate(stored);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
try { install(); } catch (_) {}
|
|
384
|
-
})();`
|
|
385
|
-
|
|
386
|
-
export function injectSettingsRc8ClientPrelude(html) {
|
|
387
|
-
return injectClientCarrierHtml(html, SETTINGS_RC8_MARK, SETTINGS_RC8_CLIENT_PRELUDE)
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
export function installSettingsRc8ClientLifecycle(ctx) {
|
|
391
|
-
installClientScriptCarriers(ctx, {
|
|
392
|
-
marker: SETTINGS_RC8_MARK,
|
|
393
|
-
prelude: SETTINGS_RC8_CLIENT_PRELUDE,
|
|
394
|
-
label: 'vision-router: rc8 settings client lifecycle',
|
|
395
|
-
injectHtml: injectSettingsRc8ClientPrelude,
|
|
396
|
-
})
|
|
397
|
-
}
|