@reglow/review-mcp 0.1.0

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/server.mjs ADDED
@@ -0,0 +1,742 @@
1
+ /**
2
+ * ReView MCP 服务器:注册工具并桥接 stdio 传输
3
+ *
4
+ * 针对 ReView(uni-app x 移动端组件库,uts/uvue + `re-*` easycom 标签 + uni_modules 安装)适配:
5
+ * - get_review_document 组件文档 / 组件总览
6
+ * - get_review_code_examples 演示页与文档中的可复制示例
7
+ * - list_component_files 组件源码文件清单
8
+ * - get_file_code 读取组件内文件内容(带路径穿越防护)
9
+ * - get_function_code 提取函数 / 常量 / 类型定义
10
+ * - search_review_docs 跨文档与源码检索
11
+ * - get_review_guide 库级指南(安装 / easycom / 主题 / i18n / 平台 / 版本记录 / 约定)
12
+ *
13
+ * 工具名、参数名与返回结构与同构的 reui-mcp 一一对应,方便用户在两个库之间迁移。
14
+ * 注意:stdio 模式下 stdout 仅用于协议通信,所有日志必须走 stderr。
15
+ */
16
+
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
+ import { z } from "zod";
22
+ import { CATEGORY_KEYS, DOCS_BASE, componentLabel } from "./catalog.mjs";
23
+ import {
24
+ GUIDE_TOPICS,
25
+ GUIDE_TOPIC_KEYS,
26
+ NPM_NOT_PUBLISHED_HINT,
27
+ createKnowledge,
28
+ } from "./knowledge.mjs";
29
+ import { fetchSiteDocument } from "./site.mjs";
30
+
31
+ const PACKAGE_DIR = path.dirname(fileURLToPath(import.meta.url));
32
+ const OWN_PACKAGE = JSON.parse(
33
+ fs.readFileSync(path.join(PACKAGE_DIR, "package.json"), "utf8"),
34
+ );
35
+
36
+ /** 文本结果 */
37
+ function textResult(text) {
38
+ return { content: [{ type: "text", text }] };
39
+ }
40
+
41
+ /** 错误结果(让 AI 明确失败原因并自行纠正参数) */
42
+ function errorResult(text) {
43
+ return { content: [{ type: "text", text }], isError: true };
44
+ }
45
+
46
+ function sourceLabel(source) {
47
+ return (
48
+ {
49
+ workspace: "本地 ReView 仓库(--root / 仓库内运行)",
50
+ local: "项目内已安装 uni_modules/reglow-review(仅源码)",
51
+ version: "指定 npm 版本(本库未发布,不可用)",
52
+ site: "官方文档站点兜底(最新版)",
53
+ none: "无可用知识源",
54
+ }[source] ?? source
55
+ );
56
+ }
57
+
58
+ /** 响应头:知识来源与版本,避免 AI 误判文档对应的版本 */
59
+ function metaHeader(knowledge, extraLines = []) {
60
+ const meta = knowledge.meta();
61
+ const lines = [
62
+ `> 知识源:${sourceLabel(meta.source)}|组件库版本:\`${meta.version}\`` +
63
+ `${meta.versionDate ? `(${meta.versionDate})` : ""}|版本来源:\`${meta.versionSource ?? "未知"}\`` +
64
+ `${meta.root ? `|路径:\`${meta.root}\`` : ""}`,
65
+ ];
66
+ if (meta.requestedVersion) {
67
+ lines.push(
68
+ `> ⚠️ 收到 version=\`${meta.requestedVersion}\`,但${NPM_NOT_PUBLISHED_HINT}`,
69
+ );
70
+ }
71
+ if (meta.warning) lines.push(`> ⚠️ ${meta.warning}`);
72
+ lines.push(...extraLines);
73
+ return lines.join("\n");
74
+ }
75
+
76
+ /** 从组件条目推导引入方式(easycom 标签 + 手动 import) */
77
+ function importInfo(entry) {
78
+ if (!entry) return null;
79
+ const tags = entry.tags?.length ? entry.tags : entry.tag ? [entry.tag] : [];
80
+ const lines = [];
81
+ if (tags.length > 0) {
82
+ lines.push(
83
+ `easycom 自动引入(推荐):模板里直接用 ${tags.map((tag) => `\`<${tag} />\``).join(" / ")}`,
84
+ );
85
+ }
86
+ if (entry.dir) {
87
+ const name = entry.dir.split("/").pop();
88
+ lines.push(
89
+ `手动引入:\`import ${name
90
+ .split("-")
91
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
92
+ .join("")} from '@/${entry.dir}/${name}'\``,
93
+ );
94
+ }
95
+ return lines.length > 0 ? lines : null;
96
+ }
97
+
98
+ /** 组件概览(按 VitePress 侧边栏分类组织) */
99
+ function renderOverview(knowledge, category) {
100
+ const catalog = knowledge.catalog;
101
+ const groups = catalog.categories.filter(
102
+ (group) => !category || group.key === category,
103
+ );
104
+ const blocks = [];
105
+ for (const group of groups) {
106
+ const rows = group.components
107
+ .map((entry) => {
108
+ const tag = entry.tag ? `\`<${entry.tag}>\`` : "—";
109
+ const demo = entry.demoPath ? "✅" : "—";
110
+ return `| \`${entry.name}\` ${entry.cn} | ${entry.description || "—"} | ${tag} | ${demo} | [文档](${entry.docsUrl}) |`;
111
+ })
112
+ .filter(Boolean);
113
+ if (rows.length === 0) continue;
114
+ blocks.push(
115
+ `## ${group.label}(${group.key})\n\n| 组件 | 说明 | easycom 标签 | 演示页 | 官网 |\n| --- | --- | --- | --- | --- |\n${rows.join("\n")}`,
116
+ );
117
+ }
118
+
119
+ // 侧边栏未收录但仓库内存在的组件(内部子组件 / 新增未登记)
120
+ const listed = new Set(
121
+ catalog.categories.flatMap((group) =>
122
+ group.components.map((item) => item.name),
123
+ ),
124
+ );
125
+ const extra = catalog.components.filter((entry) => !listed.has(entry.name));
126
+ if (extra.length > 0 && !category) {
127
+ blocks.push(
128
+ `## 其他(侧边栏未收录,多为内部子组件)\n\n| 组件 | 说明 | easycom 标签 | 源码目录 |\n| --- | --- | --- | --- |\n${extra
129
+ .map(
130
+ (entry) =>
131
+ `| \`${entry.name}\` | ${entry.description || "—"} | ${entry.tag ? `\`<${entry.tag}>\`` : "—"} | \`${entry.dir ?? "(无独立目录)"}\` |`,
132
+ )
133
+ .join("\n")}`,
134
+ );
135
+ }
136
+ return blocks.join("\n\n");
137
+ }
138
+
139
+ /**
140
+ * 创建 MCP 服务器
141
+ *
142
+ * @param {{ resolveKnowledge: (version?: string) => Promise<object>, defaultKnowledge: object }} options
143
+ */
144
+ export function createServer({ resolveKnowledge, defaultKnowledge }) {
145
+ const server = new McpServer({
146
+ name: "review-mcp",
147
+ version: OWN_PACKAGE.version,
148
+ });
149
+
150
+ const withKnowledge = async (version) => resolveKnowledge(version);
151
+
152
+ // ============================ 1. 组件文档 ============================
153
+ server.registerTool(
154
+ "get_review_document",
155
+ {
156
+ title: "获取 ReView 组件文档",
157
+ description:
158
+ "获取 ReView(uni-app x / uts / uvue 移动端组件库)的组件文档。不传 component 时返回按分类组织的组件总览" +
159
+ "(组件名/说明/easycom 标签/演示页可用性/官网链接);传 component 时返回该组件的完整文档" +
160
+ "(引入方式、Props/Events/Slots 表格)。组件通过 uni_modules 安装,模板里用 `re-*` 标签(如 `<re-button />`)。" +
161
+ "仓库内缺少该组件文档时会自动回退官方文档站点并在回复中标注来源。",
162
+ inputSchema: {
163
+ component: z
164
+ .string()
165
+ .optional()
166
+ .describe(
167
+ "组件名,kebab-case 且不带 re- 前缀,如 button、search-history、date-time-picker(也可写易用标签 re-button);不传则返回组件总览",
168
+ ),
169
+ category: z
170
+ .enum(CATEGORY_KEYS)
171
+ .optional()
172
+ .describe(
173
+ "分类过滤(仅总览生效):base / navigation / input / display / feedback / list-layout / marketing / business / module",
174
+ ),
175
+ version: z
176
+ .string()
177
+ .optional()
178
+ .describe(
179
+ "指定 @reglow/review 版本;本库未发布到 npm,传值只会得到提示(知识源仍是本地仓库)",
180
+ ),
181
+ source: z
182
+ .enum(["auto", "workspace", "site"])
183
+ .optional()
184
+ .describe(
185
+ "auto(默认):本地仓库优先、缺失时回退官网;workspace:仅本地;site:强制取官网最新文档",
186
+ ),
187
+ },
188
+ annotations: {
189
+ readOnlyHint: true,
190
+ idempotentHint: true,
191
+ openWorldHint: true,
192
+ },
193
+ },
194
+ async ({ component, category, version, source }) => {
195
+ try {
196
+ const knowledge = await withKnowledge(version);
197
+ const meta = knowledge.meta();
198
+ if (!component) {
199
+ const overview = renderOverview(knowledge, category);
200
+ if (!overview) {
201
+ const siteOverview = await knowledge.siteOverview();
202
+ if (siteOverview) {
203
+ return textResult(
204
+ `${metaHeader(knowledge, ["", `> 官网总览:${siteOverview.url}`])}\n\n${siteOverview.markdown}`,
205
+ );
206
+ }
207
+ return errorResult(
208
+ `未找到任何组件信息。\n\n${metaHeader(knowledge)}\n\n` +
209
+ `请使用 \`--root <review 仓库路径>\` 启动(在 review 仓库内运行可自动识别),或用 \`--no-site\` 之外的默认配置联网兜底。`,
210
+ );
211
+ }
212
+ return textResult(
213
+ `${metaHeader(knowledge, ["", `> 官网总览:${DOCS_BASE}`])}\n\n# ReView 组件总览\n\n${overview}`,
214
+ );
215
+ }
216
+
217
+ const entry = knowledge.getComponent(component);
218
+ if (source === "site" || (meta.source === "site" && !entry)) {
219
+ const siteDoc = await fetchSiteDocument(
220
+ entry?.name ?? String(component),
221
+ { category: entry?.category ?? null },
222
+ );
223
+ if (siteDoc) {
224
+ return textResult(
225
+ `> 知识源:官方文档站点(最新版)|链接:${siteDoc.url}\n\n# ${componentLabel(entry) || component}\n\n${siteDoc.markdown}`,
226
+ );
227
+ }
228
+ return errorResult(
229
+ `官方文档站点暂无「${component}」页面(可能是新组件或名称不符)。\n可用组件见 \`get_review_document\`(不传 component)。`,
230
+ );
231
+ }
232
+
233
+ const doc = await knowledge.getDocument(component);
234
+ if (!entry && doc.kind === "none") {
235
+ const names = knowledge
236
+ .listComponents()
237
+ .map((item) => item.name)
238
+ .slice(0, 60);
239
+ return errorResult(
240
+ `未找到组件「${component}」。\n\n可用组件(部分):${names.join(", ")}\n\n` +
241
+ `提示:组件名为 kebab-case 且不带 re- 前缀(\`re-button\` → \`button\`);` +
242
+ `\`IndexesAnchor\` → \`indexes-anchor\`,\`QRCode\` → \`qrcode\`。`,
243
+ );
244
+ }
245
+
246
+ const lines = [`# ${componentLabel(entry) || component}`];
247
+ const facts = [];
248
+ if (entry) {
249
+ facts.push(
250
+ `> 分类:${entry.categoryLabel}(${entry.category})|源码目录:\`${entry.dir ?? "(该文档页对应多个组件,无独立目录)"}\``,
251
+ );
252
+ const imports = importInfo(entry);
253
+ if (imports)
254
+ facts.push("> 使用方式:", ...imports.map((line) => `> - ${line}`));
255
+ const mainFiles = entry.files.filter((file) =>
256
+ file.endsWith(".uvue"),
257
+ );
258
+ if (mainFiles.length > 0) {
259
+ facts.push(
260
+ `> 源码文件:${mainFiles.map((file) => `\`${file}\``).join("、")}`,
261
+ );
262
+ }
263
+ if (entry.demoPath) {
264
+ facts.push(
265
+ `> 演示页:\`${entry.demoPath}\`(示例代码用 \`get_review_code_examples\` 取)`,
266
+ );
267
+ }
268
+ }
269
+ if (doc.url) facts.push(`> 官方文档:${doc.url}`);
270
+ if (doc.kind === "site") {
271
+ facts.push(
272
+ "> ⚠️ 本地仓库未包含该组件文档,以下正文来自官方文档站点(最新版),可能与当前版本存在差异。",
273
+ );
274
+ }
275
+ if (doc.kind === "readme") {
276
+ facts.push(
277
+ "> ℹ️ 正文来自组件目录内的 `readme.md`(脚本生成物,内容可能落后于源码)。",
278
+ );
279
+ }
280
+ if (doc.kind === "source") {
281
+ facts.push(
282
+ "> ⚠️ 本地既无组件文档也无官网可访问,以下内容由组件 `.uvue` 源码摘要而成(仅前若干行,无 API 表格)。",
283
+ );
284
+ }
285
+ lines.push(
286
+ "",
287
+ metaHeader(knowledge, facts),
288
+ "",
289
+ "---",
290
+ "",
291
+ doc.markdown || "(无文档内容)",
292
+ );
293
+ return textResult(lines.join("\n"));
294
+ } catch (error) {
295
+ return errorResult(`获取文档失败:${error.message}`);
296
+ }
297
+ },
298
+ );
299
+
300
+ // ============================ 2. 代码示例 ============================
301
+ server.registerTool(
302
+ "get_review_code_examples",
303
+ {
304
+ title: "获取 ReView 组件代码示例",
305
+ description:
306
+ "从官方演示页(`pages/<category>/<name>/<name>.uvue`)中提取可直接复制的 uvue 示例,按 `<!-- 小节标题 -->` 切分并保留小节标题;" +
307
+ "本地缺失时回退官网文档站。用于快速拿到「官方推荐写法」,避免臆造 props / 事件。可用 index 取第 N 个示例,lang 过滤语言。",
308
+ inputSchema: {
309
+ component: z
310
+ .string()
311
+ .describe("组件名,kebab-case(如 button、infinite-loading)"),
312
+ index: z
313
+ .number()
314
+ .int()
315
+ .min(0)
316
+ .optional()
317
+ .describe("示例下标(从 0 开始);不传则返回全部"),
318
+ lang: z
319
+ .enum([
320
+ "all",
321
+ "uvue",
322
+ "uts",
323
+ "vue",
324
+ "ts",
325
+ "js",
326
+ "json",
327
+ "bash",
328
+ "text",
329
+ ])
330
+ .optional()
331
+ .describe("按语言过滤,默认 all"),
332
+ version: z
333
+ .string()
334
+ .optional()
335
+ .describe("指定 @reglow/review 版本(未发布 npm,仅提示)"),
336
+ },
337
+ annotations: {
338
+ readOnlyHint: true,
339
+ idempotentHint: true,
340
+ openWorldHint: true,
341
+ },
342
+ },
343
+ async ({ component, index, lang, version }) => {
344
+ try {
345
+ const knowledge = await withKnowledge(version);
346
+ const result = await knowledge.getCodeExamples(component, {
347
+ index,
348
+ lang: lang ?? "all",
349
+ });
350
+ if (result.examples.length === 0) {
351
+ return errorResult(
352
+ `未找到「${component}」的代码示例(来源:${result.source})。\n` +
353
+ `可先用 \`get_review_document\` 确认组件是否存在,或改用 \`get_review_code_examples\` 的 lang=all。\n` +
354
+ `官网文档:${result.url ?? "无"}`,
355
+ );
356
+ }
357
+ const blocks = result.examples.map(
358
+ (example, i) =>
359
+ `### ${index !== undefined ? index : i}. ${example.title}\n\n\`\`\`${example.lang}\n${example.code}\n\`\`\``,
360
+ );
361
+ const origin =
362
+ result.source === "demo"
363
+ ? `官方演示页(\`${result.path}\`,与仓库版本一致)`
364
+ : result.source === "doc"
365
+ ? `组件文档(\`${result.path}\`)`
366
+ : result.source === "site"
367
+ ? "官方文档站点(最新版,示例可能高于当前版本)"
368
+ : "未知来源";
369
+ return textResult(
370
+ `# ${componentLabel(knowledge.getComponent(component)) || component} 代码示例(共 ${result.examples.length} 个)\n\n` +
371
+ `> 来源:${origin}${result.url ? `|官方文档:${result.url}` : ""}\n\n` +
372
+ metaHeader(knowledge) +
373
+ "\n\n---\n\n" +
374
+ blocks.join("\n\n"),
375
+ );
376
+ } catch (error) {
377
+ return errorResult(`获取代码示例失败:${error.message}`);
378
+ }
379
+ },
380
+ );
381
+
382
+ // ============================ 3. 文件清单 ============================
383
+ server.registerTool(
384
+ "list_component_files",
385
+ {
386
+ title: "列出 ReView 组件源码文件",
387
+ description:
388
+ "列出组件目录(`uni_modules/reglow-review/components/re-*/`)下的源码文件清单(相对路径 / 行数 / 字节数),用于了解组件结构," +
389
+ "再配合 get_file_code 查看具体实现。典型文件:`re-xxx.uvue`(组件实现)、`readme.md`(脚本生成的文档,中文可能有编码问题)、" +
390
+ "`*.uts`(类型 / 工具)、`static/*`(静态资源)。子组件目录(如 re-chat-sender)也可直接用组件名查询。",
391
+ inputSchema: {
392
+ component: z.string().describe("组件名,kebab-case"),
393
+ version: z
394
+ .string()
395
+ .optional()
396
+ .describe("指定 @reglow/review 版本(未发布 npm,仅提示)"),
397
+ },
398
+ annotations: {
399
+ readOnlyHint: true,
400
+ idempotentHint: true,
401
+ openWorldHint: true,
402
+ },
403
+ },
404
+ async ({ component, version }) => {
405
+ try {
406
+ const knowledge = await withKnowledge(version);
407
+ const entry = knowledge.getComponent(component);
408
+ if (!entry)
409
+ return errorResult(
410
+ `未找到组件「${component}」。可用组件见 get_review_document(不传 component)。`,
411
+ );
412
+ const files = knowledge.listFiles(component) ?? [];
413
+ const rows = files
414
+ .map(
415
+ (file) =>
416
+ `| \`${file.path}\` | ${file.lines || "—"} | ${file.size} |`,
417
+ )
418
+ .join("\n");
419
+ const imports = importInfo(entry);
420
+ return textResult(
421
+ `# ${componentLabel(entry)} 源码文件\n\n` +
422
+ metaHeader(knowledge, [
423
+ `> 分类:${entry.categoryLabel}|目录:\`${entry.dir ?? "(无独立源码目录)"}\`|文件数:${files.length}`,
424
+ ...(imports ? imports.map((line) => `> - ${line}`) : []),
425
+ ]) +
426
+ (rows
427
+ ? `\n\n| 文件 | 行数 | 字节 |\n| --- | --- | --- |\n${rows}`
428
+ : "\n\n(该组件在仓库内没有独立源码目录)") +
429
+ `\n\n> 下一步:\`get_file_code\` 读取具体文件,\`get_function_code\` 直接取某个函数实现。`,
430
+ );
431
+ } catch (error) {
432
+ return errorResult(`列出文件失败:${error.message}`);
433
+ }
434
+ },
435
+ );
436
+
437
+ // ============================ 4. 文件内容 ============================
438
+ server.registerTool(
439
+ "get_file_code",
440
+ {
441
+ title: "读取 ReView 组件文件内容",
442
+ description:
443
+ "读取组件目录内某个文件的代码(支持 startLine/endLine 只取片段)。用于查看组件实现细节、`type Props` / `withDefaults(defineProps<Props>())`、" +
444
+ "`defineEmits` 与默认值 —— ReView 的部分文档页 Props 表格为空,此时必须以源码为准。文件路径来自 list_component_files,禁止越出组件目录。",
445
+ inputSchema: {
446
+ component: z.string().describe("组件名,kebab-case"),
447
+ file: z
448
+ .string()
449
+ .describe(
450
+ "组件目录内的相对路径,如 re-button.uvue、type.uts、readme.md",
451
+ ),
452
+ version: z
453
+ .string()
454
+ .optional()
455
+ .describe("指定 @reglow/review 版本(未发布 npm,仅提示)"),
456
+ startLine: z
457
+ .number()
458
+ .int()
459
+ .min(1)
460
+ .optional()
461
+ .describe("起始行(从 1 开始,含)"),
462
+ endLine: z.number().int().min(1).optional().describe("结束行(含)"),
463
+ },
464
+ annotations: {
465
+ readOnlyHint: true,
466
+ idempotentHint: true,
467
+ openWorldHint: true,
468
+ },
469
+ },
470
+ async ({ component, file, version, startLine, endLine }) => {
471
+ try {
472
+ const knowledge = await withKnowledge(version);
473
+ const result = knowledge.readComponentFile(component, file, {
474
+ startLine,
475
+ endLine,
476
+ });
477
+ if (result.error)
478
+ return errorResult(`${result.error}\n\n${metaHeader(knowledge)}`);
479
+ const ext = path.extname(result.path).replace(".", "") || "ts";
480
+ const lang =
481
+ ext === "uvue"
482
+ ? "vue"
483
+ : ext === "uts"
484
+ ? "ts"
485
+ : ext === "d.ts"
486
+ ? "ts"
487
+ : ext;
488
+ return textResult(
489
+ `# \`${result.path}\`\n\n` +
490
+ metaHeader(knowledge, [
491
+ `> 组件:${componentLabel(knowledge.getComponent(component)) || component}|总行数:${result.totalLines}${startLine || endLine
492
+ ? `|当前片段:${result.startLine}-${result.endLine}`
493
+ : ""
494
+ }`,
495
+ ...(result.garbled
496
+ ? [
497
+ "> ⚠️ 该文件是脚本生成的二次编码文本(中文为乱码),请只参考其中的代码片段。",
498
+ ]
499
+ : []),
500
+ ]) +
501
+ `\n\n\`\`\`${lang}\n${result.text}\n\`\`\``,
502
+ );
503
+ } catch (error) {
504
+ return errorResult(`读取文件失败:${error.message}`);
505
+ }
506
+ },
507
+ );
508
+
509
+ // ============================ 5. 函数实现 ============================
510
+ server.registerTool(
511
+ "get_function_code",
512
+ {
513
+ title: "获取 ReView 组件函数实现",
514
+ description:
515
+ "提取组件源码中某个函数 / 常量 / 类 / 接口 / 类型(`type Props` 等)的完整实现(含起止行号),用于深入理解组件逻辑。" +
516
+ "未命中时会返回该组件可用符号清单,便于纠正名称后重试。",
517
+ inputSchema: {
518
+ component: z.string().describe("组件名,kebab-case"),
519
+ name: z
520
+ .string()
521
+ .describe("符号名,如 onTouchStart、buildStateByValues、Props"),
522
+ file: z
523
+ .string()
524
+ .optional()
525
+ .describe(
526
+ "限定文件(如 re-cascader.uvue);不传则在组件内所有 .uvue/.uts 中查找",
527
+ ),
528
+ version: z
529
+ .string()
530
+ .optional()
531
+ .describe("指定 @reglow/review 版本(未发布 npm,仅提示)"),
532
+ },
533
+ annotations: {
534
+ readOnlyHint: true,
535
+ idempotentHint: true,
536
+ openWorldHint: true,
537
+ },
538
+ },
539
+ async ({ component, name, file, version }) => {
540
+ try {
541
+ const knowledge = await withKnowledge(version);
542
+ const result = knowledge.findFunction(component, name, { file });
543
+ if (result.error) {
544
+ const similar = (result.similarSymbols ?? []).join(", ");
545
+ const symbols = (result.availableSymbols ?? [])
546
+ .slice(0, 200)
547
+ .join(", ");
548
+ return errorResult(
549
+ `${result.error}\n\n` +
550
+ (similar ? `最相近的符号:${similar}\n\n` : "") +
551
+ (result.searched?.length
552
+ ? `已检索:${result.searched.map((item) => `\`${item}\``).join("、")}\n\n`
553
+ : "") +
554
+ (symbols
555
+ ? `该组件可用符号(共 ${result.allSymbolsCount ?? "?"} 个,展示前 200 个):${symbols}`
556
+ : "") +
557
+ `\n\n提示:也可用 \`search_review_docs\` 以关键词定位,再用 \`get_file_code\` 读取上下文。`,
558
+ );
559
+ }
560
+ return textResult(
561
+ `# \`${result.name}\`(${result.kind})\n\n` +
562
+ metaHeader(knowledge, [
563
+ `> 组件:${componentLabel(knowledge.getComponent(component)) || component}|文件:\`${result.path}\`|行号:${result.startLine}-${result.endLine}`,
564
+ ]) +
565
+ `\n\n\`\`\`ts\n${result.text}\n\`\`\``,
566
+ );
567
+ } catch (error) {
568
+ return errorResult(`提取函数失败:${error.message}`);
569
+ }
570
+ },
571
+ );
572
+
573
+ // ============================ 6. 文档检索 ============================
574
+ server.registerTool(
575
+ "search_review_docs",
576
+ {
577
+ title: "检索 ReView 文档与源码",
578
+ description:
579
+ "在组件文档、演示页、组件源码与库级文档(README / CHANGELOG / docs/guide)中做关键词检索(多关键词空格分隔,按「全部命中」过滤)," +
580
+ "返回「组件 / 文件:行号 + 上下文片段 + 文档链接」。适合不确定用哪个组件、或想找某个 prop / 事件 / CSS 变量在何处实现时使用。",
581
+ inputSchema: {
582
+ query: z
583
+ .string()
584
+ .optional()
585
+ .describe("关键词,支持多词(空格分隔,如「级联 lazy」)"),
586
+ keywords: z
587
+ .array(z.string())
588
+ .optional()
589
+ .describe("关键词数组写法(等价于 query,多个关键词为「全部命中」)"),
590
+ component: z.string().optional().describe("限定单个组件"),
591
+ category: z.enum(CATEGORY_KEYS).optional().describe("限定分类"),
592
+ limit: z
593
+ .number()
594
+ .int()
595
+ .min(1)
596
+ .max(60)
597
+ .optional()
598
+ .describe("最多返回条数,默认 20"),
599
+ version: z
600
+ .string()
601
+ .optional()
602
+ .describe("指定 @reglow/review 版本(未发布 npm,仅提示)"),
603
+ },
604
+ annotations: {
605
+ readOnlyHint: true,
606
+ idempotentHint: true,
607
+ openWorldHint: true,
608
+ },
609
+ },
610
+ async ({ query, keywords, component, category, limit, version }) => {
611
+ try {
612
+ const knowledge = await withKnowledge(version);
613
+ const text = keywords?.length ? keywords.join(" ") : (query ?? "");
614
+ const { hits, scanned, terms } = knowledge.search(text, {
615
+ component,
616
+ category,
617
+ limit,
618
+ });
619
+ if (hits.length === 0) {
620
+ return textResult(
621
+ `未检索到与「${text}」匹配的内容(已扫描 ${scanned} 个文件)。\n\n` +
622
+ `${metaHeader(knowledge)}\n\n` +
623
+ `建议:减少关键词数量、换用英文标识(如 \`customStyle\`、\`themeVars\`)、或先用 \`get_review_document\` 浏览组件总览。`,
624
+ );
625
+ }
626
+ const grouped = new Map();
627
+ for (const hit of hits) {
628
+ if (!grouped.has(hit.component)) grouped.set(hit.component, []);
629
+ grouped.get(hit.component).push(hit);
630
+ }
631
+ const blocks = [...grouped.entries()].map(([name, items]) => {
632
+ const link = items[0].docsUrl
633
+ ? `|[官方文档](${items[0].docsUrl})`
634
+ : "";
635
+ const body = items
636
+ .map(
637
+ (hit) =>
638
+ `- \`${hit.path}:${hit.line}\`\n\n\`\`\`\n${hit.text}\n\`\`\``,
639
+ )
640
+ .join("\n");
641
+ return `## ${componentLabel(knowledge.getComponent(name)) || name}${link}\n\n${body}`;
642
+ });
643
+ return textResult(
644
+ `# 检索「${text}」\n\n` +
645
+ `${metaHeader(knowledge, [
646
+ `> 命中 ${hits.length} 处(扫描 ${scanned} 个文件,关键词:${terms.join(" + ")})`,
647
+ ])}\n\n---\n\n${blocks.join("\n\n")}`,
648
+ );
649
+ } catch (error) {
650
+ return errorResult(`检索失败:${error.message}`);
651
+ }
652
+ },
653
+ );
654
+
655
+ // ============================ 7. 库级指南 ============================
656
+ server.registerTool(
657
+ "get_review_guide",
658
+ {
659
+ title: "获取 ReView 使用指南",
660
+ description:
661
+ "获取库级指南:安装与引入(uni_modules / 插件市场 / easycom)、easycom 细节、主题与样式定制(`--re-*` CSS 变量)、" +
662
+ "国际化(zh-CN / zh-TW / en-US)、平台差异与运行方式(HBuilderX、App/H5/小程序)、版本记录与工程约定。" +
663
+ "在动手前用它对齐「正确引入方式 / 主题变量 / 语言包 / 平台限制」,避免踩坑。",
664
+ inputSchema: {
665
+ topic: z
666
+ .enum(GUIDE_TOPIC_KEYS)
667
+ .optional()
668
+ .describe(
669
+ GUIDE_TOPICS.map((item) => `${item.key}(${item.label})`).join(
670
+ " | ",
671
+ ),
672
+ ),
673
+ version: z
674
+ .string()
675
+ .optional()
676
+ .describe("指定 @reglow/review 版本(未发布 npm,仅提示)"),
677
+ },
678
+ annotations: {
679
+ readOnlyHint: true,
680
+ idempotentHint: true,
681
+ openWorldHint: true,
682
+ },
683
+ },
684
+ async ({ topic, version }) => {
685
+ try {
686
+ const knowledge = await withKnowledge(version);
687
+ const guide = knowledge.getGuide(topic ?? "overview");
688
+ if (!guide || guide.sections.length === 0) {
689
+ return textResult(
690
+ `${metaHeader(knowledge)}\n\n` +
691
+ `未取到本地指南章节(topic=${topic ?? "overview"})。\n` +
692
+ `可用方式:用 \`--root <review 仓库路径>\` 启动以读取 \`README.md\` / \`docs/guide/*.md\`;或直接改用 \`get_review_document\`。`,
693
+ );
694
+ }
695
+ const blocks = guide.sections.map(
696
+ (section) =>
697
+ `<!-- 来源:${section.file}${section.title ? ` · ${section.title}` : ""} -->\n\n${section.text}`,
698
+ );
699
+ return textResult(
700
+ `# ReView 指南(${topic ?? "overview"})\n\n` +
701
+ metaHeader(knowledge, [
702
+ `> 章节数:${guide.sections.length}|官方文档:${guide.docsBase}`,
703
+ ]) +
704
+ "\n\n---\n\n" +
705
+ blocks.join("\n\n---\n\n"),
706
+ );
707
+ } catch (error) {
708
+ return errorResult(`获取指南失败:${error.message}`);
709
+ }
710
+ },
711
+ );
712
+
713
+ return server;
714
+ }
715
+
716
+ /**
717
+ * 默认知识解析器:按 version 缓存(同一进程内复用)
718
+ *
719
+ * version 在本库不生效(未发布 npm),但保留参数以与 reui-mcp 的工具签名一致。
720
+ *
721
+ * @param {{ root?: string, allowSite?: boolean, logger?: (message: string) => void }} options
722
+ */
723
+ export function createKnowledgeResolver(options = {}) {
724
+ const cache = new Map();
725
+ return async function resolveKnowledge(version) {
726
+ const key = version ? `version:${version}` : "default";
727
+ if (!cache.has(key)) {
728
+ cache.set(
729
+ key,
730
+ await createKnowledge({
731
+ root: options.root,
732
+ version,
733
+ allowSite: options.allowSite,
734
+ logger: options.logger ?? (() => { }),
735
+ }),
736
+ );
737
+ }
738
+ return cache.get(key);
739
+ };
740
+ }
741
+
742
+ export { createKnowledge };