DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

new-sailfish /

new-sailfish/duck-fleet

Verified

Herd computers like ducks. 🦆

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

DuckFleet(鸭群)

中文 | English

npm version License DSH plugin Listed on awesome-dsh-hub

一群机器,一声令下。

DuckFleet 是一个 DSH 插件:往主控机上装一次,把多台机器接进同一群, 每台机器自动获得一个派活工具,然后你就可以用自然语言让「某个 / 某几个 / 全部」机器干活。

你:让所有机器汇报当前的 CPU 和内存占用

DuckFleet:→ 并发派给 3 台机器 → 逐台汇总结果回来

它解决什么问题

原来的做法是:每加一台机器,就要在 cordis.patch.yml 里手写两行挂载 —— 一个 dsh-subagent-acp provider、一个 dsh-tool-subagent 工具, 还要自己对 id / providerName / toolName / maxDepth / cwd / permission 这些字段。

写错了不一定会报错,可能只是工具不出现。机器一多,这份 YAML 就变成了一堆复制粘贴。

装上 DuckFleet 之后:

  • 机器清单变成一处数据($DSH_HOME/fleet.json,或者一个可视化设置页);
  • 加一台机器,插件立刻在同一进程里注册好它的 ACP provider 和派活工具;
  • 换地址只需改 host 一个字段,运行时当场重注册,不用重启;
  • 被控机不需要装这个插件,它只需要 sshd + dsh + 一个 acp profile。

安装

插件只装在主控机上。

从 npm

dsh plugin --profile desktop add dsh-duck-fleet

从本地检出

git clone git@github.com:new-sailfish/duck-fleet.git
dsh plugin --profile desktop add /path/to/duck-fleet

dsh plugin add 会把包装进 profile 并同步到 dsh.profile.bundles。

装完要重启一次 DSH。

面板属于客户端半,而客户端插件的 bundle 是在应用启动时建立并缓存的 —— DSH 以 immutable 缓存它,版本号由文件的时间戳与大小算出,所以刷新页面没用: 正在跑的进程不会重读磁盘上的新 bundle。

服务端那一半(fleet_* 工具、派活)在插件行重新加载后即时生效,重启只是为了面板。

手工挂载(等价做法)

profile 的 package.json:

{
  "dependencies": { "dsh-duck-fleet": "link:/path/to/duck-fleet" },
  "dsh": { "profile": { "bundles": ["…", "dsh-duck-fleet"] } }
}

改完插件服务端源码后,把 fleet 这一行在设置里禁用再启用一次即可生效; 改了客户端(lib/client.js,即面板)则要重启 DSH。


快速开始

1. 主控机:装上插件

dsh plugin add 装好之后,插件会做这些事(都在主控机上):

  • 在设置里加一个鸭群页面;
  • 往系统提示注册一段用法说明,让模型知道「某个 / 某几个 / 全部机器」该怎么派活;
  • 注册一批管理工具(fleet_list / fleet_add / fleet_test / fleet_setup / …)。

此时清单还是空的,一台机器都还没有 —— 插件此刻只是在服务你,还没连上任何机器。 打开设置 → 鸭群,没有机器时页面上会直接展开新手引导。

2. 被控机:只需要装好 DSH

被控机上不需要装这个插件。它需要的是:

需要 说明
sshd 在跑 主控机要能 ssh 进去
dsh 已安装 这是唯一的前提
一个 acp profile 由下面的提示词创建
主控机的公钥 由下面的提示词写入

SSH 服务、防火墙、profile、公钥 —— 这四件事都交给提示词里的被控机 agent 自己去查、去装, 不需要你手工配,也不需要你懂这些细节。

3. 拿到配置提示词 —— 三个入口,两种送达方式

入口(主控机上,任选一个):

入口 位置
新手引导 设置 → 鸭群 → 「新手引导」按钮(右上角,任何时候都在)
编辑机器时 添加/编辑机器的对话框里 → 「复制被控端配置提示词」
让 agent 拿 直接说:「用 fleet_setup 生成被控端配置提示词,用户 dev、主机 192.168.1.10」

