github-actions[bot] commited on
Commit
9e13dac
·
1 Parent(s): 844fe52

Update from GitHub Actions

Browse files
Files changed (8) hide show
  1. Dockerfile +13 -8
  2. config.js +7 -7
  3. package-lock.json +23 -0
  4. package.json +6 -1
  5. scheduler.js +13 -13
  6. start-services.js +118 -0
  7. utils/common-utils.js +78 -0
  8. web-server.js +335 -0
Dockerfile CHANGED
@@ -39,23 +39,28 @@ COPY cookies.json ./
39
  COPY execute-command.js ./
40
  COPY login.js ./
41
  COPY scheduler.js ./
 
 
 
42
  COPY utils/ ./utils/
43
 
44
- # 创建 screenshots 目录
45
- RUN mkdir -p screenshots
46
 
47
  # 设置非 root 用户(安全最佳实践)
48
  RUN addgroup -g 1001 -S nodejs && \
49
  adduser -S nodejs -u 1001
50
 
51
- # 更改文件所有权
52
- RUN chown -R nodejs:nodejs /app
 
 
53
 
54
  # 切换到非 root 用户
55
  USER nodejs
56
 
57
- # 暴露端口(如果需要的话,这里暂时不暴露)
58
- # EXPOSE 3000
59
 
60
- # 设置默认命令
61
- CMD ["npm", "run", "scheduler"]
 
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 test-web-server.js ./
45
  COPY utils/ ./utils/
46
 
47
+ # 创建 screenshots 和 logs 目录
48
+ RUN mkdir -p screenshots logs
49
 
50
  # 设置非 root 用户(安全最佳实践)
51
  RUN addgroup -g 1001 -S nodejs && \
52
  adduser -S nodejs -u 1001
53
 
54
+ # 更改文件所有权,包括新创建的目录
55
+ RUN chown -R nodejs:nodejs /app && \
56
+ chmod -R 755 /app/screenshots && \
57
+ chmod -R 755 /app/logs
58
 
59
  # 切换到非 root 用户
60
  USER nodejs
61
 
62
+ # 暴露端口 7860 用于 Web 服务器
63
+ EXPOSE 7860
64
 
65
+ # 设置默认命令 - 启动 Web 服务器
66
+ CMD ["npm", "start"]
config.js CHANGED
@@ -45,13 +45,13 @@ const config = {
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
  };
 
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
  };
package-lock.json CHANGED
@@ -9,10 +9,24 @@
9
  "version": "1.0.0",
10
  "license": "ISC",
11
  "dependencies": {
 
12
  "dotenv": "^16.5.0",
 
13
  "playwright": "^1.52.0"
14
  }
15
  },
 
 
 
 
 
 
 
 
 
 
 
 
16
  "node_modules/dotenv": {
17
  "version": "16.5.0",
18
  "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.5.0.tgz",
@@ -39,6 +53,15 @@
39
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
40
  }
41
  },
 
 
 
 
 
 
 
 
 
42
  "node_modules/playwright": {
43
  "version": "1.52.0",
44
  "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.52.0.tgz",
 
9
  "version": "1.0.0",
10
  "license": "ISC",
11
  "dependencies": {
12
+ "@hono/node-server": "^1.14.2",
13
  "dotenv": "^16.5.0",
14
+ "hono": "^4.7.10",
15
  "playwright": "^1.52.0"
16
  }
17
  },
18
+ "node_modules/@hono/node-server": {
19
+ "version": "1.14.2",
20
+ "resolved": "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.14.2.tgz",
21
+ "integrity": "sha512-GHjpOeHYbr9d1vkID2sNUYkl5IxumyhDrUJB7wBp7jvqYwPFt+oNKsAPBRcdSbV7kIrXhouLE199ks1QcK4r7A==",
22
+ "license": "MIT",
23
+ "engines": {
24
+ "node": ">=18.14.1"
25
+ },
26
+ "peerDependencies": {
27
+ "hono": "^4"
28
+ }
29
+ },
30
  "node_modules/dotenv": {
31
  "version": "16.5.0",
32
  "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.5.0.tgz",
 
53
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
54
  }
55
  },
56
+ "node_modules/hono": {
57
+ "version": "4.7.10",
58
+ "resolved": "https://registry.npmmirror.com/hono/-/hono-4.7.10.tgz",
59
+ "integrity": "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ==",
60
+ "license": "MIT",
61
+ "engines": {
62
+ "node": ">=16.9.0"
63
+ }
64
+ },
65
  "node_modules/playwright": {
66
  "version": "1.52.0",
67
  "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.52.0.tgz",
package.json CHANGED
@@ -6,14 +6,19 @@
6
  "scripts": {
7
  "login": "node login.js",
8
  "execute": "node execute-command.js",
9
- "scheduler": "node scheduler.js"
 
 
 
10
  },
11
  "keywords": ["playwright", "automation", "webide"],
12
  "author": "",
13
  "license": "ISC",
14
  "type": "module",
15
  "dependencies": {
 
16
  "dotenv": "^16.5.0",
 
17
  "playwright": "^1.52.0"
18
  }
19
  }
 
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": "",
16
  "license": "ISC",
