DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

qaz320621 /

qaz320621/dsh-likemobaxtearm-chumc

Verified

DSH(DeepSeek Harness)右侧栏 SSH 控制台插件 · 安装:https://github.com/qaz320621/dsh-likemobaxtearm-chumc

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@f6c931d8

DSH SSH 控制台

在 DeepSeek Harness(DSH)的右侧栏里做 MobaXterm 式的 SSH: 本机 ssh 客户端跑在真实 PTY 中,用户与 AI 共用同一条会话,双方操作互相可见并全部写入 JSONL 审计日志;AI 通过 9 个工具操作会话,并受三级权限约束,危险命令执行前会先告知。

English: An SSH console plugin for the DeepSeek Harness Web UI. The local OpenSSH client runs in a real PTY; the operator and the AI share one session, every action is visible to both sides and appended to a JSONL audit log, and the AI's writes are gated by three permission levels (view-only / ask / full) with a cancellable pre-execution warning for dangerous commands.

包名:dsh-likemobaxtearm-chumc · 版本:1.0.0 · 许可:MIT · 自检:九套(npm test,其中五套零依赖,跑完会打印项数)


目录

  • 特性
  • 平台支持
  • 快速开始:从构建到跑起来
  • 使用
  • 配置
  • 测试
  • 数据与隐私
  • 分发
  • 架构与实现要点
  • 已知限制
  • 版本与变更
  • 发布清单
  • 许可

