@hyzyn/dsh-tty 0.20.2 → 0.22.0-rc.1
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 -4
- package/README.md +76 -6
- package/client.js +135 -36
- package/lib/index.d.ts +51 -0
- package/lib/index.js +507 -99
- package/lib/index.js.map +1 -1
- package/lib/probe.d.ts +29 -1
- package/lib/probe.js +98 -19
- package/lib/probe.js.map +1 -1
- package/lib/sftp.js +66 -22
- package/lib/sftp.js.map +1 -1
- package/lib/ssh-config.d.ts +34 -2
- package/lib/ssh-config.js +150 -32
- package/lib/ssh-config.js.map +1 -1
- package/lib/ssh.d.ts +256 -2
- package/lib/ssh.js +637 -6
- package/lib/ssh.js.map +1 -1
- package/lib/tunnels.d.ts +2 -0
- package/lib/tunnels.js +79 -7
- package/lib/tunnels.js.map +1 -1
- package/package.json +7 -5
- package/scripts/integration.mjs +51 -19
- package/scripts/jump-smoke.mjs +175 -0
- package/scripts/lib/proxy-bridge.mjs +30 -0
- package/scripts/preview/harness.js +74 -26
- package/scripts/probe-route-smoke.mjs +10 -1
- package/scripts/proxycommand-smoke.mjs +267 -0
- package/scripts/windows-smoke.mjs +23 -0
package/README.en.md
CHANGED
|
@@ -97,7 +97,7 @@ Both were reproduced on **Windows 11 ARM (24H2) + Node 22 ARM64**. The fix:
|
|
|
97
97
|
on Windows, and its `_deferNoArgs` re-throws that error from a socket callback, where the caller’s
|
|
98
98
|
try/catch cannot see it.
|
|
99
99
|
|
|
100
|
-
## Agent tools
|
|
100
|
+
## Agent tools
|
|
101
101
|
|
|
102
102
|
The plugin injects sixteen tools into the agent (with the same power as the bash tool; operations show up live in the user’s terminal):
|
|
103
103
|
|
|
@@ -109,7 +109,7 @@ The plugin injects sixteen tools into the agent (with the same power as the bash
|
|
|
109
109
|
| `tty_stats` | Read live host metrics for a session’s machine (0.20.0): CPU / memory / disk / TCP connections / network rates / temperature / uptime. Local sessions report the host; SSH sessions report that remote host over a separate non-PTY channel that never touches the terminal. Check it before deploying or load-testing |
|
|
110
110
|
| `tty_capture` | Read recent output (last N lines, ANSI stripped by default, `raw:true` for the raw stream); **`last:true` returns only the output + exit code of the previous completed command** (shell integration markers, see the next section); when a command is **in flight** (just sent, completion marker not in yet) it returns `inProgress:true` without the stale result, so the previous command is never mistaken for this one (0.19.0) |
|
|
111
111
|
| `tty_screen` | Read the **currently visible screen** as rendered (xterm-headless virtual screen, plain text) — it can genuinely read TUI interfaces such as vim / htop / menus |
|
|
112
|
-
| `tty_expect` | Wait with a regex for a readiness signal
|
|
112
|
+
| `tty_expect` | Wait with a regex for a readiness signal (dev server URL, build finished, …). It **looks back first** at output that has not been read yet (including the full output of “the previous command”, so a command that finished instantly no longer burns the whole timeout) and then waits for subsequent output; on a hit it returns `matched:true` plus `matchedFrom` (`live` produced during this wait / `last` the previous command’s output / `buffered` buffered output that had already arrived). A timeout does not throw (`matched:false` + tail output, and it **does not consume the unread region**, so a different pattern can still look back at the same output), and a command that ends early also returns early with its exit code; the echo never counts as a hit (via the OSC 133 A..B boundary — see the next section for environments without markers); at most 5 in-flight calls per session, and the accumulated window keeps only the last 64KB (0.19.0; look-back matching, see D72) |
|
|
113
113
|
| `tty_send` | Send keys/text to a given session (such as `q` to a dev server, or a menu selection) |
|
|
114
114
|
| `sftp_list` | List a remote SSH directory (name/type/size/mtime, directories first); `book` is the connection-book entry name and `path` defaults to the login home; at most 500 entries by default (`truncated:true` beyond that), and `isSymlink` distinguishes a symlink from a real directory (0.19.0) |
|
|
115
115
|
| `sftp_read` | Read a remote **text** file (≤256KB by default, adjustable to 1MB, truncated beyond that); `offset` pages from a given byte (handy for log tails), an invalid `maxBytes` errors out instead of silently falling back, and binary detection is a double test (NUL + illegal-UTF-8 ratio) (0.19.0) |
|
|
@@ -152,6 +152,30 @@ At spawn time hooks are injected through the existing `-c` wrapper layer accordi
|
|
|
152
152
|
code / `OSC 7 file://…` cwd reporting (`tty_list.cwd` follows `cd`, and SSH sessions report the remote path);
|
|
153
153
|
- Other shells are silently disabled; `shellIntegration: false` turns the whole thing off (escape hatch).
|
|
154
154
|
|
|
155
|
+
**Look-back matching and the read watermark (D72)**: `tty_expect` first looks back at output that has
|
|
156
|
+
**not been read yet** (when a command finishes instantly, the marker you are waiting for is already in the
|
|
157
|
+
buffer). The look-back lower bound is raised to “just after the last `B` marker” inside the unread region —
|
|
158
|
+
the echo sits between `A` and `B`, so text in the echo never counts as a hit (deliberately no text
|
|
159
|
+
comparison here: line wrapping / ANSI / multi-line paste would break it).
|
|
160
|
+
|
|
161
|
+
- **Environments without `B` markers** (integration off / local Windows tabs / a remote host without the
|
|
162
|
+
hooks) cannot tell the echo apart and degrade to “scan from the watermark, possibly matching the echo
|
|
163
|
+
itself” — a known limitation;
|
|
164
|
+
- **The tradeoff in that look-back range**: raising the lower bound to “just after the last `B` marker”
|
|
165
|
+
costs you the unread output that precedes the newest command’s start. It deliberately prefers a miss
|
|
166
|
+
over a false hit — treating the echo as a match would make the agent believe the event just happened.
|
|
167
|
+
Use `tty_capture` for that text. The mirror-image corner exists too: right after `tty_send`, before the
|
|
168
|
+
new command’s `B` marker arrives, the unread region still holds the previous command’s output, so a
|
|
169
|
+
pattern in it matches at once (the agent’s next call almost always lands after that `B`, where this
|
|
170
|
+
text has already been cut off by it);
|
|
171
|
+
- **Read watermark**: whatever `tty_capture` / `tty_expect` returns counts as read, and later
|
|
172
|
+
`tty_expect` calls will not look back at it (use `tty_capture` to re-read older output). `tty_screen`
|
|
173
|
+
deliberately does **not** advance it (the screen is a different representation, not a text stream);
|
|
174
|
+
`tty_send` only initialises it (the output of the command just sent has not been seen yet); **a timeout
|
|
175
|
+
does not consume the unread region** either (another pattern can still look back at the same output).
|
|
176
|
+
A tab the user opened gets its watermark placed at the moment the agent first touches it — history
|
|
177
|
+
before that does not count as unread.
|
|
178
|
+
|
|
155
179
|
SSH sessions are scheduled on the same table: entries with `kind: 'ssh'` in `tty_list` are identified by
|
|
156
180
|
`target` (user@host[:port]), and `tty_capture` / `tty_expect` / `tty_send` are used exactly as for local
|
|
157
181
|
sessions — dev server logs and key interactions on the remote machine remain available as usual.
|
|
@@ -248,6 +272,11 @@ references one connection-book entry (host and authentication come with it), in
|
|
|
248
272
|
SSH connection), and tunnels keep running with the panel closed; SSH disconnects reconnect automatically
|
|
249
273
|
with exponential backoff (1s→15s cap), and the remote direction re-runs forwardIn after a reconnect; after
|
|
250
274
|
the connection-book password changes, a reconnect uses the new credentials automatically;
|
|
275
|
+
- **Profiles running side by side must offset their localPort**: port forwarding is a **machine-level**
|
|
276
|
+
resource, while the configuration is stored per profile (copying a profile copies its tunnels too). Two
|
|
277
|
+
profiles running the same tunnel at the same time leave the later one with `EADDRINUSE`, stuck in
|
|
278
|
+
`error` with `fatal:true` (**no retry**; changing the configuration rebuilds it from the new spec). The
|
|
279
|
+
error message names “possibly the host process of another DSH profile” and offers two ways out;
|
|
251
280
|
- **Status badges**: while the card is expanded it polls live status every 2s (active green/connecting
|
|
252
281
|
blue/error red/stopped grey + last error); connection-book entries in the “+” menu show a `⇄N` tunnel
|
|
253
282
|
badge; the agent can query status with the `tunnel_list` tool;
|
|
@@ -455,7 +484,22 @@ and the agent tools all reuse the same scheduling.
|
|
|
455
484
|
`agentForward` and show `· fwd` in the list;
|
|
456
485
|
- **`~/.ssh/config` import (0.4.0)**: “Import from ~/.ssh/config” in the connection-book area of the settings
|
|
457
486
|
card — parses `HostName/User/Port/IdentityFile` into candidate entries (skipping wildcard blocks and
|
|
458
|
-
entries without a User; `Include` is not expanded), skips same names, and writes them on “save
|
|
487
|
+
entries without a User; `Include` is not expanded), skips same names, and writes them on “save”.
|
|
488
|
+
**Every skip is now reported** (this round): blocks that depend on a jump host (`ProxyJump` /
|
|
489
|
+
`ProxyCommand`) are **not imported and are named** — this version does not support jump hosts, and
|
|
490
|
+
importing one would only produce an entry that cannot connect and reports a generic timeout; the
|
|
491
|
+
over-limit count and the “not a concrete host” count are reported too. `ProxyJump none` /
|
|
492
|
+
`ProxyCommand none` mean an explicit direct connection and are imported as usual.
|
|
493
|
+
**`ProxyJump` is resolved into the entry's jump host (single hop) and imported with it**: the
|
|
494
|
+
value may be `user@host:port` (IPv6 as `[::1]:22`) or another concrete Host in the same config
|
|
495
|
+
(resolved by block name, in any order); the jump host **inherits the target hop's credentials**
|
|
496
|
+
by default, so set `jump.keyPath` / `jump.password` on the entry when it needs its own.
|
|
497
|
+
Unresolvable ones (missing alias, or an alias that itself needs a jump) are still skipped and
|
|
498
|
+
named; `ProxyCommand` is not supported (it would let a settings field drive arbitrary local
|
|
499
|
+
command execution — a different trust tier, see project ROADMAP item 2). **The dialog also has a “Jump host” section**: one
|
|
500
|
+
`[user@]host[:port]` input (matching OpenSSH), with user / auth / key / passphrase overrides
|
|
501
|
+
revealed only after ticking “use separate credentials”; entry rows show “⇢ via X”, and the
|
|
502
|
+
“Test” button reports the jump hop on its own line;
|
|
459
503
|
- **Credential-reference picker (0.4.0; decoupled from the env plugin since 0.17)**: next to the password /
|
|
460
504
|
passphrase fields in the SSH dialog there is a filter box plus a height-limited list whose candidates are
|
|
461
505
|
**the reference names the credential store already knows** (the host reads the `refs:` keys of
|
|
@@ -597,6 +641,12 @@ ctx.inject(['ttyConnbar'], (c) => {
|
|
|
597
641
|
| `requestRender()` | Asks tty to re-render the connection bar (for when a consumer has new data asynchronously and needs the button to appear immediately) |
|
|
598
642
|
|
|
599
643
|
- It only fires on **SSH tabs**; the connection bar of a local tab is hidden anyway.
|
|
644
|
+
- **Command tabs do not fire it** (`spawnSpec.command` non-empty, i.e. a tab running a command through
|
|
645
|
+
`ttyTerminal.open`, such as dsh-docker’s `docker exec -it …`): those connection-bar extensions act on
|
|
646
|
+
**the connection itself**, and hanging them on a command tab would mislead (SFTP would browse the host,
|
|
647
|
+
not the inside of the container the user has in mind). Built-in and third-party actions are hidden
|
|
648
|
+
together; the reopen entry point for an exited tab is not affected — the terminal body already carries a
|
|
649
|
+
“click to reopen” overlay.
|
|
600
650
|
- A throwing factory is only logged with `console.warn`, without affecting the connection bar or the built-in buttons.
|
|
601
651
|
- The service name `ttyConnbar` is not declared on tty’s `Context` type surface, so consumers can inject it by
|
|
602
652
|
string; when tty is not installed or is older than 0.13.0 the injection never fires, so consumers must treat
|
|
@@ -730,6 +780,22 @@ ctx.inject(['ttyPanel'], (c) => {
|
|
|
730
780
|
- The title bar (title / collapse / ✕) is provided by tty, and consumers only own their own body; a throwing
|
|
731
781
|
`onClose` is only logged with `console.warn`, without affecting closing the panel.
|
|
732
782
|
|
|
783
|
+
**`minimize()` (contract v2)** folds the whole terminal panel away: the modal is hidden, but the DOM /
|
|
784
|
+
WebSocket / xterm buffers are all kept and **the session keeps running**; restoring it goes through the badge
|
|
785
|
+
on the sidebar’s “Terminal” entry. Consumers use it to “give the stage back” — the typical case is dsh-docker
|
|
786
|
+
handing the logs over to the session and folding the terminal away automatically, so the user sees the session
|
|
787
|
+
directly instead of staring at a modal covering it and guessing “did my click do nothing?”. The return value is
|
|
788
|
+
the minimized state after the call, which the caller uses to decide whether its message still needs to say
|
|
789
|
+
“the session is behind the panel”.
|
|
790
|
+
|
|
791
|
+
```js
|
|
792
|
+
ctx.inject(['ttyPanel'], (c) => {
|
|
793
|
+
if (Number(c.ttyPanel.version ?? 0) < 2 || typeof c.ttyPanel.minimize !== 'function') return false
|
|
794
|
+
if (c.ttyPanel.isOpen() !== true) return false
|
|
795
|
+
return c.ttyPanel.minimize() // fold the terminal away so the session shows through
|
|
796
|
+
})
|
|
797
|
+
```
|
|
798
|
+
|
|
733
799
|
> Contract versions: `ttyConnbar.version === 1`, `ttyTerminal.version === 3` (1 = `open` only,
|
|
734
800
|
> 2 = adds `mount`, 3 = `open` reuses an existing live tab for the same connection + command by default),
|
|
735
801
|
> `ttyPanel.version === 2` (1 = `mountPane` + `isOpen`, 2 = adds `minimize`). Consumers **decide capabilities
|
|
@@ -964,7 +1030,13 @@ Host half (src/index.ts)
|
|
|
964
1030
|
│ /api/dsh-tty/credential-refs (reference names known to the credential store — names only, see
|
|
965
1031
|
│ “Credential storage”), /api/dsh-tty/env-vars (variable names managed by the env plugin), /api/dsh-tty/known-hosts
|
|
966
1032
|
│ (TOFU fingerprint prefill, src/known-hosts.ts parses hashed entries too),
|
|
967
|
-
│ /api/dsh-tty/shells (shell path candidates) — all behind the loopback fence
|
|
1033
|
+
│ /api/dsh-tty/shells (shell path candidates) — all behind the loopback fence (hardened
|
|
1034
|
+
│ tier: the whole 127/8 range + DNS confirmation for alias hostnames + source checks before
|
|
1035
|
+
│ DNS; one implementation only, in @hyzyn/dsh-kit); **state-changing endpoints additionally
|
|
1036
|
+
│ require a same-origin proof** (POST /probe, sftp mkdir/rename/remove/upload, local-fs
|
|
1037
|
+
│ mkdir/rename/remove/transfer); read-only endpoints (including sftp /list and /download)
|
|
1038
|
+
│ need no proof, so bare curl still works. POST /config is deliberately exempt — it is the
|
|
1039
|
+
│ only recovery entry once the plugin is disabled
|
|
968
1040
|
├─ SFTP (src/sftp.ts, 0.7.0): lazy connection pool (reclaimed after 120s idle, reconnected on
|
|
969
1041
|
│ demand when dropped, TOFU shared) → POST /api/dsh-tty/sftp/list|mkdir|rename|remove|download|
|
|
970
1042
|
│ upload (spec in body/headers, credentials never in the URL; uploads/downloads streamed via pipe) +
|
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ dsh plugin --profile web add link:$(pwd)/packages/tty # 仓库开发调试
|
|
|
91
91
|
出现的问题——node-pty 在 Windows 上不接受 signal,而它的 `_deferNoArgs` 会把这个异常推迟到
|
|
92
92
|
socket 回调里抛出,调用方的 try/catch 拦不住。
|
|
93
93
|
|
|
94
|
-
## agent
|
|
94
|
+
## agent 工具
|
|
95
95
|
|
|
96
96
|
插件向 agent 注入十六个工具(与 bash 工具同权,操作实时显示在用户终端里):
|
|
97
97
|
|
|
@@ -103,7 +103,7 @@ dsh plugin --profile web add link:$(pwd)/packages/tty # 仓库开发调试
|
|
|
103
103
|
| `tty_stats` | 读会话所在机器的实时指标(0.20.0):CPU / 内存 / 磁盘 / TCP 连接数 / 网速 / 温度 / 在线时长。本地会话取宿主机;SSH 会话取那台远程主机(另开一段非 PTY 通道,不影响终端)。部署、压测前先看它 |
|
|
104
104
|
| `tty_capture` | 读取近期输出(尾部 N 行,默认清洗 ANSI,`raw:true` 取原始流);**`last:true` 只返回上一条已完成命令的输出 + 退出码**(shell 集成标记,见下节);命令**在途**时(刚发送、完成标记未到)返回 `inProgress:true` 且不带旧结果——避免把上一条的输出当成这一条(0.19.0) |
|
|
105
105
|
| `tty_screen` | 读取**当前可见屏幕**的渲染结果(xterm-headless 虚拟屏,纯文本)——能真正读懂 vim / htop / 菜单等 TUI 界面 |
|
|
106
|
-
| `tty_expect` |
|
|
106
|
+
| `tty_expect` | 用正则等一个就绪信号(dev server URL、构建完成等)。**先回溯**还没被读过的输出(含「上一条命令」的完整输出,故命令瞬间跑完也不会白等),再等后续输出;命中即返回 `matched:true` + `matchedFrom`(`live` 本次等待期间新产生 / `last` 上一条命令的输出 / `buffered` 此前已到达的缓冲输出)。超时不抛错(`matched:false` + 尾部输出,且**不消耗未读区**——换个 pattern 还能回溯到同一段),命令提前结束也会带退出码早停;回显不算命中(靠 OSC 133 的 A..B 边界,无标记环境见下节);同一会话在途调用最多 5 个,累积窗口只保留尾部 64KB(0.19.0;回溯匹配见 D72) |
|
|
107
107
|
| `tty_send` | 向指定会话发送按键/文本(如 dev server 的 q 键、菜单选择) |
|
|
108
108
|
| `sftp_list` | 列出 SSH 远程目录内容(名称/类型/大小/修改时间,目录在前);`book` 为连接簿条目名,`path` 缺省为登录 home;默认最多 500 项(超限 `truncated:true`),`isSymlink` 区分软链与真目录(0.19.0) |
|
|
109
109
|
| `sftp_read` | 读取远程**文本**文件(默认 ≤256KB 可调至 1MB,超出截断);`offset` 可从指定字节分页(适合读日志尾部),非法 `maxBytes` 直接报错,二进制判定 = NUL + 非法 UTF-8 占比双判据(0.19.0) |
|
|
@@ -112,7 +112,7 @@ dsh plugin --profile web add link:$(pwd)/packages/tty # 仓库开发调试
|
|
|
112
112
|
| `sftp_rename` | 重命名/移动远程文件或目录(`to` 与 `from` 不同目录即移动;不覆盖已存在的目标) |
|
|
113
113
|
| `sftp_remove` | 删除远程文件/目录;目录默认 rmdir(非空明确报错),`recursive:true` 整树删除(不可恢复);会拒绝 `/`、`~`、含 `.`/`..` 段的路径(不可恢复操作的前置护栏,0.19.0) |
|
|
114
114
|
| `sftp_tree` | 递归列举远程目录结构(深度优先、目录优先;`maxDepth` 1~8 / `maxEntries` 1~2000 限流,超限 `truncated:true`;symlink 不跟随防环) |
|
|
115
|
-
| `tunnel_list` |
|
|
115
|
+
| `tunnel_list` | 列出端口转发隧道及其实时状态(活跃/连接中/错误/停止、规则、连接数);`fatal:true` = 人工介入级故障(本地监听失败 / 连接簿缺失),**不会自动重试**,修配置后重建 |
|
|
116
116
|
|
|
117
117
|
典型 agent 流程(推荐):`tty_open` 开一个会话(长驻进程用 `persistName` 要 tmux 持久化)
|
|
118
118
|
→ `tty_send` 启动命令 → `tty_expect` 等就绪标记 → `tty_capture{last:true}` 拿单条命令结果
|
|
@@ -144,6 +144,24 @@ spawn 时经既有的 `-c` 包装层按 shell 类型注入钩子(对用户透
|
|
|
144
144
|
SSH 会话则上报远程路径);
|
|
145
145
|
- 其他 shell 静默关闭;配置 `shellIntegration: false` 可整体关掉(逃生门)。
|
|
146
146
|
|
|
147
|
+
**回溯匹配与已读水位线(D72)**:`tty_expect` 会先回看**还没被读过**的输出(命令瞬间跑完时
|
|
148
|
+
要等的标记早就在缓冲区里了),回看的起点抬到「未读区内最后一个 `B` 标记之后」——回显落在 `A`
|
|
149
|
+
与 `B` 之间,所以回显里的字样不会被当成命中(这里刻意不做文本比对:折行 / ANSI / 多行粘贴
|
|
150
|
+
都会把它打碎)。
|
|
151
|
+
|
|
152
|
+
- **没有 `B` 标记的环境**(未开集成 / Windows 本地标签 / 远端未装集成)分不出回显,退化为
|
|
153
|
+
「从水位线起扫,可能匹配到回显本身」——这是已知限制;
|
|
154
|
+
- **回看范围的取舍**:起点抬到「未读区内最后一个 `B` 标记之后」,代价是最后一条命令**开始
|
|
155
|
+
之前**的那段未读输出不在回看范围内。刻意选「宁可漏、不可误报」——把回显当成命中会让 AI
|
|
156
|
+
以为事件刚刚发生。要那段内容用 `tty_capture`。反过来的窄窗口也有一个:`tty_send` 之后、
|
|
157
|
+
新命令的 `B` 标记到达之前,未读区里仍然是上一条命令的输出,里面有要等的 pattern 就会立即
|
|
158
|
+
命中(AI 的下一次调用几乎总在新 `B` 之后,那种情况下这段已被新 `B` 切掉);
|
|
159
|
+
- **已读水位线**:`tty_capture` / `tty_expect` 返回的内容算「已读」,之后的 `tty_expect` 不再
|
|
160
|
+
回扫它们(要回看更早的内容用 `tty_capture`)。`tty_screen` 刻意**不**推进(屏幕是另一种
|
|
161
|
+
表示,不是文本流);`tty_send` 只初始化不推进(刚发出去的命令输出还没被看见);**超时也不
|
|
162
|
+
消耗未读区**(换一个 pattern 还能回溯到同一段)。用户自己开的标签在 agent 第一次触达时
|
|
163
|
+
才把水位线落在当下——那之前的历史输出不算「未读」。
|
|
164
|
+
|
|
147
165
|
SSH 会话同表调度:`tty_list` 里 `kind: 'ssh'` 的条目按 `target`
|
|
148
166
|
(user@host[:port])识别,`tty_capture` / `tty_expect` / `tty_send` 用法与
|
|
149
167
|
本地会话完全一致——远程机器上的 dev server 日志与按键交互照常可用。
|
|
@@ -220,7 +238,13 @@ SSH 会话同表调度:`tty_list` 里 `kind: 'ssh'` 的条目按 `target`
|
|
|
220
238
|
dev server 暴露给远程/内网;
|
|
221
239
|
- **宿主自持生命周期**:隧道与终端标签互相独立(各有各的 SSH 连接),面板
|
|
222
240
|
关了隧道照跑;SSH 断线自动指数退避重连(1s→15s 封顶),remote 方向重连
|
|
223
|
-
后自动重新 forwardIn
|
|
241
|
+
后自动重新 forwardIn;连接簿改密码后重连自动用新凭证。**例外**:本地监听失败
|
|
242
|
+
(端口被占等)/ 连接簿条目缺失是人工介入级故障——状态停在 `error` 且
|
|
243
|
+
`fatal:true`、**不会自动重试**,改配置(或恢复条目)后按新规格重建(见 DEFECTS D58);
|
|
244
|
+
- **多 profile 同跑要错开 localPort**:端口转发是**机器级**资源,而配置按 profile
|
|
245
|
+
各存一份(复制 profile 会连隧道一起拷走)。两个 profile 同时跑同一条隧道 → 后起的
|
|
246
|
+
那个 `EADDRINUSE`,状态停在 `error` 且 `fatal:true`(**不重试**,改配置后按新规格
|
|
247
|
+
重建);报错文案会直接点明「可能是另一个 DSH profile 的宿主进程」并给出两条出路;
|
|
224
248
|
- **状态徽标**:卡片展开期间 2s 轮询实时状态(活跃绿/连接中蓝/错误红/停止
|
|
225
249
|
灰 + 最近错误);「+」菜单的连接簿条目显示 `⇄N` 隧道徽标;agent 可用
|
|
226
250
|
`tunnel_list` 工具查询状态;
|
|
@@ -313,6 +337,10 @@ subsystem,宿主半体 `src/sftp.ts`):
|
|
|
313
337
|
tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离),断线保活
|
|
314
338
|
超时、甚至宿主重启后都能接回:
|
|
315
339
|
|
|
340
|
+
> ⚠️ **socket 是全 profile 共用的**(`tmux -L dsh-tty`,不随 profile 区分)。多 profile
|
|
341
|
+
> 同跑时:`tty_list` 的持久会话清单会**跨 profile** 出现;而「改 tmux 配置后生效」用的
|
|
342
|
+
> `tmux -L dsh-tty kill-server` 会**一并杀掉另一个 profile 的持久会话**。
|
|
343
|
+
|
|
316
344
|
- **入口(0.10.1 简化)**:设置卡片「会话持久化」选 `tmux` 即唯一开关——开启后
|
|
317
345
|
**所有新开的标签默认持久化**:「+」菜单的「本地终端」、连接簿条目点击、
|
|
318
346
|
SSH 连接对话框(「持久会话」默认勾选,单次连接可取消)。不再有单独的
|
|
@@ -399,7 +427,44 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
|
|
|
399
427
|
失效。连接簿条目随 `agentForward` 保存,列表里显示 `· fwd`;
|
|
400
428
|
- **`~/.ssh/config` 导入(0.4.0)**:设置卡片连接簿区「从 ~/.ssh/config
|
|
401
429
|
导入」——解析 `HostName/User/Port/IdentityFile` 生成候选条目(跳过通配符
|
|
402
|
-
块与无 User 条目,`Include`
|
|
430
|
+
块与无 User 条目,`Include` 不展开),同名跳过,随「保存」写入。
|
|
431
|
+
**每一种跳过都会当场说明**(本轮起):依赖跳板机的块(`ProxyJump` / `ProxyCommand`)
|
|
432
|
+
**不导入并点名**——本版本不支持跳板机,导进来只会得到一条连不上、且只报通用超时的条目;
|
|
433
|
+
超过导入上限的条数与「非具体主机」的条数也各自报数。`ProxyJump none` /
|
|
434
|
+
`ProxyCommand none` 是显式直连,照常导入。
|
|
435
|
+
**`ProxyJump` 会解析成连接条目的跳板机(单跳)一起导入**:值可以是 `user@host:port`
|
|
436
|
+
(IPv6 写 `[::1]:22`),也可以是同一份 config 里的另一个具体 Host(按块名解析,块序任意);
|
|
437
|
+
跳板机缺省**继承目标那一跳的凭据**,要不同的钥匙就在条目里填 `jump` 的 `keyPath`/`password`。
|
|
438
|
+
解析不出来的(别名缺失、别名自己又依赖跳板机)仍会跳过并点名。
|
|
439
|
+
**`ProxyCommand` 永不自动导入**(单独报数并说明为什么):它等于让设置字段驱动本地任意
|
|
440
|
+
命令执行,所以只在**手动填写 + 打开开关**之后才会跑(见下一段)。
|
|
441
|
+
**对话框里也有「跳板机」一段**:一个 `[用户@]主机[:端口]` 输入框(与 OpenSSH 写法一致),
|
|
442
|
+
勾「使用独立凭据」才展开用户名 / 认证方式 / 私钥 / 口令;条目行显示「⇢ 经 X」,「试连」
|
|
443
|
+
会把跳板机那一跳单列出来;
|
|
444
|
+
- **代理命令 / ProxyCommand(0.21.0,默认关)**:连接簿条目(或连接对话框)里可填一条
|
|
445
|
+
**本机命令**,它的 stdin/stdout 就是到目标的 SSH 传输——等价于 OpenSSH 的 `ProxyCommand`
|
|
446
|
+
(典型写法 `ssh -W %h:%p bastion`)。支持 `%h` 目标主机、`%p` 端口、`%r` 用户名、`%n` 主机、
|
|
447
|
+
`%%` 字面 `%`,其余 `%X` 原样保留;**代入值只允许字母数字与 `._@:[]-`,含 shell 特殊字符
|
|
448
|
+
直接拒绝执行**(不做「按平台各写一套转义」:命令最终交给 `sh -c` / `cmd /c`,两边规则不同)。
|
|
449
|
+
**闸门**:设置卡片「允许 ProxyCommand(本机执行命令)」**默认关**,它的**提权有两条通道**:
|
|
450
|
+
① **启动环境变量** `DSH_TTY_ALLOW_PROXY_COMMAND=1`(最严档,进程启动时采样,只认继承来的
|
|
451
|
+
`process` 层——写项目 `.env` 或 `~/.dsh/env.yml` **不算**授权);② **就地提权**(免重启):
|
|
452
|
+
卡片上点那个开关 → 面板给出一条「在宿主终端执行」的命令 → 执行后十秒内解锁并替你打开开关。
|
|
453
|
+
HTTP / 界面**不能凭空打开**它(给 `true` 而无授权会被 400 拒绝并说清两条路),但**关掉**永远
|
|
454
|
+
可用(紧急刹车不能依赖重启)。授权是**持久**的(落在 `<DSH home>/dsh-kit/capability-grants.json`,
|
|
455
|
+
0600):宿主重启后直接生效、不再确认,所以每次启动会打一行
|
|
456
|
+
`[dsh-tty] elevation: load capability=… via=file grantedAt=…`,卡片上也显示「已授权 · 时刻」并给
|
|
457
|
+
「撤销宿主授权」入口。**升级影响**:升级前靠界面打开的这个开关会变成关。为什么这么设计:
|
|
458
|
+
回环围栏与同源证明都拦不住跨站页面与页内脚本,而这一档是「配置里写一行就在本机跑命令」——
|
|
459
|
+
配置界面若能凭空提权,这道闸门等于没有(细节见
|
|
460
|
+
[architecture.md § 7](../../docs/architecture.md#7-一条请求经过什么))。关着时携带代理命令的连接
|
|
461
|
+
**明确失败、不退回直连**(直连多半也连不上,还会把配置问题伪装成网络问题),「试连」在
|
|
462
|
+
阶段 0 就返回并点名这个开关。与跳板机同时填时按 OpenSSH 语义**跳板机优先**,并记一条 warn。
|
|
463
|
+
闸门只有一处(本插件 settings):docker 容器面板读同一个开关——连接簿配一次、两处生效;
|
|
464
|
+
代价是代理命令是**本机**执行的,docker 目标也会用这条命令(详见
|
|
465
|
+
[docs/proxyjump-plan.md](../../docs/proxyjump-plan.md) §6.1 的闸门定案表);
|
|
466
|
+
与跳板机一样,**它是另一条独立传输,收尾必须成对**(终端 / SFTP / 隧道 / 探针四条路都在
|
|
467
|
+
同一个收尾入口里 `dispose()`:关传输 + 杀进程组,子进程不会变成常驻孤儿);
|
|
403
468
|
- **凭据引用选择器(0.4.0,0.17 起与 env 插件解耦)**:SSH 对话框的密码/口令字段旁有筛选框 +
|
|
404
469
|
限高列表,候选 = **凭据存储里已有的引用名**(宿主读 `.credentials.yaml` 的 `refs:` 键,
|
|
405
470
|
**只回名字、绝不含值**)∪ **本机连接簿里已经在用的引用名**;点击即填 `env:NAME`,也可手输
|
|
@@ -500,6 +565,7 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
|
|
|
500
565
|
| `persistence` | `off` | 会话持久化:`off` 会话随宿主生死(默认);`tmux` 开启后**所有新开的标签默认由 tmux server 托管**、可跨宿主重启恢复(需本机/远程安装 tmux);SSH 对话框可对单次连接取消 |
|
|
501
566
|
| `endOnPageClose` | `false` | 页面(最后一个连接)断开且保活期结束时,是否连 tmux 持久会话一起结束。默认 `false` = 留存可恢复;`true` = 页面关了就不保活(保活期内刷新仍可无缝接回) |
|
|
502
567
|
| `sftpLimits` | `{maxDownloadMb: 1024, maxUploadMb: 2048, maxUploadFiles: 1000}` | SFTP 传输限制(浏览器侧保护,均为 **0 = 不限**):`maxDownloadMb` 单文件下载上限(超限中止并提示用双栏 `⇦`/终端 scp)、`maxUploadMb` 单文件上传上限、`maxUploadFiles` 一次批量/拖拽上传的文件数上限;大文件请走双栏 `⇨/⇦` 服务端直传(字节不经过浏览器,不占内存) |
|
|
568
|
+
| `allowProxyCommand` | `false` | 允许连接簿条目里的**代理命令(ProxyCommand)在本机执行**。默认关:关着时携带代理命令的连接**明确失败**(不退回直连),`~/.ssh/config` 导入**永不**自动带入该字段。docker 容器面板读同一个开关(连接簿只有一处)。支持 `%h/%p/%r/%n/%%` 展开,代入值走白名单;命令的子进程不会残留(四条连接路径共用同一收尾)。**两条提权通道**:① 启动环境变量 `DSH_TTY_ALLOW_PROXY_COMMAND=1`(判定源是宿主的**启动环境快照**,只认继承来的 `process` 层:写项目 `.env` 或 `~/.dsh/env.yml` **不算**授权);② **就地提权**(免重启):设置卡片点那个开关 → 面板给出「在宿主终端执行」的命令 → 十秒内解锁。HTTP / 界面**不能凭空打开**它(给 `true` 而无授权 400 并说清两条路),但**关掉**永远可用;配置里的 `true` 不算授权。授权持久(`<DSH home>/dsh-kit/capability-grants.json`),重启后直接生效并在卡片上显示时刻与「撤销宿主授权」(见 [architecture.md § 7](../../docs/architecture.md#7-一条请求经过什么)) |
|
|
503
569
|
| `statsEnabled` | true | 服务器状态条(0.17.0):按标签可见性采集并推送 CPU / 内存 / 磁盘 / 在线时长 / TCP 连接数 / 网速 / CPU 温度;关闭即停表(远端 exec channel 一并关闭),保存即热生效 |
|
|
504
570
|
|
|
505
571
|
## 连接栏扩展点(客户端服务 `ttyConnbar`,0.13.0)
|
|
@@ -856,7 +922,11 @@ node scripts/preview.mjs --theme=light # 浅色主题
|
|
|
856
922
|
│ /api/dsh-tty/credential-refs(凭据存储里已知的引用名 —— 只要名字,见「凭据存储」)、
|
|
857
923
|
│ /api/dsh-tty/env-vars(env 插件托管变量名)、/api/dsh-tty/known-hosts
|
|
858
924
|
│ (TOFU 指纹预填充,src/known-hosts.ts 解析含 hashed 条目)、
|
|
859
|
-
│ /api/dsh-tty/shells(Shell 路径候选)——均 loopback
|
|
925
|
+
│ /api/dsh-tty/shells(Shell 路径候选)——均 loopback 围栏(加固档:127/8 全段 +
|
|
926
|
+
│ 别名主机名的 DNS 确认 + 来源检查在 DNS 之前,实现只有一份在 @hyzyn/dsh-kit);
|
|
927
|
+
│ **改状态的端点另需同源证明**(POST /probe、sftp 的 mkdir/rename/remove/upload、
|
|
928
|
+
│ local-fs 的 mkdir/rename/remove/transfer);只读端点(含 sftp /list 与 /download)
|
|
929
|
+
│ 不要求证明,裸 curl 也能读。POST /config 刻意豁免——它是插件被禁用后唯一的恢复入口
|
|
860
930
|
├─ SFTP(src/sftp.ts,0.7.0):懒连接池(空闲 120s 回收、断开按需重连、
|
|
861
931
|
│ TOFU 共用)→ POST /api/dsh-tty/sftp/list|mkdir|rename|remove|download|
|
|
862
932
|
│ upload(spec 走体/头,凭证不进 URL;上传下载流式 pipe)+
|