03 - 核心模块详解¶
本章按目录逐模块说明职责与关键类型。代码路径均相对仓库根目录
src/。
入口与装配(src/index.ts)¶
- 注册 Handlebars Helper:
index(从 1 开始)、json_stringify、time。 - 依次初始化:
Config.registerConfig()、initAgents()、checkUpdate()、Tool.registerTool()、TimerManager.init()、knowledgeService.init()、registerCmd()、PrivilegeManager.reviveCmdPriv()。 - 写入存储版本标记
storage_version(当前STORAGE_VERSION = 2,结构变更时递增)。 - 绑定九个 SealDice 事件:
onPoke、onNotCommandReceived、onCommandReceived、onMessageSend、onGroupMemberJoined、onGroupLeave、onMessageDeleted、onBecomeFriend、onGroupJoined,消息与戳一戳委托给MessagePipeline,原生通知事件统一交给MessagePipeline.handleNativeNoticeEvent(与 ob11 依赖通知共用白名单/去重,避免双录)。
消息管线(src/pipeline.ts)¶
MessagePipeline 是唯一的事件处理入口:
handleNonCommand(ctx, msg):执行忽略/触发判定,见 02-架构设计。handleCommand(ctx, msg, cmdArgs):处理收到的指令消息;可选录入上下文。扩展指令工具所需CmdArgs不从这里缓存,而是在run_ext_command调用现场构造。handleBotMessage(ctx, msg):转发给工具监听;可选录入上下文。- 通知/请求事件(
handleOb11NoticeEvent/handleOb11RequestEvent/handleNativeNoticeEvent):转文本提示词,仅当会话待机或全局待机开启时录入上下文(仅背景、不触发 AI),原生与 ob11 依赖共用白名单与 3 秒事件级去重避免双录——去重 key 含整包内容指纹(连续但不同的事件不会互吞),重复/双路径到达时"后到覆盖先到"就地替换已入库条目;机器人自身入群/退群由原生回调覆盖、依赖侧跳过。事件原始数据随条目挂载(不渲染给模型、不设条数/长度上限,随消息清理/归档失效),AI 经read_raw kind=event读取完整 JSON(单次返回条数上限 20、总字符有硬上限);事件提示词文本超长仅纯代码 head 截断(2000 字)兜底(原get_event_detail工具已删除)。 - 会话统一通过
getSession(sid)获取;sid私聊为用户 ID、群聊为群 ID。 - CQ 码白名单
CQ_TYPES_ALLOW = ["at","image","reply","face","poke"],含白名单之外 CQ 类型的消息不处理。
配置(src/config/)¶
config.ts¶
- 通过
seal.ext.find/new/register创建扩展实例ext(插件名aiplugin4)。 configMap集中登记 18 个配置分组(按注册顺序,「错误处理」固定最后):base、model、role、context、received、trigger、reply、tool、mcp、skills、subagent、memory、knowledgeBase、image、backend、event、resource、error。- 每个分组由
register()(向 SealDice 注册配置项)与get()(读取并返回类型化对象)构成。 - 读取带缓存:
CONFIG_CACHE_TTL = 60000毫秒,Object.defineProperty把Config.<分组>变为缓存 getter;ModelConfig.get()在注册时预热:同步解析「模型配置」(v5 JSON,校验失败整份拒绝)→ 写死models的连接即时生效,models留空的连接异步拉取模型列表(首条真实消息前await Model.ensureLoaded())。 getRegexConfig():把模板配置的多行正则拼接为单个RegExp,失败时返回永不匹配的/(?!)/。
static_config/(静态常量)¶
VERSION(当前 4.27.0)、AUTHOR、NAME、STORAGE_VERSION(当前 2)、CONFIG_CACHE_TTL、CQ_TYPES_ALLOW。PRIVILEGE_LEVEL_MAP:master=100 / whitelist=70 / owner=60 / admin=50 / inviter=40 / user=0 / blacklist=-30。HELP_MAP、ALIAS_MAP(命令别名表)、FACE_MAP(QQ 表情编号 → 名称)。PROVIDER_MAP:10 家厂商的默认baseURL(仅用于配置生成器与反查参考;v5 的baseURL是必填项、不再兜默认值)。DEFAULT_CHAT_MODEL_BODY/DEFAULT_MULTIMODAL_MODEL_BODY/DEFAULT_EMBEDDING_MODEL_BODY:默认请求体兜底(用途组groups[].body未覆盖时生效)。- 按类别拆分为
meta.ts(版本/作者/插件名/缓存 TTL/存储版本)、cq.ts、permission.ts、help.ts、alias.ts、face.ts、providers.ts(厂商默认地址)、model_defaults.ts、memory.ts(记忆检索内置常量),由index.ts统一 re-export,历史导入路径../config/static_config保持不变。
configs/(19 个分组)¶
| 文件 | 分组 | 主要配置项 |
|---|---|---|
base.ts |
基础 | 日志级别/简短打印/记录消息内容、请求超时、请求并发/队列限制、海豹核心路径、全局待机 |
error.ts |
错误处理 | 启用报错自动处理、上下文超长自动归档重试、余额不足自动切换模型、自动切换触发错误(默认 balance)、自动切换策略(跨厂商优先/配置顺序)、切换后发送通知 |
model.ts |
模型 | v5:v5 JSON 模型配置(src/config/v5/)——解析 providers(api/baseURL/apiKey/provider 可省/models/enabled/request)、purposes(用途 → 模型 id 有序数组)、groups(用途组 body),解析后 Model.bootstrap() 建连接状态机并异步拉取模型列表,同步全局分用途覆盖(modelPurposeOverrides 存储键)。校验失败整份拒绝(不做部分生效),错误带 $.providers[0].models[1].name 式路径;没有 v4 兼容/迁移(旧两项已注销,见 docs/13) |
role.ts |
角色设定 | 角色设定(v1:roles[] 的 name/prompt;名称 >20 截断,$gSYSPROMPT/.ai role 切换,重载 JS 生效) |
context.ts |
上下文 | 预设上下文(role 按 user/assistant 轮流)、对话保存轮数、上下文最大token(默认 1000000;0 或负数 = 跟随报错探测到的模型真实窗口 × 0.8,未探测到回退 100 万,显式正数优先)、插入 system message 间隔轮数、消息压缩阈值(压缩前原文保留,read_raw kind=user 可查)、冷态窗口字数、冷态单条截断字数、归档保留原文、窗口上限字数、结算水位 / 结算最小门槛 / 结算空闲门槛 / 单次结算调用上限(分片数)、原文分片保留天数 / 上限、待机入库配额;归档与窗口移出分片由 src/context/archive_store.ts 管理 |
received.ts |
消息接收 | 接收图片/指令/骰子消息、忽略私聊、忽略豹语条件、忽略正则 |
event.ts |
事件接收 | 接收依赖通知事件、通知事件白名单(notify 子类型单独匹配,含入群/好友申请) |
trigger.ts |
消息触发 | 默认计数器/计时器/概率/活跃时间、评分触发配置、触发正则、触发条件、令牌桶 |
image.ts |
图片 | 图片全局识别豹语条件、识别图片时将 URL 转 base64(永不/自动/总是)、图片识别展示截断字数(默认5000,0=不截断不保留) |
tool.ts |
工具 | 函数调用开关、提示词工程、拉黑前需骰主确认、无工具调用时续跑提示、工具方向提示、连续调用次数(默认0=不限制)、工具响应截断字数(默认10000)、禁止/默认关闭函数与 OB11 action、可调用指令白名单、是否允许调用所有指令、指令前缀、音乐服务配置、音色 |
mcp.ts |
MCP | 是否启用MCP、MCP服务器(标准 mcpServers 对象)、MCP会话空闲回收分钟、MCP每服务器最大会话数 |
skills.ts |
技能 | 技能列表(v1:skills[] 的 name/description/platform/content;缺失的默认技能自动补入) |
subagent.ts |
子代理 | 是否启用子代理(默认开)、最大委派深度(默认 3,0=禁止委派)、子代理禁止调用工具(留空=继承主会话全部已开启工具) |
memory.ts |
记忆 | 长期/观察记忆开关、记忆LLM抽取/重排/观察合成/推理、自动维护固定心智模型、新近度权重/半衰期、长期记忆条数上限(默认100,0=不限制)、巩固后自动刷新心智模型、刷新最小间隔、心智模型刷新模式(full/delta)、刷新排除其它心智模型、定时刷新间隔 |
knowledge_base.ts |
知识库 | 知识库开关、知识库配置(v1:libraries[] 的 name/description/platform/content,正文为 Markdown) |
reply.ts |
回复 | 引用(含戳戳跳过)、最大字数、去首尾空白、换行转义、分段发送延时、禁止复读与最低相似度 |
resource.ts |
资源 | 本地图片路径、本地语音路径(语音名=路径)、本地文件路径、本地视频路径;供 list_resources / get_resource_path 查询 |
backend.ts |
后端 | 后端配置(v1 JSON:backends.<后端名>.{url,token};所有后端都支持 token,请求带 Authorization: Bearer <token>,401 时提示重新复制后端配置;空=全部出厂默认;旧 8 个独立键不迁移、不兼容);网页读取与 md/html 渲染(MCP)在「MCP → MCP服务器」 |
forum.ts |
论坛 | 论坛地址、论坛API Token、论坛签名密钥 |
web.ts |
联网 | 联网配置(v1 JSON):providers(dialect/baseURL/apiKey/enabled/timeout/headers;generic 另带 search/fetch 模板与取数路径)、routes;留空=内置 SearXNG;10 家 provider + generic;校验失败只记日志、本次不生效。返回条数/详细程度/超时/正文截断/失败回退为同页 5 个独立配置项(不写进 JSON) |
注意:本地图片/语音/文件/视频路径由资源分组统一注册;list_resources 查询资源 ID,get_resource_path 查询实际路径/URI;当前会话发送图片优先使用 [img:图片ID]。
prompt 模板(src/prompt/templates.ts)¶
模板在代码中以 TEMPLATES 常量内置(不再注册为配置项,避免误改导致渲染损坏),读取时 compileTemplate 编译,仅在编译失败时回退空函数;渲染期异常不兜底,由调用方处理。模板清单:
system prompt模板长期记忆prompt模板观察记忆prompt模板工具函数prompt模板图片识别prompt模板记忆观察prompt模板
模型层(src/model/)¶
v5/(schema.ts / adapt.ts / vendor.ts / default.ts):v5 配置的纯逻辑层(不依赖 seal,便于单测与跨仓库联测)。schema.ts定义类型 +parseV5Config/validateV5Config(一次报出全部问题,带 JSON 路径);adapt.ts的adaptV5把 v5 翻成下游沿用的ConnConfigLike[] + ModelRuleTemplate[](per-modelrequest段 → 一条models定向的规则),运行时改动面因此被限制在少数几个字段;vendor.ts由baseURL的 host 反查厂商标识(resolveVendor/vendorByUrlSuffix);default.ts是纯数据的出厂最小示例(单独成文件以便集成联测直接打包引用)。api_dialect.ts:协议方言判定,把"是不是 anthropic"这类按厂商名猜协议的硬编码集中到一处(resolveApiDialect/isAnthropicMessages/isOpenAiResponses)。v5 按api字段分派;api缺省时退回按 provider 名推断,保证 v4 迁移期连接与既有单测不因缺新字段而行为突变。openai-responses本期未实现,运行时明确报错而不是静默走错协议。baidu_token.ts:百度access_token的获取与缓存——百度接口只认 token 而非 API Key,而连接只有apiKey一个字段,故约定填"APIKey:SecretKey"自动换取(也接受|分隔;密钥含冒号用\:转义,2 个以上未转义冒号视为写法非法、整体当令牌)。只存内存不落盘、过期前 5 分钟重新换取、同连接并发请求共享同一次换取(in-flight 去重)、换取失败缓存 60 秒避免反复撞鉴权接口。model.ts:Model静态注册表(states连接状态 +entries模型实例)与全局分用途覆盖purposeModelOverrides(use → 模型 id;由.ai model写入modelPurposeOverrides存储键),另有配置默认层configPurposes(v5purposes的有序数组,优先级低于运行时覆盖)。模型实例来自 v5 连接的models写死项或启动自动拉取,只存内存、不持久化,按能力标签(catalog.ts)分类进各用途候选;ref优先用配置里的模型 id(稳定标识,不受连接顺序/重名影响,取代 v4 的[连接序号]:模型名)。getChatModel(use)/getMultimodalModel(use)/getEmbeddingModel(use)按「运行时覆盖 > 配置绑定(有序,失效自动往后切) > 该用途首个可用」三层解析,resolveSourceForUse(use)返回当前生效层(override/config/auto/none)供.ai model展示;findModelByRef按标识反查;listModelsForUse(use)返回候选列表(.ai model与余额不足自动切模型共用);ensureLoaded()/pull()供首条消息等待与.ai model pull立即重拉(强制网络);reset()供测试/热重载。catalog.ts:模型能力分类(text/vision/embed/gen/tts)——判定顺序models[].kind显式声明 > 命名终值(reranker/生图/语音/嵌入白名单,如 text-embedding-/bge-/dall-e/cosyvoice/fish-speech) > 接口自报能力位(仅正向证据:vision/embed) > 兜底 text。⚠️ v5 删除了视觉的命名白名单(原GENERIC_VISION_PATTERNS:pixtral/glm-4v/qwen-vl/*vision/gpt-4o/gpt-5/kimi-k2 等)——那批正则最容易过期(新模型名永远追不上),且猜错会让用户误以为模型已具备视觉能力;视觉现在必须显式声明(接口自报能力位仍作正向证据)。生图白名单的flux/sd3/seedream/midjourney/imagen/veo改用词边界匹配并补qwen-image(聚合平台的厂商前缀名如black-forest-labs/FLUX.1-schnell由此可识别)。classifyModel的extra承载 manual 与接口能力位;语音仍必须先于能力位判定——否则含gpt-4o的gpt-4o-mini-tts会被误判(顺序约束保留以防回归)。request_rules.ts:per-model 请求规则的存储与合并(v5 里由adapt.ts从models[].request生成)。字段:use(用途组)、models(按模型定向,优先级高于 use)、body、params(AI 可覆写参数元信息)、prompt(extra 自由提示词)、request、response、bodyMode。命中规则分两趟收集:先 use 组(未声明 models 的行)、后 models 定向行(后者覆盖前者)。bodyDefaultsFor(use[, ref])返回合并后的请求体模板;promptExtraFor(顺序追加) /responseRuleFor/requestOverridesFor分别返回提示词、取数规则与调用期覆盖;ruleBodyFor/ruleParamsFor只取规则部分(供与内置预设合成),ruleBodyModeFor取命中规则里最后设置的bodyMode;defaultMediaParamsFor给媒体用途的代码默认参数(音色 alloy、语速 1.0、尺寸 1024x1024)。媒体用途不叠代码兜底 body:请求结构由「内置预设」提供(见media_presets.ts),bodyMode未声明时媒体用途默认replace(整体替换预设结构),声明merge则逐键合并——避免预设里 OpenAI 形状的键混进阿里/Google 等原生结构;mergeInto只在两侧都是普通对象时深合并(否则会把字符串占位符"{text}"展开成{0:'{',1:'t'…})。media_presets.ts(4.24.0 起):供应商内置请求预设,逐条迁移自旧依赖插件的tts/MODELS.md与tti/MODELS.md。MEDIA_PRESETS按tts/image-generation+ 小写 vendor 键给出结构(body/request/response/params),同一 vendor 可用match(正则)按模型名细分——百炼原生wanx*(异步轮询)与qwen-image*(同步)、Stability 的 core/ultra、OpenAI 的 gpt-image 与 dall-e 由此区分;未收录的 vendor 落''的通用(OpenAI 兼容)预设。resolveMediaPreset(vendor, use, modelName)做选择。地址约定:「模型配置」的baseURL是根地址,路径由预设的request.urlSuffix拼接;joinMediaUrl会识别"baseURL 已以该路径结尾"(兼容直接粘旧版完整接口地址),urlSuffix = ""表示baseURL原样即接口地址。百度预设另声明auth_token = "baidu-token"触发换 token(见baidu_token.ts)。media_template.ts(4.24.0 起):媒体模板引擎(纯模块,便于单测)。composeMediaTemplate(use, vendor, modelName, modelRef)做预设 ← per-model 规则的合成(body 按bodyMode决定整体替换或逐键合并、request/response/params 逐键覆盖、headers逐键合并、poll_*也接受写在request的旧写法),resolveMediaUrl按「规则 url > 规则 urlSuffix > 预设 url > 预设 urlSuffix > 用途默认」定址,templateParamKeys采集请求体与地址里可覆写的自定义参数;collectPlaceholders(template)采集模板里出现的自定义占位符(排除内置);renderMediaBody(template, ctx)渲染请求体——整值占位符保留原始类型、混合串按字符串拼接、既无传参也无 default 时整个键删除、嵌套表键全删时剪枝;normalizeMediaParams(raw, allowed, meta, opts)做参数归一与校验(语义别名 speed→rate/emotion→instruction、保留字拒绝、类型强转、min/max 与 enum 校验,coerceStringTypes供命令 kwargs 把"1.5"转成数字);buildMediaParamHint/useParamSummary/describeMediaParam由合成后的模板生成给 AI 的逐模型参数清单(因此不在配置里写任何请求覆盖也会列出预设里的 voice/rate/size)。buildMediaSubmitReply生成同步/异步两种提交回执文案。media.ts(4.24.0 起):媒体请求编排与响应解析。requestMedia(spec)复用requestModel的超时/重试/ApiError 分类,按response.data_type决定取数方式(有data_path走 JSON;base64/hex且无data_path表示整个响应体即数据,如 TTS 音频流,并按 content-type 自适应);normalizeMediaData兼容 url/base64/hex/data-url,extractMediaData支持data_path与各厂商候选路径兜底,extractMediaError提取业务错误,并在声明了task_id_path+poll_url时轮询异步任务。list.ts:连接级「获取可用模型列表」适配——OpenAI 兼容GET {base}/models(Bearer);anthropic-messages协议走x-api-key+/v1/models并limit=1000循环翻页;支持连接request(list_url/auth_header_name/headers/timeout)覆盖。baseURL填成完整接口端点时给出可操作提示:拼出的/models必然不存在(典型是把百炼原生的.../multimodal-generation/generation当baseURL填了),此时按vendorByUrlSuffix判定(只有唯一 vendor 拥有的路径才命中,自建网关不会误报)并提示"改填基础地址或指定 list_url",而不是发一个注定失败的请求再报含糊 404。对象项额外提取供应商自报能力位(vision/embed:type/model_type/kind/task、architecture.input_modalities、capabilities、supports_vision/vision;未知取值含 anthropic 的type: "model"一律忽略),normalizeModelListResult归一化为「模型名(保序去重)+能力位映射」交上层分类;拉取结果类型为ModelListResult(string[]是其等价子集,兼容既有单测桩);不发额外请求(不做 /api/show、逐模型详情等探测);拉取函数可注入便于单测。balance.ts:连接级账户余额查询(.ai balance 用)——deepseek/moonshot/siliconflow 内置余额端点;其余平台支持在连接request里配置balance_url/balance_json_path(+balance_divisor/balance_currency)后查询自定义网关(one-api/new-api 等)。api_error.ts:ApiError与语义分类——把 OpenAI 兼容/Anthropic/Google/智谱/阿里/SiliconFlow/网关等非 2xx 报错归一化为 balance(余额不足)/context_length(上下文超长)/rate_limit(限速)/overload(过载)/auth/permission/content/parameter/server/unknown 类别,并携带 HTTP 状态码、服务商与响应体原文。provider.ts:requestModel()统一请求入口:解析协议方言(resolveApiDialect)、超时(TIMEOUT或规则request.timeout)、按类别退避重试(429/过载/服务端/网络类,尊重Retry-After,其余不重试;标了noRetry的运行时不具备能力如 goja 无 arrayBuffer 直接失败,不白等退避)、成功时上报 token 用量(媒体用途 tts/image-generation 跳过用量与 token 校准——媒体响应里的 usage 与对话 token 语义无关);错误统一抛带 status/provider/响应体/类别的ApiError(对外 message 文案不变);overridesFromRule把规则的 request 段转成RequestOverrides(含authToken/authTokenScope),媒体路径由MediaRequestSpec.overrides直接传入(已合成,不再二次查规则)。adapter.ts按协议方言转换请求/响应格式:对话路径anthropic-messages走 Messages 格式,媒体请求体一律原样透传(结构由内置预设或骰主的body决定,适配层不再做阿里/Google 的扁平 body 兜底包装)。types.ts:用途类型ChatModelUse('chat'|'compression'|'summarization'|'judge')、MultimodalModelUse('image-understanding'|上述对话用途)、EmbeddingModelUse('text-embedding')、MediaModelUse('tts'|'image-generation');isMediaUse(use)供跳过用量等判定复用。
请求服务(src/agent/stream.ts)¶
sendChatRequest():非流式对话,函数调用模式(STATUS && !PROMPT_ENGINEERING)下注入tools/tool_choice,统计用量;支持throwClassified上抛分类错误(默认关闭,不影响压缩/评分等调用方)。startStream()/pollStream()/endStream():把模型配置(URL/Key/body)发给后端流式输出服务中转,按id轮询增量结果。- token 估算与校准:
src/token_calibration.ts按模型记录实际 prompt token 与估算的比值(校准系数),utils/message.ts裁剪时用系数预测结果;流式请求结束按真实 usage 回写校准,避免长上下文估算偏差。
会话(src/session/)¶
session.ts:Setting:priv、standby、regexTrigger、counter、timer、prob、judge、activeTimeInfo(start/end/segs)。Session:agentName、sessionId、sessionType、state、context、memory、setting、stream、bucket、tool(state/callCount/listen)、lastCtx(不持久化);运行时字段running(是否有 run/runStream 在跑)、starting(是否正在启动 run/runStream,置位期间同会话新消息一律挂起)、stopVersion(.ai stop 自增的中止版本号)、stopEvent(停止信号,fireStopEvent 唤醒等待者)、pendingQueue(会话忙时挂起的待入库消息队列)、activeRuns(本会话当前在跑的请求数)均不持久化。toolStategetter:按BLOCKED与DEFAULT_CLOSED同步每会话工具开关,并清理已删除工具的残留状态;若会话带toolRestriction(子代理继承的工具面 allow/deny 求交,deny 优先)则在其上二次收窄。- 技能/知识库会话级开关
skillState/kbState:只记录被.ai skill off/.ai kb off关闭的项,缺省=开启,随session.save()落盘,并参与 system prompt 静态段缓存 key。 - 子代理运行时字段(均不持久化):
headless(reply() 不发送、文本进 headlessTranscript)、toolRestriction、parentSessionId、subagentDepth、noticeQueue(子代理结算通知队列)、headlessTranscript;flushNotices()在每轮模型请求前把队列写入上下文([system:子代理 …],与 flushPending 同点调用)。 chat():配额检查 → 重置 → 流式判断 →Agent.run/runStream→save()。reply():逐段发送并写入上下文。- 活跃时间:把时间段切分为
segs段,checkActiveTimer用TimerManager安排下一次触发。 session_service.ts:getSession(sid)从存储复活会话(经revive),SessionService持有sessionMap与全局memory。group.ts/user.ts:群档案(角色/群主/管理员/成员/忽略名单)与用户档案(名称)。types.ts:SessionType('group'|'user')、State、GroupInfo、UserInfo。
上下文(src/context/)¶
types.ts:消息类型定义:UserMessageItem、SystemUserMessageItem、AssistantMessageItem、ToolCallsMessage、ToolCallbackMessage、RequestMessage。message.ts:Message静态方法判断消息类型;handleMessages把内部消息转为 API 请求格式,元数据(时间/用户/消息ID/系统来源)存在消息项字段中,多模态模式下把[img:...]等图片标签转成image_url内容块直接传给模型。系统名义消息(群事件/触发原因/定时器等)与工具返回分别以[system:名称]…[/system]、[tool_result]…[/tool_result]成对边界渲染,配合 system prompt 声明防注入;stripInternalTags统一剥离内部标签及其闭合形式([/system]等),防止标签逃逸。pipeline.ts的接收适配遵循核心优先、依赖补充原则:核心msg.message/Milkymsg.segments直接进入统一转换器;ob11 依赖只补充核心不接收的 file/video/record/json/music/node 等段,并通过唯一消息 ID 去重。转换器输出统一的[from:...]、[time:...]、[msg_id:...]、[img:...]、[at:...]、[reply:...]、[face:...]、[poke:...]标签。handleMessages在组装 system、预设上下文、历史和工具定义后进行整包 token 估算;system 与预设上下文不裁剪,历史从最早消息开始裁剪。长对话中按INSERT_COUNT插入短提醒,不会重复复制完整 system prompt。context.ts:Context类:- 添加用户消息(自动改名、超长消息交给
compress_agent压缩)、助手消息(写入后触发持久化 token 超限归档检查)、系统用户消息、工具调用/回调消息。 - 持久化 token 超限归档:
archiveByTokenIfNeeded()在新增消息后按需执行——超过MAX_CONTEXT_TOKENS时保留最近MAX_ROUNDS个真实 user 轮(出现一次真实 user 视作一轮),更早消息按ARCHIVE_CHUNK_TOKENS(50000 token)切块,逐块经MemoryManager.summarizeChunkWithRetry(最多重试 3 次)沉淀为观察/长期记忆;归档的群聊原文写入独立分片 keysession_<id>:arc:<seq>(只写一次、永不重写,见src/context/archive_store.ts),再从上下文移除;轮数不够时(待机群常见:只有 1 轮)按 contentItem 粒度走archiveOldestItems()归档,同样先 summarize 再写分片;归档失败不再降级硬删(数据保持原样并返回 false,调用方据此如实记录)。函数支持显式target预算(模型报错「上下文超长」时按 min(本地预算, 报文窗口×0.8) 复用同一链路归档后自动重发)。handleMessages另有请求级 token 预算裁剪(整条优先丢弃、仅剩单条保护消息时兜底截断)。 - 冷热状态(算出来的,不持久化):
Session.stateOf()按"是否待机 + 是否有在途任务 + 距上次相关活动时长"返回 HOT/COOLING/COLD;「相关活动」=任何一次session.chat()(普通消息入库不算)。冷态下addUserMessage零 LLM:只追加 + 纯代码中段截断(冷态单条截断字数),条目标记compressed表示永不参与 LLM 压缩;热态压缩遵守三条规则——同一批只尝试一次(含失败)、只压未尝试过的尾部条目、结果必须比原文更短才采纳。buildContextMessages按「冷态窗口字数」做渲染窗口折叠(窗口 60% + 纯代码折叠摘要 40%,零 LLM),档位由pickTier()依最新消息文本判定(L0 默认/L1 含指代/L2 明确要求回顾不折叠)。 - 会话保存节流(
Session.save(coalesce)):高频入库路径(待机消息/事件)传coalesce=true,5 秒或 20 条最多落盘一次并安排兜底写入;回复、命令、归档等关键路径保持立即写。海豹 storage 是叠加式存储,写越少越好。 - 事件提示词条目支持
eventType/raw字段(addSystemUserMessage第三参传入):eventType供事件过滤,raw为事件原始数据、不参与渲染、仅由 read_raw kind=event 读取,随所在消息清理/归档失效,不设条数与长度上限。 - 事件去重"后到覆盖先到":
addSystemUserMessageTracked()返回条目定位引用(SystemEntryRef),去重窗口内重复到达时replaceSystemUserMessage()按引用就地替换旧副本(文本/raw/systemName 取最后到达,不新增条目、位置不变);原位置已被合并压缩/归档改写时返回 false,由调用方追加新副本兜底。 - 工具回调消息支持
rawText(addToolCallbackMessage在超长截断时保留截断前完整原文);用户消息条目在压缩替换时保留rawText(单条)/rawItems(连续多条合并压缩块);均不参与渲染与 token 估算,随所在消息清理/归档失效;由grep_raw/read_raw按 kind 只读检索。 getUserById/getGroupById:仅按用户ID或群ID获取目标,不执行名称搜索。findImage:支持user_avatar、group_avatar、图片 ID、本地图片路径、记忆内图片。setName/updateName:按autoNameMod(0/1/2)自动把名字改为昵称或群名片。
联网(src/web/)¶
types.ts:联网配置与 provider / routes / defaults 的类型定义;schema.ts:v1 JSON 解析与结构校验(错误带$.providers[0].baseURL式路径;校验失败整体拒绝、只记日志、本次不生效,不回退默认);defaults是运行时字段,由configs/web.ts的 5 个独立配置项填充,JSON 里的旧defaults字段忽略。presets.ts:10 家 provider 方言(DeepSeek / SearXNG / Kimi / Brave / Tavily / Serper / 博查 / 智谱 / Exa / Firecrawl)+ generic 的请求构造与响应解析、搜索/抓取路由、内置默认 SearXNG(https://searxng.fishwhite.top);client.ts:withTimeout发送与错误分类(400/422 参数错误不换家,其余可回退);service.ts:runSearch/runFetch回退编排、正文截断、结果渲染(renderSearchResult/renderFetchResult支持start_index续读);template.ts:结果给模型的固定引导文案与模板渲染。- 对外工具在
src/tool/tools/web/(分类「网页」):web_search(多 provider 搜索 + 编号摘要)、web_fetch(正文 Markdown,max_chars/start_index续读)、forum_*(论坛工具,走「后端」页论坛地址/Token)。
记忆(src/memory/)¶
session_memory.ts:SessionMemoryService会话记忆门面:只包装 Hindsight-like 新引擎(不再使用旧 MemoryService/MemoryItem),提供retainMemory/recallMemory/deleteMemory/clearMemory/buildObservationPrompt等,保留validKeysMap与兼容方法以平滑读旧存档。revision.ts:记忆/观察缓存失效:bumpMemoryRevision()/bumpSummaryRevision()自增全局版本号并主动失效对应 prompt 缓存(一次总结可能写入多个会话,统一全量失效)。session_target.ts:resolveTargetSession()按「当前会话所属 agent」的 sessionService 定位私聊/群聊目标会话,避免写入与检索落到不同 agent 的会话空间。knowledge_chunk.ts:splitMarkdownIntoChunks()按#/##/###标题与段落把 Markdown 文档切块(单块约 800 字符、块间保留重叠),生成稳定 ID(kb_xxxxxxxx)。knowledge_base.ts:KnowledgeBaseService单例,配置驱动(JSON 里的 Markdown 正文),解析时不出向量(懒向量);search()先关键词过滤,嵌入可用时仅对命中候选(≤20 块)做惰性向量重排,失败自动降级关键词;buildKnowledgePrompt()只展示条目索引:ID + 标题,1500 字符预算内尽可能多列(≤100 条),不注入正文,详情由模型用knowledge_read按 ID 读取;formatIndex(limit)支持索引条数上限。knowledge.ts:兼容导出层,knowledgeService = knowledgeBase(旧KnowledgeService已移除)。manager.ts:MemoryManager统一入口:buildLongTermPrompt(心智模型 + 长期记忆混合注入,群聊按「群聊 / 最近发言者」分组)、buildObservationPrompt/buildKnowledgePrompt、retainMemory/recallMemory/consolidateMemory/reflectMemory、summarizeChunk/summarizeChunkWithRetry(上下文 token 超限归档:把将删除的消息块交给summarize_agent抽取 facts → 写入长期记忆 → consolidate 为观察记忆,最多重试 3 次)、migrateLegacyPersona(旧 persona 懒迁移为心智模型并清空)。v2/(4.17.0 起):Hindsight-like 纯 TS 记忆引擎。engine.ts实现 Retain(抽取原子事实/观察记忆,可选 LLM 结构化抽取 occurred_at/entities)、Recall(RRF 混合检索 + Hindsight 式新近度加权(按 lastAccessedAt 指数衰减,权重/半衰期可配)+ 可选 LLM 重排)、Consolidation(LLM 合成观察记忆、合并重复、清理 90 天过期观察)、MentalModel(心智模型:question/answer/scopeTags/status/lastRefreshedAt/history/trigger,支持 refresh/delete、persona 懒迁移)、固定模板(templates.ts按场景写死「设定/偏好/规则」问题,ensureDefaultMentalModels按seededMentalModelVersion版本化播种:升级只增量补新、删除不复活;固定模板优先注入、语义召回补余)、遗忘机制(长期记忆条数上限,默认 100,0=不限制;retain/consolidate 后超限时按「已被观察覆盖 → 普通按衰减分 → observation 沉淀 → 保护项(importance≥0.9 或 pinned)」优先级物理删除多余记忆,上限为硬约束);repository.ts统一存取长期/观察记忆与心智模型,bank_resolver.ts按会话解析记忆库(私聊user_<id>/ 群聊group_<gid>),migrate.ts负责旧格式(memoryMap/summaries/persona)自动迁移,llm.ts提供可选的 LLM 抽取/重排/合成/推理回调,prompt.ts负责注入裁剪(scopeTags 任一命中过滤、心智模型只注入 ready、观察按 lastVerifiedAt 剔除 90 天过期)与分组渲染(buildGroupedMemoryPrompt:每组段头标注会话身份与证据范围——群聊/个人 + 群号/ID(含名称),reflect 提示词开头注入会话范围行)。manager.ts buildLongTermPrompt在群聊时除群记忆库外,还会并入最近发言者(最多 3 人)的个人记忆库并分组渲染:群回忆 ≤20、群心智模型 ≤3(固定优先),每个发言者个人回忆 ≤20 条且单用户 2048 字符预算、个人心智模型 ≤2(固定模板 ≤1),观察记忆由buildObservationPrompt独立注入(≤20 条、90 天过期剔除),私有记忆仅创建者本人可见;查询向量每轮只算一次,复用于群聊与各发言者个人库。配套记忆工具为memory_*系列(增删改查/巩固/推理/心智模型,含memory_mm_list/view查看心智模型、memory_mm_delete可按 model_id 或 question 删除),.ai memo子命令重构为 status/add/list/update/delete/clear/obs/consolidate/reflect/mm,范围参数改用--u/--g。
智能体(src/agent/)¶
agent.ts:Agent字段:name、description、instruction(可为函数)、use、sessionService、tools、subAgents。Agent.get(name)/Agent.save(agent):从agent_<name>存储复活/保存。chat(prompt):直接发起一次单轮对话(压缩/摘要智能体使用)。run():完整编排循环——组装消息 → 请求 → 工具执行(函数调用或提示词工程)→ 复读检测重试 → 最终回复。runStream():流式编排,轮询后端流,检测 ```function 代码块截断,执行工具后递归续流。- 模型报错自动处理(
ErrorConfig开关):主链路捕获分类ApiError后——上下文超长按 min(本地预算, 报文窗口×0.8) 观察归档删除并自动重发一次;余额不足等触发类别自动把 chat 用途切到备用模型(按跨厂商优先/配置顺序选,写入全局覆盖并持久化,可选ctx.notice通知;单 run 各至多一次);其余未覆盖错误仅记日志不回复。 .ai stop中止机制:run/runStream 启动时捕获session.stopVersion并通过代际检查;stop 时fireStopEvent置位会话级stopEvent(StopEvent)并同步唤醒所有withTimeout/requestModel等待者(以 StopError reject),同时归零activeRuns/running、清空挂起队列与待触发计时器;StopError 不算工具失败回填,消息组装/请求返回/工具执行后/最终回复前均设检查点,检测到版本变化或 StopEvent 即中止;goja 无 AbortController,底层 fetch 无法硬中断,但返回结果会被丢弃。并发队列中该会话的等待项由requestLimiter.cancelBySession丢弃。- 挂起队列机制:会话运行(或正在启动)时收到的新消息经
session.deferReceipt进入pendingQueue挂起,run/runStream每轮模型请求前flushPending统一入库(此时上一轮工具回调已写入上下文,插入位置合法);记录类只入库不续跑,触发类在链结束后由resumePending用第一条触发消息串行再起一轮;.ai stop清空挂起队列并自增stopVersion,停止后不复活。 run_context.ts:AgentRunContext记录单次 run 的观测信息(runId/轮次/工具调用事件/耗时)。api.ts:对外 API,启动时挂载globalThis.aiplugin4(智能体调用chat/run/getAgent与工具注册registerTool),供其他海豹插件使用。agents/:包含compress_agent(压缩)、summarize_agent(摘要/上下文归档总结);samples.ts的sample_agent仅作参考,不在initAgents中初始化。subagent/(4.22 起,DSH 对齐子代理框架):把独立任务委派给 headless 子代理——child.ts子会话编排(spawn 无 seed / fork 继承父上下文已完成轮次;原生模式只向子代理注入 list_tools/search_tools/call_tool;单次激活默认 8 轮、总预算 120s),child_system.ts子代理 system 组装(persona/默认指令 + 工具引导,不含父记忆/知识库/委派指引),providers.tsspawn/fork executor,service.ts服务层(启动/前台与可续跑/发送消息/打断/结算通知/清理),jobs.ts后台 job(run_in_background,idsubagent-job-N,结果由job_output收取,完成不投 notice),store.tsKV 持久化(subagent_index:<父>/subagent_seq:<父>/subagent:roots/session_sub:<父>:<child>,每父会话保留 100 条),wake.ts父会话空闲唤醒决策,rules.tsfork seed 计算与错误文案,limits.ts委派深度限制与 system prompt 委派指引文本,wiring.ts运行时接线(ext KV、notice 入父noticeQueue(agent run 每轮 flushNotices 呈现)、重载后 running 对账为「已中断·可续」)。配套工具在src/tool/tools/subagent/init.ts(subagent/subagent_fork/send_message/interrupt_agent/list_agents/job_list/job_output/job_kill),命令.ai subagent,配置分组「子代理」。
媒体生成(src/media/ + src/model/media*.ts,4.24.0 起内置)¶
语音合成与文生图不再依赖外部插件,在「模型」页模型配置里给模型声明 kind: "tts" / "gen" 即进入 tts / image-generation 用途候选。请求结构默认由内置供应商预设提供(src/config/static_config/media_presets.ts,迁移自旧依赖插件的 MODELS.md):骰主只填连接就能生成,该模型的 request 只在接口与预设不一致时才写覆盖。
src/media/task_queue.ts:MediaTask(id/sid/isPrivate/epId/kind/origin/status/createdAt/updatedAt/request/result/error/notified/requester/save/name,含validKeys供revive恢复)与MediaTaskManager静态管理器。- 提交与执行:
submit()建任务入队、落盘并立即返回(工具/命令随即把任务号回给 AI 或用户);pump()在「媒体任务并发上限」(默认 3)内启动 pending 任务,run()调entry.callTTS/entry.callTTI执行。 - 结算分流(
origin):command(人类敲.ai tts/.ai tti)由插件直接replyToSender投递;tool(AI 调工具)先把结果写入会话noticeQueue,再尽力触发一轮对话投递(session.chat(ctx, msg, '生成任务完成', undefined, { skipBucket: true }))。- ⚠️ 硬约束(不得违反):工具来源的媒体结果"要么已呈现给 AI、要么仍在
noticeQueue待呈现",绝不允许两头落空。 这条约束的存在原因是一次真实线上故障:实现曾写成果完成后无条件session.chat(),而Agent.run()/runStream()开头有一道同会话闸门 媒体唤醒恰好是唯一绕开 pipeline 分流、直接调chat()的调用方(普通消息在 pipeline 里已由deferReceipt挂起,不会撞到它)。于是生成极快时: 完成通知入队 → 直接chat()→ 撞闸门被静默丢弃(Promise 正常 resolve,.catch不触发,日志还写着"触发成功") → 若本轮已过最后一次模型请求,flushNotices不会再消费它 → 永久等待。 更糟的是入队时就置了notified = true(谎报成功),把"重载补发(done && !notified)"这条兜底也一并堵死。 改动媒体送达链路时必须保持下列三段式,缺一不可: - 入队:单独一步、置于所有可能抛错的调用之前;通知带
taskId作为"是否已被消费"的身份标识; 入队时不得置notified——notified的唯一语义是"已呈现给 AI",只能由flushNotices真正消费时经MediaTaskManager.markNotifiedByNotice()回置。 - 判定:走
decideWake同一份口径;会话忙(running || starting)则 defer(通知留在队列,不丢), 不得假定"忙 → 本轮 flushNotices 一定会注入"(本轮可能已过最后一次模型请求)。 注意:媒体结果不受待机拦截(待机约束的是"主动搭话",这是用户请求的送达),与子代理的唤醒口径不同。 - 轮末补触发:
Agent.run()/runStream()的finally中,resumePending()之后调用MediaTaskManager.wakePendingNotices(session)—— 由"会话真的结束了"这一确定事件驱动,不靠猜时机、不会永久等待。 判定条件:activeRuns === 0 && !running && !starting、未stopEvent.fired、且队列里仍有带taskId的媒体通知; 起一轮后该轮结束时再检查,届时通知已被消费 → 自动停止,不会死循环。 (历史教训:曾有实现改用"忙则定时重试 N 次"(wakeRetryMax/wakeRetryDelayMs),后被整体移除;计时器方案要猜"还要忙多久", 猜短白跑、猜长延迟送达,次数用尽仍可能落空 —— 故不采用。) - 入队与唤醒分离:入队单独一步且置于所有可能抛错的调用之前,唤醒侧失败(如端点离线导致
getSessionCtxAndMsg抛错)只影响"是否立刻起一轮",通知不会丢;每一步的跳过/失败都留含任务号与原因的日志(原实现把两步包在同一个 try 里,异常被一行 WARN 吞掉,表现为"任务完成什么都没发生")。 - 免桶送达:调用
session.chat(...)时带{ skipBucket: true }—— 跳过令牌桶检查且不扣减。注意与旧有reason === '函数回调触发'的区别:那条豁免只跳过检查、仍扣减令牌,行为未改动。
- ⚠️ 硬约束(不得违反):工具来源的媒体结果"要么已呈现给 AI、要么仍在
- 提交后同步等待窗口:工具调用(
text_to_image/generate_audio)在submit()后用MediaTaskManager.waitForSettle(task, ms)轮询等待,窗口由「工具 → 媒体生成同步等待秒数」控制(默认 5 秒,0=关闭)。 窗口内已结算(done/failed)→ 工具当场返回可执行结果(图片直接给[img:ID],语音给 record 段指引), 这批结果根本不进唤醒链路;未结算则返回任务号走异步。 价值:生成过快时(本地模型/缓存命中/极短文本)省掉一轮唤醒与一次模型调用,也天然避开临界点。 - 结果落地:音频
seal.base64ToImage存本地文件、图片走Image(imageId);save意图在完成时用getSessionCtxAndMsg(epId,sid,isPrivate)重建 ctx 后写记忆。队列与通知只存句柄/短摘要,绝不落 base64;参考图 base64 只存运行期内存(runtimeMap)。 - 恢复与清理:每会话最多保留 50 条已结束任务(未结束永不淘汰);启动读盘时
running→failed('插件重载中断'),done && !notified且来源为工具则补通知;cancel(sid,id)只对 pending/running 生效。 src/tool/tools/media/:媒体工具。shared.ts提供buildMediaToolInfo(把description与parameters做成惰性 getter,读取时按当前 per-model 规则生成逐模型参数清单与 schema,规避"注册时机早于模型列表就绪"的脆弱性)与resolveMediaEntry/buildMediaSubmitReply;tool_media_tasks.ts注册media_tasks(list/cancel,归「多媒体」分类)。- 工具与命令:
generate_audio与text_to_image改为提交任务并立即返回(generate_audio传path时仍同步解析本地/MCP 音频资源,不进队列);新增.ai tts/.ai tti/.ai media(src/cmd/sub_cmd/{tts,tti,media,media_shared}.ts),三者的priv约定见 05-命令与配置。 - 注意:
.ai tts/.ai tti的priv不能带args——PrivilegeManager.checkPriv会把cmdArgs.args逐段拼进权限链路,带args时正文会被当成子指令而误报"命令不存在";正文解析从cmdArgs.args第 2 位起按RE_KEYWORD剔除--xxx后拼接(不用getRestArgsFrom,它在真实 SealDice 下可能把--rate=1.5并进正文)。
命令系统(src/cmd/)¶
root_cmd.ts:注册根命令.ai(注册到cmdMap['ai']与cmdMap['AI']),别名经aliasToCmd归一;子命令统一分发,兜住异步异常并回复错误;支持--page/--p翻页。privilege.ts:命令权限系统,CmdPrivInfo { priv: [会话权限, 用户权限, 强触权限] },预置U(user)/M(master)/I(inviter)/S(会话需 inviter,否则骰主);PrivilegeManager管理cmdPriv存储与校验,支持*通配。sub_cmd/:30 个文件,其中 28 个注册到根命令(balance/kb/mcp/model/pub/skill/subagent/tts/tti/media 等),sample.ts未注册(仅开发参考)、media_shared.ts为 tts/tti/media 共用的解析与模型定位辅助(不注册子命令),详见 05-命令与配置。
定时器(src/timer.ts)¶
TimerInfo:sid/isPrivate/epId/set/target/interval/count/type/content;type 分target(目标时间)、interval(间隔)、activeTime(活跃时间)。TimerManager:队列持久化到timerQueue;executeTask()每 5 秒扫一轮,每个定时器执行间隔 2 秒;超过目标时间 1 小时未执行则忽略;触发时注入系统用户消息并调用session.chat。
用量统计(src/usage.ts)¶
UsageManager:按模型按天(Y-M-D)累计 prompt/completion token,持久化到usageMap;过期数据归并(30 天前 →Y-M-0,11 个月前 →0-0-0)。get_chart_url():把用量数据 POST 给用量图表后端/chart,返回图片 URL。
日志(src/logger.ts)¶
- 输出格式:
【AI骰娘4】[级别短标签] [模块标签] 内容。级别短标签为 ERROR/WARN/INFO/DEBUG,与海豹面板日志级别/颜色对应,便于按级别过滤。 - 模块标签:通过
logger.withTag('模块名')得到带标签的日志对象,已覆盖 agent/model/pipeline/timer/send/ob11/context/image 等核心链路;全局logger仍可用。 - 级别:从不/错误/警告/信息/调试,由
日志级别配置控制。 - 单条截断 2000 字符;
日志简短打印时首尾各保留 500 字符;logLong()超长文本按片打印并标注[第x段/总段数]。 - 密钥脱敏:
api_key/token/authorization/secret/password/app_secret/appkey字段、URL query 中的同名参数、Bearer xxx、sk-前缀密钥、access/refresh token。 exception()错误进 error、堆栈进 debug;printRequestMessages()默认只打[请求上下文] run=... 共N条摘要,逐条 role/长度/toolCalls 与正文进 debug(正文受日志记录消息内容开关控制,截断 300 字符,默认不打印 system)。
通用工具(src/utils/)¶
| 文件 | 职责 |
|---|---|
string.ts |
消息段解析/转换、图片抽取、回复拆分、复读检测、特殊 token 解析、编辑距离/相似度、fmtDate、fixJsonString、活跃时间解析 |
message.ts |
system prompt 组装、预设上下文、上下文拼接与请求级 token 预算裁剪(整条优先、单条兜底截断,含模型校准系数)、最终请求消息、getRoleSetting |
ob11.ts |
OneBot 11 API 封装:收发消息、用户/群信息、禁言、精华消息、打卡、取/撤消息、合并转发、AI 语音 |
seal.ts |
构造 ctx/msg、会话 ID 工具 |
update.ts |
版本比较与启动更新日志 |
utils.ts |
消息 ID 进制转换、generateId、replyToSender、withTimeout、revive、别名、余弦相似度、resolveLocalPath |
web.ts |
fetchData 统一 POST 请求(错误信息提取、鉴权头/内容类型覆盖、按 content_type 编码请求体:JSON / multipart 自行拼串并回填 boundary / x-www-form-urlencoded);dataType 支持 json(默认)/binary(响应体即二进制,转 base64;按 content-type 自适应,JSON 响应仍走解析)/text 三种取数方式,供媒体生成读取音频流;binary 在配置了 backends.media-fetch 时直接由后端代发(只产生一次上游请求;后端 4xx 状态码/上游 bodyText 还原成 ApiError 供分类) |
图片资源(src/resource/image.ts)¶
Image:图片对象(url/local/base64 三种来源),提供CQCode、urlToBase64、imageToText;全局imageMap缓存与save。- 头像:
getUserAvatar/getGroupAvatar(经 ob11 获取头像 URL)。
其他¶
src/trigger.ts:触发模块占位(预留)。src/pub/(4.22 起):公开会话目录——registry.ts目录条目持久化(存储键pubSessions)与 list/add/按平台·Bot·会话过滤删除,snapshot.ts把目标会话context.messages渲染为只读快照文本;配套命令.ai pub list/add/rm(src/cmd/sub_cmd/pub.ts)与工具pub_read/pub_send(src/tool/tools/pub/)。src/changelog.ts:版本更新日志(changelog),启动时由checkUpdate输出。src/note.txt:作者开发备忘(TODO 与重构思路),不属于运行代码。