dsh-dock 0.12.0 → 0.13.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 +51 -6
- package/client.js +633 -234
- package/features/mobile/view.js +305 -0
- package/features/mobile-relay/gateway.js +187 -11
- package/features/mobile-relay/host.js +175 -50
- package/features/mobile-relay/view.jsx +217 -86
- package/index.js +5 -1
- package/package.json +2 -2
- package/src/client.jsx +3 -2
- package/src/host-core.js +129 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 访问地址二维码;远程设备上经 window.__DSH_REMOTE__ 显示退出登录。
|
|
5
5
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
6
6
|
import QRCode from 'qrcode'
|
|
7
|
-
import { openPanel,
|
|
7
|
+
import { openPanel, subscribePanel, panelNav } from '../../src/shared.js'
|
|
8
8
|
|
|
9
9
|
function rpc(method, payload) {
|
|
10
10
|
return fetch('/dsh-dock/mobile-relay/' + method, {
|
|
@@ -145,7 +145,7 @@ function RemoteCard() {
|
|
|
145
145
|
<label className="dmr-field"><span>监听端口</span><input value={port} onChange={(e) => setPort(e.target.value)} inputMode="numeric" type="number" min="1024" max="65535" placeholder={'默认 ' + suggestedPort} aria-describedby="dmr-lan-port-help"/><small id="dmr-lan-port-help">网关和主服务是同一台机器上的两个端口,不能相同:主实例 {lan && lan.mainPort ? lan.mainPort : '…'} 只留给本机,其他设备走网关端口登录。留空即用 {suggestedPort}(主端口+1)。</small></label>
|
|
146
146
|
<div className="dmr-security"><strong>安全说明</strong><p>主实例保持仅监听 127.0.0.1:远程设备只能经这个登录网关进入,不存在免登录的直连路径。账号密码只发给自己;改密后所有设备需重新登录。</p></div>
|
|
147
147
|
</div>
|
|
148
|
-
{active ? <div className="dmr-share"><div className="dmr-share-layout"><div className="dmr-qr-card">{qr ? <img src={qr} alt="远程访问地址二维码" width="220" height="220"/> : <div className="dmr-qr-loading">正在生成二维码…</div>}<strong>扫码或输入地址</strong><small>打开后输入账号密码登录</small></div><div className="dmr-share-detail"><div><span className="dmr-eyebrow"><i/> 登录网关已运行</span><h4>{lanLink}</h4></div><div className="dmr-link" title={lanLink}>{lanLink}</div><div className="dmr-share-actions"><button type="button" className="dmr-primary" onClick={copyLink} disabled={!lanLink}><RelayIcon name="copy"/>复制地址</button><button type="button" className="dmr-secondary dmr-danger" onClick={disable} disabled={busy === 'stop'}><RelayIcon name="close"/>{busy === 'stop' ? '正在关闭…' : '关闭远程访问'}</button></div><small>主实例保持仅本机(结构上不存在免登录直连);账号密码只发给自己。若开启前已有旧服务器模式(0.0.0.0)配置,会一并移除并提示重启。</small></div></div></div>
|
|
148
|
+
{active ? <div className="dmr-share"><div className="dmr-share-layout"><div className="dmr-qr-card">{qr ? <img src={qr} alt="远程访问地址二维码" width="220" height="220"/> : <div className="dmr-qr-loading">正在生成二维码…</div>}<strong>扫码或输入地址</strong><small>打开后输入账号密码登录</small></div><div className="dmr-share-detail"><div><span className="dmr-eyebrow"><i/> 登录网关已运行</span><h4>{lanLink}</h4></div><div className="dmr-link" title={lanLink}>{lanLink}</div><div className="dmr-share-actions"><button type="button" className="dmr-primary" onClick={copyLink} disabled={!lanLink}><RelayIcon name="copy"/>复制地址</button><button type="button" className="dmr-secondary dmr-danger" onClick={disable} disabled={busy === 'stop'}><RelayIcon name="close"/>{busy === 'stop' ? '正在关闭…' : '关闭远程访问'}</button></div><small>已登录设备:{typeof lan.devices === 'number' ? lan.devices : 0} 台(改密或关闭入口即全部下线)</small><small>主实例保持仅本机(结构上不存在免登录直连);账号密码只发给自己。若开启前已有旧服务器模式(0.0.0.0)配置,会一并移除并提示重启。</small></div></div></div>
|
|
149
149
|
: <button type="button" className="dmr-primary" onClick={enable} disabled={busy === 'start'}>{busy === 'start' ? '正在开启…' : <><RelayIcon name="link"/>开启远程访问</>}</button>}
|
|
150
150
|
{active ? <div className="dmr-section"><div className="dmr-section-head"><h4>修改账号密码</h4><button type="button" className="dmr-text-button" onClick={() => setChanging((v) => !v)}>{changing ? '收起' : '修改'}</button></div>{changing ? <div className="dmr-note"><div className="dmr-network-grid"><label className="dmr-field"><span>账号</span><input value={username} onChange={(e) => setUsername(e.target.value)} autoComplete="username"/></label><label className="dmr-field"><span>新密码</span><input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} autoComplete="new-password" placeholder="至少 6 位"/></label></div><button type="button" className="dmr-secondary" onClick={saveAuth} disabled={busy === 'auth' || !username.trim() || newPassword.length < 6}>{busy === 'auth' ? '保存中…' : '保存(所有设备重新登录)'}</button></div> : <p className="dmr-note">当前账号:{lan.username || '—'}。修改后所有已登录设备将被强制退出。</p>}</div> : null}
|
|
151
151
|
{message ? <div className={'dmr-message ' + (messageKind === 'success' ? 'success' : 'error')} role="alert">{message}</div> : null}
|
|
@@ -157,66 +157,110 @@ export function MobileRelayView() {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
|
-
* 手机端会话抽屉(窄屏 ≤700px
|
|
160
|
+
* 手机端会话抽屉(窄屏 ≤700px):底部 Tab 栏「会话」页签拉出侧边栏 + 遮罩点击收起。
|
|
161
161
|
*
|
|
162
162
|
* 直接操作 document.body 下的 DOM(渲染返回 null)——若作为 overlayLayer 的子元素,
|
|
163
|
-
* 会落入 overlayLayer(z 90)的堆叠上下文,遮罩压不住侧栏抽屉(z 80
|
|
164
|
-
* 挂到 body 下 z
|
|
163
|
+
* 会落入 overlayLayer(z 90)的堆叠上下文,遮罩压不住侧栏抽屉(z 80)、Tab 栏也盖不住内容;
|
|
164
|
+
* 挂到 body 下 z 序才是设计值(遮罩 75 < 侧栏 80 < overlayLayer 90)。
|
|
165
|
+
* 左缘浮动「抽屉把手」已移除(与底部 Tab 栏「会话」页签功能重复,还悬浮在内容上)。
|
|
165
166
|
*
|
|
166
167
|
* 与宿主 tapIndex 注入版(features/mobile-relay/host.js,重启 dsh web 后才生效)互斥:
|
|
167
|
-
* window.__dshDockMobileDrawer 先到先得——宿主脚本在 <head>
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
168
|
+
* window.__dshDockMobileDrawer 先到先得——宿主脚本在 <head> 先跑并置 'host',本组件
|
|
169
|
+
* 见旗标即整体让位(遮罩/底部 Tab/收起逻辑全归宿主);宿主未就位(旧版无注入、
|
|
170
|
+
* 或刷新早于重启)时本组件自建全套。本组件的价值是 client bundle 按内容 hash 服务、
|
|
171
|
+
* 刷新页面即生效,无需重启。
|
|
172
|
+
* ⚠️ 两个版本绝不能各建一套:曾出现双底部 Tab 栏(两套按钮叠在一起)与客户端 CSS
|
|
173
|
+
* 覆盖宿主 place() 定位的回归。
|
|
174
|
+
* 与旧版宿主行为脚本并存是安全的:收起链路同一事件内第二次 collapse() 因 aria-label
|
|
175
|
+
* 已翻转而自然空转。
|
|
171
176
|
*/
|
|
172
177
|
export function MobileRelayOverlay() {
|
|
178
|
+
// —— 预览版说明自动确认(独立 effect,与下方抽屉 chrome 的宿主互斥无关)——
|
|
179
|
+
// 根因(dsh 0.2.0-rc.2 ui-settings-models/src/client/welcome-store.ts,文件头注释原文):
|
|
180
|
+
// 回环浏览器的确认走宿主持久化,一次确认永久生效;**远程(非回环)浏览器的设置 scope
|
|
181
|
+
// 是 memory 模式,确认只存页面进程内存,刷新即丢**——所以从局域网/组网 IP 打开时
|
|
182
|
+
// 「预览版说明」每次刷新都重弹,profile patch 里钉 welcomeNoticeVersion 也救不了
|
|
183
|
+
// (memory 分支根本不读持久值)。这里在远程访问场景下替用户点一次「继续」:
|
|
184
|
+
// 每次页面加载最多点一次,回环地址不介入(让 dsh 原生的一次性持久确认自然工作)。
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return undefined
|
|
187
|
+
const host = (window.location && window.location.hostname) || ''
|
|
188
|
+
const loopback = host === '127.0.0.1' || host === 'localhost' || host === '::1' || host.endsWith('.localhost')
|
|
189
|
+
if (loopback) return undefined
|
|
190
|
+
if (typeof MutationObserver === 'undefined') return undefined
|
|
191
|
+
let done = false
|
|
192
|
+
const tryDismiss = () => {
|
|
193
|
+
if (done) return
|
|
194
|
+
const dialogs = document.querySelectorAll('[role="dialog"]')
|
|
195
|
+
for (const dialog of dialogs) {
|
|
196
|
+
const text = (dialog.textContent || '')
|
|
197
|
+
if (!/预览版说明|Preview Notice/.test(text)) continue
|
|
198
|
+
const btn = [...dialog.querySelectorAll('button')].find((b) => {
|
|
199
|
+
const t = (b.textContent || '').trim()
|
|
200
|
+
return t === '继续' || t === 'Continue'
|
|
201
|
+
})
|
|
202
|
+
if (btn) {
|
|
203
|
+
done = true
|
|
204
|
+
btn.click()
|
|
205
|
+
observer.disconnect()
|
|
206
|
+
}
|
|
207
|
+
break
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const observer = new MutationObserver(tryDismiss)
|
|
211
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
212
|
+
tryDismiss()
|
|
213
|
+
// 兜底停止:说明若 60 秒内没出现(未来版本改版/被官方移除),观察器自动退场
|
|
214
|
+
const stopTimer = setTimeout(() => observer.disconnect(), 60000)
|
|
215
|
+
return () => { clearTimeout(stopTimer); observer.disconnect() }
|
|
216
|
+
}, [])
|
|
217
|
+
|
|
173
218
|
useEffect(() => {
|
|
174
219
|
if (typeof window === 'undefined' || typeof document === 'undefined') return undefined
|
|
175
|
-
// 宿主版(<head> 注入,重启后接管)就位时,抽屉把手/遮罩/收起逻辑由宿主负责,
|
|
176
|
-
// 本组件只补宿主旧版没有的底部 Tab 栏;宿主未就位(旧版无抽屉 DOM)时全套自建。
|
|
177
|
-
const hostPresent = !!(window.__dshDockMobileDrawer && document.querySelector('.dsh-mobile-drawer-btn'))
|
|
178
220
|
const mq = window.matchMedia ? window.matchMedia('(max-width:700px)') : null
|
|
179
221
|
if (!mq) return undefined
|
|
180
|
-
|
|
222
|
+
// 宿主版接管中:整套 chrome 由宿主负责,本组件零副作用退出。
|
|
223
|
+
if (window.__dshDockMobileDrawer === 'host') return undefined
|
|
224
|
+
window.__dshDockMobileDrawer = 'client'
|
|
181
225
|
|
|
182
226
|
const narrow = () => mq.matches
|
|
183
227
|
const frameEl = () => document.querySelector('[class$="frame"]')
|
|
184
228
|
const sidebarCollapsed = () => { const f = frameEl(); return !!f && f.hasAttribute('data-sidebar-collapsed') }
|
|
185
229
|
const dialogOpen = () => !!document.querySelector('[role="dialog"][class*="panel"]')
|
|
230
|
+
// 右侧详情面板:dsh 0.1.7 起 detailsCol 改名 rightbarCol,窄屏原生全屏展开
|
|
231
|
+
// (data-sidebar-right-panel / data-sidebar-right-open,折叠钮 data-sidebar-right-toggle)。
|
|
232
|
+
const rightOpen = () => !!document.querySelector('[data-sidebar-right-panel][data-sidebar-right-open]')
|
|
186
233
|
const collapse = () => { const b = document.querySelector('button[aria-label="收起侧边栏"],button[aria-label="Collapse sidebar"]'); if (b) b.click() }
|
|
187
234
|
const expand = () => { const b = document.querySelector('button[aria-label="打开侧边栏"],button[aria-label="Open sidebar"]'); if (b) b.click() }
|
|
188
235
|
const closeDetails = () => {
|
|
189
|
-
const
|
|
190
|
-
if (!
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
236
|
+
const panel = document.querySelector('[data-sidebar-right-panel][data-sidebar-right-open]')
|
|
237
|
+
if (!panel) return
|
|
238
|
+
const toggle = panel.querySelector('[data-sidebar-right-toggle]')
|
|
239
|
+
if (toggle) toggle.click()
|
|
193
240
|
}
|
|
194
241
|
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (!hostPresent) {
|
|
202
|
-
fab = document.createElement('button')
|
|
203
|
-
fab.type = 'button'
|
|
204
|
-
fab.className = 'dsh-mobile-drawer-btn'
|
|
205
|
-
fab.setAttribute('aria-label', '打开会话列表')
|
|
206
|
-
fab.innerHTML = '<svg viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/></svg>'
|
|
207
|
-
fab.addEventListener('click', expand)
|
|
208
|
-
scrim = document.createElement('div')
|
|
209
|
-
scrim.className = 'dsh-mobile-scrim'
|
|
210
|
-
document.body.appendChild(fab)
|
|
211
|
-
document.body.appendChild(scrim)
|
|
212
|
-
}
|
|
242
|
+
// 抽屉遮罩:侧栏展开(浮层态)时盖住页面,点击经全局捕获 handler 收起。
|
|
243
|
+
// 左缘的浮动「抽屉把手」已移除:底部 Tab 栏的「会话」页签就是会话抽屉的唯一入口,
|
|
244
|
+
// 把手悬浮在内容上还压过输入区左下角(用户实测截图),纯属重复。
|
|
245
|
+
const scrim = document.createElement('div')
|
|
246
|
+
scrim.className = 'dsh-mobile-scrim'
|
|
247
|
+
document.body.appendChild(scrim)
|
|
213
248
|
|
|
214
|
-
// ---- 仿 ZCode 手机端底部 Tab 栏:会话 /
|
|
215
|
-
//
|
|
249
|
+
// ---- 仿 ZCode 手机端底部 Tab 栏:会话 / 插件 / 设置 ----
|
|
250
|
+
// 「任务」「功能坞」合并为一个「插件」页签(用户反馈两个入口冗余):插件面板里
|
|
251
|
+
// 本来就有运行状态页,任务状态以角标显示在「插件」上——进行中蓝色数量、等待确认
|
|
252
|
+
// 红色脉冲;有任务在跑时点「插件」直接落到运行状态页,否则进面板首页。
|
|
253
|
+
// 挂 body(避开 overlayLayer 堆叠上下文),z 序 65:
|
|
216
254
|
// 低于遮罩 75 / 侧栏抽屉 80 / 功能坞面板 200,这些打开时自然盖住 Tab 栏。
|
|
255
|
+
// 走到这里说明宿主旗标不存在(宿主版未注入)——宿主版自建的 Tab 栏不会出现;
|
|
256
|
+
// 兜底摘一次历史上可能残留的旧 Tab 栏,保证全程只有一份。
|
|
257
|
+
document.querySelectorAll('.dsh-mobile-tabbar').forEach((el) => el.remove())
|
|
217
258
|
const tabbar = document.createElement('nav')
|
|
218
259
|
tabbar.className = 'dsh-mobile-tabbar'
|
|
219
260
|
tabbar.setAttribute('aria-label', '底部导航')
|
|
261
|
+
// 最近一次角标轮询看到的任务数(驱动「插件」页签的直达目标与角标显隐)。
|
|
262
|
+
let tasksActive = 0
|
|
263
|
+
let tasksWaiting = 0
|
|
220
264
|
const tabDefs = [
|
|
221
265
|
{
|
|
222
266
|
id: 'sessions', label: '会话',
|
|
@@ -224,9 +268,15 @@ export function MobileRelayOverlay() {
|
|
|
224
268
|
act: () => expand(),
|
|
225
269
|
},
|
|
226
270
|
{
|
|
227
|
-
id: 'dock', label: '
|
|
271
|
+
id: 'dock', label: '插件',
|
|
228
272
|
icon: '<svg viewBox="0 0 24 24"><rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/></svg>',
|
|
229
|
-
act: () => {
|
|
273
|
+
act: () => {
|
|
274
|
+
// 始终打开、不再当开关用:若功能坞处于打开态但被原生全屏页/弹层(如从侧栏
|
|
275
|
+
// 进的官方插件管理页)盖住,再点一下会把功能坞关掉、露出的反而是原生页——
|
|
276
|
+
// 看起来就是「点插件出官方插件页」(用户实测截图)。关闭走面板自己的 ✕。
|
|
277
|
+
// 配合手机端把功能坞面板 z 序抬到原生弹层之上(媒体查询内 2000)。
|
|
278
|
+
openPanel((tasksActive > 0 || tasksWaiting > 0) ? 'runstate' : 'home')
|
|
279
|
+
},
|
|
230
280
|
},
|
|
231
281
|
{
|
|
232
282
|
id: 'settings', label: '设置',
|
|
@@ -234,6 +284,7 @@ export function MobileRelayOverlay() {
|
|
|
234
284
|
act: () => { const b = [...document.querySelectorAll('button')].find((x) => (x.getAttribute('aria-label') || '') === '设置' || (x.textContent || '').trim() === '设置'); if (b) b.click() },
|
|
235
285
|
},
|
|
236
286
|
]
|
|
287
|
+
let tasksBadge = null
|
|
237
288
|
const tabButtons = tabDefs.map((def) => {
|
|
238
289
|
const b = document.createElement('button')
|
|
239
290
|
b.type = 'button'
|
|
@@ -241,6 +292,12 @@ export function MobileRelayOverlay() {
|
|
|
241
292
|
b.dataset.tab = def.id
|
|
242
293
|
b.innerHTML = def.icon + '<span>' + def.label + '</span>'
|
|
243
294
|
b.addEventListener('click', () => def.act())
|
|
295
|
+
if (def.id === 'dock') {
|
|
296
|
+
tasksBadge = document.createElement('span')
|
|
297
|
+
tasksBadge.className = 'dsh-mobile-tab-badge'
|
|
298
|
+
tasksBadge.style.display = 'none'
|
|
299
|
+
b.appendChild(tasksBadge)
|
|
300
|
+
}
|
|
244
301
|
tabbar.appendChild(b)
|
|
245
302
|
return b
|
|
246
303
|
})
|
|
@@ -250,46 +307,73 @@ export function MobileRelayOverlay() {
|
|
|
250
307
|
const settingsOpen = dialogOpen()
|
|
251
308
|
for (const b of tabButtons) {
|
|
252
309
|
const id = b.dataset.tab
|
|
253
|
-
const on = (id === 'sessions' && expanded)
|
|
310
|
+
const on = (id === 'sessions' && expanded)
|
|
311
|
+
|| (id === 'dock' && panelNav.open)
|
|
312
|
+
|| (id === 'settings' && settingsOpen)
|
|
254
313
|
b.classList.toggle('on', on)
|
|
255
314
|
}
|
|
256
315
|
}
|
|
257
316
|
const unsubPanel = subscribePanel(syncTabs)
|
|
258
317
|
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
318
|
+
// 「任务」角标:低频轮询 runstate 宿主路由(3s,仅窄屏 + 页面可见时发);
|
|
319
|
+
// 进行中 = 蓝色数量,等待确认 = 红色数量 + 脉冲。路由不存在(runstate 未启用
|
|
320
|
+
// 或宿主旧版)时静默停轮询并摘掉角标,不报错不打扰。
|
|
321
|
+
let runstateGone = false
|
|
322
|
+
const pollTasks = () => {
|
|
323
|
+
if (runstateGone || !tasksBadge || !narrow() || document.visibilityState === 'hidden' || !tabbar.isConnected) return
|
|
324
|
+
fetch('/dsh-dock/runstate/status', {
|
|
325
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}',
|
|
326
|
+
}).then(async (res) => {
|
|
327
|
+
if (res.status === 404 || res.status === 405) { runstateGone = true; tasksActive = 0; tasksWaiting = 0; tasksBadge.style.display = 'none'; return null }
|
|
328
|
+
const body = await res.json().catch(() => null)
|
|
329
|
+
if (!body) return null
|
|
330
|
+
const active = (body.data && body.data.active) || []
|
|
331
|
+
const waiting = active.filter((t) => t.approvals && t.approvals.length).length
|
|
332
|
+
tasksActive = active.length
|
|
333
|
+
tasksWaiting = waiting
|
|
334
|
+
if (waiting > 0) {
|
|
335
|
+
tasksBadge.textContent = String(waiting)
|
|
336
|
+
tasksBadge.className = 'dsh-mobile-tab-badge wait'
|
|
337
|
+
tasksBadge.style.display = ''
|
|
338
|
+
} else if (active.length > 0) {
|
|
339
|
+
tasksBadge.textContent = String(active.length)
|
|
340
|
+
tasksBadge.className = 'dsh-mobile-tab-badge'
|
|
341
|
+
tasksBadge.style.display = ''
|
|
342
|
+
} else {
|
|
343
|
+
tasksBadge.style.display = 'none'
|
|
273
344
|
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
345
|
+
}).catch(() => {})
|
|
346
|
+
}
|
|
347
|
+
const tasksTimer = setInterval(pollTasks, 3000)
|
|
348
|
+
|
|
349
|
+
// 软键盘判定:visualViewport 与布局视口高度差超阈值视为键盘弹出(挂
|
|
350
|
+
// html.dsh-dock-kbd,样式表随之隐藏 Tab 栏并收窄滚动体留白——打字时
|
|
351
|
+
// 这两样只会挤占键盘上方的一线空间)。老内核没有 visualViewport 就跳过
|
|
352
|
+
// (那些内核键盘行为是整页缩放,原生命中适配)。
|
|
353
|
+
const vv = window.visualViewport || null
|
|
354
|
+
const kbdSync = () => {
|
|
355
|
+
if (!vv) return
|
|
356
|
+
const kbd = vv.height > 0 && (window.innerHeight - vv.height) > 120
|
|
357
|
+
document.documentElement.classList.toggle('dsh-dock-kbd', kbd)
|
|
277
358
|
}
|
|
278
359
|
|
|
279
360
|
const sync = () => {
|
|
361
|
+
kbdSync()
|
|
362
|
+
// 兜底摘除历史上可能残留的旧 Tab 栏(正常路径下走到这里说明宿主版未注入,
|
|
363
|
+
// 不会有第二份;保险起见 sync 周期里也保持全局只有一份)。
|
|
364
|
+
document.querySelectorAll('.dsh-mobile-tabbar').forEach((el) => { if (el !== tabbar) el.remove() })
|
|
280
365
|
const on = narrow() && !!frameEl() && !dialogOpen()
|
|
281
366
|
const collapsed = sidebarCollapsed()
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (showFab) place()
|
|
286
|
-
}
|
|
367
|
+
// 右侧详情面板(窄屏原生全屏,z 40)打开时:Tab 栏 65 比它高会压边,整体让位,
|
|
368
|
+
// 面板收起(它自带折叠钮)后再回来。
|
|
369
|
+
const right = rightOpen()
|
|
287
370
|
if (scrim) scrim.style.display = on && !collapsed ? '' : 'none'
|
|
288
|
-
// Tab
|
|
289
|
-
tabbar.style.display = narrow() && !!frameEl() && !panelNav.open ? '' : 'none'
|
|
371
|
+
// Tab 栏:窄屏且页面骨架就绪后常驻;功能坞面板/设置弹窗/右侧详情面板打开时隐藏(避免露边压内容)
|
|
372
|
+
tabbar.style.display = narrow() && !!frameEl() && !panelNav.open && !right ? '' : 'none'
|
|
290
373
|
syncTabs()
|
|
291
374
|
}
|
|
292
375
|
sync()
|
|
376
|
+
pollTasks()
|
|
293
377
|
// MutationObserver 跟属性翻转即时同步;1.5s 轮询覆盖路由重渲等一切边角
|
|
294
378
|
const observer = typeof MutationObserver !== 'undefined' ? new MutationObserver(sync) : null
|
|
295
379
|
if (observer) observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-sidebar-collapsed'], subtree: true })
|
|
@@ -297,41 +381,59 @@ export function MobileRelayOverlay() {
|
|
|
297
381
|
const onMq = () => sync()
|
|
298
382
|
if (mq.addEventListener) mq.addEventListener('change', onMq)
|
|
299
383
|
else if (mq.addListener) mq.addListener(onMq)
|
|
300
|
-
window.addEventListener('resize',
|
|
384
|
+
window.addEventListener('resize', kbdSync)
|
|
385
|
+
if (vv && vv.addEventListener) vv.addEventListener('resize', kbdSync)
|
|
386
|
+
const onVisible = () => { if (document.visibilityState === 'visible') pollTasks() }
|
|
387
|
+
document.addEventListener('visibilitychange', onVisible)
|
|
301
388
|
|
|
302
389
|
// 捕获阶段统一处理(仅自建模式;宿主就位时宿主已有同款全局捕获):
|
|
303
|
-
//
|
|
390
|
+
// Tab 栏自身排除(否则页签动作会被"点外部收起"当场撤销);
|
|
304
391
|
// 遮罩不挂自己的 click——它的点击也走这里 collapse(),单一路径防止同一事件两次 toggle。
|
|
305
392
|
const onClick = (e) => {
|
|
306
393
|
if (!narrow()) return
|
|
307
394
|
const t = e.target
|
|
308
395
|
if (!t || !t.closest) return
|
|
309
|
-
if (t.closest('.dsh-mobile-
|
|
396
|
+
if (t.closest('.dsh-mobile-tabbar')) return
|
|
310
397
|
const expanded = !!document.querySelector('button[aria-label="收起侧边栏"],button[aria-label="Collapse sidebar"]')
|
|
311
398
|
const col = document.querySelector('[class*="sidebarCol"]')
|
|
312
399
|
const inSidebar = col && col.contains(t)
|
|
313
400
|
if (!expanded && !inSidebar) return
|
|
314
401
|
if (inSidebar) {
|
|
402
|
+
// 只有点「会话」才收抽屉:会话行的 data-row-key 以 session: 开头(0.1.7 结构,
|
|
403
|
+
// ui-workspace Rows.tsx);工作区文件夹行是 workspace: 开头——点它只是展开/收拢
|
|
404
|
+
// 分组,不能收抽屉(旧的「无子 treeitem 即叶子」判定会误伤折叠状态的文件夹:
|
|
405
|
+
// 折叠时其子节点不在 DOM 里)。搜索结果行无 data-row-key,回退按无子叶判定。
|
|
315
406
|
const row = t.closest('[role="treeitem"]')
|
|
316
|
-
const
|
|
407
|
+
const rowKey = (row && row.getAttribute('data-row-key')) || ''
|
|
408
|
+
const sessionRow = (rowKey && rowKey.startsWith('session:'))
|
|
409
|
+
|| (row && !rowKey && !row.querySelector('[role="treeitem"]') && row.closest('[role="tree"]'))
|
|
317
410
|
const fresh = t.closest('[class*="newSession"]')
|
|
318
|
-
if (
|
|
411
|
+
if (sessionRow || fresh) setTimeout(() => { collapse(); closeDetails() }, 300)
|
|
412
|
+
else {
|
|
413
|
+
// 侧栏入口(插件/自动化任务/设置/记忆/自动进化…)点击后会弹出面板,而面板
|
|
414
|
+
// z 序常低于展开的抽屉——被盖住等于「点了没反应」(用户实测截图)。稍候
|
|
415
|
+
// 探测:有面板弹出就收起抽屉让它露出来;目录折叠/搜索等不弹面板的点击不受影响。
|
|
416
|
+
setTimeout(() => { if (document.querySelector('[role="dialog"]')) collapse() }, 260)
|
|
417
|
+
}
|
|
319
418
|
return
|
|
320
419
|
}
|
|
321
|
-
if (t.closest('[role="dialog"],[class*="dockm"],[class*="dgfab"],[class*="dgwin"],[class*="dgame"],[
|
|
420
|
+
if (t.closest('[role="dialog"],[class*="dockm"],[class*="dgfab"],[class*="dgwin"],[class*="dgame"],[data-sidebar-right-panel]')) return
|
|
322
421
|
collapse()
|
|
323
422
|
}
|
|
324
|
-
|
|
423
|
+
document.addEventListener('click', onClick, true)
|
|
325
424
|
|
|
326
425
|
return () => {
|
|
327
|
-
|
|
426
|
+
document.removeEventListener('click', onClick, true)
|
|
328
427
|
if (observer) observer.disconnect()
|
|
329
428
|
clearInterval(timer)
|
|
429
|
+
clearInterval(tasksTimer)
|
|
330
430
|
unsubPanel()
|
|
331
431
|
if (mq.removeEventListener) mq.removeEventListener('change', onMq)
|
|
332
432
|
else if (mq.removeListener) mq.removeListener(onMq)
|
|
333
|
-
window.removeEventListener('resize',
|
|
334
|
-
if (
|
|
433
|
+
window.removeEventListener('resize', kbdSync)
|
|
434
|
+
if (vv && vv.removeEventListener) vv.removeEventListener('resize', kbdSync)
|
|
435
|
+
document.removeEventListener('visibilitychange', onVisible)
|
|
436
|
+
document.documentElement.classList.remove('dsh-dock-kbd')
|
|
335
437
|
if (scrim) scrim.remove()
|
|
336
438
|
tabbar.remove()
|
|
337
439
|
if (window.__dshDockMobileDrawer === 'client') delete window.__dshDockMobileDrawer
|
|
@@ -343,9 +445,15 @@ export function MobileRelayOverlay() {
|
|
|
343
445
|
export function MobileRelayHomeStat() {
|
|
344
446
|
const [summary, setSummary] = useState('未开启,开启后可远程登录')
|
|
345
447
|
useEffect(() => {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
448
|
+
const refresh = () => {
|
|
449
|
+
rpc('lan').then((data) => {
|
|
450
|
+
const devices = typeof data.devices === 'number' && data.devices > 0 ? ' · ' + data.devices + ' 台设备在线' : ''
|
|
451
|
+
setSummary(data.gatewayActive ? '入口运行中 · 端口 ' + data.gatewayPort + devices : data.accountSet ? '账号已设置,入口未开启' : '未开启,开启后可远程登录')
|
|
452
|
+
}).catch(() => {})
|
|
453
|
+
}
|
|
454
|
+
refresh()
|
|
455
|
+
const timer = setInterval(refresh, 10000)
|
|
456
|
+
return () => clearInterval(timer)
|
|
349
457
|
}, [])
|
|
350
458
|
return <span>{summary}</span>
|
|
351
459
|
}
|
|
@@ -355,9 +463,9 @@ export const feature = {
|
|
|
355
463
|
description: '账号密码登录的远程入口:所有设备访问同一个 DSH,任务进度实时一致', defaultEnabled: false,
|
|
356
464
|
css: `
|
|
357
465
|
.dmr{--dmr-accent:var(--dk-accent,#2f6fed);--dmr-accent-soft:color-mix(in srgb,var(--dmr-accent) 13%,transparent);display:flex;flex-direction:column;gap:16px;max-width:720px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}.dmr h3,.dmr h4,.dmr p{margin:0}.dmr h3{font-size:17px;line-height:1.25;letter-spacing:-.01em}.dmr h4{font-size:13px}.dmr-hero,.dmr-status-head{display:flex;align-items:flex-start;gap:12px}.dmr-hero>div,.dmr-status-head>div{min-width:0;display:flex;flex-direction:column;gap:4px}.dmr-hero p,.dmr-status-head p,.dmr-field small,.dmr-share small,.dmr-overview-card small,.dmr-task small,.dmr-note p{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.55}.dmr-hero-icon{width:42px;height:42px;display:inline-flex;align-items:center;justify-content:center;flex:none;border-radius:14px;background:var(--dmr-accent-soft);color:var(--dmr-accent);border:1px solid color-mix(in srgb,var(--dmr-accent) 35%,var(--dsw-alias-border-l1))}.dmr-steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin:0;padding:0;list-style:none}.dmr-steps li{display:flex;gap:9px;padding:11px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:12px}.dmr-steps li>span{width:21px;height:21px;display:inline-flex;align-items:center;justify-content:center;flex:none;border-radius:50%;font-size:11px;font-weight:700;color:var(--dmr-accent);background:var(--dmr-accent-soft)}.dmr-steps div{display:flex;flex-direction:column;gap:3px;min-width:0}.dmr-steps strong{font-size:12px}.dmr-steps small{font-size:11px;color:var(--dsw-alias-label-secondary);line-height:1.45}.dmr-field{display:flex;flex-direction:column;gap:6px;font-weight:600}.dmr-field input,.dmr-field select,.dmr-note textarea{box-sizing:border-box;width:100%;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:10px;padding:10px 12px;min-height:44px;font:inherit;outline:none;transition:border-color .18s ease,box-shadow .18s ease}.dmr-field input:focus,.dmr-field select:focus,.dmr-note textarea:focus{border-color:var(--dmr-accent);box-shadow:0 0 0 3px var(--dmr-accent-soft)}.dmr-field small{font-weight:400}.dmr-network-grid{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(150px,.6fr);gap:12px}.dmr-security{padding:12px 13px;border:1px solid color-mix(in srgb,var(--dk-warn) 48%,var(--dsw-alias-border-l1));border-radius:12px;background:color-mix(in srgb,var(--dk-warn) 9%,transparent)}.dmr-security strong{display:block;margin-bottom:4px;color:var(--dk-warn);font-size:12px}.dmr-security p{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.55}.dmr-primary,.dmr-secondary,.dmr-text-button,.dmr-icon-button{font:inherit;touch-action:manipulation;cursor:pointer;transition:transform .15s ease,background .18s ease,border-color .18s ease,opacity .18s ease}.dmr-primary,.dmr-secondary{min-height:44px;display:inline-flex;align-items:center;justify-content:center;gap:8px;border-radius:10px;padding:9px 13px;font-weight:600}.dmr-primary{align-self:flex-start;border:1px solid var(--dmr-accent);background:var(--dmr-accent);color:#fff}.dmr-primary:hover{filter:brightness(1.05)}.dmr-secondary{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.dmr-secondary:hover{border-color:var(--dmr-accent);background:var(--dmr-accent-soft)}.dmr-primary:active,.dmr-secondary:active,.dmr-icon-button:active{transform:scale(.98)}.dmr-primary:disabled,.dmr-secondary:disabled,.dmr-text-button:disabled,.dmr-icon-button:disabled{cursor:not-allowed;opacity:.5}.dmr-message{display:flex;align-items:flex-start;gap:7px;padding:10px 12px;border-radius:10px;font-size:12px}.dmr-message.success{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent)}.dmr-message.error{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}.dmr-status-head{justify-content:space-between}.dmr-eyebrow{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:600;color:var(--dmr-accent)}.dmr-eyebrow i{width:7px;height:7px;border-radius:50%;background:currentColor;box-shadow:0 0 0 4px var(--dmr-accent-soft)}.dmr-icon-button{display:inline-flex;align-items:center;justify-content:center;flex:none;width:44px;height:44px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:transparent;color:var(--dsw-alias-label-secondary)}.dmr-icon-button:hover{color:var(--dsw-alias-state-error-primary);border-color:currentColor}.dmr-share{display:flex;flex-direction:column;gap:9px;padding:13px;border:1px solid color-mix(in srgb,var(--dmr-accent) 32%,var(--dsw-alias-border-l1));border-radius:13px;background:var(--dmr-accent-soft)}.dmr-share-layout{display:grid;grid-template-columns:196px minmax(0,1fr);gap:16px;align-items:center}.dmr-qr-card{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;gap:5px;padding:10px;border-radius:12px;background:#fff;color:#111827;text-align:center}.dmr-qr-card img{display:block;width:176px;height:176px;max-width:100%;object-fit:contain}.dmr-qr-card strong{font-size:12px}.dmr-qr-card small{color:#4b5563;font-size:10px}.dmr-qr-loading{display:grid;place-items:center;width:176px;height:176px;color:#64748b;font-size:12px}.dmr-share-detail{min-width:0;display:flex;flex-direction:column;gap:10px}.dmr-share-detail>div:first-child{display:flex;flex-direction:column;gap:4px}.dmr-link{padding:10px 11px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all;color:var(--dsw-alias-label-secondary)}.dmr-share-actions{display:flex;gap:8px;flex-wrap:wrap}.dmr-share-actions .dmr-primary{align-self:auto}.dmr-overview{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.dmr-overview-card{display:flex;flex-direction:column;gap:3px;padding:12px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1)}.dmr-overview-card>span{font-size:11px;color:var(--dsw-alias-label-tertiary)}.dmr-overview-card strong{font-size:16px;letter-spacing:-.01em}.dmr-section{display:flex;flex-direction:column;gap:9px}.dmr-section-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.dmr-section-head small{font-size:11px;color:var(--dsw-alias-label-tertiary)}.dmr-text-button{border:0;background:transparent;color:var(--dmr-accent);padding:8px;min-height:36px;font-weight:600}.dmr-task-list{display:flex;flex-direction:column;gap:8px}.dmr-task{display:flex;align-items:flex-start;gap:9px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}.dmr-task>div{display:flex;min-width:0;flex:1;flex-direction:column;gap:2px}.dmr-task strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dmr-phase{flex:none;border-radius:999px;padding:3px 8px;font-size:11px;background:var(--dmr-accent-soft);color:var(--dmr-accent)}.dmr-phase.write{color:var(--dsw-alias-state-success-primary)}.dmr-phase.code{color:var(--dk-warn)}.dmr-phase.search{color:#0d9488}.dmr-empty{padding:18px 12px;text-align:center;border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;color:var(--dsw-alias-label-secondary);font-size:12px}.dmr-note{display:flex;flex-direction:column;gap:9px;padding-top:2px}.dmr-note blockquote{margin:0;padding:10px 12px;border-left:3px solid var(--dmr-accent);border-radius:0 9px 9px 0;background:var(--dmr-accent-soft);white-space:pre-wrap;font-size:12px}.dmr-note textarea{min-height:88px;resize:vertical;line-height:1.5}.dmr-note .dmr-secondary{align-self:flex-start}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media (max-width:680px){.dmr{gap:14px;font-size:14px}.dmr h3{font-size:18px}.dmr-steps{grid-template-columns:1fr;gap:8px}.dmr-steps li{padding:10px}.dmr-steps small,.dmr-hero p,.dmr-status-head p,.dmr-field small,.dmr-share small,.dmr-overview-card small,.dmr-task small,.dmr-note p{font-size:12px}.dmr-network-grid{grid-template-columns:1fr;gap:10px}.dmr-field input,.dmr-field select{font-size:16px}.dmr-primary,.dmr-secondary{width:100%;font-size:14px}.dmr-share-layout{grid-template-columns:1fr;gap:12px}.dmr-qr-card{width:min(240px,100%);margin:0 auto}.dmr-qr-card img,.dmr-qr-loading{width:210px;height:210px}.dmr-share-actions{flex-direction:column}.dmr-share-actions .dmr-primary{width:100%}.dmr-overview{gap:8px}.dmr-overview-card{padding:11px}.dmr-status-head{gap:8px}.dmr-link{font-size:11px}.dmr-note textarea{font-size:16px;min-height:104px}.dmr-note .dmr-secondary{align-self:stretch}.dmr-compact .dmr-hero-icon{width:40px;height:40px;border-radius:13px}}@media (prefers-reduced-motion:reduce){.dmr-primary,.dmr-secondary,.dmr-text-button,.dmr-icon-button,.dmr-field input,.dmr-field select,.dmr-note textarea{transition:none}}.dmr-lan{flex:none;margin-top:6px;padding-top:16px;border-top:1px solid var(--dsw-alias-border-l1)}.dmr-lan-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.dmr-lan-title{min-width:0;display:flex;flex-direction:column;gap:4px}.dmr-lan-title p{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.55}.dmr-lan-badge{flex:none;display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:5px 10px;font-size:11px;font-weight:700;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary)}.dmr-lan-badge.on{border-color:color-mix(in srgb,var(--dmr-accent) 45%,transparent);background:var(--dmr-accent-soft);color:var(--dmr-accent)}.dmr-lan-active,.dmr-lan-idle{display:flex;flex-direction:column;gap:12px}.dmr-lan-active .dmr-share-detail small strong{color:var(--dsw-alias-state-error-primary)}.dmr-danger{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 45%,var(--dsw-alias-border-l2));color:var(--dsw-alias-state-error-primary)}.dmr-danger:hover{border-color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
|
|
358
|
-
/*
|
|
359
|
-
*
|
|
360
|
-
@media (max-width:700px){.dsh-mobile-
|
|
466
|
+
/* 抽屉遮罩:侧栏浮层展开时盖住页面。左缘浮动「抽屉把手」已移除——会话入口
|
|
467
|
+
* 统一走底部 Tab 栏「会话」页签,把手悬浮在内容上还压过输入区(用户实测)。 */
|
|
468
|
+
@media (max-width:700px){.dsh-mobile-scrim{position:fixed;inset:0;z-index:75;background:rgba(8,10,14,.45);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);touch-action:none}
|
|
361
469
|
/* ---- 仿 ZCode 手机端:底部 Tab 栏 + 输入区吸附底部 ---- */
|
|
362
470
|
.dsh-mobile-tabbar{position:fixed;left:0;right:0;bottom:0;z-index:65;display:flex;align-items:stretch;justify-content:space-around;padding:6px 8px calc(8px + env(safe-area-inset-bottom,0px));background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,#1c2230) 92%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-top:1px solid var(--dsw-alias-border-l1,rgba(127,139,161,.25));touch-action:manipulation}
|
|
363
471
|
.dsh-mobile-tab{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;min-height:48px;padding:4px 0;border:0;background:transparent;color:var(--dsw-alias-label-tertiary,#8b95ab);font:600 11px/1.2 inherit;cursor:pointer;-webkit-tap-highlight-color:transparent;border-radius:10px}
|
|
@@ -365,16 +473,39 @@ export const feature = {
|
|
|
365
473
|
.dsh-mobile-tab.on{color:var(--dsw-alias-label-primary,#e6eaf2)}
|
|
366
474
|
.dsh-mobile-tab.on svg{color:var(--dk-accent,#4d9fff)}
|
|
367
475
|
.dsh-mobile-tab:active{transform:scale(.94)}
|
|
476
|
+
/* 「任务」页签角标:进行中=蓝(数量),等待确认=红(数量+脉冲)——接力场景里
|
|
477
|
+
* 等待确认是最需要人介入的状态,一眼可见;数据来自 runstate 宿主路由低频轮询。 */
|
|
478
|
+
.dsh-mobile-tab{position:relative}
|
|
479
|
+
.dsh-mobile-tab-badge{position:absolute;top:0;right:calc(50% - 26px);box-sizing:border-box;min-width:16px;height:16px;padding:0 4px;border-radius:9px;display:flex;align-items:center;justify-content:center;background:#4d9fff;color:#fff;font-size:10px;font-weight:700;line-height:1;font-variant-numeric:tabular-nums;box-shadow:0 2px 8px rgba(0,0,0,.35);pointer-events:none}
|
|
480
|
+
.dsh-mobile-tab-badge.wait{background:#ef4444;animation:dsh-badge-pulse 1.2s ease-in-out infinite}
|
|
481
|
+
@keyframes dsh-badge-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.18)}}
|
|
482
|
+
@media (prefers-reduced-motion:reduce){.dsh-mobile-tab-badge.wait{animation:none}}
|
|
368
483
|
/* 输入区吸附到 Tab 栏上方:宿主把欢迎内容 justify-content:center 垂直居中,
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
|
|
372
|
-
|
|
484
|
+
* 手机视口下输入卡片悬在中部、下方留出大片空白。改用「输入区 margin-top:auto」
|
|
485
|
+
* 贴底,并给滚动体留出 Tab 栏高度的内边距,避免内容被 Tab 栏遮挡。
|
|
486
|
+
* ⚠️ 绝不能给滚动体加 justify-content:flex-end/center 贴底:内容会被挤到滚动体
|
|
487
|
+
* 起点之外,Chrome 不把起点方向溢出算进 scrollHeight(手机视口实测
|
|
488
|
+
* scrollHeight==clientHeight、maxScroll==0),会话记录彻底滚不动、首条消息永远
|
|
489
|
+
* 看不到。宿主半部旧版注入里还留着那条 flex-end(要重启 dsh web 才消失),这里
|
|
490
|
+
* 用更高优先级的选择器显式复位:刷新页面即修好,不必等重启。 */
|
|
491
|
+
[class*="scrollBody"][data-conversation-scroll]{justify-content:flex-start !important;padding-bottom:calc(72px + env(safe-area-inset-bottom,0px)) !important}
|
|
492
|
+
[class*="scrollBody"]{padding-bottom:calc(72px + env(safe-area-inset-bottom,0px)) !important}
|
|
493
|
+
[class*="scrollBody"]>[class*="composerSeat"]{margin-top:auto;padding-bottom:6px}
|
|
494
|
+
/* 输入区工具行上的 dsh-dock chip(用量/余额):窄屏下 28cqw 的宽度上限把它们挤成
|
|
495
|
+
* 「…」「余..」(手机实测),既读不出数值又白占输入行空间。手机端整组收起,数值在
|
|
496
|
+
* 底部「功能坞」里看(用量/余额页信息更全);桌面端不动。 */
|
|
497
|
+
.dockchip-row{display:none}
|
|
373
498
|
/* 收起的侧栏轨道(collapsed rail)在窄屏只剩 1px 占位,但轨道里的按钮
|
|
374
499
|
* 仍会溢出贴在左边缘(功能坞/进化/设置三枚露出半边的圆钮)。隐藏整列,
|
|
375
500
|
* 这些入口已由底部 Tab 栏接管;展开抽屉(z 80)不受影响。 */
|
|
376
501
|
[class*="sidebarCol"]:has([class*="collapsed"]){visibility:hidden}
|
|
377
|
-
/*
|
|
378
|
-
|
|
502
|
+
/* 软键盘弹出(visualViewport 判定,挂 html.dsh-dock-kbd):Tab 栏整体让位(打字时
|
|
503
|
+
* 它只会挤占键盘上方的一线空间),滚动体留白收窄。选择器特异性高于上面的 72px
|
|
504
|
+
* 留白规则,能压过去。宿主接管期间由宿主脚本同步置/摘这个类。 */
|
|
505
|
+
html.dsh-dock-kbd .dsh-mobile-tabbar{display:none !important}
|
|
506
|
+
html.dsh-dock-kbd [class*="scrollBody"]{padding-bottom:12px !important}
|
|
507
|
+
/* 功能坞面板在手机端抬到原生弹层之上:从侧栏进的官方插件管理页等原生弹层 z 序高于
|
|
508
|
+
* 面板默认的 200,功能坞开在下面会被盖住——「插件」页签点了像没反应/露出原页面。 */
|
|
509
|
+
[class*="dockm-backdrop"]{z-index:2000 !important}}`,
|
|
379
510
|
View: MobileRelayView, HomeStat: MobileRelayHomeStat, Overlay: MobileRelayOverlay,
|
|
380
511
|
}
|
package/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// - notify 任务通知:完成/异常/需确认通知 + 提示音/系统通知/钉钉飞书推送(从任务动画拆出)
|
|
17
17
|
// - runstate 运行状态:进行中任务与最近完成一览(从任务动画拆出;只读,无配置段)
|
|
18
18
|
// - mobile-relay 手机接力(未发布):扫码反向代理接力 + 局域网电脑直连(0.0.0.0)
|
|
19
|
-
import { DockConfig, sendJson, readBody, migrateNotifyConfig, migrateModelsFeatureId, migrateImportedFeatures, bindDockConfig, readDockRoot, mutateDockSection } from './src/host-core.js'
|
|
19
|
+
import { DockConfig, sendJson, readBody, migrateNotifyConfig, migrateModelsFeatureId, migrateImportedFeatures, migrateImportedSections, bindDockConfig, readDockRoot, mutateDockSection } from './src/host-core.js'
|
|
20
20
|
import { feature as fModels } from './features/modelconfig/host.js'
|
|
21
21
|
import { feature as fVisionProxy } from './features/visionproxy/host.js'
|
|
22
22
|
import { feature as fBalance } from './features/balance/host.js'
|
|
@@ -132,6 +132,10 @@ export function apply(ctx, config) {
|
|
|
132
132
|
ctx.inject(['settings'], (sctx) => {
|
|
133
133
|
// 0.1.7-alpha.1 起 settings.register 已移除——不再调用;导出 Config 即注册。
|
|
134
134
|
Promise.resolve()
|
|
135
|
+
// settings.yaml.imported 整段恢复(账号 remoteAuth / notify / animation /
|
|
136
|
+
// visionProxy——Config 导出前导入失败停在 imported 文件里的那批)。必须最先跑:
|
|
137
|
+
// 后面的迁移与功能启用都要读到恢复后的配置(远程访问账号丢了网关就开不起来)。
|
|
138
|
+
.then(() => migrateImportedSections(sctx))
|
|
135
139
|
// 通知配置从 animation 段搬到 notify 段(【任务通知】独立成模块)。
|
|
136
140
|
// 必须在任何面板保存动作之前跑——animation 模块保存时整段写回,旧字段会被覆盖丢失。
|
|
137
141
|
.then(() => migrateNotifyConfig(sctx))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dock",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计,支持各模型自定义单价、多段分时价与周末/节假日谷价)、模型设置、图片理解代理、模型余额、任务动画(19 种运行动画 + 桌面伙伴 3D 场景)、任务通知(完成/异常/需确认通知、提示音、系统通知、钉钉/飞书推送)、运行状态(进行中任务与最近完成一览);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build:client": "node scripts/build-client.mjs",
|
|
45
45
|
"test:client": "node scripts/test-client-views.mjs",
|
|
46
|
-
"test:host": "node scripts/test-task-host.mjs && node scripts/test-tokenlog-host.mjs",
|
|
46
|
+
"test:host": "node scripts/test-task-host.mjs && node scripts/test-tokenlog-host.mjs && node scripts/test-legacy-import.mjs && node scripts/test-mobile-relay-host.mjs && node scripts/test-mobile-relay.mjs",
|
|
47
47
|
"dev:client": "node scripts/watch-client.mjs",
|
|
48
48
|
"dev:link": "node scripts/dev-link-deps.mjs"
|
|
49
49
|
},
|
package/src/client.jsx
CHANGED
|
@@ -28,12 +28,13 @@ import { feature as fNotify } from "../features/notify/view.jsx";
|
|
|
28
28
|
import { feature as fRunState } from "../features/runstate/view.jsx";
|
|
29
29
|
import { feature as fGames } from "../features/games/view.jsx";
|
|
30
30
|
import { feature as fMobileRelay } from "../features/mobile-relay/view.jsx";
|
|
31
|
+
import { feature as fMobile } from "../features/mobile/view.js";
|
|
31
32
|
|
|
32
33
|
const name = "dsh-dock";
|
|
33
|
-
const DOCK_VERSION = "0.
|
|
34
|
+
const DOCK_VERSION = "0.13.0";
|
|
34
35
|
|
|
35
36
|
// ---- 内置功能注册表:新功能 = features/<id>/ 加模块 + 这里 import 一行 ----
|
|
36
|
-
const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation, fNotify, fRunState, fGames, fMobileRelay];
|
|
37
|
+
const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation, fNotify, fRunState, fGames, fMobileRelay, fMobile];
|
|
37
38
|
// 规划占位(路线图):接入后移除并建 features/<id>/ 模块
|
|
38
39
|
const PLANNED_FEATURES = [];
|
|
39
40
|
const PLANNED_NOTES = {};
|