---
name: research
description: 查 paper_server 文献库(基于 OpenAlex 元数据 + Sci-Hub 下载的内部部署)。用户要查文献、找 DOI、拉 PDF、做文献综述、写带引文的申报书 / 研究方案 / 调研报告时使用。
---

# Research

paper_server 是内部部署的 Django 文献库:元数据来自 OpenAlex,PDF / XML 由 Sci-Hub / OpenAlex 异步抓取。**库里主语料是英文**(OpenAlex 主索引英文文献),少量中文。本 skill 给你四个 helper(`search` / `get_paper` / `fetch_pdf` / `fetch_xml`),用 `run_python` 调用,**不要**自己 `httpx` 裸调 API。

## 何时用

- 用户要查 / 找 / 看 / 推荐文献
- 要 DOI、要某篇 PDF、要 abstract
- 写申报书 / 研究方案 / 调研报告的"国内外现状"段需要真实文献支撑
- 配合 `proposal` skill 的「立项依据」起草 —— 先 `search` 拿候选,看 abstract 决定要不要引

## 何时不用

- 用户只问通识(直接答即可,不需要文献支撑)
- 用户已经给了具体文献清单(直接用,不要二次校验)

## 准备

```python
from skills.research.paper import search, get_paper, fetch_pdf, fetch_xml
```

(import 路径由 `run_python` 注入的 `PYTHONPATH` 提供,直接写就行,不必折腾 `sys.path`)

> **本 skill 持一把低价值 key**。paper_server 的 GET 接口要 `PAPER_SERVER_API_KEY`(helper 自动以 `?api_key=` 查询参数带上,你不用管);它是唯一刻意放进 sandbox 的凭证 —— `run_python` 的 env 过滤器对它放行,docker 模式由宿主透传进容器 → host / docker 任何模式都能用。`PAPER_SERVER_URL` 可选覆盖(默认 `http://paper.xxhhcty.xyz:8080`)。**别因 documents / pymatgen 报"key 没配"就连带放弃 research**,这俩不可用时它就是降级首选。

## 关键:keyword 优先用英文

`search(keyword=...)` 走 paper_server SearchFilter,模糊匹配 **title / first_author / first_author_institution**(目前不含 abstract)。库里 95%+ 文献 title 是英文,中文 keyword 命中率很低。

**用户输入中文 → 先转成专业英文术语再 search**:

| 用户原话 | 不要这样 | 这样 |
|---|---|---|
| 水泥水化 | `search("水泥水化")` | `search("cement hydration")` |
| 钢筋锈蚀 | `search("钢筋锈蚀")` | `search("steel reinforcement corrosion")` |
| 混凝土碳化 | `search("混凝土碳化")` | `search("concrete carbonation")` |
| 锂离子电池电解液 | `search("锂离子电池电解液")` | `search("lithium-ion battery electrolyte")` |

转译策略:
- 用领域内标准英文术语(查不准就先英文 keyword 试一次看返回 title 是不是相关)
- 多词术语用空格分隔(`SearchFilter` 默认空格视作 AND,要更宽用单词)
- 不确定时同义词都试一遍:`search("CO2 absorption")` 没结果 → 试 `search("carbon dioxide capture")`
- 中文期刊 paper 也可中文 keyword 单独搜一次(`search("水泥") + filter 中文期刊` 命中率不算低,但远不如英文主搜)

## 四个函数

### `search(keyword="", year=None, year_gte=None, year_lte=None, doi="", first_author="", publication_name="", has_pdf=None, is_oa=None, limit=10) -> list[dict]`

搜文献,返回精简列表(每条含 16 字段:id / doi / title / first_author / first_author_institution / publication_year / publication_date / publication_name / has_fulltext_pdf / has_fulltext_xml / has_abstract / is_oa / type / **abstract** / **pdf_url** / **xml_url**)。

- `keyword`: SearchFilter,匹配 title / first_author / first_author_institution(**英文为主**,见上节)
- `year` / `year_gte` / `year_lte`: 精确年份 / 范围(做"近 5 年文献"用 `year_gte=2020`)
- `doi`: 精确 DOI(命中 0 / 1 条)
- `first_author` / `publication_name`: 精确作者 / 期刊
- `has_pdf=True` 仅返 PDF 已下好的;`False` 仅返没 PDF 的;`None` 都返
- `is_oa=True` 仅返开放获取(OA);`False` 仅返非 OA;`None` 都返
- `limit`: 默认 10,上限 50

```python
# 找近 5 年水泥水化研究,且 PDF 已下好
papers = search(keyword="cement hydration", year_gte=2020, has_pdf=True, limit=10)
for p in papers:
    print(p["title"], p["publication_year"])
    if p["abstract"]:
        print(p["abstract"][:200])  # 看摘要前 200 字判断是否切题
```

### `get_paper(id_or_doi) -> dict`

取单条完整 metadata + abstract。`id_or_doi` 既接受 paper_server 内部 id,也接受 DOI(自动解析)。

**list 端点已带 abstract,正常工作流不需要调本函数** —— 仅在用户给单个 id / DOI 想拿全字段(含 OpenAlex 原始字段如 `o_keywords` 等)时用。

```python
paper = get_paper("10.1016/j.cemconres.2020.106156")
print(paper["title"])
print(paper["abstract"])
```

### `fetch_pdf(id_or_doi, working_dir) -> str`

