DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Tty — DSH Plugin for DeepSeek Harness
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins

@hyzyn/dsh-tty

Tty

DSH Web GUI terminal panel plugin: a large “Terminal” dialog in the sidebar, a fully interactive terminal with xterm.js + PTY (Route 1), native SSH remote connections with ssh2 (Solution C), tmux session persistence (0.10.0), the connection bar extension point ttyConnbar (0.13.0), the terminal servi

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add @hyzyn/dsh-tty@0.19.3
READMECompatibilityVersions

Description

DSH Web GUI terminal panel plugin: a large “Terminal” dialog in the sidebar, a fully interactive terminal with xterm.js + PTY (Route 1), native SSH remote connections with ssh2 (Solution C), tmux session persistence (0.10.0), the connection bar extension point ttyConnbar (0.13.0), the terminal service ttyTerminal (0.14.0 open / 0.15.0 mount for in-place embedding), the in-panel mounting point ttyPanel (0.16.0 right-side dock), and a server status bar (0.17.0: based on tag visibility, collects the terminal host’s CPU / memory / disk / uptime /

Compatibility and provenance

Tty is published as @hyzyn/dsh-tty and currently resolves to version 0.19.3. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
web
Release source
npm
Registry updated
9/20/2026

Versions

0.19.3stable
9/20/2026
0.19.2stable
9/19/2026
0.19.1stable
9/19/2026
Show 23 more versionsCollapse versions
0.19.0stable
9/19/2026
0.18.3stable
9/18/2026
0.18.2stable
9/17/2026
0.18.1stable
9/17/2026
0.18.0stable
9/16/2026
0.17.2stable
9/15/2026
0.17.1stable
9/14/2026
0.17.0stable
9/13/2026
0.16.2stable
9/13/2026
0.16.1stable
9/12/2026
0.16.0stable
9/10/2026
0.15.0stable
9/10/2026
0.12.0stable
9/8/2026
0.11.0stable
9/7/2026
0.10.3stable

Related plugins

Loading related plugins…

Latest
0.19.3
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
1.8 MB
Files
51
Surface
web
License
Apache-2.0
Source
npm
GitHub
★ 11
Weekly downloads
1,312
Security scan
✓ v0.19.3 scan passed
Last push
9/20/2026
View source ↗Project homepage ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue
9/7/2026
0.10.2stable
9/6/2026
0.10.1stable
9/5/2026
0.10.0stable
9/5/2026
0.9.0stable
9/3/2026
0.5.0stable
8/30/2026
0.2.1stable
8/30/2026
0.2.0stable
8/29/2026
0.1.2stable
8/29/2026

Related plugins

More verified plugins in developer-tools.

Web App@deepseek-ai/dsh-web-appThe dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)Sdk Minimal@deepseek-ai/dsh-sdk-minimalThe standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessionsSdk App@deepseek-ai/dsh-sdk-appThe dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-baseSubagent Codex@deepseek-ai/dsh-subagent-codexOne-shot Codex subagent provider over the official app-server protocol

README

@hyzyn/dsh-tty

中文 | English

DSH 侧边栏「终端」面板:xterm.js + 真实 PTY 的完整终端,本地与 SSH 一视同仁,长任务可断线保活。

特性

  • 真 PTY + WebGL 渲染:node-pty 真实 PTY,vim / htop / dev server 等 TUI 均可运行;多标签页。
  • 可选 tmux 会话持久化:宿主重启 / 网络抖动后重开即恢复现场;docker exec 这类「命令标签」自动重开。
  • ssh2 原生连接:agent forwarding + 主机指纹 TOFU 钉扎,连接簿统一管理;另有 SFTP 上传下载与 端口转发(-L / -R,断线自动重连)。
  • agent 侧按「命令」粒度取数:shell 集成(OSC 133/7)让 tty_capture{last} / tty_expect 拿到「上一条命令」的输出与退出码,而不是抓屏猜。
  • 对其它插件开放两个客户端服务:ttyConnbar(连接栏动作)与 ttyTerminal(就地开终端);dsh-docker 的「容器 / 终端」按钮即走这两个扩展点。

终端面板:多标签页 xterm 弹窗,工具栏含搜索/清屏/复制/粘贴,标题栏含最小化「—」与关闭 ✕

安装

dsh plugin --profile web add @hyzyn/dsh-tty    # npm 安装(发布后)
dsh plugin --profile web add link:$(pwd)/packages/tty   # 仓库开发调试

装完重启 dsh web,侧边栏出现「终端」入口,点击打开面板;设置 → 插件 → 「终端面板」卡片可改配置(保存即热生效,无需重启)。

使用

  • 打开面板自动创建第一个终端(默认 $SHELL,macOS 上通常是 zsh);
  • 多标签页:标签栏「+」新建终端(0.2.0 起「+」为菜单:本地终端 / SSH 连接簿(条目带 ✎ 编辑)/ SSH 连接…,SSH 见下节)、标签 ✕ 关闭; 双击标签可重命名(重命名随标签持久化,断线恢复后保留);每个标签 独立会话(本地 PTY 或 SSH channel);
  • 工作目录跟随当前 DSH 会话:新标签默认在当前会话工作目录打开 (宿主配置 cwd 作兜底)。0.1.6 起会话列表快照不再带 current(视图选中项搬到了 workspace 域),客户端改看 retainedBy.mainView > 0 认当前会话——只认老字段会让 cwd 恒为空、新标签回落宿主启动目录;
  • 支持 vim / htop / less 等 TUI(TERM 已注入为 xterm-256color);
  • 面板大小变化自动 resize(xterm fit → PTY 原生 resize);
  • Ctrl+F 终端内搜索(Enter 下一个 / Shift+Enter 上一个 / Esc 只关搜索框), 输出中的链接可点击,工具栏提供 清屏 / 复制选中 / 粘贴;
  • 断线自动重连(0.3.0):刷新页面、网络抖动等异常断开后,会话在宿主 保活 reconnectGraceSec(默认 120 秒),客户端指数退避自动重连(封顶 5s), 重连后按 sid attach 回原会话并回放断线期间的输出缓冲;页面刷新后从 sessionStorage 恢复标签列表(宿主侧已结束的会话自动丢弃);
  • WebGL 渲染器(0.3.0):高吞吐输出(build 日志)渲染性能质变;WebGL 上下文丢失(多标签超出浏览器配额等)时自动回退 DOM 渲染器;
  • 最小化(状态并入侧边栏入口):点弹窗外空白处、按 Esc 或标题栏「—」 把面板收起——PTY 会话与输出缓冲保持存活,侧边栏「终端」入口上显示 「运行中/总数」徽标与状态点(有输出时脉冲提示),点击入口即可恢复; 悬浮条 ✕ / 标题栏 ✕ 才真正关闭并结束全部会话;
  • 标题栏 ✕ 关闭面板并结束全部会话(PTY 树级清理;tmux 持久标签同时 kill-session);会话退出后点终端区域可重开;
  • 并发上限默认 4(配置 maxSessions,1~16)。

终端面板设置卡片:shell / TERM / 并发上限等保存即热生效

Windows 宿主(0.18.1,best-effort)

0.18.1 之前,Windows 宿主上的本地终端一条都开不起来,两处都在同一条路径上:

  1. $SHELL 在 Windows 上根本不存在,默认 shell 无条件回落 /bin/zsh → spawn 直接 ENOENT(实测:spawn /bin/zsh -> ENOENT,而 %COMSPEC% 可用);
  2. 启动计划是 POSIX 形状的 -c 'export TERM=…; export COLORTERM=…; exec "$shell"' —— cmd.exe 不认 -c(忽略整行、空跑一场就退出,标签开了就消失),PowerShell 认 -c 但把 export 当不存在的 cmdlet 报错。

两条都在 Windows 11 ARM(24H2)+ Node 22 ARM64 上实测复现,修法:

  • 默认 shell = %COMSPEC%(系统保证存在);要 PowerShell 就在设置卡片把「Shell 路径」 填成 powershell.exe / pwsh.exe 的完整路径。Windows 上候选列表给的是 %COMSPEC% + Windows PowerShell 5.1 + PowerShell 7(装了才有),默认项排最前;
  • Windows 上不做包装层:直接 [shell];PowerShell 家族补 -NoLogo(去掉版权横幅), 不补 -NoProfile —— 用户的 profile 正是别名与函数的来源。TERM / COLORTERM 对 ConPTY 没有意义,也不再注入;
  • 「跑一条命令」的标签(docker exec、agent 起的命令标签)走 cmd /c 或 PowerShell -Command;
  • 不支持的三项(宿主侧自动关掉,设置卡片里写明原因):
    • shell 集成(OSC 133/7):注入靠 POSIX rc 桩 + -c 包装层,cmd / PowerShell 上都不成立, 因此恒关 —— 本地标签的 tty_list.cwd 跟随 cd、tty_capture{last} 与 tty_expect 的命令粒度不可用(远程 Linux / macOS 主机照旧支持);
    • tmux 持久化:Windows 上没有 tmux,本地标签照常打开但不受托管(SSH 到 Linux 主机仍可用);
    • 服务器状态条:本地只拿得到 CPU / 内存 / 在线时长(node:os),磁盘 / TCP / 网速 / 温度 显示「无」(远端 Windows 主机走 PowerShell 那一跳,见「服务器状态条」一节);
  • 验证到什么程度:Windows 11 ARM 上跑过完整安装(dsh plugin add @hyzyn/dsh-all)、九个插件 装载、dsh web 起服务与浏览器半体交付(与 macOS 上服务的是同一份产物,构建后比对大小与 内容标记一致;不写死字节数——每次重建都会变)、[dsh-tty] mounted (shell=C:\WINDOWS\system32\cmd.exe);x64 Windows 由 CI 的三平台矩阵覆盖(build / typecheck / test,见「开发」)。
  • 端到端覆盖(0.19.0):scripts/windows-smoke.mjs 在 CI 的 windows-latest 上跑真实 cmd.exe 的 spawn → 输入回显 → kill → 重开。它抓出并钉住了「强杀本地 PTY 会崩宿主」这类只在 Windows 出现的问题——node-pty 在 Windows 上不接受 signal,而它的 _deferNoArgs 会把这个异常推迟到 socket 回调里抛出,调用方的 try/catch 拦不住。

