泉方智慧云图书馆 · 申请全文自动化(纯 HTTP 版 v0.4.2)
对齐 ablesci-downloader 模式,把 tsgyun「检索 → 申请全文 → 我的文件夹等回传 → 下载 PDF」自动化。
v0.2 关键升级:放弃浏览器自动化,改纯 HTTP 请求——不依赖 Edge/Chrome 调试、不需要代理/VPN 插件,您平时能上网就一定能用。
v0.4.2(可移植化 + 支持市场分发,2026-10-02):
📦 新增 scripts/pack_skill.py 打包脚本 + .skillignore 排除清单
· 安全红线:强制排除登录态凭证文件(含用户真实账号 cookie),另有 JWT/cookie
二次扫描兜底,确保发布包零凭证;
· 声明式排除(.skillignore),与验收脚本共用同一口径;
· 打包前自校验:frontmatter 合规 / 文件数上限 100 / 文档断链 / 凭证扫描 / 全量语法编译,
任一不过拒绝出包;产出 PACK_MANIFEST.md 列明入包与排除清单。
🔧 可移植性:状态目录可重定向
· 新增 state_dir():登录态优先写 TSGYUN_STATE_DIR(环境变量)→ 技能目录(可写时)→
~/.tsgyun-fulltext(技能目录只读时兜底)。解决"市场把技能装到只读/共享路径后写不进去"的问题;
· 写入失败给出明确指引而非崩溃;登录态落盘后尝试 chmod 600(仅本机可读)。
📄 文档:USAGE.md 顶部新增「新用户三步上手」;新增状态目录配置说明;
scripts/login.example.json 提供登录态格式模板(不含任何真实值)。
v0.4.1 修复(OA 候选池剔除 HTML 落地页,2026-10-02):
🟡 resolve_oa_candidates() 会把 PMC/出版社的 HTML 落地页当 OA 候选 → 假阳性
实测 DOI 10.1001/jamaneurol.2021.5420(JAMA Neurol 订阅文)时发现:Europe PMC 的
fullTextUrlList 会把 PMC 落地页(https://www.ncbi.nlm.nih.gov/pmc/articles/PMC8861899,
HTML 形态)标记为 Free,于是候选池里出现了 1 条永远下不下来的 html 链接。
虽被下游 %PDF- 校验拦下、不会污染产物,但① 白耗一次网络请求;② 日志打印
"检测到 1 个 OA 候选"误导判断(让人以为有免费全文)。
现新增 _is_pdf_url() 形态判定:只接受 .pdf 后缀 / blobtype=pdf / format=pdf /
pdf=render / /pdf/ 路径段,HTML 落地页一律不入池;resolve_oa_candidates() 与
try_free_download() 的 extra_urls 旁路同步过滤。
效果:该订阅文候选池 1 → 0(诚实报"OA 源无记录",直接走机构通道);
OA 文(10.1007/s13311-021-01119-1)候选池仍为 2 条真 PDF 直链,未误伤。
回归测试扩充至 56 项(原 46 + 新增 10)。
v0.4.0 修复(全面审计后整修 15 项,2026-10-02):
🔴 P0-1 pick_item() PMID 分支恒为假 → PMID 输入会下错文献(最严重)
原 if kind == "PMID" and it_pmid == meta[0]——meta[0] 是 kind 标签字符串 "PMID",
拿真实 PMID 号去比它必然为假;且 PMID 号根本没被传进 pick_item(resolve_metadata 丢了它)。
后果:PMID 输入时逐条匹配全部落空 → 落到 return items[0] 随手取第一条 → 下错文献。
(实测:输入 PMID 42830007,命中 pmid 11111111 的无关文章。)
现改为 pick_item(items, meta, identifier) 显式传标识,匹配顺序 DOI 精确 → PMID 精确 → 标题;
有明确标识却匹配不上时返回 None、判"未找到",绝不再下错。
🔴 P0-2 normalize_title() 抹掉连字符 → 漏匹配/错配
原实现把 - _ . , : ; 一律换成空格,TREM2-PPARA 变成 trem2 ppara,
① 站点标题与检索标题标点写法不一致时漏匹配(已回传的 PDF 取不回来);
② X-Y 与 X Y 被判为同一篇 → 错配。
新增两级匹配 norm_strict() / norm_loose() + is_same_text():严格优先(保留连字符语义),
宽松回退;"包含"判定设 12 字符下限,避免短词误配。连字符对 TREM2-PPARA/Ca²⁺-lysosome-TFEB/SCF+G-CSF 是语义组成部分,必须保留。
🔴 P0-3 safe_filename() 未规避 Windows 保留设备名 → 落盘必失败
CON/NUL/COM1/LPT1/PRN/AUX 在 Windows 上任何扩展名都无法创建。
现命中即加 _ 前缀;同时剔除控制字符(\x00-\x1f)避免 open() 抛 ValueError。
🟠 P1-4 find_existing_case() 只查前 50 条 → 查重失效、重复扣额度
新增 iter_all_cases() 翻页遍历全部记录(与 download_all 共用同一遍历逻辑)。
旧实现在申请记录超 50 条后查重失效 → 重复提交申请、白耗机构额度,与"防扣分"目标正相反。
🟠 P1-5 getcases 字段名未验证:新增 case_pmid() / case_doi() 多候选键兜底
(pmid/pmidNcNlmId/ncbiPmid/pmId/_id),避免字段名不符时查重整条失效。
🟠 P1-6 --auto 用 ,;| 分隔 → 切碎含逗号的标题与含分号的 DOI
新增 split_identifiers():整体是合法标识则绝不切分;仅按换行/竖线切;
单条含逗号时保持整条并提示改用 --file。
🟠 P1-7 OA 通道缺 is_oa 校验 → 可能拿到预印本
Unpaywall 加 is_oa 门槛并按 published > accepted > submitted 排序;
Semantic Scholar 加 isOpenAccess 校验且置于候选末位(其链接常指向 arXiv 预印本)。
🟠 P1-8 resolve_metadata() 不识别 "42830007." 尾点形态
_id 实战形态带尾点,会被误判成标题、检索词变成一串数字。现先 strip(".") 规范化;
并补 PMID:xxxx / PMC xxxx 等写法识别。
🟡 P2 七项:manifest 改为按 identifier 合并(不再覆盖,可累计审计);
rename_existing 支持经 getcases 反查 case*.pdf 的真实标题(原来永远跳过);
poll_cases 用 is_same_text 并说明 status 语义;轮询加指数退避(1.5×,上限 600s);
删除 _oa_candidates_europepmc 的死参数 title;精简 DEFAULT_CFG 未使用项;
新增模式互斥校验(--rename-existing --auto 不再静默忽略);--auto 结果去重。
📄 文档修正:references/workflow.md 删去并不存在的 --resume 参数说明(续轮询=重跑原命令);
USAGE.md 更正"空格/逗号分隔"的错误表述(实测与实现冲突)。
v0.3.0 修复(文件名一律 = 论文标题,2026-10-02):
- 根因 1:文件名退化成纯数字。 原 title = item.get("title") or identifier 在检索命中项 title 字段为空时,回退到检索项的 _id(实战形态为 "42830007."),经 safe_filename 去掉尾点后落盘成 42830007.pdf。新增 title_from_item():标题按 title → articleTitle → titleEn 取,最后才考虑 _id,且纯数字 / DOI / PMCID 形态一律拒绝。
- 根因 2:OA 通道抢在标题解析之前下载。 DOI 输入时 try_free_download 收到的标题为空,落到 safe_filename(title or doi) → 文件名是 DOI 串。现改为下载前先反查真实标题。
- 新增 resolve_title() 统一标题解析层(下载命名唯一入口),五级回退:① 已知标题 → ② 检索命中项标题 → ③ 免费元数据源(Europe PMC / OpenAlex,按 DOI、PMCID、PMID 反查)→ ④ tsgyun 自身检索兜底 → ⑤ 原标识兜底。
- 新增 metatitle_by_doi() / metatitle_by_pmcid():DOI / PMCID 反查真实标题。
- 检索兜底按需触发(B 方案):仅当存在真实标识(DOI/PMID/PMCID)时才发起检索,case123 这类文件占位名不会拿去检索、不浪费请求。
- 六处落盘调用点全部接入 resolve_title():OA 免费通道、申请直链、轮询回传、pdfviewer 兜底、查重命中、--download-all 批量。
- --download-all 补全通道:条目标题为空时按 pmcid → doi → pmid 逐级反查(原实现只走 PMID,DOI-only 条目会退化成一串数字)。
- 实测:7/7 用例通过(含 _id 拒绝、占位名不触发检索、元数据反查)。
v0.2.1 修复(实测验证):
- 流式下载(边读边写,不再把整份 PDF 读进内存)→ 连下 30+ 大 PDF 不再 OOM 崩溃;
- safe_filename():解码 HTML 实体('→',保留 β 等)、替换 Windows 非法字符、截断/兜底,修复 Windows GBK 终端下特殊字符导致退出码 1 的静默失败;
- 单篇异常隔离 + 翻页容错:某篇失败不中断整体;
- 断点续传:已存在且 >10KB 的文件自动跳过,重跑可续;
- 全局异常兜底 + stdout/stderr 强制 utf-8(replace),任何崩溃都打印 traceback 而非静默退出。
v0.2.2 修复(2026-08-25 实测验证):
- OA 误报根除:非 OA 文章不再把 Europe PMC 网页当 PDF 存盘;下载统一先写 .part 临时文件、确认 %PDF- 才重命名 .pdf,任何非 PDF 绝不伪装成 .pdf。
- Europe PMC 候选加 isOpenAccess/hasPDF=Y 门槛,避免订阅文误判为 OA。
- PMID 输入解析修复:pmid_hint 原误取 kind 字符串 "PMID",导致检索词变成字面 "PMID"、PMID 下载永远失败;现改为用真实 PMID 号(实测 PMID 42608572 正确命中)。
- 按论文标题命名:DOI/PMID 输入时文件名改用检索到的真实标题(不再是一串 DOI);safe_filename 默认长度 80→150,长标题不再被截断。
- poll_cases 守卫:指定 caseId 等待回传时严格按 caseId/标题匹配,不再把别的旧申请的 PDF 错下回来。
- 登录态预检:主流程建好会话后先调 check_login(),token 失效立即提示 python tsgyun_bot.py --login 并返回 1,省得白跑申请。
- download_via_pdfviewer 兜底写文件同样改 .part 一致性处理。
可移植性 / 部署须知(上传 SkillHub 重点)
- 纯标准库主流程:检索/申请/轮询/下载全部用
urllib,无第三方依赖,Python 3.8+ 直接可跑,跨 Windows/macOS/Linux。 - 登录态文件不随包分发:用户本人浏览器登录后生成的会话态文件(同目录
login前缀的 JSON,含 cookie/token,属敏感凭据)切勿提交或上传;新机器首次用--login自行生成。打包清单(.skillignore)已把该文件列入安全红线,构建时强制剔除。 - 配置模板可安全分发:
scripts/config.example.json仅含公开域名(pm.yuntsg.com 等),无密钥;用户如需自定义机构入口,复制成同目录的config前缀 JSON 即可(该实配置文件属本机配置,不入包)。 - 状态目录可重定向:登录态默认写脚本同目录;技能目录只读时可用环境变量
TSGYUN_STATE_DIR指定可写目录,脚本亦会自动兜底到~/.tsgyun-fulltext。 --login浏览器登录的可移植性:detect_browser()跨平台探测本机 Edge/Chrome/Brave(Windows/macOS 硬编码路径 +shutil.which兜底 Linux 的 chrome/chromium);若都没有,ensure_playwright()会自动pip install playwright+playwright install chromium,或改用 USAGE.md 的手动 cookie 方式,无需 GUI 也能登录。- 产物落盘:
--out指定目录(默认脚本同目录papers/),PDF 一律按论文真实标题命名(经resolve_title()统一解析,DOI/PMID/标题输入均适用),manifest 记录状态/校验/来源。 - 依赖:
requirements.txt标注playwright>=1.40(仅--login可选);主流程零依赖。
何时使用
用户要用机构 tsgyun 云图书馆 / 本地PubMed(yuntsg.com)申请外文全文,说"泉方申请全文 / tsgyun 下文献 / 云图书馆全文 / 本地PubMed 全文"。仅限该用户所属机构已订阅/试用且本人有账号。
核心用法(两步)
# ① 登录一次(弹出浏览器,您本人登录,脚本保存登录态,之后无需再登)
python <skill>/scripts/tsgyun_bot.py --login
# ② 申请全文(DOI/PMID/标题均可,OA 免费优先,拿不到免费才走机构额度)
python <skill>/scripts/tsgyun_bot.py --auto "10.1016/j.cell.2023.01.001" --out ./papers
# 批量(每行一条,# 注释):--file ids.txt;禁用 OA:--no-free
依赖:主流程零第三方依赖(纯 urllib);仅 --login 需要 pip install playwright(可选,也可手动配 cookie,见 USAGE.md)。
关键行为
- OA 免费优先 → tsgyun 兜底:DOI / PMID / 标题 均可触发免费通道——DOI 直查 Europe PMC / Unpaywall / Semantic Scholar / OpenAlex;仅 PMID/标题时,先检索或经 Europe PMC 按 PMID 反查 DOI/PMCID,再走 OA(
--no-free禁用)。 - 检索:
POST /api/search/search(本地PubMed,参数 term/size/page…),按 DOI>PMID>标题 匹配结果。 - 申请:
POST /api/cases/add(JSON 数组,单次≤20 条,字段对齐页面 scVal 结构,sourcecode='pm')。 - 登录态复用:浏览器登录一次 → cookie/token 存本机登录态文件 → 后续请求自动携带,无需重复登录。
- 产物:
<out>/manifest.json+manifest.tsv(状态/路径/校验/来源/备注)。
流程(与 ablesci 对齐)
- 解析标识(PMID/DOI/PMC/标题)。
- OA 免费优先尝试(同 ablesci,已验证可用)。
- 检索本地PubMed → 匹配文献 →
resolve_title()统一解析出论文真实标题 → 申请全文 → 存入『我的文件夹』。 - 轮询『我的文件夹』(user.tsgyun.com/center/bookshelf):回传后下载 PDF → 校验
%PDF-;落盘文件名一律用解析出的论文标题。 - 写入 manifest。
✅ 实测状态(2026-08-24 登录态下已全部打通)
- 登录:
--login弹浏览器(复用本机 Edge),本人登录一次 → cookie+token 存本机登录态文件(实测通过)。 - 检索:
POST /api/search/search实测通过(DOI 精确命中 PMID;关键:POST 参数只进 body 不进 URL、exclude=0、请求头带token+X-Requested-With: XMLHttpRequest)。 - 申请全文:
POST /api/cases/add实测通过(返回"文献已添加至个人中心")。 - 轮询回传:
GET /api/folder/getcases(个人中心-我申请的文献)实测通过;status==2且有pdfUrl即回传完成;按 caseId/标题匹配。 - 下载:
pdfUrl直链(http://attach.pubtsg.com:8088/attach/...pdf?query=...)——该服务器仅在机构网络可访问,脚本在用户本机运行时即可下载。 - OA 免费优先:实测通过(Europe PMC/PLOS 直连,加 Referer 头)。
- 全部环节已实测,技能为可用的成品;建议在您本机网络环境跑一次申请→回传→下载全链路回归。
合规边界(务必遵守)
- 仅限本人所属机构已订阅/试用的合法账号、个人科研使用;禁批量抓取、禁传播盈利、禁商业再分发。
- 密码/验证码由用户在浏览器完成,脚本绝不读取/存储/中转;登录态仅存本机,不发送任何第三方。
详细资料(按需加载)
USAGE.md— 命令示例、参数速查、手动 cookie 配置、实测步骤。references/workflow.md— 已逆向确认的接口清单、登录流程、实测补全步骤。scripts/tsgyun_bot.py— 主脚本;直接运行即可,无需读入上下文。scripts/pack_skill.py— 打包发布脚本(出包前自动做安全与合规校验)。.skillignore— 打包排除清单(凭证/本机状态/产物)。
打包发布(维护者)
python scripts/pack_skill.py --check # 只校验,不出包
python scripts/pack_skill.py # 产出 dist/tsgyun-fulltext.zip + PACK_MANIFEST.md
打包脚本会自动:拦下登录态凭证文件、剔除 __pycache__ 与 dist 目录、
校验 frontmatter 与文档断链、编译全量 Python 源码、检查文件数上限。
出包后建议再用 skill-package-acceptance 技能的验收脚本独立验一次。