---
name: report-workpaper
description: >
  把券商研究报告的每一段话找到网络数据来源，做成 Excel 底稿（左侧报告段落截图，
  右侧来源网页截图+红框高亮支撑句+可点击标题链接，逐行对齐）。半自动：逐段联网搜索、
  列候选给用户确认，再截图组装。表格内容单独建 sheet 写来源。
  触发：底稿、做底稿、数据来源、报告找来源、research workpaper、source check、
  把报告每段找来源、合规底稿。
metadata:
  author: lihonghao
  version: "0.3"
---

# report-workpaper · 研报底稿生成

把一份 `.docx` 研报的某一章/节，逐段找到网络数据来源，产出一个 Excel **底稿**：

```
┌──────────────────────┬───┬─────────────────────────────────────────────┐
│  左：报告段落截图       │   │  右：证据列 —— 每一句话一个来源，从左到右依次排    │
│  (红色小标题+加粗首句)  │   │  句1来源→ 句2来源→ 句3来源→ …（每个上方=可点击标题）│
│  一段 = 一个横向条带     │   │  红框圈出该句在原文里的支撑文字                  │
└──────────────────────┴───┴─────────────────────────────────────────────┘
```
**排布规则（用户定义，务必遵守）**：左侧报告内容**从上往下**逐段排；右侧证据**从左到右**
依次放——**报告里的每一句话**配一个来源截图，按句子顺序左→右排满；这一段所有句子都有来源后，
再下移到下一段重复。一段有 N 个可考证句子 → 右侧就有 N 个来源横向并排。
- 每个三级小节（如 4.3.1）= 一个 sheet，sheet 名即小节号。
- 节内的**表格/图**另起 sheet（sheet 名=表/图标题），单独写来源（见 Phase 2）。

## 逐句必须找源（默认每一句都要核实，不要轻易留白）
**默认对报告里的每一句话都要做数据源核实、找到能支撑它的材料**——这是用户的硬要求。
不要因为"看起来像观点"就整段跳过（这是早期版本常犯的偷懒）。具体做法：
- **事实/数据句**（数字、占比、时间、政策、公司动态、产品、订单、第三方测算）→ 必须配权威逐字源。
- **总述/承上启下/topic 句**（"深耕…铸就全球头部Tier1""股权治理结构稳定"）→ **拆出其中的事实内核去配源**
  （如"全球头部Tier1"→招股书"全球汽车零部件排名第41""全球第二被动安全"；"股权治理稳定"→实控人持股结构）。
- **战略/框架句**（"三大战略结构清晰""稳基石/强增长/拓远期"）→ 找公司**公开披露的对应战略表述/业务布局**做支撑
  （官网战略、招股书业务概览、业绩会、定点公告），而不是直接判定"本所观点、无需来源"。
- **真正的纯主观判断**（"我们认为/我们看好/预计…将"且无任何可外部验证的事实内核）→ 才可留白，
  但**必须在备注里写明：已尝试的检索角度 + 为何无外部源**，不能默默空着。
- **只有在多角度穷尽检索后仍找不到**真实逐字源，才留 `sources:[]`，并在核查备注标"留白原因+已试关键词"。
宁可一句话配一个"支撑其事实内核"的源，也不要整段空白。逐句覆盖率是本 skill 的核心质量指标。

**例外·财务数据可不溯源（用户约定）**：**典型的大片财务数据——收入/营收、利润/净利/毛利率、股权与持股结构——不需要本 skill 找源**，
因为用户用**同花顺(iFinD) / 万德(Wind)**自己做溯源。这类句子在底稿对应位置留空即可，并在核查备注标注"财务/股权数据→按约定由 iFinD/Wind 溯源，不在底稿核查范围"。
**注意边界**：① 仍需逐句核的是**非财务数据**——产品、订单/定点金额、市占率与行业地位（定性）、战略与业务布局、并购/设立/控股等公司事件、子公司分工、技术与合作、行业规模/渗透率/政策。
② 订单/定点金额、市占率虽带数字，但不在标准财报/Wind 口径内，**仍要找源**；只有进财报三表与股东名册的收入/利润/股权才豁免。
③ 报告里财务数字与官方"对不上"的，仍可在核查备注提示（供用户用 iFinD/Wind 复核），但不必为其找网页红框源。

