# continuation.md > `checkpoint` @ 2026-08-22 --- ## 本次完成 (2026-09-25) — 日报 AI 摘要为空修复(推理模型 reasoning 占满 max_tokens) **现象**:用户反馈 2026-09-25 日报没有 AI 摘要。`news_report` 中 `id=357`(2026-09-25, finance) `ai_summary` 为 `NULL`;当天 07:13:55 日志: ``` WARNING | scheduler.reporter:_llm_call - AI 摘要可能被截断: max_tokens=1500 finish_reason=length 实际输出 0 字符 ``` **根因(证据链闭合)**: - 日报场景(daily_report)调用 `deepseek-v4.1-flash`(Token Plan),这是**推理模型**:`reasoning_content` 的 token 与正文**共用** `max_tokens` 预算 - 用当天真实素材原样复现:**`reasoning_tokens=1500` / `text_tokens=0` / `content=0 字符` / `finish_reason=length`** —— 预算被"思考"全部吃掉,正文为空 - 同素材把预算提到 4000:`finish_reason=stop`、reasoning 938 + text 337、摘要 523 字 ✓ - 代码缺陷:`scheduler/reporter.py:_llm_call` 只取 `message.content`,空内容**不抛异常** → `_generate_ai_summary` 返回 `""` → `ai_summary=None` 入库,pipeline 仍标 report ✅(**静默失败、无重试**) - **非本次 Token Plan 迁移引入**:历史同为空的还有 9-12 / 9-13 / 9-15 / 9-18,当时用的是 `deepseek-v4-flash`(provider=deepseek),同样是推理类模型 → 长期间歇性缺陷 **修复(方案 B:配置化 + 代码兜底)**: 1. `configs/llm_models.yaml`:`daily_report` 新增 `max_tokens: 4000`(注释说明 reasoning 共用预算);YAML 保存即热生效 2. `llm/client.py`:`LLMConfig` 新增可选字段 `max_tokens`;`load_llm_config` 用新增的 `_pick_optional_int` 读取场景配置(未配置 = `None`,调用方回退内置默认) 3. `scheduler/reporter.py`: - 新增常量 `DEFAULT_SUMMARY_MAX_TOKENS=4000` / `DEFAULT_SUMMARY_CHUNK_MAX_TOKENS=2000` / `MAX_SUMMARY_MAX_TOKENS=16000` / `_MAX_BUDGET_ESCALATIONS=2` - 新增 `_summary_max_tokens()`(场景配置 > 内置默认)、`_chunk_max_tokens()`(不超过合并预算) - `_llm_call` 拆出 `_call_once`;保持网络异常指数退避重试语义不变;新增**空正文 + finish_reason=length 时自动加倍预算重试**(上限 16000),用尽后返回空串降级(不抛异常) - 分块预算 800 → 2000;合并预算 1500 → 配置值(4000) **验证**: - 线上复现 → 修复后回归:`uv run a-share report --date 20260925` 重跑,**无截断告警**,`report_id=357` 原地更新(幂等 upsert),`ai_summary` 466 字 ✓ - 测试:新增 `TestReasoningBudgetEscalation`(升级恢复 / 场景值优先 / 用尽降级返回空 / 上限)+ `test_llm.py` 的 `max_tokens` 场景配置、`_pick_optional_int`、真实 YAML 预算 ≥4000 回归保护 → `tests/test_report_builder.py tests/test_llm.py` **56 passed** - `ruff` 改动文件无新增问题(5 条 N806/SIM115 为 reporter.py 既有);`mypy` 仅剩 `llm/client.py:86` 既有告警 **运维动作**:`report` 步骤由调度器**进程内**执行,已 `sudo systemctl restart a-share-research`(10:55,重启后任务同步正常),明早 07:00 起生效。 **遗留与后续**: - `scheduler/stock_reporter.py:285` 个股日报 `max_tokens=500`,配置模型 `qwen3.6-flash` 亦属推理类,**同类隐患**(当前个股日报禁用);启用前建议一并按本方案处理 - 空摘要目前只降级为"无摘要",未做告警;可考虑连续 N 天为空时推送通知 - 本次改动尚未 git commit(工作区还混有 9-22 Token Plan 迁移的未提交改动,避免混提) --- ## 本次完成 (2026-09-10) — cninfo 抓取压穿内存导致整机冻结的修复 **现象**:2026-09-06 / 09-08 / 09-10 连续三次早上 06:0x 整机冻结,看门狗(硬件 2min)硬复位。 **根因(证据链闭合)**: - 三次冻结时刻 = `cninfo` 公告管道 06:00 定时任务:`logs/scheduler_error.log` 显示 09-10 06:00:03.180~.888 **0.7 秒内打印 15 条「抓取…公告」**(15 只股票协程同时进入渲染),随后日志全无直到 06:14:08 重启 - `data/raw/cninfo/` 缺 20260906/08/10 三个目录(存续目录 mtime 均为 06:02,而落盘在 `_save_items()` 中、抓取全部完成后才执行 → 崩在写数据之前) - `pcp-pmie` 09-10 06:01:20 报 **load 65**(4 核);DNS 全面超时(frpc/dockerd resolver) - `journalctl --list-boots` 与三次重启时刻吻合 **代码缺陷**(`crawler/cninfo.py`): 1. `_render_page` **每次调用都 `async with AsyncWebCrawler(...)` 新建完整 Chromium**(最贵的错误) 2. `crawl_watchlist` 用 `asyncio.as_completed` 对 watchlist **全量并发**(15 只) 3. **无任何并发限制**;service 亦无资源限制(`CPUQuota=infinity`、`TasksMax=9626`) 4. 本机 `cgroup_disable=memory` → **`MemoryMax` 不可用**(只会 `cpuset cpu io pids`) **实测(本机 8GB)**:基线 chrome=0 → **2 并发峰值 1713MB / 20 进程 = 857MB/实例**;外推 15 并发 ≈**12.5GB** ≫ 7.9GB RAM → 必然压穿(时好时坏是 zram 与时序侥幸) **修复(第一步:限并发)**: - `crawler/cninfo.py`:新增 `MAX_RENDER_CONCURRENCY`(默认 **2**,env `CNINFO_RENDER_CONCURRENCY` 可覆盖)+ 模块级 `_render_sem` 信号量,`_render_page` 全程持槽(含浏览器启停) - 顺带清理 3 个既有死导入(`time`/`datetime`/`Any`) **实测验证(2026-09-10 20:43 实跑 `a-share cninfo`)**: | 指标 | 修复前(15 并发) | 修复后(2 并发) | |------|----------------|---------------| | Chromium 内存峰值 | ≈12.5GB(外推) | **1796 MB** | | 进程峰值 | ≈150 | **22** | | load 峰值 | **65** | **2.75** | | available 最低 | 压穿冻结 | **3561 MB** | | 耗时 | 崩(无 END) | **441 s**(上限 900s) | | 结果 | 无数据 | 抓 34 条/存 23 条,补上 09-10 缺失目录 ✅ | **测试**:新增 `tests/test_cninfo.py` 8 个(并发上限/串行/异常释放槽位/env 解析);全量 **291 passed**(3 个 crawler 基线失败无关);ruff 干净 **遗留与后续优化**: - 耗时由 127~145s 增至 441s(用时间换内存安全),`cninfo_crawl` 超时 900s 余量由 6 倍降至 2 倍;如嫌慢可 `CNINFO_RENDER_CONCURRENCY=3`(峰值约 2.6GB,仍安全) - **第二步(未做)**:复用单个 `AsyncWebCrawler` + `arun_many()` 批量渲染(浏览器组 15→1,可同时提速降内存) - **第三步(未做)**:`_fetch_irm_requests()` 是同步 `requests.get` 却在 async 中直接调用,**阻塞事件循环**;可改 `asyncio.to_thread` - 长期:改用 cninfo 公告 POST JSON API(`hisAnnouncement/query`)彻底去掉浏览器依赖 - 未执行 systemd 资源限制:`CPUQuota` 对内存型崩溃基本无效(反而延长驻留),`TasksMax` 过小会让抓取永久失败 --- ## 本次完成 (2026-08-23) — MCP 新闻查询服务确认与修复 **用户需求**:实现新闻查询 MCP 服务(阅读文档步骤,确认是否已实现)。 **确认结论:核心实现完整且可用(此前已实现,本次验证 + 修复一处配置错误)**: | 文档要求 | 现状 | 验证 | |----------|------|------| | `scripts/run_mcp_server.py`(stdio/SSE 入口) | ✅ 存在 | stdio 端到端调用通过 | | `mcp_server/tools.py` 5 个工具 | ✅ 齐全 | `tools/list` 返回 5 工具,`tools/call` 真实检索成功 | | pyproject 依赖 `mcp>=1.0` | ✅ 已声明 | 已安装 | | `tests/test_mcp.py` | ✅ 10 个测试 | 全过 | | `.mcp.json`(Claude Code 配置) | ❌ **配置错误** | 见下 | **发现并修复的差距**: - `.mcp.json` 被跟踪的内容是 **Mac 上另一项目(serena)的残留配置**(路径指向 `/Users/summer/...`),与本项目无关 → 按 `docs/user-guide.md` 8.2 重写为 `a-share-research` 配置 - 补充 1 个测试(`test_fmt_results_multi_source_tag`,验证 A2 多源展示) **端到端验证**(真实调用,非 mock): 1. 5 个工具函数直接调用:全部返回检索结果 ✓ 2. SSE 模式 `--sse 8765`:`GET /sse` 返回 `event: endpoint` 握手 ✓ 3. **MCP 协议层**(stdio 客户端 → `scripts.run_mcp_server`):`tools/list` 返回 5 个工具与文档一致;`tools/call search_news("国务院常务会议")` 命中《李强主持召开国务院常务会议》等 ✓ **全量测试**:283 passed(新增 1 个多源展示),3 个 crawler 基线失败与本次无关;ruff 干净 **遗留**:无(仅既有 reporter 5 个 ruff 问题、Qdrant 本地模式性能警告) --- ## 本次完成 (2026-08-23) — P1-2/P1-3 补跑步骤与时区一致性修复 **P1-2(守护进程补跑误含 cninfo 三步)**: - 原因:`scripts/run_scheduler.py` 启动补跑逻辑 `steps = [k for k in STEP_COMMANDS if k != "report"]` 只排除 report,**漏掉 cninfo_crawl/cninfo_extract/cninfo_pdf**;错过定时任务重启补跑时会额外执行整套公告管道(且 cninfo_crawl 无 --date,补跑历史日期静默空转) - 修复:提取共享常量 `scheduler.pipeline.DEFAULT_NEWS_STEPS`(= 全链路去除 report 与 cninfo 三步),定时任务、补跑、run_pipeline 默认三处统一引用 **P1-3(调度时区与 date.today() 不一致)**: - 原因:cron 触发器显式用 `Asia/Shanghai`,但 `date.today()`/`datetime.now()` 取**系统时区**;若系统时区非上海(如容器 UTC),07:00 上海(=前一日 23:00 UTC)触发时日期会错一天,整条 pipeline 落错日目录 - 修复:新增 `scheduler/timeutil.py`(schedule_tz/today_str/now,env `SCHEDULE_TZ` 可覆盖,默认 Asia/Shanghai);`run_scheduler` 定时/补跑/`--once`、`pipeline.run_step` crawler 补跑保护、`reporter.generate_report` 兜底日期全部改用它 **测试**: - 新增 6 个(DEFAULT_NEWS_STEPS 不含 cninfo/run_pipeline 默认步骤行为/补跑源码引用/时区两例/--once 缺省日期),并入 `tests/test_incremental.py` - 全量 **282 passed**,3 个 crawler 基线失败与本次无关;ruff 干净(5 个 reporter 既有问题未动) **验证**:DEFAULT_NEWS_STEPS = [crawler, xwlb, extractor, dedup, llm, embedding, qdrant] ✓;today_str = 20260823(Asia/Shanghai)✓ --- ## 本次完成 (2026-08-22) — 多源新闻记录链路修复(方案 A + B) **用户需求**:一条新闻有多个来源时,全部来源都要记录并可见;此前"找不到多源"。 **诊断结论**: - M3→M4 多源记录逻辑**本身正常**:指纹库 227 条多源、当日 sources.json 11 条、events 313 条全含 sources、MySQL news_event.sources 填充率 100% 且有 7 条真实多源(如许家印案 `["cls","sina"]`) - 用户"看不到"的原因:① 展示层(日报 HTML/CLI/MCP)只渲染单源 `source_id`;② 知识库链路 M5→M6 **真断点**——`EmbeddingResult` 无 `sources` 字段、Qdrant payload 只写 `source_id`、`SearchResult` 无 `sources` **修复内容**: - B1 模型层: `embedding/models.py` `EmbeddingResult.sources` + validator(主源居首/去重/旧产物兜底);`vectorstore/models.py` `SearchResult.sources` - B2 `scripts/run_embedding.py`: `_build_text_from_event/_build_text_from_article` 透传 sources 进 EmbeddingResult - B3 `scripts/run_qdrant_ingest.py`: payload 写 `sources`(M5 产物 → 回查 M4 → 兜底 [主源]) - B4 `vectorstore/client.py`: 检索读取 payload.sources - A1 `scheduler/reporter.py`: 日报 HTML 多源时显示「财联社 / 新浪 📰」 - A2 `a_share_cli/main.py` + `mcp_server/tools.py`: 检索展示「cecn / cscn [多源]」,MCP 返回 sources 数组 - 存量回填 `scripts/backfill_qdrant_sources.py`(新):以指纹库 source_ids 为权威源回填 Qdrant payload - ⚠️ 坑:本地文件模式 Qdrant 写入逐点 ≈0.4~0.65s,**全量 3.3 万条回填需 4~6 小时**且占用单进程锁,不可行 - 改为**定向回填**:只有多源 point 需要 sources(单源点展示层兜底 source_id),仅回填指纹库 227 条多源 → **1m57s 完成 226 条**(1 条不在集合) - 执行时机:避开定时调度窗口(22:00 pipeline 结束后 22:34 执行) **验证**: - 新增 `tests/test_multisource.py` 10 个(validator/`_result_to_point` 三优先级/文本构造透传/回填加载) - 全量 **276 passed**(3 个 crawler 基线失败与本次无关);ruff 干净(reporter 5 个 N806/SIM115 为既有问题未动) - 今日 M5/M6 重跑:313 条全含 sources(11 条多源);CLI 检索显示「来源: cecn / cscn [多源]」✓,MCP 返回 `sources: ['cecn','cscn']` ✓ - 存量回填:226 条多源 point payload 已补 sources;CLI 检索「碧根果反倾销」显示「来源: cnstock / cscn [多源]」✓;日报 HTML 冒烟测试多源渲染「财联社 / 新浪财经 / 东方财富 📰」✓ **待办/遗留**: - 单源存量 point 无 sources 字段(展示层兜底 source_id,行为不变,如需统一可后续补) - P1-2(补跑 steps 含 cninfo)、P1-3(时区一致性),待用户决策 --- ## 本次完成 (2026-08-22) — P1-1 修复 dedup 高重复率返回码语义 **问题**:`run_dedup` 重复率 > 5% 时返回 1;`scheduler/pipeline.py` 曾把 `dedup` 的 rc=1 无条件视为成功并打日志"无新数据场景"。结果:高重复率(可能是正常无新数据,也可能是抓取源/指纹库异常)被一刀切掩盖,真实异常无法上报。 **修复(分离"执行成功"与"统计告警")**: - `scripts/run_dedup.py`: - 生产模式(默认):重复率仅作 **WARNING 告警**,不影响退出码(执行成功即 0);阈值常量 `_DUP_RATE_THRESHOLD = 0.05` - 新增 `--strict` 验收模式:保留 M3 验收门槛,重复率 > 5% 时返回 1(供人工验收) - 新增统计快照 `data/deduped/{date}/stats.json`(原子写):unique/duplicates/total/dup_rate/layers/fingerprint_total/generated_at,供运维排查与监控 - 顺带修复:空日场景 `sources.json` 写入前补 `mkdir`(原会 FileNotFoundError) - `scheduler/pipeline.py`:移除 `dedup` rc=1 特判,恢复"非 0 即失败"的统一语义 **测试**: - 新增 `tests/test_run_dedup.py` 7 个(生产模式高重复返回 0 / --strict 高重复返回 1 / --strict 低重复返回 0 / 空日返回 0 / stats.json 结构与数值 / pipeline 不再掩蔽失败) - 全量 **266 passed**,3 个 crawler 基线失败与本次无关 **生产实测**:同日重跑重复率 100% → 生产模式 `rc=0` + WARNING + stats.json 完整;`--strict` 同场景 `rc=1`(用非管道方式核实退出码) **待办/遗留**: - P1-2(守护进程补跑 steps 含 cninfo 三步)、P1-3(调度时区与 date.today() 一致性),待用户决策 --- ## 本次完成 (2026-08-22) — P0-3/P0-4 补跑日期语义修复 **P0-3(crawler 补跑历史日期静默空转)**: - 原因:`crawler/storage.py` 落盘硬编码 `date.today()`(engine 不传 day),`pipeline --once --date {历史}` 时下游读历史目录为空,且 `run_extractor` 对空目录返回成功 → 全链路静默空转 - 修复:`run_step()` 开头加补跑保护——`date_str != 今天` 时跳过 crawler(首页只含当天内容,历史文章已滚走,补抓不可能),WARNING 日志 + `tail_msg` 说明,返回 success(补跑复用已有 raw 数据是正确行为);xwlb 不跳过(API 支持任意历史日期) **P0-4(report 硬编码 date.today() 忽略传入日期)**: - 原因:`run_step("report")` 未使用入参 `date_str`,取当前时间;补跑历史日期+--report 会生成今天的空日报并覆盖当天已有日报(唯一键 file_name='') - 修复:一行改动 `report_date = date_str`;`reporter.generate_report(day_str)` 本身已支持任意日期,无需其它改动 **测试**: - 新增 4 个(补跑跳过不执行子进程/当天正常执行/report 收到传入日期/全链路仅 crawler 跳过),并入 `tests/test_incremental.py` - 修正 3 个既有测试(`tests/test_scheduler.py`)适配新行为(用当天日期测正常执行路径) - 全量 **259 passed**,3 个 crawler 基线失败与本次无关 **待办/遗留**: - ~~P1-1(dedup rc=1 静默转成功)~~ 已修复(见上方小节);余:守护进程补跑 steps 含 cninfo_*、调度时区与 date.today() 一致性,待用户决策 - 既有小问题:`tests/test_scheduler.py` 部分 run_pipeline 测试未传 state_path,会写真实 `data/pipeline/state.json`(既有行为,未改) --- ## 本次完成 (2026-08-22) — xwlb 日期错位修复(方案 A) **背景问题**: - P0-1: `run_xwlb` 硬编码抓前一天并落盘到「数据日」目录,而下游 extractor 按「处理日」扫目录 → 60 天联播数据从未进入知识库(实测: `data/processed/xwlb/` 仅 20260622 一天) - P0-2: `run_xwlb` 用 `open("a")` 裸追加,同一天被多次调度触发导致 index.jsonl 重复行(实测 20260821: 70 行仅 24 唯一) **业务约束(保持不变)**: 当天日报需要前一天晚上(19:00 播出)的新闻联播内容。 **方案 A 实施**: 1. `scripts/run_xwlb.py` 重构: - 新增 `--date` 参数(处理日,默认今天),与管道其它步骤日期语义一致 - 内部 `数据日 = 处理日 - 1`(业务约束:抓前一晚已播出的联播) - **落盘目录改为处理日** `data/raw/xwlb/{处理日}/` → extractor/dedup/llm/embedding/qdrant 零改动即可处理 - **幂等落盘**: 落盘前读已有 index.jsonl 构建 `url_hash` 集合,已存在条目跳过;index.jsonl 重写为去重完整集(原子写),不再裸追加 2. `scheduler/pipeline.py`: xwlb 步骤命令加 `--date {date}` 3. `scripts/run_extractor.py`: xwlb 假 HTML 无时间节点 → 新增 `_fill_xwlb_publish_time`,从 url(`xwlb://YYYY-MM-DD/sid`)兜底解析真实播出日填充 `publish_time`(否则检索时间过滤/排序失效) 4. **历史死数据 A1 清理**: 删除 `data/raw/xwlb/` 62 个旧目录(20260622~20260822)+ `data/processed/xwlb/`(仅 20260622) **验证(生产端到端,20260822)**: - 新增 `tests/test_run_xwlb.py` 9 个测试(处理日目录、幂等、空数据、坏日期),全部通过 - 全量测试 **255 passed**(3 个 crawler 基线失败与本次无关) - 实跑: 处理日=20260822 抓数据日 20260821 联播 23 条 → 二次运行新增 0/跳过 23(幂等)→ M2 提取 23/23 → M3 唯一 23 → M4 LLM 23/23 → M5 23/23 → M6 Qdrant 净增 23 - 检索验证: `a-share search "国务院常务会议"` top2 命中 xwlb《李强主持召开国务院常务会议》,时间 2026-08-21(真实播出日)✓ - 日报路径不受影响(仍走 `reporter._collect_xwlb` API 直读) **运维规则新增**: 每次代码升级完成后必须执行 `sudo systemctl restart a-share-research.service` **待办/遗留**: - ~~P0-3(crawler 无 --date,补跑历史日期静默空转)、P0-4(report 硬编码 date.today())~~ 已修复(见上方小节) - P1(dedup rc=1 静默/补跑含 cninfo/时区)未修,待用户决策 - `run_extractor --force` 重提取会重复追加 `data/processed/{src}/{date}/index.jsonl`(仅影响该辅助索引,下游读 *.json 不受影响),暂不修 - 历史 60 天联播未入库(按用户决策 A1 清理,不回补) --- ## 本次完成 (2026-08-22) — 文档清理与重构 **目标:** 清理历史上多个 AI agent 文档残留,重构项目文档为 4 个核心文件。 **删除的过时/残留文件 (4 个):** 1. `project_plan.md` (700 行) — 9 个 Milestone 全部完成,计划文档已无价值 2. `docs/agent_prompt.md` (80 行) — Cherry Studio Agent 使用说明,非本项目核心文档 3. `docs/optimization_plan.md` (144 行) — 8 项优化中 5 项已完成,剩余不再跟踪 4. `docs/report_db_design.md` (330 行) — M10 中间设计稿,逻辑已下沉到代码 **新建/重写的文档 (5 个):** | 文件 | 行数 | 说明 | |------|------|------| | `docs/architecture.md` | ~500 | 项目架构:11 包职责与导出 API、核心数据模型、配置体系、产物目录 | | `docs/user-guide.md` | ~500 | 用户手册:15 个 CLI 子命令全量 + 增量/断点续跑 + MCP + systemd + FAQ | | `README.md` | ~120 | 精简为项目入口:简介 + 文档索引 + 命令速查 | | `docs/db-schema.md` | 保留 | 日报表结构(API/前端对接,无需改动) | | `deploy/README.md` | 删除 | 已在服务器上直接修改,无需部署文档 | **docs/ 最终目录:** ``` docs/ ├── architecture.md # 项目架构(新增) ├── user-guide.md # 用户手册(重写) └── db-schema.md # 日报表结构(保留) ``` **验证:** - 所有文档中文撰写,Markdown 格式,层次清晰 - architecture.md 准确反映 11 个包的实际公开 API(基于源码 `__init__.py` 导出) - user-guide.md 覆盖全部 15 个 CLI 子命令(crawl/extract/dedup/events/embed/ingest/pipeline/search/status/report/report-import/cninfo/stock-report/watchlist/discover/add-entry) - 无过时信息残留(不再提 M9 Agent/HTML 日报/旧版 Mac 部署等) **待办:** - git 提交本次改动(包括 2026-08-12 增量处理改动,仍未提交) - 同步 pi5(代码 + 文档),重启 a-share-research --- --- ## 当前状态 | 项目 | 值 | | --- | --- | | 新闻源 | 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 场景: event_extraction/daily_report/stock_report/embedding);YAML 优先、`.env` 兜底;模型必须显式配置,无内置兜底 | | 去重 | 多源记录:指纹库 `source_ids` 列 + uniques JSON `sources` 字段 + `data/deduped/{day}/sources.json` | | 增量/断点 | M2/M4/M5 产物存在即跳过(`--force` 全量);`pipeline --once --resume` 断点续跑(状态 `data/pipeline/state.json`) | | 服务器 | `pi@192.168.1.160`(生产)/ `pi@192.168.1.10`(DB 隧道宿主) | | 抓取方式 | js_render=false → httpx 直连;js_render=true → Playwright | --- ## 本次完成 (2026-08-12) — 增量处理与 pipeline 断点续跑 **目标:** ① 全链路中断后可从断点恢复;② 各子任务排除已处理文件,避免全量重跑与重复 API 计费。 **1. 各步骤增量处理(产物存在即跳过,`--force` 全量):** - M2 `run_extractor.py`:输出目录已有 `{url_hash}.json` 即跳过提取,仅回补 index 行;`--force` 重建;成功率统计含跳过项(修复全跳过时误报 rc=1) - M4 `run_event_extraction.py`:`data/events/{day}/{url_hash}.json` 已存在即跳过(**不重复调用 LLM API**);`--force` 全量;failed.jsonl 只保留本次失败、index 累积追加 - M5 `run_embedding.py`:`data/embeddings/{day}/{url_hash}.json` 已存在即跳过(**不重复调用 embed API**);`--force` 全量 - M1(seen_urls 增量)/ M3(指纹库判重)/ M6(upsert 幂等)为既有能力,README 汇总成表 **2. pipeline 断点续跑(scheduler/pipeline.py + run_scheduler.py):** - 新增 `data/pipeline/state.json`(按日期隔离,记录每步骤 ok/failed + 退出码 + 耗时),原子写 - `run_pipeline(resume=True)` 跳过连续成功前缀,从首个失败/未执行步骤继续执行到结尾 - `pipeline --once --resume`(默认全量不变;`--resume` 与 `--steps` 互斥报错);定时守护模式不受影响 **验证(Mac 本地,20260616 数据 100 篇):** - pytest **225 passed**(新增 tests/test_incremental.py 10 个:M2/M4/M5 跳过、状态记录、resume 续跑、resume 全完成 noop、--resume+--steps 互斥);crawler 3 个基线失败仍与本次无关 - ruff 零新增(9 个基线错误不变) - 端到端:M2 增量重跑 140 条跳过 126 条,2.5s 完成、rc=0(修复前误报失败);state.json 正确记录 extractor ok **效果评估(100 篇规模中断重跑场景):** - M4 减少约 100 次 LLM API 调用、M5 减少约 100 次 embedding API 调用 → 中断恢复不再重复计费,耗时从分钟级降至秒级 - M2 重跑从全量 GNE 提取(分钟级)降至约 2.5s - 断点恢复操作:中断后直接重跑同一条 `pipeline --once --resume` 命令即可 **待办:** - 同步 pi5(代码 + 文档),重启 `a-share-research`;首次同步后 pi5 的 `data/pipeline/state.json` 不存在 → resume 按全量处理,行为安全 - git 提交(本次改动尚未提交) --- ## 本次完成 (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+部署