跳至正文
浏览章节

频道、网关与计划任务

把一个会话放进聊天话题里、每个数据目录留一个常驻的 bingo,以及把一个回合推迟或重复。

频道

聊天平台和别的界面没有两样(ADR-0016)。bingo-channels 装着一个投递器和一组适配器; 每个适配器交出它真正实现了的那些机制——编辑一条消息、按钮、正在输入指示、话题——所以 一项能力绝不可能由一个渲染器画不出它的适配器来宣称。

今天存在两个适配器:loopback,一个进程内的、限额可配的适配器,每一项行为都是拿它 来验证的契约;以及 feishu,用于企业自建的飞书应用。飞书提供的长连接对应用商店的 应用不开放。

监听

bingo channels                   # listen on the configured channels and nothing else
bingo --channels loopback        # listen beside an ordinary run
bingo --channels loopback=127.0.0.1:7777

--channels 可以重复给出,而 channels 设置说的是同一件事。

配置一个

应用的名字是设置;它用来签名的东西不是。

{
  "channels": {
    "feishu": { "appId": "cli_xxx" }
  }
}

secret 来自 BINGO_FEISHU_APP_SECRET(id 则来自 BINGO_FEISHU_APP_ID),或者对于 继承不到任何 shell 的网关,来自凭据存储。有两条命令写它们:

bingo channels add feishu      # asks for the app id and the secret together
bingo channels secret feishu   # pastes a secret on its own, for rotation

一场对话会变成什么

一次私聊就是一个会话。群聊只在机器人被提及时才介入。一个话题延续它所在聊天的会话。 会话键是 <adapter>/<chat>[/<thread>],只由这个插件生成,别人不许生成;而 Origin.principal 是平台自己的用户 id,由适配器打上,绝不从消息正文里读出来。

流式回答是拿累积到目前为止的整段文本重画的,而不是发一个增量;先按句子边界合并,再按 新增字符数,最后按定时器——每场对话只保留一份待发的快照,所以过期的重画绝不可能盖掉 最终文本。

在适配器有原生按钮的地方,权限提问会渲染成按钮,否则渲染成一个带编号的列表,你回复 编号或标签来回答。当这个问题在任何地方被解决——在终端里、在 wire 上、被任何人——那条 已投递的消息就会被编辑:按钮被去掉,结果被追加上去(approved in the TUI)。没有一个 活按钮会比它的问题活得更久。

一个应用一个进程

飞书的长连接是集群模式的,而 Telegram 的轮询会互相顶掉,所以针对同一份凭据再起第二个 bingo,等于一次静默的半瘫痪。适配器会在数据目录下为每份凭据取一个锁文件,并大声拒绝, 而不是安静地降级。

网关

一扇朝内的门没有宽限期:人们在任何时刻都会写消息,而飞书的连接没有回放。 bingo channels 把那扇门跑在前台,随终端一起死掉,所以 bingo gateway 会为每个数据 目录跑一个常驻的 bingo,并像管理一项服务那样管理它(ADR-0020)。

bingo gateway start        # spawn it detached, wait until it is up
bingo gateway status       # whether one is running here, as what, and since when
bingo gateway logs -n 100  # the log, and where it is
bingo gateway stop         # ask it to stop, wait for its locks to come back
bingo gateway restart
bingo gateway doctor       # read settings, credentials and every lock; say what to do
bingo gateway doctor --fix # remove exactly the locks whose process is gone
bingo gateway run          # the resident process itself, in the foreground

网关是一个完整的宿主,不是一座桥:它把普通的插件宿主装配起来,并让频道界面在其中监听; 会话、对话记录、计划任务和锁,全都是正常的那些,放在正常的位置。没有任何东西被代理。

路径是什么
~/.bingo/data/gateway/gateway.pidpid、二进制的版本和启动时间
~/.bingo/data/gateway/gateway.logstdout、stderr,以及 tracing 的输出端

install 让它跨越登录会话活下去:

bingo gateway install
bingo gateway uninstall

在 macOS 上,这会写下 ~/Library/LaunchAgents/com.bingo.gateway.plist 并用 launchctl 加载它;在 Linux 上是 ~/.config/systemd/user/bingo-gateway.service,用 systemctl --user 启用。这个单元运行 <current exe> gateway run,不携带任何 secret。 只要服务是装着的,这些动词就委派给服务管理器,于是 launchd 和一个手工起的进程绝不会 争同一个 pidfile;status 和 doctor 会指明当前是哪种模式在起作用。

install、start 和 restart 会先预检频道——设置无法解析、没有配置任何频道,或者 某个频道签不了名,都会被拒绝,并给出 doctor 自己的那几行。一份跑不起来的配置绝不会被 交给服务管理器去反复崩溃重启。

Linux 的用户单元在退出登录时会停下,除非开了 lingering;doctor 为此有一行,并且它 打印那条 loginctl enable-linger 命令,而不是替你运行。Windows 不在范围内。

计划任务

bingo-schedule 把回合推迟和重复(ADR-0019)。一条记录是 ~/.bingo/data/schedules/<id>.json 下的一个 JSON 文件,可以手改,一条一个文件。

规格是一小套语法,不是 cron:

every 45s · every 30m · every 2h
daily at 09:00
once at 2026-09-01T09:00:00-07:00

天不是 every 的单位:一天是一件民用时间上的事,夏令时会把它变长或变短,而那正是 daily at 存在的意义。

一次触发就是在这条计划任务自己的会话上跑一个回合,会话键是 schedule/<id>,在这条 记录的工作目录里。对话记录就是记录本身——结果、失败和开销都落在其他每个回合落的地方, 而 --resume 读得到它们。一条 once at 记录触发之后会自行停用。

模型用 ScheduleCreate、ScheduleList 和 ScheduleForget 创建和忘掉它们;人用 /schedule 读那张表。无人值守的运行使用该记录自己的权限模式,而没人回答的问题会被 谢绝,方式和无头运行谢绝它们时一样——回合诚实地继续或失败。

有两条诚实的限制。计划任务只在有一个 bingo 进程运行时才会触发——终端界面、 bingo serve、bingo channels 或者网关。它们没有自己的守护进程,而 /schedule 会在 最后一行如实说明——schedules: held by this process,或者 schedules: dormant — held by pid 42。以及每个存储一个运行者:第一个拿到锁文件的 进程持有它们,第二个进程跑起来时计划任务处于休眠状态,并带一条通知说明是谁持有。启动 时,一条已经过期的记录只触发一次,绝不为每一个错过的间隔各触发一次。