下载 PDF 到 `<working_dir>/papers/<safe_doi>.pdf`,返回相对路径 `papers/<safe_doi>.pdf`(safe_doi 把 `/` 换成 `_`)。**走 paper_server media 静态直链**(从 list/retrieve 返回的 `pdf_url` 字段),跟 `fetch_xml` 同范式。已存在跳过下载直接复用。

`has_fulltext_pdf=False` 或 `pdf_url` 空(publication_date 缺失)→ 抛 `RuntimeError`。

```python
rel = fetch_pdf("10.1016/j.cemconres.2020.106156", working_dir="<task_dir>")  # <task_dir> = system prompt 给的工作目录绝对路径(docker 沙盒里形如 /workspace/<wd>)
# rel == "papers/10.1016_j.cemconres.2020.106156.pdf"
```

### `fetch_xml(id_or_doi, working_dir) -> str`

下载 XML 到 `<working_dir>/papers/<safe_doi>.xml`,对称 `fetch_pdf`。**走 paper_server 的 media 静态直链**(由 list/retrieve 返回的 `xml_url` 字段提供),paper_pdf_view 只覆盖 PDF,XML 没对应 API。已存在跳过下载直接复用。

`has_fulltext_xml=False` 或 `xml_url` 空(publication_date 缺失时会空)→ 抛 `RuntimeError`。

**为什么 XML 优先 PDF**:XML 已结构化 —— 章节标题 / 摘要 / 段落 / 参考文献 / 图表 caption 都有标签,LLM 读取无需 OCR 或 PDF 文本抽取的不确定性;文献综述 / 引文清单 / 章节定位场景比 PDF 友好得多。能拿 XML 就别拿 PDF;只有需要看具体公式 / 图表内容 / 表格数据时才下 PDF。

## 标准工作流

1. **(若用户输入中文)转专业英文术语**
2. **search**:按 keyword + filter(年份范围 / OA / has_pdf 等)缩窄候选,`limit=10` 起
3. **直接看返回里的 abstract**(list 端点已带):
   - abstract 非空 → 看前 200-400 字判断切题
   - abstract 空(`has_abstract=False`)→ 仅凭 title + 期刊 + 年份判断,信号弱时下条候选
4. **下全文(若需要)** —— **优先级**:
   - `has_fulltext_xml=True` → `fetch_xml`(LLM 友好,结构化)
   - 仅 `has_fulltext_pdf=True` → `fetch_pdf`(回退,需要 PDF 文本抽取)
   - 两者都 False → 仅凭 abstract 写综述,告诉用户哪几篇没全文
5. **read 全文**:fetch 返回相对路径,用主 agent 的 `read` 工具读取(zcbot 已内置 PDF 文本抽取;XML 直接当文本读)

## 错误处理

- 网络超时 / paper_server 不可达:`httpx.ConnectError` / `httpx.TimeoutException` —— 告诉用户"paper_server 暂时连不上",不要重试堆栈刷屏
- `paper_server auth failed (HTTP 400/401/403, not_authenticated/...)`:`PAPER_SERVER_API_KEY` 未配置或无效 —— 告诉用户找管理员在 `.env` 配 key,换 keyword 重试没用
- `doi 未命中` / `doi 命中多条`:`get_paper` / `fetch_pdf` / `fetch_xml` 内部 `_resolve_to_id` 抛 `ValueError` —— DOI 拼写错或库里没收录,改 keyword 重搜
- `has_fulltext_pdf=False` / `has_fulltext_xml=False`:`fetch_pdf` / `fetch_xml` 抛 `RuntimeError` —— 服务器还没下到对应格式;若另一格式存在则换用,都没就只能用 abstract
- `xml_url` 空(publication_date 缺失,paper 落到 unknown 目录):`fetch_xml` 抛 `RuntimeError(xml_url unavailable...)` —— 改试 `fetch_pdf`
- 文件 disk 缺失(`has_fulltext_pdf=True` 但 paper_server 侧文件丢了 → HTTP 404):helper 透传 `httpx.HTTPStatusError`,告诉用户换一篇
- `abstract` 字段为空字符串:正常情况,不是错;告诉用户"这篇没收录摘要"即可
- search 命中 0 条:先尝试同义词 / 缩短 keyword / 放宽 filter,3 次还是 0 条告诉用户库里没覆盖,**不要凭训练数据脑补文献**

## 反模式

- 用任何 HTTP 客户端(`httpx` / `requests` / `urllib` / `aiohttp` / shell `curl`)裸调 paper_server API —— 一律走 helper。裸调跳过本 skill "中文转英文术语" / `has_pdf` / `year_gte` filter 教学,典型坑:`search=cement+based` 字符级模糊匹配返几千条横跨无人机 / 锂电池 / 热界面,LLM 还以为搜对了
- 用户输中文直接 `search(中文)` —— 转英文术语,见上节
- `search(limit=50)` 一次拉满后 dump 给 LLM 全文(只 print 前 5-10 条精简就够,要全部让用户自己 `print(papers)`)
- 已经看到 abstract 还 `get_paper` 一遍(list 已带 abstract,重复调白费 roundtrip)
- 没看 abstract 就 `fetch_pdf` / `fetch_xml`(80% 场景 abstract 够用,下载全文慢且费带宽)
- `has_fulltext_xml=True` 时盲走 `fetch_pdf`(XML 对 LLM 更友好,先试 fetch_xml)
- 编造 DOI / title / 作者 —— 不在 paper_server 库里就**明确告诉用户"未命中"**,不要凭训练数据脑补
- 把 `fetch_pdf` 返回的相对路径当绝对路径用(它是相对 `working_dir` 的)
