源码路径:
src/services/
Claude Code 的服务层采用模块化设计, 将核心业务逻辑拆分为独立的服务模块。每个服务负责一个特定的关注点: API 通信、遥测分析、上下文压缩、语言服务器协议、OAuth 认证、企业策略限制等。本文档深入分析各服务模块的内部架构与关键实现。
API 客户端是整个系统的通信核心, 负责与 Anthropic API 的所有交互。支持多种后端: 直连 Anthropic API、AWS Bedrock、Google Vertex AI、Azure Foundry。
客户端通过工厂函数创建, 根据环境变量自动选择后端:
// 支持的后端类型:
// - Direct API (ANTHROPIC_API_KEY)
// - AWS Bedrock (CLAUDE_CODE_USE_BEDROCK)
// - Google Vertex AI (CLAUDE_CODE_USE_VERTEX)
// - Azure Foundry (CLAUDE_CODE_USE_FOUNDRY)
// - OAuth (Claude.ai 订阅用户)
function getAnthropicClient(): Promise<Anthropic>客户端首次创建后复用, 仅在认证失败 (401/403) 或连接重置 (ECONNRESET/EPIPE) 时重建。
重试系统是 API 层最复杂的部分, 采用 AsyncGenerator 模式, 允许在等待重试期间向调用方发送状态消息:
export async function* withRetry<T>(
getClient: () => Promise<Anthropic>,
operation: (
client: Anthropic,
attempt: number,
context: RetryContext,
) => Promise<T>,
options: RetryOptions,
): AsyncGenerator<SystemAPIErrorMessage, T>核心类型:
export interface RetryContext {
maxTokensOverride?: number
model: string
thinkingConfig: ThinkingConfig
fastMode?: boolean
}错误分类与策略:
| 错误类型 | HTTP 状态码 | 策略 |
|---|---|---|
| 速率限制 | 429 | 指数退避, Fast Mode 降级 |
| 过载 | 529 | 最多 MAX_529_RETRIES=3 次, 仅前台查询重试 |
| 认证失败 | 401 | 刷新 OAuth Token 后重试 |
| Token 吊销 | 403 | 刷新 OAuth Token 后重试 |
| 连接重置 | ECONNRESET/EPIPE | 禁用 keep-alive, 重建客户端 |
| Bedrock 认证 | 403/CredentialsProviderError | 清除凭证缓存, 重建客户端 |
| Vertex 认证 | 401 | 刷新 GCP 凭证后重试 |
前台 529 重试白名单 -- 仅这些 QuerySource 类型会重试 529 错误:
const FOREGROUND_529_RETRY_SOURCES = new Set<QuerySource>([
'repl_main_thread',
'sdk',
'agent:custom', 'agent:default', 'agent:builtin',
'compact',
'hook_agent', 'hook_prompt',
'verification_agent',
'side_question',
'auto_mode',
// ...
])后台任务 (summaries, titles, suggestions) 遇到 529 立即放弃, 避免容量级联期间的网关放大。
持久重试模式 (CLAUDE_CODE_UNATTENDED_RETRY): 无人值守会话对 429/529 无限重试, 最大退避 5 分钟, 重置上限 6 小时, 心跳间隔 30 秒。
Fast Mode 降级: 短 retry-after 时保持 Fast Mode 等待 (保留 prompt cache); 长 retry-after 进入冷却期切换标准速度; Overage 不可用时永久禁用。
核心查询函数 queryModelWithStreaming 是一个 122KB 的大文件, 处理流式 API 调用的完整生命周期:
export async function* queryModelWithStreaming(
options: QueryModelWithStreamingOptions,
): AsyncGenerator<StreamEvent | Message, void, unknown>关键职责: 消息规范化、System prompt 组装、Beta header 管理、流事件解析与 token 统计、错误映射为 AssistantMessage、Prompt cache 命中检测、Session cost 累计、遥测记录。
支持 CLAUDE_CODE_EXTRA_BODY 环境变量注入自定义参数, 1P CLI 包含 anti_distillation: ['fake_tools'] 反蒸馏防护。
启动时拉取客户端配置数据和附加模型选项:
const bootstrapResponseSchema = z.object({
client_data: z.record(z.unknown()).nullish(),
additional_model_options: z.array(
z.object({
model: z.string(),
name: z.string(),
description: z.string(),
})
).nullish(),
})
export async function fetchBootstrapData(): Promise<void>仅 firstParty provider 有效。使用 OAuth (优先) 或 API key 认证。响应通过 Zod schema 校验, 仅在数据变更时持久化到磁盘 (isEqual 对比)。
管理文件上传与下载, 用于会话启动时下载文件附件:
export type FilesApiConfig = {
oauthToken: string
baseUrl?: string // 默认 https://api.anthropic.com
sessionId: string
}
export type DownloadResult = {
fileId: string
path: string
success: boolean
error?: string
bytesWritten?: number
}
// MAX_RETRIES=3, BASE_DELAY_MS=500, MAX_FILE_SIZE=500MB错误处理模块提供分类型的错误消息生成和解析:
export const API_ERROR_MESSAGE_PREFIX = 'API Error'
export const PROMPT_TOO_LONG_ERROR_MESSAGE = 'Prompt is too long'
export const REPEATED_529_ERROR_MESSAGE = 'Repeated 529 Overloaded errors'
export const CUSTOM_OFF_SWITCH_MESSAGE =
'Opus is experiencing high load, please use /model to switch to Sonnet'
export function parsePromptTooLongTokenCounts(rawMessage: string): {
actualTokens: number | undefined
limitTokens: number | undefined
}
export function getPromptTooLongTokenGap(msg: AssistantMessage): number | undefinedgetPromptTooLongTokenGap 被 reactive compact 用来计算超限 token 数, 从而跳过多个消息组而非逐个剥离。
Media 错误检测:
export function isMediaSizeError(raw: string): boolean
export function isMediaSizeErrorMessage(msg: AssistantMessage): boolean
// 用于 reactive compact 的 summarize retry, 决定是剥离图片重试还是直接失败事件系统采用 sink 模式, 核心 API 零依赖以避免循环引用:
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED = never
export type AnalyticsSink = {
logEvent: (eventName: string, metadata: LogEventMetadata) => void
logEventAsync: (
eventName: string,
metadata: LogEventMetadata,
) => Promise<void>
}
export function logEvent(
eventName: string,
metadata: { [key: string]: boolean | number | undefined },
): void
export function logEventAsync(
eventName: string,
metadata: { [key: string]: boolean | number | undefined },
): Promise<void>关键设计:
- 类型安全 PII 防护:
AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS是never类型, 强制显式 cast - PROTO 前缀:
_PROTO_*键仅路由到 1P 特权列, 通用后端通过stripProtoFields()过滤 - 预初始化队列: sink 附加前事件入队, 附加后
queueMicrotask异步排空 - 幂等附加:
attachAnalyticsSink()多次调用安全
Sink 实现将事件路由到两个后端:
function logEventImpl(eventName: string, metadata: LogEventMetadata): void {
// 1. 采样检查 (shouldSampleEvent)
// 2. Datadog -- 通用后端, stripProtoFields 过滤 PII
// 3. 1P Event Logging -- 接收完整 payload 包括 _PROTO_*
}Datadog 门控:
const DATADOG_GATE_NAME = 'tengu_log_datadog_events'
function shouldTrackDatadog(): boolean {
if (isSinkKilled('datadog')) return false
// GrowthBook feature gate 控制
return checkStatsigFeatureGate_CACHED_MAY_BE_STALE(DATADOG_GATE_NAME)
}支持 killswitch (sinkKillswitch.ts) 和 GrowthBook 门控两级开关。
GrowthBook 是 feature flag 和 A/B 实验的核心基础设施:
export type GrowthBookUserAttributes = {
id: string
sessionId: string
deviceID: string
platform: 'win32' | 'darwin' | 'linux'
apiBaseUrlHost?: string
organizationUUID?: string
accountUUID?: string
userType?: string
subscriptionType?: string
rateLimitTier?: string
firstTokenTime?: number
email?: string
appVersion?: string
github?: GitHubActionsMetadata
}核心 API:
export function getFeatureValue_CACHED_MAY_BE_STALE<T>(
feature: string, defaultValue: T
): T
export function onGrowthBookRefresh(
listener: GrowthBookRefreshListener
): () => void
export function getDynamicConfig_BLOCKS_ON_INIT<T>(
configName: string
): T | null关键机制: Remote Eval 模式 (服务端评估); 磁盘缓存 (上次会话 fallback); 延迟曝光日志 (pendingExposures); 曝光去重 (loggedExposures Set); 环境变量覆盖 (CLAUDE_INTERNAL_FC_OVERRIDES, ant-only); 重初始化等待 (reinitializingPromise)。
export function isAnalyticsDisabled(): boolean {
return (
process.env.NODE_ENV === 'test' ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
isTelemetryDisabled()
)
}三方云提供商 (Bedrock/Vertex/Foundry) 默认禁用 analytics。反馈调查 (isFeedbackSurveyDisabled) 不受三方提供商限制。
压缩系统是 Claude Code 管理长会话上下文窗口的核心机制。共有四种策略: 自动压缩、微压缩、API 微压缩、反应式压缩。
Token 使用量达到阈值时自动触发全量压缩:
export type AutoCompactTrackingState = {
compacted: boolean
turnCounter: number
turnId: string
consecutiveFailures?: number
}
export const AUTOCOMPACT_BUFFER_TOKENS = 13_000
export const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000
export const MANUAL_COMPACT_BUFFER_TOKENS = 3_000
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3阈值计算:
export function getAutoCompactThreshold(model: string): number {
const effectiveContextWindow = getEffectiveContextWindowSize(model)
return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
}
export function getEffectiveContextWindowSize(model: string): number {
// contextWindow - min(maxOutputTokens, 20_000)
// 可通过 CLAUDE_CODE_AUTO_COMPACT_WINDOW 环境变量覆盖
}Token 警告状态: calculateTokenWarningState() 返回 percentLeft、各级阈值标志和 isAtBlockingLimit。
递归防护: compact/session_memory/marble_origami 查询源不触发自动压缩。连续失败熔断: MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3 (源于 BQ 数据: 1279 会话出现 50+ 次连续失败)。
压缩的核心实现, 59KB 的大文件:
export type CompactionResult = {
messages: Message[]
summary: string
tokensFreed: number
// ...
}
export type RecompactionInfo = {
// 重压缩相关信息
}压缩流程: pre-compact hooks -> 消息按 API round 分组 -> 剥离图片/文档 -> forked agent 生成 <analysis> + <summary> 摘要 -> 构建 compact boundary message -> post-compact 清理 -> 恢复关键文件 -> 记录遥测。
压缩提示 (prompt.ts): 包含 NO_TOOLS_PREAMBLE (防止 Sonnet 4.6+ 自适应思考模型在 maxTurns:1 下尝试工具调用) 和 <analysis> + <summary> 输出结构。BASE 变体作用于全量压缩, PARTIAL 变体作用于部分压缩。
Post-compact 恢复: 最多 5 个文件, 总预算 50K tokens, 单文件/Skill 各 5K tokens, Skills 总预算 25K tokens。
微压缩通过清除旧工具结果来释放空间, 无需 LLM 生成摘要。可压缩工具集: FileRead, Shell, Grep, Glob, WebSearch, WebFetch, FileEdit, FileWrite。
时间维度微压缩 (timeBasedMCConfig.ts): 根据消息年龄清除内容, 保留最近消息。
缓存感知微压缩 (Cached MC): 维护 pending/pinned 两种编辑状态。consumePendingCacheEdits() 获取新编辑 (清除 pending), getPinnedCacheEdits() 返回必须在原位重发的已固定编辑 (保证 cache hit)。
服务端原生上下文管理, 通过 API 参数指导服务端清除旧内容:
export type ContextEditStrategy =
| { type: 'clear_tool_uses_20250919'; trigger?: { type: 'input_tokens'; value: number }
clear_tool_inputs?: boolean | string[]; exclude_tools?: string[]
clear_at_least?: { type: 'input_tokens'; value: number } }
| { type: 'clear_thinking_20251015'; keep: { type: 'thinking_turns'; value: number } | 'all' }
export type ContextManagementConfig = { edits: ContextEditStrategy[] }策略组合: (1) Thinking 清理 -- 保留全部 (> 1h 空闲时仅保留最后 1 个); (2) 工具结果清理 (ant-only, 触发 180K tokens, 目标保留 40K); (3) 工具调用清理 (ant-only, 排除 FileEdit/FileWrite/NotebookEdit)。
反应式压缩 (reactive compact) 是 ant-only 的实验性策略, 在 API 返回 prompt-too-long 错误时触发。通过 feature flag REACTIVE_COMPACT 门控, 条件加载:
// query.ts 中的条件加载
const reactiveCompact = feature('REACTIVE_COMPACT')
? require('./services/compact/reactiveCompact.js')
: null与主动压缩相反, 反应式压缩等待 API 明确拒绝后才行动。利用 getPromptTooLongTokenGap() 计算超限 token 数, 一次跳过多个消息组而非逐个剥离。
当 tengu_cobalt_raccoon feature flag 开启时, 主动自动压缩被抑制, 完全依赖反应式压缩。
实验性功能, 将 session memory 作为压缩的摘要来源:
export type SessionMemoryCompactConfig = {
minTokens: number // 压缩后最少保留的 tokens
// minTextMessages: 保留的最少文本消息数
}与传统压缩的区别: 不调用 LLM 生成摘要, 而是使用已提取的 session memory 内容作为压缩后的上下文。
LSP Client 类型:
export type LSPClient = {
readonly capabilities: ServerCapabilities | undefined
readonly isInitialized: boolean
start: (command: string, args: string[],
options?: { env?: Record<string, string>; cwd?: string }) => Promise<void>
initialize: (params: InitializeParams) => Promise<InitializeResult>
sendRequest: <TResult>(method: string, params: unknown) => Promise<TResult>
sendNotification: (method: string, params: unknown) => Promise<void>
onNotification: (method: string, handler: (params: unknown) => void) => void
stop: () => Promise<void>
}崩溃恢复通过 onCrash 回调; 连接就绪前注册的 handler 进入 pending 队列。
管理多个 LSP 服务器实例, 按文件扩展名路由请求。使用工厂函数 + 闭包模式封装状态:
export type LSPServerManager = {
initialize(): Promise<void>
shutdown(): Promise<void>
getServerForFile(filePath: string): LSPServerInstance | undefined
ensureServerStarted(filePath: string): Promise<LSPServerInstance | undefined>
sendRequest<T>(filePath: string, method: string, params: unknown): Promise<T | undefined>
getAllServers(): Map<string, LSPServerInstance>
openFile(filePath: string, content: string): Promise<void>
changeFile(filePath: string, content: string): Promise<void>
saveFile(filePath: string): Promise<void>
closeFile(filePath: string): Promise<void>
isFileOpen(filePath: string): boolean
}单例模式管理 LSP 生命周期, 状态机: not-started -> pending -> success | failed。Bare mode 下跳过初始化。
诊断注册表存储异步 LSP 诊断通知, 使用 LRUCache (MAX_DELIVERED_FILES=500) 跨 turn 去重, 限制每文件 10 条、总计 30 条诊断。被动反馈 (passiveFeedback.ts) 将编译错误/警告自动注入对话上下文。
实现 OAuth 2.0 Authorization Code Flow with PKCE:
export class OAuthService {
private codeVerifier: string
private authCodeListener: AuthCodeListener | null = null
private port: number | null = null
private manualAuthCodeResolver: ((authorizationCode: string) => void) | null = null
async startOAuthFlow(
authURLHandler: (url: string, automaticUrl?: string) => Promise<void>,
options?: {
loginWithClaudeAi?: boolean
inferenceOnly?: boolean
expiresIn?: number
orgUUID?: string
loginHint?: string
loginMethod?: string
skipBrowserOpen?: boolean
},
): Promise<OAuthTokens>
}双路径认证:
- 自动流程: 打开浏览器 -> 重定向到 localhost callback -> 捕获 authorization code
- 手动流程: 用户复制粘贴 code (用于无浏览器环境)
SDK 控制协议使用 skipBrowserOpen, 将 URL 交给调用方处理。
底层 HTTP 操作: buildAuthUrl() 构建授权 URL, exchangeCodeForTokens() 交换 token。
Scope 体系: claude_ai:inference (inference-only 长生命期 token) 和 ALL_OAUTH_SCOPES (完整 scope)。shouldUseClaudeAIAuth() 检查是否具有推理 scope。
PKCE 加密 (crypto.ts) 生成 code_verifier/code_challenge (S256) 和 state 参数。
从 API 获取组织级策略限制, 用于禁用特定 CLI 功能:
// types.ts
export const PolicyLimitsResponseSchema = z.object({
restrictions: z.record(
z.string(),
z.object({ allowed: z.boolean() })
),
})
export type PolicyLimitsResponse = z.infer<
ReturnType<typeof PolicyLimitsResponseSchema>
>
export type PolicyLimitsFetchResult = {
success: boolean
restrictions?: PolicyLimitsResponse['restrictions'] | null
etag?: string
error?: string
skipRetry?: boolean
}设计原则: Fail Open
API 失败时不阻塞, 继续运行不加限制。只有成功获取到限制时才执行。
资格判定:
| 用户类型 | 资格 |
|---|---|
| Console (API key) | 全部 |
| OAuth Team | 有资格 |
| OAuth Enterprise/C4E | 有资格 |
| OAuth 其他 | 无资格 |
轮询与缓存: ETag 缓存 (304 时复用本地), session 级内存缓存, 1 小时轮询, 最多 5 次重试, 30 秒超时防死锁。initializePolicyLimitsLoadingPromise() 允许其他系统等待首次加载。
企业客户的远程配置管理, 模式与 policyLimits 类似 (1h 轮询, 5 次重试, fail-open)。额外包含安全检查 (securityCheck.tsx) 和 checksum 验证最小化网络流量。syncCache.ts 管理缓存状态和用户资格判定。
跨环境同步用户设置和 memory 文件 (10s 超时, 3 次重试, 500KB/文件上限):
| 环境 | 方向 | 触发时机 |
|---|---|---|
| Interactive CLI | 上传本地 -> 远程 | preAction |
| CCR (Remote) | 下载远程 -> 本地 | 插件安装前 |
增量同步, 需 OAuth 认证和 tengu_enable_settings_sync_push feature flag。
上下文感知的提示系统, 在合适时机向用户展示功能提示:
// tipRegistry.ts
type Tip = {
// 提示内容、条件、优先级等
}
type TipContext = {
bashTools?: // ...
}条件系统: 每个 tip 基于 IDE 检测、终端类型、插件安装状态、用户配置、会话计数、feature flag 等多维条件决定是否显示。调度器控制频率, 历史记录跟踪上次显示时间。
在用户等待时预测性地生成下一步建议:
export type PromptVariant = 'user_intent' | 'stated_intent'
export function shouldEnablePromptSuggestion(): boolean
// GrowthBook gate: tengu_chomp_inflection
// 非交互模式禁用
// Swarm teammate 禁用 (仅 leader 显示)包含 speculation 子系统 (speculation.ts, 30KB), 在后台预执行可能的用户请求。
子 agent 的周期性进度摘要:
const SUMMARY_INTERVAL_MS = 30_000 // 每 30 秒
export function startAgentSummarization(
taskId: string,
agentId: AgentId,
cacheSafeParams: CacheSafeParams,
setAppState: TaskContext['setAppState'],
): { stop: () => void }使用 runForkedAgent() fork 子 agent 对话, 生成 3-5 词的进度描述 (如 "Reading runAgent.ts"、"Fixing null check in validate.ts")。
为共享 prompt cache, 使用与父 agent 相同的 CacheSafeParams, 工具保留在请求中但通过 canUseTool 拒绝。
后台记忆巩固, 条件满足时自动触发 /dream prompt:
type AutoDreamConfig = {
minHours: number // 默认 24 小时
minSessions: number // 默认 5 个会话
}三级门控 (由廉到贵): 时间门 (>= minHours)、会话门 (>= minSessions)、锁门 (无并发巩固)。通过 tengu_onyx_plover GrowthBook config 控制阈值, 扫描节流 10 分钟。
自动维护特殊标记的 markdown 文档:
const MAGIC_DOC_HEADER_PATTERN = /^#\s*MAGIC\s+DOC:\s*(.+)$/im
export function detectMagicDocHeader(content: string): {
title: string
instructions?: string
} | null当读取含 # MAGIC DOC: [title] 头部的文件时, 周期性地在后台使用 forked subagent 更新文档内容。支持可选的 italics 行作为更新指令。
为 SDK 生成工具执行摘要:
export type GenerateToolUseSummaryParams = {
tools: ToolInfo[]
signal: AbortSignal
isNonInteractiveSession: boolean
lastAssistantText?: string
}
export async function generateToolUseSummary(
params: GenerateToolUseSummaryParams,
): Promise<string | null>使用 Haiku 模型生成简短的 (约 30 字符) 工具执行摘要, 用于移动 App 的单行进度显示。非关键路径, 失败时静默返回 null。
- Token 估算 (
tokenEstimation.ts, 16KB): 粗略 token 计数, 用于微压缩等不需精确计数的场景 - Rate Limit 消息 (
rateLimitMessages.ts, 10KB): 根据限制类型 (429/529/overage) 生成人性化错误消息 - Voice Service (
voice.ts, 16KB): 语音输入, 含 STT 流处理 (voiceStreamSTT.ts, 20KB) - VCR (
vcr.ts, 11KB): API 请求录制/回放,withStreamingVCR()/withVCR() - Diagnostic Tracking (
diagnosticTracking.ts, 12KB): 代码诊断信息跟踪, 作为上下文注入会话 - Claude.ai Limits (
claudeAiLimits.ts, 16KB): 订阅速率限制状态管理,currentLimits()/extractQuotaStatusFromHeaders()
几乎所有服务都依赖 GrowthBook 进行 feature flag 检查:
analytics/growthbook.ts
<- compact/autoCompact.ts (tengu_cobalt_raccoon -- 反应式压缩)
<- compact/microCompact.ts (时间维度配置)
<- api/withRetry.ts (tengu_disable_keepalive_on_econnreset)
<- api/claude.ts (tengu_anti_distill_fake_tool_injection)
<- PromptSuggestion/ (tengu_chomp_inflection)
<- autoDream/ (tengu_onyx_plover)
<- settingsSync/ (tengu_enable_settings_sync_push)
<- tips/tipRegistry.ts (多个 gate)
多个服务使用 runForkedAgent() 在后台执行 LLM 查询, 共享 prompt cache:
| 服务 | 用途 |
|---|---|
| compact | 生成对话摘要 |
| AgentSummary | 生成子 agent 进度摘要 |
| autoDream | 后台记忆巩固 |
| MagicDocs | 自动更新标记文档 |
| PromptSuggestion | 预测性建议生成 |
| SessionMemory | session memory 提取 |
远程配置/策略服务统一采用 fail-open 设计:
policyLimits -> 获取失败 -> 不限制
remoteManagedSettings -> 获取失败 -> 不加远程设置
settingsSync -> 获取失败 -> 使用本地设置
bootstrap -> 获取失败 -> 使用缓存
GrowthBook -> 初始化前 -> 使用上次缓存值
远程数据服务共享相同的轮询模式:
policyLimits -> 1h 轮询, ETag 缓存, 5 次重试
remoteManagedSettings -> 1h 轮询, checksum, 5 次重试
- PII 防护贯穿始终: analytics 的类型系统强制显式标记, 防止意外泄露代码或文件路径
- 多层压缩策略: 从轻量 (微压缩/API 微压缩) 到重量 (全量压缩), 从主动 (阈值触发) 到被动 (错误触发), 形成完整的上下文管理梯队
- Build-time 死码消除:
feature()宏配合bun:bundle确保 ant-only 代码不出现在外部构建中 - AsyncGenerator 错误传播: withRetry 使用 generator 在重试等待期间向调用方 yield 状态消息, 不阻塞 UI 更新
- 连续失败熔断: 自动压缩的 3 次连续失败上限源于真实生产数据 (BQ 2026-03-10), 防止无效 API 调用浪费