## 环境依赖（本机已具备）
`python3` + `openpyxl` + `Pillow` + `playwright`(python) + 已装 chromium。无需 LibreOffice。
`fetch_verbatim.py` 只用 stdlib（urllib），**无额外依赖、无需 MCP**。
联网遵循用户的 **web-access** 规则：① 搜索用 `WebSearch`（找候选 URL）；② **取页面逐字正文**用本
skill 的 `fetch_verbatim.py`（Jina Reader `r.jina.ai`，免 key 20 RPM / 设 `JINA_API_KEY` 500 RPM）——
**不要用 `WebFetch` 挑红框短语，它会改写原文**；③ 截图用 `capture_source.py`（自带 headless chromium）；
登录态/反爬站点（微信公众号、小红书、登录后 PDF、Jina 取回 403 的页）→ 让 Chrome 开
`--remote-debugging-port=9222`（或经 web-access 开启），再给 `capture_source.py` 传
`--cdp http://localhost:9222` 复用用户已登录浏览器读实时 DOM。

**脚本路径（关键，全局可用）**：本 skill 可在任何项目/目录触发。运行下面命令前先固定路径——
```bash
SK="$HOME/.claude/skills/report-workpaper/scripts"   # 或 ${CLAUDE_SKILL_DIR}/scripts
```
下文命令里的 `scripts/X.py` 一律按 `python3 "$SK/X.py"` 来跑（不要假设当前目录就是 skill 目录）。
所有产物（截图、manifest、xlsx）写到**当前工作的项目目录**里，不要写进 skill 目录。

> 换机器/换系统时：需自行装 `openpyxl/Pillow/playwright`+chromium，并确认有中文字体
> （本机用 macOS 自带 PingFang SC；Linux/Windows 需改 `render_paragraphs.py` 字体）。

---

## 工作流（半自动·逐段确认）

### Step 1 — 选定章节
```bash
python3 scripts/extract_report.py REPORT.docx --list-headings
```
把树状目录给用户，确认要做哪一节（如「4.3 缺电」）。标题样式按 styles.xml 的
outlineLvl 识别（0/1/2=H1/H2/H3），跨报告通用。

### Step 2 — 抽取该节正文
```bash
python3 scripts/extract_report.py REPORT.docx --h2-contains 缺电 --label 4.3 \
        --out section.json
```
得到 `section.json`：每个小节(4.3.1/4.3.2…)的标题、各段正文(含逐 run 粗体)、
以及节内出现的图/表标题（供 Phase 2 建表格 sheet）。

### Step 3 — 渲染左侧报告段落截图
```bash
python3 scripts/render_paragraphs.py section.json --outdir left_imgs/ \
        --out-manifest left_manifest.json
```
每段渲染成 PNG（红色小标题+加粗首句+两端对齐正文，仿报告样式；dpr=3 高清）。

### Step 3.5 — 逐句覆盖清单（确保不漏句）
```bash
python3 scripts/split_sentences.py section.json --out coverage.json
```
把每段切成句子并标 factual/opinion，作为找源清单——**报告里每一句话都要在右侧有对应来源**，
按句序左→右排满该段，再下移。**即使被标成 opinion 的句子也要先按上面「逐句必须找源」去拆事实内核找源**，
只有穷尽检索后确实无外部源的纯主观判断才留白（并在备注写明已试角度）。不要看到 opinion 标签就跳过。

### Step 4 — 逐句找来源（**与用户确认**）

