@local/dsh-ssh · DSH SSH 插件
在 DSH 右侧边栏里管理 SSH 连接:连接档案、多会话工作区(终端 / 命令 / 文件 / 日志)、SFTP 传输、审计日志,并同时以 Agent 工具的形式暴露给模型。
- host 半边:Node + Cordis 插件(
src/**→lib/**),用ssh2实现连接池、命令执行、PTY 与 SFTP。 - client 半边:自包含懒加载 bundle(
client/src/**→lib/client.js,唯一外部依赖react),注册右侧栏标签与面板,主题全部走--dsw-*token。 - 文档:
docs/DESIGN.md(总体设计)、docs/ICD.md(冻结接口契约 v1.0.x)、docs/M0-SPIKE.md(传输实测)、docs/TESTING.md(分层测试与运行命令)、docs/DEMO.md(验收走查)、docs/REAL-TARGET.md(真机信息,不含密码)、docs/SCREENSHOTS.md(截图工具链:为何布局缺陷必须靠真实浏览器)。
1. 安装
前置:一个 DSH profile —— 桌面版(Electron 应用,profile 名通常为 desktop)或网页端(dsh web,profile 名 web)。本包是免重启装载的本地插件。
1.1 选哪种 profile,以及两种装载路线
| 桌面端(Electron) | 网页端(dsh web) |
|
|---|---|---|
| 启动方式 | 打开 DSH 桌面应用 | 终端执行 dsh web,浏览器打开其地址 |
| 本机在用 | --profile desktop |
--profile web |
| 装载路线 | patch 路线:patch 层插入一条托管 Loader 行 | bundles 路线:写进 dsh.profile.bundles,再由 patch 层按 id 启用 |
两条路线都能用,一条路线装一个插件即可。同时用两条会各加一行(--check 会给告警;本机 desktop 就是这个状态,实测仍可用,但不是推荐形态)。
1.2 安装(按你的 profile 选一条)
# 桌面端(patch 路线,本机默认)
node scripts/profile-install.mjs --profile desktop --install-deps
# 网页端(bundles 路线;--install-deps 会代为在 profile 目录执行 pnpm install)
node scripts/profile-install.mjs --profile web --use-bundles --install-deps
--profile 既接受裸 profile 名(web / desktop,自动解析到 $DSH_HOME/profiles/<name>),也接受 profile 目录路径。每一步写入前都会生成时间戳备份(*.bak-dsh-ssh-<stamp>)。
1.3 验收:确认"真的装上了、而且能组成"
# 一条命令给结论:路线、各块是否存在、node_modules 链接、以及组成是否成功
node scripts/profile-install.mjs --profile web --check # 有问题时退出码非 0
# 组成里应当恰好有一条 dsh-ssh 行,且带完整 config
dsh --profile web --dump-config | Select-String -Pattern 'dsh-ssh' -Context 0,8
--check 的 composition: ok 意味着 DSH 真的解析了这个 profile 并且只找到一条插件行——这是唯一能回答"它到底加载了吗"的检查。网页端随后在浏览器里应当看到:SSH 图标出现在侧栏面板列表 → 新建连接 → 会话页签(终端 / 命令 / 文件 / 日志 / 活动)。
回滚:node scripts/profile-install.mjs --profile web --uninstall。
1.4 故障排查(web 端)
dsh --profile web --dump-config报cannot resolve profile bundle "@deepseek-ai/dsh-experimental-…":这个 profile 的dsh.profile.bundles里声明了一个既不在 DSH 安装里、也不在 profile 依赖里的包(本机就是因为@deepseek-ai/dsh-experimental-agent-team-profile/-auto-review而整个 web profile 起不来,与 dsh-ssh 无关)。处置:把它从dsh.profile.bundles摘掉,或按报错提示dsh plugin --profile web install把它装进 profile 依赖。route: none或组成里没有dsh-ssh行:装到了另一个 profile;用--profile <名字>指对目标,然后重新--check。- 改了
client/src/**后网页端没变化:必须重建lib/client.js(pnpm run build:client);页面只在该文件字节变化时重新执行apply()。host 侧改了src/**要重建lib/**(pnpm run build:host)——装载时写入的 hmr watch 块会让重建后的lib/、src/、client/src/被热重载,无需重启应用。 --check报both routes are in use:patch 层托管块与dsh.profile.bundles各声明了一次。删掉其中一条(--uninstall会同时清掉托管块与 bundle 条目)。
1.5 其它常用命令
node scripts/profile-install.mjs --profile web --status # 只报告,不改文件
node scripts/profile-install.mjs --profile web --dry-run # 只打印将要写入的文件
node scripts/profile-install.mjs --help
要点(M0 实测结论,见 docs/M0-SPIKE.md):
- profile 的
cordis.patch.yml是共写文件,本脚本只增删自己的# >>> dsh-ssh … >>>标记块;禁止整文件重写。 - host 侧改动要重跑
apply()时,用plugin_manager把include:dsh-ssh行 toggle 关→开(entryId是include:dsh-ssh)。
2. 配置
全部配置项与默认值在 cordis.patch.yml(ICD §6)。常用项:
| 配置 | 默认 | 说明 |
|---|---|---|
maxSessions |
10 |
并发会话上限(与"10 会话并发"验收对齐) |
connectTimeoutMs / operationTimeoutMs |
15000 / 120000 |
建连 / 单次操作超时 |
graceKillMs |
3000 |
timeoutMs 到期后 SIGTERM → SIGKILL 的宽限 |
keepaliveIntervalMs / keepaliveCountMax |
20000 / 3 |
心跳与判死阈值(连续失败 → SSH_TIMEOUT_IDLE) |
retries |
2 / 500 / 5000 / true |
仅对 retryable 错误重试,指数退避 + 抖动 |
hostKey.policy |
accept-new |
strict(首连必须人工确认)/ accept-new / insecure |
hostKey.knownHostsFile |
'' → <DSH_HOME>/known_hosts |
OpenSSH 兼容;支持 ` |
sftp.chunkBytes / maxConcurrentChunks |
262144 / 4 |
分块大小与并发分块数 |
sftp.resume / sftp.verify |
true / size+mtime |
断点续传与校验(none/size+mtime/sha256) |
secrets.provider / envPrefix |
credentials / DSH_SSH_ |
凭据来源;环境变量优先级最高且只读 |
logging.redact / redactKeys |
true / […] |
三层脱敏中的日志层 |
maxOutputBytes |
262144 |
单命令输出上限(超限保留头尾并置 truncated) |
activity.enabled |
true |
是否记录 agent 经 ssh_* 工具做的事(「终端」标签的活动镜像,ICD §4.7) |
activity.maxRecords |
200 |
镜像保留的记录条数(环满先丢最老的已结束记录,运行中的不淘汰) |
activity.maxRecordBytes |
65536 |
单条记录的文本上限(超出丢尾部并置 truncated) |
activity.maxTotalBytes |
1048576 |
整个镜像的文本上限 |
ui.defaultWidthPx / terminalFontSize |
420 / 13 |
侧栏宽度与终端字号 |
凭据永远不落配置文件:密码/passphrase 走 ctx.credentials,或用一次性内存凭据(connect 的 secrets),或 DSH_SSH_<PROFILE_SLUG>_PASSWORD 环境变量(只读、UI 显示 source:'env')。UI 与日志中只会出现固定 8 个圆点的掩码。
3. UI 使用
实测状态(2026-09-27,真实浏览器 + 真机
203.0.113.10):连接与交互式终端已端到端打通 —— console 首行 markerssh-client-2026-09-26.4-console-clean,rpc stream sshPlugin/openShell打开,逐键shellWrite {"data":"l"/"s"/" "/"-"/"a"/"\r"}全部ok,终端有提示符且ls -a有真实输出,console 无未捕获错误。host 日志:connected to root@203.0.113.10:22 in 712ms (auth=password(••••••••), hostKey=accept-new)。原文与复现方法见docs/ACCEPTANCE.md§11。 十条验收标准全部通过(用户 2026-09-27 实机走查):GUI 内top全屏、文件页签的目录导航(进入/..返回)、上传(rpc stream sshPlugin/upload+ 传输条进度)、下载(download open+ 6938 B 文件落地)、多会话标签与亮/暗主题切换,均已实测通过。逐条原话与 console/host 日志见docs/ACCEPTANCE.md§11.7/§11.8。 证据边界(如实):100 MiB 级传输的闭环在引擎/工具层(真机 100.0 MiB 实测);UI 侧实测为小文件(6938 B),不宣称“UI 端完成 100 MiB 传输”。 平台限制:当前桌面端组合无凭据服务("persisted": false, "reason": "no credentials service in this composition"),密码只存在于会话内存,每次重启 DSH 需重新输入;属平台能力缺失,非插件缺陷。
- 打开右侧栏:点侧栏底部 SSH 动作(
sidebar.footer.action),或会话输入框左侧的 SSH 图标;Ctrl/Cmd+Shift+S亦可。 - 新建连接(
Ctrl/Cmd+T):填 名称 / 主机 / 端口 / 用户 / 认证方式;测试连接(conn.test)会显示延迟、服务端 banner 与主机密钥指纹。 - 首次连接:策略为
strict时弹指纹确认;accept-new自动记住到known_hosts;指纹变化一定触发二次确认(SSH_HOSTKEY_MISMATCH)。 - 连接后进入会话工作区,四个标签:
- 终端:真 PTY,跑
top/vim等全屏程序正常,支持复制粘贴、清屏、字号、重连。这个标签有两面,用标签内的一行切换:- 终端:交互式 PTY —— 你的会话;
- AI 活动:镜像模型经
ssh_*工具做过什么 —— 命令与其实时 stdout/stderr、上传/下载、列目录、连接/断开,每条带主机、状态、退出码与耗时;模型在另一台主机上干活时这里也看得见。切换器上的圆点是未读计数,每条记录可一键复制。 - 跟随规则:有活动的会话直接开在活动面;新活动自动切过去,除非你正在终端里打字、或你自己选过面(显式选择不再被自动覆盖)。
- 命令:单命令 stdout/stderr/退出码/耗时,历史上下翻;
- 文件:远端目录浏览、上传/下载(进度、断点续传、校验)、新建/重命名/删除/chmod;
- 日志:本会话审计(脱敏后),可导出。
- 终端:真 PTY,跑
- 多会话标签(
Ctrl/Cmd+1..9跳转、Ctrl/Cmd+W关闭并二次确认),状态栏显示延迟、连接时长、流量。 - 断开:状态栏
断开,或关标签。快捷键总表见 ICD §8.4。
4. Agent 工具
allowAgentTools: true(默认)时向模型暴露 7 个工具:ssh_connect、ssh_disconnect、ssh_sessions、ssh_exec、ssh_upload、ssh_download、ssh_list_dir。工具名与 cordis.patch.yml 的 tools 列表一致(单测断言两者相等),也与 dsh.plugin.json 一致。
4.1 模型做的事在面板里看得见(agent 活动镜像,ICD §4.7)
工具调用发生在 host 侧,不经过浏览器,所以模型干活时面板本来是看不到的。现在每次工具调用都由 host 记进一个有界的活动镜像,在「终端」标签的 AI 活动 面按时间列出:命令及其实时 stdout/stderr、上传/下载(含进度行)、列目录、连接/断开/列会话;每条记录带主机(user@host)、状态、退出码、耗时与失败码,可一键复制整条。
- 端点:
sshPlugin/followActivity(流,无参数 —— 镜像是全局的,每条记录自带sessionId,所以另一台主机上的工作也不会被藏起来)与sshPlugin/clearActivity(清全局历史)。 - 边界(刻意如此):镜像是视图不是日志 —— 只在内存里保留最近若干条(
activity.*),不落盘、不脱敏、不承诺跨重载或重启存活;要事后追查请读审计文件(queryAudit,脱敏后 JSONL 落盘)。镜像持有的正是你自己会话的输出,所以命令打印的密钥也会显示在这里 —— 这是与审计文件的有意区别(ICD §12 R10)。 - 关闭:
activity.enabled: false时整体不记录(零事件、空快照),工具行为不变。
4.2 会话里的 ssh_exec 也是一张终端卡片
同一次调用在会话流里显示为终端卡片:$ <命令>、按 host 的 [stderr] 标记分节的输出、退出码或信号徽标,以及 outcome / 耗时 / streamId 等事实行。这不是 DSH 自动给的:Web 客户端不消费 host 的 presentCall/presentResult 视图,而是按 wire 工具名分派键控槽位 tool.call.toolview,未注册就落回通用行(Tool call · ssh_exec · <第一个字符串参数> 加 Input/Output 文本)。插件因此在自己的 client 半边注册了 key: 'ssh_exec'(client/src/session/toolview.js)。
5. FAQ
Q. 面板是空的 / 标签打不开?
先看侧栏 SSH 标签内的诊断条:它显示客户端解析出的传输通道(carrier)。carrier: unresolved 表示浏览器→host 的通道都没命中;此时 host 侧仍可正常,问题在页面。诊断记录写在 <DSH_HOME>/logs/dsh-ssh/client-transport.json。
Q. 改了代码但界面没变?
- 改
client/src/**→node scripts/build-client.mjs;只有lib/client.js字节变化时页面才会重新 apply()。 - 改
src/**(host 半边)→ 本机(Windows)无法热更新:lib/**重建后不会产生任何 reload 事件,toggleinclude:dsh-ssh行也只是让已缓存的模块重跑一次apply(),新代码进不来。要生效必须重启 DSH。 - 改配置值(profile 的
cordis.patch.yml)→ toggle 行即可:apply()会用新配置重跑;这与"模块代码是否重新加载"是两件事。 - 判据(不要再靠猜):host 每次
apply()都写<DSH_HOME>\logs\dsh-ssh\host-ready.json,里面是这个活着的实例实际注册的remoteMethods列表 —— 方法数没变,就是新代码没进来。 - 实测证据与根因(
hmr行的ignored默认值在 Windows 上把整棵监视树都忽略掉)见docs/ACCEPTANCE.md§13 与docs/TESTING.md§2.5。
Q. pnpm install 报 cpu-features 构建被跳过?
非致命:ssh2 自动回退纯 JS 加密。本机实测仍有 30 MiB/s 上传、20 MiB/s 下载(docs/TESTING.md 有实测数字)。
Q. 连接超时/一直转圈?
看错误码:SSH_NET_REFUSED(端口拒绝)、SSH_NET_DNS(域名解析失败)、SSH_TIMEOUT_CONNECT(建连超时,可调 connectTimeoutMs)、SSH_TIMEOUT_IDLE(keepalive 连续失败,链路已死)。retryable: true 的错误会自动退避重试。
Q. 为什么没有跳板机 / 端口转发 / 密钥生成?
本期非目标(见 docs/DESIGN.md §1)。密钥可用系统 ssh-keygen 生成后在档案里选私钥文件。
Q. 传输中断了要重传吗?
不用:sftp.resume: true 时续传,resumedFrom 回带;verify: 'sha256' 会在传输后比对,不一致报 SSH_SFTP_VERIFY_MISMATCH(可续传重试)。
Q. 测试怎么跑?
见 docs/TESTING.md:node scripts/verify-all.mjs(一键 lint + 类型 + 构建 + 单测 + 组件 + 集成 + E2E + 性能),真机层用 --real + DSH_SSH_TEST_REAL_* 显式开启。
6. 开发
node node_modules/typescript/bin/tsc -p tsconfig.json --noEmit # 类型(第一道门)
node scripts/lint.mjs # lint
node scripts/build-client.mjs && node scripts/build-client.mjs --check # 客户端产物 + 确定性
node --test --test-concurrency=1 --test-force-exit "test/unit/*.test.mjs" # host 单测
node --test --test-concurrency=1 --test-force-exit "test/client/*.test.mjs" # 组件
node --test --test-concurrency=1 --test-force-exit "test/integration/*.test.mjs" # 集成(本地 sshd 靶机)
node test/e2e/run.mjs # 无头 E2E 走查
node scripts/verify-all.mjs # 全部(每层硬超时)
同一时刻只允许一份全仓测试在跑(ICD §12 R9):并发跑会让某个文件看起来"卡住"。verify-all 会先自检并在检测到另一份测试运行时拒绝启动(除非 --allow-concurrent)。
7. 截图
docs/img/ 下是组件测试与真机走查用的界面截图(亮/暗各一套):
| 亮色 | 暗色 |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
8. 自测结果
(本节由交付者维护,见 docs/TESTING.md 的"自测结果"小节,含逐层命令、结果与已知缺口。)








No comments yet. Be the first to write one.