@linxin666/dsh-ssh 0.1.12 → 0.1.13

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.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # node scripts/verify-docs.mjs --write <dir>
5
+ README.md: ab9bd6b8b9dfe08a1be7f7dbb3bcd41d086659e0
6
+ README.zh.md: 3972324c8541d87d0603de64be7817f32d3e98ad
package/README.md CHANGED
@@ -1,46 +1,41 @@
1
- # dsh-ssh — 远程 SSH 运维插件(DSH 版 ssh-skill)
1
+ # dsh-ssh — Remote SSH operations plugin for DSH
2
2
 
3
- 基于 [badseal/ssh-skill](https://github.com/badseal/ssh-skill) 的能力清单,为
4
- DeepSeek Harness(DSH)定制的远程 SSH 插件:Host 进程内的持久连接池 + Web GUI
5
- 主机管理面板 + Web 终端 + Agent 工具,全部通过官方 NPM SDK 实现,不修改 DSH
6
- 源码。
3
+ English | [中文](README.zh.md)
7
4
 
8
- ## 能力
5
+ Built on the capability list of [badseal/ssh-skill](https://github.com/badseal/ssh-skill), a remote SSH plugin tailored for DeepSeek Harness (DSH): a persistent connection pool inside the Host process + a Web GUI host-management panel + a Web terminal + Agent tools, all implemented through the official NPM SDK without modifying DSH source.
9
6
 
10
- | 能力 | 说明 |
7
+ ## Capabilities
8
+
9
+ | Capability | Description |
11
10
  | --- | --- |
12
- | 主机管理 | 增删改查、搜索、连接测试;配置存 `~/.dsh/dsh-ssh.json`;支持密钥 / 密码认证、passphrase 密钥、ProxyJump 跳板机(多级) |
13
- | 配置导入 | 一键解析标准 `~/.ssh/config`(Host/HostName/User/Port/IdentityFile/ProxyJump 等),已有别名自动跳过 |
14
- | 持久连接池 | 每台主机复用长连接(对应 ssh-skill 的守护进程),空闲 30 分钟自动断开,断线自动重连(最多 3 次) |
15
- | 命令执行 | exec 带超时(默认 60s),stdout/stderr 分离,输出截断保护(2MB) |
16
- | Web 终端 | xterm.js + WebSocket PTY 终端,自适应尺寸,实时输出 |
17
- | 文件传输 | SFTP 上传(浏览器选文件,NDJSON 进度流)、下载(进度条 + 浏览器保存);远程目录浏览 |
18
- | 端口转发 | 本地端口转发隧道(仅监听 127.0.0.1),访问远程数据库 / 内网服务,支持列表 / 停止 |
19
- | 集群执行 | 一条命令并发跑多台主机(按别名 / 环境 / 标签过滤,默认并发 8) |
20
- | Agent 工具 | `ssh_list` / `ssh_exec` / `ssh_upload` / `ssh_download` / `ssh_tunnel` / `ssh_cluster`,GUI 与 Agent 共享同一份主机配置 |
21
-
22
- ## 安全模型
23
-
24
- - 所有 `/api/dsh-ssh/*` 路由仅限 loopback 访问(含同源校验)——对远程服务器执行
25
- 命令的接口不会暴露给局域网。
26
- - 密码 / 密钥口令以明文保存在 `~/.dsh/dsh-ssh.json`,文件权限 0600、目录 0700
27
- (与 ssh-skill 把密码写进 ssh-config 注释同一信任模型)。
28
- - 隧道只监听 `127.0.0.1`。
29
- - Agent 使用工具前,主机需先在 GUI 中配置(或从 ~/.ssh/config 导入)。
30
- - `ssh_upload` / `ssh_download` 以宿主进程权限直接读写本机任意路径(不经 bash
31
- 沙箱)——与 ssh-skill 的宿主本地路径语义一致,注意该权限面。
32
- - exec / cluster 的远程输出原样返回(不脱敏),命令如 `env` 可能把远端环境中的
33
- 密钥带回对话记录。
34
-
35
- ## 安装
36
-
37
- 推荐直接安装全家桶聚合包 `@linxin666/dsh-web-ui-all`(一个包装齐全部功能插件与皮肤),或单独安装本插件:
11
+ | Host management | CRUD, search, connection test; config stored in `~/.dsh/dsh-ssh.json`; supports key / password auth, passphrase keys, ProxyJump jump hosts (multi-level) |
12
+ | Config import | One-click parse of a standard `~/.ssh/config` (Host/HostName/User/Port/IdentityFile/ProxyJump etc.); existing aliases are skipped |
13
+ | Persistent connection pool | Reuses a long-lived connection per host (opposite of the ssh-skill daemon), automatically disconnects after 30 minutes idle, auto-reconnects on disconnect (up to 3 times) |
14
+ | Command execution | exec with a timeout (default 60s), stdout/stderr separated, output truncation guard (2MB) |
15
+ | Web terminal | xterm.js + WebSocket PTY terminal, auto-sizing, real-time output |
16
+ | File transfer | SFTP upload (browser file picker, NDJSON progress stream), download (progress bar + browser save); remote directory browsing |
17
+ | Port forwarding | Local port-forward tunnel (listens on 127.0.0.1 only) to reach a remote database / intranet service; list / stop supported |
18
+ | Cluster execution | One command run concurrently across many hosts (filter by alias / environment / tag, default concurrency 8) |
19
+ | Agent tools | `ssh_list` / `ssh_exec` / `ssh_upload` / `ssh_download` / `ssh_tunnel` / `ssh_cluster`; GUI and Agent share the same host config |
20
+
21
+ ## Security model
22
+
23
+ - All `/api/dsh-ssh/*` routes are loopback-only (with same-origin checks) — the interfaces that execute commands against remote servers are not exposed to the LAN.
24
+ - Passwords / key passphrases are stored in plain text in `~/.dsh/dsh-ssh.json`, file mode 0600, directory 0700 (the same trust model as ssh-skill writing passwords into ssh-config comments).
25
+ - Tunnels only listen on `127.0.0.1`.
26
+ - Before the Agent uses a tool, the host must first be configured in the GUI (or imported from ~/.ssh/config).
27
+ - `ssh_upload` / `ssh_download` read/write arbitrary local paths on this machine with host-process privileges (not through the bash sandbox) — same host-local-path semantics as ssh-skill, be aware of that permission surface.
28
+ - The remote output of exec / cluster is returned verbatim (not sanitized); a command like `env` may bring secrets from the remote environment back into the conversation log.
29
+
30
+ ## Install
31
+
32
+ Install the family aggregate package `@linxin666/dsh-web-ui-all` (all plugins and skins in one) or this plugin alone:
38
33
 
39
34
  ```sh
40
- # 推荐:直接从 npm 安装
35
+ ### 从 npm 安装(推荐)
41
36
  dsh plugin --profile web add @linxin666/dsh-ssh
42
37
 
43
- # 或从仓库安装(开发调试)
38
+ ### 从仓库安装(开发调试)
44
39
  git clone https://github.com/zhu1090093659/dsh-web-ui.git
45
40
  cd dsh-web-ui
46
41
  pnpm install && pnpm -r build
@@ -48,31 +43,30 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-ssh
48
43
 
49
44
  ```
50
45
 
51
- 安装后**重启 `dsh web`**:侧边栏出现「SSH」入口;Agent 提示词中自动出现插件说明。
46
+ After installing, **restart `dsh web`**: a "SSH" entry appears in the sidebar; the plugin description is injected into the Agent prompt automatically.
52
47
 
53
- ## 配置
48
+ ## Configuration
54
49
 
55
- 设置面板(插件配置)可开关 `announceToAgent`(是否向 Agent 宣告插件)与
56
- `enabled`(总开关)。
50
+ The settings panel (plugin config) toggles `announceToAgent` (whether to announce the plugin to the Agent) and `enabled` (master switch).
57
51
 
58
- ## 数据
52
+ ## Data
59
53
 
60
- - 主机配置:`~/.dsh/dsh-ssh.json`(版本化 JSON,原子写入)
61
- - 传输暂存:`os.tmpdir()/dsh-ssh-uploads/`
54
+ - Host config: `~/.dsh/dsh-ssh.json` (versioned JSON, atomic write)
55
+ - Transfer staging: `os.tmpdir()/dsh-ssh-uploads/`
62
56
 
63
- ## 开发
57
+ ## Development
64
58
 
65
59
  ```sh
66
60
  pnpm install --filter @linxin666/dsh-ssh...
67
- pnpm --filter @linxin666/dsh-ssh test # 单测:store + 引擎(内嵌 ssh2 Server + 真实 sshd)
68
- pnpm --filter @linxin666/dsh-ssh build # tsc 类型 + tsdown 双半区产物
61
+ pnpm --filter @linxin666/dsh-ssh test # unit tests: store + engine (embedded ssh2 Server + real sshd)
62
+ pnpm --filter @linxin666/dsh-ssh build # tsc types + tsdown dual-half artifacts
69
63
  ```
70
64
 
71
- ## 已知限制
65
+ ## Known limitations
72
66
 
73
- - 上传的远程目标路径必须是绝对路径(相对路径会被拒绝)。
74
- - 下载暂不支持整个目录(逐文件下载);上传支持目录递归(walk 本地目录逐文件传)。
75
- - exec 断线自动重连(最多 3 次)可能重复执行非幂等命令——长命令注意副作用。
76
- - 跳板机 ProxyJump 的每一跳必须是本插件已配置的主机别名。
77
- - 断点续传(resume)暂未实现。
78
- - Agent 工具的传输为宿主机器本地路径(与 ssh-skill 相同的语义)。
67
+ - The remote target path of an upload must be an absolute path (relative paths are rejected).
68
+ - Download does not support a whole directory yet (download files individually); upload supports recursive directories (walks the local directory and transfers file by file).
69
+ - exec auto-reconnect on disconnect (up to 3 times) may re-execute non-idempotent commands — watch out for side effects on long commands.
70
+ - Every hop of a ProxyJump jump host must be a host alias already configured in this plugin.
71
+ - Resume (broken-transfer continuation) is not implemented yet.
72
+ - The transfer of Agent tools is a host-machine local path (same semantics as ssh-skill).
package/README.zh.md ADDED
@@ -0,0 +1,72 @@
1
+ # dsh-ssh — 远程 SSH 运维插件(DSH 版 ssh-skill)
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 基于 [badseal/ssh-skill](https://github.com/badseal/ssh-skill) 的能力清单,为 DeepSeek Harness(DSH)定制的远程 SSH 插件:Host 进程内的持久连接池 + Web GUI 主机管理面板 + Web 终端 + Agent 工具,全部通过官方 NPM SDK 实现,不修改 DSH 源码。
6
+
7
+ ## 能力
8
+
9
+ | 能力 | 说明 |
10
+ | --- | --- |
11
+ | 主机管理 | 增删改查、搜索、连接测试;配置存 `~/.dsh/dsh-ssh.json`;支持密钥 / 密码认证、passphrase 密钥、ProxyJump 跳板机(多级) |
12
+ | 配置导入 | 一键解析标准 `~/.ssh/config`(Host/HostName/User/Port/IdentityFile/ProxyJump 等),已有别名自动跳过 |
13
+ | 持久连接池 | 每台主机复用长连接(对应 ssh-skill 的守护进程),空闲 30 分钟自动断开,断线自动重连(最多 3 次) |
14
+ | 命令执行 | exec 带超时(默认 60s),stdout/stderr 分离,输出截断保护(2MB) |
15
+ | Web 终端 | xterm.js + WebSocket PTY 终端,自适应尺寸,实时输出 |
16
+ | 文件传输 | SFTP 上传(浏览器选文件,NDJSON 进度流)、下载(进度条 + 浏览器保存);远程目录浏览 |
17
+ | 端口转发 | 本地端口转发隧道(仅监听 127.0.0.1),访问远程数据库 / 内网服务,支持列表 / 停止 |
18
+ | 集群执行 | 一条命令并发跑多台主机(按别名 / 环境 / 标签过滤,默认并发 8) |
19
+ | Agent 工具 | `ssh_list` / `ssh_exec` / `ssh_upload` / `ssh_download` / `ssh_tunnel` / `ssh_cluster`,GUI 与 Agent 共享同一份主机配置 |
20
+
21
+ ## 安全模型
22
+
23
+ - 所有 `/api/dsh-ssh/*` 路由仅限 loopback 访问(含同源校验)——对远程服务器执行命令的接口不会暴露给局域网。
24
+ - 密码 / 密钥口令以明文保存在 `~/.dsh/dsh-ssh.json`,文件权限 0600、目录 0700(与 ssh-skill 把密码写进 ssh-config 注释同一信任模型)。
25
+ - 隧道只监听 `127.0.0.1`。
26
+ - Agent 使用工具前,主机需先在 GUI 中配置(或从 ~/.ssh/config 导入)。
27
+ - `ssh_upload` / `ssh_download` 以宿主进程权限直接读写本机任意路径(不经 bash 沙箱)——与 ssh-skill 的宿主本地路径语义一致,注意该权限面。
28
+ - exec / cluster 的远程输出原样返回(不脱敏),命令如 `env` 可能把远端环境中的密钥带回对话记录。
29
+
30
+ ## 安装
31
+
32
+ 推荐直接安装全家桶聚合包 `@linxin666/dsh-web-ui-all`(一个包装齐全部功能插件与皮肤),或单独安装本插件:
33
+
34
+ ```sh
35
+ ### 从 npm 安装(推荐)
36
+ dsh plugin --profile web add @linxin666/dsh-ssh
37
+
38
+ ### 从仓库安装(开发调试)
39
+ git clone https://github.com/zhu1090093659/dsh-web-ui.git
40
+ cd dsh-web-ui
41
+ pnpm install && pnpm -r build
42
+ dsh plugin --profile web add link:$(pwd)/packages/dsh-ssh
43
+
44
+ ```
45
+
46
+ 安装后**重启 `dsh web`**:侧边栏出现「SSH」入口;Agent 提示词中自动出现插件说明。
47
+
48
+ ## 配置
49
+
50
+ 设置面板(插件配置)可开关 `announceToAgent`(是否向 Agent 宣告插件)与 `enabled`(总开关)。
51
+
52
+ ## 数据
53
+
54
+ - 主机配置:`~/.dsh/dsh-ssh.json`(版本化 JSON,原子写入)
55
+ - 传输暂存:`os.tmpdir()/dsh-ssh-uploads/`
56
+
57
+ ## 开发
58
+
59
+ ```sh
60
+ pnpm install --filter @linxin666/dsh-ssh...
61
+ pnpm --filter @linxin666/dsh-ssh test # 单测:store + 引擎(内嵌 ssh2 Server + 真实 sshd)
62
+ pnpm --filter @linxin666/dsh-ssh build # tsc 类型 + tsdown 双半区产物
63
+ ```
64
+
65
+ ## 已知限制
66
+
67
+ - 上传的远程目标路径必须是绝对路径(相对路径会被拒绝)。
68
+ - 下载暂不支持整个目录(逐文件下载);上传支持目录递归(walk 本地目录逐文件传)。
69
+ - exec 断线自动重连(最多 3 次)可能重复执行非幂等命令——长命令注意副作用。
70
+ - 跳板机 ProxyJump 的每一跳必须是本插件已配置的主机别名。
71
+ - 断点续传(resume)暂未实现。
72
+ - Agent 工具的传输为宿主机器本地路径(与 ssh-skill 相同的语义)。
package/lib/client.js CHANGED
@@ -594,8 +594,8 @@ window.__ModuleLoader__.load({
594
594
  return String(error);
595
595
  }
596
596
  //#endregion
597
- //#region \0dsh-css:/home/runner/work/dsh-web-ui/dsh-web-ui/packages/dsh-ssh/src/client/panel/panel.module.css.mjs
598
- const css = "[data-pane=conversation]{position:relative}[data-dsh-ssh-view]{z-index:60;background:var(--dsw-alias-bg-base);display:none;position:absolute;inset:0}html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-dsh-ssh-view]{display:block}html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-pane=conversation]>:not([data-dsh-ssh-view]),html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*=centerCol]>:not([data-dsh-ssh-view]){display:none!important}._X8bTW_entry{width:100%;height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 12px;font-size:13px;display:flex}._X8bTW_entry:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}._X8bTW_entry[data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}._X8bTW_entryIcon{flex:none;justify-content:center;align-items:center;display:inline-flex}._X8bTW_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] ._X8bTW_entry{justify-content:center;width:100%;padding:0}[data-dsh-frame][data-sidebar-collapsed] ._X8bTW_entryLabel{display:none}._X8bTW_view{overflow:hidden}._X8bTW_panel{background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:10px;padding:14px 16px 16px;display:flex}._X8bTW_panelHeader{flex:none;align-items:center;gap:10px;display:flex}._X8bTW_panelTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;flex:1;margin:0;font-size:16px;font-weight:700}._X8bTW_tabBar{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;gap:2px;display:flex}._X8bTW_tab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-bottom:2px solid #0000;border-radius:6px 6px 0 0;padding:7px 14px;font-size:13px}._X8bTW_tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._X8bTW_tab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}._X8bTW_panelContent{flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}._X8bTW_tabBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow-y:auto}._X8bTW_fillBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow:hidden}._X8bTW_toolbar,._X8bTW_controls{flex-wrap:wrap;flex:none;align-items:center;gap:8px;display:flex}._X8bTW_controls ._X8bTW_input{flex:0 260px;min-width:150px}._X8bTW_toolbarSpacer{flex:1}._X8bTW_search{min-width:120px;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:0 260px;padding:6px 10px;font-size:13px}._X8bTW_search::placeholder{color:var(--dsw-alias-label-tertiary)}._X8bTW_tableWrap{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex:1;min-height:0;overflow:auto}._X8bTW_table{border-collapse:collapse;width:100%;font-size:12.5px}._X8bTW_table th{z-index:1;text-align:left;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-bottom:1px solid var(--dsw-alias-border-l1);white-space:nowrap;padding:8px 10px;font-weight:600;position:sticky;top:0}._X8bTW_table td{border-bottom:1px solid var(--dsw-alias-separator-primary);vertical-align:top;padding:7px 10px}._X8bTW_table tbody tr:last-child td{border-bottom:none}._X8bTW_table tbody tr:hover td{background:var(--dsw-alias-interactive-bg-hover)}._X8bTW_mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}._X8bTW_cellMuted{color:var(--dsw-alias-label-tertiary)}._X8bTW_actions{white-space:nowrap;align-items:center;gap:8px;display:flex}._X8bTW_inlineTest{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:11.5px;display:inline-flex}._X8bTW_inlineTest[data-status=ok]{color:var(--dsw-alias-state-success-primary)}._X8bTW_inlineTest[data-status=fail]{color:var(--dsw-alias-state-error-primary)}._X8bTW_badge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;line-height:1.6;display:inline-block}._X8bTW_badge[data-kind=key]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}._X8bTW_badge[data-kind=password]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._X8bTW_badge[data-status=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._X8bTW_badge[data-status=fail]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._X8bTW_badge[data-status=timeout]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._X8bTW_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:600}._X8bTW_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}._X8bTW_primaryButton:disabled{opacity:.5;cursor:default}._X8bTW_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}._X8bTW_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._X8bTW_ghostButton:disabled{opacity:.45;cursor:default}._X8bTW_iconButton{width:26px;height:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;font-size:13px;display:inline-flex}._X8bTW_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}._X8bTW_linkButton{color:var(--dsw-alias-state-business-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0;font-size:12px}._X8bTW_linkButton:hover:not(:disabled){text-decoration:underline}._X8bTW_linkButton:disabled{opacity:.45;cursor:default}._X8bTW_linkButton[data-danger]{color:var(--dsw-alias-state-error-primary)}._X8bTW_spinner{border:2px solid var(--dsw-alias-state-business-primary);vertical-align:-1px;border-top-color:#0000;border-radius:50%;flex:none;width:11px;height:11px;animation:.8s linear infinite _X8bTW_dshSshSpin;display:inline-block}@keyframes _X8bTW_dshSshSpin{to{transform:rotate(360deg)}}._X8bTW_banner{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;border-radius:8px;padding:8px 12px;font-size:12.5px;line-height:1.5}._X8bTW_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._X8bTW_banner[data-kind=error]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._X8bTW_banner[data-kind=info]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}._X8bTW_empty,._X8bTW_loading{text-align:center;color:var(--dsw-alias-label-tertiary);padding:28px 12px;font-size:12.5px}._X8bTW_modalBackdrop{z-index:40;background:var(--dsw-alias-bg-mask-1);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}._X8bTW_modal{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);width:min(560px,100vw - 48px);max-height:calc(100vh - 96px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:14px;flex-direction:column;gap:12px;padding:18px;display:flex;overflow-y:auto}._X8bTW_modalTitle{margin:0;font-size:15px;font-weight:700}._X8bTW_modalFooter{justify-content:flex-end;gap:10px;margin-top:4px;display:flex}._X8bTW_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}._X8bTW_hint{color:var(--dsw-alias-label-tertiary);font-size:11.5px}._X8bTW_field{flex-direction:column;gap:5px;display:flex}._X8bTW_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}._X8bTW_input{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);resize:vertical;border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px}._X8bTW_input:focus{border-color:var(--dsw-alias-state-business-primary)}._X8bTW_input::placeholder{color:var(--dsw-alias-label-tertiary)}._X8bTW_input:disabled{opacity:.55}._X8bTW_formRow{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px 12px;display:grid}._X8bTW_radioRow{align-items:center;gap:16px;display:flex}._X8bTW_radioLabel{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:6px;font-size:13px;display:inline-flex}._X8bTW_termBody{flex-direction:column;flex:1;gap:8px;min-height:0;display:flex;overflow:hidden}._X8bTW_termWrap{border:1px solid var(--dsw-alias-border-l1);background:#0b0e14;border-radius:10px;flex:1;min-height:0;position:relative;overflow:hidden}._X8bTW_termContainer{padding:8px 10px;position:absolute;inset:0}._X8bTW_termPlaceholder{z-index:2;text-align:center;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-base);justify-content:center;align-items:center;padding:0 24px;font-size:12.5px;display:flex;position:absolute;inset:0}._X8bTW_hiddenFile{display:none}._X8bTW_browsePanel{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;display:flex;overflow:hidden}._X8bTW_browseHeader{background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;gap:8px;padding:8px 10px;display:flex}._X8bTW_browsePath{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;overflow:hidden}._X8bTW_browseList{max-height:240px;overflow-y:auto}._X8bTW_dirRow{text-align:left;width:100%;font:inherit;color:var(--dsw-alias-label-primary);border:none;border-bottom:1px solid var(--dsw-alias-separator-primary);cursor:pointer;background:0 0;grid-template-columns:minmax(0,1fr) auto auto;gap:12px;padding:5px 10px;font-size:12.5px;display:grid}._X8bTW_dirRow:hover{background:var(--dsw-alias-interactive-bg-hover)}._X8bTW_dirRow[data-type=dir] ._X8bTW_dirName{color:var(--dsw-alias-state-business-primary)}._X8bTW_dirRow[data-up]{color:var(--dsw-alias-label-secondary)}._X8bTW_dirName{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden}._X8bTW_dirType{color:var(--dsw-alias-label-tertiary);white-space:nowrap}._X8bTW_dirSize{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-align:right}._X8bTW_transferBlock{flex-direction:column;flex:none;gap:6px;display:flex}._X8bTW_progressMeta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:12px;font-size:12px;display:flex}._X8bTW_progressTrack{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;height:8px;overflow:hidden}._X8bTW_progressBar{background:var(--dsw-alias-state-business-primary);border-radius:999px;height:100%;transition:width .12s linear}._X8bTW_tunnelList{flex-direction:column;flex:none;gap:8px;display:flex}._X8bTW_tunnelRow{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;align-items:center;gap:10px;padding:8px 12px;display:flex}._X8bTW_tunnelRow[data-state=forwarding]{border-color:var(--dsw-alias-state-success-primary)}._X8bTW_tunnelRow[data-state=connecting]{border-color:var(--dsw-alias-state-warn-primary)}._X8bTW_tunnelRow[data-state=failed]{border-color:var(--dsw-alias-state-error-primary)}._X8bTW_tunnelLabel{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;overflow:hidden}._X8bTW_formCard{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;gap:10px;padding:12px;display:flex}._X8bTW_clusterForm{flex-direction:column;flex:none;gap:8px;display:flex}._X8bTW_clusterFilters{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;display:grid}._X8bTW_commandInput{min-height:96px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}._X8bTW_cellDetails summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px}._X8bTW_cellPre{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;border-radius:6px;max-height:180px;margin:4px 0 0;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;overflow:auto}";
597
+ //#region \0dsh-css:packages/dsh-ssh/src/client/panel/panel.module.css.mjs
598
+ const css = "[data-pane=conversation]{position:relative}[data-dsh-ssh-view]{z-index:60;background:var(--dsw-alias-bg-base);display:none;position:absolute;inset:0}html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-dsh-ssh-view]{display:block}html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-pane=conversation]>:not([data-dsh-ssh-view]),html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*=centerCol]>:not([data-dsh-ssh-view]){display:none!important}.mL8Uca_entry{width:100%;height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 12px;font-size:13px;display:flex}.mL8Uca_entry:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}.mL8Uca_entry[data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}.mL8Uca_entryIcon{flex:none;justify-content:center;align-items:center;display:inline-flex}.mL8Uca_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entry{justify-content:center;width:100%;padding:0}[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entryLabel{display:none}.mL8Uca_view{overflow:hidden}.mL8Uca_panel{background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:10px;padding:14px 16px 16px;display:flex}.mL8Uca_panelHeader{flex:none;align-items:center;gap:10px;display:flex}.mL8Uca_panelTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;flex:1;margin:0;font-size:16px;font-weight:700}.mL8Uca_tabBar{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;gap:2px;display:flex}.mL8Uca_tab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-bottom:2px solid #0000;border-radius:6px 6px 0 0;padding:7px 14px;font-size:13px}.mL8Uca_tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_tab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}.mL8Uca_panelContent{flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}.mL8Uca_tabBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow-y:auto}.mL8Uca_fillBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow:hidden}.mL8Uca_toolbar,.mL8Uca_controls{flex-wrap:wrap;flex:none;align-items:center;gap:8px;display:flex}.mL8Uca_controls .mL8Uca_input{flex:0 260px;min-width:150px}.mL8Uca_toolbarSpacer{flex:1}.mL8Uca_search{min-width:120px;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:0 260px;padding:6px 10px;font-size:13px}.mL8Uca_search::placeholder{color:var(--dsw-alias-label-tertiary)}.mL8Uca_tableWrap{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex:1;min-height:0;overflow:auto}.mL8Uca_table{border-collapse:collapse;width:100%;font-size:12.5px}.mL8Uca_table th{z-index:1;text-align:left;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-bottom:1px solid var(--dsw-alias-border-l1);white-space:nowrap;padding:8px 10px;font-weight:600;position:sticky;top:0}.mL8Uca_table td{border-bottom:1px solid var(--dsw-alias-separator-primary);vertical-align:top;padding:7px 10px}.mL8Uca_table tbody tr:last-child td{border-bottom:none}.mL8Uca_table tbody tr:hover td{background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.mL8Uca_cellMuted{color:var(--dsw-alias-label-tertiary)}.mL8Uca_actions{white-space:nowrap;align-items:center;gap:8px;display:flex}.mL8Uca_inlineTest{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:11.5px;display:inline-flex}.mL8Uca_inlineTest[data-status=ok]{color:var(--dsw-alias-state-success-primary)}.mL8Uca_inlineTest[data-status=fail]{color:var(--dsw-alias-state-error-primary)}.mL8Uca_badge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;line-height:1.6;display:inline-block}.mL8Uca_badge[data-kind=key]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_badge[data-kind=password]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_badge[data-status=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_badge[data-status=fail]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_badge[data-status=timeout]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:600}.mL8Uca_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}.mL8Uca_primaryButton:disabled{opacity:.5;cursor:default}.mL8Uca_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}.mL8Uca_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_ghostButton:disabled{opacity:.45;cursor:default}.mL8Uca_iconButton{width:26px;height:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;font-size:13px;display:inline-flex}.mL8Uca_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.mL8Uca_linkButton{color:var(--dsw-alias-state-business-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0;font-size:12px}.mL8Uca_linkButton:hover:not(:disabled){text-decoration:underline}.mL8Uca_linkButton:disabled{opacity:.45;cursor:default}.mL8Uca_linkButton[data-danger]{color:var(--dsw-alias-state-error-primary)}.mL8Uca_spinner{border:2px solid var(--dsw-alias-state-business-primary);vertical-align:-1px;border-top-color:#0000;border-radius:50%;flex:none;width:11px;height:11px;animation:.8s linear infinite mL8Uca_dshSshSpin;display:inline-block}@keyframes mL8Uca_dshSshSpin{to{transform:rotate(360deg)}}.mL8Uca_banner{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;border-radius:8px;padding:8px 12px;font-size:12.5px;line-height:1.5}.mL8Uca_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_banner[data-kind=error]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_banner[data-kind=info]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_empty,.mL8Uca_loading{text-align:center;color:var(--dsw-alias-label-tertiary);padding:28px 12px;font-size:12.5px}.mL8Uca_modalBackdrop{z-index:40;background:var(--dsw-alias-bg-mask-1);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.mL8Uca_modal{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);width:min(560px,100vw - 48px);max-height:calc(100vh - 96px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:14px;flex-direction:column;gap:12px;padding:18px;display:flex;overflow-y:auto}.mL8Uca_modalTitle{margin:0;font-size:15px;font-weight:700}.mL8Uca_modalFooter{justify-content:flex-end;gap:10px;margin-top:4px;display:flex}.mL8Uca_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}.mL8Uca_hint{color:var(--dsw-alias-label-tertiary);font-size:11.5px}.mL8Uca_field{flex-direction:column;gap:5px;display:flex}.mL8Uca_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.mL8Uca_input{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);resize:vertical;border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px}.mL8Uca_input:focus{border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_input::placeholder{color:var(--dsw-alias-label-tertiary)}.mL8Uca_input:disabled{opacity:.55}.mL8Uca_formRow{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px 12px;display:grid}.mL8Uca_radioRow{align-items:center;gap:16px;display:flex}.mL8Uca_radioLabel{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:6px;font-size:13px;display:inline-flex}.mL8Uca_termBody{flex-direction:column;flex:1;gap:8px;min-height:0;display:flex;overflow:hidden}.mL8Uca_termWrap{border:1px solid var(--dsw-alias-border-l1);background:#0b0e14;border-radius:10px;flex:1;min-height:0;position:relative;overflow:hidden}.mL8Uca_termContainer{padding:8px 10px;position:absolute;inset:0}.mL8Uca_termPlaceholder{z-index:2;text-align:center;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-base);justify-content:center;align-items:center;padding:0 24px;font-size:12.5px;display:flex;position:absolute;inset:0}.mL8Uca_hiddenFile{display:none}.mL8Uca_browsePanel{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;display:flex;overflow:hidden}.mL8Uca_browseHeader{background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;gap:8px;padding:8px 10px;display:flex}.mL8Uca_browsePath{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;overflow:hidden}.mL8Uca_browseList{max-height:240px;overflow-y:auto}.mL8Uca_dirRow{text-align:left;width:100%;font:inherit;color:var(--dsw-alias-label-primary);border:none;border-bottom:1px solid var(--dsw-alias-separator-primary);cursor:pointer;background:0 0;grid-template-columns:minmax(0,1fr) auto auto;gap:12px;padding:5px 10px;font-size:12.5px;display:grid}.mL8Uca_dirRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_dirRow[data-type=dir] .mL8Uca_dirName{color:var(--dsw-alias-state-business-primary)}.mL8Uca_dirRow[data-up]{color:var(--dsw-alias-label-secondary)}.mL8Uca_dirName{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden}.mL8Uca_dirType{color:var(--dsw-alias-label-tertiary);white-space:nowrap}.mL8Uca_dirSize{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-align:right}.mL8Uca_transferBlock{flex-direction:column;flex:none;gap:6px;display:flex}.mL8Uca_progressMeta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:12px;font-size:12px;display:flex}.mL8Uca_progressTrack{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;height:8px;overflow:hidden}.mL8Uca_progressBar{background:var(--dsw-alias-state-business-primary);border-radius:999px;height:100%;transition:width .12s linear}.mL8Uca_tunnelList{flex-direction:column;flex:none;gap:8px;display:flex}.mL8Uca_tunnelRow{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;align-items:center;gap:10px;padding:8px 12px;display:flex}.mL8Uca_tunnelRow[data-state=forwarding]{border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_tunnelRow[data-state=connecting]{border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_tunnelRow[data-state=failed]{border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_tunnelLabel{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;overflow:hidden}.mL8Uca_formCard{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;gap:10px;padding:12px;display:flex}.mL8Uca_clusterForm{flex-direction:column;flex:none;gap:8px;display:flex}.mL8Uca_clusterFilters{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;display:grid}.mL8Uca_commandInput{min-height:96px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.mL8Uca_cellDetails summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px}.mL8Uca_cellPre{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;border-radius:6px;max-height:180px;margin:4px 0 0;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;overflow:auto}";
599
599
  const tagId = "@linxin666/dsh-ssh/panel.module.css";
600
600
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
601
601
  const tag = document.createElement("style");
@@ -605,76 +605,76 @@ window.__ModuleLoader__.load({
605
605
  document.head.appendChild(tag);
606
606
  }
607
607
  var panel_module_css_default = {
608
- "actions": "_X8bTW_actions",
609
- "badge": "_X8bTW_badge",
610
- "banner": "_X8bTW_banner",
611
- "browseHeader": "_X8bTW_browseHeader",
612
- "browseList": "_X8bTW_browseList",
613
- "browsePanel": "_X8bTW_browsePanel",
614
- "browsePath": "_X8bTW_browsePath",
615
- "cellDetails": "_X8bTW_cellDetails",
616
- "cellMuted": "_X8bTW_cellMuted",
617
- "cellPre": "_X8bTW_cellPre",
618
- "clusterFilters": "_X8bTW_clusterFilters",
619
- "clusterForm": "_X8bTW_clusterForm",
620
- "commandInput": "_X8bTW_commandInput",
621
- "controls": "_X8bTW_controls",
622
- "dirName": "_X8bTW_dirName",
623
- "dirRow": "_X8bTW_dirRow",
624
- "dirSize": "_X8bTW_dirSize",
625
- "dirType": "_X8bTW_dirType",
626
- "dshSshSpin": "_X8bTW_dshSshSpin",
627
- "empty": "_X8bTW_empty",
628
- "entry": "_X8bTW_entry",
629
- "entryIcon": "_X8bTW_entryIcon",
630
- "entryLabel": "_X8bTW_entryLabel",
631
- "field": "_X8bTW_field",
632
- "fieldLabel": "_X8bTW_fieldLabel",
633
- "fillBody": "_X8bTW_fillBody",
634
- "formCard": "_X8bTW_formCard",
635
- "formError": "_X8bTW_formError",
636
- "formRow": "_X8bTW_formRow",
637
- "ghostButton": "_X8bTW_ghostButton",
638
- "hiddenFile": "_X8bTW_hiddenFile",
639
- "hint": "_X8bTW_hint",
640
- "iconButton": "_X8bTW_iconButton",
641
- "inlineTest": "_X8bTW_inlineTest",
642
- "input": "_X8bTW_input",
643
- "linkButton": "_X8bTW_linkButton",
644
- "loading": "_X8bTW_loading",
645
- "modal": "_X8bTW_modal",
646
- "modalBackdrop": "_X8bTW_modalBackdrop",
647
- "modalFooter": "_X8bTW_modalFooter",
648
- "modalTitle": "_X8bTW_modalTitle",
649
- "mono": "_X8bTW_mono",
650
- "panel": "_X8bTW_panel",
651
- "panelContent": "_X8bTW_panelContent",
652
- "panelHeader": "_X8bTW_panelHeader",
653
- "panelTitle": "_X8bTW_panelTitle",
654
- "primaryButton": "_X8bTW_primaryButton",
655
- "progressBar": "_X8bTW_progressBar",
656
- "progressMeta": "_X8bTW_progressMeta",
657
- "progressTrack": "_X8bTW_progressTrack",
658
- "radioLabel": "_X8bTW_radioLabel",
659
- "radioRow": "_X8bTW_radioRow",
660
- "search": "_X8bTW_search",
661
- "spinner": "_X8bTW_spinner",
662
- "tab": "_X8bTW_tab",
663
- "tabBar": "_X8bTW_tabBar",
664
- "tabBody": "_X8bTW_tabBody",
665
- "table": "_X8bTW_table",
666
- "tableWrap": "_X8bTW_tableWrap",
667
- "termBody": "_X8bTW_termBody",
668
- "termContainer": "_X8bTW_termContainer",
669
- "termPlaceholder": "_X8bTW_termPlaceholder",
670
- "termWrap": "_X8bTW_termWrap",
671
- "toolbar": "_X8bTW_toolbar",
672
- "toolbarSpacer": "_X8bTW_toolbarSpacer",
673
- "transferBlock": "_X8bTW_transferBlock",
674
- "tunnelLabel": "_X8bTW_tunnelLabel",
675
- "tunnelList": "_X8bTW_tunnelList",
676
- "tunnelRow": "_X8bTW_tunnelRow",
677
- "view": "_X8bTW_view"
608
+ "actions": "mL8Uca_actions",
609
+ "badge": "mL8Uca_badge",
610
+ "banner": "mL8Uca_banner",
611
+ "browseHeader": "mL8Uca_browseHeader",
612
+ "browseList": "mL8Uca_browseList",
613
+ "browsePanel": "mL8Uca_browsePanel",
614
+ "browsePath": "mL8Uca_browsePath",
615
+ "cellDetails": "mL8Uca_cellDetails",
616
+ "cellMuted": "mL8Uca_cellMuted",
617
+ "cellPre": "mL8Uca_cellPre",
618
+ "clusterFilters": "mL8Uca_clusterFilters",
619
+ "clusterForm": "mL8Uca_clusterForm",
620
+ "commandInput": "mL8Uca_commandInput",
621
+ "controls": "mL8Uca_controls",
622
+ "dirName": "mL8Uca_dirName",
623
+ "dirRow": "mL8Uca_dirRow",
624
+ "dirSize": "mL8Uca_dirSize",
625
+ "dirType": "mL8Uca_dirType",
626
+ "dshSshSpin": "mL8Uca_dshSshSpin",
627
+ "empty": "mL8Uca_empty",
628
+ "entry": "mL8Uca_entry",
629
+ "entryIcon": "mL8Uca_entryIcon",
630
+ "entryLabel": "mL8Uca_entryLabel",
631
+ "field": "mL8Uca_field",
632
+ "fieldLabel": "mL8Uca_fieldLabel",
633
+ "fillBody": "mL8Uca_fillBody",
634
+ "formCard": "mL8Uca_formCard",
635
+ "formError": "mL8Uca_formError",
636
+ "formRow": "mL8Uca_formRow",
637
+ "ghostButton": "mL8Uca_ghostButton",
638
+ "hiddenFile": "mL8Uca_hiddenFile",
639
+ "hint": "mL8Uca_hint",
640
+ "iconButton": "mL8Uca_iconButton",
641
+ "inlineTest": "mL8Uca_inlineTest",
642
+ "input": "mL8Uca_input",
643
+ "linkButton": "mL8Uca_linkButton",
644
+ "loading": "mL8Uca_loading",
645
+ "modal": "mL8Uca_modal",
646
+ "modalBackdrop": "mL8Uca_modalBackdrop",
647
+ "modalFooter": "mL8Uca_modalFooter",
648
+ "modalTitle": "mL8Uca_modalTitle",
649
+ "mono": "mL8Uca_mono",
650
+ "panel": "mL8Uca_panel",
651
+ "panelContent": "mL8Uca_panelContent",
652
+ "panelHeader": "mL8Uca_panelHeader",
653
+ "panelTitle": "mL8Uca_panelTitle",
654
+ "primaryButton": "mL8Uca_primaryButton",
655
+ "progressBar": "mL8Uca_progressBar",
656
+ "progressMeta": "mL8Uca_progressMeta",
657
+ "progressTrack": "mL8Uca_progressTrack",
658
+ "radioLabel": "mL8Uca_radioLabel",
659
+ "radioRow": "mL8Uca_radioRow",
660
+ "search": "mL8Uca_search",
661
+ "spinner": "mL8Uca_spinner",
662
+ "tab": "mL8Uca_tab",
663
+ "tabBar": "mL8Uca_tabBar",
664
+ "tabBody": "mL8Uca_tabBody",
665
+ "table": "mL8Uca_table",
666
+ "tableWrap": "mL8Uca_tableWrap",
667
+ "termBody": "mL8Uca_termBody",
668
+ "termContainer": "mL8Uca_termContainer",
669
+ "termPlaceholder": "mL8Uca_termPlaceholder",
670
+ "termWrap": "mL8Uca_termWrap",
671
+ "toolbar": "mL8Uca_toolbar",
672
+ "toolbarSpacer": "mL8Uca_toolbarSpacer",
673
+ "transferBlock": "mL8Uca_transferBlock",
674
+ "tunnelLabel": "mL8Uca_tunnelLabel",
675
+ "tunnelList": "mL8Uca_tunnelList",
676
+ "tunnelRow": "mL8Uca_tunnelRow",
677
+ "view": "mL8Uca_view"
678
678
  };
679
679
  //#endregion
680
680
  //#region src/client/panel/ClusterTab.tsx
package/lib/index.js CHANGED
@@ -1741,8 +1741,7 @@ var SshEngine = class {
1741
1741
  if (!remotePath.startsWith("/")) throw new Error(`remotePath must be an absolute path (got '${remotePath}')`);
1742
1742
  const local = resolve(localPath);
1743
1743
  if (!existsSync(local)) throw new Error(`local path not found: '${localPath}'`);
1744
- return this.withClient(alias, async (client) => {
1745
- const sftp = await this.sftp(client);
1744
+ return this.withClient(alias, (client) => this.withSftp(client, async (sftp) => {
1746
1745
  const stat = statSync(local);
1747
1746
  let files;
1748
1747
  if (stat.isDirectory()) {
@@ -1765,12 +1764,11 @@ var SshEngine = class {
1765
1764
  bytes,
1766
1765
  files: files.length
1767
1766
  };
1768
- });
1767
+ }));
1769
1768
  }
1770
1769
  /** Download one remote file to a local path. */
1771
1770
  async download(alias, remotePath, localPath, onProgress) {
1772
- return this.withClient(alias, async (client) => {
1773
- const sftp = await this.sftp(client);
1771
+ return this.withClient(alias, (client) => this.withSftp(client, async (sftp) => {
1774
1772
  if ((await new Promise((resolve, reject) => {
1775
1773
  sftp.stat(remotePath, (error, stats) => error !== void 0 ? reject(error) : resolve(stats));
1776
1774
  })).isDirectory()) throw new Error(`'${remotePath}' is a directory — directory download is not supported yet (download individual files)`);
@@ -1778,12 +1776,11 @@ var SshEngine = class {
1778
1776
  if (!existsSync(dirname(local))) mkdirSync(dirname(local), { recursive: true });
1779
1777
  await this.fastGet(sftp, remotePath, local, onProgress);
1780
1778
  return { bytes: statSync(local).size };
1781
- });
1779
+ }));
1782
1780
  }
1783
1781
  /** List a remote directory (file browser). */
1784
1782
  async ls(alias, path) {
1785
- return this.withClient(alias, async (client) => {
1786
- const sftp = await this.sftp(client);
1783
+ return this.withClient(alias, (client) => this.withSftp(client, async (sftp) => {
1787
1784
  return await new Promise((resolve, reject) => {
1788
1785
  sftp.readdir(path, (error, list) => {
1789
1786
  if (error !== void 0) {
@@ -1799,7 +1796,30 @@ var SshEngine = class {
1799
1796
  })));
1800
1797
  });
1801
1798
  });
1802
- });
1799
+ }));
1800
+ }
1801
+ /**
1802
+ * Open one SFTP channel, run the operation, and release the channel exactly
1803
+ * once when the operation settles (success or error). ssh2 keeps each
1804
+ * subsystem channel open until end(); without this, every transfer leaks a
1805
+ * channel until sshd's MaxSessions cap makes all later opens fail.
1806
+ */
1807
+ async withSftp(client, run) {
1808
+ const sftp = await this.sftp(client);
1809
+ let ended = false;
1810
+ const endOnce = () => {
1811
+ if (ended) return;
1812
+ ended = true;
1813
+ try {
1814
+ sftp.end();
1815
+ } catch {}
1816
+ };
1817
+ sftp.once("close", endOnce);
1818
+ try {
1819
+ return await run(sftp);
1820
+ } finally {
1821
+ endOnce();
1822
+ }
1803
1823
  }
1804
1824
  sftp(client) {
1805
1825
  return new Promise((resolve, reject) => {
@@ -111,6 +111,13 @@ export declare class SshEngine {
111
111
  }>;
112
112
  /** List a remote directory (file browser). */
113
113
  ls(alias: string, path: string): Promise<import('./protocol.ts').RemoteDirEntry[]>;
114
+ /**
115
+ * Open one SFTP channel, run the operation, and release the channel exactly
116
+ * once when the operation settles (success or error). ssh2 keeps each
117
+ * subsystem channel open until end(); without this, every transfer leaks a
118
+ * channel until sshd's MaxSessions cap makes all later opens fail.
119
+ */
120
+ private withSftp;
114
121
  private sftp;
115
122
  /** Create a remote directory chain (stat-then-mkdir per segment). */
116
123
  private ensureRemoteDir;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-ssh",
3
3
  "description": "Remote SSH operations for the dsh web GUI: host config store (~/.dsh/dsh-ssh.json, import from ~/.ssh/config), a persistent ssh2 connection pool with jump-host support, exec / PTY web terminal / SFTP transfer / local port-forward tunnels / cluster execution, plus agent tools (ssh_list, ssh_exec, ssh_upload, ssh_download, ssh_tunnel, ssh_cluster). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
4
- "version": "0.1.12",
4
+ "version": "0.1.13",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -44,16 +44,6 @@
44
44
  "ws": "^8.18.0"
45
45
  },
46
46
  "peerDependencies": {
47
- "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
48
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
49
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
50
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
51
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.6",
52
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
53
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
54
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
55
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
56
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
57
47
  "react": "^18.2.0",
58
48
  "react-dom": "^18.2.0"
59
49
  },
package/src/engine.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  import { createServer, type Server as NetServer } from 'node:net'
9
9
  import { existsSync, mkdirSync, readFileSync, statSync, readdirSync } from 'node:fs'
10
10
  import { dirname, join, relative, resolve as resolvePath } from 'node:path'
11
- import { Client, type ClientChannel, type ConnectConfig } from 'ssh2'
11
+ import { Client, type ClientChannel, type ConnectConfig, type SFTPWrapper } from 'ssh2'
12
12
  import type { ClusterResult, ExecResult, SshHostEntry, SshHostSummary, TestResult, TransferProgress, TunnelInfo } from './protocol.ts'
13
13
  import { expandHome, type HostStore } from './store.ts'
14
14
 
@@ -490,8 +490,7 @@ export class SshEngine {
490
490
  }
491
491
  const local = resolvePath(localPath)
492
492
  if (!existsSync(local)) throw new Error(`local path not found: '${localPath}'`)
493
- return this.withClient(alias, async (client) => {
494
- const sftp = await this.sftp(client)
493
+ return this.withClient(alias, (client) => this.withSftp(client, async (sftp) => {
495
494
  const stat = statSync(local)
496
495
  let files: string[]
497
496
  if (stat.isDirectory()) {
@@ -512,13 +511,12 @@ export class SshEngine {
512
511
  bytes += statSync(src).size
513
512
  }
514
513
  return { bytes, files: files.length }
515
- })
514
+ }))
516
515
  }
517
516
 
518
517
  /** Download one remote file to a local path. */
519
518
  async download(alias: string, remotePath: string, localPath: string, onProgress?: (progress: TransferProgress) => void): Promise<{ bytes: number }> {
520
- return this.withClient(alias, async (client) => {
521
- const sftp = await this.sftp(client)
519
+ return this.withClient(alias, (client) => this.withSftp(client, async (sftp) => {
522
520
  const stat = await new Promise<{ isDirectory: () => boolean }>((resolve, reject) => {
523
521
  sftp.stat(remotePath, (error, stats) => error !== undefined ? reject(error) : resolve(stats))
524
522
  })
@@ -529,13 +527,12 @@ export class SshEngine {
529
527
  if (!existsSync(dirname(local))) mkdirSync(dirname(local), { recursive: true })
530
528
  await this.fastGet(sftp, remotePath, local, onProgress)
531
529
  return { bytes: statSync(local).size }
532
- })
530
+ }))
533
531
  }
534
532
 
535
533
  /** List a remote directory (file browser). */
536
534
  async ls(alias: string, path: string): Promise<import('./protocol.ts').RemoteDirEntry[]> {
537
- return this.withClient(alias, async (client) => {
538
- const sftp = await this.sftp(client)
535
+ return this.withClient(alias, (client) => this.withSftp(client, async (sftp) => {
539
536
  return await new Promise((resolve, reject) => {
540
537
  sftp.readdir(path, (error, list) => {
541
538
  if (error !== undefined) {
@@ -552,17 +549,41 @@ export class SshEngine {
552
549
  resolve(entries)
553
550
  })
554
551
  })
555
- })
552
+ }))
553
+ }
554
+
555
+ /**
556
+ * Open one SFTP channel, run the operation, and release the channel exactly
557
+ * once when the operation settles (success or error). ssh2 keeps each
558
+ * subsystem channel open until end(); without this, every transfer leaks a
559
+ * channel until sshd's MaxSessions cap makes all later opens fail.
560
+ */
561
+ private async withSftp<T>(client: Client, run: (sftp: SFTPWrapper) => Promise<T>): Promise<T> {
562
+ const sftp = await this.sftp(client)
563
+ let ended = false
564
+ const endOnce = (): void => {
565
+ if (ended) return
566
+ ended = true
567
+ try { sftp.end() } catch { /* channel already closed */ }
568
+ }
569
+ // The channel can also close underneath us (peer reset, timeout); the
570
+ // guard makes the finally below a no-op instead of ending it twice.
571
+ sftp.once('close', endOnce)
572
+ try {
573
+ return await run(sftp)
574
+ } finally {
575
+ endOnce()
576
+ }
556
577
  }
557
578
 
558
- private sftp(client: Client): Promise<import('ssh2').SFTPWrapper> {
579
+ private sftp(client: Client): Promise<SFTPWrapper> {
559
580
  return new Promise((resolve, reject) => {
560
581
  client.sftp((error, sftp) => error !== undefined ? reject(error) : resolve(sftp))
561
582
  })
562
583
  }
563
584
 
564
585
  /** Create a remote directory chain (stat-then-mkdir per segment). */
565
- private ensureRemoteDir(sftp: import('ssh2').SFTPWrapper, remote: string): Promise<void> {
586
+ private ensureRemoteDir(sftp: SFTPWrapper, remote: string): Promise<void> {
566
587
  return new Promise((resolve, reject) => {
567
588
  const segments = remote.replace(/^\/+/, '').split('/').filter(segment => segment !== '')
568
589
  const walk = (index: number): void => {
@@ -591,7 +612,7 @@ export class SshEngine {
591
612
  })
592
613
  }
593
614
 
594
- private fastPut(sftp: import('ssh2').SFTPWrapper, src: string, dst: string, onProgress?: (progress: TransferProgress) => void): Promise<void> {
615
+ private fastPut(sftp: SFTPWrapper, src: string, dst: string, onProgress?: (progress: TransferProgress) => void): Promise<void> {
595
616
  return new Promise((resolve, reject) => {
596
617
  let last = 0
597
618
  let lastEmit = 0
@@ -625,7 +646,7 @@ export class SshEngine {
625
646
  })
626
647
  }
627
648
 
628
- private fastGet(sftp: import('ssh2').SFTPWrapper, src: string, dst: string, onProgress?: (progress: TransferProgress) => void): Promise<void> {
649
+ private fastGet(sftp: SFTPWrapper, src: string, dst: string, onProgress?: (progress: TransferProgress) => void): Promise<void> {
629
650
  return new Promise((resolve, reject) => {
630
651
  let last = 0
631
652
  let lastEmit = 0