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