## Context 当前 `services/hotspot.py` 提供三个纯函数式的热点功能:`search_hotspots`(搜索)、`analyze_and_suggest`(LLM 分析)、`generate_from_hotspot`(基于热点生成文案)。分析结果在函数内部被渲染成 Markdown 后直接返回 UI,结构化数据(`hot_topics`、`suggestions` 等)即刻丢失。 `services/scheduler.py` 已有一套成熟的定时调度模式(`_scheduler_loop` + `threading.Event` + daemon thread),可以复用此模式实现热点自动采集。 `services/topic_engine.py` 的 `TopicEngine.recommend_topics(hotspot_data=...)` 已支持接收热点数据但从未被实际传入。 `services/config_manager.py` 使用单例 + JSON 持久化,新增配置节点零改动即可被 `cfg.get()` 读取。 ## Goals / Non-Goals **Goals:** - 让分析结果在内存中以结构化形式持续可用,供生成和选题引擎消费 - 让用户在热点 UI 中直接从分析出的建议列表选择选题,而非手动输入 - 提供后台自动热点采集,无需人工触发即可获得最新热点数据 - 将热点分析数据桥接到 TopicEngine,使智能选题获得热点维度加权 **Non-Goals:** - 不做热点数据持久化到磁盘(进程内缓存即可,重启后重新采集) - 不修改 LLM prompt 或分析逻辑(`LLMService.analyze_hotspots` 保持不变) - 不改造现有调度器架构(复用已有 `threading.Event` + loop 模式) - 不增加新的外部依赖 ## Decisions ### D1: 模块级状态 + RLock 存储分析结果 **选择**:在 `services/hotspot.py` 中新增 `_last_analysis: dict | None` 和对应的 `get_last_analysis()` / `set_last_analysis()` 线程安全接口,复用已有的 `_cache_lock`(RLock)。 **替代方案**: - 单独的 `HotspotState` 类 — 过度封装,模块内部状态无需类化 - 写入文件持久化 — 增加 IO 复杂度,热点数据本身时效性短,内存缓存足够 **理由**:与已有的 `_cached_proactive_entries` 模式一致,最小改动,线程安全由已有的 `_cache_lock` 覆盖。 ### D2: `analyze_and_suggest` 副作用写入状态 **选择**:在 `analyze_and_suggest` 函数中,调用 `svc.analyze_hotspots()` 获得 `analysis` dict 后,先调用 `set_last_analysis(analysis)` 缓存,再渲染 Markdown 返回 UI。 **理由**:无需改变函数签名和返回值,对现有 UI 绑定零破坏。 ### D3: 热点选题下拉组件 **选择**:在热点探测 UI(`ui/app.py`)中新增 `gr.Dropdown`(选题下拉),当 `analyze_and_suggest` 完成后动态更新下拉选项为 `suggestions` 的 `topic` 列表。选中后写入 `topic_from_hot` Textbox。 **替代方案**: - 用 Radio 按钮 — 选项数量不固定,Dropdown 更适合 - 保持现有 Textbox 手动输入 — 无法利用分析出的建议 **理由**:Gradio `gr.Dropdown` 支持 `gr.update(choices=...)` 动态更新,与已有的笔记列表下拉模式一致。 ### D4: `generate_from_hotspot` 增强上下文 **选择**:新增可选参数 `analysis_summary: str = None`。若提供,将其拼入 `reference_notes` 前部(结构化摘要 + 原始片段),总长度仍限制在 3000 字符以内。函数内部同时尝试从 `get_last_analysis()` 自动获取摘要。 **理由**:向后兼容,现有调用方无需修改。 ### D5: TopicEngine 桥接 **选择**:新增独立函数 `feed_hotspot_to_engine(topic_engine: TopicEngine)` 在 `services/hotspot.py` 中。该函数读取 `get_last_analysis()`,调用 `topic_engine.recommend_topics(hotspot_data=data)`。 **替代方案**: - 在 `TopicEngine` 中直接引用 `hotspot.get_last_analysis()` — 导致循环依赖风险 - 在 UI 层手动传递 — 增加 UI 复杂度 **理由**:单向依赖(hotspot → topic_engine),职责清晰。 ### D6: 自动采集任务集成到调度器 **选择**:在 `services/scheduler.py` 中新增独立的 `_hotspot_collector_loop` + `start_hotspot_collector` / `stop_hotspot_collector`,复用现有的 `threading.Event` + daemon thread 模式。与已有 `_learn_scheduler_loop` 模式完全对齐。 **配置节点**:`config.json` 新增 `hotspot_auto_collect` 对象: ```json { "hotspot_auto_collect": { "enabled": false, "keywords": ["穿搭", "美妆", "好物"], "interval_hours": 4 } } ``` **采集流程**:每个 interval → 遍历 keywords → 调用 `search_hotspots` + `analyze_and_suggest`(复用现有函数) → 结果自动写入 `_last_analysis` 状态 → 休眠至下个周期。 **替代方案**: - 合并到现有 `_scheduler_loop` — 该循环参数已经过多,耦合度太高 - 使用 APScheduler 等库 — 引入新依赖,不符合项目风格 **理由**:独立线程与已有调度器互不干扰,可独立启停。 ## Risks / Trade-offs - **内存状态丢失** → 进程重启后 `_last_analysis` 为空,自动采集线程会在首个 interval 后重新填充,可接受 - **多关键词分析结果覆盖** → 遍历多个 keywords 时后者覆盖前者的分析结果 → 采用合并策略:新分析的 `hot_topics` 和 `suggestions` 追加到已有列表并去重 - **LLM 调用频率** → 自动采集每个关键词都需调用一次 LLM → 通过 `interval_hours`(默认 4 小时)+ keywords 数量(默认 3 个)控制成本 - **线程安全竞态** → 手动分析和自动采集可能同时写入 `_last_analysis` → `_cache_lock`(RLock)已覆盖