跳转至

09 - 注意事项与常见问题

README 与代码的差异(重要)

项目根目录的 README 面向商店用户,只保留安装/配置/命令/工具手册,开发文档见 docs/01-项目概览 与 docs/07-开发指南。README 会随发版同步,发布前按 07-开发指南「开发完成检查清单」核对;若发现 README 与本章或源码不一致,以源码为准,并同步 README。

  • 旧版 README"开发文档"中的目录结构(src/tools/、src/AI/)、ConfigManager、ToolManager、tool_choice 字段、cmdInfo 等均为旧版架构,已不存在;README 已移除该章节。
  • README 配置手册的模型配置为一份 JSON(v5),与代码一致;旧的「api连接」「模型规则」两个 TOML 框已废弃(4.25.0 起不再读取,也不做迁移)。发布流程为推送 v* 标签自动发版(见 07-开发指南「CI 与发布」)。

代码中的已知不一致

  • src/config/static_config/meta.ts 的 VERSION 与 src/changelog/<版本>.ts 的最新版本条目需保持一致(当前 4.28.0);checkUpdate 按 VERSION 比较输出对应更新日志。聚合索引 src/changelog.ts 由 npm run changelog:gen 生成。
  • 实际构建产物是 dist/aiplugin4.js(esbuild 单文件);package.json 的 main/typings/engines 字段与陈旧 build/ 目录均已清理(不参与构建)。
  • 实际构建/测试依赖较新 Node(CI 用 20);本地若无 dist/ 先 npm run build。
  • 4.7 已删除未注册的开发示例文件(sub_cmd/sample.ts、agent/agents/samples.ts 等);新增示例不要注册到运行时。
  • src/note.txt 是作者备忘(TODO 与重构思路),不是运行代码,其中的方案可能与现状不符。

