# Travel Intel

> 贵州之客旅游情报系统 — 统一采集+分析贵州旅游行业与竞品信息，定时产出分级报告，同时为其他技能提供两级回退查询接口。合并 travel-knowledge + travel-monitor。

- Skill: `jorinyang/travel-intel` (Agent Skill, multi-file: 30 files)
- Install (CLI): `npx skillmds@latest add jorinyang/travel-intel`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jorinyang/travel-intel/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: jorinyang (https://skillmd.com/u/jorinyang)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/jorinyang/travel-intel

---


# travel-intel — 贵州之客旅游情报系统

> **定位：** 合并 travel-knowledge + travel-monitor。一条流水线：采集→入库→过期校验→分级报告+查询。

## 架构

```
采集层 (Collector)
  ├─ L1a: agent-browser 百度+夸克 (WSL本地 06:30)
  ├─ L1b: opencli 微博热搜+知乎热榜 (WSL本地 06:35) ★新增
  ├─ L2: urllib 站点直抓 (云端 07:00)
  ├─ L3: Bitable 队列分发 → agent-browser 深度搜索 (WSL本地 每5分钟)
  ├─ 分类路由：竞品→VYsjwBZ6Ri / 行业→WnAmwXK6xi (✅ 2026-07-04 节点重建)
  └─ 同 URL 去重
      ↓
校验层 (Expiry)
  ├─ 每日 03:00 扫描三节点（行业+竞品+咨询洞察一级）
  └─ 15类过期规则 + 评论标记
      ↓
报告层 (Reporter)                   查询层 (Querier)
  ├─ 每日简报 09:00                   ├─ Wiki 搜索 → 降权
  ├─ 周度分析 周一 09:00              └─ 未命中 → web_search
  └─ 综合洞察 周六 10:00
```

详见 [references/agent-browser-setup.md](references/agent-browser-setup.md) 和 [references/l3-bitable-dispatch.md](references/l3-bitable-dispatch.md)。

## 存储节点

> 行业资讯和竞品动态现已归入「咨询洞察」一级分类下。node_token 不变。

| 节点 | Wiki Token | 所属分类 | 内容 |
|------|-----------|---------|------|
| 行业资讯 | `WnAmwXK6xiqlh3kXGN6cHYGinxe` | 咨询洞察 | 政策/景点/交通/酒店/活动/报告 |
| 竞品动态 | `VYsjwBZ6Rif5dFkboigchjgDnac` | 咨询洞察 | 竞品价格/新品/营销/社媒 |
| **咨询洞察(一级)** | **`UF7Cw5w2WiHGfjkKVvBcxj8Hnib`** | **一级分类** | **回退节点；正常情况使用子分类** |
| L3 任务队列 | `TDYYwZ0T0ifLtdkK9iOcp2HTnwf` (Bitable) | 咨询洞察·行业资讯下 | 深度搜索关键词队列 |
| Space ID | `7643710721485753535` | | |
| 推送群 | `oc_40570cc921ca1f645f8667151c1e85e6` | | |

---

## 模块 1: 采集引擎 (Collector)

### 三条通道

| 优先级 | 通道 | 方法 | 环境 | 调度 | 备注 |
|:--:|------|------|:--:|------|------|
| L1 | 百度 + 夸克 | agent-browser 通用搜索 | 🏠 WSL本地 | 06:30 | 行业+竞品关键词，双引擎互补 |
| L2 | urllib 站点直抓 | Python urllib.request | ☁️ 云端 | 07:00 | 品橙/迈点/闻旅/执惠 |
| L3 | 百度/B站/头条 | agent-browser 深度搜索 | 🏠 WSL本地 | 每5分钟轮询 | 云端 cron → Bitable 队列 → 本地 poller |

### L3 Bitable 分发架构 ★

云端 travel-intel-collect (07:00) 在完成 L2 站点直抓后，根据当日采集概况精选 4-6 个深度研究关键词，写入 Bitable 任务队列。WSL 本地 l3_poller.py 每 5 分钟轮询，使用 agent-browser 执行搜索，结果入库 Wiki。

```
Cloud cron (07:00)        Bitable 队列               WSL crontab (每5分)
┌─────────────────┐     ┌──────────────┐     ┌────────────────────┐
│ travel-intel-   │────▶│ TDYYwZ0T0if  │────▶│ l3_poller.py       │
│ collect         │     │ tblVKG82oOl  │     │ ├─ 读取 pending     │
│ Step 4: L3分发  │     │  搜索关键词   │     │ ├─ agent-browser    │
│  4-6个关键词    │     │  平台(百度/   │     │ │  百度/B站/头条    │
│  竞品优先       │     │  B站/头条/综合│     │ ├─ 创建Wiki文档    │
└─────────────────┘     │  结果摘要     │     │ └─ 更新任务状态    │
                        └──────────────┘     └────────────────────┘
```

**Bitable 字段:**
| 字段 | 用途 | 备注 |
|------|------|------|
| Text | 任务名称 (如"竞品_探洞行业动态") | 含"竞品"→路由到竞品动态节点 |
| 搜索关键词 | agent-browser 搜索词 | |
| 平台 | 百度/B站/头条/综合 | 综合=三平台全搜 |
| 结果摘要 | 任务状态+结果URL | pending→processing→done/failed |

**平台选择建议:**
- 竞品深度研究 → 综合 (百度+B站+头条全覆盖)
- 政策类 → 百度
- 视频内容 → B站
- 资讯动态 → 头条

### L1 本地采集架构

**L1 由两个子通道组成：**

| 子通道 | 引擎 | 数据源 | 内容 | 调度 |
|:--:|------|------|------|:--:|
| **L1a** | agent-browser | 百度 + 夸克 | 关键词搜索（行业10词+竞品10词） | 06:30 |
| **L1b** | opencli | 微博热搜 + 知乎热榜 | 热榜扫描→旅游关键词过滤 | 06:35 |

**WSL crontab (每天 06:30) → l3_cron.sh → browser_collector.py --channel L1 → hotlist_collector.py**

### L1b 社交热榜扫描 ★

微博和知乎因反爬机制（微博强制登录、知乎 40362），agent-browser 无法直接访问。通过 opencli（Chrome CDP 真实浏览器驱动）获取热搜/热榜数据，过滤旅游相关话题后入库。

```
opencli weibo hot -f json          → 30条热搜
opencli zhihu hot -f json --limit 50 → 50条热榜
       ↓ 旅游关键词过滤
  ├─ 贵州直接相关 (贵州/黔西南/兴义/万峰林…)  → 竞品动态节点 (高关注)
  └─ 泛旅游/户外 (旅游/探洞/徒步/避暑…)      → 行业资讯节点 (趋势信号)
```

**过滤关键词** — 两层匹配：
- **贵州直接**：贵州、黔西南、黔南、黔东南、兴义、安龙、贞丰、万峰林、马岭河、黄果树、荔波、梵净山…
- **泛旅游/户外**：旅游、景区、探洞、天坑、桨板、SUP、漂流、溯溪、户外、徒步、避暑、康养、旅居、研学…

**关键约束**：
- 依赖 opencli → Chrome Extension + Daemon（WSL 本地）
- 非关键路径：Chrome 未运行时静默失败，不影响 L1a/L2/L3
- 热榜数据天然低频命中（80条中通常 0-3 条旅游相关），定位为趋势信号补充而非主力采集

> ⚠️ **crontab PATH 陷阱 (2026-06-01 修复):** crontab 环境无完整 PATH，`lark-cli`（位于 `~/.local/bin/`）和 `node`（位于 `~/.hermes/node/bin/`）均不可用。`l3_cron.sh` 已添加 `export PATH="$HOME/.local/bin:$HOME/.hermes/node/bin:$PATH"`，否则 browser_collector.py 的 `subprocess.run(['lark-cli', ...])` 会触发 `FileNotFoundError`，导致所有采集结果推送失败（134 条全丢）。

```
L1 通用搜索:
  ├─ 🔍 百度 10 关键词        → ~40条  (政策/赛事/官方)
  └─ 🔍 夸克 10 关键词        → ~34条  (攻略/8264/跨平台)

去重 → lark-cli → 飞书 Wiki
  ├─ 行业资讯 WnAmwXK6xiqlh3kXGN6cHYGinxe
  └─ 竞品动态 VYsjwBZ6Rif5dFkboigchjgDnac
```

**平台实测结果 (2026-05-30, 更新 2026-06-01):**

| 平台 | 状态 | 方式 | 说明 |
|------|:--:|------|------|
| 百度 | ✅ | agent-browser | 通用搜索，4s渲染 |
| 夸克 | ✅ | agent-browser | AI搜索，5s渲染 |
| B站 | ✅ | agent-browser | 视频搜索，可提取标题/URL/UP主 |
| 头条 | ✅ | agent-browser | 资讯+视频，需滚动跳过热榜 |
| **微博** | ✅ | **opencli** | 热搜榜，30条/次；原 agent-browser 直访 ❌ (强制登录) |
| **知乎** | ✅ | **opencli** | 热榜(top 50)，tophub.today 数据源；原 agent-browser 直访 ❌ (反爬40362) |
| 小红书 | ❌ | — | IP风控300012，opencli 浏览器模式需登录后可用 |

### L2 urllib 站点直抓（云端）

**站点配置**

| 站点 | URL | 编码 | 产出/次 | 提取模式 | 状态 |
|------|-----|:--:|:--:|------|:--:|
| **品橙旅游** | pinchain.com | utf-8 | 10-12 | `<h2><a href>` | ✅ 主力 |
| **迈点网 文旅** | meadin.com/wl/ | utf-8 | 0-30 | img alt 属性 (噪音多) — ⚠️ 2026-06-12: 连续多日提取为0，可能迁至SPA | ⚠️ 降级监控 |
| **迈点网 景区** | meadin.com/jq/ | utf-8 | 0-30 | img alt 属性 (噪音多) — ⚠️ 2026-06-12: 连续多日提取为0，可能迁至SPA | ⚠️ 降级监控 |
| **闻旅** | wenlvnews.com | utf-8 | ~100 (仅5-10条实质) | 含大量政宣/导航，需旅行关键词过滤后 ~10条 | ⚠️ 高噪需精选 |
| 执惠旅游 | tripvivid.com | utf-8 | 30-60 | 行业日报汇总，信息密度高（需旅行关键词过滤后 ~36条实质内容） | ✅ 高产但需精选 |
| ~~贵州文旅厅~~ | whhly.guizhou.gov.cn | — | 0 | ❌ JS-SPA |
| ~~8264户外~~ | 8264.com | — | 0 | ❌ JS-SPA(4KB壳) |
| ~~中国旅游报~~ | ctnews.com.cn | — | 0 | ❌ JS-SPA |

### 运行时

```bash
# L2 站点直抓 → /tmp/l2_results.json
# ⚠️ 不要加重定向——脚本内部已用 json.dump() 写文件到 /tmp/l2_results.json
# 更不要加 2>&1——状态信息走 stderr，混入 JSON 文件会导致解析失败
python3 scripts/l2_collect.py $(date +%Y-%m-%d)

# 三阶段预过滤 → /tmp/l2_ingest.json
python3 scripts/l2_prefilter.py
```

> **注意**: cron 环境中禁止管道到解释器，预过滤脚本需先 `write_file` 再 `terminal` 执行。

输出 JSON 数组，每条：`{title, url, snippet, source, trust(high|medium|low), date}`

---

## 模块 2: 入库引擎 (Ingestor)

### 文档命名规范 ★

**两类场景，两套规则：**

| 场景 | 触发方 | 命名规则 | 示例 |
|------|:--:|------|------|
| **用户对话产出** | 用户直接要求创建 | **纯主题命名**，无日期/来源前缀 | `贵州之客户外安全操作规范` |
| **自动化情报采集** | cron 定时任务 | `YYYY-MM-DD_[source]_简短主题` | `2026-06-01_baidu_贵州探洞新发现` |

**自动化情报命名详情** — 适用于 L1a/L1b/L2/L3 四个通道：

| 通道 | 命名来源 | 格式示例 |
|------|---------|------|
| L1a (百度/夸克) | `_make_doc_title()` | `2026-06-01_baidu_贵州探洞新发现` |
| L1b (微博/知乎热榜) | `_make_doc_title()` | `2026-06-01_zhihu_hot_徒步25公里野炊引争议` |
| L2 (urllib站点) | ingestor XML `<title>` | `2026-06-01_行业_品橙旅游发布贵州避暑报告` |
| L3 (Bitable深度) | 同 L1a 逻辑 | `2026-06-01_bilibili_UP主探洞实战视频` |

**自动化标题生成规则** (`_sanitize_title` + `_make_doc_title`):
1. 去除控制字符、换行、emoji — 保留中英文/数字/基本标点
2. 前缀 `日期_来源_`（约 18-22 字符），剩余空间 38-42 字符用于主题
3. 在自然断点截断（句号/逗号/空格），不断在词中
4. 兜底：标题为空/过短时使用搜索关键词替代
5. 总长 ≤ 60 字符 — 避免飞书 API 静默拒绝

> ⚠️ **修复记录 (2026-06-01):** 原 browser_collector.py 和 hotlist_collector.py 使用 100 字符原始标题 + 简单正则清理，飞书 API 对过长/含特殊字符的标题会静默回退为默认名称"无标题"。现已统一替换为上述规范。

### 分类路由

采集结果按标题+摘要匹配关键词：

| 目标节点 | 关键词正则 |
|---------|-----------|
| 竞品动态 | 探洞|天坑|桨板|SUP|竞品|新品|价格调整|营销|
| 行业资讯 | 其余全部 + 政策|规划|景区|5A|旅居|康养|酒店|交通|节庆|

> ✅ 2026-07-04 节点重建后，新文档直接创建在对应子分类下。分拣工作流已废弃，保留 [references/reclassification-workflow.md](references/reclassification-workflow.md) 作为故障恢复参考。

### 命名规范

文档标题：`YYYY-MM-DD_类型_主题`

### 入库前预过滤 ★ (2026-06-05, 更新 2026-06-09)

L2 采集原始产量波动大（品橙 ~15 + 迈点 ~80 + 闻旅 ~100 + 执惠 ~60 = **~260 条**），含大量模板占位符、非旅游内容和跨类目重复。**入库前必须三阶段过滤**，避免噪音写入 Wiki 并浪费 API 配额：

| 阶段 | 操作 | 示例效果 (2026-06-09) |
|:--:|------|:--:|
| 1 | 去模板占位符 `{{name}}`/空白/过短标题 (<8 chars) | 260 → 195 |
| 2 | **旅行相关性过滤** — 标题含旅游关键词才保留 (见 `references/l2-pre-filter-keywords.md`) | 195 → 116 |
| 3 | 去部门导航链接 + 标题去重 (case-insensitive) | 116 → **94** |

> **为什么需要阶段 2**：闻旅返回 ~100 条中大量为政治宣传/部门导航链接（如"更好担负起新的文化使命""云南省文化和旅游厅"），无旅行关键词过滤会保留 165 条噪音 → 入库耗时 ~25min；过滤后仅 94 条实质内容 → 入库 ~12min，节省近一半时间。
>
> ⚠️ **阶段 3a 去部门导航不可跳过 (2026-06-10 验证):** 如果预过滤仅做去模板+去重（跳过旅行关键词和去导航阶段），部门链接「XX省文化和旅游厅」会因含"旅游"关键词通过旅行过滤，但最终无阶段 3a 的去导航规则拦截。2026-06-10 实测：23/93 入库条目为部门导航（占比 25%），均来自闻旅 `img alt` 提取。完整三阶段预过滤后应为 70 条实质内容。**始终使用 `scripts/l2_prefilter.py`，不要写内联简化版。**

### 入库

```bash
# L2 入库（foreground ≤50条, background >50条）
python3 -u scripts/l2_ingestor.py $(date +%Y-%m-%d) /tmp/l2_ingest.json
```

> ⚠️ **入库进度验证必须用 `--page-limit 0`**：`wiki +node-list --page-all` 默认 500 条封顶，新文档在封顶外不可见。详见「实测陷阱 → wiki +node-list --page-all 静默截断陷阱」。

---

## 模块 3: 过期校验 (Expiry)

15 类过期规则见 `references/expiry-rules.yaml`。

标注方式：`lark-cli drive +add-comment` 添加整文档评论（纯 ASCII）。

```bash
python3 scripts/expiry_checker.py
```

---

## 模块 4: 报告引擎 (Reporter)

| 报告 | cron | 内容 |
|------|------|------|
| 每日简报 | 09:00 | 当日采集概要 + 行业/竞品/政策分组 |
| 周度分析 | 周一 09:00 | 本周汇总 + 趋势 + 关键词审计 + 建议 |
| 综合洞察 | 周六 10:00 | 跨节点 6 维 LLM 分析 |

全部创建飞书文档 → 推送 ≤3000 字符群摘要。群摘要格式见 [templates/daily_brief.md](templates/daily_brief.md)。

> **执行指南：** 综合洞察和周度分析的具体执行步骤（文档优先级、批量读取策略、效率提示）见 [references/insight-execution-guide.md](references/insight-execution-guide.md)。

> **简报执行须知：** Wiki 中文档是骨架格式（仅标题 + 来源信息 + 可选 `<bookmark>` 链接），无正文内容。L2 采集文档可能不含 bookmark URL（仅 callout 标题+来源），此时回退为从标题生成概要。完整策略见 [references/briefing-execution-notes.md](references/briefing-execution-notes.md)。分类与噪音过滤实践见 [references/daily-brief-classification.md](references/daily-brief-classification.md)。

---

## 模块 5: 查询引擎 (Querier)

两级回退：

```
Wiki 搜索 → 过期降权(过滤权重<30%) → 有效结果？
  ├─ 是 → 返回（标注"知识库·采集于{日期}"）
  └─ 否 → web_search → 返回（标注"互联网·实时搜索"）
```

触发词：`查XX景点` `知识库有没有XX` `搜一下XX`

---

## 查询示例

其他技能调用方式（在 SKILL.md 中引用）：

```markdown
# 查询目的地知识库
加载 travel-intel，使用 querier 模块查询"{目的地} {信息类型}"。
结果标注来源后返回给用户。
```

---

## Cron 清单

| 名称 | Hermes Job ID | 调度 | 通道 | 环境 |
|------|:------------:|------|:--:|:--:|
| travel-intel-collect | `07ceed5fc5a8` | 0 7 * * * | **L2 collect+prefilter+ingest + L3-dispatch** | ☁️ agent |
| travel-intel-l1-local | *(WSL crontab)* | 30 6 * * * | **L1a**(百度+夸克)+**L1b**(微博+知乎) | 🏠 l3_cron.sh |
| travel-intel-l3-poller | `e92c1aeeb70e` | */5 * * * * | L3(Bitable→百度/B站/头条) | 🏠 WSL `no_agent` script |
| travel-intel-expire | `09c5407d9244` | 0 3 * * * | 过期校验 | ☁️ agent |
| travel-intel-daily | `646091130172` | 5 9 * * * | 每日简报 | ☁️ agent |
| travel-intel-weekly | `011f4af010cd` | 5 9 * * 1 | 周度分析 | ☁️ agent |
| travel-intel-insight | `dda612e69d65` | 0 10 * * 6 | 综合洞察 | ☁️ agent |

全部 deliver: `feishu:oc_40570cc921ca1f645f8667151c1e85e6`，除 l3-poller 为 `local`（仅脚本输出存档），l1-local 为 WSL 本地 crontab（非 Hermes cron）。

> ⚠️ **Agent 模式 job 铁律**：所有加载 `travel-intel` skill 的 agent 模式 cron job（collect/daily/weekly/insight/expire）必须设置 `enabled_toolsets: ["terminal","file","feishu_doc","feishu_drive"]`，否则巨型 skill（SKILL.md 2100+ 行）导致 context 膨胀 → 流式超时断管（Broken pipe / Connection error）。miss 任何一个都会复发。

> **注意**: L1a+L1b 由 WSL 本地 crontab 调度（`l3_cron.sh`），不经过 Hermes cron 系统。上表中的 `travel-intel-l1-local` 仅供文档记录，并非 Hermes cron job。

**本地脚本:**
| 脚本 | 路径 | 用途 |
|------|------|------|
| l3_poller.py | `~/.hermes-feishu/scripts/l3_poller.py` | L3 Bitable 轮询器 (no_agent cron, 每5分钟) |
| l2_collect.py | `skills/travel/travel-intel/scripts/l2_collect.py` | L2 urllib 站点直抓 (品橙/迈点/闻旅/执惠) — cron 直接调用 |
| l2_ingestor.py | `skills/travel/travel-intel/scripts/l2_ingestor.py` | L2 urllib 结果入库 (替代有 bug 的 ingestor.py，批冷却防限流) |
| l2_prefilter.py | `skills/travel/travel-intel/scripts/l2_prefilter.py` | L2 三阶段预过滤：去模板 → 旅行关键词 → 去导航+去重 |
| ingestor.py | `skills/travel/travel-intel/scripts/ingestor.py` | 通用入库引擎 (✅ 2026-07-04 节点重建后可用，l2_ingestor.py 仍为首选) |
| browser_collector.py | `skills/travel/travel-intel/scripts/browser_collector.py` | L1a 百度+夸克 agent-browser 采集 |
| hotlist_collector.py | `skills/travel/travel-intel/scripts/hotlist_collector.py` | L1b 微博+知乎 opencli 热榜采集 |
| classify_daily_docs.py | `skills/travel/travel-intel/scripts/classify_daily_docs.py` | 每日简报分类脚本：wiki node-list输出→按贵州/户外/政策/常规分组 |
| classify_daily_brief.py | `skills/travel/travel-intel/scripts/classify_daily_brief.py` | 每日简报全流程分类脚本：拉取三节点→JSON解析(处理内嵌引号)→分类输出 |

---

## 与其他技能的关系

| 技能 | 关系 | 方式 |
|------|------|------|
| travel-itinerary | 消费者 | 调用 querier 查目的地信息 |
| trip-landing | 消费者 | 调用 querier 查景点/须知 |
| feishu-wiki | 依赖 | 节点管理 |
| opencli | 依赖 | L1b 微博热搜+知乎热榜采集 |

---

## 关键约束

1. **L1a 仅在 WSL 本地运行** — browser_collector.py (百度+夸克)，WSL crontab 每天 6:30。云端 cron 不执行 L1（已从 prompt 移除 web_search）。
2. **L1b 仅在 WSL 本地运行** — hotlist_collector.py (微博+知乎)，依赖 opencli (Chrome Extension + Daemon)。非关键路径，失败不影响其他通道。
3. **L2 为云端主力** — urllib 站点直抓 (品橙/迈点/闻旅/执惠)，云端 cron 每天 7:00
4. **L3 云端分发、本地执行** — 云端 cron → Bitable 队列 → WSL l3_poller.py 轮询 → agent-browser 搜索
5. **WSL crontab 必须包含 lark-cli/node 路径** — `l3_cron.sh` 已添加 `export PATH="$HOME/.local/bin:$HOME/.hermes/node/bin:$PATH"`，否则推送 100% 失败（FileNotFoundError）

## 实测陷阱

### lark-cli 响应格式差异 (2026-05-30, 更新 2026-06-03)

### l2_collect.py 重定向陷阱 (2026-06-25) ★

`l2_collect.py` 内部已通过 `json.dump()` 直接写入 `/tmp/l2_results.json`，状态信息全部输出到 `stderr`。运行时**不要加任何 shell 重定向**：

```bash
# ❌ 错误 — stdout 为空，2>&1 将 stderr 混入 JSON 导致解析失败
python3 scripts/l2_collect.py DATE > /tmp/l2_results.json 2>&1
# → json.decoder.JSONDecodeError: Invalid control character

# ❌ 也会出错 — > 重定向创建空文件，后被脚本覆盖，看似正常但 2>&1 仍致命
python3 scripts/l2_collect.py DATE > /tmp/l2_results.json 2>&1

# ✅ 正确 — 脚本自行管理文件，无需任何重定向
python3 scripts/l2_collect.py DATE
```

**如需同时保留日志**，只重定向 stderr：
```bash
python3 scripts/l2_collect.py DATE 2>/tmp/l2_collect.log
```

### lark-cli 响应格式差异 (2026-05-30, 更新 2026-06-03)

`lark-cli api` 子命令返回**原始飞书 API 响应** `{code: 0, data: {...}}`，**输出到 stderr 而非 stdout**。其他子命令（`+create`/`+update`/`+record-list` 等）输出到 stdout 且有 lark-cli 的 `ok` 包裹层。

```python
# ✅ 正确解析逻辑 — 注意 stderr/stdout 差异
def parse_larkcli_output(result):
    # lark-cli 'api' subcommand → stderr
    # lark-cli '+create' / '+node-list' etc → stdout
    raw = result.stderr.strip() or result.stdout.strip()
    data = json.loads(raw)
    
    if data.get("ok"):
        inner = data.get("data", data)  # lark-cli 子命令
    else:
        inner = data                    # lark-cli api (raw Feishu response)
    
    if isinstance(inner, dict) and "code" in inner:
        if inner["code"] != 0:
            raise ...                   # API error
        return inner.get("data", inner) # unwrap Feishu data envelope
    return inner
```

**lark-cli api vs 子命令 输出目标一览：**

| 命令类型 | 输出目标 | 格式 | 示例 |
|---------|:--:|------|------|
| `lark-cli api GET/POST ...` | **stderr** | `{code: 0, data: {...}}` | Bitable records, node info |
| `lark-cli docs +create` | **stdout** | `{ok: true, data: {...}}` | Document creation |
| `lark-cli docs +update` | **stdout** | `{ok: true, data: {...}}` | Document update |
| `lark-cli wiki +node-list` | **stdout** | `{ok: true, data: {nodes: [...]}}` | Node listing |

### 飞书 API 频率限制 (99991400)

连续 `docs +create` 超过 ~10 次/分钟会触发 rate limit。**入库必须加延迟和批间冷却**。

> ⚠️ **确认参数 (2026-06-05 最终验证):**
> - BATCH=8 + COOL=12s + DELAY=4s → **17/64 限流 (26.5%)** ❌
> - BATCH=6 + COOL=15s + DELAY=5s → 0 限流（skill 中 l2_ingestor.py 默认值）✅
> - BATCH=4 + COOL=20s + DELAY=6s → **17/17 全成功，零重试**（最保守，用于纯重试场景）✅
>
> **首条脆弱模式**：冷却后的批次第一项最容易触发限流（令牌桶在冷却期间未完全恢复）。减小 BATCH_SIZE 比增大 COOL_DOWN 更有效。
>
> `l2_ingestor.py` 默认值: BATCH_SIZE=6, COOL_DOWN=15s, ITEM_DELAY=5s, RETRY_DELAY=15s, MAX_RETRIES=3 (2026-06-14 添加)。遇到 99991400 错误自动等待 RETRY_DELAY 后重试，最多 3 次。直接调用此脚本即可。重试实现细节见 [references/l2-ingestor-retry-pattern.md](references/l2-ingestor-retry-pattern.md)。

### 云端 cron 管道安全限制 (2026-06-01, 更新 2026-06-05)

云端 agent 运行 cron 时，安全扫描器会拦截所有**管道到解释器**的写法，且 `execute_code` 工具被禁用：

```
❌ curl ... | python3 -c "..."     # tirith:curl_pipe_shell
❌ cat file.json | python3 -c "..." # tirith:pipe_to_interpreter
❌ python3 collector.py | python3 -c "..."  # 同上
❌ execute_code 工具              # BLOCKED: cron mode
```

**正确做法：** 先用 `write_file` 写入 .py 脚本文件，再用 `terminal` 直接执行：

```bash
# ✅ 分两步
write_file /tmp/my_script.py  # 写入完整脚本
terminal python3 /tmp/my_script.py  # 直接执行，不经过管道
```

> ⚠️ **write_file Bearer Token 自动脱敏 (2026-06-11):** `write_file` 会对包含 `Bearer {token}` 模式的 Python 脚本进行自动脱敏替换（token → `***`），导致语法错误。绕过方法：先用 `terminal` 写 token 到文件，脚本内读取；或使用 `terminal` + `cat` heredoc 写入脚本。详见 feishu-wiki 技能「实测陷阱」同名条目。

### L3 Bitable 分发 — 命令执行方式 (2026-06-01, 更新 2026-06-07)

Python `subprocess.run(['lark-cli', ...])` 方式在脚本中容易因 import 顺序、PATH 环境变量传递等问题静默失败。**推荐直接在 shell 中逐条调用**，利用 `$(...)` 捕获输出。

**关键注意事项 (2026-06-03 验证):**
- Bitable 字段名为 `Text`（非 `任务名称`），API 对不存在的字段名返回 1254045 FieldNameNotFound
- `lark-cli api POST` 输出到 **stderr** 非 stdout，Python subprocess 需读 `r.stderr`
- Bitable app token 为 `TDYYwZ0T0ifLtdkK9iOcp2HTnwf`（旧 `DhZcbnof3aj` 已删除）
- Table ID 为 `tblVKG82oOl3UaNW`

**★ lark-cli API stderr 多行 JSON 解析 (2026-06-07):** `lark-cli api POST` 返回的 JSON 是**多行格式化**的（pretty-print），不能用 `raw.split("\n")[-1]` 取最后一行。正确做法：

```python
raw = r.stderr.strip() or r.stdout.strip()
idx = raw.find('{')  # 找第一個 { 而非最后一行
resp = json.loads(raw[idx:])
```

`raw.split("\n")[-1]` 只拿到残缺片段（如 `"id":`），`json.loads` 报 `Expecting value` 错误。此问题在逐条发送多条 Bitable 记录时每一条都触发，6条全失败。

```bash
# ✅ 推荐：直接 shell 调用（cron 中需写入 .py 脚本后 terminal 执行）
export PATH="/home/aorus/.local/bin:$PATH"
r=$(lark-cli api POST "/open-apis/bitable/v1/apps/TDYYwZ0T0ifLtdkK9iOcp2HTnwf/tables/tblVKG82oOl3UaNW/records" \
  --as bot --data '{"fields":{"Text":"任务名","搜索关键词":"kw","平台":"百度","结果摘要":"pending"}}' 2>&1)
```

**Python dispatch 示例：**
```python
import subprocess, json, os

ENV = os.environ.copy()
ENV["PATH"] = f"{os.path.expanduser('~/.local/bin')}:{ENV.get('PATH', '')}"

body = {"fields": {
    "Text": "竞品_探洞行业最新动态",
    "搜索关键词": "探洞 贵州 户外 2026",
    "平台": "综合",
    "结果摘要": "pending"
}}

r = subprocess.run(
    ["lark-cli", "api", "POST",
     "/open-apis/bitable/v1/apps/TDYYwZ0T0ifLtdkK9iOcp2HTnwf/tables/tblVKG82oOl3UaNW/records",
     "--as", "bot", "--data", json.dumps(body, ensure_ascii=False)],
    capture_output=True, text=True, timeout=15, env=ENV
)

# lark-cli api outputs to stderr
resp = json.loads(r.stderr.strip() or r.stdout.strip())
if resp.get("code") == 0:
    record_id = resp["data"]["record"]["record_id"]
```

### Bing → 百度+夸克 迁移（2026-05，多周确认的系统性问题）

L1 原使用 Hermes 内置 web_search（底层 Bing 中文搜索）。对"探洞""天坑""桨板""SUP"等垂直长尾关键词，Bing 大量返回字典页（zdic/hgcha/cidianwang）而非行业新闻，中文意图识别差，负向关键词和 site: 操作符均无效。**连续多周可复现，非偶发故障。**

**对策：** L1 全面迁移至 agent-browser 百度+夸克双引擎（WSL本地 06:30）。百度覆盖政策/赛事/官方信息，夸克补充攻略/UGC/跨平台内容。web_search 降级为仅查询层回退（模块5 querier）。

### 周六预生成 → 周一 Cron 冲突 (2026-06-01)

travel-intel-insight (周六 10:00 综合洞察) 可能已为当前周生成了 `{YYYY}_WW周_周度分析`，导致周一 09:00 的 travel-intel-weekly cron 重复运行。**周一 cron 必须先检测已有报告，采用 check-and-append 模式而非创建新文档。**

```bash
# Step 1: 检测已有报告
lark-cli wiki +node-list --space-id 7643710721485753535 \
  --parent-node-token WnAmwXK6xiqlh3kXGN6cHYGinxe --page-all --as bot 2>&1 \
  | grep "{YYYY}_WW周_周度分析"

# Step 2: 如存在 → grep 检查周日新增文档 → 读取最新每日简报 → append 补充
# Step 3: 修正统计数据（文档数、实质内容占比）并在群摘要中高亮新增发现
```

> 详情见 [references/insight-execution-guide.md](references/insight-execution-guide.md) 步骤 0。

### Python 输出缓冲：background 模式下静默 (2026-06-05, 更新 2026-06-06) ★

Hermes `terminal(background=true)` + Python 脚本时，`print()` 输出被完全缓冲，`process(action='log')` 和 `process(action='poll')` 始终返回 0 行/空 output_preview。即使脚本正常运行，也无任何可见输出。

```bash
# ❌ 静默 — 无输出，无法判断进度
terminal python3 l2_ingestor.py ... --background=true

# ❌ 仍然静默 (2026-06-06 验证) — -u 对 background 模式无效
terminal python3 -u l2_ingestor.py ... --background=true
# process(action='poll') → output_preview: "" (543s, 61 篇已入库但零可见输出)

# ✅ 实时输出 — foreground 模式 + -u 才有可见进度
terminal python3 -u l2_ingestor.py ...  # foreground, timeout=600
```

**根因**：`-u` 解决 Python 层缓冲，但 background 模式下输出经过 **Hermes 进程捕获层**，该层在进程退出前不刷新中间输出。`notify_on_complete` 会在进程退出后送达完整输出，但运行中无法观察进度。

**正确的 background 模式使用策略**：
- 信任 `notify_on_complete`：进程退出后会自动送达完整结果
- **不要在运行中反复 poll/log 来判断进度** — 始终返回空
- 如需监控进度，用旁路验证：`lark-cli wiki +node-list` 检查已入库文档数

**入库脚本模式选择指南**：
| 场景 | 模式 | 理由 |
|------|:--:|------|
| ≤50 条，预计 <10 分钟 | **foreground + timeout=600** | 可见输出，安心等待 |
| >50 条，预计 >10 分钟 | **background + notify_on_complete** | 超 600s 上限，信任 notify |
| 需要实时进度 | foreground (必须) | background 无可信进度 |

> ⚠️ **Foreground timeout 硬上限 600s (2026-06-14 确认):** `terminal(timeout=N)` 的最大 foreground timeout 为 600 秒。设置 900s 会拒绝执行并提示 "use background=true"。52 条入库 (BATCH=6, DELAY=5, COOL=15) 实际耗时 ~400s，在 600s 内可完成。如需处理 >80 条，强制使用 background 模式。

### 入库进程卡死恢复流程 (2026-06-06, 更新 2026-06-11) ★

当 background 模式的 ingestor 长时间无输出，**不要立即 kill**——进程可能在静默工作。按以下步骤处理：

```
1. 验证实际进度（非 poll/log）
   lark-cli wiki +node-list --page-limit 0 → 统计今日已创建文档数
   ⚠️ 必须加 --page-limit 0，否则默认10页(500条)封顶，新文档可能在封顶外不可见
   
2. 判断真实状态
   ├─ 文档数持续增长 → 进程正常，继续等待 notify_on_complete
   └─ 文档数停滞 >5 分钟 → 进程可能卡死，执行恢复

3. 恢复：kill 原进程 → diff 找出缺失条目 → foreground 重跑
   python3 find_missing.py  # 对比 /tmp/l2_ingest.json vs wiki node-list (--page-limit 0)
   python3 -u l2_ingestor.py DATE /tmp/l2_missing.json  # 仅入库缺失部分
```

**2026-06-06 验证**：86 条入库，background 模式下 543s 无输出→被误杀（实际已入库 61/86）。恢复后 foreground 模式 27 条缺失全部成功，总耗时 ~200s。

**⚠️ background+foreground 重叠导致重复入库 (2026-06-11 验证):** 先 background (736s 无输出→被 kill)、再 foreground (59/82 超时) 的两段式恢复会产生重复文档。background 进程在静默期间可能已完成部分入库，foreground 从第1条开始会重复创建。本次 130 条总数 vs 71 条去重唯一，重复率 45%。**正确恢复顺序：必须先 kill background，再验证 wiki 存量，最后仅入库缺失条目。**

### wiki +node-list --page-all 静默截断陷阱 (2026-06-11 发现, 2026-06-27 更新) ★

`lark-cli wiki +node-list --page-all` 的 **默认 `--page-limit 10`**（10页×50条=**500条封顶**）。超过 500 条的知识库会被静默截断，后续文档不可见。

```bash
# ❌ 默认 — 最多返回500条，后面的看不见
lark-cli wiki +node-list --space-id SPACE --parent-node-token TOKEN --page-all --as bot

# ✅ 必须 — 咨询洞察节点已 1100+ 条，--page-limit 25 为当前最佳值
lark-cli wiki +node-list --space-id SPACE --parent-node-token TOKEN --page-all --page-limit 25 --as bot

# ⚠️ 不推荐 — --page-limit 0（不限）在 1100+ 条节点上可能超时
```

**⚠️ 维护提醒**：节点每月增长 ~700 条（日均48×22工作日），需每月检查 `--page-limit` 是否足够。预计 7 月中旬需升至 30（1500条），8 月初需升至 35。
- 过期校验 (`expiry_checker.py`)：可能漏检文档
- 每日简报/周度分析：统计计数偏低，无法发现当日新增文档（新文档排在列表末尾）
- 入库恢复 (`find_missing.py`)：误判"0 today docs" → 重新入库已存在条目 → 产生大量重复
- **weekly/insight cron 直接失败（Broken pipe）— 找不到本周数据**

**2026-06-27 确认**：咨询洞察节点 ~1100 条，`--page-limit 20` 仅可见至 06-16，06-17 起新文档完全不可见。已升至 `--page-limit 25`。

### L2 站点直抓 — 品橙/闻旅 采集修正 (2026-06-03)

**品橙旅游 (pinchain.com) 正则模式错误**：原 `title=` 属性匹配在首页仅命中 `点击看更多` 一个 UI 元素，真实文章标题在 `<h2><a href="/article/NNN">标题</a></h2>` 结构中。修正：

```python
# ❌ 原模式 — 仅命中 UI 元素
for m in re.finditer(r'<a[^>]*title=\"([^\"]+)\"[^>]*href=\"([^\"]+)\"', html):

# ✅ 修正模式 — 命中所有文章
for m in re.finditer(r'<h2[^>]*>\s*<a[^>]*href=\"([^\"]+)\"[^>]*>([^<]+)</a>', html):
    url, title = m.group(1), m.group(2)
```

**闻旅 (wenlvnews.com) SSL 证书错误**：`urlopen()` 默认验证 SSL 证书，闻旅站返回 `CERTIFICATE_VERIFY_FAILED`。需创建不验证的 SSL context：

```python
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
html = urllib.request.urlopen(req, timeout=15, context=ctx).read().decode('utf-8', errors='ignore')
```

即使修复 SSL，闻旅首页仅返回 3 条标签链接（非真实文章），实际产出几乎为零。**建议从主力采集站降级或移除。**

### travel-intel skill 目录丢失与恢复 (2026-06-14 发现) ★

`travel-intel` skill 所在的目录 `/home/aorus/.hermes-feishu/skills/travel/travel-intel/` 可能在系统迁移/清理过程中被删除。当 cron 调度提示 "Skill not found and skipped: travel-intel" 时，按以下步骤恢复：

```bash
# 1. 检查备份目录
ls /home/aorus/.hermes-shared/backups/

# 2. 从最新备份恢复整个 skill 目录
cp -r /home/aorus/.hermes-shared/backups/feishu-skills-20260613-204813/travel/travel-intel \
     /home/aorus/.hermes-feishu/skills/travel/travel-intel

# 3. 验证恢复
ls /home/aorus/.hermes-feishu/skills/travel/travel-intel/scripts/
# 应包含: l2_collect.py, l2_ingestor.py, l2_prefilter.py, expiry_checker.py 等
```

**注意**：备份中的 `l2_ingestor.py` 可能不含 99991400 自动重试逻辑（2026-06-14 添加），恢复后需验证 `ingest_one()` 函数是否有 `MAX_RETRIES` 和 `for attempt in range(...)` 循环。如缺失需手动添加（见该脚本顶部的 `MAX_RETRIES = 3` 和重试逻辑）。

`/tmp` 也可能有旧版本缓存：
```bash
ls /tmp/l2_ingestor*.py
# l2_ingestor_v2.py - 早期版本 (BATCH=8, COOL=12, 无重试)
# l2_ingestor_fixed.py - 中间版本 (PARENT_TOKEN fallback, BATCH=8, 无重试)
```

### L2 迈点网提取持续退化 (2026-06-12)

迈点文旅 (`meadin.com/wl/`) 和迈点景区 (`meadin.com/jq/`) 的 `<img alt="...">` 提取模式连续多日返回 0 条。站点可能已迁至 JS-SPA 或改版。

```python
# 当前模式 — 2026-06-12 返回 0
matches = re.finditer(r'<img[^>]*alt="([^"]+)"[^>]*>', html)
```

**诊断命令**（下次 cron 运行时验证）：
```bash
curl -sL 'https://www.meadin.com/wl/' | grep -c '<img alt='
# 若返回 0 → 确认 img alt 已从 HTML 中消失，需换提取模式或标记不可用
```

**影响**：理论产出 40-60 条/日（两站各 20-30），现为 0。品橙+执惠+闻旅仍可产出 50-70 条预过滤后 ~45 条，短期不影响日报质量。

### ingestor.py 全量 3380002 故障 (2026-06-03 定位+修复)

原 `scripts/ingestor.py` 对所有入库条目返回 3380002 "Parent node not found"，即使 node_token 经 `wiki +node-list` 验证有效、且同 token 的单独 `docs +create` 调用成功。

**根因未完全定位**（可能涉及 `shutil.which("lark-cli")` 在模块加载时的 PATH 解析、或 XML 构建中的字符转义差异），但已验证的替代方案有效：

- ✅ 使用 Python `subprocess.run(['lark-cli', ...])` + 显式 `PATH` 环境变量
- ✅ XML 使用简单 `str.replace()` 转义（移除控制字符，& → &amp;，< → &lt;，> → &gt;）
- ✅ 每 6 条插入 15s 冷却（BATCH_SIZE=6, COOL_DOWN=15, ITEM_DELAY=5, RETRY_DELAY=15）— 2026-06-05 确认 0 限流

**替代入库脚本**见 `scripts/l2_ingestor.py`（从 `/tmp/l2_ingestor_v2.py` 提取）。原 `ingestor.py` 待完整重写。

原5站中仅品橙旅游(pinchain.com)仍为服务端渲染（39KB静态HTML可提取）。其余4站已全面迁移至JS动态渲染（SPA）：
- 贵州文旅厅(whhly): 44KB HTML全是document.write壳
- 8264: 4KB JS壳，所有内容异步加载
- 执惠: 530KB但仅2条静态残留
- 中国旅游报: 仅导航链接，文章列表JS加载

**根因不是正则匹配问题，是SPA架构不可爬。** 已标记为不可用并从配置移除。

详见 [references/collector-diagnostics.md](references/collector-diagnostics.md)。

### 过期校验：REST API 日期回退 (2026-06-03)

`lark-cli wiki +node-list` 不返回 `obj_edit_time` 字段，导致 663 篇文档中仅 88 篇有 `YYYY-MM-DD_` 标题前缀可提取日期，其余 575 篇因无日期被跳过。

**对策**：`expiry_checker.py` 已升级至 v2：
1. `list_docs()` 改用 curl + REST API（返回 `obj_edit_time` Unix 时间戳）
2. `parse_title_date()` 新增 `YYYY_WW周_` 格式支持
3. `check_expiry()` 降级逻辑：标题日期 → `obj_edit_time` → 放弃

**依赖**：需要 `FEISHU_APP_ID` / `FEISHU_APP_SECRET` 环境变量（用于获取 tenant_access_token）。cron 环境需确保这些变量已配置。

### 过期校验覆盖缺口：顶层节点未扫描 (2026-06-09, ✅ 已修复 2026-06-10) ★

`expiry_checker.py` 仅扫描两个子分类节点（`WnAmwXK6xi` + `VYsjwBZ6Ri`），但子分类 token 3380002 失效后，L1/L2/L3 新增文档统一创建在「咨询洞察」一级节点 `UF7Cw5w2Wi` 下。**这些新文档完全不在过期扫描范围内**。

> **2026-06-10 已修复**: `expiry_checker.py` L13 已更新，NODES 列表加入 `UF7Cw5w2WiHGfjkKVvBcxj8Hnib`。现在三节点全量覆盖：行业资讯 593 + 竞品动态 333 + 咨询洞察 527 = 1,453 篇。

2026-06-09 巡检数据（修复前）：
| 扫描节点 | 文档数 | 状态 |
|----------|:--:|------|
| 行业资讯 `WnAmwXK6xi` | 593 | 已覆盖 |
| 竞品动态 `VYsjwBZ6Ri` | 333 | 已覆盖 |
| **咨询洞察 `UF7Cw5w2Wi`** | **432** | **❌ 未覆盖** |

2026-06-10 巡检数据（修复后）：
| 扫描节点 | 文档数 | 状态 |
|----------|:--:|------|
| 行业资讯 `WnAmwXK6xi` | 593 | ✅ 已覆盖 |
| 竞品动态 `VYsjwBZ6Ri` | 333 | ✅ 已覆盖 |
| 咨询洞察 `UF7Cw5w2Wi` | 527 | ✅ 已覆盖 |
| **合计** | **1,453** | **全量覆盖**

### 过期规则 `days: null` 不触发检查 (2026-06-09)

`references/expiry-rules.yaml` 中 4 条规则的 `days` 字段为 `null`（文旅厅通知/文件、景点基础信息、节庆/活动、季节性信息）。这些规则标注了特殊逻辑（如"截止日+3"、"活动结束+7"、"跨季当天"），但 `check_expiry()` 函数在遇到 `days: null` 时直接返回 `(0, None)`，**永不触发过期标记**。

```python
# expiry_checker.py L145-150 — 当前逻辑
for rule in rules:
    if rule["type"] == rule_type:
        rd = rule.get("days")
        if rd and age > rd:    # None 在此为 falsy → 永不进入
            return age, rule
        return 0, None          # 所有 null-days 规则走这里
```

**影响**：即使存在过期的节庆活动/文旅厅通知文档，也不会被标记。当前数据库年轻（最老 15 天）未暴露此问题，但随着时间推移将产生漏检。

**待修复**：需为这 4 条规则实现对应的动态阈值逻辑，而非统一 `days: null`。

### agent-browser Chrome 长期运行僵死 (2026-06-13 发现+修复) ★

agent-browser 的 headless Chrome 进程在连续运行 10+ 天后会逐渐僵死：所有 `agent-browser open` 和 `eval` 调用均超时（20s timeout），采集产出降为 0。6/5 开始劣化，6/7 彻底无响应，持续到 6/13 重启前。

**诊断特征**：
- 日志中全部关键字搜索均为 `[百度] timeout` / `[夸克] timeout`
- `ps aux | grep agent-browser` 显示进程 uptime 超过 10 天
- 手动 `agent-browser eval "1+1"` 也超时 → 确认 agent-browser 本身僵死

**修复**：
1. `l3_cron.sh` 增加健康检查：采集前 `agent-browser eval "1+1"` 验证，失败则 kill + 重启
2. 保守估计 agent-browser 健康寿命 ~7 天，健康检查在每次 cron 运行时自动执行

**手动恢复**（如果健康检查也失败）：
```bash
pkill -f agent-browser; pkill -f agent-browser-chrome; sleep 3
agent-browser &
sleep 5
agent-browser eval "1+1"  # 验证恢复
```

### 百度反爬验证码 (2026-06-13 发现+修复) ★

agent-browser 访问百度搜索时会被重定向到 `wappass.baidu.com` 验证页面，导致所有搜索结果提取为 0。

**修复**：`search_baidu()` 增加验证码检测 + 最多 3 次重试（间隔 10s），同时增加 5 组 CSS 选择器兜底（适配百度 DOM 变化）。

### L1b opencli 依赖 Chrome + Daemon (2026-06-01)

hotlist_collector.py 通过 opencli 调用微博/知乎，依赖链：`opencli → Daemon (127.0.0.1:19825) → TCP relay → Chrome Extension → Chrome CDP`。此依赖链仅在 WSL 本地且 Chrome 运行时可工作。

**cron 凌晨 6:35 运行时的注意事项**：
- Chrome 是否为开机自启？否则首次 cron 运行 L1b 会静默失败
- WSL IP 重启后可能变化，需更新 portproxy 和 TCP relay 的 IP 绑定
- 建议：将 Chrome 设为 Windows 开机自启，确保 Daemon + relay 在 WSL 启动后自动拉起

**验证命令**：
```bash
curl -s http://$(ip route show default | awk '{print $3}'):9222/json/version | grep Chrome
opencli doctor
```

### `docs +create --doc-format markdown` 中标题控制 (2026-06-02, 2026-06-20 更新)

> ⚠️ **lark-cli 1.0.53+:** `--title` 标志已完全移除。标题现在仅从内容中提取（Markdown `#` heading 或 XML `<title>`）。

**历史行为**（lark-cli <1.0.53）：当 markdown 内容以 `# 标题` 开头时，lark-cli 将文档标题设为 heading 文本，忽略 `--title` 参数。

| 方案 | 结果 | 适用 |
|------|------|------|
| markdown + `# Title` | 标题 = heading 文本 | ✅ 唯一方式（lark-cli ≥1.0.53） |
| XML + `<title>` | 标题 = XML 中 `<title>` 值 | ✅ 完全控制 |

**对策**（当前）：如需自动化标题格式（如 `YYYY-MM-DD_source_topic`），在 Markdown 中使用 `# YYYY-MM-DD_source_topic` 作为第一行。

### L3 Bitable 队列迁移 (2026-06-02 已解决)

Bitable 分发目标 `DhZcbnof3aj/tblVKG82oOl` 返回 91402 NOTEXIST — 旧 app 已删除。但 Bitable 实际存活：被迁移到行业资讯节点下，app token 变为 `TDYYwZ0T0ifLtdkK9iOcp2HTnwf`（标题"任务队列"），table_id `tblVKG82oOl3UaNW` 不变，字段/记录完整。

**已修复**：
1. l3_poller.py / task_poller.py → BASE_TOKEN 已更新
2. SKILL.md / l3-bitable-dispatch.md → 引用已更新

**验证命令**：
```bash
lark-cli api GET "/open-apis/bitable/v1/apps/TDYYwZ0T0ifLtdkK9iOcp2HTnwf/tables" --as bot
# 正常返回 tables 列表，包含 tblVKG82oOl3UaNW
```

### 子分类节点物理删除 (131005) + 重建流程 (2026-07-04) ★★★

子分类节点被物理删除后返回 `131005 not found`（区别于 `3380002 Parent node not found`）。`3380002` 是 token 仍在 wiki 树中但创建文档时挂载失败；`131005` 是节点不存在——**历史文档随节点删除永久丢失**。

**重建步骤：**
```bash
# REST API 创建 wiki 节点（lark-cli wiki +node-create 也可用，但只创建 docx origin）
lark-cli api POST "/open-apis/wiki/v2/spaces/{space_id}/nodes" \
  --as bot \
  --data '{"parent_node_token":"PARENT_TOKEN","node_type":"origin","obj_type":"docx","title":"行业资讯"}'
```

**重建后必须更新的文件（7+个）：**

| 文件 | 更新内容 |
|------|---------|
| `SKILL.md` | 存储节点表（全 token）、架构图注释、代码示例中的 token |
| `expiry_checker.py` | `NODES` 列表 |
| `l2_ingestor.py` | `COMPETITOR_NODE`/`INDUSTRY_NODE`/`DEFAULT_NODE` |
| `ingestor.py` | 同上 |
| `querier.py` | `WIKI_NODES` |
| `classify_daily_brief.py` | `V0_TOKEN`/`EA_TOKEN` |
| `insight-execution-guide.md` | 三节点 token + 注释 |
| `travel-intel-expire` cron prompt | token 引用 |

> ⚠️ 用 `replace_all` 分别替换全 token 和短 token（如 `V0Lhwl7KYiWYDDk1vCncv2GhnYf` 和短引用 `V0Lhwl7KYi`），确保代码块和注释内引用全部覆盖。

**验证：** 节点重建后 `docs +create --parent-token <新token>` 直接写入 wiki 树正常，无需两步法。

| 节点 | 旧 Token | 新 Token (2026-07-04) |
|------|---------|----------------------|
| 行业资讯 | ~~`V0Lhwl7KYiWYDDk1vCncv2GhnYf`~~ | `WnAmwXK6xiqlh3kXGN6cHYGinxe` |
| 竞品动态 | ~~`EAMYw1CPoipVWtkObbtcR2oDnNc`~~ | `VYsjwBZ6Rif5dFkboigchjgDnac` |

**历史 (2026-06-04)：** 原子分类 token 对 `docs +create` 全量返回 3380002。回退使用 UF7Cw5w2Wi 一级分类 + Move API 分拣方案运行至 7月4日节点重建。Move API 不受 3380002 影响。

### lark-cli 内容写入与验证陷阱 ★ (2026-06-01 定位+校正, 2026-06-15 更新)

**认知校正**：`docs +update v2` 对 Wiki docx 节点**实际可以写入**——之前认为"永远无效"是 `docs +fetch` 的误报（fetch 显示 blocks=0 但 REST API 确认内容存在）。但两步法仍不推荐——一步法更可靠。

**三步现状**：

| # | 问题 | 影响 | 状态 |
|---|------|------|:--:|
| 1 | `lark-cli doc +create` 无效命令（应为 `docs`） | hotlist_collector 从未成功 | ✅ 已修正为 `docs +create` |
| 2 | `lark-cli docs +create --markdown` 用 v1 API | **实测可正常工作** (REST API 验证) | ✅ 可靠，已回退到一步法 |
| 3 | `docs +fetch` 不可靠（blocks=0 误报） | 无法通过 CLI 验证内容 | 用 REST API 验证 |

**API v2 语法注意事项 (2026-06-20 更新, lark-cli 1.0.53+):**

> ⚠️ **lark-cli 1.0.53 重大变更 (2026-06-20):** `--title`, `--wiki-node`, `--wiki-space` 三个 v1 标志已完全移除。使用旧标志会报 `validation: invalid_argument`。详见下方新陷阱条目。

| 要点 | 说明 |
|------|------|
| 格式 flag | `--doc-format markdown` 或 `--doc-format xml`（默认），v2 API 使用此 flag |
| 内容传参 | `--content "$(cat /tmp/file.md)"` 或 `--content "inline markdown..."` |
| Wiki 放置 | `--parent-token TOKEN`（替代旧 `--wiki-node`） |
| 标题 | Markdown `# Title` 或 XML `<title>`，旧 `--title` 已废弃 |

**travel-intel 特有**：Wiki 文档为骨架格式（标题 + bookmark 链接），无正文内容。内容写入失败不影响采集流水线——简报生成时 `curl` 取原文提取摘要。

**内容验证**：不用 `docs +fetch`，用 REST API：
```bash
lark-cli api GET "/open-apis/docx/v1/documents/{obj_token}/blocks/{obj_token}/children" --as bot
```

### Cron 09:00 时段 agent 模式争抢 → Broken pipe (2026-06-05) ★

当多个 agent 模式 cron job 调度在同一分钟（尤其是 09:00），基础设施层可能出现进程争抢，导致某个 job 被终止并报 `RuntimeError: [Errno 32] Broken pipe`。这是 Python 写管道时对端已关闭的典型症状——agent 进程被调度器提前终止或 API 连接中断。

**诊断方法**（不要急着修代码，先排除环境因素）：

1. **对比同时间槽其他 job**：`cronjob list` 查看同一分钟的其他 job 状态
   - agent 模式 job 全失败 → 基础设施争抢（概率高）
   - 仅 script/no_agent job 正常 → 进一步确认是 agent 运行时问题
2. **区分 agent vs script 模式**：`no_agent: true` 的脚本 job 不受影响说明不是 API/网络问题
3. **手动重跑验证**：`cronjob run <job_id>` 触发单次运行
   - 重跑成功 → 确认是瞬态，等下次调度即可
   - 重跑仍失败 → 检查数据源/API/模型可用性
4. **`cronjob list` 不实时更新手动运行状态** — `cronjob run` 后 `last_status` 可能仍显示之前的错误，不要以此判断手动运行结果。关注 job 是否实际推送到群。

**已知 09:00 共存 job**（容易互相影响）：

| Job | 类型 | 状态历史 |
|-----|:--:|------|
| travel-intel-daily | agent | 2026-06-05 Broken pipe |
| kanban-daily-review | agent | 2026-06-05 delivery error |
| zhike-morning | no_agent script | ✅ 正常 |

**缓解措施**：
1. 已将 `travel-intel-daily` 和 `travel-intel-weekly` 调度从 `0 9 * * *` 错开至 `5 9 * * *`（09:05），避开整点争抢窗口（2026-06-08 应用）。
2. **2026-06-24 二次修复**：`travel-intel-daily/weekly/insight` 三个报告 job 加载巨型 travel-intel skill（SKILL.md 2000+ 行）导致 DeepSeek context 膨胀 → 流式 180s 超时断管。已将所有三个 job 的 `enabled_toolsets` 收紧为 `["terminal","file","feishu_doc","feishu_drive"]`，砍掉 browser/vision/web_search 等重型工具定义以减少 context 体积。
3. **2026-07-01 三次修复**：`travel-intel-expire` 同为 agent 模式 + 加载巨型 skill，缺失 `enabled_toolsets` 导致同样症状（Connection error / 断管）。已补加 `["terminal","file","feishu_doc","feishu_drive"]` 并恢复 job。**铁律：所有加载 `travel-intel` skill 的 agent 模式 cron job 必须设置 `enabled_toolsets`，否则 context 必然超限。**

### `docs +create` CLI 命令格式变更 ☆ (2026-06-20 发现+修复) ★

lark-cli 1.0.53 完全移除了 v1 遗留标志，之前所有 insight/weekly/briefing cron 中使用的命令格式全部失效：

```bash
# ❌ lark-cli ≥1.0.53 — 三个标志均被拒绝
lark-cli docs +create --api-version v2 \
  --title "标题" \              # validation: invalid_argument — 已移除
  --wiki-node TOKEN \           # 同上 — 用 --parent-token 替代
  --wiki-space SPACE_ID \       # 同上 — 用 --parent-position 替代
  --content @file.md

# ✅ 正确格式（lark-cli 1.0.53+）
# Markdown 格式
cd /tmp && lark-cli docs +create --api-version v2 \
  --doc-format markdown \
  --parent-token UF7Cw5w2WiHGfjkKVvBcxj8Hnib \
  --content "$(cat w25_insight.md)" \
  --as bot

# 标题从 Markdown 第一个 # heading 或 XML <title> 中提取
```

**影响范围**：
- `insight-execution-guide.md` 步骤 5（综合洞察/周度分析创建文档）
- 每日简报 cron（`travel-intel-daily`）
- 任何使用 `docs +create` 写入 Wiki 节点的脚本

**已修复**：`references/insight-execution-guide.md` 步骤 5 已更新为新格式。
**检测方法**：运行时若看到 `validation: invalid_argument` + `legacy v1 flag(s) --title, --wiki-node, --wiki-space` 即确认版本升级。

### 两步入库修复：docs+create --parent-token 不挂载 wiki 树 (2026-06-28 定位+修复) ★★★

**根因**：`lark-cli docs +create --parent-token <wiki_node>` 只设置 Drive 父目录，**不将文档挂载到 wiki 知识库树**。导致 6月23-28日采集结果全部创建为孤立 Drive 文档（api 返回 success 但 wiki +node-list 不可见）。

**验证**：
```bash
# docs+create 返回 doc_id=xxx，document 存在
lark-cli api GET "/open-apis/docx/v1/documents/{doc_id}" --as bot  # ✅ code=0
# 但 wiki 树中查不到
lark-cli api GET "/open-apis/wiki/v2/spaces/7643710721485753535/nodes/{doc_id}" --as bot  # ❌ 131005 not found
```

**修复（两步入库）**：
```python
# Step 1: docs+create (只在 Drive 创建文档)
r = subprocess.run(["lark-cli", "docs", "+create", "--api-version", "v2",
    "--doc-format", "xml", "--content", "@file.xml",
    "--parent-token", TOKEN, "--as", "bot"], ...)
doc_id = r.json()["data"]["document"]["document_id"]

# Step 2: wiki+move (将文档移入知识库树)
r2 = subprocess.run(["lark-cli", "wiki", "+move",
    "--obj-token", doc_id, "--obj-type", "docx",
    "--target-parent-token", TOKEN, "--target-space-id", SPACE_ID,
    "--as", "bot"], ...)
```

**影响范围（全量修复 2026-06-28）**：
- `l2_ingestor.py` ✅ — 两步入库 + WIKI_SPACE_ID 常量
- `browser_collector.py` ✅ — v1→v2 API + wiki+move + WIKI_NODES 回退 token
- `hotlist_collector.py` ✅ — --wiki-node/--title/--markdown 废弃标志修复 + wiki+move
- `l3_poller.py` ✅ — lark-cli 路径修复（PATH 不含 ~/.local/bin）
- Cron prompts ✅ — `travel-intel-collect/daily/weekly` --page-limit 升至 40

### expiry_checker.py 静默 JSON 解析失败 (2026-07-01 发现) ★

`list_docs()` (L189-190) 的 `except json.JSONDecodeError: break` 会**静默吞噬所有 API 故障**。若 REST API 返回非 JSON（如 `404 page not found`、网关超时 HTML），函数返回空列表（0 docs），cron 状态显示 "ok" 但实际未扫描任何文档。

```python
# expiry_checker.py L189-190 — 静默陷阱
except json.JSONDecodeError:
    break  # 不记录任何错误，直接返回空列表
```

**诊断方法**：手动运行 `python3 scripts/expiry_checker.py --dry-run 2>&1 | grep "node"` 确认每个节点返回的文档数 > 0。若三节点均为 0 → API 端点失效。

**API 端点验证**：用 `lark-cli --dry-run` 确认正确端点格式（不要盲改 Python f-string）：
```bash
lark-cli wiki +node-list --space-id 7643710721485753535 \
  --parent-node-token WnAmwXK6xiqlh3kXGN6cHYGinxe --page-size 3 --dry-run --as bot
# 输出显示实际调用的 endpoint URL 和 params
```

当前正确端点：`GET /open-apis/wiki/v2/spaces/{space_id}/nodes?parent_node_token={token}&page_size={size}`

### 周编号歧义：`%W` vs `%V` (2026-06-20)

`date +%W` 和 `date +%V`（ISO 8601）返回不同的周编号：

```bash
$ date -d 2026-06-20 '+%W %V'
24 25
```

- `%W`：周一起始，01 从第一个周一开始（1月5日当周）→ 6月20日为 W24
- `%V`（ISO 8601）：周一起始，W01 为含1月4日的那周 → 6月20日为 W25

**当前 convention**：综合洞察和周度分析使用 **ISO 周编号** (`isocalendar()[1]`)，与 `date +%V` 一致。用 `date +%W` 会产生 1 周偏差。
