dsh-remote 0.8.42 → 0.8.45

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/README.en.md CHANGED
@@ -57,6 +57,7 @@ The image above is the overview. What follows is only what the image does not ma
57
57
  The rest:
58
58
 
59
59
  - **22 model tools** (listed so they can be copied or searched): `rw_info`, `rw_connect`, `rw_machines`, `rw_pick_workspace`, `rw_list_dir`, `rw_stat`, `rw_read_file`, `rw_write_file`, `rw_edit`, `rw_append`, `rw_mkdir`, `rw_remove`, `rw_move`, `rw_exec`, `rw_search`, `rw_download`, `rw_upload`, `rw_sync`, `rw_push`, `rw_forward`, `rw_disconnect`, `rw_deploy_probe`.
60
+ - **Tell local and remote Sessions apart in the sidebar** (`0.8.43+`) — a remote Session row carries a **green dot** and its hover card names `user@host:port` plus the remote path; opening it also shows a host chip in the conversation header (a local Session shows neither). A Session counts as remote only when its cwd sits inside a `$DSH_HOME/remote-workspaces/…` mirror, so a machine that is merely saved never labels a local Session.
60
61
  - **Open a remote machine's DSH Web UI locally** (`0.8.36+`) — nothing has to be exposed on the remote: the plugin SSHes out, starts a `dsh web` there that binds **`127.0.0.1` only**, and tunnels its port back to a loopback port on this machine. The URL carries a one-time login token, consumed once by a redirect.
61
62
  - **One-click health check & deploy** (`0.8.36+`) — **a failed connection is usually the remote dsh version or environment, not the plugin.** The read-only check reports platform / node / npm / dsh version / **whether the native module can load** / proxy and npm registry, with a suggested fix; "Deploy & verify" installs into a remote **private directory** (no system directory, no PATH change, no replacing the version you use — deleting it is a complete rollback) and verifies step by step. The button **no longer requires running the check first** (`0.8.40+`) — the deploy re-checks internally. If that still fails, the built-in `dsh-remote-deploy` skill takes over.
62
63
  - **Cross-platform remotes** — all file access is SFTP-protocol-level (no POSIX shell), so Linux/macOS/Windows remotes all work.
@@ -132,7 +133,17 @@ A new tab opens showing that remote machine's full DSH interface (chat, tool tre
132
133
  no second instance will be started.
133
134
  - When the remote `dsh` is not on the SSH login PATH, point `webAttachCommand` at its absolute path; to
134
135
  keep an attached instance out of the remote user's own sessions and settings, point `webAttachDshHome`
135
- at a scratch directory.
136
+ at a scratch directory. This is **per machine**: Settings → Remote workspace → the machine card →
137
+ expand "Advanced" → "Remote dsh command" (e.g.
138
+ `/home/you/.nvm/versions/node/v22.19.0/bin/node /home/you/.dsh-remote/dsh/node_modules/@deepseek-ai/dsh/lib/bin.js`).
139
+ Empty falls back to the global `webAttachCommand`. A successful "Deploy & verify" writes this field for you.
140
+ - The tunnel follows the SSH connection: when it drops, the UI shows "Disconnected" and the local DSH
141
+ process is **not** affected. After the pool reconnects the plugin re-resolves the remote port (restarting
142
+ the remote process it started, if that one died), so hitting "Reconnect" is enough to carry on.
143
+ - The remote node must be **≥ 20.12** (booting dsh needs `parseEnv` from `node:util`, which only exists
144
+ from 20.12). An older node is reported as a blocker by the check: npm only warns (EBADENGINE) and
145
+ installs anyway, and the crash arrives later as
146
+ `does not provide an export named 'parseEnv'` — so it has to be caught before the install.
136
147
 
137
148
  ### Cannot connect? Check the environment, then deploy in one click
138
149
 
@@ -242,7 +253,9 @@ Deploying to a product profile is a separate, explicit action (`./sync.sh`) for
242
253
  | `hostKeyMode` | string | `accept-new` | host-key policy: `accept-new` (TOFU), `verify` (reject unknown hosts), `off` (skip) |
