@zhangfengshun/dsh-remote-ssh 2.0.6 → 2.0.8

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.
Files changed (3) hide show
  1. package/README.md +38 -33
  2. package/lib/client.js +11 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -11,11 +11,10 @@
11
11
  | 🔌 远程连接 | SSH 连接超算 / 服务器,密钥认证(推荐)或密码认证(需本机 `sshpass`),内置「测试连接」;支持 `ProxyJump` 跳板机 |
12
12
  | ⚡ 连接复用 | 持久 SSH 会话池,文件操作复用同一条已认证连接,不再每次握手(首次后近乎瞬时) |
13
13
  | 📥 配置导入 | 一键从 `~/.ssh/config`(递归 `Include`)发现主机并批量导入连接配置 |
14
- | 🗂️ 远程文件 | 创建远程工作区时自动同步到本地镜像,内置「文件」页签直接显示远程文件;编辑后通过设置页同步/推送回传 |
14
+ | 📂 远程文件 | 内置「文件」页签通过 fs.* 拦截**直接 SSH 读写远程文件**,无需同步——打开即读远程,保存即写远程 |
15
15
  | 💻 远程终端 | 内置「终端」页签自动通过 shell wrapper 检测远程工作区,打开 SSH 交互式终端(仅密钥认证) |
16
- | 🔄 双向同步 | 设置页一键同步(远端 本地镜像)/ 推送(本地镜像 → 远端),或通过 `remote_ssh_sync` / `remote_ssh_push` 工具 |
17
- | 🌐 远程工作区 | 选择远程目录创建**原生工作区**(本地镜像目录 + 原生注册 + 自动同步),一键打开进入远程环境 |
18
- | 🤖 模型工具 | 12 个 `remote_ssh_*` 工具,模型可读写远程文件、执行远程命令、内容搜索、文件名查找、目录/删除/移动、双向同步;在远程工作区会话中免填连接参数 |
16
+ | 🌐 远程工作区 | 选择远程目录创建**原生工作区**(本地镜像目录 + 原生注册),一键打开进入远程环境 |
17
+ | 🤖 模型工具 | 12 `remote_ssh_*` 工具,模型可读写远程文件、执行远程命令、内容搜索、文件名查找、目录/删除/移动;在远程工作区会话中免填连接参数 |
19
18
  | 🌍 国际化 | 界面文案 + 工具描述中英双语,通过 `ctx.locale` 跟随 DSH 语言设置自动切换 |
20
19
 
21
20
  ## 安装
@@ -29,7 +28,7 @@ dsh plugin --profile <name> add /absolute/path/to/dsh-remote-ssh
29
28
  ### 发布后安装
30
29
 
31
30
  ```bash
32
- dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.0.0
31
+ dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.0.7
33
32
  ```
34
33
 
35
34
  > ⚠️ 安装后需**重启 DSH** 才生效;后续仅修改 Client 半边时刷新浏览器即可。
@@ -41,7 +40,7 @@ dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.0.0
41
40
  1. 安装插件并重启 DSH(安装时自动配置 better-sidebar 的 shell 指向 wrapper 脚本)。
42
41
  2. 打开 **设置 → 🖥️ 远程连接**,添加一条连接(主机 / 端口 / 用户名 / 密钥或密码),点「测试连接」验证。
43
42
  3. 需要把远程目录当作工作区时:走 DSH 原生「添加工作区」流程,在弹窗中选择「**选择远程目录…**」→ 选择连接 → 浏览并选择远程目录。
44
- 4. 创建后远程文件自动同步到本地镜像,内置「文件」页签直接可见。
43
+ 4. 创建后内置「文件」页签**直接显示远程文件**——打开文件即读远程,保存即写远程,无需同步。
45
44
  5. 在远程工作区中打开内置「终端」页签 → 自动 SSH 到远程主机(仅密钥认证)。
46
45
 
47
46
  ## 使用指南
@@ -64,10 +63,14 @@ dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.0.0
64
63
 
65
64
  ### 2. 远程文件(内置「文件」页签)
66
65
 