送达(把提示词送到被控机,两种方式):

方式 怎么做 适用
复制粘贴 点**「复制提示词」**,粘到被控机的 DSH 或终端里执行 你能直接操作那台机器
局域网链接 点**「改用局域网链接」,得到一个形如 http://192.168.1.10:<port>/abcd 的地址(端口由系统分配),在被控机上打开**它取提示词 被控机不在手边(另一台机器、另一块屏幕)

局域网链接是临时的:默认 5 分钟自动关闭、4 位小写字母的随机路径、只有取走它的那一台机器能访问(其他 host 一律 403), 取走后可以点「立即关闭」马上失效。它只在本机监听,不经过任何外部服务。

这段提示词自己是自包含的:它让被控机的 agent 先查现状再动手,只装缺的东西, 所以同一段提示词对新机器和配了一半的机器都适用。它只会带上公钥,私钥永远留在主控机。

为什么 profile 那一步不能省:acp profile 默认带沙箱。在由 ssh 拉起的无人值守会话里, 受限沙箱会让子进程创建失败(Windows 上表现为 0xC0000142 / STATUS_DLL_INIT_FAILED, 连 Write-Output 都起不来)。症状是「能连上、能对话,但什么命令都执行不了」, 看起来像机器坏了,其实只是少了两行配置。提示词里会写入正确的 cordis.patch.yml。

4. 回来加机器

按提示词走完,再回到主控机把这台机器登记进来:

方式 A:让 agent 加(推荐)

用 fleet_add 加一台机器:名称 laptop-01,主机 192.168.1.10,用户 dev

名称的 ASCII 字母会决定工具名(Home Server → pc_home_server),你也可以随时手改。 加完立刻调用 fleet_test 验证 SSH + ACP 握手(不花 token)。

方式 B:设置页 —— 设置 → 鸭群 → 「添加机器」,填 host 和 user,保存,点「测试」。

方式 C:直接编辑 $DSH_HOME/fleet.json —— 改完不用重启。

5. 派活

三种范围都支持,用自然语言说就行:

你要的 你说
某个 让 desktop-01 汇报一下磁盘占用
某几个 让 laptop-01 和 desktop-01 都检查一下 Node 版本
全部 所有机器都把日志目录打包发我

多台机器是并发执行的(同一条消息里每台各调一次工具),所以不会等一倍的时间。 长任务加 run_in_background: true,之后用 job_output 收结果。


机器字段

机器清单在 $DSH_HOME/fleet.json。共享设置只与主控机有关,设一次所有机器继承; 单台字段每台独立,新建时从共享设置继承。

共享设置(defaults)

字段 默认 说明
keyFile 空 SSH 私钥路径。必须对,否则 ssh 会静默用别的身份
sshCommand ssh ssh 可执行文件
profile acp 远端 profile 名
autoArchive 关闭 实验性。会话清理规则,见下

为什么 remoteCommand 不在这里:它的可用值是被控机上 dsh CLI 的绝对路径, 而那个路径里带着那台机器的用户名(比如 C:/Users/dev/AppData/...)。共享出去就等于 让别的机器去跑另一台机器的程序。判据是:共享值不能依赖任何一台被控机。

单台字段

字段 默认 说明
label 必填 显示名,也是工具名的来源
host / user / port 必填 / 必填 / 22 SSH 目标
toolName 由 label 派生 模型调用的工具名,可随时改
remoteCommand dsh 怎么在那台机器上启动 agent。非交互式 SSH 的 PATH 里通常没有 dsh,所以值的常态是绝对路径,如 C:/Users/dev/AppData/Local/Programs/DeepSeek Harness/resources/runtime/cli/bin/dsh.cmd
cwd 空 那台机器上的工作区。空 = 让远端开自己的默认工作区;真实派活时退回主控机会话的工作区
permission allow 那台机器上授权提示怎么答
description 空 一句话说明这台的用途,会附到工具描述里
extraArgs [] 额外的 ssh 参数

