import { chromium } from 'playwright'; import log from 'electron-log'; import tabsPkg from './common/tabs.js'; const { preparePage, safeDisconnectBrowser } = tabsPkg; const parseDateInput = (dateStr) => { if (!dateStr) return null; if (typeof dateStr === 'number' && Number.isFinite(dateStr)) return new Date(dateStr); if (dateStr instanceof Date && !Number.isNaN(dateStr.getTime())) return dateStr; const raw = String(dateStr).trim(); const ymdMatch = raw.match(/(\d{4}-\d{2}-\d{2})/); if (ymdMatch) return new Date(`${ymdMatch[1]}T00:00:00`); const mmddMatch = raw.match(/(\d{2}-\d{2})/); if (mmddMatch) { const year = new Date().getFullYear(); return new Date(`${year}-${mmddMatch[1]}T00:00:00`); } return null; }; const formatDate = (d) => { const yyyy = d.getFullYear(); const mm = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); return `${yyyy}-${mm}-${dd}`; }; const buildDateList = (startDate, endDate) => { const start = parseDateInput(startDate); const end = parseDateInput(endDate || startDate); if (!start || !end) return []; const from = start <= end ? start : end; const to = start <= end ? end : start; const dates = []; const cur = new Date(from.getTime()); while (cur <= to) { dates.push(formatDate(cur)); cur.setDate(cur.getDate() + 1); } return dates; }; const toggleRoomByDate = async (page, { roomType, dateStr, operation }) => { // Find row by room type const row = page.locator('.lifep-table-row').filter({ hasText: roomType }).first(); if (await row.count() === 0) { throw new Error(`Room type not found: ${roomType}`); } // Find cell by date class (e.g., .date-2026-03-14) const cell = row.locator(`.lifep-table-cell.date-${dateStr}`); if (await cell.count() === 0) { log.warn(`Date cell not found: ${dateStr}`); return; } // Find switch components // Structure: .byted-switch (label) > input[type=checkbox] + .byted-switch-wrapper (visual) // The user provided structure shows: .lifep-table-cell... > .room-swtich-container-PxrDYy > .byted-switch > .byted-switch-wrapper // We can just find .byted-switch inside the cell, it should be unique enough. const switchLabel = cell.locator('.byted-switch').first(); const switchWrapper = cell.locator('.byted-switch-wrapper').first(); const switchInput = cell.locator('input[type="checkbox"]').first(); if (await switchLabel.count() === 0) { // If not found, log details and return log.warn(`Switch not found for ${roomType} on ${dateStr}`); return; } await switchWrapper.scrollIntoViewIfNeeded(); // Check state // Preferred: check input state or wrapper class let isOpen = false; if (await switchInput.count() > 0) { // In dy.md, when checked, the input has a `checked=""` attribute. // However, playwright's isChecked() might not always sync perfectly if the framework uses synthetic events. // So we check both isChecked and the classes. isOpen = await switchInput.isChecked(); } if (!isOpen) { const labelClass = await switchLabel.getAttribute('class') || ''; const wrapperClass = await switchWrapper.getAttribute('class') || ''; // Based on dy.md: `byted-switch-checked` is added to the label // and `byted-switch-wrapper-checked` is added to the wrapper isOpen = labelClass.includes('byted-switch-checked') || wrapperClass.includes('byted-switch-wrapper-checked'); } if (operation === 'open' && !isOpen) { // Click the wrapper as seen in recording await switchWrapper.click(); log.info(`Opened room ${roomType} on ${dateStr}`); await handleConfirmation(page); } else if (operation === 'close' && isOpen) { await switchWrapper.click(); log.info(`Closed room ${roomType} on ${dateStr}`); await handleConfirmation(page); } else { log.info(`Room ${roomType} already ${operation === 'open' ? 'open' : 'closed'} on ${dateStr}`); } }; const handleConfirmation = async (page) => { try { const confirmBtn = page.getByRole('button', { name: '确定' }); // Wait slightly longer for dialog animation if (await confirmBtn.isVisible({ timeout: 3000 })) { await confirmBtn.click(); // Wait for dialog to disappear await confirmBtn.waitFor({ state: 'hidden', timeout: 3000 }).catch(() => {}); } } catch (e) { // Ignore if no confirmation dialog appears } }; const navigateToRoomStatusManagement = async (page) => { try { const currentUrl = page.url(); if (currentUrl.includes('/p/travel-ari/hotel/price_amount_state')) return; } catch (e) {} try { const tableRow = page.locator('.lifep-table-row').first(); if (await tableRow.isVisible({ timeout: 1500 })) return; } catch (e) {} const managementEntry = page.getByText('房价房态管理', { exact: true }); try { if (await managementEntry.isVisible({ timeout: 1500 })) return; } catch (e) {} const candidates = [ page.locator('div').filter({ hasText: /^房价房态$/ }).nth(1), page.locator('div').filter({ hasText: /^房价房态$/ }).first(), page.getByText('房价房态', { exact: true }), page.getByRole('menuitem', { name: '房价房态' }).first(), ]; for (const candidate of candidates) { try { await candidate.waitFor({ state: 'visible', timeout: 6000 }); await candidate.click({ timeout: 6000, force: true }); if (await managementEntry.isVisible({ timeout: 3000 })) break; } catch (e) {} } await managementEntry.waitFor({ state: 'visible', timeout: 10000 }); await managementEntry.click({ timeout: 10000, force: true }); }; (async () => { let browser; try { const groupId = '1816249020842116'; const homeUrl = `https://life.douyin.com/p/home?groupid=${groupId}`; const priceAmountStateUrl = `https://life.douyin.com/p/travel-ari/hotel/price_amount_state?groupid=${groupId}`; const prepared = await preparePage(chromium, { targetUrl: priceAmountStateUrl }); browser = prepared.browser; const page = prepared.page; try { if (!page.url().includes('/p/travel-ari/hotel/price_amount_state')) { await page.goto(priceAmountStateUrl, { waitUntil: 'domcontentloaded' }); } } catch (e) { try { await page.goto(homeUrl, { waitUntil: 'domcontentloaded' }); } catch (e2) {} } // Navigation logic (User provided) try { // Try to click the store/login button if visible const storeBtn = page.getByText('息烽南山天沐温泉我创建的'); if (await storeBtn.isVisible({ timeout: 3000 })) { await storeBtn.click(); await page.getByRole('button', { name: '确认登录' }).click(); await page.waitForTimeout(2000); } // Navigate to Price/Room Status await navigateToRoomStatusManagement(page); await page.waitForTimeout(2000); } catch (e) { log.warn('Navigation steps skipped or failed, assuming already on page or different flow:', e.message); } const roomType = process.env.ROOM_TYPE; const startDate = process.env.START_DATE; const endDate = process.env.END_DATE; const operation = process.env.OPERATION === 'close' ? 'close' : 'open'; if (!roomType || !startDate) { log.info('ROOM_TYPE/START_DATE not provided, skip room toggle.'); return; } // Wait for table rows to appear await page.locator('.lifep-table-row').first().waitFor({ state: 'visible', timeout: 15000 }); const dateList = buildDateList(startDate, endDate); for (const dateStr of dateList) { try { await toggleRoomByDate(page, { roomType, dateStr, operation }); } catch (e) { log.error(`Failed to toggle ${roomType} on ${dateStr}: ${e.message}`); } await page.waitForTimeout(500 + Math.random() * 500); } } catch (error) { log.error(error); process.exitCode = 1; } finally { await safeDisconnectBrowser(browser); process.exit(process.exitCode || 0); } })();