说明:本文是笔者在实践 HKUDS 推出的 CLI-Anything 项目后,独立开发 CLI-Any-Webapi 的完整心路历程。
背景
CLI-Anything 是香港大学数据科学实验室(HKUDS)推出的一个 Claude Code 插件,已经开源。其核心理念非常有意思:CLI 是 AI Agent 的"原语"。
什么意思呢?传统的自动化方案往往是让 AI 模拟人类操作浏览器——点击按钮、填写表单、等待加载……这种方式不仅效率低下,还极其脆弱。而 CLI 不一样,它天然就是程序化调用的最佳载体:
- 输入输出结构化(JSON)
- 无需维护 UI 状态
- 可组合、可管道
- 错误处理清晰(exit code)
CLI-Anything 正是基于这个理念,提供了一套 CLI Harness 方法论,指导 Agent 如何将任意系统转换为 CLI 工具。这让我眼前一亮——如果能把公司内部的 Web API 全部转成 CLI,那 AI Agent 岂不是可以直接调用后端接口,完成各种自动化任务?
实践遇坑
说干就干。笔者按照 CLI-Anything 的指引,尝试让 Claude 为一个 NestJS 后端项目生成 CLI。
结果嘛……怎么说呢,能跑,但不好用。
具体问题如下:
| 问题 | 表现 |
|---|---|
| 类型推断粗糙 | 大部分参数是 any,失去了 TypeScript 的意义 |
| 命令结构混乱 | 没有统一的三级命令规范,每次生成风格都不一样 |
| 认证方式错误 | 简单使用 Cookie 作为认证机制,没有考虑到 Token 认证 |
| 依赖过重 | 动不动就引入 axios、got 等库,包体积膨胀 |
| API缺失 | 部分API没有识别出来 |
最无法接受的是最后一点。缺少 API 那还玩个毛线啊!!!
于是,一个想法在脑海中萌生:能不能基于 CLI-Anything 的方法论,开发一个专门针对 Web API 的生成器?
剖析 CLI-Anything,搭建核心框架
在动手之前,我先仔细研究了 CLI-Anything 的设计思路。核心在于它的 Harness 方法论——一套标准化的生成流程:
启动
│
▼
1. 🔍 分析 - 扫描源码,映射 API
2. 📐 设计 - 规划命令结构
3. 🔨 实现 - 构建 CLI
4. 📋 规划测试
5. 🧪 编写测试
6. 📝 文档生成
│
▼
可用的 CLI 工具,可发布安装
这套方法论的精髓在于:Agent 不是漫无目的地生成代码,而是按照严格定义的阶段逐步推进。每个阶段有明确的输入、输出和验证标准。
但 CLI-Anything 是个通用框架,它要处理各种类型的系统——文件系统、数据库、消息队列……这种通用性反而成了它的软肋:针对 Web API 这个垂直场景,它没有做足够的特化。
基于此,我确定了 CLI-Any-Webapi 的设计方向:
1. 聚焦 Web API 场景
不追求通用,只做一件事:把 HTTP REST API 转换为类型安全的 TypeScript CLI。
2. 强化类型推断
CLI-Anything 的类型推断依赖 Agent 的"临场发挥",质量参差不齐。考虑到web api 部署的复杂性(这一点无法在源码中得到体现)。我的补偿方案是引入 API 请求日志(HAR、CSV、JSONL)作为类型推断的数据源。真实的请求响应数据,比任何猜测都准确。
3. 统一命令结构
定义死三级命令格式,不给 Agent 发挥空间:
<project>-api-cli <module> <method> <path> [options]
示例:
my-app-api user get /api/v1/user --params='{"page":1}'
my-app-api order post /api/v1/order --params='{"productId":1}'
4. 配置驱动认证
支持多种认证方式,认证信息(Token、Cookie)全部放到项目本地的 config.json,代码里只做注入,不做存储:
{
"baseUrl": "http://localhost:3000",
"headers": {
"Cookie": "name=xxx;tts=xxx"
"Authorization": "Bearer <your-token>"
},
"timeout": 30000
}
切换测试环境?改配置文件就行,CLI 代码一行不动。
5. 详尽的 AI 文档
生成的 CLI 必须附带 SKILL.md,且 每个端点都要有独立的调用示例。这份文档就是 AI Agent 的"说明书"——没有它,Agent 就是睁眼瞎。
实施方案:拥抱原生 API
设计确定后,进入实施阶段。这里重点说一个关键决策:HTTP 客户端选型。
告别 axios,拥抱原生 fetch
CLI-Anything 生成的代码通常会引入 axios 作为 HTTP 客户端。这很合理——axios 的 API 设计确实优雅,拦截器、超时、错误处理都很方便。
但在我的场景里,axios 是个"累赘":
| 特性 | axios | 原生 fetch (Node >= 18) |
|---|---|---|
| 外部依赖 | 1 (+ 传递依赖) | 0 |
| 包体积 | ~40KB (gzip) | 0 |
| 超时控制 | timeout 选项 | AbortController |
| 类型安全 | AxiosRequestConfig | 自定义 ClientRequestOptions |
Node.js 18+ 已经内置了全局 fetch API,功能完全够用。既然如此,何必多此一举?
于是我在 HARNESS.md 里写死了一条硬性约束:
严禁引入任何第三方 HTTP 库(axios、got、node-fetch、undici、ky、superagent 等),违反此规则视为生成失败。
这不是偏执,而是"约束即自由"——把选择权从 Agent 手里拿走,反而能保证生成结果的一致性。
HTTP 客户端模板实现
来看看最终的 http-client.ts.template:
import { loadConfig } from './config.js';
const cfg = loadConfig();
export interface ClientRequestOptions {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
path: string;
params?: Record<string, unknown>;
body?: Record<string, unknown>;
headers?: Record<string, string>;
baseUrl?: string;
}
export const client = {
async request<T = unknown>(options: ClientRequestOptions): Promise<T> {
const baseUrl = (options.baseUrl ?? cfg.baseUrl).replace(/\/+$/, '');
let url = `${baseUrl}${options.path}`;
// Query 参数拼接
if (options.params && Object.keys(options.params).length > 0) {
const qs = new URLSearchParams();
for (const [k, v] of Object.entries(options.params)) {
if (v !== undefined && v !== null) qs.append(k, String(v));
}
url += `?${qs.toString()}`;
}
// 超时控制:AbortController
const controller = new AbortController();
const timeout = cfg.timeout ?? 30000;
const timer = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, {
method: options.method,
headers: {
'Content-Type': 'application/json',
...cfg.headers, // 从配置注入认证头
...options.headers,
},
body: options.body ? JSON.stringify(options.body) : undefined,
signal: controller.signal,
});
clearTimeout(timer);
const text = await res.text();
let data: unknown;
try {
data = JSON.parse(text);
} catch {
data = text;
}
if (!res.ok) {
console.error(JSON.stringify({ error: true, status: res.status, data }, null, 2));
process.exit(1);
}
return data as T;
} catch (err: unknown) {
clearTimeout(timer);
if (err instanceof DOMException && err.name === 'AbortError') {
console.error(JSON.stringify({
error: true,
status: 0,
data: `Request timeout after ${timeout}ms`
}, null, 2));
} else {
console.error(JSON.stringify({ error: true, status: 0, data: String(err) }, null, 2));
}
process.exit(1);
}
},
};
代码不复杂,但每一行都有讲究:
- 超时用 AbortController:这是原生 fetch 处理超时的标准方式,比 Promise.race 更优雅
- Headers 三层合并:Content-Type → config.headers → options.headers,优先级递增
- 错误输出到 stderr:方便 Agent 通过 exit code 判断执行结果
- JSON 解析兜底:响应不是 JSON?没关系,返回原始文本
配置文件设计
另一个关键点是配置文件的位置。最初我想放到 ~/.cli-any-webapi/config.json——全局统一管理,多方便!
但转念一想,这会出问题:用户可能同时生成多个 CLI 项目(连接不同后端),公共配置会互相覆盖。
最终决定:每个 CLI 项目独享自己的配置文件,放在项目根目录下:
my-backend-api-cli/
├── config.json ← 项目本地配置
├── package.json
├── src/
│ └── config.ts ← 配置读取逻辑
└── ...
config.ts 的定位逻辑:
function getConfigPath(): string {
// 从编译产物位置(dist/)向上一级即为项目根目录
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = resolve(__dirname, '..');
return resolve(projectRoot, 'config.json');
}
这样一来,npm link 后全局调用也能正确找到配置文件。
7 阶段生成流程
最后,整理出完整的生成流程(详见 HARNESS.md):
| Phase | 任务 | 输出 |
|---|---|---|
| 0 | 输入验证 | 环境检查通过 |
| 1 | 路由发现 | 端点映射表 (TEST.md Part 1) |
| 2 | 类型推断 | TypeScript 类型定义 |
| 3 | 模块分组 | module → endpoints 映射 |
| 4 | 类型生成 | src/types/<module>.ts |
| 5 | 命令生成 | src/commands/<module>.ts |
| 6 | 配置生成 | package.json, tsconfig.json, SKILL.md |
| 7 | 验证 | npm build + --help 测试 |
每个阶段都有明确的校验标准,Agent 必须逐一完成才能进入下一阶段。这种"流水线"式的约束,是保证生成质量的关键。
效果展示
开发完成后,来看看实际效果。以一个 NestJS 后端为例:
# 生成 CLI(源码 + HAR 日志)
/cli-any-webapi generate ./my-backend --logs ./api-logs.har
# 构建安装
cd my-backend-api-cli
npm install && npm run build && npm link
# 配置认证
cat > config.json << 'EOF'
{
"baseUrl": "http://localhost:3000",
"headers": { "Authorization": "Bearer eyJ..." }
}
EOF
# 开始使用!
my-backend-api user get /api/v1/user --params='{"page":1,"pageSize":20}'
my-backend-api user post /api/v1/user --params='{"username":"alice","email":"alice@x.com"}'
my-backend-api user delete /api/v1/user/:id --path-params='{"id":"42"}'
生成的 SKILL.md 包含每个端点的详细调用示例:
GET /api/v1/user — 获取用户列表
my-backend-api user get /api/v1/user --params='{"page":1,"pageSize":20}'
请求参数 (query):
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | number | 否 | 页码,默认 1 |
| pageSize | number | 否 | 每页条数 |
响应示例:
{ "total": 100, "list": [{ "id": 1, "username": "alice" }] }
AI Agent 拿到这份文档,就能准确调用任意 API——这才是"AI 友好"的真正含义。
总结
- CLI-Anything 提供了优秀的 Harness 方法论,但在 Web API 场景下还不够特化;
- CLI-Any-Webapi 聚焦 HTTP REST API,通过引入请求日志实现精准类型推断;
- 拥抱原生 API(fetch、AbortController、URLSearchParams),减少外部依赖;
- 配置驱动认证 + 项目本地配置,解决多环境切换问题;
- 强制生成 SKILL.md,让 AI Agent 能够"自学"如何调用 CLI。
项目已开源:CLI-Any-Webapi