xieani090612/dsh-remote-panel ↗★ 1
dsh-remote-panel
DSH 远程/WSL 状态预览:探测本机 WSL 发行版与指定 IP 的 SSH 机器(CPU/内存/磁盘/GPU/Docker/进程/服务),渲染到独立 WinUI 3 悬浮窗,并提供 /wsx 命令、remote_* 原生工具、Skills 与 MCP stdio 服务器 适合在DSH中频繁使用远程服务器或WSL环境进行开发运维的用户。
설치
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:xieani090612/dsh-remote-panel配置目标
目标写在 profile 的 cordis.patch.yml 里,位于 dsh-remote-panel 那一行的 config.targets 下。改完重启 DSH。
- id: dsh-remote-panel
name: dsh-remote-panel
config:
targets:
# A local WSL distribution
- kind: wsl
name: WSL Ubuntu 24.04
distro: Ubuntu-24.04
channel: auto # auto = try SSH first, fall back to wsl.exe
user: root # optional: the default user for a WSL target is "root"
tags: [local]
services: [ssh, docker, cron]
# A remote machine (IP or hostname)
- kind: ssh
name: lab-gpu-01
host: 192.168.1.50
port: 22
user: ubuntu
identityFile: ~/.ssh/id_ed25519
tags: [lab, gpu]
# Or just use a Host alias from ~/.ssh/config
- kind: ssh
name: prod-web-1
host: prod-web-1
user: deploy
目标可以用 id、name、主机名,或其中任意一个的唯一片段来选中。省略 id 时,它由 kind + user + host 推导出来——例如 ssh-ubuntu-192.168.1.50。
配置项参考
| 选项 | 默认值 | 说明 |
|---|---|---|
enabled | true | false 会停止所有探测。插件仍然加载,命令/工具/面板仍然可用,但每个目标都停在 unknown。 |
probeIntervalMs | 15000 | 探测间隔(钳制在 3000–3600000)。 |
timeoutMs | 60000 | 全局单次探测超时(钳制在 3000–600000)。WSL 冷启动要花 18–88s —— 如果你会盯着 WSL,别调低这个值。 |
targets[].timeoutMs | 继承全局值 | 单目标覆盖(同样是 3000–600000 的钳制)。只收紧某一个目标——比如一直温热的 WSL 发行版,或纯 SSH 部署。 |
connectTimeoutMs | 10000 | 建连超时(ssh 的 ConnectTimeout)。真正能逮住“机器确实挂了”的就是它,所以全局那 60s 永远不会让你白等。 |
probeOnStart | true | 插件启动时探测一次(错峰进行)。 |
maxConcurrentProbes | 4 | 同时探测多少个目标(钳制在 1–32)。 |
allowMutations | true | 写操作总开关;false 即保持只读。 |
collectDocker | true | 采集开关——关掉能减轻目标端负载。 |
collectProcesses | true | |
collectServices | true | |
collectGpu | true | |
historyLength | 40 | 每个目标保留多少条历史样本用于趋势线(钳制在 0–600)。一次失败仍会推入一个 latencyMs=0 的断点。 |
flushIntervalMs | 500 | 调度器 tick 周期(钳制在 100–60000)。 |
heartbeatMs | 2000 | 什么都没变时多久重写一次快照——面板靠它区分“没变化”和“host 没了”(钳制在 500–300000)。 |
autoLaunch | true | DSH 启动时打开面板窗口。 |
appPath | '' | 面板 exe 的显式路径;留空表示“在包内查找”。 |
stateFile | $DSH_HOME/remote-panel/state.json | 快照写入的位置。 |
targetsFile | — | 从一个 JSON 文件读取额外的目标数组。 |
ssh.executable | 自动检测 | 显式指定 ssh.exe。 |
ssh.user | '' | kind: ssh 目标的默认用户。 |
ssh.port | 22 | kind: ssh 目标的默认端口。 |
ssh.identityFile | '' | 默认身份文件。 |
ssh.commonFlags | [] | 追加到每次 ssh 调用后的额外 argv。 |
ssh.controlMaster | true | 连接复用。热探测从 ~300ms 降到 ~80ms。 |
ssh.controlPersistSec | 60 | 复用的 master 保持存活多久(钳制在 0–3600)。 |
wsl.executable | wsl.exe | |
wsl.defaultUser | '' | 目标没有 user 时使用。 |
单目标选项:kind(wsl | ssh)、id、name、distro、host、port、user、channel(auto | ssh | wsl)、identityFile、sshConfigHost、strictHostKeyChecking、timeoutMs、tags、enabled、services。kind: wsl 目标还可以设 sshPort(默认 2222)——那是通过 SSH 访问该 WSL 目标时使用的端口,这类目标的 host 侧永远是 127.0.0.1。
为什么
timeoutMs默认是 60000。 最后一个wsl.exe会话结束约一分钟后,WSL 会把整个发行版拆掉,所以冷启动的第一次探测要花 18–88s。早先 20s 的默认值让健康的 WSL 目标每隔几轮就抖成 “offline”——数据是对的,只是还没到。真正的浪费(等一台根本连不上的机器)改由connectTimeoutMs吸收,所以 60s 只作用于“连得上但很慢”的目标——而这正是 WSL 冷启动的样子。已经温热的部署可以用targets[].timeoutMs收紧单个目标。
配置项参考
| 选项 | 默认值 | 说明 |
|---|---|---|
enabled | true | false 会停止所有探测。插件仍然加载,命令/工具/面板仍然可用,但每个目标都停在 unknown。 |
probeIntervalMs | 15000 | 探测间隔(钳制在 3000–3600000)。 |
timeoutMs | 60000 | 全局单次探测超时(钳制在 3000–600000)。WSL 冷启动要花 18–88s —— 如果你会盯着 WSL,别调低这个值。 |
targets[].timeoutMs | 继承全局值 | 单目标覆盖(同样是 3000–600000 的钳制)。只收紧某一个目标——比如一直温热的 WSL 发行版,或纯 SSH 部署。 |
connectTimeoutMs | 10000 | 建连超时(ssh 的 ConnectTimeout)。真正能逮住“机器确实挂了”的就是它,所以全局那 60s 永远不会让你白等。 |
probeOnStart | true | 插件启动时探测一次(错峰进行)。 |
maxConcurrentProbes | 4 | 同时探测多少个目标(钳制在 1–32)。 |
allowMutations | true | 写操作总开关;false 即保持只读。 |
collectDocker | true | 采集开关——关掉能减轻目标端负载。 |
collectProcesses | true | |
collectServices | true | |
collectGpu | true | |
historyLength | 40 | 每个目标保留多少条历史样本用于趋势线(钳制在 0–600)。一次失败仍会推入一个 latencyMs=0 的断点。 |
flushIntervalMs | 500 | 调度器 tick 周期(钳制在 100–60000)。 |
heartbeatMs | 2000 | 什么都没变时多久重写一次快照——面板靠它区分“没变化”和“host 没了”(钳制在 500–300000)。 |
autoLaunch | true | DSH 启动时打开面板窗口。 |
appPath | '' | 面板 exe 的显式路径;留空表示“在包内查找”。 |
stateFile | $DSH_HOME/remote-panel/state.json | 快照写入的位置。 |
targetsFile | — | 从一个 JSON 文件读取额外的目标数组。 |
ssh.executable | 自动检测 | 显式指定 ssh.exe。 |
ssh.user | '' | kind: ssh 目标的默认用户。 |
ssh.port | 22 | kind: ssh 目标的默认端口。 |
ssh.identityFile | '' | 默认身份文件。 |
ssh.commonFlags | [] | 追加到每次 ssh 调用后的额外 argv。 |
ssh.controlMaster | true | 连接复用。热探测从 ~300ms 降到 ~80ms。 |
ssh.controlPersistSec | 60 | 复用的 master 保持存活多久(钳制在 0–3600)。 |
wsl.executable | wsl.exe | |
wsl.defaultUser | '' | 目标没有 user 时使用。 |
单目标选项:kind(wsl | ssh)、id、name、distro、host、port、user、channel(auto | ssh | wsl)、identityFile、sshConfigHost、strictHostKeyChecking、timeoutMs、tags、enabled、services。kind: wsl 目标还可以设 sshPort(默认 2222)——那是通过 SSH 访问该 WSL 目标时使用的端口,这类目标的 host 侧永远是 127.0.0.1。
为什么
timeoutMs默认是 60000。 最后一个wsl.exe会话结束约一分钟后,WSL 会把整个发行版拆掉,所以冷启动的第一次探测要花 18–88s。早先 20s 的默认值让健康的 WSL 目标每隔几轮就抖成 “offline”——数据是对的,只是还没到。真正的浪费(等一台根本连不上的机器)改由connectTimeoutMs吸收,所以 60s 只作用于“连得上但很慢”的目标——而这正是 WSL 冷启动的样子。已经温热的部署可以用targets[].timeoutMs收紧单个目标。
Usage
Deeper references (written alongside this README):
- docs/commands.md — every
/wsxsubcommand with sample output - docs/skills.md — what the two Skills teach the agent
- docs/mcp.md — running and wiring the MCP server
/wsx ... commands
/wsx status overview of local WSL and remote machines
/wsx status [target] status overview (optionally one target)
/wsx list list all configured targets and their channel
/wsx probe [target] probe now (all targets when none is given)
/wsx docker [target] list containers
/wsx services [target] list key service states
/wsx ps [target] [n] list processes by CPU
/wsx exec run a command on the target
/wsx panel show / raise the desktop panel window
/wsx open open the plugin data directory and the state file
/wsx help the command list above
/wsx list also prints a tool registration path line, showing whether the tools were
registered through the official defineTool helper (with extra schema validation) or fell
back to raw JSON Schema. Both work.
On the machine this was developed on the fallback (
raw) path is what runs, and the reason is worth recording: the@deepseek-ai/*packages exist only insideapp.asarand are not unpacked to disk, so a third-party plugin's ESMimport('@deepseek-ai/dsh-tools')cannot resolve in Node (Cannot find package '@deepseek-ai/dsh-tools').So the plugin translates its own parameter dialect into standard JSON Schema via
projectParameters()and registers throughctx.tools.register's raw path — a path that uses nothing but Node builtins and is therefore always available. The registration-path line reports honestly which path is in use; seeingrawis not a fault. Behaviour, argument validation and the safety gates are identical either way.
Agent tools
Seven remote_* tools, sharing one implementation and one set of gates with the MCP server:
| Tool | Purpose |
|---|---|
remote_status | target overview; refresh: true re-probes immediately |
remote_exec | run a command on the target, return stdout/stderr/exit code |
remote_files | list / read / upload / download |
remote_docker | ps / logs / start / stop / restart / pause / unpause |
remote_services | status / start / stop / restart / reload |
remote_processes | list (by cpu/mem) and kill |
remote_panel | show the WinUI 3 panel window |
MCP
cordis.patch.yml already carries a row that exposes the same capabilities over MCP stdio:
- id: mcp-remote-panel
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: remote_panel
transport: stdio
command: !!js process.execPath
args: !!js "process.env.DSH_PROFILE_DIR ? [process.env.DSH_PROFILE_DIR + '/node_modules/dsh-remote-panel/bin/mcp-server.js'] : []"
The tools appear as mcp__remote_panel__. Delete that whole block if you do not
want MCP — nothing else is affected. (The real file also sets cwd,
failOnStartupError: false and toolCallTimeoutMs; the excerpt above is trimmed to the parts
that matter for understanding how it is wired.)
To check the MCP server by hand, without DSH:
$env:DSH_WSX_CONFIG = '{"config":{"targets":[{"kind":"wsl","distro":"Ubuntu-24.04","channel":"wsl","user":"root"}]}}'
'{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node .\bin\mcp-server.js
stdout must carry protocol JSON only (newline-delimited); all diagnostics go to stderr.
When adding logging to that file, write to stderr — one extra character on stdout breaks
the protocol.
The target list comes from the config.resolved.json the host plugin writes, so the MCP side
and the DSH side always see the same targets; configuration merging is not reimplemented there.
Skills
Two skills, loadable by the agent once installed:
| Skill | Purpose |
|---|---|
remote-panel | which surface to use when, typical tool usage, the two WSL channels and cold starts |
remote-panel-troubleshooting | layered diagnosis: config → link → collection → state file → MCP → refused writes |
They are installed into DSH's default skill scan root, $DSH_HOME/skills/, by install.ps1
(and by the manual steps in INSTALL.md) rather than through a bundle patch.
The long comment in cordis.patch.yml explains why a bundle patch cannot enable
skill-filesystem here (the web-app layer disables that row, and a non-insert patch only
assigns the keys it carries, so it can never clear the inherited disabled). Edits to
skill content take effect live — that directory is watched; code and config changes need
a restart.