zhoujie 4d83c0f4a9
Some checks failed
CI / Lint (ruff) (push) Has been cancelled
CI / Import Check (push) Has been cancelled
feat(scheduler): 新增热点自动采集功能并优化发布路径
- 新增热点自动采集后台线程,支持定时搜索关键词并执行 AI 分析,结果缓存至结构化状态
- 新增热点分析状态管理接口,提供线程安全的 `get_last_analysis` 和 `set_last_analysis` 方法
- 新增热点数据桥接函数 `feed_hotspot_to_engine`,将分析结果注入 TopicEngine 实现热点加权推荐
- 新增热点选题下拉组件,分析完成后自动填充推荐选题,选中后自动写入选题输入框
- 优化 `generate_from_hotspot` 函数,自动获取结构化分析摘要并增强生成上下文
- 新增热点自动采集配置节点,支持通过 `config.json` 管理关键词和采集间隔

♻️ refactor(queue): 实现智能排期引擎并统一发布路径

- 新增智能排期引擎,基于 `AnalyticsService` 的 `time_weights` 自动计算最优发布时段
- 新增 `PublishQueue.suggest_schedule_time` 和 `auto_schedule_item` 方法,支持时段冲突检测和内容分布控制
- 修改 `generate_to_queue` 函数,新增 `auto_schedule` 和 `auto_approve` 参数,支持自动排期和自动审核
- 重构 `_scheduler_loop` 的自动发布分支,改为调用 `generate_to_queue` 通过队列发布,统一发布路径
- 重构 `auto_publish_once` 函数,移除直接发布逻辑,改为生成内容入队并返回队列信息
- 新增队列时段使用情况查询方法 `get_slot_usage`,支持 UI 热力图展示

📝 docs(openspec): 新增内容排期优化和热点探测优化规范文档

- 新增 `smart-schedule-engine` 规范,定义智能排期引擎的功能需求和场景
- 新增 `unified-publish-path` 规范,定义统一发布路径的改造方案
- 新增 `hotspot-analysis-state` 规范,定义热点分析状态存储的线程安全接口
- 新增 `hotspot-auto-collector` 规范,定义定时热点自动采集的任务流程
- 新增 `hotspot-engine-bridge` 规范,定义热点数据注入 TopicEngine 的桥接机制
- 新增 `hotspot-topic-selector` 规范,定义热点选题下拉组件的交互行为
- 更新 `services-queue`、`services-scheduler` 和 `services-hotspot` 规范,反映功能修改和新增参数

🔧 chore(config): 新增热点自动采集默认配置

- 在 `DEFAULT_CONFIG` 中新增 `hotspot_auto_collect` 配置节点,包含 `enabled`、`keywords` 和 `interval_hours` 字段
- 提供默认关键词列表 `["穿搭", "美妆", "好物"]` 和默认采集间隔 4 小时

🐛 fix(llm): 增强 JSON 解析容错能力

- 新增 `_try_fix_truncated_json` 方法,尝试修复被 token 限制截断的 JSON 输出
- 支持多种截断场景的自动补全,包括字符串值、数组和嵌套对象的截断修复
- 提高 LLM 分析热点等返回 JSON 的函数的稳定性

💄 style(ui): 优化队列管理和热点探测界面

- 在队列生成区域新增自动排期复选框,勾选后隐藏手动排期输入框
- 在日历视图旁新增推荐时段 Markdown 面板,展示各时段权重和建议热力图
- 在热点探测 Tab 新增推荐选题下拉组件,分析完成后动态填充选项
- 在热点探测 Tab 新增热点自动采集控制区域,支持启动、停止和配置采集参数
2026-02-28 22:22:27 +08:00

100 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 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已覆盖