@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.
- package/README.i18n.yaml +6 -0
- package/README.md +46 -52
- package/README.zh.md +72 -0
- package/lib/client.js +72 -72
- package/lib/index.js +29 -9
- package/lib/types/engine.d.ts +7 -0
- package/package.json +1 -11
- package/src/engine.ts +35 -14
package/README.i18n.yaml
ADDED
|
@@ -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 —
|
|
1
|
+
# dsh-ssh — Remote SSH operations plugin for DSH
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
| Web
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
| Agent
|
|
21
|
-
|
|
22
|
-
##
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
61
|
-
-
|
|
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 #
|
|
68
|
-
pnpm --filter @linxin666/dsh-ssh build # tsc
|
|
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
|
-
-
|
|
75
|
-
- exec
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
- Agent
|
|
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
|
|
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": "
|
|
609
|
-
"badge": "
|
|
610
|
-
"banner": "
|
|
611
|
-
"browseHeader": "
|
|
612
|
-
"browseList": "
|
|
613
|
-
"browsePanel": "
|
|
614
|
-
"browsePath": "
|
|
615
|
-
"cellDetails": "
|
|
616
|
-
"cellMuted": "
|
|
617
|
-
"cellPre": "
|
|
618
|
-
"clusterFilters": "
|
|
619
|
-
"clusterForm": "
|
|
620
|
-
"commandInput": "
|
|
621
|
-
"controls": "
|
|
622
|
-
"dirName": "
|
|
623
|
-
"dirRow": "
|
|
624
|
-
"dirSize": "
|
|
625
|
-
"dirType": "
|
|
626
|
-
"dshSshSpin": "
|
|
627
|
-
"empty": "
|
|
628
|
-
"entry": "
|
|
629
|
-
"entryIcon": "
|
|
630
|
-
"entryLabel": "
|
|
631
|
-
"field": "
|
|
632
|
-
"fieldLabel": "
|
|
633
|
-
"fillBody": "
|
|
634
|
-
"formCard": "
|
|
635
|
-
"formError": "
|
|
636
|
-
"formRow": "
|
|
637
|
-
"ghostButton": "
|
|
638
|
-
"hiddenFile": "
|
|
639
|
-
"hint": "
|
|
640
|
-
"iconButton": "
|
|
641
|
-
"inlineTest": "
|
|
642
|
-
"input": "
|
|
643
|
-
"linkButton": "
|
|
644
|
-
"loading": "
|
|
645
|
-
"modal": "
|
|
646
|
-
"modalBackdrop": "
|
|
647
|
-
"modalFooter": "
|
|
648
|
-
"modalTitle": "
|
|
649
|
-
"mono": "
|
|
650
|
-
"panel": "
|
|
651
|
-
"panelContent": "
|
|
652
|
-
"panelHeader": "
|
|
653
|
-
"panelTitle": "
|
|
654
|
-
"primaryButton": "
|
|
655
|
-
"progressBar": "
|
|
656
|
-
"progressMeta": "
|
|
657
|
-
"progressTrack": "
|
|
658
|
-
"radioLabel": "
|
|
659
|
-
"radioRow": "
|
|
660
|
-
"search": "
|
|
661
|
-
"spinner": "
|
|
662
|
-
"tab": "
|
|
663
|
-
"tabBar": "
|
|
664
|
-
"tabBody": "
|
|
665
|
-
"table": "
|
|
666
|
-
"tableWrap": "
|
|
667
|
-
"termBody": "
|
|
668
|
-
"termContainer": "
|
|
669
|
-
"termPlaceholder": "
|
|
670
|
-
"termWrap": "
|
|
671
|
-
"toolbar": "
|
|
672
|
-
"toolbarSpacer": "
|
|
673
|
-
"transferBlock": "
|
|
674
|
-
"tunnelLabel": "
|
|
675
|
-
"tunnelList": "
|
|
676
|
-
"tunnelRow": "
|
|
677
|
-
"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 (
|
|
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 (
|
|
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 (
|
|
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) => {
|
package/lib/types/engine.d.ts
CHANGED
|
@@ -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.
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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<
|
|
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:
|
|
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:
|
|
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:
|
|
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
|