DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

jk-uzi /

jk-uzi/dsh-multi-remote

Verified

DSH 插件:SSH / WSL / Docker 三种远程开发 —— agent 的文件读写、命令、终端全部落在目标环境

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@17691cf2

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 一样):

  1. 连机器 —— 只连机器,不用填项目路径
  2. 选项目文件夹 —— 连上后显示远程真实的目录树:点进去、返回上一层, 找到项目后点「就用这个文件夹」。

选好之后插件自动建好工作区(镜像 + 预设 + 锚点),关掉弹窗并切回会话 —— 你只要在那个工作区下面新建会话。

弹窗里没有「打开本机文件夹」:那是 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), 然后把返回的锚点目录告诉你。

连上之后(两个入口都一样)

  1. 把返回的锚点目录作为工作区打开
  2. 在那个工作区里新建会话 —— 预设只能在会话第一轮之前选定

这一步不能省:旧会话已经定型了。漏了它,你会发现命令还是跑在本机。

关于「模式」:列表里为什么会多一个「远程模式」

新建会话时可以选模式(标准 / 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>

安装器会按顺序做完三件事,并把每一步的结果打出来:

  1. 往你的 profile 写入 pnpm 构建脚本表态(本插件零原生依赖,见下)
  2. 调用 dsh plugin --profile <p> add @jk-uzi/dsh-multi-remote
  3. 回读校验插件真的被登记进了 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 契约,以及几个很容易踩的坑。

许可证

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 17691cf2ff8c

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