运行与部署注意事项

  • SealDice v1.4.6 分离部署(napcat/llonebot)下图片相关功能存在问题,建议 v1.5.0+。
  • run_core_command 需要启动 ob11-core-bridge,并在「后端 → 后端配置」的 backends.ob11-core-bridge 填 /plugin WebSocket 地址(SealDice 正向连接 /core);run_ext_command 在插件内本地直调扩展 solve,无需中间件/MCP。核心指令白名单使用 core|指令名,但 core|ext 无需加入白名单即可查看扩展。两条路径都复用多消息收集器(空闲窗口 + 最大消息数 + 超时)。
  • ob11 相关工具(发消息、取用户/群信息、禁言、打卡等)需要安装 ob11 网络连接依赖或 HTTP 依赖;未安装时相关工具返回"未找到ob11网络连接依赖"。
  • ⚠️ 4.25.0 破坏性升级注意(模型配置 v4 → v5):「模型」页的「api连接」「模型规则」两个 TOML 框已废弃——两个配置项已注销、UI 上不再出现,插件不再读取它们,也不做自动迁移。原因是海豹的 seal.ext 没有配置写入 API,"读旧→转换→写新"的第三步永远做不到,兼容垫片只能退化成每次启动跑一遍、旧配置删不掉、用户也看不到转换结果。升级后请用配置生成器重新生成一份 JSON 粘进「模型配置」(或按 README「模型」章手写);不重建的话插件会载入出厂 DeepSeek 示例并打 WARN 提示,原有模型全部不可用。字段对照:api_key→providers[].apiKey、base_url→providers[].baseURL(必填、不再兜默认值)、provider→通常不用填(由 baseURL 的 host 自动识别)、models→providers[].models[]({id, name, kind})、[types]→models[].kind、ignore = 1→providers[].enabled = false、[request]→providers[].request(连接级)或 models[].request.request(模型级)、规则的 use 组→顶层 purposes + groups、规则的 [body]/[params]/[prompt]/[response] 与 models 定向→ models[].request.{body,params,prompt,response}
  • ⚠️ 4.27.0 升级注意(后端配置 JSON 化 / 论坛独立页签):「后端」页签的 8 个独立地址项合并为单框 JSON「后端配置」(v1),旧键不再注册、UI 隐藏、不再读取——不迁移、不兼容;升级后请启动 aiplugin4-backends WebUI,点「📋 复制后端配置」后整段粘贴。空的「后端配置」= 全部使用出厂默认地址,不报错;校验失败整份拒绝并回退默认。论坛地址 / 论坛API Token / 论坛签名密钥移到新的「论坛」页签(键名不变,值保留)。不支持降级,升级前请备份 config.json。同时修复 QQ 官方 bot 图片 URL 的 &amp; 未解码问题(插件侧在解析消息图片时还原),并补上 image-host 的 upstream_status: 4xx 永久失败识别;MCP image_search 返回的图床 id 可直接写 [img:ID] 发送(插件用图床稳定地址,不落 storage)。图床对外地址已写进配置(image-host.publicUrl):url 只给插件访问图床接口(可为 127.0.0.1),publicUrl 给 QQ 协议端与模型(填 IP 或域名);启动器「复制后端配置」的「图床对外地址」会自动生成它(留空自动探测本机 IP),不用复制后手改。没有 publicUrl 且 url 是回环时,插件降级为「发图落本地文件、模型用 base64」并在启动探测时 WARN。
  • ⚠️ 4.26.1 升级注意(角色/技能/MCP/知识库配置 JSON 化): 四项配置从多框模板改为单框 JSON,使用新键名「角色设定」「技能列表」「MCP服务器」「知识库配置」;旧键「角色扮演设定」「技能配置」「MCP服务器配置」「知识库」不再注册、UI 隐藏、插件不再读取,不做自动迁移(海豹没有配置写入 API,原因同模型 v5)。升级后请用配置生成器对应页签重新生成并粘贴;旧值仍留在 config.json 里但不会生效。不支持降级(旧版本读不了新的 String 配置),升级前请备份 config.json。MCP 同时改成标准 mcpServers JSON,且插件默认配置为空(不预置服务器),保留「是否启用MCP」总开关(默认关闭);技能的标准 SKILL.md frontmatter 只在生成器文件导入时使用,插件运行时只读 JSON。
  • ⚠️ 4.25.0 另一处行为变更(视觉不再按命名猜测):模型类型里只有视觉不再自动推断——gpt-4o、gemini、qwen-vl、*vision 这类名字不会被认成视觉模型,一律归 text。原因是一批正则最容易过期(新模型名永远追不上),且猜错会让用户误以为模型已具备视觉能力。要识图必须在「模型配置」里显式写 "kind": "vision"(配置生成器会自动预填);接口自报的能力位(vision)仍作正向证据。嵌入/生图/语音/reranker 仍按命名白名单推断。
  • 4.24.0 升级注意(语音/生图依赖移除):生成音频(tts)与生成图片(tti)依赖插件已废弃,完整包不再附带(只留 ob11 网络连接)。请卸载旧的 tts/tti 依赖插件——插件不再读取 globalThis.tts / globalThis.tti / globalThis.aiDrawing。语音合成与文生图改由「模型」页模型配置声明(给模型写 kind: "tts" / "gen" 即进入 tts / image-generation 用途候选):请求结构由插件内置的供应商预设提供(逐条迁移自旧依赖插件的 MODELS.md,覆盖 OpenAI/硅基流动/智谱/百炼/Google Cloud/Gemini/MiniMax/FishAudio/ElevenLabs/火山方舟/腾讯 TokenHub/Stability/百度/本地自建等),所以一般什么都不用写——只在接口与预设不一致时才写该模型的 request.body(默认逐键合并,bodyMode: "replace" 改回整体替换)或 request.request/request.response(逐键覆盖);命令入口由旧的 .tts / .tti 改为 .ai tts / .ai tti,并有 .ai media 查看/取消任务队列。注意 baseURL 推荐填根地址(如 https://api.siliconflow.cn/v1),完整接口地址也能用(已以预设路径结尾时不再重复拼接)。
  • 「ai语音使用的音色」下拉的归属(易误解):该下拉服务的是协议端自带的 AI 语音(NapCat/Lagrange 的 AI 语音 API,角色名如小新/猴哥/妲己),与插件 TTS 无关;这条链路本就无需配置模型,现在 AI 直接用 call_ob11_api 调 get_ai_characters(查角色,取 character_id)/send_group_ai_record、send_private_ai_record(发送)/get_ai_record(只取语音 URL),用法与常见角色名见默认知识库 ob11-api。下拉里的「自定义」选项对应的才是插件 TTS 链路(当年需额外的 tts 依赖 + ffmpeg),现已内置:内置预设里已经带 {voice} 占位符,改该模型 request.params.voice 的 default 或按次传 --voice(命令)/voice(工具参数)即可。两者互不影响,协议端不支持这些 action 时改走 generate_audio。
  • 中间件捕获消息默认转发到协议端;若希望 AI 内部执行且不打扰真实 QQ,需在工具参数中设置 capture.forward=false。海豹核心连接 /core,协议端由中间件出站主动连接(WebUI「⚙ 配置」填地址,重启生效),插件通过 /plugin WebSocket 调用。纯 OB11 没有 trace_id,同一群并发动作可能返回 ambiguous=true;中间件已按群/私聊 lane 串行化,并以空闲窗口、最大消息数和超时结束收集。
  • 简单配置(开关/数值/单行字符串/纯字符串数组/「后端配置」JSON)修改后自动生效(缓存最多 1 分钟,无需重载 JS);复杂配置(模型「模型配置」、触发/忽略正则、评分触发、「角色设定」、「MCP服务器」、MCP 总开关、「知识库配置」、本地资源路径、音乐服务)启动时解析一次、常驻内存,修改后需重载 JS 才生效(其中「MCP服务器」「知识库配置」两类也可用 .ai mcp refresh / .ai kb refresh 立即生效);「技能列表」修改后下一条消息自动生效(也可用 .ai skill refresh 强制重解析)。
  • 嵌入输出维度取自该模型 request.body 的 dimensions(默认 1024,须与后端一致,如 text-embedding-v4 为 1024);未配置维度时记忆检索自动降级为关键词/分数检索、知识库检索自动降级为关键词匹配(嵌入仅作候选重排,加载知识库本身不请求嵌入)。
  • 流式输出需要自建或使用公共后端,并在「后端 → 后端配置」的 backends.stream-output 配置 URL;在 chat 用途组 groups[].body 里设 stream = true 的模型才会走流式(anthropic-messages 协议不支持,会自动回退非流式)。
  • 模型类型判定顺序:models[].kind 显式声明 → 命名终值(reranker/生图/语音/嵌入白名单) → 接口自报能力位(vision/embed) → 兜底纯文本。⚠️ 视觉不在自动推断之列:网关自命名模型(如 my-embed-1)与视觉模型(如 inhouse-vl、gpt-4o、gemini)都要在「模型配置」里显式写 kind(取 text/vision/embed/gen/tts),否则会被当纯文本模型、甚至被选为默认对话模型。非 OpenAI 兼容的语音/生图服务(如 Google Cloud TTS)没有 /models 列表,需把模型写死在 models 里声明 kind 才能接入。
  • 请求超时时限 同时约束模型请求与工具调用,过小会导致长回复/慢工具超时。语音/图片生成不受此限:它们是异步任务,提交后立即返回,HTTP 超时由该模型 request.request.timeout(或连接级 request.timeout)控制,因此慢出图(SD 等)不会被工具层 180s 截断。
  • 4.25.0:AI 调 generate_audio / text_to_image 时先等一段同步窗口(「工具 → 媒体生成同步等待秒数」,默认 5 秒,0=关闭)——窗口内出结果就把结果当场返回给 AI(图片给 [img:图片ID]、语音给 record 段发送指引),不再走异步通知;超时才返回任务号,生成完由通知链路唤醒投递。命令 .ai tts / .ai tti 不走该窗口(始终由插件直接投递)。若把该值调大,AI 的这轮响应会相应变慢,但省掉一次唤醒与模型调用;调成 0 则一律走异步。
  • 4.25.0 媒体送达的三段式(改动该链路时必须保持):工具来源的媒体结果必须满足"要么已呈现给 AI、要么仍在 noticeQueue 待呈现",不得两头落空。三段缺一不可:① 入队(带 taskId,notified 只在真正被消费时回置);② 会话忙则 defer,不假定"本轮 flushNotices 一定会注入";③ Agent.run()/runStream() 的 finally 里 resumePending() 之后调用 MediaTaskManager.wakePendingNotices() 补触发。详见 03-核心模块详解 的硬约束说明。
  • 4.26.0 上下文成本治理的配置与行为变化:待机群(会话待机或全局待机)的普通消息入库改为冷态零 LLM(只追加 + 纯代码截断,不再每条消息压一次);归档 / 窗口移出改为只写一次的分片(read_raw kind=user id=arc:序号 / id=raw:序号 回查),.ai forget --all 可清空;记忆结算改为按「结算水位 + 空闲 / 转冷」触发。新增 .ai ctx / .ai ctxset / .ai tk use,以及「上下文」页的冷态窗口字数、冷态单条截断字数、归档保留原文、窗口上限字数、结算水位 / 最小门槛 / 空闲门槛 / 单次结算调用上限、原文分片保留天数 / 上限、待机入库配额等配置。注意:「单次结算调用上限」限制的是分片数,不是 LLM 调用数;冷态阈值改为连续 10 分钟无相关活动才转冷。
  • 4.26.0 联网搜索:新增「联网 → 联网配置」(v1 JSON,可接 10 家 provider + generic,留空 = 内置 SearXNG,开箱可用);旧「后端 → 联网搜索」配置项已删除、不再读取;web_search 参数与新增的 web_fetch(正文 Markdown、start_index 续读)见 README「联网」章。坏 JSON / 校验失败只记日志、本次不生效(不抛异常、也不回退默认);修改后需重载 JS 生效。

常见问题排查

不回复 / 触发不生效

按以下顺序检查:

  1. 日志级别设为"调试",观察触发日志(trigger reply: ...)与请求日志。
  2. 检查"触发正则表达式"与"触发需要满足的条件"(豹语表达式)是否命中;触发条件命中为 1 才触发。
  3. 检查"触发次数上限/补充间隔"(令牌桶),桶为空会跳过回复(bucket empty, skip reply)。
  4. 检查消息是否被"忽略正则"或"忽略消息豹语条件"拦下(日志有 忽略 字样)。
  5. 检查消息是否含白名单之外的 CQ 码类型(CQ_TYPES_ALLOW 之外的 type 不处理)。
  6. 确认模型配置可访问:url/API Key/模型名正确、余额充足、支持工具调用。

回复中的字面量 \n 没有换行

AI 回复中的字面量 \n 或 \r\n 会在发送前转换为真实换行;如果需要发送多条独立消息,使用 \f 作为分隔符。代码块或示例中的 \n 也按此规则转换。

工具调用失败

  • 查看日志中的 调用函数失败: 原因:未注册/未经许可/参数缺失/类型不符/会话类型不符/超时等。
  • 用 .ai tool 查看开关状态,.ai tool help <函数名> 查看参数,.ai tool call <函数名> --参数=值 手动试用。
  • 工具在"禁止调用的函数"列表中时无法开启;新会话中"默认关闭的函数"需要 .ai tool on <函数名> 手动开启。

记忆/知识库检索不到

  • 记忆检索:确认已有嵌入模型可用(「模型配置」里有 kind: "embed" 的模型,且 .ai model text-embedding 已绑定或存在可用默认),该模型 request.body.dimensions 与后端输出一致(默认 1024),且"启用长期记忆"开关打开;检索有内置相似度下限(0.8)过滤,条目太旧(衰减)或相似度过低不会展示。模型名无法被自动识别为嵌入时(网关自命名),在「模型配置」里显式写 "kind": "embed"。
  • 知识库:"启用知识库记忆"开关打开后,内容来自「知识库配置」配置(Markdown 正文,# 条目标题、##/### 小节,超长自动分块);不按角色加载,修改配置后需 .ai kb refresh 或重载 JS 生效,可让 AI 通过 knowledge_search / knowledge_read 工具检索验证;嵌入未配置时自动降级为关键词匹配,不影响基本检索。

语音/图片生成不生效

按以下顺序检查(4.24.0 起为内置能力,与对话模型同一套连接):

  1. .ai model tts / .ai model image-generation 查看该用途候选与当前模型:为空说明「模型配置」里没有 kind 为 tts / gen 的模型(命名白名单未命中时显式写 kind)。
  2. 先看 vendor(由 baseURL 的 host 自动识别)是否在内置预设覆盖范围内(README「媒体用途」表):在范围内就不用写任何请求覆盖,只填连接 + kind 即可;不在范围内(例如文心一格生图、自建服务)才需要在 request.body 里自行描述结构。预设里没有的 vendor 一律按 OpenAI 兼容处理。
  3. 百度短文本语音合成已内置(baseURL 为 tsn.baidu.com 时识别为 baidu):它是 access_token 鉴权,而连接只有 apiKey 一个密钥字段,故约定 apiKey 填 "APIKey:SecretKey"(冒号分隔,密钥含冒号用 \: 转义),插件按连接换取并缓存 access_token(30 天、带 5 分钟提前量、同一连接不重复换);直接填 access_token 也支持。该服务无 /models 列表,models 里要写死一个模型并声明 kind: "tts"。
  4. 百度返回失败时仍是 HTTP 200,靠 Content-Type(成功以 audio 开头、失败为 application/json)区分,业务错误在 err_msg(err_no 取值不固定,故只取 err_msg),预设已配 error_message_path = "err_msg",故失败会显示百度的原始原因而不是"未找到音频数据"。
  5. 语音报"当前运行时无法读取二进制响应体":该接口是「整个响应体即音频流」(OpenAI 兼容 /audio/speech、百度、阿里 CosyVoice 等),而海豹的 JS 引擎(goja)的 fetch 只有 text()/json(),没有 arrayBuffer(),且 text() 会按 UTF-8 解码把音频字节替换成 U+FFFD 永久损坏——所以必须由后端代读。启动 aiplugin4-backends 的 media-fetch 后端(默认端口 46679),并在「后端 → 后端配置」的 backends.media-fetch 确认地址即可。配置了 media-fetch 时插件直接走后端、不再先直连一次(同一上游只被打一次);上游 4xx 非 JSON 的错误原文由后端 bodyText 带回(需 media-fetch ≥ 1.0.1)。返回 JSON 的接口不受影响(MiniMax 走 data_path=hex、Google Cloud 走 audioContent、绝大多数生图接口),不需要该后端。
  6. 检查 request.body 的占位符是否写全:{text}/{prompt} 是正文,{model} 是模型名;request.response.data_path 指错会报"响应中未找到音频/图片数据",整响应即音频时留空 data_path 并设 data_type = "base64"。
  7. 用 .ai media 看队列状态与失败原因;.ai tts 测试 / .ai tti 测试 可绕过 AI 直接验证。
  8. 参数不生效时检查该参数是否真的出现在最终模板(预设或你的 request.body)的 {占位符} 里——只有模板里出现的键才接受覆写,其余一律忽略(这是刻意的:让模型不支持的参数不会污染请求)。
  9. 地址不对时优先不用改请求覆盖:baseURL 填根地址即可(如 https://api.siliconflow.cn/v1);确实要换地址再写 request.request.url(绝对地址)或 request.request.urlSuffix(urlSuffix = "" 表示 baseURL 原样即接口地址)。

MCP 服务器 image-host 注册失败 unauthorized

image-host 的 /mcp 需要访问 token;日志里的「服务器未初始化:unauthorized」实际是 401 鉴权失败(不是没启动)。处理:

  1. 启动器 WebUI → image-host 卡片「⚙ 配置」→ 设置 Token(与插件的图床 token 一致),保存后重启 image-host(token 是启动时注入的);
  2. 重新点「📋 复制 MCP 配置」,确认 image-host 条目带 headers.Authorization: Bearer …,再粘到「MCP → MCP服务器」保存;
  3. .ai mcp refresh(或重载 JS);
  4. 不需要 image-host 的 MCP 工具(搜索/重命名/打标签/收图)时,直接从 MCP 配置里删掉 image-host 条目即可——「后端配置」里的图床不受影响。

后端 401 / token 不一致

任意后端都可以在启动器卡片「⚙ 配置」里设访问 token(AIPLUGIN4_BACKEND_TOKEN)。设置后:

  1. 重启该后端(token 是启动时注入的);
  2. 重新点「📋 复制后端配置」,确认 backends.<后端名>.token 与卡片里的 token 一致,整段粘贴到「后端配置」;
  3. 等约 60 秒(或重载 JS)后重试。

日志/回复里的 401 会提示去哪个 backends.<名>.token 核对;流式输出、图片转base64、媒体中转、用量图表、图床、核心桥都适用。复制的 JSON 含明文 token,不要公开分享。

token 模式下「.ai token chart」裂图/失败

图表图片由 QQ 协议端拉取,带不上 Authorization。插件改为让后端直接返回 base64(seal.base64ToImage 落本地图)。要求 usage-chart ≥ 1.0.1 且插件 ≥ 4.27.0;旧版后端在 token 模式下会返回 401 的临时图 URL(表现为裂图)。升级后端后重新「复制后端配置」即可。

AI 调工具生成的语音/图片"没动静"(没有自动发出来)

AI 侧(generate_audio / text_to_image)完成后的行为是确定的:① 把完成通知写入会话队列(这步先做且不会因后续失败而丢);② 直接触发一轮对话投递(不判断忙/待机/全局待机——这是用户请求的结果送达,不是主动搭话)。命令侧(.ai tts / .ai tti)由插件直接投递,不经过 AI。日志按模块标签 [media] 过滤即可看到每一步:

  • 媒体任务 #N 完成,触发会话 <sid> 起一轮对话 → 已起新对话,AI 该在下一轮发出;
  • 媒体任务 #N 直接投递失败:... → 协议端离线/端点异常,结果仍在任务队列(可用 .ai media 看),下一轮对话会呈现;
  • 媒体任务 #N 通知/触发对话失败(结果仍在任务队列,可用 media_tasks 查看):... → 同上,run 侧出错也不丢结果。
  • 该触发免触发令牌桶(桶空也送达),所以"触发次数上限"导致的不发言已排除;若仍不发,查 chat 是否被 .ai stop 中止或模型侧报错。
  • 若 AI 收到了通知却发不出去:[audio:xxx] 只认「资源」页配置的本地语音资源名,生成结果的路径/URL 要用 call_ob11_api 的 record 段;图片用 [img:图片ID],且该图片必须已注册进 Image.imageMap(任务落地时会自动 Image.save)。

图片识别异常

  • 确认图片 URL 可在浏览器访问(过期或 QQ 图床 bug 时更换协议端版本)。
  • 模型不支持 QQ 图床时,把"识别图片时将url转换为base64"设为"总是"或"自动"。
  • 图片转文字依赖视觉模型:先在「模型配置」里给视觉模型显式写 "kind": "vision"(⚠️ 视觉不再按模型名自动识别,glm-4v/gemini/pixtral 这类名字也不会被自动认成视觉模型),再用 .ai model image-understanding <模型> 绑定。
  • 图片直链失效不会再卡死会话:QQ 直链的 rkey 过期(图床返回 download url has expired,后端与模型都拿不到图)时,插件会把这些图片降级为 [img:ID] 文本标签,不再产生 image_url,模型也就不会因"下载图片失败"整轮 400;日志里出现 图片xxx的URL已确认不可用 或 [model-error] 模型无法下载上下文图片 即为此场景,属于正常兜底。
  • 若历史上下文里已留下坏图片(如旧版本遗留)导致该会话一直不回复,执行 .ai forget 清上下文后即可恢复(新版会自动把这类图片降级,不再需要手工清理)。

HTTP 请求出错

  • 日志中的 请求失败! 状态码: xxx 对照 HTTP 错误码与大模型文档排查;常见原因:url 填错、API Key 错、模型名错、不支持工具调用、余额不足、请求频繁。

待机群相关(上下文成本治理)

背景:待机群里所有群聊消息会累积进同一条 user 消息块;旧版本每来一条消息都会重判"整块是否超过「消息压缩阈值」",超过就调一次压缩智能体——群里越热闹花钱越多,哪怕机器人一句话都没说。现在改为冷态入库零 LLM。

  • 为什么待机群不再"每条消息都压缩"? 待机(会话待机或全局待机)且无在途对话时,消息只追加 + 纯代码中段截断,compression 调用为 0(用 .ai ctx 的"本会话调用:压缩"确认)。压缩只发生在热态,且遵守三条规则:同一批只尝试一次(含失败)、只压尾部连续未压缩段、结果必须比原文更短才采纳。若你确实想要压缩,用 @ 让会话转热即可。

  • 怎么看花了多少钱? .ai ctx(权限 U)显示:当前冷/热状态、预算与冷态窗口、上下文条数与估算 token、归档分片数、本会话 compression/summarization/judge 调用次数、全局用途用量与缓存命中。全局分模型用量仍看 .ai tk。

  • 归档分片怎么清? 归档出来的原文写进 session_<会话ID>:arc:<三位序号> 分片(只写一次、永不重写)。.ai forget 只清上下文,不碰分片(分片是留档,供 read_raw 回查)。分片数量上限默认 200,超出时最旧的分片会被置空。按会话清理分片用 .ai forget --all(清空本会话 arc/raw 分片并重置结算水位,不影响已沉淀的长期记忆);分片 TTL 见「原文分片保留天数」。

  • 「归档保留原文」开关怎么用? 开(默认):归档时把原文写进分片(read_raw kind=user id=arc:序号 可回查)并调总结智能体沉淀记忆。关:归档不留原文、不写分片(省存储),但仍然会调总结智能体沉淀记忆——历史照样进长期记忆,只是之后无法回查原文。注意:关掉它不会省钱(总结调用照旧),只省存储。

  • 待机群"记不住"更早的内容怎么调? 冷态下模型默认只看到最近「冷态窗口字数」(默认 5000 字)的内容,更早内容折叠成纯代码摘要(零 LLM)。调大该值可让模型默认看到更多;设 0 = 不折叠(注意会直接抬高每轮 token)。消息里出现"它/这个/刚才/继续"这类指代词时会自动升档多装一段,"总结/回顾"这类明确要求回顾的则不折叠。更早的原文可让 AI 用 read_raw / grep_raw 回查。

关键行为不变量(实现约束)

以下是从历史设计/方案文档(子代理、模型配置 v5、上下文成本治理、联网搜索)中提炼的、仍属当前行为的硬约束;完整历史推导见 Git 历史(v4.26.0 及更早提交)。

子代理(DSH 对齐)

  • 委派深度:主会话按「最大委派深度」(默认 3;0=禁止委派),child 不允许再委派;超限返回错误工具结果,但委派工具保持可见。注意:实际只有 1 层——depth>0 时 effectiveMaxDepth 恒返回 0,该配置是「允许/禁止主会话委派」的开关,默认 3 不代表可以委派 3 层。
  • 强制非流式:child 激活统一走 sendChatRequest(..., {stream:false}),即使 chat 用途 groups[].body.stream=true 也只取一次性 JSON;不要给 headless child 接流式。
  • 工具面:Tool.handleToolCall 的 child.toolState 是唯一执行闸门,不得绕过(含 call_tool 内层、提示词工程路径);spawn 原生模式只向 child 注入 list_tools/search_tools/call_tool,fork 继承父已完成轮次;「子代理禁止调用工具」只收窄、deny 优先。
  • 结算与通知:settle 只在一次激活结束时触发一次;前台结果直接回父(工具结果),continuable 入父 noticeQueue,one-shot 后台 job 不投 notice(只能用 job_output 取);父运行中每轮 flushNotices 注入;父空闲且会话/全局均非待机才唤醒(待机不唤醒,notice 留存),令牌桶空则本次不跑;未 flush 的队列重载丢弃,运行中子代理记为 stopped(可续状态以存储记录为准)。
  • 停止原因→结果映射固定:completed 回 output;aborted/error/max-tokens/refusal/timeout 抛错并附 Diagnostic: 与 Partial output:;前台 Promise.allSettled 后必执行 dispose(),不半成功。
  • 断点续跑:每轮请求前落 checkpoint(tool 配对 + reasoning 无损编解码),续跑原样回放已存 messages 再追加新任务;send_message 只对 idle/interrupted 续跑,done/stopped 回「不再唤醒」。
  • 中断/清理:.ai stop 递归中止全部子代理(只停当前轮、可续);重载后 running 对账为「已中断·可续」;每父会话记录保留 100 条(自动清理最旧已结束记录),.ai subagent clean 手动清理;child 请求不得二次 acquire requestLimiter(会自锁)。
  • 空回复:headless 轮最终文本为空则不发送、不入上下文,仅记日志。
  • 子代理请求照常计入 usageMap 用量(按用途),与主会话同口径。

模型配置 v5

  • 解析期一次完成全部校验,任一条失败整份拒绝(不做部分生效);错误带 $.providers[0].models[1].name 式路径、一次报全部;version 固定 5。
  • 校验要点:模型 id 跨 provider 全局唯一;purposes 引用必须存在且 kind 与用途匹配(isEligible 精确:embed 只进 text-embedding、vision 只进 image-understanding、tts 只进 tts、对话类排除 embed/gen/tts,缺该类型返回 null、不退而求其次);apiKey 非空且字符 ∈ [\x21-\x7E](HTTP 头可携带集);baseURL 必填且 http(s)://;api ∈ 3 值、kind ∈ 5 值、bodyMode ∈ 2 值;groups[].purposes 元素须为 8 用途之一。
  • 协议按 api 分派(openai-completions / anthropic-messages),不再按厂商名猜;openai-responses 只接受枚举、运行时明确报错(不静默走错协议)。provider 只用于 baseURL 的 host→vendor 反查(媒体预设/余额),一般不用填。百度双密钥是唯一保留的厂商特例,靠 request.auth_token = "baidu-token" 触发,而不是 provider === 'baidu'。
  • bodyMode 默认值:媒体用途(tts / image-generation)默认 replace(整体替换预设结构),写 merge 才逐键合并;非媒体的 groups[].body 才默认逐键合并。深合并只在两侧都是普通对象时进行(否则会把字符串占位符 "{text}" 展开成 {0:'{',1:'t'…})。
  • 模型类型判定:显式 kind 最高优先;命名白名单只保留 rerank/生图/语音/嵌入终值;视觉不再按名字推断(gpt-4o / gemini / *vision 一律归 text,要识图必须写 "kind": "vision");接口自报能力位只作正向证据,不产出 text。
  • 列表与用途:models 留空/缺省 = 启动拉取 /models;写了 = 钉住清单、跳过拉取;单连接失败只降级该连接;.ai model pull 强制重拉。用途解析顺序:运行时覆盖(storage)→ 配置 purposes → 该用途候选第一个。purposes 未列出的同类型模型仍是候选但排在列出项之后;失败回退 pickNextModel 只往后走(pool.slice(idx+1),末尾返回 null,不绕回开头,避免 A→B→A 来回跳)。层失效时保留现场、记日志并回退下一层。
  • vendor 识别是 4 步且回落不报错:显式 provider > host 反查 > 路径尾段(只收"恰好一个 vendor 拥有"的路径,跳过通用预设与含 {} 的路径)> ''(按 OpenAI 兼容)。共用 host 的 alibaba/dashscope 必须显式写;识别失败时媒体请求会静默套通用结构,原生厂商会发错 body 且不报错。
  • 媒体细节:异步轮询兜底 15s × 20 次,成功值 SUCCEEDED/SUCCESS/COMPLETED;numOrDefault 只接受 number/数字字符串(Number(null)===0 会让 poll_interval=0 打爆接口)。识图/嵌入没有 vendor 预设机制(MediaPresetUse 仅 tts/image-generation);media_presets 保留内置;媒体用途跳过 token 用量与校准。
  • 不做 v4 兼容/迁移:旧的两个 TOML 框已注销且不读取;海豹无配置写入 API,只能用配置生成器重建(升级时日志会提示)。

上下文成本治理

  • 状态是算出来的、不持久化:非待机恒 HOT;在途任务(running||starting||activeRuns>0)冻结衰减;COLD_AFTER_MS=10 分钟、COOL_DOWN_MS=1 分钟、HOT_HOLD_MS=9 分钟;时钟回拨用 max(0, now-lastRelevantAt) 防负值。Session.state 已被角色状态占用,不要用 state 作新字段名。
  • 「相关活动」只认 session.chat()(@、触发、judge SPEAK、媒体/子代理送达、定时任务);普通消息入库与系统事件不算。
  • 冷态入库:零 LLM;单条超「冷态单条截断字数」用 truncateMiddle 留头 2/3 + 尾 1/3(保住末尾 [img:]/[record:] 不被切断),原文进 rawText + 「原文已截断」标记;冷态不做合并压缩。
  • 压缩三规则(仅热态):同批只尝试一次(含失败)、只压尾部连续未压缩段、结果必须比原文短才采纳。addUserMessage 用每会话 promise 链串行化(运行时状态放模块级 WeakMap),防止并发 await 期间互相覆盖 contentItems。
  • 渲染折叠:预算 = 冷态窗口字数;L0 = 窗口 60% + 折叠摘要 40%(摘要下限 200 字)、L1 = L0 + 20000、L2 = 不折叠;档位只看本轮最新一条真实用户发言(跳过系统条目,防「继续/总结」系统文案带偏);最新一条/最新原子组必留(整包可能略超预算);非待机不折叠(renderFoldBudget=0);折叠只影响渲染、不改持久化消息。
  • 归档/窗口移出分片:session_<id>:arc:<seq>(token 超限归档)与 raw:<seq>(窗口上限移出)都是只写一次、永不重写;先落分片再移除;写入/读异常返回 failed 时不移除来源条目(宁留重试不丢原文),exists 也推进序号;移出后必须立即 save()(延迟写会导致崩溃后消息复活 + 重复留档);分片超上限要清「所有超限序号」(只清一个会让旧分片永久滞留),按「原文分片上限」(默认 200)与「原文分片保留天数」(默认 30,惰性)回收。归档连续失败 ARCHIVE_FAIL_LIMIT=3 次后降级丢弃最早轮次(不写记忆)以恢复自愈。
  • 工具配对是硬不变量:窗口移出/折叠/归档不得拆散 assistant(tool_calls) 与其 tool 结果(否则请求 400);实现是粗选窗口 + alignToToolPairing 配对修复,连续 / 非连续 / 前导悬空三种形态一致成立。上下文内字段是 camelCase toolCallId,tool_call_id 只出现在出站请求体,配对检查统一走 toolResultCallId()。提示词工程模式不得做孤儿剔除(PE 把 tool 转成 user 文本,无配对 tool 合法,删掉会丢工具结果)。
  • 结算三条件:积压 ≥「结算水位」(默认 3000)且(空闲 ≥「结算空闲门槛」600s 或 会话转冷)且上下文 ≥「结算最小门槛」(20000 tokens);积压按全部未结算分片计;settledSeq 水位幂等,失败按 5/15/60 分钟退避(仅运行时);转冷时也会结算一次;「单次结算调用上限」(默认 2)限制的是分片数,不是 LLM 调用数(0=关闭自动结算)。
  • judge 体积上界:renderWindowedHistory 复用主对话折叠规则 + head 硬截断 …(更早内容已省略),预算为 0 时退冷态窗口字数 → 内置 5000;judge 是冷态零 LLM 的常规例外(gate ≤20 次/小时)。
  • 保存节流:只有高频入库路径 save(true)(5 秒 / 20 条合并);回复、命令、归档、挂起队列始终立即写。节流窗口内进程被杀最多丢最近 ≤5 秒 / ≤20 条冷态入库。
  • 「归档保留原文」关 = 只省存储、不省 LLM(仍 summarize);.ai forget --all 只清本会话 arc/raw 分片与水位,不影响已沉淀长期记忆(memv2:*);.ai ctxset 只影响本会话。
  • npm run sim(15 项)/ sim:matrix(76 项,90 组顺序×预算)/ fuzz(确定性 PRNG,可 seed 回放)是代码路径级模拟:能抓回归,但不能替代真机(覆盖不到真实厂商 API 请求体校验、goja storage 行为、QQ 消息形态、多会话并发时序)。

联网搜索

  • 路由与回退精确矩阵:providers 顺序即尝试顺序(routes.search/fetch 可覆盖);跳过 enabled:false、需要 key 但为空/占位符(如 sk-xxxx)的 provider;联网搜索失败回退=false 只试第一个(排障用)。401/402/403 直接换下一家;429/5xx/网络/超时在本家重试 1 次(固定 300ms,不读 Retry-After)再换下一家;其余 4xx(含 400/404/422)都不换家,直接报错(参数/配置错,换家只会掩盖问题);全部失败返回逐家原因汇总。
  • 空配置 = 内置 SearXNG(https://searxng.fishwhite.top),开箱可用;「联网配置」为 v1 JSON,version≠1 整体拒绝,坏 JSON / 校验失败只记日志、本次不生效(不回退默认),修改后需重载 JS。
  • generic 模板:占位符 {query}{count}{need_content}{page}{offset}{site}{time_range}{ori_question}{apiKey}{baseURL};整值占位符保留原始类型(数字/布尔)、混合串拼接、为空则删键;results_path 走 getByPath(a.b.0.c);fetch 支持 response_type: json|text。模板在 src/web/template.ts 自实现,不得反向依赖 model/media_template。
  • web_search:query 必填(兼容旧 q)/count/offset/detail/allowed_domains/blocked_domains/site/time_range/page;参数未指定时使用「联网」页的 联网搜索返回条数 / 联网搜索详细程度;detail=content 必须逐条按 联网搜索正文截断(默认 5000)截断(summary 每条 600~800 字,10 条可到 8k 字符),整体仍走「工具响应截断字数」兜底。
  • web_fetch:实参 url / max_chars / start_index(续读),默认转 Markdown;截断附「可传 start_index=N 继续读取」,到末尾给「没有更多内容」。(原方案里的 raw:true 返回原始 HTML 未实现。)
  • 两条禁改约定:工具名必须保持 web_search(session.toolState 按工具名持久化,改名会让所有会话开关失效);provider 不进模型可见 schema(模型爱乱填,强制指定走内部调用)。
  • 厂商托管搜索(server-side)属模型请求参数、不进联网 providers,与 web_search 同开有重复计费风险(应二选一);结果缓存/去重/二次 LLM 总结、WebUI 管理页、.ai web 命令、QPS min_interval、自建抓取 SSRF/robots.txt 加固均未实现(见 10-TODO)。

维护建议

  • README 与本知识库互补:README 面向普通用户(安装/配置/命令),docs/ 面向开发者(架构/源码/扩展)。代码结构变化时优先同步 docs/。
  • 发版前统一 VERSION、header.txt @version、package.json / package-lock.json 的 version 与 src/changelog/<版本>.ts 最新条目(sealpack/info.toml 由打包脚本自动同步);npm run version:check 可一键校验全部来源。
  • 架构约定:依赖只向下(L0→L4)、宿主能力走 getHost()、双向协作走 src/core/ports/、配置解析/应用分离;改动后跑 npm run deps:check,新增目录先补 LAYER_RULES。