feat: 日报改为每日一次 + 新增 LLM 使用/成本/缓存文档

- domestic_full.sh / pipeline.sh 新增 --no-report 参数:跳过日报生成,
  保持 M1 抓取 + M2→M6 管道不变,用于 12:00/18:00 只抓不生成日报
- crontab: 07:00 全量含日报,12:00/18:00 --no-report(日报每日一次,降低 LLM 费用)
- docs/llm-models.md: 各 LLM 调用点分别用的 provider/model 事实清单
- docs/llm-cost-analysis.md: 真实 token 消耗与费用构成、降本建议
- docs/llm-cache-optimization.md: DeepSeek 上下文缓存机制与命中率提升设计
- docs/README.md 增加上述三个文档索引
This commit is contained in:
2026-09-04 10:38:33 +08:00
parent 9e4f5b4c75
commit c1ab751d95
6 changed files with 347 additions and 16 deletions
+3
View File
@@ -12,6 +12,9 @@
| [快速开始](quickstart.md) | 环境准备、安装、配置、首次运行 | | [快速开始](quickstart.md) | 环境准备、安装、配置、首次运行 |
| [使用手册](usage.md) | CLI 命令、Shell 脚本、MCP 服务、日报查看 | | [使用手册](usage.md) | CLI 命令、Shell 脚本、MCP 服务、日报查看 |
| [流水线详解](pipeline.md) | M1→M9 各步骤输入/输出、增量逻辑、幂等机制 | | [流水线详解](pipeline.md) | M1→M9 各步骤输入/输出、增量逻辑、幂等机制 |
| [LLM 模型使用清单](llm-models.md) | 各用到 LLM 的地方分别用哪个 provider / 模型 |
| [LLM 成本分析](llm-cost-analysis.md) | LLM 调用点、token 消耗与费用构成、降本建议 |
| [LLM 缓存优化设计](llm-cache-optimization.md) | DeepSeek 上下文缓存机制与命中率提升方案 |
| [配置说明](configuration.md) | `system.yaml`、`sources.yaml`、Profile、`.env` | | [配置说明](configuration.md) | `system.yaml`、`sources.yaml`、Profile、`.env` |
| [部署与运维](deployment.md) | Pi 服务器部署、Xvfb/代理、crontab、日志清理 | | [部署与运维](deployment.md) | Pi 服务器部署、Xvfb/代理、crontab、日志清理 |
| [开发指南](development.md) | 源码结构、测试、已知问题、新增新闻源 | | [开发指南](development.md) | 源码结构、测试、已知问题、新增新闻源 |
+103
View File
@@ -0,0 +1,103 @@
# DeepSeek 上下文缓存优化设计方案
> 目标:提高 DeepSeek API 的 prompt 缓存命中率,从而降低 M4 翻译与 M7 日报的输入成本。
> 依据:DeepSeek「上下文硬盘缓存」机制([官方文档](https://api-docs.deepseek.com/zh-cn/guides/kv_cache))。
> 状态:**设计稿**,尚未改动生产代码;文中「改动点」需确认后实施。
## 一、机制要点(DeepSeek 上下文硬盘缓存)
1. **默认开启、零代码开启**:所有请求自动参与缓存,无需在请求里额外传参。
2. **「缓存前缀单元」完整匹配**才命中:每次请求的输入结束位 / 输出结束位、以及系统检测到的公共前缀,都会落盘为**缓存前缀单元**。后续请求必须**完整匹配**某个缓存前缀单元,才会计入「缓存命中」。
- `A+B` 之后再来 `A+B+C` → 命中 `A+B`(沿用例一看)。
- `A+B` 之后再来 `A+C` → **不命中**,但系统会把公共前缀 `A` 落盘,下一轮 `A+D` 可命中 `A`。
3. **价格差异极大**(`deepseek-v4-flash`):
| 输入 | 空闲 | 高峰 |
|------|------|------|
| 缓存命中 | 0.05 元/百万 | 0.10 元/百万 |
| 缓存未命中 | 1.5 元/百万 | 3.0 元/百万 |
- 命中比未命中便宜 **30 倍**。提升命中率是单位成本上最强的手段。
4. **可观测**:响应 `usage` 里的 `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` 直接给出命中情况,可据此验证优化前后。
## 二、当前实现的问题
### 场景一:M4 翻译(`llm/extractor.py`)
当前 `user` message 结构(`prompts/translation_and_extraction.md`):
```
请处理以下英文财经新闻:
**标题**: {title} ← 每篇不同
**来源**: {source_name} ← 每篇不同
**发布时间**: {publish_time} ← 每篇不同
**正文**:
{content} ← 每篇不同
```
- `system` prompt 完全固定 → 可作为一个稳定前缀,但只有几百 token,占单篇输入比例很小。
- `user` prompt 里「标题/来源/发布时间」都在正文之前且每篇不同 → 导致**每篇文章的 user 前缀都错位**,正文 `{content}` 无法形成稳定公共前缀,命中率大幅下降。
- 实测单篇平均输入 ≈1667 token,估算其中 75%+ 是正文 `{content}`;若能让正文前缀稳定,可显著命中。
### 场景二:M7 日报 AI 摘要(`scheduler/reporter.py`)
- `system` prompt 固定("你是国际财经日报撰写助手…"),user prompt 是当日事件列表,每批内容不同 → 只有极短的 system 前缀可命中,命中率低但总量小(每天仅几千 token),非成本重点。
## 三、优化方案
### 方案 A(推荐,改动小、收益大):稳定 M4 user prompt 前缀
把「每篇变化但重复出现的结构性开头」从正文前挪到结构里,让**正文 `{content}` 作为相对固定的前缀占位**,或至少让「固定指令段」全部前置。
调整 `prompts/translation_and_extraction.md` 的 user 段,把固定指令前置,变化字段后置:
```
请处理以下英文财经新闻,输出 JSON(标题译文、正文全文译文、事件列表):
[MODEL_INSTRUCTION_BLOCK] ← 完全固定的指令/示例(移到最前)
【待处理新闻】
正文:
{content} ← 长固定位置内容
【元信息】
标题: {title}
来源: {source_name}
发布时间: {publish_time}
```
> 说明:把很长的 `{content}` 提前、固定,让后续连续请求以同一篇或相似内容为前缀;配合条件:同一批 82 篇连续翻译时,尽量让公共 system + 固定指令 + 常见正文前缀命中缓存。
### 方案 B(最直接):「正文最早、短字段最后」重排
即便不改语义,仅调整 user 段字段顺序也能提升命中:把 `{content}`(变量庞大但相对可复用的重复新闻源)尽量前置且稳定,`{title}`/`{source_name}`/`{publish_time}`(每篇必变)后置。这样 system + 固定指令 + 正文前缀可形成缓存前缀单元,后续请求命中。
### 方案 C:并发下最大化公共前缀(很少改动)
- M4 并发 `concurrency=3` 同时发 3 个不同文章 → 冷启动多,前缀难以公共。可在首篇(或任务开头)先发 1 篇建立缓存前缀,再并发,后续命中率高。收益存在但不稳定,不强制。
### 方案 D(工程化验证):在 events 文件记录缓存命中
利用 `usage.prompt_cache_hit_tokens / prompt_cache_miss_tokens`,把这两项写入 `data/events/{date}/{hash}.json`(现有字段 `prompt_tokens` 旁),用于量化验证方案 A/B 落地后的命中率提升。属观测增强,不影响功能。
### 方案 E(日报场景,次要)
M7 摘要量小,可不动;若要优化,保持 system prompt 稳定即可(当前已稳定)。可复用「截断重试时复用同一前缀」。
## 四、建议实施顺序
1. 先落地**可观测性(方案 D)**:给 events 记录缓存命中 token(小改动,零风险),跑 1 次对比基线。
2. 再实施**方案 A / B**(重排 M4 user prompt),对比命中率与单篇输入成本。
3. 酌情试点**方案 C**(并发预热)。
## 五、验收标准
- 单篇输入 token 中 `prompt_cache_hit_tokens` 占比显著上升(目标:日常连续翻译批 >50%)。
- 单篇平均输入成本下降(命中 0.05 vs 未命中 1.5 元/百万)。
- 翻译质量、输出格式不受影响(缓存只作用于输入前缀,不影响输出随机性)。
## 六、参考
- [DeepSeek 上下文硬盘缓存](https://api-docs.deepseek.com/zh-cn/guides/kv_cache)
- [DeepSeek 模型与价格](https://api-docs.deepseek.com/zh-cn/quick_start/pricing/)
- 相关代码:`prompts/translation_and_extraction.md`、`llm/extractor.py`、`scheduler/reporter.py`
- 配套成本分析:[llm-cost-analysis.md](llm-cost-analysis.md)
+126
View File
@@ -0,0 +1,126 @@
# LLM 使用点与成本分析
> 本文档基于 2026-08-30 的代码与真实运行数据编写,用于:
> 1. 盘点本项目所有使用大模型(LLM / embedding)的位置与调用方式;
> 2. 分析 DeepSeek 每日费用的构成与量级;
> 3. 给出降本改善建议(不改写生产代码,仅供决策参考)。
## 一、本项目所有用到「大模型」的地方
本项目有两类大模型能力,计费方式和对成本的影响完全不同:
- **对话 / 补全类(chat completion)**:贵,按「输入 token + 输出 token」计费。目前全部走 **DeepSeek(`deepseek-v4-flash`)**。
- **向量化(embedding)**:便宜,按 token 计费。目前走 **DashScope(`text-embedding-v3`)**,与 DeepSeek 是两套账户/两笔账单。
### 1.1 对话类 LLM 调用点(付费大头,DeepSeek)
| # | 场景 | 位置 | 触发频率 | 单次 token 规模 | 说明 |
|---|------|------|----------|----------------|------|
| 1 | **M4 全文英译中 + 投资事件抽取** | `llm/extractor.py::translate_and_extract`(`_call_llm_sync`) | 每个去重后的唯一文章 **1 次**(失败重试最多 3 次) | 单篇平均 输入≈1667 / 输出≈2342 | 这是**最大的成本来源**,每篇独立调用、整篇中文全文均输出 |
| 2 | **M7 日报 AI 摘要** | `scheduler/reporter.py::_call_llm_simple`(`_generate_ai_summary`) | 每天 3 次定时(07:00/12:00/18:00),每次 1~N 批 + 截断重试 | 每批 max_tokens 500–800(截断重试 1200) | 依赖当日高重要度事件条数;事件 ≤10 一条批,超过则分批 |
> ⚠️ 是否重复翻译(重要成本放大项):
> M4 设计了**增量机制**——同一 `url_hash` 的 `data/events/{date}/{hash}.json` 已存在就跳过,不重复调用 LLM。但每天 3 次定时全流程都会把**当天新增的去重文章**翻译一遍,若某天连续中断/失败多次,缺的篇会在下次补齐,当周补跑可能一次性翻译多天积压文章。
### 1.2 向量类(embedding,DashScope,低费用)
| # | 场景 | 位置 | 触发 | 说明 |
|---|------|------|------|------|
| 3 | **M5 向量生成(入库)** | `embedding/pipeline.py::embed_all_events` | 每天对已翻译文章批量 | `text-embedding-v3`,dim=1024,batch_size=10 |
| 4 | **M6 / MCP 检索查询向量化** | `vectorstore/pipeline.py::search_news`、`mcp_server/server.py` | 每次语义搜索 | 单个查询向量化,量极小 |
### 1.3 不使用大模型的环节
- M1 抓取、M2 正文提取(trafilatura)、M3 去重(SimHash/指纹库)、M6 Qdrant 入库、M9 MySQL 写入 —— 均**不调用大模型**。
---
## 二、真实 token 消耗与费用估算
以 `deepseek-v4-flash`(官方模型名 `DeepSeek-V4-Flash-0731`)当前价格计([官方定价](https://api-docs.deepseek.com/zh-cn/quick_start/pricing/)):
| 计价项 | 空闲时段 | 高峰时段(周一至五 9:00–12:00、14:00–18:00) |
|---|---|---|
| 输入(缓存未命中) | 1.5 元 / 百万 token | 3.0 元 / 百万 token |
| 输出 | 4.5 元 / 百万 token | 9.0 元 / 百万 token |
> 注:本项目常见的定时运行在 07:00(空闲)与 12:00/18:00(接近高峰边界),因此同一日费用波动约 ±100%。
### 2.1 M4 翻译实测 token(来自 `data/events/*/` 的 `prompt_tokens`/`completion_tokens`)
| 日期 | 去重后篇数 | 输入 token | 输出 token | 合计 |
|------|-----------|-----------|-----------|------|
| 2026-08-21(大日) | 82 | 135,616 | 208,146 | **343,762** |
| 2026-08-22(中) | 43 | 74,063 | 88,893 | 162,956 |
| 2026-08-30(小日) | 44 | 73,364 | 103,091 | 176,455 |
单篇平均:输入 ≈1650–1720,输出 ≈2050–2540 token。
### 2.2 单日费用估算(仅 M4)
**小日(约 44 篇,176k token)**
- 空闲:176,455 输入˝中位 ×1.5 + … 简化按 输入73k×1.5 + 输出103k×4.5 = 0.11 + 0.46 = **≈ 0.57 元**
- 高峰:输入73k×3 + 输出103k×9 = 0.22 + 0.93 = **≈ 1.15 元**
**大日(约 82 篇,344k token)**
- 空闲:135,616×1.5 + 208,146×4.5 = 0.20 + 0.94 = **≈ 1.14 元**
- 高峰:135,616×3 + 208,146×9 = 0.41 + 1.87 = **≈ 2.28 元**
### 2.3 日报摘要(M7)
每次约 1–3 批,每批输出几百 token、输入为事件列表,单日 3 次合计约几千 token,费用 ≈ **0.03–0.1 元/天**,可忽略。
### 2.4 结论:费用的绝对构成
- **M4 翻译是绝对主力(>95% 费用)**,日报摘要与 embedding 开销可忽略。
- 单日正常落在 **1–3 元** 区间(视当天文章量与命不命中高峰时段)。
- 若出现 **补跑/积压**:一次把一两天缺的篇全翻,或当日文章量冲到 80+ 篇,单日可能到 **3–6 元**。这通常是用户感受到「费用较高」的真实触发点。
---
## 三、成本高的根因分析
1. **输出 token 巨大**:M4 做的是「全文英译中」,每篇都把整篇中文翻译完整输出,单篇输出 2000+ token,是费用的绝对主体(输出单价是输入的 3 倍)。
2. **每篇独立调用**:82 篇文章 = 82 次往返,system prompt(约 3000 字符模板)每篇重复计入输入。
3. **每天 3 次全流程**:虽增量去重,但当天新文章都翻译,且 07/12/18 各跑一次,频繁触发。
4. **重试放大**:`max_attempts=3`,失败自动重试(实测约 +3%)。
5. **高峰时段触发**:12:00/18:00 的定时可能落入高峰计价,费用 ×2。
6. **付费墙源白跑**:CNBC/Reuters/FT/WSJ 等源抓不到正文,M2 标记 no_content 后**不会**进入 LLM(有防御过滤),因此不会为这些源付费——但它们也没产出,相当于 M1/M2 白耗。真正进 LLM 的只有 investinglive/zerohedge 等能抓到的源,这些源文章较长、翻译输出也就大。
---
## 四、降本改善建议(按性价比排序)
> 以下均为「建议」,未改动生产代码;实施前请评估收益与副作用。
### 建议 A(收益最大):限制 / 缩短 M4 输出
- 全文英译中的输出是主要开销。考虑:
- 将 `llm_scenes.translation.max_tokens` 从 8192 降到合理值(如 4096),或
- 改 prompt,只输出**中文摘要 + 关键事件**,不再整篇英译中(日报和检索实际只消费 `content_zh_preview` 前 300 字与 events,不必整篇全文翻译)。
- 收益:可显著削减输出 token(目前占费用 >60%)。
### 建议 B:尽量把翻译放在空闲时段
- M4 翻译集中到 07:00(空闲,半价)。避免 12:00/18:00 落入高峰计价。
- 仅日报生成放在 18:00(摘要量小,价差影响可忽略)。
### 建议 C:减少不必要的重复翻译(积压/补跑)
- 确保增量机制生效,避免因重复运行把同日文章翻译多遍。
- 若当日中断,下个定时点补齐即可;不要为了「补日报」频繁手动全流程重跑。
### 建议 D:缓存命中优化
- DeepSeek 对「缓存命中」输入大幅降价(0.05 元 vs 1.5 元/百万)。把 system prompt 固定化、相同前缀复用,可提高缓存命中率,降低输入成本(输入约占 40% 费用)。
### 建议 E:换更便宜的模型 / 分流
- 若质量可接受,翻译/摘要改用更低价的同类模型或「空闲时段」更明显的档位。
- embedding 已用 DashScope 低价档,无需动。
### 建议 F:成本监控
- 为 `_process_one` 的成功返回顺带累计 token 到日报 `stats`,或定期统计 `data/events/*/prompt_tokens` 总和,用于成本对账与异常预警。
---
## 五、参考
- [DeepSeek 官方模型与价格](https://api-docs.deepseek.com/zh-cn/quick_start/pricing/)
- 相关代码:`llm/extractor.py`、`llm/pipeline.py`、`scheduler/reporter.py`
- 配置:`configs/system.yaml` 的 `llm` / `llm_scenes` / `embedding` 段
+84
View File
@@ -0,0 +1,84 @@
# LLM 使用点与模型对照清单
> 本文档是「各使用大模型(LLM)的地方分别用哪个 provider / 哪个模型」的**唯一事实源**。
> 数据来源:当前 `configs/system.yaml` + 已生成 `data/events/*/` 的实际调用记录(provider/model 字段)。
## 一、结论速览(当前生效)
| 场景 | 使用位置 | Provider | 模型 | 计费账户 |
|------|----------|----------|------|----------|
| M4 翻译 + 事件抽取 | `llm/pipeline.py` → `llm/extractor.py` | **DeepSeek** | `deepseek-v4-flash` | DeepSeek |
| M7 日报 AI 摘要 | `scheduler/reporter.py::_call_llm_simple` | **DeepSeek** | `deepseek-v4-flash` | DeepSeek |
| M5 向量生成 | `embedding/pipeline.py` | **DashScope** | `text-embedding-v3` | 阿里云百炼 |
| M6 入库向量/MCP 检索查询向量化 | `vectorstore/pipeline.py`、`mcp_server/server.py` | **DashScope** | `text-embedding-v3` | 阿里云百炼 |
> ⚠️ **常见误区**:M4 翻译**用的就是 DeepSeek**(不是 Qwen/DashScope)。有人会误记成「翻译用的是别的模型」,很可能是因为 **M5/M6 的向量化走的是 DashScope(阿里云百炼)**,而翻译走的是 DeepSeek——两者账户 / 账单是分开的。已在 2026-08-21 / 08-22 / 08-30 生成的 `data/events/*/xxx.json` 中核实 87 条记录的 `provider=deepseek`、`model=deepseek-v4-flash`。
## 二、各调用点详情
### 1. M4 翻译 + 事件抽取(DeepSeek)
- **入口**:`llm/pipeline.py::translate_all_deduped` → `llm/extractor.py::translate_and_extract`
- **配置来源**:`configs/system.yaml` → `llm_scenes.translation`
```yaml
llm_scenes:
translation:
provider: "deepseek"
model: "deepseek-v4-flash"
temperature: 0.1
max_tokens: 8192
```
- **API 密钥**:`DEEPSEEK_API_KEY`、`DEEPSEEK_BASE_URL=https://api.deepseek.com`(`.env`)
- **说明**:单篇独立调用,全文英译中 + 投资事件抽取合并一次输出(JSON)。是全项目 token 消耗与费用的绝对主力。
### 2. M7 日报 AI 摘要(DeepSeek)
- **入口**:`scheduler/reporter.py::_call_llm_simple`(`_generate_ai_summary`)
- **配置来源**:`configs/system.yaml` → `llm_scenes.daily_report`
```yaml
llm_scenes:
daily_report:
provider: "deepseek"
model: "deepseek-v4-flash"
temperature: 0.3
max_tokens: 1500
```
- **说明**:高重要度事件分批(≤10 条/批)生成各批摘要后合并;截断时以更高 max_tokens 重试;全部失败走规则兜底。
### 3. M5 向量生成(DashScope)
- **入口**:`embedding/pipeline.py::embed_all_events`
- **配置来源**:`configs/system.yaml` → `embedding` 段
```yaml
embedding:
provider: "dashscope"
dashscope_model: "text-embedding-v3"
dimension: 1024
batch_size: 10
max_attempts: 3
timeout_sec: 30
```
- **API 密钥**:`QWEN_API_KEY` / `DASHSCOPE_API_KEY`、`QWEN_BASE_URL`(`.env`)
- **说明**:批量向量化,输出 dim=1024。费用极低,不计入 DeepSeek 账单。
### 4. M6 入库 / MCP 检索查询向量化(DashScope)
- **入口**:`vectorstore/pipeline.py::search_news`(入库 payload 由 M5 结果构建)、`mcp_server/server.py::search_news`
- **配置来源**:同上 `embedding` 段(入库向量与查询向量必须同模型,故共用一份配置)。
## 三、不使用大模型的环节
- M1 抓取(Playwright/Crawl4AI)、M2 正文提取(trafilatura)、M3 去重(SimHash/指纹库)、M6 Qdrant upsert、M9 MySQL 写入 —— **均不调用 LLM / embedding**。
## 四、切换 provider / 模型时须知
1. **翻译 / 摘要可选 DeepSeek 或 Qwen**:`llm_scenes.<场景>.provider` 设为 `deepseek` 或 `qwen`,同时确认对应 `.env` 里有对应 `*_API_KEY`/`*_BASE_URL`。`llm/client.py::load_llm_config` 支持 provider 覆盖。
2. **Embedding 不能随便切**:Qdrant `en_finance_news` 入库向量与检索向量必须同模型。切换模型必须 `recreate` collection 并全量回灌(`ingest_all_embeddings(recreate=True)`),否则跨模型向量无法比较。
3. **改配置后建议**:改 `provider/model` 后重跑一次管道确认 `data/events/*/` 的 `provider/model` 字段一致,避免两套模型混用。
## 五、参考
- `configs/system.yaml`(`llm` / `llm_scenes` / `embedding`)
- [docs/llm-cost-analysis.md](llm-cost-analysis.md)(成本构成与降本建议)
- [docs/configuration.md](configuration.md) 第 3.5/3.6 节(配置说明)
- [DeepSeek 官方模型与价格](https://api-docs.deepseek.com/zh-cn/quick_start/pricing/)
+15 -8
View File
@@ -7,9 +7,11 @@
# 每天 06:00 / 12:00 / 18:00 / 22:00 各执行一次 # 每天 06:00 / 12:00 / 18:00 / 22:00 各执行一次
# ============================================= # =============================================
# 用法: # 用法:
# ./scripts/domestic_full.sh # 全新执行 # ./scripts/domestic_full.sh # 全新执行(含日报)
# ./scripts/domestic_full.sh --resume # 从中断处继续(跳过已完成步骤, # ./scripts/domestic_full.sh --resume # 从中断处继续(跳过已完成步骤,
# # 步骤状态见 data/run_state/{date}.state) # # 步骤状态见 data/run_state/{date}.state)
# ./scripts/domestic_full.sh --no-report # 只抓取 + M2→M6,跳过日报生成
# ./scripts/domestic_full.sh --no-report --resume
# ============================================= # =============================================
set -euo pipefail set -euo pipefail
@@ -19,10 +21,12 @@ cd "$PROJECT_DIR"
# ── 参数解析 ── # ── 参数解析 ──
RESUME=0 RESUME=0
NO_REPORT=0
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
--resume) RESUME=1 ;; --resume) RESUME=1 ;;
*) echo "未知参数: ${arg}(支持 --resume)" >&2; exit 1 ;; --no-report) NO_REPORT=1 ;;
*) echo "未知参数: ${arg}(支持 --resume / --no-report)" >&2; exit 1 ;;
esac esac
done done
@@ -36,7 +40,7 @@ mkdir -p "$LOG_DIR"
FULL_LOG="$LOG_DIR/domestic_full_$(date +%Y%m%d_%H%M%S).log" FULL_LOG="$LOG_DIR/domestic_full_$(date +%Y%m%d_%H%M%S).log"
: > "$FULL_LOG" : > "$FULL_LOG"
LOG "══════ 国内全流程开始(resume=${RESUME})═══════" LOG "══════ 国内全流程开始(resume=${RESUME} no_report=${NO_REPORT})═══════"
LOG "全流程日志: ${FULL_LOG}(终端实时显示完整进度)" LOG "全流程日志: ${FULL_LOG}(终端实时显示完整进度)"
# ── 加载 .env ── # ── 加载 .env ──
@@ -62,13 +66,16 @@ if step_should_run M1_crawl; then
fi fi
fi fi
# ── 2. M2→M6 管道(含日报)── # ── 2. M2→M6 管道(+ 可选日报)──
LOG "━━━ [2/2] M2→M6 管道 + 日报 ━━━" if [ "$NO_REPORT" = "1" ]; then
if [ "$RESUME" = "1" ]; then LOG "━━━ [2/2] M2→M6 管道(已跳过日报,--no-report)━━━"
bash "$SCRIPT_DIR/pipeline.sh" --resume 2>&1 | tee -a "$FULL_LOG"
else else
bash "$SCRIPT_DIR/pipeline.sh" 2>&1 | tee -a "$FULL_LOG" LOG "━━━ [2/2] M2→M6 管道 + 日报 ━━━"
fi fi
PIPELINE_ARGS=""
[ "$RESUME" = "1" ] && PIPELINE_ARGS="$PIPELINE_ARGS --resume"
[ "$NO_REPORT" = "1" ] && PIPELINE_ARGS="$PIPELINE_ARGS --no-report"
bash "$SCRIPT_DIR/pipeline.sh" $PIPELINE_ARGS 2>&1 | tee -a "$FULL_LOG"
if [ "$M1_FAILED" -eq 1 ]; then if [ "$M1_FAILED" -eq 1 ]; then
LOG "⚠️ 全流程结束,但 M1 抓取未成功,退出码=1" LOG "⚠️ 全流程结束,但 M1 抓取未成功,退出码=1"
+12 -4
View File
@@ -5,6 +5,8 @@
# 用法: # 用法:
# ./scripts/pipeline.sh # 全新执行(步骤级不跳过) # ./scripts/pipeline.sh # 全新执行(步骤级不跳过)
# ./scripts/pipeline.sh --resume # 从中断处继续(跳过已完成步骤) # ./scripts/pipeline.sh --resume # 从中断处继续(跳过已完成步骤)
# ./scripts/pipeline.sh --no-report # 只跑 M2→M6,跳过日报生成
# ./scripts/pipeline.sh --no-report --resume
# 前提:domestic_sync.sh 已完成 # 前提:domestic_sync.sh 已完成
# ============================================= # =============================================
# 各步骤"跳过已处理文件"(文件级增量,由各 Python 模块自带): # 各步骤"跳过已处理文件"(文件级增量,由各 Python 模块自带):
@@ -24,10 +26,12 @@ cd "$PROJECT_DIR"
# ── 参数解析 ── # ── 参数解析 ──
RESUME=0 RESUME=0
NO_REPORT=0
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
--resume) RESUME=1 ;; --resume) RESUME=1 ;;
*) echo "未知参数: ${arg}(支持 --resume)" >&2; exit 1 ;; --no-report) NO_REPORT=1 ;;
*) echo "未知参数: ${arg}(支持 --resume / --no-report)" >&2; exit 1 ;;
esac esac
done done
@@ -40,7 +44,7 @@ LOG_DIR="logs"
mkdir -p "$LOG_DIR" mkdir -p "$LOG_DIR"
STEP_LOG="$LOG_DIR/pipeline_$(date +%Y%m%d_%H%M%S).log" STEP_LOG="$LOG_DIR/pipeline_$(date +%Y%m%d_%H%M%S).log"
: > "$STEP_LOG" : > "$STEP_LOG"
LOG "══════ 全链路管道开始(resume=${RESUME})═══════" LOG "══════ 全链路管道开始(resume=${RESUME} no_report=${NO_REPORT})═══════"
LOG "步骤日志: ${STEP_LOG}(终端实时显示完整进度)" LOG "步骤日志: ${STEP_LOG}(终端实时显示完整进度)"
# ── AI 模型信息读取(供阶段 banner 显性展示供应商/模型)── # ── AI 模型信息读取(供阶段 banner 显性展示供应商/模型)──
@@ -113,11 +117,15 @@ print(f'M6: {stats[\"ingested\"]}/{stats[\"total\"]} 条, {stats[\"elapsed_sec\"
" "
# ── 日报(AI 大模型)── # ── 日报(AI 大模型)──
LOG "━━━ 日报生成(AI 大模型: $(ai_model_info daily_report))━━━" if [ "$NO_REPORT" = "1" ]; then
step_run report .venv/bin/python3 -c " LOG "━━━ 日报生成(已跳过,--no-report)━━━"
else
LOG "━━━ 日报生成(AI 大模型: $(ai_model_info daily_report))━━━"
step_run report .venv/bin/python3 -c "
from scheduler.reporter import generate_report from scheduler.reporter import generate_report
report_id = generate_report() report_id = generate_report()
print(f'日报: report_id={report_id}' if report_id is not None else '日报: 无数据/失败') print(f'日报: report_id={report_id}' if report_id is not None else '日报: 无数据/失败')
" "
fi
LOG "══════ 全链路管道完成 ✅ ═══════" LOG "══════ 全链路管道完成 ✅ ═══════"