> **推荐打法·逐句覆盖用「多 agent find→对抗verify」工作流**（实测对整章/逐句覆盖最稳，2026-06 在均胜1.1验证）
> 句子多时不要自己一句句串行查。**先把主源拿下，再并行核每一句**：
> 1. **先定主源**：找到标的公司的 **H股招股书/聆讯后资料集 或 年报/季报**（港交所 hkexnews、巨潮、公司官网 IR）。
>    一份招股书往往一页就覆盖市场规模/市占率/ASP/产品矩阵/公司排名/历史沿革多句——下载后用 `pdftotext` 按页检索关键数字定位页码，
>    `pdftoppm` 渲染该页整页图+手标（PDF 红框留人工）。这类"大片定性/产品/排名"事实优先走招股书一手。
> 2. **再逐句并行核**：对每个**非财务**句子派一个 agent，**两段流水线**——
>    - **find**：给它[公司背景+源纪律+已知线索URL]，要它找一个**逐字可验证**的权威源，亲自 `fetch_verbatim.py --find` 校验 ok:true 才回传；
>    - **verify(对抗)**：另一个 agent **独立**复核——重跑 `--find` 确认逐字命中、并判断该源是否**真支撑该句事实内核**（不是擦边），不轻信 find。
>    用 Workflow 工具的 `pipeline(SENTENCES, findFn, verifyFn)` 一次跑完（每句 find 完即 verify，无需等齐）；返回结构化 `{sid, verdict, final_url, final_phrases, note}`。
> 3. **分类落账**（写进"逐句覆盖报告"，见 Step 5）：
>    - **support**：源逐字支撑事实内核 → 直接红框。
>    - **partial**：事实内核已逐字坐实可红框，但报告的**概括/对仗措辞或个别细节系作者提炼**（如"三大战略""稳基石/强增长/拓远期"、eVTOL、微电机等）→ 红框框事实句，备注提示正文**软化**。
>    - **豁免**：收入/利润/股权等财务数据 → 留空+备注（iFinD/Wind）。
>    - **留白**：多角度穷尽仍无权威源 → `sources:[]`+备注"已试关键词"。
> 目标：**非财务句的真·留白=0**。

对每个 factual 句子（不只是每段），若不走上面工作流而手动来：
1. 用 `WebSearch` / `firecrawl_search` 搜该句的关键数字/事实。**密集查找时可并行派多个 general-purpose / Explore
   agent**，每个包 1 个事实点，要求其**实际抓取页面核实"逐字短语真实存在"**再回传 URL+短语。
2. **给用户列候选来源**（标题+链接+说明支撑哪一句），让用户挑/否决/补链接（半自动核心，不替用户拍板）。
3. 确定**红框高亮的原文句子**：必须是来源网页里**逐字存在**的短句（含数字，如「5,427 data centers」
   「65GW」「doubles roughly every five months」）。注意全角/半角、"基荷"≠"基础负荷"、花引号。
   **不要信 `WebFetch` 返回的"原文"——它会改写**，挑出来的短语在页面上往往不逐字存在、红框就框不上。
   改用 `fetch_verbatim.py` 先取页面**逐字正文**（Jina Reader `r.jina.ai`，免 key、stdlib，无需 MCP），
   从中挑短语，再用 `--find` 校验该短语**确实会被命中**（校验逻辑和 capture_source 的 HIGHLIGHT_JS 完全一致）：
