dsh-remote-plugin 0.6.6 → 0.6.8
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.en.md +31 -25
- package/README.md +30 -24
- package/apk/dsh-remote.apk +0 -0
- package/client.js +11 -9
- package/gateway.cjs +119 -1
- package/index.mjs +10 -1
- package/package.json +1 -1
- package/public/admin.html +103 -7
- package/public/admin.js +24 -0
- package/public/announcements.json +12 -0
- package/public/app.js +850 -57
- package/public/desktop/desktop.css +170 -20
- package/public/desktop/desktop.html +102 -13
- package/public/desktop/desktop.js +422 -26
- package/public/desktop/i18n.js +33 -1
- package/public/index.html +200 -40
- package/public/plugin.html +151 -0
- package/public/plugin.js +179 -0
- package/public/styles.css +220 -15
- package/public/theme-vars.css +9 -0
- package/public/update.json +12 -4
- package/public/version.json +1 -1
package/README.en.md
CHANGED
|
@@ -1,46 +1,52 @@
|
|
|
1
1
|
# dsh-remote-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The official DSH bundle plugin for DSH Remote. It adds a DSH sidebar entry, a compact status panel, a full admin console, and a gateway managed alongside DSH.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
The DSH bundle plugin for DSH Remote: registers a native entry in DSH's left sidebar and opens an admin drawer from the right. The plugin **bundles the gateway and auto-starts/stops it with DSH** (standalone systemd unit); the drawer shows the token, host IP and device monitor. Works with the [dsh-Remote](https://github.com/Blank-not-black/dsh-Remote) Android app for remote control and file transfer (`/fs/list`, `/fs/file`, `/fs/upload`). The panel ships four themes (Deep Space / Sunset / Elbphilharmonie / Prairie Tower) — light / dark / neutral, following the system preference automatically.
|
|
5
|
+
[English](README.en.md) · [中文](README.md)
|
|
8
6
|
|
|
9
7
|
## Install
|
|
10
8
|
|
|
11
9
|
```sh
|
|
12
10
|
dsh plugin --profile web add dsh-remote-plugin
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
|
|
12
|
+
# Optional version pin
|
|
13
|
+
dsh plugin --profile web add dsh-remote-plugin@0.6.8
|
|
15
14
|
```
|
|
16
15
|
|
|
17
|
-
Restart DSH Web and
|
|
16
|
+
Restart DSH Web and refresh the browser. The DSH Remote entry will appear in the sidebar.
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
## What the plugin provides
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
- Compact panel: gateway state, connected devices, token usage, and quick actions.
|
|
21
|
+
- Admin console: port, upstream, devices, requests, token statistics, QR pairing, and token rotation.
|
|
22
|
+
- Bundled `gateway.cjs`: Bearer-token gateway listening on `0.0.0.0:8787` by default.
|
|
23
|
+
- Self-healing lifecycle: relaunches the gateway after a DSH restart or unexpected exit; start and stop it from the panel.
|
|
24
|
+
- `/fs/*` file endpoints: list, download, chunked upload, resume, pause/continue/cancel, and SHA-256 verification.
|
|
25
|
+
- Mobile, desktop, and admin WebUI assets, plus the Android APK distributed with the plugin.
|
|
26
|
+
|
|
27
|
+
## Mobile capabilities
|
|
28
|
+
|
|
29
|
+
The Android app / mobile WebUI has five main destinations: Sessions, Files, Home, Stats, and Settings. Session detail supports goals, subagent interruption, model selection, fullscreen input, slash commands, and image attachments. Images can come from the camera or gallery and are sent as image content in `session.prompt`.
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
Notification settings include approval / question notifications, background polling, peak reminders, task completion notices, and announcement history. Four themes are retained: Default Deep Space, Sunset, Elbphilharmonie, and Prairie Tower.
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
- On/off intent persists in `~/.dsh-remote/gateway.enabled`; it can be stopped/started from the drawer.
|
|
29
|
-
- Token lives in `~/.dsh-remote/token` (auto-generated on first run, reused and never overwritten), shown in the drawer and copyable; supports **QR pairing** and **one-click rotation**.
|
|
30
|
-
- Env var `DSH_REMOTE_AUTOSTART=0` disables auto management.
|
|
31
|
-
- File endpoints: `/fs/list` (list directory), `/fs/file` (download with Range support), `/fs/upload` (chunked resume with pause/cancel, SHA-256 verified before writing to disk); default root is `~`, and `DSH_REMOTE_FS_ROOT` opens multiple roots (`:` on Linux/macOS, `;` on Windows).
|
|
32
|
-
- Feedback endpoint: `POST /feedback` (the app / desktop "Write feedback" dialog), forwarded by the gateway to the feedback collector; default `http://100.84.128.29/submit` (Tailscale internal network), overridable via `DSH_REMOTE_FEEDBACK_URL` — no tokens to configure.
|
|
33
|
+
## Gateway configuration
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
- Port priority: `DSH_REMOTE_GATEWAY_PORT` → `~/.dsh-remote/gateway-port` → `8787`.
|
|
36
|
+
- Token: `~/.dsh-remote/token`, generated on first run.
|
|
37
|
+
- Self-healing: `~/.dsh-remote/gateway.enabled`; use `DSH_REMOTE_AUTOSTART=0` to disable automatic management.
|
|
38
|
+
- File roots: `DSH_REMOTE_FS_ROOT`, separated by `:` on Linux/macOS and `;` on Windows.
|
|
39
|
+
- Upload limit: `DSH_REMOTE_FS_MAX_UPLOAD`, 2 GB by default.
|
|
40
|
+
- DSH upstream: `http://127.0.0.1:3080` by default.
|
|
35
41
|
|
|
36
|
-
The
|
|
42
|
+
The token grants remote control of DSH. Keep it private. For cross-network access, prefer Tailscale or another authenticated secure tunnel.
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
- Or open `http://<gateway-IP>:8787` in a browser to download
|
|
40
|
-
- App updates are pushed by the gateway too (`update.json` relative path), never touching GitHub
|
|
41
|
-
- A desktop browser opening `http://<gateway-IP>:8787` auto-enters the desktop WebUI (sidebar sessions + files + settings + stats + approval notification stack)
|
|
44
|
+
## Links
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
- Admin page: `http://<gateway-ip>:8787/admin`
|
|
47
|
+
- Desktop WebUI: `http://<gateway-ip>:8787`
|
|
48
|
+
- Main project: [dsh-Remote](https://github.com/Blank-not-black/dsh-Remote)
|
|
49
|
+
- Stable releases: [GitHub Releases](https://github.com/Blank-not-black/dsh-Remote/releases/latest)
|
|
44
50
|
|
|
45
51
|
## License
|
|
46
52
|
|
package/README.md
CHANGED
|
@@ -1,46 +1,52 @@
|
|
|
1
1
|
# dsh-remote-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DSH Remote 的官方 bundle 插件:在 DSH 侧边栏提供入口,打开快速状态面板和完整管理控制台,并内置随 DSH 自动启停的远程网关。
|
|
4
4
|
|
|
5
5
|
**中文** · [English](README.en.md)
|
|
6
6
|
|
|
7
|
-
DSH Remote 的 DSH bundle 插件:在 DSH 左侧原生边栏注册入口,点击从右侧滑出管理抽屉;插件**内置网关程序并随 DSH 自动启停**(独立 systemd 单元),抽屉直显令牌、主机 IP 与设备监控,配合 [dsh-Remote](https://github.com/Blank-not-black/dsh-Remote) 的 Android App 实现手机远程操控与文件互传(`/fs/list`、`/fs/file`、`/fs/upload`)。面板内置四套皮肤(深空 / 落日 / 易北爱乐厅 / 草原孤塔),亮暗中性三档随系统偏好自动切换。
|
|
8
|
-
|
|
9
7
|
## 安装
|
|
10
8
|
|
|
11
9
|
```sh
|
|
12
10
|
dsh plugin --profile web add dsh-remote-plugin
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
|
|
12
|
+
# 也可以安装指定版本
|
|
13
|
+
dsh plugin --profile web add dsh-remote-plugin@0.6.8
|
|
15
14
|
```
|
|
16
15
|
|
|
17
|
-
重启 DSH Web
|
|
16
|
+
重启 DSH Web 后刷新浏览器,左侧边栏会出现 DSH Remote 入口。
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
## 插件提供什么
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
- 快速面板:网关状态、在线设备、Token 用量和快捷操作;
|
|
21
|
+
- 管理控制台:端口、上游、设备、请求、Token 统计、二维码和令牌轮换;
|
|
22
|
+
- 内置 `gateway.cjs`:默认监听 `0.0.0.0:8787`,带 Bearer token 鉴权;
|
|
23
|
+
- 网关自愈:DSH 重启或网关意外退出后自动拉起,可在面板中停止或启动;
|
|
24
|
+
- `/fs/*` 文件端点:列表、下载、分块上传、断点续传、暂停/继续/取消和 SHA-256 校验;
|
|
25
|
+
- 手机端、桌面端和管理页 WebUI,以及随插件分发的 Android APK。
|
|
26
|
+
|
|
27
|
+
## 手机端能力
|
|
28
|
+
|
|
29
|
+
Android 应用 / 手机 WebUI 采用五个主要页面:会话、文件、主页、统计、设置。会话详情支持目标控制、子代理中断、模型切换、全屏输入、斜杠命令和图片附件。图片附件可从相机或相册选择,并以 `session.prompt` 图片内容发送到当前会话。
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
通知设置支持审批 / 提问通知、后台轮询、峰谷提醒、任务完成提醒和历史公告。当前保留四套主题:默认深空、落日、易北爱乐厅、草原孤塔。
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
- 开关持久化在 `~/.dsh-remote/gateway.enabled`;抽屉内可停止/启动。
|
|
29
|
-
- 令牌在 `~/.dsh-remote/token`(首次自动生成,重复使用不覆盖),抽屉里显示并可复制;支持**二维码扫码配对**与**一键轮换**。
|
|
30
|
-
- 环境变量 `DSH_REMOTE_AUTOSTART=0` 可关闭自动管理。
|
|
31
|
-
- 文件端点:`/fs/list`(列目录)、`/fs/file`(下载,支持 Range)、`/fs/upload`(分块续传,支持暂停/取消,落盘前 SHA-256 校验);默认根目录 `~`,`DSH_REMOTE_FS_ROOT` 可开多根(Linux/macOS 用 `:`,Windows 用 `;` 分隔)。
|
|
32
|
-
- 反馈端点:`POST /feedback`(App / 桌面端「写反馈」),网关转发到反馈收集器;默认 `http://100.84.128.29/submit`(Tailscale 内网),可用 `DSH_REMOTE_FEEDBACK_URL` 覆盖,无需配置任何 token。
|
|
33
|
+
## 网关配置
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
- 网关端口优先级:`DSH_REMOTE_GATEWAY_PORT` 环境变量 → `~/.dsh-remote/gateway-port` → `8787`;
|
|
36
|
+
- 令牌:`~/.dsh-remote/token`,首次运行自动生成;
|
|
37
|
+
- 自愈开关:`~/.dsh-remote/gateway.enabled`,或使用 `DSH_REMOTE_AUTOSTART=0` 禁用自动管理;
|
|
38
|
+
- 文件根目录:`DSH_REMOTE_FS_ROOT`,Linux/macOS 使用 `:`,Windows 使用 `;`;
|
|
39
|
+
- 文件上限:`DSH_REMOTE_FS_MAX_UPLOAD`,默认 2GB;
|
|
40
|
+
- DSH 上游:默认 `http://127.0.0.1:3080`。
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
令牌等同于 DSH 远程操作凭证,请不要公开或提交到仓库。局域网访问建议配合防火墙;跨网络访问建议使用 Tailscale 或其他带认证的安全隧道。
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
- 或浏览器直接访问 `http://<网关IP>:8787` 下载
|
|
40
|
-
- App 更新同样走网关自动推送(`update.json` 相对路径),全程不绕 GitHub
|
|
41
|
-
- 电脑浏览器打开 `http://<网关IP>:8787` 自动进入桌面端 WebUI(侧栏会话 + 文件 + 设置 + 统计 + 审批通知卡片栈)
|
|
44
|
+
## 相关地址
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
- 管理页:`http://<网关IP>:8787/admin`
|
|
47
|
+
- 桌面 WebUI:`http://<网关IP>:8787`
|
|
48
|
+
- 主项目:[dsh-Remote](https://github.com/Blank-not-black/dsh-Remote)
|
|
49
|
+
- 正式版本:[GitHub Releases](https://github.com/Blank-not-black/dsh-Remote/releases/latest)
|
|
44
50
|
|
|
45
51
|
## License
|
|
46
52
|
|
package/apk/dsh-remote.apk
CHANGED
|
Binary file
|
package/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* dsh-remote 插件 client half
|
|
2
2
|
* 注册进 DSH 原生左侧边栏 footer(与 OpenBiliClaw 上下并列) + 右侧 shell.overlay 抽屉。
|
|
3
|
-
* 抽屉内 iframe 懒加载 /remote/
|
|
3
|
+
* 抽屉内 iframe 懒加载 /remote/plugin.html —— 插件端快速状态面板;深入管理再打开控制台。
|
|
4
4
|
* 产物入库, 无构建步骤; 参考 @openbiliclaw/dsh-plugin 的官方 slot 注册模式。
|
|
5
5
|
*/
|
|
6
6
|
window.__ModuleLoader__.load({
|
|
@@ -13,9 +13,10 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
var runtime = require('@deepseek-ai/dsh-client-runtime/client')
|
|
14
14
|
|
|
15
15
|
var DRAWER_STYLE = {
|
|
16
|
-
position: 'fixed', top:
|
|
17
|
-
width: 'min(
|
|
18
|
-
|
|
16
|
+
position: 'fixed', top: 12, right: 12, bottom: 12, zIndex: 2147483000,
|
|
17
|
+
width: 'min(520px, calc(100vw - 24px))', background: '#0b0e1a',
|
|
18
|
+
border: '1px solid rgba(125, 207, 255, .22)', borderRadius: 24, overflow: 'hidden',
|
|
19
|
+
boxShadow: '-16px 18px 48px rgba(0,0,0,.46), 0 0 0 1px rgba(91,140,255,.08)',
|
|
19
20
|
transform: 'translateX(102%)', transition: 'transform .22s ease',
|
|
20
21
|
display: 'flex', flexDirection: 'column',
|
|
21
22
|
}
|
|
@@ -51,8 +52,9 @@ window.__ModuleLoader__.load({
|
|
|
51
52
|
style: {
|
|
52
53
|
display: 'flex', alignItems: 'center', gap: 7, width: '100%',
|
|
53
54
|
padding: '6px 10px', border: 'none', borderRadius: 8,
|
|
54
|
-
background:
|
|
55
|
-
|
|
55
|
+
background: open ? 'rgba(125, 207, 255, .14)' : hover ? 'rgba(125, 207, 255, .10)' : 'transparent',
|
|
56
|
+
border: open ? '1px solid rgba(125, 207, 255, .24)' : '1px solid transparent',
|
|
57
|
+
transition: 'background .15s ease, border-color .15s ease',
|
|
56
58
|
cursor: 'pointer', color: open ? '#5df2d6' : 'inherit',
|
|
57
59
|
font: '500 13px/1.3 system-ui, sans-serif', textAlign: 'left',
|
|
58
60
|
},
|
|
@@ -73,7 +75,7 @@ window.__ModuleLoader__.load({
|
|
|
73
75
|
function onMsg(e) {
|
|
74
76
|
if (e.origin !== location.origin) return
|
|
75
77
|
var d = e.data
|
|
76
|
-
if (d && d.source === 'dsh-remote-admin' && d.type === 'close') props.actions.close()
|
|
78
|
+
if (d && (d.source === 'dsh-remote-admin' || d.source === 'dsh-remote-plugin') && d.type === 'close') props.actions.close()
|
|
77
79
|
}
|
|
78
80
|
window.addEventListener('message', onMsg)
|
|
79
81
|
return function () { window.removeEventListener('message', onMsg) }
|
|
@@ -83,8 +85,8 @@ window.__ModuleLoader__.load({
|
|
|
83
85
|
style: Object.assign({}, DRAWER_STYLE, { transform: open ? 'translateX(0)' : 'translateX(102%)' }),
|
|
84
86
|
},
|
|
85
87
|
loaded.current ? React.createElement('iframe', {
|
|
86
|
-
src: '/remote/
|
|
87
|
-
title: 'DSH Remote',
|
|
88
|
+
src: '/remote/plugin.html',
|
|
89
|
+
title: 'DSH Remote 快速状态面板',
|
|
88
90
|
sandbox: 'allow-scripts allow-same-origin allow-forms allow-modals allow-popups',
|
|
89
91
|
style: { flex: 1, width: '100%', border: 'none', background: '#0b0e1a' },
|
|
90
92
|
}) : null)
|
package/gateway.cjs
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* TOKEN_FILE 令牌文件, 默认 ~/.dsh-remote/token
|
|
21
21
|
* DSH_REMOTE_FS_ROOT 文件传输允许根, 默认 ~, 使用系统路径分隔符配置多根
|
|
22
22
|
* DSH_REMOTE_FS_MAX_UPLOAD 上传字节上限, 默认 2147483648 (2GB)
|
|
23
|
+
* DSH_REMOTE_WORKBENCH 工作台绑定文件, 默认 ~/.dsh-remote/workbench.json
|
|
23
24
|
*/
|
|
24
25
|
'use strict'
|
|
25
26
|
|
|
@@ -48,6 +49,7 @@ const WS_IDLE_MS = Number(process.env.GATEWAY_WS_IDLE_MS) || 60000
|
|
|
48
49
|
const UPSTREAM = new URL(process.env.DSH_UPSTREAM || 'http://127.0.0.1:3080')
|
|
49
50
|
const TOKEN_FILE = process.env.TOKEN_FILE || path.join(os.homedir(), '.dsh-remote', 'token')
|
|
50
51
|
const NOTES_FILE = process.env.DSH_REMOTE_NOTES || path.join(os.homedir(), '.dsh-remote', 'device-notes.json')
|
|
52
|
+
const WORKBENCH_FILE = process.env.DSH_REMOTE_WORKBENCH || path.join(os.homedir(), '.dsh-remote', 'workbench.json')
|
|
51
53
|
const STARTED_AT = Date.now()
|
|
52
54
|
const DSH_SERVICE = String(process.env.DSH_REMOTE_DSH_SERVICE || 'dsh-web').trim()
|
|
53
55
|
|
|
@@ -914,11 +916,20 @@ function serveStatic(req, res, url) {
|
|
|
914
916
|
return
|
|
915
917
|
}
|
|
916
918
|
const ext = path.extname(filePath).toLowerCase()
|
|
919
|
+
const lastModified = st.mtime.toUTCString()
|
|
920
|
+
const mtimeSec = Math.floor(st.mtime.getTime() / 1000) * 1000
|
|
917
921
|
cors(res)
|
|
922
|
+
const ims = req.headers['if-modified-since']
|
|
923
|
+
if (ims && new Date(ims).getTime() >= mtimeSec) {
|
|
924
|
+
res.writeHead(304, { 'last-modified': lastModified })
|
|
925
|
+
res.end()
|
|
926
|
+
return
|
|
927
|
+
}
|
|
918
928
|
res.writeHead(200, {
|
|
919
929
|
'content-type': MIME[ext] || 'application/octet-stream',
|
|
920
930
|
'cache-control': ext === '.html' || ext === '.js' || ext === '.css' ? 'no-cache' : 'public, max-age=300',
|
|
921
|
-
'content-length': st.size
|
|
931
|
+
'content-length': st.size,
|
|
932
|
+
'last-modified': lastModified
|
|
922
933
|
})
|
|
923
934
|
if (req.method === 'HEAD') res.end()
|
|
924
935
|
else fs.createReadStream(filePath).pipe(res)
|
|
@@ -1753,6 +1764,112 @@ function serveFs(req, res, url) {
|
|
|
1753
1764
|
fsJson(res, 404, { error: 'not-found' })
|
|
1754
1765
|
}
|
|
1755
1766
|
|
|
1767
|
+
// ---------- /workbench 工作台绑定 ----------
|
|
1768
|
+
// 工作台绑定一个文件夹;其下的子文件夹由客户端映射为 DSH 项目工作区。
|
|
1769
|
+
function workbenchPathInfo(rawPath) {
|
|
1770
|
+
if (typeof rawPath !== 'string' || !path.isAbsolute(rawPath)) return { error: 'bad-path' }
|
|
1771
|
+
const abs = path.resolve(rawPath)
|
|
1772
|
+
let st
|
|
1773
|
+
try { st = fs.statSync(abs) } catch (err) {
|
|
1774
|
+
return { error: err.code === 'ENOENT' ? 'not-found' : 'permission-denied' }
|
|
1775
|
+
}
|
|
1776
|
+
if (!st.isDirectory()) return { error: 'not-a-directory' }
|
|
1777
|
+
const checked = fsRealChecked(abs)
|
|
1778
|
+
if (checked.error) return { error: checked.error === 'forbidden' ? 'outside-roots' : checked.error }
|
|
1779
|
+
return { path: checked.abs }
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function loadWorkbench() {
|
|
1783
|
+
try {
|
|
1784
|
+
const raw = JSON.parse(fs.readFileSync(WORKBENCH_FILE, 'utf8'))
|
|
1785
|
+
if (!raw || typeof raw.path !== 'string' || !raw.path) return null
|
|
1786
|
+
const checked = workbenchPathInfo(raw.path)
|
|
1787
|
+
return checked.path ? { path: checked.path } : null
|
|
1788
|
+
} catch {
|
|
1789
|
+
return null
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
function saveWorkbench(binding) {
|
|
1794
|
+
try {
|
|
1795
|
+
fs.mkdirSync(path.dirname(WORKBENCH_FILE), { recursive: true })
|
|
1796
|
+
fs.writeFileSync(WORKBENCH_FILE, JSON.stringify(binding, null, 2) + '\n')
|
|
1797
|
+
return true
|
|
1798
|
+
} catch {
|
|
1799
|
+
return false
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function serveWorkbench(req, res, url) {
|
|
1804
|
+
const sub = url.pathname.slice('/workbench'.length)
|
|
1805
|
+
if (req.method === 'OPTIONS') {
|
|
1806
|
+
cors(res)
|
|
1807
|
+
res.writeHead(204)
|
|
1808
|
+
res.end()
|
|
1809
|
+
return
|
|
1810
|
+
}
|
|
1811
|
+
if (!fsAuthorized(req, url, res)) return
|
|
1812
|
+
|
|
1813
|
+
if (sub === '' && req.method === 'GET') {
|
|
1814
|
+
const binding = loadWorkbench()
|
|
1815
|
+
fsJson(res, 200, {
|
|
1816
|
+
bound: !!binding,
|
|
1817
|
+
path: binding?.path || null,
|
|
1818
|
+
title: binding ? path.basename(binding.path) : null
|
|
1819
|
+
})
|
|
1820
|
+
return
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
if (sub === '/bind' && req.method === 'POST') {
|
|
1824
|
+
let body = ''
|
|
1825
|
+
let done = false
|
|
1826
|
+
const fail = (status, payload) => {
|
|
1827
|
+
if (done || res.headersSent) return
|
|
1828
|
+
done = true
|
|
1829
|
+
fsJson(res, status, payload)
|
|
1830
|
+
}
|
|
1831
|
+
req.on('data', chunk => {
|
|
1832
|
+
if (done) return
|
|
1833
|
+
body += chunk
|
|
1834
|
+
if (Buffer.byteLength(body) > 4096) {
|
|
1835
|
+
req.destroy()
|
|
1836
|
+
fail(413, { error: 'too-large' })
|
|
1837
|
+
}
|
|
1838
|
+
})
|
|
1839
|
+
req.on('error', () => { if (!done) done = true })
|
|
1840
|
+
req.on('end', () => {
|
|
1841
|
+
if (done) return
|
|
1842
|
+
try {
|
|
1843
|
+
const rawPath = JSON.parse(body || '{}')?.path
|
|
1844
|
+
const checked = workbenchPathInfo(rawPath)
|
|
1845
|
+
if (checked.error) {
|
|
1846
|
+
const status = checked.error === 'forbidden' ? 403 : 400
|
|
1847
|
+
fail(status, { error: checked.error, detail: checked.error === 'outside-roots' ? '绑定目录必须在文件传输允许根目录内' : undefined })
|
|
1848
|
+
return
|
|
1849
|
+
}
|
|
1850
|
+
if (!saveWorkbench({ path: checked.path })) {
|
|
1851
|
+
fail(500, { error: 'save-failed' })
|
|
1852
|
+
return
|
|
1853
|
+
}
|
|
1854
|
+
done = true
|
|
1855
|
+
fsJson(res, 200, { bound: true, path: checked.path, title: path.basename(checked.path) })
|
|
1856
|
+
} catch {
|
|
1857
|
+
fail(400, { error: 'bad-request' })
|
|
1858
|
+
}
|
|
1859
|
+
})
|
|
1860
|
+
return
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
if (sub === '/unbind' && req.method === 'POST') {
|
|
1864
|
+
try { fs.rmSync(WORKBENCH_FILE, { force: true }) } catch {}
|
|
1865
|
+
fsJson(res, 200, { bound: false })
|
|
1866
|
+
return
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
res.writeHead(405, { allow: 'GET, POST' })
|
|
1870
|
+
res.end()
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1756
1873
|
// ---------- /api 代理 ----------
|
|
1757
1874
|
function proxyApi(req, res, url) {
|
|
1758
1875
|
if (req.method === 'OPTIONS') {
|
|
@@ -1851,6 +1968,7 @@ const server = http.createServer((req, res) => {
|
|
|
1851
1968
|
try {
|
|
1852
1969
|
const url = new URL(req.url, 'http://dsh-remote.local')
|
|
1853
1970
|
if (url.pathname === '/fs' || url.pathname.startsWith('/fs/')) return serveFs(req, res, url)
|
|
1971
|
+
if (url.pathname === '/workbench' || url.pathname.startsWith('/workbench/')) return serveWorkbench(req, res, url)
|
|
1854
1972
|
if (url.pathname === '/feedback') return serveFeedback(req, res, url)
|
|
1855
1973
|
if (url.pathname.startsWith('/admin/api')) return serveAdminApi(req, res, url)
|
|
1856
1974
|
if (url.pathname.startsWith('/stats')) return serveStats(req, res, url)
|
package/index.mjs
CHANGED
|
@@ -150,7 +150,7 @@ function runExit(cmd, args) {
|
|
|
150
150
|
|
|
151
151
|
const GATEWAY_ENV_KEYS = [
|
|
152
152
|
'TOKEN', 'TOKEN_FILE', 'DSH_REMOTE_TOKEN', 'DSH_REMOTE_FS_ROOT', 'DSH_REMOTE_FS_MAX_UPLOAD',
|
|
153
|
-
'DSH_REMOTE_NOTES', 'DSH_REMOTE_DSH_SERVICE', 'DSH_REMOTE_FEEDBACK_URL',
|
|
153
|
+
'DSH_REMOTE_NOTES', 'DSH_REMOTE_WORKBENCH', 'DSH_REMOTE_DSH_SERVICE', 'DSH_REMOTE_FEEDBACK_URL',
|
|
154
154
|
'UPDATE_CHECK_URL', 'UPDATE_INTERVAL_MS', 'UPDATE_PROXY', 'GATEWAY_WS_IDLE_MS',
|
|
155
155
|
'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY',
|
|
156
156
|
'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy'
|
|
@@ -704,10 +704,19 @@ async function serveStatic(req, res, ctx) {
|
|
|
704
704
|
return
|
|
705
705
|
}
|
|
706
706
|
const { abs, info } = found
|
|
707
|
+
const lastModified = info.mtime.toUTCString()
|
|
708
|
+
const mtimeSec = Math.floor(info.mtime.getTime() / 1000) * 1000
|
|
709
|
+
const ims = req.headers['if-modified-since']
|
|
710
|
+
if (ims && new Date(ims).getTime() >= mtimeSec) {
|
|
711
|
+
res.writeHead(304, { 'last-modified': lastModified })
|
|
712
|
+
res.end()
|
|
713
|
+
return
|
|
714
|
+
}
|
|
707
715
|
res.writeHead(200, {
|
|
708
716
|
'content-type': MIME[extname(abs)] ?? 'application/octet-stream',
|
|
709
717
|
'content-length': info.size,
|
|
710
718
|
'cache-control': 'no-cache',
|
|
719
|
+
'last-modified': lastModified,
|
|
711
720
|
})
|
|
712
721
|
if (req.method === 'HEAD') {
|
|
713
722
|
res.end()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-plugin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
4
4
|
"description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
package/public/admin.html
CHANGED
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
padding: 7px 8px; border-radius: 10px; border: none; background: transparent; color: var(--dsr-text);
|
|
146
146
|
font: inherit; font-size: 13px; text-align: left; cursor: pointer; text-decoration: none;
|
|
147
147
|
}
|
|
148
|
-
.fb-item:hover, .fb-item:focus-visible { background: var(--dsr-bg);
|
|
148
|
+
.fb-item:hover, .fb-item:focus-visible { background: var(--dsr-bg); }
|
|
149
149
|
.fb-item.primary { background: var(--dsr-accent-soft); border: 1px solid var(--dsr-accent-line); }
|
|
150
150
|
.fb-ico {
|
|
151
151
|
width: 32px; height: 32px; border-radius: 9px; flex-shrink: 0;
|
|
@@ -174,6 +174,86 @@
|
|
|
174
174
|
.admin-title .right { gap: 4px; }
|
|
175
175
|
.tb-btn { padding: 0 8px; }
|
|
176
176
|
}
|
|
177
|
+
.login-card input:focus-visible, .gateway-port-row input:focus-visible {
|
|
178
|
+
outline: 2px solid var(--dsr-accent);
|
|
179
|
+
outline-offset: 2px;
|
|
180
|
+
}
|
|
181
|
+
@media (prefers-reduced-motion: reduce) {
|
|
182
|
+
*, *::before, *::after {
|
|
183
|
+
animation-duration: .01ms !important;
|
|
184
|
+
animation-iteration-count: 1 !important;
|
|
185
|
+
transition-duration: .01ms !important;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* 控制台面板:减少整面纯色,建立“工具台”层次 */
|
|
190
|
+
html, body { font-family: system-ui, -apple-system, "PingFang SC", "Noto Sans CJK SC", sans-serif; text-rendering: optimizeLegibility; }
|
|
191
|
+
body { background: radial-gradient(900px 520px at 92% -12%, var(--dsr-accent-soft), transparent 68%), linear-gradient(180deg, var(--dsr-bg), var(--dsr-bg-2)); }
|
|
192
|
+
.admin-wrap { max-width: 1320px; padding: calc(env(safe-area-inset-top, 0px) + 18px) clamp(14px, 3vw, 30px) 48px; }
|
|
193
|
+
.admin-title { margin-bottom: 18px; padding: 12px 14px; border: 1px solid var(--dsr-line); border-radius: 18px; background: linear-gradient(135deg, var(--dsr-panel), var(--dsr-bg-2)); box-shadow: 0 14px 36px var(--dsr-shadow); backdrop-filter: blur(16px); }
|
|
194
|
+
.admin-title h1 { font-size: 21px; letter-spacing: -.2px; }
|
|
195
|
+
.admin-title .left, .admin-title .right { gap: 7px; }
|
|
196
|
+
.tb-btn { border-radius: 11px; background: linear-gradient(135deg, var(--dsr-panel-2), var(--dsr-bg-2)); border-color: var(--dsr-line); transition: transform .18s ease, border-color .18s ease, background .18s ease; }
|
|
197
|
+
.tb-btn:hover { transform: translateY(-1px); border-color: var(--dsr-accent-line); background: var(--dsr-accent-soft); }
|
|
198
|
+
.tb-btn svg { width: 17px; height: 17px; }
|
|
199
|
+
.login-card, .stat-card, .stats-chart, .table-wrap, .gateway-port-row, .pair-box { background: linear-gradient(145deg, var(--dsr-panel), var(--dsr-bg-2)); border-color: var(--dsr-line); border-radius: 18px; box-shadow: 0 12px 30px var(--dsr-shadow); }
|
|
200
|
+
.login-card { padding: 18px; }
|
|
201
|
+
.stat-grid { gap: 12px; }
|
|
202
|
+
.stat-card { padding: 15px 16px; box-shadow: inset 0 1px var(--dsr-divider), 0 10px 26px var(--dsr-shadow); }
|
|
203
|
+
.stat-card .v { font-size: 24px; font-variant-numeric: tabular-nums; }
|
|
204
|
+
.stats-dash { margin-top: 4px; }
|
|
205
|
+
.stats-chart { height: 210px; padding: 16px 18px 12px; }
|
|
206
|
+
.stats-bar .bars { border-radius: 9px 9px 3px 3px; box-shadow: 0 0 16px var(--dsr-glow); }
|
|
207
|
+
.section-head { margin: 18px 2px 9px; font-weight: 750; letter-spacing: .2px; }
|
|
208
|
+
.token-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 13px; margin: 0 0 14px; border: 1px solid var(--dsr-line); border-radius: 18px; background: linear-gradient(145deg, var(--dsr-panel), var(--dsr-bg-2)); box-shadow: 0 10px 28px var(--dsr-shadow); }
|
|
209
|
+
.token-row code { width: auto; min-width: 0; padding: 10px 12px; border-radius: 11px; background: var(--dsr-bg); border-color: var(--dsr-line); }
|
|
210
|
+
.token-actions { justify-content: flex-end; }
|
|
211
|
+
.token-actions .mini-btn { flex: 0 0 auto; border-radius: 10px; }
|
|
212
|
+
.gateway-port-row { padding: 12px 14px; }
|
|
213
|
+
.table-wrap { overflow: hidden; }
|
|
214
|
+
th, td { border-bottom-color: var(--dsr-divider); }
|
|
215
|
+
tbody tr { transition: background .18s ease; }
|
|
216
|
+
.pair-box { padding: 18px; }
|
|
217
|
+
.admin-hero { display:flex; align-items:flex-end; justify-content:space-between; gap:18px; padding:20px; margin:0 0 16px; border:1px solid var(--dsr-accent-line); border-radius:20px; background:linear-gradient(135deg, var(--dsr-panel-2), var(--dsr-bg-2)); box-shadow:0 16px 34px var(--dsr-shadow); }
|
|
218
|
+
.admin-hero-copy { min-width:0; }
|
|
219
|
+
.admin-eyebrow { color:var(--dsr-accent-strong); font-size:10px; font-weight:800; letter-spacing:1.6px; }
|
|
220
|
+
.admin-hero h2 { margin:7px 0 4px; font-size:26px; line-height:1.15; letter-spacing:-.5px; }
|
|
221
|
+
.admin-hero p { margin:0; color:var(--dsr-muted); font-size:12px; }
|
|
222
|
+
.admin-hero-side { display:flex; flex-direction:column; align-items:flex-end; gap:10px; flex:none; }
|
|
223
|
+
.admin-hero-status { color:var(--dsr-success); font-size:12px; font-weight:700; }
|
|
224
|
+
.admin-hero.attention { border-color:var(--dsr-warning-line); }
|
|
225
|
+
.admin-hero.attention .admin-hero-status { color:var(--dsr-warning); }
|
|
226
|
+
.admin-hero.plugin, .admin-hero.offline { border-color:var(--dsr-line); }
|
|
227
|
+
.admin-hero.plugin .admin-hero-status, .admin-hero.offline .admin-hero-status { color:var(--dsr-muted); }
|
|
228
|
+
.admin-hero-action { min-height:36px; padding:7px 13px; border-color:var(--dsr-accent-strong); background:var(--dsr-accent-strong); color:var(--dsr-on-accent); font-weight:700; }
|
|
229
|
+
.admin-hero.plugin .admin-hero-action { border-color:var(--dsr-accent-strong); background:var(--dsr-accent-strong); }
|
|
230
|
+
.admin-hero.offline .admin-hero-action { border-color:var(--dsr-accent-line); background:var(--dsr-accent-soft); color:var(--dsr-accent-strong); }
|
|
231
|
+
#stats { grid-template-columns:repeat(4,minmax(0,1fr)); }
|
|
232
|
+
#stats .stat-card:nth-child(n+5) { background:transparent; box-shadow:none; }
|
|
233
|
+
#stats .stat-card:nth-child(n+5) .v { font-size:20px; }
|
|
234
|
+
@media (max-width: 760px) {
|
|
235
|
+
.admin-hero { align-items:stretch; flex-direction:column; gap:14px; padding:16px; }
|
|
236
|
+
.admin-hero h2 { font-size:23px; }
|
|
237
|
+
.admin-hero-side { align-items:stretch; flex-direction:row; justify-content:space-between; }
|
|
238
|
+
.admin-hero-action { flex:0 0 auto; }
|
|
239
|
+
#stats { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
|
240
|
+
#stats .stat-card:nth-child(n+5) { background:linear-gradient(145deg, var(--dsr-panel), var(--dsr-bg-2)); box-shadow:inset 0 1px var(--dsr-divider), 0 10px 26px var(--dsr-shadow); }
|
|
241
|
+
}
|
|
242
|
+
@media (max-width: 720px) {
|
|
243
|
+
.token-row { display: flex; flex-direction: column; align-items: stretch; }
|
|
244
|
+
.token-actions { justify-content: stretch; }
|
|
245
|
+
.token-actions .mini-btn { flex: 1 1 auto; }
|
|
246
|
+
}
|
|
247
|
+
@media (max-width: 900px) {
|
|
248
|
+
.admin-title { display: grid; grid-template-columns: minmax(0, 1fr) auto; row-gap: 10px; }
|
|
249
|
+
.admin-title .left { min-width: 0; }
|
|
250
|
+
.admin-title .right { grid-column: 1 / -1; justify-content: flex-start; min-width: 0; overflow-x: auto; scrollbar-width: none; }
|
|
251
|
+
.admin-title .right::-webkit-scrollbar { display: none; }
|
|
252
|
+
}
|
|
253
|
+
@media (max-width: 440px) {
|
|
254
|
+
.admin-title h1 { display: block; min-width: 0; overflow: visible; font-size: 18px; }
|
|
255
|
+
.admin-title .left { gap: 7px; }
|
|
256
|
+
}
|
|
177
257
|
</style>
|
|
178
258
|
</head>
|
|
179
259
|
<body>
|
|
@@ -185,11 +265,11 @@
|
|
|
185
265
|
<h1>DSH Remote <span data-i18n="admin">管理</span></h1>
|
|
186
266
|
</div>
|
|
187
267
|
<div class="right">
|
|
188
|
-
<a class="mini-btn gh-btn tb-btn" href="https://github.com/Blank-not-black/dsh-Remote" target="_blank" rel="noopener" title="GitHub">
|
|
268
|
+
<a class="mini-btn gh-btn tb-btn" href="https://github.com/Blank-not-black/dsh-Remote" target="_blank" rel="noopener" title="GitHub" data-i18n-aria="repo">
|
|
189
269
|
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8a8.01 8.01 0 0 0 5.47 7.59c.4.08.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg>
|
|
190
270
|
<span class="gh-label" data-i18n="repo">仓库</span>
|
|
191
271
|
</a>
|
|
192
|
-
<button id="btn-donate" class="mini-btn tb-btn" data-i18n-title="donateTitle"
|
|
272
|
+
<button id="btn-donate" class="mini-btn tb-btn" data-i18n-title="donateTitle" data-i18n-aria="donateTitle"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 8h11v6a4 4 0 0 1-4 4H10a4 4 0 0 1-4-4Z"/><path d="M17 10h1.5a2.5 2.5 0 0 1 0 5H17M9 4c0 1 1 1.2 1 2M13 4c0 1 1 1.2 1 2M5 20h13"/></svg></button>
|
|
193
273
|
<div class="fb-wrap">
|
|
194
274
|
<button id="btn-feedback" class="mini-btn fb-btn tb-btn" data-i18n-title="feedbackTitle" data-i18n-aria="feedbackTitle" aria-haspopup="menu" aria-expanded="false">
|
|
195
275
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-8 8H5l-1.5 2L4 20a8 8 0 0 1-1-4 8 8 0 0 1 18-4Z"/></svg>
|
|
@@ -223,6 +303,18 @@
|
|
|
223
303
|
</div>
|
|
224
304
|
|
|
225
305
|
<div id="main-view" class="hidden">
|
|
306
|
+
<section id="admin-hero" class="admin-hero" aria-live="polite">
|
|
307
|
+
<div class="admin-hero-copy">
|
|
308
|
+
<div class="admin-eyebrow" data-i18n="hero.eyebrow">GATEWAY CONTROL</div>
|
|
309
|
+
<h2 id="admin-hero-title">正在检查网关</h2>
|
|
310
|
+
<p id="admin-hero-desc">读取运行状态与上游连接</p>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="admin-hero-side">
|
|
313
|
+
<span id="admin-hero-status" class="admin-hero-status">—</span>
|
|
314
|
+
<button id="admin-hero-action" class="mini-btn admin-hero-action" type="button" data-i18n="hero.openDevices">查看设备</button>
|
|
315
|
+
</div>
|
|
316
|
+
</section>
|
|
317
|
+
|
|
226
318
|
<div class="token-row">
|
|
227
319
|
<code id="token-full">—</code>
|
|
228
320
|
<div class="token-actions">
|
|
@@ -274,7 +366,7 @@
|
|
|
274
366
|
</div>
|
|
275
367
|
|
|
276
368
|
<!-- 皮肤选择模态 -->
|
|
277
|
-
<div id="modal-theme" class="modal hidden">
|
|
369
|
+
<div id="modal-theme" class="modal hidden" role="dialog" aria-modal="true">
|
|
278
370
|
<div class="modal-card">
|
|
279
371
|
<div class="modal-title" data-i18n="theme.panelTitle">选择配色</div>
|
|
280
372
|
<div id="theme-options" class="theme-panel-list"></div>
|
|
@@ -285,11 +377,11 @@
|
|
|
285
377
|
</div>
|
|
286
378
|
|
|
287
379
|
<!-- 赞赏支持模态 -->
|
|
288
|
-
<div id="modal-donate" class="modal hidden">
|
|
380
|
+
<div id="modal-donate" class="modal hidden" role="dialog" aria-modal="true">
|
|
289
381
|
<div class="modal-card">
|
|
290
382
|
<div class="modal-title" data-i18n="donateTitle">赞赏支持</div>
|
|
291
383
|
<div class="modal-body donate-body">
|
|
292
|
-
<img src="donate.png" alt="" class="donate-img" style="display:block;margin:0 auto;max-width:100%;max-height:60vh;border-radius:
|
|
384
|
+
<img src="../donate.png" alt="赞赏二维码" width="1152" height="1152" class="donate-img" style="display:block;margin:0 auto;max-width:100%;height:auto;max-height:60vh;border-radius:12px">
|
|
293
385
|
<p class="donate-thanks" data-i18n="donateThanks" style="text-align:center;margin-top:12px">感谢你的支持 ☕</p>
|
|
294
386
|
</div>
|
|
295
387
|
<div class="modal-actions">
|
|
@@ -298,7 +390,7 @@
|
|
|
298
390
|
</div>
|
|
299
391
|
</div>
|
|
300
392
|
|
|
301
|
-
<div id="toast" class="toast hidden"></div>
|
|
393
|
+
<div id="toast" class="toast hidden" role="status" aria-live="polite"></div>
|
|
302
394
|
<script>
|
|
303
395
|
window.ADMIN_STR = {
|
|
304
396
|
zh: {
|
|
@@ -306,6 +398,8 @@
|
|
|
306
398
|
'consoleTitle': '返回控制台',
|
|
307
399
|
'collapse': '收起面板',
|
|
308
400
|
'admin': '管理',
|
|
401
|
+
'hero.eyebrow': 'GATEWAY CONTROL', 'hero.running': '网关运行正常', 'hero.attention': '网关需要关注', 'hero.plugin': 'DSH 插件已连接', 'hero.offline': '网关未运行', 'hero.checking': '正在检查网关',
|
|
402
|
+
'hero.runningDesc': '{online} 台设备在线 · {requests} 次请求', 'hero.attentionDesc': 'DSH 上游暂时不可达,请打开诊断', 'hero.pluginDesc': '插件已连接 DSH,网关可按需启动', 'hero.offlineDesc': '启动网关后即可接受 App 和桌面端连接', 'hero.openDevices': '查看设备', 'hero.startGateway': '启动网关', 'hero.copyToken': '复制令牌',
|
|
309
403
|
'theme.default': '默认深空', 'theme.dark': '落日', 'theme.light': '易北爱乐厅', 'theme.neutral': '草原孤塔',
|
|
310
404
|
'theme.panelTitle': '选择配色', 'theme.close': '关闭',
|
|
311
405
|
'repo': '仓库',
|
|
@@ -389,6 +483,8 @@
|
|
|
389
483
|
'consoleTitle': 'Back to console',
|
|
390
484
|
'collapse': 'Collapse panel',
|
|
391
485
|
'admin': 'Admin',
|
|
486
|
+
'hero.eyebrow': 'GATEWAY CONTROL', 'hero.running': 'Gateway is healthy', 'hero.attention': 'Gateway needs attention', 'hero.plugin': 'DSH plugin connected', 'hero.offline': 'Gateway is not running', 'hero.checking': 'Checking gateway',
|
|
487
|
+
'hero.runningDesc': '{online} devices online · {requests} requests', 'hero.attentionDesc': 'DSH upstream is unreachable; open diagnostics', 'hero.pluginDesc': 'The plugin is connected to DSH; start the gateway when needed', 'hero.offlineDesc': 'Start the gateway to accept App and desktop connections', 'hero.openDevices': 'View devices', 'hero.startGateway': 'Start gateway', 'hero.copyToken': 'Copy token',
|
|
392
488
|
'theme.default': 'Default', 'theme.dark': 'Sunset', 'theme.light': 'Elbphilharmonie', 'theme.neutral': 'Prairie Tower',
|
|
393
489
|
'theme.panelTitle': 'Choose theme', 'theme.close': 'Close',
|
|
394
490
|
'repo': 'Repo',
|
package/public/admin.js
CHANGED
|
@@ -220,6 +220,24 @@ function render(st) {
|
|
|
220
220
|
if (cur) cur.textContent = t('gatewayPort.current', { port: gatewayPort })
|
|
221
221
|
}
|
|
222
222
|
const upOk = st.upstream.reachable
|
|
223
|
+
const hero = $('admin-hero')
|
|
224
|
+
if (hero) {
|
|
225
|
+
const heroState = isGateway ? (upOk ? 'running' : 'attention') : isPlugin ? 'plugin' : 'offline'
|
|
226
|
+
hero.className = 'admin-hero ' + heroState
|
|
227
|
+
const titleKey = heroState === 'running' ? 'hero.running' : heroState === 'attention' ? 'hero.attention' : heroState === 'plugin' ? 'hero.plugin' : 'hero.offline'
|
|
228
|
+
const descKey = heroState === 'running' ? 'hero.runningDesc' : heroState === 'attention' ? 'hero.attentionDesc' : heroState === 'plugin' ? 'hero.pluginDesc' : 'hero.offlineDesc'
|
|
229
|
+
$('admin-hero-title').textContent = t(titleKey)
|
|
230
|
+
$('admin-hero-desc').textContent = heroState === 'running'
|
|
231
|
+
? t(descKey, { online: st.onlineCount || 0, requests: st.totalRequests || 0 })
|
|
232
|
+
: t(descKey)
|
|
233
|
+
$('admin-hero-status').textContent = isGateway ? (upOk ? t('stat.reachable') : t('stat.unreachable')) : t(isPlugin ? 'badge.embedded' : 'badge.gatewayDown')
|
|
234
|
+
const action = $('admin-hero-action')
|
|
235
|
+
if (action) {
|
|
236
|
+
const actionKey = heroState === 'plugin' ? 'hero.startGateway' : heroState === 'offline' ? 'hero.copyToken' : 'hero.openDevices'
|
|
237
|
+
action.textContent = t(actionKey)
|
|
238
|
+
action.dataset.heroAction = heroState === 'plugin' ? 'start' : heroState === 'offline' ? 'copy' : 'devices'
|
|
239
|
+
}
|
|
240
|
+
}
|
|
223
241
|
const hostIPs = (st.lanIPs || []).join(t('stat.ipSep')) || '127.0.0.1'
|
|
224
242
|
const latestHtml = st.latest?.newer
|
|
225
243
|
? `<div class="v">${t('stat.updateAvailable', { version: st.latest.version })}</div><div class="k">${t('stat.currentV', { version: st.version })} · <a href="${st.latest.url || '#'}" target="_blank" rel="noopener" style="color:var(--dsr-accent-strong)">${t('stat.download')}</a></div>`
|
|
@@ -388,6 +406,12 @@ $('btn-logout').addEventListener('click', logout)
|
|
|
388
406
|
$('btn-close-drawer').addEventListener('click', () => {
|
|
389
407
|
window.parent.postMessage({ source: 'dsh-remote-admin', type: 'close' }, location.origin)
|
|
390
408
|
})
|
|
409
|
+
$('admin-hero-action').addEventListener('click', () => {
|
|
410
|
+
const action = $('admin-hero-action').dataset.heroAction
|
|
411
|
+
if (action === 'start') return $('btn-gateway').click()
|
|
412
|
+
if (action === 'copy') return $('btn-copy').click()
|
|
413
|
+
$('device-rows').closest('.table-wrap')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
414
|
+
})
|
|
391
415
|
$('btn-copy').addEventListener('click', async () => {
|
|
392
416
|
try {
|
|
393
417
|
await navigator.clipboard.writeText(shownToken || token)
|