qat-cli 0.3.5 → 0.3.7

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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Quick Auto Testing — 面向 Vue 项目,集成 Vitest & Playwright,AI 驱动覆盖测试全流程**
6
6
 
7
- [![npm version](https://img.shields.io/badge/version-0.3.05-blue.svg)](https://www.npmjs.com/package/qat-cli)
7
+ [![npm version](https://img.shields.io/badge/version-0.3.07-blue.svg)](https://www.npmjs.com/package/qat-cli)
8
8
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-green.svg)](https://nodejs.org/)
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue.svg)](https://www.typescriptlang.org/)
package/dist/cli.js CHANGED
@@ -787,6 +787,37 @@ function debugLog(tag, ...args) {
787
787
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
788
788
  console.error(chalk2.gray(`[DEBUG ${timestamp}] [${tag}]`), ...args);
789
789
  }
790
+ var ConcurrencyPool = class {
791
+ constructor(maxConcurrency) {
792
+ this.maxConcurrency = maxConcurrency;
793
+ this.running = 0;
794
+ this.queue = [];
795
+ }
796
+ async acquire() {
797
+ if (this.running < this.maxConcurrency) {
798
+ this.running++;
799
+ return;
800
+ }
801
+ return new Promise((resolve) => {
802
+ this.queue.push({ resolve });
803
+ });
804
+ }
805
+ release() {
806
+ this.running--;
807
+ const next = this.queue.shift();
808
+ if (next) {
809
+ this.running++;
810
+ next.resolve();
811
+ }
812
+ }
813
+ get pending() {
814
+ return this.queue.length;
815
+ }
816
+ get active() {
817
+ return this.running;
818
+ }
819
+ };
820
+ var aiPool = new ConcurrencyPool(3);
790
821
  var OpenAICompatibleProvider = class {
791
822
  constructor(config) {
792
823
  this.capabilities = {
@@ -902,9 +933,34 @@ ${errorDetails}`;
902
933
  return { ok: false, message: `\u8FDE\u63A5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`, latencyMs };
903
934
  }
904
935
  }
936
+ /**
937
+ * 核心 chat 方法
938
+ *
939
+ * 调度策略:
940
+ * 1. 始终使用流式(stream: true),避免长时间等待无响应导致超时
941
+ * 2. 分段超时:连接/首chunk/后续chunk 各自独立超时,只要 AI 在输出就不超时
942
+ * 3. 并发池控制:acquire → 请求 → release,避免同时太多请求
943
+ * 4. 指数退避重试:429/5xx/timeout 自动重试,最多 2 次
944
+ */
905
945
  async chat(systemPrompt, userPrompt, retries = 2) {
946
+ await aiPool.acquire();
947
+ try {
948
+ return await this._chatInner(systemPrompt, userPrompt, retries);
949
+ } finally {
950
+ aiPool.release();
951
+ }
952
+ }
953
+ async _chatInner(systemPrompt, userPrompt, retries) {
906
954
  const url = `${this.baseUrl}/chat/completions`;
907
- const useStream = isDebug();
955
+ const TIMEOUT = {
956
+ /** 连接建立超时:fetch 请求本身的最大等待时间 */
957
+ CONNECT: 15e3,
958
+ /** 首个 chunk 超时:连接建好后等待 AI 开始输出的时间 */
959
+ FIRST_CHUNK: 3e4,
960
+ /** chunk 间超时:每个 chunk 之间的最大间隔 */
961
+ CHUNK_INTERVAL: 2e4
962
+ };
963
+ debugLog("SCHEDULER", `\u5E76\u53D1\u6C60: active=${aiPool.active} pending=${aiPool.pending} \u5206\u6BB5\u8D85\u65F6: connect=${TIMEOUT.CONNECT / 1e3}s first=${TIMEOUT.FIRST_CHUNK / 1e3}s interval=${TIMEOUT.CHUNK_INTERVAL / 1e3}s`);
908
964
  const body = {
909
965
  model: this.model,
910
966
  messages: [
@@ -912,19 +968,16 @@ ${errorDetails}`;
912
968
  { role: "user", content: userPrompt }
913
969
  ],
914
970
  temperature: 0.3,
915
- max_tokens: 4096
971
+ max_tokens: 4096,
972
+ stream: true
916
973
  };
917
- if (useStream) {
918
- body.stream = true;
919
- }
920
974
  const headers = {
921
975
  "Content-Type": "application/json"
922
976
  };
923
977
  if (this.apiKey) {
924
978
  headers["Authorization"] = `Bearer ${this.apiKey}`;
925
979
  }
926
- debugLog("REQUEST", `POST ${url}`);
927
- debugLog("REQUEST", `model=${this.model} stream=${useStream}`);
980
+ debugLog("REQUEST", `POST ${url} model=${this.model} stream=true`);
928
981
  debugLog("SYSTEM", systemPrompt.length > 500 ? `${systemPrompt.slice(0, 500)}...` : systemPrompt);
929
982
  debugLog("USER", userPrompt.length > 1e3 ? `${userPrompt.slice(0, 1e3)}...` : userPrompt);
930
983
  let lastError = null;
@@ -933,12 +986,12 @@ ${errorDetails}`;
933
986
  if (attempt > 0) {
934
987
  debugLog("RETRY", `\u7B2C${attempt}\u6B21\u91CD\u8BD5...`);
935
988
  }
989
+ const connectStart = Date.now();
936
990
  const response = await fetch(url, {
937
991
  method: "POST",
938
992
  headers,
939
993
  body: JSON.stringify(body),
940
- signal: AbortSignal.timeout(12e4)
941
- // 120s timeout
994
+ signal: AbortSignal.timeout(TIMEOUT.CONNECT)
942
995
  });
943
996
  if (!response.ok) {
944
997
  const text = await response.text().catch(() => "");
@@ -951,17 +1004,21 @@ ${errorDetails}`;
951
1004
  }
952
1005
  throw new Error(`AI API \u8BF7\u6C42\u5931\u8D25 (${response.status}): ${text.slice(0, 500)}`);
953
1006
  }
954
- if (useStream && response.body) {
955
- const content = await this.readStream(response.body);
956
- debugLog("RESPONSE", content.length > 500 ? `${content.slice(0, 500)}...` : content);
1007
+ const connectMs = Date.now() - connectStart;
1008
+ debugLog("CONNECT", `\u8FDE\u63A5\u5EFA\u7ACB ${connectMs}ms`);
1009
+ if (response.body) {
1010
+ const content = await this.readStreamWithTimeout(
1011
+ response.body,
1012
+ TIMEOUT.FIRST_CHUNK,
1013
+ TIMEOUT.CHUNK_INTERVAL
1014
+ );
1015
+ debugLog("RESPONSE", `${content.length} chars, \u603B\u8017\u65F6 ${Date.now() - connectStart}ms`);
957
1016
  return content;
958
1017
  }
959
1018
  const data = await response.json();
960
1019
  if (!data.choices?.[0]?.message?.content) {
961
1020
  throw new Error("AI API \u8FD4\u56DE\u7A7A\u54CD\u5E94");
962
1021
  }
963
- debugLog("RESPONSE", `tokens: prompt=${data.usage?.prompt_tokens} completion=${data.usage?.completion_tokens} total=${data.usage?.total_tokens}`);
964
- debugLog("RESPONSE", data.choices[0].message.content.length > 500 ? `${data.choices[0].message.content.slice(0, 500)}...` : data.choices[0].message.content);
965
1022
  return data.choices[0].message.content;
966
1023
  } catch (error) {
967
1024
  if (error instanceof Error && error.name === "TimeoutError" && attempt < retries) {
@@ -975,18 +1032,53 @@ ${errorDetails}`;
975
1032
  throw lastError || new Error("AI API \u8BF7\u6C42\u5931\u8D25");
976
1033
  }
977
1034
  /**
978
- * 读取 SSE 流式响应,实时输出内容
1035
+ * 分段超时的流式读取
1036
+ *
1037
+ * 核心思路:每个 chunk 独立超时,只要 AI 持续输出就永远不会超时
1038
+ *
1039
+ * 三级超时:
1040
+ * - firstChunkTimeout: 连接建立后等待首个 AI 输出的超时(AI 思考时间)
1041
+ * - chunkIntervalTimeout: 每个 chunk 之间的超时(网络中断检测)
1042
+ * - 每个 read() 操作独立计时,收到 chunk 立即重置
979
1043
  */
980
- async readStream(body) {
1044
+ async readStreamWithTimeout(body, firstChunkTimeout, chunkIntervalTimeout) {
981
1045
  const chunks = [];
982
1046
  const decoder = new TextDecoder();
983
1047
  const reader = body.getReader();
984
1048
  let buffer = "";
985
- let lineCount = 0;
1049
+ let isFirstChunk = true;
1050
+ let lastChunkTime = Date.now();
986
1051
  try {
987
1052
  while (true) {
988
- const { done, value } = await reader.read();
1053
+ const currentTimeout = isFirstChunk ? firstChunkTimeout : chunkIntervalTimeout;
1054
+ const elapsed = Date.now() - lastChunkTime;
1055
+ if (elapsed > currentTimeout) {
1056
+ const phase = isFirstChunk ? "\u9996chunk" : "chunk\u95F4";
1057
+ debugLog("STREAM-TIMEOUT", `${phase}\u8D85\u65F6(${currentTimeout / 1e3}s)\uFF0C\u5DF2\u6536 ${chunks.length} chunks`);
1058
+ if (chunks.length > 0) {
1059
+ debugLog("STREAM-TIMEOUT", `\u5DF2\u6709\u5185\u5BB9\uFF0C\u8FD4\u56DE\u5DF2\u6536\u5230\u7684 ${chunks.join("").length} chars`);
1060
+ break;
1061
+ }
1062
+ throw new Error(`AI \u6D41\u5F0F\u54CD\u5E94${phase}\u8D85\u65F6(${currentTimeout / 1e3}s)`);
1063
+ }
1064
+ const remainingMs = currentTimeout - elapsed;
1065
+ let readResult;
1066
+ try {
1067
+ readResult = await Promise.race([
1068
+ reader.read(),
1069
+ new Promise(
1070
+ (_, reject) => setTimeout(() => reject(new Error("__CHUNK_TIMEOUT__")), remainingMs)
1071
+ )
1072
+ ]);
1073
+ } catch (e) {
1074
+ if (e instanceof Error && e.message === "__CHUNK_TIMEOUT__") {
1075
+ continue;
1076
+ }
1077
+ throw e;
1078
+ }
1079
+ const { done, value } = readResult;
989
1080
  if (done) break;
1081
+ lastChunkTime = Date.now();
990
1082
  buffer += decoder.decode(value, { stream: true });
991
1083
  const lines = buffer.split("\n");
992
1084
  buffer = lines.pop() || "";
@@ -998,11 +1090,13 @@ ${errorDetails}`;
998
1090
  const json = JSON.parse(trimmed.slice(6));
999
1091
  const delta = json.choices?.[0]?.delta?.content;
1000
1092
  if (delta) {
1093
+ if (isFirstChunk) {
1094
+ isFirstChunk = false;
1095
+ debugLog("STREAM", `\u9996chunk\u5230\u8FBE ${Date.now() - lastChunkTime}ms`);
1096
+ }
1001
1097
  chunks.push(delta);
1002
- lineCount++;
1003
- process.stderr.write(chalk2.gray(delta));
1004
- if (lineCount % 20 === 0) {
1005
- process.stderr.write("\n");
1098
+ if (isDebug()) {
1099
+ process.stderr.write(chalk2.gray(delta));
1006
1100
  }
1007
1101
  }
1008
1102
  if (json.choices?.[0]?.finish_reason === "stop") {
@@ -1015,7 +1109,7 @@ ${errorDetails}`;
1015
1109
  } finally {
1016
1110
  reader.releaseLock();
1017
1111
  }
1018
- if (chunks.length > 0) {
1112
+ if (isDebug() && chunks.length > 0) {
1019
1113
  process.stderr.write("\n");
1020
1114
  }
1021
1115
  return chunks.join("");
@@ -1156,6 +1250,8 @@ ${this.compressSourceCode(req.context, 3e3, importPathRewrites)}
1156
1250
  if (srcDirMatch) {
1157
1251
  variations.add(`@/${srcDirMatch[1]}`);
1158
1252
  variations.add(`@/${srcDirMatch[1].replace(/\.(ts|js|tsx|jsx)$/, "")}`);
1253
+ variations.add(`#/${srcDirMatch[1]}`);
1254
+ variations.add(`#/${srcDirMatch[1].replace(/\.(ts|js|tsx|jsx)$/, "")}`);
1159
1255
  }
1160
1256
  const pathParts = targetPath.replace(/^\.\//, "").split("/");
1161
1257
  const fileName = pathParts[pathParts.length - 1];
@@ -1890,7 +1986,7 @@ function extractImports(content) {
1890
1986
  function analyzeImportSignatures(imports, baseDir) {
1891
1987
  const signatures = [];
1892
1988
  for (const imp of imports) {
1893
- if (!imp.source.startsWith(".") && !imp.source.startsWith("@/") && !imp.source.startsWith("~")) {
1989
+ if (!imp.source.startsWith(".") && !imp.source.startsWith("@/") && !imp.source.startsWith("#/") && !imp.source.startsWith("~")) {
1894
1990
  continue;
1895
1991
  }
1896
1992
  const resolvedPath = resolveImportPath(imp.source, baseDir);
@@ -1932,8 +2028,8 @@ function analyzeImportSignatures(imports, baseDir) {
1932
2028
  }
1933
2029
  function resolveImportPath(importSource, baseDir) {
1934
2030
  let resolved;
1935
- if (importSource.startsWith("@/") || importSource.startsWith("~")) {
1936
- const relativePath = importSource.replace(/^[@~]\//, "src/");
2031
+ if (importSource.startsWith("@/") || importSource.startsWith("#/") || importSource.startsWith("~")) {
2032
+ const relativePath = importSource.replace(/^[@#~]\//, "src/");
1937
2033
  resolved = path4.resolve(process.cwd(), relativePath);
1938
2034
  } else if (importSource.startsWith(".")) {
1939
2035
  resolved = path4.resolve(baseDir, importSource);
@@ -2718,6 +2814,7 @@ import ora2 from "ora";
2718
2814
  import path8 from "path";
2719
2815
  var MAX_RETRIES = 3;
2720
2816
  var REVIEW_THRESHOLD = 0.6;
2817
+ var REVIEW_SKIP_SOURCE_SIZE = 8e3;
2721
2818
  async function generateWithReview(params) {
2722
2819
  const { testType, targetPath, sourceCode, analysis, aiConfig, framework, onAttempt, onProgress } = params;
2723
2820
  const generatorProvider = createAIProvider(aiConfig);
@@ -2732,6 +2829,13 @@ async function generateWithReview(params) {
2732
2829
  let approved = false;
2733
2830
  let attempts = 0;
2734
2831
  const targetName = path8.basename(targetPath);
2832
+ const skipReview = sourceCode.length > REVIEW_SKIP_SOURCE_SIZE;
2833
+ if (skipReview) {
2834
+ onProgress?.(`\u6E90\u7801\u8F83\u5927(${(sourceCode.length / 1024).toFixed(1)}KB)\uFF0C\u8DF3\u8FC7\u5BA1\u8BA1 \u2190 ${targetName}`);
2835
+ if (process.env.QAT_DEBUG === "true") {
2836
+ console.error(chalk4.gray(`[DEBUG] \u6E90\u7801 ${sourceCode.length} chars > ${REVIEW_SKIP_SOURCE_SIZE}\uFF0C\u8DF3\u8FC7\u5BA1\u8BA1\u73AF\u8282`));
2837
+ }
2838
+ }
2735
2839
  for (let i = 0; i < MAX_RETRIES; i++) {
2736
2840
  attempts = i + 1;
2737
2841
  onAttempt?.(attempts, MAX_RETRIES);
@@ -2760,6 +2864,11 @@ ${lastReview.suggestions.map((s) => `- ${s}`).join("\n")}
2760
2864
  currentCode = generateResponse.code;
2761
2865
  currentDescription = generateResponse.description;
2762
2866
  currentConfidence = generateResponse.confidence;
2867
+ if (skipReview) {
2868
+ approved = true;
2869
+ lastReview = { approved: true, score: 0.7, feedback: "\u6E90\u7801\u8F83\u5927\uFF0C\u8DF3\u8FC7\u5BA1\u8BA1", issues: [], suggestions: [] };
2870
+ break;
2871
+ }
2763
2872
  onProgress?.(`\u5BA1\u8BA1\u5BA1\u67E5 \u2190 ${targetName}`);
2764
2873
  if (process.env.QAT_DEBUG === "true") {
2765
2874
  console.error(chalk4.gray(`[DEBUG] \u2500\u2500\u2500 \u5BA1\u8BA1\u5458 AI \u5BA1\u67E5 \u2500\u2500\u2500`));
@@ -2815,6 +2924,61 @@ function printReviewReport(report) {
2815
2924
  }
2816
2925
 
2817
2926
  // src/commands/create.ts
2927
+ var MultiProgressBar = class {
2928
+ constructor(labels) {
2929
+ this.lastRenderLines = 0;
2930
+ this.renderScheduled = false;
2931
+ this.labels = labels;
2932
+ this.statuses = labels.map(() => "waiting");
2933
+ this.statusLabels = labels.map(() => "\u7B49\u5F85\u4E2D");
2934
+ this.render();
2935
+ }
2936
+ update(index, status, label) {
2937
+ if (index < 0 || index >= this.labels.length) return;
2938
+ this.statuses[index] = status;
2939
+ this.statusLabels[index] = label;
2940
+ if (!this.renderScheduled) {
2941
+ this.renderScheduled = true;
2942
+ queueMicrotask(() => {
2943
+ this.renderScheduled = false;
2944
+ this.render();
2945
+ });
2946
+ }
2947
+ }
2948
+ done() {
2949
+ this.render();
2950
+ process.stderr.write("\n");
2951
+ }
2952
+ render() {
2953
+ if (this.lastRenderLines > 0) {
2954
+ process.stderr.write(`\x1B[${this.lastRenderLines}A\x1B[0J`);
2955
+ }
2956
+ const lines = [];
2957
+ for (let i = 0; i < this.labels.length; i++) {
2958
+ const icon = this.getIcon(this.statuses[i]);
2959
+ const label = this.statusLabels[i];
2960
+ lines.push(` ${icon} ${chalk5.bold(this.labels[i])} ${chalk5.gray(label)}`);
2961
+ }
2962
+ process.stderr.write(lines.join("\n") + "\n");
2963
+ this.lastRenderLines = lines.length;
2964
+ }
2965
+ getIcon(status) {
2966
+ switch (status) {
2967
+ case "waiting":
2968
+ return chalk5.gray("\u25CB");
2969
+ case "generating":
2970
+ return chalk5.cyan("\u27F3");
2971
+ case "reviewing":
2972
+ return chalk5.yellow("\u27F3");
2973
+ case "done":
2974
+ return chalk5.green("\u25CF");
2975
+ case "failed":
2976
+ return chalk5.red("\u2717");
2977
+ case "paused":
2978
+ return chalk5.yellow("\u23F8");
2979
+ }
2980
+ }
2981
+ };
2818
2982
  var TEST_TYPE_LABELS = {
2819
2983
  unit: "\u5355\u5143\u6D4B\u8BD5",
2820
2984
  component: "\u7EC4\u4EF6\u6D4B\u8BD5",
@@ -2914,59 +3078,114 @@ async function executeCreate(options) {
2914
3078
  ]);
2915
3079
  useAI = ai;
2916
3080
  }
3081
+ const tasks = [];
3082
+ for (const testType of types) {
3083
+ for (const testTarget of targets) {
3084
+ tasks.push({ testType, target: testTarget });
3085
+ }
3086
+ }
2917
3087
  const createdFiles = [];
2918
3088
  let skippedCount = 0;
2919
3089
  const reviewReportEntries = [];
2920
- for (const testType of types) {
2921
- for (const testTarget of targets) {
2922
- const spinner = ora3(`\u6B63\u5728\u751F\u6210 ${TEST_TYPE_LABELS[testType]} - ${path9.basename(testTarget)}...`).start();
2923
- try {
2924
- let content;
2925
- if (useAI && testTarget) {
2926
- const aiResult = await generateWithAI(testType, name, testTarget, config, (text) => {
2927
- spinner.text = `${text}`;
2928
- });
2929
- content = aiResult.code;
2930
- if (aiResult.reviewEntry) {
2931
- reviewReportEntries.push(aiResult.reviewEntry);
3090
+ if (useAI && tasks.length > 0) {
3091
+ const fileLabels = tasks.map((t) => path9.basename(t.target));
3092
+ const progress = new MultiProgressBar(fileLabels);
3093
+ const results = await Promise.allSettled(
3094
+ tasks.map(async (task, idx) => {
3095
+ progress.update(idx, "generating", "\u751F\u6210\u6D4B\u8BD5\u4EE3\u7801...");
3096
+ const result = await generateWithAI(task.testType, name, task.target, config, (text) => {
3097
+ const isReview = text.includes("\u5BA1\u8BA1");
3098
+ const isRetry = text.includes("\u91CD\u65B0\u751F\u6210");
3099
+ if (isReview) {
3100
+ progress.update(idx, "reviewing", "\u5BA1\u8BA1\u5BA1\u67E5...");
3101
+ } else if (isRetry) {
3102
+ progress.update(idx, "generating", text);
3103
+ } else {
3104
+ progress.update(idx, "generating", text);
2932
3105
  }
3106
+ });
3107
+ return { ...task, ...result };
3108
+ })
3109
+ );
3110
+ let approvedCount = 0;
3111
+ let failedCount = 0;
3112
+ for (let i = 0; i < results.length; i++) {
3113
+ const r = results[i];
3114
+ const task = tasks[i];
3115
+ if (r.status === "rejected") {
3116
+ progress.update(i, "failed", r.reason instanceof Error ? r.reason.message : String(r.reason));
3117
+ failedCount++;
3118
+ continue;
3119
+ }
3120
+ const { code, reviewEntry } = r.value;
3121
+ const outputDir = getTestOutputDir(task.testType);
3122
+ const filePath = path9.join(outputDir, `${name}.${task.testType === "e2e" ? "spec" : "test"}.ts`);
3123
+ if (fs8.existsSync(filePath)) {
3124
+ progress.update(i, "paused", "\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7");
3125
+ skippedCount++;
3126
+ continue;
3127
+ }
3128
+ if (!fs8.existsSync(outputDir)) {
3129
+ fs8.mkdirSync(outputDir, { recursive: true });
3130
+ }
3131
+ fs8.writeFileSync(filePath, code, "utf-8");
3132
+ createdFiles.push({ type: task.testType, filePath });
3133
+ if (reviewEntry) {
3134
+ reviewReportEntries.push(reviewEntry);
3135
+ if (reviewEntry.approved) {
3136
+ progress.update(i, "done", `\u5BA1\u8BA1\u901A\u8FC7 (${(reviewEntry.score * 100).toFixed(0)}%)`);
3137
+ approvedCount++;
2933
3138
  } else {
2934
- const analysis = testTarget ? analyzeFile(testTarget) : void 0;
2935
- content = renderTemplate(testType, {
2936
- name,
2937
- target: testTarget || `./${config.project.srcDir}`,
2938
- framework: projectInfo.framework,
2939
- vueVersion: projectInfo.vueVersion,
2940
- typescript: projectInfo.typescript,
2941
- uiLibrary: projectInfo.uiLibrary,
2942
- extraImports: projectInfo.componentTestSetup?.extraImports,
2943
- globalPlugins: projectInfo.componentTestSetup?.globalPlugins,
2944
- globalStubs: projectInfo.componentTestSetup?.globalStubs,
2945
- mountOptions: projectInfo.componentTestSetup?.mountOptions,
2946
- // 注入源码分析结果
2947
- exports: analysis?.exports.map((e) => ({
2948
- name: e.name,
2949
- kind: e.kind,
2950
- params: e.params,
2951
- isAsync: e.isAsync,
2952
- returnType: e.returnType
2953
- })),
2954
- props: analysis?.vueAnalysis?.props.map((p) => ({
2955
- name: p.name,
2956
- type: p.type,
2957
- required: p.required,
2958
- defaultValue: p.defaultValue
2959
- })),
2960
- emits: analysis?.vueAnalysis?.emits.map((e) => ({
2961
- name: e.name,
2962
- params: e.params
2963
- })),
2964
- methods: analysis?.vueAnalysis?.methods,
2965
- computed: analysis?.vueAnalysis?.computed
2966
- });
3139
+ progress.update(i, "paused", `\u5BA1\u8BA1\u672A\u901A\u8FC7 (${(reviewEntry.score * 100).toFixed(0)}%) \u2014 \u6682\u505C`);
3140
+ failedCount++;
2967
3141
  }
2968
- const outputDir = getTestOutputDir(testType);
2969
- const filePath = path9.join(outputDir, `${name}.${testType === "e2e" ? "spec" : "test"}.ts`);
3142
+ } else {
3143
+ progress.update(i, "done", "\u5B8C\u6210");
3144
+ approvedCount++;
3145
+ }
3146
+ }
3147
+ progress.done();
3148
+ console.log(chalk5.cyan(`
3149
+ \u751F\u6210\u5B8C\u6210: ${chalk5.green(approvedCount + " \u901A\u8FC7")} ${failedCount > 0 ? chalk5.red(failedCount + " \u672A\u901A\u8FC7") : ""} ${skippedCount > 0 ? chalk5.yellow(skippedCount + " \u8DF3\u8FC7") : ""}`));
3150
+ } else {
3151
+ for (const task of tasks) {
3152
+ const spinner = ora3(`\u6B63\u5728\u751F\u6210 ${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)}...`).start();
3153
+ try {
3154
+ const analysis = task.target ? analyzeFile(task.target) : void 0;
3155
+ const content = renderTemplate(task.testType, {
3156
+ name,
3157
+ target: task.target || `./${config.project.srcDir}`,
3158
+ framework: projectInfo.framework,
3159
+ vueVersion: projectInfo.vueVersion,
3160
+ typescript: projectInfo.typescript,
3161
+ uiLibrary: projectInfo.uiLibrary,
3162
+ extraImports: projectInfo.componentTestSetup?.extraImports,
3163
+ globalPlugins: projectInfo.componentTestSetup?.globalPlugins,
3164
+ globalStubs: projectInfo.componentTestSetup?.globalStubs,
3165
+ mountOptions: projectInfo.componentTestSetup?.mountOptions,
3166
+ // 注入源码分析结果
3167
+ exports: analysis?.exports.map((e) => ({
3168
+ name: e.name,
3169
+ kind: e.kind,
3170
+ params: e.params,
3171
+ isAsync: e.isAsync,
3172
+ returnType: e.returnType
3173
+ })),
3174
+ props: analysis?.vueAnalysis?.props.map((p) => ({
3175
+ name: p.name,
3176
+ type: p.type,
3177
+ required: p.required,
3178
+ defaultValue: p.defaultValue
3179
+ })),
3180
+ emits: analysis?.vueAnalysis?.emits.map((e) => ({
3181
+ name: e.name,
3182
+ params: e.params
3183
+ })),
3184
+ methods: analysis?.vueAnalysis?.methods,
3185
+ computed: analysis?.vueAnalysis?.computed
3186
+ });
3187
+ const outputDir = getTestOutputDir(task.testType);
3188
+ const filePath = path9.join(outputDir, `${name}.${task.testType === "e2e" ? "spec" : "test"}.ts`);
2970
3189
  if (fs8.existsSync(filePath)) {
2971
3190
  spinner.warn(`\u6D4B\u8BD5\u6587\u4EF6\u5DF2\u5B58\u5728: ${path9.relative(process.cwd(), filePath)}`);
2972
3191
  skippedCount++;
@@ -2976,10 +3195,10 @@ async function executeCreate(options) {
2976
3195
  fs8.mkdirSync(outputDir, { recursive: true });
2977
3196
  }
2978
3197
  fs8.writeFileSync(filePath, content, "utf-8");
2979
- spinner.succeed(`${TEST_TYPE_LABELS[testType]} - ${path9.basename(testTarget)}`);
2980
- createdFiles.push({ type: testType, filePath });
3198
+ spinner.succeed(`${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)}`);
3199
+ createdFiles.push({ type: task.testType, filePath });
2981
3200
  } catch (error) {
2982
- spinner.fail(`${TEST_TYPE_LABELS[testType]} - ${path9.basename(testTarget)} \u521B\u5EFA\u5931\u8D25`);
3201
+ spinner.fail(`${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)} \u521B\u5EFA\u5931\u8D25`);
2983
3202
  console.log(chalk5.gray(` ${error instanceof Error ? error.message : String(error)}`));
2984
3203
  }
2985
3204
  }
@@ -2988,6 +3207,49 @@ async function executeCreate(options) {
2988
3207
  if (reviewReportEntries.length > 0) {
2989
3208
  printReviewReport(reviewReportEntries);
2990
3209
  }
3210
+ const failedEntries = reviewReportEntries.filter((e) => !e.approved);
3211
+ if (failedEntries.length > 0 && useAI) {
3212
+ console.log();
3213
+ console.log(chalk5.yellow(` \u23F8 ${failedEntries.length} \u4E2A\u6D4B\u8BD5\u7528\u4F8B\u5BA1\u8BA1\u672A\u901A\u8FC7:`));
3214
+ for (const entry of failedEntries) {
3215
+ console.log(chalk5.gray(` - ${path9.basename(entry.target)} (${(entry.score * 100).toFixed(0)}%) ${entry.feedback}`));
3216
+ }
3217
+ const { retry } = await inquirer2.prompt([
3218
+ {
3219
+ type: "confirm",
3220
+ name: "retry",
3221
+ message: `\u662F\u5426\u91CD\u65B0\u751F\u6210\u8FD9\u4E9B\u672A\u901A\u8FC7\u7684\u6D4B\u8BD5\u7528\u4F8B\uFF1F`,
3222
+ default: false
3223
+ }
3224
+ ]);
3225
+ if (retry) {
3226
+ console.log();
3227
+ const retryLabels = failedEntries.map((e) => path9.basename(e.target));
3228
+ const retryProgress = new MultiProgressBar(retryLabels);
3229
+ const retryResults = await Promise.allSettled(
3230
+ failedEntries.map(async (entry, idx) => {
3231
+ retryProgress.update(idx, "generating", "\u91CD\u65B0\u751F\u6210...");
3232
+ const aiResult = await generateWithAI(entry.testType, name, entry.target, config, (text) => {
3233
+ const isReview = text.includes("\u5BA1\u8BA1");
3234
+ retryProgress.update(idx, isReview ? "reviewing" : "generating", text);
3235
+ });
3236
+ const outputDir = getTestOutputDir(entry.testType);
3237
+ const filePath = path9.join(outputDir, `${name}.${entry.testType === "e2e" ? "spec" : "test"}.ts`);
3238
+ fs8.writeFileSync(filePath, aiResult.code, "utf-8");
3239
+ if (aiResult.reviewEntry?.approved) {
3240
+ retryProgress.update(idx, "done", `\u5BA1\u8BA1\u901A\u8FC7 (${(aiResult.reviewEntry.score * 100).toFixed(0)}%)`);
3241
+ } else {
3242
+ retryProgress.update(idx, "failed", aiResult.reviewEntry ? `\u4ECD\u672A\u901A\u8FC7 (${(aiResult.reviewEntry.score * 100).toFixed(0)}%)` : "\u5931\u8D25");
3243
+ }
3244
+ return aiResult;
3245
+ })
3246
+ );
3247
+ retryProgress.done();
3248
+ const retryPassed = retryResults.filter((r) => r.status === "fulfilled" && r.value.reviewEntry?.approved).length;
3249
+ const retryFailed = retryResults.length - retryPassed;
3250
+ console.log(chalk5.cyan(` \u91CD\u8BD5\u5B8C\u6210: ${chalk5.green(retryPassed + " \u901A\u8FC7")} ${retryFailed > 0 ? chalk5.red(retryFailed + " \u4ECD\u9700\u5BA1\u67E5") : ""}`));
3251
+ }
3252
+ }
2991
3253
  }
2992
3254
  async function selectTargets(types, projectInfo, srcDir) {
2993
3255
  const allTargets = [];
@@ -5922,7 +6184,7 @@ async function executeChange(_options) {
5922
6184
  }
5923
6185
 
5924
6186
  // src/cli.ts
5925
- var VERSION = "0.3.05";
6187
+ var VERSION = "0.3.07";
5926
6188
  function printLogo() {
5927
6189
  const logo = `
5928
6190
  ${chalk13.bold.cyan(" ___ _ _ _ _ _____ _ _ ")}