dsh-dock 0.8.0 → 0.9.1
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 +136 -0
- package/client.js +2607 -187
- package/features/animation/view.jsx +39 -39
- package/features/balance/view.js +19 -17
- package/features/games/view.jsx +4 -4
- package/features/mobile-relay/gateway.js +217 -81
- package/features/mobile-relay/host.js +305 -230
- package/features/mobile-relay/view.jsx +107 -140
- package/features/modelconfig/view.js +4 -4
- package/features/tokenlog/view.jsx +8 -8
- package/index.js +20 -1
- package/package.json +2 -1
- package/src/client.jsx +22 -8
- package/src/host-core.js +7 -1
|
@@ -1,42 +1,20 @@
|
|
|
1
|
-
// dsh-dock ·
|
|
2
|
-
//
|
|
1
|
+
// dsh-dock · 功能模块【远程访问】· 客户端视图
|
|
2
|
+
// 单实例架构:主 DSH 只监听 127.0.0.1;局域网/虚拟网设备经账号密码登录的网关
|
|
3
|
+
// 访问同一个 DSH(会话、任务进度实时一致)。本视图管理:开启/关闭入口、账号密码、
|
|
4
|
+
// 访问地址二维码;远程设备上经 window.__DSH_REMOTE__ 显示退出登录。
|
|
3
5
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
4
6
|
import QRCode from 'qrcode'
|
|
5
7
|
|
|
6
|
-
const DEVICE_KEY = 'dsh-dock/mobile-relay/device/v1'
|
|
7
|
-
const SESSION_KEY = 'dsh-dock/mobile-relay/session/v1'
|
|
8
|
-
const LAUNCH_KEY = 'dsh-dock/mobile-relay/launch/v1'
|
|
9
|
-
|
|
10
8
|
function rpc(method, payload) {
|
|
11
9
|
return fetch('/dsh-dock/mobile-relay/' + method, {
|
|
12
10
|
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload || {}),
|
|
13
11
|
}).then(async (res) => {
|
|
14
12
|
const body = await res.json().catch(() => ({}))
|
|
15
13
|
if (res.ok && body && body.ok) return body.data
|
|
16
|
-
if (res.status === 404 || res.status === 405) throw new Error('宿主进程仍是旧版本,请重启 dsh web
|
|
14
|
+
if (res.status === 404 || res.status === 405) throw new Error('宿主进程仍是旧版本,请重启 dsh web 后再试')
|
|
17
15
|
throw new Error((body && body.error && body.error.message) || ('请求失败(' + res.status + ')'))
|
|
18
16
|
})
|
|
19
17
|
}
|
|
20
|
-
function deviceId() {
|
|
21
|
-
let id = ''
|
|
22
|
-
try { id = localStorage.getItem(DEVICE_KEY) || '' } catch { /* memory only */ }
|
|
23
|
-
if (!id) {
|
|
24
|
-
id = (globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2))
|
|
25
|
-
try { localStorage.setItem(DEVICE_KEY, id) } catch { /* memory only */ }
|
|
26
|
-
}
|
|
27
|
-
return id
|
|
28
|
-
}
|
|
29
|
-
function readSession() {
|
|
30
|
-
try { const raw = sessionStorage.getItem(SESSION_KEY); return raw ? JSON.parse(raw) : null } catch { return null }
|
|
31
|
-
}
|
|
32
|
-
function saveSession(value) { try { sessionStorage.setItem(SESSION_KEY, JSON.stringify(value)) } catch { /* memory state still works */ } }
|
|
33
|
-
function clearSession() { try { sessionStorage.removeItem(SESSION_KEY) } catch { /* ignore */ } }
|
|
34
|
-
function fmtDuration(ms) {
|
|
35
|
-
const sec = Math.max(0, Math.floor((ms || 0) / 1000)); const min = Math.floor(sec / 60); const rest = sec % 60
|
|
36
|
-
return min > 0 ? min + ' 分 ' + String(rest).padStart(2, '0') + ' 秒' : rest + ' 秒'
|
|
37
|
-
}
|
|
38
|
-
function fmtTime(value) { return value ? new Date(value).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '' }
|
|
39
|
-
function phaseLabel(phase) { return ({ think: '正在思考', write: '正在输出', code: '正在开发', search: '正在查资料' })[phase] || '进行中' }
|
|
40
18
|
function copy(value) {
|
|
41
19
|
if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(value)
|
|
42
20
|
return Promise.reject(new Error('当前浏览器不支持复制,请长按或手动复制'))
|
|
@@ -44,12 +22,10 @@ function copy(value) {
|
|
|
44
22
|
|
|
45
23
|
function RelayIcon({ name, size = 18 }) {
|
|
46
24
|
const common = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true }
|
|
47
|
-
if (name === 'phone') return <svg {...common}><rect x="7" y="2.5" width="10" height="19" rx="2.2"/><path d="M10 18.5h4"/></svg>
|
|
48
25
|
if (name === 'link') return <svg {...common}><path d="M10.5 13.5a4 4 0 0 0 5.66.01l2-2a4 4 0 0 0-5.66-5.66l-1.15 1.14"/><path d="M13.5 10.5a4 4 0 0 0-5.66-.01l-2 2a4 4 0 0 0 5.66 5.66l1.15-1.14"/></svg>
|
|
49
26
|
if (name === 'copy') return <svg {...common}><rect x="8" y="8" width="11" height="12" rx="1.5"/><path d="M16 8V5.5A1.5 1.5 0 0 0 14.5 4h-9A1.5 1.5 0 0 0 4 5.5v9A1.5 1.5 0 0 0 5.5 16H8"/></svg>
|
|
50
|
-
if (name === 'check') return <svg {...common}><path d="m5 12 4.2 4.2L19 6.5"/></svg>
|
|
51
|
-
if (name === 'arrow') return <svg {...common}><path d="M5 12h13"/><path d="m14 7 5 5-5 5"/></svg>
|
|
52
27
|
if (name === 'close') return <svg {...common}><path d="m6 6 12 12M18 6 6 18"/></svg>
|
|
28
|
+
if (name === 'check') return <svg {...common}><path d="m5 12 4.2 4.2L19 6.5"/></svg>
|
|
53
29
|
return <svg {...common}><circle cx="12" cy="12" r="8"/><path d="M12 8v4l2.5 2"/></svg>
|
|
54
30
|
}
|
|
55
31
|
|
|
@@ -63,145 +39,136 @@ function useCompact() {
|
|
|
63
39
|
return compact
|
|
64
40
|
}
|
|
65
41
|
|
|
66
|
-
|
|
42
|
+
/** 远程访问卡片:入口开关、账号密码、访问地址二维码、退出登录。 */
|
|
43
|
+
function RemoteCard() {
|
|
67
44
|
const compact = useCompact()
|
|
68
|
-
const [
|
|
69
|
-
const [
|
|
70
|
-
const [
|
|
71
|
-
const [
|
|
72
|
-
const [
|
|
73
|
-
const [
|
|
74
|
-
const [
|
|
45
|
+
const [lan, setLan] = useState(null)
|
|
46
|
+
const [username, setUsername] = useState('')
|
|
47
|
+
const [password, setPassword] = useState('')
|
|
48
|
+
const [newPassword, setNewPassword] = useState('')
|
|
49
|
+
const [port, setPort] = useState('')
|
|
50
|
+
const [address, setAddress] = useState('')
|
|
51
|
+
const [qr, setQr] = useState('')
|
|
75
52
|
const [busy, setBusy] = useState('')
|
|
76
|
-
const [
|
|
77
|
-
const [
|
|
78
|
-
const
|
|
53
|
+
const [message, setMessage] = useState('')
|
|
54
|
+
const [messageKind, setMessageKind] = useState('error')
|
|
55
|
+
const [changing, setChanging] = useState(false)
|
|
56
|
+
const [remote, setRemote] = useState(false)
|
|
79
57
|
|
|
80
|
-
const
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (session) return
|
|
83
|
-
rpc('network').then((data) => {
|
|
84
|
-
setNetwork(data)
|
|
85
|
-
if (data && data.defaultPort) setPort(data.defaultPort)
|
|
86
|
-
const first = data && data.addresses && data.addresses[0]
|
|
87
|
-
if (first) setSelectedAddress(first.address)
|
|
88
|
-
}).catch((e) => setError(e && e.message ? e.message : String(e)))
|
|
89
|
-
}, [session])
|
|
90
|
-
const refresh = useCallback(async (candidate = session) => {
|
|
91
|
-
if (!candidate) return
|
|
92
|
-
try {
|
|
93
|
-
const result = await rpc('status', candidate)
|
|
94
|
-
setPair(result.pair); setError('')
|
|
95
|
-
} catch (e) {
|
|
96
|
-
const message = e && e.message ? e.message : String(e)
|
|
97
|
-
setError(message)
|
|
98
|
-
if (/过期|未完成配对/.test(message)) setCurrent(null)
|
|
99
|
-
}
|
|
100
|
-
}, [session, setCurrent])
|
|
58
|
+
const notify = useCallback((text, kind) => { setMessage(text); setMessageKind(kind || 'error') }, [])
|
|
101
59
|
|
|
102
|
-
|
|
60
|
+
const refresh = useCallback(async () => {
|
|
61
|
+
try {
|
|
62
|
+
const data = await rpc('lan')
|
|
63
|
+
setLan(data)
|
|
64
|
+
setAddress((prev) => prev || (data.addresses && data.addresses[0] ? data.addresses[0].address : ''))
|
|
65
|
+
setUsername((prev) => prev || (data.username || ''))
|
|
66
|
+
} catch (e) { notify(e && e.message ? e.message : String(e)) }
|
|
67
|
+
}, [notify])
|
|
103
68
|
useEffect(() => {
|
|
104
|
-
|
|
105
|
-
const timer = setInterval(
|
|
69
|
+
refresh()
|
|
70
|
+
const timer = setInterval(refresh, 3000)
|
|
106
71
|
return () => clearInterval(timer)
|
|
107
|
-
}, [
|
|
72
|
+
}, [refresh])
|
|
108
73
|
useEffect(() => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!launch) return
|
|
113
|
-
try { sessionStorage.removeItem(LAUNCH_KEY) } catch { /* ignore */ }
|
|
114
|
-
const [pairId, code] = launch.split('.')
|
|
115
|
-
if (!pairId || !code) return
|
|
116
|
-
setBusy('join')
|
|
117
|
-
rpc('join', { pairId, code, deviceId: deviceId(), label: '手机端' })
|
|
118
|
-
.then((data) => { setCurrent({ pairId, secret: data.secret, role: 'mobile', deviceId: deviceId() }); setPair(data.pair); setNotice('已连接到电脑端,任务状态会持续同步。') })
|
|
119
|
-
.catch((e) => setError(e && e.message ? e.message : String(e)))
|
|
120
|
-
.finally(() => setBusy(''))
|
|
121
|
-
}, [session, setCurrent])
|
|
74
|
+
// 页面经网关访问时(网关注入 health 探针可用),显示退出登录。
|
|
75
|
+
fetch('/__dsh_auth/health', { method: 'POST' }).then((r) => setRemote(Boolean(r.ok))).catch(() => setRemote(false))
|
|
76
|
+
}, [])
|
|
122
77
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
78
|
+
const active = Boolean(lan && lan.gatewayActive)
|
|
79
|
+
const gatewayPort = lan && lan.gatewayPort ? lan.gatewayPort : 3081
|
|
80
|
+
// 未开启时的建议端口:跟随主实例端口(+1)。网关和主服务是同一台机器上的两个
|
|
81
|
+
// 监听,结构上不能同端口;主端口只留给本机,网关端口给其他设备登录用。
|
|
82
|
+
const suggestedPort = lan && lan.mainPort ? lan.mainPort + 1 : gatewayPort
|
|
83
|
+
const lanLink = useMemo(() => (address && active ? 'http://' + address + ':' + gatewayPort : ''), [address, active, gatewayPort])
|
|
127
84
|
useEffect(() => {
|
|
128
|
-
let
|
|
129
|
-
if (!
|
|
130
|
-
QRCode.toDataURL(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
.catch((e) => { if (active) setError('二维码生成失败:' + (e && e.message ? e.message : String(e))) })
|
|
135
|
-
return () => { active = false }
|
|
136
|
-
}, [mobileLink])
|
|
85
|
+
let live = true
|
|
86
|
+
if (!lanLink) { setQr(''); return () => { live = false } }
|
|
87
|
+
QRCode.toDataURL(lanLink, { errorCorrectionLevel: 'M', margin: 2, width: 220, color: { dark: '#111827', light: '#ffffff' } })
|
|
88
|
+
.then((value) => { if (live) setQr(value) }).catch(() => {})
|
|
89
|
+
return () => { live = false }
|
|
90
|
+
}, [lanLink])
|
|
137
91
|
|
|
138
|
-
async function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
92
|
+
async function enable() {
|
|
93
|
+
notify('')
|
|
94
|
+
if (!lan.accountSet && (!username.trim() || password.length < 6)) {
|
|
95
|
+
notify('首次开启请设置账号和至少 6 位的密码。'); return
|
|
96
|
+
}
|
|
143
97
|
setBusy('start')
|
|
144
98
|
try {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
99
|
+
const payload = {}
|
|
100
|
+
if (username.trim() && newPassword) { payload.username = username.trim(); payload.password = newPassword }
|
|
101
|
+
else if (!lan.accountSet) { payload.username = username.trim(); payload.password = password }
|
|
102
|
+
const numericPort = Number(port)
|
|
103
|
+
if (Number.isInteger(numericPort) && numericPort >= 1024 && numericPort <= 65535) payload.port = numericPort
|
|
104
|
+
const data = await rpc('lan/start', payload)
|
|
105
|
+
setLan(data); setPassword(''); setNewPassword(''); setChanging(false)
|
|
106
|
+
notify('远程访问已开启:设备访问下方地址并用账号密码登录。' + (data.needsRestart ? '(工作区目录的浏览选择需重启 dsh web 后可用)' : ''), 'success')
|
|
107
|
+
} catch (e) { notify(e && e.message ? e.message : String(e)) } finally { setBusy('') }
|
|
108
|
+
}
|
|
109
|
+
async function disable() {
|
|
110
|
+
notify('')
|
|
111
|
+
setBusy('stop')
|
|
112
|
+
try {
|
|
113
|
+
const data = await rpc('lan/stop', {})
|
|
114
|
+
setLan(data)
|
|
115
|
+
notify('远程访问已关闭,已登录设备全部退出。' + (data.needsRestart ? '重启 dsh web 后恢复工作区原生选择器。' : ''), 'success')
|
|
116
|
+
} catch (e) { notify(e && e.message ? e.message : String(e)) } finally { setBusy('') }
|
|
117
|
+
}
|
|
118
|
+
async function saveAuth() {
|
|
119
|
+
notify('')
|
|
120
|
+
if (!username.trim() || newPassword.length < 6) { notify('请填写账号和至少 6 位的新密码。'); return }
|
|
121
|
+
setBusy('auth')
|
|
122
|
+
try {
|
|
123
|
+
const data = await rpc('auth/set', { username: username.trim(), password: newPassword })
|
|
124
|
+
setLan(data); setPassword(''); setNewPassword(''); setChanging(false)
|
|
125
|
+
notify('账号密码已更新,所有设备需重新登录。', 'success')
|
|
126
|
+
} catch (e) { notify(e && e.message ? e.message : String(e)) } finally { setBusy('') }
|
|
153
127
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
setBusy('note'); setError('')
|
|
157
|
-
try { const data = await rpc('note', Object.assign({}, session, { note })); setPair(data.pair); setNote(''); setNotice('接力备注已同步。') }
|
|
158
|
-
catch (e) { setError(e && e.message ? e.message : String(e)) } finally { setBusy('') }
|
|
128
|
+
function copyLink() {
|
|
129
|
+
copy(lanLink).then(() => notify('地址已复制。', 'success')).catch((e) => notify(e.message))
|
|
159
130
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
try { await rpc('end', session); setCurrent(null); setPair(null); setNotice('手机连接已关闭。') }
|
|
164
|
-
catch (e) { setError(e && e.message ? e.message : String(e)) } finally { setBusy('') }
|
|
131
|
+
function logout() {
|
|
132
|
+
const info = typeof window !== 'undefined' ? window.__DSH_REMOTE__ : null
|
|
133
|
+
if (info && info.logout) window.location.href = info.logout
|
|
165
134
|
}
|
|
166
|
-
function copyLink() { copy(mobileLink).then(() => setNotice('连接链接已复制。')).catch((e) => setError(e.message)) }
|
|
167
135
|
|
|
168
|
-
|
|
169
|
-
<div className="dmr-
|
|
170
|
-
<ol className="dmr-steps"><li><span>1</span><div><strong
|
|
136
|
+
return <section className={'dmr ' + (compact ? 'dmr-compact' : '')}>
|
|
137
|
+
<div className="dmr-status-head"><div><span className="dmr-eyebrow"><i/> 远程访问</span><h3>{active ? '入口已开启' : '从任何设备访问这个 DSH'}</h3><p>{active ? '设备访问下方地址,用账号密码登录即可使用完整 DSH(与本机同一实例,任务进度实时一致)。' : '开启后,局域网/虚拟网设备访问网关地址并登录,即可使用与本机完全一致的 DSH。'}</p></div>{remote && <button type="button" className="dmr-secondary" onClick={logout}><RelayIcon name="close"/>退出登录</button>}</div>
|
|
138
|
+
<ol className="dmr-steps"><li><span>1</span><div><strong>设置账号密码</strong><small>远程登录的唯一凭据,改密后所有设备重新登录。</small></div></li><li><span>2</span><div><strong>开启入口</strong><small>网关监听 0.0.0.0(默认主端口+1,可改),主实例保持仅本机。</small></div></li><li><span>3</span><div><strong>设备访问</strong><small>扫码或输入地址登录;异地组网(Tailscale 等)用组网 IP 直连。</small></div></li></ol>
|
|
139
|
+
{!active ? <div className="dmr-network-grid">
|
|
140
|
+
<label className="dmr-field"><span>账号</span><input value={username} onChange={(e) => setUsername(e.target.value)} autoComplete="username" placeholder="登录账号"/></label>
|
|
141
|
+
<label className="dmr-field"><span>密码</span><input type="password" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="new-password" placeholder={lan && lan.accountSet ? '已设置,留空沿用' : '至少 6 位'}/></label>
|
|
142
|
+
</div> : null}
|
|
171
143
|
<div className="dmr-network-grid">
|
|
172
|
-
<label className="dmr-field"><span
|
|
173
|
-
<
|
|
144
|
+
<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>
|
|
145
|
+
<div className="dmr-security"><strong>安全说明</strong><p>主实例保持仅监听 127.0.0.1:远程设备只能经这个登录网关进入,不存在免登录的直连路径。账号密码只发给自己;改密后所有设备需重新登录。</p></div>
|
|
174
146
|
</div>
|
|
175
|
-
<div className="dmr-
|
|
176
|
-
|
|
177
|
-
{
|
|
147
|
+
{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
|
+
: <button type="button" className="dmr-primary" onClick={enable} disabled={busy === 'start'}>{busy === 'start' ? '正在开启…' : <><RelayIcon name="link"/>开启远程访问</>}</button>}
|
|
149
|
+
{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}
|
|
150
|
+
{message ? <div className={'dmr-message ' + (messageKind === 'success' ? 'success' : 'error')} role="alert">{message}</div> : null}
|
|
178
151
|
</section>
|
|
152
|
+
}
|
|
179
153
|
|
|
180
|
-
|
|
181
|
-
return <
|
|
182
|
-
<div className="dmr-status-head"><div><span className="dmr-eyebrow"><i/> {mobile ? '已连接到电脑' : '手机连接已开启'}</span><h3>{mobile ? '可以在这里继续开发' : '等待手机接入'}</h3><p>{mobile ? '任务状态和接力备注会自动同步。继续开发请回到当前 DSH 会话输入区。' : '将下面的安全链接发到手机;连接在 ' + fmtTime(pair && pair.expiresAt) + ' 前有效。'}</p></div><button type="button" className="dmr-icon-button" onClick={end} disabled={busy === 'end'} title="关闭手机连接" aria-label="关闭手机连接"><RelayIcon name="close"/></button></div>
|
|
183
|
-
{!mobile && <div className="dmr-share"><div className="dmr-share-layout"><div className="dmr-qr-card">{qrDataUrl ? <img src={qrDataUrl} alt="手机接力一次性连接二维码" width="260" height="260"/> : <div className="dmr-qr-loading" aria-live="polite">正在生成二维码…</div>}<strong>使用手机相机扫码</strong><small>手机与电脑需在同一局域网</small></div><div className="dmr-share-detail"><div><span className="dmr-eyebrow"><i/> 反向代理已运行</span><h4>{session.gatewayAddress}:{session.gatewayPort} → 127.0.0.1:3080</h4></div><div className="dmr-link" title={mobileLink}>{mobileLink || '局域网入口未就绪'}</div><div className="dmr-share-actions"><button type="button" className="dmr-primary" onClick={copyLink} disabled={!mobileLink}><RelayIcon name="copy"/>复制连接链接</button></div><small>二维码和链接只可成功验证一次,10 分钟后失效。如需另一台手机,请关闭后重新开启。</small></div></div></div>}
|
|
184
|
-
<div className="dmr-overview"><div className="dmr-overview-card"><span>设备状态</span><strong>{mobile ? '手机已接力' : onlineMobiles.length ? onlineMobiles.length + ' 台手机在线' : '等待手机'}</strong><small>{mobile ? '保持此页面打开即可同步状态' : onlineMobiles.length ? '状态每 2.5 秒刷新' : '连接码仅在当前配对窗口有效'}</small></div><div className="dmr-overview-card"><span>进行中的任务</span><strong>{pair && pair.tasks ? pair.tasks.length : '—'}</strong><small>{pair && pair.tasks && pair.tasks.length ? '与 DSH 当前会话共用同一任务' : '开始任务后会自动显示在这里'}</small></div></div>
|
|
185
|
-
<div className="dmr-section"><div className="dmr-section-head"><h4>实时任务</h4><button type="button" className="dmr-text-button" onClick={() => refresh()} disabled={busy}>刷新</button></div>{pair && pair.tasks && pair.tasks.length ? <div className="dmr-task-list">{pair.tasks.map((task) => <article key={task.sessionId} className="dmr-task"><span className={'dmr-phase ' + task.phase}>{phaseLabel(task.phase)}</span><div><strong>{task.title}</strong><small>已运行 {fmtDuration(task.elapsed)} · 最近活动 {fmtTime(task.lastActivityAt)}</small></div></article>)}</div> : <div className="dmr-empty">当前没有正在运行的任务。启动任务后,手机和电脑都会看到同一份状态。</div>}</div>
|
|
186
|
-
<form className="dmr-note" onSubmit={sendNote}><div className="dmr-section-head"><h4>接力备注</h4>{pair && pair.noteAt ? <small>{pair.noteFrom} · {fmtTime(pair.noteAt)}</small> : null}</div>{pair && pair.note ? <blockquote>{pair.note}</blockquote> : <p>把当前进度、下一步或需要注意的事项留给另一台设备。</p>}<label><span className="sr-only">接力备注</span><textarea value={note} onChange={(e) => setNote(e.target.value)} maxLength="1200" placeholder={mobile ? '例如:我已在手机查看到构建错误,下一步请在电脑运行测试。' : '例如:已经完成接口修改,手机端继续检查页面交互。'} /></label><button className="dmr-secondary" disabled={!note.trim() || busy === 'note'}>{busy === 'note' ? '正在同步…' : <><RelayIcon name="arrow"/>同步备注</>}</button></form>
|
|
187
|
-
{error && <div className="dmr-message error" role="alert">{error}</div>}{notice && <div className="dmr-message success" aria-live="polite"><RelayIcon name="check"/>{notice}</div>}
|
|
188
|
-
</section>
|
|
154
|
+
export function MobileRelayView() {
|
|
155
|
+
return <RemoteCard/>
|
|
189
156
|
}
|
|
190
157
|
|
|
191
158
|
export function MobileRelayHomeStat() {
|
|
192
|
-
const [summary, setSummary] = useState('
|
|
159
|
+
const [summary, setSummary] = useState('未开启,开启后可远程登录')
|
|
193
160
|
useEffect(() => {
|
|
194
|
-
|
|
195
|
-
|
|
161
|
+
rpc('lan').then((data) => {
|
|
162
|
+
setSummary(data.gatewayActive ? '入口运行中 · 端口 ' + data.gatewayPort : data.accountSet ? '账号已设置,入口未开启' : '未开启,开启后可远程登录')
|
|
163
|
+
}).catch(() => {})
|
|
196
164
|
}, [])
|
|
197
165
|
return <span>{summary}</span>
|
|
198
166
|
}
|
|
199
167
|
|
|
200
168
|
export const feature = {
|
|
201
|
-
id: 'mobile-relay', name: '
|
|
202
|
-
description: '
|
|
169
|
+
id: 'mobile-relay', name: '远程访问', order: 80, accent: '#38bdf8',
|
|
170
|
+
description: '账号密码登录的远程入口:所有设备访问同一个 DSH,任务进度实时一致', defaultEnabled: true,
|
|
203
171
|
css: `
|
|
204
|
-
.dmr{--dmr-accent:#38bdf8;--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,#f59e0b 38%,var(--dsw-alias-border-l1));border-radius:12px;background:color-mix(in srgb,#f59e0b 8%,transparent)}.dmr-security strong{display:block;margin-bottom:4px;color:#f59e0b;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:#062238}.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:#f59e0b}.dmr-phase.search{color:#14b8a6}.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}}
|
|
205
|
-
`,
|
|
172
|
+
.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)}`,
|
|
206
173
|
View: MobileRelayView, HomeStat: MobileRelayHomeStat,
|
|
207
174
|
}
|
|
@@ -441,11 +441,11 @@ export const feature = {
|
|
|
441
441
|
description: "编辑各 Provider 模型目录:输入类型(文本/图片 + 标注)与思考强度档位;写回官方配置热生效,会话模型选择器即时可选",
|
|
442
442
|
css: [
|
|
443
443
|
".dkm-note{color:var(--dsw-alias-label-secondary);font-size:12px;}",
|
|
444
|
-
".dkm-warn{color:var(--
|
|
444
|
+
".dkm-warn{color:var(--dk-warn);}",
|
|
445
445
|
".dkm-error{color:var(--dsw-alias-state-error-primary);font-size:12px;}",
|
|
446
446
|
".dkm-chips{display:flex;flex-wrap:wrap;gap:6px;}",
|
|
447
447
|
".dkm-chip{cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);border-radius:999px;padding:3px 12px;font-family:inherit;font-size:12px;}",
|
|
448
|
-
".dkm-chip.on{color:var(--
|
|
448
|
+
".dkm-chip.on{color:var(--dk-accent);border-color:currentColor;}",
|
|
449
449
|
".dkm-prov{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px;}",
|
|
450
450
|
".dkm-prov-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
|
|
451
451
|
".dkm-name{font-weight:600;}",
|
|
@@ -461,7 +461,7 @@ export const feature = {
|
|
|
461
461
|
".dkm-input{box-sizing:border-box;width:100%;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px 8px;font-family:inherit;font-size:12px;}",
|
|
462
462
|
".dkm-checks{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}",
|
|
463
463
|
".dkm-check{display:inline-flex;align-items:center;gap:5px;cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;}",
|
|
464
|
-
".dkm-check input{accent-color:var(--
|
|
464
|
+
".dkm-check input{accent-color:var(--dk-accent);margin:0;}",
|
|
465
465
|
".dkm-tag{color:var(--dsw-alias-label-tertiary);}",
|
|
466
466
|
".dkm-efforts{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}",
|
|
467
467
|
".dkm-del{margin-left:auto;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:8px;padding:2px 10px;font-family:inherit;font-size:12px;flex:none;}",
|
|
@@ -469,7 +469,7 @@ export const feature = {
|
|
|
469
469
|
".dkm-add{cursor:pointer;border:1px dashed var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);border-radius:10px;padding:6px 14px;font-family:inherit;font-size:12px;align-self:flex-start;}",
|
|
470
470
|
".dkm-add:hover{color:var(--dsw-alias-label-primary);}",
|
|
471
471
|
".dkm-savebar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px;}",
|
|
472
|
-
".dkm-save{cursor:pointer;border:1px solid var(--
|
|
472
|
+
".dkm-save{cursor:pointer;border:1px solid var(--dk-accent);color:var(--dk-accent);background:transparent;border-radius:8px;padding:4px 14px;font-family:inherit;font-size:12px;flex:none;}",
|
|
473
473
|
".dkm-save:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
474
474
|
".dkm-save[disabled]{opacity:.5;cursor:default;}",
|
|
475
475
|
".dkm-msg{font-size:12px;color:var(--dsw-alias-label-secondary);}",
|
|
@@ -106,7 +106,7 @@ const css = `
|
|
|
106
106
|
.dtok-btn{cursor:pointer;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);padding:4px 12px;font-size:12px;font-family:inherit;flex:none;}
|
|
107
107
|
.dtok-btn:hover{background:var(--dsw-alias-interactive-bg-hover);}
|
|
108
108
|
.dtok-btn[disabled]{opacity:.5;cursor:default;}
|
|
109
|
-
.dtok-btn.primary{background:var(--
|
|
109
|
+
.dtok-btn.primary{background:var(--dk-accent);border-color:var(--dk-accent);color:#fff;}
|
|
110
110
|
.dtok-btn.primary:hover{filter:brightness(1.1);}
|
|
111
111
|
.dtok-body{display:flex;flex-direction:column;gap:8px;min-width:0;}
|
|
112
112
|
.dtok-cards{display:flex;gap:8px;flex-wrap:wrap;}
|
|
@@ -118,22 +118,22 @@ const css = `
|
|
|
118
118
|
.dtok-table{border-collapse:collapse;width:100%;font-size:12px;white-space:nowrap;color:var(--dsw-alias-label-primary);}
|
|
119
119
|
.dtok-table th{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);text-align:left;padding:7px 10px;position:sticky;top:0;z-index:1;border-bottom:1px solid var(--dsw-alias-border-l1);font-weight:600;user-select:none;cursor:pointer;}
|
|
120
120
|
.dtok-table th:hover{color:var(--dsw-alias-label-primary);}
|
|
121
|
-
.dtok-table td{padding:6px 10px;border-bottom:1px solid
|
|
122
|
-
.dtok-table tr:hover td{background:
|
|
121
|
+
.dtok-table td{padding:6px 10px;border-bottom:1px solid var(--dk-tdim);}
|
|
122
|
+
.dtok-table tr:hover td{background:var(--dk-hover-tint);}
|
|
123
123
|
.dtok-empty{text-align:center;color:var(--dsw-alias-label-secondary);padding:32px 0;font-size:13px;}
|
|
124
|
-
.dtok-sid{color:var(--
|
|
124
|
+
.dtok-sid{color:var(--dk-accent);cursor:pointer;text-decoration:underline dotted;}
|
|
125
125
|
.dtok-sid:hover{filter:brightness(1.25);}
|
|
126
126
|
.dtok-code{font-weight:700;font-family:ui-monospace,monospace;}
|
|
127
127
|
.dtok-code.ok{color:var(--dsw-alias-state-success-primary,#4ade80);}
|
|
128
|
-
.dtok-code.warn{color:var(--
|
|
128
|
+
.dtok-code.warn{color:var(--dk-warn);}
|
|
129
129
|
.dtok-code.err{color:var(--dsw-alias-state-error-primary,#f87171);}
|
|
130
130
|
.dtok-code.pend{color:var(--dsw-alias-label-tertiary,#94a3b8);}
|
|
131
|
-
.dtok-detail-link{color:var(--
|
|
131
|
+
.dtok-detail-link{color:var(--dk-accent);cursor:pointer;font-size:11px;}
|
|
132
132
|
.dtok-detail-link:hover{text-decoration:underline;}
|
|
133
133
|
.dtok-detail{position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;}
|
|
134
134
|
.dtok-detail-card{background:var(--dsw-alias-bg-layer-2,#1c212b);border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:12px;padding:20px 24px;max-width:640px;width:92%;max-height:80vh;overflow:auto;color:var(--dsw-alias-label-primary,#e8eaf0);}
|
|
135
135
|
.dtok-detail-card h3{margin:0 0 12px;font-size:15px;}
|
|
136
|
-
.dtok-detail-row{display:flex;gap:8px;padding:4px 0;font-size:12px;border-bottom:1px solid
|
|
136
|
+
.dtok-detail-row{display:flex;gap:8px;padding:4px 0;font-size:12px;border-bottom:1px solid var(--dk-tdim);}
|
|
137
137
|
.dtok-detail-row .k{color:var(--dsw-alias-label-secondary,#9aa3b5);min-width:110px;flex-shrink:0;}
|
|
138
138
|
.dtok-detail-row .v{word-break:break-all;}
|
|
139
139
|
.dtok-sort-mark{opacity:0.6;margin-left:3px;}
|
|
@@ -542,7 +542,7 @@ export function TokenLogChip(props) {
|
|
|
542
542
|
return (
|
|
543
543
|
<button type="button" className={"dockchip" + (snap.err && !t ? " err" : "")} title={title} aria-label="会话用量"
|
|
544
544
|
onClick={() => openPanel("tokenlog", { sessionId: sid })}>
|
|
545
|
-
<span className="dockchip-dot" style={{ background: "
|
|
545
|
+
<span className="dockchip-dot" style={{ background: "var(--dk-warn)" }} />
|
|
546
546
|
<span>{label}</span>
|
|
547
547
|
</button>
|
|
548
548
|
);
|
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-dock · Host 半部(Node 侧入口)· v0.
|
|
1
|
+
// dsh-dock · Host 半部(Node 侧入口)· v0.9.0 模块化架构
|
|
2
2
|
//
|
|
3
3
|
// 功能坞 = hub + 一个个独立功能模块(像 dsh 本身由小包组成):
|
|
4
4
|
// - 每个功能是一个 features/<id>/ 目录:host.js(宿主半部)+ view.js(x)(客户端视图),
|
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
// - balance 模型余额(v0.2.0):各 Provider 账户余额/配额
|
|
14
14
|
// - tokenlog 用量记录(v0.4.0):LLM 调用记账与统计(移植自 @wycto/dsh-token-usage)
|
|
15
15
|
// - animation 任务动画(v0.5.0):会话任务追踪 + 动效/通知配置持久化(参照 @wycto/dsh-task-pulse)
|
|
16
|
+
// - mobile-relay 手机接力(未发布):扫码反向代理接力 + 局域网电脑直连(0.0.0.0)
|
|
16
17
|
import { DOCK_NS, DockConfig } from './src/host-core.js'
|
|
17
18
|
import { feature as fModels } from './features/modelconfig/host.js'
|
|
18
19
|
import { feature as fVisionProxy } from './features/visionproxy/host.js'
|
|
19
20
|
import { feature as fBalance } from './features/balance/host.js'
|
|
20
21
|
import { feature as fTokenlog } from './features/tokenlog/host.js'
|
|
21
22
|
import { feature as fAnimation } from './features/animation/host.js'
|
|
23
|
+
import { feature as fMobileRelay } from './features/mobile-relay/host.js'
|
|
22
24
|
|
|
23
25
|
export const name = 'dsh-dock'
|
|
24
26
|
|
|
@@ -28,6 +30,22 @@ export const inject = ['webServer', 'llm', 'settings']
|
|
|
28
30
|
/** 测试钩子:清空识别缓存(冒烟在用例间隔离;实现在 visionproxy 模块)。 */
|
|
29
31
|
export { __clearDescribeCache } from './features/visionproxy/host.js'
|
|
30
32
|
|
|
33
|
+
// 宿主进程健壮性护栏:Node 对已半脱离 HTTP 管理的 socket 上到达的传输层重置
|
|
34
|
+
// 没有兜底监听——手机锁屏/切后台、页面跳转中断请求等日常 RST 会以未捕获异常
|
|
35
|
+
// 打崩整个 dsh web(实测:Error: read ECONNRESET, Emitted 'error' on Socket)。
|
|
36
|
+
// 这里只吞 ECONNRESET/EPIPE 这类纯传输层错误(连接本身已死,吞掉等价于 nginx
|
|
37
|
+
// 对客户端断连的处理),其余未捕获异常照常抛出。
|
|
38
|
+
if (typeof process !== 'undefined' && typeof process.on === 'function' && !process.listenerCount('uncaughtException')) {
|
|
39
|
+
process.on('uncaughtException', (err) => {
|
|
40
|
+
const code = err && err.code
|
|
41
|
+
if (code === 'ECONNRESET' || code === 'EPIPE') {
|
|
42
|
+
console.error('[dsh-dock] swallowed transport error:', code, (err && err.message) || err)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
throw err
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
export function apply(ctx) {
|
|
32
50
|
// ---- 功能注册表(Host 侧):每个条目来自对应功能模块的 feature 描述符 ----
|
|
33
51
|
// defaultEnabled:与 Client 半部保持一致。已接入的功能默认打开,
|
|
@@ -38,6 +56,7 @@ export function apply(ctx) {
|
|
|
38
56
|
fBalance,
|
|
39
57
|
fTokenlog,
|
|
40
58
|
fAnimation,
|
|
59
|
+
fMobileRelay,
|
|
41
60
|
]
|
|
42
61
|
|
|
43
62
|
const state = new Map()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dock",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计)、模型设置、图片理解代理、模型余额、任务动画(8 种氛围动效 + 桌面伙伴 3D 场景、完成通知与提示音、钉钉/飞书推送,开关独立、配置持久化);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"access": "public"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
+
"js-yaml": "^4.1.0",
|
|
52
53
|
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
|
|
53
54
|
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
54
55
|
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
|