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.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 includeMap = {
1686
- unit: ["tests/unit"],
1687
- component: ["tests/component"],
1688
- api: ["tests/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 includes = includeMap[options.type];
1691
- if (includes) {
1692
- args.push("--include", includes.map((d) => `${d}/**/*.test.ts`).join(","));
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
- return this.parseGenerateTestResponse(content);
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 = `\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
2521
- \u8F93\u51FA\u683C\u5F0F\uFF1A
2522
- 1. \u5206\u6790\u6458\u8981\uFF081-3\u53E5\u8BDD\uFF09
2523
- 2. \u6539\u8FDB\u5EFA\u8BAE\u5217\u8868\uFF08\u6BCF\u6761\u5EFA\u8BAE\u5177\u4F53\u3001\u53EF\u64CD\u4F5C\uFF09`;
2524
- const resultSummary = req.testResults.map((r) => {
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 = `\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
2543
- \u6BCF\u6761\u5EFA\u8BAE\u5E94\u8BE5\u5305\u542B\uFF1A
2544
- 1. \u95EE\u9898\u5B9A\u4F4D
2545
- 2. \u4FEE\u590D\u65B9\u6848
2546
- 3. \u793A\u4F8B\u4EE3\u7801\uFF08\u5982\u679C\u9002\u7528\uFF09`;
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
- return this.parseReviewTestResponse(content);
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
- const response = await fetch(url, {
2640
- method: "POST",
2641
- headers,
2642
- body: JSON.stringify(body),
2643
- signal: AbortSignal.timeout(6e4)
2644
- // 60s timeout
2645
- });
2646
- if (!response.ok) {
2647
- const text = await response.text().catch(() => "");
2648
- throw new Error(`AI API \u8BF7\u6C42\u5931\u8D25 (${response.status}): ${text.slice(0, 500)}`);
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
- const data = await response.json();
2651
- if (!data.choices?.[0]?.message?.content) {
2652
- throw new Error("AI API \u8FD4\u56DE\u7A7A\u54CD\u5E94");
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 data.choices[0].message.content;
2796
+ return compressed;
2655
2797
  }
2656
2798
  buildGenerateTestSystemPrompt(req) {
2657
2799
  const typeMap = {
2658
- unit: "\u5355\u5143\u6D4B\u8BD5\uFF08Vitest + @vue/test-utils\uFF09",
2659
- component: "\u7EC4\u4EF6\u6D4B\u8BD5\uFF08Vitest + @vue/test-utils + mount\uFF09",
2660
- e2e: "E2E\u7AEF\u5230\u7AEF\u6D4B\u8BD5\uFF08Playwright\uFF09",
2661
- api: "API\u63A5\u53E3\u6D4B\u8BD5\uFF08Vitest + fetch\uFF09",
2662
- visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5\uFF08Playwright screenshot\uFF09",
2663
- performance: "\u6027\u80FD\u6D4B\u8BD5\uFF08Playwright + performance metrics\uFF09"
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\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
2666
- \u8981\u6C42\uFF1A
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
- let prompt = `\u8BF7\u4E3A\u4EE5\u4E0B\u6587\u4EF6\u751F\u6210${req.type}\u6D4B\u8BD5\u4EE3\u7801:
2676
- \u76EE\u6807\u6587\u4EF6: ${req.target}
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
- prompt += "\n\u6E90\u7801\u5206\u6790\u7ED3\u679C:\n";
2815
+ const parts = [];
2680
2816
  if (req.analysis.exports?.length > 0) {
2681
- prompt += `\u5BFC\u51FA\u9879:
2682
- ${req.analysis.exports.map((e) => {
2683
- const params = e.params?.length ? `(${e.params.join(", ")})` : "";
2684
- const asyncFlag = e.isAsync ? "async " : "";
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
- prompt += `Props:
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
- prompt += `Emits:
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
- prompt += `Methods: ${req.analysis.methods.join(", ")}
2705
- `;
2829
+ parts.push(`Methods:${req.analysis.methods.join(",")}`);
2706
2830
  }
2707
2831
  if (req.analysis.computed?.length) {
2708
- prompt += `Computed: ${req.analysis.computed.join(", ")}
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\u5185\u5BB9:
2715
- \`\`\`typescript
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\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
2738
-
2739
- \u5BA1\u67E5\u6807\u51C6\uFF1A
2740
- 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
2741
- 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
2742
- 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
2743
- 4. **\u5BFC\u5165\u6B63\u786E\u6027**\uFF1Aimport \u8DEF\u5F84\u548C\u6A21\u5757\u662F\u5426\u6B63\u786E
2744
- 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
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
- let prompt = `\u8BF7\u5BA1\u67E5\u4EE5\u4E0B\u6D4B\u8BD5\u7528\u4F8B\u662F\u5426\u4E0E\u6E90\u7801\u8D34\u5207\u4E14\u51C6\u786E\u3002
2755
-
2756
- \u88AB\u6D4B\u6587\u4EF6: ${req.target}
2757
- \u6D4B\u8BD5\u7C7B\u578B: ${req.testType}
2758
-
2759
- --- \u6E90\u7801\u5185\u5BB9 ---
2760
- \`\`\`typescript
2761
- ${req.sourceCode}
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
- prompt += "\n\n--- \u6E90\u7801\u5206\u6790 ---";
2911
+ const parts = [];
2770
2912
  if (req.analysis.exports?.length > 0) {
2771
- prompt += `
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
- prompt += `
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
- Emits:
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();