Skip to content

@aalis/plugin-trigger-policy ​

平台无关的开口判定 —— 规则触发插件(@ 提及 / 戳一戳 / 名字命中 / 计数 / 评分阈值)+ 禁言关键词 / 闲置主动开口

定位 ​

回答"这条消息要不要让 agent 开口"。本插件是一个触发插件(见 trigger 服务):持有每会话的计数、活跃指数与闲置调度状态,在 inbound:trigger 相位处理生效范围、禁言、禁言关键词、点名识别与规则判定,放行时标记 triggerType。它只在自己是 trigger 服务选中的那一家(生效的触发插件)时判定;与模型触发插件 @aalis/plugin-trigger-laya 二选一。放行之后能不能说(禁言、冷却、限速)由下一相位的 plugin-flow-control 把关。

插件声明 ​

ts
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。

判定流程(同步完成:记入站、判定、清零在同一拍做完,放行顺序即到达顺序):

  1. 本插件不是生效的触发插件(trigger 服务选中的是另一家)→ next(),什么都不做:不计数、不识别、不归档。生效者每次入站只取一次,判定途中切换偏好不会让同一条消息被判两次。
  2. 带 source 的内部注入(闲置触发、定时任务、workflow)→ next() 跳过策略:不计数,不改写 triggerType(workflow agent 节点的 proactive 原样保留)。真人消息由平台适配器投递,不设 source。从别的平台往房间里插话(owner 从 WebUI 往群里发,api-session-manager 的 fromElsewhere)同样直接放行。
  3. 不在生效范围内 → next() 放行。生效范围按网关算的聊天窗口(InboundPhaseData.chat)的多方标记判,按这个会话现读;WebUI 与终端的会话没有聊天窗口,不在任何生效范围里。生效范围判断先于禁言关键词,避免群聊的禁言关键词作用到私聊等不在生效范围内的会话。
  4. 会话处于禁言期(flow.isMuted)→ 本会话计数与活跃指数清零,记为真人活动(见第 5 步),next() 交给 flow 相位吞掉。禁言期内的消息不累计计数,也不再识别禁言关键词(不会缩短平台禁言)。平台禁言只能在这一步清零:平台禁言期内若一条消息都没有,禁言前攒下的计数保留到解禁后。本插件还没有该会话的状态时(如平台禁言先于任何消息)照样建立。
  5. 命中禁言关键词 → flow.setMuted(sessionId, muteTimeSeconds, platform),本会话计数与活跃指数当场清零,记为真人活动 → 影子归档 → 吞掉。戳一戳通知跳过这一步:其正文是合成文案,内嵌戳者昵称,与名字检测同理不当发言评估。
  6. 记入站:评分衰减、计数 +1、评分增量、用户交互次数;记为真人活动:更新最近消息时间,闲置退避复位为 1,并从这条消息起重排 session 档闲置触发。
  7. 识别点名:戳一戳按 triggerOnPoke;其余消息按 triggerOnAt(@ 自己)与名字检测(triggerNames 与在用的人设按本会话取的名字、昵称)。triggerOnPoke 关闭时戳一戳不算点名,也不做 @ / 名字检测。禁言关键词与名字检测都只看发言人自己写的话,随消息转发来的聊天记录不算。
  8. 判定:点名直接开口;否则按 intervalMode 看此刻的计数与活跃指数是否达标。记一行判定日志(见下)。
  9. 开口:计数与活跃指数清零、记录触发时间;点名的写 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 档闲置触发,不复位闲置退避。

配置 ​

字段类型默认值说明
audienceselect'multiParty'生效范围:off 不生效、multiParty 多方房间、oneToOne 一对一房间、all 全部房间
intervalModeselect'both'间隔模式:fixed / dynamic / both
triggerOnAtbooleantrue检测 @ 提及:@ 自己算被点名,点名直接开口,回合记为 immediate
triggerOnPokebooleantrue戳一戳直触发:戳一戳等注意力动作视同 @ 即时触发;关闭后此类动作落回正常意愿评估,不强制回复。
triggerNamesstring''触发名别名(逗号或换行分隔)
muteKeywordsstring''禁言关键词(逗号或换行分隔)
muteTimeSecondsnumber60禁言关键词命中时长(秒)
fixedIntervalnumber5固定间隔(每 N 条触发)
activityScoreLowernumber0.3活跃指数阈值下限
activityScoreUppernumber0.85活跃指数阈值上限
activityDecayMinutesnumber10阈值衰减分钟
scoreDecayMinutesnumber0评分衰减分钟(0=不衰减)
idleTriggerScopeselect'off'闲置触发范围
idleTriggerStrategyselect'all-quiet'闲置触发策略(platform 档)
idleTriggerMinutesnumber180闲置触发分钟
idleTriggerStyleselect'exponential'闲置触发风格(session 档)
idleTriggerMaxMinutesnumber1440闲置触发上限分钟
idleTriggerJitterbooleantrue闲置触发抖动(±10%,不低于 60 秒)
idleTriggerPromptstring''闲置触发系统提示(留空用内置提示)

除 idleTriggerStrategy 外都是可按会话覆盖的字段(api-session-settings),只适用于房间,只有 owner 能改:全局层是这里的插件配置,另可按平台档、平台类型档、群(场所)与单个会话设,例如只给某个群设别的禁言关键词或固定间隔。每条消息按会话现读,改了不重载插件,下一条消息起生效。会话设置不在场时只取插件配置里的值。

状态清理 ​

会话状态每天扫描一次:没有闲置定时器、且真人消息与 bot 开口都在 30 天以前的会话被删除。

与 persona 的协作 ​

  • 名字检测(@aalis/api-trigger 的 createBotNames 与 isAddressed)自动合并在用的人设的 getPersonaName() 与 getNickNames(),角色卡里声明的名字、昵称无需在触发名里重复配置。人设按会话取,与 agent 同一取法:把这条消息的会话交给这两个方法,人设服务按会话读它的「人设」;会话改用别的角色卡时,算点名的是那张卡的名字、昵称,主卡的不算,别的会话不受影响。在用的人设是本插件按服务偏好选出的人设提供者:同时装了多个人设插件时,只有它的名字算点名;给 agent 单设了人设偏好时,本插件要设同一个。
  • 禁言关键词不合并 persona:统一由本插件配置下发,避免角色卡措辞意外成为禁言开关,也避免进程级单例 persona 跨平台泄漏。

文档对应 dev@c4e046e41,core 0.21.0,构建于 2026-11-02
基于 MIT 许可发布