dsh-vision-router 2.2.7 → 2.3.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 +8 -8
- package/README.zh.md +8 -8
- 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/compat-inventory.md +26 -34
- package/docs/architecture/dsh-020-rc2-adaptation.md +23 -0
- package/docs/architecture/dsh-compatibility-matrix.md +65 -54
- package/docs/architecture/dsh-support-window.md +29 -21
- package/docs/architecture/final-closure-audit.md +2 -0
- package/docs/architecture/host-first-proxy-convergence.md +1 -1
- package/docs/releases/v2.2.8.md +25 -0
- package/docs/releases/v2.3.0.md +53 -0
- package/entry.js +8 -56
- package/index.js +161 -691
- package/lib/catalog-corrections.js +1 -2
- package/lib/client-host-compat-prelude.js +7 -12
- package/lib/client-maintenance-actions.js +145 -0
- package/lib/client-presentation-boundary-main.js +82 -32
- package/lib/client-presentation-boundary.js +60 -40
- package/lib/client-script-carrier.js +90 -0
- package/lib/client.js +248 -146
- package/lib/core-apply-lifecycle.js +47 -0
- package/lib/core-primitives.js +88 -135
- package/lib/desktop-screenshot-tool.js +262 -0
- package/lib/dsh-contract-compat.js +11 -26
- package/lib/dsh-support-window.js +4 -4
- package/lib/guide-vision-toggle-highlight.js +7 -10
- package/lib/http-compat.js +1 -2
- package/lib/live-model-client-prelude.js +7 -14
- package/lib/local-remote-settings-permission.js +41 -16
- package/lib/maintenance-routes.js +136 -0
- package/lib/public-config.js +57 -0
- package/lib/public-entry.js +2 -7
- 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/runtime-composition.js +105 -30
- package/lib/runtime-i18n-boundary.js +10 -11
- package/lib/session-message-source-compat.js +7 -3
- package/lib/session-vision-index.js +154 -9
- package/lib/session-vision-mode-boundary.js +43 -8
- package/lib/settings-client-loader-lifecycle.js +129 -0
- package/lib/settings-factory-lifecycle.js +25 -14
- package/lib/settings-ia-client-prelude.js +11 -11
- package/lib/settings-limit-client-prelude.js +7 -9
- package/lib/sharp-runtime.js +0 -7
- package/lib/strict-live-model-client-prelude.js +7 -10
- package/lib/structured-flow-hardening.js +3 -2
- package/lib/v2-settings-ia-integration.js +21 -6
- package/lib/vision-backend-runtime-policy.js +3 -2
- package/lib/vision-background-benchmark.js +3 -5
- package/lib/vision-capability-benchmark-client.js +7 -10
- package/lib/vision-capability-benchmark-service.js +1 -0
- package/lib/vision-diagnostics-routes.js +216 -0
- package/lib/vision-evidence-guidance.js +3 -1
- package/lib/vision-exact-check-client.js +11 -6
- package/lib/vision-model-visibility-boundary-main.js +7 -11
- package/lib/vision-model-visibility-boundary.js +7 -12
- package/lib/vision-provider-transport.js +0 -29
- package/lib/vision-routing-settings-prelude.js +7 -11
- package/lib/vision-toggle-root-hardening.js +50 -8
- package/lib/vision-tool-runtime-boundary.js +5 -1
- package/lib/vision-turn-budget-client-prelude.js +7 -10
- package/lib/web/remote-settings-client.js +9 -14
- package/package.json +23 -13
- package/lib/android-attachment-compat.js +0 -244
- package/lib/settings-client-rc8-lifecycle.js +0 -402
|
@@ -1,113 +1,25 @@
|
|
|
1
1
|
import { htmlHasScriptMarker } from './html-script-marker.js'
|
|
2
|
+
import { REMOTE_SETTINGS_RISK_CLIENT_BOUNDARY_SOURCE } from './remote-settings-risk-client-boundary.js'
|
|
2
3
|
|
|
3
4
|
const RISK_PRELUDE_MARK = 'data-vision-router-remote-settings-risk-confirmation'
|
|
4
5
|
|
|
5
6
|
export const REMOTE_SETTINGS_RISK_CLIENT_PRELUDE = String.raw`(function(){
|
|
6
7
|
'use strict';
|
|
7
|
-
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
function riskMessage() {
|
|
23
|
-
return chineseLocale()
|
|
24
|
-
? '启用远程设置?\n\n当前页面通过远程地址访问 DSH。启用后,能够访问此 trusted host 的客户端可以修改 Vision Router 已开放的设置。\n\nDSH trustedHosts 不是身份认证,请仅在你信任当前网络和访问者时继续。API Key、HTTP Provider 凭据、本地 Ollama / LM Studio、产物路径等未列入远程白名单的敏感配置仍不会开放。\n\n确定要允许远程修改设置吗?'
|
|
25
|
-
: '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?';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isPermissionDisabledDescribe(channel, endpoint, result) {
|
|
29
|
-
return channel === CHANNEL
|
|
30
|
-
&& endpoint === 'describe'
|
|
31
|
-
&& result && result.ok === true
|
|
32
|
-
&& result.value && result.value.enabled !== true
|
|
33
|
-
&& result.value.reason === 'permission-disabled';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function authorizeAndRefresh(target, call) {
|
|
37
|
-
if (authorizationPromise) return authorizationPromise;
|
|
38
|
-
authorizationPromise = (async function(){
|
|
39
|
-
if (typeof window.confirm !== 'function' || window.confirm(riskMessage()) !== true) return undefined;
|
|
40
|
-
var authorized = await call.call(target, CHANNEL, AUTHORIZE_ENDPOINT, { acceptedRisk: true });
|
|
41
|
-
if (!authorized || authorized.ok !== true) {
|
|
42
|
-
var message = authorized && authorized.error && authorized.error.message
|
|
43
|
-
? authorized.error.message
|
|
44
|
-
: 'Vision Router could not enable remote settings.';
|
|
45
|
-
try { if (typeof window.alert === 'function') window.alert(message); } catch (_) {}
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
return call.call(target, CHANNEL, 'describe', {});
|
|
49
|
-
})();
|
|
50
|
-
try { return await authorizationPromise; }
|
|
51
|
-
finally { authorizationPromise = undefined; }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function wrapRpc(rpc) {
|
|
55
|
-
if (!rpc || (typeof rpc !== 'object' && typeof rpc !== 'function')) return rpc;
|
|
56
|
-
if (rpcCache && rpcCache.has(rpc)) return rpcCache.get(rpc);
|
|
57
|
-
var wrapped = new Proxy(rpc, {
|
|
58
|
-
get: function(target, property) {
|
|
59
|
-
if (property === 'call') {
|
|
60
|
-
var call = Reflect.get(target, property, target);
|
|
61
|
-
if (typeof call !== 'function') return call;
|
|
62
|
-
return async function(channel, endpoint) {
|
|
63
|
-
var result = await call.apply(target, arguments);
|
|
64
|
-
if (!isPermissionDisabledDescribe(channel, endpoint, result)) return result;
|
|
65
|
-
var refreshed = await authorizeAndRefresh(target, call);
|
|
66
|
-
return refreshed || result;
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
var value = Reflect.get(target, property, target);
|
|
70
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
if (rpcCache) rpcCache.set(rpc, wrapped);
|
|
74
|
-
return wrapped;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function wrapConnection(connection) {
|
|
78
|
-
if (!connection || (typeof connection !== 'object' && typeof connection !== 'function')) return connection;
|
|
79
|
-
if (connectionCache && connectionCache.has(connection)) return connectionCache.get(connection);
|
|
80
|
-
var wrapped = new Proxy(connection, {
|
|
81
|
-
get: function(target, property) {
|
|
82
|
-
if (property === 'rpc') return wrapRpc(Reflect.get(target, property, target));
|
|
83
|
-
var value = Reflect.get(target, property, target);
|
|
84
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
if (connectionCache) connectionCache.set(connection, wrapped);
|
|
88
|
-
return wrapped;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function wrapContext(ctx) {
|
|
92
|
-
if (!ctx || typeof ctx !== 'object') return ctx;
|
|
93
|
-
if (contextCache && contextCache.has(ctx)) return contextCache.get(ctx);
|
|
94
|
-
var wrapped = new Proxy(ctx, {
|
|
95
|
-
get: function(target, property) {
|
|
96
|
-
if (property === 'get') {
|
|
97
|
-
var get = Reflect.get(target, property, target);
|
|
98
|
-
if (typeof get !== 'function') return get;
|
|
99
|
-
return function(name) {
|
|
100
|
-
var value = get.apply(target, arguments);
|
|
101
|
-
return name === 'connection' ? wrapConnection(value) : value;
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
var value = Reflect.get(target, property, target);
|
|
105
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
if (contextCache) contextCache.set(ctx, wrapped);
|
|
109
|
-
return wrapped;
|
|
110
|
-
}
|
|
8
|
+
${REMOTE_SETTINGS_RISK_CLIENT_BOUNDARY_SOURCE}
|
|
9
|
+
var riskBoundary = createRemoteSettingsRiskClientBoundary({
|
|
10
|
+
confirmImpl: function(message) {
|
|
11
|
+
return typeof window.confirm === 'function' && window.confirm(message) === true;
|
|
12
|
+
},
|
|
13
|
+
alertImpl: function(message) {
|
|
14
|
+
try { if (typeof window.alert === 'function') window.alert(message); } catch (_) {}
|
|
15
|
+
},
|
|
16
|
+
locale: function() {
|
|
17
|
+
try {
|
|
18
|
+
return String((document && document.documentElement && document.documentElement.lang)
|
|
19
|
+
|| (navigator && navigator.language) || '');
|
|
20
|
+
} catch (_) { return ''; }
|
|
21
|
+
}
|
|
22
|
+
});
|
|
111
23
|
|
|
112
24
|
function patchLoader(loader) {
|
|
113
25
|
if (!loader || typeof loader.load !== 'function' || loader.load.__visionRouterRemoteRiskConfirmation) return;
|
|
@@ -122,7 +34,7 @@ export const REMOTE_SETTINGS_RISK_CLIENT_PRELUDE = String.raw`(function(){
|
|
|
122
34
|
var apply = exports.apply;
|
|
123
35
|
var wrappedApply = function(ctx) {
|
|
124
36
|
var rest = Array.prototype.slice.call(arguments, 1);
|
|
125
|
-
return apply.apply(exports, [wrapContext(ctx)].concat(rest));
|
|
37
|
+
return apply.apply(exports, [riskBoundary.wrapContext(ctx)].concat(rest));
|
|
126
38
|
};
|
|
127
39
|
Object.defineProperty(wrappedApply, '__visionRouterRemoteRiskConfirmation', { value: true });
|
|
128
40
|
exports.apply = wrappedApply;
|
|
@@ -14,6 +14,7 @@ import { contextWithNativeImageCoexistence } from './native-image-coexistence.js
|
|
|
14
14
|
import { createCoreVisionSurfaceRuntime } from './core-vision-surface.js'
|
|
15
15
|
import { installSessionVisionIndexBoundary } from './session-vision-index.js'
|
|
16
16
|
import { createSessionVisionRuntime } from './session-vision-runtime.js'
|
|
17
|
+
import { runCoreApplyLifecycle } from './core-apply-lifecycle.js'
|
|
17
18
|
import { installLegacyCoreVisionPolicyBridge } from './legacy-core-vision-policy-bridge.js'
|
|
18
19
|
import { installSessionVisionModeBoundary } from './session-vision-mode-boundary.js'
|
|
19
20
|
import { installPiAiBridgeWireCompat } from './pi-ai-bridge-wire-compat.js'
|
|
@@ -23,7 +24,6 @@ import { installAdversarialHardening } from './adversarial-hardening.js'
|
|
|
23
24
|
import { installOllamaColdStartGuard } from './ollama-cold-start.js'
|
|
24
25
|
import { installLocalVisionStabilizer } from './local-vision-stabilizer.js'
|
|
25
26
|
import { installWrapperDirectoryAlias } from './wrapper-directory.js'
|
|
26
|
-
import { installAndroidAttachmentCompat } from './android-attachment-compat.js'
|
|
27
27
|
import { contextWithCoalescedAdapterUpdates } from './adapter-update-coalescer.js'
|
|
28
28
|
import { installTesseractExecFileCompat } from './tesseract-exec-compat.js'
|
|
29
29
|
import { installLocalMutationRouteBoundary } from './web-capability-boundary.js'
|
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
} from './structured-flow-hardening.js'
|
|
58
58
|
import { installVisionLimitDiagnostics } from './vision-limit-diagnostics.js'
|
|
59
59
|
import {
|
|
60
|
-
|
|
60
|
+
installDshHostCapabilityDiagnostics,
|
|
61
61
|
createSessionEventReader,
|
|
62
62
|
createSessionEventTailReader,
|
|
63
63
|
createSessionLogReader,
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
} from './web/index.js'
|
|
74
74
|
import { createRuntimeI18nCoreFacade } from './runtime-i18n-core.js'
|
|
75
75
|
import { createRuntimeI18nCoreScope } from './runtime-i18n-core-scope.js'
|
|
76
|
+
import { installVisionMaintenanceRoutes } from './maintenance-routes.js'
|
|
76
77
|
|
|
77
78
|
function liveVisionSettings(ctx, fallback) {
|
|
78
79
|
try {
|
|
@@ -95,7 +96,7 @@ function legacyCoreVisionSettings(ctx, fallback) {
|
|
|
95
96
|
return { ...current, autoActivateOnImage: false }
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
function installHostAndSecurityBoundaries(ctx, config, core) {
|
|
99
100
|
// SessionVisionIndex calls planGuardStopShadows before Core.apply. Keep that
|
|
100
101
|
// one helper behind the live-locale facade; the final Core itself still gets
|
|
101
102
|
// the ordinary composition identity and is localized by the scoped context.
|
|
@@ -174,10 +175,29 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
|
|
|
174
175
|
Config: core.Config,
|
|
175
176
|
})
|
|
176
177
|
: ownershipCtx
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
// DVR 2.3 supports DSH from the 0.1.5 train, whose attachment
|
|
179
|
+
// contract already owns batch persistence. The old no-batch Android wrapper
|
|
180
|
+
// is therefore unreachable; keep capability diagnostics as an explicit
|
|
181
|
+
// composition responsibility instead of hiding that side effect in a shim.
|
|
182
|
+
installDshHostCapabilityDiagnostics(settingsCtx)
|
|
183
|
+
const attachmentCompatCtx = settingsCtx
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
attachmentCompatCtx,
|
|
187
|
+
batchAttachmentHost,
|
|
188
|
+
capabilityStore,
|
|
189
|
+
hardenedConfig,
|
|
190
|
+
hostOwnsOfficialDeepSeek,
|
|
191
|
+
logging,
|
|
192
|
+
runtimeConfig,
|
|
193
|
+
runtimeI18nCore,
|
|
194
|
+
runtimeI18nCoreScope,
|
|
195
|
+
runtimePerformanceStore,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
180
198
|
|
|
199
|
+
function createRuntimeOwners(host, core) {
|
|
200
|
+
const { attachmentCompatCtx, logging, runtimeConfig, runtimeI18nCore } = host
|
|
181
201
|
const toolRuntimeCtx = installVisionToolRuntimeBoundary(attachmentCompatCtx, runtimeConfig)
|
|
182
202
|
const nativeImageCompat = contextWithNativeImageCoexistence(toolRuntimeCtx, runtimeConfig)
|
|
183
203
|
const coreVisionSurfaceRuntime = createCoreVisionSurfaceRuntime({
|
|
@@ -213,6 +233,17 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
|
|
|
213
233
|
legacyCoreCompat.config,
|
|
214
234
|
)
|
|
215
235
|
|
|
236
|
+
return {
|
|
237
|
+
coreVisionSurfaceRuntime,
|
|
238
|
+
nativeImageCompat,
|
|
239
|
+
sessionVisionModeCompat,
|
|
240
|
+
sessionVisionRuntime,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function installRoutingAndHostProducts(host, owners, core) {
|
|
245
|
+
const { capabilityStore, logging, runtimeConfig, runtimePerformanceStore } = host
|
|
246
|
+
const { sessionVisionModeCompat } = owners
|
|
216
247
|
// Final structured-flow guard sits closest to core.apply so it sees the
|
|
217
248
|
// actual tool registrations and pre-step listener. The diagnostic observer
|
|
218
249
|
// remains immediately inside it so existing timeout/budget semantics stay
|
|
@@ -266,6 +297,19 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
|
|
|
266
297
|
})
|
|
267
298
|
installPiAiBridgeWireCompat(reconciledCtx, logging.logger)
|
|
268
299
|
|
|
300
|
+
return {
|
|
301
|
+
breakerShadowHealth,
|
|
302
|
+
liveDiscovery,
|
|
303
|
+
reconciledCtx,
|
|
304
|
+
sessionEventTailReader,
|
|
305
|
+
sessionTurnResolver,
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function installExecutionBoundaries(host, owners, routing, core, providerTransport) {
|
|
310
|
+
const { capabilityStore, logging, runtimeConfig, runtimeI18nCoreScope, runtimePerformanceStore } = host
|
|
311
|
+
const { sessionVisionRuntime } = owners
|
|
312
|
+
const { breakerShadowHealth, liveDiscovery, reconciledCtx } = routing
|
|
269
313
|
const executionCtx = contextWithVisionExecutionPolicy(reconciledCtx, {
|
|
270
314
|
isBridgeEvidence: (provider, model) => liveDiscovery.hasModel(provider, model),
|
|
271
315
|
evidenceSource: (provider, model) => liveDiscovery.evidenceSource?.(provider, model),
|
|
@@ -314,11 +358,13 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
|
|
|
314
358
|
core,
|
|
315
359
|
evidenceSource: (provider, model) => liveDiscovery.evidenceSource?.(provider, model),
|
|
316
360
|
logger: logging.logger,
|
|
361
|
+
providerTransport,
|
|
317
362
|
finalContextDecorator: runtimeI18nCoreScope.decorate,
|
|
318
363
|
})
|
|
319
364
|
const capabilityBenchmark = installCapabilityBenchmarkService(backendRuntimeCtx, runtimeConfig, core, {
|
|
320
365
|
logger: logging.logger,
|
|
321
366
|
store: capabilityStore,
|
|
367
|
+
providerTransport,
|
|
322
368
|
})
|
|
323
369
|
attachCapabilityBenchmarkPresentation(capabilityBenchmark, {
|
|
324
370
|
ctx: backendRuntimeCtx,
|
|
@@ -338,6 +384,22 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
|
|
|
338
384
|
})
|
|
339
385
|
const coreRequestAuthorityCtx = contextWithAgentRequestRouteAuthority(backendRuntimeCtx)
|
|
340
386
|
|
|
387
|
+
return { coreRequestAuthorityCtx }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function applyMatureCoreBridge(host, owners, routing, execution, core, runtime) {
|
|
391
|
+
const {
|
|
392
|
+
attachmentCompatCtx,
|
|
393
|
+
batchAttachmentHost,
|
|
394
|
+
hardenedConfig,
|
|
395
|
+
hostOwnsOfficialDeepSeek,
|
|
396
|
+
logging,
|
|
397
|
+
runtimeConfig,
|
|
398
|
+
runtimeI18nCoreScope,
|
|
399
|
+
} = host
|
|
400
|
+
const { coreVisionSurfaceRuntime, sessionVisionModeCompat, sessionVisionRuntime } = owners
|
|
401
|
+
const { breakerShadowHealth, sessionEventTailReader, sessionTurnResolver } = routing
|
|
402
|
+
const { coreRequestAuthorityCtx } = execution
|
|
341
403
|
try {
|
|
342
404
|
const c = hardenedConfig && typeof hardenedConfig === 'object' ? hardenedConfig : {}
|
|
343
405
|
const local = c.localOllama && typeof c.localOllama === 'object' ? c.localOllama : {}
|
|
@@ -354,8 +416,27 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
|
|
|
354
416
|
/* diagnostics must never break apply */
|
|
355
417
|
}
|
|
356
418
|
|
|
357
|
-
|
|
358
|
-
|
|
419
|
+
let schemaBootstrapFinished = false
|
|
420
|
+
const finishSchemaBootstrapOnce = () => {
|
|
421
|
+
if (schemaBootstrapFinished) return
|
|
422
|
+
schemaBootstrapFinished = true
|
|
423
|
+
coreVisionSurfaceRuntime.finishSchemaBootstrap()
|
|
424
|
+
}
|
|
425
|
+
const installPostCoreRuntime = () => {
|
|
426
|
+
installVisionMaintenanceRoutes(coreRequestAuthorityCtx, { logger: logging.logger })
|
|
427
|
+
finishSchemaBootstrapOnce()
|
|
428
|
+
installWrapperDirectoryAlias(attachmentCompatCtx, runtimeConfig, logging.logger)
|
|
429
|
+
}
|
|
430
|
+
const failCoreApply = (error) => {
|
|
431
|
+
finishSchemaBootstrapOnce()
|
|
432
|
+
logging.logger.error(
|
|
433
|
+
'vision-router: plugin apply failed: %s',
|
|
434
|
+
error && error.stack ? error.stack : error && error.message ? error.message : String(error),
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return runCoreApplyLifecycle(
|
|
439
|
+
() => runtimeI18nCoreScope.run(() =>
|
|
359
440
|
withVisionCircuitBreakerObserver(
|
|
360
441
|
breakerShadowHealth.capture,
|
|
361
442
|
() => core.apply(
|
|
@@ -367,28 +448,22 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
|
|
|
367
448
|
sessionTurnResolver,
|
|
368
449
|
sessionEventTailReader,
|
|
369
450
|
hostOwnsOfficialDeepSeek,
|
|
451
|
+
providerTransport: runtime?.providerTransport,
|
|
370
452
|
},
|
|
371
453
|
),
|
|
372
454
|
),
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
logging.logger.error(
|
|
389
|
-
'vision-router: plugin apply failed: %s',
|
|
390
|
-
error && error.stack ? error.stack : error && error.message ? error.message : String(error),
|
|
391
|
-
)
|
|
392
|
-
throw error
|
|
393
|
-
}
|
|
394
|
-
}
|
|
455
|
+
),
|
|
456
|
+
{
|
|
457
|
+
onSuccess: installPostCoreRuntime,
|
|
458
|
+
onFailure: failCoreApply,
|
|
459
|
+
},
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function applyVisionRuntimeComposition(ctx, config = {}, core, runtime = {}) {
|
|
464
|
+
const host = installHostAndSecurityBoundaries(ctx, config, core)
|
|
465
|
+
const owners = createRuntimeOwners(host, core)
|
|
466
|
+
const routing = installRoutingAndHostProducts(host, owners, core)
|
|
467
|
+
const execution = installExecutionBoundaries(host, owners, routing, core, runtime?.providerTransport)
|
|
468
|
+
return applyMatureCoreBridge(host, owners, routing, execution, core, runtime)
|
|
469
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createRuntimeI18n, DEEP_TOOL_MOUNT_STATE } from './runtime-i18n.js'
|
|
2
2
|
import { depthCopyFor } from './depth-guidance.js'
|
|
3
3
|
import { blocksHaveRetainedImage } from './image-offload-compat.js'
|
|
4
|
+
import { captureFetchDelegate, installFetchWrapper } from './fetch-wrapper-lifecycle.js'
|
|
5
|
+
import { visionRouterMessageSource } from './session-message-source-compat.js'
|
|
4
6
|
|
|
5
7
|
const PLUGIN_NAME = 'dsh-vision-router'
|
|
6
8
|
const DEEP_TOOL_NAMES = new Set([
|
|
@@ -432,7 +434,7 @@ function addAutoMountReminder(decision, payload, i18n) {
|
|
|
432
434
|
: `${Date.now()}-${Math.floor(Math.random() * 1e9)}`
|
|
433
435
|
}`,
|
|
434
436
|
content: [{ type: 'text', text: i18n.t('autoMountReminder') }],
|
|
435
|
-
source:
|
|
437
|
+
source: visionRouterMessageSource(payload?.agent?.session),
|
|
436
438
|
}
|
|
437
439
|
const structuredIndex = base.findIndex((message) =>
|
|
438
440
|
typeof message?.id === 'string' && message.id.startsWith('vision-router-structured-'),
|
|
@@ -520,9 +522,9 @@ function localizeRequestBody(value, i18n) {
|
|
|
520
522
|
function installPromptFetchBoundary(ctx, i18n) {
|
|
521
523
|
if (typeof ctx?.effect !== 'function' || typeof globalThis.fetch !== 'function') return
|
|
522
524
|
ctx.effect(() => {
|
|
523
|
-
const
|
|
525
|
+
const delegateFetch = captureFetchDelegate()
|
|
524
526
|
const localizedFetch = (input, init) => {
|
|
525
|
-
if (i18n.language() !== 'en' || typeof init?.body !== 'string') return
|
|
527
|
+
if (i18n.language() !== 'en' || typeof init?.body !== 'string') return delegateFetch(input, init)
|
|
526
528
|
const body = init.body
|
|
527
529
|
if (
|
|
528
530
|
!body.includes('请按以下结构识别这张图片') &&
|
|
@@ -530,21 +532,18 @@ function installPromptFetchBoundary(ctx, i18n) {
|
|
|
530
532
|
!body.includes(OCR_FALLBACK_PROMPT_ZH) &&
|
|
531
533
|
!body.includes(LONG_SCREENSHOT_OCR_PROMPT_ZH)
|
|
532
534
|
) {
|
|
533
|
-
return
|
|
535
|
+
return delegateFetch(input, init)
|
|
534
536
|
}
|
|
535
537
|
try {
|
|
536
538
|
const parsed = JSON.parse(body)
|
|
537
539
|
const localized = localizeRequestBody(parsed, i18n)
|
|
538
|
-
if (localized === parsed) return
|
|
539
|
-
return
|
|
540
|
+
if (localized === parsed) return delegateFetch(input, init)
|
|
541
|
+
return delegateFetch(input, { ...init, body: JSON.stringify(localized) })
|
|
540
542
|
} catch {
|
|
541
|
-
return
|
|
543
|
+
return delegateFetch(input, init)
|
|
542
544
|
}
|
|
543
545
|
}
|
|
544
|
-
|
|
545
|
-
return () => {
|
|
546
|
-
if (globalThis.fetch === localizedFetch) globalThis.fetch = originalFetch
|
|
547
|
-
}
|
|
546
|
+
return installFetchWrapper(localizedFetch)
|
|
548
547
|
}, 'vision-router: runtime i18n prompt boundary')
|
|
549
548
|
}
|
|
550
549
|
|
|
@@ -62,9 +62,13 @@ function normalizeHookResult(event, args, result) {
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* DSH Session V3 requires the historical {kind:'plugin', plugin:...} source,
|
|
65
|
-
* while V4 rejects that wrapper in favor of producer-owned source kinds.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* while V4 rejects that wrapper in favor of producer-owned source kinds.
|
|
66
|
+
*
|
|
67
|
+
* Session-facing DVR producers are responsible for calling
|
|
68
|
+
* visionRouterMessageSource(session) when they create a message. This final
|
|
69
|
+
* publication boundary is deliberately defense-in-depth: it protects legacy
|
|
70
|
+
* or future producers that accidentally reach a hook with the wrong DVR source,
|
|
71
|
+
* but producer correctness must not depend on this proxy being the sole owner.
|
|
68
72
|
*/
|
|
69
73
|
export function installVisionRouterMessageSourceBoundary(ctx) {
|
|
70
74
|
if (!ctx || typeof ctx !== 'object') return ctx
|
|
@@ -3,6 +3,9 @@ import { sessionSurfaceReplacementIntent } from './session-surface-compat.js'
|
|
|
3
3
|
import { createSessionVisionStateStore } from './session-vision-state.js'
|
|
4
4
|
|
|
5
5
|
const MAX_PENDING_REPAIR_EVENTS = 256
|
|
6
|
+
const MAX_REPAIR_OVERFLOW_RANGES = 32
|
|
7
|
+
const MAX_REPAIR_OVERFLOW_INSPECTIONS_PER_PASS = 512
|
|
8
|
+
const MAX_REPAIR_OVERFLOW_READS_PER_PASS = 32
|
|
6
9
|
|
|
7
10
|
function isObject(value) {
|
|
8
11
|
return value !== null && typeof value === 'object'
|
|
@@ -87,6 +90,8 @@ export function createSessionVisionIndex({
|
|
|
87
90
|
const guardSurfaceScans = new WeakMap()
|
|
88
91
|
const pendingToolRepairEvents = new WeakMap()
|
|
89
92
|
const pendingGuardRepairEvents = new WeakMap()
|
|
93
|
+
const toolRepairOverflowRanges = new WeakMap()
|
|
94
|
+
const guardRepairOverflowRanges = new WeakMap()
|
|
90
95
|
const unsupportedSurfaceContracts = new WeakSet()
|
|
91
96
|
const repairReadWarnings = new WeakMap()
|
|
92
97
|
const repairFeedOverflowWarnings = new WeakSet()
|
|
@@ -295,7 +300,132 @@ export function createSessionVisionIndex({
|
|
|
295
300
|
return undefined
|
|
296
301
|
}
|
|
297
302
|
|
|
298
|
-
|
|
303
|
+
// Feed overflow stores only dropped sequence ranges, never frozen event
|
|
304
|
+
// objects. Adjacent ranges coalesce; pathological sparse overflow is
|
|
305
|
+
// conservatively collapsed once the range count reaches a fixed bound. The
|
|
306
|
+
// recovery loop then advances that range in bounded numeric/read batches and
|
|
307
|
+
// consults only Host-owned readSessionEvent(), so memory remains bounded
|
|
308
|
+
// without returning to whole-Session history materialization.
|
|
309
|
+
const addRepairOverflowSeq = (overflowBySession, session, seq) => {
|
|
310
|
+
if (!Number.isSafeInteger(seq) || seq < 0 || Object.is(seq, -0)) return
|
|
311
|
+
let ranges = overflowBySession.get(session)
|
|
312
|
+
if (!ranges) {
|
|
313
|
+
ranges = []
|
|
314
|
+
overflowBySession.set(session, ranges)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let start = seq
|
|
318
|
+
let end = seq
|
|
319
|
+
let insertAt = 0
|
|
320
|
+
while (insertAt < ranges.length && ranges[insertAt].end + 1 < start) insertAt += 1
|
|
321
|
+
while (insertAt < ranges.length && ranges[insertAt].start - 1 <= end) {
|
|
322
|
+
start = Math.min(start, ranges[insertAt].start)
|
|
323
|
+
end = Math.max(end, ranges[insertAt].end)
|
|
324
|
+
ranges.splice(insertAt, 1)
|
|
325
|
+
}
|
|
326
|
+
ranges.splice(insertAt, 0, { start, end })
|
|
327
|
+
|
|
328
|
+
if (ranges.length > MAX_REPAIR_OVERFLOW_RANGES) {
|
|
329
|
+
overflowBySession.set(session, [{
|
|
330
|
+
start: ranges[0].start,
|
|
331
|
+
end: ranges[ranges.length - 1].end,
|
|
332
|
+
}])
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const advanceRepairOverflowRange = (ranges) => {
|
|
337
|
+
const range = ranges?.[0]
|
|
338
|
+
if (!range) return
|
|
339
|
+
if (range.start >= range.end) ranges.shift()
|
|
340
|
+
else range.start += 1
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const recoverRepairOverflowEvents = async (overflowBySession, pendingBySession, session) => {
|
|
344
|
+
const ranges = overflowBySession.get(session)
|
|
345
|
+
if (!ranges || ranges.length === 0) return []
|
|
346
|
+
|
|
347
|
+
const nodes = surfaceNodes(session)
|
|
348
|
+
if (!nodes || nodes.length === 0) {
|
|
349
|
+
overflowBySession.delete(session)
|
|
350
|
+
repairFeedOverflowWarnings.delete(session)
|
|
351
|
+
return []
|
|
352
|
+
}
|
|
353
|
+
const current = new Set(nodes)
|
|
354
|
+
const pending = pendingBySession.get(session)
|
|
355
|
+
const recovered = []
|
|
356
|
+
let inspections = 0
|
|
357
|
+
let reads = 0
|
|
358
|
+
|
|
359
|
+
while (
|
|
360
|
+
ranges.length > 0
|
|
361
|
+
&& inspections < MAX_REPAIR_OVERFLOW_INSPECTIONS_PER_PASS
|
|
362
|
+
&& reads < MAX_REPAIR_OVERFLOW_READS_PER_PASS
|
|
363
|
+
) {
|
|
364
|
+
const seq = ranges[0].start
|
|
365
|
+
inspections += 1
|
|
366
|
+
|
|
367
|
+
// A dropped repair that is no longer on the live surface is settled. A
|
|
368
|
+
// seq still present in the fast-path map will be consumed normally.
|
|
369
|
+
if (!current.has(seq) || pending?.has(seq)) {
|
|
370
|
+
advanceRepairOverflowRange(ranges)
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (typeof readSessionEvent !== 'function') {
|
|
375
|
+
warnRepairReadFailure(
|
|
376
|
+
session,
|
|
377
|
+
seq,
|
|
378
|
+
new Error('Session event reader is unavailable during feed overflow recovery'),
|
|
379
|
+
)
|
|
380
|
+
break
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let result
|
|
384
|
+
try {
|
|
385
|
+
result = await readSessionEvent(session, seq)
|
|
386
|
+
} catch (error) {
|
|
387
|
+
warnRepairReadFailure(session, seq, error)
|
|
388
|
+
break
|
|
389
|
+
}
|
|
390
|
+
if (result?.supported !== true) {
|
|
391
|
+
warnRepairReadFailure(
|
|
392
|
+
session,
|
|
393
|
+
seq,
|
|
394
|
+
result?.supported === false
|
|
395
|
+
? new Error('Session event reader is unsupported during feed overflow recovery')
|
|
396
|
+
: new Error('Session event reader returned an invalid capability result during feed overflow recovery'),
|
|
397
|
+
)
|
|
398
|
+
break
|
|
399
|
+
}
|
|
400
|
+
const event = result.event
|
|
401
|
+
if (!event || typeof event !== 'object' || (event.seq !== undefined && event.seq !== seq)) {
|
|
402
|
+
warnRepairReadFailure(
|
|
403
|
+
session,
|
|
404
|
+
seq,
|
|
405
|
+
new Error(`Session event reader returned no matching event for overflow seq ${seq}`),
|
|
406
|
+
)
|
|
407
|
+
break
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
reads += 1
|
|
411
|
+
advanceRepairOverflowRange(ranges)
|
|
412
|
+
repairReadWarnings.delete(session)
|
|
413
|
+
// The live feed may deliver the same committed event while the exact read
|
|
414
|
+
// is in flight. In that case the fast-path map owns it; do not return a
|
|
415
|
+
// second copy from recovery.
|
|
416
|
+
if (!pendingBySession.get(session)?.has(seq) && !isReplacementEvent(event)) {
|
|
417
|
+
recovered.push({ seq, event })
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (ranges.length === 0) {
|
|
422
|
+
overflowBySession.delete(session)
|
|
423
|
+
repairFeedOverflowWarnings.delete(session)
|
|
424
|
+
}
|
|
425
|
+
return recovered
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const cachePendingRepairEvent = (pendingBySession, overflowBySession, session, event) => {
|
|
299
429
|
if (!session || !Number.isSafeInteger(event?.seq) || event.seq < 0 || Object.is(event.seq, -0)) return
|
|
300
430
|
let pending = pendingBySession.get(session)
|
|
301
431
|
if (!pending) {
|
|
@@ -304,11 +434,14 @@ export function createSessionVisionIndex({
|
|
|
304
434
|
}
|
|
305
435
|
if (!pending.has(event.seq) && pending.size >= MAX_PENDING_REPAIR_EVENTS) {
|
|
306
436
|
const oldest = pending.keys().next().value
|
|
307
|
-
if (oldest !== undefined)
|
|
437
|
+
if (oldest !== undefined) {
|
|
438
|
+
pending.delete(oldest)
|
|
439
|
+
addRepairOverflowSeq(overflowBySession, session, oldest)
|
|
440
|
+
}
|
|
308
441
|
if (!repairFeedOverflowWarnings.has(session)) {
|
|
309
442
|
repairFeedOverflowWarnings.add(session)
|
|
310
443
|
logger?.warn?.(
|
|
311
|
-
'vision-router: session surface repair event feed exceeded %s pending events;
|
|
444
|
+
'vision-router: session surface repair event feed exceeded %s pending events; scheduling bounded exact-event recovery',
|
|
312
445
|
MAX_PENDING_REPAIR_EVENTS,
|
|
313
446
|
)
|
|
314
447
|
}
|
|
@@ -340,10 +473,10 @@ export function createSessionVisionIndex({
|
|
|
340
473
|
event,
|
|
341
474
|
(events, seqs) => core.planToolResultImageShadows(events, seqs, () => true),
|
|
342
475
|
)
|
|
343
|
-
if (plans.length > 0) cachePendingRepairEvent(pendingToolRepairEvents, session, event)
|
|
476
|
+
if (plans.length > 0) cachePendingRepairEvent(pendingToolRepairEvents, toolRepairOverflowRanges, session, event)
|
|
344
477
|
} else if (event.type === 'user/message' && typeof core.planGuardStopShadows === 'function') {
|
|
345
478
|
const plans = planAgainstOneEvent(event, (events, seqs) => core.planGuardStopShadows(events, seqs))
|
|
346
|
-
if (plans.length > 0) cachePendingRepairEvent(pendingGuardRepairEvents, session, event)
|
|
479
|
+
if (plans.length > 0) cachePendingRepairEvent(pendingGuardRepairEvents, guardRepairOverflowRanges, session, event)
|
|
347
480
|
}
|
|
348
481
|
repairFeedObserverWarnings.delete(session)
|
|
349
482
|
} catch (error) {
|
|
@@ -439,11 +572,17 @@ export function createSessionVisionIndex({
|
|
|
439
572
|
|
|
440
573
|
const repairToolResultSurfaceFromFeed = async (session) => {
|
|
441
574
|
if (typeof session?.append !== 'function' || typeof core.planToolResultImageShadows !== 'function') return 0
|
|
575
|
+
const recovered = await recoverRepairOverflowEvents(
|
|
576
|
+
toolRepairOverflowRanges,
|
|
577
|
+
pendingToolRepairEvents,
|
|
578
|
+
session,
|
|
579
|
+
)
|
|
442
580
|
const pending = consumePendingRepairEvents(pendingToolRepairEvents, session)
|
|
443
|
-
|
|
581
|
+
const entries = [...recovered, ...pending]
|
|
582
|
+
if (entries.length === 0) return 0
|
|
444
583
|
const preserve = keepToolResultImages()
|
|
445
584
|
let repaired = 0
|
|
446
|
-
for (const { seq, event } of
|
|
585
|
+
for (const { seq, event } of entries) {
|
|
447
586
|
const events = { [seq]: event }
|
|
448
587
|
const plans = core.planToolResultImageShadows(events, [seq], () => !preserve)
|
|
449
588
|
for (const plan of plans) {
|
|
@@ -470,10 +609,16 @@ export function createSessionVisionIndex({
|
|
|
470
609
|
|
|
471
610
|
const repairGuardStopSurfaceFromFeed = async (session) => {
|
|
472
611
|
if (typeof session?.append !== 'function' || typeof core.planGuardStopShadows !== 'function') return 0
|
|
612
|
+
const recovered = await recoverRepairOverflowEvents(
|
|
613
|
+
guardRepairOverflowRanges,
|
|
614
|
+
pendingGuardRepairEvents,
|
|
615
|
+
session,
|
|
616
|
+
)
|
|
473
617
|
const pending = consumePendingRepairEvents(pendingGuardRepairEvents, session)
|
|
474
|
-
|
|
618
|
+
const entries = [...recovered, ...pending]
|
|
619
|
+
if (entries.length === 0) return 0
|
|
475
620
|
let repaired = 0
|
|
476
|
-
for (const { seq, event } of
|
|
621
|
+
for (const { seq, event } of entries) {
|
|
477
622
|
const events = { [seq]: event }
|
|
478
623
|
const plans = core.planGuardStopShadows(events, [seq])
|
|
479
624
|
for (const plan of plans) {
|