Spaces:
Paused
Paused
github-actions[bot]
commited on
Commit
·
dd27eb1
1
Parent(s):
ed42d52
Update from GitHub Actions
Browse files- src/config.js +1 -1
- src/execute-command.js +6 -5
- src/login.js +19 -18
- src/scheduler.js +17 -16
- src/start-services.js +1 -1
- src/utils/common-utils.js +19 -112
- src/utils/logger.js +195 -0
- src/utils/webide-utils.js +21 -20
- src/web-server.js +29 -1
src/config.js
CHANGED
|
@@ -29,7 +29,7 @@ const config = {
|
|
| 29 |
},
|
| 30 |
|
| 31 |
// 要执行的命令
|
| 32 |
-
command: 'service cron start',
|
| 33 |
|
| 34 |
// 截图保存目录
|
| 35 |
screenshotDir: './screenshots',
|
|
|
|
| 29 |
},
|
| 30 |
|
| 31 |
// 要执行的命令
|
| 32 |
+
command: 'service cron start && date',
|
| 33 |
|
| 34 |
// 截图保存目录
|
| 35 |
screenshotDir: './screenshots',
|
src/execute-command.js
CHANGED
|
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'url';
|
|
| 3 |
import path from 'path';
|
| 4 |
|
| 5 |
import { createBrowserSession, navigateToWebIDE, executeCommandFlow } from './utils/webide-utils.js';
|
|
|
|
| 6 |
|
| 7 |
async function executeCommand() {
|
| 8 |
|
|
@@ -20,16 +21,16 @@ async function executeCommand() {
|
|
| 20 |
|
| 21 |
// 保持浏览器打开一段时间以便查看结果
|
| 22 |
if (!config.browserOptions.headless) {
|
| 23 |
-
|
| 24 |
await page.waitForTimeout(5000);
|
| 25 |
}
|
| 26 |
|
| 27 |
-
} catch (
|
| 28 |
-
|
| 29 |
} finally {
|
| 30 |
if (browser) {
|
| 31 |
await browser.close();
|
| 32 |
-
|
| 33 |
}
|
| 34 |
}
|
| 35 |
}
|
|
@@ -39,7 +40,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
| 39 |
const scriptPath = path.resolve(process.argv[1]);
|
| 40 |
|
| 41 |
if (path.resolve(__filename) === scriptPath) {
|
| 42 |
-
executeCommand().catch(
|
| 43 |
}
|
| 44 |
|
| 45 |
export { executeCommand };
|
|
|
|
| 3 |
import path from 'path';
|
| 4 |
|
| 5 |
import { createBrowserSession, navigateToWebIDE, executeCommandFlow } from './utils/webide-utils.js';
|
| 6 |
+
import { info, error } from './utils/logger.js';
|
| 7 |
|
| 8 |
async function executeCommand() {
|
| 9 |
|
|
|
|
| 21 |
|
| 22 |
// 保持浏览器打开一段时间以便查看结果
|
| 23 |
if (!config.browserOptions.headless) {
|
| 24 |
+
info('浏览器将保持打开5秒以便查看结果...');
|
| 25 |
await page.waitForTimeout(5000);
|
| 26 |
}
|
| 27 |
|
| 28 |
+
} catch (err) {
|
| 29 |
+
error('执行命令过程中发生错误:', err);
|
| 30 |
} finally {
|
| 31 |
if (browser) {
|
| 32 |
await browser.close();
|
| 33 |
+
info('浏览器已关闭');
|
| 34 |
}
|
| 35 |
}
|
| 36 |
}
|
|
|
|
| 40 |
const scriptPath = path.resolve(process.argv[1]);
|
| 41 |
|
| 42 |
if (path.resolve(__filename) === scriptPath) {
|
| 43 |
+
executeCommand().catch(error);
|
| 44 |
}
|
| 45 |
|
| 46 |
export { executeCommand };
|
src/login.js
CHANGED
|
@@ -3,9 +3,10 @@ import fs from 'fs';
|
|
| 3 |
import config from './config.js';
|
| 4 |
import { fileURLToPath } from 'url';
|
| 5 |
import path from 'path';
|
| 6 |
-
import { loadCookies
|
|
|
|
| 7 |
async function login() {
|
| 8 |
-
|
| 9 |
const browser = await chromium.launch(config.browserOptions);
|
| 10 |
const context = await browser.newContext();
|
| 11 |
|
|
@@ -15,55 +16,55 @@ async function login() {
|
|
| 15 |
const page = await context.newPage();
|
| 16 |
|
| 17 |
try {
|
| 18 |
-
|
| 19 |
// 首先访问主页面,通常会重定向到登录页面
|
| 20 |
await page.goto(config.webideUrl);
|
| 21 |
|
| 22 |
// 等待页面加载
|
| 23 |
await page.waitForTimeout(config.waitTimes.pageLoad);
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
|
| 28 |
// 检查是否已经登录(如果页面包含编辑器元素,说明已登录)
|
| 29 |
const isLoggedIn = await page.locator(config.selectors.editor).count() > 0;
|
| 30 |
|
| 31 |
if (isLoggedIn) {
|
| 32 |
-
|
| 33 |
} else {
|
| 34 |
-
|
| 35 |
-
|
| 36 |
|
| 37 |
// 等待用户手动登录
|
| 38 |
await waitForUserInput();
|
| 39 |
|
| 40 |
// 等待登录完成,检查是否出现编辑器界面
|
| 41 |
-
|
| 42 |
try {
|
| 43 |
await page.goto(config.webideUrl);
|
| 44 |
await page.waitForSelector(config.selectors.editor, {
|
| 45 |
timeout: 60000
|
| 46 |
});
|
| 47 |
|
| 48 |
-
} catch (
|
| 49 |
-
|
| 50 |
}
|
| 51 |
}
|
| 52 |
|
| 53 |
// 保存cookies
|
| 54 |
const cookies = await context.cookies();
|
| 55 |
fs.writeFileSync(config.cookieFile, JSON.stringify(cookies, null, 2));
|
| 56 |
-
|
| 57 |
-
|
| 58 |
|
| 59 |
// 显示保存的cookie信息(仅显示名称,不显示值)
|
| 60 |
-
|
| 61 |
cookies.forEach(cookie => {
|
| 62 |
-
|
| 63 |
});
|
| 64 |
|
| 65 |
-
} catch (
|
| 66 |
-
|
| 67 |
} finally {
|
| 68 |
await browser.close();
|
| 69 |
}
|
|
@@ -90,6 +91,6 @@ const __filename = fileURLToPath(import.meta.url);
|
|
| 90 |
const scriptPath = path.resolve(process.argv[1]);
|
| 91 |
|
| 92 |
if (path.resolve(__filename) === scriptPath) {
|
| 93 |
-
login().catch(
|
| 94 |
}
|
| 95 |
|
|
|
|
| 3 |
import config from './config.js';
|
| 4 |
import { fileURLToPath } from 'url';
|
| 5 |
import path from 'path';
|
| 6 |
+
import { loadCookies } from './utils/common-utils.js';
|
| 7 |
+
import { info, error } from './utils/logger.js';
|
| 8 |
async function login() {
|
| 9 |
+
info('启动浏览器...');
|
| 10 |
const browser = await chromium.launch(config.browserOptions);
|
| 11 |
const context = await browser.newContext();
|
| 12 |
|
|
|
|
| 16 |
const page = await context.newPage();
|
| 17 |
|
| 18 |
try {
|
| 19 |
+
info(`导航到登录页面:${config.webideUrl}...`);
|
| 20 |
// 首先访问主页面,通常会重定向到登录页面
|
| 21 |
await page.goto(config.webideUrl);
|
| 22 |
|
| 23 |
// 等待页面加载
|
| 24 |
await page.waitForTimeout(config.waitTimes.pageLoad);
|
| 25 |
|
| 26 |
+
info('当前页面URL:', page.url());
|
| 27 |
+
info('页面标题:', await page.title());
|
| 28 |
|
| 29 |
// 检查是否已经登录(如果页面包含编辑器元素,说明已登录)
|
| 30 |
const isLoggedIn = await page.locator(config.selectors.editor).count() > 0;
|
| 31 |
|
| 32 |
if (isLoggedIn) {
|
| 33 |
+
info('检测到已经登录状态,保存cookie...');
|
| 34 |
} else {
|
| 35 |
+
info('需要登录,请在浏览器中手动完成登录过程...');
|
| 36 |
+
info('登录完成后,请按 Enter 键继续...');
|
| 37 |
|
| 38 |
// 等待用户手动登录
|
| 39 |
await waitForUserInput();
|
| 40 |
|
| 41 |
// 等待登录完成,检查是否出现编辑器界面
|
| 42 |
+
info('等待登录完成...');
|
| 43 |
try {
|
| 44 |
await page.goto(config.webideUrl);
|
| 45 |
await page.waitForSelector(config.selectors.editor, {
|
| 46 |
timeout: 60000
|
| 47 |
});
|
| 48 |
|
| 49 |
+
} catch (err) {
|
| 50 |
+
info('未检测到编辑器界面,但继续保存cookie...');
|
| 51 |
}
|
| 52 |
}
|
| 53 |
|
| 54 |
// 保存cookies
|
| 55 |
const cookies = await context.cookies();
|
| 56 |
fs.writeFileSync(config.cookieFile, JSON.stringify(cookies, null, 2));
|
| 57 |
+
info(`Cookies已保存到 ${config.cookieFile}`);
|
| 58 |
+
info(`保存了 ${cookies.length} 个cookies`);
|
| 59 |
|
| 60 |
// 显示保存的cookie信息(仅显示名称,不显示值)
|
| 61 |
+
info('保存的cookie名称:');
|
| 62 |
cookies.forEach(cookie => {
|
| 63 |
+
info(` - ${cookie.name} (域名: ${cookie.domain})`);
|
| 64 |
});
|
| 65 |
|
| 66 |
+
} catch (err) {
|
| 67 |
+
error('登录过程中发生错误:', err);
|
| 68 |
} finally {
|
| 69 |
await browser.close();
|
| 70 |
}
|
|
|
|
| 91 |
const scriptPath = path.resolve(process.argv[1]);
|
| 92 |
|
| 93 |
if (path.resolve(__filename) === scriptPath) {
|
| 94 |
+
login().catch(error);
|
| 95 |
}
|
| 96 |
|
src/scheduler.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
| 1 |
import config from './config.js';
|
| 2 |
import { fileURLToPath } from 'url';
|
| 3 |
import path from 'path';
|
| 4 |
-
import {
|
| 5 |
import { createBrowserSession, navigateToWebIDE, executeCommandFlow } from './utils/webide-utils.js';
|
|
|
|
| 6 |
|
| 7 |
// 执行单次命令的函数
|
| 8 |
async function executeCommandOnce(page) {
|
| 9 |
-
|
| 10 |
return executeCommandFlow(page, 'scheduler');
|
| 11 |
}
|
| 12 |
|
| 13 |
// 主调度器函数
|
| 14 |
async function startScheduler() {
|
| 15 |
|
| 16 |
-
|
| 17 |
const intervalSeconds = Math.round(config.schedulerInterval / 1000);
|
| 18 |
-
|
| 19 |
|
| 20 |
let browser;
|
| 21 |
try {
|
|
@@ -33,43 +34,43 @@ async function startScheduler() {
|
|
| 33 |
const intervalId = setInterval(async () => {
|
| 34 |
try {
|
| 35 |
// 重新导航到页面以确保页面活跃
|
| 36 |
-
|
| 37 |
await navigateToWebIDE(page);
|
| 38 |
|
| 39 |
// 执行命令
|
| 40 |
await executeCommandOnce(page);
|
| 41 |
-
} catch (
|
| 42 |
-
|
| 43 |
}
|
| 44 |
}, config.schedulerInterval);
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
|
| 49 |
// 监听进程退出信号
|
| 50 |
process.on('SIGINT', async () => {
|
| 51 |
-
|
| 52 |
clearInterval(intervalId);
|
| 53 |
if (browser) {
|
| 54 |
await browser.close();
|
| 55 |
}
|
| 56 |
-
|
| 57 |
process.exit(0);
|
| 58 |
});
|
| 59 |
|
| 60 |
// 保持进程运行
|
| 61 |
process.on('SIGTERM', async () => {
|
| 62 |
-
|
| 63 |
clearInterval(intervalId);
|
| 64 |
if (browser) {
|
| 65 |
await browser.close();
|
| 66 |
}
|
| 67 |
-
|
| 68 |
process.exit(0);
|
| 69 |
});
|
| 70 |
|
| 71 |
-
} catch (
|
| 72 |
-
|
| 73 |
if (browser) {
|
| 74 |
await browser.close();
|
| 75 |
}
|
|
@@ -81,7 +82,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
| 81 |
const scriptPath = path.resolve(process.argv[1]);
|
| 82 |
|
| 83 |
if (path.resolve(__filename) === scriptPath) {
|
| 84 |
-
startScheduler().catch(
|
| 85 |
}
|
| 86 |
|
| 87 |
export { startScheduler };
|
|
|
|
| 1 |
import config from './config.js';
|
| 2 |
import { fileURLToPath } from 'url';
|
| 3 |
import path from 'path';
|
| 4 |
+
import { getHumanReadableTimestamp } from './utils/common-utils.js';
|
| 5 |
import { createBrowserSession, navigateToWebIDE, executeCommandFlow } from './utils/webide-utils.js';
|
| 6 |
+
import { info, error } from './utils/logger.js';
|
| 7 |
|
| 8 |
// 执行单次命令的函数
|
| 9 |
async function executeCommandOnce(page) {
|
| 10 |
+
info(`[${getHumanReadableTimestamp()}] 开始执行命令...`);
|
| 11 |
return executeCommandFlow(page, 'scheduler');
|
| 12 |
}
|
| 13 |
|
| 14 |
// 主调度器函数
|
| 15 |
async function startScheduler() {
|
| 16 |
|
| 17 |
+
info(`[${getHumanReadableTimestamp()}] 启动调度器...`);
|
| 18 |
const intervalSeconds = Math.round(config.schedulerInterval / 1000);
|
| 19 |
+
info(`调度器将每${intervalSeconds}秒执行一次命令以防止编辑器休眠`);
|
| 20 |
|
| 21 |
let browser;
|
| 22 |
try {
|
|
|
|
| 34 |
const intervalId = setInterval(async () => {
|
| 35 |
try {
|
| 36 |
// 重新导航到页面以确保页面活跃
|
| 37 |
+
info(`[${getHumanReadableTimestamp()}] 重新导航到WebIDE页面...`);
|
| 38 |
await navigateToWebIDE(page);
|
| 39 |
|
| 40 |
// 执行命令
|
| 41 |
await executeCommandOnce(page);
|
| 42 |
+
} catch (err) {
|
| 43 |
+
error(`[${getHumanReadableTimestamp()}] 定时任务执行失败:`, err);
|
| 44 |
}
|
| 45 |
}, config.schedulerInterval);
|
| 46 |
|
| 47 |
+
info(`[${getHumanReadableTimestamp()}] 调度器已启动,将每${intervalSeconds}秒执行一次命令`);
|
| 48 |
+
info('按 Ctrl+C 停止调度器');
|
| 49 |
|
| 50 |
// 监听进程退出信号
|
| 51 |
process.on('SIGINT', async () => {
|
| 52 |
+
info(`\n[${getHumanReadableTimestamp()}] 收到停止信号,正在关闭调度器...`);
|
| 53 |
clearInterval(intervalId);
|
| 54 |
if (browser) {
|
| 55 |
await browser.close();
|
| 56 |
}
|
| 57 |
+
info('调度器已停止,浏览器已关闭');
|
| 58 |
process.exit(0);
|
| 59 |
});
|
| 60 |
|
| 61 |
// 保持进程运行
|
| 62 |
process.on('SIGTERM', async () => {
|
| 63 |
+
info(`\n[${getHumanReadableTimestamp()}] 收到终止信号,正在关闭调度器...`);
|
| 64 |
clearInterval(intervalId);
|
| 65 |
if (browser) {
|
| 66 |
await browser.close();
|
| 67 |
}
|
| 68 |
+
info('调度器已停止,浏览器已关闭');
|
| 69 |
process.exit(0);
|
| 70 |
});
|
| 71 |
|
| 72 |
+
} catch (err) {
|
| 73 |
+
error(`[${getHumanReadableTimestamp()}] 调度器启动失败:`, err);
|
| 74 |
if (browser) {
|
| 75 |
await browser.close();
|
| 76 |
}
|
|
|
|
| 82 |
const scriptPath = path.resolve(process.argv[1]);
|
| 83 |
|
| 84 |
if (path.resolve(__filename) === scriptPath) {
|
| 85 |
+
startScheduler().catch(error);
|
| 86 |
}
|
| 87 |
|
| 88 |
export { startScheduler };
|
src/start-services.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
import { spawn } from 'child_process';
|
| 9 |
-
import { log, logError } from './utils/
|
| 10 |
|
| 11 |
// 存储子进程
|
| 12 |
const processes = [];
|
|
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
import { spawn } from 'child_process';
|
| 9 |
+
import { info as log, error as logError } from './utils/logger.js';
|
| 10 |
|
| 11 |
// 存储子进程
|
| 12 |
const processes = [];
|
src/utils/common-utils.js
CHANGED
|
@@ -1,99 +1,6 @@
|
|
| 1 |
import fs from 'fs';
|
| 2 |
import path from 'path';
|
| 3 |
-
|
| 4 |
-
// 日志文件路径
|
| 5 |
-
const LOG_FILE = './logs/app.log';
|
| 6 |
-
const LOG_DIR = './logs';
|
| 7 |
-
|
| 8 |
-
/**
|
| 9 |
-
* 确保日志目录存在
|
| 10 |
-
*/
|
| 11 |
-
function ensureLogDirectory() {
|
| 12 |
-
if (!fs.existsSync(LOG_DIR)) {
|
| 13 |
-
fs.mkdirSync(LOG_DIR, { recursive: true });
|
| 14 |
-
}
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
/**
|
| 18 |
-
* 写入日志到文件
|
| 19 |
-
* @param {string} level - 日志级别 (INFO, ERROR, WARN)
|
| 20 |
-
* @param {string} message - 日志消息
|
| 21 |
-
*/
|
| 22 |
-
export function writeLog(level, message) {
|
| 23 |
-
ensureLogDirectory();
|
| 24 |
-
const timestamp = new Date().toISOString();
|
| 25 |
-
const logEntry = `[${timestamp}] [${level}] ${message}\n`;
|
| 26 |
-
|
| 27 |
-
try {
|
| 28 |
-
fs.appendFileSync(LOG_FILE, logEntry);
|
| 29 |
-
} catch (error) {
|
| 30 |
-
console.error('写入日志文件失败:', error);
|
| 31 |
-
}
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
/**
|
| 35 |
-
* 增强的 console.log,同时输出到控制台和文件
|
| 36 |
-
* @param {...any} args - 要记录的参数
|
| 37 |
-
*/
|
| 38 |
-
export function log(...args) {
|
| 39 |
-
const message = args.map(arg =>
|
| 40 |
-
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
| 41 |
-
).join(' ');
|
| 42 |
-
|
| 43 |
-
console.log(...args);
|
| 44 |
-
writeLog('INFO', message);
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
/**
|
| 48 |
-
* 增强的 console.error,同时输出到控制台和文件
|
| 49 |
-
* @param {...any} args - 要记录的参数
|
| 50 |
-
*/
|
| 51 |
-
export function logError(...args) {
|
| 52 |
-
const message = args.map(arg =>
|
| 53 |
-
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
| 54 |
-
).join(' ');
|
| 55 |
-
|
| 56 |
-
console.error(...args);
|
| 57 |
-
writeLog('ERROR', message);
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
/**
|
| 61 |
-
* 读取最近的日志
|
| 62 |
-
* @param {number} lines - 要读取的行数,默认100行
|
| 63 |
-
* @returns {Array} 日志行数组
|
| 64 |
-
*/
|
| 65 |
-
export function getRecentLogs(lines = 100) {
|
| 66 |
-
try {
|
| 67 |
-
if (!fs.existsSync(LOG_FILE)) {
|
| 68 |
-
return [];
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
const content = fs.readFileSync(LOG_FILE, 'utf8');
|
| 72 |
-
const logLines = content.trim().split('\n').filter(line => line.length > 0);
|
| 73 |
-
|
| 74 |
-
// 返回最后 N 行
|
| 75 |
-
return logLines.slice(-lines);
|
| 76 |
-
} catch (error) {
|
| 77 |
-
console.error('读取日志文件失败:', error);
|
| 78 |
-
return [];
|
| 79 |
-
}
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
/**
|
| 83 |
-
* 清空日志文件
|
| 84 |
-
* @returns {boolean} 是否成功清空
|
| 85 |
-
*/
|
| 86 |
-
export function clearLogFile() {
|
| 87 |
-
try {
|
| 88 |
-
ensureLogDirectory();
|
| 89 |
-
fs.writeFileSync(LOG_FILE, '');
|
| 90 |
-
console.log('日志文件已清空');
|
| 91 |
-
return true;
|
| 92 |
-
} catch (error) {
|
| 93 |
-
console.error('清空日志文件失败:', error);
|
| 94 |
-
return false;
|
| 95 |
-
}
|
| 96 |
-
}
|
| 97 |
|
| 98 |
/**
|
| 99 |
* 创建人类可读的时间戳
|
|
@@ -118,7 +25,7 @@ export function getHumanReadableTimestamp() {
|
|
| 118 |
export function ensureScreenshotDirectory(dir) {
|
| 119 |
if (!fs.existsSync(dir)) {
|
| 120 |
fs.mkdirSync(dir, { recursive: true });
|
| 121 |
-
|
| 122 |
}
|
| 123 |
}
|
| 124 |
|
|
@@ -131,20 +38,20 @@ export function ensureScreenshotDirectory(dir) {
|
|
| 131 |
export function checkCookieAvailability(cookieFile, cookiesFromEnv) {
|
| 132 |
// 优先检查环境变量
|
| 133 |
if (cookiesFromEnv) {
|
| 134 |
-
|
| 135 |
try {
|
| 136 |
JSON.parse(cookiesFromEnv);
|
| 137 |
return true;
|
| 138 |
-
} catch (
|
| 139 |
-
|
| 140 |
-
|
| 141 |
}
|
| 142 |
}
|
| 143 |
|
| 144 |
// 检查cookie文件
|
| 145 |
if (!fs.existsSync(cookieFile)) {
|
| 146 |
-
|
| 147 |
-
|
| 148 |
return false;
|
| 149 |
}
|
| 150 |
return true;
|
|
@@ -157,8 +64,8 @@ export function checkCookieAvailability(cookieFile, cookiesFromEnv) {
|
|
| 157 |
*/
|
| 158 |
export function checkCookieFile(cookieFile) {
|
| 159 |
if (!fs.existsSync(cookieFile)) {
|
| 160 |
-
|
| 161 |
-
|
| 162 |
return false;
|
| 163 |
}
|
| 164 |
return true;
|
|
@@ -174,23 +81,23 @@ export function loadCookies(cookieFile, cookiesFromEnv) {
|
|
| 174 |
try {
|
| 175 |
// 优先使用环境变量
|
| 176 |
if (cookiesFromEnv) {
|
| 177 |
-
|
| 178 |
const cookies = JSON.parse(cookiesFromEnv);
|
| 179 |
-
|
| 180 |
return cookies;
|
| 181 |
}
|
| 182 |
|
| 183 |
// 使用文件
|
| 184 |
if (fs.existsSync(cookieFile)) {
|
| 185 |
-
|
| 186 |
const cookies = JSON.parse(fs.readFileSync(cookieFile, 'utf8'));
|
| 187 |
-
|
| 188 |
return cookies;
|
| 189 |
}
|
| 190 |
return [];
|
| 191 |
-
} catch (
|
| 192 |
-
|
| 193 |
-
throw
|
| 194 |
}
|
| 195 |
}
|
| 196 |
|
|
@@ -204,8 +111,8 @@ export function loadCookies(cookieFile, cookiesFromEnv) {
|
|
| 204 |
export async function saveScreenshot(page, screenshotDir, prefix = 'screenshot') {
|
| 205 |
ensureScreenshotDirectory(screenshotDir);
|
| 206 |
const timestamp = getHumanReadableTimestamp();
|
| 207 |
-
const screenshotPath = path.join(screenshotDir, `${prefix}
|
| 208 |
await page.screenshot({ path: screenshotPath });
|
| 209 |
-
|
| 210 |
return screenshotPath;
|
| 211 |
}
|
|
|
|
| 1 |
import fs from 'fs';
|
| 2 |
import path from 'path';
|
| 3 |
+
import { info, error } from './logger.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
/**
|
| 6 |
* 创建人类可读的时间戳
|
|
|
|
| 25 |
export function ensureScreenshotDirectory(dir) {
|
| 26 |
if (!fs.existsSync(dir)) {
|
| 27 |
fs.mkdirSync(dir, { recursive: true });
|
| 28 |
+
info(`创建截图目录: ${dir}`);
|
| 29 |
}
|
| 30 |
}
|
| 31 |
|
|
|
|
| 38 |
export function checkCookieAvailability(cookieFile, cookiesFromEnv) {
|
| 39 |
// 优先检查环境变量
|
| 40 |
if (cookiesFromEnv) {
|
| 41 |
+
info('发现环境变量COOKIES,将使用环境变量中的cookies');
|
| 42 |
try {
|
| 43 |
JSON.parse(cookiesFromEnv);
|
| 44 |
return true;
|
| 45 |
+
} catch (err) {
|
| 46 |
+
error('环境变量COOKIES格式无效:', err.message);
|
| 47 |
+
info('将尝试使用cookie文件...');
|
| 48 |
}
|
| 49 |
}
|
| 50 |
|
| 51 |
// 检查cookie文件
|
| 52 |
if (!fs.existsSync(cookieFile)) {
|
| 53 |
+
error(`Cookie文件不存在: ${cookieFile}`);
|
| 54 |
+
info('请先运行 npm run login 进行登录,或设置环境变量COOKIES');
|
| 55 |
return false;
|
| 56 |
}
|
| 57 |
return true;
|
|
|
|
| 64 |
*/
|
| 65 |
export function checkCookieFile(cookieFile) {
|
| 66 |
if (!fs.existsSync(cookieFile)) {
|
| 67 |
+
error(`Cookie文件不存在: ${cookieFile}`);
|
| 68 |
+
info('请先运行 npm run login 进行登录');
|
| 69 |
return false;
|
| 70 |
}
|
| 71 |
return true;
|
|
|
|
| 81 |
try {
|
| 82 |
// 优先使用环境变量
|
| 83 |
if (cookiesFromEnv) {
|
| 84 |
+
info('从环境变量COOKIES加载cookies...');
|
| 85 |
const cookies = JSON.parse(cookiesFromEnv);
|
| 86 |
+
info(`已从环境变量加载 ${cookies.length} 个cookies`);
|
| 87 |
return cookies;
|
| 88 |
}
|
| 89 |
|
| 90 |
// 使用文件
|
| 91 |
if (fs.existsSync(cookieFile)) {
|
| 92 |
+
info(`从文件加载cookies: ${cookieFile}`);
|
| 93 |
const cookies = JSON.parse(fs.readFileSync(cookieFile, 'utf8'));
|
| 94 |
+
info(`已从文件加载 ${cookies.length} 个cookies`);
|
| 95 |
return cookies;
|
| 96 |
}
|
| 97 |
return [];
|
| 98 |
+
} catch (err) {
|
| 99 |
+
error('读取 Cookie 失败:', err);
|
| 100 |
+
throw err;
|
| 101 |
}
|
| 102 |
}
|
| 103 |
|
|
|
|
| 111 |
export async function saveScreenshot(page, screenshotDir, prefix = 'screenshot') {
|
| 112 |
ensureScreenshotDirectory(screenshotDir);
|
| 113 |
const timestamp = getHumanReadableTimestamp();
|
| 114 |
+
const screenshotPath = path.join(screenshotDir, `${prefix}.png`);
|
| 115 |
await page.screenshot({ path: screenshotPath });
|
| 116 |
+
info(`截图已保存: ${screenshotPath}`);
|
| 117 |
return screenshotPath;
|
| 118 |
}
|
src/utils/logger.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from 'fs';
|
| 2 |
+
|
| 3 |
+
// 日志级别枚举
|
| 4 |
+
export const LogLevel = {
|
| 5 |
+
DEBUG: 'DEBUG',
|
| 6 |
+
INFO: 'INFO',
|
| 7 |
+
WARN: 'WARN',
|
| 8 |
+
ERROR: 'ERROR'
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
// 日志配置
|
| 12 |
+
const LOG_CONFIG = {
|
| 13 |
+
logFile: './logs/app.log',
|
| 14 |
+
logDir: './logs',
|
| 15 |
+
enableConsole: true,
|
| 16 |
+
enableFile: true,
|
| 17 |
+
logLevel: LogLevel.INFO
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* 确保日志目录存在
|
| 22 |
+
*/
|
| 23 |
+
function ensureLogDirectory() {
|
| 24 |
+
if (!fs.existsSync(LOG_CONFIG.logDir)) {
|
| 25 |
+
fs.mkdirSync(LOG_CONFIG.logDir, { recursive: true });
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/**
|
| 30 |
+
* 格式化日志消息
|
| 31 |
+
* @param {...any} args - 要记录的参数
|
| 32 |
+
* @returns {string} 格式化后的消息
|
| 33 |
+
*/
|
| 34 |
+
function formatMessage(...args) {
|
| 35 |
+
return args.map(arg =>
|
| 36 |
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
| 37 |
+
).join(' ');
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* 写入日志到文件
|
| 42 |
+
* @param {string} level - 日志级别
|
| 43 |
+
* @param {string} message - 日志消息
|
| 44 |
+
*/
|
| 45 |
+
function writeToFile(level, message) {
|
| 46 |
+
if (!LOG_CONFIG.enableFile) return;
|
| 47 |
+
|
| 48 |
+
ensureLogDirectory();
|
| 49 |
+
const timestamp = new Date().toISOString();
|
| 50 |
+
const logEntry = `[${timestamp}] [${level}] ${message}\n`;
|
| 51 |
+
|
| 52 |
+
try {
|
| 53 |
+
fs.appendFileSync(LOG_CONFIG.logFile, logEntry);
|
| 54 |
+
} catch (error) {
|
| 55 |
+
// 避免循环调用,直接使用console
|
| 56 |
+
console.error('写入日志文件失败:', error);
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/**
|
| 61 |
+
* 输出到控制台
|
| 62 |
+
* @param {string} level - 日志级别
|
| 63 |
+
* @param {...any} args - 要记录的参数
|
| 64 |
+
*/
|
| 65 |
+
function writeToConsole(level, ...args) {
|
| 66 |
+
if (!LOG_CONFIG.enableConsole) return;
|
| 67 |
+
|
| 68 |
+
switch (level) {
|
| 69 |
+
case LogLevel.DEBUG:
|
| 70 |
+
console.debug(...args);
|
| 71 |
+
break;
|
| 72 |
+
case LogLevel.INFO:
|
| 73 |
+
console.log(...args);
|
| 74 |
+
break;
|
| 75 |
+
case LogLevel.WARN:
|
| 76 |
+
console.warn(...args);
|
| 77 |
+
break;
|
| 78 |
+
case LogLevel.ERROR:
|
| 79 |
+
console.error(...args);
|
| 80 |
+
break;
|
| 81 |
+
default:
|
| 82 |
+
console.log(...args);
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/**
|
| 87 |
+
* 通用日志记录函数
|
| 88 |
+
* @param {string} level - 日志级别
|
| 89 |
+
* @param {...any} args - 要记录的参数
|
| 90 |
+
*/
|
| 91 |
+
function writeLog(level, ...args) {
|
| 92 |
+
const message = formatMessage(...args);
|
| 93 |
+
|
| 94 |
+
// 输出到控制台
|
| 95 |
+
writeToConsole(level, ...args);
|
| 96 |
+
|
| 97 |
+
// 写入文件
|
| 98 |
+
writeToFile(level, message);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/**
|
| 102 |
+
* DEBUG级别日志
|
| 103 |
+
* @param {...any} args - 要记录的参数
|
| 104 |
+
*/
|
| 105 |
+
export function debug(...args) {
|
| 106 |
+
writeLog(LogLevel.DEBUG, ...args);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
/**
|
| 110 |
+
* INFO级别日志(替代console.log)
|
| 111 |
+
* @param {...any} args - 要记录的参数
|
| 112 |
+
*/
|
| 113 |
+
export function info(...args) {
|
| 114 |
+
writeLog(LogLevel.INFO, ...args);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/**
|
| 118 |
+
* WARN级别日志(替代console.warn)
|
| 119 |
+
* @param {...any} args - 要记录的参数
|
| 120 |
+
*/
|
| 121 |
+
export function warn(...args) {
|
| 122 |
+
writeLog(LogLevel.WARN, ...args);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/**
|
| 126 |
+
* ERROR级别日志(替代console.error)
|
| 127 |
+
* @param {...any} args - 要记录的参数
|
| 128 |
+
*/
|
| 129 |
+
export function error(...args) {
|
| 130 |
+
writeLog(LogLevel.ERROR, ...args);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/**
|
| 134 |
+
* 读取最近的日志
|
| 135 |
+
* @param {number} lines - 要读取的行数,默认100行
|
| 136 |
+
* @returns {Array} 日志行数组
|
| 137 |
+
*/
|
| 138 |
+
export function getRecentLogs(lines = 100) {
|
| 139 |
+
try {
|
| 140 |
+
if (!fs.existsSync(LOG_CONFIG.logFile)) {
|
| 141 |
+
return [];
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
const content = fs.readFileSync(LOG_CONFIG.logFile, 'utf8');
|
| 145 |
+
const logLines = content.trim().split('\n').filter(line => line.length > 0);
|
| 146 |
+
|
| 147 |
+
// 返回最后 N 行
|
| 148 |
+
return logLines.slice(-lines);
|
| 149 |
+
} catch (err) {
|
| 150 |
+
console.error('读取日志文件失败:', err);
|
| 151 |
+
return [];
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/**
|
| 156 |
+
* 清空日志文件
|
| 157 |
+
* @returns {boolean} 是否成功清空
|
| 158 |
+
*/
|
| 159 |
+
export function clearLogFile() {
|
| 160 |
+
try {
|
| 161 |
+
ensureLogDirectory();
|
| 162 |
+
fs.writeFileSync(LOG_CONFIG.logFile, '');
|
| 163 |
+
info('日志文件已清空');
|
| 164 |
+
return true;
|
| 165 |
+
} catch (err) {
|
| 166 |
+
error('清空日志文件失败:', err);
|
| 167 |
+
return false;
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
/**
|
| 172 |
+
* 配置日志设置
|
| 173 |
+
* @param {Object} config - 日志配置
|
| 174 |
+
*/
|
| 175 |
+
export function configureLogger(config = {}) {
|
| 176 |
+
Object.assign(LOG_CONFIG, config);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// 为了向后兼容,导出原有的函数名
|
| 180 |
+
export const log = info;
|
| 181 |
+
export const logError = error;
|
| 182 |
+
|
| 183 |
+
// 默认导出logger对象
|
| 184 |
+
export default {
|
| 185 |
+
debug,
|
| 186 |
+
info,
|
| 187 |
+
warn,
|
| 188 |
+
error,
|
| 189 |
+
log: info,
|
| 190 |
+
logError: error,
|
| 191 |
+
getRecentLogs,
|
| 192 |
+
clearLogFile,
|
| 193 |
+
configureLogger,
|
| 194 |
+
LogLevel
|
| 195 |
+
};
|
src/utils/webide-utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import { chromium } from 'playwright';
|
| 2 |
import config from '../config.js';
|
| 3 |
import { loadCookies, saveScreenshot, getHumanReadableTimestamp } from './common-utils.js';
|
|
|
|
| 4 |
|
| 5 |
/**
|
| 6 |
* 创建浏览器实例和上下文
|
|
@@ -9,7 +10,7 @@ import { loadCookies, saveScreenshot, getHumanReadableTimestamp } from './common
|
|
| 9 |
* @returns {Object} { browser, context, page }
|
| 10 |
*/
|
| 11 |
export async function createBrowserSession(cookieFile, cookiesFromEnv) {
|
| 12 |
-
|
| 13 |
const browser = await chromium.launch(config.browserOptions);
|
| 14 |
const context = await browser.newContext();
|
| 15 |
|
|
@@ -27,24 +28,24 @@ export async function createBrowserSession(cookieFile, cookiesFromEnv) {
|
|
| 27 |
* @param {Object} page - Playwright 页面对象
|
| 28 |
*/
|
| 29 |
export async function navigateToWebIDE(page) {
|
| 30 |
-
|
| 31 |
await page.goto(config.webideUrl);
|
| 32 |
|
| 33 |
// 等待页面加载
|
| 34 |
await page.waitForTimeout(config.waitTimes.pageLoad);
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
|
| 39 |
// 检查是否成功登录
|
| 40 |
try {
|
| 41 |
await page.waitForSelector(config.selectors.editor, {
|
| 42 |
timeout: 60000
|
| 43 |
});
|
| 44 |
-
|
| 45 |
return true;
|
| 46 |
-
} catch (
|
| 47 |
-
|
| 48 |
return false;
|
| 49 |
}
|
| 50 |
}
|
|
@@ -57,14 +58,14 @@ export async function handleModalDialog(page) {
|
|
| 57 |
try {
|
| 58 |
const dialogButton = await page.waitForSelector(config.selectors.dialogButton, { timeout: 30000 });
|
| 59 |
if (dialogButton && await dialogButton.isVisible()) {
|
| 60 |
-
|
| 61 |
await dialogButton.click();
|
| 62 |
await page.waitForTimeout(500);
|
| 63 |
return true;
|
| 64 |
}
|
| 65 |
-
} catch (
|
| 66 |
// 没有找到对话框按钮,继续执行
|
| 67 |
-
|
| 68 |
}
|
| 69 |
return false;
|
| 70 |
}
|
|
@@ -75,7 +76,7 @@ export async function handleModalDialog(page) {
|
|
| 75 |
* @returns {Object|null} 终端元素或 null
|
| 76 |
*/
|
| 77 |
export async function openTerminal(page) {
|
| 78 |
-
|
| 79 |
|
| 80 |
// 确保页面获得焦点
|
| 81 |
await page.click('body');
|
|
@@ -97,17 +98,17 @@ export async function openTerminal(page) {
|
|
| 97 |
try {
|
| 98 |
terminalElement = await page.waitForSelector(selector, { timeout: 2000 });
|
| 99 |
if (terminalElement) {
|
| 100 |
-
|
| 101 |
terminalFound = true;
|
| 102 |
break;
|
| 103 |
}
|
| 104 |
-
} catch (
|
| 105 |
// 继续尝试下一个选择器
|
| 106 |
}
|
| 107 |
}
|
| 108 |
|
| 109 |
if (!terminalFound) {
|
| 110 |
-
|
| 111 |
return null;
|
| 112 |
} else {
|
| 113 |
// 点击终端区域确保焦点
|
|
@@ -123,7 +124,7 @@ export async function openTerminal(page) {
|
|
| 123 |
* @param {string} command - 要执行的命令
|
| 124 |
*/
|
| 125 |
export async function executeTerminalCommand(page, command) {
|
| 126 |
-
|
| 127 |
|
| 128 |
// 输入命令
|
| 129 |
await page.keyboard.type(command);
|
|
@@ -135,7 +136,7 @@ export async function executeTerminalCommand(page, command) {
|
|
| 135 |
// 等待命令执行
|
| 136 |
await page.waitForTimeout(config.waitTimes.commandExecution);
|
| 137 |
|
| 138 |
-
|
| 139 |
}
|
| 140 |
|
| 141 |
/**
|
|
@@ -156,12 +157,12 @@ export async function executeCommandFlow(page, screenshotPrefix = 'screenshot')
|
|
| 156 |
await executeTerminalCommand(page, config.command);
|
| 157 |
|
| 158 |
// 截图保存执行结果
|
| 159 |
-
|
| 160 |
-
|
| 161 |
|
| 162 |
return true;
|
| 163 |
-
} catch (
|
| 164 |
-
|
| 165 |
return false;
|
| 166 |
}
|
| 167 |
}
|
|
|
|
| 1 |
import { chromium } from 'playwright';
|
| 2 |
import config from '../config.js';
|
| 3 |
import { loadCookies, saveScreenshot, getHumanReadableTimestamp } from './common-utils.js';
|
| 4 |
+
import { info, error } from './logger.js';
|
| 5 |
|
| 6 |
/**
|
| 7 |
* 创建浏览器实例和上下文
|
|
|
|
| 10 |
* @returns {Object} { browser, context, page }
|
| 11 |
*/
|
| 12 |
export async function createBrowserSession(cookieFile, cookiesFromEnv) {
|
| 13 |
+
info('启动浏览器...');
|
| 14 |
const browser = await chromium.launch(config.browserOptions);
|
| 15 |
const context = await browser.newContext();
|
| 16 |
|
|
|
|
| 28 |
* @param {Object} page - Playwright 页面对象
|
| 29 |
*/
|
| 30 |
export async function navigateToWebIDE(page) {
|
| 31 |
+
info('导航到WebIDE页面...');
|
| 32 |
await page.goto(config.webideUrl);
|
| 33 |
|
| 34 |
// 等待页面加载
|
| 35 |
await page.waitForTimeout(config.waitTimes.pageLoad);
|
| 36 |
|
| 37 |
+
info('当前页面URL:', page.url());
|
| 38 |
+
info('页面标题:', await page.title());
|
| 39 |
|
| 40 |
// 检查是否成功登录
|
| 41 |
try {
|
| 42 |
await page.waitForSelector(config.selectors.editor, {
|
| 43 |
timeout: 60000
|
| 44 |
});
|
| 45 |
+
info('成功进入WebIDE界面');
|
| 46 |
return true;
|
| 47 |
+
} catch (err) {
|
| 48 |
+
info('警告: 未检测到编辑器界面,可能需要重新登录');
|
| 49 |
return false;
|
| 50 |
}
|
| 51 |
}
|
|
|
|
| 58 |
try {
|
| 59 |
const dialogButton = await page.waitForSelector(config.selectors.dialogButton, { timeout: 30000 });
|
| 60 |
if (dialogButton && await dialogButton.isVisible()) {
|
| 61 |
+
info('发现模态对话框按钮,点击处理...');
|
| 62 |
await dialogButton.click();
|
| 63 |
await page.waitForTimeout(500);
|
| 64 |
return true;
|
| 65 |
}
|
| 66 |
+
} catch (err) {
|
| 67 |
// 没有找到对话框按钮,继续执行
|
| 68 |
+
info('未发现模态对话框,继续执行...');
|
| 69 |
}
|
| 70 |
return false;
|
| 71 |
}
|
|
|
|
| 76 |
* @returns {Object|null} 终端元素或 null
|
| 77 |
*/
|
| 78 |
export async function openTerminal(page) {
|
| 79 |
+
info('尝试打开终端 (Ctrl+~)...');
|
| 80 |
|
| 81 |
// 确保页面获得焦点
|
| 82 |
await page.click('body');
|
|
|
|
| 98 |
try {
|
| 99 |
terminalElement = await page.waitForSelector(selector, { timeout: 2000 });
|
| 100 |
if (terminalElement) {
|
| 101 |
+
info(`找到终端元素: ${selector}`);
|
| 102 |
terminalFound = true;
|
| 103 |
break;
|
| 104 |
}
|
| 105 |
+
} catch (err) {
|
| 106 |
// 继续尝试下一个选择器
|
| 107 |
}
|
| 108 |
}
|
| 109 |
|
| 110 |
if (!terminalFound) {
|
| 111 |
+
info('未找到终端元素,尝试直接输入命令...');
|
| 112 |
return null;
|
| 113 |
} else {
|
| 114 |
// 点击终端区域确保焦点
|
|
|
|
| 124 |
* @param {string} command - 要执行的命令
|
| 125 |
*/
|
| 126 |
export async function executeTerminalCommand(page, command) {
|
| 127 |
+
info(`执行命令: ${command}`);
|
| 128 |
|
| 129 |
// 输入命令
|
| 130 |
await page.keyboard.type(command);
|
|
|
|
| 136 |
// 等待命令执行
|
| 137 |
await page.waitForTimeout(config.waitTimes.commandExecution);
|
| 138 |
|
| 139 |
+
info('命令已执行');
|
| 140 |
}
|
| 141 |
|
| 142 |
/**
|
|
|
|
| 157 |
await executeTerminalCommand(page, config.command);
|
| 158 |
|
| 159 |
// 截图保存执行结果
|
| 160 |
+
const screenshotDir = config.screenshotDir || './screenshots';
|
| 161 |
+
const screenshotPath = await saveScreenshot(page, screenshotDir, screenshotPrefix);
|
| 162 |
|
| 163 |
return true;
|
| 164 |
+
} catch (err) {
|
| 165 |
+
error(`[${getHumanReadableTimestamp()}] 执行命令时发生错误:`, err);
|
| 166 |
return false;
|
| 167 |
}
|
| 168 |
}
|
src/web-server.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
| 1 |
import { Hono } from 'hono';
|
| 2 |
import { serve } from '@hono/node-server';
|
| 3 |
-
import {
|
|
|
|
|
|
|
| 4 |
import config from './config.js';
|
| 5 |
|
| 6 |
const app = new Hono();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
// 静态 HTML 页面
|
| 8 |
const indexHTML = `
|
| 9 |
<!DOCTYPE html>
|
|
@@ -129,6 +135,23 @@ const indexHTML = `
|
|
| 129 |
color: #6c757d;
|
| 130 |
font-style: italic;
|
| 131 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
</style>
|
| 133 |
</head>
|
| 134 |
<body>
|
|
@@ -160,6 +183,11 @@ const indexHTML = `
|
|
| 160 |
<div class="status" id="status">
|
| 161 |
准备就绪
|
| 162 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
</div>
|
| 164 |
|
| 165 |
<script>
|
|
|
|
| 1 |
import { Hono } from 'hono';
|
| 2 |
import { serve } from '@hono/node-server';
|
| 3 |
+
import { serveStatic } from '@hono/node-server/serve-static';
|
| 4 |
+
import { getRecentLogs, clearLogFile } from './utils/logger.js';
|
| 5 |
+
import { info as log, error as logError } from './utils/logger.js';
|
| 6 |
import config from './config.js';
|
| 7 |
|
| 8 |
const app = new Hono();
|
| 9 |
+
|
| 10 |
+
// 静态文件服务 - 提供 screenshots 目录
|
| 11 |
+
app.use('/screenshots/*', serveStatic({ root: './' }));
|
| 12 |
+
|
| 13 |
// 静态 HTML 页面
|
| 14 |
const indexHTML = `
|
| 15 |
<!DOCTYPE html>
|
|
|
|
| 135 |
color: #6c757d;
|
| 136 |
font-style: italic;
|
| 137 |
}
|
| 138 |
+
.screenshot-section {
|
| 139 |
+
padding: 20px;
|
| 140 |
+
border-bottom: 1px solid #eee;
|
| 141 |
+
text-align: center;
|
| 142 |
+
}
|
| 143 |
+
.screenshot-section h2 {
|
| 144 |
+
margin: 0 0 15px 0;
|
| 145 |
+
color: #333;
|
| 146 |
+
font-size: 1.2em;
|
| 147 |
+
}
|
| 148 |
+
.screenshot-section img {
|
| 149 |
+
max-width: 100%;
|
| 150 |
+
height: auto;
|
| 151 |
+
border: 1px solid #ddd;
|
| 152 |
+
border-radius: 4px;
|
| 153 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
| 154 |
+
}
|
| 155 |
</style>
|
| 156 |
</head>
|
| 157 |
<body>
|
|
|
|
| 183 |
<div class="status" id="status">
|
| 184 |
准备就绪
|
| 185 |
</div>
|
| 186 |
+
<div class="screenshot-section">
|
| 187 |
+
<h2>📸 最新截图</h2>
|
| 188 |
+
<img src="/screenshots/scheduler.png" alt="Scheduler Screenshot" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
|
| 189 |
+
<div style="display: none; color: #6c757d; font-style: italic;">截图文件不存在或无法加载</div>
|
| 190 |
+
</div>
|
| 191 |
</div>
|
| 192 |
|
| 193 |
<script>
|