dsh-multi-remote
DSH(DeepSeek Harness)插件:同时支持 SSH / WSL / Docker 三种远程环境。 连上之后,agent 的文件读写、bash 执行、搜索全部在目标环境里执行——大脑留在本地,文件和命令在远端。
当前状态:三种后端全部可用(SSH / WSL / Docker)。 见 CHANGELOG。
这是什么
| 在哪 | |
|---|---|
| 模型调用、思考、决策 | 本地(装了 DSH 的这台机器) |
| 文件读写、命令执行 | 远端(文件只有一份,就在远端) |
界面要看的文件树 / @ 补全 / 改动对比 |
本地锚点目录里的只读镜像 |
那个本地锚点目录是干什么的?会把我整个项目拉下来吗?
不会。 它是给界面看的:DSH 的文件树、编辑器、改动对比读的都是本地文件, 没有这份镜像,界面上就是空的。但它是有界的、只读的、且不是 agent 干活的地方。
分两个阶段,各有边界(都超出即停,并如实标记截断):
① 打开工作区时(要快 —— 用户在等着):
| 边界 | 默认值 | 超了会怎样 |
|---|---|---|
| 目录深度 | 12 层 | 更深的子树不搬 |
| 条目数 | 20 000 个(文件 + 目录) | 剩下的不搬 |
| 单文件大小 | 256 KiB | 本地留一个占位文件(见下) |
| 墙钟时间 | 30 秒 | 到点就停,树可能只覆盖一部分 |
拉取有多快(2026-10-09 实测,WSL、197 个文本文件的项目): 首次全量 ~2.3 秒(批量读:一次调用拉一批,而不是每文件一次); 重连 ~1.1 秒(指纹命中跳过);改动一两个文件后重连,只拉变化的那几个。 SSH 走并发拉取(SFTP 长连接天然支持流水线),Docker 的文件内容本来就走 Engine API 的 archive(一次请求拿一批)。
② 连上之后的「后台补拉」(可以慢 —— 不阻塞任何操作):
| 边界 | 默认值 | 说明 |
|---|---|---|
| 只拉文本类文件 | 按扩展名(源码/配置/文档/脚本…) | 网格、数据库、压缩包、图片不拉 —— 它们本来也不适合在界面里读 |
| 单文件大小 | 2 MiB | 比首轮大得多,覆盖绝大多数源码与文档 |
| 总字节上限 | 64 MiB | 到点就停(不会因为连了个 8 GB 的工程就往本地灌 8 GB) |
| 墙钟时间 | 120 秒 | 每次连接都会补一次;上一轮拉过的会跳过,所以会一轮比一轮覆盖得多 |
为什么要后台补拉:界面的读文件路径是宿主独占的(我们插不进手), 所以「点开文件时自动从远程拉」做不到。退而求其次 —— 在点开之前把 你真正会看的文件先补到本地。实测:一个 1.5 GB 的代码仓库,后台补拉 拉回 138 个文本文件(3.1 MB),README / 配置 / 源码点开就有内容。
占位文件:超过单文件上限的文件,本地会留一个几百字节的说明文件,内容形如:
<!-- dsh-multi-remote:not-synced -->
这个文件没有同步到本地镜像。
远端大小:69.8 MB(单文件上限 256.0 KB)
远端路径:/home/user/my-project/data/mesh.msh
它只在远端 —— 内容没有拉下来(避免为了显示一个文件把几十 MB 搬过来)。
要看或改它,直接在会话里让 agent 读(agent 的文件读写走的是远端,不受这里限制),
或者在会话里用 bash 查看(head / less / file / python 都行)。
这样文件在界面的文件树里看得见(以前是直接跳过 → 树里根本不显示,用户以为不存在), 点开也能立刻知道「它在哪、多大、怎么看」。
⚙️ 后台补拉可以关掉或调参:插件配置
mirrorDeepen = { enabled, maxFileBytes, maxTotalBytes, maxDurationMs }。
实测例子(一个 8.2 GB / 44 662 个文件的工程):
远端:8.2 GB,44 662 个文件
本地锚点:0.37 MB,30 个文件 ← 0.005%,且 >256 KiB 的内容一个字节都没拉
上面量的是首轮格局层(①)的边界效果。之后的后台补拉(②)会在此基础上再补进 「文本类 + ≤ 2 MiB」的文件,所以实际落到本地的会比这个数字多一些 —— 但仍然有界。
⚠️ 代价要如实说(2026-10-09 按代码复核过):
- 项目特别大时,镜像会被边界截断 —— 被截掉的那部分子树本地根本没有, 界面的文件树里也不显示(首轮的深度 / 条目数 / 30 秒墙钟这三个边界)
- 首轮超限(> 256 KiB)的文件,本地只有占位:文件树里看得见、点开有一句说明, 但内容没有拉下来 —— 所以界面里看不到内容、也不能编辑 (2026-10-07 之前是直接跳过:那些文件在树里根本不显示,用户以为不存在)
- 后台补拉会把占位换成真内容 —— 只要它是文本类且 ≤ 2 MiB; 非文本类(网格 / 数据库 / 压缩包 / 图片)或超过 2 MiB 的,保持占位
这几条都不影响 agent 干活(读写、命令、搜索全在远端,不受镜像边界约束), 只影响界面里能看到多少。想看得更全,就把工作区指向更小的目录(比如指向代码仓库本身, 而不是包含备份和数据集的上层目录)。
远端不需要装任何东西:
- SSH:只要有
sshd和一个能登录的账号 —— 不需要联网、不需要 API key、不需要装 DSH - WSL:只要有发行版本身(本机进程,连凭据都不需要)
- Docker:只要容器已在运行(连凭据都不需要)
支持的后端
| 后端 | 状态 | 说明 |
|---|---|---|
| SSH | ✅ 可用 | 用 ssh2 库;host key TOFU,变更一律拒绝 |
| WSL | ✅ 可用 | 驱动 wsl.exe;不需要凭据 |
| Docker | ✅ 可用 | 直连 Engine API(不依赖 docker CLI);只连已运行的容器,不需要凭据 |
三者共用同一套上层(镜像层 / 预设层 / 会话绑定 / 工具)—— 加一个后端只需要写一个 transport 文件,上层零改动。
模型能用上的能力
| 工具 | 干什么 |
|---|---|
remote_connect / remote_profiles / remote_list / remote_disconnect / remote_refresh |
连接管理 |
宿主自带的 bash / read / write / edit |
自动落到远程(在隔离域内换掉了 fs 与 shell) |
glob / grep |
也在远程执行(我们自己实现,用远程的 find / grep;见下) |
人设 / AGENTS.md / 技能 / 目标 / 计划模式 / 后台任务 |
与本地会话同一套(预设里逐项对齐) |
remote_terminal |
远程交互式终端(真 PTY):步进调试器、REPL、需要跨调用保留状态的探索 |
remote_terminal 的要点:
- 真 PTY(Docker / WSL 实测拿到
/dev/pts/N),不是管道 ——sudo、密码输入、 全屏 TUI 都正常 - 状态跨调用保留:
cd、导出的变量、函数、后台任务都留着 - 有界滚动缓冲 + 分页读,不会因为一条
yes把内存吃光 SIGINT真的能打断前台命令(走 PTY 控制字符,命中前台进程组)- 返回的
waitReason告诉你「跑完了」有多可靠:session_exit最确定,inferred_idle是推断,timeout表示可能还在跑
有界的一次性命令请用
bash—— 它更省事,而且有输出上限与超时保护。 终端是给「状态必须留在终端里」的场景用的。
搜索也是在远程执行的
glob / grep 由本项目自己实现(src/world/search.js),在隔离域内通过
ctx.shell 调用远程的 find / grep。
之所以不用宿主自带的那两个:它们用的是打包在宿主里的 ripgrep 可执行文件,
通过 subprocess 接缝运行(宿主源码注释明确写着 "never ctx.shell")——
也就是说跑在你本机、搜的是本地镜像:大于 256 KiB 的文件、
超过 12 层或 2 万条目之外的部分根本搜不到,而且不会告诉你漏了。
自己实现之后,搜索与读文件、跑命令一样是原生远程的 —— 实测在一个 328 KB 的文件里搜到了内容,而那个文件本地镜像里压根没有。
与宿主那份的差异(已写进工具描述,不静默):
| 项 | 说明 |
|---|---|
| 正则方言 | 用远程的 grep -E(POSIX 扩展正则),不是 ripgrep 语法;rg 特有的写法(如 lookaround)不支持 |
| 忽略规则 | 用内置剪枝名单(.git、node_modules、__pycache__…),不读远程的 .gitignore |
| 结果上限 | 超过上限截断并如实标注(宿主那份会写 spill 文件) |
include |
支持简单 glob 与大括号(*.{js,jsx} 会展开成多个 -name) |
需要的话仍可用
bash跑rg(若远程装了)—— 那是另一条路,不受上面的方言限制。
连接远程工作区
有两个入口,它们最终走同一套内部实现,所以行为不会漂移。
入口一:侧边栏的「远程开发」弹窗(推荐)
侧边栏里(和官方的 插件 并列)有一行 远程开发 —— 点开是一个固定大小的弹窗: 左边选后端,右边是识别出来的目标(不用自己填名字):
┌────────────────────────────────────────┐
│ 远程开发 ✕ │
│ 连上 SSH / WSL / Docker 机器… │
├───────────┬────────────────────────────┤
│ WSL 2 个 │ 选择发行版 │
│ SSH 用过 │ ○ Ubuntu-24.04 │
│ Docker │ ○ docker-desktop │
├───────────┴────────────────────────────┤
│ [取消] [连接] │
└────────────────────────────────────────┘
三个后端各自怎么选目标:
| 后端 | 怎么选 |
|---|---|
| WSL | 自动识别已安装的发行版(wsl -l -q,不会启动发行版)—— 一个就直接点,多个就在列表里挑。列表与 wsl -l -q 完全一致;Docker Desktop 自己装的那个系统发行版(docker-desktop)只是排在最后。busybox 发行版也支持(没有 bash 就用 sh,find/stat 走 busybox 兼容写法) |
| Docker | 自动识别容器(运行中的排在前面并标出状态);守护进程没跑时如实说明原因 |
| SSH | 没有「自动识别」这回事(那需要凭据),所以列出用过的连接 —— 点一条就把主机 / 端口 / 账号 / 私钥路径填好,只需再给密码;也可以「新建连接」 |
列表长了会自己出现筛选框(容器可能几十个);标题栏的 ↻ 是「重新识别」—— Docker Desktop 是按需启动的,启动完点一下就有容器了,不用关掉弹窗再开。
「探测失败」和「成功但没有」是两回事:探测失败(守护进程没跑、API 不对…) 会显示原因并给一条手填兜底;而探测成功、只是没有目标(没有容器 / 没装发行版)时, 给的是空状态 + 引导(
docker run/wsl --install,然后点 ↻)—— 那种情况下给输入框只会让人以为「填对了就能连」。
然后分两步(和 VS Code / ZCode 一样):
- 连机器 —— 只连机器,不用填项目路径
- 选项目文件夹 —— 连上后显示远程真实的目录树:点进去、返回上一层, 找到项目后点「就用这个文件夹」。
选好之后插件自动建好工作区(镜像 + 预设 + 锚点),关掉弹窗并切回会话 —— 你只要在那个工作区下面新建会话。
弹窗里没有「打开本机文件夹」:那是 DSH 自己「添加工作区」的活。 本地目录不属于远程开发流程,混在一起只会让人以为它也是远程的一步。
为什么不用一次填完:那要求你盲填远程项目根。填错了(比如漏写一层
桌面/)连接照样「成功」,但之后每条命令都切不到目录 —— 非常难排查。 改成点选之后,路径不再靠猜。
⚠️ 为什么入口在侧边栏,而不是「工作区」旁的
+按钮里: 我们试过把+按钮的选目录流程换成自己的界面 —— 结果官方的 选文件夹插件激活失败,整个 DSH 打不开(那个流程已经有官方归属, 插件之间会冲突)。所以改成完全加法式的入口:多一行导航,谁也不影响谁。实现上用的是宿主自己的三个槽位:
sidebar.panellist(列表型 —— 多一行导航)、main(keyed —— 那一行必须对应一格,用一个新 key 就是加法)、shell.overlay(全窗口浮层 —— 弹窗挂在这里才盖得住整个框架)。 图标由宿主画,我们只交一个currentColor线框 SVG, 所以选中态、配色、留白都和官方一致;弹窗配色走--dsw-alias-*主题变量。
入口二:让 agent 调用 remote_connect
连到 WSL 的 Ubuntu-24.04,远程目录 /home/user/我的项目
agent 会调用 remote_connect(kind 为 ssh / wsl / docker),
然后把返回的锚点目录告诉你。
连上之后(两个入口都一样)
- 把返回的锚点目录作为工作区打开
- 在那个工作区里新建会话 —— 预设只能在会话第一轮之前选定
这一步不能省:旧会话已经定型了。漏了它,你会发现命令还是跑在本机。
关于「模式」:列表里为什么会多一个「远程模式」
新建会话时可以选模式(标准 / PTC / 极简 / 创造)。连上远程之后, 列表里会多出一个「远程模式」 —— 那不是另一种功能,而是远程会话的载体:
- DSH 要求一个会话的插件组合来自一个已注册的预设; 插件要把会话切到「文件与命令都在远程执行」,就必须先注册这么一个预设
- 它的功能与标准模式逐项对齐(读/写/编辑、命令、搜索、终端、计划、目标、技能、 后台任务、AGENTS.md 全都有)—— 差别只在「底下的文件系统与命令执行换成了远程版」
- 它排在最后,是因为连接时才注册(前四个是 DSH 开机就有的)
- DSH 没有「注册但不显示」的开关,所以它一定会出现在列表里
名字固定叫「远程模式」,「是哪台机器、项目在哪」写在它下面那行说明里:
远程模式
WSL docker-desktop · /root/.docker/desktop ← 发行版/容器/主机 · 项目根
为什么名字不拼目标:拼出来是
远程:<派生 id>(WSL <发行版>), 而派生 id 又长又没信息量,会把真正有用的部分挤出显示宽度(实测被截断成远程:wsl-root-docker-desktop(WSL docker-de)。现在把目标放进说明里,整行都是它的。
你不用管它。 在那个远程工作区里新建会话时,插件会自动把会话切到它:
会话创建 → 工作目录在某个远程工作区里吗?
├─ 不在 → 什么都不做(本地会话完全不受影响)
└─ 在 → 切到该工作区的远程预设 → **回读确认**真的生效
→ 失败就拒绝这一步并报错,**绝不让你在本机跑**
⚠️ 所以:在远程工作区里选别的模式会被切回来 —— 这是故意的。 不切的话,那个会话的文件读写会落在本地镜像上: 你以为在改远程的代码,其实改的是本地那份副本(改了、以为生效了、远程根本没变)。
工作区的名字形如 ssh:my-proj、wsl:test-proj —— 后端 + 最后一层目录名。
(完整路径不挤进名字里:选目录那一步的弹窗里已经显示过。)
安装
用自带安装器(推荐)
node scripts/install.mjs --profile <你的 profile>
安装器会按顺序做完三件事,并把每一步的结果打出来:
- 往你的 profile 写入 pnpm 构建脚本表态(本插件零原生依赖,见下)
- 调用
dsh plugin --profile <p> add @jk-uzi/dsh-multi-remote - 回读校验插件真的被登记进了
dsh.profile.bundles,没登记就非零退出
为什么需要第 1 步和第 3 步:pnpm 11+ 默认拒绝任何「没提前表态」的依赖构建脚本, 并且以非零退出码失败;而
dsh plugin只在安装成功时才把插件登记进 bundle 栈。 结果是包装进了node_modules、却永远不会被加载,而且没有任何提示。 安装器就是用来堵这个洞的。
手动安装
dsh plugin --profile <profile> add @jk-uzi/dsh-multi-remote
手动装完之后请确认 dsh.profile.bundles 里有 @jk-uzi/dsh-multi-remote。
没有的话插件不会生效。
从源码(开发用)
git clone https://github.com/jk-uzi/dsh-multi-remote.git
dsh plugin --profile <profile> add "link:$PWD/dsh-multi-remote"
卸载
dsh plugin --profile <profile> remove @jk-uzi/dsh-multi-remote
使用
连上一台机器,然后用 agent 的常规工具干活。
SSH
> 连接 user@example.com:22,项目根 /home/user/proj,用 ~/.ssh/id_ed25519
(调用 remote_connect 工具,kind='ssh')
已连接 …(user@example.com:22)
远程项目根:/home/user/proj
本地镜像锚点:<本地锚点目录>/mr-…
预设:multi-remote-…(可用)
下一步:把上面的「本地镜像锚点」作为工作区打开,然后新建会话。
SSH 掉线了怎么办? 长连接会被网络抖动、服务端重启 sshd、NAT 表项过期打断。 断掉之后插件会自动重连一次,通常你不需要做任何事。 重连时 host key 校验照旧生效(不会因为重连就放行陌生主机)。
WSL
> 连上本机的 Ubuntu-24.04,项目根 /home/user/proj
(调用 remote_connect 工具,kind='wsl', distro='Ubuntu-24.04')
已连接 …(WSL Ubuntu-24.04)
远程项目根:/home/user/proj
本地镜像锚点:…
预设:multi-remote-…(可用)
WSL 不需要任何凭据 —— 它是本机进程。用
wsl -l -q可以列出可用发行版。
Docker
> 连上容器 my-app,项目根 /app
(调用 remote_connect 工具,kind='docker', container='my-app')
已连接 …(容器 my-app)
远程项目根:/app
本地镜像锚点:…
预设:multi-remote-…(可用)
Docker 不需要任何凭据 —— 它连的是本机守护进程。用
docker ps看有哪些容器在跑。 只连已运行的容器,不会替你创建或启动。
Docker Desktop 重启过怎么办? 引擎重启会让旧连接失效,插件会自动重连一次, 通常你不需要做任何事。但注意引擎重启会把容器也停掉 —— 此时报错会说清 「容器没有在运行」并提示
docker start,而不是丢一个看不懂的错误给你。
⚠️ 用户流程有硬约束:必须先选远程工作区(= 锚点目录)、再新建会话。 远程预设只能在会话第一轮开始之前选定,反过来换不了。 走反了的话,插件会拒绝这一步并明确报错,而不是让命令静默落在本机。
工具
| 工具 | 作用 |
|---|---|
remote_connect |
连接一台机器,建立本地镜像 + 注册远程预设 |
remote_profiles |
列出 / 删除已保存的连接档案(list / delete) |
remote_list |
列出当前已连接的远程工作区 |
remote_disconnect |
断开(本地镜像保留,重连后界面立刻有内容) |
remote_refresh |
重新拉取远程目录结构到本地镜像 |
凭据
- 连接档案只保存非敏感字段(主机、端口、账号、路径、私钥路径)
- 口令与私钥内容永不落盘,每次连接现给
- 档案文件权限
0600
设置
设置 → 远程开发 里可以直接改这几项(不用编辑配置文件):
| 项 | 说明 | 默认 |
|---|---|---|
| 连上之后在后台补拉镜像 | 把「文本类 + 上限内」的文件补到本地镜像,界面里点开就有内容 | 开 |
| 后台补拉的单个文件上限 | 只管后台补拉那一趟(MiB)。首轮遍历另有一套更小的常量,不受这项影响 | 2 |
| 后台补拉的总量上限 | 同上(MiB) | 64 |
| 审计日志条数上限 | GET /multi-remote/audit 的环形缓冲容量 |
500 |
想彻底关掉插件? 用 DSH 自己的插件开关(插件管理器里关掉它, 或往 profile 的
cordis.patch.yml写disabled: true)—— 宿主会整行不加载。 我们不自造第二个「显示 / 隐藏入口」的开关:既和宿主重复,又半残 (客户端半边跑在浏览器里,改了配置要刷新页面才生效)。
⚠️ 保存后会重载插件:已连接的远程工作区会断开,重新连一次即可。 这是宿主
configEditor的正常行为 —— 配置变更走 Loader 重新应用。
改动不自己写文件:走宿主的 configEditor 服务(校验 + 持久化 + 重新应用),
落点是 profile 的 cordis.patch.yml 里一条 id: multi-remote 的定向覆盖。
如果那个 profile 没有
configEditor服务(精简 profile),这一页会如实说明改不了, 并给出手工改cordis.patch.yml的办法 —— 不会假装有个能用的开关。
本地开发
不要动你日常在用的 profile。 用独立的 DSH_HOME 建一个隔离 profile:
# 1) 找一个仓库外的目录当隔离 DSH_HOME
export DSH_HOME="$HOME/.dsh-multi-remote" # Windows PowerShell: $env:DSH_HOME = "$HOME\.dsh-multi-remote"
# 2) 从内置模板建一个隔离 profile
dsh --profile dev --from-default-profile web --dump-config
# 3) 挂载本仓库
dsh plugin --profile dev add "link:$PWD"
# 4) 启动
dsh --profile dev --no-open --port 19388
本仓库无构建步骤——宿主直接加载
src/*.js,改完代码重启 profile 即可生效。
自检
| 检查 | 期望 |
|---|---|
GET http://127.0.0.1:19388/multi-remote/ping |
{"ok":true,...},含已连接工作区 |
GET http://127.0.0.1:19388/multi-remote/selfcheck |
5 个工具 registered: true、visibleToModel: true,并报告三种后端各自的可用性 |
GET http://127.0.0.1:19388/multi-remote/workspaces |
已连接工作区的安全描述(不含凭据) |
GET http://127.0.0.1:19388/multi-remote/audit |
审计日志(?limit= / ?event= 过滤,?clear=1 清空) |
审计日志
记面向操作的轨迹:连过哪些机器、成没成、失败原因、栅栏拒绝(安全事件)。
三条约束:有界(环形缓冲,默认 500 条,超界会报 dropped)、
绝不落凭据(统一脱敏 —— 字段名、错误信息、连 URL 里的 ?token= 都会抹掉)、
绝不抛错(审计坏了不能让连接失败)。
curl 'http://127.0.0.1:19388/multi-remote/audit?limit=20'
curl 'http://127.0.0.1:19388/multi-remote/audit?event=connect-failed'
curl 'http://127.0.0.1:19388/multi-remote/audit?clear=1'
自检最有用的部分是 backends 那一段 —— 它先回答「这台机器上哪种后端能用」:
{
"ssh": { "status": "client-ready", "detail": "客户端可用;能不能连上取决于目标 sshd 与账号" },
"wsl": { "status": "available", "detail": "找到 2 个发行版", "distros": ["Ubuntu-24.04"] },
"docker": { "status": "available", "detail": "守护进程 29.8.1,API 1.56",
"endpoint": "\\\\.\\pipe\\dockerDesktopLinuxEngine" }
}
status 的取值:available / unavailable / unsupported / client-ready / unknown。
探不到时会带上原始错误,不会只说一句「失败」。
SSH 那条永远只说「客户端可用」,不会说「你的服务器连得上」—— 自检不该要凭据,也不该在你没让它连的时候发连接。
探测默认开启(三个并行,实测约 50 ms)。脚本里只想要工具状态可以加
?backends=0跳过。
跑测试
node --test # 静态闸门 + 镜像层 + 预设 + transport + 安装器
其中 SSH / WSL / Docker 的端到端测试需要真机,默认跳过:
# SSH(需要一台能登录的机器)
DSH_MR_SSH_TEST=1 \
DSH_MR_SSH_HOST=<host> DSH_MR_SSH_PORT=<port> DSH_MR_SSH_USER=<user> \
DSH_MR_SSH_KEY=<私钥路径> DSH_MR_SSH_SANDBOX=/tmp/dsh-mr-e2e \
node --test
# WSL(需要 Windows + WSL2)
DSH_MR_WSL_TEST=1 DSH_MR_WSL_DISTRO=Ubuntu-24.04 \
DSH_MR_WSL_SANDBOX=/tmp/dsh-mr-wsl-e2e \
node --test
# Docker(需要一个已在运行的容器)
DSH_MR_DOCKER_TEST=1 DSH_MR_DOCKER_CONTAINER=<容器名> \
DSH_MR_DOCKER_SANDBOX=/tmp/dsh-mr-docker-e2e \
node --test
端到端测试只会在
*_SANDBOX指定的远程目录里读写,不会碰其他文件。
结构
| 路径 | 作用 |
|---|---|
src/index.js |
宿主半边入口:组装各层、注册 remote_* 工具与自检路由 |
src/diagnostics.js |
后端可用性探测(自检里的 backends 那一段) |
src/audit.js |
审计日志:有界、脱敏、绝不抛错 |
src/transport/ |
Transport 契约 + 三个后端:SSH(ssh.js)/ WSL(wsl.js)/ Docker(docker.js)+ 最小 tar(tar.js) |
src/mirror/ |
锚点目录的只读镜像(格局层 / 照片层 / 路径围栏 / git 基线) |
src/session/ |
连接档案、连接管理、预设注册(preset.js:单一「远程模式」预设)、会话绑定、工作区路由(workspace-routes.js:路径→连接) |
src/session/plan-mode-section.js |
计划模式的提示词段(由 scripts/sync-plan-mode-section.mjs 生成,别手改) |
src/world/ |
隔离域内的 provider:fs / shell / search(远程 glob/grep)、终端后端与工具的挂载点(terminal.js);按路径动态路由(见 session/workspace-routes.js) |
src/terminal/ |
交互式终端:与后端无关的会话机械(session.js)、三个后端的 PTY(backends.js)、宿主 TerminalBackend 适配(backend.js)、模型工具(tool.js) |
src/http/fence.js |
HTTP 路由的 Host / Origin 栅栏(fail-closed) |
src/client.js |
客户端半边:设置页小节(文案走宿主 i18n,中英双语) |
scripts/install.mjs |
安装器(构建脚本表态 + 安装 + 回读校验) |
scripts/sync-plan-mode-section.mjs |
从宿主的默认预设同步计划模式提示词段 |
依赖
只有一个运行时依赖:ssh2(纯 JavaScript)。
它的可选原生加速 cpu-features 被显式拒绝构建(见 pnpm-workspace.yaml):
允许它会让没有 C++ 工具链的用户直接装不上,换来的只是一点特性探测。
所以本插件零原生依赖,任何平台都能直接装上。
兼容性与测试矩阵
宿主与运行时
| 项 | 要求 | 实测于 |
|---|---|---|
| DSH | 见 peerDependencies(@deepseek-ai/dsh-tools >=0.2.0-rc.2 <0.3.0) |
0.2.0-rc.2 |
| Node | >=20(engines) |
v24.14.0 |
| 插件主机 | Windows / macOS / Linux | Windows 11 25H2 |
| 目标环境 | POSIX shell(原生 Windows 远端不在范围) | Ubuntu / Debian / Alpine |
三个后端各自的前置条件
| 后端 | 需要什么 | 不需要什么 | 可用平台 |
|---|---|---|---|
| SSH | 可达的 sshd + 一个能登录的账号 |
远端不装任何东西(无 DSH、无 API key、不联网) | 全部 |
| WSL | Windows + WSL2 + 一个发行版 | 凭据、网络、远端安装 | 仅 Windows |
| Docker | 可达的 Docker 守护进程 + 已在运行的容器 | docker CLI(我们直连 Engine API)、凭据 |
全部(按平台自动选端点) |
Docker 的端点按平台解析:Windows 用命名管道(dockerDesktopLinuxEngine → docker_engine),
类 Unix 用 /var/run/docker.sock → /run/docker.sock;DOCKER_HOST 显式给出时优先。
tcp:// 与 ssh:// 不在支持范围(需要 TLS 与额外协议),会明确报错而不是假装能连。
WSL 的交互式终端额外需要发行版里有 util-linux 的
script(用来分配 PTY)。没有的话终端会明确报错,其他功能不受影响。
目标环境的实测覆盖
| 目标 | 文件读写 | 命令执行 | 交互式终端 |
|---|---|---|---|
| 真实 OpenSSH 服务器(Linux,有登录 banner) | ✅ | ✅ | ✅ PTY /dev/pts/N、resize 生效 |
| OpenSSH 服务器(容器夹具,CI 可复现) | ✅ | ✅ | ✅ PTY /dev/pts/0、resize 生效 |
| Ubuntu-24.04(WSL) | ✅ | ✅ | ✅ PTY /dev/pts/N |
node:22-bookworm-slim(Docker) |
✅ | ✅ | ✅ PTY /dev/pts/N |
alpine:3.20(Docker) |
✅ | ✅ | 未测 |
scratch(无 shell、已停止) |
✅ 内容读写仍可用 | ⛔ 明确报错 | ⛔ 明确报错 |
真机比夹具多测出三件事(夹具环境太干净,这些都不出现): 登录 banner、中文 motd 造成的终端折行、以及 shell 启动期间的就绪竞态。 其中就绪竞态是个真 bug,已修(见 CHANGELOG)。
alpine上实测发现 busybox 的find没有-printf、stat没有--printf, 所以内部脚本用的是两者都支持的写法(见 CONTRIBUTING §4.11)。
交互式终端的窗口大小变更:SSH / Docker 支持;WSL 不支持 (util-linux 的
script没有这个能力),resize是空操作 —— 如实的能力差异。
交互式终端首次发送前会先等登录 banner 打完再写命令 (否则命令输出会被 banner 的间隙吞掉)。已经安静的终端几乎不增加延迟; 一直不安静也会照常发送,并在结果里如实标
readyReason: 'not-quiet-yet'。
实测过的宿主版本
| 组件 | 版本 |
|---|---|
| DSH | 0.2.0-rc.2 |
| Node | v24.14.0 |
| 宿主 OS | Windows 11 25H2(build 26200.9457) |
| WSL | 2.7.3.0 / kernel 6.6.114.1-1 / Ubuntu-24.04 |
| Docker | Desktop 4.93.0 / Engine 29.8.1 / API 1.56 |
上表是实测过的组合,不是支持范围的下界。
engines声明的是node >=20; 其他平台(macOS / Linux)上 SSH 与 Docker 应当可用,但没有实测过 —— 按本项目惯例,没测过的就标成没测过。
测试怎么跑
node --test # 全部单元与契约测试(不需要任何外部环境)
真机端到端测试默认跳过,按环境变量开关(见下一节)。当前规模
(2026-10-09 复核:node --test → 518 通过 / 14 跳过 / 0 失败):
| 类别 | 数量 |
|---|---|
| 单元与契约测试 | 518 |
| 真机端到端(默认跳过) | 14 |
| 合计 | 532 |
SSH 的多数测试不需要靶机:断线重连那组用
ssh2自带的Server在进程内 起一个真 SSH 服务端(真握手、真认证、真断线),所以不需要网络也不需要 Docker, 已经算在上面「单元与契约测试」里。只有「容器里跑 sshd」那组真机端到端默认跳过。
⚠️ 同时打开多个真机端到端时请加
--test-concurrency=1: Node 的测试运行器默认并行跑不同测试文件,而wsl.exe在并发下会明显退化 (见 CONTRIBUTING §4.6),会把 Docker 的用例拖到超时。 实测:两个都开 + 默认并发 → Docker 用例 32 秒超时;--test-concurrency=1则通过。 这不是代码问题,是测试环境互相干扰。
贡献
欢迎参与。动手前请先读 CONTRIBUTING.md——里面写了开发环境、 插件 API 契约,以及几个很容易踩的坑。
No comments yet. Be the first to write one.