67
- - 创建远程工作区时,远程文件自动同步到本地镜像目录,内置「文件」页签直接显示;
68
- - 内置编辑器编辑的是本地镜像副本,编辑后需**推送回远端**;
69
- - 推送方式:设置远程连接 → 远程工作区 → 点「⬆ 推送」按钮,或模型调用 `remote_ssh_push` 工具;
70
- - 远程有更新时点「⬇ 同步」重新拉取。
66
+ **直接 SSH 读写,无需同步**:
67
+
68
+ - 在远程工作区中打开内置「文件」页签直接显示远程文件树;
69
+ - 点击文件 → 直接通过 SSH 读取远程文件内容;
70
+ - 编辑保存 → 直接通过 SSH 写回远程文件;
71
+ - 全程透明:客户端看到的是本地镜像路径,Host 拦截器自动转换为远程路径并通过 SSH 执行。
72
+
73
+ **技术原理**:插件注册了 4 个 exact 路由(`/sidebar/api/fs.tree`、`fs.read`、`fs.write`、`fs.search`),在 better-sidebar 的 prefix 路由之前拦截。当请求路径对应的会话 cwd 含 `.remote-ssh.json` 时走 SSH,否则走本地 fs(行为与 better-sidebar 原始实现一致)。
71
74
 
72
75
  ### 3. 远程终端(内置「终端」页签)
73
76
 
@@ -83,19 +86,19 @@ dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.0.0
83
86
  2. 安装时自动覆盖 better-sidebar 的 `shell` config 指向 wrapper 脚本。
84
87
  3. 重启 DSH 后,在远程工作区中打开终端 → 自动 SSH 到远程主机;在本地工作区中打开终端 → 照常启动本地 shell。
85
88
 
86
- > ⚠️ 终端透明接入仅支持**密钥认证**(密码无法安全传入 wrapper 脚本)。密码认证的连接仍可使用模型工具。
89
+ > ⚠️ 终端透明接入仅支持**密钥认证**(密码无法安全传入 wrapper 脚本)。密码认证的连接仍可使用模型工具和远程文件操作。
87
90
  > 若需覆盖自动配置,在 profile 的 `cordis.patch.yml` 中加一条 `id: better-sidebar` 的 `config.shell` 即可(profile patch 优先于 bundle patch)。
88
91
 
89
92
  ### 4. 远程工作区(🌐)
90
93
 
91
94
  1. 打开 DSH 原生「添加工作区」流程;
92
95
  2. 选择「**选择远程目录…**」→ 选择连接 → 浏览并选择远程目录;
93
- 3. 插件在本地生成镜像目录 `~/.dsh/remote-workspaces/<id>`,写入 `.remote-ssh.json`(连接信息,供 shell wrapper 读取),自动同步远程文件,并注册进原生工作区列表(标题 `🌐 <名称>`);
96
+ 3. 插件在本地生成镜像目录 `~/.dsh/remote-workspaces/<id>`,写入 `.remote-ssh.json`(连接信息,供 shell wrapper fs.* 拦截器读取),并注册进原生工作区列表(标题 `🌐 <名称>`);
94
97
  4. 从工作区创建会话后,会话工作目录即该镜像目录,模型工具自动感知对应的远程连接与目录。
95
98
 
96
99
  ### 5. 模型工具(🤖)
97
100
 
98
- 插件向模型注册 5 个工具,模型可直接操作远程环境:
101
+ 插件向模型注册 12 个工具,模型可直接操作远程环境:
99
102
 
100
103
  | 工具 | 用途 |
101
104
  | --- | --- |
@@ -121,7 +124,7 @@ dsh-remote-ssh/
121
124
  ├── package.json # npm 元数据 + dsh.bundle / dsh.client 声明
122
125
  ├── cordis.patch.yml # bundle 挂载补丁(install 时加入 profile bundle 栈)
123
126
  ├── lib/
124
- │ ├── index.js # Host 半边(SSH + settings + 5 个工具 + HTTP API
127
+ │ ├── index.js # Host 半边(SSH + settings + 12 个工具 + HTTP API + fs.* 拦截)
125
128
  │ └── client.js # Client 半边(better-sidebar 页签 + 设置小节 + i18n)
126
129
  ├── LICENSE
127
130
  ├── CHANGELOG.md
