🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

SSH 控制台 qaz320621/dsh-likemobaxtearm-chumc

DeepSeek Harness
qaz32062131 天前有提交已提供中文文档扫描:含敏感能力

在右侧栏开 SSH 会话,与 AI 共用终端并留审计日志

29.9/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/5 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add qaz320621/dsh-likemobaxtearm-chumc
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:需留意
是什么[目录]—
dsh 原生插件 · platform
暂无时间戳·判据 →
装得上吗[实装验证]!
实装验证未通过(unknown),装前请到仓库确认最近更新与 issue
L4 · 真实安装等级说明 →
数据截至 2026/11/05·判据 →
安全吗[自动扫描]—
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
数据截至 2026/11/04·判据 →
还在维护吗[仓库]—
更新放缓:最近一次提交在 31 天前
暂无时间戳·判据 →
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →

可信度证据

⚠︎ 实装验证未通过(unknown · 2026/11/5) ——可能是验证环境差异,装前建议到 GitHub 仓库确认最近更新与 issue。
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

读写/删除本地文件发起外部网络请求
查看扫描证据(共 24 处,展示前 20 条)
  • 写入本地文件
    index.js:229
    mkdirSync(diagnosticsDir(), { recursive: true });
  • 写入本地文件
    index.js:230
    appendFileSync(join(diagnosticsDir(), 'client-errors.log'), line, 'utf8');
  • 发起网络请求
    client.js:332
    const response = await fetch(INVOKE_PATH, {
  • 发起网络请求
    client.js:478
    const response = await fetch(STREAM_PATH, {
  • 写入本地文件
    src/log.js:64
    mkdirSync(dir, { recursive: true });
  • 写入本地文件
    src/log.js:65
    this.#stream = createWriteStream(this.path, { flags: 'a' });
  • 读取本地文件
    src/log.js:234
    try { names = readdirSync(dir); } catch { return result; }
  • 读取本地文件
    src/log.js:245
    try { stat = statSync(path); } catch { continue; }
  • 写入本地文件
    src/log.js:248
    mkdirSync(archiveDir, { recursive: true });
  • 读取本地文件
    src/log.js:249
    writeFileSync(join(archiveDir, `${name}.gz`), gzipSync(readFileSync(path), { level: 6 }));
  • 写入本地文件
    src/log.js:249
    writeFileSync(join(archiveDir, `${name}.gz`), gzipSync(readFileSync(path), { level: 6 }));
  • 删除本地文件
    src/log.js:250
    unlinkSync(path);
  • 读取本地文件
    src/log.js:260
    try { archived = readdirSync(archiveDir); } catch { archived = []; }
  • 读取本地文件
    src/log.js:265
    if (statSync(path).mtimeMs < retentionCutoff) {
  • 删除本地文件
    src/log.js:266
    unlinkSync(path);
  • 读取本地文件
    src/log.js:282
    try { names = readdirSync(base); } catch { return; }
  • 读取本地文件
    src/manager.js:27
    if (existsSync(this.#path)) {
  • 读取本地文件
    src/manager.js:28
    const parsed = JSON.parse(readFileSync(this.#path, 'utf8'));
  • 写入本地文件
    src/manager.js:68
    mkdirSync(dirname(this.#path), { recursive: true });
  • 写入本地文件
    src/manager.js:69
    writeFileSync(this.#path, `${JSON.stringify({ profiles: this.#profiles }, null, 2)}\n`, 'utf8');
扫描于 2026/11/4(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

社区实装还没有人打卡 · 装过的第一个就是你
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 右侧栏开SSH会话
  • 与AI共用终端
  • 留存审计日志

README

由 DeepSeek 最新模型翻译生成
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.
展开完整文档(README 剩余 1 节)
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\033K + 重写当前行)。
一位 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 字段即可。
预览分享卡片长什么样 ↓qaz320621/dsh-likemobaxtearm-chumc
分享 · DeepSeek Harness Hub
qaz320621/dsh-likemobaxtearm-chumc

在右侧栏开 SSH 会话,与 AI 共用终端并留审计日志

这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。
认领条目 →
回到顶部