feat: 完成美团|抖音自动化脚本

This commit is contained in:
duanshuwen
2026-03-14 22:47:15 +08:00
parent a6f010a1f4
commit 2fc8ad3140
9 changed files with 9089 additions and 10 deletions

8099
src/main/scripts/dy.md Normal file

File diff suppressed because one or more lines are too long

BIN
src/main/scripts/dy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

View File

@@ -1,4 +1,224 @@
import { chromium } from 'playwright';
import log from 'electron-log';
import tabsPkg from './common/tabs.js';
log.info('dy_trace.mjs placeholder: not implemented');
process.exit(0);
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);
}
})();

View File

@@ -4,15 +4,217 @@ 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 targetUrl = process.env.TARGET_URL || 'https://life.douyin.com/';
const prepared = await preparePage(chromium, { targetUrl });
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);
}
log.info('dy_hotel_trace.mjs placeholder: not implemented');
} catch (error) {
log.error(error);
process.exitCode = 1;

View File

@@ -42,6 +42,40 @@ const buildDateList = (startDate, endDate) => {
return dates;
};
const clickWithRetry = async (locator, { attempts = 6 } = {}) => {
let lastError;
for (let i = 0; i < attempts; i++) {
try {
await locator.scrollIntoViewIfNeeded().catch(() => {});
await locator.waitFor({ state: 'visible', timeout: 8000 });
await locator.click({ timeout: 8000, force: true });
return;
} catch (e) {
lastError = e;
try {
await locator.page().keyboard.press('Escape');
} catch (e2) {}
await locator.page().waitForTimeout(400 + Math.random() * 600);
}
}
throw lastError;
};
const ensureRoomPriceCalendar = async (page) => {
const container = page.locator('#price-reserve-table-container');
try {
if (await container.isVisible({ timeout: 1500 })) return;
} catch (e) {}
const menuTitle = page.getByRole('menuitem', { name: /房价房量管理/ }).first();
await clickWithRetry(menuTitle);
const calendarEntry = page.getByRole('menuitem', { name: /房价房量日历/ }).first();
await clickWithRetry(calendarEntry);
await container.waitFor({ state: 'visible', timeout: 30000 });
};
const findHeaderDateIndex = async (container, mmdd) => {
for (let attempt = 0; attempt < 10; attempt++) {
const headerCells = container.locator(
@@ -62,7 +96,12 @@ const findHeaderDateIndex = async (container, mmdd) => {
};
const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation }) => {
const roomAnchor = container.getByText(roomType, { exact: true }).first();
const strictAnchor = container.getByText(roomType, { exact: true }).first();
const looseAnchor = container.getByText(roomType).first();
const roomAnchor = (await strictAnchor.count()) > 0 ? strictAnchor : looseAnchor;
if ((await roomAnchor.count()) === 0) {
throw new Error(`Room type not found in table: ${roomType}`);
}
await roomAnchor.scrollIntoViewIfNeeded();
const row = roomAnchor.locator(
@@ -101,9 +140,7 @@ const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation
await page.waitForTimeout(4000 + Math.random() * 300);
await page.getByRole('menuitem', { name: '房价房量管理' }).click();
await page.waitForTimeout(4000 + Math.random() * 1000);
await page.getByText('房价房量日历').click();
await ensureRoomPriceCalendar(page);
const roomType = process.env.ROOM_TYPE;
const startDate = process.env.START_DATE;

305
src/main/scripts/fz.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -1,8 +1,174 @@
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 formatMMDD = (d) => {
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${mm}.${dd}`; // Meituan uses MM.DD format
};
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(formatMMDD(cur));
cur.setDate(cur.getDate() + 1);
}
return dates;
};
// Find the column class (e.g. "col_55") for a specific date
const findDateColumnClass = async (page, mmdd) => {
// Try visible headers first
const headerCells = page.locator('.vxe-header--column');
const checkHeaders = async () => {
const c = await headerCells.count();
for (let i = 0; i < c; i++) {
const cell = headerCells.nth(i);
const text = (await cell.innerText()).replace(/\s+/g, ' ').trim();
if (text.includes(mmdd)) {
// Extract the col_X class
const classList = await cell.getAttribute('class');
const match = classList.match(/col_\d+/);
if (match) return match[0];
}
}
return null;
};
let colClass = await checkHeaders();
if (colClass) return colClass;
// Try scrolling if not found
const scrollContainer = page.locator('.vxe-table--body-wrapper').first();
if (await scrollContainer.isVisible()) {
for (let attempt = 0; attempt < 10; attempt++) {
await scrollContainer.evaluate((el) => {
el.scrollLeft += 600;
});
await page.waitForTimeout(300);
colClass = await checkHeaders();
if (colClass) return colClass;
}
}
return null;
};
// Find the row index for a specific room type
const findRoomRowIndex = async (page, roomType) => {
// We look for the row in ANY part of the table (fixed left or main)
// and assume row indexes align across split tables.
const rows = page.locator('.vxe-body--row');
const count = await rows.count();
for (let i = 0; i < count; i++) {
const text = await rows.nth(i).innerText();
if (text.includes(roomType)) {
// Found the row. Now we need to determine its "logical" index.
// If the table is split into left/main, 'rows' contains ALL rows from both tables mixed or sequential.
// A safer way is to find the specific TR and get its rowIndex or check its parent.
// Optimization: Usually room names are in the fixed-left wrapper.
// Let's try to find the row specifically in the left wrapper first if it exists.
const leftRows = page.locator('.vxe-table--fixed-left-wrapper .vxe-body--row');
if (await leftRows.count() > 0) {
const leftCount = await leftRows.count();
for(let j=0; j<leftCount; j++) {
if ((await leftRows.nth(j).innerText()).includes(roomType)) {
return j;
}
}
}
// Fallback: Just return this index if it's a simple table
return i;
}
}
return -1;
};
const toggleRoom = async (page, { roomType, mmdd, operation }) => {
// 1. Find the column class for the date (e.g. "col_6")
const colClass = await findDateColumnClass(page, mmdd);
if (!colClass) {
throw new Error(`Date column not found for ${mmdd}`);
}
// log.info(`Date ${mmdd} maps to column class: ${colClass}`);
// 2. Find the row index for the room
const rowIndex = await findRoomRowIndex(page, roomType);
if (rowIndex === -1) {
throw new Error(`Room row not found for ${roomType}`);
}
// log.info(`Room ${roomType} maps to row index: ${rowIndex}`);
// 3. Locate the target cell in the MAIN body wrapper (where switches usually are)
// using the row index and column class.
const mainBody = page.locator('.vxe-table--body-wrapper');
const targetRow = mainBody.locator('.vxe-body--row').nth(rowIndex);
const targetCell = targetRow.locator(`.${colClass}`);
// 4. Find and toggle switch
const switchEl = targetCell.locator('.lz-switch').first();
if (await switchEl.count() === 0) {
log.warn(`No switch found at ${roomType} / ${mmdd} (Row ${rowIndex}, Col ${colClass})`);
return;
}
await switchEl.scrollIntoViewIfNeeded();
// Check state
// Inspecting common switch classes: 'lz-switch-active' (Meituan), 'is-checked', etc.
const classList = await switchEl.getAttribute('class');
const isChecked = classList && (
classList.includes('is-checked') ||
classList.includes('checked') ||
classList.includes('lz-switch-checked') ||
classList.includes('lz-switch-active')
);
const isOpen = isChecked;
if (operation === 'open' && !isOpen) {
await switchEl.click();
log.info(`Opened room ${roomType} on ${mmdd}`);
} else if (operation === 'close' && isOpen) {
await switchEl.click();
log.info(`Closed room ${roomType} on ${mmdd}`);
} else {
log.info(`Room ${roomType} already ${operation === 'open' ? 'open' : 'closed'} on ${mmdd}`);
}
};
(async () => {
let browser;
@@ -11,6 +177,56 @@ const { preparePage, safeDisconnectBrowser } = tabsPkg;
const prepared = await preparePage(chromium, { targetUrl });
browser = prepared.browser;
const page = prepared.page;
await page.waitForTimeout(4000 + Math.random() * 300);
// Navigation logic from user snippet
try {
const menu = page.locator('#vina-menu-item-100114001 > .lz-submenu-title');
if (await menu.isVisible()) {
await menu.click();
await page.waitForTimeout(1000);
} else {
// Fallback if the submenu title isn't there
const menuFallback = page.locator('#vina-menu-item-100114001');
if (await menuFallback.isVisible()) {
await menuFallback.click();
await page.waitForTimeout(1000);
}
}
const calendarLink = page.locator('a').filter({ hasText: '房价房量日历' });
if (await calendarLink.isVisible()) {
await calendarLink.click();
await page.waitForTimeout(3000);
}
} catch (e) {
log.warn('Navigation attempt failed, assuming already on page:', 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
await page.locator('.vxe-table--body-wrapper').waitFor({ state: 'visible', timeout: 15000 });
const dateList = buildDateList(startDate, endDate);
for (const mmdd of dateList) {
try {
await toggleRoom(page, { roomType, mmdd, operation });
} catch (e) {
log.error(`Failed to toggle ${roomType} on ${mmdd}: ${e.message}`);
}
await page.waitForTimeout(600 + Math.random() * 600);
}
} catch (error) {
log.error(error);
process.exitCode = 1;