Ingest Paper

API key required
Other

自动解析用户提供的 arxiv 论文或 PDF,生成结构化摘要、更新知识库中的概念和资源页,并同步飞书表格。

Install

openclaw skills install ingest-paper

Skill: ingest_paper — paper-kb 存入文档

用途

把用户发来的 arxiv 论文或上传的 PDF 文档,自动分析并存入其专属的 Gitea 知识库: 生成结构化摘要页(summaries/)、更新跨文档概念页(concepts/)和科研资源页 (resources/),同步飞书多维表格,并回复用户。

触发条件

Activate when(满足任一):

  • 消息中包含 arxiv 链接,且表达了存储意图("存"、"入库"、"保存"、"记录"、"加到知识库"等)。
  • 用户上传了 PDF 文件,且表达了存储意图。
  • 消息中只有 arxiv 链接或只上传了文件、没有说明意图 → 先问一句: "需要我把这篇存入你的知识库吗?" 用户确认后再执行。
  • 上一轮查重发现疑似重复后,用户回复"覆盖"/"是"/"继续存"。

Do NOT activate when:

  • 用户在提问、查找文献("有没有……的论文")→ 交给 query_papers。
  • 用户未注册(init_user check 返回 registered=false)→ 先走 init_user。
  • 用户发的链接不是 arxiv(GitHub、新闻等)→ 告知当前只支持 arxiv 链接和 PDF 上传。
  • 用户明确说不要存了 / 取消。

前置依赖

  • current_user_open_id:从消息上下文 sender 获取,传给所有脚本的 --open_id
  • 本 Skill 根目录需有 .env(GITEA_URL / GITEA_ADMIN_TOKEN / GITEA_BOT_USERNAME)。
  • 用户必须已注册(init_user 的 check 返回 registered=true)。 用户记录里的 research_direction(研究方向)在分析步骤要用。

临时文件路径约定(重要)

所有中间文件放在 /tmp/paperkb/

  • arxiv PDF:/tmp/paperkb/arxiv_{arxiv_id}.pdf(/ 替换为 _)
  • 提取文本:与 PDF 同名 .txt
  • 用户上传的 PDF:保存为 /tmp/paperkb/upload_{原文件名}.pdf
  • 你生成的页面草稿:/tmp/paperkb/draft_summary.md/tmp/paperkb/draft_concept_{名}.md

如果执行中途丢失了 PDF 路径:arxiv 论文按上述规则用 arxiv_id 重建路径; 重建后文件不存在则重新运行 fetch_arxiv.py。

完整执行流程

Step 1:获取文档

A. arxiv 链接:

python3 scripts/fetch_arxiv.py --url "<用户发的链接或ID>"

成功输出含:arxiv_idtitleauthorspublishedprimary_categoryabstractpdf_path。失败时把 message 转告用户,流程终止。

B. 用户上传 PDF: 把附件保存到 /tmp/paperkb/upload_{文件名}.pdf,记下路径。此时还没有标题, 标题在 Step 3 分析后由 AI 提取。

Step 2:提取全文

python3 scripts/process_pdf.py --pdf_path "<PDF路径>"

输出含 text_path(全文 txt 路径)、truncated(是否截断)、head(开头600字)。 失败(如纯扫描件)时把 message 转告用户,流程终止。

Step 3:AI 分析(你自己完成,不调脚本)

用文件工具读取 text_path 的全文,按下面的要求分析。 分析前先取用户的 research_direction(注册时已记录)。

分析输出必须是严格的 JSON(你内部使用,不直接发给用户):

