
Alapi
- 84 installs
- 2 repo stars
- Updated April 9, 2026
- alapi-sdk/skill
alapi is a Claude Code skill that helps developers search ALAPI (alapi.cn) endpoints, read their OpenAPI docs, and generate integration code for the platform.
About
alapi is a Chinese-language skill that helps developers integrate the ALAPI platform (alapi.cn). It searches endpoints, reads their OpenAPI specs, extracts parameters, and generates minimal working integration code in Python, JS/TS, PHP, or Go. A developer uses it when wiring an app to ALAPI endpoints. All access goes through a zero-dependency scripts/alapi.py CLI, and it only makes real API calls when the user explicitly provides a token and confirms.
- Searches ALAPI (alapi.cn) endpoints, reads their OpenAPI docs, extracts parameters, and generates integration code
- Runs all endpoint access through a zero-dependency scripts/alapi.py CLI (search, explore, detail, openapi, call)
- Only makes real API calls when the user explicitly provides a token and confirms
Alapi by the numbers
- 84 all-time installs (skills.sh)
- Ranked #3,035 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
alapi capabilities & compatibility
Free to search and generate code; real API calls require a user-provided ALAPI token
- Capabilities
- api integration · code generation
- Use cases
- api development
- Pricing
- Bring your own API key
What alapi says it does
Base URL: `https://v3.alapi.cn`
所有接口数据获取通过 [alapi.py](./scripts/alapi.py) 完成。脚本零依赖,优先用它
npx skills add https://github.com/alapi-sdk/skill --skill alapiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 84 |
|---|---|
| repo stars | ★ 2 |
| Last updated | April 9, 2026 |
| Repository | alapi-sdk/skill ↗ |
What it does
Search ALAPI endpoints, read their OpenAPI docs, and generate integration code for the ALAPI platform.
Who is it for?
Integrating the ALAPI platform's endpoints into an application
Skip if: Calling ALAPI without an explicitly provided token or generating final code before a candidate endpoint is confirmed
When should I use this skill?
A user mentions ALAPI, alapi.cn, an ALAPI doc URL or token, or wants to integrate ALAPI endpoints
What you get
The correct ALAPI endpoint is located and minimal working integration code is generated from its OpenAPI spec.
- Minimal runnable ALAPI integration code
- Endpoint candidate lists and OpenAPI-derived parameter details
By the numbers
- 6 CLI commands (search, explore, detail, openapi, call, --json)
- Base URL https://v3.alapi.cn
Files
ALAPI 接口对接助手
Overview
使用这个 skill 处理 ALAPI 平台的接口检索、文档读取、参数提取、代码生成和按需真实调用。
ALAPI 的关键平台约定:
{
"success": true,
"code": 200,
"message": "success",
"data": { ... },
"request_id": "xxx",
"time": 1700000000
}- Base URL:
https://v3.alapi.cn - 文档页:
https://www.alapi.cn/api/{id}/introduction - Token 创建:
https://www.alapi.cn/dashboard/data/token - 所有接口都要传
token参数,通常放在 GET query 或 POST body - token 优先级:显式
--token高于环境变量ALAPI_TOKEN
所有接口数据获取通过 alapi.py 完成。脚本零依赖,优先用它,不要手工拼 ALAPI 的内部文档接口。
不要假设当前工作目录一定是 skill 仓库根目录,也不要写死旧的仓库内相对路径。优先从当前 skill 目录推导脚本路径:
SKILL_DIR=/absolute/path/to/alapi
SCRIPT="$SKILL_DIR/scripts/alapi.py"
python3 "$SCRIPT" --json search "IP查询"Quick Workflow
用户给了 ALAPI 文档 URL
从 URL 中提取 api/{id},然后直接读取 OpenAPI:
python3 "$SCRIPT" --json openapi {id}用户只描述了功能
先搜索,再决定是否继续:
python3 "$SCRIPT" --json search "用户描述的关键词"如果搜索结果为空,不要立刻放弃。按这个顺序做关键词回退:
1. 去掉修饰词,只保留核心名词 2. 改用更短的同义词或上位词重新搜索 3. 对组合词拆词搜索,例如先搜“视频”,再搜“解析”,再搜“短视频”
只有在 2 到 3 轮回退后仍然没有结果时,才告诉用户当前没有匹配接口。
如果命中多个结果,先列出 2 到 5 个候选,不要擅自选一个。用户确认后再读取 OpenAPI:
python3 "$SCRIPT" --json openapi {id}用户要浏览全部接口
python3 "$SCRIPT" --json explore用户要代码,不要真实请求
优先读取 openapi,再从结果中提取:
- 请求路径
- 请求方法
- 必填参数
- 可选参数
- 响应结构
- 文档链接
https://www.alapi.cn/api/{id}/introduction
如果当前只是候选阶段,不要直接给“最终确定版代码”。最多只给:
- 候选列表
- 候选差异
- 确认后会生成哪种语言的代码
除非只有单一明确命中,或者用户已经确认了具体接口。
用户明确要求真实调用
仅在用户明确要求“直接调用/测试接口”且已提供 token 时执行:
python3 "$SCRIPT" --json call {path} --token {用户的token} --param key=value如果用户没有 token,引导去创建:
请先在 https://www.alapi.cn/dashboard/data/token 创建 API Token。
如果用户没有显式给出 token,但环境里已经有 ALAPI_TOKEN,可以直接使用;如果两者都存在,始终以显式 token 为准。
Critical Rules
- 默认优先做只读操作:
search、detail、openapi、explore - 优先使用
--json,再基于 JSON 结果生成自然语言说明 - 不要手工猜 ALAPI 文档接口路径,优先使用
scripts/alapi.py - 不要假设 cwd 一定在 skill 根目录
- 不要在最终回复中回显用户的真实 token
- 支持从环境变量
ALAPI_TOKEN读取 token,但显式--token优先级最高 - 搜索结果不唯一时,先列候选,不要擅自决定接口
- 搜索 0 结果时,先做关键词回退,不要第一步就判定不存在
- 用户只要“文档”或“代码示例”时,不要真实调用接口
- 候选未确认前,不要输出“最终定稿代码”;先输出候选和差异
- 如果用户没有提供足够参数,不要猜值,明确指出缺失参数
- 如果 OpenAPI 没有写清楚,不要补脑生成不存在的参数名、默认值或响应字段
call失败时,优先保留平台原始错误结构,再结合错误码给出下一步建议- 对需要真实参数才能调用的接口,如果用户未给足参数,先说明缺什么,不要猜值
Command Reference
| 命令 | 用途 |
|---|---|
search <keyword> | 搜索接口 |
explore | 浏览全部接口 |
detail <id> | 读取接口基础信息 |
openapi <id> | 读取 OpenAPI 规格 |
call <path> | 发起真实请求 |
--json | 输出稳定 JSON,供 agent 继续处理 |
Code Generation Rules
- Token 占位符统一使用
ALAPI_TOKEN - 默认生成最小可运行版本,不要过度封装
- 保留响应解析和错误处理逻辑,覆盖
success/code/message/data/request_id/time - Python 用
requests;JS/TS 用fetch;PHP 用curl;Go 用net/http - 不引入非必要依赖
- 如果用户在服务端项目中接入,优先建议把 token 放到环境变量或服务端配置,不要放前端
- 如果参数或方法来自 OpenAPI,就按 OpenAPI 生成;如果 OpenAPI 缺失,不要伪造
- 详细模板见 code-examples.md
前端/后端分层规则:
- 浏览器端或 Next.js Client Component 默认不要直连 ALAPI
- Node.js 服务端、PHP 后端、Python 后端可以直接读取
ALAPI_TOKEN - 如果用户说“前端项目接入”,优先给服务端代理方案,除非用户明确说明是在纯后端环境运行
Reporting Template
当文档不完全、存在候选、或存在不确定字段时,优先按这个结构输出:
已确认
- 从 OpenAPI 或真实调用中明确确认的接口路径、方法、参数、返回字段
待确认
- 当前还需要用户确认的候选接口或业务选择
不要假设
- 文档未写清楚,因此不能主动编造的参数、默认值、返回字段或平台行为
Live Call Rules
执行真实调用后,优先保留这些信息:
- 接口名和路径
- token 来源:显式
--token还是环境变量ALAPI_TOKEN - 请求里实际传入的业务参数
- 原始响应中的关键字段:
success、code、request_id、data摘要
不要保留这些信息:
- token 原文
- 不必要的敏感上下文
- 会导致凭证泄露的命令历史
Real Usage Examples
帮我查一下 IP: 8.8.8.8
先定位 IP 查询接口,再在用户明确要求真实调用且 token 可用时执行查询,或先给出查询代码。
帮我生成视频解析的代码
先搜索“视频解析”相关接口,若命中多个候选先列给用户确认,再基于对应 OpenAPI 生成最小可运行代码。
Failure Handling
| code | 含义 | 建议 |
|---|---|---|
| 200 | 成功 | — |
| 401 | Token 无效或未传 | 检查 token |
| 403 | 套餐不含此接口 | 升级套餐 |
| 422 | 参数校验失败 | 优先指出缺失或格式错误的参数 |
| 429 | 频率超限 | 降频、加缓存,必要时延迟重试 |
| 500 | 服务端错误 | 保留原始错误,建议重试 |
如果搜索结果为空,直接说明没有匹配接口,并提示改用更明确的关键词。
如果 openapi 缺失但 detail 或文档页存在,明确告诉用户规范不完整,并退回基础信息 + 文档链接。
__pycache__/
*.pyc
interface:
display_name: "ALAPI 接口助手"
short_description: "搜索 ALAPI 文档、提取参数并生成可直接使用的对接代码"
default_prompt: "Use $alapi to find the right ALAPI endpoint, summarize its parameters, and generate a working integration example."
{
"skill_name": "alapi",
"evals": [
{
"id": 1,
"prompt": "这是 ALAPI 的接口文档链接:https://www.alapi.cn/api/23/introduction 。别直接发请求,先帮我看一下这个接口是做什么的,需要哪些参数,返回结构大概是什么,再顺手给我一个 Python 调用示例。",
"expected_output": "识别出文档 URL 中的接口 ID,读取对应 OpenAPI 或接口信息,概述接口用途、参数和响应结构,并生成最小可运行的 Python 示例代码。",
"files": [],
"expectations": [
"The response identifies the ALAPI interface ID from the provided documentation URL.",
"The response summarizes the endpoint purpose, required parameters, and response structure.",
"The response includes a Python example for calling the ALAPI endpoint.",
"The response does not perform or claim a live API call."
]
},
{
"id": 2,
"prompt": "我想接一个 ALAPI 的 IP 查询能力,目标 IP 是 8.8.8.8。先不要真的请求接口,帮我找到合适的接口,然后给我一份 cURL 和 Node.js 的调用示例,token 用环境变量 ALAPI_TOKEN。",
"expected_output": "通过搜索找到合适的 IP 查询接口,在不发起真实调用的前提下,给出 cURL 和 Node.js 示例,并使用 ALAPI_TOKEN 作为 token 读取方式。",
"files": [],
"expectations": [
"The response selects or recommends an ALAPI IP lookup endpoint.",
"The response includes a cURL example that uses ALAPI_TOKEN rather than a hard-coded token literal.",
"The response includes a JavaScript or Node.js example for the same endpoint.",
"The response does not claim to have queried IP 8.8.8.8 live."
]
},
{
"id": 3,
"prompt": "帮我找 ALAPI 里跟视频解析相关的接口,并直接给我代码。如果有多个候选,不要替我做决定,先把候选列出来并说明差异。",
"expected_output": "如果“视频解析”直接搜索无结果,应主动做关键词回退或拆词搜索;找到候选后应列出候选和差异,而不是直接假定某个接口;在用户确认前不应输出最终定稿代码。",
"files": [],
"expectations": [
"If the original keyword search returns no result, the response falls back to better search terms instead of stopping immediately.",
"If multiple candidate endpoints exist, the response presents multiple options instead of assuming one.",
"The response explains at least one meaningful difference between candidates or explicitly states what is unknown.",
"Before the user confirms a candidate, the response does not output a final committed code sample for a single endpoint."
]
},
{
"id": 4,
"prompt": "我已经有 ALAPI_TOKEN 环境变量了,帮我测试一下 hitokoto 接口,参数 type=a。你可以直接调用,但不要在最终结果里泄露我的 token。除了返回结果,也告诉我你是怎么拿到 token 的优先级规则。",
"expected_output": "在允许真实调用的情况下使用环境变量中的 ALAPI_TOKEN 发起请求,不回显 token,并说明显式 token 高于环境变量的优先级规则。",
"files": [],
"expectations": [
"The run uses a live API call only because the prompt explicitly allows it.",
"The response does not print or expose the raw token value.",
"The response mentions that explicit token input overrides ALAPI_TOKEN from the environment.",
"The response includes the returned API result or a faithful summary of it."
]
},
{
"id": 5,
"prompt": "给我一个 ALAPI 的视频解析接口接入方案,后端是 PHP,前端是 Next.js。不要把 token 放到前端。先说明应该怎么分层,再给我一份后端最小可运行代码示例。假设文档里如果没写清楚参数,就明确告诉我哪里不确定。",
"expected_output": "给出安全的服务端接入方案,强调不要在前端暴露 token,提供 PHP 后端最小示例,并在 OpenAPI 不清晰时明确标出不确定点。",
"files": [],
"expectations": [
"The response recommends keeping the ALAPI token on the server side rather than in Next.js client code.",
"The response includes a PHP example for the backend integration.",
"The response distinguishes confirmed parameters from unknown or undocumented details.",
"The response does not fabricate missing parameter names or default values."
]
}
]
}
ALAPI Skill
ALAPI 官方 skill,用于帮助 AI 或开发者更高效地完成 ALAPI 接口搜索、文档理解、参数提取、代码生成和按需真实调用。
这个 skill 主要解决这些问题:
- 搜索 ALAPI 接口
- 读取 ALAPI OpenAPI 和文档页
- 提取参数、返回结构和调用约定
- 生成可直接使用的对接代码
- 在用户明确允许且 token 可用时执行真实调用
适合的典型场景包括:
- 想快速找到某个能力对应的 ALAPI 接口
- 已有文档链接,想直接生成接入代码
- 需要区分已确认参数和文档未明确说明的字段
- 需要在不泄露 token 的前提下测试真实接口
仓库地址
- GitHub:
https://github.com/ALAPI-SDK/skill
安装方式
可以直接通过 npx skills add 安装:
npx skills add https://github.com/ALAPI-SDK/skill安装完成后,可按你的 skills 管理方式启用或保留这个 skill。
文件结构
SKILL.md: skill 主说明,面向 AI 的执行规则agents/openai.yaml: UI 元数据scripts/alapi.py: 零依赖 CLI,负责搜索、读文档、读 OpenAPI、真实调用references/code-examples.md: 多语言接入模板tests/test_alapi.py: 最小回归测试evals/evals.json: 评测集
这个 Skill 解决什么问题
ALAPI 接入请求经常会重复几件事:
- 找接口
- 看参数
- 确认返回结构
- 写接入代码
- 必要时做一次真实调用
这个 skill 的作用,就是把这些步骤收敛成一套更稳定、可复用的流程,避免 AI 每次都从零猜测。
行为约定
这是一个 ALAPI 专用 skill,不是通用 API skill。
默认优先执行只读操作:
searchexploredetailopenapi
只有在下面两个条件同时满足时,才执行真实 call:
1. 用户明确要求真实调用或测试接口 2. 已提供可用 token
token 优先级:
- 显式
--token最高优先级 - 否则读取环境变量
ALAPI_TOKEN
最终回复中不应回显用户的真实 token。
CLI 用法
不要假设当前工作目录就是仓库根目录。应先基于 skill 目录定位脚本:
SKILL_DIR=/absolute/path/to/alapi
SCRIPT="$SKILL_DIR/scripts/alapi.py"
python3 "$SCRIPT" --help示例:
python3 "$SCRIPT" --json search "IP查询"
python3 "$SCRIPT" --json openapi 27
python3 "$SCRIPT" --json call ip --token "$ALAPI_TOKEN" --param ip=8.8.8.8
ALAPI_TOKEN=your_token python3 "$SCRIPT" --json call ip --param ip=8.8.8.8真实使用示例
帮我查一下 IP: 8.8.8.8帮我生成视频解析的代码这个关键词搜不到,帮我换几个更合理的 ALAPI 搜索词如果视频解析有多个候选接口,先别直接写代码,先帮我确认选哪个
机器可读输出
当输出会被另一个 agent 消费,或需要串联下一步时,优先使用 --json。
支持的命令:
search <keyword>exploredetail <id>openapi <id>call <path> --token <token> [--param key=value] [--method GET|POST]
验证
运行:
python3 -m unittest discover -s tests -p 'test_*.py'
python3 /Users/anhao/.codex/skills/.system/skill-creator/scripts/quick_validate.py .说明
- 当前 README 默认以 skill 仓库根目录为基准说明命令和文件结构
- 如果你把它移动到别的仓库或 skill 目录,内部相对结构最好保持不变
- 不要在自动化流程里写死旧的仓库相对路径
ALAPI 多语言对接代码模板
以下模板用于生成对接代码。将{PATH}替换为接口路径,{PARAMS}替换为实际参数。
Token 约定:
- 默认使用环境变量
ALAPI_TOKEN - 如果同时有显式 token 和环境变量,显式 token 优先
- 生成代码时优先展示环境变量读取方式,避免硬编码密钥
---
cURL
# GET
curl "https://v3.alapi.cn/api/{PATH}?token=${ALAPI_TOKEN}&key=value"
# POST
curl -X POST "https://v3.alapi.cn/api/{PATH}" \
-H "Content-Type: application/json" \
-d '{"token": "'"${ALAPI_TOKEN}"'", "key": "value"}'---
Python (requests)
import os
import requests
BASE_URL = "https://v3.alapi.cn/api"
TOKEN = os.environ["ALAPI_TOKEN"]
def call_alapi(path: str, params: dict = None, method: str = "GET") -> dict:
"""调用 ALAPI 接口。"""
if params is None:
params = {}
params["token"] = TOKEN
url = f"{BASE_URL}/{path}"
if method == "POST":
resp = requests.post(url, json=params, timeout=10)
else:
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
result = resp.json()
if not result.get("success"):
raise Exception(f"ALAPI Error {result.get('code')}: {result.get('message')}")
return result["data"]
# 示例调用
data = call_alapi("{PATH}", {"{PARAMS}"})
print(data)---
JavaScript / TypeScript (fetch)
const BASE_URL = "https://v3.alapi.cn/api";
const TOKEN = process.env.ALAPI_TOKEN;
if (!TOKEN) {
throw new Error("Missing ALAPI_TOKEN environment variable");
}
async function callAlapi<T = any>(
path: string,
params: Record<string, string | number> = {}
): Promise<T> {
const url = new URL(`${BASE_URL}/${path}`);
url.searchParams.set("token", TOKEN);
for (const [k, v] of Object.entries(params)) {
url.searchParams.set(k, String(v));
}
const res = await fetch(url.toString());
const json = await res.json();
if (!json.success) {
throw new Error(`ALAPI Error ${json.code}: ${json.message}`);
}
return json.data as T;
}
// 示例调用
const data = await callAlapi("{PATH}", { /* {PARAMS} */ });
console.log(data);---
PHP (curl)
<?php
function callAlapi(string $path, array $params = [], string $method = 'GET'): array
{
$baseUrl = 'https://v3.alapi.cn/api';
$token = getenv('ALAPI_TOKEN');
if (!$token) {
throw new RuntimeException('Missing ALAPI_TOKEN environment variable');
}
$params['token'] = $token;
if ($method === 'POST') {
$url = $baseUrl . '/' . $path;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($params),
]);
} else {
$url = $baseUrl . '/' . $path . '?' . http_build_query($params);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
}
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
if (!$result['success']) {
throw new RuntimeException("ALAPI Error {$result['code']}: {$result['message']}");
}
return $result['data'];
}
// 示例调用
$data = callAlapi('{PATH}', [/* {PARAMS} */]);
print_r($data);---
Go (net/http)
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
const (
baseURL = "https://v3.alapi.cn/api"
)
type Response struct {
Success bool `json:"success"`
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}
func callAlapi(path string, params url.Values) (json.RawMessage, error) {
token := os.Getenv("ALAPI_TOKEN")
if token == "" {
return nil, fmt.Errorf("missing ALAPI_TOKEN environment variable")
}
params.Set("token", token)
endpoint := fmt.Sprintf("%s/%s?%s", baseURL, path, params.Encode())
resp, err := http.Get(endpoint)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result Response
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
if !result.Success {
return nil, fmt.Errorf("ALAPI Error %d: %s", result.Code, result.Message)
}
return result.Data, nil
}
func main() {
params := url.Values{}
// params.Set("key", "value") // {PARAMS}
data, err := callAlapi("{PATH}", params)
if err != nil {
panic(err)
}
fmt.Println(string(data))
}---
Java (OkHttp)
import okhttp3.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class AlapiClient {
private static final String BASE_URL = "https://v3.alapi.cn/api";
private static final String TOKEN = System.getenv("ALAPI_TOKEN");
private final OkHttpClient client = new OkHttpClient();
private final ObjectMapper mapper = new ObjectMapper();
public JsonNode callAlapi(String path, java.util.Map<String, String> params) throws Exception {
if (TOKEN == null || TOKEN.isBlank()) {
throw new IllegalStateException("Missing ALAPI_TOKEN environment variable");
}
HttpUrl.Builder urlBuilder = HttpUrl.parse(BASE_URL + "/" + path).newBuilder();
urlBuilder.addQueryParameter("token", TOKEN);
params.forEach(urlBuilder::addQueryParameter);
Request request = new Request.Builder().url(urlBuilder.build()).build();
try (Response response = client.newCall(request).execute()) {
JsonNode json = mapper.readTree(response.body().string());
if (!json.get("success").asBoolean()) {
throw new RuntimeException("ALAPI Error " + json.get("code") + ": " + json.get("message").asText());
}
return json.get("data");
}
}
}---
通用建议
- Token 安全: 不要在前端硬编码 token,使用环境变量或后端代理
- Token 优先级: 显式传入 token 高于环境变量
ALAPI_TOKEN - 重试: 对 5xx 错误做指数退避重试(最多 3 次)
- 缓存: 行情类数据缓存 60s,内容类缓存 5min
- 超时: 建议设置 10s 超时
#!/usr/bin/env python3
"""
ALAPI CLI - 接口搜索、查询文档、直接调用的命令行工具。
用法:
python alapi.py search <keyword> 搜索接口
python alapi.py explore 浏览全部接口(按分类)
python alapi.py detail <id> 获取接口基本信息
python alapi.py openapi <id> 获取接口 OpenAPI Spec
python alapi.py call <path> [options] 调用接口
调用接口选项:
--token <token> API Token(最高优先级)
--param <key=value> 请求参数(可多次使用)
--method <GET|POST> 请求方法,默认 GET
环境变量:
ALAPI_TOKEN 未指定 --token 时使用
示例:
python alapi.py search "IP"
python alapi.py openapi 27
python alapi.py call ip --token ALAPI_TOKEN --param ip=8.8.8.8
ALAPI_TOKEN=your_token python alapi.py call hitokoto --param type=a
"""
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
BASE_URL = "https://v3.alapi.cn"
OUTPUT_JSON = False
def _request(url, method="GET", data=None, headers=None):
"""发起 HTTP 请求,返回解析后的 JSON 或原始文本。"""
if headers is None:
headers = {}
headers.setdefault("User-Agent", "ALAPI-CLI/1.0")
body = None
if data and method == "POST":
body = json.dumps(data).encode("utf-8")
headers["Content-Type"] = "application/json"
elif data and method == "GET":
url = url + ("&" if "?" in url else "?") + urllib.parse.urlencode(data)
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
raw = resp.read().decode("utf-8")
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8")
try:
err = json.loads(raw)
print(json.dumps(err, indent=2, ensure_ascii=False))
except json.JSONDecodeError:
print(f"HTTP {e.code}: {raw}", file=sys.stderr)
sys.exit(1)
except urllib.error.URLError as e:
print(f"网络错误: {e.reason}", file=sys.stderr)
sys.exit(1)
try:
return json.loads(raw)
except json.JSONDecodeError:
return raw
def _emit(payload, render_text=None):
"""根据输出模式返回 JSON 或文本。"""
if OUTPUT_JSON:
print(json.dumps(payload, indent=2, ensure_ascii=False))
return
if render_text is None:
print(json.dumps(payload, indent=2, ensure_ascii=False))
return
print(render_text(payload))
def _search_payload(result):
payload = {
"command": "search",
"success": False,
"keyword": None,
"count": 0,
"items": [],
"raw": result,
}
if isinstance(result, dict) and result.get("data"):
apis = result["data"].get("list", [])
payload["success"] = True
payload["count"] = len(apis)
payload["items"] = [
{
"id": api.get("id"),
"name": api.get("name"),
"description": api.get("description"),
"doc_url": f"https://www.alapi.cn/api/{api.get('id')}/introduction",
}
for api in apis
]
return payload
def _render_search(payload):
if not payload["items"]:
return "未找到匹配的接口。"
lines = [f"找到 {payload['count']} 个接口:\n"]
for api in payload["items"]:
lines.append(f" ID: {api.get('id', '')} | {api.get('name', '')}")
desc = api.get("description")
if desc:
lines.append(f" {desc[:80]}")
lines.append(f" 文档: {api.get('doc_url')}")
lines.append("")
return "\n".join(lines).rstrip()
def _explore_payload(result):
payload = {
"command": "explore",
"success": False,
"categories": [],
"groups": [],
"raw": result,
}
if isinstance(result, dict) and result.get("data"):
data = result["data"]
payload["success"] = True
payload["categories"] = data.get("categories", [])
payload["groups"] = data.get("apis", [])
return payload
def _render_explore(payload):
if not payload["success"]:
return json.dumps(payload["raw"], indent=2, ensure_ascii=False)
lines = ["=== ALAPI 接口分类 ===\n"]
for cat in payload["categories"]:
lines.append(f" {cat.get('name', '')} ({cat.get('apis_count', 0)} 个)")
lines.append("")
for group in payload["groups"]:
lines.append(f"【{group.get('category', '')}】")
for api in group.get("apis", []):
lines.append(f" ID: {api.get('id', '')} | {api.get('name', '')}")
lines.append("")
return "\n".join(lines).rstrip()
def _detail_payload(api_id, result):
return {
"command": "detail",
"success": isinstance(result, dict) and bool(result.get("data")),
"api_id": api_id,
"data": result.get("data") if isinstance(result, dict) else None,
"raw": result,
}
def _openapi_payload(api_id, result):
return {
"command": "openapi",
"success": isinstance(result, dict),
"api_id": api_id,
"spec": result if isinstance(result, dict) else None,
"raw": result,
}
def _call_payload(path, method, result):
return {
"command": "call",
"success": isinstance(result, dict) and bool(result.get("success")),
"path": path,
"method": method,
"response": result,
}
def _resolve_token(explicit_token):
"""解析 token,显式参数优先,其次环境变量。"""
if explicit_token:
return explicit_token
return os.environ.get("ALAPI_TOKEN")
def cmd_search(keyword):
"""搜索接口。"""
url = f"{BASE_URL}/frontend/api/search"
result = _request(url, data={"keywords": keyword})
payload = _search_payload(result)
payload["keyword"] = keyword
_emit(payload, _render_search)
def cmd_explore():
"""浏览全部接口(按分类)。"""
url = f"{BASE_URL}/frontend/api/explore"
result = _request(url)
_emit(_explore_payload(result), _render_explore)
def cmd_detail(api_id):
"""获取接口基本信息。"""
url = f"{BASE_URL}/frontend/api/find/{api_id}"
result = _request(url)
payload = _detail_payload(api_id, result)
_emit(payload["data"] if not OUTPUT_JSON and payload["success"] else payload)
def cmd_openapi(api_id):
"""获取接口 OpenAPI Spec。"""
url = f"{BASE_URL}/openapi/{api_id}.json"
result = _request(url)
payload = _openapi_payload(api_id, result)
_emit(payload["spec"] if not OUTPUT_JSON and payload["success"] else payload)
def cmd_call(path, token, params, method="GET"):
"""直接调用接口。"""
token = _resolve_token(token)
if not token:
print("错误: 调用接口必须提供 --token 参数,或设置环境变量 ALAPI_TOKEN。", file=sys.stderr)
print("创建 Token: https://www.alapi.cn/dashboard/data/token", file=sys.stderr)
sys.exit(1)
url = f"{BASE_URL}/api/{path}"
params["token"] = token
if method == "POST":
result = _request(url, method="POST", data=params)
else:
result = _request(url, method="GET", data=params)
payload = _call_payload(path, method, result)
_emit(result if not OUTPUT_JSON else payload)
def main():
global OUTPUT_JSON
args = sys.argv[1:]
if "--json" in args:
OUTPUT_JSON = True
args = [arg for arg in args if arg != "--json"]
if len(args) < 1:
print(__doc__)
sys.exit(0)
command = args[0].lower()
if command == "search":
if len(args) < 2:
print("用法: python alapi.py search <keyword>", file=sys.stderr)
sys.exit(1)
cmd_search(args[1])
elif command == "explore":
cmd_explore()
elif command == "detail":
if len(args) < 2:
print("用法: python alapi.py detail <id>", file=sys.stderr)
sys.exit(1)
cmd_detail(args[1])
elif command == "openapi":
if len(args) < 2:
print("用法: python alapi.py openapi <id>", file=sys.stderr)
sys.exit(1)
cmd_openapi(args[1])
elif command == "call":
if len(args) < 2:
print("用法: python alapi.py call <path> --token <token> [--param k=v ...]", file=sys.stderr)
sys.exit(1)
path = args[1]
token = None
params = {}
method = "GET"
i = 2
while i < len(args):
arg = args[i]
if arg == "--token" and i + 1 < len(args):
token = args[i + 1]
i += 2
elif arg == "--param" and i + 1 < len(args):
kv = args[i + 1]
if "=" in kv:
k, v = kv.split("=", 1)
params[k] = v
i += 2
elif arg == "--method" and i + 1 < len(args):
method = args[i + 1].upper()
i += 2
else:
print(f"未知参数: {arg}", file=sys.stderr)
i += 1
cmd_call(path, token, params, method)
else:
print(f"未知命令: {command}", file=sys.stderr)
print(__doc__)
sys.exit(1)
if __name__ == "__main__":
main()
import json
import os
import sys
import unittest
from contextlib import redirect_stdout
from importlib.util import module_from_spec, spec_from_file_location
from io import StringIO
from pathlib import Path
SCRIPT_PATH = Path(__file__).resolve().parent.parent / "scripts" / "alapi.py"
class AlapiCliTests(unittest.TestCase):
def _load_module(self):
spec = spec_from_file_location("alapi_cli", SCRIPT_PATH)
module = module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _run_main_with_stub(self, args, stub_result):
module = self._load_module()
def fake_request(url, method="GET", data=None, headers=None):
return json.loads(json.dumps(stub_result, ensure_ascii=False))
module._request = fake_request
stdout = StringIO()
old_argv = sys.argv
try:
sys.argv = ["alapi.py"] + args
with redirect_stdout(stdout):
module.main()
finally:
sys.argv = old_argv
return stdout.getvalue()
def test_search_json_outputs_machine_readable_payload(self):
stub = {
"success": True,
"data": {
"list": [
{
"id": 27,
"name": "IP 查询",
"description": "查询 IP 地址归属地",
}
]
},
}
output = self._run_main_with_stub(["--json", "search", "IP"], stub)
payload = json.loads(output)
self.assertEqual(payload["command"], "search")
self.assertEqual(payload["keyword"], "IP")
self.assertEqual(payload["count"], 1)
self.assertEqual(payload["items"][0]["id"], 27)
self.assertIn("/api/27/introduction", payload["items"][0]["doc_url"])
def test_call_json_wraps_live_response(self):
stub = {
"success": True,
"code": 200,
"message": "success",
"data": {"city": "Shanghai"},
}
output = self._run_main_with_stub(
["--json", "call", "ip", "--token", "secret", "--param", "ip=8.8.8.8"],
stub,
)
payload = json.loads(output)
self.assertEqual(payload["command"], "call")
self.assertEqual(payload["path"], "ip")
self.assertEqual(payload["method"], "GET")
self.assertTrue(payload["success"])
self.assertEqual(payload["response"]["data"]["city"], "Shanghai")
def test_call_uses_environment_token_when_flag_missing(self):
module = self._load_module()
captured = {}
def fake_request(url, method="GET", data=None, headers=None):
captured["data"] = data
return {"success": True, "code": 200, "message": "success", "data": {}}
module._request = fake_request
old_env = os.environ.get("ALAPI_TOKEN")
old_argv = sys.argv
stdout = StringIO()
try:
os.environ["ALAPI_TOKEN"] = "env-token"
sys.argv = ["alapi.py", "--json", "call", "ip", "--param", "ip=8.8.8.8"]
with redirect_stdout(stdout):
module.main()
finally:
if old_env is None:
os.environ.pop("ALAPI_TOKEN", None)
else:
os.environ["ALAPI_TOKEN"] = old_env
sys.argv = old_argv
self.assertEqual(captured["data"]["token"], "env-token")
def test_explicit_token_overrides_environment_token(self):
module = self._load_module()
captured = {}
def fake_request(url, method="GET", data=None, headers=None):
captured["data"] = data
return {"success": True, "code": 200, "message": "success", "data": {}}
module._request = fake_request
old_env = os.environ.get("ALAPI_TOKEN")
old_argv = sys.argv
stdout = StringIO()
try:
os.environ["ALAPI_TOKEN"] = "env-token"
sys.argv = [
"alapi.py",
"--json",
"call",
"ip",
"--token",
"flag-token",
"--param",
"ip=8.8.8.8",
]
with redirect_stdout(stdout):
module.main()
finally:
if old_env is None:
os.environ.pop("ALAPI_TOKEN", None)
else:
os.environ["ALAPI_TOKEN"] = old_env
sys.argv = old_argv
self.assertEqual(captured["data"]["token"], "flag-token")
if __name__ == "__main__":
unittest.main()
Related skills
FAQ
When does alapi make real API calls?
Only when the user explicitly requests a real call and has provided a token; it defaults to read-only search, detail, openapi, and explore operations.
What languages can it generate code for?
Python with requests, JS/TS with fetch, PHP with curl, and Go with net/http.