Spaces:
Running
Running
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>
|