
Linkfox Zhihuiya Pdf
- 161 installs
- 64 repo stars
- Updated August 3, 2026
- linkfox-ai/linkfox-skills
linkfox-zhihuiya-pdf is a LinkFox Agent Skill that downloads patent full-text PDF documents from the Zhihuiya (PatSnap) database by patent ID or publication number for developers doing IP research.
About
linkfox-zhihuiya-pdf is one of 77 API-driven skills in linkfox-ai/linkfox-skills for cross-border e-commerce. It queries the Zhihuiya patent PDF service via the LinkFox tool gateway, returning pdfPath download URLs for up to 100 patents per request by patentId or patentNumber. When both identifiers are supplied, patentId takes precedence. Set replaceByRelated to 1 to substitute a family patent PDF when the original is unavailable. Requires LINKFOXAGENT_API_KEY and runs zhihuiya_pdf_data.py or response_io.py for large batch persistence. Part of the PatSnap patent skill group alongside claim-data, legal-status, and patent-family tools. Use when validating product IP risk or exporting patent documents—not for keyword patent discovery.
- Batch up to 100 patents per request by patentId or patentNumber via LinkFox gateway
- replaceByRelated=1 substitutes family patent PDF when original unavailable
- Runs scripts/zhihuiya_pdf_data.py with response_io.py for large batch field projection
- One of 77 LinkFox Skills requiring LINKFOXAGENT_API_KEY environment variable
Linkfox Zhihuiya Pdf by the numbers
- 161 all-time installs (skills.sh)
- Ranked #640 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/linkfox-ai/linkfox-skills --skill linkfox-zhihuiya-pdfAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 161 |
|---|---|
| repo stars | ★ 64 |
| Last updated | August 3, 2026 |
| Repository | linkfox-ai/linkfox-skills ↗ |
How do you download patent PDFs from Zhihuiya?
Download up to 100 Zhihuiya patent PDFs by publication number or patent ID through the LinkFox gateway API.
Who is it for?
E-commerce developers doing patent compliance checks who need batch Zhihuiya PDF retrieval by publication number or patent ID.
Skip if: Keyword-based patent discovery, claim translation, or workflows without a LinkFox Agent API key.
When should I use this skill?
User requests patent PDF download, full-text patent documents, PatSnap PDF export, or batch patent file retrieval.
What you get
Patent PDF download URLs with publication numbers, optional family-substitution notes, and persisted batch response files.
- Patent PDF download URLs
- Batch response files
By the numbers
- Batch limit of 100 patents per Zhihuiya PDF request
- Part of linkfox-ai/linkfox-skills collection with 77 API-driven skills
Files
Zhihuiya Patent PDF Downloader
This skill guides you on how to retrieve patent PDF full-text download links from the Zhihuiya patent database, supporting lookup by patent ID or publication number.
Core Concepts
The Zhihuiya Patent PDF service provides direct download links to the full-text PDF documents of patents worldwide. You can query by patent ID or publication number (also called public announcement number), and retrieve up to 100 patents in a single request.
Lookup priority: When both patentId and patentNumber are supplied, the service uses patentId first. This is important to remember when both identifiers are available.
Family substitution: If a patent's own PDF is unavailable, the service can optionally return the PDF of a related family patent instead.
Parameter Guide
| Parameter | Type | Required | Description |
|---|---|---|---|
| patentId | string | Conditionally | Patent ID(s). At least one of patentId or patentNumber must be provided. Separate multiple values with commas. Max 100 entries. |
| patentNumber | string | Conditionally | Publication/announcement number(s). At least one of patentId or patentNumber must be provided. Separate multiple values with commas. Max 100 entries. |
| replaceByRelated | string | No | Whether to substitute with a family patent PDF when the original is unavailable. 1 = yes, 0 = no. Defaults to no substitution. |
How to Choose the Right Identifier
- Patent ID (
patentId): An internal numeric identifier within the Zhihuiya system. Use this when the user provides Zhihuiya-specific IDs. - Publication Number (
patentNumber): The public-facing patent number (e.g.,US20230012345A1,CN115000000A). Use this when the user provides standard patent numbers.
Response Fields
| Field | Description |
|---|---|
| total | Number of records returned |
| data | Array of patent objects, each containing download information |
| data[].patentId | The patent ID |
| data[].pn | The publication/announcement number |
| data[].pdfPath | The PDF full-text download URL |
| data[].pnRelated | Publication number of the substitute family patent (only present when family substitution is used) |
| costToken | Tokens consumed by the request |
API Usage
This tool calls the LinkFox tool gateway API. See references/api.md for calling conventions, request parameters, and response structure. You can also execute scripts/zhihuiya_pdf_data.py directly to run queries.
Usage Examples
1. Single Patent by Publication Number
Retrieve the PDF for patent publication number US20230012345A1.Parameters: {"patentNumber": "US20230012345A1"}
2. Multiple Patents by Publication Number
Download PDFs for CN115000000A, CN115000001A, and CN115000002A.Parameters: {"patentNumber": "CN115000000A,CN115000001A,CN115000002A"}
3. Single Patent by Patent ID
Get the full-text PDF for patent ID 12345678.Parameters: {"patentId": "12345678"}
4. With Family Substitution Enabled
Download the PDF for EP4000000A1. If it is unavailable, use a family patent PDF instead.Parameters: {"patentNumber": "EP4000000A1", "replaceByRelated": "1"}
5. Batch Download by Patent IDs
Retrieve PDFs for patent IDs 11111111, 22222222, 33333333.Parameters: {"patentId": "11111111,22222222,33333333"}
Display Rules
1. Present download links clearly: For each patent, show the publication number and its PDF download link in a clean table or list format. 2. Highlight substitutions: If a PDF was provided via family patent substitution, explicitly note this and show the pnRelated value so the user knows which family patent was used. 3. Batch results: When multiple patents are returned, present them in a table with columns: Publication Number, Patent ID, PDF Link, and Substitution Note (if applicable). 4. Error handling: When a query fails or returns no results, explain the reason and suggest the user verify the patent ID or publication number. If replaceByRelated was not enabled, suggest enabling it as an alternative. 5. No PDF available: If a patent entry is returned without a pdfPath, inform the user that the PDF is not available and suggest enabling family substitution.
Important Limitations
- Batch limit: A maximum of 100 patents per request.
- Identifier requirement: At least one of
patentIdorpatentNumbermust be provided; the request will fail if both are empty. - Priority rule: When both identifiers are provided,
patentIdtakes precedence overpatentNumber. - PDF availability: Not all patents have PDFs available. Use the
replaceByRelatedoption to fall back to family patents.
User Expression & Scenario Quick Reference
Applicable -- Patent PDF document retrieval tasks:
| User Says | Scenario |
|---|---|
| "Download the PDF for patent XX" | Single patent PDF retrieval |
| "Get full-text documents for these patents" | Batch patent PDF download |
| "I need the PDF for publication number XX" | Lookup by publication number |
| "Can I get the patent document even if it's not directly available" | Family substitution scenario |
| "Batch export patent PDFs" | Multi-patent batch download |
Not applicable -- Needs beyond patent PDF retrieval:
- Patent search or discovery (finding patents by keyword, assignee, etc.)
- Patent citation or legal status analysis
- Patent claim interpretation or translation
- Patent portfolio analytics or landscape mapping
Feedback:
Auto-detect and report feedback via the Feedback API when any of the following apply: 1. The functionality or purpose described in this skill does not match actual behavior 2. The skill's results do not match the user's intent 3. The user expresses dissatisfaction or praise about this skill 4. Anything you believe could be improved
Call the feedback API as specified in references/api.md. Do not interrupt the user's flow.
<!-- LF_LARGE_RESPONSE_BLOCK -->
Handling Large Responses
To avoid overflowing the agent context, persist the response to disk and extract only the fields you need:
python scripts/response_io.py run --script scripts/zhihuiya_pdf_data.py --out-dir <DIR> '<params>'
python scripts/response_io.py read <file> --fields "<paths>" # or --path "<JMESPath>"Pick--out-diroutside any git working tree (e.g./tmp/...on Unix,%TEMP%/...on Windows). Persisted responses may contain PII, pricing, or auth-sensitive data — do not commit them. Files are not auto-deleted; clean up when the task is done.
run writes the full response to a file and emits only a schema preview + file path. read projects specific fields, with --limit/--offset for slicing and --format json|jsonl|csv|table for output.
When to prefer this pattern — apply your judgment based on the response characteristics, e.g.:
- High field count per record, or fields you don't need
- Batch/paginated results (multiple items per call)
- Long-text fields (descriptions, reviews, HTML, time series)
- Output reused across later steps rather than consumed immediately
For small, single-use responses, calling the main script directly is fine.
⚠️ The preview is a truncated schema + sample, not the full data. Any field-level decision must read from the persisted file via read. <!-- /LF_LARGE_RESPONSE_BLOCK -->
--- For more high-quality, professional cross-border e-commerce skills, set [LinkFox Skills](https://skill.linkfox.com/).
智慧芽 PDF全文查询 API 参考
调用规范
- 请求地址:
https://tool-gateway.linkfox.com/zhihuiya/pdfData - 请求方式:POST,Content-Type: application/json
- 认证方式:Header
Authorization: <api_key>,api_key 从环境变量LINKFOXAGENT_API_KEY读取(如未配置,提示用户前往 https://skill.linkfox.com/linkfoxskills/guide.htm 申请)
请求参数
POST Body(JSON):
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| patentId | string | 条件必填 | 专利ID(专利ID和公开号两个参数必须要有一个,如果两个都存在,会优先使用专利ID),多个用英文逗号隔开,上限100条 |
| patentNumber | string | 条件必填 | 公开公告号(专利ID和公开号两个参数必须要有一个,如果两个都存在,会优先使用专利ID),多个用英文逗号隔开,上限100条 |
| replaceByRelated | string | 否 | 当前专利PDF无法获取时是否用同族专利的PDF替代:1 是,0 否 |
响应结构
| 字段 | 类型 | 说明 |
|---|---|---|
| total | integer | 记录数 |
| data | array | 专利列表,每个元素包含PDF信息 |
| data[].patentId | string | 专利ID |
| data[].pn | string | 公开(公告)号 |
| data[].pdfPath | string | PDF全文下载地址(含path等信息) |
| data[].pnRelated | string | 替代专利的公开号(仅当使用同族专利替代时提供) |
| columns | array | 渲染的列定义 |
| costToken | integer | 消耗token |
| type | string | 渲染的样式 |
错误码
正常情况下,接口的 HTTP 状态码均为 200,业务的成功与否通过响应体中的 errorCode 字段区分(errorCode = 200 表示成功,其他值表示业务错误)。当遇到未授权等情况时,HTTP 状态码为 401,且对应的 errorCode 也是 401。
| errcode | 含义 | 处理建议 |
|---|---|---|
| 200 | 成功 | 正常解析 data 等业务字段 |
| 401 | 认证失败 | 检查请求头 Authorization 是否正确携带 API Key;API Key 申请方式请参考上述调用规范下的认证方式。 |
| 其他非200值 | 业务异常 | 参考 errmsg 字段获取具体错误原因 |
错误响应示例:
{
"errcode": 401,
"errmsg": "authorized error"
}curl 示例
通过公开号查询
curl -X POST https://tool-gateway.linkfox.com/zhihuiya/pdfData \
-H "Authorization: $LINKFOXAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"patentNumber": "US20230012345A1"}'通过专利ID查询并启用同族替代
curl -X POST https://tool-gateway.linkfox.com/zhihuiya/pdfData \
-H "Authorization: $LINKFOXAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"patentId": "12345678", "replaceByRelated": "1"}'批量查询多个公开号
curl -X POST https://tool-gateway.linkfox.com/zhihuiya/pdfData \
-H "Authorization: $LINKFOXAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"patentNumber": "CN115000000A,CN115000001A,CN115000002A"}'---
Feedback API
This endpoint is separate from the tool API above. Do not mix the two base URLs.
- POST
https://skill-api.linkfox.com/api/v1/public/feedback - Content-Type:
application/json
{
"skillName": "linkfox-xxx-xxx",
"sentiment": "POSITIVE",
"category": "OTHER",
"content": "Results were accurate, user was satisfied."
}Field rules:
skillName: Use this skill'snamefrom the YAML frontmattersentiment: Choose ONE —POSITIVE(praise),NEUTRAL(suggestion without emotion),NEGATIVE(complaint or error)category: Choose ONE —BUG(malfunction or wrong data),COMPLAINT(user dissatisfaction),SUGGESTION(improvement idea),OTHERcontent: Include what the user said or intended, what actually happened, and why it is a problem or praise
#!/usr/bin/env python3
"""
Skill response I/O helper — wraps any main script to persist large API
responses to disk, then offers a `read` subcommand to extract specific fields
from those persisted files. Generic, business-agnostic.
This script is bundled into each skill's scripts/ directory by tools/response_io/sync.py.
The agent must pass --script <path> to identify which main script to execute.
Usage:
python scripts/response_io.py run --script <PATH> --out-dir <DIR> '<json_params>' [--label NAME] [--timeout SEC]
python scripts/response_io.py read <file> (--path "<JMESPath>" | --fields "f1,f2,...") [--limit N] [--offset M] [--format json|jsonl|csv|table]
"""
from __future__ import annotations
import sys
if sys.version_info < (3, 10):
sys.exit(
"Error: Python 3.10+ required (current: "
f"{sys.version_info.major}.{sys.version_info.minor}). "
"Please upgrade Python."
)
import argparse
import csv
import io
import json
import os
import re
import secrets
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Any
# Force UTF-8 stdout/stderr so non-ASCII chars in previews and API responses
# print correctly on Windows (default cp936 / gbk).
for stream in (sys.stdout, sys.stderr):
try:
stream.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
except (AttributeError, OSError):
pass
try:
import jmespath # type: ignore
HAS_JMESPATH = True
except ImportError:
HAS_JMESPATH = False
MAX_STRING_LEN = 120
MAX_DEPTH = 3
SAMPLE_KEY_CAP = 15
RAW_TEXT_PEEK = 500
DEFAULT_TIMEOUT_SEC = 300
# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------
def _err(msg: str, code: int = 1) -> None:
print(msg, file=sys.stderr)
sys.exit(code)
def _resolve_script(script_arg: str) -> Path:
p = Path(script_arg).expanduser()
if not p.is_absolute():
# Resolve relative to the current working directory the agent invoked from.
p = (Path.cwd() / p).resolve()
else:
p = p.resolve()
if not p.is_file():
_err(f"--script path not found: {p}")
return p
def _resolve_skill_name(main_script: Path) -> str:
"""Best-effort skill name extraction for filename prefixing.
main_script lives at <skill_dir>/scripts/<name>.py — return <skill_dir>'s
folder name. Fall back to the script's stem if structure differs.
"""
try:
if main_script.parent.name == "scripts":
return main_script.parents[1].name
except IndexError:
pass
return main_script.stem
def _sanitize_label(label: str) -> str:
"""Allow only safe filename chars in --label to prevent path traversal."""
cleaned = re.sub(r"[^\w\-]", "_", label)
return cleaned[:64] # cap length
def _truncate_string(s: str) -> str:
if len(s) <= MAX_STRING_LEN:
return s
return s[:MAX_STRING_LEN] + f"...(truncated, total {len(s)} chars)"
def _truncate_value(value: Any, depth: int = 0) -> Any:
"""Recursively truncate strings, deep nesting, and large arrays for preview."""
if depth >= MAX_DEPTH:
if isinstance(value, dict):
return f"<truncated nested object, keys: {list(value.keys())[:10]}>"
if isinstance(value, list):
return f"<truncated nested array, length: {len(value)}>"
if isinstance(value, str):
return _truncate_string(value)
return value
if isinstance(value, str):
return _truncate_string(value)
if isinstance(value, dict):
out = {k: _truncate_value(v, depth + 1) for k, v in value.items()}
return out
if isinstance(value, list):
if not value:
return []
truncated = [_truncate_value(value[0], depth + 1)]
if len(value) > 1:
# Note total length on the parent — keep the array type-homogeneous
# so downstream consumers can iterate without special-casing strings.
truncated.append({"_omitted_items": len(value) - 1})
return truncated
return value
def _shape_of(value: Any, top: bool = False) -> Any:
"""Lightweight schema description for the preview block."""
if isinstance(value, dict):
keys = list(value.keys())
out: dict[str, Any] = {"type": "object", "top_keys" if top else "keys": keys}
if top:
for k in keys[:8]:
out[k] = _shape_of(value[k])
return out
if isinstance(value, list):
out = {"type": "array", "length": len(value)}
if value and isinstance(value[0], dict):
out["item_keys"] = list(value[0].keys())
elif value:
out["item_type"] = type(value[0]).__name__
return out
return {"type": type(value).__name__}
def _build_sample(value: Any) -> Any:
"""First-record sample with explicit truncation marker."""
if isinstance(value, list):
if not value:
return {"_truncated_record": True, "_note": "array is empty"}
first = value[0]
if isinstance(first, dict):
sample = {"_truncated_record": True, "_note": f"first of {len(value)} items"}
sample.update(_truncate_value(first, depth=1))
return sample
return {"_truncated_record": True, "_note": f"first of {len(value)} items", "value": _truncate_value(first, depth=1)}
if isinstance(value, dict):
sample = {"_truncated_record": True, "_note": "top-level object (truncated)"}
sample.update(_truncate_value(value, depth=1))
return sample
return {"_truncated_record": True, "value": _truncate_value(value, depth=1)}
def _shrink_preview(preview: dict) -> dict:
"""Cap the sample's value fields when it has many keys.
`shape.*.item_keys` is the single source of truth for the full key list
(always complete, no truncation). The sample only ever shows up to
SAMPLE_KEY_CAP fields with their concrete values, since the agent only
needs a feel for value shapes — for the full menu of available fields,
they read `shape`.
"""
sample = preview.get("sample")
if isinstance(sample, dict):
meta_keys = {"_truncated_record", "_note"}
data_keys = [k for k in sample.keys() if k not in meta_keys]
if len(data_keys) > SAMPLE_KEY_CAP:
kept = data_keys[:SAMPLE_KEY_CAP]
new_sample = {k: v for k, v in sample.items() if k in meta_keys or k in kept}
base_note = sample.get("_note", "")
extra = (
f"showing first {SAMPLE_KEY_CAP} of {len(data_keys)} fields "
f"(see `shape` for the complete key list)"
)
new_sample["_note"] = f"{base_note}; {extra}" if base_note else extra
preview["sample"] = new_sample
return preview
# ---------------------------------------------------------------------------
# `run` subcommand
# ---------------------------------------------------------------------------
def cmd_run(args: argparse.Namespace) -> int:
main_script = _resolve_script(args.script)
skill_name = _resolve_skill_name(main_script)
out_dir = Path(args.out_dir).expanduser().resolve()
try:
out_dir.mkdir(parents=True, exist_ok=True)
except OSError as e:
_err(f"Failed to create --out-dir {out_dir}: {e}")
if not os.access(out_dir, os.W_OK):
_err(f"--out-dir is not writable: {out_dir}")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
rand = secrets.token_hex(3)
safe_label = _sanitize_label(args.label) if args.label else ""
label_part = f"__{safe_label}" if safe_label else ""
out_file = out_dir / f"{skill_name}__{timestamp}_{rand}{label_part}.json"
# Force the child process to emit UTF-8 regardless of the host console
# encoding (Windows defaults to cp936 / gbk and would otherwise corrupt
# non-ASCII bytes when we read them back).
child_env = os.environ.copy()
child_env["PYTHONIOENCODING"] = "utf-8"
timed_out = False
try:
proc = subprocess.run(
[sys.executable, str(main_script), args.params],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=child_env,
timeout=args.timeout,
)
stdout_text = proc.stdout or ""
stderr_text = proc.stderr or ""
returncode = proc.returncode
except subprocess.TimeoutExpired as e:
timed_out = True
stdout_text = (e.stdout.decode("utf-8", errors="replace") if isinstance(e.stdout, bytes) else (e.stdout or "")) or ""
stderr_text = (e.stderr.decode("utf-8", errors="replace") if isinstance(e.stderr, bytes) else (e.stderr or "")) or ""
returncode = 124 # convention for timeout
# Always write the captured stdout to disk, even if not JSON.
try:
out_file.write_text(stdout_text, encoding="utf-8")
except OSError as e:
_err(f"Failed to write output file {out_file}: {e}")
if stderr_text:
sys.stderr.write(stderr_text)
# Try to parse the captured stdout as JSON for the preview.
try:
parsed = json.loads(stdout_text) if stdout_text.strip() else None
format_kind = "json"
except json.JSONDecodeError:
parsed = None
format_kind = "raw_text"
preview: dict[str, Any] = {
"_preview": {
"is_preview": True,
"warning": (
"PREVIEW ONLY — NOT FULL DATA. The full response is saved to `file`. "
"Use `python scripts/response_io.py read <file> --fields '...'` to extract "
"specific fields, or `--path '<JMESPath>'` for complex projections."
),
},
}
# Surface failures prominently so agents don't mistake a stub preview for success.
if returncode != 0 or timed_out:
stderr_snippet = stderr_text[-500:] if stderr_text else ""
preview["_error"] = {
"exit_code": returncode,
"timed_out": timed_out,
"stderr_snippet": stderr_snippet,
"hint": "The wrapped script failed or timed out. The output file may be empty or partial.",
}
preview.update({
"file": str(out_file),
"size_bytes": out_file.stat().st_size,
"skill": skill_name,
"exit_code": returncode,
"format": format_kind,
"label": safe_label or None,
"next_steps_hint": (
"use: python scripts/response_io.py read <file> --fields '...' | --path '...'"
),
})
if format_kind == "json":
preview["shape"] = _shape_of(parsed, top=True)
preview["sample"] = _build_sample(parsed)
else:
peek = stdout_text[:RAW_TEXT_PEEK]
preview["raw_text_peek"] = peek
preview["raw_text_total_chars"] = len(stdout_text)
preview["sample"] = {
"_truncated_record": True,
"_note": f"stdout was not valid JSON; first {RAW_TEXT_PEEK} chars shown above in raw_text_peek",
}
preview = _shrink_preview(preview)
print(json.dumps(preview, ensure_ascii=False, indent=2))
return returncode
# ---------------------------------------------------------------------------
# `read` subcommand
# ---------------------------------------------------------------------------
def _load_json(path: Path) -> Any:
try:
text = path.read_text(encoding="utf-8")
except OSError as e:
_err(f"Failed to read file {path}: {e}")
try:
return json.loads(text)
except json.JSONDecodeError as e:
_err(f"File is not valid JSON: {path}\n{e}")
def _basic_dot_path(data: Any, path: str) -> Any:
"""Pure-stdlib dot-path resolver. No [*] support — callers fall back here only when jmespath is unavailable AND the path has no [*]."""
cur = data
for part in path.split("."):
if isinstance(cur, dict):
cur = cur.get(part)
else:
return None
return cur
def _resolve_field(data: Any, expr: str) -> Any:
if HAS_JMESPATH:
return jmespath.search(expr, data)
if "[" in expr or "*" in expr:
_err(
f"jmespath is required for expression '{expr}'. "
f"Install with: pip install jmespath"
)
return _basic_dot_path(data, expr)
def _project_fields(data: Any, fields: list[str]) -> Any:
"""Run each field expr; if any returns a list, zip them into list-of-dicts."""
resolved: dict[str, Any] = {f: _resolve_field(data, f) for f in fields}
list_lengths = [len(v) for v in resolved.values() if isinstance(v, list)]
if not list_lengths:
return resolved
# All list values must be same length to zip cleanly.
if len(set(list_lengths)) > 1:
# Fallback: return the dict as-is so caller can inspect mismatches.
return resolved
n = list_lengths[0]
rows = []
for i in range(n):
row = {}
for f, v in resolved.items():
row[f] = v[i] if isinstance(v, list) else v
rows.append(row)
return rows
def _apply_slice(value: Any, limit: int | None, offset: int | None) -> Any:
if not isinstance(value, list):
return value
start = offset or 0
end = (start + limit) if limit is not None else None
return value[start:end]
def _format_output(value: Any, fmt: str) -> str:
if fmt == "json":
return json.dumps(value, ensure_ascii=False, indent=2)
if fmt == "jsonl":
if isinstance(value, list):
return "\n".join(json.dumps(item, ensure_ascii=False) for item in value)
return json.dumps(value, ensure_ascii=False)
if fmt in ("csv", "table"):
if not isinstance(value, list) or not value:
_err(f"--format {fmt} requires a non-empty list result")
if not all(isinstance(item, dict) for item in value):
_err(f"--format {fmt} requires list-of-objects, got list of {type(value[0]).__name__}")
keys: list[str] = []
for item in value:
for k in item.keys():
if k not in keys:
keys.append(k)
if fmt == "csv":
buf = io.StringIO()
writer = csv.DictWriter(buf, fieldnames=keys, extrasaction="ignore")
writer.writeheader()
for item in value:
writer.writerow({k: _stringify(item.get(k)) for k in keys})
return buf.getvalue().rstrip("\n")
# table: simple aligned columns
rows = [[_stringify(item.get(k)) for k in keys] for item in value]
widths = [len(k) for k in keys]
for row in rows:
for i, cell in enumerate(row):
widths[i] = max(widths[i], len(cell))
lines = [
" ".join(k.ljust(widths[i]) for i, k in enumerate(keys)),
" ".join("-" * widths[i] for i in range(len(keys))),
]
for row in rows:
lines.append(" ".join(row[i].ljust(widths[i]) for i in range(len(keys))))
return "\n".join(lines)
_err(f"Unknown --format: {fmt}")
return "" # unreachable
def _stringify(v: Any) -> str:
if v is None:
return ""
if isinstance(v, (dict, list)):
return json.dumps(v, ensure_ascii=False)
return str(v)
def cmd_read(args: argparse.Namespace) -> int:
if not args.path and not args.fields:
_err("read: either --path or --fields is required")
if args.path and args.fields:
_err("read: --path and --fields are mutually exclusive")
file_path = Path(args.file).expanduser().resolve()
data = _load_json(file_path)
if args.path:
result = _resolve_field(data, args.path)
else:
fields = [f.strip() for f in args.fields.split(",") if f.strip()]
if not fields:
_err("--fields parsed to empty list")
result = _project_fields(data, fields)
result = _apply_slice(result, args.limit, args.offset)
print(_format_output(result, args.format))
return 0
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(
prog="response_io.py",
description="Persist large skill API responses to disk and read fields on demand.",
)
sub = parser.add_subparsers(dest="cmd", required=True)
p_run = sub.add_parser(
"run",
help="Execute a main script and persist its stdout to a file; "
"print only a lightweight preview to stdout.",
)
p_run.add_argument("params", help="JSON params string passed verbatim to the main script (argv[1]).")
p_run.add_argument("--script", required=True, help="Path to the main script to execute, e.g. scripts/my_api.py")
p_run.add_argument("--out-dir", required=True, help="Directory to write the response file into (created if missing).")
p_run.add_argument("--label", default=None, help="Optional filename suffix; sanitized to safe filename characters.")
p_run.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT_SEC, help=f"Subprocess timeout in seconds (default: {DEFAULT_TIMEOUT_SEC}).")
p_run.set_defaults(func=cmd_run)
p_read = sub.add_parser(
"read",
help="Extract specific fields from a previously persisted response file.",
)
p_read.add_argument("file", help="Path to the persisted JSON response file.")
g = p_read.add_mutually_exclusive_group()
g.add_argument("--path", default=None, help="JMESPath expression, e.g. 'data[*].{asin: asin, title: title}'.")
g.add_argument("--fields", default=None, help="Comma-separated field paths, e.g. 'data[*].asin,data[*].title'.")
p_read.add_argument("--limit", type=int, default=None, help="Take at most N items (when result is a list).")
p_read.add_argument("--offset", type=int, default=None, help="Skip the first M items (when result is a list).")
p_read.add_argument("--format", choices=["json", "jsonl", "csv", "table"], default="json", help="Output format (default: json).")
p_read.set_defaults(func=cmd_read)
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
Zhihuiya Patent PDF Downloader - LinkFox Skill
Calls the zhihuiya/pdfData API endpoint to retrieve patent PDF download links.
Usage:
python zhihuiya_pdf_data.py '{"patentNumber": "US20230012345A1"}'
python zhihuiya_pdf_data.py '{"patentId": "12345678", "replaceByRelated": "1"}'
python zhihuiya_pdf_data.py '{"patentNumber": "CN115000000A,CN115000001A,CN115000002A"}'
"""
import json
import os
import sys
from urllib.request import urlopen, Request
from urllib.error import HTTPError, URLError
API_URL = "https://tool-gateway.linkfox.com/zhihuiya/pdfData"
def get_api_key():
"""Retrieve the API key from environment, with a friendly prompt if missing."""
key = os.environ.get("LINKFOXAGENT_API_KEY")
if not key:
print(
"API Key not configured. Please complete authorization first:\n"
"1. Visit https://skill.linkfox.com/linkfoxskills/guide.htm to obtain your Key\n"
"2. Set the environment variable: export LINKFOXAGENT_API_KEY=your-key-here",
file=sys.stderr,
)
sys.exit(1)
return key
def validate_params(params: dict):
"""Ensure at least one of patentId or patentNumber is provided."""
patent_id = params.get("patentId", "").strip()
patent_number = params.get("patentNumber", "").strip()
if not patent_id and not patent_number:
print(
"Error: At least one of 'patentId' or 'patentNumber' must be provided.",
file=sys.stderr,
)
sys.exit(1)
# Validate batch limit (max 100 entries per field)
for field_name in ("patentId", "patentNumber"):
value = params.get(field_name, "").strip()
if value:
count = len(value.split(","))
if count > 100:
print(
f"Error: '{field_name}' contains {count} entries, "
f"but the maximum is 100.",
file=sys.stderr,
)
sys.exit(1)
# Validate replaceByRelated if present
replace = params.get("replaceByRelated", "").strip()
if replace and replace not in ("0", "1"):
print(
"Error: 'replaceByRelated' must be '0' or '1'.",
file=sys.stderr,
)
sys.exit(1)
def call_api(params: dict) -> dict:
"""Call the tool gateway API and return the parsed JSON response."""
api_key = get_api_key()
data = json.dumps(params).encode("utf-8")
req = Request(
API_URL,
data=data,
headers={
"Authorization": api_key,
"Content-Type": "application/json",
"User-Agent": "LinkFox-Skill/1.0",
},
method="POST",
)
try:
with urlopen(req, timeout=60) as response:
return json.loads(response.read().decode("utf-8"))
except HTTPError as e:
body = e.read().decode("utf-8") if e.fp else ""
return {"error": f"HTTP {e.code}: {e.reason}", "details": body}
except URLError as e:
return {"error": f"Connection failed: {e.reason}"}
def main():
if len(sys.argv) < 2:
print("Usage: zhihuiya_pdf_data.py '<JSON parameters>'", file=sys.stderr)
print(
"Example: zhihuiya_pdf_data.py "
"'{\"patentNumber\": \"US20230012345A1\"}'",
file=sys.stderr,
)
print(
"Example: zhihuiya_pdf_data.py "
"'{\"patentId\": \"12345678\", \"replaceByRelated\": \"1\"}'",
file=sys.stderr,
)
sys.exit(1)
# Parse the JSON argument
try:
params = json.loads(sys.argv[1])
except json.JSONDecodeError as e:
print(f"Invalid parameter format: {e}", file=sys.stderr)
sys.exit(1)
# Validate required parameters
validate_params(params)
# Call the API and print the result
result = call_api(params)
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
Related skills
How it compares
Use linkfox-zhihuiya-pdf for PDF retrieval—not linkfox-zhihuiya-claim-data or legal-status skills which return text metadata instead of documents.
FAQ
How many patents can linkfox-zhihuiya-pdf fetch per request?
linkfox-zhihuiya-pdf accepts up to 100 comma-separated patentId or patentNumber values per API call through the LinkFox gateway. When both identifiers are provided for a patent, patentId takes precedence over publication number per Zhihuiya API rules.
What API key does linkfox-zhihuiya-pdf require?
linkfox-zhihuiya-pdf requires the LINKFOXAGENT_API_KEY environment variable set before invocation. The skill calls the LinkFox tool gateway API or runs scripts/zhihuiya_pdf_data.py directly, returning pdfPath download URLs in the response data array.