dsh-zen-remote 2.0.3 → 2.2.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/AGENTS.md +15 -1
- package/README.md +11 -5
- package/README.zh-CN.md +11 -5
- package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +15 -0
- package/docs/adr/0002-relay-emulates-websocket.md +8 -0
- package/docs/interface.md +3 -1
- package/docs/plugin-bridge.md +140 -0
- package/docs/remote-access.en.md +57 -0
- package/docs/remote-access.md +57 -0
- package/lib/adapters/dsh-better-sidebar/index.js +144 -0
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -0
- package/lib/adapters/dsh-better-sidebar/methods.js +198 -0
- package/lib/adapters/dsh-better-sidebar/methods.js.map +1 -0
- package/lib/adapters/index.js +17 -0
- package/lib/adapters/index.js.map +1 -0
- package/lib/adapters/types.js +17 -0
- package/lib/adapters/types.js.map +1 -0
- package/lib/admin-routes.js +51 -0
- package/lib/admin-routes.js.map +1 -1
- package/lib/client-routes.js +18 -1
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +449 -45
- package/lib/config.js +27 -0
- package/lib/config.js.map +1 -1
- package/lib/http.js +10 -0
- package/lib/http.js.map +1 -1
- package/lib/index.js +246 -0
- package/lib/index.js.map +1 -1
- package/lib/plugin-adapter-core.js +385 -0
- package/lib/plugin-adapter-core.js.map +1 -0
- package/lib/plugin-bridge.js +318 -0
- package/lib/plugin-bridge.js.map +1 -0
- package/lib/plugin-loopback.js +207 -0
- package/lib/plugin-loopback.js.map +1 -0
- package/lib/plugin-route-intercept.js +1103 -0
- package/lib/plugin-route-intercept.js.map +1 -0
- package/lib/plugin-tunnel.js +416 -0
- package/lib/plugin-tunnel.js.map +1 -0
- package/lib/relay-client.js +319 -2
- package/lib/relay-client.js.map +1 -1
- package/lib/relay-server.js +633 -0
- package/lib/relay-server.js.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +27 -0
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -0
- package/lib/types/adapters/dsh-better-sidebar/methods.d.ts +51 -0
- package/lib/types/adapters/dsh-better-sidebar/methods.d.ts.map +1 -0
- package/lib/types/adapters/index.d.ts +17 -0
- package/lib/types/adapters/index.d.ts.map +1 -0
- package/lib/types/adapters/types.d.ts +42 -0
- package/lib/types/adapters/types.d.ts.map +1 -0
- package/lib/types/admin-routes.d.ts +16 -0
- package/lib/types/admin-routes.d.ts.map +1 -1
- package/lib/types/client/effects/remote-session.d.ts +0 -22
- package/lib/types/client/effects/remote-session.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +16 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts.map +1 -1
- package/lib/types/client-data/plugin-bridge.d.ts +79 -0
- package/lib/types/client-data/plugin-bridge.d.ts.map +1 -0
- package/lib/types/client-data/remote-status.d.ts +20 -0
- package/lib/types/client-data/remote-status.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +37 -1
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +13 -0
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/config.d.ts +8 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/http.d.ts +7 -0
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/plugin-adapter-core.d.ts +263 -0
- package/lib/types/plugin-adapter-core.d.ts.map +1 -0
- package/lib/types/plugin-bridge.d.ts +187 -0
- package/lib/types/plugin-bridge.d.ts.map +1 -0
- package/lib/types/plugin-loopback.d.ts +104 -0
- package/lib/types/plugin-loopback.d.ts.map +1 -0
- package/lib/types/plugin-route-intercept.d.ts +114 -0
- package/lib/types/plugin-route-intercept.d.ts.map +1 -0
- package/lib/types/plugin-tunnel.d.ts +96 -0
- package/lib/types/plugin-tunnel.d.ts.map +1 -0
- package/lib/types/relay-client.d.ts +76 -0
- package/lib/types/relay-client.d.ts.map +1 -1
- package/lib/types/relay-server.d.ts +32 -1
- package/lib/types/relay-server.d.ts.map +1 -1
- package/llms.txt +1 -0
- package/package.json +2 -2
- package/scripts/check-plugin-bridge-ui.mjs +168 -0
- package/scripts/check-settings-form.mjs +3 -1
- package/src/adapters/dsh-better-sidebar/index.ts +144 -0
- package/src/adapters/dsh-better-sidebar/methods.ts +215 -0
- package/src/adapters/index.ts +19 -0
- package/src/adapters/types.ts +44 -0
- package/src/admin-routes.ts +64 -0
- package/src/client/effects/remote-session.ts +82 -10
- package/src/client/locales.ts +36 -0
- package/src/client/settings/SettingsSection.tsx +107 -3
- package/src/client/styles/remote-session.css.ts +70 -31
- package/src/client-data/plugin-bridge.ts +150 -0
- package/src/client-data/remote-status.ts +30 -0
- package/src/client-data/settings-form.ts +78 -0
- package/src/client-routes.ts +29 -1
- package/src/config.ts +32 -0
- package/src/http.ts +12 -0
- package/src/index.ts +252 -0
- package/src/plugin-adapter-core.ts +530 -0
- package/src/plugin-bridge.ts +435 -0
- package/src/plugin-loopback.ts +299 -0
- package/src/plugin-route-intercept.ts +1193 -0
- package/src/plugin-tunnel.ts +470 -0
- package/src/relay-client.ts +394 -2
- package/src/relay-server.ts +656 -1
package/src/client/locales.ts
CHANGED
|
@@ -253,6 +253,22 @@ export const zh = {
|
|
|
253
253
|
'settings.client.diagRecentFailures': '最近的远程调用失败',
|
|
254
254
|
'settings.client.diagCompatGroups': '指纹有差异的组:{groups}',
|
|
255
255
|
'settings.client.diagCompatCalls': '最近的不兼容调用',
|
|
256
|
+
// --- PB-5 插件桥:主服务端同意小节 + 子客户端诊断小节 ---
|
|
257
|
+
'settings.bridge.title': '插件桥',
|
|
258
|
+
'settings.bridge.empty': '暂无插件登记插件桥',
|
|
259
|
+
'settings.bridge.loading': '正在读取插件桥登记…',
|
|
260
|
+
'settings.bridge.loadFailed': '插件桥列表读取失败(旧版服务端或经网关打开的页面)',
|
|
261
|
+
'settings.bridge.sessionOnly': '随会话共享转发',
|
|
262
|
+
'settings.bridge.consentToggle': '允许转发整机数据',
|
|
263
|
+
'settings.bridge.consentHint': '打开后,已配对的桌面应用端可在远程会话里读取该插件的整机数据。',
|
|
264
|
+
'settings.bridge.stateBridged': '已桥接',
|
|
265
|
+
'settings.bridge.stateNotOnServer': '服务端未安装',
|
|
266
|
+
'settings.bridge.stateVersionMismatch': '版本不同(服务端 {version})',
|
|
267
|
+
'settings.bridge.stateDeclarationMismatch': '声明不一致',
|
|
268
|
+
'settings.bridge.stateConsentOff': '服务端主人未允许整机数据',
|
|
269
|
+
'settings.bridge.stateOffline': '中继离线',
|
|
270
|
+
'settings.bridge.stateServerTooOld': '服务端 zen-remote 版本过旧',
|
|
271
|
+
'settings.bridge.interceptOff': '本机改道未启用:{reason}',
|
|
256
272
|
// --- T34 子客户端远程状态:标题行图标、横幅、关闭远程 ---
|
|
257
273
|
'remoteStatusOnline': '远程会话 · 已连接',
|
|
258
274
|
'remoteStatusOffline': '服务端离线,正在重连',
|
|
@@ -269,6 +285,9 @@ export const zh = {
|
|
|
269
285
|
'remoteStatusOfflineClick': '服务端离线,无法操作',
|
|
270
286
|
'remoteStatusUnshareFail': '关闭远程失败,请稍后再试',
|
|
271
287
|
'remoteStatusUnshareFailCode': '关闭远程失败({code}),请稍后再试',
|
|
288
|
+
// --- PB-14: the unbridged better-sidebar note (a CSS variable the
|
|
289
|
+
// remote-session stylesheet spends as the empty tab's ::before copy) ---
|
|
290
|
+
'remoteBridgeUnbridgedNote': 'better-sidebar 未在远程会话桥接(服务端未装同版本或未允许),文件与改动请以会话内消息为准',
|
|
272
291
|
// 远程分组标题的后缀(后台 merge-streams 拿不到界面语言,固定用中文);
|
|
273
292
|
// 这里登记英文对照,供界面侧需要复述同样状态时使用。
|
|
274
293
|
'remoteGroupOffline': '(离线)',
|
|
@@ -524,6 +543,22 @@ export const en: Record<MobileNavKey, string> = {
|
|
|
524
543
|
'settings.client.diagRecentFailures': 'Recent remote call failures',
|
|
525
544
|
'settings.client.diagCompatGroups': 'Groups with fingerprint differences: {groups}',
|
|
526
545
|
'settings.client.diagCompatCalls': 'Recent incompatible calls',
|
|
546
|
+
// --- PB-5 plugin bridge: host consent section + sub-client diagnostics ---
|
|
547
|
+
'settings.bridge.title': 'Plugin bridge',
|
|
548
|
+
'settings.bridge.empty': 'No plugin has registered a bridge yet',
|
|
549
|
+
'settings.bridge.loading': 'Loading the bridge registrations…',
|
|
550
|
+
'settings.bridge.loadFailed': 'The bridge listing failed to load (an older server, or the page opened through the gateway)',
|
|
551
|
+
'settings.bridge.sessionOnly': 'Forwards with shared sessions',
|
|
552
|
+
'settings.bridge.consentToggle': 'Allow forwarding whole-machine data',
|
|
553
|
+
'settings.bridge.consentHint': 'When on, paired desktop apps can read this plugin\u2019s whole-machine data in remote sessions.',
|
|
554
|
+
'settings.bridge.stateBridged': 'Bridged',
|
|
555
|
+
'settings.bridge.stateNotOnServer': 'Not installed on the server',
|
|
556
|
+
'settings.bridge.stateVersionMismatch': 'Version differs (server {version})',
|
|
557
|
+
'settings.bridge.stateDeclarationMismatch': 'Declaration mismatch',
|
|
558
|
+
'settings.bridge.stateConsentOff': 'Server owner has not allowed whole-machine data',
|
|
559
|
+
'settings.bridge.stateOffline': 'Relay offline',
|
|
560
|
+
'settings.bridge.stateServerTooOld': 'Server zen-remote is too old',
|
|
561
|
+
'settings.bridge.interceptOff': 'Local rerouting not enabled: {reason}',
|
|
527
562
|
// --- T34 remote-status icon, composer banner, unshare ---
|
|
528
563
|
'remoteStatusOnline': 'Remote session · connected',
|
|
529
564
|
'remoteStatusOffline': 'Server offline — reconnecting',
|
|
@@ -540,6 +575,7 @@ export const en: Record<MobileNavKey, string> = {
|
|
|
540
575
|
'remoteStatusOfflineClick': 'The server is offline — nothing to do',
|
|
541
576
|
'remoteStatusUnshareFail': 'Closing the remote failed — try again later',
|
|
542
577
|
'remoteStatusUnshareFailCode': 'Closing the remote failed ({code}) — try again later',
|
|
578
|
+
'remoteBridgeUnbridgedNote': 'better-sidebar is not bridged for this remote session (the server lacks the same version or has not allowed it) — treat files and changes as session messages only',
|
|
543
579
|
// Remote group title suffixes (the background merger cannot know the UI
|
|
544
580
|
// language and always uses the Chinese forms); the English equivalents
|
|
545
581
|
// live here for UI surfaces that restate the same status.
|
|
@@ -35,11 +35,12 @@
|
|
|
35
35
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
|
|
36
36
|
import type { ReactNode } from 'react'
|
|
37
37
|
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
38
|
-
import { Button, SettingsForm, SettingsValueField } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
38
|
+
import { Button, SettingsForm, SettingsValueField, Switch } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
39
39
|
import type { SettingsFormLabels } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
40
40
|
import {
|
|
41
41
|
ADMIN_ACTION_ROUTE,
|
|
42
42
|
ADMIN_PAIR_ROUTE,
|
|
43
|
+
ADMIN_PLUGIN_BRIDGE_ROUTE,
|
|
43
44
|
ADMIN_PUSH_TEST_ROUTE,
|
|
44
45
|
ADMIN_STATUS_ROUTE,
|
|
45
46
|
CLIENT_CLAIM_ROUTE,
|
|
@@ -55,6 +56,8 @@ import {
|
|
|
55
56
|
normalizePairingCode,
|
|
56
57
|
shouldFollowServerDeviceName,
|
|
57
58
|
} from '../../client-data/settings-form.ts'
|
|
59
|
+
import { bridgeSectionState, togglePluginConsent } from '../../client-data/plugin-bridge.ts'
|
|
60
|
+
import type { PluginBridgeStatusView } from '../../client-data/plugin-bridge.ts'
|
|
58
61
|
import type {
|
|
59
62
|
AdminStatusBody,
|
|
60
63
|
ClaimRouteBody,
|
|
@@ -374,6 +377,40 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
374
377
|
? deriveSettingsView(load.body, { now, rowUser: config.rowUser() })
|
|
375
378
|
: undefined
|
|
376
379
|
const clientRole = config.savedRoleIsClient()
|
|
380
|
+
|
|
381
|
+
// The plugin bridge's registered-plugin listing (PB-5), host page only:
|
|
382
|
+
// loaded once per page life and refreshed whenever a save lands (the
|
|
383
|
+
// consent staged by the toggles below rides that very save). Starts in
|
|
384
|
+
// the loading state — the section renders the loading copy until the
|
|
385
|
+
// first answer lands (or the load fails, which is its own state, never
|
|
386
|
+
// the empty one); a composition without the route stays loading.
|
|
387
|
+
// PB-9-fix: the listing is a THREE-state read — loading, failed (an old
|
|
388
|
+
// build, or the page opened through the gateway where the listing is
|
|
389
|
+
// refused), or an answer that carries zero rows. Only the third means
|
|
390
|
+
// "nothing registered"; the first two must not masquerade as it.
|
|
391
|
+
const [bridgeLoad, setBridgeLoad] = useState<{ state: 'loading' | 'error' | 'ready', rows: { plugin: string, version: string, hasServerScope: boolean, consent: boolean }[] }>({ state: 'loading', rows: [] })
|
|
392
|
+
const loadBridge = useCallback(() => {
|
|
393
|
+
setBridgeLoad((prev) => ({ state: 'loading', rows: prev.rows }))
|
|
394
|
+
fetch(ADMIN_PLUGIN_BRIDGE_ROUTE)
|
|
395
|
+
.then((res) => {
|
|
396
|
+
if (!res.ok) return Promise.reject(new Error(String(res.status)))
|
|
397
|
+
return res.json() as Promise<{ value?: { plugins?: unknown } }>
|
|
398
|
+
})
|
|
399
|
+
.then((body) => {
|
|
400
|
+
const plugins = body?.value?.plugins
|
|
401
|
+
if (!Array.isArray(plugins)) return
|
|
402
|
+
setBridgeLoad({
|
|
403
|
+
state: 'ready',
|
|
404
|
+
rows: plugins.filter((row): row is { plugin: string, version: string, hasServerScope: boolean, consent: boolean } =>
|
|
405
|
+
row !== null && typeof row === 'object' && typeof (row as { plugin?: unknown }).plugin === 'string'),
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
.catch(() => { setBridgeLoad((prev) => ({ state: 'error', rows: prev.rows })) })
|
|
409
|
+
}, [])
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
if (!clientRole) loadBridge()
|
|
412
|
+
}, [clientRole, loadBridge])
|
|
413
|
+
const bridgeState = bridgeSectionState(bridgeLoad.state, bridgeLoad.rows)
|
|
377
414
|
const statusPoll = config.statusPoll()
|
|
378
415
|
|
|
379
416
|
useEffect(() => {
|
|
@@ -817,6 +854,9 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
817
854
|
else setClientLoad({ state: 'loading' })
|
|
818
855
|
const refresh = config.savedRoleIsClient() ? loadClientStatus : loadStatus
|
|
819
856
|
refresh()
|
|
857
|
+
// The plugin bridge listing re-reads too (PB-5): the consent toggles
|
|
858
|
+
// stage through the very save that just landed.
|
|
859
|
+
if (!config.savedRoleIsClient()) loadBridge()
|
|
820
860
|
// Immediate pull + 1.5s covers a hot-only save. A save that reloads the
|
|
821
861
|
// plugin row (T17) restarts the row and the gateway child with it — the
|
|
822
862
|
// reload and the gateway's rebinding of its port both take longer than
|
|
@@ -901,6 +941,49 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
901
941
|
<h3 className="zr-settings-card-title">{t('settings.shareTitle')}</h3>
|
|
902
942
|
{text(form.idleHours, 'idleHours', t('settings.fieldIdleHours'), t('settings.fieldIdleHoursHint'), t('settings.invalidHours'), true)}
|
|
903
943
|
{bool(form.autoShareNewSessions, 'autoShareNewSessions', t('settings.fieldAutoShare'), t('settings.fieldAutoShareHint'))}
|
|
944
|
+
|
|
945
|
+
{/* 插件桥 (PB-5): the consent toggles stage the NEXT consent map
|
|
946
|
+
over the SAME form — one save button, one revision fence —
|
|
947
|
+
reading the CURRENT map off the staged (or displayed) draft
|
|
948
|
+
so consecutive toggles compose. */}
|
|
949
|
+
<h3 className="zr-settings-card-title">{t('settings.bridge.title')}</h3>
|
|
950
|
+
{bridgeState === 'loading' && <p className="zr-settings-hint">{t('settings.bridge.loading')}</p>}
|
|
951
|
+
{bridgeState === 'error' && <p className="zr-settings-hint" data-invalid="true">{t('settings.bridge.loadFailed')}</p>}
|
|
952
|
+
{bridgeState === 'empty' && <p className="zr-settings-hint">{t('settings.bridge.empty')}</p>}
|
|
953
|
+
{bridgeLoad.rows.map((row) => {
|
|
954
|
+
let current: Record<string, boolean> = {}
|
|
955
|
+
try {
|
|
956
|
+
const parsed: unknown = JSON.parse(form.pluginBridgeConsent.text)
|
|
957
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
958
|
+
current = parsed as Record<string, boolean>
|
|
959
|
+
}
|
|
960
|
+
} catch { /* a malformed draft reads as nothing consented */ }
|
|
961
|
+
return (
|
|
962
|
+
<div className="zr-settings-field" key={row.plugin}>
|
|
963
|
+
<div className="zr-settings-head">
|
|
964
|
+
<span className="zr-settings-head-title">{row.plugin}</span>
|
|
965
|
+
<span className="zr-settings-badge">{row.version}</span>
|
|
966
|
+
</div>
|
|
967
|
+
{row.hasServerScope ? (
|
|
968
|
+
<>
|
|
969
|
+
<div className="zr-settings-head">
|
|
970
|
+
<label>{t('settings.bridge.consentToggle')}</label>
|
|
971
|
+
<Switch
|
|
972
|
+
checked={current[row.plugin] === true}
|
|
973
|
+
onChange={(checked) => {
|
|
974
|
+
config.stage('pluginBridgeConsent', JSON.stringify(togglePluginConsent(current, row.plugin, checked)))
|
|
975
|
+
}}
|
|
976
|
+
label={t('settings.bridge.consentToggle')}
|
|
977
|
+
/>
|
|
978
|
+
</div>
|
|
979
|
+
<p className="zr-settings-hint">{t('settings.bridge.consentHint')}</p>
|
|
980
|
+
</>
|
|
981
|
+
) : (
|
|
982
|
+
<p className="zr-settings-hint">{t('settings.bridge.sessionOnly')}</p>
|
|
983
|
+
)}
|
|
984
|
+
</div>
|
|
985
|
+
)
|
|
986
|
+
})}
|
|
904
987
|
</>
|
|
905
988
|
)}
|
|
906
989
|
</SettingsForm>
|
|
@@ -1073,8 +1156,9 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
1073
1156
|
client/status body. The interceptor (T23b) and compat (T42)
|
|
1074
1157
|
groups render only when the response carried their fields —
|
|
1075
1158
|
earlier servers answer neither, and the section itself only
|
|
1076
|
-
exists when there is at least one line to show.
|
|
1077
|
-
|
|
1159
|
+
exists when there is at least one line to show. PB-5 adds the
|
|
1160
|
+
bridge rows and the rerouting note on the same presence gate. */}
|
|
1161
|
+
{clientView !== undefined && (clientView.lastError !== '' || clientView.intercept !== undefined || clientView.compat !== undefined || (clientView.pluginBridge?.length ?? 0) > 0 || clientView.pluginRouteIntercept !== undefined) && (
|
|
1078
1162
|
<>
|
|
1079
1163
|
<h3 className="zr-settings-card-title">{t('settings.client.diagTitle')}</h3>
|
|
1080
1164
|
{clientView.lastError !== '' && (
|
|
@@ -1132,6 +1216,26 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
1132
1216
|
)}
|
|
1133
1217
|
</>
|
|
1134
1218
|
)}
|
|
1219
|
+
{(clientView.pluginBridge?.length ?? 0) > 0 && clientView.pluginBridge !== undefined && (
|
|
1220
|
+
<div className="zr-settings-field">
|
|
1221
|
+
<div className="zr-settings-head">
|
|
1222
|
+
<label>{t('settings.bridge.title')}</label>
|
|
1223
|
+
</div>
|
|
1224
|
+
{clientView.pluginBridge.map((row: PluginBridgeStatusView) => (
|
|
1225
|
+
<p key={row.plugin} className="zr-settings-hint">
|
|
1226
|
+
{row.plugin} · {row.localVersion !== '' ? `v${row.localVersion}` : '—'} ·{' '}
|
|
1227
|
+
{row.stateKey === 'settings.bridge.stateVersionMismatch'
|
|
1228
|
+
? t(row.stateKey, { version: row.serverVersion ?? '?' })
|
|
1229
|
+
: t(row.stateKey)}
|
|
1230
|
+
</p>
|
|
1231
|
+
))}
|
|
1232
|
+
</div>
|
|
1233
|
+
)}
|
|
1234
|
+
{clientView.pluginRouteIntercept !== undefined && !clientView.pluginRouteIntercept.installed && (
|
|
1235
|
+
<p className="zr-settings-hint" data-invalid="true">
|
|
1236
|
+
{t('settings.bridge.interceptOff', { reason: clientView.pluginRouteIntercept.reasons.join('; ') || '—' })}
|
|
1237
|
+
</p>
|
|
1238
|
+
)}
|
|
1135
1239
|
</>
|
|
1136
1240
|
)}
|
|
1137
1241
|
</div>
|
|
@@ -32,17 +32,23 @@ html[data-zr-remote-session="1"] [data-open-target] {
|
|
|
32
32
|
display: none !important;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/* ---------- remote session: hide dsh-better-sidebar's file surfaces (T53) ----------
|
|
36
|
-
dsh-better-sidebar 0.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
35
|
+
/* ---------- remote session: hide dsh-better-sidebar's file surfaces (T53, two regimes since PB-14) ----------
|
|
36
|
+
dsh-better-sidebar 0.25.0 is the second big "open the machine's files" UI
|
|
37
|
+
AND, since PB-12/13, the first ADAPTER-BRIDGED one: at exactly 0.25.0 its
|
|
38
|
+
/sidebar/api routes, its two WebSockets and its side chat cross the byte
|
|
39
|
+
tunnel and read/write the SERVER's data, gated on the whole-machine-data
|
|
40
|
+
consent. The hiding below therefore applies ONLY while the plugin is NOT
|
|
41
|
+
bridged — the :not([data-zr-bridge-dsh-better-sidebar="1"]) stamp the
|
|
42
|
+
remote-status poll writes; see the PB-14 note above): bridged, the panels
|
|
43
|
+
are remote-aware and stay visible; unbridged (a different version, the
|
|
44
|
+
server without the adapter's consent), every tab it registers (files/
|
|
45
|
+
editor, git changes, subagent/tasks, side chat, the markdown/html/code
|
|
46
|
+
file viewers) builds its requests from a {sessionId, cwd} scope and POSTs
|
|
47
|
+
them to the plugin's own /sidebar/api/* routes on whichever machine
|
|
48
|
+
answers — in a remote session that is the SUB-CLIENT's machine (client.js
|
|
49
|
+
call() ~3111, fetchUpload ~3162, encodeHtmlUrl /sidebar/html/ ~3069-3414),
|
|
50
|
+
so the tree would show same-named LOCAL files as if they were the
|
|
51
|
+
session's and an edit would WRITE locally.
|
|
46
52
|
|
|
47
53
|
Anchors, all the plugin's or the host's own rendered contracts (no hashed
|
|
48
54
|
class names; the one suffix selector, _chipIcon, is unique among the
|
|
@@ -51,18 +57,21 @@ html[data-zr-remote-session="1"] [data-open-target] {
|
|
|
51
57
|
@liustack/modsearch / @wxg-prc-cpg/browser-skill-dsh-plugin (modsearch /
|
|
52
58
|
browser-skill) / dshmarket and every @deepseek-ai sidebar package for it):
|
|
53
59
|
|
|
54
|
-
HONEST LIMITS (T53-fix
|
|
60
|
+
HONEST LIMITS (T53-fix, adjusted PB-14): unbridged, the plugin's
|
|
55
61
|
components stay mounted and keep silently POSTing READ-ONLY /sidebar/api
|
|
56
62
|
requests to the local machine (session.cwd, changes.ops, the tree reads a
|
|
57
63
|
mounted tab refreshes with) — the data never leaves that machine, and no
|
|
58
|
-
write can happen because every write control is inside the hidden
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
write can happen because every write control is inside the hidden
|
|
65
|
+
surfaces (the tab body hides its CHILDREN and spends the note copy as a
|
|
66
|
+
::before, so an already-open tab reads WHY it is empty instead of
|
|
67
|
+
sitting blank). Known boundary: a dsh-resource://file/** address opened
|
|
68
|
+
through sidebarRight.openResource in a remote session (file links in
|
|
69
|
+
chat, deliverables, @-references, skills) is CLAIMED by the plugin's
|
|
70
|
+
editor registration (client.js ~21377: patterns ["dsh-resource://file/**"])
|
|
71
|
+
before the host preview sees it — the right sidebar expands, the rules
|
|
72
|
+
below hide the tab's body content and chip, and the note fills the panel.
|
|
73
|
+
Bridged (0.25.0 + consent), none of this applies: the tab reads the
|
|
74
|
+
server's data through the adapter and stays visible whole.
|
|
66
75
|
|
|
67
76
|
- [data-dsh-bottom-toggle] — the header-utilities button that expands the
|
|
68
77
|
bottom workbench (client.js BottomDockToggle ~20863; slot registration
|
|
@@ -135,31 +144,61 @@ html[data-zr-remote-session="1"] [data-open-target] {
|
|
|
135
144
|
attribute and this rule hides it too (cosmetic; the page stays reachable
|
|
136
145
|
through its shortcut command path and the plugin's takeover is the
|
|
137
146
|
"files" implementation on every deployment this plugin pairs with
|
|
138
|
-
better-sidebar anyway).
|
|
139
|
-
|
|
147
|
+
better-sidebar anyway).
|
|
148
|
+
|
|
149
|
+
/* PB-14: every T53 rule below carries
|
|
150
|
+
:not([data-zr-bridge-dsh-better-sidebar="1"]) — when the adapter
|
|
151
|
+
(PB-12) bridges the plugin, the panels read the SERVER's data through
|
|
152
|
+
the byte tunnel and MUST stay visible; the hiding applies only to the
|
|
153
|
+
unbridged combination, and the empty tab body then carries a note
|
|
154
|
+
instead of blankness. The stamp is written by
|
|
155
|
+
effects/remote-session.ts off the remote-status poll's bridgedPlugins
|
|
156
|
+
list (client-data/remote-status.ts shouldStampBridgeAttribute). */
|
|
157
|
+
html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-dsh-bottom-toggle] {
|
|
140
158
|
display: none !important;
|
|
141
159
|
}
|
|
142
|
-
html[data-zr-remote-session="1"] [data-dsh-panel-host] {
|
|
160
|
+
html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-dsh-panel-host] {
|
|
143
161
|
display: none !important;
|
|
144
162
|
}
|
|
145
|
-
html[data-zr-remote-session="1"] {
|
|
163
|
+
html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) {
|
|
146
164
|
--dsh-sidebar-height: 0px !important;
|
|
147
165
|
}
|
|
148
|
-
|
|
166
|
+
/* The unbridged tab body: hide the plugin's own content and spend the
|
|
167
|
+
note copy as a ::before — an already-open tab reads WHY it is empty
|
|
168
|
+
(0.25.0 keeps tabs mounted, so a stale body would otherwise sit blank).
|
|
169
|
+
--zr-bridge-note is set on <html> by the stamp effect, in the
|
|
170
|
+
browser's language; unset (effect not running yet) the content falls
|
|
171
|
+
through and nothing renders. */
|
|
172
|
+
html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-dsh-native-tab-host] > * {
|
|
173
|
+
display: none !important;
|
|
174
|
+
}
|
|
175
|
+
html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-dsh-native-tab-host]::before {
|
|
176
|
+
content: var(--zr-bridge-note);
|
|
177
|
+
display: block;
|
|
178
|
+
padding: 24px 16px;
|
|
179
|
+
font-size: 13px;
|
|
180
|
+
line-height: 1.7;
|
|
181
|
+
color: var(--dsh-fg-muted, #6b7280);
|
|
182
|
+
word-break: break-word;
|
|
183
|
+
}
|
|
184
|
+
html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-dockkit-tab]:has([class$="_chipIcon"]) {
|
|
149
185
|
display: none !important;
|
|
150
186
|
}
|
|
151
|
-
html[data-zr-remote-session="1"] [data-
|
|
187
|
+
html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-floating-window]:has([data-window-body]) {
|
|
152
188
|
display: none !important;
|
|
153
189
|
}
|
|
154
|
-
html[data-zr-remote-session="1"]
|
|
190
|
+
html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-sidebar-right-guide-entry="git"],
|
|
191
|
+
html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-sidebar-right-guide-entry="subagent"],
|
|
192
|
+
html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-sidebar-right-guide-entry="sidechat"] {
|
|
155
193
|
display: none !important;
|
|
156
194
|
}
|
|
157
|
-
html[data-zr-remote-session="1"] [data-sidebar-right-guide-entry="
|
|
158
|
-
html[data-zr-remote-session="1"] [data-sidebar-right-guide-entry="subagent"],
|
|
159
|
-
html[data-zr-remote-session="1"] [data-sidebar-right-guide-entry="sidechat"] {
|
|
195
|
+
html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-sidebar-right-guide-entry="files"]:not([aria-keyshortcuts]) {
|
|
160
196
|
display: none !important;
|
|
161
197
|
}
|
|
162
|
-
|
|
198
|
+
/* 0.25.0's dock fallback — the floating quick-open button the plugin pins
|
|
199
|
+
for empty workspaces. Same treatment as every other open-the-machine's-
|
|
200
|
+
files control: hidden while unbridged, untouched when bridged. */
|
|
201
|
+
html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-dsh-dock-fallback] {
|
|
163
202
|
display: none !important;
|
|
164
203
|
}
|
|
165
204
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin bridge's settings-page pure pieces (PB-5): the status → copy
|
|
3
|
+
* mapping the 子客户端诊断区 renders, and the consent-toggle arithmetic the
|
|
4
|
+
* 主服务端插件桥 section stages. Pure by the settings-form discipline — no
|
|
5
|
+
* fetch, no clock, no context — so scripts/check-plugin-bridge-ui.mjs pins
|
|
6
|
+
* every copy and every toggle shape.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately import-free: this file rides the BROWSER bundle, and the
|
|
9
|
+
* bridge's own module (src/plugin-bridge.ts) drags node:crypto in. The
|
|
10
|
+
* status shape is therefore restated structurally here — it mirrors
|
|
11
|
+
* lib/plugin-bridge.js's `BridgeStatus` verbatim, and a drifted wire is
|
|
12
|
+
* handled by the tolerant mapping, not by the type system.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** One plugin's bridge verdict, exactly the shape `client/status`'s
|
|
16
|
+
* `pluginBridge` array carries (PB-2). */
|
|
17
|
+
export interface PluginBridgeStatusLike {
|
|
18
|
+
plugin: string
|
|
19
|
+
bridged: boolean
|
|
20
|
+
reason?: string
|
|
21
|
+
localVersion: string
|
|
22
|
+
serverVersion?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One row of the 诊断区's 插件桥 group: the plugin, its LOCAL version, and
|
|
26
|
+
* the copy key (plus the interpolation value) the locale renders. */
|
|
27
|
+
export interface PluginBridgeStatusView {
|
|
28
|
+
plugin: string
|
|
29
|
+
localVersion: string
|
|
30
|
+
/** The locale key under `settings.bridge.state*`. */
|
|
31
|
+
stateKey: PluginBridgeStateKey
|
|
32
|
+
/** The server's version, when the verdict names one (版本不同's copy). */
|
|
33
|
+
serverVersion: string | undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type PluginBridgeStateKey =
|
|
37
|
+
| 'settings.bridge.stateBridged'
|
|
38
|
+
| 'settings.bridge.stateNotOnServer'
|
|
39
|
+
| 'settings.bridge.stateVersionMismatch'
|
|
40
|
+
| 'settings.bridge.stateDeclarationMismatch'
|
|
41
|
+
| 'settings.bridge.stateConsentOff'
|
|
42
|
+
| 'settings.bridge.stateOffline'
|
|
43
|
+
| 'settings.bridge.stateServerTooOld'
|
|
44
|
+
|
|
45
|
+
/** The reason → copy-key table, one entry per BridgeReason plus the
|
|
46
|
+
* everything-else fallback (an unknown reason from a newer server reads as
|
|
47
|
+
* 未桥接-generic — the offline copy, honest without inventing a claim). */
|
|
48
|
+
const REASON_KEYS: Readonly<Record<string, PluginBridgeStateKey>> = {
|
|
49
|
+
'not-on-server': 'settings.bridge.stateNotOnServer',
|
|
50
|
+
'version-mismatch': 'settings.bridge.stateVersionMismatch',
|
|
51
|
+
'declaration-mismatch': 'settings.bridge.stateDeclarationMismatch',
|
|
52
|
+
'consent-off': 'settings.bridge.stateConsentOff',
|
|
53
|
+
offline: 'settings.bridge.stateOffline',
|
|
54
|
+
'server-too-old': 'settings.bridge.stateServerTooOld',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Tolerant read of one `pluginBridge` wire array into the render rows,
|
|
58
|
+
* input order kept (the route already sorts by plugin name). A malformed
|
|
59
|
+
* entry is skipped, never thrown into the settings page. */
|
|
60
|
+
export function derivePluginBridgeStatuses(value: unknown): PluginBridgeStatusView[] {
|
|
61
|
+
if (!Array.isArray(value)) return []
|
|
62
|
+
const rows: PluginBridgeStatusView[] = []
|
|
63
|
+
for (const entry of value) {
|
|
64
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) continue
|
|
65
|
+
const record = entry as Record<string, unknown>
|
|
66
|
+
if (typeof record.plugin !== 'string' || record.plugin === '') continue
|
|
67
|
+
const bridged = record.bridged === true
|
|
68
|
+
const reason = typeof record.reason === 'string' ? record.reason : undefined
|
|
69
|
+
rows.push({
|
|
70
|
+
plugin: record.plugin,
|
|
71
|
+
localVersion: typeof record.localVersion === 'string' ? record.localVersion : '',
|
|
72
|
+
stateKey: bridged
|
|
73
|
+
? 'settings.bridge.stateBridged'
|
|
74
|
+
: reason !== undefined && REASON_KEYS[reason] !== undefined
|
|
75
|
+
? REASON_KEYS[reason]
|
|
76
|
+
: 'settings.bridge.stateOffline',
|
|
77
|
+
serverVersion: typeof record.serverVersion === 'string' ? record.serverVersion : undefined,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
return rows
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The three-state resolution for the host settings page's 插件桥 list
|
|
85
|
+
* (PB-9-fix): a fetch that has not answered is LOADING, a failed or
|
|
86
|
+
* refused fetch is an ERROR — and only a successful answer with zero rows
|
|
87
|
+
* means "no plugin has registered a bridge". Collapsing the first two into
|
|
88
|
+
* the empty copy would tell the user nothing is bridged while the answer is
|
|
89
|
+
* simply not in yet (or the page was opened through the gateway, where the
|
|
90
|
+
* listing is refused). Pure so the check pins all four outcomes.
|
|
91
|
+
*/
|
|
92
|
+
export function bridgeSectionState(load: 'loading' | 'error' | 'ready', rows: unknown): 'loading' | 'error' | 'empty' | 'plugins' {
|
|
93
|
+
if (load === 'loading') return 'loading'
|
|
94
|
+
if (load === 'error') return 'error'
|
|
95
|
+
return Array.isArray(rows) && rows.length > 0 ? 'plugins' : 'empty'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One consent-toggle click: the NEXT whole-machine-data consent map, staged
|
|
100
|
+
* over the row's `pluginBridgeConsent` field. Only `plugin`'s key moves —
|
|
101
|
+
* every other key (including entries for plugins no longer registered; the
|
|
102
|
+
* settings page deliberately never prunes what it does not render) is
|
|
103
|
+
* carried over verbatim, in its existing key order. An illegal current map
|
|
104
|
+
* (a hand-edited row) reads as the empty map: the toggle then writes exactly
|
|
105
|
+
* one key, and the junk is left for the row validator to keep refusing.
|
|
106
|
+
*/
|
|
107
|
+
export function togglePluginConsent(current: unknown, plugin: string, allow: boolean): Record<string, boolean> {
|
|
108
|
+
const next: Record<string, boolean> = {}
|
|
109
|
+
if (current !== null && typeof current === 'object' && !Array.isArray(current)) {
|
|
110
|
+
for (const [key, value] of Object.entries(current as Record<string, unknown>)) {
|
|
111
|
+
if (typeof value === 'boolean') next[key] = value
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
next[plugin] = allow
|
|
115
|
+
return next
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** One row of the host settings page's 插件桥 list, as the admin listing
|
|
119
|
+
* answers it (PB-5: plugin/version/hasServerScope/consent; PB-11 adds the
|
|
120
|
+
* adapter mark). */
|
|
121
|
+
export interface AdminBridgeRowView {
|
|
122
|
+
plugin: string
|
|
123
|
+
version: string
|
|
124
|
+
hasServerScope: boolean
|
|
125
|
+
consent: boolean
|
|
126
|
+
/** True when zen-remote's own adapter stands in for this plugin (the
|
|
127
|
+
* row renders「由 zen-remote 适配」); a wiring that predates adapters
|
|
128
|
+
* never sets it. */
|
|
129
|
+
adapted: boolean
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Tolerant read of one admin listing row into the render shape (PB-11).
|
|
134
|
+
* A malformed row is `undefined`, never a throw — the settings page skips
|
|
135
|
+
* it the same way {@link derivePluginBridgeStatuses} skips a bad verdict.
|
|
136
|
+
* `adapted` defaults to false: an older server's answer simply reads as
|
|
137
|
+
* "nothing is adapted".
|
|
138
|
+
*/
|
|
139
|
+
export function readAdminBridgeRow(row: unknown): AdminBridgeRowView | undefined {
|
|
140
|
+
if (row === null || typeof row !== 'object' || Array.isArray(row)) return undefined
|
|
141
|
+
const record = row as Record<string, unknown>
|
|
142
|
+
if (typeof record.plugin !== 'string' || record.plugin === '') return undefined
|
|
143
|
+
return {
|
|
144
|
+
plugin: record.plugin,
|
|
145
|
+
version: typeof record.version === 'string' ? record.version : '',
|
|
146
|
+
hasServerScope: record.hasServerScope === true,
|
|
147
|
+
consent: record.consent === true,
|
|
148
|
+
adapted: record.adapted === true,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -51,6 +51,11 @@ export interface RemoteStatusView {
|
|
|
51
51
|
versionMismatch: boolean
|
|
52
52
|
serverName: string
|
|
53
53
|
closed: Readonly<Record<string, RemoteClosedReason>>
|
|
54
|
+
/** The plugins whose bridge verdict is bridged RIGHT NOW (PB-2; sorted
|
|
55
|
+
* server-side). A garbage or missing field degrades to the empty list —
|
|
56
|
+
* nothing is ever considered bridged on a shape the parse does not
|
|
57
|
+
* trust. */
|
|
58
|
+
bridgedPlugins: readonly string[]
|
|
54
59
|
/** When this view was parsed off a GET body. */
|
|
55
60
|
asOf: number
|
|
56
61
|
}
|
|
@@ -80,10 +85,35 @@ export function parseRemoteStatusBody(body: unknown, now: number): RemoteStatusV
|
|
|
80
85
|
versionMismatch: record.versionMismatch === true,
|
|
81
86
|
serverName: typeof record.serverName === 'string' ? record.serverName : '',
|
|
82
87
|
closed,
|
|
88
|
+
bridgedPlugins: Array.isArray(record.bridgedPlugins)
|
|
89
|
+
? record.bridgedPlugins.filter((name): name is string => typeof name === 'string' && name !== '')
|
|
90
|
+
: [],
|
|
83
91
|
asOf: now,
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
94
|
|
|
95
|
+
// --- the bridge stamp (PB-14) -----------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether ONE plugin's bridge verdict is bridged RIGHT NOW, judged off the
|
|
99
|
+
* `bridgedPlugins` list the remote-status body carries. Tolerant by the
|
|
100
|
+
* same discipline as the parse: a garbage list (an older server that never
|
|
101
|
+
* learned the field, a broken body) reads as "not bridged" — the hiding
|
|
102
|
+
* rules stay in force unless the server positively says otherwise, never
|
|
103
|
+
* the reverse. This is the pure core behind the
|
|
104
|
+
* `html[data-zr-bridge-<plugin>]` stamp the remote-session stylesheet
|
|
105
|
+
* spends.
|
|
106
|
+
*/
|
|
107
|
+
export function shouldStampBridgeAttribute(bridgedPlugins: unknown, plugin: string): boolean {
|
|
108
|
+
return Array.isArray(bridgedPlugins) && bridgedPlugins.includes(plugin)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The plugin the bridge stamp names. */
|
|
112
|
+
export const BRIDGE_STAMP_PLUGIN = 'dsh-better-sidebar'
|
|
113
|
+
|
|
114
|
+
/** The html attribute the stamp writes (`data-zr-bridge-dsh-better-sidebar`). */
|
|
115
|
+
export const BRIDGE_STAMP_ATTRIBUTE = `data-zr-bridge-${BRIDGE_STAMP_PLUGIN}`
|
|
116
|
+
|
|
87
117
|
// --- the icon's states -------------------------------------------------------
|
|
88
118
|
|
|
89
119
|
/** The locale keys `describeRemoteStatus` needs. */
|