# 实施计划：P14 — AI 模块改造 — DashScope 迁移 + 调度器完善

## 概述

按依赖关系从底层到顶层逐步实施：环境变量/配置 → DashScope 客户端 → 防护层（熔断/限流/预算） → 运行日志 → 调度器 → API 路由 → 服务适配 → ETL 触发 → 数据库迁移 → 收尾。每个任务构建在前序任务之上，确保无孤立代码。

## 任务

- [x] 1. 环境变量与配置基础
  - [x] 1.1 重写 `apps/backend/app/ai/config.py`，实现 `AIConfig` dataclass
    - 定义所有 `DASHSCOPE_*` 环境变量字段和 `INTERNAL_API_TOKEN`
    - 实现 `from_env()` 类方法，缺失必需变量时立即抛出异常
    - 删除所有 `BAILIAN_*` 引用
    - _需求: 2.1, 2.2, 2.3, 2.5_

  - [x] 1.2 编写属性测试：环境变量校验完整性
    - **Property 2: 环境变量校验完整性**
    - 对任意必需变量子集缺失，`from_env()` 应抛出异常，不返回含空字符串的配置
    - **验证: 需求 2.5**

  - [x] 1.3 更新 `.env`、`.env.template`、`pyproject.toml`
    - `.env` / `.env.template`：`BAILIAN_*` → `DASHSCOPE_*`，新增 `DASHSCOPE_WORKSPACE_ID`、`INTERNAL_API_TOKEN`
    - `pyproject.toml`：移除 `openai` 依赖，新增 `dashscope` 依赖
    - _需求: 2.4, 1.7_

- [x] 2. DashScopeClient 核心客户端
  - [x] 2.1 创建 `apps/backend/app/ai/dashscope_client.py`，实现 `DashScopeClient` 类
    - `call_app()` — App2~8 单轮调用，`asyncio.to_thread()` 包装，返回 `(parsed_json, tokens_used, new_session_id)`
    - `call_app_stream()` — App1 流式调用，线程消费同步迭代器 + `asyncio.Queue` 桥接 async generator
    - `_call_with_retry()` — 指数退避重试（1s→2s→4s），HTTP 4xx 不重试，5xx/超时/连接错误重试
    - 非合法 JSON 响应纯重试（最大 3 次），不做本地修复
    - _需求: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6_

  - [x] 2.2 定义异常层级
    - `DashScopeError` 基类及子类：`DashScopeApiError`、`DashScopeAuthError`、`DashScopeTimeoutError`、`DashScopeJsonParseError`、`CircuitOpenError`、`RateLimitExceededError`、`BudgetExceededError`
    - _需求: 1.5, 5.6, 6.3, 7.3_

  - [x] 2.3 编写属性测试：重试策略正确性
    - **Property 1: 重试策略正确性**
    - 4xx 立即抛出不重试，5xx/超时/连接错误最多重试 3 次，非法 JSON 触发重试
    - **验证: 需求 1.5, 1.6**

  - [x] 2.4 编写属性测试：Application API 响应解析
    - **Property 20: Application API 响应解析**
    - 合法 JSON 正确解析为 dict，非法 JSON 触发重试
    - **验证: 需求 4.4**

- [x] 3. 检查点 — 确保所有测试通过
  - 确保所有测试通过，ask the user if questions arise.

