@kkeve/suite 0.0.1 → 0.0.3
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 +43 -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/system_prompt.md +240 -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 +46 -24
- 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,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
|
+
}
|
package/doc/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
标准测试金字塔(Martin Fowler / Mike Cohn 经典模型)
|
|
2
|
+
▲
|
|
3
|
+
╱ ╲
|
|
4
|
+
╱ ╲
|
|
5
|
+
╱ E2E ╲ ← 少量:跨页面完整用户旅程
|
|
6
|
+
╱ 测试 ╲ 运行慢、成本高、脆弱
|
|
7
|
+
╱━━━━━━━━━╲
|
|
8
|
+
╱ ╲
|
|
9
|
+
╱ 集成测试 ╲ ← 中量:模块间交互、API 调用
|
|
10
|
+
╱ (Integration) ╲ 模拟依赖、验证契约
|
|
11
|
+
╱━━━━━━━━━━━━━━━━━╲
|
|
12
|
+
╱ ╲
|
|
13
|
+
╱ 单元测试 ╲ ← 大量:纯函数、工具方法
|
|
14
|
+
╱ (Unit Test) ╲ 毫秒级、确定性、低成本
|
|
15
|
+
╱━━━━━━━━━━━━━━━━━━━━━━━━╲
|
|
16
|
+
╱ ╲
|
|
17
|
+
╱ 静态分析 (Static) ╲ ← 最底层:TypeScript、ESLint、Prettier
|
|
18
|
+
╱━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╲ 不运行代码、零成本、实时反馈
|
|
19
|
+
|
|
20
|
+
原则:越底层越多、越快、越稳定;越顶层越少、越慢、越脆弱。
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
前端测试金字塔(细化版)
|
|
24
|
+
对于前端项目,经典三层太粗,需要细化:
|
|
25
|
+
▲
|
|
26
|
+
╱ ╲
|
|
27
|
+
╱ ╲
|
|
28
|
+
╱ E2E ╲ ← Playwright/Cypress
|
|
29
|
+
╱ 测试 ╲ 跨页面用户旅程
|
|
30
|
+
╱━━━━━━━━━━╲ 5~10 条
|
|
31
|
+
╱ ╲
|
|
32
|
+
╱ 视觉回归 ╲ ← 截图对比
|
|
33
|
+
╱ (Visual) ╲ UI 一致性
|
|
34
|
+
╱━━━━━━━━━━━━━━━━╲
|
|
35
|
+
╱ ╲
|
|
36
|
+
╱ 组件集成测试 ╲ ← Testing Library
|
|
37
|
+
╱ (Integration) ╲ 组件 + 子组件 + Store
|
|
38
|
+
╱━━━━━━━━━━━━━━━━━━━━━━╲ 模拟 API、验证交互流
|
|
39
|
+
╱ ╲
|
|
40
|
+
╱ 组件单元测试 ╲ ← Vitest + Vue Test Utils
|
|
41
|
+
╱ (Component Unit) ╲ 孤立组件、mock 依赖
|
|
42
|
+
╱━━━━━━━━━━━━━━━━━━━━━━━━━━━━╲ Props in → 渲染 out
|
|
43
|
+
╱ ╲
|
|
44
|
+
╱ 纯函数单元测试 ╲ ← Vitest
|
|
45
|
+
╱ (Pure Unit) ╲ utils / hooks / store
|
|
46
|
+
╱━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╲ 无 DOM 依赖
|
|
47
|
+
╱ ╲
|
|
48
|
+
╱ 静态分析 (Static) ╲ ← TypeScript + ESLint
|
|
49
|
+
╱━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╲ 编译期捕获类型/规范错误
|
|
50
|
+
|
|
51
|
+
Kent C. Dodds 的"测试奖杯"模型
|
|
52
|
+
前端社区更流行的其实是 Testing Trophy,因为前端 E2E 成本极高,集成测试才是 ROI 最高的层次。
|
|
53
|
+
🏆
|
|
54
|
+
╱ ╲
|
|
55
|
+
╱E2E╲ ← 极少:仅冒烟 / 关键路径
|
|
56
|
+
╱━━━━━╲ 3~5
|
|
57
|
+
条
|
|
58
|
+
╱ ╲
|
|
59
|
+
╱ 集成测试 ╲ ← 最多:组件集成 + API mock
|
|
60
|
+
╱ (Integration) ╲ ROI 最高、信心最强
|
|
61
|
+
╱━━━━━━━━━━━━━━━━╲
|
|
62
|
+
╱ ╲
|
|
63
|
+
╱ 单元测试 ╲ ← 补充:纯逻辑、边界条件
|
|
64
|
+
╱ (Unit) ╲ 快但不验证用户行为
|
|
65
|
+
╱━━━━━━━━━━━━━━━━━━━━━━━━╲
|
|
66
|
+
╱ ╲
|
|
67
|
+
╱ 静态分析 (Static) ╲ ← 基座:零成本捕获大量错误
|
|
68
|
+
╱━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╲
|
|
69
|
+
|
|
70
|
+
核心区别:奖杯模型中集成测试最宽(而非单元测试最宽),因为:
|
|
71
|
+
- 单元测试只验证"零件",集成测试验证"装配"
|
|
72
|
+
- 前端 bug 多出在组件协作、API 对接、状态管理,而非纯函数逻辑
|
|
73
|
+
- 集成测试的信心度远高于单元测试(更接近真实使用)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
当前keve-agent生成的那就应该是前端集成测试(Integration Test)
|
|
77
|
+
|
|
78
|
+
传统 E2E(后端视角):
|
|
79
|
+
用户浏览器 ──────────────────────────────→ 数据库
|
|
80
|
+
┃ 前端 API 后端 ┃
|
|
81
|
+
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
82
|
+
"端到端" = 从用户操作到数据库落盘,全链路
|
|
83
|
+
|
|
84
|
+
keve-agent 做的测试(前端视角):
|
|
85
|
+
用户浏览器 ─────────→ 前端 devServer
|
|
86
|
+
┃ 前端 ┃
|
|
87
|
+
┗━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
88
|
+
"端" = 只到前端渲染层,后端是 mock/stub
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
┌────────────────────────────────────────────────────────────────────────┐
|
|
93
|
+
│ 静态分析能力谱(三阶段) │
|
|
94
|
+
├─────────────┬─────────────────────────────────────────────────────────┤
|
|
95
|
+
│ Phase 1 │ tsc 类型检查 + eslint 规范(P0 必修,自动修复) │
|
|
96
|
+
│ tsc+eslint │ │
|
|
97
|
+
│ │ ✅ 类型不匹配、缺失属性、隐式 any、缺失导入 │
|
|
98
|
+
│ │ ✅ 可空类型误用、未使用变量/参数 │
|
|
99
|
+
│ │ ✅ 缺失依赖项、等号误赋值、console.log │
|
|
100
|
+
│ │ ✅ 缺失返回值、一致性规范(命名/排序) │
|
|
101
|
+
├─────────────┼─────────────────────────────────────────────────────────┤
|
|
102
|
+
│ Phase 2 │ Code-Graph 关系分析(P1 建议修,只输出不自动改) │
|
|
103
|
+
│ Code-Graph │ │
|
|
104
|
+
│ (Tree-sitter)│ ✅ 循环依赖检测(A→B→C→A) │
|
|
105
|
+
│ │ ✅ 幽灵依赖检测(import 了但 package.json 没声明) │
|
|
106
|
+
│ │ ✅ 死代码检测(无调用者的非导出函数) │
|
|
107
|
+
│ │ ✅ 架构层级违规(utils 不应 import components) │
|
|
108
|
+
│ │ ✅ 变更影响范围(改了函数 X,哪些模块受影响) │
|
|
109
|
+
├─────────────┼─────────────────────────────────────────────────────────┤
|
|
110
|
+
│ Phase 3 │ Git 变更分析(P2 信息参考,不自动改) │
|
|
111
|
+
│ Git 分析 │ │
|
|
112
|
+
│ │ ✅ 当前 diff 修改了哪些文件/函数 │
|
|
113
|
+
│ │ ✅ 变更影响范围(结合 Phase 2 计算受影响的调用链) │
|
|
114
|
+
│ │ ✅ 热点文件(30天内高频变更 = 不稳定区域) │
|
|
115
|
+
│ │ ✅ 新增文件(可能缺少测试覆盖) │
|
|
116
|
+
├─────────────┼─────────────────────────────────────────────────────────┤
|
|
117
|
+
│ ❌ 未覆盖 │ 安全漏洞 → keve-static-analysis Phase 4(已规划) │
|
|
118
|
+
│ │ XSS/SQL注入/ReDoS/硬编码敏感信息/npm audit │
|
|
119
|
+
│ │ 运行时风险 → Phase 4(已规划) │
|
|
120
|
+
│ │ 内存泄漏/事件监听器未移除/Promise rejection │
|
|
121
|
+
│ │ 代码克隆 → Code-Graph AST 比对 + AI 模式匹配(可覆盖)│
|
|
122
|
+
│ │ 圈/认知复杂度 → Code-Graph 获取函数体 + AI 计算决策点 │
|
|
123
|
+
│ │ → 大部分未覆盖项可通过 Code-Graph + AI 间接覆盖 │
|
|
124
|
+
└─────────────┴─────────────────────────────────────────────────────────┘
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: keve-dev-test-agent
|
|
3
|
+
description: 开发阶段测试流水线编排代理。协调 static-analysis → unit-gen → component-gen 三技能按序执行,支持阶段检测、断点续跑、覆盖率摘要输出。
|
|
4
|
+
tools:
|
|
5
|
+
- Bash(shell:*)
|
|
6
|
+
- Bash(npx:*)
|
|
7
|
+
- Bash(npm:*)
|
|
8
|
+
- Bash(node:*)
|
|
9
|
+
- Bash(git:*)
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# KEVE 开发阶段测试编排代理
|
|
13
|
+
|
|
14
|
+
> 本代理是**薄编排层**,负责:检测起点、创建执行计划、调度技能、输出覆盖率摘要。
|
|
15
|
+
> 不包含技能细节——不做类型修复、不写测试脚本、不做组件分析。
|
|
16
|
+
|
|
17
|
+
## 触发方式
|
|
18
|
+
|
|
19
|
+
用户输入 `/keve-dev-test` 指令触发,跳过意图识别直接进入本流程。
|
|
20
|
+
|
|
21
|
+
适用场景:**开发阶段**,需要快速反馈(秒级),不涉及 E2E 测试。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 整体流程
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
1. 阶段检测 → 确定触发模式(dev),创建 test-plan.yaml
|
|
29
|
+
2. 调度 keve-static-analysis → 修类型/lint 错误
|
|
30
|
+
3. 调度 keve-unit-gen → 生成单测 + coverage.yaml
|
|
31
|
+
4. 调度 keve-component-gen → 生成组件模型 + 组件测试 + coverage.yaml
|
|
32
|
+
5. 输出覆盖率摘要 + 缺口列表
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 阶段检测
|
|
38
|
+
|
|
39
|
+
### Step 0: 读取或创建 test-plan.yaml
|
|
40
|
+
|
|
41
|
+
1. **已有 test-plan.yaml**:
|
|
42
|
+
- 读取 `context.trigger` 和各 phase.status
|
|
43
|
+
- 输出:"检测到已有执行计划,当前进度:"
|
|
44
|
+
- 列出各阶段状态(✅ completed / 🔄 in_progress / ⏳ pending / ⏭️ skipped)
|
|
45
|
+
- 询问用户:"从断点继续 / 重新开始?"
|
|
46
|
+
|
|
47
|
+
2. **无 test-plan.yaml**:
|
|
48
|
+
- 根据 trigger=dev 创建新 test-plan.yaml
|
|
49
|
+
- 输出执行计划,等待用户确认
|
|
50
|
+
|
|
51
|
+
### 阶段自动检测
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
if 用户指定 MR ID:
|
|
55
|
+
→ 建议使用 /keve-test (keve-test-agent)
|
|
56
|
+
→ 确认后继续或切换
|
|
57
|
+
|
|
58
|
+
elif git diff 有未提交变更:
|
|
59
|
+
→ 开发阶段 → trigger=dev
|
|
60
|
+
|
|
61
|
+
else:
|
|
62
|
+
→ 询问用户意图(开发测试 / MR 提测 / 其他)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Dev 模式的 Phase 模板
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
phases:
|
|
69
|
+
- { id: static, skill: keve-static-analysis, status: pending }
|
|
70
|
+
- { id: unit, skill: keve-unit-gen, status: pending }
|
|
71
|
+
- { id: component, skill: keve-component-gen, status: pending }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 调度技能
|
|
77
|
+
|
|
78
|
+
| 步骤 | 调度目标 | use_skill 名称 | 输入 | 期望输出 |
|
|
79
|
+
|------|----------|---------------|------|----------|
|
|
80
|
+
| Step 2 | 静态分析 | `keve-static-analysis` | 项目源码 | 0 type error + 0 lint error |
|
|
81
|
+
| Step 3 | 单测生成 | `keve-unit-gen` | src/utils/ + hooks/ | `keve_test_spec/util/__test__/*.test.ts` + `coverage.yaml` |
|
|
82
|
+
| Step 4 | 组件测试生成 | `keve-component-gen` | src/components/ | `keve_test_spec/component/models/*.model.ts` + `__test__/*.test.ts` + `coverage.yaml` |
|
|
83
|
+
|
|
84
|
+
**每个步骤完成后**:
|
|
85
|
+
1. 更新 test-plan.yaml 中对应 phase 的 status
|
|
86
|
+
2. 确认产物已生成
|
|
87
|
+
3. 进入下一步
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 断点续跑
|
|
92
|
+
|
|
93
|
+
如果 test-plan.yaml 中存在 `status = in_progress` 或 `status = pending` 的 phase:
|
|
94
|
+
1. 找到第一个未完成的 phase
|
|
95
|
+
2. 从该 phase 开始继续执行
|
|
96
|
+
3. 跳过已 completed 的 phase
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 输出摘要
|
|
101
|
+
|
|
102
|
+
所有 phase 完成后,输出:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
━━━ KEVE 开发阶段测试报告 ━━━
|
|
106
|
+
|
|
107
|
+
静态分析: ✅ 通过 (0 type error, 0 lint error)
|
|
108
|
+
单元测试: ✅ 13/15 函数已覆盖 (87%)
|
|
109
|
+
组件测试: 🔄 12/20 组件已覆盖 (60%)
|
|
110
|
+
|
|
111
|
+
覆盖率缺口:
|
|
112
|
+
❌ src/utils/validator.ts — 未生成测试
|
|
113
|
+
❌ src/components/ToolManagement.vue — 未生成测试
|
|
114
|
+
⚠️ src/utils/format.ts:formatCurrency — 缺少 NaN输入 场景
|
|
115
|
+
|
|
116
|
+
下一步:
|
|
117
|
+
- 运行 npx vitest run --coverage 查看详细覆盖率
|
|
118
|
+
- 修复缺口后重新运行 /keve-dev-test
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 与 keve-test-agent 的关系
|
|
124
|
+
|
|
125
|
+
| 维度 | keve-dev-test-agent | keve-test-agent |
|
|
126
|
+
|------|---------------------|-----------------|
|
|
127
|
+
| 适用阶段 | 开发中 | MR 提测前 |
|
|
128
|
+
| 编排 | static → unit → component | bddcase-gen → e2e-gen → test-eval |
|
|
129
|
+
| 反馈速度 | 秒级 | 分钟级 |
|
|
130
|
+
| 产出 | 覆盖率 + 缺口列表 | E2E 报告 + 评估 |
|
|
131
|
+
| test-plan trigger | dev | mr |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 禁止事项
|
|
136
|
+
|
|
137
|
+
- 不做代码分析、POM 设计、定位器生成
|
|
138
|
+
- 不写测试脚本
|
|
139
|
+
- 不运行 Playwright E2E 测试
|
|
140
|
+
- 不自行决定修复策略(只根据 phase status 决策调度方向)
|