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.cjs
CHANGED
|
@@ -1047,12 +1047,36 @@ import_handlebars.default.registerHelper("propTestValue", (prop) => {
|
|
|
1047
1047
|
};
|
|
1048
1048
|
return map[prop.type] || "'test-value'";
|
|
1049
1049
|
});
|
|
1050
|
+
function resolveImportPath(testType, targetPath) {
|
|
1051
|
+
if (!targetPath) return targetPath;
|
|
1052
|
+
const testDirMap = {
|
|
1053
|
+
unit: "tests/unit",
|
|
1054
|
+
component: "tests/component",
|
|
1055
|
+
e2e: "tests/e2e",
|
|
1056
|
+
api: "tests/api",
|
|
1057
|
+
visual: "tests/visual",
|
|
1058
|
+
performance: "tests/e2e"
|
|
1059
|
+
};
|
|
1060
|
+
const testDir = testDirMap[testType];
|
|
1061
|
+
if (!testDir) return targetPath;
|
|
1062
|
+
if (targetPath.startsWith("../") || targetPath.startsWith("./../")) {
|
|
1063
|
+
return targetPath;
|
|
1064
|
+
}
|
|
1065
|
+
const depth = testDir.split("/").length;
|
|
1066
|
+
const prefix = "../".repeat(depth);
|
|
1067
|
+
let cleanPath = targetPath.replace(/^\.\//, "");
|
|
1068
|
+
if (!cleanPath.endsWith(".vue")) {
|
|
1069
|
+
cleanPath = cleanPath.replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
1070
|
+
}
|
|
1071
|
+
return `${prefix}${cleanPath}`;
|
|
1072
|
+
}
|
|
1050
1073
|
function registerTemplate(type, templateContent) {
|
|
1051
1074
|
customTemplates.set(type, templateContent);
|
|
1052
1075
|
}
|
|
1053
1076
|
function renderTemplate(type, context) {
|
|
1054
1077
|
const templateContent = loadTemplate(type);
|
|
1055
1078
|
const template = import_handlebars.default.compile(templateContent);
|
|
1079
|
+
const resolvedTarget = resolveImportPath(type, context.target);
|
|
1056
1080
|
const fullContext = {
|
|
1057
1081
|
vueVersion: 3,
|
|
1058
1082
|
typescript: true,
|
|
@@ -1073,6 +1097,8 @@ function renderTemplate(type, context) {
|
|
|
1073
1097
|
requiredProps: [],
|
|
1074
1098
|
optionalProps: [],
|
|
1075
1099
|
...context,
|
|
1100
|
+
// 使用计算后的正确路径
|
|
1101
|
+
target: resolvedTarget,
|
|
1076
1102
|
framework: context.framework || "vue",
|
|
1077
1103
|
camelName: context.camelName || toCamelCase(context.name),
|
|
1078
1104
|
pascalName: context.pascalName || toPascalCase(context.name)
|
|
@@ -1774,14 +1800,14 @@ function buildVitestArgs(options) {
|
|
|
1774
1800
|
if (options.files && options.files.length > 0) {
|
|
1775
1801
|
args.push(...options.files);
|
|
1776
1802
|
} else {
|
|
1777
|
-
const
|
|
1778
|
-
unit:
|
|
1779
|
-
component:
|
|
1780
|
-
api:
|
|
1803
|
+
const pathMap = {
|
|
1804
|
+
unit: "tests/unit/**/*.test.ts",
|
|
1805
|
+
component: "tests/component/**/*.test.ts",
|
|
1806
|
+
api: "tests/api/**/*.test.ts"
|
|
1781
1807
|
};
|
|
1782
|
-
const
|
|
1783
|
-
if (
|
|
1784
|
-
args.push(
|
|
1808
|
+
const testPattern = pathMap[options.type];
|
|
1809
|
+
if (testPattern) {
|
|
1810
|
+
args.push(testPattern);
|
|
1785
1811
|
}
|
|
1786
1812
|
}
|
|
1787
1813
|
if (options.coverage) {
|
|
@@ -2590,6 +2616,15 @@ var NoopAIProvider = class {
|
|
|
2590
2616
|
};
|
|
2591
2617
|
|
|
2592
2618
|
// src/ai/openai-provider.ts
|
|
2619
|
+
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
2620
|
+
function isDebug() {
|
|
2621
|
+
return process.env.QAT_DEBUG === "true";
|
|
2622
|
+
}
|
|
2623
|
+
function debugLog(tag, ...args) {
|
|
2624
|
+
if (!isDebug()) return;
|
|
2625
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
2626
|
+
console.error(import_chalk2.default.gray(`[DEBUG ${timestamp}] [${tag}]`), ...args);
|
|
2627
|
+
}
|
|
2593
2628
|
var OpenAICompatibleProvider = class {
|
|
2594
2629
|
constructor(config) {
|
|
2595
2630
|
this.capabilities = {
|
|
@@ -2603,25 +2638,20 @@ var OpenAICompatibleProvider = class {
|
|
|
2603
2638
|
this.baseUrl = config.baseUrl || this.getDefaultBaseUrl(config.provider);
|
|
2604
2639
|
}
|
|
2605
2640
|
async generateTest(req) {
|
|
2641
|
+
debugLog("GENERATE", `type=${req.type} target=${req.target}`);
|
|
2606
2642
|
const systemPrompt = this.buildGenerateTestSystemPrompt(req);
|
|
2607
2643
|
const userPrompt = this.buildGenerateTestUserPrompt(req);
|
|
2608
2644
|
const content = await this.chat(systemPrompt, userPrompt);
|
|
2609
|
-
|
|
2645
|
+
const result = this.parseGenerateTestResponse(content);
|
|
2646
|
+
debugLog("GENERATE", `done code=${result.code.length}chars confidence=${result.confidence}`);
|
|
2647
|
+
return result;
|
|
2610
2648
|
}
|
|
2611
2649
|
async analyzeResult(req) {
|
|
2612
|
-
const systemPrompt = `\
|
|
2613
|
-
\u8F93\u51FA\
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
const
|
|
2617
|
-
const failed = r.suites.flatMap((s) => s.tests.filter((t) => t.status === "failed"));
|
|
2618
|
-
return `\u7C7B\u578B: ${r.type}, \u72B6\u6001: ${r.status}, \u8017\u65F6: ${r.duration}ms, \u5931\u8D25\u7528\u4F8B: ${failed.length}`;
|
|
2619
|
-
}).join("\n");
|
|
2620
|
-
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";
|
|
2621
|
-
const userPrompt = `\u6D4B\u8BD5\u7ED3\u679C:
|
|
2622
|
-
${resultSummary}
|
|
2623
|
-
|
|
2624
|
-
\u9519\u8BEF\u8BE6\u60C5:
|
|
2650
|
+
const systemPrompt = `\u6D4B\u8BD5\u5206\u6790\u4E13\u5BB6\u3002\u627E\u95EE\u9898\u6839\u56E0\uFF0C\u7ED9\u53EF\u64CD\u4F5C\u5EFA\u8BAE\u3002
|
|
2651
|
+
\u8F93\u51FA:1.\u6458\u8981(1-3\u53E5) 2.\u5EFA\u8BAE\u5217\u8868`;
|
|
2652
|
+
const failed = req.testResults.flatMap((r) => r.suites.flatMap((s) => s.tests.filter((t) => t.status === "failed")));
|
|
2653
|
+
const errorDetails = req.errorLogs?.join("\n") || failed.map((t) => `[${t.name}] ${t.error?.message}`).join("\n") || "\u65E0";
|
|
2654
|
+
const userPrompt = `\u5931\u8D25:${failed.length}
|
|
2625
2655
|
${errorDetails}`;
|
|
2626
2656
|
const content = await this.chat(systemPrompt, userPrompt);
|
|
2627
2657
|
return {
|
|
@@ -2631,23 +2661,22 @@ ${errorDetails}`;
|
|
|
2631
2661
|
};
|
|
2632
2662
|
}
|
|
2633
2663
|
async suggestFix(error) {
|
|
2634
|
-
const systemPrompt = `\
|
|
2635
|
-
\
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
const userPrompt = `\u9519\u8BEF\u4FE1\u606F: ${error.message}
|
|
2640
|
-
${error.stack ? `\u5806\u6808: ${error.stack}` : ""}
|
|
2641
|
-
${error.expected ? `\u671F\u671B\u503C: ${error.expected}` : ""}
|
|
2642
|
-
${error.actual ? `\u5B9E\u9645\u503C: ${error.actual}` : ""}`;
|
|
2664
|
+
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`;
|
|
2665
|
+
const userPrompt = `\u9519\u8BEF:${error.message}${error.stack ? `
|
|
2666
|
+
\u5806\u6808:${error.stack}` : ""}${error.expected ? `
|
|
2667
|
+
\u671F\u671B:${error.expected}` : ""}${error.actual ? `
|
|
2668
|
+
\u5B9E\u9645:${error.actual}` : ""}`;
|
|
2643
2669
|
const content = await this.chat(systemPrompt, userPrompt);
|
|
2644
2670
|
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);
|
|
2645
2671
|
}
|
|
2646
2672
|
async reviewTest(req) {
|
|
2673
|
+
debugLog("REVIEW", `target=${req.target} type=${req.testType}`);
|
|
2647
2674
|
const systemPrompt = this.buildReviewTestSystemPrompt(req);
|
|
2648
2675
|
const userPrompt = this.buildReviewTestUserPrompt(req);
|
|
2649
2676
|
const content = await this.chat(systemPrompt, userPrompt);
|
|
2650
|
-
|
|
2677
|
+
const result = this.parseReviewTestResponse(content);
|
|
2678
|
+
debugLog("REVIEW", `approved=${result.approved} score=${result.score} feedback=${result.feedback}`);
|
|
2679
|
+
return result;
|
|
2651
2680
|
}
|
|
2652
2681
|
// ─── 内部方法 ──────────────────────────────────────────────
|
|
2653
2682
|
/**
|
|
@@ -2711,8 +2740,9 @@ ${error.actual ? `\u5B9E\u9645\u503C: ${error.actual}` : ""}`;
|
|
|
2711
2740
|
return { ok: false, message: `\u8FDE\u63A5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`, latencyMs };
|
|
2712
2741
|
}
|
|
2713
2742
|
}
|
|
2714
|
-
async chat(systemPrompt, userPrompt) {
|
|
2743
|
+
async chat(systemPrompt, userPrompt, retries = 2) {
|
|
2715
2744
|
const url = `${this.baseUrl}/chat/completions`;
|
|
2745
|
+
const useStream = isDebug();
|
|
2716
2746
|
const body = {
|
|
2717
2747
|
model: this.model,
|
|
2718
2748
|
messages: [
|
|
@@ -2722,99 +2752,218 @@ ${error.actual ? `\u5B9E\u9645\u503C: ${error.actual}` : ""}`;
|
|
|
2722
2752
|
temperature: 0.3,
|
|
2723
2753
|
max_tokens: 4096
|
|
2724
2754
|
};
|
|
2755
|
+
if (useStream) {
|
|
2756
|
+
body.stream = true;
|
|
2757
|
+
}
|
|
2725
2758
|
const headers = {
|
|
2726
2759
|
"Content-Type": "application/json"
|
|
2727
2760
|
};
|
|
2728
2761
|
if (this.apiKey) {
|
|
2729
2762
|
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
2730
2763
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2764
|
+
debugLog("REQUEST", `POST ${url}`);
|
|
2765
|
+
debugLog("REQUEST", `model=${this.model} stream=${useStream}`);
|
|
2766
|
+
debugLog("SYSTEM", systemPrompt.length > 500 ? `${systemPrompt.slice(0, 500)}...` : systemPrompt);
|
|
2767
|
+
debugLog("USER", userPrompt.length > 1e3 ? `${userPrompt.slice(0, 1e3)}...` : userPrompt);
|
|
2768
|
+
let lastError = null;
|
|
2769
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
2770
|
+
try {
|
|
2771
|
+
if (attempt > 0) {
|
|
2772
|
+
debugLog("RETRY", `\u7B2C${attempt}\u6B21\u91CD\u8BD5...`);
|
|
2773
|
+
}
|
|
2774
|
+
const response = await fetch(url, {
|
|
2775
|
+
method: "POST",
|
|
2776
|
+
headers,
|
|
2777
|
+
body: JSON.stringify(body),
|
|
2778
|
+
signal: AbortSignal.timeout(12e4)
|
|
2779
|
+
// 120s timeout
|
|
2780
|
+
});
|
|
2781
|
+
if (!response.ok) {
|
|
2782
|
+
const text = await response.text().catch(() => "");
|
|
2783
|
+
if ((response.status === 429 || response.status >= 500) && attempt < retries) {
|
|
2784
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt), 8e3);
|
|
2785
|
+
debugLog("RETRY", `HTTP ${response.status}, ${delay}ms\u540E\u91CD\u8BD5`);
|
|
2786
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
2787
|
+
lastError = new Error(`AI API \u8BF7\u6C42\u5931\u8D25 (${response.status}): ${text.slice(0, 200)}`);
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
throw new Error(`AI API \u8BF7\u6C42\u5931\u8D25 (${response.status}): ${text.slice(0, 500)}`);
|
|
2791
|
+
}
|
|
2792
|
+
if (useStream && response.body) {
|
|
2793
|
+
const content = await this.readStream(response.body);
|
|
2794
|
+
debugLog("RESPONSE", content.length > 500 ? `${content.slice(0, 500)}...` : content);
|
|
2795
|
+
return content;
|
|
2796
|
+
}
|
|
2797
|
+
const data = await response.json();
|
|
2798
|
+
if (!data.choices?.[0]?.message?.content) {
|
|
2799
|
+
throw new Error("AI API \u8FD4\u56DE\u7A7A\u54CD\u5E94");
|
|
2800
|
+
}
|
|
2801
|
+
debugLog("RESPONSE", `tokens: prompt=${data.usage?.prompt_tokens} completion=${data.usage?.completion_tokens} total=${data.usage?.total_tokens}`);
|
|
2802
|
+
debugLog("RESPONSE", data.choices[0].message.content.length > 500 ? `${data.choices[0].message.content.slice(0, 500)}...` : data.choices[0].message.content);
|
|
2803
|
+
return data.choices[0].message.content;
|
|
2804
|
+
} catch (error) {
|
|
2805
|
+
if (error instanceof Error && error.name === "TimeoutError" && attempt < retries) {
|
|
2806
|
+
debugLog("TIMEOUT", `\u7B2C${attempt}\u6B21\u8D85\u65F6\uFF0C\u91CD\u8BD5\u4E2D...`);
|
|
2807
|
+
lastError = error;
|
|
2808
|
+
continue;
|
|
2809
|
+
}
|
|
2810
|
+
throw error;
|
|
2811
|
+
}
|
|
2741
2812
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2813
|
+
throw lastError || new Error("AI API \u8BF7\u6C42\u5931\u8D25");
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* 读取 SSE 流式响应,实时输出内容
|
|
2817
|
+
*/
|
|
2818
|
+
async readStream(body) {
|
|
2819
|
+
const chunks = [];
|
|
2820
|
+
const decoder = new TextDecoder();
|
|
2821
|
+
const reader = body.getReader();
|
|
2822
|
+
let buffer = "";
|
|
2823
|
+
let lineCount = 0;
|
|
2824
|
+
try {
|
|
2825
|
+
while (true) {
|
|
2826
|
+
const { done, value } = await reader.read();
|
|
2827
|
+
if (done) break;
|
|
2828
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2829
|
+
const lines = buffer.split("\n");
|
|
2830
|
+
buffer = lines.pop() || "";
|
|
2831
|
+
for (const line of lines) {
|
|
2832
|
+
const trimmed = line.trim();
|
|
2833
|
+
if (!trimmed || trimmed === "data: [DONE]") continue;
|
|
2834
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
2835
|
+
try {
|
|
2836
|
+
const json = JSON.parse(trimmed.slice(6));
|
|
2837
|
+
const delta = json.choices?.[0]?.delta?.content;
|
|
2838
|
+
if (delta) {
|
|
2839
|
+
chunks.push(delta);
|
|
2840
|
+
lineCount++;
|
|
2841
|
+
process.stderr.write(import_chalk2.default.gray(delta));
|
|
2842
|
+
if (lineCount % 20 === 0) {
|
|
2843
|
+
process.stderr.write("\n");
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
if (json.choices?.[0]?.finish_reason === "stop") {
|
|
2847
|
+
debugLog("STREAM", "\u5B8C\u6210");
|
|
2848
|
+
}
|
|
2849
|
+
} catch {
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
} finally {
|
|
2854
|
+
reader.releaseLock();
|
|
2855
|
+
}
|
|
2856
|
+
if (chunks.length > 0) {
|
|
2857
|
+
process.stderr.write("\n");
|
|
2858
|
+
}
|
|
2859
|
+
return chunks.join("");
|
|
2860
|
+
}
|
|
2861
|
+
/**
|
|
2862
|
+
* 压缩源码:保留签名和关键逻辑,剔除注释、空行、样式块
|
|
2863
|
+
* 目标:在保留准确性的前提下减少 token 消耗
|
|
2864
|
+
*/
|
|
2865
|
+
compressSourceCode(code, maxLength = 3e3) {
|
|
2866
|
+
let compressed = code;
|
|
2867
|
+
compressed = compressed.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
2868
|
+
compressed = compressed.replace(/<template[^>]*>([\s\S]*?)<\/template>/gi, (_match, content) => {
|
|
2869
|
+
return `<template>${content.replace(/\s*(?:class|style)\s*=\s*["'][^"']*["']/gi, "")}</template>`;
|
|
2870
|
+
});
|
|
2871
|
+
compressed = compressed.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
2872
|
+
compressed = compressed.replace(/(^|[^:])(\/\/.*$)/gm, "$1");
|
|
2873
|
+
compressed = compressed.replace(/\n\s*\n\s*\n/g, "\n\n");
|
|
2874
|
+
compressed = compressed.split("\n").map((line) => line.trimEnd()).join("\n").trim();
|
|
2875
|
+
if (compressed.length > maxLength) {
|
|
2876
|
+
const scriptMatch = compressed.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
|
|
2877
|
+
const templateMatch = compressed.match(/<template[^>]*>([\s\S]*?)<\/template>/i);
|
|
2878
|
+
if (scriptMatch) {
|
|
2879
|
+
let scriptPart = scriptMatch[1].trim();
|
|
2880
|
+
scriptPart = compressFunctionBodies(scriptPart, maxLength * 0.7);
|
|
2881
|
+
const templatePart = templateMatch ? `<template>${templateMatch[1].replace(/\s+/g, " ").trim().slice(0, 300)}...</template>` : "";
|
|
2882
|
+
compressed = `${templatePart}
|
|
2883
|
+
<script${scriptMatch[0].match(/<script[^>]*>/)?.[0]?.slice(7) || ">"}>${scriptPart}</script>`;
|
|
2884
|
+
} else {
|
|
2885
|
+
compressed = compressFunctionBodies(compressed, maxLength);
|
|
2886
|
+
}
|
|
2745
2887
|
}
|
|
2746
|
-
return
|
|
2888
|
+
return compressed;
|
|
2747
2889
|
}
|
|
2748
2890
|
buildGenerateTestSystemPrompt(req) {
|
|
2749
2891
|
const typeMap = {
|
|
2750
|
-
unit: "\u5355\u5143\u6D4B\u8BD5
|
|
2751
|
-
component: "\u7EC4\u4EF6\u6D4B\u8BD5
|
|
2752
|
-
e2e: "E2E\
|
|
2753
|
-
api: "API\
|
|
2754
|
-
visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5
|
|
2755
|
-
performance: "\u6027\u80FD\u6D4B\u8BD5
|
|
2892
|
+
unit: "\u5355\u5143\u6D4B\u8BD5(Vitest)",
|
|
2893
|
+
component: "\u7EC4\u4EF6\u6D4B\u8BD5(Vitest+@vue/test-utils)",
|
|
2894
|
+
e2e: "E2E\u6D4B\u8BD5(Playwright)",
|
|
2895
|
+
api: "API\u6D4B\u8BD5(Vitest+fetch)",
|
|
2896
|
+
visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5(Playwright)",
|
|
2897
|
+
performance: "\u6027\u80FD\u6D4B\u8BD5(Playwright)"
|
|
2756
2898
|
};
|
|
2757
|
-
return `\u4F60\u662F\
|
|
2758
|
-
\
|
|
2759
|
-
1. \u53EA\u8F93\u51FA\u6D4B\u8BD5\u4EE3\u7801\uFF0C\u4E0D\u8981\u591A\u4F59\u7684\u89E3\u91CA
|
|
2760
|
-
2. \u4EE3\u7801\u5FC5\u987B\u53EF\u76F4\u63A5\u8FD0\u884C\uFF0C\u5305\u542B\u6240\u6709\u5FC5\u8981\u7684 import
|
|
2761
|
-
3. \u6D4B\u8BD5\u7528\u4F8B\u8986\u76D6\uFF1A\u6B63\u5E38\u8DEF\u5F84\u3001\u8FB9\u754C\u6761\u4EF6\u3001\u9519\u8BEF\u5904\u7406
|
|
2762
|
-
4. \u4F7F\u7528\u4E2D\u6587\u63CF\u8FF0 it/test \u5757\u540D\u79F0
|
|
2763
|
-
5. Vue \u7EC4\u4EF6\u6D4B\u8BD5\u4F7F\u7528 @vue/test-utils \u7684 mount
|
|
2764
|
-
6. \u5982\u679C\u6709 props/emits \u4FE1\u606F\uFF0C\u52A1\u5FC5\u9488\u5BF9\u6BCF\u4E2A prop \u548C emit \u751F\u6210\u6D4B\u8BD5`;
|
|
2899
|
+
return `\u4F60\u662F\u524D\u7AEF\u6D4B\u8BD5\u5DE5\u7A0B\u5E08\uFF0C\u7F16\u5199${typeMap[req.type] || req.type}\u3002
|
|
2900
|
+
\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`;
|
|
2765
2901
|
}
|
|
2766
2902
|
buildGenerateTestUserPrompt(req) {
|
|
2767
|
-
|
|
2768
|
-
|
|
2903
|
+
const importPath = this.computeTestImportPath(req.type, req.target);
|
|
2904
|
+
let prompt = `\u4E3A${req.target}\u751F\u6210${req.type}\u6D4B\u8BD5\u3002import\u8DEF\u5F84:${importPath}
|
|
2769
2905
|
`;
|
|
2770
2906
|
if (req.analysis) {
|
|
2771
|
-
|
|
2907
|
+
const parts = [];
|
|
2772
2908
|
if (req.analysis.exports?.length > 0) {
|
|
2773
|
-
|
|
2774
|
-
${
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
return ` - ${asyncFlag}${e.name}${params} [${e.kind}]`;
|
|
2778
|
-
}).join("\n")}
|
|
2779
|
-
`;
|
|
2909
|
+
parts.push(`\u5BFC\u51FA:${req.analysis.exports.map((e) => {
|
|
2910
|
+
const p = e.params?.length ? `(${e.params.join(",")})` : "";
|
|
2911
|
+
return `${e.isAsync ? "async " : ""}${e.name}${p}[${e.kind}]`;
|
|
2912
|
+
}).join(",")}`);
|
|
2780
2913
|
}
|
|
2781
2914
|
if (req.analysis.props?.length) {
|
|
2782
|
-
|
|
2783
|
-
${req.analysis.props.map(
|
|
2784
|
-
(p) => ` - ${p.name}: ${p.type}${p.required ? " (\u5FC5\u586B)" : " (\u53EF\u9009)"}`
|
|
2785
|
-
).join("\n")}
|
|
2786
|
-
`;
|
|
2915
|
+
parts.push(`Props:${req.analysis.props.map((p) => `${p.name}:${p.type}${p.required ? "!" : "?"}`).join(",")}`);
|
|
2787
2916
|
}
|
|
2788
2917
|
if (req.analysis.emits?.length) {
|
|
2789
|
-
|
|
2790
|
-
${req.analysis.emits.map(
|
|
2791
|
-
(e) => ` - ${e.name}${e.params?.length ? `(${e.params.join(", ")})` : ""}`
|
|
2792
|
-
).join("\n")}
|
|
2793
|
-
`;
|
|
2918
|
+
parts.push(`Emits:${req.analysis.emits.map((e) => `${e.name}(${e.params?.join(",") || ""})`).join(",")}`);
|
|
2794
2919
|
}
|
|
2795
2920
|
if (req.analysis.methods?.length) {
|
|
2796
|
-
|
|
2797
|
-
`;
|
|
2921
|
+
parts.push(`Methods:${req.analysis.methods.join(",")}`);
|
|
2798
2922
|
}
|
|
2799
2923
|
if (req.analysis.computed?.length) {
|
|
2800
|
-
|
|
2801
|
-
`;
|
|
2924
|
+
parts.push(`Computed:${req.analysis.computed.join(",")}`);
|
|
2802
2925
|
}
|
|
2926
|
+
prompt += parts.join("\n") + "\n";
|
|
2803
2927
|
}
|
|
2804
2928
|
if (req.context) {
|
|
2805
2929
|
prompt += `
|
|
2806
|
-
\u6E90\u7801
|
|
2807
|
-
\`\`\`
|
|
2808
|
-
${req.context}
|
|
2930
|
+
\u6E90\u7801:
|
|
2931
|
+
\`\`\`
|
|
2932
|
+
${this.compressSourceCode(req.context)}
|
|
2809
2933
|
\`\`\`
|
|
2810
2934
|
`;
|
|
2811
2935
|
}
|
|
2812
2936
|
if (req.framework) {
|
|
2813
|
-
prompt +=
|
|
2814
|
-
\u6846\u67B6: ${req.framework}`;
|
|
2937
|
+
prompt += `\u6846\u67B6:${req.framework}`;
|
|
2815
2938
|
}
|
|
2816
2939
|
return prompt;
|
|
2817
2940
|
}
|
|
2941
|
+
/**
|
|
2942
|
+
* 根据测试类型和源文件路径,计算从测试文件到源文件的正确相对导入路径
|
|
2943
|
+
*/
|
|
2944
|
+
computeTestImportPath(testType, targetPath) {
|
|
2945
|
+
if (!targetPath) return targetPath;
|
|
2946
|
+
const testDirMap = {
|
|
2947
|
+
unit: "tests/unit",
|
|
2948
|
+
component: "tests/component",
|
|
2949
|
+
e2e: "tests/e2e",
|
|
2950
|
+
api: "tests/api",
|
|
2951
|
+
visual: "tests/visual",
|
|
2952
|
+
performance: "tests/e2e"
|
|
2953
|
+
};
|
|
2954
|
+
const testDir = testDirMap[testType];
|
|
2955
|
+
if (!testDir) return targetPath;
|
|
2956
|
+
if (targetPath.startsWith("../") || targetPath.startsWith("./../")) {
|
|
2957
|
+
return targetPath;
|
|
2958
|
+
}
|
|
2959
|
+
const depth = testDir.split("/").length;
|
|
2960
|
+
const prefix = "../".repeat(depth);
|
|
2961
|
+
let cleanPath = targetPath.replace(/^\.\//, "");
|
|
2962
|
+
if (!cleanPath.endsWith(".vue")) {
|
|
2963
|
+
cleanPath = cleanPath.replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
2964
|
+
}
|
|
2965
|
+
return `${prefix}${cleanPath}`;
|
|
2966
|
+
}
|
|
2818
2967
|
parseGenerateTestResponse(content) {
|
|
2819
2968
|
const codeBlockMatch = content.match(/```(?:typescript|ts|javascript|js)?\s*\n([\s\S]*?)```/);
|
|
2820
2969
|
const code = codeBlockMatch ? codeBlockMatch[1].trim() : content.replace(/^(?:```[\s\S]*?\n)?/, "").replace(/\n?```$/, "").trim();
|
|
@@ -2826,67 +2975,49 @@ ${req.context}
|
|
|
2826
2975
|
};
|
|
2827
2976
|
}
|
|
2828
2977
|
buildReviewTestSystemPrompt(_req) {
|
|
2829
|
-
return `\u4F60\u662F\
|
|
2830
|
-
|
|
2831
|
-
\
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
\u8F93\u51FA\u683C\u5F0F\uFF08\u4E25\u683C\u9075\u5B88\uFF09\uFF1A
|
|
2839
|
-
APPROVED: true \u6216 false
|
|
2840
|
-
SCORE: 0.0 \u5230 1.0 \u4E4B\u95F4\u7684\u8BC4\u5206
|
|
2841
|
-
FEEDBACK: \u4E00\u53E5\u8BDD\u5BA1\u8BA1\u610F\u89C1
|
|
2842
|
-
ISSUES: \u95EE\u9898\u5217\u8868\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF0C\u683C\u5F0F "- \u95EE\u9898\u63CF\u8FF0"\uFF09
|
|
2843
|
-
SUGGESTIONS: \u6539\u8FDB\u5EFA\u8BAE\u5217\u8868\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF0C\u683C\u5F0F "- \u5EFA\u8BAE\u63CF\u8FF0"\uFF09`;
|
|
2978
|
+
return `\u4F60\u662F\u6D4B\u8BD5\u5BA1\u8BA1\u4E13\u5BB6\u3002\u5BA1\u67E5\u6D4B\u8BD5\u4EE3\u7801\u662F\u5426\u51C6\u786E\u3002
|
|
2979
|
+
\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
|
|
2980
|
+
\u8F93\u51FA:
|
|
2981
|
+
APPROVED:true\u6216false
|
|
2982
|
+
SCORE:0.0-1.0
|
|
2983
|
+
FEEDBACK:\u4E00\u53E5\u8BDD
|
|
2984
|
+
ISSUES:- \u95EE\u9898(\u6BCF\u884C\u4E00\u4E2A)
|
|
2985
|
+
SUGGESTIONS:- \u5EFA\u8BAE(\u6BCF\u884C\u4E00\u4E2A)`;
|
|
2844
2986
|
}
|
|
2845
2987
|
buildReviewTestUserPrompt(req) {
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2988
|
+
const importPath = this.computeTestImportPath(req.testType, req.target);
|
|
2989
|
+
let prompt = `\u5BA1\u67E5\u6D4B\u8BD5\u4EE3\u7801\u3002\u88AB\u6D4B:${req.target} \u7C7B\u578B:${req.testType} import\u8DEF\u5F84:${importPath}
|
|
2990
|
+
`;
|
|
2991
|
+
prompt += `
|
|
2992
|
+
\u6E90\u7801:
|
|
2993
|
+
\`\`\`
|
|
2994
|
+
${this.compressSourceCode(req.sourceCode, 2e3)}
|
|
2995
|
+
\`\`\`
|
|
2996
|
+
`;
|
|
2997
|
+
prompt += `
|
|
2998
|
+
\u6D4B\u8BD5\u4EE3\u7801:
|
|
2854
2999
|
\`\`\`
|
|
2855
|
-
|
|
2856
|
-
--- \u751F\u6210\u7684\u6D4B\u8BD5\u4EE3\u7801 ---
|
|
2857
|
-
\`\`\`typescript
|
|
2858
3000
|
${req.testCode}
|
|
2859
3001
|
\`\`\``;
|
|
2860
3002
|
if (req.analysis) {
|
|
2861
|
-
|
|
3003
|
+
const parts = [];
|
|
2862
3004
|
if (req.analysis.exports?.length > 0) {
|
|
2863
|
-
|
|
2864
|
-
\u5BFC\u51FA\u9879:
|
|
2865
|
-
${req.analysis.exports.map((e) => {
|
|
2866
|
-
const params = e.params?.length ? `(${e.params.join(", ")})` : "";
|
|
2867
|
-
const asyncFlag = e.isAsync ? "async " : "";
|
|
2868
|
-
return ` - ${asyncFlag}${e.name}${params} [${e.kind}]`;
|
|
2869
|
-
}).join("\n")}`;
|
|
3005
|
+
parts.push(`\u5BFC\u51FA:${req.analysis.exports.map((e) => `${e.isAsync ? "async " : ""}${e.name}(${e.params?.join(",") || ""})[${e.kind}]`).join(",")}`);
|
|
2870
3006
|
}
|
|
2871
3007
|
if (req.analysis.props?.length) {
|
|
2872
|
-
|
|
2873
|
-
Props:
|
|
2874
|
-
${req.analysis.props.map(
|
|
2875
|
-
(p) => ` - ${p.name}: ${p.type}${p.required ? " (\u5FC5\u586B)" : " (\u53EF\u9009)"}`
|
|
2876
|
-
).join("\n")}`;
|
|
3008
|
+
parts.push(`Props:${req.analysis.props.map((p) => `${p.name}:${p.type}${p.required ? "!" : "?"}`).join(",")}`);
|
|
2877
3009
|
}
|
|
2878
3010
|
if (req.analysis.emits?.length) {
|
|
3011
|
+
parts.push(`Emits:${req.analysis.emits.map((e) => `${e.name}(${e.params?.join(",") || ""})`).join(",")}`);
|
|
3012
|
+
}
|
|
3013
|
+
if (parts.length > 0) {
|
|
2879
3014
|
prompt += `
|
|
2880
|
-
|
|
2881
|
-
${req.analysis.emits.map(
|
|
2882
|
-
(e) => ` - ${e.name}${e.params?.length ? `(${e.params.join(", ")})` : ""}`
|
|
2883
|
-
).join("\n")}`;
|
|
3015
|
+
\u5206\u6790:${parts.join("|")}`;
|
|
2884
3016
|
}
|
|
2885
3017
|
}
|
|
2886
3018
|
if (req.generationDescription) {
|
|
2887
3019
|
prompt += `
|
|
2888
|
-
|
|
2889
|
-
\u751F\u6210\u8005\u8BF4\u660E: ${req.generationDescription}`;
|
|
3020
|
+
\u8BF4\u660E:${req.generationDescription}`;
|
|
2890
3021
|
}
|
|
2891
3022
|
return prompt;
|
|
2892
3023
|
}
|
|
@@ -2946,6 +3077,61 @@ ${req.analysis.emits.map(
|
|
|
2946
3077
|
return urlMap[provider] || "https://api.openai.com/v1";
|
|
2947
3078
|
}
|
|
2948
3079
|
};
|
|
3080
|
+
function compressFunctionBodies(code, maxLength) {
|
|
3081
|
+
const lines = code.split("\n");
|
|
3082
|
+
const result = [];
|
|
3083
|
+
let depth = 0;
|
|
3084
|
+
let fnDepth = 0;
|
|
3085
|
+
let inFunction = false;
|
|
3086
|
+
let braceBalance = 0;
|
|
3087
|
+
let capturedLines = 0;
|
|
3088
|
+
for (const line of lines) {
|
|
3089
|
+
const trimmed = line.trim();
|
|
3090
|
+
for (const ch of trimmed) {
|
|
3091
|
+
if (ch === "{") {
|
|
3092
|
+
braceBalance++;
|
|
3093
|
+
depth++;
|
|
3094
|
+
}
|
|
3095
|
+
if (ch === "}") {
|
|
3096
|
+
braceBalance--;
|
|
3097
|
+
depth = Math.max(0, depth - 1);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
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);
|
|
3101
|
+
if (isFnSignature && !inFunction) {
|
|
3102
|
+
inFunction = true;
|
|
3103
|
+
fnDepth = depth;
|
|
3104
|
+
result.push(line);
|
|
3105
|
+
capturedLines++;
|
|
3106
|
+
continue;
|
|
3107
|
+
}
|
|
3108
|
+
if (inFunction) {
|
|
3109
|
+
const isReturn = trimmed.startsWith("return ");
|
|
3110
|
+
const isBranch = /^(if|else|switch|case|try|catch|finally|for|while)/.test(trimmed);
|
|
3111
|
+
const isClosing = trimmed === "}" && depth <= fnDepth - 1;
|
|
3112
|
+
if (isClosing) {
|
|
3113
|
+
result.push(line);
|
|
3114
|
+
inFunction = false;
|
|
3115
|
+
capturedLines++;
|
|
3116
|
+
} else if (isReturn || isBranch) {
|
|
3117
|
+
result.push(trimmed.length > 120 ? `${trimmed.slice(0, 120)}...` : line);
|
|
3118
|
+
capturedLines++;
|
|
3119
|
+
} else if (trimmed.startsWith("//") || trimmed === "") {
|
|
3120
|
+
} else if (capturedLines < maxLength / 30) {
|
|
3121
|
+
result.push(trimmed.length > 100 ? `${trimmed.slice(0, 100)}...` : line);
|
|
3122
|
+
capturedLines++;
|
|
3123
|
+
}
|
|
3124
|
+
} else {
|
|
3125
|
+
result.push(line);
|
|
3126
|
+
capturedLines++;
|
|
3127
|
+
}
|
|
3128
|
+
if (result.join("\n").length > maxLength) {
|
|
3129
|
+
result.push("// ... (truncated)");
|
|
3130
|
+
break;
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
return result.join("\n");
|
|
3134
|
+
}
|
|
2949
3135
|
|
|
2950
3136
|
// src/ai/provider.ts
|
|
2951
3137
|
var providerRegistry = /* @__PURE__ */ new Map();
|