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 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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "validpilot-oss",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "ValidPilot Open Source - Browser automation and validation framework with MCP protocol",
5
5
  "bin": {
6
6
  "validpilot": "bin/validpilot.js",