agent 工具(P1)

插件向 agent 注入十三个工具(与 bash 工具同权,操作实时显示在用户终端里):

工具作用
tty_list列出活跃终端会话(sid / kind(local|ssh)/ target / pid / cwd 实时跟随 cd / 活动时间;tmux 持久会话带 persist 标记)
tty_capture读取近期输出(尾部 N 行,默认清洗 ANSI,raw:true 取原始流);last:true 只返回上一条已完成命令的输出 + 退出码(shell 集成标记,见下节);命令在途时(刚发送、完成标记未到)返回 inProgress:true 且不带旧结果——避免把上一条的输出当成这一条(0.19.0)
tty_screen读取当前可见屏幕的渲染结果(xterm-headless 虚拟屏,纯文本)——能真正读懂 vim / htop / 菜单等 TUI 界面
tty_expect用正则等待后续输出中的就绪信号(dev server URL、构建完成等);超时不抛错(matched:false + 尾部输出),命令提前结束也会带退出码早停;同一会话在途调用最多 5 个,累积窗口只保留尾部 64KB(0.19.0)
tty_send向指定会话发送按键/文本(如 dev server 的 q 键、菜单选择)
sftp_list列出 SSH 远程目录内容(名称/类型/大小/修改时间,目录在前);book 为连接簿条目名,path 缺省为登录 home;默认最多 500 项(超限 truncated:true),isSymlink 区分软链与真目录(0.19.0)
sftp_read读取远程文本文件(默认 ≤256KB 可调至 1MB,超出截断);offset 可从指定字节分页(适合读日志尾部),非法 maxBytes 直接报错,二进制判定 = NUL + 非法 UTF-8 占比双判据(0.19.0)
sftp_write写远程文本文件(默认覆盖,append:true 追加;单次 ≤1MB)
sftp_mkdir创建远程目录;parents:true 逐级补齐缺失父目录(等效 mkdir -p,自底向上创建,已存在目录幂等跳过)
sftp_rename重命名/移动远程文件或目录(to 与 from 不同目录即移动;不覆盖已存在的目标)
sftp_remove删除远程文件/目录;目录默认 rmdir(非空明确报错),recursive:true 整树删除(不可恢复);会拒绝 /、~、含 ./.. 段的路径(不可恢复操作的前置护栏,0.19.0)
sftp_tree递归列举远程目录结构(深度优先、目录优先;maxDepth 18 / maxEntries 12000 限流,超限 truncated:true;symlink 不跟随防环)
tunnel_list列出端口转发隧道及其实时状态(活跃/连接中/错误/停止、规则、连接数)

典型 agent 流程(推荐):tty_send 启动长任务 → tty_expect 等就绪标记 → tty_capture{last:true} 拿单条命令结果。此外 systemPrompt 里注册了动态 context,每轮对话自动携带活跃终端快照(sid / kind / cwd),无需先调 tty_list 也有上下文。

shell 集成(OSC 133/7,0.4.0)

spawn 时经既有的 -c 包装层按 shell 类型注入钩子(对用户透明,不改 rc):

  • zsh:ZDOTDIR 指向临时桩目录(VS Code 同款方案),桩文件先 source 用户原 rc 再追加 precmd/preexec 钩子;
  • bash:--rcfile 桩(先 source ~/.bashrc);命令开始标记按版本二选一: bash ≥ 4.4 走 PS0;bash < 4.4(macOS 自带 3.2)无 PS0,用 DEBUG trap 兜底(handler 按 $BASH_COMMAND 过滤掉 PROMPT_COMMAND 机制自身的 fire,避免幻影标记把用户输出切出捕获区间;bash 3.2 的 trap - DEBUG 在 handler 内卸载不生效,故按「永久武装 + 过滤」设计)。 副作用:循环体等复合命令的内部命令会多发 B 标记,仅影响 tty_capture{last} 对这类命令的截取起点,D/退出码与 tty_expect 不受影响; PROMPT_COMMAND 挂钩兼容字符串与数组(bash 5.1+)两种形态;
  • 标记语义:133;A prompt 开始 / 133;B 命令开始 / 133;D;<exit> 命令 结束带退出码 / OSC 7 file://… cwd 上报(tty_list.cwd 跟随 cd, SSH 会话则上报远程路径);
  • 其他 shell 静默关闭;配置 shellIntegration: false 可整体关掉(逃生门)。

SSH 会话同表调度:tty_list 里 kind: 'ssh' 的条目按 target (user@host[:port])识别,tty_capture / tty_expect / tty_send 用法与 本地会话完全一致——远程机器上的 dev server 日志与按键交互照常可用。

凭据存储(连接密码不必只落明文)

连接对话框的「密码」下面有一行 凭据存储:勾上「保存时存入凭据存储」,密码就写进 官方凭据存储,字段里只留 env:NAME 引用(值在 ~/.dsh/.credentials.yaml,不进环境、 不回传浏览器)。这个勾选框默认就是勾上的(宿主提供了 remote.credentials 时):输明文 再保存 → 值进存储、设置里只留引用,比把密码明文写进设置文件更好(设置会被送到浏览器,凭据 存储的值永不回传);宿主没有这个服务时它会被拨回未勾 + 禁用,并说明只能明文保存。

  • 跟着保存走,不额外点一次:勾选后由「保存修改」/「连接(并保存)」执行写入——没有 "存了但没保存"的悬空引用。没勾「保存到连接簿」时不会动存储(那时没有配置可依附, 只会留下一个没人引用的孤儿)。设置卡片的行内编辑是同一行、同一个默认值,只是提交入口 叫「应用」(那张卡片的行级提交就是「应用」,随后随卡片「保存」落盘),所以勾选框文案是 「应用时存入凭据存储」;代价同一个:值先落存储,若此后放弃「保存」,存储里会留下一个 尚未被引用的名字(引用选择器里可见、可清)。

  • 默认勾选的两个副作用(知道就不会意外):① 编辑一条明文密码的老连接、只改别的字段 再点「保存修改」,密码也会被搬进存储(设置里换成引用)——不想搬就取消勾选;② 存储拒绝 写入时(典型是引用被只读源遮蔽)保存会被中止并把官方的原文错误显示出来,而不是偷偷 改成明文落盘(那个引用名带 DSH_TTY_ 前缀 + 哈希,实际几乎不可能被遮蔽)。

  • 布局克制:明文态这一行只有一个勾选框;字段已是引用时才换上「清除已存凭据」与状态 (已存入(来源 file)· 引用名)。完整的安全边界写在勾选框的悬停说明里,不铺在对话框里。

  • 模型:配置持引用、值归存储。与 SecureCRT 的「命名凭据集按标题引用」、iTerm2 的 「按名从密码管理器取」是同一派;实现走 DSH 官方的 ctx.remote.credentials (describe / set / unset,值单向写入、没有任何读路径),与官方设置卡片同款。

  • 名字(规则恒定):DSH_TTY_<用户名>_<主机>[_<端口>]_<字段>,端口留空或 22(默认)时省略 —— 与连接侧的 spec.port ?? 22 一致(留空即 22),所以"留空 / 22 / " 22 ""三种写法归成同一个键, 同一个账号不会有两个名字、同一个密码不会存两份;非默认端口进键(同一主机不同端口常是 NAT 后面的 不同盒子)。字段 = PASSWORD / PASSPHRASE;例 hsadmin@192.0.2.10:22 → DSH_TTY_HSADMIN_192_168_80_248_PASSWORD。按资源身份派生、不带哈希——与 git-credential-store 的 protocol://username@host、docker credential helpers 的 ServerURL + Username 同一派:host 与 username 本来就是 ASCII 标识符,不需要清洗、 也就不需要哈希兜底。曾经的哈希版是在补救"把人类标签清洗成键":引用文法只认 ASCII, HS 248 / lab-a / HS_248 折出来完全一样,只能靠哈希避免静默覆盖 ✗。资源身份没有这个 死结——撞名只可能发生在同一主机、同一用户、同一端口,而那本来就该是同一个密码(共享是 正确行为)。连接名完全不参与键,所以改连接名/改备注都不会换键。空主机或空用户名则拒绝 存入(键的全部来源,缺一就退化成常量)。代价是可读性弱于人类标签:本对话框的「存入」只按 上面的规则命名,不接受自定名字 ✗;要复用一个自定义名字,就在字段里直接手输 env:名字 (前提是那个名字已在凭据层有值——例如由官方设置卡片 / env 卡片托管)。

  • 派生只发生在「存入」那一刻:存完以后配置里那个 env:NAME 就是唯一事实来源,没有任何地方 会再派生一次——所以改连接名不会让已存的值失效,也不会留下孤儿(旧哈希版才会:连接名一进 键,改名后重新存入就会留下旧名字。要清就按字段里的引用用「清除已存凭据」清掉)。

  • 共享:引用是扁平命名空间,任何按同一套解析的消费方都能用——例如填进 dsh-docker 目标的 password,同一个密码两处复用。

  • 可见性:对话框的引用选择器会列出存储里已有的名字——宿主侧 /api/dsh-tty/credential-refs 只读 的键名回来(只要名字,值永远不出宿主)。为什么要自己 读:引用半边在协议上(原因写在选择器那节),而"我存过哪些名字"正是这个选择器要回答 的问题。这也让(例如改过命名规则后留下的旧名字)重新可见、可选、可清。

端口转发(0.5.0)

设置 → 插件 → 终端面板 卡片的「端口转发」区块维护隧道;每条隧道引用一条 连接簿条目(主机与认证随之),两个方向:

  • 本地转发(-L):本地 127.0.0.1:localPort 监听 → 经 SSH 在服务端侧 连到 remoteHost:remotePort——把远程数据库/内部服务映射到本地(最高频 用法:localPort=5432 → db.internal:5432);
  • 远程转发(-R):服务端监听 remoteHost:remotePort(缺省 127.0.0.1)→ 入站连接拨回本地 localTargetHost:localTargetPort——把本机 dev server 暴露给远程/内网;
  • 宿主自持生命周期:隧道与终端标签互相独立(各有各的 SSH 连接),面板 关了隧道照跑;SSH 断线自动指数退避重连(1s→15s 封顶),remote 方向重连 后自动重新 forwardIn;连接簿改密码后重连自动用新凭证;
  • 状态徽标:卡片展开期间 2s 轮询实时状态(活跃绿/连接中蓝/错误红/停止 灰 + 最近错误);「+」菜单的连接簿条目显示 ⇄N 隧道徽标;agent 可用 tunnel_list 工具查询状态;
  • TOFU 与终端会话共享同一份 hostKeys 钉扎;端口不占用 maxSessions 名额。

