desktop reads everything by itself: one background read scheduler (4 per store, starts spaced per account, App Store Connect X-Rate-Limit slows it below 20 % and pauses at 100 left, backoff with jitter on 429 / RESOURCE_EXHAUSTED, visible app first, one read per key), per-method TTLs over the saved reads, Settings auto-read (on) and interval (15 min, paused while hidden), Refresh all, a refresh icon and 'read 3 min ago' / 'couldn't refresh' per card, writes invalidate, status line with progress and quota; quota reaches the window through ToolResult.quota (cli/lib/quota.ts); Health top issues open with versions, sample stack (health --issue / --signature, read health_issue), Open in Play Console, Copy for AI and likely-cause hints (cli/lib/crash-hints.ts); tests/scheduler.test.ts, tests/quota-crash-hints.test.ts, boot check auto-read case

This commit is contained in:
CRTkafa 2026-10-13 23:36:30 +08:00
parent 89256f1e80
commit 0fb4ce0001
46 changed files with 1531 additions and 141 deletions

View file

@ -4,6 +4,7 @@ import { createHash, createPrivateKey, sign } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { ascKey } from '../lib/credentials';
import { errorBody } from '../lib/http';
import { noteAsc } from '../lib/quota';
import { AmbiguousWriteError, retrySafe } from '../lib/upsert';
import * as rt from '../lib/rt';
@ -77,6 +78,7 @@ export async function ascClient(root: string, bundleId: string, appIdHint?: stri
if (!safe) throw new AmbiguousWriteError(`${method} ${path} -> no answer (${(e as Error).message})`);
throw e;
}
noteAsc(r);
const t = await r.text();
if ((r.status === 429 || (r.status >= 500 && safe)) && attempt < 5) { await rt.sleep(2000 * attempt); return asc(method, path, body, attempt + 1); }
if (r.status >= 500 && !safe) throw new AmbiguousWriteError(`${method} ${path} -> ${r.status} ${errorBody(t)}`, r.status);
@ -89,6 +91,7 @@ export async function ascClient(root: string, bundleId: string, appIdHint?: stri
async function getAs(path: string, accept: string): Promise<J> {
for (let attempt = 1; ; attempt++) {
const r = await fetch('https://api.appstoreconnect.apple.com' + path, { headers: { Authorization: 'Bearer ' + jwt(), Accept: accept } });
noteAsc(r);
const t = await r.text();
if ((r.status >= 500 || r.status === 429) && attempt < 5) { await rt.sleep(2000 * attempt); continue; }
if (!r.ok) throw new Error(`GET ${path} -> ${r.status} ${errorBody(t, 800)}`);
@ -101,6 +104,7 @@ export async function ascClient(root: string, bundleId: string, appIdHint?: stri
async function bytes(path: string, accept = 'application/a-gzip'): Promise<Uint8Array | null> {
for (let attempt = 1; ; attempt++) {
const r = await fetch('https://api.appstoreconnect.apple.com' + path, { headers: { Authorization: 'Bearer ' + jwt(), Accept: accept } });
noteAsc(r);
if ((r.status >= 500 || r.status === 429) && attempt < 5) { await r.body?.cancel(); await rt.sleep(2000 * attempt); continue; }
if (r.status === 404) { await r.body?.cancel(); return null; }
if (!r.ok) throw new Error(`GET ${path.replace(/\?.*$/, '')} -> ${r.status} ${errorBody(await r.text(), 600)}`);