价格

架构总览

相关源文件

本页面内容基于以下源文件生成:

本项目是一个基于 AstrBot 框架的塔罗牌占卜插件,采用 Python 异步架构实现。插件通过集成 LLM(大语言模型)提供智能塔罗牌解析功能,支持多主题资源扩展、群聊消息转发、正逆位随机判定等核心特性。整体架构遵循单一职责原则,将命令处理、业务逻辑、资源管理和 AI 集成解耦为独立模块。

系统架构总览

正在加载图表渲染器...

架构要点说明

  1. 分层设计:系统采用四层架构,用户交互层负责命令解析,核心业务层处理占卜流程,资源管理层负责主题和图片,AI 集成层处理 LLM 交互
  2. 异步通信:所有跨层调用均采用 async/await 模式,确保 I/O 密集型操作(文件读取、LLM 调用)不阻塞主线程
  3. 资源热插拔:主题资源通过文件系统动态扫描加载,无需修改代码即可扩展新主题(main.py:32-37
  4. AI 能力集成:通过 AstrBot 的 context.get_using_provider() 获取 LLM Provider,实现牌阵智能匹配和占卜解析(main.py:113-119

核心模块详解

插件入口与初始化模块

职责边界

  • 负责 AstrBot 框架集成,注册命令处理器
  • 实例化核心业务类 Tarot,传递配置和上下文
  • 不处理具体业务逻辑,仅作为命令路由层

入口 API

python
1@command("占卜")
2async def divine_handler(self, event: AstrMessageEvent, text: str = "")
3
4@command("塔罗牌")
5async def onetime_divine_handler(self, event: AstrMessageEvent, text: str = "")
6
7@command("开启群聊转发")
8async def enable_chain_reply(self, event: AstrMessageEvent, text: str = "")

关键数据结构

  • AstrMessageEvent:封装消息事件,提供 plain_result()chain_result() 等响应方法
  • Context:AstrBot 上下文对象,提供配置读取和 LLM Provider 访问接口
  • AstrBotConfig:插件配置字典,包含 resource_pathchain_replyinclude_ai_in_chain 等字段

初始化流程

  1. TarotPlugin 继承 Star 基类,在构造函数中实例化 Tarot 核心类(main.py:277-280
  2. Tarot 类读取配置,初始化资源路径和功能开关(main.py:17-30
  3. 检查 tarot.json 是否存在,缺失则抛出异常终止初始化

错误处理

  • 配置缺失时使用默认值(resource_path 默认 "resource"chain_reply 默认 True
  • 核心资源文件缺失时直接抛出异常,快速失败

核心业务引擎模块

职责边界

  • 实现完整占卜流程:牌阵匹配 → 抽牌 → 图片处理 → AI 解析 → 结果输出
  • 管理群聊转发模式和单张/多张占卜逻辑
  • 不直接处理命令解析,由上层 TarotPlugin 路由调用

关键方法

方法功能入参出参
divine()完整占卜流程event, user_inputAsyncGenerator
onetime_divine()单张牌占卜event, user_inputAsyncGenerator
pick_theme()随机选择主题str
_random_cards()随机抽牌all_cards, theme, numList[Dict]
_get_text_and_image()图片处理theme, card_infoTuple[bool, str, str, bool]

抽牌算法实现main.py:47-57):

  1. 调用 pick_sub_types() 获取当前主题下的可用子类型(大阿卡纳、圣杯、星币、宝剑、权杖)
  2. 过滤 all_cards 字典,仅保留属于选中子类型的牌
  3. 使用 random.sample() 无放回抽取指定数量的牌,确保不重复

正逆位处理逻辑main.py:78-91):

  • 通过 random.random() < 0.5 随机判定正逆位(50% 概率)
  • 逆位时使用 PIL 将图片旋转 180 度,缓存为 {name}_rotated.png
  • 根据正逆位从 meaning 字段读取对应解释文本

群聊转发模式

  • 开启时:使用 NodesNode 构建合并转发消息,所有牌面和 AI 解析合并为一条转发消息(main.py:171-199
  • 关闭时:逐张发送牌面,每张间隔 2 秒,最后发送 AI 解析文本(main.py:203-216

AI 集成模块

职责边界

  • 实现牌阵智能匹配:优先关键词匹配,失败时调用 LLM 进行语义理解
  • 生成塔罗牌解析:根据牌阵、牌面、正逆位构建 Prompt,调用 LLM 生成解读
  • 不处理 LLM Provider 的初始化和配置,依赖 AstrBot 框架注入

牌阵匹配策略main.py:102-128):

正在加载图表渲染器...

匹配流程说明

  1. 关键词匹配:预定义关键词列表("情感"、"爱情"、"事业"等),遍历牌阵的 representations 字段进行模糊匹配
  2. LLM 降级匹配:关键词匹配失败时,构建 Prompt 让 LLM 从牌阵列表中选择最合适的
  3. 随机兜底:LLM 返回"随机选择"或无效结果时,使用 random.choice() 随机选取

AI 解析 Prompt 构建main.py:130-150):

  • 系统提示词:"你是一个专业的塔罗牌占卜师,擅长提供深入且简洁的解析。"
  • 用户提示词结构:牌阵名称 + 抽到的牌及位置 + 用户原始指令 + 解析要求(200-300 字,善用换行和颜表情)

错误处理

  • LLM 调用失败时返回默认文本 "抱歉,AI 解析生成失败,请稍后再试。"
  • 牌阵匹配失败时降级为随机选择,确保流程不中断

资源管理模块

职责边界

  • 管理塔罗牌主题资源目录,支持动态扫描和加载
  • 处理图片文件的读取、旋转和缓存
  • 不负责资源下载和安装,由用户手动配置

目录结构规范How-to-add-new-tarot-theme.md:13-32):

resources/
├ BilibiliTarot/
│ ├ MajorArcana/
│ │ ├ 0-愚者.png
│ │ └ ...
│ ├ Cups/
│ ├ Pentacles/
│ ├ Swords/
│ └ Wands/
└ TouhouTarot/
  └ MajorArcana/
    ├ 0-愚者.jpg
    └ ...

主题扫描逻辑main.py:32-37):

  • 使用 Path.iterdir() 遍历资源目录下的所有子目录
  • 每个子目录视为一个独立主题,通过 random.choice() 随机选择

子类型过滤main.py:39-45):

  • 预定义标准子类型列表:["MajorArcana", "Cups", "Pentacles", "Swords", "Wands"]
  • 扫描主题目录下的子目录,仅保留与标准列表匹配的
  • 若主题目录下无任何标准子类型,则使用完整标准列表(允许资源不完整)