SFTP 文件传输(0.7.0,0.8.0/0.9.0 增强)

不动终端、不占会话名额,直接对 SSH 连接做远程文件操作(ssh2 的 sftp subsystem,宿主半体 src/sftp.ts):

SFTP 单窗体(0.16.0 起挂在终端下方抽屉):远程目录浏览,行内 下载/重命名/删除

SFTP 双栏(0.9.0;0.16.0 起挂在终端下方抽屉):左本机 / 右远程,行内 ⇨/⇦ 服务端直传

  • 落点(0.16.0):终端面板开着且本标签的挂载位空着时,文件浏览挂在终端下方——路径栏 / 列表 / 传输进度占满整宽(文件列表是横向宽表,下方全宽比右侧窄栏好用,终端也保住 宽度不会折行;双栏两栏并排更需要这个宽度),终端继续可见可用。高度可拖、可折叠成 一条标题栏(折叠不关面板、不中断浏览);同一标签的挂载位已被别的面板(如容器面板) 占用、或面板没开时,退回原来的居中对话框,不会把别人的面板挤掉。标题 / 折叠 / ✕ 由 tty 的挂载位提供;
  • 跟着标签切(0.19.0):文件浏览连的是打开它的那个标签那台主机,所以它归属该标签: 切到别的标签时整块收起(在途传输照跑,切回来还在原地),标签关掉时一并收掉。所有入口 用同一条规则——连接栏「SFTP」、连接簿条目的 📂、SSH 对话框 / 设置卡片的「文件浏览」都 归属打开那一刻的活动标签;只有「面板开着但一个标签都没有」才算不隶属任何标签(永远可见)。 此前不认归属,切完标签面板还停在原处——标题写着 A 主机、底下活动标签是 B,最坏会往错主机上传;
  • 入口:① 标签栏「+」菜单的连接簿条目带 📂(按该条目打开文件浏览); ② SSH 连接对话框填好主机/认证后点「文件浏览」(不落连接簿也能浏览); ③ SSH 标签的连接栏「SFTP」按钮(归属该标签,见上一条);
  • 操作:目录浏览(路径框回车跳转、..(上级目录)、单击文件即下载)、 上传(多选文件,XHR 流式 + 进度百分比;0.8.0 起支持拖拽——文件与 文件夹直接拖入对话框,文件夹经 webkitGetAsEntry 递归展开逐个上传,目录 用 mkdir parents 逐级补齐)、下载(POST → 浏览器 Blob → <a download>)、新建目录、重命名(行内编辑器)、删除 (🗑 二次点击确认,目录带 recursive 整目录删除);
  • 传输进度条(0.11.0):底部状态行右侧新增细进度条 + 百分比——上传按 XHR 流式进度(多文件带 i/n · 文件名 标签);下载改为流式读 response.body,按响应 content-length 实时算百分比(无长度时降级为已传 字节文本);
  • 取消传输(0.12.0):进度条右侧 ✕——上传中断在途 XHR(批次里剩余 文件一并跳过);下载用 AbortController 打断流式读取;双栏 ⇨/⇦ 直传改为服务端任务化(start 返回 jobId → 400ms 轮询真实字节进度 → ✕ 打 cancel 中止),不再是一个打不断的同步 HTTP 请求。取消后半截文件自动 删除(上传的远端残留文件 / 下载的本机残留文件;清理失败只记日志),状态行 走「已取消」而非红色失败态;关闭文件浏览对话框也会收掉在途传输,不留 「看不见但还在写远端」的后台搬运;
  • 连接管理:懒连接池——首次操作才建 SSH 连接,空闲 120 秒自动回收, 断开后下次操作自动重连;连接簿条目在每次(重)连接时实时解析(改密码 后自动用新凭证);TOFU 与终端会话/隧道共用同一份 hostKeys 钉扎,指纹 变同样拒绝;SFTP 不计入 maxSessions 名额;
  • 传输通道:POST /api/dsh-tty/sftp/list|mkdir|rename|remove|download| upload(全部 loopback 围栏)。连接规格经 JSON 体(连接簿名或内联字段, 与 WS ssh 帧同语义「条目作基底 + 内联逐项覆盖」)或 upload 的 x-dsh-sftp-meta 头(base64url)携带——凭证不进 URL/查询串;上传下载 均为流式 pipe,不整文件进内存;
  • agent 工具:sftp_list / sftp_read / sftp_write / sftp_mkdir / sftp_rename / sftp_remove / sftp_tree(见上表)——只收 book 连接簿条目名,不接受内联凭证(agent 上下文不进明文密钥);
  • 双栏风格(0.9.0 可选,配置 ):左本机 / 右远程两栏——本机 侧浏览与文件操作走新增的 路由(list/mkdir/rename/ remove,loopback 围栏);行内 把条目对拷到对面栏的当前目录 ( 由宿主服务端把两个路径流式直传,目录 递归、同名覆盖,);单窗体风格照旧,设置卡片切换, 重新打开 SFTP 生效。

会话持久化(tmux,0.10.0)

默认安全模型不变:会话随宿主生死(内核级 PTY 清理)。需要「活得比宿主久」 的工作(dev server、build、训练任务),开持久终端——会话状态委托给 tmux server(专用 socket dsh-tty,与用户自己的 tmux 完全隔离),断线保活 超时、甚至宿主重启后都能接回:

  • 入口(0.10.1 简化):设置卡片「会话持久化」选 tmux 即唯一开关——开启后 所有新开的标签默认持久化:「+」菜单的「本地终端」、连接簿条目点击、 SSH 连接对话框(「持久会话」默认勾选,单次连接可取消)。不再有单独的 「持久终端」菜单项;
  • 条目级取消(sshHosts[].persist):连接簿条目上的「持久会话」勾选框存的是取消项 ——显式取消过(persist: false)的条目点开时不再 tmux 托管,其余条目(true / 没写过)跟随全局开关(所以 ~/.ssh/config 导入的条目不受影响)。设置卡片与连接对话框 都只有全局开关开着时才显示这个勾选框;在设置里编辑条目不再丢掉它(0.17.x 之前只改 一个用户名就会静默把它清成 false);
  • 机制:spawn/ssh 帧带 persist + 客户端生成、随标签规格保存的稳定 persistName——本地把 -c 包装层换成 exec tmux -L dsh-tty -f <conf> new-session -A -s dsh-<名>(cwd 由 node-pty spawn 继承);SSH 则 远程 exec tmux -L dsh-tty -f /dev/null new-session -A -s dsh-<名> 开 pty channel。-A attach-or-create:宿主重启后重开标签按同名接回原 tmux 会话,正在跑的程序、pane 状态原样恢复;
  • 恢复链路:浏览器重连后查 sessions——持久标签 sid 已失效的,客户端 自动按原 persistName 重新 spawn(非持久标签维持丢弃语义);保活回收器 超时只杀 PTY(tmux 客户端),不杀 tmux 会话,回收后照样可接回; 同宿主内的重连(attach)对 tmux 会话不回放宿主缓冲——回放会把可见屏 先写进全新 xterm(幽灵滚动条),tmux 整屏重画再画一遍(重影)——改为 强制 tmux refresh-client 重画恰好一次; 持久标签规格同时写入 localStorage(sessionStorage 只对同一浏览器标签 可见,dsh 重启自动打开的新窗口原本读不到、恢复就断在这一环)——全新 窗口打开面板时按规格直接 respawn:tmux 会话存活则接回原现场,已消失 (远程重装/重装丢失)则开新 shell;规格只随「标签被主动关闭/退出」淘汰, 不做事前存活确认(确认依赖的留存状态一旦漂移会让恢复静默失效);
  • 关闭语义:kill 帧(标签 ✕ / 关面板)对 tmux 背书会话先 tmux kill-session 再杀客户端——真正结束,而不是 detach 留活口; 整个页面关闭时默认留存(保活期后可再恢复),配置 endOnPageClose: true 则改为保活期结束时连 tmux 会话一起结束;
  • shell 集成兼容:tmux 会吞掉不认识的转义序列——钩子检测 $TMUX 把 OSC 133/7 包进 DCS passthrough 信封(payload 内 ESC 双写),tmux ≥3.3 + allow-passthrough on(宿主自动写入桩 conf)时解包转发,宿主解析器看到的 仍是裸标记:tty_expect / OSC 7 cwd 跟随在持久标签内照常工作; tty_capture{last} 另有一层修正——tmux 的 pane 重画是异步批量的,命令输出 会落在 D 标记之后逃出捕获窗口,故钩子在发 D 前先 capture-pane 把 pane 内容经 OSC 133;T(base64,最近 200 行)随流直送,宿主优先采用 T 快照 作为命令输出(超出 200 行的输出截断头部,与环形缓冲语义一致);
  • 运行资产:<DSH_HOME|~/.dsh>/tty/ 下的稳定桩目录(tmux server 比宿主 进程活得久,不能用临时目录):tmux.conf(status off 防重绘污染捕获、 真色 overrides、default-command 指向内层启动器)与 inner.sh(按当前 配置 exec 内层 shell,zsh ZDOTDIR / bash --rcfile 桩照常注入);配置热改 对新开 pane 生效,tmux.conf 本身只在 tmux server 首启时读取;
  • 降级:本机/远程未安装 tmux 时,持久 spawn 自动落回普通会话,终端里 回一行灰字提示;不装任何东西也能正常用,只是无持久化。

SSH 连接