```bash
python3 scripts/fetch_verbatim.py --url URL --out page.md          # 取逐字正文，从中挑短语
python3 scripts/fetch_verbatim.py --url URL --find "5,427个" --find "占全球总量的45%"
   # ok:true=会命中可直接截图；unmatched=换更短的逐字片段；
   # fold_only=短语存在但全/半角不一致（如 ５,４２７ vs 5,427），改成页面里的写法
   # 高频批量可设环境变量 JINA_API_KEY（免费，500 RPM）；403/反爬页改走下面 --cdp 读实时 DOM
```
4. 截图（CDP 抓取，自带红框）：
```bash
python3 scripts/capture_source.py --url URL --out src_imgs/431_p1s2_xxx.png \
   --title "来源标题 [S2]" \
   --highlight "逐字支撑句1" --highlight "逐字支撑句2"
   # 登录态站点加： --cdp http://localhost:9222（复用已登录 Chrome）
   # 找不到精确句时加 --full 出整页图，红框留给用户手标
   # 可调： --crop-w 760（强制横裁宽度） --context 130（上下文行） --max-h 1500（高度上限）
```
**裁剪是“贴着高亮句”的**（不是整页）：横向裁到高亮所在**段落/正文列**（≈600–800px），纵向只取
高亮行 + 上下各一两行上下文。原因：底稿把每张源图按**固定宽度**展示（build_workpaper `SW`），
屏幕上文字高度 ≈ `网页字号 × SW ÷ 裁剪宽度`——整页宽裁会把 17px 的字缩成 ~8px 看不清。
**一张源图只配一句话**：`--highlight` 尽量给**一句**（或同一段里**相邻**的两三个短语）；
**别把相隔很远的多个短语塞进一次截图**，否则裁剪会拉得很高、字很小。脚本会把落在裁剪框外的
高亮放进返回 JSON 的 `off_crop`——**看到 `off_crop` 非空就拆成多次截图**（每句一张，左→右并排）。
返回 JSON 报 `matched`/`unmatched`/`off_crop`，没命中就换更短的逐字片段重试。

> **隐藏 tab / 滚动渐显动画会截出空白**：有的官网页面把文字放进未激活的 tab/手风琴
> （`display:none`、定位屏外），或用**滚动渐显动画**（初始 `opacity:0`/与背景同色，滚到才显形）。
> 这些文本节点在 DOM 里**能逐字匹配到、红框也会画上**，但渲染区是空白 → 截出来是「白底+空黄框」。
> **改用该页默认就可见的逐字句，或换一个把同一事实写在正文里的来源**（官网 `news/info/*` 新闻页、年报摘要、财经媒体正文）。
> **均胜官网已实测的空白页→替代源（直接用，别再踩坑）**：
> - `about.html` 发展历程时间轴（2004起步/涡轮增压进气/空气管理）= 空白 → 换 `news/info/710.html`（"成立于2004年""核心产品涵盖空气管理系统、发动机进气系统"，正文可见）。
> - `driving.html` 智能驾驶 tab（均联智行/L2++至L4）= 空白 → 换 `news/info/1251.html`（"均联智行发布首款智能驾驶域控制器"）或招股书智能网联页。
> **截完务必逐张抽看**确认不是空白——`ink%` 粗筛会漏掉这种「空框」图（黄框本身有像素，ink<3.5% 基本是空白），
> 最稳的是把所有源图拼成缩略图 montage 一眼扫一遍（见 redo_1.1/capture_jobs_11.py 末尾的 montage 写法）。
>
> **红框落点偏移/空框**：个别站点（如东方财富「财富号」`caifuhao.eastmoney.com`）DOM 结构异常，
> 红框会画到右边距的空白处或整体偏移几个字；逗号串里的裸数字（“497.93亿元、…”跨行）也易框错。
> 解决：① `--highlight` 用**带前后词的整句片段**（如「全年实现营业收入558.6亿元」而非「558.6亿元」）；
> ② 换一个红框正常的来源（公司官网/上证报·中国证券网/证券时报/第一财经/盖世 实测都正常）。

### Step 5 — 组装底稿 + 预览
把每段拼进 manifest（schema 见下），然后：
```bash
python3 scripts/build_workpaper.py build_manifest.json --out 底稿_4.3.xlsx \
        --preview-dir preview/
```
`preview/preview_<label>.png` 是**所见即所得预览图**（和 Excel 同坐标），先给用户看，
确认无误再交付 `.xlsx`。

