@zhangfengshun/dsh-remote-ssh 1.8.3 → 1.9.1

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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  本文件的版本号与 `package.json` 的 `version` 保持一致。每个版本对应一个 Cordis Package 快照(`pkg-N`)。
4
4
 
5
+ ## [1.9.1] — 安装时自动配置 better-sidebar shell
6
+ ### 改进
7
+ - **安装即生效**:`cordis.patch.yml` 新增 `id: better-sidebar` 的 `config.shell` 覆盖,用 `!!js` 动态计算 wrapper 脚本路径(适配不同平台和用户主目录)。安装插件后无需手动编辑任何配置文件,重启 DSH 即可使用远程终端透明接入。
8
+
9
+ ## [1.9.0] — 远程文件/终端合并到内置页签
10
+ ### 重大变更
11
+ - **移除 `remssh:files` 和 `remssh:term` 页签**:远程文件和远程终端不再使用独立侧边栏页签。
12
+ - **远程文件 → 内置「文件」页签**:创建远程工作区时自动 `tar | ssh` 同步远程文件到本地镜像目录,内置「文件」页签直接显示真实远程文件。编辑后通过设置页「同步/推送」按钮或 `remote_ssh_push` 工具回传。
13
+ - **远程终端 → 内置「终端」页签**:插件启动时自动生成跨平台 shell wrapper 脚本(`~/.dsh/remote-ssh/dsh-remote-shell[.cmd]`)。在 DSH 设置中将 better-sidebar 的 `shell` 指向该脚本后,内置「终端」页签在远程工作区中自动 SSH 到远程主机,在本地工作区中照常启动本地 shell。
14
+ - **设置页新增「远程工作区」管理区**:列出所有远程工作区,支持同步/推送/删除操作。
15
+ - **创建工作区时写入 `.remote-ssh.json`**:镜像目录中写入连接信息(host/port/user/keyPath/proxyJump/remotePath),供 shell wrapper 读取。
16
+ - 保留隐藏的 `remssh:editor` 页签用于远程文件编辑器。
17
+ - ⚠️ 终端透明接入仅支持**密钥认证**(密码无法安全传入 wrapper 脚本)。
18
+
5
19
  ## [1.8.3] — 浏览按钮改回「打开」
6
20
  ### 变更
7
21
  - 「浏览」按钮改回「打开」:按输入框路径加载该路径下的文件树,空路径时打开主目录。本地/远程行为一致,不再调用 `pickDirectory`(该原生对话框在仅有 `browse` capability 的环境下不可用)。回车导航保留。
package/README.md CHANGED
@@ -11,13 +11,12 @@
11
11
  | 🔌 远程连接 | SSH 连接超算 / 服务器,密钥认证(推荐)或密码认证(需本机 `sshpass`),内置「测试连接」;支持 `ProxyJump` 跳板机 |
12
12
  | ⚡ 连接复用 | 持久 SSH 会话池,文件操作复用同一条已认证连接,不再每次握手(首次后近乎瞬时) |
13
13
  | 📥 配置导入 | 一键从 `~/.ssh/config`(递归 `Include`)发现主机并批量导入连接配置 |
