@kanyun-ai-infra/pilot
v0.1.1
Published
Typed TypeScript client for the Pilot REST API
Readme
@kanyun-ai-infra/pilot
类型完备的 Pilot REST API TypeScript 客户端。 在 Node 20+ 与现代浏览器(原生
fetch+ SSE)里都能跑。
适合谁用:
- Node 脚本 / 后端服务 — 把 Pilot 嵌进自己的工具链或 Webhook handler。
- CI 流水线 — 触发 Pilot 跑 Agent,再把结果回写到 PR 评论里。
- 业务前端 — 在 Web App 里直接接消息流(推荐用
@ai-sdk/react,本 SDK 暴露的 SSE 也兼容)。
只想在终端里跑命令?请用 @kanyun-ai-infra/pilot-cli。
安装
npm install @kanyun-ai-infra/pilot
# pnpm
pnpm add @kanyun-ai-infra/pilot
# yarn
yarn add @kanyun-ai-infra/pilot无额外 peer dependency。Zod 已作为运行时依赖一并下发。
3 步上手
1. 拿到 Token
三种方式任选其一(推荐 ② / ③):
| 方式 | 适用场景 | 操作 |
|---|---|---|
| ① CLI 浏览器登录 | 本地开发 | pilot login --base-url https://pilot.example.com,Token 写到 ~/.pilot/config.json |
| ② Web UI 签发长期 Token | CI / 自动化 | 打开 /settings/tokens/new,选 CI 自动化 模板(默认 90 天),复制一次性 plaintext |
| ③ 已有 Token 注入环境变量 | 容器 / Serverless | export PILOT_TOKEN=… PILOT_BASE_URL=… |
2. 创建客户端
import { PilotClient } from "@kanyun-ai-infra/pilot";
const pilot = new PilotClient({
baseUrl: "https://pilot.example.com", // 或省略,由 PILOT_BASE_URL 提供
token: process.env.PILOT_TOKEN, // 或省略,由 PILOT_TOKEN 提供
});构造函数 不发起任何网络请求 —— 第一次 IO 发生在你调具体方法时。
3. 触发一次 Run 并看输出
// 一行同时建好 Agent + 它的第一个 Run
const { agentId, runId } = await pilot.agents.create({
projectId: "00000000-0000-0000-0000-000000000000",
prompt: "Add a /health endpoint that returns { status: 'ok' }",
});
// 流式消费 SSE,每个 chunk 是 contracts 里的标准 SDKMessage
for await (const msg of pilot.runs.stream(agentId, runId)) {
if (msg.type === "text-delta") process.stdout.write(msg.text ?? "");
if (msg.type === "done") break;
}
// 阻塞直到 Run 进入终态(10 分钟超时),返回 PR 链接 / 错误信息等
const result = await pilot.runs.wait(agentId, runId, { timeoutMs: 600_000 });
console.log(result.status, result.run?.pullRequestUrl);完整端到端示例(含错误处理):见 docs/sdk-quickstart.md。
配置
new PilotClient(config: PilotClientConfig) 接受:
| 字段 | 类型 | 说明 |
|---|---|---|
| baseUrl | string | Pilot Web 地址。省略时回退到 process.env.PILOT_BASE_URL。必须是 http(s)。 |
| token | string | 平台 Token (JWT)。省略时回退到 process.env.PILOT_TOKEN。两者都缺会抛 UnauthenticatedError。 |
| fetch | typeof fetch | 注入自定义 fetch,多用于测试 / 老 Runtime(注 undici)。 |
| signal | AbortSignal | 默认 AbortSignal,会和单次请求的 signal 合并。React Query / 取消传播场景常用。 |
| defaultHeaders | Record<string, string> | 每个请求的额外 header。不能覆盖 Authorization(SDK 强制注入 Bearer)。 |
| userAgent | string | 服务端日志归因用。默认 @kanyun-ai-infra/pilot/<version>。 |
| onVersionWarning | (message) => void | 当服务端响应缺 x-pilot-api-version header 时触发一次警告。默认 console.warn,传 noop 可静默。 |
API 速查
所有方法都是 Promise 或 AsyncIterable,签名完全 typed。
client.agents
| 方法 | 说明 |
|---|---|
| agents.list(query?, options?) | GET /api/agents 列出当前 Token 可见的 Agent。 |
| agents.get(agentId, options?) | GET /api/agents/:id 拉取详情(嵌套 runs)。 |
| agents.create(request, options?) | POST /api/agents 一次性建 Agent + 第一个 Run。最常用入口。 |
| agents.update(agentId, request, options?) | PATCH /api/agents/:id 改名 / 关闭。 |
| agents.delete(agentId, options?) | DELETE /api/agents/:id 软删。 |
| agents.merge(agentId, request, options?) | POST /api/agents/:id/merge 合并 Agent 最近一个 Run 产出的 PR/MR。需要 runs:merge scope,高危。 |
client.runs
| 方法 | 说明 |
|---|---|
| runs.create(request, options?) | POST /api/runs 创建 Run(可绑定到已有 Agent)。 |
| runs.abort(agentId, runId, request?, options?) | POST /api/agents/:agentId/runs/:runId/abort 让服务端真停止。 |
| runs.stream(agentId, runId, options?) | AsyncIterable<SDKMessage>,自动重连。详见下文【流式细节】。 |
| runs.wait(agentId, runId, options?) | 阻塞直到终态。先走流(快路径),失败回退轮询。返回 { status, run, terminalMessage }。 |
client.projects
| 方法 | 说明 |
|---|---|
| projects.list(options?) | GET /api/projects 列出可见项目。 |
| projects.create(request, options?) | POST /api/projects 创建通用项目(kind: "general")。 |
| projects.get(projectId, options?) | 从 list 里挑出单个项目(服务端暂无单查路由)。 |
| projects.getSettings(projectId, options?) | 读取自动化配置(Webhook / WeCom / Jira)。 |
| projects.updateSettings(projectId, request, options?) | 部分更新自动化配置。 |
Cookbook
Non-coding tasks(无仓库任务)
const { projectId } = await pilot.projects.create({
name: "Weekly research",
kind: "general",
description: "Analyst workspace",
});
const { agentId, runId } = await pilot.agents.create({
projectId,
prompt: "整理本周 AI Infra 新闻,生成一份 markdown 周报",
});
const result = await pilot.runs.wait(agentId, runId, { timeoutMs: 600_000 });
console.log(result.status);general 项目不绑定 Git 仓库,交付通过 artifacts 返回;coding 项目继续走 Diff + PR/MR 闭环。
触发后等结果,CI 一键脚本
import { PilotClient } from "@kanyun-ai-infra/pilot";
const pilot = new PilotClient(); // 读取 PILOT_BASE_URL / PILOT_TOKEN
const { agentId, runId } = await pilot.agents.create({
projectId: process.env.PROJECT_ID!,
prompt: process.env.PROMPT!,
});
const result = await pilot.runs.wait(agentId, runId, { timeoutMs: 30 * 60_000 });
if (result.status === "completed" || result.status === "finalized") {
console.log(`✅ Done. PR: ${result.run?.pullRequestUrl ?? "(none)"}`);
process.exit(0);
} else if (result.status === "failed") {
console.error(`❌ Failed: ${result.run?.errorMessage ?? "unknown"}`);
process.exit(1);
} else {
console.warn(`⚠️ Needs human attention: ${result.status}`);
process.exit(2);
}流式输出 + 用户主动取消
const ac = new AbortController();
process.on("SIGINT", () => ac.abort());
try {
for await (const msg of pilot.runs.stream(agentId, runId, { signal: ac.signal })) {
switch (msg.type) {
case "text-delta": process.stdout.write(msg.text ?? ""); break;
case "tool-use": console.log(`\n🔧 ${msg.tool?.name}`); break;
case "tool-result": console.log(`✅ ${msg.tool?.name}`); break;
case "tool-error": console.warn(`⚠️ ${msg.tool?.name}: ${msg.tool?.error}`); break;
case "error": throw new Error(msg.error?.message ?? "stream error");
case "done": return;
}
}
} catch (err) {
if ((err as Error).name === "AbortError") {
console.warn("(detached — run still executing on server)");
// 真要停服务端,调一次 abort:
await pilot.runs.abort(agentId, runId, { reason: "user-cancelled" });
} else {
throw err;
}
}根据 LDAP 列出我能访问的项目
const { items } = await pilot.projects.list();
for (const p of items) {
console.log(`${p.id} ${p.name}`);
}用断点续传跨进程恢复 SSE
let lastCursor = await readCursorFromDisk(); // 业务自己持久化
for await (const msg of pilot.runs.stream(agentId, runId, {
lastCursor,
onCursor: (c) => writeCursorToDisk(c),
})) {
// 处理 msg…
}错误处理
每个请求都会抛 typed error。用 instanceof 分支即可,code 字段直通服务端 JSON code。
| 类 | HTTP | 何时出现 / 怎么处理 |
|---|---|---|
| PilotApiError | 任意 | 基类,含 .status .code .requestId .message。 |
| UnauthenticatedError | 401 | Token 缺 / 过期 / 已吊销。引导用户重新 pilot login 或换 Token。 |
| ForbiddenError | 403 | 常见 code:INSUFFICIENT_SCOPE(Token 模板不够)/ PROJECT_NOT_IN_TOKEN_SCOPE(项目不在白名单)。 |
| NotFoundError | 404 | 资源不存在或当前 Token 不可见。 |
| RateLimitError | 429 | .retryAfterSeconds 来自 Retry-After header。指数退避后重试。 |
| ServerError | 5xx | 服务端临时抖动。带退避重试通常能恢复。 |
| VersionMismatchError | — | 服务端 API 版本与 SDK 不兼容。.clientVersion / .serverVersion,升级 SDK 解决。 |
import { PilotApiError, ForbiddenError, RateLimitError } from "@kanyun-ai-infra/pilot";
try {
await pilot.agents.create({ projectId, prompt });
} catch (err) {
if (err instanceof ForbiddenError && err.code === "INSUFFICIENT_SCOPE") {
// 引导用户去 /settings/tokens/new 重新签发 Token
} else if (err instanceof RateLimitError) {
await sleep((err.retryAfterSeconds ?? 5) * 1000);
// …重试
} else if (err instanceof PilotApiError) {
console.error(`Pilot ${err.code} (request ${err.requestId}): ${err.message}`);
} else {
throw err;
}
}需要自己包一层 fetch?工厂函数 pilotErrorFromResponse 也对外导出。
流式细节
runs.stream(agentId, runId, options?) 返回 AsyncIterable<SDKMessage>。SDKMessage 从 @pilot/contracts re-export,常见 type:
| type | 含义 |
|---|---|
| text-delta | 模型文本增量。 |
| reasoning | 思维链 / 推理文本。 |
| tool-use | 工具调用,含 { tool: { name, callId, input } }。 |
| tool-result / tool-error | 工具执行结果 / 失败。 |
| step-start / step-end | 步骤边界,通常忽略。 |
| finalize-progress | 终结化阶段进度。 |
| done | 流终止 chunk。不一定包含最终 RunStatus,要拿请用 runs.wait() / agents.get()。 |
| error | 流错误终止,含 error.message。 |
自动重连 — SDK 用 Last-Event-ID 透明重连断开的 SSE,默认最多 6 次(1s → 2s → 4s → 8s → 15s → 30s)。重连用尽后:
- 上一次失败原因 以 throw 形式抛给
for await(一般是PilotApiError形态);或 - 服务端干净 EOF 但没下发终止 chunk —— 迭代器静默结束,不会合成
errorSDKMessage。
两条结论:
- 在意错误的话,把
for await包进try / catch。 - 看到
done不能等价于"Run 跑完了"。要确认终态请调runs.wait()—— 它会在 SSE 拿不到时自动回退到轮询 Agent 详情。
可调参数:
pilot.runs.stream(agentId, runId, {
signal: ac.signal, // 取消本地流(不会停服务端)
lastCursor: 1234, // 跨进程断点续传
maxReconnectAttempts: 6, // SSE 重连预算
maxStreamReadyAttempts: 90, // 等 Agent 启动的轮询次数(默认 90s 窗口)
streamReadyPollIntervalMs: 1000, // 等待间隔
onCursor: (c) => persist(c), // 每个 id: 行触发一次
onReconnect: ({ attempt, delay }) => log(`重连 ${attempt} 次,等 ${delay}ms`),
});
streamvsabort:stream(...)接受 AbortSignal 只是 断本地读 — 服务端 Run 继续。要让服务端真停止请调runs.abort(agentId, runId)。CLIpilot run stream的 Ctrl-C 行为也是同样语义。
API 版本握手
每个响应都带 x-pilot-api-version: v1 header。SDK 拒绝消费 major 不兼容的服务端响应,会抛 VersionMismatchError。如果服务端完全没下发这个 header(老部署或代理剥掉了),SDK 在每个 client 实例上只警告一次(通过 config.onVersionWarning 透出,默认 console.warn),后续静默以避免日志噪声。详见 spec §流式契约版本化。
类型导出
主入口 @kanyun-ai-infra/pilot 重导出了所有需要的类型,不要直接 import @pilot/contracts:
import type {
// Agents / Runs
SdkCreateAgentRequest, SdkCreateAgentResponse,
SdkCreateRunRequest, SdkCreateRunResponse,
SdkAbortRunRequest, SdkRunSummary, SdkAgentSummary,
SdkPatchAgentRequest, SdkMergeAgentRequest, SdkMergeAgentResponse,
// Projects
SdkProjectSummary, SdkProjectSettings, UpdateProjectSettingsRequest,
ListProjectsResponse, ListAgentsQuery, ListAgentsResponse,
// Stream
SDKMessage, SDKMessageType,
// Primitives
RunStatus, TriggerSource, ProviderType,
// Auth
PlatformScope, ScopeTemplate, PlatformTokenPayload,
// MCP / Skill enable lists
EnabledMcpEntry, EnabledSkillEntry,
} from "@kanyun-ai-infra/pilot";相关文档
以下文档放在 Pilot 内部 GitHub 仓库(
kanyun-inc/pilot),需公司员工权限访问。
- CLI —
@kanyun-ai-infra/pilot-cli:终端里登录 / 触发 / 看流。 - 5 分钟教程 —
docs/sdk-quickstart.md:从登录到拿 PR 的端到端走查。 - 权限规范 —
specs/features/pilot-sdk-auth.md:Scope / Actor / Token 模型权威定义。
License
公司内部使用,详见仓库根 LICENSE。