图片处理流程main.py:59-100):

  1. 根据 card_info 中的 typepic 字段定位图片文件
  2. 使用 glob() 匹配任意后缀的图片文件(支持 jpg/png/webp 等)
  3. 用 PIL 打开图片,判定正逆位,逆位时旋转并缓存
  4. 返回处理结果:(成功标志, 文本, 图片路径, 是否正位)

边界条件处理

  • 图片不存在时返回错误信息,不中断流程(main.py:70-72
  • 旋转图片缓存时检查是否已存在,避免重复处理(main.py:83-88

数据流与调用链

正在加载图表渲染器...

调用链关键节点

  1. 命令入口TarotPlugin.divine_handler() 接收用户命令,调用 Tarot.divine()main.py:282-294
  2. 牌阵匹配:优先使用关键词匹配,失败时调用 LLM 进行语义理解(main.py:102-128
  3. 抽牌处理:根据牌阵所需牌数调用 _random_cards(),返回牌面信息列表(main.py:163
  4. 图片处理:每张牌调用 _get_text_and_image(),处理正逆位和图片旋转(main.py:174
  5. AI 解析:收集所有牌面信息后,调用 _generate_ai_interpretation() 生成整体解读(main.py:187
  6. 结果输出:根据群聊转发模式,选择合并转发或逐条发送(main.py:199main.py:210

异步流特点

  • 使用 AsyncGeneratoryield)实现流式响应,用户可逐步看到占卜过程
  • LLM 调用和图片处理均为异步操作,不阻塞消息发送
  • 群聊转发模式下,所有消息先收集到 Nodes 列表,最后一次性发送

核心设计决策与取舍

1. 异步生成器模式 vs 同步返回

决策:使用 AsyncGeneratorasync def divine(...): yield result)而非同步返回完整结果

理由

  • 占卜过程包含多个耗时操作(LLM 调用、图片处理),流式输出提升用户体验
  • 支持"正在洗牌中..."等中间状态提示,增强交互感
  • 与 AstrBot 框架的消息处理机制天然契合

代价

  • 调用方必须使用 async for 迭代结果,增加调用复杂度
  • 错误处理需要在生成器内部捕获并 yield 错误消息

2. 牌阵匹配的降级策略

决策:关键词匹配 → LLM 语义匹配 → 随机选择,三级降级

理由

  • 关键词匹配快速且准确率高,覆盖常见场景("情感"、"事业")
  • LLM 匹配处理复杂语义,但存在延迟和失败风险
  • 随机选择作为兜底,确保任何情况下都能完成占卜

代价

  • 三级策略增加了代码复杂度
  • LLM 匹配可能产生意外结果(返回不存在的牌阵名称)

3. 图片旋转缓存策略

决策:逆位图片首次生成后缓存为 {name}_rotated.png,后续直接复用

理由

  • 避免每次占卜都进行图片旋转处理,减少 CPU 开销
  • 缓存文件命名规范,易于管理和清理

代价

  • 缓存文件占用磁盘空间(每张逆位牌额外一份)
  • 原图更新时需手动清理缓存,否则显示旧版本

4. 群聊转发模式的双模式设计

决策:提供"转发模式"和"逐条模式"两种输出方式,通过配置切换

理由

  • 群聊转发模式减少消息刷屏,体验更整洁
  • 逐条模式适合私聊或需要逐张查看的场景
  • 配置项 chain_replyinclude_ai_in_chain 提供细粒度控制

代价

  • 两套输出逻辑增加代码维护成本
  • 转发消息在某些平台可能受限(如 QQ 群转发消息数量限制)

5. 资源目录的动态扫描

决策:运行时扫描资源目录,而非硬编码主题列表

理由

  • 支持用户自定义主题,无需修改代码
  • 新增主题只需放入 resources/ 目录,重启插件即可生效
  • How-to-add-new-tarot-theme.md 文档描述的扩展机制一致

代价

  • 每次占卜都需扫描目录,存在轻微性能开销
  • 目录结构不规范时可能导致异常

6. LLM Provider 的依赖注入

决策:通过 context.get_using_provider() 获取 LLM Provider,而非自行初始化

理由

  • 与 AstrBot 框架深度集成,复用框架的 LLM 配置
  • 支持多 Provider 切换(OpenAI、Claude、本地模型等)
  • 避免插件内部管理 API Key 等敏感信息

代价

  • 强依赖 AstrBot 框架,无法独立运行
  • 框架升级可能导致接口变更

7. 错误处理的快速失败策略

决策:核心资源缺失时直接抛出异常终止初始化,运行时错误降级处理

理由

  • tarot.json 缺失属于致命错误,无法恢复,应快速失败
  • 图片缺失或 LLM 调用失败属于可恢复错误,应降级处理并返回友好提示
  • 通过日志记录错误详情,便于排查

代价

  • 初始化失败会导致整个插件不可用
  • 需要在文档中明确资源完整性要求

技术选型

技术用途选型理由替代方案
Python 3.8+主要开发语言AstrBot 框架要求,异步支持完善Node.js(需重写框架集成)
AstrBot Star插件基类提供命令注册、消息处理、配置管理等基础设施自建机器人框架(开发成本高)
asyncio异步编程原生支持异步 I/O,适合 LLM 调用和文件操作threading(复杂度高,性能差)
PIL/Pillow图片处理成熟稳定,支持旋转、格式转换等操作OpenCV(过于重量级)
pathlib路径处理面向对象 API,跨平台兼容性好os.path(API 较底层)
random随机算法内置模块,满足抽牌和正逆位判定需求numpy.random(过度依赖)
json数据解析tarot.json 格式简单,无需复杂解析PyYAML(增加依赖)
LLM ProviderAI 解析通过 AstrBot 框架统一管理,支持多后端直接调用 OpenAI API(失去框架优势)
Mermaid架构文档Markdown 原生支持,易于维护PlantUML(需额外工具)

模块依赖关系

正在加载图表渲染器...

依赖层次说明

  1. 框架依赖TarotPlugin 依赖 AstrBot 的 Star 基类、ContextAstrMessageEvent
  2. 核心依赖Tarot 类依赖 Context 获取 LLM Provider,依赖 AstrBotConfig 读取配置
  3. 工具依赖:图片处理依赖 PIL,数据解析依赖 json,路径处理依赖 pathlib
  4. 资源依赖:运行时依赖文件系统中的 tarot.jsonresources/ 目录

依赖风险

  • PIL 库安装可能需要系统级依赖(如 libjpeg)
  • LLM Provider 可用性依赖外部服务(OpenAI API 等)
  • 文件系统访问权限可能影响资源加载

关键配置与启动流程

配置项说明

配置键类型默认值说明
resource_pathstr"resource"塔罗牌资源目录名,相对于插件根目录
chain_replyboolTrue群聊转发模式开关
include_ai_in_chainboolFalseAI 解析是否包含在转发消息中

配置加载代码main.py:21-24):

python
1resource_path_str: str = config.get("resource_path", "resource")
2self.resource_path: Path = Path(__file__).parent / resource_path_str
3self.is_chain_reply: bool = config.get("chain_reply", True)
4self.include_ai_in_chain: bool = config.get("include_ai_in_chain", False)

启动流程

正在加载图表渲染器...

启动检查点

  1. JSON 完整性检查tarot.json 必须存在,否则抛出异常(main.py:27-29
  2. 资源目录创建:使用 os.makedirs(exist_ok=True) 确保资源目录存在(main.py:26
  3. 主题可用性检查:首次调用 pick_theme() 时检查是否有可用主题(main.py:34-36

运行时配置切换

插件支持运行时通过命令切换群聊转发模式:

  • 开启群聊转发:调用 switch_chain_reply(True)main.py:318-326
  • 关闭群聊转发:调用 switch_chain_reply(False)main.py:328-336

注意:运行时切换仅影响当前会话,重启后恢复为配置文件中的默认值。