1 引言

接入第三方 API 时,最麻烦的往往不是写调用代码,而是搞清楚"有哪些接口、路径是什么、参数怎么传"。传统做法是维护一份静态文档,但文档和实际接口之间容易脱节——接口改了路径、换了参数名,文档没同步,调用就废了。

docs.csqaq.com 提供了 39 个 API 端点用于查询游戏商品的价格、库存、排行等数据。但这些端点的文档散落在 39 个独立页面中,没有任何统一入口。如果要接入这些接口,传统方式是手动翻阅每个文档页面、把路径和参数复制出来——工作量大、易出错、接口更新后也无法及时感知。

CSQAQ Market Lookup 这个 Skill 解决的核心问题是:不要手动维护接口列表,让工具从文档站点的 sitemap 实时同步。所有接口信息来自 docs.csqaq.com 的实际页面,文档和代码始终保持一致。

2 核心问题

docs.csqaq.com 的文档结构:

  • /sitemap.xml — 列出所有文档页面 URL
  • /api-{id}.md — 每个接口对应一个 Markdown 页面,页面内嵌 OpenAPI YAML 定义

手动维护接口列表的问题在于:接口一旦更新,列表就过时了。而且每次要翻文档页面、复制路径参数,工作量大。

理想工作流:从 sitemap 发现所有文档 → 从每个页面提取 OpenAPI 定义 → 合并成统一目录 → 直接用目录信息调用接口

3 整体架构

工具分为三层:

docs.csqaq.com
    ↓
┌──────────────────────────────────────────┐
│  Sync 层:sitemap → 文档页面 → YAML      │
│  输入:sitemap.xml                         │
│  输出:endpoints.json / openapi-merged.json│
└──────────────────────────────────────────┘
    ↓
┌──────────────────────────────────────────┐
│  索引层:endpoints.json(接口目录)        │
│  字段:doc_id, path, method,             │
│        operationId, summary, tags         │
└──────────────────────────────────────────┘
    ↓
┌──────────────────────────────────────────┐
│  调用层:CLI(sync / list / call)        │
│  用途:查询接口、发起真实 HTTP 调用       │
└──────────────────────────────────────────┘

4 Sync 层:从 sitemap 到接口目录

4.1 从 sitemap 发现文档 ID

sitemap.xml 中的 URL 形如 https://docs.csqaq.com/api-327138094,数字部分即为文档 ID。用正则从 sitemap 中提取所有这类 URL:

API_URL_RE = re.compile(r"^https://docs\.csqaq\.com/api-(\d+)$")

def parse_api_doc_ids(sitemap_xml: str) -> list[str]:
    root = ET.fromstring(sitemap_xml)
    for node in root.findall(".//sm:url/sm:loc", ns):
        loc = node.text.strip()
        if match := API_URL_RE.match(loc):
            doc_ids.append(match.group(1))
    return doc_ids

拿到所有 doc_id 后,逐个抓取对应页面 /api-{doc_id}.md

4.2 从 Markdown 中提取 OpenAPI YAML

文档页面内嵌一段 YAML 格式的 OpenAPI 定义,用代码块包裹。提取逻辑:

FENCED_YAML_RE = re.compile(r"```yaml\s*(.*?)\s*```", re.DOTALL)

def extract_openapi_yaml(markdown_text: str) -> dict:
    match = FENCED_YAML_RE.search(markdown_text)
    if not match:
        raise ValueError("No fenced YAML block found.")
    return yaml.safe_load(match.group(1))

每个文档页面解析后得到一个独立的 OpenAPI spec dict。

4.3 合并多个 OpenAPI spec

39 个文档得到 39 个独立 spec,需要合并成一份统一文档。合并逻辑处理了三个关键问题:

路径合并:遍历每个 spec 的 paths 字段,按 path + method 组合写入合并结果。如果同一 path + method 在多个 spec 中出现,记录为 collision,强制提示人工介入。

for doc_id, spec in specs:
    for path, path_item in spec["paths"].items():
        for method, operation in path_item.items():
            key = (path, method.lower())
            if key in merged_paths:
                collisions.append({"path": path, "method": method, "doc_id": doc_id})
                continue  # 跳过重复定义,不做 silent覆盖
            merged_paths[key] = operation

Schema 合并components/schemas 字段去重合并,同名字段保留第一个出现的定义。

Security Schemes 合并:同上。

最终输出三份文件:

文件 内容
endpoints.json 扁平化接口目录(39 条记录)
openapi-merged.json 合并后完整 OpenAPI 文档
sync-report.json 同步报告(成功/失败数、碰撞检测结果)

