架构总览
相关源文件
本页面内容基于以下源文件生成:
- main.py
- tarot.json
- metadata.yaml
- requirements.txt
- LICENSE
- _conf_schema.json
- How-to-add-new-tarot-theme.md
- resources/TouhouTarot/MajorArcana/0-愚者.jpg
- resources/TouhouTarot/MajorArcana/03-女皇.jpg
- resources/TouhouTarot/MajorArcana/04-皇帝.jpg
- resources/TouhouTarot/MajorArcana/05-教皇.jpg
- resources/TouhouTarot/MajorArcana/06-恋人.jpg
- resources/TouhouTarot/MajorArcana/07-战车.jpg
- resources/TouhouTarot/MajorArcana/08-力量.jpg
- resources/TouhouTarot/MajorArcana/09-隐士.jpg
- resources/TouhouTarot/MajorArcana/11-正义.jpg
- resources/TouhouTarot/MajorArcana/13-死神.jpg
- resources/TouhouTarot/MajorArcana/14-节制.jpg
- resources/TouhouTarot/MajorArcana/15-恶魔.jpg
- resources/TouhouTarot/MajorArcana/16-高塔.jpg
本项目是一个基于 AstrBot 框架的塔罗牌占卜插件,采用 Python 异步架构实现。插件通过集成 LLM(大语言模型)提供智能塔罗牌解析功能,支持多主题资源扩展、群聊消息转发、正逆位随机判定等核心特性。整体架构遵循单一职责原则,将命令处理、业务逻辑、资源管理和 AI 集成解耦为独立模块。
系统架构总览
正在加载图表渲染器...
架构要点说明:
- 分层设计:系统采用四层架构,用户交互层负责命令解析,核心业务层处理占卜流程,资源管理层负责主题和图片,AI 集成层处理 LLM 交互
- 异步通信:所有跨层调用均采用
async/await模式,确保 I/O 密集型操作(文件读取、LLM 调用)不阻塞主线程 - 资源热插拔:主题资源通过文件系统动态扫描加载,无需修改代码即可扩展新主题(main.py:32-37)
- AI 能力集成:通过 AstrBot 的
context.get_using_provider()获取 LLM Provider,实现牌阵智能匹配和占卜解析(main.py:113-119)
核心模块详解
插件入口与初始化模块
职责边界:
- 负责 AstrBot 框架集成,注册命令处理器
- 实例化核心业务类
Tarot,传递配置和上下文 - 不处理具体业务逻辑,仅作为命令路由层
入口 API:
python1@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_path、chain_reply、include_ai_in_chain等字段
初始化流程:
TarotPlugin继承Star基类,在构造函数中实例化Tarot核心类(main.py:277-280)Tarot类读取配置,初始化资源路径和功能开关(main.py:17-30)- 检查
tarot.json是否存在,缺失则抛出异常终止初始化
错误处理:
- 配置缺失时使用默认值(
resource_path默认"resource",chain_reply默认True) - 核心资源文件缺失时直接抛出异常,快速失败
核心业务引擎模块
职责边界:
- 实现完整占卜流程:牌阵匹配 → 抽牌 → 图片处理 → AI 解析 → 结果输出
- 管理群聊转发模式和单张/多张占卜逻辑
- 不直接处理命令解析,由上层
TarotPlugin路由调用
关键方法:
| 方法 | 功能 | 入参 | 出参 |
|---|---|---|---|
divine() | 完整占卜流程 | event, user_input | AsyncGenerator |
onetime_divine() | 单张牌占卜 | event, user_input | AsyncGenerator |
pick_theme() | 随机选择主题 | 无 | str |
_random_cards() | 随机抽牌 | all_cards, theme, num | List[Dict] |
_get_text_and_image() | 图片处理 | theme, card_info | Tuple[bool, str, str, bool] |
抽牌算法实现(main.py:47-57):
- 调用
pick_sub_types()获取当前主题下的可用子类型(大阿卡纳、圣杯、星币、宝剑、权杖) - 过滤
all_cards字典,仅保留属于选中子类型的牌 - 使用
random.sample()无放回抽取指定数量的牌,确保不重复
正逆位处理逻辑(main.py:78-91):
- 通过
random.random() < 0.5随机判定正逆位(50% 概率) - 逆位时使用 PIL 将图片旋转 180 度,缓存为
{name}_rotated.png - 根据正逆位从
meaning字段读取对应解释文本
群聊转发模式:
- 开启时:使用
Nodes和Node构建合并转发消息,所有牌面和 AI 解析合并为一条转发消息(main.py:171-199) - 关闭时:逐张发送牌面,每张间隔 2 秒,最后发送 AI 解析文本(main.py:203-216)
AI 集成模块
职责边界:
- 实现牌阵智能匹配:优先关键词匹配,失败时调用 LLM 进行语义理解
- 生成塔罗牌解析:根据牌阵、牌面、正逆位构建 Prompt,调用 LLM 生成解读
- 不处理 LLM Provider 的初始化和配置,依赖 AstrBot 框架注入
牌阵匹配策略(main.py:102-128):
正在加载图表渲染器...
匹配流程说明:
- 关键词匹配:预定义关键词列表("情感"、"爱情"、"事业"等),遍历牌阵的
representations字段进行模糊匹配 - LLM 降级匹配:关键词匹配失败时,构建 Prompt 让 LLM 从牌阵列表中选择最合适的
- 随机兜底: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):
- 根据
card_info中的type和pic字段定位图片文件 - 使用
glob()匹配任意后缀的图片文件(支持 jpg/png/webp 等) - 用 PIL 打开图片,判定正逆位,逆位时旋转并缓存
- 返回处理结果:
(成功标志, 文本, 图片路径, 是否正位)
边界条件处理:
- 图片不存在时返回错误信息,不中断流程(main.py:70-72)
- 旋转图片缓存时检查是否已存在,避免重复处理(main.py:83-88)
数据流与调用链
正在加载图表渲染器...
调用链关键节点:
- 命令入口:
TarotPlugin.divine_handler()接收用户命令,调用Tarot.divine()(main.py:282-294) - 牌阵匹配:优先使用关键词匹配,失败时调用 LLM 进行语义理解(main.py:102-128)
- 抽牌处理:根据牌阵所需牌数调用
_random_cards(),返回牌面信息列表(main.py:163) - 图片处理:每张牌调用
_get_text_and_image(),处理正逆位和图片旋转(main.py:174) - AI 解析:收集所有牌面信息后,调用
_generate_ai_interpretation()生成整体解读(main.py:187) - 结果输出:根据群聊转发模式,选择合并转发或逐条发送(main.py:199 或 main.py:210)
异步流特点:
- 使用
AsyncGenerator(yield)实现流式响应,用户可逐步看到占卜过程 - LLM 调用和图片处理均为异步操作,不阻塞消息发送
- 群聊转发模式下,所有消息先收集到
Nodes列表,最后一次性发送
核心设计决策与取舍
1. 异步生成器模式 vs 同步返回
决策:使用 AsyncGenerator(async def divine(...): yield result)而非同步返回完整结果
理由:
- 占卜过程包含多个耗时操作(LLM 调用、图片处理),流式输出提升用户体验
- 支持"正在洗牌中..."等中间状态提示,增强交互感
- 与 AstrBot 框架的消息处理机制天然契合
代价:
- 调用方必须使用
async for迭代结果,增加调用复杂度 - 错误处理需要在生成器内部捕获并 yield 错误消息
2. 牌阵匹配的降级策略
决策:关键词匹配 → LLM 语义匹配 → 随机选择,三级降级
理由:
- 关键词匹配快速且准确率高,覆盖常见场景("情感"、"事业")
- LLM 匹配处理复杂语义,但存在延迟和失败风险
- 随机选择作为兜底,确保任何情况下都能完成占卜
代价:
- 三级策略增加了代码复杂度
- LLM 匹配可能产生意外结果(返回不存在的牌阵名称)
3. 图片旋转缓存策略
决策:逆位图片首次生成后缓存为 {name}_rotated.png,后续直接复用
理由:
- 避免每次占卜都进行图片旋转处理,减少 CPU 开销
- 缓存文件命名规范,易于管理和清理
代价:
- 缓存文件占用磁盘空间(每张逆位牌额外一份)
- 原图更新时需手动清理缓存,否则显示旧版本
4. 群聊转发模式的双模式设计
决策:提供"转发模式"和"逐条模式"两种输出方式,通过配置切换
理由:
- 群聊转发模式减少消息刷屏,体验更整洁
- 逐条模式适合私聊或需要逐张查看的场景
- 配置项
chain_reply和include_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 Provider | AI 解析 | 通过 AstrBot 框架统一管理,支持多后端 | 直接调用 OpenAI API(失去框架优势) |
| Mermaid | 架构文档 | Markdown 原生支持,易于维护 | PlantUML(需额外工具) |
模块依赖关系
正在加载图表渲染器...
依赖层次说明:
- 框架依赖:
TarotPlugin依赖 AstrBot 的Star基类、Context和AstrMessageEvent - 核心依赖:
Tarot类依赖Context获取 LLM Provider,依赖AstrBotConfig读取配置 - 工具依赖:图片处理依赖 PIL,数据解析依赖 json,路径处理依赖 pathlib
- 资源依赖:运行时依赖文件系统中的
tarot.json和resources/目录
依赖风险:
- PIL 库安装可能需要系统级依赖(如 libjpeg)
- LLM Provider 可用性依赖外部服务(OpenAI API 等)
- 文件系统访问权限可能影响资源加载
关键配置与启动流程
配置项说明
| 配置键 | 类型 | 默认值 | 说明 |
|---|---|---|---|
resource_path | str | "resource" | 塔罗牌资源目录名,相对于插件根目录 |
chain_reply | bool | True | 群聊转发模式开关 |
include_ai_in_chain | bool | False | AI 解析是否包含在转发消息中 |
配置加载代码(main.py:21-24):
python1resource_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)
启动流程
正在加载图表渲染器...
启动检查点:
- JSON 完整性检查:
tarot.json必须存在,否则抛出异常(main.py:27-29) - 资源目录创建:使用
os.makedirs(exist_ok=True)确保资源目录存在(main.py:26) - 主题可用性检查:首次调用
pick_theme()时检查是否有可用主题(main.py:34-36)
运行时配置切换
插件支持运行时通过命令切换群聊转发模式:
开启群聊转发:调用switch_chain_reply(True)(main.py:318-326)关闭群聊转发:调用switch_chain_reply(False)(main.py:328-336)
注意:运行时切换仅影响当前会话,重启后恢复为配置文件中的默认值。