0.2.0 起标签栏「+」变为一键菜单,除本地终端外还能开 SSH 标签页:宿主 半体用 ssh2 原生建立连接并打开 shell channel(不经过本地 ssh 进程,也 不占 node-pty),包装成与本地 PTY 完全一致的会话对象——输入、resize、 关闭、输出缓冲、背压与 agent 工具全部复用同一套调度。

  • 「+」菜单三个入口:本地终端 / SSH 连接簿(配置里保存过的条目, 显示 user@host[:port] · auth,条目带 📂 文件浏览与 ✎ 编辑)/ SSH 连接… (表单手填 host / port / username / auth,连接前可勾选保存,对话框底部 「文件浏览」可跳过终端直接以当前信息打开 SFTP);
  • 连接簿:SSH 连接对话框勾选「保存到连接簿」即存为条目(同名覆盖, 名称留空用主机名);「+」菜单条目的 ✎ 与设置卡片里的 编辑 走的是同一套表单—— 连接 / 认证 / 选项 三段分组 + 凭据存储 + 凭据引用选择器 + 试连 + 文件浏览, 支持改名、同名冲突校验。两者只差提交方式:菜单对话框是「保存修改 / 连接(并保存)」 立即落盘,设置卡片是「应用」进卡片表单、再随卡片「保存」写入配置;
  • 认证方式(auth)三选一:
    • agent(默认)——走 ssh-agent(SSH_AUTH_SOCK),凭证不落盘,最推荐;
    • key——keyPath 私钥文件(~ 开头可省略 home),passphrase 可选;
    • password——密码认证,同时挂 keyboard-interactive(不少服务端只开这个);
  • 密码 / 口令支持 env:VAR:password / passphrase 填 env:MY_SECRET 时按凭据层解析 —— 官方凭据 provider 优先(叠 $DSH_HOME/.credentials.yaml / 进程环境 / project-env / user-env,每次连接重新解析),它没有才退回宿主进程环境变量(配合 dsh-env-manager 插件托管 密钥,避免明文写进 settings 文件);解析不到时报错会同时点到"引用名"与"两处都没有";
  • 端口:默认 22,非 22 端口在 target 里显示为 user@host:port;
  • 标签与状态:SSH 标签标题用连接名或 user@host(本地标签是 「终端 N」);连接中先回显灰字 Connecting user@host …,就绪后状态栏 显示 SSH user@host 已连接;连接失败(连接超时 / 认证被拒 / 主机 不可达)以 error 帧带回原因,标签规格已随标签保存,点终端区域可按 原规格重开;状态栏描述的是当前活动标签——切标签 / 关标签立刻换成 该标签自己的状态(失败原因、退出码都记在标签身上),所以关掉那个连不上 的标签后,红字不会继续挂在头上(宿主级消息如「连接断开 — 自动重连中」 不属于任何标签,不会被切标签抹掉);反过来说,后台标签自己的失败只记在 它身上(标签栏状态点转红、终端区浮层带原文),不会顶掉活动标签的显示;
  • agent forwarding(0.4.0):SSH 对话框勾选「agent forwarding」后远程 可用本地 ssh-agent 的钥匙(远程 git clone 私有仓库等)。任意认证方式下 都可开(凭证仍不落盘);本机未运行 ssh-agent 时连接会明确报错而非静默 失效。连接簿条目随 agentForward 保存,列表里显示 · fwd;
  • ~/.ssh/config 导入(0.4.0):设置卡片连接簿区「从 ~/.ssh/config 导入」——解析 HostName/User/Port/IdentityFile 生成候选条目(跳过通配符 块与无 User 条目,Include 不展开),同名跳过,随「保存」写入;
  • 凭据引用选择器(0.4.0,0.17 起与 env 插件解耦):SSH 对话框的密码/口令字段旁有筛选框 + 限高列表,候选 = 凭据存储里已有的引用名(宿主读 .credentials.yaml 的 refs: 键, 只回名字、绝不含值)∪ 本机连接簿里已经在用的引用名;点击即填 env:NAME,也可手输 任意 (连接时按解析:provider 优先、 兜底)。 :引用的发现路径官方定的是"配置界面从 得知有哪些引用"——引用半边( 的 注释原话:"the reference half, which has no enumeration because configuration surfaces learn which references exist from settings schemas"),浏览器侧的 也只开 / / ,连 都没开。于是 "这本存储里到底存过哪些名字"在浏览器侧根本问不到 ✗ —— 而选择器的用途恰恰就是"我存过什么、 能不能复用"。所以宿主侧开了一条通路 (loopback 围栏, 只解析 的键名、不返回值,见 )。这是官方"引用不可 枚举"设计的一处(代价:引用名会进浏览器,值不会);若要完全守官方口径,就只用连接簿那一半。 边界:本地 provider 若被配了自定义 / ,那些引用这里看不到;宿主读取失败或旧 宿主没有这条路由时,候选安静退回连接簿那一半。 候选一个都没有时,这一行(不再摆一个永远点不开的下拉——那看着就像坏 了):文案点明要么勾上面「保存时存入凭据存储」新建一个,要么在字段里直接手输 ; 口令那行上方没有勾选框,文案相应改成只提手输。 (同样只在筛选框获得焦点时展开),差别只在呈现:卡片里是列表而非浮层——卡片本身是 可滚动的长表单,浮层在那边会被裁掉;

服务器状态条(0.17.0)

终端面板(含服务器状态条):终端上方一条 CPU / 内存 / 磁盘 / 核心 / 在线 / TCP / 网速的细监控条

(图中是本机会话;SSH 会话的状态条同款同字段——见下方 SFTP 单窗体那张图,终端上方那条就是远端主机的指标。)

每个可见标签在终端上方挂一条细状态条,实时展示该会话所属主机的资源指标 (观感对齐 FinalShell 的会话监控条):

CPU ▮▮▯▯ 6% │ 内存 ▮▮▮▮ 78% │ 磁盘 ▮▮▮ 63% │ 核心 16 │ 内存 49.6G/62.3G │
在线 2w4d7h16m │ TCP 570 │ 磁盘 40.8G/68.8G │ CPU温度 无 │ 网络 ↓92.3K/s ↑93.1K/s
  • 两条采集路径,同一帧形状:SSH 会话在同一条 ssh2 连接上另开一条 非 PTY 的 exec channel,远端跑常驻循环每秒吐一行 JSON(CPU%/网速等速率 在远端算好);本地会话由宿主自己采(Linux 直读 /proc,macOS 走 os/netstat/ vm_stat)。两条路径都不碰 PTY 数据流。
  • 远端平台覆盖 Linux / Windows:默认跑 POSIX sh + awk 脚本;若 channel 在 一帧数据都没读过时就结束(Windows 上 cmd.exe / PowerShell 解析不了脚本), 自动换 PowerShell 版再试一次(-EncodedCommand 递送、同样字段形状),两跳都 失败才判失败——macOS/BSD 远端就是这条路径(无 /proc 也无 PowerShell)。
  • 订阅制、懒启动:客户端按标签可见性发 {t:'statsOn'} / {t:'statsOff'}, 宿主首个订阅才起采集;退订清零、会话退出、孤儿回收、插件禁用、配置关闭 都会停表并关掉远端 channel,不留定时器/远端循环。
  • best-effort:某项拿不到就省略该字段(前端显示「无」),采集失败静默停表、 状态条整条隐藏(8s 收不到新帧即收起——不是 3s:宿主某个采集子进程偶发变慢时帧间隔 会被拉长,窗口太紧就会整条一闪一闪)——绝不写 PTY、绝不弹错误。进度条阈值 配色:<70 正常 / 70~90 黄 / >90 红。
  • 慢命令不拖垮节奏:走子进程的字段(df / netstat / vm_stat)只在首次采样时等一次, 之后一律「用上一次的值 + 到点后台刷新」,某台机器某个命令卡住只会让那一格短暂显示旧值, 不会把「每秒一帧」的时间轴拖散(实测 3000ms/帧 → 3~12ms/帧)。
  • 热生效:关掉 statsEnabled 立刻停采集(状态条消失、WS 不再发 stats 帧), 重新打开后按仍存留的订阅自动恢复,无需重启或用例重开标签。
  • 固定槽位 + 增量刷新:每个值的字符槽位固定(右对齐,最宽的一档照样只占 自己那一格),条目只建一次、之后只写真的变了的文本。所以 CPU 5% → 12%、 TCP 36 → 1024、内存 9.2 GB → 17.8 GB 都不会把后面的条目推着横移(曾表现为整条 每秒跳一次),进度条宽度也能真正走 CSS 过渡;窄窗口下横向滚到右侧看 网络 时, 滚动位置不会被下一秒的刷新弹回开头。

配置(设置 → 插件 → 终端面板,保存即热生效)

项默认说明
enabledtrue关闭整个插件(需重启生效)
announceToAgenttrue是否向 agent 公告终端面板能力(systemPrompt 注入)
maxSessions4并发 PTY 会话上限(1~16)
shell$SHELLshell 路径;设置卡片可选可输入(下拉候选来自 /etc/shells + $SHELL + 常见安装路径,仅列存在且可执行者,$SHELL 优先),也可手输任意路径
termxterm-256colorTERM 值
colorTermtruecolorCOLORTERM 值
cwd宿主启动目录兜底工作目录(客户端当前会话 cwd 优先)
reconnectGraceSec120异常断开后会话保活秒数(0~3600):刷新页面/网络抖动后会话存活等待重连,超时由回收器结束;0 = 旧行为,断开立即结束
sshHosts[]SSH 连接簿(面板「+」菜单可选):条目 {name, host, port=22, username, auth=agent|key|password, keyPath, passphrase, password, agentForward, persist=false};保存时整体替换、同名覆盖;password / passphrase 支持 env:VAR 引用,避免明文入库;持久化开启时条目点击默认以 tmux 持久会话打开,persist=false 是取消项
hostKeys[]SSH 主机指纹记录(TOFU,自动维护):条目 {host, port, fingerprints[]}(旧版单 fingerprint 字段读取时自动迁移合并);按 host:port 唯一,一机多把钥匙共用一条记录,首次连接自动追加、任一指纹匹配放行、全部不匹配拒绝连接;设置卡片可删除重置
shellIntegrationtrue注入 OSC 133/7 shell 集成(命令边界标记 + cwd 上报;tty_capture{last} 依赖它);zsh/bash 支持,其他 shell 自动跳过;出兼容问题时可关闭
tunnels[]端口转发隧道:条目 {name, bookName, direction=local|remote, localPort?, remoteHost?, remotePort?, localTargetHost?, localTargetPort?, enabled};bookName 引用连接簿条目提供主机与认证;卡片「端口转发」区块可视化维护
sftpStyledialogSFTP 文件浏览界面风格:dialog 单窗体(远程目录 + 上传/下载/拖拽)/ dual 双栏(左本机 / 右远程,行内 ⇨/⇦ 宿主服务端直传);重新打开 SFTP 生效

