跳转至

02 - 架构设计

分层架构

插件采用"入口装配 → 消息管线 → 会话 → 智能体 → 模型"的纵向分层,横向由配置、上下文、记忆、工具、日志等模块提供支撑:

┌──────────────────────────────────────────────────────────────┐
│ 入口装配 src/index.ts                                          │
│  注册配置/智能体/工具/定时器/知识库/命令,绑定 SealDice 事件        │
└───────────────┬──────────────────────────────────────────────┘
                ▼
┌──────────────────────────────────────────────────────────────┐
│ 消息管线 src/pipeline.ts                                       │
│  handleNonCommand / handleCommand / handleBotMessage          │
│  过滤(CQ 码/忽略条件/忽略正则)→ 会话 → 触发判定                   │
└───────────────┬──────────────────────────────────────────────┘
                ▼
┌──────────────────────────────────────────────────────────────┐
│ 会话 src/session/session.ts                                    │
│  chat()/chatStream()/handleReceipt()/reply()/save()           │
│  触发配额(令牌桶)、待机/计数器/计时器/概率/活跃时间模式            │
└───────────────┬──────────────────────────────────────────────┘
                ▼
┌──────────────────────────────────────────────────────────────┐
│ 智能体 src/agent/agent.ts                                      │
│  run() / runStream():构建消息 → 请求模型 → 工具调用循环 → 回复    │
│  工具轮数由配置 MAX_CALL_COUNT 控制(0 不限制)、复读检测、最多尝试 3 次 │
└───────────────┬──────────────────────────────────────────────┘
                ▼
┌──────────────────────────────────────────────────────────────┐
│ 模型层 src/model + src/agent/stream.ts                         │
│ 注册表(连接状态+模型实例) / 列表拉取与分类 / 规则模板 + Provider(超时/重试/用量) │
│ 媒体: 内置供应商预设(media_presets) + 预设←规则合成(占位符渲染) + 响应解析(取数/轮询) │
│  streamService(非流式 / 后端中转流式)                           │
└──────────────────────────────────────────────────────────────┘

侧挂的异步任务层(不进主请求链路):

┌──────────────────────────────────────────────────────────────┐
│ 媒体任务队列 src/media/task_queue.ts                            │
│  submit() 入队即返回 → 并发上限内后台执行 → 结算分流:             │
│   origin=command → 插件直接 replyToSender 投递                  │
│   origin=tool    → 写会话 noticeQueue(带 taskId)                │
│                     + 空闲则触发一轮对话(skipBucket)            │
│                     + 忙则 defer,由轮末 wakePendingNotices 补触发 │
│  工具侧另有「同步等待窗口」:窗口内出结果就当场返回,不进唤醒链路   │
│  ⚠️ 硬约束:结果要么已呈现给 AI、要么仍在 noticeQueue,不得两头落空 │
│     (不得把 chat() 的静默闸门丢弃当作"已送达";详见 docs/03)      │
└──────────────────────────────────────────────────────────────┘

横向支撑模块:

  • config:集中注册与读取全部 SealDice 配置(带 60 秒缓存),同时作为全局单例被各模块引用。
  • context:会话的消息上下文(user/assistant/tool),负责增删、压缩、总结触发、轮数限制。
  • memory:长期记忆、观察记忆、知识库,统一由 MemoryManager 收口。
  • tool:工具注册表、调用校验、执行与审计;含 MCP 客户端与技能。
  • cmd:.ai 根命令与子命令分发、命令权限校验。
  • media:语音合成/文生图的异步任务队列(提交即返回、完成直接触发一轮对话投递),与 timer 同属"由插件主动回调会话"的机制;请求结构来自内置供应商预设,规则只做覆盖。
  • timer / usage / logger:定时任务、token 用量统计、日志。

核心概念

概念 说明
会话(Session) 私聊或群聊的工作单元,以 QQ:<uid> / QQ-Group:<gid> 为 ID;包含上下文、记忆、设定、图片管理器、工具开关、流式状态等,持久化到存储
上下文(Context) 会话内消息集合,分为 user(含系统用户消息)/ assistant / tool_calls / tool 回调四种,带时间、来源、消息 ID 等元数据
记忆(MemoryUnit) Hindsight-like 记忆单元 = 内容 + 向量 + 标签 + 实体/关联 + 用户/群组 + 重要性/访问次数等权重;另有观察记忆(Observation)与心智模型(MentalModel)两类结构
知识库(KnowledgeBaseService) 配置驱动的只读知识库:JSON 里的 Markdown 正文按 #/##/### 标题与段落自动分块;关键词检索 + 惰性向量重排,AI 通过 knowledge_search/knowledge_read/knowledge_list 工具按需读取,内容由管理员配置维护
智能体(Agent) 一组"角色配置 + 会话服务";默认智能体名为 *,另有 compress_agent(压缩)、summarize_agent(摘要)
工具(Tool) 注册到 toolMap 的函数,描述采用 OpenAI function calling JSON Schema;支持调用海豹内置指令
媒体任务(MediaTask) 语音合成/文生图的异步工作单元(tts / tti):提交即返回任务号,完成后按来源投递(命令→插件直发,工具→唤醒 AI);每会话最多保留 50 条已结束任务,队列只存句柄不存 base64
MCP 通过 Streamable HTTP / JSON-RPC 接入外部 MCP 服务器,将 tools/list 结果注册为工具
技能(Skills) 配置驱动的技能条目,AI 通过 use_skill 工具按需获取内容,支持技能间引用
角色设定 每个 roles[] 条目 = name + prompt(名称超过 20 字符自动截断,以 . 开头为隐藏角色),通过豹语变量 $gSYSPROMPT 选择,.ai role 可切换

