qat-cli 0.3.3 → 0.3.5

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
@@ -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
- 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;
2544
2556
  }
2545
2557
  async analyzeResult(req) {
2546
- 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
2547
- \u8F93\u51FA\u683C\u5F0F\uFF1A
2548
- 1. \u5206\u6790\u6458\u8981\uFF081-3\u53E5\u8BDD\uFF09
2549
- 2. \u6539\u8FDB\u5EFA\u8BAE\u5217\u8868\uFF08\u6BCF\u6761\u5EFA\u8BAE\u5177\u4F53\u3001\u53EF\u64CD\u4F5C\uFF09`;
2550
- const resultSummary = req.testResults.map((r) => {
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 = `\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
2569
- \u6BCF\u6761\u5EFA\u8BAE\u5E94\u8BE5\u5305\u542B\uFF1A
2570
- 1. \u95EE\u9898\u5B9A\u4F4D
2571
- 2. \u4FEE\u590D\u65B9\u6848
2572
- 3. \u793A\u4F8B\u4EE3\u7801\uFF08\u5982\u679C\u9002\u7528\uFF09`;
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
- 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;
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,202 @@ ${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
- const response = await fetch(url, {
2666
- method: "POST",
2667
- headers,
2668
- body: JSON.stringify(body),
2669
- signal: AbortSignal.timeout(6e4)
2670
- // 60s timeout
2671
- });
2672
- if (!response.ok) {
2673
- const text = await response.text().catch(() => "");
2674
- 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
+ }
2675
2720
  }
2676
- const data = await response.json();
2677
- if (!data.choices?.[0]?.message?.content) {
2678
- 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
+ * @param code 源码内容
2773
+ * @param maxLength 最大长度
2774
+ * @param importPathRewrites import 路径重写映射(原路径→正确路径)
2775
+ */
2776
+ compressSourceCode(code, maxLength = 3e3, importPathRewrites) {
2777
+ let compressed = code;
2778
+ if (importPathRewrites && importPathRewrites.size > 0) {
2779
+ compressed = this.rewriteImportPaths(compressed, importPathRewrites);
2679
2780
  }
2680
- return data.choices[0].message.content;
2781
+ compressed = compressed.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
2782
+ compressed = compressed.replace(/<template[^>]*>([\s\S]*?)<\/template>/gi, (_match, content) => {
2783
+ return `<template>${content.replace(/\s*(?:class|style)\s*=\s*["'][^"']*["']/gi, "")}</template>`;
2784
+ });
2785
+ compressed = compressed.replace(/\/\*[\s\S]*?\*\//g, "");
2786
+ compressed = compressed.replace(/(^|[^:])(\/\/.*$)/gm, "$1");
2787
+ compressed = compressed.replace(/\n\s*\n\s*\n/g, "\n\n");
2788
+ compressed = compressed.split("\n").map((line) => line.trimEnd()).join("\n").trim();
2789
+ if (compressed.length > maxLength) {
2790
+ const scriptMatch = compressed.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
2791
+ const templateMatch = compressed.match(/<template[^>]*>([\s\S]*?)<\/template>/i);
2792
+ if (scriptMatch) {
2793
+ let scriptPart = scriptMatch[1].trim();
2794
+ scriptPart = compressFunctionBodies(scriptPart, maxLength * 0.7);
2795
+ const templatePart = templateMatch ? `<template>${templateMatch[1].replace(/\s+/g, " ").trim().slice(0, 300)}...</template>` : "";
2796
+ compressed = `${templatePart}
2797
+ <script${scriptMatch[0].match(/<script[^>]*>/)?.[0]?.slice(7) || ">"}>${scriptPart}</script>`;
2798
+ } else {
2799
+ compressed = compressFunctionBodies(compressed, maxLength);
2800
+ }
2801
+ }
2802
+ return compressed;
2681
2803
  }
2682
2804
  buildGenerateTestSystemPrompt(req) {
2683
2805
  const typeMap = {
2684
- unit: "\u5355\u5143\u6D4B\u8BD5\uFF08Vitest + @vue/test-utils\uFF09",
2685
- component: "\u7EC4\u4EF6\u6D4B\u8BD5\uFF08Vitest + @vue/test-utils + mount\uFF09",
2686
- e2e: "E2E\u7AEF\u5230\u7AEF\u6D4B\u8BD5\uFF08Playwright\uFF09",
2687
- api: "API\u63A5\u53E3\u6D4B\u8BD5\uFF08Vitest + fetch\uFF09",
2688
- visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5\uFF08Playwright screenshot\uFF09",
2689
- performance: "\u6027\u80FD\u6D4B\u8BD5\uFF08Playwright + performance metrics\uFF09"
2806
+ unit: "\u5355\u5143\u6D4B\u8BD5(Vitest)",
2807
+ component: "\u7EC4\u4EF6\u6D4B\u8BD5(Vitest+@vue/test-utils)",
2808
+ e2e: "E2E\u6D4B\u8BD5(Playwright)",
2809
+ api: "API\u6D4B\u8BD5(Vitest+fetch)",
2810
+ visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5(Playwright)",
2811
+ performance: "\u6027\u80FD\u6D4B\u8BD5(Playwright)"
2690
2812
  };
2691
- 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
2692
- \u8981\u6C42\uFF1A
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`;
2813
+ return `\u4F60\u662F\u524D\u7AEF\u6D4B\u8BD5\u5DE5\u7A0B\u5E08\uFF0C\u7F16\u5199${typeMap[req.type] || req.type}\u3002
2814
+ \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
2815
  }
2700
2816
  buildGenerateTestUserPrompt(req) {
2701
2817
  const importPath = this.computeTestImportPath(req.type, req.target);
2702
- let prompt = `\u8BF7\u4E3A\u4EE5\u4E0B\u6587\u4EF6\u751F\u6210${req.type}\u6D4B\u8BD5\u4EE3\u7801:
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
2818
+ let prompt = `\u4E3A${req.target}\u751F\u6210${req.type}\u6D4B\u8BD5\u3002
2819
+ \u3010\u5F3A\u5236\u3011import\u88AB\u6D4B\u6A21\u5757\u5FC5\u987B\u7528: ${importPath}
2708
2820
  `;
2709
2821
  if (req.analysis) {
2710
- prompt += "\n\u6E90\u7801\u5206\u6790\u7ED3\u679C:\n";
2822
+ const parts = [];
2711
2823
  if (req.analysis.exports?.length > 0) {
2712
- prompt += `\u5BFC\u51FA\u9879:
2713
- ${req.analysis.exports.map((e) => {
2714
- const params = e.params?.length ? `(${e.params.join(", ")})` : "";
2715
- const asyncFlag = e.isAsync ? "async " : "";
2716
- return ` - ${asyncFlag}${e.name}${params} [${e.kind}]`;
2717
- }).join("\n")}
2718
- `;
2824
+ parts.push(`\u5BFC\u51FA:${req.analysis.exports.map((e) => {
2825
+ const p = e.params?.length ? `(${e.params.join(",")})` : "";
2826
+ return `${e.isAsync ? "async " : ""}${e.name}${p}[${e.kind}]`;
2827
+ }).join(",")}`);
2719
2828
  }
2720
2829
  if (req.analysis.props?.length) {
2721
- prompt += `Props:
2722
- ${req.analysis.props.map(
2723
- (p) => ` - ${p.name}: ${p.type}${p.required ? " (\u5FC5\u586B)" : " (\u53EF\u9009)"}`
2724
- ).join("\n")}
2725
- `;
2830
+ parts.push(`Props:${req.analysis.props.map((p) => `${p.name}:${p.type}${p.required ? "!" : "?"}`).join(",")}`);
2726
2831
  }
2727
2832
  if (req.analysis.emits?.length) {
2728
- prompt += `Emits:
2729
- ${req.analysis.emits.map(
2730
- (e) => ` - ${e.name}${e.params?.length ? `(${e.params.join(", ")})` : ""}`
2731
- ).join("\n")}
2732
- `;
2833
+ parts.push(`Emits:${req.analysis.emits.map((e) => `${e.name}(${e.params?.join(",") || ""})`).join(",")}`);
2733
2834
  }
2734
2835
  if (req.analysis.methods?.length) {
2735
- prompt += `Methods: ${req.analysis.methods.join(", ")}
2736
- `;
2836
+ parts.push(`Methods:${req.analysis.methods.join(",")}`);
2737
2837
  }
2738
2838
  if (req.analysis.computed?.length) {
2739
- prompt += `Computed: ${req.analysis.computed.join(", ")}
2740
- `;
2839
+ parts.push(`Computed:${req.analysis.computed.join(",")}`);
2840
+ }
2841
+ if (req.analysis.importSignatures?.length) {
2842
+ parts.push(`\u4F9D\u8D56\u7B7E\u540D:${req.analysis.importSignatures.map(
2843
+ (imp) => `${imp.source}{${Object.entries(imp.signatures).map(([k, v]) => `${k}:${v}`).join(",")}}`
2844
+ ).join(";")}`);
2741
2845
  }
2846
+ prompt += parts.join("\n") + "\n";
2742
2847
  }
2743
2848
  if (req.context) {
2849
+ const importPathRewrites = this.buildImportPathRewrites(req.type, req.target);
2744
2850
  prompt += `
2745
- \u6E90\u7801\u5185\u5BB9:
2746
- \`\`\`typescript
2747
- ${req.context}
2851
+ \u6E90\u7801:
2852
+ \`\`\`
2853
+ ${this.compressSourceCode(req.context, 3e3, importPathRewrites)}
2748
2854
  \`\`\`
2749
2855
  `;
2750
2856
  }
2751
2857
  if (req.framework) {
2752
- prompt += `
2753
- \u6846\u67B6: ${req.framework}`;
2858
+ prompt += `\u6846\u67B6:${req.framework}`;
2754
2859
  }
2755
2860
  return prompt;
2756
2861
  }
