qat-cli 0.3.2 → 0.3.4
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 +1 -1
- package/dist/cli.js +572 -379
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +320 -134
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +133 -120
- package/dist/index.d.ts +133 -120
- package/dist/index.js +320 -134
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -955,12 +955,36 @@ Handlebars.registerHelper("propTestValue", (prop) => {
|
|
|
955
955
|
};
|
|
956
956
|
return map[prop.type] || "'test-value'";
|
|
957
957
|
});
|
|
958
|
+
function resolveImportPath(testType, targetPath) {
|
|
959
|
+
if (!targetPath) return targetPath;
|
|
960
|
+
const testDirMap = {
|
|
961
|
+
unit: "tests/unit",
|
|
962
|
+
component: "tests/component",
|
|
963
|
+
e2e: "tests/e2e",
|
|
964
|
+
api: "tests/api",
|
|
965
|
+
visual: "tests/visual",
|
|
966
|
+
performance: "tests/e2e"
|
|
967
|
+
};
|
|
968
|
+
const testDir = testDirMap[testType];
|
|
969
|
+
if (!testDir) return targetPath;
|
|
970
|
+
if (targetPath.startsWith("../") || targetPath.startsWith("./../")) {
|
|
971
|
+
return targetPath;
|
|
972
|
+
}
|
|
973
|
+
const depth = testDir.split("/").length;
|
|
974
|
+
const prefix = "../".repeat(depth);
|
|
975
|
+
let cleanPath = targetPath.replace(/^\.\//, "");
|
|
976
|
+
if (!cleanPath.endsWith(".vue")) {
|
|
977
|
+
cleanPath = cleanPath.replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
978
|
+
}
|
|
979
|
+
return `${prefix}${cleanPath}`;
|
|
980
|
+
}
|
|
958
981
|
function registerTemplate(type, templateContent) {
|
|
959
982
|
customTemplates.set(type, templateContent);
|
|
960
983
|
}
|
|
961
984
|
function renderTemplate(type, context) {
|
|
962
985
|
const templateContent = loadTemplate(type);
|
|
963
986
|
const template = Handlebars.compile(templateContent);
|
|
987
|
+
const resolvedTarget = resolveImportPath(type, context.target);
|
|
964
988
|
const fullContext = {
|
|
965
989
|
vueVersion: 3,
|
|
966
990
|
typescript: true,
|
|
@@ -981,6 +1005,8 @@ function renderTemplate(type, context) {
|
|
|
981
1005
|
requiredProps: [],
|
|
982
1006
|
optionalProps: [],
|
|
983
1007
|
...context,
|
|
1008
|
+
// 使用计算后的正确路径
|
|
1009
|
+
target: resolvedTarget,
|
|
984
1010
|
framework: context.framework || "vue",
|
|
985
1011
|
camelName: context.camelName || toCamelCase(context.name),
|
|
986
1012
|
pascalName: context.pascalName || toPascalCase(context.name)
|
|
@@ -1682,14 +1708,14 @@ function buildVitestArgs(options) {
|
|
|
1682
1708
|
if (options.files && options.files.length > 0) {
|
|
1683
1709
|
args.push(...options.files);
|
|
1684
1710
|
} else {
|
|
1685
|
-
const
|
|
1686
|
-
unit:
|
|
1687
|
-
component:
|
|
1688
|
-
api:
|
|
1711
|
+
const pathMap = {
|
|
1712
|
+
unit: "tests/unit/**/*.test.ts",
|
|
1713
|
+
component: "tests/component/**/*.test.ts",
|
|
1714
|
+
api: "tests/api/**/*.test.ts"
|
|
1689
1715
|
};
|
|
1690
|
-
const
|
|
1691
|
-
if (
|
|
1692
|
-
args.push(
|
|
1716
|
+
const testPattern = pathMap[options.type];
|
|
1717
|
+
if (testPattern) {
|
|
1718
|
+
args.push(testPattern);
|
|
1693
1719
|
}
|
|
1694
1720
|
}
|
|
1695
1721
|
if (options.coverage) {
|
|
@@ -2498,6 +2524,15 @@ var NoopAIProvider = class {
|
|
|
2498
2524
|
};
|
|
2499
2525
|
|
|
2500
2526
|
// src/ai/openai-provider.ts
|
|
2527
|
+
import chalk2 from "chalk";
|
|
2528
|
+
function isDebug() {
|
|
2529
|
+
return process.env.QAT_DEBUG === "true";
|
|
2530
|
+
}
|
|
2531
|
+
function debugLog(tag, ...args) {
|
|
2532
|
+
if (!isDebug()) return;
|
|
2533
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
2534
|
+
console.error(chalk2.gray(`[DEBUG ${timestamp}] [${tag}]`), ...args);
|
|
2535
|
+
}
|
|
2501
2536
|
var OpenAICompatibleProvider = class {
|
|
2502
2537
|
constructor(config) {
|
|
2503
2538
|
this.capabilities = {
|
|
@@ -2511,25 +2546,20 @@ var OpenAICompatibleProvider = class {
|
|
|
2511
2546
|
this.baseUrl = config.baseUrl || this.getDefaultBaseUrl(config.provider);
|
|
2512
2547
|
}
|
|
2513
2548
|
async generateTest(req) {
|
|
2549
|
+
debugLog("GENERATE", `type=${req.type} target=${req.target}`);
|
|
2514
2550
|
const systemPrompt = this.buildGenerateTestSystemPrompt(req);
|
|
2515
2551
|
const userPrompt = this.buildGenerateTestUserPrompt(req);
|
|
2516
2552
|
const content = await this.chat(systemPrompt, userPrompt);
|
|
2517
|
-
|
|
2553
|
+
const result = this.parseGenerateTestResponse(content);
|
|
2554
|
+
debugLog("GENERATE", `done code=${result.code.length}chars confidence=${result.confidence}`);
|
|
2555
|
+
return result;
|
|
2518
2556
|
}
|
|
2519
2557
|
async analyzeResult(req) {
|
|
2520
|
-
const systemPrompt = `\
|
|
2521
|
-
\u8F93\u51FA\
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
const
|
|
2525
|
-
const failed = r.suites.flatMap((s) => s.tests.filter((t) => t.status === "failed"));
|
|
2526
|
-
return `\u7C7B\u578B: ${r.type}, \u72B6\u6001: ${r.status}, \u8017\u65F6: ${r.duration}ms, \u5931\u8D25\u7528\u4F8B: ${failed.length}`;
|
|
2527
|
-
}).join("\n");
|
|
2528
|
-
const errorDetails = req.errorLogs?.join("\n") || req.testResults.flatMap((r) => r.suites.flatMap((s) => s.tests.filter((t) => t.status === "failed" && t.error))).map((t) => `[${t.name}] ${t.error?.message}`).join("\n") || "\u65E0\u9519\u8BEF\u8BE6\u60C5";
|
|
2529
|
-
const userPrompt = `\u6D4B\u8BD5\u7ED3\u679C:
|
|
2530
|
-
${resultSummary}
|
|
2531
|
-
|
|
2532
|
-
\u9519\u8BEF\u8BE6\u60C5:
|
|
2558
|
+
const systemPrompt = `\u6D4B\u8BD5\u5206\u6790\u4E13\u5BB6\u3002\u627E\u95EE\u9898\u6839\u56E0\uFF0C\u7ED9\u53EF\u64CD\u4F5C\u5EFA\u8BAE\u3002
|
|
2559
|
+
\u8F93\u51FA:1.\u6458\u8981(1-3\u53E5) 2.\u5EFA\u8BAE\u5217\u8868`;
|
|
2560
|
+
const failed = req.testResults.flatMap((r) => r.suites.flatMap((s) => s.tests.filter((t) => t.status === "failed")));
|
|
2561
|
+
const errorDetails = req.errorLogs?.join("\n") || failed.map((t) => `[${t.name}] ${t.error?.message}`).join("\n") || "\u65E0";
|
|
2562
|
+
const userPrompt = `\u5931\u8D25:${failed.length}
|
|
2533
2563
|
${errorDetails}`;
|
|
2534
2564
|
const content = await this.chat(systemPrompt, userPrompt);
|
|
2535
2565
|
return {
|
|
@@ -2539,23 +2569,22 @@ ${errorDetails}`;
|
|
|
2539
2569
|
};
|
|
2540
2570
|
}
|
|
2541
2571
|
async suggestFix(error) {
|
|
2542
|
-
const systemPrompt = `\
|
|
2543
|
-
\
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
const userPrompt = `\u9519\u8BEF\u4FE1\u606F: ${error.message}
|
|
2548
|
-
${error.stack ? `\u5806\u6808: ${error.stack}` : ""}
|
|
2549
|
-
${error.expected ? `\u671F\u671B\u503C: ${error.expected}` : ""}
|
|
2550
|
-
${error.actual ? `\u5B9E\u9645\u503C: ${error.actual}` : ""}`;
|
|
2572
|
+
const systemPrompt = `\u4EE3\u7801\u4FEE\u590D\u4E13\u5BB6\u3002\u7ED9\u51FA:1.\u95EE\u9898\u5B9A\u4F4D 2.\u4FEE\u590D\u65B9\u6848 3.\u793A\u4F8B\u4EE3\u7801`;
|
|
2573
|
+
const userPrompt = `\u9519\u8BEF:${error.message}${error.stack ? `
|
|
2574
|
+
\u5806\u6808:${error.stack}` : ""}${error.expected ? `
|
|
2575
|
+
\u671F\u671B:${error.expected}` : ""}${error.actual ? `
|
|
2576
|
+
\u5B9E\u9645:${error.actual}` : ""}`;
|
|
2551
2577
|
const content = await this.chat(systemPrompt, userPrompt);
|
|
2552
2578
|
return content.split("\n").filter((l) => l.trim().startsWith("-") || l.trim().startsWith("\u2022") || l.trim().match(/^\d+\./)).map((l) => l.replace(/^[-•\d.]+\s*/, "").trim()).filter(Boolean);
|
|
2553
2579
|
}
|
|
2554
2580
|
async reviewTest(req) {
|
|
2581
|
+
debugLog("REVIEW", `target=${req.target} type=${req.testType}`);
|
|
2555
2582
|
const systemPrompt = this.buildReviewTestSystemPrompt(req);
|
|
2556
2583
|
const userPrompt = this.buildReviewTestUserPrompt(req);
|
|
2557
2584
|
const content = await this.chat(systemPrompt, userPrompt);
|
|
2558
|
-
|
|
2585
|
+
const result = this.parseReviewTestResponse(content);
|
|
2586
|
+
debugLog("REVIEW", `approved=${result.approved} score=${result.score} feedback=${result.feedback}`);
|
|
2587
|
+
return result;
|
|
2559
2588
|
}
|
|
2560
2589
|
// ─── 内部方法 ──────────────────────────────────────────────
|
|
2561
2590
|
/**
|
|
@@ -2619,8 +2648,9 @@ ${error.actual ? `\u5B9E\u9645\u503C: ${error.actual}` : ""}`;
|
|
|
2619
2648
|
return { ok: false, message: `\u8FDE\u63A5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`, latencyMs };
|
|
2620
2649
|
}
|
|
2621
2650
|
}
|
|
2622
|
-
async chat(systemPrompt, userPrompt) {
|
|
2651
|
+
async chat(systemPrompt, userPrompt, retries = 2) {
|
|
2623
2652
|
const url = `${this.baseUrl}/chat/completions`;
|
|
2653
|
+
const useStream = isDebug();
|
|
2624
2654
|
const body = {
|
|
2625
2655
|
model: this.model,
|
|
2626
2656
|
messages: [
|
|
@@ -2630,99 +2660,218 @@ ${error.actual ? `\u5B9E\u9645\u503C: ${error.actual}` : ""}`;
|
|
|
2630
2660
|
temperature: 0.3,
|
|
2631
2661
|
max_tokens: 4096
|
|
2632
2662
|
};
|
|
2663
|
+
if (useStream) {
|
|
2664
|
+
body.stream = true;
|
|
2665
|
+
}
|
|
2633
2666
|
const headers = {
|
|
2634
2667
|
"Content-Type": "application/json"
|
|
2635
2668
|
};
|
|
2636
2669
|
if (this.apiKey) {
|
|
2637
2670
|
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
2638
2671
|
}
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2672
|
+
debugLog("REQUEST", `POST ${url}`);
|
|
2673
|
+
debugLog("REQUEST", `model=${this.model} stream=${useStream}`);
|
|
2674
|
+
debugLog("SYSTEM", systemPrompt.length > 500 ? `${systemPrompt.slice(0, 500)}...` : systemPrompt);
|
|
2675
|
+
debugLog("USER", userPrompt.length > 1e3 ? `${userPrompt.slice(0, 1e3)}...` : userPrompt);
|
|
2676
|
+
let lastError = null;
|
|
2677
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
2678
|
+
try {
|
|
2679
|
+
if (attempt > 0) {
|
|
2680
|
+
debugLog("RETRY", `\u7B2C${attempt}\u6B21\u91CD\u8BD5...`);
|
|
2681
|
+
}
|
|
2682
|
+
const response = await fetch(url, {
|
|
2683
|
+
method: "POST",
|
|
2684
|
+
headers,
|
|
2685
|
+
body: JSON.stringify(body),
|
|
2686
|
+
signal: AbortSignal.timeout(12e4)
|
|
2687
|
+
// 120s timeout
|
|
2688
|
+
});
|
|
2689
|
+
if (!response.ok) {
|
|
2690
|
+
const text = await response.text().catch(() => "");
|
|
2691
|
+
if ((response.status === 429 || response.status >= 500) && attempt < retries) {
|
|
2692
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt), 8e3);
|
|
2693
|
+
debugLog("RETRY", `HTTP ${response.status}, ${delay}ms\u540E\u91CD\u8BD5`);
|
|
2694
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
2695
|
+
lastError = new Error(`AI API \u8BF7\u6C42\u5931\u8D25 (${response.status}): ${text.slice(0, 200)}`);
|
|
2696
|
+
continue;
|
|
2697
|
+
}
|
|
2698
|
+
throw new Error(`AI API \u8BF7\u6C42\u5931\u8D25 (${response.status}): ${text.slice(0, 500)}`);
|
|
2699
|
+
}
|
|
2700
|
+
if (useStream && response.body) {
|
|
2701
|
+
const content = await this.readStream(response.body);
|
|
2702
|
+
debugLog("RESPONSE", content.length > 500 ? `${content.slice(0, 500)}...` : content);
|
|
2703
|
+
return content;
|
|
2704
|
+
}
|
|
2705
|
+
const data = await response.json();
|
|
2706
|
+
if (!data.choices?.[0]?.message?.content) {
|
|
2707
|
+
throw new Error("AI API \u8FD4\u56DE\u7A7A\u54CD\u5E94");
|
|
2708
|
+
}
|
|
2709
|
+
debugLog("RESPONSE", `tokens: prompt=${data.usage?.prompt_tokens} completion=${data.usage?.completion_tokens} total=${data.usage?.total_tokens}`);
|
|
2710
|
+
debugLog("RESPONSE", data.choices[0].message.content.length > 500 ? `${data.choices[0].message.content.slice(0, 500)}...` : data.choices[0].message.content);
|
|
2711
|
+
return data.choices[0].message.content;
|
|
2712
|
+
} catch (error) {
|
|
2713
|
+
if (error instanceof Error && error.name === "TimeoutError" && attempt < retries) {
|
|
2714
|
+
debugLog("TIMEOUT", `\u7B2C${attempt}\u6B21\u8D85\u65F6\uFF0C\u91CD\u8BD5\u4E2D...`);
|
|
2715
|
+
lastError = error;
|
|
2716
|
+
continue;
|
|
2717
|
+
}
|
|
2718
|
+
throw error;
|
|
2719
|
+
}
|
|
2649
2720
|
}
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2721
|
+
throw lastError || new Error("AI API \u8BF7\u6C42\u5931\u8D25");
|
|
2722
|
+
}
|
|
2723
|
+
/**
|
|
2724
|
+
* 读取 SSE 流式响应,实时输出内容
|
|
2725
|
+
*/
|
|
2726
|
+
async readStream(body) {
|
|
2727
|
+
const chunks = [];
|
|
2728
|
+
const decoder = new TextDecoder();
|
|
2729
|
+
const reader = body.getReader();
|
|
2730
|
+
let buffer = "";
|
|
2731
|
+
let lineCount = 0;
|
|
2732
|
+
try {
|
|
2733
|
+
while (true) {
|
|
2734
|
+
const { done, value } = await reader.read();
|
|
2735
|
+
if (done) break;
|
|
2736
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2737
|
+
const lines = buffer.split("\n");
|
|
2738
|
+
buffer = lines.pop() || "";
|
|
2739
|
+
for (const line of lines) {
|
|
2740
|
+
const trimmed = line.trim();
|
|
2741
|
+
if (!trimmed || trimmed === "data: [DONE]") continue;
|
|
2742
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
2743
|
+
try {
|
|
2744
|
+
const json = JSON.parse(trimmed.slice(6));
|
|
2745
|
+
const delta = json.choices?.[0]?.delta?.content;
|
|
2746
|
+
if (delta) {
|
|
2747
|
+
chunks.push(delta);
|
|
2748
|
+
lineCount++;
|
|
2749
|
+
process.stderr.write(chalk2.gray(delta));
|
|
2750
|
+
if (lineCount % 20 === 0) {
|
|
2751
|
+
process.stderr.write("\n");
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
if (json.choices?.[0]?.finish_reason === "stop") {
|
|
2755
|
+
debugLog("STREAM", "\u5B8C\u6210");
|
|
2756
|
+
}
|
|
2757
|
+
} catch {
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
} finally {
|
|
2762
|
+
reader.releaseLock();
|
|
2763
|
+
}
|
|
2764
|
+
if (chunks.length > 0) {
|
|
2765
|
+
process.stderr.write("\n");
|
|
2766
|
+
}
|
|
2767
|
+
return chunks.join("");
|
|
2768
|
+
}
|
|
2769
|
+
/**
|
|
2770
|
+
* 压缩源码:保留签名和关键逻辑,剔除注释、空行、样式块
|
|
2771
|
+
* 目标:在保留准确性的前提下减少 token 消耗
|
|
2772
|
+
*/
|
|
2773
|
+
compressSourceCode(code, maxLength = 3e3) {
|
|
2774
|
+
let compressed = code;
|
|
2775
|
+
compressed = compressed.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
2776
|
+
compressed = compressed.replace(/<template[^>]*>([\s\S]*?)<\/template>/gi, (_match, content) => {
|
|
2777
|
+
return `<template>${content.replace(/\s*(?:class|style)\s*=\s*["'][^"']*["']/gi, "")}</template>`;
|
|
2778
|
+
});
|
|
2779
|
+
compressed = compressed.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
2780
|
+
compressed = compressed.replace(/(^|[^:])(\/\/.*$)/gm, "$1");
|
|
2781
|
+
compressed = compressed.replace(/\n\s*\n\s*\n/g, "\n\n");
|
|
2782
|
+
compressed = compressed.split("\n").map((line) => line.trimEnd()).join("\n").trim();
|
|
2783
|
+
if (compressed.length > maxLength) {
|
|
2784
|
+
const scriptMatch = compressed.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
|
|
2785
|
+
const templateMatch = compressed.match(/<template[^>]*>([\s\S]*?)<\/template>/i);
|
|
2786
|
+
if (scriptMatch) {
|
|
2787
|
+
let scriptPart = scriptMatch[1].trim();
|
|
2788
|
+
scriptPart = compressFunctionBodies(scriptPart, maxLength * 0.7);
|
|
2789
|
+
const templatePart = templateMatch ? `<template>${templateMatch[1].replace(/\s+/g, " ").trim().slice(0, 300)}...</template>` : "";
|
|
2790
|
+
compressed = `${templatePart}
|
|
2791
|
+
<script${scriptMatch[0].match(/<script[^>]*>/)?.[0]?.slice(7) || ">"}>${scriptPart}</script>`;
|
|
2792
|
+
} else {
|
|
2793
|
+
compressed = compressFunctionBodies(compressed, maxLength);
|
|
2794
|
+
}
|
|
2653
2795
|
}
|
|
2654
|
-
return
|
|
2796
|
+
return compressed;
|
|
2655
2797
|
}
|
|
2656
2798
|
buildGenerateTestSystemPrompt(req) {
|
|
2657
2799
|
const typeMap = {
|
|
2658
|
-
unit: "\u5355\u5143\u6D4B\u8BD5
|
|
2659
|
-
component: "\u7EC4\u4EF6\u6D4B\u8BD5
|
|
2660
|
-
e2e: "E2E\
|
|
2661
|
-
api: "API\
|
|
2662
|
-
visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5
|
|
2663
|
-
performance: "\u6027\u80FD\u6D4B\u8BD5
|
|
2800
|
+
unit: "\u5355\u5143\u6D4B\u8BD5(Vitest)",
|
|
2801
|
+
component: "\u7EC4\u4EF6\u6D4B\u8BD5(Vitest+@vue/test-utils)",
|
|
2802
|
+
e2e: "E2E\u6D4B\u8BD5(Playwright)",
|
|
2803
|
+
api: "API\u6D4B\u8BD5(Vitest+fetch)",
|
|
2804
|
+
visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5(Playwright)",
|
|
2805
|
+
performance: "\u6027\u80FD\u6D4B\u8BD5(Playwright)"
|
|
2664
2806
|
};
|
|
2665
|
-
return `\u4F60\u662F\
|
|
2666
|
-
\
|
|
2667
|
-
1. \u53EA\u8F93\u51FA\u6D4B\u8BD5\u4EE3\u7801\uFF0C\u4E0D\u8981\u591A\u4F59\u7684\u89E3\u91CA
|
|
2668
|
-
2. \u4EE3\u7801\u5FC5\u987B\u53EF\u76F4\u63A5\u8FD0\u884C\uFF0C\u5305\u542B\u6240\u6709\u5FC5\u8981\u7684 import
|
|
2669
|
-
3. \u6D4B\u8BD5\u7528\u4F8B\u8986\u76D6\uFF1A\u6B63\u5E38\u8DEF\u5F84\u3001\u8FB9\u754C\u6761\u4EF6\u3001\u9519\u8BEF\u5904\u7406
|
|
2670
|
-
4. \u4F7F\u7528\u4E2D\u6587\u63CF\u8FF0 it/test \u5757\u540D\u79F0
|
|
2671
|
-
5. Vue \u7EC4\u4EF6\u6D4B\u8BD5\u4F7F\u7528 @vue/test-utils \u7684 mount
|
|
2672
|
-
6. \u5982\u679C\u6709 props/emits \u4FE1\u606F\uFF0C\u52A1\u5FC5\u9488\u5BF9\u6BCF\u4E2A prop \u548C emit \u751F\u6210\u6D4B\u8BD5`;
|
|
2807
|
+
return `\u4F60\u662F\u524D\u7AEF\u6D4B\u8BD5\u5DE5\u7A0B\u5E08\uFF0C\u7F16\u5199${typeMap[req.type] || req.type}\u3002
|
|
2808
|
+
\u89C4\u5219:1.\u53EA\u8F93\u51FA\u6D4B\u8BD5\u4EE3\u7801 2.\u5305\u542B\u6240\u6709import 3.\u8986\u76D6\u6B63\u5E38/\u8FB9\u754C/\u9519\u8BEF 4.it\u540D\u7528\u4E2D\u6587 5.Vue\u7528mount 6.\u5FC5\u6D4B\u6BCF\u4E2Aprop\u548Cemit`;
|
|
2673
2809
|
}
|
|
2674
2810
|
buildGenerateTestUserPrompt(req) {
|
|
2675
|
-
|
|
2676
|
-
|
|
2811
|
+
const importPath = this.computeTestImportPath(req.type, req.target);
|
|
2812
|
+
let prompt = `\u4E3A${req.target}\u751F\u6210${req.type}\u6D4B\u8BD5\u3002import\u8DEF\u5F84:${importPath}
|
|
2677
2813
|
`;
|
|
2678
2814
|
if (req.analysis) {
|
|
2679
|
-
|
|
2815
|
+
const parts = [];
|
|
2680
2816
|
if (req.analysis.exports?.length > 0) {
|
|
2681
|
-
|
|
2682
|
-
${
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
return ` - ${asyncFlag}${e.name}${params} [${e.kind}]`;
|
|
2686
|
-
}).join("\n")}
|
|
2687
|
-
`;
|
|
2817
|
+
parts.push(`\u5BFC\u51FA:${req.analysis.exports.map((e) => {
|
|
2818
|
+
const p = e.params?.length ? `(${e.params.join(",")})` : "";
|
|
2819
|
+
return `${e.isAsync ? "async " : ""}${e.name}${p}[${e.kind}]`;
|
|
2820
|
+
}).join(",")}`);
|
|
2688
2821
|
}
|
|
2689
2822
|
if (req.analysis.props?.length) {
|
|
2690
|
-
|
|
2691
|
-
${req.analysis.props.map(
|
|
2692
|
-
(p) => ` - ${p.name}: ${p.type}${p.required ? " (\u5FC5\u586B)" : " (\u53EF\u9009)"}`
|
|
2693
|
-
).join("\n")}
|
|
2694
|
-
`;
|
|
2823
|
+
parts.push(`Props:${req.analysis.props.map((p) => `${p.name}:${p.type}${p.required ? "!" : "?"}`).join(",")}`);
|
|
2695
2824
|
}
|
|
2696
2825
|
if (req.analysis.emits?.length) {
|
|
2697
|
-
|
|
2698
|
-
${req.analysis.emits.map(
|
|
2699
|
-
(e) => ` - ${e.name}${e.params?.length ? `(${e.params.join(", ")})` : ""}`
|
|
2700
|
-
).join("\n")}
|
|
2701
|
-
`;
|
|
2826
|
+
parts.push(`Emits:${req.analysis.emits.map((e) => `${e.name}(${e.params?.join(",") || ""})`).join(",")}`);
|
|
2702
2827
|
}
|
|
2703
2828
|
if (req.analysis.methods?.length) {
|
|
2704
|
-
|
|
2705
|
-
`;
|
|
2829
|
+
parts.push(`Methods:${req.analysis.methods.join(",")}`);
|
|
2706
2830
|
}
|
|
2707
2831
|
if (req.analysis.computed?.length) {
|
|
2708
|
-
|
|
2709
|
-
`;
|
|
2832
|
+
parts.push(`Computed:${req.analysis.computed.join(",")}`);
|
|
2710
2833
|
}
|
|
2834
|
+
prompt += parts.join("\n") + "\n";
|
|
2711
2835
|
}
|
|
2712
2836
|
if (req.context) {
|
|
2713
2837
|
prompt += `
|
|
2714
|
-
\u6E90\u7801
|
|
2715
|
-
\`\`\`
|
|
2716
|
-
${req.context}
|
|
2838
|
+
\u6E90\u7801:
|
|
2839
|
+
\`\`\`
|
|
2840
|
+
${this.compressSourceCode(req.context)}
|
|
2717
2841
|
\`\`\`
|
|
2718
2842
|
`;
|
|
2719
2843
|
}
|
|
2720
2844
|
if (req.framework) {
|
|
2721
|
-
prompt +=
|
|
2722
|
-
\u6846\u67B6: ${req.framework}`;
|
|
2845
|
+
prompt += `\u6846\u67B6:${req.framework}`;
|
|
2723
2846
|
}
|
|
2724
2847
|
return prompt;
|
|
2725
2848
|
}
|
|
2849
|
+
/**
|
|
2850
|
+
* 根据测试类型和源文件路径,计算从测试文件到源文件的正确相对导入路径
|
|
2851
|
+
*/
|
|
2852
|
+
computeTestImportPath(testType, targetPath) {
|
|
2853
|
+
if (!targetPath) return targetPath;
|
|
2854
|
+
const testDirMap = {
|
|
2855
|
+
unit: "tests/unit",
|
|
2856
|
+
component: "tests/component",
|
|
2857
|
+
e2e: "tests/e2e",
|
|
2858
|
+
api: "tests/api",
|
|
2859
|
+
visual: "tests/visual",
|
|
2860
|
+
performance: "tests/e2e"
|
|
2861
|
+
};
|
|
2862
|
+
const testDir = testDirMap[testType];
|
|
2863
|
+
if (!testDir) return targetPath;
|
|
2864
|
+
if (targetPath.startsWith("../") || targetPath.startsWith("./../")) {
|
|
2865
|
+
return targetPath;
|
|
2866
|
+
}
|
|
2867
|
+
const depth = testDir.split("/").length;
|
|
2868
|
+
const prefix = "../".repeat(depth);
|
|
2869
|
+
let cleanPath = targetPath.replace(/^\.\//, "");
|
|
2870
|
+
if (!cleanPath.endsWith(".vue")) {
|
|
2871
|
+
cleanPath = cleanPath.replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
2872
|
+
}
|
|
2873
|
+
return `${prefix}${cleanPath}`;
|
|
2874
|
+
}
|
|
2726
2875
|
parseGenerateTestResponse(content) {
|
|
2727
2876
|
const codeBlockMatch = content.match(/```(?:typescript|ts|javascript|js)?\s*\n([\s\S]*?)```/);
|
|
2728
2877
|
const code = codeBlockMatch ? codeBlockMatch[1].trim() : content.replace(/^(?:```[\s\S]*?\n)?/, "").replace(/\n?```$/, "").trim();
|
|
@@ -2734,67 +2883,49 @@ ${req.context}
|
|
|
2734
2883
|
};
|
|
2735
2884
|
}
|
|
2736
2885
|
buildReviewTestSystemPrompt(_req) {
|
|
2737
|
-
return `\u4F60\u662F\
|
|
2738
|
-
|
|
2739
|
-
\
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
\u8F93\u51FA\u683C\u5F0F\uFF08\u4E25\u683C\u9075\u5B88\uFF09\uFF1A
|
|
2747
|
-
APPROVED: true \u6216 false
|
|
2748
|
-
SCORE: 0.0 \u5230 1.0 \u4E4B\u95F4\u7684\u8BC4\u5206
|
|
2749
|
-
FEEDBACK: \u4E00\u53E5\u8BDD\u5BA1\u8BA1\u610F\u89C1
|
|
2750
|
-
ISSUES: \u95EE\u9898\u5217\u8868\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF0C\u683C\u5F0F "- \u95EE\u9898\u63CF\u8FF0"\uFF09
|
|
2751
|
-
SUGGESTIONS: \u6539\u8FDB\u5EFA\u8BAE\u5217\u8868\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF0C\u683C\u5F0F "- \u5EFA\u8BAE\u63CF\u8FF0"\uFF09`;
|
|
2886
|
+
return `\u4F60\u662F\u6D4B\u8BD5\u5BA1\u8BA1\u4E13\u5BB6\u3002\u5BA1\u67E5\u6D4B\u8BD5\u4EE3\u7801\u662F\u5426\u51C6\u786E\u3002
|
|
2887
|
+
\u6807\u51C6:1.\u7C7B\u578B\u5339\u914D 2.\u8986\u76D6\u6838\u5FC3\u5BFC\u51FA/props/emits 3.\u65AD\u8A00\u6709\u6548(\u975E\u6C38\u771F) 4.import\u6B63\u786E 5.\u53EF\u8FD0\u884C
|
|
2888
|
+
\u8F93\u51FA:
|
|
2889
|
+
APPROVED:true\u6216false
|
|
2890
|
+
SCORE:0.0-1.0
|
|
2891
|
+
FEEDBACK:\u4E00\u53E5\u8BDD
|
|
2892
|
+
ISSUES:- \u95EE\u9898(\u6BCF\u884C\u4E00\u4E2A)
|
|
2893
|
+
SUGGESTIONS:- \u5EFA\u8BAE(\u6BCF\u884C\u4E00\u4E2A)`;
|
|
2752
2894
|
}
|
|
2753
2895
|
buildReviewTestUserPrompt(req) {
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2896
|
+
const importPath = this.computeTestImportPath(req.testType, req.target);
|
|
2897
|
+
let prompt = `\u5BA1\u67E5\u6D4B\u8BD5\u4EE3\u7801\u3002\u88AB\u6D4B:${req.target} \u7C7B\u578B:${req.testType} import\u8DEF\u5F84:${importPath}
|
|
2898
|
+
`;
|
|
2899
|
+
prompt += `
|
|
2900
|
+
\u6E90\u7801:
|
|
2901
|
+
\`\`\`
|
|
2902
|
+
${this.compressSourceCode(req.sourceCode, 2e3)}
|
|
2903
|
+
\`\`\`
|
|
2904
|
+
`;
|
|
2905
|
+
prompt += `
|
|
2906
|
+
\u6D4B\u8BD5\u4EE3\u7801:
|
|
2762
2907
|
\`\`\`
|
|
2763
|
-
|
|
2764
|
-
--- \u751F\u6210\u7684\u6D4B\u8BD5\u4EE3\u7801 ---
|
|
2765
|
-
\`\`\`typescript
|
|
2766
2908
|
${req.testCode}
|
|
2767
2909
|
\`\`\``;
|
|
2768
2910
|
if (req.analysis) {
|
|
2769
|
-
|
|
2911
|
+
const parts = [];
|
|
2770
2912
|
if (req.analysis.exports?.length > 0) {
|
|
2771
|
-
|
|
2772
|
-
\u5BFC\u51FA\u9879:
|
|
2773
|
-
${req.analysis.exports.map((e) => {
|
|
2774
|
-
const params = e.params?.length ? `(${e.params.join(", ")})` : "";
|
|
2775
|
-
const asyncFlag = e.isAsync ? "async " : "";
|
|
2776
|
-
return ` - ${asyncFlag}${e.name}${params} [${e.kind}]`;
|
|
2777
|
-
}).join("\n")}`;
|
|
2913
|
+
parts.push(`\u5BFC\u51FA:${req.analysis.exports.map((e) => `${e.isAsync ? "async " : ""}${e.name}(${e.params?.join(",") || ""})[${e.kind}]`).join(",")}`);
|
|
2778
2914
|
}
|
|
2779
2915
|
if (req.analysis.props?.length) {
|
|
2780
|
-
|
|
2781
|
-
Props:
|
|
2782
|
-
${req.analysis.props.map(
|
|
2783
|
-
(p) => ` - ${p.name}: ${p.type}${p.required ? " (\u5FC5\u586B)" : " (\u53EF\u9009)"}`
|
|
2784
|
-
).join("\n")}`;
|
|
2916
|
+
parts.push(`Props:${req.analysis.props.map((p) => `${p.name}:${p.type}${p.required ? "!" : "?"}`).join(",")}`);
|
|
2785
2917
|
}
|
|
2786
2918
|
if (req.analysis.emits?.length) {
|
|
2919
|
+
parts.push(`Emits:${req.analysis.emits.map((e) => `${e.name}(${e.params?.join(",") || ""})`).join(",")}`);
|
|
2920
|
+
}
|
|
2921
|
+
if (parts.length > 0) {
|
|
2787
2922
|
prompt += `
|
|
2788
|
-
|
|
2789
|
-
${req.analysis.emits.map(
|
|
2790
|
-
(e) => ` - ${e.name}${e.params?.length ? `(${e.params.join(", ")})` : ""}`
|
|
2791
|
-
).join("\n")}`;
|
|
2923
|
+
\u5206\u6790:${parts.join("|")}`;
|
|
2792
2924
|
}
|
|
2793
2925
|
}
|
|
2794
2926
|
if (req.generationDescription) {
|
|
2795
2927
|
prompt += `
|
|
2796
|
-
|
|
2797
|
-
\u751F\u6210\u8005\u8BF4\u660E: ${req.generationDescription}`;
|
|
2928
|
+
\u8BF4\u660E:${req.generationDescription}`;
|
|
2798
2929
|
}
|
|
2799
2930
|
return prompt;
|
|
2800
2931
|
}
|
|
@@ -2854,6 +2985,61 @@ ${req.analysis.emits.map(
|
|
|
2854
2985
|
return urlMap[provider] || "https://api.openai.com/v1";
|
|
2855
2986
|
}
|
|
2856
2987
|
};
|
|
2988
|
+
function compressFunctionBodies(code, maxLength) {
|
|
2989
|
+
const lines = code.split("\n");
|
|
2990
|
+
const result = [];
|
|
2991
|
+
let depth = 0;
|
|
2992
|
+
let fnDepth = 0;
|
|
2993
|
+
let inFunction = false;
|
|
2994
|
+
let braceBalance = 0;
|
|
2995
|
+
let capturedLines = 0;
|
|
2996
|
+
for (const line of lines) {
|
|
2997
|
+
const trimmed = line.trim();
|
|
2998
|
+
for (const ch of trimmed) {
|
|
2999
|
+
if (ch === "{") {
|
|
3000
|
+
braceBalance++;
|
|
3001
|
+
depth++;
|
|
3002
|
+
}
|
|
3003
|
+
if (ch === "}") {
|
|
3004
|
+
braceBalance--;
|
|
3005
|
+
depth = Math.max(0, depth - 1);
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
const isFnSignature = /^(export\s+)?(async\s+)?function\s|=>\s*\{|=>\s*$/m.test(trimmed) || /^(const|let|var)\s+\w+\s*=\s*(async\s+)?(\([^)]*\)|[^=])\s*=>/.test(trimmed);
|
|
3009
|
+
if (isFnSignature && !inFunction) {
|
|
3010
|
+
inFunction = true;
|
|
3011
|
+
fnDepth = depth;
|
|
3012
|
+
result.push(line);
|
|
3013
|
+
capturedLines++;
|
|
3014
|
+
continue;
|
|
3015
|
+
}
|
|
3016
|
+
if (inFunction) {
|
|
3017
|
+
const isReturn = trimmed.startsWith("return ");
|
|
3018
|
+
const isBranch = /^(if|else|switch|case|try|catch|finally|for|while)/.test(trimmed);
|
|
3019
|
+
const isClosing = trimmed === "}" && depth <= fnDepth - 1;
|
|
3020
|
+
if (isClosing) {
|
|
3021
|
+
result.push(line);
|
|
3022
|
+
inFunction = false;
|
|
3023
|
+
capturedLines++;
|
|
3024
|
+
} else if (isReturn || isBranch) {
|
|
3025
|
+
result.push(trimmed.length > 120 ? `${trimmed.slice(0, 120)}...` : line);
|
|
3026
|
+
capturedLines++;
|
|
3027
|
+
} else if (trimmed.startsWith("//") || trimmed === "") {
|
|
3028
|
+
} else if (capturedLines < maxLength / 30) {
|
|
3029
|
+
result.push(trimmed.length > 100 ? `${trimmed.slice(0, 100)}...` : line);
|
|
3030
|
+
capturedLines++;
|
|
3031
|
+
}
|
|
3032
|
+
} else {
|
|
3033
|
+
result.push(line);
|
|
3034
|
+
capturedLines++;
|
|
3035
|
+
}
|
|
3036
|
+
if (result.join("\n").length > maxLength) {
|
|
3037
|
+
result.push("// ... (truncated)");
|
|
3038
|
+
break;
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
return result.join("\n");
|
|
3042
|
+
}
|
|
2857
3043
|
|
|
2858
3044
|
// src/ai/provider.ts
|
|
2859
3045
|
var providerRegistry = /* @__PURE__ */ new Map();
|