@aalis/plugin-trigger-policy
平台无关的开口判定 —— 规则触发插件(@ 提及 / 戳一戳 / 名字命中 / 计数 / 评分阈值)+ 禁言关键词 / 闲置主动开口
定位
回答"这条消息要不要让 agent 开口"。本插件是一个触发插件(见 trigger 服务):持有每会话的计数、活跃指数与闲置调度状态,在 inbound:trigger 相位处理生效范围、禁言、禁言关键词、点名识别与规则判定,放行时标记 triggerType。它只在自己是 trigger 服务选中的那一家(生效的触发插件)时判定;与模型触发插件 @aalis/plugin-trigger-laya 二选一。放行之后能不能说(禁言、冷却、限速)由下一相位的 plugin-flow-control 把关。
插件声明
export default definePlugin({
name: '@aalis/plugin-trigger-policy',
provides: [trigger],
uses: {
logger,
events,
hooks,
lifecycle,
config,
provide,
gateway,
trigger: optional(trigger),
flowControl: optional(flowControl),
persona: optional(persona),
sessionManager: optional(sessionManager),
messageArchive: optional(messageArchive),
sessionFields: optional(sessionFields),
document: optional(ownHostConfig),
},
apply(caps) { /* 见源码 */ },
});本插件向 trigger 服务提供自己的实例(标签「规则(计数/评分)」,优先级 0),并以 optional 声明 trigger 本身:经它判断自己是不是生效的触发插件,写 required 会把激活闸架在自己的产出上。flow-control 缺席时不设禁言:禁言关键词照样吞掉当条消息,但不会写入禁言期。session-manager 用来判从别的平台往房间里插话(入口平台与房间的出生平台不同),缺席时判不出。session-settings 用来按会话读生效范围与参数,缺席时取插件配置里的值。
接入相位
inbound:trigger (由 plugin-gateway 在 inbound:command 之后、inbound:flow 之前触发)房间类别为 owner-only 的会话(owner 自己的会话)入站不经本相位:plugin-gateway 跳过它(结构性放行),生效范围怎么配都不计数、不判定、不识别禁言关键词,也不进状态表、不排闲置话题。会话在本插件建过状态之后才变成 owner-only 的(如 owner 先在私聊里说过话、之后才登记为 owner),已建的状态与闲置定时器留到本插件停用或重启,其间闲置话题到点照常注入,注入直达 agent。
判定流程(同步完成:记入站、判定、清零在同一拍做完,放行顺序即到达顺序):
- 本插件不是生效的触发插件(
trigger服务选中的是另一家)→next(),什么都不做:不计数、不识别、不归档。生效者每次入站只取一次,判定途中切换偏好不会让同一条消息被判两次。 - 带
source的内部注入(闲置触发、定时任务、workflow)→next()跳过策略:不计数,不改写triggerType(workflow agent 节点的proactive原样保留)。真人消息由平台适配器投递,不设source。从别的平台往房间里插话(owner 从 WebUI 往群里发,api-session-manager 的fromElsewhere)同样直接放行。 - 不在生效范围内 →
next()放行。生效范围按网关算的聊天窗口(InboundPhaseData.chat)的多方标记判,按这个会话现读;WebUI 与终端的会话没有聊天窗口,不在任何生效范围里。生效范围判断先于禁言关键词,避免群聊的禁言关键词作用到私聊等不在生效范围内的会话。 - 会话处于禁言期(
flow.isMuted)→ 本会话计数与活跃指数清零,记为真人活动(见第 5 步),next()交给 flow 相位吞掉。禁言期内的消息不累计计数,也不再识别禁言关键词(不会缩短平台禁言)。平台禁言只能在这一步清零:平台禁言期内若一条消息都没有,禁言前攒下的计数保留到解禁后。本插件还没有该会话的状态时(如平台禁言先于任何消息)照样建立。 - 命中禁言关键词 →
flow.setMuted(sessionId, muteTimeSeconds, platform),本会话计数与活跃指数当场清零,记为真人活动 → 影子归档 → 吞掉。戳一戳通知跳过这一步:其正文是合成文案,内嵌戳者昵称,与名字检测同理不当发言评估。 - 记入站:评分衰减、计数 +1、评分增量、用户交互次数;记为真人活动:更新最近消息时间,闲置退避复位为 1,并从这条消息起重排 session 档闲置触发。
- 识别点名:戳一戳按
triggerOnPoke;其余消息按triggerOnAt(@ 自己)与名字检测(triggerNames与在用的人设按本会话取的名字、昵称)。triggerOnPoke关闭时戳一戳不算点名,也不做 @ / 名字检测。禁言关键词与名字检测都只看发言人自己写的话,随消息转发来的聊天记录不算。 - 判定:点名直接开口;否则按
intervalMode看此刻的计数与活跃指数是否达标。记一行判定日志(见下)。 - 开口:计数与活跃指数清零、记录触发时间;点名的写
triggerType = 'immediate',否则写'interval'(api-trigger 的markTriggered);next()。点名只决定她回不回与触发类型,不动授权身份:被频率挑中的那句话照旧按说它的人算(他的等级、权限组与免确认,owner 的能产生审批)。不开口:影子归档后吞掉。
在用的人设读名字抛错时,名字检测只用别名,照常判定,记一条 warn(同一原因只记一次):
[trigger] 人设读名字失败,点名识别只用别名: <错误>判定是同步的,同一会话接连到达的消息按到达顺序逐条判定:intervalMode 为 fixed、fixedInterval 为 2 时,同一时刻到达的 4 条放行第 2、4 条(默认的 both 下第 1 条的活跃指数已达下限,放行第 1、3 条)。
计数在判定放行时即复位。放行后若被 flow 相位的冷却或限速吞掉,这次触发作废,动态阈值也回到上限;冷却通常只有十秒量级,这期间再次攒够阈值的概率很低,因此不做预判。
判定日志:每条判定一行 debug 日志,不含消息正文:
[trigger] 判定 | session=<会话> | speak=<true|false> | addressed=<true|false> | reason=<reason>reason 形如 计数=3/5 指数=0.412 (阈值=0.850),点名时为 点名。
与模型触发插件二选一
本插件与仓库内的私有插件 @aalis/plugin-trigger-laya(经本机侧车由 Laya 模型判定,不发布到 npm,说明见 packages/plugin-trigger-laya/README.md)都是完整的触发插件,由 trigger 服务选中的那一家决定哪个生效(偏好 > 优先级 > 注册顺序)。两个都启用、没有偏好时本插件生效(Laya 的优先级 -10 低于本插件的 0,两者都是插件里的常量,不可配置);偏好指向 Laya 时 Laya 生效,本插件对每条消息直接放行,计数与闲置都停下。切换方式:
- 切到 Laya:在配置文件的
servicePreferences写trigger: "@aalis/plugin-trigger-laya",或在 WebUI 设置「系统」的服务偏好里把trigger切到「Laya 模型」(即时生效)。 - 切到规则判定(即时):在 WebUI 设置「系统」的服务偏好里把
trigger切到「规则(计数/评分)」,下一条消息起由本插件判定。 - 停用 Laya:下一条消息起由本插件接手。
- 手改
aalis.config.yaml的servicePreferences随配置热重载生效。
本插件的计数、活跃指数与闲置活动时间只统计它生效时经过的消息与 bot 回复;从不生效切回生效时,计数接着它上次生效时的状态算。
计数与评分
- 每条入站计数 +1;活跃指数增加
1 / fixedInterval,同一用户交互越多权重越高(上限 1.5 倍)。scoreDecayMinutes > 0时活跃指数按距上一条消息的时间线性衰减。 - 动态阈值在
activityScoreLower与activityScoreUpper之间:刚触发时为上限,随距上次触发的时间在activityDecayMinutes内线性降到下限。 intervalMode:fixed只看计数是否达到fixedInterval;dynamic只看活跃指数是否达到当前阈值;both任一满足即可。scoreDecayMinutes = 0且activityScoreUpper ≤ 1时,计数达标必然伴随活跃指数达标,both与dynamic等价。
闲置触发
闲置触发只在本插件生效时开口:到点时它不是生效的触发插件就跳过,不注入,也不记为 bot 开口(session 档按原退避重排)。
idleTriggerScope 三档:
off:关闭。session:每会话一个定时器,真人消息到来时退避复位为 1 并重排。到点时会话处于禁言期则跳过并按原退避重排;否则gateway.ingressMessage注入一条source='idle-trigger'消息,exponential风格下退避翻倍(上限idleTriggerMaxMinutes)。只有真人消息复位退避,agent 回复(包括回复闲置提示)不复位。platform:跨会话共用一个定时器。idleTriggerStrategy决定触发时机:all-quiet在所有会话都静默满idleTriggerMinutes后触发,fixed每隔idleTriggerMinutes触发一次。到点后,在不处于禁言、冷却或限速已满状态的会话中,选最近活动最早的一个注入闲置触发消息。候选还要按它自己的设置:这个会话生效的idleTriggerScope不是platform(被单独关成off或改成session)就跳过,提示词也按候选会话生效的idleTriggerPrompt取。跨会话的开关与节奏只看全局层(插件配置):idleTriggerScope的全局层是platform才排这个定时器,idleTriggerMinutes取全局层,idleTriggerStrategy只在插件配置里;某个会话层设成platform不会开起跨会话选举。每轮之间至少隔一个阈值量级(idleTriggerMinutes,下限 60 秒)。进程内还没有任何活动记录时,以启动时刻为静默起点。
闲置参数改了不重载插件:session 档下一次排定时器时现读;platform 档每次排定时器时现读全局层,宿主配置文档变了(host-config:changed)时随即重排,所以从关闭改成 platform 时不用重启就开始排。
会话的"最近活动"取真人消息与 bot 开口中较晚者;bot 开口指 agent 的真实回复或闲置注入本身。因此 agent 对闲置提示沉默时,刚被注入的会话在下一轮也不会再次当选。禁言期内的真人消息与命中禁言关键词的那条同样算真人活动,只是不计数(判定流程第 3、4 步):session 档从这条消息起重排、退避复位为 1,platform 档的静默计时与挑选候选看的最近活动也随之更新。
注入的消息携带 triggerType: 'idle'、source: 'idle-trigger':本相位跳过策略判定;flow 相位对它不查回复后冷却,禁言照常生效,会话在 flow-control 的生效范围内时限速也照常生效(内部注入没有网关算的聊天窗口,flow-control 按会话管理或流控状态记下的窗口判,默认的多方房间下发往群的闲置提示在限速窗口已满时被吞;闲置提示按存档的规则不落档,见 services/message-archive.md)。
出站联动
监听 outbound:message:source === 'agent'、本插件生效且持有该会话状态时,记为 bot 开口并重排 session 档闲置触发,不复位闲置退避。
配置
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
audience | select | 'multiParty' | 生效范围:off 不生效、multiParty 多方房间、oneToOne 一对一房间、all 全部房间 |
intervalMode | select | 'both' | 间隔模式:fixed / dynamic / both |
triggerOnAt | boolean | true | 检测 @ 提及:@ 自己算被点名,点名直接开口,回合记为 immediate |
triggerOnPoke | boolean | true | 戳一戳直触发:戳一戳等注意力动作视同 @ 即时触发;关闭后此类动作落回正常意愿评估,不强制回复。 |
triggerNames | string | '' | 触发名别名(逗号或换行分隔) |
muteKeywords | string | '' | 禁言关键词(逗号或换行分隔) |
muteTimeSeconds | number | 60 | 禁言关键词命中时长(秒) |
fixedInterval | number | 5 | 固定间隔(每 N 条触发) |
activityScoreLower | number | 0.3 | 活跃指数阈值下限 |
activityScoreUpper | number | 0.85 | 活跃指数阈值上限 |
activityDecayMinutes | number | 10 | 阈值衰减分钟 |
scoreDecayMinutes | number | 0 | 评分衰减分钟(0=不衰减) |
idleTriggerScope | select | 'off' | 闲置触发范围 |
idleTriggerStrategy | select | 'all-quiet' | 闲置触发策略(platform 档) |
idleTriggerMinutes | number | 180 | 闲置触发分钟 |
idleTriggerStyle | select | 'exponential' | 闲置触发风格(session 档) |
idleTriggerMaxMinutes | number | 1440 | 闲置触发上限分钟 |
idleTriggerJitter | boolean | true | 闲置触发抖动(±10%,不低于 60 秒) |
idleTriggerPrompt | string | '' | 闲置触发系统提示(留空用内置提示) |
除 idleTriggerStrategy 外都是可按会话覆盖的字段(api-session-settings),只适用于房间,只有 owner 能改:全局层是这里的插件配置,另可按平台档、平台类型档、群(场所)与单个会话设,例如只给某个群设别的禁言关键词或固定间隔。每条消息按会话现读,改了不重载插件,下一条消息起生效。会话设置不在场时只取插件配置里的值。
状态清理
会话状态每天扫描一次:没有闲置定时器、且真人消息与 bot 开口都在 30 天以前的会话被删除。
与 persona 的协作
- 名字检测(
@aalis/api-trigger的createBotNames与isAddressed)自动合并在用的人设的getPersonaName()与getNickNames(),角色卡里声明的名字、昵称无需在触发名里重复配置。人设按会话取,与 agent 同一取法:把这条消息的会话交给这两个方法,人设服务按会话读它的「人设」;会话改用别的角色卡时,算点名的是那张卡的名字、昵称,主卡的不算,别的会话不受影响。在用的人设是本插件按服务偏好选出的人设提供者:同时装了多个人设插件时,只有它的名字算点名;给 agent 单设了人设偏好时,本插件要设同一个。 - 禁言关键词不合并 persona:统一由本插件配置下发,避免角色卡措辞意外成为禁言开关,也避免进程级单例 persona 跨平台泄漏。