连接栏扩展点(客户端服务 ttyConnbar,0.13.0)

其他插件可以在 SSH 连接栏(SFTP / 隧道按钮那一行)追加自己的上下文按钮,而不需要 tty 认识它——tty 只暴露一个通用客户端服务。内置动作(重新打开 / SFTP / 隧道)也 走同一条注册通道,显示顺序 = 注册顺序;未注册任何扩展时行为与之前完全一致。

// 消费方(如 dsh-docker)在自己的客户端半体里可选注入:tty 没装就不会触发
ctx.inject(['ttyConnbar'], (c) => {
  const dispose = c.ttyConnbar.addAction(({ tab, spec, bookName, addAction }) => {
    // 每次 renderConnbar 都会调用一次;自行决定这次要不要加按钮
    if (spec.t !== 'ssh') return
    addAction(iconSvg, '容器', '打开该主机的 Docker 容器面板', () => { /* 打开自己的面板 */ })
  })
  // 卸载时调用 dispose()
})
成员说明
addAction(factory)注册按钮工厂;返回注销函数。factory 收到 {tab, spec, bookName, addAction}:spec 是会话的 spawnSpec({t:'ssh', name?, host, port, username, ...}),bookName 是连接簿条目名(内联连接为 ''),addAction(icon, label, title, onClick) 用 tty 的按钮样式追加一个按钮
requestRender()请 tty 重新渲染连接栏(消费方异步拿到新数据后需要按钮立刻出现时用)
  • 只在 SSH 标签上触发;本地标签的连接栏本身是隐藏的。
  • 命令标签不触发(spawnSpec.command 非空,即经 ttyTerminal.open 跑一条命令的标签, 如 dsh-docker 的 docker exec -it …):连接栏那几个扩展都作用于连接本身,挂在命令 标签上会误导(SFTP 浏览的是宿主机,不是用户以为的容器内)。内置与第三方动作一起隐藏; 退出态的重开入口不在此列——终端体内浮层本来就有「点击重新打开」。
  • 工厂抛错只记 console.warn,不影响连接栏与内置按钮。
  • 服务名 ttyConnbar 未声明在 tty 的 Context 类型面上,消费方用字符串注入即可; tty 未安装或版本 < 0.13.0 时注入不会触发,消费方需按可选依赖处理。

open() 与 ttyPanel.mountPane() 都会让面板可见:没开就开,最小化中则恢复。 (曾经只判「弹窗是否存在」,于是面板最小化时新标签被加进一个隐藏的弹窗,消费方看到的是 「点了没反应」——minimize() 是隐藏而不是关闭,这一条对调用方是硬保证。)

终端命令标签(客户端服务 ttyTerminal,0.14.0)

open() 默认复用(契约 v3):同一个「连接 + 命令」已经有活标签时,聚焦它而不再新开, 返回被复用的那个标签。动机是实测——容器卡片「终端」按钮连点几下就是三个一模一样的 docker exec 标签,而每个标签各占一个会话名额,并发上限被白白吃光,接着满屏都是 「会话数已达上限」。想要并列两个同样的会话,传 reuse: false。 (复用键按固定字段列表构造,不直接 JSON.stringify——从 sessionStorage 恢复的 spec 与现场 构造的键顺序未必一致,用字符串化会漏判。)

比连接栏按钮更进一步的扩展点:让其他插件开一个标签直接跑一条命令(典型用途 是 dsh-docker 的卡片「终端」按钮 → docker exec -it <容器> sh)。

ctx.inject(['ttyTerminal'], (c) => {
  c.ttyTerminal.open({
    command: "docker exec -it 'ems-consumer-test' sh",  // 必填,单行,≤2000 字符
    book: 'lab-a',        // 二选一:连接簿条目名 → SSH 标签
    // spec: { host, port, username, auth, agentForward },  // 内联 SSH 字段
    // (都不传 = 本地标签,用 cwd 指定工作目录)
    label: 'ems-consumer-test · exec',
    cwd: '/optional/local/cwd',
  })
})
  • 命令标签不做 tmux 持久化(命令短命,attach 无意义),也不走登录 shell; SSH 侧用 conn.exec(command, {pty}),本地侧用 sh -c 'export TERM=…; exec <command>'。
  • 命令标签会自动重开:宿主重启 / 断线重连后 sid 已失效,客户端对 spawnSpec.command 的标签按原规格重新执行命令(普通非持久标签维持「点击重试」 的旧行为)。页面刷新后同样按原命令恢复。
  • 命令来自宿主侧插件(不是远程用户输入),信任级与插件本身相同;tty 只校验 形状:非空、单行、长度 ≤2000(换行会破坏本地 -c 包装层)。
  • 服务名 ttyTerminal 同样未声明在 Context 类型面上,按可选依赖注入;tty 未安装 或版本 < 0.14.0 时不会触发(dsh-docker 会退化为「复制命令」)。

就地嵌入终端(ttyTerminal.mount,0.15.0)

open 是「借 tty 的弹窗开一个标签」——用户的面板会被弹窗盖住/被顶到后面;如果消费方 希望在自己的面板里就地放一块终端(典型是 dsh-docker 的终端抽屉:看着容器日志直接 进容器敲命令,上下文不断),用 mount:

ctx.inject(['ttyTerminal'], (c) => {
  if (Number(c.ttyTerminal.version ?? 0) < 2) { /* 老版本:退回 open */ }
  const dispose = c.ttyTerminal.mount(hostEl, {
    command: "docker exec -it 'ems-consumer-test' sh",  // 与 open 同一套 options
    book: 'lab-a',                                     // book > spec > 本地
    label: 'ems-consumer-test · exec',
  })
  // 收起自己的抽屉时:
  // dispose()
})
  • hostEl 需是 HTMLElement:tty 往里塞一个绝对定位的 .tt_term,所以挂载点要 position: relative 且有确定尺寸(尺寸变化会被 ResizeObserver 接住并同步给 PTY)。
  • 嵌入终端与标签共用同一条 WebSocket 与会话表,但语义是「别人面板里的一块终端」: 不进标签栏、不写 sessionStorage、不参与 tty 面板的显隐;tty 面板关闭不会波及它 (反过来说:嵌入会话在跑时,tty 的连接不会被关掉)。
  • 断线自动重连、宿主重启后按原命令重跑、退出后点击遮罩重开,全部沿用既有逻辑; dispose() 结束会话并卸载 DOM。挂载是冷启动安全的——tty 面板没开、连接还没建, mount 也会把连接拉起来(创建帧先排队,onopen 后补发)。
  • 嵌入终端没有 tty 面板头部的搜索/清屏/复制工具栏,Ctrl+F 交还浏览器。

面板内挂载位(客户端服务 ttyPanel,0.16.0)

tty 自己的 SFTP 文件浏览也走这条通道(0.16.0):挂载位空着时挂在下方, 被同标签的别的面板占用时退回对话框。

mount 解决的是「消费方给宿主,tty 往里塞终端」;ttyPanel 是它的镜像——tty 在 终端面板里给消费方一块位置,让消费方把自己的界面挂进来。典型场景:dsh-docker 从 SSH 连接栏点「容器」,容器面板挂在终端右侧,终端继续可见、可点、可输入,而不是被整屏 弹窗盖住(0.15 之前那正是用户的痛点)。

挂载位是连接级的:它的凭证 / 目标来自打开它的那个终端标签。所以 0.19.0 起每块 pane 记一个「归属标签」(options.ownerSid,默认 = 挂载那一刻的活动标签):切到别的 标签时整块收起(data-dock-hidden,DOM 与你 render 的树都保活,在途传输继续跑), 切回来恢复现场;归属标签被关掉时 pane 一并收掉。不这么做的话,切完标签面板还停在原处 ——标题写着 SFTP · lab-b、底下活动标签却是 192.0.2.10,面板里躺着另一台 主机的文件列表(最坏的情况是往错主机上传)。显式传 ownerSid: null 表示「不隶属任何 标签」(一直可见)——面板开着但一个标签都没有时自动落进这一档;消费方也可以用它声明 「这块与标签无关,别跟着切」。

