@nick848/sf-cli 1.0.16 → 1.0.18

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.mjs CHANGED
@@ -59,6 +59,7 @@ async function handleNew(args, ctx) {
59
59
  context: null,
60
60
  clarityScore: 0,
61
61
  clarificationQuestions: [],
62
+ referenceResources: [],
62
63
  complexity: 0,
63
64
  bddScenarios: [],
64
65
  specItems: [],
@@ -101,7 +102,7 @@ async function executeWorkflow(ctx) {
101
102
  const lines = [];
102
103
  try {
103
104
  if (activeSession.phase === "context") {
104
- lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 1/8: \u9879\u76EE\u4E0A\u4E0B\u6587\u83B7\u53D6 \u2501\u2501\u2501"));
105
+ lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 1/9: \u9879\u76EE\u4E0A\u4E0B\u6587\u83B7\u53D6 \u2501\u2501\u2501"));
105
106
  lines.push("");
106
107
  activeSession.context = await readProjectContext(ctx.options.workingDirectory);
107
108
  lines.push(chalk9.gray(` \u9879\u76EE: ${activeSession.context.name}`));
@@ -115,7 +116,7 @@ async function executeWorkflow(ctx) {
115
116
  }
116
117
  if (activeSession.phase === "clarify") {
117
118
  lines.push("");
118
- lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 2/8: \u9700\u6C42\u6F84\u6E05 \u2501\u2501\u2501"));
119
+ lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 2/9: \u9700\u6C42\u6F84\u6E05 \u2501\u2501\u2501"));
119
120
  lines.push("");
120
121
  const clarityResult = analyzeRequirementClarity(
121
122
  activeSession.requirement,
@@ -141,11 +142,40 @@ async function executeWorkflow(ctx) {
141
142
  return { output: lines.join("\n") };
142
143
  }
143
144
  lines.push(chalk9.green(" \u2713 \u9700\u6C42\u6E05\u6670\uFF0C\u7EE7\u7EED\u4E0B\u4E00\u6B65"));
145
+ activeSession.phase = "reference";
146
+ }
147
+ if (activeSession.phase === "reference") {
148
+ lines.push("");
149
+ lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 3/9: \u53C2\u8003\u8D44\u6E90\u5206\u6790 \u2501\u2501\u2501"));
150
+ lines.push("");
151
+ const urls = extractUrls(activeSession.refinedRequirement);
152
+ if (urls.length > 0) {
153
+ lines.push(chalk9.gray(` \u53D1\u73B0 ${urls.length} \u4E2A\u53C2\u8003\u94FE\u63A5`));
154
+ lines.push("");
155
+ for (const url of urls) {
156
+ lines.push(chalk9.gray(` \u{1F4CE} ${url}`));
157
+ try {
158
+ const resource = await fetchAndAnalyzeReference(url, ctx);
159
+ activeSession.referenceResources.push(resource);
160
+ lines.push(chalk9.green(` \u2713 \u5DF2\u5206\u6790`));
161
+ activeSession.refinedRequirement += `
162
+
163
+ \u3010\u53C2\u8003\u8D44\u6E90\u5206\u6790 - ${url}\u3011
164
+ ${resource.analysis}`;
165
+ } catch (error) {
166
+ lines.push(chalk9.yellow(` \u26A0 \u83B7\u53D6\u5931\u8D25: ${error.message}`));
167
+ }
168
+ }
169
+ lines.push("");
170
+ lines.push(chalk9.green(" \u2713 \u53C2\u8003\u8D44\u6E90\u5206\u6790\u5B8C\u6210"));
171
+ } else {
172
+ lines.push(chalk9.gray(" \u65E0\u5916\u90E8\u53C2\u8003\u94FE\u63A5"));
173
+ }
144
174
  activeSession.phase = "analysis";
145
175
  }
146
176
  if (activeSession.phase === "analysis") {
147
177
  lines.push("");
148
- lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 3/8: \u590D\u6742\u5EA6\u8BC4\u4F30 \u2501\u2501\u2501"));
178
+ lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 4/9: \u590D\u6742\u5EA6\u8BC4\u4F30 \u2501\u2501\u2501"));
149
179
  lines.push("");
150
180
  activeSession.complexity = analyzeComplexity(
151
181
  activeSession.refinedRequirement,
@@ -162,13 +192,28 @@ async function executeWorkflow(ctx) {
162
192
  }
163
193
  if (activeSession.phase === "bdd") {
164
194
  lines.push("");
165
- lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 4/8: BDD \u573A\u666F\u62C6\u89E3 \u2501\u2501\u2501"));
195
+ lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 5/9: BDD \u573A\u666F\u62C6\u89E3 \u2501\u2501\u2501"));
166
196
  lines.push("");
167
- activeSession.bddScenarios = generateBDDScenarios(
168
- activeSession.refinedRequirement,
169
- activeSession.context,
170
- activeSession.clarificationQuestions
171
- );
197
+ const loader = new LoadingIndicator("AI \u6B63\u5728\u751F\u6210 BDD \u573A\u666F");
198
+ loader.start();
199
+ try {
200
+ activeSession.bddScenarios = await generateBDDScenariosWithAI(
201
+ activeSession.refinedRequirement,
202
+ activeSession.context,
203
+ activeSession.clarificationQuestions,
204
+ activeSession.referenceResources,
205
+ ctx
206
+ );
207
+ loader.stop(chalk9.green(" \u2713 BDD \u573A\u666F\u5DF2\u751F\u6210"));
208
+ } catch {
209
+ loader.stop(chalk9.yellow(" \u26A0 \u4F7F\u7528\u57FA\u7840 BDD \u751F\u6210"));
210
+ activeSession.bddScenarios = generateBDDScenarios(
211
+ activeSession.refinedRequirement,
212
+ activeSession.context,
213
+ activeSession.clarificationQuestions,
214
+ activeSession.referenceResources
215
+ );
216
+ }
172
217
  for (const scenario of activeSession.bddScenarios) {
173
218
  lines.push(chalk9.white(` Feature: ${scenario.feature}`));
174
219
  for (const s of scenario.scenarios.slice(0, 3)) {
@@ -182,13 +227,14 @@ async function executeWorkflow(ctx) {
182
227
  }
183
228
  if (activeSession.phase === "spec") {
184
229
  lines.push("");
185
- lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 5/8: OpenSpec \u89C4\u683C \u2501\u2501\u2501"));
230
+ lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 6/9: OpenSpec \u89C4\u683C \u2501\u2501\u2501"));
186
231
  lines.push("");
187
232
  activeSession.specItems = generateSpecItems(
188
233
  activeSession.refinedRequirement,
189
234
  activeSession.context,
190
235
  activeSession.bddScenarios,
191
- activeSession.clarificationQuestions
236
+ activeSession.clarificationQuestions,
237
+ activeSession.referenceResources
192
238
  );
193
239
  const specPath = await saveSpecFile(ctx.options.workingDirectory, activeSession);
194
240
  lines.push(chalk9.green(" \u2713 \u89C4\u683C\u6587\u4EF6\u5DF2\u751F\u6210"));
@@ -212,9 +258,9 @@ async function executeWorkflow(ctx) {
212
258
  }
213
259
  if (activeSession.phase === "tdd") {
214
260
  lines.push("");
215
- lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 6/8: TDD \u6D4B\u8BD5\u751F\u6210 \u2501\u2501\u2501"));
261
+ lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 7/9: TDD \u6D4B\u8BD5\u751F\u6210 \u2501\u2501\u2501"));
216
262
  lines.push("");
217
- activeSession.testFiles = await generateTests(ctx.options.workingDirectory, activeSession);
263
+ activeSession.testFiles = await generateTests(ctx.options.workingDirectory, activeSession, ctx);
218
264
  lines.push(chalk9.green(" \u2713 \u6D4B\u8BD5\u6587\u4EF6\u5DF2\u751F\u6210"));
219
265
  for (const file of activeSession.testFiles) {
220
266
  lines.push(chalk9.gray(` - ${file}`));
@@ -223,7 +269,7 @@ async function executeWorkflow(ctx) {
223
269
  }
224
270
  if (activeSession.phase === "develop") {
225
271
  lines.push("");
226
- lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 7/8: \u5F00\u53D1\u5B9E\u73B0 \u2501\u2501\u2501"));
272
+ lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 8/9: \u5F00\u53D1\u5B9E\u73B0 \u2501\u2501\u2501"));
227
273
  lines.push("");
228
274
  lines.push(chalk9.yellow(" \u{1F680} \u6B63\u5728\u8C03\u7528 AI \u751F\u6210\u4EE3\u7801..."));
229
275
  try {
@@ -248,7 +294,7 @@ async function executeWorkflow(ctx) {
248
294
  }
249
295
  if (activeSession.phase === "review") {
250
296
  lines.push("");
251
- lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 8/8: \u4EE3\u7801\u5BA1\u6838 \u2501\u2501\u2501"));
297
+ lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 9/9: \u4EE3\u7801\u5BA1\u6838 \u2501\u2501\u2501"));
252
298
  lines.push("");
253
299
  lines.push(chalk9.yellow(" \u{1F50D} \u6B63\u5728\u8FDB\u884C\u4EE3\u7801\u5BA1\u6838..."));
254
300
  try {
@@ -511,6 +557,8 @@ function getCategoryLabel(category) {
511
557
  async function executeDevelopment(ctx, session) {
512
558
  const workingDir = ctx.options.workingDirectory;
513
559
  const files = [];
560
+ const loader = new LoadingIndicator("AI \u6B63\u5728\u751F\u6210\u4EE3\u7801");
561
+ loader.start();
514
562
  try {
515
563
  const systemPrompt = buildDevelopmentPrompt(session);
516
564
  const messages = [
@@ -538,11 +586,15 @@ ${session.context.devStandards.slice(0, 2e3)}` : ""}`
538
586
  content: systemPrompt
539
587
  }
540
588
  ];
589
+ loader.update("\u6B63\u5728\u8C03\u7528 AI \u6A21\u578B");
541
590
  const response = await ctx.modelService.sendMessage(messages, {
542
591
  temperature: 0.3,
543
592
  maxTokens: 8e3,
544
- agent: "frontend-dev"
593
+ agent: "frontend-dev",
594
+ timeout: 18e4
595
+ // 3 分钟超时
545
596
  });
597
+ loader.update("\u6B63\u5728\u89E3\u6790\u4EE3\u7801");
546
598
  const codeBlocks = parseCodeBlocks(response.content);
547
599
  for (const block of codeBlocks) {
548
600
  const filePath = path5.join(workingDir, block.filename);
@@ -571,8 +623,10 @@ export function ${featureName.replace(/[^a-zA-Z0-9]/g, "")}() {
571
623
  await fs4.writeFile(filePath, stubCode, "utf-8");
572
624
  files.push(`src/features/${fileName}`);
573
625
  }
626
+ loader.stop(chalk9.green(` \u2713 \u5DF2\u751F\u6210 ${files.length} \u4E2A\u6587\u4EF6`));
574
627
  return { success: true, files };
575
628
  } catch (error) {
629
+ loader.stop();
576
630
  return {
577
631
  success: false,
578
632
  files: [],
@@ -790,13 +844,41 @@ function analyzeComplexity(requirement, context) {
790
844
  if (!context.framework) score += 0.5;
791
845
  return Math.max(1, Math.min(10, Math.round(score)));
792
846
  }
793
- function generateBDDScenarios(requirement, context, questions) {
847
+ function generateBDDScenarios(requirement, context, questions, references = []) {
794
848
  const scenarios = [];
795
849
  questions.find((q) => q.category === "ui" && q.answered)?.answer;
796
850
  const interactionAnswer = questions.find((q) => q.category === "interaction" && q.answered)?.answer;
797
851
  const edgeAnswer = questions.find((q) => q.category === "edge" && q.answered)?.answer;
852
+ if (references.length > 0) {
853
+ for (const ref of references) {
854
+ const refFeatures = extractFeaturesFromReference(ref);
855
+ for (const feature of refFeatures) {
856
+ const scenario = {
857
+ feature: feature.title,
858
+ description: feature.description,
859
+ scenarios: []
860
+ };
861
+ scenario.scenarios.push({
862
+ name: `\u6B63\u5E38\u6D41\u7A0B: ${feature.title}`,
863
+ given: [`\u7528\u6237\u8FDB\u5165\u76F8\u5173\u9875\u9762`],
864
+ when: [`\u7528\u6237\u6267\u884C "${feature.title}" \u64CD\u4F5C`],
865
+ then: [`\u7CFB\u7EDF\u5E94\u6B63\u786E\u5904\u7406\u5E76\u8FD4\u56DE\u9884\u671F\u7ED3\u679C`]
866
+ });
867
+ if (feature.hasInput) {
868
+ scenario.scenarios.push({
869
+ name: `\u8FB9\u754C\u60C5\u51B5: \u8F93\u5165\u9A8C\u8BC1`,
870
+ given: [`\u7528\u6237\u8FDB\u5165\u8F93\u5165\u754C\u9762`],
871
+ when: [`\u7528\u6237\u8F93\u5165\u8FB9\u754C\u503C\u6216\u7A7A\u503C`],
872
+ then: [`\u7CFB\u7EDF\u5E94\u6B63\u786E\u5904\u7406\u8FB9\u754C\u60C5\u51B5`]
873
+ });
874
+ }
875
+ scenarios.push(scenario);
876
+ }
877
+ }
878
+ }
798
879
  const features = extractFeatures(requirement);
799
880
  for (const feature of features) {
881
+ if (scenarios.some((s) => s.feature === feature.title)) continue;
800
882
  const scenario = {
801
883
  feature: feature.title,
802
884
  description: feature.description,
@@ -828,6 +910,135 @@ function generateBDDScenarios(requirement, context, questions) {
828
910
  }
829
911
  return scenarios;
830
912
  }
913
+ async function generateBDDScenariosWithAI(requirement, context, questions, references, ctx) {
914
+ const prompt2 = `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u6D4B\u8BD5\u5DE5\u7A0B\u5E08\u548C\u4E1A\u52A1\u5206\u6790\u5E08\u3002\u8BF7\u6839\u636E\u4EE5\u4E0B\u9700\u6C42\u751F\u6210\u8BE6\u7EC6\u7684 BDD (Behavior Driven Development) \u573A\u666F\u3002
915
+
916
+ ## \u9700\u6C42\u63CF\u8FF0
917
+ ${requirement}
918
+
919
+ ## \u9879\u76EE\u4E0A\u4E0B\u6587
920
+ - \u6280\u672F\u6808: ${context.techStack?.join(", ") || "\u672A\u6307\u5B9A"}
921
+ - \u6846\u67B6: ${context.framework || "\u672A\u6307\u5B9A"}
922
+
923
+ ## \u6F84\u6E05\u95EE\u7B54
924
+ ${questions.filter((q) => q.answered).map((q) => `- Q: ${q.question}
925
+ A: ${q.answer}`).join("\n")}
926
+
927
+ ## \u53C2\u8003\u8D44\u6E90\u5206\u6790
928
+ ${references.map((r) => `### ${r.url}
929
+ ${r.analysis}`).join("\n\n")}
930
+
931
+ ## \u8981\u6C42
932
+ 1. \u6BCF\u4E2A\u529F\u80FD\u6A21\u5757\u751F\u6210\u4E00\u4E2A\u72EC\u7ACB\u7684 Feature
933
+ 2. \u6BCF\u4E2A Feature \u5305\u542B\u591A\u4E2A\u5177\u4F53\u7684 Scenario
934
+ 3. \u4F7F\u7528 Given-When-Then \u683C\u5F0F
935
+ 4. \u573A\u666F\u8981\u8986\u76D6: \u6B63\u5E38\u6D41\u7A0B\u3001\u8FB9\u754C\u60C5\u51B5\u3001\u5F02\u5E38\u5904\u7406
936
+ 5. \u573A\u666F\u8981\u5177\u4F53\u53EF\u6D4B\u8BD5\uFF0C\u4E0D\u8981\u6CDB\u6CDB\u800C\u8C08
937
+
938
+ ## \u8F93\u51FA\u683C\u5F0F (JSON)
939
+ \`\`\`json
940
+ [
941
+ {
942
+ "feature": "\u529F\u80FD\u540D\u79F0",
943
+ "description": "\u529F\u80FD\u63CF\u8FF0",
944
+ "scenarios": [
945
+ {
946
+ "name": "\u573A\u666F\u540D\u79F0",
947
+ "given": ["\u524D\u7F6E\u6761\u4EF61", "\u524D\u7F6E\u6761\u4EF62"],
948
+ "when": ["\u64CD\u4F5C1", "\u64CD\u4F5C2"],
949
+ "then": ["\u9884\u671F\u7ED3\u679C1", "\u9884\u671F\u7ED3\u679C2"]
950
+ }
951
+ ]
952
+ }
953
+ ]
954
+ \`\`\`
955
+
956
+ \u8BF7\u76F4\u63A5\u8F93\u51FA JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u6709\u5176\u4ED6\u5185\u5BB9\u3002`;
957
+ const response = await ctx.modelService.sendMessage([
958
+ { role: "user", content: prompt2 }
959
+ ], {
960
+ temperature: 0.3,
961
+ maxTokens: 4e3,
962
+ timeout: 12e4
963
+ });
964
+ try {
965
+ const jsonMatch = response.content.match(/```json\s*([\s\S]*?)```/);
966
+ if (jsonMatch) {
967
+ return JSON.parse(jsonMatch[1].trim());
968
+ }
969
+ return JSON.parse(response.content);
970
+ } catch {
971
+ return generateBDDScenarios(requirement, context, questions, references);
972
+ }
973
+ }
974
+ function extractFeaturesFromReference(ref) {
975
+ const features = [];
976
+ const analysis = ref.analysis;
977
+ const featureSection = analysis.match(/###?\s*4\.\s*功能拆分建议[\s\S]*?(?=###?\s*\d|$)/i);
978
+ if (featureSection) {
979
+ const taskMatches = featureSection[0].matchAll(/[-*]\s*\*\*([^*]+)\*\*[::]?\s*([^\n]+)/g);
980
+ for (const match of taskMatches) {
981
+ features.push({
982
+ title: match[1].trim(),
983
+ description: match[2].trim(),
984
+ hasInput: match[2].includes("\u8F93\u5165") || match[2].includes("\u8868\u5355") || match[2].includes("\u7528\u6237")
985
+ });
986
+ }
987
+ }
988
+ const bizSection = analysis.match(/###?\s*1\.\s*业务功能分析[\s\S]*?(?=###?\s*\d|$)/i);
989
+ if (bizSection && features.length === 0) {
990
+ const lines = bizSection[0].split("\n").filter((l) => l.trim().startsWith("-") || l.trim().startsWith("*"));
991
+ for (const line of lines.slice(0, 5)) {
992
+ const content = line.replace(/^[-*]\s*/, "").trim();
993
+ if (content.length > 5) {
994
+ features.push({
995
+ title: content.slice(0, 20),
996
+ description: content,
997
+ hasInput: content.includes("\u8F93\u5165") || content.includes("\u586B\u5199")
998
+ });
999
+ }
1000
+ }
1001
+ }
1002
+ if (features.length === 0) {
1003
+ const lowerAnalysis = analysis.toLowerCase();
1004
+ if (lowerAnalysis.includes("\u8F93\u5165") || lowerAnalysis.includes("\u8868\u5355")) {
1005
+ features.push({
1006
+ title: "\u8F93\u5165\u8868\u5355",
1007
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u8F93\u5165\u8868\u5355\u529F\u80FD",
1008
+ hasInput: true
1009
+ });
1010
+ }
1011
+ if (lowerAnalysis.includes("\u6309\u94AE") || lowerAnalysis.includes("\u64CD\u4F5C")) {
1012
+ features.push({
1013
+ title: "\u4EA4\u4E92\u6309\u94AE",
1014
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u6309\u94AE\u4EA4\u4E92",
1015
+ hasInput: false
1016
+ });
1017
+ }
1018
+ if (lowerAnalysis.includes("\u8868\u683C") || lowerAnalysis.includes("\u5217\u8868")) {
1019
+ features.push({
1020
+ title: "\u6570\u636E\u5217\u8868",
1021
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u6570\u636E\u5C55\u793A",
1022
+ hasInput: false
1023
+ });
1024
+ }
1025
+ if (lowerAnalysis.includes("\u56FE\u8868") || lowerAnalysis.includes("\u53EF\u89C6\u5316")) {
1026
+ features.push({
1027
+ title: "\u56FE\u8868\u5C55\u793A",
1028
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u56FE\u8868\u53EF\u89C6\u5316",
1029
+ hasInput: false
1030
+ });
1031
+ }
1032
+ }
1033
+ if (features.length === 0) {
1034
+ features.push({
1035
+ title: "\u53C2\u8003\u529F\u80FD\u5B9E\u73B0",
1036
+ description: `\u57FA\u4E8E\u53C2\u8003\u8D44\u6E90 ${ref.url} \u5B9E\u73B0\u7684\u529F\u80FD`,
1037
+ hasInput: true
1038
+ });
1039
+ }
1040
+ return features;
1041
+ }
831
1042
  function extractFeatures(requirement) {
832
1043
  const features = [];
833
1044
  const urlMatch = requirement.match(/https?:\/\/[^\s]+/);
@@ -868,15 +1079,26 @@ function extractFeatures(requirement) {
868
1079
  }
869
1080
  return features;
870
1081
  }
871
- function generateSpecItems(requirement, context, bddScenarios, questions) {
1082
+ function generateSpecItems(requirement, context, bddScenarios, questions, references = []) {
872
1083
  const items = [];
873
1084
  let id = 1;
1085
+ for (const ref of references) {
1086
+ items.push({
1087
+ id: `T${id.toString().padStart(3, "0")}`,
1088
+ title: `\u53C2\u8003\u5206\u6790: ${ref.type}`,
1089
+ description: `\u5206\u6790\u53C2\u8003\u8D44\u6E90 ${ref.url}`,
1090
+ priority: "high",
1091
+ files: [],
1092
+ tests: []
1093
+ });
1094
+ id++;
1095
+ }
874
1096
  for (const scenario of bddScenarios) {
875
1097
  items.push({
876
1098
  id: `T${id.toString().padStart(3, "0")}`,
877
1099
  title: scenario.feature,
878
1100
  description: scenario.description,
879
- priority: id <= 2 ? "high" : "medium",
1101
+ priority: id <= 3 ? "high" : "medium",
880
1102
  files: [],
881
1103
  tests: []
882
1104
  });
@@ -919,6 +1141,19 @@ function formatSpecFile(session) {
919
1141
  lines.push("---");
920
1142
  lines.push("");
921
1143
  }
1144
+ if (session.referenceResources.length > 0) {
1145
+ lines.push("## \u53C2\u8003\u8D44\u6E90");
1146
+ lines.push("");
1147
+ for (const ref of session.referenceResources) {
1148
+ lines.push(`### ${ref.url}`);
1149
+ lines.push(`> \u7C7B\u578B: ${ref.type}`);
1150
+ lines.push("");
1151
+ lines.push(ref.analysis);
1152
+ lines.push("");
1153
+ }
1154
+ lines.push("---");
1155
+ lines.push("");
1156
+ }
922
1157
  if (session.clarificationQuestions.some((q) => q.answered)) {
923
1158
  lines.push("## \u9700\u6C42\u6F84\u6E05");
924
1159
  lines.push("");
@@ -957,33 +1192,119 @@ function formatSpecFile(session) {
957
1192
  lines.push("**\u786E\u8BA4\u72B6\u6001**: \u23F3 \u7B49\u5F85\u786E\u8BA4");
958
1193
  return lines.join("\n");
959
1194
  }
960
- async function generateTests(workingDir, session) {
1195
+ async function generateTests(workingDir, session, ctx) {
961
1196
  const testDir = path5.join(workingDir, "tests");
962
1197
  await fs4.mkdir(testDir, { recursive: true });
963
1198
  const testFiles = [];
964
- for (const scenario of session.bddScenarios) {
965
- const testName = scenario.feature.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "_");
966
- const testPath = path5.join(testDir, `${testName}.test.ts`);
967
- const content = generateTestFile(scenario);
968
- await fs4.writeFile(testPath, content, "utf-8");
969
- testFiles.push(`tests/${testName}.test.ts`);
1199
+ if (ctx?.modelService) {
1200
+ for (const scenario of session.bddScenarios) {
1201
+ const testName = scenario.feature.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "_");
1202
+ const testPath = path5.join(testDir, `${testName}.test.ts`);
1203
+ const loader = new LoadingIndicator(`\u751F\u6210\u6D4B\u8BD5: ${scenario.feature.slice(0, 20)}...`);
1204
+ loader.start();
1205
+ try {
1206
+ const content = await generateTestFileWithAI(scenario, session, ctx);
1207
+ await fs4.writeFile(testPath, content, "utf-8");
1208
+ testFiles.push(`tests/${testName}.test.ts`);
1209
+ loader.stop(chalk9.green(` \u2713 \u6D4B\u8BD5\u6587\u4EF6\u5DF2\u751F\u6210`));
1210
+ } catch {
1211
+ const content = generateTestFile(scenario, session);
1212
+ await fs4.writeFile(testPath, content, "utf-8");
1213
+ testFiles.push(`tests/${testName}.test.ts`);
1214
+ loader.stop(chalk9.yellow(" \u26A0 \u4F7F\u7528\u57FA\u7840\u6D4B\u8BD5\u6A21\u677F"));
1215
+ }
1216
+ }
1217
+ } else {
1218
+ for (const scenario of session.bddScenarios) {
1219
+ const testName = scenario.feature.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "_");
1220
+ const testPath = path5.join(testDir, `${testName}.test.ts`);
1221
+ const content = generateTestFile(scenario, session);
1222
+ await fs4.writeFile(testPath, content, "utf-8");
1223
+ testFiles.push(`tests/${testName}.test.ts`);
1224
+ }
970
1225
  }
971
1226
  return testFiles;
972
1227
  }
973
- function generateTestFile(scenario) {
1228
+ async function generateTestFileWithAI(scenario, session, ctx) {
1229
+ const prompt2 = `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u6D4B\u8BD5\u5DE5\u7A0B\u5E08\u3002\u8BF7\u6839\u636E\u4EE5\u4E0B BDD \u573A\u666F\u751F\u6210\u5B8C\u6574\u7684 Vitest \u6D4B\u8BD5\u4EE3\u7801\u3002
1230
+
1231
+ ## \u529F\u80FD\u540D\u79F0
1232
+ ${scenario.feature}
1233
+
1234
+ ## BDD \u573A\u666F
1235
+ ${scenario.scenarios.map((s) => `
1236
+ ### ${s.name}
1237
+ - Given: ${s.given.join(", ")}
1238
+ - When: ${s.when.join(", ")}
1239
+ - Then: ${s.then.join(", ")}
1240
+ `).join("\n")}
1241
+
1242
+ ## \u9879\u76EE\u4E0A\u4E0B\u6587
1243
+ - \u6280\u672F\u6808: ${session.context?.techStack?.join(", ") || "TypeScript"}
1244
+ - \u6846\u67B6: ${session.context?.framework || "\u672A\u6307\u5B9A"}
1245
+
1246
+ ## \u8981\u6C42
1247
+ 1. \u4F7F\u7528 vitest \u6D4B\u8BD5\u6846\u67B6 (describe, it, expect, beforeEach \u7B49)
1248
+ 2. \u6BCF\u4E2A\u573A\u666F\u751F\u6210\u4E00\u4E2A\u72EC\u7ACB\u7684\u6D4B\u8BD5\u7528\u4F8B
1249
+ 3. \u6D4B\u8BD5\u4EE3\u7801\u8981\u5B8C\u6574\u53EF\u8FD0\u884C\uFF0C\u5305\u542B\u5FC5\u8981\u7684 mock \u548C setup
1250
+ 4. \u4F7F\u7528\u4E2D\u6587\u6CE8\u91CA\u8BF4\u660E\u6D4B\u8BD5\u610F\u56FE
1251
+ 5. \u6D4B\u8BD5\u8981\u8986\u76D6\u6B63\u5E38\u6D41\u7A0B\u548C\u8FB9\u754C\u60C5\u51B5
1252
+
1253
+ \u8BF7\u76F4\u63A5\u8F93\u51FA\u6D4B\u8BD5\u4EE3\u7801\uFF0C\u4E0D\u9700\u8981\u89E3\u91CA\u3002`;
1254
+ const response = await ctx.modelService.sendMessage([
1255
+ { role: "user", content: prompt2 }
1256
+ ], {
1257
+ temperature: 0.3,
1258
+ maxTokens: 4e3
1259
+ });
1260
+ const codeMatch = response.content.match(/```(?:typescript|ts|javascript|js)?\n([\s\S]*?)```/);
1261
+ if (codeMatch) {
1262
+ return codeMatch[1].trim();
1263
+ }
1264
+ return response.content;
1265
+ }
1266
+ function generateTestFile(scenario, session) {
974
1267
  const lines = [];
975
- lines.push(`import { describe, it, expect } from 'vitest';`);
1268
+ lines.push(`import { describe, it, expect, beforeEach } from 'vitest';`);
976
1269
  lines.push("");
1270
+ lines.push(`/**`);
1271
+ lines.push(` * ${scenario.feature} \u529F\u80FD\u6D4B\u8BD5`);
1272
+ lines.push(` * `);
1273
+ lines.push(` * BDD \u573A\u666F\u6570\u91CF: ${scenario.scenarios.length}`);
1274
+ if (session?.context?.techStack) {
1275
+ lines.push(` * \u6280\u672F\u6808: ${session.context.techStack.join(", ")}`);
1276
+ }
1277
+ lines.push(` */`);
977
1278
  lines.push(`describe('${scenario.feature}', () => {`);
978
1279
  for (const s of scenario.scenarios) {
979
- lines.push(` it('${s.name}', () => {`);
980
- lines.push(` // Given: ${s.given.join(", ")}`);
981
- lines.push(` // When: ${s.when.join(", ")}`);
982
- lines.push(` // Then: ${s.then.join(", ")}`);
983
- lines.push(` expect(true).toBe(true); // TODO: \u5B9E\u73B0\u6D4B\u8BD5`);
984
- lines.push(` });`);
985
1280
  lines.push("");
1281
+ lines.push(` /**`);
1282
+ lines.push(` * \u573A\u666F: ${s.name}`);
1283
+ lines.push(` * Given: ${s.given.join(", ")}`);
1284
+ lines.push(` * When: ${s.when.join(", ")}`);
1285
+ lines.push(` * Then: ${s.then.join(", ")}`);
1286
+ lines.push(` */`);
1287
+ lines.push(` it('${s.name}', async () => {`);
1288
+ lines.push(` // Arrange (Given)`);
1289
+ for (const g of s.given) {
1290
+ lines.push(` // ${g}`);
1291
+ }
1292
+ lines.push(` const input = {}; // TODO: \u8BBE\u7F6E\u521D\u59CB\u72B6\u6001`);
1293
+ lines.push("");
1294
+ lines.push(` // Act (When)`);
1295
+ for (const w of s.when) {
1296
+ lines.push(` // ${w}`);
1297
+ }
1298
+ lines.push(` const result = {}; // TODO: \u6267\u884C\u64CD\u4F5C`);
1299
+ lines.push("");
1300
+ lines.push(` // Assert (Then)`);
1301
+ for (const t of s.then) {
1302
+ lines.push(` // ${t}`);
1303
+ }
1304
+ lines.push(` expect(result).toBeDefined(); // TODO: \u5B8C\u5584\u65AD\u8A00`);
1305
+ lines.push(` });`);
986
1306
  }
1307
+ lines.push("");
987
1308
  lines.push(`});`);
988
1309
  return lines.join("\n");
989
1310
  }
@@ -1024,6 +1345,124 @@ function generateSessionId() {
1024
1345
  const random = Math.random().toString(36).slice(2, 6);
1025
1346
  return `WF-${timestamp}-${random}`.toUpperCase();
1026
1347
  }
1348
+ function extractUrls(text) {
1349
+ const urlRegex = /https?:\/\/[^\s<>"{}|\\^`\[\]]+/gi;
1350
+ const matches = text.match(urlRegex);
1351
+ return matches ? [...new Set(matches)] : [];
1352
+ }
1353
+ async function fetchAndAnalyzeReference(url, ctx) {
1354
+ const type = detectResourceType(url);
1355
+ let content = "";
1356
+ let analysis = "";
1357
+ try {
1358
+ const response = await fetch(url, {
1359
+ headers: {
1360
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
1361
+ }
1362
+ });
1363
+ if (!response.ok) {
1364
+ throw new Error(`HTTP ${response.status}`);
1365
+ }
1366
+ content = await response.text();
1367
+ if (ctx.modelService.getCurrentModel()) {
1368
+ analysis = await analyzeReferenceContent(url, content, type, ctx);
1369
+ } else {
1370
+ analysis = extractBasicInfo(content, type);
1371
+ }
1372
+ } catch (error) {
1373
+ throw new Error(`\u65E0\u6CD5\u83B7\u53D6\u53C2\u8003\u8D44\u6E90: ${error.message}`);
1374
+ }
1375
+ return { url, type, content: content.slice(0, 1e4), analysis };
1376
+ }
1377
+ function detectResourceType(url) {
1378
+ if (url.includes("figma.com") || url.includes("lanhuapp.com")) {
1379
+ return "design";
1380
+ }
1381
+ if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(url)) {
1382
+ return "image";
1383
+ }
1384
+ if (/api\//i.test(url)) {
1385
+ return "api";
1386
+ }
1387
+ return "webpage";
1388
+ }
1389
+ async function analyzeReferenceContent(url, content, type, ctx) {
1390
+ const prompt2 = `
1391
+ \u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u4EA7\u54C1\u7ECF\u7406\u548C\u524D\u7AEF\u5F00\u53D1\u5DE5\u7A0B\u5E08\u3002\u8BF7\u6DF1\u5165\u5206\u6790\u4EE5\u4E0B\u53C2\u8003\u8D44\u6E90\uFF0C\u63D0\u53D6\u4E1A\u52A1\u529F\u80FD\u548C\u6280\u672F\u5B9E\u73B0\u7EC6\u8282\u3002
1392
+
1393
+ ## \u53C2\u8003\u8D44\u6E90\u4FE1\u606F
1394
+ - URL: ${url}
1395
+ - \u7C7B\u578B: ${type}
1396
+
1397
+ ## \u7F51\u9875\u5185\u5BB9
1398
+ \`\`\`html
1399
+ ${content.slice(0, 8e3)}
1400
+ \`\`\`
1401
+
1402
+ ## \u5206\u6790\u8981\u6C42
1403
+
1404
+ \u8BF7\u6309\u7167\u4EE5\u4E0B\u7ED3\u6784\u8FDB\u884C\u8BE6\u7EC6\u5206\u6790\uFF1A
1405
+
1406
+ ### 1. \u4E1A\u52A1\u529F\u80FD\u5206\u6790
1407
+ - \u6838\u5FC3\u4E1A\u52A1\u529F\u80FD\u662F\u4EC0\u4E48\uFF1F\uFF08\u8BE6\u7EC6\u63CF\u8FF0\uFF09
1408
+ - \u7528\u6237\u53EF\u4EE5\u505A\u4EC0\u4E48\u64CD\u4F5C\uFF1F
1409
+ - \u4E1A\u52A1\u6D41\u7A0B\u662F\u4EC0\u4E48\uFF1F
1410
+ - \u6570\u636E\u8F93\u5165\u8F93\u51FA\u662F\u4EC0\u4E48\uFF1F
1411
+
1412
+ ### 2. UI/UX \u7ED3\u6784\u5206\u6790
1413
+ - \u9875\u9762\u5E03\u5C40\u7ED3\u6784
1414
+ - \u4E3B\u8981\u7EC4\u4EF6\u6709\u54EA\u4E9B\uFF1F
1415
+ - \u7EC4\u4EF6\u4E4B\u95F4\u7684\u5173\u7CFB
1416
+ - \u4EA4\u4E92\u65B9\u5F0F\uFF08\u70B9\u51FB\u3001\u8F93\u5165\u3001\u62D6\u62FD\u7B49\uFF09
1417
+
1418
+ ### 3. \u6570\u636E\u6A21\u578B\u5206\u6790
1419
+ - \u9700\u8981\u54EA\u4E9B\u6570\u636E\uFF1F
1420
+ - \u6570\u636E\u4E4B\u95F4\u7684\u5173\u7CFB
1421
+ - \u6570\u636E\u6765\u6E90\uFF08\u7528\u6237\u8F93\u5165/\u8BA1\u7B97/API\uFF09
1422
+
1423
+ ### 4. \u529F\u80FD\u62C6\u5206\u5EFA\u8BAE
1424
+ \u8BF7\u5C06\u529F\u80FD\u62C6\u5206\u4E3A\u53EF\u72EC\u7ACB\u5F00\u53D1\u7684\u4EFB\u52A1\uFF0C\u6BCF\u4E2A\u4EFB\u52A1\u5305\u542B\uFF1A
1425
+ - \u4EFB\u52A1\u540D\u79F0
1426
+ - \u4EFB\u52A1\u63CF\u8FF0
1427
+ - \u6280\u672F\u8981\u70B9
1428
+ - \u4F9D\u8D56\u5173\u7CFB
1429
+
1430
+ ### 5. \u6280\u672F\u5B9E\u73B0\u5EFA\u8BAE
1431
+ - \u63A8\u8350\u7684\u6280\u672F\u65B9\u6848
1432
+ - \u9700\u8981\u6CE8\u610F\u7684\u6280\u672F\u96BE\u70B9
1433
+ - \u6027\u80FD\u4F18\u5316\u5EFA\u8BAE
1434
+
1435
+ \u8BF7\u4EE5 Markdown \u683C\u5F0F\u8F93\u51FA\uFF0C\u91CD\u70B9\u7A81\u51FA\u4E1A\u52A1\u903B\u8F91\u548C\u529F\u80FD\u5B9E\u73B0\u7EC6\u8282\u3002
1436
+ `;
1437
+ const loader = new LoadingIndicator("AI \u6B63\u5728\u5206\u6790\u53C2\u8003\u8D44\u6E90");
1438
+ loader.start();
1439
+ try {
1440
+ const response = await ctx.modelService.sendMessage([
1441
+ { role: "user", content: prompt2 }
1442
+ ], {
1443
+ temperature: 0.3,
1444
+ maxTokens: 4e3
1445
+ });
1446
+ loader.stop(chalk9.green(" \u2713 \u5206\u6790\u5B8C\u6210"));
1447
+ return response.content;
1448
+ } catch (error) {
1449
+ loader.stop();
1450
+ throw error;
1451
+ }
1452
+ }
1453
+ function extractBasicInfo(content, type) {
1454
+ const titleMatch = content.match(/<title[^>]*>([^<]+)<\/title>/i);
1455
+ const descMatch = content.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i);
1456
+ const parts = [];
1457
+ if (titleMatch) {
1458
+ parts.push(`\u6807\u9898: ${titleMatch[1]}`);
1459
+ }
1460
+ if (descMatch) {
1461
+ parts.push(`\u63CF\u8FF0: ${descMatch[1]}`);
1462
+ }
1463
+ parts.push(`\u8D44\u6E90\u7C7B\u578B: ${type}`);
1464
+ return parts.join("\n");
1465
+ }
1027
1466
  function generateComplexityBar(score) {
1028
1467
  const filled = Math.round(score / 2);
1029
1468
  const empty = 5 - filled;
@@ -1033,6 +1472,7 @@ function getPhaseLabel(phase) {
1033
1472
  const labels = {
1034
1473
  context: "\u9879\u76EE\u4E0A\u4E0B\u6587\u83B7\u53D6",
1035
1474
  clarify: "\u9700\u6C42\u6F84\u6E05",
1475
+ reference: "\u53C2\u8003\u8D44\u6E90\u5206\u6790",
1036
1476
  analysis: "\u590D\u6742\u5EA6\u8BC4\u4F30",
1037
1477
  bdd: "BDD \u573A\u666F\u62C6\u89E3",
1038
1478
  spec: "OpenSpec \u89C4\u683C",
@@ -1048,10 +1488,43 @@ function getActiveSession() {
1048
1488
  function clearActiveSession() {
1049
1489
  activeSession = null;
1050
1490
  }
1051
- var MAX_FILE_SIZE2, COMPLEXITY_THRESHOLD, CLARITY_THRESHOLD, activeSession, new_default;
1491
+ var LoadingIndicator, MAX_FILE_SIZE2, COMPLEXITY_THRESHOLD, CLARITY_THRESHOLD, activeSession, new_default;
1052
1492
  var init_new = __esm({
1053
1493
  "src/commands/new.ts"() {
1054
1494
  init_esm_shims();
1495
+ LoadingIndicator = class {
1496
+ frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1497
+ frameIndex = 0;
1498
+ interval = null;
1499
+ message;
1500
+ constructor(message) {
1501
+ this.message = message;
1502
+ }
1503
+ start() {
1504
+ process.stdout.write("\x1B[?25l");
1505
+ this.interval = setInterval(() => {
1506
+ const frame = this.frames[this.frameIndex];
1507
+ process.stdout.write(`\r${chalk9.cyan(frame)} ${this.message}...`);
1508
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
1509
+ }, 80);
1510
+ }
1511
+ update(message) {
1512
+ this.message = message;
1513
+ }
1514
+ stop(finalMessage) {
1515
+ if (this.interval) {
1516
+ clearInterval(this.interval);
1517
+ this.interval = null;
1518
+ }
1519
+ process.stdout.write("\x1B[?25h");
1520
+ if (finalMessage) {
1521
+ process.stdout.write(`\r${finalMessage}
1522
+ `);
1523
+ } else {
1524
+ process.stdout.write("\r" + " ".repeat(60) + "\r");
1525
+ }
1526
+ }
1527
+ };
1055
1528
  MAX_FILE_SIZE2 = 1024 * 1024;
1056
1529
  COMPLEXITY_THRESHOLD = 6;
1057
1530
  CLARITY_THRESHOLD = 0.6;