@kkeve/suite 0.0.1 → 0.0.2
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/README.md +53 -0
- package/dist/bin/keve.d.ts +13 -0
- package/dist/bin/keve.js +164 -0
- package/dist/commands/auth-update.d.ts +16 -0
- package/dist/commands/auth-update.js +88 -0
- package/dist/commands/clean.d.ts +10 -0
- package/dist/commands/clean.js +78 -0
- package/dist/commands/config.d.ts +17 -0
- package/dist/commands/config.js +99 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +292 -0
- package/dist/commands/report.d.ts +12 -0
- package/dist/commands/report.js +159 -0
- package/dist/commands/run.d.ts +29 -0
- package/dist/commands/run.js +417 -0
- package/dist/commands/test.d.ts +12 -0
- package/dist/commands/test.js +102 -0
- package/dist/commands/vitest.d.ts +14 -0
- package/dist/commands/vitest.js +166 -0
- package/dist/config.d.ts +109 -0
- package/dist/config.js +291 -0
- package/dist/core/action-log-writer.d.ts +47 -0
- package/dist/{tools/write-file.js → core/action-log-writer.js} +42 -26
- package/dist/core/auth.d.ts +17 -0
- package/dist/core/auth.js +224 -0
- package/dist/core/cdp-resolver.d.ts +5 -0
- package/dist/core/cdp-resolver.js +93 -0
- package/dist/core/generateReport/report.d.ts +19 -0
- package/dist/core/generateReport/report.js +91 -0
- package/dist/core/generateReport/reportData.d.ts +215 -0
- package/dist/core/generateReport/reportData.js +735 -0
- package/dist/core/global-setup.d.ts +6 -0
- package/dist/core/global-setup.js +113 -0
- package/dist/core/keve-aspect.d.ts +54 -0
- package/dist/core/keve-aspect.js +52 -0
- package/dist/core/keve-decorators.d.ts +51 -0
- package/dist/core/keve-decorators.js +95 -0
- package/dist/core/keve-registry.d.ts +38 -0
- package/dist/core/keve-registry.js +25 -0
- package/dist/core/keve-report.d.ts +27 -0
- package/dist/core/keve-report.js +357 -0
- package/dist/core/keve-test.d.ts +36 -0
- package/dist/core/keve-test.js +364 -0
- package/dist/core/learned-actions.d.ts +24 -0
- package/dist/core/learned-actions.js +65 -0
- package/dist/core/pw-config.d.ts +19 -0
- package/dist/core/pw-config.js +180 -0
- package/dist/index.d.ts +8 -23
- package/dist/index.js +36 -43
- package/dist/page-agent/agent.d.ts +129 -0
- package/dist/page-agent/agent.js +585 -0
- package/dist/page-agent/hooks.d.ts +13 -0
- package/dist/page-agent/hooks.js +218 -0
- package/dist/page-agent/tools.d.ts +29 -0
- package/dist/page-agent/tools.js +297 -0
- package/doc/README.md +124 -0
- package/doc/agents/keve-dev-test-agent.md +140 -0
- package/doc/agents/keve-test-agent.md +178 -0
- package/doc/asserts//346/265/213/350/257/225/345/220/204/351/230/266/346/256/265/350/257/264/346/230/216.png +0 -0
- package/doc/keve-agent-demo.html +370 -0
- package/doc/skills/keve-bddcase-gen/SKILL.md +332 -0
- package/doc/skills/keve-bddcase-gen/references/test-case-rules.md +91 -0
- package/doc/skills/keve-bddcase-gen/references/test-cases-example.md +103 -0
- package/doc/skills/keve-component-gen/SKILL.md +255 -0
- package/doc/skills/keve-component-gen/references/component-mock-strategy.md +74 -0
- package/doc/skills/keve-component-gen/references/component-model-template.md +98 -0
- package/doc/skills/keve-component-gen/references/component-test-template.md +100 -0
- package/doc/skills/keve-component-gen/references/vue-test-utils-guide.md +91 -0
- package/doc/skills/keve-e2e-gen/SKILL.md +179 -0
- package/doc/skills/keve-e2e-gen/references/code-analysis-guide.md +208 -0
- package/doc/skills/keve-e2e-gen/references/decisions.md +36 -0
- package/doc/skills/keve-e2e-gen/references/pom-generation-guide.md +140 -0
- package/doc/skills/keve-e2e-gen/references/script-generation-guide.md +236 -0
- package/doc/skills/keve-e2e-gen/references/test-auditor-guide.md +189 -0
- package/doc/skills/keve-e2e-gen/scripts/extract-components.sh +85 -0
- package/doc/skills/keve-e2e-gen/scripts/inject-testids.sh +95 -0
- package/doc/skills/keve-static-analysis/SKILL.md +362 -0
- package/doc/skills/keve-static-analysis/references/codegraph-analysis-guide.md +108 -0
- package/doc/skills/keve-static-analysis/references/git-impact-analysis.md +81 -0
- package/doc/skills/keve-static-analysis/references/lint-fix-guide.md +69 -0
- package/doc/skills/keve-static-analysis/references/runtime-risk-patterns.md +92 -0
- package/doc/skills/keve-static-analysis/references/security-scan-guide.md +32 -0
- package/doc/skills/keve-static-analysis/references/ts-error-patterns.md +98 -0
- package/doc/skills/keve-static-analysis/references/vue-security-guide.md +72 -0
- package/doc/skills/keve-test-eval/SKILL.md +78 -0
- package/doc/skills/keve-test-eval/references/common-fix-guide.md +166 -0
- package/doc/skills/keve-test-eval/references/confidence-data-guide.md +78 -0
- package/doc/skills/keve-test-eval/references/fix-loop-guide.md +67 -0
- package/doc/skills/keve-unit-gen/SKILL.md +203 -0
- package/doc/skills/keve-unit-gen/references/boundary-conditions.md +78 -0
- package/doc/skills/keve-unit-gen/references/unit-mock-strategy.md +71 -0
- package/doc/skills/keve-unit-gen/references/unit-test-template.md +105 -0
- package/package.json +47 -25
- package/dist/agent/factory.d.ts +0 -25
- package/dist/agent/factory.js +0 -99
- package/dist/agent/index.d.ts +0 -3
- package/dist/agent/index.js +0 -7
- package/dist/agent/loader.d.ts +0 -22
- package/dist/agent/loader.js +0 -117
- package/dist/agent/types.d.ts +0 -9
- package/dist/agent/types.js +0 -2
- package/dist/agent-loop/complete-task.d.ts +0 -28
- package/dist/agent-loop/complete-task.js +0 -70
- package/dist/agent-loop/index.d.ts +0 -16
- package/dist/agent-loop/index.js +0 -26
- package/dist/agent-loop/loop.d.ts +0 -42
- package/dist/agent-loop/loop.js +0 -282
- package/dist/agent-loop/runtime.d.ts +0 -40
- package/dist/agent-loop/runtime.js +0 -163
- package/dist/agent-loop/types.d.ts +0 -161
- package/dist/agent-loop/types.js +0 -105
- package/dist/context/index.d.ts +0 -8
- package/dist/context/index.js +0 -29
- package/dist/context/tokenCalculation.d.ts +0 -55
- package/dist/context/tokenCalculation.js +0 -95
- package/dist/context/truncation.d.ts +0 -67
- package/dist/context/truncation.js +0 -142
- package/dist/context/types.d.ts +0 -7
- package/dist/context/types.js +0 -27
- package/dist/llm/OpenAIClient.d.ts +0 -31
- package/dist/llm/OpenAIClient.js +0 -455
- package/dist/llm/constants.d.ts +0 -2
- package/dist/llm/constants.js +0 -6
- package/dist/llm/errors.d.ts +0 -25
- package/dist/llm/errors.js +0 -51
- package/dist/llm/index.d.ts +0 -49
- package/dist/llm/index.js +0 -158
- package/dist/llm/types.d.ts +0 -124
- package/dist/llm/types.js +0 -2
- package/dist/llm/utils.d.ts +0 -29
- package/dist/llm/utils.js +0 -284
- package/dist/skill/frontmatter-parser.d.ts +0 -50
- package/dist/skill/frontmatter-parser.js +0 -152
- package/dist/skill/index.d.ts +0 -12
- package/dist/skill/index.js +0 -17
- package/dist/skill/loader.d.ts +0 -96
- package/dist/skill/loader.js +0 -256
- package/dist/tools/ask-user.d.ts +0 -8
- package/dist/tools/ask-user.js +0 -47
- package/dist/tools/bash.d.ts +0 -47
- package/dist/tools/bash.js +0 -213
- package/dist/tools/edit-file.d.ts +0 -9
- package/dist/tools/edit-file.js +0 -76
- package/dist/tools/glob.d.ts +0 -8
- package/dist/tools/glob.js +0 -101
- package/dist/tools/grep.d.ts +0 -10
- package/dist/tools/grep.js +0 -76
- package/dist/tools/index.d.ts +0 -24
- package/dist/tools/index.js +0 -47
- package/dist/tools/read-files.d.ts +0 -8
- package/dist/tools/read-files.js +0 -81
- package/dist/tools/write-file.d.ts +0 -8
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractDiagnosticHints = extractDiagnosticHints;
|
|
4
|
+
/* scriptRefine \u5df2\u6ce8\u91ca \u2014 \u4ea7\u51fa\u65e0\u4e0b\u6e38\u6d88\u8d39\u4e14\u4e0d\u53d7 keveGoal \u8d85\u65f6\u63a7\u5236\uff0c\u8be6\u89c1 WW-02 \u6839\u56e0\u5206\u6790
|
|
5
|
+
const REFINE_OUTPUT_SCHEMA = z.object({
|
|
6
|
+
analysis: z.string().describe('Brief analysis of what was missing in the original fn'),
|
|
7
|
+
patch: z.string().describe('Unified diff patch to fix the fn. Use standard git diff format.'),
|
|
8
|
+
confidence: z.number().min(0).max(100).describe('Confidence 0-100'),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const REFINE_SYSTEM_PROMPT = `You are a test script refinement assistant. You analyze test execution logs and generate git diff patches to improve test scripts.
|
|
12
|
+
|
|
13
|
+
Given:
|
|
14
|
+
1. The original test script file content
|
|
15
|
+
2. The fn source code that was executed
|
|
16
|
+
3. The gap description (what was missing)
|
|
17
|
+
4. The Re-Act exploration actions (what AI did to fill the gap)
|
|
18
|
+
|
|
19
|
+
Generate a unified diff patch that modifies the .keve.ts file to include the missing steps.
|
|
20
|
+
Rules:
|
|
21
|
+
- Only modify the fn body (the async () => { ... } callback inside keveGoal)
|
|
22
|
+
- Add the operations that Re-Act discovered (scroll, click, waitFor, etc.)
|
|
23
|
+
- Preserve the existing fn operations
|
|
24
|
+
- Use Playwright native API (page.getByRole, page.locator, etc.)
|
|
25
|
+
- Output standard unified diff format`;
|
|
26
|
+
|
|
27
|
+
export interface RefineInput {
|
|
28
|
+
specFilePath: string;
|
|
29
|
+
step: string;
|
|
30
|
+
expected: string;
|
|
31
|
+
testTitle: string;
|
|
32
|
+
order: number;
|
|
33
|
+
fnSource: string;
|
|
34
|
+
gap: string;
|
|
35
|
+
reactActions: Array<{ tool: string; role?: string; name?: string; url?: string; text?: string; key?: string; evaluation?: string }>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function scriptRefine(llm: any, input: RefineInput): Promise<string | null> {
|
|
39
|
+
if (!input.specFilePath || !fs.existsSync(input.specFilePath)) return null;
|
|
40
|
+
if (!input.reactActions?.length) return null;
|
|
41
|
+
|
|
42
|
+
const specContent = fs.readFileSync(input.specFilePath, 'utf-8');
|
|
43
|
+
|
|
44
|
+
const userMessage = `## Test Script File: ${input.specFilePath}
|
|
45
|
+
|
|
46
|
+
\`\`\`typescript
|
|
47
|
+
${specContent}
|
|
48
|
+
\`\`\`
|
|
49
|
+
|
|
50
|
+
## Executed fn:
|
|
51
|
+
\`\`\`typescript
|
|
52
|
+
${input.fnSource}
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
## Gap (what was missing):
|
|
56
|
+
${input.gap}
|
|
57
|
+
|
|
58
|
+
## Re-Act Actions (what AI did to achieve the expected state):
|
|
59
|
+
${JSON.stringify(input.reactActions, null, 2)}
|
|
60
|
+
|
|
61
|
+
Generate a unified diff patch to fix the fn. Add the missing operations that Re-Act discovered.`;
|
|
62
|
+
|
|
63
|
+
const refineTool = {
|
|
64
|
+
RefineOutput: {
|
|
65
|
+
description: 'Output your script refinement as a unified diff patch.',
|
|
66
|
+
inputSchema: REFINE_OUTPUT_SCHEMA,
|
|
67
|
+
execute: async (args: any) => args,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const llmResult = await llm.invoke(
|
|
73
|
+
[{ role: 'system', content: REFINE_SYSTEM_PROMPT }, { role: 'user', content: userMessage }],
|
|
74
|
+
refineTool,
|
|
75
|
+
new AbortController().signal,
|
|
76
|
+
{ toolChoiceName: 'RefineOutput' },
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const parsed = REFINE_OUTPUT_SCHEMA.parse(llmResult.toolCall.args);
|
|
80
|
+
|
|
81
|
+
// Save patch file
|
|
82
|
+
const taskDir = process.env.KEVE_TASK_DIR || '.keve';
|
|
83
|
+
const round = process.env.KEVE_ROUND || 'latest';
|
|
84
|
+
const refineDir = path.join(taskDir, 'test-artifacts', `round-${round}`, 'refine');
|
|
85
|
+
fs.mkdirSync(refineDir, { recursive: true });
|
|
86
|
+
const safeName = input.testTitle.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '_').slice(0, 40);
|
|
87
|
+
const patchFile = path.join(refineDir, `refine-${safeName}-step${input.order}.patch`);
|
|
88
|
+
fs.writeFileSync(patchFile, [
|
|
89
|
+
`# script-refine for: ${input.testTitle} / ${input.step}`,
|
|
90
|
+
`# Gap: ${input.gap.slice(0, 200)}`,
|
|
91
|
+
`# Analysis: ${parsed.analysis}`,
|
|
92
|
+
`# Confidence: ${parsed.confidence}%`,
|
|
93
|
+
``,
|
|
94
|
+
parsed.patch,
|
|
95
|
+
].join('\n'), 'utf-8');
|
|
96
|
+
|
|
97
|
+
console.log(`[script-refine] Generated patch: ${path.relative(taskDir, patchFile)}`);
|
|
98
|
+
console.log(`[script-refine] Analysis: ${parsed.analysis}`);
|
|
99
|
+
console.log(`[script-refine] Confidence: ${parsed.confidence}%`);
|
|
100
|
+
|
|
101
|
+
return patchFile;
|
|
102
|
+
} catch (err: any) {
|
|
103
|
+
console.warn(`[script-refine] Failed to generate patch: ${err.message}`);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// \u2500\u2500 scriptRefine \u6ce8\u91ca\u7ed3\u675f \u2500\u2500 */
|
|
108
|
+
/**
|
|
109
|
+
* diagnostic.ts — Extract diagnostic hints from agent result + page state
|
|
110
|
+
*
|
|
111
|
+
* Called in onAfterTask hook to generate human-readable failure reasons.
|
|
112
|
+
* Inspects the agent's action history, final snapshot, and live page DOM
|
|
113
|
+
* to produce actionable diagnostic hints.
|
|
114
|
+
*/
|
|
115
|
+
/**
|
|
116
|
+
* Extract diagnostic hints from agent result and current page state.
|
|
117
|
+
* Only meaningful when result.success === false.
|
|
118
|
+
*/
|
|
119
|
+
async function extractDiagnosticHints(page, result) {
|
|
120
|
+
const hints = [];
|
|
121
|
+
// 1. From done action's text — AI's own failure reason
|
|
122
|
+
const doneEvent = result.events
|
|
123
|
+
.filter((e) => e.type === 'step')
|
|
124
|
+
.find(e => e.toolName === 'done');
|
|
125
|
+
if (doneEvent?.toolOutput) {
|
|
126
|
+
hints.push(doneEvent.toolOutput);
|
|
127
|
+
}
|
|
128
|
+
// 2. From finalSnapshot — key page state signals
|
|
129
|
+
if (result.finalSnapshot) {
|
|
130
|
+
const snapshot = String(result.finalSnapshot);
|
|
131
|
+
const disabledButtons = snapshot.match(/button\s+"[^"]+"\s+\[disabled\]/g);
|
|
132
|
+
if (disabledButtons) {
|
|
133
|
+
for (const db of disabledButtons) {
|
|
134
|
+
const label = db.match(/button\s+"([^"]+)"/)?.[1] || '';
|
|
135
|
+
hints.push(`按钮「${label}」为禁用(disabled)状态,当前用户可能无操作权限`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (/login|auth|signin|sso|cas/i.test(snapshot)) {
|
|
139
|
+
hints.push('页面被重定向到登录页,当前浏览器未登录或登录已过期');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// 3. Inspect live page DOM
|
|
143
|
+
try {
|
|
144
|
+
const currentUrl = page.url();
|
|
145
|
+
console.log(`[diagnostic] page.url() = ${currentUrl}`);
|
|
146
|
+
if (currentUrl === 'about:blank' || currentUrl === '') {
|
|
147
|
+
hints.push('Playwright 页面对象停留在 about:blank,AI 探索未导航到目标页面(LLM 不可用或未配置)');
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// 3a. Check disabled buttons
|
|
151
|
+
const disabledButtons = await page.locator('button[disabled], button.ant-btn-disabled, button[class*="disabled"]').allInnerTexts();
|
|
152
|
+
console.log(`[diagnostic] disabledButtons=${JSON.stringify(disabledButtons)}`);
|
|
153
|
+
if (disabledButtons.length) {
|
|
154
|
+
for (const text of disabledButtons) {
|
|
155
|
+
const label = text.trim();
|
|
156
|
+
if (label && !hints.some(h => h.includes(label))) {
|
|
157
|
+
hints.push(`按钮「${label}」为禁用(disabled)状态,当前用户可能无操作权限`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// 3b. Check login redirect
|
|
162
|
+
if ((currentUrl.includes('sso') || currentUrl.includes('cas/login') || /login|auth|signin/i.test(currentUrl))
|
|
163
|
+
&& !hints.some(h => h.includes('登录页'))) {
|
|
164
|
+
hints.push('页面被重定向到登录页,当前浏览器未登录或登录已过期');
|
|
165
|
+
}
|
|
166
|
+
// 3c. Overlay/modal/alert text
|
|
167
|
+
const overlaySelectors = [
|
|
168
|
+
'.ant-modal-body', '.ant-modal-confirm-content', '.ant-alert-message', '.ant-alert-description',
|
|
169
|
+
'.ant-notification-notice-message', '.ant-notification-notice-description',
|
|
170
|
+
'.ant-result-title', '.ant-result-subtitle', '.ant-empty-description',
|
|
171
|
+
'[class*="overlay"]', '[class*="mask"]', '[class*="forbidden"]', '[class*="ban-edit"]',
|
|
172
|
+
'[class*="disabled-mask"]', '[role="dialog"]', '[role="alert"]', '[role="alertdialog"]',
|
|
173
|
+
'.ant-modal-mask + .ant-modal-wrap',
|
|
174
|
+
].join(', ');
|
|
175
|
+
const overlayTexts = await page.locator(overlaySelectors).allInnerTexts();
|
|
176
|
+
if (overlayTexts.length) {
|
|
177
|
+
for (const text of overlayTexts) {
|
|
178
|
+
const t = text.trim();
|
|
179
|
+
if (t && !hints.some(h => h.includes(t))) {
|
|
180
|
+
hints.push(`页面显示提示: 「${t}」`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// 3d. Fallback: key lines from body
|
|
185
|
+
if (hints.length === 0 || !hints.some(h => h.includes('页面显示提示'))) {
|
|
186
|
+
try {
|
|
187
|
+
const bodyText = await page.locator('body').innerText({ timeout: 3000 });
|
|
188
|
+
const keyLines = bodyText.split('\n')
|
|
189
|
+
.map(l => l.trim())
|
|
190
|
+
.filter(l => l.length > 4 && l.length < 200 &&
|
|
191
|
+
(l.includes('禁止') || l.includes('下线') || l.includes('无权') || l.includes('无法') ||
|
|
192
|
+
l.includes('不存在') || l.includes('已删除') || l.includes('过期') || l.includes('失败') ||
|
|
193
|
+
l.includes('错误') || l.includes('异常') || l.includes('提醒') || l.includes('通知')));
|
|
194
|
+
for (const line of keyLines.slice(0, 3)) {
|
|
195
|
+
if (!hints.some(h => h.includes(line))) {
|
|
196
|
+
hints.push(`页面实际显示: 「${line}」`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch { /* ignore */ }
|
|
201
|
+
}
|
|
202
|
+
// 3e. Last resort: report page title
|
|
203
|
+
if (hints.length === 0) {
|
|
204
|
+
try {
|
|
205
|
+
const pageTitle = await page.title();
|
|
206
|
+
if (pageTitle) {
|
|
207
|
+
hints.push(`当前页面标题: 「${pageTitle}」(URL: ${currentUrl})`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch { /* ignore */ }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
console.log(`[diagnostic] page inspection failed: ${e.message}`);
|
|
216
|
+
}
|
|
217
|
+
return hints;
|
|
218
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* page-agent tools — Playwright-native action tools
|
|
3
|
+
*
|
|
4
|
+
* Design: Each tool has independent inputSchema + execute.
|
|
5
|
+
* Tools are registered in a Map, then packed into a MacroTool for LLM invocation.
|
|
6
|
+
*
|
|
7
|
+
* Element targeting: aria-ref via page.locator('aria-ref=xxx')
|
|
8
|
+
* - mode:'ai' snapshot provides [ref=f5e13] identifiers
|
|
9
|
+
* - aria-ref selector automatically penetrates iframe boundaries
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import type { KevePageAgent } from './agent';
|
|
13
|
+
export interface ToolContext {
|
|
14
|
+
signal: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
export interface PageAgentTool<TParams = any> {
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: z.ZodType<TParams>;
|
|
19
|
+
execute: (this: KevePageAgent, args: TParams, ctx: ToolContext) => Promise<string>;
|
|
20
|
+
}
|
|
21
|
+
export declare function tool<TParams>(options: PageAgentTool<TParams>): PageAgentTool<TParams>;
|
|
22
|
+
export declare const tools: Map<string, PageAgentTool<any>>;
|
|
23
|
+
export declare function getZodShape(schema: any): Record<string, any>;
|
|
24
|
+
export declare function packMacroToolSchema(): z.ZodObject<{
|
|
25
|
+
evaluation_previous_goal: z.ZodOptional<z.ZodString>;
|
|
26
|
+
memory: z.ZodOptional<z.ZodString>;
|
|
27
|
+
next_goal: z.ZodOptional<z.ZodString>;
|
|
28
|
+
}, z.core.$loose>;
|
|
29
|
+
export type MacroToolInput = z.infer<ReturnType<typeof packMacroToolSchema>>;
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* page-agent tools — Playwright-native action tools
|
|
4
|
+
*
|
|
5
|
+
* Design: Each tool has independent inputSchema + execute.
|
|
6
|
+
* Tools are registered in a Map, then packed into a MacroTool for LLM invocation.
|
|
7
|
+
*
|
|
8
|
+
* Element targeting: aria-ref via page.locator('aria-ref=xxx')
|
|
9
|
+
* - mode:'ai' snapshot provides [ref=f5e13] identifiers
|
|
10
|
+
* - aria-ref selector automatically penetrates iframe boundaries
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.tools = void 0;
|
|
14
|
+
exports.tool = tool;
|
|
15
|
+
exports.getZodShape = getZodShape;
|
|
16
|
+
exports.packMacroToolSchema = packMacroToolSchema;
|
|
17
|
+
const zod_1 = require("zod");
|
|
18
|
+
function tool(options) {
|
|
19
|
+
return options;
|
|
20
|
+
}
|
|
21
|
+
// ─── Tool Registry ────────────────────────────────────────────────────
|
|
22
|
+
exports.tools = new Map();
|
|
23
|
+
// --- done ---
|
|
24
|
+
exports.tools.set('done', tool({
|
|
25
|
+
description: 'Complete the task with a test conclusion. Use this when the expected state is achieved, not achieved, or you are blocked.',
|
|
26
|
+
inputSchema: zod_1.z.object({
|
|
27
|
+
verdict: zod_1.z.enum(['pass', 'fail', 'blocked']).describe('Test verdict: "pass"=expected state achieved; "fail"=page accessible but expected not met; "blocked"=cannot continue (SSO, connection refused, etc.)'),
|
|
28
|
+
text: zod_1.z.string().describe('Summary of what was achieved, why it failed, or why blocked'),
|
|
29
|
+
}),
|
|
30
|
+
execute: async function (input) {
|
|
31
|
+
const icons = { pass: '✅', fail: '❌', blocked: '🚫' };
|
|
32
|
+
let verdict = input.verdict;
|
|
33
|
+
// 强制校验:verdict 缺失或非法时从 text 推断(MacroTool 扁平化导致 verdict 变 optional)
|
|
34
|
+
if (!icons[verdict]) {
|
|
35
|
+
console.warn(`[done] ⚠️ verdict missing or invalid ("${input.verdict}"), inferring from text`);
|
|
36
|
+
const t = String(input.text || '').toLowerCase();
|
|
37
|
+
if (t.includes('fail') || t.includes('failure') || t.includes('失败') || t.includes('不通过') || t.includes('未通过')) {
|
|
38
|
+
verdict = 'fail';
|
|
39
|
+
}
|
|
40
|
+
else if (t.includes('blocked') || t.includes('阻塞') || t.includes('阻止')) {
|
|
41
|
+
verdict = 'blocked';
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
verdict = 'pass'; // 有 text 且无否定词 → 默认 pass
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return `${icons[verdict]} Task ${verdict}: ${input.text}`;
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
// --- click ---
|
|
51
|
+
exports.tools.set('click', tool({
|
|
52
|
+
description: 'Click an element by its ref identifier from the accessibility tree. The aria-ref selector automatically penetrates iframe boundaries.',
|
|
53
|
+
inputSchema: zod_1.z.object({
|
|
54
|
+
ref: zod_1.z.string().describe('Element ref from the accessibility tree, e.g. "f5e13"'),
|
|
55
|
+
}),
|
|
56
|
+
execute: async function (input) {
|
|
57
|
+
const locator = this.page.locator(`aria-ref=${input.ref}`);
|
|
58
|
+
await locator.click({ timeout: 5000, force: true });
|
|
59
|
+
return `✅ Clicked element [ref=${input.ref}]`;
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
// --- type ---
|
|
63
|
+
exports.tools.set('type', tool({
|
|
64
|
+
description: 'Type text into an input element by its ref identifier. Clears existing content first. The aria-ref selector automatically penetrates iframe boundaries.',
|
|
65
|
+
inputSchema: zod_1.z.object({
|
|
66
|
+
ref: zod_1.z.string().describe('Element ref from the accessibility tree, e.g. "f5e91"'),
|
|
67
|
+
text: zod_1.z.string().describe('Text to type into the element'),
|
|
68
|
+
submit: zod_1.z.boolean().optional().describe('Whether to press Enter after typing'),
|
|
69
|
+
}),
|
|
70
|
+
execute: async function (input) {
|
|
71
|
+
const locator = this.page.locator(`aria-ref=${input.ref}`);
|
|
72
|
+
await locator.fill(input.text, { timeout: 5000, force: true });
|
|
73
|
+
if (input.submit)
|
|
74
|
+
await this.page.keyboard.press('Enter');
|
|
75
|
+
return `✅ Typed "${input.text.slice(0, 50)}" into element [ref=${input.ref}]${input.submit ? ' + Enter' : ''}`;
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
// --- hover ---
|
|
79
|
+
exports.tools.set('hover', tool({
|
|
80
|
+
description: 'Hover over an element by its ref identifier. The aria-ref selector automatically penetrates iframe boundaries.',
|
|
81
|
+
inputSchema: zod_1.z.object({
|
|
82
|
+
ref: zod_1.z.string().describe('Element ref from the accessibility tree, e.g. "f5e13"'),
|
|
83
|
+
}),
|
|
84
|
+
execute: async function (input) {
|
|
85
|
+
const locator = this.page.locator(`aria-ref=${input.ref}`);
|
|
86
|
+
await locator.hover({ timeout: 5000, force: true });
|
|
87
|
+
return `✅ Hovered over element [ref=${input.ref}]`;
|
|
88
|
+
},
|
|
89
|
+
}));
|
|
90
|
+
// --- pressKey ---
|
|
91
|
+
exports.tools.set('pressKey', tool({
|
|
92
|
+
description: 'Press a keyboard key (Enter, Escape, Tab, ArrowDown, etc.)',
|
|
93
|
+
inputSchema: zod_1.z.object({
|
|
94
|
+
key: zod_1.z.string().describe('Key to press: Enter, Escape, Tab, ArrowDown, etc.'),
|
|
95
|
+
}),
|
|
96
|
+
execute: async function (input) {
|
|
97
|
+
await this.page.keyboard.press(input.key);
|
|
98
|
+
return `✅ Pressed key "${input.key}"`;
|
|
99
|
+
},
|
|
100
|
+
}));
|
|
101
|
+
// --- scroll ---
|
|
102
|
+
exports.tools.set('scroll', tool({
|
|
103
|
+
description: 'Scroll the page vertically. Use when target content is off-screen.',
|
|
104
|
+
inputSchema: zod_1.z.object({
|
|
105
|
+
direction: zod_1.z.enum(['down', 'up']).default('down').describe('Scroll direction'),
|
|
106
|
+
amount: zod_1.z.number().optional().describe('Scroll amount in pixels (default 500)'),
|
|
107
|
+
}),
|
|
108
|
+
execute: async function (input) {
|
|
109
|
+
const pixels = input.amount ?? 500;
|
|
110
|
+
const dir = input.direction === 'down' ? 1 : -1;
|
|
111
|
+
await this.page.mouse.wheel(0, pixels * dir);
|
|
112
|
+
return `✅ Scrolled ${input.direction} ${pixels}px`;
|
|
113
|
+
},
|
|
114
|
+
}));
|
|
115
|
+
// --- wait ---
|
|
116
|
+
exports.tools.set('wait', tool({
|
|
117
|
+
description: 'Wait for a specified duration (in milliseconds). Use when page is loading.',
|
|
118
|
+
inputSchema: zod_1.z.object({
|
|
119
|
+
time: zod_1.z.number().min(100).max(10000).describe('Wait time in milliseconds'),
|
|
120
|
+
}),
|
|
121
|
+
execute: async function (input) {
|
|
122
|
+
await this.page.waitForTimeout(input.time);
|
|
123
|
+
return `✅ Waited ${input.time}ms`;
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
// --- navigate ---
|
|
127
|
+
exports.tools.set('navigate', tool({
|
|
128
|
+
description: 'Navigate to a URL. Use KEVE_TARGET_URL (from test-cases.yaml principle) for the main application page. For other pages, use URLs from envVars (e.g., process.env.PAGE_KNOWLEDGE). Do NOT construct or guess URLs yourself.',
|
|
129
|
+
inputSchema: zod_1.z.object({
|
|
130
|
+
url: zod_1.z.string().describe('URL to navigate to. Use process.env.KEVE_TARGET_URL for the main page, or envVars for other declared pages. Supports absolute URLs or relative paths starting with /'),
|
|
131
|
+
}),
|
|
132
|
+
execute: async function (input) {
|
|
133
|
+
let url = input.url;
|
|
134
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
135
|
+
const base = process.env.KEVE_TARGET_URL || process.env.BASE_URL || '';
|
|
136
|
+
url = new URL(url, base).href;
|
|
137
|
+
}
|
|
138
|
+
await this.page.goto(url, { waitUntil: 'networkidle', timeout: 15000 });
|
|
139
|
+
return `✅ Navigated to ${url}`;
|
|
140
|
+
},
|
|
141
|
+
}));
|
|
142
|
+
// --- execute_javascript ---
|
|
143
|
+
exports.tools.set('execute_javascript', tool({
|
|
144
|
+
description: 'READ-ONLY query tool: execute JavaScript in the BROWSER DOM context to retrieve precise values that screenshots and accessibility trees CANNOT provide — such as computed CSS styles (getComputedStyle), DOM measurements (offsetWidth, getBoundingClientRect), or element properties (checked, disabled, value).\n\n⚠️ EXECUTION CONTEXT: This runs inside the browser page via page.evaluate — it is a plain browser JavaScript sandbox. There is NO `page` object, NO Playwright API, NO `locator`, NO top-level `await`. Using `page.locator(...)`, `page.evaluate(...)`, `await`, or `async () => {}` wrappers will cause SyntaxError or return undefined.\n\nDo NOT use for: clicking, typing, navigating, form filling, or any page interaction — use click/type/navigate tools instead. Do NOT use for: assertions or logical checks — just return the raw value, you judge the result yourself. Do NOT use for: modifying page state (setting styles, cookies, localStorage, DOM mutations).',
|
|
145
|
+
inputSchema: zod_1.z.object({
|
|
146
|
+
script: zod_1.z.string().describe("JavaScript code to execute inside the browser DOM. MUST start with `return` to capture the result. Use `var` for variable declarations (NOT `const`/`let` — they cause SyntaxError in some evaluate contexts). Only browser DOM APIs: document.querySelector, window.getComputedStyle, etc.\n\n✅ Correct: 'return document.querySelector(\".x\").innerText'\n✅ Correct: 'var el = document.querySelector(\".x\"); return window.getComputedStyle(el).backgroundColor'\n❌ Wrong: 'page.locator(\".x\")' — no Playwright `page` object in browser context\n❌ Wrong: 'await page.locator(...)' — no `await` or `async`\n❌ Wrong: 'const el = ...' — use `var` instead of `const`/`let`\n❌ Wrong: 'async () => { ... }' — do NOT wrap in async function, returns undefined\n\nRead-only queries only."),
|
|
147
|
+
}),
|
|
148
|
+
execute: async function (input) {
|
|
149
|
+
try {
|
|
150
|
+
let script = input.script.trim();
|
|
151
|
+
// Auto-unwrap arrow function wrappers that Agent frequently generates:
|
|
152
|
+
// async () => { ... return ... } → ... return ...
|
|
153
|
+
// () => { ... return ... } → ... return ...
|
|
154
|
+
// (async () => { ... })() → ... return ...
|
|
155
|
+
// These wrappers cause page.evaluate to return undefined because the
|
|
156
|
+
// arrow function is created but never invoked inside the IIFE.
|
|
157
|
+
const arrowMatch = script.match(/^(?:async\s+)?(?:\(\s*\)\s*|)\s*=>\s*\{([\s\S]*)\}\s*;?\s*$/);
|
|
158
|
+
if (arrowMatch) {
|
|
159
|
+
script = arrowMatch[1].trim();
|
|
160
|
+
}
|
|
161
|
+
// Also strip IIFE self-call: (async () => { ... })()
|
|
162
|
+
const iifeMatch = script.match(/^\(\s*(?:async\s+)?\(\s*\)\s*=>\s*\{([\s\S]*)\}\s*\)\s*\(\s*\)\s*;?\s*$/);
|
|
163
|
+
if (iifeMatch) {
|
|
164
|
+
script = iifeMatch[1].trim();
|
|
165
|
+
}
|
|
166
|
+
// Read-only guard: block DOM mutation APIs to prevent Agent from altering page state.
|
|
167
|
+
const writePatterns = [
|
|
168
|
+
/\b(?:document|element|el|node)\.\s*(?:remove|appendChild|insertBefore|replaceChild|removeChild|appendChild)\s*\(/,
|
|
169
|
+
/\b(?:document|element|el|node)\.\s*(?:innerHTML|outerHTML|textContent|innerText)\s*=/,
|
|
170
|
+
/\b(?:document|element|el|node)\.\s*(?:setAttribute|removeAttribute|classList)\s*\./,
|
|
171
|
+
/\b\.style\s*\.\s*\w+\s*=/,
|
|
172
|
+
/\blocation\s*(?:\.href\s*=|\.assign\s*\(|\.replace\s*\()/,
|
|
173
|
+
/\bwindow\s*\.\s*(?:close|stop|open)\s*\(/,
|
|
174
|
+
/\bdocument\.\s*(?:write|writeln|execCommand)\s*\(/,
|
|
175
|
+
/\blocalStorage\s*\.\s*set\s*\(/,
|
|
176
|
+
/\bsessionStorage\s*\.\s*set\s*\(/,
|
|
177
|
+
/\bdocument\.cookie\s*=/,
|
|
178
|
+
/\beval\s*\(/,
|
|
179
|
+
];
|
|
180
|
+
for (const pattern of writePatterns) {
|
|
181
|
+
if (pattern.test(script)) {
|
|
182
|
+
return `❌ JS execution blocked: write/mutation API detected. execute_javascript is read-only. Use click/type/navigate tools for interactions.`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Require explicit `return` — no auto-wrapping.
|
|
186
|
+
// Wrap in IIFE so `return` is valid (page.evaluate treats bare strings as expressions).
|
|
187
|
+
if (!/\breturn\b/.test(script)) {
|
|
188
|
+
return `❌ Script must start with 'return' to capture the result. Example: "return window.getComputedStyle(document.body).backgroundColor"`;
|
|
189
|
+
}
|
|
190
|
+
const wrapped = `(function() { ${script} })()`;
|
|
191
|
+
const result = await this.page.evaluate(wrapped);
|
|
192
|
+
const output = typeof result === 'object' && result !== null
|
|
193
|
+
? JSON.stringify(result, null, 2)
|
|
194
|
+
: String(result ?? 'undefined');
|
|
195
|
+
return `✅ JS executed. Result: ${output}`;
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
return `❌ JS execution error: ${err.message || String(err)}`;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
}));
|
|
202
|
+
// --- visual_locate ---
|
|
203
|
+
exports.tools.set('visual_locate', tool({
|
|
204
|
+
description: 'Locate and click an element by visual analysis of a screenshot. FALLBACK: only use after `click` fails 2+ times on the same element (non-standard ARIA role, invisible in accessibility tree, or element visible in screenshot but missing from accessibility tree). Provide a concise visual description of the element. It will screenshot the page, send it to a multimodal LLM to locate the element, then click at the predicted coordinates. After clicking, verify the effect on the next screenshot.',
|
|
205
|
+
inputSchema: zod_1.z.object({
|
|
206
|
+
description: zod_1.z.string().describe('Visual description of the element to find, e.g. "the dropdown button showing 当前Agent▼"'),
|
|
207
|
+
}),
|
|
208
|
+
execute: async function (input) {
|
|
209
|
+
// 1. Capture screenshot
|
|
210
|
+
let screenshotBase64;
|
|
211
|
+
try {
|
|
212
|
+
const buf = await this.page.screenshot({ type: 'png', timeout: 10000 });
|
|
213
|
+
screenshotBase64 = buf.toString('base64');
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
throw new Error(`visual_locate screenshot failed: ${e.message}`);
|
|
217
|
+
}
|
|
218
|
+
// 2. Call LLM for visual element location
|
|
219
|
+
const locateResult = await this.visualLocateElement(screenshotBase64, input.description);
|
|
220
|
+
if (!locateResult.found || !locateResult.bbox || locateResult.bbox.length !== 4) {
|
|
221
|
+
return `❌ Visual locate failed: element "${input.description}" not found. ${locateResult.analysis || ''}`;
|
|
222
|
+
}
|
|
223
|
+
// 3. Convert normalized bbox (0-1000) to viewport pixel coordinates
|
|
224
|
+
const viewport = this.page.viewportSize() || { width: 1280, height: 720 };
|
|
225
|
+
const [x1, y1, x2, y2] = locateResult.bbox;
|
|
226
|
+
const centerX = Math.round(((x1 + x2) / 2) * viewport.width / 1000);
|
|
227
|
+
const centerY = Math.round(((y1 + y2) / 2) * viewport.height / 1000);
|
|
228
|
+
// 4. Click at coordinates (bypasses iframe boundaries)
|
|
229
|
+
await this.page.mouse.click(centerX, centerY);
|
|
230
|
+
return `✅ Visual locate & click: "${input.description}" at (${centerX}, ${centerY})`;
|
|
231
|
+
},
|
|
232
|
+
}));
|
|
233
|
+
// ── verify_expected fully removed (Phase 6) ──
|
|
234
|
+
// LLM now sees screenshots via multimodal prompt and self-evaluates.
|
|
235
|
+
// No separate verification tool or function needed — each step's screenshot
|
|
236
|
+
// is injected into the LLM prompt directly, so LLM judges goal state itself.
|
|
237
|
+
// ─── Zod schema helper (used by packMacroToolSchema & MacroTool execute) ──
|
|
238
|
+
function getZodShape(schema) {
|
|
239
|
+
const def = schema?._def;
|
|
240
|
+
if (!def)
|
|
241
|
+
return schema?.shape || {};
|
|
242
|
+
const s = def.shape;
|
|
243
|
+
return typeof s === 'function' ? s() : (s || schema?.shape || {});
|
|
244
|
+
}
|
|
245
|
+
// ─── Pack tools into MacroTool schema for LLM ────────────────────────
|
|
246
|
+
// Auto-derive flat schema from tools Map (like page-agent's #packMacroTool).
|
|
247
|
+
// Key insight: z.union() is UNSTABLE with keve-core's safeParse.
|
|
248
|
+
// Solution: merge all tool inputSchema fields into one flat object with `tool` enum.
|
|
249
|
+
function packMacroToolSchema() {
|
|
250
|
+
// Collect tool names for enum
|
|
251
|
+
const toolNames = Array.from(exports.tools.keys());
|
|
252
|
+
// Collect all unique fields from all tool schemas
|
|
253
|
+
const fieldMap = new Map();
|
|
254
|
+
for (const [name, t] of exports.tools.entries()) {
|
|
255
|
+
// Unwrap ZodObject to get its shape
|
|
256
|
+
const shape = getZodShape(t.inputSchema);
|
|
257
|
+
for (const [key, val] of Object.entries(shape)) {
|
|
258
|
+
if (!fieldMap.has(key)) {
|
|
259
|
+
// First encounter — store the schema and add tool names to description
|
|
260
|
+
const zodField = val;
|
|
261
|
+
const desc = zodField._def?.description || '';
|
|
262
|
+
fieldMap.set(key, { schema: zodField.optional(), description: desc });
|
|
263
|
+
}
|
|
264
|
+
// If field already exists, just ensure description mentions both tools
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Build the flat schema
|
|
268
|
+
const reflectionFields = {
|
|
269
|
+
evaluation_previous_goal: zod_1.z.string().optional()
|
|
270
|
+
.describe('Concise one-sentence analysis of your last action. State success, failure, or uncertain.'),
|
|
271
|
+
memory: zod_1.z.string().optional()
|
|
272
|
+
.describe('1-3 concise sentences of key observations that will help in future steps.'),
|
|
273
|
+
next_goal: zod_1.z.string().optional()
|
|
274
|
+
.describe('State the next immediate goal and action to achieve it.'),
|
|
275
|
+
};
|
|
276
|
+
const actionFields = {
|
|
277
|
+
tool: zod_1.z.enum(toolNames).describe('The action tool to use'),
|
|
278
|
+
};
|
|
279
|
+
// Add all tool fields as optional
|
|
280
|
+
for (const [key, { schema, description }] of fieldMap.entries()) {
|
|
281
|
+
// Make all fields optional + add which tools use this field
|
|
282
|
+
const usedBy = Array.from(exports.tools.entries())
|
|
283
|
+
.filter(([_, t]) => {
|
|
284
|
+
const shape = getZodShape(t.inputSchema);
|
|
285
|
+
return key in shape;
|
|
286
|
+
})
|
|
287
|
+
.map(([name]) => name);
|
|
288
|
+
const enrichedDesc = usedBy.length > 1
|
|
289
|
+
? `${description} (for ${usedBy.join('/')})`
|
|
290
|
+
: description;
|
|
291
|
+
actionFields[key] = schema.describe(enrichedDesc);
|
|
292
|
+
}
|
|
293
|
+
return zod_1.z.object({
|
|
294
|
+
...reflectionFields,
|
|
295
|
+
...actionFields,
|
|
296
|
+
}).passthrough(); // Allow extra LLM fields for stability
|
|
297
|
+
}
|