freeform-modeling-mcp 1.0.32 → 1.0.34
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 +3 -1
- package/package.json +3 -1
- package/src/kds-api-knowledge/api-plugin-outline-whitelist.ts +24 -0
- package/src/kds-api-knowledge/batch-methods-full.ts +480 -0
- package/src/kds-api-knowledge/batch-search-methods.ts +124 -0
- package/src/kds-api-knowledge/core-modeling-api-graph.ts +389 -0
- package/src/kds-api-knowledge/kds-api-dts.ts +190 -0
- package/src/kds-api-knowledge/kds-system-prompt.ts +101 -0
- package/src/lib/wsbridge/bridge.ts +78 -18
- package/src/llmClient.ts +27 -6
- package/src/prompt.ts +30 -2
- package/src/server.ts +18 -0
- package/src/tools/knowledge-tools.ts +170 -0
- package/src/tools/script-tools.ts +186 -0
- package/src/tools/tools-info.ts +19 -0
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- 🧰 丰富工具集(`src/tools/`包含50+建模工具)
|
|
6
6
|
- 基础建模工具(立方体/球体/圆环创建)
|
|
7
7
|
- KJL资源库集成(资产搜索/置入/替换)
|
|
8
|
+
- KDS 脚本片段(`run_kds_script`)+ API 知识检索(`get_core_modeling_api` / `batch_search_methods` / `batch_get_methods_full`)
|
|
8
9
|
- 通用 GLB/GLTF 导入
|
|
9
10
|
- 材质替换
|
|
10
11
|
|
|
@@ -27,7 +28,7 @@ npx -y freeform-modeling-mcp
|
|
|
27
28
|
### 故障排除
|
|
28
29
|
若启动失败,可尝试安装指定版本:
|
|
29
30
|
```bash
|
|
30
|
-
npm i -g freeform-modeling-mcp@1.0.
|
|
31
|
+
npm i -g freeform-modeling-mcp@1.0.33 --registry=https://registry.npmjs.org
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
## 核心模块
|
|
@@ -56,6 +57,7 @@ npm i -g freeform-modeling-mcp@1.0.26 --registry=https://registry.npmjs.org
|
|
|
56
57
|
1. 工具扩展:在`src/tools/`创建新工具类
|
|
57
58
|
2. API集成:参考`import-tools.ts`接入第三方服务
|
|
58
59
|
3. 客户端协议:遵循`llmClient.ts`定义的消息格式
|
|
60
|
+
4. KDS API 知识实现位于`src/kds-api-knowledge/`(自 `kds-Plugin-MCP-Server/src/lib` 抽拷;升级时对照上游同步该目录,并钉齐 `@qunhe/kds-api` 版本)
|
|
59
61
|
|
|
60
62
|
## 贡献者协议
|
|
61
63
|
请遵循`src/prompt.ts`中定义的建模规范提交代码
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freeform-modeling-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.34",
|
|
4
4
|
"description": "KooMaster MCP Server with WebSocket Bridge",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"license": "ISC",
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@kudashi/kds-api": "^2.14.8",
|
|
23
24
|
"@types/cors": "^2.8.17",
|
|
24
25
|
"@types/express": "^5.0.1",
|
|
25
26
|
"@types/node": "22.13.5",
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"multer": "^1.4.5-lts.1",
|
|
39
40
|
"node-fetch": "^3.3.2",
|
|
40
41
|
"nodemon": "^3.1.9",
|
|
42
|
+
"ts-morph": "^27.0.2",
|
|
41
43
|
"ts-node": "^10.9.2",
|
|
42
44
|
"tsx": "^4.19.3",
|
|
43
45
|
"typescript": "^5.8.2",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件场景下给 LLM 的精简 API 轮廓白名单(与 declare global 中声明名一致)。
|
|
3
|
+
* 顺序即输出排序依据。
|
|
4
|
+
*/
|
|
5
|
+
export const API_PLUGIN_OUTLINE_ORDER: readonly string[] = [
|
|
6
|
+
'app',
|
|
7
|
+
'GeomLib',
|
|
8
|
+
'KApplication',
|
|
9
|
+
'KDesign',
|
|
10
|
+
'KGroupDefinition',
|
|
11
|
+
'KGroupInstance',
|
|
12
|
+
'KEntity',
|
|
13
|
+
'KVertex',
|
|
14
|
+
'KHalfEdge',
|
|
15
|
+
'KEdge',
|
|
16
|
+
'KLoop',
|
|
17
|
+
'KWire',
|
|
18
|
+
'KFace',
|
|
19
|
+
'KShell',
|
|
20
|
+
'KMesh',
|
|
21
|
+
'KGeomLib',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export const API_PLUGIN_OUTLINE_WHITELIST = new Set(API_PLUGIN_OUTLINE_ORDER);
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 基于 @qunhe/kds-api/api.d.ts(declare global)解析 interface/class 方法签名,
|
|
3
|
+
* 将参数/返回类型展开为嵌套 JSON;顶层引用去重写入 dependencies。
|
|
4
|
+
*/
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
import type { TopMaps } from './kds-api-dts.js';
|
|
7
|
+
import {
|
|
8
|
+
buildTopMaps,
|
|
9
|
+
collectMethodNamesFromIfaceOrClass,
|
|
10
|
+
extractCommentFromInterfaceMember,
|
|
11
|
+
extractCommentText,
|
|
12
|
+
getInterfaceLikeRelations,
|
|
13
|
+
resolveKdsApiDtsPath,
|
|
14
|
+
} from './kds-api-dts.js';
|
|
15
|
+
|
|
16
|
+
export type BatchMethodsItem = { interface: string; methods: string[] };
|
|
17
|
+
|
|
18
|
+
export type BatchMethodsFullResult = {
|
|
19
|
+
full: Array<{
|
|
20
|
+
interface: string;
|
|
21
|
+
extends: string[];
|
|
22
|
+
implements: string[];
|
|
23
|
+
methods: Array<{
|
|
24
|
+
name: string;
|
|
25
|
+
/** 该方法上的 JSDoc / 前导注释合并文本 */
|
|
26
|
+
comments?: string;
|
|
27
|
+
parameters: Record<string, unknown>;
|
|
28
|
+
returnType: unknown;
|
|
29
|
+
}>;
|
|
30
|
+
}>;
|
|
31
|
+
dependencies: {
|
|
32
|
+
consts: Array<{ name: string; comments?: string }>;
|
|
33
|
+
interfaces: Array<{
|
|
34
|
+
name: string;
|
|
35
|
+
comments?: string;
|
|
36
|
+
extends: string[];
|
|
37
|
+
implements: string[];
|
|
38
|
+
methods: string[];
|
|
39
|
+
}>;
|
|
40
|
+
types: Array<{ name: string; comments?: string }>;
|
|
41
|
+
enums: Array<{ name: string; comments?: string; values: string[] }>;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function mergeCommentText(...parts: (string | undefined)[]): string | undefined {
|
|
46
|
+
const pieces: string[] = [];
|
|
47
|
+
for (const p of parts) {
|
|
48
|
+
if (p?.trim()) pieces.push(p.trim());
|
|
49
|
+
}
|
|
50
|
+
return pieces.length ? pieces.join('\n\n') : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type DepState = {
|
|
54
|
+
consts: Map<string, { name: string; comments?: string }>;
|
|
55
|
+
interfaces: Map<
|
|
56
|
+
string,
|
|
57
|
+
{
|
|
58
|
+
name: string;
|
|
59
|
+
comments?: string;
|
|
60
|
+
extends: string[];
|
|
61
|
+
implements: string[];
|
|
62
|
+
methods: string[];
|
|
63
|
+
}
|
|
64
|
+
>;
|
|
65
|
+
types: Map<string, { name: string; comments?: string }>;
|
|
66
|
+
enums: Map<string, { name: string; comments?: string; values: string[] }>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const MAX_TYPE_DEPTH = 40;
|
|
70
|
+
|
|
71
|
+
type SerCtx = {
|
|
72
|
+
checker: ts.TypeChecker;
|
|
73
|
+
sourceFile: ts.SourceFile;
|
|
74
|
+
top: TopMaps;
|
|
75
|
+
deps: DepState;
|
|
76
|
+
expanding: Set<string>;
|
|
77
|
+
depth: number;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function safeTypeToString(checker: ts.TypeChecker, type: ts.Type): string {
|
|
81
|
+
try {
|
|
82
|
+
return checker.typeToString(type);
|
|
83
|
+
} catch {
|
|
84
|
+
return '<type>';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function registerConst(ctx: SerCtx, qualifiedName: string, vd: ts.VariableDeclaration): void {
|
|
89
|
+
if (ctx.deps.consts.has(qualifiedName)) return;
|
|
90
|
+
const list = vd.parent;
|
|
91
|
+
const stmt = list?.parent;
|
|
92
|
+
const stmtComments =
|
|
93
|
+
stmt && ts.isVariableStatement(stmt) ? extractCommentText(stmt, ctx.sourceFile) : undefined;
|
|
94
|
+
const row: { name: string; comments?: string } = { name: qualifiedName };
|
|
95
|
+
const c = mergeCommentText(stmtComments, extractCommentText(vd, ctx.sourceFile));
|
|
96
|
+
if (c?.trim()) row.comments = c;
|
|
97
|
+
ctx.deps.consts.set(qualifiedName, row);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function registerInterfaceLike(
|
|
101
|
+
ctx: SerCtx,
|
|
102
|
+
qualifiedName: string,
|
|
103
|
+
node: ts.InterfaceDeclaration | ts.ClassDeclaration
|
|
104
|
+
): void {
|
|
105
|
+
if (ctx.deps.interfaces.has(qualifiedName)) return;
|
|
106
|
+
const relations = getInterfaceLikeRelations(node, ctx.sourceFile);
|
|
107
|
+
const row: {
|
|
108
|
+
name: string;
|
|
109
|
+
comments?: string;
|
|
110
|
+
extends: string[];
|
|
111
|
+
implements: string[];
|
|
112
|
+
methods: string[];
|
|
113
|
+
} = {
|
|
114
|
+
name: qualifiedName,
|
|
115
|
+
extends: relations.extends,
|
|
116
|
+
implements: relations.implements,
|
|
117
|
+
methods: collectMethodNamesFromIfaceOrClass(node, ctx.sourceFile),
|
|
118
|
+
};
|
|
119
|
+
const c = extractCommentText(node, ctx.sourceFile);
|
|
120
|
+
if (c?.trim()) row.comments = c;
|
|
121
|
+
ctx.deps.interfaces.set(qualifiedName, row);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function registerTypeAlias(ctx: SerCtx, qualifiedName: string, node: ts.TypeAliasDeclaration): void {
|
|
125
|
+
if (ctx.deps.types.has(qualifiedName)) return;
|
|
126
|
+
const row: { name: string; comments?: string } = { name: qualifiedName };
|
|
127
|
+
const c = extractCommentText(node, ctx.sourceFile);
|
|
128
|
+
if (c?.trim()) row.comments = c;
|
|
129
|
+
ctx.deps.types.set(qualifiedName, row);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function registerEnum(ctx: SerCtx, qualifiedName: string, node: ts.EnumDeclaration): void {
|
|
133
|
+
if (ctx.deps.enums.has(qualifiedName)) return;
|
|
134
|
+
const values = node.members.map((m) => m.name.getText(ctx.sourceFile));
|
|
135
|
+
const row: { name: string; comments?: string; values: string[] } = {
|
|
136
|
+
name: qualifiedName,
|
|
137
|
+
values,
|
|
138
|
+
};
|
|
139
|
+
const c = extractCommentText(node, ctx.sourceFile);
|
|
140
|
+
if (c?.trim()) row.comments = c;
|
|
141
|
+
ctx.deps.enums.set(qualifiedName, row);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function findQualifiedNameForDecl(
|
|
145
|
+
decl: ts.Declaration,
|
|
146
|
+
top: TopMaps
|
|
147
|
+
): { kind: keyof TopMaps; name: string } | undefined {
|
|
148
|
+
for (const [name, node] of top.interfaces) {
|
|
149
|
+
if (node === decl) return { kind: 'interfaces', name };
|
|
150
|
+
}
|
|
151
|
+
for (const [name, node] of top.classes) {
|
|
152
|
+
if (node === decl) return { kind: 'classes', name };
|
|
153
|
+
}
|
|
154
|
+
for (const [name, node] of top.typeAliases) {
|
|
155
|
+
if (node === decl) return { kind: 'typeAliases', name };
|
|
156
|
+
}
|
|
157
|
+
for (const [name, node] of top.enums) {
|
|
158
|
+
if (node === decl) return { kind: 'enums', name };
|
|
159
|
+
}
|
|
160
|
+
for (const [name, node] of top.consts) {
|
|
161
|
+
if (node === decl) return { kind: 'consts', name };
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isPlainObjectType(type: ts.Type, checker: ts.TypeChecker): boolean {
|
|
167
|
+
return (checker.getPropertiesOfType(type) ?? []).length > 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 若为 api.d.ts 顶层 interface/class/enum/type alias/const,写入 dependencies 并返回 true(由调用方输出 safeTypeToString)。 */
|
|
171
|
+
function registerIfTopLevelDeclaration(type: ts.Type, ctx: SerCtx): boolean {
|
|
172
|
+
const { top } = ctx;
|
|
173
|
+
|
|
174
|
+
const aliasSym = type.aliasSymbol;
|
|
175
|
+
if (aliasSym) {
|
|
176
|
+
const ad = aliasSym.getDeclarations()?.find(ts.isTypeAliasDeclaration);
|
|
177
|
+
if (ad) {
|
|
178
|
+
const hit = findQualifiedNameForDecl(ad, top);
|
|
179
|
+
if (hit?.kind === 'typeAliases') {
|
|
180
|
+
const node = top.typeAliases.get(hit.name);
|
|
181
|
+
if (node) {
|
|
182
|
+
registerTypeAlias(ctx, hit.name, node);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const sym = type.getSymbol();
|
|
190
|
+
if (!sym) return false;
|
|
191
|
+
const decl = sym.getDeclarations()?.[0];
|
|
192
|
+
|
|
193
|
+
if (decl && (ts.isInterfaceDeclaration(decl) || ts.isClassDeclaration(decl))) {
|
|
194
|
+
const hit = findQualifiedNameForDecl(decl, top);
|
|
195
|
+
if (hit?.kind === 'interfaces' || hit?.kind === 'classes') {
|
|
196
|
+
const node =
|
|
197
|
+
hit.kind === 'interfaces' ? top.interfaces.get(hit.name)! : top.classes.get(hit.name)!;
|
|
198
|
+
registerInterfaceLike(ctx, hit.name, node);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (sym.flags & ts.SymbolFlags.Enum) {
|
|
204
|
+
const ed = sym.getDeclarations()?.find(ts.isEnumDeclaration);
|
|
205
|
+
if (ed) {
|
|
206
|
+
const hit = findQualifiedNameForDecl(ed, top);
|
|
207
|
+
if (hit?.kind === 'enums') {
|
|
208
|
+
const node = top.enums.get(hit.name);
|
|
209
|
+
if (node) {
|
|
210
|
+
registerEnum(ctx, hit.name, node);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (decl && ts.isVariableDeclaration(decl)) {
|
|
218
|
+
const hit = findQualifiedNameForDecl(decl, top);
|
|
219
|
+
if (hit?.kind === 'consts') {
|
|
220
|
+
const vd = top.consts.get(hit.name);
|
|
221
|
+
if (vd) {
|
|
222
|
+
registerConst(ctx, hit.name, vd);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function tryArrayElementType(type: ts.Type, checker: ts.TypeChecker): ts.Type | undefined {
|
|
232
|
+
const anyChk = checker as ts.TypeChecker & {
|
|
233
|
+
isArrayType?: (t: ts.Type) => boolean;
|
|
234
|
+
getElementTypeOfArrayType?: (t: ts.Type) => ts.Type | undefined;
|
|
235
|
+
};
|
|
236
|
+
if (typeof anyChk.isArrayType === 'function' && anyChk.isArrayType(type)) {
|
|
237
|
+
return anyChk.getElementTypeOfArrayType?.(type);
|
|
238
|
+
}
|
|
239
|
+
const ref = type as ts.TypeReference;
|
|
240
|
+
const sym = ref.symbol ?? ref.aliasSymbol;
|
|
241
|
+
const n = sym?.name;
|
|
242
|
+
if ((n === 'Array' || n === 'ReadonlyArray') && ref.typeArguments?.length === 1) {
|
|
243
|
+
return ref.typeArguments[0];
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function serializeType(type: ts.Type, ctx: SerCtx): unknown {
|
|
249
|
+
ctx.depth += 1;
|
|
250
|
+
try {
|
|
251
|
+
if (ctx.depth > MAX_TYPE_DEPTH) {
|
|
252
|
+
return '…';
|
|
253
|
+
}
|
|
254
|
+
return serializeTypeInner(type, ctx);
|
|
255
|
+
} finally {
|
|
256
|
+
ctx.depth -= 1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function serializeTypeInner(type: ts.Type, ctx: SerCtx): unknown {
|
|
261
|
+
const { checker } = ctx;
|
|
262
|
+
|
|
263
|
+
if (type.flags & ts.TypeFlags.TypeParameter) {
|
|
264
|
+
return safeTypeToString(checker, type);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (
|
|
268
|
+
type.flags &
|
|
269
|
+
(ts.TypeFlags.String |
|
|
270
|
+
ts.TypeFlags.Number |
|
|
271
|
+
ts.TypeFlags.Boolean |
|
|
272
|
+
ts.TypeFlags.Null |
|
|
273
|
+
ts.TypeFlags.Undefined |
|
|
274
|
+
ts.TypeFlags.Void |
|
|
275
|
+
ts.TypeFlags.Never |
|
|
276
|
+
ts.TypeFlags.ESSymbol |
|
|
277
|
+
ts.TypeFlags.BigInt)
|
|
278
|
+
) {
|
|
279
|
+
return safeTypeToString(checker, type);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (type.flags & ts.TypeFlags.Any || type.flags & ts.TypeFlags.Unknown) {
|
|
283
|
+
return safeTypeToString(checker, type);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (type.flags & ts.TypeFlags.Literal) {
|
|
287
|
+
return safeTypeToString(checker, type);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (type.flags & ts.TypeFlags.Intersection) {
|
|
291
|
+
const inter = type as ts.IntersectionType;
|
|
292
|
+
const parts = inter.types.map((t) => serializeType(t, ctx));
|
|
293
|
+
if (parts.every((p) => p !== null && typeof p === 'object' && !Array.isArray(p))) {
|
|
294
|
+
return Object.assign({}, ...(parts as Record<string, unknown>[]));
|
|
295
|
+
}
|
|
296
|
+
return safeTypeToString(checker, type);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (type.isUnion()) {
|
|
300
|
+
return safeTypeToString(checker, type);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (registerIfTopLevelDeclaration(type, ctx)) {
|
|
304
|
+
return safeTypeToString(checker, type);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const callSigs = type.getCallSignatures();
|
|
308
|
+
if (callSigs.length > 0 && checker.getPropertiesOfType(type).length === 0) {
|
|
309
|
+
const sig = callSigs[callSigs.length - 1];
|
|
310
|
+
return serializeMethodSignature(
|
|
311
|
+
sig,
|
|
312
|
+
{
|
|
313
|
+
checker: ctx.checker,
|
|
314
|
+
sourceFile: ctx.sourceFile,
|
|
315
|
+
top: ctx.top,
|
|
316
|
+
deps: ctx.deps,
|
|
317
|
+
depth: ctx.depth,
|
|
318
|
+
},
|
|
319
|
+
ctx.expanding
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const sym = type.getSymbol();
|
|
324
|
+
|
|
325
|
+
if (sym && sym.flags & ts.SymbolFlags.EnumMember) {
|
|
326
|
+
return safeTypeToString(checker, type);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const elem = tryArrayElementType(type, checker);
|
|
330
|
+
if (elem) {
|
|
331
|
+
if (registerIfTopLevelDeclaration(elem, ctx)) {
|
|
332
|
+
return { '[]': safeTypeToString(checker, elem) };
|
|
333
|
+
}
|
|
334
|
+
return { '[]': serializeType(elem, ctx) };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (checker.isTupleType(type)) {
|
|
338
|
+
const ref = type as ts.TypeReference;
|
|
339
|
+
const args = checker.getTypeArguments(ref);
|
|
340
|
+
return {
|
|
341
|
+
__tuple: args.map((t) =>
|
|
342
|
+
registerIfTopLevelDeclaration(t, ctx) ? safeTypeToString(checker, t) : serializeType(t, ctx)
|
|
343
|
+
),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const typeRef = type as ts.TypeReference;
|
|
348
|
+
const targetSym = typeRef.target?.symbol ?? typeRef.aliasSymbol ?? sym;
|
|
349
|
+
if (targetSym?.name === 'Promise' && checker.getTypeArguments(typeRef).length >= 1) {
|
|
350
|
+
const args = checker.getTypeArguments(typeRef);
|
|
351
|
+
const inner = args[0];
|
|
352
|
+
if (registerIfTopLevelDeclaration(inner, ctx)) {
|
|
353
|
+
return `Promise<${safeTypeToString(checker, inner)}>`;
|
|
354
|
+
}
|
|
355
|
+
const body = serializeType(inner, ctx);
|
|
356
|
+
return typeof body === 'string' ? `Promise<${body}>` : { __promise: body };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (isPlainObjectType(type, checker)) {
|
|
360
|
+
return expandObjectProperties(type, ctx);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return safeTypeToString(checker, type);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function expandObjectProperties(type: ts.Type, ctx: SerCtx): Record<string, unknown> {
|
|
367
|
+
const { checker } = ctx;
|
|
368
|
+
const out: Record<string, unknown> = {};
|
|
369
|
+
for (const prop of checker.getPropertiesOfType(type)) {
|
|
370
|
+
const n = prop.getName();
|
|
371
|
+
if (n.startsWith('__')) continue;
|
|
372
|
+
const pt = checker.getTypeOfSymbol(prop);
|
|
373
|
+
out[n] = serializeType(pt, ctx);
|
|
374
|
+
}
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function serializeMethodSignature(
|
|
379
|
+
sig: ts.Signature,
|
|
380
|
+
base: Omit<SerCtx, 'expanding'>,
|
|
381
|
+
expanding: Set<string>
|
|
382
|
+
): { parameters: Record<string, unknown>; returnType: unknown } {
|
|
383
|
+
const ctx: SerCtx = { ...base, expanding };
|
|
384
|
+
const parameters: Record<string, unknown> = {};
|
|
385
|
+
for (const p of sig.getParameters()) {
|
|
386
|
+
if (p.name === 'this') continue;
|
|
387
|
+
const t = ctx.checker.getTypeOfSymbol(p);
|
|
388
|
+
parameters[p.name] = serializeType(t, ctx);
|
|
389
|
+
}
|
|
390
|
+
const ret = ctx.checker.getReturnTypeOfSignature(sig);
|
|
391
|
+
return { parameters, returnType: serializeType(ret, ctx) };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function batchGetMethodsFull(items: BatchMethodsItem[]): BatchMethodsFullResult {
|
|
395
|
+
const apiPath = resolveKdsApiDtsPath();
|
|
396
|
+
const program = ts.createProgram([apiPath], {
|
|
397
|
+
target: ts.ScriptTarget.Latest,
|
|
398
|
+
skipLibCheck: true,
|
|
399
|
+
allowJs: false,
|
|
400
|
+
});
|
|
401
|
+
const checker = program.getTypeChecker();
|
|
402
|
+
const sf = program.getSourceFile(apiPath);
|
|
403
|
+
if (!sf) {
|
|
404
|
+
throw new Error(`无法加载源文件:${apiPath}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const top = buildTopMaps(sf);
|
|
408
|
+
|
|
409
|
+
const deps: DepState = {
|
|
410
|
+
consts: new Map(),
|
|
411
|
+
interfaces: new Map(),
|
|
412
|
+
types: new Map(),
|
|
413
|
+
enums: new Map(),
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const full: BatchMethodsFullResult['full'] = [];
|
|
417
|
+
const expandingRoot = new Set<string>();
|
|
418
|
+
const baseCtx = { checker, sourceFile: sf, top, deps, depth: 0 };
|
|
419
|
+
|
|
420
|
+
for (const item of items) {
|
|
421
|
+
const ifaceName = item.interface;
|
|
422
|
+
const ifaceNode = top.interfaces.get(ifaceName) ?? top.classes.get(ifaceName);
|
|
423
|
+
if (!ifaceNode) {
|
|
424
|
+
throw new Error(`未找到 interface/class:${ifaceName}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const ifaceType = checker.getTypeAtLocation(ifaceNode);
|
|
428
|
+
const methodsOut: BatchMethodsFullResult['full'][0]['methods'] = [];
|
|
429
|
+
const relations = getInterfaceLikeRelations(ifaceNode, sf);
|
|
430
|
+
|
|
431
|
+
for (const methodName of item.methods) {
|
|
432
|
+
const prop = ifaceType.getProperty(methodName);
|
|
433
|
+
if (!prop) {
|
|
434
|
+
throw new Error(`${ifaceName} 上不存在方法:${methodName}`);
|
|
435
|
+
}
|
|
436
|
+
const propType = checker.getTypeOfSymbol(prop);
|
|
437
|
+
const sigs = propType.getCallSignatures();
|
|
438
|
+
if (sigs.length === 0) {
|
|
439
|
+
throw new Error(`${ifaceName}.${methodName} 不是可调用的方法签名`);
|
|
440
|
+
}
|
|
441
|
+
const sig = sigs[sigs.length - 1];
|
|
442
|
+
const { parameters, returnType } = serializeMethodSignature(sig, baseCtx, expandingRoot);
|
|
443
|
+
const sigDecl = sig.getDeclaration();
|
|
444
|
+
let methodComments: string | undefined =
|
|
445
|
+
sigDecl !== undefined ? extractCommentText(sigDecl, sf) : undefined;
|
|
446
|
+
if (!methodComments?.trim()) {
|
|
447
|
+
methodComments = extractCommentFromInterfaceMember(ifaceNode, methodName, sf);
|
|
448
|
+
}
|
|
449
|
+
const methodEntry: BatchMethodsFullResult['full'][0]['methods'][0] = {
|
|
450
|
+
name: methodName,
|
|
451
|
+
parameters,
|
|
452
|
+
returnType,
|
|
453
|
+
};
|
|
454
|
+
if (methodComments?.trim()) {
|
|
455
|
+
methodEntry.comments = methodComments.trim();
|
|
456
|
+
}
|
|
457
|
+
methodsOut.push(methodEntry);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
full.push({
|
|
461
|
+
interface: ifaceName,
|
|
462
|
+
extends: relations.extends,
|
|
463
|
+
implements: relations.implements,
|
|
464
|
+
methods: methodsOut,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const sortByName = <T extends { name: string }>(arr: T[]): T[] =>
|
|
469
|
+
[...arr].sort((a, b) => a.name.localeCompare(b.name));
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
full,
|
|
473
|
+
dependencies: {
|
|
474
|
+
consts: sortByName([...deps.consts.values()]),
|
|
475
|
+
interfaces: sortByName([...deps.interfaces.values()]),
|
|
476
|
+
types: sortByName([...deps.types.values()]),
|
|
477
|
+
enums: sortByName([...deps.enums.values()]),
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 在 @qunhe/kds-api/api.d.ts 全局 interface/class 上按**方法名字符串**子串检索,
|
|
3
|
+
* 方法名枚举与 batchGetMethodsFull 一致(含继承合并类型上的可调用成员),命中后调用 batchGetMethodsFull 展开。
|
|
4
|
+
*/
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
import type { BatchMethodsFullResult, BatchMethodsItem } from './batch-methods-full.js';
|
|
7
|
+
import { batchGetMethodsFull } from './batch-methods-full.js';
|
|
8
|
+
import { buildTopMaps, resolveKdsApiDtsPath } from './kds-api-dts.js';
|
|
9
|
+
|
|
10
|
+
export type BatchSearchMethodsOptions = {
|
|
11
|
+
/** 默认 true:英文大小写不敏感 */
|
|
12
|
+
caseInsensitive?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type BatchSearchMethodsResult = BatchMethodsFullResult & {
|
|
16
|
+
keywords: string[];
|
|
17
|
+
/** 未匹配到任何方法名的关键词(子串匹配) */
|
|
18
|
+
unmatchedKeywords: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** 与 batchGetMethodsFull 中 ifaceType.getProperty(name) 所见方法名集合对齐(含继承,排除无调用签名的属性) */
|
|
22
|
+
function listCallableMethodNames(
|
|
23
|
+
checker: ts.TypeChecker,
|
|
24
|
+
ifaceNode: ts.InterfaceDeclaration | ts.ClassDeclaration
|
|
25
|
+
): string[] {
|
|
26
|
+
const ifaceType = checker.getTypeAtLocation(ifaceNode);
|
|
27
|
+
const names: string[] = [];
|
|
28
|
+
for (const prop of checker.getPropertiesOfType(ifaceType)) {
|
|
29
|
+
const methodName = prop.getName();
|
|
30
|
+
if (methodName.startsWith('__')) continue;
|
|
31
|
+
const propType = checker.getTypeOfSymbol(prop);
|
|
32
|
+
if (propType.getCallSignatures().length === 0) continue;
|
|
33
|
+
names.push(methodName);
|
|
34
|
+
}
|
|
35
|
+
return names;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeQueryKeywords(keywords: string[]): string[] {
|
|
39
|
+
const seen = new Set<string>();
|
|
40
|
+
const out: string[] = [];
|
|
41
|
+
for (const raw of keywords) {
|
|
42
|
+
const t = raw.trim();
|
|
43
|
+
if (!t || seen.has(t)) continue;
|
|
44
|
+
seen.add(t);
|
|
45
|
+
out.push(t);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function batchSearchMethodsByKeywords(
|
|
51
|
+
keywords: string[],
|
|
52
|
+
options: BatchSearchMethodsOptions = {}
|
|
53
|
+
): BatchSearchMethodsResult {
|
|
54
|
+
const queryKeywords = normalizeQueryKeywords(keywords);
|
|
55
|
+
const caseInsensitive = options.caseInsensitive !== false;
|
|
56
|
+
const apiPath = resolveKdsApiDtsPath();
|
|
57
|
+
const program = ts.createProgram([apiPath], {
|
|
58
|
+
target: ts.ScriptTarget.Latest,
|
|
59
|
+
skipLibCheck: true,
|
|
60
|
+
allowJs: false,
|
|
61
|
+
});
|
|
62
|
+
const checker = program.getTypeChecker();
|
|
63
|
+
const maybeSf = program.getSourceFile(apiPath);
|
|
64
|
+
if (maybeSf === undefined) {
|
|
65
|
+
throw new Error(`无法加载源文件:${apiPath}`);
|
|
66
|
+
}
|
|
67
|
+
const sourceFile: ts.SourceFile = maybeSf;
|
|
68
|
+
|
|
69
|
+
const top = buildTopMaps(sourceFile);
|
|
70
|
+
const ifaceEntries: Array<[string, ts.InterfaceDeclaration | ts.ClassDeclaration]> = [
|
|
71
|
+
...top.interfaces.entries(),
|
|
72
|
+
...top.classes.entries(),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const methodNamesByIface = new Map<string, string[]>();
|
|
76
|
+
for (const [ifaceName, ifaceNode] of ifaceEntries) {
|
|
77
|
+
methodNamesByIface.set(ifaceName, listCallableMethodNames(checker, ifaceNode));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** interface -> 命中方法名(去重) */
|
|
81
|
+
const acc = new Map<string, Set<string>>();
|
|
82
|
+
|
|
83
|
+
function addMethod(ifaceName: string, methodName: string): void {
|
|
84
|
+
let s = acc.get(ifaceName);
|
|
85
|
+
if (!s) {
|
|
86
|
+
s = new Set();
|
|
87
|
+
acc.set(ifaceName, s);
|
|
88
|
+
}
|
|
89
|
+
s.add(methodName);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const unmatchedKeywords: string[] = [];
|
|
93
|
+
|
|
94
|
+
for (const kw of queryKeywords) {
|
|
95
|
+
const needle = caseInsensitive ? kw.toLowerCase() : kw;
|
|
96
|
+
let hit = false;
|
|
97
|
+
for (const [ifaceName, names] of methodNamesByIface) {
|
|
98
|
+
for (const methodName of names) {
|
|
99
|
+
const hay = caseInsensitive ? methodName.toLowerCase() : methodName;
|
|
100
|
+
if (!hay.includes(needle)) continue;
|
|
101
|
+
hit = true;
|
|
102
|
+
addMethod(ifaceName, methodName);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!hit) {
|
|
106
|
+
unmatchedKeywords.push(kw);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const items: BatchMethodsItem[] = [...acc.entries()]
|
|
111
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
112
|
+
.map(([iface, names]) => ({
|
|
113
|
+
interface: iface,
|
|
114
|
+
methods: [...names].sort((a, b) => a.localeCompare(b)),
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
const expanded = batchGetMethodsFull(items);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
...expanded,
|
|
121
|
+
keywords: queryKeywords,
|
|
122
|
+
unmatchedKeywords,
|
|
123
|
+
};
|
|
124
|
+
}
|