chore: restructure project and add i18n support
- Reorganize project structure with new electron and shared directories - Add comprehensive i18n support with Chinese, English, and Japanese locales - Update build configurations and TypeScript paths for new structure - Add various UI components including chat interface and task management - Include Windows release binaries and localization files - Update dependencies and fix import paths throughout the codebase
This commit is contained in:
138
electron/scripts/common/tabs.js
Normal file
138
electron/scripts/common/tabs.js
Normal file
@@ -0,0 +1,138 @@
|
||||
const normalizeUrl = (value) => {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return new URL(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isSameTarget = (currentUrl, targetUrl) => {
|
||||
if (!currentUrl || !targetUrl) return false;
|
||||
if (currentUrl.startsWith(targetUrl)) return true;
|
||||
|
||||
const current = normalizeUrl(currentUrl);
|
||||
const target = normalizeUrl(targetUrl);
|
||||
if (!current || !target) return false;
|
||||
if (current.origin !== target.origin) return false;
|
||||
if (!current.pathname.startsWith(target.pathname)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const isSameOrigin = (currentUrl, targetUrl) => {
|
||||
const current = normalizeUrl(currentUrl);
|
||||
const target = normalizeUrl(targetUrl);
|
||||
if (!current || !target) return false;
|
||||
|
||||
return current.origin === target.origin;
|
||||
};
|
||||
|
||||
const isBlankLikePage = (url) => {
|
||||
const u = String(url || '').trim().toLowerCase();
|
||||
if (!u) return true;
|
||||
if (u === 'about:blank' || u.startsWith('about:blank#') || u.startsWith('about:blank?')) return true;
|
||||
if (u === 'chrome://newtab/' || u.startsWith('chrome://newtab')) return true;
|
||||
if (u.startsWith('chrome://new-tab-page')) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const preparePage = async (
|
||||
chromium,
|
||||
{
|
||||
tabIndex = Number(process.env.TAB_INDEX),
|
||||
endpoint = process.env.CDP_ENDPOINT || 'http://127.0.0.1:9222',
|
||||
targetUrl,
|
||||
} = {},
|
||||
) => {
|
||||
const browser = await chromium.connectOverCDP(endpoint);
|
||||
const context = browser.contexts()[0];
|
||||
|
||||
if (!context) {
|
||||
throw new Error('No browser context available');
|
||||
}
|
||||
|
||||
await context.addInitScript(() => {
|
||||
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
||||
});
|
||||
|
||||
let pages = await context.pages();
|
||||
let page = null;
|
||||
|
||||
if (targetUrl) {
|
||||
// 1. Try exact/path match
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const p = pages[i];
|
||||
if (isSameTarget(p.url(), targetUrl)) {
|
||||
page = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try origin match
|
||||
if (!page) {
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const p = pages[i];
|
||||
if (isSameOrigin(p.url(), targetUrl)) {
|
||||
page = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
const isTabIndexSet = Number.isFinite(tabIndex) && tabIndex >= 0;
|
||||
|
||||
// If targetUrl provided and NO specific tabIndex requested, avoid hijacking tab 0
|
||||
if (targetUrl && !isTabIndexSet) {
|
||||
// Try blank page
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
if (isBlankLikePage(pages[i].url())) {
|
||||
page = pages[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Else new page
|
||||
if (!page) {
|
||||
page = await context.newPage();
|
||||
}
|
||||
} else {
|
||||
// Legacy/Default behavior: use tabIndex (default 0)
|
||||
const idx = isTabIndexSet ? Math.floor(tabIndex) : 0;
|
||||
while (pages.length <= idx) {
|
||||
await context.newPage();
|
||||
pages = await context.pages();
|
||||
}
|
||||
page = pages[idx];
|
||||
}
|
||||
}
|
||||
|
||||
await page.bringToFront();
|
||||
|
||||
if (targetUrl) {
|
||||
const currentUrl = page.url();
|
||||
if (!currentUrl || !isSameTarget(currentUrl, targetUrl)) {
|
||||
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
|
||||
}
|
||||
}
|
||||
|
||||
return { browser, context, page };
|
||||
};
|
||||
|
||||
const safeDisconnectBrowser = async (browser) => {
|
||||
if (!browser) return;
|
||||
|
||||
try {
|
||||
if (typeof browser.disconnect === 'function') {
|
||||
await browser.disconnect();
|
||||
} else {
|
||||
await browser.close();
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
preparePage,
|
||||
safeDisconnectBrowser,
|
||||
};
|
||||
8099
electron/scripts/dy.md
Normal file
8099
electron/scripts/dy.md
Normal file
File diff suppressed because one or more lines are too long
225
electron/scripts/dy_hot_spring_trace.js
Normal file
225
electron/scripts/dy_hot_spring_trace.js
Normal file
@@ -0,0 +1,225 @@
|
||||
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);
|
||||
}
|
||||
})();
|
||||
225
electron/scripts/dy_hotel_trace.js
Normal file
225
electron/scripts/dy_hotel_trace.js
Normal file
@@ -0,0 +1,225 @@
|
||||
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);
|
||||
}
|
||||
})();
|
||||
3
electron/scripts/fg.md
Normal file
3
electron/scripts/fg.md
Normal file
File diff suppressed because one or more lines are too long
174
electron/scripts/fg_trace.js
Normal file
174
electron/scripts/fg_trace.js
Normal file
@@ -0,0 +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}`;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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(
|
||||
"xpath=.//div[contains(@class,'headerRow')]/div[contains(@class,'headerContent')]",
|
||||
);
|
||||
const count = await headerCells.count();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const text = (await headerCells.nth(i).innerText()).replace(/\s+/g, ' ').trim();
|
||||
if (text.includes(mmdd)) return i;
|
||||
}
|
||||
|
||||
await container.evaluate((el) => {
|
||||
el.scrollLeft += 600;
|
||||
});
|
||||
await container.page().waitForTimeout(120);
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation }) => {
|
||||
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(
|
||||
"xpath=ancestor::*[.//div[contains(@class,'boardRow')]][1]",
|
||||
);
|
||||
const boardRow = row.locator("xpath=.//div[contains(@class,'boardRow')]").first();
|
||||
|
||||
const boardColPosition = dateIndex + 2;
|
||||
const dayColumn = boardRow.locator(`xpath=./div[${boardColPosition}]`);
|
||||
|
||||
const targetStateClass = operation === 'open' ? 'error' : 'success';
|
||||
const expectedCurrentLabel = operation === 'open' ? '满' : '有';
|
||||
const barByLabel = dayColumn
|
||||
.locator(
|
||||
`xpath=.//*[ (contains(@class,'success') or contains(@class,'error')) and .//*[contains(normalize-space(),'${expectedCurrentLabel}')] ]//*[contains(@class,'bar')]`,
|
||||
)
|
||||
.first();
|
||||
const barByClass = dayColumn
|
||||
.locator(`xpath=.//*[contains(@class,'${targetStateClass}')]//*[contains(@class,'bar')]`)
|
||||
.first();
|
||||
const bar = (await barByLabel.count()) > 0 ? barByLabel : barByClass;
|
||||
|
||||
await bar.waitFor({ state: 'visible' });
|
||||
await bar.scrollIntoViewIfNeeded();
|
||||
await bar.click();
|
||||
};
|
||||
|
||||
(async () => {
|
||||
let browser;
|
||||
|
||||
try {
|
||||
const targetUrl = 'https://hotel.fliggy.com/ebooking/hotelBaseInfoUv.htm#/ebk/homeV1';
|
||||
const prepared = await preparePage(chromium, { targetUrl });
|
||||
browser = prepared.browser;
|
||||
const page = prepared.page;
|
||||
|
||||
await page.waitForTimeout(4000 + Math.random() * 300);
|
||||
|
||||
await ensureRoomPriceCalendar(page);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const container = page.locator('#price-reserve-table-container');
|
||||
await container.waitFor({ state: 'visible' });
|
||||
|
||||
const dateList = buildDateList(startDate, endDate);
|
||||
for (const mmdd of dateList) {
|
||||
const dateIndex = await findHeaderDateIndex(container, mmdd);
|
||||
if (dateIndex < 0) {
|
||||
throw new Error(`Date not found in header: ${mmdd}`);
|
||||
}
|
||||
await toggleRoomByDateIndex(container, { roomType, dateIndex, operation });
|
||||
await page.waitForTimeout(600 + Math.random() * 600);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await safeDisconnectBrowser(browser);
|
||||
process.exit(process.exitCode || 0);
|
||||
}
|
||||
})();
|
||||
3
electron/scripts/mt.md
Normal file
3
electron/scripts/mt.md
Normal file
File diff suppressed because one or more lines are too long
237
electron/scripts/mt_trace.js
Normal file
237
electron/scripts/mt_trace.js
Normal file
@@ -0,0 +1,237 @@
|
||||
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;
|
||||
|
||||
try {
|
||||
const targetUrl = 'https://me.meituan.com/ebooking/merchant/product#/index';
|
||||
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;
|
||||
} finally {
|
||||
await safeDisconnectBrowser(browser);
|
||||
process.exit(process.exitCode || 0);
|
||||
}
|
||||
})();
|
||||
133
electron/scripts/open_all_channel.js
Normal file
133
electron/scripts/open_all_channel.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import { chromium } from 'playwright';
|
||||
import log from 'electron-log';
|
||||
|
||||
const parseChannels = () => {
|
||||
const raw = process.env.CHANNELS || '';
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeUrl = (value) => {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return new URL(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isSameTarget = (currentUrl, targetUrl) => {
|
||||
if (!currentUrl || !targetUrl) return false;
|
||||
if (currentUrl.startsWith(targetUrl)) return true;
|
||||
|
||||
const current = normalizeUrl(currentUrl);
|
||||
const target = normalizeUrl(targetUrl);
|
||||
if (!current || !target) return false;
|
||||
if (current.origin !== target.origin) return false;
|
||||
if (!current.pathname.startsWith(target.pathname)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const isBlankLikePage = (url) => {
|
||||
const u = String(url || '').trim().toLowerCase();
|
||||
if (!u) return true;
|
||||
if (u === 'about:blank' || u.startsWith('about:blank#') || u.startsWith('about:blank?')) return true;
|
||||
if (u === 'chrome://newtab/' || u.startsWith('chrome://newtab')) return true;
|
||||
if (u.startsWith('chrome://new-tab-page')) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
(async () => {
|
||||
let browser;
|
||||
|
||||
try {
|
||||
const channels = parseChannels();
|
||||
|
||||
if (!channels.length) {
|
||||
log.info('No channels provided, skip.');
|
||||
return;
|
||||
}
|
||||
|
||||
browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
|
||||
const context = browser.contexts()[0];
|
||||
|
||||
if (!context) {
|
||||
throw new Error('No browser context available');
|
||||
}
|
||||
|
||||
await context.addInitScript(() => {
|
||||
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
||||
});
|
||||
|
||||
const usedPages = new Set();
|
||||
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
const targetUrl = channels[i]?.channelUrl;
|
||||
|
||||
if (!targetUrl) continue;
|
||||
|
||||
let pages = await context.pages();
|
||||
let page = null;
|
||||
for (let j = 0; j < pages.length; j++) {
|
||||
const p = pages[j];
|
||||
const current = p.url();
|
||||
if (isSameTarget(current, targetUrl)) {
|
||||
page = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
for (let j = 0; j < pages.length; j++) {
|
||||
const p = pages[j];
|
||||
if (usedPages.has(p)) continue;
|
||||
if (isBlankLikePage(p.url())) {
|
||||
page = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
page = await context.newPage();
|
||||
}
|
||||
await page.bringToFront();
|
||||
usedPages.add(page);
|
||||
|
||||
const current = page.url();
|
||||
|
||||
if (!current || !isSameTarget(current, targetUrl)) {
|
||||
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
|
||||
}
|
||||
}
|
||||
|
||||
const pagesAfter = await context.pages();
|
||||
for (const p of pagesAfter) {
|
||||
if (!usedPages.has(p)) {
|
||||
try {
|
||||
await p.close();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if(browser){
|
||||
try {
|
||||
if (typeof browser.disconnect === 'function') {
|
||||
await browser.disconnect();
|
||||
} else {
|
||||
await browser.close();
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
process.exit(process.exitCode || 0);
|
||||
}
|
||||
})();
|
||||
348
electron/scripts/roomType.md
Normal file
348
electron/scripts/roomType.md
Normal file
@@ -0,0 +1,348 @@
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": null,
|
||||
"data": [
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:19",
|
||||
"id": "0d6898fb7105419f895e2153e48df115",
|
||||
"pmsName": "亲子太空舱",
|
||||
"xcName": "亲子太空舱",
|
||||
"fzName": "亲子太空舱",
|
||||
"mtName": "亲子太空舱",
|
||||
"dyHotelName": "亲子太空舱-含私汤",
|
||||
"dyHotSrpingName": "亲子太空舱-含私汤",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:20",
|
||||
"id": "1c3616b31de946ee868f74b550e9a88f",
|
||||
"pmsName": "豪华特色",
|
||||
"xcName": "豪华特色房",
|
||||
"fzName": "豪华特色房(麻将)",
|
||||
"mtName": "豪华特色房",
|
||||
"dyHotelName": "豪华特色房",
|
||||
"dyHotSrpingName": "豪华特色房(含双人温泉,入住期间不限次)",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:21",
|
||||
"id": "2cfdf51d2cff4384ba53a0fa5b8a6c77",
|
||||
"pmsName": "行政套房A",
|
||||
"xcName": "行政套房A",
|
||||
"fzName": "行政套房A",
|
||||
"mtName": "行政套房A",
|
||||
"dyHotelName": "行政套房A",
|
||||
"dyHotSrpingName": "行政套房A(含双人温泉,入住期间不限次)",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:22",
|
||||
"id": "30ca794cc12743a19e6a2a8483838234",
|
||||
"pmsName": "亲子房",
|
||||
"xcName": "亲子房",
|
||||
"fzName": "亲子房",
|
||||
"mtName": "亲子房",
|
||||
"dyHotelName": "亲子房",
|
||||
"dyHotSrpingName": "亲子房(含三人温泉,入住期间不限次)",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:24",
|
||||
"id": "4b307fd07e34483083f47d5ed32d9a26",
|
||||
"pmsName": "太空舱",
|
||||
"xcName": "太空舱(含私汤)",
|
||||
"fzName": "太空舱",
|
||||
"mtName": "太空舱(私汤)",
|
||||
"dyHotelName": "太空舱",
|
||||
"dyHotSrpingName": "太空舱(含双人温泉、入住期间不限次)",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:23",
|
||||
"id": "4b77c93f42d34f7fa588a223dd1e1762",
|
||||
"pmsName": "豪华行政套房",
|
||||
"xcName": "",
|
||||
"fzName": "",
|
||||
"mtName": "",
|
||||
"dyHotelName": "",
|
||||
"dyHotSrpingName": "",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:26",
|
||||
"id": "53b3c2a68dd844cb8f8d4f339ce521ef",
|
||||
"pmsName": "至大",
|
||||
"xcName": "至尊大床房(含私汤)",
|
||||
"fzName": "和雅小筑-至尊大床房(户外泡池)",
|
||||
"mtName": "至尊大床房",
|
||||
"dyHotelName": "",
|
||||
"dyHotSrpingName": "至尊大床房-私汤",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:26",
|
||||
"id": "5cd720a6ed124c11a2ef61f1eef5a98d",
|
||||
"pmsName": "行政套房B",
|
||||
"xcName": "行政套房B(含私汤)",
|
||||
"fzName": "行政套房B",
|
||||
"mtName": "",
|
||||
"dyHotelName": "",
|
||||
"dyHotSrpingName": "",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:27",
|
||||
"id": "76d9ff061da54e1f9ff1afd663dd22a5",
|
||||
"pmsName": "汤墅会馆",
|
||||
"xcName": "汤墅会馆(含私汤)",
|
||||
"fzName": "汤墅会馆",
|
||||
"mtName": "汤墅会馆(私汤)",
|
||||
"dyHotelName": "",
|
||||
"dyHotSrpingName": "",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:28",
|
||||
"id": "7a21366f0ed3421592a3ee6835e03433",
|
||||
"pmsName": "高级家庭套房",
|
||||
"xcName": "高级家庭套房",
|
||||
"fzName": "",
|
||||
"mtName": "",
|
||||
"dyHotelName": "高级家庭套房",
|
||||
"dyHotSrpingName": "高级家庭套房",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:30",
|
||||
"id": "822bede1a81641e6853a6f6313185b85",
|
||||
"pmsName": "高级标准间",
|
||||
"xcName": "高级标准间",
|
||||
"fzName": "高级标准间",
|
||||
"mtName": "高级标准间",
|
||||
"dyHotelName": "高级标准间",
|
||||
"dyHotSrpingName": "高级标准间(含双人温泉,入住期间不限次)",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:29",
|
||||
"id": "88bfd80b66454f14bb0fe2ea61cee7b3",
|
||||
"pmsName": "至尊汤屋",
|
||||
"xcName": "至尊养生汤屋(含私汤)",
|
||||
"fzName": "至尊养生汤屋(含私汤)",
|
||||
"mtName": "至尊养生汤屋(私汤)",
|
||||
"dyHotelName": "至尊养生汤屋",
|
||||
"dyHotSrpingName": "至尊养生汤屋-含双人温泉、入住期间不限次",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:32",
|
||||
"id": "9011e8d8d33945409344751132d21912",
|
||||
"pmsName": "尊享豪大",
|
||||
"xcName": "尊享豪华大床房(含私汤)",
|
||||
"fzName": "豪华尊享大床房(双人温泉)",
|
||||
"mtName": "尊享豪华大床房(私汤)",
|
||||
"dyHotelName": "",
|
||||
"dyHotSrpingName": "尊享豪华大床房-私汤",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:34",
|
||||
"id": "94281bdcf8664a5bbcb3add6f62736fe",
|
||||
"pmsName": "高级双人套房",
|
||||
"xcName": "",
|
||||
"fzName": "",
|
||||
"mtName": "",
|
||||
"dyHotelName": "高级双人套房",
|
||||
"dyHotSrpingName": "高级双人套房-含双人温泉、入住期间不限次",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:33",
|
||||
"id": "a211b49ff3f94f5a8d8c30bf80f7289e",
|
||||
"pmsName": "豪华至尊养生汤屋",
|
||||
"xcName": "豪华至尊养生汤屋(含私汤)",
|
||||
"fzName": "豪华至尊养生汤屋(私汤)",
|
||||
"mtName": "豪华至尊养生汤屋(私汤)",
|
||||
"dyHotelName": "豪华至尊养生汤屋",
|
||||
"dyHotSrpingName": "豪华至尊养生汤屋",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:35",
|
||||
"id": "b9042d303bbe4ee09a4ffb567692714a",
|
||||
"pmsName": "豪华家庭套房",
|
||||
"xcName": "",
|
||||
"fzName": "",
|
||||
"mtName": "",
|
||||
"dyHotelName": "",
|
||||
"dyHotSrpingName": "",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:36",
|
||||
"id": "bafb56cd74f44b44b836d727f5b604a7",
|
||||
"pmsName": "豪华套房",
|
||||
"xcName": "豪华套房(含私汤)",
|
||||
"fzName": "天伦小筑-豪华套房(含私汤)",
|
||||
"mtName": "豪华套房(私汤)",
|
||||
"dyHotelName": "",
|
||||
"dyHotSrpingName": "",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:37",
|
||||
"id": "c03a339b407348438b056aca908b5580",
|
||||
"pmsName": "雅致大床房",
|
||||
"xcName": "雅致大床房",
|
||||
"fzName": "",
|
||||
"mtName": "",
|
||||
"dyHotelName": "雅致大床",
|
||||
"dyHotSrpingName": "雅致大床-房含双人温泉、入住期间不限次",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:38",
|
||||
"id": "c68fa888684641ff9be8e25b83e4e347",
|
||||
"pmsName": "榻榻米双床房",
|
||||
"xcName": "榻榻米双床房",
|
||||
"fzName": "榻榻米双床房",
|
||||
"mtName": "榻榻米双床房",
|
||||
"dyHotelName": "榻榻米双床房",
|
||||
"dyHotSrpingName": "榻榻米双床房-含双人温泉,入住期间不限次",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-03-07 04:48:15",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-03-07 14:22:39",
|
||||
"id": "e6f8f7fb6f014a3a84f0a037df8544e6",
|
||||
"pmsName": "雅致标准间",
|
||||
"xcName": "雅致双床房",
|
||||
"fzName": "雅致双床房",
|
||||
"mtName": "",
|
||||
"dyHotelName": "雅致标准间",
|
||||
"dyHotSrpingName": "雅致标准间-含双人温泉、入住期间不限次",
|
||||
"pcConfigId": "11",
|
||||
"delFlag": 0,
|
||||
"size": null,
|
||||
"current": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user