# 自动定时生成报告 — 方案设计 > **需求(已锁定)**:前端提供「手动 / 自动」两种模式。 > - **手动**:保持现状不变 —— 用户选时间段,点一下,跑 AI 分析 + 生成报告。 > - **自动**:用户设置一个间隔(如每 30 分钟 / 每小时 / 每天),系统到点**自动**跑「AI 分析 + 生成报告」全流程,无需人工点。 --- ## 一、关键结论:地基已经存在,不用从零搭 调研现有代码后发现,自动模式的基础设施**已经埋好了,只是之前被主动关掉**: | 已有的东西 | 位置 | 现状 | |-----------|------|------| | 后台调度器 APScheduler | [scheduler.py](chatlog_fastAPI/scheduler.py) | 已在 `lifespan` 启动([main.py:43](chatlog_fastAPI/main.py#L43)),目前只做账号/数据库切换检测 | | `groups.poll_interval` 字段 | [database.py:117](chatlog_fastAPI/database.py#L117) | 默认 300 秒,建群时可传([groups.py:12](chatlog_fastAPI/routers/groups.py#L12)),但**目前未被使用** —— 正好用来存「多久自动一次」 | | `groups.cursor_seq` 字段 | [database.py:115](chatlog_fastAPI/database.py#L115) | 为增量游标预留,**目前未被使用** | | `register_poll_job` 轮询注册 | [scheduler.py:16](chatlog_fastAPI/scheduler.py#L16) | **已废弃成空函数** | **最关键的一条线索** —— [scheduler.py](chatlog_fastAPI/scheduler.py) 顶部注释: > APScheduler — 仅保留 wxid/数据库切换检测。 > (不再运行任何 AI 分类轮询:AI 分析改为用户手动按时间窗口触发) 这说明自动轮询曾经跑过,后来被撤掉、改成了现在的「手动」模式。**撤掉的原因,正是新自动模式必须解决的坑。** --- ## 二、手动模式现在是怎么跑的(自动模式要复用它) 理解现状是设计自动模式的前提。手动模式的完整链路: ``` 前端 ChatlogPage 选时间段 → POST /api/groups/{id}/init {start_time, end_time} └─ run_classify_window(group_id, start_ts, end_ts) [topic_engine.py:960] 1. 全量拉取该时间段的消息 2. _delete_ai_topics 删掉旧 AI 话题 ← 注意这里是「全删重建」 3. 分批 LLM 分类 → 合并 → 补充分配 → 落库 topics (报告仍需用户在前端对每个话题点「生成」) └─ run_summarize(topic_id) [summary_engine.py:471] 从 chatlog 拉回话题消息原文 → LLM 生成 Markdown → 写 knowledge_docs ``` > 注意:手动模式目前**分类和报告是分两步**的。分类出话题后,报告要用户在前端逐个点。 > 自动模式的「全自动」就体现在:分类完,自动把需要的话题也一并生成报告,不用人点。 --- ## 三、必须正视的三个约束(也是当年撤掉自动轮询的原因) ### 约束 ① 分类是全局串行的,一次只能跑一个群 [topic_engine.py:26-27](chatlog_fastAPI/services/topic_engine.py#L26-L27): ```python _classify_lock = asyncio.Lock() _classifying_group: int | None = None ``` [groups.py:70-72](chatlog_fastAPI/routers/groups.py#L70-L72):只要有群在分析,手动 `/init` 直接返回 `409 已有群正在分析`。 **影响**:多个群各自定时,会互相撞锁。 **对策**:自动调度**串成一条队列**,并且**自动任务要让位于手动**(用户手点时不被自动任务卡住)。 ### 约束 ② 分类是「全量重跑」,不是增量 [topic_engine.py:1033](chatlog_fastAPI/services/topic_engine.py#L1033):每次分类先 `_delete_ai_topics` 把旧 AI 话题**全删重建**。 **影响**:自动模式如果每轮都对一个大窗口(如最近 7 天)重跑,每轮都把这些消息**全部重新喂 LLM**。间隔越短,成本越爆。**这几乎可以肯定是当年撤掉自动轮询的主因。** **对策**:见第四节的窗口策略 —— 这是本方案唯一需要你拍板的关键点。 ### 约束 ③ 自动跑报告 = 话题数 × LLM 调用 全自动生成报告时,每个有更新的话题都要调一次 LLM。 **对策**:报告只对**本轮真正有新消息变动的话题**生成/更新,不要把所有历史话题每轮重刷。 --- ## 四、唯一需要你拍板的点:自动模式每轮分析「哪段消息」 这是整个方案的核心,直接决定**成本**和**改动量**。三个候选: ### 方案 A:自上次运行至今(增量按时间)—— **推荐** 每轮窗口 = `[上次运行时间, 现在]`,只分析这段时间的新消息。 - **成本**:低且稳定,只跟「新消息量」相关,跟历史总量无关。 - **要解决的冲突**:现有 `run_classify_window` 会 `_delete_ai_topics` 全删。增量模式下**不能删历史话题**,要把新消息**补充归并**到已有话题或新建话题(复用现有 `_supplement_assignments` / `_coalesce_device_issue_topics` 的思路)。 - **改动量**:中。是本方案最核心的改造。 ### 方案 B:固定回看窗口(每轮全量重跑最近 N 天) 每轮窗口 = `[现在 - N 天, 现在]`,直接复用现有 `run_classify_window`(含全删重建)。 - **成本**:随间隔频率线性上升,且每轮重复分析窗口内所有消息。 - **优点**:几乎不用改分类逻辑,最快跑通。 - **代价**:① 成本高;② 超出窗口的历史话题会被删掉(因为全删重建只覆盖窗口内消息)。**不适合做知识库沉淀。** ### 方案 C:混合(增量为主 + 每天全量重跑一次) 平时走方案 A 增量;每天凌晨全量重跑一次,重组当天话题、提升分类精度。 - **成本**:介于 A、B 之间。 - **优点**:兼顾成本与分类质量(增量归并精度略低于全量重组,每天校正一次)。 - **改动量**:最大(A 的全部 + 一个定时全量任务)。 > **我的建议**:先按**方案 A** 落地(成本可控、能沉淀知识库、符合"定时增量出报告"的直觉)。 > 若后续发现增量归并的分类精度不够,再加方案 C 的每日全量校正。 > **方案 B 不建议**,除非你只想快速验证、且能接受历史被覆盖。 --- ## 五、推荐架构(按方案 A:增量 + 全自动 + 每群独立间隔) 核心思路:**一个 tick 调度器扫表,挑到期的群丢进串行队列,逐个增量处理并出报告。** ``` APScheduler 每 60 秒 tick 一次(单个 job,不是每群一个) │ ├─ 扫描所有 groups,挑出满足条件的群: │ auto_enabled = 1 且 now - last_run_at >= poll_interval │ └─ 把到期的群按顺序逐个 await 处理(串行,复用 _classify_lock) │ └─ 对每个群 run_auto_analyze(group_id): 1. 读 groups.cursor_seq,只拉游标之后的新消息(get_messages 已支持 min_seq) 2. 无新消息 → 更新 last_run_at,结束 3. 增量分类:把新消息归并进已有话题 / 新建话题(不删历史) 4. 对「本轮有新增消息的话题」逐个 run_summarize 生成/更新报告 5. 更新 cursor_seq = 本轮最大 seq,last_run_at = now ``` ### 为什么这样设计 | 设计点 | 原因 | |--------|------| | **单 tick job 扫表**,而非每群一个 APScheduler job | 新增/删群、改间隔都不用动调度器;天然串行,避开并发撞锁 | | **用 `cursor_seq` 增量** | 字段现成,从根本上解决「全量重跑」的成本问题 | | **串行队列 + 自动让位手动** | 符合现有 `_classify_lock` 全局串行约束,用户手点时优先 | | **报告只生成有变动的话题** | 避免每轮把所有历史话题重刷 LLM | | **间隔存 `groups.poll_interval`** | 字段现成,天然支持「每群单独设多久自动一次」 | --- ## 六、具体改动清单 ### 6.1 数据库 schema([database.py](chatlog_fastAPI/database.py)) `groups` 表加一个字段(用现有 `PRAGMA table_info` 迁移模式补列): ```sql auto_enabled INTEGER DEFAULT 0 -- 是否开启该群的自动分析 last_run_at DATETIME -- 上次自动跑的时间(判断是否到期) ``` `poll_interval`(间隔)、`cursor_seq`(增量游标)已存在,直接复用。 ### 6.2 调度器([scheduler.py](chatlog_fastAPI/scheduler.py)) - 新增 `_auto_analyze_tick()` job,`interval` 60 秒。 - tick 内扫 `groups`,挑 `auto_enabled=1 AND (last_run_at IS NULL OR now - last_run_at >= poll_interval)`。 - 到期的群**逐个 `await`** 处理(串行,不用 `create_task` 并发)。 - 处理前检查 `get_classifying_group()`,若手动任务在跑则本轮跳过,下轮再来(让位手动)。 ### 6.3 增量分析逻辑([topic_engine.py](chatlog_fastAPI/services/topic_engine.py)) 新增 `run_auto_analyze(group_id)`(与现有 `run_classify_window` 并存,不破坏手动): - 读 `cursor_seq`,调 `chatlog_client.get_messages(talker, min_seq=cursor_seq+1, ...)` 只拉新消息(`get_messages` 已支持 `min_seq`,见 [chatlog_client.py:77](chatlog_fastAPI/services/chatlog_client.py#L77))。 - **不调用** `_delete_ai_topics`(这是和手动模式的关键区别)。 - 把新消息走「补充分配」逻辑归入已有话题或新建话题(复用 `_supplement_assignments`、`_coalesce_device_issue_topics`)。 - 收集本轮被改动 / 新建的 `topic_id` 列表。 - 更新 `cursor_seq`、`last_run_at`。 ### 6.4 自动报告([summary_engine.py](chatlog_fastAPI/services/summary_engine.py)) - `run_auto_analyze` 分类完后,对「本轮有变动的话题」逐个 `await run_summarize(topic_id, topic)`。 - `run_summarize` 已支持「已存在则更新」([summary_engine.py:444-454](chatlog_fastAPI/services/summary_engine.py#L444-L454)),直接复用。 ### 6.5 接口([groups.py](chatlog_fastAPI/routers/groups.py)) - `GroupPatch` 增加 `auto_enabled`、`poll_interval`,`patch_group` 支持更新。 - (可选)`GET /api/groups/{id}/auto-status`:返回 `auto_enabled / poll_interval / last_run_at / 下次预计运行时间`,给前端展示状态。 ### 6.6 前端(群管理 / [SettingsPage.jsx](chatlab-web/frontend/src/pages/SettingsPage.jsx)) - 每个群一个「自动分析」开关 + 间隔下拉(如 30 分钟 / 1 小时 / 3 小时 / 每天)。 - 手动模式 UI 保持不变(现有「查询时间段 + 手动分析」照旧)。 - 调用 `patchGroup(groupId, { auto_enabled, poll_interval })`([api/index.js:232](chatlab-web/frontend/src/api/index.js#L232) 已有 `patchGroup`)。 - (可选)展示「上次自动分析时间 / 下次预计时间」。 --- ## 七、风险与注意点 1. **LLM 成本是头号风险**:间隔越短、群越多,成本越高。方案 A 增量 + 报告只刷变动话题已是最省的组合;前端间隔下拉**建议最小 30 分钟**,不要给「每 5 分钟」这类档位。 2. **自动要让位手动**:tick 处理前必须检查 `get_classifying_group()`,避免占着锁让用户手动分析报 409。 3. **chatlog 服务可能未就绪**:tick 要捕获 `MessageIndexNotReady`([chatlog_client.py:17](chatlog_fastAPI/services/chatlog_client.py#L17)),跳过本轮、**不更新游标**,下轮重试。 4. **账号切换**:`cursor_seq` 按 `groups` 表存,数据库随微信账号切换([database.py:80](chatlog_fastAPI/database.py#L80)),游标天然隔离。但要确认 tick 用的是 `get_active_db_path()` 当前库。 5. **增量分类精度权衡**:增量归并(新消息往已有话题靠)比全量重组精度略低。若要求高,用方案 C 每日全量校正一次。 6. **首次开启自动的冷启动**:群刚开自动且 `cursor_seq=0` 时,第一轮会把全部历史拉进来分析。建议开启自动时让用户先手动跑一次(或把 `cursor_seq` 初始化为当前最新 seq),避免首轮巨量分析。 --- ## 八、建议的落地顺序 1. **先确认第四节的窗口策略**(推荐方案 A)。 2. schema 加字段 + `groups.py` 接口打通(小改动,先让「开关 + 间隔」能存能读)。 3. 调度器接 tick + 串行队列骨架(先只 `log`「本应处理 group X」,不真跑 LLM,验证调度和让位逻辑正确)。 4. 接增量分类 `run_auto_analyze`(先不出报告,验证话题增量归并正确)。 5. 接自动报告。 6. 前端配置 UI(自动开关 + 间隔下拉)。 7. 小间隔实测 LLM 成本与分类质量,再决定前端开放哪些间隔档位、是否需要方案 C。 --- *本文档为方案设计,未改动任何代码。确认第四节窗口策略后即可进入实现。*