{
  "doc_type": "论文 | 调研报告 | 会议纪要 | 技术文档 | 其他",
  "title_zh": "中文标题(原标题是英文时翻译;上传PDF时从内容提取标题)",
  "title_original": "原文标题",
  "brief": "一句话简介(50字内,用于目录)",
  "summary": "中文综述,200-300字:做了什么、用什么方法、得到什么结论、有什么价值",
  "research_question": "这篇文档要解决的核心问题(1-2句)",
  "methods": ["核心方法要点1", "要点2", "..."],
  "conclusions": ["主要结论1(尽量带关键数据)", "..."],
  "sections": [{"name": "章节名", "points": "该章2-3句要点"}],
  "keywords": ["关键词1", "...共5-8个"],
  "relevance": {"score": 8, "reason": "结合用户研究方向「{research_direction}」说明评分理由"},
  "concepts": ["从文档中提取的抽象概念,如 力控制、模仿学习"],
  "resources": [{"name": "具体资源名", "type": "数据集|开源项目|工具|硬件",
                 "note": "文档中如何使用/评价它"}]
}

要求:

  • 全部中文。
  • relevance.score 是该文档与用户研究方向的相关性(1-10),不是文档质量分。
  • concepts 只列抽象概念;resources 只列文档中实际使用或评测过的具体资源, 只是提了一下名字的不算。
  • 上传 PDF 场景:title_zh 就是后续的文件名,必须从内容里准确提取。

Step 4:查重

python3 scripts/check_duplicate.py --open_id <open_id> \
    --title "<title_zh>" --arxiv_id "<arxiv_id,无则省略>" \
    --text_path "<text_path>"
  • duplicate: true → 告知用户已存在(给出 existing 里的标题和时间), 问"是否覆盖?"。用户确认后从 Step 5 继续(save 时加 --force);否则终止。
  • possible_duplicate: true → 告知用户疑似与《existing.title》重复 (相似度 similarity),同样问是否继续。
  • duplicate: false 且无 possible → 直接继续。

Step 5:概念与资源规划(你自己完成)

先读取已有概念和资源的目录:

python3 scripts/kb_read.py --open_id <open_id> --list all

根据 Step 3 的 concepts / resources 和已有目录,决定每一项是 create(新建)/ update(更新已有)/ skip(仅在 summary 里提及,不建页)

规划规则(严格遵守):

  • 知识库文档数 ≤ 3 时,每篇最多新建 2-3 个概念页,宁缺毋滥。
  • 名字相同或含义重叠的,一律 update 已有页,不要 create 新页。
  • 不要为"文档主题本身"建概念页(那是 summary 的工作)。
  • 资源页同理:已有的 update,新的才 create;每篇文档新建资源页一般 0-5 个。

Step 6:生成并保存概念页 / 资源页

对每个 create 项,按模板写 Markdown 草稿到 /tmp/paperkb/draft_concept_{名}.md, 然后:

python3 scripts/save_page.py --open_id <open_id> --kind concept \
    --name "<概念名>" --file "<草稿路径>" --brief "<一句话定义>"

对每个 update 项,先读旧内容:

python3 scripts/kb_read.py --open_id <open_id> --read "concepts/<概念名>"

把新文档的信息自然地融合改写进全文(不是简单追加),保留原有结构, 写入草稿后用同样的 save_page.py 保存(同名自动覆盖)。

资源页用 --kind resource 并加 --resource_type

概念页模板:

# <概念名>

## 定义
<清晰的中文定义,2-3句>

## 相关文档中的论述
- **[[summaries/<文档标题>]]**:<该文档对此概念的处理方式和结论,1-2句>
(每个相关文档一条,新文档的条目融入而非堆在最后)

## 方法对比
(仅当多篇文档用了不同方法时写,用表格:方法|优点|缺点|来源)

## 关联概念
[[concepts/<其他概念>]](只链接确实存在的页面)

## 矛盾与待解决问题
(不同文档结论冲突时记录在此;没有则写"暂无")

资源页模板:

# <资源名>

## 类型
数据集 / 开源项目 / 工具 / 硬件

## 简介
<2-3句>

## 在哪些文档中被使用
- **[[summaries/<文档标题>]]**:<如何使用、效果如何>

