Bundle
dsh-duck-fleet
DuckFleet: one flock of machines, one command. Add a machine once and its ACP delegation tool appears — drive one, several, or all of them from natural language. / 鸭群:一群机器,一声令下。添加一台机器即自动获得它的派活工具,用自然语言让某个/某几个/全部机器干活。
- Source
- new-sailfish
- License
- MIT
- Updated
- Updated 8 days ago
Readme
# DuckFleet(鸭群)
**中文** | [English](./README_EN.md)
[](https://www.npmjs.com/package/dsh-duck-fleet)
[](./LICENSE)
[](https://github.com/new-sailfish/duck-fleet)
[](https://github.com/awesome-dsh-hub)
> 一群机器,一声令下。
DuckFleet 是一个 DSH 插件:往主控机上装一次,把多台机器接进同一群,
**每台机器自动获得一个派活工具**,然后你就可以用自然语言让「某个 / 某几个 / 全部」机器干活。
```
你:让所有机器汇报当前的 CPU 和内存占用
DuckFleet:→ 并发派给 3 台机器 → 逐台汇总结果回来
```
---
## 它提供什么
- 机器清单一处维护:`$DSH_HOME/fleet.json`,或设置页里的可视化编辑。
- 加机器后**立即**注册好它的 ACP provider 与派活工具,**不用重启**。
- 改 `host` 一个字段即可换地址,运行时当场重注册。
- 被控机**不需要装这个插件**:它只需要 sshd、`dsh`、一个 `acp` profile。
---
## 安装
插件**只装在主控机上**。
### 从 npm
```sh
dsh plugin --profile desktop add dsh-duck-fleet
```
### 从本地检出
```sh
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 是**应用启动时**建立并缓存的,
> 所以**刷新页面没用** —— 正在跑的进程不会重读磁盘上的新 bundle。
>
> 服务端那一半(`fleet_*` 工具、派活)在插件行重新加载后即时生效,**重启只是为了面板**。
>
> 判断标准:**工具能用 = 服务端已更新;面板还是旧的 = 需要重启**。
### 手工挂载(等价做法)
profile 的 `package.json`:
```json
{
"dependencies": { "dsh-duck-fleet": "link:/path/to/duck-fleet" },
"dsh": { "profile": { "bundles": ["…", "dsh-duck-fleet"] } }
}
```
---
## 快速开始
### 1. 主控机:装上插件
装好之后,插件会在**主控机**上:
- 在**设置**里加一个**鸭群**页面;
- 往系统提示注册一段用法说明,让模型知道「某个 / 某几个 / 全部机器」该怎么派活;
- 注册一批管理工具(`fleet_list` / `fleet_add` / `fleet_test` / `fleet_setup` / …)。
打开**设置 → 鸭群**。
### 2. 被控机:只需要装好 DSH
| 需要 | 说明 |
|---|---|
| sshd 在跑 | 主控机要能 ssh 进去 |
| `dsh` 已安装 | **这是唯一的前提** |
| 一个 `acp` profile | 由提示词创建 |
| 主控机的公钥 | 由提示词写入 |
这四件事**都交给提示词里的被控机 agent 自己去查、去装**,不需要手工配。
### 3. 两种送达方式
「自动获取配置」生成的那段提示词,可以这样送到被控机:
| 方式 | 怎么做 | 适用 |
|---|---|---|
| **复制粘贴** | 点**「复制提示词」**,粘到被控机的 DSH 或终端里执行 | 你能直接操作那台机器 |
| **局域网链接** | 点**「改用局域网链接」**,得到一个形如 `http://192.168.1.10:<port>/abcd` 的地址,在**被控机上打开**它取提示词 | 被控机不在手边 |
也可以在任意会话里说:「用 `fleet_setup` 生成被控端配置提示词,用户 `dev`、主机 `192.168.1.10`」。
**局域网链接是临时的**:只在取提示词之前有效,取到即止,最多 1 分钟;
路径是 4 位小写字母,**只有取走它的那一台机器能访问**(其他 host 一律 403)。
它只在本机监听,**不经过任何外部服务**。
提示词自己是自包含的:让被控机的 agent **先查现状再动手**,只装缺的东西,
所以对新机器和配了一半的机器都适用。它只带**公钥**,私钥永远留在主控机。
> **profile 那一步不能省。** `acp` profile 默认带沙箱,而在由 ssh 拉起的无人值守会话里,
> 受限沙箱会让**子进程创建失败**。症状是「能连上、能对话,但什么命令都执行不了」,
> 看起来像机器坏了,其实只差配置文件里的两行。
### 4. 加机器
**设置 → 鸭群 →「添加机器」** 有两条路,选一条即可:
| | 路径 | 适合 |
|---|---|---|
| **推荐** | **「自动获取配置」** | 把提示词交给被控机执行,它查完自己回报,**主控机自动写入并验证** |
| 备选 | **「手动填写配置」** | 你已经知道主机名、账号、端口,或想直接编辑 JSON |
#### 自动获取配置(推荐)
1. 点**「自动获取配置」**,把提示词送到被控机 —— 点**「复制提示词」**粘贴,或点**「改用局域网链接」**让被控机打开链接自取;
2. 被控机执行完,**六个阶段**的进度会实时显示在面板上;
3. 它最后回报完整配置,主控机**自动加进列表并验证**。地址多于一个时,面板让你选。
#### 手动填写配置
1. 点**「手动填写配置」**,填 `label` / `host` / `user` / `port`(其余字段有默认值);
2. 也可以切到 **JSON 视图**直接编辑这台机器的记录;
3. 保存后点**「测试」**验证 SSH + ACP 握手。
名称的 ASCII 字母决定工具名(`Home Server` → `pc_home_server`),随时可改。
#### 也可以让 agent 做
> 用 `fleet_add` 加一台机器:名称 `laptop-01`,主机 `192.168.1.10`,用户 `dev`
或直接改 `$DSH_HOME/fleet.json`,改完不用重启。
### 5. 派活
| 你要的 | 你说 |
|---|---|
| 某个 | 让 **desktop-01** 汇报一下磁盘占用 |
| 某几个 | 让 **laptop-01** 和 **desktop-01** 都检查一下 Node 版本 |
| 全部 | **所有机器**都把日志目录打包发我 |
多台机器**并发**执行,不会等一倍的时间。长任务加 `run_in_background: true`,之后用 `job_output` 收结果。
每次派活都会在任务前面**自动附加**一段「环境与工作规范」——那台机器的环境事实、由事实推出的硬约束、以及跨平台的工作通则。写任务时不要重复这些内容。哪台机器带哪一版由它的 `platform` 字段决定。
---
## 机器字段
机器清单在 `$DSH_HOME/fleet.json`。**共享设置**设一次所有机器继承;**单台字段**每台独立,新建时继承共享设置。
### 共享设置(`defaults`)
| 字段 | 默认 | 说明 |
|---|---|---|
| `keyFile` | 空 | SSH 私钥路径。**必须对,否则 ssh 会静默用别的身份** |
| `sshCommand` | `ssh` | ssh 可执行文件 |
| `profile` | `acp` | 远端 profile 名 |
| `autoArchive` | 关闭 | **实验性**。会话清理规则,见下 |
### 单台字段
| 字段 | 默认 | 说明 |
|---|---|---|
| `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` | 空 | 一句话说明这台的用途,会附到工具描述里 |
| `platform` | `windows` | 那台机器跑什么系统,决定派活前置词带哪一版:`windows` 全带、`posix` 只带工作通则、`none` 不带。**不是 Windows 的机器要显式设置** —— 默认值是假设 |
| `extraArgs` | `[]` | 额外的 ssh 参数 |
`remoteCommand` 不在共享设置里,因为它的可用值是被控机上 `dsh` 的绝对路径,
而那个路径带着那台机器的用户名 —— 共享出去等于让别的机器去跑另一台机器的程序。
---
## 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` | 报告当前加载的是哪个版本的实现 |
插件往系统提示里注册了一段用法说明,告诉模型:先调 `fleet_list` 拿「机器名 → 工具名」的映射,
然后在同一条消息里并发调用。工具 schema 只描述「怎么调」,从不描述「什么时候调」。
---
## 能力边界
DuckFleet 是**给自己用的、内网的工具**。下面是**设计上就没做**的事 —— 不是 bug,是边界。
越过它们,安全性由你自己负责。
### 网络:只面向你信得过的内网
- 插件假设主控机和被控机在**同一个你能控制的网络**里。它**没有**做任何面向公网的设计。
- 局域网链接只做三件事:4 位随机路径、只允许那一台机器读取、取到或超时即关闭。
它**没有** TLS、**没有**账号体系、**没有**速率限制、**没有**审计日志。
- **不要把它反向代理到公网、不要做端口转发对外暴露。** 4 位路径不是密码。
- **主控机与被控机之间没有本插件引入的额外认证**,信任完全建立在 **SSH** 上:
谁能用那把私钥 ssh 上被控机,谁就能在那台机器上派活。私钥保管是你的责任。
- 主控机上的面板路由挂在 harness 已有的监听地址上(默认 `127.0.0.1`)。
改了 harness 的监听范围,面板也跟着变 —— **插件不额外做访问控制**。
### 权限:被控端默认不问
被控机的 ACP profile **默认关掉审批**。那条会话**对面没有人可以回答提示**,问了只会白等一轮;
授权判断由主控机这边承担(记录的 `permission` 字段)。
**后果**:派过去的任务在被控机上以**那个 SSH 账号的全部权限**运行,不会中途停下来问你。
派活之前,**你要对那条命令本身负责**。
### 数据:清单是明文,提示词含公钥
- 机器清单在 `$DSH_HOME/fleet.json`,**明文**,含主机名、用户名、私钥路径。
- 配置提示词含**主控机的公钥**(公钥本就可公开),**私钥永远不进去**。
- 局域网链接把这段提示词挂在局域网上一小会儿 —— 里面没有私钥,但**有主机名、账号名和公钥**。
### 它不做什么
- **不装到被控机上。** 插件本身只跑在主控机。
- **不做增删改以外的编排。** 没有任务队列、没有重试、没有失败自动转移。
- **不代理文件传输。** 要传文件就在任务里让被控机的 agent 自己处理。
---
## 会话清理(实验性,仅限 Windows)
### 它解决什么
每次派活都会在被控机上开一个**新的 ACP 会话**,而 ACP 那条路径**不把会话归入任何 workspace** ——
它只带一个 `cwd`。结果是这些会话全部落进侧边栏的**「未分组」**里,用一次多一个。
主控机这边**没有办法事后补救**:能完成挂载的操作既没有 ACP 等价物,也没有 HTTP 路由。
**能动的只有归档。**
### 怎么用
```bash
# 预演:只探测平台,不改动任何东西
fleet_prune --inspect
# 清理全部机器,保留最近 5 个
fleet_prune
# 只清理一台,保留最近 2 个
fleet_prune --id huawei-vm --keep 2
```
面板里也有对应入口(共享设置下方的虚线框),可以逐台点「清理」。
### 规则
| 项目 | 行为 |
|---|---|
| 候选范围 | **只处理「裸 UUID」目录名的会话** |
| 保留数量 | 最近 N 个(默认 5,可配 `autoArchive.keepLast`) |
| 备份 | 首次运行写 `workspace.json.bak-before-archive`,**不覆盖已有备份** |
| 可逆 | 归档可在侧边栏「已归档」里取消 |
判据是「裸 UUID」:ACP 建的会话 ID 是裸 UUID,而 DSH 界面里建的是 `session-` 前缀。
只按目录名筛选不够 —— 被控机的 `cwd` 往往就是你自己也在用的目录。
### 这个功能会打断正在跑的任务
清理需要**停掉被控机的 DSH**,归档后再自动启动。
**整个过程约 30 秒,期间那台机器上的派活会中断。**
### 平台支持
| 平台 | 状态 |
|---|---|
| Windows | ✅ 已在真机上反复验证 |
| macOS / Linux | ❌ **未实现也未测试**,遇到会**直接拒绝**,不会尝试 |
拒绝而不是「试一试」,是因为两个环节都是平台相关的:注册表路径不同,
而且**停掉之后如何把 GUI 应用放回交互式桌面**,POSIX 的等价做法还没有写过。
> **已知的误判风险**:如果 DSH 将来改了会话命名规则,或者你在同一目录里手工建了裸 UUID 会话,
> 它们会被当作派活会话归档。备份和可逆性是为这个准备的。
---
## 已知限制
- **升级插件后要重启 DSH,面板才会换新**(原因同安装一节)。工具能用 = 服务端已更新。
- **`cwd` 分两侧**:本机 ssh 进程的工作目录永远是本机会话目录;
被控机的工作区走 ACP 的 `session/new` 参数。
- **被控机必须真的能执行命令**:握手成功**不等于**能干活。受限沙箱下 ACP 会话能起来、
能对话,但任何子进程都会失败。`fleet_test` 只验证到握手,**实际执行一次才算验证**。
- **一台机器一个工具**:没有单独的广播工具。「全部」= `fleet_list` 报告出来的每一台,
由模型在同一轮里逐个调用。
---
## 许可证
MIT
Install
dsh plugin --profile web add github:new-sailfish/duck-fleet
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-duck-fleet from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.