console: Play App content card Saves are the agent's (declaration-save; Send changes for review, Submit, Publish, Restart review stay forbidden), one owner batch per plan with the IARC Terms tick before the run, console plan play-app-content --all, New App fields typed not set, play.appId from the Create app redirect (console verify --url --write) and storebound play app-id across app list pages

This commit is contained in:
CRTkafa 2026-10-13 05:43:56 +08:00
parent 1366e7e231
commit f741c15829
29 changed files with 1029 additions and 165 deletions

View file

@ -16,12 +16,15 @@ import { type Answer, consoleAnswers, type FormId, type PlanFormId, type PlanOnl
import { APP_CONTENT_CARDS, playOnlyPart, startLabel } from './console-play-plans';
import { APPLE_PRIVACY_TYPES } from './app-privacy';
import { typeLabels } from './console-forms';
import { playAppIdInText } from './play-app-id';
import { APPLE_PURPOSES } from './console-answers';
export type RowStatus = 'match' | 'mismatch' | 'not-found' | 'ask-owner';
export interface VerifyRow { item: string; expected: string; seen: string; status: RowStatus; note?: string }
export interface VerifyResult {
form: PlanFormId;
/** play-app-record: the Play Console app id read from the URL after Create app (/app/<appId>/...). */
playAppId?: string;
source: 'page-text' | 'inputs' | 'api';
rows: VerifyRow[];
mismatches: number;
@ -143,6 +146,8 @@ export interface InputStates {
checked?: string[];
/** The visible page text (for "Select …" hints and availability messages). */
text?: string;
/** location.href (after Create app: /app/<appId>/app-dashboard). */
url?: string;
}
/** The capture snippet an agent runs in the page for `console verify play-app-record --from-text -` (prints JSON). */
@ -150,7 +155,7 @@ export const PLAY_CREATE_CAPTURE = `(() => { const t = e => (e?.innerText ?? e?.
const name = e => e.getAttribute('aria-label') || t(e.labels?.[0]) || (e.getAttribute('aria-labelledby') || '').split(/\\s+/).map(id => t(document.getElementById(id))).join(' ').trim() || t(e.closest('label')) || t(e.parentElement);
const fields = {}; for (const i of document.querySelectorAll('input:not([type]), input[type=text], textarea')) if (i.offsetParent) fields[name(i)] = i.value;
const checked = [...document.querySelectorAll('[role=radio][aria-checked=true], input[type=radio]:checked')].map(r => name(r) || t(r.closest('material-radio')));
return JSON.stringify({ fields, checked, text: (document.querySelector('main') ?? document.body).innerText }); })()`;
return JSON.stringify({ url: location.href, fields, checked, text: (document.querySelector('main') ?? document.body).innerText }); })()`;
export function parseInputStates(raw: string): InputStates | null {
const s = raw.trim();
@ -159,7 +164,7 @@ export function parseInputStates(raw: string): InputStates | null {
const j = JSON.parse(s) as Record<string, unknown>;
const fields = j.fields && typeof j.fields === 'object' ? Object.fromEntries(Object.entries(j.fields as Record<string, unknown>).filter(([, v]) => typeof v === 'string')) as Record<string, string> : undefined;
const checked = Array.isArray(j.checked) ? j.checked.filter((x): x is string => typeof x === 'string') : undefined;
return { ...(fields ? { fields } : {}), ...(checked ? { checked } : {}), ...(typeof j.text === 'string' ? { text: j.text } : {}) };
return { ...(fields ? { fields } : {}), ...(checked ? { checked } : {}), ...(typeof j.text === 'string' ? { text: j.text } : {}), ...(typeof j.url === 'string' ? { url: j.url } : {}) };
} catch { return null; }
}
@ -169,6 +174,16 @@ export const SELECT_HINT = /^select (app or game|free or paid|an? .+)$/i;
export function verifyPlayAppRecord(config: ShipConfig, raw: string): VerifyResult {
const answers = consoleAnswers('play-app-record', config).filter(a => !a.agreement);
const st = parseInputStates(raw);
// after Create app (verified 2026-10-12 / 2026-10-13): the console redirects to /app/<appId>/app-dashboard; the form
// is gone, so only the app id is checked (and handed to the play.appId upsert)
const appId = playAppIdInText(st?.url ?? raw);
const formShown = st ? Object.keys(st.fields ?? {}).length > 0 : /\b(App name|App package name|Uygulama adı)\b/.test(raw);
if (appId && !formShown) {
const want = config.play?.appId;
const row: VerifyRow = { item: 'Play app id (the URL after Create app)', expected: want ?? '(not in the config yet: play.appId)', seen: appId,
status: want && want !== appId ? 'mismatch' : 'match', ...(want && want !== appId ? { note: 'play.appId differs: --write replaces it' } : {}) };
return { ...result('play-app-record', st ? 'inputs' : 'page-text', [row], ['The Create app form is gone: the app was created. --write saves the id into play.appId.']), playAppId: appId };
}
const text = st?.text ?? (st ? '' : raw);
const ls = lines(text);
const rows: VerifyRow[] = [];
@ -332,7 +347,7 @@ export function verifyPlayDeclaration(form: Exclude<PlanOnlyFormId, 'play-app-co
const blob = fold(text);
const rows: VerifyRow[] = [];
for (const a of part.answers) {
if (a.status === 'agreement') continue;
if (a.status === 'agreement' || a.id === 'credentials') continue; // the demo account is never read back or printed
if (a.status === 'ask-owner' || a.value === null) { rows.push({ item: a.question, expected: '(owner decides)', seen: '-', status: 'ask-owner' }); continue; }
const want = a.action === 'type' || a.action === 'choose' ? String(a.value) : a.label ?? '';
const plain = !want || /^(yes|no)$/i.test(want) || a.action === 'untick';