4.4 Collision 检测

合并多个 spec 时,同一 path + method 出现多次,不直接覆盖,而是记录到 collision 列表:

collisions.append({
    "doc_id": doc_id,
    "path": path,
    "method": method_lower,
    "reason": "duplicate path+method"
})

这防止了 silent override——两个文档定义同一接口时,应该提示人工判断,而不是默默取后者覆盖前者。

实际同步结果:39 个文档,0 个 collision。文档结构本身是干净的。

5 索引层:endpoints.json 的结构

endpoints.json 是查询基础,每条记录:

字段 含义
doc_id 文档页面 ID(URL 中的数字)
doc_url 文档主页 URL
doc_md_url 文档原始 Markdown URL
path 接口路径,如 /api/v1/current_data
method HTTP 方法(GET/POST/PUT 等)
operationId OpenAPI operationId(文档内唯一标识)
summary 接口描述
tags 接口标签分组
deprecated 是否已废弃
requires_api_token 是否需要 Token 认证

全部来自文档页面的 OpenAPI YAML,无人工添加内容。

6 调用层:三种端点解析路径

6.1 通过 operationId 定位

python scripts/csqaq_api.py call \
  --operation-id ______________api_v1_current_data_get

operationId 全局唯一时直接命中。

6.2 通过 path + method 定位

python scripts/csqaq_api.py call \
  --path /api/v1/current_data \
  --method GET

只知道接口路径时使用。

6.3 通过 docId + path + method 定位

python scripts/csqaq_api.py call \
  --doc-id 327138094 \
  --path /api/v1/current_data \
  --method GET

多个文档中存在同名 path + method 时,用 --doc-id 精确指定。

三种方式最终都解析成统一的 (path, method) 对,再组装成真实 HTTP 请求。

7 Token 管理与请求构建

API Token 通过环境变量 CSQAQ_API_TOKEN 注入,CLI 自动注入到 HTTP Header:

token = args.api_token or os.getenv(args.token_env)
if token and not has_apitoken_header:
    headers["ApiToken"] = token

不需要调用方手动处理 Header。也可用 --api-token 参数直接传入,覆盖环境变量。

请求构建支持 query 参数、header 参数,以及三种 body 格式:

# query 参数
--query type=init

# 三种 body 格式(Content-Type 自动选择)
--json-body '{"key": "value"}'   # application/json
--body-file /path/to/file         # multipart/form-data
--raw-body "raw text content"    # text/plain

8 关键技术决策

8.1 不硬编码接口

传统 API 集成方案的问题:接口列表是项目开始时写的,之后没人管。接口改路径、下线、重命名,代码里的 endpoint URL 慢慢就变成隐藏的 bug。

本工具的做法:接口目录本身是工具的输入,而不是输出。每次运行 sync 都重新从 docs.csqaq.com 拉取最新文档,接口变了同步就会反映最新状态。

8.2 collision 检测优先

合并多个 spec 时,同一 path + method 出现多次,不 silent override,而是记录 collision 强制提示人工介入。

8.3 无额外依赖

同步脚本只依赖 Python 标准库(urllib、xml.etree、json、re、argparse)和 PyYAML。不需要 requests、httpx 等第三方包,有 Python 环境就能跑。

8.4 解析容错

Markdown 中 YAML 块格式不一定完全规范。解析用 yaml.safe_load,单个文档失败只记录到报告中,不影响其他文档的同步。

9 同步结果

指标 数值
sitemap 发现文档数 39
成功同步数 39
同步失败数 0
提取端点数 39
路径+方法碰撞 0

39 个文档全部成功同步,无失败、无碰撞。接口覆盖实时数据查询、商品信息、价格数据、排行榜等多个业务模块。

10 适用场景与局限性

适用:需要接入 CSQAQ API 且希望保持接口文档与代码同步的场景;接口数量多、人工维护成本高的团队。

局限:依赖文档站点的 sitemap 结构,文档 URL 格式变化时需同步更新解析正则;工具本身不处理 API 响应解析和结果缓存。

11 总结

这个工具的核心不是 HTTP 调用逻辑,而是保证调用方拿到的接口信息永远和文档站点保持一致

三个关键设计:

  1. sitemap 先行:从 sitemap 发现文档,不依赖人工 URL 列表
  2. OpenAPI YAML 解析:从 Markdown 中提取结构化定义,避免手工录入
  3. collision 保护:合并时强制检测重复定义,防止 silent data corruption

Skill 目录:https://skillhub.cn/skills/csqaq-market-lookup