**配套交付·逐句覆盖报告**（强烈建议，证明"每句都配了源"）：另出一个小 xlsx，逐句一行列
`段·句 | 报告句要点 | 数据来源(可点击) | 已验证逐字短语 | 支撑度(support绿/partial黄/豁免灰) | 备注(口径/软化/留白)`，
顶部"说明"sheet 写覆盖统计（如"19句→support12+partial5+豁免2+留白0"）与源纪律。
模板见 `redo_1.1/build_coverage_11.py`；差异/对不上的另出"核查备注"见 `ch2/build_notes2.py`（可合并多章）。

### Phase 2 — 表格 sheet（暂缓，按需开启）
节内每个表/图另起 sheet（名=表/图标题），把表格每一格/每条数据的来源写进去；
在对应正文 sheet 用 `{"type":"sheet","title":"见表: …","target":"<表sheet名>"}`
建跨表跳转链接。当前版本先做文字+图片，表格按用户节奏再加。

---

## build_manifest.json schema
```json
{
  "section_label": "4.3",
  "section_title": "缺电：逻辑持续演绎 海内外算力景气共振",
  "sheets": [
    {
      "label": "4.3.1",
      "title": "北美缺电背景：需求端大幅增长与供给端多重约束的失衡",
      "rows": [
        {
          "left_img": "/abs/left_imgs/4.3.1_p1.png",
          "sources": [
            {"title": "来源标题（会做成可点击链接）",
             "url": "https://…",
             "img": "/abs/src_imgs/431_p1_xxx.png"}
          ]
        },
        {"left_img": "/abs/left_imgs/4.3.1_p2.png", "sources": []}
      ]
    }
  ]
}
```
- 一段未找到来源 → `"sources": []`（底稿会留白，提示待补）。
- 一段多来源 → `sources` 多个，右侧自动横向并排。
- 跨表链接 → 源对象用 `{"type":"sheet","title":"见表: 表10","target":"表sheet名"}`（无 img）。

## 布局参数（scripts/build_workpaper.py 顶部常量，可调）
`LW` 左图宽 / `SW` 来源图宽 / `GAP`,`SGAP` 间距 / `TITLE_H` 标题带高 / `ROW_GAP` 段间距。
像素网格 `ROW_PX=20`,`COL_PX=64`=**正常大小单元格**（width=(COL_PX-5)/7 精确映射回像素）；
`showGridLines=True` 显示网格线，底稿看起来就是普通表格。图片用 twoCellAnchor 锚真实单元格
+ 子单元格 EMU 偏移定位，所以格子大小不影响图片对齐。段间分隔线 `SEP_SIDE`=红色 medium。

## 已知限制（用户已确认可接受）
- **微信公众号 / 小红书**：这两类来源**不需要**，直接跳过，不必为它们费力截图。
- **PDF 来源**：chromium 能打开但 PDF 阅读器里无法逐字定位红框 → 用 `--full` 出整页图，
  红框留给用户手标即可，不必强求自动框选。
- **来源优先级**：① 除了**国内券商友商研报（同业研报，如东吴/中信等）不能引**，其余来源
  （官方/外媒/IEA/大行/公司财报/行业媒体/199IT/前瞻网/OFweek 等）都可以。
  ② **优先中文源**：若中文数据源能**充分且很好地覆盖该论点（每一句话）**，就优先用中文源；
  仅当中文覆盖不足/质量不够（如北美电力等议题一手数据多为英文）时，再用外文源。
  ③ **主源优先**：公司类章节先拿**招股书/年报/季报/公告**一手（市占率/市场规模/产品/排名/沿革常一页覆盖多句），
  官网新闻页(`news/info/*`)次之，权威媒体(上证报/证券时报/第一财经/光纤在线/银柿/盖世/中证网/同花顺/21财经)再次之。

## 注意事项
- **来源必须真实且确实支撑该句**——这是合规底稿，宁缺毋滥；找不到就留 `sources:[]`（底稿留白=待补），
  绝不编造链接。不确定就列候选给用户判断。
