Skip to content
dsh.fish
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)

[![npm version](https://img.shields.io/npm/v/dsh-duck-fleet)](https://www.npmjs.com/package/dsh-duck-fleet)
[![License](https://img.shields.io/npm/l/dsh-duck-fleet)](./LICENSE)
[![DSH plugin](https://img.shields.io/badge/DSH-plugin-4dabf7)](https://github.com/new-sailfish/duck-fleet)
[![Listed on awesome-dsh-hub](https://img.shields.io/badge/Listed%20on-awesome--dsh--hub-4dabf7)](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

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source