dsh-remote 0.8.35 → 0.8.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +76 -13
- package/README.md +65 -13
- package/lib/client.js +238 -0
- package/lib/http-transport.js +93 -1
- package/lib/index.js +385 -1
- package/lib/pool.js +9 -1
- package/lib/update.js +39 -3
- package/lib/web-attach.js +467 -0
- package/lib/web-deploy-skill.js +205 -0
- package/lib/web-deploy.js +652 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -20,21 +20,13 @@ Manage several SSH machines, then pick a **remote workspace** (or a **local** on
|
|
|
20
20
|
|
|
21
21
|
The harness Web UI intentionally binds `127.0.0.1` (the CLI rejects `--host 0.0.0.0` for safety). This plugin goes the other way: **you connect out** to the machines you maintain, pick a workspace, and work in it through the normal DSH workspace + agent fs flows — no changes to `dsh-workspace` or the harness core.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
One anonymous heartbeat per launch (at least 6 hours apart), used only to measure usage: **de-duplicated daily active installs, the version actually running, and platform distribution**. npm download counts are release-driven and include mirrors/crawlers, and GitHub clones include CI, so neither can answer that.
|
|
26
|
-
|
|
27
|
-
Exactly five fields are sent: `idHash` (a pseudonym, `HMAC-SHA256('dsh-remote/telemetry/v1', installId)`), `version`, `platform`, `arch`, `node`. **Not sent:** hostnames, usernames, paths, IPs, SSH hosts/ports/keys, your machine list, session content. The raw `installId` (`<DSH_HOME>/.dsh-remote-install-id`) never leaves your machine — only its HMAC is transmitted.
|
|
28
|
-
|
|
29
|
-
The heartbeat is fire-and-forget: it never blocks loading and failures are ignored.
|
|
30
|
-
|
|
31
|
-
[live usage stats](https://flymysql.github.io/dsh-remote/stats/) · [plugin homepage](https://flymysql.github.io/dsh-remote/)
|
|
23
|
+
It works the other way too: **you can open the remote machine's own DSH Web UI as a page in your local browser** — again without exposing any port on the remote. And when a connection will not come up, a **one-click health check & deploy** repairs and verifies the remote environment for you.
|
|
32
24
|
|
|
33
25
|
## Screen previews
|
|
34
26
|
|
|
35
27
|
**Settings → 远程工作区** — machine list, advanced config (key / jump host / agent), connection check, port forwarding, audit log, update:
|
|
36
28
|
|
|
37
|
-
<img src="docs/shots/settings-panel.png" alt="dsh-remote settings: machine list, advanced config, port forwarding, audit log, update" width="
|
|
29
|
+
<img src="docs/shots/settings-panel.png" alt="dsh-remote settings: machine list, advanced config, remote DSH Web UI, health check & deploy, port forwarding, audit log, update" width="612"/>
|
|
38
30
|
|
|
39
31
|
The native **"Add workspace"** flow — a centered modal with two tabs, opening on Local; here switched to **Remote**:
|
|
40
32
|
|
|
@@ -47,7 +39,7 @@ The native **"Add workspace"** flow — a centered modal with two tabs, opening
|
|
|
47
39
|
|
|
48
40
|
## Features
|
|
49
41
|
|
|
50
|
-

|
|
51
43
|
|
|
52
44
|
The image above is the overview. What follows is only what the image does not make obvious.
|
|
53
45
|
|
|
@@ -60,11 +52,13 @@ The image above is the overview. What follows is only what the image does not ma
|
|
|
60
52
|
|
|
61
53
|
**Settings** (machine list, connection check, port forwarding, audit log, update mode):
|
|
62
54
|
|
|
63
|
-
<img src="docs/shots/settings-panel.png" alt="dsh-remote settings: machine list, advanced config, port forwarding, audit log, update" width="
|
|
55
|
+
<img src="docs/shots/settings-panel.png" alt="dsh-remote settings: machine list, advanced config, remote DSH Web UI, health check & deploy, port forwarding, audit log, update" width="612"/>
|
|
64
56
|
|
|
65
57
|
The rest:
|
|
66
58
|
|
|
67
|
-
- **
|
|
59
|
+
- **21 model tools** (listed so they can be copied or searched): `rw_info`, `rw_connect`, `rw_pick_workspace`, `rw_list_dir`, `rw_stat`, `rw_read_file`, `rw_write_file`, `rw_edit`, `rw_append`, `rw_mkdir`, `rw_remove`, `rw_move`, `rw_exec`, `rw_search`, `rw_download`, `rw_upload`, `rw_sync`, `rw_push`, `rw_forward`, `rw_disconnect`, `rw_deploy_probe`.
|
|
60
|
+
- **Open a remote machine's DSH Web UI locally** (`0.8.36+`) — nothing has to be exposed on the remote: the plugin SSHes out, starts a `dsh web` there that binds **`127.0.0.1` only**, and tunnels its port back to a loopback port on this machine. The URL carries a one-time login token, consumed once by a redirect.
|
|
61
|
+
- **One-click health check & deploy** (`0.8.36+`) — **a failed connection is usually the remote dsh version or environment, not the plugin.** The read-only check reports platform / node / npm / dsh version / **whether the native module can load** / proxy and npm registry, with a suggested fix; "Deploy & verify" installs into a remote **private directory** (no system directory, no PATH change, no replacing the version you use — deleting it is a complete rollback) and verifies step by step. If that still fails, the built-in `dsh-remote-deploy` skill takes over.
|
|
68
62
|
- **Cross-platform remotes** — all file access is SFTP-protocol-level (no POSIX shell), so Linux/macOS/Windows remotes all work.
|
|
69
63
|
- **Windows remotes** — the platform is auto-detected and commands go through `bash -s` over stdin, so quoting and backslash escaping are never an issue (`config.shell` can pin a path or `native` disables wrapping); `C:\Users\dev` and `/c/Users/dev` are both accepted.
|
|
70
64
|
- **Async long tasks** — `rw_sync`/`rw_push` with `async: true` return a `taskId` with progress/result/cancel.
|
|
@@ -115,6 +109,58 @@ Since **v0.8.18** it installs and mounts only itself; the Web sidebar ([dsh-bett
|
|
|
115
109
|
|
|
116
110
|
> **Remote context is session-scoped:** the "Remote workspace" system-prompt section appears only when the current session's workspace is a remote mirror; local sessions are unaffected and the model will not call `rw_*` on its own.
|
|
117
111
|
|
|
112
|
+
## Open a remote machine's DSH Web UI locally
|
|
113
|
+
|
|
114
|
+
The `dsh web` running on a remote machine can also be opened **as a page on this machine** (issue #46). The direction stays "this machine dials out":
|
|
115
|
+
|
|
116
|
+
> **The remote opens no port.** DSH deliberately refuses `--host 0.0.0.0` (it would put remote
|
|
117
|
+
> code execution on the network), so the correct shape is: SSH out, start a `dsh web` on the remote
|
|
118
|
+
> that listens on **127.0.0.1 only**, and carry its port back over the SSH connection to a loopback
|
|
119
|
+
> port here.
|
|
120
|
+
|
|
121
|
+
Usage: Settings → Remote workspaces → **"Remote DSH Web UI"** → pick a machine → **Connect & open**.
|
|
122
|
+
A new tab opens showing that remote machine's full DSH interface (chat, tool tree, settings).
|
|
123
|
+
|
|
124
|
+
- The URL looks like `http://127.0.0.1:3088/?token=…`. That token is a one-time credential the remote
|
|
125
|
+
process prints at startup and is used once, on this machine, by that redirect: DSH exchanges it for a
|
|
126
|
+
30-day signed cookie and lands on a clean `/`. **Do not share that tokenized URL.**
|
|
127
|
+
- "Disconnect" only closes the tunnel and deliberately does **not** kill the remote process (it may be
|
|
128
|
+
an instance you use elsewhere). "Disconnect & stop remote" also ends the process — but only one this
|
|
129
|
+
plugin started.
|
|
130
|
+
- If the remote already runs an interface (for example one you started in a terminal), pick the machine
|
|
131
|
+
and paste its `http://127.0.0.1:<port>/?token=…` into the field, then "Attach to running instance" —
|
|
132
|
+
no second instance will be started.
|
|
133
|
+
- When the remote `dsh` is not on the SSH login PATH, point `webAttachCommand` at its absolute path; to
|
|
134
|
+
keep an attached instance out of the remote user's own sessions and settings, point `webAttachDshHome`
|
|
135
|
+
at a scratch directory.
|
|
136
|
+
|
|
137
|
+
### Cannot connect? Check the environment, then deploy in one click
|
|
138
|
+
|
|
139
|
+
**A failed connect is usually not the plugin's fault: the remote dsh version or environment is wrong.**
|
|
140
|
+
The classic case: dsh 0.1.0-rc.6 depends on `node-pty@1.1.0`, whose published package ships
|
|
141
|
+
**no `linux-x64` prebuild**, so `dsh web` cannot start on Linux at all. Previously all you saw was
|
|
142
|
+
"the remote DSH did not report a startup token", with no way to tell why.
|
|
143
|
+
|
|
144
|
+
Settings → Remote workspaces → **"Remote dsh health check & deploy"**:
|
|
145
|
+
|
|
146
|
+
1. **Check (read-only)** — probes the remote's platform / node / npm / dsh version / **whether the
|
|
147
|
+
native module can load** / whether it knows `--no-open` / proxy and npm registry, and reports a
|
|
148
|
+
verdict with a suggested fix. It **writes nothing**, so it is safe to run freely.
|
|
149
|
+
2. **Deploy & verify** — appears only when step 1 says it is both needed and possible. It installs into
|
|
150
|
+
a **private directory** on the remote (default `~/.dsh-remote/dsh`): **no system directory, no PATH
|
|
151
|
+
change, no replacing the version you use**; deleting that directory is a complete rollback. It then
|
|
152
|
+
verifies step by step (binary / native module / `web` subcommand) and, on success, **records the
|
|
153
|
+
resulting dsh path per machine** so later connects use it.
|
|
154
|
+
3. **Let the AI investigate** — appears only after a failure. It creates a session and hands the problem
|
|
155
|
+
to the built-in `dsh-remote-deploy` skill, which covers the long tail the deterministic path cannot
|
|
156
|
+
express (no npm, sudo-only systems, proxies, internal mirrors, Windows remotes). **Nothing is created
|
|
157
|
+
unless you click it**, because it spends model budget.
|
|
158
|
+
|
|
159
|
+
Related config: `webInstallPrefix` (where; default `$HOME/.dsh-remote/dsh`), `webInstallVersion`
|
|
160
|
+
(which version; default `0.1.5-rc.2`, the first that can start the web surface on Linux), and
|
|
161
|
+
`webInstallRegistry` (npm registry; **empty keeps the remote's own config**, so an internal mirror is
|
|
162
|
+
never overridden).
|
|
163
|
+
|
|
118
164
|
## CLI defaults (optional)
|
|
119
165
|
|
|
120
166
|
Provide a default machine in `cordis.patch.yml`:
|
|
@@ -210,6 +256,13 @@ Deploying to a product profile is a separate, explicit action (`./sync.sh`) for
|
|
|
210
256
|
| `updateMode` | string | `auto` | self-update behaviour: `auto` checks npm on load and every 6h and applies a newer release; `manual` only checks when asked; `off` disables checks. **Default changed to `auto` in 0.8.27** — safe because 0.8.24 added the host-half hot swap |
|
|
211
257
|
| `updateCheckIntervalMs` | int | 21600000 (6h) | how often `auto` mode checks npm (floor 60000) |
|
|
212
258
|
| `updateAutoReload` | bool | `true` | hot-swap the host half after an update lands; `false` defers it to the next process start and the panel reports `pendingReload` |
|
|
259
|
+
| `webAttachPortStart` | int | `3088` | first local port for the "Remote DSH Web UI" tunnel (later ports are tried when taken; loopback only) |
|
|
260
|
+
| `webAttachCommand` | string | `dsh` | command used to start `dsh web` on the remote; change it when the remote `dsh` is not on the SSH login PATH |
|
|
261
|
+
| `webAttachDshHome` | string | `''` | `DSH_HOME` exported on the remote when attaching; empty reuses the remote user's own harness home |
|
|
262
|
+
| `webAttachWaitSeconds` | int | `45` | how long to wait for a freshly started remote `dsh web` to print its launch token |
|
|
263
|
+
| `webInstallPrefix` | string | `''` | where an automatic deployment installs dsh; empty means `$HOME/.dsh-remote/dsh` (no system directory, no PATH change) |
|
|
264
|
+
| `webInstallVersion` | string | `0.1.5-rc.2` | dsh version an automatic deployment installs; the default is the first that can start the web surface on Linux |
|
|
265
|
+
| `webInstallRegistry` | string | `''` | npm registry for the install; **empty keeps the remote's own config**, so an internal mirror is never overridden |
|
|
213
266
|
|
|
214
267
|
> The authoritative list is the `Config` schema in `lib/index.js`; this table mirrors it.
|
|
215
268
|
|
|
@@ -235,6 +288,16 @@ Deploying to a product profile is a separate, explicit action (`./sync.sh`) for
|
|
|
235
288
|
|
|
236
289
|
**The plugin vanished after a DSH upgrade** — DSH's compatibility check dropped the bundle; upgrade to **0.8.29+** (see "DSH version compatibility" above).
|
|
237
290
|
|
|
291
|
+
**"Remote DSH Web UI" will not connect / spins forever** — first check that `dsh web` starts on that
|
|
292
|
+
machine *at all*. Two known traps (the plugin names them in its error):
|
|
293
|
+
1. **dsh 0.1.0-rc.6 cannot start the web surface on Linux** — the `node-pty@1.1.0` it depends on ships
|
|
294
|
+
macOS/Windows prebuilds but **no `linux-x64`**, so it fails with `Failed to load native module:
|
|
295
|
+
pty.node`. Upgrade to 0.1.5-rc.2+ (its `node-pty@1.2.0-beta.15` ships the Linux build).
|
|
296
|
+
2. **dsh 0.1.0-rc.6 does not know `--no-open`** (the flag came later); the plugin drops it and retries
|
|
297
|
+
once automatically, so there is nothing to do.
|
|
298
|
+
If the remote `dsh` is not on the SSH login PATH (a private prefix, say), point `webAttachCommand` at
|
|
299
|
+
its absolute path.
|
|
300
|
+
|
|
238
301
|
## Safety
|
|
239
302
|
|
|
240
303
|
Giving the plugin a machine's credentials lets the agent run **shell commands as your user** on that host — only add machines you trust. Passwords live in a local file (or the OS keychain); treat them as sensitive. With `auditLog` on, every command is recorded.
|
package/README.md
CHANGED
|
@@ -20,21 +20,13 @@
|
|
|
20
20
|
|
|
21
21
|
DSH 的 Web 界面刻意只监听 `127.0.0.1`(CLI 为安全拒绝 `--host 0.0.0.0`)。本插件反过来:**由你主动连出**到你维护的机器,选一个工作区,然后通过 DSH 原生的工作区 + 文件流来工作——**不改动 `dsh-workspace` 核心**。
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
每次启动发送一次匿名心跳(同一安装最少间隔 6 小时),只用于统计用量:**去重日活、实际在跑的版本、平台分布**。npm 下载量由发版驱动、且含镜像与爬虫,GitHub clone 混有 CI,都回答不了这些。
|
|
26
|
-
|
|
27
|
-
只发送 5 个字段:`idHash`(`HMAC-SHA256('dsh-remote/telemetry/v1', installId)` 的伪名)、`version`、`platform`、`arch`、`node`。**不发送**主机名、用户名、路径、IP、SSH 主机/端口/密钥、机器列表、会话内容。原始 `installId`(`<DSH_HOME>/.dsh-remote-install-id`)不离开本机,只传它的 HMAC。
|
|
28
|
-
|
|
29
|
-
心跳是尽力而为的旁路:不阻塞加载,失败静默忽略。
|
|
30
|
-
|
|
31
|
-
[实时用量看板](https://flymysql.github.io/dsh-remote/stats/) · [插件主页](https://flymysql.github.io/dsh-remote/)
|
|
23
|
+
反过来也成立:**你可以把远端那台机器的 DSH 网页界面,当成本机浏览器里的一个页面打开**——同样不需要在远端开放任何端口(详见 [在本地打开远程机器上的 DSH 界面](#在本地打开远程机器上的-dsh-界面))。连不上时还有**一键体检与部署**,它会替你把远端环境修好并验证。
|
|
32
24
|
|
|
33
25
|
## 界面预览
|
|
34
26
|
|
|
35
|
-
**设置 → 远程工作区** —— 多机列表、高级配置(私钥/跳板机/agent
|
|
27
|
+
**设置 → 远程工作区** —— 多机列表、高级配置(私钥/跳板机/agent)、连接体检、远程 DSH 界面、端口转发、审计日志、更新:
|
|
36
28
|
|
|
37
|
-
<img src="docs/shots/settings-panel.png" alt="dsh-remote
|
|
29
|
+
<img src="docs/shots/settings-panel.png" alt="dsh-remote 设置页:机器列表、高级配置、远程 DSH 界面、体检与部署、端口转发、审计日志、更新" width="612"/>
|
|
38
30
|
|
|
39
31
|
原生 **「Add workspace / 选择工作区」** 流程 —— 居中弹窗、两个 tab,默认落在「本机」;切到**「远程」**:
|
|
40
32
|
|
|
@@ -47,13 +39,15 @@ DSH 的 Web 界面刻意只监听 `127.0.0.1`(CLI 为安全拒绝 `--host 0.0.
|
|
|
47
39
|
|
|
48
40
|
## 功能
|
|
49
41
|
|
|
50
|
-

|
|
51
43
|
|
|
52
44
|
上图是能力总览;下面只列**上图没说清、但用起来需要知道**的部分。
|
|
53
45
|
|
|
54
46
|
其余要点:
|
|
55
47
|
|
|
56
|
-
- **
|
|
48
|
+
- **21 个模型工具**(便于复制/检索):`rw_info`、`rw_connect`、`rw_pick_workspace`、`rw_list_dir`、`rw_stat`、`rw_read_file`、`rw_write_file`、`rw_edit`、`rw_append`、`rw_mkdir`、`rw_remove`、`rw_move`、`rw_exec`、`rw_search`、`rw_download`、`rw_upload`、`rw_sync`、`rw_push`、`rw_forward`、`rw_disconnect`、`rw_deploy_probe`。
|
|
49
|
+
- **把远端机器的 DSH 界面挂到本机**(`0.8.36+`)—— 不用在远端开任何端口:插件主动 SSH 连过去,在远端**只监听 `127.0.0.1`** 地起一个 `dsh web`,再把端口经隧道搬回本机的一个 loopback 端口。地址带一次性登录令牌,只在本机这次跳转里用一次。
|
|
50
|
+
- **连不上时一键体检与部署**(`0.8.36+`)—— **连不上大多不是插件的问题,而是远端 dsh 的版本或环境不对**。体检(只读)会报出平台/node/npm/dsh 版本、**原生模块能否启动**、代理与 npm 源,并给出修复建议;「部署并验证」把它装到远端**私有目录**(不写系统目录、不改 PATH、不覆盖你在用的版本,删目录即回滚)并逐步校验。失败还能交给内置的 `dsh-remote-deploy` 技能排查。
|
|
57
51
|
- **远端跨平台** —— 文件访问走 SFTP 协议层(不依赖 POSIX shell),Linux/macOS/Windows 远端都能列/读/写/搜索/同步。
|
|
58
52
|
- **Windows 主机** —— 自动探测平台并定位 Git Bash,命令经 `bash -s` 走 stdin 执行,不受引号/反斜杠转义困扰(`config.shell` 可指定或设 `native` 关闭);`C:\Users\dev` 与 `/c/Users/dev` 两种写法都接受。
|
|
59
53
|
- **长任务异步化** —— `rw_sync`/`rw_push` 传 `async: true` 返回 `taskId`,可查询进度/结果/取消。
|
|
@@ -105,6 +99,49 @@ dsh plugin add dsh-remote
|
|
|
105
99
|
|
|
106
100
|
> **Remote context 是 session 级的**:只有当前 session 的工作区是某个远程镜像时,system prompt 才注入「Remote workspace」段落;普通本地 session 不受影响,模型也不会主动调 `rw_*`。
|
|
107
101
|
|
|
102
|
+
## 在本地打开远程机器上的 DSH 界面
|
|
103
|
+
|
|
104
|
+
远程机器上那个 `dsh web` 也可以**当成本机的一个页面打开**(issue #46)。方向仍然是「本地主动连出去」:
|
|
105
|
+
|
|
106
|
+
> **远端不需要开放任何端口**。DSH 刻意拒绝 `--host 0.0.0.0`(它会把远程代码执行能力暴露到网络上),
|
|
107
|
+
> 所以正确的做法是由本机 SSH 连过去,在远端**只监听 127.0.0.1** 地起一个 `dsh web`,
|
|
108
|
+
> 再把它的端口经 SSH 隧道搬回本机的一个 loopback 端口。
|
|
109
|
+
|
|
110
|
+
用法:设置 → 远程工作区 → **「远程 DSH 界面」** → 选一台机器 → **连接并打开**。
|
|
111
|
+
浏览器会新开一个标签页,显示的就是**那台远端机器**的完整 DSH 界面(聊天、工具树、设置都在)。
|
|
112
|
+
|
|
113
|
+
- 界面地址形如 `http://127.0.0.1:3088/?token=…`。那个 token 是远端进程启动时打印的一次性凭据,
|
|
114
|
+
只在**本机**的这次跳转里用一次:DSH 会把它换成 30 天有效的签名 cookie 并跳转到干净的 `/`。
|
|
115
|
+
**请不要把这个带 token 的地址外发**。
|
|
116
|
+
- 「断开」只关隧道,**不会**杀掉远端进程(它可能是你在别处也在用的实例);
|
|
117
|
+
要连远端进程一起停,用「断开并停止远端」——它只会结束**本次由插件启动**的那个进程。
|
|
118
|
+
- 若远端已经在跑一个界面(比如你自己在终端里起的),可以选机器后把它的
|
|
119
|
+
`http://127.0.0.1:<端口>/?token=…` 粘进输入框,点「连接已有实例」,插件不会再去启第二个。
|
|
120
|
+
- 远端 `dsh` 不在 SSH 登录 PATH 时,用 `webAttachCommand` 指向它的绝对路径;
|
|
121
|
+
想让它别污染远端用户自己的会话/设置,用 `webAttachDshHome` 指一个临时目录。
|
|
122
|
+
|
|
123
|
+
### 连不上?先体检,再一键部署
|
|
124
|
+
|
|
125
|
+
**连不上大多不是插件的问题,而是远端 dsh 的版本或环境不对。** 最典型的一种:dsh 0.1.0-rc.6
|
|
126
|
+
依赖 `node-pty@1.1.0`,而**那个版本发布的包里没有 `linux-x64` 预编译产物** ⇒ `dsh web`
|
|
127
|
+
在 Linux 上根本起不来。这种情况以前只能看到「没拿到启动令牌」,无从判断。
|
|
128
|
+
|
|
129
|
+
设置 → 远程工作区 → **「远端 dsh 体检与部署」**:
|
|
130
|
+
|
|
131
|
+
1. **体检(只读)** —— 探测远端平台/node/npm/dsh 版本/**原生模块能否启动**/是否认识
|
|
132
|
+
`--no-open`/代理与 npm 源,给出结论与修复建议。**不写任何东西**,可以随便点。
|
|
133
|
+
2. **部署并验证** —— 只在上一步判定「需要且可以自动装」时才出现。它会装到远端
|
|
134
|
+
**私有目录**(默认 `~/.dsh-remote/dsh`):**不写系统目录、不改 PATH、不覆盖你在用的版本**,
|
|
135
|
+
删掉那个目录即完全回滚。装完逐步校验(二进制 / 原生模块 / `web` 子命令),
|
|
136
|
+
成功后**按机器记住**这条 dsh 路径,之后连接就用它。
|
|
137
|
+
3. **让 AI 排查** —— 只在失败后出现。它会创建一个会话,让内置的 `dsh-remote-deploy`
|
|
138
|
+
技能去处理确定性流程覆盖不到的长尾(没有 npm、要 sudo、代理、内网镜像源、Windows 远端等)。
|
|
139
|
+
**只有你点它才会创建**,因为这会消耗模型额度。
|
|
140
|
+
|
|
141
|
+
相关配置:`webInstallPrefix`(装哪,默认 `$HOME/.dsh-remote/dsh`)、
|
|
142
|
+
`webInstallVersion`(装哪个版本,默认 `0.1.5-rc.2`——第一个在 Linux 上能起 web 的版本)、
|
|
143
|
+
`webInstallRegistry`(npm 源;**留空则沿用远端自己的配置**,不会覆盖你的内网镜像)。
|
|
144
|
+
|
|
108
145
|
## 可选:CLI 默认机
|
|
109
146
|
|
|
110
147
|
可在 `cordis.patch.yml` 提供默认机:
|
|
@@ -201,6 +238,13 @@ scripts/dev-run.sh --status # 是否在运行
|
|
|
201
238
|
| `updateMode` | string | `auto` | 自更新模式:`auto`=加载时及每 6 小时检查并自动应用、`manual`=仅在手动检查时查、`off`=完全不查。**0.8.27 起默认 `auto`**——之所以现在才安全,是因为 0.8.24 补上了宿主半热切换 |
|
|
202
239
|
| `updateCheckIntervalMs` | int | 21600000(6h) | `auto` 模式检查 npm 的间隔(下限 60000) |
|
|
203
240
|
| `updateAutoReload` | bool | `true` | 更新落地后自动热切换宿主半;`false` 则留到下次启动,设置页会显示 `pendingReload` |
|
|
241
|
+
| `webAttachPortStart` | int | `3088` | 「远程 DSH 界面」在本机监听的起始端口(占用则顺延,仅监听 127.0.0.1) |
|
|
242
|
+
| `webAttachCommand` | string | `dsh` | 在远端启动 `dsh web` 用的命令;远端 `dsh` 不在 SSH 登录 PATH 时改这里 |
|
|
243
|
+
| `webAttachDshHome` | string | `''` | 远端启动时导出的 `DSH_HOME`;留空则复用远端用户自己的 harness home |
|
|
244
|
+
| `webAttachWaitSeconds` | int | `45` | 等待远端 `dsh web` 打印启动令牌的秒数 |
|
|
245
|
+
| `webInstallPrefix` | string | `''` | 自动部署装到远端的哪个目录;留空用 `$HOME/.dsh-remote/dsh`(不写系统目录、不改 PATH) |
|
|
246
|
+
| `webInstallVersion` | string | `0.1.5-rc.2` | 自动部署安装的 dsh 版本;默认值是第一个在 Linux 上能启动 web 的版本 |
|
|
247
|
+
| `webInstallRegistry` | string | `''` | 安装用的 npm 源;**留空沿用远端自己的配置**,不会覆盖内网镜像 |
|
|
204
248
|
|
|
205
249
|
> 权威清单是 `lib/index.js` 里的 `Config` schema,本表与之一致。
|
|
206
250
|
|
|
@@ -226,6 +270,14 @@ scripts/dev-run.sh --status # 是否在运行
|
|
|
226
270
|
|
|
227
271
|
**升级后插件整个不见了** —— DSH 兼容性判定丢弃了 bundle,升到 **0.8.29+** 即可(见上文「DSH 版本兼容性」)。
|
|
228
272
|
|
|
273
|
+
**「远程 DSH 界面」连不上/一直转圈** —— 先确认那台机器上的 `dsh web` **本身**能起来。
|
|
274
|
+
两个已知坑(插件会在报错里直接点出来):
|
|
275
|
+
① **dsh 0.1.0-rc.6 在 Linux 上无法启动 web**——它依赖的 `node-pty@1.1.0` 只带了
|
|
276
|
+
macOS/Windows 的预编译产物,**没有 `linux-x64`**,会报 `Failed to load native module: pty.node`。
|
|
277
|
+
升到 0.1.5-rc.2+ 即可(其 `node-pty@1.2.0-beta.15` 带 Linux 预编译)。
|
|
278
|
+
② **dsh 0.1.0-rc.6 不认识 `--no-open`**(该 flag 是后加的);插件会自动去掉它重试一次,无需你处理。
|
|
279
|
+
如果远端 `dsh` 不在 SSH 登录 PATH 里(例如装在私有前缀),用 `webAttachCommand` 指向它的绝对路径。
|
|
280
|
+
|
|
229
281
|
## 安全提醒
|
|
230
282
|
|
|
231
283
|
把凭据交给插件等于允许 Agent 以你的用户身份在该主机执行 **shell 命令**——只添加可信机器。密码存在本机文件(或钥匙串),请当作敏感数据。开启 `auditLog` 时每条命令都会记入审计日志。
|
package/lib/client.js
CHANGED
|
@@ -105,6 +105,36 @@ window.__ModuleLoader__.load({
|
|
|
105
105
|
'settings.audit': '最近执行的远程命令(审计日志)',
|
|
106
106
|
'settings.auditEmpty': '暂无记录(在远端执行的命令会记录到 $DSH_HOME/remote-workspaces/audit.log)',
|
|
107
107
|
'settings.auditRefresh': '刷新日志',
|
|
108
|
+
'settings.webAttach': '远程 DSH 界面',
|
|
109
|
+
'settings.webAttachHint': '在本机浏览器里打开远程机器上的 DSH 网页界面。不需要在远端开放任何端口:插件会主动 SSH 连出去,在远端启一个只监听 127.0.0.1 的 dsh web,再把它的端口通过 SSH 隧道搬回本机。',
|
|
110
|
+
'settings.webAttachMachine': '机器',
|
|
111
|
+
'settings.webAttachOpen': '连接并打开',
|
|
112
|
+
'settings.webAttachOpening': '正在连接…',
|
|
113
|
+
'settings.webAttachReopen': '打开',
|
|
114
|
+
'settings.webAttachClose': '断开',
|
|
115
|
+
'settings.webAttachCloseStop': '断开并停止远端',
|
|
116
|
+
'settings.webAttachReconnect': '重连',
|
|
117
|
+
'settings.webAttachDropped': '连接已断开',
|
|
118
|
+
'settings.webAttachNone': '尚未连接任何远程界面',
|
|
119
|
+
'settings.webAttachExisting': '或粘贴远端已在运行的界面地址(http://127.0.0.1:<端口>/?token=…)',
|
|
120
|
+
'settings.webAttachExistingGo': '连接已有实例',
|
|
121
|
+
'settings.webAttachTokenWarn': '地址含登录令牌,仅供本机使用,请勿外发。',
|
|
122
|
+
'settings.webAttachFailed': '连接失败:',
|
|
123
|
+
'settings.deploy': '远端 dsh 体检与部署',
|
|
124
|
+
'settings.deployHint': '连接失败大多不是插件的问题,而是远端 dsh 版本或环境不对(例如旧版在 Linux 上无法启动 web)。这里可以只读体检,也可以一键装到远端私有目录(不影响用户在用的版本)。',
|
|
125
|
+
'settings.deployProbe': '体检(只读)',
|
|
126
|
+
'settings.deployProbing': '正在体检…',
|
|
127
|
+
'settings.deployInstall': '部署并验证',
|
|
128
|
+
'settings.deployInstalling': '正在部署…',
|
|
129
|
+
'settings.deployConfirm': '将在远端私有目录安装 dsh(不写系统目录、不改 PATH、不覆盖用户在用的版本)。继续?',
|
|
130
|
+
'settings.deployInvestigate': '让 AI 排查',
|
|
131
|
+
'settings.deployInvestigating': '正在创建排查会话…',
|
|
132
|
+
'settings.deployOk': '远端环境可用',
|
|
133
|
+
'settings.deployBlocked': '远端环境有问题',
|
|
134
|
+
'settings.deployUseCommand': '将使用的 dsh',
|
|
135
|
+
'settings.deploySteps': '安装步骤',
|
|
136
|
+
'settings.deployDone': '部署完成,已记录该机器使用的 dsh',
|
|
137
|
+
'settings.deployInvestigateStarted': '已创建排查会话,请在会话里跟进。',
|
|
108
138
|
'settings.badgePwd': '密码',
|
|
109
139
|
'settings.badgeKeychain': '钥匙串',
|
|
110
140
|
'settings.badgeProxy': '跳板',
|
|
@@ -333,6 +363,36 @@ window.__ModuleLoader__.load({
|
|
|
333
363
|
'settings.audit': 'Recent remote commands (audit log)',
|
|
334
364
|
'settings.auditEmpty': 'No records yet (commands run remotely are logged to $DSH_HOME/remote-workspaces/audit.log)',
|
|
335
365
|
'settings.auditRefresh': 'Refresh log',
|
|
366
|
+
'settings.webAttach': 'Remote DSH Web UI',
|
|
367
|
+
'settings.webAttachHint': 'Open the DSH web interface of a remote machine in this browser. The remote never opens an inbound port: this plugin SSHes out, starts a loopback-only dsh web there, and carries its port back over the SSH connection.',
|
|
368
|
+
'settings.webAttachMachine': 'Machine',
|
|
369
|
+
'settings.webAttachOpen': 'Connect & open',
|
|
370
|
+
'settings.webAttachOpening': 'Connecting…',
|
|
371
|
+
'settings.webAttachReopen': 'Open',
|
|
372
|
+
'settings.webAttachClose': 'Disconnect',
|
|
373
|
+
'settings.webAttachCloseStop': 'Disconnect & stop remote',
|
|
374
|
+
'settings.webAttachReconnect': 'Reconnect',
|
|
375
|
+
'settings.webAttachDropped': 'Disconnected',
|
|
376
|
+
'settings.webAttachNone': 'No remote UI attached',
|
|
377
|
+
'settings.webAttachExisting': 'Or paste the URL of an interface already running there (http://127.0.0.1:<port>/?token=…)',
|
|
378
|
+
'settings.webAttachExistingGo': 'Attach to running instance',
|
|
379
|
+
'settings.webAttachTokenWarn': 'The URL carries a login token. It is for this machine only — do not share it.',
|
|
380
|
+
'settings.webAttachFailed': 'Could not attach: ',
|
|
381
|
+
'settings.deploy': 'Remote dsh health check & deploy',
|
|
382
|
+
'settings.deployHint': 'A failed connect is usually not the plugin\'s fault: the remote dsh version or environment is wrong (an old one cannot start the web surface on Linux at all). Check read-only, or install into a private directory on the remote that leaves the user\'s own version untouched.',
|
|
383
|
+
'settings.deployProbe': 'Check (read-only)',
|
|
384
|
+
'settings.deployProbing': 'Checking…',
|
|
385
|
+
'settings.deployInstall': 'Deploy & verify',
|
|
386
|
+
'settings.deployInstalling': 'Deploying…',
|
|
387
|
+
'settings.deployConfirm': 'Install dsh into a private directory on the remote (no system directory, no PATH change, no replacing the version in use). Continue?',
|
|
388
|
+
'settings.deployInvestigate': 'Let the AI investigate',
|
|
389
|
+
'settings.deployInvestigating': 'Creating a session…',
|
|
390
|
+
'settings.deployOk': 'Remote environment is usable',
|
|
391
|
+
'settings.deployBlocked': 'Remote environment has a problem',
|
|
392
|
+
'settings.deployUseCommand': 'dsh that will be used',
|
|
393
|
+
'settings.deploySteps': 'Install steps',
|
|
394
|
+
'settings.deployDone': 'Deployed; this machine now records the dsh it uses',
|
|
395
|
+
'settings.deployInvestigateStarted': 'A session was created — follow up there.',
|
|
336
396
|
'settings.badgePwd': 'password',
|
|
337
397
|
'settings.badgeKeychain': 'keychain',
|
|
338
398
|
'settings.badgeProxy': 'jump',
|
|
@@ -785,6 +845,21 @@ window.__ModuleLoader__.load({
|
|
|
785
845
|
const [advOpen, setAdvOpen] = React.useState(false) // 高级配置折叠
|
|
786
846
|
const [fwdOpen, setFwdOpen] = React.useState(true)
|
|
787
847
|
const [auditOpen, setAuditOpen] = React.useState(false)
|
|
848
|
+
// Issue #46: attach a REMOTE machine's DSH Web UI as a local page. One
|
|
849
|
+
// session per machine; `webAttach` holds the machineId currently opening.
|
|
850
|
+
const [webAttaches, setWebAttaches] = React.useState([])
|
|
851
|
+
const [webAttachMachine, setWebAttachMachine] = React.useState('')
|
|
852
|
+
const [webAttachOpen, setWebAttachOpen] = React.useState(false)
|
|
853
|
+
const [webAttachBusy, setWebAttachBusy] = React.useState('')
|
|
854
|
+
const [webAttachErr, setWebAttachErr] = React.useState('')
|
|
855
|
+
const [webAttachExisting, setWebAttachExisting] = React.useState('')
|
|
856
|
+
// Issue #46 P1–P4: deterministic remote deployment + the AI escape hatch.
|
|
857
|
+
// `deploy` holds the last probe/install outcome for the selected machine;
|
|
858
|
+
// `deployBusy` is the running step id, so the UI can show progress.
|
|
859
|
+
const [deploy, setDeploy] = React.useState(null)
|
|
860
|
+
const [deployBusy, setDeployBusy] = React.useState('')
|
|
861
|
+
const [deployErr, setDeployErr] = React.useState('')
|
|
862
|
+
const [deployOpen, setDeployOpen] = React.useState(false)
|
|
788
863
|
React.useEffect(() => { ensureChromeCss() }, [])
|
|
789
864
|
|
|
790
865
|
const testConn = () => {
|
|
@@ -874,6 +949,43 @@ window.__ModuleLoader__.load({
|
|
|
874
949
|
const refreshForwards = () => api('GET', '/dsh-remote/forwards').then((r) => setForwards(r.forwards || [])).catch(() => {})
|
|
875
950
|
React.useEffect(() => { refreshForwards(); api('GET', '/dsh-remote/audit?limit=30').then((r) => setAuditLines(r.lines || [])).catch(() => {}) }, [])
|
|
876
951
|
|
|
952
|
+
// ── 远程 DSH 界面(issue #46)─────────────────────────────────────────
|
|
953
|
+
// The remote never opens an inbound port; we SSH out and carry the remote
|
|
954
|
+
// loopback web UI back to a local port. One tunnel per machine.
|
|
955
|
+
const refreshWebAttaches = () => api('GET', '/dsh-remote/web-attach')
|
|
956
|
+
.then((r) => setWebAttaches(r.attaches || []))
|
|
957
|
+
.catch(() => {})
|
|
958
|
+
React.useEffect(() => { refreshWebAttaches(); const t = setInterval(refreshWebAttaches, 15000); return () => clearInterval(t) }, [])
|
|
959
|
+
const webAttachAction = (action, machineId, extra) => {
|
|
960
|
+
setWebAttachBusy(machineId + ':' + action); setWebAttachErr('')
|
|
961
|
+
return api('POST', '/dsh-remote/web-attach', { action, machineId, ...(extra || {}) })
|
|
962
|
+
.then((r) => { refreshWebAttaches(); return r })
|
|
963
|
+
.catch((e) => { setWebAttachErr(String((e && e.message) || e)); return null })
|
|
964
|
+
.then((r) => { setWebAttachBusy(''); return r })
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// ── 远程部署:体检 / 部署并验证 / 让 AI 排查 ───────────────────────────
|
|
968
|
+
// The probe is read-only, so it is safe to run before the user agrees to
|
|
969
|
+
// anything; the install requires an explicit second click (confirm: true).
|
|
970
|
+
const deployAction = (action, machineId, extra) => {
|
|
971
|
+
setDeployBusy(action); setDeployErr('')
|
|
972
|
+
return api('POST', '/dsh-remote/web-attach', { action, machineId, ...(extra || {}) })
|
|
973
|
+
.then((r) => {
|
|
974
|
+
if (action === 'probe') setDeploy({ kind: 'probe', ...r, at: Date.now() })
|
|
975
|
+
if (action === 'install') setDeploy({ kind: 'install', ...r, at: Date.now() })
|
|
976
|
+
if (action === 'investigate') setDeploy({ kind: 'investigate', ...r, at: Date.now() })
|
|
977
|
+
return r
|
|
978
|
+
})
|
|
979
|
+
.catch((e) => {
|
|
980
|
+
const msg = String((e && e.message) || e)
|
|
981
|
+
setDeployErr(msg)
|
|
982
|
+
// Keep the failing step list so the operator sees WHERE it stopped.
|
|
983
|
+
setDeploy({ kind: action, ok: false, error: msg, at: Date.now() })
|
|
984
|
+
return null
|
|
985
|
+
})
|
|
986
|
+
.then((r) => { setDeployBusy(''); refreshWebAttaches(); return r })
|
|
987
|
+
}
|
|
988
|
+
|
|
877
989
|
const setF = (k) => (ev) => setForm({ ...form, [k]: ev.target.value })
|
|
878
990
|
const setFb = (k) => (ev) => setForm({ ...form, [k]: ev.target.checked })
|
|
879
991
|
const startEdit = (m) => {
|
|
@@ -1092,6 +1204,132 @@ window.__ModuleLoader__.load({
|
|
|
1092
1204
|
workspaces.map((w, i) => React.createElement('div', { key: w + i, style: { display: 'flex', gap: 8, alignItems: 'center', padding: '4px 0' } },
|
|
1093
1205
|
React.createElement('code', { style: { flex: 1, fontSize: 12, fontFamily: 'monospace' } }, w),
|
|
1094
1206
|
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px' }, onClick: () => setWorkspaceNow(w) }, tr('settings.switch'))))) : null,
|
|
1207
|
+
// 远程 DSH 界面(issue #46)
|
|
1208
|
+
React.createElement('div', { style: boxS },
|
|
1209
|
+
sectionToggle(webAttachOpen, setWebAttachOpen, tr('settings.webAttach')),
|
|
1210
|
+
webAttachOpen ? React.createElement(React.Fragment, null,
|
|
1211
|
+
React.createElement('div', { style: { opacity: 0.65, fontSize: 11, lineHeight: 1.5, marginBottom: 6 } }, tr('settings.webAttachHint')),
|
|
1212
|
+
// The URL below carries a one-time login token. It stays on loopback,
|
|
1213
|
+
// but a user could paste it into a chat — say so where the URL is.
|
|
1214
|
+
React.createElement('div', { style: { color: T.warn, fontSize: 11, lineHeight: 1.5, marginBottom: 6 } }, tr('settings.webAttachTokenWarn')),
|
|
1215
|
+
// Live tunnels, one row per machine.
|
|
1216
|
+
webAttaches.length ? webAttaches.map((a) => React.createElement('div', { key: a.machineId || a.localPort, style: { display: 'flex', gap: 8, alignItems: 'center', padding: '6px 0', fontSize: 12, flexWrap: 'wrap' } },
|
|
1217
|
+
React.createElement('span', { style: { color: a.active ? T.ok : T.muted } }, a.active ? '●' : '○'),
|
|
1218
|
+
React.createElement('code', { style: { fontFamily: 'monospace' } }, a.cleanUrl || ''),
|
|
1219
|
+
React.createElement('span', { style: { opacity: 0.6, fontFamily: 'monospace' } }, '→ :' + a.remotePort),
|
|
1220
|
+
a.active ? React.createElement('a', {
|
|
1221
|
+
className: 'dsh-rw-btn dsh-rw-primary', style: { ...primaryBtnS, textDecoration: 'none' },
|
|
1222
|
+
// The token rides on this loopback URL only; DSH exchanges it
|
|
1223
|
+
// for a signed cookie and redirects to a clean '/'.
|
|
1224
|
+
href: a.url, target: '_blank', rel: 'noopener noreferrer',
|
|
1225
|
+
}, tr('settings.webAttachReopen')) : null,
|
|
1226
|
+
// A tunnel whose listener died (remote restart, sshd drop) keeps
|
|
1227
|
+
// its row so the user can see what happened and reconnect. The
|
|
1228
|
+
// remote process is NOT auto-restarted: that would silently
|
|
1229
|
+
// resurrect a server on a machine the user may be done with.
|
|
1230
|
+
!a.active ? React.createElement('button', {
|
|
1231
|
+
type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' },
|
|
1232
|
+
disabled: !!webAttachBusy, onClick: () => webAttachAction('open', a.machineId),
|
|
1233
|
+
}, tr('settings.webAttachReconnect')) : null,
|
|
1234
|
+
!a.active ? React.createElement('span', { style: { color: T.muted, fontSize: 11 } }, a.error || tr('settings.webAttachDropped')) : null,
|
|
1235
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' },
|
|
1236
|
+
disabled: !!webAttachBusy, onClick: () => webAttachAction('close', a.machineId) }, tr('settings.webAttachClose')),
|
|
1237
|
+
// Only offered for a process we started: an attach onto the user's
|
|
1238
|
+
// own running DSH must never signal it.
|
|
1239
|
+
a.managed
|
|
1240
|
+
? React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' },
|
|
1241
|
+
disabled: !!webAttachBusy, onClick: () => webAttachAction('close', a.machineId, { stopRemote: true }) }, tr('settings.webAttachCloseStop'))
|
|
1242
|
+
: null)) : React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, tr('settings.webAttachNone')),
|
|
1243
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', marginTop: 8, flexWrap: 'wrap' } },
|
|
1244
|
+
React.createElement('select', { className: 'dsh-rw-input', value: webAttachMachine, onChange: (e) => setWebAttachMachine(e.target.value), style: { ...inputS, maxWidth: 200 } },
|
|
1245
|
+
React.createElement('option', { value: '' }, tr('settings.webAttachMachine') + '…'),
|
|
1246
|
+
machines.map((m) => React.createElement('option', { key: m.id, value: m.id }, `${m.name || m.host} (${m.username}@${m.host})`))),
|
|
1247
|
+
React.createElement('button', {
|
|
1248
|
+
type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS,
|
|
1249
|
+
disabled: !!webAttachBusy || !webAttachMachine,
|
|
1250
|
+
// The machine is named explicitly in the request, so attaching
|
|
1251
|
+
// never has to move the user's "current machine" selection.
|
|
1252
|
+
onClick: () => webAttachAction('open', webAttachMachine),
|
|
1253
|
+
}, webAttachBusy.endsWith(':open') ? tr('settings.webAttachOpening') : tr('settings.webAttachOpen'))),
|
|
1254
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', marginTop: 6, flexWrap: 'wrap' } },
|
|
1255
|
+
React.createElement('input', { className: 'dsh-rw-input', value: webAttachExisting, onChange: (e) => setWebAttachExisting(e.target.value), placeholder: tr('settings.webAttachExisting'), style: { ...inputS, flex: 1, minWidth: 260 } }),
|
|
1256
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' },
|
|
1257
|
+
disabled: !!webAttachBusy || !webAttachMachine || !webAttachExisting.trim(),
|
|
1258
|
+
onClick: () => webAttachAction('open', webAttachMachine, { existingUrl: webAttachExisting.trim() }).then(() => setWebAttachExisting('')) }, tr('settings.webAttachExistingGo'))),
|
|
1259
|
+
webAttachErr ? React.createElement('div', { style: { color: T.danger, fontSize: 12, marginTop: 6, wordBreak: 'break-all' } }, tr('settings.webAttachFailed') + webAttachErr) : null,
|
|
1260
|
+
) : React.createElement('div', { style: { fontSize: 11, opacity: 0.55 } }, String(webAttaches.length)),
|
|
1261
|
+
),
|
|
1262
|
+
// 远端 dsh 体检与部署(issue #46 P1–P4):确定性的体检/安装,失败再交给 AI。
|
|
1263
|
+
React.createElement('div', { style: boxS },
|
|
1264
|
+
sectionToggle(deployOpen, setDeployOpen, tr('settings.deploy')),
|
|
1265
|
+
deployOpen ? React.createElement(React.Fragment, null,
|
|
1266
|
+
React.createElement('div', { style: { opacity: 0.65, fontSize: 11, lineHeight: 1.5, marginBottom: 6 } }, tr('settings.deployHint')),
|
|
1267
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', marginBottom: 6, flexWrap: 'wrap' } },
|
|
1268
|
+
React.createElement('select', { className: 'dsh-rw-input', value: webAttachMachine, onChange: (e) => setWebAttachMachine(e.target.value), style: { ...inputS, maxWidth: 200 } },
|
|
1269
|
+
React.createElement('option', { value: '' }, tr('settings.webAttachMachine') + '…'),
|
|
1270
|
+
machines.map((m) => React.createElement('option', { key: m.id, value: m.id }, `${m.name || m.host} (${m.username}@${m.host})`))),
|
|
1271
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px' },
|
|
1272
|
+
disabled: !!deployBusy || !webAttachMachine,
|
|
1273
|
+
onClick: () => deployAction('probe', webAttachMachine) },
|
|
1274
|
+
deployBusy === 'probe' ? tr('settings.deployProbing') : tr('settings.deployProbe'))),
|
|
1275
|
+
// Verdict from the last probe/install, rendered as findings with the
|
|
1276
|
+
// severity the backend assigned — the UI never re-classifies them.
|
|
1277
|
+
deploy ? React.createElement('div', { style: { fontSize: 12, lineHeight: 1.6 } },
|
|
1278
|
+
deploy.kind === 'probe' && deploy.verdict
|
|
1279
|
+
? React.createElement('div', { style: { fontWeight: 600, color: deploy.verdict.ok ? T.ok : T.danger, marginBottom: 4 } },
|
|
1280
|
+
deploy.verdict.ok ? tr('settings.deployOk') : tr('settings.deployBlocked'))
|
|
1281
|
+
: null,
|
|
1282
|
+
(deploy.verdict && deploy.verdict.findings || []).map((f, i) => React.createElement('div', { key: f.code + i, style: { display: 'flex', gap: 6, alignItems: 'flex-start', padding: '2px 0' } },
|
|
1283
|
+
React.createElement('span', { style: { color: f.severity === 'blocker' ? T.danger : f.severity === 'warn' ? T.warn : f.severity === 'ok' ? T.ok : T.muted } },
|
|
1284
|
+
f.severity === 'blocker' ? '●' : f.severity === 'ok' ? '●' : '○'),
|
|
1285
|
+
React.createElement('span', null,
|
|
1286
|
+
React.createElement('span', null, f.summary),
|
|
1287
|
+
f.detail ? React.createElement('span', { style: { opacity: 0.7 } }, ' —— ' + f.detail) : null,
|
|
1288
|
+
f.fix ? React.createElement('div', { style: { opacity: 0.8, fontSize: 11 } }, '→ ' + f.fix) : null))),
|
|
1289
|
+
deploy.command || (deploy.verdict && deploy.verdict.useCommand)
|
|
1290
|
+
? React.createElement('div', { style: { marginTop: 4, fontFamily: 'monospace', fontSize: 11, opacity: 0.8, wordBreak: 'break-all' } },
|
|
1291
|
+
tr('settings.deployUseCommand') + ':' + (deploy.command || deploy.verdict.useCommand))
|
|
1292
|
+
: null,
|
|
1293
|
+
// Per-step progress: a failure names the step it stopped at, which
|
|
1294
|
+
// is the whole reason the plan is split into separate commands.
|
|
1295
|
+
Array.isArray(deploy.steps) && deploy.steps.length
|
|
1296
|
+
? React.createElement('div', { style: { marginTop: 6 } },
|
|
1297
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: 2 } }, tr('settings.deploySteps')),
|
|
1298
|
+
deploy.steps.map((s) => React.createElement('div', { key: s.id, style: { fontSize: 11, fontFamily: 'monospace', display: 'flex', gap: 6 } },
|
|
1299
|
+
React.createElement('span', { style: { color: s.ok ? T.ok : T.danger } }, s.ok ? '✓' : '✗'),
|
|
1300
|
+
React.createElement('span', null, s.title),
|
|
1301
|
+
!s.ok && s.output ? React.createElement('span', { style: { opacity: 0.75, whiteSpace: 'pre-wrap', wordBreak: 'break-all' } }, ' — ' + String(s.output).slice(0, 300)) : null)))
|
|
1302
|
+
: null,
|
|
1303
|
+
deploy.ok === true && deploy.steps
|
|
1304
|
+
? React.createElement('div', { style: { color: T.ok, marginTop: 4 } }, tr('settings.deployDone'))
|
|
1305
|
+
: null,
|
|
1306
|
+
deploy.kind === 'investigate' && deploy.sessionId
|
|
1307
|
+
? React.createElement('div', { style: { color: T.ok, marginTop: 4 } }, tr('settings.deployInvestigateStarted') + ' (' + deploy.sessionId + ')')
|
|
1308
|
+
: null,
|
|
1309
|
+
) : null,
|
|
1310
|
+
// The install button only appears when a probe says it is both needed
|
|
1311
|
+
// and possible; clicking it is the explicit consent (confirm: true).
|
|
1312
|
+
deploy && deploy.verdict && deploy.verdict.canAutoInstall && !deploy.verdict.ok
|
|
1313
|
+
? React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: { ...primaryBtnS, marginTop: 6 },
|
|
1314
|
+
disabled: !!deployBusy,
|
|
1315
|
+
onClick: () => { if (window.confirm(tr('settings.deployConfirm'))) deployAction('install', webAttachMachine, { confirm: true }) } },
|
|
1316
|
+
deployBusy === 'install' ? tr('settings.deployInstalling') : tr('settings.deployInstall'))
|
|
1317
|
+
: null,
|
|
1318
|
+
// The AI escape hatch: offered after a failure, and ONLY on click —
|
|
1319
|
+
// it costs a model call, so it is never automatic.
|
|
1320
|
+
(deployErr || (deploy && deploy.ok === false))
|
|
1321
|
+
? React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px', marginTop: 6 },
|
|
1322
|
+
disabled: !!deployBusy,
|
|
1323
|
+
onClick: () => deployAction('investigate', webAttachMachine, {
|
|
1324
|
+
findings: (deploy && deploy.verdict && deploy.verdict.findings) || [],
|
|
1325
|
+
facts: deploy && deploy.facts,
|
|
1326
|
+
error: deployErr || (deploy && deploy.error) || '',
|
|
1327
|
+
}) },
|
|
1328
|
+
deployBusy === 'investigate' ? tr('settings.deployInvestigating') : tr('settings.deployInvestigate'))
|
|
1329
|
+
: null,
|
|
1330
|
+
deployErr ? React.createElement('div', { style: { color: T.danger, fontSize: 12, marginTop: 6, wordBreak: 'break-all' } }, deployErr) : null,
|
|
1331
|
+
) : React.createElement('div', { style: { fontSize: 11, opacity: 0.55 } }, deploy && deploy.verdict ? (deploy.verdict.ok ? tr('settings.deployOk') : tr('settings.deployBlocked')) : ''),
|
|
1332
|
+
),
|
|
1095
1333
|
// 端口转发面板
|
|
1096
1334
|
React.createElement('div', { style: boxS },
|
|
1097
1335
|
sectionToggle(fwdOpen, setFwdOpen, tr('settings.forwards')),
|