- 红框靠"逐字匹配"实现：`--highlight` 必须用来源页里**真实存在的原文**（注意全角/半角、"基荷"≠"基础负荷"、
  花引号等），不是报告里的转述。**关键修复**：早期用 `WebFetch` 读页面挑短语——它会**改写原文**，
  导致挑出的短语在页面上不逐字存在、红框框不上。现在统一先用 `fetch_verbatim.py`（`r.jina.ai`，免 key）
  取**逐字正文**再挑短语，并用 `--find` 校验（其归一逻辑与 HIGHLIGHT_JS 一致，还会用 `fold_only` 提示全/半角不符）。
- **图片锚定（关键，勿回退）**：组装时图片必须用 `twoCellAnchor` 锚到**真实单元格**（见
  build_workpaper.py `cell_marker`）。早期版本用 `oneCellAnchor` 锚 A1+大像素偏移，macOS
  Excel/Numbers 会把偏移**截断**导致所有图挤在左上角重叠——已修复，不要改回去。
- **截图勿用 full_page，且要“贴着高亮句”裁**：高 dpr 下整页截图会变成上亿像素（PIL 解压炸弹）
  且广告页会卡 "waiting for fonts"。capture_source.py 用 **CDP `Page.captureScreenshot` + clip**，
  **横向裁到高亮所在正文列、纵向只取高亮行+少量上下文**（dpr=3）——这样底稿里源图的字才够大。
  不要改回 full_page，也不要把裁剪放宽到整页宽（会让字缩小到看不清）。多个相距远的短语**拆成多张**。
- 大文件：原始底稿可能上百 MB（满是截图），新建独立 `.xlsx`，不要直接改用户的原文件。
- 渲染/截图字体用 PingFang SC（macOS 自带）；换机器需确认中文字体。

## 源发现（WebSearch + Firecrawl MCP 已接入）
逐字正文用 `fetch_verbatim.py`（Jina Reader，免 key、已默认接入）。**源发现**有两条互补通道：
- **`WebSearch`**（内置，默认）：通用闭环，无需任何配置。
- **Firecrawl 远程 MCP**（已接入，2026-06-25 配好并实测 ✓）：中文发现更强。提供 `firecrawl_search`
  （发现，Bing/Google 风格，实测能直出新浪/东财/上证报等中文源）+ `firecrawl_scrape`（逐字 markdown/rawHtml）。
  - 配置：`claude mcp add --transport http firecrawl https://mcp.firecrawl.dev/v2/mcp --header "x-firecrawl-api-key: fc-..."`
    （key 写进 header；远程服务，无需 docker；免费档 ~1000 credits/月）。已加在本机 local config，**重启会话后** `firecrawl_search`/`firecrawl_scrape` 才出现在工具列表。
  - **源纪律照旧（重要）**：Firecrawl 搜索会混入研报库结果（实测 `pdf.dfcfw.com`/东财研报、`fxbaogao` 等）。
    这些**只能当线索**，必须主动剔除；真正取源与红框仍走官网/巨潮公告/上证报·中国证券网/证券时报/第一财经/新浪/光纤在线/盖世/银柿等权威源，并经 `fetch_verbatim.py` 逐字校验。
- **SearXNG MCP**（备选，真免 key 但需自建 docker）：`docker run -d -p 8080:8080 searxng/searxng`（settings.yml 开 `json`），
  `.mcp.json` 加 `{"mcpServers":{"searxng":{"command":"npx","args":["-y","mcp-searxng"],"env":{"SEARXNG_URL":"http://localhost:8080"}}}}`；
  `searxng_web_search(query, language="zh", engines="baidu,bing,sogou")` 聚合百度/搜狗/必应。Firecrawl 已接入时一般用不到。
- **避免**：Perplexity（返回改写后的 LLM 答案，正是要躲的 bug）、Brave（2026-02 取消免费档、仅 snippet）。

## 待办（低优先，暂缓）
- 单段来源过多（5+）时右侧超宽，可加自动换行；manifest 改相对路径提升可移植性；
  跨平台中文字体回退；URL 格式校验。这些不影响当前 macOS 单机使用。