- [x] 4. 防护层：熔断器、限流器、Token 预算
  - [x] 4.1 创建 `apps/backend/app/ai/circuit_breaker.py`，实现 `CircuitBreaker` 类
    - 按 `app_id` 独立计数，`_BreakerState` 内部状态
    - `check()` / `record_success()` / `record_failure()` 方法
    - 状态机：CLOSED → OPEN（连续 5 次失败）→ HALF_OPEN（60 秒后）→ CLOSED/OPEN
    - _需求: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6_

  - [x] 4.2 编写属性测试：熔断器 app_id 隔离 + 状态机转换
    - **Property 5: 熔断器 app_id 隔离**
    - **Property 6: 熔断器状态机转换**
    - 不同 app_id 互不影响；状态转换符合 CLOSED→OPEN→HALF_OPEN→CLOSED/OPEN 规则
    - **验证: 需求 5.1, 5.2, 5.3, 5.4, 5.5**

  - [x] 4.3 创建 `apps/backend/app/ai/rate_limiter.py`，实现 `RateLimiter` 类
    - 滑动窗口内存计数器
    - `check_user_rate()` — App1 每用户每分钟 10 次
    - `check_store_rate()` — App2~8 每门店每小时 100 次
    - _需求: 6.1, 6.2, 6.3, 6.4_

  - [x] 4.4 编写属性测试：限流器窗口控制
    - **Property 7: 限流器窗口控制**
    - 窗口内未超阈值允许，超阈值拒绝；窗口外历史不影响当前判断
    - **验证: 需求 6.1, 6.2**

  - [x] 4.5 创建 `apps/backend/app/ai/budget_tracker.py`，实现 `BudgetTracker` 类
    - `check_budget()` 从 `ai_run_logs` 聚合日/月 token 消耗
    - 日预算 100,000 / 月预算 2,000,000
    - 返回 `BudgetStatus(allowed, daily_used, monthly_used, reason)`
    - _需求: 7.1, 7.2, 7.3, 7.4, 7.5_

  - [x] 4.6 编写属性测试：Token 预算检查正确性
    - **Property 8: Token 预算检查正确性**
    - 日聚合 = 当日 success 记录 tokens_used 之和；超限时 allowed=false
    - **验证: 需求 7.1, 7.3**

- [x] 5. AI 运行日志服务
  - [x] 5.1 创建 `apps/backend/app/ai/run_log_service.py`，实现 `AIRunLogService` 类
    - `create_log()` — 创建 pending 记录
    - `update_running()` / `update_success()` / `update_failed()` / `update_timeout()` — 状态转换
    - `get_daily_token_usage()` / `get_monthly_token_usage()` — 聚合查询
    - `request_prompt` 截断为前 2000 字符
    - _需求: 16.1, 16.2, 16.3, 16.4, 16.5, 16.6_

  - [x] 5.2 编写属性测试：AI 运行日志状态机 + Prompt 截断
    - **Property 18: AI 运行日志状态机**
    - **Property 19: Prompt 截断不变量**
    - 状态转换正确（pending→running→success/failed/timeout）；prompt ≤ 2000 字符
    - **验证: 需求 16.1, 16.2, 16.3, 16.4, 16.5, 16.6**

- [x] 6. 检查点 — 确保所有测试通过
  - 确保所有测试通过，ask the user if questions arise.

- [x] 7. Dispatcher 调度器重写
  - [x] 7.1 重写 `apps/backend/app/ai/dispatcher.py`，实现 `AIDispatcher` 类
    - 移除所有 `asyncio.run()` 和 `asyncio.new_event_loop()` 调用
    - 所有入口改为 `async def`，用 `asyncio.create_task()` 发起后台任务
    - 超时用 `asyncio.wait_for()`
    - 集成 CircuitBreaker、RateLimiter、BudgetTracker
    - `_run_step()` — 单步执行：熔断检查→限流检查→预算检查→调用→记录日志
    - _需求: 8.1, 8.2, 8.3, 8.4_

  - [x] 7.2 实现事件触发链编排
    - `_handle_consumption()` — 消费事件：App3→App8→App7（无助教）/ +App4→App5（有助教）
    - `_handle_note()` — 备注事件：App6→App8
    - `_handle_task_assigned()` — 任务分配：App4→App5
    - `_handle_dws_completed()` — DWS 完成：App2 预生成（8 个时间维度）
    - 调用链某步失败时记录错误，后续步骤使用已有缓存继续
    - _需求: 9.1, 9.2, 9.3, 9.4, 9.5, 11.1, 11.2, 11.3, 11.4_

  - [x] 7.3 实现幂等去重逻辑
    - `_check_dedup()` — 按 `(event_type, member_id, site_id, date)` 去重
    - `is_forced=true` 时跳过去重检查
    - 写入 `ai_trigger_jobs` 记录
    - _需求: 12.1, 12.2_

  - [x] 7.4 编写属性测试：事件类型到调用链映射
    - **Property 9: 事件类型到调用链映射**
    - 各事件类型映射到正确的 App 调用链
    - **验证: 需求 9.1, 9.2, 9.3, 11.1**

  - [x] 7.5 编写属性测试：调用链容错不中断
    - **Property 10: 调用链容错不中断**
    - 任意步骤失败后后续步骤仍继续执行
    - **验证: 需求 9.4**

  - [x] 7.6 编写属性测试：事件去重与强制执行
    - **Property 12: 事件去重与强制执行**
    - 重复自动事件跳过；`is_forced=true` 正常执行
    - **验证: 需求 12.1, 12.2**

