@kkeve/suite 0.0.15 → 0.0.18
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/bin/keve.js +2 -1
- package/dist/commands/clean.js +2 -2
- package/dist/commands/config.js +7 -7
- package/dist/commands/init.js +2 -2
- package/dist/commands/report.js +7 -7
- package/dist/commands/run.js +12 -11
- package/dist/commands/test.js +7 -7
- package/dist/config.js +7 -7
- package/dist/core/action-log-writer.js +4 -1
- package/dist/core/decorator/keve-decorators.d.ts +128 -0
- package/dist/core/decorator/keve-decorators.js +254 -0
- package/dist/core/decorator/keve-host.d.ts +40 -0
- package/dist/core/decorator/keve-host.js +33 -0
- package/dist/core/{keve-registry.d.ts → decorator/keve-registry.d.ts} +19 -0
- package/dist/core/{keve-registry.js → decorator/keve-registry.js} +13 -1
- package/dist/core/generateReport/report.d.ts +1 -1
- package/dist/core/generateReport/report.js +2 -2
- package/dist/core/generateReport/reportData.d.ts +3 -0
- package/dist/core/generateReport/reportData.js +49 -15
- package/dist/core/judge/confidence.d.ts +119 -0
- package/dist/core/judge/confidence.js +295 -0
- package/dist/core/judge/goal-heuristics.d.ts +32 -0
- package/dist/core/judge/goal-heuristics.js +128 -0
- package/dist/core/judge/pngCodec.d.ts +23 -0
- package/dist/core/judge/pngCodec.js +215 -0
- package/dist/core/judge/screenshotDiff.d.ts +112 -0
- package/dist/core/judge/screenshotDiff.js +420 -0
- package/dist/core/page-object/definePage.d.ts +95 -0
- package/dist/core/page-object/definePage.js +62 -0
- package/dist/core/page-object/elementResolver.d.ts +32 -0
- package/dist/core/page-object/elementResolver.js +236 -0
- package/dist/core/page-object/engine-types.d.ts +140 -0
- package/dist/core/page-object/engine-types.js +34 -0
- package/dist/core/sso-cookie.d.ts +18 -0
- package/dist/core/sso-cookie.js +10 -0
- package/dist/dsl/index.d.ts +15 -0
- package/dist/dsl/index.js +29 -0
- package/dist/dsl/locatorSource.d.ts +25 -0
- package/dist/dsl/locatorSource.js +323 -0
- package/dist/dsl/ops.d.ts +46 -0
- package/dist/dsl/ops.js +95 -0
- package/dist/dsl/toDecorator.d.ts +98 -0
- package/dist/dsl/toDecorator.js +747 -0
- package/dist/engine-cypress/adapter.d.ts +108 -0
- package/dist/engine-cypress/adapter.js +514 -0
- package/dist/engine-cypress/agent-page.d.ts +159 -0
- package/dist/engine-cypress/agent-page.js +566 -0
- package/dist/engine-cypress/agentDom.d.ts +22 -0
- package/dist/engine-cypress/agentDom.js +590 -0
- package/dist/engine-cypress/bridge.d.ts +39 -0
- package/dist/engine-cypress/bridge.js +598 -0
- package/dist/engine-cypress/cdp.d.ts +78 -0
- package/dist/engine-cypress/cdp.js +339 -0
- package/dist/engine-cypress/host.d.ts +22 -0
- package/dist/engine-cypress/host.js +480 -0
- package/dist/engine-cypress/index.d.ts +26 -0
- package/dist/engine-cypress/index.js +58 -0
- package/dist/engine-cypress/keveGoal.d.ts +43 -0
- package/dist/engine-cypress/keveGoal.js +265 -0
- package/dist/engine-cypress/pageOps.d.ts +41 -0
- package/dist/engine-cypress/pageOps.js +439 -0
- package/dist/engine-cypress/reporter.d.ts +20 -0
- package/dist/engine-cypress/reporter.js +128 -0
- package/dist/engine-cypress/setup.d.ts +21 -0
- package/dist/engine-cypress/setup.js +27 -0
- package/dist/engine-cypress/shotBuffer.d.ts +40 -0
- package/dist/engine-cypress/shotBuffer.js +41 -0
- package/dist/engine-cypress/state.d.ts +56 -0
- package/dist/engine-cypress/state.js +81 -0
- package/dist/engine-playwright/engineAdapter.d.ts +78 -0
- package/dist/engine-playwright/engineAdapter.js +371 -0
- package/dist/engine-playwright/global-setup.d.ts +28 -0
- package/dist/engine-playwright/global-setup.js +142 -0
- package/dist/{core → engine-playwright}/keve-report.d.ts +0 -1
- package/dist/engine-playwright/keve-report.js +137 -0
- package/dist/{core → engine-playwright}/keve-test.d.ts +10 -8
- package/dist/{core → engine-playwright}/keve-test.js +204 -200
- package/dist/{core → engine-playwright}/pw-config.js +8 -7
- package/dist/index.d.ts +13 -8
- package/dist/index.js +43 -28
- package/dist/page-agent/agent.d.ts +82 -7
- package/dist/page-agent/agent.js +252 -88
- package/dist/page-agent/diagnostics.d.ts +184 -0
- package/dist/page-agent/diagnostics.js +377 -0
- package/dist/page-agent/hooks.d.ts +3 -2
- package/dist/page-agent/hooks.js +4 -3
- package/dist/page-agent/page-like.d.ts +116 -0
- package/dist/page-agent/page-like.js +14 -0
- package/dist/page-agent/playwright-page.d.ts +20 -0
- package/dist/page-agent/playwright-page.js +98 -0
- package/dist/page-agent/system-prompt.d.ts +7 -0
- package/dist/page-agent/system-prompt.js +10 -0
- package/dist/page-agent/tools.d.ts +1 -1
- package/dist/page-agent/tools.js +144 -8
- package/package.json +24 -11
- package/scripts/gen-system-prompt.mjs +36 -0
- package/scripts/run-dsl-task.mjs +652 -0
- package/scripts/runtime/cypress-preprocessor.mjs +44 -0
- package/scripts/runtime/cypress.config.mjs +35 -0
- package/scripts/runtime/playwright.config.mjs +41 -0
- package/scripts/verify-shot-diff.mjs +243 -0
- package/dist/commands/auth-update.d.ts +0 -16
- package/dist/commands/auth-update.js +0 -88
- package/dist/core/auth.d.ts +0 -17
- package/dist/core/auth.js +0 -224
- package/dist/core/cdp-resolver.d.ts +0 -5
- package/dist/core/cdp-resolver.js +0 -93
- package/dist/core/global-setup.d.ts +0 -6
- package/dist/core/global-setup.js +0 -123
- package/dist/core/keve-decorators.d.ts +0 -51
- package/dist/core/keve-decorators.js +0 -95
- package/dist/core/keve-report.js +0 -376
- /package/dist/core/{keve-aspect.d.ts → decorator/keve-aspect.d.ts} +0 -0
- /package/dist/core/{keve-aspect.js → decorator/keve-aspect.js} +0 -0
- /package/dist/{core → engine-playwright}/pw-config.d.ts +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diagnostics — 运行诊断的统一数据契约(引擎无关,零 Node 依赖)
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独成模块:Playwright 与 Cypress 的采集手段不同(前者有原生
|
|
5
|
+
* page 事件,后者只能主世界注入 + 快照),但报告里必须是同一份结构,
|
|
6
|
+
* 否则「运行诊断」会出现两套字段语义。这里只放类型与纯函数,
|
|
7
|
+
* 两端各自把自己的原始记录 normalize 成这里的输入即可。
|
|
8
|
+
*
|
|
9
|
+
* 约束:
|
|
10
|
+
* - 不保存完整请求/响应 Body,避免体积与隐私问题;
|
|
11
|
+
* - samples 限量并截断文本,防止诊断本身把报告撑爆;
|
|
12
|
+
* - 诊断只产出风险评估,不把 passed 用例自动降级为 failed。
|
|
13
|
+
*/
|
|
14
|
+
export interface PerformanceDiagnostics {
|
|
15
|
+
/** 用例总耗时(ms) */
|
|
16
|
+
totalMs: number;
|
|
17
|
+
/** AI 推理(LLM 调用)累计耗时(ms) */
|
|
18
|
+
aiReasoningMs: number;
|
|
19
|
+
/** 工具/操作执行累计耗时(ms) */
|
|
20
|
+
actionMs: number;
|
|
21
|
+
/** 非静态网络请求累计耗时(ms),无法采集时为 0 */
|
|
22
|
+
networkMs: number;
|
|
23
|
+
/** 耗时最长的单个操作 */
|
|
24
|
+
slowestAction?: {
|
|
25
|
+
name: string;
|
|
26
|
+
durationMs: number;
|
|
27
|
+
};
|
|
28
|
+
/** 页面级性能指标(Chrome 原生能力,Cypress 经主世界采集) */
|
|
29
|
+
page?: PagePerformanceMetrics;
|
|
30
|
+
}
|
|
31
|
+
export interface PagePerformanceMetrics {
|
|
32
|
+
/** DOMContentLoaded 耗时(ms,navigation timing) */
|
|
33
|
+
domContentLoadedMs?: number;
|
|
34
|
+
/** load 事件耗时(ms) */
|
|
35
|
+
loadMs?: number;
|
|
36
|
+
/** 最大内容绘制(ms) */
|
|
37
|
+
lcpMs?: number;
|
|
38
|
+
/** 长任务次数(>50ms) */
|
|
39
|
+
longTaskCount?: number;
|
|
40
|
+
/** 长任务累计阻塞时长(ms) */
|
|
41
|
+
longTaskMs?: number;
|
|
42
|
+
/** JS 堆已用内存(MB,非标准 API,Chrome 独有) */
|
|
43
|
+
heapUsedMb?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface AiUsageDiagnostics {
|
|
46
|
+
/** LLM 调用次数 */
|
|
47
|
+
llmCalls: number;
|
|
48
|
+
promptTokens: number;
|
|
49
|
+
completionTokens: number;
|
|
50
|
+
totalTokens: number;
|
|
51
|
+
/** 命中 prompt 缓存的 token 数(供应商上报时才有) */
|
|
52
|
+
cachedTokens?: number;
|
|
53
|
+
/** 推理模型消耗的 reasoning token(供应商上报时才有) */
|
|
54
|
+
reasoningTokens?: number;
|
|
55
|
+
}
|
|
56
|
+
export interface NetworkSample {
|
|
57
|
+
url: string;
|
|
58
|
+
method?: string;
|
|
59
|
+
/** HTTP 状态码;失败请求为 0,未拿到响应为 undefined/null */
|
|
60
|
+
status?: number | null;
|
|
61
|
+
durationMs?: number;
|
|
62
|
+
error?: string;
|
|
63
|
+
/** 采样时刻(epoch ms) */
|
|
64
|
+
at?: number;
|
|
65
|
+
}
|
|
66
|
+
export interface NetworkDiagnostics {
|
|
67
|
+
total: number;
|
|
68
|
+
failed: number;
|
|
69
|
+
http4xx: number;
|
|
70
|
+
http5xx: number;
|
|
71
|
+
slow: number;
|
|
72
|
+
samples: NetworkSample[];
|
|
73
|
+
}
|
|
74
|
+
export interface BrowserSample {
|
|
75
|
+
/** console 的 level;pageerror/exception 固定为 error */
|
|
76
|
+
level: string;
|
|
77
|
+
/** console | pageerror | exception */
|
|
78
|
+
kind: 'console' | 'pageerror' | 'exception';
|
|
79
|
+
text: string;
|
|
80
|
+
at?: number;
|
|
81
|
+
}
|
|
82
|
+
export interface BrowserDiagnostics {
|
|
83
|
+
consoleErrors: number;
|
|
84
|
+
consoleWarnings: number;
|
|
85
|
+
pageErrors: number;
|
|
86
|
+
uncaughtExceptions: number;
|
|
87
|
+
samples: BrowserSample[];
|
|
88
|
+
}
|
|
89
|
+
export interface QualityDiagnostics {
|
|
90
|
+
/** 环境/软等待等重试次数 */
|
|
91
|
+
retries: number;
|
|
92
|
+
/** AI 自愈后恢复的次数 */
|
|
93
|
+
recoveries: number;
|
|
94
|
+
/** 连续重复动作次数(stuck/oscillation 信号) */
|
|
95
|
+
repeatedActionCount: number;
|
|
96
|
+
/** Agent 判 PASS 但确定性断言判 FAIL 的次数 */
|
|
97
|
+
verdictConflict: boolean;
|
|
98
|
+
/** 人可读的风险原因 */
|
|
99
|
+
riskReasons: string[];
|
|
100
|
+
}
|
|
101
|
+
export interface RuntimeDiagnostics {
|
|
102
|
+
performance: PerformanceDiagnostics;
|
|
103
|
+
ai: AiUsageDiagnostics;
|
|
104
|
+
network: NetworkDiagnostics;
|
|
105
|
+
browser: BrowserDiagnostics;
|
|
106
|
+
quality: QualityDiagnostics;
|
|
107
|
+
}
|
|
108
|
+
export declare const DIAGNOSTICS_MAX_NETWORK_SAMPLES = 20;
|
|
109
|
+
export declare const DIAGNOSTICS_MAX_BROWSER_SAMPLES = 20;
|
|
110
|
+
export declare const DIAGNOSTICS_MAX_TEXT_LENGTH = 1000;
|
|
111
|
+
export declare const DIAGNOSTICS_SLOW_REQUEST_MS = 3000;
|
|
112
|
+
/** 主世界诊断快照挂到 DOM 属性上的键名(isolated world 通过它读取) */
|
|
113
|
+
export declare const DIAGNOSTICS_DATASET_KEY = "keveDiagnostics";
|
|
114
|
+
/** 主世界诊断快照自身的版本号,注入幂等依赖它 */
|
|
115
|
+
export declare const DIAGNOSTICS_SOURCE_VERSION = "v1";
|
|
116
|
+
/** 静态资源不计入网络耗时与慢请求统计 */
|
|
117
|
+
export declare function isStaticUrl(url: string): boolean;
|
|
118
|
+
/** 截断诊断文本,避免单条样本撑爆报告 */
|
|
119
|
+
export declare function clipDiagnosticText(text: unknown, max?: number): string;
|
|
120
|
+
export interface RawConsoleRecord {
|
|
121
|
+
level: string;
|
|
122
|
+
text: string;
|
|
123
|
+
at?: number;
|
|
124
|
+
kind?: 'console' | 'pageerror' | 'exception';
|
|
125
|
+
}
|
|
126
|
+
export interface RawNetworkRecord {
|
|
127
|
+
url: string;
|
|
128
|
+
method?: string;
|
|
129
|
+
status?: number | null;
|
|
130
|
+
durationMs?: number;
|
|
131
|
+
error?: string;
|
|
132
|
+
at?: number;
|
|
133
|
+
isStatic?: boolean;
|
|
134
|
+
}
|
|
135
|
+
export interface RawActionRecord {
|
|
136
|
+
name: string;
|
|
137
|
+
durationMs?: number;
|
|
138
|
+
error?: boolean;
|
|
139
|
+
}
|
|
140
|
+
export interface RawAiUsageRecord {
|
|
141
|
+
llmCalls?: number;
|
|
142
|
+
promptTokens?: number;
|
|
143
|
+
completionTokens?: number;
|
|
144
|
+
totalTokens?: number;
|
|
145
|
+
cachedTokens?: number;
|
|
146
|
+
reasoningTokens?: number;
|
|
147
|
+
}
|
|
148
|
+
export interface RuntimeDiagnosticsInput {
|
|
149
|
+
totalMs?: number;
|
|
150
|
+
aiReasoningMs?: number;
|
|
151
|
+
actionMs?: number;
|
|
152
|
+
actions?: RawActionRecord[];
|
|
153
|
+
consoleRecords?: RawConsoleRecord[];
|
|
154
|
+
networkRecords?: RawNetworkRecord[];
|
|
155
|
+
retries?: number;
|
|
156
|
+
recoveries?: number;
|
|
157
|
+
repeatedActionCount?: number;
|
|
158
|
+
verdictConflict?: boolean;
|
|
159
|
+
extraRiskReasons?: string[];
|
|
160
|
+
/** 页面级性能指标 */
|
|
161
|
+
pageMetrics?: PagePerformanceMetrics;
|
|
162
|
+
/** LLM token 用量(与耗时分离,单独成段) */
|
|
163
|
+
aiUsage?: RawAiUsageRecord;
|
|
164
|
+
}
|
|
165
|
+
/** 把两端采集的原始记录归一成统一诊断结构 */
|
|
166
|
+
export declare function buildRuntimeDiagnostics(input?: RuntimeDiagnosticsInput): RuntimeDiagnostics;
|
|
167
|
+
/** 无采集数据时的空结构,保证前端字段恒存在 */
|
|
168
|
+
export declare function emptyRuntimeDiagnostics(): RuntimeDiagnostics;
|
|
169
|
+
/**
|
|
170
|
+
* 页面级性能探针源码(自包含,可直接丢给 Playwright `page.evaluate` 或
|
|
171
|
+
* Cypress 主世界/隔离世界求值)。
|
|
172
|
+
*
|
|
173
|
+
* 说明:LCP / longtask 走 PerformanceEntry 查询而非常驻 PerformanceObserver,
|
|
174
|
+
* 因此只能拿到「探针执行时刻已缓冲」的条目;Chromium 对这两类条目有缓冲,
|
|
175
|
+
* 覆盖大多数场景,缺失时字段为 undefined,前端按「未采集」展示即可。
|
|
176
|
+
*/
|
|
177
|
+
export declare function pagePerformanceProbeSource(): string;
|
|
178
|
+
/**
|
|
179
|
+
* 合并多个 goal 的诊断(用例级 = 各步骤求和/去重后的聚合)。
|
|
180
|
+
*
|
|
181
|
+
* 注意不能从 samples 反推计数:samples 是截断后的异常样本,
|
|
182
|
+
* total/failed/4xx/5xx/slow 必须直接对聚合字段求和,否则多步骤用例会少算。
|
|
183
|
+
*/
|
|
184
|
+
export declare function mergeRuntimeDiagnostics(items: Array<RuntimeDiagnostics | undefined | null>): RuntimeDiagnostics;
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* diagnostics — 运行诊断的统一数据契约(引擎无关,零 Node 依赖)
|
|
4
|
+
*
|
|
5
|
+
* 为什么单独成模块:Playwright 与 Cypress 的采集手段不同(前者有原生
|
|
6
|
+
* page 事件,后者只能主世界注入 + 快照),但报告里必须是同一份结构,
|
|
7
|
+
* 否则「运行诊断」会出现两套字段语义。这里只放类型与纯函数,
|
|
8
|
+
* 两端各自把自己的原始记录 normalize 成这里的输入即可。
|
|
9
|
+
*
|
|
10
|
+
* 约束:
|
|
11
|
+
* - 不保存完整请求/响应 Body,避免体积与隐私问题;
|
|
12
|
+
* - samples 限量并截断文本,防止诊断本身把报告撑爆;
|
|
13
|
+
* - 诊断只产出风险评估,不把 passed 用例自动降级为 failed。
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.DIAGNOSTICS_SOURCE_VERSION = exports.DIAGNOSTICS_DATASET_KEY = exports.DIAGNOSTICS_SLOW_REQUEST_MS = exports.DIAGNOSTICS_MAX_TEXT_LENGTH = exports.DIAGNOSTICS_MAX_BROWSER_SAMPLES = exports.DIAGNOSTICS_MAX_NETWORK_SAMPLES = void 0;
|
|
17
|
+
exports.isStaticUrl = isStaticUrl;
|
|
18
|
+
exports.clipDiagnosticText = clipDiagnosticText;
|
|
19
|
+
exports.buildRuntimeDiagnostics = buildRuntimeDiagnostics;
|
|
20
|
+
exports.emptyRuntimeDiagnostics = emptyRuntimeDiagnostics;
|
|
21
|
+
exports.pagePerformanceProbeSource = pagePerformanceProbeSource;
|
|
22
|
+
exports.mergeRuntimeDiagnostics = mergeRuntimeDiagnostics;
|
|
23
|
+
// ─── 常量 ────────────────────────────────────────────────────────────
|
|
24
|
+
exports.DIAGNOSTICS_MAX_NETWORK_SAMPLES = 20;
|
|
25
|
+
exports.DIAGNOSTICS_MAX_BROWSER_SAMPLES = 20;
|
|
26
|
+
exports.DIAGNOSTICS_MAX_TEXT_LENGTH = 1000;
|
|
27
|
+
exports.DIAGNOSTICS_SLOW_REQUEST_MS = 3000;
|
|
28
|
+
const STATIC_URL_RE = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|map)(\?|$)/i;
|
|
29
|
+
/** 主世界诊断快照挂到 DOM 属性上的键名(isolated world 通过它读取) */
|
|
30
|
+
exports.DIAGNOSTICS_DATASET_KEY = 'keveDiagnostics';
|
|
31
|
+
/** 主世界诊断快照自身的版本号,注入幂等依赖它 */
|
|
32
|
+
exports.DIAGNOSTICS_SOURCE_VERSION = 'v1';
|
|
33
|
+
/** 静态资源不计入网络耗时与慢请求统计 */
|
|
34
|
+
function isStaticUrl(url) {
|
|
35
|
+
return STATIC_URL_RE.test(String(url || ''));
|
|
36
|
+
}
|
|
37
|
+
/** 截断诊断文本,避免单条样本撑爆报告 */
|
|
38
|
+
function clipDiagnosticText(text, max = exports.DIAGNOSTICS_MAX_TEXT_LENGTH) {
|
|
39
|
+
const s = String(text ?? '');
|
|
40
|
+
return s.length > max ? `${s.slice(0, max)}…` : s;
|
|
41
|
+
}
|
|
42
|
+
// ─── 构建 ────────────────────────────────────────────────────────────
|
|
43
|
+
function safeNumber(value, fallback = 0) {
|
|
44
|
+
const n = Number(value);
|
|
45
|
+
return Number.isFinite(n) && n >= 0 ? Math.round(n) : fallback;
|
|
46
|
+
}
|
|
47
|
+
function optionalNumber(value) {
|
|
48
|
+
const n = Number(value);
|
|
49
|
+
return Number.isFinite(n) && n >= 0 ? Math.round(n) : undefined;
|
|
50
|
+
}
|
|
51
|
+
function normalizePageMetrics(input) {
|
|
52
|
+
if (!input)
|
|
53
|
+
return undefined;
|
|
54
|
+
const page = {
|
|
55
|
+
domContentLoadedMs: optionalNumber(input.domContentLoadedMs),
|
|
56
|
+
loadMs: optionalNumber(input.loadMs),
|
|
57
|
+
lcpMs: optionalNumber(input.lcpMs),
|
|
58
|
+
longTaskCount: optionalNumber(input.longTaskCount),
|
|
59
|
+
longTaskMs: optionalNumber(input.longTaskMs),
|
|
60
|
+
heapUsedMb: optionalNumber(input.heapUsedMb),
|
|
61
|
+
};
|
|
62
|
+
return Object.values(page).some(v => v !== undefined) ? page : undefined;
|
|
63
|
+
}
|
|
64
|
+
function normalizeAiUsage(input) {
|
|
65
|
+
const promptTokens = safeNumber(input?.promptTokens, 0);
|
|
66
|
+
const completionTokens = safeNumber(input?.completionTokens, 0);
|
|
67
|
+
// total 缺失时用输入 + 输出兜底,避免供应商只回明细不回总量
|
|
68
|
+
const totalTokens = input?.totalTokens !== undefined
|
|
69
|
+
? safeNumber(input.totalTokens, 0)
|
|
70
|
+
: promptTokens + completionTokens;
|
|
71
|
+
return {
|
|
72
|
+
llmCalls: safeNumber(input?.llmCalls, 0),
|
|
73
|
+
promptTokens,
|
|
74
|
+
completionTokens,
|
|
75
|
+
totalTokens,
|
|
76
|
+
cachedTokens: optionalNumber(input?.cachedTokens),
|
|
77
|
+
reasoningTokens: optionalNumber(input?.reasoningTokens),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function buildBrowserDiagnostics(records) {
|
|
81
|
+
let consoleErrors = 0;
|
|
82
|
+
let consoleWarnings = 0;
|
|
83
|
+
let pageErrors = 0;
|
|
84
|
+
let uncaughtExceptions = 0;
|
|
85
|
+
const samples = [];
|
|
86
|
+
for (const record of records) {
|
|
87
|
+
const kind = record.kind || 'console';
|
|
88
|
+
const level = String(record.level || (kind === 'console' ? 'log' : 'error')).toLowerCase();
|
|
89
|
+
if (kind === 'pageerror')
|
|
90
|
+
pageErrors += 1;
|
|
91
|
+
else if (kind === 'exception')
|
|
92
|
+
uncaughtExceptions += 1;
|
|
93
|
+
else if (level === 'error')
|
|
94
|
+
consoleErrors += 1;
|
|
95
|
+
else if (level === 'warning' || level === 'warn')
|
|
96
|
+
consoleWarnings += 1;
|
|
97
|
+
// 只保留 error/warning 级别或异常,避免占用采样额度
|
|
98
|
+
const noteworthy = kind !== 'console' || level === 'error' || level === 'warning' || level === 'warn';
|
|
99
|
+
if (!noteworthy)
|
|
100
|
+
continue;
|
|
101
|
+
if (samples.length >= exports.DIAGNOSTICS_MAX_BROWSER_SAMPLES)
|
|
102
|
+
continue;
|
|
103
|
+
samples.push({
|
|
104
|
+
level,
|
|
105
|
+
kind,
|
|
106
|
+
text: clipDiagnosticText(record.text),
|
|
107
|
+
at: record.at,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return { consoleErrors, consoleWarnings, pageErrors, uncaughtExceptions, samples };
|
|
111
|
+
}
|
|
112
|
+
function buildNetworkDiagnostics(records) {
|
|
113
|
+
let failed = 0;
|
|
114
|
+
let http4xx = 0;
|
|
115
|
+
let http5xx = 0;
|
|
116
|
+
let slow = 0;
|
|
117
|
+
let networkMs = 0;
|
|
118
|
+
const samples = [];
|
|
119
|
+
for (const record of records) {
|
|
120
|
+
const staticRes = record.isStatic ?? isStaticUrl(record.url);
|
|
121
|
+
const status = record.status;
|
|
122
|
+
const durationMs = safeNumber(record.durationMs, 0);
|
|
123
|
+
if (!staticRes && durationMs > 0)
|
|
124
|
+
networkMs += durationMs;
|
|
125
|
+
const isFailed = !!record.error || status === 0;
|
|
126
|
+
const is4xx = typeof status === 'number' && status >= 400 && status < 500;
|
|
127
|
+
const is5xx = typeof status === 'number' && status >= 500;
|
|
128
|
+
const isSlow = !staticRes && durationMs >= exports.DIAGNOSTICS_SLOW_REQUEST_MS;
|
|
129
|
+
if (isFailed)
|
|
130
|
+
failed += 1;
|
|
131
|
+
if (is4xx)
|
|
132
|
+
http4xx += 1;
|
|
133
|
+
if (is5xx)
|
|
134
|
+
http5xx += 1;
|
|
135
|
+
if (isSlow)
|
|
136
|
+
slow += 1;
|
|
137
|
+
// 采样优先级:失败 > 4xx/5xx > 慢请求
|
|
138
|
+
const noteworthy = isFailed || is4xx || is5xx || isSlow;
|
|
139
|
+
if (!noteworthy)
|
|
140
|
+
continue;
|
|
141
|
+
if (samples.length >= exports.DIAGNOSTICS_MAX_NETWORK_SAMPLES)
|
|
142
|
+
continue;
|
|
143
|
+
samples.push({
|
|
144
|
+
url: clipDiagnosticText(record.url, 500),
|
|
145
|
+
method: record.method,
|
|
146
|
+
status: status === undefined ? null : status,
|
|
147
|
+
durationMs: durationMs || undefined,
|
|
148
|
+
error: record.error ? clipDiagnosticText(record.error) : undefined,
|
|
149
|
+
at: record.at,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
diagnostics: { total: records.length, failed, http4xx, http5xx, slow, samples },
|
|
154
|
+
networkMs,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function buildQualityDiagnostics(input, browser, network) {
|
|
158
|
+
const retries = safeNumber(input.retries, 0);
|
|
159
|
+
const recoveries = safeNumber(input.recoveries, 0);
|
|
160
|
+
const repeatedActionCount = safeNumber(input.repeatedActionCount, 0);
|
|
161
|
+
const verdictConflict = !!input.verdictConflict;
|
|
162
|
+
const riskReasons = [];
|
|
163
|
+
if (network.http5xx > 0)
|
|
164
|
+
riskReasons.push(`存在 ${network.http5xx} 个 5xx 请求`);
|
|
165
|
+
if (network.failed > 0)
|
|
166
|
+
riskReasons.push(`存在 ${network.failed} 个失败请求`);
|
|
167
|
+
if (network.slow > 0)
|
|
168
|
+
riskReasons.push(`存在 ${network.slow} 个慢请求(≥${exports.DIAGNOSTICS_SLOW_REQUEST_MS}ms)`);
|
|
169
|
+
if (browser.pageErrors > 0)
|
|
170
|
+
riskReasons.push(`发生 ${browser.pageErrors} 次页面未捕获异常`);
|
|
171
|
+
if (browser.uncaughtExceptions > 0)
|
|
172
|
+
riskReasons.push(`发生 ${browser.uncaughtExceptions} 次未捕获异常`);
|
|
173
|
+
if (browser.consoleErrors > 0)
|
|
174
|
+
riskReasons.push(`存在 ${browser.consoleErrors} 条 Console Error`);
|
|
175
|
+
if (retries > 0)
|
|
176
|
+
riskReasons.push(`发生 ${retries} 次重试`);
|
|
177
|
+
if (repeatedActionCount > 0)
|
|
178
|
+
riskReasons.push(`检测到 ${repeatedActionCount} 次重复动作`);
|
|
179
|
+
if (verdictConflict)
|
|
180
|
+
riskReasons.push('AI 结论与确定性断言冲突');
|
|
181
|
+
const page = normalizePageMetrics(input.pageMetrics);
|
|
182
|
+
if (page?.longTaskCount && page.longTaskCount > 0) {
|
|
183
|
+
riskReasons.push(`存在 ${page.longTaskCount} 个长任务(累计 ${page.longTaskMs || 0}ms)`);
|
|
184
|
+
}
|
|
185
|
+
for (const reason of input.extraRiskReasons || []) {
|
|
186
|
+
if (reason && !riskReasons.includes(reason))
|
|
187
|
+
riskReasons.push(reason);
|
|
188
|
+
}
|
|
189
|
+
return { retries, recoveries, repeatedActionCount, verdictConflict, riskReasons };
|
|
190
|
+
}
|
|
191
|
+
/** 把两端采集的原始记录归一成统一诊断结构 */
|
|
192
|
+
function buildRuntimeDiagnostics(input = {}) {
|
|
193
|
+
const actions = input.actions || [];
|
|
194
|
+
const actionMs = input.actionMs !== undefined
|
|
195
|
+
? safeNumber(input.actionMs, 0)
|
|
196
|
+
: actions.reduce((sum, a) => sum + safeNumber(a.durationMs, 0), 0);
|
|
197
|
+
let slowestAction;
|
|
198
|
+
for (const action of actions) {
|
|
199
|
+
const durationMs = safeNumber(action.durationMs, 0);
|
|
200
|
+
if (durationMs <= 0)
|
|
201
|
+
continue;
|
|
202
|
+
if (!slowestAction || durationMs > slowestAction.durationMs) {
|
|
203
|
+
slowestAction = { name: clipDiagnosticText(action.name, 120), durationMs };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const browser = buildBrowserDiagnostics(input.consoleRecords || []);
|
|
207
|
+
const { diagnostics: network, networkMs } = buildNetworkDiagnostics(input.networkRecords || []);
|
|
208
|
+
const quality = buildQualityDiagnostics(input, browser, network);
|
|
209
|
+
return {
|
|
210
|
+
performance: {
|
|
211
|
+
totalMs: safeNumber(input.totalMs, 0),
|
|
212
|
+
aiReasoningMs: safeNumber(input.aiReasoningMs, 0),
|
|
213
|
+
actionMs,
|
|
214
|
+
networkMs,
|
|
215
|
+
slowestAction,
|
|
216
|
+
page: normalizePageMetrics(input.pageMetrics),
|
|
217
|
+
},
|
|
218
|
+
ai: normalizeAiUsage(input.aiUsage),
|
|
219
|
+
network,
|
|
220
|
+
browser,
|
|
221
|
+
quality,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** 无采集数据时的空结构,保证前端字段恒存在 */
|
|
225
|
+
function emptyRuntimeDiagnostics() {
|
|
226
|
+
return buildRuntimeDiagnostics();
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* 页面级性能探针源码(自包含,可直接丢给 Playwright `page.evaluate` 或
|
|
230
|
+
* Cypress 主世界/隔离世界求值)。
|
|
231
|
+
*
|
|
232
|
+
* 说明:LCP / longtask 走 PerformanceEntry 查询而非常驻 PerformanceObserver,
|
|
233
|
+
* 因此只能拿到「探针执行时刻已缓冲」的条目;Chromium 对这两类条目有缓冲,
|
|
234
|
+
* 覆盖大多数场景,缺失时字段为 undefined,前端按「未采集」展示即可。
|
|
235
|
+
*/
|
|
236
|
+
function pagePerformanceProbeSource() {
|
|
237
|
+
return `(() => {
|
|
238
|
+
"use strict";
|
|
239
|
+
try {
|
|
240
|
+
var out = {};
|
|
241
|
+
var nav = (performance.getEntriesByType && performance.getEntriesByType("navigation") || [])[0];
|
|
242
|
+
if (nav) {
|
|
243
|
+
if (nav.domContentLoadedEventEnd) out.domContentLoadedMs = Math.round(nav.domContentLoadedEventEnd);
|
|
244
|
+
if (nav.loadEventEnd) out.loadMs = Math.round(nav.loadEventEnd);
|
|
245
|
+
}
|
|
246
|
+
var lcp = 0;
|
|
247
|
+
var paints = performance.getEntriesByType ? performance.getEntriesByType("largest-contentful-paint") : [];
|
|
248
|
+
for (var i = 0; i < paints.length; i++) {
|
|
249
|
+
var t = paints[i].startTime || paints[i].renderTime || 0;
|
|
250
|
+
if (t > lcp) lcp = t;
|
|
251
|
+
}
|
|
252
|
+
if (lcp > 0) out.lcpMs = Math.round(lcp);
|
|
253
|
+
var longTaskCount = 0;
|
|
254
|
+
var longTaskMs = 0;
|
|
255
|
+
var longs = performance.getEntriesByType ? performance.getEntriesByType("longtask") : [];
|
|
256
|
+
for (var j = 0; j < longs.length; j++) {
|
|
257
|
+
longTaskCount += 1;
|
|
258
|
+
longTaskMs += Math.round(longs[j].duration || 0);
|
|
259
|
+
}
|
|
260
|
+
if (longTaskCount > 0) {
|
|
261
|
+
out.longTaskCount = longTaskCount;
|
|
262
|
+
out.longTaskMs = longTaskMs;
|
|
263
|
+
}
|
|
264
|
+
var mem = performance.memory;
|
|
265
|
+
if (mem && mem.usedJSHeapSize) out.heapUsedMb = Math.round(mem.usedJSHeapSize / 1048576 * 10) / 10;
|
|
266
|
+
return out;
|
|
267
|
+
} catch (e) {
|
|
268
|
+
return {};
|
|
269
|
+
}
|
|
270
|
+
})()`;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* 合并多个 goal 的诊断(用例级 = 各步骤求和/去重后的聚合)。
|
|
274
|
+
*
|
|
275
|
+
* 注意不能从 samples 反推计数:samples 是截断后的异常样本,
|
|
276
|
+
* total/failed/4xx/5xx/slow 必须直接对聚合字段求和,否则多步骤用例会少算。
|
|
277
|
+
*/
|
|
278
|
+
function mergeRuntimeDiagnostics(items) {
|
|
279
|
+
const valid = items.filter(Boolean);
|
|
280
|
+
if (!valid.length)
|
|
281
|
+
return emptyRuntimeDiagnostics();
|
|
282
|
+
const browserSamples = [];
|
|
283
|
+
const networkSamples = [];
|
|
284
|
+
const riskReasons = [];
|
|
285
|
+
let totalMs = 0;
|
|
286
|
+
let aiReasoningMs = 0;
|
|
287
|
+
let actionMs = 0;
|
|
288
|
+
let networkMs = 0;
|
|
289
|
+
let slowestAction;
|
|
290
|
+
let consoleErrors = 0;
|
|
291
|
+
let consoleWarnings = 0;
|
|
292
|
+
let pageErrors = 0;
|
|
293
|
+
let uncaughtExceptions = 0;
|
|
294
|
+
let networkTotal = 0;
|
|
295
|
+
let networkFailed = 0;
|
|
296
|
+
let http4xx = 0;
|
|
297
|
+
let http5xx = 0;
|
|
298
|
+
let slow = 0;
|
|
299
|
+
let retries = 0;
|
|
300
|
+
let recoveries = 0;
|
|
301
|
+
let repeatedActionCount = 0;
|
|
302
|
+
let verdictConflict = false;
|
|
303
|
+
let pageMetrics;
|
|
304
|
+
let llmCalls = 0;
|
|
305
|
+
let promptTokens = 0;
|
|
306
|
+
let completionTokens = 0;
|
|
307
|
+
let totalTokens = 0;
|
|
308
|
+
let cachedTokens = 0;
|
|
309
|
+
let reasoningTokens = 0;
|
|
310
|
+
for (const item of valid) {
|
|
311
|
+
totalMs += item.performance.totalMs;
|
|
312
|
+
aiReasoningMs += item.performance.aiReasoningMs;
|
|
313
|
+
actionMs += item.performance.actionMs;
|
|
314
|
+
networkMs += item.performance.networkMs;
|
|
315
|
+
if (item.performance.slowestAction
|
|
316
|
+
&& (!slowestAction || item.performance.slowestAction.durationMs > slowestAction.durationMs)) {
|
|
317
|
+
slowestAction = item.performance.slowestAction;
|
|
318
|
+
}
|
|
319
|
+
if (item.performance.page) {
|
|
320
|
+
const prev = pageMetrics || {};
|
|
321
|
+
pageMetrics = {
|
|
322
|
+
domContentLoadedMs: Math.max(prev.domContentLoadedMs || 0, item.performance.page.domContentLoadedMs || 0) || undefined,
|
|
323
|
+
loadMs: Math.max(prev.loadMs || 0, item.performance.page.loadMs || 0) || undefined,
|
|
324
|
+
lcpMs: Math.max(prev.lcpMs || 0, item.performance.page.lcpMs || 0) || undefined,
|
|
325
|
+
longTaskCount: (prev.longTaskCount || 0) + (item.performance.page.longTaskCount || 0),
|
|
326
|
+
longTaskMs: (prev.longTaskMs || 0) + (item.performance.page.longTaskMs || 0),
|
|
327
|
+
heapUsedMb: Math.max(prev.heapUsedMb || 0, item.performance.page.heapUsedMb || 0) || undefined,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const ai = normalizeAiUsage(item.ai);
|
|
331
|
+
llmCalls += ai.llmCalls;
|
|
332
|
+
promptTokens += ai.promptTokens;
|
|
333
|
+
completionTokens += ai.completionTokens;
|
|
334
|
+
totalTokens += ai.totalTokens;
|
|
335
|
+
cachedTokens += ai.cachedTokens || 0;
|
|
336
|
+
reasoningTokens += ai.reasoningTokens || 0;
|
|
337
|
+
consoleErrors += item.browser.consoleErrors;
|
|
338
|
+
consoleWarnings += item.browser.consoleWarnings;
|
|
339
|
+
pageErrors += item.browser.pageErrors;
|
|
340
|
+
uncaughtExceptions += item.browser.uncaughtExceptions;
|
|
341
|
+
for (const sample of item.browser.samples) {
|
|
342
|
+
if (browserSamples.length < exports.DIAGNOSTICS_MAX_BROWSER_SAMPLES)
|
|
343
|
+
browserSamples.push(sample);
|
|
344
|
+
}
|
|
345
|
+
networkTotal += item.network.total;
|
|
346
|
+
networkFailed += item.network.failed;
|
|
347
|
+
http4xx += item.network.http4xx;
|
|
348
|
+
http5xx += item.network.http5xx;
|
|
349
|
+
slow += item.network.slow;
|
|
350
|
+
for (const sample of item.network.samples) {
|
|
351
|
+
if (networkSamples.length < exports.DIAGNOSTICS_MAX_NETWORK_SAMPLES)
|
|
352
|
+
networkSamples.push(sample);
|
|
353
|
+
}
|
|
354
|
+
retries += item.quality.retries;
|
|
355
|
+
recoveries += item.quality.recoveries;
|
|
356
|
+
repeatedActionCount += item.quality.repeatedActionCount;
|
|
357
|
+
verdictConflict = verdictConflict || item.quality.verdictConflict;
|
|
358
|
+
for (const reason of item.quality.riskReasons) {
|
|
359
|
+
if (!riskReasons.includes(reason))
|
|
360
|
+
riskReasons.push(reason);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
performance: { totalMs, aiReasoningMs, actionMs, networkMs, slowestAction, page: normalizePageMetrics(pageMetrics) },
|
|
365
|
+
ai: {
|
|
366
|
+
llmCalls,
|
|
367
|
+
promptTokens,
|
|
368
|
+
completionTokens,
|
|
369
|
+
totalTokens,
|
|
370
|
+
cachedTokens: cachedTokens || undefined,
|
|
371
|
+
reasoningTokens: reasoningTokens || undefined,
|
|
372
|
+
},
|
|
373
|
+
network: { total: networkTotal, failed: networkFailed, http4xx, http5xx, slow, samples: networkSamples },
|
|
374
|
+
browser: { consoleErrors, consoleWarnings, pageErrors, uncaughtExceptions, samples: browserSamples },
|
|
375
|
+
quality: { retries, recoveries, repeatedActionCount, verdictConflict, riskReasons },
|
|
376
|
+
};
|
|
377
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AgentResult } from './agent';
|
|
1
|
+
import type { AgentResult } from './agent.js';
|
|
2
|
+
import type { AgentPage } from './page-like.js';
|
|
2
3
|
/**
|
|
3
4
|
* diagnostic.ts — Extract diagnostic hints from agent result + page state
|
|
4
5
|
*
|
|
@@ -10,4 +11,4 @@ import type { AgentResult } from './agent';
|
|
|
10
11
|
* Extract diagnostic hints from agent result and current page state.
|
|
11
12
|
* Only meaningful when result.success === false.
|
|
12
13
|
*/
|
|
13
|
-
export declare function extractDiagnosticHints(page:
|
|
14
|
+
export declare function extractDiagnosticHints(page: AgentPage, result: AgentResult): Promise<string[]>;
|
package/dist/page-agent/hooks.js
CHANGED
|
@@ -80,8 +80,9 @@ Generate a unified diff patch to fix the fn. Add the missing operations that Re-
|
|
|
80
80
|
|
|
81
81
|
// Save patch file
|
|
82
82
|
const taskDir = process.env.KEVE_TASK_DIR || '.keve';
|
|
83
|
-
const
|
|
84
|
-
|
|
83
|
+
const refineDir = process.env.KEVE_RESULT_DIR
|
|
84
|
+
? path.join(path.resolve(process.env.KEVE_RESULT_DIR), 'refine')
|
|
85
|
+
: path.join(taskDir, 'test-artifacts', `round-${process.env.KEVE_ROUND || 'latest'}`, 'refine');
|
|
85
86
|
fs.mkdirSync(refineDir, { recursive: true });
|
|
86
87
|
const safeName = input.testTitle.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '_').slice(0, 40);
|
|
87
88
|
const patchFile = path.join(refineDir, `refine-${safeName}-step${input.order}.patch`);
|
|
@@ -141,7 +142,7 @@ async function extractDiagnosticHints(page, result) {
|
|
|
141
142
|
}
|
|
142
143
|
// 3. Inspect live page DOM
|
|
143
144
|
try {
|
|
144
|
-
const currentUrl = page.url();
|
|
145
|
+
const currentUrl = await page.url();
|
|
145
146
|
console.log(`[diagnostic] page.url() = ${currentUrl}`);
|
|
146
147
|
if (currentUrl === 'about:blank' || currentUrl === '') {
|
|
147
148
|
hints.push('Playwright 页面对象停留在 about:blank,AI 探索未导航到目标页面(LLM 不可用或未配置)');
|