github-actions[bot] commited on
Commit
86b6928
·
1 Parent(s): ed58e62

Update from GitHub Actions

Browse files
Dockerfile CHANGED
@@ -33,27 +33,21 @@ COPY package*.json ./
33
  # 安装 Node.js 依赖
34
  RUN npm ci --only=production
35
 
36
- # 复制应用程序文件
37
- COPY config.js ./
38
- COPY cookies.json ./
39
- COPY execute-command.js ./
40
- COPY login.js ./
41
- COPY scheduler.js ./
42
- COPY web-server.js ./
43
- COPY start-services.js ./
44
- COPY utils/ ./utils/
45
-
46
- # 创建 screenshots 和 logs 目录
47
- RUN mkdir -p screenshots logs
48
-
49
  # 设置非 root 用户(安全最佳实践)
50
  RUN addgroup -g 1001 -S nodejs && \
51
  adduser -S nodejs -u 1001
52
 
53
- # 更改文件所有权,包括新创建的目录
54
- RUN chown -R nodejs:nodejs /app && \
55
- chmod -R 755 /app/screenshots && \
56
- chmod -R 755 /app/logs
 
 
 
 
 
 
 
57
 
58
  # 切换到非 root 用户
59
  USER nodejs
 
33
  # 安装 Node.js 依赖
34
  RUN npm ci --only=production
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  # 设置非 root 用户(安全最佳实践)
37
  RUN addgroup -g 1001 -S nodejs && \
38
  adduser -S nodejs -u 1001
39
 
40
+ # 复制应用程序文件
41
+ COPY src/ ./src/
42
+
43
+ # 复制 cookies.json 文件(如果存在)
44
+ COPY cookies.json* ./
45
+
46
+ # 创建 screenshots 和 logs 目录,并设置正确的权限
47
+ RUN mkdir -p screenshots logs && \
48
+ chown -R nodejs:nodejs /app && \
49
+ chmod -R 775 /app/screenshots && \
50
+ chmod -R 775 /app/logs
51
 
52
  # 切换到非 root 用户
53
  USER nodejs
package.json CHANGED
@@ -4,12 +4,11 @@
4
  "description": "Playwright automation for CloudStudio WebIDE",
5
  "main": "index.js",
6
  "scripts": {
7
- "login": "node login.js",
8
- "execute": "node execute-command.js",
9
- "scheduler": "node scheduler.js",
10
- "web-server": "node web-server.js",
11
- "start": "node start-services.js",
12
- "start-web-only": "node web-server.js"
13
  },
14
  "keywords": ["playwright", "automation", "webide"],
15
  "author": "",
 
4
  "description": "Playwright automation for CloudStudio WebIDE",
5
  "main": "index.js",
6
  "scripts": {
7
+ "login": "node src/login.js",
8
+ "execute": "node src/execute-command.js",
9
+ "scheduler": "node src/scheduler.js",
10
+ "web-server": "node src/web-server.js",
11
+ "start": "node src/start-services.js"
 
12
  },
13
  "keywords": ["playwright", "automation", "webide"],
14
  "author": "",