特性

  • 一个宿主标签 + 自建二级标签条:宿主标签条里只有「SSH 控制台」一项,所有会话都在这个页面自己的标签条上 (滚轮横滚 / ◀ ▶ / ▾ 看全部 / 每项 ✕ 关闭);每个会话一台主机、一个真实终端(xterm.js,全屏程序可用)
  • AI 可操作:9 个工具 ssh_list_hosts / ssh_list_sessions / ssh_connect / ssh_exec / ssh_send / ssh_read / ssh_status / ssh_log / ssh_disconnect
  • 三级 AI 权限(每个会话独立,你随时切换)
    • 仅可看:AI 只能读,写入一律被拒绝
    • 问答(默认):AI 每条命令都要你点「允许」
    • 完全:AI 直接执行;危险命令先弹出可取消的告知窗口
  • 输入权互斥:同一时刻只有一个写者;AI 持锁时你可以一键「收回控制」——真的发送 Ctrl+C
  • 双向可见:AI 的命令在终端里标注出现;你的操作 AI 可用 ssh_log / ssh_read 主动拉取
  • 用户留言给 AI:提问 / 报错(自动附最近终端输出)/ 仅记录(只写日志,不消耗 AI token)
  • 断线重连:Harness 重启后标签仍在,按 r(或点按钮)在原标签内重连;连接信息记在浏览器
  • Windows/ConPTY 适配:\r\033[K 式整屏重绘不再灌满日志与分页;空闲重绘帧去重;哨兵支持 posix / cmd / PowerShell 三方言
  • 审计日志:<workspace>/.dsh-ssh/logs/*.jsonl —— 命令原文全量、输出超阈值截断并标注 truncated/fullBytes/lines、口令提示后的输入记 [redacted]

平台支持

环节 Linux macOS Windows
浏览器 UI(xterm 渲染、权限/日志/留言面板) ✅ ✅ ✅
插件宿主半边(起 ssh、日志、AI 工具) ✅ 已实测 理论可用(未实测) 理论可用(未实测)
需要的本机 ssh PATH 上的 ssh 系统自带 /usr/bin/ssh OpenSSH 客户端(Win10 1809+ 自带 C:\Windows\System32\OpenSSH\ssh.exe,需在 PATH)
默认 sshBinary ssh(经 ctx.subprocess.resolveExecutable 解析成真实路径并记入日志) 同左 同左;不在 PATH 时用配置项指绝对路径
交互式操作(你打字 / ssh_send) ✅ ✅ ✅ 远端任意 shell
ssh_exec(AI 执行 + 退出码) ✅ ✅ ✅ 三方言(自动探测 posix / cmd / PowerShell,可用 remoteShell 指定)
远端是 cmd / PowerShell ✅ 走 call echo …%ERRORLEVEL% / Write-Output $LASTEXITCODE 哨兵 同左 同左
密码 / 2FA 提示 ✅ 终端内输入 ✅ ✅ 使用 OpenSSH 时(plink/PuTTY 不是 ssh,不支持)
~/.ssh/config、密钥、ssh-agent、ProxyJump ✅ ✅ ✅(OpenSSH 语义)

实现上没有 Linux 专属代码:PTY 来自宿主 ctx.subprocess.spawnTerminal(它自身覆盖 Linux/macOS/Windows), 日志与路径全部走 node:path,ssh 可执行文件走 PATH 解析。

环境要求

  • DSH 0.2.0-rc.2 / Web profile(开发与测试版本)。插件依赖这些宿主契约: ctx.subprocess.spawnTerminal、ctx.connection.fetch、ctx.tools、ctx.systemPrompt、ctx.agents, 以及客户端的 sidebarRightTabs / slots API。契约若变动,npm test 里的真 Cordis 激活测试会先报出来。
  • 本机有 OpenSSH 客户端(默认从 PATH 找 ssh)。
  • 不需要在远端安装任何东西:走的是你本机的 ssh,复用 ~/.ssh/config、密钥、agent。
  • 插件不保存任何口令;交互式密码/2FA 由你在终端里输入,日志里会打码。

快速开始:从构建到跑起来

1. 获取源码

git clone https://github.com/qaz320621/dsh-likemobaxtearm-chumc.git dsh-ssh
cd dsh-ssh

仓库里已经附带构建产物 client.xterm.js,所以第 2 步是可选的;改了 chunk/terminal.js 才必须重建。

2. 构建客户端终端 chunk(可选)

client.xterm.js 是把 xterm.js 打进懒加载 chunk 的产物,源码是 chunk/terminal.js:

# 本插件的运行时**零依赖**;这两个只是构建期依赖,--no-save 保持 package.json 干净
npm install --no-save esbuild @xterm/xterm @xterm/addon-fit

node build.mjs        # → 生成/覆盖 client.xterm.js

(也可以装成 devDependencies:npm i -D esbuild @xterm/xterm @xterm/addon-fit。要求 Node ≥ 18。)

3. 安装到 DSH profile

插件是一个 DSH bundle(package.json 里声明 dsh.bundle.patch + dsh.client)。

方式 A:让 AI 装(推荐) —— 在 DSH 会话里说:

用 plugin_manager 的 install_bundle,target 是 <这个目录的绝对路径>

方式 B:自己在 Web UI 装 —— 侧边栏 Plugins 页 → 安装 → 填入该目录的绝对路径(或 npm 包名)。

方式 C:直接从本仓库(GitHub)装 —— 「添加插件」也接受 Git 地址,填下面任一形式即可:

https://github.com/qaz320621/dsh-likemobaxtearm-chumc
github:qaz320621/dsh-likemobaxtearm-chumc
  • qaz320621/dsh-likemobaxtearm-chumc 这种省略前缀的写法会被当成 npm 包名而拒绝, 必须带 https:// 或 github: 前缀。
  • 本仓库已经附带构建产物 client.xterm.js,所以从 Git 安装不需要跑任何构建步骤。

install_bundle 会调用 profile 的包管理器完成安装与选择。安装动作需要 danger-full-access 或审批, 因为插件 Host 代码会在 Harness 进程内、工作区沙箱之外执行。

4. 重启 Harness 一次,然后刷新页面

  • Host 半边:DSH 按 URL 缓存已加载的 JS 模块代,所以必须重启才会加载新代码。
  • Client 半边:client.js / client.xterm.js 是每次从磁盘读取的,刷新页面即可。

重启后如果插件没激活,启动日志会给出具体原因(例如某个服务没注入)。

5. 第一次连接

右侧栏 → 加号 → Guide 里的 「SSH 会话管理器」 → 填 target(host / user@host / ~/.ssh/config 别名) → 连接。密码/2FA 提示会出现在终端里,由你输入。

使用

你(操作者)

  • 终端:直接打字;Ctrl+C、全屏程序(vim/top)都正常。
  • 工具栏:
    • 权限三档:仅可看 / 问答 / 完全(对 AI 生效)
    • 收回控制:AI 正在跑命令时,一键打断它(真 Ctrl+C)并拿回输入权
    • 重命名:改标签名
  • 底部四个抽屉:
    • 日志:当前会话的日志,按 user / ai / system / remote 过滤,可刷新
    • 历史:全部日志文件(含 archive/ 里的归档),点「查看」只读打开(默认末尾 300 行)
    • 留言:给 AI 留言,三种类型 —— 提问、报错(自动附最近 20 行终端输出)、仅记录(只写日志,不消耗 AI token)
    • 待确认:AI 请求执行的命令;危险命令的告知窗口也在这里(可取消)
  • 断线重连:Harness 重启后标签会显示"会话已断开",按 r 或点「重新连接」——在原标签内重连,不会多开标签。 连接信息(target/user/port/name)存在浏览器里,所以重启后不用重打。
  • 最近连接:管理器里列出连过的目标,一键连接;新标签页里也会给出这个列表。
  • 会话切换由插件自己管:宿主标签条是 overflow: hidden 且不可改,所以会话不放在那里 —— 页面顶部的二级标签条是插件自己渲染的,能滚动:
    • 第一项 「控制台」是常驻页:主机管理、新建连接、日志目录都在这里,永远在、不可关闭;会话排在它后面;
    • 鼠标滚轮 → 横向滚动;◀ ▶ → 左右微调;▾ → 列出全部页面(滚动条之外也能直达);
    • ▾ 下拉点一下展开,点面板外部或按 Esc 自动收起(再点 ▾ 也能收起);
    • 每项 ✕ → 关闭该会话;Ctrl+Alt+← / Ctrl+Alt+→ → 依次切换;
    • 圆点配色:🟢 就绪、🟠 连接中 / 已退出、⚪ 已关闭或未知;红色只用于危险命令与危险操作按钮 (常驻的「控制台」页不带圆点 —— 它没有会话状态,不该显示任何告警色);
    • 关掉「SSH 控制台」这个宿主标签不会杀掉会话(AI 仍可继续操作),重新打开就能看到它们;
    • 旧版本留下的每会话标签:打开时会自动把会话并入控制台并自行关闭(没关掉就手动 ✕)。

AI(模型侧)

工具 作用
ssh_list_hosts 列出保存的主机(不含口令)
ssh_list_sessions 当前 DSH 会话里的 SSH 会话(状态/权限/输入权/日志路径)
ssh_connect 新建会话;会自动在右侧栏开一个可见标签
ssh_exec 执行一条命令并等结束,返回输出 + 退出码(受三级权限约束)
ssh_send 写入原始文本(可选回车),用于交互式程序、发 \u0003
ssh_read 按行分页读取终端输出(去控制序列)
ssh_status 单个会话的状态快照 + 最近输出
ssh_log 读取 JSONL 审计日志(按 actor/kind 过滤)
ssh_disconnect 关闭会话并回收进程树

权限语义:

  • 仅可看 → 写入类工具直接拒绝(SSH_PERMISSION_VIEW_ONLY),并要求 AI 请你切换权限,不会反复重试
  • 问答 → 命令进入待确认队列,你点「允许」才执行;超时未确认返回 SSH_APPROVAL_TIMEOUT
  • 完全 → 直接执行;命中危险规则时先给出可取消的告知窗口(dangerGraceMs,默认 5s)

其它约定:你按键收回输入权时,AI 的调用以 waitReason=revoked 结束(这是正常打断,不是错误); 远端在等凭据时 AI 的写入被拒绝(SSH_AUTH_REQUIRED,AI 不得代填口令)。

ssh_exec 的退出码用哨兵标记解析,属于启发式:命令自己后台化、或把 shell 换掉时可能拿不到真实退出码; 且哨兵依赖 POSIX printf,所以远端需要是 POSIX shell(远端是 cmd/PowerShell 时请用 ssh_send + ssh_read)。

日志字段

ai.command / user.input 的 command 全量保留;output / ai.result 的 text 超过 outputLogTruncateChars(默认 200)会被截断,并带 truncated / fullBytes / lines。 口令提示后输入的行记 text:"[redacted]" + sensitive:true。

日志按一次交互合并,规则是"看换行,不只看时间":终端每敲一个键就重画整行,纯按时间合并会把 ls 和 -a 切成两条,所以 ——

  • 缓冲区已有换行(命令提交了 / 输出成行了)→ 安静 outputFlushQuietMs(默认 400ms)就落一条;
  • 缓冲区还没有换行且你最近按过键(正在编辑这一行,方向键、翻历史、补全都算)→ 完全不落盘, 直到你回车。纯时间窗口治不了这件事:你停 10 秒也只是一次停顿,不代表"这行打完了";
  • 既没有换行、也没人在打字(裸提示符、进度条之类)→ 等 outputIdleFlushMs(默认 5000ms)再落;
  • 回车时先把这一行的回显落成一条,命令结果另成一条(结果因此有独立的 200 字符额度);
  • 还有:超过 outputFlushMaxBytes 立即落一条,会话结束/关闭时也会冲刷。

实时流不受影响:浏览器始终逐字节收到输出,被合并的只有落盘。

日志的生命周期

  • 一个 SSH 会话一个文件:logs/<会话 id>-<主机>_<YYYYMMDD-HHMMSS>.jsonl,会话结束即定稿 (所以同一台主机的多次连接是多个文件,重连会产生新文件);
  • 自动归档:超过 logArchiveAfterDays(默认 7 天)的日志被 gzip 成同目录 archive/<同名>.jsonl.gz —— 内容一字不差,体积约 1/10;正在写日志的活跃会话永不被移动;
  • 阈值可以精确到小时:logArchiveAfterDays: 0.5(小数天 = 12 小时)或直接写时长 logArchiveAfterDays: '12h' / '90m' / '30s'。归档扫描在启动后 5 秒、每次打开会话、 以及每 10 分钟各跑一次,所以小时级阈值不需要有人操作也会生效;
  • 0 是"关闭"而不是"立即":logArchiveAfterDays: 0 表示不归档(写错成 0 不会把日志一次性全压掉); 想激进归档就写一个真实时长(如 '30s');
  • 保留期:只有把 logRetentionDays 设为大于 0(默认 0 = 永久保留),超过该时长的归档才会被删除; 当前日志永远不会被自动删除;
  • 界面里能看历史:控制台页底部的「历史日志」列出全部文件(含归档,标注日期/会话/主机/大小/是否归档), 点「查看」只读打开,默认取末尾 300 行;解压后超过读取上限的归档会明确拒绝并报出实际大小,而不是卡死;
  • 文件名经过校验(../ 之类直接拒绝),插件只读自己的日志目录。

配置

配置写在 profile 的 patch 文件里:~/.dsh/profiles/<profile>/cordis.patch.yml(Harness 的插件配置统一在这里, 宿主没有图形化的插件配置页)。按行 id 覆盖,只写你要改的键即可,其余键取默认值:

# ~/.dsh/profiles/web/cordis.patch.yml(安装插件时这一行已经存在,补上 config 即可)
- id: dsh-ssh
  disabled: false
  config:
    logArchiveAfterDays: '12h'   # 归档阈值:数字=天(可小数),或 12h / 90m / 30s;0 = 不归档
    logRetentionDays: 0          # 归档保留:同样写法;0 = 永久保留

改完即时生效(profile patch 层是 live 的)。⚠️ 但配置变更会让这一行重新挂载,而插件卸载的清理动作会 关闭当前所有 SSH 会话(标签显示"已断开",按 r 即可重连)——所以最好在会话不重要时改,或干脆改完重启一次。 生效后可以在控制台页「历史日志」表头看到当前策略(如"归档阈值 12 小时,归档保留 永久")。

字段 默认 含义
logDir .dsh-ssh/logs 相对 workspace 的日志目录
logEnabled true 关闭则只保留内存日志
outputLogTruncateChars 200 日志中输出截断阈值(0 = 不截断)
outputFlushQuietMs 400 输出合并窗口:缓冲区已有换行时,安静这么久落一条
outputIdleFlushMs 5000 还在输入中的行(缓冲区尚无换行)等这么久才落一条,避免把半行切开
outputFlushMaxBytes 8192 超过此量立即落一条
maxConsecutiveBlankLines 1 投射时最多保留几个连续空行(0 = 全删);ConPTY 重绘曾把空行放大到 90%
repaintDedupe true 丢弃空闲期的整屏重绘帧(判定规则见「Windows / ConPTY 适配」)
repaintIdleMs 2000 多久没有输入才算空闲,用于重绘判定
repaintResizeMs 3000 resize 之后这么久内,重复出现过的整屏内容按重绘丢弃(命令执行中也生效,见下)
remoteShell auto ssh_exec 哨兵方言:auto / posix / cmd / powershell
markerMode auto 哨兵写法:auto(Windows 上 plain,其它平台 hidden)/ plain / hidden
markerFailThreshold 2 连续几次哨兵超时后该会话降级(不再等满超时)
degradedTimeoutMs 15000 降级后单条命令的等待上限
maxLogBytesPerSession 8388608 单会话日志上限,超出后停止追加
logArchiveAfterDays 7 归档阈值:数字=天(可用小数,0.5 = 12 小时),或时长字符串 12h / 90m / 30s;0 = 关闭归档
logRetentionDays 0 归档保留期:同样的天/时长写法;0 = 永久保留
maxReadBytes 262144 ssh_read 单次上限
maxToolOutputBytes 16384 返回给 AI 的输出上限
defaultPermission ask 新会话默认权限
commandTimeoutMs 120000 单条命令等待上限
approvalTimeoutMs 120000 问答 权限下等待确认的超时
dangerGraceMs 5000 完全 权限下危险命令的可取消窗口
noteContextLines 20 「报错」留言附带的终端输出行数
defaultRows / defaultCols 24 / 80 初始终端尺寸
sshBinary ssh ssh 可执行文件(PATH 名或绝对路径)
extraSshArgs [] 追加到 ssh 命令行的参数
dangerPatterns 内置表 危险命令规则({id, pattern, note}),非空则替换内置表
dangerDisabledIds [] 从生效规则里禁用某些 id
workspaceRoot '' 强制日志/hosts 的根目录;留空则用会话 workspace

例:

- id: dsh-ssh
  name: 'dsh-likemobaxtearm-chumc'
  config:
    defaultPermission: ask
    dangerGraceMs: 8000
    outputLogTruncateChars: 400
    logArchiveAfterDays: 0.5   # 等价于 '12h':半天前的日志自动 gzip 归档
    logRetentionDays: 0        # 归档永久保留(>0 才按天/小时清理)

测试

npm test            # 九套自检,全部通过后打印总项数(当前 253 项)

绝大多数测试不需要正在运行的 Harness —— 它们自己搭环境:

套件 覆盖 依赖
test/inject-audit.mjs 静态审计两半代码里每个 ctx.<服务> 都已在 inject 声明;禁止再出现消费者不可用的 rpc.handle / ctx.webServer;默认 sshBinary 可移植 无
test/connection-contract.mjs 对已安装 connection 源码的事实断言(为什么用 fetch 路由而不是 rpc.handle)+ 真 Cordis 下 fetch 路由可用 DSH 安装
test/cordis-activation.mjs 用真 Cordis 加载真 index.js:9 工具 + 提示段 + 两条路由注册成功,且不碰 webServer、不调 rpc.handle DSH 安装
test/host-smoke.mjs 真 PTY + 真 ssh + 真 sshd:连接、哨兵退出码、回显剥离(含折行)、日志截断、日志合并、三级权限、危险告知、关闭回收 一个可 ssh 的靶机
test/host-rpc.mjs 真 index.js 装配:RPC/流、9 工具、用宿主自带校验器裁决工具 schema 与返回值、留言三类、会话隔离、工作区根解析 DSH 安装 + 靶机
test/client-wiring.mjs 真 client.js 的注册与数据通路:tab 类型/插槽座位、hello→补水、AI 建会话开标签、选中会话不新开宿主标签、常驻首页、二级标签条数据、最近连接与绑定记忆 无
test/log-retention.mjs 日志生命周期(临时目录,不碰真实日志):按天/小时归档、活跃会话保护、保留期、清单、读取尾部、../ 拒绝、超大归档拒绝、0 = 关闭 无
test/windows-conpty.mjs 把三份 Windows 实机报告的字节级证据变成回归测试:\r 重写、空行折叠、重绘判定矩阵、三方言哨兵、两种方言的命令/哨兵块顺序、折行 token、降级与 revokedSource 无
test/identity.mjs 名字与版本一致性:包名在 patch 行/宿主导出/客户端注册/chunk 注册/README 元数据里必须一致;版本号在 README 与客户端构建标记里必须一致;tab kind 与存储键不随包名变 无

两个环境变量:

  • DSH_NODE_MODULES=<node_modules 目录> —— 自动定位不到 DSH 安装位置时显式指定(测试只借它拿 node-pty / cordis / dsh-tools)。
  • DSH_SSH_TEST_TARGET=<host> —— 需要一个可 ssh 的靶机时用(默认 localhost,即本机 sshd)。

只跑不依赖 Harness 的五套(含一致性自检与 ConPTY 用例):

npm run test:offline

量日志指标(和报告同一把尺子)

仓库自带一个度量脚本,口径与几份 Windows 验证报告一致(bytes/line、重复帧、横幅/提示符次数、记账字段、事件分布), 并按验收阈值给出 PASS/FAIL:

node tools/log-metrics.mjs <workspace>/.dsh-ssh/logs/<session>.jsonl
npm run metrics -- <同上>

数据与隐私

插件不把任何数据写进包目录。运行时只写到使用者自己的环境:

位置 内容
<workspace>/.dsh-ssh/logs/*.jsonl 会话活动日志(命令全量、输出截断、口令打码)
<workspace>/.dsh-ssh/logs/archive/*.jsonl.gz 归档日志(超过 logArchiveAfterDays 天自动压缩,保留策略见 logRetentionDays)
<workspace>/.dsh-ssh/hosts.json 保存的主机(target/user/port/name/备注,无口令)
<workspace>/.dsh-ssh/client-errors.log 界面崩溃堆栈(仅出错时产生)
浏览器 localStorage dsh-ssh.recents.v1、dsh-ssh.binding.v1.*(最近连接 / 标签绑定)

⚠️ 日志会包含你在远端执行过的命令及其输出,分享日志前请自行检查。 口令打码是启发式(识别 password/passphrase/密码/口令/验证码/token 等提示行),不是安全边界。 不想要落盘日志就设 logEnabled: false。仓库自带的 dist.mjs 会在打包时拒绝包含日志/hosts/绝对家目录路径的副本。

分发

本仓库是发布镜像:包含作者的运行时代码与构建产物(client.xterm.js)以及可移植测试, 但不含打包脚本 dist.mjs 与 SPEC.md。因此下面的 node dist.mjs 在本仓库不可用 —— 需要打包/改名时请向作者索取完整源码包(或自行按 build.mjs 重建客户端 chunk)。

node dist.mjs                    # 组装干净副本 → dist/<包名>
node dist.mjs --name dsh-ssh     # 改名(会一致改写 package.json / patch 行 / 宿主导出 / client 与 chunk 注册 id)

dist.mjs 只带运行时 + README + 可移植测试,排除 SPEC.md、USER-CHECK.md、dist/、node_modules, 并在输出前自检:没有日志、没有 hosts、没有绝对家目录路径 —— 发现问题会直接拒绝生成。

架构与实现要点

index.js            宿主装配:配置、会话管理器、两条 fetch 路由、9 个工具、提示段
src/session.js      一个会话 = 一个真实 PTY 跑本机 ssh;输出环、哨兵执行、输入权、日志
src/manager.js      会话注册表、审批队列、危险告知、主机配置存储、工作区根解析
src/rpc.js          /api/dsh-ssh/invoke(一元)+ /api/dsh-ssh/stream(NDJSON 帧流)
src/tools.js        9 个模型工具 + 系统提示段
src/log.js          JSONL 日志(截断、打码、上限)
src/ring.js         有界输出环(分页读取、纯文本投影)
src/exec.js         哨兵协议(折行回显剥离、口令提示识别)
src/danger.js       危险命令规则表
client.js           单个「SSH 控制台」宿主标签 + 插件自建的可滚动二级标签条、终端/管理器 UI、浏览器端连接记忆
chunk/terminal.js   xterm 渲染组件(由 build.mjs 打成 client.xterm.js)

几个关键决定(都是被实践逼出来的):

  • 本机 ssh 跑在真 PTY,而不是用 ssh2 库直连:白拿 ~/.ssh/config、密钥、agent、ProxyJump、 密码/2FA 交互,而且 AI 操作的就是你眼前那条 shell —— 这是"双向可见"的前提。
  • 共享单通道:用户与 AI 共用一个 PTY,输入权用 token 互斥,避免"两个通道各说各话"。
  • 传输用 ctx.connection.fetch 的精确路由,而不是 connection.rpc.handle:后者把注册作用域绑在 提供者的 ctx 上并访问 owner.webServer,消费者侧无法满足,会在启动时抛错并拖垮整个插件。
  • chunk 懒加载:xterm 体积大,用 require.async('./client.xterm.js') 按需加载;React 由 client.js 注入 chunk,保证页面只有一份 React。
  • 诊断可观测:客户端组件崩溃会回传宿主写进 client-errors.log;管理器与断开面板显示客户端版本号, 一眼判断"页面是不是旧代码"。

Windows / ConPTY 适配

Windows 桌面版的本地 PTY 是 ConPTY,它会按自己的节奏整屏重绘(\r\033[K + 重写当前行)。 一位 Windows 用户的实测报告指出:一次「接受 host key + 输口令 + 一条 echo」的会话,PTY 实际送来 7,964 字节 / 530 行,人工可见文本只有约 600 字节(平均 15 字节/行,而真实文本是 40–90), ssh_read 翻页拿到的几乎全是空行。0.1.1 针对这条链路做了四件事:

报告中的问题 处理
整屏重绘灌入输出环与日志 投射层把 \r 当作重写当前行(而不是换行),连续空行折叠到 maxConsecutiveBlankLines;日志新增 rawBytes / collapsedBytes / blankLines / rewrites 记账,噪声水平可直接从日志量出来
空闲期也在刷帧 空闲重绘去重:会话空闲 repaintIdleMs 且没有命令在执行时,与上一帧可见文本完全相同、且 ≥128 字节 / ≥4 行的帧不再进环、不再落日志;被丢弃的帧数记在会话状态与下一条日志的 repaintsSkipped / repaintOf 上
重绘插进回显中间导致 stripEcho 失效 回显剥离只做块级删除(不按边界截断):删掉「提示符+命令回显(含折行续行)」「哨兵脚本回显(含折行续行)」「取值行」「尾部提示符」四块,中间剩下的就是真输出。两种方言的块顺序不同:posix 命令与哨兵同一行回显,cmd/PowerShell 哨兵另起一行(于是输出夹在命令回显与哨兵回显之间)—— 按固定顺序截断会把输出删掉,这正是 0.1.3 在 cmd 上正文恒空的原因
ssh_read 分页被空行淹没 readLines() 改为在投射后的可见行上分页
Windows 远端(cmd/PowerShell)没有可用哨兵 remoteShell 三方言:POSIX 用 printf … $?;cmd 的哨兵必须单独一行(cmd /c exit 5 之后换行再 call echo …%ERRORLEVEL%)—— 实测证明写成同一行 … & call echo …%ERRORLEVEL% 拿到的永远是旧值:cmd 在整行解析期就展开了 %ERRORLEVEL%,call 只是把已替换完的字符串再跑一遍;PowerShell 用 Write-Output …$LASTEXITCODE。auto 从提示符形状(PS C:\> / C:\> / $·#)自动判定,结果见 ssh_status 的 detectedShell
无法区分"客户端重发尺寸"和"ConPTY 自发重绘" 新增 session.resize 日志事件(cols / rows / source / duplicates),ssh_status 增加 visibleBytes / repaintsSkipped / repaintBytesSaved / resizeDuplicates

0.1.2 追加修复(来自 0.1.1 的实机验证报告):

  • 陈旧哨兵值:实测 cmd /c exit 5 & call echo TOKEN%ERRORLEVEL% 报 0 —— 改为哨兵独立行; 解析时也只认本次 token 自己的回显之后的第一个取值行(重放的旧屏里出现的是旧 token 或旧取值,天然被排除);
  • 重放污染抓取窗口:审批条出现/消失会把终端行数 16↔14 来回挤,每次 resize 都让 ConPTY 整屏重绘并把上一轮的哨兵行重放进流里 —— 而旧版的重绘去重正好在"命令执行中"被关掉了,于是旧内容进了本次抓取。现在:resize 后 repaintResizeMs 内 历史指纹命中的整屏内容一律按重放丢弃(命令执行中也生效)、审批条改为覆盖层(不再改变终端尺寸)、 客户端的 resize 发送加 250ms 去抖(一次布局变化只发一次 PTY 尺寸)。

0.1.3 追加修复(来自 0.1.1/0.1.2 的实机验证报告):

  • ConPTY 会吃掉「打印后立刻擦掉」的行:报告用 A/B/C/D 对照实验证明,元凶是 \033[K(擦到行尾), 与 SGR 8 隐藏无关 —— 同一渲染帧内被擦掉的内容一个字节都不会进输出流,于是 POSIX 哨兵在 Windows 上永远等不到。 现在 markerMode: auto 在 Windows 上改用明文哨兵 printf '\n<token>%d\n' $?(不隐藏、不擦行); hidden 仍然可用(Linux/macOS 默认)。
  • 折行会把 token 劈开:ConPTY 在 63 列处插入 \b + \n,__DSH_SSH_DONE_f2\b\n85f4__。 现在 token 匹配对 \b/\r/\n 容忍(splitTolerant),解析与剥离都不再因此失效 —— 这同时修掉了 0.1.2 的一个回归:折行时哨兵脚本回显会残留在给模型的结果里。
  • 回显剥离改为按哨兵回显一刀切(单行命令):提示符 + 命令回显 + 哨兵脚本回显是一个整块, 在哨兵回显行结束处切开,不再逐行猜。验收:ver 的结果恰好 1 行、cmd /c exit 5 的结果为空。
  • 降级快速失败:连续 markerFailThreshold 次哨兵超时后,该会话不再等满 commandTimeoutMs, 而是按 degradedTimeoutMs 收尾并在结果里标 markerUnavailable: true,提示改用 ssh_send + ssh_read。
  • revoked 的来源可读:ai.result.revokedSource 记 user keystroke(直接按键)/ user revoke(点「收回控制」)/ teardown; 配合 input.owner 事件的 reason 可以区分「用户打断」与「结算」。
  • 用法建议:连接后等提示符稳定再发第一条命令(登录横幅/升级提示还在刷时抢跑会让第一条命令的哨兵难对齐)。

去重的安全边界(重要):只在空闲时生效 —— 刚写过输入(用户按键或 AI 命令)的窗口内一律不去重, 所以 echo hi; echo hi、tail -f 的重复行都不会被吃掉;单行帧、小于 128 字节的帧也永不去重。 不想要这个行为就设 repaintDedupe: false。这条规则由 test/windows-conpty.mjs 逐条钉住。

已知限制

  • ssh_exec 的退出码是启发式(哨兵):命令后台化或替换掉 shell 时拿不到真实退出码; PowerShell 若执行的是纯 cmdlet(不设置 $LASTEXITCODE)会表现为超时,而不是错误码。
  • macOS / Windows 未实机验证(代码无平台分支,PTY 由宿主提供)。
  • 口令打码是启发式,不是安全边界。
  • 尚未实现(原计划的 M2/M3):SFTP / 文件浏览器、端口转发、跳板机可视化、空闲会话回收、ssh2 结构化执行通道。
  • 会话是进程内的:Harness 重启后进程结束(标签可一键重连,但不恢复远端会话状态)。
  • 远端全屏程序的回放只有有界缓冲,不是完整录像。
  • 宿主标签条溢出时不可滚动(上游 sidebar-right 是 overflow: hidden,且插件不允许改宿主 DOM/样式), 所以会话不占用宿主标签:插件在单个宿主标签内自建二级标签条并自己实现滚动。 代价是宿主的分屏/浮动不再能"按会话"使用(要做分屏得我们在二级层自己实现)。

版本与变更

当前版本 1.0.0(首个正式版本)。完整历史见 CHANGELOG.md。名字与版本的一致性由 test/identity.mjs 自动把关: package.json / bundle patch 行 / 宿主导出 / 客户端与 chunk 的注册 id / README 元数据必须全部相同, 版本号必须同时出现在 README 与客户端构建标记(管理器底部那行 客户端版本:…)里。

1.0.0 由三份 Windows 实机验证报告驱动完成 ConPTY 适配(见「Windows / ConPTY 适配」与 CHANGELOG):

  • 0.1.1 \r 重写语义 / 空行折叠 / 重绘去重
  • 0.1.2 cmd 哨兵独立行 + 取值锚定 + resize 重放去重 + 审批覆盖层
  • 0.1.3 Windows 明文哨兵(擦行会被 ConPTY 吃掉)+ 折行容忍 + 降级快速失败
  • 0.1.4 块级回显删除(修掉 0.1.3 在 cmd 上正文恒空的回归)

验证状态:

环境 状态
Linux(作者本机,真 sshd + 真 Cordis) ✅ 九套 253 项全绿
Windows 10 22H2 + DSH 桌面版 0.2.0-rc.2(第三方实测) ✅ 0.1.3 起 POSIX 哨兵打通、cmd 退出码正确;1.0.0 的块级删除修复尚未在 Windows 复测
macOS ⚠️ 未实测(代码无平台分支,PTY 由宿主提供)

发布清单

npm test                      # 九套自检(当前 253 项)
npm run test:offline          # 零依赖的五套(CI 跑这个 + 语法检查)
node dist.mjs                 # 组装干净副本(自检:无日志 / 无 hosts / 无绝对家目录)
node dist.mjs --name <包名>   # 改名:package.json / patch 行 / 宿主导出 / 客户端与 chunk 注册 / README 一起改

发布前必须成立(test/identity.mjs 与 dist.mjs 会强制):

  1. 包名在 package.json、bundle patch 行、index.js 导出、client.js 与 client.xterm.js 的注册 id、README 元数据里完全一致;
  2. 版本号同时出现在 package.json、README 元数据、客户端构建标记里;
  3. 分发包内没有 .dsh-ssh/、*.jsonl、hosts.json 或绝对家目录路径;
  4. tab 的 kind(ssh-console / ssh)与浏览器存储键不随包名变 —— 改名/升级不丢标签布局与最近连接;
  5. CI(.github/workflows/ci.yml)在 Ubuntu 上跑语法检查 + 五套零依赖自检,全部依赖-free。

许可

MIT © 2026 dsh-ssh contributors —— 可自由使用、修改、分发(保留版权与许可声明)。 如需换成别的许可(例如 Apache-2.0、MPL-2.0),替换 LICENSE 并同步 package.json 的 license 字段即可。

—/ 5

No ratings yet

Verified DSH bundle

Commit f6c931d82bb4

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout