@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.
Files changed (152) hide show
  1. package/README.md +53 -0
  2. package/dist/bin/keve.d.ts +13 -0
  3. package/dist/bin/keve.js +164 -0
  4. package/dist/commands/auth-update.d.ts +16 -0
  5. package/dist/commands/auth-update.js +88 -0
  6. package/dist/commands/clean.d.ts +10 -0
  7. package/dist/commands/clean.js +78 -0
  8. package/dist/commands/config.d.ts +17 -0
  9. package/dist/commands/config.js +99 -0
  10. package/dist/commands/init.d.ts +15 -0
  11. package/dist/commands/init.js +292 -0
  12. package/dist/commands/report.d.ts +12 -0
  13. package/dist/commands/report.js +159 -0
  14. package/dist/commands/run.d.ts +29 -0
  15. package/dist/commands/run.js +417 -0
  16. package/dist/commands/test.d.ts +12 -0
  17. package/dist/commands/test.js +102 -0
  18. package/dist/commands/vitest.d.ts +14 -0
  19. package/dist/commands/vitest.js +166 -0
  20. package/dist/config.d.ts +109 -0
  21. package/dist/config.js +291 -0
  22. package/dist/core/action-log-writer.d.ts +47 -0
  23. package/dist/{tools/write-file.js → core/action-log-writer.js} +42 -26
  24. package/dist/core/auth.d.ts +17 -0
  25. package/dist/core/auth.js +224 -0
  26. package/dist/core/cdp-resolver.d.ts +5 -0
  27. package/dist/core/cdp-resolver.js +93 -0
  28. package/dist/core/generateReport/report.d.ts +19 -0
  29. package/dist/core/generateReport/report.js +91 -0
  30. package/dist/core/generateReport/reportData.d.ts +215 -0
  31. package/dist/core/generateReport/reportData.js +735 -0
  32. package/dist/core/global-setup.d.ts +6 -0
  33. package/dist/core/global-setup.js +113 -0
  34. package/dist/core/keve-aspect.d.ts +54 -0
  35. package/dist/core/keve-aspect.js +52 -0
  36. package/dist/core/keve-decorators.d.ts +51 -0
  37. package/dist/core/keve-decorators.js +95 -0
  38. package/dist/core/keve-registry.d.ts +38 -0
  39. package/dist/core/keve-registry.js +25 -0
  40. package/dist/core/keve-report.d.ts +27 -0
  41. package/dist/core/keve-report.js +357 -0
  42. package/dist/core/keve-test.d.ts +36 -0
  43. package/dist/core/keve-test.js +364 -0
  44. package/dist/core/learned-actions.d.ts +24 -0
  45. package/dist/core/learned-actions.js +65 -0
  46. package/dist/core/pw-config.d.ts +19 -0
  47. package/dist/core/pw-config.js +180 -0
  48. package/dist/index.d.ts +8 -23
  49. package/dist/index.js +36 -43
  50. package/dist/page-agent/agent.d.ts +129 -0
  51. package/dist/page-agent/agent.js +585 -0
  52. package/dist/page-agent/hooks.d.ts +13 -0
  53. package/dist/page-agent/hooks.js +218 -0
  54. package/dist/page-agent/tools.d.ts +29 -0
  55. package/dist/page-agent/tools.js +297 -0
  56. package/doc/README.md +124 -0
  57. package/doc/agents/keve-dev-test-agent.md +140 -0
  58. package/doc/agents/keve-test-agent.md +178 -0
  59. 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
  60. package/doc/keve-agent-demo.html +370 -0
  61. package/doc/skills/keve-bddcase-gen/SKILL.md +332 -0
  62. package/doc/skills/keve-bddcase-gen/references/test-case-rules.md +91 -0
  63. package/doc/skills/keve-bddcase-gen/references/test-cases-example.md +103 -0
  64. package/doc/skills/keve-component-gen/SKILL.md +255 -0
  65. package/doc/skills/keve-component-gen/references/component-mock-strategy.md +74 -0
  66. package/doc/skills/keve-component-gen/references/component-model-template.md +98 -0
  67. package/doc/skills/keve-component-gen/references/component-test-template.md +100 -0
  68. package/doc/skills/keve-component-gen/references/vue-test-utils-guide.md +91 -0
  69. package/doc/skills/keve-e2e-gen/SKILL.md +179 -0
  70. package/doc/skills/keve-e2e-gen/references/code-analysis-guide.md +208 -0
  71. package/doc/skills/keve-e2e-gen/references/decisions.md +36 -0
  72. package/doc/skills/keve-e2e-gen/references/pom-generation-guide.md +140 -0
  73. package/doc/skills/keve-e2e-gen/references/script-generation-guide.md +236 -0
  74. package/doc/skills/keve-e2e-gen/references/test-auditor-guide.md +189 -0
  75. package/doc/skills/keve-e2e-gen/scripts/extract-components.sh +85 -0
  76. package/doc/skills/keve-e2e-gen/scripts/inject-testids.sh +95 -0
  77. package/doc/skills/keve-static-analysis/SKILL.md +362 -0
  78. package/doc/skills/keve-static-analysis/references/codegraph-analysis-guide.md +108 -0
  79. package/doc/skills/keve-static-analysis/references/git-impact-analysis.md +81 -0
  80. package/doc/skills/keve-static-analysis/references/lint-fix-guide.md +69 -0
  81. package/doc/skills/keve-static-analysis/references/runtime-risk-patterns.md +92 -0
  82. package/doc/skills/keve-static-analysis/references/security-scan-guide.md +32 -0
  83. package/doc/skills/keve-static-analysis/references/ts-error-patterns.md +98 -0
  84. package/doc/skills/keve-static-analysis/references/vue-security-guide.md +72 -0
  85. package/doc/skills/keve-test-eval/SKILL.md +78 -0
  86. package/doc/skills/keve-test-eval/references/common-fix-guide.md +166 -0
  87. package/doc/skills/keve-test-eval/references/confidence-data-guide.md +78 -0
  88. package/doc/skills/keve-test-eval/references/fix-loop-guide.md +67 -0
  89. package/doc/skills/keve-unit-gen/SKILL.md +203 -0
  90. package/doc/skills/keve-unit-gen/references/boundary-conditions.md +78 -0
  91. package/doc/skills/keve-unit-gen/references/unit-mock-strategy.md +71 -0
  92. package/doc/skills/keve-unit-gen/references/unit-test-template.md +105 -0
  93. package/package.json +47 -25
  94. package/dist/agent/factory.d.ts +0 -25
  95. package/dist/agent/factory.js +0 -99
  96. package/dist/agent/index.d.ts +0 -3
  97. package/dist/agent/index.js +0 -7
  98. package/dist/agent/loader.d.ts +0 -22
  99. package/dist/agent/loader.js +0 -117
  100. package/dist/agent/types.d.ts +0 -9
  101. package/dist/agent/types.js +0 -2
  102. package/dist/agent-loop/complete-task.d.ts +0 -28
  103. package/dist/agent-loop/complete-task.js +0 -70
  104. package/dist/agent-loop/index.d.ts +0 -16
  105. package/dist/agent-loop/index.js +0 -26
  106. package/dist/agent-loop/loop.d.ts +0 -42
  107. package/dist/agent-loop/loop.js +0 -282
  108. package/dist/agent-loop/runtime.d.ts +0 -40
  109. package/dist/agent-loop/runtime.js +0 -163
  110. package/dist/agent-loop/types.d.ts +0 -161
  111. package/dist/agent-loop/types.js +0 -105
  112. package/dist/context/index.d.ts +0 -8
  113. package/dist/context/index.js +0 -29
  114. package/dist/context/tokenCalculation.d.ts +0 -55
  115. package/dist/context/tokenCalculation.js +0 -95
  116. package/dist/context/truncation.d.ts +0 -67
  117. package/dist/context/truncation.js +0 -142
  118. package/dist/context/types.d.ts +0 -7
  119. package/dist/context/types.js +0 -27
  120. package/dist/llm/OpenAIClient.d.ts +0 -31
  121. package/dist/llm/OpenAIClient.js +0 -455
  122. package/dist/llm/constants.d.ts +0 -2
  123. package/dist/llm/constants.js +0 -6
  124. package/dist/llm/errors.d.ts +0 -25
  125. package/dist/llm/errors.js +0 -51
  126. package/dist/llm/index.d.ts +0 -49
  127. package/dist/llm/index.js +0 -158
  128. package/dist/llm/types.d.ts +0 -124
  129. package/dist/llm/types.js +0 -2
  130. package/dist/llm/utils.d.ts +0 -29
  131. package/dist/llm/utils.js +0 -284
  132. package/dist/skill/frontmatter-parser.d.ts +0 -50
  133. package/dist/skill/frontmatter-parser.js +0 -152
  134. package/dist/skill/index.d.ts +0 -12
  135. package/dist/skill/index.js +0 -17
  136. package/dist/skill/loader.d.ts +0 -96
  137. package/dist/skill/loader.js +0 -256
  138. package/dist/tools/ask-user.d.ts +0 -8
  139. package/dist/tools/ask-user.js +0 -47
  140. package/dist/tools/bash.d.ts +0 -47
  141. package/dist/tools/bash.js +0 -213
  142. package/dist/tools/edit-file.d.ts +0 -9
  143. package/dist/tools/edit-file.js +0 -76
  144. package/dist/tools/glob.d.ts +0 -8
  145. package/dist/tools/glob.js +0 -101
  146. package/dist/tools/grep.d.ts +0 -10
  147. package/dist/tools/grep.js +0 -76
  148. package/dist/tools/index.d.ts +0 -24
  149. package/dist/tools/index.js +0 -47
  150. package/dist/tools/read-files.d.ts +0 -8
  151. package/dist/tools/read-files.js +0 -81
  152. 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
+ }