@aiscene/aiserver 1.2.3 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +26 -5
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +10 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/core/autobots-client.d.ts +37 -0
- package/dist/core/autobots-client.d.ts.map +1 -0
- package/dist/core/autobots-client.js +132 -0
- package/dist/core/autobots-client.js.map +1 -0
- package/dist/debug/websocket-server.d.ts +6 -0
- package/dist/debug/websocket-server.d.ts.map +1 -1
- package/dist/debug/websocket-server.js +128 -59
- package/dist/debug/websocket-server.js.map +1 -1
- package/dist/executor/android-executor.d.ts.map +1 -1
- package/dist/executor/android-executor.js +45 -4
- package/dist/executor/android-executor.js.map +1 -1
- package/dist/task/scheduler.d.ts.map +1 -1
- package/dist/task/scheduler.js +13 -1
- package/dist/task/scheduler.js.map +1 -1
- package/dist/web/debug-api.d.ts +9 -0
- package/dist/web/debug-api.d.ts.map +1 -0
- package/dist/web/debug-api.js +368 -0
- package/dist/web/debug-api.js.map +1 -0
- package/dist/web/debug-page.d.ts +7 -0
- package/dist/web/debug-page.d.ts.map +1 -0
- package/dist/web/debug-page.js +1591 -0
- package/dist/web/debug-page.js.map +1 -0
- package/dist/web/debug.html +1069 -0
- package/dist/web/dist/debug.html +1069 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +456 -86
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
package/dist/web/server.js
CHANGED
|
@@ -1,11 +1,211 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import axios from 'axios';
|
|
3
4
|
import { createLogger } from '../core/logger.js';
|
|
4
5
|
import { deviceRepo } from '../storage/repositories/device-repo.js';
|
|
5
6
|
import { taskRepo } from '../storage/repositories/task-repo.js';
|
|
6
7
|
import { debugLogRepo } from '../storage/repositories/debug-log-repo.js';
|
|
7
8
|
import { executionLogRepo } from '../storage/repositories/execution-log-repo.js';
|
|
9
|
+
import { getDebugPageHtml } from './debug-page.js';
|
|
10
|
+
import { getConfig } from '../config/index.js';
|
|
11
|
+
import { AutobotsClient } from '../core/autobots-client.js';
|
|
8
12
|
const logger = createLogger('WebServer');
|
|
13
|
+
/**
|
|
14
|
+
* 构建脚本生成的上下文信息
|
|
15
|
+
*/
|
|
16
|
+
function buildScriptContext(runMode, platform, url, deviceId, packageName) {
|
|
17
|
+
const context = [];
|
|
18
|
+
context.push('请根据用户的需求生成自动化测试脚本。');
|
|
19
|
+
context.push('');
|
|
20
|
+
if (runMode === 'browser') {
|
|
21
|
+
context.push('运行环境:浏览器模式');
|
|
22
|
+
if (url) {
|
|
23
|
+
context.push(`测试URL:${url}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
context.push('运行环境:真机模式');
|
|
28
|
+
context.push(`平台:${platform === 'ios' ? 'iOS' : 'Android'}`);
|
|
29
|
+
if (deviceId) {
|
|
30
|
+
context.push(`设备ID:${deviceId}`);
|
|
31
|
+
}
|
|
32
|
+
if (packageName) {
|
|
33
|
+
context.push(`应用包名:${packageName}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
context.push('');
|
|
37
|
+
context.push('请生成符合以下要求的脚本:');
|
|
38
|
+
context.push('1. 使用 @midscene/web(浏览器模式)或 @midscene/android/@midscene/ios(真机模式)');
|
|
39
|
+
context.push('2. 脚本应该是可执行的JavaScript代码');
|
|
40
|
+
context.push('3. 使用aiAction方法执行自然语言指令');
|
|
41
|
+
context.push('4. 包含必要的初始化和清理代码');
|
|
42
|
+
return context.join('\n');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 清理生成的脚本,移除markdown代码块标记
|
|
46
|
+
*/
|
|
47
|
+
function cleanGeneratedScript(script) {
|
|
48
|
+
// 移除 markdown 代码块标记
|
|
49
|
+
let cleaned = script;
|
|
50
|
+
// 移除 ```javascript, ```js, ``` 等代码块开始标记
|
|
51
|
+
cleaned = cleaned.replace(/```javascript\s*/gi, '');
|
|
52
|
+
cleaned = cleaned.replace(/```js\s*/gi, '');
|
|
53
|
+
cleaned = cleaned.replace(/```typescript\s*/gi, '');
|
|
54
|
+
cleaned = cleaned.replace(/```ts\s*/gi, '');
|
|
55
|
+
cleaned = cleaned.replace(/```\s*/gi, '');
|
|
56
|
+
// 移除可能的行内代码标记
|
|
57
|
+
cleaned = cleaned.replace(/`/g, '');
|
|
58
|
+
// 移除前后空白
|
|
59
|
+
cleaned = cleaned.trim();
|
|
60
|
+
return cleaned;
|
|
61
|
+
}
|
|
62
|
+
function generateScriptFromNaturalLanguage(options) {
|
|
63
|
+
const { naturalLanguage, runMode, platform, url, deviceId, packageName } = options;
|
|
64
|
+
// 解析自然语言,提取关键动作
|
|
65
|
+
const nl = naturalLanguage.toLowerCase();
|
|
66
|
+
const lines = [];
|
|
67
|
+
// 生成头部注释
|
|
68
|
+
lines.push('// ============================================');
|
|
69
|
+
lines.push('// AI自动生成的自动化测试脚本');
|
|
70
|
+
lines.push('// ============================================');
|
|
71
|
+
lines.push('// 生成时间: ' + new Date().toLocaleString('zh-CN'));
|
|
72
|
+
lines.push('// 运行模式: ' + (runMode === 'browser' ? '浏览器' : '真机'));
|
|
73
|
+
lines.push('// 平台: ' + (platform === 'android' ? 'Android' : 'iOS'));
|
|
74
|
+
if (url)
|
|
75
|
+
lines.push('// 测试URL: ' + url);
|
|
76
|
+
if (deviceId)
|
|
77
|
+
lines.push('// 设备ID: ' + deviceId);
|
|
78
|
+
if (packageName)
|
|
79
|
+
lines.push('// 包名: ' + packageName);
|
|
80
|
+
lines.push('// ============================================');
|
|
81
|
+
lines.push('');
|
|
82
|
+
if (runMode === 'browser') {
|
|
83
|
+
// 浏览器模式脚本
|
|
84
|
+
lines.push('// Midscene Web 自动化测试脚本');
|
|
85
|
+
lines.push("const { Midscene } = require('@midscene/web');");
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push('async function main() {');
|
|
88
|
+
lines.push(' const midscene = new Midscene();');
|
|
89
|
+
lines.push('');
|
|
90
|
+
// 解析自然语言生成对应的Web操作
|
|
91
|
+
if (nl.includes('点击') || nl.includes('tap') || nl.includes('click')) {
|
|
92
|
+
const match = naturalLanguage.match(/点击[\s\S]*?['"]([^'"]+)['"][\s\S]*?/i) ||
|
|
93
|
+
naturalLanguage.match(/tap[\s\S]*?['"]([^'"]+)['"]/i) ||
|
|
94
|
+
naturalLanguage.match(/click[\s\S]*?['"]([^'"]+)['"]/i);
|
|
95
|
+
if (match) {
|
|
96
|
+
lines.push(` // 点击操作`);
|
|
97
|
+
lines.push(` await midscene.aiAction('点击 "${match[1]}"');`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
lines.push(` // 点击操作`);
|
|
101
|
+
lines.push(` await midscene.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (nl.includes('输入') || nl.includes('type') || nl.includes('fill')) {
|
|
105
|
+
const match = naturalLanguage.match(/输入['"]([^'"]+)['"][\s\S]*?/i) ||
|
|
106
|
+
naturalLanguage.match(/type[\s\S]*?['"]([^'"]+)['"]/i);
|
|
107
|
+
if (match) {
|
|
108
|
+
lines.push(` // 输入操作`);
|
|
109
|
+
lines.push(` await midscene.aiAction('输入 "${match[1]}"');`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
lines.push(` // 输入操作`);
|
|
113
|
+
lines.push(` await midscene.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (nl.includes('等待') || nl.includes('wait')) {
|
|
117
|
+
lines.push(` // 等待操作`);
|
|
118
|
+
lines.push(` await midscene.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
|
|
119
|
+
}
|
|
120
|
+
else if (nl.includes('滑动') || nl.includes('scroll') || nl.includes('滚动')) {
|
|
121
|
+
lines.push(` // 滚动操作`);
|
|
122
|
+
lines.push(` await midscene.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// 默认使用AI动作
|
|
126
|
+
lines.push(' // 自然语言指令:');
|
|
127
|
+
lines.push(' // ' + naturalLanguage);
|
|
128
|
+
lines.push(` await midscene.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
|
|
129
|
+
}
|
|
130
|
+
lines.push(' ');
|
|
131
|
+
lines.push(' // 可添加更多操作步骤...');
|
|
132
|
+
lines.push(' ');
|
|
133
|
+
lines.push(' await midscene.sleep(2000); // 等待2秒查看效果');
|
|
134
|
+
lines.push(' await midscene.quit();');
|
|
135
|
+
lines.push('}');
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push('main().catch(console.error);');
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// 真机模式脚本
|
|
141
|
+
const isAndroid = platform === 'android';
|
|
142
|
+
const agentLib = isAndroid ? '@midscene/android' : '@midscene/ios';
|
|
143
|
+
const AgentClass = isAndroid ? 'AndroidAgent' : 'IOSAgent';
|
|
144
|
+
lines.push(`// Midscene ${isAndroid ? 'Android' : 'iOS'} 自动化测试脚本`);
|
|
145
|
+
lines.push(`const { ${AgentClass} } = require('${agentLib}');`);
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push('async function main() {');
|
|
148
|
+
lines.push(` const agent = new ${AgentClass}();`);
|
|
149
|
+
if (deviceId)
|
|
150
|
+
lines.push(` await agent.setDevice('${deviceId}');`);
|
|
151
|
+
if (packageName)
|
|
152
|
+
lines.push(` await agent.setPackage('${packageName}');`);
|
|
153
|
+
lines.push('');
|
|
154
|
+
// 解析自然语言生成对应的移动端操作
|
|
155
|
+
if (nl.includes('点击') || nl.includes('tap') || nl.includes('click')) {
|
|
156
|
+
const match = naturalLanguage.match(/点击[\s\S]*?['"]([^'"]+)['"][\s\S]*?/i) ||
|
|
157
|
+
naturalLanguage.match(/tap[\s\S]*?['"]([^'"]+)['"]/i);
|
|
158
|
+
if (match) {
|
|
159
|
+
lines.push(` // 点击操作`);
|
|
160
|
+
lines.push(` await agent.aiAction('点击 "${match[1]}"');`);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
lines.push(` // 点击操作`);
|
|
164
|
+
lines.push(` await agent.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (nl.includes('输入') || nl.includes('type') || nl.includes('输入文本')) {
|
|
168
|
+
const match = naturalLanguage.match(/输入['"]([^'"]+)['"][\s\S]*?/i) ||
|
|
169
|
+
naturalLanguage.match(/type[\s\S]*?['"]([^'"]+)['"]/i) ||
|
|
170
|
+
naturalLanguage.match(/输入文本['"]([^'"]+)['"]/i);
|
|
171
|
+
if (match) {
|
|
172
|
+
lines.push(` // 输入操作`);
|
|
173
|
+
lines.push(` await agent.aiAction('输入 "${match[1]}"');`);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
lines.push(` // 输入操作`);
|
|
177
|
+
lines.push(` await agent.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (nl.includes('滑动') || nl.includes('swipe') || nl.includes('滚动')) {
|
|
181
|
+
lines.push(` // 滑动操作`);
|
|
182
|
+
lines.push(` await agent.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
|
|
183
|
+
}
|
|
184
|
+
else if (nl.includes('启动') || nl.includes('start') || nl.includes('launch')) {
|
|
185
|
+
lines.push(` // 启动应用`);
|
|
186
|
+
lines.push(` await agent.aiAction('启动应用');`);
|
|
187
|
+
}
|
|
188
|
+
else if (nl.includes('截图') || nl.includes('screenshot')) {
|
|
189
|
+
lines.push(` // 截图`);
|
|
190
|
+
lines.push(` await agent.screenshot();`);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// 默认使用AI动作
|
|
194
|
+
lines.push(' // 自然语言指令:');
|
|
195
|
+
lines.push(' // ' + naturalLanguage);
|
|
196
|
+
lines.push(` await agent.aiAction('${naturalLanguage.replace(/'/g, "\\'")}');`);
|
|
197
|
+
}
|
|
198
|
+
lines.push(' ');
|
|
199
|
+
lines.push(' // 可添加更多操作步骤...');
|
|
200
|
+
lines.push(' ');
|
|
201
|
+
lines.push(' await agent.sleep(2000); // 等待2秒查看效果');
|
|
202
|
+
lines.push(' await agent.quit();');
|
|
203
|
+
lines.push('}');
|
|
204
|
+
lines.push('');
|
|
205
|
+
lines.push('main().catch(console.error);');
|
|
206
|
+
}
|
|
207
|
+
return lines.join('\n');
|
|
208
|
+
}
|
|
9
209
|
export class WebServer {
|
|
10
210
|
app;
|
|
11
211
|
config;
|
|
@@ -95,6 +295,162 @@ export class WebServer {
|
|
|
95
295
|
const limit = parseInt(req.query.limit) || 1000;
|
|
96
296
|
res.json(debugLogRepo.getLogsBySession(req.params.sessionId, limit));
|
|
97
297
|
});
|
|
298
|
+
// ===== Test Case Management (代理后端API) =====
|
|
299
|
+
const BACKEND_API = 'http://nethp-test.jd.com';
|
|
300
|
+
// 获取文件夹配置树
|
|
301
|
+
api.get('/testcases/folder-tree', async (_, res) => {
|
|
302
|
+
try {
|
|
303
|
+
const response = await axios.get(`${BACKEND_API}/rest/ai-validator/folder-config-tree`, { timeout: 10000 });
|
|
304
|
+
res.json({ success: true, data: response.data });
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
logger.warn(`获取文件夹树失败: ${error.message}`);
|
|
308
|
+
res.json({ success: false, data: [], message: error.message });
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
// 获取文件夹下的用例列表
|
|
312
|
+
api.get('/testcases/folder/:folderId', async (req, res) => {
|
|
313
|
+
try {
|
|
314
|
+
const response = await axios.get(`${BACKEND_API}/rest/ai-validator/folder-config/${req.params.folderId}`, { timeout: 10000 });
|
|
315
|
+
res.json({ success: true, data: response.data });
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
logger.warn(`获取用例列表失败: ${error.message}`);
|
|
319
|
+
res.json({ success: false, data: [], message: error.message });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
// 搜索用例
|
|
323
|
+
api.get('/testcases/search', async (req, res) => {
|
|
324
|
+
try {
|
|
325
|
+
const { configName, updatedBy } = req.query;
|
|
326
|
+
const response = await axios.get(`${BACKEND_API}/rest/ai-validator/configs/search/with-folder`, {
|
|
327
|
+
params: { configName, updatedBy },
|
|
328
|
+
timeout: 10000,
|
|
329
|
+
});
|
|
330
|
+
res.json({ success: true, data: response.data });
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
logger.warn(`搜索用例失败: ${error.message}`);
|
|
334
|
+
res.json({ success: false, data: [], message: error.message });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
// 保存/更新测试用例
|
|
338
|
+
api.post('/testcases', async (req, res) => {
|
|
339
|
+
try {
|
|
340
|
+
const response = await axios.post(`${BACKEND_API}/rest/ai-validator/configs`, req.body, { timeout: 10000 });
|
|
341
|
+
res.json({ success: true, data: response.data, message: '用例保存成功' });
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
const msg = axios.isAxiosError(error) ? `HTTP ${error.response?.status}: ${JSON.stringify(error.response?.data)}` : error.message;
|
|
345
|
+
logger.warn(`保存用例失败: ${msg}`);
|
|
346
|
+
res.json({ success: false, message: msg });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
api.put('/testcases/:id', async (req, res) => {
|
|
350
|
+
try {
|
|
351
|
+
const response = await axios.put(`${BACKEND_API}/rest/ai-validator/configs/${req.params.id}`, req.body, { timeout: 10000 });
|
|
352
|
+
res.json({ success: true, data: response.data, message: '用例更新成功' });
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
const msg = axios.isAxiosError(error) ? `HTTP ${error.response?.status}: ${JSON.stringify(error.response?.data)}` : error.message;
|
|
356
|
+
logger.warn(`更新用例失败: ${msg}`);
|
|
357
|
+
res.json({ success: false, message: msg });
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
// 获取在线设备列表(用于调试页面选择设备)
|
|
361
|
+
api.get('/debug/devices', (_, res) => {
|
|
362
|
+
res.json(deviceRepo.getOnline());
|
|
363
|
+
});
|
|
364
|
+
// 调试执行脚本 (通过WebSocket协议的HTTP触发)
|
|
365
|
+
api.post('/debug/execute', async (req, res) => {
|
|
366
|
+
try {
|
|
367
|
+
const { script, deviceId, url, runMode, platform, packageName, bundleId, mobileMode, loginUsername, loginPassword, isRnUrl, skipAppiumDriver } = req.body;
|
|
368
|
+
// 直接返回需要通过WebSocket执行的指令信息
|
|
369
|
+
res.json({
|
|
370
|
+
success: true,
|
|
371
|
+
message: '请通过WebSocket连接执行调试',
|
|
372
|
+
wsUrl: `ws://${req.hostname || 'localhost'}:8002`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
res.json({ success: false, message: error.message });
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
// AI脚本生成API
|
|
380
|
+
api.post('/ai/generate-script', async (req, res) => {
|
|
381
|
+
try {
|
|
382
|
+
const { naturalLanguage, runMode, platform, url, deviceId, packageName } = req.body;
|
|
383
|
+
if (!naturalLanguage || naturalLanguage.trim() === '') {
|
|
384
|
+
res.json({ success: false, message: '请输入自然语言描述' });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
// 获取脚本生成配置
|
|
388
|
+
const config = getConfig();
|
|
389
|
+
const scriptGenConfig = config.scriptGeneration;
|
|
390
|
+
// 如果配置了真实的AI服务,使用AutobotsClient调用
|
|
391
|
+
if (scriptGenConfig && scriptGenConfig.enabled) {
|
|
392
|
+
logger.info('[generate-script] 使用Autobots API生成脚本, baseUrl: ' + scriptGenConfig.baseUrl);
|
|
393
|
+
const client = new AutobotsClient({
|
|
394
|
+
enabled: scriptGenConfig.enabled,
|
|
395
|
+
baseUrl: scriptGenConfig.baseUrl,
|
|
396
|
+
agentId: scriptGenConfig.agentId,
|
|
397
|
+
token: scriptGenConfig.token,
|
|
398
|
+
connectTimeout: scriptGenConfig.connectTimeout || 30000,
|
|
399
|
+
requestTimeout: scriptGenConfig.requestTimeout || 120000,
|
|
400
|
+
socketTimeout: scriptGenConfig.socketTimeout || 300000,
|
|
401
|
+
});
|
|
402
|
+
// 构建上下文信息
|
|
403
|
+
const contextInfo = buildScriptContext(runMode, platform, url, deviceId, packageName);
|
|
404
|
+
const prompt = contextInfo + '\n\n用户需求:' + naturalLanguage.trim();
|
|
405
|
+
// 调用Autobots API生成脚本(流式)
|
|
406
|
+
let generatedScript = '';
|
|
407
|
+
for await (const result of client.generateScriptStream(prompt)) {
|
|
408
|
+
if (result.success) {
|
|
409
|
+
// 处理特殊标记
|
|
410
|
+
if (result.content.startsWith('FULL_CONTENT:')) {
|
|
411
|
+
generatedScript = result.content.substring(13);
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
generatedScript += result.content;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
logger.error('[generate-script] Autobots API错误: ' + result.error);
|
|
419
|
+
throw new Error(result.error);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// 清理生成的脚本(移除可能的markdown标记)
|
|
423
|
+
const cleanedScript = cleanGeneratedScript(generatedScript);
|
|
424
|
+
logger.info('[generate-script] 脚本生成成功,长度: ' + cleanedScript.length);
|
|
425
|
+
res.json({
|
|
426
|
+
success: true,
|
|
427
|
+
script: cleanedScript,
|
|
428
|
+
message: '脚本生成成功'
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
// 没有配置AI服务,使用本地生成
|
|
433
|
+
logger.info('[generate-script] 未配置Autobots API,使用本地生成');
|
|
434
|
+
const script = generateScriptFromNaturalLanguage({
|
|
435
|
+
naturalLanguage: naturalLanguage.trim(),
|
|
436
|
+
runMode: runMode || 'browser',
|
|
437
|
+
platform: platform || 'android',
|
|
438
|
+
url: url || '',
|
|
439
|
+
deviceId: deviceId || '',
|
|
440
|
+
packageName: packageName || ''
|
|
441
|
+
});
|
|
442
|
+
res.json({
|
|
443
|
+
success: true,
|
|
444
|
+
script,
|
|
445
|
+
message: '本地模式生成成功'
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
logger.warn(`AI脚本生成失败: ${error.message}`);
|
|
451
|
+
res.json({ success: false, message: error.message });
|
|
452
|
+
}
|
|
453
|
+
});
|
|
98
454
|
this.app.use('/api', api);
|
|
99
455
|
}
|
|
100
456
|
setupStaticFiles() {
|
|
@@ -114,44 +470,44 @@ export class WebServer {
|
|
|
114
470
|
<head>
|
|
115
471
|
<meta charset="UTF-8">
|
|
116
472
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
117
|
-
<title>AIServer
|
|
473
|
+
<title>AIServer 管理面板</title>
|
|
118
474
|
<style>
|
|
119
475
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
120
|
-
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#
|
|
476
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#fff;color:#111;min-height:100vh}
|
|
121
477
|
.container{max-width:1400px;margin:0 auto;padding:20px}
|
|
122
478
|
.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}
|
|
123
479
|
.header h1{font-size:24px;color:#38bdf8}
|
|
124
|
-
.tabs{display:flex;gap:4px;background:#
|
|
480
|
+
.tabs{display:flex;gap:4px;background:#fff;border-radius:8px;padding:4px;margin-bottom:24px}
|
|
125
481
|
.tab{padding:8px 20px;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;color:#94a3b8;border:none;background:none;transition:all .2s}
|
|
126
482
|
.tab.active{background:#38bdf8;color:#0f172a}
|
|
127
483
|
.tab:hover:not(.active){color:#e2e8f0}
|
|
128
484
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-bottom:24px}
|
|
129
|
-
.card{background:#
|
|
485
|
+
.card{background:#fff;border-radius:8px;padding:20px;border:1px solid #334155}
|
|
130
486
|
.card h3{font-size:13px;color:#94a3b8;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}
|
|
131
487
|
.card .value{font-size:32px;font-weight:700;color:#38bdf8}
|
|
132
488
|
.card .sub{font-size:12px;color:#64748b;margin-top:4px}
|
|
133
489
|
table{width:100%;border-collapse:collapse;margin-top:8px}
|
|
134
490
|
th,td{text-align:left;padding:10px 14px;border-bottom:1px solid #1e293b;font-size:13px}
|
|
135
|
-
th{color:#94a3b8;font-size:11px;text-transform:uppercase;letter-spacing:.5px;background:#
|
|
136
|
-
tr:hover{background:#
|
|
491
|
+
th{color:#94a3b8;font-size:11px;text-transform:uppercase;letter-spacing:.5px;background:#fff;position:sticky;top:0}
|
|
492
|
+
tr:hover{background:#fff}
|
|
137
493
|
.badge{display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600}
|
|
138
|
-
.b-online{background:#
|
|
139
|
-
.b-offline{background:#
|
|
140
|
-
.b-running{background:#
|
|
141
|
-
.b-completed{background:#
|
|
142
|
-
.b-failed{background:#
|
|
143
|
-
.b-pending{background:#
|
|
144
|
-
.b-idle{background:#
|
|
145
|
-
.b-stopped{background:#
|
|
494
|
+
.b-online{background:#fff;color:#22c55e}
|
|
495
|
+
.b-offline{background:#fff;color:#ef4444}
|
|
496
|
+
.b-running{background:#fff;color:#f59e0b}
|
|
497
|
+
.b-completed{background:#fff;color:#22c55e}
|
|
498
|
+
.b-failed{background:#fff;color:#ef4444}
|
|
499
|
+
.b-pending{background:#fff;color:#38bdf8}
|
|
500
|
+
.b-idle{background:#fff;color:#94a3b8}
|
|
501
|
+
.b-stopped{background:#fff;color:#94a3b8}
|
|
146
502
|
.btn{padding:6px 14px;border-radius:6px;border:none;cursor:pointer;font-size:12px;font-weight:600;transition:all .2s}
|
|
147
|
-
.btn-primary{background:#
|
|
148
|
-
.btn-primary:hover{background:#
|
|
503
|
+
.btn-primary{background:#fff;color:#0f172a}
|
|
504
|
+
.btn-primary:hover{background:#fff}
|
|
149
505
|
.btn-sm{padding:4px 10px;font-size:11px}
|
|
150
506
|
.section-title{font-size:16px;font-weight:600;color:#e2e8f0;margin:20px 0 12px;display:flex;align-items:center;gap:8px}
|
|
151
507
|
.mono{font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px}
|
|
152
|
-
.log-viewer{background:#
|
|
508
|
+
.log-viewer{background:#fff;border:1px solid #334155;border-radius:8px;padding:16px;max-height:600px;overflow-y:auto;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px;line-height:1.6}
|
|
153
509
|
.log-viewer .log-line{padding:2px 0;border-bottom:1px solid #1e293b20}
|
|
154
|
-
.log-viewer .log-line:hover{background:#
|
|
510
|
+
.log-viewer .log-line:hover{background:#fff}
|
|
155
511
|
.log-time{color:#64748b;margin-right:8px}
|
|
156
512
|
.log-level-info{color:#38bdf8}
|
|
157
513
|
.log-level-warn{color:#f59e0b}
|
|
@@ -159,10 +515,10 @@ tr:hover{background:#1e293b}
|
|
|
159
515
|
.log-level-debug{color:#94a3b8}
|
|
160
516
|
.log-content{color:#cbd5e1}
|
|
161
517
|
.log-new{animation:flashLog 0.5s ease-out}
|
|
162
|
-
@keyframes flashLog{0%{background:#
|
|
518
|
+
@keyframes flashLog{0%{background:#fff}100%{background:transparent}}
|
|
163
519
|
.log-live{color:#22c55e;font-size:10px;margin-left:8px;animation:pulse 2s infinite}
|
|
164
520
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
|
|
165
|
-
.detail-panel{background:#
|
|
521
|
+
.detail-panel{background:#fff;border:1px solid #334155;border-radius:8px;padding:20px;margin-top:16px}
|
|
166
522
|
.detail-row{display:flex;padding:8px 0;border-bottom:1px solid #33415520}
|
|
167
523
|
.detail-label{width:140px;color:#94a3b8;font-size:13px;flex-shrink:0}
|
|
168
524
|
.detail-value{color:#e2e8f0;font-size:13px;word-break:break-all}
|
|
@@ -171,7 +527,7 @@ tr:hover{background:#1e293b}
|
|
|
171
527
|
.refresh-bar{display:flex;align-items:center;gap:12px;margin-bottom:16px}
|
|
172
528
|
.refresh-bar .auto{font-size:12px;color:#64748b}
|
|
173
529
|
.refresh-bar .auto.on{color:#22c55e}
|
|
174
|
-
.search{background:#
|
|
530
|
+
.search{background:#fff;border:1px solid #334155;border-radius:6px;padding:8px 14px;color:#e2e8f0;font-size:13px;width:220px}
|
|
175
531
|
.search:focus{outline:none;border-color:#38bdf8}
|
|
176
532
|
.collapsed{display:none}
|
|
177
533
|
</style>
|
|
@@ -179,22 +535,24 @@ tr:hover{background:#1e293b}
|
|
|
179
535
|
<body>
|
|
180
536
|
<div class="container">
|
|
181
537
|
<div class="header">
|
|
182
|
-
<h1>AIServer
|
|
538
|
+
<h1>AIServer 管理面板</h1>
|
|
183
539
|
<div class="refresh-bar">
|
|
184
|
-
<span class="auto" id="autoLabel"
|
|
185
|
-
<button class="btn btn-primary" onclick="refresh()"
|
|
540
|
+
<span class="auto" id="autoLabel">自动刷新: 开启</span>
|
|
541
|
+
<button class="btn btn-primary" onclick="refresh()">刷新</button>
|
|
186
542
|
</div>
|
|
187
543
|
</div>
|
|
188
544
|
<div class="tabs">
|
|
189
|
-
<button class="tab active" onclick="switchTab('dashboard')"
|
|
190
|
-
<button class="tab" onclick="switchTab('devices')"
|
|
191
|
-
<button class="tab" onclick="switchTab('tasks')"
|
|
192
|
-
<button class="tab" onclick="switchTab('debug')"
|
|
545
|
+
<button class="tab active" onclick="switchTab('dashboard')">仪表盘</button>
|
|
546
|
+
<button class="tab" onclick="switchTab('devices')">设备</button>
|
|
547
|
+
<button class="tab" onclick="switchTab('tasks')">任务</button>
|
|
548
|
+
<button class="tab" onclick="switchTab('debug')">调试会话</button>
|
|
549
|
+
<button class="tab" onclick="switchTab('debugger')">调试执行</button>
|
|
193
550
|
</div>
|
|
194
551
|
<div id="page-dashboard"></div>
|
|
195
552
|
<div id="page-devices" class="collapsed"></div>
|
|
196
553
|
<div id="page-tasks" class="collapsed"></div>
|
|
197
554
|
<div id="page-debug" class="collapsed"></div>
|
|
555
|
+
<div id="page-debugger" class="collapsed">${getDebugPageHtml()}</div>
|
|
198
556
|
</div>
|
|
199
557
|
<script>
|
|
200
558
|
let currentTab='dashboard';
|
|
@@ -321,10 +679,17 @@ function switchTab(tab){
|
|
|
321
679
|
currentTab=tab;
|
|
322
680
|
detailOpen=false;
|
|
323
681
|
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
|
324
|
-
document.querySelectorAll('.tab').forEach(t=>{
|
|
682
|
+
document.querySelectorAll('.tab').forEach(t=>{
|
|
683
|
+
var txt=t.textContent.toLowerCase();
|
|
684
|
+
if(tab==='dashboard'&&txt.includes('仪表盘'))t.classList.add('active');
|
|
685
|
+
else if(tab==='devices'&&txt.includes('设备'))t.classList.add('active');
|
|
686
|
+
else if(tab==='tasks'&&txt.includes('任务'))t.classList.add('active');
|
|
687
|
+
else if(tab==='debug'&&txt.includes('调试会话'))t.classList.add('active');
|
|
688
|
+
else if(tab==='debugger'&&txt.includes('调试执行'))t.classList.add('active');
|
|
689
|
+
});
|
|
325
690
|
document.querySelectorAll('[id^="page-"]').forEach(p=>p.classList.add('collapsed'));
|
|
326
691
|
document.getElementById('page-'+tab).classList.remove('collapsed');
|
|
327
|
-
refresh();
|
|
692
|
+
if(tab!=='debugger') refresh();
|
|
328
693
|
}
|
|
329
694
|
|
|
330
695
|
function badge(s){
|
|
@@ -337,10 +702,10 @@ function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace
|
|
|
337
702
|
function timeAgo(d){
|
|
338
703
|
if(!d)return '-';
|
|
339
704
|
const diff=(Date.now()-new Date(d).getTime())/1000;
|
|
340
|
-
if(diff<60)return Math.floor(diff)+'
|
|
341
|
-
if(diff<3600)return Math.floor(diff/60)+'
|
|
342
|
-
if(diff<86400)return Math.floor(diff/3600)+'
|
|
343
|
-
return Math.floor(diff/86400)+'
|
|
705
|
+
if(diff<60)return Math.floor(diff)+'秒前';
|
|
706
|
+
if(diff<3600)return Math.floor(diff/60)+'分钟前';
|
|
707
|
+
if(diff<86400)return Math.floor(diff/3600)+'小时前';
|
|
708
|
+
return Math.floor(diff/86400)+'天前';
|
|
344
709
|
}
|
|
345
710
|
|
|
346
711
|
function fmtTime(d){return d?new Date(d).toLocaleString():'-'}
|
|
@@ -354,6 +719,11 @@ async function refresh(){
|
|
|
354
719
|
else if(currentTab==='devices')await loadDevices();
|
|
355
720
|
else if(currentTab==='tasks')await loadTasks();
|
|
356
721
|
else if(currentTab==='debug')await loadDebug();
|
|
722
|
+
else if(currentTab==='debugger')loadDebugger();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function loadDebugger(){
|
|
726
|
+
// 调试页面内容已在服务端渲染时注入
|
|
357
727
|
}
|
|
358
728
|
|
|
359
729
|
// ===== Dashboard =====
|
|
@@ -362,14 +732,14 @@ async function loadDashboard(){
|
|
|
362
732
|
if(!d)return;
|
|
363
733
|
const p=document.getElementById('page-dashboard');
|
|
364
734
|
let h='<div class="grid">';
|
|
365
|
-
h+='<div class="card"><h3
|
|
366
|
-
h+='<div class="card"><h3
|
|
367
|
-
h+='<div class="card"><h3
|
|
735
|
+
h+='<div class="card"><h3>在线设备</h3><div class="value">'+d.devices.online+'/'+d.devices.total+'</div><div class="sub">已连接设备</div></div>';
|
|
736
|
+
h+='<div class="card"><h3>运行中任务</h3><div class="value">'+d.tasks.running+'</div><div class="sub">总任务: '+d.tasks.total+'</div></div>';
|
|
737
|
+
h+='<div class="card"><h3>调试会话</h3><div class="value">'+d.debugSessions.length+'</div><div class="sub">近期会话</div></div>';
|
|
368
738
|
h+='</div>';
|
|
369
739
|
|
|
370
740
|
// Devices overview
|
|
371
741
|
if(d.devices.list&&d.devices.list.length>0){
|
|
372
|
-
h+='<div class="section-title"
|
|
742
|
+
h+='<div class="section-title">设备列表</div><table><tr><th>序列号</th><th>型号</th><th>品牌</th><th>平台</th><th>状态</th><th>最近心跳</th></tr>';
|
|
373
743
|
d.devices.list.forEach(dev=>{
|
|
374
744
|
h+='<tr><td class="mono">'+esc(dev.serialNumber)+'</td><td>'+esc(dev.model||'-')+'</td><td>'+esc(dev.brand||'-')+'</td><td>'+esc(dev.platform)+'</td><td>'+badge(dev.status)+'</td><td>'+timeAgo(dev.lastHeartbeatAt)+'</td></tr>';
|
|
375
745
|
});
|
|
@@ -378,18 +748,18 @@ async function loadDashboard(){
|
|
|
378
748
|
|
|
379
749
|
// Recent tasks
|
|
380
750
|
if(d.tasks.recent&&d.tasks.recent.length>0){
|
|
381
|
-
h+='<div class="section-title"
|
|
751
|
+
h+='<div class="section-title">近期任务</div><table><tr><th>任务ID</th><th>类型</th><th>状态</th><th>开始时间</th><th>完成时间</th><th>操作</th></tr>';
|
|
382
752
|
d.tasks.recent.forEach(t=>{
|
|
383
|
-
h+='<tr><td class="mono">'+esc(t.taskId)+'</td><td>'+esc(t.type)+'</td><td>'+badge(t.status)+'</td><td>'+fmtTime(t.startedAt)+'</td><td>'+fmtTime(t.completedAt)+'</td><td><button class="btn btn-primary btn-sm" onclick="showTaskLogs(\\''+esc(t.taskId)+'\\')"
|
|
753
|
+
h+='<tr><td class="mono">'+esc(t.taskId)+'</td><td>'+esc(t.type)+'</td><td>'+badge(t.status)+'</td><td>'+fmtTime(t.startedAt)+'</td><td>'+fmtTime(t.completedAt)+'</td><td><button class="btn btn-primary btn-sm" onclick="showTaskLogs(\\''+esc(t.taskId)+'\\')">日志</button></td></tr>';
|
|
384
754
|
});
|
|
385
755
|
h+='</table>';
|
|
386
756
|
}
|
|
387
757
|
|
|
388
758
|
// Recent debug sessions
|
|
389
759
|
if(d.debugSessions&&d.debugSessions.length>0){
|
|
390
|
-
h+='<div class="section-title"
|
|
760
|
+
h+='<div class="section-title">近期调试会话</div><table><tr><th>会话ID</th><th>设备</th><th>平台</th><th>状态</th><th>开始时间</th><th>操作</th></tr>';
|
|
391
761
|
d.debugSessions.slice(0,10).forEach(s=>{
|
|
392
|
-
h+='<tr><td class="mono">'+esc(s.sessionId.substring(0,24))+'...</td><td>'+esc(s.deviceId||'-')+'</td><td>'+esc(s.platform||'-')+'</td><td>'+badge(s.status)+'</td><td>'+fmtTime(s.startedAt)+'</td><td><button class="btn btn-primary btn-sm" onclick="showDebugLogs(\\''+esc(s.sessionId)+'\\')"
|
|
762
|
+
h+='<tr><td class="mono">'+esc(s.sessionId.substring(0,24))+'...</td><td>'+esc(s.deviceId||'-')+'</td><td>'+esc(s.platform||'-')+'</td><td>'+badge(s.status)+'</td><td>'+fmtTime(s.startedAt)+'</td><td><button class="btn btn-primary btn-sm" onclick="showDebugLogs(\\''+esc(s.sessionId)+'\\')">日志</button></td></tr>';
|
|
393
763
|
});
|
|
394
764
|
h+='</table>';
|
|
395
765
|
}
|
|
@@ -405,19 +775,19 @@ async function loadDevices(){
|
|
|
405
775
|
const p=document.getElementById('page-devices');
|
|
406
776
|
const online=devs.filter(d=>d.status==='online').length;
|
|
407
777
|
let h='<div class="grid">';
|
|
408
|
-
h+='<div class="card"><h3
|
|
409
|
-
h+='<div class="card"><h3
|
|
410
|
-
h+='<div class="card"><h3
|
|
778
|
+
h+='<div class="card"><h3>设备总数</h3><div class="value">'+devs.length+'</div></div>';
|
|
779
|
+
h+='<div class="card"><h3>在线</h3><div class="value">'+online+'</div></div>';
|
|
780
|
+
h+='<div class="card"><h3>离线</h3><div class="value">'+(devs.length-online)+'</div></div>';
|
|
411
781
|
h+='</div>';
|
|
412
782
|
|
|
413
783
|
if(devs.length>0){
|
|
414
|
-
h+='<div class="section-title"
|
|
784
|
+
h+='<div class="section-title">所有设备</div><table><tr><th>序列号</th><th>型号</th><th>品牌</th><th>平台</th><th>状态</th><th>最近心跳</th><th>创建时间</th></tr>';
|
|
415
785
|
devs.forEach(d=>{
|
|
416
786
|
h+='<tr><td class="mono">'+esc(d.serialNumber)+'</td><td>'+esc(d.model||'-')+'</td><td>'+esc(d.brand||'-')+'</td><td>'+esc(d.platform)+'</td><td>'+badge(d.status)+'</td><td>'+timeAgo(d.lastHeartbeatAt)+'</td><td>'+fmtTime(d.createdAt)+'</td></tr>';
|
|
417
787
|
});
|
|
418
788
|
h+='</table>';
|
|
419
789
|
}else{
|
|
420
|
-
h+='<div class="empty-state"><div class="icon">📱</div><p
|
|
790
|
+
h+='<div class="empty-state"><div class="icon">📱</div><p>暂无设备</p></div>';
|
|
421
791
|
}
|
|
422
792
|
p.innerHTML=h;
|
|
423
793
|
}
|
|
@@ -432,21 +802,21 @@ async function loadTasks(){
|
|
|
432
802
|
const failed=tasks.filter(t=>t.status==='failed').length;
|
|
433
803
|
|
|
434
804
|
let h='<div class="grid">';
|
|
435
|
-
h+='<div class="card"><h3
|
|
436
|
-
h+='<div class="card"><h3
|
|
437
|
-
h+='<div class="card"><h3
|
|
438
|
-
h+='<div class="card"><h3
|
|
805
|
+
h+='<div class="card"><h3>任务总数</h3><div class="value">'+tasks.length+'</div></div>';
|
|
806
|
+
h+='<div class="card"><h3>运行中</h3><div class="value">'+running+'</div></div>';
|
|
807
|
+
h+='<div class="card"><h3>已完成</h3><div class="value">'+completed+'</div></div>';
|
|
808
|
+
h+='<div class="card"><h3>已失败</h3><div class="value">'+failed+'</div></div>';
|
|
439
809
|
h+='</div>';
|
|
440
810
|
|
|
441
811
|
if(tasks.length>0){
|
|
442
|
-
h+='<div class="section-title"
|
|
812
|
+
h+='<div class="section-title">所有任务</div><table><tr><th>任务ID</th><th>执行ID</th><th>类型</th><th>状态</th><th>优先级</th><th>开始时间</th><th>完成时间</th><th>错误</th><th>操作</th></tr>';
|
|
443
813
|
tasks.forEach(t=>{
|
|
444
814
|
const err=t.errorMessage?(t.errorMessage.length>50?esc(t.errorMessage.substring(0,50))+'...':esc(t.errorMessage)):'-';
|
|
445
|
-
h+='<tr><td class="mono">'+esc(t.taskId)+'</td><td class="mono">'+esc(t.executionId||'-')+'</td><td>'+esc(t.type)+'</td><td>'+badge(t.status)+'</td><td>'+t.priority+'</td><td>'+fmtTime(t.startedAt)+'</td><td>'+fmtTime(t.completedAt)+'</td><td style="color:#ef4444;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+err+'</td><td><button class="btn btn-primary btn-sm" onclick="showTaskDetail(\\''+esc(t.taskId)+'\\')"
|
|
815
|
+
h+='<tr><td class="mono">'+esc(t.taskId)+'</td><td class="mono">'+esc(t.executionId||'-')+'</td><td>'+esc(t.type)+'</td><td>'+badge(t.status)+'</td><td>'+t.priority+'</td><td>'+fmtTime(t.startedAt)+'</td><td>'+fmtTime(t.completedAt)+'</td><td style="color:#ef4444;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+err+'</td><td><button class="btn btn-primary btn-sm" onclick="showTaskDetail(\\''+esc(t.taskId)+'\\')">详情</button></td></tr>';
|
|
446
816
|
});
|
|
447
817
|
h+='</table>';
|
|
448
818
|
}else{
|
|
449
|
-
h+='<div class="empty-state"><div class="icon">📋</div><p
|
|
819
|
+
h+='<div class="empty-state"><div class="icon">📋</div><p>暂无任务</p></div>';
|
|
450
820
|
}
|
|
451
821
|
h+='<div id="task-detail"></div>';
|
|
452
822
|
p.innerHTML=h;
|
|
@@ -459,33 +829,33 @@ async function showTaskDetail(taskId){
|
|
|
459
829
|
const task=await api('/tasks/'+encodeURIComponent(taskId));
|
|
460
830
|
const logs=await api('/tasks/'+encodeURIComponent(taskId)+'/logs');
|
|
461
831
|
const panel=document.getElementById('task-detail');
|
|
462
|
-
if(!task){panel.innerHTML='<p style="color:#ef4444"
|
|
832
|
+
if(!task){panel.innerHTML='<p style="color:#ef4444">任务未找到</p>';detailOpen=false;currentDetailTaskId=null;return}
|
|
463
833
|
|
|
464
834
|
currentDetailTaskStatus=task.status;
|
|
465
835
|
const isRunning=task.status==='running'||task.status==='pending';
|
|
466
836
|
|
|
467
|
-
let h='<div class="detail-panel"><div class="section-title"
|
|
468
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
469
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
470
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
471
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
472
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
473
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
474
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
475
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
476
|
-
if(task.errorMessage)h+='<div class="detail-row"><div class="detail-label"
|
|
837
|
+
let h='<div class="detail-panel"><div class="section-title">任务详情: '+esc(task.taskId)+' <button class="btn btn-sm" style="background:#334155;color:#94a3b8;margin-left:12px" onclick="closeTaskDetail()">关闭</button></div>';
|
|
838
|
+
h+='<div class="detail-row"><div class="detail-label">任务ID</div><div class="detail-value mono">'+esc(task.taskId)+'</div></div>';
|
|
839
|
+
h+='<div class="detail-row"><div class="detail-label">执行ID</div><div class="detail-value mono">'+esc(task.executionId||'-')+'</div></div>';
|
|
840
|
+
h+='<div class="detail-row"><div class="detail-label">类型</div><div class="detail-value">'+esc(task.type)+'</div></div>';
|
|
841
|
+
h+='<div class="detail-row"><div class="detail-label">状态</div><div class="detail-value">'+badge(task.status)+'</div></div>';
|
|
842
|
+
h+='<div class="detail-row"><div class="detail-label">优先级</div><div class="detail-value">'+task.priority+'</div></div>';
|
|
843
|
+
h+='<div class="detail-row"><div class="detail-label">开始时间</div><div class="detail-value">'+fmtTime(task.startedAt)+'</div></div>';
|
|
844
|
+
h+='<div class="detail-row"><div class="detail-label">完成时间</div><div class="detail-value">'+fmtTime(task.completedAt)+'</div></div>';
|
|
845
|
+
h+='<div class="detail-row"><div class="detail-label">创建时间</div><div class="detail-value">'+fmtTime(task.createdAt)+'</div></div>';
|
|
846
|
+
if(task.errorMessage)h+='<div class="detail-row"><div class="detail-label">错误信息</div><div class="detail-value" style="color:#ef4444">'+esc(task.errorMessage)+'</div></div>';
|
|
477
847
|
if(task.config){
|
|
478
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
848
|
+
h+='<div class="detail-row"><div class="detail-label">配置</div><div class="detail-value mono" style="white-space:pre-wrap;font-size:11px">'+esc(typeof task.config==='object'?JSON.stringify(task.config,null,2):task.config)+'</div></div>';
|
|
479
849
|
}
|
|
480
850
|
if(task.result){
|
|
481
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
851
|
+
h+='<div class="detail-row"><div class="detail-label">结果</div><div class="detail-value mono" style="white-space:pre-wrap;font-size:11px">'+esc(typeof task.result==='object'?JSON.stringify(task.result,null,2):task.result)+'</div></div>';
|
|
482
852
|
}
|
|
483
853
|
|
|
484
854
|
// Execution logs with real-time support
|
|
485
855
|
const logSafeId=esc(taskId).replace(/[^a-zA-Z0-9-_]/g,'_');
|
|
486
856
|
const existingLogsCount=logs?logs.length:0;
|
|
487
857
|
if(existingLogsCount>0||isRunning){
|
|
488
|
-
h+='<div class="section-title" style="margin-top:20px"
|
|
858
|
+
h+='<div class="section-title" style="margin-top:20px">执行日志 <span id="task-log-count-'+logSafeId+'" style="color:#64748b;font-size:12px">'+(existingLogsCount||'')+'</span>';
|
|
489
859
|
if(isRunning)h+=' <span style="color:#22c55e;font-size:12px">● LIVE</span>';
|
|
490
860
|
h+='</div>';
|
|
491
861
|
h+='<div class="log-viewer" id="task-log-'+logSafeId+'" style="max-height:400px">';
|
|
@@ -497,7 +867,7 @@ async function showTaskDetail(taskId){
|
|
|
497
867
|
}
|
|
498
868
|
h+='</div>';
|
|
499
869
|
}else{
|
|
500
|
-
h+='<div class="section-title" style="margin-top:20px"
|
|
870
|
+
h+='<div class="section-title" style="margin-top:20px">执行日志</div><p style="color:#64748b">暂无日志</p>';
|
|
501
871
|
}
|
|
502
872
|
|
|
503
873
|
h+='</div>';
|
|
@@ -527,18 +897,18 @@ async function loadDebug(){
|
|
|
527
897
|
const running=sessions.filter(s=>s.status==='running').length;
|
|
528
898
|
|
|
529
899
|
let h='<div class="grid">';
|
|
530
|
-
h+='<div class="card"><h3
|
|
531
|
-
h+='<div class="card"><h3
|
|
900
|
+
h+='<div class="card"><h3>会话总数</h3><div class="value">'+sessions.length+'</div></div>';
|
|
901
|
+
h+='<div class="card"><h3>运行中</h3><div class="value">'+running+'</div></div>';
|
|
532
902
|
h+='</div>';
|
|
533
903
|
|
|
534
904
|
if(sessions.length>0){
|
|
535
|
-
h+='<div class="section-title"
|
|
905
|
+
h+='<div class="section-title">所有调试会话</div><table><tr><th>会话ID</th><th>任务ID</th><th>设备</th><th>平台</th><th>状态</th><th>开始时间</th><th>完成时间</th><th>操作</th></tr>';
|
|
536
906
|
sessions.forEach(s=>{
|
|
537
|
-
h+='<tr><td class="mono">'+esc(s.sessionId.substring(0,28))+'...</td><td class="mono">'+esc(s.taskId||'-')+'</td><td>'+esc(s.deviceId||'-')+'</td><td>'+esc(s.platform||'-')+'</td><td>'+badge(s.status)+'</td><td>'+fmtTime(s.startedAt)+'</td><td>'+fmtTime(s.completedAt)+'</td><td><button class="btn btn-primary btn-sm" onclick="showDebugDetail(\\''+esc(s.sessionId)+'\\')"
|
|
907
|
+
h+='<tr><td class="mono">'+esc(s.sessionId.substring(0,28))+'...</td><td class="mono">'+esc(s.taskId||'-')+'</td><td>'+esc(s.deviceId||'-')+'</td><td>'+esc(s.platform||'-')+'</td><td>'+badge(s.status)+'</td><td>'+fmtTime(s.startedAt)+'</td><td>'+fmtTime(s.completedAt)+'</td><td><button class="btn btn-primary btn-sm" onclick="showDebugDetail(\\''+esc(s.sessionId)+'\\')">详情</button></td></tr>';
|
|
538
908
|
});
|
|
539
909
|
h+='</table>';
|
|
540
910
|
}else{
|
|
541
|
-
h+='<div class="empty-state"><div class="icon">🐛</div><p
|
|
911
|
+
h+='<div class="empty-state"><div class="icon">🐛</div><p>暂无调试会话</p></div>';
|
|
542
912
|
}
|
|
543
913
|
h+='<div id="debug-detail"></div>';
|
|
544
914
|
p.innerHTML=h;
|
|
@@ -549,21 +919,21 @@ async function showDebugDetail(sessionId){
|
|
|
549
919
|
const session=await api('/debug/sessions/'+encodeURIComponent(sessionId));
|
|
550
920
|
const logs=await api('/debug/sessions/'+encodeURIComponent(sessionId)+'/logs');
|
|
551
921
|
const panel=document.getElementById('debug-detail');
|
|
552
|
-
if(!session){panel.innerHTML='<p style="color:#ef4444"
|
|
553
|
-
|
|
554
|
-
let h='<div class="detail-panel"><div class="section-title"
|
|
555
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
556
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
557
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
558
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
559
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
560
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
561
|
-
h+='<div class="detail-row"><div class="detail-label"
|
|
922
|
+
if(!session){panel.innerHTML='<p style="color:#ef4444">会话未找到</p>';detailOpen=false;return}
|
|
923
|
+
|
|
924
|
+
let h='<div class="detail-panel"><div class="section-title">调试会话详情: '+esc(sessionId)+' <button class="btn btn-sm" style="background:#334155;color:#94a3b8;margin-left:12px" onclick="closeDebugDetail()">关闭</button></div>';
|
|
925
|
+
h+='<div class="detail-row"><div class="detail-label">会话ID</div><div class="detail-value mono">'+esc(session.sessionId)+'</div></div>';
|
|
926
|
+
h+='<div class="detail-row"><div class="detail-label">任务ID</div><div class="detail-value mono">'+esc(session.taskId||'-')+'</div></div>';
|
|
927
|
+
h+='<div class="detail-row"><div class="detail-label">设备</div><div class="detail-value mono">'+esc(session.deviceId||'-')+'</div></div>';
|
|
928
|
+
h+='<div class="detail-row"><div class="detail-label">平台</div><div class="detail-value">'+esc(session.platform||'-')+'</div></div>';
|
|
929
|
+
h+='<div class="detail-row"><div class="detail-label">状态</div><div class="detail-value">'+badge(session.status)+'</div></div>';
|
|
930
|
+
h+='<div class="detail-row"><div class="detail-label">开始时间</div><div class="detail-value">'+fmtTime(session.startedAt)+'</div></div>';
|
|
931
|
+
h+='<div class="detail-row"><div class="detail-label">完成时间</div><div class="detail-value">'+fmtTime(session.completedAt)+'</div></div>';
|
|
562
932
|
h+='</div>';
|
|
563
933
|
|
|
564
934
|
// Session logs
|
|
565
935
|
if(logs&&logs.length>0){
|
|
566
|
-
h+='<div class="detail-panel" style="margin-top:16px"><div class="section-title"
|
|
936
|
+
h+='<div class="detail-panel" style="margin-top:16px"><div class="section-title">会话日志 ('+logs.length+')</div>';
|
|
567
937
|
h+='<div class="log-viewer">';
|
|
568
938
|
logs.forEach(l=>{
|
|
569
939
|
const lvl='log-level-'+esc(l.type==='log_output'?'info':l.type==='error'?'error':'info');
|
|
@@ -572,7 +942,7 @@ async function showDebugDetail(sessionId){
|
|
|
572
942
|
});
|
|
573
943
|
h+='</div></div>';
|
|
574
944
|
}else{
|
|
575
|
-
h+='<div class="detail-panel" style="margin-top:16px"><div class="section-title"
|
|
945
|
+
h+='<div class="detail-panel" style="margin-top:16px"><div class="section-title">会话日志</div><p style="color:#64748b">暂无日志</p></div>';
|
|
576
946
|
}
|
|
577
947
|
|
|
578
948
|
panel.innerHTML=h;
|