@@ -2781,18 +2886,54 @@ ${req.context}
2781
2886
  return `${prefix}${cleanPath}`;
2782
2887
  }
2783
2888
  /**
2784
- * 获取测试输出目录
2889
+ * 构建 import 路径重写映射
2890
+ * 将源码中可能引用自身的各种写法,映射到测试文件中正确的导入路径
2785
2891
  */
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";
2892
+ buildImportPathRewrites(testType, targetPath) {
2893
+ const rewrites = /* @__PURE__ */ new Map();
2894
+ if (!targetPath) return rewrites;
2895
+ const correctImportPath = this.computeTestImportPath(testType, targetPath);
2896
+ const variations = /* @__PURE__ */ new Set();
2897
+ variations.add(targetPath);
2898
+ variations.add(targetPath.replace(/^\.\//, ""));
2899
+ const withoutExt = targetPath.replace(/\.(ts|js|tsx|jsx)$/, "");
2900
+ variations.add(withoutExt);
2901
+ const srcDirMatch = targetPath.match(/^(?:\.\/)?(src\/.+)$/);
2902
+ if (srcDirMatch) {
2903
+ variations.add(`@/${srcDirMatch[1]}`);
2904
+ variations.add(`@/${srcDirMatch[1].replace(/\.(ts|js|tsx|jsx)$/, "")}`);
2905
+ }
2906
+ const pathParts = targetPath.replace(/^\.\//, "").split("/");
2907
+ const fileName = pathParts[pathParts.length - 1];
2908
+ const fileNameNoExt = fileName.replace(/\.(ts|js|tsx|jsx|vue)$/, "");
2909
+ variations.add(`./${fileName}`);
2910
+ variations.add(`./${fileNameNoExt}`);
2911
+ for (let i = 1; i < pathParts.length; i++) {
2912
+ const relPath = "../".repeat(i) + pathParts.slice(i).join("/");
2913
+ variations.add(relPath);
2914
+ variations.add(relPath.replace(/\.(ts|js|tsx|jsx)$/, ""));
2915
+ }
2916
+ for (const variant of variations) {
2917
+ if (variant && variant !== correctImportPath) {
2918
+ rewrites.set(variant, correctImportPath);
2919
+ }
2920
+ }
2921
+ return rewrites;
2922
+ }
2923
+ /**
2924
+ * 重写源码中的 import 路径
2925
+ * 将 from 'oldPath' / from "oldPath" 替换为正确的路径
2926
+ */
2927
+ rewriteImportPaths(code, rewrites) {
2928
+ let result = code;
2929
+ for (const [oldPath, newPath] of rewrites) {
2930
+ const escaped = oldPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2931
+ const singleQuoteRegex = new RegExp(`(from\\s+')${escaped}(')`, "g");
2932
+ const doubleQuoteRegex = new RegExp(`(from\\s+")${escaped}(")`, "g");
2933
+ result = result.replace(singleQuoteRegex, `$1${newPath}$2`);
2934
+ result = result.replace(doubleQuoteRegex, `$1${newPath}$2`);
2935
+ }
2936
+ return result;
2796
2937
  }
2797
2938
  parseGenerateTestResponse(content) {
2798
2939
  const codeBlockMatch = content.match(/```(?:typescript|ts|javascript|js)?\s*\n([\s\S]*?)```/);
@@ -2805,69 +2946,56 @@ ${req.context}
2805
2946
  };
2806
2947
  }
2807
2948
  buildReviewTestSystemPrompt(_req) {
2808
- 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
2809
-
2810
- \u5BA1\u67E5\u6807\u51C6\uFF1A
2811
- 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
2812
- 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
2813
- 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
2814
- 4. **\u5BFC\u5165\u6B63\u786E\u6027**\uFF1Aimport \u8DEF\u5F84\u548C\u6A21\u5757\u662F\u5426\u6B63\u786E
2815
- 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
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`;
2949
+ return `\u4F60\u662F\u6D4B\u8BD5\u5BA1\u8BA1\u4E13\u5BB6\u3002\u5BA1\u67E5\u6D4B\u8BD5\u4EE3\u7801\u662F\u5426\u51C6\u786E\u3002
2950
+ \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
2951
+ \u8F93\u51FA:
2952
+ APPROVED:true\u6216false
2953
+ SCORE:0.0-1.0
2954
+ FEEDBACK:\u4E00\u53E5\u8BDD
2955
+ ISSUES:- \u95EE\u9898(\u6BCF\u884C\u4E00\u4E2A)
2956
+ SUGGESTIONS:- \u5EFA\u8BAE(\u6BCF\u884C\u4E00\u4E2A)`;
2823
2957
  }
2824
2958
  buildReviewTestUserPrompt(req) {
2825
2959
  const importPath = this.computeTestImportPath(req.testType, req.target);
2826
- let prompt = `\u8BF7\u5BA1\u67E5\u4EE5\u4E0B\u6D4B\u8BD5\u7528\u4F8B\u662F\u5426\u4E0E\u6E90\u7801\u8D34\u5207\u4E14\u51C6\u786E\u3002
2827
-
2828
- \u88AB\u6D4B\u6587\u4EF6: ${req.target}
2829
- \u6D4B\u8BD5\u7C7B\u578B: ${req.testType}
2830
- \u6B63\u786E\u7684 import \u8DEF\u5F84\u5E94\u4E3A: ${importPath}\uFF08\u6D4B\u8BD5\u6587\u4EF6\u4F4D\u4E8E ${this.getTestOutputDir(req.testType)}/\uFF09
2831
-
2832
- --- \u6E90\u7801\u5185\u5BB9 ---
2833
- \`\`\`typescript
2834
- ${req.sourceCode}
2960
+ let prompt = `\u5BA1\u67E5\u6D4B\u8BD5\u4EE3\u7801\u3002\u88AB\u6D4B:${req.target} \u7C7B\u578B:${req.testType}
2961
+ \u3010\u5F3A\u5236\u3011import\u88AB\u6D4B\u6A21\u5757\u5FC5\u987B\u7528: ${importPath}
2962
+ `;
2963
+ const importPathRewrites = this.buildImportPathRewrites(req.testType, req.target);
2964
+ prompt += `
2965
+ \u6E90\u7801:
2966
+ \`\`\`
2967
+ ${this.compressSourceCode(req.sourceCode, 2e3, importPathRewrites)}
2968
+ \`\`\`
2969
+ `;
2970
+ prompt += `
2971
+ \u6D4B\u8BD5\u4EE3\u7801:
2835
2972
  \`\`\`
2836
-
2837
- --- \u751F\u6210\u7684\u6D4B\u8BD5\u4EE3\u7801 ---
2838
- \`\`\`typescript
2839
2973
  ${req.testCode}
2840
2974
  \`\`\``;
2841
2975
  if (req.analysis) {
2842
- prompt += "\n\n--- \u6E90\u7801\u5206\u6790 ---";
2976
+ const parts = [];
2843
2977
  if (req.analysis.exports?.length > 0) {
2844
- prompt += `
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")}`;
2978
+ parts.push(`\u5BFC\u51FA:${req.analysis.exports.map((e) => `${e.isAsync ? "async " : ""}${e.name}(${e.params?.join(",") || ""})[${e.kind}]`).join(",")}`);
2851
2979
  }
2852
2980
  if (req.analysis.props?.length) {
2853
- prompt += `
2854
- Props:
2855
- ${req.analysis.props.map(
2856
- (p) => ` - ${p.name}: ${p.type}${p.required ? " (\u5FC5\u586B)" : " (\u53EF\u9009)"}`
2857
- ).join("\n")}`;
2981
+ parts.push(`Props:${req.analysis.props.map((p) => `${p.name}:${p.type}${p.required ? "!" : "?"}`).join(",")}`);
2858
2982
  }
2859
2983
  if (req.analysis.emits?.length) {
2984
+ parts.push(`Emits:${req.analysis.emits.map((e) => `${e.name}(${e.params?.join(",") || ""})`).join(",")}`);
2985
+ }
2986
+ if (req.analysis.importSignatures?.length) {
2987
+ parts.push(`\u4F9D\u8D56:${req.analysis.importSignatures.map(
2988
+ (imp) => `${imp.source}{${Object.entries(imp.signatures).map(([k, v]) => `${k}:${v}`).join(",")}}`
2989
+ ).join(";")}`);
2990
+ }
2991
+ if (parts.length > 0) {
2860
2992
  prompt += `
2861
- Emits:
2862
- ${req.analysis.emits.map(
2863
- (e) => ` - ${e.name}${e.params?.length ? `(${e.params.join(", ")})` : ""}`
2864
- ).join("\n")}`;
2993
+ \u5206\u6790:${parts.join("|")}`;
2865
2994
  }
2866
2995
  }
2867
2996
  if (req.generationDescription) {
2868
2997
  prompt += `
2869
-
2870
- \u751F\u6210\u8005\u8BF4\u660E: ${req.generationDescription}`;
2998
+ \u8BF4\u660E:${req.generationDescription}`;
2871
2999
  }
2872
3000
  return prompt;
2873
3001
  }
@@ -2927,6 +3055,61 @@ ${req.analysis.emits.map(
2927
3055
  return urlMap[provider] || "https://api.openai.com/v1";
2928
3056
  }
2929
3057
  };
3058
+ function compressFunctionBodies(code, maxLength) {
3059
+ const lines = code.split("\n");
3060
+ const result = [];
3061
+ let depth = 0;
3062
+ let fnDepth = 0;
3063
+ let inFunction = false;
3064
+ let braceBalance = 0;
3065
+ let capturedLines = 0;
3066
+ for (const line of lines) {
3067
+ const trimmed = line.trim();
3068
+ for (const ch of trimmed) {
3069
+ if (ch === "{") {
3070
+ braceBalance++;
3071
+ depth++;
3072
+ }
3073
+ if (ch === "}") {
3074
+ braceBalance--;
3075
+ depth = Math.max(0, depth - 1);
3076
+ }
3077
+ }
3078
+ 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);
3079
+ if (isFnSignature && !inFunction) {
3080
+ inFunction = true;
3081
+ fnDepth = depth;
3082
+ result.push(line);
3083
+ capturedLines++;
3084
+ continue;
3085
+ }
3086
+ if (inFunction) {
3087
+ const isReturn = trimmed.startsWith("return ");
3088
+ const isBranch = /^(if|else|switch|case|try|catch|finally|for|while)/.test(trimmed);
3089
+ const isClosing = trimmed === "}" && depth <= fnDepth - 1;
3090
+ if (isClosing) {
3091
+ result.push(line);
3092
+ inFunction = false;
3093
+ capturedLines++;
3094
+ } else if (isReturn || isBranch) {
3095
+ result.push(trimmed.length > 120 ? `${trimmed.slice(0, 120)}...` : line);
3096
+ capturedLines++;
3097
+ } else if (trimmed.startsWith("//") || trimmed === "") {
3098
+ } else if (capturedLines < maxLength / 30) {
3099
+ result.push(trimmed.length > 100 ? `${trimmed.slice(0, 100)}...` : line);
3100
+ capturedLines++;
3101
+ }
3102
+ } else {
3103
+ result.push(line);
3104
+ capturedLines++;
3105
+ }
3106
+ if (result.join("\n").length > maxLength) {
3107
+ result.push("// ... (truncated)");
3108
+ break;
3109
+ }
3110
+ }
3111
+ return result.join("\n");
3112
+ }
2930
3113
 
2931
3114
  // src/ai/provider.ts
2932
3115
  var providerRegistry = /* @__PURE__ */ new Map();
@@ -3416,15 +3599,18 @@ import path9 from "path";
3416
3599
  function analyzeFile(filePath) {
3417
3600
  const absolutePath = path9.resolve(process.cwd(), filePath);
3418
3601
  if (!fs9.existsSync(absolutePath)) {
3419
- return { filePath, exports: [], apiCalls: [] };
3602
+ return { filePath, exports: [], imports: [], importSignatures: [], apiCalls: [] };
3420
3603
  }
3421
3604
  const content = fs9.readFileSync(absolutePath, "utf-8");
3422
3605
  const ext = path9.extname(filePath);
3423
3606
  const result = {
3424
3607
  filePath,
3425
3608
  exports: extractExports(content, ext),
3609
+ imports: extractImports(content),
3610
+ importSignatures: [],
3426
3611
  apiCalls: extractAPICalls(content, filePath)
3427
3612
  };
3613
+ result.importSignatures = analyzeImportSignatures(result.imports, path9.dirname(absolutePath));
3428
3614
  if (ext === ".vue") {
3429
3615
  result.vueAnalysis = analyzeVueComponent(content, path9.basename(filePath, ".vue"));
3430
3616
  const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
@@ -3847,6 +4033,90 @@ function generateMockRoutesFromAPICalls(apiCalls) {
3847
4033
  }
3848
4034
  return routes;
3849
4035
  }
4036
+ function extractImports(content) {
4037
+ const imports = [];
4038
+ const namedImportRegex = /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
4039
+ let match;
4040
+ while ((match = namedImportRegex.exec(content)) !== null) {
4041
+ const names = match[1].split(",").map((n) => {
4042
+ const parts = n.trim().split(/\s+as\s+/);
4043
+ return parts[0].trim();
4044
+ }).filter(Boolean);
4045
+ imports.push({ names, source: match[2], isDefault: false, isNamespace: false });
4046
+ }
4047
+ const defaultImportRegex = /import\s+(\w+)\s+from\s*['"]([^'"]+)['"]/g;
4048
+ while ((match = defaultImportRegex.exec(content)) !== null) {
4049
+ const fullLine = content.slice(Math.max(0, match.index - 5), match.index + match[0].length);
4050
+ if (fullLine.includes("{")) continue;
4051
+ imports.push({ names: [match[1]], source: match[2], isDefault: true, isNamespace: false });
4052
+ }
4053
+ const namespaceImportRegex = /import\s*\*\s*as\s+(\w+)\s*from\s*['"]([^'"]+)['"]/g;
4054
+ while ((match = namespaceImportRegex.exec(content)) !== null) {
4055
+ imports.push({ names: [match[1]], source: match[2], isDefault: false, isNamespace: true });
4056
+ }
4057
+ return imports;
4058
+ }
4059
+ function analyzeImportSignatures(imports, baseDir) {
4060
+ const signatures = [];
4061
+ for (const imp of imports) {
4062
+ if (!imp.source.startsWith(".") && !imp.source.startsWith("@/") && !imp.source.startsWith("~")) {
4063
+ continue;
4064
+ }
4065
+ const resolvedPath = resolveImportPath2(imp.source, baseDir);
4066
+ if (!resolvedPath || !fs9.existsSync(resolvedPath)) continue;
4067
+ try {
4068
+ const content = fs9.readFileSync(resolvedPath, "utf-8");
4069
+ const ext = path9.extname(resolvedPath);
4070
+ const exports = extractExports(content, ext);
4071
+ const sigMap = {};
4072
+ for (const name of imp.names) {
4073
+ const exp = exports.find((e) => e.name === name);
4074
+ if (exp) {
4075
+ const params = exp.params.length > 0 ? `(${exp.params.join(", ")})` : "";
4076
+ const ret = exp.returnType ? `: ${exp.returnType}` : "";
4077
+ const async = exp.isAsync ? "async " : "";
4078
+ sigMap[name] = `${async}${exp.kind}${params}${ret}`;
4079
+ } else if (imp.isDefault) {
4080
+ const defaultExp = exports.find((e) => e.kind === "default");
4081
+ if (defaultExp) {
4082
+ sigMap[name] = `default[${defaultExp.name || "anonymous"}]`;
4083
+ }
4084
+ } else if (imp.isNamespace) {
4085
+ sigMap[name] = `{${exports.map((e) => e.name).join(", ")}}`;
4086
+ }
4087
+ }
4088
+ if (ext === ".vue") {
4089
+ const vueAnalysis = analyzeVueComponent(content, path9.basename(resolvedPath, ".vue"));
4090
+ if (vueAnalysis.props.length > 0) {
4091
+ sigMap["__props__"] = vueAnalysis.props.map((p) => `${p.name}:${p.type}${p.required ? "!" : "?"}`).join(",");
4092
+ }
4093
+ }
4094
+ if (Object.keys(sigMap).length > 0) {
4095
+ signatures.push({ source: imp.source, signatures: sigMap });
4096
+ }
4097
+ } catch {
4098
+ }
4099
+ }
4100
+ return signatures;
4101
+ }
4102
+ function resolveImportPath2(importSource, baseDir) {
4103
+ let resolved;
4104
+ if (importSource.startsWith("@/") || importSource.startsWith("~")) {
4105
+ const relativePath = importSource.replace(/^[@~]\//, "src/");
4106
+ resolved = path9.resolve(process.cwd(), relativePath);
4107
+ } else if (importSource.startsWith(".")) {
4108
+ resolved = path9.resolve(baseDir, importSource);
4109
+ } else {
4110
+ return null;
4111
+ }
4112
+ const extensions = [".ts", ".tsx", ".js", ".jsx", ".vue", "/index.ts", "/index.js"];
4113
+ if (fs9.existsSync(resolved)) return resolved;
4114
+ for (const ext of extensions) {
4115
+ const withExt = resolved + ext;
4116
+ if (fs9.existsSync(withExt)) return withExt;
4117
+ }
4118
+ return null;
4119
+ }
3850
4120
  export {
3851
4121
  AI_PRESET_PROVIDERS,
3852
4122
  DEFAULT_CONFIG,