14
- | 🗂️ 远程文件 | better-sidebar「远程文件」页签浏览 / 打开 / 编辑 / 保存远程文件;选中工作区后可双向同步 |
15
- | 💻 远程终端 | better-sidebar「远程终端」页签打开 `ssh -tt` 集成终端,支持多终端并发 |
16
- | 🔄 双向同步 | `remote_ssh_sync`(远端 → 本地镜像)/ `remote_ssh_push`(本地镜像远端),tar 流式管道,同步后内置文件工具可见 |
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,17 +29,18 @@ 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.8.3
32
+ dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@1.9.1
34
33
  ```
35
34
 
36
35
  > ⚠️ 安装后需**重启 DSH** 才生效;后续仅修改 Client 半边时刷新浏览器即可。
37
36
 
38
37
  ## 快速开始
39
38
 
40
- 1. 安装插件并重启 DSH
39
+ 1. 安装插件并重启 DSH(安装时自动配置 better-sidebar 的 shell 指向 wrapper 脚本)。
41
40
  2. 打开 **设置 → 🖥️ 远程连接**,添加一条连接(主机 / 端口 / 用户名 / 密钥或密码),点「测试连接」验证。
42
- 3. 在侧边栏打开 **🗂️ 远程文件** **💻 远程终端** 页签使用。
43
- 4. 需要把远程目录当作工作区时:走 DSH 原生「添加工作区」流程,在弹窗中选择「**选择远程目录…**」。
41
+ 3. 需要把远程目录当作工作区时:走 DSH 原生「添加工作区」流程,在弹窗中选择「**选择远程目录…**」→ 选择连接 浏览并选择远程目录。
42
+ 4. 创建后远程文件自动同步到本地镜像,内置「文件」页签直接可见。
43
+ 5. 在远程工作区中打开内置「终端」页签 → 自动 SSH 到远程主机(仅密钥认证)。
44
44
 
45
45
  ## 使用指南
46
46
 
@@ -60,22 +60,35 @@ dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@1.8.3
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
- - 选择连接配置,点「新建终端」打开 `ssh -tt` 交互式终端;
72
- - 支持同时打开多个终端,会话切换后终端列表自动恢复(终端本体在宿主进程存活,恢复后继续回显)。
72
+ 通过 shell wrapper 实现:终端启动时自动检测当前工作目录下的 `.remote-ssh.json`,若存在且含密钥路径 自动 `ssh -tt` 连接远程主机;否则启动本地 shell。
73
+
74
+ **自动配置**:安装插件后,`cordis.patch.yml` 自动把 better-sidebar 的 `shell` 配置覆盖为 wrapper 脚本路径(用 `!!js` 动态计算,适配不同平台和用户主目录)。**无需手动编辑任何配置文件。**
75
+
76
+ 工作流程:
77
+ 1. 插件启动时自动在 `~/.dsh/remote-ssh/` 下生成 wrapper 脚本:
78
+ - `dsh-remote-shell.js`(核心逻辑:检测 `.remote-ssh.json` → `ssh -tt`;否则本地 shell)
79
+ - Windows: `dsh-remote-shell.cmd`(薄壳调用 .js)
80
+ - POSIX: `dsh-remote-shell`(薄壳调用 .js)
81
+ 2. 安装时自动覆盖 better-sidebar 的 `shell` config 指向 wrapper 脚本。
82
+ 3. 重启 DSH 后,在远程工作区中打开终端 → 自动 SSH 到远程主机;在本地工作区中打开终端 → 照常启动本地 shell。
83
+
84
+ > ⚠️ 终端透明接入仅支持**密钥认证**(密码无法安全传入 wrapper 脚本)。密码认证的连接仍可使用模型工具。
85
+ > 若需覆盖自动配置,在 profile 的 `cordis.patch.yml` 中加一条 `id: better-sidebar` 的 `config.shell` 即可(profile patch 优先于 bundle patch)。
73
86
 
74
87
  ### 4. 远程工作区(🌐)
75
88
 
76
89
  1. 打开 DSH 原生「添加工作区」流程;
77
90
  2. 选择「**选择远程目录…**」→ 选择连接 → 浏览并选择远程目录;
78
- 3. 插件在本地生成镜像目录 `~/.dsh/remote-workspaces/<id>` 并注册进原生工作区列表(标题 `🌐 <名称>`);
91
+ 3. 插件在本地生成镜像目录 `~/.dsh/remote-workspaces/<id>`,写入 `.remote-ssh.json`(连接信息,供 shell wrapper 读取),自动同步远程文件,并注册进原生工作区列表(标题 `🌐 <名称>`);
79
92
  4. 从工作区创建会话后,会话工作目录即该镜像目录,模型工具自动感知对应的远程连接与目录。
80
93
 
81
94
  ### 5. 模型工具(🤖)
@@ -118,13 +131,18 @@ dsh-remote-ssh/
118
131
  ```
119
132
  浏览器 (Client 半边) Node 进程 (Host 半边)
120
133
  ┌───────────────────────────────┐ fetch ┌──────────────────────────────────┐
