@zhangfengshun/dsh-remote-ssh 2.4.2 → 2.4.4
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/CHANGELOG.md +20 -0
- package/README.md +132 -9
- package/README_EN.md +132 -9
- package/lib/index.js +71 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
本文件的版本号与 `package.json` 的 `version` 保持一致。每个版本对应一个 Cordis Package 快照(`pkg-N`)。
|
|
4
4
|
|
|
5
|
+
## [2.4.4] — 文档结构补全(README 代码示例 / 兼容性矩阵 / 故障排查)
|
|
6
|
+
### 文档
|
|
7
|
+
- **新增「代码示例」章节**(中英双语):6 组可直接照抄的示例——远程命令(含 `timeoutMs`)、文件读写、长时任务与挂起恢复(`timeoutMs: 0` / `remote_ssh_kill`)、远程工作区内的免参调用、`~/.ssh/config` 导入、镜像 sync/push,并给出真实返回值形状。
|
|
8
|
+
- **新增「兼容性」矩阵**:逐行列出 DSH 0.1.5-rc.1 / 0.1.2 稳定线、dsh-better-sidebar 0.15–0.18(4 端点)与 0.19.x(6 端点,并标注上游主机半边加载问题)、远程 sshd 的实测状态。
|
|
9
|
+
- **新增「故障排查」表**:公钥认证失败(口令密钥 / Administrators 授权文件 / 用户名写法)、git-bash 启动、文件页签「这类内容还没有可用的查看方式。」、终端仅密钥认证、`minimumReleaseAge`、命令挂起、大文件截断——每条给出原因与解法。
|
|
10
|
+
- **新增「安装」前置要求表与卸载命令**、README 顶部目录导航、「更新日志」入口与 Star 引导。
|
|
11
|
+
- 中文 README 与英文 README_EN 保持结构对齐。
|
|
12
|
+
|
|
13
|
+
## [2.4.3] — 适配 better-sidebar 0.19 新增端点(fs.rename / fs.remove)+ 版本兼容性核查
|
|
14
|
+
### 新增
|
|
15
|
+
- **拦截 better-sidebar 0.19 新增的 `fs.rename` / `fs.remove` 端点**(exact 路由从 4 条增至 6 条):远程工作区的「重命名 / 删除」此前会落到本地镜像目录(0.19 之前 better-sidebar 没有这两个端点,操作走客户端 API,不存在该问题)。现在:
|
|
16
|
+
- 远程工作区 → 走 SSH(`mv -f --` / `rm -rf --`),语义对齐 better-sidebar(单个路径段校验、目标已存在返回 409、禁止操作工作区根),并失效相关读/列举缓存;
|
|
17
|
+
- 本地工作区 → 本地实现(不覆盖、lstat 判定符号链接、目录递归删除),契约与 better-sidebar 完全一致;
|
|
18
|
+
- 旧版 better-sidebar(无此端点)不调用这两个路由,注册无副作用。
|
|
19
|
+
|
|
20
|
+
### 兼容性核查(重要)
|
|
21
|
+
- **dsh-better-sidebar 0.19.0 / 0.19.1(以及 0.18.1)在当前 DSH Desktop(v2.0.9,DSH 0.1.5-rc.1)上主机半边无法加载**:其 `lib/index.js` 以**值**方式导入 `SessionLogOffset`(`import { SessionLogOffset } from "@deepseek-ai/dsh-session"`),而 DSH 2.0.9 面向插件的模块面只提供该符号的**类型**声明,运行时导入抛 `does not provide an export named 'SessionLogOffset'` → better-sidebar 主机半边(fs/git/terminal/jobs 等全部 API)整体失效,侧边栏文件页签因此回落到「这类内容还没有可用的查看方式。」
|
|
22
|
+
- **已验证的版本矩阵**:`0.19.1`(导入该符号)、`0.19.0`(导入)、`0.18.1`(导入)→ 在 DSH 2.0.9 上不可用;`0.18.0`、`0.17.1`(不导入)→ 可用。本插件在 `0.18.0` 上逐点验证:`fs.tree/read/write/search` 返回形状、`betterSidebar` 服务与 `registerTab`、`/sidebar/upload` 上传拦截全部咬合。
|
|
23
|
+
- 建议:在 better-sidebar 修复该导入(或 DSH 恢复该运行时导出)之前,desktop profile 使用 **dsh-better-sidebar@0.18.0**;本插件对 0.18/0.19 两套契约均已适配。
|
|
24
|
+
|
|
5
25
|
## [2.4.2] — 修复设置导航显示器图标「先齿轮后显示器」闪现
|
|
6
26
|
### 修复
|
|
7
27
|
- **图标替换从防抖路径改为同步执行**:此前设置面板挂载后,显示器图标替换走 500ms 防抖定时器,用户会先看到半秒壳层默认齿轮再变成显示器图标。现在在 MutationObserver 回调里**同步原位替换**——回调是微任务、在浏览器绘制前执行,齿轮刚插入 DOM 就被换掉,**第一帧即最终图标**,无闪现。
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
类 **VSCode Remote-SSH** 的 **DSH** 插件:通过 SSH 连接远程超算 / 服务器,在 DSH 内置「文件」「终端」页签中直接操作远程文件和终端。
|
|
6
6
|
|
|
7
|
+
**目录**:[功能](#功能) · [截图](#截图) · [安装](#安装) · [使用](#使用) · [代码示例](#代码示例) · [模型工具](#模型工具) · [命令超时与恢复](#命令超时与恢复) · [兼容性](#兼容性) · [故障排查](#故障排查) · [原理](#原理) · [缓存与一致性](#缓存与一致性) · [许可证](#许可证)
|
|
8
|
+
|
|
7
9
|
## 功能
|
|
8
10
|
|
|
9
11
|
| 能力 | 说明 |
|
|
@@ -31,20 +33,107 @@
|
|
|
31
33
|
|
|
32
34
|
## 安装
|
|
33
35
|
|
|
36
|
+
**前置要求**
|
|
37
|
+
|
|
38
|
+
| 项 | 要求 |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| DSH | ≥ 0.1.5-rc.1(0.1.2 稳定线请用 v0.18.1 时代的插件版本) |
|
|
41
|
+
| dsh-better-sidebar | ≥ 0.15(本插件依赖其 `/sidebar/api/fs.*` 文件 API) |
|
|
42
|
+
| 本机 SSH 客户端 | Windows:系统自带 OpenSSH(`%SystemRoot%\System32\OpenSSH\ssh.exe`);Linux/macOS:openssh-client |
|
|
43
|
+
| 远程主机 | 任意标准 sshd(超算 / 服务器 / 跳板机均可) |
|
|
44
|
+
|
|
45
|
+
**一条命令安装**(无需 token、API Key 或额外配置):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.4.4
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
安装后**重启 DSH**。`@zhangfengshun/dsh-remote-ssh` 必须在 bundles 列表中排在 `dsh-better-sidebar` **之后**。
|
|
52
|
+
|
|
53
|
+
卸载:
|
|
54
|
+
|
|
34
55
|
```bash
|
|
35
|
-
dsh plugin --profile <name>
|
|
56
|
+
dsh plugin --profile <name> remove @zhangfengshun/dsh-remote-ssh
|
|
36
57
|
```
|
|
37
58
|
|
|
38
|
-
>
|
|
39
|
-
>
|
|
40
|
-
> 内置「文件」页签的 SSH 直读依赖 **dsh-better-sidebar ≥ 0.15** 的文件 API(`/sidebar/api/fs.*` 端点),请勿使用更早版本;当前已逐点验证至 **dsh-better-sidebar 0.19.0** 与 **DSH 0.1.5-rc.1**(主机服务 / settings / tools / slot / 上传下载拦截全部咬合)。
|
|
59
|
+
> ⚠️ **dsh-better-sidebar 版本兼容性(2026-09 实测)**:`0.18.1 / 0.19.0 / 0.19.1` 在 DSH Desktop v2.0.9(DSH 0.1.5-rc.1)上**主机半边无法加载**(它们以值方式导入 `SessionLogOffset`,桌面版模块面只提供该类型声明)→ 侧边栏文件页签显示「这类内容还没有可用的查看方式。」。请使用 **0.18.0**,或使用已修复该导入的构建;本插件对 0.18(4 端点)与 0.19(6 端点,含 `fs.rename`/`fs.remove`)两套契约均已适配。
|
|
41
60
|
|
|
42
61
|
## 使用
|
|
43
62
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
**三步上手**
|
|
64
|
+
|
|
65
|
+
1. **设置 → 远程连接** → 添加连接(主机 / 端口 / 用户 / 密钥)→ 点「测试连接」验证;已有 `~/.ssh/config` 可直接一键导入
|
|
66
|
+
2. **添加工作区** → 选「选择远程目录…」→ 选连接 → 浏览并选择远程目录(该目录会成为原生 DSH 工作区)
|
|
67
|
+
3. 进入该工作区会话:内置「文件」页签直接显示远程文件(编辑保存直写远程),「终端」页签自动 SSH 到远程主机(仅密钥认证)
|
|
68
|
+
|
|
69
|
+
**会话内直接对模型说**(远程工作区会话中免填连接参数):
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
看看 /home/user/project 下有什么,然后把 train.py 的第 20 行改掉
|
|
73
|
+
跑一下 squeue -u $USER,把排队情况整理成表格
|
|
74
|
+
把这个目录的 *.log 里含 ERROR 的行抓出来
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 代码示例
|
|
78
|
+
|
|
79
|
+
**示例 1 · 执行远程命令**(`remote_ssh_exec`,默认 120s 超时):
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"command": "sinfo -h -o '%P %a %D %t %N' | head -20",
|
|
84
|
+
"timeoutMs": 30000
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
返回 `{ ok, exitCode, stdout, stderr, error, truncated, isTimeout }`——例如:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{ "ok": true, "exitCode": 0, "stdout": "cpu* up 12 idle 8 ...\n", "stderr": "", "error": "", "truncated": false, "isTimeout": false }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**示例 2 · 文件读写(无需同步)**:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "path": "~/project/config.yaml", "content": "lr: 0.001\nepochs: 50\n" }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{ "path": "~/project/train.py" }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`remote_ssh_cat` 走 base64 传输(二进制安全),`remote_ssh_write` 为原子写入(写临时文件再 rename)。
|
|
105
|
+
|
|
106
|
+
**示例 3 · 长时任务与挂起恢复**(构建 / 训练显式放宽,卡住可强杀会话):
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{ "command": "cd ~/project && bash run_train.sh", "timeoutMs": 0 }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{ "all": true }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`timeoutMs: 0` 禁用本次超时;环境变量 `DSH_REMOTE_SSH_CMD_TIMEOUT_MS=600000` 可改全局默认。超时后池化会话自动丢弃重建,`remote_ssh_kill` 是随时可用的手动兜底。
|
|
117
|
+
|
|
118
|
+
**示例 4 · 远程工作区内的工具调用**(免填 `profileId`,相对路径基于工作区远程目录):
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{ "path": "configs/exp1.yaml" }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**示例 5 · 从 `~/.ssh/config` 导入连接**:设置 → 远程连接 → 「导入 SSH config」→ 勾选主机 → 自动填充 host / user / port / keyPath / ProxyJump。
|
|
125
|
+
|
|
126
|
+
**示例 6 · 本地镜像同步与回推**(离线批改后再一次性上传):
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{ "workspaceId": "w_xxx" }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{ "workspaceId": "w_xxx" }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`remote_ssh_sync` 把远端拉进本地镜像目录,`remote_ssh_push` 把镜像改动推回远端(tar over ssh,批量高效)。
|
|
48
137
|
|
|
49
138
|
## 模型工具
|
|
50
139
|
|
|
@@ -75,9 +164,35 @@ dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.4.1
|
|
|
75
164
|
- **手动兜底**:`remote_ssh_kill`(或 `all: true`)强制关闭某个/全部池化会话,挂起命令随时可清理;
|
|
76
165
|
- 超时命令**不做自动重试**(重试一条挂起的命令只会再次挂起),由模型决定是否改用 `remote_ssh_kill` 或换命令重试。
|
|
77
166
|
|
|
167
|
+
## 兼容性
|
|
168
|
+
|
|
169
|
+
**实测矩阵**(2026-09-12,均为真机验证):
|
|
170
|
+
|
|
171
|
+
| 组件 | 版本 | 状态 |
|
|
172
|
+
| --- | --- | --- |
|
|
173
|
+
| DSH | 0.1.5-rc.1(DSH Desktop v2.0.9) | ✅ 主机服务 / settings / tools / slot / 上传下载拦截全部咬合 |
|
|
174
|
+
| DSH | 0.1.2-rc.1 稳定线 | ✅(插件 2.3.x 时代基线) |
|
|
175
|
+
| dsh-better-sidebar | 0.15.0 – 0.18.0 | ✅ `fs.tree/read/write/search`(4 端点契约) |
|
|
176
|
+
| dsh-better-sidebar | 0.19.x | ⚠️ 插件侧已适配 6 端点(含 `fs.rename`/`fs.remove`);但 0.19.0/0.19.1 自身在 DSH Desktop 上主机半边无法加载,需等上游修复(见[安装](#安装)的警告) |
|
|
177
|
+
| 远程主机 sshd | 标准 OpenSSH(Linux / 超算 / Windows) | ✅ 密钥认证;密码认证需本机 `sshpass`(POSIX) |
|
|
178
|
+
|
|
179
|
+
插件不修改 DSH 源码、不注入 profile 依赖树,全部能力经官方 `cordis.patch.yml` + profile 机制挂载。
|
|
180
|
+
|
|
181
|
+
## 故障排查
|
|
182
|
+
|
|
183
|
+
| 现象 | 原因与解法 |
|
|
184
|
+
| --- | --- |
|
|
185
|
+
| 「测试连接」报 `Permission denied (publickey)` | ① 私钥**带口令**:插件以批处理模式运行(`BatchMode=yes`),无法交互输口令——先用 `ssh-add` 加载,或去掉密钥口令;② Windows host 且用户在 Administrators 组时,公钥须写入 `C:\ProgramData\ssh\administrators_authorized_keys`;③ 用户名的写法(`user` / `.\user` / `user@domain`)要与手动连接一致 |
|
|
186
|
+
| 从 git-bash 启动 `dsh web` 后密钥认证失败 | 2.3.9 起已修复:Windows 下 ssh 解析固定为系统 OpenSSH 绝对路径(此前会误用 Git 自带的 MSYS2 ssh) |
|
|
187
|
+
| 侧边栏文件页签显示「这类内容还没有可用的查看方式。」 | `dsh-better-sidebar` 主机半边未加载:0.18.1 / 0.19.0 / 0.19.1 在 DSH Desktop 上会因 `SessionLogOffset` 运行时导入失败——降到 0.18.0 或使用修复版(上游 PR [#641](https://github.com/omdsh-dev/DSH-better-sidebar/pull/641)) |
|
|
188
|
+
| 内置「终端」页签连不上 | 终端为 `ssh -tt` 交互式通道,**仅支持密钥认证**;密码认证请改用「文件」页签与模型工具 |
|
|
189
|
+
| 安装时提示 `minimumReleaseAge` 或「No matching version」(刚发布) | npm 供应链新鲜度策略,等 1–5 分钟后重试即可 |
|
|
190
|
+
| 命令卡住不返回 | 默认 120s 超时后自动丢弃会话;长时任务用 `timeoutMs: 0`,随时可用 `remote_ssh_kill` 强杀 |
|
|
191
|
+
| 大文件读取被截断 | 单文件读取上限 4MB、下载池化路径约 6.29MB(更大自动回落一次性连接);用 `remote_ssh_exec` + `head`/`tail` 分段处理 |
|
|
192
|
+
|
|
78
193
|
## 原理
|
|
79
194
|
|
|
80
|
-
插件注册
|
|
195
|
+
插件注册 6 个 exact 路由(`/sidebar/api/fs.tree`、`fs.read`、`fs.write`、`fs.search`,以及 better-sidebar 0.19 新增的 `fs.rename`、`fs.remove`),在 better-sidebar 的 prefix 路由之前拦截。会话 cwd 含 `.remote-ssh.json` 时走 SSH,否则走本地 fs。客户端看到的是本地镜像路径,Host 自动转换为远程路径——对客户端完全透明。
|
|
81
196
|
|
|
82
197
|
远程读取采用**单往返合并读**:一条池化命令同时返回 `size/mtime` 帧与文件内容(文本类扩展名优先 raw 直传,字节长 + U+FFFD 双校验失败自动回退 base64,结果逐字节一致);配合主机侧结果缓存与变更失效(见下节)。
|
|
83
198
|
|
|
@@ -101,6 +216,14 @@ Shell wrapper(`~/.dsh/remote-ssh/dsh-remote-shell[.cmd]`)检测工作区 `.r
|
|
|
101
216
|
|
|
102
217
|
—— 2026 年 8 月 18 日
|
|
103
218
|
|
|
219
|
+
## 更新日志
|
|
220
|
+
|
|
221
|
+
版本历史与每版修复细节见 [CHANGELOG.md](./CHANGELOG.md)(最近:2.4.3 适配 better-sidebar 0.19 端点、2.4.2 修复设置图标闪现、2.4.0 命令级超时与 `remote_ssh_kill`)。
|
|
222
|
+
|
|
104
223
|
## 许可证
|
|
105
224
|
|
|
106
225
|
[MIT](./LICENSE)
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
如果这个插件帮到了你,欢迎在 GitHub 上点个 ⭐ [Star](https://github.com/ZhangFengshun/dsh-remote-ssh),或到 [DSH Market](https://dshmarket.com) 收藏——这会帮助更多需要远程超算开发的人找到它。
|
package/README_EN.md
CHANGED
|
@@ -4,6 +4,8 @@ English | [中文](./README.md)
|
|
|
4
4
|
|
|
5
5
|
A **DSH** plugin like **VSCode Remote-SSH**: connect to remote HPC / servers via SSH, and directly operate remote files and terminals within DSH's built-in **Files** and **Terminal** sidebar tabs.
|
|
6
6
|
|
|
7
|
+
**Contents**: [Features](#features) · [Screenshots](#screenshots) · [Installation](#installation) · [Usage](#usage) · [Examples](#examples) · [Model Tools](#model-tools) · [Command Timeout & Recovery](#command-timeout--recovery) · [Compatibility](#compatibility) · [Troubleshooting](#troubleshooting) · [How It Works](#how-it-works) · [Caching & Consistency](#caching--consistency) · [License](#license)
|
|
8
|
+
|
|
7
9
|
## Features
|
|
8
10
|
|
|
9
11
|
| Feature | Description |
|
|
@@ -31,20 +33,107 @@ A **DSH** plugin like **VSCode Remote-SSH**: connect to remote HPC / servers via
|
|
|
31
33
|
|
|
32
34
|
## Installation
|
|
33
35
|
|
|
36
|
+
**Prerequisites**
|
|
37
|
+
|
|
38
|
+
| Item | Requirement |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| DSH | ≥ 0.1.5-rc.1 (on the 0.1.2 stable line, use the v0.18.1-era plugin release) |
|
|
41
|
+
| dsh-better-sidebar | ≥ 0.15 (this plugin uses its `/sidebar/api/fs.*` file API) |
|
|
42
|
+
| Local SSH client | Windows: built-in OpenSSH (`%SystemRoot%\System32\OpenSSH\ssh.exe`); Linux/macOS: openssh-client |
|
|
43
|
+
| Remote host | Any standard sshd (HPC / server / bastion) |
|
|
44
|
+
|
|
45
|
+
**One command** (no token, API key or extra configuration needed):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.4.4
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Restart DSH** after installation. `@zhangfengshun/dsh-remote-ssh` must come **after** `dsh-better-sidebar` in the bundles list.
|
|
52
|
+
|
|
53
|
+
Uninstall:
|
|
54
|
+
|
|
34
55
|
```bash
|
|
35
|
-
dsh plugin --profile <name>
|
|
56
|
+
dsh plugin --profile <name> remove @zhangfengshun/dsh-remote-ssh
|
|
36
57
|
```
|
|
37
58
|
|
|
38
|
-
> **
|
|
39
|
-
>
|
|
40
|
-
> The built-in **Files** tab SSH interception relies on the file API of **dsh-better-sidebar ≥ 0.15** (`/sidebar/api/fs.*` endpoints) — do not use older versions; verified point-by-point against **dsh-better-sidebar 0.19.0** and **DSH 0.1.5-rc.1** (host services / settings / tools / slots / upload & download interception all compatible).
|
|
59
|
+
> ⚠️ **dsh-better-sidebar compatibility (measured 2026-09)**: `0.18.1 / 0.19.0 / 0.19.1` cannot load their host half on DSH Desktop v2.0.9 (DSH 0.1.5-rc.1) — they value-import `SessionLogOffset`, which the desktop module surface exposes only as a type — so the sidebar Files tab falls back to "Nothing here can view this kind of content yet." Use **0.18.0** or a build carrying the fix; this plugin supports both contracts (4 endpoints on 0.18, 6 including `fs.rename`/`fs.remove` on 0.19).
|
|
41
60
|
|
|
42
61
|
## Usage
|
|
43
62
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
**Three steps**
|
|
64
|
+
|
|
65
|
+
1. **Settings → Remote SSH** → Add a connection (host / port / user / key) → Click "Test Connection"; an existing `~/.ssh/config` can be imported in one click
|
|
66
|
+
2. **Add Workspace** → Choose "Select Remote Directory…" → Pick a connection → Browse and select a remote directory (it becomes a native DSH workspace)
|
|
67
|
+
3. Inside that workspace session: the built-in **Files** tab shows remote files (edits save straight back to remote), and the **Terminal** tab auto-SSHes to the host (key auth only)
|
|
68
|
+
|
|
69
|
+
**Just ask the model** (connection params are auto-filled inside a remote-workspace session):
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
What's inside /home/user/project? Then fix line 20 of train.py
|
|
73
|
+
Run squeue -u $USER and summarise the queue as a table
|
|
74
|
+
Grep every ERROR line from the *.log files in this directory
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Examples
|
|
78
|
+
|
|
79
|
+
**1 · Run a remote command** (`remote_ssh_exec`, 120s timeout by default):
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"command": "sinfo -h -o '%P %a %D %t %N' | head -20",
|
|
84
|
+
"timeoutMs": 30000
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Returns `{ ok, exitCode, stdout, stderr, error, truncated, isTimeout }` — e.g.
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{ "ok": true, "exitCode": 0, "stdout": "cpu* up 12 idle 8 ...\n", "stderr": "", "error": "", "truncated": false, "isTimeout": false }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**2 · Read / write files (no sync needed)**:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "path": "~/project/config.yaml", "content": "lr: 0.001\nepochs: 50\n" }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{ "path": "~/project/train.py" }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`remote_ssh_cat` transfers base64 (binary-safe); `remote_ssh_write` is atomic (temp file + rename).
|
|
105
|
+
|
|
106
|
+
**3 · Long jobs and hung-command recovery**:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{ "command": "cd ~/project && bash run_train.sh", "timeoutMs": 0 }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{ "all": true }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`timeoutMs: 0` disables the timeout for that call; `DSH_REMOTE_SSH_CMD_TIMEOUT_MS=600000` changes the global default. Timed-out pooled sessions are discarded and rebuilt, and `remote_ssh_kill` is the manual hatch.
|
|
117
|
+
|
|
118
|
+
**4 · Tool calls inside a remote workspace** (`profileId` omitted, relative paths resolve against the remote root):
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{ "path": "configs/exp1.yaml" }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**5 · Import connections from `~/.ssh/config`**: Settings → Remote SSH → "Import SSH config" → tick hosts → host / user / port / keyPath / ProxyJump are filled in.
|
|
125
|
+
|
|
126
|
+
**6 · Mirror sync and push-back** (review offline, then upload in one shot):
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{ "workspaceId": "w_xxx" }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{ "workspaceId": "w_xxx" }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`remote_ssh_sync` pulls the remote tree into the local mirror; `remote_ssh_push` sends mirror changes back (tar over ssh, batched).
|
|
48
137
|
|
|
49
138
|
## Model Tools
|
|
50
139
|
|
|
@@ -75,9 +164,35 @@ All SSH commands default to a **120-second** timeout (issue #5): a hung remote c
|
|
|
75
164
|
- **Manual hatch**: `remote_ssh_kill` (or `all: true`) force-closes one or all pooled sessions at any time;
|
|
76
165
|
- Timed-out commands are **never auto-retried** (retrying a hung command just hangs again) — the model decides whether to kill the session or retry differently.
|
|
77
166
|
|
|
167
|
+
## Compatibility
|
|
168
|
+
|
|
169
|
+
**Measured matrix** (2026-09-12, all verified on real machines):
|
|
170
|
+
|
|
171
|
+
| Component | Version | Status |
|
|
172
|
+
| --- | --- | --- |
|
|
173
|
+
| DSH | 0.1.5-rc.1 (DSH Desktop v2.0.9) | ✅ host services / settings / tools / slots / upload & download interception all compatible |
|
|
174
|
+
| DSH | 0.1.2-rc.1 stable line | ✅ (the 2.3.x-era baseline) |
|
|
175
|
+
| dsh-better-sidebar | 0.15.0 – 0.18.0 | ✅ `fs.tree/read/write/search` (4-endpoint contract) |
|
|
176
|
+
| dsh-better-sidebar | 0.19.x | ⚠️ this plugin already supports the 6-endpoint contract (incl. `fs.rename`/`fs.remove`); 0.19.0/0.19.1 themselves cannot load their host half on DSH Desktop until upstream fixes it (see the warning under [Installation](#installation)) |
|
|
177
|
+
| Remote sshd | standard OpenSSH (Linux / HPC / Windows) | ✅ key auth; password auth needs `sshpass` on the host (POSIX) |
|
|
178
|
+
|
|
179
|
+
The plugin never patches DSH sources or injects into the profile dependency tree — everything mounts through the official `cordis.patch.yml` + profile mechanism.
|
|
180
|
+
|
|
181
|
+
## Troubleshooting
|
|
182
|
+
|
|
183
|
+
| Symptom | Cause and fix |
|
|
184
|
+
| --- | --- |
|
|
185
|
+
| "Test Connection" reports `Permission denied (publickey)` | ① key has a **passphrase**: the plugin runs in batch mode (`BatchMode=yes`) and cannot prompt — load it with `ssh-add` first, or strip the passphrase; ② on a Windows host where the user is in Administrators, the public key must go to `C:\ProgramData\ssh\administrators_authorized_keys`; ③ the username spelling (`user` / `.\user` / `user@domain`) must match a manual connection |
|
|
186
|
+
| Key auth fails after launching `dsh web` from git-bash | Fixed in 2.3.9: on Windows the ssh binary is pinned to the system OpenSSH absolute path (previously Git's MSYS2 ssh was picked up) |
|
|
187
|
+
| Sidebar Files tab says "Nothing here can view this kind of content yet." | `dsh-better-sidebar` host half failed to load: 0.18.1 / 0.19.0 / 0.19.1 hit the `SessionLogOffset` runtime import on DSH Desktop — downgrade to 0.18.0 or use a fixed build (upstream PR [#641](https://github.com/omdsh-dev/DSH-better-sidebar/pull/641)) |
|
|
188
|
+
| Built-in Terminal tab cannot connect | The terminal is an `ssh -tt` interactive channel and supports **key auth only**; use the Files tab and the model tools for password auth |
|
|
189
|
+
| Install fails with `minimumReleaseAge` or "No matching version" right after a release | npm supply-chain freshness policy — retry after 1–5 minutes |
|
|
190
|
+
| A command hangs forever | The 120s timeout discards the pooled session automatically; use `timeoutMs: 0` for long jobs and `remote_ssh_kill` at any time |
|
|
191
|
+
| Large files are truncated | 4MB per read, ≈6.29MB on the pooled download path (larger files fall back to a one-shot connection); use `remote_ssh_exec` with `head`/`tail` to page through |
|
|
192
|
+
|
|
78
193
|
## How It Works
|
|
79
194
|
|
|
80
|
-
The plugin registers
|
|
195
|
+
The plugin registers 6 exact routes (`/sidebar/api/fs.tree`, `fs.read`, `fs.write`, `fs.search`, plus `fs.rename` and `fs.remove` added by better-sidebar 0.19) that intercept better-sidebar's prefix route. When the session cwd contains `.remote-ssh.json`, requests go through SSH; otherwise local fs. The client sees local mirror paths — the Host transparently translates them to remote paths.
|
|
81
196
|
|
|
82
197
|
Remote reads use a **single-roundtrip merged read**: one pooled command returns the `size/mtime` frame plus the file content (text extensions prefer raw transfer with byte-length + U+FFFD validation and automatic base64 fallback — results are byte-identical), combined with host-side result caching and change invalidation (see below).
|
|
83
198
|
|
|
@@ -101,6 +216,14 @@ May it connect us as closely as it connects to distant supercomputers. Happy Qix
|
|
|
101
216
|
|
|
102
217
|
—— August 18, 2026
|
|
103
218
|
|
|
219
|
+
## Changelog
|
|
220
|
+
|
|
221
|
+
Version history and per-release details live in [CHANGELOG.md](./CHANGELOG.md) (latest: 2.4.3 better-sidebar 0.19 endpoints, 2.4.2 settings icon flash, 2.4.0 command-level timeout + `remote_ssh_kill`).
|
|
222
|
+
|
|
104
223
|
## License
|
|
105
224
|
|
|
106
225
|
[MIT](./LICENSE)
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
If this plugin helps you, a ⭐ [star on GitHub](https://github.com/ZhangFengshun/dsh-remote-ssh) or a favourite on [DSH Market](https://dshmarket.com) helps more people who work on remote supercomputers find it.
|
package/lib/index.js
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import z from "schemastery";
|
|
12
12
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
13
|
-
import { mkdir, rm, writeFile, readFile, opendir, stat, open, rename, readdir } from "node:fs/promises";
|
|
13
|
+
import { mkdir, rm, writeFile, readFile, opendir, stat, open, rename, readdir, lstat } from "node:fs/promises";
|
|
14
14
|
import { existsSync, readFileSync } from "node:fs";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
|
-
import { join, dirname } from "node:path";
|
|
16
|
+
import { join, dirname, basename } from "node:path";
|
|
17
17
|
|
|
18
18
|
/** Plugin identity for cordis.yml rows. */
|
|
19
19
|
const name = "@zhangfengshun/dsh-remote-ssh";
|
|
@@ -2139,6 +2139,28 @@ function apply(ctx, config) {
|
|
|
2139
2139
|
return { ok: true };
|
|
2140
2140
|
}
|
|
2141
2141
|
|
|
2142
|
+
// 本地 rename(对齐 better-sidebar 0.19 的 fs.rename 契约:单段名、不覆盖、返回新绝对路径)
|
|
2143
|
+
async function localRenameEntry(path, name) {
|
|
2144
|
+
if (!path) return { ok: false, error: "path 为必填项" };
|
|
2145
|
+
if (!name || name === "." || name === ".." || /[\\/]/.test(name)) {
|
|
2146
|
+
return { ok: false, error: "name 必须是单个路径段" };
|
|
2147
|
+
}
|
|
2148
|
+
if (basename(path) === name) return { ok: true, path: path };
|
|
2149
|
+
const dest = join(dirname(path), name);
|
|
2150
|
+
if (existsSync(dest)) return { ok: false, error: "\"" + name + "\" 已存在" };
|
|
2151
|
+
await rename(path, dest);
|
|
2152
|
+
return { ok: true, path: dest };
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// 本地 remove(文件 unlink / 目录递归;lstat 判定,符号链接只删链接本身)
|
|
2156
|
+
async function localRemoveEntry(path) {
|
|
2157
|
+
if (!path) return { ok: false, error: "path 为必填项" };
|
|
2158
|
+
const st = await lstat(path);
|
|
2159
|
+
if (st.isDirectory()) await rm(path, { recursive: true, force: true });
|
|
2160
|
+
else await rm(path, { force: true });
|
|
2161
|
+
return { ok: true, path: path };
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2142
2164
|
// 本地 searchFiles(简单递归文件名搜索)
|
|
2143
2165
|
async function localSearchFiles(root, query) {
|
|
2144
2166
|
if (!query) return { entries: [] };
|
|
@@ -2247,6 +2269,46 @@ function apply(ctx, config) {
|
|
|
2247
2269
|
result = { entries: (r.files || []).map(function (f) {
|
|
2248
2270
|
return { path: join(remoteBase, f), isDir: false };
|
|
2249
2271
|
}), truncated: !!r.truncated };
|
|
2272
|
+
} else if (method === "fs.rename") {
|
|
2273
|
+
// better-sidebar 0.19 新增端点:重命名必须落在远端,绝不能让本地镜像目录被改。
|
|
2274
|
+
const localPath = payload.path || "";
|
|
2275
|
+
const name = String(payload.name || "");
|
|
2276
|
+
if (!localPath) { writeJson(res, 400, { ok: false, error: { code: "bad-request", message: "path is required" } }); return; }
|
|
2277
|
+
if (!name || name === "." || name === ".." || /[\\/]/.test(name)) {
|
|
2278
|
+
writeJson(res, 400, { ok: false, error: { code: "bad-request", message: "name must be a single path segment" } }); return;
|
|
2279
|
+
}
|
|
2280
|
+
const srcRemote = localToRemote(localPath, remoteBase, remoteInfo.remotePath);
|
|
2281
|
+
if (srcRemote === remoteInfo.remotePath) {
|
|
2282
|
+
writeJson(res, 400, { ok: false, error: { code: "fs-error", message: "cannot rename the workspace root" } }); return;
|
|
2283
|
+
}
|
|
2284
|
+
const dstRemote = posixDirname(srcRemote) + "/" + name;
|
|
2285
|
+
const exists = await runPooled(profile, "test -e " + shellQuotePath(dstRemote), undefined, 4096);
|
|
2286
|
+
if (exists.ok) {
|
|
2287
|
+
writeJson(res, 409, { ok: false, error: { code: "fs-error", message: "\"" + name + "\" already exists" } }); return;
|
|
2288
|
+
}
|
|
2289
|
+
const mv = await runPooled(profile, "mv -f -- " + shellQuotePath(srcRemote) + " " + shellQuotePath(dstRemote), undefined, 4096);
|
|
2290
|
+
if (!mv.ok) {
|
|
2291
|
+
writeJson(res, 400, { ok: false, error: { code: "fs-error", message: String(mv.stderr || mv.error || "rename failed").trim().slice(0, 500) } }); return;
|
|
2292
|
+
}
|
|
2293
|
+
invalidateRemoteCaches(profile, srcRemote, { subtree: true });
|
|
2294
|
+
invalidateRemoteCaches(profile, dstRemote, { subtree: true });
|
|
2295
|
+
result = { path: join(dirname(localPath), name) };
|
|
2296
|
+
} else if (method === "fs.remove") {
|
|
2297
|
+
// better-sidebar 0.19 新增端点:删除同样必须在远端执行(本地镜像仅供注册工作区)。
|
|
2298
|
+
const localPath = payload.path || "";
|
|
2299
|
+
if (!localPath) { writeJson(res, 400, { ok: false, error: { code: "bad-request", message: "path is required" } }); return; }
|
|
2300
|
+
const srcRemote = localToRemote(localPath, remoteBase, remoteInfo.remotePath);
|
|
2301
|
+
if (srcRemote === remoteInfo.remotePath) {
|
|
2302
|
+
writeJson(res, 400, { ok: false, error: { code: "fs-error", message: "cannot remove the workspace root" } }); return;
|
|
2303
|
+
}
|
|
2304
|
+
const rmv = await runPooled(profile, "rm -rf -- " + shellQuotePath(srcRemote), undefined, 4096);
|
|
2305
|
+
if (!rmv.ok) {
|
|
2306
|
+
writeJson(res, 400, { ok: false, error: { code: "fs-error", message: String(rmv.stderr || rmv.error || "remove failed").trim().slice(0, 500) } }); return;
|
|
2307
|
+
}
|
|
2308
|
+
// 本地镜像若有同名副本一并清理(镜像不是事实来源,失败可忽略)。
|
|
2309
|
+
try { await rm(localPath, { recursive: true, force: true }); } catch (e) {}
|
|
2310
|
+
invalidateRemoteCaches(profile, srcRemote, { subtree: true });
|
|
2311
|
+
result = { path: localPath };
|
|
2250
2312
|
} else {
|
|
2251
2313
|
writeJson(res, 404, { ok: false, error: { code: "not-found", message: "unknown method " + method } }); return;
|
|
2252
2314
|
}
|
|
@@ -2261,6 +2323,10 @@ function apply(ctx, config) {
|
|
|
2261
2323
|
result = await localWriteFile(payload.path, payload.content || "");
|
|
2262
2324
|
} else if (method === "fs.search") {
|
|
2263
2325
|
result = await localSearchFiles(sessionCwd, payload.query || "");
|
|
2326
|
+
} else if (method === "fs.rename") {
|
|
2327
|
+
result = await localRenameEntry(payload.path, String(payload.name || ""));
|
|
2328
|
+
} else if (method === "fs.remove") {
|
|
2329
|
+
result = await localRemoveEntry(payload.path);
|
|
2264
2330
|
} else {
|
|
2265
2331
|
writeJson(res, 404, { ok: false, error: { code: "not-found", message: "unknown method " + method } }); return;
|
|
2266
2332
|
}
|
|
@@ -2275,7 +2341,9 @@ function apply(ctx, config) {
|
|
|
2275
2341
|
}
|
|
2276
2342
|
}
|
|
2277
2343
|
|
|
2278
|
-
|
|
2344
|
+
// fs.rename / fs.remove 是 better-sidebar 0.19 新增端点:旧版客户端不会调用,
|
|
2345
|
+
// 注册它们对旧版无副作用;新版下远程工作区的重命名/删除才不会落到本地镜像。
|
|
2346
|
+
["fs.tree", "fs.read", "fs.write", "fs.search", "fs.rename", "fs.remove"].forEach(function (m) {
|
|
2279
2347
|
ctx.effect(() => ctx.webServer.register({
|
|
2280
2348
|
kind: "exact",
|
|
2281
2349
|
path: "/sidebar/api/" + m,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhangfengshun/dsh-remote-ssh",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.4",
|
|
4
4
|
"description": "DSH web plugin: VSCode Remote-SSH-like remote development (SSH to supercomputers/servers, remote workspace, file explorer, integrated terminal), integrated with dsh-better-sidebar and DSH settings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|