Skip to content

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:#666666

2. 多用户路由 ​

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:#d6b656

4. 故障处理流程 ​

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:#82b366

5. 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 平台都按此计费。