dsh-remote-plugin 0.6.7 → 0.6.9
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-stats.cjs +26 -11
- package/gateway.cjs +494 -87
- package/index.mjs +12 -2
- 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 +592 -91
- package/public/desktop/desktop.css +126 -19
- package/public/desktop/desktop.html +63 -14
- package/public/desktop/desktop.js +295 -77
- package/public/index.html +145 -41
- package/public/plugin.html +151 -0
- package/public/plugin.js +179 -0
- package/public/styles.css +176 -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-stats.cjs
CHANGED
|
@@ -282,6 +282,15 @@ class StatsStore {
|
|
|
282
282
|
let processed = 0
|
|
283
283
|
let currentModel = ''
|
|
284
284
|
let headerParsed = false
|
|
285
|
+
let lineCount = 0
|
|
286
|
+
let yielding = false
|
|
287
|
+
const dirtyDays = new Map()
|
|
288
|
+
let scanError = ''
|
|
289
|
+
const flush = () => {
|
|
290
|
+
for (const day of dirtyDays.values()) this._saveDay(day)
|
|
291
|
+
dirtyDays.clear()
|
|
292
|
+
if (lastSeq >= 0) this._setCursor(sessionId, lastSeq)
|
|
293
|
+
}
|
|
285
294
|
|
|
286
295
|
const zstd = spawn('zstd', ['-dc', file], { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
287
296
|
const rl = readline.createInterface({ input: zstd.stdout })
|
|
@@ -292,11 +301,12 @@ class StatsStore {
|
|
|
292
301
|
} else {
|
|
293
302
|
console.warn(`[stats] zstd 解压失败 ${file}: ${err.message}`)
|
|
294
303
|
}
|
|
304
|
+
scanError = err.code || err.message
|
|
295
305
|
rl.close()
|
|
296
|
-
resolvePromise({ sessionId, processed, skipped: 0, error: err.code || err.message })
|
|
297
306
|
})
|
|
298
307
|
|
|
299
308
|
rl.on('line', (line) => {
|
|
309
|
+
lineCount++
|
|
300
310
|
let event
|
|
301
311
|
try {
|
|
302
312
|
event = JSON.parse(line)
|
|
@@ -324,24 +334,28 @@ class StatsStore {
|
|
|
324
334
|
const model = eventModel(event) || currentModel || 'unknown'
|
|
325
335
|
const { date, hour, period } = eventKey(event.time)
|
|
326
336
|
if (date >= PRICING_START_DATE) {
|
|
327
|
-
const day = this._loadDay(date)
|
|
337
|
+
const day = dirtyDays.get(date) || this._loadDay(date)
|
|
338
|
+
dirtyDays.set(date, day)
|
|
328
339
|
const hourBucket = day.hours[hour] || (day.hours[hour] = {})
|
|
329
340
|
const modelBucket = hourBucket[model] || (hourBucket[model] = emptyBucket())
|
|
330
341
|
addUsage(modelBucket, model, period, usage)
|
|
331
|
-
this._saveDay(day)
|
|
332
342
|
processed++
|
|
333
343
|
}
|
|
334
344
|
}
|
|
335
345
|
}
|
|
336
346
|
lastSeq = event.seq
|
|
347
|
+
// 大历史文件按批次让出事件循环,避免回填长期占住实时 HTTP/WS 处理。
|
|
348
|
+
if (lineCount % 500 === 0 && !yielding) {
|
|
349
|
+
yielding = true
|
|
350
|
+
rl.pause()
|
|
351
|
+
setImmediate(() => { yielding = false; rl.resume() })
|
|
352
|
+
}
|
|
337
353
|
})
|
|
338
354
|
|
|
339
355
|
rl.on('close', () => {
|
|
340
|
-
|
|
341
|
-
this._setCursor(sessionId, lastSeq)
|
|
342
|
-
}
|
|
356
|
+
flush()
|
|
343
357
|
if (onProgress) onProgress({ sessionId, processed })
|
|
344
|
-
resolvePromise({ sessionId, processed })
|
|
358
|
+
resolvePromise({ sessionId, processed, ...(scanError ? { error: scanError } : {}) })
|
|
345
359
|
})
|
|
346
360
|
})
|
|
347
361
|
}
|
|
@@ -351,13 +365,14 @@ class StatsStore {
|
|
|
351
365
|
const root = sessionsRoot || path.join(os.homedir(), '.dsh', 'sessions')
|
|
352
366
|
let files = []
|
|
353
367
|
try {
|
|
354
|
-
const walk = (dir) => {
|
|
355
|
-
|
|
356
|
-
|
|
368
|
+
const walk = async (dir) => {
|
|
369
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true })
|
|
370
|
+
for (const ent of entries) {
|
|
371
|
+
if (ent.isDirectory()) await walk(path.join(dir, ent.name))
|
|
357
372
|
else if (ent.name === 'session.jsonl.zstd') files.push(path.join(dir, ent.name))
|
|
358
373
|
}
|
|
359
374
|
}
|
|
360
|
-
walk(root)
|
|
375
|
+
await walk(root)
|
|
361
376
|
} catch (err) {
|
|
362
377
|
console.warn('[stats] 扫描会话目录失败: ' + (err.message || err))
|
|
363
378
|
return { files: 0, processed: 0 }
|