Backend Data Flow: AI Watching Summary
日期: 2026-07-14 用途: 向 Bangumi-Syncer 维护者澄清 AI 追番总结功能的后端数据流设计,作为
SDD/spec.md的补充说明。
1. 完整数据流(Cron → DB → LLM → Webhook)
mermaid
flowchart TD
subgraph Trigger["1. APScheduler Cron 触发"]
A["SummaryScheduler._run_job(job_id=1)"] --> B["从 config.ini [summary-1] 读取配置\n{enabled, name, cron, lookback_days,\n user_name, system_prompt,\n user_prompt_template, max_records}"]
end
B --> C["SummaryService.execute_job(job_config)"]
subgraph Service["2. SummaryService"]
C --> D["Step A: 计算时间范围\n date_from = now - lookback_days\n date_to = now"]
D --> E["Step B: 查询 sync_records 表"]
E --> F["Step C: 格式化记录为文本\n 2026-07-14 20:30 | 葬送的芙莉莲 | S1E10 | plex | success\n ..."]
F --> G["Step D: 渲染 Prompt 模板\n user_prompt_template.format(\n date_from, date_to, records,\n record_count, lookback_days)"]
G --> H["构建 messages:\n [{role:'system', content:...},\n {role:'user', content:...}]"]
end
H --> I["LLMClient.chat(messages)"]
subgraph LLM["3. LLMClient"]
I --> J["Step A: 读取 [llm] 配置\n {api_base, api_key, model,\n max_tokens, temperature, timeout}"]
J --> K["Step B: HTTP POST (重试 2 次, 退避 1s/3s)\n POST {api_base}/chat/completions\n Body: {model, messages, max_tokens, temperature}"]
K --> L["Step C: 解析响应\n choices[0].message.content → summary_text\n usage → {prompt_tokens, completion_tokens, total_tokens}"]
L --> M["Step D: 写入 llm_usage_logs 表\n (fire-and-forget)"]
end
M --> N["返回 summary_text 到 SummaryService"]
subgraph Notify["4. SummaryService (continued)"]
N --> O["Step A: 动态 notification type\n user_name 为空 → 'watching_summary'\n user_name='dad' → 'watching_summary_dad'"]
O --> P["Step B: 构建 data dict\n {timestamp, job_name, summary_text,\n date_range, record_count, ...}"]
P --> Q["Step C: 发送\n notifier.send_notification_by_type(\n notif_type, data)"]
end
Q --> R["5. Notifier"]
subgraph Deliver["5. Notifier"]
R --> S["匹配 webhook configs\n types 包含 notif_type → match"]
S --> T["构建 JSON payload\n 'watching_summary' in notif_type → True"]
T --> U["HTTP POST / SMTP → webhook/email 收到总结"]
end
subgraph DBs["Databases"]
E -.-> DB1[("SQLite: sync_records\n索引: idx_timestamp\n idx_user_name")]
M -.-> DB2[("SQLite: llm_usage_logs\n索引: idx_timestamp\n idx_job_id")]
end
style Trigger fill:#ffe6cc,stroke:#d79b00
style Service fill:#dae8fc,stroke:#6c8ebf
style LLM fill:#d5e8d4,stroke:#82b366
style Notify fill:#dae8fc,stroke:#6c8ebf
style Deliver fill:#e1d5e7,stroke:#9673a6
style DBs fill:#f5f5f5,stroke:#6666662. 多用户路由
mermaid
flowchart LR
subgraph Jobs["Summary Jobs"]
J1["Job 1: user_name=''\n notif_type = watching_summary"]
J2["Job 2: user_name='dad'\n notif_type = watching_summary_dad"]
J3["Job 3: user_name='kid'\n notif_type = watching_summary_kid"]
end
subgraph Webhooks["Webhook Configs"]
W1["[webhook-1]\n types = watching_summary_dad\n → 只收爸爸"]
W2["[webhook-2]\n types = watching_summary_dad,\n watching_summary_kid\n → 爸爸 + 孩子"]
W3["[webhook-3]\n types = all\n → 全部接收"]
end
J1 -->|" 'watching_summary' in types? "| W3
J2 -->|" 'watching_summary_dad' in types? "| W1
J2 -->|" 'watching_summary_dad' in types? "| W2
J3 -->|" 'watching_summary_kid' in types? "| W2
J3 -->|" 'watching_summary_kid' in types? "| W3
style Jobs fill:#dae8fc,stroke:#6c8ebf
style Webhooks fill:#e1d5e7,stroke:#9673a6匹配机制:types 字段是逗号分隔字符串,Notifier 用 notification_type in types(Python 字符串包含)做匹配。例如 "watching_summary_dad" in "watching_summary_dad,watching_summary" → True。
3. 数据存储架构
mermaid
flowchart TD
subgraph App["Bangumi-Syncer 应用层"]
SC["SummaryService"]
LC["LLMClient"]
LG["Logger"]
end
SC -->|"写入观看记录\n(现有能力)"| SR[("sync_records\n────────────\nid, timestamp, user_name,\ntitle, season, episode,\nsubject_id, status, source,\nmedia_type, bgm_title")]
SC -->|"读取观看记录\nget_records_in_date_range()"| SR
LC -->|"log_llm_usage()\n每次 LLM 调用后写入"| UL[("llm_usage_logs\n────────────\nid, timestamp, job_id,\njob_name, model, provider,\nprompt_tokens,\ncompletion_tokens,\ntotal_tokens, latency_ms,\nstatus, error_message")]
LG -->|"错误堆栈\n重试信息"| LOG[("log.txt\n────────────\n纯文本文件\n按 2MB 自动轮转")]
Dashboard["Dashboard 用量卡片"] -.->|"GET /llm/stats"| UL
SummaryPage["/summary 图表"] -.->|"GET /llm/stats"| UL
LogsPage["/logs 页面"] -.->|"GET /api/logs"| LOG
style SR fill:#f5f5f5,stroke:#666666
style UL fill:#d5e8d4,stroke:#82b366
style LOG fill:#f5f5f5,stroke:#666666
style App fill:#fff2cc,stroke:#d6b6564. 故障处理流程
mermaid
flowchart TD
Start["LLMClient.chat() 被调用"] --> Try["POST {api_base}/chat/completions"]
Try -->|成功| Parse["解析 response → ChatResponse"]
Try -->|失败| Retry1["重试 #1 (等 1s)"]
Retry1 -->|成功| Parse
Retry1 -->|失败| Retry2["重试 #2 (等 3s)"]
Retry2 -->|成功| Parse
Retry2 -->|失败| Fail["返回 ChatResponse(content='')\nstatus=error"]
Parse --> LogSuccess["log_llm_usage(status='success')"]
Fail --> LogError["log_llm_usage(status='error',\nerror_message=异常信息)\nLogger.error(异常信息)"]
LogSuccess --> Return["返回给 SummaryService"]
LogError --> Skip["SummaryService 静默跳过\n不抛异常\n不影响 scheduler 其他任务"]
style Fail fill:#f8cecc,stroke:#b85450
style LogError fill:#f8cecc,stroke:#b85450
style Skip fill:#f8cecc,stroke:#b85450
style Parse fill:#d5e8d4,stroke:#82b366
style LogSuccess fill:#d5e8d4,stroke:#82b3665. Token 计数来源
mermaid
sequenceDiagram
participant SC as SummaryService
participant LC as LLMClient
participant API as OpenAI API
participant DB as llm_usage_logs
SC->>LC: chat(messages)
LC->>API: POST /chat/completions
API-->>LC: 200 OK { choices: [...], usage: { prompt_tokens: 850, completion_tokens: 420, total_tokens: 1270 } }
Note over LC: 从 response["usage"] 直接提取<br/>不本地计算 token
LC->>DB: INSERT (prompt_tokens=850, completion_tokens=420, total_tokens=1270, latency_ms=1234)
LC-->>SC: ChatResponse(content="今日爸爸...", usage={...})Token 数据来自 API 服务端的真实计费用量。准确性取决于服务端 tokenizer,但这是唯一可靠标准——所有 LLM 平台都按此计费。
