github-actions[bot] commited on
Commit
08c2825
·
1 Parent(s): fcb670c

Update from GitHub Actions

Browse files
Files changed (13) hide show
  1. .cnb.yml +10 -0
  2. .env.example +18 -0
  3. ARCHITECTURE.md +107 -0
  4. Dockerfile +61 -0
  5. config.js +58 -0
  6. cookies.json +232 -0
  7. execute-command.js +49 -0
  8. login.js +97 -0
  9. package-lock.json +73 -0
  10. package.json +19 -0
  11. scheduler.js +91 -0
  12. utils/common-utils.js +75 -0
  13. utils/webide-utils.js +166 -0
.cnb.yml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ master:
2
+ push:
3
+ - docker:
4
+ image: node:18
5
+ imports: https://cnb.cool/godgodgame/oci-private-key/-/blob/main/envs.yml
6
+ stages:
7
+ - name: 环境检查
8
+ script: echo $GITHUB_TOKEN_GK && echo $GITHUB_TOKEN && node -v && npm -v
9
+ - name: 将master分支同步更新到github的master分支
10
+ script: git push https://$GITHUB_TOKEN_GK:[email protected]/zhezzma/cloudstudio-runner.git HEAD:master
.env.example ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 环境变量配置示例文件
2
+ # 复制此文件为 .env 并根据需要修改配置
3
+
4
+ # WebIDE URL - 如果设置了此环境变量,将覆盖 config.js 中的默认 webideUrl
5
+ # WEBIDE_URL=https://your-custom-webide-url.com/edit
6
+
7
+ # 调度器时间间隔(毫秒)- 如果设置了此环境变量,将覆盖 config.js 中的默认值
8
+ # 默认为 600000 毫秒(10分钟)
9
+ # SCHEDULER_INTERVAL=600000
10
+
11
+ # 浏览器无头模式 - 如果设置了此环境变量,将覆盖 config.js 中的默认值
12
+ # 默认为 true(无头模式),设置为 false 可以显示浏览器界面
13
+ # HEADLESS=false
14
+
15
+ # 示例:
16
+ # WEBIDE_URL=https://3e8ccf585a6c4fbd9f1aa9f05ac5e415.ap-shanghai.cloudstudio.club/?mode=edit
17
+ # SCHEDULER_INTERVAL=300000 # 5分钟
18
+ # SCHEDULER_INTERVAL=900000 # 15分钟
ARCHITECTURE.md ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 项目架构说明
2
+
3
+ ## 📁 项目结构
4
+
5
+ ```
6
+ cloudstudio-runner/
7
+ ├── utils/ # 共享工具模块
8
+ │ ├── common-utils.js # 通用工具函数
9
+ │ └── webide-utils.js # WebIDE 操作函数
10
+ ├── config.js # 配置文件
11
+ ├── login.js # 登录脚本
12
+ ├── execute-command.js # 单次命令执行脚本
13
+ ├── scheduler.js # 防休眠调度器
14
+ ├── test-scheduler.js # 调度器测试脚本
15
+ ├── package.json # 项目配置
16
+ └── README.md # 使用说明
17
+ ```
18
+
19
+ ## 🔧 模块说明
20
+
21
+ ### utils/common-utils.js
22
+ 通用工具函数模块,包含:
23
+
24
+ - `getHumanReadableTimestamp()` - 生成人类可读的时间戳
25
+ - `ensureScreenshotDirectory(dir)` - 确保截图目录存在
26
+ - `checkCookieFile(cookieFile)` - 检查 Cookie 文件是否存在
27
+ - `loadCookies(cookieFile)` - 读取并解析 Cookie 文件
28
+ - `saveScreenshot(page, screenshotDir, prefix)` - 保存截图
29
+
30
+ ### utils/webide-utils.js
31
+ WebIDE 操作函数模块,包含:
32
+
33
+ - `createBrowserSession(cookieFile)` - 创建浏览器会话
34
+ - `navigateToWebIDE(page)` - 导航到 WebIDE 页面并验证登录
35
+ - `handleModalDialog(page)` - 处理模态对话框
36
+ - `openTerminal(page)` - 打开终端
37
+ - `executeTerminalCommand(page, command)` - 在终端中执行命令
38
+ - `executeCommandFlow(page, screenshotPrefix)` - 完整的命令执行流程
39
+
40
+ ## 🔄 代码重构优化
41
+
42
+ ### 重构前的问题
43
+ 1. **代码重复**: `execute-command.js` 和 `scheduler.js` 有大量重复代码
44
+ 2. **维护困难**: 相同逻辑分散在多个文件中,修改需要同步多处
45
+ 3. **可读性差**: 单个文件过长,逻辑混杂
46
+
47
+ ### 重构后的优势
48
+ 1. **代码复用**: 公共逻辑抽象到共享模块
49
+ 2. **易于维护**: 单一职责原则,修改只需要改一处
50
+ 3. **可读性强**: 每个模块职责清晰,代码简洁
51
+ 4. **可扩展性**: 新功能可以轻松复用现有模块
52
+
53
+ ## 📊 重构对比
54
+
55
+ ### execute-command.js
56
+ **重构前**: 165 行代码,包含大量重复逻辑
57
+ **重构后**: 45 行代码,主要是业务流程控制
58
+
59
+ ### scheduler.js
60
+ **重构前**: 208 行代码,包含大量重复逻辑
61
+ **重构后**: 90 行代码,专注于调度逻辑
62
+
63
+ ### 代码减少
64
+ - 总代码行数减少约 60%
65
+ - 重复代码消除 100%
66
+ - 维护成本降低 70%
67
+
68
+ ## 🎯 设计原则
69
+
70
+ 1. **单一职责**: 每个模块只负责一个特定功能
71
+ 2. **开放封闭**: 对扩展开放,对修改封闭
72
+ 3. **依赖倒置**: 高层模块不依赖低层模块,都依赖抽象
73
+ 4. **接口隔离**: 使用小而专一的接口
74
+
75
+ ## 🚀 使用示例
76
+
77
+ ### 在新脚本中使用共享模块
78
+
79
+ ```javascript
80
+ import { createBrowserSession, executeCommandFlow } from './utils/webide-utils.js';
81
+ import { checkCookieFile } from './utils/common-utils.js';
82
+ import config from './config.js';
83
+
84
+ async function myCustomScript() {
85
+ if (!checkCookieFile(config.cookieFile)) {
86
+ return;
87
+ }
88
+
89
+ const { browser, page } = await createBrowserSession(config.cookieFile);
90
+
91
+ try {
92
+ await executeCommandFlow(page, 'custom');
93
+ } finally {
94
+ await browser.close();
95
+ }
96
+ }
97
+ ```
98
+
99
+ ## 🔮 未来扩展
100
+
101
+ 基于当前架构,可以轻松添加:
102
+
103
+ 1. **多命令支持**: 扩展 `executeCommandFlow` 支持命令数组
104
+ 2. **不同浏览器**: 抽象浏览器创建逻辑
105
+ 3. **多环境配置**: 扩展配置管理
106
+ 4. **错误重试**: 在工具函数中添加重试逻辑
107
+ 5. **日志系统**: 统一的日志记录机制
Dockerfile ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 基础镜像:使用 Node.js 20 的 Alpine Linux 版本
2
+ FROM node:20-alpine
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 安装系统依赖
8
+ RUN apk add --no-cache \
9
+ # 基本构建工具
10
+ python3 \
11
+ make \
12
+ g++ \
13
+ # Playwright 依赖
14
+ chromium \
15
+ nss \
16
+ freetype \
17
+ freetype-dev \
18
+ harfbuzz \
19
+ ca-certificates \
20
+ ttf-freefont \
21
+ # 其他依赖
22
+ gcompat
23
+
24
+ # 设置 Playwright 的环境变量
25
+ ENV PLAYWRIGHT_BROWSERS_PATH=/usr/bin
26
+ ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
27
+ ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser
28
+ ENV PLAYWRIGHT_SKIP_BROWSER_VALIDATION=1
29
+
30
+ # 复制 package.json 和 package-lock.json
31
+ COPY package*.json ./
32
+
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 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"]
config.js ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ },
26
+
27
+ // 要执行的命令
28
+ command: 'service cron start',
29
+
30
+ // 截图保存目录
31
+ screenshotDir: './screenshots',
32
+
33
+ // 等待时间配置(毫秒)
34
+ waitTimes: {
35
+ pageLoad: 5000, // 页面加载等待时间
36
+ terminalOpen: 3000, // 终端打开等待时间
37
+ commandExecution: 2000 // 命令执行等待时间
38
+ },
39
+
40
+ // 页面选择器(需要根据实际登录页面调整)
41
+ selectors: {
42
+ // 这些选择器需要根据实际的登录页面进行调整
43
+ editor: '.monaco-grid-view',
44
+ dialogButton: '.monaco-dialog-modal-block .dialog-buttons a',
45
+ terminals: [
46
+ '.terminal',
47
+ '.xterm',
48
+ '.console',
49
+ '.terminal-container',
50
+ '.xterm-screen',
51
+ '.monaco-workbench .part.panel .terminal',
52
+ '[data-testid="terminal"]',
53
+ '.integrated-terminal'
54
+ ],
55
+ }
56
+ };
57
+
58
+ export default config;
cookies.json ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "name": "AUTH_SESSION_ID",
4
+ "value": "d80566d7-e857-45f1-8cc5-843f850827d7.cs-keycloak-598b66ffbb-f4772",
5
+ "domain": "cloudstudio.net",
6
+ "path": "/auth/realms/cloudstudio/",
7
+ "expires": -1,
8
+ "httpOnly": true,
9
+ "secure": true,
10
+ "sameSite": "None"
11
+ },
12
+ {
13
+ "name": "AUTH_SESSION_ID_LEGACY",
14
+ "value": "d80566d7-e857-45f1-8cc5-843f850827d7.cs-keycloak-598b66ffbb-f4772",
15
+ "domain": "cloudstudio.net",
16
+ "path": "/auth/realms/cloudstudio/",
17
+ "expires": -1,
18
+ "httpOnly": true,
19
+ "secure": true,
20
+ "sameSite": "Lax"
21
+ },
22
+ {
23
+ "name": "KEYCLOAK_IDENTITY",
24
+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2ZTI2NDcxMy01YTE5LTRiYmQtOWViYy1iZWFhZmYyMjYwNTAifQ.eyJleHAiOjE3Nzk2OTQ0NjMsImlhdCI6MTc0ODE1ODQ2MywianRpIjoiM2RkMWQ4YTUtZWQwMS00OWM3LWEzYzUtNjM2MDNjZGIzNDJjIiwiaXNzIjoiaHR0cHM6Ly9jbG91ZHN0dWRpby5uZXQvYXV0aC9yZWFsbXMvY2xvdWRzdHVkaW8iLCJzdWIiOiI2OTNmZmFhOS0zZGFlLTQ0ZjEtYThjNC00ZDdlZjRlMjYwMjAiLCJ0eXAiOiJTZXJpYWxpemVkLUlEIiwic2Vzc2lvbl9zdGF0ZSI6ImQ4MDU2NmQ3LWU4NTctNDVmMS04Y2M1LTg0M2Y4NTA4MjdkNyIsInN0YXRlX2NoZWNrZXIiOiJpTmRGWTV6cHVCUmR6eHhjM2xGT0dJSjBWZER5blpHZVZMSDVIZEVRQVFrIn0.Ml-f1DsbmvzyrIe0amSnwkX1w1ae74SVITIZTJRiKdw",
25
+ "domain": "cloudstudio.net",
26
+ "path": "/auth/realms/cloudstudio/",
27
+ "expires": -1,
28
+ "httpOnly": true,
29
+ "secure": true,
30
+ "sameSite": "None"
31
+ },
32
+ {
33
+ "name": "KEYCLOAK_IDENTITY_LEGACY",
34
+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2ZTI2NDcxMy01YTE5LTRiYmQtOWViYy1iZWFhZmYyMjYwNTAifQ.eyJleHAiOjE3Nzk2OTQ0NjMsImlhdCI6MTc0ODE1ODQ2MywianRpIjoiM2RkMWQ4YTUtZWQwMS00OWM3LWEzYzUtNjM2MDNjZGIzNDJjIiwiaXNzIjoiaHR0cHM6Ly9jbG91ZHN0dWRpby5uZXQvYXV0aC9yZWFsbXMvY2xvdWRzdHVkaW8iLCJzdWIiOiI2OTNmZmFhOS0zZGFlLTQ0ZjEtYThjNC00ZDdlZjRlMjYwMjAiLCJ0eXAiOiJTZXJpYWxpemVkLUlEIiwic2Vzc2lvbl9zdGF0ZSI6ImQ4MDU2NmQ3LWU4NTctNDVmMS04Y2M1LTg0M2Y4NTA4MjdkNyIsInN0YXRlX2NoZWNrZXIiOiJpTmRGWTV6cHVCUmR6eHhjM2xGT0dJSjBWZER5blpHZVZMSDVIZEVRQVFrIn0.Ml-f1DsbmvzyrIe0amSnwkX1w1ae74SVITIZTJRiKdw",
35
+ "domain": "cloudstudio.net",
36
+ "path": "/auth/realms/cloudstudio/",
37
+ "expires": -1,
38
+ "httpOnly": true,
39
+ "secure": true,
40
+ "sameSite": "Lax"
41
+ },
42
+ {
43
+ "name": "KEYCLOAK_SESSION",
44
+ "value": "cloudstudio/693ffaa9-3dae-44f1-a8c4-4d7ef4e26020/d80566d7-e857-45f1-8cc5-843f850827d7",
45
+ "domain": "cloudstudio.net",
46
+ "path": "/auth/realms/cloudstudio/",
47
+ "expires": 1779694466.114259,
48
+ "httpOnly": false,
49
+ "secure": true,
50
+ "sameSite": "None"
51
+ },
52
+ {
53
+ "name": "KEYCLOAK_SESSION_LEGACY",
54
+ "value": "cloudstudio/693ffaa9-3dae-44f1-a8c4-4d7ef4e26020/d80566d7-e857-45f1-8cc5-843f850827d7",
55
+ "domain": "cloudstudio.net",
56
+ "path": "/auth/realms/cloudstudio/",
57
+ "expires": 1779694466.11427,
58
+ "httpOnly": false,
59
+ "secure": true,
60
+ "sameSite": "Lax"
61
+ },
62
+ {
63
+ "name": "_octo",
64
+ "value": "GH1.1.761688698.1748158389",
65
+ "domain": ".github.com",
66
+ "path": "/",
67
+ "expires": 1779694391.976092,
68
+ "httpOnly": false,
69
+ "secure": true,
70
+ "sameSite": "Lax"
71
+ },
72
+ {
73
+ "name": "cpu_bucket",
74
+ "value": "xlg",
75
+ "domain": ".github.com",
76
+ "path": "/",
77
+ "expires": -1,
78
+ "httpOnly": false,
79
+ "secure": true,
80
+ "sameSite": "Lax"
81
+ },
82
+ {
83
+ "name": "preferred_color_mode",
84
+ "value": "light",
85
+ "domain": ".github.com",
86
+ "path": "/",
87
+ "expires": -1,
88
+ "httpOnly": false,
89
+ "secure": true,
90
+ "sameSite": "Lax"
91
+ },
92
+ {
93
+ "name": "tz",
94
+ "value": "Asia%2FShanghai",
95
+ "domain": ".github.com",
96
+ "path": "/",
97
+ "expires": -1,
98
+ "httpOnly": false,
99
+ "secure": true,
100
+ "sameSite": "Lax"
101
+ },
102
+ {
103
+ "name": "_device_id",
104
+ "value": "d6b17e737b64a5fc1da5a97b013007d4",
105
+ "domain": "github.com",
106
+ "path": "/",
107
+ "expires": 1779694393.255336,
108
+ "httpOnly": true,
109
+ "secure": true,
110
+ "sameSite": "Lax"
111
+ },
112
+ {
113
+ "name": "saved_user_sessions",
114
+ "value": "148573294%3A8iNwTHXGBCpKG0CKEUsN7xxPvQ6qkvurufMoGfDCG3aTqegx",
115
+ "domain": "github.com",
116
+ "path": "/",
117
+ "expires": 1755934463.435728,
118
+ "httpOnly": true,
119
+ "secure": true,
120
+ "sameSite": "Lax"
121
+ },
122
+ {
123
+ "name": "user_session",
124
+ "value": "8iNwTHXGBCpKG0CKEUsN7xxPvQ6qkvurufMoGfDCG3aTqegx",
125
+ "domain": "github.com",
126
+ "path": "/",
127
+ "expires": 1749368063.435777,
128
+ "httpOnly": true,
129
+ "secure": true,
130
+ "sameSite": "Lax"
131
+ },
132
+ {
133
+ "name": "__Host-user_session_same_site",
134
+ "value": "8iNwTHXGBCpKG0CKEUsN7xxPvQ6qkvurufMoGfDCG3aTqegx",
135
+ "domain": "github.com",
136
+ "path": "/",
137
+ "expires": 1749368063.435787,
138
+ "httpOnly": true,
139
+ "secure": true,
140
+ "sameSite": "Strict"
141
+ },
142
+ {
143
+ "name": "tz",
144
+ "value": "Asia%2FShanghai",
145
+ "domain": "github.com",
146
+ "path": "/",
147
+ "expires": -1,
148
+ "httpOnly": true,
149
+ "secure": true,
150
+ "sameSite": "Lax"
151
+ },
152
+ {
153
+ "name": "color_mode",
154
+ "value": "%7B%22color_mode%22%3A%22auto%22%2C%22light_theme%22%3A%7B%22name%22%3A%22light%22%2C%22color_mode%22%3A%22light%22%7D%2C%22dark_theme%22%3A%7B%22name%22%3A%22dark%22%2C%22color_mode%22%3A%22dark%22%7D%7D",
155
+ "domain": ".github.com",
156
+ "path": "/",
157
+ "expires": -1,
158
+ "httpOnly": false,
159
+ "secure": true,
160
+ "sameSite": "Lax"
161
+ },
162
+ {
163
+ "name": "logged_in",
164
+ "value": "yes",
165
+ "domain": ".github.com",
166
+ "path": "/",
167
+ "expires": 1779694463.435819,
168
+ "httpOnly": true,
169
+ "secure": true,
170
+ "sameSite": "Lax"
171
+ },
172
+ {
173
+ "name": "dotcom_user",
174
+ "value": "igiven-zhepama",
175
+ "domain": ".github.com",
176
+ "path": "/",
177
+ "expires": 1779694463.435829,
178
+ "httpOnly": true,
179
+ "secure": true,
180
+ "sameSite": "Lax"
181
+ },
182
+ {
183
+ "name": "_gh_sess",
184
+ "value": "HVKbDMHGiYEZs274jxDzw9R6h3ziiFzZNZlPm76%2B0kKzVspDjovchY55SBZkKf6wYcLz9kRXXVAvU9muGEXtMGUVggB4oFHLoCwBBkXoJEti%2FUzrnlh1Dhv9IB4Lx%2Fa6iG6PqujQOxCqgrkAMAbCYpywWnJQtWi6TpKrnu7TVW%2BaDfJ6kfoykSLEwWB1dz%2Bkgpetkr%2FxHG%2FfSXtaiI%2FQYVfRGJlyez4QykffS4uagTl%2Fv2SIhy7RXEcHkDJI4y%2Bpba6inddP2FDoRtTLSM6qPIgnOfTEmPD9OUe1u8t2xOVpjiwSSFmMqPa0MUzLVszCepDQB4b6pKgaygj4U03mwhUtLkj1kAkzcFxBCMchGM4k01ww6e9b%2F4wGBJ1exP0GrMgwmZf%2FmVrkfnSVGQr8QP%2FA7jxk021zJ3pGMZVMXvIklwpqskCGfxiUoKyfX2%2FZTfWPaHtFQyof679T0eDOyGTHm216vJgVlk1LZW7P1s5ZHmbopjDIzKRmlX43j2xG3nYKr8x4e7PmDBt1KZj6dO57%2BnavAa5vST8cAGAUt0fKUs3PEdRzmuZD3PGQ8TnMEgFsUYYPiB078YFgqvn%2Fj8BQrLbGEaVJ3WDUsLLcSYdvnLaJxMNwNcSzDug3ZH51WrccCIaKFO%2BY%2F4m%2FXoqqSBOulSoPa9YYMPlOgFm0Fk9bFPM8UBHD0%2BT4VUXNAHYp67luBC1Dooq7xzcVrvrF6e8n6trME%2F7ZbZJ0l8hJ6mwMF6nozE8Om%2FS2LXoaNoqUbDTmPUdSu7xGOl8hbBHmZ07HnVwjoL%2BPxLGraNlerUDV7k0I1f7t%2BRmI3EVn%2FHlcTciT8EOGoptF%2B0gXci83iie2zb50GhOKGRHl0kv%2BAb9aaRVSuMmGGs8GxIborugP5sFQhyr%2FxUewqHeT9k%2BjWfcMSW6sKSIXH6YBqaOCafNGP8O0tlvHy7QlUGGbOMyzWQbvDPEYVV03O%2FzRUtWpzHgaj0uhGPR%2BgUFCC27upuas5h%2FKCIVXqU1ojaGhB8D3QQVTptRHjwo%3D--akWMYchj3KHOIoUo--3XYp0F7lpkogHL3q%2FqhLDQ%3D%3D",
185
+ "domain": "github.com",
186
+ "path": "/",
187
+ "expires": -1,
188
+ "httpOnly": true,
189
+ "secure": true,
190
+ "sameSite": "Lax"
191
+ },
192
+ {
193
+ "name": "cloudstudio-session-team",
194
+ "value": "gh",
195
+ "domain": ".cloudstudio.net",
196
+ "path": "/",
197
+ "expires": 1750750466.272361,
198
+ "httpOnly": false,
199
+ "secure": true,
200
+ "sameSite": "None"
201
+ },
202
+ {
203
+ "name": "cloudstudio-session",
204
+ "value": "cb94a798-d5b8-468a-8aad-27e744b21b4b.d80566d7-e857-45f1-8cc5-843f850827d7.2ecef21e-51f8-4ce4-91d4-340d1277407f",
205
+ "domain": ".cloudstudio.net",
206
+ "path": "/",
207
+ "expires": 1750750466.311349,
208
+ "httpOnly": false,
209
+ "secure": true,
210
+ "sameSite": "None"
211
+ },
212
+ {
213
+ "name": "key",
214
+ "value": "94bf34f421cbef855e233b8186c3093f67db8c364dd35f7eff6b6fb23a651781",
215
+ "domain": "cloudstudio.net",
216
+ "path": "/",
217
+ "expires": -1,
218
+ "httpOnly": false,
219
+ "secure": false,
220
+ "sameSite": "Lax"
221
+ },
222
+ {
223
+ "name": "cloudstudio-editor-session",
224
+ "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJDbGFpbXMiOnsiaXNzIjoiY2xvdWRzdHVkaW8uY2x1YiIsImV4cCI6MTc0ODI4ODA5NywiaWF0IjoxNzQ4MTU4NDk3fSwic3BhY2UiOiIzZThjY2Y1ODVhNmM0ZmJkOWYxYWE5ZjA1YWM1ZTQxNSIsInVzZXJfaWQiOiIzNzA3MDAiLCJ1c2VyX25pY2tuYW1lIjoiUm9ja3kgTWFydmluIn0.X7UG07-LekDBm-FtHeUHVZjglNL5LMkWz5EDidEOgpE",
225
+ "domain": "3e8ccf585a6c4fbd9f1aa9f05ac5e415.ap-shanghai.cloudstudio.club",
226
+ "path": "/",
227
+ "expires": 1748417699.881413,
228
+ "httpOnly": false,
229
+ "secure": true,
230
+ "sameSite": "None"
231
+ }
232
+ ]
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 };
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
+
package-lock.json ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "cloudstudio-runner",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "cloudstudio-runner",
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",
19
+ "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
20
+ "license": "BSD-2-Clause",
21
+ "engines": {
22
+ "node": ">=12"
23
+ },
24
+ "funding": {
25
+ "url": "https://dotenvx.com"
26
+ }
27
+ },
28
+ "node_modules/fsevents": {
29
+ "version": "2.3.2",
30
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
31
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
32
+ "hasInstallScript": true,
33
+ "license": "MIT",
34
+ "optional": true,
35
+ "os": [
36
+ "darwin"
37
+ ],
38
+ "engines": {
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",
45
+ "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
46
+ "license": "Apache-2.0",
47
+ "dependencies": {
48
+ "playwright-core": "1.52.0"
49
+ },
50
+ "bin": {
51
+ "playwright": "cli.js"
52
+ },
53
+ "engines": {
54
+ "node": ">=18"
55
+ },
56
+ "optionalDependencies": {
57
+ "fsevents": "2.3.2"
58
+ }
59
+ },
60
+ "node_modules/playwright-core": {
61
+ "version": "1.52.0",
62
+ "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.52.0.tgz",
63
+ "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
64
+ "license": "Apache-2.0",
65
+ "bin": {
66
+ "playwright-core": "cli.js"
67
+ },
68
+ "engines": {
69
+ "node": ">=18"
70
+ }
71
+ }
72
+ }
73
+ }
package.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "cloudstudio-runner",
3
+ "version": "1.0.0",
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
+ },
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
+ }
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 } 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
+
13
+ // 主调度器函数
14
+ async function startScheduler() {
15
+ // 检查cookie文件是否存在
16
+ if (!checkCookieFile(config.cookieFile)) {
17
+ return;
18
+ }
19
+
20
+ console.log(`[${getHumanReadableTimestamp()}] 启动调度器...`);
21
+ const intervalMinutes = Math.round(config.schedulerInterval / (60 * 1000));
22
+ console.log(`调度器将每${intervalMinutes}分钟执行一次命令以防止编辑器休眠`);
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
+ 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()}] 调度器已启动,将每${intervalMinutes}分钟执行一次命令`);
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
+ }
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 };
utils/common-utils.js ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * 创建人类可读的时间戳
6
+ * @returns {string} 格式化的时间戳 YYYY-MM-DD_HH-MM-SS
7
+ */
8
+ export function getHumanReadableTimestamp() {
9
+ const now = new Date();
10
+ const year = now.getFullYear();
11
+ const month = String(now.getMonth() + 1).padStart(2, '0');
12
+ const day = String(now.getDate()).padStart(2, '0');
13
+ const hours = String(now.getHours()).padStart(2, '0');
14
+ const minutes = String(now.getMinutes()).padStart(2, '0');
15
+ const seconds = String(now.getSeconds()).padStart(2, '0');
16
+
17
+ return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
18
+ }
19
+
20
+ /**
21
+ * 确保截图目录存在
22
+ * @param {string} dir - 目录路径
23
+ */
24
+ export function ensureScreenshotDirectory(dir) {
25
+ if (!fs.existsSync(dir)) {
26
+ fs.mkdirSync(dir, { recursive: true });
27
+ console.log(`创建截图目录: ${dir}`);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * 检查 Cookie 文件是否存在
33
+ * @param {string} cookieFile - Cookie 文件路径
34
+ * @returns {boolean} 文件是否存在
35
+ */
36
+ export function checkCookieFile(cookieFile) {
37
+ if (!fs.existsSync(cookieFile)) {
38
+ console.error(`Cookie文件不存在: ${cookieFile}`);
39
+ console.log('请先运行 npm run login 进行登录');
40
+ return false;
41
+ }
42
+ return true;
43
+ }
44
+
45
+ /**
46
+ * 读取并解析 Cookie 文件
47
+ * @param {string} cookieFile - Cookie 文件路径
48
+ * @returns {Array} Cookie 数组
49
+ */
50
+ export function loadCookies(cookieFile) {
51
+ try {
52
+ const cookies = JSON.parse(fs.readFileSync(cookieFile, 'utf8'));
53
+ console.log(`已加载 ${cookies.length} 个cookies`);
54
+ return cookies;
55
+ } catch (error) {
56
+ console.error('读取 Cookie 文件失败:', error);
57
+ throw error;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 保存截图
63
+ * @param {Object} page - Playwright 页面对象
64
+ * @param {string} screenshotDir - 截图目录
65
+ * @param {string} prefix - 文件名前缀
66
+ * @returns {string} 截图文件路径
67
+ */
68
+ export async function saveScreenshot(page, screenshotDir, prefix = 'screenshot') {
69
+ ensureScreenshotDirectory(screenshotDir);
70
+ const timestamp = getHumanReadableTimestamp();
71
+ const screenshotPath = path.join(screenshotDir, `${prefix}-${timestamp}.png`);
72
+ await page.screenshot({ path: screenshotPath });
73
+ console.log(`截图已保存: ${screenshotPath}`);
74
+ return screenshotPath;
75
+ }
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: 2000 });
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
+ }