@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.js CHANGED
@@ -84,6 +84,7 @@ async function handleNew(args, ctx) {
84
84
  context: null,
85
85
  clarityScore: 0,
86
86
  clarificationQuestions: [],
87
+ referenceResources: [],
87
88
  complexity: 0,
88
89
  bddScenarios: [],
89
90
  specItems: [],
@@ -126,7 +127,7 @@ async function executeWorkflow(ctx) {
126
127
  const lines = [];
127
128
  try {
128
129
  if (activeSession.phase === "context") {
129
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 1/8: \u9879\u76EE\u4E0A\u4E0B\u6587\u83B7\u53D6 \u2501\u2501\u2501"));
130
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 1/9: \u9879\u76EE\u4E0A\u4E0B\u6587\u83B7\u53D6 \u2501\u2501\u2501"));
130
131
  lines.push("");
131
132
  activeSession.context = await readProjectContext(ctx.options.workingDirectory);
132
133
  lines.push(chalk9__default.default.gray(` \u9879\u76EE: ${activeSession.context.name}`));
@@ -140,7 +141,7 @@ async function executeWorkflow(ctx) {
140
141
  }
141
142
  if (activeSession.phase === "clarify") {
142
143
  lines.push("");
143
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 2/8: \u9700\u6C42\u6F84\u6E05 \u2501\u2501\u2501"));
144
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 2/9: \u9700\u6C42\u6F84\u6E05 \u2501\u2501\u2501"));
144
145
  lines.push("");
145
146
  const clarityResult = analyzeRequirementClarity(
146
147
  activeSession.requirement,
@@ -166,11 +167,40 @@ async function executeWorkflow(ctx) {
166
167
  return { output: lines.join("\n") };
167
168
  }
168
169
  lines.push(chalk9__default.default.green(" \u2713 \u9700\u6C42\u6E05\u6670\uFF0C\u7EE7\u7EED\u4E0B\u4E00\u6B65"));
170
+ activeSession.phase = "reference";
171
+ }
172
+ if (activeSession.phase === "reference") {
173
+ lines.push("");
174
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 3/9: \u53C2\u8003\u8D44\u6E90\u5206\u6790 \u2501\u2501\u2501"));
175
+ lines.push("");
176
+ const urls = extractUrls(activeSession.refinedRequirement);
177
+ if (urls.length > 0) {
178
+ lines.push(chalk9__default.default.gray(` \u53D1\u73B0 ${urls.length} \u4E2A\u53C2\u8003\u94FE\u63A5`));
179
+ lines.push("");
180
+ for (const url of urls) {
181
+ lines.push(chalk9__default.default.gray(` \u{1F4CE} ${url}`));
182
+ try {
183
+ const resource = await fetchAndAnalyzeReference(url, ctx);
184
+ activeSession.referenceResources.push(resource);
185
+ lines.push(chalk9__default.default.green(` \u2713 \u5DF2\u5206\u6790`));
186
+ activeSession.refinedRequirement += `
187
+
188
+ \u3010\u53C2\u8003\u8D44\u6E90\u5206\u6790 - ${url}\u3011
189
+ ${resource.analysis}`;
190
+ } catch (error) {
191
+ lines.push(chalk9__default.default.yellow(` \u26A0 \u83B7\u53D6\u5931\u8D25: ${error.message}`));
192
+ }
193
+ }
194
+ lines.push("");
195
+ lines.push(chalk9__default.default.green(" \u2713 \u53C2\u8003\u8D44\u6E90\u5206\u6790\u5B8C\u6210"));
196
+ } else {
197
+ lines.push(chalk9__default.default.gray(" \u65E0\u5916\u90E8\u53C2\u8003\u94FE\u63A5"));
198
+ }
169
199
  activeSession.phase = "analysis";
170
200
  }
171
201
  if (activeSession.phase === "analysis") {
172
202
  lines.push("");
173
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 3/8: \u590D\u6742\u5EA6\u8BC4\u4F30 \u2501\u2501\u2501"));
203
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 4/9: \u590D\u6742\u5EA6\u8BC4\u4F30 \u2501\u2501\u2501"));
174
204
  lines.push("");
175
205
  activeSession.complexity = analyzeComplexity(
176
206
  activeSession.refinedRequirement,
@@ -187,13 +217,28 @@ async function executeWorkflow(ctx) {
187
217
  }
188
218
  if (activeSession.phase === "bdd") {
189
219
  lines.push("");
190
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 4/8: BDD \u573A\u666F\u62C6\u89E3 \u2501\u2501\u2501"));
220
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 5/9: BDD \u573A\u666F\u62C6\u89E3 \u2501\u2501\u2501"));
191
221
  lines.push("");
192
- activeSession.bddScenarios = generateBDDScenarios(
193
- activeSession.refinedRequirement,
194
- activeSession.context,
195
- activeSession.clarificationQuestions
196
- );
222
+ const loader = new LoadingIndicator("AI \u6B63\u5728\u751F\u6210 BDD \u573A\u666F");
223
+ loader.start();
224
+ try {
225
+ activeSession.bddScenarios = await generateBDDScenariosWithAI(
226
+ activeSession.refinedRequirement,
227
+ activeSession.context,
228
+ activeSession.clarificationQuestions,
229
+ activeSession.referenceResources,
230
+ ctx
231
+ );
232
+ loader.stop(chalk9__default.default.green(" \u2713 BDD \u573A\u666F\u5DF2\u751F\u6210"));
233
+ } catch {
234
+ loader.stop(chalk9__default.default.yellow(" \u26A0 \u4F7F\u7528\u57FA\u7840 BDD \u751F\u6210"));
235
+ activeSession.bddScenarios = generateBDDScenarios(
236
+ activeSession.refinedRequirement,
237
+ activeSession.context,
238
+ activeSession.clarificationQuestions,
239
+ activeSession.referenceResources
240
+ );
241
+ }
197
242
  for (const scenario of activeSession.bddScenarios) {
198
243
  lines.push(chalk9__default.default.white(` Feature: ${scenario.feature}`));
199
244
  for (const s of scenario.scenarios.slice(0, 3)) {
@@ -207,13 +252,14 @@ async function executeWorkflow(ctx) {
207
252
  }
208
253
  if (activeSession.phase === "spec") {
209
254
  lines.push("");
210
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 5/8: OpenSpec \u89C4\u683C \u2501\u2501\u2501"));
255
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 6/9: OpenSpec \u89C4\u683C \u2501\u2501\u2501"));
211
256
  lines.push("");
212
257
  activeSession.specItems = generateSpecItems(
213
258
  activeSession.refinedRequirement,
214
259
  activeSession.context,
215
260
  activeSession.bddScenarios,
216
- activeSession.clarificationQuestions
261
+ activeSession.clarificationQuestions,
262
+ activeSession.referenceResources
217
263
  );
218
264
  const specPath = await saveSpecFile(ctx.options.workingDirectory, activeSession);
219
265
  lines.push(chalk9__default.default.green(" \u2713 \u89C4\u683C\u6587\u4EF6\u5DF2\u751F\u6210"));
@@ -237,9 +283,9 @@ async function executeWorkflow(ctx) {
237
283
  }
238
284
  if (activeSession.phase === "tdd") {
239
285
  lines.push("");
240
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 6/8: TDD \u6D4B\u8BD5\u751F\u6210 \u2501\u2501\u2501"));
286
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 7/9: TDD \u6D4B\u8BD5\u751F\u6210 \u2501\u2501\u2501"));
241
287
  lines.push("");
242
- activeSession.testFiles = await generateTests(ctx.options.workingDirectory, activeSession);
288
+ activeSession.testFiles = await generateTests(ctx.options.workingDirectory, activeSession, ctx);
243
289
  lines.push(chalk9__default.default.green(" \u2713 \u6D4B\u8BD5\u6587\u4EF6\u5DF2\u751F\u6210"));
244
290
  for (const file of activeSession.testFiles) {
245
291
  lines.push(chalk9__default.default.gray(` - ${file}`));
@@ -248,7 +294,7 @@ async function executeWorkflow(ctx) {
248
294
  }
249
295
  if (activeSession.phase === "develop") {
250
296
  lines.push("");
251
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 7/8: \u5F00\u53D1\u5B9E\u73B0 \u2501\u2501\u2501"));
297
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 8/9: \u5F00\u53D1\u5B9E\u73B0 \u2501\u2501\u2501"));
252
298
  lines.push("");
253
299
  lines.push(chalk9__default.default.yellow(" \u{1F680} \u6B63\u5728\u8C03\u7528 AI \u751F\u6210\u4EE3\u7801..."));
254
300
  try {
@@ -273,7 +319,7 @@ async function executeWorkflow(ctx) {
273
319
  }
274
320
  if (activeSession.phase === "review") {
275
321
  lines.push("");
276
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 8/8: \u4EE3\u7801\u5BA1\u6838 \u2501\u2501\u2501"));
322
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 9/9: \u4EE3\u7801\u5BA1\u6838 \u2501\u2501\u2501"));
277
323
  lines.push("");
278
324
  lines.push(chalk9__default.default.yellow(" \u{1F50D} \u6B63\u5728\u8FDB\u884C\u4EE3\u7801\u5BA1\u6838..."));
279
325
  try {
@@ -536,6 +582,8 @@ function getCategoryLabel(category) {
536
582
  async function executeDevelopment(ctx, session) {
537
583
  const workingDir = ctx.options.workingDirectory;
538
584
  const files = [];
585
+ const loader = new LoadingIndicator("AI \u6B63\u5728\u751F\u6210\u4EE3\u7801");
586
+ loader.start();
539
587
  try {
540
588
  const systemPrompt = buildDevelopmentPrompt(session);
541
589
  const messages = [
@@ -563,11 +611,15 @@ ${session.context.devStandards.slice(0, 2e3)}` : ""}`
563
611
  content: systemPrompt
564
612
  }
565
613
  ];
614
+ loader.update("\u6B63\u5728\u8C03\u7528 AI \u6A21\u578B");
566
615
  const response = await ctx.modelService.sendMessage(messages, {
567
616
  temperature: 0.3,
568
617
  maxTokens: 8e3,
569
- agent: "frontend-dev"
618
+ agent: "frontend-dev",
619
+ timeout: 18e4
620
+ // 3 分钟超时
570
621
  });
622
+ loader.update("\u6B63\u5728\u89E3\u6790\u4EE3\u7801");
571
623
  const codeBlocks = parseCodeBlocks(response.content);
572
624
  for (const block of codeBlocks) {
573
625
  const filePath = path4__namespace.join(workingDir, block.filename);
@@ -596,8 +648,10 @@ export function ${featureName.replace(/[^a-zA-Z0-9]/g, "")}() {
596
648
  await fs4__namespace.writeFile(filePath, stubCode, "utf-8");
597
649
  files.push(`src/features/${fileName}`);
598
650
  }
651
+ loader.stop(chalk9__default.default.green(` \u2713 \u5DF2\u751F\u6210 ${files.length} \u4E2A\u6587\u4EF6`));
599
652
  return { success: true, files };
600
653
  } catch (error) {
654
+ loader.stop();
601
655
  return {
602
656
  success: false,
603
657
  files: [],
@@ -815,13 +869,41 @@ function analyzeComplexity(requirement, context) {
815
869
  if (!context.framework) score += 0.5;
816
870
  return Math.max(1, Math.min(10, Math.round(score)));
817
871
  }
818
- function generateBDDScenarios(requirement, context, questions) {
872
+ function generateBDDScenarios(requirement, context, questions, references = []) {
819
873
  const scenarios = [];
820
874
  questions.find((q) => q.category === "ui" && q.answered)?.answer;
821
875
  const interactionAnswer = questions.find((q) => q.category === "interaction" && q.answered)?.answer;
822
876
  const edgeAnswer = questions.find((q) => q.category === "edge" && q.answered)?.answer;
877
+ if (references.length > 0) {
878
+ for (const ref of references) {
879
+ const refFeatures = extractFeaturesFromReference(ref);
880
+ for (const feature of refFeatures) {
881
+ const scenario = {
882
+ feature: feature.title,
883
+ description: feature.description,
884
+ scenarios: []
885
+ };
886
+ scenario.scenarios.push({
887
+ name: `\u6B63\u5E38\u6D41\u7A0B: ${feature.title}`,
888
+ given: [`\u7528\u6237\u8FDB\u5165\u76F8\u5173\u9875\u9762`],
889
+ when: [`\u7528\u6237\u6267\u884C "${feature.title}" \u64CD\u4F5C`],
890
+ then: [`\u7CFB\u7EDF\u5E94\u6B63\u786E\u5904\u7406\u5E76\u8FD4\u56DE\u9884\u671F\u7ED3\u679C`]
891
+ });
892
+ if (feature.hasInput) {
893
+ scenario.scenarios.push({
894
+ name: `\u8FB9\u754C\u60C5\u51B5: \u8F93\u5165\u9A8C\u8BC1`,
895
+ given: [`\u7528\u6237\u8FDB\u5165\u8F93\u5165\u754C\u9762`],
896
+ when: [`\u7528\u6237\u8F93\u5165\u8FB9\u754C\u503C\u6216\u7A7A\u503C`],
897
+ then: [`\u7CFB\u7EDF\u5E94\u6B63\u786E\u5904\u7406\u8FB9\u754C\u60C5\u51B5`]
898
+ });
899
+ }
900
+ scenarios.push(scenario);
901
+ }
902
+ }
903
+ }
823
904
  const features = extractFeatures(requirement);
824
905
  for (const feature of features) {
906
+ if (scenarios.some((s) => s.feature === feature.title)) continue;
825
907
  const scenario = {
826
908
  feature: feature.title,
827
909
  description: feature.description,
@@ -853,6 +935,135 @@ function generateBDDScenarios(requirement, context, questions) {
853
935
  }
854
936
  return scenarios;
855
937
  }
938
+ async function generateBDDScenariosWithAI(requirement, context, questions, references, ctx) {
939
+ 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
940
+
941
+ ## \u9700\u6C42\u63CF\u8FF0
942
+ ${requirement}
943
+
944
+ ## \u9879\u76EE\u4E0A\u4E0B\u6587
945
+ - \u6280\u672F\u6808: ${context.techStack?.join(", ") || "\u672A\u6307\u5B9A"}
946
+ - \u6846\u67B6: ${context.framework || "\u672A\u6307\u5B9A"}
947
+
948
+ ## \u6F84\u6E05\u95EE\u7B54
949
+ ${questions.filter((q) => q.answered).map((q) => `- Q: ${q.question}
950
+ A: ${q.answer}`).join("\n")}
951
+
952
+ ## \u53C2\u8003\u8D44\u6E90\u5206\u6790
953
+ ${references.map((r) => `### ${r.url}
954
+ ${r.analysis}`).join("\n\n")}
955
+
956
+ ## \u8981\u6C42
957
+ 1. \u6BCF\u4E2A\u529F\u80FD\u6A21\u5757\u751F\u6210\u4E00\u4E2A\u72EC\u7ACB\u7684 Feature
958
+ 2. \u6BCF\u4E2A Feature \u5305\u542B\u591A\u4E2A\u5177\u4F53\u7684 Scenario
959
+ 3. \u4F7F\u7528 Given-When-Then \u683C\u5F0F
960
+ 4. \u573A\u666F\u8981\u8986\u76D6: \u6B63\u5E38\u6D41\u7A0B\u3001\u8FB9\u754C\u60C5\u51B5\u3001\u5F02\u5E38\u5904\u7406
961
+ 5. \u573A\u666F\u8981\u5177\u4F53\u53EF\u6D4B\u8BD5\uFF0C\u4E0D\u8981\u6CDB\u6CDB\u800C\u8C08
962
+
963
+ ## \u8F93\u51FA\u683C\u5F0F (JSON)
964
+ \`\`\`json
965
+ [
966
+ {
967
+ "feature": "\u529F\u80FD\u540D\u79F0",
968
+ "description": "\u529F\u80FD\u63CF\u8FF0",
969
+ "scenarios": [
970
+ {
971
+ "name": "\u573A\u666F\u540D\u79F0",
972
+ "given": ["\u524D\u7F6E\u6761\u4EF61", "\u524D\u7F6E\u6761\u4EF62"],
973
+ "when": ["\u64CD\u4F5C1", "\u64CD\u4F5C2"],
974
+ "then": ["\u9884\u671F\u7ED3\u679C1", "\u9884\u671F\u7ED3\u679C2"]
975
+ }
976
+ ]
977
+ }
978
+ ]
979
+ \`\`\`
980
+
981
+ \u8BF7\u76F4\u63A5\u8F93\u51FA JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u6709\u5176\u4ED6\u5185\u5BB9\u3002`;
982
+ const response = await ctx.modelService.sendMessage([
983
+ { role: "user", content: prompt2 }
984
+ ], {
985
+ temperature: 0.3,
986
+ maxTokens: 4e3,
987
+ timeout: 12e4
988
+ });
989
+ try {
990
+ const jsonMatch = response.content.match(/```json\s*([\s\S]*?)```/);
991
+ if (jsonMatch) {
992
+ return JSON.parse(jsonMatch[1].trim());
993
+ }
994
+ return JSON.parse(response.content);
995
+ } catch {
996
+ return generateBDDScenarios(requirement, context, questions, references);
997
+ }
998
+ }
999
+ function extractFeaturesFromReference(ref) {
1000
+ const features = [];
1001
+ const analysis = ref.analysis;
1002
+ const featureSection = analysis.match(/###?\s*4\.\s*功能拆分建议[\s\S]*?(?=###?\s*\d|$)/i);
1003
+ if (featureSection) {
1004
+ const taskMatches = featureSection[0].matchAll(/[-*]\s*\*\*([^*]+)\*\*[::]?\s*([^\n]+)/g);
1005
+ for (const match of taskMatches) {
1006
+ features.push({
1007
+ title: match[1].trim(),
1008
+ description: match[2].trim(),
1009
+ hasInput: match[2].includes("\u8F93\u5165") || match[2].includes("\u8868\u5355") || match[2].includes("\u7528\u6237")
1010
+ });
1011
+ }
1012
+ }
1013
+ const bizSection = analysis.match(/###?\s*1\.\s*业务功能分析[\s\S]*?(?=###?\s*\d|$)/i);
1014
+ if (bizSection && features.length === 0) {
1015
+ const lines = bizSection[0].split("\n").filter((l) => l.trim().startsWith("-") || l.trim().startsWith("*"));
1016
+ for (const line of lines.slice(0, 5)) {
1017
+ const content = line.replace(/^[-*]\s*/, "").trim();
1018
+ if (content.length > 5) {
1019
+ features.push({
1020
+ title: content.slice(0, 20),
1021
+ description: content,
1022
+ hasInput: content.includes("\u8F93\u5165") || content.includes("\u586B\u5199")
1023
+ });
1024
+ }
1025
+ }
1026
+ }
1027
+ if (features.length === 0) {
1028
+ const lowerAnalysis = analysis.toLowerCase();
1029
+ if (lowerAnalysis.includes("\u8F93\u5165") || lowerAnalysis.includes("\u8868\u5355")) {
1030
+ features.push({
1031
+ title: "\u8F93\u5165\u8868\u5355",
1032
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u8F93\u5165\u8868\u5355\u529F\u80FD",
1033
+ hasInput: true
1034
+ });
1035
+ }
1036
+ if (lowerAnalysis.includes("\u6309\u94AE") || lowerAnalysis.includes("\u64CD\u4F5C")) {
1037
+ features.push({
1038
+ title: "\u4EA4\u4E92\u6309\u94AE",
1039
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u6309\u94AE\u4EA4\u4E92",
1040
+ hasInput: false
1041
+ });
1042
+ }
1043
+ if (lowerAnalysis.includes("\u8868\u683C") || lowerAnalysis.includes("\u5217\u8868")) {
1044
+ features.push({
1045
+ title: "\u6570\u636E\u5217\u8868",
1046
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u6570\u636E\u5C55\u793A",
1047
+ hasInput: false
1048
+ });
1049
+ }
1050
+ if (lowerAnalysis.includes("\u56FE\u8868") || lowerAnalysis.includes("\u53EF\u89C6\u5316")) {
1051
+ features.push({
1052
+ title: "\u56FE\u8868\u5C55\u793A",
1053
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u56FE\u8868\u53EF\u89C6\u5316",
1054
+ hasInput: false
1055
+ });
1056
+ }
1057
+ }
1058
+ if (features.length === 0) {
1059
+ features.push({
1060
+ title: "\u53C2\u8003\u529F\u80FD\u5B9E\u73B0",
1061
+ description: `\u57FA\u4E8E\u53C2\u8003\u8D44\u6E90 ${ref.url} \u5B9E\u73B0\u7684\u529F\u80FD`,
1062
+ hasInput: true
1063
+ });
1064
+ }
1065
+ return features;
1066
+ }
856
1067
  function extractFeatures(requirement) {
857
1068
  const features = [];
858
1069
  const urlMatch = requirement.match(/https?:\/\/[^\s]+/);
@@ -893,15 +1104,26 @@ function extractFeatures(requirement) {
893
1104
  }
894
1105
  return features;
895
1106
  }
896
- function generateSpecItems(requirement, context, bddScenarios, questions) {
1107
+ function generateSpecItems(requirement, context, bddScenarios, questions, references = []) {
897
1108
  const items = [];
898
1109
  let id = 1;
1110
+ for (const ref of references) {
1111
+ items.push({
1112
+ id: `T${id.toString().padStart(3, "0")}`,
1113
+ title: `\u53C2\u8003\u5206\u6790: ${ref.type}`,
1114
+ description: `\u5206\u6790\u53C2\u8003\u8D44\u6E90 ${ref.url}`,
1115
+ priority: "high",
1116
+ files: [],
1117
+ tests: []
1118
+ });
1119
+ id++;
1120
+ }
899
1121
  for (const scenario of bddScenarios) {
900
1122
  items.push({
901
1123
  id: `T${id.toString().padStart(3, "0")}`,
902
1124
  title: scenario.feature,
903
1125
  description: scenario.description,
904
- priority: id <= 2 ? "high" : "medium",
1126
+ priority: id <= 3 ? "high" : "medium",
905
1127
  files: [],
906
1128
  tests: []
907
1129
  });
@@ -944,6 +1166,19 @@ function formatSpecFile(session) {
944
1166
  lines.push("---");
945
1167
  lines.push("");
946
1168
  }
1169
+ if (session.referenceResources.length > 0) {
1170
+ lines.push("## \u53C2\u8003\u8D44\u6E90");
1171
+ lines.push("");
1172
+ for (const ref of session.referenceResources) {
1173
+ lines.push(`### ${ref.url}`);
1174
+ lines.push(`> \u7C7B\u578B: ${ref.type}`);
1175
+ lines.push("");
1176
+ lines.push(ref.analysis);
1177
+ lines.push("");
1178
+ }
1179
+ lines.push("---");
1180
+ lines.push("");
1181
+ }
947
1182
  if (session.clarificationQuestions.some((q) => q.answered)) {
948
1183
  lines.push("## \u9700\u6C42\u6F84\u6E05");
949
1184
  lines.push("");
@@ -982,33 +1217,119 @@ function formatSpecFile(session) {
982
1217
  lines.push("**\u786E\u8BA4\u72B6\u6001**: \u23F3 \u7B49\u5F85\u786E\u8BA4");
983
1218
  return lines.join("\n");
984
1219
  }
985
- async function generateTests(workingDir, session) {
1220
+ async function generateTests(workingDir, session, ctx) {
986
1221
  const testDir = path4__namespace.join(workingDir, "tests");
987
1222
  await fs4__namespace.mkdir(testDir, { recursive: true });
988
1223
  const testFiles = [];
989
- for (const scenario of session.bddScenarios) {
990
- const testName = scenario.feature.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "_");
991
- const testPath = path4__namespace.join(testDir, `${testName}.test.ts`);
992
- const content = generateTestFile(scenario);
993
- await fs4__namespace.writeFile(testPath, content, "utf-8");
994
- testFiles.push(`tests/${testName}.test.ts`);
1224
+ if (ctx?.modelService) {
1225
+ for (const scenario of session.bddScenarios) {
1226
+ const testName = scenario.feature.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "_");
1227
+ const testPath = path4__namespace.join(testDir, `${testName}.test.ts`);
1228
+ const loader = new LoadingIndicator(`\u751F\u6210\u6D4B\u8BD5: ${scenario.feature.slice(0, 20)}...`);
1229
+ loader.start();
1230
+ try {
1231
+ const content = await generateTestFileWithAI(scenario, session, ctx);
1232
+ await fs4__namespace.writeFile(testPath, content, "utf-8");
1233
+ testFiles.push(`tests/${testName}.test.ts`);
1234
+ loader.stop(chalk9__default.default.green(` \u2713 \u6D4B\u8BD5\u6587\u4EF6\u5DF2\u751F\u6210`));
1235
+ } catch {
1236
+ const content = generateTestFile(scenario, session);
1237
+ await fs4__namespace.writeFile(testPath, content, "utf-8");
1238
+ testFiles.push(`tests/${testName}.test.ts`);
1239
+ loader.stop(chalk9__default.default.yellow(" \u26A0 \u4F7F\u7528\u57FA\u7840\u6D4B\u8BD5\u6A21\u677F"));
1240
+ }
1241
+ }
1242
+ } else {
1243
+ for (const scenario of session.bddScenarios) {
1244
+ const testName = scenario.feature.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "_");
1245
+ const testPath = path4__namespace.join(testDir, `${testName}.test.ts`);
1246
+ const content = generateTestFile(scenario, session);
1247
+ await fs4__namespace.writeFile(testPath, content, "utf-8");
1248
+ testFiles.push(`tests/${testName}.test.ts`);
1249
+ }
995
1250
  }
996
1251
  return testFiles;
997
1252
  }
998
- function generateTestFile(scenario) {
1253
+ async function generateTestFileWithAI(scenario, session, ctx) {
1254
+ 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
1255
+
1256
+ ## \u529F\u80FD\u540D\u79F0
1257
+ ${scenario.feature}
1258
+
1259
+ ## BDD \u573A\u666F
1260
+ ${scenario.scenarios.map((s) => `
1261
+ ### ${s.name}
1262
+ - Given: ${s.given.join(", ")}
1263
+ - When: ${s.when.join(", ")}
1264
+ - Then: ${s.then.join(", ")}
1265
+ `).join("\n")}
1266
+
1267
+ ## \u9879\u76EE\u4E0A\u4E0B\u6587
1268
+ - \u6280\u672F\u6808: ${session.context?.techStack?.join(", ") || "TypeScript"}
1269
+ - \u6846\u67B6: ${session.context?.framework || "\u672A\u6307\u5B9A"}
1270
+
1271
+ ## \u8981\u6C42
1272
+ 1. \u4F7F\u7528 vitest \u6D4B\u8BD5\u6846\u67B6 (describe, it, expect, beforeEach \u7B49)
1273
+ 2. \u6BCF\u4E2A\u573A\u666F\u751F\u6210\u4E00\u4E2A\u72EC\u7ACB\u7684\u6D4B\u8BD5\u7528\u4F8B
1274
+ 3. \u6D4B\u8BD5\u4EE3\u7801\u8981\u5B8C\u6574\u53EF\u8FD0\u884C\uFF0C\u5305\u542B\u5FC5\u8981\u7684 mock \u548C setup
1275
+ 4. \u4F7F\u7528\u4E2D\u6587\u6CE8\u91CA\u8BF4\u660E\u6D4B\u8BD5\u610F\u56FE
1276
+ 5. \u6D4B\u8BD5\u8981\u8986\u76D6\u6B63\u5E38\u6D41\u7A0B\u548C\u8FB9\u754C\u60C5\u51B5
1277
+
1278
+ \u8BF7\u76F4\u63A5\u8F93\u51FA\u6D4B\u8BD5\u4EE3\u7801\uFF0C\u4E0D\u9700\u8981\u89E3\u91CA\u3002`;
1279
+ const response = await ctx.modelService.sendMessage([
1280
+ { role: "user", content: prompt2 }
1281
+ ], {
1282
+ temperature: 0.3,
1283
+ maxTokens: 4e3
1284
+ });
1285
+ const codeMatch = response.content.match(/```(?:typescript|ts|javascript|js)?\n([\s\S]*?)```/);
1286
+ if (codeMatch) {
1287
+ return codeMatch[1].trim();
1288
+ }
1289
+ return response.content;
1290
+ }
1291
+ function generateTestFile(scenario, session) {
999
1292
  const lines = [];
1000
- lines.push(`import { describe, it, expect } from 'vitest';`);
1293
+ lines.push(`import { describe, it, expect, beforeEach } from 'vitest';`);
1001
1294
  lines.push("");
1295
+ lines.push(`/**`);
1296
+ lines.push(` * ${scenario.feature} \u529F\u80FD\u6D4B\u8BD5`);
1297
+ lines.push(` * `);
1298
+ lines.push(` * BDD \u573A\u666F\u6570\u91CF: ${scenario.scenarios.length}`);
1299
+ if (session?.context?.techStack) {
1300
+ lines.push(` * \u6280\u672F\u6808: ${session.context.techStack.join(", ")}`);
1301
+ }
1302
+ lines.push(` */`);
1002
1303
  lines.push(`describe('${scenario.feature}', () => {`);
1003
1304
  for (const s of scenario.scenarios) {
1004
- lines.push(` it('${s.name}', () => {`);
1005
- lines.push(` // Given: ${s.given.join(", ")}`);
1006
- lines.push(` // When: ${s.when.join(", ")}`);
1007
- lines.push(` // Then: ${s.then.join(", ")}`);
1008
- lines.push(` expect(true).toBe(true); // TODO: \u5B9E\u73B0\u6D4B\u8BD5`);
1009
- lines.push(` });`);
1010
1305
  lines.push("");
1306
+ lines.push(` /**`);
1307
+ lines.push(` * \u573A\u666F: ${s.name}`);
1308
+ lines.push(` * Given: ${s.given.join(", ")}`);
1309
+ lines.push(` * When: ${s.when.join(", ")}`);
1310
+ lines.push(` * Then: ${s.then.join(", ")}`);
1311
+ lines.push(` */`);
1312
+ lines.push(` it('${s.name}', async () => {`);
1313
+ lines.push(` // Arrange (Given)`);
1314
+ for (const g of s.given) {
1315
+ lines.push(` // ${g}`);
1316
+ }
1317
+ lines.push(` const input = {}; // TODO: \u8BBE\u7F6E\u521D\u59CB\u72B6\u6001`);
1318
+ lines.push("");
1319
+ lines.push(` // Act (When)`);
1320
+ for (const w of s.when) {
1321
+ lines.push(` // ${w}`);
1322
+ }
1323
+ lines.push(` const result = {}; // TODO: \u6267\u884C\u64CD\u4F5C`);
1324
+ lines.push("");
1325
+ lines.push(` // Assert (Then)`);
1326
+ for (const t of s.then) {
1327
+ lines.push(` // ${t}`);
1328
+ }
1329
+ lines.push(` expect(result).toBeDefined(); // TODO: \u5B8C\u5584\u65AD\u8A00`);
1330
+ lines.push(` });`);
1011
1331
  }
1332
+ lines.push("");
1012
1333
  lines.push(`});`);
1013
1334
  return lines.join("\n");
1014
1335
  }
@@ -1049,6 +1370,124 @@ function generateSessionId() {
1049
1370
  const random = Math.random().toString(36).slice(2, 6);
1050
1371
  return `WF-${timestamp}-${random}`.toUpperCase();
1051
1372
  }
1373
+ function extractUrls(text) {
1374
+ const urlRegex = /https?:\/\/[^\s<>"{}|\\^`\[\]]+/gi;
1375
+ const matches = text.match(urlRegex);
1376
+ return matches ? [...new Set(matches)] : [];
1377
+ }
1378
+ async function fetchAndAnalyzeReference(url, ctx) {
1379
+ const type = detectResourceType(url);
1380
+ let content = "";
1381
+ let analysis = "";
1382
+ try {
1383
+ const response = await fetch(url, {
1384
+ headers: {
1385
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
1386
+ }
1387
+ });
1388
+ if (!response.ok) {
1389
+ throw new Error(`HTTP ${response.status}`);
1390
+ }
1391
+ content = await response.text();
1392
+ if (ctx.modelService.getCurrentModel()) {
1393
+ analysis = await analyzeReferenceContent(url, content, type, ctx);
1394
+ } else {
1395
+ analysis = extractBasicInfo(content, type);
1396
+ }
1397
+ } catch (error) {
1398
+ throw new Error(`\u65E0\u6CD5\u83B7\u53D6\u53C2\u8003\u8D44\u6E90: ${error.message}`);
1399
+ }
1400
+ return { url, type, content: content.slice(0, 1e4), analysis };
1401
+ }
1402
+ function detectResourceType(url) {
1403
+ if (url.includes("figma.com") || url.includes("lanhuapp.com")) {
1404
+ return "design";
1405
+ }
1406
+ if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(url)) {
1407
+ return "image";
1408
+ }
1409
+ if (/api\//i.test(url)) {
1410
+ return "api";
1411
+ }
1412
+ return "webpage";
1413
+ }
1414
+ async function analyzeReferenceContent(url, content, type, ctx) {
1415
+ const prompt2 = `
1416
+ \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
1417
+
1418
+ ## \u53C2\u8003\u8D44\u6E90\u4FE1\u606F
1419
+ - URL: ${url}
1420
+ - \u7C7B\u578B: ${type}
1421
+
1422
+ ## \u7F51\u9875\u5185\u5BB9
1423
+ \`\`\`html
1424
+ ${content.slice(0, 8e3)}
1425
+ \`\`\`
1426
+
1427
+ ## \u5206\u6790\u8981\u6C42
1428
+
1429
+ \u8BF7\u6309\u7167\u4EE5\u4E0B\u7ED3\u6784\u8FDB\u884C\u8BE6\u7EC6\u5206\u6790\uFF1A
1430
+
1431
+ ### 1. \u4E1A\u52A1\u529F\u80FD\u5206\u6790
1432
+ - \u6838\u5FC3\u4E1A\u52A1\u529F\u80FD\u662F\u4EC0\u4E48\uFF1F\uFF08\u8BE6\u7EC6\u63CF\u8FF0\uFF09
1433
+ - \u7528\u6237\u53EF\u4EE5\u505A\u4EC0\u4E48\u64CD\u4F5C\uFF1F
1434
+ - \u4E1A\u52A1\u6D41\u7A0B\u662F\u4EC0\u4E48\uFF1F
1435
+ - \u6570\u636E\u8F93\u5165\u8F93\u51FA\u662F\u4EC0\u4E48\uFF1F
1436
+
1437
+ ### 2. UI/UX \u7ED3\u6784\u5206\u6790
1438
+ - \u9875\u9762\u5E03\u5C40\u7ED3\u6784
1439
+ - \u4E3B\u8981\u7EC4\u4EF6\u6709\u54EA\u4E9B\uFF1F
1440
+ - \u7EC4\u4EF6\u4E4B\u95F4\u7684\u5173\u7CFB
1441
+ - \u4EA4\u4E92\u65B9\u5F0F\uFF08\u70B9\u51FB\u3001\u8F93\u5165\u3001\u62D6\u62FD\u7B49\uFF09
1442
+
1443
+ ### 3. \u6570\u636E\u6A21\u578B\u5206\u6790
1444
+ - \u9700\u8981\u54EA\u4E9B\u6570\u636E\uFF1F
1445
+ - \u6570\u636E\u4E4B\u95F4\u7684\u5173\u7CFB
1446
+ - \u6570\u636E\u6765\u6E90\uFF08\u7528\u6237\u8F93\u5165/\u8BA1\u7B97/API\uFF09
1447
+
1448
+ ### 4. \u529F\u80FD\u62C6\u5206\u5EFA\u8BAE
1449
+ \u8BF7\u5C06\u529F\u80FD\u62C6\u5206\u4E3A\u53EF\u72EC\u7ACB\u5F00\u53D1\u7684\u4EFB\u52A1\uFF0C\u6BCF\u4E2A\u4EFB\u52A1\u5305\u542B\uFF1A
1450
+ - \u4EFB\u52A1\u540D\u79F0
1451
+ - \u4EFB\u52A1\u63CF\u8FF0
1452
+ - \u6280\u672F\u8981\u70B9
1453
+ - \u4F9D\u8D56\u5173\u7CFB
1454
+
1455
+ ### 5. \u6280\u672F\u5B9E\u73B0\u5EFA\u8BAE
1456
+ - \u63A8\u8350\u7684\u6280\u672F\u65B9\u6848
1457
+ - \u9700\u8981\u6CE8\u610F\u7684\u6280\u672F\u96BE\u70B9
1458
+ - \u6027\u80FD\u4F18\u5316\u5EFA\u8BAE
1459
+
1460
+ \u8BF7\u4EE5 Markdown \u683C\u5F0F\u8F93\u51FA\uFF0C\u91CD\u70B9\u7A81\u51FA\u4E1A\u52A1\u903B\u8F91\u548C\u529F\u80FD\u5B9E\u73B0\u7EC6\u8282\u3002
1461
+ `;
1462
+ const loader = new LoadingIndicator("AI \u6B63\u5728\u5206\u6790\u53C2\u8003\u8D44\u6E90");
1463
+ loader.start();
1464
+ try {
1465
+ const response = await ctx.modelService.sendMessage([
1466
+ { role: "user", content: prompt2 }
1467
+ ], {
1468
+ temperature: 0.3,
1469
+ maxTokens: 4e3
1470
+ });
1471
+ loader.stop(chalk9__default.default.green(" \u2713 \u5206\u6790\u5B8C\u6210"));
1472
+ return response.content;
1473
+ } catch (error) {
1474
+ loader.stop();
1475
+ throw error;
1476
+ }
1477
+ }
1478
+ function extractBasicInfo(content, type) {
1479
+ const titleMatch = content.match(/<title[^>]*>([^<]+)<\/title>/i);
1480
+ const descMatch = content.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i);
1481
+ const parts = [];
1482
+ if (titleMatch) {
1483
+ parts.push(`\u6807\u9898: ${titleMatch[1]}`);
1484
+ }
1485
+ if (descMatch) {
1486
+ parts.push(`\u63CF\u8FF0: ${descMatch[1]}`);
1487
+ }
1488
+ parts.push(`\u8D44\u6E90\u7C7B\u578B: ${type}`);
1489
+ return parts.join("\n");
1490
+ }
1052
1491
  function generateComplexityBar(score) {
1053
1492
  const filled = Math.round(score / 2);
1054
1493
  const empty = 5 - filled;
@@ -1058,6 +1497,7 @@ function getPhaseLabel(phase) {
1058
1497
  const labels = {
1059
1498
  context: "\u9879\u76EE\u4E0A\u4E0B\u6587\u83B7\u53D6",
1060
1499
  clarify: "\u9700\u6C42\u6F84\u6E05",
1500
+ reference: "\u53C2\u8003\u8D44\u6E90\u5206\u6790",
1061
1501
  analysis: "\u590D\u6742\u5EA6\u8BC4\u4F30",
1062
1502
  bdd: "BDD \u573A\u666F\u62C6\u89E3",
1063
1503
  spec: "OpenSpec \u89C4\u683C",
@@ -1073,10 +1513,43 @@ function getActiveSession() {
1073
1513
  function clearActiveSession() {
1074
1514
  activeSession = null;
1075
1515
  }
1076
- var MAX_FILE_SIZE2, COMPLEXITY_THRESHOLD, CLARITY_THRESHOLD, activeSession, new_default;
1516
+ var LoadingIndicator, MAX_FILE_SIZE2, COMPLEXITY_THRESHOLD, CLARITY_THRESHOLD, activeSession, new_default;
1077
1517
  var init_new = __esm({
1078
1518
  "src/commands/new.ts"() {
1079
1519
  init_cjs_shims();
1520
+ LoadingIndicator = class {
1521
+ frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1522
+ frameIndex = 0;
1523
+ interval = null;
1524
+ message;
1525
+ constructor(message) {
1526
+ this.message = message;
1527
+ }
1528
+ start() {
1529
+ process.stdout.write("\x1B[?25l");
1530
+ this.interval = setInterval(() => {
1531
+ const frame = this.frames[this.frameIndex];
1532
+ process.stdout.write(`\r${chalk9__default.default.cyan(frame)} ${this.message}...`);
1533
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
1534
+ }, 80);
1535
+ }
1536
+ update(message) {
1537
+ this.message = message;
1538
+ }
1539
+ stop(finalMessage) {
1540
+ if (this.interval) {
1541
+ clearInterval(this.interval);
1542
+ this.interval = null;
1543
+ }
1544
+ process.stdout.write("\x1B[?25h");
1545
+ if (finalMessage) {
1546
+ process.stdout.write(`\r${finalMessage}
1547
+ `);
1548
+ } else {
1549
+ process.stdout.write("\r" + " ".repeat(60) + "\r");
1550
+ }
1551
+ }
1552
+ };
1080
1553
  MAX_FILE_SIZE2 = 1024 * 1024;
1081
1554
  COMPLEXITY_THRESHOLD = 6;
1082
1555
  CLARITY_THRESHOLD = 0.6;