# 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** | | 调度器 | APScheduler,systemd `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=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 补充) **命令:** ```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 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 当前配置 ```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 pi@192.168.1.160:/home/pi/news// # 重启服务 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+部署