@reglow/reui-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/README.md +204 -0
- package/catalog.mjs +158 -0
- package/cli-utils.mjs +29 -0
- package/index.mjs +169 -0
- package/integrations.mjs +273 -0
- package/knowledge.mjs +923 -0
- package/package.json +46 -0
- package/server.mjs +710 -0
- package/setup.mjs +400 -0
- package/site.mjs +155 -0
- package/skills/reui-components/BEST_PRACTICES.md +219 -0
- package/skills/reui-components/COMPONENTS.md +141 -0
- package/skills/reui-components/SKILL.md +172 -0
- package/skills/reui-components/WORKFLOWS.md +264 -0
- package/skills.mjs +197 -0
- package/tar.mjs +115 -0
package/knowledge.mjs
ADDED
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 知识源(Knowledge):为 MCP 工具提供组件文档、示例、源码、函数实现与检索能力
|
|
3
|
+
*
|
|
4
|
+
* 支持三种数据来源,按优先级自动选择:
|
|
5
|
+
* 1. `local` —— `--root` 指定的工作区,或从当前项目解析到的已安装 `@reglow/reui`(与项目版本一致)
|
|
6
|
+
* 2. `version` —— `version` 参数指定的 npm 版本(下载 tarball 到缓存目录后离线读取)
|
|
7
|
+
* 3. `site` —— 官方文档站点兜底(包内缺文档时,抓取 https://reglow.huoli.tech/components-library/reui)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { extractTarball } from "./tar.mjs";
|
|
16
|
+
import { fetchSiteDocument, fetchSiteOverview } from "./site.mjs";
|
|
17
|
+
import { CATEGORIES, catalogItem, docsUrlFor } from "./catalog.mjs";
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const PACKAGE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const OWN_PACKAGE = JSON.parse(
|
|
22
|
+
fs.readFileSync(path.join(PACKAGE_DIR, "package.json"), "utf8"),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const REUI_PACKAGE = "@reglow/reui";
|
|
26
|
+
const REGISTRY = "https://registry.npmjs.org";
|
|
27
|
+
/** 可参与检索 / 读取的文本文件扩展名 */
|
|
28
|
+
const TEXT_EXTENSIONS = [
|
|
29
|
+
".md",
|
|
30
|
+
".ts",
|
|
31
|
+
".vue",
|
|
32
|
+
".js",
|
|
33
|
+
".mjs",
|
|
34
|
+
".json",
|
|
35
|
+
".css",
|
|
36
|
+
".txt",
|
|
37
|
+
];
|
|
38
|
+
/** 检索时单文件最大读取体积 */
|
|
39
|
+
const MAX_SEARCH_FILE_BYTES = 512 * 1024;
|
|
40
|
+
/** 单次读取文件的最大字节数 */
|
|
41
|
+
const MAX_READ_BYTES = 1024 * 1024;
|
|
42
|
+
|
|
43
|
+
const LAYER_DIRS = [
|
|
44
|
+
{ layer: "reui", dir: "components/reui" },
|
|
45
|
+
{ layer: "ui", dir: "components/ui" },
|
|
46
|
+
// 历史发布版本可能未把 components 打进产物,此时退化为 dist 目录(仅编译产物与 .d.ts)
|
|
47
|
+
{ layer: "reui", dir: "dist/components/reui" },
|
|
48
|
+
{ layer: "ui", dir: "dist/components/ui" },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// ============================ 通用工具函数 ============================
|
|
52
|
+
|
|
53
|
+
function toPosix(value) {
|
|
54
|
+
return value.split(path.sep).join("/");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isTextFile(file) {
|
|
58
|
+
return TEXT_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readJsonSafe(file) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 递归读取目录下所有文件(相对路径,posix 分隔符),跳过测试与隐藏目录 */
|
|
70
|
+
function walkFiles(dir, { skipTests = true, base = dir } = {}) {
|
|
71
|
+
const result = [];
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
75
|
+
} catch {
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (entry.name.startsWith(".")) continue;
|
|
80
|
+
if (
|
|
81
|
+
skipTests &&
|
|
82
|
+
(entry.name === "__tests__" || entry.name.endsWith(".test.ts"))
|
|
83
|
+
)
|
|
84
|
+
continue;
|
|
85
|
+
const full = path.join(dir, entry.name);
|
|
86
|
+
if (entry.isDirectory())
|
|
87
|
+
result.push(...walkFiles(full, { skipTests, base }));
|
|
88
|
+
else if (entry.isFile()) result.push(toPosix(path.relative(base, full)));
|
|
89
|
+
}
|
|
90
|
+
return result.sort();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function lineCount(text) {
|
|
94
|
+
return text === "" ? 1 : text.split("\n").length;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 是否为 ReUI 知识点根目录 */
|
|
98
|
+
export function isReuiRoot(dir) {
|
|
99
|
+
if (!dir) return false;
|
|
100
|
+
const stat = fs.existsSync(dir) && fs.statSync(dir);
|
|
101
|
+
if (!stat || !stat.isDirectory()) return false;
|
|
102
|
+
const pkg = readJsonSafe(path.join(dir, "package.json"));
|
|
103
|
+
if (pkg?.name === REUI_PACKAGE) return true;
|
|
104
|
+
// 仓库源码形态(reui 本体)
|
|
105
|
+
return (
|
|
106
|
+
fs.existsSync(path.join(dir, "components/reui/index.ts")) &&
|
|
107
|
+
fs.existsSync(path.join(dir, "index.ts"))
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 解析本地知识点根目录 */
|
|
112
|
+
export function resolveLocalRoot(explicitRoot) {
|
|
113
|
+
const candidates = [];
|
|
114
|
+
if (explicitRoot) candidates.push(path.resolve(explicitRoot));
|
|
115
|
+
if (process.env.REUI_ROOT)
|
|
116
|
+
candidates.push(path.resolve(process.env.REUI_ROOT));
|
|
117
|
+
// 仓库内运行:reui/mcp → reui
|
|
118
|
+
candidates.push(path.resolve(PACKAGE_DIR, ".."));
|
|
119
|
+
// 当前项目已安装的 @reglow/reui
|
|
120
|
+
try {
|
|
121
|
+
candidates.push(
|
|
122
|
+
path.dirname(
|
|
123
|
+
require.resolve(`${REUI_PACKAGE}/package.json`, {
|
|
124
|
+
paths: [process.cwd()],
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
} catch {
|
|
129
|
+
/* 未安装则跳过 */
|
|
130
|
+
}
|
|
131
|
+
candidates.push(path.join(process.cwd(), "node_modules", REUI_PACKAGE));
|
|
132
|
+
return candidates.find(isReuiRoot) ?? null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================ 符号(函数)提取 ============================
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 从起始大括号处匹配到配对的结束大括号
|
|
139
|
+
*
|
|
140
|
+
* 会跳过字符串('、"、`)、行注释与块注释,避免括号出现在字面量里导致错配。
|
|
141
|
+
*/
|
|
142
|
+
export function matchBlock(text, openIndex) {
|
|
143
|
+
if (text[openIndex] !== "{") return openIndex;
|
|
144
|
+
let depth = 0;
|
|
145
|
+
for (let i = openIndex; i < text.length; i += 1) {
|
|
146
|
+
const char = text[i];
|
|
147
|
+
if (char === "/" && text[i + 1] === "/") {
|
|
148
|
+
const end = text.indexOf("\n", i);
|
|
149
|
+
i = end === -1 ? text.length : end;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (char === "/" && text[i + 1] === "*") {
|
|
153
|
+
const end = text.indexOf("*/", i + 2);
|
|
154
|
+
i = end === -1 ? text.length : end + 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
158
|
+
for (let j = i + 1; j < text.length; j += 1) {
|
|
159
|
+
if (text[j] === "\\") {
|
|
160
|
+
j += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (text[j] === char) {
|
|
164
|
+
i = j;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
if (text[j] === "\n" && char !== "`") {
|
|
168
|
+
i = j - 1;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (char === "{") depth += 1;
|
|
175
|
+
else if (char === "}") {
|
|
176
|
+
depth -= 1;
|
|
177
|
+
if (depth === 0) return i;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return text.length - 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 匹配 `(...)` 的结束位置 */
|
|
184
|
+
function matchParen(text, openIndex) {
|
|
185
|
+
let depth = 0;
|
|
186
|
+
for (let i = openIndex; i < text.length; i += 1) {
|
|
187
|
+
const char = text[i];
|
|
188
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
189
|
+
for (let j = i + 1; j < text.length; j += 1) {
|
|
190
|
+
if (text[j] === "\\") {
|
|
191
|
+
j += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (text[j] === char) {
|
|
195
|
+
i = j;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (char === "(") depth += 1;
|
|
202
|
+
else if (char === ")") {
|
|
203
|
+
depth -= 1;
|
|
204
|
+
if (depth === 0) return i;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return text.length - 1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function escapeRegExp(value) {
|
|
211
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function lineOf(text, index) {
|
|
215
|
+
return text.slice(0, index).split("\n").length;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 提取指定符号的完整定义(函数 / 常量 / 类 / 接口 / 类型)
|
|
220
|
+
*
|
|
221
|
+
* @param {string} text 源码
|
|
222
|
+
* @param {string} name 符号名
|
|
223
|
+
* @returns {{ name: string, kind: string, text: string, startLine: number, endLine: number } | null}
|
|
224
|
+
*/
|
|
225
|
+
export function extractSymbol(text, name) {
|
|
226
|
+
const safe = escapeRegExp(name);
|
|
227
|
+
const patterns = [
|
|
228
|
+
{
|
|
229
|
+
kind: "function",
|
|
230
|
+
re: new RegExp(
|
|
231
|
+
`^[\\t ]*(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?function\\s+(${safe})\\s*\\(`,
|
|
232
|
+
"m",
|
|
233
|
+
),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
kind: "const",
|
|
237
|
+
re: new RegExp(
|
|
238
|
+
`^[\\t ]*(?:export\\s+)?(?:const|let|var)\\s+(${safe})\\s*=`,
|
|
239
|
+
"m",
|
|
240
|
+
),
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
kind: "class",
|
|
244
|
+
re: new RegExp(
|
|
245
|
+
`^[\\t ]*(?:export\\s+)?(?:default\\s+)?class\\s+(${safe})\\b`,
|
|
246
|
+
"m",
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
kind: "interface",
|
|
251
|
+
re: new RegExp(
|
|
252
|
+
`^[\\t ]*(?:export\\s+)?(?:interface|type|enum)\\s+(${safe})\\b`,
|
|
253
|
+
"m",
|
|
254
|
+
),
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
kind: "property",
|
|
258
|
+
re: new RegExp(`^[\\t ]*(${safe})\\s*[:=]\\s*(?:function|\\()`, "m"),
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
for (const { kind, re } of patterns) {
|
|
263
|
+
const match = re.exec(text);
|
|
264
|
+
if (!match) continue;
|
|
265
|
+
const start = match.index;
|
|
266
|
+
let end;
|
|
267
|
+
if (kind === "function") {
|
|
268
|
+
const paren = text.indexOf("(", start);
|
|
269
|
+
const parenEnd = matchParen(text, paren);
|
|
270
|
+
const brace = text.indexOf("{", parenEnd);
|
|
271
|
+
// 无函数体(类型声明 / 重载签名)时截到行尾
|
|
272
|
+
end =
|
|
273
|
+
brace === -1 ? text.indexOf("\n", parenEnd) : matchBlock(text, brace);
|
|
274
|
+
if (end === -1) end = text.length - 1;
|
|
275
|
+
} else if (kind === "interface") {
|
|
276
|
+
const brace = text.indexOf("{", start);
|
|
277
|
+
const semi = text.indexOf(";", start);
|
|
278
|
+
if (brace !== -1 && (semi === -1 || brace < semi))
|
|
279
|
+
end = matchBlock(text, brace);
|
|
280
|
+
else end = semi === -1 ? text.indexOf("\n", start) : semi;
|
|
281
|
+
if (end === -1) end = text.length - 1;
|
|
282
|
+
} else if (kind === "class") {
|
|
283
|
+
const brace = text.indexOf("{", start);
|
|
284
|
+
end = brace === -1 ? text.length - 1 : matchBlock(text, brace);
|
|
285
|
+
} else {
|
|
286
|
+
// const / var / property:优先函数体,其次箭头函数体,最后到分号或行尾
|
|
287
|
+
const eq = text.indexOf("=", start);
|
|
288
|
+
const brace = text.indexOf("{", eq === -1 ? start : eq);
|
|
289
|
+
const semi = text.indexOf(";", eq === -1 ? start : eq);
|
|
290
|
+
const newline = text.indexOf("\n", eq === -1 ? start : eq);
|
|
291
|
+
if (brace !== -1 && brace !== -1 && (semi === -1 || brace < semi)) {
|
|
292
|
+
end = matchBlock(text, brace);
|
|
293
|
+
} else if (
|
|
294
|
+
eq !== -1 &&
|
|
295
|
+
text.slice(eq, eq + 12).includes("=>") &&
|
|
296
|
+
brace !== -1
|
|
297
|
+
) {
|
|
298
|
+
end = matchBlock(text, brace);
|
|
299
|
+
} else if (
|
|
300
|
+
semi !== -1 &&
|
|
301
|
+
semi < (newline === -1 ? text.length : newline)
|
|
302
|
+
) {
|
|
303
|
+
end = semi;
|
|
304
|
+
} else {
|
|
305
|
+
end = newline === -1 ? text.length - 1 : newline - 1;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const slice = text.slice(start, end + 1);
|
|
309
|
+
return {
|
|
310
|
+
name,
|
|
311
|
+
kind,
|
|
312
|
+
text: slice.replace(/\s+$/, ""),
|
|
313
|
+
startLine: lineOf(text, start),
|
|
314
|
+
endLine: lineOf(text, start + slice.length),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 列出源码中可提取的符号名(用于未命中时给出候选) */
|
|
321
|
+
export function listSymbols(text) {
|
|
322
|
+
const names = new Set();
|
|
323
|
+
const patterns = [
|
|
324
|
+
/^[\t ]*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/gm,
|
|
325
|
+
/^[\t ]*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/gm,
|
|
326
|
+
/^[\t ]*(?:export\s+)?(?:default\s+)?class\s+([A-Za-z_$][\w$]*)/gm,
|
|
327
|
+
/^[\t ]*(?:export\s+)?(?:interface|type|enum)\s+([A-Za-z_$][\w$]*)/gm,
|
|
328
|
+
];
|
|
329
|
+
for (const re of patterns) {
|
|
330
|
+
for (const match of text.matchAll(re)) names.add(match[1]);
|
|
331
|
+
}
|
|
332
|
+
return [...names];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ============================ 版本化知识(npm tarball) ============================
|
|
336
|
+
|
|
337
|
+
function cacheRoot() {
|
|
338
|
+
return process.env.REUI_MCP_CACHE || path.join(os.tmpdir(), "reui-mcp-cache");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function fetchJson(url, timeout = 20000) {
|
|
342
|
+
const controller = new AbortController();
|
|
343
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
344
|
+
try {
|
|
345
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
346
|
+
if (!response.ok) return null;
|
|
347
|
+
return await response.json();
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
} finally {
|
|
351
|
+
clearTimeout(timer);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function fetchBuffer(url, timeout = 60000) {
|
|
356
|
+
const controller = new AbortController();
|
|
357
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
358
|
+
try {
|
|
359
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
360
|
+
if (!response.ok) return null;
|
|
361
|
+
return Buffer.from(await response.arrayBuffer());
|
|
362
|
+
} catch {
|
|
363
|
+
return null;
|
|
364
|
+
} finally {
|
|
365
|
+
clearTimeout(timer);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** 解析版本号:支持 `latest`、dist-tag、精确版本、`^x.y.z`(取同主版本最高) */
|
|
370
|
+
async function resolveVersion(version) {
|
|
371
|
+
const wanted = String(version).trim();
|
|
372
|
+
if (/^\d+\.\d+\.\d+/.test(wanted)) return wanted;
|
|
373
|
+
const meta = await fetchJson(
|
|
374
|
+
`${REGISTRY}/${encodeURIComponent(REUI_PACKAGE)}`,
|
|
375
|
+
);
|
|
376
|
+
if (!meta) return null;
|
|
377
|
+
if (wanted === "latest" || wanted === "")
|
|
378
|
+
return meta["dist-tags"]?.latest ?? null;
|
|
379
|
+
if (meta["dist-tags"]?.[wanted]) return meta["dist-tags"][wanted];
|
|
380
|
+
const caret = wanted.match(/^[\^~](\d+)\.(\d+)\.(\d+)/);
|
|
381
|
+
if (caret) {
|
|
382
|
+
const major = Number(caret[1]);
|
|
383
|
+
const versions = Object.keys(meta.versions ?? {})
|
|
384
|
+
.filter((item) => item.startsWith(`${major}.`))
|
|
385
|
+
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
386
|
+
return versions.at(-1) ?? null;
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** 下载并解包指定版本到缓存目录,返回解包后的 package 根目录 */
|
|
392
|
+
async function ensureVersionCache(version) {
|
|
393
|
+
const resolved = await resolveVersion(version);
|
|
394
|
+
if (!resolved) {
|
|
395
|
+
const error = new Error(
|
|
396
|
+
`无法解析 @reglow/reui 版本「${version}」(可能无网络或版本不存在)`,
|
|
397
|
+
);
|
|
398
|
+
error.code = "VERSION_UNRESOLVED";
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
const dir = path.join(cacheRoot(), resolved);
|
|
402
|
+
const marker = path.join(dir, ".ready");
|
|
403
|
+
if (fs.existsSync(marker)) return { dir, version: resolved };
|
|
404
|
+
|
|
405
|
+
const meta = await fetchJson(
|
|
406
|
+
`${REGISTRY}/${encodeURIComponent(REUI_PACKAGE)}/${resolved}`,
|
|
407
|
+
);
|
|
408
|
+
const tarball = meta?.dist?.tarball;
|
|
409
|
+
if (!tarball) {
|
|
410
|
+
const error = new Error(`未找到 @reglow/reui@${resolved} 的 tarball 地址`);
|
|
411
|
+
error.code = "VERSION_UNRESOLVED";
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
const buffer = await fetchBuffer(tarball);
|
|
415
|
+
if (!buffer) {
|
|
416
|
+
const error = new Error(`下载 @reglow/reui@${resolved} 失败(网络不可用)`);
|
|
417
|
+
error.code = "VERSION_UNRESOLVED";
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
422
|
+
extractTarball(buffer, (entry) => {
|
|
423
|
+
const relative = entry.path.replace(/^package\//, "");
|
|
424
|
+
if (!relative || relative.startsWith("..")) return;
|
|
425
|
+
const target = path.join(dir, relative);
|
|
426
|
+
if (!target.startsWith(dir)) return;
|
|
427
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
428
|
+
fs.writeFileSync(target, entry.data);
|
|
429
|
+
});
|
|
430
|
+
fs.writeFileSync(
|
|
431
|
+
marker,
|
|
432
|
+
JSON.stringify({ version: resolved, tarball }, null, 2),
|
|
433
|
+
);
|
|
434
|
+
return { dir, version: resolved };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ============================ 知识对象 ============================
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 创建知识对象
|
|
441
|
+
*
|
|
442
|
+
* @param {{ root?: string, version?: string, allowSite?: boolean, logger?: (msg: string) => void }} options
|
|
443
|
+
*/
|
|
444
|
+
export async function createKnowledge(options = {}) {
|
|
445
|
+
const logger = options.logger ?? (() => { });
|
|
446
|
+
const allowSite = options.allowSite !== false;
|
|
447
|
+
let root = null;
|
|
448
|
+
let version = OWN_PACKAGE.version;
|
|
449
|
+
let source = "local";
|
|
450
|
+
let warning = null;
|
|
451
|
+
|
|
452
|
+
if (options.version) {
|
|
453
|
+
const cached = await ensureVersionCache(options.version);
|
|
454
|
+
root = cached.dir;
|
|
455
|
+
version = cached.version;
|
|
456
|
+
source = "version";
|
|
457
|
+
} else {
|
|
458
|
+
root = resolveLocalRoot(options.root);
|
|
459
|
+
if (root) {
|
|
460
|
+
const pkg = readJsonSafe(path.join(root, "package.json"));
|
|
461
|
+
version = pkg?.version ?? version;
|
|
462
|
+
source = root === path.resolve(PACKAGE_DIR, "..") ? "workspace" : "local";
|
|
463
|
+
} else if (allowSite) {
|
|
464
|
+
warning =
|
|
465
|
+
"未找到本地 @reglow/reui(既未安装也未指定 --root),文档将回退到官方文档站点(最新版)";
|
|
466
|
+
source = "site";
|
|
467
|
+
} else {
|
|
468
|
+
warning = "未找到本地 @reglow/reui,且已禁用官网回退";
|
|
469
|
+
source = "none";
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** 构建组件索引(懒执行一次) */
|
|
474
|
+
let indexCache = null;
|
|
475
|
+
function buildIndex() {
|
|
476
|
+
if (indexCache) return indexCache;
|
|
477
|
+
const entries = new Map();
|
|
478
|
+
if (!root) return (indexCache = entries);
|
|
479
|
+
|
|
480
|
+
for (const { layer, dir } of LAYER_DIRS) {
|
|
481
|
+
const abs = path.join(root, dir);
|
|
482
|
+
if (!fs.existsSync(abs)) continue;
|
|
483
|
+
for (const name of fs.readdirSync(abs)) {
|
|
484
|
+
// 同名组件 reui 优先(与组件库导出策略一致:ui 层的同名包仅做转发)
|
|
485
|
+
if (entries.has(name)) continue;
|
|
486
|
+
const componentDir = path.join(abs, name);
|
|
487
|
+
if (!fs.statSync(componentDir).isDirectory()) continue;
|
|
488
|
+
const files = walkFiles(componentDir);
|
|
489
|
+
const hasApi = files.some((file) => /index\.(ts|js|d\.ts)$/.test(file));
|
|
490
|
+
if (!hasApi && files.length === 0) continue;
|
|
491
|
+
const docFile = files.find((file) => file === "README.md") ?? null;
|
|
492
|
+
const catalog = catalogItem(name);
|
|
493
|
+
entries.set(name, {
|
|
494
|
+
name,
|
|
495
|
+
layer,
|
|
496
|
+
dir: toPosix(path.relative(root, componentDir)),
|
|
497
|
+
files,
|
|
498
|
+
docPath: docFile
|
|
499
|
+
? `${toPosix(path.relative(root, componentDir))}/${docFile}`
|
|
500
|
+
: null,
|
|
501
|
+
description: catalog?.desc ?? "",
|
|
502
|
+
cn: catalog?.cn ?? "",
|
|
503
|
+
category: catalog?.category ?? layer,
|
|
504
|
+
categoryLabel:
|
|
505
|
+
catalog?.categoryLabel ??
|
|
506
|
+
(layer === "ui" ? "基础组件(ui)" : "高级组件(reui)"),
|
|
507
|
+
docsUrl: docsUrlFor(name),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
indexCache = entries;
|
|
512
|
+
return entries;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function componentEntry(name) {
|
|
516
|
+
const key = String(name ?? "")
|
|
517
|
+
.trim()
|
|
518
|
+
.toLowerCase();
|
|
519
|
+
const index = buildIndex();
|
|
520
|
+
if (index.has(key)) return index.get(key);
|
|
521
|
+
// 兼容驼峰/符号名(如 PageTable → table、QRCode → qr-code)
|
|
522
|
+
const normalized = key
|
|
523
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
524
|
+
.replace(/\s+/g, "-")
|
|
525
|
+
.toLowerCase();
|
|
526
|
+
return index.get(normalized) ?? null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function readFileByRel(relPath) {
|
|
530
|
+
if (!root) return null;
|
|
531
|
+
const target = path.resolve(root, relPath);
|
|
532
|
+
if (!target.startsWith(path.resolve(root))) return null;
|
|
533
|
+
try {
|
|
534
|
+
const stat = fs.statSync(target);
|
|
535
|
+
if (!stat.isFile() || stat.size > MAX_READ_BYTES) return null;
|
|
536
|
+
return fs.readFileSync(target, "utf8");
|
|
537
|
+
} catch {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** 汇总组件的类型声明(版本包内只有 dist 时作为 API 文档) */
|
|
543
|
+
function typeDocs(entry) {
|
|
544
|
+
const declFiles = entry.files.filter((file) => file.endsWith(".d.ts"));
|
|
545
|
+
const chunks = declFiles
|
|
546
|
+
.map((file) => {
|
|
547
|
+
const text = readFileByRel(`${entry.dir}/${file}`);
|
|
548
|
+
if (!text) return null;
|
|
549
|
+
return `### ${file}\n\n\`\`\`ts\n${text.trim()}\n\`\`\``;
|
|
550
|
+
})
|
|
551
|
+
.filter(Boolean);
|
|
552
|
+
return chunks.join("\n\n");
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** 从 Markdown 中提取带标题上下文的代码块 */
|
|
556
|
+
function parseCodeExamples(markdown) {
|
|
557
|
+
const examples = [];
|
|
558
|
+
const lines = String(markdown ?? "").split("\n");
|
|
559
|
+
let heading = "";
|
|
560
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
561
|
+
const line = lines[i];
|
|
562
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
|
|
563
|
+
if (headingMatch) heading = headingMatch[2].trim();
|
|
564
|
+
const fenceMatch = line.match(/^```(\w+)?\s*$/);
|
|
565
|
+
if (!fenceMatch) continue;
|
|
566
|
+
const lang = (fenceMatch[1] ?? "text").toLowerCase();
|
|
567
|
+
const body = [];
|
|
568
|
+
i += 1;
|
|
569
|
+
while (i < lines.length && !/^```\s*$/.test(lines[i])) {
|
|
570
|
+
body.push(lines[i]);
|
|
571
|
+
i += 1;
|
|
572
|
+
}
|
|
573
|
+
const code = body.join("\n").replace(/\s+$/, "");
|
|
574
|
+
if (code.trim() === "") continue;
|
|
575
|
+
examples.push({ title: heading || "未命名示例", lang, code, line: i });
|
|
576
|
+
}
|
|
577
|
+
return examples;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const knowledge = {
|
|
581
|
+
meta() {
|
|
582
|
+
return {
|
|
583
|
+
version,
|
|
584
|
+
source,
|
|
585
|
+
root: root ? toPosix(root) : null,
|
|
586
|
+
warning,
|
|
587
|
+
docsBase: "https://reglow.huoli.tech/components-library/reui",
|
|
588
|
+
};
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
/** 组件清单 */
|
|
592
|
+
listComponents({ category } = {}) {
|
|
593
|
+
const all = [...buildIndex().values()];
|
|
594
|
+
const filtered = category
|
|
595
|
+
? all.filter(
|
|
596
|
+
(entry) =>
|
|
597
|
+
entry.category === category || entry.categoryLabel === category,
|
|
598
|
+
)
|
|
599
|
+
: all;
|
|
600
|
+
return filtered.sort((a, b) => a.name.localeCompare(b.name));
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
/** 组件条目 */
|
|
604
|
+
getComponent(name) {
|
|
605
|
+
return componentEntry(name);
|
|
606
|
+
},
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* 组件文档
|
|
610
|
+
* @returns {Promise<{ markdown: string, kind: "readme"|"types"|"site"|"none", path: string|null, url: string|null }>}
|
|
611
|
+
*/
|
|
612
|
+
async getDocument(name) {
|
|
613
|
+
const entry = componentEntry(name);
|
|
614
|
+
const url = entry?.docsUrl ?? docsUrlFor(name) ?? null;
|
|
615
|
+
const readme = entry?.docPath ? readFileByRel(entry.docPath) : null;
|
|
616
|
+
if (readme) {
|
|
617
|
+
return { markdown: readme, kind: "readme", path: entry.docPath, url };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 包内无 README:先用官网文档兜底,再补充 dist 类型声明
|
|
621
|
+
const types = entry ? typeDocs(entry) : "";
|
|
622
|
+
const siteDoc = allowSite
|
|
623
|
+
? await fetchSiteDocument(entry?.name ?? String(name))
|
|
624
|
+
: null;
|
|
625
|
+
if (siteDoc) {
|
|
626
|
+
const appendix = types
|
|
627
|
+
? `\n\n---\n\n## 附:该版本包内类型声明(\`dist/**/*.d.ts\`)\n\n${types}`
|
|
628
|
+
: "";
|
|
629
|
+
return {
|
|
630
|
+
markdown: siteDoc.markdown + appendix,
|
|
631
|
+
kind: "site",
|
|
632
|
+
path: null,
|
|
633
|
+
url: siteDoc.url,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
if (types) {
|
|
637
|
+
return {
|
|
638
|
+
markdown: types,
|
|
639
|
+
kind: "types",
|
|
640
|
+
path: `${entry.dir}/index.d.ts`,
|
|
641
|
+
url,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
return { markdown: "", kind: "none", path: null, url };
|
|
645
|
+
},
|
|
646
|
+
|
|
647
|
+
/** 代码示例(优先本地 README,缺失时走官网兜底) */
|
|
648
|
+
async getCodeExamples(name, { index, lang } = {}) {
|
|
649
|
+
const entry = componentEntry(name);
|
|
650
|
+
const readme = entry?.docPath ? readFileByRel(entry.docPath) : null;
|
|
651
|
+
if (readme) {
|
|
652
|
+
let examples = parseCodeExamples(readme);
|
|
653
|
+
if (lang && lang !== "all")
|
|
654
|
+
examples = examples.filter((item) => item.lang === lang);
|
|
655
|
+
return {
|
|
656
|
+
examples:
|
|
657
|
+
index === undefined ? examples : examples.slice(index, index + 1),
|
|
658
|
+
source: "readme",
|
|
659
|
+
path: entry.docPath,
|
|
660
|
+
url: entry.docsUrl ?? null,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
if (allowSite) {
|
|
664
|
+
const siteDoc = await fetchSiteDocument(entry?.name ?? String(name));
|
|
665
|
+
if (siteDoc) {
|
|
666
|
+
let examples = parseCodeExamples(siteDoc.markdown);
|
|
667
|
+
if (lang && lang !== "all")
|
|
668
|
+
examples = examples.filter((item) => item.lang === lang);
|
|
669
|
+
return {
|
|
670
|
+
examples:
|
|
671
|
+
index === undefined ? examples : examples.slice(index, index + 1),
|
|
672
|
+
source: "site",
|
|
673
|
+
path: null,
|
|
674
|
+
url: siteDoc.url,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
examples: [],
|
|
680
|
+
source: "none",
|
|
681
|
+
path: null,
|
|
682
|
+
url: entry?.docsUrl ?? null,
|
|
683
|
+
};
|
|
684
|
+
},
|
|
685
|
+
|
|
686
|
+
/** 组件文件清单 */
|
|
687
|
+
listFiles(name) {
|
|
688
|
+
const entry = componentEntry(name);
|
|
689
|
+
if (!entry) return null;
|
|
690
|
+
return entry.files.map((file) => {
|
|
691
|
+
const rel = `${entry.dir}/${file}`;
|
|
692
|
+
const abs = root ? path.join(root, rel) : null;
|
|
693
|
+
let size = 0;
|
|
694
|
+
let lines = 0;
|
|
695
|
+
if (abs) {
|
|
696
|
+
try {
|
|
697
|
+
const stat = fs.statSync(abs);
|
|
698
|
+
size = stat.size;
|
|
699
|
+
if (isTextFile(file) && size <= MAX_READ_BYTES)
|
|
700
|
+
lines = lineCount(fs.readFileSync(abs, "utf8"));
|
|
701
|
+
} catch {
|
|
702
|
+
/* 忽略 */
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return { path: file, size, lines };
|
|
706
|
+
});
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
/** 读取组件内文件(带路径穿越防护) */
|
|
710
|
+
readComponentFile(name, file, { startLine, endLine } = {}) {
|
|
711
|
+
const entry = componentEntry(name);
|
|
712
|
+
if (!entry) return { error: `未找到组件「${name}」` };
|
|
713
|
+
const normalized = toPosix(String(file ?? "")).replace(/^\.\//, "");
|
|
714
|
+
const target = path.resolve(root ?? "", `${entry.dir}/${normalized}`);
|
|
715
|
+
if (!root || !target.startsWith(path.resolve(root))) {
|
|
716
|
+
return { error: "非法文件路径" };
|
|
717
|
+
}
|
|
718
|
+
const rel = toPosix(path.relative(root, target));
|
|
719
|
+
let text;
|
|
720
|
+
try {
|
|
721
|
+
const stat = fs.statSync(target);
|
|
722
|
+
if (!stat.isFile()) return { error: `不是文件:${normalized}` };
|
|
723
|
+
if (stat.size > MAX_READ_BYTES)
|
|
724
|
+
return { error: `文件过大(>${MAX_READ_BYTES} 字节)` };
|
|
725
|
+
text = fs.readFileSync(target, "utf8");
|
|
726
|
+
} catch {
|
|
727
|
+
const available = entry.files.slice(0, 40).join("\n- ");
|
|
728
|
+
return {
|
|
729
|
+
error: `文件不存在:${normalized}\n\n该组件可用文件:\n- ${available}`,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const total = lineCount(text);
|
|
733
|
+
let output = text;
|
|
734
|
+
let from = 1;
|
|
735
|
+
let to = total;
|
|
736
|
+
if (startLine || endLine) {
|
|
737
|
+
from = Math.max(1, Number(startLine ?? 1));
|
|
738
|
+
to = Math.min(total, Number(endLine ?? total));
|
|
739
|
+
output = text
|
|
740
|
+
.split("\n")
|
|
741
|
+
.slice(from - 1, to)
|
|
742
|
+
.map((line, i) => `${from + i}: ${line}`)
|
|
743
|
+
.join("\n");
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
path: rel,
|
|
747
|
+
text: output,
|
|
748
|
+
totalLines: total,
|
|
749
|
+
startLine: from,
|
|
750
|
+
endLine: to,
|
|
751
|
+
};
|
|
752
|
+
},
|
|
753
|
+
|
|
754
|
+
/** 提取函数 / 常量 / 类定义 */
|
|
755
|
+
findFunction(name, symbol, { file } = {}) {
|
|
756
|
+
const entry = componentEntry(name);
|
|
757
|
+
if (!entry) return { error: `未找到组件「${name}」` };
|
|
758
|
+
const candidates = file
|
|
759
|
+
? [toPosix(file)]
|
|
760
|
+
: entry.files.filter(
|
|
761
|
+
(item) =>
|
|
762
|
+
/\.(vue|ts|mjs|js)$/.test(item) && !item.endsWith(".d.ts"),
|
|
763
|
+
);
|
|
764
|
+
const searched = [];
|
|
765
|
+
for (const candidate of candidates) {
|
|
766
|
+
const text = readFileByRel(`${entry.dir}/${candidate}`);
|
|
767
|
+
if (!text) continue;
|
|
768
|
+
searched.push(candidate);
|
|
769
|
+
const found = extractSymbol(text, symbol);
|
|
770
|
+
if (found) {
|
|
771
|
+
return {
|
|
772
|
+
...found,
|
|
773
|
+
path: `${entry.dir}/${candidate}`,
|
|
774
|
+
component: entry.name,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const symbols = new Set();
|
|
779
|
+
for (const candidate of searched) {
|
|
780
|
+
const text = readFileByRel(`${entry.dir}/${candidate}`);
|
|
781
|
+
if (!text) continue;
|
|
782
|
+
listSymbols(text).forEach((item) => symbols.add(item));
|
|
783
|
+
}
|
|
784
|
+
const all = [...symbols].sort((a, b) =>
|
|
785
|
+
a.localeCompare(b, "en", { sensitivity: "base" }),
|
|
786
|
+
);
|
|
787
|
+
const similar = all.filter((item) =>
|
|
788
|
+
item.toLowerCase().includes(String(symbol).toLowerCase()),
|
|
789
|
+
);
|
|
790
|
+
return {
|
|
791
|
+
error: `未在组件「${entry.name}」中找到符号「${symbol}」`,
|
|
792
|
+
searched: searched.map((item) => `${entry.dir}/${item}`),
|
|
793
|
+
availableSymbols: all.slice(0, 300),
|
|
794
|
+
similarSymbols: similar,
|
|
795
|
+
allSymbolsCount: all.length,
|
|
796
|
+
};
|
|
797
|
+
},
|
|
798
|
+
|
|
799
|
+
/** 跨组件文档 / 源码检索 */
|
|
800
|
+
search(query, { category, limit = 20, component } = {}) {
|
|
801
|
+
const terms = String(query ?? "")
|
|
802
|
+
.split(/\s+/)
|
|
803
|
+
.map((term) => term.trim().toLowerCase())
|
|
804
|
+
.filter(Boolean);
|
|
805
|
+
if (terms.length === 0) return { hits: [], scanned: 0, terms };
|
|
806
|
+
|
|
807
|
+
const entries = component
|
|
808
|
+
? [componentEntry(component)].filter(Boolean)
|
|
809
|
+
: knowledge.listComponents({ category });
|
|
810
|
+
const targets = [];
|
|
811
|
+
for (const entry of entries) {
|
|
812
|
+
if (entry.docPath) targets.push({ entry, file: entry.docPath });
|
|
813
|
+
for (const item of entry.files) {
|
|
814
|
+
if (item === "README.md") continue;
|
|
815
|
+
if (!isTextFile(item)) continue;
|
|
816
|
+
targets.push({ entry, file: `${entry.dir}/${item}` });
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
// 库级文档
|
|
820
|
+
for (const extra of [
|
|
821
|
+
"README.md",
|
|
822
|
+
"guide.md",
|
|
823
|
+
"COMPONENT_FUNCTIONALITY.md",
|
|
824
|
+
]) {
|
|
825
|
+
if (root && fs.existsSync(path.join(root, extra))) {
|
|
826
|
+
targets.push({
|
|
827
|
+
entry: { name: "reui", category: "library", docsUrl: null },
|
|
828
|
+
file: extra,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const hits = [];
|
|
834
|
+
let scanned = 0;
|
|
835
|
+
for (const target of targets) {
|
|
836
|
+
if (hits.length >= limit) break;
|
|
837
|
+
const abs = path.join(root ?? "", target.file);
|
|
838
|
+
let text;
|
|
839
|
+
try {
|
|
840
|
+
const stat = fs.statSync(abs);
|
|
841
|
+
if (!stat.isFile() || stat.size > MAX_SEARCH_FILE_BYTES) continue;
|
|
842
|
+
text = fs.readFileSync(abs, "utf8");
|
|
843
|
+
} catch {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
scanned += 1;
|
|
847
|
+
const lower = text.toLowerCase();
|
|
848
|
+
if (!terms.every((term) => lower.includes(term))) continue;
|
|
849
|
+
const lines = text.split("\n");
|
|
850
|
+
let found = 0;
|
|
851
|
+
for (
|
|
852
|
+
let i = 0;
|
|
853
|
+
i < lines.length && found < 3 && hits.length < limit;
|
|
854
|
+
i += 1
|
|
855
|
+
) {
|
|
856
|
+
const lineLower = lines[i].toLowerCase();
|
|
857
|
+
if (!terms.some((term) => lineLower.includes(term))) continue;
|
|
858
|
+
const from = Math.max(0, i - 1);
|
|
859
|
+
const to = Math.min(lines.length, i + 2);
|
|
860
|
+
hits.push({
|
|
861
|
+
component: target.entry.name,
|
|
862
|
+
path: toPosix(target.file),
|
|
863
|
+
line: i + 1,
|
|
864
|
+
text: lines.slice(from, to).join("\n"),
|
|
865
|
+
docsUrl: target.entry.docsUrl ?? null,
|
|
866
|
+
});
|
|
867
|
+
found += 1;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return { hits, scanned, terms };
|
|
871
|
+
},
|
|
872
|
+
|
|
873
|
+
/** 库级指南(安装 / 主题 / i18n / Nuxt / 构建 / 约定 等章节) */
|
|
874
|
+
getGuide(topic) {
|
|
875
|
+
if (!root) return null;
|
|
876
|
+
const sources = ["README.md", "guide.md", "COMPONENT_FUNCTIONALITY.md"]
|
|
877
|
+
.map((file) => ({ file, text: readFileByRel(file) }))
|
|
878
|
+
.filter((item) => !!item.text);
|
|
879
|
+
if (sources.length === 0) return null;
|
|
880
|
+
if (!topic || topic === "overview") {
|
|
881
|
+
return { sections: sources, docsBase: knowledge.meta().docsBase };
|
|
882
|
+
}
|
|
883
|
+
const keywords = {
|
|
884
|
+
install: ["安装", "开始", "引入", "install", "usage", "快速"],
|
|
885
|
+
theme: ["主题", "theme", "dark", "color", "radius"],
|
|
886
|
+
i18n: ["i18n", "国际化", "locale", "语言"],
|
|
887
|
+
nuxt: ["nuxt", "模块", "module", "ssr"],
|
|
888
|
+
build: ["构建", "build", "打包", "发布", "dist"],
|
|
889
|
+
conventions: ["约定", "规范", "新增组件", "目录结构", "工程"],
|
|
890
|
+
}[topic] ?? [topic];
|
|
891
|
+
const sections = [];
|
|
892
|
+
for (const { file, text } of sources) {
|
|
893
|
+
const chunks = text.split(/\n(?=#{1,3}\s)/);
|
|
894
|
+
for (const chunk of chunks) {
|
|
895
|
+
const title = chunk.match(/^#{1,3}\s+(.*)$/m)?.[1] ?? "";
|
|
896
|
+
const haystack = `${title}\n${chunk.slice(0, 400)}`.toLowerCase();
|
|
897
|
+
if (
|
|
898
|
+
keywords.some((keyword) =>
|
|
899
|
+
haystack.includes(String(keyword).toLowerCase()),
|
|
900
|
+
)
|
|
901
|
+
) {
|
|
902
|
+
sections.push({ file, text: chunk.trim() });
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return { sections, docsBase: knowledge.meta().docsBase };
|
|
907
|
+
},
|
|
908
|
+
|
|
909
|
+
/** 官网组件总览(回退用) */
|
|
910
|
+
async siteOverview() {
|
|
911
|
+
if (!allowSite) return null;
|
|
912
|
+
return fetchSiteOverview();
|
|
913
|
+
},
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
logger(
|
|
917
|
+
`[reui-mcp] 知识源:source=${source} version=${version} root=${root ?? "(none)"}` +
|
|
918
|
+
(warning ? ` warning=${warning}` : ""),
|
|
919
|
+
);
|
|
920
|
+
return knowledge;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
export { CATEGORIES, PACKAGE_DIR, REUI_PACKAGE };
|