@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/cli/index.js CHANGED
@@ -1414,6 +1414,7 @@ async function handleNew(args, ctx) {
1414
1414
  context: null,
1415
1415
  clarityScore: 0,
1416
1416
  clarificationQuestions: [],
1417
+ referenceResources: [],
1417
1418
  complexity: 0,
1418
1419
  bddScenarios: [],
1419
1420
  specItems: [],
@@ -1456,7 +1457,7 @@ async function executeWorkflow(ctx) {
1456
1457
  const lines = [];
1457
1458
  try {
1458
1459
  if (activeSession.phase === "context") {
1459
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 1/8: \u9879\u76EE\u4E0A\u4E0B\u6587\u83B7\u53D6 \u2501\u2501\u2501"));
1460
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 1/9: \u9879\u76EE\u4E0A\u4E0B\u6587\u83B7\u53D6 \u2501\u2501\u2501"));
1460
1461
  lines.push("");
1461
1462
  activeSession.context = await readProjectContext(ctx.options.workingDirectory);
1462
1463
  lines.push(chalk9__default.default.gray(` \u9879\u76EE: ${activeSession.context.name}`));
@@ -1470,7 +1471,7 @@ async function executeWorkflow(ctx) {
1470
1471
  }
1471
1472
  if (activeSession.phase === "clarify") {
1472
1473
  lines.push("");
1473
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 2/8: \u9700\u6C42\u6F84\u6E05 \u2501\u2501\u2501"));
1474
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 2/9: \u9700\u6C42\u6F84\u6E05 \u2501\u2501\u2501"));
1474
1475
  lines.push("");
1475
1476
  const clarityResult = analyzeRequirementClarity(
1476
1477
  activeSession.requirement,
@@ -1496,11 +1497,40 @@ async function executeWorkflow(ctx) {
1496
1497
  return { output: lines.join("\n") };
1497
1498
  }
1498
1499
  lines.push(chalk9__default.default.green(" \u2713 \u9700\u6C42\u6E05\u6670\uFF0C\u7EE7\u7EED\u4E0B\u4E00\u6B65"));
1500
+ activeSession.phase = "reference";
1501
+ }
1502
+ if (activeSession.phase === "reference") {
1503
+ lines.push("");
1504
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 3/9: \u53C2\u8003\u8D44\u6E90\u5206\u6790 \u2501\u2501\u2501"));
1505
+ lines.push("");
1506
+ const urls = extractUrls(activeSession.refinedRequirement);
1507
+ if (urls.length > 0) {
1508
+ lines.push(chalk9__default.default.gray(` \u53D1\u73B0 ${urls.length} \u4E2A\u53C2\u8003\u94FE\u63A5`));
1509
+ lines.push("");
1510
+ for (const url of urls) {
1511
+ lines.push(chalk9__default.default.gray(` \u{1F4CE} ${url}`));
1512
+ try {
1513
+ const resource = await fetchAndAnalyzeReference(url, ctx);
1514
+ activeSession.referenceResources.push(resource);
1515
+ lines.push(chalk9__default.default.green(` \u2713 \u5DF2\u5206\u6790`));
1516
+ activeSession.refinedRequirement += `
1517
+
1518
+ \u3010\u53C2\u8003\u8D44\u6E90\u5206\u6790 - ${url}\u3011
1519
+ ${resource.analysis}`;
1520
+ } catch (error) {
1521
+ lines.push(chalk9__default.default.yellow(` \u26A0 \u83B7\u53D6\u5931\u8D25: ${error.message}`));
1522
+ }
1523
+ }
1524
+ lines.push("");
1525
+ lines.push(chalk9__default.default.green(" \u2713 \u53C2\u8003\u8D44\u6E90\u5206\u6790\u5B8C\u6210"));
1526
+ } else {
1527
+ lines.push(chalk9__default.default.gray(" \u65E0\u5916\u90E8\u53C2\u8003\u94FE\u63A5"));
1528
+ }
1499
1529
  activeSession.phase = "analysis";
1500
1530
  }
1501
1531
  if (activeSession.phase === "analysis") {
1502
1532
  lines.push("");
1503
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 3/8: \u590D\u6742\u5EA6\u8BC4\u4F30 \u2501\u2501\u2501"));
1533
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 4/9: \u590D\u6742\u5EA6\u8BC4\u4F30 \u2501\u2501\u2501"));
1504
1534
  lines.push("");
1505
1535
  activeSession.complexity = analyzeComplexity(
1506
1536
  activeSession.refinedRequirement,
@@ -1517,13 +1547,28 @@ async function executeWorkflow(ctx) {
1517
1547
  }
1518
1548
  if (activeSession.phase === "bdd") {
1519
1549
  lines.push("");
1520
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 4/8: BDD \u573A\u666F\u62C6\u89E3 \u2501\u2501\u2501"));
1550
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 5/9: BDD \u573A\u666F\u62C6\u89E3 \u2501\u2501\u2501"));
1521
1551
  lines.push("");
1522
- activeSession.bddScenarios = generateBDDScenarios(
1523
- activeSession.refinedRequirement,
1524
- activeSession.context,
1525
- activeSession.clarificationQuestions
1526
- );
1552
+ const loader = new LoadingIndicator("AI \u6B63\u5728\u751F\u6210 BDD \u573A\u666F");
1553
+ loader.start();
1554
+ try {
1555
+ activeSession.bddScenarios = await generateBDDScenariosWithAI(
1556
+ activeSession.refinedRequirement,
1557
+ activeSession.context,
1558
+ activeSession.clarificationQuestions,
1559
+ activeSession.referenceResources,
1560
+ ctx
1561
+ );
1562
+ loader.stop(chalk9__default.default.green(" \u2713 BDD \u573A\u666F\u5DF2\u751F\u6210"));
1563
+ } catch {
1564
+ loader.stop(chalk9__default.default.yellow(" \u26A0 \u4F7F\u7528\u57FA\u7840 BDD \u751F\u6210"));
1565
+ activeSession.bddScenarios = generateBDDScenarios(
1566
+ activeSession.refinedRequirement,
1567
+ activeSession.context,
1568
+ activeSession.clarificationQuestions,
1569
+ activeSession.referenceResources
1570
+ );
1571
+ }
1527
1572
  for (const scenario of activeSession.bddScenarios) {
1528
1573
  lines.push(chalk9__default.default.white(` Feature: ${scenario.feature}`));
1529
1574
  for (const s of scenario.scenarios.slice(0, 3)) {
@@ -1537,13 +1582,14 @@ async function executeWorkflow(ctx) {
1537
1582
  }
1538
1583
  if (activeSession.phase === "spec") {
1539
1584
  lines.push("");
1540
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 5/8: OpenSpec \u89C4\u683C \u2501\u2501\u2501"));
1585
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 6/9: OpenSpec \u89C4\u683C \u2501\u2501\u2501"));
1541
1586
  lines.push("");
1542
1587
  activeSession.specItems = generateSpecItems(
1543
1588
  activeSession.refinedRequirement,
1544
1589
  activeSession.context,
1545
1590
  activeSession.bddScenarios,
1546
- activeSession.clarificationQuestions
1591
+ activeSession.clarificationQuestions,
1592
+ activeSession.referenceResources
1547
1593
  );
1548
1594
  const specPath = await saveSpecFile(ctx.options.workingDirectory, activeSession);
1549
1595
  lines.push(chalk9__default.default.green(" \u2713 \u89C4\u683C\u6587\u4EF6\u5DF2\u751F\u6210"));
@@ -1567,9 +1613,9 @@ async function executeWorkflow(ctx) {
1567
1613
  }
1568
1614
  if (activeSession.phase === "tdd") {
1569
1615
  lines.push("");
1570
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 6/8: TDD \u6D4B\u8BD5\u751F\u6210 \u2501\u2501\u2501"));
1616
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 7/9: TDD \u6D4B\u8BD5\u751F\u6210 \u2501\u2501\u2501"));
1571
1617
  lines.push("");
1572
- activeSession.testFiles = await generateTests(ctx.options.workingDirectory, activeSession);
1618
+ activeSession.testFiles = await generateTests(ctx.options.workingDirectory, activeSession, ctx);
1573
1619
  lines.push(chalk9__default.default.green(" \u2713 \u6D4B\u8BD5\u6587\u4EF6\u5DF2\u751F\u6210"));
1574
1620
  for (const file of activeSession.testFiles) {
1575
1621
  lines.push(chalk9__default.default.gray(` - ${file}`));
@@ -1578,7 +1624,7 @@ async function executeWorkflow(ctx) {
1578
1624
  }
1579
1625
  if (activeSession.phase === "develop") {
1580
1626
  lines.push("");
1581
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 7/8: \u5F00\u53D1\u5B9E\u73B0 \u2501\u2501\u2501"));
1627
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 8/9: \u5F00\u53D1\u5B9E\u73B0 \u2501\u2501\u2501"));
1582
1628
  lines.push("");
1583
1629
  lines.push(chalk9__default.default.yellow(" \u{1F680} \u6B63\u5728\u8C03\u7528 AI \u751F\u6210\u4EE3\u7801..."));
1584
1630
  try {
@@ -1603,7 +1649,7 @@ async function executeWorkflow(ctx) {
1603
1649
  }
1604
1650
  if (activeSession.phase === "review") {
1605
1651
  lines.push("");
1606
- lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 8/8: \u4EE3\u7801\u5BA1\u6838 \u2501\u2501\u2501"));
1652
+ lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 9/9: \u4EE3\u7801\u5BA1\u6838 \u2501\u2501\u2501"));
1607
1653
  lines.push("");
1608
1654
  lines.push(chalk9__default.default.yellow(" \u{1F50D} \u6B63\u5728\u8FDB\u884C\u4EE3\u7801\u5BA1\u6838..."));
1609
1655
  try {
@@ -1866,6 +1912,8 @@ function getCategoryLabel(category) {
1866
1912
  async function executeDevelopment(ctx, session) {
1867
1913
  const workingDir = ctx.options.workingDirectory;
1868
1914
  const files = [];
1915
+ const loader = new LoadingIndicator("AI \u6B63\u5728\u751F\u6210\u4EE3\u7801");
1916
+ loader.start();
1869
1917
  try {
1870
1918
  const systemPrompt = buildDevelopmentPrompt(session);
1871
1919
  const messages = [
@@ -1893,11 +1941,15 @@ ${session.context.devStandards.slice(0, 2e3)}` : ""}`
1893
1941
  content: systemPrompt
1894
1942
  }
1895
1943
  ];
1944
+ loader.update("\u6B63\u5728\u8C03\u7528 AI \u6A21\u578B");
1896
1945
  const response = await ctx.modelService.sendMessage(messages, {
1897
1946
  temperature: 0.3,
1898
1947
  maxTokens: 8e3,
1899
- agent: "frontend-dev"
1948
+ agent: "frontend-dev",
1949
+ timeout: 18e4
1950
+ // 3 分钟超时
1900
1951
  });
1952
+ loader.update("\u6B63\u5728\u89E3\u6790\u4EE3\u7801");
1901
1953
  const codeBlocks = parseCodeBlocks(response.content);
1902
1954
  for (const block of codeBlocks) {
1903
1955
  const filePath = path5__namespace.join(workingDir, block.filename);
@@ -1926,8 +1978,10 @@ export function ${featureName.replace(/[^a-zA-Z0-9]/g, "")}() {
1926
1978
  await fs5__namespace.writeFile(filePath, stubCode, "utf-8");
1927
1979
  files.push(`src/features/${fileName}`);
1928
1980
  }
1981
+ loader.stop(chalk9__default.default.green(` \u2713 \u5DF2\u751F\u6210 ${files.length} \u4E2A\u6587\u4EF6`));
1929
1982
  return { success: true, files };
1930
1983
  } catch (error) {
1984
+ loader.stop();
1931
1985
  return {
1932
1986
  success: false,
1933
1987
  files: [],
@@ -2145,13 +2199,41 @@ function analyzeComplexity(requirement, context) {
2145
2199
  if (!context.framework) score += 0.5;
2146
2200
  return Math.max(1, Math.min(10, Math.round(score)));
2147
2201
  }
2148
- function generateBDDScenarios(requirement, context, questions) {
2202
+ function generateBDDScenarios(requirement, context, questions, references = []) {
2149
2203
  const scenarios = [];
2150
2204
  questions.find((q) => q.category === "ui" && q.answered)?.answer;
2151
2205
  const interactionAnswer = questions.find((q) => q.category === "interaction" && q.answered)?.answer;
2152
2206
  const edgeAnswer = questions.find((q) => q.category === "edge" && q.answered)?.answer;
2207
+ if (references.length > 0) {
2208
+ for (const ref of references) {
2209
+ const refFeatures = extractFeaturesFromReference(ref);
2210
+ for (const feature of refFeatures) {
2211
+ const scenario = {
2212
+ feature: feature.title,
2213
+ description: feature.description,
2214
+ scenarios: []
2215
+ };
2216
+ scenario.scenarios.push({
2217
+ name: `\u6B63\u5E38\u6D41\u7A0B: ${feature.title}`,
2218
+ given: [`\u7528\u6237\u8FDB\u5165\u76F8\u5173\u9875\u9762`],
2219
+ when: [`\u7528\u6237\u6267\u884C "${feature.title}" \u64CD\u4F5C`],
2220
+ then: [`\u7CFB\u7EDF\u5E94\u6B63\u786E\u5904\u7406\u5E76\u8FD4\u56DE\u9884\u671F\u7ED3\u679C`]
2221
+ });
2222
+ if (feature.hasInput) {
2223
+ scenario.scenarios.push({
2224
+ name: `\u8FB9\u754C\u60C5\u51B5: \u8F93\u5165\u9A8C\u8BC1`,
2225
+ given: [`\u7528\u6237\u8FDB\u5165\u8F93\u5165\u754C\u9762`],
2226
+ when: [`\u7528\u6237\u8F93\u5165\u8FB9\u754C\u503C\u6216\u7A7A\u503C`],
2227
+ then: [`\u7CFB\u7EDF\u5E94\u6B63\u786E\u5904\u7406\u8FB9\u754C\u60C5\u51B5`]
2228
+ });
2229
+ }
2230
+ scenarios.push(scenario);
2231
+ }
2232
+ }
2233
+ }
2153
2234
  const features = extractFeatures(requirement);
2154
2235
  for (const feature of features) {
2236
+ if (scenarios.some((s) => s.feature === feature.title)) continue;
2155
2237
  const scenario = {
2156
2238
  feature: feature.title,
2157
2239
  description: feature.description,
@@ -2183,6 +2265,135 @@ function generateBDDScenarios(requirement, context, questions) {
2183
2265
  }
2184
2266
  return scenarios;
2185
2267
  }
2268
+ async function generateBDDScenariosWithAI(requirement, context, questions, references, ctx) {
2269
+ 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
2270
+
2271
+ ## \u9700\u6C42\u63CF\u8FF0
2272
+ ${requirement}
2273
+
2274
+ ## \u9879\u76EE\u4E0A\u4E0B\u6587
2275
+ - \u6280\u672F\u6808: ${context.techStack?.join(", ") || "\u672A\u6307\u5B9A"}
2276
+ - \u6846\u67B6: ${context.framework || "\u672A\u6307\u5B9A"}
2277
+
2278
+ ## \u6F84\u6E05\u95EE\u7B54
2279
+ ${questions.filter((q) => q.answered).map((q) => `- Q: ${q.question}
2280
+ A: ${q.answer}`).join("\n")}
2281
+
2282
+ ## \u53C2\u8003\u8D44\u6E90\u5206\u6790
2283
+ ${references.map((r) => `### ${r.url}
2284
+ ${r.analysis}`).join("\n\n")}
2285
+
2286
+ ## \u8981\u6C42
2287
+ 1. \u6BCF\u4E2A\u529F\u80FD\u6A21\u5757\u751F\u6210\u4E00\u4E2A\u72EC\u7ACB\u7684 Feature
2288
+ 2. \u6BCF\u4E2A Feature \u5305\u542B\u591A\u4E2A\u5177\u4F53\u7684 Scenario
2289
+ 3. \u4F7F\u7528 Given-When-Then \u683C\u5F0F
2290
+ 4. \u573A\u666F\u8981\u8986\u76D6: \u6B63\u5E38\u6D41\u7A0B\u3001\u8FB9\u754C\u60C5\u51B5\u3001\u5F02\u5E38\u5904\u7406
2291
+ 5. \u573A\u666F\u8981\u5177\u4F53\u53EF\u6D4B\u8BD5\uFF0C\u4E0D\u8981\u6CDB\u6CDB\u800C\u8C08
2292
+
2293
+ ## \u8F93\u51FA\u683C\u5F0F (JSON)
2294
+ \`\`\`json
2295
+ [
2296
+ {
2297
+ "feature": "\u529F\u80FD\u540D\u79F0",
2298
+ "description": "\u529F\u80FD\u63CF\u8FF0",
2299
+ "scenarios": [
2300
+ {
2301
+ "name": "\u573A\u666F\u540D\u79F0",
2302
+ "given": ["\u524D\u7F6E\u6761\u4EF61", "\u524D\u7F6E\u6761\u4EF62"],
2303
+ "when": ["\u64CD\u4F5C1", "\u64CD\u4F5C2"],
2304
+ "then": ["\u9884\u671F\u7ED3\u679C1", "\u9884\u671F\u7ED3\u679C2"]
2305
+ }
2306
+ ]
2307
+ }
2308
+ ]
2309
+ \`\`\`
2310
+
2311
+ \u8BF7\u76F4\u63A5\u8F93\u51FA JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u6709\u5176\u4ED6\u5185\u5BB9\u3002`;
2312
+ const response = await ctx.modelService.sendMessage([
2313
+ { role: "user", content: prompt2 }
2314
+ ], {
2315
+ temperature: 0.3,
2316
+ maxTokens: 4e3,
2317
+ timeout: 12e4
2318
+ });
2319
+ try {
2320
+ const jsonMatch = response.content.match(/```json\s*([\s\S]*?)```/);
2321
+ if (jsonMatch) {
2322
+ return JSON.parse(jsonMatch[1].trim());
2323
+ }
2324
+ return JSON.parse(response.content);
2325
+ } catch {
2326
+ return generateBDDScenarios(requirement, context, questions, references);
2327
+ }
2328
+ }
2329
+ function extractFeaturesFromReference(ref) {
2330
+ const features = [];
2331
+ const analysis = ref.analysis;
2332
+ const featureSection = analysis.match(/###?\s*4\.\s*功能拆分建议[\s\S]*?(?=###?\s*\d|$)/i);
2333
+ if (featureSection) {
2334
+ const taskMatches = featureSection[0].matchAll(/[-*]\s*\*\*([^*]+)\*\*[::]?\s*([^\n]+)/g);
2335
+ for (const match of taskMatches) {
2336
+ features.push({
2337
+ title: match[1].trim(),
2338
+ description: match[2].trim(),
2339
+ hasInput: match[2].includes("\u8F93\u5165") || match[2].includes("\u8868\u5355") || match[2].includes("\u7528\u6237")
2340
+ });
2341
+ }
2342
+ }
2343
+ const bizSection = analysis.match(/###?\s*1\.\s*业务功能分析[\s\S]*?(?=###?\s*\d|$)/i);
2344
+ if (bizSection && features.length === 0) {
2345
+ const lines = bizSection[0].split("\n").filter((l) => l.trim().startsWith("-") || l.trim().startsWith("*"));
2346
+ for (const line of lines.slice(0, 5)) {
2347
+ const content = line.replace(/^[-*]\s*/, "").trim();
2348
+ if (content.length > 5) {
2349
+ features.push({
2350
+ title: content.slice(0, 20),
2351
+ description: content,
2352
+ hasInput: content.includes("\u8F93\u5165") || content.includes("\u586B\u5199")
2353
+ });
2354
+ }
2355
+ }
2356
+ }
2357
+ if (features.length === 0) {
2358
+ const lowerAnalysis = analysis.toLowerCase();
2359
+ if (lowerAnalysis.includes("\u8F93\u5165") || lowerAnalysis.includes("\u8868\u5355")) {
2360
+ features.push({
2361
+ title: "\u8F93\u5165\u8868\u5355",
2362
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u8F93\u5165\u8868\u5355\u529F\u80FD",
2363
+ hasInput: true
2364
+ });
2365
+ }
2366
+ if (lowerAnalysis.includes("\u6309\u94AE") || lowerAnalysis.includes("\u64CD\u4F5C")) {
2367
+ features.push({
2368
+ title: "\u4EA4\u4E92\u6309\u94AE",
2369
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u6309\u94AE\u4EA4\u4E92",
2370
+ hasInput: false
2371
+ });
2372
+ }
2373
+ if (lowerAnalysis.includes("\u8868\u683C") || lowerAnalysis.includes("\u5217\u8868")) {
2374
+ features.push({
2375
+ title: "\u6570\u636E\u5217\u8868",
2376
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u6570\u636E\u5C55\u793A",
2377
+ hasInput: false
2378
+ });
2379
+ }
2380
+ if (lowerAnalysis.includes("\u56FE\u8868") || lowerAnalysis.includes("\u53EF\u89C6\u5316")) {
2381
+ features.push({
2382
+ title: "\u56FE\u8868\u5C55\u793A",
2383
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u56FE\u8868\u53EF\u89C6\u5316",
2384
+ hasInput: false
2385
+ });
2386
+ }
2387
+ }
2388
+ if (features.length === 0) {
2389
+ features.push({
2390
+ title: "\u53C2\u8003\u529F\u80FD\u5B9E\u73B0",
2391
+ description: `\u57FA\u4E8E\u53C2\u8003\u8D44\u6E90 ${ref.url} \u5B9E\u73B0\u7684\u529F\u80FD`,
2392
+ hasInput: true
2393
+ });
2394
+ }
2395
+ return features;
2396
+ }
2186
2397
  function extractFeatures(requirement) {
2187
2398
  const features = [];
2188
2399
  const urlMatch = requirement.match(/https?:\/\/[^\s]+/);
@@ -2223,15 +2434,26 @@ function extractFeatures(requirement) {
2223
2434
  }
2224
2435
  return features;
2225
2436
  }
2226
- function generateSpecItems(requirement, context, bddScenarios, questions) {
2437
+ function generateSpecItems(requirement, context, bddScenarios, questions, references = []) {
2227
2438
  const items = [];
2228
2439
  let id = 1;
2440
+ for (const ref of references) {
2441
+ items.push({
2442
+ id: `T${id.toString().padStart(3, "0")}`,
2443
+ title: `\u53C2\u8003\u5206\u6790: ${ref.type}`,
2444
+ description: `\u5206\u6790\u53C2\u8003\u8D44\u6E90 ${ref.url}`,
2445
+ priority: "high",
2446
+ files: [],
2447
+ tests: []
2448
+ });
2449
+ id++;
2450
+ }
2229
2451
  for (const scenario of bddScenarios) {
2230
2452
  items.push({
2231
2453
  id: `T${id.toString().padStart(3, "0")}`,
2232
2454
  title: scenario.feature,
2233
2455
  description: scenario.description,
2234
- priority: id <= 2 ? "high" : "medium",
2456
+ priority: id <= 3 ? "high" : "medium",
2235
2457
  files: [],
2236
2458
  tests: []
2237
2459
  });
@@ -2274,6 +2496,19 @@ function formatSpecFile(session) {
2274
2496
  lines.push("---");
2275
2497
  lines.push("");
2276
2498
  }
2499
+ if (session.referenceResources.length > 0) {
2500
+ lines.push("## \u53C2\u8003\u8D44\u6E90");
2501
+ lines.push("");
2502
+ for (const ref of session.referenceResources) {
2503
+ lines.push(`### ${ref.url}`);
2504
+ lines.push(`> \u7C7B\u578B: ${ref.type}`);
2505
+ lines.push("");
2506
+ lines.push(ref.analysis);
2507
+ lines.push("");
2508
+ }
2509
+ lines.push("---");
2510
+ lines.push("");
2511
+ }
2277
2512
  if (session.clarificationQuestions.some((q) => q.answered)) {
2278
2513
  lines.push("## \u9700\u6C42\u6F84\u6E05");
2279
2514
  lines.push("");
@@ -2312,33 +2547,119 @@ function formatSpecFile(session) {
2312
2547
  lines.push("**\u786E\u8BA4\u72B6\u6001**: \u23F3 \u7B49\u5F85\u786E\u8BA4");
2313
2548
  return lines.join("\n");
2314
2549
  }
2315
- async function generateTests(workingDir, session) {
2550
+ async function generateTests(workingDir, session, ctx) {
2316
2551
  const testDir = path5__namespace.join(workingDir, "tests");
2317
2552
  await fs5__namespace.mkdir(testDir, { recursive: true });
2318
2553
  const testFiles = [];
2319
- for (const scenario of session.bddScenarios) {
2320
- const testName = scenario.feature.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "_");
2321
- const testPath = path5__namespace.join(testDir, `${testName}.test.ts`);
2322
- const content = generateTestFile(scenario);
2323
- await fs5__namespace.writeFile(testPath, content, "utf-8");
2324
- testFiles.push(`tests/${testName}.test.ts`);
2554
+ if (ctx?.modelService) {
2555
+ for (const scenario of session.bddScenarios) {
2556
+ const testName = scenario.feature.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "_");
2557
+ const testPath = path5__namespace.join(testDir, `${testName}.test.ts`);
2558
+ const loader = new LoadingIndicator(`\u751F\u6210\u6D4B\u8BD5: ${scenario.feature.slice(0, 20)}...`);
2559
+ loader.start();
2560
+ try {
2561
+ const content = await generateTestFileWithAI(scenario, session, ctx);
2562
+ await fs5__namespace.writeFile(testPath, content, "utf-8");
2563
+ testFiles.push(`tests/${testName}.test.ts`);
2564
+ loader.stop(chalk9__default.default.green(` \u2713 \u6D4B\u8BD5\u6587\u4EF6\u5DF2\u751F\u6210`));
2565
+ } catch {
2566
+ const content = generateTestFile(scenario, session);
2567
+ await fs5__namespace.writeFile(testPath, content, "utf-8");
2568
+ testFiles.push(`tests/${testName}.test.ts`);
2569
+ loader.stop(chalk9__default.default.yellow(" \u26A0 \u4F7F\u7528\u57FA\u7840\u6D4B\u8BD5\u6A21\u677F"));
2570
+ }
2571
+ }
2572
+ } else {
2573
+ for (const scenario of session.bddScenarios) {
2574
+ const testName = scenario.feature.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, "_");
2575
+ const testPath = path5__namespace.join(testDir, `${testName}.test.ts`);
2576
+ const content = generateTestFile(scenario, session);
2577
+ await fs5__namespace.writeFile(testPath, content, "utf-8");
2578
+ testFiles.push(`tests/${testName}.test.ts`);
2579
+ }
2325
2580
  }
2326
2581
  return testFiles;
2327
2582
  }
2328
- function generateTestFile(scenario) {
2583
+ async function generateTestFileWithAI(scenario, session, ctx) {
2584
+ 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
2585
+
2586
+ ## \u529F\u80FD\u540D\u79F0
2587
+ ${scenario.feature}
2588
+
2589
+ ## BDD \u573A\u666F
2590
+ ${scenario.scenarios.map((s) => `
2591
+ ### ${s.name}
2592
+ - Given: ${s.given.join(", ")}
2593
+ - When: ${s.when.join(", ")}
2594
+ - Then: ${s.then.join(", ")}
2595
+ `).join("\n")}
2596
+
2597
+ ## \u9879\u76EE\u4E0A\u4E0B\u6587
2598
+ - \u6280\u672F\u6808: ${session.context?.techStack?.join(", ") || "TypeScript"}
2599
+ - \u6846\u67B6: ${session.context?.framework || "\u672A\u6307\u5B9A"}
2600
+
2601
+ ## \u8981\u6C42
2602
+ 1. \u4F7F\u7528 vitest \u6D4B\u8BD5\u6846\u67B6 (describe, it, expect, beforeEach \u7B49)
2603
+ 2. \u6BCF\u4E2A\u573A\u666F\u751F\u6210\u4E00\u4E2A\u72EC\u7ACB\u7684\u6D4B\u8BD5\u7528\u4F8B
2604
+ 3. \u6D4B\u8BD5\u4EE3\u7801\u8981\u5B8C\u6574\u53EF\u8FD0\u884C\uFF0C\u5305\u542B\u5FC5\u8981\u7684 mock \u548C setup
2605
+ 4. \u4F7F\u7528\u4E2D\u6587\u6CE8\u91CA\u8BF4\u660E\u6D4B\u8BD5\u610F\u56FE
2606
+ 5. \u6D4B\u8BD5\u8981\u8986\u76D6\u6B63\u5E38\u6D41\u7A0B\u548C\u8FB9\u754C\u60C5\u51B5
2607
+
2608
+ \u8BF7\u76F4\u63A5\u8F93\u51FA\u6D4B\u8BD5\u4EE3\u7801\uFF0C\u4E0D\u9700\u8981\u89E3\u91CA\u3002`;
2609
+ const response = await ctx.modelService.sendMessage([
2610
+ { role: "user", content: prompt2 }
2611
+ ], {
2612
+ temperature: 0.3,
2613
+ maxTokens: 4e3
2614
+ });
2615
+ const codeMatch = response.content.match(/```(?:typescript|ts|javascript|js)?\n([\s\S]*?)```/);
2616
+ if (codeMatch) {
2617
+ return codeMatch[1].trim();
2618
+ }
2619
+ return response.content;
2620
+ }
2621
+ function generateTestFile(scenario, session) {
2329
2622
  const lines = [];
2330
- lines.push(`import { describe, it, expect } from 'vitest';`);
2623
+ lines.push(`import { describe, it, expect, beforeEach } from 'vitest';`);
2331
2624
  lines.push("");
2625
+ lines.push(`/**`);
2626
+ lines.push(` * ${scenario.feature} \u529F\u80FD\u6D4B\u8BD5`);
2627
+ lines.push(` * `);
2628
+ lines.push(` * BDD \u573A\u666F\u6570\u91CF: ${scenario.scenarios.length}`);
2629
+ if (session?.context?.techStack) {
2630
+ lines.push(` * \u6280\u672F\u6808: ${session.context.techStack.join(", ")}`);
2631
+ }
2632
+ lines.push(` */`);
2332
2633
  lines.push(`describe('${scenario.feature}', () => {`);
2333
2634
  for (const s of scenario.scenarios) {
2334
- lines.push(` it('${s.name}', () => {`);
2335
- lines.push(` // Given: ${s.given.join(", ")}`);
2336
- lines.push(` // When: ${s.when.join(", ")}`);
2337
- lines.push(` // Then: ${s.then.join(", ")}`);
2338
- lines.push(` expect(true).toBe(true); // TODO: \u5B9E\u73B0\u6D4B\u8BD5`);
2339
- lines.push(` });`);
2340
2635
  lines.push("");
2636
+ lines.push(` /**`);
2637
+ lines.push(` * \u573A\u666F: ${s.name}`);
2638
+ lines.push(` * Given: ${s.given.join(", ")}`);
2639
+ lines.push(` * When: ${s.when.join(", ")}`);
2640
+ lines.push(` * Then: ${s.then.join(", ")}`);
2641
+ lines.push(` */`);
2642
+ lines.push(` it('${s.name}', async () => {`);
2643
+ lines.push(` // Arrange (Given)`);
2644
+ for (const g of s.given) {
2645
+ lines.push(` // ${g}`);
2646
+ }
2647
+ lines.push(` const input = {}; // TODO: \u8BBE\u7F6E\u521D\u59CB\u72B6\u6001`);
2648
+ lines.push("");
2649
+ lines.push(` // Act (When)`);
2650
+ for (const w of s.when) {
2651
+ lines.push(` // ${w}`);
2652
+ }
2653
+ lines.push(` const result = {}; // TODO: \u6267\u884C\u64CD\u4F5C`);
2654
+ lines.push("");
2655
+ lines.push(` // Assert (Then)`);
2656
+ for (const t of s.then) {
2657
+ lines.push(` // ${t}`);
2658
+ }
2659
+ lines.push(` expect(result).toBeDefined(); // TODO: \u5B8C\u5584\u65AD\u8A00`);
2660
+ lines.push(` });`);
2341
2661
  }
2662
+ lines.push("");
2342
2663
  lines.push(`});`);
2343
2664
  return lines.join("\n");
2344
2665
  }
@@ -2379,6 +2700,124 @@ function generateSessionId() {
2379
2700
  const random = Math.random().toString(36).slice(2, 6);
2380
2701
  return `WF-${timestamp}-${random}`.toUpperCase();
2381
2702
  }
2703
+ function extractUrls(text) {
2704
+ const urlRegex = /https?:\/\/[^\s<>"{}|\\^`\[\]]+/gi;
2705
+ const matches = text.match(urlRegex);
2706
+ return matches ? [...new Set(matches)] : [];
2707
+ }
2708
+ async function fetchAndAnalyzeReference(url, ctx) {
2709
+ const type = detectResourceType(url);
2710
+ let content = "";
2711
+ let analysis = "";
2712
+ try {
2713
+ const response = await fetch(url, {
2714
+ headers: {
2715
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
2716
+ }
2717
+ });
2718
+ if (!response.ok) {
2719
+ throw new Error(`HTTP ${response.status}`);
2720
+ }
2721
+ content = await response.text();
2722
+ if (ctx.modelService.getCurrentModel()) {
2723
+ analysis = await analyzeReferenceContent(url, content, type, ctx);
2724
+ } else {
2725
+ analysis = extractBasicInfo(content, type);
2726
+ }
2727
+ } catch (error) {
2728
+ throw new Error(`\u65E0\u6CD5\u83B7\u53D6\u53C2\u8003\u8D44\u6E90: ${error.message}`);
2729
+ }
2730
+ return { url, type, content: content.slice(0, 1e4), analysis };
2731
+ }
2732
+ function detectResourceType(url) {
2733
+ if (url.includes("figma.com") || url.includes("lanhuapp.com")) {
2734
+ return "design";
2735
+ }
2736
+ if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(url)) {
2737
+ return "image";
2738
+ }
2739
+ if (/api\//i.test(url)) {
2740
+ return "api";
2741
+ }
2742
+ return "webpage";
2743
+ }
2744
+ async function analyzeReferenceContent(url, content, type, ctx) {
2745
+ const prompt2 = `
2746
+ \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
2747
+
2748
+ ## \u53C2\u8003\u8D44\u6E90\u4FE1\u606F
2749
+ - URL: ${url}
2750
+ - \u7C7B\u578B: ${type}
2751
+
2752
+ ## \u7F51\u9875\u5185\u5BB9
2753
+ \`\`\`html
2754
+ ${content.slice(0, 8e3)}
2755
+ \`\`\`
2756
+
2757
+ ## \u5206\u6790\u8981\u6C42
2758
+
2759
+ \u8BF7\u6309\u7167\u4EE5\u4E0B\u7ED3\u6784\u8FDB\u884C\u8BE6\u7EC6\u5206\u6790\uFF1A
2760
+
2761
+ ### 1. \u4E1A\u52A1\u529F\u80FD\u5206\u6790
2762
+ - \u6838\u5FC3\u4E1A\u52A1\u529F\u80FD\u662F\u4EC0\u4E48\uFF1F\uFF08\u8BE6\u7EC6\u63CF\u8FF0\uFF09
2763
+ - \u7528\u6237\u53EF\u4EE5\u505A\u4EC0\u4E48\u64CD\u4F5C\uFF1F
2764
+ - \u4E1A\u52A1\u6D41\u7A0B\u662F\u4EC0\u4E48\uFF1F
2765
+ - \u6570\u636E\u8F93\u5165\u8F93\u51FA\u662F\u4EC0\u4E48\uFF1F
2766
+
2767
+ ### 2. UI/UX \u7ED3\u6784\u5206\u6790
2768
+ - \u9875\u9762\u5E03\u5C40\u7ED3\u6784
2769
+ - \u4E3B\u8981\u7EC4\u4EF6\u6709\u54EA\u4E9B\uFF1F
2770
+ - \u7EC4\u4EF6\u4E4B\u95F4\u7684\u5173\u7CFB
2771
+ - \u4EA4\u4E92\u65B9\u5F0F\uFF08\u70B9\u51FB\u3001\u8F93\u5165\u3001\u62D6\u62FD\u7B49\uFF09
2772
+
2773
+ ### 3. \u6570\u636E\u6A21\u578B\u5206\u6790
2774
+ - \u9700\u8981\u54EA\u4E9B\u6570\u636E\uFF1F
2775
+ - \u6570\u636E\u4E4B\u95F4\u7684\u5173\u7CFB
2776
+ - \u6570\u636E\u6765\u6E90\uFF08\u7528\u6237\u8F93\u5165/\u8BA1\u7B97/API\uFF09
2777
+
2778
+ ### 4. \u529F\u80FD\u62C6\u5206\u5EFA\u8BAE
2779
+ \u8BF7\u5C06\u529F\u80FD\u62C6\u5206\u4E3A\u53EF\u72EC\u7ACB\u5F00\u53D1\u7684\u4EFB\u52A1\uFF0C\u6BCF\u4E2A\u4EFB\u52A1\u5305\u542B\uFF1A
2780
+ - \u4EFB\u52A1\u540D\u79F0
2781
+ - \u4EFB\u52A1\u63CF\u8FF0
2782
+ - \u6280\u672F\u8981\u70B9
2783
+ - \u4F9D\u8D56\u5173\u7CFB
2784
+
2785
+ ### 5. \u6280\u672F\u5B9E\u73B0\u5EFA\u8BAE
2786
+ - \u63A8\u8350\u7684\u6280\u672F\u65B9\u6848
2787
+ - \u9700\u8981\u6CE8\u610F\u7684\u6280\u672F\u96BE\u70B9
2788
+ - \u6027\u80FD\u4F18\u5316\u5EFA\u8BAE
2789
+
2790
+ \u8BF7\u4EE5 Markdown \u683C\u5F0F\u8F93\u51FA\uFF0C\u91CD\u70B9\u7A81\u51FA\u4E1A\u52A1\u903B\u8F91\u548C\u529F\u80FD\u5B9E\u73B0\u7EC6\u8282\u3002
2791
+ `;
2792
+ const loader = new LoadingIndicator("AI \u6B63\u5728\u5206\u6790\u53C2\u8003\u8D44\u6E90");
2793
+ loader.start();
2794
+ try {
2795
+ const response = await ctx.modelService.sendMessage([
2796
+ { role: "user", content: prompt2 }
2797
+ ], {
2798
+ temperature: 0.3,
2799
+ maxTokens: 4e3
2800
+ });
2801
+ loader.stop(chalk9__default.default.green(" \u2713 \u5206\u6790\u5B8C\u6210"));
2802
+ return response.content;
2803
+ } catch (error) {
2804
+ loader.stop();
2805
+ throw error;
2806
+ }
2807
+ }
2808
+ function extractBasicInfo(content, type) {
2809
+ const titleMatch = content.match(/<title[^>]*>([^<]+)<\/title>/i);
2810
+ const descMatch = content.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i);
2811
+ const parts = [];
2812
+ if (titleMatch) {
2813
+ parts.push(`\u6807\u9898: ${titleMatch[1]}`);
2814
+ }
2815
+ if (descMatch) {
2816
+ parts.push(`\u63CF\u8FF0: ${descMatch[1]}`);
2817
+ }
2818
+ parts.push(`\u8D44\u6E90\u7C7B\u578B: ${type}`);
2819
+ return parts.join("\n");
2820
+ }
2382
2821
  function generateComplexityBar(score) {
2383
2822
  const filled = Math.round(score / 2);
2384
2823
  const empty = 5 - filled;
@@ -2388,6 +2827,7 @@ function getPhaseLabel(phase) {
2388
2827
  const labels = {
2389
2828
  context: "\u9879\u76EE\u4E0A\u4E0B\u6587\u83B7\u53D6",
2390
2829
  clarify: "\u9700\u6C42\u6F84\u6E05",
2830
+ reference: "\u53C2\u8003\u8D44\u6E90\u5206\u6790",
2391
2831
  analysis: "\u590D\u6742\u5EA6\u8BC4\u4F30",
2392
2832
  bdd: "BDD \u573A\u666F\u62C6\u89E3",
2393
2833
  spec: "OpenSpec \u89C4\u683C",
@@ -2403,10 +2843,43 @@ function getActiveSession() {
2403
2843
  function clearActiveSession() {
2404
2844
  activeSession = null;
2405
2845
  }
2406
- var MAX_FILE_SIZE2, COMPLEXITY_THRESHOLD, CLARITY_THRESHOLD, activeSession, new_default;
2846
+ var LoadingIndicator, MAX_FILE_SIZE2, COMPLEXITY_THRESHOLD, CLARITY_THRESHOLD, activeSession, new_default;
2407
2847
  var init_new = __esm({
2408
2848
  "src/commands/new.ts"() {
2409
2849
  init_cjs_shims();
2850
+ LoadingIndicator = class {
2851
+ frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2852
+ frameIndex = 0;
2853
+ interval = null;
2854
+ message;
2855
+ constructor(message) {
2856
+ this.message = message;
2857
+ }
2858
+ start() {
2859
+ process.stdout.write("\x1B[?25l");
2860
+ this.interval = setInterval(() => {
2861
+ const frame = this.frames[this.frameIndex];
2862
+ process.stdout.write(`\r${chalk9__default.default.cyan(frame)} ${this.message}...`);
2863
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
2864
+ }, 80);
2865
+ }
2866
+ update(message) {
2867
+ this.message = message;
2868
+ }
2869
+ stop(finalMessage) {
2870
+ if (this.interval) {
2871
+ clearInterval(this.interval);
2872
+ this.interval = null;
2873
+ }
2874
+ process.stdout.write("\x1B[?25h");
2875
+ if (finalMessage) {
2876
+ process.stdout.write(`\r${finalMessage}
2877
+ `);
2878
+ } else {
2879
+ process.stdout.write("\r" + " ".repeat(60) + "\r");
2880
+ }
2881
+ }
2882
+ };
2410
2883
  MAX_FILE_SIZE2 = 1024 * 1024;
2411
2884
  COMPLEXITY_THRESHOLD = 6;
2412
2885
  CLARITY_THRESHOLD = 0.6;
@@ -7468,6 +7941,7 @@ var STEP_DISPLAY_NAMES = {
7468
7941
  "archive": "\u5F52\u6863",
7469
7942
  "context": "\u4E0A\u4E0B\u6587",
7470
7943
  "clarify": "\u6F84\u6E05",
7944
+ "reference": "\u53C2\u8003\u5206\u6790",
7471
7945
  "analysis": "\u5206\u6790",
7472
7946
  "bdd": "BDD",
7473
7947
  "spec": "\u89C4\u683C",
@@ -7484,6 +7958,7 @@ var STEP_COLORS = {
7484
7958
  "archive": chalk9__default.default.gray,
7485
7959
  "context": chalk9__default.default.magenta,
7486
7960
  "clarify": chalk9__default.default.yellow,
7961
+ "reference": chalk9__default.default.blue,
7487
7962
  "analysis": chalk9__default.default.blue,
7488
7963
  "bdd": chalk9__default.default.cyan,
7489
7964
  "spec": chalk9__default.default.green,