From 4d2a5f5c2e0a66eb32d8de4e2c12687e150b66f4 Mon Sep 17 00:00:00 2001 From: luk Date: Fri, 31 Jul 2026 07:41:54 +0800 Subject: [PATCH] improved --- dcloud-renew/renew.mjs | 149 ++++++++++++++++++++++++++++------------- 1 file changed, 103 insertions(+), 46 deletions(-) diff --git a/dcloud-renew/renew.mjs b/dcloud-renew/renew.mjs index ac5f8ad..7ee89fa 100644 --- a/dcloud-renew/renew.mjs +++ b/dcloud-renew/renew.mjs @@ -1,15 +1,19 @@ import 'dotenv/config' import { chromium } from 'playwright' -// dotenv.config({ -// path: '.env.local' -// }) - -const email = process.env.UNICLOUD_EMAIL -const password = process.env.UNICLOUD_PASSWORD +// Usage: +// node renew.mjs # uses .env +// node renew.mjs # CLI args override .env +// UNICLOUD_EMAIL=... UNICLOUD_PASSWORD=... node renew.mjs # env override +const [, , cliEmail, cliPassword] = process.argv +const email = cliEmail || process.env.UNICLOUD_EMAIL +const password = cliPassword || process.env.UNICLOUD_PASSWORD if (!email || !password) { - throw new Error('Missing UNICLOUD_EMAIL or UNICLOUD_PASSWORD') + throw new Error( + 'Missing credentials. Pass them as `node renew.mjs ` ' + + 'or set UNICLOUD_EMAIL / UNICLOUD_PASSWORD env vars (or .env).' + ) } async function waitForLoginFrame (page, timeoutMs = 30000) { @@ -33,12 +37,69 @@ async function waitForUrl (page, predicate, timeoutMs = 30000) { throw new Error(`Timed out waiting for URL condition. Current: ${page.url()}`) } +// Run the purchase flow for a single "续费" button that has already been +// located on the dashboard. Resolves once the renewal is submitted and the +// browser has returned to the dashboard (so the caller can re-query rows). +async function renewOne (context, dashboardPage, renewLocator, index) { + // Clicking 续费 opens a NEW TAB that first goes through SSO, then lands on + // the uni-trade create-order page. + const newPagePromise = new Promise(resolve => context.once('page', resolve)) + await renewLocator.click() + const orderPage = await newPagePromise + + // Wait for the OAuth redirect to settle on create-order. + await waitForUrl( + orderPage, + u => u.includes('uni-trade.dcloud.net.cn') && u.includes('create-order'), + 45000 + ) + await orderPage.waitForTimeout(3000) + console.log(`[${index}] Order page ready:`, orderPage.url()) + + // Click "立即购买" — same tab navigates to /order-payment. + const buyBtn = orderPage.getByText('立即购买', { exact: true }).first() + await buyBtn.waitFor({ timeout: 30000 }) + await buyBtn.click() + + await waitForUrl(orderPage, u => u.includes('order-payment'), 30000) + await orderPage.waitForTimeout(3000) + console.log(`[${index}] Payment page ready:`, orderPage.url()) + + // Click "确认开通" to finalize. + const confirmBtn = orderPage.getByText('确认开通', { exact: true }).first() + await confirmBtn.waitFor({ timeout: 30000 }) + await confirmBtn.click() + console.log(`[${index}] Clicked 确认开通. Renewal submitted.`) + + // After confirmation the tab redirects back to unicloud.dcloud.net.cn and + // is no longer useful — close it and wait for the dashboard to settle. + await orderPage.waitForTimeout(5000) + await orderPage.close().catch(() => {}) + await dashboardPage.waitForTimeout(3000) +} + ;(async () => { - const browser = await chromium.launch({ - channel: 'msedge', - headless: false, - slowMo: 200 - }) + // Set HEADLESS=false (or omit) to watch the browser; cron jobs should run + // headless (HEADLESS=true or just leave it — defaults to true when no TTY). + const headless = + process.env.HEADLESS != null + ? process.env.HEADLESS !== 'false' && process.env.HEADLESS !== '0' + : !process.stdout.isTTY + + // On macOS use the installed Microsoft Edge; on Linux (and elsewhere) fall + // back to the Chromium bundled by Playwright (run `npx playwright install + // // chromium` once there). Override by setting BROWSER_CHANNEL, e.g. + // `BROWSER_CHANNEL=chrome` or `BROWSER_CHANNEL=`. + const isMac = process.platform === 'darwin' + const launchOptions = { headless, slowMo: headless ? 0 : 200 } + if (process.env.BROWSER_CHANNEL != null) { + if (process.env.BROWSER_CHANNEL) + launchOptions.channel = process.env.BROWSER_CHANNEL + } else if (isMac) { + launchOptions.channel = 'msedge' + } + + const browser = await chromium.launch(launchOptions) const context = await browser.newContext() const page = await context.newPage() @@ -49,6 +110,7 @@ async function waitForUrl (page, predicate, timeoutMs = 30000) { timeout: 60000 }) + console.log('<<<<<<<<<', new Date().toJSON(), 'Renew account', email) console.log('Page loaded, waiting for login iframe...') // 1. Login via the account.dcloud.net.cn iframe. @@ -74,42 +136,37 @@ async function waitForUrl (page, predicate, timeoutMs = 30000) { await page.waitForTimeout(5000) console.log('Login successful. URL:', page.url()) - // 2. Click "续费" — opens a NEW TAB that first goes through SSO - // (uni-trade.dcloud.net.cn/pages/login/login?oauthToken=...) then lands - // on the create-order page. - const renewBtn = page.getByText('续费', { exact: true }).first() - await renewBtn.waitFor({ timeout: 30000 }) - const newPagePromise = new Promise(resolve => context.once('page', resolve)) - await renewBtn.click() - const orderPage = await newPagePromise + // 2. Renew every subscription that shows a "续费" button on the dashboard. + // After renewOne() completes, the tab is closed and the dashboard + // re-renders (the renewed row's 续费 button disappears), so we re-query + // after each iteration. We keep going until no 续费 button remains, with + // a safety cap to avoid an infinite loop if something goes wrong. + const MAX_RENEWALS = 50 + let done = 0 + while (done < MAX_RENEWALS) { + await page.waitForTimeout(1500) + const renewBtns = page.getByText('续费', { exact: true }) + const count = await renewBtns.count() + if (count === 0) { + console.log( + `No more "续费" buttons found. Renewed ${done} subscription(s).` + ) + break + } + console.log( + `Found ${count} subscription(s) still to renew. Renewing next...` + ) + // Always pick the first remaining row. After renewal the button disappears. + await renewOne(context, page, renewBtns.first(), done + 1) + done++ + } - // Wait for the OAuth redirect to settle on create-order. - await waitForUrl( - orderPage, - u => u.includes('uni-trade.dcloud.net.cn') && u.includes('create-order'), - 45000 + console.log( + '>>>>>>>>>', + new Date().toJSON(), + `Done. ${done} subscription(s) renewed for accouint`, + email ) - await orderPage.waitForTimeout(3000) - console.log('Order page ready:', orderPage.url()) - - // 3. Click "立即购买" — same tab navigates to /order-payment. - const buyBtn = orderPage.getByText('立即购买', { exact: true }).first() - await buyBtn.waitFor({ timeout: 30000 }) - await buyBtn.click() - - await waitForUrl(orderPage, u => u.includes('order-payment'), 30000) - await orderPage.waitForTimeout(3000) - console.log('Payment page ready:', orderPage.url()) - - // 4. Click "确认开通" to finalize. - const confirmBtn = orderPage.getByText('确认开通', { exact: true }).first() - await confirmBtn.waitFor({ timeout: 30000 }) - await confirmBtn.click() - console.log('Clicked 确认开通. Renewal submitted.') - - // Wait briefly for the confirmation redirect to complete, then exit. - await orderPage.waitForTimeout(5000) - await browser.close() process.exit(0) })()