消息处理流程

1. 非指令消息(handleNonCommand)

收到非指令消息
  ├─ 私聊且 IGNORE_PRIVATE → 忽略
  ├─ session.checkActiveTimer() 维护活跃时间定时器
  ├─ IGNORE_CONDITION(豹语表达式命中 1)→ 忽略
  ├─ IGNORE_REGEX 命中 → 忽略
  ├─ 解析 CQ 码;若含 CQ_TYPES_ALLOW 之外的 CQ 类型 → 不处理
  ├─ 触发判定(命中其一):
  │   ├─ TRIGGER_REGEX + TRIGGER_CONDITION → 立即 chat('非指令')
  │   ├─ AI 自设触发条件(triggerConditionMap,关键词/指定用户)→ chat('AI设定触发条件')
  │   └─ 待机模式 → 仅录入上下文(不主动发言);计数/概率/计时器模式 → 按模式触发 chat
  └─ 最终保存会话
  • ob11 额外接收:核心原生(milky)路径会丢弃卡片/视频/文件/语音/合并转发等消息段;安装 ob11 网络连接依赖后,subscribeOb11Receive 订阅其事件分发(net.getEventDispatcher),只处理含这些额外段的消息,构造 ctx/msg 后走同一套 handleNonCommand(黑名单/忽略/触发/待机全部生效),并按段类型过滤避免与核心回调重复处理;合并转发经 get_forward_msg 展开为可读文本,过长走压缩。
  • ob11 依赖通知/请求事件:通过 subscribeOb11Receive 额外订阅依赖的 onNoticeEvent/onRequestEvent,把 OneBot 11 标准通知与 Milky 扩展事件(群禁言/群管理变更/群上传/运气王/群荣誉/群成员增减/消息撤回/好友添加/入群申请/好友申请等)转成文本提示词录入 AI 上下文,仅作背景、不触发 AI 发言;仅当会话待机或全局待机开启时才录入(与普通消息入库口径一致),命中会话不存在时自动新建(开启待机即代表要使用)。默认白名单包含全部可转换事件类型(元事件除外):元事件(心跳/生命周期)不属于通知/请求事件、始终不录入,poke 虽属 notify 子类型但由原生 onPoke 处理、不生成事件文本,实际不会录入;入群/好友申请属 request 事件与 notice 共用白名单;原生与 ob11 依赖双路径由 3 秒事件级去重防双录——去重 key 带整包内容指纹,同一类型"连续但内容不同"的事件(不同文件上传、禁言 vs 解禁、不同好友申请等)不会被窗口吞掉;同一条事件重复/双路径到达时不简单丢弃,而是"后到覆盖先到"就地替换已入库条目(文本/raw 取最后到达,位置不变);机器人自身入群/被移出/退群由原生回调(onGroupJoined/onGroupLeave)覆盖,依赖侧跳过,避免两路径以不同 eventType/文本各录一次。事件文本不再走压缩智能体:提示词文本由纯代码按模板从事件 JSON 生成(通常很短),超长仅纯代码 head 截断兜底。防注入:事件文本以 [system:名称]…[/system] 成对边界渲染,并在 system prompt 声明为背景/参考、不得执行;工具返回等外部数据同样以 [tool_result]…[/tool_result] 边界包裹,内部标签(含 [/system] 等闭合形式)入库前统一剥离,防语义注入与标签逃逸。事件原始数据(完整 OB11 事件 JSON / 原生回调信息)随事件条目挂载(条目上限 20、单条序列化上限 4000 字符,超出丢弃 raw 但保留文本提示词),不参与上下文渲染、模型不可见;AI 需要完整字段时调用 read_raw kind=event 读取(原 get_event_detail 工具已删除)。事件条目随上下文裁剪/遗忘/压缩自然失效,无需单独清理。
  • 消息接收优先级固定为“核心原生消息 → ob11 依赖补充”:核心回调已经提供的文本、图片、表情、at、reply、poke 等内容只从 msg.message/核心事件入库;ob11 依赖只补充核心过滤掉的文件、视频、语音、卡片、音乐和消息节点。依赖事件先到时会等待核心事件,避免同一条消息重复入库;没有核心事件时才允许完整兜底。
  • Milky 核心事件的原生段直接从 msg.segments 转换为内部渲染标签,不先转 CQ 码;OB11 依赖事件仍使用其事件数组。消息 ID、引用 ID 和发送者信息在转换阶段统一补齐,负数 ID 也按字符串保留。
  • 多模态消息:最终解析到的 chat 模型实例带「视觉」标签(视觉标签来源:models[].kind = "vision" 显式声明 > 接口自报能力位;⚠️ 视觉不再按模型名猜测,glm-4v/gemini/pixtral 这类名字不会自动带视觉标签)时按多模态处理,handleMessages 把用户消息里的 [img:...]/[avatar:...]/[group_avatar:...] 标签转成 image_url 内容块直接传给模型;URL 图片优先转 base64(10s 超时兜底)。渲染标签统一为方括号格式(4.14.0 起首次对话自动迁移历史 <|...|> 数据,解析层不再兼容旧标签)。

