
Aminer Pdf Citation Verifier
- 30 installs
- 55 repo stars
- Updated July 23, 2026
- canxiangcc/aminer-open-skill
Search and analyze academic research papers and citations
About
Enables searching, accessing, and analyzing academic research papers and citations. Essential during the idea phase for researching existing solutions, competitive analysis, and understanding problem domains.
- Academic search
- Paper discovery
- Research data access
Aminer Pdf Citation Verifier by the numbers
- 30 all-time installs (skills.sh)
- Ranked #1,856 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/canxiangcc/aminer-open-skill --skill aminer-pdf-citation-verifierAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 30 |
|---|---|
| repo stars | ★ 55 |
| Last updated | July 23, 2026 |
| Repository | canxiangcc/aminer-open-skill ↗ |
What it does
Search and analyze academic research papers and citations
What you get
- research findings
- paper references
Files
PDF Citation Verifier
Verify whether the references in a paper PDF actually exist by submitting the PDF to the AMiner pdf-citation-verifier service, polling the asynchronous job, and returning a structured summary. Invoke via natural language or /pdf-citation-verifier.
What This Skill Does
For each reference parsed from the uploaded PDF, the upstream service queries AMiner SearchPro and labels the citation with one of:
REAL— high-confidence match in AMiner.LIKELY_REAL— partial match, likely genuine.NEEDS_REVIEW— evidence is inconclusive; ask a human.LIKELY_FAKE— partial mismatch, probably fabricated.FAKE— no plausible match found.
Each call to the gateway returns the standard envelope {"code": 200, "success": true, "msg": "", "data": ..., "log_id": "..."}. The script unwraps it before printing.
POST /api/v3/paper/citation/verify/uploadreturnsdata: {"job_id": "verify_..."}.GET /api/v3/paper/citation/result?job_id=...returnsdata: [<record>]where the single record has top-level fields likeis_finish,has_hallucination,hallucination_ratio,total,counts_by_status,summary,urls,report,result.- Whenever the script sees
is_finish: true, it also auto-downloadsurls.result(the per-reference JSON) and inlines it asdetailson the returned payload — so a single--outputfile contains both the summary and every record'sstatus/confidence/title/first_author/key_reasons/top_match, with no need to follow the 5-minute OSS link.
The skill returns that record plus the job_id so the user can re-poll later.
File Map
SKILL.md/SKILL.zh.md— English / Chinese skill definitions (this file).commands/pdf-citation-verifier.md— slash command entry.scripts/verify_pdf.py— HTTP client: upload → poll → print the unwrapped result record.requirements.txt— Python dependencies (requests).
Pre-flight
Run these checks before invoking the script. Stop and surface the error to the user if any check fails.
1. AMINER_API_KEY
[ -z "${AMINER_API_KEY+x}" ] && echo "AMINER_API_KEY missing" || echo "AMINER_API_KEY exists"If missing, stop and tell the user to obtain a token from https://open.aminer.cn and export AMINER_API_KEY=<token>. Never print the token value.
2. Python dependency
python3 - <<'PY'
import importlib.util
missing = [name for name in ("requests",) if importlib.util.find_spec(name) is None]
print("Missing: " + ", ".join(missing) if missing else "Python dependencies exist")
PYIf missing, instruct: pip install -r "${CLAUDE_PLUGIN_ROOT}/requirements.txt".
3. PDF input
The user must supply an existing local .pdf file path. If they only describe a paper without a file, ask them to provide the PDF path. Do not invent or download a PDF.
Execution Example
Basic verification with defaults (max 50 references, auto-polls until done):
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/verify_pdf.py" \
--pdf "/abs/path/to/paper.pdf"Full options:
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/verify_pdf.py" \
--pdf "/abs/path/to/paper.pdf" \
--max-refs 80 \
--strict \
--timeout 900 \
--poll-interval 5 \
--output outputs/pdf-citation-verifier/<safe-paper-stem>/result.jsonSubmit-only (no polling, return job_id for later lookup):
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/verify_pdf.py" \
--pdf "/abs/path/to/paper.pdf" --no-waitFetch the result for an existing job_id (no new upload):
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/verify_pdf.py" \
--job-id verify_20260527T090207Z_a72c9ba5Parameters
| Flag | Default | Notes |
|---|---|---|
--pdf | required (unless --job-id) | Local .pdf file path. Server caps body at 50 MB. |
--job-id | – | Skip upload and just fetch the result for an existing job. |
--max-refs | 50 | Server hard cap is 100. |
--strict | off | Stricter FAKE judgement on partial matches. |
--no-wait | off | With --pdf: submit and return job_id without polling. With --job-id: single fetch, return immediately without looping. |
--timeout | 600 | Overall polling timeout in seconds. |
--poll-interval | 5 | Seconds between result polls. |
--request-timeout | 120 | Per-HTTP-request timeout. |
--output | - | Optional path to also write the JSON response. |
Environment Variables
| Var | Required | Purpose |
|---|---|---|
AMINER_API_KEY | yes | JWT used in the Authorization header. |
PDF_CITATION_VERIFIER_BASE_URL | no | Override the gateway base URL. Defaults to https://datacenter.aminer.cn/gateway/open_platform. |
Runtime Constraints
- Never print, log, or echo the value of
AMINER_API_KEY. - Never fabricate verification verdicts. If the script fails or times out, surface the error verbatim — do not synthesize results.
urls,report,result,pdfin the response point to server-side artifacts and may be pre-signed forurl_expire_seconds. Do not claim those local paths exist on the user's machine. Use--outputif the user needs a local copy of the JSON.- Respect the per-user active job cap (server returns 429 when exceeded). If a 429 surfaces, stop and tell the user to wait for prior jobs to finish.
- Treat any
LIKELY_FAKE/FAKEverdict as a flag for human review, not a final accusation. Surfacecounts_by_statusand per-record reasons when the response includes them.
Output Presentation
After the script returns, summarize the result for the user with at minimum:
job_idtotal(number of references verified)has_hallucination,hallucination_ratio- A short table built from
counts_by_status(REAL / LIKELY_REAL / NEEDS_REVIEW / LIKELY_FAKE / FAKE / etc.) - If
details.records[]is present (auto-fetched fromurls.result), list each FAKE / LIKELY_FAKE / NEEDS_REVIEW record'stitle,first_author,year, andkey_reasonsso the user does not have to follow the 5-minute OSS link - Any
urls.report/urls.resultlinks from the response, with a note that they may expire afterurl_expire_seconds - The full JSON should be either saved (via
--output) or echoed back to the user, never silently dropped.
If the inline details fetch failed, the payload carries a details_fetch_error string — surface it and tell the user to GET urls.result themselves before url_expire_seconds runs out.
If is_finish is true and a status / msg field signals failure, report it and suggest re-running.
/pdf-citation-verifier — PDF Citation Verifier
User invoked the PDF Citation Verifier skill with:
$ARGUMENTSLanguage Routing / 语言路由
- If
$ARGUMENTSor the conversation is mainly Chinese, follow 中文命令流程 and read${CLAUDE_PLUGIN_ROOT}/SKILL.zh.md. - Otherwise follow English Command Flow and read
${CLAUDE_PLUGIN_ROOT}/SKILL.md. - Parameter names stay English:
pdf,job-id,max-refs,strict,no-wait,output. - JSON keys, status labels (
REAL/LIKELY_REAL/NEEDS_REVIEW/LIKELY_FAKE/FAKE), and reason codes stay English. - 如果
$ARGUMENTS或对话主要是中文,使用 中文命令流程。 - 否则使用 English Command Flow。
English Command Flow
1. Pre-flight
Run the checks below in order. Any failed check stops the flow — do not run the script.
1. Check AMINER_API_KEY is set:
[ -z "${AMINER_API_KEY+x}" ] && echo "AMINER_API_KEY missing" || echo "AMINER_API_KEY exists"If missing, tell the user to get a token from https://open.aminer.cn and export AMINER_API_KEY=<token>. Never echo the token value.
2. Check Python dependency:
python3 - <<'PY'
import importlib.util
missing = [name for name in ("requests",) if importlib.util.find_spec(name) is None]
print("Missing: " + ", ".join(missing) if missing else "Python dependencies exist")
PYIf missing, instruct: pip install -r "${CLAUDE_PLUGIN_ROOT}/requirements.txt".
3. Confirm the user supplied a local .pdf file path, or alternatively a job-id to fetch results for a previously submitted job. If neither, ask for one. Never fabricate or download a PDF.
2. Parse $ARGUMENTS
Accept structured fields and natural language together:
| Field | Values | Default | Meaning |
|---|---|---|---|
pdf | absolute PDF path | required unless job-id is given | Local file to verify |
job-id | verify_YYYYMMDDTHHMMSSZ_<8hex> | – | Fetch result for an existing job instead of uploading |
max-refs | 1-100 | 50 | Max references to verify (upload only) |
strict | yes / no | no | Stricter FAKE judgement (upload only) |
no-wait | yes / no | no | With pdf: submit and return job_id without polling. With job-id: single fetch, no polling loop. |
timeout | seconds | 600 | Overall polling timeout |
output | path | – | Optional JSON output path |
If both pdf and job-id are missing, or the PDF path does not exist, stop and ask the user.
3. Run
Build the command from parsed args. For a fresh upload:
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/verify_pdf.py" \
--pdf "<pdf-path>"For fetching an existing job:
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/verify_pdf.py" \
--job-id "<verify_...>"Add flags only for values the user explicitly provided:
--max-refs Nwhenmax-refsis set--strictwhenstrict: yes--no-waitwhenno-wait: yes--timeout Nwhentimeoutis set--output <path>whenoutputis set
4. Present the Result
Stdout is JSON (the unwrapped record from data[0], plus an auto-inlined details field when is_finish=true). Show the user:
job_idtotal,has_hallucination,hallucination_ratio- A short table built from
counts_by_status(or top-levelREAL/LIKELY_REAL/NEEDS_REVIEW/LIKELY_FAKE/FAKEcounts) - If
details.records[]is present, list each FAKE / LIKELY_FAKE / NEEDS_REVIEW record'stitle,first_author,year,key_reasons— auto-fetched fromurls.result, so the user does not have to follow the 5-minute OSS link urls.report/urls.resultif present, noting they may expire afterurl_expire_seconds- The path written when
--outputwas used
If the gateway returned a non-200 code, the script exited with an error, or the payload contains details_fetch_error, surface the error verbatim. Do not invent verdicts.
中文命令流程
1. Pre-flight
依次执行下列检查,任何一项失败立即停止,不要运行脚本。
1. 检查 AMINER_API_KEY 是否已设置:
[ -z "${AMINER_API_KEY+x}" ] && echo "AMINER_API_KEY missing" || echo "AMINER_API_KEY exists"缺失则提示用户到 https://open.aminer.cn 申请 token,然后 export AMINER_API_KEY=<token>。禁止回显 token 值。
2. 检查 Python 依赖:
python3 - <<'PY'
import importlib.util
missing = [name for name in ("requests",) if importlib.util.find_spec(name) is None]
print("Missing: " + ", ".join(missing) if missing else "Python dependencies exist")
PY缺失则提示:pip install -r "${CLAUDE_PLUGIN_ROOT}/requirements.txt"。
3. 确认用户给了存在的本地 .pdf 路径,或者给了一个 job-id 用于查询已提交作业。两者都没有就主动追问,不要自行编造或下载 PDF。
2. 解析 $ARGUMENTS
同时支持结构化字段和自然语言:
| 字段 | 取值 | 默认 | 含义 |
|---|---|---|---|
pdf | PDF 绝对路径 | 没给 job-id 时必填 | 要核验的本地文件 |
job-id | verify_YYYYMMDDTHHMMSSZ_<8hex> | – | 不重新上传,只查已提交作业 |
max-refs | 1-100 | 50 | 最多核验多少条参考文献(仅上传时有效) |
strict | yes / no | no | 是否启用更严格的 FAKE 判定(仅上传时有效) |
no-wait | yes / no | no | 与 pdf 连用:仅提交不轮询;与 job-id 连用:单次查询不循环。 |
timeout | 秒 | 600 | 轮询总超时 |
output | 路径 | – | 可选的 JSON 落地路径 |
如果 pdf 和 job-id 都缺失,或者 PDF 路径不存在,停下并向用户追问。
3. 运行
按用户实际给的参数拼命令。新上传:
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/verify_pdf.py" \
--pdf "<pdf-path>"只查已有作业:
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/verify_pdf.py" \
--job-id "<verify_...>"仅当用户显式提供时才加这些 flag:
- 给了
max-refs→--max-refs N strict: yes→--strictno-wait: yes→--no-wait- 给了
timeout→--timeout N - 给了
output→--output <path>
4. 展示结果
脚本的 stdout 是 JSON(已拆掉网关信封,即 data[0] 这个记录;is_finish=true 时还会自动注入 details 字段)。向用户展示:
job_idtotal、has_hallucination、hallucination_ratio- 基于
counts_by_status(或顶层REAL/LIKELY_REAL/NEEDS_REVIEW/LIKELY_FAKE/FAKE计数)的状态小表 - 如果
details.records[]存在,逐条列出 FAKE / LIKELY_FAKE / NEEDS_REVIEW 的title、first_author、year、key_reasons(来自自动拉取的urls.result),用户就不必去点 5 分钟过期的 OSS 链接 - 响应里的
urls.report/urls.result,需要附注会在url_expire_seconds后过期 - 如果用了
--output,告诉用户落盘路径
如果网关 code 非 200、脚本以错误退出,或 payload 里出现 details_fetch_error,原样汇报错误,禁止伪造核验结论。
requests>=2.31.0
"""Upload a PDF to the pdf-citation-verifier service and poll for the result.
This is a thin HTTP client around the AMiner pdf-citation-verifier API:
POST /api/v3/paper/citation/verify/upload
GET /api/v3/paper/citation/result?job_id=<job_id>
Both endpoints return the AMiner gateway envelope:
{"code": 200, "success": true, "msg": "", "data": <obj or list>, "log_id": "..."}
`upload` -> data is an object: {"job_id": "verify_..."}
`result` -> data is a list with one element: [{"is_finish": bool, ...}]
It reads the auth token from `AMINER_API_KEY` and the (optional) base URL
override from `PDF_CITATION_VERIFIER_BASE_URL`.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
import time
from pathlib import Path
from typing import Any
import requests
DEFAULT_BASE_URL = "https://datacenter.aminer.cn/gateway/open_platform"
UPLOAD_PATH = "/api/v3/paper/citation/verify/upload"
RESULT_PATH = "/api/v3/paper/citation/result"
JOB_ID_PATTERN = re.compile(r"^verify_\d{8}T\d{6}Z_[0-9a-f]{8}$")
def _resolve_base_url() -> str:
return os.environ.get("PDF_CITATION_VERIFIER_BASE_URL", DEFAULT_BASE_URL).rstrip("/")
def _auth_headers(api_key: str) -> dict[str, str]:
return {"Authorization": api_key}
def _unwrap_gateway(body: dict[str, Any], *, context: str) -> Any:
"""Validate the gateway envelope and return its `data` payload."""
if not isinstance(body, dict):
raise SystemExit(f"ERROR: {context} returned non-object body: {body!r}")
code = body.get("code")
success = body.get("success")
if code != 200 or success is not True:
msg = body.get("msg") or body.get("message") or ""
raise SystemExit(f"ERROR: {context} returned code={code} success={success} msg={msg!r}")
if "data" not in body:
raise SystemExit(f"ERROR: {context} response missing 'data' field: {body!r}")
return body["data"]
def upload_pdf(
pdf_path: Path,
*,
api_key: str,
base_url: str,
max_refs: int,
strict: bool,
request_timeout: int,
) -> str:
url = f"{base_url}{UPLOAD_PATH}"
with pdf_path.open("rb") as fp:
files = {"file": (pdf_path.name, fp, "application/pdf")}
data = {"max_refs": str(max_refs), "strict": "true" if strict else "false"}
resp = requests.post(
url,
headers=_auth_headers(api_key),
files=files,
data=data,
timeout=request_timeout,
)
if resp.status_code == 401:
raise SystemExit("ERROR: 401 unauthorized — check AMINER_API_KEY.")
if resp.status_code == 413:
raise SystemExit("ERROR: 413 PDF too large (server cap, default 50 MB).")
if resp.status_code == 429:
raise SystemExit("ERROR: 429 too many active jobs for this user (server cap).")
if resp.status_code >= 400:
raise SystemExit(f"ERROR: upload failed with HTTP {resp.status_code}: {resp.text[:300]}")
payload = _unwrap_gateway(resp.json(), context="upload")
if not isinstance(payload, dict):
raise SystemExit(f"ERROR: upload data is not an object: {payload!r}")
job_id = payload.get("job_id")
if not isinstance(job_id, str) or not JOB_ID_PATTERN.match(job_id):
raise SystemExit(f"ERROR: server returned an invalid job_id: {job_id!r}")
return job_id
def fetch_result(
job_id: str,
*,
api_key: str,
base_url: str,
request_timeout: int,
) -> dict[str, Any]:
url = f"{base_url}{RESULT_PATH}"
resp = requests.get(
url,
headers=_auth_headers(api_key),
params={"job_id": job_id},
timeout=request_timeout,
)
if resp.status_code == 429:
raise SystemExit(
"ERROR: 429 too many active jobs for this user (server cap). "
"Wait for prior jobs to finish before polling again."
)
if resp.status_code >= 400:
raise SystemExit(
f"ERROR: polling failed with HTTP {resp.status_code}: {resp.text[:300]}"
)
payload = _unwrap_gateway(resp.json(), context="result")
if isinstance(payload, list):
if not payload:
raise SystemExit(f"ERROR: result data list is empty for job {job_id}")
record = payload[0]
elif isinstance(payload, dict):
record = payload
else:
raise SystemExit(f"ERROR: result data has unexpected type: {payload!r}")
if not isinstance(record, dict):
raise SystemExit(f"ERROR: result record is not an object: {record!r}")
return record
def poll_result(
job_id: str,
*,
api_key: str,
base_url: str,
poll_interval: float,
overall_timeout: int,
request_timeout: int,
) -> dict[str, Any]:
deadline = time.monotonic() + overall_timeout
last_status: str | None = None
while True:
record = fetch_result(
job_id,
api_key=api_key,
base_url=base_url,
request_timeout=request_timeout,
)
if record.get("is_finish") is True:
return record
status = str(record.get("status", "running"))
if status != last_status:
print(f"[poll] {job_id}: {status}", file=sys.stderr)
last_status = status
if time.monotonic() >= deadline:
raise SystemExit(
f"ERROR: timed out after {overall_timeout}s waiting for job {job_id}. "
f"Last status: {status}. You can keep polling manually with --no-wait + --job-id."
)
time.sleep(poll_interval)
def _enrich_with_details(payload: dict[str, Any], *, request_timeout: int) -> None:
"""If urls.result is present, GET the detailed JSON and inline it under payload['details'].
OSS URLs expire in ~5 minutes, so we fetch immediately. Failure is logged
but never fatal — the caller still has the summary in `payload`.
"""
result_url = (payload.get("urls") or {}).get("result")
if not result_url:
return
try:
resp = requests.get(result_url, timeout=request_timeout)
resp.raise_for_status()
payload["details"] = resp.json()
print(f"[details] inlined {len(resp.content)} bytes from urls.result", file=sys.stderr)
except Exception as exc:
payload["details_fetch_error"] = f"{type(exc).__name__}: {exc}"
print(f"[details] WARN: could not fetch urls.result: {exc}", file=sys.stderr)
def main() -> int:
parser = argparse.ArgumentParser(
description="Submit a PDF to pdf-citation-verifier and print the verification result."
)
parser.add_argument(
"--pdf",
help="Path to the PDF to verify. Required unless --job-id is given.",
)
parser.add_argument(
"--job-id",
help="Skip upload and only fetch the result for an existing job_id.",
)
parser.add_argument(
"--max-refs",
type=int,
default=50,
help="Max references to verify (1-100, default 50).",
)
parser.add_argument(
"--strict",
action="store_true",
help="Enable stricter FAKE judgement on partial matches.",
)
parser.add_argument(
"--no-wait",
action="store_true",
help="Submit the job and print job_id without polling.",
)
parser.add_argument(
"--timeout",
type=int,
default=600,
help="Overall polling timeout in seconds (default 600).",
)
parser.add_argument(
"--poll-interval",
type=float,
default=5.0,
help="Polling interval in seconds (default 5).",
)
parser.add_argument(
"--request-timeout",
type=int,
default=120,
help="Per-HTTP-request timeout in seconds (default 120).",
)
parser.add_argument(
"--output",
help="Optional path to write the final JSON response.",
)
args = parser.parse_args()
api_key = os.environ.get("AMINER_API_KEY")
if not api_key:
print(
"ERROR: AMINER_API_KEY is not set. Get a token from https://open.aminer.cn and "
"export it before running this skill.",
file=sys.stderr,
)
return 2
if not (1 <= args.max_refs <= 100):
print("ERROR: --max-refs must be between 1 and 100.", file=sys.stderr)
return 2
if not args.pdf and not args.job_id:
print("ERROR: provide either --pdf (to upload) or --job-id (to fetch).", file=sys.stderr)
return 2
base_url = _resolve_base_url()
if args.job_id:
if not JOB_ID_PATTERN.match(args.job_id):
print(f"ERROR: --job-id format is invalid: {args.job_id!r}", file=sys.stderr)
return 2
job_id = args.job_id
print(f"[fetch] using existing job_id={job_id}", file=sys.stderr)
else:
pdf_path = Path(args.pdf).expanduser()
if not pdf_path.is_file():
print(f"ERROR: PDF not found: {pdf_path}", file=sys.stderr)
return 2
if pdf_path.suffix.lower() != ".pdf":
print(f"ERROR: input must be a .pdf file: {pdf_path}", file=sys.stderr)
return 2
job_id = upload_pdf(
pdf_path,
api_key=api_key,
base_url=base_url,
max_refs=args.max_refs,
strict=args.strict,
request_timeout=args.request_timeout,
)
print(f"[upload] accepted job_id={job_id}", file=sys.stderr)
if args.no_wait:
if args.job_id:
# --job-id + --no-wait: single status check, return immediately regardless of is_finish
payload = fetch_result(
job_id,
api_key=api_key,
base_url=base_url,
request_timeout=args.request_timeout,
)
payload.setdefault("job_id", job_id)
else:
# --pdf + --no-wait: just uploaded, return job_id without polling
payload: dict[str, Any] = {"job_id": job_id, "is_finish": False, "status": "submitted"}
else:
payload = poll_result(
job_id,
api_key=api_key,
base_url=base_url,
poll_interval=args.poll_interval,
overall_timeout=args.timeout,
request_timeout=args.request_timeout,
)
payload.setdefault("job_id", job_id)
if payload.get("is_finish") is True:
_enrich_with_details(payload, request_timeout=args.request_timeout)
if args.output:
out_path = Path(args.output).expanduser()
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"[output] wrote {out_path}", file=sys.stderr)
json.dump(payload, sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
return 0
if __name__ == "__main__":
sys.exit(main())
PDF Citation Verifier(PDF 引用核验)
把一篇论文 PDF 上传到 AMiner pdf-citation-verifier 服务,等待异步作业完成,并返回结构化的核验结果。可以通过自然语言触发,也可以使用 /pdf-citation-verifier。
这个 Skill 做什么
服务端会从 PDF 中解析参考文献,逐条调用 AMiner SearchPro 查询,给每条引用一个标签:
REAL——AMiner 中能高置信度匹配。LIKELY_REAL——部分匹配,大概率真实。NEEDS_REVIEW——证据不足,需要人工审阅。LIKELY_FAKE——部分不匹配,疑似伪造。FAKE——找不到合理匹配。
网关返回统一信封 {"code": 200, "success": true, "msg": "", "data": ..., "log_id": "..."},脚本会自动拆掉这层再输出。
POST /api/v3/paper/citation/verify/upload的data是对象:{"job_id": "verify_..."}。GET /api/v3/paper/citation/result?job_id=...的data是只含一个元素的数组,元素里有顶层字段is_finish、has_hallucination、hallucination_ratio、total、counts_by_status、summary、urls、report、result等。- 一旦脚本看到
is_finish: true,会自动 GET `urls.result`(逐条引用 JSON)合并进返回 payload 的details字段——这样一个--output文件里同时有 summary 和每条记录的status/confidence/title/first_author/key_reasons/top_match,用户不必在 5 分钟内去点 OSS 链接。
Skill 返回这个记录加上 job_id,用户后续可凭 job_id 再次查询。
文件结构
SKILL.md/SKILL.zh.md——英文 / 中文 Skill 定义(本文件)。commands/pdf-citation-verifier.md——slash command 入口。scripts/verify_pdf.py——HTTP 客户端:上传 → 轮询 → 拆封信封后打印结果记录。requirements.txt——Python 依赖(requests)。
Pre-flight 检查
在执行脚本前必须先过下面三项;任何一项失败立即停止并告知用户。
1. AMINER_API_KEY
[ -z "${AMINER_API_KEY+x}" ] && echo "AMINER_API_KEY missing" || echo "AMINER_API_KEY exists"缺失则停止,引导用户到 https://open.aminer.cn 获取 Token,然后 export AMINER_API_KEY=<token>。任何输出中都不得打印 token 的值。
2. Python 依赖
python3 - <<'PY'
import importlib.util
missing = [name for name in ("requests",) if importlib.util.find_spec(name) is None]
print("Missing: " + ", ".join(missing) if missing else "Python dependencies exist")
PY缺失则提示安装:pip install -r "${CLAUDE_PLUGIN_ROOT}/requirements.txt"。
3. PDF 输入
用户必须提供存在的本地 .pdf 路径。如果只描述了论文但没给文件,必须主动追问 PDF 路径。不要自行编造或下载 PDF。
执行示例
默认参数(最多核验 50 条,自动轮询到完成):
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/verify_pdf.py" \
--pdf "/abs/path/to/paper.pdf"完整参数:
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/verify_pdf.py" \
--pdf "/abs/path/to/paper.pdf" \
--max-refs 80 \
--strict \
--timeout 900 \
--poll-interval 5 \
--output outputs/pdf-citation-verifier/<safe-paper-stem>/result.json仅提交不等待(拿到 job_id 后让用户自己查):
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/verify_pdf.py" \
--pdf "/abs/path/to/paper.pdf" --no-wait凭已有 job_id 直接拉结果(不重新上传):
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/verify_pdf.py" \
--job-id verify_20260527T090207Z_a72c9ba5参数说明
| 参数 | 默认 | 说明 |
|---|---|---|
--pdf | 必填(除非给了 --job-id) | 本地 .pdf 路径。服务端 body 上限 50 MB。 |
--job-id | – | 不上传,只拉已有作业的结果。 |
--max-refs | 50 | 服务端硬上限 100。 |
--strict | 关闭 | 开启后对部分匹配会更严格地判 FAKE。 |
--no-wait | 关闭 | 与 --pdf 连用:仅提交作业并返回 job_id,不轮询;与 --job-id 连用:单次查询后立即返回,不进入轮询循环。 |
--timeout | 600 | 轮询的整体超时(秒)。 |
--poll-interval | 5 | 两次轮询之间的间隔(秒)。 |
--request-timeout | 120 | 单次 HTTP 请求超时(秒)。 |
--output | - | 可选:把最终 JSON 同步落到本地路径。 |
环境变量
| 变量 | 是否必需 | 用途 |
|---|---|---|
AMINER_API_KEY | 是 | 写入请求头 Authorization 的 JWT。 |
PDF_CITATION_VERIFIER_BASE_URL | 否 | 覆盖网关 base URL,默认 https://datacenter.aminer.cn/gateway/open_platform。 |
运行约束
- 绝对不要以任何方式打印、日志或回显
AMINER_API_KEY的值。 - 绝对不要伪造核验结果。脚本失败或超时时,原样汇报错误,不得自行编造。
- 响应中的
urls、report、result、pdf都是服务端产物,多半是带有效期url_expire_seconds的预签名链接。不要谎称这些路径在用户本机存在;需要本地 JSON 副本时用--output。 - 注意单用户活跃作业上限(服务端超出会返回 429)。出现 429 时停下并告知用户先等已提交的作业完成。
- 任何
LIKELY_FAKE/FAKE都只是"需要人工复核"的信号,不是终审。展示结果时尽量带上counts_by_status与逐条原因(若响应包含)。
结果展示
脚本返回后,至少向用户呈现:
job_idtotal(核验的引用数量)has_hallucination、hallucination_ratio- 基于
counts_by_status的状态计数小表(REAL / LIKELY_REAL / NEEDS_REVIEW / LIKELY_FAKE / FAKE 等) - 如果
details.records[]存在(自动从urls.result拉的),逐条列出 FAKE / LIKELY_FAKE / NEEDS_REVIEW 的title、first_author、year、key_reasons,省得用户去点会过期的 OSS 链接 - 响应里的
urls.report/urls.result链接,需要附注会在url_expire_seconds后过期 - 完整 JSON 要么
--output落盘,要么直接回显给用户,不要静默丢弃。
如果 details 自动拉取失败,payload 会带一个 details_fetch_error 字段——把错误告诉用户并建议在 url_expire_seconds 秒内自行 GET urls.result。
如果 is_finish 为 true 且 status / msg 指示失败,把信息告知用户并建议重试。