mcp-feature-doc-generator 1.0.0 → 1.0.2

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.
@@ -0,0 +1,15 @@
1
+ export type FeatureDocContextBundle = {
2
+ featurePath: string;
3
+ featureDir: string;
4
+ outputPath: string;
5
+ promptText: string;
6
+ featureFiles: string[];
7
+ dependencyFiles: string[];
8
+ componentNames: string[];
9
+ apiImports: string[];
10
+ dictImports: string[];
11
+ permissionTokens: string[];
12
+ warnings: string[];
13
+ renderedText: string;
14
+ };
15
+ export declare function collectFeatureDocContext(rawFeaturePath: string, promptText: string, workspaceRoot?: string): FeatureDocContextBundle;
@@ -0,0 +1,491 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, extname, relative, resolve, sep } from "node:path";
3
+ const SOURCE_EXTENSIONS = [".vue", ".ts", ".js"];
4
+ const PRIMARY_FILE_ORDER = ["index.vue", "form.vue", "options.ts", "options.js"];
5
+ const EXCLUDED_TOP_LEVEL_DIRS = new Set(["error", "login", "demo", "i18n", "components"]);
6
+ const FORCED_IGNORE_NAMES = new Set([
7
+ "microme-operator",
8
+ "upload-file",
9
+ "process",
10
+ "customquery",
11
+ "tablecolumndrawer",
12
+ "exportexcel",
13
+ "circulationform",
14
+ ]);
15
+ const MAX_FEATURE_FILES = 10;
16
+ const MAX_DEPENDENCY_FILES = 4;
17
+ const MAX_SNIPPET_LINES = 220;
18
+ const MAX_SNIPPET_MATCHES = 18;
19
+ const DYNAMIC_IMPORT_REGEX = /import\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
20
+ const STATIC_IMPORT_REGEX = /from\s+["'`]([^"'`]+)["'`]/g;
21
+ const COMPONENT_NAME_REGEXES = [
22
+ /<script[^>]*\bname=["'`]([^"'`]+)["'`][^>]*>/g,
23
+ /defineOptions\s*\(\s*{[\s\S]*?\bname\s*:\s*["'`]([^"'`]+)["'`]/g,
24
+ ];
25
+ const PERMISSION_ATTRIBUTE_REGEXES = [
26
+ /(?:v-auth|v-hasPermi)\s*=\s*"([^"]*)"/g,
27
+ /(?:v-auth|v-hasPermi)\s*=\s*'([^']*)'/g,
28
+ ];
29
+ const PERMISSION_TOKEN_REGEX = /[A-Za-z][A-Za-z0-9:-]*(?:_[A-Za-z0-9:-]+)+/g;
30
+ const SNIPPET_HINT_REGEX = /defineAsyncComponent|from\s+['"`]\/@\/api|from\s+['"`]\.\/|from\s+['"`]\.\.\/|defineProps|defineEmits|watch\s*\(|computed\s*\(|submit|save|validate|fetch|addObj|putObj|delObj|getObj|openDialog|processDefKey|useRoute|useI18n|<el-table|<el-form|<template|@click|@change|@submit/i;
31
+ export function collectFeatureDocContext(rawFeaturePath, promptText, workspaceRoot = detectWorkspaceRoot(process.cwd())) {
32
+ const featurePath = normalizeFeaturePath(rawFeaturePath);
33
+ const topLevelDir = featurePath.split("/")[0];
34
+ if (EXCLUDED_TOP_LEVEL_DIRS.has(topLevelDir)) {
35
+ throw new Error(`指定路径位于非业务目录:src/views/${featurePath}`);
36
+ }
37
+ const featureDir = resolve(workspaceRoot, "src", "views", ...featurePath.split("/"));
38
+ if (!existsSync(featureDir) || !statSync(featureDir).isDirectory()) {
39
+ throw new Error(`功能目录不存在:src/views/${featurePath}`);
40
+ }
41
+ const allFeatureFiles = walkSourceFiles(featureDir);
42
+ if (allFeatureFiles.length === 0) {
43
+ throw new Error(`功能目录下未找到可分析的 vue/ts/js 文件:src/views/${featurePath}`);
44
+ }
45
+ const warnings = [];
46
+ const hasPrimaryFile = allFeatureFiles.some((filePath) => PRIMARY_FILE_ORDER.includes(relative(featureDir, filePath).split(sep).join("/")));
47
+ if (!hasPrimaryFile) {
48
+ warnings.push("未检测到 index.vue、form.vue 或 options.ts,已退化为分析目录下的源码文件。");
49
+ }
50
+ const featureFiles = sortSourceFiles(allFeatureFiles, featureDir).slice(0, MAX_FEATURE_FILES);
51
+ const dependencyFiles = collectDependencyFiles(featureFiles, featureDir, workspaceRoot).slice(0, MAX_DEPENDENCY_FILES);
52
+ const insights = [
53
+ ...featureFiles.map((filePath) => analyzeFile(filePath, "feature", workspaceRoot, featureDir)),
54
+ ...dependencyFiles.map((filePath) => analyzeFile(filePath, "dependency", workspaceRoot, featureDir)),
55
+ ];
56
+ const componentNames = unique(insights.flatMap((item) => item.componentNames));
57
+ const apiImports = unique(insights.flatMap((item) => item.apiImports));
58
+ const dictImports = unique(insights.flatMap((item) => item.dictImports));
59
+ const permissionTokens = unique(insights.flatMap((item) => item.permissionTokens));
60
+ const renderedText = renderContextBundle({
61
+ featurePath,
62
+ outputPath: `开发规范/views/${featurePath}-README.md`,
63
+ promptText,
64
+ insights,
65
+ componentNames,
66
+ apiImports,
67
+ dictImports,
68
+ permissionTokens,
69
+ warnings,
70
+ });
71
+ return {
72
+ featurePath,
73
+ featureDir,
74
+ outputPath: `开发规范/views/${featurePath}-README.md`,
75
+ promptText,
76
+ featureFiles: featureFiles.map((filePath) => toRepoPath(filePath, workspaceRoot)),
77
+ dependencyFiles: dependencyFiles.map((filePath) => toRepoPath(filePath, workspaceRoot)),
78
+ componentNames,
79
+ apiImports,
80
+ dictImports,
81
+ permissionTokens,
82
+ warnings,
83
+ renderedText,
84
+ };
85
+ }
86
+ function detectWorkspaceRoot(startDir) {
87
+ let currentDir = resolve(startDir);
88
+ while (true) {
89
+ const viewsDir = resolve(currentDir, "src", "views");
90
+ if (existsSync(viewsDir) && statSync(viewsDir).isDirectory()) {
91
+ return currentDir;
92
+ }
93
+ const parentDir = resolve(currentDir, "..");
94
+ if (parentDir === currentDir) {
95
+ break;
96
+ }
97
+ currentDir = parentDir;
98
+ }
99
+ throw new Error("未找到项目根目录,请确保当前工作目录或其父目录下存在 src/views。");
100
+ }
101
+ function normalizeFeaturePath(rawFeaturePath) {
102
+ const featurePath = rawFeaturePath.trim().replace(/\\/g, "/").replace(/^src\/views\//, "").replace(/^\/+|\/+$/g, "");
103
+ if (!featurePath) {
104
+ throw new Error("featurePath 不能为空。");
105
+ }
106
+ const segments = featurePath.split("/");
107
+ if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
108
+ throw new Error(`featurePath 非法:${rawFeaturePath}`);
109
+ }
110
+ return segments.join("/");
111
+ }
112
+ function walkSourceFiles(rootDir) {
113
+ const results = [];
114
+ for (const entry of readdirSync(rootDir, { withFileTypes: true })) {
115
+ const fullPath = resolve(rootDir, entry.name);
116
+ if (entry.isDirectory()) {
117
+ results.push(...walkSourceFiles(fullPath));
118
+ continue;
119
+ }
120
+ if (SOURCE_EXTENSIONS.includes(extname(entry.name))) {
121
+ results.push(fullPath);
122
+ }
123
+ }
124
+ return results;
125
+ }
126
+ function sortSourceFiles(filePaths, baseDir) {
127
+ return [...filePaths].sort((left, right) => {
128
+ const leftRelative = relative(baseDir, left).split(sep).join("/");
129
+ const rightRelative = relative(baseDir, right).split(sep).join("/");
130
+ const leftPriority = filePriority(leftRelative);
131
+ const rightPriority = filePriority(rightRelative);
132
+ if (leftPriority !== rightPriority) {
133
+ return leftPriority - rightPriority;
134
+ }
135
+ return leftRelative.localeCompare(rightRelative);
136
+ });
137
+ }
138
+ function filePriority(relativePath) {
139
+ const exactIndex = PRIMARY_FILE_ORDER.indexOf(relativePath);
140
+ if (exactIndex >= 0) {
141
+ return exactIndex;
142
+ }
143
+ const fileName = relativePath.split("/").pop() ?? relativePath;
144
+ const fallbackIndex = PRIMARY_FILE_ORDER.indexOf(fileName);
145
+ if (fallbackIndex >= 0) {
146
+ return PRIMARY_FILE_ORDER.length + fallbackIndex;
147
+ }
148
+ if (relativePath.endsWith(".vue")) {
149
+ return 20;
150
+ }
151
+ if (relativePath.endsWith(".ts")) {
152
+ return 30;
153
+ }
154
+ return 40;
155
+ }
156
+ function collectDependencyFiles(featureFiles, featureDir, workspaceRoot) {
157
+ const dependencies = new Set();
158
+ for (const filePath of featureFiles) {
159
+ const source = readFileSync(filePath, "utf-8");
160
+ for (const specifier of extractImportSpecifiers(source)) {
161
+ const resolvedPath = resolveViewImport(filePath, specifier, workspaceRoot);
162
+ if (!resolvedPath) {
163
+ continue;
164
+ }
165
+ if (isInsideDir(resolvedPath, featureDir)) {
166
+ continue;
167
+ }
168
+ if (isForcedIgnored(resolvedPath)) {
169
+ continue;
170
+ }
171
+ dependencies.add(resolvedPath);
172
+ }
173
+ }
174
+ return sortSourceFiles([...dependencies], resolve(workspaceRoot, "src", "views"));
175
+ }
176
+ function extractImportSpecifiers(source) {
177
+ const values = extractStaticImportStatements(source)
178
+ .map((statement) => parseImportStatement(statement)?.specifier)
179
+ .filter((value) => Boolean(value));
180
+ DYNAMIC_IMPORT_REGEX.lastIndex = 0;
181
+ let match = DYNAMIC_IMPORT_REGEX.exec(source);
182
+ while (match) {
183
+ values.push(match[1]);
184
+ match = DYNAMIC_IMPORT_REGEX.exec(source);
185
+ }
186
+ return unique(values);
187
+ }
188
+ function resolveViewImport(sourceFilePath, specifier, workspaceRoot) {
189
+ const normalizedSpecifier = specifier.trim();
190
+ let candidate;
191
+ if (normalizedSpecifier.startsWith("/@/views/")) {
192
+ candidate = resolve(workspaceRoot, "src", "views", normalizedSpecifier.slice("/@/views/".length));
193
+ }
194
+ else if (normalizedSpecifier.startsWith("@/views/")) {
195
+ candidate = resolve(workspaceRoot, "src", "views", normalizedSpecifier.slice("@/views/".length));
196
+ }
197
+ else if (normalizedSpecifier.startsWith("src/views/")) {
198
+ candidate = resolve(workspaceRoot, normalizedSpecifier);
199
+ }
200
+ else if (normalizedSpecifier.startsWith("./") || normalizedSpecifier.startsWith("../")) {
201
+ candidate = resolve(dirname(sourceFilePath), normalizedSpecifier);
202
+ }
203
+ if (!candidate) {
204
+ return undefined;
205
+ }
206
+ return resolveSourceCandidate(candidate);
207
+ }
208
+ function resolveSourceCandidate(candidate) {
209
+ if (existsSync(candidate)) {
210
+ const stats = statSync(candidate);
211
+ if (stats.isFile() && SOURCE_EXTENSIONS.includes(extname(candidate))) {
212
+ return candidate;
213
+ }
214
+ if (stats.isDirectory()) {
215
+ for (const fileName of PRIMARY_FILE_ORDER) {
216
+ const entry = resolve(candidate, fileName);
217
+ if (existsSync(entry) && statSync(entry).isFile()) {
218
+ return entry;
219
+ }
220
+ }
221
+ const nestedFiles = walkSourceFiles(candidate);
222
+ if (nestedFiles.length > 0) {
223
+ return sortSourceFiles(nestedFiles, candidate)[0];
224
+ }
225
+ }
226
+ }
227
+ for (const extension of SOURCE_EXTENSIONS) {
228
+ const filePath = `${candidate}${extension}`;
229
+ if (existsSync(filePath) && statSync(filePath).isFile()) {
230
+ return filePath;
231
+ }
232
+ }
233
+ for (const fileName of PRIMARY_FILE_ORDER) {
234
+ const filePath = resolve(candidate, fileName);
235
+ if (existsSync(filePath) && statSync(filePath).isFile()) {
236
+ return filePath;
237
+ }
238
+ }
239
+ return undefined;
240
+ }
241
+ function analyzeFile(filePath, scope, workspaceRoot, featureDir) {
242
+ const source = readFileSync(filePath, "utf-8");
243
+ return {
244
+ repoPath: toRepoPath(filePath, workspaceRoot),
245
+ scope,
246
+ componentNames: extractComponentNames(source),
247
+ apiImports: extractApiImports(source),
248
+ dictImports: extractDictImports(source),
249
+ permissionTokens: extractPermissionTokens(source),
250
+ viewImports: extractImportSpecifiers(source)
251
+ .map((specifier) => resolveViewImport(filePath, specifier, workspaceRoot))
252
+ .filter((value) => Boolean(value))
253
+ .filter((resolvedPath) => !isInsideDir(resolvedPath, featureDir))
254
+ .filter((resolvedPath) => !isForcedIgnored(resolvedPath))
255
+ .map((resolvedPath) => toRepoPath(resolvedPath, workspaceRoot)),
256
+ snippet: buildSnippet(source),
257
+ };
258
+ }
259
+ function extractComponentNames(source) {
260
+ const values = [];
261
+ for (const regex of COMPONENT_NAME_REGEXES) {
262
+ regex.lastIndex = 0;
263
+ let match = regex.exec(source);
264
+ while (match) {
265
+ values.push(match[1]);
266
+ match = regex.exec(source);
267
+ }
268
+ }
269
+ return unique(values);
270
+ }
271
+ function extractApiImports(source) {
272
+ return unique(extractStaticImportStatements(source)
273
+ .map((statement) => parseImportStatement(statement))
274
+ .filter((parsed) => Boolean(parsed))
275
+ .filter((parsed) => parsed.specifier.startsWith("/@/api/"))
276
+ .map((parsed) => `${parsed.specifier.slice("/@/api/".length)} :: ${compactImportNames(parsed.clause)}`));
277
+ }
278
+ function extractDictImports(source) {
279
+ return unique(extractStaticImportStatements(source)
280
+ .map((statement) => parseImportStatement(statement))
281
+ .filter((parsed) => Boolean(parsed))
282
+ .filter((parsed) => parsed.specifier === "/@/enums/dict")
283
+ .map((parsed) => compactImportNames(parsed.clause)));
284
+ }
285
+ function extractPermissionTokens(source) {
286
+ const values = [];
287
+ for (const regex of PERMISSION_ATTRIBUTE_REGEXES) {
288
+ regex.lastIndex = 0;
289
+ let match = regex.exec(source);
290
+ while (match) {
291
+ const attributeValue = match[1];
292
+ const tokens = attributeValue.match(PERMISSION_TOKEN_REGEX);
293
+ if (tokens) {
294
+ values.push(...tokens);
295
+ }
296
+ match = regex.exec(source);
297
+ }
298
+ }
299
+ return unique(values);
300
+ }
301
+ function compactImportNames(rawImportClause) {
302
+ return rawImportClause
303
+ .replace(/\btype\b/g, "")
304
+ .replace(/\s+/g, " ")
305
+ .replace(/[{}]/g, "")
306
+ .trim();
307
+ }
308
+ function buildSnippet(source) {
309
+ const normalizedSource = source.replace(/\r\n/g, "\n");
310
+ const lines = normalizedSource.split("\n");
311
+ if (lines.length <= MAX_SNIPPET_LINES) {
312
+ return formatLineRange(lines, 0, lines.length - 1);
313
+ }
314
+ const ranges = [[0, Math.min(lines.length - 1, 119)]];
315
+ const interestingLines = [];
316
+ for (let index = 0; index < lines.length; index += 1) {
317
+ if (SNIPPET_HINT_REGEX.test(lines[index])) {
318
+ interestingLines.push(index);
319
+ if (interestingLines.length >= MAX_SNIPPET_MATCHES) {
320
+ break;
321
+ }
322
+ }
323
+ }
324
+ for (const lineIndex of interestingLines) {
325
+ ranges.push([Math.max(0, lineIndex - 6), Math.min(lines.length - 1, lineIndex + 12)]);
326
+ }
327
+ const mergedRanges = mergeRanges(ranges);
328
+ const output = [];
329
+ let emittedLines = 0;
330
+ let previousEnd = -1;
331
+ for (const [start, end] of mergedRanges) {
332
+ if (emittedLines >= MAX_SNIPPET_LINES) {
333
+ break;
334
+ }
335
+ if (start > previousEnd + 1) {
336
+ output.push("...");
337
+ }
338
+ const availableLines = MAX_SNIPPET_LINES - emittedLines;
339
+ const cappedEnd = Math.min(end, start + availableLines - 1);
340
+ output.push(formatLineRange(lines, start, cappedEnd));
341
+ emittedLines += cappedEnd - start + 1;
342
+ previousEnd = cappedEnd;
343
+ }
344
+ if (previousEnd < lines.length - 1) {
345
+ output.push("...");
346
+ }
347
+ return output.join("\n");
348
+ }
349
+ function mergeRanges(ranges) {
350
+ const sortedRanges = [...ranges].sort((left, right) => left[0] - right[0]);
351
+ const merged = [];
352
+ for (const [start, end] of sortedRanges) {
353
+ const previous = merged[merged.length - 1];
354
+ if (!previous || start > previous[1] + 1) {
355
+ merged.push([start, end]);
356
+ continue;
357
+ }
358
+ previous[1] = Math.max(previous[1], end);
359
+ }
360
+ return merged;
361
+ }
362
+ function formatLineRange(lines, start, end) {
363
+ const output = [];
364
+ for (let index = start; index <= end; index += 1) {
365
+ output.push(`${String(index + 1).padStart(4, " ")} | ${lines[index]}`);
366
+ }
367
+ return output.join("\n");
368
+ }
369
+ function renderContextBundle(params) {
370
+ const { featurePath, outputPath, promptText, insights, componentNames, apiImports, dictImports, permissionTokens, warnings } = params;
371
+ const featureInsights = insights.filter((item) => item.scope === "feature");
372
+ const dependencyInsights = insights.filter((item) => item.scope === "dependency");
373
+ const blocks = [];
374
+ blocks.push("请直接基于下面的固定提示词与代码上下文继续完成任务,不要要求用户重复提供整段提示词。");
375
+ blocks.push("");
376
+ blocks.push("## 本次目标");
377
+ blocks.push(`- featurePath: \`src/views/${featurePath}\``);
378
+ blocks.push(`- 建议输出: \`${outputPath}\``);
379
+ blocks.push("");
380
+ blocks.push("## 固定提示词");
381
+ blocks.push("```text");
382
+ blocks.push(promptText);
383
+ blocks.push("```");
384
+ blocks.push("");
385
+ blocks.push("## 自动收集的上下文摘要");
386
+ blocks.push(`- 主功能源码文件: ${joinList(featureInsights.map((item) => `\`${item.repoPath}\``))}`);
387
+ blocks.push(`- 直接引用的业务文件: ${joinList(dependencyInsights.map((item) => `\`${item.repoPath}\``), "未检测到")}`);
388
+ blocks.push(`- 组件名称: ${joinList(componentNames.map((item) => `\`${item}\``), "未检测到")}`);
389
+ blocks.push(`- API 模块: ${joinList(apiImports.map((item) => `\`${item}\``), "未检测到")}`);
390
+ blocks.push(`- 字典引用: ${joinList(dictImports.map((item) => `\`${item}\``), "未检测到")}`);
391
+ blocks.push(`- 权限标识: ${joinList(permissionTokens.map((item) => `\`${item}\``), "未检测到")}`);
392
+ if (warnings.length > 0) {
393
+ blocks.push(`- 额外提示: ${joinList(warnings, "无")}`);
394
+ }
395
+ blocks.push("");
396
+ blocks.push("## 相关源码片段");
397
+ for (const insight of insights) {
398
+ blocks.push("");
399
+ blocks.push(`### ${insight.repoPath} [${insight.scope}]`);
400
+ if (insight.viewImports.length > 0) {
401
+ blocks.push(`- 直接引用的业务视图: ${joinList(insight.viewImports.map((item) => `\`${item}\``))}`);
402
+ }
403
+ if (insight.apiImports.length > 0) {
404
+ blocks.push(`- API 导入: ${joinList(insight.apiImports.map((item) => `\`${item}\``))}`);
405
+ }
406
+ if (insight.dictImports.length > 0) {
407
+ blocks.push(`- 字典导入: ${joinList(insight.dictImports.map((item) => `\`${item}\``))}`);
408
+ }
409
+ if (insight.permissionTokens.length > 0) {
410
+ blocks.push(`- 权限标识: ${joinList(insight.permissionTokens.map((item) => `\`${item}\``))}`);
411
+ }
412
+ blocks.push("```text");
413
+ blocks.push(insight.snippet);
414
+ blocks.push("```");
415
+ }
416
+ blocks.push("");
417
+ blocks.push("## 继续执行要求");
418
+ blocks.push("- 直接使用上面的固定提示词,不要改写提示词规则。");
419
+ blocks.push(`- 仅基于 \`src/views/${featurePath}\` 及已收集到的直接引用业务文件继续分析。`);
420
+ blocks.push("- 如果上下文不足,再以当前已识别的文件路径为线索做最小增量阅读,不要扩展到未引用功能。");
421
+ return blocks.join("\n");
422
+ }
423
+ function joinList(values, emptyText = "无") {
424
+ return values.length > 0 ? values.join("、") : emptyText;
425
+ }
426
+ function extractStaticImportStatements(source) {
427
+ const lines = source.replace(/\r\n/g, "\n").split("\n");
428
+ const statements = [];
429
+ let currentStatement = [];
430
+ let collecting = false;
431
+ for (const line of lines) {
432
+ const trimmedLine = stripInlineComment(line).trim();
433
+ if (!collecting) {
434
+ if (!trimmedLine.startsWith("import ")) {
435
+ continue;
436
+ }
437
+ collecting = true;
438
+ currentStatement = [trimmedLine];
439
+ if (trimmedLine.includes(" from ")) {
440
+ statements.push(currentStatement.join(" "));
441
+ currentStatement = [];
442
+ collecting = false;
443
+ }
444
+ continue;
445
+ }
446
+ currentStatement.push(trimmedLine);
447
+ if (trimmedLine.includes(" from ")) {
448
+ statements.push(currentStatement.join(" "));
449
+ currentStatement = [];
450
+ collecting = false;
451
+ }
452
+ }
453
+ return statements;
454
+ }
455
+ function parseImportStatement(statement) {
456
+ const normalizedStatement = statement.replace(/;$/, "");
457
+ const match = normalizedStatement.match(/^import\s+([\s\S]*?)\s+from\s+["'`]([^"'`]+)["'`]$/);
458
+ if (!match) {
459
+ return undefined;
460
+ }
461
+ return {
462
+ clause: match[1],
463
+ specifier: match[2],
464
+ };
465
+ }
466
+ function stripInlineComment(line) {
467
+ const commentIndex = line.indexOf("//");
468
+ if (commentIndex < 0) {
469
+ return line;
470
+ }
471
+ const quoteCount = (line.slice(0, commentIndex).match(/["'`]/g) ?? []).length;
472
+ if (quoteCount % 2 !== 0) {
473
+ return line;
474
+ }
475
+ return line.slice(0, commentIndex);
476
+ }
477
+ function unique(values) {
478
+ return [...new Set(values)];
479
+ }
480
+ function toRepoPath(filePath, workspaceRoot) {
481
+ return relative(workspaceRoot, filePath).split(sep).join("/");
482
+ }
483
+ function isInsideDir(targetPath, dirPath) {
484
+ const relativePath = relative(dirPath, targetPath);
485
+ return relativePath === "" || (!relativePath.startsWith("..") && !relativePath.startsWith(`..${sep}`));
486
+ }
487
+ function isForcedIgnored(filePath) {
488
+ const fileName = filePath.split(/[\\/]/).pop() ?? "";
489
+ const normalizedName = fileName.replace(/\.(vue|ts|js)$/i, "").toLowerCase();
490
+ return FORCED_IGNORE_NAMES.has(normalizedName);
491
+ }
package/dist/index.js CHANGED
@@ -2,18 +2,31 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { PROMPT_NAME, PROMPT_DESCRIPTION, buildPromptMessages, } from "./prompts/generate-feature-doc.js";
5
+ import { collectFeatureDocContext } from "./analysis/feature-context.js";
6
+ import { PROMPT_DESCRIPTION, PROMPT_NAME, buildPromptMessages, } from "./prompts/generate-feature-doc.js";
6
7
  import { getTemplateContent } from "./resources/template.js";