src/config.js ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 配置文件
2
+ import dotenv from 'dotenv';
3
+
4
+ // 加载环境变量
5
+ dotenv.config();
6
+
7
+ const config = {
8
+ // WebIDE URL - 可以通过环境变量 WEBIDE_URL 覆盖
9
+ webideUrl: process.env.WEBIDE_URL || 'https://3e8ccf585a6c4fbd9f1aa9f05ac5e415.ap-shanghai.cloudstudio.club/?mode=edit',
10
+
11
+ // 调度器时间间隔(毫秒)- 可以通过环境变量 SCHEDULER_INTERVAL 覆盖
12
+ // 默认为 10 分钟 (10 * 60 * 1000 = 600000 毫秒)
13
+ schedulerInterval: parseInt(process.env.SCHEDULER_INTERVAL) || 10 * 60 * 1000,
14
+
15
+ // Cookie文件路径
16
+ cookieFile: './cookies.json',
17
+
18
+ // 浏览器配置
19
+ browserOptions: {
20
+ // 默认无头模式,可通过环境变量 HEADLESS=false 设置为有头模式
21
+ // 支持 false/False/FALSE 等不同大小写形式
22
+ headless: (process.env.HEADLESS || 'true').toLowerCase() !== 'false',
23
+ slowMo: 100, // 操作间隔时间(毫秒)
24
+ timeout: 30000, // 超时时间(毫秒)
25
+ executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
26
+ },
27
+
28
+ // 要执行的命令
29
+ command: 'service cron start',
30
+
31
+ // 截图保存目录
32
+ screenshotDir: './screenshots',
33
+
34
+ // 等待时间配置(毫秒)
35
+ waitTimes: {
36
+ pageLoad: 5000, // 页面加载等待时间
37
+ terminalOpen: 3000, // 终端打开等待时间
38
+ commandExecution: 2000 // 命令执行等待时间
39
+ },
40
+
41
+ // 页面选择器(需要根据实际登录页面调整)
42
+ selectors: {
43
+ // 这些选择器需要根据实际的登录页面进行调整
44
+ editor: '.monaco-grid-view',
45
+ dialogButton: '.monaco-dialog-modal-block .dialog-buttons a.monaco-button',
46
+ terminals: [
47
+ '.terminal',
48
+ // '.xterm',
49
+ // '.console',
50
+ // '.terminal-container',
51
+ // '.xterm-screen',
52
+ // '.monaco-workbench .part.panel .terminal',
53
+ // '[data-testid="terminal"]',
54
+ // '.integrated-terminal'
55
+ ],
56
+ }
57
+ };
58
+
59
+ export default config;
src/execute-command.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import config from './config.js';
2
+ import { fileURLToPath } from 'url';
3
+ import path from 'path';
4
+ import { checkCookieFile } from './utils/common-utils.js';
5
+ import { createBrowserSession, navigateToWebIDE, executeCommandFlow } from './utils/webide-utils.js';
6
+
7
+ async function executeCommand() {
8
+ // 检查cookie文件是否存在
9
+ if (!checkCookieFile(config.cookieFile)) {
10
+ return;
11
+ }
12
+
13
+ let browser;
14
+ try {
15
+ // 创建浏览器会话
16
+ const { browser: browserInstance, page } = await createBrowserSession(config.cookieFile);
17
+ browser = browserInstance;
18
+
19
+ // 导航到WebIDE页面
20
+ await navigateToWebIDE(page);
21
+
22
+ // 执行命令流程
23
+ const success = await executeCommandFlow(page, 'screenshot');
24
+
25
+ // 保持浏览器打开一段时间以便查看结果
26
+ if (!config.browserOptions.headless) {
27
+ console.log('浏览器将保持打开5秒以便查看结果...');
28
+ await page.waitForTimeout(5000);
29
+ }
30
+
31
+ } catch (error) {
32
+ console.error('执行命令过程中发生错误:', error);
33
+ } finally {
34
+ if (browser) {
35
+ await browser.close();
36
+ console.log('浏览器已关闭');
37
+ }
38
+ }
39
+ }
40
+
41
+ // 运行命令执行脚本
42
+ const __filename = fileURLToPath(import.meta.url);
43
+ const scriptPath = path.resolve(process.argv[1]);
44
+
45
+ if (path.resolve(__filename) === scriptPath) {
46
+ executeCommand().catch(console.error);
47
+ }
48
+
49
+ export { executeCommand };
src/login.js ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { chromium } from 'playwright';
2
+ import fs from 'fs';
3
+ import config from './config.js';
4
+ import { fileURLToPath } from 'url';
5
+ import path from 'path';
6
+ import { loadCookies, saveScreenshot, getHumanReadableTimestamp } from './utils/common-utils.js';
7
+ async function login() {
8
+ console.log('启动浏览器...');
9
+ const browser = await chromium.launch(config.browserOptions);
10
+ const context = await browser.newContext();
11
+
12
+ if (fs.existsSync(config.cookieFile)) {
13
+ // 读取并设置cookies
14
+ const cookies = loadCookies(config.cookieFile);
15
+ await context.addCookies(cookies);
16
+ }
17
+
18
+ const page = await context.newPage();
19
+
20
+ try {
21
+ console.log(`导航到登录页面:${config.webideUrl}...`);
22
+ // 首先访问主页面,通常会重定向到登录页面
23
+ await page.goto(config.webideUrl);
24
+
25
+ // 等待页面加载
26
+ await page.waitForTimeout(config.waitTimes.pageLoad);
27
+
28
+ console.log('当前页面URL:', page.url());
29
+ console.log('页面标题:', await page.title());
30
+
31
+ // 检查是否已经登录(如果页面包含编辑器元素,说明已登录)
32
+ const isLoggedIn = await page.locator(config.selectors.editor).count() > 0;
33
+
34
+ if (isLoggedIn) {
35
+ console.log('检测到已经登录状态,保存cookie...');
36
+ } else {
37
+ console.log('需要登录,请在浏览器中手动完成登录过程...');
38
+ console.log('登录完成后,请按 Enter 键继续...');
39
+
40
+ // 等待用户手动登录
41
+ await waitForUserInput();
42
+
43
+ // 等待登录完成,检查是否出现编辑器界面
44
+ console.log('等待登录完成...');
45
+ try {
46
+ await page.waitForSelector(config.selectors.editor, {
47
+ timeout: 60000
48
+ });
49
+
50
+ } catch (error) {
51
+ console.log('未检测到编辑器界面,但继续保存cookie...');
52
+ }
53
+ }
54
+
55
+ // 保存cookies
56
+ const cookies = await context.cookies();
57
+ fs.writeFileSync(config.cookieFile, JSON.stringify(cookies, null, 2));
58
+ console.log(`Cookies已保存到 ${config.cookieFile}`);
59
+ console.log(`保存了 ${cookies.length} 个cookies`);
60
+
61
+ // 显示保存的cookie信息(仅显示名称,不显示值)
62
+ console.log('保存的cookie名称:');
63
+ cookies.forEach(cookie => {
64
+ console.log(` - ${cookie.name} (域名: ${cookie.domain})`);
65
+ });
66
+
67
+ } catch (error) {
68
+ console.error('登录过程中发生错误:', error);
69
+ } finally {
70
+ await browser.close();
71
+ }
72
+ }
73
+
74
+ // 等待用户输入的辅助函数
75
+ async function waitForUserInput() {
76
+ const { default: readline } = await import('readline');
77
+ return new Promise((resolve) => {
78
+ const rl = readline.createInterface({
79
+ input: process.stdin,
80
+ output: process.stdout
81
+ });
82
+
83
+ rl.question('', () => {
84
+ rl.close();
85
+ resolve();
86
+ });
87
+ });
88
+ }
89
+
90
+ // 运行命令执行脚本
91
+ const __filename = fileURLToPath(import.meta.url);
92
+ const scriptPath = path.resolve(process.argv[1]);
93
+
94
+ if (path.resolve(__filename) === scriptPath) {
95
+ login().catch(console.error);
96
+ }
97
+
src/scheduler.js ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import config from './config.js';
2
+ import { fileURLToPath } from 'url';
3
+ import path from 'path';
4
+ import { checkCookieFile, getHumanReadableTimestamp, log, logError } from './utils/common-utils.js';
5
+ import { createBrowserSession, navigateToWebIDE, executeCommandFlow } from './utils/webide-utils.js';
6
+
7
+ // 执行单次命令的函数
8
+ async function executeCommandOnce(page) {
9
+ log(`[${getHumanReadableTimestamp()}] 开始执行命令...`);
10
+ return executeCommandFlow(page, 'scheduler');
11
+ }
12
+
13
+ // 主调度器函数
14
+ async function startScheduler() {
15
+ // 检查cookie文件是否存在
16
+ if (!checkCookieFile(config.cookieFile)) {
17
+ return;
18
+ }
19
+
20
+ log(`[${getHumanReadableTimestamp()}] 启动调度器...`);
21
+ const intervalSeconds = Math.round(config.schedulerInterval / 1000);
22
+ log(`调度器将每${intervalSeconds}秒执行一次命令以防止编辑器休眠`);
23
+
24
+ let browser;
25
+ try {
26
+ // 创建浏览器会话
27
+ const { browser: browserInstance, page } = await createBrowserSession(config.cookieFile);
28
+ browser = browserInstance;
29
+
30
+ // 导航到WebIDE页面
31
+ await navigateToWebIDE(page);
32
+
33
+ // 立即执行一次命令
34
+ await executeCommandOnce(page);
35
+
36
+ // 设置定时器,按配置的时间间隔执行
37
+ const intervalId = setInterval(async () => {
38
+ try {
39
+ // 重新导航到页面以确保页面活跃
40
+ log(`[${getHumanReadableTimestamp()}] 重新导航到WebIDE页面...`);
41
+ await navigateToWebIDE(page);
42
+
43
+ // 执行命令
44
+ await executeCommandOnce(page);
45
+ } catch (error) {
46
+ logError(`[${getHumanReadableTimestamp()}] 定时任务执行失败:`, error);
47
+ }
48
+ }, config.schedulerInterval);
49
+
50
+ log(`[${getHumanReadableTimestamp()}] 调度器已启动,将每${intervalSeconds}秒执行一次命令`);
51
+ log('按 Ctrl+C 停止调度器');
52
+
53
+ // 监听进程退出信号
54
+ process.on('SIGINT', async () => {
55
+ log(`\n[${getHumanReadableTimestamp()}] 收到停止信号,正在关闭调度器...`);
56
+ clearInterval(intervalId);
57
+ if (browser) {
58
+ await browser.close();
59
+ }
60
+ log('调度器已停止,浏览器已关闭');
61
+ process.exit(0);
62
+ });
63
+
64
+ // 保持进程运行
65
+ process.on('SIGTERM', async () => {
66
+ log(`\n[${getHumanReadableTimestamp()}] 收到终止信号,正在关闭调度器...`);
67
+ clearInterval(intervalId);
68
+ if (browser) {
69
+ await browser.close();
70
+ }
71
+ log('调度器已停止,浏览器已关闭');
72
+ process.exit(0);
73
+ });
74
+
75
+ } catch (error) {
76
+ logError(`[${getHumanReadableTimestamp()}] 调度器启动失败:`, error);
77
+ if (browser) {
78
+ await browser.close();
79
+ }
80
+ }
81
+ }
82
+
83
+ // 运行调度器
84
+ const __filename = fileURLToPath(import.meta.url);
85
+ const scriptPath = path.resolve(process.argv[1]);
86
+
87
+ if (path.resolve(__filename) === scriptPath) {
88
+ startScheduler().catch(console.error);
89
+ }
90
+
91
+ export { startScheduler };
src/start-services.js ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 服务启动脚本
5
+ * 同时启动 Web 服务器和调度器
6
+ */
7
+
8
+ import { spawn } from 'child_process';
9
+ import { log, logError } from './utils/common-utils.js';
10
+
11
+ // 存储子进程
12
+ const processes = [];
13
+
14
+ /**
15
+ * 启动子进程
16
+ */
17
+ function startProcess(name, command, args = []) {
18
+ log(`启动 ${name}...`);
19
+
20
+ const child = spawn('node', [command, ...args], {
21
+ stdio: 'inherit',
22
+ cwd: process.cwd()
23
+ });
24
+
25
+ child.on('error', (error) => {
26
+ logError(`${name} 启动失败:`, error);
27
+ });
28
+
29
+ child.on('exit', (code, signal) => {
30
+ if (code !== 0) {
31
+ logError(`${name} 异常退出,代码: ${code}, 信号: ${signal}`);
32
+ } else {
33
+ log(`${name} 正常退出`);
34
+ }
35
+ });
36
+
37
+ processes.push({ name, process: child });
38
+ return child;
39
+ }
40
+
41
+ /**
42
+ * 优雅关闭所有进程
43
+ */
44
+ function gracefulShutdown() {
45
+ log('收到关闭信号,正在停止所有服务...');
46
+
47
+ processes.forEach(({ name, process }) => {
48
+ if (!process.killed) {
49
+ log(`停止 ${name}...`);
50
+ process.kill('SIGTERM');
51
+ }
52
+ });
53
+
54
+ // 等待一段时间后强制关闭
55
+ setTimeout(() => {
56
+ processes.forEach(({ name, process }) => {
57
+ if (!process.killed) {
58
+ log(`强制停止 ${name}...`);
59
+ process.kill('SIGKILL');
60
+ }
61
+ });
62
+ process.exit(0);
63
+ }, 5000);
64
+ }
65
+
66
+ /**
67
+ * 主函数
68
+ */
69
+ async function main() {
70
+ log('🚀 启动 CloudStudio Runner 服务');
71
+ log('='.repeat(50));
72
+
73
+ try {
74
+ // 启动 Web 服务器
75
+ const webServer = startProcess('Web 服务器', 'src/web-server.js');
76
+
77
+ // 等待一下确保 Web 服务器启动
78
+ await new Promise(resolve => setTimeout(resolve, 2000));
79
+
80
+ // 启动调度器
81
+ const scheduler = startProcess('调度器', 'src/scheduler.js');
82
+
83
+ log('='.repeat(50));
84
+ log('✅ 所有服务已启动');
85
+ log('📊 Web 界面: http://localhost:7860');
86
+ log('⏰ 调度器: 每10分钟执行一次任务');
87
+ log('按 Ctrl+C 停止所有服务');
88
+
89
+ // 监听退出信号
90
+ process.on('SIGINT', gracefulShutdown);
91
+ process.on('SIGTERM', gracefulShutdown);
92
+
93
+ // 保持主进程运行
94
+ process.on('exit', () => {
95
+ log('主进程退出');
96
+ });
97
+
98
+ } catch (error) {
99
+ logError('启动服务失败:', error);
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ // 运行主函数
105
+ import { fileURLToPath } from 'url';
106
+ import path from 'path';
107
+
108
+ const __filename = fileURLToPath(import.meta.url);
109
+ const scriptPath = path.resolve(process.argv[1]);
110
+
111
+ if (path.resolve(__filename) === scriptPath) {
112
+ main().catch(error => {
113
+ logError('服务启动异常:', error);
114
+ process.exit(1);
115
+ });
116
+ }
117
+
118
+ export { main };
src/utils/common-utils.js ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 {string} 格式化的时间戳 YYYY-MM-DD_HH-MM-SS
85
+ */
86
+ export function getHumanReadableTimestamp() {
87
+ const now = new Date();
88
+ const year = now.getFullYear();
89
+ const month = String(now.getMonth() + 1).padStart(2, '0');
90
+ const day = String(now.getDate()).padStart(2, '0');
91
+ const hours = String(now.getHours()).padStart(2, '0');
92
+ const minutes = String(now.getMinutes()).padStart(2, '0');
93
+ const seconds = String(now.getSeconds()).padStart(2, '0');
94
+
95
+ return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
96
+ }
97
+
98
+ /**
99
+ * 确保截图目录存在
100
+ * @param {string} dir - 目录路径
101
+ */
102
+ export function ensureScreenshotDirectory(dir) {
103
+ if (!fs.existsSync(dir)) {
104
+ fs.mkdirSync(dir, { recursive: true });
105
+ console.log(`创建截图目录: ${dir}`);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * 检查 Cookie 文件是否存在
111
+ * @param {string} cookieFile - Cookie 文件路径
112
+ * @returns {boolean} 文件是否存在
113
+ */
114
+ export function checkCookieFile(cookieFile) {
115
+ if (!fs.existsSync(cookieFile)) {
116
+ console.error(`Cookie文件不存在: ${cookieFile}`);
117
+ console.log('请先运行 npm run login 进行登录');
118
+ return false;
119
+ }
120
+ return true;
121
+ }
122
+
123
+ /**
124
+ * 读取并解析 Cookie 文件
125
+ * @param {string} cookieFile - Cookie 文件路径
126
+ * @returns {Array} Cookie 数组
127
+ */
128
+ export function loadCookies(cookieFile) {
129
+ try {
130
+ const cookies = JSON.parse(fs.readFileSync(cookieFile, 'utf8'));
131
+ console.log(`已加载 ${cookies.length} 个cookies`);
132
+ return cookies;
133
+ } catch (error) {
134
+ console.error('读取 Cookie 文件失败:', error);
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * 保存截图
141
+ * @param {Object} page - Playwright 页面对象
142
+ * @param {string} screenshotDir - 截图目录
143
+ * @param {string} prefix - 文件名前缀
144
+ * @returns {string} 截图文件路径
145
+ */
146
+ export async function saveScreenshot(page, screenshotDir, prefix = 'screenshot') {
147
+ ensureScreenshotDirectory(screenshotDir);
148
+ const timestamp = getHumanReadableTimestamp();
149
+ const screenshotPath = path.join(screenshotDir, `${prefix}-${timestamp}.png`);
150
+ await page.screenshot({ path: screenshotPath });
151
+ console.log(`截图已保存: ${screenshotPath}`);
152
+ return screenshotPath;
153
+ }
src/utils/webide-utils.js ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { chromium } from 'playwright';
2
+ import config from '../config.js';
3
+ import { loadCookies, saveScreenshot, getHumanReadableTimestamp } from './common-utils.js';
4
+
5
+ /**
6
+ * 创建浏览器实例和上下文
7
+ * @param {string} cookieFile - Cookie 文件路径
8
+ * @returns {Object} { browser, context, page }
9
+ */
10
+ export async function createBrowserSession(cookieFile) {
11
+ console.log('启动浏览器...');
12
+ const browser = await chromium.launch(config.browserOptions);
13
+ const context = await browser.newContext();
14
+
15
+ // 读取并设置cookies
16
+ const cookies = loadCookies(cookieFile);
17
+ await context.addCookies(cookies);
18
+
19
+ const page = await context.newPage();
20
+
21
+ return { browser, context, page };
22
+ }
23
+
24
+ /**
25
+ * 导航到 WebIDE 页面并验证登录状态
26
+ * @param {Object} page - Playwright 页面对象
27
+ */
28
+ export async function navigateToWebIDE(page) {
29
+ console.log('导航到WebIDE页面...');
30
+ await page.goto(config.webideUrl);
31
+
32
+ // 等待页面加载
33
+ await page.waitForTimeout(config.waitTimes.pageLoad);
34
+
35
+ console.log('当前页面URL:', page.url());
36
+ console.log('页面标题:', await page.title());
37
+
38
+ // 检查是否成功登录
39
+ try {
40
+ await page.waitForSelector(config.selectors.editor, {
41
+ timeout: 60000
42
+ });
43
+ console.log('成功进入WebIDE界面');
44
+ return true;
45
+ } catch (error) {
46
+ console.log('警告: 未检测到编辑器界面,可能需要重新登录');
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * 处理模态对话框
53
+ * @param {Object} page - Playwright 页面对象
54
+ */
55
+ export async function handleModalDialog(page) {
56
+ try {
57
+ const dialogButton = await page.waitForSelector(config.selectors.dialogButton, { timeout: 30000 });
58
+ if (dialogButton && await dialogButton.isVisible()) {
59
+ console.log('发现模态对话框按钮,点击处理...');
60
+ await dialogButton.click();
61
+ await page.waitForTimeout(500);
62
+ return true;
63
+ }
64
+ } catch (error) {
65
+ // 没有找到对话框按钮,继续执行
66
+ console.log('未发现模态对话框,继续执行...');
67
+ }
68
+ return false;
69
+ }
70
+
71
+ /**
72
+ * 打开终端
73
+ * @param {Object} page - Playwright 页面对象
74
+ * @returns {Object|null} 终端元素或 null
75
+ */
76
+ export async function openTerminal(page) {
77
+ console.log('尝试打开终端 (Ctrl+~)...');
78
+
79
+ // 确保页面获得焦点
80
+ await page.click('body');
81
+ await page.waitForTimeout(500);
82
+
83
+ // 按下 Ctrl+~ 打开终端
84
+ await page.keyboard.press('Control+`');
85
+
86
+ // 等待终端打开
87
+ await page.waitForTimeout(config.waitTimes.terminalOpen);
88
+
89
+ // 尝试多种方式查找终端
90
+ const terminalSelectors = config.selectors.terminals;
91
+
92
+ let terminalFound = false;
93
+ let terminalElement = null;
94
+
95
+ for (const selector of terminalSelectors) {
96
+ try {
97
+ terminalElement = await page.waitForSelector(selector, { timeout: 2000 });
98
+ if (terminalElement) {
99
+ console.log(`找到终端元素: ${selector}`);
100
+ terminalFound = true;
101
+ break;
102
+ }
103
+ } catch (error) {
104
+ // 继续尝试下一个选择器
105
+ }
106
+ }
107
+
108
+ if (!terminalFound) {
109
+ console.log('未找到终端元素,尝试直接输入命令...');
110
+ return null;
111
+ } else {
112
+ // 点击终端区域确保焦点
113
+ await terminalElement.click();
114
+ await page.waitForTimeout(500);
115
+ return terminalElement;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 在终端中执行命令
121
+ * @param {Object} page - Playwright 页面对象
122
+ * @param {string} command - 要执行的命令
123
+ */
124
+ export async function executeTerminalCommand(page, command) {
125
+ console.log(`执行命令: ${command}`);
126
+
127
+ // 输入命令
128
+ await page.keyboard.type(command);
129
+ await page.waitForTimeout(500);
130
+
131
+ // 按回车执行命令
132
+ await page.keyboard.press('Enter');
133
+
134
+ // 等待命令执行
135
+ await page.waitForTimeout(config.waitTimes.commandExecution);
136
+
137
+ console.log('命令已执行');
138
+ }
139
+
140
+ /**
141
+ * 完整的命令执行流程
142
+ * @param {Object} page - Playwright 页面对象
143
+ * @param {string} screenshotPrefix - 截图文件名前缀
144
+ * @returns {boolean} 执行是否成功
145
+ */
146
+ export async function executeCommandFlow(page, screenshotPrefix = 'screenshot') {
147
+ try {
148
+ // 处理模态对话框
149
+ await handleModalDialog(page);
150
+
151
+ // 打开终端
152
+ await openTerminal(page);
153
+
154
+ // 执行命令
155
+ await executeTerminalCommand(page, config.command);
156
+
157
+ // 截图保存执行结果
158
+ // const screenshotDir = config.screenshotDir || './screenshots';
159
+ // const screenshotPath = await saveScreenshot(page, screenshotDir, screenshotPrefix);
160
+
161
+ return true;
162
+ } catch (error) {
163
+ console.error(`[${getHumanReadableTimestamp()}] 执行命令时发生错误:`, error);
164
+ return false;
165
+ }
166
+ }
src/web-server.js ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Hono } from 'hono';
2
+ import { serve } from '@hono/node-server';
3
+ import { getRecentLogs, log, logError } from './utils/common-utils.js';
4
+ import config from './config.js';
5
+
6
+ const app = new Hono();
7
+
8
+ // 静态 HTML 页面
9
+ const indexHTML = `
10
+ <!DOCTYPE html>
11
+ <html lang="zh-CN">
12
+ <head>
13
+ <meta charset="UTF-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
15
+ <title>CloudStudio Runner - 日志查看器</title>
16
+ <style>
17
+ body {
18
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
19
+ margin: 0;
20
+ padding: 20px;
21
+ background-color: #f5f5f5;
22
+ color: #333;
23
+ }
24
+ .container {
25
+ max-width: 1200px;
26
+ margin: 0 auto;
27
+ background: white;
28
+ border-radius: 8px;
29
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
30
+ overflow: hidden;
31
+ }
32
+ .header {
33
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
34
+ color: white;
35
+ padding: 20px;
36
+ text-align: center;
37
+ }
38
+ .header h1 {
39
+ margin: 0;
40
+ font-size: 2em;
41
+ }
42
+ .header p {
43
+ margin: 10px 0 0 0;
44
+ opacity: 0.9;
45
+ }
46
+ .controls {
47
+ padding: 20px;
48
+ border-bottom: 1px solid #eee;
49
+ display: flex;
50
+ gap: 10px;
51
+ align-items: center;
52
+ flex-wrap: wrap;
53
+ }
54
+ .controls label {
55
+ font-weight: bold;
56
+ }
57
+ .controls select, .controls button {
58
+ padding: 8px 12px;
59
+ border: 1px solid #ddd;
60
+ border-radius: 4px;
61
+ font-size: 14px;
62
+ }
63
+ .controls button {
64
+ background: #667eea;
65
+ color: white;
66
+ border: none;
67
+ cursor: pointer;
68
+ transition: background 0.3s;
69
+ }
70
+ .controls button:hover {
71
+ background: #5a6fd8;
72
+ }
73
+ .log-container {
74
+ padding: 20px;
75
+ max-height: 600px;
76
+ overflow-y: auto;
77
+ }
78
+ .log-entry {
79
+ margin-bottom: 8px;
80
+ padding: 8px;
81
+ border-radius: 4px;
82
+ font-family: 'Courier New', monospace;
83
+ font-size: 13px;
84
+ line-height: 1.4;
85
+ border-left: 3px solid #ddd;
86
+ }
87
+ .log-entry.info {
88
+ background-color: #f8f9fa;
89
+ border-left-color: #28a745;
90
+ }
91
+ .log-entry.error {
92
+ background-color: #fff5f5;
93
+ border-left-color: #dc3545;
94
+ color: #721c24;
95
+ }
96
+ .log-entry.warn {
97
+ background-color: #fffbf0;
98
+ border-left-color: #ffc107;
99
+ color: #856404;
100
+ }
101
+ .timestamp {
102
+ color: #6c757d;
103
+ font-weight: bold;
104
+ }
105
+ .level {
106
+ font-weight: bold;
107
+ margin: 0 8px;
108
+ }
109
+ .level.info { color: #28a745; }
110
+ .level.error { color: #dc3545; }
111
+ .level.warn { color: #ffc107; }
112
+ .message {
113
+ word-break: break-word;
114
+ }
115
+ .status {
116
+ padding: 10px 20px;
117
+ background: #e9ecef;
118
+ border-top: 1px solid #ddd;
119
+ font-size: 14px;
120
+ color: #6c757d;
121
+ }
122
+ .loading {
123
+ text-align: center;
124
+ padding: 40px;
125
+ color: #6c757d;
126
+ }
127
+ .empty {
128
+ text-align: center;
129
+ padding: 40px;
130
+ color: #6c757d;
131
+ font-style: italic;
132
+ }
133
+ </style>
134
+ </head>
135
+ <body>
136
+ <div class="container">
137
+ <div class="header">
138
+ <h1>🚀 CloudStudio Runner</h1>
139
+ <p>实时日志查看器 - 监控自动化任务执行状态</p>
140
+ </div>
141
+
142
+ <div class="controls">
143
+ <label for="lines">显示行数:</label>
144
+ <select id="lines">
145
+ <option value="50">50 行</option>
146
+ <option value="100" selected>100 行</option>
147
+ <option value="200">200 行</option>
148
+ <option value="500">500 行</option>
149
+ <option value="1000">1000 行</option>
150
+ </select>
151
+
152
+ <button onclick="refreshLogs()">🔄 刷新日志</button>
153
+ <button onclick="toggleAutoRefresh()">⏱️ 自动刷新</button>
154
+ <button onclick="clearLogs()">🗑️ 清空显示</button>
155
+ </div>
156
+
157
+ <div class="log-container" id="logContainer">
158
+ <div class="loading">正在加载日志...</div>
159
+ </div>
160
+
161
+ <div class="status" id="status">
162
+ 准备就绪
163
+ </div>
164
+ </div>
165
+
166
+ <script>
167
+ let autoRefreshInterval = null;
168
+ let isAutoRefresh = false;
169
+
170
+ async function fetchLogs() {
171
+ try {
172
+ const lines = document.getElementById('lines').value;
173
+ const response = await fetch(\`/api/logs?lines=\${lines}\`);
174
+ const data = await response.json();
175
+
176
+ if (data.success) {
177
+ displayLogs(data.logs);
178
+ updateStatus(\`已加载 \${data.logs.length} 条日志 - \${new Date().toLocaleString()}\`);
179
+ } else {
180
+ updateStatus('获取日志失败: ' + data.error);
181
+ }
182
+ } catch (error) {
183
+ updateStatus('网络错误: ' + error.message);
184
+ }
185
+ }
186
+
187
+ function displayLogs(logs) {
188
+ const container = document.getElementById('logContainer');
189
+
190
+ if (logs.length === 0) {
191
+ container.innerHTML = '<div class="empty">暂无日志数据</div>';
192
+ return;
193
+ }
194
+
195
+ const logHTML = logs.map(log => {
196
+ const match = log.match(/\\[(.*?)\\]\\s*\\[(.*?)\\]\\s*(.*)/);
197
+ if (match) {
198
+ const [, timestamp, level, message] = match;
199
+ const levelClass = level.toLowerCase();
200
+ return \`
201
+ <div class="log-entry \${levelClass}">
202
+ <span class="timestamp">\${timestamp}</span>
203
+ <span class="level \${levelClass}">[\${level}]</span>
204
+ <span class="message">\${message}</span>
205
+ </div>
206
+ \`;
207
+ } else {
208
+ return \`
209
+ <div class="log-entry">
210
+ <span class="message">\${log}</span>
211
+ </div>
212
+ \`;
213
+ }
214
+ }).join('');
215
+
216
+ container.innerHTML = logHTML;
217
+ container.scrollTop = container.scrollHeight;
218
+ }
219
+
220
+ function updateStatus(message) {
221
+ document.getElementById('status').textContent = message;
222
+ }
223
+
224
+ function refreshLogs() {
225
+ fetchLogs();
226
+ }
227
+
228
+ function toggleAutoRefresh() {
229
+ const button = event.target;
230
+
231
+ if (isAutoRefresh) {
232
+ clearInterval(autoRefreshInterval);
233
+ autoRefreshInterval = null;
234
+ isAutoRefresh = false;
235
+ button.textContent = '⏱️ 自动刷新';
236
+ updateStatus('自动刷新已停止');
237
+ } else {
238
+ autoRefreshInterval = setInterval(fetchLogs, 5000);
239
+ isAutoRefresh = true;
240
+ button.textContent = '⏹️ 停止刷新';
241
+ updateStatus('自动刷新已启动 (每5秒)');
242
+ }
243
+ }
244
+
245
+ function clearLogs() {
246
+ document.getElementById('logContainer').innerHTML = '<div class="empty">日志显示已清空</div>';
247
+ updateStatus('显示已清空');
248
+ }
249
+
250
+ // 页面加载时获取日志
251
+ document.addEventListener('DOMContentLoaded', fetchLogs);
252
+ </script>
253
+ </body>
254
+ </html>
255
+ `;
256
+
257
+ // 路由定义
258
+ app.get('/', (c) => {
259
+ return c.html(indexHTML);
260
+ });
261
+
262
+ // API 路由 - 获取日志
263
+ app.get('/api/logs', (c) => {
264
+ try {
265
+ const lines = parseInt(c.req.query('lines') || '100');
266
+ const logs = getRecentLogs(lines);
267
+
268
+ return c.json({
269
+ success: true,
270
+ logs: logs,
271
+ count: logs.length,
272
+ timestamp: new Date().toISOString()
273
+ });
274
+ } catch (error) {
275
+ logError('获取日志API错误:', error);
276
+ return c.json({
277
+ success: false,
278
+ error: error.message
279
+ }, 500);
280
+ }
281
+ });
282
+
283
+ // API 路由 - 系统状态
284
+ app.get('/api/status', (c) => {
285
+ return c.json({
286
+ success: true,
287
+ status: 'running',
288
+ uptime: process.uptime(),
289
+ memory: process.memoryUsage(),
290
+ timestamp: new Date().toISOString(),
291
+ config: {
292
+ webideUrl: config.webideUrl,
293
+ schedulerInterval: config.schedulerInterval,
294
+ headless: config.browserOptions.headless
295
+ }
296
+ });
297
+ });
298
+
299
+ // 健康检查
300
+ app.get('/health', (c) => {
301
+ return c.json({ status: 'ok', timestamp: new Date().toISOString() });
302
+ });
303
+
304
+ // 启动服务器
305
+ const port = process.env.PORT || 7860;
306
+
307
+ async function startServer() {
308
+ try {
309
+ log(`启动 Web 服务器,端口: ${port}`);
310
+ log(`访问地址: http://localhost:${port}`);
311
+
312
+ serve({
313
+ fetch: app.fetch,
314
+ port: port,
315
+ });
316
+
317
+ log('Web 服务器启动成功');
318
+ } catch (error) {
319
+ logError('启动 Web 服务器失败:', error);
320
+ process.exit(1);
321
+ }
322
+ }
323
+
324
+ // 如果直接运行此文件,启动服务器
325
+ import { fileURLToPath } from 'url';
326
+ import path from 'path';
327
+
328
+ const __filename = fileURLToPath(import.meta.url);
329
+ const scriptPath = path.resolve(process.argv[1]);
330
+
331
+ if (path.resolve(__filename) === scriptPath) {
332
+ startServer();
333
+ }
334
+
335
+ export { app, startServer };