@hyzyn/dsh-tty 0.24.0 → 0.26.0
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 +65 -13
- package/README.md +51 -13
- package/client.js +464 -192
- package/lib/index.js +164 -9
- package/lib/index.js.map +1 -1
- package/lib/probe.js +11 -6
- package/lib/probe.js.map +1 -1
- package/lib/shell-integration.d.ts +40 -1
- package/lib/shell-integration.js +180 -102
- package/lib/shell-integration.js.map +1 -1
- package/lib/ssh-config.d.ts +1 -1
- package/lib/ssh-config.js +9 -0
- package/lib/ssh-config.js.map +1 -1
- package/lib/ssh.d.ts +32 -2
- package/lib/ssh.js +32 -12
- package/lib/ssh.js.map +1 -1
- package/lib/tmux.d.ts +13 -4
- package/lib/tmux.js +23 -15
- package/lib/tmux.js.map +1 -1
- package/lib/tunnels.d.ts +101 -0
- package/lib/tunnels.js +233 -0
- package/lib/tunnels.js.map +1 -1
- package/package.json +3 -3
- package/scripts/integration.mjs +3 -3
- package/scripts/lib/crash-trace.mjs +164 -0
- package/scripts/preview/harness.js +53 -0
- package/scripts/preview/mock-host.js +12 -1
- package/scripts/preview.mjs +60 -20
- package/scripts/windows-smoke.mjs +76 -0
package/README.en.md
CHANGED
|
@@ -114,7 +114,7 @@ Both were reproduced on **Windows 11 ARM (24H2) + Node 22 ARM64**. The fix:
|
|
|
114
114
|
|
|
115
115
|
## Agent tools
|
|
116
116
|
|
|
117
|
-
The plugin injects
|
|
117
|
+
The plugin injects nineteen tools into the agent (with the same power as the bash tool; operations show up live in the user’s terminal):
|
|
118
118
|
|
|
119
119
|
| Tool | Purpose |
|
|
120
120
|
| --- | --- |
|
|
@@ -135,6 +135,7 @@ The plugin injects seventeen tools into the agent (with the same power as the ba
|
|
|
135
135
|
| `sftp_remove` | Delete a remote file/directory; a directory uses rmdir by default (a non-empty one errors explicitly), and `recursive:true` deletes the whole tree (irrecoverable); paths pointing at `/`, `~`, or containing `.`/`..` segments are refused (guard for an irrecoverable operation, 0.19.0) |
|
|
136
136
|
| `sftp_tree` | Recursively list a remote directory structure (depth-first, directories first; `maxDepth` 1~8 / `maxEntries` 1~2000 cap it, `truncated:true` when exceeded; symlinks are not followed, to avoid cycles) |
|
|
137
137
|
| `tunnel_list` | List port-forwarding tunnels and their live state (active/connecting/error/stopped, rules, connection counts) |
|
|
138
|
+
| `tunnel_start` / `tunnel_stop` | Start or stop one port-forwarding tunnel (0.25.0; `name` comes from `tunnel_list`). These **rewrite the configuration** — the same thing as ticking/unticking “Enable” in the settings card (settings → hot apply → `reconcile`), so the change is persistent and visible to the user. The returned snapshot usually still says `connecting` (dialling is async); call `tunnel_list` again for the final verdict |
|
|
138
139
|
|
|
139
140
|
Typical agent flow (recommended): for a command that simply runs to completion, use `tty_run` to get its
|
|
140
141
|
output + exit code in one call; for a **long-lived** session, `tty_open` opens one (pass `persistName` for tmux
|
|
@@ -337,6 +338,21 @@ references one connection-book entry (host and authentication come with it), in
|
|
|
337
338
|
profiles running the same tunnel at the same time leave the later one with `EADDRINUSE`, stuck in
|
|
338
339
|
`error` with `fatal:true` (**no retry**; changing the configuration rebuilds it from the new spec). The
|
|
339
340
|
error message names “possibly the host process of another DSH profile” and offers two ways out;
|
|
341
|
+
- **Ports are probed at save time (0.26.0)**: saving the tunnel configuration first **probes each local
|
|
342
|
+
port** (an exclusive probe: `listen`, then release immediately) so two classes of problem surface at the
|
|
343
|
+
moment you save instead of later — ① the configuration being submitted has two local tunnels on the same
|
|
344
|
+
`localPort` (decidable at configuration level), and ② the port is held by something **outside this
|
|
345
|
+
process** (another profile's host, or another program).
|
|
346
|
+
**The probe only warns; it never blocks the save**: the 200 response carries an extra `warnings` array and
|
|
347
|
+
the card shows it in the **yellow** warning slot (which replaces the green “is active” — that tunnel will
|
|
348
|
+
sit in red `error` and never retry, so “is active” would read as everything being fine). Why not reject:
|
|
349
|
+
the card saves the **whole table at once**, so rejecting would lock a user whose config already contains a
|
|
350
|
+
conflict out of saving *anything* — and fixing it is exactly what requires saving first.
|
|
351
|
+
Two rules worth remembering: a port **this process already holds is exempt** (editing a running tunnel's
|
|
352
|
+
other fields leaves its port held by itself — that is not a conflict, since reconcile stops then starts and
|
|
353
|
+
the port returns in the same tick), while a **disabled** tunnel still counts as a duplicate (it can be
|
|
354
|
+
enabled at any moment). A remote direction's `remotePort` is listened on the server side and is not probed
|
|
355
|
+
locally;
|
|
340
356
|
- **Status badges**: while the card is expanded it polls live status every 2s (active green/connecting
|
|
341
357
|
blue/error red/stopped grey + last error); connection-book entries in the “+” menu show a `⇄N` tunnel
|
|
342
358
|
badge; the agent can query status with the `tunnel_list` tool;
|
|
@@ -422,8 +438,24 @@ slot (`ssh2`’s sftp subsystem, host half in `src/sftp.ts`):
|
|
|
422
438
|
connection-book entries are resolved live on every (re)connect (a changed password takes effect
|
|
423
439
|
automatically); TOFU shares the same `hostKeys` pinning with terminal sessions/tunnels, and a changed
|
|
424
440
|
fingerprint is rejected the same way; SFTP does not count against `maxSessions`;
|
|
441
|
+
- **Resumable downloads (0.25.0, panel downloads)**: when a download drops, it **resumes from the bytes
|
|
442
|
+
already received** — the host's `/sftp/download` route now accepts `offset` (`>0` returns 206 +
|
|
443
|
+
`content-range` with `content-length` reporting the **remaining** length; `offset ≥ file size` returns 416
|
|
444
|
+
rather than a zero-byte 200, so “what you wanted is already local” is never mistaken for “the file is
|
|
445
|
+
empty”). The rules live in the pure module `client-src/download-resume.js` (`planResume`): **clicking ✕
|
|
446
|
+
never resumes** (cancel means “stop”, auto-retrying ignores the user), zero progress never resumes, the
|
|
447
|
+
retry budget is capped (3 by default), and the progress bar does **not** reset to zero while resuming.
|
|
448
|
+
Directory-level direct transfer (dual-pane ⇨/⇦) still sends everything again;
|
|
449
|
+
- **Dual-pane drag & drop and hidden files (0.25.0)**: drop a file/folder from the file manager onto the
|
|
450
|
+
**right-hand remote pane** to upload it into that pane's current directory (folders are expanded through
|
|
451
|
+
`webkitGetAsEntry` and filled in with `mkdir parents`, same as the single-pane path); dropping onto the
|
|
452
|
+
**local pane** says explicitly that there is nothing to do instead of failing silently — the rule lives in
|
|
453
|
+
`client-src/sftp-view.js` (`planDrop`). Each pane's toolbar gains a “Hidden files” toggle (off by default,
|
|
454
|
+
preference kept in localStorage) so `.env` / `.gitignore` can be shown on demand; **filtering happens only
|
|
455
|
+
in the render layer** — the host `list` and the agent's `sftp_list` always return every entry (an agent
|
|
456
|
+
that cannot see hidden files draws wrong conclusions);
|
|
425
457
|
- **Transfer channel**: `POST /api/dsh-tty/sftp/list|mkdir|rename|remove|download|
|
|
426
|
-
upload` (all loopback-fenced). The connection spec travels in the JSON body (connection-book name or
|
|
458
|
+
upload` (all loopback-fenced). `download` also accepts an optional `offset` (resume). The connection spec travels in the JSON body (connection-book name or
|
|
427
459
|
inline fields, with the same semantics as WS ssh frames: “entry as the base + inline per-field overrides”)
|
|
428
460
|
or, for upload, in the `x-dsh-sftp-meta` header (base64url) — **credentials never enter the URL/query
|
|
429
461
|
string**; uploads and downloads are streamed pipes, and a whole file never enters memory;
|
|
@@ -441,8 +473,14 @@ slot (`ssh2`’s sftp subsystem, host half in `src/sftp.ts`):
|
|
|
441
473
|
|
|
442
474
|
The default safety model is unchanged: sessions live and die with the host (kernel-level PTY cleanup). For
|
|
443
475
|
work that must “outlive the host” (dev servers, builds, training jobs), turn on a **persistent terminal** —
|
|
444
|
-
session state is delegated to a tmux server (dedicated socket
|
|
445
|
-
|
|
476
|
+
session state is delegated to a tmux server (a dedicated socket, fully isolated from the user’s own tmux),
|
|
477
|
+
so it survives the keep-alive timeout and can even be reattached after a host restart:
|
|
478
|
+
|
|
479
|
+
> **The socket is profile-scoped** (since 0.25.0): `tmux -L dsh-tty-<profile>`, and the runtime assets
|
|
480
|
+
> (`tmux.conf` / `inner.sh` / shell stubs under `<DSH_HOME>/tty/<profile>/`) are layered the same way — with
|
|
481
|
+
> several profiles running, each sees only its own persistent sessions and `kill-server` only affects its
|
|
482
|
+
> own. **A host started without a profile keeps the historical `dsh-tty`** (and `<DSH_HOME>/tty/`), so a
|
|
483
|
+
> single-profile user sees no path or behaviour change after upgrading.
|
|
446
484
|
|
|
447
485
|
- **Entry (simplified in 0.10.1)**: choosing `tmux` for “Session persistence” in the settings card is the
|
|
448
486
|
only switch — once it is on, **every newly opened tab is persistent by default**: “Local terminal” in the
|
|
@@ -456,9 +494,10 @@ tmux), so it survives the keep-alive timeout and can even be reattached after a
|
|
|
456
494
|
switch is on, and **editing an entry in the settings card no longer drops it** (before 0.17.x, renaming a
|
|
457
495
|
single field silently reset it to `false`);
|
|
458
496
|
- **Mechanism**: spawn/ssh frames carry `persist` plus a stable `persistName` generated by the client and
|
|
459
|
-
saved with the tab spec — locally the `-c` wrapper layer becomes `exec tmux -L
|
|
497
|
+
saved with the tab spec — locally the `-c` wrapper layer becomes `exec tmux -L <socket> -f
|
|
460
498
|
<conf> new-session -A -s dsh-<name>` (cwd is inherited from the node-pty spawn); over SSH the remote runs
|
|
461
|
-
`exec tmux -L
|
|
499
|
+
`exec tmux -L <socket> -f /dev/null new-session -A -s dsh-<name>` to open the pty channel
|
|
500
|
+
(`<socket>` = `tmuxSocketName()`, profile-scoped since 0.25.0). `-A` is
|
|
462
501
|
attach-or-create: after a host restart, reopening a tab reattaches to the same tmux session by name,
|
|
463
502
|
restoring running programs and pane state exactly;
|
|
464
503
|
- **Recovery chain**: after the browser reconnects it queries `sessions` — persistent tabs whose sid is gone
|
|
@@ -597,6 +636,17 @@ and the agent tools all reuse the same scheduling.
|
|
|
597
636
|
all of a host’s rsa/ed25519 entries (no longer first-entry-only); host
|
|
598
637
|
names from the connection book are also used to restore `|1|` hashed entries, and non-default ports are
|
|
599
638
|
parsed as `[host]:port`;
|
|
639
|
+
- **Host-key alias (`hostKeyAlias`, 0.25.0)**: TOFU records and comparisons are located by **connection
|
|
640
|
+
address** by default, but one real host is often reached through several addresses — the internal IP behind
|
|
641
|
+
a bastion, or `127.0.0.1:2222` through a port forward. “Same key, different record” then makes every
|
|
642
|
+
address switch look like a **fingerprint change** (a false MITM alarm that **rejects the connection**),
|
|
643
|
+
while the user’s `known_hosts` only has one line. Filling in the same alias lets those addresses share a
|
|
644
|
+
single host-key record. Semantics match OpenSSH’s `HostKeyAlias`: **it only decides where the fingerprint
|
|
645
|
+
is stored, never the connection address**; empty means address-based (byte-for-byte the old behaviour).
|
|
646
|
+
A `HostKeyAlias` in `~/.ssh/config` is **imported as-is** (it is a purely local locator with no executable
|
|
647
|
+
component, a very different trust level from `ProxyCommand`). All four connection points (terminal / SFTP /
|
|
648
|
+
tunnel / test probe) share one key derivation, so “the test says it matches, the real connection says it
|
|
649
|
+
changed” cannot happen;
|
|
600
650
|
- **Connection test (0.11.0)**: a “Test” button on each connection-book row of the settings card, plus a
|
|
601
651
|
“Test connection” button in the SSH connection dialog — both perform **link diagnostics only** (no session,
|
|
602
652
|
no `maxSessions` slot, no shell): first a TCP pre-check (DNS + connect, failures classified as
|
|
@@ -713,7 +763,7 @@ sub-page under the Plugins sidebar, and `≤0.1.5` uses the settings-page card.
|
|
|
713
763
|
| `colorTerm` | `truecolor` | COLORTERM value |
|
|
714
764
|
| `cwd` | host startup directory | Fallback working directory (the client’s current session cwd wins) |
|
|
715
765
|
| `reconnectGraceSec` | 120 | Seconds a session is kept alive after an abnormal disconnect (0~3600): the session survives a page refresh/network blip waiting for a reconnect, and the reaper ends it on timeout; `0` = the old behavior, end immediately on disconnect |
|
|
716
|
-
| `sshHosts` | `[]` | SSH connection book (selectable in the panel “+” menu): entries `{name, host, port=22, username, auth=agent\|key\|password, keyPath, passphrase, password, agentForward, persist=false}`; saved as a whole-set replacement, the same name overwrites; `password` / `passphrase` support `env:VAR` references so no plaintext is stored; with persistence on, clicking an entry opens a tmux persistent session by default, and `persist=false` is an **opt-out
|
|
766
|
+
| `sshHosts` | `[]` | SSH connection book (selectable in the panel “+” menu): entries `{name, host, port=22, username, auth=agent\|key\|password, keyPath, passphrase, password, agentForward, hostKeyAlias='', persist=false}`; saved as a whole-set replacement, the same name overwrites; `password` / `passphrase` support `env:VAR` references so no plaintext is stored; with persistence on, clicking an entry opens a tmux persistent session by default, and `persist=false` is an **opt-out**; an empty `hostKeyAlias` locates fingerprints by host address (TOFU), while a non-empty one locates them by alias (several addresses of one host share a record — see “SSH connections”) |
|
|
717
767
|
| `hostKeys` | `[]` | SSH host key records (TOFU, maintained automatically): entries `{host, port, fingerprints[]}` (the legacy single `fingerprint` field is migrated and merged on read); unique by host:port, one record holds all of a host’s keys, appended automatically on the first connection, any matching fingerprint is allowed, and a full mismatch rejects the connection; the settings card can delete them to reset |
|
|
718
768
|
| `shellIntegration` | true | Injects the OSC 133/7 shell integration (command boundary markers + cwd reporting; `tty_capture{last}` depends on it); zsh/bash supported, other shells skipped automatically; can be turned off when compatibility problems appear |
|
|
719
769
|
| `tunnels` | `[]` | Port-forwarding tunnels: entries `{name, bookName, direction=local\|remote, localPort?, remoteHost?, remotePort?, localTargetHost?, localTargetPort?, enabled}`; `bookName` references a connection-book entry for host and authentication; maintained graphically in the “Port forwarding” block of the card |
|
|
@@ -1060,17 +1110,18 @@ verify things like “is there still a white panel after switching light/dark th
|
|
|
1060
1110
|
SSH connection is independent of terminal sessions and both use TOFU pinning and connection-book
|
|
1061
1111
|
authentication; tunnel spec changes (port/target/start-stop) take effect hot on “save”, while hot-changing
|
|
1062
1112
|
connection-book credentials takes effect on the next reconnect.
|
|
1063
|
-
- **Session persistence (tmux) boundaries**: persistent tabs are hosted by the tmux server (dedicated socket
|
|
1113
|
+
- **Session persistence (tmux) boundaries**: persistent tabs are hosted by the tmux server (a dedicated socket,
|
|
1114
|
+
profile-scoped since 0.25.0 — `dsh-tty` without a profile, `dsh-tty-<profile>` with one)
|
|
1064
1115
|
`dsh-tty`) — when the host is hard-killed / the keep-alive reaper fires / the browser loses the tab spec,
|
|
1065
1116
|
the tmux session is **retained** (which is exactly what makes recovery possible) until the machine restarts
|
|
1066
|
-
or `tmux -L
|
|
1117
|
+
or `tmux -L <socket> kill-server` is run manually; the agent’s command-granularity tools
|
|
1067
1118
|
(capture{last}/expect) depend on DCS `allow-passthrough` in tmux ≥3.3, and on older versions persistence
|
|
1068
1119
|
works but that capability degrades (SSH remote sessions do not inject shell integration hooks, so
|
|
1069
1120
|
capture{last} was never available there, independently of persistence); recovery redraws the currently
|
|
1070
1121
|
visible screen, while pre-disconnect scroll history lives in tmux’s own history buffer (copy-mode), not in
|
|
1071
1122
|
the outer xterm scrollback; a persistent tab’s `exit` frame exit code is the tmux client’s (0), while the
|
|
1072
1123
|
shell’s exit code remains available through the OSC 133;D marker as usual; `tmux.conf` is read only when the
|
|
1073
|
-
tmux server first starts (after changing the configuration, run `tmux -L
|
|
1124
|
+
tmux server first starts (after changing the configuration, run `tmux -L <socket> kill-server` so the next
|
|
1074
1125
|
spawn rebuilds the server); with `grace=0`, “end immediately on disconnect” also kill-sessions persistent
|
|
1075
1126
|
tabs (the tmux session does not survive); persistent SSH sessions require tmux on the remote (without it
|
|
1076
1127
|
they degrade to a normal session automatically, and the connection bar shows a permanent “not persistent”
|
|
@@ -1144,7 +1195,7 @@ Host half (src/index.ts)
|
|
|
1144
1195
|
│ truncated packets across chunks) → command boundary capture (tty_capture{last} / tty_expect
|
|
1145
1196
|
│ early stop) and cwd tracking (tty_list)
|
|
1146
1197
|
├─ local path: ctx.get('subprocess').spawnTerminal({ argv: shell -c wrapper, cwd })
|
|
1147
|
-
│ persistent tabs (0.10.0): the wrapper becomes `exec tmux -L
|
|
1198
|
+
│ persistent tabs (0.10.0): the wrapper becomes `exec tmux -L <socket> -f <conf> new -A -s dsh-<name>`
|
|
1148
1199
|
│ (src/tmux.ts: probing / asset generation / spawn planning / kill-session; stable assets under
|
|
1149
1200
|
│ <DSH_HOME|~/.dsh>/tty/ — tmux.conf + inner.sh + zsh/bash stubs; tmux swallows sequences it
|
|
1150
1201
|
│ does not recognize, so the shell-integration hook detects $TMUX and wraps OSC 133/7 in a DCS
|
|
@@ -1166,7 +1217,8 @@ Host half (src/index.ts)
|
|
|
1166
1217
|
│ sftp_list/read/write/mkdir/rename/remove/tree tools (accept only a connection-book name;
|
|
1167
1218
|
│ mkdir supports parents, filling levels bottom-up, tree recurses with depth/count limits)
|
|
1168
1219
|
├─ port forwarding (src/tunnels.ts): host-owned tunnels (-L/-R both ways), backoff reconnect on
|
|
1169
|
-
│ drop, TOFU shared, connection counters; GET /api/dsh-tty/tunnels live status + tunnel_list
|
|
1220
|
+
│ drop, TOFU shared, connection counters; GET /api/dsh-tty/tunnels live status + tunnel_list /
|
|
1221
|
+
│ tunnel_start / tunnel_stop tools
|
|
1170
1222
|
└─ frame protocol: spawn|ssh / input / resize / kill / sessions / attach
|
|
1171
1223
|
↔ ready/data/exit/error/sessions + backpressure
|
|
1172
1224
|
|
|
@@ -1176,7 +1228,7 @@ SSH path (src/ssh.ts)
|
|
|
1176
1228
|
through HostKeyStore), opening a shell channel wrapped into a TermHandle shaped like a PTY
|
|
1177
1229
|
(pid=null, kind='ssh', target=user@host[:port]),
|
|
1178
1230
|
backpressure is passed through to the channel as well — after that it is scheduled just like a local PTY;
|
|
1179
|
-
with persist on it first probes `command -v tmux` and then `exec tmux -L
|
|
1231
|
+
with persist on it first probes `command -v tmux` and then `exec tmux -L <socket> -f /dev/null
|
|
1180
1232
|
new -A -s dsh-<name>` to open a pty channel (remote tmux hosting; without tmux it degrades to a plain
|
|
1181
1233
|
shell channel + a grey-text hint; kill is finished off by `tmux kill-session` inside the connection)
|
|
1182
1234
|
```
|
package/README.md
CHANGED
|
@@ -105,7 +105,7 @@ dsh plugin --profile web add link:$(pwd)/packages/tty # 仓库开发调试
|
|
|
105
105
|
|
|
106
106
|
## agent 工具
|
|
107
107
|
|
|
108
|
-
插件向 agent
|
|
108
|
+
插件向 agent 注入十九个工具(与 bash 工具同权,操作实时显示在用户终端里):
|
|
109
109
|
|
|
110
110
|
| 工具 | 作用 |
|
|
111
111
|
| --- | --- |
|
|
@@ -126,6 +126,7 @@ dsh plugin --profile web add link:$(pwd)/packages/tty # 仓库开发调试
|
|
|
126
126
|
| `sftp_remove` | 删除远程文件/目录;目录默认 rmdir(非空明确报错),`recursive:true` 整树删除(不可恢复);会拒绝 `/`、`~`、含 `.`/`..` 段的路径(不可恢复操作的前置护栏,0.19.0) |
|
|
127
127
|
| `sftp_tree` | 递归列举远程目录结构(深度优先、目录优先;`maxDepth` 1~8 / `maxEntries` 1~2000 限流,超限 `truncated:true`;symlink 不跟随防环) |
|
|
128
128
|
| `tunnel_list` | 列出端口转发隧道及其实时状态(活跃/连接中/错误/停止、规则、连接数);`fatal:true` = 人工介入级故障(本地监听失败 / 连接簿缺失),**不会自动重试**,修配置后重建 |
|
|
129
|
+
| `tunnel_start` / `tunnel_stop` | 启停一条端口转发隧道(0.25.0,`name` 来自 `tunnel_list`)。**会改写配置**——与在设置卡片里勾选/取消那个「启用」是同一件事(写回 settings → 热应用 → `reconcile`),所以是持久的、用户在卡片上看得见。返回值是操作后的状态快照:`state` 通常还是 `connecting`(拨号异步),要最终结论稍后 `tunnel_list` |
|
|
129
130
|
|
|
130
131
|
典型 agent 流程(推荐):跑完就算完的命令用 `tty_run` 一次调用拿回输出 + 退出码;
|
|
131
132
|
需要**长驻**的会话用 `tty_open`(长驻进程用 `persistName` 要 tmux 持久化)→ `tty_send` 启动命令
|
|
@@ -294,6 +295,17 @@ SSH 会话同表调度:`tty_list` 里 `kind: 'ssh'` 的条目按 `target`
|
|
|
294
295
|
各存一份(复制 profile 会连隧道一起拷走)。两个 profile 同时跑同一条隧道 → 后起的
|
|
295
296
|
那个 `EADDRINUSE`,状态停在 `error` 且 `fatal:true`(**不重试**,改配置后按新规格
|
|
296
297
|
重建);报错文案会直接点明「可能是另一个 DSH profile 的宿主进程」并给出两条出路;
|
|
298
|
+
- **保存时就探测端口(0.26.0)**:保存隧道配置时宿主会**先探一遍本地端口**(独占探测:
|
|
299
|
+
`listen` 成功即立刻释放),把两类问题提前到保存那一拍说清楚——① 这次提交的配置里
|
|
300
|
+
有两条 local 隧道用同一个 `localPort`(配置层就能判死);② 端口被**本进程之外**的
|
|
301
|
+
东西占着(另一个 profile 的宿主、别的程序)。
|
|
302
|
+
**探测只警告、不拦保存**:返回 200 的响应里多一个 `warnings` 数组,卡片用**黄色**警告
|
|
303
|
+
档显示它(绿色「已生效」会被它顶掉——那条隧道会停在红色 `error` 且不重试,说「已生效」
|
|
304
|
+
会让人以为一切正常)。为什么不硬拒:卡片保存是**整表提交**,配置里已经躺着冲突时硬拒
|
|
305
|
+
会把用户锁死在「任何一项都存不下去」,而修它恰恰要先能保存。
|
|
306
|
+
两条口径值得记:**本进程自己正占着的端口被豁免**(编辑一条在跑的隧道时端口当然是自己
|
|
307
|
+
占的,那不是冲突;reconcile 会先 stop 再 start,端口在同一拍就还回来);**停用的那条
|
|
308
|
+
也算重名冲突**(它随时会被启用)。remote 方向的 `remotePort` 在服务端监听,不参与本机探测;
|
|
297
309
|
- **状态徽标**:卡片展开期间 2s 轮询实时状态(活跃绿/连接中蓝/错误红/停止
|
|
298
310
|
灰 + 最近错误);「+」菜单的连接簿条目显示 `⇄N` 隧道徽标;agent 可用
|
|
299
311
|
`tunnel_list` 工具查询状态;
|
|
@@ -364,8 +376,22 @@ subsystem,宿主半体 `src/sftp.ts`):
|
|
|
364
376
|
断开后下次操作自动重连;连接簿条目在每次(重)连接时实时解析(改密码
|
|
365
377
|
后自动用新凭证);TOFU 与终端会话/隧道共用同一份 `hostKeys` 钉扎,指纹
|
|
366
378
|
变同样拒绝;SFTP 不计入 `maxSessions` 名额;
|
|
379
|
+
- **断点续传(0.25.0,面板下载)**:下载中途断线时**自动从已收字节接着下**——
|
|
380
|
+
宿主侧 `/sftp/download` 接上了 `offset`(>0 回 206 + `content-range`,
|
|
381
|
+
`content-length` 报**剩余**长度;`offset ≥ 文件大小` 回 416 而不是 0 字节的 200,
|
|
382
|
+
免得把「要的东西已经在本地了」误读成「文件是空的」)。判定规则住在纯模块
|
|
383
|
+
`client-src/download-resume.js`(`planResume`):**用户点 ✕ 一律不续**(取消的语义
|
|
384
|
+
就是「不要了」,自动重试是无视用户意图)、零进度不续、重试预算封顶(默认 3 次),
|
|
385
|
+
且续传期间进度条**不回零**。目录级直传(双栏 ⇨/⇦)仍是整份重传;
|
|
386
|
+
- **双栏拖拽上传与隐藏文件(0.25.0)**:把一个文件/文件夹从文件管理器拖到**右侧远程栏**
|
|
387
|
+
即上传到该栏当前目录(文件夹经 `webkitGetAsEntry` 递归展开、目录用 `mkdir parents`
|
|
388
|
+
逐级补齐,与单窗体同源);拖到**本机栏**会明确提示「没有可执行的动作」而不是静默
|
|
389
|
+
——判定住在 `client-src/sftp-view.js`(`planDrop`)。每栏工具行多一个「隐藏文件」
|
|
390
|
+
开关(默认关,偏好记在 localStorage):`.env` / `.gitignore` 这类条目按需显示;
|
|
391
|
+
**过滤只在渲染层**——宿主 `list` 与 agent 的 `sftp_list` 始终照实返回全部条目
|
|
392
|
+
(agent 看不到隐藏文件会得出错误结论);
|
|
367
393
|
- **传输通道**:`POST /api/dsh-tty/sftp/list|mkdir|rename|remove|download|
|
|
368
|
-
upload`(全部 loopback
|
|
394
|
+
upload`(全部 loopback 围栏)。`download` 另接受可选的 `offset`(续传)。连接规格经 JSON 体(连接簿名或内联字段,
|
|
369
395
|
与 WS ssh 帧同语义「条目作基底 + 内联逐项覆盖」)或 upload 的
|
|
370
396
|
`x-dsh-sftp-meta` 头(base64url)携带——**凭证不进 URL/查询串**;上传下载
|
|
371
397
|
均为流式 pipe,不整文件进内存;
|
|
@@ -386,9 +412,11 @@ subsystem,宿主半体 `src/sftp.ts`):
|
|
|
386
412
|
tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离),断线保活
|
|
387
413
|
超时、甚至宿主重启后都能接回:
|
|
388
414
|
|
|
389
|
-
>
|
|
390
|
-
>
|
|
391
|
-
>
|
|
415
|
+
> **socket 按 profile 区分**(0.25.0 起):`tmux -L dsh-tty-<profile>`,运行时资产
|
|
416
|
+
> (`tmux.conf` / `inner.sh` / shell 桩,`<DSH_HOME>/tty/<profile>/`)同步分层——多 profile
|
|
417
|
+
> 同跑时各自的持久会话清单互不可见,`kill-server` 也只影响自己那个 profile。
|
|
418
|
+
> **不带 profile 启动的宿主仍用历史名 `dsh-tty`**(路径也是 `<DSH_HOME>/tty/`),
|
|
419
|
+
> 所以单 profile 用户升级后行为与路径**一字不变**。
|
|
392
420
|
|
|
393
421
|
- **入口(0.10.1 简化)**:设置卡片「会话持久化」选 `tmux` 即唯一开关——开启后
|
|
394
422
|
**所有新开的标签默认持久化**:「+」菜单的「本地终端」、连接簿条目点击、
|
|
@@ -400,10 +428,10 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
|
|
|
400
428
|
都只有全局开关开着时才显示这个勾选框;**在设置里编辑条目不再丢掉它**(0.17.x 之前只改
|
|
401
429
|
一个用户名就会静默把它清成 `false`);
|
|
402
430
|
- **机制**:spawn/ssh 帧带 `persist` + 客户端生成、随标签规格保存的稳定
|
|
403
|
-
`persistName`——本地把 `-c` 包装层换成 `exec tmux -L
|
|
431
|
+
`persistName`——本地把 `-c` 包装层换成 `exec tmux -L <socket> -f
|
|
404
432
|
<conf> new-session -A -s dsh-<名>`(cwd 由 node-pty spawn 继承);SSH 则
|
|
405
|
-
远程 `exec tmux -L
|
|
406
|
-
pty channel
|
|
433
|
+
远程 `exec tmux -L <socket> -f /dev/null new-session -A -s dsh-<名>` 开
|
|
434
|
+
pty channel(`<socket>` = `tmuxSocketName()`,0.25.0 起按 profile 区分)。`-A` attach-or-create:宿主重启后重开标签按同名接回原
|
|
407
435
|
tmux 会话,正在跑的程序、pane 状态原样恢复;
|
|
408
436
|
- **恢复链路**:浏览器重连后查 `sessions`——持久标签 sid 已失效的,客户端
|
|
409
437
|
自动按原 persistName 重新 spawn(非持久标签维持丢弃语义);保活回收器
|
|
@@ -461,6 +489,15 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
|
|
|
461
489
|
`user-env`,每次连接重新解析),它没有才退回宿主进程环境变量(配合 dsh-env-manager 插件托管
|
|
462
490
|
密钥,避免明文写进 settings 文件);解析不到时报错会同时点到"引用名"与"两处都没有";
|
|
463
491
|
- **端口**:默认 22,非 22 端口在 target 里显示为 `user@host:port`;
|
|
492
|
+
- **主机密钥别名(`hostKeyAlias`,0.25.0)**:TOFU 的记录/比对默认按**连接地址**定位,
|
|
493
|
+
而同一台真实主机常常经不同地址触达——跳板机后面写内网 IP、或经端口转发落在
|
|
494
|
+
`127.0.0.1:2222`。那时「同一把钥匙、不同记录」会让每次切换地址都判成**指纹变更**
|
|
495
|
+
(假 MITM 告警并**拒绝连接**),而用户的 `known_hosts` 里本来就只有一条。填同一个别名
|
|
496
|
+
即可让这些地址共用一条主机密钥记录。语义与 OpenSSH 的 `HostKeyAlias` 对齐:**只决定
|
|
497
|
+
指纹记在哪条记录里,不改连接地址**;留空 = 按地址定位(与从前逐字一致)。
|
|
498
|
+
`~/.ssh/config` 里的 `HostKeyAlias` 会**照原样导入**(它是纯本地的定位开关,不含可执行
|
|
499
|
+
成分,信任级与 `ProxyCommand` 完全不同)。四个连接点(终端 / SFTP / 隧道 / 试连探针)
|
|
500
|
+
共用同一份键推导,所以「试连说匹配、真连说变更」这种自相矛盾不会出现;
|
|
464
501
|
- **标签与状态**:SSH 标签标题用连接名或 `user@host`(本地标签是
|
|
465
502
|
「终端 N」);连接中先回显灰字 `Connecting user@host …`,就绪后状态栏
|
|
466
503
|
显示 `SSH user@host 已连接`;连接失败(连接超时 / 认证被拒 / 主机
|
|
@@ -476,7 +513,8 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
|
|
|
476
513
|
失效。连接簿条目随 `agentForward` 保存,列表里显示 `· fwd`;
|
|
477
514
|
- **`~/.ssh/config` 导入(0.4.0)**:设置卡片连接簿区「从 ~/.ssh/config
|
|
478
515
|
导入」——解析 `HostName/User/Port/IdentityFile` 生成候选条目(跳过通配符
|
|
479
|
-
块与无 User 条目,`Include`
|
|
516
|
+
块与无 User 条目,`Include` 不展开),同名跳过,随「保存」写入;`HostKeyAlias` 也在
|
|
517
|
+
映射之列(0.25.0)。
|
|
480
518
|
**每一种跳过都会当场说明**(本轮起):依赖跳板机的块(`ProxyJump` / `ProxyCommand`)
|
|
481
519
|
**不导入并点名**——本版本不支持跳板机,导进来只会得到一条连不上、且只报通用超时的条目;
|
|
482
520
|
超过导入上限的条数与「非具体主机」的条数也各自报数。`ProxyJump none` /
|
|
@@ -642,7 +680,7 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
|
|
|
642
680
|
| `colorTerm` | `truecolor` | COLORTERM 值 |
|
|
643
681
|
| `cwd` | 宿主启动目录 | 兜底工作目录(客户端当前会话 cwd 优先) |
|
|
644
682
|
| `reconnectGraceSec` | 120 | 异常断开后会话保活秒数(0~3600):刷新页面/网络抖动后会话存活等待重连,超时由回收器结束;`0` = 旧行为,断开立即结束 |
|
|
645
|
-
| `sshHosts` | `[]` | SSH 连接簿(面板「+」菜单可选):条目 `{name, host, port=22, username, auth=agent\|key\|password, keyPath, passphrase, password, agentForward, persist=false}`;保存时整体替换、同名覆盖;`password` / `passphrase` 支持 `env:VAR` 引用,避免明文入库;持久化开启时条目点击默认以 tmux 持久会话打开,`persist=false`
|
|
683
|
+
| `sshHosts` | `[]` | SSH 连接簿(面板「+」菜单可选):条目 `{name, host, port=22, username, auth=agent\|key\|password, keyPath, passphrase, password, agentForward, hostKeyAlias='', persist=false}`;保存时整体替换、同名覆盖;`password` / `passphrase` 支持 `env:VAR` 引用,避免明文入库;持久化开启时条目点击默认以 tmux 持久会话打开,`persist=false` 是**取消项**;`hostKeyAlias` 空串 = 按主机地址定位指纹(TOFU),填了则按别名定位(同一台主机经不同地址触达时共用一条记录,见「SSH 连接」一节) |
|
|
646
684
|
| `hostKeys` | `[]` | SSH 主机指纹记录(TOFU,自动维护):条目 `{host, port, fingerprints[]}`(旧版单 `fingerprint` 字段读取时自动迁移合并);按 host:port 唯一,一机多把钥匙共用一条记录,首次连接自动追加、任一指纹匹配放行、全部不匹配拒绝连接;设置卡片可删除重置 |
|
|
647
685
|
| `shellIntegration` | true | 注入 OSC 133/7 shell 集成(命令边界标记 + cwd 上报;`tty_capture{last}` 依赖它);zsh/bash 支持,其他 shell 自动跳过;出兼容问题时可关闭 |
|
|
648
686
|
| `tunnels` | `[]` | 端口转发隧道:条目 `{name, bookName, direction=local\|remote, localPort?, remoteHost?, remotePort?, localTargetHost?, localTargetPort?, enabled}`;`bookName` 引用连接簿条目提供主机与认证;卡片「端口转发」区块可视化维护 |
|
|
@@ -969,7 +1007,7 @@ node scripts/preview.mjs --theme=light # 浅色主题
|
|
|
969
1007
|
`PROMPT_COMMAND`/钩子数组,集成可能失效——可关闭 `shellIntegration` 或
|
|
970
1008
|
反馈补丁兼容。
|
|
971
1009
|
- **端口转发边界**:本地监听固定 127.0.0.1(不暴露局域网);remote 方向服务端监听还受服务端 sshd `GatewayPorts` 限制;隧道的 SSH 连接与终端会话独立,均走 TOFU 钉扎与连接簿认证;隧道规格变更(端口/目标/启停)经「保存」热生效,热改连接簿凭证则在下次重连时生效。
|
|
972
|
-
- **会话持久化(tmux)边界**:持久标签由 tmux server(专用 socket `dsh-tty
|
|
1010
|
+
- **会话持久化(tmux)边界**:持久标签由 tmux server(专用 socket,0.25.0 起按 profile 区分:`tmuxSocketName()` = 无 profile 时 `dsh-tty`、有 profile 时 `dsh-tty-<profile>`)托管——宿主被硬杀 / 保活回收 / 浏览器丢失标签规格时,tmux 会话会**留存**(这正是恢复能力的前提),直到机器重启或手动 `tmux -L <socket> kill-server`;agent 命令粒度工具(capture{last}/expect)依赖 tmux ≥3.3 的 DCS `allow-passthrough`,更低版本持久化可用但该能力降级(SSH 远程会话不注入 shell 集成钩子,capture{last} 本就不可用,与持久化无关);恢复接回重画的是当前可见屏,断线前的滚动历史在 tmux 自己的 history buffer(copy-mode)里,不在外层 xterm 滚动区;持久标签的 `exit` 帧退出码是 tmux 客户端的(0),shell 退出码经 OSC 133;D 标记照常可用;`tmux.conf` 只在 tmux server 首启时读取(改配置后需 `tmux -L <socket> kill-server` 让下次 spawn 重建 server;`<socket>` 就写在生成的 `tmux.conf` 注释里);`grace=0` 的「断开立即结束」对持久标签同样会 kill-session(tmux 会话不存活);持久 SSH 会话要求远程装有 tmux(未装自动降级普通会话,连接栏常驻「未持久化」标记),且远程 `~/.tmux.conf` 不影响专用 socket 的独立 conf(`-f /dev/null`);SSH 持久会话名随 settings 留存;**两个窗口同时接回同一持久会话**时共享同一个宿主 PTY(0.10.1,单客户端扇出,名额不翻倍),两侧行数以最近调整尺寸的窗口为准(尺寸不一致时较大一侧由 onResize 自愈重画)。
|
|
973
1011
|
- **SFTP 边界**:文件权限 = 对应 SSH 账号的终端权限(无额外沙箱/chroot);下载经浏览器内存(超大文件建议终端 `scp`/`rsync`);agent 工具 `sftp_read` ≤1MB 且拒绝二进制、`sftp_write` 单次 ≤1MB(大内容走面板上传或终端);`sftp_*` 工具只收连接簿条目名,内联凭证仅供面板对话框使用;覆盖写走同目录临时分片 `.dsh-part-<uuid>` + rename 原子落盘——宿主进程崩溃 / 断电时可能留下分片孤儿(下次向同目录覆盖上传时会自动清理超过 24h 的残留)。
|
|
974
1012
|
- **SSH host key 为 TOFU 钉扎**:首次连接自动记录 sha256 指纹(trust on
|
|
975
1013
|
first use),之后任一记录指纹匹配放行、全部不匹配拒绝——不再是无条件放行的
|
|
@@ -1012,7 +1050,7 @@ node scripts/preview.mjs --theme=light # 浅色主题
|
|
|
1012
1050
|
│ 残包 carry)→ 命令边界捕获(tty_capture{last} / tty_expect 早停)
|
|
1013
1051
|
│ 与 cwd 跟随(tty_list)
|
|
1014
1052
|
├─ 本地路径:ctx.get('subprocess').spawnTerminal({ argv: shell -c 包装层, cwd })
|
|
1015
|
-
│ 持久标签(0.10.0):包装层换 `exec tmux -L
|
|
1053
|
+
│ 持久标签(0.10.0):包装层换 `exec tmux -L <socket> -f <conf> new -A -s dsh-<名>`
|
|
1016
1054
|
│ (src/tmux.ts:探测/资产生成/spawn 计划/kill-session;稳定资产在
|
|
1017
1055
|
│ <DSH_HOME|~/.dsh>/tty/——tmux.conf + inner.sh + zsh/bash 桩;tmux 会吞
|
|
1018
1056
|
│ 不认识的序列,shell 集成钩子检测 $TMUX 把 OSC 133/7 包 DCS passthrough
|
|
@@ -1042,7 +1080,7 @@ SSH 路径 (src/ssh.ts)
|
|
|
1042
1080
|
TOFU 钉扎),开 shell channel 包装成与 PTY 同形状的 TermHandle
|
|
1043
1081
|
(pid=null,kind='ssh',target=user@host[:port]),
|
|
1044
1082
|
背压一并透传到 channel —— 之后与本地 PTY 无差别调度;
|
|
1045
|
-
persist 时先 `command -v tmux` 探测再 `exec tmux -L
|
|
1083
|
+
persist 时先 `command -v tmux` 探测再 `exec tmux -L <socket> -f /dev/null
|
|
1046
1084
|
new -A -s dsh-<名>` 开 pty channel(远程 tmux 托管;无 tmux 降级普通
|
|
1047
1085
|
shell channel + 灰字提示;kill 经连接内 `tmux kill-session` 收尾)
|
|
1048
1086
|
```
|