8
+ const SERVER_VERSION = "1.0.2";
9
+ const FEATURE_PATH_DESCRIPTION = "功能路径,如 project/pmbasupplierevaluation 或 pm/ba/PmBaRentcontractSettle";
7
10
  const server = new McpServer({
8
11
  name: "feature-doc-generator",
9
- version: "1.0.0",
12
+ version: SERVER_VERSION,
10
13
  });
11
- // 注册 prompt
12
- server.prompt(PROMPT_NAME, PROMPT_DESCRIPTION, { featurePath: z.string().describe("功能路径,如 project/pmbasupplierevaluation 或 pm/ba/PmBaRentcontractSettle") }, ({ featurePath }) => ({
14
+ function getPromptText(featurePath) {
15
+ const [message] = buildPromptMessages(featurePath);
16
+ if (!message || typeof message.content !== "object" || message.content.type !== "text") {
17
+ throw new Error("Failed to build feature doc prompt text.");
18
+ }
19
+ return message.content.text;
20
+ }
21
+ server.prompt(PROMPT_NAME, PROMPT_DESCRIPTION, {
22
+ featurePath: z.string().describe(FEATURE_PATH_DESCRIPTION),
23
+ }, ({ featurePath }) => ({
13
24
  messages: buildPromptMessages(featurePath),
14
25
  }));
15
- // 注册 resource:功能说明模板
16
- server.resource("doc-template", "template://feature-doc", { description: "功能说明文档模板(开发规范/功能说明模板.md)", mimeType: "text/markdown" }, () => ({
26
+ server.resource("doc-template", "template://feature-doc", {
27
+ description: "功能说明文档模板(开发规范/功能说明模板.md)",
28
+ mimeType: "text/markdown",
29
+ }, () => ({
17
30
  contents: [
18
31
  {
19
32
  uri: "template://feature-doc",
@@ -22,11 +35,39 @@ server.resource("doc-template", "template://feature-doc", { description: "功能
22
35
  },
23
36
  ],
24
37
  }));
38
+ server.tool("generate_feature_doc", "返回固定提示词和自动收集的代码上下文,供 IDE 当前模型继续生成文档。", {
39
+ featurePath: z.string().describe(FEATURE_PATH_DESCRIPTION),
40
+ }, async ({ featurePath }) => {
41
+ try {
42
+ const promptText = getPromptText(featurePath);
43
+ const contextBundle = collectFeatureDocContext(featurePath, promptText);
44
+ return {
45
+ content: [
46
+ {
47
+ type: "text",
48
+ text: contextBundle.renderedText,
49
+ },
50
+ ],
51
+ };
52
+ }
53
+ catch (error) {
54
+ const message = error instanceof Error ? error.message : "未知错误";
55
+ return {
56
+ isError: true,
57
+ content: [
58
+ {
59
+ type: "text",
60
+ text: `generate_feature_doc 执行失败:${message}`,
61
+ },
62
+ ],
63
+ };
64
+ }
65
+ });
25
66
  async function main() {
26
67
  const transport = new StdioServerTransport();
27
68
  await server.connect(transport);
28
69
  }
29
- main().catch((err) => {
30
- console.error("MCP server failed to start:", err);
70
+ main().catch((error) => {
71
+ console.error("MCP server failed to start:", error);
31
72
  process.exit(1);
32
73
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-feature-doc-generator",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "MCP server for generating Vue 3 feature documentation via prompt template",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",