@@ -133,28 +136,32 @@ dsh-remote-ssh/
133
136
  ```
134
137
  浏览器 (Client 半边) Node 进程 (Host 半边)
135
138
  ┌───────────────────────────────┐ fetch ┌──────────────────────────────────┐
136
- │ ctx.betterSidebar.registerTab │ ──────▶ │ ctx.webServer /remote-ssh/api/*
137
- │ · remssh:editor (隐藏) │ ◀────── │ · SSH:listDir/readFile/…
138
- │ ctx.slots: settings.section │ JSON │ · settings:profiles/workspaces
139
- │ · 连接配置 + 工作区管理 │ │ · 自动同步 + shell wrapper 生成
140
- │ ctx.slots: directoryFlow │ │ ctx.tools:12 个 remote_ssh_*
141
- │ · 本地 / 远程目录两选弹窗 │ └───────────────┬──────────────────┘
142
- └───────────────────────────────┘ SSH
143
- ┌─────────────┴──────────┐
144
- │ ~/.dsh/remote-ssh/ │
145
- dsh-remote-shell.js │
146
- │ dsh-remote-shell[.cmd]│ ← better-sidebar shell 指向此文件
147
- └────────────────────────┘
148
- 远程超算 / 服务器
139
+ │ ctx.betterSidebar.registerTab │ ──────▶ │ ctx.webServer
140
+ │ · remssh:editor (隐藏) │ ◀────── │ · /sidebar/api/fs.* (exact)
141
+ │ ctx.slots: settings.section │ JSON │ 拦截 better-sidebar 的 fs API
142
+ │ · 连接配置 + 工作区管理 │ │ 远程→SSH / 本地→本地 fs
143
+ │ ctx.slots: directoryFlow │ │ · /remote-ssh/api/* (prefix)
144
+ │ · 本地 / 远程目录两选弹窗 │ │ listDir/readFile/writeFile/… │
145
+ └───────────────────────────────┘ · settings + shell wrapper 生成 │
146
+ │ ctx.tools:12 个 remote_ssh_* │
147
+ └───────────────┬──────────────────┘
148
+ SSH
149
+ ┌───────────────┴──────────────┐
150
+ │ ~/.dsh/remote-ssh/ │
151
+ │ dsh-remote-shell.js │
152
+ │ dsh-remote-shell[.cmd] │ ← better-sidebar shell 指向此文件
153
+ └─────────────────────────────┘
154
+ 远程超算 / 服务器
149
155
  ```
150
156
 
151
157
  关键设计:
152
158
 
159
+ - **fs.* 拦截(核心)**:注册 4 个 exact 路由(`/sidebar/api/fs.tree` 等),在 `match()` 中 exact 优先于 prefix 检查,从而拦截 better-sidebar 的 fs API。当会话 cwd 含 `.remote-ssh.json` 时走 SSH,否则走本地 fs。
160
+ - **路径透明映射**:客户端看到的是本地镜像路径,Host 拦截器自动转换为远程路径,通过 SSH 执行后返回结果。对客户端完全透明。
153
161
  - **免 shell 引号转义**:远程命令通过 `subprocess.spawn({ argv })` 以参数数组调起 `ssh.exe`,避免引号 / 空格问题。
154
162
  - **二进制安全读文件**:远程 `base64 -w0` 输出,Host 用 `Buffer` 解码为 UTF-8。
155
163
  - **写文件走 stdin**:远程 `cat > <file>`,内容经子进程 stdin 传入。
156
164
  - **文件列举**:`find -printf '%Y\t%f\t%s\n'`(`%Y` 跟随软链接取目标类型,适配超算家目录软链接)。
157
- - **自动同步**:创建远程工作区时立即 `tar | ssh` 拉取远程文件到本地镜像,使内置「文件」页签可见真实远程文件。
158
165
  - **Shell wrapper**:插件启动时生成跨平台 wrapper 脚本(Node.js + 薄壳),检测工作区 `.remote-ssh.json` 自动 SSH,使内置「终端」页签透明接入远程。
159
166
  - **国际化**:Client 接入 `ctx.locale`,注册 `zh` / `en` 词典,界面与工具描述跟随 DSH 语言自动切换。
160
167
 
@@ -163,17 +170,15 @@ dsh-remote-ssh/
163
170
  1. **终端透明接入仅限密钥认证**:shell wrapper 通过 `.remote-ssh.json` 读取密钥路径,密码认证无法安全传入。密码认证的连接仍可使用模型工具和远程文件操作。
164
171
  2. **文件操作依赖 GNU 工具**(`find -printf`、`base64 -w0`):目标为 Linux 超算时通用。
165
172
  3. **密码认证需本机 `sshpass`**(Windows 默认没有);密钥认证无此依赖。
166
- 4. **同步为全量 tar**:`remote_ssh_sync` / `remote_ssh_push` 用 `tar` 流式全量同步,单次同步大目录耗时与传输量较高,后续可换 `rsync -e ssh` 增量。
167
- 5. **内置编辑器编辑的是本地镜像副本**,不会自动回传 —— 须在设置页点「推送」或调用 `remote_ssh_push` 同步回远端。
173
+ 4. **fs.search 远程搜索**返回的路径为相对于远程根目录的相对路径,客户端拼接为本地镜像路径格式显示。
168
174
 
169
175
  ## 路线图
170
176
 
171
177
  - [x] 从 `~/.ssh/config` 自动导入连接