agent 管理工具

工具 用途
fleet_list 列出机器:toolName user@host label + 工作区 + 权限 + 注册状态
fleet_add 加机器或改机器(按 id 或 label 匹配)
fleet_remove 删机器,并注销它的 provider 和工具
fleet_test 只做 SSH + ACP 握手,不花 token
fleet_setup 生成给被控机的配置提示词(附公钥)
fleet_defaults 读/改共享设置
fleet_version 报告当前加载的是哪个版本的实现

自然语言是怎么找到正确工具的

插件往系统提示里注册了一段用法说明(ctx.systemPrompt.section), 告诉模型:先调 fleet_list 拿「机器名 → 工具名」的映射,然后在同一条消息里并发调用。

这段说明是必需的:工具 schema 只描述「怎么调」,从不描述「什么时候调」。 没有它,「在所有机器上跑」就没有收件人,而用户按设置页里看到的名字点名的机器, 也只能靠猜。


能力边界(请先读这一节)

DuckFleet 是一个给自己用的、内网的工具。下面这些是设计上就没做的事 —— 不是 bug,是边界。 如果你的用法越过了它们,安全性要你自己负责。

网络:只面向你信得过的内网

插件假设主控机和被控机在同一个你能控制的网络里(局域网、VPN、或者本来就连得通的两台机器)。 它没有做任何面向公网的设计:

  • 局域网链接是为了省去手打的麻烦,不是一套对外服务。 它只做三件事:一个 4 位随机路径、 只允许取走它的那一台机器再读(其他 host 403)、5 分钟后自动关闭。 它没有 TLS、没有账号体系、没有速率限制、没有审计日志。
  • 不要把它反向代理到公网、不要做端口转发对外暴露。 那样做的话,上面那点保护等于不存在 —— 4 位路径不是密码,5 分钟也不保证没被扫到。我们没为这种用法做安全策略。
  • 主控机与被控机之间没有本插件引入的额外认证。 信任完全建立在 SSH 本身上: 谁能用那把私钥 ssh 上被控机,谁就能在那台机器上派活。私钥的保管是你的责任。
  • 主控机上的 HTTP 面板路由挂在 harness 已有的监听地址上(默认 127.0.0.1)。 你如果改了 harness 的监听范围,这个面板也跟着变 —— 插件不额外做访问控制。

权限:被控端默认不问

被控机的 ACP profile 默认关掉审批(approval: policy: never)。 理由很直接:那条会话对面没有人可以回答提示,问了只会白等一轮。授权判断由主控机这边承担 (机器记录的 permission 字段)。

后果:派过去的任务在被控机上以那个 SSH 账号的全部权限运行,不会中途停下来问你。 所以 —— 派活之前,你要对那条命令本身负责。

数据:清单是明文,提示词含公钥

  • 机器清单在 $DSH_HOME/fleet.json,明文,含主机名、用户名、私钥路径。
  • 配置提示词里含主控机的公钥(公钥本就是可以公开的),私钥永远不进去。
  • 局域网链接把这段提示词挂在局域网上一小会儿 —— 里面没有私钥,但有你的主机名、账号名和公钥。

它不做什么

  • 不装到被控机上。 被控机只需要 sshd + dsh + acp profile,插件本身只跑在主控机。
  • 不做机器的增删改以外的编排。 没有任务队列、没有重试、没有失败自动转移 —— 派出去的活由被控机上的 agent 自己完成,出错了由模型逐台汇报。
  • 不代理文件传输。 要传文件就在任务里让被控机的 agent 自己处理。

会话清理(实验性,仅限 Windows)

它解决什么

每次派活都会在被控机上开一个新的 ACP 会话。而 ACP 那条路径不把会话归入任何 workspace —— 它只带一个 cwd,从不调用 workspace.attachSession。结果是这些会话全部落进侧边栏的 **「未分组」**里,用一次多一个,几十次之后就没法看了。

