@linxin666/dsh-remote-web-ui 0.4.2 → 0.4.4
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.i18n.yaml +2 -2
- package/README.md +12 -10
- package/README.zh.md +12 -10
- package/lib/client.js +278 -599
- package/lib/client.js.map +1 -1
- package/lib/index.js +396 -913
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +8 -9
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +1 -1
- package/lib/types/client/index.d.ts +32 -3
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +51 -8
- package/lib/types/client/locales.d.ts +0 -84
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +0 -84
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +142 -8
- package/lib/types/client/settings-entry-form.d.ts +50 -0
- package/lib/types/client/settings-entry-form.d.ts.map +1 -0
- package/lib/types/client/settings-entry-form.js +98 -0
- package/lib/types/index.d.ts +99 -7
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +150 -149
- package/lib/types/lan-bind-plan.d.ts +14 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -1
- package/lib/types/lan-bind-plan.js +16 -0
- package/lib/types/lan-bind.d.ts +1 -1
- package/lib/types/lan-bind.js +1 -1
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/mount-once.d.ts.map +1 -1
- package/lib/types/mount-once.js +96 -12
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +43 -0
- package/lib/types/remote-api.d.ts +13 -1
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +14 -1
- package/lib/types/tunnel.d.ts +60 -3
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +136 -3
- package/package.json +20 -20
- package/src/client/RemoteEntry.tsx +7 -9
- package/src/client/RemoteSettingsCard.tsx +0 -2
- package/src/client/index.ts +53 -9
- package/src/client/locales.ts +0 -84
- package/src/client/mobile-adapt.ts +144 -5
- package/src/client/remote.module.css +50 -200
- package/src/client/settings-entry-form.ts +142 -0
- package/src/index.ts +202 -164
- package/src/lan-bind-plan.ts +22 -0
- package/src/lan-bind.ts +1 -1
- package/src/mount-once.ts +107 -13
- package/src/pairing.ts +44 -1
- package/src/remote-api.ts +14 -1
- package/src/tunnel.ts +142 -3
- package/lib/types/client/UpdateEntry.d.ts +0 -14
- package/lib/types/client/UpdateEntry.d.ts.map +0 -1
- package/lib/types/client/UpdateEntry.js +0 -112
- package/lib/types/client/UpdatePanel.d.ts +0 -42
- package/lib/types/client/UpdatePanel.d.ts.map +0 -1
- package/lib/types/client/UpdatePanel.js +0 -101
- package/lib/types/client/update-api.d.ts +0 -29
- package/lib/types/client/update-api.d.ts.map +0 -1
- package/lib/types/client/update-api.js +0 -47
- package/lib/types/console-output.d.ts +0 -41
- package/lib/types/console-output.d.ts.map +0 -1
- package/lib/types/console-output.js +0 -89
- package/lib/types/update-routes.d.ts +0 -30
- package/lib/types/update-routes.d.ts.map +0 -1
- package/lib/types/update-routes.js +0 -47
- package/lib/types/update.d.ts +0 -282
- package/lib/types/update.d.ts.map +0 -1
- package/lib/types/update.js +0 -773
- package/src/client/UpdateEntry.tsx +0 -144
- package/src/client/UpdatePanel.tsx +0 -256
- package/src/client/update-api.ts +0 -49
- package/src/console-output.ts +0 -97
- package/src/update-routes.ts +0 -65
- package/src/update.ts +0 -915
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Scan-to-pair remote access for the dsh web GUI that shares one official interface: a QR beside the settings button pairs phones and PCs into the same Web GUI (a portrait-touch adaptation layer for phones, full desktop on PCs) through one-time tokens and revocable device sessions, with a LAN bind toggle, optional Cloudflare tunnel, and one-click family self-update",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.4",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"license": "Apache-2.0",
|
|
39
39
|
"dsh": {
|
|
40
40
|
"engines": {
|
|
41
|
-
"dsh": ">=0.
|
|
41
|
+
"dsh": ">=0.2.0-rc.1"
|
|
42
42
|
},
|
|
43
43
|
"bundle": {
|
|
44
44
|
"patch": "./cordis.patch.yml"
|
|
@@ -53,36 +53,36 @@
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
56
57
|
"cloudflared": "^0.7.3",
|
|
57
58
|
"clsx": "^2.1.1",
|
|
58
59
|
"qrcode.react": "^4.2.0",
|
|
59
|
-
"schemastery": "^3.18.0",
|
|
60
60
|
"yaml": "^2.8.2",
|
|
61
61
|
"zod": "^4.4.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@deepseek-ai/dsh": ">=0.
|
|
64
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.1",
|
|
65
65
|
"react": "^18.2.0",
|
|
66
66
|
"react-dom": "^18.2.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
70
|
-
"@deepseek-ai/dsh-agent": "^0.
|
|
71
|
-
"@deepseek-ai/dsh-api-remotes": "^0.
|
|
72
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.
|
|
73
|
-
"@deepseek-ai/dsh-api-workspace-controller": "^0.
|
|
74
|
-
"@deepseek-ai/dsh-client-connection": "^0.
|
|
75
|
-
"@deepseek-ai/dsh-client-locale": "^0.
|
|
76
|
-
"@deepseek-ai/dsh-client-store": "^0.
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.
|
|
78
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.
|
|
79
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.
|
|
80
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.
|
|
81
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.
|
|
82
|
-
"@deepseek-ai/dsh-commands": "^0.
|
|
83
|
-
"@deepseek-ai/dsh-host-webserver": "^0.
|
|
84
|
-
"@deepseek-ai/dsh-invariants": "^0.
|
|
85
|
-
"@deepseek-ai/dsh-settings": "^0.
|
|
70
|
+
"@deepseek-ai/dsh-agent": "^0.2.0-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.1",
|
|
72
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.2.0-rc.1",
|
|
73
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.2.0-rc.1",
|
|
74
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.0-rc.1",
|
|
75
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.1",
|
|
76
|
+
"@deepseek-ai/dsh-client-store": "^0.2.0-rc.1",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.1",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.1",
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.1",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.2.0-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-commands": "^0.2.0-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-invariants": "^0.2.0-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.1",
|
|
86
86
|
"@testing-library/dom": "^10.4.1",
|
|
87
87
|
"@testing-library/react": "^16.3.2",
|
|
88
88
|
"@types/node": "^22.20.0",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The sidebar remote-control seat: the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* The sidebar remote-control seat: the phone-icon trigger beside the
|
|
3
|
+
* settings button, and the pairing panel modal. Owns the panel behavior —
|
|
4
|
+
* token minting on open, the status SSE subscription, stop/refresh/copy — and
|
|
5
|
+
* renders the pure {@link RemotePanel} body. The family self-update seat is
|
|
6
|
+
* its own plugin (dsh-update) and no longer rides this row. Component-local
|
|
7
|
+
* state per the client stack rules: nothing here survives remounts or crosses
|
|
8
|
+
* entries.
|
|
9
9
|
*/
|
|
10
10
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
11
11
|
import { createPortal } from 'react-dom'
|
|
@@ -14,7 +14,6 @@ import type { PairingPhase } from '../pairing.ts'
|
|
|
14
14
|
import { RemotePanel, type PanelState } from './RemotePanel.tsx'
|
|
15
15
|
import { copyText, issuePair, revokePair, stopPair, type DeviceFrame, type IssueResponse, type PairStateFrame, type RelayStatusFrame, type TunnelStatusFrame } from './pair-api.ts'
|
|
16
16
|
import { PhoneIcon } from './PhoneIcon.tsx'
|
|
17
|
-
import { UpdateEntry } from './UpdateEntry.tsx'
|
|
18
17
|
import css from './remote.module.css'
|
|
19
18
|
|
|
20
19
|
/** Entry props: the sidebar column state and the standard locale seat. */
|
|
@@ -233,7 +232,6 @@ export function RemoteEntry({ wide, t }: RemoteEntryProps) {
|
|
|
233
232
|
return (
|
|
234
233
|
<>
|
|
235
234
|
<div className={css.entryRow} data-rail={wide ? undefined : 'rail'}>
|
|
236
|
-
<UpdateEntry wide={wide} t={t} />
|
|
237
235
|
<TooltipAnchor wide={wide} label={t('entry.label')} onClick={openPanel} expanded={open} />
|
|
238
236
|
</div>
|
|
239
237
|
{open && createPortal((
|
|
@@ -175,8 +175,6 @@ export function RemoteSettingsCard(props: RemoteSettingsCardProps) {
|
|
|
175
175
|
descriptionKey="settings.description"
|
|
176
176
|
defaultOpen={false}
|
|
177
177
|
state={state}
|
|
178
|
-
renderChildrenWhenNotExposed
|
|
179
|
-
hideNotExposedNotice
|
|
180
178
|
onSave={props.save}
|
|
181
179
|
onDiscard={props.discard}
|
|
182
180
|
>
|
package/src/client/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import { FenceNotice } from './FenceNotice.tsx'
|
|
|
41
41
|
import { reportDailyHeartbeat } from './telemetry.ts'
|
|
42
42
|
import { startMobileAdapt, type RemoteAdaptGlobal } from './mobile-adapt.ts'
|
|
43
43
|
import { installPluginCard } from './plugin-card-seat.ts'
|
|
44
|
+
import { createServedEntryForm } from './settings-entry-form.ts'
|
|
44
45
|
|
|
45
46
|
// Portrait-touch adaptation of the official UI: installed under the plugin
|
|
46
47
|
// lifecycle (apply) so disabling the plugin in cordis patch (disabled: true)
|
|
@@ -51,8 +52,6 @@ export type { PanelState, RemotePanelProps } from './RemotePanel.tsx'
|
|
|
51
52
|
export type { PairFailedNoticeProps } from './PairFailedNotice.tsx'
|
|
52
53
|
export type { RemoteKey } from './locales.ts'
|
|
53
54
|
export type { RemoteSettingsCardFace, RemoteSettingsCardState } from './RemoteSettingsCard.tsx'
|
|
54
|
-
export type { UpdateEntryProps } from './UpdateEntry.tsx'
|
|
55
|
-
export type { UpdatePanelProps, UpdateView } from './UpdatePanel.tsx'
|
|
56
55
|
|
|
57
56
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
58
57
|
interface LocaleNamespaceMap {
|
|
@@ -110,22 +109,70 @@ declare module '@deepseek-ai/cordis' {
|
|
|
110
109
|
/** Dictionary namespace owned by this plugin. */
|
|
111
110
|
const NS = 'remote'
|
|
112
111
|
|
|
113
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Settings namespace the remote-control card edits: the family identity of
|
|
114
|
+
* this plugin's own settings form, and the row id a standalone bundle install
|
|
115
|
+
* carries.
|
|
116
|
+
*/
|
|
114
117
|
const REMOTE_WEB_UI_NS = 'remote-web-ui'
|
|
118
|
+
|
|
119
|
+
/** Profile entry id the family aggregate's generated row carries. */
|
|
115
120
|
const AGGREGATE_ENTRY_ID = 'web-ui-remote-web-ui'
|
|
121
|
+
|
|
122
|
+
/** Profile entry ids this package's patch rows carry, most specific first. */
|
|
116
123
|
const REMOTE_WEB_UI_ENTRY_IDS: readonly string[] = [AGGREGATE_ENTRY_ID, 'ui-remote-web-ui', REMOTE_WEB_UI_NS]
|
|
117
124
|
|
|
118
|
-
|
|
125
|
+
/**
|
|
126
|
+
* The profile entry id this package's own row carries, for a page that serves
|
|
127
|
+
* no family binder.
|
|
128
|
+
*
|
|
129
|
+
* The shared describe mirror answers asynchronously, so at plugin activation it
|
|
130
|
+
* usually holds nothing yet — and an unanswered or empty mirror is not evidence
|
|
131
|
+
* of absence. Binding the bare namespace there left the card bound to an entry
|
|
132
|
+
* the Host does not serve, and the form is bound once per session, so the card
|
|
133
|
+
* never recovered (every save answered `No configurable plugin entry`). The
|
|
134
|
+
* aggregate row id matches nearly every deployment; only a mirror that answers
|
|
135
|
+
* with OTHER plugins' rows falls back to the namespace itself (the pre-0.1.7
|
|
136
|
+
* keying shape), because that answer is the one that actually proves this
|
|
137
|
+
* package's own row is not served.
|
|
138
|
+
* @param forms - the shared configuration forms service.
|
|
139
|
+
* @returns the entry id to bind.
|
|
140
|
+
*/
|
|
141
|
+
export function servedEntryId(forms: ConfigForms): string {
|
|
119
142
|
let served: readonly string[] | undefined
|
|
120
143
|
try {
|
|
121
144
|
served = forms.describe().getSnapshot().view?.namespaces.map(view => view.ns)
|
|
122
145
|
} catch {
|
|
146
|
+
// A mirror that refuses the read is unanswered too, not absent.
|
|
123
147
|
served = undefined
|
|
124
148
|
}
|
|
125
|
-
if (
|
|
149
|
+
if (served === undefined || served.length === 0) return AGGREGATE_ENTRY_ID
|
|
126
150
|
return REMOTE_WEB_UI_ENTRY_IDS.find(id => served.includes(id)) ?? REMOTE_WEB_UI_NS
|
|
127
151
|
}
|
|
128
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Bind the settings form the remote-control card reads and writes.
|
|
155
|
+
*
|
|
156
|
+
* The family binder comes first: it resolves this package's family namespace
|
|
157
|
+
* onto the profile entry id the Host serves the form under, and keeps the
|
|
158
|
+
* loopback bridge as its own fallback. A page without that group (or one where
|
|
159
|
+
* its client half has not applied yet) binds through the shared forms service,
|
|
160
|
+
* on the entry id the describe mirror justifies and rebound as soon as the
|
|
161
|
+
* mirror answers — see {@link createServedEntryForm}.
|
|
162
|
+
* @param ctx - client context carrying the family binder and/or the shared forms service.
|
|
163
|
+
* @returns the form the card stages and saves through.
|
|
164
|
+
*/
|
|
165
|
+
export function bindSettingsForm(ctx: ClientContext): ConfigForm<RemoteSettings> {
|
|
166
|
+
const family = ctx.get('webUiSettings')
|
|
167
|
+
if (family !== undefined && typeof family.bind === 'function') {
|
|
168
|
+
return family.bind<RemoteSettings>({ namespace: REMOTE_WEB_UI_NS })
|
|
169
|
+
}
|
|
170
|
+
return createServedEntryForm<RemoteSettings>({
|
|
171
|
+
forms: ctx.configForms,
|
|
172
|
+
entryIds: REMOTE_WEB_UI_ENTRY_IDS,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
129
176
|
/** Heartbeat cadence from a paired phone (presence + revocation liveness). */
|
|
130
177
|
const HEARTBEAT_INTERVAL_MS = 10_000
|
|
131
178
|
|
|
@@ -204,10 +251,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
204
251
|
// shared form (natively, or over its loopback bridge); without that group the
|
|
205
252
|
// namespace IS the owning entry id, so the official service is addressed
|
|
206
253
|
// directly.
|
|
207
|
-
const
|
|
208
|
-
const settingsForm: ConfigForm<RemoteSettings> = family !== undefined && typeof family.bind === 'function'
|
|
209
|
-
? family.bind<RemoteSettings>({ namespace: REMOTE_WEB_UI_NS })
|
|
210
|
-
: ctx.configForms.get<RemoteSettings>(servedEntryId(ctx.configForms))
|
|
254
|
+
const settingsForm: ConfigForm<RemoteSettings> = bindSettingsForm(ctx)
|
|
211
255
|
const enabled = (): boolean => {
|
|
212
256
|
const snapshot = settingsForm.getSnapshot()
|
|
213
257
|
return snapshot.status === 'ready'
|
package/src/client/locales.ts
CHANGED
|
@@ -123,48 +123,6 @@ export const zh = {
|
|
|
123
123
|
'settings.saveFailed': '部署未接受这些值,已保留供你修改。',
|
|
124
124
|
'settings.invalidNumber': '请输入数字,留空则使用默认值。',
|
|
125
125
|
// 更新面板(侧边栏更新按钮)。
|
|
126
|
-
'update.label': '检查更新',
|
|
127
|
-
'update.badge': '有更新',
|
|
128
|
-
'update.availableLabel': '发现新版本,检查更新',
|
|
129
|
-
'update.title': '检查更新',
|
|
130
|
-
'update.checking': '正在检查远程仓库中的新版本…',
|
|
131
|
-
'update.upToDate': '当前已是最新版本',
|
|
132
|
-
'update.upToDateDetail': '{name} 已是最新版本 v{version}。',
|
|
133
|
-
'update.found': '发现新版本',
|
|
134
|
-
'update.foundDetail': '{name} 有新版本 v{version}。确认后开始更新。',
|
|
135
|
-
'update.start': '开始更新',
|
|
136
|
-
'update.updating': '正在更新 {name} 至 v{version},可能需要 1-2 分钟…',
|
|
137
|
-
'update.cooldownNotice': '本次更新由你主动触发,将跳过 pnpm 的 24 小时发布冷却期(供应链保护)直接安装所选版本。',
|
|
138
|
-
'update.updatingTitle': '正在更新…',
|
|
139
|
-
'update.done': '更新完成',
|
|
140
|
-
'update.doneDetail': '所有组件已更新到最新版本。',
|
|
141
|
-
'update.restartHint': '请重启 dsh web 使新版本生效:在终端按 Ctrl+C 停止后重新运行 dsh web。',
|
|
142
|
-
'update.linkMode': '当前为本地开发模式',
|
|
143
|
-
'update.linkModeDetail': 'dsh-web 或其组件通过本地链接安装(开发模式),无法自动更新。npm 上最新版本:{version}。请同步本地仓库,或移除与全家桶重复的独立链接。',
|
|
144
|
-
'update.missing': '未找到 dsh-web 安装',
|
|
145
|
-
'update.missingDetail': '无法定位已安装的 dsh-web 聚合包。请先运行 dsh plugin --profile <profile> add @linxin666/dsh-web-all。',
|
|
146
|
-
'update.offline': '无法连接更新源',
|
|
147
|
-
'update.offlineDetail': '网络不可用或更新服务异常,请稍后重试。',
|
|
148
|
-
'update.unmounted': '更新服务未加载',
|
|
149
|
-
'update.unmountedDetail': '当前 dsh web 进程运行的插件版本不含更新服务,请重启 dsh web(Ctrl+C 后重新运行)后再试。',
|
|
150
|
-
'update.error': '更新失败',
|
|
151
|
-
'update.packages': '组件版本',
|
|
152
|
-
'update.releaseNotes': 'v{version} 更新说明',
|
|
153
|
-
'update.releaseFeatures': '新增功能',
|
|
154
|
-
'update.releaseFixes': '修复',
|
|
155
|
-
'update.releaseOther': '其他变动',
|
|
156
|
-
'update.releaseUnavailable': '本次发布说明暂不可用,可前往 GitHub Release 查看。',
|
|
157
|
-
'update.componentVersions': '查看组件版本',
|
|
158
|
-
'update.retry': '重新检查',
|
|
159
|
-
'update.close': '关闭更新面板',
|
|
160
|
-
'update.error.pnpmMissing': '未找到 pnpm,已尝试 pnpm / corepack / npx 仍不可用。请安装 pnpm 后重启应用(Windows: npm install -g pnpm 或 corepack enable;macOS: brew install pnpm 或 corepack enable)。',
|
|
161
|
-
'update.error.timeout': '更新超时,已终止安装进程,请重试。',
|
|
162
|
-
'update.error.notFound': '未找到 dsh-web 聚合包安装,无法更新。',
|
|
163
|
-
'update.error.link': 'dsh-web 或其组件使用本地链接,无法自动更新;请同步本地仓库,或移除与全家桶重复的独立链接。',
|
|
164
|
-
'update.error.pnpmFailed': '更新执行失败(pnpm 退出码 {code}),详见下方输出。',
|
|
165
|
-
'update.error.stale': '更新命令已执行,但安装版本未变化。常见原因:pnpm 11 的 minimumReleaseAge 门禁会静默跳过发布不足 24 小时的新版本。请在 profile 目录(~/.dsh/profiles/<profile>)的 pnpm-workspace.yaml 中配置 minimumReleaseAgeExclude(例如 "@linxin666/*")或 minimumReleaseAge: 0 后重新点击更新。',
|
|
166
|
-
'update.error.verifyFailed': '更新命令已执行,但无法核对安装后的版本(可能是 registry 探测失败或安装路径变化)。请查看下方输出,或稍后重试。',
|
|
167
|
-
'update.error.unknown': '更新失败,请重试。',
|
|
168
126
|
} satisfies Record<string, string>
|
|
169
127
|
|
|
170
128
|
/** The remote namespace key union. */
|
|
@@ -296,46 +254,4 @@ export const en = {
|
|
|
296
254
|
'settings.saveFailed': 'The deployment did not accept these values; they were left for you to correct.',
|
|
297
255
|
'settings.invalidNumber': 'Enter a number, or leave blank to use the default.',
|
|
298
256
|
// Update panel (the sidebar update trigger).
|
|
299
|
-
'update.label': 'Check for updates',
|
|
300
|
-
'update.badge': 'Update available',
|
|
301
|
-
'update.availableLabel': 'New version available. Check for updates',
|
|
302
|
-
'update.title': 'Check for updates',
|
|
303
|
-
'update.checking': 'Checking the remote repository for newer releases…',
|
|
304
|
-
'update.upToDate': 'Everything is up to date',
|
|
305
|
-
'update.upToDateDetail': '{name} is at the latest version v{version}.',
|
|
306
|
-
'update.found': 'A new version is available',
|
|
307
|
-
'update.foundDetail': '{name} has a newer release v{version}. Confirm to start the update.',
|
|
308
|
-
'update.start': 'Update now',
|
|
309
|
-
'update.updating': 'Updating {name} to v{version}; this may take 1-2 minutes…',
|
|
310
|
-
'update.cooldownNotice': 'This user-initiated update bypasses pnpm\'s 24-hour release cooldown (supply-chain protection) and installs the selected version immediately.',
|
|
311
|
-
'update.updatingTitle': 'Updating…',
|
|
312
|
-
'update.done': 'Update complete',
|
|
313
|
-
'update.doneDetail': 'All components are up to date.',
|
|
314
|
-
'update.restartHint': 'Restart dsh web for the new version to take effect: stop it with Ctrl+C in the terminal, then run dsh web again.',
|
|
315
|
-
'update.linkMode': 'Local development mode',
|
|
316
|
-
'update.linkModeDetail': 'dsh-web or one of its components is installed via a local link and cannot be updated remotely. Latest npm release: {version}. Sync the local checkout, or remove a standalone link duplicated by the aggregate.',
|
|
317
|
-
'update.missing': 'dsh-web not found',
|
|
318
|
-
'update.missingDetail': 'Could not locate the installed dsh-web aggregate package. Install it with: dsh plugin --profile <profile> add @linxin666/dsh-web-all',
|
|
319
|
-
'update.offline': 'Cannot reach the update source',
|
|
320
|
-
'update.offlineDetail': 'The network is unavailable or the update service is failing; try again later.',
|
|
321
|
-
'update.unmounted': 'The update service is not loaded',
|
|
322
|
-
'update.unmountedDetail': 'The running dsh web process loads a plugin build without the update service; restart dsh web (Ctrl+C, then run dsh web again) and retry.',
|
|
323
|
-
'update.error': 'Update failed',
|
|
324
|
-
'update.packages': 'Component versions',
|
|
325
|
-
'update.releaseNotes': 'v{version} release notes',
|
|
326
|
-
'update.releaseFeatures': 'New Features',
|
|
327
|
-
'update.releaseFixes': 'Bug Fixes',
|
|
328
|
-
'update.releaseOther': 'Other Changes',
|
|
329
|
-
'update.releaseUnavailable': 'Release notes are unavailable for this version; see the GitHub release instead.',
|
|
330
|
-
'update.componentVersions': 'View component versions',
|
|
331
|
-
'update.retry': 'Check again',
|
|
332
|
-
'update.close': 'Close update panel',
|
|
333
|
-
'update.error.pnpmMissing': 'pnpm was not found (tried pnpm / corepack / npx). Install pnpm and restart the app (Windows: npm install -g pnpm or corepack enable; macOS: brew install pnpm or corepack enable).',
|
|
334
|
-
'update.error.timeout': 'The update timed out and the install process was killed; try again.',
|
|
335
|
-
'update.error.notFound': 'The dsh-web aggregate package is not installed; nothing to update.',
|
|
336
|
-
'update.error.link': 'dsh-web or one of its components uses a local link. Sync the checkout, or remove a standalone link duplicated by the aggregate.',
|
|
337
|
-
'update.error.pnpmFailed': 'The update failed (pnpm exited with code {code}); see the output below.',
|
|
338
|
-
'update.error.stale': 'The update command ran but the installed versions did not change. Likely cause: the pnpm 11 minimumReleaseAge gate silently skips releases published less than 24 hours ago. Add minimumReleaseAgeExclude (e.g. "@linxin666/*") or set minimumReleaseAge: 0 in pnpm-workspace.yaml under the profile directory (~/.dsh/profiles/<profile>), then run the update again.',
|
|
339
|
-
'update.error.verifyFailed': 'The update command ran but the installed versions could not be verified afterwards (registry probe failed or the install path changed). Inspect the output below, or retry later.',
|
|
340
|
-
'update.error.unknown': 'Update failed; try again.',
|
|
341
257
|
} satisfies Record<RemoteKey, string>
|
|
@@ -54,6 +54,17 @@ const ADAPT_CSS_ID = 'dsh-remote-web-ui/mobile-adapt.css'
|
|
|
54
54
|
const ACTIVE_CLASS = 'dsh-remote-portrait'
|
|
55
55
|
/** Body class while the collapsed rail is hidden behind the whale. */
|
|
56
56
|
const RAIL_HIDDEN_CLASS = 'dsh-remote-rail-hidden'
|
|
57
|
+
/**
|
|
58
|
+
* The official sidebar column root. Also the scope of the row sweep: every row
|
|
59
|
+
* the drag suppression covers is a descendant of it, so the document-wide scan
|
|
60
|
+
* it replaces only ever returned these.
|
|
61
|
+
*/
|
|
62
|
+
const SIDEBAR_SELECTOR = '[class*="_sidebarCol"]'
|
|
63
|
+
/** The official draggable session/project rows (see disableRowDrag). */
|
|
64
|
+
const ROW_SELECTOR = '[class*="_sessionRow"], [class*="_projectRow"]'
|
|
65
|
+
/** The injected stylesheet, addressed through the same cached lookup. */
|
|
66
|
+
const STYLE_SELECTOR = `style[data-plugin-css="${ADAPT_CSS_ID}"]`
|
|
67
|
+
|
|
57
68
|
/** Whale button id. */
|
|
58
69
|
const WHALE_ID = 'dshRemoteWhale'
|
|
59
70
|
/** Compact picker: synthesized model button id. */
|
|
@@ -346,6 +357,8 @@ export function startMobileAdapt(): void {
|
|
|
346
357
|
let whaleObserver: MutationObserver | null = null
|
|
347
358
|
/** Header subtree observer: marks the geometry measurement dirty on re-render. */
|
|
348
359
|
let headerObserver: MutationObserver | null = null
|
|
360
|
+
/** Document child-list observer: invalidates the cached-absent selector set. */
|
|
361
|
+
let domObserver: MutationObserver | null = null
|
|
349
362
|
let observedHeader: Element | null = null
|
|
350
363
|
/** Whether the seated-actions geometry needs re-measuring (see alignActionsText). */
|
|
351
364
|
let headerGeometryDirty = true
|
|
@@ -368,17 +381,28 @@ export function startMobileAdapt(): void {
|
|
|
368
381
|
* rail compaction) while the body class stays.
|
|
369
382
|
*/
|
|
370
383
|
function ensureAdaptStyle(): void {
|
|
371
|
-
if (nodeOf(
|
|
384
|
+
if (nodeOf(STYLE_SELECTOR) !== null) return
|
|
372
385
|
const tag = document.createElement('style')
|
|
373
386
|
tag.dataset.plugin = 'remote-web-ui'
|
|
374
387
|
tag.dataset.pluginCss = ADAPT_CSS_ID
|
|
375
388
|
tag.textContent = ADAPT_CSS.join('')
|
|
376
389
|
document.head.appendChild(tag)
|
|
390
|
+
// Seed the lookup cache: the tag is created here, outside nodeOf, so the
|
|
391
|
+
// negative cache could otherwise report it missing for one more tick and
|
|
392
|
+
// stack a duplicate stylesheet.
|
|
393
|
+
nodeCache.set(STYLE_SELECTOR, tag)
|
|
394
|
+
nodeMissCache.delete(STYLE_SELECTOR)
|
|
377
395
|
}
|
|
378
396
|
|
|
379
397
|
function apply(): void {
|
|
380
398
|
if (active) return
|
|
381
399
|
active = true
|
|
400
|
+
// First statement of the activation, before any lookup: nothing observed the
|
|
401
|
+
// document while the layer was reverted (the observer is created below), so
|
|
402
|
+
// no "absent" verdict recorded earlier may be served now. Clearing here
|
|
403
|
+
// rather than beside the observer also covers the lookups this path runs
|
|
404
|
+
// before that point.
|
|
405
|
+
forgetAbsentSelectors()
|
|
382
406
|
document.body.classList.add(ACTIVE_CLASS)
|
|
383
407
|
// A details panel opened before the viewport rotated into portrait (or
|
|
384
408
|
// restored across reloads) would sit behind the display:none above;
|
|
@@ -397,6 +421,7 @@ export function startMobileAdapt(): void {
|
|
|
397
421
|
meta.setAttribute('content', `${savedViewportContent}, viewport-fit=cover`)
|
|
398
422
|
}
|
|
399
423
|
ensureWhale()
|
|
424
|
+
ensureDomObserver()
|
|
400
425
|
syncWhale()
|
|
401
426
|
// React builds nodes with attributes before inserting them, so
|
|
402
427
|
// attribute observers miss the initial collapsed state — a light
|
|
@@ -429,6 +454,45 @@ export function startMobileAdapt(): void {
|
|
|
429
454
|
whaleObserver.disconnect()
|
|
430
455
|
whaleObserver = null
|
|
431
456
|
}
|
|
457
|
+
if (domObserver !== null) {
|
|
458
|
+
domObserver.disconnect()
|
|
459
|
+
domObserver = null
|
|
460
|
+
}
|
|
461
|
+
// Nothing observes the document from here on; a verdict recorded now could
|
|
462
|
+
// not be trusted after the next apply().
|
|
463
|
+
forgetAbsentSelectors()
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Observe the document for the two kinds of change that can make a cached-absent
|
|
468
|
+
* target discoverable again (see nodeOf): node insertion/removal, and the
|
|
469
|
+
* class / compat-stamp attributes the cached selectors match on.
|
|
470
|
+
*
|
|
471
|
+
* Body class writes are excluded by target: the layer toggles three body
|
|
472
|
+
* classes every tick, and counting those would invalidate the negative cache
|
|
473
|
+
* on every tick and restore the very scan this cache removes. They cannot
|
|
474
|
+
* create a target either — every cached selector matches an element other
|
|
475
|
+
* than <body>.
|
|
476
|
+
*
|
|
477
|
+
* The record queue is drained on a microtask, so this stays off the layout
|
|
478
|
+
* path; a chat turn's insertions are a handful per second, not per element.
|
|
479
|
+
*/
|
|
480
|
+
function ensureDomObserver(): void {
|
|
481
|
+
if (domObserver !== null || typeof MutationObserver === 'undefined' || !document.body) return
|
|
482
|
+
domObserver = new MutationObserver((records) => {
|
|
483
|
+
for (const record of records) {
|
|
484
|
+
if (record.type === 'childList' || record.target !== document.body) {
|
|
485
|
+
noteDomChange()
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
})
|
|
490
|
+
domObserver.observe(document.body, {
|
|
491
|
+
childList: true,
|
|
492
|
+
subtree: true,
|
|
493
|
+
attributes: true,
|
|
494
|
+
attributeFilter: ['class', 'data-dsh-frame'],
|
|
495
|
+
})
|
|
432
496
|
}
|
|
433
497
|
|
|
434
498
|
/** Start/stop the 600ms sync tick; a no-op when already in the asked state. */
|
|
@@ -702,7 +766,7 @@ export function startMobileAdapt(): void {
|
|
|
702
766
|
const dragOverridden = new Map<Element, string | null>()
|
|
703
767
|
function disableRowDrag(): void {
|
|
704
768
|
if (!active) return
|
|
705
|
-
const rows =
|
|
769
|
+
const rows = sidebarRows()
|
|
706
770
|
for (const row of rows) {
|
|
707
771
|
if (row.getAttribute('draggable') !== 'false') {
|
|
708
772
|
if (!dragOverridden.has(row)) dragOverridden.set(row, row.getAttribute('draggable'))
|
|
@@ -731,15 +795,90 @@ export function startMobileAdapt(): void {
|
|
|
731
795
|
* node on a major re-render, which the isConnected guard detects.
|
|
732
796
|
*/
|
|
733
797
|
const nodeCache = new Map<string, Element>()
|
|
798
|
+
/**
|
|
799
|
+
* Selectors known to be absent as of {@link domGeneration}. A miss is the
|
|
800
|
+
* common case, not the exception: the layer holds no overlay of its own, and
|
|
801
|
+
* several official surfaces it looks for (tabs row, tools row, sidebar rows)
|
|
802
|
+
* legitimately do not exist at once. Re-walking a conversation-sized document
|
|
803
|
+
* for the same absent selector on every 600ms tick was waste that almost
|
|
804
|
+
* always resolved to null: five document-wide lookups per tick on the measured
|
|
805
|
+
* fixture, zero after this cache. That is a lookup count — the wall-clock
|
|
806
|
+
* saving is real while the mounted content is quiet and shrinks to the
|
|
807
|
+
* row-scope saving when a turn streams (see the Agent Note for the harness and
|
|
808
|
+
* the before/after numbers).
|
|
809
|
+
*
|
|
810
|
+
* A miss is trusted only while the observed content has not changed, so a
|
|
811
|
+
* surface that appears is still found on the very next tick — the same
|
|
812
|
+
* discovery latency the unconditional probe had. The cache is never aged by a
|
|
813
|
+
* clock: it is cleared by {@link noteDomChange} while the observer is live and
|
|
814
|
+
* by {@link forgetAbsentSelectors} across the windows where it is not (apply
|
|
815
|
+
* and revert). One blind spot is deliberate and load-bearing: the observer
|
|
816
|
+
* watches <body>, so a head-resident target is not covered by that signal —
|
|
817
|
+
* the one such target is seeded into the cache where it is created (see
|
|
818
|
+
* ensureAdaptStyle).
|
|
819
|
+
*/
|
|
820
|
+
const nodeMissCache = new Set<string>()
|
|
821
|
+
/**
|
|
822
|
+
* Bumped whenever a cached target may have appeared or disappeared. A surface
|
|
823
|
+
* qualifies on a class token or on the aggregate compat stamp
|
|
824
|
+
* (`data-dsh-frame`), so both insertions/removals AND those two attributes
|
|
825
|
+
* can change the answer. Nothing the layer writes per tick lands here: its own
|
|
826
|
+
* class writes are all on <body>, and its row/transform/label writes touch
|
|
827
|
+
* other attributes entirely (see ensureDomObserver).
|
|
828
|
+
*/
|
|
829
|
+
let domGeneration = 0
|
|
830
|
+
/** The generation the negative cache was recorded against. */
|
|
831
|
+
let missGeneration = -1
|
|
832
|
+
|
|
833
|
+
function noteDomChange(): void {
|
|
834
|
+
domGeneration += 1
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Drop every cached "absent" verdict. Called on apply and revert: while the
|
|
839
|
+
* observer is disconnected the layer is blind to insertions, so a verdict
|
|
840
|
+
* recorded before the gap could otherwise outlive the change that falsified
|
|
841
|
+
* it and be served without a probe. The positive cache needs no equivalent —
|
|
842
|
+
* its `isConnected` guard already re-resolves a replaced node.
|
|
843
|
+
*/
|
|
844
|
+
function forgetAbsentSelectors(): void {
|
|
845
|
+
nodeMissCache.clear()
|
|
846
|
+
domGeneration += 1
|
|
847
|
+
}
|
|
848
|
+
|
|
734
849
|
function nodeOf(selector: string): Element | null {
|
|
735
850
|
const cached = nodeCache.get(selector)
|
|
736
|
-
if (cached !== undefined
|
|
851
|
+
if (cached !== undefined) {
|
|
852
|
+
// A node React replaced is no longer connected; drop it and fall through
|
|
853
|
+
// so the lookup below can find its replacement.
|
|
854
|
+
if (cached.isConnected) return cached
|
|
855
|
+
nodeCache.delete(selector)
|
|
856
|
+
}
|
|
857
|
+
if (missGeneration !== domGeneration) {
|
|
858
|
+
nodeMissCache.clear()
|
|
859
|
+
missGeneration = domGeneration
|
|
860
|
+
}
|
|
861
|
+
if (nodeMissCache.has(selector)) return null
|
|
737
862
|
const found = document.querySelector(selector)
|
|
738
|
-
if (found === null)
|
|
739
|
-
|
|
863
|
+
if (found === null) {
|
|
864
|
+
nodeMissCache.add(selector)
|
|
865
|
+
return null
|
|
866
|
+
}
|
|
867
|
+
nodeCache.set(selector, found)
|
|
740
868
|
return found
|
|
741
869
|
}
|
|
742
870
|
|
|
871
|
+
/**
|
|
872
|
+
* The session/project rows the drag suppression covers, scoped to the cached
|
|
873
|
+
* sidebar root. Each row carries the sidebar column class as an ancestor, so
|
|
874
|
+
* the document-wide scan this replaces only ever returned rows from here.
|
|
875
|
+
*/
|
|
876
|
+
function sidebarRows(): Element[] {
|
|
877
|
+
const sidebar = nodeOf(SIDEBAR_SELECTOR)
|
|
878
|
+
if (sidebar === null) return []
|
|
879
|
+
return Array.from(sidebar.querySelectorAll(ROW_SELECTOR))
|
|
880
|
+
}
|
|
881
|
+
|
|
743
882
|
/** The official application frame, through the same cached lookup. */
|
|
744
883
|
function frameEl(): HTMLElement | null {
|
|
745
884
|
const frame = nodeOf(APP_FRAME_SELECTOR)
|