ctx.inject(['ttyPanel'], (c) => {
  // 面板没开(或 tty < 0.16)时走自己的弹窗
  if (Number(c.ttyPanel.version ?? 0) < 1 || c.ttyPanel.isOpen() !== true) { /* fallback */ }
  const pane = c.ttyPanel.mountPane({
    title: 'Docker 容器',  // 面板标题
    hint: 'prod-web-01',   // 标题右侧灰字(可选)
    side: 'right',         // 'right'(默认,竖向列表 / 列表+详情)| 'bottom'(横向宽表)
    size: 520,             // 初始尺寸 px:right = 宽度(默认 460)、bottom = 高度(默认 320)
    min: 360,              // 最小尺寸 px(可选,默认 280 / 160)
    ownerSid: tab.sid,     // 归属标签(可选):省略 = 当前活动标签,null = 不隶属任何标签
    onClose: () => { /* tty 收掉面板时回调:在这里 unmount 自己的 React root */ },
  })
  createRoot(pane.element).render(<MyPanel />)
  // 自己收起时:pane.dispose()
})
成员说明
isOpen()终端面板是否正开着(最小化不算)。消费方据此决定「挂进来」还是「走自己的弹窗」
mountPane(options)在面板右侧 / 下方挂一块位置并返回 handle;同时只挂一个,后来的 mountPane 会先收掉前一个(并回调它的 onClose)。side:'bottom' 时占满宽度、由高度定尺寸(拖上边缘),折叠成一条标题栏。ownerSid 记归属标签(默认当前活动标签),切换标签时归属别的标签的 pane 会被收起(DOM 与你的 React 树保活,切回来恢复;见上文「连接级」那段)
handle.element消费方 render 的宿主(flex 纵向、已 overflow:hidden,撑满正文区)
handle.setTitle(text) / setHint(text)改标题 / 灰字
handle.expand() / collapse() / toggle() / isCollapsed()折叠成 32px 窄条(竖排标题 + 展开/关闭按钮),终端立刻拿回宽度
handle.dispose()消费方主动收起(幂等,不会再触发 onClose)
  • 生命周期:侧栏的 DOM 长在终端弹窗里——最小化 / 恢复跟着面板走,消费方不用管; 面板被关闭(✕ / 宿主卸载)时 tty 先调 onClose(消费方在这里 unmount),随后才摘 DOM。
  • 拖尺寸:右侧 pane 拖左边缘、下方 pane 拖上边缘(上限 = 面板卡片长边的 72%, 给终端留位置),尺寸在同一次页面会话内按方向分别记住。终端区的尺寸变化由既有 ResizeObserver 接住,自动 refit 并把新行列数同步给 PTY。
  • 视口锚定:pane 开合 / 拖尺寸会改变终端区高度,xterm 自己挪视口会让内容 「被往上顶」。refit 时按用户当时的意图锚定——本来贴着底部(在看最新输出)就 继续贴底,翻在历史里就锁住原来那几行,不会跳走。
  • 方向怎么挑:竖向列表 / 列表+详情(如容器面板)用 right;横向宽表(如 SFTP 文件列表、本地↔远程双栏)用 bottom —— 全宽摆得下更多列,也不挤终端宽度。
  • 标题栏(标题 / 折叠 / ✕)由 tty 提供,消费方只管自己的正文;onClose 抛错只记 console.warn,不影响面板关闭。

minimize()(契约 v2) 把整个终端面板折进去:弹窗藏起来,但 DOM / WebSocket / xterm 缓冲全保留、会话继续跑;恢复靠侧边栏「终端」入口上的徽标。消费方用它「把舞台让出去」 ——典型是 dsh-docker 把日志交给会话之后自动折起终端,让用户直接看到会话,而不是对着一个 盖住会话的弹窗猜「点了没有反应」。返回值是调用后的最小化态,调用方据此决定提示文案里还要 不要写「会话在面板后面」。

ctx.inject(['ttyPanel'], (c) => {
  if (Number(c.ttyPanel.version ?? 0) < 2 || typeof c.ttyPanel.minimize !== 'function') return false
  if (c.ttyPanel.isOpen() !== true) return false
  return c.ttyPanel.minimize()   // 折起终端,让会话露出来
})

契约版本:ttyConnbar.version === 1、ttyTerminal.version === 3(1 = 只有 open, 2 = 增加 mount,3 = open 默认复用同「连接 + 命令」的活标签)、ttyPanel.version === 2(1 = mountPane + isOpen,2 = 增加 minimize)。消费方按版本号判断能力,不要用 typeof fn === 'function' 之外的假设;老版本 tty 上 inject 依然会触发,但没有 对应字段。

帧协议(/api/dsh-tty/ws,JSON 文本帧;v3 = 单连接多会话 + 断线重连)

方向帧说明
C→S{t:'spawn', sid?, cols?, rows?, cwd?, persist?, persistName?, command?}创建会话;sid 缺省由宿主生成,cwd 缺省用配置兜底;persist + 稳定 persistName(0.10.0)= tmux 持久会话(dsh-<名>,需 persistence=tmux);command(0.14.0)= 直接跑一条命令(不做持久化)
C→S{t:'ssh', sid?, cols?, rows?, name? | host, username, …, persist?, persistName?}创建 SSH 会话(ssh2 原生);name 引用连接簿条目作基底,内联 host/port/username/auth/keyPath/passphrase/password/agentForward 可逐项覆盖;persist 语义同 spawn(远程 tmux 托管)
C→S{t:'input', sid?, d}按键/粘贴数据
C→S{t:'resize', sid?, cols, rows}面板尺寸变化
C→S{t:'refresh', sid?}强制重画(0.10.1):宿主对 tmux 会话执行 refresh-client(客户端 reset 清残 scrollback 后请现场重画;非 tmux 会话 no-op)
C→S{t:'kill', sid?}关闭会话(孤儿会话也允许跨连接 kill,防泄漏)
C→S{t:'sessions'}列出全局会话快照(attachable 标记可重连者)
C→S{t:'attach', sid}重连孤儿会话(保活窗口内):ready(reattached:true) 后紧跟一帧 data 回放输出缓冲
C→S{t:'statsOn' | 'statsOff', sid}订阅/退订该会话的服务器状态条(0.17.0):按标签可见性驱动,宿主只在有订阅时采集(懒启动 + 退订即停表并关远端 channel)
S→C{t:'ready', sid, pid, kind, target?, persist?, reattached?}会话就绪;kind:'local' 带 pid,kind:'ssh' 时 pid=null、target=user@host[:port];attach 复用此帧并带 reattached:true;persist:true 表示 tmux 持久会话(0.10.0)
S→C{t:'data', sid, d}终端输出(utf8 文本,StringDecoder 兜跨帧多字节序列);12ms 窗口/64KB 阈值合并成帧(0.4.1),exit/kill 前强制冲刷保证帧序
S→C{t:'stats', sid, stats}资源指标帧(0.17.0):{cpuPct, cores, memUsed, memTotal, memPct, diskUsed, diskTotal, diskPct, uptimeSec, tcpConns, rxRate, txRate, tempC?};缺失字段即省略(best-effort,前端显示「无」),字节类为 bytes、速率为 B/s
S→C{t:'exit', sid, code, signal}

断线保活语义:客户端正常关面板会先逐个发 kill 再断开;因此「WS close 且仍有存活会话」判定为异常断开——会话转入孤儿状态(输出继续积累进环形 缓冲,不向任何连接发送),保活 reconnectGraceSec 后由回收器清理;期间 新连接可 {t:'sessions'} 查询 + {t:'attach', sid} 重连回放。

sid 省略时按「该连接唯一会话」路由;连接上存在 0 或多个会话时省略 sid 会报错。upgrade 路由带 loopback 信任围栏(remoteAddress + Host + Origin 校验),仅本机 Web GUI 可连。

开发

pnpm --filter @hyzyn/dsh-tty build        # tsc 宿主 + esbuild 浏览器半体(client.js)
pnpm --filter @hyzyn/dsh-tty typecheck
pnpm --filter @hyzyn/dsh-tty probe        # M0 探针:PTY 原语验证(需真实 PTY)
pnpm --filter @hyzyn/dsh-tty integration  # 集成测试:真实插件 × 真实 DSH 服务组合
pnpm --filter @hyzyn/dsh-tty live         # 存活冒烟:需先起 dsh web(默认连 ws://127.0.0.1:3080,DSH_TTY_WS_URL 可覆盖)
pnpm --filter @hyzyn/dsh-tty tui          # TUI 冒烟:vim/htop 全屏渲染(需先起 dsh web,默认连 :3090,DSH_TTY_WS_URL 可覆盖)
pnpm --filter @hyzyn/dsh-tty ssh-smoke    # SSH 冒烟:内存 SSH server(ssh2.Server)× 真实 spawnSsh 端到端(需先 build)
pnpm --filter @hyzyn/dsh-tty probe-smoke        # 试连分类与 TOFU(7 例,自包含)
pnpm --filter @hyzyn/dsh-tty probe-route-smoke  # 试连 HTTP 路由 + 连接簿(9 例)
pnpm --filter @hyzyn/dsh-tty sftplimits-smoke   # sftpLimits 配置归一化(6 例)
pnpm --filter @hyzyn/dsh-tty windows-smoke      # Windows 端到端(只在 Windows 上有意义,其他平台打印原因后跳过)

CI(`.github/workflows/ci.yml`)在 ubuntu 跑 `integration` + `ssh-smoke` + 三个 smoke,在
windows-latest 跑 `windows-smoke`,另有一道「构建产物与源码一致」闸门(`client.js` + `lib/`)。
0.19.0 的 48 项修复与审计索引见 [`DEFECTS.md`](./DEFECTS.md)。
pnpm --filter @hyzyn/dsh-tty preview      # 视觉预览:headless Chrome 逐场景截图(见下)

浏览器半体源码在 client-src/index.js,样式表独立成 client-src/tty.css(由 esbuild 的 text loader 内联进 client.js)。构建产物 client.js(含 xterm 内核), 改客户端后需重新 pnpm build 并刷新页面(可能需硬刷新)。

视觉预览 / 截图回归(scripts/preview.mjs)

改样式不该靠「刷新页面看一眼」:脚本把 client.js 装进一个纯静态夹具页 (scripts/preview/harness.html + mock-host.js 伪造的 DSH 宿主:module loader / fetch / WebSocket),用 headless Chrome 把 29 个界面状态逐个渲染并 截图到 packages/tty/.preview/shots/:

node scripts/preview.mjs                 # 全场景
node scripts/preview.mjs local menu ssh  # 指定场景
node scripts/preview.mjs --list          # 列出场景
node scripts/preview.mjs --theme=light   # 浅色主题

覆盖:本地终端 / 多标签 + SSH 连接栏 / 「+」菜单 / SSH 对话框(新建、编辑、试连)/ 设置卡片(含与 docker 并排对照)/ SFTP(单窗体、双栏、落点回退)/ 挂载位跟着标签切(dock-pane-tab,0.19.0 的回归)/ 最小化徽标 / 退出与错误遮罩 / 隧道弹层 / 搜索框 / toast / 嵌入式终端(单独与面板共存)/ docker 面板与「容器 → 终端抽屉」。

场景可以把函数形态的断言挂到 window.__previewAssert(返回 null = 通过,返回 字符串 / 数组 = 失败),脚本会跑掉它并把结果计入 ✓/✗。断言光挂在夹具里、只有手工 取 diag 时才有人看,回归等于没钉——0.19.0 修「面板不跟标签切」时就吃到这个亏:断言 早写好了,但因为混在带函数的 diag 对象里,整条求值静默失败,一路都是 ✓。

