Files
simon 1d3ba02703 feat: 日志轮转(保留 14 天)
main.log 无轮转、已涨到 34MB,且每晚任务都会继续追加。

- logrotate/xwlb 模板 + scripts/install_logrotate.sh(幂等,装到 /etc/logrotate.d/xwlb)
- 每天轮转、保留 14 份、立即压缩 → main.log-YYYYMMDD.gz
- 必须用 copytruncate:脚本以 >>main.log 长开句柄,改名式轮转会
  让当晚日志写进归档、新文件空着
- journald 同步设 MaxRetentionSec=14day + SystemMaxUse=500M
- 安装脚本自带 logrotate -d 校验(这条立刻抓到"指令行尾注释"被当成参数值的错误)
- tests/test_logrotate.py 19 项,锁住保留天数、行尾注释、占位符替换

实测:34MB → 2.9MB,归档 32453 行与轮转前完全一致
2026-09-25 12:50:11 +08:00

370 lines
28 KiB
Markdown
Raw Permalink 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.
# xwlb 项目缺陷清单
审查范围:仓库内全部 Python 源码 + `main.log`(2026-06-01 ~ 2026-09-23,34MB,约 46 个有效运行日)。
结论口径:每条缺陷给出**位置 → 问题 → 影响 → 证据 → 修复方向**。证据栏区分"日志实测"与"代码推断"。
严重度定义:
- **P0**:造成数据丢失或数据错误(静默发生、无人察觉)
- **P1**:功能不生效、崩溃、成本浪费、磁盘/日志失控
- **P2**:工程与可移植性问题(换环境即失败)
---
## 汇总表
| # | 严重度 | 位置 | 一句话问题 | 实际影响 | 本次状态 |
|---|---|---|---|---|---|
| B1 | P0 | `audioRead.py:186,191-193,260` | qwen 校对超时(300s)被当成 ASR 失败,整片文本丢弃 | 46 天、57 次,每天约丢 2/11 分片(~1400 字)静默缺口 | ✅ 已修复并实测 |
| B2 | P0 | `audioRead.py:275,289-292` | 校对结果恒为 `None`,回退原文 | 校对功能 0 收益,却贡献了全部 57 次超时与 token 成本 | ✅ 已修复并实测 |
| B3 | P0 | `newsProcess.py:89,119-121` | DeepSeek 返回非纯 JSON(```json 围栏 / 截断)即整批失败 | 22 天 `xwlb_daily_ext` 当天 0 条 | ✅ 已修复(回归测试覆盖) |
| B4 | P0 | `getVideo5.py:69,92,156` | `get_xwlb_video_link()` 返回值错误(只回最后一个 / 未定义 / `[]`) | 取错链接、`UnboundLocalError`、无效 URL 仍进下载 | ✅ 已修复并实测(联网) |
| B5 | P0 | `getVideo5.py:104-146`、`audioRead.py:389-399` | 下载与分片落库无幂等 | 重跑 → 重复下载 + `xwlb_daily` 重复行 → 新闻重复、token 翻倍 | ✅ 已修复并实测 |
| B6 | P1 | `audioRead.py:260`、`getVideo5.py:132-140` | 关键外部调用无 timeout/重试;ffmpeg 输出未接管 | 单点失败无补救;`main.log` 34MB 且无轮转 | 🟡 部分(校对已加超时/重试;ffmpeg 输出已接管;**日志轮转已完成**;yt-dlp 进度未处理) |
| B7 | P1 | `deepseek.py:257-268` | 降级分支 `return result`(变量错/可能未绑定) | DeepSeek 失败时抛 `NameError`,降级形同虚设 | ✅ 已修复(`return fallback_result`) |
| B8 | P1 | `audioRead.py:422-439` | `__main__` 示例用 3 参数调 4 参数签名 | `python audioRead.py` 必错 | ✅ 已修复(改为 `python audioRead.py [YYYYMMDD]`) |
| B9 | P1 | `audioRead.py:387`、`getVideo5.py:104-146` | 临时文件不清理(chunk/input.wav/mp4) | 单日约 180MB 常驻,磁盘持续增长 | 🟡 部分(分片与按日 WAV 已在 `finally` 清理;mp4/mp3 保留策略待定) |
| B10 | P2 | `mysqlHandle.py:5` | 依赖父项目 `utils.mysql_handler`,本机不存在 | 所有脚本 import 即 `ModuleNotFoundError` | ✅ 已修复(内置 `mysql_handler.py`) |
| B11 | P2 | `getVideo5.py:170,178`、`audioRead.py` | 硬编码 `/home/simon/myquant/djapi/...` | 换机即废(本机已复现) | ✅ 已修复(基于项目目录 + 环境变量可覆盖) |
| B12 | P2 | `env.py:9` | `.env` 定位按老 djapi 目录层级,且不覆盖已有变量 | 指向 `/home/pi/.env`,实际不存在 | ✅ 已修复(项目内 `.env`,支持 `XWLB_ENV_FILE`) |
| B13 | P2 | `newsProcess.py:56`、`newsRedo.py:69` | "已处理"判据为 `COUNT(*) > 5` | 少于 5 条的日期被判定未处理,无限重跑 | ✅ 已修复(存在任意记录即视为已处理,`--force` 可强制) |
| B14 | P2 | 仓库根目录 | 产物入库、非 git 仓库 | 354MB 产物 + 34MB 日志 + 失效 `.pyc` | 🟡 部分(已加 `.gitignore`、清理失效 `__pycache__`;未 `git init`、历史产物未清) |
| B15 | P2 | 全局 | 无 `.env.example`,密钥缺失/失效时报错不指向配置 | 401 当天 33 次报警,排查成本高 | 🟡 部分(已加 `.env.example` 与连接失败明确报错;启动期配置校验未做) |
---
## 本次修复的验证记录(实测,非推断)
验证环境:项目 venv(Python 3.13.5)+ 真实 DashScope/DeepSeek key + 通过 SSH 隧道(13306)连接生产 MariaDB。
端到端验证使用**测试日期 `1900-01-01`** 与裁剪后的 5 分钟音频,验证完成后已删除测试数据(`xwlb_daily` 2 行、`xwlb_daily_ext` 3 行,复查均为 0)。
| 验证项 | 方法 | 结果 |
|---|---|---|
| B12 配置加载 | 运行任意脚本 | `已加载配置文件 /home/pi/project/xwlb/.env(注入 18 个变量)` |
| B10 DB 层 | `import mysqlHandle` 后直接构造 `MySQLDB()` | 连接成功;故意用错端口时抛出带修复指引的 `RuntimeError`(不再是 `AttributeError`) |
| B8/B1/B2 真实链路 | `process_long_audio()` 处理 5 分钟真实音频 | 2 个分片全部成功;校对**有真实产出**:677→699、635→632 字;`返回 {'chunks': 2, 'ok': 2, 'failed': 0}`;落库 2 行(2011/2079、1885/1880 字),**空白行 0** |
| B5 分片幂等 | 同 `(日期, 分片)` 连续写入 3 次(其中 2 次同键) | 只保留 2 行(子号 0、1)且为最新内容;对照旧逻辑会累积成 3 行 |
| B3 解析健壮性 | `tests/test_news_parse.py`,含日志中真实坏返回 | 15 项全部通过(```json 围栏、夹解释文字、对象包装、混入字符串、缺字段等均可解析;截断/非 JSON 按预期抛错触发重试) |
| B3/B5/B13 切分入库 | 对同一测试日期连续执行 3 次 `news_to_db` | 第 1 次切出 3 条并写入;第 2 次(无 force)正确跳过;第 3 次(force)**替换**旧 3 条 → 仍为 3 条,重复 `sub_id` 组数 = 0 |
| B4 抓取 | 联网解析 20260923/20260924/20260925 三天页面 | 返回类型为 list;09-23、09-24 各解析出 1 条完整版链接;09-25(尚不存在,404)被明确记录并跳过,不再产生空 URL |
| **2026-09-04 生产级实跑** | `getVideo5.py 20260904 20260904` 完整跑一遍(下载 98MB → 抽音 → 11 片 ASR+校对 → 切分入库) | 11/11 分片成功(原空白分片 7 补回 2091 字);切分首轮遇 `Unterminated string`(**正是当初让该日 0 条精编的原因**),第 2 轮强化提示词成功切出 34 条;见下方"事实篡改"一节,修复后复查 0 漂移 |
| 数值事实守卫 | `tests/test_fidelity_guard.py`(13 用例) | 全部通过:`2027→2024`、`十一→九`、`十五五→十四五`、`5.3→5.31`、`300→30` 均拦下;`7↔七`、`2026↔二零二六`、`55%↔百分之五十五` 均放行 |
> 未验证项:本轮未对 `xwlb_daily` 中既有的 101 行空白记录与 203 天缺失 `xwlb_daily_ext` 做数据修复/回填——那属于数据治理,需你确认后再执行(会消耗 API 额度)。
---
## P0 详情
### B1 qwen 校对超时导致已成功的 ASR 结果被静默丢弃
**位置**:`audioRead.py:186`(`transcribe_audio` 内调用 `analyze_and_correct_text`)、`:260`(`Generation.call`,未设 timeout)、`:191-193`(异常兜底 `return ['', '']`)、`:369-399`(`process_long_audio` 落库,无条件插入)
**问题**:ASR 识别与"qwen 文本校对"被串在同一个 `try` 里。校对调用一旦抛异常(实测全部是 300s 读超时),异常被 `transcribe_audio` 的兜底捕获,函数返回 `['', '']`——**已经识别成功的文本不再返回**。`process_long_audio` 随后照常以空字符串插入一行。
**影响**:
- 每个受影响分片丢失约 700 字识别文本,且**没有任何失败标记**(`news_raw=''`、`news_improve=''` 与"正常但短"无法区分);
- 46 个运行日受影响,每天约 2 个分片,累计约 4 万字内容缺口;
- `news_to_db` 拿到的是缺失版全文,DeepSeek 切分出的新闻条数/内容随之缺失,且上游完全无感知。
**证据(日志实测)**:`read timeout=300` 共 57 次,**前一行 100% 是"调用通义千问模型进行文本修正"**(校验:`grep -B1 "read timeout=300" | grep -c 调用通义千问` = 57,`grep -c 开始识别音频` = 0),说明超时发生在校对而非识别。分布 2026-06-18 ~ 2026-09-23,覆盖 46 个不同日期,最后一天(09-23)仍在发生,**属存活缺陷**。
**修复方向**:
1. 解耦:ASR 成功即先落库 `news_raw`,校对作为独立后续步骤(失败保留原文,不丢数据);
2. 校对调用单独设 `timeout`(dashscope 支持 `timeout=`)+ 有限重试,超时按"放弃校对"处理而非"放弃识别";
3. 空文本不入库,或入库但显式记录 `status=failed` 供 `news_to_db` 跳过。
---
### B2 qwen 校对功能实际从未生效
**位置**:`audioRead.py:275`(`return response.output.text`)、`:277-299`(`analyze_and_correct_text`)
**问题**:`text_correction` 返回 `None`(未抛异常),`analyze_and_correct_text:290-292` 判断为 `None` 后回退原文。
**影响**:
- 校对本应修错别字/标点,实际 `news_improve == news_raw`,**功能 0 收益**;
- 代价是全部 57 次 300s 超时(B1 的根因)、每个分片 30~300s 的额外耗时,以及 qwen 的全部 token 成本;
- 它同时把"校对"变成了"随机丢分片"的赌局。
**证据(日志实测)**:每个分片固定出现 `✓ 文本修正完成` → `WARNING - 文本修正返回 None,使用原始文本` → `原始文本长度: N` / `修正后文本长度: N`(两值恒等;如 09-23 的 798/798、708/708、688/688)。
**疑似原因**:`Generation.call` 走 `messages` 时该 SDK 版本的文本取法应为 `response.output.choices[0].message.content`,`.output.text` 为空;或 `max_tokens=30000` 超出模型上限导致 output 为空但 status 仍 200。
**修复方向**:修正结果字段取法(或改用 OpenAI 兼容接口);若确认无收益则直接删掉这一步,只保留 ASR。
---
### B2 补充(2026-09-25 实测发现):校对修好后会**篡改事实**
把 B2 修好之后校对第一次真正生效,随即在 2026-09-04 的真实数据上发现它**改错了事实**:
| 内容 | ASR 原文 | qwen 校对后 | 判定 |
|---|---|---|---|
| 条例施行日期 | 自 **2027** 年1月1日起施行 | 自 **2024** 年1月1日起施行 | ❌ 改错(新签署的条例不可能追溯生效) |
| 五年规划 | **十五五** | **十四五** | ❌ 改错(2026 年应为十五五) |
| 东方经济论坛届次 | **第十一**届 | **第九**届 | ❌ 改错(同一天开场提要也作"第十一届") |
| 年份(多处) | 2025 / 2026 | 2023 / 2024 | ❌ 改错 |
| 数量 | 55 / 300亿 | 60 / 30亿 | ❌ 改错 |
11 个分片中 **8 个**存在数值/序数被改写;这些错误会经 `news_improve` 直接进入最终 `xwlb_daily_ext`(DeepSeek 忠实复制了它的输入)。
**影响**:这比"校对不生效"更危险——原文虽是 ASR 结果但事实可信,校对后反而产生**看起来通顺但事实错误**的文本。
**已实施的修复**:
1. **数值事实守卫**(`audioRead._number_drift`):中文数字与阿拉伯数字统一归一到数值后比较,若校对前后数值集合与拼接串均不一致,则该分片**整片回退 ASR 原文**;`百分之X` 与 `X%`、`7 ↔ 七`、`2026 ↔ 二零二六` 等纯书写差异不会误拦。
2. **提示词加固**:明确禁止改动数字/年份/日期/届次/数量/机构名/人名/地名/专有名词。
3. **开关**:`LLM_CORRECT_ENABLED=0` 可完全关闭校对(直接以 ASR 原文入库)。
4. 回归测试 `tests/test_fidelity_guard.py`(13 个用例,含上表中的真实篡改样例)。
**取舍**:被守卫拦下的分片会连"合理的 ASR 修复"一起放弃(例如同一分片里的 `弗拉迪沃斯。波克` → `符拉迪沃斯托克`、`丁学祥` → `丁薛祥`)。守卫的取向是**宁保留原文,不接受 LLM 改数**。
**待你决定**:`LLM_CORRECT_ENABLED` 的默认值。当前默认 `1`(开启+守卫),另一选择是默认 `0`(完全不改写原文,最保守,且每天省下 11 次 qwen 调用)。
---
### B3 DeepSeek 返回非纯 JSON 时整批切分丢失
**位置**:`newsProcess.py:86-89`(提示词与 `deepseek_text`)、`:119-121`(`json.JSONDecodeError` 仅记录后放弃)
**问题**:依赖 `response_format={"type":"json_object"}` 保证纯 JSON,但模型仍会输出 ```json 围栏或超长截断(`max_tokens=20000`),解析失败后**不重试、不降级**,直接丢弃当天全部文本。
**影响**:22 个日期 `xwlb_daily_ext` 当天 0 条记录;由于 B13 的判据,这些日期之后会被反复重跑,而每次重跑又触发 B5 的重复写入。
**证据(日志实测)**:`JSON解析失败` 22 次,覆盖 22 个不同日期(2026-06-03 ~ 2026-09-14);报错内容含 `Expecting value: line 1 column 1`(前后被 ``` 包裹,日志里可见 `DeepSeek API返回内容: ```json`)与 `Unterminated string starting at: line 80 column 21`(截断)。另有 1 次 `插入数据库失败: string indices must be integers`,说明 `news_list` 元素有时是字符串而非 dict(`news["news_id"]` 直接炸)。
**修复方向**:剥离围栏后再解析 → 失败则重试一次(提高 max_tokens / 降低 temperature)→ 仍失败则按空行/段落降级切分;对 `news_list` 元素做类型校验与字段缺失兜底。
---
### B4 `get_xwlb_video_link()` 返回值错误
**位置**:`getVideo5.py:34-69`(构建 `video_links` 但 `return video_url`)、`:86-94`(`get_all_video_links`)、`:149-171`(`process_videos`)
**问题**:三处叠加。
1. `:69` 返回的是循环里最后一次赋值的**单个 URL 字符串**,`video_links` 列表被丢弃 → 当天若有多个匹配链接只取最后一个;
2. 请求异常路径 `return []`(`:28,:31`),但 `get_all_video_links:92` 仍包装成 `{"url": [], "date": ...}`;
3. `process_videos:156` 用 `if sub_url:` 判断——非空 dict 恒为真,**错误值不会被拦截**,`[]` 会被送进 yt-dlp;
4. 若一个 a 标签都没匹配到,`video_url` 从未赋值 → `UnboundLocalError`(不是被捕获的异常类型)。
**影响**:抓取失败/页面改版时不是"跳过并告警",而是带着无效 URL 继续下载,或用错链接下载到非新闻联播内容;幂等性判断(B5)也因此失效。
**证据**:代码推断(`get_xwlb_video_link` 内 `video_links` 变量在 `:34` 定义、`:61` 追加、`:69` 未被返回,可静态确认)。
**修复方向**:`return video_links`;调用方展开列表并对空值 `continue`;`if sub_url.get('url')` 显式判空。
---
### B5 下载与分片落库无幂等
**位置**:`getVideo5.py:104-146`(`download_and_extract_audio` 不检查 mp4/mp3 是否已存在)、`audioRead.py:389-399`(无条件 `insert_data`)
**问题**:`newsRedo.py:79-108` / `main_videos.py:23-58` 判断"是否需要处理"只看 `xwlb_daily` 有没有当天的行。而重跑会**追加**一整组 `daily_sub_id = 0..N` 的新行(`nid` 自增,`(news_days, daily_sub_id)` 无唯一约束)。
**影响**:
- `get_news_improve_by_date`(`newsProcess.py:61-65`)按 `daily_sub_id` 排序拼接,会把同一天的文本**重复拼两遍**送进 DeepSeek → 切分结果重复、token 翻倍、可能超出上下文;
- 重复下载 100MB 级 mp4(B9 磁盘问题同步放大);
- 与 B13 组合会形成"重跑 → 重复 → 再重跑"的循环。
**证据**:代码推断(无唯一索引、无存在性检查)。
**修复方向**:`(news_days, daily_sub_id)` 加唯一索引 + `INSERT ... ON DUPLICATE KEY UPDATE`;下载前检查目标 mp4/mp3 是否存在且非空;`daily_sub_id` 用分片序号而非可变下标。
---
## P1 详情
### B6 外部调用无超时/重试,子进程输出未接管
**位置**:`audioRead.py:258-266`(`Generation.call` 无 timeout、无重试)、`:168-186`(`Recognition.call` 单次失败即放弃)、`getVideo5.py:132-140`(ffmpeg `check=True` 但 `stdout=None, stderr=None`)、`:123`(yt-dlp 进度 hook 直接 `print`)
**影响**:
- 单分片 ASR 失败无任何补救,直接进入下一片;
- ffmpeg 的完整 banner(版本、编译参数、流信息)与 yt-dlp 的逐帧进度全部落到 stdout,`main.log` 已达 34MB 且**无 logrotate**;一次失败排查需要在数万行进度里翻找;
- 超时阈值 300s 是 SDK 默认,与业务预期(每天 51 分钟跑完)不匹配。
**修复方向**:统一 timeout + 指数退避重试;ffmpeg 用 `stdout=subprocess.DEVNULL, stderr=subprocess.PIPE` 并只在失败时打印尾部;日志加 `RotatingFileHandler` 或把进度输出降级为 DEBUG。
---
### B7 `deepseek.py` 降级分支变量写错
**位置**:`deepseek.py:241-268`
**问题**:`try` 中 `result = api_client.process_text(...)`;`except` 里调用 `process_text_with_fallback(...)` 得到 `fallback_result`,却 `return result`。异常路径下 `result` 未绑定 → `NameError`(若部分成功则返回错误内容)。
**影响**:本应"降级返回原文"的兜底变成二次异常,被 `newsProcess.py:122` 的宽 `except` 吞成一行 `插入数据库失败`,与 B3 的静默失败叠加。
**证据**:代码推断(`deepseek.py:261-268`)。**附带**:`deepseek.py:271` 的 `deepseek_text()` 缺必填参数,`python deepseek.py` 直接 `TypeError`。
**修复方向**:`return fallback_result`;`__main__` 补参数或改成 CLI。
---
### B8 `audioRead.py` 的 `__main__` 示例已过期
**位置**:`audioRead.py:422-439`
**问题**:`process_long_audio(mp3_path, prompt, output_folder)` 传 3 个位置参数,而签名是 `(mp3_path, output_folder, date_str)` → `prompt` 被当作输出目录,`date_str` 缺失。
**影响**:任何人按文件底部示例单独调试都会失败,且会把中文提示词当目录名创建。
**修复方向**:改为 `process_long_audio(mp3_path, output_folder, date_str)`,或直接删掉示例。
---
### B9 临时文件与产物不清理
**位置**:`audioRead.py:387`(仅成功分支 `os.remove(chunk_path)`)、`:352-354`(`input.wav` 不删)、`getVideo5.py:113-114`(mp4/mp3 长期保留)
**影响**:单日约 104MB mp4 + 24MB mp3 + 55MB wav ≈ 180MB;仓库当前已含 `xwlb_video/` 299MB、`audio_processing/input.wav` 55MB。长期运行必然打满磁盘,且在失败分支 chunk 会残留。
**修复方向**:处理完即删中间产物(或按保留天数清理);mp3 作为可重建产物不必长期保存;`finally` 中清理 chunk。
---
## P2 详情
### B10 `utils.mysql_handler` 缺失(当前无法运行的首要原因)
**位置**:`mysqlHandle.py:4-5`
**问题**:`sys.path.insert(0, <xwlb 的父目录>)` 后 `from utils.mysql_handler import MySQLDB`,而 `/home/pi/project/utils/` 不存在。
**影响**:所有脚本(包括仅查库的 `newsRedo.py`)**import 阶段即失败**:已在本机复现 `ModuleNotFoundError: No module named 'utils'`。
**证据(日志实测,反向线索)**:日志中 `查询失败: local variable 'cursor' referenced before assignment` 说明该模块在连接失败时自身还有未初始化变量缺陷——重写时应一并避免。
**修复方向**:在项目内提供自包含的 `mysql_handler.py`,保持 `MySQLDB()` / `query_data(table, columns, where, params)` / `insert_data(table, data)` / `close()` 四个接口不变,配置从环境变量读取。
---
### B11 硬编码绝对路径
**位置**:`getVideo5.py:170`(`/home/simon/myquant/djapi/api/video/xwlb_video`)、`:178`(同前缀 `audio_processing`)、`audioRead.py:353` 的输出目录同样来自该前缀
**影响**:迁移到任何其他机器都不可用;本机 `pwd` 为 `/home/pi/project/xwlb`,正确位置应是仓库内的 `xwlb_video/` 与 `audio_processing/`(数据也确实已放在那里)。
**修复方向**:以 `Path(__file__).parent` 为基准,或由 `env` 提供 `XWLB_VIDEO_DIR` / `XWLB_AUDIO_DIR`。
---
### B12 `.env` 定位与优先级
**位置**:`env.py:9`(`parent.parent.parent / '.env'` → `/home/pi/.env`)、`:23`(`if key not in os.environ` 不覆盖)
**影响**:按老 `djapi/api/video/` 三层结构推导,本项目只有一层,指向错误位置且文件不存在 → 密钥全部缺失;"不覆盖已有变量"的行为未文档化,容易与容器注入的变量混淆。
**修复方向**:改为项目根/`xwlb` 目录下的 `.env`,并支持 `XWLB_ENV_FILE` 覆盖;在文档中写明优先级(环境变量 > `.env`)。
---
### B13 "已处理"判据用行数阈值
**位置**:`newsProcess.py:49-57`、`newsRedo.py:62-71`
**影响**:某天若只切出 ≤5 条新闻(短节目/切分异常),会被永久判定为"未处理"并在每次补漏时重跑;配合 B5 造成重复数据。
**修复方向**:以"是否已经跑过切分"的显式状态为准(如 `xwlb_daily_ext` 存在任意行即视为已处理,或新增处理状态表/字段)。
---
### B14 仓库卫生
**位置**:仓库根目录
**问题**:`main.log` 34MB、`xwlb_video/` 299MB、`audio_processing/input.wav` 55MB 与源码混放;`__pycache__/` 内含 `ai.cpython-310.pyc`、`parsem3u8.cpython-310.pyc` 等**已删除模块**的产物(3.10/3.11,与当前 3.13 不符);目录**不是 git 仓库**,无变更历史可回溯。
**修复方向**:加 `.gitignore`(`*.mp4 *.mp3 *.wav main.log __pycache__/`)、`git init` 纳入版本管理、清理失效 `__pycache__`。
---
### B15 配置与密钥错误不可诊断
**位置**:全局(`deepseek.py:21-23` 只 warning、`audioRead.py:166` 直接赋值空串)
**影响**:`DASHSCOPE_API_KEY` 无效/缺失时表现为 2026-06-01 那天连续 33 条 `401 Unauthorized` + 11 个分片全废,但报错信息不指向"检查 `.env`";`DeepSeekAPI` 缺 key 仅 warning,随后才在调用处抛异常。
**修复方向**:提供 `.env.example`;启动时做一次配置校验(缺 key 直接 fail-fast 并打印期望的文件路径)。
---
## 本轮新发现(2026-09-25,B16~B19)
### B16(严重 · 静默换模型)兜底分支偷偷用另一个模型
**位置**:`deepseek.py` `process_text_with_fallback`
**问题**:兜底调用 `self.process_text(prompt, text, system_prompt)` **未传 model**,于是用函数签名里的硬编码默认值 `deepseek-chat`。
**影响**:配置里的模型名无效(如 `deepseek-v4.1-flash`)时,主调用 400 报错、兜底却用另一个模型成功返回,**"按配置运行"看起来完全正常,实际配置从未生效**——排障方向会被彻底带偏。
**修复**:兜底显式传入同一模型与参数;切分环节改为 `use_fallback=False`(它自带重试与解析校验),配置错误直接暴露。现已验证:模型名写错立刻报 `The supported API model names are ...`。
### B17(严重 · 数据完整性)重试判定会把"半天"当"一天"
**位置**:`scripts/day_status.py`(本轮新增)
**问题**:只看"库里有没有分片"就判断"只缺切分"。识别中途卡死同样会留下部分分片,
于是重试走"只重跑切分",把半天内容切分入库并标记为完成。
**影响**:静默产生**不完整的一天**,且不会报错;事后极难发现(那天看起来是正常的)。
**修复**:识别全部完成后写标记 `state/.asr_complete_<日期>`(放 `state/`、**不随清理删除**,
否则无法与"识别只跑了一半但切分照样写了 ext"区分),`day_status.py` 只有**同时**满足
"有分片 + 有标记"才允许只重跑切分,否则重跑全链路;
并在 `process_videos` 中改成**识别不完整就不执行切分**,不发布"看起来完整"的半天数据。
### B18(高 · 推理模型截断/空回复)max_tokens 未按推理模型调整
**位置**:`config.yml` `llm_split.max_tokens`(原 20000 / 重试 32000)
**问题**:`deepseek-flash` 是推理模型,reasoning token 实测在 14k~24k 间波动且**计入 max_tokens**。
20000 时实测出现两种故障:输出截断在 JSON 中途(`finish_reason=length`)、
或推理耗尽全部额度导致 **content 为空**(报"LLM 响应为空")。
**影响**:切分随机失败或写入被截断的新闻;每晚概率性发生。
**修复**:上限提到 60000(重试 80000)——按实际产出计费,给足上限不额外花钱;
`timeout` 60→180 秒(2 万 token 级别输出需要更久)。
### B19(高 · 可用性)ASR 的 `Recognition.call` 无超时,长连接悬挂会卡死整晚
**位置**:`audioRead.transcribe_audio` → dashscope `Recognition.call`
**问题**:SDK 内部 `for part in responses` 无限等待 websocket 响应,**没有任何超时参数**
(签名无 timeout,内部 `while True`)。实测 2026-09-24 重跑时卡在 chunk 3 之后:
进程 0 CPU、`futex_do_wait`、无新日志,**15 分钟以上无进展**。
**影响**:手动跑会一直挂着;定时任务靠外层上限兜底,但会白白耗掉一次尝试。
**缓解**:`scripts/run_daily.sh` 单次尝试 40 分钟上限 + 21:30/22:00 重试;
systemd `TimeoutStartSec=10800`。**根治需要给 ASR 加子进程看门狗**(列入待办)。
---
## 后续待办(本轮新增)
7. **ASR 看门狗**(对应 B19):把 `Recognition.call` 放进子进程执行并加超时,
超时终止子进程、该分片记失败后**继续下一片**,而不是整晚挂死。
8. **回补校对失效期**(见 `REPORT_raw_vs_improve.md`):2026-06-15~2026-09-23 的
`news_improve` 实际等于 ASR 原文(2026-07/08 为 100% 未改动)。校对现已修复且
关掉思维链后成本降到约 1/13,回补约 100 天 ≈ 1M token,需确认是否执行。
9. **重试跳过已识别分片**:当前重试会重跑全部 ASR(识别已完成的片也重做)。
加"已入库分片跳过"可让重试只补缺失片,需注意别让 `newsRedo.py` 的强制重跑被静默跳过。
---
## 后续待办(本轮未做)
1. **数据治理**:`xwlb_daily` 现存 101 行空白记录(可识别、可重跑修复);725 天原文里 203 天没有 `xwlb_daily_ext`——回填需重新调用 DeepSeek,属于有成本的操作,需你确认范围后再执行。
2. **B6 收尾**:yt-dlp 进度 hook 仍直接 `print`;
~~`main.log` 无轮转~~ —— **已完成(2026-09-25)**:logrotate 每天轮转、保留 14 天、立即压缩
(`logrotate/xwlb` + `scripts/install_logrotate.sh`),journal 同步设 `MaxRetentionSec=14day`;
实测 34MB → 2.9MB 且归档行数与轮转前一致(32453 行)。
3. **B9 收尾**:`xwlb_video/*.mp4` 与 `*.mp3` 是否保留需定策略(mp3 可重跑,mp4 价值较低)。
4. ~~**B14 收尾**:`git init` 纳入版本管理~~ —— **已完成(2026-09-25)**:
已建仓并推送到 `git@192.168.1.10:simon/xwlb.git`(分支 `main`),
`.gitignore` 覆盖 `xwlb_video/`、`audio_processing/`、`*.log`、`.env`、`.venv/`、`state/`;
首提交 35 个文件、无密钥(已按 `.env` 逐个值核对)。
仍未做:`main.log` 已 34MB 且无轮转(见第 2 条 B6),建议加 logrotate。
5. **B15 收尾**:启动期做一次配置校验(缺 key / 连不上库时 fail-fast 并打印期望文件路径)。
6. **可选加固**:给 `xwlb_daily` 加唯一索引 `(news_days, daily_sub_id)`、`xwlb_daily_ext` 加 `(news_date, sub_id)`。当前代码已用「先删后插」实现幂等,索引属额外保险;注意建索引前必须先清理既有重复行(如 2026-06-15 全量重复 2 份、2026-06-18 ext 重复 3 份)。
## 修复优先级建议(原始评估,供回看)
1. **B2 + B1 一起做**(改/删校对步骤,保住已识别文本)—— 直接消除最大的静默数据缺口,同时省下每分片 30~300s 与全部 qwen 成本。
2. **B3**(JSON 解析健壮化 + 重试)—— 消除 22 天的整天数据缺失。
3. **B10 + B11 + B12**(可移植化)—— 让项目在任何机器可运行,是继续验证与修复的前提。
4. **B5 + B13**(幂等 + 判据)—— 防止修复过程中把重复数据写进生产库。
5. **B4**(抓取返回值)—— 抓取链路的正确性兜底。
6. B6~B9、B14、B15 —— 稳定性与工程卫生。
> 注:`main.log` 中 `'str' object has no attribute 'append'`(22 次)与 `object of type 'NoneType' has no len()`(11 次)**仅出现在 2026-06-01 与 06-16**,当前 `audioRead.py` 已分别通过"返回 list"与"`None` 回退原文"修掉,故未单列;但它们与 B1/B2 同源,重写校对步骤时不要再引入同类返回类型不一致的写法。