121
- │ ctx.betterSidebar.registerTab │ ──────▶ │ ctx.webServer /remote-ssh/api/* │
122
- │ · 远程文件 / 远程终端 │ ◀────── │ · SSH:listDir/readFile/… │
123
- │ ctx.slots: settings.section │ JSON │ · settings:profiles/workspaces │
124
- │ · DSH 设置页「远程连接」 │ │ · terminals:ssh -tt 管道
125
- └───────────────────────────────┘ │ ctx.tools:12 个 remote_ssh_* │
126
- └───────────────┬──────────────────┘
127
- │ SSH
134
+ │ ctx.betterSidebar.registerTab │ ──────▶ │ ctx.webServer /remote-ssh/api/* │
135
+ │ · remssh:editor (隐藏) │ ◀────── │ · SSH:listDir/readFile/… │
136
+ │ ctx.slots: settings.section │ JSON │ · settings:profiles/workspaces │
137
+ │ · 连接配置 + 工作区管理 │ │ · 自动同步 + shell wrapper 生成
138
+ │ ctx.slots: directoryFlow │ │ ctx.tools:12 个 remote_ssh_* │
139
+ │ · 本地 / 远程目录两选弹窗 │ └───────────────┬──────────────────┘
140
+ └───────────────────────────────┘ │ SSH
141
+ ┌─────────────┴──────────┐
142
+ │ ~/.dsh/remote-ssh/ │
143
+ │ dsh-remote-shell.js │
144
+ │ dsh-remote-shell[.cmd]│ ← better-sidebar shell 指向此文件
145
+ └────────────────────────┘
128
146
  远程超算 / 服务器
129
147
  ```
130
148
 
@@ -134,24 +152,26 @@ dsh-remote-ssh/
134
152
  - **二进制安全读文件**:远程 `base64 -w0` 输出,Host 用 `Buffer` 解码为 UTF-8。
135
153
  - **写文件走 stdin**:远程 `cat > <file>`,内容经子进程 stdin 传入。
136
154
  - **文件列举**:`find -printf '%Y\t%f\t%s\n'`(`%Y` 跟随软链接取目标类型,适配超算家目录软链接)。
137
- - **终端**:`ssh -tt` 分配远程 PTY,本地管道转发,客户端 HTTP 轮询回显(无 `node-pty` 依赖,Windows 可用)。
155
+ - **自动同步**:创建远程工作区时立即 `tar | ssh` 拉取远程文件到本地镜像,使内置「文件」页签可见真实远程文件。
156
+ - **Shell wrapper**:插件启动时生成跨平台 wrapper 脚本(Node.js + 薄壳),检测工作区 `.remote-ssh.json` 自动 SSH,使内置「终端」页签透明接入远程。
138
157
  - **国际化**:Client 接入 `ctx.locale`,注册 `zh` / `en` 词典,界面与工具描述跟随 DSH 语言自动切换。
139
158
 
140
159
  ## 已知限制
141
160
 
142
- 1. **终端非本地 PTY**:走 `ssh -tt` 管道通道(跨平台可用);`vim` / `htop` 等全屏程序体验受限,后续可接入 `terminals.registerBackend` + `node-pty` 真 PTY。
161
+ 1. **终端透明接入仅限密钥认证**:shell wrapper 通过 `.remote-ssh.json` 读取密钥路径,密码认证无法安全传入。密码认证的连接仍可使用模型工具和远程文件操作。
143
162
  2. **文件操作依赖 GNU 工具**(`find -printf`、`base64 -w0`):目标为 Linux 超算时通用。
144
163
  3. **密码认证需本机 `sshpass`**(Windows 默认没有);密钥认证无此依赖。
145
164
  4. **同步为全量 tar**:`remote_ssh_sync` / `remote_ssh_push` 用 `tar` 流式全量同步,单次同步大目录耗时与传输量较高,后续可换 `rsync -e ssh` 增量。
146
- 5. **原生工具编辑的是本地镜像副本**,不会自动回传 —— 须点「推送」或调用 `remote_ssh_push` 同步回远端(与 `dsh-remote` 的 `rw_sync` / `rw_push` 模型一致)。
165
+ 5. **内置编辑器编辑的是本地镜像副本**,不会自动回传 —— 须在设置页点「推送」或调用 `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/cordis.patch.yml CHANGED
@@ -5,3 +5,17 @@
5
5
  - insert:
6
6
  - id: remote-ssh
7
7
  name: '@zhangfengshun/dsh-remote-ssh'
8
+ # 自动覆盖 better-sidebar 的 shell 配置,指向本插件生成的 shell wrapper。
9
+ # wrapper 脚本在插件 apply() 时写入 ~/.dsh/remote-ssh/dsh-remote-shell[.cmd]:
10
+ # - 远程工作区(cwd 含 .remote-ssh.json 且 keyPath 非空)→ ssh -tt 连远程
11
+ # - 本地工作区 → 照常启动本地 shell
12
+ # 路径用 !!js 动态计算,适配不同平台和用户主目录。
13
+ - id: better-sidebar
14
+ config:
15
+ shell: !!js |
16
+ var os = require('os');
17
+ var path = require('path');
18
+ var dir = path.join(os.homedir(), '.dsh', 'remote-ssh');
19
+ var isWin = process.platform === 'win32';
20
+ var wrapper = isWin ? path.join(dir, 'dsh-remote-shell.cmd') : path.join(dir, 'dsh-remote-shell');
21
+ return wrapper;
package/lib/client.js CHANGED
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * @zhangfengshun/dsh-remote-ssh — Client 半边(静态 DSH Web 插件,浏览器 bundle)
3
3
  *
4
- * 通过 dsh-better-sidebar 的 `ctx.betterSidebar.registerTab` 注册两个侧边栏页签:
5
- * - remssh:files 远程文件(文件浏览器 + 工作区选择)
6
- * - remssh:term 远程终端(ssh -tt 管道终端)
7
- * 并在 DSH 设置页注册「远程连接」小节(settings.section)管理连接配置;
8
- * 同时接管原生「添加工作区」流程(directoryFlow 槽位,优先级 -1 覆盖内置选择器),
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",
@@ -899,15 +903,34 @@ window.__ModuleLoader__.load({
899
903
  function SettingsSection(props) {
900
904
  var t = useT();
901
905
  var [profiles, setProfiles] = React.useState([]);
906
+ var [workspaces, setWorkspaces] = React.useState([]);
902
907
  var [form, setForm] = React.useState({ name: "", host: "", port: "22", user: "", authMethod: "key", keyPath: "", password: "", remoteRoot: "~", proxyJump: "" });
903
908
  var [result, setResult] = React.useState(null);
904
909
  var [busy, setBusy] = React.useState(false);
905
910
  var [importDlg, setImportDlg] = React.useState(null);
911
+ var [wsSync, setWsSync] = React.useState({});
906
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; }); };
907
913
 
908
- function reload() { api("listProfiles").then(function (r) { if (r && r.ok) setProfiles(r.profiles || []); }); }
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
+ }
909
918
  React.useEffect(reload, []);
910
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
+
911
934
  function save() {
912
935
  setBusy(true); setResult(null);
913
936
  api("saveProfile", Object.assign({}, form, { port: parseInt(form.port, 10) || 22 })).then(function (r) {
@@ -985,6 +1008,22 @@ window.__ModuleLoader__.load({
985
1008
  );
986
1009
  }),
987
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,
988
1027
  h("div", { className: "rssh-card" },
989
1028
  h("div", { className: "rssh-title" }, t("settings.add")),
990
1029
  Field({ label: t("settings.name"), value: form.name, onChange: function (v) { set("name", v); }, placeholder: t("settings.namePlaceholder") }),
@@ -1055,20 +1094,9 @@ window.__ModuleLoader__.load({
1055
1094
  });
1056
1095
 
1057
1096
  if (betterSidebar) {
1058
- ctx.effect(function () {
1059
- return betterSidebar.registerTab({
1060
- id: "remssh:files",
1061
- title: function () { return i18n.t("tabs.files"); },
1062
- order: 90,
1063
- single: true,
1064
- icon: function () { return h("span", null, "🗂️"); },
1065
- component: function (props) {
1066
- var sid = props && props.scope ? props.scope.sessionId : null;
1067
- return h(FilesTab, { key: "files:" + (sid || "global"), sessionId: sid, betterSidebar: props && props.ctx ? props.ctx.betterSidebar : null });
1068
- }
1069
- });
1070
- });
1071
- // 远程文件编辑器页签(隐藏页签,仅由文件树点击打开,每个远程文件单开一栏)。
1097
+ // 远程文件编辑器页签(隐藏页签,仅由代码打开,每个远程文件单开一栏)。
1098
+ // remssh:files 和 remssh:term 已移除:远程文件由内置「文件」页签显示(自动同步到镜像目录),
1099
+ // 远程终端由内置「终端」页签提供(shell wrapper 自动 SSH)。
1072
1100
  ctx.effect(function () {
1073
1101
  return betterSidebar.registerTab({
1074
1102
  id: "remssh:editor",
@@ -1081,19 +1109,6 @@ window.__ModuleLoader__.load({
1081
1109
  }
1082
1110
  });
1083
1111
  });
1084
- ctx.effect(function () {
1085
- return betterSidebar.registerTab({
1086
- id: "remssh:term",
1087
- title: function () { return i18n.t("tabs.term"); },
1088
- order: 91,
1089
- single: true,
1090
- icon: function () { return h("span", null, "💻"); },
1091
- component: function (props) {
1092
- var sid = props && props.scope ? props.scope.sessionId : null;
1093
- return h(TerminalTab, { key: "term:" + (sid || "global"), sessionId: sid });
1094
- }
1095
- });
1096
- });
1097
1112
  }
1098
1113
 
1099
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.8.3",
3
+ "version": "1.9.1",
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",