172
178
  - [x] ProxyJump / 跳板机、SSH config 复用
173
- - [x] `tar`-over-ssh 双向同步(`scp` / `sftp` 增量同步为后续优化)
174
- - [x] 远程文件合并到内置「文件」页签(自动同步 + 镜像)
179
+ - [x] 远程文件合并到内置「文件」页签(fs.* 拦截 + 直接 SSH 读写,无需同步)
175
180
  - [x] 远程终端合并到内置「终端」页签(shell wrapper 透明 SSH)
176
- - [ ] `rsync -e ssh` 增量同步
181
+ - [x] 远程工作区目录选择器(本地/远程两选,不依赖 browse capability)
177
182
  - [ ] 密码凭据走 `credentials` 服务(避免明文进 argv)
178
183
 
179
184
  ## 许可证
package/lib/client.js CHANGED
@@ -2,8 +2,8 @@
2
2
  * @zhangfengshun/dsh-remote-ssh — Client 半边(静态 DSH Web 插件,浏览器 bundle)
3
3
  *
4
4
  * 远程文件与远程终端已合并到内置「文件」和「终端」页签:
5
- * - 远程文件:创建远程工作区时自动同步到本地镜像目录,内置「文件」页签直接显示远程文件。
6
- * 编辑后通过设置页的「同步/推送」按钮回传远程(或使用 remote_ssh_push 工具)。
5
+ * - 远程文件:内置「文件」页签通过 fs.* 拦截直接 SSH 读写远程文件,无需同步。
6
+ * 远程终端由内置「终端」页签提供(shell wrapper 自动 SSH)。
7
7
  * - 远程终端:shell wrapper 自动检测工作区 .remote-ssh.json,在远程工作区中打开 SSH 终端。
8
8
  * 需在 better-sidebar 设置中将 shell 指向 wrapper 脚本(见 README)。
9
9
  * 仍保留隐藏的 remssh:editor 页签用于远程文件编辑器。
