flow-control 服务
1. 定位
flow-control 是会话级的节流硬闸:禁言、回复后冷却、限速窗口。它只回答"现在能不能说",不参与"要不要开口"——后者由前一相位生效的触发插件判定(见 trigger;规则版 @aalis/plugin-trigger-policy 看 @ / 名字 / 计数与评分,另有闲置主动开口)。
- 服务注册名:
'flow-control'(描述符flowControl,消费方经flowControl.use调用) - 契约包:
@aalis/api-flow-control - 参考实现:
@aalis/plugin-flow-control - 相邻相位:
inbound:trigger(生效的触发插件)→inbound:flow(本服务的参考实现)→inbound:dispatch。
2. 契约
完整接口(packages/api-flow-control/src/index.ts):
export interface FlowControlService {
/** 当前是否在禁言期 */
isMuted(sessionId: string): boolean;
/** 当前是否在回复后冷却期 */
isCoolingDown(sessionId: string): boolean;
/** 限速窗口内的回复数是否已达上限(true 表示已超限) */
isRateLimited(sessionId: string): boolean;
/**
* 设置或解除禁言(禁言关键词命中或平台禁言事件时调用)。
* - durationSec > 0:禁言到 now + durationSec 秒
* - durationSec <= 0:解除禁言
* 会话尚无流控状态时,只有同时给出 platform 才会建立状态并禁言。
*/
setMuted(sessionId: string, durationSec: number, platform?: string): void;
}要点:
- 三个查询方法对未知会话返回
false(无状态即不限制)。 - 冷却与限速按 agent 的真实回复(
outbound:message且source === 'agent')计,由实现自己监听,契约不暴露记账方法。 - 类型随描述符走:下游
import { flowControl } from '@aalis/api-flow-control'写进uses即可,不必依赖实现包。
3. 谁提供 / 谁消费
提供方(参考实现):@aalis/plugin-flow-control,provide.use(flowControl, service)(packages/plugin-flow-control/src/index.ts)。它同时占据 inbound:flow 相位做硬闸,并监听 outbound:message 记冷却与限速。禁言(setMuted 时长大于 0)时经 agent(optional)的 abort(sessionId, { queued: true, reason: '禁言' }) 停下这个会话在跑的一轮、放掉排着的,不交 by(停下的人):是谁禁的,会话历史里挨着那条(关键词那句话或平台的禁言通知)本来就有;解禁不停。
消费点(全部 optional,缺席即不设闸):
@aalis/plugin-trigger-policy:isMuted决定禁言期不计数、不识别关键词;命中禁言关键词时setMuted;session 档闲置到点前查isMuted,platform 档挑候选时排除isMuted/isCoolingDown/isRateLimited为真的会话(packages/plugin-trigger-policy/src/index.ts、packages/plugin-trigger-policy/src/idle-scheduler.ts)。@aalis/plugin-trigger-laya(仓库内私有插件):isMuted为真时放行给 flow 相位吞掉,不识别关键词;命中禁言关键词时setMuted(packages/plugin-trigger-laya/src/index.ts)。@aalis/plugin-adapter-onebot:bot 自身被禁言/解禁的 notice、重连后按shut_up_timestamp恢复时调setMuted(packages/plugin-adapter-onebot/src/index.ts)。
4. 写一个 provider
替换默认实现时须实现整个 FlowControlService,并把 inbound:flow 中间件与 outbound:message 记账一并搬过来,否则禁言、冷却、限速闸门会失效。双源 manifest 必须同步(package.json 的 aalis.service 与源码 provides / uses,见 concepts/manifest-metadata)。
参考实现的 package.json → aalis.service:
{ "aalis": { "service": {
"required": ["config", "events", "hooks", "lifecycle", "logger", "provide"],
"optional": ["storage", "message-archive", "session-manager", "session-settings", "host-config"],
"provides": ["flow-control"]
} } }最小骨架(省略禁言持久化、生效范围与按会话的参数):
import { type FlowControlService, flowControl } from '@aalis/api-flow-control';
import { INBOUND_PHASE } from '@aalis/api-gateway';
import { hooks } from '@aalis/api-hooks';
import { definePlugin, events, provide } from '@aalis/core';
export default definePlugin({
name: '@aalis/plugin-my-flow-control',
provides: [flowControl],
uses: { provide, hooks, events },
apply({ provide, hooks, events }) {
const mutedUntil = new Map<string, number>();
const cooldownUntil = new Map<string, number>();
const service: FlowControlService = {
isMuted: sid => Date.now() < (mutedUntil.get(sid) ?? 0),
isCoolingDown: sid => Date.now() < (cooldownUntil.get(sid) ?? 0),
isRateLimited: () => false,
setMuted(sid, sec) {
if (sec > 0) mutedUntil.set(sid, Date.now() + sec * 1000);
else mutedUntil.delete(sid);
},
};
provide.use(flowControl, service);
hooks.use.middleware(INBOUND_PHASE.FLOW, async (data, next) => {
const { message } = data;
if (service.isMuted(message.sessionId)) return; // 禁言期一律不说话
// immediate 穿透冷却;带 source 的内部注入不过冷却(限速仍挡,示例从略)
if (message.triggerType !== 'immediate' && !message.source && service.isCoolingDown(message.sessionId)) return;
await next();
});
events.use.on('outbound:message', msg => {
if (msg.source === 'agent' && msg.sessionId) cooldownUntil.set(msg.sessionId, Date.now() + 10_000);
});
},
});flow-control 是单实例服务,用全局 Map<sessionId, state> 管多会话,不需要 entryId;priority 留默认即可。同名时选中哪一家的规则见 concepts/service-model。
5. 标准消费方式
业务句柄 flowControl.use 是活的(规则见 选择层与业务层)。缺席时先看 present,缺席按"不设闸"处理:
// platform 档闲置挑候选时(packages/plugin-trigger-policy/src/idle-scheduler.ts)
const flow = this.caps.flowControl.present ? this.caps.flowControl.use : undefined;
for (const [sid, s] of this.states) {
if (flow?.isMuted(sid) || flow?.isCoolingDown(sid) || flow?.isRateLimited(sid)) continue;
// ...
}6. 行为不变量
- owner 自己的会话不经本相位。房间类别为
owner-only的会话由 plugin-gateway 跳过inbound:flow(结构性放行),禁言、冷却与限速对它们的入站都不起作用;出站照常记账,只是不挡消息。 - 禁言不看生效范围、不看来源。
inbound:flow先查禁言:闲置触发、定时任务注入的消息在禁言期同样被吞。禁言状态只由关键词或平台禁言针对具体会话写入,所以不会误伤生效范围外的会话。禁言在入站挡之后的消息,同时经 agent 停下这个会话在跑的一轮、放掉排着的:禁言写入时正在生成的回复不外发、不落库,提交点之后已在外发的那一条照常发出(见插件页「已知局限」)。 - immediate 穿透冷却与限速。被 @、戳一戳、叫名字时即使处于冷却或限速窗口也放行;禁言期除外。带
source的内部注入(闲置触发、定时任务、workflow)不受冷却约束,但仍受禁言与限速约束(限速只在生效范围内时)。内部注入没有网关算的聊天窗口,判生效范围与回复记账同一口径:先取会话管理记下的聊天窗口,没有再用流控状态里记下的,所以 bot 在某个群的限速窗口已满时,发往该群的内部注入同样被挡下。 - 生效范围先于关键词。生效范围是 api-gateway 导出的「生效范围」字段(
REPLY_AUDIENCE_FIELD,默认多方房间),按聊天窗口的多方标记判(audienceIncludes),可按会话设。触发插件在识别禁言关键词之前先判生效范围,否则群聊的禁言关键词会作用到私聊等不在生效范围内的会话。 - 禁言态要持久化。禁言可能是小时级的用户意图,参考实现把
mutedUntil落盘到data:/flow-control-mutes.json,冷却与限速不落盘。该文件读不懂(不存在以外的读取错误、解析失败、顶层不是对象)时,本次运行不再整表回写,禁言改动只在内存生效,storage 换人重读时重新判定。换 provider 时若不持久化,重启会让被禁言的群立即恢复发言。 - 限速是防刷屏与平台风控的护栏。冷却与限速记在 agent 真实回复上,只对生效范围内的房间记账:先取会话管理记下的出生平台与聊天窗口(
originPlatform、originChat的多方标记),没有再用流控状态里记下的,结果只写进流控自己的状态、不回写消息,入站带source的内部注入判生效范围也用这一口径;没有聊天窗口的会话(WebUI、终端、子任务)不是房间,不计入。自建主动发送通道应发source: 'agent'的outbound:message,才能被计入。
7. 注意事项与边界情形
影子归档顺序竞态(现状未收口)。被吞掉的入站消息会做影子归档,下次触发时作为上下文:
flow-control的shadowArchive(packages/plugin-flow-control/src/index.ts)与触发插件共用的archiveSwallowed(packages/api-trigger/src/index.ts)都直接awaitmessage-archive 的archiveIncoming(message)。- 真正进了一轮的消息由 agent 在它进入那一轮时归档(
packages/plugin-agent/src/index.ts),agent 在一个会话里的写入依次发生。 - 两条路径不共享顺序,
archiveIncoming以Date.now()作时间戳(packages/plugin-message-archive/src/index.ts)。吞掉与 agent 的写入在毫秒级交错时,归档可能乱序或时间戳并列。
消息都会进档、都会发 inbound:message:archived,但会话内严格时序不保证。依赖严格时序的下游应以单调序列号而非 Date.now() 排序。闲置触发的合成提示不做影子归档(与 agent 的归档规则一致)。
其它:
- 命令回复与系统回复(
source不是agent)不计冷却与限速。 - 会话状态每天扫描一次,无挂起禁言/冷却且 30 天未见的会话被删除。
8. 交叉链接
- plugins/plugin-flow-control、plugins/plugin-trigger-policy — 两个相位的处理顺序与配置。
- services/gateway — 入站相位
COMMAND → TRIGGER → FLOW → DISPATCH(相位常量见packages/api-gateway/src/index.ts)。 - concepts/service-model、concepts/lazy-service-access — 服务选名规则、业务句柄是活的。
- concepts/manifest-metadata —
provides/uses双源同步。 - concepts/storage-uri-grammar — 禁言持久化用的
data:root 文法。 - services/message-archive —
archiveIncoming语义与inbound:message:archived事件。