中途说一句 say
一轮最多 3 次、每次 80 字以内、不能换行,直接发出去并重新点亮「正在输入」。说过之后这一轮不再被合并重来,也不过发送前审核。
链路
消息先落进持久化队列,再经过确定性门禁和接话评分决定回不回;要回时组装上下文、交给模型或 Agent 生成,过完发送前审核才分条发回来源通道。每一步拦下都会留下结果代码和原因。
总览
OneBot / Telegram / QQ / 钉钉 / 飞书 / 企业微信,归一化为 MessageEvent。
断线补拉、缺口检测、失败重试,进程重启也不丢。
确定性门禁 → 直接触发 → 接话评分。
SOUL.md、历史、记忆、媒体,稳定部分在前以命中前缀缓存。
开了 Agent 走工具循环(可能用到浏览器),否则一次模型调用。
沉默、去重、审核模型、群叫停。
发送时守卫、幂等、退避重试。
inbound_events 的结果代码与原因,WebUI 事件中心据此展示。
01 回复触发
HandleEvent 不直接生成回复,而是先把消息写进 SQLite 入站队列再唤醒 worker。进程崩溃、重启或者模型卡住时消息还在库里:worker 认领时拿的是 10 分钟租约,过期后会被重新认领。
自己发的消息只记历史;戳一戳、进群、加好友、撤回这类通知各走各的开关与冷却,不进队列。
全部 / 仅主人 / 白名单。
ignored_private_admissionnoteDirectedInbound:入队前就登记,后面用来打断同一人的旧回复。
优先级:直接触发 100、引用回复 80、可解析链接 60、其余 0;同优先级按时间。
每会话默认群 3 条、私聊 2 条同时处理,另受全局回复并发限制。
backfill_history_only早于重放窗口ignored_stalerouteMessageEvent。转写暂时失败会让这一条稍后重试。
12 项不调用模型的检查,见下方表格。
ignored_*私聊、@、引用机器人、称呼、主人命令、可解析链接、插件命令;先要通过回复准入(主人直通 → 屏蔽 → 白名单 → 豁免 → 活跃时段 → 群等级)。
2 分钟内追加的消息由模型判断是重复、补充、纠正还是独立问题。
merged_into_reply处理出错按 5 秒起、翻倍到 5 分钟重试,最多 5 次,用尽记 dropped_retries_exhausted;平台明确拒收记 dropped_send_rejected,不再重试。
01 回复触发
掉线期间平台上的消息收不到推送,只能事后向平台拉历史。协调器 runInboundCoordinator 负责这件事。
释放过期租约,读取上次在线的检查点。在线时每 30 秒保存一次检查点。
停止认领,保存检查点。
重放截止时间 = 断线时刻 − 30 分钟,最多回看 24 小时。
1 秒后补一次,+1 分钟、+5 分钟再各补一次。遍历群列表和最近联系人,每页 100 条、每会话上限 200 条,逐个会话串行拉。
和已入队、已入库的消息比对,同一条不会处理两次。
每个会话只有最新 N 条(history_backfill_message_limit,默认 3)、且本来就会直接触发的消息正常入队。
掉线一小时回来,对攒下的上百条群聊逐条接话只会刷屏。补回来的历史照样进上下文,只是不再逐条触发回复。
在线时还有一道缺口检测:重连后每个群的第一条实时消息、以及任何 seq 跳号,都会触发 verifyGroupSeqGap——等补拉结束后定向重拉,并在 15 秒、1 分钟、3 分钟复查。日志动作是 backfill_gap_detected / resolved / unresolved。
01 回复触发
按顺序检查,任何一步拦下都带着具体原因结束。这些检查不调用模型(第 12、13 项除外),结果稳定可复现。
| # | 检查 | 拦下时的结果 |
|---|---|---|
| 1 | 机器人已退群或群不可用 | ignored_unavailable_group |
| 2 | 补全引用、转发、图片,插件观察,写入历史 | — |
| 3 | 群未启用 / 不在准入范围(主人的抑制命令例外)。停用分两档:彻底休眠(默认)只落本地历史,不跑长期记忆提取、会话摘要、语义索引和语音转写;静默旁观照常做这些,只是不回复 | ignored_policy;被屏蔽的用户记 ignored_user_blocked |
| 4 | 机器人在该群被禁言 | ignored_bot_muted;开了「禁言期间仍判断」时跑完判断再记 ignored_bot_muted_judged |
| 5 | 群模型配额用尽 | ignored_model_quota |
| 6 | 事件触发器(关键词 / 正则任务) | 不拦截,任务在后台照常跑 |
| 7 | 主人的清空上下文命令 | 立即处理 |
| 8 | 积压交接(见「并发与合并」) | merged_into_backlog_turn |
| 9 | 群成员等级门槛(OneBot;@、主人、管理员豁免) | ignored_member_level |
| 10 | 响应抑制期内 | ignored_response_suppression |
| 11 | 只有视频没有文字 | ignored_video |
| 12 | 发送者是被标记的机器人或 Telegram bot,且没有明确 @ 或引用本机器人:用模型判断是不是在跟自己说话 | ignored_bot_message |
| 13 | 回复疲劳攒满(对同一个人回得多,且对方长句、没新意、没目的;分快慢两档消退,时间常数 10 分钟和 6 小时,总疲劳 = 快 + 0.2 × 慢):已决定要回之后先看攒着的判断:最近几轮审核打的目的分按权重攒着、30 分钟消退,这个人一直在追问、纠正、请它做事就照回;攒得不够再用模型看这句(和意图识别同一份按时间排的对话)有没有新东西、是不是在提问或请求,两样都没有才不回,@ 和引用也一样;疲劳没攒满或攒着的目的够高时不调用 | ignored_reply_fatigue |
01 回复触发
没有直接触发的普通群消息,先过群的随机采样比例(主人豁免),再交给接话模型。模型对每条候选只回答两件事:relevance.directed(是不是明确在跟机器人说话,只有是或否)和 chat_in.score(0–1 的闲聊适合度),各附一句理由。
寻址信息、按时间排的对话稿、本群补充判据。
用途 proactive_reply_router,可绑对话模型或判断模型 Jev,最多等 60 秒。
表示对方在叫停或正在机械循环。
replied_proactive冷却默认 30 秒,闲聊回复真正发出去后才开始计时。
ignoredreplied_proactive,进入回复流程。
| 档位 | 界面名称 | 闲聊分门槛 |
|---|---|---|
off | 从不闲聊 | 不闲聊 |
minimal | 很少插话 | ≥ 0.90(最严) |
low | 偶尔接话 | ≥ 0.70 |
medium | 适度参与 | ≥ 0.50 |
high | 积极参与 | ≥ 0.30 |
always | 完全不限制 | 不看分数,仍受冷却限制 |
「是不是在跟机器人说话」不打分:给 0–1 的分数,模型会在 0.45 和 0.55 之间摇摆,同一类消息这次回下次不回。模型出错时一律沉默——以前超时会退回关键词规则(扫到问号就当公开提问),判错的方向是「本来不该说话却开口」。评分细节写在 proactive_reply_route 日志里;档位、打分口径和补充判据见仓库的 发言偏好,判断模型的接入见 判断模型(Jev)。
01 回复触发
同一会话里消息来得快,三种机制避免「一条一回」或者答非所问:
且后面还有同会话消息;只看第一次处理,主人的消息不交接。
最多 20 条,保留 10 分钟。
最新的直接触发当回复目标;没有直接触发时,能接话的候选一起交给接话评分。
仅在确定走可重生成的对话路径后开放;解析、固定插件结果和本地命令不接走新问题。
置信度 ≥ 阈值(默认 75%)才合并。
Agent 还在规划时,不带媒体的补充在下一步规划前接进上下文,已有工具结果保留,正在跑的那步规划作废重做;其余情况立刻中断这一版重新生成(工具运行中等它结束,已写外部系统则不中断)。最多生成 3 次;最后一次生成前、开始发送或产生外部副作用时关闭窗口。回答之后再问,走新的回复。
superseded_follow_up;撤回只去掉引用装饰,不取消回复。
合并先记为持久化待定交接。包含这些请求的回答完整送达后才落定;沉默、生成失败、取消或仅发出部分答案时,未完成的请求重新排队。进程重启会恢复无人负责的待定交接。已经尝试发送、却无法确认结果的答案不自动重发;未包含在该答案里的补充仍恢复处理。插件事实投递保留独立交付,可选的模型跟评遵守追问发送闸门。
在群里 @、引用或点名机器人说「闭嘴」「别说了」,发送前审核会顺带判出叫停,给本群记 10 分钟窗口:窗口内接话评分直接跳过,在路上的主动接话回复发送前丢掉;@、引用和点名照常回答——叫停说的是别插话,不是不理人。
02 回复流程
replyAndRecord 最多跑 3 次 replyTo:同一个人在生成期间补了一句、被判为补充或纠正时,Agent 还在规划就把补充接进这一轮继续跑;接不进去(非 Agent、带媒体、Agent 已收尾)时旧的这版作废,带上新消息重新生成;第 3 次之前封口,不再接受合并。
当前消息图片、历史、身份脱敏、请求超时、「正在输入」(Telegram 与 OneBot 私聊)。
可能启动后台任务,也可能直接给出回复。
见下方提示词顺序。回复规则可以指定本轮的模型配置,或把回复换成语音。
取出控制标记;拦下写成文字的工具调用;平台不支持富文本时转纯文本;超长先在自然边界拆、再单独拆代码块,还不行就用模型压缩(30 秒、每条最多 2 次)。
ignored_*见下一节。
历史、消息 ID、投递阶段、事件结果;闲聊冷却开始计时;排入关系评估。
系统头(SOUL.md、固定规则、工具规则)→ 摘要检查点 → 稳定历史 → 缓存断点 → 可变块(插件上下文、会话状态、记忆、笔记本、世界书、自我笔记、媒体索引、最近引用来源、最近工具调用)→ 同轮候选消息 → 依赖图 → 系统尾(说话人)→ 时钟 → 装饰规则 → 当前消息。断点之前的内容在同一个群里轮与轮之间保持不变,让上游的前缀缓存命中。发给模型的请求里真实的用户 ID、群号换成别名,回复和工具调用里再换回来。
生成结果为空时按标记兜底:暂停响应就发一句人设内的提示后沉默;拒答发固定文本;有出图任务在排队就把「开始生成」当回复;这一轮已经用 say 说过话就沉默;都不是时发「我这边没有生成有效回复。」
02 回复流程
按 [diana-msg] / [diana-line] 分条,去句尾句号;同会话的回复排队,不交错。
只有第一条带引用和 @;触发消息被撤回时不引用,回复针对另一位参与者时不引用当前消息。
第二条起等 max(分条间隔 300 毫秒, 每字 100 毫秒),最多 6 秒;等待时保持「正在输入」。
凭据打码 → 本地图片与语音准备 → 解析 [diana-reply] / [diana-at] → 发送时守卫。
按指纹记在本轮的投递账本里。
每次重试前续期到至少十分钟以后。
记 Acknowledged、写入聊天历史、记下消息 ID,暂停「正在输入」。
聊天回复在单次调用内只发一次;平台因媒体校验拒收时会短暂重试,连同第一次最多 3 次。send_retry_attempts 用于定时推送等非回复发送。入站队列整轮重试时,已经发出去的分条靠投递账本跳过,不会重复发。
03 Agent 运行
机器人开了 Agent 且对方的关系允许用工具时,这一轮由 Runner.Run 生成:模型每一步要么调用一个工具,要么用 agent_finalize 收尾。
不设工具调用次数上限;主人单次工具输出 20000 字,其他人 8000 字。单工具 60 秒,收尾预留 20 秒,协议修复 3 次。
核心工具每轮直接可用;其余只在目录里列一行,先 tools_load 再经 tools_execute 调用。按权限白名单与机器人 / 群级扩展开关裁剪,无权工具回答「没有权限」而不是「不存在」。群聊加载过的工具随群会话快照跨轮恢复。
协议提示词与延迟工具目录;按触发词注入 Skill;可信时钟;上一轮被迫收尾时附上续跑档案。
规划截止 = 请求截止 − 收尾预留(配置值与剩余时间三分之一取小)。
同批其他调用回「未执行」,让每一步都能看到上一步的结果。
延迟工具须先加载并过 schema 校验 → 未知或无权 → 参数纠正 → 扩展变更须带确认码 → 与上一步完全相同则跳过 → 配额(web_search 3 次,自省 6 次、tools_load 4 次不占步数)。
单工具超时与输出预算;run_command 先过命令白名单,再按配置放进沙箱;MCP 另有 60 秒超时。
凭据打码、超长截断;搜索和渲染结果记入证据账本;附上剩余预算。工具报错不致命,变成观察结果交回模型。
分条必须用 [diana-msg] / [diana-line]。
强制收尾发生在步数用完、修复用完、收尾预留时间到了、证据要求没满足时:追加收尾指令和证据账本摘要,只开放 agent_finalize 并强制调用。结束后,被迫收尾时存下最多 12 条步骤摘要作续跑档案(10 分钟有效);最近 8 次工具调用保留 30 分钟注入后续轮次。
一轮最多 3 次、每次 80 字以内、不能换行,直接发出去并重新点亮「正在输入」。说过之后这一轮不再被合并重来,也不过发送前审核。
同步的、没有工具也没有历史的一次模型调用,每条回复最多 4 次、材料不超过 6000 字、90 秒超时。
依次把最长的文本消息摘要(最多 2 次)、把图片换成描述、缩小图片、最后丢掉图片。
Runner 不重试。provider 栈里瞬时错误 700 毫秒后重试一次,上下文超长减半重试最多 4 次,再按配置顺序切到下一个模型配置,已完成的工具步骤不丢。
04 浏览器机制
一次性无头渲染 browser_render 所有人可用、始终注册;带登录态的交互式浏览器只有主人能驱动,每轮只注册一套,模型不会同时拿到两套再自己去猜。
browser_renderbrowsersource.Pick,默认内置在前、扩展在后,取第一个用得上的。
内置浏览器:已勾选且找得到 Chrome
agent_browser_box_disabled
browser_* 改接外部 CDP 地址注册 browser_*。
扩展:总开关开、有连接、没人接管
agent_browser_control_enabled,默认关。
注册 browser_ext_*。
都用不上
不是默认的 127.0.0.1:9222。
注册 browser_*。
内置浏览器的可用性只看「开着且找得到 Chrome」,接管期间 browser_* 直接报「用户正在人工接管内置浏览器」。扩展正好相反:所有连接都在接管时算作用不上,会轮到下一个来源。
04 浏览器机制
插件每条消息最多渲染 2 个链接,正文以「不可信内容」块注入上下文。
只允许 http/https,拒绝内网、回环、链路本地、localhost、.local 等地址。
默认同时 3 个,最多排队 20 秒,排队时间不计入超时。
临时 profile(0700),HOME / XDG 指到临时目录,屏蔽本机域名解析。
每个主文档请求都重新校验地址;触发下载就中止。
DOM、网络、定时器都安静下来;默认 25 秒,最多 60 秒。
结束进程,删掉临时目录。
--screenshot(产物上限 8 MiB,占名额);带脚本或动画的走 html_capture,页面放在假域名 diana-render.invalid 下,请求要么本地应答要么被拦,另有严格 CSP 和虚拟时钟,最多 600 帧、4096 像素,目前不占名额。DIANA_HEADLESS_BROWSER_EXECUTABLE → DIANA_AGENT_BROWSER_EXECUTABLE → 常见安装位置(含下载来的 Chrome for Testing)→ PATH。安装先用系统包管理器,glibc Linux 上装不了时下载 Chrome for Testing 到 DIANA_BROWSER_DIR。04 浏览器机制
只检查 http/https 或 about:blank,加上内置浏览器的 denied_hosts。
标签页被用过后 10 分钟内算作那个会话占用。
按 agent_browser_timeout_ms(默认 15 秒)计时,超时 stopLoading 并报错;正文迟迟传不完的等 8 秒后停止加载。
click / type / key / wait / eval / screenshot…;点击和按键用真实输入事件,被遮挡时退回 el.click();browser_eval 返回值里的 Cookie 打码。
截图存在工作目录 .agent-browser/(每会话一个文件,7 天清理),同时作为图片附给模型;每次调用写一条 browser_action 日志。
| 内置浏览器 | 外部 CDP | 扩展(你的 Chrome) | |
|---|---|---|---|
| 工具 | browser_* | browser_* | browser_ext_* |
| 登录态 | 在这个浏览器里自己登的,按机器人隔离 | 那个 Chrome 自己的 | 你日常浏览器的,Diana 不碰 Cookie |
| 网络边界 | scheme 检查 + denied_hosts,能访问局域网 | scheme 检查 | 站点白名单 / 黑名单、读写档位、限速 |
| 接管 | 点画面自动接管,5 分钟无输入自动交还 | — | 扩展或 WebUI 里切换 |
| 截图 | 有 | 有 | 没有(避免扩大扩展权限) |
进程细节(按需启动、崩溃 3 秒后重启、Xvfb、实时画面)见仓库的 内置浏览器,扩展的授权模型见 浏览器控制扩展,Docker 与 seccomp 见 浏览器依赖。
05 发送前审核
回复生成出来之后、真正发出去之前,还有一串检查可以丢弃、改写或推迟它。其中只有一次是专门的审核模型调用,其余是程序检查。
ignored_model_silent已有副作用 / 插件事实结果 / 出图排队不许沉默,按空回复兜底ignored_no_natural_reply[[DIANA_IGNORE_CURRENT_USER_30M]] / [[DIANA_REFUSE_CURRENT]]
对最终候选回复执行一次审核,不再另行调用语义去重模型丢弃或改写正文。
见下一节。
ignored_* 或公开提示ignored_stop_requested每条消息再过发送时守卫。
05 发送前审核
审核只有开和关两档(默认开,群配置可单独覆盖)。一次调用同时判断下面几项,哪一项都不需要时整个跳过。结论按这个顺序落地:
私聊里机器人上一条发出 3 分钟内才判;只认置信度 ≥ 0.90 的结论。
ignored_stop_requested,非主人暂停响应 30 分钟道别超过宽限次数ignored_conversation_closed群里 @、引用、点名机器人的消息顺带判;判出来给本群记 10 分钟窗口,这一条照常发。
机器人互聊检测默认开。没内容时记一次空转;对方被标记为机器人时,回得密又没目的也记一次。未标记的账号斗嘴不计。同一次审核还给这一轮的新意和目的打分,发出去后按对方这句的篇幅累加回复疲劳。
ignored_ai_reply_loop刻意压低:疑似风险仍放行,只有接近确定的不安全结论才拦。
自相矛盾、答非所问、被截断、有害建议,或 send_confidence 低于 proactive_reply_threshold。
ignored_proactive_reply_quality判为拒答(置信度 ≥ 0.90)的照常发,30 分钟内累计 4 次拒答后暂停响应。
顺序是有意的:对方都说「别回了」,再纠结这条够不够准确没有意义,所以收尾排最前。质量门槛只给主动回复——被点名在说话却因为一个打分沉默,对方看到的就是装死。插件直接给出的回复也过同一次审核。
| 守卫 | 条件 | 结果 |
|---|---|---|
| 凭据打码 | 文本里出现已登记的凭据 | 替换成打码后的文本 |
| 提及与引用标记 | [diana-at] / [diana-reply] 格式不对或指向不存在的消息 | 改写或去掉 |
| 群不可用 | 已确认退群或群被禁用 | ignored_unavailable_group |
| 暂停响应 | 对这个人的暂停期内 | ignored_response_suppression |
| 补充消息 | 同一轮里对方补了一句,被合并进来 | Agent 规划中接进本轮;否则这一版作废,重新生成 |
| 被新消息取代 | 群里同一人又直接叫了机器人;消息已被合并进别的轮次;主动接话批次已变 | superseded_follow_up / superseded_reply_turn / superseded_proactive |
| 幂等 | 这一步已经发过 | 跳过,复用原消息 ID |
| 禁言 / 离线 / 退避 | 被禁言;连接离线;群发送退避中 | 禁言丢弃;离线交给队列重试;退避等待,窗口用尽后丢弃 |
打断和取代只在还没开始分条发送、也没产生外部副作用时生效;已经发出第一条的回复会发完。身份脱敏不是发送时检查:真实 ID 在发给模型时就换成了别名。
排查
每条消息无论回没回,最后都会写一条结果:decision 是结果代码,decision_reason 是给人看的原因。排查时先看它停在哪一段,再回到对应章节。
| 阶段 | 结果代码 |
|---|---|
| 入站队列 | ignored_stale backfill_history_only ignored_private_admission dropped_retries_exhausted |
| 确定性门禁 | ignored_policy ignored_user_blocked ignored_bot_muted ignored_model_quota ignored_member_level ignored_response_suppression ignored_video ignored_bot_message ignored_reply_fatigue |
| 接话评分 | ignored replied_proactive |
| 并发与合并 | merged_into_reply merged_into_backlog_turn superseded_follow_up superseded_reply_turn superseded_proactive |
| 发送前审核 | ignored_model_silent ignored_no_natural_reply ignored_duplicate_reply ignored_proactive_reply_quality ignored_stop_requested ignored_conversation_closed ignored_ai_reply_loop |
| 发送 | ignored_unavailable_group dropped_send_rejected |
| 环节 | 文件 |
|---|---|
| 入站与补拉 | model/assistant/runtime_inbound.go inbound_queue.go inbound_gap.go inbound_retry.go inbound_backlog_merge.go |
| 门禁与接话 | model/assistant/runtime.go(routeMessageEvent、routeProactiveReplyBatch)participation_single_score.go direct_reply_merge.go reply_interrupt.go group_stop.go |
| 回复与发送 | model/assistant/runtime.go(replyTo、sendDecorated)reply_compression.go runtime_outbound.go group_send_guard.go outbound_idempotency.go |
| Agent | model/agent/runner.go finalize.go deferred_tools.go claim_evidence.go;model/assistant/agent_scope.go |
| 浏览器 | model/browsersource/ model/browserbox/ model/browserctl/ model/agent/headless_browser*.go browser_tools.go html_capture.go |
| 发送前审核 | model/assistant/proactive_reply_quality.go reply_semantic_gate.go private_closing.go reply_suppression.go outbound_forward_gate.go |
| 结果记录 | model/storage/inbound_queue.go event_audit.go |