- [x] 8. 检查点 — 确保所有测试通过 ✅ 76 passed
  - 确保所有测试通过，ask the user if questions arise.

- [x] 9. API 路由与端点
  - [x] 9.1 创建 `apps/backend/app/routers/internal_ai.py`，实现内部触发 API
    - `POST /api/internal/ai/trigger` 端点
    - `TriggerRequest` Pydantic 模型（event_type, connector_type, site_id, member_id, payload）
    - `verify_internal_token()` 依赖注入，校验 `Authorization: Internal-Token {token}`
    - 写入 `ai_trigger_jobs` 后立即返回 `{trigger_job_id, status: "pending"}`，后台异步执行
    - 在 FastAPI app 中注册路由
    - _需求: 10.1, 10.2, 10.3, 10.4, 10.5_

  - [x] 9.2 编写属性测试：内部 API 认证
    - **Property 11: 内部 API 认证**
    - token 缺失/不匹配返回 401；匹配时正常处理
    - **验证: 需求 10.2, 10.3**

  - [x] 9.3 适配 `apps/backend/app/routers/xcx_chat.py` SSE 端点
    - 替换 `_get_bailian_client()` → `_get_dashscope_client()`
    - 调用 `client.call_app_stream(app_id, prompt, session_id, biz_params)`
    - 构建 `prompt` + `biz_params`（User_ID, Role, Nickname）
    - 保持 SSE 事件格式：`event: message` / `event: done` / `event: error`
    - 流结束后记录 `ai_run_logs`
    - _需求: 15.1, 15.2, 15.3, 15.4, 3.3_

  - [x] 9.4 编写属性测试：SSE 事件流格式
    - **Property 17: SSE 事件流格式**
    - 零或多个 `event: message`，最终以恰好一个 `done` 或 `error` 结束
    - **验证: 需求 15.2**

- [x] 10. 服务层适配
  - [x] 10.1 适配 `apps/backend/app/services/ai/chat_service.py` — session_id 双轨逻辑
    - 新对话生成 session_id（格式 `conv_{conversation_id}_{created_timestamp}`）
    - 每条消息同时写入本地 `ai_messages`
    - session_id 过期时从本地加载最近 20 条历史重建
    - 保持对话复用规则：task 无时限、customer/coach 3 天、general 新建
    - _需求: 3.1, 3.2, 3.4, 3.5, 3.6_

  - [x] 10.2 编写属性测试：session_id 格式 + 对话复用规则
    - **Property 3: session_id 格式不变量**
    - **Property 4: 对话复用规则**
    - session_id 匹配 `^conv_\d+_\d+$`；复用规则按入口类型正确判断
    - **验证: 需求 3.1, 3.6**

  - [x] 10.3 适配 `apps/backend/app/services/ai/cache_service.py` — 缓存状态与过期策略
    - 新增 `status` 字段处理（valid/expired/invalidated/generating）
    - 写入前设 `generating`，完成后设 `valid`
    - 查询仅返回 `status='valid'` 且未过期的记录
    - 按 App 类型设置过期时间（App2 当日 23:59:59、App3~5/7/8 七天、App6 三十天）
    - App2~8 每 App 保留最新 20,000 条，超限清理最旧记录
    - _需求: 13.1, 13.2, 13.3, 13.4, 13.5_

  - [x] 10.4 编写属性测试：缓存过期策略 + 查询过滤 + 保留上限
    - **Property 14: 缓存过期策略正确性**
    - **Property 15: 缓存查询过滤**
    - **Property 16: 缓存保留上限**
    - 过期时间匹配策略；查询仅返回 valid 且未过期；每 App ≤ 20,000 条
    - **验证: 需求 13.1, 13.4, 13.5**

  - [x] 10.5 实现 App8 幂等写入 `member_retention_clue`
    - 事务中 DELETE + INSERT，同一 member 同一天只执行一次
    - 事务失败自动回滚，记录错误到 `ai_trigger_jobs.error_message`
    - _需求: 12.3, 12.4_

  - [x] 10.6 编写属性测试：App8 幂等写入
    - **Property 13: App8 幂等写入**
    - 多次执行后同一 member 同一天记录数恒为 1
    - **验证: 需求 12.3**

