@nick848/sf-cli 1.0.19 → 1.0.21

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
@@ -229,23 +229,38 @@ ${resource.analysis}`;
229
229
  lines.push("");
230
230
  lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 6/9: OpenSpec \u89C4\u683C \u2501\u2501\u2501"));
231
231
  lines.push("");
232
- activeSession.specItems = generateSpecItems(
233
- activeSession.refinedRequirement,
234
- activeSession.context,
235
- activeSession.bddScenarios,
236
- activeSession.clarificationQuestions,
237
- activeSession.referenceResources
238
- );
232
+ const loader = new LoadingIndicator("AI \u6B63\u5728\u62C6\u5206\u89C4\u683C");
233
+ loader.start();
234
+ try {
235
+ activeSession.specItems = await generateSpecItemsWithAI(
236
+ activeSession.refinedRequirement,
237
+ activeSession.context,
238
+ activeSession.bddScenarios,
239
+ activeSession.clarificationQuestions,
240
+ activeSession.referenceResources,
241
+ ctx
242
+ );
243
+ loader.stop();
244
+ } catch {
245
+ loader.stop(chalk9.yellow(" \u26A0 \u4F7F\u7528\u57FA\u7840\u89C4\u683C\u62C6\u5206"));
246
+ activeSession.specItems = generateSpecItems(
247
+ activeSession.refinedRequirement,
248
+ activeSession.context,
249
+ activeSession.bddScenarios,
250
+ activeSession.clarificationQuestions,
251
+ activeSession.referenceResources
252
+ );
253
+ }
239
254
  const specPath = await saveSpecFile(ctx.options.workingDirectory, activeSession);
240
255
  lines.push(chalk9.green(" \u2713 \u89C4\u683C\u6587\u4EF6\u5DF2\u751F\u6210"));
241
256
  lines.push(chalk9.gray(` \u8DEF\u5F84: ${specPath}`));
242
257
  lines.push("");
243
258
  lines.push(chalk9.cyan(" \u4EFB\u52A1\u6982\u89C8:"));
244
- for (const item of activeSession.specItems.slice(0, 5)) {
259
+ for (const item of activeSession.specItems.slice(0, 8)) {
245
260
  const icon = item.priority === "high" ? "\u{1F534}" : item.priority === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
246
261
  lines.push(chalk9.gray(` ${icon} [${item.id}] ${item.title}`));
247
262
  }
248
- if (activeSession.specItems.length > 5) {
263
+ if (activeSession.specItems.length > 8) {
249
264
  lines.push(chalk9.gray(` ... \u5171 ${activeSession.specItems.length} \u4E2A\u4EFB\u52A1`));
250
265
  }
251
266
  lines.push("");
@@ -392,13 +407,15 @@ async function handleWorkflowInput(input, ctx) {
392
407
  activeSession.bddScenarios = generateBDDScenarios(
393
408
  activeSession.refinedRequirement,
394
409
  activeSession.context,
395
- activeSession.clarificationQuestions
410
+ activeSession.clarificationQuestions,
411
+ activeSession.referenceResources
396
412
  );
397
413
  activeSession.specItems = generateSpecItems(
398
414
  activeSession.refinedRequirement,
399
415
  activeSession.context,
400
416
  activeSession.bddScenarios,
401
- activeSession.clarificationQuestions
417
+ activeSession.clarificationQuestions,
418
+ activeSession.referenceResources
402
419
  );
403
420
  const specPath = await saveSpecFile(ctx.options.workingDirectory, activeSession);
404
421
  return {
@@ -557,107 +574,115 @@ function getCategoryLabel(category) {
557
574
  async function executeDevelopment(ctx, session) {
558
575
  const workingDir = ctx.options.workingDirectory;
559
576
  const files = [];
560
- const loader = new LoadingIndicator("AI \u6B63\u5728\u751F\u6210\u4EE3\u7801");
561
- loader.start();
562
- try {
563
- const systemPrompt = buildDevelopmentPrompt(session);
564
- const messages = [
565
- {
566
- role: "system",
567
- content: `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u524D\u7AEF\u5F00\u53D1\u5DE5\u7A0B\u5E08\u3002\u8BF7\u6839\u636E\u9700\u6C42\u89C4\u683C\u751F\u6210\u4EE3\u7801\u5B9E\u73B0\u3002
577
+ console.log("");
578
+ for (let i = 0; i < session.specItems.length; i++) {
579
+ const item = session.specItems[i];
580
+ const prefix = `[${i + 1}/${session.specItems.length}]`;
581
+ if (item.title.includes("\u6D4B\u8BD5") || item.title.includes("test")) {
582
+ continue;
583
+ }
584
+ const loader = new LoadingIndicator(`${prefix} \u751F\u6210: ${item.title.slice(0, 20)}...`);
585
+ loader.start();
586
+ try {
587
+ const taskPrompt = buildTaskPrompt(session, item, i);
588
+ const messages = [
589
+ {
590
+ role: "system",
591
+ content: `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u524D\u7AEF\u5F00\u53D1\u5DE5\u7A0B\u5E08\u3002\u8BF7\u6839\u636E\u4EFB\u52A1\u63CF\u8FF0\u751F\u6210\u4EE3\u7801\u5B9E\u73B0\u3002
568
592
 
569
593
  \u26A0\uFE0F \u91CD\u8981\u89C4\u5219\uFF1A
570
- 1. \u6280\u672F\u5B9E\u73B0\u5FC5\u987B\u4E25\u683C\u9075\u5FAA\u9879\u76EE\u73B0\u6709\u7684\u5F00\u53D1\u89C4\u8303
571
- 2. \u53C2\u8003\u8D44\u6E90\uFF08\u5982\u679C\u6709\uFF09\u4EC5\u7528\u4E8E\u7406\u89E3\u4E1A\u52A1\u529F\u80FD\uFF0C\u4E0D\u8981\u590D\u5236\u5176\u6280\u672F\u5B9E\u73B0
572
- 3. \u4F7F\u7528\u9879\u76EE\u6307\u5B9A\u7684\u6280\u672F\u6808\u548C\u6846\u67B6
573
-
574
- \u4EE3\u7801\u8981\u6C42\uFF1A
575
- 1. \u751F\u6210\u5B8C\u6574\u3001\u53EF\u8FD0\u884C\u7684\u4EE3\u7801
576
- 2. \u9075\u5FAA\u9879\u76EE\u73B0\u6709\u7684\u4EE3\u7801\u98CE\u683C\u548C\u89C4\u8303
577
- 3. \u4F7F\u7528\u9879\u76EE\u6307\u5B9A\u7684\u6280\u672F\u6808
578
- 4. \u4EE3\u7801\u8981\u6709\u9002\u5F53\u7684\u6CE8\u91CA
579
- 5. \u8FD4\u56DE\u683C\u5F0F\uFF1A\u6BCF\u4E2A\u6587\u4EF6\u7528 \`\`\`filename \u4EE3\u7801 \`\`\` \u5305\u88F9
594
+ 1. \u53EA\u751F\u6210\u5F53\u524D\u4EFB\u52A1\u76F8\u5173\u7684\u4EE3\u7801\uFF0C\u4E0D\u8981\u751F\u6210\u5176\u4ED6\u4EFB\u52A1\u7684\u4EE3\u7801
595
+ 2. \u6280\u672F\u5B9E\u73B0\u5FC5\u987B\u4E25\u683C\u9075\u5FAA\u9879\u76EE\u73B0\u6709\u7684\u5F00\u53D1\u89C4\u8303
596
+ 3. \u751F\u6210\u5B8C\u6574\u3001\u53EF\u8FD0\u884C\u7684\u4EE3\u7801
597
+ 4. \u8FD4\u56DE\u683C\u5F0F\uFF1A\u6BCF\u4E2A\u6587\u4EF6\u7528 \`\`\`filename \u4EE3\u7801 \`\`\` \u5305\u88F9
580
598
 
581
599
  \u9879\u76EE\u4FE1\u606F\uFF1A
582
600
  - \u540D\u79F0: ${session.context?.name}
583
601
  - \u6846\u67B6: ${session.context?.framework || "\u672A\u6307\u5B9A"}
584
602
  - \u6280\u672F\u6808: ${session.context?.techStack.join(", ") || "\u672A\u6307\u5B9A"}
585
603
 
586
- ${session.context?.devStandards ? `\u3010\u5FC5\u987B\u9075\u5FAA\u7684\u5F00\u53D1\u89C4\u8303\u3011
587
- ${session.context.devStandards.slice(0, 2500)}` : ""}`
588
- },
589
- {
590
- role: "user",
591
- content: systemPrompt
604
+ ${session.context?.devStandards ? `\u3010\u5F00\u53D1\u89C4\u8303\u3011
605
+ ${session.context.devStandards.slice(0, 2e3)}` : ""}`
606
+ },
607
+ {
608
+ role: "user",
609
+ content: taskPrompt
610
+ }
611
+ ];
612
+ const response = await ctx.modelService.sendMessage(messages, {
613
+ temperature: 0.3,
614
+ maxTokens: 4e3,
615
+ // 单个任务减少 token
616
+ agent: "frontend-dev",
617
+ timeout: 12e4
618
+ // 2分钟超时
619
+ });
620
+ const codeBlocks = parseCodeBlocks(response.content);
621
+ if (codeBlocks.length > 0) {
622
+ for (const block of codeBlocks) {
623
+ const filePath = path5.join(workingDir, block.filename);
624
+ const dir = path5.dirname(filePath);
625
+ await fs4.mkdir(dir, { recursive: true });
626
+ await fs4.writeFile(filePath, block.code, "utf-8");
627
+ files.push(block.filename);
628
+ }
629
+ loader.stop(chalk9.green(`${prefix} \u2713 ${item.title.slice(0, 25)} (${codeBlocks.length} \u4E2A\u6587\u4EF6)`));
630
+ } else {
631
+ const implDir = path5.join(workingDir, "src");
632
+ await fs4.mkdir(implDir, { recursive: true });
633
+ const fileName = `${item.title.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "_")}.ts`;
634
+ const filePath = path5.join(implDir, fileName);
635
+ await fs4.writeFile(filePath, `// TODO: ${item.title}
636
+ // ${item.description}
637
+ `, "utf-8");
638
+ files.push(`src/${fileName}`);
639
+ loader.stop(chalk9.yellow(`${prefix} \u26A0 \u751F\u6210\u57FA\u7840\u6A21\u677F: ${item.title.slice(0, 20)}`));
640
+ }
641
+ if (i < session.specItems.length - 1) {
642
+ await new Promise((resolve4) => setTimeout(resolve4, 500));
592
643
  }
593
- ];
594
- loader.update("\u6B63\u5728\u8C03\u7528 AI \u6A21\u578B");
595
- const response = await ctx.modelService.sendMessage(messages, {
596
- temperature: 0.3,
597
- maxTokens: 8e3,
598
- agent: "frontend-dev",
599
- timeout: 18e4
600
- // 3 分钟超时
601
- });
602
- loader.update("\u6B63\u5728\u89E3\u6790\u4EE3\u7801");
603
- const codeBlocks = parseCodeBlocks(response.content);
604
- for (const block of codeBlocks) {
605
- const filePath = path5.join(workingDir, block.filename);
606
- const dir = path5.dirname(filePath);
607
- await fs4.mkdir(dir, { recursive: true });
608
- await fs4.writeFile(filePath, block.code, "utf-8");
609
- files.push(block.filename);
610
- }
611
- if (files.length === 0) {
612
- const implDir = path5.join(workingDir, "src", "features");
613
- await fs4.mkdir(implDir, { recursive: true });
614
- const featureName = session.specItems[0]?.title || "feature";
615
- const fileName = `${featureName.replace(/[^a-zA-Z0-9]/g, "_")}.ts`;
616
- const filePath = path5.join(implDir, fileName);
617
- const stubCode = `/**
618
- * ${session.requirement}
619
- *
620
- * TODO: \u6B64\u6587\u4EF6\u7531 AI \u81EA\u52A8\u751F\u6210\uFF0C\u8BF7\u6839\u636E\u9700\u6C42\u5B8C\u5584\u5B9E\u73B0
621
- */
622
-
623
- export function ${featureName.replace(/[^a-zA-Z0-9]/g, "")}() {
624
- // TODO: \u5B9E\u73B0\u529F\u80FD
625
- console.log('${featureName} - \u5F85\u5B9E\u73B0');
626
- }
627
- `;
628
- await fs4.writeFile(filePath, stubCode, "utf-8");
629
- files.push(`src/features/${fileName}`);
644
+ } catch (error) {
645
+ loader.stop(chalk9.red(`${prefix} \u2717 \u5931\u8D25: ${error.message.slice(0, 40)}`));
630
646
  }
631
- loader.stop(chalk9.green(` \u2713 \u5DF2\u751F\u6210 ${files.length} \u4E2A\u6587\u4EF6`));
647
+ }
648
+ if (files.length > 0) {
632
649
  return { success: true, files };
633
- } catch (error) {
634
- loader.stop();
650
+ } else {
635
651
  return {
636
652
  success: false,
637
653
  files: [],
638
- error: error.message
654
+ error: "\u6240\u6709\u4EFB\u52A1\u4EE3\u7801\u751F\u6210\u5931\u8D25"
639
655
  };
640
656
  }
641
657
  }
642
- function buildDevelopmentPrompt(session) {
658
+ function buildTaskPrompt(session, item, index) {
643
659
  const lines = [];
644
- lines.push("## \u9700\u6C42\u63CF\u8FF0");
645
- lines.push(session.refinedRequirement);
646
- lines.push("");
647
- lines.push("## BDD \u573A\u666F");
648
- for (const scenario of session.bddScenarios) {
649
- lines.push(`### ${scenario.feature}`);
650
- for (const s of scenario.scenarios) {
651
- lines.push(`- ${s.name}`);
660
+ lines.push(`## \u5F53\u524D\u4EFB\u52A1 (#${index + 1})`);
661
+ lines.push(`**\u6807\u9898**: ${item.title}`);
662
+ lines.push(`**\u63CF\u8FF0**: ${item.description}`);
663
+ lines.push(`**\u4F18\u5148\u7EA7**: ${item.priority}`);
664
+ if (item.tests && item.tests.length > 0) {
665
+ lines.push(`**\u9A8C\u6536\u6807\u51C6**:`);
666
+ for (const t of item.tests) {
667
+ lines.push(`- ${t}`);
652
668
  }
653
669
  }
654
670
  lines.push("");
655
- lines.push("## \u4EFB\u52A1\u5217\u8868");
656
- for (const item of session.specItems) {
657
- lines.push(`- [${item.id}] ${item.title}: ${item.description}`);
671
+ lines.push(`## \u6574\u4F53\u9700\u6C42\u80CC\u666F`);
672
+ lines.push(session.requirement);
673
+ const relatedScenario = session.bddScenarios.find(
674
+ (s) => item.title.includes(s.feature) || s.feature.includes(item.title)
675
+ );
676
+ if (relatedScenario) {
677
+ lines.push("");
678
+ lines.push(`## \u76F8\u5173 BDD \u573A\u666F`);
679
+ lines.push(`Feature: ${relatedScenario.feature}`);
680
+ for (const s of relatedScenario.scenarios.slice(0, 2)) {
681
+ lines.push(`- ${s.name}`);
682
+ }
658
683
  }
659
684
  lines.push("");
660
- lines.push("\u8BF7\u6839\u636E\u4EE5\u4E0A\u9700\u6C42\u89C4\u683C\u751F\u6210\u4EE3\u7801\u5B9E\u73B0\u3002");
685
+ lines.push("\u8BF7\u751F\u6210\u5B9E\u73B0\u6B64\u4EFB\u52A1\u7684\u4EE3\u7801\u3002\u53EA\u751F\u6210\u5F53\u524D\u4EFB\u52A1\u9700\u8981\u7684\u6587\u4EF6\u3002");
661
686
  return lines.join("\n");
662
687
  }
663
688
  function parseCodeBlocks(content) {
@@ -1124,6 +1149,111 @@ function generateSpecItems(requirement, context, bddScenarios, questions, refere
1124
1149
  });
1125
1150
  return items;
1126
1151
  }
1152
+ async function generateSpecItemsWithAI(requirement, context, bddScenarios, questions, references, ctx) {
1153
+ const prompt2 = `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u9879\u76EE\u7ECF\u7406\u548C\u6280\u672F\u67B6\u6784\u5E08\u3002\u8BF7\u5C06\u4EE5\u4E0B\u9700\u6C42\u62C6\u5206\u4E3A\u7CBE\u7EC6\u5316\u7684\u5F00\u53D1\u4EFB\u52A1\u3002
1154
+
1155
+ ## \u9700\u6C42\u63CF\u8FF0
1156
+ ${requirement}
1157
+
1158
+ ## \u9879\u76EE\u4E0A\u4E0B\u6587
1159
+ - \u6280\u672F\u6808: ${context.techStack?.join(", ") || "TypeScript"}
1160
+ - \u6846\u67B6: ${context.framework || "\u672A\u6307\u5B9A"}
1161
+ ${context.devStandards ? `
1162
+ ## \u5F00\u53D1\u89C4\u8303\uFF08\u5FC5\u987B\u9075\u5FAA\uFF09
1163
+ ${context.devStandards.slice(0, 2e3)}
1164
+ ` : ""}
1165
+
1166
+ ## BDD \u573A\u666F\u53C2\u8003
1167
+ ${bddScenarios.map((s) => `- Feature: ${s.feature} (${s.scenarios.length} \u4E2A\u573A\u666F)`).join("\n")}
1168
+
1169
+ ## \u6F84\u6E05\u4FE1\u606F
1170
+ ${questions.filter((q) => q.answered).map((q) => `- ${q.question}: ${q.answer}`).join("\n") || "\u65E0"}
1171
+
1172
+ ## \u62C6\u5206\u8981\u6C42
1173
+
1174
+ \u8BF7\u5C06\u9700\u6C42\u62C6\u5206\u4E3A **\u7EC6\u7C92\u5EA6\u7684\u5F00\u53D1\u4EFB\u52A1**\uFF0C\u6BCF\u4E2A\u4EFB\u52A1\u5E94\u8BE5\uFF1A
1175
+ 1. **\u5355\u4E00\u804C\u8D23** - \u4E00\u4E2A\u4EFB\u52A1\u53EA\u505A\u4E00\u4EF6\u4E8B
1176
+ 2. **\u53EF\u72EC\u7ACB\u6D4B\u8BD5** - \u6709\u660E\u786E\u7684\u9A8C\u6536\u6807\u51C6
1177
+ 3. **2-4\u5C0F\u65F6\u53EF\u5B8C\u6210** - \u5982\u679C\u4EFB\u52A1\u592A\u5927\uFF0C\u7EE7\u7EED\u62C6\u5206
1178
+ 4. **\u6709\u660E\u786E\u7684\u8F93\u5165\u8F93\u51FA** - \u6E05\u695A\u77E5\u9053\u9700\u8981\u4EC0\u4E48\u3001\u4EA7\u51FA\u4EC0\u4E48
1179
+
1180
+ ## \u8F93\u51FA\u683C\u5F0F (JSON)
1181
+ \`\`\`json
1182
+ [
1183
+ {
1184
+ "id": "T001",
1185
+ "title": "\u4EFB\u52A1\u6807\u9898\uFF08\u7B80\u77ED\u660E\u786E\uFF09",
1186
+ "description": "\u8BE6\u7EC6\u63CF\u8FF0\uFF1A\u8981\u505A\u4EC0\u4E48\u3001\u5982\u4F55\u505A\u3001\u9A8C\u6536\u6807\u51C6",
1187
+ "priority": "high",
1188
+ "estimatedHours": 2,
1189
+ "dependencies": [],
1190
+ "acceptanceCriteria": ["\u9A8C\u6536\u6807\u51C61", "\u9A8C\u6536\u6807\u51C62"]
1191
+ }
1192
+ ]
1193
+ \`\`\`
1194
+
1195
+ ## \u62C6\u5206\u5EFA\u8BAE
1196
+ \u5BF9\u4E8E\u590D\u6742\u529F\u80FD\uFF08\u5982\u7B97\u6CD5\u7C7B\uFF09\uFF0C\u5E94\u8BE5\u62C6\u5206\u4E3A\uFF1A
1197
+ - \u6570\u636E\u7ED3\u6784/\u6A21\u578B\u5B9A\u4E49
1198
+ - \u6838\u5FC3\u7B97\u6CD5\u5206\u6B65\u5B9E\u73B0\uFF08\u6BCF\u4E2A\u8BA1\u7B97\u6B65\u9AA4\u4E00\u4E2A\u4EFB\u52A1\uFF09
1199
+ - \u8F93\u5165\u9A8C\u8BC1
1200
+ - \u7ED3\u679C\u683C\u5F0F\u5316
1201
+ - UI \u5C55\u793A\u7EC4\u4EF6
1202
+ - \u96C6\u6210\u6D4B\u8BD5
1203
+
1204
+ \u8BF7\u76F4\u63A5\u8F93\u51FA JSON \u6570\u7EC4\u3002`;
1205
+ const loader = new LoadingIndicator("AI \u6B63\u5728\u62C6\u5206\u89C4\u683C");
1206
+ loader.start();
1207
+ try {
1208
+ const response = await ctx.modelService.sendMessage([
1209
+ { role: "user", content: prompt2 }
1210
+ ], {
1211
+ temperature: 0.3,
1212
+ maxTokens: 4e3,
1213
+ timeout: 18e4
1214
+ // 增加到3分钟
1215
+ });
1216
+ const jsonMatch = response.content.match(/```json\s*([\s\S]*?)```/);
1217
+ if (jsonMatch) {
1218
+ try {
1219
+ const parsed = JSON.parse(jsonMatch[1].trim());
1220
+ loader.stop(chalk9.green(` \u2713 \u5DF2\u62C6\u5206 ${parsed.length} \u4E2A\u4EFB\u52A1`));
1221
+ return parsed.map((item, index) => ({
1222
+ id: item.id || `T${String(index + 1).padStart(3, "0")}`,
1223
+ title: item.title,
1224
+ description: item.description,
1225
+ priority: item.priority || "medium",
1226
+ files: [],
1227
+ tests: item.acceptanceCriteria || []
1228
+ }));
1229
+ } catch (parseError) {
1230
+ loader.stop(chalk9.yellow(` \u26A0 JSON \u89E3\u6790\u5931\u8D25: ${parseError.message.slice(0, 50)}`));
1231
+ return generateSpecItems(requirement, context, bddScenarios, questions, references);
1232
+ }
1233
+ }
1234
+ try {
1235
+ const parsed = JSON.parse(response.content);
1236
+ if (Array.isArray(parsed)) {
1237
+ loader.stop(chalk9.green(` \u2713 \u5DF2\u62C6\u5206 ${parsed.length} \u4E2A\u4EFB\u52A1`));
1238
+ return parsed.map((item, index) => ({
1239
+ id: item.id || `T${String(index + 1).padStart(3, "0")}`,
1240
+ title: item.title,
1241
+ description: item.description,
1242
+ priority: item.priority || "medium",
1243
+ files: [],
1244
+ tests: item.acceptanceCriteria || []
1245
+ }));
1246
+ }
1247
+ } catch {
1248
+ }
1249
+ loader.stop(chalk9.yellow(" \u26A0 \u672A\u80FD\u89E3\u6790 AI \u54CD\u5E94\uFF0C\u4F7F\u7528\u57FA\u7840\u62C6\u5206"));
1250
+ return generateSpecItems(requirement, context, bddScenarios, questions, references);
1251
+ } catch (error) {
1252
+ const errMsg = error.message.slice(0, 80);
1253
+ loader.stop(chalk9.yellow(` \u26A0 AI \u8C03\u7528\u5931\u8D25: ${errMsg}`));
1254
+ return generateSpecItems(requirement, context, bddScenarios, questions, references);
1255
+ }
1256
+ }
1127
1257
  async function saveSpecFile(workingDir, session) {
1128
1258
  const specDir = path5.join(workingDir, "openspec", "changes");
1129
1259
  await fs4.mkdir(specDir, { recursive: true });