主控机这边没有任何办法事后补救:能完成挂载的操作(workspaceRegistry.attachSession) 既没有 ACP 等价物,也没有 HTTP 路由,而插件的所有派活都走 ACP。

能动的只有归档。 workspace.json 里的 archivedSessionIds 是一个平铺集合, validateStoredState 对它的内容只要求 id 存在 —— 所以「不属于任何 workspace 的会话」 也可以被隐藏。

怎么用

# 预演:只探测平台,不改动任何东西
fleet_prune --inspect

# 清理全部机器,保留最近 5 个
fleet_prune

# 只清理一台,保留最近 2 个
fleet_prune --id huawei-vm --keep 2

面板里也有对应入口(共享设置下方的虚线框),可以逐台点「清理」。

规则与安全性质

项目 行为
候选范围 只处理「裸 UUID」目录名的会话
保留数量 最近 N 个(默认 5,可配 autoArchive.keepLast)
备份 首次运行写 workspace.json.bak-before-archive,不覆盖已有备份
可逆 归档可在侧边栏「已归档」里取消
写入校验 写完重新解析,并逐字比对每个 workspace 的标题

判据为什么是「裸 UUID」:ACP 建的会话 ID 是裸 UUID(1da62ac9-…), 而 DSH 界面里建的会话带 session- 前缀(session-f7737053-…)。 只按目录名筛选是不够的 —— 被控机的 cwd 往往就是操作者自己也在用的目录, 那样会把你自己的会话一起归档。这个区分是实测出来的,不是推测。

这个功能会打断正在跑的任务

清理需要停掉被控机的 DSH(注册表在内存里,不停就会被覆盖写回),归档后再自动启动。 整个过程约 30 秒,期间那台机器上的派活会中断。

平台支持

平台 状态
Windows ✅ 已在两台真机上反复验证
macOS / Linux ❌ 未实现也未测试,遇到会直接拒绝,不会尝试

拒绝而不是「试一试」,是因为两个环节都是平台相关的:注册表路径不同, 而且停掉之后如何把 GUI 应用放回交互式桌面,Windows 用的是计划任务 + 交互式主体, POSIX 的等价做法还没有写过。

已知的误判风险:如果 DSH 将来改了会话命名规则,或者你在同一目录里手工建了裸 UUID 会话, 它们会被当作派活会话归档。备份和可逆性是为这个准备的。


已知限制

  • 主控机要是被控机的「上级」:插件在运行时注册 provider 和工具,所以加机器不用重启 (改 fleet.json、共享设置、机器增删都立即生效)。
  • 升级插件后要重启 DSH,面板才会换新。这不是本插件的缺陷,而是 DSH 加载客户端插件的方式: 客户端 bundle 在应用启动时建立,以 immutable 缓存,版本号由文件的时间戳与大小算出 —— 所以刷新页面没用,运行中的进程不会重读磁盘。 服务端那一半(fleet_* 工具、派活)在插件行重新加载后立即生效,重启只是为了面板。 判断标准很简单:工具能用 = 服务端已更新;面板还是旧的 = 需要重启。
  • cwd 分两侧:本机 ssh 进程的工作目录永远是本机会话目录(一个只存在于被控机的路径 不能当本机的工作目录 —— 那样连 ssh 这个名字都解析不出来,报 ENOENT: spawn ssh ENOENT, 读起来像「ssh 没装」)。被控机的工作区走 ACP 的 session/new 参数。
  • 被控机必须真的能执行命令:握手成功 不等于 能干活。受限沙箱下 ACP 会话能起来、 能对话,但任何子进程都会失败。用 fleet_test 只能验证到握手,实际执行一次才算验证。
  • 一台机器一个工具:没有单独的广播工具。「全部」= fleet_list 报告出来的每一台, 由模型在同一轮里逐个调用。

许可证

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit d0f4a94a3352

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