Files
news/continuation.md
T
simon 65ead54b4f docs: 文档清理与重构 — 统一为 3 个核心文档
- 删除 5 个过时/残留文档(project_plan/agent_prompt/optimization_plan/report_db_design/deploy/README)
- 新建 docs/architecture.md(项目架构:11 包职责+数据模型+配置+产物)
- 重写 docs/user-guide.md(CLI 全量+增量/断点续跑+MCP+FAQ)
- 重写 README.md(精简入口+文档索引)
- 更新 continuation.md(追加本次记录)
- 更新 .gitignore(排除 data/* 运行产物)
2026-08-22 17:10:39 +08:00

387 lines
17 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-22
---
## 本次完成 (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 <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+部署