
频道、花名册与 路由把多智能体协作塞进聊天界面【免费下载链接】OpenMausBotOpen-source Grok Bot alternative with a virtual machine that bots can use项目地址: https://gitcode.com/GitHub_Trending/op/OpenMausBot当“多智能体”从演示 demo 走向日常使用最先暴露的不是模型能力而是界面承载方式一个对话框里塞进三五个 Bot上下文必然糊成一团每开一个任务就复制一份 Agent配置又会分裂成无数个近似副本。OpenMausBot 选择的路线是把协作还原成人类最熟悉的心智模型——频道Channel/Room 花名册Roster 提及频道承载场景与共享上下文花名册定义谁能被叫到 决定这一句话由谁接。本文不谈概念宣传而是沿着仓库源码把这三层拆开看频道记录里到底存了什么、 路由如何在服务端与渲染端保持一致、上下文为什么不会互相干扰以及“把工作流做成消息流”必须付出哪些工程代价。一、频道不是复制粘贴的 Bot而是一份共享记录在数据模型里频道就是WireGroup服务端持久化为GroupRecord它不是“一个 Bot 的副本”而是一组指向既有 Bot 的 id 加上一层场景配置。核心字段集中在 shared/wire.tsthreadId频道当前活动任务的会话线程tasks?: GroupTask[]是频道下并列的多个任务新任务在前。也就是说“频道”与“对话线程”是一对多频道可以不断开新任务而不丢失历史。memberIds花名册只是对 Bot 记录的引用所以同一批 Bot 可以同时出现在工作、生活、项目三个频道里各自的模型、账号、虚拟机配置只有一份。bulletin房间级共享指令长期约定与每个成员的 profile 分开description是标题下的一行短描述注释特意写明null explicitly cleared, so a clients shallow merge drops a stale value——客户端浅合并时如何安全清空字段这类细节在协作型 UI 里比功能本身更容易翻车。defaultResponder无人被 时的兜底路由取值为everyone/mentions/member/auto。cwd/pinnedCwd房间的共享工作目录。它有一个非常“产品化”的约束见 server/room-folder.ts房间会一直保留它创建时的文件夹如果该目录消失就直接在回合开始前报错——This rooms working folder no longer exists: … Create that folder again: a room keeps the folder it started in.而不是让引擎误把问题算到某个 Bot 头上。section侧边栏分区audienceFloor这个房间历史最窄的可见范围——注释写得很直白Bot 退出不会放宽谁可以看这段对话记录。权限只会收紧不会因为成员变动被悄悄放大。dm?: booleanBot 与 Bot 自动建的一对一频道走同一套结构但被排除在 holds、任务列表、everyone等群语义之外。值得注意的是频道共享的是“场景”指令、目录、成员、兜底路由而不是“上下文”——上下文是每个成员各自一份的这正是下一节要处理的问题。二、独立上下文一个频道N 份互不干扰的会话多智能体聊天最容易出现的问题叫“上下文串味”A 的中间推理被 B 当成事实或者 B 把 A 的临时草稿当作最终结论继续往下推。OpenMausBot 的处理分三层。第一层给 Bot 的输入是一个有界窗口而不是无限长的聊天记录。server/host/room-transcript.ts 负责把线程渲染成给模型看的文本并且只截取最后GROUP_CONTEXT_MESSAGES条export const GROUP_CONTEXT_MESSAGES DRAIN_COALESCE_MAX_ITEMS; // 30 ... const context messages.filter(isRoomContextMessage); const readable context.filter((m) !narration.has(m.id)); const shown readable.slice(-GROUP_CONTEXT_MESSAGES);isRoomContextMessage只放行三类消息有正文的text、摘要digest、以及roomRequest.phase result的队友结果回报。工程日志、审批卡片下的过程旁白roomTurnNarrationIds不会进入窗口。这个 30 不是随手写死的常量server/admission.ts 里给出了理由Room turns read only the room-context windows last N messages, so a bigger group would append lines the responder never sees.也就是说队列合批上限必须小于等于上下文窗口否则一次合并进 40 条消息模型只看得到后 30 条前面 10 条就变成了“说了但没人看见”的幽灵输入。第二层每个成员有自己的 token 账本与压缩阈值。server/room-context.ts 的RoomContextTracker是按(threadId, botId)记账的record(threadId, botId, reading, model, catalog) // tokens window dispatch(threadId, botId, instanceId, settings) // { reset, displaced } takeReset(...) // reason: requested | threshold默认阈值DEFAULT_ROOM_COMPACT_AT 0.9可通过context.roomCompactAt比例或绝对 token 数与autoCompact调整定义在 server/room-context-settings.ts 与 server/config.ts。用户看到的不是一段沉默而是一行明确说明由roomResetNote生成reset.reason requested ? compacted ${name}s context (${reset.percent}% full): fresh session with the rooms recent messages : ${name}s context was ${reset.percent}% full: compacted to a fresh session with the rooms recent messages同时dispatched()里有一句关键注释Each engine keeps one warm process per thread, so this turn closes the other members processes on its engine.同一引擎在同一线程里只保留一个热进程轮到别人说话时前一个成员在该引擎上的进程会被关闭并计入displaced。这既是资源约束也是隔离手段不让两个 Bot 的进程在同一线程里共享状态。第三层增量续传必须可证明“没漏行”。当context.roomSessions打开或OMB_ROOM_SESSIONS1时server/host/room-sessions.ts 只把该成员没见过的行追加进它原来的原生会话。文件头第一行就是一条不变式// Invariant: every window line is in the text sent, or was shown to this same native session in exactly this form; any doubt sends the whole window.任何不确定例如进程被保活跨过了会话起点、strictResume不可用、房间上下文在两次轮次之间被改写都退化为“整窗重发”而不是赌一把增量。这与delta-context.ts里“房间帖子/Webhook 帖子从未有过原生会话”的处理是同一套保守策略——宁可多花 token也不让某个成员基于自己没看到的半截对话行动。三、 路由一处定义、两端复用、三类边界渲染端要点亮Juniper服务端要据此决定唤醒谁。如果两边各写一套正则UI 高亮的名字和服务端真正路由的名字迟早会分叉。仓库把最底层的判定抽到了 shared/mention-boundary.ts前后端共用const OPENING_MENTION_BOUNDARY /[(*_~[{\u2018\u201c\u3008\u300a\u300c\u300e\u3010\uff08]/u; export function isMentionBoundary(text: string, at: number): boolean { if (at 0) return true; const before text[at - 1]; return /\s/u.test(before) || OPENING_MENTION_BOUNDARY.test(before); } export function isMentionNameContinuation(value: string | undefined): boolean { return value ! undefined /^[\p{L}\p{N}\p{M}_]/u.test(value); }注释解释了取舍只允许空白与“开括号/Markdown 前缀”作为边界因此/bot仍然是 URL 路径而(bot)、【bot】、**bot**会被认作提及名字后面紧跟字母、数字、组合符号或下划线则不算命中——所以Juniper2、調査担当者不会误伤Juniper、調査担当。在这层之上服务端的选人逻辑是 server/store.ts 的mentionedBots与roomRespondersconst candidates peers .filter((p) !p.hidden p.name.trim()) .sort((a, b) b.name.length - a.name.length); // 长名优先避免短名前缀抢先排序是关键细节如果花名册里同时存在Review和Reviewer长名优先匹配否则Reviewer会先被Review吃掉。roomResponders再叠加三层优先级出现独立的everyone同样走边界与续接判定→ 所有可见成员文本里点名了谁 → 只唤醒被点名者去重后按出现顺序都没命中 → 按defaultResponder兜底everyone全员、member指定主责、mentions不唤醒任何人、auto交给决策模型。auto的实际行为在 server/index.ts 的发送路径里只有当“无人被 、没有 goal 协调者、房间成员多于 1”时才走自动路由并且决策模型的回答是异步的兜底而不是阻塞——// Autos responders are a fallback until the decision model answers; the append and the return stay synchronous.消息先落库、接口先返回路由结果随后补齐否则每次发消息都要卡在模型延迟上。渲染端则在 src/lib/group-routing.ts 复刻同一套语义用于输入框的提示文案groupComposerHint、目标协调者goalCoordinatorForComposer与发送前预测src/lib/mentions.ts 再负责把命中的名字转成带颜色的区间颜色只取MAUS_COLORS白名单值不把任意字符串塞进 CSS。src/components/Composer.tsx 里的候选列表、Tab/Enter 选择、Escape 关闭、以及“输入法确认 Enter 不抢候选”的判断都建立在这套共享判定之上。三类边界值得单独说明归档与隐藏成员。被 到已归档的 Bot不会静默失败而是在线程里追加一条活动消息X is archived and cant respond — restore it or mention an active room member.隐藏成员从一开始就被filter((m) !m.hidden)排除。everyone 的适用范围。它只在频道成立在一对一 DM 里既不显示高亮也不进入候选docs/verification/mentions.md 专门记录了这条验证路径。引用与代码里的 不算点名。maskQuotedAndCode会把代码围栏、行内代码、引用块和引号内的内容折叠成“只保留 名字”的形式避免文档里的示例提及被当成真实指令。四、别让“消息流”变成“互相刷屏”队列、持有与熔断把工作流画成消息流最性感的 demo 是“Bot 们互相讨论直到完成任务”最难的生产问题是同一个它们会互相回帖到天荒地老。仓库为此准备了三道机制。1频道级队列工作期间的新消息不直接进 transcript。server/channel-queue.ts 的头注释说明了动机Follow-ups sent while a channel works stay off the transcript until it settles, so the current responder cannot appear to answer words it never saw.队列本身是持久化的saveChatFollowup/settleChatFollowupsstatus走pending → dispatching → settled/cancelled/interrupted进程重启后由restoreChannelMessages()从 SQLite 恢复因此“重启丢消息”不会发生。合批规则在headChannelGroup→drainCoalesceHead同一发送者、间隔不超过DRAIN_COALESCE_WINDOW_MS 120_000、最多 30 条合并成一次轮次而带via: api的调用和 goal 模式永不合并coalesceIdentity返回api:${id}/mode:…因为它们没有“人在连续打字”的语义。2成员级互斥一次只允许一个 Bot 在这个线程里说话。server/room-member-lock.ts 用一张botId → threadId的占用表封住“Bot 刚被唤醒但还没标记 busy”的窗口// Covers the gap before a member turn marks the bot busy: woken waiters must claim in the same synchronous step, or all read the bot as free. claim(botId, threadId): boolean { ... }配合groupSpeakers当前发言者与groupTurnOperations更宽的“正在处理”涵盖 setup、排队中的响应者前端拿到的是RoomQueueEntry见 server/room-queue.ts能看到“谁在跑、谁在第几位、在等谁”。这让“排队”变成可展示的状态而不是一个转圈的加载图标。3循环熔断把“无限自我对话”当故障处理。server/room-post-budget.ts 是这套设计的精华——它不假设 Bot 会自觉。判定顺序是熔断冷却5 分钟→ 60 秒内同 Bot 同文重复 →环检测→ 单发送者 60 秒 10 条 → 5 分钟内无人回应的 Bot 帖子达 2 条就要求升级给人。环检测的写法很克制function closesRing(senders: string[], candidate: string): boolean { const tail [...senders.slice(-3), candidate]; if (tail.length 4) return false; const [first, second, third, last] tail; return first last new Set([first, second, third]).size 3; }也就是A→B→C→A这个最小环一旦闭合房间发帖立即关闭并设置trippedAt返回给模型的话术明确到不留后路Posting into this room is now closed… Do not call post_to_room again for this room — say what you wanted to post in your own reply, where the user will read it.注意Refusals are never recorded: counting them would let a blocked bot deepen its own block.——被拒绝的调用不计入预算否则越撞墙越出不来。五、把工作流做成消息流产品取舍清单这套设计并非没有代价源码里到处是“我们选择了哪一边”的痕迹串行优先于并行。同群发言者串行要并行必须显式开启并行房间作业docs/verification/room-coordination.md 记录了这条边界。串行的好处是读者永远能顺着一条时间线读懂“谁在回应什么”坏处是长任务会排队。仓库的补偿是把排队显性化队列位置、waitingForBotId、waitingForTeammates而不是假装实时。有界窗口优先于全量历史。30 条窗口 90% 压缩阈值意味着频道可以很长但任何单个成员看到的永远是最近的、有界的片段。这也是为什么长目标必须靠结构化协调而不是靠“大家读完整段聊天”。“发帖”与“干活”分离。post_to_room的说明写得很清楚A post adds one message to the room; it does not start anyones turn, so nobody replies to it automatically.写进频道 ≠ 触发回合。真正的回合由人的消息、 提及、goal 协调者或队友委派驱动。这条分离避免了“发一条状态更新引爆七个 Bot”。stop 是房间语义不是单个 Bot 语义。X stop会命中holdserver/room-holds.ts并且在消息入队之前就应用这样停机能追上正在干活的成员被 hold 的成员期间收到的点名会被记入heldMessages等resume或再次 时以“你被暂停期间这些话是找你的”形式补投。stop 词的距离判定STOP_WINDOW 2与对象判定this/that/my…时不算命令说明作者踩过“stop this task被误判为停止整个房间”的坑。目标模式是有限回合制的状态机。goal模式下协调者最多 13 轮GROUP_GOAL_MAX_TURNS 13注释Odd so the final bounded turn always falls to the coordinator for the completion decision协调者通过openmaus-goal控制块输出continue/completed/needs-input/blocked控制块会从用户可见文本里剥离避免把内部协议泄到聊天里。空回复是合法回复。roomTurnReply把空串和(pass)视为“不发言”借鉴 Hermes Agent 的桌面群聊设计文件头有 MIT 出处说明并配套一个ROOM_SUMMARY_REASK如果某个回合结束时既没有最终消息也没有结论会追问一次“用 1–3 行说结果”。最后是“谁来管理房间”的问题。频道可以由人来建也可以由 Chief of Staff Bot 通过工具建server/drivers/agents-catalog.ts 里的create_room限每回合最多 4 个、只能建在自己部门、成员可选任意部门的活跃 Bot、创建者自动成为默认响应者list_rooms则只列出“你所属的共享房间”并在你无权发帖时给出不带 id 的名字和原因。对应的回归覆盖在 docs/verification/channels.md重命名、改成员、改公告、拒绝外部门成员、以及“停掉拥有该代理的回合后代理再改房间名会被拒”。结语聊天界面真正的价值是“可回看的协作”把多智能体塞进聊天界面收益不是“看起来像 Slack”而是三件工程上可验证的事场景与上下文解耦——频道共享指令、目录、成员与兜底路由上下文按成员独立记账、独立压缩路由语义单一来源——isMentionBoundary/isMentionNameContinuation让高亮、候选、服务端选人、Markdown 渲染用同一套判定避免“亮了的没被叫到”把失控当常态设计——队列、互斥锁、环检测、发帖预算、有限回合状态机都是默认路径上的护栏而不是出事后的补丁。代价同样清楚消息流天然是串行的、有界的、需要人回来拍板的。OpenMausBot 没有假装绕开这些限制而是把它们做成了界面上可见的状态——谁在说、谁在等、谁被暂停、上下文压缩了多少。对多智能体产品而言这种“诚实的可见性”比再多一个并发开关更有价值。【免费下载链接】OpenMausBotOpen-source Grok Bot alternative with a virtual machine that bots can use项目地址: https://gitcode.com/GitHub_Trending/op/OpenMausBot创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考