- [x] 11. 检查点 — 确保所有测试通过 ✅ 106 passed
  - 确保所有测试通过，ask the user if questions arise.

- [x] 12. ETL 触发集成
  - [x] 12.1 修改 `apps/etl/connectors/feiqiu/tasks/` 相关 DWS 任务
    - DWS 任务完成后通过 `requests` 发送 `POST /api/internal/ai/trigger`
    - 携带 `Authorization: Internal-Token {INTERNAL_API_TOKEN}` Header
    - 事件类型：`dws_completed`（App2 预生成）/ `consumption`（消费事件链）
    - 新增 `utils/ai_trigger.py` 工具函数 + `BaseDwsTask.AI_TRIGGER_EVENT` 类属性
    - `FinanceDailyTask` → `dws_completed`，`MemberConsumptionTask` → `consumption`
    - _需求: 10.6_

- [x] 13. 数据库迁移
  - [x] 13.1 编写迁移脚本 `db/zqyy_app/migrations/2026-03-22__p14_ai_module.sql`
    - CREATE TABLE `biz.ai_run_logs`（含 3 个索引）
    - CREATE TABLE `biz.ai_trigger_jobs`（含 3 个索引，含去重部分索引）
    - ALTER TABLE `biz.ai_conversations` ADD COLUMN `session_id`
    - ALTER TABLE `biz.ai_cache` ADD COLUMN `status` + CHECK 约束
    - 编写对应回滚脚本（逆序 DROP/ALTER）+ 验证 SQL（7 条）
    - _需求: 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7_

- [x] 14. 收尾：文档与集成验证
  - [x] 14.1 更新 BD 手册
    - 更新 `docs/database/BD_Manual_ai_tables.md`：新增 `ai_run_logs`、`ai_trigger_jobs` 表结构
    - 更新 `ai_cache.status` 和 `ai_conversations.session_id` 字段说明
    - _需求: 14.1, 14.2, 14.3, 14.4_

  - [x] 14.2 文档同步
    - 更新 `docs/prd/ai-app-prompts.md`：环境变量映射 BAILIAN_* → DASHSCOPE_*
    - 更新 `apps/backend/README.md`：AI 模块架构说明
    - 更新 `docs/DOCUMENTATION-MAP.md`：新增文档条目
    - _需求: 2.4_

- [x] 15. 最终检查点 — 确保所有测试通过 ✅ 505 passed（P14 全部 15 个测试文件通过，80 个预存失败均非 P14 相关）
  - 确保所有测试通过，ask the user if questions arise.

## 说明

- 标记 `*` 的子任务为可选（属性测试），可跳过以加速 MVP
- 每个任务关联了具体的需求编号，确保可追溯
- 属性测试任务标注了对应的 Property 编号和验证的需求条款
- 检查点确保增量验证，避免问题累积
- 实现语言：Python（与设计文档一致）
