@nick848/sf-cli 1.0.16 → 1.0.17

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,12 +192,13 @@ 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
197
  activeSession.bddScenarios = generateBDDScenarios(
168
198
  activeSession.refinedRequirement,
169
199
  activeSession.context,
170
- activeSession.clarificationQuestions
200
+ activeSession.clarificationQuestions,
201
+ activeSession.referenceResources
171
202
  );
172
203
  for (const scenario of activeSession.bddScenarios) {
173
204
  lines.push(chalk9.white(` Feature: ${scenario.feature}`));
@@ -182,13 +213,14 @@ async function executeWorkflow(ctx) {
182
213
  }
183
214
  if (activeSession.phase === "spec") {
184
215
  lines.push("");
185
- lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 5/8: OpenSpec \u89C4\u683C \u2501\u2501\u2501"));
216
+ lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 6/9: OpenSpec \u89C4\u683C \u2501\u2501\u2501"));
186
217
  lines.push("");
187
218
  activeSession.specItems = generateSpecItems(
188
219
  activeSession.refinedRequirement,
189
220
  activeSession.context,
190
221
  activeSession.bddScenarios,
191
- activeSession.clarificationQuestions
222
+ activeSession.clarificationQuestions,
223
+ activeSession.referenceResources
192
224
  );
193
225
  const specPath = await saveSpecFile(ctx.options.workingDirectory, activeSession);
194
226
  lines.push(chalk9.green(" \u2713 \u89C4\u683C\u6587\u4EF6\u5DF2\u751F\u6210"));
@@ -212,7 +244,7 @@ async function executeWorkflow(ctx) {
212
244
  }
213
245
  if (activeSession.phase === "tdd") {
214
246
  lines.push("");
215
- lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 6/8: TDD \u6D4B\u8BD5\u751F\u6210 \u2501\u2501\u2501"));
247
+ lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 7/9: TDD \u6D4B\u8BD5\u751F\u6210 \u2501\u2501\u2501"));
216
248
  lines.push("");
217
249
  activeSession.testFiles = await generateTests(ctx.options.workingDirectory, activeSession);
218
250
  lines.push(chalk9.green(" \u2713 \u6D4B\u8BD5\u6587\u4EF6\u5DF2\u751F\u6210"));
@@ -223,7 +255,7 @@ async function executeWorkflow(ctx) {
223
255
  }
224
256
  if (activeSession.phase === "develop") {
225
257
  lines.push("");
226
- lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 7/8: \u5F00\u53D1\u5B9E\u73B0 \u2501\u2501\u2501"));
258
+ lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 8/9: \u5F00\u53D1\u5B9E\u73B0 \u2501\u2501\u2501"));
227
259
  lines.push("");
228
260
  lines.push(chalk9.yellow(" \u{1F680} \u6B63\u5728\u8C03\u7528 AI \u751F\u6210\u4EE3\u7801..."));
229
261
  try {
@@ -248,7 +280,7 @@ async function executeWorkflow(ctx) {
248
280
  }
249
281
  if (activeSession.phase === "review") {
250
282
  lines.push("");
251
- lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 8/8: \u4EE3\u7801\u5BA1\u6838 \u2501\u2501\u2501"));
283
+ lines.push(chalk9.cyan("\u2501\u2501\u2501 \u9636\u6BB5 9/9: \u4EE3\u7801\u5BA1\u6838 \u2501\u2501\u2501"));
252
284
  lines.push("");
253
285
  lines.push(chalk9.yellow(" \u{1F50D} \u6B63\u5728\u8FDB\u884C\u4EE3\u7801\u5BA1\u6838..."));
254
286
  try {
@@ -790,13 +822,41 @@ function analyzeComplexity(requirement, context) {
790
822
  if (!context.framework) score += 0.5;
791
823
  return Math.max(1, Math.min(10, Math.round(score)));
792
824
  }
793
- function generateBDDScenarios(requirement, context, questions) {
825
+ function generateBDDScenarios(requirement, context, questions, references = []) {
794
826
  const scenarios = [];
795
827
  questions.find((q) => q.category === "ui" && q.answered)?.answer;
796
828
  const interactionAnswer = questions.find((q) => q.category === "interaction" && q.answered)?.answer;
797
829
  const edgeAnswer = questions.find((q) => q.category === "edge" && q.answered)?.answer;
830
+ if (references.length > 0) {
831
+ for (const ref of references) {
832
+ const refFeatures = extractFeaturesFromReference(ref);
833
+ for (const feature of refFeatures) {
834
+ const scenario = {
835
+ feature: feature.title,
836
+ description: feature.description,
837
+ scenarios: []
838
+ };
839
+ scenario.scenarios.push({
840
+ name: `\u6B63\u5E38\u6D41\u7A0B: ${feature.title}`,
841
+ given: [`\u7528\u6237\u8FDB\u5165\u76F8\u5173\u9875\u9762`],
842
+ when: [`\u7528\u6237\u6267\u884C "${feature.title}" \u64CD\u4F5C`],
843
+ then: [`\u7CFB\u7EDF\u5E94\u6B63\u786E\u5904\u7406\u5E76\u8FD4\u56DE\u9884\u671F\u7ED3\u679C`]
844
+ });
845
+ if (feature.hasInput) {
846
+ scenario.scenarios.push({
847
+ name: `\u8FB9\u754C\u60C5\u51B5: \u8F93\u5165\u9A8C\u8BC1`,
848
+ given: [`\u7528\u6237\u8FDB\u5165\u8F93\u5165\u754C\u9762`],
849
+ when: [`\u7528\u6237\u8F93\u5165\u8FB9\u754C\u503C\u6216\u7A7A\u503C`],
850
+ then: [`\u7CFB\u7EDF\u5E94\u6B63\u786E\u5904\u7406\u8FB9\u754C\u60C5\u51B5`]
851
+ });
852
+ }
853
+ scenarios.push(scenario);
854
+ }
855
+ }
856
+ }
798
857
  const features = extractFeatures(requirement);
799
858
  for (const feature of features) {
859
+ if (scenarios.some((s) => s.feature === feature.title)) continue;
800
860
  const scenario = {
801
861
  feature: feature.title,
802
862
  description: feature.description,
@@ -828,6 +888,46 @@ function generateBDDScenarios(requirement, context, questions) {
828
888
  }
829
889
  return scenarios;
830
890
  }
891
+ function extractFeaturesFromReference(ref) {
892
+ const features = [];
893
+ const analysis = ref.analysis.toLowerCase();
894
+ if (analysis.includes("\u8F93\u5165") || analysis.includes("\u8868\u5355")) {
895
+ features.push({
896
+ title: "\u8F93\u5165\u8868\u5355",
897
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u8F93\u5165\u8868\u5355\u529F\u80FD",
898
+ hasInput: true
899
+ });
900
+ }
901
+ if (analysis.includes("\u6309\u94AE") || analysis.includes("\u64CD\u4F5C")) {
902
+ features.push({
903
+ title: "\u4EA4\u4E92\u6309\u94AE",
904
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u6309\u94AE\u4EA4\u4E92",
905
+ hasInput: false
906
+ });
907
+ }
908
+ if (analysis.includes("\u8868\u683C") || analysis.includes("\u5217\u8868")) {
909
+ features.push({
910
+ title: "\u6570\u636E\u5217\u8868",
911
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u6570\u636E\u5C55\u793A",
912
+ hasInput: false
913
+ });
914
+ }
915
+ if (analysis.includes("\u56FE\u8868") || analysis.includes("\u53EF\u89C6\u5316")) {
916
+ features.push({
917
+ title: "\u56FE\u8868\u5C55\u793A",
918
+ description: "\u53C2\u8003\u754C\u9762\u4E2D\u7684\u56FE\u8868\u53EF\u89C6\u5316",
919
+ hasInput: false
920
+ });
921
+ }
922
+ if (features.length === 0) {
923
+ features.push({
924
+ title: "\u53C2\u8003\u529F\u80FD\u5B9E\u73B0",
925
+ description: `\u57FA\u4E8E\u53C2\u8003\u8D44\u6E90 ${ref.url} \u5B9E\u73B0\u7684\u529F\u80FD`,
926
+ hasInput: true
927
+ });
928
+ }
929
+ return features;
930
+ }
831
931
  function extractFeatures(requirement) {
832
932
  const features = [];
833
933
  const urlMatch = requirement.match(/https?:\/\/[^\s]+/);
@@ -868,15 +968,26 @@ function extractFeatures(requirement) {
868
968
  }
869
969
  return features;
870
970
  }
871
- function generateSpecItems(requirement, context, bddScenarios, questions) {
971
+ function generateSpecItems(requirement, context, bddScenarios, questions, references = []) {
872
972
  const items = [];
873
973
  let id = 1;
974
+ for (const ref of references) {
975
+ items.push({
976
+ id: `T${id.toString().padStart(3, "0")}`,
977
+ title: `\u53C2\u8003\u5206\u6790: ${ref.type}`,
978
+ description: `\u5206\u6790\u53C2\u8003\u8D44\u6E90 ${ref.url}`,
979
+ priority: "high",
980
+ files: [],
981
+ tests: []
982
+ });
983
+ id++;
984
+ }
874
985
  for (const scenario of bddScenarios) {
875
986
  items.push({
876
987
  id: `T${id.toString().padStart(3, "0")}`,
877
988
  title: scenario.feature,
878
989
  description: scenario.description,
879
- priority: id <= 2 ? "high" : "medium",
990
+ priority: id <= 3 ? "high" : "medium",
880
991
  files: [],
881
992
  tests: []
882
993
  });
@@ -919,6 +1030,19 @@ function formatSpecFile(session) {
919
1030
  lines.push("---");
920
1031
  lines.push("");
921
1032
  }
1033
+ if (session.referenceResources.length > 0) {
1034
+ lines.push("## \u53C2\u8003\u8D44\u6E90");
1035
+ lines.push("");
1036
+ for (const ref of session.referenceResources) {
1037
+ lines.push(`### ${ref.url}`);
1038
+ lines.push(`> \u7C7B\u578B: ${ref.type}`);
1039
+ lines.push("");
1040
+ lines.push(ref.analysis);
1041
+ lines.push("");
1042
+ }
1043
+ lines.push("---");
1044
+ lines.push("");
1045
+ }
922
1046
  if (session.clarificationQuestions.some((q) => q.answered)) {
923
1047
  lines.push("## \u9700\u6C42\u6F84\u6E05");
924
1048
  lines.push("");
@@ -1024,6 +1148,96 @@ function generateSessionId() {
1024
1148
  const random = Math.random().toString(36).slice(2, 6);
1025
1149
  return `WF-${timestamp}-${random}`.toUpperCase();
1026
1150
  }
1151
+ function extractUrls(text) {
1152
+ const urlRegex = /https?:\/\/[^\s<>"{}|\\^`\[\]]+/gi;
1153
+ const matches = text.match(urlRegex);
1154
+ return matches ? [...new Set(matches)] : [];
1155
+ }
1156
+ async function fetchAndAnalyzeReference(url, ctx) {
1157
+ const type = detectResourceType(url);
1158
+ let content = "";
1159
+ let analysis = "";
1160
+ try {
1161
+ const response = await fetch(url, {
1162
+ headers: {
1163
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
1164
+ }
1165
+ });
1166
+ if (!response.ok) {
1167
+ throw new Error(`HTTP ${response.status}`);
1168
+ }
1169
+ content = await response.text();
1170
+ if (ctx.modelService.getCurrentModel()) {
1171
+ analysis = await analyzeReferenceContent(url, content, type, ctx);
1172
+ } else {
1173
+ analysis = extractBasicInfo(content, type);
1174
+ }
1175
+ } catch (error) {
1176
+ throw new Error(`\u65E0\u6CD5\u83B7\u53D6\u53C2\u8003\u8D44\u6E90: ${error.message}`);
1177
+ }
1178
+ return { url, type, content: content.slice(0, 1e4), analysis };
1179
+ }
1180
+ function detectResourceType(url) {
1181
+ if (url.includes("figma.com") || url.includes("lanhuapp.com")) {
1182
+ return "design";
1183
+ }
1184
+ if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(url)) {
1185
+ return "image";
1186
+ }
1187
+ if (/api\//i.test(url)) {
1188
+ return "api";
1189
+ }
1190
+ return "webpage";
1191
+ }
1192
+ async function analyzeReferenceContent(url, content, type, ctx) {
1193
+ const typePrompts = {
1194
+ webpage: "\u5206\u6790\u8FD9\u4E2A\u7F51\u9875\u7684\u529F\u80FD\u3001UI\u7EC4\u4EF6\u548C\u4EA4\u4E92\u65B9\u5F0F",
1195
+ design: "\u5206\u6790\u8FD9\u4E2A\u8BBE\u8BA1\u7A3F\u7684\u5E03\u5C40\u3001\u7EC4\u4EF6\u548C\u6837\u5F0F",
1196
+ image: "\u63CF\u8FF0\u8FD9\u4E2A\u56FE\u7247\u7684\u5185\u5BB9\u548C\u8BBE\u8BA1\u5143\u7D20",
1197
+ api: "\u5206\u6790\u8FD9\u4E2AAPI\u7684\u7ED3\u6784\u548C\u53C2\u6570"
1198
+ };
1199
+ const prompt2 = `
1200
+ \u8BF7\u5206\u6790\u4EE5\u4E0B\u53C2\u8003\u8D44\u6E90\u7684 URL\uFF0C\u63D0\u53D6\u5BF9\u5F00\u53D1\u6709\u7528\u7684\u4FE1\u606F\uFF1A
1201
+
1202
+ URL: ${url}
1203
+ \u7C7B\u578B: ${type}
1204
+
1205
+ \u5185\u5BB9\u6458\u8981:
1206
+ ${content.slice(0, 5e3)}
1207
+
1208
+ ${typePrompts[type]}
1209
+
1210
+ \u8BF7\u63D0\u53D6\uFF1A
1211
+ 1. \u4E3B\u8981\u529F\u80FD\u70B9
1212
+ 2. UI\u7EC4\u4EF6\u7ED3\u6784
1213
+ 3. \u4EA4\u4E92\u65B9\u5F0F
1214
+ 4. \u6570\u636E\u7ED3\u6784\uFF08\u5982\u679C\u6709\uFF09
1215
+ 5. \u6280\u672F\u5B9E\u73B0\u5EFA\u8BAE
1216
+
1217
+ \u4EE5\u7B80\u6D01\u7684\u8981\u70B9\u5F62\u5F0F\u8F93\u51FA\u3002
1218
+ `;
1219
+ try {
1220
+ const response = await ctx.modelService.sendMessage([
1221
+ { role: "user", content: prompt2 }
1222
+ ], { temperature: 0.3, maxTokens: 2e3 });
1223
+ return response.content;
1224
+ } catch {
1225
+ return extractBasicInfo(content, type);
1226
+ }
1227
+ }
1228
+ function extractBasicInfo(content, type) {
1229
+ const titleMatch = content.match(/<title[^>]*>([^<]+)<\/title>/i);
1230
+ const descMatch = content.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i);
1231
+ const parts = [];
1232
+ if (titleMatch) {
1233
+ parts.push(`\u6807\u9898: ${titleMatch[1]}`);
1234
+ }
1235
+ if (descMatch) {
1236
+ parts.push(`\u63CF\u8FF0: ${descMatch[1]}`);
1237
+ }
1238
+ parts.push(`\u8D44\u6E90\u7C7B\u578B: ${type}`);
1239
+ return parts.join("\n");
1240
+ }
1027
1241
  function generateComplexityBar(score) {
1028
1242
  const filled = Math.round(score / 2);
1029
1243
  const empty = 5 - filled;
@@ -1033,6 +1247,7 @@ function getPhaseLabel(phase) {
1033
1247
  const labels = {
1034
1248
  context: "\u9879\u76EE\u4E0A\u4E0B\u6587\u83B7\u53D6",
1035
1249
  clarify: "\u9700\u6C42\u6F84\u6E05",
1250
+ reference: "\u53C2\u8003\u8D44\u6E90\u5206\u6790",
1036
1251
  analysis: "\u590D\u6742\u5EA6\u8BC4\u4F30",
1037
1252
  bdd: "BDD \u573A\u666F\u62C6\u89E3",
1038
1253
  spec: "OpenSpec \u89C4\u683C",