夹具还会把 --dsw-* 皮肤变量与真实界面一并渲染,因此能验 「明暗主题切换后是否还有白色面板」这类问题。产物目录 .preview/ 已 gitignore。

夹具里的 ctx.inject 与真实 cordis 同语义:依赖里有一个服务不存在就不触发回调, 而不是把 undefined 塞进 scope。塞 undefined 的后果是一个可选依赖(例如 dsh-docker 那条 sidebarRight/sidebarRightTabs 支路)就能让所有场景在挂载阶段炸掉 (Cannot read properties of undefined (reading 'register'))——夹具不提供宿主侧的 侧栏服务时,那一支就该安静地不注册。

夹具需要 Chrome/Chromium(默认找 playwright 缓存的 Chrome for Testing, 也可用 CHROME_PATH 指定)。若宿主环境限制了 Chrome 的沙箱(子进程被 拒),需要放开后运行,否则浏览器起不来。

已知限制

  • resize 为内部耦合:DSH 的 spawnTerminal handle 未暴露 resize, 插件直接透传 (handle).terminal.resize(cols, rows)(node-pty 原生 API, 同进程可达)。DSH 升级若改内部结构,0.3.0 起会警告一次并退化为固定尺寸, 不再逐帧抛错。
  • TERM 注入用 -c 包装层(仅 POSIX):DSH 硬编码 node-pty name:"dumb",而 node-pty 里 name 优先于 env.TERM,因此 shell 以 sh -c 'export TERM=...; exec "$shell"' 方式启动(对用户透明;TERM / COLORTERM 值做白名单校验,防止破坏包装层命令)。Windows 上没有这一层 —— cmd / PowerShell 都不认这套语法,ConPTY 也不需要 TERM(见「Windows 宿主」一节)。
  • Windows 宿主:本地终端可用(默认 %COMSPEC%),但 shell 集成 / tmux 持久化 / 磁盘·TCP·网速·温度这几项不适用或拿不到 —— 完整边界与验证范围见「Windows 宿主」一节。
  • terminate() 有「幸存者」竞态:DSH 树级清理偶发报 terminal cleanup failed; surviving pids,插件按 best-effort 处理 (失败降级对顶层 shell 直接 SIGKILL),退出码/信号可能为 null。
  • 输出为 utf8 文本流:node-pty 数据经 DSH 按 utf8 编码传输,非 UTF-8 字节会被替换字符吃掉(如 cat 二进制文件),属预期行为;跨 chunk 的 多字节 UTF-8 序列已由 StringDecoder 兜住(0.3.0),中文高速输出不再花。
  • 浏览器半体依赖官方 dsh-web-app 的侧边栏结构([data-pane="sidebar"]), 非官方 Web GUI 可能不显示入口。
  • 断线保活窗口有限:异常断开后会话仅在 reconnectGraceSec(默认 120s) 内保活可重连,宿主进程重启则所有会话结束;超期后未重连的会话由回收器 结束,输出缓冲(尾部 256KB)之外的滚动历史无法恢复。
  • shell 集成仅 zsh / bash:其他 shell 自动跳过(tty_capture{last} 会 明确报错而非错报)。bash < 4.4 走 DEBUG trap 兜底:循环体等复合命令的 内部命令会多发 B 标记,tty_capture{last} 对这类命令只截取最后一个内部 命令之后的输出(退出码与 tty_expect 不受影响)。用户 rc 若覆盖 PROMPT_COMMAND/钩子数组,集成可能失效——可关闭 shellIntegration 或 反馈补丁兼容。
  • 端口转发边界:本地监听固定 127.0.0.1(不暴露局域网);remote 方向服务端监听还受服务端 sshd GatewayPorts 限制;隧道的 SSH 连接与终端会话独立,均走 TOFU 钉扎与连接簿认证;隧道规格变更(端口/目标/启停)经「保存」热生效,热改连接簿凭证则在下次重连时生效。
  • 会话持久化(tmux)边界:持久标签由 tmux server(专用 socket dsh-tty)托管——宿主被硬杀 / 保活回收 / 浏览器丢失标签规格时,tmux 会话会留存(这正是恢复能力的前提),直到机器重启或手动 tmux -L dsh-tty 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 dsh-tty kill-server 让下次 spawn 重建 server);grace=0 的「断开立即结束」对持久标签同样会 kill-session(tmux 会话不存活);持久 SSH 会话要求远程装有 tmux(未装自动降级普通会话,连接栏常驻「未持久化」标记),且远程 ~/.tmux.conf 不影响专用 socket 的独立 conf(-f /dev/null);SSH 持久会话名随 settings 留存;时共享同一个宿主 PTY(0.10.1,单客户端扇出,名额不翻倍),两侧行数以最近调整尺寸的窗口为准(尺寸不一致时较大一侧由 onResize 自愈重画)。

工作原理

浏览器半体 (client.js, esbuild bundle)
  ├─ 侧边栏「终端」入口 → 大弹窗
  ├─ 标签栏:每标签一个 xterm.js 实例(独立 sid;WebGL 渲染器,丢失回退 DOM)
  ├─ 断线重连:指数退避自动重连 + sessions 查询 + attach 恢复;
  │  标签列表存 sessionStorage(刷新后按 sid 重连保活会话)
  ├─ sessions 客户端服务:新标签带当前会话 cwd
  └─ WebSocket ──→ /api/dsh-tty/ws (webServer.registerUpgrade)
                        │
宿主半体 (src/index.ts)
  ├─ 连接内会话表(sid → 本地 PTY / SSH channel,单连接多会话)
  ├─ SessionManager(maxSessions 上限,热调整;SSH 会话同表调度;
  │  孤儿回收器按 reconnectGraceSec 清理异常断开的会话)
  ├─ 每会话 256KB 环形缓冲(tty_capture / 断线回放)+ xterm-headless
  │  虚拟屏(tty_screen)+ StringDecoder(utf8 分帧兜底)
  ├─ shell 集成(src/shell-integration.ts):zsh ZDOTDIR / bash --rcfile
  │  桩注入 OSC 133/7 钩子;输出流解析(feedShellIntegration,跨 chunk
  │  残包 carry)→ 命令边界捕获(tty_capture{last} / tty_expect 早停)
  │  与 cwd 跟随(tty_list)
  ├─ 本地路径:ctx.get('subprocess').spawnTerminal({ argv: shell -c 包装层, cwd })
  │  持久标签(0.10.0):包装层换 `exec tmux -L dsh-tty -f <conf> new -A -s dsh-<名>`
  │  (src/tmux.ts:探测/资产生成/spawn 计划/kill-session;稳定资产在
  │  <DSH_HOME|~/.dsh>/tty/——tmux.conf + inner.sh + zsh/bash 桩;tmux 会吞
  │  不认识的序列,shell 集成钩子检测 $TMUX 把 OSC 133/7 包 DCS passthrough
  │  信封,tmux ≥3.3 解包转发,宿主解析器零改动)
  ├─ 辅助路由:/api/dsh-tty/ssh-config(~/.ssh/config 导入候选)、
  │  /api/dsh-tty/credential-refs(凭据存储里已知的引用名 —— 只要名字,见「凭据存储」)、
  │  /api/dsh-tty/env-vars(env 插件托管变量名)、/api/dsh-tty/known-hosts
  │  (TOFU 指纹预填充,src/known-hosts.ts 解析含 hashed 条目)、
  │  /api/dsh-tty/shells(Shell 路径候选)——均 loopback 围栏
  ├─ SFTP(src/sftp.ts,0.7.0):懒连接池(空闲 120s 回收、断开按需重连、
  │  TOFU 共用)→ POST /api/dsh-tty/sftp/list|mkdir|rename|remove|download|
  │  upload(spec 走体/头,凭证不进 URL;上传下载流式 pipe)+
  │  sftp_list/read/write/mkdir/rename/remove/tree 工具(只收连接簿名;
  │  mkdir 支持 parents 自底向上逐级补齐,tree 限深限数递归列举)
  ├─ 端口转发(src/tunnels.ts):宿主自持隧道(-L/-R 双向),断线退避重连、
  │  TOFU 共用、连接计数;GET /api/dsh-tty/tunnels 实时状态 + tunnel_list 工具
  └─ 帧协议:spawn|ssh / input / resize / kill / sessions / attach
     ↔ ready/data/exit/error/sessions + 背压

SSH 路径 (src/ssh.ts)
  └─ {t:'ssh'} → spawnSsh:ssh2 Client 原生连接(agent / key / password,
     password·passphrase 支持 env:VAR 取密;host key 经 HostKeyStore
     TOFU 钉扎),开 shell channel 包装成与 PTY 同形状的 TermHandle
     (pid=null,kind='ssh',target=user@host[:port]),
     背压一并透传到 channel —— 之后与本地 PTY 无差别调度;
     persist 时先 `command -v tmux` 探测再 `exec tmux -L dsh-tty -f /dev/null
     new -A -s dsh-<名>` 开 pty channel(远程 tmux 托管;无 tmux 降级普通
     shell channel + 灰字提示;kill 经连接内 `tmux kill-session` 收尾)

M0 探针、集成测试(B1B24 共 58 项断言)与真实实例冒烟(live / TUI)在 真实 DSH 服务组合上验证过:TERM 注入、resize 透传、sid 冲突、并发上限、 loopback 围栏、多会话数据隔离、cwd 跟随与校验、配置热生效(settings/updated)、 kill→exit 全链路、断线保活 + sessions/attach 重连回放、tty_screen 虚拟屏、 tty_capture ANSI 清洗、shell 集成(capture{last} + exitCode、OSC 7 cwd 跟随)、tty_expect 匹配与超时、/.ssh/config 解析器、端口转发(双隧道 active + forwardOut 往返 + reconcile 清理)、 shells 候选路由、bash 3.2 shell 集成(DEBUG trap 兜底:capture{last} + exitCode、tty_expect 命令结束早停)、SFTP 文件传输(list/mkdir/upload/ download/remove 路由 + sftp_* 工具,test-sshd 内存 sshd 端到端)、SFTP 管理 闭环(agent sftp_mkdir/-rename/-remove/-tree:parents 补齐、tree 限深截断、 跨目录移动、非空删除拒绝与递归删除);会话持久化(B25/B26:persistence 门控、tmux 会话托管与 kill-session 收尾、无 tmux 降级提示、DCS passthrough 下 capture{last}、保活回收只杀 PTY 不杀 tmux 会话、同 persistName 接回现场 ——本机无 tmux 时自动只跑降级路径)。SSH 路径由 ssh-smoke (内存 SSH server × 真实 spawnSsh)验证:password 认证建链与 prompt、 命令往返、pty-req 初始尺寸与 resize(window-change)、terminate / exit-status 全链路,以及 TOFU 指纹记录(S7)与指纹变更拒绝连接(S8); SFTP 由 ssh-smoke S9(test-sshd 的 sftp subsystem × 真实 SftpManager)覆盖: 目录列表与 realpath home、上传覆盖+追加、下载(stat size 作 content-length)、 mkdir/rename/非递归删除拒绝/递归删除、TOFU 指纹变更拒绝;S9g 覆盖 mkdir parents 自底向上逐级补齐(幂等)与 tree 限深截断/全量。