243
254
  | `useAgent` | bool | `false` | authenticate via the OpenSSH agent (`SSH_AUTH_SOCK`) |
244
255
  | `keyboardInteractive` | bool | `false` | allow keyboard-interactive auth (OTP/MFA) with the configured password |
245
- | `proxy` | object | — | jump host: `{ host, port?, username?, password?, privateKeyPath? }` |
256
+ | `backend` | string | `native` | SSH transport backend (issue #51). `native` = the built-in ssh2 client (password / private key / agent / OTP); `openssh` = **drive the local `ssh` command**, which is the only way to reach hosts whose sole authentication method is GSSAPI/Kerberos (log in once with `kinit`), and which also honours `ProxyCommand`, `PKCS11Provider`, `Match` blocks and the rest of `~/.ssh/config`. **Settable per machine.** The cost is a fresh connection per command (slower than native); SFTP still reuses one session. The host name is handed to ssh **verbatim** (so `~/.ssh/config` stays the single source of truth) and host keys are checked by ssh against your own `known_hosts` |
257
+ | `sshBinary` | string | `''` | the `ssh` executable the `openssh` backend drives (empty = the `ssh` on PATH) |
258
+ | `proxy` | object | — | jump host: `{ host, port?, username?, password?, privateKeyPath? }`; it means "reach the target host:port from the bastion", which is also what reaches a docker container's sshd on the bastion (see the issue #56 entry in the FAQ) |
246
259
  | `autoPush` | bool | `false` | auto-push edited mirror files back to the remote (watcher, debounced) |
247
260
  | `auditLog` | bool | `true` | append executed commands to `$DSH_HOME/remote-workspaces/audit.log` |
248
261
  | `encoding` | string | `utf-8` | text encoding for remote file reads/writes (e.g. `gbk`) |
@@ -257,7 +270,7 @@ Deploying to a product profile is a separate, explicit action (`./sync.sh`) for
257
270
  | `updateCheckIntervalMs` | int | 21600000 (6h) | how often `auto` mode checks npm (floor 60000) |
258
271
  | `updateAutoReload` | bool | `true` | hot-swap the host half after an update lands; `false` defers it to the next process start and the panel reports `pendingReload` |
259
272
  | `webAttachPortStart` | int | `3088` | first local port for the "Remote DSH Web UI" tunnel (later ports are tried when taken; loopback only) |
260
- | `webAttachCommand` | string | `dsh` | command used to start `dsh web` on the remote; change it when the remote `dsh` is not on the SSH login PATH |
273
+ | `webAttachCommand` | string | `dsh` | command used to start `dsh web` on the remote; change it when the remote `dsh` is not on the SSH login PATH (also settable per machine under "Advanced" in the settings page, which wins over this default) |
261
274
  | `webAttachDshHome` | string | `''` | `DSH_HOME` exported on the remote when attaching; empty reuses the remote user's own harness home |
262
275
  | `webAttachWaitSeconds` | int | `45` | how long to wait for a freshly started remote `dsh web` to print its launch token |
263
276
  | `webInstallPrefix` | string | `''` | where an automatic deployment installs dsh; empty means `$HOME/.dsh-remote/dsh` (no system directory, no PATH change) |
@@ -270,12 +283,34 @@ Deploying to a product profile is a separate, explicit action (`./sync.sh`) for
270
283
 
271
284
  **`@` lists remote files but the built-in read tool cannot open them** — the harness's own file tools see the **local mirror**, which stays empty until `rw_sync` downloads it. Read remote files with `rw_read_file` or the sidebar remote tab.
272
285
 
286
+ **A file link in a remote Session does not open** (issue #55) — fixed. When a link is clicked the harness turns it into `dsh-resource://file/session/<session>/<path>` and hands it to the built-in text preview, which reads the **local** filesystem; a remote Session's cwd is a **local mirror directory** (empty until `rw_sync`), so the open simply failed. The plugin now registers a higher-priority handler for that address and opens it over SFTP through the same editor as the sidebar "Remote Files" tab. The safety rule is deliberately conservative: it takes over only when the Session's cwd is **provably inside a mirror** and the mirror registry has loaded, so local Sessions — and the instant right after a page load — keep the built-in preview and no local file is ever mistaken for a remote one. The two cases that still cannot be mapped say so instead of showing a blank page: an address with no mirror mapping, and a Session whose workspace is not a remote mirror directory.
287
+
288
+ **The terminal in a remote Session is still the local one** (issue #55) — that is **by design, and a plugin cannot change it**, but there is an equivalent: DSH's sidebar terminal is core, its PTY comes from the **context-global** `subprocess` service, and its cwd is the Session's own workspace directory (the local mirror for a remote Session). A plugin can neither replace that service nor retarget the core terminal onto SSH. For a **genuine remote terminal** use the "Remote DSH Web UI" (above): it opens the **remote machine's own** DSH in this browser, and that interface's terminal runs on the remote host, with no extra configuration. To run a single remote command instead, use `rw_exec` (add `pty: true` for an interactive PTY).
289
+
273
290
  **Host key changed** — `/remote forget-key` (or Settings → machine → trust again).
274
291
 
275
292
  **"Authentication failed"** — check the username/password/key path; fill in the passphrase for an encrypted key; enable keyboard-interactive when the host requires OTP.
276
293
 
277
294
  **Cannot reach an internal machine** — set a jump host (or add the bastion as its own machine first).
278
295
 
296
+ **The host only allows GSSAPI/Kerberos (`ssh` works after `kinit`, the plugin does not)** — in Settings → 远程工作区 → Advanced, switch **Backend** to `openssh` (drive the local `ssh`) and tick "use a ~/.ssh/config alias", with Host set to the **alias** in `~/.ssh/config` (not the IP). The login is then performed by your local ssh with your Kerberos ticket, and `ProxyCommand`, `GSSAPIAuthentication` etc. all apply per `~/.ssh/config` — **one source of truth for the configuration** (issues #50/#51). Command-line equivalent: `rw_connect(host="<alias>", useSshConfig=true, backend="openssh")`.
297
+
298
+ **Can a docker container on the remote host be the workspace?** (issue #56) — Yes, and **no plugin change is needed**. There is no container-specific code anywhere in the plugin, yet the two configurations below already cover the request. Pick by whether the container **publishes a port**:
299
+
300
+ - **A: the container maps its sshd** (e.g. `docker run -p 127.0.0.1:2222:22 …`) — a plain "jump host" configuration: Host `127.0.0.1`, Port `2222`, the container's user/key, and the **remote host** as the jump host. Why the files really land in the container: `proxy` means "connect to `<host>:<port>` **as resolved by the bastion**" (`lib/pool.js`: `pclient.forwardOut(...this.config.host, this.config.port...)`), and the resulting channel becomes the main connection's own transport (`opts.sock`), so SFTP rides the **container's** sshd — `rw_list_dir`/`rw_read_file`/`rw_write_file`/`rw_sync` all read and write container files. Command-line equivalent: `rw_connect(host="127.0.0.1", port=2222, username="<container user>", privateKeyPath="<key>")`. Requires `AllowTcpForwarding` on the remote sshd (the default).
301
+ - **B: the container publishes nothing** — pure `~/.ssh/config` plus the system-OpenSSH backend, on **your** machine:
302
+
303
+ ```sshconfig
304
+ Host devbox
305
+ HostName devbox.invalid
306
+ User dev
307
+ IdentityFile ~/.ssh/id_ed25519
308
+ ProxyCommand ssh <remote-host> docker exec -i <container> sh -c 'exec socat - TCP:127.0.0.1:22'
309
+ ```
310
+
311
+ then `rw_connect(host="devbox", useSshConfig=true, backend="openssh")`. Why **both** commands and file operations reach the container: the `openssh` backend hands the alias to `ssh` verbatim and deliberately never emits its own `ProxyCommand` (`lib/openssh.js` omits it so that `~/.ssh/config` decides), while `ssh -s <dest> sftp` and `ssh -- <dest> <cmd>` travel the same stream. Requires an sshd inside the container plus `socat` (or an `nc` that speaks stdio).
312
+ ⚠️ **A Windows caveat**: POSIX OpenSSH shell-expands `ProxyCommand`, but **Windows' own OpenSSH 9.5 does not** (measured: it passes the whole string through as literal argv). So on a Windows host either keep the `ProxyCommand` a single command with no shell metacharacters, or point "ssh path" at Git Bash's `ssh.exe`. (Also: whether `docker exec -i` carries binary data byte-for-byte is unverified here — no docker on this machine — so try a small file with B first.)
313
+
279
314
  **`rw_sync`/`rw_push` reports conflicts** — files changed on both sides are skipped and listed (never silently overwritten); merge manually and retry, or pass `force=true`.
280
315
 
281
316
  **Windows remotes** — everything goes over SFTP, no POSIX shell needed; read Chinese files with `encoding=gbk`.
package/README.md CHANGED
@@ -46,6 +46,7 @@ DSH 的 Web 界面刻意只监听 `127.0.0.1`(CLI 为安全拒绝 `--host 0.0.
46
46
  其余要点:
47
47
 
48
48
  - **22 个模型工具**(便于复制/检索):`rw_info`、`rw_connect`、`rw_machines`、`rw_pick_workspace`、`rw_list_dir`、`rw_stat`、`rw_read_file`、`rw_write_file`、`rw_edit`、`rw_append`、`rw_mkdir`、`rw_remove`、`rw_move`、`rw_exec`、`rw_search`、`rw_download`、`rw_upload`、`rw_sync`、`rw_push`、`rw_forward`、`rw_disconnect`、`rw_deploy_probe`。
49
+ - **会话列表里区分本地/远程**(`0.8.43+`)—— 远程会话的行首有一个**绿点**,悬停显示 `user@host:port` 与远端路径;打开该会话后,标题栏还会常显一个主机标签(本地会话两者都没有)。判定只看会话 cwd 是否落在 `$DSH_HOME/remote-workspaces/…` 镜像里,所以「保存过但没在用」的机器不会给本地会话打标。
49
50
  - **把远端机器的 DSH 界面挂到本机**(`0.8.36+`)—— 不用在远端开任何端口:插件主动 SSH 连过去,在远端**只监听 `127.0.0.1`** 地起一个 `dsh web`,再把端口经隧道搬回本机的一个 loopback 端口。地址带一次性登录令牌,只在本机这次跳转里用一次。
50
51
  - **连不上时一键体检与部署**(`0.8.36+`)—— **连不上大多不是插件的问题,而是远端 dsh 的版本或环境不对**。体检(只读)会报出平台/node/npm/dsh 版本、**原生模块能否启动**、代理与 npm 源,并给出修复建议;「部署并验证」把它装到远端**私有目录**(不写系统目录、不改 PATH、不覆盖你在用的版本,删目录即回滚)并逐步校验。按钮**不再需要先体检**——直接点即可(`0.8.40+`),部署过程自己会重新体检。失败还能交给内置的 `dsh-remote-deploy` 技能排查。
51
52
  - **远端跨平台** —— 文件访问走 SFTP 协议层(不依赖 POSIX shell),Linux/macOS/Windows 远端都能列/读/写/搜索/同步。
@@ -119,6 +120,15 @@ dsh plugin add dsh-remote
119
120
  `http://127.0.0.1:<端口>/?token=…` 粘进输入框,点「连接已有实例」,插件不会再去启第二个。
120
121
  - 远端 `dsh` 不在 SSH 登录 PATH 时,用 `webAttachCommand` 指向它的绝对路径;
121
122
  想让它别污染远端用户自己的会话/设置,用 `webAttachDshHome` 指一个临时目录。
123
+ **按机器设**:设置 → 远程工作区 → 机器卡片 → 展开「高级」→「远端 dsh 命令」,
124
+ 填绝对路径即可(例如
125
+ `/home/you/.nvm/versions/node/v22.19.0/bin/node /home/you/.dsh-remote/dsh/node_modules/@deepseek-ai/dsh/lib/bin.js`);
126
+ 留空则用全局的 `webAttachCommand`。「部署并验证」成功后也会自动写进这个字段。
127
+ - 隧道跟着 SSH 连接走:连接断了界面会显示「连接已断开」,**不会**影响本机 DSH 进程;
128
+ 池自动重连后插件会重新解析远端端口(必要时重启它自己启动的那个远端进程),界面点「重连」即可继续用。
129
+ - 远端 node 需要 **≥ 20.12**(dsh 启动要用 `node:util` 的 `parseEnv`,该导出从 20.12 起才有)。
130
+ node 太旧时体检会直接报 blocker:npm 只会给 EBADENGINE 警告、照样把包装完,
131
+ 直到启动才崩在 `does not provide an export named 'parseEnv'`,所以必须在装之前拦住。
122
132
 
123
133
  ### 连不上?先体检,再一键部署
124
134
 
@@ -224,7 +234,9 @@ scripts/dev-run.sh --status # 是否在运行
224
234
  | `hostKeyMode` | string | `accept-new` | 主机指纹策略:`accept-new`(首次信任)、`verify`(拒绝未知主机)、`off`(跳过校验) |
225
235
  | `useAgent` | bool | `false` | 用 OpenSSH agent(`SSH_AUTH_SOCK`)认证 |
226
236
  | `keyboardInteractive` | bool | `false` | 允许 keyboard-interactive 认证(OTP/MFA)并复用配置的密码 |
227
- | `proxy` | object | — | 跳板机:`{ host, port?, username?, password?, privateKeyPath? }` |
237
+ | `backend` | string | `native` | SSH 传输后端(issue #51)。`native`=内置 ssh2(密码/私钥/agent/动态码);`openssh`=**调用本机 `ssh` 命令**,因此支持 GSSAPI/Kerberos(`kinit` 后免密)以及 `ProxyCommand`、`PKCS11Provider`、`Match` 等 OpenSSH 专有特性。**可按机器单独设置**。代价:每条命令新建一次连接(比 native 慢);SFTP 仍复用单条长连接。主机名会被**原样**交给 ssh(`~/.ssh/config` 是配置唯一来源),主机密钥也由 ssh 用你自己的 `known_hosts` 校验 |
238
+ | `sshBinary` | string | `''` | `openssh` 后端调用的 `ssh` 可执行文件(留空=用 PATH 里的 `ssh`) |
239
+ | `proxy` | object | — | 跳板机:`{ host, port?, username?, password?, privateKeyPath? }`;语义是「在跳板机上连目标 host:port」,所以也能用来连跳板机上 docker 容器的 sshd(见「常见问题」里的 issue #56) |
228
240
  | `autoPush` | bool | `false` | 镜像内文件被编辑后自动推回远端(watcher,带防抖) |
229
241
  | `auditLog` | bool | `true` | 把执行的命令追加到 `$DSH_HOME/remote-workspaces/audit.log` |
230
242
  | `encoding` | string | `utf-8` | 远程文件读写的文本编码(如 `gbk`) |
@@ -239,7 +251,7 @@ scripts/dev-run.sh --status # 是否在运行
239
251
  | `updateCheckIntervalMs` | int | 21600000(6h) | `auto` 模式检查 npm 的间隔(下限 60000) |
240
252
  | `updateAutoReload` | bool | `true` | 更新落地后自动热切换宿主半;`false` 则留到下次启动,设置页会显示 `pendingReload` |
241
253
  | `webAttachPortStart` | int | `3088` | 「远程 DSH 界面」在本机监听的起始端口(占用则顺延,仅监听 127.0.0.1) |
242
- | `webAttachCommand` | string | `dsh` | 在远端启动 `dsh web` 用的命令;远端 `dsh` 不在 SSH 登录 PATH 时改这里 |
254
+ | `webAttachCommand` | string | `dsh` | 在远端启动 `dsh web` 用的命令;远端 `dsh` 不在 SSH 登录 PATH 时改这里(也可按机器在设置页「高级」里单独设,机器级优先) |
243
255
  | `webAttachDshHome` | string | `''` | 远端启动时导出的 `DSH_HOME`;留空则复用远端用户自己的 harness home |
244
256
  | `webAttachWaitSeconds` | int | `45` | 等待远端 `dsh web` 打印启动令牌的秒数 |
245
257
  | `webInstallPrefix` | string | `''` | 自动部署装到远端的哪个目录;留空用 `$HOME/.dsh-remote/dsh`(不写系统目录、不改 PATH) |
@@ -252,12 +264,61 @@ scripts/dev-run.sh --status # 是否在运行
252
264
 
253
265
  **`@` 能列出远程文件,但内置读文件工具打不开** —— harness 自带工具看到的是**本地镜像**,要等 `rw_sync` 下载后才有内容。读远程文件请用 `rw_read_file` 或侧栏远程文件 tab。
254
266
 
267
+ **远程会话里的文件链接打不开**(issue #55)—— 已修。链接被点开时 harness 会把它变成
268
+ `dsh-resource://file/session/<会话>/<路径>`,然后交给内置的文本预览去读;而那个预览读的是**本机**
269
+ 文件系统,远程会话的 cwd 是**本地镜像目录**(`rw_sync` 之前是空的),所以打开只会失败。
270
+ 插件现在注册了一个更高优先级的地址处理器,把这个地址按远程主机解析后用 SFTP 打开(和侧栏「远程文件」同一个编辑器)。
271
+ 安全边界是保守的:只有当该会话的 cwd **确实落在某个镜像目录里**、且镜像表已加载时才接管,
272
+ 本地会话与页面刚加载的那一瞬间仍然走内置预览 —— 不会把本地文件误当成远程文件。
273
+ 两个仍然开不了的情况会给出明确提示而不是空白页:链接指向的是**绝对**远端路径之外无法映射的地址,
274
+ 或该会话的工作区不是一个远程镜像目录。
275
+
276
+ **远程会话里的终端还是本机的**(issue #55)—— 这是**设计如此,插件改不了**,但有等效方案。
277
+ DSH 的侧栏终端是核心功能:它的 PTY 来自**全局唯一**的 `subprocess` 服务,cwd 取会话自己的
278
+ 工作区目录(远程会话就是本地镜像),插件既不能替换那个服务、也不能把核心终端重定向到 SSH。
279
+ 想要「真正的远程终端」,用**「远程 DSH 界面」**(见上文):它在本机浏览器里打开**远端机器自己**的
280
+ DSH,那个界面里的终端就跑在远端主机上,零额外配置。只想偶尔跑一条远程命令则用 `rw_exec`
281
+ (要交互式 PTY 就加 `pty: true`)。
282
+
255
283
  **主机指纹变了** —— `/remote forget-key`(或设置页 → 机器 → 重新信任)。
256
284
 
257
285
  **连接报「认证失败」** —— 检查用户名/密码/私钥路径;加密私钥要填 Passphrase;需要动态码时勾选 keyboard-interactive。
258
286
 
259
287
  **连不上内网机器** —— 填「跳板机」主机(也可先把跳板机本身配成一台机器)。
260
288
 
289
+ **能不能拿远程主机上的 docker 容器当工作空间?**(issue #56)—— 能,**不需要改插件**;
290
+ 插件对容器没有任何专门代码,但下面两条既有配置已经完整覆盖这个需求。请按容器**有没有对外暴露端口**二选一:
291
+
292
+ - **A:容器把 sshd 端口映射出来了**(例如 `docker run -p 127.0.0.1:2222:22 …`)—— 纯「跳板机」配置:
293
+ Host 填 `127.0.0.1`、Port 填 `2222`、用户/私钥填容器里的,再把「跳板机」主机填**那台远程主机**。
294
+ 为什么这样文件就能落到容器里:`proxy` 的语义是「**在跳板机上**连 `<host>:<port>`」
295
+ (`lib/pool.js` 的 `pclient.forwardOut(...this.config.host, this.config.port...)`),
296
+ 拿到的通道会被当成主连接自己的 transport(`opts.sock`),于是 SFTP 走的是**容器自己的 sshd** ⇒
297
+ `rw_list_dir`/`rw_read_file`/`rw_write_file`/`rw_sync` 全部读写的都是容器内的文件。
298
+ 命令行等价写法:`rw_connect(host="127.0.0.1", port=2222, username="<容器用户>", privateKeyPath="<私钥>")`。
299
+ 要求:远程主机的 sshd 允许转发(`AllowTcpForwarding yes`,默认就是)。
300
+ - **B:容器没有暴露端口** —— 纯 `~/.ssh/config` + 系统 OpenSSH 后端,在**本机** `~/.ssh/config` 写:
301
+
302
+ ```sshconfig
303
+ Host devbox
304
+ HostName devbox.invalid
305
+ User dev
306
+ IdentityFile ~/.ssh/id_ed25519
307
+ ProxyCommand ssh <远程主机> docker exec -i <容器名> sh -c 'exec socat - TCP:127.0.0.1:22'
308
+ ```
309
+
310
+ 然后 `rw_connect(host="devbox", useSshConfig=true, backend="openssh")`。
311
+ 为什么 `exec` 和文件操作**都会**进容器:`openssh` 后端把别名原样交给 `ssh`,
312
+ 自己**从不**生成 `ProxyCommand`(`lib/openssh.js` 明确不传,就是为了让 `~/.ssh/config` 说了算),
313
+ 而 `ssh -s <目标> sftp` 与 `ssh -- <目标> <命令>` 走的是同一条流 ⇒ 两者都穿过这个 ProxyCommand。
314
+ 要求:容器里有 sshd,且有 `socat`(或用支持 stdio 的 `nc`)。
315
+ ⚠️ **Windows 上有个坑**:POSIX OpenSSH 会把 `ProxyCommand` 交给 shell 展开,**Windows 自带的 OpenSSH 9.5 不会**
316
+ (实测它把整串按字面当成 argv)。所以在 Windows 跑本插件时,要么把 `ProxyCommand` 写成
317
+ 不含 shell 元字符的单条命令,要么把「ssh 路径」指向 Git Bash 的 `ssh.exe`。
318
+ (另:`docker exec -i` 传二进制是否逐字节无损,我这边没有 docker 环境验证过,B 方案建议先用小文件试。)
319
+
320
+ **企业机器只允许 GSSAPI/Kerberos(`kinit` 后 `ssh` 能进,插件进不去)** —— 在设置 → 远程工作区 → 高级配置里把「连接后端」改成 **`openssh`**(调用本机 `ssh`),并勾选「使用 ssh_config 别名」,Host 填 `~/.ssh/config` 里那个**别名**(不是 IP)。这样登录由本机 ssh 用你的 Kerberos 票据完成,且 `ProxyCommand`、`GSSAPIAuthentication` 等全部照 `~/.ssh/config` 生效 —— **配置只有 `~/.ssh/config` 一个来源**(issue #50/#51)。命令行等价写法:`rw_connect(host="<别名>", useSshConfig=true, backend="openssh")`。
321
+
261
322
  **`rw_sync`/`rw_push` 报冲突** —— 两边都改过的文件会被跳过并列出(绝不静默覆盖);手动合并后重试,或用 `force=true` 以一边为准。
262
323
 
263
324
  **Windows 远程** —— 全部走 SFTP,不依赖 POSIX shell;中文文件用 `encoding=gbk`。