关于 gpc-sdk-usage
gpc-sdk-usage 是一项面向 TypeScript 开发者的技能,介绍如何单独使用 @gpc-cli/api 和 @gpc-cli/auth 以编程方式访问 Google Play。内容涵盖认证、API 客户端及常见发布和运营操作。
主要功能
- 01配置服务账号认证
- 02创建类型化 API 客户端
- 03管理应用编辑与发布
- 04处理评论、订阅和购买
- 05分页查询与速率限制
适用场景
- 01从 TypeScript 服务管理 Google Play 发布
- 02在脚本中查询并回复应用评论
- 03在后端验证应用内购买
- 04将 Play 操作集成到现有应用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yasserstudio/ gpc-skills --skill gpc-sdk-usage安装后在 Claude Code 中输入/gpc-sdk-usage确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/api-reference.mdauth-patterns.mdscripts/detect_gpc.mjs详细介绍
gpc-sdk-usage
将 @gpc-cli/api 和 @gpc-cli/auth 用作独立的 TypeScript SDK 包,以编程方式访问 Google Play。
适用场景
- 构建与 Google Play 交互的后端服务
- 创建自定义仪表板或自动化脚本
- 使用 TypeScript/JavaScript 以编程方式管理发布
- 直接使用类型化 API 客户端(而非通过 CLI)
- 将 Google Play 操作集成到更大型的应用程序中
所需输入
- Node.js 20+ 和 TypeScript 5+
- @gpc-cli/api 和 @gpc-cli/auth 包
- 服务账号密钥——JSON 文件或原始 JSON 字符串
操作步骤
0. 安装包
npm install @gpc-cli/api @gpc-cli/auth
这些是独立的包,无需安装完整的 CLI。
1. 身份验证
import { resolveAuth } from "@gpc-cli/auth";
// From file path
const auth = await resolveAuth({
serviceAccountPath: "/path/to/key.json",
});
// From JSON string (e.g., from environment variable)
const auth = await resolveAuth({
serviceAccountJson: process.env.PLAY_SA_KEY,
});
// From environment (GPC_SERVICE_ACCOUNT or GOOGLE_APPLICATION_CREDENTIALS)
const auth = await resolveAuth();
Read: references/auth-patterns.md,了解高级身份验证模式和令牌缓存。
2. 创建 API 客户端
import { createApiClient } from "@gpc-cli/api";
const client = createApiClient({
auth,
maxRetries: 3,
timeout: 30_000,
});
该客户端提供对 234 个 Google Play Developer API 端点的类型化访问,涵盖 Android Publisher v3、Play Developer Reporting v1beta1、Play Custom App Publishing v1(v0.9.56+)和 Games Configuration v1configuration(v0.9.86+)API。
2a. 创建 Enterprise 客户端(v0.9.56+)
若要通过 Play Custom App Publishing API 发布私有应用,请使用单独的工厂函数:
import { createEnterpriseClient, type CustomApp } from "@gpc-cli/api";
const enterprise = createEnterpriseClient({ auth });
const app: CustomApp = await enterprise.apps.create(
"1234567890", // developer account ID (int64, from Play Console URL)
"./app.aab", // bundle path
{
title: "My Private App",
languageCode: "en_US",
organizations: [{ organizationId: "customer-org-id" }],
},
);
console.log("Assigned package name:", app.packageName);
// com.google.customapp.A1B2C3D4E5 (Google-assigned, you cannot influence)
注意:
- 私有应用将永久保持私有。创建后,无法将其设为公开。
- 创建后,后续操作(版本上传、轨道、商品详情)需使用返回的
packageName,通过常规的createApiClient()执行。 - 你的服务账号需要在 Play Console 中拥有“创建并发布私有应用”权限。
- 底层的
HttpClient.uploadCustomApp<T>(path, filePath, metadata, contentType)方法会处理可恢复的分段上传,并在初始会话发起 POST 请求中携带 JSON 元数据。有关将此模式与其他 Google API 配合使用的方法,请参阅ResumableUploadOptions.initialMetadata。
请参阅 gpc-enterprise 技能,了解 CLI 的对应操作方式和完整设置步骤。
Read: references/api-reference.md,查看包含所有命名空间和方法的完整客户端 API。
3. 编辑生命周期
大多数 Google Play 操作都需要编辑会话:
const APP = "com.example.app";
// 1. Create an edit
const edit = await client.edits.insert(APP);
// 2. Make changes within the edit
const tracks = await client.tracks.list(APP, edit.id);
const details = await client.details.get(APP, edit.id);
// 3. Validate before committing
await client.edits.validate(APP, edit.id);
// 4. Commit the edit (applies all changes)
await client.edits.commit(APP, edit.id);
// Optional: commit with options (v0.9.51+)
await client.edits.commit(APP, edit.id, {
changesNotSentForReview: true,
changesInReviewBehavior: "HALT_REVIEW",
});
重要: 同一时间只能打开一个编辑。务必提交或删除编辑。
4. 常见模式
创建自定义封闭式测试轨道(v0.9.79+)
// Create a custom closed testing track before uploading to it
const track = await client.edits.tracks.create(packageName, "my-custom-track");
必须先创建自定义轨道,才能向其分配发布。创建后,使用标准的 client.tracks.update() 调用,将发布推送到新轨道。
上传发布
const edit = await client.edits.insert(APP);
// Upload the bundle
const bundle = await client.bundles.upload(APP, edit.id, "app-release.aab");
// Upload with device tier config (v0.9.51+)
const bundle2 = await client.bundles.upload(APP, edit.id, "app-release.aab", {
deviceTierConfigId: "my-tier-config",
});
// Set the track
await client.tracks.update(APP, edit.id, "beta", {
track: "beta",
releases: [{
versionCodes: [bundle.versionCode],
status: "completed",
releaseNotes: [
{ language: "en-US", text: "Bug fixes and improvements" },
],
}],
});
// Commit
await client.edits.validate(APP, edit.id);
await client.edits.commit(APP, edit.id);
列出评价并回复
// No edit needed for reviews
const reviews = await client.reviews.list(APP, {
maxResults: 50,
translationLanguage: "en",
startIndex: 0, // pagination offset (v0.9.51+)
});
for (const review of reviews.reviews ?? []) {
if (review.comments?.[0]?.userComment?.starRating <= 2) {
await client.reviews.reply(APP, review.reviewId, "Thanks for the feedback!");
}
}
管理订阅
// No edit needed for subscriptions
const subs = await client.subscriptions.list(APP);
// Get a specific subscription
const sub = await client.subscriptions.get(APP, "premium_monthly");
// Update with mutation options (v0.9.51+)
await client.subscriptions.update(APP, "premium_monthly", data, "price", {
allowMissing: true,
latencyTolerance: "PRODUCT_UPDATE_LATENCY_TOLERANCE_LATENCY_TOLERANT",
});
// Activate a base plan
await client.subscriptions.activateBasePlan(APP, "premium_monthly", "monthly");
验证购买
// No edit needed for purchases
const purchase = await client.purchases.getProduct(APP, "coins_100", purchaseToken);
if (purchase.purchaseState === 0 && purchase.acknowledgementState === 0) {
await client.purchases.acknowledgeProduct(APP, "coins_100", purchaseToken);
}
上传反混淆文件(v0.9.51+)
// Upload ProGuard mapping
await client.deobfuscation.upload(APP, edit.id, versionCode, "mapping.txt", "proguard");
// Upload native debug symbols
await client.deobfuscation.upload(APP, edit.id, versionCode, "symbols.zip", "nativeCode");
管理扩展文件(v0.9.51+)
// Get expansion file info
const obb = await client.expansionFiles.get(APP, edit.id, versionCode, "main");
// Upload a new expansion file
const uploaded = await client.expansionFiles.upload(APP, edit.id, versionCode, "main", "main.obb");
// Patch expansion file references
await client.expansionFiles.patch(APP, edit.id, versionCode, "main", {
referencesVersion: 10,
});
分页列出一次性商品(v0.9.51+)
const products = await client.oneTimeProducts.list(APP, {
pageSize: 25,
pageToken: nextToken,
});
5. 分页
使用内置分页工具处理大型结果集:
import { paginate, paginateAll } from "@gpc-cli/api";
// Async generator (stream results)
for await (const page of paginate(
(token) => client.subscriptions.list(APP, { pageToken: token }),
{ limit: 100 },
)) {
for (const sub of page) {
console.log(sub.productId);
}
}
// Collect all results
const all = await paginateAll(
(token) => client.subscriptions.list(APP, { pageToken: token }),
);
v0.9.83 新增:
paginateAll现在会返回真实的续页令牌,因此--limit+--next-page可以在评价、用户、购买、IAP 和订阅列表中正确续页。所有 CLI 列表命令也统一使用{ <key>, nextPageToken, meta.count, message? }JSON 信封格式(v0.9.87 起扩展至 grants、testers 和 tracks)。从列表命令读取裸数组的脚本将无法运行,请更新为解构带键的字段。
v0.9.98:
client.users.list()始终会发送pageSize=-1,这是 Google 唯一接受的值(“目前不支持分页”)。调用方提供的pageSize会被忽略,并在类型中标记为弃用;整个列表会在一次响应中返回。
6. 速率限制
从 v0.9.47 开始,createApiClient() 会使用 Google 的六桶模型(每桶 3,000 次查询/分钟)自动对所有 API 调用应用速率限制。无需手动配置:
// Rate limiting is automatic — all calls are throttled by resource type
const client = createApiClient({ auth });
// Buckets: edits, purchases, reviews, reporting, monetization, default
若要自定义速率限制(例如,在多个进程间共享配额):
import { createRateLimiter, RATE_LIMIT_BUCKETS } from "@gpc-cli/api";
// Override specific buckets
const limiter = createRateLimiter([
{ ...RATE_LIMIT_BUCKETS.edits, maxTokens: 1500 }, // Half of default
{ ...RATE_LIMIT_BUCKETS.purchases, maxTokens: 1500 },
]);
const client = createApiClient({ auth, rateLimiter: limiter });
resolveBucket(path) 函数会自动将 API 路径映射到对应的桶:
/edits/路径 →edits桶/purchases/、/orders→purchases桶/reviews→reviews桶- Reporting API →
reporting桶 /subscriptions、/oneTimeProducts、/inappproducts→monetization桶- 其他所有路径 →
default桶
7. 错误处理
import { PlayApiError } from "@gpc-cli/api";
import { AuthError } from "@gpc-cli/auth";
try {
await client.edits.insert(APP);
} catch (error) {
if (error instanceof AuthError) {
console.error(`Auth failed: ${error.code}`);
} else if (error instanceof PlayApiError) {
console.error(`API error ${error.status}: ${error.code}`);
console.error(`Suggestion: ${error.suggestion}`);
}
}
更新日志生成(v0.9.62+)
gpc changelog generate 的更新日志流水线以独立的 @gpc-cli/core 导出项形式提供,可直接获取经过聚类和检查的数据结构,适用于 CI 工具。
import {
generateChangelog,
resolveLocales,
renderPlayStore,
PLAY_STORE_LIMIT, // 500
type LocaleBundle,
type GeneratedChangelog,
} from "@gpc-cli/core";
const generated: GeneratedChangelog = await generateChangelog({
from: "v0.9.61",
to: "HEAD",
});
// GitHub target: three renderers exposed as RENDERERS["md" | "json" | "prompt"]
// Play Store target: resolveLocales + renderPlayStore
const locales = await resolveLocales("en-US,fr-FR,de-DE");
const { output, bundle } = renderPlayStore(generated, {
locales,
format: "json",
});
for (const entry of bundle.locales) {
console.log(`${entry.language}: ${entry.chars}/${entry.limit} (${entry.status})`);
}
对于 --locales auto,将 { client, packageName } 作为第二个参数传给 resolveLocales——它会调用 client.listings.list,从你当前的 Play Store 列表推断语言区域集合。
将发布说明应用到草稿(v0.9.64+)
import {
applyReleaseNotes,
validateBundleForApply,
bundleToReleaseNotes,
waitForBundleProcessing,
} from "@gpc-cli/core";
// Convert a LocaleBundle to the API shape
const releaseNotes = bundleToReleaseNotes(bundle);
// Validate (returns blocked locale errors, if any)
const errors = validateBundleForApply(bundle);
if (errors.length > 0) throw new Error(errors.join(", "));
// Write into the latest draft on a track
await applyReleaseNotes(client, "com.example.app", "production", releaseNotes);
// waitForBundleProcessing (v0.9.64+, extended v0.9.77): polls bundles.list
// after AAB upload with Fibonacci backoff (2s, 3s, 5s, 8s, 13s, 21s, 34s ~86s)
// until the uploaded versionCode appears. Fixes large-AAB race.
// v0.9.77 also adds multi-retry guard on validate/commit (15s, 30s, 45s).
await waitForBundleProcessing(client, "com.example.app", editId, versionCode);
配置类型中的 VitalsThresholds(v0.9.82+)
VitalsThresholds 现在属于 @gpc-cli/config 提供的类型化配置接口:
import type { GpcConfig } from "@gpc-cli/config";
const config: GpcConfig = {
vitals: {
thresholds: { crashRate: 2.0 },
},
};
ResolvedConfig(完全合并后的运行时结构)中也包含 VitalsThresholds。在构建以编程方式读取或写入 GPC 配置文件的工具时,可以使用它。
Orders 上的 OfferPhaseDetails(v0.9.79+)
Orders 上的扁平 offerPhase 字符串字段已弃用。请改为读取 offerPhaseDetails:
const order = await client.purchases.orders.get(APP, orderId);
// Deprecated: order.offerPhase
// Preferred:
const phase = order.offerPhaseDetails; // OfferPhaseDetails — phase type, cycle counts, pricing
download() 指数退避(v0.9.80+)
client.download()(用于下载 APK/AAB 二进制文件)现在会自动使用指数退避重试,与 request() 的重试行为一致。无需更改代码——临时的 5xx 错误和网络超时都会自动重试。
从 Play GCS 存储桶获取批量报告(v0.9.93+)
Play 的月度批量报告是存储在与开发者账号关联的 Cloud Storage 存储桶中的 CSV 对象,而不是 Publisher API 资源。**v0.9.93 中的破坏性变更:**已从 @gpc-cli/api 移除不可用的 client.reports.list,并从 @gpc-cli/core 移除旧的 downloadReport 导出项。请改用三个新的 @gpc-cli/core 导出项。
import { resolveAuth, DEFAULT_SCOPES, STORAGE_READ_ONLY_SCOPE } from "@gpc-cli/auth";
import {
listReports,
downloadStatsReport,
downloadFinancialReport,
resolveReportsBucket,
parseMonth,
type ListReportsResult,
} from "@gpc-cli/core";
// The reports path needs one extra scope. Request it only here: storage-scoped tokens are
// cached under a separate key, so other commands' tokens never carry storage access.
const auth = await resolveAuth({
serviceAccountPath: "key.json",
scopes: [...DEFAULT_SCOPES, STORAGE_READ_ONLY_SCOPE],
});
// pubsite_prod_<developerId>, or an explicit reports.bucket value
const bucket = resolveReportsBucket({ developerId: "1234567890" });
const month = parseMonth("2026-02");
const { reports, nextPageToken }: ListReportsResult = await listReports(
auth,
bucket,
"installs",
{ packageName: "com.example.app", month, maxResults: 50 },
);
// Stats: per app, one CSV per dimension (reviews reports have no dimension)
const stats = await downloadStatsReport(
auth, bucket, "com.example.app", "installs", month, "country",
);
console.log(stats.objectName, stats.csv); // gunzipped, UTF-16 decoded to UTF-8
// Financial: account-level, ZIP archives
const fin = await downloadFinancialReport(auth, bucket, "earnings", month);
if (fin.kind === "csv") {
console.log(fin.text);
} else {
for (const entry of fin.entries) console.log(entry.name, entry.csv.length);
}
auth 只需满足 ReportsAuth({ getAccessToken(): Promise<string> })接口,因此任何令牌来源都可使用。失败时会抛出带有 REPORT_* 代码的 GpcError(参见 gpc-troubleshooting);首次运行时会抛出 REPORT_ACCESS_DENIED,直到在 Play Console 中为服务账号授予“查看应用信息并下载批量报告(只读)”权限。
API 正确性历史(近期)
- v0.9.57:
apprecovery.cancel/deployURL 现在使用复数路径/appRecoveries/。dataSafety.update使用POST,而非PUT。已移除不存在的dataSafety.get。新增onetimeproducts.offers.activateOffer/deactivateOffer。新增getVitalsErrorCount函数。 - **v0.9.58 / v0.9.59:**Vitals LMK 指标集为
lmkRateMetricSet,包含指标userPerceivedLmkRate、userPerceivedLmkRate7dUserWeighted、userPerceivedLmkRate28dUserWeighted、distinctUsers。(v0.9.58 使用了错误的资源名称;v0.9.59 为修正后的版本。)
验证
resolveAuth()返回有效的认证客户端createApiClient({ auth })创建可用的客户端client.edits.insert(APP)成功打开编辑- API 调用返回类型化响应
- 错误处理能够捕获
PlayApiError和AuthError
故障模式/调试
| 症状 | 可能原因 | 修复方法 |
|---|---|---|
AUTH_NO_CREDENTIALS | 未找到身份验证来源 | 传入 serviceAccountPath 或设置 GPC_SERVICE_ACCOUNT |
AUTH_INVALID_KEY | 密钥文件中的 JSON 无效 | 从 Google Cloud Console 重新下载 |
| 编辑插入操作失败,返回 403 | 服务账号没有 API 访问权限 | 在 GCP 中启用 Google Play Developer API |
| 并发编辑冲突 | 另一个编辑操作仍处于打开状态 | 先提交或删除现有编辑操作 |
PlayApiError,状态码为 429 | 请求受限 | 使用 createRateLimiter() 并设置合适的分桶 |
| 类型无法解析 | TypeScript 配置错误 | 确保 moduleResolution: "bundler" 或 "node16" |
client.reports.list 不是函数 | 在 v0.9.93 中已移除(此前从未正常工作) | 使用 @gpc-cli/core 中的 listReports / downloadStatsReport / downloadFinancialReport |
报表调用返回 REPORT_ACCESS_DENIED | 服务账号缺少批量报表授权 | 在 Play Console 中启用“查看应用信息并下载批量报表(只读)”,然后重试 |
| 已授权,但报表调用仍返回 403 | 创建令牌时未包含 devstorage.read_only | 将 scopes: [...DEFAULT_SCOPES, STORAGE_READ_ONLY_SCOPE] 传给 resolveAuth |
相关技能
- gpc-setup — 创建服务账号并配置身份验证
- gpc-plugin-development — 构建内部使用 SDK 的插件
- gpc-troubleshooting — 解读 API 错误代码