improved
This commit is contained in:
@@ -1,15 +1,19 @@
|
|||||||
import 'dotenv/config'
|
import 'dotenv/config'
|
||||||
import { chromium } from 'playwright'
|
import { chromium } from 'playwright'
|
||||||
|
|
||||||
// dotenv.config({
|
// Usage:
|
||||||
// path: '.env.local'
|
// node renew.mjs # uses .env
|
||||||
// })
|
// node renew.mjs <email> <password> # CLI args override .env
|
||||||
|
// UNICLOUD_EMAIL=... UNICLOUD_PASSWORD=... node renew.mjs # env override
|
||||||
const email = process.env.UNICLOUD_EMAIL
|
const [, , cliEmail, cliPassword] = process.argv
|
||||||
const password = process.env.UNICLOUD_PASSWORD
|
const email = cliEmail || process.env.UNICLOUD_EMAIL
|
||||||
|
const password = cliPassword || process.env.UNICLOUD_PASSWORD
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
throw new Error('Missing UNICLOUD_EMAIL or UNICLOUD_PASSWORD')
|
throw new Error(
|
||||||
|
'Missing credentials. Pass them as `node renew.mjs <email> <password>` ' +
|
||||||
|
'or set UNICLOUD_EMAIL / UNICLOUD_PASSWORD env vars (or .env).'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForLoginFrame (page, timeoutMs = 30000) {
|
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()}`)
|
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 () => {
|
;(async () => {
|
||||||
const browser = await chromium.launch({
|
// Set HEADLESS=false (or omit) to watch the browser; cron jobs should run
|
||||||
channel: 'msedge',
|
// headless (HEADLESS=true or just leave it — defaults to true when no TTY).
|
||||||
headless: false,
|
const headless =
|
||||||
slowMo: 200
|
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 context = await browser.newContext()
|
||||||
const page = await context.newPage()
|
const page = await context.newPage()
|
||||||
@@ -49,6 +110,7 @@ async function waitForUrl (page, predicate, timeoutMs = 30000) {
|
|||||||
timeout: 60000
|
timeout: 60000
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('<<<<<<<<<', new Date().toJSON(), 'Renew account', email)
|
||||||
console.log('Page loaded, waiting for login iframe...')
|
console.log('Page loaded, waiting for login iframe...')
|
||||||
|
|
||||||
// 1. Login via the account.dcloud.net.cn 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)
|
await page.waitForTimeout(5000)
|
||||||
console.log('Login successful. URL:', page.url())
|
console.log('Login successful. URL:', page.url())
|
||||||
|
|
||||||
// 2. Click "续费" — opens a NEW TAB that first goes through SSO
|
// 2. Renew every subscription that shows a "续费" button on the dashboard.
|
||||||
// (uni-trade.dcloud.net.cn/pages/login/login?oauthToken=...) then lands
|
// After renewOne() completes, the tab is closed and the dashboard
|
||||||
// on the create-order page.
|
// re-renders (the renewed row's 续费 button disappears), so we re-query
|
||||||
const renewBtn = page.getByText('续费', { exact: true }).first()
|
// after each iteration. We keep going until no 续费 button remains, with
|
||||||
await renewBtn.waitFor({ timeout: 30000 })
|
// a safety cap to avoid an infinite loop if something goes wrong.
|
||||||
const newPagePromise = new Promise(resolve => context.once('page', resolve))
|
const MAX_RENEWALS = 50
|
||||||
await renewBtn.click()
|
let done = 0
|
||||||
const orderPage = await newPagePromise
|
while (done < MAX_RENEWALS) {
|
||||||
|
await page.waitForTimeout(1500)
|
||||||
// Wait for the OAuth redirect to settle on create-order.
|
const renewBtns = page.getByText('续费', { exact: true })
|
||||||
await waitForUrl(
|
const count = await renewBtns.count()
|
||||||
orderPage,
|
if (count === 0) {
|
||||||
u => u.includes('uni-trade.dcloud.net.cn') && u.includes('create-order'),
|
console.log(
|
||||||
45000
|
`No more "续费" buttons found. Renewed ${done} subscription(s).`
|
||||||
)
|
)
|
||||||
await orderPage.waitForTimeout(3000)
|
break
|
||||||
console.log('Order page ready:', orderPage.url())
|
}
|
||||||
|
console.log(
|
||||||
// 3. Click "立即购买" — same tab navigates to /order-payment.
|
`Found ${count} subscription(s) still to renew. Renewing next...`
|
||||||
const buyBtn = orderPage.getByText('立即购买', { exact: true }).first()
|
)
|
||||||
await buyBtn.waitFor({ timeout: 30000 })
|
// Always pick the first remaining row. After renewal the button disappears.
|
||||||
await buyBtn.click()
|
await renewOne(context, page, renewBtns.first(), done + 1)
|
||||||
|
done++
|
||||||
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)
|
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'>>>>>>>>>',
|
||||||
|
new Date().toJSON(),
|
||||||
|
`Done. ${done} subscription(s) renewed for accouint`,
|
||||||
|
email
|
||||||
|
)
|
||||||
await browser.close()
|
await browser.close()
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})()
|
})()
|
||||||
|
|||||||
Reference in New Issue
Block a user