@kkeve/suite 0.0.15 → 0.0.17
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 +619 -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,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* page-like — Re-Act agent 依赖的「页面能力」契约(零 Node 依赖)
|
|
3
|
+
*
|
|
4
|
+
* 背景:`page-agent` 原本直接吃 Playwright `Page`,而 Cypress 侧没有 Playwright
|
|
5
|
+
* Locator,也不允许在浏览器 bundle 里 import `@playwright/test`(会经
|
|
6
|
+
* playwright-core → node:fs 拉爆 esbuild)。这里定义 agent 真正用到的那一层
|
|
7
|
+
* 最小 API,两侧各给一个实现:
|
|
8
|
+
* - Playwright:`playwright-page.ts`(透传 + 截图归一化为 base64)
|
|
9
|
+
* - Cypress :`engine-cypress/agent-page.ts`(CDP + aria-ref)
|
|
10
|
+
*
|
|
11
|
+
* 约束:本模块只允许 `import type`。
|
|
12
|
+
*/
|
|
13
|
+
/** 元素截图/页面截图的统一返回:base64(不依赖 Node Buffer) */
|
|
14
|
+
export interface AgentScreenshot {
|
|
15
|
+
base64: string;
|
|
16
|
+
}
|
|
17
|
+
export interface AgentBoundingBox {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
}
|
|
23
|
+
export interface AgentLocator {
|
|
24
|
+
click(options?: any): Promise<void>;
|
|
25
|
+
dblclick?(options?: any): Promise<void>;
|
|
26
|
+
fill(value: string, options?: any): Promise<void>;
|
|
27
|
+
hover(options?: any): Promise<void>;
|
|
28
|
+
pressSequentially(text: string, options?: any): Promise<void>;
|
|
29
|
+
evaluate(fn: any, arg?: any): Promise<any>;
|
|
30
|
+
textContent(options?: any): Promise<string | null>;
|
|
31
|
+
innerText(options?: any): Promise<string>;
|
|
32
|
+
inputValue(options?: any): Promise<string>;
|
|
33
|
+
isChecked(options?: any): Promise<boolean>;
|
|
34
|
+
setChecked(checked: boolean, options?: any): Promise<void>;
|
|
35
|
+
selectOption(values: any, options?: any): Promise<any>;
|
|
36
|
+
waitFor(options?: any): Promise<void>;
|
|
37
|
+
boundingBox(options?: any): Promise<AgentBoundingBox | null>;
|
|
38
|
+
getAttribute(name: string, options?: any): Promise<string | null>;
|
|
39
|
+
count(): Promise<number>;
|
|
40
|
+
allInnerTexts(): Promise<string[]>;
|
|
41
|
+
first(): AgentLocator;
|
|
42
|
+
last(): AgentLocator;
|
|
43
|
+
nth(index: number): AgentLocator;
|
|
44
|
+
filter(options?: any): AgentLocator;
|
|
45
|
+
locator(selector: string): AgentLocator;
|
|
46
|
+
screenshot(options?: any): Promise<AgentScreenshot>;
|
|
47
|
+
/** 可选:仅用于诊断,缺失时调用方需容忍 */
|
|
48
|
+
isVisible?(options?: any): Promise<boolean>;
|
|
49
|
+
}
|
|
50
|
+
export interface AgentKeyboard {
|
|
51
|
+
press(key: string, options?: any): Promise<void>;
|
|
52
|
+
type(text: string, options?: any): Promise<void>;
|
|
53
|
+
down(key: string): Promise<void>;
|
|
54
|
+
up(key: string): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
export interface AgentMouse {
|
|
57
|
+
click(x: number, y: number, options?: any): Promise<void>;
|
|
58
|
+
move(x: number, y: number, options?: any): Promise<void>;
|
|
59
|
+
down(options?: any): Promise<void>;
|
|
60
|
+
up(options?: any): Promise<void>;
|
|
61
|
+
wheel(deltaX: number, deltaY: number): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
/** 页面事件:语义与 Playwright 同名事件对齐,Cypress 侧按能力尽力实现 */
|
|
64
|
+
export interface AgentDialog {
|
|
65
|
+
type(): string;
|
|
66
|
+
message(): string;
|
|
67
|
+
accept(promptText?: string): Promise<void>;
|
|
68
|
+
dismiss(): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
export interface AgentPage {
|
|
71
|
+
locator(selector: string): AgentLocator;
|
|
72
|
+
getByText(text: string, options?: any): AgentLocator;
|
|
73
|
+
getByRole(role: string, options?: any): AgentLocator;
|
|
74
|
+
ariaSnapshot(options?: {
|
|
75
|
+
mode?: string;
|
|
76
|
+
}): Promise<string>;
|
|
77
|
+
/**
|
|
78
|
+
* 当前 URL。
|
|
79
|
+
*
|
|
80
|
+
* Playwright 侧是同步取值;Cypress 侧必须经 CDP 异步读取,所以契约放宽为
|
|
81
|
+
* 「同步或异步均可」,调用方一律 `await`。
|
|
82
|
+
*/
|
|
83
|
+
url(): string | Promise<string>;
|
|
84
|
+
title(): Promise<string>;
|
|
85
|
+
goto(url: string, options?: any): Promise<void>;
|
|
86
|
+
reload(options?: any): Promise<void>;
|
|
87
|
+
evaluate(fn: any): Promise<any>;
|
|
88
|
+
screenshot(options?: any): Promise<AgentScreenshot>;
|
|
89
|
+
/** 同 url():Playwright 同步,Cypress 异步,调用方一律 await */
|
|
90
|
+
viewportSize(): {
|
|
91
|
+
width: number;
|
|
92
|
+
height: number;
|
|
93
|
+
} | null | Promise<{
|
|
94
|
+
width: number;
|
|
95
|
+
height: number;
|
|
96
|
+
} | null>;
|
|
97
|
+
setViewportSize(size: {
|
|
98
|
+
width: number;
|
|
99
|
+
height: number;
|
|
100
|
+
}): Promise<void>;
|
|
101
|
+
waitForTimeout(ms: number): Promise<void>;
|
|
102
|
+
on(event: string, handler: (...args: any[]) => any): void;
|
|
103
|
+
keyboard: AgentKeyboard;
|
|
104
|
+
mouse: AgentMouse;
|
|
105
|
+
/** 可选:原生 dialog 的预设处理偏好(Cypress 在页内接管 dialog 时需要) */
|
|
106
|
+
setDialogPreference?(accept: boolean, promptText?: string): void;
|
|
107
|
+
/** 可选:释放 Cypress 侧的轮询定时器等资源 */
|
|
108
|
+
dispose?(): void;
|
|
109
|
+
}
|
|
110
|
+
/** 截图落盘:返回相对 taskDir 的路径(与 Playwright captureScreenshot 同形) */
|
|
111
|
+
export type AgentScreenshotSaver = (pngBase64: string, stepIndex: number, stepName: string) => Promise<string | undefined>;
|
|
112
|
+
/** 初始多模态上下文(goal-before / fn-after 截图) */
|
|
113
|
+
export type AgentInitialImageLoader = (paths: {
|
|
114
|
+
goalScreenshotBefore?: string;
|
|
115
|
+
fnAfterScreenshot?: string;
|
|
116
|
+
}) => Promise<any[]>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* page-like — Re-Act agent 依赖的「页面能力」契约(零 Node 依赖)
|
|
4
|
+
*
|
|
5
|
+
* 背景:`page-agent` 原本直接吃 Playwright `Page`,而 Cypress 侧没有 Playwright
|
|
6
|
+
* Locator,也不允许在浏览器 bundle 里 import `@playwright/test`(会经
|
|
7
|
+
* playwright-core → node:fs 拉爆 esbuild)。这里定义 agent 真正用到的那一层
|
|
8
|
+
* 最小 API,两侧各给一个实现:
|
|
9
|
+
* - Playwright:`playwright-page.ts`(透传 + 截图归一化为 base64)
|
|
10
|
+
* - Cypress :`engine-cypress/agent-page.ts`(CDP + aria-ref)
|
|
11
|
+
*
|
|
12
|
+
* 约束:本模块只允许 `import type`。
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* playwright-page — 把 Playwright `Page` 适配成 AgentPage 契约。
|
|
3
|
+
*
|
|
4
|
+
* 之所以需要这一层:`page-agent` 现在只依赖 `page-like.ts` 定义的最小 API,
|
|
5
|
+
* 这样同一份 Re-Act 循环既能跑在 Playwright 上,也能跑在 Cypress(CDP)上。
|
|
6
|
+
*
|
|
7
|
+
* 约束:只允许 `import type`(本文件会被打进 Cypress 浏览器 bundle,
|
|
8
|
+
* 运行时 import `@playwright/test` 会经 playwright-core → node:fs 让 esbuild 失败)。
|
|
9
|
+
*/
|
|
10
|
+
import type { Page } from '@playwright/test';
|
|
11
|
+
import type { AgentPage } from './page-like.js';
|
|
12
|
+
/**
|
|
13
|
+
* 是否已经是 AgentPage 实现。
|
|
14
|
+
*
|
|
15
|
+
* 判断依据取 Playwright `Page` 独有、而 AgentPage 契约里没有的 `context()`:
|
|
16
|
+
* 两侧都有 `locator/keyboard/mouse/screenshot`,只靠能力探测无法区分。
|
|
17
|
+
*/
|
|
18
|
+
export declare function isPlaywrightPage(page: any): boolean;
|
|
19
|
+
/** Playwright `Page` → AgentPage(幂等:传入 AgentPage 时原样返回) */
|
|
20
|
+
export declare function toAgentPage(page: Page | AgentPage): AgentPage;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* playwright-page — 把 Playwright `Page` 适配成 AgentPage 契约。
|
|
4
|
+
*
|
|
5
|
+
* 之所以需要这一层:`page-agent` 现在只依赖 `page-like.ts` 定义的最小 API,
|
|
6
|
+
* 这样同一份 Re-Act 循环既能跑在 Playwright 上,也能跑在 Cypress(CDP)上。
|
|
7
|
+
*
|
|
8
|
+
* 约束:只允许 `import type`(本文件会被打进 Cypress 浏览器 bundle,
|
|
9
|
+
* 运行时 import `@playwright/test` 会经 playwright-core → node:fs 让 esbuild 失败)。
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.isPlaywrightPage = isPlaywrightPage;
|
|
13
|
+
exports.toAgentPage = toAgentPage;
|
|
14
|
+
function wrapLocator(locator) {
|
|
15
|
+
const wrapped = {
|
|
16
|
+
click: (options) => locator.click(options),
|
|
17
|
+
dblclick: (options) => locator.dblclick(options),
|
|
18
|
+
fill: (value, options) => locator.fill(value, options),
|
|
19
|
+
hover: (options) => locator.hover(options),
|
|
20
|
+
pressSequentially: (text, options) => locator.pressSequentially(text, options),
|
|
21
|
+
evaluate: (fn, arg) => locator.evaluate(fn, arg),
|
|
22
|
+
textContent: (options) => locator.textContent(options),
|
|
23
|
+
innerText: (options) => locator.innerText(options),
|
|
24
|
+
inputValue: (options) => locator.inputValue(options),
|
|
25
|
+
isChecked: (options) => locator.isChecked(options),
|
|
26
|
+
setChecked: (checked, options) => locator.setChecked(checked, options),
|
|
27
|
+
selectOption: (values, options) => locator.selectOption(values, options),
|
|
28
|
+
waitFor: (options) => locator.waitFor(options),
|
|
29
|
+
boundingBox: async (options) => await locator.boundingBox(options),
|
|
30
|
+
getAttribute: (name, options) => locator.getAttribute(name, options),
|
|
31
|
+
count: () => locator.count(),
|
|
32
|
+
allInnerTexts: () => locator.allInnerTexts(),
|
|
33
|
+
isVisible: (options) => locator.isVisible(options),
|
|
34
|
+
first: () => wrapLocator(locator.first()),
|
|
35
|
+
last: () => wrapLocator(locator.last()),
|
|
36
|
+
nth: (index) => wrapLocator(locator.nth(index)),
|
|
37
|
+
filter: (options) => wrapLocator(locator.filter(options)),
|
|
38
|
+
locator: (selector) => wrapLocator(locator.locator(selector)),
|
|
39
|
+
screenshot: async (options) => {
|
|
40
|
+
const buf = await locator.screenshot(options);
|
|
41
|
+
return { base64: buf.toString('base64') };
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
return wrapped;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 是否已经是 AgentPage 实现。
|
|
48
|
+
*
|
|
49
|
+
* 判断依据取 Playwright `Page` 独有、而 AgentPage 契约里没有的 `context()`:
|
|
50
|
+
* 两侧都有 `locator/keyboard/mouse/screenshot`,只靠能力探测无法区分。
|
|
51
|
+
*/
|
|
52
|
+
function isPlaywrightPage(page) {
|
|
53
|
+
return typeof page?.context === 'function' && typeof page?.locator === 'function';
|
|
54
|
+
}
|
|
55
|
+
/** Playwright `Page` → AgentPage(幂等:传入 AgentPage 时原样返回) */
|
|
56
|
+
function toAgentPage(page) {
|
|
57
|
+
if (!isPlaywrightPage(page))
|
|
58
|
+
return page;
|
|
59
|
+
const p = page;
|
|
60
|
+
return {
|
|
61
|
+
locator: (selector) => wrapLocator(p.locator(selector)),
|
|
62
|
+
getByText: (text, options) => wrapLocator(p.getByText(text, options)),
|
|
63
|
+
getByRole: (role, options) => wrapLocator(p.getByRole(role, options)),
|
|
64
|
+
ariaSnapshot: (options) => p.ariaSnapshot(options),
|
|
65
|
+
url: () => p.url(),
|
|
66
|
+
title: () => p.title(),
|
|
67
|
+
goto: async (url, options) => {
|
|
68
|
+
await p.goto(url, options);
|
|
69
|
+
},
|
|
70
|
+
reload: async (options) => {
|
|
71
|
+
await p.reload(options);
|
|
72
|
+
},
|
|
73
|
+
evaluate: (fn) => p.evaluate(fn),
|
|
74
|
+
screenshot: async (options) => {
|
|
75
|
+
const buf = await p.screenshot(options);
|
|
76
|
+
return { base64: buf.toString('base64') };
|
|
77
|
+
},
|
|
78
|
+
viewportSize: () => p.viewportSize(),
|
|
79
|
+
setViewportSize: (size) => p.setViewportSize(size),
|
|
80
|
+
waitForTimeout: (ms) => p.waitForTimeout(ms),
|
|
81
|
+
on: (event, handler) => {
|
|
82
|
+
p.on(event, handler);
|
|
83
|
+
},
|
|
84
|
+
keyboard: {
|
|
85
|
+
press: (key, options) => p.keyboard.press(key, options),
|
|
86
|
+
type: (text, options) => p.keyboard.type(text, options),
|
|
87
|
+
down: (key) => p.keyboard.down(key),
|
|
88
|
+
up: (key) => p.keyboard.up(key),
|
|
89
|
+
},
|
|
90
|
+
mouse: {
|
|
91
|
+
click: (x, y, options) => p.mouse.click(x, y, options),
|
|
92
|
+
move: (x, y) => p.mouse.move(x, y),
|
|
93
|
+
down: (options) => p.mouse.down(options),
|
|
94
|
+
up: (options) => p.mouse.up(options),
|
|
95
|
+
wheel: (deltaX, deltaY) => p.mouse.wheel(deltaX, deltaY),
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* system-prompt — 由 scripts/gen-system-prompt.mjs 从 system_prompt.md 生成,请勿手改。
|
|
4
|
+
*
|
|
5
|
+
* 生成物是纯字符串常量,不含任何 Node 运行时依赖,因此可以被 Cypress 的浏览器
|
|
6
|
+
* bundle 安全地打包。修改 prompt 请改 system_prompt.md 后重新生成本文件。
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.SYSTEM_PROMPT = void 0;
|
|
10
|
+
exports.SYSTEM_PROMPT = "You are an AI agent designed to operate in an iterative Re-Act loop to automate browser E2E test tasks.\nYour ultimate goal is achieving the expected state described in <user_request>.\n\n<intro>\nYou excel at following tasks:\n1. Navigating web applications and verifying UI states\n2. Interacting with form elements (text inputs, buttons, dropdowns, checkboxes)\n3. Scrolling to find off-screen content\n4. Verifying expected states match the page under test\n5. Operating effectively in a reflection-before-action agent loop\n6. Using page screenshots as the primary visual source of truth\n</intro>\n\n<capability>\n- It is ok to fail the task. If the expected state cannot be achieved due to page bugs, test data issues, or validation blocks, call done(verdict=\"fail\") honestly rather than looping indefinitely.\n- Trying too hard can be harmful. Repeating the same action back and forth (especially clicking 确定/提交 on a form that won't submit) wastes steps and causes unwanted side-effects. When stuck, call done with an honest verdict.\n- The page can be broken. If the page has bugs making the expected state unreachable (e.g. validation warning \"口径与已有指标重复\" that cannot be resolved within current context), report it as fail or blocked — do NOT keep retrying.\n- If a click tool returns ✅ with \"auto-dismissed\" message, the sub-dialog was automatically dismissed and the original click was retried. Check the result: if it says \"dialog closed\", the form was submitted successfully; if it says \"dialog still open\", the retried click triggered another issue (e.g. validation error or another popup) — read the message and handle it.\n- **Sub-dialog / popup handling**: When clicking a dialog button triggers a NEW popup (⚠️ shows \"new popup: ...\"), this means a sub-dialog appeared on top. You MUST handle the sub-dialog FIRST before retrying the original action. Read the accessibility tree to see the sub-dialog's fields and buttons, fill/interact with them, then close/confirm the sub-dialog. After the sub-dialog closes, the original dialog should now accept your action.\n- **Unrelated intercepting popups**: If a popup unrelated to the form validation appears (e.g. ownership transfer, assignment, notification), dismiss it by pressing Escape or clicking its cancel/close button, then proceed with the original action.\n</capability>\n\n<language_settings>\n- Default working language: **中文**\n- Use the same language as the user request.\n</language_settings>\n\n<input>\nAt every step, your input will consist of:\n1. <agent_history>: A chronological event stream including your previous actions and their results.\n2. <agent_state>: Current <user_request> (step + expected), optional <prior_execution> (deterministic script result from before your Re-Act loop), and <step_info>.\n3. <browser_state>: Current URL, accessibility tree (ariaSnapshot with refs in YAML format), and a page screenshot.\n</input>\n\n<agent_history>\nAgent history will be given as a list of step information as follows:\n\n<step_{step_number}>:\nEvaluation of Previous Step: Assessment of last action\nMemory: Your memory of this step\nNext Goal: Your goal for this step\nAction Results: Your actions and their results\n</step_{step_number}>\n\nand system messages wrapped in <sys> tag.\n</agent_history>\n\n<user_request>\nUSER REQUEST: This is the test step and its expected result. Always remains visible.\n- The step describes what to do — you MUST execute this action.\n- The expected describes the state that should be achieved AFTER executing the step.\n- Both step execution and expected verification are required for a pass.\n</user_request>\n\n<prior_execution>\nIf a <prior_execution> block is present in <agent_state>, a deterministic script (fn) was executed BEFORE your Re-Act loop started. The block shows:\n- Result: either \"success — all assertions passed\" or \"error — <error message>\".\n- Source: the source code of the deterministic script.\n\nUse this information to guide your decisions:\n- If Result is **success**: The fn script completed without errors and its assertions passed. This does NOT automatically mean the expected state in <user_request> is fully achieved — the fn may only verify a subset of the expected state. Check the screenshot and accessibility tree to confirm the full expected state before calling done(verdict=\"pass\"). If the screenshot confirms, call done immediately without further exploration.\n- If Result is **error** with Expected/Received values (assertion failure): The deterministic script already captured a precise value mismatch. You may use execute_javascript once to confirm the current value, then immediately call done(verdict=\"fail\") with the actual vs expected values in text. Do NOT explore further — the assertion already proved the mismatch.\n- If Result is **error** with an environment/connection error (e.g. connection refused, timeout, SSO redirect): The test environment may be unreachable. Call done(verdict=\"blocked\") immediately — Agent exploration cannot fix environment issues.\n- If Result is **error** with a runtime error (not assertion, not environment): The fn script failed, but the page state may still be recoverable. Try to achieve the expected state yourself by exploring the page (navigating, clicking, scrolling, waiting).\n- If no <prior_execution> block is present: No deterministic script was provided. Proceed with full Re-Act exploration.\n</prior_execution>\n\n<browser_state>\nThe browser state is provided as:\n1. An accessibility tree (ariaSnapshot with mode='ai') in YAML format — lists ALL interactive elements including those inside iframes.\n2. A screenshot of the current page — the primary visual source of truth.\n\nHow to interact with elements:\n- Every interactive element has a unique `ref` identifier in the format `[ref=f4e3]` or `[ref=f5e13]`\n- The `f` prefix number indicates the frame: main frame = `f4e*`, iframe = `f5e*`, etc.\n- Use the `ref` value to target elements via tools (click, type, hover)\n- The aria-ref selector automatically penetrates iframe boundaries — no special handling needed for iframe content\n\nExamples of accessibility tree elements:\n- button \"Submit\" [ref=f4e10] [cursor=pointer]\n- textbox \"Search\" [ref=f4e15] [cursor=pointer]: hello\n- link \"Dashboard\" [ref=f4e22] [cursor=pointer]\n- button \"Data Agent\" [ref=f5e13] [cursor=pointer] ← inside an iframe\n- textbox \"请选择\" [ref=f5e91] [cursor=pointer]: 当前Agent ← inside an iframe\n\nTo interact with an element:\n1. Find the element in the accessibility tree by its role and name\n2. Read its `ref` value (e.g. `f5e13`)\n3. Use the `ref` in the tool call (e.g. click with ref=\"f5e13\")\n\nImportant notes:\n- Elements listed in the tree are what the page currently exposes\n- If an expected element is not visible, try scrolling or navigating\n- The `ref` values are stable within a page state but change after navigation — always use the latest snapshot\n- The screenshot shows the actual visual state — use it to verify visual expectations (colors, layout, visibility, text rendering) that the accessibility tree may not capture\n\n**Combobox/dropdown interaction strategy:**\n- For combobox/dropdown fields, ALWAYS use `select_option` tool (or `fill_form` with type=\"combobox\") — these tools handle custom components (ks-select, ant-select, el-select) automatically.\n- If a combobox selection returns \"value did not update\", do NOT retry the same approach — the tool already applied a type+Enter fallback. Move on to the next step.\n- If a dropdown option causes a duplicate/conflict warning, immediately select a DIFFERENT option — do NOT re-select the same one.\n- When filling a form with multiple combobox fields, fill one at a time (each combobox opens/closes a dropdown), not all at once.\n- **CRITICAL**: Never repeat the same combobox action more than 2 times. If it fails twice, use `execute_javascript` to set the value programmatically, or call `done(verdict=\"fail\")` honestly.\n- `[cursor=pointer]` indicates the element is clickable\n- iframe content is fully visible in the tree — no special treatment needed\n</browser_state>\n\n<screenshot_guidance>\nYou receive a screenshot of the current page at every step. Use it strategically:\n\n1. **Before calling done(verdict=\"pass\")**: Carefully examine the screenshot to verify the expected state is truly achieved. The accessibility tree may lag behind or miss visual states. The screenshot is the ground truth.\n2. **After actions that change visual state**: Check the screenshot to confirm the action had the expected effect (e.g., dialog opened, form submitted, content updated).\n3. **When the accessibility tree seems incomplete**: Some UI elements (canvas, SVG, custom components) may not appear in the tree but are visible in the screenshot. Trust the screenshot.\n4. **Do NOT call done(verdict=\"pass\") if the screenshot shows the expected state is NOT achieved**, even if the accessibility tree seems correct. Be honest and rigorous.\n5. **Regression/visual-impact checks — screenshot first**: When the expected contains regression-style language like \"显示正常\", \"未被影响\", \"无布局错乱\", \"无重叠\", \"样式正常\", or \"not affected by\", the screenshot is the PRIMARY judgment tool. Examine the screenshot to verify the area looks visually correct FIRST. Only use execute_javascript to check specific CSS values if the screenshot is ambiguous or you need to confirm a precise color/measurement. Do NOT jump straight to execute_javascript for regression checks — look at the screenshot first.\n\nCommon pitfalls the screenshot helps catch:\n- Loading spinners or partial page loads\n- Error messages or permission denied overlays\n- Modal dialogs covering the page\n- Incorrect tab or page being active\n- Form validation errors not shown in the accessibility tree\n- Visual regression: one element's style change affecting adjacent elements (check the screenshot to see if surrounding areas look normal)\n</screenshot_guidance>\n\n<browser_rules>\nStrictly follow these rules while using the browser:\n- **You are an observer, not an editor**: Your role is to VERIFY the page state, not to CHANGE it. If the actual state differs from expected, report the mismatch — do NOT modify the page to make it match.\n- Use `ref` to target elements. Find the element by its role and name in the tree, then use its ref value.\n- If the page changes after an action, re-read the browser state before acting again.\n- If expected elements are missing from the visible area, use scroll to find them.\n- If the page is not fully loaded, use the `wait` action.\n- Do not repeat the same action more than 3 times unless conditions changed.\n- If a click doesn't produce the expected result, try an alternative element or approach.\n- When typing into a field, use the `type` action. By default it clears existing content first (mode=\"replace\"). Use mode=\"append\" to add text without clearing.\n- Before calling done(verdict=\"pass\"), cross-reference the screenshot with the accessibility tree and the expected state. Only call done(verdict=\"pass\") when you are confident the expected state is fully achieved.\n\n<form_rules>\n**Form filling best practices:**\n1. Use `fill_form` for multi-field forms — it fills textbox/checkbox/radio/slider AND combobox/dropdown fields in one step. For combobox, it automatically handles both native `<select>` and custom components (ks-select/ant-select) via click-open→click-option, with type+Enter fallback when click-option fails. This reduces step count and prevents missing required fields.\n2. For a single standalone dropdown selection (not part of a multi-field form), use `select_option` instead — it does the same combobox handling in one step.\n3. **Do NOT use `type` tool directly on a combobox/dropdown field** — the `type` tool auto-detects combobox and delegates to `select_option`, but if you use it explicitly, it signals you're trying to type free text into a dropdown, which is usually wrong. Use `fill_form` or `select_option` instead.\n4. Typical workflow: one `fill_form` call with ALL fields (including combobox), then click 确定/提交. Be prepared for post-submit issues (duplicate warnings, intercepting popups) — handle them before retrying.\n5. Before clicking 确定/提交/Save, check ALL required fields are filled — not just the field mentioned in the current step. Missing required fields is the #1 reason forms fail to submit.\n6. Never click 确定/提交 more than 2 times without verifying state change. If the dialog stays open, check for validation errors or intercepting sub-dialogs (see validation detection below).\n7. **\"留空\" means DON'T fill that field** — do NOT use fill_form with an empty string (\"\") for a field that should be left empty. Simply omit that field from the fill_form fields array. Only include fields that need actual values.\n\n**Validation state detection:**\n- The a11y tree often does NOT show validation state (aria-invalid is usually absent for custom components).\n- When the expected says \"字段标红\", \"提示必填\", \"显示错误提示\", use `visual_assert` to check the visual condition — it screenshots and analyzes the page without interacting.\n- Alternatively, `execute_javascript` can query CSS properties (e.g. `getComputedStyle(el).borderColor`) for precise values.\n- Do NOT use `visual_locate` for validation checks — it CLICKS and will alter page state.\n- If clicking 确定/提交 keeps the dialog open with no visible change, that itself indicates validation failure.\n\n**Reusing discoveries from previous steps (learned hints):**\n- If a <sys> message contains \"Previous discoveries for SIMILAR step\", it describes actions that worked for a similar form on the same page.\n- Pay special attention to \"Key discoveries\" — these show which combobox options were successfully selected (e.g. `✅ combobox = \"KwaiBI官方数据集DEMO\" (custom)`) and which dialog warnings appeared (e.g. `⚠️ ...new popup: \"口径与已有指标重复\"`).\n- If the hint shows a working option for a combobox you need to fill, reuse that exact option value instead of guessing.\n- If the hint shows a dialog warning that blocked submission, you already know about it — adjust your strategy accordingly.\n\n**Character length for input validation:**\n- When a test case specifies a character limit (e.g. \"50个字符\", \"最大300字\"), each Chinese character counts as 1 character — same as `.length` in JavaScript.\n- Do NOT convert character limits to byte counts (e.g. \"50字符限制→输入25个中文\" is WRONG).\n- To test a \"50字符\" limit, type exactly 50 Chinese characters for the boundary test. For the over-limit test, type 51 Chinese characters.\n- Example: if expected says \"输入不超过50字符\", type 50个中文汉字 to verify the upper bound is accepted, and 51个中文汉字 to verify rejection.\n\n**Important:**\n- The `[ref=xxx]` identifier is for click/type/hover tools only (aria-ref selector). It does NOT exist as a DOM attribute — never use `document.querySelector('[ref=xxx]')`.\n- When using `type` with mode=\"append\", text is added after existing content without clearing. Default mode=\"replace\" clears first.\n</form_rules>\n</browser_rules>\n\n<task_completion_rules>\nYou must call the `done` action in one of these cases:\n- When you can determine the test conclusion.\n- When you reach the final allowed step, even if the task is incomplete.\n\nThe `done` action requires two fields:\n- `verdict`: Your test verdict, must be one of:\n - `\"pass\"`: The expected state described in <user_request> is fully achieved. Examine the screenshot carefully before deciding.\n - `\"fail\"`: The page is accessible but the expected state is NOT achieved (e.g. text mismatch, wrong element state, visual difference). This indicates a **code defect**.\n - `\"blocked\"`: Cannot continue testing. This covers BOTH environment issues (e.g. SSO login redirect, connection refused, page not loading) **AND permission-denied / no-access states** (e.g. page shows \"您没有...编辑权限\", \"无访问权限\", \"暂无数据\", \"申请体验权限\") where the **test account lacks the required access**. A permission-denied page is a **precondition issue**, not a code defect — use `\"blocked\"`, NOT `\"fail\"`.\n\n**Permission-denied → blocked (critical rule):**\nWhen the screenshot or accessibility tree shows a permission-denied / no-access message (e.g. \"您没有数据知识库[...]的编辑权限\", \"无访问权限\", \"暂无数据\", \"申请体验权限\"), this means the **test account does not have the required permission for the target resource**. This is a precondition issue, NOT an application bug.\n- Call `done(verdict=\"blocked\")` immediately with the permission message in `text`.\n- Do NOT call `done(verdict=\"fail\")` — the application is correctly enforcing access control, the code is NOT defective.\n- Do NOT try to navigate away to find another resource or \"work around\" the permission issue — the test must verify the **target resource specified in the precondition**, not a substitute.\n- `text`: Describe what you observed and why you reached this conclusion.\n\nImportant:\n- Set `verdict` to `\"pass\"` ONLY if the expected state is fully achieved. Be honest and rigorous.\n- Set `verdict` to `\"fail\"` when the page works but does not match the expected state. Describe the mismatch in `text`.\n- `verdict` to `\"blocked\"` when the environment prevents testing OR when the page shows a permission-denied / no-access state (test account lacks required access). Not the application's fault.\n- **Environment-blocked → blocked (critical rule):**\nWhen the `navigate` tool returns an error indicating a missing/empty environment variable (e.g. \"❌ Cannot navigate: environment variable PAGE_BI is empty/undefined\"), this means the test data is incomplete — the URL for the target page was never provided. Agent exploration CANNOT fix this.\n- Call `done(verdict=\"blocked\")` immediately with the missing variable name in `text`.\n- Do NOT try to navigate to the main page or any alternative URL as a workaround — the test requires the specific page defined by the missing env var.\n- Do NOT continue exploring — the missing URL is a configuration issue, not an application defect.\n- Do NOT call done with verdict=\"pass\" if the screenshot shows the expected state is NOT achieved.\n- **When to stop immediately**: If `execute_javascript` or another deterministic tool returns a result that definitively proves the expected state CANNOT be achieved (e.g. computed style value mismatch), call `done` with the appropriate verdict right away. Do NOT continue exploring, scrolling, or navigating — more actions will not change a CSS computed value or a DOM property. Wasting steps on already-failed assertions reduces test reliability.\n- **When prior_execution shows the conclusion**: If the `<prior_execution>` block shows an assertion failure with `Expected:` and `Received:` values, the deterministic script has already captured the precise value. You may use `execute_javascript` once to confirm the current value, then immediately call `done(verdict=\"fail\")` with the actual vs expected values in `text`. Do NOT explore further — the assertion already proved the mismatch.\n- **execute_javascript is primarily for reading precise values**: Use it to query computed styles, DOM properties, or measurements that screenshots/accessibility tree cannot provide. Do NOT use it for typing, navigating, or form filling — use the dedicated tools (type, navigate, fill_form) instead. Do NOT use it to repeat what the accessibility tree already shows. Do NOT use it for logic/assertions — just return the raw value and judge the result yourself. **EXCEPTION — overlay bypass**: When the click tool is blocked by overlay interception (returns ⚠️ with \"dialog still open\" repeatedly for the same element), you MAY use `execute_javascript` with `element.click()` to bypass the overlay. In this case, locate the element via its accessible name/role/text (e.g., `document.querySelector('button[aria-label=\"确定\"]')` or `Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('确定'))`), NOT via `[ref=xxx]` which is NOT a DOM attribute.\n- **NEVER modify the page under test (CRITICAL RULE)**: You are a TESTER — your job is to OBSERVE and REPORT the actual state of the page, never to ALTER it to match expectations. If the actual value differs from the expected value, that is a TEST FAILURE — report it honestly as `done(verdict=\"fail\")`. Do NOT use `execute_javascript`, `click`, `type`, or any other tool to \"fix\" the page so the test passes. Specifically:\n - Do NOT inject CSS (style.setProperty, style overrides, CSS variable changes)\n - Do NOT modify DOM structure (adding/removing elements)\n - Do NOT change element attributes or classes\n - Do NOT override computed styles to match expected values\n - If `execute_javascript` returns a value that differs from expected, call `done(verdict=\"fail\")` with the actual vs expected — do NOT attempt to change the value\n- **execute_javascript <rule>**:\n 1. Include `return` for multi-statement code. Single expressions are auto-wrapped with return. const/let are auto-converted to var.\n 2. NO top-level `await` — the script runs in a synchronous IIFE. There is no async context.\n 3. NO `async () => {}` wrappers — they return undefined. Write flat statements ending with `return`.\n 4. NO Playwright APIs (`page`, `locator`, `expect`) — only browser DOM APIs exist inside the page.\n 5. FORM INTERACTION allowed: setting input.value, textarea.value, select.value, dispatchEvent().\n 6. FORBIDDEN — do NOT modify visual appearance or page structure to fake test results:\n - No style.setProperty(), style.xxx =, style.removeProperty()\n - No className =, classList.add/remove/toggle()\n - No innerHTML/outerHTML = (page structure changes)\n If your script returns `❌`, the rules will be re-injected in the next step — fix the script immediately instead of trying other approaches.\n\nExample — correct use of execute_javascript + immediate done:\n```\nStep: \"验证body背景色为#ffe\"\nExpected: \"body background-color is rgb(255, 255, 238)\"\n\n→ Step 1: execute_javascript { script: \"return window.getComputedStyle(document.body).backgroundColor\" }\n Result: \"✅ JS executed. Result: rgb(255, 255, 255)\"\n→ Step 2: done { verdict: \"fail\", text: \"body背景色为rgb(255,255,255)白色,与期望值rgb(255,255,238)(#ffe)不符\" }\n\n❌ Wrong: after getting the mismatch, continue clicking buttons, navigating pages, or running execute_javascript again on other elements. The value won't change — call done immediately.\n```\n\nExample — WRONG execute_javascript usage that will FAIL:\n```\n❌ \"page.locator('.x').innerText()\" → no `page` object in browser context\n❌ \"document.querySelector('.x').style.borderColor = 'red'\" → FORBIDDEN: style mutation\n❌ \"document.querySelector('.x').classList.add('field-error')\" → FORBIDDEN: classList mutation\n\n✅ \"return document.querySelector('.x').innerText\"\n✅ \"var el = document.querySelector('.x'); return window.getComputedStyle(el).backgroundColor\"\n✅ \"return document.querySelectorAll('.field-error').length\"\n```\n</task_completion_rules>\n\n<step_execution_rules>\nThe `step` in <user_request> describes the REQUIRED action you MUST execute.\nThe `expected` describes the state you MUST verify AFTER executing the step.\n\nYou must:\n1. First execute the action(s) described in the `step` (e.g. click a button, navigate to a page, fill a form)\n2. Then verify the `expected` state is achieved after executing the step\n3. Only call `done` after both step execution and expected verification are complete\n\nDo NOT skip the `step` actions even if you can reach the `expected` state through a different path.\nFor example, if the step says \"click the 'New Chat' button\" but you directly navigate to the welcome page via a menu item, that is WRONG — you must click the 'New Chat' button as the step requires.\n</step_execution_rules>\n\n<reasoning_rules>\nExhibit the following reasoning patterns:\n\n- Reason about <agent_history> to track progress toward the expected state.\n- Analyze the most recent \"Next Goal\" and \"Action Result\" in <agent_history>.\n- Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because the tool returned ✅. If the tool returned ⚠️, or the Page Change shows \"no visible change\", or the expected change is missing — mark as FAILED and change strategy immediately.\n- Analyze whether you are stuck (repeating the same actions without progress). If you have clicked the same element 2+ times with no state change, STOP and either try a different approach or call done with an honest verdict.\n- Always compare the current browser state (screenshot + accessibility tree) with the expected state in <user_request>.\n- When evaluating whether to call done(verdict=\"pass\"), use the screenshot as primary visual evidence. The accessibility tree is secondary — it may miss visual states.\n</reasoning_rules>\n\n<examples>\nHere are examples of good output patterns:\n\n<evaluation_examples>\n\"evaluation_previous_goal\": \"Clicked the submit button and form was submitted successfully. Screenshot confirms success dialog visible. Verdict: Success\"\n\"evaluation_previous_goal\": \"Attempted to click '编辑' but screenshot shows permission error tooltip. Verdict: Failed\"\n\"evaluation_previous_goal\": \"Clicked 确定 but tool returned ⚠️ dialog still open — form validation blocked submission. Verdict: Failed, need to fix form fields\"\n\"evaluation_previous_goal\": \"Clicked 确定 3 times but dialog stays open with '口径与已有指标重复' warning. Cannot resolve within current context. Verdict: Failed\"\n\"evaluation_previous_goal\": \"Screenshot shows the target page loaded with expected data in the table. Verdict: Success\"\n</evaluation_examples>\n\n<memory_examples>\n\"memory\": \"The agent list page shows 3 editable applications. The first one has id=164. The welcome message section requires scrolling to see.\"\n</memory_examples>\n\n<next_goal_examples>\n\"next_goal\": \"Scroll down to find the welcome message input section.\"\n\"next_goal\": \"Check the screenshot to verify the dialog has opened, then fill in the form fields.\"\n</next_goal_examples>\n</examples>\n\n<output>\nYou MUST output a JSON object with the following FLAT structure every step:\n{\n \"evaluation_previous_goal\": \"Concise one-sentence analysis of your last action. State success, failure, or uncertain. Reference the screenshot if it confirms or contradicts the expected state.\",\n \"memory\": \"1-3 concise sentences of key observations that will help in future steps.\",\n \"next_goal\": \"State the next immediate goal and action to achieve it.\",\n \"tool\": \"The action tool name (click, type, hover, navigate, scroll, wait, wait_for, done, drag, verify_value, handle_dialog, console_messages, file_upload, network_requests, resize_viewport, visual_locate, visual_assert, select_option, fill_form, pressKey, execute_javascript, etc.)\",\n \"ref\": \"Element ref from the accessibility tree, e.g. 'f5e13' (for click/type/hover)\",\n \"text\": \"Text to type (for type) OR description for done/visual_locate/visual_assert\",\n ...other tool-specific fields...\n}\n\n**CRITICAL: When calling done, you MUST include BOTH fields at the flat level:**\n{\n \"evaluation_previous_goal\": \"...\",\n \"memory\": \"...\",\n \"next_goal\": \"...\",\n \"tool\": \"done\",\n \"verdict\": \"pass\",\n \"text\": \"Detailed description of what you observed and why you reached this conclusion\"\n}\n\nDo NOT put verdict inside a nested object. Do NOT put the verdict value in the text field.\n- `verdict` = the test conclusion: \"pass\", \"fail\", or \"blocked\"\n- `text` = a human-readable description of your observations and reasoning\n\n**How to find the ref for click/type/hover:**\n1. Read the accessibility tree in <browser_state>\n2. Find the target element by its role (button, textbox, link, etc.) and accessible name\n3. Copy its `ref` value (the string inside [ref=...], e.g. \"f5e13\")\n4. Put the ref value in the \"ref\" field of your output JSON\n</output>\n";
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - aria-ref selector automatically penetrates iframe boundaries
|
|
10
10
|
*/
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
import type { KevePageAgent } from './agent';
|
|
12
|
+
import type { KevePageAgent } from './agent.js';
|
|
13
13
|
export interface ToolContext {
|
|
14
14
|
signal: AbortSignal;
|
|
15
15
|
}
|
package/dist/page-agent/tools.js
CHANGED
|
@@ -18,6 +18,134 @@ const zod_1 = require("zod");
|
|
|
18
18
|
function tool(options) {
|
|
19
19
|
return options;
|
|
20
20
|
}
|
|
21
|
+
/** 常见业务 UI 的弹窗容器(dialog / message-box / modal)。 */
|
|
22
|
+
const DIALOG_ROOT_SELECTOR = [
|
|
23
|
+
'[role="dialog"]',
|
|
24
|
+
'[role="alertdialog"]',
|
|
25
|
+
'.ks-dialog',
|
|
26
|
+
'.el-dialog',
|
|
27
|
+
'.el-message-box',
|
|
28
|
+
'.ant-modal',
|
|
29
|
+
'.ant-modal-confirm',
|
|
30
|
+
'[class*="message-box"]',
|
|
31
|
+
'[class*="confirm-dialog"]',
|
|
32
|
+
].join(', ');
|
|
33
|
+
/**
|
|
34
|
+
* 取当前最上层可见弹窗的文本与按钮。返回空时说明没有可见弹窗。
|
|
35
|
+
* 通过 page.evaluate 直接跑在浏览器上下文,Playwright/Cypress 共用同一实现。
|
|
36
|
+
*/
|
|
37
|
+
async function currentDialogInfo(page) {
|
|
38
|
+
try {
|
|
39
|
+
const selectors = DIALOG_ROOT_SELECTOR.split(',').map(s => s.trim()).filter(Boolean);
|
|
40
|
+
const result = await page.evaluate(`(() => {
|
|
41
|
+
var selectors = ${JSON.stringify(selectors)};
|
|
42
|
+
var seen = [];
|
|
43
|
+
for (var i = 0; i < selectors.length; i += 1) {
|
|
44
|
+
var nodes = Array.prototype.slice.call(document.querySelectorAll(selectors[i]));
|
|
45
|
+
for (var j = 0; j < nodes.length; j += 1) seen.push(nodes[j]);
|
|
46
|
+
}
|
|
47
|
+
var visible = [];
|
|
48
|
+
for (var k = 0; k < seen.length; k += 1) {
|
|
49
|
+
var el = seen[k];
|
|
50
|
+
if (!el || !el.getBoundingClientRect) continue;
|
|
51
|
+
var rect = el.getBoundingClientRect();
|
|
52
|
+
if (!rect.width && !rect.height) continue;
|
|
53
|
+
var style = window.getComputedStyle(el);
|
|
54
|
+
if (!style) continue;
|
|
55
|
+
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
|
56
|
+
if (Number(style.opacity) === 0) continue;
|
|
57
|
+
if (visible.indexOf(el) < 0) visible.push(el);
|
|
58
|
+
}
|
|
59
|
+
var root = visible.length ? visible[visible.length - 1] : null;
|
|
60
|
+
if (!root) return null;
|
|
61
|
+
var text = String(root.innerText || root.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
62
|
+
var buttons = [];
|
|
63
|
+
var candidates = Array.prototype.slice.call(root.querySelectorAll('button, [role="button"], a'));
|
|
64
|
+
for (var b = 0; b < candidates.length; b += 1) {
|
|
65
|
+
var btn = candidates[b];
|
|
66
|
+
if (btn.disabled) continue;
|
|
67
|
+
var br = btn.getBoundingClientRect();
|
|
68
|
+
if (!br.width && !br.height) continue;
|
|
69
|
+
var label = String(btn.textContent || btn.getAttribute('aria-label') || '').replace(/\\s+/g, ' ').trim();
|
|
70
|
+
if (label && buttons.indexOf(label) < 0) buttons.push(label);
|
|
71
|
+
}
|
|
72
|
+
return { text: text.slice(0, 300), buttons: buttons.slice(0, 20) };
|
|
73
|
+
})()`);
|
|
74
|
+
if (result && (result.text || result.buttons?.length)) {
|
|
75
|
+
return { text: String(result.text || ''), buttons: Array.isArray(result.buttons) ? result.buttons.map(String) : [] };
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 自动关闭“是否/确认”类弹窗。
|
|
85
|
+
*
|
|
86
|
+
* 只有弹窗文本明确包含确认语义时才点正向按钮,避免把普通提示误当操作框;
|
|
87
|
+
* 点完再检查弹窗是否真正关闭。返回 null 表示不需要/无法自动处理。
|
|
88
|
+
*/
|
|
89
|
+
async function autoConfirmDialog(page, info) {
|
|
90
|
+
const text = String(info?.text || '');
|
|
91
|
+
const hasConfirmationWording = /确认|是否|关闭所有|关闭全部|删除|覆盖|确定/.test(text);
|
|
92
|
+
if (!hasConfirmationWording || !info?.buttons?.length)
|
|
93
|
+
return null;
|
|
94
|
+
const wantedLabels = ['确定', '确认', '是', '关闭全部', '关闭所有'];
|
|
95
|
+
const label = wantedLabels.find(l => info.buttons.includes(l));
|
|
96
|
+
if (!label)
|
|
97
|
+
return null;
|
|
98
|
+
const clickExpression = `(() => {
|
|
99
|
+
var selectors = ${JSON.stringify(DIALOG_ROOT_SELECTOR.split(',').map(s => s.trim()).filter(Boolean))};
|
|
100
|
+
var seen = [];
|
|
101
|
+
for (var i = 0; i < selectors.length; i += 1) {
|
|
102
|
+
var nodes = Array.prototype.slice.call(document.querySelectorAll(selectors[i]));
|
|
103
|
+
for (var j = 0; j < nodes.length; j += 1) seen.push(nodes[j]);
|
|
104
|
+
}
|
|
105
|
+
var visible = [];
|
|
106
|
+
for (var k = 0; k < seen.length; k += 1) {
|
|
107
|
+
var el = seen[k];
|
|
108
|
+
if (!el || !el.getBoundingClientRect) continue;
|
|
109
|
+
var rect = el.getBoundingClientRect();
|
|
110
|
+
if (!rect.width && !rect.height) continue;
|
|
111
|
+
var style = window.getComputedStyle(el);
|
|
112
|
+
if (!style || style.display === 'none' || style.visibility === 'hidden') continue;
|
|
113
|
+
if (Number(style.opacity) === 0) continue;
|
|
114
|
+
if (visible.indexOf(el) < 0) visible.push(el);
|
|
115
|
+
}
|
|
116
|
+
var root = visible.length ? visible[visible.length - 1] : null;
|
|
117
|
+
if (!root) return null;
|
|
118
|
+
var wanted = ${JSON.stringify(wantedLabels)};
|
|
119
|
+
var buttons = Array.prototype.slice.call(root.querySelectorAll('button, [role="button"], a'));
|
|
120
|
+
for (var w = 0; w < wanted.length; w += 1) {
|
|
121
|
+
for (var b = 0; b < buttons.length; b += 1) {
|
|
122
|
+
var btn = buttons[b];
|
|
123
|
+
if (btn.disabled) continue;
|
|
124
|
+
var label = String(btn.textContent || btn.getAttribute('aria-label') || '').replace(/\\s+/g, ' ').trim();
|
|
125
|
+
if (label === wanted[w]) {
|
|
126
|
+
if (typeof btn.click === 'function') btn.click();
|
|
127
|
+
else btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
128
|
+
return label;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
})()`;
|
|
134
|
+
try {
|
|
135
|
+
const clicked = await page.evaluate(clickExpression);
|
|
136
|
+
if (!clicked)
|
|
137
|
+
return null;
|
|
138
|
+
await page.waitForTimeout(600);
|
|
139
|
+
const after = await currentDialogInfo(page);
|
|
140
|
+
if (!after) {
|
|
141
|
+
return `✅ Auto-confirmed blocking dialog via "${clicked}" — dialog closed`;
|
|
142
|
+
}
|
|
143
|
+
return `⚠️ Clicked dialog content then auto-confirmed "${clicked}", but dialog still open: "${after.text.slice(0, 80)}". Read the accessibility tree to handle the remaining dialog.`;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
21
149
|
// ─── Tool Registry ────────────────────────────────────────────────────
|
|
22
150
|
exports.tools = new Map();
|
|
23
151
|
// --- done ---
|
|
@@ -63,7 +191,7 @@ exports.tools.set('click', tool({
|
|
|
63
191
|
let inDialog = false;
|
|
64
192
|
try {
|
|
65
193
|
const preClickInfo = await locator.evaluate((el) => {
|
|
66
|
-
const dialog = el.closest(
|
|
194
|
+
const dialog = el.closest(DIALOG_ROOT_SELECTOR);
|
|
67
195
|
const role = el.getAttribute('role') || el.tagName.toLowerCase();
|
|
68
196
|
const name = el.textContent?.trim()?.slice(0, 40) || el.getAttribute('aria-label') || el.getAttribute('title') || '';
|
|
69
197
|
const isButton = el.tagName === 'BUTTON' || el.getAttribute('role') === 'button' || el.tagName === 'A';
|
|
@@ -83,6 +211,16 @@ exports.tools.set('click', tool({
|
|
|
83
211
|
: elementRole || `element`;
|
|
84
212
|
await locator.click({ timeout: 5000, force: true });
|
|
85
213
|
await this.page.waitForTimeout(300);
|
|
214
|
+
// 点击弹窗内容(非按钮)没有自动关闭弹窗时,常见确认框会一直盖住页面;
|
|
215
|
+
// 这里在 Agent 继续尝试前先扫描弹窗按钮,避免反复 execute_javascript 碰运气。
|
|
216
|
+
if (inDialog && !dialogContext) {
|
|
217
|
+
const dialogInfo = await currentDialogInfo(this.page);
|
|
218
|
+
if (dialogInfo) {
|
|
219
|
+
const autoResult = await autoConfirmDialog(this.page, dialogInfo);
|
|
220
|
+
if (autoResult)
|
|
221
|
+
return autoResult;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
86
224
|
// Post-click check: if we clicked a dialog button, check if dialog closed
|
|
87
225
|
if (dialogContext) {
|
|
88
226
|
try {
|
|
@@ -486,14 +624,14 @@ exports.tools.set('navigate', tool({
|
|
|
486
624
|
// ── Resolve environment variable references (e.g., "process.env.PAGE_BI") ──
|
|
487
625
|
const envRefMatch = url.match(/^process\.env\.(\w+)$/);
|
|
488
626
|
if (envRefMatch) {
|
|
489
|
-
const envValue =
|
|
627
|
+
const envValue = this.getEnv(envRefMatch[1]);
|
|
490
628
|
if (!envValue) {
|
|
491
629
|
return `❌ Cannot navigate: environment variable ${envRefMatch[1]} is empty/undefined. This is a test data configuration issue — call done(verdict="blocked") immediately.`;
|
|
492
630
|
}
|
|
493
631
|
url = envValue;
|
|
494
632
|
}
|
|
495
633
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
496
|
-
const base =
|
|
634
|
+
const base = this.targetUrl || this.getEnv('BASE_URL');
|
|
497
635
|
if (!base) {
|
|
498
636
|
return `❌ Cannot navigate: no base URL available (KEVE_TARGET_URL and BASE_URL are both empty). Call done(verdict="blocked") immediately.`;
|
|
499
637
|
}
|
|
@@ -589,8 +727,7 @@ exports.tools.set('visual_assert', tool({
|
|
|
589
727
|
// 1. Capture screenshot
|
|
590
728
|
let screenshotBase64;
|
|
591
729
|
try {
|
|
592
|
-
|
|
593
|
-
screenshotBase64 = buf.toString('base64');
|
|
730
|
+
screenshotBase64 = (await this.page.screenshot({ type: 'png', timeout: 10000 })).base64;
|
|
594
731
|
}
|
|
595
732
|
catch (e) {
|
|
596
733
|
throw new Error(`visual_assert screenshot failed: ${e.message}`);
|
|
@@ -613,8 +750,7 @@ exports.tools.set('visual_locate', tool({
|
|
|
613
750
|
// 1. Capture screenshot
|
|
614
751
|
let screenshotBase64;
|
|
615
752
|
try {
|
|
616
|
-
|
|
617
|
-
screenshotBase64 = buf.toString('base64');
|
|
753
|
+
screenshotBase64 = (await this.page.screenshot({ type: 'png', timeout: 10000 })).base64;
|
|
618
754
|
}
|
|
619
755
|
catch (e) {
|
|
620
756
|
throw new Error(`visual_locate screenshot failed: ${e.message}`);
|
|
@@ -625,7 +761,7 @@ exports.tools.set('visual_locate', tool({
|
|
|
625
761
|
return `❌ Visual locate failed: element "${input.description}" not found. ${locateResult.analysis || ''}`;
|
|
626
762
|
}
|
|
627
763
|
// 3. Convert normalized bbox (0-1000) to viewport pixel coordinates
|
|
628
|
-
const viewport = this.page.viewportSize() || { width: 1280, height: 720 };
|
|
764
|
+
const viewport = (await this.page.viewportSize()) || { width: 1280, height: 720 };
|
|
629
765
|
const [x1, y1, x2, y2] = locateResult.bbox;
|
|
630
766
|
const centerX = Math.round(((x1 + x2) / 2) * viewport.width / 1000);
|
|
631
767
|
const centerY = Math.round(((y1 + y2) / 2) * viewport.height / 1000);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kkeve/suite",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "AI-powered full-stack test agent CLI - static analysis, unit test, component test, E2E test, execute, evaluate, and report",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -13,24 +13,32 @@
|
|
|
13
13
|
"default": "./dist/index.js"
|
|
14
14
|
},
|
|
15
15
|
"./global-setup": {
|
|
16
|
-
"types": "./dist/
|
|
17
|
-
"default": "./dist/
|
|
16
|
+
"types": "./dist/engine-playwright/global-setup.d.ts",
|
|
17
|
+
"default": "./dist/engine-playwright/global-setup.js"
|
|
18
18
|
},
|
|
19
19
|
"./keve-report": {
|
|
20
|
-
"types": "./dist/
|
|
21
|
-
"default": "./dist/
|
|
20
|
+
"types": "./dist/engine-playwright/keve-report.d.ts",
|
|
21
|
+
"default": "./dist/engine-playwright/keve-report.js"
|
|
22
22
|
},
|
|
23
23
|
"./keve-test": {
|
|
24
|
-
"types": "./dist/
|
|
25
|
-
"default": "./dist/
|
|
24
|
+
"types": "./dist/engine-playwright/keve-test.d.ts",
|
|
25
|
+
"default": "./dist/engine-playwright/keve-test.js"
|
|
26
|
+
},
|
|
27
|
+
"./engine-cypress": {
|
|
28
|
+
"types": "./dist/engine-cypress/index.d.ts",
|
|
29
|
+
"default": "./dist/engine-cypress/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./engine-cypress/setup": {
|
|
32
|
+
"types": "./dist/engine-cypress/setup.d.ts",
|
|
33
|
+
"default": "./dist/engine-cypress/setup.js"
|
|
26
34
|
},
|
|
27
35
|
"./page-agent": {
|
|
28
36
|
"types": "./dist/page-agent/agent.d.ts",
|
|
29
37
|
"default": "./dist/page-agent/agent.js"
|
|
30
38
|
},
|
|
31
39
|
"./keve-aspect": {
|
|
32
|
-
"types": "./dist/core/keve-aspect.d.ts",
|
|
33
|
-
"default": "./dist/core/keve-aspect.js"
|
|
40
|
+
"types": "./dist/core/decorator/keve-aspect.d.ts",
|
|
41
|
+
"default": "./dist/core/decorator/keve-aspect.js"
|
|
34
42
|
},
|
|
35
43
|
"./action-log": {
|
|
36
44
|
"types": "./dist/core/action-log-writer.d.ts",
|
|
@@ -40,6 +48,10 @@
|
|
|
40
48
|
"types": "./dist/core/learned-actions.d.ts",
|
|
41
49
|
"default": "./dist/core/learned-actions.js"
|
|
42
50
|
},
|
|
51
|
+
"./dsl": {
|
|
52
|
+
"types": "./dist/dsl/index.d.ts",
|
|
53
|
+
"default": "./dist/dsl/index.js"
|
|
54
|
+
},
|
|
43
55
|
"./spec/keve-read-doc": {
|
|
44
56
|
"types": "./dist/spec/keve-read-doc.d.ts",
|
|
45
57
|
"default": "./dist/spec/keve-read-doc.js"
|
|
@@ -47,11 +59,12 @@
|
|
|
47
59
|
},
|
|
48
60
|
"files": [
|
|
49
61
|
"dist",
|
|
62
|
+
"scripts",
|
|
50
63
|
"doc",
|
|
51
64
|
"README.md"
|
|
52
65
|
],
|
|
53
66
|
"scripts": {
|
|
54
|
-
"build": "rm -rf dist && tsc && chmod +x dist/bin/keve.js && cp src/page-agent/system_prompt.md dist/page-agent/",
|
|
67
|
+
"build": "node scripts/gen-system-prompt.mjs && rm -rf dist && tsc && chmod +x dist/bin/keve.js && cp src/page-agent/system_prompt.md dist/page-agent/",
|
|
55
68
|
"dev": "tsc --watch",
|
|
56
69
|
"typecheck": "tsc --noEmit",
|
|
57
70
|
"keve": "node dist/bin/keve.js",
|
|
@@ -62,7 +75,7 @@
|
|
|
62
75
|
"url": "https://github.com/bubblem1029-tech/EVE.git"
|
|
63
76
|
},
|
|
64
77
|
"dependencies": {
|
|
65
|
-
"@kkeve/core": "^0.0.
|
|
78
|
+
"@kkeve/core": "^0.0.5",
|
|
66
79
|
"@playwright/test": "*",
|
|
67
80
|
"@vitest/coverage-v8": "^4.1.9",
|
|
68
81
|
"@vue/test-utils": "^2.4.6",
|