@linxin666/dsh-ssh 0.1.11 → 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/LICENSE +202 -0
- 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 +2 -12
- package/src/client/panel/panel.module.css +10 -2
- package/src/engine.ts +35 -14
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
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;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]){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
|
},
|
|
@@ -93,7 +83,7 @@
|
|
|
93
83
|
"cordis.patch.yml",
|
|
94
84
|
"README.md"
|
|
95
85
|
],
|
|
96
|
-
"license": "
|
|
86
|
+
"license": "Apache-2.0",
|
|
97
87
|
"repository": {
|
|
98
88
|
"type": "git",
|
|
99
89
|
"url": "https://github.com/zhu1090093659/dsh-web-ui.git"
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
panel and its modals paint over the input card, mirroring the task board
|
|
23
23
|
(issue #87). */
|
|
24
24
|
z-index: 60;
|
|
25
|
+
/* Opaque backdrop: the conversation subtree stays mounted under the panel.
|
|
26
|
+
Even if the hide rule below were defeated by a stronger host inline
|
|
27
|
+
style, this background keeps sticky code-block banners (host z-index 6)
|
|
28
|
+
from showing through the active panel (issue #100). */
|
|
29
|
+
background: var(--dsw-alias-bg-base);
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
/* The center column is single-occupant; the :not() guards keep the two
|
|
@@ -32,8 +37,11 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-dsh-ssh-view] {
|
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
/* While the panel is active, the conversation content underneath is hidden
|
|
35
|
-
(it stays mounted and stateful).
|
|
36
|
-
|
|
40
|
+
(it stays mounted and stateful). The rule targets both the pane attribute
|
|
41
|
+
and the center-column class so the chat subtree (including the code-block
|
|
42
|
+
copy banner, issue #100) stays hidden whichever host container carries it. */
|
|
43
|
+
html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-pane='conversation'] > :not([data-dsh-ssh-view]),
|
|
44
|
+
html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol'] > :not([data-dsh-ssh-view]) {
|
|
37
45
|
display: none !important;
|
|
38
46
|
}
|
|
39
47
|
|
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
|