Files
news/continuation.md
T

16 KiB
Raw Blame History

continuation.md

checkpoint @ 2026-08-12 13:54


当前状态

项目 值
新闻源 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
调度器 APScheduler,systemd a-share-research.service(pi5);每天 07:00 首次任务生成日报(12/18/22 点不生成)
LLM 场景化配置 configs/llm_models.yaml(4 场景);YAML 优先、.env 兜底
去重 多源记录:指纹库 source_ids 列 + uniques sources 字段 + sources.json;M4 事件 / 日报 / DB news_event.sources 全链路透传
增量/断点 M2/M4/M5 产物存在即跳过(--force 全量);pipeline --once --resume 断点续跑
服务器 pi@192.168.1.160(生产)/ pi@192.168.1.10(DB 隧道宿主)
抓取方式 js_render=false → httpx 直连;js_render=true → Playwright

本次完成 (2026-08-12 13:00) — 日报事件多新闻源入库 + 阶段/AI 显性输出 + 摘要崩溃修复

1. 多新闻源贯通至 DB(方案 A:新增 sources 列):

  • llm/models.py:ExtractedEvent 增加 sources(validator 保主源居首、去重;旧产物兜底 [source_id])
  • llm/extractor.py:extract_event(_async) 增加 sources 参数透传
  • scripts/run_event_extraction.py:读取 deduped uniques 的 sources 字段传入抽取(_load_sources)
  • scheduler/reporter.py:_load_events_from_dir 读取 sources(兜底 [source_id])→ EventRow.sources
  • report_db:news_event 建表加 sources TEXT 列;init_schema 幂等迁移(ALTER,Duplicate column 捕获);save_report INSERT 写 JSON
  • 生产 pi5:init_schema 已执行,news_event.sources 列就绪(text, NULL);旧数据为 NULL,下次全链路 M4 后新事件带多源

2. pipeline 阶段与 AI 显性输出(scheduler/pipeline.py):

  • 每步前打印 阶段 i/n: 中文名 [步骤] 日期 分隔标题
  • llm/report/embedding 步骤打印 🤖 AI 大模型: provider=..., model=...(按场景配置解析,缺 key 也可展示)

3. 修复日报 AI 摘要崩溃('str' object has no attribute 'model'):

  • _llm_summarize 内 3 处 _llm_call(client, model, ...) 改为传 config(上轮改签名遗漏)
  • 新增回归测试(单块/多分块全链路)

验证(pi5 生产):

  • 本地 pytest 231 passed(新增 ExtractedEvent.sources×2、EventRow.sources×2、_load_events_from_dir×1);ruff 基线 9 零新增
  • pi5:init_schema 迁移成功(news_event 新增 sources 列);日报生成入库正常(report_id=225, events=60)
  • 部署前已修复并在生产实测 AI 摘要恢复

待办:

  • 下次全链路运行后抽查 news_event.sources 多源值(当前生产 events 为旧产物,sources=NULL)
  • 可选:日报 HTML 展示多源(数据已贯通,渲染层加一行即可)

git 版本(今日已全部推送 origin/main):

  • 3c65701 feat: 大模型使用场景化配置与去重多源记录
  • c1a8039 docs: 大模型场景配置与去重多源记录说明
  • 2b4efea feat: 增量处理与 pipeline 断点续跑
  • 01bb2e5 docs: 增量处理与断点续跑说明与效果评估
  • 8f2db92 feat: pipeline 执行时显性输出当前阶段与 AI 大模型信息
  • 182f788 fix: 修复日报 AI 摘要崩溃('str' object has no attribute 'model')
  • 794ce6c feat: 日报事件多新闻源入库(news_event.sources 列)
  • 8bbf510 docs: 多源入库与生产迁移说明

本次完成 (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=191(2026-08-05):news 20 + cninfo 20 + xwlb 34(08-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 被拦截(EHOSTUNREACH;nc/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=0;DB 177 行(finance 49 + intl 128,1 个跨目录同名文件被幂等合并)+ 事件 4222 条
  • 端到端:a-share report --date 20260616 → report_id=180 入库成功,无 HTML 产出
  • 单测 24 个通过(parser 19 + builder 4 + models/import 补充)

命令:

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 key,AI 摘要会降级 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 当前配置

# ---- 调度 ----
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

常用命令

# 全链路 + 日报
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+部署