validpilot-oss 1.1.0 → 1.1.1
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/CHANGELOG.md +10 -0
- package/hands/verification_runner.js +200 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.1.1] - 2026-06-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- 修复 CLI 运行时报错 `Cannot find module '../hands/verification_runner'` 的问题
|
|
10
|
+
- 新增 `hands/verification_runner.js` 模块,实现独立的 `validationQuickRun` 功能
|
|
11
|
+
- 支持 7 项快速检查:load_time / no_js_errors / no_5xx / no_404 / not_blank / has_title / has_content
|
|
12
|
+
- 修复 package.json 中 `files` 字段引用不存在的 `scripts/` 目录
|
|
13
|
+
- 修复 package.json 中 `scripts.check` 指向不存在文件的问题
|
|
14
|
+
|
|
5
15
|
## [1.1.0] - 2026-06-28
|
|
6
16
|
|
|
7
17
|
### Added
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { PlaywrightAdapter } = require('../engines/playwright_adapter');
|
|
5
|
+
const { redact } = require('../core/redaction');
|
|
6
|
+
|
|
7
|
+
async function validationQuickRun(args = {}) {
|
|
8
|
+
const startTime = Date.now();
|
|
9
|
+
const timeout = args.timeout || 30000;
|
|
10
|
+
const url = args.url;
|
|
11
|
+
if (!url) throw new Error('url 参数必填');
|
|
12
|
+
|
|
13
|
+
const allChecks = ['load_time', 'no_js_errors', 'no_5xx', 'no_404', 'not_blank', 'has_title', 'has_content'];
|
|
14
|
+
const requestedChecks = Array.isArray(args.checks) && args.checks.length > 0 ? args.checks : allChecks;
|
|
15
|
+
const checksToRun = requestedChecks.filter(c => allChecks.includes(c));
|
|
16
|
+
|
|
17
|
+
const adapter = new PlaywrightAdapter({ headless: args.headless !== false });
|
|
18
|
+
const checks = [];
|
|
19
|
+
let loadTime = 0;
|
|
20
|
+
let screenshotPath = '';
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const navStart = Date.now();
|
|
24
|
+
await adapter.open({ url, timeout, waitUntil: 'domcontentloaded' });
|
|
25
|
+
loadTime = Date.now() - navStart;
|
|
26
|
+
|
|
27
|
+
if (checksToRun.includes('load_time')) {
|
|
28
|
+
checks.push({ name: 'load_time', passed: true, detail: `页面加载成功,耗时 ${loadTime}ms` });
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
loadTime = Date.now() - startTime;
|
|
32
|
+
if (checksToRun.includes('load_time')) {
|
|
33
|
+
checks.push({ name: 'load_time', passed: false, detail: `页面加载失败: ${error.message}` });
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const shot = await adapter.screenshot({ name: 'quick-run-error' });
|
|
37
|
+
screenshotPath = shot.artifactPath || '';
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// ignore
|
|
40
|
+
}
|
|
41
|
+
const duration = Date.now() - startTime;
|
|
42
|
+
const remaining = checksToRun.filter(c => !checks.find(ch => ch.name === c)).map(name => ({
|
|
43
|
+
name,
|
|
44
|
+
passed: false,
|
|
45
|
+
detail: '页面加载失败,无法执行后续检查'
|
|
46
|
+
}));
|
|
47
|
+
const result = redact({
|
|
48
|
+
pass: false,
|
|
49
|
+
mode: 'quick',
|
|
50
|
+
url,
|
|
51
|
+
loadTime,
|
|
52
|
+
totalChecks: checksToRun.length,
|
|
53
|
+
passedChecks: 0,
|
|
54
|
+
failedChecks: checksToRun.length,
|
|
55
|
+
checks: checks.concat(remaining),
|
|
56
|
+
errors: getErrorsFromAdapter(adapter),
|
|
57
|
+
screenshot: screenshotPath,
|
|
58
|
+
duration,
|
|
59
|
+
summary: `页面加载失败: ${error.message}`,
|
|
60
|
+
topErrors: [{ message: error.message, source: 'load' }],
|
|
61
|
+
artifacts: screenshotPath ? [screenshotPath] : [],
|
|
62
|
+
timestamp: new Date().toISOString()
|
|
63
|
+
});
|
|
64
|
+
await adapter.close().catch(() => {});
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (checksToRun.includes('no_js_errors')) {
|
|
69
|
+
const consoleErrors = adapter.consoleLogs.filter(e => e.type === 'error');
|
|
70
|
+
const hasJsErrors = consoleErrors.length > 0 || adapter.pageErrors.length > 0;
|
|
71
|
+
checks.push({
|
|
72
|
+
name: 'no_js_errors',
|
|
73
|
+
passed: !hasJsErrors,
|
|
74
|
+
detail: hasJsErrors
|
|
75
|
+
? `检测到 ${consoleErrors.length} 个 console.error 和 ${adapter.pageErrors.length} 个 pageerror`
|
|
76
|
+
: '无 JS 错误'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (checksToRun.includes('no_5xx')) {
|
|
81
|
+
const serverErrors = adapter.networkLogs.filter(e => e.status >= 500 && e.status < 600);
|
|
82
|
+
checks.push({
|
|
83
|
+
name: 'no_5xx',
|
|
84
|
+
passed: serverErrors.length === 0,
|
|
85
|
+
detail: serverErrors.length === 0
|
|
86
|
+
? '无 5xx 服务器错误'
|
|
87
|
+
: `检测到 ${serverErrors.length} 个 5xx 错误: ${serverErrors.slice(0, 3).map(e => `${e.status} ${e.url}`).join('; ')}`
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (checksToRun.includes('no_404')) {
|
|
92
|
+
const notFoundErrors = adapter.networkLogs.filter(e => e.status === 404);
|
|
93
|
+
checks.push({
|
|
94
|
+
name: 'no_404',
|
|
95
|
+
passed: notFoundErrors.length === 0,
|
|
96
|
+
detail: notFoundErrors.length === 0
|
|
97
|
+
? '无 404 错误'
|
|
98
|
+
: `检测到 ${notFoundErrors.length} 个 404 错误: ${notFoundErrors.slice(0, 3).map(e => e.url).join('; ')}`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let domInfo = { bodyTextLength: 0, imgCount: 0, linkCount: 0, buttonCount: 0, title: '' };
|
|
103
|
+
try {
|
|
104
|
+
const evalResult = await adapter.eval({
|
|
105
|
+
expression: `(() => { const bodyText = document.body?.innerText || ''; const imgCount = document.querySelectorAll('img').length; const linkCount = document.querySelectorAll('a[href]').length; const buttonCount = document.querySelectorAll('button, [role="button"], input[type="button"], input[type="submit"]').length; const title = document.title || ''; return { bodyTextLength: bodyText.length, imgCount, linkCount, buttonCount, title }; })()`
|
|
106
|
+
});
|
|
107
|
+
if (evalResult.result) {
|
|
108
|
+
domInfo = evalResult.result;
|
|
109
|
+
}
|
|
110
|
+
} catch (e) {
|
|
111
|
+
// ignore
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (checksToRun.includes('not_blank')) {
|
|
115
|
+
const hasContent = domInfo.bodyTextLength > 50 && (domInfo.imgCount > 0 || domInfo.linkCount > 0 || domInfo.buttonCount > 0);
|
|
116
|
+
checks.push({
|
|
117
|
+
name: 'not_blank',
|
|
118
|
+
passed: hasContent,
|
|
119
|
+
detail: hasContent
|
|
120
|
+
? `页面有实际内容(文本长度: ${domInfo.bodyTextLength},图片: ${domInfo.imgCount},链接: ${domInfo.linkCount},按钮: ${domInfo.buttonCount})`
|
|
121
|
+
: `页面疑似白屏(文本长度: ${domInfo.bodyTextLength},图片: ${domInfo.imgCount},链接: ${domInfo.linkCount},按钮: ${domInfo.buttonCount})`
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (checksToRun.includes('has_title')) {
|
|
126
|
+
const hasTitle = domInfo.title && domInfo.title.trim().length > 0;
|
|
127
|
+
checks.push({
|
|
128
|
+
name: 'has_title',
|
|
129
|
+
passed: hasTitle,
|
|
130
|
+
detail: hasTitle ? `页面标题: ${domInfo.title}` : '页面无标题或标题为空'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (checksToRun.includes('has_content')) {
|
|
135
|
+
const hasMainContent = domInfo.imgCount > 0 || domInfo.linkCount >= 3 || domInfo.buttonCount > 0;
|
|
136
|
+
checks.push({
|
|
137
|
+
name: 'has_content',
|
|
138
|
+
passed: hasMainContent,
|
|
139
|
+
detail: hasMainContent
|
|
140
|
+
? `页面有主要内容元素(图片: ${domInfo.imgCount},链接: ${domInfo.linkCount},按钮: ${domInfo.buttonCount})`
|
|
141
|
+
: `页面缺少主要内容元素(图片: ${domInfo.imgCount},链接: ${domInfo.linkCount},按钮: ${domInfo.buttonCount})`
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const shot = await adapter.screenshot({ name: 'quick-run' });
|
|
147
|
+
screenshotPath = shot.artifactPath || '';
|
|
148
|
+
} catch (e) {
|
|
149
|
+
// ignore
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const passedChecks = checks.filter(c => c.passed).length;
|
|
153
|
+
const failedChecks = checks.filter(c => !c.passed).length;
|
|
154
|
+
const pass = failedChecks === 0;
|
|
155
|
+
const duration = Date.now() - startTime;
|
|
156
|
+
|
|
157
|
+
const errors = getErrorsFromAdapter(adapter);
|
|
158
|
+
const topErrors = errors.slice(0, 5).map(e => ({ message: e.text || e.detail, source: e.source }));
|
|
159
|
+
|
|
160
|
+
const result = redact({
|
|
161
|
+
pass,
|
|
162
|
+
mode: 'quick',
|
|
163
|
+
url,
|
|
164
|
+
loadTime,
|
|
165
|
+
totalChecks: checks.length,
|
|
166
|
+
passedChecks,
|
|
167
|
+
failedChecks,
|
|
168
|
+
checks,
|
|
169
|
+
errors,
|
|
170
|
+
screenshot: screenshotPath,
|
|
171
|
+
duration,
|
|
172
|
+
summary: pass
|
|
173
|
+
? `所有 ${checks.length} 项检查通过,加载耗时 ${loadTime}ms`
|
|
174
|
+
: `${failedChecks} 项检查失败,加载耗时 ${loadTime}ms`,
|
|
175
|
+
topErrors,
|
|
176
|
+
artifacts: screenshotPath ? [screenshotPath] : [],
|
|
177
|
+
timestamp: new Date().toISOString()
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await adapter.close().catch(() => {});
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getErrorsFromAdapter(adapter) {
|
|
185
|
+
const errors = [];
|
|
186
|
+
(adapter.consoleLogs || []).filter(e => e.type === 'error').forEach(e => {
|
|
187
|
+
errors.push({ source: 'console', type: 'error', text: e.text, timestamp: e.timestamp });
|
|
188
|
+
});
|
|
189
|
+
(adapter.pageErrors || []).forEach(e => {
|
|
190
|
+
errors.push({ source: 'pageerror', type: 'error', text: e.text, timestamp: e.timestamp });
|
|
191
|
+
});
|
|
192
|
+
(adapter.networkLogs || []).filter(e => e.status >= 400 || e.failed).forEach(e => {
|
|
193
|
+
errors.push({ source: 'network', status: e.status, url: e.url, method: e.method, failed: e.failed, text: e.errorText, timestamp: e.timestamp });
|
|
194
|
+
});
|
|
195
|
+
return errors;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
validationQuickRun
|
|
200
|
+
};
|