Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ botmux 可以管理 CLI 无关的自定义 Skill Registry,并按 bot 配置在

同一台机器上可运行多个飞书机器人,每个机器人可对应不同的 CLI。同一群聊中通过 @mention 路由消息,仅「你 + 1 个机器人」的 1v1 群无需 @ 自动响应,多人群默认必须 @(可通过「群聊 @ 策略」配置话题内免 @ / 全群免 @);多机器人时 `@<bot1> @<bot2> /t xxx` 可让每个被 @ 的机器人在同一条消息上各自独立开新话题。先发一次 `@<bot1> @<bot2> /introduce` 让它们互相登记 open_id,之后各 bot 就能在自己的会话里显式 @mention 对方协作(命令详见 [📖 文档 · 斜杠命令](https://deepcoldy.github.io/botmux/slash-commands))。

针对一次 PR 的持续协作可用 [`botmux pr-room`](docs/pr-review-room.md):提交 PR
后幂等创建作者/Owner agent review 群,或接管 Owner 已先创建的群;合并、关闭或
废弃时显式结束生命周期并保留审查记录。

### 多话题协作模式

「多机器人协作」的升级版:主 bot(**编排者**)把一个大任务拆成多个**子项目**,在群里**自动开多条话题**,每条话题派一组 bot 并行推进(常见「一个写代码 + 一个 review」),用一张**飞书任务清单**当所有人共享的进度板,最后由主 bot 收齐汇总。一个普通群就是一个并行工作台,你在飞书任务面板一眼看完成度。
Expand Down
91 changes: 91 additions & 0 deletions docs/pr-review-room.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# PR Review Room

`botmux pr-room` 把一次 Pull Request 变成一个有生命周期的飞书协作群。作者
agent、显式选定的 Owner/reviewer agent,以及这些 agent 已绑定的人类 Owner 会被
拉进同一个群;群内对话继续走 Botmux 原有的 agent 会话、工作目录和消息路由。

## 在提交 PR 后建群

作者 agent 创建并 push PR 后执行:

```bash
botmux pr-room open https://github.com/acme/service/pull/123 \
--owner-agent owner-reviewer \
--working-dir /path/to/service
```

- 作者 agent 默认取当前会话的 `BOTMUX_LARK_APP_ID`,也可用
`--author-agent <name|larkAppId>` 显式指定。
- `--owner-agent` 可重复。名称不唯一时命令会拒绝猜测,并要求改用
`larkAppId`。
- PR URL 是 team 内的幂等键;重复执行会返回原 `chatId`,不会重复拉群。
- 建群复用 federation roster 和联邦拉群链路,因此 reviewer agent 在其他
Botmux 部署上也可以被邀请。远端部署会先确认 reviewer 已入群并写入团队信任,
再允许作者 agent 发出 @ kickoff;离线或未就绪部署会明确降级,而不会假报成功。
- 建群后 Botmux 会 @ reviewer agent,要求其独立检查 diff、测试、风险和
可维护性;作者 agent 根据评论修改、验证和 push。
- 建群请求超时或本地连接在提交窗口中断时,Botmux 会把结果标记为“不确定”并阻止自动重试,
避免远端其实已成功却又创建重复群;找到实际群后用 `pr-room adopt` 接管。若
确认没有建成,执行 `pr-room finish` 终止该不确定记录,再用 `--reopen` 重试。

## 接管已经创建的群

如果 Owner 已经先建了 review 群,不要再开一个:

```bash
botmux pr-room adopt https://github.com/acme/service/pull/123 \
--chat-id oc_xxx \
--owner-agent owner-reviewer \
--working-dir /path/to/service
```

`adopt` 会把现有群绑定到该 PR。若 PR 已绑定另一个群,它会拒绝覆盖。传入
`--owner-agent` 时会先验证作者/reviewer 都在群中、同步团队信任,再触发 review;
省略时只建立生命周期记录。`--working-dir` 只绑定作者 agent 的工作目录。

## 修复降级 setup

如果建群已经成功,但 reviewer 邀请、远端信任、工作目录或 kickoff 失败,room 会
保留为 `active/degraded`,重复 `open` 不会假报成功,也不会再建群。修复外部原因后
显式重做 setup:

```bash
botmux pr-room repair https://github.com/acme/service/pull/123 \
--owner-agent owner-reviewer
```

`repair` 只重做尚未完成的群内准备,不创建新群。首次 setup 会持久化 reviewer 和
workdir 意图,所以常规重试不必重复参数;显式传参可替换对应意图。每次 repair 都会
原子领取 attempt,同一 room 的并发命令不会重复发送 kickoff。人类 Owner 未入群或
未绑定会作为独立未完成项保留;确认 Owner 已手动入群后执行:

```bash
botmux pr-room repair https://github.com/acme/service/pull/123 \
--ack-owner-present
```

旧版本遗留、缺少结构化 setup 意图的 pending 记录不会被直接标成 ready,必须显式
补传 `--owner-agent`、`--working-dir` 或 `--ack-owner-present`。

## 结束

PR 合并、关闭或明确废弃后执行:

```bash
botmux pr-room finish https://github.com/acme/service/pull/123
```

这只把生命周期标记为结束。群、消息和审查记录都会保留,不会自动解散或删除。
若 setup 正在执行,`finish` 会先记录结束请求,由当前 setup attempt 完成后原子结束,
避免在 room 已终结后仍发送 kickoff。
`botmux pr-room list` 可查看当前 team 的活跃及历史记录。

结束后的同一 PR 默认不能覆盖原生命周期;如确需重开,显式传 `--reopen`。幂等
锁当前落在发起命令的 Botmux 部署上,因此团队约定由 PR 作者所在的 hub/主部署
执行 `open`;跨部署同时发起尚不提供分布式唯一性保证。

## 边界

该命令负责 PR 创建后的协作编排,不代替 GitHub/SCM 创建、审批或合并 PR。
当前版本要求创建 PR 的 agent 紧接着调用 `pr-room open`;未来可由 SCM webhook
调用同一套生命周期与联邦建群能力,而不改变群内协作模型。
12 changes: 12 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4570,6 +4570,13 @@ botmux v${getVersion()} — IM ↔ AI 编程 CLI 桥接
新建飞书群:
create-group --bot <name> [--bot ...] [--name "群名"]
用指定 bot 起新群;详见 \`botmux create-group --help\`
pr-room open <PR_URL> --owner-agent <name|larkAppId>
为 PR 建作者/Owner agent 协作群;同一 PR 幂等复用
adopt <PR_URL> --chat-id <oc_xxx>
接管 Owner 已创建的 review 群,避免重复拉群
repair <PR_URL> 修复降级的 workdir / 远端信任 / kickoff
finish <PR_URL> 结束生命周期(保留群与审查记录)
list 列出 PR Room

预设分享(导出某 bot 的可分享配置给同事,绝不含密钥):
preset export <bot> [--from-chat <chatId>] [--out <file>] [--yes]
Expand Down Expand Up @@ -9165,6 +9172,11 @@ switch (command) {
case 'dispatch': await cmdDispatch(process.argv.slice(3)); break;
case 'report': await cmdReport(process.argv.slice(3)); break;
case 'create-group': await cmdCreateGroup(process.argv.slice(3)); break;
case 'pr-room': {
const { cmdPrRoom } = await import('./cli/pr-room.js');
await cmdPrRoom(process.argv[3] ?? '', process.argv.slice(4));
break;
}
case 'bots': await cmdBots(process.argv[3] ?? 'list', process.argv.slice(4)); break;
case 'preset': await cmdPreset(process.argv[3] ?? '', process.argv.slice(4)); break;
case 'history': await cmdHistory(process.argv.slice(3)); break;
Expand Down
27 changes: 27 additions & 0 deletions src/cli/arg-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,30 @@ export function firstPositional(args: string[], flagsWithValue: string[]): strin
}
return undefined;
}

/** Collect values from repeatable `--flag value` / `--flag=value` options. */
export function argValues(args: string[], ...flags: string[]): string[] {
const values: string[] = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const equalsFlag = flags.find(flag => arg.startsWith(`${flag}=`));
if (equalsFlag) {
const value = arg.slice(equalsFlag.length + 1).trim();
if (value) values.push(value);
continue;
}
if (!flags.includes(arg)) continue;
const value = args[i + 1];
if (value !== undefined && !value.startsWith('--')) {
const trimmed = value.trim();
if (trimmed) values.push(trimmed);
i++;
}
}
return values;
}

/** Pick the first value from a set of equivalent option names. */
export function argValue(args: string[], ...flags: string[]): string | undefined {
return argValues(args, ...flags)[0];
}
Loading