dsh-vision-router 2.2.8 → 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 +7 -7
- package/README.zh.md +7 -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/compat-inventory.md +26 -34
- package/docs/architecture/dsh-compatibility-matrix.md +65 -54
- package/docs/architecture/dsh-support-window.md +27 -21
- package/docs/architecture/final-closure-audit.md +2 -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-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 +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 +3 -3
- package/lib/http-compat.js +1 -2
- package/lib/local-remote-settings-permission.js +33 -7
- 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/settings-client-loader-lifecycle.js +129 -0
- package/lib/sharp-runtime.js +0 -7
- package/lib/structured-flow-hardening.js +3 -2
- package/lib/vision-backend-runtime-policy.js +3 -2
- 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-provider-transport.js +0 -29
- package/lib/vision-toggle-root-hardening.js +22 -5
- package/lib/vision-tool-runtime-boundary.js +5 -1
- package/lib/web/remote-settings-client.js +2 -2
- package/package.json +23 -13
- package/lib/android-attachment-compat.js +0 -244
- package/lib/settings-client-rc8-lifecycle.js +0 -397
package/lib/client.js
CHANGED
|
@@ -12,6 +12,147 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
const { useState, useMemo } = React
|
|
13
13
|
const { ImageGallery } = require('@deepseek-ai/dsh-client-ui-attachment')
|
|
14
14
|
|
|
15
|
+
// <generated:client-maintenance-actions>
|
|
16
|
+
function createClientMaintenanceActions({
|
|
17
|
+
fetchImpl,
|
|
18
|
+
t,
|
|
19
|
+
getTestState,
|
|
20
|
+
setTestState,
|
|
21
|
+
getUpdateState,
|
|
22
|
+
setUpdateState,
|
|
23
|
+
getSelfUpdateState,
|
|
24
|
+
setSelfUpdateState,
|
|
25
|
+
confirmImpl,
|
|
26
|
+
alertImpl,
|
|
27
|
+
} = {}) {
|
|
28
|
+
if (typeof fetchImpl !== 'function') throw new TypeError('client maintenance actions require fetch')
|
|
29
|
+
const translate = typeof t === 'function' ? t : (key) => String(key)
|
|
30
|
+
|
|
31
|
+
const diagnosticError = (value, fallback) => {
|
|
32
|
+
if (typeof value === 'string' && value.trim() !== '') return value.trim()
|
|
33
|
+
if (value && typeof value.message === 'string' && value.message.trim() !== '') {
|
|
34
|
+
return value.message.trim()
|
|
35
|
+
}
|
|
36
|
+
return fallback
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const runTestConnection = async () => {
|
|
40
|
+
if (getTestState?.()?.status === 'running') return
|
|
41
|
+
setTestState?.({ status: 'running' })
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetchImpl('/_dsh/vision-router/test-connection')
|
|
44
|
+
const result = await response.json().catch(() => undefined)
|
|
45
|
+
setTestState?.({ status: 'done', result })
|
|
46
|
+
} catch (error) {
|
|
47
|
+
setTestState?.({
|
|
48
|
+
status: 'done',
|
|
49
|
+
result: { ok: false, error: error && error.message ? error.message : String(error) },
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const requestDesktopScreenshotPermission = () => {
|
|
55
|
+
// Fire-and-forget: the native permission dialog must never hold Settings
|
|
56
|
+
// save completion open. The actual screenshot tool still reports denial.
|
|
57
|
+
try {
|
|
58
|
+
void Promise.resolve(fetchImpl('/_dsh/vision-router/request-screenshot-permission', {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
cache: 'no-store',
|
|
61
|
+
})).catch(() => {})
|
|
62
|
+
} catch {
|
|
63
|
+
// Permission prompting is best-effort after an already-landed setting.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const runUpdateCheck = async (force = false) => {
|
|
68
|
+
const current = getUpdateState?.() ?? {}
|
|
69
|
+
if (current.status === 'running') return
|
|
70
|
+
setUpdateState?.({ status: 'running', result: current.result })
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetchImpl(
|
|
73
|
+
'/_dsh/vision-router/update-check' + (force ? '?force=1' : ''),
|
|
74
|
+
{ cache: 'no-store' },
|
|
75
|
+
)
|
|
76
|
+
const result = await response.json().catch(() => undefined)
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error(diagnosticError(result && result.error, `HTTP ${response.status}`))
|
|
79
|
+
}
|
|
80
|
+
if (!result || typeof result !== 'object' || typeof result.ok !== 'boolean') {
|
|
81
|
+
throw new Error(translate('updateInvalidResponse'))
|
|
82
|
+
}
|
|
83
|
+
setUpdateState?.({ status: 'done', result })
|
|
84
|
+
} catch (error) {
|
|
85
|
+
setUpdateState?.({
|
|
86
|
+
status: 'done',
|
|
87
|
+
result: {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: error && error.message ? error.message : String(error),
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const runSelfUpdate = async () => {
|
|
96
|
+
if (getSelfUpdateState?.()?.status === 'running') return
|
|
97
|
+
const checked = getUpdateState?.()?.result
|
|
98
|
+
const auto = checked && checked.autoUpdate
|
|
99
|
+
if (!checked || checked.ok !== true || checked.updateAvailable !== true || !auto || !auto.token) return
|
|
100
|
+
if (typeof confirmImpl === 'function' && confirmImpl(translate('updateConfirm')) !== true) return
|
|
101
|
+
setSelfUpdateState?.({ status: 'running', result: undefined })
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetchImpl('/_dsh/vision-router/self-update', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
cache: 'no-store',
|
|
106
|
+
headers: { 'x-dsh-vision-router-update-token': auto.token },
|
|
107
|
+
})
|
|
108
|
+
const result = await response.json().catch(() => undefined)
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
throw new Error(diagnosticError(result && result.error, `HTTP ${response.status}`))
|
|
111
|
+
}
|
|
112
|
+
if (!result || typeof result !== 'object' || typeof result.ok !== 'boolean') {
|
|
113
|
+
throw new Error(translate('updateInvalidResponse'))
|
|
114
|
+
}
|
|
115
|
+
setSelfUpdateState?.({ status: 'done', result })
|
|
116
|
+
} catch (error) {
|
|
117
|
+
setSelfUpdateState?.({
|
|
118
|
+
status: 'error',
|
|
119
|
+
result: { ok: false, error: error && error.message ? error.message : String(error) },
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const openLogFolder = async () => {
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetchImpl('/_dsh/vision-router/logs', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
cache: 'no-store',
|
|
129
|
+
})
|
|
130
|
+
const result = await response.json().catch(() => undefined)
|
|
131
|
+
if (!response.ok || !result || result.ok !== true) {
|
|
132
|
+
throw new Error(result && result.error ? result.error : `HTTP ${response.status}`)
|
|
133
|
+
}
|
|
134
|
+
return result
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (typeof alertImpl === 'function') {
|
|
137
|
+
alertImpl(
|
|
138
|
+
translate('openLogFolderFailed') + ':' +
|
|
139
|
+
(error && error.message ? error.message : String(error)),
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
return undefined
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
runTestConnection,
|
|
148
|
+
requestDesktopScreenshotPermission,
|
|
149
|
+
runUpdateCheck,
|
|
150
|
+
runSelfUpdate,
|
|
151
|
+
openLogFolder,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// </generated:client-maintenance-actions>
|
|
155
|
+
|
|
15
156
|
// ── locale dictionaries (follow the app's language setting) ─────────────
|
|
16
157
|
const NS = 'vision-router'
|
|
17
158
|
const zh = {
|
|
@@ -37,7 +178,8 @@ window.__ModuleLoader__.load({
|
|
|
37
178
|
guideStep1Body: '聊天页右下角、输入框旁的模型选择器已经被高亮。选择你平时使用的聊天模型;发图片时请选择带「+ 自动识图」的版本。选好后点击「下一步」。',
|
|
38
179
|
guideStepNext: '下一步',
|
|
39
180
|
guidePromptTitle: '第 2 步 · 选择识图模型',
|
|
40
|
-
|
|
181
|
+
guidePromptLauncherBody: '点击侧边栏左下角被高亮的设置入口;Desktop 中这里可能显示为账号头像或「更多」。也可以直接点「下一步」继续。',
|
|
182
|
+
guidePromptMenuBody: '在弹出的账号菜单中点击被高亮的「设置」(或点「下一步」自动打开设置面板)。',
|
|
41
183
|
guidePromptNavBody: '在设置面板左侧点击被高亮的「Vision Router」入口(或点「下一步」自动进入)。我会直接定位到「识图模型」。',
|
|
42
184
|
guidePromptCancel: '结束引导',
|
|
43
185
|
guideChainTitle: '第 3 步 · 设置备用模型',
|
|
@@ -238,12 +380,9 @@ window.__ModuleLoader__.load({
|
|
|
238
380
|
'本地后端不受影响,仍在最前。关闭时保持既有顺序(付费在前、免费补全在后)。',
|
|
239
381
|
hintDesktopScreenshot: '允许模型按需截取桌面屏幕用于识图。默认关闭;开启后才提供 vision_screenshot 工具。macOS 在保存开启时会立即触发一次屏幕录制授权检查;如果已经授权,系统不会重复弹窗。',
|
|
240
382
|
hintStealth:
|
|
241
|
-
'
|
|
242
|
-
'
|
|
243
|
-
'
|
|
244
|
-
stealthOfficialDeadHint:
|
|
245
|
-
'提示:这是旧版 Host,且官方 DeepSeek 行当前被禁用;Vision Router 正使用兼容性 keep-alive 路由。' +
|
|
246
|
-
'如需恢复完全官方的原生行,请在 cordis.patch.yml 中重新启用 llm-deepseek 后重启。',
|
|
383
|
+
'仅保留旧配置字段兼容。DVR 2.3 支持的 DSH 中官方 provider 始终归 Host 所有,Vision Router 只使用' +
|
|
384
|
+
'「+ 自动识图」包装,不再接管或重建 deepseek-official,也不要求你禁用 llm-deepseek。opencode 等' +
|
|
385
|
+
'自定义路由默认自动包装,只有要限制范围时才用下方手动包装。',
|
|
247
386
|
stealthOfficialHostOwnedHint:
|
|
248
387
|
'检测到官方 DeepSeek provider 被禁用或不可用,但当前 DSH 已由 Host 负责该路由。Vision Router 不会自行重建它。' +
|
|
249
388
|
'请重新启用 llm-deepseek,再通过「+ 自动识图」包装 /「👁 识图」处理图片任务。',
|
|
@@ -325,7 +464,8 @@ window.__ModuleLoader__.load({
|
|
|
325
464
|
guideStep1Body: 'The model selector next to the chat input in the lower-right corner is now highlighted. For image turns, pick a group marked “+ Auto Vision”; the model inside still handles chat, reasoning, and tool calls. Click “Next” when done.',
|
|
326
465
|
guideStepNext: 'Next',
|
|
327
466
|
guidePromptTitle: 'Step 2 · Choose the vision model',
|
|
328
|
-
|
|
467
|
+
guidePromptLauncherBody: 'Click the highlighted settings launcher at the bottom-left of the sidebar. In Desktop it may appear as your account avatar or the More menu. You can also press “Next”.',
|
|
468
|
+
guidePromptMenuBody: 'In the account menu, click the highlighted “Settings” item (or press “Next” to open Settings automatically).',
|
|
329
469
|
guidePromptNavBody: 'In Settings, click the highlighted “Vision Router” entry (or press “Next”). I will take you directly to “Vision model”.',
|
|
330
470
|
guidePromptCancel: 'End guide',
|
|
331
471
|
guideChainTitle: 'Step 3 · Add fallback models',
|
|
@@ -527,13 +667,10 @@ window.__ModuleLoader__.load({
|
|
|
527
667
|
'Local backends are unaffected and stay first. When off, the existing order is preserved (paid first, free appended last).',
|
|
528
668
|
hintDesktopScreenshot: 'Lets the model capture the desktop on demand for image understanding. Off by default; vision_screenshot is exposed only when enabled. On macOS, saving the enabled setting immediately triggers a screen-recording permission check; if permission was already granted, macOS will not prompt again.',
|
|
529
669
|
hintStealth:
|
|
530
|
-
'
|
|
531
|
-
'uses the “+ Auto Vision” wrapper and never
|
|
532
|
-
'
|
|
533
|
-
'
|
|
534
|
-
stealthOfficialDeadHint:
|
|
535
|
-
'Notice: on this legacy Host the official DeepSeek row is disabled, so Vision Router is using its compatibility ' +
|
|
536
|
-
'keep-alive route. To restore the fully official route, re-enable llm-deepseek in cordis.patch.yml and restart.',
|
|
670
|
+
'Retained only for persisted-setting compatibility. On every DSH Host supported by DVR 2.3, the official provider stays ' +
|
|
671
|
+
'Host-owned: Vision Router uses the “+ Auto Vision” wrapper and never takes over or reconstructs deepseek-official, nor ' +
|
|
672
|
+
'asks you to disable llm-deepseek. Custom routes like opencode are auto-wrapped by default; use the manual wrapper section ' +
|
|
673
|
+
'below only when you want to restrict their scope.',
|
|
537
674
|
stealthOfficialHostOwnedHint:
|
|
538
675
|
'The official DeepSeek provider is disabled or unavailable, but this DSH Host owns that provider route. Vision Router ' +
|
|
539
676
|
'will not recreate it. Re-enable llm-deepseek, then use the “+ Auto Vision” wrapper / Vision control for image tasks.',
|
|
@@ -1849,15 +1986,49 @@ window.__ModuleLoader__.load({
|
|
|
1849
1986
|
}
|
|
1850
1987
|
return undefined
|
|
1851
1988
|
}
|
|
1852
|
-
//
|
|
1853
|
-
//
|
|
1854
|
-
function
|
|
1989
|
+
// Web/older Hosts expose a direct Settings dialog button; Desktop can
|
|
1990
|
+
// replace it with an account launcher. Anchor that shape to sidebar.settings.
|
|
1991
|
+
function guideSidebarSettingsRoot() {
|
|
1992
|
+
if (typeof document === 'undefined') return undefined
|
|
1993
|
+
return document.querySelector('[data-slot="sidebar.settings"]') || undefined
|
|
1994
|
+
}
|
|
1995
|
+
function guideDirectSettingsButton() {
|
|
1855
1996
|
if (typeof document === 'undefined') return undefined
|
|
1997
|
+
const root = guideSidebarSettingsRoot()
|
|
1998
|
+
const direct = root && root.querySelector('button[aria-haspopup="dialog"]')
|
|
1999
|
+
if (guideElementUsable(direct)) return direct
|
|
2000
|
+
// Older supported Hosts may predate the sidebar.settings wrapper.
|
|
1856
2001
|
const list = Array.from(document.querySelectorAll('button[aria-haspopup="dialog"]')).filter(
|
|
1857
2002
|
(el) => !el.closest || !el.closest('[data-composer-card]'),
|
|
1858
2003
|
)
|
|
1859
2004
|
return list.find(guideElementUsable)
|
|
1860
2005
|
}
|
|
2006
|
+
function guideAccountSettingsLauncher() {
|
|
2007
|
+
const root = guideSidebarSettingsRoot()
|
|
2008
|
+
if (!root) return undefined
|
|
2009
|
+
const launcher = root.querySelector('button[aria-haspopup="menu"]')
|
|
2010
|
+
return guideElementUsable(launcher) ? launcher : undefined
|
|
2011
|
+
}
|
|
2012
|
+
function guideSettingsLauncher() {
|
|
2013
|
+
return guideDirectSettingsButton() || guideAccountSettingsLauncher()
|
|
2014
|
+
}
|
|
2015
|
+
function guideSettingsMenuItem() {
|
|
2016
|
+
if (typeof document === 'undefined') return undefined
|
|
2017
|
+
const launcher = guideAccountSettingsLauncher()
|
|
2018
|
+
if (!launcher || launcher.getAttribute('aria-expanded') !== 'true') return undefined
|
|
2019
|
+
const menus = Array.from(document.querySelectorAll('[role="menu"]'))
|
|
2020
|
+
for (let index = menus.length - 1; index >= 0; index--) {
|
|
2021
|
+
const menu = menus[index]
|
|
2022
|
+
if (!guideElementUsable(menu)) continue
|
|
2023
|
+
const rows = Array.from(menu.querySelectorAll('button[role="menuitem"]'))
|
|
2024
|
+
const match = rows.find((row) => {
|
|
2025
|
+
const label = (row.textContent || '').trim()
|
|
2026
|
+
return label.startsWith('设置') || label.startsWith('Settings')
|
|
2027
|
+
})
|
|
2028
|
+
if (guideElementUsable(match)) return match
|
|
2029
|
+
}
|
|
2030
|
+
return undefined
|
|
2031
|
+
}
|
|
1861
2032
|
// The walkthrough has one canonical destination: the first-class
|
|
1862
2033
|
// Vision Router settings section registered below.
|
|
1863
2034
|
function guideVisionRouterNav() {
|
|
@@ -1878,13 +2049,20 @@ window.__ModuleLoader__.load({
|
|
|
1878
2049
|
settingsPanel: guideSettingsPanel,
|
|
1879
2050
|
settingsOpen: guideSettingsPanelOpen,
|
|
1880
2051
|
chatModelTarget: () => guideFindFirstUsable(GUIDE_SELECTOR_TARGETS),
|
|
1881
|
-
|
|
2052
|
+
settingsLauncher: guideSettingsLauncher,
|
|
2053
|
+
settingsMenuItem: guideSettingsMenuItem,
|
|
1882
2054
|
visionRouterNav: guideVisionRouterNav,
|
|
1883
2055
|
closeSettings: closeSettingsShell,
|
|
1884
|
-
|
|
1885
|
-
const
|
|
1886
|
-
if (!
|
|
1887
|
-
|
|
2056
|
+
advanceSettingsLauncher() {
|
|
2057
|
+
const launcher = guideSettingsLauncher()
|
|
2058
|
+
if (!launcher || typeof launcher.click !== 'function') return false
|
|
2059
|
+
launcher.click()
|
|
2060
|
+
return true
|
|
2061
|
+
},
|
|
2062
|
+
openSettingsFromMenu() {
|
|
2063
|
+
const item = guideSettingsMenuItem()
|
|
2064
|
+
if (!item || typeof item.click !== 'function') return false
|
|
2065
|
+
item.click()
|
|
1888
2066
|
return true
|
|
1889
2067
|
},
|
|
1890
2068
|
openVisionRouter() {
|
|
@@ -1905,15 +2083,17 @@ window.__ModuleLoader__.load({
|
|
|
1905
2083
|
return guideHostUi.chatModelTarget() ? 'selector' : 'corner'
|
|
1906
2084
|
}
|
|
1907
2085
|
if (guideSessionState === GUIDE_STATE.VISION_MODEL) return 'done'
|
|
1908
|
-
if (typeof document === 'undefined') return '
|
|
2086
|
+
if (typeof document === 'undefined') return 'launcher'
|
|
1909
2087
|
if (document.querySelector('[data-vr-guide-target="vision-backend"]')) return 'done'
|
|
1910
2088
|
if (guideHostUi.settingsPanel()) return 'nav'
|
|
1911
|
-
return '
|
|
2089
|
+
if (guideHostUi.settingsMenuItem()) return 'menu'
|
|
2090
|
+
return 'launcher'
|
|
1912
2091
|
}
|
|
1913
2092
|
function guideTarget(step, phase) {
|
|
1914
2093
|
if (step === 'step1') return guideHostUi.chatModelTarget()
|
|
1915
2094
|
if (phase === 'nav') return guideHostUi.visionRouterNav()
|
|
1916
|
-
return guideHostUi.
|
|
2095
|
+
if (phase === 'menu') return guideHostUi.settingsMenuItem()
|
|
2096
|
+
return guideHostUi.settingsLauncher()
|
|
1917
2097
|
}
|
|
1918
2098
|
function ensureGuideSpotlight() {
|
|
1919
2099
|
if (visionGuideSpotlight) return visionGuideSpotlight
|
|
@@ -1954,7 +2134,11 @@ window.__ModuleLoader__.load({
|
|
|
1954
2134
|
if (step === 'step1') return { title: t('guideStep1Title'), body: t('guideStep1Body') }
|
|
1955
2135
|
return {
|
|
1956
2136
|
title: t('guidePromptTitle'),
|
|
1957
|
-
body: phase === 'nav'
|
|
2137
|
+
body: phase === 'nav'
|
|
2138
|
+
? t('guidePromptNavBody')
|
|
2139
|
+
: phase === 'menu'
|
|
2140
|
+
? t('guidePromptMenuBody')
|
|
2141
|
+
: t('guidePromptLauncherBody'),
|
|
1958
2142
|
}
|
|
1959
2143
|
}
|
|
1960
2144
|
function buildGuidePrompt(t, step, phase) {
|
|
@@ -1988,12 +2172,19 @@ window.__ModuleLoader__.load({
|
|
|
1988
2172
|
next.type = 'button'
|
|
1989
2173
|
next.className = 'vr-btn vr-btn-save'
|
|
1990
2174
|
next.textContent = t('guideStepNext')
|
|
1991
|
-
|
|
2175
|
+
const keepSettingsMenuOpen = (event) => {
|
|
2176
|
+
if (step !== 'step2' || prompt.dataset.vrPhase !== 'menu') return
|
|
2177
|
+
event.preventDefault(); event.stopPropagation()
|
|
2178
|
+
}
|
|
2179
|
+
next.addEventListener('pointerdown', keepSettingsMenuOpen)
|
|
2180
|
+
next.addEventListener('click', (event) => {
|
|
1992
2181
|
if (step === 'step1') {
|
|
1993
2182
|
dispatchGuide({ type: GUIDE_EVENT.NEXT }, t)
|
|
1994
2183
|
} else {
|
|
1995
|
-
const currentPhase = guidePhase('step2')
|
|
1996
|
-
if (currentPhase === '
|
|
2184
|
+
const currentPhase = prompt.dataset.vrPhase || guidePhase('step2')
|
|
2185
|
+
if (currentPhase === 'menu') keepSettingsMenuOpen(event)
|
|
2186
|
+
if (currentPhase === 'launcher') guideHostUi.advanceSettingsLauncher()
|
|
2187
|
+
else if (currentPhase === 'menu') guideHostUi.openSettingsFromMenu()
|
|
1997
2188
|
else if (currentPhase === 'nav') guideHostUi.openVisionRouter()
|
|
1998
2189
|
}
|
|
1999
2190
|
clearGuidePromptUI()
|
|
@@ -2055,9 +2246,8 @@ window.__ModuleLoader__.load({
|
|
|
2055
2246
|
if (arrow) arrow.classList.add(step === 'step1' ? 'vr-guide-arrow-corner-se' : 'vr-guide-arrow-corner-sw')
|
|
2056
2247
|
return
|
|
2057
2248
|
}
|
|
2058
|
-
// The
|
|
2059
|
-
|
|
2060
|
-
const prefer = phase === 'gear' ? ['top', 'right', 'bottom', 'left'] : ['right', 'top', 'bottom', 'left']
|
|
2249
|
+
// The bottom-left Settings launcher reads best with the prompt above it.
|
|
2250
|
+
const prefer = phase === 'launcher' ? ['top', 'right', 'bottom', 'left'] : ['right', 'top', 'bottom', 'left']
|
|
2061
2251
|
const clampX = (value) => Math.min(Math.max(value, margin), Math.max(margin, vw - pw - margin))
|
|
2062
2252
|
const clampY = (value) => Math.min(Math.max(value, margin), Math.max(margin, vh - ph - margin))
|
|
2063
2253
|
// Keep a small halo around the target clear so the prompt never sits
|
|
@@ -3089,91 +3279,24 @@ window.__ModuleLoader__.load({
|
|
|
3089
3279
|
}
|
|
3090
3280
|
}
|
|
3091
3281
|
|
|
3092
|
-
const
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
cache: 'no-store',
|
|
3111
|
-
}).catch(() => {})
|
|
3112
|
-
}
|
|
3113
|
-
|
|
3114
|
-
const diagnosticError = (value, fallback) => {
|
|
3115
|
-
if (typeof value === 'string' && value.trim() !== '') return value.trim()
|
|
3116
|
-
if (value && typeof value.message === 'string' && value.message.trim() !== '') {
|
|
3117
|
-
return value.message.trim()
|
|
3118
|
-
}
|
|
3119
|
-
return fallback
|
|
3120
|
-
}
|
|
3121
|
-
|
|
3122
|
-
const runUpdateCheck = async (force = false) => {
|
|
3123
|
-
if (updateState.status === 'running') return
|
|
3124
|
-
setUpdateState({ status: 'running', result: updateState.result })
|
|
3125
|
-
try {
|
|
3126
|
-
const response = await fetch(
|
|
3127
|
-
'/_dsh/vision-router/update-check' + (force ? '?force=1' : ''),
|
|
3128
|
-
{ cache: 'no-store' },
|
|
3129
|
-
)
|
|
3130
|
-
const result = await response.json().catch(() => undefined)
|
|
3131
|
-
if (!response.ok) {
|
|
3132
|
-
throw new Error(diagnosticError(result && result.error, `HTTP ${response.status}`))
|
|
3133
|
-
}
|
|
3134
|
-
if (!result || typeof result !== 'object' || typeof result.ok !== 'boolean') {
|
|
3135
|
-
throw new Error(t('updateInvalidResponse'))
|
|
3136
|
-
}
|
|
3137
|
-
setUpdateState({ status: 'done', result })
|
|
3138
|
-
} catch (error) {
|
|
3139
|
-
setUpdateState({
|
|
3140
|
-
status: 'done',
|
|
3141
|
-
result: {
|
|
3142
|
-
ok: false,
|
|
3143
|
-
error: error && error.message ? error.message : String(error),
|
|
3144
|
-
},
|
|
3145
|
-
})
|
|
3146
|
-
}
|
|
3147
|
-
}
|
|
3148
|
-
|
|
3149
|
-
const runSelfUpdate = async () => {
|
|
3150
|
-
if (selfUpdateState.status === 'running') return
|
|
3151
|
-
const checked = updateState.result
|
|
3152
|
-
const auto = checked && checked.autoUpdate
|
|
3153
|
-
if (!checked || checked.ok !== true || checked.updateAvailable !== true || !auto || !auto.token) return
|
|
3154
|
-
if (typeof window.confirm === 'function' && !window.confirm(t('updateConfirm'))) return
|
|
3155
|
-
setSelfUpdateState({ status: 'running', result: undefined })
|
|
3156
|
-
try {
|
|
3157
|
-
const response = await fetch('/_dsh/vision-router/self-update', {
|
|
3158
|
-
method: 'POST',
|
|
3159
|
-
cache: 'no-store',
|
|
3160
|
-
headers: { 'x-dsh-vision-router-update-token': auto.token },
|
|
3161
|
-
})
|
|
3162
|
-
const result = await response.json().catch(() => undefined)
|
|
3163
|
-
if (!response.ok) {
|
|
3164
|
-
throw new Error(diagnosticError(result && result.error, `HTTP ${response.status}`))
|
|
3165
|
-
}
|
|
3166
|
-
if (!result || typeof result !== 'object' || typeof result.ok !== 'boolean') {
|
|
3167
|
-
throw new Error(t('updateInvalidResponse'))
|
|
3168
|
-
}
|
|
3169
|
-
setSelfUpdateState({ status: 'done', result })
|
|
3170
|
-
} catch (error) {
|
|
3171
|
-
setSelfUpdateState({
|
|
3172
|
-
status: 'error',
|
|
3173
|
-
result: { ok: false, error: error && error.message ? error.message : String(error) },
|
|
3174
|
-
})
|
|
3175
|
-
}
|
|
3176
|
-
}
|
|
3282
|
+
const {
|
|
3283
|
+
runTestConnection,
|
|
3284
|
+
requestDesktopScreenshotPermission,
|
|
3285
|
+
runUpdateCheck,
|
|
3286
|
+
runSelfUpdate,
|
|
3287
|
+
openLogFolder,
|
|
3288
|
+
} = createClientMaintenanceActions({
|
|
3289
|
+
fetchImpl: (...args) => fetch(...args),
|
|
3290
|
+
t,
|
|
3291
|
+
getTestState: () => testState,
|
|
3292
|
+
setTestState,
|
|
3293
|
+
getUpdateState: () => updateState,
|
|
3294
|
+
setUpdateState,
|
|
3295
|
+
getSelfUpdateState: () => selfUpdateState,
|
|
3296
|
+
setSelfUpdateState,
|
|
3297
|
+
confirmImpl: typeof window.confirm === 'function' ? window.confirm.bind(window) : undefined,
|
|
3298
|
+
alertImpl: typeof window.alert === 'function' ? window.alert.bind(window) : undefined,
|
|
3299
|
+
})
|
|
3177
3300
|
|
|
3178
3301
|
const overriddenBadge = (key) =>
|
|
3179
3302
|
userHas(snapshot, key)
|
|
@@ -3898,8 +4021,8 @@ window.__ModuleLoader__.load({
|
|
|
3898
4021
|
// fetched (including after a remount that reset the state), start the
|
|
3899
4022
|
// fetch. React supports setState during render for the same component,
|
|
3900
4023
|
// and the status flips to 'loading' so this cannot loop. The same kick
|
|
3901
|
-
// runs the cheap test-connection probe once so the
|
|
3902
|
-
//
|
|
4024
|
+
// runs the cheap test-connection probe once so the Host-owned official
|
|
4025
|
+
// provider availability notice can render without a button click.
|
|
3903
4026
|
if (open && catalog.status === 'idle') {
|
|
3904
4027
|
loadCatalog()
|
|
3905
4028
|
}
|
|
@@ -4055,11 +4178,8 @@ window.__ModuleLoader__.load({
|
|
|
4055
4178
|
if (testState.status !== 'done') return null
|
|
4056
4179
|
const state = testState.result && testState.result.stealth
|
|
4057
4180
|
if (!state) return null
|
|
4058
|
-
if (state.reason
|
|
4059
|
-
|
|
4060
|
-
}
|
|
4061
|
-
if (state.active !== true || state.reason !== 'official-unavailable') return null
|
|
4062
|
-
return h('p', { className: 'vr-hint vr-stealth-notice' }, t('stealthOfficialDeadHint'))
|
|
4181
|
+
if (state.reason !== 'host-owned-official-unavailable') return null
|
|
4182
|
+
return h('p', { className: 'vr-hint vr-stealth-notice' }, t('stealthOfficialHostOwnedHint'))
|
|
4063
4183
|
}
|
|
4064
4184
|
|
|
4065
4185
|
return h('li', { className: 'vr-card' + (open ? ' vr-card-open' : '') },
|
|
@@ -4272,25 +4392,7 @@ window.__ModuleLoader__.load({
|
|
|
4272
4392
|
) : null, h('div', { className: 'vr-footer' },
|
|
4273
4393
|
h('button', {
|
|
4274
4394
|
type: 'button', className: 'vr-btn',
|
|
4275
|
-
onClick:
|
|
4276
|
-
try {
|
|
4277
|
-
const response = await fetch('/_dsh/vision-router/logs', {
|
|
4278
|
-
method: 'POST',
|
|
4279
|
-
cache: 'no-store',
|
|
4280
|
-
})
|
|
4281
|
-
const result = await response.json().catch(() => undefined)
|
|
4282
|
-
if (!response.ok || !result || result.ok !== true) {
|
|
4283
|
-
throw new Error(result && result.error ? result.error : `HTTP ${response.status}`)
|
|
4284
|
-
}
|
|
4285
|
-
} catch (error) {
|
|
4286
|
-
if (typeof window.alert === 'function') {
|
|
4287
|
-
window.alert(
|
|
4288
|
-
t('openLogFolderFailed') + ':' +
|
|
4289
|
-
(error && error.message ? error.message : String(error)),
|
|
4290
|
-
)
|
|
4291
|
-
}
|
|
4292
|
-
}
|
|
4293
|
-
},
|
|
4395
|
+
onClick: openLogFolder,
|
|
4294
4396
|
}, t('openLogFolder')),
|
|
4295
4397
|
failed ? h('p', { className: 'vr-failed', role: 'alert' },
|
|
4296
4398
|
t('saveFailed') + (failedFields.length > 0 ? `(${failedFields.join('、')})` : '')) : null,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preserve Core's synchronous apply contract while giving Promise-like Core
|
|
3
|
+
* implementations one explicit settlement boundary for post-registration work.
|
|
4
|
+
* This helper owns no runtime state; callers provide success/failure lifecycle
|
|
5
|
+
* effects explicitly.
|
|
6
|
+
*/
|
|
7
|
+
export function runCoreApplyLifecycle(invoke, { onSuccess, onFailure } = {}) {
|
|
8
|
+
if (typeof invoke !== 'function') throw new TypeError('Core apply lifecycle requires an invoke function')
|
|
9
|
+
const succeed = typeof onSuccess === 'function' ? onSuccess : () => {}
|
|
10
|
+
const fail = (error) => {
|
|
11
|
+
try {
|
|
12
|
+
onFailure?.(error)
|
|
13
|
+
} catch {
|
|
14
|
+
// Cleanup/diagnostic failure must not replace the Core apply error.
|
|
15
|
+
}
|
|
16
|
+
throw error
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let result
|
|
20
|
+
try {
|
|
21
|
+
result = invoke()
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return fail(error)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (result && typeof result.then === 'function') {
|
|
27
|
+
return Promise.resolve(result).then(
|
|
28
|
+
(value) => {
|
|
29
|
+
try {
|
|
30
|
+
succeed()
|
|
31
|
+
return value
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return fail(error)
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
fail,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
succeed()
|
|
42
|
+
return result
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return fail(error)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|