17
  "type": "module",
18
  "dependencies": {
19
+ "@hono/node-server": "^1.13.7",
20
  "dotenv": "^16.5.0",
21
+ "hono": "^4.6.12",
22
  "playwright": "^1.52.0"
23
  }
24
  }
scheduler.js CHANGED
@@ -1,12 +1,12 @@
1
  import config from './config.js';
2
  import { fileURLToPath } from 'url';
3
  import path from 'path';
4
- import { checkCookieFile, getHumanReadableTimestamp } from './utils/common-utils.js';
5
  import { createBrowserSession, navigateToWebIDE, executeCommandFlow } from './utils/webide-utils.js';
6
 
7
  // 执行单次命令的函数
8
  async function executeCommandOnce(page) {
9
- console.log(`[${getHumanReadableTimestamp()}] 开始执行命令...`);
10
  return executeCommandFlow(page, 'scheduler');
11
  }
12
 
@@ -17,9 +17,9 @@ async function startScheduler() {
17
  return;
18
  }
19
 
20
- console.log(`[${getHumanReadableTimestamp()}] 启动调度器...`);
21
  const intervalSeconds = Math.round(config.schedulerInterval / 1000);
22
- console.log(`调度器将每${intervalSeconds}秒执行一次命令以防止编辑器休眠`);
23
 
24
  let browser;
25
  try {
@@ -37,43 +37,43 @@ async function startScheduler() {
37
  const intervalId = setInterval(async () => {
38
  try {
39
  // 重新导航到页面以确保页面活跃
40
- console.log(`[${getHumanReadableTimestamp()}] 重新导航到WebIDE页面...`);
41
  await navigateToWebIDE(page);
42
 
43
  // 执行命令
44
  await executeCommandOnce(page);
45
  } catch (error) {
46
- console.error(`[${getHumanReadableTimestamp()}] 定时任务执行失败:`, error);
47
  }
48
  }, config.schedulerInterval);
49
 
50
- console.log(`[${getHumanReadableTimestamp()}] 调度器已启动,将每${intervalSeconds}秒执行一次命令`);
51
- console.log('按 Ctrl+C 停止调度器');
52
 
53
  // 监听进程退出信号
54
  process.on('SIGINT', async () => {
55
- console.log(`\n[${getHumanReadableTimestamp()}] 收到停止信号,正在关闭调度器...`);
56
  clearInterval(intervalId);
57
  if (browser) {
58
  await browser.close();
59
  }
60
- console.log('调度器已停止,浏览器已关闭');
61
  process.exit(0);
62
  });
63
 
64
  // 保持进程运行
65
  process.on('SIGTERM', async () => {
66
- console.log(`\n[${getHumanReadableTimestamp()}] 收到终止信号,正在关闭调度器...`);
67
  clearInterval(intervalId);
68
  if (browser) {
69
  await browser.close();
70
  }
71
- console.log('调度器已停止,浏览器已关闭');
72
  process.exit(0);
73
  });
74
 
75
  } catch (error) {
76
- console.error(`[${getHumanReadableTimestamp()}] 调度器启动失败:`, error);
77
  if (browser) {
78
  await browser.close();
79
  }
 
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
 
 
17
  return;
18
  }
19
 
20
+ log(`[${getHumanReadableTimestamp()}] 启动调度器...`);
21
  const intervalSeconds = Math.round(config.schedulerInterval / 1000);
22
+ log(`调度器将每${intervalSeconds}秒执行一次命令以防止编辑器休眠`);
23
 
24
  let browser;
25
  try {
 
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
  }
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 服务器', 'web-server.js');
76
+
77
+ // 等待一下确保 Web 服务器启动
78
+ await new Promise(resolve => setTimeout(resolve, 2000));
79
+
80
+ // 启动调度器
81
+ const scheduler = startProcess('调度器', '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 };
utils/common-utils.js CHANGED
@@ -1,6 +1,84 @@
1
  import fs from 'fs';
2
  import path from 'path';
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  /**
5
  * 创建人类可读的时间戳
6
  * @returns {string} 格式化的时间戳 YYYY-MM-DD_HH-MM-SS
 
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
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 };