upload --pre-check/--evidence, shots copy translate, game-center --locale-map, play games API-disabled hint

This commit is contained in:
CRTkafa 2026-10-24 12:10:23 +08:00
parent 29a9683ab7
commit 8ffa27d4f8
24 changed files with 598 additions and 27 deletions

View file

@ -14,16 +14,30 @@ import { confirm, need } from '../lib/ctx';
import { dryPlan } from '../lib/dry';
import { hasAndroidProject, type ItemPlan, pgsFindings, type PlayExisting, planPlayGames, planRows, readPgsProject, usesPlayGames } from '../lib/game-services';
import { PLAY_SCOPE, tokenSource } from '../lib/google-auth';
import { enableUrl, findingText, parseGoogleError } from '../lib/google-errors';
import { errorBody } from '../lib/http';
import { done, log, warn } from '../lib/log';
import * as rt from '../lib/rt';
import { AmbiguousWriteError, createOnce, retrySafe } from '../lib/upsert';
const TAG = 'play-games';
export const GAMES_SERVICE = 'gamesconfiguration.googleapis.com';
export const GAMES_API_TITLE = 'Google Play Game Services Publishing API';
export const GAMES_CONFIG_API = 'https://gamesconfiguration.googleapis.com/games/v1configuration';
/** The Games Configuration API is off in the service account's Cloud project (403 SERVICE_DISABLED / accessNotConfigured): the exact API name, where to turn it on and the console plan. Null for any other error. */
export function gamesApiDisabledText(errText: string, fallbackProject: string | null): string | null {
const f = parseGoogleError(errText);
if (f?.kind !== 'api-disabled') return null;
const projectId = f.projectId ?? fallbackProject;
const line = findingText({ ...f, service: GAMES_SERVICE, serviceTitle: GAMES_API_TITLE, projectId, activationUrl: enableUrl(GAMES_SERVICE, projectId) }, { withUrl: true });
return `${line}
API: ${GAMES_API_TITLE} (${GAMES_SERVICE}). Enable it, wait a minute, then run storebound play games again. Plan: storebound console plan play-games-services`;
}
export function gamesConfigClient(root: string) {
const token = tokenSource(playServiceAccount(root), PLAY_SCOPE);
const account = playServiceAccount(root);
const token = tokenSource(account, PLAY_SCOPE);
async function call<T = unknown>(method: string, path: string, body?: unknown): Promise<T> {
const url = GAMES_CONFIG_API + path;
const safe = method !== 'POST' && retrySafe(method, url);
@ -37,7 +51,11 @@ export function gamesConfigClient(root: string) {
throw new Error(`${method} ${path} -> ${r.status} ${errorBody(t, 300)}`);
}
const t = await r.text();
if (!r.ok) throw new Error(`${method} ${path} -> ${r.status} ${errorBody(t, 800)}`);
if (!r.ok) {
const msg = `${method} ${path} -> ${r.status} ${errorBody(t, 800)}`;
const off = r.status === 403 ? gamesApiDisabledText(msg, account.project_id ?? null) : null;
throw new Error(off ? `${msg.split(' -> ')[0]} -> 403: ${off}` : msg);
}
return (t ? JSON.parse(t) : {}) as T;
}
}