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/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 includeMap = {
1778
- unit: ["tests/unit"],
1779
- component: ["tests/component"],
1780
- api: ["tests/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 includes = includeMap[options.type];
1783
- if (includes) {
1784
- args.push("--include", includes.map((d) => `${d}/**/*.test.ts`).join(","));
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
- return this.parseGenerateTestResponse(content);
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 = `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u6D4B\u8BD5\u5206\u6790\u4E13\u5BB6\u3002\u5206\u6790\u6D4B\u8BD5\u8FD0\u884C\u7ED3\u679C\uFF0C\u627E\u51FA\u95EE\u9898\u6839\u56E0\uFF0C\u7ED9\u51FA\u5177\u4F53\u53EF\u64CD\u4F5C\u7684\u6539\u8FDB\u5EFA\u8BAE\u3002
2613
- \u8F93\u51FA\u683C\u5F0F\uFF1A
2614
- 1. \u5206\u6790\u6458\u8981\uFF081-3\u53E5\u8BDD\uFF09
2615
- 2. \u6539\u8FDB\u5EFA\u8BAE\u5217\u8868\uFF08\u6BCF\u6761\u5EFA\u8BAE\u5177\u4F53\u3001\u53EF\u64CD\u4F5C\uFF09`;
2616
- const resultSummary = req.testResults.map((r) => {
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 = `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u4EE3\u7801\u4FEE\u590D\u4E13\u5BB6\u3002\u6839\u636E\u6D4B\u8BD5\u9519\u8BEF\u4FE1\u606F\uFF0C\u7ED9\u51FA\u5177\u4F53\u7684\u4FEE\u590D\u5EFA\u8BAE\u3002
2635
- \u6BCF\u6761\u5EFA\u8BAE\u5E94\u8BE5\u5305\u542B\uFF1A
2636
- 1. \u95EE\u9898\u5B9A\u4F4D
2637
- 2. \u4FEE\u590D\u65B9\u6848
2638
- 3. \u793A\u4F8B\u4EE3\u7801\uFF08\u5982\u679C\u9002\u7528\uFF09`;
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
- return this.parseReviewTestResponse(content);
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
- const response = await fetch(url, {
2732
- method: "POST",
2733
- headers,
2734
- body: JSON.stringify(body),
2735
- signal: AbortSignal.timeout(6e4)
2736
- // 60s timeout
2737
- });
2738
- if (!response.ok) {
2739
- const text = await response.text().catch(() => "");
2740
- throw new Error(`AI API \u8BF7\u6C42\u5931\u8D25 (${response.status}): ${text.slice(0, 500)}`);
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
- const data = await response.json();
2743
- if (!data.choices?.[0]?.message?.content) {
2744
- throw new Error("AI API \u8FD4\u56DE\u7A7A\u54CD\u5E94");
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 data.choices[0].message.content;
2888
+ return compressed;
2747
2889
  }
2748
2890
  buildGenerateTestSystemPrompt(req) {
2749
2891
  const typeMap = {
2750
- unit: "\u5355\u5143\u6D4B\u8BD5\uFF08Vitest + @vue/test-utils\uFF09",
2751
- component: "\u7EC4\u4EF6\u6D4B\u8BD5\uFF08Vitest + @vue/test-utils + mount\uFF09",
2752
- e2e: "E2E\u7AEF\u5230\u7AEF\u6D4B\u8BD5\uFF08Playwright\uFF09",
2753
- api: "API\u63A5\u53E3\u6D4B\u8BD5\uFF08Vitest + fetch\uFF09",
2754
- visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5\uFF08Playwright screenshot\uFF09",
2755
- performance: "\u6027\u80FD\u6D4B\u8BD5\uFF08Playwright + performance metrics\uFF09"
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\u4E00\u4E2A\u4E13\u4E1A\u7684\u524D\u7AEF\u6D4B\u8BD5\u5DE5\u7A0B\u5E08\uFF0C\u64C5\u957F\u7F16\u5199\u9AD8\u8D28\u91CF\u7684${typeMap[req.type] || req.type}\u3002
2758
- \u8981\u6C42\uFF1A
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
- let prompt = `\u8BF7\u4E3A\u4EE5\u4E0B\u6587\u4EF6\u751F\u6210${req.type}\u6D4B\u8BD5\u4EE3\u7801:
2768
- \u76EE\u6807\u6587\u4EF6: ${req.target}
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
- prompt += "\n\u6E90\u7801\u5206\u6790\u7ED3\u679C:\n";
2907
+ const parts = [];
2772
2908
  if (req.analysis.exports?.length > 0) {
2773
- prompt += `\u5BFC\u51FA\u9879:
2774
- ${req.analysis.exports.map((e) => {
2775
- const params = e.params?.length ? `(${e.params.join(", ")})` : "";
2776
- const asyncFlag = e.isAsync ? "async " : "";
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
- prompt += `Props:
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
- prompt += `Emits:
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
- prompt += `Methods: ${req.analysis.methods.join(", ")}
2797
- `;
2921
+ parts.push(`Methods:${req.analysis.methods.join(",")}`);
2798
2922
  }
2799
2923
  if (req.analysis.computed?.length) {
2800
- prompt += `Computed: ${req.analysis.computed.join(", ")}
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\u5185\u5BB9:
2807
- \`\`\`typescript
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\u4E00\u4F4D\u4E25\u8C28\u7684\u6D4B\u8BD5\u5BA1\u8BA1\u4E13\u5BB6\u3002\u4F60\u7684\u804C\u8D23\u662F\u5BA1\u67E5 AI \u751F\u6210\u7684\u6D4B\u8BD5\u7528\u4F8B\u662F\u5426\u4E0E\u6E90\u7801\u8D34\u5207\u4E14\u51C6\u786E\u3002
2830
-
2831
- \u5BA1\u67E5\u6807\u51C6\uFF1A
2832
- 1. **\u6D4B\u8BD5\u7C7B\u578B\u5339\u914D**\uFF1A\u6D4B\u8BD5\u7C7B\u578B\u662F\u5426\u4E0E\u6E90\u7801\u6587\u4EF6\u6027\u8D28\u5339\u914D\uFF08\u5982 .vue \u5E94\u4E3A\u7EC4\u4EF6\u6D4B\u8BD5\uFF0Cutils \u5E94\u4E3A\u5355\u5143\u6D4B\u8BD5\uFF09
2833
- 2. **\u8986\u76D6\u5B8C\u6574\u6027**\uFF1A\u662F\u5426\u8986\u76D6\u4E86\u6E90\u7801\u4E2D\u7684\u6838\u5FC3\u5BFC\u51FA\u9879\uFF08\u51FD\u6570\u3001\u7EC4\u4EF6\u7684 props/emits/methods\uFF09
2834
- 3. **\u65AD\u8A00\u6709\u6548\u6027**\uFF1A\u65AD\u8A00\u662F\u5426\u771F\u5B9E\u68C0\u9A8C\u4E86\u88AB\u6D4B\u884C\u4E3A\uFF0C\u800C\u975E\u7A7A\u65AD\u8A00\u6216\u6C38\u771F\u65AD\u8A00
2835
- 4. **\u5BFC\u5165\u6B63\u786E\u6027**\uFF1Aimport \u8DEF\u5F84\u548C\u6A21\u5757\u662F\u5426\u6B63\u786E
2836
- 5. **\u4EE3\u7801\u53EF\u8FD0\u884C\u6027**\uFF1A\u6D4B\u8BD5\u4EE3\u7801\u662F\u5426\u53EF\u76F4\u63A5\u8FD0\u884C\uFF0C\u65E0\u8BED\u6CD5\u9519\u8BEF
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
- let prompt = `\u8BF7\u5BA1\u67E5\u4EE5\u4E0B\u6D4B\u8BD5\u7528\u4F8B\u662F\u5426\u4E0E\u6E90\u7801\u8D34\u5207\u4E14\u51C6\u786E\u3002
2847
-
2848
- \u88AB\u6D4B\u6587\u4EF6: ${req.target}
2849
- \u6D4B\u8BD5\u7C7B\u578B: ${req.testType}
2850
-
2851
- --- \u6E90\u7801\u5185\u5BB9 ---
2852
- \`\`\`typescript
2853
- ${req.sourceCode}
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
- prompt += "\n\n--- \u6E90\u7801\u5206\u6790 ---";
3003
+ const parts = [];
2862
3004
  if (req.analysis.exports?.length > 0) {
2863
- prompt += `
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
- prompt += `
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
- Emits:
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();