结论

“聊天文件登记 → 生成 file_id → 模型调用工具 → 本地读取并转 Base64 → 云端生成讲义”这一分层设计是可行的。模型不应直接接触本地路径或 Base64,file_id 只作为受控引用。

关键陷阱

AstrBot 的 @filter.event_message_type(...) 处理器并不是在唤醒检查之前执行。管道顺序是:

  1. waking_check 读取消息链并筛选 AdapterMessageEvent handlers。
  2. 只有通过筛选的 handlers 才在 process_stage 中实际执行。
  3. on_llm_request 发生在后续 LLM 请求阶段。

因此,单靠普通插件 handler 无法保证捕获“未 @ 机器人、没有唤醒词”的群文件。若要支持这类消息,必须在适配器/事件入口登记,或修改核心唤醒逻辑;把文件强制设为 is_wake 会导致所有文件消息进入唤醒链,可能产生无意的 LLM 调用和刷屏。

推荐交互

两步模式(更稳)

  1. 适配器入口收到文件后立即登记,生成 file-000001,只回复文件 ID 和元数据,不调用 LLM。
  2. 用户随后发送“处理 file-000001,按……生成讲义”,模型调用 process_uploaded_file。

一步模式

文件和处理指令在同一消息中时,需要在进入 Agent 前完成登记,并把 ID/元数据注入该事件上下文。这通常需要适配器层或 AstrBot 核心提供 pre-wake hook,不能依赖普通 event_message_type handler 的执行时机。

排查方法

  • 先确认实际加载副本,而不是开发目录;比较两个 main.py 的 SHA-256。
  • 诊断探针必须在重启后的新进程中验证。修改文件时间晚于测试消息时,旧进程不会加载新代码。
  • 分别记录:waking 是否看到 File、handler 是否进入 activated_handlers、handler 是否真正执行、index.json 是否写入。
  • 模型无法自行读取 QQ/NapCat 事件对象;它只能使用注入上下文或工具返回的文件列表。

安全边界

  • file_id 必须严格校验格式并通过索引解析,禁止模型传路径。
  • Base64 只在工具调用内部生成,不写入提示词或聊天记录。
  • 需要限制文件大小、文件数量,并校验解析后的路径仍位于存储目录。

异步任务与工具循环 (Async Task and Tool-Loop Control)

6767 的 handouts_create 是后台任务接口:创建调用只返回真实 taskId,讲义生成和 PDF 下载在后续查询中完成。LLM 工具返回结果不会自动结束 AstrBot 的 Agent Loop,是否输出 final message 由模型决定。

因此,提示词不能写成“提交后必须继续查询”,否则模型可能在同一轮反复调用创建或查询工具。正确约束是:

  1. process_uploaded_file 只提交一次,并要求模型在 instructions 中复述用户原始要求、原样保留网页链接。
  2. 创建成功后,本轮最多调用一次 get_handout_task;如果状态是 pending/running,立即输出 final message,等待用户下一条消息再查。
  3. 插件侧按事件记录已提交的 taskId 和查询次数,拒绝同一轮重复创建/无限轮询;完成后 PDF 只发送一次。

AstrBot 的 Agent Runner 仍有全局 max_agent_step 兜底,但它只是达到上限后强制总结,不能替代工具自身的幂等和轮询限制。

后台任务不能用 Cron 唤醒 Agent

对异步任务进行周期查询时,必须区分“查询业务状态”和“唤醒完整 Agent”两件事。

错误做法是:每创建一个任务,就创建一个 AstrBot active_agent Cron,让 Cron 每分钟唤醒模型并询问任务状态。这样会形成正反馈:

创建任务 → 创建 Cron → Cron 唤醒 Agent → Agent 再次调用创建工具
        ↑                                      ↓
        └──────────── 新任务与新 Cron 自我繁殖 ────┘

症状包括群聊刷屏、任务重复、Cron 数量增长、Token 持续消耗,以及“任务越多查询越频繁”。

正确做法是由插件内部使用 asyncio 后台任务直接查询 REST API:

  • queued / pending / running 完全静默。
  • succeeded 下载产物并只发送一次。
  • failed 最多通知一次。
  • 查询过程不进入 LLM,不写入聊天 messages,不创建 AstrBot Cron。
  • 任务状态持久化 delivered_at、terminal_notified_at 等幂等标记,重启后仍不会重复投递。

一体化子进程的所有权模型 (Bundled Subprocess Ownership Model)

把本地 MCP/REST 服务打进 AstrBot 插件时,插件必须明确进程所有权:

  1. 初始化先请求 /health。
  2. 若目标端口已有匹配的外部服务,复用它,但不记录为插件所有。
  3. 只有插件自己 create_subprocess_exec 创建的进程才保存进程对象。
  4. terminate() 只终止这个进程对象,不能按进程名或端口批量杀进程。
  5. 子进程的 stdout/stderr 必须持续排空,否则长时间运行后可能因管道缓冲区写满而阻塞。
  6. Windows 创建后台进程使用 CREATE_NO_WINDOW,避免弹出额外终端窗口。

端口健康检查还必须校验服务身份,不能只判断“HTTP 200”。如果端口被另一个服务占用,应明确报告冲突,而不是把对方误判成可复用实例。

一体化密钥与运行数据隔离

发行 ZIP 只能包含源码和静态资源,不能包含真实 Key、任务、产物或供应商配置。首次启动时在插件数据目录随机生成:

  • Fleet Bearer API Key;
  • Claude worker 访问本地模型代理的固定 Key;
  • 本地管理密码。

服务端的 TASK_ROOT、KEYSTORE_FILE、PROVIDERS_FILE 也应全部指向 data/plugin_data/<plugin>/,不能写进插件安装目录。这样插件升级不会覆盖运行数据,导出的 ZIP 也不会意外携带凭据。

访问本地服务的 HTTP 客户端应设置 trust_env=False;worker 是否使用出站代理则作为独立显式配置,不能默认强塞某个本机代理端口。

双层防循环

外层 AstrBot Agent 与内层 Claude Code worker 是两个不同的 Agent Loop,必须分别限流:

  • 外层:同一事件只允许调用一次创建工具,成功后要求立即输出 final message。
  • 内层:设置 --max-turns,并保留服务端硬超时。

只限制外层不能阻止 Claude Code 在产物已经生成后反复做质量检查;只限制内层也不能阻止 AstrBot 重复创建后台任务。

验证标准

异步产物链路不能以“返回 taskId”或“目录里出现 PDF”为完成标准。完整验收至少包括:

  1. AstrBot 环境实际导入插件,LLM 工具完成登记。
  2. 内置服务 /health 与带鉴权业务 API 返回 200。
  3. 创建任务后状态经历真实 worker 执行并到达 succeeded。
  4. 产物下载接口返回 200。
  5. 文件头与最小大小校验通过,例如 PDF 必须以 %PDF 开头。
  6. 插件停止后,只释放自己启动的服务端口。
  7. ZIP 解压后重新执行 Python、Node 与 AstrBot 导入检查,防止“源码目录能跑、发行包缺文件”。

项目证据:astrbot_plugin_claude_fleet v1.1.0 将 claude-fleet-server、MCP、Claude Code worker 与 LaTeX 讲义执行器打入同一个 AstrBot 插件包,并按上述方式完成真实 PDF 验收。


本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/AI apps