- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
21 KiB
21 KiB
测试用例 — AstrBot频道优化(P0-P2 全部完成)
测试日期: 2026-06-01
测试工程师: 测试工程师
测试依据: 项目负责人指定的AstrBot频道优化需求 + 前后端代码分析
需求范围: 统一Webhook端点(6平台)、消息组件系统(5种组件)、平台注册机制(6个适配器)、MessageSession统一标识、Pipeline管道(5 Stage)、Redis事件总线、二维码localhost根因修复(config.py三级防护)
1. 需求依据
- PRD 路径:
/data/share/project/docs/PRD.md - 本轮优化范围(项目负责人指定):
- 统一Webhook端点 — 为6个IM平台(企业微信/飞书/钉钉/Telegram/Slack/WhatsApp)提供统一的
/webhook/{platform}入口 - 消息组件系统 — 5种消息组件(Plain/Image/At/Reply/File),支持序列化/反序列化
- 平台注册机制 — 使用
@register_platform_adapter装饰器标准化注册6个适配器 - MessageSession统一会话标识 —
platform:type:session_id格式 - Pipeline管道 — 5 Stage 链式处理(WakingCheck→RateLimit→ContentSafety→Process→Respond)
- Redis事件总线 — 基于Redis List的生产者-消费者消息队列
- 二维码localhost根因修复 —
config.py三级防护(环境变量→.env→硬编码域名)
- 统一Webhook端点 — 为6个IM平台(企业微信/飞书/钉钉/Telegram/Slack/WhatsApp)提供统一的
2. 代码功能模块概览
| 模块 | 职责 | 代码位置 |
|---|---|---|
| Config配置模块 | APP_ACCESS_URL三级防护,全局配置管理 | app/config.py:36-63 |
| Pipeline管道系统 | 5 Stage链式消息处理,PipelineScheduler调度器 | app/pipeline.py (349行) |
| Redis事件总线 | MessageEvent序列化,生产者-消费者模式 | app/event_bus.py (293行) |
| 消息组件系统 | 5种组件(Plain/Image/At/Reply/File),序列化工厂 | app/message_components.py (340行) |
| 平台注册机制 | @register_platform_adapter装饰器,全局注册表 | app/platform_adapter.py (126行) |
| 6个平台适配器 | wecom/feishu/dingtalk/telegram/slack/whatsapp | app/platform_adapters/*.py |
| MessageSession | 统一会话标识 platform:type:session_id | app/message_session.py (55行) |
| 统一Webhook端点 | POST /webhook/{platform},适配器+EventBus集成 | app/api/channels.py:1453-1579 |
| 频道QR二维码 | 微信ClawBot API优先,本地qrcode库回退 | app/api/channels.py:420-498 |
| Webhook平台列表 | GET /webhook/platforms 列出所有支持平台 | app/api/channels.py:1582-1597 |
3. 需求与代码差异说明
3.1 PRD中未在代码中体现的内容
无。本轮优化的7个功能点在代码中均已有完整实现。
3.2 PRD中未覆盖但代码中存在的功能模块
| 编号 | 模块名称 | 代码位置 | 建议 |
|---|---|---|---|
| N1 | 旧版webhook处理函数(_handle_*_webhook) |
app/api/channels.py:1600-1918 |
与适配器模式重复,本次已统一到适配器架构 |
| N2 | Pipeline单元测试(自测代码) | app/pipeline.py:202-347 |
内建9个单元测试,建议产品确认纳入CI |
| N3 | EventBus单元测试(自测代码) | app/event_bus.py:182-292 |
内建7个单元测试,建议产品确认纳入CI |
| N4 | 频道消息组件与webhook解析的桥接 | 跨文件 | 适配器 parse_webhook() 返回dict而非BaseMessageComponent链,存在集成断点 |
4. 测试用例
4.1 统一Webhook端点 (Q1)
TC-WH-01 [P0] 验证统一Webhook端点存在且路径正确
- 前置条件: 后端服务运行在18080端口
- 步骤:
[1] 向
POST /app/api/channels/webhook/{platform}各平台发送请求 [2] 验证返回404之外的HTTP状态码(400表示平台不支持但端点存在) [3] 逐个平台验证:wecom/feishu/dingtalk/telegram/slack/whatsapp - 预期结果: 所有6个平台的webhook端点均可访问(返回400"不支持的平台"或正常响应)
- 备注: 请求体为空时,FastAPI可能返回422(Body缺失),这也是端点存在的证明
TC-WH-02 [P0] 验证Webhook端点消息接收和记录
- 前置条件: 已有活跃的微信频道
- 步骤:
[1] 构造合法的微信平台webhook消息payload
[2] 发送POST到
/app/api/channels/webhook/wecom[3] 验证返回code: 0且data.handled=true[4] 验证返回包含message_id、session和platform字段 [5] 检查数据库中channel_messages表存在对应记录 - 预期结果: 消息成功接收并写入数据库;返回标准webhook响应格式
- 备注: 验证从webhook入站到数据库记录的完整链路
TC-WH-03 [P0] 验证Webhook平台验证(飞书challenge/ Slack url_verification)
- 前置条件: 后端服务运行
- 步骤:
[1] 向
/webhook/feishu发送{"challenge": "test_challenge_123"}的POST请求 [2] 验证返回{"challenge": "test_challenge_123"}[3] 向/webhook/slack发送{"type": "url_verification", "challenge": "slack_challenge_456"}[4] 验证返回{"challenge": "slack_challenge_456"} - 预期结果: 飞书和Slack的URL验证正确返回challenge值
- 备注: URL验证是各平台接入的第一步,必须正确处理
TC-WH-04 [P1] 验证Webhook对未注册平台的拒绝
- 前置条件: 后端服务运行
- 步骤:
[1] 向
/webhook/unknown_platform发送POST请求 [2] 验证返回400状态码 [3] 验证错误消息包含"不支持的平台"和可用平台列表 - 预期结果: 未注册平台被正确拒绝,返回明确错误
TC-WH-05 [P1] 验证无活跃频道时Webhook行为
- 前置条件: 所有wecom频道已断开或删除
- 步骤:
[1] 向
/webhook/wecom发送消息payload [2] 验证返回code: 0且data.handled=false[3] 验证消息包含"无活跃的wecom频道" - 预期结果: 优雅处理无活跃频道的情况,不报500错误
TC-WH-06 [P0] 验证Webhook支持平台列表接口
- 前置条件: 6个适配器已注册
- 步骤:
[1] 发送GET请求到
/app/api/channels/webhook/platforms[2] 验证返回code: 0[3] 验证data.platforms包含6个平台条目 [4] 验证每个平台条目包含name/display_name/description/support能力字段 - 预期结果: 返回完整的6个平台元数据
4.2 消息组件系统 (Q2)
TC-MC-01 [P0] 验证Plain文本组件序列化/反序列化
- 前置条件: 消息组件系统代码可导入
- 步骤:
[1] 创建
Plain(text="你好世界")[2] 调用to_dict()序列化 [3] 验证返回{"type": "plain", "text": "你好世界"}[4] 调用Plain.from_dict(serialized)反序列化 [5] 验证plain_obj.text == "你好世界" - 预期结果: Plain组件序列化/反序列化正确
TC-MC-02 [P0] 验证Image图片组件序列化/反序列化
- 前置条件: 同上
- 步骤:
[1] 创建
Image(url="https://example.com/img.png", width=800, height=600)[2] 调用to_dict()验证url/width/height字段 [3] 调用Image.from_dict()反序列化验证 - 预期结果: Image组件序列化/反序列化正确
TC-MC-03 [P0] 验证At提及组件序列化/反序列化
- 前置条件: 同上
- 步骤:
[1] 创建
At(user_id="user_001", display_name="张三")[2] 序列化验证user_id和display_name [3] 反序列化验证 [4] 测试不带display_name的场景 - 预期结果: At组件正确处理user_id和可选的display_name
TC-MC-04 [P0] 验证Reply回复组件序列化/反序列化
- 前置条件: 同上
- 步骤:
[1] 创建
Reply(message_id="msg_001", content_preview="原消息预览")[2] 序列化验证message_id和content_preview [3] 反序列化验证 - 预期结果: Reply组件序列化/反序列化正确
TC-MC-05 [P0] 验证File文件组件序列化/反序列化
- 前置条件: 同上
- 步骤:
[1] 创建
File(filename="report.pdf", url="https://example.com/report.pdf", size=102400)[2] 序列化验证filename/url/size [3] 验证不带size时的序列化 - 预期结果: File组件序列化/反序列化正确
TC-MC-06 [P0] 验证消息链整体序列化/反序列化 (message_chain_to_dict/from_dict)
- 前置条件: 同上
- 步骤:
[1] 构建复合消息链:
[Reply(...), Plain(...), At(...), Image(...)][2] 调用message_chain_to_dict(chain)生成字典列表 [3] 调用message_chain_from_dict(dict_list)恢复组件链 [4] 验证每个组件的类型和关键字段正确 - 预期结果: 完整消息链循环序列化正确
TC-MC-07 [P1] 验证辅助提取函数
- 前置条件: 同上
- 步骤:
[1] 构建包含多个组件的消息链
[2] 测试
extract_plain_text()正确拼接所有Plain文本 [3] 测试extract_image_urls()提取所有图片URL [4] 测试extract_at_user_ids()提取所有@用户ID [5] 测试has_component_type()对存在和不存在的类型 - 预期结果: 所有辅助提取函数正确
TC-MC-08 [P1] 验证未知组件类型降级处理
- 前置条件: 同上
- 步骤:
[1] 构造
[{"type": "unknown_type", "data": "test"}]传入message_chain_from_dict[2] 验证返回结果为Plain组件,text为原始dict的字符串表示 - 预期结果: 未知类型优雅降级,不抛异常
4.3 平台注册机制 (Q3)
TC-PR-01 [P0] 验证6个适配器均已正确注册
- 前置条件: 6个适配器文件已导入
- 步骤:
[1] 从
platform_adapter导入platform_registry和platform_cls_map[2] 验证len(platform_registry) == 6[3] 验证注册表包含所有6个平台:{"wecom", "feishu", "dingtalk", "telegram", "slack", "whatsapp"}[4] 验证platform_cls_map中的每个类都是BasePlatformAdapter的子类 - 预期结果: 6个适配器全部正确注册,name和class映射完整
TC-PR-02 [P0] 验证平台适配器工厂函数
- 前置条件: 适配器已注册
- 步骤:
[1] 对每个平台调用
get_platform_adapter(name, config={})[2] 验证返回的实例是其对应适配器类的实例 [3] 验证每个适配器get_status()返回 "stopped" [4] 调用start()后验证get_status()返回 "running" - 预期结果: 工厂函数正确创建适配器实例,状态管理正常
TC-PR-03 [P0] 验证平台元数据完整性
- 前置条件: 适配器已注册
- 步骤:
[1] 对每个平台获取
platform_registry[name][2] 验证元数据包含: name/display_name/description/icon/version [3] 验证能力标记: support_streaming/support_image/support_file/support_voice [4] 验证不同平台的能力标记符合实际情况: 飞书streaming=true, 企业微信streaming=false等 - 预期结果: 所有平台元数据字段完整正确
TC-PR-04 [P0] 验证list_platforms API
- 前置条件: 适配器已注册
- 步骤:
[1] 调用
list_platforms()[2] 验证返回6个PlatformMetadata实例 [3] 验证每个实例的name字段唯一且非空 - 预期结果: 列出所有已注册平台
TC-PR-05 [P1] 验证未注册平台的错误处理
- 前置条件: 适配器已注册
- 步骤:
[1] 调用
get_platform_adapter("unknown_platform", {})[2] 验证抛出ValueError异常 [3] 验证异常消息包含"未知平台"和可用平台列表 - 预期结果: 未注册平台被正确拒绝,有清晰错误提示
TC-PR-06 [P1] 验证装饰器中非BasePlatformAdapter子类的拒绝
- 前置条件: 适配器代码
- 步骤:
[1] 确认代码中
register_platform_adapter装饰器的类型检查逻辑存在 [2] 验证issubclass(cls, BasePlatformAdapter)检查在第93行 - 预期结果: 装饰器强制类型约束,非适配器子类会抛出TypeError
4.4 MessageSession统一会话标识 (Q4)
TC-MS-01 [P0] 验证MessageSession创建和字符串化
- 前置条件: message_session模块可用
- 步骤:
[1] 创建
MessageSession(platform_name="wecom", session_type=SessionType.GROUP, session_id="group_123")[2] 验证unified_msg_origin返回"wecom:group:group_123"[3] 创建MessageSession(platform_name="feishu", session_type=SessionType.FRIEND, session_id="user_456")[4] 验证unified_msg_origin返回"feishu:friend:user_456" - 预期结果: 统一会话标识格式化为
platform:type:session_id
TC-MS-02 [P0] 验证from_str解析
- 前置条件: 同上
- 步骤:
[1] 调用
MessageSession.from_str("telegram:friend:user_789")[2] 验证 platform_name="telegram", session_type=SessionType.FRIEND, session_id="user_789" [3] 调用MessageSession.from_str("slack:channel:ch_001")[4] 验证 session_type=SessionType.CHANNEL - 预期结果: 正确从字符串解析,所有SessionType枚举值均支持
TC-MS-03 [P0] 验证from_channel_event自动推断
- 前置条件: 同上
- 步骤:
[1] 有group_id时:
from_channel_event("wecom", "ch_001", "user_001", group_id="grp_001")[2] 验证返回GROUP类型,session_id="grp_001" [3] 无group_id时:from_channel_event("feishu", "ch_002", "user_002")[4] 验证返回FRIEND类型,session_id="user_002" - 预期结果: 自动根据group_id判断会话类型
TC-MS-04 [P1] 验证hashable/equality行为
- 前置条件: 同上
- 步骤:
[1] 创建
ms1 = MessageSession("wecom", SessionType.FRIEND, "user_001")[2] 创建ms2 = MessageSession("wecom", SessionType.FRIEND, "user_001")[3] 验证ms1 == ms2为True [4] 验证hash(ms1) == hash(ms2)[5] 验证可以放入set去重:len({ms1, ms2}) == 1 - 预期结果: MessageSession支持相等比较和哈希,可用作集合元素
TC-MS-05 [P1] 验证from_str格式错误处理
- 前置条件: 同上
- 步骤:
[1] 调用
MessageSession.from_str("invalid_format")[2] 验证抛出ValueError异常 [3] 验证异常消息包含正确的格式说明 - 预期结果: 格式错误的字符串被正确拒绝
4.5 Pipeline管道系统 (Q5)
TC-PL-01 [P0] 验证Pipeline默认5阶段按顺序执行
- 前置条件: pipeline模块可用
- 步骤:
[1] 创建
PipelineScheduler()默认管道 [2] 构造完整上下文(含message/platform/sender_id/is_private等) [3] 调用pipeline.execute(context)[4] 验证返回success=True[5] 验证pipeline_log包含5条记录,顺序为: waking_check → rate_limit_check → content_safety_check → process → respond - 预期结果: 5个阶段按正确顺序执行,pipeline_log记录完整
TC-PL-02 [P0] 验证WakingCheck阶段:私聊自动唤醒
- 前置条件: 同上
- 步骤:
[1] 创建私聊场景:
is_private=True[2] 执行WakingCheckStage.process() [3] 验证should_respond=True,stop=False - 预期结果: 私聊消息自动唤醒AI响应
TC-PL-03 [P0] 验证WakingCheck阶段:群聊@bot唤醒
- 前置条件: 同上
- 步骤:
[1] 创建群聊场景:
is_private=False, message="@bot 帮我查询" [2] 执行WakingCheckStage.process() [3] 验证should_respond=True,stop=False - 预期结果: 群聊中@bot后唤醒AI
TC-PL-04 [P0] 验证WakingCheck阶段:群聊不@bot则停止
- 前置条件: 同上
- 步骤:
[1] 创建群聊场景:
is_private=False, message="今天天气不错" [2] 执行WakingCheckStage.process() [3] 验证should_respond=False,stop=True - 预期结果: 群聊中未@bot时停止管道
TC-PL-05 [P0] 验证RateLimit阶段:限流生效
- 前置条件: 同上
- 步骤:
[1] 设置
RateLimitCheckStage(max_per_minute=3)[2] 同一sender_id连续发送4次 [3] 验证前3次返回success=True [4] 验证第4次返回success=False, error含"频繁", stop=True - 预期结果: 超过限制后被正确限流
TC-PL-06 [P1] 验证ContentSafety阶段:正常内容通过
- 前置条件: 同上
- 步骤: [1] 发送内容为"正常的聊天内容" [2] 验证success=True, stop=False
- 预期结果: 正常内容不受影响
TC-PL-07 [P1] 验证Pipeline阶段异常处理
- 前置条件: 同上
- 步骤: [1] 创建抛出异常的BrokenStage [2] 执行PipelineScheduler(stages=[BrokenStage()]) [3] 验证返回success=False [4] 验证error消息包含阶段名和异常信息
- 预期结果: stage异常被正确捕获,返回结构化错误
TC-PL-08 [P1] 验证Pipeline阶段动态增删
- 前置条件: 同上
- 步骤:
[1] 创建空stages的管道
[2] 调用
add_stage()添加WakingCheckStage [3] 调用add_stage()插入到位置0 [4] 验证阶段顺序正确 [5] 调用remove_stage("waking_check")移除 [6] 验证移除后管道阶段列表正确 - 预期结果: 动态增删阶段正常
4.6 Redis事件总线 (Q6)
TC-EB-01 [P0] 验证MessageEvent创建和序列化
- 前置条件: Redis可用
- 步骤:
[1] 创建MessageEvent(platform="feishu", channel_id="ch_001", content="测试", sender_id="user_001")
[2] 调用
to_dict()序列化 [3] 验证返回字典包含所有9个字段 [4] 验证timestamp字段存在且为整数 - 预期结果: MessageEvent序列化包含完整字段,含自动生成的timestamp
TC-EB-02 [P0] 验证MessageEvent反序列化
- 前置条件: 同上
- 步骤:
[1] 从to_dict结果调用
MessageEvent.from_dict(dict_data)[2] 验证所有字段完全恢复 [3] 特别验证platform/channel_id/content/sender_id正确 - 预期结果: 序列化→反序列化循环后数据一致
TC-EB-03 [P0] 验证RedisEventBus事件发布
- 前置条件: Redis可用
- 步骤:
[1] 创建RedisEventBus实例
[2] 创建MessageEvent并调用
bus.publish(event)[3] 验证返回True(发布成功) [4] 调用bus.queue_size()验证队列长度 ≥1 - 预期结果: 事件成功写入Redis List
TC-EB-04 [P0] 验证RedisEventBus事件消费
- 前置条件: Redis可用
- 步骤:
[1] 创建RedisEventBus并启动1个消费者
[2] 注册feishu平台的handler
[3] 发布一个feishu平台事件
[4] 等待1秒后验证handler被调用
[5] 验证handler接收到的事件字段正确
[6] 调用
bus.stop()停止消费者 - 预期结果: 事件被正确消费并分发给对应平台handler
TC-EB-05 [P0] 验证全局单例
- 前置条件: 同上
- 步骤:
[1] 调用
get_event_bus()两次 [2] 验证两个引用指向同一个实例(bus1 is bus2) - 预期结果: 全局单例正确
TC-EB-06 [P1] 验证订阅通配符 *
- 前置条件: Redis可用
- 步骤:
[1] 注册
*通配符handler [2] 发布不同平台(feishu/wecom)的事件 [3] 验证通配符handler都能接收到 [4] 清理 - 预期结果: 通配符订阅接收所有平台事件
TC-EB-07 [P1] 验证多消费者并发
- 前置条件: Redis可用
- 步骤: [1] 启动2个消费者线程 [2] 发布多个事件 [3] 验证所有事件被消费,不重复不丢失 [4] 停止消费者
- 预期结果: 多消费者并发正常工作
TC-EB-08 [P1] 验证Redis队列清理
- 前置条件: Redis可用
- 步骤:
[1] 清理测试key:
DEL aiflow:event_queue[2] 验证queue_size() == 0 - 预期结果: 队列清理后为空
4.7 二维码URL非localhost (Q7)
TC-QR-01 [P0] 验证config.py三级防护机制存在
- 前置条件: config.py可读取
- 步骤:
[1] 检查
APP_ACCESS_URL赋值逻辑 [2] 验证第40-41行:首选环境变量os.getenv("APP_ACCESS_URL")[3] 验证第43-58行:临时域名检测(team-coding/localhost/127.0.0.1/0.0.0.0),检测到后从.env覆盖 [4] 验证第61-63行:最终兜底为https://bot.qihuyun.com - 预期结果: 三级防护完整:环境变量→.env→硬编码正式域名
TC-QR-02 [P0] 验证二维码生成API返回非localhost的URL
- 前置条件: 频道已创建
- 步骤:
[1] 调用
POST /app/api/channels/authorize/scan?platform=wechat生成二维码 [2] 验证返回的qr_url字段存在(base64 data URI) [3] 通过API调用确认绑定URL使用APP_ACCESS_URL,非localhost [4] 验证API响应中的二维码图片不编码localhost地址 - 预期结果: 二维码包含生产地址,不含localhost
TC-QR-03 [P1] 验证临时域名检测模式列表
- 前置条件: config.py可读取
- 步骤:
[1] 验证
_TEMP_DOMAIN_PATTERNS包含: "team-coding", "localhost", "127.0.0.1", "0.0.0.0" [2] 测试 "http://s-3686137677310083075-773.team-coding:8080" → 应被识别为临时 [3] 测试 "https://bot.qihuyun.com" → 不应被识别为临时 [4] 测试 "http://localhost:8080" → 应被识别为临时 - 预期结果: 临时域名检测准确
TC-QR-04 [P1] 验证.env文件不存在的容错
- 前置条件: 模拟场景
- 步骤:
[1] 确认代码中
.env读取在try-except块中 [2] 验证当.env不存在或读取失败时不抛异常 [3] 验证最终仍走到兜底域名 - 预期结果: .env缺失不会导致启动失败
5. 测试用例统计
| 模块 | P0用例 | P1用例 | 合计 |
|---|---|---|---|
| Q1: 统一Webhook端点 | 4 | 2 | 6 |
| Q2: 消息组件系统 | 6 | 2 | 8 |
| Q3: 平台注册机制 | 4 | 2 | 6 |
| Q4: MessageSession | 3 | 2 | 5 |
| Q5: Pipeline管道 | 5 | 3 | 8 |
| Q6: Redis事件总线 | 5 | 3 | 8 |
| Q7: 二维码URL | 2 | 2 | 4 |
| 合计 | 29 | 16 | 45 |
文档状态: 已完成
创建日期: 2026-06-01
基于: 后端代码功能分析 + 项目负责人指定优化范围