refs:
不可枚举
孤儿引用
  • 解析路径(连接能真正用上存储里的值的关键):连接 / 试连 / SFTP / 端口转发都走同一个 buildConnectConfig,其中的 env:NAME 经官方凭据 provider 解析(resolve,每操作重解析, 改完下一个操作即生效、不必重启宿主);provider 没有这个引用、或宿主没有该服务时才退回 process.env。为什么必须走 provider:凭据存储里的值永远不会被 materialize 进环境 (provider README 原话:"a store the harness owns and never materializes into the environment"), 所以只读 process.env 等于"存进去的密码连接时读不到"。provider 抛错不吞 —— 环境变量也没有时, 错误里会把两个来源一起写出来(否则"凭据服务坏了"会伪装成"你没配")。

  • 保守的边界(务必知道):~/.credentials.yaml 是 0600 的普通文件,没有主密码、没有 OS keychain —— 它挡的是别的 OS 用户,挡不住以你身份运行的进程与 agent;而且它机器本地, 换机器要重存。所以业界共识仍然是:能用密钥 / agent 就别存密码(本插件两者都支持)。

  • 降级:宿主没有 remote.credentials(老版本)时按钮禁用并说明原因,对话框仍可明文保存。

  • sftpStyle
    /api/dsh-tty/local-fs
    ⇨ / ⇦
    /api/dsh-tty/local-fs/transfer
    字节不经过浏览器
    env:NAME
    凭据层
    process.env
    为什么读文件 / 为什么不用 env 插件的托管清单
    自己的 settings schema
    故意不可枚举
    @deepseek-ai/dsh-credentials
    listRecords
    ctx.remote.credentials
    describe
    set
    unset
    listRecords
    只读
    /api/dsh-tty/credential-refs
    refs:
    readCredentialRefNames
    有意偏离
    path
    dshHome
    整体退化成一行说明
    env:NAME
    设置卡片的编辑表单里有同一行、同一份候选
    内联
  • 主机指纹 TOFU 钉扎(0.3.0):首次连接成功后把该主机(host:port)的 sha256 指纹记录进 hostKeys(随 settings 持久化);之后每次连接校验, 集合内任一指纹匹配即放行,指纹变更直接拒绝连接(防中间人冒充),错误 信息带重置指引。同一主机的多把钥匙(0.19.0,如 rsa + ed25519)各记一条 指纹、合并存于同一记录——算法协商变化不再误报变更。主机重装/换钥匙后, 到 设置 → 插件 → 终端面板 → 「SSH 主机密钥记录」删除对应记录再重连即可 (记录列表支持删除)。「从 known_hosts 导入」(0.4.1):一键解析 ~/.ssh/known_hosts 把已有主机指纹批量预填充,同主机的 rsa/ed25519 等多条 记录全部保留(不再只留首条);连接簿里的主机名还会用于还原 |1| hashed 条目,非默认端口按 [host]:port 解析;
  • 连接测试(0.11.0):设置卡片连接簿条目行内「测试」按钮,SSH 连接 对话框另有「试连」按钮——两者都只做链路诊断(不建会话、不占 maxSessions 名额、不开 shell):先 TCP 预检(DNS + 建连,失败分类为 拒绝/超时/DNS/不可达),再 ssh2 握手做主机密钥 TOFU 比对,最后完成 认证。结果逐段展示:成功显示耗时与「主机密钥匹配 / 已记录」;失败给出 具体阶段原因(如「认证被拒绝:所有认证方式均失败」「主机密钥不匹配」 附已记录与本次指纹 + 重置指引)。连接簿条目「测试」 完整 TOFU:首测 即把新指纹记录进 hostKeys(与真实连接同语义);对话框「试连」 只 比对不落盘(草稿未入库不建立钉扎)。
  • 计入 maxSessions 并发上限;关断与本地会话一致:标签 ✕ / kill 帧关闭 ssh2 channel,exit 帧照常带回退出码 / 信号。
  • persistenceoff会话持久化:off 会话随宿主生死(默认);tmux 开启后所有新开的标签默认由 tmux server 托管、可跨宿主重启恢复(需本机/远程安装 tmux);SSH 对话框可对单次连接取消
    endOnPageClosefalse页面(最后一个连接)断开且保活期结束时,是否连 tmux 持久会话一起结束。默认 false = 留存可恢复;true = 页面关了就不保活(保活期内刷新仍可无缝接回)
    sftpLimits{maxDownloadMb: 1024, maxUploadMb: 2048, maxUploadFiles: 1000}SFTP 传输限制(浏览器侧保护,均为 0 = 不限):maxDownloadMb 单文件下载上限(超限中止并提示用双栏 ⇦/终端 scp)、maxUploadMb 单文件上传上限、maxUploadFiles 一次批量/拖拽上传的文件数上限;大文件请走双栏 ⇨/⇦ 服务端直传(字节不经过浏览器,不占内存)
    statsEnabledtrue服务器状态条(0.17.0):按标签可见性采集并推送 CPU / 内存 / 磁盘 / 在线时长 / TCP 连接数 / 网速 / CPU 温度;关闭即停表(远端 exec channel 一并关闭),保存即热生效
    PTY 退出事实(恰好一次;attach 换连接后仍随当前连接送达)
    S→C{t:'error', sid?, m}错误
    S→C{t:'sessions', list, tmux?}会话快照({sid, kind, target, pid?, cwd, startedAt, lastOutputAt, attachable, persist?});tmux = 专用 socket 上现存的持久会话名 + SSH 持久会话留存名(0.10.1,可观测字段)
    两个窗口同时接回同一持久会话
  • SFTP 边界:文件权限 = 对应 SSH 账号的终端权限(无额外沙箱/chroot);下载经浏览器内存(超大文件建议终端 scp/rsync);agent 工具 sftp_read ≤1MB 且拒绝二进制、sftp_write 单次 ≤1MB(大内容走面板上传或终端);sftp_* 工具只收连接簿条目名,内联凭证仅供面板对话框使用;覆盖写走同目录临时分片 .dsh-part-<uuid> + rename 原子落盘——宿主进程崩溃 / 断电时可能留下分片孤儿(下次向同目录覆盖上传时会自动清理超过 24h 的残留)。
  • SSH host key 为 TOFU 钉扎:首次连接自动记录 sha256 指纹(trust on first use),之后任一记录指纹匹配放行、全部不匹配拒绝——不再是无条件放行的 accept-and-log。注意 TOFU 的固有边界:首次连接若已遭遇 MITM 则记录的 就是伪指纹;hostKeys 随 settings 落盘,指纹变更需人工在设置卡片确认 并删除记录;同一主机的多把钥匙(rsa/ed25519/ecdsa)合并为一条记录的多 指纹集合(0.19.0),算法协商变化不再误报「指纹变更」;known_hosts 导入 同样保留同主机的全部指纹。
  • 浏览器标签持久化不含明文凭证(0.19.0):SSH 标签写入 sessionStorage / localStorage 的规格副本会剥掉明文 password / passphrase (env: 引用保留),并打 credsStripped 标记——恢复/重开时终端里提示 重新输入。本会话内存里的规格不受影响。
  • SSH 密码 / 口令建议 env:VAR 引用:连接簿随 settings 文件落盘, password / passphrase 明文入库有泄露面;建议 env:VAR + dsh-env-manager 托管,或直接用 agent 认证(凭证不落盘)。
  • 服务器状态条(0.17.0)边界:远端靠两跳兜底:先 POSIX(要 /proc + awk),一帧未出则回退 PowerShell——Windows 远端由此覆盖(温度多数组件没有、显示「无」,磁盘取系统盘 %SystemDrive%,TCP 走 netstat -an,网速走 Get-NetAdapterStatistics 差值;只输出整数,从根上避开区域小数点与科学计数法),macOS/BSD 远端两跳都不成立、一个字段都拿不到、状态条整条隐藏;非 Linux 远端的每次(重新)订阅会多一跳注定失败的 exec(约百毫秒,不影响出帧延迟);Linux 上 CPU 温度只在暴露 /sys/class/thermal 的机器上有(多数云主机/虚拟机没有,显示「无」);磁盘固定取 /(Windows 取系统盘)所在文件系统(不做多挂载点);远端脚本经 sh -c 单引号包裹(登录 shell 是 fish/csh 也能跑);本地会话的指标属于宿主机器(多个本地标签共享同一份采样,CPU/网速差值窗口连续),macOS 没有 /proc、ss、sysfs,改用 os/netstat/vm_stat(网速那条是 netstat -ibn——必须带 -n:不带时 netstat 会对接口地址做反查,DNS 不响应就挂几十秒,D50 的根因就是它),故温度恒为「无」,内存按 vm_stat 的 active+wired+compressed 计(os.freemem 会把文件缓存算成已用、长期 99%);指标是瞬时值,没有历史曲线,也不提供 agent 工具(agent 侧仍用 tty_* 命令粒度能力)。
  • SSH 会话没有本地 pid:ssh2 shell channel 不是本机进程,ready.pid 为 null、tty_list 显示 target 而非 pid,本机 ps / kill 对远程 进程无效——关闭请用标签 ✕ 或 kill 帧(关闭的是 ssh2 channel)。