@aiscene/aiserver 1.2.4 → 1.2.6
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 +6 -28
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +11 -7
- 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.map +1 -1
- package/dist/debug/websocket-server.js +3 -8
- package/dist/debug/websocket-server.js.map +1 -1
- package/dist/task/scheduler.d.ts.map +1 -1
- package/dist/task/scheduler.js +19 -6
- package/dist/task/scheduler.js.map +1 -1
- package/dist/web/debug-page.d.ts.map +1 -1
- package/dist/web/debug-page.js +367 -35
- package/dist/web/debug-page.js.map +1 -1
- package/dist/web/server.d.ts +3 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +586 -203
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
package/dist/web/server.js
CHANGED
|
@@ -7,7 +7,205 @@ import { taskRepo } from '../storage/repositories/task-repo.js';
|
|
|
7
7
|
import { debugLogRepo } from '../storage/repositories/debug-log-repo.js';
|
|
8
8
|
import { executionLogRepo } from '../storage/repositories/execution-log-repo.js';
|
|
9
9
|
import { getDebugPageHtml } from './debug-page.js';
|
|
10
|
+
import { getConfig } from '../config/index.js';
|
|
11
|
+
import { AutobotsClient } from '../core/autobots-client.js';
|
|
10
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
|
+
}
|
|
11
209
|
export class WebServer {
|
|
12
210
|
app;
|
|
13
211
|
config;
|
|
@@ -15,6 +213,7 @@ export class WebServer {
|
|
|
15
213
|
this.app = express();
|
|
16
214
|
this.setupMiddleware();
|
|
17
215
|
this.setupApiRoutes();
|
|
216
|
+
this.setupDetailRoutes();
|
|
18
217
|
this.setupStaticFiles();
|
|
19
218
|
}
|
|
20
219
|
setupMiddleware() {
|
|
@@ -178,8 +377,103 @@ export class WebServer {
|
|
|
178
377
|
res.json({ success: false, message: error.message });
|
|
179
378
|
}
|
|
180
379
|
});
|
|
380
|
+
// AI脚本生成API
|
|
381
|
+
api.post('/ai/generate-script', async (req, res) => {
|
|
382
|
+
try {
|
|
383
|
+
const { naturalLanguage, runMode, platform, url, deviceId, packageName } = req.body;
|
|
384
|
+
if (!naturalLanguage || naturalLanguage.trim() === '') {
|
|
385
|
+
res.json({ success: false, message: '请输入自然语言描述' });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
// 获取脚本生成配置
|
|
389
|
+
const config = getConfig();
|
|
390
|
+
const scriptGenConfig = config.scriptGeneration;
|
|
391
|
+
// 如果配置了真实的AI服务,使用AutobotsClient调用
|
|
392
|
+
if (scriptGenConfig && scriptGenConfig.enabled) {
|
|
393
|
+
logger.info('[generate-script] 使用Autobots API生成脚本, baseUrl: ' + scriptGenConfig.baseUrl);
|
|
394
|
+
const client = new AutobotsClient({
|
|
395
|
+
enabled: scriptGenConfig.enabled,
|
|
396
|
+
baseUrl: scriptGenConfig.baseUrl,
|
|
397
|
+
agentId: scriptGenConfig.agentId,
|
|
398
|
+
token: scriptGenConfig.token,
|
|
399
|
+
connectTimeout: scriptGenConfig.connectTimeout || 30000,
|
|
400
|
+
requestTimeout: scriptGenConfig.requestTimeout || 120000,
|
|
401
|
+
socketTimeout: scriptGenConfig.socketTimeout || 300000,
|
|
402
|
+
});
|
|
403
|
+
// 构建上下文信息
|
|
404
|
+
const contextInfo = buildScriptContext(runMode, platform, url, deviceId, packageName);
|
|
405
|
+
const prompt = contextInfo + '\n\n用户需求:' + naturalLanguage.trim();
|
|
406
|
+
// 调用Autobots API生成脚本(流式)
|
|
407
|
+
let generatedScript = '';
|
|
408
|
+
for await (const result of client.generateScriptStream(prompt)) {
|
|
409
|
+
if (result.success) {
|
|
410
|
+
// 处理特殊标记
|
|
411
|
+
if (result.content.startsWith('FULL_CONTENT:')) {
|
|
412
|
+
generatedScript = result.content.substring(13);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
generatedScript += result.content;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
logger.error('[generate-script] Autobots API错误: ' + result.error);
|
|
420
|
+
throw new Error(result.error);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// 清理生成的脚本(移除可能的markdown标记)
|
|
424
|
+
const cleanedScript = cleanGeneratedScript(generatedScript);
|
|
425
|
+
logger.info('[generate-script] 脚本生成成功,长度: ' + cleanedScript.length);
|
|
426
|
+
res.json({
|
|
427
|
+
success: true,
|
|
428
|
+
script: cleanedScript,
|
|
429
|
+
message: '脚本生成成功'
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
// 没有配置AI服务,使用本地生成
|
|
434
|
+
logger.info('[generate-script] 未配置Autobots API,使用本地生成');
|
|
435
|
+
const script = generateScriptFromNaturalLanguage({
|
|
436
|
+
naturalLanguage: naturalLanguage.trim(),
|
|
437
|
+
runMode: runMode || 'browser',
|
|
438
|
+
platform: platform || 'android',
|
|
439
|
+
url: url || '',
|
|
440
|
+
deviceId: deviceId || '',
|
|
441
|
+
packageName: packageName || ''
|
|
442
|
+
});
|
|
443
|
+
res.json({
|
|
444
|
+
success: true,
|
|
445
|
+
script,
|
|
446
|
+
message: '本地模式生成成功'
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
logger.warn(`AI脚本生成失败: ${error.message}`);
|
|
452
|
+
res.json({ success: false, message: error.message });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
181
455
|
this.app.use('/api', api);
|
|
182
456
|
}
|
|
457
|
+
setupDetailRoutes() {
|
|
458
|
+
// 任务详情页
|
|
459
|
+
this.app.get('/task-detail', (req, res) => {
|
|
460
|
+
const taskId = req.query.id;
|
|
461
|
+
if (!taskId) {
|
|
462
|
+
res.status(400).send('Missing id');
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
res.status(200).send(this.getTaskDetailHtml());
|
|
466
|
+
});
|
|
467
|
+
// 调试会话详情页
|
|
468
|
+
this.app.get('/debug-detail', (req, res) => {
|
|
469
|
+
const sessionId = req.query.id;
|
|
470
|
+
if (!sessionId) {
|
|
471
|
+
res.status(400).send('Missing id');
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
res.status(200).send(this.getDebugDetailHtml());
|
|
475
|
+
});
|
|
476
|
+
}
|
|
183
477
|
setupStaticFiles() {
|
|
184
478
|
const distPath = path.resolve(import.meta.dirname, 'dist');
|
|
185
479
|
this.app.use(express.static(distPath));
|
|
@@ -200,63 +494,72 @@ export class WebServer {
|
|
|
200
494
|
<title>AIServer 管理面板</title>
|
|
201
495
|
<style>
|
|
202
496
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
203
|
-
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#
|
|
204
|
-
.container{max-width:1400px;margin:0 auto;padding:20px}
|
|
497
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f8fafc;color:#1e293b;min-height:100vh}
|
|
498
|
+
.container{max-width:1400px;margin:0 auto;padding:20px;height:calc(100vh - 40px);display:flex;flex-direction:column}
|
|
499
|
+
.container.debugger-full{max-width:100%;padding:0;height:100vh;overflow:hidden}
|
|
500
|
+
.container.debugger-full .header{display:none}
|
|
501
|
+
.container.debugger-full .tabs{display:none}
|
|
502
|
+
.container.debugger-full>[id^="page-"]{flex:1;min-height:0}
|
|
205
503
|
.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}
|
|
206
|
-
.header h1{font-size:24px;color:#
|
|
207
|
-
.tabs{display:flex;gap:4px;background:#
|
|
208
|
-
.tab{padding:8px 20px;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;color:#
|
|
209
|
-
.tab.active{background:#38bdf8;color:#
|
|
210
|
-
.tab:hover:not(.active){color:#
|
|
504
|
+
.header h1{font-size:24px;color:#0f172a}
|
|
505
|
+
.tabs{display:flex;gap:4px;background:#f1f5f9;border-radius:8px;padding:4px;margin-bottom:24px}
|
|
506
|
+
.tab{padding:8px 20px;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;color:#64748b;border:none;background:none;transition:all .2s}
|
|
507
|
+
.tab.active{background:#38bdf8;color:#fff}
|
|
508
|
+
.tab:hover:not(.active){color:#334155}
|
|
211
509
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-bottom:24px}
|
|
212
|
-
.card{background:#fff;border-radius:8px;padding:20px;border:1px solid #
|
|
213
|
-
.card h3{font-size:13px;color:#
|
|
214
|
-
.card .value{font-size:32px;font-weight:700;color:#
|
|
215
|
-
.card .sub{font-size:12px;color:#
|
|
510
|
+
.card{background:#fff;border-radius:8px;padding:20px;border:1px solid #e2e8f0;box-shadow:0 1px 3px rgba(0,0,0,0.04)}
|
|
511
|
+
.card h3{font-size:13px;color:#64748b;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}
|
|
512
|
+
.card .value{font-size:32px;font-weight:700;color:#0f172a}
|
|
513
|
+
.card .sub{font-size:12px;color:#94a3b8;margin-top:4px}
|
|
216
514
|
table{width:100%;border-collapse:collapse;margin-top:8px}
|
|
217
|
-
th,td{text-align:left;padding:10px 14px;border-bottom:1px solid #
|
|
218
|
-
th{color:#
|
|
219
|
-
tr:hover{background:#
|
|
515
|
+
th,td{text-align:left;padding:10px 14px;border-bottom:1px solid #e2e8f0;font-size:13px;color:#1e293b}
|
|
516
|
+
th{color:#64748b;font-size:11px;text-transform:uppercase;letter-spacing:.5px;background:#f8fafc;position:sticky;top:0}
|
|
517
|
+
tr:hover{background:#f8fafc}
|
|
220
518
|
.badge{display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600}
|
|
221
|
-
.b-online{background:#
|
|
222
|
-
.b-offline{background:#
|
|
223
|
-
.b-running{background:#
|
|
224
|
-
.b-completed{background:#
|
|
225
|
-
.b-failed{background:#
|
|
226
|
-
.b-pending{background:#
|
|
227
|
-
.b-idle{background:#
|
|
228
|
-
.b-stopped{background:#
|
|
519
|
+
.b-online{background:#dcfce7;color:#16a34a}
|
|
520
|
+
.b-offline{background:#fee2e2;color:#dc2626}
|
|
521
|
+
.b-running{background:#fef3c7;color:#d97706}
|
|
522
|
+
.b-completed{background:#dcfce7;color:#16a34a}
|
|
523
|
+
.b-failed{background:#fee2e2;color:#dc2626}
|
|
524
|
+
.b-pending{background:#dbeafe;color:#2563eb}
|
|
525
|
+
.b-idle{background:#f1f5f9;color:#64748b}
|
|
526
|
+
.b-stopped{background:#f1f5f9;color:#64748b}
|
|
229
527
|
.btn{padding:6px 14px;border-radius:6px;border:none;cursor:pointer;font-size:12px;font-weight:600;transition:all .2s}
|
|
230
|
-
.btn-primary{background:#
|
|
231
|
-
.btn-primary:hover{background:#
|
|
528
|
+
.btn-primary{background:#2563eb;color:#fff}
|
|
529
|
+
.btn-primary:hover{background:#1d4ed8}
|
|
232
530
|
.btn-sm{padding:4px 10px;font-size:11px}
|
|
233
|
-
.section-title{font-size:16px;font-weight:600;color:#
|
|
531
|
+
.section-title{font-size:16px;font-weight:600;color:#1e293b;margin:20px 0 12px;display:flex;align-items:center;gap:8px}
|
|
234
532
|
.mono{font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px}
|
|
235
|
-
.log-viewer{background:#fff;border:1px solid #
|
|
236
|
-
.log-viewer .log-line{padding:2px 0;border-bottom:1px solid #
|
|
237
|
-
.log-viewer .log-line:hover{background:#
|
|
238
|
-
.log-time{color:#
|
|
239
|
-
.log-level-info{color:#
|
|
240
|
-
.log-level-warn{color:#
|
|
241
|
-
.log-level-error{color:#
|
|
533
|
+
.log-viewer{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px 16px 40px;max-height:600px;overflow-y:auto;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px;line-height:1.6}
|
|
534
|
+
.log-viewer .log-line{padding:2px 0;border-bottom:1px solid #f1f5f9}
|
|
535
|
+
.log-viewer .log-line:hover{background:#f8fafc}
|
|
536
|
+
.log-time{color:#94a3b8;margin-right:8px}
|
|
537
|
+
.log-level-info{color:#0284c7}
|
|
538
|
+
.log-level-warn{color:#d97706}
|
|
539
|
+
.log-level-error{color:#dc2626}
|
|
242
540
|
.log-level-debug{color:#94a3b8}
|
|
243
|
-
.log-content{color:#
|
|
541
|
+
.log-content{color:#334155}
|
|
244
542
|
.log-new{animation:flashLog 0.5s ease-out}
|
|
245
|
-
@keyframes flashLog{0%{background:#
|
|
246
|
-
.log-live{color:#
|
|
543
|
+
@keyframes flashLog{0%{background:#dbeafe}100%{background:transparent}}
|
|
544
|
+
.log-live{color:#16a34a;font-size:10px;margin-left:8px;animation:pulse 2s infinite}
|
|
247
545
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
|
|
248
|
-
.detail-panel{background:#fff;border:1px solid #
|
|
249
|
-
.detail-row{display:flex;padding:8px 0;border-bottom:1px solid #
|
|
250
|
-
.detail-label{width:140px;color:#
|
|
251
|
-
.detail-value{color:#
|
|
252
|
-
.empty-state{text-align:center;padding:60px 20px;color:#
|
|
546
|
+
.detail-panel{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:20px;margin-top:16px;box-shadow:0 1px 3px rgba(0,0,0,0.04)}
|
|
547
|
+
.detail-row{display:flex;padding:8px 0;border-bottom:1px solid #f1f5f9}
|
|
548
|
+
.detail-label{width:140px;color:#64748b;font-size:13px;flex-shrink:0}
|
|
549
|
+
.detail-value{color:#1e293b;font-size:13px;word-break:break-all}
|
|
550
|
+
.empty-state{text-align:center;padding:60px 20px;color:#94a3b8}
|
|
253
551
|
.empty-state .icon{font-size:48px;margin-bottom:16px}
|
|
254
552
|
.refresh-bar{display:flex;align-items:center;gap:12px;margin-bottom:16px}
|
|
255
|
-
.refresh-bar .auto{font-size:12px;color:#
|
|
256
|
-
.refresh-bar .auto.on{color:#
|
|
257
|
-
.search{background:#fff;border:1px solid #
|
|
553
|
+
.refresh-bar .auto{font-size:12px;color:#94a3b8}
|
|
554
|
+
.refresh-bar .auto.on{color:#16a34a}
|
|
555
|
+
.search{background:#fff;border:1px solid #cbd5e1;border-radius:6px;padding:8px 14px;color:#1e293b;font-size:13px;width:220px}
|
|
258
556
|
.search:focus{outline:none;border-color:#38bdf8}
|
|
259
557
|
.collapsed{display:none}
|
|
558
|
+
.table-wrap{max-height:480px;overflow-y:auto;border:1px solid #e2e8f0;border-radius:8px;margin-top:8px}
|
|
559
|
+
.table-wrap table{margin-top:0}
|
|
560
|
+
.pager{display:flex;align-items:center;justify-content:space-between;padding:8px 0;font-size:12px;color:#64748b}
|
|
561
|
+
.pager .btn{margin:0 2px}
|
|
562
|
+
.pager .info{display:flex;align-items:center;gap:8px}
|
|
260
563
|
</style>
|
|
261
564
|
</head>
|
|
262
565
|
<body>
|
|
@@ -285,7 +588,6 @@ tr:hover{background:#fff}
|
|
|
285
588
|
let currentTab='dashboard';
|
|
286
589
|
let autoRefresh=true;
|
|
287
590
|
let refreshTimer=null;
|
|
288
|
-
let detailOpen=false; // track if a detail panel is open
|
|
289
591
|
|
|
290
592
|
// ===== WebSocket for Real-time Task Logs =====
|
|
291
593
|
let ws=null;
|
|
@@ -398,13 +700,9 @@ function updateTaskLogUI(taskId){
|
|
|
398
700
|
logViewer.scrollTop=logViewer.scrollHeight;
|
|
399
701
|
}
|
|
400
702
|
|
|
401
|
-
// ===== Tab
|
|
402
|
-
let currentDetailTaskId=null;
|
|
403
|
-
let currentDetailTaskStatus=null;
|
|
404
|
-
|
|
703
|
+
// ===== Tab switching =====
|
|
405
704
|
function switchTab(tab){
|
|
406
705
|
currentTab=tab;
|
|
407
|
-
detailOpen=false;
|
|
408
706
|
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
|
409
707
|
document.querySelectorAll('.tab').forEach(t=>{
|
|
410
708
|
var txt=t.textContent.toLowerCase();
|
|
@@ -416,6 +714,9 @@ function switchTab(tab){
|
|
|
416
714
|
});
|
|
417
715
|
document.querySelectorAll('[id^="page-"]').forEach(p=>p.classList.add('collapsed'));
|
|
418
716
|
document.getElementById('page-'+tab).classList.remove('collapsed');
|
|
717
|
+
// 调试执行页面全屏展示
|
|
718
|
+
const container=document.querySelector('.container');
|
|
719
|
+
if(tab==='debugger'){container.classList.add('debugger-full')}else{container.classList.remove('debugger-full')}
|
|
419
720
|
if(tab!=='debugger') refresh();
|
|
420
721
|
}
|
|
421
722
|
|
|
@@ -424,7 +725,9 @@ function badge(s){
|
|
|
424
725
|
return '<span class="badge '+cls+'">'+s+'</span>';
|
|
425
726
|
}
|
|
426
727
|
|
|
427
|
-
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'
|
|
728
|
+
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
|
729
|
+
|
|
730
|
+
function openDetail(el){window.open(el.dataset.type+'?id='+encodeURIComponent(el.dataset.id),'_blank')}
|
|
428
731
|
|
|
429
732
|
function timeAgo(d){
|
|
430
733
|
if(!d)return '-';
|
|
@@ -443,9 +746,9 @@ async function api(path){
|
|
|
443
746
|
|
|
444
747
|
async function refresh(){
|
|
445
748
|
if(currentTab==='dashboard')await loadDashboard();
|
|
446
|
-
else if(currentTab==='devices')await loadDevices();
|
|
447
|
-
else if(currentTab==='tasks')await loadTasks();
|
|
448
|
-
else if(currentTab==='debug')await loadDebug();
|
|
749
|
+
else if(currentTab==='devices'){await loadDevicesData();loadDevices();}
|
|
750
|
+
else if(currentTab==='tasks'){await loadTasksData();loadTasks();}
|
|
751
|
+
else if(currentTab==='debug'){await loadDebugData();loadDebug();}
|
|
449
752
|
else if(currentTab==='debugger')loadDebugger();
|
|
450
753
|
}
|
|
451
754
|
|
|
@@ -466,41 +769,46 @@ async function loadDashboard(){
|
|
|
466
769
|
|
|
467
770
|
// Devices overview
|
|
468
771
|
if(d.devices.list&&d.devices.list.length>0){
|
|
469
|
-
h+='<div class="section-title">设备列表</div><table><tr><th>序列号</th><th>型号</th><th>品牌</th><th>平台</th><th>状态</th><th>最近心跳</th></tr>';
|
|
470
|
-
d.devices.list.forEach(dev=>{
|
|
772
|
+
h+='<div class="section-title">设备列表</div><div class="table-wrap"><table><tr><th>序列号</th><th>型号</th><th>品牌</th><th>平台</th><th>状态</th><th>最近心跳</th></tr>';
|
|
773
|
+
d.devices.list.slice(0,20).forEach(dev=>{
|
|
471
774
|
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>';
|
|
472
775
|
});
|
|
473
|
-
h+='</table>';
|
|
776
|
+
h+='</table></div>';
|
|
474
777
|
}
|
|
475
778
|
|
|
476
779
|
// Recent tasks
|
|
477
780
|
if(d.tasks.recent&&d.tasks.recent.length>0){
|
|
478
|
-
h+='<div class="section-title">近期任务</div><table><tr><th>任务ID</th><th>类型</th><th>状态</th><th>开始时间</th><th>完成时间</th><th>操作</th></tr>';
|
|
479
|
-
d.tasks.recent.forEach(t=>{
|
|
480
|
-
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"
|
|
781
|
+
h+='<div class="section-title">近期任务</div><div class="table-wrap"><table><tr><th>任务ID</th><th>类型</th><th>状态</th><th>开始时间</th><th>完成时间</th><th>操作</th></tr>';
|
|
782
|
+
d.tasks.recent.slice(0,20).forEach(t=>{
|
|
783
|
+
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" data-type="/task-detail" data-id="'+esc(t.taskId)+'" onclick="openDetail(this)">日志</button></td></tr>';
|
|
481
784
|
});
|
|
482
|
-
h+='</table>';
|
|
785
|
+
h+='</table></div>';
|
|
483
786
|
}
|
|
484
787
|
|
|
485
788
|
// Recent debug sessions
|
|
486
789
|
if(d.debugSessions&&d.debugSessions.length>0){
|
|
487
|
-
h+='<div class="section-title">近期调试会话</div><table><tr><th>会话ID</th><th>设备</th><th>平台</th><th>状态</th><th>开始时间</th><th>操作</th></tr>';
|
|
790
|
+
h+='<div class="section-title">近期调试会话</div><div class="table-wrap"><table><tr><th>会话ID</th><th>设备</th><th>平台</th><th>状态</th><th>开始时间</th><th>操作</th></tr>';
|
|
488
791
|
d.debugSessions.slice(0,10).forEach(s=>{
|
|
489
|
-
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"
|
|
792
|
+
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" data-type="/debug-detail" data-id="'+esc(s.sessionId)+'" onclick="openDetail(this)">日志</button></td></tr>';
|
|
490
793
|
});
|
|
491
|
-
h+='</table>';
|
|
794
|
+
h+='</table></div>';
|
|
492
795
|
}
|
|
493
796
|
|
|
494
|
-
h+='<div id="detail-panel"></div>';
|
|
495
797
|
p.innerHTML=h;
|
|
496
798
|
}
|
|
497
799
|
|
|
498
800
|
// ===== Devices =====
|
|
499
|
-
async function
|
|
801
|
+
async function loadDevicesData(){
|
|
500
802
|
const devs=await api('/devices');
|
|
501
|
-
if(!devs)return;
|
|
502
|
-
|
|
803
|
+
if(!devs)return[];
|
|
804
|
+
allDevices=devs;
|
|
805
|
+
return devs;
|
|
806
|
+
}
|
|
807
|
+
function loadDevices(page){
|
|
808
|
+
if(page)devicePage=page;
|
|
809
|
+
const devs=allDevices;
|
|
503
810
|
const online=devs.filter(d=>d.status==='online').length;
|
|
811
|
+
const p=document.getElementById('page-devices');
|
|
504
812
|
let h='<div class="grid">';
|
|
505
813
|
h+='<div class="card"><h3>设备总数</h3><div class="value">'+devs.length+'</div></div>';
|
|
506
814
|
h+='<div class="card"><h3>在线</h3><div class="value">'+online+'</div></div>';
|
|
@@ -508,11 +816,13 @@ async function loadDevices(){
|
|
|
508
816
|
h+='</div>';
|
|
509
817
|
|
|
510
818
|
if(devs.length>0){
|
|
511
|
-
|
|
512
|
-
devs.
|
|
819
|
+
const slice=paginate(devs,devicePage);
|
|
820
|
+
h+='<div class="section-title">所有设备</div>'+renderPager('dev',devs.length,devicePage,'loadDevices');
|
|
821
|
+
h+='<div class="table-wrap"><table><tr><th>序列号</th><th>型号</th><th>品牌</th><th>平台</th><th>状态</th><th>最近心跳</th><th>创建时间</th></tr>';
|
|
822
|
+
slice.forEach(d=>{
|
|
513
823
|
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>';
|
|
514
824
|
});
|
|
515
|
-
h+='</table>';
|
|
825
|
+
h+='</table></div>';
|
|
516
826
|
}else{
|
|
517
827
|
h+='<div class="empty-state"><div class="icon">📱</div><p>暂无设备</p></div>';
|
|
518
828
|
}
|
|
@@ -520,10 +830,15 @@ async function loadDevices(){
|
|
|
520
830
|
}
|
|
521
831
|
|
|
522
832
|
// ===== Tasks =====
|
|
523
|
-
async function
|
|
524
|
-
const tasks=await api('/tasks?limit=
|
|
525
|
-
if(!tasks)return;
|
|
526
|
-
|
|
833
|
+
async function loadTasksData(){
|
|
834
|
+
const tasks=await api('/tasks?limit=200');
|
|
835
|
+
if(!tasks)return[];
|
|
836
|
+
allTasks=tasks;
|
|
837
|
+
return tasks;
|
|
838
|
+
}
|
|
839
|
+
function loadTasks(page){
|
|
840
|
+
if(page)taskPage=page;
|
|
841
|
+
const tasks=allTasks;
|
|
527
842
|
const running=tasks.filter(t=>t.status==='running').length;
|
|
528
843
|
const completed=tasks.filter(t=>t.status==='completed').length;
|
|
529
844
|
const failed=tasks.filter(t=>t.status==='failed').length;
|
|
@@ -536,91 +851,31 @@ async function loadTasks(){
|
|
|
536
851
|
h+='</div>';
|
|
537
852
|
|
|
538
853
|
if(tasks.length>0){
|
|
539
|
-
|
|
540
|
-
tasks.
|
|
854
|
+
const slice=paginate(tasks,taskPage);
|
|
855
|
+
h+='<div class="section-title">所有任务</div>'+renderPager('task',tasks.length,taskPage,'loadTasks');
|
|
856
|
+
h+='<div class="table-wrap"><table><tr><th>任务ID</th><th>执行ID</th><th>类型</th><th>状态</th><th>优先级</th><th>开始时间</th><th>完成时间</th><th>错误</th><th>操作</th></tr>';
|
|
857
|
+
slice.forEach(t=>{
|
|
541
858
|
const err=t.errorMessage?(t.errorMessage.length>50?esc(t.errorMessage.substring(0,50))+'...':esc(t.errorMessage)):'-';
|
|
542
|
-
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"
|
|
859
|
+
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" data-type="/task-detail" data-id="'+esc(t.taskId)+'" onclick="openDetail(this)">详情</button></td></tr>';
|
|
543
860
|
});
|
|
544
|
-
h+='</table>';
|
|
861
|
+
h+='</table></div>';
|
|
545
862
|
}else{
|
|
546
863
|
h+='<div class="empty-state"><div class="icon">📋</div><p>暂无任务</p></div>';
|
|
547
864
|
}
|
|
548
|
-
|
|
865
|
+
const p=document.getElementById('page-tasks');
|
|
549
866
|
p.innerHTML=h;
|
|
550
867
|
}
|
|
551
868
|
|
|
552
|
-
async function showTaskDetail(taskId){
|
|
553
|
-
detailOpen=true;
|
|
554
|
-
currentDetailTaskId=taskId;
|
|
555
|
-
currentDetailTaskStatus=null;
|
|
556
|
-
const task=await api('/tasks/'+encodeURIComponent(taskId));
|
|
557
|
-
const logs=await api('/tasks/'+encodeURIComponent(taskId)+'/logs');
|
|
558
|
-
const panel=document.getElementById('task-detail');
|
|
559
|
-
if(!task){panel.innerHTML='<p style="color:#ef4444">任务未找到</p>';detailOpen=false;currentDetailTaskId=null;return}
|
|
560
|
-
|
|
561
|
-
currentDetailTaskStatus=task.status;
|
|
562
|
-
const isRunning=task.status==='running'||task.status==='pending';
|
|
563
|
-
|
|
564
|
-
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>';
|
|
565
|
-
h+='<div class="detail-row"><div class="detail-label">任务ID</div><div class="detail-value mono">'+esc(task.taskId)+'</div></div>';
|
|
566
|
-
h+='<div class="detail-row"><div class="detail-label">执行ID</div><div class="detail-value mono">'+esc(task.executionId||'-')+'</div></div>';
|
|
567
|
-
h+='<div class="detail-row"><div class="detail-label">类型</div><div class="detail-value">'+esc(task.type)+'</div></div>';
|
|
568
|
-
h+='<div class="detail-row"><div class="detail-label">状态</div><div class="detail-value">'+badge(task.status)+'</div></div>';
|
|
569
|
-
h+='<div class="detail-row"><div class="detail-label">优先级</div><div class="detail-value">'+task.priority+'</div></div>';
|
|
570
|
-
h+='<div class="detail-row"><div class="detail-label">开始时间</div><div class="detail-value">'+fmtTime(task.startedAt)+'</div></div>';
|
|
571
|
-
h+='<div class="detail-row"><div class="detail-label">完成时间</div><div class="detail-value">'+fmtTime(task.completedAt)+'</div></div>';
|
|
572
|
-
h+='<div class="detail-row"><div class="detail-label">创建时间</div><div class="detail-value">'+fmtTime(task.createdAt)+'</div></div>';
|
|
573
|
-
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>';
|
|
574
|
-
if(task.config){
|
|
575
|
-
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>';
|
|
576
|
-
}
|
|
577
|
-
if(task.result){
|
|
578
|
-
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>';
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// Execution logs with real-time support
|
|
582
|
-
const logSafeId=esc(taskId).replace(/[^a-zA-Z0-9-_]/g,'_');
|
|
583
|
-
const existingLogsCount=logs?logs.length:0;
|
|
584
|
-
if(existingLogsCount>0||isRunning){
|
|
585
|
-
h+='<div class="section-title" style="margin-top:20px">执行日志 <span id="task-log-count-'+logSafeId+'" style="color:#64748b;font-size:12px">'+(existingLogsCount||'')+'</span>';
|
|
586
|
-
if(isRunning)h+=' <span style="color:#22c55e;font-size:12px">● LIVE</span>';
|
|
587
|
-
h+='</div>';
|
|
588
|
-
h+='<div class="log-viewer" id="task-log-'+logSafeId+'" style="max-height:400px">';
|
|
589
|
-
if(logs&&logs.length>0){
|
|
590
|
-
logs.forEach(l=>{
|
|
591
|
-
const lvl='log-level-'+esc(l.level||'info');
|
|
592
|
-
h+='<div class="log-line"><span class="log-time">'+fmtTime(l.createdAt)+'</span><span class="'+lvl+'">['+esc(l.level||'info').toUpperCase()+']</span> <span class="log-content">'+esc(l.content)+'</span></div>';
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
h+='</div>';
|
|
596
|
-
}else{
|
|
597
|
-
h+='<div class="section-title" style="margin-top:20px">执行日志</div><p style="color:#64748b">暂无日志</p>';
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
h+='</div>';
|
|
601
|
-
panel.innerHTML=h;
|
|
602
|
-
panel.scrollIntoView({behavior:'smooth'});
|
|
603
|
-
|
|
604
|
-
// 滚动到日志底部
|
|
605
|
-
const logViewer=document.getElementById('task-log-'+logSafeId);
|
|
606
|
-
if(logViewer)logViewer.scrollTop=logViewer.scrollHeight;
|
|
607
|
-
|
|
608
|
-
// 订阅实时日志
|
|
609
|
-
if(isRunning){
|
|
610
|
-
subscribeTaskLogs(taskId);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function showTaskLogs(taskId){
|
|
615
|
-
if(currentTab!=='tasks'){switchTab('tasks');setTimeout(()=>showTaskDetail(taskId),500);return}
|
|
616
|
-
showTaskDetail(taskId);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
869
|
// ===== Debug Sessions =====
|
|
620
|
-
async function
|
|
870
|
+
async function loadDebugData(){
|
|
621
871
|
const sessions=await api('/debug/sessions');
|
|
622
|
-
if(!sessions)return;
|
|
623
|
-
|
|
872
|
+
if(!sessions)return[];
|
|
873
|
+
allDebugSessions=sessions;
|
|
874
|
+
return sessions;
|
|
875
|
+
}
|
|
876
|
+
function loadDebug(page){
|
|
877
|
+
if(page)debugPage=page;
|
|
878
|
+
const sessions=allDebugSessions;
|
|
624
879
|
const running=sessions.filter(s=>s.status==='running').length;
|
|
625
880
|
|
|
626
881
|
let h='<div class="grid">';
|
|
@@ -629,80 +884,208 @@ async function loadDebug(){
|
|
|
629
884
|
h+='</div>';
|
|
630
885
|
|
|
631
886
|
if(sessions.length>0){
|
|
632
|
-
|
|
633
|
-
sessions.
|
|
634
|
-
|
|
887
|
+
const slice=paginate(sessions,debugPage);
|
|
888
|
+
h+='<div class="section-title">所有调试会话</div>'+renderPager('dbg',sessions.length,debugPage,'loadDebug');
|
|
889
|
+
h+='<div class="table-wrap"><table><tr><th>会话ID</th><th>任务ID</th><th>设备</th><th>平台</th><th>状态</th><th>开始时间</th><th>完成时间</th><th>操作</th></tr>';
|
|
890
|
+
slice.forEach(s=>{
|
|
891
|
+
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" data-type="/debug-detail" data-id="'+esc(s.sessionId)+'" onclick="openDetail(this)">详情</button></td></tr>';
|
|
635
892
|
});
|
|
636
|
-
h+='</table>';
|
|
893
|
+
h+='</table></div>';
|
|
637
894
|
}else{
|
|
638
895
|
h+='<div class="empty-state"><div class="icon">🐛</div><p>暂无调试会话</p></div>';
|
|
639
896
|
}
|
|
640
|
-
|
|
897
|
+
const p=document.getElementById('page-debug');
|
|
641
898
|
p.innerHTML=h;
|
|
642
899
|
}
|
|
643
900
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
h
|
|
653
|
-
h+='<
|
|
654
|
-
h+='<
|
|
655
|
-
h+='<
|
|
656
|
-
h+='<
|
|
657
|
-
h+='
|
|
658
|
-
h
|
|
901
|
+
// ===== Pagination State =====
|
|
902
|
+
const PAGE_SIZE=20;
|
|
903
|
+
let taskPage=1, devicePage=1, debugPage=1;
|
|
904
|
+
let allTasks=[], allDevices=[], allDebugSessions=[];
|
|
905
|
+
|
|
906
|
+
function renderPager(prefix,total,page,loadFn){
|
|
907
|
+
const pages=Math.ceil(total/PAGE_SIZE)||1;
|
|
908
|
+
if(total<=PAGE_SIZE)return '<div class="pager"><div class="info">共 '+total+' 条</div></div>';
|
|
909
|
+
let h='<div class="pager"><div class="info">共 '+total+' 条,第 '+page+'/'+pages+' 页</div><div>';
|
|
910
|
+
if(page>1)h+='<button class="btn btn-primary btn-sm" onclick="'+loadFn+'(1)">首页</button>';
|
|
911
|
+
if(page>1)h+='<button class="btn btn-primary btn-sm" onclick="'+loadFn+'('+(page-1)+')">上一页</button>';
|
|
912
|
+
if(page<pages)h+='<button class="btn btn-primary btn-sm" onclick="'+loadFn+'('+(page+1)+')">下一页</button>';
|
|
913
|
+
if(page<pages)h+='<button class="btn btn-primary btn-sm" onclick="'+loadFn+'('+pages+')">末页</button>';
|
|
914
|
+
h+='</div></div>';
|
|
915
|
+
return h;
|
|
916
|
+
}
|
|
917
|
+
function paginate(arr,page){const s=(page-1)*PAGE_SIZE;return arr.slice(s,s+PAGE_SIZE)}
|
|
918
|
+
|
|
919
|
+
// ===== Init =====
|
|
920
|
+
initWebSocket(); // 初始化 WebSocket 连接以接收实时日志
|
|
921
|
+
refresh();
|
|
922
|
+
refreshTimer=setInterval(()=>{if(autoRefresh)refresh()},5000);
|
|
923
|
+
</script>
|
|
924
|
+
</body>
|
|
925
|
+
</html>`;
|
|
926
|
+
}
|
|
927
|
+
getTaskDetailHtml() {
|
|
928
|
+
return `<!DOCTYPE html>
|
|
929
|
+
<html lang="zh-CN">
|
|
930
|
+
<head>
|
|
931
|
+
<meta charset="UTF-8">
|
|
932
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
933
|
+
<title>任务详情</title>
|
|
934
|
+
<style>
|
|
935
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
936
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f8fafc;color:#1e293b;padding:24px}
|
|
937
|
+
h2{font-size:18px;font-weight:600;margin-bottom:16px;display:flex;align-items:center;gap:8px}
|
|
938
|
+
.badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:11px;font-weight:600}
|
|
939
|
+
.b-running{background:#fef3c7;color:#d97706}.b-completed{background:#dcfce7;color:#16a34a}.b-failed{background:#fee2e2;color:#dc2626}.b-pending{background:#f1f5f9;color:#64748b}.b-unknown{background:#f1f5f9;color:#64748b}
|
|
940
|
+
.section{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px}
|
|
941
|
+
.section-title{font-size:13px;font-weight:600;color:#64748b;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px}
|
|
942
|
+
.row{display:flex;gap:12px;padding:8px 0;border-bottom:1px solid #f1f5f9}
|
|
943
|
+
.row:last-child{border-bottom:none}
|
|
944
|
+
.label{width:100px;color:#64748b;font-size:13px;flex-shrink:0}
|
|
945
|
+
.value{color:#1e293b;font-size:13px;word-break:break-all;flex:1}
|
|
946
|
+
.mono{font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px}
|
|
947
|
+
.log-viewer{max-height:500px;overflow-y:auto;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:12px}
|
|
948
|
+
.log-line{padding:4px 0;border-bottom:1px solid #f1f5f9;font-size:12px;line-height:1.5}
|
|
949
|
+
.log-time{color:#94a3b8;margin-right:8px;font-size:11px}
|
|
950
|
+
.log-level-info{color:#0284c7}.log-level-warn{color:#d97706}.log-level-error{color:#dc2626}
|
|
951
|
+
.log-content{color:#475569}
|
|
952
|
+
.loading{text-align:center;padding:60px;color:#94a3b8}
|
|
953
|
+
.error{color:#dc2626;padding:20px}
|
|
954
|
+
</style>
|
|
955
|
+
</head>
|
|
956
|
+
<body>
|
|
957
|
+
<h2>📋 任务详情</h2>
|
|
958
|
+
<div id="content"><div class="loading">加载中...</div></div>
|
|
959
|
+
<script>
|
|
960
|
+
const params=new URLSearchParams(location.search);
|
|
961
|
+
const taskId=params.get('id');
|
|
962
|
+
if(!taskId){document.getElementById('content').innerHTML='<div class="error">缺少任务ID</div>';}
|
|
963
|
+
|
|
964
|
+
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
|
965
|
+
|
|
966
|
+
function fmtTime(d){if(!d)return '-';return new Date(d).toLocaleString('zh-CN',{hour12:false})}
|
|
967
|
+
function badge(s){const cls='b-'+(s||'unknown');return '<span class="badge '+cls+'">'+esc(s)+'</span>'}
|
|
968
|
+
|
|
969
|
+
async function load(){
|
|
970
|
+
const task=await (await fetch('/api/tasks/'+encodeURIComponent(taskId))).json();
|
|
971
|
+
const logs=await (await fetch('/api/tasks/'+encodeURIComponent(taskId)+'/logs')).json();
|
|
972
|
+
const c=document.getElementById('content');
|
|
973
|
+
if(!task){c.innerHTML='<div class="error">任务未找到</div>';return}
|
|
974
|
+
|
|
975
|
+
document.title='任务详情: '+task.taskId;
|
|
976
|
+
let h='<div class="section"><div class="section-title">基本信息</div>';
|
|
977
|
+
h+='<div class="row"><div class="label">任务ID</div><div class="value mono">'+esc(task.taskId)+'</div></div>';
|
|
978
|
+
h+='<div class="row"><div class="label">执行ID</div><div class="value mono">'+esc(task.executionId||'-')+'</div></div>';
|
|
979
|
+
h+='<div class="row"><div class="label">类型</div><div class="value">'+esc(task.type)+'</div></div>';
|
|
980
|
+
h+='<div class="row"><div class="label">状态</div><div class="value">'+badge(task.status)+'</div></div>';
|
|
981
|
+
h+='<div class="row"><div class="label">优先级</div><div class="value">'+task.priority+'</div></div>';
|
|
982
|
+
h+='<div class="row"><div class="label">开始时间</div><div class="value">'+fmtTime(task.startedAt)+'</div></div>';
|
|
983
|
+
h+='<div class="row"><div class="label">完成时间</div><div class="value">'+fmtTime(task.completedAt)+'</div></div>';
|
|
984
|
+
h+='<div class="row"><div class="label">创建时间</div><div class="value">'+fmtTime(task.createdAt)+'</div></div>';
|
|
985
|
+
if(task.errorMessage)h+='<div class="row"><div class="label">错误信息</div><div class="value" style="color:#dc2626">'+esc(task.errorMessage)+'</div></div>';
|
|
659
986
|
h+='</div>';
|
|
660
987
|
|
|
661
|
-
|
|
988
|
+
if(task.config){
|
|
989
|
+
h+='<div class="section"><div class="section-title">配置</div><pre class="mono" style="white-space:pre-wrap;font-size:11px;background:#f8fafc;padding:12px;border-radius:4px">'+esc(typeof task.config==='object'?JSON.stringify(task.config,null,2):task.config)+'</pre></div>';
|
|
990
|
+
}
|
|
991
|
+
if(task.result){
|
|
992
|
+
h+='<div class="section"><div class="section-title">结果</div><pre class="mono" style="white-space:pre-wrap;font-size:11px;background:#f8fafc;padding:12px;border-radius:4px">'+esc(typeof task.result==='object'?JSON.stringify(task.result,null,2):task.result)+'</pre></div>';
|
|
993
|
+
}
|
|
994
|
+
|
|
662
995
|
if(logs&&logs.length>0){
|
|
663
|
-
h+='<div class="
|
|
664
|
-
h+='<div class="log-viewer">';
|
|
996
|
+
h+='<div class="section"><div class="section-title">执行日志 ('+logs.length+')</div><div class="log-viewer">';
|
|
665
997
|
logs.forEach(l=>{
|
|
666
|
-
const lvl='log-level-'+esc(l.
|
|
667
|
-
|
|
668
|
-
h+='<div class="log-line"><span class="log-time">'+fmtTime(l.createdAt)+'</span> <span class="log-content">'+esc(content)+'</span></div>';
|
|
998
|
+
const lvl='log-level-'+esc(l.level||'info');
|
|
999
|
+
h+='<div class="log-line"><span class="log-time">'+fmtTime(l.createdAt)+'</span><span class="'+lvl+'">['+esc(l.level||'info').toUpperCase()+']</span> <span class="log-content">'+esc(l.content)+'</span></div>';
|
|
669
1000
|
});
|
|
670
1001
|
h+='</div></div>';
|
|
671
1002
|
}else{
|
|
672
|
-
h+='<div class="
|
|
1003
|
+
h+='<div class="section"><div class="section-title">执行日志</div><p style="color:#94a3b8">暂无日志</p></div>';
|
|
673
1004
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1005
|
+
c.innerHTML=h;
|
|
1006
|
+
const lv=c.querySelector('.log-viewer');
|
|
1007
|
+
if(lv)lv.scrollTop=lv.scrollHeight;
|
|
677
1008
|
}
|
|
1009
|
+
load();
|
|
1010
|
+
</script>
|
|
1011
|
+
</body>
|
|
1012
|
+
</html>`;
|
|
1013
|
+
}
|
|
1014
|
+
getDebugDetailHtml() {
|
|
1015
|
+
return `<!DOCTYPE html>
|
|
1016
|
+
<html lang="zh-CN">
|
|
1017
|
+
<head>
|
|
1018
|
+
<meta charset="UTF-8">
|
|
1019
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
1020
|
+
<title>调试会话详情</title>
|
|
1021
|
+
<style>
|
|
1022
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
1023
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f8fafc;color:#1e293b;padding:24px}
|
|
1024
|
+
h2{font-size:18px;font-weight:600;margin-bottom:16px;display:flex;align-items:center;gap:8px}
|
|
1025
|
+
.badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:11px;font-weight:600}
|
|
1026
|
+
.b-running{background:#fef3c7;color:#d97706}.b-completed{background:#dcfce7;color:#16a34a}.b-failed{background:#fee2e2;color:#dc2626}.b-pending{background:#f1f5f9;color:#64748b}.b-unknown{background:#f1f5f9;color:#64748b}
|
|
1027
|
+
.section{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px}
|
|
1028
|
+
.section-title{font-size:13px;font-weight:600;color:#64748b;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px}
|
|
1029
|
+
.row{display:flex;gap:12px;padding:8px 0;border-bottom:1px solid #f1f5f9}
|
|
1030
|
+
.row:last-child{border-bottom:none}
|
|
1031
|
+
.label{width:100px;color:#64748b;font-size:13px;flex-shrink:0}
|
|
1032
|
+
.value{color:#1e293b;font-size:13px;word-break:break-all;flex:1}
|
|
1033
|
+
.mono{font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px}
|
|
1034
|
+
.log-viewer{max-height:500px;overflow-y:auto;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:12px}
|
|
1035
|
+
.log-line{padding:4px 0;border-bottom:1px solid #f1f5f9;font-size:12px;line-height:1.5}
|
|
1036
|
+
.log-time{color:#94a3b8;margin-right:8px;font-size:11px}
|
|
1037
|
+
.log-level-info{color:#0284c7}.log-level-warn{color:#d97706}.log-level-error{color:#dc2626}
|
|
1038
|
+
.log-content{color:#475569}
|
|
1039
|
+
.loading{text-align:center;padding:60px;color:#94a3b8}
|
|
1040
|
+
.error{color:#dc2626;padding:20px}
|
|
1041
|
+
</style>
|
|
1042
|
+
</head>
|
|
1043
|
+
<body>
|
|
1044
|
+
<h2>🐛 调试会话详情</h2>
|
|
1045
|
+
<div id="content"><div class="loading">加载中...</div></div>
|
|
1046
|
+
<script>
|
|
1047
|
+
const params=new URLSearchParams(location.search);
|
|
1048
|
+
const sessionId=params.get('id');
|
|
1049
|
+
if(!sessionId){document.getElementById('content').innerHTML='<div class="error">缺少会话ID</div>';}
|
|
678
1050
|
|
|
679
|
-
function
|
|
680
|
-
if(currentTab!=='debug'){switchTab('debug');setTimeout(()=>showDebugDetail(sessionId),500);return}
|
|
681
|
-
showDebugDetail(sessionId);
|
|
682
|
-
}
|
|
1051
|
+
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
|
683
1052
|
|
|
684
|
-
function
|
|
685
|
-
|
|
686
|
-
if(currentDetailTaskId&&(currentDetailTaskStatus==='running'||currentDetailTaskStatus==='pending')){
|
|
687
|
-
unsubscribeTaskLogs(currentDetailTaskId);
|
|
688
|
-
}
|
|
689
|
-
const panel=document.getElementById('task-detail');
|
|
690
|
-
if(panel)panel.innerHTML='';
|
|
691
|
-
detailOpen=false;
|
|
692
|
-
currentDetailTaskId=null;
|
|
693
|
-
currentDetailTaskStatus=null;
|
|
694
|
-
}
|
|
1053
|
+
function fmtTime(d){if(!d)return '-';return new Date(d).toLocaleString('zh-CN',{hour12:false})}
|
|
1054
|
+
function badge(s){const cls='b-'+(s||'unknown');return '<span class="badge '+cls+'">'+esc(s)+'</span>'}
|
|
695
1055
|
|
|
696
|
-
function
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
1056
|
+
async function load(){
|
|
1057
|
+
const session=await (await fetch('/api/debug/sessions/'+encodeURIComponent(sessionId))).json();
|
|
1058
|
+
const logs=await (await fetch('/api/debug/sessions/'+encodeURIComponent(sessionId)+'/logs')).json();
|
|
1059
|
+
const c=document.getElementById('content');
|
|
1060
|
+
if(!session){c.innerHTML='<div class="error">会话未找到</div>';return}
|
|
701
1061
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1062
|
+
document.title='调试会话详情: '+session.sessionId;
|
|
1063
|
+
let h='<div class="section"><div class="section-title">基本信息</div>';
|
|
1064
|
+
h+='<div class="row"><div class="label">会话ID</div><div class="value mono">'+esc(session.sessionId)+'</div></div>';
|
|
1065
|
+
h+='<div class="row"><div class="label">任务ID</div><div class="value mono">'+esc(session.taskId||'-')+'</div></div>';
|
|
1066
|
+
h+='<div class="row"><div class="label">设备</div><div class="value mono">'+esc(session.deviceId||'-')+'</div></div>';
|
|
1067
|
+
h+='<div class="row"><div class="label">平台</div><div class="value">'+esc(session.platform||'-')+'</div></div>';
|
|
1068
|
+
h+='<div class="row"><div class="label">状态</div><div class="value">'+badge(session.status)+'</div></div>';
|
|
1069
|
+
h+='<div class="row"><div class="label">开始时间</div><div class="value">'+fmtTime(session.startedAt)+'</div></div>';
|
|
1070
|
+
h+='<div class="row"><div class="label">完成时间</div><div class="value">'+fmtTime(session.completedAt)+'</div></div>';
|
|
1071
|
+
h+='</div>';
|
|
1072
|
+
|
|
1073
|
+
if(logs&&logs.length>0){
|
|
1074
|
+
h+='<div class="section"><div class="section-title">会话日志 ('+logs.length+')</div><div class="log-viewer">';
|
|
1075
|
+
logs.forEach(l=>{
|
|
1076
|
+
const lvl='log-level-'+(l.type==='error'?'error':'info');
|
|
1077
|
+
const content=l.content||'';
|
|
1078
|
+
h+='<div class="log-line"><span class="log-time">'+fmtTime(l.createdAt)+'</span><span class="'+lvl+'">['+esc(l.type==='log_output'?'info':l.type==='error'?'error':'info').toUpperCase()+']</span> <span class="log-content">'+esc(content)+'</span></div>';
|
|
1079
|
+
});
|
|
1080
|
+
h+='</div></div>';
|
|
1081
|
+
}else{
|
|
1082
|
+
h+='<div class="section"><div class="section-title">会话日志</div><p style="color:#94a3b8">暂无日志</p></div>';
|
|
1083
|
+
}
|
|
1084
|
+
c.innerHTML=h;
|
|
1085
|
+
const lv=c.querySelector('.log-viewer');
|
|
1086
|
+
if(lv)lv.scrollTop=lv.scrollHeight;
|
|
1087
|
+
}
|
|
1088
|
+
load();
|
|
706
1089
|
</script>
|
|
707
1090
|
</body>
|
|
708
1091
|
</html>`;
|