## 获取方式
<文档中提到的链接/地址;没有则写"文档未提供">

## 相关资源
[[resources/<其他资源>]](只链接确实存在的页面)

Step 7:生成最终版 summary 页(两步重写在这里完成)

现在所有概念页/资源页已确定。白名单 = 本次 create/update 的页面 + kb_read 列出的已有页面。按模板生成最终 summary,其中 [[wikilinks]] 只允许指向白名单 内的页面;白名单外的概念以纯文本出现。

summary 页模板:

---
标题: <title_zh>
原文标题: <title_original>
类型: <doc_type>
来源: <arxiv / 上传PDF>
arxiv_id: <有则填>
作者: <逗号分隔,上传PDF无作者信息则省略此行>
发表时间: <published>
关键词: [<关键词逗号分隔>]
相关性评分: <score>
存入时间: <今天日期>
---

# <title_zh>

## 一句话总结
<brief>

## 中文综述
<summary>

## 研究问题
<research_question>

## 核心方法
- <methods 逐条>

## 主要结论
- <conclusions 逐条>

## 章节要点
- **<章节名>**:<要点>

## 与我研究方向的相关性
评分:<score>/10
理由:<reason>

## 关键概念
[[concepts/<概念1>]] [[concepts/<概念2>]](仅白名单内)

## 科研资源
[[resources/<资源1>]](仅白名单内;没有则省略本节)

写入 /tmp/paperkb/draft_summary.md,然后保存:

python3 scripts/save_paper.py --open_id <open_id> \
    --title "<title_zh>" \
    --summary_file /tmp/paperkb/draft_summary.md \
    --doc_type "<doc_type>" \
    --keywords "<关键词逗号分隔>" \
    --score <score> \
    --brief "<brief>" \
    --arxiv_id "<有则填>" \
    --pdf_path "<PDF路径>" \
    --text_path "<text_path>" \
    [--force]   # 仅覆盖模式

输出含 summary_urlpdf_urlrepo_url

Step 8:同步飞书多维表格(可选,失败不阻塞)

用户记录里有 feishu_app_tokenfeishu_table_id 时(非空),调用 feishu_bitable_app_table_record(action: create):

app_token: <用户的 feishu_app_token>
table_id:  <用户的 feishu_table_id>
fields: {
  "标题": "<title_zh>",
  "类型": "<doc_type>",
  "关键词": "<逗号分隔字符串>",
  "相关性评分": <score 数字>,
  "存入时间": <当前毫秒时间戳,数字>,
  "Gitea链接": {"link": "<summary_url>", "text": "<title_zh>"},
  "arxiv_id": "<有则填,无则空字符串>"
}
  • 字段为空(用户初始化时飞书未启用)→ 直接跳过本步。
  • 调用失败 → 最多重试 1 次,仍失败则跳过,在回复中注明"飞书表格同步失败, 不影响知识库"。绝不因此中断流程或丢弃已保存的内容。

Step 9:回复用户

✅ 已存入知识库!

📄 <title_zh>
🏷 类型:<doc_type>|🔑 关键词:<前几个关键词>
⭐ 与你研究方向的相关性:<score>/10 — <reason 精简到一句>
🧠 概念页更新:<新建X个/更新Y个,列名字>
🛠 资源页:<同上,没有则省略>
🔗 查看详情:<summary_url>
  • 覆盖模式時开头改为"✅ 已覆盖更新!"。
  • 上传 PDF 场景追加一行:💡 建议把本地 PDF 重命名为:<title_zh>.pdf
  • 飞书表格跳过/失败时追加说明。

错误处理总则

  • 所有脚本输出单行 JSON;success: false 时按 message 转告用户, 不要把原始报错堆给用户。
  • stdout 出现非 JSON 内容 = 脚本异常,截取关键信息告知用户并建议联系管理员。
  • 任何一步失败都不要静默吞掉——告诉用户哪一步失败、能否重试。