内容
<div align="center">
<img src="./assets/banner.svg" alt="space-bus Banner" width="100%" />
# 空间总线
> 跨工作区控制项目代理
[](https://www.npmjs.com/package/@fro.bot/space-bus) [](https://github.com/fro-bot/space-bus/actions) [](LICENSE)
[简介](#简介) · [安装](#安装) · [配置](#配置) · [工具](#工具) · [库接口](#库接口) · [Claude 桌面端](#claude-桌面端) · [开发](#开发)
</div>
---
## 简介
OpenCode 的工作区代理总线。控制代理 — 普通的 OpenCode TUI 运行并安装此插件 — 在单个 `opencode serve`/`harness serve` 实例上为您的名单中的每个项目任务专门的代理,使用每个请求的目录路由。薄的 stdio MCP 外观公开相同的工具给 Claude 桌面端。
**前提条件**:已经运行的 `opencode serve` 或 `harness serve` 实例;如果您使用 Claude 桌面端 MCP 二进制文件,则需要 Bun(`bunx`)。
## 安装
将插件添加到 `opencode.json`:
```json
{
"plugin": ["@fro.bot/space-bus"]
}
```
然后在同一目录中添加一个 `spacebus.json` 名单:
```json
{
"server": {
"baseUrl": "http://127.0.0.1:4096"
},
"projects": [
{
"name": "我的项目",
"path": "~/src/my-project",
"description": "我的项目的代理运行时和 API"
}
]
}
```
## 配置
空间总线从工作区目录(OpenCode 启动的目录)读取 `spacebus.json` 名单。
字段:
- `server.baseUrl` — 必须解析为 localhost (`127.0.0.1`、`::1` 或 `localhost`);非本地主机被拒绝,因此总线凭证永远不会离开机器。
- `projects[].name` — 传递给 `bus_task` 的 `project` 参数的标识符。
- `projects[].path` — 项目的文件系统路径;支持 `~` 扩展。
- `projects[].description` — 在 `bus_roster` 输出中显示。
设置 `SPACE_BUS_CONFIG` 以覆盖名单发现 — 它必须是绝对路径或以 `~` 开头(URL 和裸相对路径被拒绝)。名单在每次工具调用时都会被读取 — 没有缓存,因此编辑立即生效。
## 工具
- `bus_roster` — 列出空间总线清单项目以及每个项目的实时会话状态。
- `bus_task` — 将提示分派给给定空间总线清单项目中的代理,或通过传递 `sessionId` 来控制现有会话(回答其待处理问题,否则发送后续提示)。立即返回;不等待完成。除了格式化文本外,结果还带有结构化 `{sessionId, project, mode}` 元数据 — 插件 `ToolResult.metadata` 在工具表面,MCP `structuredContent` 在_stdio_ 表面 — 用于调用者无需解析字符串即可获取调度结果。
- `bus_status` — 报告空间总线会话的状态以及其最新待办事项和差异的摘要。还报告会话何时被交互式问题阻塞,等待回复。
- `bus_result` — 返回已完成的空间总线会话的最终助手消息和差异。如果会话仍在运行,则会报错 — 使用 `bus_status` 进行检查。
## 受控服务器
`server` 的两种名单模式,互斥:
- `server.baseUrl` — 外部管理的,仅附加。今天的默认行为:您自己启动 `opencode serve`/`harness serve`;插件只是连接。
- `server.managed` — 插件管理的生命周期。插件为您启动和监督服务器。
```json
{
"server": {
"managed": {
"command": ["harness", "serve"],
"cwd": "~/src/my-workspace",
"port": 0
}
},
"projects": [{ "name": "我的项目", "path": "~/src/my-project" }]
}
```
`command`、`cwd` 和 `port` 都是可选的(默认值:`harness serve` — 设置 `managed.command` 为自定义命令,如 `opencode serve` — 名单的目录,和一个临时端口)。第一个调用者启动:无论哪个消费者首先触摸托管名单,都会按需启动服务器 — 生成的密码和 0600 发现文件位于状态目录下( `$XDG_STATE_HOME|~/.local/state/space-bus/<hash>/discovery.json`),每个后续调用者都附加到同一个实例。它是一个持久守护进程,而不是请求范围的进程 — 它比调用者寿命长,并且如果它死亡,则没有自动重启;下一个 `ensure` 调用会注意到过时的发现文件并通过生成新的文件进行修复。
CLI(`space-bus`,包装相同的生命周期):
```sh
space-bus serve [--foreground] [--json] [--config <path>]
space-bus status [--json] [--config <path>]
space-bus stop [--json] [--config <path>]
```
MCP 默认情况下仅附加 — 它从不生成。设置 `SPACE_BUS_MCP_SPAWN=1` 在 MCP 服务器的环境中,以选择加入托管名单的服务器启动。
安全立场:每个生成都生成一个全新密码(从不重复使用,从不在 argv 中,从不记录);循环守卫随发现握手一起传输,因此附加的端点无论来源如何都重新验证为仅限本地访问。相同用户进程泄露超出范围 — 任何可以读取您的状态目录或追踪子进程的人已经拥有他们需要的东西。
可以直接导入 `@fro.bot/space-bus/server` (仅限 Node.js;`ensureServer`/`serverStatus`/`stopServer`)的程序消费者(例如 Mothership),以直接驱动生命周期而无需 CLI。
## 库接口
实验性子路径导出直接公开总线的语义 — 相同的函数由四个工具运行 — 用于渲染器和其他需要结构化状态而不是格式化字符串的消费者。子路径是实验性的:形状可能会在小版本中更改;如果您采用它们,请固定版本。
- `@fro.bot/space-bus/core` — 浏览器安全;无 Node.js 内置模块,无环境变量读取。每个导出的函数都接受一个 `context: BusContext`(`{ roster, credentials? }`),您需要自己构建并传入 — 核心永远不会为您解决它。包括 `snapshot()`,一个一次性复合的名单 + 每个项目的状态 + 待处理问题,带有限的扇出。
- `@fro.bot/space-bus/config` — 仅限 Node.js。`loadContext(directory?)` 读取 `spacebus.json`(与插件相同的方式处理 `SPACE_BUS_CONFIG`)并返回一个可用的 `BusContext`,带有每个项目的 `exists` 标志和环境变量派生的凭证。每次调用都构建一个新的上下文;按照约定,它是每个调用/短寿命的,不适用于跨文件系统更改缓存。
- `@fro.bot/space-bus/contract` — OpenCode API 和 `BusContext` 背后的 zod 模式(和推断类型),用于直接访问服务器的消费者,并希望使用相同的形状。模式是 zod v4。
- `@fro.bot/space-bus/format` — 工具用于渲染输出的纯格式化程序,用于工具相同的文本。
Node.js 示例(`/config` + `/core`):
```ts
import { loadContext } from "@fro.bot/space-bus/config";
import { snapshot } from "@fro.bot/space-bus/core";
const context = loadContext(); // 从 SPACE_BUS_CONFIG 或您传递的目录解析 spacebus.json — 无 cwd 回退
const result = await snapshot({ context });
if (result.ok) {
for (const project of result.projects) {
console.log(project.name, project.exists);
}
}
```
浏览器消费者:`/core`、`/contract` 和 `/format` 是浏览器安全的(它们的模块图在 CI 中经过测试,以排除 `node:*` 导入,并且从不访问 `/config`)。`/config` 仅限 Node.js — 浏览器代码无法调用 `loadContext`,因此凭证必须由构建 `BusContext` 的服务器端进程显式注入(永远不会被核心读取)。本地主机守卫随上下文一起传输:它在核心的单个验证门处重新检查,因此从非本地 `baseUrl` 构建的上下文在那里被拒绝,而不仅仅是在配置加载时。
## Claude 桌面端
```json
{
"mcpServers": {
"space-bus": {
"command": "bunx",
"args": ["--package=@fro.bot/space-bus", "space-bus-mcp"],
"env": {
"SPACE_BUS_CONFIG": "/absolute/path/to/spacebus.json"
}
}
}
}
```
需要 `opencode serve`/`harness serve` 已经在名单的 `baseUrl` 上运行。
## 开发
```sh
bun install
bun run fixture # 生成 gitignored fixtures/dev-workspace/(opencode.json + spacebus.json)
harness serve --port 4096 & # 或:opencode serve --port 4096
opencode --dir fixtures/dev-workspace # 或直接打开该目录
bun run smoke # 冒烟测试:目录路由隔离对抗实时服务器
bun run test
bun run typecheck
bun run lint
bun run dev # 监视构建到 dist/
```
`@opencode-ai/*` 版本与 OpenCode CLI 锁定步调。一起升级两者。设置 `OPENCODE_SERVER_PASSWORD` (和可选的 `OPENCODE_SERVER_USERNAME`)以启用每个总线请求的 HTTP 基本身份验证。
## 实现说明
- 会话存储在目录头全局范围内:`GET /session/{id}` 解析无论哪个项目目录被发送。 总线通过会话自己的 `directory` 字段将会话归因于其所属项目,而不是探测头。 `GET /session` (列表)和 `/session/status` 按目录范围。
- 上游 opencode #30127(v1.16.0)将会话级差异摘要归零,因此 `GET /session/{id}/diff` 始终返回 `[]`。用户消息的每回合差异(`GET /session/{id}/message`)保持完整并包括未跟踪的文件,因此 `bus_status`/`bus_result` 聚合这些(最后一个回合按文件赢取,像上游 PR #33444 一样)。 Harness 构建 ≥`1.17.13+harness.ee55e157` 直接携带 #33444,因此 `GET /session/{id}` 的 `summary.diffs` 字段已填充并提供差异,无需每回合聚合(仍标记为 `diffSource: "session"`);库存二进制文件将其留空并回落到每回合聚合。 `GET /vcs/status` 仍然是一个最后的手段,整个仓库范围的回退,标记为 *工作树*。
- `/session/status` 可以在最终消息可查询之前报告会话空闲一拍;`scripts/smoke.ts` 通过对消息获取的有限重试来吸收这一点。
- 养成了从 API 原始路径中导航的需求 — 委托阻止交互式问题。 该导航路径最终成为 `bus_task` 上的可选 `sessionId` 而不是第五个工具:传递它会回答待处理问题或发送现有会话的后续提示。 `bus_status` 还会显示待处理交互式问题(`pendingQuestion` / 一行 `blocked:`),因此不会将阻塞的委托误认为正在工作的委托。
## 发布
PRs 通过 `bunx changeset` 推送更改集。合并到 `main` 后,CI 会打开一个版本 PR;合并该 PR 会通过受信任的发布(OIDC)发布到 npm — 无需 `NPM_TOKEN`。
---
<div align="center">
<sub> Fro Bot 生态系统的一部分 </sub>
</div>