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 CHANGED
@@ -1,46 +1,52 @@
1
1
  # dsh-remote-plugin
2
2
 
3
- [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
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
- **English** · [中文](README.md)
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
- # or pin a version
14
- dsh plugin --profile web add dsh-remote-plugin@0.5.0
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 hard-refresh (Ctrl+F5); the app icon entry appears at the bottom of the left sidebar.
16
+ Restart DSH Web and refresh the browser. The DSH Remote entry will appear in the sidebar.
18
17
 
19
- git source install (equivalent):
18
+ ## What the plugin provides
20
19
 
21
- ```sh
22
- dsh plugin --profile web add "github:Blank-not-black/dsh-Remote#main&path:/packages/plugin"
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
- ## Gateway
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
- - Auto-start by default: on DSH startup or drawer refresh, the plugin launches the built-in `gateway.cjs` (`0.0.0.0:8787`), independent of the DSH process; Linux uses a standalone systemd unit, and environments without systemd (e.g. macOS) automatically fall back to a resident child process.
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
- ## Mobile App
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 app is embedded in the plugin package (`apk/dsh-remote.apk`) install the plugin and you have it, no GitHub needed:
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
- - Tap **QR code** in the desktop drawer → scan it with the phone to open the admin page → download the app from that page
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
- The app has built-in multi-server latency switching and offline chat history caching; downloaded files are stored in the system `Download/dsh-remote` folder.
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
- [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
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
- # 或 pin 版本
14
- dsh plugin --profile web add dsh-remote-plugin@0.5.0
11
+
12
+ # 也可以安装指定版本
13
+ dsh plugin --profile web add dsh-remote-plugin@0.6.8
15
14
  ```
16
15
 
17
- 重启 DSH Web Ctrl+F5,左侧边栏底部出现 App 图标入口。
16
+ 重启 DSH Web 后刷新浏览器,左侧边栏会出现 DSH Remote 入口。
18
17
 
19
- git 源安装(等价):
18
+ ## 插件提供什么
20
19
 
21
- ```sh
22
- dsh plugin --profile web add "github:Blank-not-black/dsh-Remote#main&path:/packages/plugin"
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
- - 默认**自动启动**:DSH 启动或抽屉刷新时,插件会拉起内置 `gateway.cjs`(`0.0.0.0:8787`),独立于 DSH 进程;Linux 使用独立 systemd 单元,macOS 等无 systemd 环境自动回退为常驻子进程。
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
- ## 手机 App
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
- App 已随插件包内嵌(`apk/dsh-remote.apk`),装好插件即可获取,无需上 GitHub:
42
+ 令牌等同于 DSH 远程操作凭证,请不要公开或提交到仓库。局域网访问建议配合防火墙;跨网络访问建议使用 Tailscale 或其他带认证的安全隧道。
37
43
 
38
- - 桌面抽屉点「二维码」→ 手机扫码打开管理页 → 页面提供 App 下载
39
- - 或浏览器直接访问 `http://<网关IP>:8787` 下载
40
- - App 更新同样走网关自动推送(`update.json` 相对路径),全程不绕 GitHub
41
- - 电脑浏览器打开 `http://<网关IP>:8787` 自动进入桌面端 WebUI(侧栏会话 + 文件 + 设置 + 统计 + 审批通知卡片栈)
44
+ ## 相关地址
42
45
 
43
- App 内置多服务器测速切换与聊天记录离线缓存,下载文件统一放系统「下载/dsh-remote」。
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
 
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/admin/ —— 即本地网关管理控制台的桌面 UI。
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: 0, right: 0, bottom: 0, zIndex: 2147483000,
17
- width: 'min(460px, 96vw)', background: '#0b0e1a',
18
- boxShadow: '-10px 0 32px rgba(0,0,0,.5)',
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: hover ? 'rgba(125, 207, 255, .14)' : 'transparent',
55
- transition: 'background .15s ease',
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/admin/?embedded=1',
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
- if (lastSeq >= 0) {
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
- for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
356
- if (ent.isDirectory()) walk(path.join(dir, ent.name))
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 }