price changes of existing products need --allow-price-changes (play products, play/apple subscriptions, prices import): --yes and flow run --apply --yes confirm creates, first prices and other updates only; without the flag the run writes the rest, names each held change and exits 3 (declined), dry runs mark them owner, flows refuse the flag; lib/price-gate.ts, tests/price-gate.test.ts. CLI wording: console fill leaves what it cannot open or fill to the calling agent's browser tool (console plan / verify) instead of 'yourself'; other 'by hand' output points at the config; do-it-dont-ask test scans cli/ output
This commit is contained in:
parent
b5b8f2da9d
commit
e01a27d7b7
33 changed files with 414 additions and 117 deletions
|
|
@ -781,13 +781,18 @@ export async function resumeAt(page: Page, plan: NavStep[]): Promise<number | nu
|
|||
return null;
|
||||
}
|
||||
|
||||
/** Where the calling agent picks up when storebound's own fill cannot: its browser tool, with the plan and the read-back. */
|
||||
export function agentPart(form: string): string {
|
||||
return `the calling agent with its browser tool (\`storebound console plan ${form}\` has the steps and answers, \`storebound console verify ${form}\` checks the page)`;
|
||||
}
|
||||
|
||||
function printText(res: ConsoleResult): void {
|
||||
console.log(`${res.title}\n${res.url}\n`);
|
||||
for (const a of res.answers) {
|
||||
const tag = a.agreement ? '[you] ' : a.ownerCheck ? '[owner]' : a.certain ? '[ok] ' : '[check]';
|
||||
console.log(` ${tag} ${a.label}: ${a.agreement ? '(you tick it; storebound never does)' : valueText(a)} (${a.source})${(a.certain && !a.ownerCheck) || !a.note ? '' : `\n ${a.note}`}`);
|
||||
}
|
||||
console.log(`\n${res.uncertain} answer(s) to check yourself.\n\nSteps:\n${res.guidance.map(g => ` - ${g}`).join('\n')}`);
|
||||
console.log(`\n${res.uncertain} answer(s) the config does not prove ([check] / [owner]): ask the owner for them in one batch, put them in storebound.config.ts, run again.\n\nSteps:\n${res.guidance.map(g => ` - ${g}`).join('\n')}`);
|
||||
}
|
||||
|
||||
export async function run(ctx: Ctx): Promise<void> {
|
||||
|
|
@ -1012,7 +1017,7 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
if (r.signIn) return;
|
||||
// refused / ambiguous: not retried on this page; "not found" / "did not appear" is retried when the step shows up
|
||||
if (!/^(not found|did not appear)$/.test(r.why ?? '')) tried = `${(active() ?? b.page).url()}#${plan.findIndex(s => s.label === r.failed)}`;
|
||||
warn(TAG, `could not open the form by itself at step "${r.failed}" (${r.why}). Open it yourself; storebound keeps watching and fills it when it appears:`);
|
||||
warn(TAG, `could not open the form by itself at step "${r.failed}" (${r.why}). Opening it is left to ${agentPart(form)}; storebound keeps watching and fills it when it appears:`);
|
||||
if (r.candidates) say(r.failed === 'the developer account' ? `developer accounts: ${r.candidates.join(' | ')} (set play.developerAccount or play.developerId to pick one)`
|
||||
: `apps that match: ${r.candidates.join(' | ')} (set apple.appId / play.packageName in the config to pick one)`);
|
||||
if (first) for (const s of meta.open) say(`open the form: ${s}`);
|
||||
|
|
@ -1100,7 +1105,7 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
const open = async (key: string, what: string, loc: Locator | null): Promise<void> => {
|
||||
if (!loc) return;
|
||||
const n = opened.get(key) ?? 0;
|
||||
if (n === 2) { opened.set(key, 3); warn(TAG, `${what} did not open after two clicks: the console UI may have changed. Open it yourself; storebound fills it when it appears`); }
|
||||
if (n === 2) { opened.set(key, 3); warn(TAG, `${what} did not open after two clicks: the console UI may have changed. Opening it is left to ${agentPart(form)}; storebound fills it when it appears`); }
|
||||
if (n >= 2) return;
|
||||
opened.set(key, n + 1);
|
||||
await flowClick(loc, form, dataTypes, say, clicked);
|
||||
|
|
@ -1198,8 +1203,8 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
const own: string[] = inDialog ? await dl.getByRole('button').filter({ visible: true }).evaluateAll((els: any[]) => els.map(e => (e.getAttribute('aria-label') || e.textContent || '').trim().replace(/\s+/g, ' ')).filter(Boolean).slice(0, 10)).catch(() => []) : [];
|
||||
const top = own.length ? own : dbg.topButtons(d?.summary ?? await dbg.summarize(p).catch(() => null));
|
||||
say(`${expecting} is not a page storebound knows (nothing it can fill or click after ${mins(Date.now() - progressAt)}). Buttons ${own.length ? 'in the dialog' : 'there'}: ${top.length ? top.map(t => `"${t}"`).join(', ') : '(none)'}${d ? `. Debug saved to ${d.dir}` : ''}`);
|
||||
if (currentType && inDialog) say(`paused on "${currentType}": this step of its dialog is new to storebound. Answer it in the browser and go on in the dialog; storebound fills the next step it knows (it stops after ${mins(idleMs)} without progress)`);
|
||||
say(`do this part yourself; storebound keeps watching and goes on when it sees a page it knows (it stops after ${mins(idleMs)} without progress):`);
|
||||
if (currentType && inDialog) say(`paused on "${currentType}": this step of its dialog is new to storebound. It is left to ${agentPart(form)}: answer it and go on in the dialog; storebound fills the next step it knows (it stops after ${mins(idleMs)} without progress)`);
|
||||
say(`this part is left to ${agentPart(form)}; storebound keeps watching and goes on when it sees a page it knows (it stops after ${mins(idleMs)} without progress):`);
|
||||
for (const g of res.guidance) say(` ${g}`);
|
||||
};
|
||||
/** Every round: a heartbeat line, the step watchdog (not while the owner answers or signs in), the idle limit. */
|
||||
|
|
@ -1254,10 +1259,10 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
misses = 0;
|
||||
// the miss lasts: fill every field that was found, leave the missing ones to the owner (only all-missing is a UI change)
|
||||
if (s.fallback) { try { s = await fillPage(p, def, pageAnswers, done, true, currentType); } catch { await Bun.sleep(tick); continue; } }
|
||||
if (!s.fallback && s.missing.length) warn(TAG, `${s.title}: could not find ${s.missing.map(id => labels.get(id) ?? id).join(', ')} on the page (the console UI may have changed): fill ${s.missing.length === 1 ? 'it' : 'them'} yourself`);
|
||||
if (!s.fallback && s.missing.length) warn(TAG, `${s.title}: could not find ${s.missing.map(id => labels.get(id) ?? id).join(', ')} on the page (the console UI may have changed): filling ${s.missing.length === 1 ? 'it' : 'them'} is left to ${agentPart(form)}`);
|
||||
if (s.fallback) {
|
||||
fallback = true;
|
||||
warn(TAG, `the page does not match what storebound expects (${s.missing.join(', ')}): the console UI changed. Nothing more is filled; follow these steps:`);
|
||||
warn(TAG, `the page does not match what storebound expects (${s.missing.join(', ')}): the console UI changed. Nothing more is filled; the rest is left to ${agentPart(form)}, following these steps:`);
|
||||
for (const g of res.guidance) say(` ${g}`);
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue