qat-cli 0.3.3 → 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 +507 -387
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +259 -146
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +259 -146
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -736,16 +736,21 @@ declare class OpenAICompatibleProvider implements AIProvider {
|
|
|
736
736
|
latencyMs?: number;
|
|
737
737
|
}>;
|
|
738
738
|
private chat;
|
|
739
|
+
/**
|
|
740
|
+
* 读取 SSE 流式响应,实时输出内容
|
|
741
|
+
*/
|
|
742
|
+
private readStream;
|
|
743
|
+
/**
|
|
744
|
+
* 压缩源码:保留签名和关键逻辑,剔除注释、空行、样式块
|
|
745
|
+
* 目标:在保留准确性的前提下减少 token 消耗
|
|
746
|
+
*/
|
|
747
|
+
private compressSourceCode;
|
|
739
748
|
private buildGenerateTestSystemPrompt;
|
|
740
749
|
private buildGenerateTestUserPrompt;
|
|
741
750
|
/**
|
|
742
751
|
* 根据测试类型和源文件路径,计算从测试文件到源文件的正确相对导入路径
|
|
743
752
|
*/
|
|
744
753
|
private computeTestImportPath;
|
|
745
|
-
/**
|
|
746
|
-
* 获取测试输出目录
|
|
747
|
-
*/
|
|
748
|
-
private getTestOutputDir;
|
|
749
754
|
private parseGenerateTestResponse;
|
|
750
755
|
private buildReviewTestSystemPrompt;
|
|
751
756
|
private buildReviewTestUserPrompt;
|
package/dist/index.d.ts
CHANGED
|
@@ -736,16 +736,21 @@ declare class OpenAICompatibleProvider implements AIProvider {
|
|
|
736
736
|
latencyMs?: number;
|
|
737
737
|
}>;
|
|
738
738
|
private chat;
|
|
739
|
+
/**
|
|
740
|
+
* 读取 SSE 流式响应,实时输出内容
|
|
741
|
+
*/
|
|
742
|
+
private readStream;
|
|
743
|
+
/**
|
|
744
|
+
* 压缩源码:保留签名和关键逻辑,剔除注释、空行、样式块
|
|
745
|
+
* 目标:在保留准确性的前提下减少 token 消耗
|
|
746
|
+
*/
|
|
747
|
+
private compressSourceCode;
|
|
739
748
|
private buildGenerateTestSystemPrompt;
|
|
740
749
|
private buildGenerateTestUserPrompt;
|
|
741
750
|
/**
|
|
742
751
|
* 根据测试类型和源文件路径,计算从测试文件到源文件的正确相对导入路径
|
|
743
752
|
*/
|
|
744
753
|
private computeTestImportPath;
|
|
745
|
-
/**
|
|
746
|
-
* 获取测试输出目录
|
|
747
|
-
*/
|
|
748
|
-
private getTestOutputDir;
|
|
749
754
|
private parseGenerateTestResponse;
|
|
750
755
|
private buildReviewTestSystemPrompt;
|
|
751
756
|
private buildReviewTestUserPrompt;
|
package/dist/index.js
CHANGED
|
@@ -2524,6 +2524,15 @@ var NoopAIProvider = class {
|
|
|
2524
2524
|
};
|
|
2525
2525
|
|
|
2526
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
|
+
}
|
|
2527
2536
|
var OpenAICompatibleProvider = class {
|
|
2528
2537
|
constructor(config) {
|
|
2529
2538
|
this.capabilities = {
|
|
@@ -2537,25 +2546,20 @@ var OpenAICompatibleProvider = class {
|
|
|
2537
2546
|
this.baseUrl = config.baseUrl || this.getDefaultBaseUrl(config.provider);
|
|
2538
2547
|
}
|
|
2539
2548
|
async generateTest(req) {
|
|
2549
|
+
debugLog("GENERATE", `type=${req.type} target=${req.target}`);
|
|
2540
2550
|
const systemPrompt = this.buildGenerateTestSystemPrompt(req);
|
|
2541
2551
|
const userPrompt = this.buildGenerateTestUserPrompt(req);
|
|
2542
2552
|
const content = await this.chat(systemPrompt, userPrompt);
|
|
2543
|
-
|
|
2553
|
+
const result = this.parseGenerateTestResponse(content);
|
|
2554
|
+
debugLog("GENERATE", `done code=${result.code.length}chars confidence=${result.confidence}`);
|
|
2555
|
+
return result;
|
|
2544
2556
|
}
|
|
2545
2557
|
async analyzeResult(req) {
|
|
2546
|
-
const systemPrompt = `\
|
|
2547
|
-
\u8F93\u51FA\
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
const
|
|
2551
|
-
const failed = r.suites.flatMap((s) => s.tests.filter((t) => t.status === "failed"));
|
|
2552
|
-
return `\u7C7B\u578B: ${r.type}, \u72B6\u6001: ${r.status}, \u8017\u65F6: ${r.duration}ms, \u5931\u8D25\u7528\u4F8B: ${failed.length}`;
|
|
2553
|
-
}).join("\n");
|
|
2554
|
-
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";
|
|
2555
|
-
const userPrompt = `\u6D4B\u8BD5\u7ED3\u679C:
|
|
2556
|
-
${resultSummary}
|
|
2557
|
-
|
|
2558
|
-
\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}
|
|
2559
2563
|
${errorDetails}`;
|
|
2560
2564
|
const content = await this.chat(systemPrompt, userPrompt);
|
|
2561
2565
|
return {
|
|
@@ -2565,23 +2569,22 @@ ${errorDetails}`;
|
|
|
2565
2569
|
};
|
|
2566
2570
|
}
|
|
2567
2571
|
async suggestFix(error) {
|
|
2568
|
-
const systemPrompt = `\
|
|
2569
|
-
\
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
const userPrompt = `\u9519\u8BEF\u4FE1\u606F: ${error.message}
|
|
2574
|
-
${error.stack ? `\u5806\u6808: ${error.stack}` : ""}
|
|
2575
|
-
${error.expected ? `\u671F\u671B\u503C: ${error.expected}` : ""}
|
|
2576
|
-
${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}` : ""}`;
|
|
2577
2577
|
const content = await this.chat(systemPrompt, userPrompt);
|
|
2578
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);
|
|
2579
2579
|
}
|
|
2580
2580
|
async reviewTest(req) {
|
|
2581
|
+
debugLog("REVIEW", `target=${req.target} type=${req.testType}`);
|
|
2581
2582
|
const systemPrompt = this.buildReviewTestSystemPrompt(req);
|
|
2582
2583
|
const userPrompt = this.buildReviewTestUserPrompt(req);
|
|
2583
2584
|
const content = await this.chat(systemPrompt, userPrompt);
|
|
2584
|
-
|
|
2585
|
+
const result = this.parseReviewTestResponse(content);
|
|
2586
|
+
debugLog("REVIEW", `approved=${result.approved} score=${result.score} feedback=${result.feedback}`);
|
|
2587
|
+
return result;
|
|
2585
2588
|
}
|
|
2586
2589
|
// ─── 内部方法 ──────────────────────────────────────────────
|
|
2587
2590
|
/**
|
|
@@ -2645,8 +2648,9 @@ ${error.actual ? `\u5B9E\u9645\u503C: ${error.actual}` : ""}`;
|
|
|
2645
2648
|
return { ok: false, message: `\u8FDE\u63A5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`, latencyMs };
|
|
2646
2649
|
}
|
|
2647
2650
|
}
|
|
2648
|
-
async chat(systemPrompt, userPrompt) {
|
|
2651
|
+
async chat(systemPrompt, userPrompt, retries = 2) {
|
|
2649
2652
|
const url = `${this.baseUrl}/chat/completions`;
|
|
2653
|
+
const useStream = isDebug();
|
|
2650
2654
|
const body = {
|
|
2651
2655
|
model: this.model,
|
|
2652
2656
|
messages: [
|
|
@@ -2656,101 +2660,189 @@ ${error.actual ? `\u5B9E\u9645\u503C: ${error.actual}` : ""}`;
|
|
|
2656
2660
|
temperature: 0.3,
|
|
2657
2661
|
max_tokens: 4096
|
|
2658
2662
|
};
|
|
2663
|
+
if (useStream) {
|
|
2664
|
+
body.stream = true;
|
|
2665
|
+
}
|
|
2659
2666
|
const headers = {
|
|
2660
2667
|
"Content-Type": "application/json"
|
|
2661
2668
|
};
|
|
2662
2669
|
if (this.apiKey) {
|
|
2663
2670
|
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
2664
2671
|
}
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
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
|
+
}
|
|
2675
2720
|
}
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
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();
|
|
2679
2763
|
}
|
|
2680
|
-
|
|
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
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
return compressed;
|
|
2681
2797
|
}
|
|
2682
2798
|
buildGenerateTestSystemPrompt(req) {
|
|
2683
2799
|
const typeMap = {
|
|
2684
|
-
unit: "\u5355\u5143\u6D4B\u8BD5
|
|
2685
|
-
component: "\u7EC4\u4EF6\u6D4B\u8BD5
|
|
2686
|
-
e2e: "E2E\
|
|
2687
|
-
api: "API\
|
|
2688
|
-
visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5
|
|
2689
|
-
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)"
|
|
2690
2806
|
};
|
|
2691
|
-
return `\u4F60\u662F\
|
|
2692
|
-
\
|
|
2693
|
-
1. \u53EA\u8F93\u51FA\u6D4B\u8BD5\u4EE3\u7801\uFF0C\u4E0D\u8981\u591A\u4F59\u7684\u89E3\u91CA
|
|
2694
|
-
2. \u4EE3\u7801\u5FC5\u987B\u53EF\u76F4\u63A5\u8FD0\u884C\uFF0C\u5305\u542B\u6240\u6709\u5FC5\u8981\u7684 import
|
|
2695
|
-
3. \u6D4B\u8BD5\u7528\u4F8B\u8986\u76D6\uFF1A\u6B63\u5E38\u8DEF\u5F84\u3001\u8FB9\u754C\u6761\u4EF6\u3001\u9519\u8BEF\u5904\u7406
|
|
2696
|
-
4. \u4F7F\u7528\u4E2D\u6587\u63CF\u8FF0 it/test \u5757\u540D\u79F0
|
|
2697
|
-
5. Vue \u7EC4\u4EF6\u6D4B\u8BD5\u4F7F\u7528 @vue/test-utils \u7684 mount
|
|
2698
|
-
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`;
|
|
2699
2809
|
}
|
|
2700
2810
|
buildGenerateTestUserPrompt(req) {
|
|
2701
2811
|
const importPath = this.computeTestImportPath(req.type, req.target);
|
|
2702
|
-
let prompt = `\
|
|
2703
|
-
\u76EE\u6807\u6587\u4EF6: ${req.target}
|
|
2704
|
-
\u6D4B\u8BD5\u6587\u4EF6\u5C06\u653E\u5728: ${this.getTestOutputDir(req.type)}/
|
|
2705
|
-
\u6B63\u786E\u7684 import \u8DEF\u5F84: ${importPath}
|
|
2706
|
-
|
|
2707
|
-
\u91CD\u8981\uFF1Aimport \u8BED\u53E5\u4E2D\u5FC5\u987B\u4F7F\u7528\u4E0A\u8FF0\u6B63\u786E\u7684\u76F8\u5BF9\u8DEF\u5F84 ${importPath}\uFF0C\u4E0D\u8981\u4F7F\u7528 ${req.target} \u6216\u5176\u4ED6\u8DEF\u5F84\uFF01
|
|
2812
|
+
let prompt = `\u4E3A${req.target}\u751F\u6210${req.type}\u6D4B\u8BD5\u3002import\u8DEF\u5F84:${importPath}
|
|
2708
2813
|
`;
|
|
2709
2814
|
if (req.analysis) {
|
|
2710
|
-
|
|
2815
|
+
const parts = [];
|
|
2711
2816
|
if (req.analysis.exports?.length > 0) {
|
|
2712
|
-
|
|
2713
|
-
${
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
return ` - ${asyncFlag}${e.name}${params} [${e.kind}]`;
|
|
2717
|
-
}).join("\n")}
|
|
2718
|
-
`;
|
|
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(",")}`);
|
|
2719
2821
|
}
|
|
2720
2822
|
if (req.analysis.props?.length) {
|
|
2721
|
-
|
|
2722
|
-
${req.analysis.props.map(
|
|
2723
|
-
(p) => ` - ${p.name}: ${p.type}${p.required ? " (\u5FC5\u586B)" : " (\u53EF\u9009)"}`
|
|
2724
|
-
).join("\n")}
|
|
2725
|
-
`;
|
|
2823
|
+
parts.push(`Props:${req.analysis.props.map((p) => `${p.name}:${p.type}${p.required ? "!" : "?"}`).join(",")}`);
|
|
2726
2824
|
}
|
|
2727
2825
|
if (req.analysis.emits?.length) {
|
|
2728
|
-
|
|
2729
|
-
${req.analysis.emits.map(
|
|
2730
|
-
(e) => ` - ${e.name}${e.params?.length ? `(${e.params.join(", ")})` : ""}`
|
|
2731
|
-
).join("\n")}
|
|
2732
|
-
`;
|
|
2826
|
+
parts.push(`Emits:${req.analysis.emits.map((e) => `${e.name}(${e.params?.join(",") || ""})`).join(",")}`);
|
|
2733
2827
|
}
|
|
2734
2828
|
if (req.analysis.methods?.length) {
|
|
2735
|
-
|
|
2736
|
-
`;
|
|
2829
|
+
parts.push(`Methods:${req.analysis.methods.join(",")}`);
|
|
2737
2830
|
}
|
|
2738
2831
|
if (req.analysis.computed?.length) {
|
|
2739
|
-
|
|
2740
|
-
`;
|
|
2832
|
+
parts.push(`Computed:${req.analysis.computed.join(",")}`);
|
|
2741
2833
|
}
|
|
2834
|
+
prompt += parts.join("\n") + "\n";
|
|
2742
2835
|
}
|
|
2743
2836
|
if (req.context) {
|
|
2744
2837
|
prompt += `
|
|
2745
|
-
\u6E90\u7801
|
|
2746
|
-
\`\`\`
|
|
2747
|
-
${req.context}
|
|
2838
|
+
\u6E90\u7801:
|
|
2839
|
+
\`\`\`
|
|
2840
|
+
${this.compressSourceCode(req.context)}
|
|
2748
2841
|
\`\`\`
|
|
2749
2842
|
`;
|
|
2750
2843
|
}
|
|
2751
2844
|
if (req.framework) {
|
|
2752
|
-
prompt +=
|
|
2753
|
-
\u6846\u67B6: ${req.framework}`;
|
|
2845
|
+
prompt += `\u6846\u67B6:${req.framework}`;
|
|
2754
2846
|
}
|
|
2755
2847
|
return prompt;
|
|
2756
2848
|
}
|
|
@@ -2780,20 +2872,6 @@ ${req.context}
|
|
|
2780
2872
|
}
|
|
2781
2873
|
return `${prefix}${cleanPath}`;
|
|
2782
2874
|
}
|
|
2783
|
-
/**
|
|
2784
|
-
* 获取测试输出目录
|
|
2785
|
-
*/
|
|
2786
|
-
getTestOutputDir(testType) {
|
|
2787
|
-
const dirMap = {
|
|
2788
|
-
unit: "tests/unit",
|
|
2789
|
-
component: "tests/component",
|
|
2790
|
-
e2e: "tests/e2e",
|
|
2791
|
-
api: "tests/api",
|
|
2792
|
-
visual: "tests/visual",
|
|
2793
|
-
performance: "tests/e2e"
|
|
2794
|
-
};
|
|
2795
|
-
return dirMap[testType] || "tests/unit";
|
|
2796
|
-
}
|
|
2797
2875
|
parseGenerateTestResponse(content) {
|
|
2798
2876
|
const codeBlockMatch = content.match(/```(?:typescript|ts|javascript|js)?\s*\n([\s\S]*?)```/);
|
|
2799
2877
|
const code = codeBlockMatch ? codeBlockMatch[1].trim() : content.replace(/^(?:```[\s\S]*?\n)?/, "").replace(/\n?```$/, "").trim();
|
|
@@ -2805,69 +2883,49 @@ ${req.context}
|
|
|
2805
2883
|
};
|
|
2806
2884
|
}
|
|
2807
2885
|
buildReviewTestSystemPrompt(_req) {
|
|
2808
|
-
return `\u4F60\u662F\
|
|
2809
|
-
|
|
2810
|
-
\
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
\u8F93\u51FA\u683C\u5F0F\uFF08\u4E25\u683C\u9075\u5B88\uFF09\uFF1A
|
|
2818
|
-
APPROVED: true \u6216 false
|
|
2819
|
-
SCORE: 0.0 \u5230 1.0 \u4E4B\u95F4\u7684\u8BC4\u5206
|
|
2820
|
-
FEEDBACK: \u4E00\u53E5\u8BDD\u5BA1\u8BA1\u610F\u89C1
|
|
2821
|
-
ISSUES: \u95EE\u9898\u5217\u8868\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF0C\u683C\u5F0F "- \u95EE\u9898\u63CF\u8FF0"\uFF09
|
|
2822
|
-
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)`;
|
|
2823
2894
|
}
|
|
2824
2895
|
buildReviewTestUserPrompt(req) {
|
|
2825
2896
|
const importPath = this.computeTestImportPath(req.testType, req.target);
|
|
2826
|
-
let prompt = `\
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
\
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
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:
|
|
2835
2907
|
\`\`\`
|
|
2836
|
-
|
|
2837
|
-
--- \u751F\u6210\u7684\u6D4B\u8BD5\u4EE3\u7801 ---
|
|
2838
|
-
\`\`\`typescript
|
|
2839
2908
|
${req.testCode}
|
|
2840
2909
|
\`\`\``;
|
|
2841
2910
|
if (req.analysis) {
|
|
2842
|
-
|
|
2911
|
+
const parts = [];
|
|
2843
2912
|
if (req.analysis.exports?.length > 0) {
|
|
2844
|
-
|
|
2845
|
-
\u5BFC\u51FA\u9879:
|
|
2846
|
-
${req.analysis.exports.map((e) => {
|
|
2847
|
-
const params = e.params?.length ? `(${e.params.join(", ")})` : "";
|
|
2848
|
-
const asyncFlag = e.isAsync ? "async " : "";
|
|
2849
|
-
return ` - ${asyncFlag}${e.name}${params} [${e.kind}]`;
|
|
2850
|
-
}).join("\n")}`;
|
|
2913
|
+
parts.push(`\u5BFC\u51FA:${req.analysis.exports.map((e) => `${e.isAsync ? "async " : ""}${e.name}(${e.params?.join(",") || ""})[${e.kind}]`).join(",")}`);
|
|
2851
2914
|
}
|
|
2852
2915
|
if (req.analysis.props?.length) {
|
|
2853
|
-
|
|
2854
|
-
Props:
|
|
2855
|
-
${req.analysis.props.map(
|
|
2856
|
-
(p) => ` - ${p.name}: ${p.type}${p.required ? " (\u5FC5\u586B)" : " (\u53EF\u9009)"}`
|
|
2857
|
-
).join("\n")}`;
|
|
2916
|
+
parts.push(`Props:${req.analysis.props.map((p) => `${p.name}:${p.type}${p.required ? "!" : "?"}`).join(",")}`);
|
|
2858
2917
|
}
|
|
2859
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) {
|
|
2860
2922
|
prompt += `
|
|
2861
|
-
|
|
2862
|
-
${req.analysis.emits.map(
|
|
2863
|
-
(e) => ` - ${e.name}${e.params?.length ? `(${e.params.join(", ")})` : ""}`
|
|
2864
|
-
).join("\n")}`;
|
|
2923
|
+
\u5206\u6790:${parts.join("|")}`;
|
|
2865
2924
|
}
|
|
2866
2925
|
}
|
|
2867
2926
|
if (req.generationDescription) {
|
|
2868
2927
|
prompt += `
|
|
2869
|
-
|
|
2870
|
-
\u751F\u6210\u8005\u8BF4\u660E: ${req.generationDescription}`;
|
|
2928
|
+
\u8BF4\u660E:${req.generationDescription}`;
|
|
2871
2929
|
}
|
|
2872
2930
|
return prompt;
|
|
2873
2931
|
}
|
|
@@ -2927,6 +2985,61 @@ ${req.analysis.emits.map(
|
|
|
2927
2985
|
return urlMap[provider] || "https://api.openai.com/v1";
|
|
2928
2986
|
}
|
|
2929
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
|
+
}
|
|
2930
3043
|
|
|
2931
3044
|
// src/ai/provider.ts
|
|
2932
3045
|
var providerRegistry = /* @__PURE__ */ new Map();
|