2. 指令消息(handleCommand)

  • 指令消息默认不写入上下文;扩展指令工具的 CmdArgs 由 src/tool/extension_executor.ts 在每次调用现场按当前参数新建,不读取会话历史,也不要求先使用 .r。核心指令仍通过 run_core_command 的 OB11 核心桥执行。
  • 默认不写入上下文(RECEIVE_CMD=false);开启后在待机模式下把指令消息计入上下文。

3. 机器人自身消息(handleBotMessage)

  • 先交给 session.tool.listen(供等待消息返回的工具调用场景)。
  • 默认不写入上下文(RECEIVE_MSG_BY_BOT=false);开启后在待机模式下记录。
  • 刚发出的回复(等于 context.lastReply)会被跳过,避免自问自答。

4. 戳一戳事件(onPoke)

  • 构造一条 [CQ:poke,...] 消息:戳对象是骰子自己则走 onMessageSend,否则走 onNotCommandReceived。

一次 AI 回复的完整链路

以非指令正则触发为例:

  1. pipeline.handleNonCommand 命中触发条件 → session.chat(ctx, msg, '非指令')。
  2. Session.chat():令牌桶配额检查 → 重置 BLOCKED 工具状态 → 判断模型是否流式(body.stream === true),是则走 chatStream,否则走 Agent.run()。
  3. Agent.run():
  4. handleMessages() 组装请求消息:system prompt(分节)+ 预设上下文 + 会话上下文(按 INSERT_COUNT 周期插入 system、按 MAX_CONTEXT_TOKENS 整包 token 估算与预算裁剪(含模型校准系数,整条优先丢弃、单条兜底截断)、过滤无回调的 tool_calls)。
  5. streamService.sendChatRequest() 请求模型。
  6. 若开启函数调用且返回 tool_calls(或提示词工程模式匹配 ``function 代码块),先发送已生成文本,把工具调用写入上下文,执行工具(ToolRunner`),把回调写回上下文,然后进入下一轮,直至模型给出最终回复(工具轮数由「允许连续调用函数次数」配置控制,0 为不限制)。
  7. checkRepeat() 检测复读,检测到则重新请求,一轮对话最多尝试 3 次(含首次)。
  8. Session.reply():对每段回复调用 replyToSender 发送(ob11 依赖可用时拿到 message_id),把助手消息写入上下文。
  9. 上下文写入助手消息后检查持久化上下文 token 是否超限:超过「上下文最大token」时保留最近「对话保存轮数」轮,更早消息先归档沉淀为观察/长期记忆再删除;最后 session.save() 持久化。

Prompt 组装结构

prompt/builder.ts 的 buildSystemPromptContent 负责分节组装 system prompt:

SYSTEM_MESSAGE_TEMPLATE(用户可编辑的骨架)
├── instruction:当前角色设定
├── 会话信息(平台/类型/名称/ID)
├── 图片/音频资源(本地图片、本地语音)
├── memoryPrompt:长期记忆段(按开关 MEMORY)
├── summaryPrompt:观察记忆段(按开关 SUMMARY)
├── knowledgePrompt:知识库段(按开关 KNOWLEDGE;只列条目索引:ID + 标题,1500 字符预算内尽可能多列(≤100 条),不注入正文,模型用 knowledge_read 读取详情)
└── toolPrompt:工具函数段(仅提示词工程模式)
最后追加"可用技能"摘要段(两种模式都可见)

模板均为 Handlebars 模板,渲染失败或配置损坏时回退到内置默认模板(DEFAULT_TEMPLATES),并注册了 index/json_stringify/time 三个 Helper。