@lorrylurui/code-intelligence-mcp 1.0.1
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 +276 -0
- package/dist/cli/detect-duplicates.js +349 -0
- package/dist/cli/index-codebase.js +43 -0
- package/dist/config/env.js +23 -0
- package/dist/db/mysql.js +20 -0
- package/dist/index.js +14 -0
- package/dist/indexer/embedText.js +28 -0
- package/dist/indexer/extractMeta.js +118 -0
- package/dist/indexer/heuristics.js +96 -0
- package/dist/indexer/indexProject.js +178 -0
- package/dist/indexer/persistSymbols.js +55 -0
- package/dist/prompts/recommendComponentPrompt.js +30 -0
- package/dist/prompts/reusableCodeAdvisorPrompt.js +63 -0
- package/dist/repositories/symbolRepository.js +219 -0
- package/dist/server/createServer.js +29 -0
- package/dist/services/embeddingClient.js +37 -0
- package/dist/services/ranking.js +161 -0
- package/dist/services/reindex.js +45 -0
- package/dist/services/vectorMath.js +17 -0
- package/dist/skills/rankSymbols.js +24 -0
- package/dist/skills/recommendComponent.js +39 -0
- package/dist/tools/getSymbolDetail.js +32 -0
- package/dist/tools/recommendComponent.js +28 -0
- package/dist/tools/reindex.js +37 -0
- package/dist/tools/searchByStructure.js +55 -0
- package/dist/tools/searchSymbols.js +81 -0
- package/dist/types/symbol.js +1 -0
- package/package.json +45 -0
package/dist/db/mysql.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import mysql from "mysql2/promise";
|
|
2
|
+
import { env } from "../config/env.js";
|
|
3
|
+
let pool = null;
|
|
4
|
+
export function getMySqlPool() {
|
|
5
|
+
if (!env.mysqlEnabled) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
if (!pool) {
|
|
9
|
+
pool = mysql.createPool({
|
|
10
|
+
host: env.mysqlHost,
|
|
11
|
+
port: env.mysqlPort,
|
|
12
|
+
user: env.mysqlUser,
|
|
13
|
+
password: env.mysqlPassword,
|
|
14
|
+
database: env.mysqlDatabase,
|
|
15
|
+
waitForConnections: true,
|
|
16
|
+
connectionLimit: 10
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return pool;
|
|
20
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { validateEnv } from './config/env.js';
|
|
4
|
+
import { createServer } from './server/createServer.js';
|
|
5
|
+
async function main() {
|
|
6
|
+
validateEnv();
|
|
7
|
+
const server = createServer();
|
|
8
|
+
const transport = new StdioServerTransport();
|
|
9
|
+
await server.connect(transport);
|
|
10
|
+
}
|
|
11
|
+
main().catch((error) => {
|
|
12
|
+
console.error('MCP 服务启动失败:', error);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function briefMeta(meta) {
|
|
2
|
+
const keys = ["props", "params", "properties", "hooks"];
|
|
3
|
+
const parts = [];
|
|
4
|
+
for (const k of keys) {
|
|
5
|
+
const v = meta[k];
|
|
6
|
+
if (Array.isArray(v)) {
|
|
7
|
+
const strs = v.filter((x) => typeof x === "string");
|
|
8
|
+
if (strs.length)
|
|
9
|
+
parts.push(`${k}: ${strs.slice(0, 24).join(", ")}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return parts.join("; ");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 拼成一段供向量模型编码的文本(名称、路径、注释、meta 摘要、源码片段)。
|
|
16
|
+
*/
|
|
17
|
+
export function indexedRowToEmbedText(row) {
|
|
18
|
+
const metaBit = briefMeta(row.meta);
|
|
19
|
+
return [
|
|
20
|
+
`${row.type} ${row.name}`,
|
|
21
|
+
row.path,
|
|
22
|
+
row.description ?? "",
|
|
23
|
+
metaBit,
|
|
24
|
+
(row.content ?? "").slice(0, 1200)
|
|
25
|
+
]
|
|
26
|
+
.filter((s) => s.length > 0)
|
|
27
|
+
.join("\n");
|
|
28
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从 AST 抽取写入 `symbols.meta` 的结构化字段(props/params、hooks、返回值、类型成员等)。
|
|
3
|
+
*/
|
|
4
|
+
import { Node, } from 'ts-morph';
|
|
5
|
+
/**
|
|
6
|
+
* 将节点收窄为可调用的函数形态,便于统一抽取参数与函数体。
|
|
7
|
+
* @returns 若不是函数声明/表达式/箭头函数则为 `undefined`。
|
|
8
|
+
*/
|
|
9
|
+
function asFn(node) {
|
|
10
|
+
if (Node.isFunctionDeclaration(node))
|
|
11
|
+
return node;
|
|
12
|
+
if (Node.isFunctionExpression(node))
|
|
13
|
+
return node;
|
|
14
|
+
if (Node.isArrowFunction(node))
|
|
15
|
+
return node;
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 取第一个形参的「对外可见名」:对象解构取各属性名,单参数取标识符文本。
|
|
20
|
+
* @returns 用于后续在 `component` 中映射为 `props`,在 `util` 中保留为 `params`。
|
|
21
|
+
*/
|
|
22
|
+
function extractParamNames(fn) {
|
|
23
|
+
const params = fn.getParameters();
|
|
24
|
+
if (params.length === 0)
|
|
25
|
+
return [];
|
|
26
|
+
const first = params[0];
|
|
27
|
+
const nameNode = first.getNameNode();
|
|
28
|
+
if (Node.isObjectBindingPattern(nameNode)) {
|
|
29
|
+
return nameNode.getElements().map((el) => el.getName());
|
|
30
|
+
}
|
|
31
|
+
if (Node.isIdentifier(nameNode)) {
|
|
32
|
+
return [nameNode.getText()];
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 从第一个参数的类型信息提取字段名(例如 `props: DialogProps` -> `title/content/onClose`)。
|
|
38
|
+
* @returns 若无可解析类型或无法安全推断,则返回空数组。
|
|
39
|
+
*/
|
|
40
|
+
function extractParamTypeFieldNames(fn) {
|
|
41
|
+
const params = fn.getParameters();
|
|
42
|
+
if (params.length === 0)
|
|
43
|
+
return [];
|
|
44
|
+
const first = params[0];
|
|
45
|
+
const type = first.getType();
|
|
46
|
+
const properties = type.getProperties();
|
|
47
|
+
if (!properties.length)
|
|
48
|
+
return [];
|
|
49
|
+
const names = properties.map((p) => p.getName());
|
|
50
|
+
// 防止出现泛型对象原型噪音字段。
|
|
51
|
+
return names.filter((n) => n !== '__type');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 在函数体内收集形如 `useXxx(...)` 的调用名(React Hooks 约定)。
|
|
55
|
+
* @returns 去重、排序后的钩子名列表,写入 `meta.hooks`;无则为空数组。
|
|
56
|
+
*/
|
|
57
|
+
export function extractHooksFromBody(node) {
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
node.forEachDescendant((n) => {
|
|
60
|
+
if (Node.isCallExpression(n)) {
|
|
61
|
+
const ex = n.getExpression();
|
|
62
|
+
if (Node.isIdentifier(ex)) {
|
|
63
|
+
const id = ex.getText();
|
|
64
|
+
if (id.startsWith('use'))
|
|
65
|
+
seen.add(id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return [...seen].sort();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 读取显式标注的返回类型节点文本(无则 `null`)。
|
|
73
|
+
* @returns 写入 `meta.returnType`,供检索与对比工具函数签名。
|
|
74
|
+
*/
|
|
75
|
+
export function extractReturnTypeText(fn) {
|
|
76
|
+
const rt = fn.getReturnTypeNode();
|
|
77
|
+
return rt ? rt.getText() : null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 汇总单个可调用代码块的元数据:`params`、可选 `hooks`、可选 `returnType`。
|
|
81
|
+
* @returns 扁平对象,由上层按 `component`/`util` 再映射为 `props` 或保留 `params`。
|
|
82
|
+
*/
|
|
83
|
+
export function extractFunctionMeta(fn) {
|
|
84
|
+
const params = extractParamNames(fn);
|
|
85
|
+
const paramTypeFields = extractParamTypeFieldNames(fn);
|
|
86
|
+
const hooks = extractHooksFromBody(fn);
|
|
87
|
+
const returnType = extractReturnTypeText(fn);
|
|
88
|
+
return {
|
|
89
|
+
params,
|
|
90
|
+
...(paramTypeFields.length ? { paramTypeFields } : {}),
|
|
91
|
+
...(hooks.length ? { hooks } : {}),
|
|
92
|
+
...(returnType ? { returnType } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 若节点本身是函数声明/表达式/箭头函数,则抽取 `extractFunctionMeta` 结果。
|
|
97
|
+
* @returns 非可调用节点时为 `null`。
|
|
98
|
+
*/
|
|
99
|
+
export function extractMetaFromCallable(node) {
|
|
100
|
+
const fn = asFn(node);
|
|
101
|
+
if (!fn)
|
|
102
|
+
return null;
|
|
103
|
+
return extractFunctionMeta(fn);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 为 `interface` / `type` 代码块构造 `meta`:`interface` 带属性名列表,别名仅标记 `typeAlias`。
|
|
107
|
+
* @returns 写入 `symbols.meta`,供结构化搜索(如按属性名)扩展使用。
|
|
108
|
+
*/
|
|
109
|
+
export function extractInterfaceOrTypeMeta(node) {
|
|
110
|
+
if (Node.isInterfaceDeclaration(node)) {
|
|
111
|
+
const props = node.getProperties().map((p) => p.getName());
|
|
112
|
+
return { kind: 'interface', properties: props };
|
|
113
|
+
}
|
|
114
|
+
if (Node.isTypeAliasDeclaration(node)) {
|
|
115
|
+
return { kind: 'typeAlias' };
|
|
116
|
+
}
|
|
117
|
+
return { kind: 'unknown' };
|
|
118
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSX 检测、路径/selector 启发式、JSDoc 摘要、相对路径。
|
|
3
|
+
*/
|
|
4
|
+
import { SyntaxKind } from "ts-morph";
|
|
5
|
+
/**
|
|
6
|
+
* 判断源文件路径是否为 TSX(React JSX 语法所在扩展名)。
|
|
7
|
+
* @returns `true` 表示按「可能出现 JSX」处理,用于与 `.ts` 区分组件启发式。
|
|
8
|
+
*/
|
|
9
|
+
export function isTsxFile(filePath) {
|
|
10
|
+
return filePath.toLowerCase().endsWith(".tsx");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 在 AST 子树中是否出现 JSX 节点(元素、自闭合标签或 Fragment)。
|
|
14
|
+
* @returns `true` 表示该导出更像 React 组件(与 `isTsxFile` 等组合用于分类)。
|
|
15
|
+
*/
|
|
16
|
+
export function hasJsxInNode(node) {
|
|
17
|
+
let found = false;
|
|
18
|
+
node.forEachDescendant((n) => {
|
|
19
|
+
const k = n.getKind();
|
|
20
|
+
if (k === SyntaxKind.JsxElement ||
|
|
21
|
+
k === SyntaxKind.JsxSelfClosingElement ||
|
|
22
|
+
k === SyntaxKind.JsxFragment) {
|
|
23
|
+
found = true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return found;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 从文件路径推断业务语义目录名(如 `.../components/form/Button.tsx` → `form`)。
|
|
31
|
+
* @returns 命中 `components|features|...` 下一级目录名;无法推断时为 `null`,写入 DB 的 `category` 字段。
|
|
32
|
+
*/
|
|
33
|
+
export function inferCategoryFromPath(filePath) {
|
|
34
|
+
const norm = filePath.replace(/\\/g, "/");
|
|
35
|
+
const parts = norm.split("/");
|
|
36
|
+
const markers = ["components", "features", "modules", "pages", "widgets", "hooks"];
|
|
37
|
+
for (let i = 0; i < parts.length; i++) {
|
|
38
|
+
const m = markers.indexOf(parts[i]);
|
|
39
|
+
if (m >= 0 && parts[i + 1]) {
|
|
40
|
+
return parts[i + 1];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 启发式判断导出是否更像状态选择器(selector)。
|
|
47
|
+
* @returns `true` 时索引类型记为 `selector`,否则同文件下函数多为 `util`。
|
|
48
|
+
*/
|
|
49
|
+
export function isSelectorLike(filePath, exportName) {
|
|
50
|
+
const lowerPath = filePath.toLowerCase();
|
|
51
|
+
if (lowerPath.includes("selector"))
|
|
52
|
+
return true;
|
|
53
|
+
if (/selector$/i.test(exportName))
|
|
54
|
+
return true;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 读取节点(或父节点)上第一段 JSDoc 的 **描述正文**(不含 `@tag`)。
|
|
59
|
+
* @returns 供写入 `symbols.description`;无注释时为 `null`。
|
|
60
|
+
*/
|
|
61
|
+
export function getLeadingDocDescription(node) {
|
|
62
|
+
const tryNode = (n) => {
|
|
63
|
+
const jd = n.getJsDocs;
|
|
64
|
+
if (typeof jd !== "function")
|
|
65
|
+
return null;
|
|
66
|
+
const docs = jd.call(n);
|
|
67
|
+
if (!docs?.length)
|
|
68
|
+
return null;
|
|
69
|
+
const t = docs[0].getDescription().trim();
|
|
70
|
+
return t || null;
|
|
71
|
+
};
|
|
72
|
+
return tryNode(node) ?? (node.getParent() ? tryNode(node.getParent()) : null);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 将绝对路径转为相对 `projectRoot` 的路径,作为库中 `symbols.path`(便于跨机器、展示)。
|
|
76
|
+
* @returns 相对路径;若无法裁掉前缀则回退为原始绝对路径。
|
|
77
|
+
*/
|
|
78
|
+
export function getRelativePathForDisplay(projectRoot, absolutePath) {
|
|
79
|
+
const r = projectRoot.replace(/\\/g, "/");
|
|
80
|
+
const a = absolutePath.replace(/\\/g, "/");
|
|
81
|
+
if (a.startsWith(r)) {
|
|
82
|
+
return a.slice(r.length + 1);
|
|
83
|
+
}
|
|
84
|
+
return absolutePath;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 截取节点对应源码文本,用于 `symbols.content`(全文检索或人工核对)。
|
|
88
|
+
* @param maxLen 超长时截断并追加注释标记,避免单行 MEDIUMTEXT 过大。
|
|
89
|
+
* @returns 源码字符串;无额外语义,仅作存档片段。
|
|
90
|
+
*/
|
|
91
|
+
export function snippetForNode(node, maxLen = 4000) {
|
|
92
|
+
const raw = node.getText();
|
|
93
|
+
if (raw.length <= maxLen)
|
|
94
|
+
return raw;
|
|
95
|
+
return raw.slice(0, maxLen) + "\n/* ... truncated ... */";
|
|
96
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 扫描源码目录,用 ts-morph 解析导出并生成待写入 MySQL 的代码块行。
|
|
3
|
+
*/
|
|
4
|
+
import fg from 'fast-glob';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { Node, Project } from 'ts-morph';
|
|
7
|
+
import { extractInterfaceOrTypeMeta, extractMetaFromCallable, } from './extractMeta.js';
|
|
8
|
+
import { getLeadingDocDescription, getRelativePathForDisplay, hasJsxInNode, inferCategoryFromPath, isSelectorLike, isTsxFile, snippetForNode, } from './heuristics.js';
|
|
9
|
+
/**
|
|
10
|
+
* 将 `extractFunctionMeta` 的 `params` 转为组件用的 `props` 名,或保留为 `params`。
|
|
11
|
+
* @returns 供 `IndexedSymbolRow.meta` 序列化入库。
|
|
12
|
+
*/
|
|
13
|
+
function mergeCallableMeta(symbolType, raw) {
|
|
14
|
+
if (!raw)
|
|
15
|
+
return {};
|
|
16
|
+
const params = raw.params;
|
|
17
|
+
const paramTypeFields = raw.paramTypeFields;
|
|
18
|
+
const { params: _p, paramTypeFields: _f, ...rest } = raw;
|
|
19
|
+
if (symbolType === 'component' && params?.length) {
|
|
20
|
+
const props = [
|
|
21
|
+
...new Set([...(paramTypeFields ?? []), ...params]),
|
|
22
|
+
].filter((name) => name.toLowerCase() !== 'props');
|
|
23
|
+
return { ...rest, props: props.length ? props : params };
|
|
24
|
+
}
|
|
25
|
+
return { ...rest, ...(params?.length ? { params } : {}) };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 将 MCP/ts 的 `default` 导出键还原为具体代码块名(函数名、变量名、类名)。
|
|
29
|
+
* @returns 用于 `IndexedSymbolRow.name` 与去重键 `(path, name)`。
|
|
30
|
+
*/
|
|
31
|
+
function resolveExportName(exportName, decl) {
|
|
32
|
+
if (exportName !== 'default')
|
|
33
|
+
return exportName;
|
|
34
|
+
if (Node.isFunctionDeclaration(decl) && decl.getName()) {
|
|
35
|
+
return decl.getName();
|
|
36
|
+
}
|
|
37
|
+
if (Node.isVariableDeclaration(decl)) {
|
|
38
|
+
return decl.getName();
|
|
39
|
+
}
|
|
40
|
+
if (Node.isClassDeclaration(decl) && decl.getName()) {
|
|
41
|
+
return decl.getName();
|
|
42
|
+
}
|
|
43
|
+
return 'default';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 将单个导出声明转为一条索引行;不支持的声明类型返回 `null`。
|
|
47
|
+
* @returns 成功时含分类结果与 meta,供 `persistSymbols` 写入数据库。
|
|
48
|
+
*/
|
|
49
|
+
function processDeclaration(exportName, decl, sf, projectRoot) {
|
|
50
|
+
const filePath = sf.getFilePath();
|
|
51
|
+
const relPath = getRelativePathForDisplay(projectRoot, filePath);
|
|
52
|
+
const category = inferCategoryFromPath(filePath);
|
|
53
|
+
const name = resolveExportName(exportName, decl);
|
|
54
|
+
const description = getLeadingDocDescription(decl) ?? null;
|
|
55
|
+
if (Node.isInterfaceDeclaration(decl) ||
|
|
56
|
+
Node.isTypeAliasDeclaration(decl)) {
|
|
57
|
+
const meta = extractInterfaceOrTypeMeta(decl);
|
|
58
|
+
return {
|
|
59
|
+
name,
|
|
60
|
+
type: 'type',
|
|
61
|
+
category,
|
|
62
|
+
path: relPath,
|
|
63
|
+
description,
|
|
64
|
+
content: snippetForNode(decl),
|
|
65
|
+
meta,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (Node.isFunctionDeclaration(decl)) {
|
|
69
|
+
const jsx = isTsxFile(filePath) && hasJsxInNode(decl);
|
|
70
|
+
const type = jsx
|
|
71
|
+
? 'component'
|
|
72
|
+
: isSelectorLike(filePath, name)
|
|
73
|
+
? 'selector'
|
|
74
|
+
: 'util';
|
|
75
|
+
const raw = extractMetaFromCallable(decl);
|
|
76
|
+
const meta = mergeCallableMeta(type, raw);
|
|
77
|
+
return {
|
|
78
|
+
name,
|
|
79
|
+
type,
|
|
80
|
+
category,
|
|
81
|
+
path: relPath,
|
|
82
|
+
description,
|
|
83
|
+
content: snippetForNode(decl),
|
|
84
|
+
meta,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (Node.isVariableDeclaration(decl)) {
|
|
88
|
+
const init = decl.getInitializer();
|
|
89
|
+
if (!init)
|
|
90
|
+
return null;
|
|
91
|
+
let callable;
|
|
92
|
+
if (Node.isArrowFunction(init) || Node.isFunctionExpression(init)) {
|
|
93
|
+
callable = init;
|
|
94
|
+
}
|
|
95
|
+
else if (Node.isCallExpression(init)) {
|
|
96
|
+
const arg0 = init.getArguments()[0];
|
|
97
|
+
if (arg0 &&
|
|
98
|
+
(Node.isArrowFunction(arg0) || Node.isFunctionExpression(arg0))) {
|
|
99
|
+
callable = arg0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!callable)
|
|
103
|
+
return null;
|
|
104
|
+
const jsx = isTsxFile(filePath) && hasJsxInNode(callable);
|
|
105
|
+
const type = jsx
|
|
106
|
+
? 'component'
|
|
107
|
+
: isSelectorLike(filePath, name)
|
|
108
|
+
? 'selector'
|
|
109
|
+
: 'util';
|
|
110
|
+
const raw = extractMetaFromCallable(callable);
|
|
111
|
+
const meta = mergeCallableMeta(type, raw);
|
|
112
|
+
return {
|
|
113
|
+
name,
|
|
114
|
+
type,
|
|
115
|
+
category,
|
|
116
|
+
path: relPath,
|
|
117
|
+
description,
|
|
118
|
+
content: snippetForNode(callable),
|
|
119
|
+
meta,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (Node.isClassDeclaration(decl)) {
|
|
123
|
+
// 轻量:仅将 class 记为 util(后续可扩展为带 JSX 的组件类)
|
|
124
|
+
return {
|
|
125
|
+
name,
|
|
126
|
+
type: 'util',
|
|
127
|
+
category,
|
|
128
|
+
path: relPath,
|
|
129
|
+
description,
|
|
130
|
+
content: snippetForNode(decl),
|
|
131
|
+
meta: { kind: 'class' },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const DEFAULT_IGNORE = [
|
|
137
|
+
'**/node_modules/**',
|
|
138
|
+
'**/dist/**',
|
|
139
|
+
'**/.git/**',
|
|
140
|
+
'**/coverage/**',
|
|
141
|
+
];
|
|
142
|
+
/**
|
|
143
|
+
* 按 glob 收集文件,用 ts-morph 加载并遍历每个文件的导出,生成全部代码块行。
|
|
144
|
+
* @returns 数组可交给 `upsertSymbols`;无匹配文件时返回空数组(不写库)。
|
|
145
|
+
*/
|
|
146
|
+
export async function indexProject(opts) {
|
|
147
|
+
const projectRoot = resolve(opts.projectRoot);
|
|
148
|
+
const patterns = (opts.globPatterns ?? ['src/**/*.{ts,tsx}']).map((p) => p.startsWith('/') ? p : join(projectRoot, p).replace(/\\/g, '/'));
|
|
149
|
+
const ignore = [...DEFAULT_IGNORE, ...(opts.ignore ?? [])];
|
|
150
|
+
const files = await fg(patterns, {
|
|
151
|
+
absolute: true,
|
|
152
|
+
ignore,
|
|
153
|
+
onlyFiles: true,
|
|
154
|
+
dot: false,
|
|
155
|
+
});
|
|
156
|
+
if (files.length === 0) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const project = new Project({
|
|
160
|
+
tsConfigFilePath: join(projectRoot, 'tsconfig.json'),
|
|
161
|
+
skipAddingFilesFromTsConfig: true,
|
|
162
|
+
skipFileDependencyResolution: true,
|
|
163
|
+
});
|
|
164
|
+
project.addSourceFilesAtPaths(files);
|
|
165
|
+
const out = [];
|
|
166
|
+
for (const sf of project.getSourceFiles()) {
|
|
167
|
+
const exported = sf.getExportedDeclarations();
|
|
168
|
+
for (const [exportName, decls] of exported) {
|
|
169
|
+
for (const decl of decls) {
|
|
170
|
+
const row = processDeclaration(exportName, decl, sf, projectRoot);
|
|
171
|
+
if (row) {
|
|
172
|
+
out.push(row);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 依赖表上 `(path, name)` 唯一键:新行插入,已存在则更新类型/描述/内容与 meta;**不**修改 `usage_count`。
|
|
3
|
+
* @param rows 来自 `indexProject`;空数组时立即返回,不开启事务。
|
|
4
|
+
* @param embeddings 与 `rows` 等长;某项为 `null` 表示本行不更新已有 `embedding`(新行则写入 NULL)。
|
|
5
|
+
* @returns Promise 在提交成功时 resolve;任一行失败则整批回滚并抛出异常。
|
|
6
|
+
*/
|
|
7
|
+
export async function upsertSymbols(pool, rows, embeddings) {
|
|
8
|
+
if (rows.length === 0)
|
|
9
|
+
return;
|
|
10
|
+
if (embeddings && embeddings.length !== rows.length) {
|
|
11
|
+
throw new Error("upsertSymbols: embeddings length must match rows");
|
|
12
|
+
}
|
|
13
|
+
const actor = process.env.GITHUB_USERNAME?.trim() || "LorryIsLuRui";
|
|
14
|
+
const sql = `
|
|
15
|
+
INSERT INTO symbols (name, type, category, path, description, content, meta, insert_user, updated_user, embedding)
|
|
16
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
17
|
+
ON DUPLICATE KEY UPDATE
|
|
18
|
+
type = VALUES(type),
|
|
19
|
+
category = VALUES(category),
|
|
20
|
+
description = VALUES(description),
|
|
21
|
+
content = VALUES(content),
|
|
22
|
+
meta = VALUES(meta),
|
|
23
|
+
updated_user = VALUES(updated_user),
|
|
24
|
+
embedding = CASE WHEN VALUES(embedding) IS NOT NULL THEN VALUES(embedding) ELSE embedding END
|
|
25
|
+
`;
|
|
26
|
+
const conn = await pool.getConnection();
|
|
27
|
+
try {
|
|
28
|
+
await conn.beginTransaction();
|
|
29
|
+
for (let i = 0; i < rows.length; i++) {
|
|
30
|
+
const r = rows[i];
|
|
31
|
+
const emb = embeddings?.[i];
|
|
32
|
+
const embJson = emb !== undefined && emb !== null ? JSON.stringify(emb) : null;
|
|
33
|
+
await conn.query(sql, [
|
|
34
|
+
r.name,
|
|
35
|
+
r.type,
|
|
36
|
+
r.category,
|
|
37
|
+
r.path,
|
|
38
|
+
r.description,
|
|
39
|
+
r.content,
|
|
40
|
+
JSON.stringify(r.meta),
|
|
41
|
+
actor,
|
|
42
|
+
actor,
|
|
43
|
+
embJson
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
await conn.commit();
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
await conn.rollback();
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
conn.release();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const DESCRIPTION = "使用 recommend_component 流程推荐最合适的可复用组件候选。";
|
|
3
|
+
export function registerRecommendComponentPrompt(server) {
|
|
4
|
+
server.prompt("recommend-component", DESCRIPTION, {
|
|
5
|
+
requirement: z.string().describe("用户需求描述,例如:带校验的表单组件"),
|
|
6
|
+
props: z
|
|
7
|
+
.array(z.string())
|
|
8
|
+
.optional()
|
|
9
|
+
.describe("结构过滤所需的可选 props,例如 onChange,value")
|
|
10
|
+
}, async (args) => {
|
|
11
|
+
const propsHint = args.props?.length
|
|
12
|
+
? `\n并传入 props 参数:${JSON.stringify(args.props)}`
|
|
13
|
+
: "";
|
|
14
|
+
return {
|
|
15
|
+
description: DESCRIPTION,
|
|
16
|
+
messages: [
|
|
17
|
+
{
|
|
18
|
+
role: "user",
|
|
19
|
+
content: {
|
|
20
|
+
type: "text",
|
|
21
|
+
text: `你正在执行 Phase 4 的 skill 验证流程。\n` +
|
|
22
|
+
`1)调用 recommend_component,query 参数为:${JSON.stringify(args.requirement)}。${propsHint}\n` +
|
|
23
|
+
`2)返回 Top 结果,并给出 score、reason 以及使用建议。\n` +
|
|
24
|
+
`3)如果没有结果,请明确说明缺口,并给出最接近的候选项。`
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const REUSABLE_CODE_ADVISOR_DESCRIPTION = '在实现需求时检索并推荐最合适的可复用代码代码块(函数/类/模块等)。在用户要求复用现有代码、询问是否已有组件/函数/服务、或要在多个代码块中选最优时使用。';
|
|
3
|
+
/** 与 SKILL.md 中 `# 可复用代码推荐` 起至约束、示例说明为止的正文一致(无 YAML frontmatter)。 */
|
|
4
|
+
const REUSABLE_CODE_ADVISOR_MARKDOWN = `# 可复用代码推荐
|
|
5
|
+
|
|
6
|
+
## 工作流
|
|
7
|
+
|
|
8
|
+
当用户需要可复用代码或实现类需求时,按顺序执行:
|
|
9
|
+
|
|
10
|
+
1. 先判断意图:若意图是“组件推荐”(如推荐表单组件、弹窗组件、列表组件等),优先调用 \`recommend_component\`;否则走通用多工具流程。
|
|
11
|
+
2. 通用多工具流程中,先调用 \`search_symbols\` 检索可能满足需求的候选代码块;query 用短英文/关键词(如 formInput),需要限定类型时再加 type(如 component),不要用整句中文。
|
|
12
|
+
3. 执行细则:先 \`search_symbols(limit=20)\` 拉候选,再对 Top 3 调用 \`get_symbol_detail\` 做深度判断。
|
|
13
|
+
4. 若仅凭签名/摘要无法判断,对最相关的若干候选调用 \`get_symbol_detail\` 获取详情。
|
|
14
|
+
5. 从以下维度对比候选:
|
|
15
|
+
- 与用户需求的**功能匹配度**
|
|
16
|
+
- **API 是否简单**、入参是否合适
|
|
17
|
+
- **依赖与副作用**风险
|
|
18
|
+
- **复用安全性**(稳定性、耦合度、是否便于扩展)
|
|
19
|
+
6. 给出**唯一首选**推荐,并说明理由。
|
|
20
|
+
|
|
21
|
+
## 回复结构
|
|
22
|
+
|
|
23
|
+
按此结构输出(字段名可保留英文或改为中文小标题,二选一全文统一):
|
|
24
|
+
|
|
25
|
+
- **首选:** \`<代码块名>\`
|
|
26
|
+
- **理由:** 1~3 条要点
|
|
27
|
+
- **其他候选:** 简要列出及取舍
|
|
28
|
+
- **用法提示:** 结合用户场景的最小集成说明
|
|
29
|
+
|
|
30
|
+
## 约束
|
|
31
|
+
|
|
32
|
+
- 优先推荐已有可复用代码块,避免轻易建议新写一套。
|
|
33
|
+
- 若无合适代码块,明确说明,并给出最接近的选项及差距。
|
|
34
|
+
- 推理简洁,面向落地实现。
|
|
35
|
+
|
|
36
|
+
## 更多示例
|
|
37
|
+
|
|
38
|
+
与仓库内 \`.cursor/skills/reusable-code-advisor/examples.md\` 中的示例一致(在 Cursor 或本地打开该文件查看)。
|
|
39
|
+
`;
|
|
40
|
+
export function registerReusableCodeAdvisorPrompt(server) {
|
|
41
|
+
server.prompt('reusable-code-advisor', REUSABLE_CODE_ADVISOR_DESCRIPTION, {
|
|
42
|
+
userRequest: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('用户当前需求或关键词,用于聚焦检索与推荐(可选)'),
|
|
46
|
+
}, async (args) => {
|
|
47
|
+
const suffix = args.userRequest?.trim()
|
|
48
|
+
? `\n\n## 当前上下文\n\n${args.userRequest.trim()}\n`
|
|
49
|
+
: '';
|
|
50
|
+
return {
|
|
51
|
+
description: REUSABLE_CODE_ADVISOR_DESCRIPTION,
|
|
52
|
+
messages: [
|
|
53
|
+
{
|
|
54
|
+
role: 'user',
|
|
55
|
+
content: {
|
|
56
|
+
type: 'text',
|
|
57
|
+
text: `${REUSABLE_CODE_ADVISOR_MARKDOWN}${suffix}`,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|