API marker 客户端 深度解析
拆解 api.ts 如何把 SV Number 的文本 marker、HTTP 状态和 JSON 解析失败转成 Agent 可行动错误。
导入链与契约
API client 是整个 server 最薄但最关键的边界:src/api.ts:1 负责其中一个检查点;src/api.ts:3 负责其中一个检查点;src/api.ts:6 负责其中一个检查点;src/api.ts:8 负责其中一个检查点;src/api.ts:10 负责其中一个检查点;src/api.ts:16 负责其中一个检查点;src/api.ts:43 说明外部服务返回 plain text marker,而且仍可能是 HTTP 200。上层工具只 import src/index.ts:15 负责其中一个检查点;src/index.ts:41 负责其中一个检查点;src/index.ts:57 负责其中一个检查点;src/index.ts:122 负责其中一个检查点;src/index.ts:167 负责其中一个检查点;src/index.ts:191,因此所有外部状态都先经过 call、expect 或 json。
1// Thin client over the SV Number handler API. 2// 3// The API answers with plain text markers and HTTP 200, so every reply is checked against 4// the marker it is supposed to start with before anything is parsed out of it. 5 6const BASE = process.env.SVN_API_BASE 7 ?? "https://sms-verification-number.com/stubs/handler_api"; 8const LANG = process.env.SVN_LANG ?? "en"; 9 10export class ApiError extends Error { 11 constructor(public marker: string, message: string) { 12 super(message); 13 } 14}
核心实现,逐段拆
▸错误字典
错误表把平台 marker 翻译成下一步动作:src/api.ts:17 负责其中一个检查点;src/api.ts:18 负责其中一个检查点;src/api.ts:21 负责其中一个检查点;src/api.ts:24 负责其中一个检查点;src/api.ts:27 负责其中一个检查点;src/api.ts:29 负责其中一个检查点;src/api.ts:30 负责其中一个检查点;src/api.ts:31 负责其中一个检查点;src/api.ts:32 负责其中一个检查点;src/api.ts:33 负责其中一个检查点;src/api.ts:34 负责其中一个检查点;src/api.ts:35 覆盖 key、参数、余额、库存、activation 和 rate limit。这样 Agent 可以调整 country/operator、充值或放慢轮询,而不是盲目重试。
16// what the markers mean, in words an agent can act on 17const ERRORS: Record<string, string> = { 18 BAD_KEY: "The API key is not accepted. Check SVN_API_KEY.", 19 // checked against the live API: an unknown service, an unknown country and an unknown 20 // activation id all come back as ERROR_API, so this is not only a server-side failure 21 ERROR_API: "Either the parameters are wrong (unknown service, country or activation id) " 22 + "or the provider had a hiccup. Check the values against list_services and " 23 + "list_countries before retrying.", 24 ERROR_SQL: "Server-side error on the provider side. Retry with a backoff.", 25 BAD_ACTION: "Malformed parameters, for example an activation id that is not a number.", 26 NO_METHOD: "Unknown action.", 27 BAD_STATUS: "The activation is not in a state where that call makes sense: it may be " 28 + "cancelled, already finished, or still waiting for its first code.", 29 NO_BALANCE: "The account balance is empty. It has to be topped up before ordering.", 30 NO_NUMBERS: "No numbers for this country and service right now. Change country, or set operator to any.", 31 NO_ACTIVATION: "Unknown activation id, or the 20 minutes already ran out.", 32 CANNOT_BEFORE_2_MIN: "This cancel came too early for the API. Wait a couple of minutes and repeat.", 33 REQUEST_LIMIT: "Over the rate limit. Slow the polling down.", 34 WRONG_MAX_PRICE: "The max price is below the current minimum for this service.", 35 BANNED: "The account is blocked.", 36};
▸请求构造不回显密钥
src/api.ts:43 进入 call 后才读取 key,src/api.ts:47 只把它写进 URLSearchParams,src/api.ts:50 直接交给 fetch;页面没有把完整 query 或 key 放回 text response,这对 MCP 工具尤其关键。
▸expect 与 json
call 只负责发请求,expect 和 json 才负责语义校验。src/api.ts:43 负责其中一个检查点;src/api.ts:45 负责其中一个检查点;src/api.ts:47 负责其中一个检查点;src/api.ts:50 负责其中一个检查点;src/api.ts:51 负责其中一个检查点;src/api.ts:52 负责其中一个检查点;src/api.ts:56 负责其中一个检查点;src/api.ts:58 负责其中一个检查点;src/api.ts:59 负责其中一个检查点;src/api.ts:63 负责其中一个检查点;src/api.ts:66 负责其中一个检查点;src/api.ts:68 负责其中一个检查点;src/api.ts:72 显示 marker 不匹配和 JSON parse 失败都会进入 ApiError,这能把 provider 的文本协议收敛成 TypeScript 异常。
43export async function call(action: string, params: Record<string, string | number> = {}): Promise<string> { 44 const key = process.env.SVN_API_KEY; 45 if (!key) throw new Error("SVN_API_KEY is not set in the environment."); 46 47 const query = new URLSearchParams({ api_key: key, action, lang: LANG }); 48 for (const [name, value] of Object.entries(params)) query.set(name, String(value)); 49 50 const response = await fetch(`${BASE}?${query}`, { headers: { Accept: "*/*" } }); 51 if (!response.ok) throw new Error(`HTTP ${response.status} from the API.`); 52 return (await response.text()).trim(); 53} 54 55/** Calls an action that must answer with `marker`, and returns the reply. */ 56export async function expect(marker: string, action: string, 57 params: Record<string, string | number> = {}): Promise<string> { 58 const reply = await call(action, params); 59 if (!reply.startsWith(marker)) throw new ApiError(reply.split(":")[0], explain(reply)); 60 return reply; 61} 62 63export async function json<T>(action: string, params: Record<string, string | number> = {}): Promise<T> { 64 const reply = await call(action, params); 65 try { 66 return JSON.parse(reply) as T; 67 } catch { 68 throw new ApiError(reply.split(":")[0], explain(reply)); 69 } 70} 71 72export const pollSeconds = Number(process.env.SVN_POLL_SECONDS ?? 4);
关键流程
这条链路的设计重点是“晚绑定”:src/api.ts:47 负责其中一个检查点;src/api.ts:48 负责其中一个检查点;src/api.ts:50 到请求前才拼参数,src/api.ts:58 负责其中一个检查点;src/api.ts:59 到具体工具才校验期望 marker,src/index.ts:167 负责其中一个检查点;src/index.ts:215 负责其中一个检查点;src/index.ts:231 负责其中一个检查点;src/index.ts:246 让每个生命周期工具声明自己期待的成功前缀。
边界 / 失败模式
src/api.ts:45缺 key 直接阻断,避免向 provider 发送无效请求。src/api.ts:51非 2xx HTTP 状态仍会变成普通 Error。src/api.ts:68JSON 解析失败时仍会提取 marker 解释,而不是丢给 Agent 一段不可读文本。