diff --git a/DESIGN.md b/DESIGN.md index 19504cf..ca1fa1a 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -90,6 +90,8 @@ yaml 是手填的,probe 用真实调用对账(basic_chat/parallel_tools/thinking ### 3.5 Skill 系统(Anthropic 渐进披露) 三层加载:Discovery(name+description,几百 token)→ Activation(`load_skill` 完整 SKILL.md)→ Execution(references 按需拉)。写 WHY+WHAT 不写 Step 1/2/3;description 决定触发。 +**出版物检索统一入口**(2026-08-25):`literature` 按用户任务而非数据源划分 skill,统一论文、书籍、章节、会议论文、学位论文、报告、标准、专利、预印本及未来出版物的发现、获取、跨源去重与证据核验;`paper_server`、内部材料知识库及未来来源只是可替换后端。出版物类型与来源正交,同一出版物可有多条来源记录;跨源机械层只自动合并 DOI、ISBN+版次、标准号、公开号等精确键或题名+主要责任者+年份完全一致项,疑似重复、不同版次、章节、标准修订版和专利族成员保守保留。题录、摘要、片段、全文四级证据分开标注,下载成功不等于全文已经核验。取舍是不为每个数据库新增 skill,也不把检索编排固化成统一高级工具:skill 负责路由与判断,现有 helper/host-side tool 保持各自信任域,确定性标准化与精确去重下沉到可重建脚本。 + **用户私有 skill**(2026-06-11):registry 收有序来源列表——内置 `ROOT/skills`(只读)+ 用户 `user_root/.skills`(可写)。取舍:① **user wins 同名覆盖**(核心用例是"copy 内置再改",覆盖只作用于本人会话,blast radius 锁死),覆盖显式标注不静默;② **创作走 host-side `save_skill`/`fork_skill`**——fs 工具的 base_dir 跨 backend 够不到 `user_root/.skills`,host 工具一个落点两模式通吃;③ 用户 skill 加载失败收进 `load_errors` 注入 prompt 提示修,不崩整次扫描。 **Skill 定向模型**(frontmatter `model:`,2026-07-06):内置 skill 可声明"该工作流用这个模型最好";当前未配置定向模型的 skill。**单一执行点**:对话中 `load_skill` 命中 → run 内热切(loop 换 `self.llm/self.caps`,下一轮生效)+ 持久化 task 模型;切失败降级原模型。取舍:**跳档位门控**(产品决策,任何档位可用);**只信内置 skill**(用户 skill 的 model 忽略,防自写 frontmatter 绕门控);**不自动切回**("skill 结束"不可判定);**不设 DB 开关、不做建 task 预切**——frontmatter 那一行本身就是热配置(删行即停,per-skill 粒度),全局开关是第二事实源、预切是第二执行点,都砍;**已选国际旗舰模型(unifyllm 网关族:Claude/GPT/Gemini)豁免切换**(2026-07-15)——定向 pin 本意是给较弱的国产默认模型托底产物质量,用户已主动选了旗舰模型则尊重其选择、不降级(判据:current profile 的 family==unifyllm)。 diff --git a/PROGRESS.md b/PROGRESS.md index 9926eaa..5ffef6a 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 -最后更新:2026-08-24(Origin 图型扩展,bump 0.68.0) +最后更新:2026-08-25(literature 统一出版物检索入口,未发版) --- @@ -10,7 +10,7 @@ | Phase | 标题 | 状态 | 备注 | |---|---|---|---| -| 1-3 | 骨架 + Skill + run_python | ✅ | 多 skill(coding/proposal/ppt/research/documents/imagegen/videogen/review/patent);CoreCoder 唯一匹配 edit;敏感 env 过滤 | +| 1-3 | 骨架 + Skill + run_python | ✅ | 多 skill(coding/proposal/ppt/literature/imagegen/videogen/review/patent);CoreCoder 唯一匹配 edit;敏感 env 过滤 | | 4 | 演化性能力 | 🟡 | Model Profile + Probing ✅;版本化 prompt 未做 | | 5 | Eval Suite | ⏸ 不做 | dogfooding 替代,probe 覆盖健康检查 | | 6 | 长任务工程化 | 🟡 | task + 恢复 ✅;双层记忆 ✅;context 压缩 ✅(加压力门槛);长会话中段折叠摘要 ✅(§8.8 Phase 2) | @@ -20,6 +20,8 @@ --- ## 已完成关键能力 +- **08-25 / Unreleased / literature 统一出版物检索入口**:删除按数据源拆分的 `research` / `documents` skill,新增 `literature` 统一论文、书籍、章节及未来学术与技术出版物的发现、获取、跨源去重和证据核验;paper_server helper 归入新 skill,内部材料库客户端移到 host-side `tools/` 保持密钥隔离。新增开放出版物模型、来源与证据 references,以及只自动合并精确键、保守标记疑似重复的 `merge_publications.py`;`brief/paper/review/rebuttal/patent/standard/analyze` 和 Skill 清单统一改走新入口,不提升版本号。 + - **08-24 / 0.68.0 / Origin 小提琴分布图**:`origin.plot@v2` / adapter 1.3.0 新增 `violin`,每条 `input/y` 系列作为一组原始观测,Worker 复制到受控全 Y 展示表并通过 Origin 2024 原生 `Violin.otpu` 一次性生成分类分布图;默认隐藏冗余图例,保留分类标签并为 X 轴标题预留底部空间。首版不开放逐系列 style、分裂/半小提琴及带箱线变体,避免把未稳定映射的模板属性写入公共契约。固定 QA 新增三组抗压强度分布、中位数 oracle、OPJU 重开与全格式复导;Origin 2024 / originpro 1.1.15 数据、视觉与进程释放门禁通过,Origin/合同/Job/Node 专项 126 项 unittest、Ruff 致命规则、py_compile、diff 检查与独立 adapter 打包通过,未连接或写入数据库。 - **08-24 / 0.68.0 / Origin 材料谱图错位叠加**:`origin.plot@v2` / adapter 1.2.0 新增 `stacked_line`,面向 XRD、XPS 和光谱多曲线比较;请求显式声明间隔百分比,Worker 保留原始工作表并生成基线对齐的受控展示表,按全体最大谱幅确定逐条偏移,允许不同 X 采样点且不依赖节点模板。默认使用曲线右端直标并按画布定位标题,显式图例请求仍兼容。固定 QA harness 同步修正 OriginPro 列式读取、空白工作簿及短/长名称和有效列标签识别;Origin 2024 / originpro 1.1.15 真机生成、OPJU 重开、PNG/SVG/PDF 复导、偏移基线 oracle、视觉检查与进程释放通过。Origin/合同/Job/Node 专项 124 项 unittest、ruff 致命规则、py_compile 与独立 adapter 打包校验通过,未连接或写入数据库。 - **08-24 / 0.67.0 / Origin 真机 QA harness MVP**:Origin adapter 新增独立 `acceptance.py`,以 annotations、2×2 Recipe、heatmap、surface_3d、stacked、band 六个固定合成用例通过生产 `worker.py` 子进程执行 OPJU/PNG/SVG/PDF 全格式任务,再由独立 Origin 会话重开工程、核对图层/系列/工作表/矩阵与固定数值 oracle 并复导三种图件;报告记录环境指纹、请求/产物摘要、像素尺寸和 Origin 进程释放结果,失败也原子落盘。独立 adapter 包同步携带验收脚本;Origin/合同/Job/Node 专项 121 项 unittest、ruff 致命规则、py_compile 与独立 adapter 打包校验通过,目标 Origin 2024 真机执行待部署节点完成,未连接或写入数据库。 diff --git a/SKILL_LIST.md b/SKILL_LIST.md index d5766e9..180a131 100644 --- a/SKILL_LIST.md +++ b/SKILL_LIST.md @@ -1,8 +1,8 @@ # zcbot Skill 清单 服务对象:中国建筑材料科学研究总院 —— 无机非金属材料 R&D(水泥 / 混凝土 / 玻璃 / 陶瓷 / 耐火 / 新型建材) -最后更新:2026-08-25(research / documents 统一支持论文、书籍与章节查阅,并精简检索路由约束) -Skill 总数:18 +最后更新:2026-08-25(literature 统一所有学术与技术出版物的检索、跨源去重和证据核验) +Skill 总数:17 zcbot 的"skill"是一份可加载的工作流脚本(`skills//SKILL.md` + 配套 templates / scripts / Python helper),模型在识别用户意图后挂载对应 skill,按其内置的阶段化流程产出可交付物。本文档面向**使用方 / 协作方**,按"做什么、什么时候用、什么时候别用、典型产物"组织。 @@ -22,9 +22,8 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills//SKILL.md` + | 科研写作 | [rebuttal](#rebuttal) | 回复审稿意见(修回):逐点回复信 + 稿件修改清单 + 修回 cover letter | | 演示出图 | [ppt](#ppt) | 生成可编辑 PowerPoint(SVG-first:逐页手写 SVG → 原生 DrawingML;总院红品牌模板可选) | | 演示出图 | [plot_pub](#plot_pub) | 出版级 matplotlib 学术图(中文 + viridis + 矢量 + 投稿级复合图设计纪律) | -| 文献检索 | [research](#research) | 查 paper_server 中的论文、书籍与章节(题录 / 摘要 / 可用全文) | -| 文献检索 | [documents](#documents) | 查内部 7 学科材料知识库(论文 / 书籍 / 全文语义检索) | -| 文献检索 | [brief](#brief) | 科研方向简报:三路检索(research + 内部库取文献 / web 取动向)→ 重要论文列表(带摘要概述)+ 内容总结,只描述不给建议 | +| 文献检索 | [literature](#literature) | 查论文、书籍、章节及其他出版物;统一 paper_server 与内部材料库,跨源去重并核验证据 | +| 文献检索 | [brief](#brief) | 科研方向简报:literature 两路取文献 + web 取动向 → 重要论文列表(带摘要概述)+ 内容总结,只描述不给建议 | | 科研计算 | [pymatgen](#pymatgen) | 晶体结构 / XRD 模拟 / 相图 / Materials Project(host-side tool 持 key) | | 科研计算 | [stats_ml](#stats_ml) | 配方-性能建模与机器学习(三库分工) | | 内容生成 | [imagegen](#imagegen) | 豆包 Seedream 5.0 文生图 + 改图 i2i(¥0.22 / 张) | @@ -51,11 +50,11 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills//SKILL.md` + **何时不用**: - ⛔ 只改 / 润色已有稿 → 走 review - ⛔ 写本子 / 申报书 → 走 proposal;写交底书 → patent;写标准 → standard -- ⛔ 只查文献 → research / documents;只出图 → plot_pub +- ⛔ 只查出版物 → literature;只出图 → plot_pub **核心能力**: - 三类论文 × 中英双语的 IMRaD / 主题式骨架 + 篇幅预算(`paper_types.md`) -- **引文三角核验**(`citation_verify.md`,移植 ARS 思路、后端换成自有 documents/research 库):存在性 → 三角印证 → 支撑度(抓原文比对 ≤25 词锚点,partial 就改论断迁就证据),编造引文零容忍 +- **引文三角核验**(`citation_verify.md`,移植 ARS 思路、检索接 literature 的 paper_server 与内部材料库):存在性 → 三角印证 → 支撑度(抓原文比对 ≤25 词锚点,partial 就改论断迁就证据),编造引文零容忍 - "先定图表再写正文"纪律(接 plot_pub 出 figure)+ **图表纪律**(`figure_discipline.md`,移植 figure-planner:一图一论断、panel 角色分工、锚 panel、主图 vs 补充材料、图例四规则)+ 文献矩阵立证据底座 - 写作顺序 Methods→Results→Intro→Discussion→Abstract→Title;关键章一段一卡 + 预告下一段 - `quality_check.py`:结构 / 占位符 / 过度宣称 + **引文交叉核对**(orphan / uncited / 编号连续);docx/pdf 调平台渲染层 `rendering/render.py --profile paper`(中英字体切换 + 图题自增);`word_count.py` 按类型 × 语言核篇幅 @@ -155,7 +154,7 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills//SKILL.md` + **何时不用**: - 从零起草新文本 → 走对应写作 skill(proposal / patent) -- 事实核查 / 文献真伪 / 最新政策 → 先用 research / documents / web 核验 +- 事实核查 / 出版物真伪 / 最新政策 → 先用 literature / web 核验 - 代码审查 → 走 coding **审稿顺序**(先大后小,分层处理): @@ -185,7 +184,7 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills//SKILL.md` + **核心能力**: - **拆条分诊**:编辑要求 E.x / 审稿意见 Rn.m 原子化编号 → 8 类意见 × 4 档严重度 × 12 个动作标签(ACCEPT_TEXT / CLARIFY_EXISTING / SOFTEN_CLAIM / OUT_OF_SCOPE / AUTHOR_INPUT_NEEDED / BLOCKING...)逐条建 tracker -- **零编造铁律**:不虚构实验 / 数据 / 行号 / 图号 / 引文 / "已修改"声明;用户腹稿"我们已经改了"类含糊陈述一律追问到位置和内容(中文备注九种含糊的转换表);补引文走 documents/research + 引文三角核验 +- **零编造铁律**:不虚构实验 / 数据 / 行号 / 图号 / 引文 / "已修改"声明;用户腹稿"我们已经改了"类含糊陈述一律追问到位置和内容(中文备注九种含糊的转换表);补引文走 literature + 引文三角核验 - **先改稿后写信**:回复信对着改完的稿子写;每条回复三段式(原评论全文引用 → 头 1-2 句直说动作 → 修改后原文节选斜体) - **语气纪律**:合作但不卑;不同意走五步窄口径结构;拒做实验只用研究设计 / 范围边界作理由(时间经费借口禁用);"审稿人误解了"一律转写成稿件清晰度问题 - **疑难案例库**:做不了的实验 / 审稿人事实错误 / 两审稿人打架 / 统计大修 / 合规缺件 / 转投稿 / appeal 识别 @@ -253,55 +252,30 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills//SKILL.md` + ## 文献检索 -### research -**检索独立 `paper_server` 项目中的论文、书籍和书籍章节。** +### literature +**检索、获取、合并与核验所有学术和技术出版物。** -`research` 是 zcbot 对 `paper_server` 的客户端适配,可取得题录、DOI、摘要及记录实际提供的 PDF/XML。适合跨学科发现、精确题名或 DOI 查找、出版类型识别和引文核验;材料领域任务可与 `documents` 并查。 +`literature` 把论文、书籍、章节、会议论文、学位论文、技术报告、标准、专利、预印本及未来接入的其他出版物视为统一对象;出版物类型与数据源分离。当前接入两个来源: -按任务所需证据深度选择题录、摘要或全文:结构化段落适合 XML,页码、公式、图表与版式核验适合 PDF。输出明确区分“仅题录”“摘要已核对”“全文已核对”。 +- `paper_server`:基于 OpenAlex 元数据,擅长 DOI、题名、作者、期刊、年份和跨学科发现,可取得摘要及记录实际提供的 PDF/XML。 +- 内部材料知识库:覆盖胶凝、陶瓷、玻璃、晶体、复合、耐火和检验检测,擅长跨语言全文语义检索、Markdown 正文片段和原件下载。 -**四个 helper**:`search` / `get_paper` / `fetch_pdf` / `fetch_xml`。 +材料性能、工艺和表征优先内部材料库;精确 DOI 和跨学科发现优先 `paper_server`;系统调研、重要引用和关键论断并查可用来源。跨源结果先按 DOI、ISBN、标准号、公开号等类型标识去重,无稳定标识时只对题名、主要责任者和年份完全一致的记录自动合并;版本、章节、标准修订版和专利族成员保守保留。 -**典型产物**:论文、书籍和章节候选清单,以及可核验的摘要、全文和引用条目。 +证据统一标记为 `metadata_only` / `abstract_verified` / `snippet_verified` / `fulltext_verified`。下载原件但未实际读取,不算全文已核对;出版物真实存在也不代表它支持当前论断。 ---- +**主要能力**:`search` / `get_paper` / `fetch_pdf` / `fetch_xml`,以及 host-side `document_list_kb` / `document_search` / `document_download`;`merge_publications.py` 负责确定性的标准化、精确合并和疑似重复标记。 -### documents -**检索内部 7 个材料学科知识库中的论文、书籍及其他学术文档。** - -按 `kb_name` 分为胶凝、陶瓷、玻璃、晶体、复合、耐火和检验检测 7 个材料学科库,提供跨语言语义检索、Markdown 正文片段和原件下载。 - -**7 大学科库**(`classification_id` 1-7): -| 学科 | 内容 | -|---|---| -| 胶凝材料 | 水泥 / 混凝土 / 砂浆 | -| 陶瓷基材料 | 结构 / 功能 / 先进陶瓷 | -| 玻璃基材料 | 平板 / 光学 / 玻璃陶瓷 | -| 晶体材料 | 单晶 / 多晶 / 晶体生长 | -| 复合材料 | 纤维 / 颗粒 / 层状复合 | -| 耐火材料 | 耐火砖 / 不定形 / 高温防护 | -| 检验检测 | 表征方法 / 标准 / 仪器 | - -**核心特色**: -- **跨语言语义检索** —— 中英文查询均可用于发现学术文档 -- **Markdown 正文** —— 可直接筛选论点、数据和章节,必要时再读取原件 - -**何时用**: -- 查材料领域论文、书籍、性能数据或工艺信息 -- 需要全文语义检索,或为申报书、方案、报告和综述寻找可核验证据 - -**关系**:与 `research` 互补。`documents` 擅长材料领域全文语义检索,`research` 擅长跨学科题录和 DOI 检索;重要任务可并查,并按 DOI 或规范化题名去重。论文或书籍的来源类型不作为排他路由条件。 - -**三个 host-side tool**:`document_list_kb` / `document_search` / `document_download`。只有宿主配置 `DOCUMENT_SEARCH_API_KEY` 时注册;key 不进入 sandbox。 +**典型产物**:多类型出版物候选清单、规范化 `publications.json`、可核验摘要或全文、引文与论断证据台账。 --- ### brief **生成科研方向简报(重要文献速览)。** -给定一个研究方向 + 时间窗,从各大相关期刊(**Elsevier 数据库优先**)挑选近期**重要论文**,产出两段式简报:**先一份重要论文列表(每篇带标题/作者/期刊/年月/DOI + 一段简介或摘要概述),再对这批论文做内容总结**。三路取数:research(逐刊精确取最新 Elsevier 论文 + DOI)+ documents(内部材料库取全文)取文献,web search 取政策·标准·产业动向(单列)。**只描述不给建议**——呈现"发了什么、讲了什么",判断留给读者。简报 ≠ 综述论文,要**快、准、客观**,5–20 分钟掌握一个方向近期发了哪些重要论文。 +给定一个研究方向 + 时间窗,从各大相关期刊(**Elsevier 数据库优先**)挑选近期**重要论文**,产出两段式简报:**先一份重要论文列表(每篇带标题/作者/期刊/年月/DOI + 一段简介或摘要概述),再对这批论文做内容总结**。三路取数:`literature` 的 paper_server 与内部材料库取文献,web search 取政策·标准·产业动向(单列)。**只描述不给建议**——呈现"发了什么、讲了什么",判断留给读者。简报 ≠ 综述论文,要**快、准、客观**,5–20 分钟掌握一个方向近期发了哪些重要论文。 -**五阶段**:定题对齐 spec(方向+边界 / 时间窗 / 期刊范围 / 深度 / 数据源 / 语言 / 关注点)→ 三路取数(research+documents 取文献、web 取动向;中→英术语转译 + 跨源去重)→ 列清单(带摘要概述)+ 内容总结 → 引文核验 → 渲染验收。 +**五阶段**:定题对齐 spec(方向+边界 / 时间窗 / 期刊范围 / 深度 / 数据源 / 语言 / 关注点)→ 三路取数(literature 两个来源取文献、web 取动向;中→英术语转译 + 跨源去重)→ 列清单(带摘要概述)+ 内容总结 → 引文核验 → 渲染验收。 **深度三档(按篇数)**:`flash` 10–20 篇 / `standard` 20–40 篇 / `deep` 40–80 篇。 @@ -310,14 +284,14 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills//SKILL.md` + - ✅ 立项前想快速摸清一个方向近期发了哪些重要论文(产出可喂 proposal / analyze) **何时不用**: -- ⛔ 只要文献清单 / DOI / PDF → research / documents +- ⛔ 只要出版物清单 / DOI / PDF → literature - ⛔ 要写可投稿的综述论文(几十页、定论)→ paper(review 类型) - ⛔ 要把模糊科学问题拆成子问题 + 路线图 → analyze - ⛔ 要"对本院的建议" / 写本子 → proposal **核心能力**: - **逐刊取重要论文**(`references/journals.md`):各建材子领域主流期刊清单(Elsevier 优先),精确 `publication_name` + `year_gte` 取最新,0 命中降级到 keyword 搜;按重要性(期刊层级 + 主题相关性 + 发现分量)筛、按 publication_date 留最新 -- **三路分工 + 去重**:research+documents 取文献(同 DOI 一条、documents 全文优先)、web 单列产业政策动向不混论文总结;中文方向→英文术语转译(SCM/LC3 等缩写展开) +- **三路分工 + 去重**:literature 的 paper_server + 内部材料库取文献(同 DOI 一条、正文更完整来源优先作内容证据)、web 单列产业政策动向不混论文总结;中文方向→英文术语转译(SCM/LC3 等缩写展开) - **每篇带摘要概述**:列表不只标题,每篇 2–4 句讲研究对象/方法/主要发现,基于 abstract 或全文、不夸张不评判 - **引文核验**:存在性 / DOI 真伪(以库返回字段为准)/ 支撑度(摘要概述与原文一致,partial 改概述迁就证据),编造零容忍 - **平台渲染层 `rendering/render.py --profile brief`**(docx/pdf):商务红主题 + 论文列表 `[n]` 作锚点、正文 `[n]`/`[Wn]` 引文上标回链 + DOI/URL 可点击超链接(条目内 DOI 子串也链)+ 化学式下标(CO₂/C₃S...,白名单不误伤 LC3/Ca2+);pdf 走沙盒 chromium;做 deck 转 ppt @@ -460,7 +434,7 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills//SKILL.md` + ### analyze **科学问题分析 / 拆解 / 引导。** -用户抛过来一个模糊的高层科研问题,本 skill 引导用户把它**翻译成可操作的子问题 + 实施路线图**,然后把每个子问题接力给合适的下游 skill(proposal / research / pymatgen / stats_ml ...)。 +用户抛过来一个模糊的高层科研问题,本 skill 引导用户把它**翻译成可操作的子问题 + 实施路线图**,然后把每个子问题接力给合适的下游 skill(proposal / literature / pymatgen / stats_ml ...)。 **本 skill 不直接产出最终交付物**(不查文献 / 不写本子 / 不跑模型),只产出 `analysis.md` —— 一个共识性的"问题理解 + 路线图"文件。 @@ -474,7 +448,7 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills//SKILL.md` + - ⛔ 用户已经写明要做什么(配方对比 / 标准检测 / 文献综述 / 写本子)→ 直接走对应 skill - ⛔ 用户问通识知识 / 名词定义 → 直接答 - ⛔ 用户要审稿 / 改文 → 走 review -- ⛔ 用户做异常排查但已经定位到具体环节 → 走 research +- ⛔ 用户做异常排查但已经定位到具体环节 → 走 literature **四段工作流**: 1. **PICO 化**(BLOCKING)—— P 对象 / I 干预 / C 对照 / O 输出(必须量化:指标 + 单位 + 期望值),跑 FINER 自检 @@ -539,13 +513,13 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills//SKILL.md` + 实际任务往往跨多个 skill,典型组合: -- **写论文全流程**:analyze(拆问题) → stats_ml / pymatgen(算数据出结果) → research / documents(建文献矩阵) → plot_pub(先定图表) → paper(逐章起草 + 引文三角核验) → review(投稿前反谄媚终审) → 投出后收到审稿意见 → rebuttal(修回) -- **修回全流程**:rebuttal(拆条分诊 + 逐点回复信) → review(需大改的章节深审 / 英文润色子模式) → documents / research(审稿人要求补引文时检索核验) → plot_pub(要求补图 / 改图时) -- **写本子全流程**:analyze(拆问题) → research / documents(查文献) → stats_ml(算配方-性能模型出预实验数据) → plot_pub(出图) → proposal(写本子) → review(审稿) -- **写专利全流程**:patent(挖点 + 检索 + 起草) → research(查现有技术) → plot_pub(出附图) → review(终审) -- **写标准全流程**:analyze(定标准化对象) → stats_ml(配方-性能 / 精密度试验数据定指标) → research / documents(查国内外现有标准与现状) → standard(起草标准 + 编制说明) → plot_pub(出图) → review(送审前终审) +- **写论文全流程**:analyze(拆问题) → stats_ml / pymatgen(算数据出结果) → literature(建文献矩阵) → plot_pub(先定图表) → paper(逐章起草 + 引文三角核验) → review(投稿前反谄媚终审) → 投出后收到审稿意见 → rebuttal(修回) +- **修回全流程**:rebuttal(拆条分诊 + 逐点回复信) → review(需大改的章节深审 / 英文润色子模式) → literature(审稿人要求补引文时检索核验) → plot_pub(要求补图 / 改图时) +- **写本子全流程**:analyze(拆问题) → literature(查出版物) → stats_ml(算配方-性能模型出预实验数据) → plot_pub(出图) → proposal(写本子) → review(审稿) +- **写专利全流程**:patent(挖点 + 检索 + 起草) → literature(查现有技术出版物) → plot_pub(出附图) → review(终审) +- **写标准全流程**:analyze(定标准化对象) → stats_ml(配方-性能 / 精密度试验数据定指标) → literature(查国内外现有标准与现状) → standard(起草标准 + 编制说明) → plot_pub(出图) → review(送审前终审) - **方向简报 → 立项**:brief(三路取数,出重要论文列表 + 内容总结,只描述) → analyze(把方向拆成子问题 + 路线图) → proposal(写本子、给建议) / paper(写综述);简报要做成汇报 → ppt -- **PPT 汇报**:analyze(提炼论点) → research / documents(找数据 + 引文) → plot_pub(出图) → ppt(组装 deck) → imagegen(可选,做封面 / 引子页) +- **PPT 汇报**:analyze(提炼论点) → literature(找数据 + 引文) → plot_pub(出图) → ppt(组装 deck) → imagegen(可选,做封面 / 引子页) - **晶体计算**:pymatgen(算 XRD / 相图) → plot_pub(出图) → proposal / patent(写到本子 / 交底书里) - **定制能力**:skill-creator(fork 某内置 skill,如 ppt / proposal) → 改造成本组 / 本人专属版本(术语 / 模板 / 默认值),之后日常任务直接用改造版 diff --git a/core/executor_docker.py b/core/executor_docker.py index 188693b..88a7187 100644 --- a/core/executor_docker.py +++ b/core/executor_docker.py @@ -89,7 +89,7 @@ _CONTAINER_ENV = { "HOME": "/tmp", } -# research skill 在容器里直连 paper_server,这两个 env 按需透传(host .env → docker exec -e)。 +# literature skill 在容器里直连 paper_server,这两个 env 按需透传(host .env → docker exec -e)。 # PAPER_SERVER_API_KEY 是唯一刻意放进 sandbox 的凭证(低价值只读文献库 key,可随时在 # paper_server admin 撤销),与「Bocha/ARK 等高价值 key 不入容器」的规矩不冲突; # PAPER_SERVER_URL 顺带透传,修掉 host 覆盖 URL 时 docker 模式仍用硬编码默认值的缺口。 diff --git a/scripts/smoke_paper_skill.py b/scripts/smoke_paper_skill.py index 856874c..2e8b7d0 100644 --- a/scripts/smoke_paper_skill.py +++ b/scripts/smoke_paper_skill.py @@ -1,4 +1,4 @@ -"""Smoke: paper_server → zcbot research skill 三步链路。 +"""Smoke: paper_server → zcbot literature skill 三步链路。 跑法: .venv/Scripts/python.exe scripts/smoke_paper_skill.py @@ -33,7 +33,7 @@ if env_file.exists(): k, _, v = line.partition("=") os.environ.setdefault(k.strip(), v.strip()) -from skills.research.paper import _BASE_URL, fetch_pdf, fetch_xml, get_paper, search +from skills.literature.paper import _BASE_URL, fetch_pdf, fetch_xml, get_paper, search def _hr(title: str) -> None: diff --git a/skills/analyze/SKILL.md b/skills/analyze/SKILL.md index 550dc73..bb2fb2a 100644 --- a/skills/analyze/SKILL.md +++ b/skills/analyze/SKILL.md @@ -23,7 +23,7 @@ description: 科学问题分析 / 拆解 / 引导。用户提出模糊的高层 - 用户已经写明白要做什么(配方对比 / 标准检测 / 文献综述 / 写本子)→ 直接走对应 skill - 用户问通识知识 / 名词定义 → 直接答 - 用户要审稿 / 改文 → 走 review -- 用户要做异常排查但已经定位到具体环节(只差查文献验证) → 走 research +- 用户要做异常排查但已经定位到具体环节(只差查出版物验证) → 走 literature ## 资源 @@ -106,10 +106,10 @@ description: 科学问题分析 / 拆解 / 引导。用户提出模糊的高层 - 阶段一不让用户确认 PICO 就开拆解(O 没量化的 issue tree 拆出来全是空话) - Issue tree 拆到 4 层以上(LLM 容易堆细节,3 层足够 R&D 决策用) -- 叶子节点写"用 research skill"/"用 stats_ml skill"(skill 名硬编码,将来 skill 改名要回来改;用能力描述代替) +- 叶子节点写"用 literature skill"/"用 stats_ml skill"(skill 名硬编码,将来 skill 改名要回来改;用能力描述代替) - 用户已经知道要做什么时强行让填 PICO(走对应 skill 即可,不要浪费时间) - 真去执行子问题(查文献 / 算 XRD / 跑回归) —— analyze 只做"想清楚",执行交棒下游 -- 编造文献 / 数据支撑某个分支的可行性(不知道就标 `` + 建议接 research 验证) +- 编造出版物 / 数据支撑某个分支的可行性(不知道就标 `` + 建议接 literature 验证) - TRIZ / DoE 全套强推 —— 只在叶子节点性质匹配时用对应分支,不为了用框架而用 - 报告写成学术综述(分支只是工具,产物是路线图不是 paper) diff --git a/skills/analyze/references/issue_tree_patterns.md b/skills/analyze/references/issue_tree_patterns.md index 9d012d6..6c05d65 100644 --- a/skills/analyze/references/issue_tree_patterns.md +++ b/skills/analyze/references/issue_tree_patterns.md @@ -86,7 +86,7 @@ P42.5 早强偏低 - 拆出的叶子节点之间有重叠(违反 MECE 的 ME)—— 例:既有"原料 SiO2 含量"又有"配料 SiO2 比例",合并 - 漏掉显然该有的分支(违反 MECE 的 CE)—— 例:讨论强度问题但漏了"养护条件" -- 叶子标"用 research skill"/"用 stats_ml skill" —— 写能力描述,不写 skill 名(改名要回来改) +- 叶子标"用 literature skill"/"用 stats_ml skill" —— 写能力描述,不写 skill 名(改名要回来改) - 叶子优先级全标"高" —— 假装 MECE,等于没排 - 拆到 4 层才发现叶子全是 `` —— 说明 PICO 阶段问题没问清,回阶段一 - 拆完 tree 自己开始查文献做分析 —— analyze 只拆,执行交棒下游 diff --git a/skills/analyze/references/pico_template.md b/skills/analyze/references/pico_template.md index f436f3c..495e43a 100644 --- a/skills/analyze/references/pico_template.md +++ b/skills/analyze/references/pico_template.md @@ -68,7 +68,7 @@ PICO 填完后,5 个维度各给 1-5 分 + 一句话说明: - 用户问的是**文献调研类**("综述 X 领域") —— 直接 research,PICO 强行套反而别扭 - 用户问的是**工程实施类**("怎么把 X 推到生产") —— 走工艺优化路线,PICO 仍可用但 I/C 改成"工艺参数 vs 现行工艺" -- 用户的问题本身就是**工具选型**("用什么仪器测 X") —— 不需要 PICO,直接给经验答 / 走 research +- 用户的问题本身就是**工具选型**("用什么仪器测 X") —— 不需要 PICO,直接给经验答 / 走 literature ## 拒绝信号(说明问题还没准备好) diff --git a/skills/analyze/references/root_cause.md b/skills/analyze/references/root_cause.md index 706d2f2..7016f71 100644 --- a/skills/analyze/references/root_cause.md +++ b/skills/analyze/references/root_cause.md @@ -91,5 +91,5 @@ Why 5: ... → ... - Fishbone 列了 30 条 —— 大杂烩,失去优先级意义;每支 2-5 条,总数 < 20 - "原因"和"现象"混用(原因是过程:"温度漂移",不是名词标签:"温度") —— 拆错粒度 - 标"高优先"但没给优先理由 —— 写一句"为什么这条优先" -- 在 analyze skill 内部自己跑 5 Whys 自圆其说(模型自答自证) —— 验证步骤交棒下游(research 查文献 / 实验设计走 DoE) +- 在 analyze skill 内部自己跑 5 Whys 自圆其说(模型自答自证) —— 验证步骤交棒下游(literature 查出版物 / 实验设计走 DoE) - 假设单根因(只有一条原因) —— 工程问题常多根因叠加,Fishbone 阶段就要并列多个高优先项 diff --git a/skills/analyze/templates/analysis_report.md b/skills/analyze/templates/analysis_report.md index 105d9d6..88d3478 100644 --- a/skills/analyze/templates/analysis_report.md +++ b/skills/analyze/templates/analysis_report.md @@ -84,4 +84,4 @@ --- -**下一步建议**:<由 LLM 按当下 skill 清单匹配,例:"先用 research 能力查证假设 X,完成后再回 analyze 更新 §5"> +**下一步建议**:<由 LLM 按当下 skill 清单匹配,例:"先用 literature 能力查证假设 X,完成后再回 analyze 更新 §5"> diff --git a/skills/brief/SKILL.md b/skills/brief/SKILL.md index f4219d5..1fcc06a 100644 --- a/skills/brief/SKILL.md +++ b/skills/brief/SKILL.md @@ -15,7 +15,7 @@ description: 生成科研方向简报(research direction briefing / 重要文献 ## 边界(免得和别的 skill 撞) -- vs `research`/`documents`:它们**只取文献**;brief 把取回的论文**组织成可读列表 + 客观总结**。 +- vs `literature`:它**只取出版物并核验证据**;brief 把取回的论文**组织成可读列表 + 客观总结**。 - vs `paper`(review):paper 写**可投稿综述**(几十页、定论);brief 出**轻量速览**(几页、客观、不给判断)。 - vs `analyze`:analyze 拆**科学问题**;brief 围绕**已定方向**列近期重要论文。 - vs `proposal`:proposal 写**本子、给建议**;brief 只列论文 + 客观总结。要"对本院的建议" → 转 proposal。 @@ -35,18 +35,18 @@ description: 生成科研方向简报(research direction briefing / 重要文献 2. **时间窗**:默认**近 1 年**(简报是"最新文献",窗口宜短);换算成 `year_gte`(今年见 system prompt) 3. **期刊范围**:默认按方向所属子领域取 `journals.md` 主流期刊(Elsevier 优先);用户可增删指定刊 4. **深度 / 篇数**:`flash` 10–20 篇 / `standard`(默认)20–40 篇 / `deep` 40–80 篇 -5. **数据源(默认三路并用)**:research + documents **都是获取文献的主力**(research 按期刊精确取最新 Elsevier 论文 + DOI;documents 取内部材料库全文),web search 取政策·标准·产业动向(**单列、不混进论文总结**)。某一路不可用时降级用其余两路,不整体放弃 +5. **数据源(默认三路并用)**:`literature` 的 paper_server + 内部材料库**都是获取文献的主力**(前者按期刊精确取最新 Elsevier 论文 + DOI,后者取内部材料库全文),web search 取政策·标准·产业动向(**单列、不混进论文总结**)。某一路不可用时降级用其余来源,不整体放弃 6. **语言**:中文(默认)/ 英文 7. **特殊关注点**(可选):想重点呈现的材料体系 / 方法(仍只描述,不给建议) -## 阶段二:三路取数(research + documents 取文献 / web 取动向) +## 阶段二:三路取数(literature 两个来源取文献 / web 取动向) **先读 `references/journals.md`**。**中文方向先转专业英文术语**(库主语料英文):低碳水泥→low-carbon cement / clinker substitution;SCM→supplementary cementitious materials / fly ash / GGBFS / calcined clay;LC3→limestone calcined clay cement;碳化养护→CO2 curing / carbonation。缩写与全称都试。 -**research(逐刊取最新 Elsevier 论文 + DOI)** —— `run_python`: +**literature / paper_server(逐刊取最新 Elsevier 论文 + DOI)** —— `run_python`: ```python -from skills.research.paper import search +from skills.literature.paper import search # 逐刊拉最新:publication_name 精确匹配 + 时间窗;list 自带 abstract,看前 200-400 字判切题与分量 for jname in ["Cement and Concrete Research", "Cement and Concrete Composites", "Construction and Building Materials", "Journal of Cleaner Production"]: @@ -55,12 +55,12 @@ for jname in ["Cement and Concrete Research", "Cement and Concrete Composites", ``` 某刊精确名 0 命中 → 换 `keyword=<方向英文术语>` 再搜,从返回里挑 `publication_name` 命中目标刊的;仍空记"该刊本窗口库内无收录"。 -**documents(内部材料库取全文,材料类首选)** —— host-side tool `document_search`,中英 query 都行(后端跨语言语义检索);胶凝材料库 `classification_id=1`。取 `md_content` 既做候选也供引文核验抓锚点最顺。 +**literature / 内部材料库(取全文,材料类首选)** —— host-side tool `document_search`,中英 query 都行(后端跨语言语义检索);胶凝材料库 `classification_id=1`。取 `md_content` 既做候选也供引文核验抓锚点最顺。 **web search(取动向)** —— 政策(双碳/碳配额)、标准(新国标/团标)、行业会议、企业产线中试。**单列"其他动向",不混进论文列表与总结**。 -- 汇成证据表 `/evidence.md`:期刊 | 标题 | 第一作者(机构)| 年-月 | 摘要概述 | DOI | 来源(research/documents/web)。 -- 跨源去重:同 DOI 一条(documents 全文优先,DOI 记自 research);web 不与论文去重、单列。 +- 汇成证据表 `/evidence.md`:期刊 | 标题 | 第一作者(机构)| 年-月 | 摘要概述 | DOI | 来源(paper_server/materials_library/web)。 +- 跨源去重:按 `literature` 规则合并,同 DOI 一条(内部材料库全文优先作内容证据,paper_server 的 DOI 元数据优先);web 不与论文去重、单列。 > **context 纪律(省时省钱,务必遵守)**:检索结果(尤其全文 abstract)**落进 `evidence.md` / `selected_papers.json` 文件**,**不要在对话里反复 `run_python`/`print` 把整批 abstract 灌进上下文**。工具输出会永久留在 context 并每轮重发——同一批摘要 dump 三次,context 就滚成雪球(实测一次简报因此累计烧 2.5M 输入 token、跑满超时被掐断)。需要看某几篇时按需 `read` 文件片段,看完即弃,别整批重打。 @@ -89,9 +89,9 @@ for jname in ["Cement and Concrete Research", "Cement and Concrete Composites", ## 阶段四:引文核验(渲染前必跑) -论文直接来自 research/documents,DOI 以**库返回字段为准**(不沿用记忆、不编造)。逐条核验: +论文直接来自 `literature`,DOI 以**来源返回字段为准**(不沿用记忆、不编造)。逐条核验: -1. **存在性**:`search()`/`get_paper(doi)` 或 documents 命中确认真实存在;查不到 → 标 `[未核实]`,告诉用户"找不到来源,请提供 DOI 或删去",**不编造**。 +1. **存在性**:`search()`/`get_paper(doi)` 或内部材料库命中确认真实存在;查不到 → 标 `[未核实]`,告诉用户"找不到来源,请提供 DOI 或删去",**不编造**。 2. **支撑度**:摘要概述 / `[n]` 论断要和 abstract(或全文)一致;不一致 → **改概述迁就证据**,不是改证据。 3. **web**:记原始 URL + 访问日期 + 发布机构,标"截至 <日期>";不当学术结论引。 diff --git a/skills/documents/SKILL.md b/skills/documents/SKILL.md deleted file mode 100644 index f673ade..0000000 --- a/skills/documents/SKILL.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -name: documents -description: 检索内部 7 个材料学科知识库中的论文、书籍及其他学术文档。适合材料领域主题检索、全文语义检索、性能或工艺数据查找;可与 research(paper_server)并用并按 DOI 或题名去重。 ---- - -# Documents - -`documents` 是内部材料知识库的客户端 skill。知识库覆盖胶凝、陶瓷、玻璃、晶体、复合、耐火和检验检测,提供跨语言语义检索、Markdown 正文片段和原件下载。 - -## 选择与协作 - -- 材料领域主题、性能数据或全文语义检索:优先使用 `documents`。 -- 跨学科发现、精确 DOI 或题录检索:优先使用 `research`。 -- 系统调研、综述或重要引用:可同时查询两者;按 DOI 去重,无 DOI 时按规范化题名、作者和年份去重。 -- 用户已提供文件或个人知识库路径:直接读取该来源。 - -论文、书籍和书籍章节都可作为候选,来源类型本身不决定使用哪个 skill;以相关性、元数据完整度和可核验正文为准。 - -## 工具 - -本 skill 使用宿主工具,API Key 不进入 sandbox: - -- `document_list_kb()`:列出当前有效知识库。用户未指定材料方向且需要缩窄库范围时调用。 -- `document_search(queries, kb_names=None, classification_ids=None, max_documents=6, content_chars_per_doc=1200)`:批量搜索,返回文件元数据与截断的 `md_content`。中文和英文均可,专业术语可同时准备中英文表达。 -- `document_download(items)`:把选定原件下载到当前任务的 `documents/` 目录。`items` 使用搜索结果中的 `file_name` 和 `kb_name`。 - -工具会自动限制批量大小、去重并控制返回体积。需要更多正文时,对少量高相关候选增加 `content_chars_per_doc`,或下载原件后读取。 - -## 工作流 - -1. 根据用户问题形成少量有区分度的查询;需要分类时先列知识库。 -2. 搜索并依据题名、文件名、正文片段和库来源筛选候选。 -3. 相关性判断可使用片段;论点、数据、公式、表格、章节或页码核验应读取足够正文或原件。 -4. 与 `research` 并查时合并去重,优先保留正文更完整、元数据更可靠的记录。 -5. 输出时区分“仅题录”“片段已核对”“全文已核对”,并标注可追溯的文件名、DOI 或其他真实标识。 - -## 失败与真实性 - -- 工具不可用或认证失败时,说明 `document_search` 当前不可用,并改用 `research` 或用户提供的文件。 -- 未命中时可调整术语、语言或库范围;没有新增检索思路时如实说明覆盖不足。 -- 只使用工具返回的 `file_name`、`kb_name` 和来源信息,不推测缺失的作者、DOI、ISBN、页码或正文内容。 diff --git a/skills/literature/SKILL.md b/skills/literature/SKILL.md new file mode 100644 index 0000000..25dc10f --- /dev/null +++ b/skills/literature/SKILL.md @@ -0,0 +1,67 @@ +--- +name: literature +description: 检索、获取、合并与核验各类学术和技术出版物,包括期刊论文、书籍、书籍章节、会议论文、学位论文、技术报告、标准、专利、预印本及未来接入的其他出版物。适用于主题检索、精确题名/作者/DOI/ISBN/标准号查找、全文获取、跨来源去重、引文真实性和论断支撑度核验;按任务选择 paper_server、内部材料知识库及其他可用来源。不用于从零撰写论文、标准、专利或科研简报。 +--- + +# Literature + +把出版物视为统一检索对象,把 `paper_server`、内部材料知识库和未来来源视为可替换的数据后端。按用户问题选择来源、证据深度和输出形式,不按来源拆成多个 skill。 + +## 边界 + +- 本 skill 负责发现、精确查找、题录识别、原件获取、跨源去重和证据核验。 +- 论文、书籍、章节、会议论文、学位论文、报告、标准、专利和预印本都可作为候选;新类型保留来源原始类型,不强行归为论文。 +- 写论文或综述使用 `paper`,生成科研简报使用 `brief`,写标准使用 `standard`,写专利交底书使用 `patent`;这些 skill 可消费本 skill 的证据。 +- 用户已提供文件或个人知识库路径时,直接读取该来源;只有任务要求补充、核验或扩展出版物时再检索。 + +## 按需读取 + +- 任何任务先读 `references/publication-model.md`,统一类型、标识符、来源和证据字段。 +- 使用 `paper_server` 时读 `references/source-paper-server.md`。 +- 使用内部材料知识库时读 `references/source-materials-library.md`。 +- 合并两个以上来源或处理版本、章节、标准、专利时读 `references/deduplication.md`。 +- 核验引文、数据、公式、表格、页码或具体论断时读 `references/evidence-verification.md`。 + +## 来源选择 + +- 材料主题、性能、配方、工艺、表征或全文语义检索:优先内部材料知识库。 +- DOI、题名、作者、期刊、年份或跨学科发现:优先 `paper_server`。 +- 系统调研、综述、重要引用或关键论断:并查可用来源。 +- 用户指定来源:只查指定来源,除非该来源无法满足用户要求并需要说明降级方案。 +- 某一路不可用:继续使用其他来源,并明确实际覆盖范围。 + +出版物类型与来源是正交字段。例如一本书可来自内部材料库,一篇论文可同时来自两个后端;来源不同不等于出版物不同。 + +## 工作流 + +1. 识别任务是主题发现、精确查找、原件获取还是论断核验;提取类型、主题、时间、作者、载体和标识符约束。 +2. 形成少量有区分度的中英文查询。英文题名占多数的来源优先使用标准英文术语,同时保留缩写、全称和必要同义词。 +3. 按来源规则搜索并筛选候选。题名只用于初筛;摘要、正文片段或全文决定能否支持具体论断。 +4. 将多来源原始结果分别保存为 JSON,需要合并时运行: + + ```bash + python /scripts/merge_publications.py \ + --input paper_server=raw-paper-server.json \ + --input materials_library=raw-materials-library.json \ + --output publications.json + ``` + +5. 只对精确标识或完全一致的题名、主要责任者和年份自动合并;疑似重复保留并标记。不同版次、章节、标准修订版和专利族成员不得直接覆盖。 +6. 根据问题所需深度读取摘要、片段或全文。下载原件但没有实际读取,不得写成“全文已核对”。 +7. 输出真实题名、责任者、年份、载体、标识符、来源和证据等级,并列出未核实字段、来源冲突和可能重复项。 + +## 证据等级 + +- `metadata_only`:仅题录或标识符已核对。 +- `abstract_verified`:摘要已读取并核对。 +- `snippet_verified`:相关正文片段已读取并核对。 +- `fulltext_verified`:全文中的相关部分已读取并核对。 + +出版物真实存在不代表它支持当前论断。支撑不足时缩窄或删除论断,不修改证据迁就论断。 + +## 失败与真实性 + +- 认证、网络或来源不可用时,指出具体来源和失败类型,不把单源失败写成全部文献不可用。 +- 未命中时可调整术语、语言、类型或过滤条件;没有新的检索思路时如实说明覆盖不足。 +- 只使用来源实际返回的题名、责任者、标识符、版本、页码和正文,不推测缺失字段。 +- 不把搜索摘要当作者摘要,不把目录命中当章节正文,不把下载成功当内容已经核验。 diff --git a/skills/literature/agents/openai.yaml b/skills/literature/agents/openai.yaml new file mode 100644 index 0000000..2562290 --- /dev/null +++ b/skills/literature/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "文献与出版物" + short_description: "检索、获取、合并、去重并系统核验各类学术与技术出版物" + default_prompt: "Use $literature to find and verify publications for this research question." diff --git a/skills/research/paper.py b/skills/literature/paper.py similarity index 99% rename from skills/research/paper.py rename to skills/literature/paper.py index 48bd812..0b79d5f 100644 --- a/skills/research/paper.py +++ b/skills/literature/paper.py @@ -1,4 +1,4 @@ -"""paper_server 客户端 helper。base_url 默认硬编码,env 可覆盖。""" +"""literature skill 的 paper_server 客户端 helper。""" from __future__ import annotations import os diff --git a/skills/literature/references/deduplication.md b/skills/literature/references/deduplication.md new file mode 100644 index 0000000..a8eddd8 --- /dev/null +++ b/skills/literature/references/deduplication.md @@ -0,0 +1,30 @@ +# 出版物去重 + +自动合并必须保守。先规范化标识符,再按出版物类型选择键;不确定时保留两条并标记可能重复。 + +## 精确键 + +- 论文、预印本、章节:规范化 DOI。 +- 书籍:规范化 ISBN 与版次;不同版次不合并。 +- 会议论文:DOI,或完全一致的题名、主要责任者、会议和年份。 +- 学位论文:完全一致的题名、作者、授予机构和年份。 +- 标准:发布机构、标准号和年份或版本。 +- 专利:公开号;申请号和专利族只用于关联。 +- 技术报告:发布机构和报告号。 + +DOI 去除 `doi:`、`https://doi.org/`、空白和末尾标点,并转为小写。ISBN 去除分隔符并统一 ISBN-10/13 表示。 + +## 无稳定标识符 + +只有规范化题名、主要责任者和年份完全一致时自动合并。题名高度相似但字段不全或存在冲突时,只加入 `possible_duplicates`。 + +章节不得与整书合并;不同版本、译本、标准修订版和专利族成员不得因题名相似自动覆盖。 + +## 合并优先级 + +- 保留所有来源的 `backend` 和 `source_id`。 +- 正式标识元数据优先于文件名推断。 +- 正文更完整的来源用于内容证据,但不因此覆盖另一来源更可靠的题录。 +- 非空字段冲突时保留首条值并记录 `conflicts`,不静默改写。 + +使用 `scripts/merge_publications.py` 做机械规范化和精确合并。学术相关性、版本关系和论断支撑度仍由模型基于原始记录判断。 diff --git a/skills/literature/references/evidence-verification.md b/skills/literature/references/evidence-verification.md new file mode 100644 index 0000000..f7e37af --- /dev/null +++ b/skills/literature/references/evidence-verification.md @@ -0,0 +1,29 @@ +# 出版物证据核验 + +把“出版物真实存在”和“出版物支持当前论断”分开核验。 + +## 四级证据 + +- `metadata_only`:只核对题录或标识符。 +- `abstract_verified`:已读取摘要并核对。 +- `snippet_verified`:已读取相关正文片段并核对。 +- `fulltext_verified`:已读取全文中的相关部分并核对。 + +文件已经下载但未读取时仍不能标记 `fulltext_verified`。搜索引擎生成的摘要不是作者摘要,目录或题名命中不是正文证据。 + +## 三步核验 + +1. **存在性**:从可用来源取得真实题名、责任者、年份、载体和标识符;查不到则标记未核实。 +2. **交叉印证**:关键论断尽量由两个独立来源确认题录;字段冲突以可核验的正式标识元数据为准,并保留冲突记录。 +3. **支撑度**:定位与论断相关的摘要或正文锚点,判定为 `support`、`partial` 或 `not_support`。 + +`partial` 时缩窄论断使其与证据一致;`not_support` 时删除引用或更换出版物。无法取得正文时可以用摘要做弱核验,但必须明确等级。 + +## 特殊出版物 + +- 书籍:核对具体版次和章节,不能用整书题录替代章节证据。 +- 标准:核对发布机构、标准号、年份和有效版本,具体要求需定位条款。 +- 专利:核对公开号、申请人和公开日期;权利要求、实施例和法律状态不能互相替代。 +- 报告与学位论文:核对发布或授予机构,避免把二次转载当原始来源。 + +输出时为每条关键证据保留来源标识、核验层级和必要的位置说明。不得为补齐引用而推测出版信息。 diff --git a/skills/literature/references/publication-model.md b/skills/literature/references/publication-model.md new file mode 100644 index 0000000..abfafa9 --- /dev/null +++ b/skills/literature/references/publication-model.md @@ -0,0 +1,68 @@ +# 统一出版物模型 + +把“出版物是什么”和“从哪里取得”分开记录。保留来源原始字段;统一字段只用于跨源整理,不替代原始记录。 + +## 类型 + +使用以下开放枚举: + +- `article`:期刊论文 +- `book`:书籍、专著 +- `book_chapter`:书籍章节 +- `conference_paper`:会议论文 +- `proceedings`:会议论文集 +- `thesis`:学位论文 +- `report`:技术或研究报告 +- `standard`:标准 +- `patent`:专利文献 +- `preprint`:预印本 +- `dataset_publication`:数据集出版记录 +- `other`:尚未归类的出版物 + +同时保留 `raw_type`。无法可靠映射时使用 `other`,不凭题名猜测。 + +## 统一字段 + +```yaml +type: article +raw_type: journal-article +title: "" +creators: [] +issued: {year: null, month: null, day: null} +container: {title: "", type: "", volume: "", issue: "", pages: ""} +publisher: {name: "", place: ""} +edition: {label: "", number: null} +identifiers: + doi: "" + isbn: [] + issn: [] + standard_number: "" + patent_application_number: "" + patent_publication_number: "" + report_number: "" +relations: + is_part_of: "" + has_parts: [] + edition_of: "" + translation_of: "" + patent_family: "" +language: "" +abstract: "" +access: {has_abstract: false, has_snippet: false, has_fulltext: false, formats: [], local_paths: []} +evidence_level: metadata_only +sources: [] +``` + +`creators` 可包含 `name`、`role` 和来源提供的 ORCID 等标识。`container` 表示论文所属期刊、章节所属书籍或会议论文所属论文集。 + +## 层级与版本 + +- 章节与整书是不同记录,通过 `container` 或来源关系关联。 +- 不同版次是不同记录;电子版、精装和平装是否属于同一版次,以版次和来源元数据判断。 +- 预印本与期刊版、会议版与扩展期刊版可建立关系,但不自动互相覆盖。 +- 标准修订版、修改单和旧版分别保留。 +- 专利族表示关联,不表示各公开号是重复记录。 + +## 溯源 + +每个 `sources` 条目至少保留 `backend`、`source_id` 和检索时间。原始批量结果单独落盘;统一记录不重复嵌入大段摘要或全文,避免文件和上下文膨胀。 diff --git a/skills/literature/references/source-materials-library.md b/skills/literature/references/source-materials-library.md new file mode 100644 index 0000000..990986a --- /dev/null +++ b/skills/literature/references/source-materials-library.md @@ -0,0 +1,24 @@ +# 内部材料知识库来源 + +内部知识库覆盖胶凝、陶瓷、玻璃、晶体、复合、耐火和检验检测,提供跨语言语义检索、Markdown 正文片段和原件下载。API Key 留在宿主工具中,不进入 sandbox。 + +## 工具 + +- `document_list_kb()`:列出当前有效知识库。用户未指定材料方向且需要缩窄范围时调用。 +- `document_search(queries, kb_names=None, classification_ids=None, max_documents=6, content_chars_per_doc=1200)`:批量检索并返回元数据和截断的 `md_content`。 +- `document_download(items)`:把选定原件下载到当前任务的 `documents/` 目录。 + +查询可使用中文或英文;复杂专业术语通常使用英文更精确。先规划互不重复的查询并批量调用,不逐个近义词反复搜索。 + +## 使用深度 + +- 题名、文件名、正文片段和知识库来源用于候选筛选。 +- 片段足以核验局部论断时无需下载原件。 +- 数据、公式、表格、章节、页码或版式需要更多上下文时,提高少量候选的片段长度或下载原件。 +- 下载只使用搜索返回的 `file_name` 和 `kb_name`,不编造来源标识。 + +## 失败 + +- 工具不存在表示宿主未配置 `DOCUMENT_SEARCH_API_KEY`;继续使用其他来源。 +- 认证、网络或知识库错误时说明内部材料库当前不可用。 +- 未命中时调整术语、语言或库范围;没有新增思路时如实说明覆盖不足。 diff --git a/skills/literature/references/source-paper-server.md b/skills/literature/references/source-paper-server.md new file mode 100644 index 0000000..31d3113 --- /dev/null +++ b/skills/literature/references/source-paper-server.md @@ -0,0 +1,30 @@ +# paper_server 来源 + +`paper_server` 以 OpenAlex 元数据为基础,并按记录实际可用性提供摘要、PDF 或 XML。当前语料以英文为主,也包含中文记录。 + +## 调用 + +通过 `run_python` 使用 helper: + +```python +from skills.literature.paper import search, get_paper, fetch_pdf, fetch_xml +``` + +- `search(keyword="", year=None, year_gte=None, year_lte=None, doi="", first_author="", publication_name="", has_pdf=None, is_oa=None, limit=10)`:搜索题录并返回摘要和全文可用状态。 +- `get_paper(id_or_doi)`:按内部 ID 或 DOI 获取完整记录。 +- `fetch_xml(id_or_doi, working_dir)` / `fetch_pdf(id_or_doi, working_dir)`:把可用全文保存到任务的 `papers/` 目录。 + +helper 自动使用 `PAPER_SERVER_API_KEY` 和可选的 `PAPER_SERVER_URL`。只通过 helper 访问,由它处理认证、URL 和下载路径。 + +## 查询与全文 + +- 将中文概念转换为常用专业英文术语,并按需补中文、缩写、全称或同义词。 +- 用 DOI、题名、作者、出版物、年份和来源返回的 `type` 筛选候选。 +- 相关性初筛使用题名和摘要;精确数据、章节、图表或页码必须读取正文。 +- 结构化段落和参考文献优先 XML;版式、页码、公式和图表优先 PDF。只获取记录实际提供的格式。 + +## 失败 + +- 认证失败需要管理员检查 `PAPER_SERVER_API_KEY`。 +- DOI 未命中、格式缺失或服务器文件不存在时,改查其他候选或使用现有摘要,并降低证据等级。 +- 只引用返回记录中的真实字段,不补写缺失的 ISBN、出版社、版本或页码。 diff --git a/skills/literature/scripts/merge_publications.py b/skills/literature/scripts/merge_publications.py new file mode 100644 index 0000000..8975877 --- /dev/null +++ b/skills/literature/scripts/merge_publications.py @@ -0,0 +1,390 @@ +"""Normalize and conservatively merge publication records from multiple backends.""" +from __future__ import annotations + +import argparse +import json +import re +import unicodedata +from difflib import SequenceMatcher +from pathlib import Path +from typing import Any, Iterable + + +EVIDENCE_LEVELS = ( + "metadata_only", + "abstract_verified", + "snippet_verified", + "fulltext_verified", +) + +TYPE_ALIASES = { + "article": "article", + "journal-article": "article", + "journal_article": "article", + "book": "book", + "book-chapter": "book_chapter", + "book_chapter": "book_chapter", + "chapter": "book_chapter", + "conference-paper": "conference_paper", + "conference_paper": "conference_paper", + "proceedings-article": "conference_paper", + "proceedings": "proceedings", + "thesis": "thesis", + "dissertation": "thesis", + "report": "report", + "standard": "standard", + "patent": "patent", + "preprint": "preprint", + "dataset": "dataset_publication", + "dataset_publication": "dataset_publication", +} + + +def normalize_doi(value: Any) -> str: + doi = str(value or "").strip().lower() + doi = re.sub(r"^(?:doi\s*:\s*|https?://(?:dx\.)?doi\.org/)", "", doi) + return doi.rstrip(".,;:)]}") + + +def _isbn13_from_10(value: str) -> str: + core = "978" + value[:9] + total = sum((1 if i % 2 == 0 else 3) * int(ch) for i, ch in enumerate(core)) + return core + str((10 - total % 10) % 10) + + +def normalize_isbn(value: Any) -> str: + isbn = re.sub(r"[^0-9Xx]", "", str(value or "")).upper() + if len(isbn) == 10 and isbn[:9].isdigit() and (isbn[-1].isdigit() or isbn[-1] == "X"): + return _isbn13_from_10(isbn) + return isbn if len(isbn) == 13 and isbn.isdigit() else "" + + +def normalize_title(value: Any) -> str: + title = unicodedata.normalize("NFKC", str(value or "")).casefold() + title = re.sub(r"[^\w]+", " ", title, flags=re.UNICODE) + return " ".join(title.split()) + + +def normalize_creator(value: Any) -> str: + if isinstance(value, dict): + value = value.get("name") or value.get("literal") or "" + return normalize_title(value) + + +def normalize_type(value: Any) -> tuple[str, str]: + raw = str(value or "").strip() + key = raw.casefold().replace(" ", "-") + return TYPE_ALIASES.get(key, "other"), raw + + +def _first(record: dict, *keys: str) -> Any: + for key in keys: + value = record.get(key) + if value not in (None, "", [], {}): + return value + return "" + + +def _creator_list(record: dict) -> list[dict[str, str]]: + raw = _first(record, "creators", "authors") + if not raw: + raw = [_first(record, "first_author", "author", "creator")] + elif not isinstance(raw, list): + raw = [raw] + creators: list[dict[str, str]] = [] + for item in raw: + if isinstance(item, dict): + name = str(item.get("name") or item.get("literal") or "").strip() + role = str(item.get("role") or "author").strip() + orcid = str(item.get("orcid") or "").strip() + else: + name, role, orcid = str(item or "").strip(), "author", "" + if name: + creators.append({"name": name, "role": role, "orcid": orcid}) + return creators + + +def _issued(record: dict) -> dict[str, int | None]: + issued = record.get("issued") + current = dict(issued) if isinstance(issued, dict) else {} + date_parts = str(record.get("publication_date") or "").split("-") + value = current.get("year") or _first(record, "publication_year", "year") + try: + year = int(value) if value not in (None, "") else int(date_parts[0]) if date_parts and date_parts[0] else None + except (TypeError, ValueError): + year = None + parts: dict[str, int | None] = {"year": year, "month": None, "day": None} + for index, key in ((1, "month"), (2, "day")): + candidate = current.get(key) + if candidate in (None, "") and len(date_parts) > index: + candidate = date_parts[index] + try: + parts[key] = int(candidate) if candidate not in (None, "") else None + except (TypeError, ValueError): + parts[key] = None + return parts + + +def _string_list(value: Any, normalizer=None) -> list[str]: + values = value if isinstance(value, list) else [value] + out: list[str] = [] + for item in values: + normalized = normalizer(item) if normalizer else str(item or "").strip() + if normalized and normalized not in out: + out.append(normalized) + return out + + +def normalize_record(record: dict[str, Any], backend: str) -> dict[str, Any]: + identifiers = dict(record.get("identifiers") or {}) + doi = normalize_doi(_first(identifiers, "doi") or record.get("doi")) + isbn = _string_list(_first(identifiers, "isbn") or record.get("isbn"), normalize_isbn) + issn = _string_list(_first(identifiers, "issn") or record.get("issn")) + publication_type, raw_type = normalize_type(_first(record, "type", "publication_type")) + title = str(_first(record, "title", "file_name") or "").strip() + creators = _creator_list(record) + abstract = str(record.get("abstract") or "").strip() + snippet = str(_first(record, "snippet", "md_content") or "").strip() + formats = _string_list((record.get("access") or {}).get("formats") if isinstance(record.get("access"), dict) else []) + if record.get("has_fulltext_pdf") and "pdf" not in formats: + formats.append("pdf") + if record.get("has_fulltext_xml") and "xml" not in formats: + formats.append("xml") + evidence = str(record.get("evidence_level") or "metadata_only") + if evidence not in EVIDENCE_LEVELS: + evidence = "metadata_only" + source_id = str(_first(record, "id", "source_id", "file_name") or "") + container = dict(record.get("container") or {}) + container.setdefault("title", str(_first(record, "publication_name", "container_title") or "")) + container.setdefault("type", "") + container.setdefault("volume", str(record.get("volume") or "")) + container.setdefault("issue", str(record.get("issue") or "")) + container.setdefault("pages", str(_first(record, "pages", "page") or "")) + raw_publisher = record.get("publisher") + if isinstance(raw_publisher, str): + publisher = {"name": raw_publisher, "place": ""} + else: + publisher = dict(raw_publisher or {}) + publisher.setdefault("name", str(record.get("publisher_name") or "")) + publisher.setdefault("place", str(record.get("publisher_place") or "")) + raw_edition = record.get("edition") + if isinstance(raw_edition, str): + edition = {"label": raw_edition, "number": None} + else: + edition = dict(raw_edition or {}) + edition.setdefault("label", str(record.get("edition_label") or "")) + edition.setdefault("number", record.get("edition_number")) + raw_relations = record.get("relations") + relations = dict(raw_relations) if isinstance(raw_relations, dict) else {} + relations.setdefault("is_part_of", str(record.get("is_part_of") or "")) + relations["has_parts"] = _string_list(relations.get("has_parts") or record.get("has_parts") or []) + relations.setdefault("edition_of", str(record.get("edition_of") or "")) + relations.setdefault("translation_of", str(record.get("translation_of") or "")) + relations.setdefault("patent_family", str(record.get("patent_family") or "")) + return { + "type": publication_type, + "raw_type": raw_type, + "title": title, + "creators": creators, + "issued": _issued(record), + "container": container, + "publisher": publisher, + "edition": edition, + "identifiers": { + "doi": doi, + "isbn": isbn, + "issn": issn, + "standard_number": str(_first(identifiers, "standard_number") or record.get("standard_number") or "").strip(), + "patent_application_number": str(_first(identifiers, "patent_application_number") or record.get("patent_application_number") or "").strip(), + "patent_publication_number": str(_first(identifiers, "patent_publication_number") or record.get("patent_publication_number") or "").strip(), + "report_number": str(_first(identifiers, "report_number") or record.get("report_number") or "").strip(), + }, + "relations": relations, + "language": str(record.get("language") or "").strip(), + "abstract": abstract, + "access": { + "has_abstract": bool(abstract or record.get("has_abstract")), + "has_snippet": bool(snippet), + "has_fulltext": bool(formats or record.get("has_fulltext")), + "formats": formats, + "local_paths": _string_list((record.get("access") or {}).get("local_paths") if isinstance(record.get("access"), dict) else record.get("local_paths") or []), + }, + "evidence_level": evidence, + "sources": [{ + "backend": backend, + "source_id": source_id, + "retrieved_at": str(record.get("retrieved_at") or ""), + }], + "conflicts": {}, + } + + +def _edition_key(record: dict[str, Any]) -> str: + edition = record.get("edition") or {} + return normalize_title(edition.get("number") or edition.get("label")) + + +def exact_key(record: dict[str, Any]) -> str: + ids = record["identifiers"] + if ids.get("doi"): + return "doi:" + ids["doi"] + year = record["issued"].get("year") or "" + publisher = normalize_title(record["publisher"].get("name")) + if record["type"] == "standard" and ids.get("standard_number"): + return f"standard:{publisher}:{normalize_title(ids['standard_number'])}:{year}" + if record["type"] == "patent" and ids.get("patent_publication_number"): + return "patent:" + normalize_title(ids["patent_publication_number"]) + if record["type"] == "report" and ids.get("report_number"): + return f"report:{publisher}:{normalize_title(ids['report_number'])}" + if record["type"] == "book" and ids.get("isbn"): + return f"book:{ids['isbn'][0]}:{_edition_key(record)}" + title = normalize_title(record.get("title")) + creators = record.get("creators") or [] + creator = normalize_creator(creators[0]) if creators else "" + if title and creator and year: + extra = _edition_key(record) if record["type"] == "book" else "" + if record["type"] == "book_chapter": + extra = normalize_title(record["container"].get("title")) + return f"fallback:{record['type']}:{title}:{creator}:{year}:{extra}" + return "" + + +def _add_conflict(target: dict[str, Any], field: str, value: Any) -> None: + if value in (None, "", [], {}): + return + values = target["conflicts"].setdefault(field, []) + if value not in values: + values.append(value) + + +def merge_record(target: dict[str, Any], incoming: dict[str, Any]) -> None: + for source in incoming["sources"]: + if source not in target["sources"]: + target["sources"].append(source) + if EVIDENCE_LEVELS.index(incoming["evidence_level"]) > EVIDENCE_LEVELS.index(target["evidence_level"]): + target["evidence_level"] = incoming["evidence_level"] + if not target["abstract"] and incoming["abstract"]: + target["abstract"] = incoming["abstract"] + elif target["abstract"] and incoming["abstract"] and target["abstract"] != incoming["abstract"]: + _add_conflict(target, "abstract", incoming["abstract"]) + for flag in ("has_abstract", "has_snippet", "has_fulltext"): + target["access"][flag] = target["access"][flag] or incoming["access"][flag] + for field in ("formats", "local_paths"): + for value in incoming["access"][field]: + if value not in target["access"][field]: + target["access"][field].append(value) + for field in ("doi", "standard_number", "patent_application_number", "patent_publication_number", "report_number"): + current, value = target["identifiers"].get(field), incoming["identifiers"].get(field) + if not current and value: + target["identifiers"][field] = value + elif current and value and current != value: + _add_conflict(target, f"identifiers.{field}", value) + for field in ("isbn", "issn"): + for value in incoming["identifiers"].get(field, []): + if value not in target["identifiers"][field]: + target["identifiers"][field].append(value) + for field in ("title", "raw_type", "language"): + current, value = target.get(field), incoming.get(field) + if not current and value: + target[field] = value + elif current and value and normalize_title(current) != normalize_title(value): + _add_conflict(target, field, value) + for field in ("container", "publisher", "edition"): + for key, value in incoming[field].items(): + current = target[field].get(key) + if not current and value not in (None, ""): + target[field][key] = value + elif current and value not in (None, "") and current != value: + _add_conflict(target, f"{field}.{key}", value) + for key, value in incoming["relations"].items(): + current = target["relations"].get(key) + if isinstance(value, list): + if not isinstance(current, list): + current = [] + target["relations"][key] = current + for item in value: + if item not in current: + current.append(item) + elif not current and value: + target["relations"][key] = value + elif current and value and current != value: + _add_conflict(target, f"relations.{key}", value) + + +def merge_publications(records: Iterable[tuple[str, dict[str, Any]]]) -> dict[str, Any]: + publications: list[dict[str, Any]] = [] + key_to_index: dict[str, int] = {} + for backend, raw in records: + normalized = normalize_record(raw, backend) + key = exact_key(normalized) + if key and key in key_to_index: + merge_record(publications[key_to_index[key]], normalized) + else: + if key: + key_to_index[key] = len(publications) + publications.append(normalized) + + possible: list[dict[str, Any]] = [] + for left_index, left in enumerate(publications): + left_title = normalize_title(left["title"]) + if not left_title: + continue + for right_index in range(left_index + 1, len(publications)): + right = publications[right_index] + if left["type"] != right["type"]: + continue + left_year, right_year = left["issued"]["year"], right["issued"]["year"] + if left_year and right_year and abs(left_year - right_year) > 1: + continue + ratio = SequenceMatcher(None, left_title, normalize_title(right["title"])).ratio() + if ratio >= 0.93: + possible.append({ + "left_index": left_index, + "right_index": right_index, + "left_title": left["title"], + "right_title": right["title"], + "title_similarity": round(ratio, 4), + }) + return {"publications": publications, "possible_duplicates": possible} + + +def _load_records(path: Path) -> list[dict[str, Any]]: + data = json.loads(path.read_text(encoding="utf-8")) + if isinstance(data, list): + return data + if not isinstance(data, dict): + raise ValueError(f"{path}: JSON 顶层必须是列表或对象") + for key in ("records", "publications", "results"): + if isinstance(data.get(key), list): + return data[key] + nested = data.get("data") + if isinstance(nested, list): + return nested + raise ValueError(f"{path}: 未找到 records/publications/results 列表") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--input", action="append", required=True, metavar="BACKEND=PATH") + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args(argv) + records: list[tuple[str, dict[str, Any]]] = [] + for spec in args.input: + if "=" not in spec: + parser.error("--input 必须使用 BACKEND=PATH") + backend, raw_path = spec.split("=", 1) + backend = backend.strip() + if not backend: + parser.error("--input 的 BACKEND 不可为空") + records.extend((backend, item) for item in _load_records(Path(raw_path))) + result = merge_publications(records) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + print(f"[OK] merged {len(records)} source records into {len(result['publications'])} publications") + if result["possible_duplicates"]: + print(f"[WARN] possible duplicates: {len(result['possible_duplicates'])}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/paper/SKILL.md b/skills/paper/SKILL.md index 5fc127d..8d2976a 100644 --- a/skills/paper/SKILL.md +++ b/skills/paper/SKILL.md @@ -15,10 +15,10 @@ description: 撰写学术期刊投稿论文(中文核心 / 英文 SCI;原创研 |---|---| | vs `proposal` | proposal 写**本子/任务书**(立项依据骨架);paper 写**期刊投稿稿**(IMRaD 骨架)。两者各自独立 | | vs `review` | review 改**已有稿**;paper **从零起草**。paper 阶段六终审**调用** review 的协议,不重复造 | -| vs `research`/`documents` | 它们查文献;paper 是消费方,引文核验(阶段五)接到它们头上 | +| vs `literature` | 它查出版物并核验证据;paper 是消费方,引文核验(阶段五)接到它头上 | | vs `patent`/`standard` | 写交底书→patent;写标准→standard | -**何时不用**:只改不写→review;写本子→proposal;只查文献→research/documents;只出图→plot_pub。 +**何时不用**:只改不写→review;写本子→proposal;只查出版物→literature;只出图→plot_pub。 ## 资源 @@ -35,7 +35,7 @@ description: 撰写学术期刊投稿论文(中文核心 / 英文 SCI;原创研 - `references/figure_discipline.md` —— 一图一论断 / panel 角色 / 主图 vs 补充材料 / 图例规则(plot_pub 管画好一张图,这份管图和论文的关系) **阶段五必读**: -- `references/citation_verify.md` —— 引文三角核验协议(存在性 / 三角印证 / 支撑度,接 documents/research) +- `references/citation_verify.md` —— 引文三角核验协议(存在性 / 三角印证 / 支撑度,接 literature) **阶段六必读**: - `references/submission_audit.md` —— 投稿前审计六步(前半对齐 / 图例同步 / 术语漂移 / 机理语言)+ 数据可用性声明模式 @@ -93,8 +93,8 @@ spec 定下「类型 + 语言」后,**按 §资源 条件加载**对应的 cite_ > 移植自 ARS,后端用 zcbot 自己的库。Introduction 与 Discussion 靠这份矩阵,不靠记忆。 -1. 据 spec §3/§4 的贡献与 gap,列要查的主题(英文 keyword 优先,见 research/documents 规则) -2. 用 `documents`(材料类优先,中英 query 都行)/ `research`(要 DOI 走这个)检索,建矩阵到 `/lit_matrix.md`: +1. 据 spec §3/§4 的贡献与 gap,列要查的主题(英文 keyword 优先,见 literature 规则) +2. 用 `literature` 检索(材料类全文语义优先内部材料库,精确 DOI 优先 paper_server),建矩阵到 `/lit_matrix.md`: | 文献(真实条目) | DOI | 一句话贡献 | 在本文用在哪(Intro/Methods/Disc) | |---|---|---|---| @@ -143,7 +143,7 @@ spec 定下「类型 + 语言」后,**按 §资源 条件加载**对应的 cite_ > 论文最致命的失分是编造引文 / 引而不实。**逐条**走 `references/citation_verify.md` 三层: -1. **存在性**:每条引文在 documents/research 查到真实条目,字段以库返回为准;查不到标 `[未核实]`,**不编造** +1. **存在性**:每条引文在 literature 查到真实条目,字段以来源返回为准;查不到标 `[未核实]`,**不编造** 2. **三角印证**:关键论断的支撑引文至少两个独立来源一致 3. **支撑度**:抓回 md_content/PDF,定位 ≤25 词锚点原文,判 support/partial/not-support;partial→**改论断迁就证据**,not-support→删或换 4. 台账写 `/CITATIONS.md`;只有 verified 的进编号 diff --git a/skills/paper/references/citation_verify.md b/skills/paper/references/citation_verify.md index 32b7778..b270d36 100644 --- a/skills/paper/references/citation_verify.md +++ b/skills/paper/references/citation_verify.md @@ -2,7 +2,7 @@ 论文最致命的失分是**编造引文**(hallucinated citation)与**引而不实**(cite 的文献不支撑该论断)。 本协议把每条引文从"看起来对"逼到"经得起查"。移植自 ARS 的 triangulation + claim-faithfulness 思路, -**后端换成 zcbot 自己的 `documents` / `research` 库**(它们本就带 DOI + md_content,做 anchor 比对反而更顺)。 +**检索入口换成 zcbot 的 `literature` skill**(paper_server 与内部材料库可交叉提供 DOI、摘要和正文锚点)。 > 这是**协议**不是脚本 —— 你(模型)拿 host-side tool 逐条执行。quality_check.py 只做机械的 > orphan/uncited/编号核对,真伪与支撑度靠本协议。 @@ -18,7 +18,7 @@ 每条引文先确认"这篇文献真实存在": -1. `documents` 库语义检索(材料类优先,中英 query 都行)/ `research` 库 `search()` / `get_paper(doi)` +1. 用 `literature` 检索:材料类优先内部材料库语义检索,精确 DOI 使用 paper_server 的 `search()` / `get_paper(doi)` 2. 命中 → 记下真实 DOI / 作者 / 年份 / 期刊 / 卷期页;**以库里返回为准**,不沿用记忆里的字段 3. 两个库都查不到 → 标 `[未核实]`,**不得编造条目**;告诉用户"这条找不到来源,请提供 PDF/DOI 或删去该论断" @@ -26,7 +26,7 @@ 关键论断(创新点对比、机理依据、定量结论)的支撑引文,**至少两个独立信息源一致**才算稳: -- documents 命中 + research/DOI 一致 → 通过 +- 内部材料库命中 + paper_server/DOI 一致 → 通过 - 仅单一来源 → 标"单源,谨慎",提示用户复核 - 不同来源字段冲突(年份/卷期不一致)→ 以可验证的 DOI 元数据为准,修正条目 @@ -34,7 +34,7 @@ 最容易翻车的一层:文献存在,但**并不支撑你写的那句话**。逐条做: -1. 抓回该文献的 `md_content`(documents 直接给整篇 Markdown)/ `fetch_xml` / `fetch_pdf`(research) +1. 抓回该文献的 `md_content`(内部材料库正文片段)/ `fetch_xml` / `fetch_pdf`(paper_server) 2. 在原文里定位与论断相关的**锚点证据**:一句 ≤25 词的原文引语 + 出现的段落/小节位置 3. 判定支撑度三档: - **support**:原文明确支撑该论断 → 通过 @@ -50,7 +50,7 @@ # 引文核验台账 > 每条引文的存在性/三角/支撑度核验结果。渲染前所有条目应为 verified 或经用户确认。 -- [1] Provis & Bernal 2014, Annu. Rev. Mater. Res. 44:299 | exists:✓(documents+DOI) | triangulate:✓ | claim:support "geopolymers form via dissolution-polymerisation"(§2.1) | status: verified +- [1] Provis & Bernal 2014, Annu. Rev. Mater. Res. 44:299 | exists:✓(materials_library+DOI) | triangulate:✓ | claim:support "geopolymers form via dissolution-polymerisation"(§2.1) | status: verified - [2] ... | exists:✓ | claim:partial → 已把"显著提高"改为"在 28d 提高约 12%" | status: verified-revised - [3] ... | exists:✗ 两库未命中 | status: 待用户提供来源 ``` diff --git a/skills/paper/references/cite_elsevier.md b/skills/paper/references/cite_elsevier.md index 400f09c..3c62e79 100644 --- a/skills/paper/references/cite_elsevier.md +++ b/skills/paper/references/cite_elsevier.md @@ -11,7 +11,7 @@ ## 真实性铁律 - ❌ 不可编造 authors / year / journal / volume / pages / DOI -- ✅ 引文必须经 `citation_verify.md` 核验(优先 documents / research 库) +- ✅ 引文必须经 `citation_verify.md` 核验(走 literature 的可用来源) - ✅ 用户给 BibTeX / RIS 你只排版 ## 文中标注(numbered) diff --git a/skills/paper/references/cite_gbt7714.md b/skills/paper/references/cite_gbt7714.md index 246d4b6..24f7dda 100644 --- a/skills/paper/references/cite_gbt7714.md +++ b/skills/paper/references/cite_gbt7714.md @@ -8,7 +8,7 @@ - ❌ **不可编造**作者 / 年份 / 期刊 / 卷期 / 页码 / DOI - ❌ **不可凭印象**写"某某 2020 提到过…" —— 大概率错 -- ✅ 引文必须经 `citation_verify.md` 的核验流程拿到真实条目(优先查 documents / research 库) +- ✅ 引文必须经 `citation_verify.md` 的核验流程拿到真实条目(使用 literature) - ✅ 用户给 BibTeX / EndNote / 纯文本均可,你只**排版**,不补全凭空内容 ## 文中标注 @@ -42,7 +42,7 @@ Provis 等 [2] 提出了地聚物的纳米结构演化模型。 ## 写作流程(与 citation_verify 配合) 1. 起草正文时,引用处先放占位符 `[CITE-<关键词>]`(如 `[CITE-geopolymer-strength]`) -2. 草稿成形后,走 `citation_verify.md`:对每个占位逐条查 documents / research 拿真实文献 +2. 草稿成形后,走 `citation_verify.md`:对每个占位逐条用 literature 拿真实文献 3. 核验通过的文献按**文中首次出现顺序**编号,占位符替换成 `[1][2][3]...` 4. 按 GB/T 7714 重排 `06_references.md` diff --git a/skills/patent/SKILL.md b/skills/patent/SKILL.md index ab1e2c8..877dc4c 100644 --- a/skills/patent/SKILL.md +++ b/skills/patent/SKILL.md @@ -59,8 +59,8 @@ markitdown https://example.com/ -o /source/外部.md 3. **执行检索**(优先级从高到低): - **`web_search`** —— 优先搜中国专利文库 / Google Patents / 期刊综述。query 里带 `site:patents.google.com` / `专利` / `CN10` 等限定符可显著提升信号 - **`web_fetch`** —— 命中的关键专利/论文页拉全文摘要 - - **`documents` skill** —— 本地材料学科库(7 个学科共 100W+ 论文)如果命中 - - **`research` skill** —— OpenAlex 学术文献库,找综述与对比方案 + - **`literature` 的内部材料库来源** —— 本地 7 个材料学科库,如果命中则读取正文证据 + - **`literature` 的 paper_server 来源** —— OpenAlex 元数据基础的出版物库,找综述与对比方案 4. **每条命中归档** (写到 spec §4): - 公开号 / 标题 / 申请人 / 公开日 (专利) 或 DOI / 作者 / 期刊 / 年 (论文) - 一句话概括其技术方案 diff --git a/skills/patent/references/prior_art_search.md b/skills/patent/references/prior_art_search.md index 2c2bfff..0b4dd41 100644 --- a/skills/patent/references/prior_art_search.md +++ b/skills/patent/references/prior_art_search.md @@ -51,9 +51,9 @@ A ∩ (B | C) # 核心 + (对象 | 效果) — 较宽 |---|---|---|---| | 1 | **中国专利公开** | `web_search` + `site:patents.google.com country:CN` 或 `site:cnipa.gov.cn` | **必查** — 同地区先发的优先权可能挡你的路 | | 2 | **国际专利** | `web_search` + `site:patents.google.com` / `site:wipo.int` | **必查** — 国外同款方案也算现有技术 | -| 3 | **学术论文** | `research` skill (OpenAlex) / `documents` skill (材料库) / `web_search` + `site:arxiv.org` | **强烈推荐** — 论文公开早于专利,常是创造性杀手 | +| 3 | **学术论文** | `literature` skill (paper_server + 内部材料库) / `web_search` + `site:arxiv.org` | **强烈推荐** — 论文公开早于专利,常是创造性杀手 | | 4 | **行业产品/公开演示** | `web_search` 一般 query | 视情况 — 大厂博客 / 产品文档 / 会议演示 | -| 5 | **本地文献库** | `documents` skill (材料学科 7 个库) / `research` skill (paper_server) | 涉及材料 / 化学 / 工程领域时优先 | +| 5 | **本地文献库** | `literature` skill 的内部材料库与 paper_server 来源 | 涉及材料 / 化学 / 工程领域时优先 | > 注:CNIPA 官网爬虫本 skill **不实现**(反爬重 + 维护成本高)。如果用户要正式可作为 IDS 提交的检索证据,建议人工跑专利数据库(智慧芽 / Patentics / incoPat / 谷歌 Patents 自己手动检索)。本 skill 出的检索结论定位为"代理师写文件前的尽职检索 + 风险预警",不替代正式律所/代理所检索。 diff --git a/skills/rebuttal/SKILL.md b/skills/rebuttal/SKILL.md index ba51dc8..d4c5f33 100644 --- a/skills/rebuttal/SKILL.md +++ b/skills/rebuttal/SKILL.md @@ -15,7 +15,7 @@ description: 回复期刊审稿意见(修回)。把编辑决定信 / 审稿人 |---|---| | vs `paper` | paper **从零写投稿稿**;rebuttal 处理**投出去之后收到的审稿意见**。paper 阶段六的投稿件(首投 cover letter)也不归这里 —— 这里只管修回 | | vs `review` | review 是**替作者审自己的稿**(审稿人视角挑毛病);rebuttal 是**答别人审出来的毛病**。修回中要大改某章时可调 review 深审该章 | -| vs `research`/`documents` | 审稿人要求补引文时,引文的真实性核验接到它们头上(复用 paper 的 citation_verify 三层协议) | +| vs `literature` | 审稿人要求补引文时,引文的真实性核验接到它头上(复用 paper 的 citation_verify 三层协议) | **何时不用**:还没投稿 / 没有审稿意见→paper 或 review;申诉拒稿决定(appeal)→ 不是修回,流程不同,明确告知用户本 skill 只处理修回,申诉信可给建议但不套本流程。 @@ -81,7 +81,7 @@ markitdown /manuscript.docx -o /source/manuscript.md **A. 稿件修改**(有原稿时): 1. 按 tracker 逐条落实改动,产出 `/manuscript_changes.md`:每条 ID 对应「修改位置 + 修改前 → 修改后」对照(位置用章节名;**行号只有用户给了才写,不编**) -2. 需要大改某章(如重写 Discussion)→ 调 `review` skill 深审该章;需要补引文 → 走 `documents`/`research` 检索并按 paper 的 `citation_verify.md` 三层核验,**未核验的引文不进回复信** +2. 需要大改某章(如重写 Discussion)→ 调 `review` skill 深审该章;需要补引文 → 走 `literature` 检索并按 paper 的 `citation_verify.md` 三层核验,**未核验的引文不进回复信** 3. 补的实验 / 分析数据**只能来自用户**,数据没到就保持占位 **B. 逐审稿人起草回复信**(写到 `/response/`,一个审稿人一个文件 `NN_reviewer.md`): diff --git a/skills/rebuttal/references/comment_taxonomy.md b/skills/rebuttal/references/comment_taxonomy.md index 2bedc72..95c8bd7 100644 --- a/skills/rebuttal/references/comment_taxonomy.md +++ b/skills/rebuttal/references/comment_taxonomy.md @@ -20,7 +20,7 @@ | 方法 | 方法细节缺失 / 不可复现 / 仪器参数与标准未写 | 向用户要精确细节(型号 / 参数 / 标准号) | | 统计 | 检验不当 / 缺效应量 / 多重检验 / 样本量不足 | 向用户要检验名 / 样本量 / 置信区间;**不许编数** | | 数据与材料 | 缺数据可用性 / 原始数据不可得 | 向用户要存储库 / DOI / 获取途径 | -| 引文与定位 | 漏引前人工作 / 新颖性表述不准 | 只加**经核验**的引文(走 documents/research + citation_verify) | +| 引文与定位 | 漏引前人工作 / 新颖性表述不准 | 只加**经核验**的引文(走 literature + citation_verify) | | 范围与可行性 | 要求超出本文范围的实验 / 期刊契合度 | 认可价值 → 给替代证据 → 以研究设计为界婉拒 | | 伦理与合规 | 缺审批号 / 知情同意 / 图像完整性 | 通常 `blocking`,向用户要准确审批信息 | diff --git a/skills/rebuttal/references/difficult_cases.md b/skills/rebuttal/references/difficult_cases.md index 6407c9d..bd7b951 100644 --- a/skills/rebuttal/references/difficult_cases.md +++ b/skills/rebuttal/references/difficult_cases.md @@ -40,7 +40,7 @@ R1 要求删的,R2 要求扩;R1 要简化模型,R2 要加参数。 ## 6. 要求引用特定文献(疑似 coercive citation) -- 逐篇判断**真实相关性**:相关 → 走 documents/research 核验后加(`ADD_CITATION`),措辞中性;不相关 → 婉拒,说明本文定位,不暗示审稿人自引 +- 逐篇判断**真实相关性**:相关 → 走 literature 核验后加(`ADD_CITATION`),措辞中性;不相关 → 婉拒,说明本文定位,不暗示审稿人自引 - 不核验就照单全收 = 把编造引文风险带进修回 ## 7. 转投稿(transfer after review) diff --git a/skills/research/SKILL.md b/skills/research/SKILL.md deleted file mode 100644 index 1f2bbe1..0000000 --- a/skills/research/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -name: research -description: 检索 paper_server 中的论文、书籍和书籍章节,获取题录、DOI、摘要及可用的 PDF/XML 全文。适合跨学科文献发现、精确文献查找和引文核验;材料领域可与 documents 并用。 ---- - -# Research - -`research` 是 zcbot 对独立 `paper_server` 项目的客户端适配。`paper_server` 以 OpenAlex 元数据为基础,并按记录实际可用性提供摘要、PDF 或 XML;当前语料以英文为主,也包含中文文献。 - -## 选择与协作 - -- 跨学科发现、题名或 DOI 检索、出版类型识别:优先使用 `research`。 -- 材料领域全文语义检索、性能或工艺数据:优先使用 `documents`。 -- 系统调研、综述或重要引用:可同时查询两者;按 DOI 去重,无 DOI 时按规范化题名、作者和年份去重。 -- 用户已提供具体文件:直接读取;用户只提供题录而任务要求核验论断时,继续获取摘要或全文。 - -论文、书籍和书籍章节均可检索。是否读取摘要、XML 或 PDF,由用户问题所需的证据深度决定。 - -## 调用方式 - -通过 `run_python` 使用 helper: - -```python -from skills.research.paper import search, get_paper, fetch_pdf, fetch_xml -``` - -- `search(keyword="", year=None, year_gte=None, year_lte=None, doi="", first_author="", publication_name="", has_pdf=None, is_oa=None, limit=10)`:搜索题录,返回类型、题名、作者、年份、期刊或出版物、DOI、摘要及全文可用状态。 -- `get_paper(id_or_doi)`:按 `paper_server` ID 或 DOI 获取单条完整记录。 -- `fetch_xml(id_or_doi, working_dir)` / `fetch_pdf(id_or_doi, working_dir)`:把可用全文下载到当前任务的 `papers/` 目录并返回相对路径。 - -helper 自动使用 `PAPER_SERVER_API_KEY` 和可选的 `PAPER_SERVER_URL`。通过 helper 访问 `paper_server`,由它处理认证、URL 和下载路径。 - -## 工作流 - -1. 将用户概念转换为常用专业术语;英文题名占多数时优先使用英文,并可用中文或同义词补充。 -2. 用主题、年份、作者、出版物或 DOI 搜索并筛选候选;`type` 可帮助识别论文、书籍和章节。 -3. 相关性初筛可使用题名和摘要;精确论断、数据、章节、图表或页码应获取足够正文。 -4. 结构化段落和参考文献通常适合读取 XML;版式、页码、公式和图表通常适合读取 PDF。只获取记录实际提供的格式。 -5. 与 `documents` 并查时合并去重,输出中区分“仅题录”“摘要已核对”“全文已核对”。 - -## 失败与真实性 - -- 认证或网络失败时,明确说明 `paper_server` 当前不可用;认证失败需要管理员检查 zcbot 的 `PAPER_SERVER_API_KEY` 配置。 -- DOI 未命中、全文格式缺失或服务器文件不存在时,可改查其他候选或使用现有摘要,不把“已收录”写成“已读全文”。 -- 未命中时可调整术语和过滤条件;没有新增检索思路时如实说明覆盖不足。 -- 只引用返回记录中的真实题名、作者、DOI 和正文,不推测缺失的 ISBN、出版社、版本、页码或内容。 diff --git a/skills/review/SKILL.md b/skills/review/SKILL.md index 0af78be..dcbd603 100644 --- a/skills/review/SKILL.md +++ b/skills/review/SKILL.md @@ -17,7 +17,7 @@ description: 审稿、润色、校对文本时使用。用户要求检查语法 ## 何时不用 - 用户只是要从零起草新文本: 按对应写作 skill 或通用写作处理 -- 用户要求事实核查、文献真伪、最新政策: 需要先用 research / documents / web 或用户给的材料核验 +- 用户要求事实核查、出版物真伪、最新政策: 需要先用 literature / web 或用户给的材料核验 - 用户给的是代码审查: 用 coding ## 审稿顺序 diff --git a/skills/skill-creator/SKILL.md b/skills/skill-creator/SKILL.md index fc4c193..f59c115 100644 --- a/skills/skill-creator/SKILL.md +++ b/skills/skill-creator/SKILL.md @@ -65,7 +65,7 @@ description 是**唯一进每轮 skill 列表、决定路由**的字段。写糊 - 别写成功能罗列,要写成"路由信号" 照抄内置的结构,比如: -> `description: 生成 PowerPoint(.pptx)。✅ 触发:PPT / 幻灯片 / slide / deck / .pptx。⛔ 不触发:报告 / 文档 / 纪要(走 documents/proposal)。...` +> `description: 生成 PowerPoint(.pptx)。✅ 触发:PPT / 幻灯片 / slide / deck / .pptx。⛔ 不触发:报告 / 文档 / 纪要(走对应写作 skill)。...` ### 4. SKILL.md 正文骨架(从零时给用户的模板) diff --git a/skills/standard/SKILL.md b/skills/standard/SKILL.md index 9ce3c59..41c57b0 100644 --- a/skills/standard/SKILL.md +++ b/skills/standard/SKILL.md @@ -184,7 +184,7 @@ python /sandbox/rendering/render.py --profile proposal --format docx / - **可考核**:删"高/好/适当/尽量/合理"等模糊词;每条要求 = 量+数值+单位+判定方向(≥/≤)+对应试验方法 - **指标闭环**:每条技术要求在标准内有方可验(要求章 ↔ 试验方法章呼应) - **术语规则**:一术一义、替换式定义、引用标来源、不重复已有国标术语 -- **引用真实**:标准号/名称/条款号一字不编,存疑用 research/web 核;固定引导语见 gbt_1_1_structure.md §5 +- **引用真实**:标准号/名称/条款号一字不编,存疑用 literature/web 核;固定引导语见 gbt_1_1_structure.md §5 - **量与单位**:法定计量单位(SI),量符号斜体单位正体,修约按 GB/T 8170 - **编制说明必交**:报批稿必附,§4 逐条解释关键指标确定依据 - **命名**:按标准名命名 docx,不要 output.docx / 标准.docx diff --git a/skills/standard/references/drafting_rules.md b/skills/standard/references/drafting_rules.md index 60f4b19..6a38c97 100644 --- a/skills/standard/references/drafting_rules.md +++ b/skills/standard/references/drafting_rules.md @@ -72,7 +72,7 @@ ## 5. 引用真实性(不可编造) -- **标准号、标准名、来源条款号一字不编**。不确定真实性 → 用 research / web_search 核实,核不到就标 ``,不靠训练数据脑补。 +- **标准号、标准名、来源条款号一字不编**。不确定真实性 → 用 literature / web_search 核实,核不到就标 ``,不靠训练数据脑补。 - 建材常用真实基础标准(可直接引,仍建议核版本年号): - GB/T 8170 数值修约规则与极限数值的表示和判定 - GB/T 17671 水泥胶砂强度检验方法(ISO 法) diff --git a/skills/standard/references/gbt_1_1_structure.md b/skills/standard/references/gbt_1_1_structure.md index 70b98ab..a05faf4 100644 --- a/skills/standard/references/gbt_1_1_structure.md +++ b/skills/standard/references/gbt_1_1_structure.md @@ -79,7 +79,7 @@ GB/T 17671 水泥胶砂强度检验方法(ISO 法) - 文件号在前、文件名在后,**按标准号字母+数字升序排**(GB 在前,然后 GB/T、JC/T、ISO…)。 - **只列正文中以规范性方式引用到的**(即引用后构成必须遵守的条款);仅作背景参考的放"参考文献"。 - 若无规范性引用文件,本章可省略(但试验/产品标准几乎都有)。 -- 不编标准号 —— 拿不准真实性时走 research/web 核实,见 drafting_rules.md §引用真实性。 +- 不编标准号 —— 拿不准真实性时走 literature/web 核实,见 drafting_rules.md §引用真实性。 ## 6. 封面要素排布(自上而下) diff --git a/skills/standard/templates/drafting_note.md b/skills/standard/templates/drafting_note.md index 3daa737..6b72207 100644 --- a/skills/standard/templates/drafting_note.md +++ b/skills/standard/templates/drafting_note.md @@ -46,7 +46,7 @@ [采用国际标准时] 本标准 采用 。 ``` -> 此节常需真实文献/标准支撑 → 可联动 research / documents / web_search 查证,**不编标准号**。 +> 此节常需真实出版物/标准支撑 → 可联动 literature / web_search 查证,**不编标准号**。 --- diff --git a/tests/test_literature_merge.py b/tests/test_literature_merge.py new file mode 100644 index 0000000..85ded59 --- /dev/null +++ b/tests/test_literature_merge.py @@ -0,0 +1,110 @@ +import json +import tempfile +import unittest +from pathlib import Path + +from core.skills import SkillRegistry +from skills.literature.scripts.merge_publications import ( + exact_key, + main, + merge_publications, + normalize_doi, + normalize_isbn, + normalize_record, +) + + +ROOT = Path(__file__).resolve().parents[1] + + +class LiteratureSkillTests(unittest.TestCase): + def test_literature_is_the_only_publication_search_skill(self) -> None: + registry = SkillRegistry(ROOT / "skills") + self.assertIn("literature", registry.skills) + self.assertNotIn("research", registry.skills) + self.assertNotIn("documents", registry.skills) + + +class LiteratureNormalizationTests(unittest.TestCase): + def test_normalizes_doi_and_isbn(self) -> None: + self.assertEqual(normalize_doi("https://doi.org/10.1000/ABC.1."), "10.1000/abc.1") + self.assertEqual(normalize_isbn("0-306-40615-2"), "9780306406157") + + def test_preserves_unknown_raw_type(self) -> None: + record = normalize_record({"title": "A", "type": "future-object"}, "future") + self.assertEqual(record["type"], "other") + self.assertEqual(record["raw_type"], "future-object") + + def test_different_book_editions_have_different_keys(self) -> None: + first = normalize_record( + {"title": "Materials", "type": "book", "isbn": "9780306406157", "edition": "1st"}, + "books", + ) + second = normalize_record( + {"title": "Materials", "type": "book", "isbn": "9780306406157", "edition": "2nd"}, + "books", + ) + self.assertNotEqual(exact_key(first), exact_key(second)) + + def test_preserves_date_and_relations(self) -> None: + record = normalize_record( + { + "title": "Chapter", + "type": "book-chapter", + "publication_date": "2025-03-09", + "is_part_of": "Book A", + }, + "books", + ) + self.assertEqual(record["issued"], {"year": 2025, "month": 3, "day": 9}) + self.assertEqual(record["relations"]["is_part_of"], "Book A") + + +class LiteratureMergeTests(unittest.TestCase): + def test_merges_same_doi_and_preserves_sources(self) -> None: + result = merge_publications([ + ("paper_server", {"id": "p1", "doi": "10.1/ABC", "title": "Title", "abstract": "A"}), + ("materials_library", {"id": "m1", "doi": "https://doi.org/10.1/abc", "title": "Title"}), + ]) + self.assertEqual(len(result["publications"]), 1) + self.assertEqual( + {source["backend"] for source in result["publications"][0]["sources"]}, + {"paper_server", "materials_library"}, + ) + + def test_does_not_merge_chapter_with_book(self) -> None: + result = merge_publications([ + ("source", {"title": "Hydration", "type": "book", "first_author": "Li", "year": 2024}), + ("source", {"title": "Hydration", "type": "book_chapter", "first_author": "Li", "year": 2024}), + ]) + self.assertEqual(len(result["publications"]), 2) + + def test_keeps_standard_revisions_and_patent_family_members(self) -> None: + result = merge_publications([ + ("standards", {"title": "Method", "type": "standard", "standard_number": "GB/T 1", "publisher": "SAC", "year": 2020}), + ("standards", {"title": "Method", "type": "standard", "standard_number": "GB/T 1", "publisher": "SAC", "year": 2025}), + ("patents", {"title": "Binder", "type": "patent", "patent_publication_number": "CN100A", "patent_family": "F1"}), + ("patents", {"title": "Binder", "type": "patent", "patent_publication_number": "US100B", "patent_family": "F1"}), + ]) + self.assertEqual(len(result["publications"]), 4) + + def test_marks_similar_titles_as_possible_duplicates(self) -> None: + result = merge_publications([ + ("a", {"title": "Hydration of low carbon cement systems", "type": "article", "year": 2024}), + ("b", {"title": "Hydration of low-carbon cement system", "type": "article", "year": 2024}), + ]) + self.assertEqual(len(result["publications"]), 2) + self.assertEqual(len(result["possible_duplicates"]), 1) + + def test_cli_writes_output(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = root / "source.json" + output = root / "output.json" + source.write_text(json.dumps([{"title": "A", "doi": "10.1/a"}]), encoding="utf-8") + self.assertEqual(main(["--input", f"paper_server={source}", "--output", str(output)]), 0) + self.assertEqual(len(json.loads(output.read_text(encoding="utf-8"))["publications"]), 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/skills/documents/client.py b/tools/document_client.py similarity index 97% rename from skills/documents/client.py rename to tools/document_client.py index 567c3ae..0d74e9c 100644 --- a/skills/documents/client.py +++ b/tools/document_client.py @@ -1,4 +1,4 @@ -"""document_search API 客户端 helper。 +"""内部材料知识库的 host-side 客户端。 base_url / api_key 走 env: DOCUMENT_SEARCH_URL 默认 https://ai.ctc-zc.com:8100/api @@ -42,7 +42,7 @@ def _api_key() -> str: key = os.environ.get("DOCUMENT_SEARCH_API_KEY", "").strip() if not key: raise RuntimeError( - "DOCUMENT_SEARCH_API_KEY env 未设置 —— 配置后再调 documents skill" + "DOCUMENT_SEARCH_API_KEY env 未设置 —— 配置后再使用 literature skill 的内部材料库来源" ) return key diff --git a/tools/documents.py b/tools/documents.py index 5d8b42f..b5db3db 100644 --- a/tools/documents.py +++ b/tools/documents.py @@ -9,7 +9,7 @@ from concurrent.futures import ThreadPoolExecutor from pathlib import Path from typing import Optional -from skills.documents import client as doc_client +from . import document_client as doc_client from .base import Tool diff --git a/tools/run_python.py b/tools/run_python.py index b95843d..5b47000 100644 --- a/tools/run_python.py +++ b/tools/run_python.py @@ -22,7 +22,7 @@ from .base import Tool from .output import compact_tool_output, format_timeout_result _SENSITIVE_PATTERNS = ("API_KEY", "TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY") -# 刻意放行的例外:research skill 在 sandbox 里直连 paper_server,这把只读文献库 key +# 刻意放行的例外:literature skill 在 sandbox 里直连 paper_server,这把只读文献库 key # 的消费方就是沙盒代码本身;低价值、可随时在 paper_server admin 撤销。 _ENV_ALLOWLIST = frozenset({"PAPER_SERVER_API_KEY"})