@zhangfengshun/dsh-remote-ssh 1.8.2 → 1.9.0
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 +14 -0
- package/README.md +50 -30
- package/lib/client.js +56 -59
- package/lib/index.js +72 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
本文件的版本号与 `package.json` 的 `version` 保持一致。每个版本对应一个 Cordis Package 快照(`pkg-N`)。
|
|
4
4
|
|
|
5
|
+
## [1.9.0] — 远程文件/终端合并到内置页签
|
|
6
|
+
### 重大变更
|
|
7
|
+
- **移除 `remssh:files` 和 `remssh:term` 页签**:远程文件和远程终端不再使用独立侧边栏页签。
|
|
8
|
+
- **远程文件 → 内置「文件」页签**:创建远程工作区时自动 `tar | ssh` 同步远程文件到本地镜像目录,内置「文件」页签直接显示真实远程文件。编辑后通过设置页「同步/推送」按钮或 `remote_ssh_push` 工具回传。
|
|
9
|
+
- **远程终端 → 内置「终端」页签**:插件启动时自动生成跨平台 shell wrapper 脚本(`~/.dsh/remote-ssh/dsh-remote-shell[.cmd]`)。在 DSH 设置中将 better-sidebar 的 `shell` 指向该脚本后,内置「终端」页签在远程工作区中自动 SSH 到远程主机,在本地工作区中照常启动本地 shell。
|
|
10
|
+
- **设置页新增「远程工作区」管理区**:列出所有远程工作区,支持同步/推送/删除操作。
|
|
11
|
+
- **创建工作区时写入 `.remote-ssh.json`**:镜像目录中写入连接信息(host/port/user/keyPath/proxyJump/remotePath),供 shell wrapper 读取。
|
|
12
|
+
- 保留隐藏的 `remssh:editor` 页签用于远程文件编辑器。
|
|
13
|
+
- ⚠️ 终端透明接入仅支持**密钥认证**(密码无法安全传入 wrapper 脚本)。
|
|
14
|
+
|
|
15
|
+
## [1.8.3] — 浏览按钮改回「打开」
|
|
16
|
+
### 变更
|
|
17
|
+
- 「浏览」按钮改回「打开」:按输入框路径加载该路径下的文件树,空路径时打开主目录。本地/远程行为一致,不再调用 `pickDirectory`(该原生对话框在仅有 `browse` capability 的环境下不可用)。回车导航保留。
|
|
18
|
+
|
|
5
19
|
## [1.8.2] — 修复浏览按钮:本地回退 + 远程打开主目录
|
|
6
20
|
### 修复
|
|
7
21
|
- **本地浏览**:`pickDirectory()` 在仅有 `browse` capability(无 `native`)的 DSH 环境下会报错 `host.pickDirectory needs the native capability`。现改为:尝试原生选择框,失败时静默回退到加载用户主目录(`listDirectory` 默认列举 home)。用户取消也回到主目录。
|
package/README.md
CHANGED
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
| 🔌 远程连接 | SSH 连接超算 / 服务器,密钥认证(推荐)或密码认证(需本机 `sshpass`),内置「测试连接」;支持 `ProxyJump` 跳板机 |
|
|
12
12
|
| ⚡ 连接复用 | 持久 SSH 会话池,文件操作复用同一条已认证连接,不再每次握手(首次后近乎瞬时) |
|
|
13
13
|
| 📥 配置导入 | 一键从 `~/.ssh/config`(递归 `Include`)发现主机并批量导入连接配置 |
|
|
14
|
-
| 🗂️ 远程文件 |
|
|
15
|
-
| 💻 远程终端 |
|
|
16
|
-
| 🔄 双向同步 |
|
|
17
|
-
| 🌐 远程工作区 | 选择远程目录创建**原生工作区**(本地镜像目录 +
|
|
14
|
+
| 🗂️ 远程文件 | 创建远程工作区时自动同步到本地镜像,内置「文件」页签直接显示远程文件;编辑后通过设置页同步/推送回传 |
|
|
15
|
+
| 💻 远程终端 | 内置「终端」页签自动通过 shell wrapper 检测远程工作区,打开 SSH 交互式终端(仅密钥认证) |
|
|
16
|
+
| 🔄 双向同步 | 设置页一键同步(远端 → 本地镜像)/ 推送(本地镜像 → 远端),或通过 `remote_ssh_sync` / `remote_ssh_push` 工具 |
|
|
17
|
+
| 🌐 远程工作区 | 选择远程目录创建**原生工作区**(本地镜像目录 + 原生注册 + 自动同步),一键打开进入远程环境 |
|
|
18
18
|
| 🤖 模型工具 | 12 个 `remote_ssh_*` 工具,模型可读写远程文件、执行远程命令、内容搜索、文件名查找、目录/删除/移动、双向同步;在远程工作区会话中免填连接参数 |
|
|
19
19
|
| 🌍 国际化 | 界面文案 + 工具描述中英双语,通过 `ctx.locale` 跟随 DSH 语言设置自动切换 |
|
|
20
|
-
| 🧠 会话记忆 | 切换会话后,「远程文件」「远程终端」页签记住已选连接、浏览目录、打开的文件与终端 |
|
|
21
20
|
|
|
22
21
|
## 安装
|
|
23
22
|
|
|
@@ -30,7 +29,7 @@ dsh plugin --profile <name> add /absolute/path/to/dsh-remote-ssh
|
|
|
30
29
|
### 发布后安装
|
|
31
30
|
|
|
32
31
|
```bash
|
|
33
|
-
dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@1.
|
|
32
|
+
dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@1.9.0
|
|
34
33
|
```
|
|
35
34
|
|
|
36
35
|
> ⚠️ 安装后需**重启 DSH** 才生效;后续仅修改 Client 半边时刷新浏览器即可。
|
|
@@ -39,8 +38,9 @@ dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@1.8.2
|
|
|
39
38
|
|
|
40
39
|
1. 安装插件并重启 DSH。
|
|
41
40
|
2. 打开 **设置 → 🖥️ 远程连接**,添加一条连接(主机 / 端口 / 用户名 / 密钥或密码),点「测试连接」验证。
|
|
42
|
-
3.
|
|
43
|
-
4.
|
|
41
|
+
3. 需要把远程目录当作工作区时:走 DSH 原生「添加工作区」流程,在弹窗中选择「**选择远程目录…**」→ 选择连接 → 浏览并选择远程目录。
|
|
42
|
+
4. 创建后远程文件自动同步到本地镜像,内置「文件」页签直接可见。
|
|
43
|
+
5. **(可选)远程终端**:在 DSH 设置中把 better-sidebar 的 `shell` 配置指向 wrapper 脚本(见下方「远程终端配置」),内置「终端」页签即可在远程工作区中自动打开 SSH 终端。
|
|
44
44
|
|
|
45
45
|
## 使用指南
|
|
46
46
|
|
|
@@ -60,22 +60,34 @@ dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@1.8.2
|
|
|
60
60
|
|
|
61
61
|
连接配置持久化在 DSH 设置命名空间 `dsh-remote-ssh` 中(密码字段 `role('secret')` 脱敏)。
|
|
62
62
|
|
|
63
|
-
### 2.
|
|
63
|
+
### 2. 远程文件(内置「文件」页签)
|
|
64
64
|
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
65
|
+
- 创建远程工作区时,远程文件自动同步到本地镜像目录,内置「文件」页签直接显示;
|
|
66
|
+
- 内置编辑器编辑的是本地镜像副本,编辑后需**推送回远端**;
|
|
67
|
+
- 推送方式:设置 → 远程连接 → 远程工作区 → 点「⬆ 推送」按钮,或模型调用 `remote_ssh_push` 工具;
|
|
68
|
+
- 远程有更新时点「⬇ 同步」重新拉取。
|
|
68
69
|
|
|
69
|
-
### 3.
|
|
70
|
+
### 3. 远程终端(内置「终端」页签)
|
|
70
71
|
|
|
71
|
-
-
|
|
72
|
-
|
|
72
|
+
通过 shell wrapper 实现:终端启动时自动检测当前工作目录下的 `.remote-ssh.json`,若存在且含密钥路径 → 自动 `ssh -tt` 连接远程主机;否则启动本地 shell。
|
|
73
|
+
|
|
74
|
+
**配置步骤**:
|
|
75
|
+
|
|
76
|
+
1. 插件启动时自动在 `~/.dsh/remote-ssh/` 下生成 wrapper 脚本:
|
|
77
|
+
- Windows: `dsh-remote-shell.cmd`(调用 `dsh-remote-shell.js`)
|
|
78
|
+
- POSIX: `dsh-remote-shell`(调用 `dsh-remote-shell.js`)
|
|
79
|
+
2. 在 DSH 设置中找到 better-sidebar 的 `shell` 配置项,填入 wrapper 脚本的完整路径:
|
|
80
|
+
- Windows: `C:\Users\<你>\.dsh\remote-ssh\dsh-remote-shell.cmd`
|
|
81
|
+
- POSIX: `/home/<你>/.dsh/remote-ssh/dsh-remote-shell`
|
|
82
|
+
3. 重启 DSH 后,在远程工作区中打开终端 → 自动 SSH 到远程主机;在本地工作区中打开终端 → 照常启动本地 shell。
|
|
83
|
+
|
|
84
|
+
> ⚠️ 终端透明接入仅支持**密钥认证**(密码无法安全传入 wrapper 脚本)。密码认证的连接仍可使用模型工具。
|
|
73
85
|
|
|
74
86
|
### 4. 远程工作区(🌐)
|
|
75
87
|
|
|
76
88
|
1. 打开 DSH 原生「添加工作区」流程;
|
|
77
89
|
2. 选择「**选择远程目录…**」→ 选择连接 → 浏览并选择远程目录;
|
|
78
|
-
3. 插件在本地生成镜像目录 `~/.dsh/remote-workspaces/<id
|
|
90
|
+
3. 插件在本地生成镜像目录 `~/.dsh/remote-workspaces/<id>`,写入 `.remote-ssh.json`(连接信息,供 shell wrapper 读取),自动同步远程文件,并注册进原生工作区列表(标题 `🌐 <名称>`);
|
|
79
91
|
4. 从工作区创建会话后,会话工作目录即该镜像目录,模型工具自动感知对应的远程连接与目录。
|
|
80
92
|
|
|
81
93
|
### 5. 模型工具(🤖)
|
|
@@ -118,13 +130,18 @@ dsh-remote-ssh/
|
|
|
118
130
|
```
|
|
119
131
|
浏览器 (Client 半边) Node 进程 (Host 半边)
|
|
120
132
|
┌───────────────────────────────┐ fetch ┌──────────────────────────────────┐
|
|
121
|
-
│ ctx.betterSidebar.registerTab
|
|
122
|
-
│ ·
|
|
123
|
-
│ ctx.slots: settings.section
|
|
124
|
-
│ ·
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
│ ctx.betterSidebar.registerTab │ ──────▶ │ ctx.webServer /remote-ssh/api/* │
|
|
134
|
+
│ · remssh:editor (隐藏) │ ◀────── │ · SSH:listDir/readFile/… │
|
|
135
|
+
│ ctx.slots: settings.section │ JSON │ · settings:profiles/workspaces │
|
|
136
|
+
│ · 连接配置 + 工作区管理 │ │ · 自动同步 + shell wrapper 生成 │
|
|
137
|
+
│ ctx.slots: directoryFlow │ │ ctx.tools:12 个 remote_ssh_* │
|
|
138
|
+
│ · 本地 / 远程目录两选弹窗 │ └───────────────┬──────────────────┘
|
|
139
|
+
└───────────────────────────────┘ │ SSH
|
|
140
|
+
┌─────────────┴──────────┐
|
|
141
|
+
│ ~/.dsh/remote-ssh/ │
|
|
142
|
+
│ dsh-remote-shell.js │
|
|
143
|
+
│ dsh-remote-shell[.cmd]│ ← better-sidebar shell 指向此文件
|
|
144
|
+
└────────────────────────┘
|
|
128
145
|
远程超算 / 服务器
|
|
129
146
|
```
|
|
130
147
|
|
|
@@ -134,24 +151,27 @@ dsh-remote-ssh/
|
|
|
134
151
|
- **二进制安全读文件**:远程 `base64 -w0` 输出,Host 用 `Buffer` 解码为 UTF-8。
|
|
135
152
|
- **写文件走 stdin**:远程 `cat > <file>`,内容经子进程 stdin 传入。
|
|
136
153
|
- **文件列举**:`find -printf '%Y\t%f\t%s\n'`(`%Y` 跟随软链接取目标类型,适配超算家目录软链接)。
|
|
137
|
-
-
|
|
154
|
+
- **自动同步**:创建远程工作区时立即 `tar | ssh` 拉取远程文件到本地镜像,使内置「文件」页签可见真实远程文件。
|
|
155
|
+
- **Shell wrapper**:插件启动时生成跨平台 wrapper 脚本(Node.js + 薄壳),检测工作区 `.remote-ssh.json` 自动 SSH,使内置「终端」页签透明接入远程。
|
|
138
156
|
- **国际化**:Client 接入 `ctx.locale`,注册 `zh` / `en` 词典,界面与工具描述跟随 DSH 语言自动切换。
|
|
139
157
|
|
|
140
158
|
## 已知限制
|
|
141
159
|
|
|
142
|
-
1.
|
|
143
|
-
2.
|
|
144
|
-
3.
|
|
145
|
-
4.
|
|
146
|
-
5.
|
|
160
|
+
1. **终端透明接入仅限密钥认证**:shell wrapper 通过 `.remote-ssh.json` 读取密钥路径,密码认证无法安全传入。密码认证的连接仍可使用模型工具和远程文件操作。
|
|
161
|
+
2. **终端需手动配置 shell**:需在 DSH 设置中把 better-sidebar 的 `shell` 指向 wrapper 脚本路径,终端透明接入才生效。
|
|
162
|
+
3. **文件操作依赖 GNU 工具**(`find -printf`、`base64 -w0`):目标为 Linux 超算时通用。
|
|
163
|
+
4. **密码认证需本机 `sshpass`**(Windows 默认没有);密钥认证无此依赖。
|
|
164
|
+
5. **同步为全量 tar**:`remote_ssh_sync` / `remote_ssh_push` 用 `tar` 流式全量同步,单次同步大目录耗时与传输量较高,后续可换 `rsync -e ssh` 增量。
|
|
165
|
+
6. **内置编辑器编辑的是本地镜像副本**,不会自动回传 —— 须在设置页点「推送」或调用 `remote_ssh_push` 同步回远端。
|
|
147
166
|
|
|
148
167
|
## 路线图
|
|
149
168
|
|
|
150
169
|
- [x] 从 `~/.ssh/config` 自动导入连接
|
|
151
170
|
- [x] ProxyJump / 跳板机、SSH config 复用
|
|
152
171
|
- [x] `tar`-over-ssh 双向同步(`scp` / `sftp` 增量同步为后续优化)
|
|
172
|
+
- [x] 远程文件合并到内置「文件」页签(自动同步 + 镜像)
|
|
173
|
+
- [x] 远程终端合并到内置「终端」页签(shell wrapper 透明 SSH)
|
|
153
174
|
- [ ] `rsync -e ssh` 增量同步
|
|
154
|
-
- [ ] 真 PTY 终端(`terminals.registerBackend` + `node-pty`)
|
|
155
175
|
- [ ] 密码凭据走 `credentials` 服务(避免明文进 argv)
|
|
156
176
|
|
|
157
177
|
## 许可证
|
package/lib/client.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zhangfengshun/dsh-remote-ssh — Client 半边(静态 DSH Web 插件,浏览器 bundle)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* 远程文件与远程终端已合并到内置「文件」和「终端」页签:
|
|
5
|
+
* - 远程文件:创建远程工作区时自动同步到本地镜像目录,内置「文件」页签直接显示远程文件。
|
|
6
|
+
* 编辑后通过设置页的「同步/推送」按钮回传远程(或使用 remote_ssh_push 工具)。
|
|
7
|
+
* - 远程终端:shell wrapper 自动检测工作区 .remote-ssh.json,在远程工作区中打开 SSH 终端。
|
|
8
|
+
* 需在 better-sidebar 设置中将 shell 指向 wrapper 脚本(见 README)。
|
|
9
|
+
* 仍保留隐藏的 remssh:editor 页签用于远程文件编辑器。
|
|
10
|
+
* 在 DSH 设置页注册「远程连接」小节管理连接配置和远程工作区;
|
|
11
|
+
* 同时接管原生「添加工作区」流程提供「本地目录 / 远程目录」两选。
|
|
10
12
|
*/
|
|
11
13
|
window.__ModuleLoader__.load({
|
|
12
14
|
id: "@zhangfengshun/dsh-remote-ssh",
|
|
@@ -66,6 +68,7 @@ window.__ModuleLoader__.load({
|
|
|
66
68
|
"settings.desc": "管理 Remote-SSH 的远程连接配置(SSH 密钥或密码认证)。",
|
|
67
69
|
"settings.empty": "还没有连接配置。请在下方添加。",
|
|
68
70
|
"settings.add": "添加连接",
|
|
71
|
+
"settings.workspaces": "远程工作区",
|
|
69
72
|
"settings.name": "名称",
|
|
70
73
|
"settings.namePlaceholder": "如:我的超算",
|
|
71
74
|
"settings.host": "主机 host",
|
|
@@ -165,6 +168,7 @@ window.__ModuleLoader__.load({
|
|
|
165
168
|
"settings.desc": "Manage Remote-SSH connection profiles (SSH key or password authentication).",
|
|
166
169
|
"settings.empty": "No connection profiles yet. Add one below.",
|
|
167
170
|
"settings.add": "Add Profile",
|
|
171
|
+
"settings.workspaces": "Remote Workspaces",
|
|
168
172
|
"settings.name": "Name",
|
|
169
173
|
"settings.namePlaceholder": "e.g. My HPC",
|
|
170
174
|
"settings.host": "Host",
|
|
@@ -790,8 +794,6 @@ window.__ModuleLoader__.load({
|
|
|
790
794
|
|
|
791
795
|
var listFnRef = React.useRef(props.listFn);
|
|
792
796
|
listFnRef.current = props.listFn;
|
|
793
|
-
var pickRef = React.useRef(props.pickNative);
|
|
794
|
-
pickRef.current = props.pickNative;
|
|
795
797
|
|
|
796
798
|
function load(p) {
|
|
797
799
|
setLoading(true); setErr(null);
|
|
@@ -809,35 +811,19 @@ window.__ModuleLoader__.load({
|
|
|
809
811
|
if (up !== null) load(up);
|
|
810
812
|
}
|
|
811
813
|
|
|
812
|
-
//
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
// 远程模式:打开远程用户主目录(load(undefined) → remoteRoot/~)。
|
|
816
|
-
function browse() {
|
|
817
|
-
if (pickRef.current) {
|
|
818
|
-
setLoading(true); setErr(null);
|
|
819
|
-
pickRef.current().then(function (picked) {
|
|
820
|
-
setLoading(false);
|
|
821
|
-
if (picked) load(picked);
|
|
822
|
-
else load(undefined); // 用户取消 → 回到主目录
|
|
823
|
-
}).catch(function (e) {
|
|
824
|
-
setLoading(false);
|
|
825
|
-
// native capability 不可用("browse" only)→ 回退到加载主目录
|
|
826
|
-
load(undefined);
|
|
827
|
-
});
|
|
828
|
-
} else {
|
|
829
|
-
load(undefined); // 远程:打开用户主目录(~)
|
|
830
|
-
}
|
|
814
|
+
// 打开按钮:按输入框路径加载该路径下的文件树。空路径时打开主目录。
|
|
815
|
+
function open() {
|
|
816
|
+
load(path || undefined);
|
|
831
817
|
}
|
|
832
818
|
|
|
833
|
-
// 路径栏:输入框(回车导航)+
|
|
819
|
+
// 路径栏:输入框(回车导航)+ 打开按钮(紧挨在右侧)+ 上级
|
|
834
820
|
return h("div", null,
|
|
835
821
|
h("div", { className: "rssh-pathbar" },
|
|
836
822
|
h("input", { className: "rssh-input", value: path,
|
|
837
823
|
onChange: function (e) { setPath(e.target.value); },
|
|
838
|
-
onKeyDown: function (e) { if (e.key === "Enter") { e.preventDefault();
|
|
824
|
+
onKeyDown: function (e) { if (e.key === "Enter") { e.preventDefault(); open(); } },
|
|
839
825
|
placeholder: props.placeholder || t("picker.dir") }),
|
|
840
|
-
h("button", { className: "rssh-btn", onClick:
|
|
826
|
+
h("button", { className: "rssh-btn", onClick: open, disabled: loading }, t("common.open")),
|
|
841
827
|
h("button", { className: "rssh-btn", onClick: goUp }, t("common.up"))
|
|
842
828
|
),
|
|
843
829
|
loading ? h("div", { className: "rssh-muted" }, t("common.loading")) : null,
|
|
@@ -906,7 +892,7 @@ window.__ModuleLoader__.load({
|
|
|
906
892
|
isRemote
|
|
907
893
|
? (profileId ? h(DirPicker, { resetKey: profileId, placeholder: t("flow.remoteDir"), listFn: function (p) { return remoteListFn(profileId, p); }, onChoose: chooseRemote, disabled: creating })
|
|
908
894
|
: h("div", { className: "rssh-pick-empty" }, t("common.selectProfile")))
|
|
909
|
-
: h(DirPicker, { resetKey: "local", placeholder: t("flow.localDir"), listFn: function (p) { return localListFn(props.workspaces, p); },
|
|
895
|
+
: h(DirPicker, { resetKey: "local", placeholder: t("flow.localDir"), listFn: function (p) { return localListFn(props.workspaces, p); }, onChoose: chooseLocal })
|
|
910
896
|
)
|
|
911
897
|
);
|
|
912
898
|
}
|
|
@@ -917,15 +903,34 @@ window.__ModuleLoader__.load({
|
|
|
917
903
|
function SettingsSection(props) {
|
|
918
904
|
var t = useT();
|
|
919
905
|
var [profiles, setProfiles] = React.useState([]);
|
|
906
|
+
var [workspaces, setWorkspaces] = React.useState([]);
|
|
920
907
|
var [form, setForm] = React.useState({ name: "", host: "", port: "22", user: "", authMethod: "key", keyPath: "", password: "", remoteRoot: "~", proxyJump: "" });
|
|
921
908
|
var [result, setResult] = React.useState(null);
|
|
922
909
|
var [busy, setBusy] = React.useState(false);
|
|
923
910
|
var [importDlg, setImportDlg] = React.useState(null);
|
|
911
|
+
var [wsSync, setWsSync] = React.useState({});
|
|
924
912
|
var set = function (k, v) { setForm(function (f) { var n = {}; for (var key in f) n[key] = f[key]; n[k] = v; return n; }); };
|
|
925
913
|
|
|
926
|
-
function reload() {
|
|
914
|
+
function reload() {
|
|
915
|
+
api("listProfiles").then(function (r) { if (r && r.ok) setProfiles(r.profiles || []); });
|
|
916
|
+
api("listWorkspaces").then(function (r) { if (r && r.ok) setWorkspaces(r.workspaces || []); });
|
|
917
|
+
}
|
|
927
918
|
React.useEffect(reload, []);
|
|
928
919
|
|
|
920
|
+
function profileName(id) {
|
|
921
|
+
var p = profiles.find(function (x) { return x.id === id; });
|
|
922
|
+
return p ? p.name : id;
|
|
923
|
+
}
|
|
924
|
+
function doWsSync(wsId, dir) {
|
|
925
|
+
setWsSync(function (s) { var n = {}; for (var k in s) n[k] = s[k]; n[wsId] = { busy: true, msg: dir === "down" ? t("sync.syncing") : t("sync.pushing"), ok: null }; return n; });
|
|
926
|
+
api(dir === "down" ? "syncDown" : "syncUp", { workspaceId: wsId }).then(function (r) {
|
|
927
|
+
setWsSync(function (s) { var n = {}; for (var k in s) n[k] = s[k]; n[wsId] = { busy: false, msg: r && r.ok ? t("sync.done") : ((r && r.error) || t("sync.fail")), ok: !!(r && r.ok) }; return n; });
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
function delWs(wsId) {
|
|
931
|
+
api("deleteWorkspace", { id: wsId }).then(function (r) { if (r && r.ok) setWorkspaces(r.workspaces || []); });
|
|
932
|
+
}
|
|
933
|
+
|
|
929
934
|
function save() {
|
|
930
935
|
setBusy(true); setResult(null);
|
|
931
936
|
api("saveProfile", Object.assign({}, form, { port: parseInt(form.port, 10) || 22 })).then(function (r) {
|
|
@@ -1003,6 +1008,22 @@ window.__ModuleLoader__.load({
|
|
|
1003
1008
|
);
|
|
1004
1009
|
}),
|
|
1005
1010
|
profiles.length === 0 ? h("div", { className: "rssh-empty" }, t("settings.empty")) : null,
|
|
1011
|
+
workspaces.length > 0 ? h("div", { className: "rssh-card" },
|
|
1012
|
+
h("div", { className: "rssh-title" }, t("settings.workspaces")),
|
|
1013
|
+
workspaces.map(function (ws) {
|
|
1014
|
+
var st = wsSync[ws.id];
|
|
1015
|
+
return h("div", { key: ws.id, className: "rssh-card", style: { marginBottom: 6 } },
|
|
1016
|
+
h("div", { className: "rssh-title" }, ws.title || ws.id),
|
|
1017
|
+
h("div", { className: "rssh-sub" }, profileName(ws.profileId) + " → " + (ws.remotePath || "")),
|
|
1018
|
+
h("div", { className: "rssh-actions" },
|
|
1019
|
+
h("button", { className: "rssh-btn", onClick: function () { doWsSync(ws.id, "down"); }, disabled: st && st.busy }, t("sync.down")),
|
|
1020
|
+
h("button", { className: "rssh-btn", onClick: function () { doWsSync(ws.id, "up"); }, disabled: st && st.busy }, t("sync.up")),
|
|
1021
|
+
h("button", { className: "rssh-btn", onClick: function () { delWs(ws.id); } }, t("common.delete"))
|
|
1022
|
+
),
|
|
1023
|
+
st ? h("span", { className: st.ok === false ? "rssh-err" : (st.ok ? "rssh-ok" : "rssh-muted"), style: { fontSize: 11 } }, st.msg) : null
|
|
1024
|
+
);
|
|
1025
|
+
})
|
|
1026
|
+
) : null,
|
|
1006
1027
|
h("div", { className: "rssh-card" },
|
|
1007
1028
|
h("div", { className: "rssh-title" }, t("settings.add")),
|
|
1008
1029
|
Field({ label: t("settings.name"), value: form.name, onChange: function (v) { set("name", v); }, placeholder: t("settings.namePlaceholder") }),
|
|
@@ -1073,20 +1094,9 @@ window.__ModuleLoader__.load({
|
|
|
1073
1094
|
});
|
|
1074
1095
|
|
|
1075
1096
|
if (betterSidebar) {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
title: function () { return i18n.t("tabs.files"); },
|
|
1080
|
-
order: 90,
|
|
1081
|
-
single: true,
|
|
1082
|
-
icon: function () { return h("span", null, "🗂️"); },
|
|
1083
|
-
component: function (props) {
|
|
1084
|
-
var sid = props && props.scope ? props.scope.sessionId : null;
|
|
1085
|
-
return h(FilesTab, { key: "files:" + (sid || "global"), sessionId: sid, betterSidebar: props && props.ctx ? props.ctx.betterSidebar : null });
|
|
1086
|
-
}
|
|
1087
|
-
});
|
|
1088
|
-
});
|
|
1089
|
-
// 远程文件编辑器页签(隐藏页签,仅由文件树点击打开,每个远程文件单开一栏)。
|
|
1097
|
+
// 远程文件编辑器页签(隐藏页签,仅由代码打开,每个远程文件单开一栏)。
|
|
1098
|
+
// remssh:files 和 remssh:term 已移除:远程文件由内置「文件」页签显示(自动同步到镜像目录),
|
|
1099
|
+
// 远程终端由内置「终端」页签提供(shell wrapper 自动 SSH)。
|
|
1090
1100
|
ctx.effect(function () {
|
|
1091
1101
|
return betterSidebar.registerTab({
|
|
1092
1102
|
id: "remssh:editor",
|
|
@@ -1099,19 +1109,6 @@ window.__ModuleLoader__.load({
|
|
|
1099
1109
|
}
|
|
1100
1110
|
});
|
|
1101
1111
|
});
|
|
1102
|
-
ctx.effect(function () {
|
|
1103
|
-
return betterSidebar.registerTab({
|
|
1104
|
-
id: "remssh:term",
|
|
1105
|
-
title: function () { return i18n.t("tabs.term"); },
|
|
1106
|
-
order: 91,
|
|
1107
|
-
single: true,
|
|
1108
|
-
icon: function () { return h("span", null, "💻"); },
|
|
1109
|
-
component: function (props) {
|
|
1110
|
-
var sid = props && props.scope ? props.scope.sessionId : null;
|
|
1111
|
-
return h(TerminalTab, { key: "term:" + (sid || "global"), sessionId: sid });
|
|
1112
|
-
}
|
|
1113
|
-
});
|
|
1114
|
-
});
|
|
1115
1112
|
}
|
|
1116
1113
|
|
|
1117
1114
|
if (slots) {
|
package/lib/index.js
CHANGED
|
@@ -819,6 +819,18 @@ function apply(ctx, config) {
|
|
|
819
819
|
try {
|
|
820
820
|
await writeFile(join(mirrorDir, "README.md"), remoteWorkspaceReadme(p.name, a.remotePath), "utf8");
|
|
821
821
|
} catch (e) {}
|
|
822
|
+
// 写 .remote-ssh.json:供 shell wrapper 读取连接信息(仅密钥认证,不含密码)。
|
|
823
|
+
try {
|
|
824
|
+
const connInfo = {
|
|
825
|
+
profileId: p.id, host: p.host, port: p.port || 22, user: p.user,
|
|
826
|
+
keyPath: p.keyPath || "", proxyJump: p.proxyJump || "", remotePath: a.remotePath
|
|
827
|
+
};
|
|
828
|
+
await writeFile(join(mirrorDir, ".remote-ssh.json"), JSON.stringify(connInfo, null, 2), "utf8");
|
|
829
|
+
} catch (e) {}
|
|
830
|
+
// 自动同步:把远程文件拉到本地镜像,使内置「文件」页签可见真实远程文件。
|
|
831
|
+
try {
|
|
832
|
+
await remoteSyncDown(subprocess, p, a.remotePath, mirrorDir);
|
|
833
|
+
} catch (e) {}
|
|
822
834
|
let workspaceId = null;
|
|
823
835
|
if (workspaceRegistry) {
|
|
824
836
|
try {
|
|
@@ -1297,6 +1309,66 @@ function apply(ctx, config) {
|
|
|
1297
1309
|
}
|
|
1298
1310
|
});
|
|
1299
1311
|
|
|
1312
|
+
// ---- 创建 shell wrapper(供 better-sidebar 的 shell 配置指向它,实现远程终端透明接入)----
|
|
1313
|
+
// wrapper 逻辑:读取当前工作目录下的 .remote-ssh.json,若存在且含 keyPath → exec ssh -tt;
|
|
1314
|
+
// 否则降级到平台默认 shell(bash -l / powershell)。
|
|
1315
|
+
// 实现方式:跨平台 Node.js 脚本 + 平台特定的薄壳调用它。
|
|
1316
|
+
const wrapperDir = join(homedir(), ".dsh", "remote-ssh");
|
|
1317
|
+
const isWin = process.platform === "win32";
|
|
1318
|
+
const wrapperJsPath = join(wrapperDir, "dsh-remote-shell.js");
|
|
1319
|
+
const wrapperPath = isWin ? join(wrapperDir, "dsh-remote-shell.cmd") : join(wrapperDir, "dsh-remote-shell");
|
|
1320
|
+
const wrapperJs = [
|
|
1321
|
+
"// DSH remote-ssh shell wrapper — auto-detects remote workspace via .remote-ssh.json",
|
|
1322
|
+
"const fs = require('fs');",
|
|
1323
|
+
"const path = require('path');",
|
|
1324
|
+
"const { spawn } = require('child_process');",
|
|
1325
|
+
"const info = path.join(process.cwd(), '.remote-ssh.json');",
|
|
1326
|
+
"try {",
|
|
1327
|
+
" if (fs.existsSync(info)) {",
|
|
1328
|
+
" const j = JSON.parse(fs.readFileSync(info, 'utf8'));",
|
|
1329
|
+
" if (j.keyPath && j.keyPath !== '' && j.host && j.user) {",
|
|
1330
|
+
" const port = j.port || 22;",
|
|
1331
|
+
" const args = ['-tt', '-o', 'StrictHostKeyChecking=no', '-p', String(port), '-i', j.keyPath, j.user + '@' + j.host];",
|
|
1332
|
+
" if (j.proxyJump) { args.splice(2, 0, '-J', j.proxyJump); }",
|
|
1333
|
+
" const ssh = spawn('ssh', args, { stdio: 'inherit' });",
|
|
1334
|
+
" ssh.on('exit', function(c) { process.exit(c == null ? 0 : c); });",
|
|
1335
|
+
" return;",
|
|
1336
|
+
" }",
|
|
1337
|
+
" }",
|
|
1338
|
+
"} catch (e) {}",
|
|
1339
|
+
"// 默认本地 shell",
|
|
1340
|
+
"var sh = process.env.SHELL || (process.platform === 'win32' ? 'powershell.exe' : '/bin/bash');",
|
|
1341
|
+
"var shArgs = process.platform === 'win32' ? [] : ['-l'];",
|
|
1342
|
+
"var p = spawn(sh, shArgs, { stdio: 'inherit' });",
|
|
1343
|
+
"p.on('exit', function(c) { process.exit(c == null ? 0 : c); });",
|
|
1344
|
+
""
|
|
1345
|
+
].join("\n");
|
|
1346
|
+
const winCmd = [
|
|
1347
|
+
"@echo off",
|
|
1348
|
+
'node "%~dp0dsh-remote-shell.js"',
|
|
1349
|
+
""
|
|
1350
|
+
].join("\r\n");
|
|
1351
|
+
const posixSh = [
|
|
1352
|
+
"#!/usr/bin/env bash",
|
|
1353
|
+
"# DSH remote-ssh shell wrapper (POSIX) — delegates to Node.js script",
|
|
1354
|
+
'exec node "$(dirname "$0")/dsh-remote-shell.js"',
|
|
1355
|
+
""
|
|
1356
|
+
].join("\n");
|
|
1357
|
+
(async function () {
|
|
1358
|
+
try {
|
|
1359
|
+
await mkdir(wrapperDir, { recursive: true });
|
|
1360
|
+
await writeFile(wrapperJsPath, wrapperJs, "utf8");
|
|
1361
|
+
if (isWin) {
|
|
1362
|
+
await writeFile(wrapperPath, winCmd, "utf8");
|
|
1363
|
+
} else {
|
|
1364
|
+
await writeFile(wrapperPath, posixSh, "utf8");
|
|
1365
|
+
try { await (await import("node:fs/promises")).chmod(wrapperPath, 0o755); } catch (e) {}
|
|
1366
|
+
}
|
|
1367
|
+
} catch (e) {
|
|
1368
|
+
ctx.logger?.warn("[dsh-remote-ssh] 创建 shell wrapper 失败: " + String(e && e.message ? e.message : e));
|
|
1369
|
+
}
|
|
1370
|
+
})();
|
|
1371
|
+
|
|
1300
1372
|
// ---- 清理 ----
|
|
1301
1373
|
ctx.effect(() => () => {
|
|
1302
1374
|
clearInterval(idleTimer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhangfengshun/dsh-remote-ssh",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
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",
|