@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 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
- | 🗂️ 远程文件 | 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,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.8.2
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. 需要把远程目录当作工作区时:走 DSH 原生「添加工作区」流程,在弹窗中选择「**选择远程目录…**」。
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
- - 选择连接配置,点「新建终端」打开 `ssh -tt` 交互式终端;
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 │ ──────▶ │ 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
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
- - **终端**:`ssh -tt` 分配远程 PTY,本地管道转发,客户端 HTTP 轮询回显(无 `node-pty` 依赖,Windows 可用)。
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. **终端非本地 PTY**:走 `ssh -tt` 管道通道(跨平台可用);`vim` / `htop` 等全屏程序体验受限,后续可接入 `terminals.registerBackend` + `node-pty` 真 PTY。
143
- 2. **文件操作依赖 GNU 工具**(`find -printf`、`base64 -w0`):目标为 Linux 超算时通用。
144
- 3. **密码认证需本机 `sshpass`**(Windows 默认没有);密钥认证无此依赖。
145
- 4. **同步为全量 tar**:`remote_ssh_sync` / `remote_ssh_push` 用 `tar` 流式全量同步,单次同步大目录耗时与传输量较高,后续可换 `rsync -e ssh` 增量。
146
- 5. **原生工具编辑的是本地镜像副本**,不会自动回传 —— 须点「推送」或调用 `remote_ssh_push` 同步回远端(与 `dsh-remote` `rw_sync` / `rw_push` 模型一致)。
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
- * 通过 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",
@@ -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
- // 本地模式:尝试弹原生系统目录选择框(pickDirectory),若 native capability 不可用
814
- // 则回退到加载用户主目录(load(undefined) 列举 home)。
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(); load(path); } },
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: browse, disabled: loading }, t("flow.browse")),
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); }, pickNative: function () { return props.workspaces.pickDirectory(); }, onChoose: chooseLocal })
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() { 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
+ }
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
- ctx.effect(function () {
1077
- return betterSidebar.registerTab({
1078
- id: "remssh:files",
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.8.2",
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",