@@ -403,7 +403,6 @@ window.__ModuleLoader__.load({
403
403
  var [err, setErr] = React.useState(null);
404
404
  var [loading, setLoading] = React.useState(false);
405
405
  var [defaultDir, setDefaultDir] = useSessionMemory(sessionId, "defaultDir", "");
406
- var [syncSt, setSyncSt] = React.useState(null); // { busy, msg, ok }
407
406
  var [opsDlg, setOpsDlg] = React.useState(null); // { type, p1, p2, result, busy }
408
407
 
409
408
  React.useEffect(function () {
@@ -450,18 +449,6 @@ window.__ModuleLoader__.load({
450
449
  });
451
450
  }
452
451
 
453
- function doSync(direction) {
454
- if (!wsId) return;
455
- var ws = workspaces.find(function (w) { return w.id === wsId; });
456
- if (!ws) return;
457
- setSyncSt({ busy: true, msg: direction === "down" ? t("sync.syncing") : t("sync.pushing"), ok: null });
458
- var method = direction === "down" ? "syncDown" : "syncUp";
459
- api(method, { workspaceId: wsId }).then(function (r) {
460
- if (r && r.ok) setSyncSt({ busy: false, msg: direction === "down" ? t("sync.done") : t("sync.done"), ok: true });
461
- else setSyncSt({ busy: false, msg: (r && r.error) || t("sync.fail"), ok: false });
462
- });
463
- }
464
-
465
452
  function activeProfileId() {
466
453
  if (wsId) {
467
454
  var ws = workspaces.find(function (w) { return w.id === wsId; });
@@ -581,12 +568,13 @@ window.__ModuleLoader__.load({
581
568
  h("button", { className: "rssh-btn", onClick: function () { load(profileId, path); } }, t("common.open")),
582
569
  h("button", { className: "rssh-btn", onClick: goUp }, t("common.up"))
583
570
  ),
584
- wsId ? h("div", { className: "rssh-pathbar" },
585
- h("button", { className: "rssh-btn", onClick: function () { doSync("down"); }, disabled: syncSt && syncSt.busy }, t("sync.down")),
586
- h("button", { className: "rssh-btn", onClick: function () { doSync("up"); }, disabled: syncSt && syncSt.busy }, t("sync.up")),
587
- syncSt ? h("span", { className: syncSt.ok === false ? "rssh-err" : (syncSt.ok ? "rssh-ok" : "rssh-muted"), style: { flex: 1, fontSize: 11 } }, syncSt.msg) : null
588
- ) : null,
589
- (wsId || profileId) ? h("div", { className: "rssh-pathbar", style: { flexWrap: "wrap" } },
571
+ wsId ? h("div", { className: "rssh-pathbar", style: { flexWrap: "wrap" } },
572
+ h("button", { className: "rssh-btn", onClick: function () { openOps("grep"); } }, t("ops.search")),
573
+ h("button", { className: "rssh-btn", onClick: function () { openOps("glob"); } }, t("ops.find")),
574
+ h("button", { className: "rssh-btn", onClick: function () { openOps("mkdir"); } }, t("ops.newDir")),
575
+ h("button", { className: "rssh-btn", onClick: function () { openOps("move"); } }, t("ops.rename")),
576
+ h("button", { className: "rssh-btn", onClick: function () { openOps("delete"); } }, t("ops.delete"))
577
+ ) : (wsId || profileId) ? h("div", { className: "rssh-pathbar", style: { flexWrap: "wrap" } },
590
578
  h("button", { className: "rssh-btn", onClick: function () { openOps("grep"); } }, t("ops.search")),
591
579
  h("button", { className: "rssh-btn", onClick: function () { openOps("glob"); } }, t("ops.find")),
592
580
  h("button", { className: "rssh-btn", onClick: function () { openOps("mkdir"); } }, t("ops.newDir")),
@@ -916,7 +904,6 @@ window.__ModuleLoader__.load({
916
904
  var [result, setResult] = React.useState(null);
917
905
  var [busy, setBusy] = React.useState(false);
918
906
  var [importDlg, setImportDlg] = React.useState(null);
919
- var [wsSync, setWsSync] = React.useState({});
920
907
  var set = function (k, v) { setForm(function (f) { var n = {}; for (var key in f) n[key] = f[key]; n[k] = v; return n; }); };
921
908
 
922
909
  function reload() {
@@ -929,12 +916,6 @@ window.__ModuleLoader__.load({
929
916
  var p = profiles.find(function (x) { return x.id === id; });
930
917
  return p ? p.name : id;
931
918
  }
932
- function doWsSync(wsId, dir) {
933
- 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; });
934
- api(dir === "down" ? "syncDown" : "syncUp", { workspaceId: wsId }).then(function (r) {
935
- 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; });
936
- });
937
- }
938
919
  function delWs(wsId) {
939
920
  api("deleteWorkspace", { id: wsId }).then(function (r) { if (r && r.ok) setWorkspaces(r.workspaces || []); });
940
921
  }
@@ -1019,16 +1000,12 @@ window.__ModuleLoader__.load({
1019
1000
  workspaces.length > 0 ? h("div", { className: "rssh-card" },
1020
1001
  h("div", { className: "rssh-title" }, t("settings.workspaces")),
1021
1002
  workspaces.map(function (ws) {
1022
- var st = wsSync[ws.id];
1023
1003
  return h("div", { key: ws.id, className: "rssh-card", style: { marginBottom: 6 } },
1024
1004
  h("div", { className: "rssh-title" }, ws.title || ws.id),
1025
1005
  h("div", { className: "rssh-sub" }, profileName(ws.profileId) + " → " + (ws.remotePath || "")),
1026
1006
  h("div", { className: "rssh-actions" },
1027
- h("button", { className: "rssh-btn", onClick: function () { doWsSync(ws.id, "down"); }, disabled: st && st.busy }, t("sync.down")),
1028
- h("button", { className: "rssh-btn", onClick: function () { doWsSync(ws.id, "up"); }, disabled: st && st.busy }, t("sync.up")),
1029
1007
  h("button", { className: "rssh-btn", onClick: function () { delWs(ws.id); } }, t("common.delete"))
1030
- ),
1031
- st ? h("span", { className: st.ok === false ? "rssh-err" : (st.ok ? "rssh-ok" : "rssh-muted"), style: { fontSize: 11 } }, st.msg) : null
1008
+ )
1032
1009
  );
1033
1010
  })
1034
1011
  ) : null,
@@ -1103,7 +1080,7 @@ window.__ModuleLoader__.load({
1103
1080
 
1104
1081
  if (betterSidebar) {
1105
1082
  // 远程文件编辑器页签(隐藏页签,仅由代码打开,每个远程文件单开一栏)。
1106
- // remssh:files 和 remssh:term 已移除:远程文件由内置「文件」页签显示(自动同步到镜像目录),
1083
+ // remssh:files 和 remssh:term 已移除:远程文件由内置「文件」页签直接 SSH 读写,
1107
1084
  // 远程终端由内置「终端」页签提供(shell wrapper 自动 SSH)。
1108
1085
  ctx.effect(function () {
1109
1086
  return betterSidebar.registerTab({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhangfengshun/dsh-remote-ssh",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
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",