Files
news/continuation.md
T
simon 2f2428aa9a fix: 日报摘要可靠性(去模型兜底+重试) 与取数逻辑优化
- llm/client: 移除内置默认模型兜底(deepseek-chat/qwen-plus), 模型必须显式配置否则报错
- reporter._llm_call: 指数退避重试(LLM_RETRY_TIMES 默认3 / LLM_RETRY_BACKOFF_SEC 默认2s)
- pipeline report: report_date 改为当天(原昨天+回溯3天)
- reporter._collect_news_events: 读当天+前一天目录, publish_time 30h 回溯(NEWS_LOOKBACK_HOURS=30), 统一时区
- reporter._collect_xwlb: 固定取 day_str 前一日(已播出联播), source_date 同步
- 公告/调研/互动保持近15日设置(CNINFO_DAYS_BACK), 不受 30h 影响
- 测试: 新增 30h回溯/带时区/重试/模型缺失/xwlb 前一日 用例
2026-08-05 08:34:02 +08:00

308 lines
13 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.
# continuation.md
> `checkpoint` @ 2026-08-05 08:30
---
## 当前状态
| 项目 | 值 |
| --- | --- |
| 新闻源 | 14 个(13 Web + 1 API: xwlb 新闻联播) |
| Qdrant | 本地文件模式 `data/qdrant_storage/` |
| 日报 | **M10 完成并已部署 pi5: 结构化入库 MySQL;日报按当天日期生成(新闻 30h 回溯 / xwlb 取前一日 / 公告调研近 15 日)** |
| DB 连接 | pi 上 systemd 服务 `a-share-db-tunnel` 常驻(0.0.0.0:13306 → doorcome.cn:3306);**pi5 直连 192.168.1.10:13306** |
| 调度器 | APSchedulersystemd `a-share-research.service`(pi5);每天 07:00 首次任务生成日报(12/18/22 点不生成) |
| LLM | `deepseek-v4-flash`(绝不允许擅自修改;模型必须显式配置,无内置兜底) |
| 服务器 | `pi@192.168.1.160`(生产)/ `pi@192.168.1.10`DB 隧道宿主) |
| 抓取方式 | js_render=false → httpx 直连;js_render=true → Playwright |
---
## 本次完成 (2026-08-05) — 日报可靠性修复与取数逻辑优化
**目标:** 解决日报 AI 摘要偶发失败;修正日报日期与 xwlb/新闻取数语义。
**1. AI 摘要可靠性(llm/client.py + scheduler/reporter.py):**
- 去掉内置默认模型兜底(`deepseek-chat`/`qwen-plus`),模型必须显式配置(`DEEPSEEK_MODEL`/`QWEN_MODEL``LLM_MODEL`),缺失即报错,避免静默用错模型
- `_llm_call` 增加指数退避重试:`_LLM_RETRY_TIMES`(默认 3 次)、`_LLM_RETRY_BACKOFF_SEC`(默认 2.0s,可 .env 覆盖),全部失败才抛异常
- 确认 AI 摘要模型:`deepseek` + `deepseek-v4-flash`(生产实测)
**2. 日报取数逻辑(scheduler/pipeline.py + reporter.py):**
- pipeline report 步骤:`report_date = date.today()`(原为昨天+回溯 3 天)
- `_collect_news_events`:读当天+前一天事件目录,按 `publish_time` 过滤最近 30 小时(`_NEWS_LOOKBACK_HOURS=30`);时区统一(naive 假定本地时区);无时间戳事件保留
- `_collect_xwlb`:固定查 `day_str` 前一日(《新闻联播》19:00 播出,早间日报只能取昨晚已播出的一期);`source_date` 同步为实际来源日
- **公告/调研/互动保持原设置:近 15 日(`STOCK_REPORT_DAYS=15`),irm 互动仍跳过**——未受 30h 改动影响
**验证(pi5):**
- 单测 9 个(30h 回溯/带时区时间戳/重试/模型缺失报错/xwlb 前一日)全部通过;全量 212 passed + 3 crawler 预存在失败
- 生产端到端 report_id=1912026-08-05):news 20 + cninfo 20 + xwlb 3408-04 联播),AI 摘要 2076 字
- 生产服务已重启生效
**本次代码尚未 git 提交(见待办)。**
---
## 本次完成 (2026-08-03 22:00) — pi5 部署与生产测试
**操作:** M10 代码全量同步 pi5 + 生产环境验收(所有测试在 pi5 执行,Mac 不再作为测试环境)。
**文件同步:** rsync 本地 → `pi5:/home/pi/news/`(排除 .venv/data/logs/.git/.env/configs/*.yaml);清除 pi5 根目录 6 月 17 日旧版散文件(已 tar 备份 /tmp/news_backup_20260803.tar.gz);pi5 `uv sync` 装 pymysql。
**DB 隧道修复:** pi 原 autossh 参数 `-L 13306:0.0.0.0:3306` 未生效(0.0.0.0 被当远端目标),改为 `-L 0.0.0.0:13306:127.0.0.1:3306` 并持久化为 systemd 服务 `a-share-db-tunnel`enabled + active)。
**测试结果(pi5):**
- 全量 pytest**208 passed, 3 failed**3 个失败均为 crawler retry mock 预先存在问题,与 M10 无关)
- `report-import` 幂等:已存在文件正确 skipped
- 生产端到端:`a-share report --date 20260802/20260803` → report_id=181/182 入库成功(AI 摘要正常,57/58 事件)
- DB 总量:finance 52 + intl 128 = 180 行(历史 177 + 端到端测试 2 + 本机 1)
- 生产服务 `a-share-research` 已重启 active,22:00 定时任务起用新代码
**已知问题:**
- `tests/test_crawler.py` 3 个 retry 测试失败(预先存在,crawl4ai mock 行为)
- Mac 本机 anaconda/uv python 出站到 192.168.1.10 被拦截(EHOSTUNREACHnc/bash/系统 python 正常)——仅影响本机,pi5 不受影响;Mac 本地用 `127.0.0.1` + ssh 隧道绕过
- 生产 pi5 的 `.env``NEWS_DB_HOST=192.168.1.10`(直连);Mac 本地 `.env``127.0.0.1`(隧道)——**两处 .env 不同,勿互相覆盖**
---
## 本次完成 (2026-08-03) — M10 日报结构化入库
**目标:** 日报前后端分离的数据层——日报内容结构化存入 MySQL(`news_` 前缀表),历史 178 份日报 HTML 解析入库;本项目不做 API/前端(用户另行实现)。
**表结构(myquant 库,见 docs/db_schema.md):**
- `news_report`:主表,唯一键 (report_date, report_type, file_name)`file_name=''` 表示新生成日报(每天每类型一行,重复生成覆盖)
- `news_event`:事件明细,section ∈ xwlb/news/cninfo/intl
**新增/修改文件:**
- `report_db/`models/schema/db):连接、建表、幂等 save_report
- `report_import/`parser/importer):历史 HTML 解析(表头驱动列映射)+ 批量导入
- `scheduler/reporter.py``generate_report()` 完全切换为结构化入库;`_render_html/_upload` 标记废弃保留
- `a_share_cli/main.py`:新增 `report-import` 子命令;`report` 命令改为入库提示
- `pyproject.toml``uv add pymysql`(唯一新增依赖)
- `.env`:新增 `NEWS_DB_*`(连接 127.0.0.1:13306,需 ssh 隧道)、`REPORT_HISTORY_DIR`
- `docs/report_db_design.md`(实现逻辑)、`docs/db_schema.md`(表结构,供 API/前端)
**验证结果:**
- 历史 178 份全部入库:scanned=178 imported=7(首轮)+170 skipped=171 failed=0DB 177 行(finance 49 + intl 1281 个跨目录同名文件被幂等合并)+ 事件 4222 条
- 端到端:`a-share report --date 20260616` → report_id=180 入库成功,无 HTML 产出
- 单测 24 个通过(parser 19 + builder 4 + models/import 补充)
**命令:**
```bash
ssh -L 13306:127.0.0.1:13306 pi # Mac 开发连库隧道
uv run a-share report --date YYYYMMDD # 生成日报入库
uv run a-share report-import # 历史导入(幂等)
```
**注意:** 本机 .env 无 LLM API keyAI 摘要会降级 WARNING(不影响入库);生产 pi5 需配置 NEWS_DB_PASSWORD 且解决 13306 隧道可达性(见待确认项)。
---
## 历史 (2026-07-17) — 禁用个股日报
**操作:** `STOCK_REPORT_TIME=` 设为空,`run_scheduler.py` 加空值守卫。
**文件:**
- `scripts/run_scheduler.py` L127-141`STOCK_REPORT_TIME` 为空时跳过注册并输出 `已禁用个股日报`
- Pi `.env``STOCK_REPORT_TIME=08:00``STOCK_REPORT_TIME=`
**恢复方法:** 改回 `STOCK_REPORT_TIME=08:00`,重启 `a-share-research`
---
## 本次完成 (2026-07-13) — eeo 反爬修复 + 静态直连 + 超时保护
### 问题
eeo(经济观察网)在 Pi 上用 Playwright 连续抓取后触发反爬,服务器故意不响应导致 `page.goto()` 超时(30s → 60s 均超时)。而在本机 Mac 上一切正常——反爬是 **IP 级别**的,Pi 的 IP 已被限流。
### 根因
eeo 是纯静态页面(`js_render: false`),但 Crawl4AI 始终走 Playwright。eeo 检测到 headless Chrome 的连续请求模式后,对后续请求挂起 TCP 连接直至超时。
### 修复
**`crawler/engine.py` — 三个改动:**
1. **新增 `_fetch_static()`**`js_render=False` 且无 `wait_for` 的源用 `httpx` 直连,绕过 Playwright 反爬。HTML → Markdown 用 `markdownify` + BeautifulSoup 清洗。
2. **新增 `_TimeoutGuard`**:同源连续超时 2 次后自动跳过剩余请求,防止整个 pipeline 被单一故障源拖死。
3. **`_crawl_once` 分流逻辑**
```
js_render=False 且无 wait_for → _fetch_static() (httpx)
其他 → Playwright
```
**`configs/sources.yaml` — eeo 源两处调整:**
- 删除 `wait_for: "css:body"`(静态页面无需等待 CSS 选择器)
- `page_timeout_ms: 30000 → 60000`(恢复 Crawl4AI 默认值)
### 测试结果
| 环境 | Playwright (修复前) | httpx (修复后) |
|------|---------------------|-----------------|
| Mac 本地 | 全部通过 | 36 篇 / 7 秒 |
| Pi 服务器 | **前 3 个请求全部 15s 超时** | 36 篇 / 9 秒,0 失败 |
### 验证方法
Pi 上本地复现了反爬(Playwright 前 3 个 URL 全部超时),同批 URL 用 httpx 全部瞬间返回——证明方案有效。
---
## 历史 (2026-06-24)
### 1. 环境变量全面审计与修复
**触发:** 服务器 `dedup` 步骤超时 120s。
**修复 6 个文件:**
| # | 文件 | 修改 |
|---|------|------|
| 1 | `scheduler/pipeline.py` | 超时解析重写:`TIMEOUT_{NAME}` > `PIPELINE_STEP_TIMEOUT` > 硬编码 > 1800s |
| 2 | `a_share_cli/main.py` | 新增 `_resolve_timeout()`,CLI 命令超时改为读环境变量 |
| 3 | `crawler/cninfo.py` | `CNINFO_PDF_BASE` 从硬编码改为 `os.environ.get()` |
| 4 | `scheduler/reporter.py` | `STOCK_REPORT_DAYS` 默认值 7→15`max_tokens` 600→1500 |
| 5 | `.env` | 清除 10 个死配置;`EMBEDDING_MODEL`→`LOCAL_EMBEDDING_MODEL`;补全遗漏参数 |
| 6 | `.env.example` | 重写为仅包含实际生效配置 |
**审计结果:**
| 类别 | 数量 | 处理 |
|------|------|------|
| ✅ 正常工作 | 23 | — |
| ❌ 死配置 | 10 | 已清除 |
| ⚠️ 默认值不一致 | 1 | 已统一 |
| 🔧 硬编码 | 1 | 已修复 |
### 2. AI 摘要截断修复
**触发:** 日报 AI 摘要内容极少(137 字),末尾被截断。
**根因:** `_llm_call()` `max_tokens=600` 太低,deepseek-v4-flash 输出被硬截断。
**修复:** `max_tokens` 三处提升 + 截断检测日志。
| 位置 | 修复前 | 修复后 |
|------|--------|--------|
| `_llm_call()` 默认 | 600 | 1500 |
| 分块摘要 | 400 | 800 |
| 合并摘要 | 600 | 1500 |
**验证:** 手动生成日报,摘要从 137 字 → 486 字,结构完整无截断。
---
## 超时优先级(最终设计)
```
TIMEOUT_{NAME} 环境变量 ← 单步精确控制
↓ 未设
PIPELINE_STEP_TIMEOUT 环境变量 ← 全局兜底 (覆盖所有硬编码)
↓ 未设
STEP_TIMEOUTS 硬编码字典 ← 代码内默认值
↓ 步骤不在字典中
1800s ← 最终兜底
```
---
## 服务器 .env 当前配置
```env
# ---- 调度 ----
SCHEDULE_TIMES=07:00,12:00,18:00,22:00
CNINFO_SCHEDULE_TIME=06:00
STOCK_REPORT_TIME=08:00
STOCK_REPORT_DAYS=15
# ---- Pipeline 超时 ----
PIPELINE_STEP_TIMEOUT=3600
TIMEOUT_CRAWLER=900
TIMEOUT_XWLB=60
TIMEOUT_EXTRACTOR=300
TIMEOUT_DEDUP=900
TIMEOUT_LLM=900
TIMEOUT_EMBEDDING=900
TIMEOUT_QDRANT=1800
TIMEOUT_CNINFO_CRAWL=900
TIMEOUT_CNINFO_EXTRACT=900
TIMEOUT_CNINFO_PDF=900
```
---
## 调度器
| 时间 | 步骤 |
|------|------|
| 06:00 | cninfo 公告管道 |
| 07:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant→**日报** |
| 08:00 | 个股日报 |
| 12:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant |
| 18:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant |
| 22:00 | crawler→xwlb→extractor→dedup→llm→embedding→qdrant |
---
## 常用命令
```bash
# 全链路 + 日报
uv run a-share pipeline --once --report
# 仅日报 (指定日期)
/home/pi/.local/bin/uv run python -m scripts.run_scheduler --once --date 20260623 --steps report
# 检索
uv run a-share search "关键词" --top 5
# 同步到服务器
scp <file> pi@192.168.1.160:/home/pi/news/<path>/
# 重启服务
ssh pi@192.168.1.160 "sudo systemctl restart a-share-research"
```
---
## 已知技术债务
| 问题 | 文件 | 状态 |
|------|------|------|
| QdrantClient HTTP 超时硬编码 10s | `vectorstore/client.py:93` | 无可配环境变量 |
| DashScope HTTP 超时硬编码 60s | `embedding/remote.py:81` | 无可配环境变量 |
| 超时解析逻辑在 pipeline.py 和 main.py 各一份 | 两处 | 可抽取公共函数 |
| `deepseek-v4-flash` 概率性返回空 | — | 分块处理 + 失败跳过 |
| wallstreetcn JS 渲染拦截 | — | 正文提取率 0% |
| eeo 列表页仍走 Playwright(仅文章页走 httpx | `crawler/engine.py` | 列表页目前正常,暂不改动 |
---
## 架构备忘:静态/动态抓取分流
```
SourceConfig.js_render == False 且 wait_for == None
→ _fetch_static() → httpx 直连 (无浏览器, 快, 不被反爬)
→ markdownify + BeautifulSoup (清洗 script/style 后转 Markdown)
SourceConfig.js_render == True 或 wait_for 有值
→ _crawl_once() → Playwright (现有逻辑)
```
`_TimeoutGuard`:
- 同源连续超时 2 次 → `skip=True`
- 后续请求在 `semaphore` 内检查 `guard.skip`,立即返回 `Skipped`
- 一次成功即重置计数器
---
## 记忆
- `never-change-llm-model.md` — 绝不允许修改大模型配置
- `deploy-html.md` — user_guide.html 同步到 Pi 和 Web 服务器
- `sync-sources-yaml.md` — 修改 sources.yaml 前从 Pi 拉取,改完推回
- `update-user-guide.md` — 更新 md+html+部署