@mindbase/express-common 1.0.7 → 1.0.9
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/commands/prepare.ts +62 -0
- package/core/app.ts +186 -0
- package/core/module/CreateModule.ts +38 -0
- package/core/module/FindPackageRoot.ts +58 -0
- package/core/module/GetModulePath.ts +58 -0
- package/core/state.ts +67 -0
- package/feature/cron/CronManager.ts +63 -0
- package/feature/scanner/FileScanner.ts +289 -0
- package/index.ts +32 -0
- package/middleware/Cors.ts +17 -0
- package/middleware/IpParser.ts +81 -0
- package/middleware/UaParser.ts +50 -0
- package/package.json +16 -12
- package/routes/Doc.route.ts +123 -0
- package/tsconfig.json +8 -0
- package/types/DocTypes.ts +111 -0
- package/types/express.d.ts +12 -0
- package/types/index.ts +19 -0
- package/utils/AppError.ts +21 -0
- package/utils/ComponentRegistry.ts +34 -0
- package/utils/DatabaseMigration.ts +121 -0
- package/utils/Dayjs.ts +16 -0
- package/utils/DocManager.ts +279 -0
- package/utils/HttpServer.ts +41 -0
- package/utils/InitDatabase.ts +133 -0
- package/utils/InitErrorHandler.ts +82 -0
- package/utils/InitExpress.ts +38 -0
- package/utils/Logger.ts +206 -0
- package/utils/MiddlewareRegistry.ts +14 -0
- package/utils/RouteParser.ts +655 -0
- package/utils/RouteRegistry.ts +92 -0
- package/utils/TSTypeParser.ts +456 -0
- package/utils/Validate.ts +25 -0
- package/utils/ZodSchemaParser.ts +420 -0
- package/zod/Doc.schema.ts +9 -0
- package/dist/index.d.mts +0 -360
- package/dist/index.mjs +0 -2449
- package/dist/index.mjs.map +0 -1
package/dist/index.mjs
DELETED
|
@@ -1,2449 +0,0 @@
|
|
|
1
|
-
// core/state.ts
|
|
2
|
-
import express from "express";
|
|
3
|
-
function createState(options = {}) {
|
|
4
|
-
return {
|
|
5
|
-
options: {
|
|
6
|
-
host: options.host || "127.0.0.1",
|
|
7
|
-
port: options.port || 3e3,
|
|
8
|
-
logging: options.logging !== false,
|
|
9
|
-
staticPath: options.staticPath,
|
|
10
|
-
userAgent: options.userAgent !== false,
|
|
11
|
-
ip: options.ip !== false,
|
|
12
|
-
cors: options.cors !== false,
|
|
13
|
-
apiPrefix: options.apiPrefix,
|
|
14
|
-
logLevel: options.logLevel || "info",
|
|
15
|
-
authWhitelist: [],
|
|
16
|
-
database: {
|
|
17
|
-
path: options.database?.path || "./data/app.db"
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
express: express(),
|
|
21
|
-
schemas: {}
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// feature/scanner/FileScanner.ts
|
|
26
|
-
import { glob } from "glob";
|
|
27
|
-
import path from "path";
|
|
28
|
-
import fs from "fs";
|
|
29
|
-
import { pathToFileURL } from "url";
|
|
30
|
-
|
|
31
|
-
// utils/Dayjs.ts
|
|
32
|
-
import dayjs from "dayjs";
|
|
33
|
-
import "dayjs/locale/zh-cn";
|
|
34
|
-
import duration from "dayjs/plugin/duration";
|
|
35
|
-
import relativeTime from "dayjs/plugin/relativeTime";
|
|
36
|
-
import utc from "dayjs/plugin/utc";
|
|
37
|
-
dayjs.locale("zh-cn");
|
|
38
|
-
dayjs.extend(utc);
|
|
39
|
-
dayjs.extend(duration);
|
|
40
|
-
dayjs.extend(relativeTime);
|
|
41
|
-
var Dayjs_default = dayjs;
|
|
42
|
-
|
|
43
|
-
// utils/Logger.ts
|
|
44
|
-
var LOG_LEVELS = {
|
|
45
|
-
debug: 0,
|
|
46
|
-
info: 1,
|
|
47
|
-
warn: 2,
|
|
48
|
-
error: 3,
|
|
49
|
-
silent: 4
|
|
50
|
-
};
|
|
51
|
-
var currentLogLevel = "info";
|
|
52
|
-
var COLORS = {
|
|
53
|
-
debug: "\x1B[36m",
|
|
54
|
-
info: "\x1B[32m",
|
|
55
|
-
warn: "\x1B[33m",
|
|
56
|
-
error: "\x1B[31m",
|
|
57
|
-
reset: "\x1B[0m"
|
|
58
|
-
};
|
|
59
|
-
function getVisualWidth(str) {
|
|
60
|
-
let width = 0;
|
|
61
|
-
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
62
|
-
for (const { segment } of segmenter.segment(str)) {
|
|
63
|
-
const charCode = segment.charCodeAt(0);
|
|
64
|
-
if (charCode <= 255) {
|
|
65
|
-
width += 1;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const isCJK = charCode >= 19968 && charCode <= 40959 || // CJK Unified Ideographs
|
|
69
|
-
charCode >= 12288 && charCode <= 12351 || // CJK Symbols and Punctuation
|
|
70
|
-
charCode >= 65280 && charCode <= 65519;
|
|
71
|
-
const isMultiByte = segment.length > 1;
|
|
72
|
-
if (isCJK || isMultiByte) {
|
|
73
|
-
width += 2;
|
|
74
|
-
} else {
|
|
75
|
-
width += 1;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return width;
|
|
79
|
-
}
|
|
80
|
-
function truncateToWidth(str, maxWidth) {
|
|
81
|
-
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
82
|
-
const segments = Array.from(segmenter.segment(str)).map((s) => s.segment);
|
|
83
|
-
if (getVisualWidth(str) <= maxWidth) return str;
|
|
84
|
-
let currentWidth = 3;
|
|
85
|
-
let result = "";
|
|
86
|
-
for (let i = segments.length - 1; i >= 0; i--) {
|
|
87
|
-
const segment = segments[i];
|
|
88
|
-
const segmentWidth = getVisualWidth(segment);
|
|
89
|
-
if (currentWidth + segmentWidth > maxWidth) break;
|
|
90
|
-
result = segment + result;
|
|
91
|
-
currentWidth += segmentWidth;
|
|
92
|
-
}
|
|
93
|
-
return "..." + result;
|
|
94
|
-
}
|
|
95
|
-
function formatMessage(level, message) {
|
|
96
|
-
const timestamp = Dayjs_default().format("YYYY-MM-DD HH:mm:ss");
|
|
97
|
-
const terminalWidth = process.stdout.columns || 80;
|
|
98
|
-
const timeStr = ` ${timestamp}`;
|
|
99
|
-
const timeWidth = getVisualWidth(timeStr);
|
|
100
|
-
const maxMessageWidth = terminalWidth - timeWidth - 2;
|
|
101
|
-
let displayMessage = message;
|
|
102
|
-
const messageWidth = getVisualWidth(displayMessage);
|
|
103
|
-
if (messageWidth > maxMessageWidth) {
|
|
104
|
-
displayMessage = truncateToWidth(displayMessage, maxMessageWidth);
|
|
105
|
-
}
|
|
106
|
-
const currentMsgWidth = getVisualWidth(displayMessage);
|
|
107
|
-
const paddingCount = Math.max(0, terminalWidth - currentMsgWidth - timeWidth);
|
|
108
|
-
const padding = " ".repeat(paddingCount);
|
|
109
|
-
return `${COLORS[level]}${displayMessage}${padding}${timeStr}${COLORS.reset}`;
|
|
110
|
-
}
|
|
111
|
-
var startupTime = Date.now();
|
|
112
|
-
function padTag(tag, targetWidth = 8) {
|
|
113
|
-
const currentWidth = getVisualWidth(tag);
|
|
114
|
-
const padding = Math.max(0, targetWidth - currentWidth);
|
|
115
|
-
if (padding === 0) return tag;
|
|
116
|
-
const chars = [...tag];
|
|
117
|
-
const gapCount = chars.length + 1;
|
|
118
|
-
const baseSpaces = Math.floor(padding / gapCount);
|
|
119
|
-
const extraSpaces = padding % gapCount;
|
|
120
|
-
let result = "";
|
|
121
|
-
for (let i = 0; i < chars.length; i++) {
|
|
122
|
-
const spaces = baseSpaces + (i < extraSpaces ? 1 : 0);
|
|
123
|
-
result += " ".repeat(spaces) + chars[i];
|
|
124
|
-
}
|
|
125
|
-
result += " ".repeat(baseSpaces + (chars.length < extraSpaces ? 1 : 0));
|
|
126
|
-
return result;
|
|
127
|
-
}
|
|
128
|
-
function startupMessage(message, tag) {
|
|
129
|
-
const diff = Date.now() - startupTime;
|
|
130
|
-
const timeStr = diff.toString().padStart(6, "0");
|
|
131
|
-
const tagStr = tag ? `\u3010${padTag(tag)}\u3011` : "";
|
|
132
|
-
return `${COLORS["warn"]}[${timeStr}] ${tagStr}${message}${COLORS["reset"]}`;
|
|
133
|
-
}
|
|
134
|
-
function shouldLog(level) {
|
|
135
|
-
return LOG_LEVELS[level] >= LOG_LEVELS[currentLogLevel];
|
|
136
|
-
}
|
|
137
|
-
function formatArgs(args) {
|
|
138
|
-
return args.map((arg) => {
|
|
139
|
-
if (arg instanceof Error) {
|
|
140
|
-
return arg.message;
|
|
141
|
-
}
|
|
142
|
-
if (typeof arg === "object") {
|
|
143
|
-
return JSON.stringify(arg, null, 2);
|
|
144
|
-
}
|
|
145
|
-
return String(arg);
|
|
146
|
-
}).join(" ");
|
|
147
|
-
}
|
|
148
|
-
var logger = {
|
|
149
|
-
debug(...args) {
|
|
150
|
-
if (shouldLog("debug")) {
|
|
151
|
-
console.log(formatMessage("debug", formatArgs(args)));
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
info(...args) {
|
|
155
|
-
if (shouldLog("info")) {
|
|
156
|
-
console.log(formatMessage("info", formatArgs(args)));
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
warn(...args) {
|
|
160
|
-
if (shouldLog("warn")) {
|
|
161
|
-
console.warn(formatMessage("warn", formatArgs(args)));
|
|
162
|
-
}
|
|
163
|
-
},
|
|
164
|
-
error(...args) {
|
|
165
|
-
if (shouldLog("error")) {
|
|
166
|
-
console.error(formatMessage("error", formatArgs(args)));
|
|
167
|
-
const errorArg = args.find((arg) => arg instanceof Error);
|
|
168
|
-
if (errorArg) {
|
|
169
|
-
console.error(errorArg);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
},
|
|
173
|
-
startup(tagOrMessage, ...args) {
|
|
174
|
-
if (args.length > 0) {
|
|
175
|
-
console.log(startupMessage(formatArgs(args), tagOrMessage));
|
|
176
|
-
} else {
|
|
177
|
-
console.log(startupMessage(tagOrMessage));
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
setLevel(level) {
|
|
181
|
-
currentLogLevel = level;
|
|
182
|
-
},
|
|
183
|
-
getLevel() {
|
|
184
|
-
return currentLogLevel;
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
var Logger_default = logger;
|
|
188
|
-
|
|
189
|
-
// feature/scanner/FileScanner.ts
|
|
190
|
-
var CACHE_VERSION = "2.0";
|
|
191
|
-
var CACHE_DIR = path.join(process.cwd(), "node_modules/.cache/mindbase");
|
|
192
|
-
var CACHE_FILE = path.join(CACHE_DIR, "startup-cache.json");
|
|
193
|
-
var cacheData = null;
|
|
194
|
-
var state = {
|
|
195
|
-
scanPaths: [],
|
|
196
|
-
baseDir: ""
|
|
197
|
-
};
|
|
198
|
-
function setBaseDir(baseDir) {
|
|
199
|
-
state.baseDir = baseDir;
|
|
200
|
-
}
|
|
201
|
-
function addScanPath(dirPath) {
|
|
202
|
-
if (!path.isAbsolute(dirPath)) {
|
|
203
|
-
throw new Error(`\u8DEF\u5F84\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84: ${dirPath}`);
|
|
204
|
-
}
|
|
205
|
-
if (!fs.existsSync(dirPath)) {
|
|
206
|
-
throw new Error(`\u76EE\u5F55\u4E0D\u5B58\u5728: ${dirPath}`);
|
|
207
|
-
}
|
|
208
|
-
if (!fs.statSync(dirPath).isDirectory()) {
|
|
209
|
-
throw new Error(`\u8DEF\u5F84\u4E0D\u662F\u76EE\u5F55: ${dirPath}`);
|
|
210
|
-
}
|
|
211
|
-
if (state.scanPaths.includes(dirPath)) {
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (state.baseDir && dirPath.startsWith(state.baseDir)) {
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
for (const existingPath of state.scanPaths) {
|
|
218
|
-
if (dirPath.startsWith(existingPath)) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
state.scanPaths.push(dirPath);
|
|
223
|
-
}
|
|
224
|
-
function isValidDirectory(dirPath) {
|
|
225
|
-
try {
|
|
226
|
-
return fs.statSync(dirPath).isDirectory();
|
|
227
|
-
} catch {
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
function extractFileName(filePath) {
|
|
232
|
-
const fileName = path.basename(filePath);
|
|
233
|
-
return fileName.replace(/\.(route|middleware|schema)\.ts$/, "");
|
|
234
|
-
}
|
|
235
|
-
async function loadCache() {
|
|
236
|
-
try {
|
|
237
|
-
if (!fs.existsSync(CACHE_FILE)) {
|
|
238
|
-
cacheData = { version: CACHE_VERSION, entries: /* @__PURE__ */ new Map() };
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
const json = fs.readFileSync(CACHE_FILE, "utf-8");
|
|
242
|
-
const data = JSON.parse(json);
|
|
243
|
-
if (data.version !== CACHE_VERSION) {
|
|
244
|
-
Logger_default.debug(`\u7F13\u5B58\u7248\u672C\u4E0D\u5339\u914D (${data.version} vs ${CACHE_VERSION})\uFF0C\u5C06\u91CD\u65B0\u6784\u5EFA`);
|
|
245
|
-
cacheData = { version: CACHE_VERSION, entries: /* @__PURE__ */ new Map() };
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
cacheData = {
|
|
249
|
-
version: data.version,
|
|
250
|
-
entries: new Map(Object.entries(data.entries).map(([k, v]) => [k, v]))
|
|
251
|
-
};
|
|
252
|
-
Logger_default.debug(`\u5DF2\u52A0\u8F7D\u7F13\u5B58\uFF0C\u5305\u542B ${cacheData.entries.size} \u4E2A\u6587\u4EF6\u6761\u76EE`);
|
|
253
|
-
} catch (error) {
|
|
254
|
-
Logger_default.warn(`\u52A0\u8F7D\u7F13\u5B58\u5931\u8D25\uFF0C\u5C06\u91CD\u65B0\u6784\u5EFA: ${error}`);
|
|
255
|
-
cacheData = { version: CACHE_VERSION, entries: /* @__PURE__ */ new Map() };
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
async function saveCache() {
|
|
259
|
-
try {
|
|
260
|
-
if (!cacheData) return;
|
|
261
|
-
if (!fs.existsSync(CACHE_DIR)) {
|
|
262
|
-
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
263
|
-
}
|
|
264
|
-
const data = {
|
|
265
|
-
version: cacheData.version,
|
|
266
|
-
entries: Object.fromEntries(cacheData.entries)
|
|
267
|
-
};
|
|
268
|
-
fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
269
|
-
Logger_default.debug(`\u7F13\u5B58\u5DF2\u4FDD\u5B58\uFF0C\u5305\u542B ${cacheData.entries.size} \u4E2A\u6587\u4EF6\u6761\u76EE`);
|
|
270
|
-
} catch (error) {
|
|
271
|
-
Logger_default.warn(`\u4FDD\u5B58\u7F13\u5B58\u5931\u8D25: ${error}`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
async function scan(onlyPaths = false) {
|
|
275
|
-
await loadCache();
|
|
276
|
-
const results = [];
|
|
277
|
-
const allScanPaths = [...state.scanPaths];
|
|
278
|
-
const processedFiles = /* @__PURE__ */ new Set();
|
|
279
|
-
if (state.baseDir) {
|
|
280
|
-
allScanPaths.push(state.baseDir);
|
|
281
|
-
}
|
|
282
|
-
for (const dirPath of allScanPaths) {
|
|
283
|
-
if (!isValidDirectory(dirPath)) {
|
|
284
|
-
Logger_default.warn(`\u8DF3\u8FC7\u65E0\u6548\u76EE\u5F55: ${dirPath}`);
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
const patterns = [
|
|
288
|
-
{ type: "middleware", pattern: "**/*.middleware.ts" },
|
|
289
|
-
{ type: "route", pattern: "**/*.route.ts" },
|
|
290
|
-
{ type: "schema", pattern: "**/*.schema.ts" }
|
|
291
|
-
];
|
|
292
|
-
for (const { type, pattern } of patterns) {
|
|
293
|
-
const globPattern = path.join(dirPath, pattern).replace(/\\/g, "/");
|
|
294
|
-
try {
|
|
295
|
-
const files = await glob(globPattern, { absolute: true });
|
|
296
|
-
for (const file of files) {
|
|
297
|
-
if (processedFiles.has(file)) {
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
processedFiles.add(file);
|
|
301
|
-
try {
|
|
302
|
-
const fileName = extractFileName(file);
|
|
303
|
-
if (onlyPaths) {
|
|
304
|
-
results.push({
|
|
305
|
-
fileName,
|
|
306
|
-
type,
|
|
307
|
-
filePath: file,
|
|
308
|
-
defaultExport: null,
|
|
309
|
-
allExports: null
|
|
310
|
-
});
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
const stats = await fs.promises.stat(file);
|
|
314
|
-
const mtime = stats.mtimeMs;
|
|
315
|
-
const cached = cacheData?.entries.get(file);
|
|
316
|
-
const cacheHit = cached && cached.mtime === mtime;
|
|
317
|
-
if (cacheHit) {
|
|
318
|
-
Logger_default.debug(`\u7F13\u5B58\u547D\u4E2D: ${file}`);
|
|
319
|
-
}
|
|
320
|
-
const module = await import(pathToFileURL(file).href);
|
|
321
|
-
const scanResult = {
|
|
322
|
-
fileName,
|
|
323
|
-
defaultExport: module.default,
|
|
324
|
-
allExports: module,
|
|
325
|
-
type,
|
|
326
|
-
filePath: file,
|
|
327
|
-
cacheHit
|
|
328
|
-
};
|
|
329
|
-
results.push(scanResult);
|
|
330
|
-
if (cacheData) {
|
|
331
|
-
cacheData.entries.set(file, { filePath: file, mtime });
|
|
332
|
-
}
|
|
333
|
-
} catch (error) {
|
|
334
|
-
Logger_default.warn(`\u5904\u7406\u6587\u4EF6\u5931\u8D25: ${file}`, error);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
} catch (error) {
|
|
338
|
-
Logger_default.warn(`\u626B\u63CF\u76EE\u5F55\u5931\u8D25: ${dirPath} (Pattern: ${pattern})`, error);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
await saveCache();
|
|
343
|
-
return results;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// utils/InitExpress.ts
|
|
347
|
-
import express2 from "express";
|
|
348
|
-
import fs2 from "fs";
|
|
349
|
-
import cookieParser from "cookie-parser";
|
|
350
|
-
|
|
351
|
-
// middleware/Cors.ts
|
|
352
|
-
function Cors_default(req, res, next) {
|
|
353
|
-
res.header("Access-Control-Allow-Origin", "*");
|
|
354
|
-
res.header("Access-Control-Allow-Credentials", "true");
|
|
355
|
-
res.header("Access-Control-Allow-Methods", "POST,GET,OPTIONS");
|
|
356
|
-
res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
|
|
357
|
-
if (req.method == "OPTIONS") {
|
|
358
|
-
res.sendStatus(200);
|
|
359
|
-
} else {
|
|
360
|
-
next();
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// middleware/IpParser.ts
|
|
365
|
-
import ipdb from "ipip-ipdb";
|
|
366
|
-
import { join } from "path";
|
|
367
|
-
var dbPath = join(process.cwd(), "ipipfree.ipdb");
|
|
368
|
-
var cityInstance;
|
|
369
|
-
try {
|
|
370
|
-
cityInstance = new ipdb.City(dbPath);
|
|
371
|
-
} catch (error) {
|
|
372
|
-
Logger_default.error(`[IpParser] Failed to load IP database at: ${dbPath}`);
|
|
373
|
-
}
|
|
374
|
-
function IpParser_default(req, res, next) {
|
|
375
|
-
const ip = req.headers["real-ip"] || req.headers["x-real-ip"] || req.ip;
|
|
376
|
-
const info = {
|
|
377
|
-
address: ip,
|
|
378
|
-
location: "\u672A\u77E5",
|
|
379
|
-
country: "",
|
|
380
|
-
province: "",
|
|
381
|
-
city: "",
|
|
382
|
-
isp: "",
|
|
383
|
-
latitude: "",
|
|
384
|
-
longitude: "",
|
|
385
|
-
isLocal: false
|
|
386
|
-
};
|
|
387
|
-
const localIdentifiers = ["127.0.0.1", "::1", "::ffff:127.0.0.1", "localhost"];
|
|
388
|
-
if (localIdentifiers.includes(ip)) {
|
|
389
|
-
info.location = "\u672C\u673A";
|
|
390
|
-
info.isLocal = true;
|
|
391
|
-
res.locals.ip = info;
|
|
392
|
-
return next();
|
|
393
|
-
}
|
|
394
|
-
if (cityInstance) {
|
|
395
|
-
try {
|
|
396
|
-
const data = cityInstance.findMap(ip, "CN");
|
|
397
|
-
if (data) {
|
|
398
|
-
info.country = data.country_name || "";
|
|
399
|
-
info.province = data.region_name || "";
|
|
400
|
-
info.city = data.city_name || "";
|
|
401
|
-
info.isp = data.isp_domain || "";
|
|
402
|
-
info.latitude = data.latitude || "";
|
|
403
|
-
info.longitude = data.longitude || "";
|
|
404
|
-
const parts = [info.country, info.province, info.city].filter(Boolean);
|
|
405
|
-
info.location = parts.join(" ") || "\u672A\u77E5\u4F4D\u7F6E";
|
|
406
|
-
}
|
|
407
|
-
} catch (err) {
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
res.locals.ip = info;
|
|
411
|
-
next();
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// middleware/UaParser.ts
|
|
415
|
-
import { UAParser } from "ua-parser-js";
|
|
416
|
-
function UaParser_default(req, res, next) {
|
|
417
|
-
const parser = new UAParser(req.headers["user-agent"]);
|
|
418
|
-
const result = parser.getResult();
|
|
419
|
-
const ua = {
|
|
420
|
-
ua: result.ua,
|
|
421
|
-
browser: result.browser,
|
|
422
|
-
engine: result.engine,
|
|
423
|
-
os: result.os,
|
|
424
|
-
device: result.device,
|
|
425
|
-
cpu: result.cpu,
|
|
426
|
-
isMobile: result.device.type === "mobile"
|
|
427
|
-
};
|
|
428
|
-
res.locals.UA = ua;
|
|
429
|
-
next();
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// utils/InitExpress.ts
|
|
433
|
-
function initExpress(mindBaseState) {
|
|
434
|
-
const app = mindBaseState.express;
|
|
435
|
-
const options = mindBaseState.options;
|
|
436
|
-
app.getDB = () => mindBaseState.db;
|
|
437
|
-
app.configs = mindBaseState.options;
|
|
438
|
-
app.disable("x-powered-by");
|
|
439
|
-
app.use(express2.urlencoded({ extended: false, limit: "10mb" }));
|
|
440
|
-
app.use(express2.json({ limit: "10mb" }));
|
|
441
|
-
app.use(cookieParser());
|
|
442
|
-
if (options.staticPath) {
|
|
443
|
-
if (!fs2.existsSync(options.staticPath)) {
|
|
444
|
-
fs2.mkdirSync(options.staticPath, { recursive: true });
|
|
445
|
-
}
|
|
446
|
-
app.use(express2.static(options.staticPath));
|
|
447
|
-
Logger_default.startup("\u8BBE\u7F6E", `\u9759\u6001\u76EE\u5F55: ${options.staticPath}`);
|
|
448
|
-
}
|
|
449
|
-
if (options.cors) {
|
|
450
|
-
app.use(Cors_default);
|
|
451
|
-
Logger_default.startup("\u8BBE\u7F6E", "\u8DE8\u57DF\u8BF7\u6C42\u5934");
|
|
452
|
-
}
|
|
453
|
-
if (options.ip) {
|
|
454
|
-
Logger_default.startup("\u8BBE\u7F6E", "IP\u5730\u5740\u89E3\u6790");
|
|
455
|
-
app.use(IpParser_default);
|
|
456
|
-
}
|
|
457
|
-
if (options.userAgent) {
|
|
458
|
-
Logger_default.startup("\u8BBE\u7F6E", "\u7528\u6237\u4EE3\u7406\u89E3\u6790");
|
|
459
|
-
app.use(UaParser_default);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// utils/InitDatabase.ts
|
|
464
|
-
import path2 from "path";
|
|
465
|
-
import fs3 from "fs";
|
|
466
|
-
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
467
|
-
import Database from "better-sqlite3";
|
|
468
|
-
function setupDatabase(state2, scannedResults) {
|
|
469
|
-
try {
|
|
470
|
-
if (!state2) {
|
|
471
|
-
throw new Error("\u5E94\u7528\u72B6\u6001\u5BF9\u8C61\u4E0D\u80FD\u4E3A\u7A7A");
|
|
472
|
-
}
|
|
473
|
-
if (!Array.isArray(scannedResults)) {
|
|
474
|
-
throw new Error("\u626B\u63CF\u7ED3\u679C\u5FC5\u987B\u662F\u4E00\u4E2A\u6570\u7EC4");
|
|
475
|
-
}
|
|
476
|
-
const schemas = {};
|
|
477
|
-
for (const item of scannedResults) {
|
|
478
|
-
if (item.type === "schema" && item.allExports) {
|
|
479
|
-
try {
|
|
480
|
-
Object.assign(schemas, item.allExports);
|
|
481
|
-
} catch (e) {
|
|
482
|
-
Logger_default.warn(`\u5904\u7406 Schema \u6587\u4EF6\u5931\u8D25: ${item.filePath}`, e);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
state2.schemas = schemas;
|
|
487
|
-
Logger_default.startup("\u626B\u63CF", `\u63D0\u53D6\u5230 ${Object.keys(schemas).length} \u4E2A Schema \u5B9A\u4E49`);
|
|
488
|
-
return initDatabase({
|
|
489
|
-
schemas,
|
|
490
|
-
state: state2
|
|
491
|
-
});
|
|
492
|
-
} catch (error) {
|
|
493
|
-
Logger_default.error("\u6570\u636E\u5E93\u8BBE\u7F6E\u5931\u8D25\uFF1A", error);
|
|
494
|
-
throw new Error(`\u6570\u636E\u5E93\u8BBE\u7F6E\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
function initDatabase(options) {
|
|
498
|
-
if (!options) {
|
|
499
|
-
throw new Error("\u521D\u59CB\u5316\u9009\u9879\u4E0D\u80FD\u4E3A\u7A7A");
|
|
500
|
-
}
|
|
501
|
-
try {
|
|
502
|
-
const currentDir = process.cwd();
|
|
503
|
-
const dbPath2 = options.dbPath || options.state?.options?.database?.path || "./data/app.db";
|
|
504
|
-
const absoluteDbPath = path2.resolve(currentDir, dbPath2);
|
|
505
|
-
const dbDir = path2.dirname(absoluteDbPath);
|
|
506
|
-
Logger_default.startup("\u6570\u636E\u5E93", `\u521D\u59CB\u5316\uFF1A\u8DEF\u5F84=${absoluteDbPath}`);
|
|
507
|
-
try {
|
|
508
|
-
if (!fs3.existsSync(dbDir)) {
|
|
509
|
-
Logger_default.startup("\u6570\u636E\u5E93", `\u521B\u5EFA\u76EE\u5F55\uFF1A${dbDir}`);
|
|
510
|
-
fs3.mkdirSync(dbDir, { recursive: true });
|
|
511
|
-
}
|
|
512
|
-
} catch (e) {
|
|
513
|
-
throw new Error(`\u521B\u5EFA\u6570\u636E\u5E93\u76EE\u5F55\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
514
|
-
}
|
|
515
|
-
let sqlite;
|
|
516
|
-
try {
|
|
517
|
-
sqlite = new Database(absoluteDbPath);
|
|
518
|
-
} catch (e) {
|
|
519
|
-
throw new Error(`\u8FDE\u63A5\u6570\u636E\u5E93\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
520
|
-
}
|
|
521
|
-
try {
|
|
522
|
-
sqlite.pragma("journal_mode = WAL");
|
|
523
|
-
sqlite.pragma("synchronous = NORMAL");
|
|
524
|
-
sqlite.pragma("cache_size = -1048576");
|
|
525
|
-
sqlite.pragma("temp_store = MEMORY");
|
|
526
|
-
sqlite.pragma("page_size = 4096");
|
|
527
|
-
sqlite.pragma("wal_autocheckpoint = 10000");
|
|
528
|
-
sqlite.pragma("busy_timeout = 5000");
|
|
529
|
-
} catch (e) {
|
|
530
|
-
Logger_default.warn("\u8BBE\u7F6E SQLite \u6027\u80FD\u53C2\u6570\u5931\u8D25\uFF1A", e);
|
|
531
|
-
}
|
|
532
|
-
let db;
|
|
533
|
-
try {
|
|
534
|
-
db = drizzle(sqlite, { schema: options.schemas || {} });
|
|
535
|
-
} catch (e) {
|
|
536
|
-
try {
|
|
537
|
-
sqlite.close();
|
|
538
|
-
} catch (closeError) {
|
|
539
|
-
Logger_default.warn("\u5173\u95ED\u6570\u636E\u5E93\u8FDE\u63A5\u5931\u8D25\uFF1A", closeError);
|
|
540
|
-
}
|
|
541
|
-
throw new Error(`\u521D\u59CB\u5316 Drizzle ORM \u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
542
|
-
}
|
|
543
|
-
if (options.state) {
|
|
544
|
-
options.state.db = db;
|
|
545
|
-
}
|
|
546
|
-
return db;
|
|
547
|
-
} catch (error) {
|
|
548
|
-
Logger_default.error("\u6570\u636E\u5E93\u521D\u59CB\u5316\u5931\u8D25\uFF1A", error);
|
|
549
|
-
throw new Error(`\u6570\u636E\u5E93\u521D\u59CB\u5316\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// utils/MiddlewareRegistry.ts
|
|
554
|
-
async function registerMiddleware(app, config2) {
|
|
555
|
-
const { fileName = "anonymous", defaultExport: handler } = config2;
|
|
556
|
-
try {
|
|
557
|
-
app.use(handler);
|
|
558
|
-
Logger_default.startup("\u4E2D\u95F4\u4EF6", `\u6CE8\u518C: ${fileName}`);
|
|
559
|
-
} catch (error) {
|
|
560
|
-
Logger_default.error(`\u6CE8\u518C\u4E2D\u95F4\u4EF6\u5931\u8D25 ${fileName}:`, error);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// utils/RouteParser.ts
|
|
565
|
-
import {
|
|
566
|
-
Project,
|
|
567
|
-
SyntaxKind as SyntaxKind3,
|
|
568
|
-
ModuleKind,
|
|
569
|
-
ScriptTarget
|
|
570
|
-
} from "ts-morph";
|
|
571
|
-
import * as path3 from "path";
|
|
572
|
-
|
|
573
|
-
// utils/ZodSchemaParser.ts
|
|
574
|
-
import {
|
|
575
|
-
Node
|
|
576
|
-
} from "ts-morph";
|
|
577
|
-
var ZodSchemaParser = class {
|
|
578
|
-
/**
|
|
579
|
-
* 解析 schema 变量,返回字段结构
|
|
580
|
-
*/
|
|
581
|
-
parseSchemaVariable(sourceFile, schemaName) {
|
|
582
|
-
try {
|
|
583
|
-
const variableDecl = this.findVariableDeclaration(sourceFile, schemaName);
|
|
584
|
-
if (!variableDecl) {
|
|
585
|
-
Logger_default.warn(`\u672A\u627E\u5230 schema \u53D8\u91CF: ${schemaName}`);
|
|
586
|
-
return null;
|
|
587
|
-
}
|
|
588
|
-
const initializer = variableDecl.getInitializer();
|
|
589
|
-
if (!initializer) {
|
|
590
|
-
Logger_default.warn(`schema \u53D8\u91CF ${schemaName} \u6CA1\u6709\u521D\u59CB\u5316\u8868\u8FBE\u5F0F`);
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
return this.parseZodObject(initializer);
|
|
594
|
-
} catch (error) {
|
|
595
|
-
Logger_default.error(`\u89E3\u6790 schema \u53D8\u91CF\u5931\u8D25: ${schemaName}`, error);
|
|
596
|
-
return null;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* 查找变量声明
|
|
601
|
-
*/
|
|
602
|
-
findVariableDeclaration(sourceFile, name) {
|
|
603
|
-
for (const decl of sourceFile.getVariableDeclarations()) {
|
|
604
|
-
if (decl.getName() === name) {
|
|
605
|
-
return decl;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
return null;
|
|
609
|
-
}
|
|
610
|
-
/**
|
|
611
|
-
* 解析 z.object({...}) 调用
|
|
612
|
-
*/
|
|
613
|
-
parseZodObject(node) {
|
|
614
|
-
let targetNode = node;
|
|
615
|
-
if (Node.isAsExpression(node)) {
|
|
616
|
-
targetNode = node.getExpression();
|
|
617
|
-
}
|
|
618
|
-
if (Node.isCallExpression(targetNode)) {
|
|
619
|
-
const expr = targetNode.getExpression();
|
|
620
|
-
if (Node.isPropertyAccessExpression(expr)) {
|
|
621
|
-
const propText = expr.getText();
|
|
622
|
-
if (propText === "z.object") {
|
|
623
|
-
const args = targetNode.getArguments();
|
|
624
|
-
if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) {
|
|
625
|
-
return this.parseObjectLiteral(args[0]);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
return null;
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* 解析对象字面量中的字段定义
|
|
634
|
-
*/
|
|
635
|
-
parseObjectLiteral(objLit) {
|
|
636
|
-
const fields = {};
|
|
637
|
-
for (const prop of objLit.getProperties()) {
|
|
638
|
-
if (Node.isPropertyAssignment(prop)) {
|
|
639
|
-
const fieldName = prop.getName();
|
|
640
|
-
const initializer = prop.getInitializer();
|
|
641
|
-
if (initializer) {
|
|
642
|
-
fields[fieldName] = this.parseZodField(initializer);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
return fields;
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
|
-
* 解析单个 Zod 字段定义
|
|
650
|
-
*/
|
|
651
|
-
parseZodField(node) {
|
|
652
|
-
const field = {
|
|
653
|
-
type: "any",
|
|
654
|
-
required: true
|
|
655
|
-
};
|
|
656
|
-
let targetNode = node;
|
|
657
|
-
if (Node.isAsExpression(node)) {
|
|
658
|
-
targetNode = node.getExpression();
|
|
659
|
-
}
|
|
660
|
-
const callChain = this.collectCallChain(targetNode);
|
|
661
|
-
for (const call of callChain) {
|
|
662
|
-
this.applyZodMethod(field, call);
|
|
663
|
-
}
|
|
664
|
-
return field;
|
|
665
|
-
}
|
|
666
|
-
/**
|
|
667
|
-
* 收集方法调用链
|
|
668
|
-
* 例如: z.string().min(1).optional() -> [{method: "z.string", args: []}, {method: "min", args: [...]}, {method: "optional", args: []}]
|
|
669
|
-
*/
|
|
670
|
-
collectCallChain(node) {
|
|
671
|
-
const chain = [];
|
|
672
|
-
let current = node;
|
|
673
|
-
while (current && Node.isCallExpression(current)) {
|
|
674
|
-
const expr = current.getExpression();
|
|
675
|
-
const args = current.getArguments();
|
|
676
|
-
if (Node.isPropertyAccessExpression(expr)) {
|
|
677
|
-
const methodName = expr.getName();
|
|
678
|
-
chain.unshift({ method: methodName, args: [...args] });
|
|
679
|
-
current = expr.getExpression();
|
|
680
|
-
} else if (Node.isIdentifier(expr)) {
|
|
681
|
-
chain.unshift({ method: expr.getText(), args: [] });
|
|
682
|
-
break;
|
|
683
|
-
} else if (Node.isPropertyAccessExpression(expr)) {
|
|
684
|
-
chain.unshift({
|
|
685
|
-
method: expr.getName(),
|
|
686
|
-
args: []
|
|
687
|
-
});
|
|
688
|
-
current = expr.getExpression();
|
|
689
|
-
} else {
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
return chain;
|
|
694
|
-
}
|
|
695
|
-
/**
|
|
696
|
-
* 应用 Zod 方法到字段定义
|
|
697
|
-
*/
|
|
698
|
-
applyZodMethod(field, call) {
|
|
699
|
-
const { method, args } = call;
|
|
700
|
-
switch (method) {
|
|
701
|
-
// 类型定义
|
|
702
|
-
case "z.string":
|
|
703
|
-
case "string":
|
|
704
|
-
field.type = "string";
|
|
705
|
-
break;
|
|
706
|
-
case "z.number":
|
|
707
|
-
case "number":
|
|
708
|
-
field.type = "number";
|
|
709
|
-
break;
|
|
710
|
-
case "z.boolean":
|
|
711
|
-
case "boolean":
|
|
712
|
-
field.type = "boolean";
|
|
713
|
-
break;
|
|
714
|
-
case "z.array":
|
|
715
|
-
case "array":
|
|
716
|
-
field.type = "array";
|
|
717
|
-
if (args.length > 0) {
|
|
718
|
-
field.items = this.parseZodField(args[0]);
|
|
719
|
-
}
|
|
720
|
-
break;
|
|
721
|
-
case "z.object":
|
|
722
|
-
case "object":
|
|
723
|
-
field.type = "object";
|
|
724
|
-
if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) {
|
|
725
|
-
field.properties = this.parseObjectLiteral(args[0]);
|
|
726
|
-
}
|
|
727
|
-
break;
|
|
728
|
-
case "z.date":
|
|
729
|
-
case "date":
|
|
730
|
-
field.type = "date";
|
|
731
|
-
break;
|
|
732
|
-
case "z.any":
|
|
733
|
-
case "any":
|
|
734
|
-
field.type = "any";
|
|
735
|
-
break;
|
|
736
|
-
case "z.unknown":
|
|
737
|
-
case "unknown":
|
|
738
|
-
field.type = "any";
|
|
739
|
-
break;
|
|
740
|
-
case "z.undefined":
|
|
741
|
-
case "undefined":
|
|
742
|
-
field.required = false;
|
|
743
|
-
field.optional = true;
|
|
744
|
-
break;
|
|
745
|
-
case "z.null":
|
|
746
|
-
case "null":
|
|
747
|
-
field.nullable = true;
|
|
748
|
-
break;
|
|
749
|
-
case "z.void":
|
|
750
|
-
case "void":
|
|
751
|
-
field.type = "any";
|
|
752
|
-
break;
|
|
753
|
-
case "z.nativeEnum":
|
|
754
|
-
case "nativeEnum":
|
|
755
|
-
case "z.enum":
|
|
756
|
-
case "enum":
|
|
757
|
-
field.type = "string";
|
|
758
|
-
break;
|
|
759
|
-
case "coerce":
|
|
760
|
-
break;
|
|
761
|
-
// 可选性
|
|
762
|
-
case "optional":
|
|
763
|
-
field.required = false;
|
|
764
|
-
field.optional = true;
|
|
765
|
-
break;
|
|
766
|
-
case "nullable":
|
|
767
|
-
field.nullable = true;
|
|
768
|
-
break;
|
|
769
|
-
case "nullish":
|
|
770
|
-
field.required = false;
|
|
771
|
-
field.optional = true;
|
|
772
|
-
field.nullable = true;
|
|
773
|
-
break;
|
|
774
|
-
// 默认值
|
|
775
|
-
case "default":
|
|
776
|
-
if (args.length > 0) {
|
|
777
|
-
field.defaultValue = this.parseArgumentValue(args[0]);
|
|
778
|
-
field.required = false;
|
|
779
|
-
}
|
|
780
|
-
break;
|
|
781
|
-
// 约束 - 通用
|
|
782
|
-
case "min":
|
|
783
|
-
if (args.length > 0) {
|
|
784
|
-
const minVal = this.parseArgumentValue(args[0]);
|
|
785
|
-
field.constraints = field.constraints || {};
|
|
786
|
-
if (field.type === "string") {
|
|
787
|
-
field.constraints.minLength = minVal;
|
|
788
|
-
} else if (field.type === "number") {
|
|
789
|
-
field.constraints.min = minVal;
|
|
790
|
-
}
|
|
791
|
-
if (args.length > 1) {
|
|
792
|
-
const msg = this.parseArgumentValue(args[1]);
|
|
793
|
-
if (typeof msg === "string") {
|
|
794
|
-
field.constraints.custom = field.constraints.custom || [];
|
|
795
|
-
field.constraints.custom.push(msg);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
break;
|
|
800
|
-
case "max":
|
|
801
|
-
if (args.length > 0) {
|
|
802
|
-
const maxVal = this.parseArgumentValue(args[0]);
|
|
803
|
-
field.constraints = field.constraints || {};
|
|
804
|
-
if (field.type === "string") {
|
|
805
|
-
field.constraints.maxLength = maxVal;
|
|
806
|
-
} else if (field.type === "number") {
|
|
807
|
-
field.constraints.max = maxVal;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
break;
|
|
811
|
-
case "length":
|
|
812
|
-
if (args.length > 0) {
|
|
813
|
-
const lenVal = this.parseArgumentValue(args[0]);
|
|
814
|
-
field.constraints = field.constraints || {};
|
|
815
|
-
field.constraints.minLength = lenVal;
|
|
816
|
-
field.constraints.maxLength = lenVal;
|
|
817
|
-
}
|
|
818
|
-
break;
|
|
819
|
-
// 约束 - 字符串
|
|
820
|
-
case "email":
|
|
821
|
-
field.constraints = field.constraints || {};
|
|
822
|
-
field.constraints.email = true;
|
|
823
|
-
break;
|
|
824
|
-
case "url":
|
|
825
|
-
field.constraints = field.constraints || {};
|
|
826
|
-
field.constraints.url = true;
|
|
827
|
-
break;
|
|
828
|
-
case "uuid":
|
|
829
|
-
field.constraints = field.constraints || {};
|
|
830
|
-
field.constraints.uuid = true;
|
|
831
|
-
break;
|
|
832
|
-
case "regex":
|
|
833
|
-
case "pattern":
|
|
834
|
-
if (args.length > 0) {
|
|
835
|
-
field.constraints = field.constraints || {};
|
|
836
|
-
field.constraints.pattern = args[0].getText();
|
|
837
|
-
}
|
|
838
|
-
break;
|
|
839
|
-
case "startsWith":
|
|
840
|
-
case "endsWith":
|
|
841
|
-
case "includes":
|
|
842
|
-
case "trim":
|
|
843
|
-
case "toLowerCase":
|
|
844
|
-
case "toUpperCase":
|
|
845
|
-
break;
|
|
846
|
-
// 约束 - 数字
|
|
847
|
-
case "int":
|
|
848
|
-
field.constraints = field.constraints || {};
|
|
849
|
-
field.constraints.int = true;
|
|
850
|
-
break;
|
|
851
|
-
case "positive":
|
|
852
|
-
field.constraints = field.constraints || {};
|
|
853
|
-
field.constraints.positive = true;
|
|
854
|
-
break;
|
|
855
|
-
case "nonnegative":
|
|
856
|
-
field.constraints = field.constraints || {};
|
|
857
|
-
field.constraints.nonnegative = true;
|
|
858
|
-
break;
|
|
859
|
-
case "negative":
|
|
860
|
-
field.constraints = field.constraints || {};
|
|
861
|
-
field.constraints.max = -1;
|
|
862
|
-
break;
|
|
863
|
-
case "nonpositive":
|
|
864
|
-
field.constraints = field.constraints || {};
|
|
865
|
-
field.constraints.max = 0;
|
|
866
|
-
break;
|
|
867
|
-
case "finite":
|
|
868
|
-
case "safe":
|
|
869
|
-
break;
|
|
870
|
-
// 约束 - 数组
|
|
871
|
-
case "nonempty":
|
|
872
|
-
field.constraints = field.constraints || {};
|
|
873
|
-
field.constraints.minLength = 1;
|
|
874
|
-
break;
|
|
875
|
-
// 其他方法
|
|
876
|
-
case "refine":
|
|
877
|
-
case "superRefine":
|
|
878
|
-
case "transform":
|
|
879
|
-
case "pipe":
|
|
880
|
-
case " preprocess":
|
|
881
|
-
case "omit":
|
|
882
|
-
case "pick":
|
|
883
|
-
case "partial":
|
|
884
|
-
case "required":
|
|
885
|
-
case "strict":
|
|
886
|
-
case "passthrough":
|
|
887
|
-
case "extend":
|
|
888
|
-
case "merge":
|
|
889
|
-
case "keyof":
|
|
890
|
-
break;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
/**
|
|
894
|
-
* 解析参数值
|
|
895
|
-
*/
|
|
896
|
-
parseArgumentValue(node) {
|
|
897
|
-
if (Node.isStringLiteral(node)) {
|
|
898
|
-
return node.getLiteralValue();
|
|
899
|
-
} else if (Node.isNumericLiteral(node)) {
|
|
900
|
-
return node.getLiteralValue();
|
|
901
|
-
} else if (Node.isTrueLiteral(node)) {
|
|
902
|
-
return true;
|
|
903
|
-
} else if (Node.isFalseLiteral(node)) {
|
|
904
|
-
return false;
|
|
905
|
-
} else if (Node.isIdentifier(node)) {
|
|
906
|
-
return node.getText();
|
|
907
|
-
} else if (Node.isObjectLiteralExpression(node)) {
|
|
908
|
-
return node.getText();
|
|
909
|
-
} else if (Node.isArrayLiteralExpression(node)) {
|
|
910
|
-
return node.getText();
|
|
911
|
-
}
|
|
912
|
-
return node.getText();
|
|
913
|
-
}
|
|
914
|
-
};
|
|
915
|
-
var zodSchemaParser = new ZodSchemaParser();
|
|
916
|
-
|
|
917
|
-
// utils/TSTypeParser.ts
|
|
918
|
-
import {
|
|
919
|
-
Node as Node2,
|
|
920
|
-
SyntaxKind as SyntaxKind2
|
|
921
|
-
} from "ts-morph";
|
|
922
|
-
var TSTypeParser = class {
|
|
923
|
-
/**
|
|
924
|
-
* 解析类型引用字符串
|
|
925
|
-
* 例如: "ApiResponse<User[]>" -> { typeName: "ApiResponse", typeArgs: ["User[]"] }
|
|
926
|
-
*/
|
|
927
|
-
parseTypeRef(typeString) {
|
|
928
|
-
typeString = typeString.trim();
|
|
929
|
-
const genericMatch = typeString.match(/^(\w+)<(.+)>$/);
|
|
930
|
-
if (genericMatch) {
|
|
931
|
-
const typeName = genericMatch[1];
|
|
932
|
-
const argsStr = genericMatch[2];
|
|
933
|
-
const typeArgs = argsStr.split(",").map((s) => s.trim());
|
|
934
|
-
return { typeName, typeArgs };
|
|
935
|
-
}
|
|
936
|
-
if (/^\w+$/.test(typeString)) {
|
|
937
|
-
return { typeName: typeString, typeArgs: [] };
|
|
938
|
-
}
|
|
939
|
-
const arrayMatch = typeString.match(/^(\w+)\[\]$/);
|
|
940
|
-
if (arrayMatch) {
|
|
941
|
-
return { typeName: "Array", typeArgs: [arrayMatch[1]] };
|
|
942
|
-
}
|
|
943
|
-
return null;
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* 解析响应类型
|
|
947
|
-
* @param sourceFile 源文件
|
|
948
|
-
* @param typeString 类型字符串如 "ApiResponse<User[]>"
|
|
949
|
-
* @param imports 导入映射 (类型名 -> 文件路径)
|
|
950
|
-
*/
|
|
951
|
-
parseResponseType(sourceFile, typeString, imports) {
|
|
952
|
-
const typeRef = this.parseTypeRef(typeString);
|
|
953
|
-
if (!typeRef) {
|
|
954
|
-
Logger_default.debug(`\u65E0\u6CD5\u89E3\u6790\u7C7B\u578B\u5F15\u7528: ${typeString}`);
|
|
955
|
-
return null;
|
|
956
|
-
}
|
|
957
|
-
if (typeRef.typeName === "ApiResponse") {
|
|
958
|
-
return this.parseApiResponse(sourceFile, typeRef.typeArgs, imports);
|
|
959
|
-
}
|
|
960
|
-
const typeDecl = this.findTypeDeclaration(sourceFile, typeRef.typeName, imports);
|
|
961
|
-
if (!typeDecl) {
|
|
962
|
-
Logger_default.debug(`\u672A\u627E\u5230\u7C7B\u578B\u5B9A\u4E49: ${typeRef.typeName}`);
|
|
963
|
-
return null;
|
|
964
|
-
}
|
|
965
|
-
const fields = this.extractFieldsFromType(typeDecl, typeRef.typeArgs, imports);
|
|
966
|
-
return { fields };
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* 解析 ApiResponse<T> 类型
|
|
970
|
-
* ApiResponse 是标准的 API 响应包装器,包含 code, data, msg 字段
|
|
971
|
-
*/
|
|
972
|
-
parseApiResponse(sourceFile, typeArgs, imports) {
|
|
973
|
-
const fields = {
|
|
974
|
-
code: { type: "number", required: true },
|
|
975
|
-
msg: { type: "string", required: true }
|
|
976
|
-
};
|
|
977
|
-
if (typeArgs.length > 0) {
|
|
978
|
-
const dataType = typeArgs[0];
|
|
979
|
-
const dataField = this.parseDataType(sourceFile, dataType, imports);
|
|
980
|
-
fields.data = dataField;
|
|
981
|
-
} else {
|
|
982
|
-
fields.data = { type: "any", required: true };
|
|
983
|
-
}
|
|
984
|
-
return { fields };
|
|
985
|
-
}
|
|
986
|
-
/**
|
|
987
|
-
* 解析 data 字段的类型
|
|
988
|
-
*/
|
|
989
|
-
parseDataType(sourceFile, typeString, imports) {
|
|
990
|
-
if (typeString === "string") {
|
|
991
|
-
return { type: "string", required: true };
|
|
992
|
-
} else if (typeString === "number") {
|
|
993
|
-
return { type: "number", required: true };
|
|
994
|
-
} else if (typeString === "boolean") {
|
|
995
|
-
return { type: "boolean", required: true };
|
|
996
|
-
} else if (typeString === "any") {
|
|
997
|
-
return { type: "any", required: true };
|
|
998
|
-
}
|
|
999
|
-
const arrayMatch = typeString.match(/^(\w+)\[\]$/);
|
|
1000
|
-
if (arrayMatch) {
|
|
1001
|
-
const elementType = arrayMatch[1];
|
|
1002
|
-
const elementField = this.parseDataType(sourceFile, elementType, imports);
|
|
1003
|
-
return {
|
|
1004
|
-
type: "array",
|
|
1005
|
-
required: true,
|
|
1006
|
-
items: elementField
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
const typeDecl = this.findTypeDeclaration(sourceFile, typeString, imports);
|
|
1010
|
-
if (typeDecl) {
|
|
1011
|
-
const fields = this.extractFieldsFromType(typeDecl, [], imports);
|
|
1012
|
-
return {
|
|
1013
|
-
type: "object",
|
|
1014
|
-
required: true,
|
|
1015
|
-
properties: fields
|
|
1016
|
-
};
|
|
1017
|
-
}
|
|
1018
|
-
return { type: "any", required: true };
|
|
1019
|
-
}
|
|
1020
|
-
/**
|
|
1021
|
-
* 查找类型声明
|
|
1022
|
-
*/
|
|
1023
|
-
findTypeDeclaration(sourceFile, typeName, imports) {
|
|
1024
|
-
const interfaceDecl = sourceFile.getInterface(typeName);
|
|
1025
|
-
if (interfaceDecl) return interfaceDecl;
|
|
1026
|
-
const typeAlias = sourceFile.getTypeAlias(typeName);
|
|
1027
|
-
if (typeAlias) return typeAlias;
|
|
1028
|
-
const importPath = imports.get(typeName);
|
|
1029
|
-
if (importPath) {
|
|
1030
|
-
try {
|
|
1031
|
-
const importedFile = sourceFile.getProject().addSourceFileAtPath(importPath);
|
|
1032
|
-
const importedInterface = importedFile.getInterface(typeName);
|
|
1033
|
-
if (importedInterface) return importedInterface;
|
|
1034
|
-
const importedTypeAlias = importedFile.getTypeAlias(typeName);
|
|
1035
|
-
if (importedTypeAlias) return importedTypeAlias;
|
|
1036
|
-
} catch (error) {
|
|
1037
|
-
Logger_default.warn(`\u65E0\u6CD5\u52A0\u8F7D\u5BFC\u5165\u6587\u4EF6: ${importPath}`);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
return null;
|
|
1041
|
-
}
|
|
1042
|
-
/**
|
|
1043
|
-
* 从类型声明中提取字段
|
|
1044
|
-
*/
|
|
1045
|
-
extractFieldsFromType(typeDecl, typeArgs, imports) {
|
|
1046
|
-
const fields = {};
|
|
1047
|
-
let typeNode;
|
|
1048
|
-
if (Node2.isInterfaceDeclaration(typeDecl)) {
|
|
1049
|
-
for (const prop of typeDecl.getProperties()) {
|
|
1050
|
-
const fieldName = prop.getName();
|
|
1051
|
-
const fieldType = prop.getType();
|
|
1052
|
-
fields[fieldName] = this.parsePropertyType(prop, typeArgs, imports);
|
|
1053
|
-
}
|
|
1054
|
-
return fields;
|
|
1055
|
-
} else if (Node2.isTypeAliasDeclaration(typeDecl)) {
|
|
1056
|
-
typeNode = typeDecl.getTypeNode();
|
|
1057
|
-
}
|
|
1058
|
-
if (!typeNode) return fields;
|
|
1059
|
-
if (typeNode.getKind() === SyntaxKind2.TypeReference) {
|
|
1060
|
-
return this.extractFieldsFromTypeReference(typeNode, typeArgs, imports);
|
|
1061
|
-
} else if (typeNode.getKind() === SyntaxKind2.IntersectionType) {
|
|
1062
|
-
for (const child of typeNode.getTypeNodes()) {
|
|
1063
|
-
const childFields = this.extractFieldsFromTypeNode(child, typeArgs, imports);
|
|
1064
|
-
Object.assign(fields, childFields);
|
|
1065
|
-
}
|
|
1066
|
-
} else if (typeNode.getKind() === SyntaxKind2.UnionType) {
|
|
1067
|
-
const firstType = typeNode.getTypeNodes()[0];
|
|
1068
|
-
if (firstType) {
|
|
1069
|
-
return this.extractFieldsFromTypeNode(firstType, typeArgs, imports);
|
|
1070
|
-
}
|
|
1071
|
-
} else {
|
|
1072
|
-
return this.extractFieldsFromTypeNode(typeNode, typeArgs, imports);
|
|
1073
|
-
}
|
|
1074
|
-
return fields;
|
|
1075
|
-
}
|
|
1076
|
-
/**
|
|
1077
|
-
* 从类型节点提取字段
|
|
1078
|
-
*/
|
|
1079
|
-
extractFieldsFromTypeNode(typeNode, typeArgs, imports) {
|
|
1080
|
-
const fields = {};
|
|
1081
|
-
if (typeNode.getKind() === SyntaxKind2.TypeReference) {
|
|
1082
|
-
return this.extractFieldsFromTypeReference(typeNode, typeArgs, imports);
|
|
1083
|
-
} else if (typeNode.getKind() === SyntaxKind2.IntersectionType) {
|
|
1084
|
-
for (const child of typeNode.getTypeNodes()) {
|
|
1085
|
-
const childFields = this.extractFieldsFromTypeNode(child, typeArgs, imports);
|
|
1086
|
-
Object.assign(fields, childFields);
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
return fields;
|
|
1090
|
-
}
|
|
1091
|
-
/**
|
|
1092
|
-
* 从类型引用提取字段
|
|
1093
|
-
*/
|
|
1094
|
-
extractFieldsFromTypeReference(typeRefNode, typeArgs, imports) {
|
|
1095
|
-
const fields = {};
|
|
1096
|
-
const typeName = typeRefNode.getTypeName().getText();
|
|
1097
|
-
if (typeName === "Array" || typeName === "ReadonlyArray") {
|
|
1098
|
-
const typeArgsNodes = typeRefNode.getTypeArguments();
|
|
1099
|
-
if (typeArgsNodes.length > 0) {
|
|
1100
|
-
const elementType = typeArgsNodes[0];
|
|
1101
|
-
fields["[]"] = {
|
|
1102
|
-
type: "array",
|
|
1103
|
-
required: true,
|
|
1104
|
-
items: this.parseTypeNodeToFieldValidation(elementType, typeArgs, imports)
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
1107
|
-
return fields;
|
|
1108
|
-
}
|
|
1109
|
-
if (typeName === "Promise") {
|
|
1110
|
-
const typeArgsNodes = typeRefNode.getTypeArguments();
|
|
1111
|
-
if (typeArgsNodes.length > 0) {
|
|
1112
|
-
return this.extractFieldsFromTypeNode(typeArgsNodes[0], typeArgs, imports);
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
if (typeArgs.includes(typeName) && typeArgs.length > 0) {
|
|
1116
|
-
const actualType = typeArgs[0];
|
|
1117
|
-
}
|
|
1118
|
-
const sourceFile = typeRefNode.getSourceFile();
|
|
1119
|
-
const typeDecl = this.findTypeDeclarationInFile(sourceFile, typeName);
|
|
1120
|
-
if (typeDecl) {
|
|
1121
|
-
return this.extractFieldsFromType(typeDecl, typeArgs, imports);
|
|
1122
|
-
}
|
|
1123
|
-
return fields;
|
|
1124
|
-
}
|
|
1125
|
-
/**
|
|
1126
|
-
* 在文件中查找类型声明
|
|
1127
|
-
*/
|
|
1128
|
-
findTypeDeclarationInFile(sourceFile, typeName) {
|
|
1129
|
-
const interfaceDecl = sourceFile.getInterface(typeName);
|
|
1130
|
-
if (interfaceDecl) return interfaceDecl;
|
|
1131
|
-
const typeAlias = sourceFile.getTypeAlias(typeName);
|
|
1132
|
-
if (typeAlias) return typeAlias;
|
|
1133
|
-
return null;
|
|
1134
|
-
}
|
|
1135
|
-
/**
|
|
1136
|
-
* 解析属性类型
|
|
1137
|
-
*/
|
|
1138
|
-
parsePropertyType(prop, typeArgs, imports) {
|
|
1139
|
-
const type = prop.getType();
|
|
1140
|
-
const isOptional = prop.hasQuestionToken();
|
|
1141
|
-
const typeText = type.getText();
|
|
1142
|
-
if (type.isString()) {
|
|
1143
|
-
return { type: "string", required: !isOptional };
|
|
1144
|
-
} else if (type.isNumber()) {
|
|
1145
|
-
return { type: "number", required: !isOptional };
|
|
1146
|
-
} else if (type.isBoolean()) {
|
|
1147
|
-
return { type: "boolean", required: !isOptional };
|
|
1148
|
-
} else if (type.isArray()) {
|
|
1149
|
-
const elementType = type.getArrayElementType();
|
|
1150
|
-
let items;
|
|
1151
|
-
if (elementType) {
|
|
1152
|
-
items = {
|
|
1153
|
-
type: this.mapTypeToString(elementType),
|
|
1154
|
-
required: !elementType.isUndefined() && !elementType.isNull()
|
|
1155
|
-
};
|
|
1156
|
-
}
|
|
1157
|
-
return { type: "array", required: !isOptional, items };
|
|
1158
|
-
} else if (type.isObject()) {
|
|
1159
|
-
const properties = type.getProperties();
|
|
1160
|
-
if (properties.length > 0) {
|
|
1161
|
-
const props = {};
|
|
1162
|
-
for (const prop2 of properties) {
|
|
1163
|
-
const propName = prop2.getName();
|
|
1164
|
-
const propType = prop2.getTypeAtLocation(prop2.getValueDeclaration() || prop2.getDeclarations()[0]);
|
|
1165
|
-
props[propName] = {
|
|
1166
|
-
type: this.mapTypeToString(propType),
|
|
1167
|
-
required: !prop2.isOptional()
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
|
-
return { type: "object", required: !isOptional, properties: props };
|
|
1171
|
-
}
|
|
1172
|
-
return { type: "object", required: !isOptional };
|
|
1173
|
-
} else if (type.isNull()) {
|
|
1174
|
-
return { type: "any", required: false, nullable: true };
|
|
1175
|
-
} else if (type.isUndefined()) {
|
|
1176
|
-
return { type: "any", required: false, optional: true };
|
|
1177
|
-
} else if (type.isAny()) {
|
|
1178
|
-
return { type: "any", required: !isOptional };
|
|
1179
|
-
}
|
|
1180
|
-
return { type: "any", required: !isOptional };
|
|
1181
|
-
}
|
|
1182
|
-
/**
|
|
1183
|
-
* 将类型映射为字符串
|
|
1184
|
-
*/
|
|
1185
|
-
mapTypeToString(type) {
|
|
1186
|
-
if (type.isString()) return "string";
|
|
1187
|
-
if (type.isNumber()) return "number";
|
|
1188
|
-
if (type.isBoolean()) return "boolean";
|
|
1189
|
-
if (type.isArray()) return "array";
|
|
1190
|
-
if (type.isObject()) return "object";
|
|
1191
|
-
if (type.isDate()) return "date";
|
|
1192
|
-
return "any";
|
|
1193
|
-
}
|
|
1194
|
-
/**
|
|
1195
|
-
* 从类型节点创建 FieldValidation
|
|
1196
|
-
*/
|
|
1197
|
-
parseTypeNodeToFieldValidation(typeNode, typeArgs, imports) {
|
|
1198
|
-
if (Node2.isStringKeyword(typeNode)) {
|
|
1199
|
-
return { type: "string", required: true };
|
|
1200
|
-
} else if (Node2.isNumberKeyword(typeNode)) {
|
|
1201
|
-
return { type: "number", required: true };
|
|
1202
|
-
} else if (Node2.isBooleanKeyword(typeNode)) {
|
|
1203
|
-
return { type: "boolean", required: true };
|
|
1204
|
-
} else if (Node2.isArrayTypeNode(typeNode)) {
|
|
1205
|
-
const elementType = typeNode.getElementTypeNode();
|
|
1206
|
-
return {
|
|
1207
|
-
type: "array",
|
|
1208
|
-
required: true,
|
|
1209
|
-
items: this.parseTypeNodeToFieldValidation(elementType, typeArgs, imports)
|
|
1210
|
-
};
|
|
1211
|
-
} else if (typeNode.getKind() === SyntaxKind2.TypeReference) {
|
|
1212
|
-
const refNode = typeNode;
|
|
1213
|
-
const typeName = refNode.getTypeName().getText();
|
|
1214
|
-
const sourceFile = refNode.getSourceFile();
|
|
1215
|
-
const typeDecl = this.findTypeDeclarationInFile(sourceFile, typeName);
|
|
1216
|
-
if (typeDecl) {
|
|
1217
|
-
const fields = this.extractFieldsFromType(typeDecl, typeArgs, imports);
|
|
1218
|
-
return { type: "object", required: true, properties: fields };
|
|
1219
|
-
}
|
|
1220
|
-
return { type: "any", required: true };
|
|
1221
|
-
}
|
|
1222
|
-
return { type: "any", required: true };
|
|
1223
|
-
}
|
|
1224
|
-
};
|
|
1225
|
-
var tsTypeParser = new TSTypeParser();
|
|
1226
|
-
|
|
1227
|
-
// utils/RouteParser.ts
|
|
1228
|
-
var RouteParser = class {
|
|
1229
|
-
project;
|
|
1230
|
-
schemaParser;
|
|
1231
|
-
tsTypeParser;
|
|
1232
|
-
constructor() {
|
|
1233
|
-
this.project = new Project({
|
|
1234
|
-
compilerOptions: {
|
|
1235
|
-
module: ModuleKind.CommonJS,
|
|
1236
|
-
target: ScriptTarget.ES2018,
|
|
1237
|
-
allowJs: true
|
|
1238
|
-
}
|
|
1239
|
-
});
|
|
1240
|
-
this.schemaParser = new ZodSchemaParser();
|
|
1241
|
-
this.tsTypeParser = new TSTypeParser();
|
|
1242
|
-
}
|
|
1243
|
-
/**
|
|
1244
|
-
* 解析单个路由文件
|
|
1245
|
-
* @param filePath 路由文件路径
|
|
1246
|
-
* @returns 路由信息数组
|
|
1247
|
-
*/
|
|
1248
|
-
parseRouteFile(filePath) {
|
|
1249
|
-
try {
|
|
1250
|
-
const sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
1251
|
-
const routes = [];
|
|
1252
|
-
const schemaFiles = this.getImportedSchemaFiles(sourceFile, filePath);
|
|
1253
|
-
const typeImports = this.getTypeImports(sourceFile, filePath);
|
|
1254
|
-
sourceFile.forEachDescendant((node) => {
|
|
1255
|
-
const callExpr = node;
|
|
1256
|
-
if (callExpr.isKind && callExpr.isKind(SyntaxKind3.CallExpression) && callExpr.getExpression && callExpr.getExpression().isKind(SyntaxKind3.PropertyAccessExpression)) {
|
|
1257
|
-
const propAccess = callExpr.getExpression();
|
|
1258
|
-
const object = propAccess.getExpression && propAccess.getExpression().getText();
|
|
1259
|
-
const method = propAccess.getName && propAccess.getName();
|
|
1260
|
-
if (object === "router" && ["get", "post", "put", "delete", "patch"].includes(method)) {
|
|
1261
|
-
const args = callExpr.getArguments && callExpr.getArguments();
|
|
1262
|
-
if (args && args.length > 0) {
|
|
1263
|
-
const pathArg = args[0];
|
|
1264
|
-
if (pathArg.isKind && pathArg.isKind(SyntaxKind3.StringLiteral)) {
|
|
1265
|
-
const routePath = pathArg.getText && pathArg.getText().replace(/['"]/g, "");
|
|
1266
|
-
const jsDoc = callExpr.getPreviousSiblingIfKind && callExpr.getPreviousSiblingIfKind(SyntaxKind3.JSDocComment);
|
|
1267
|
-
let summary = "";
|
|
1268
|
-
let description = "";
|
|
1269
|
-
let requestSchema;
|
|
1270
|
-
let responseSchema;
|
|
1271
|
-
if (jsDoc) {
|
|
1272
|
-
const commentText = jsDoc.getCommentText && jsDoc.getCommentText();
|
|
1273
|
-
if (commentText) {
|
|
1274
|
-
const lines = commentText.split("\n").map((line) => line.trim());
|
|
1275
|
-
summary = lines[0] || "";
|
|
1276
|
-
description = lines.slice(1).join("\n").trim() || summary;
|
|
1277
|
-
}
|
|
1278
|
-
try {
|
|
1279
|
-
const fullCommentText = jsDoc.getFullText?.() || commentText || "";
|
|
1280
|
-
const apiDocTags = this.parseApiDocTags(fullCommentText);
|
|
1281
|
-
if (apiDocTags.params.length > 0) {
|
|
1282
|
-
requestSchema = this.buildRequestSchemaFromTags(apiDocTags.params);
|
|
1283
|
-
}
|
|
1284
|
-
if (apiDocTags.success.length > 0) {
|
|
1285
|
-
responseSchema = this.buildResponseSchemaFromTags(apiDocTags.success);
|
|
1286
|
-
}
|
|
1287
|
-
} catch (error) {
|
|
1288
|
-
Logger_default.warn(`\u89E3\u6790 apidoc \u6CE8\u91CA\u5931\u8D25: ${filePath}:${routePath}`, error);
|
|
1289
|
-
}
|
|
1290
|
-
if (!responseSchema) {
|
|
1291
|
-
responseSchema = this.extractResponseSchema(jsDoc, sourceFile, typeImports);
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
const middlewares = [];
|
|
1295
|
-
for (let i = 1; i < args.length - 1; i++) {
|
|
1296
|
-
const arg = args[i];
|
|
1297
|
-
if (arg.isKind(SyntaxKind3.CallExpression)) {
|
|
1298
|
-
const mwCallExpr = arg;
|
|
1299
|
-
const expr = mwCallExpr.getExpression();
|
|
1300
|
-
if (expr.isKind(SyntaxKind3.Identifier)) {
|
|
1301
|
-
middlewares.push(expr.getText());
|
|
1302
|
-
} else if (expr.isKind(SyntaxKind3.PropertyAccessExpression)) {
|
|
1303
|
-
middlewares.push(expr.getText());
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
if (!requestSchema) {
|
|
1308
|
-
requestSchema = this.extractRequestSchema(args, schemaFiles);
|
|
1309
|
-
}
|
|
1310
|
-
const routeInfo = {
|
|
1311
|
-
module: path3.basename(path3.dirname(filePath)).toLowerCase(),
|
|
1312
|
-
method: method.toUpperCase(),
|
|
1313
|
-
path: routePath,
|
|
1314
|
-
fullPath: "",
|
|
1315
|
-
// 完整路径会在注册时填充
|
|
1316
|
-
summary,
|
|
1317
|
-
description,
|
|
1318
|
-
requestSchema,
|
|
1319
|
-
responseSchema,
|
|
1320
|
-
middlewares,
|
|
1321
|
-
filePath
|
|
1322
|
-
};
|
|
1323
|
-
routes.push(routeInfo);
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
});
|
|
1329
|
-
return routes;
|
|
1330
|
-
} catch (error) {
|
|
1331
|
-
Logger_default.error(`\u89E3\u6790\u8DEF\u7531\u6587\u4EF6\u5931\u8D25: ${filePath}`, error);
|
|
1332
|
-
return [];
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
/**
|
|
1336
|
-
* 获取导入的 schema 文件映射
|
|
1337
|
-
* @returns Map<变量名, 文件绝对路径>
|
|
1338
|
-
*/
|
|
1339
|
-
getImportedSchemaFiles(sourceFile, currentFilePath) {
|
|
1340
|
-
const schemaFiles = /* @__PURE__ */ new Map();
|
|
1341
|
-
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1342
|
-
const source = importDecl.getModuleSpecifierValue();
|
|
1343
|
-
if (source.includes(".schema") || source.includes("/zod/") || source.includes("\\zod\\")) {
|
|
1344
|
-
const absolutePath = this.resolveImportPath(
|
|
1345
|
-
currentFilePath,
|
|
1346
|
-
source
|
|
1347
|
-
);
|
|
1348
|
-
if (absolutePath) {
|
|
1349
|
-
for (const specifier of importDecl.getNamedImports()) {
|
|
1350
|
-
Logger_default.debug(`\u53D1\u73B0 schema \u5BFC\u5165: ${specifier.getName()} -> ${absolutePath}`);
|
|
1351
|
-
schemaFiles.set(specifier.getName(), absolutePath);
|
|
1352
|
-
}
|
|
1353
|
-
} else {
|
|
1354
|
-
Logger_default.debug(`\u65E0\u6CD5\u89E3\u6790 schema \u5BFC\u5165\u8DEF\u5F84: ${source}`);
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
return schemaFiles;
|
|
1359
|
-
}
|
|
1360
|
-
/**
|
|
1361
|
-
* 获取所有类型导入映射(用于响应类型解析)
|
|
1362
|
-
* @returns Map<类型名, 文件绝对路径>
|
|
1363
|
-
*/
|
|
1364
|
-
getTypeImports(sourceFile, currentFilePath) {
|
|
1365
|
-
const typeImports = /* @__PURE__ */ new Map();
|
|
1366
|
-
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1367
|
-
const source = importDecl.getModuleSpecifierValue();
|
|
1368
|
-
const absolutePath = this.resolveImportPath(currentFilePath, source);
|
|
1369
|
-
if (absolutePath) {
|
|
1370
|
-
for (const specifier of importDecl.getNamedImports()) {
|
|
1371
|
-
typeImports.set(specifier.getName(), absolutePath);
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
return typeImports;
|
|
1376
|
-
}
|
|
1377
|
-
/**
|
|
1378
|
-
* 从 JSDoc 中提取响应 Schema
|
|
1379
|
-
*/
|
|
1380
|
-
extractResponseSchema(jsDoc, sourceFile, typeImports) {
|
|
1381
|
-
const tags = jsDoc.getTags();
|
|
1382
|
-
for (const tag of tags) {
|
|
1383
|
-
const tagName = tag.getTagName();
|
|
1384
|
-
if (tagName === "returns" || tagName === "return") {
|
|
1385
|
-
const tagText = tag.getText?.() || "";
|
|
1386
|
-
Logger_default.debug(`@returns \u6807\u7B7E\u6587\u672C: ${tagText}`);
|
|
1387
|
-
const typeMatch = tagText.match(/@\s*returns?\s*\{([^}]+)\}/i);
|
|
1388
|
-
if (typeMatch) {
|
|
1389
|
-
const typeString = typeMatch[1].trim();
|
|
1390
|
-
Logger_default.debug(`\u4ECE\u6807\u7B7E\u6587\u672C\u63D0\u53D6\u7C7B\u578B: ${typeString}`);
|
|
1391
|
-
const responseSchema = this.tsTypeParser.parseResponseType(
|
|
1392
|
-
sourceFile,
|
|
1393
|
-
typeString,
|
|
1394
|
-
typeImports
|
|
1395
|
-
);
|
|
1396
|
-
if (responseSchema) {
|
|
1397
|
-
return responseSchema;
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
const comment = tag.getCommentText?.();
|
|
1401
|
-
if (comment) {
|
|
1402
|
-
const commentMatch = comment.match(/^\{([^}]+)\}/);
|
|
1403
|
-
if (commentMatch) {
|
|
1404
|
-
const typeString = commentMatch[1].trim();
|
|
1405
|
-
Logger_default.debug(`\u4ECE\u6CE8\u91CA\u63D0\u53D6\u7C7B\u578B: ${typeString}`);
|
|
1406
|
-
const responseSchema = this.tsTypeParser.parseResponseType(
|
|
1407
|
-
sourceFile,
|
|
1408
|
-
typeString,
|
|
1409
|
-
typeImports
|
|
1410
|
-
);
|
|
1411
|
-
if (responseSchema) {
|
|
1412
|
-
return responseSchema;
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
const typeExpression = tag.getTypeExpression?.();
|
|
1417
|
-
if (typeExpression) {
|
|
1418
|
-
const typeNode = typeExpression.getType?.();
|
|
1419
|
-
if (typeNode) {
|
|
1420
|
-
const typeString = typeNode.getText();
|
|
1421
|
-
Logger_default.debug(`\u4ECE\u7C7B\u578B\u8868\u8FBE\u5F0F\u63D0\u53D6\u7C7B\u578B: ${typeString}`);
|
|
1422
|
-
if (typeString && typeString !== "any") {
|
|
1423
|
-
const responseSchema = this.tsTypeParser.parseResponseType(
|
|
1424
|
-
sourceFile,
|
|
1425
|
-
typeString,
|
|
1426
|
-
typeImports
|
|
1427
|
-
);
|
|
1428
|
-
if (responseSchema) {
|
|
1429
|
-
return responseSchema;
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
return void 0;
|
|
1437
|
-
}
|
|
1438
|
-
/**
|
|
1439
|
-
* 解析导入路径为绝对路径
|
|
1440
|
-
*/
|
|
1441
|
-
resolveImportPath(currentFilePath, importPath) {
|
|
1442
|
-
try {
|
|
1443
|
-
if (importPath.startsWith(".")) {
|
|
1444
|
-
const currentDir = path3.dirname(currentFilePath);
|
|
1445
|
-
const absolutePath = path3.resolve(currentDir, importPath);
|
|
1446
|
-
const tsPath = absolutePath.endsWith(".ts") ? absolutePath : `${absolutePath}.ts`;
|
|
1447
|
-
return tsPath;
|
|
1448
|
-
}
|
|
1449
|
-
return null;
|
|
1450
|
-
} catch (error) {
|
|
1451
|
-
Logger_default.warn(`\u89E3\u6790\u5BFC\u5165\u8DEF\u5F84\u5931\u8D25: ${importPath}`);
|
|
1452
|
-
return null;
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
/**
|
|
1456
|
-
* 提取请求 Schema
|
|
1457
|
-
*/
|
|
1458
|
-
extractRequestSchema(args, schemaFiles) {
|
|
1459
|
-
for (const arg of args) {
|
|
1460
|
-
let targetArg = arg;
|
|
1461
|
-
if (arg.isKind && arg.isKind(SyntaxKind3.AsExpression)) {
|
|
1462
|
-
targetArg = arg.getExpression();
|
|
1463
|
-
}
|
|
1464
|
-
if (targetArg.isKind && targetArg.isKind(SyntaxKind3.CallExpression) && targetArg.getExpression && targetArg.getExpression().isKind && targetArg.getExpression().isKind(SyntaxKind3.Identifier) && targetArg.getExpression().getText() === "validate") {
|
|
1465
|
-
const validateArgs = targetArg.getArguments();
|
|
1466
|
-
if (validateArgs.length === 0) continue;
|
|
1467
|
-
const schemaArg = validateArgs[0];
|
|
1468
|
-
let schemaName;
|
|
1469
|
-
if (schemaArg.isKind && schemaArg.isKind(SyntaxKind3.AsExpression)) {
|
|
1470
|
-
schemaName = schemaArg.getExpression().getText();
|
|
1471
|
-
} else {
|
|
1472
|
-
schemaName = schemaArg.getText();
|
|
1473
|
-
}
|
|
1474
|
-
let target = "body";
|
|
1475
|
-
if (validateArgs.length > 1) {
|
|
1476
|
-
const targetArgParam = validateArgs[1];
|
|
1477
|
-
if (targetArgParam.isKind && targetArgParam.isKind(SyntaxKind3.StringLiteral)) {
|
|
1478
|
-
target = targetArgParam.getLiteralValue();
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
const schemaFilePath = schemaFiles.get(schemaName);
|
|
1482
|
-
if (schemaFilePath) {
|
|
1483
|
-
try {
|
|
1484
|
-
const schemaSourceFile = this.project.addSourceFileAtPath(schemaFilePath);
|
|
1485
|
-
const fields = this.schemaParser.parseSchemaVariable(
|
|
1486
|
-
schemaSourceFile,
|
|
1487
|
-
schemaName
|
|
1488
|
-
);
|
|
1489
|
-
if (fields) {
|
|
1490
|
-
return { target, fields };
|
|
1491
|
-
}
|
|
1492
|
-
} catch (error) {
|
|
1493
|
-
Logger_default.warn(`\u89E3\u6790 schema \u6587\u4EF6\u5931\u8D25: ${schemaFilePath}`, error);
|
|
1494
|
-
}
|
|
1495
|
-
} else {
|
|
1496
|
-
Logger_default.debug(`\u672A\u627E\u5230 schema \u6587\u4EF6\u6620\u5C04: ${schemaName}`);
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
return void 0;
|
|
1501
|
-
}
|
|
1502
|
-
/**
|
|
1503
|
-
* 解析多个路由文件
|
|
1504
|
-
* @param filePaths 路由文件路径数组
|
|
1505
|
-
* @returns 路由信息数组
|
|
1506
|
-
*/
|
|
1507
|
-
parseRouteFiles(filePaths) {
|
|
1508
|
-
const allRoutes = [];
|
|
1509
|
-
for (const filePath of filePaths) {
|
|
1510
|
-
const routes = this.parseRouteFile(filePath);
|
|
1511
|
-
allRoutes.push(...routes);
|
|
1512
|
-
}
|
|
1513
|
-
return allRoutes;
|
|
1514
|
-
}
|
|
1515
|
-
/**
|
|
1516
|
-
* 解析 apidoc 格式的注释标签
|
|
1517
|
-
* @param commentText JSDoc 注释文本
|
|
1518
|
-
* @returns 解析出的标签信息
|
|
1519
|
-
*/
|
|
1520
|
-
parseApiDocTags(commentText) {
|
|
1521
|
-
const tags = { params: [], success: [] };
|
|
1522
|
-
const paramRegex = /@apiParam\s+\{([^}]+)\}\s*(?:\[([^\]]+)\]|(\S+))\s*(.*)/g;
|
|
1523
|
-
let match;
|
|
1524
|
-
while ((match = paramRegex.exec(commentText)) !== null) {
|
|
1525
|
-
const [, typeStr, optionalName, requiredName, description] = match;
|
|
1526
|
-
const name = optionalName || requiredName;
|
|
1527
|
-
const required = !optionalName;
|
|
1528
|
-
const typeInfo = this.parseParamType(typeStr);
|
|
1529
|
-
tags.params.push({
|
|
1530
|
-
name,
|
|
1531
|
-
type: typeInfo.type,
|
|
1532
|
-
description: description.trim(),
|
|
1533
|
-
required,
|
|
1534
|
-
enumValues: typeInfo.enumValues
|
|
1535
|
-
});
|
|
1536
|
-
}
|
|
1537
|
-
const successRegex = /@apiSuccess\s+\{([^}]+)\}\s+([^\s]+)\s*(.*)/g;
|
|
1538
|
-
while ((match = successRegex.exec(commentText)) !== null) {
|
|
1539
|
-
const [, typeStr, fieldPath, description] = match;
|
|
1540
|
-
const parts = fieldPath.split(".");
|
|
1541
|
-
const name = parts[parts.length - 1];
|
|
1542
|
-
const parent = parts.length > 1 ? parts.slice(0, -1).join(".") : void 0;
|
|
1543
|
-
const typeInfo = this.parseParamType(typeStr);
|
|
1544
|
-
tags.success.push({
|
|
1545
|
-
name,
|
|
1546
|
-
type: typeInfo.type,
|
|
1547
|
-
description: description.trim(),
|
|
1548
|
-
parent
|
|
1549
|
-
});
|
|
1550
|
-
}
|
|
1551
|
-
return tags;
|
|
1552
|
-
}
|
|
1553
|
-
/**
|
|
1554
|
-
* 解析 apidoc 类型字符串
|
|
1555
|
-
* @param typeStr 类型字符串,如 "string", "string=", "number[]", {string="a","b"}
|
|
1556
|
-
* @returns 解析后的类型信息
|
|
1557
|
-
*/
|
|
1558
|
-
parseParamType(typeStr) {
|
|
1559
|
-
typeStr = typeStr.trim();
|
|
1560
|
-
const enumMatch = typeStr.match(/^(\w+)=(.+)$/);
|
|
1561
|
-
if (enumMatch) {
|
|
1562
|
-
const baseType = enumMatch[1];
|
|
1563
|
-
const enumValues = enumMatch[2].split(",").map((v) => v.trim().replace(/^["']|["']$/g, ""));
|
|
1564
|
-
return {
|
|
1565
|
-
type: this.mapTypeName(baseType),
|
|
1566
|
-
enumValues
|
|
1567
|
-
};
|
|
1568
|
-
}
|
|
1569
|
-
if (typeStr.endsWith("[]")) {
|
|
1570
|
-
const baseType = typeStr.slice(0, -2);
|
|
1571
|
-
return {
|
|
1572
|
-
type: this.mapTypeName(baseType),
|
|
1573
|
-
array: true
|
|
1574
|
-
};
|
|
1575
|
-
}
|
|
1576
|
-
if (typeStr.endsWith("?")) {
|
|
1577
|
-
const baseType = typeStr.slice(0, -1);
|
|
1578
|
-
return {
|
|
1579
|
-
type: this.mapTypeName(baseType)
|
|
1580
|
-
};
|
|
1581
|
-
}
|
|
1582
|
-
return {
|
|
1583
|
-
type: this.mapTypeName(typeStr)
|
|
1584
|
-
};
|
|
1585
|
-
}
|
|
1586
|
-
/**
|
|
1587
|
-
* 映射类型名称到 FieldValidation 类型
|
|
1588
|
-
*/
|
|
1589
|
-
mapTypeName(typeName) {
|
|
1590
|
-
const typeMap = {
|
|
1591
|
-
string: "string",
|
|
1592
|
-
number: "number",
|
|
1593
|
-
integer: "number",
|
|
1594
|
-
boolean: "boolean",
|
|
1595
|
-
array: "array",
|
|
1596
|
-
object: "object",
|
|
1597
|
-
file: "file",
|
|
1598
|
-
date: "date"
|
|
1599
|
-
};
|
|
1600
|
-
return typeMap[typeName.toLowerCase()] || "any";
|
|
1601
|
-
}
|
|
1602
|
-
/**
|
|
1603
|
-
* 从 @apiParam 标签构建请求 Schema
|
|
1604
|
-
*/
|
|
1605
|
-
buildRequestSchemaFromTags(params) {
|
|
1606
|
-
const fields = {};
|
|
1607
|
-
for (const param of params) {
|
|
1608
|
-
fields[param.name] = {
|
|
1609
|
-
type: param.type,
|
|
1610
|
-
required: param.required,
|
|
1611
|
-
constraints: param.enumValues ? { custom: [`\u53EF\u9009\u503C: ${param.enumValues.join(", ")}`] } : void 0
|
|
1612
|
-
};
|
|
1613
|
-
}
|
|
1614
|
-
return { target: "body", fields };
|
|
1615
|
-
}
|
|
1616
|
-
/**
|
|
1617
|
-
* 从 @apiSuccess 标签构建响应 Schema
|
|
1618
|
-
*/
|
|
1619
|
-
buildResponseSchemaFromTags(success) {
|
|
1620
|
-
const fields = {};
|
|
1621
|
-
for (const field of success) {
|
|
1622
|
-
if (!field.parent && !fields[field.name]) {
|
|
1623
|
-
if (field.type === "object") {
|
|
1624
|
-
fields[field.name] = {
|
|
1625
|
-
type: "object",
|
|
1626
|
-
required: true,
|
|
1627
|
-
properties: {}
|
|
1628
|
-
};
|
|
1629
|
-
} else {
|
|
1630
|
-
fields[field.name] = {
|
|
1631
|
-
type: field.type,
|
|
1632
|
-
required: true
|
|
1633
|
-
};
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
for (const field of success) {
|
|
1638
|
-
if (field.parent) {
|
|
1639
|
-
const parts = field.parent.split(".");
|
|
1640
|
-
let current = fields;
|
|
1641
|
-
for (let i = 0; i < parts.length; i++) {
|
|
1642
|
-
const part = parts[i];
|
|
1643
|
-
if (!current[part]) {
|
|
1644
|
-
current[part] = {
|
|
1645
|
-
type: "object",
|
|
1646
|
-
required: true,
|
|
1647
|
-
properties: {}
|
|
1648
|
-
};
|
|
1649
|
-
} else if (!current[part].properties) {
|
|
1650
|
-
current[part].properties = {};
|
|
1651
|
-
}
|
|
1652
|
-
current = current[part].properties;
|
|
1653
|
-
}
|
|
1654
|
-
current[field.name] = {
|
|
1655
|
-
type: field.type,
|
|
1656
|
-
required: true
|
|
1657
|
-
};
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
return { fields };
|
|
1661
|
-
}
|
|
1662
|
-
};
|
|
1663
|
-
var routeParser = new RouteParser();
|
|
1664
|
-
|
|
1665
|
-
// utils/DocManager.ts
|
|
1666
|
-
import Database2 from "better-sqlite3";
|
|
1667
|
-
import * as path4 from "path";
|
|
1668
|
-
import * as fs4 from "fs";
|
|
1669
|
-
var DocManager = class {
|
|
1670
|
-
db = null;
|
|
1671
|
-
config;
|
|
1672
|
-
constructor(config2) {
|
|
1673
|
-
this.config = config2;
|
|
1674
|
-
this.init();
|
|
1675
|
-
}
|
|
1676
|
-
/**
|
|
1677
|
-
* 初始化数据库
|
|
1678
|
-
*/
|
|
1679
|
-
init() {
|
|
1680
|
-
try {
|
|
1681
|
-
const dbDir = path4.dirname(this.config.path);
|
|
1682
|
-
if (!fs4.existsSync(dbDir)) {
|
|
1683
|
-
fs4.mkdirSync(dbDir, { recursive: true });
|
|
1684
|
-
}
|
|
1685
|
-
this.db = new Database2(this.config.path);
|
|
1686
|
-
this.createTables();
|
|
1687
|
-
} catch (error) {
|
|
1688
|
-
Logger_default.error("\u6587\u6863\u6570\u636E\u5E93\u521D\u59CB\u5316\u5931\u8D25:", error);
|
|
1689
|
-
this.db = null;
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
/**
|
|
1693
|
-
* 创建表结构
|
|
1694
|
-
*/
|
|
1695
|
-
createTables() {
|
|
1696
|
-
if (!this.db) return;
|
|
1697
|
-
this.db.exec(`
|
|
1698
|
-
CREATE TABLE IF NOT EXISTS doc_routes (
|
|
1699
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1700
|
-
module TEXT NOT NULL,
|
|
1701
|
-
method TEXT NOT NULL,
|
|
1702
|
-
path TEXT NOT NULL,
|
|
1703
|
-
full_path TEXT NOT NULL,
|
|
1704
|
-
summary TEXT,
|
|
1705
|
-
description TEXT,
|
|
1706
|
-
request_schema TEXT,
|
|
1707
|
-
response_schema TEXT,
|
|
1708
|
-
middlewares TEXT,
|
|
1709
|
-
file_path TEXT NOT NULL,
|
|
1710
|
-
created_at INTEGER NOT NULL,
|
|
1711
|
-
updated_at INTEGER NOT NULL,
|
|
1712
|
-
UNIQUE(module, method, path)
|
|
1713
|
-
);
|
|
1714
|
-
`);
|
|
1715
|
-
this.db.exec(`
|
|
1716
|
-
CREATE INDEX IF NOT EXISTS idx_doc_routes_module ON doc_routes(module);
|
|
1717
|
-
CREATE INDEX IF NOT EXISTS idx_doc_routes_full_path ON doc_routes(full_path);
|
|
1718
|
-
`);
|
|
1719
|
-
}
|
|
1720
|
-
/**
|
|
1721
|
-
* 保存路由信息
|
|
1722
|
-
* @param routeInfo 路由信息
|
|
1723
|
-
* @param apiPrefix API 前缀
|
|
1724
|
-
* @param moduleName 模块名称
|
|
1725
|
-
*/
|
|
1726
|
-
saveRoute(routeInfo, apiPrefix = "/api", moduleName) {
|
|
1727
|
-
if (!this.db) return;
|
|
1728
|
-
try {
|
|
1729
|
-
const fullPath = `${apiPrefix}/${moduleName}${routeInfo.path}`.replace(/\/+/g, "/");
|
|
1730
|
-
const now = Date.now();
|
|
1731
|
-
const data = {
|
|
1732
|
-
module: moduleName,
|
|
1733
|
-
method: routeInfo.method,
|
|
1734
|
-
path: routeInfo.path,
|
|
1735
|
-
full_path: fullPath,
|
|
1736
|
-
summary: routeInfo.summary,
|
|
1737
|
-
description: routeInfo.description,
|
|
1738
|
-
request_schema: routeInfo.requestSchema ? JSON.stringify(routeInfo.requestSchema) : null,
|
|
1739
|
-
response_schema: routeInfo.responseSchema ? JSON.stringify(routeInfo.responseSchema) : null,
|
|
1740
|
-
middlewares: routeInfo.middlewares ? JSON.stringify(routeInfo.middlewares) : null,
|
|
1741
|
-
file_path: routeInfo.filePath,
|
|
1742
|
-
created_at: now,
|
|
1743
|
-
updated_at: now
|
|
1744
|
-
};
|
|
1745
|
-
const stmt = this.db.prepare(`
|
|
1746
|
-
INSERT INTO doc_routes (
|
|
1747
|
-
module, method, path, full_path, summary, description,
|
|
1748
|
-
request_schema, response_schema, middlewares, file_path, created_at, updated_at
|
|
1749
|
-
) VALUES (
|
|
1750
|
-
@module, @method, @path, @full_path, @summary, @description,
|
|
1751
|
-
@request_schema, @response_schema, @middlewares, @file_path, @created_at, @updated_at
|
|
1752
|
-
) ON CONFLICT(module, method, path) DO UPDATE SET
|
|
1753
|
-
full_path = @full_path,
|
|
1754
|
-
summary = @summary,
|
|
1755
|
-
description = @description,
|
|
1756
|
-
request_schema = @request_schema,
|
|
1757
|
-
response_schema = @response_schema,
|
|
1758
|
-
middlewares = @middlewares,
|
|
1759
|
-
file_path = @file_path,
|
|
1760
|
-
updated_at = @updated_at
|
|
1761
|
-
`);
|
|
1762
|
-
stmt.run(data);
|
|
1763
|
-
} catch (error) {
|
|
1764
|
-
Logger_default.error("\u4FDD\u5B58\u8DEF\u7531\u4FE1\u606F\u5931\u8D25:", error);
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
/**
|
|
1768
|
-
* 获取模块列表
|
|
1769
|
-
* @returns 模块列表
|
|
1770
|
-
*/
|
|
1771
|
-
getModules() {
|
|
1772
|
-
if (!this.db) return [];
|
|
1773
|
-
try {
|
|
1774
|
-
const stmt = this.db.prepare(`
|
|
1775
|
-
SELECT module, COUNT(*) as count
|
|
1776
|
-
FROM doc_routes
|
|
1777
|
-
GROUP BY module
|
|
1778
|
-
ORDER BY module
|
|
1779
|
-
`);
|
|
1780
|
-
return stmt.all();
|
|
1781
|
-
} catch (error) {
|
|
1782
|
-
Logger_default.error("\u83B7\u53D6\u6A21\u5757\u5217\u8868\u5931\u8D25:", error);
|
|
1783
|
-
return [];
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
/**
|
|
1787
|
-
* 获取路由列表
|
|
1788
|
-
* @param module 模块名称(可选)
|
|
1789
|
-
* @returns 路由列表
|
|
1790
|
-
*/
|
|
1791
|
-
getRoutes(module) {
|
|
1792
|
-
if (!this.db) return [];
|
|
1793
|
-
try {
|
|
1794
|
-
let query = `
|
|
1795
|
-
SELECT id, module, method, path, full_path, summary, description,
|
|
1796
|
-
request_schema, response_schema, middlewares, file_path, created_at, updated_at
|
|
1797
|
-
FROM doc_routes
|
|
1798
|
-
`;
|
|
1799
|
-
const params = {};
|
|
1800
|
-
if (module) {
|
|
1801
|
-
query += " WHERE module = @module";
|
|
1802
|
-
params.module = module;
|
|
1803
|
-
}
|
|
1804
|
-
query += " ORDER BY module, method, path";
|
|
1805
|
-
const stmt = this.db.prepare(query);
|
|
1806
|
-
const rows = stmt.all(params);
|
|
1807
|
-
return rows.map((row) => ({
|
|
1808
|
-
id: row.id,
|
|
1809
|
-
module: row.module,
|
|
1810
|
-
method: row.method,
|
|
1811
|
-
path: row.path,
|
|
1812
|
-
fullPath: row.full_path,
|
|
1813
|
-
summary: row.summary,
|
|
1814
|
-
description: row.description,
|
|
1815
|
-
requestSchema: row.request_schema ? JSON.parse(row.request_schema) : void 0,
|
|
1816
|
-
responseSchema: row.response_schema ? JSON.parse(row.response_schema) : void 0,
|
|
1817
|
-
middlewares: row.middlewares ? JSON.parse(row.middlewares) : void 0,
|
|
1818
|
-
filePath: row.file_path,
|
|
1819
|
-
createdAt: row.created_at,
|
|
1820
|
-
updatedAt: row.updated_at
|
|
1821
|
-
}));
|
|
1822
|
-
} catch (error) {
|
|
1823
|
-
Logger_default.error("\u83B7\u53D6\u8DEF\u7531\u5217\u8868\u5931\u8D25:", error);
|
|
1824
|
-
return [];
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
/**
|
|
1828
|
-
* 获取路由详情
|
|
1829
|
-
* @param id 路由 ID
|
|
1830
|
-
* @returns 路由详情
|
|
1831
|
-
*/
|
|
1832
|
-
getRouteById(id) {
|
|
1833
|
-
if (!this.db) return null;
|
|
1834
|
-
try {
|
|
1835
|
-
const stmt = this.db.prepare(`
|
|
1836
|
-
SELECT id, module, method, path, full_path, summary, description,
|
|
1837
|
-
request_schema, response_schema, middlewares, file_path, created_at, updated_at
|
|
1838
|
-
FROM doc_routes
|
|
1839
|
-
WHERE id = @id
|
|
1840
|
-
`);
|
|
1841
|
-
const row = stmt.get({ id });
|
|
1842
|
-
if (!row) return null;
|
|
1843
|
-
return {
|
|
1844
|
-
id: row.id,
|
|
1845
|
-
module: row.module,
|
|
1846
|
-
method: row.method,
|
|
1847
|
-
path: row.path,
|
|
1848
|
-
fullPath: row.full_path,
|
|
1849
|
-
summary: row.summary,
|
|
1850
|
-
description: row.description,
|
|
1851
|
-
requestSchema: row.request_schema ? JSON.parse(row.request_schema) : void 0,
|
|
1852
|
-
responseSchema: row.response_schema ? JSON.parse(row.response_schema) : void 0,
|
|
1853
|
-
middlewares: row.middlewares ? JSON.parse(row.middlewares) : void 0,
|
|
1854
|
-
filePath: row.file_path,
|
|
1855
|
-
createdAt: row.created_at,
|
|
1856
|
-
updatedAt: row.updated_at
|
|
1857
|
-
};
|
|
1858
|
-
} catch (error) {
|
|
1859
|
-
Logger_default.error("\u83B7\u53D6\u8DEF\u7531\u8BE6\u60C5\u5931\u8D25:", error);
|
|
1860
|
-
return null;
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
/**
|
|
1864
|
-
* 清空所有路由数据
|
|
1865
|
-
*/
|
|
1866
|
-
clearAllRoutes() {
|
|
1867
|
-
if (!this.db) return;
|
|
1868
|
-
try {
|
|
1869
|
-
this.db.exec("DELETE FROM doc_routes");
|
|
1870
|
-
} catch (error) {
|
|
1871
|
-
Logger_default.error("\u6E05\u7A7A\u8DEF\u7531\u6570\u636E\u5931\u8D25:", error);
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
/**
|
|
1875
|
-
* 关闭数据库连接
|
|
1876
|
-
*/
|
|
1877
|
-
close() {
|
|
1878
|
-
if (this.db) {
|
|
1879
|
-
this.db.close();
|
|
1880
|
-
this.db = null;
|
|
1881
|
-
Logger_default.info("\u6587\u6863\u6570\u636E\u5E93\u8FDE\u63A5\u5DF2\u5173\u95ED");
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
};
|
|
1885
|
-
var docManager;
|
|
1886
|
-
function initDocManager(config2) {
|
|
1887
|
-
docManager = new DocManager(config2);
|
|
1888
|
-
return docManager;
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
// utils/RouteRegistry.ts
|
|
1892
|
-
import path5 from "path";
|
|
1893
|
-
function getRoutes(router2, prefix = "") {
|
|
1894
|
-
const routes = [];
|
|
1895
|
-
if (router2 && router2.stack) {
|
|
1896
|
-
router2.stack.forEach((layer) => {
|
|
1897
|
-
if (layer.route) {
|
|
1898
|
-
const path9 = (prefix + layer.route.path).replace(/\/+/g, "/");
|
|
1899
|
-
const methods = Object.keys(layer.route.methods).map((m) => m.toUpperCase());
|
|
1900
|
-
methods.forEach((method) => {
|
|
1901
|
-
routes.push({ path: path9, method });
|
|
1902
|
-
});
|
|
1903
|
-
} else if (layer.name === "router" && layer.handle && layer.handle.stack) {
|
|
1904
|
-
const newPrefix = (prefix + (layer.regexp.source.replace("^\\/", "").replace("\\/?(?=\\/|$)", "") || "")).replace(/\/+/g, "/");
|
|
1905
|
-
routes.push(...getRoutes(layer.handle, newPrefix));
|
|
1906
|
-
}
|
|
1907
|
-
});
|
|
1908
|
-
}
|
|
1909
|
-
return routes;
|
|
1910
|
-
}
|
|
1911
|
-
async function registerRoute(app, config2, apiPrefix = "/api") {
|
|
1912
|
-
const { fileName, defaultExport: handler, filePath, cacheHit } = config2;
|
|
1913
|
-
try {
|
|
1914
|
-
const lowercaseModuleName = fileName.toLowerCase();
|
|
1915
|
-
if (!cacheHit) {
|
|
1916
|
-
const routes = routeParser.parseRouteFile(filePath);
|
|
1917
|
-
routes.forEach((route) => {
|
|
1918
|
-
docManager.saveRoute(route, apiPrefix, lowercaseModuleName);
|
|
1919
|
-
});
|
|
1920
|
-
}
|
|
1921
|
-
const filePathParts = filePath.split(path5.sep);
|
|
1922
|
-
const routesIndex = filePathParts.lastIndexOf("routes");
|
|
1923
|
-
if (routesIndex === -1) {
|
|
1924
|
-
throw new Error(`\u8DEF\u7531\u6587\u4EF6\u5FC5\u987B\u5728 routes \u76EE\u5F55\u4E0B: ${filePath}`);
|
|
1925
|
-
}
|
|
1926
|
-
const routesDir = filePathParts.slice(0, routesIndex + 1).join(path5.sep);
|
|
1927
|
-
const fileDir = path5.dirname(filePath);
|
|
1928
|
-
const relativePath = path5.relative(routesDir, fileDir);
|
|
1929
|
-
let pathParts = [];
|
|
1930
|
-
if (relativePath && relativePath !== ".") {
|
|
1931
|
-
pathParts = relativePath.split(path5.sep).map((p) => p.toLowerCase());
|
|
1932
|
-
}
|
|
1933
|
-
pathParts.push(lowercaseModuleName);
|
|
1934
|
-
const baseRoutePath = `${apiPrefix}/${pathParts.join("/")}`.replace(/\/+/g, "/");
|
|
1935
|
-
app.use(baseRoutePath, handler);
|
|
1936
|
-
const subRoutes = getRoutes(handler);
|
|
1937
|
-
if (subRoutes.length > 0) {
|
|
1938
|
-
Logger_default.startup("\u8DEF\u7531", `${baseRoutePath} (${subRoutes.length} \u4E2A\u7AEF\u70B9)`);
|
|
1939
|
-
subRoutes.forEach((route) => {
|
|
1940
|
-
const fullPath = `${baseRoutePath}${route.path}`.replace(/\/+/g, "/");
|
|
1941
|
-
const method = route.method.padEnd(6, " ");
|
|
1942
|
-
Logger_default.startup(` \u2514\u2500 ${method} ${fullPath}`);
|
|
1943
|
-
});
|
|
1944
|
-
} else {
|
|
1945
|
-
Logger_default.startup("\u8DEF\u7531", `${baseRoutePath} (\u65E0\u7AEF\u70B9)`);
|
|
1946
|
-
}
|
|
1947
|
-
} catch (error) {
|
|
1948
|
-
Logger_default.error(`\u6CE8\u518C\u8DEF\u7531\u5931\u8D25[${fileName.toLowerCase()}]: ${error}`);
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
// utils/ComponentRegistry.ts
|
|
1953
|
-
async function registerComponents(state2, scannedResults) {
|
|
1954
|
-
for (const item of scannedResults) {
|
|
1955
|
-
if (item.type === "middleware") {
|
|
1956
|
-
registerMiddleware(state2.express, item);
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
for (const item of scannedResults) {
|
|
1960
|
-
if (item.type === "route") {
|
|
1961
|
-
registerRoute(state2.express, item, state2.options.apiPrefix);
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
const syncAppRoutes = state2.express.get("syncAppRoutes");
|
|
1965
|
-
if (syncAppRoutes && typeof syncAppRoutes === "function") {
|
|
1966
|
-
try {
|
|
1967
|
-
await syncAppRoutes();
|
|
1968
|
-
} catch (error) {
|
|
1969
|
-
Logger_default.error("[ComponentRegistry] \u8DEF\u7531\u8D44\u6E90\u540C\u6B65\u5931\u8D25:", error);
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
// utils/InitErrorHandler.ts
|
|
1975
|
-
import { ZodError } from "zod";
|
|
1976
|
-
function isSQLError(err) {
|
|
1977
|
-
if (!err) return false;
|
|
1978
|
-
const msg = err.message || err.toString() || "";
|
|
1979
|
-
return /no such table|table.*does not exist|column.*does not exist|database schema has changed/i.test(msg);
|
|
1980
|
-
}
|
|
1981
|
-
function setupErrorHandlers(app, logging) {
|
|
1982
|
-
app.use((req, res) => {
|
|
1983
|
-
res.status(404).json({
|
|
1984
|
-
code: 404,
|
|
1985
|
-
data: null,
|
|
1986
|
-
msg: "\u8D44\u6E90\u4E0D\u5B58\u5728",
|
|
1987
|
-
error: "Not Found",
|
|
1988
|
-
fields: {}
|
|
1989
|
-
});
|
|
1990
|
-
});
|
|
1991
|
-
app.use((err, req, res, next) => {
|
|
1992
|
-
if (err.isAppError || err.statusCode && err.statusCode >= 400 && err.statusCode < 500) {
|
|
1993
|
-
return res.status(err.statusCode).json({
|
|
1994
|
-
code: err.statusCode,
|
|
1995
|
-
data: null,
|
|
1996
|
-
msg: err.message,
|
|
1997
|
-
error: "BusinessError",
|
|
1998
|
-
fields: {}
|
|
1999
|
-
});
|
|
2000
|
-
}
|
|
2001
|
-
if (err instanceof ZodError) {
|
|
2002
|
-
const fields = {};
|
|
2003
|
-
err.errors.forEach((e) => {
|
|
2004
|
-
const path9 = e.path.join(".");
|
|
2005
|
-
fields[path9] = e.message;
|
|
2006
|
-
});
|
|
2007
|
-
return res.status(400).json({
|
|
2008
|
-
code: 400,
|
|
2009
|
-
data: null,
|
|
2010
|
-
msg: "\u53C2\u6570\u6821\u9A8C\u5931\u8D25",
|
|
2011
|
-
error: "ZodValidationError",
|
|
2012
|
-
fields
|
|
2013
|
-
});
|
|
2014
|
-
}
|
|
2015
|
-
if (isSQLError(err)) {
|
|
2016
|
-
Logger_default.error("");
|
|
2017
|
-
Logger_default.error("\u274C \u6570\u636E\u5E93\u7ED3\u6784\u53EF\u80FD\u5DF2\u53D8\u5316\uFF0C\u8BF7\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u540C\u6B65\uFF1A");
|
|
2018
|
-
Logger_default.error("");
|
|
2019
|
-
Logger_default.error(" npm run db:migrate");
|
|
2020
|
-
Logger_default.error("");
|
|
2021
|
-
}
|
|
2022
|
-
const statusCode = err.status || err.statusCode || 500;
|
|
2023
|
-
if (!isSQLError(err)) {
|
|
2024
|
-
Logger_default.error("\u7CFB\u7EDF\u9519\u8BEF:", err);
|
|
2025
|
-
}
|
|
2026
|
-
res.status(statusCode).json({
|
|
2027
|
-
code: statusCode,
|
|
2028
|
-
data: null,
|
|
2029
|
-
msg: err.message || "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF",
|
|
2030
|
-
error: logging ? err.stack : "InternalServerError",
|
|
2031
|
-
fields: {}
|
|
2032
|
-
});
|
|
2033
|
-
});
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
// utils/HttpServer.ts
|
|
2037
|
-
async function startServer(app, port) {
|
|
2038
|
-
if (!app) {
|
|
2039
|
-
throw new Error("Express \u5B9E\u4F8B\u4E0D\u80FD\u4E3A\u7A7A");
|
|
2040
|
-
}
|
|
2041
|
-
if (!port || typeof port !== "number" || port <= 0 || port > 65535) {
|
|
2042
|
-
throw new Error(`\u65E0\u6548\u7684\u7AEF\u53E3\u53F7: ${port}`);
|
|
2043
|
-
}
|
|
2044
|
-
return new Promise((resolve3, reject) => {
|
|
2045
|
-
try {
|
|
2046
|
-
const server = app.listen(port, () => {
|
|
2047
|
-
resolve3();
|
|
2048
|
-
});
|
|
2049
|
-
server.on("error", (error) => {
|
|
2050
|
-
Logger_default.error("\u670D\u52A1\u5668\u542F\u52A8\u5931\u8D25\uFF1A", error);
|
|
2051
|
-
reject(new Error(`\u670D\u52A1\u5668\u542F\u52A8\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`));
|
|
2052
|
-
});
|
|
2053
|
-
server.on("close", () => {
|
|
2054
|
-
Logger_default.info("\u670D\u52A1\u5668\u5DF2\u5173\u95ED");
|
|
2055
|
-
});
|
|
2056
|
-
} catch (error) {
|
|
2057
|
-
Logger_default.error("\u670D\u52A1\u5668\u542F\u52A8\u5F02\u5E38\uFF1A", error);
|
|
2058
|
-
reject(new Error(`\u670D\u52A1\u5668\u542F\u52A8\u5F02\u5E38\uFF1A${error instanceof Error ? error.message : String(error)}`));
|
|
2059
|
-
}
|
|
2060
|
-
});
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
|
-
// routes/Doc.route.ts
|
|
2064
|
-
import { Router } from "express";
|
|
2065
|
-
|
|
2066
|
-
// utils/Validate.ts
|
|
2067
|
-
var validate = (schema, target = "body") => {
|
|
2068
|
-
return async (req, res, next) => {
|
|
2069
|
-
try {
|
|
2070
|
-
await schema.parseAsync(req[target]);
|
|
2071
|
-
next();
|
|
2072
|
-
} catch (error) {
|
|
2073
|
-
next(error);
|
|
2074
|
-
}
|
|
2075
|
-
};
|
|
2076
|
-
};
|
|
2077
|
-
|
|
2078
|
-
// zod/Doc.schema.ts
|
|
2079
|
-
import { z } from "zod";
|
|
2080
|
-
var routeIdParamsSchema = z.object({
|
|
2081
|
-
id: z.coerce.number().int().positive("\u65E0\u6548\u7684\u8DEF\u7531ID")
|
|
2082
|
-
});
|
|
2083
|
-
|
|
2084
|
-
// routes/Doc.route.ts
|
|
2085
|
-
var router = Router();
|
|
2086
|
-
router.get("/modules", (req, res) => {
|
|
2087
|
-
try {
|
|
2088
|
-
const modules = docManager.getModules();
|
|
2089
|
-
const response = {
|
|
2090
|
-
code: 200,
|
|
2091
|
-
data: modules,
|
|
2092
|
-
msg: "success"
|
|
2093
|
-
};
|
|
2094
|
-
res.json(response);
|
|
2095
|
-
} catch (error) {
|
|
2096
|
-
const response = {
|
|
2097
|
-
code: 500,
|
|
2098
|
-
data: null,
|
|
2099
|
-
msg: "\u83B7\u53D6\u6A21\u5757\u5217\u8868\u5931\u8D25"
|
|
2100
|
-
};
|
|
2101
|
-
res.json(response);
|
|
2102
|
-
}
|
|
2103
|
-
});
|
|
2104
|
-
router.get("/routes", (req, res) => {
|
|
2105
|
-
try {
|
|
2106
|
-
const module = req.query.module;
|
|
2107
|
-
const routes = docManager.getRoutes(module);
|
|
2108
|
-
const response = {
|
|
2109
|
-
code: 200,
|
|
2110
|
-
data: routes,
|
|
2111
|
-
msg: "success"
|
|
2112
|
-
};
|
|
2113
|
-
res.json(response);
|
|
2114
|
-
} catch (error) {
|
|
2115
|
-
const response = {
|
|
2116
|
-
code: 500,
|
|
2117
|
-
data: null,
|
|
2118
|
-
msg: "\u83B7\u53D6\u8DEF\u7531\u5217\u8868\u5931\u8D25"
|
|
2119
|
-
};
|
|
2120
|
-
res.json(response);
|
|
2121
|
-
}
|
|
2122
|
-
});
|
|
2123
|
-
router.get("/routes/:id", validate(routeIdParamsSchema, "params"), (req, res) => {
|
|
2124
|
-
try {
|
|
2125
|
-
const { id } = req.params;
|
|
2126
|
-
const route = docManager.getRouteById(id);
|
|
2127
|
-
if (!route) {
|
|
2128
|
-
const response2 = {
|
|
2129
|
-
code: 404,
|
|
2130
|
-
data: null,
|
|
2131
|
-
msg: "\u8DEF\u7531\u4E0D\u5B58\u5728"
|
|
2132
|
-
};
|
|
2133
|
-
return res.json(response2);
|
|
2134
|
-
}
|
|
2135
|
-
const response = {
|
|
2136
|
-
code: 200,
|
|
2137
|
-
data: route,
|
|
2138
|
-
msg: "success"
|
|
2139
|
-
};
|
|
2140
|
-
res.json(response);
|
|
2141
|
-
} catch (error) {
|
|
2142
|
-
const response = {
|
|
2143
|
-
code: 500,
|
|
2144
|
-
data: null,
|
|
2145
|
-
msg: "\u83B7\u53D6\u8DEF\u7531\u8BE6\u60C5\u5931\u8D25"
|
|
2146
|
-
};
|
|
2147
|
-
res.json(response);
|
|
2148
|
-
}
|
|
2149
|
-
});
|
|
2150
|
-
router.post("/sync", (req, res) => {
|
|
2151
|
-
try {
|
|
2152
|
-
const response = {
|
|
2153
|
-
code: 200,
|
|
2154
|
-
data: true,
|
|
2155
|
-
msg: "\u540C\u6B65\u6210\u529F"
|
|
2156
|
-
};
|
|
2157
|
-
res.json(response);
|
|
2158
|
-
} catch (error) {
|
|
2159
|
-
const response = {
|
|
2160
|
-
code: 500,
|
|
2161
|
-
data: null,
|
|
2162
|
-
msg: "\u540C\u6B65\u5931\u8D25"
|
|
2163
|
-
};
|
|
2164
|
-
res.json(response);
|
|
2165
|
-
}
|
|
2166
|
-
});
|
|
2167
|
-
var Doc_route_default = router;
|
|
2168
|
-
|
|
2169
|
-
// commands/prepare.ts
|
|
2170
|
-
import fs5 from "fs";
|
|
2171
|
-
import path6 from "path";
|
|
2172
|
-
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
2173
|
-
function generateDrizzleSchemasJson(scannedResults, cwd = process.cwd()) {
|
|
2174
|
-
const ormSchemaFiles = scannedResults.filter((r) => r.type === "schema" && r.filePath.includes("/orm/")).map((f) => path6.relative(cwd, f.filePath).replace(/\\/g, "/"));
|
|
2175
|
-
const schemaJsonPath = path6.join(cwd, ".drizzle-schemas.json");
|
|
2176
|
-
fs5.writeFileSync(schemaJsonPath, JSON.stringify(ormSchemaFiles, null, 2));
|
|
2177
|
-
console.log(`\u{1F4C4} \u51C6\u5907\u6A21\u5F0F\uFF1A\u751F\u6210 schema \u6E05\u5355...`);
|
|
2178
|
-
console.log(`\u2705 \u5DF2\u751F\u6210 ${ormSchemaFiles.length} \u4E2A schema \u6587\u4EF6\u8DEF\u5F84`);
|
|
2179
|
-
console.log(`\u{1F4C1} \u6587\u4EF6\u4F4D\u7F6E: ${schemaJsonPath}`);
|
|
2180
|
-
}
|
|
2181
|
-
async function prepare(cwd = process.cwd()) {
|
|
2182
|
-
console.log("\u{1F680} MindBase \u73AF\u5883\u51C6\u5907\u5DE5\u5177");
|
|
2183
|
-
console.log(`\u5DE5\u4F5C\u76EE\u5F55: ${cwd}
|
|
2184
|
-
`);
|
|
2185
|
-
const appPath = path6.join(cwd, "src/app.ts");
|
|
2186
|
-
if (!fs5.existsSync(appPath)) {
|
|
2187
|
-
console.error("\u274C \u672A\u627E\u5230 src/app.ts");
|
|
2188
|
-
process.exit(1);
|
|
2189
|
-
}
|
|
2190
|
-
try {
|
|
2191
|
-
console.log("\u{1F4C4} \u52A0\u8F7D\u5BBF\u4E3B\u5E94\u7528...");
|
|
2192
|
-
process.env.PREPARE_MODE = "true";
|
|
2193
|
-
await import(pathToFileURL2(appPath).href);
|
|
2194
|
-
console.log("\n\u2705 \u73AF\u5883\u51C6\u5907\u5B8C\u6210\uFF01");
|
|
2195
|
-
} catch (error) {
|
|
2196
|
-
console.error("\n\u274C \u51C6\u5907\u5931\u8D25:", error);
|
|
2197
|
-
process.exit(1);
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
async function precache(cwd) {
|
|
2201
|
-
return prepare(cwd);
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
// core/app.ts
|
|
2205
|
-
function createApp(options = {}) {
|
|
2206
|
-
Logger_default.startup("\u521D\u59CB\u5316", "\u5E94\u7528\u521D\u59CB\u5316\u2026\u2026");
|
|
2207
|
-
const stateInstance = createState(options);
|
|
2208
|
-
const plugins = [];
|
|
2209
|
-
if (stateInstance.options.logLevel) {
|
|
2210
|
-
Logger_default.setLevel(stateInstance.options.logLevel);
|
|
2211
|
-
}
|
|
2212
|
-
const app = {
|
|
2213
|
-
use: async (plugin) => {
|
|
2214
|
-
if (typeof plugin.install === "function") {
|
|
2215
|
-
plugins.push(plugin);
|
|
2216
|
-
}
|
|
2217
|
-
if (typeof plugin.__modulePath === "string") {
|
|
2218
|
-
Logger_default.startup("\u6A21\u5757", `\u6CE8\u518C\uFF1A${plugin.__modulePath}`);
|
|
2219
|
-
addScanPath(plugin.__modulePath);
|
|
2220
|
-
}
|
|
2221
|
-
},
|
|
2222
|
-
startup: async () => {
|
|
2223
|
-
const isPrepareMode = process.env.PREPARE_MODE === "true";
|
|
2224
|
-
if (!isPrepareMode) {
|
|
2225
|
-
initExpress(stateInstance);
|
|
2226
|
-
}
|
|
2227
|
-
await prepareScanEnvironment();
|
|
2228
|
-
const scannedResults = await scanComponents();
|
|
2229
|
-
if (isPrepareMode) {
|
|
2230
|
-
generateDrizzleSchemasJson(scannedResults, process.cwd());
|
|
2231
|
-
return;
|
|
2232
|
-
}
|
|
2233
|
-
await initializeDataStorage(scannedResults);
|
|
2234
|
-
await installPlugins();
|
|
2235
|
-
registerComponents(stateInstance, scannedResults);
|
|
2236
|
-
registerDocRoutes();
|
|
2237
|
-
setupErrorHandlers(stateInstance.express, stateInstance.options.logging);
|
|
2238
|
-
await startHttpServer();
|
|
2239
|
-
Logger_default.startup("\u521D\u59CB\u5316", "\u2705 \u5E94\u7528\u542F\u52A8\u5B8C\u6210");
|
|
2240
|
-
},
|
|
2241
|
-
getApp: () => stateInstance.express,
|
|
2242
|
-
getDB: () => stateInstance.db,
|
|
2243
|
-
getOptions: () => stateInstance.options
|
|
2244
|
-
};
|
|
2245
|
-
async function prepareScanEnvironment() {
|
|
2246
|
-
setBaseDir(process.cwd());
|
|
2247
|
-
Logger_default.startup("\u626B\u63CF", `\u57FA\u51C6\u76EE\u5F55: ${process.cwd()}`);
|
|
2248
|
-
}
|
|
2249
|
-
async function scanComponents() {
|
|
2250
|
-
const scannedResults = await scan();
|
|
2251
|
-
const routeFiles = scannedResults.filter((r) => r.type === "route");
|
|
2252
|
-
const cacheHits = routeFiles.filter((r) => r.cacheHit).length;
|
|
2253
|
-
const total = routeFiles.length;
|
|
2254
|
-
if (cacheHits > 0) {
|
|
2255
|
-
Logger_default.startup("\u626B\u63CF", `\u53D1\u73B0 ${scannedResults.length} \u4E2A\u7EC4\u4EF6\u6587\u4EF6 (\u7F13\u5B58\u547D\u4E2D: ${cacheHits}/${total})`);
|
|
2256
|
-
} else {
|
|
2257
|
-
Logger_default.startup("\u626B\u63CF", `\u53D1\u73B0 ${scannedResults.length} \u4E2A\u7EC4\u4EF6\u6587\u4EF6`);
|
|
2258
|
-
}
|
|
2259
|
-
return scannedResults;
|
|
2260
|
-
}
|
|
2261
|
-
async function initializeDataStorage(scannedResults) {
|
|
2262
|
-
setupDatabase(stateInstance, scannedResults);
|
|
2263
|
-
stateInstance.express.set("db", stateInstance.db);
|
|
2264
|
-
if (stateInstance.db) {
|
|
2265
|
-
const dbPath2 = stateInstance.options.database?.path || "./data/app.db";
|
|
2266
|
-
Logger_default.startup("\u6570\u636E\u5E93", `\u5DF2\u8FDE\u63A5: SQLite (${dbPath2})`);
|
|
2267
|
-
}
|
|
2268
|
-
const docDbPath = stateInstance.options.database?.path ? stateInstance.options.database.path.replace(/app\.db$/, "doc.db") : "./data/doc.db";
|
|
2269
|
-
initDocManager({ path: docDbPath });
|
|
2270
|
-
Logger_default.startup("\u6570\u636E\u5E93", `\u6587\u6863\u5E93\u5DF2\u521D\u59CB\u5316: ${docDbPath}`);
|
|
2271
|
-
}
|
|
2272
|
-
async function installPlugins() {
|
|
2273
|
-
for (const plugin of plugins) {
|
|
2274
|
-
await plugin.install(app);
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
async function startHttpServer() {
|
|
2278
|
-
const listenPort = stateInstance.options.port || 3e3;
|
|
2279
|
-
await startServer(stateInstance.express, listenPort);
|
|
2280
|
-
const serverUrl = `http://${stateInstance.options.host}:${listenPort}`;
|
|
2281
|
-
Logger_default.startup("\u670D\u52A1", `\u8BBF\u95EE\u5730\u5740: ${serverUrl}`);
|
|
2282
|
-
}
|
|
2283
|
-
function registerDocRoutes() {
|
|
2284
|
-
const apiPrefix = stateInstance.options.apiPrefix || "/api";
|
|
2285
|
-
const docRoutePath = `${apiPrefix}/doc`.replace(/\/+/g, "/");
|
|
2286
|
-
stateInstance.express.use(docRoutePath, Doc_route_default);
|
|
2287
|
-
stateInstance.options.authWhitelist.push(`${docRoutePath}/modules`);
|
|
2288
|
-
Logger_default.startup("\u8DEF\u7531", `${docRoutePath}/modules (GET)`);
|
|
2289
|
-
stateInstance.options.authWhitelist.push(`${docRoutePath}/routes`);
|
|
2290
|
-
Logger_default.startup("\u8DEF\u7531", `${docRoutePath}/routes (GET)`);
|
|
2291
|
-
stateInstance.options.authWhitelist.push(new RegExp(`^${docRoutePath}/routes/[^/]+$`));
|
|
2292
|
-
Logger_default.startup("\u8DEF\u7531", `${docRoutePath}/routes/:id (GET)`);
|
|
2293
|
-
stateInstance.options.authWhitelist.push(`${docRoutePath}/sync`);
|
|
2294
|
-
Logger_default.startup("\u8DEF\u7531", `${docRoutePath}/sync (POST)`);
|
|
2295
|
-
}
|
|
2296
|
-
return app;
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
// feature/cron/CronManager.ts
|
|
2300
|
-
import { CronJob } from "cron";
|
|
2301
|
-
import { randomUUID } from "crypto";
|
|
2302
|
-
var jobs = {};
|
|
2303
|
-
var config = {
|
|
2304
|
-
timezone: "Asia/Shanghai"
|
|
2305
|
-
};
|
|
2306
|
-
function addCron(pattern, handler, customId) {
|
|
2307
|
-
const id = customId || generateId();
|
|
2308
|
-
if (jobs[id]) {
|
|
2309
|
-
jobs[id].stop();
|
|
2310
|
-
}
|
|
2311
|
-
const job = new CronJob(pattern, handler, null, true, config.timezone);
|
|
2312
|
-
jobs[id] = job;
|
|
2313
|
-
return id;
|
|
2314
|
-
}
|
|
2315
|
-
function stopCron(id) {
|
|
2316
|
-
const job = jobs[id];
|
|
2317
|
-
if (job) {
|
|
2318
|
-
job.stop();
|
|
2319
|
-
delete jobs[id];
|
|
2320
|
-
return true;
|
|
2321
|
-
}
|
|
2322
|
-
return false;
|
|
2323
|
-
}
|
|
2324
|
-
function stopAllCron() {
|
|
2325
|
-
Object.values(jobs).forEach((job) => {
|
|
2326
|
-
job.stop();
|
|
2327
|
-
});
|
|
2328
|
-
jobs = {};
|
|
2329
|
-
}
|
|
2330
|
-
function generateId() {
|
|
2331
|
-
return randomUUID();
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
// core/module/FindPackageRoot.ts
|
|
2335
|
-
import path7 from "path";
|
|
2336
|
-
import fs6 from "fs";
|
|
2337
|
-
function findPackageRoot(startPath) {
|
|
2338
|
-
if (!startPath || typeof startPath !== "string") {
|
|
2339
|
-
throw new Error("\u65E0\u6548\u7684\u8D77\u59CB\u8DEF\u5F84\uFF1A\u8DEF\u5F84\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
2340
|
-
}
|
|
2341
|
-
let currentPath;
|
|
2342
|
-
try {
|
|
2343
|
-
currentPath = path7.isAbsolute(startPath) ? startPath : path7.resolve(startPath);
|
|
2344
|
-
} catch (e) {
|
|
2345
|
-
throw new Error(`\u65E0\u6CD5\u89E3\u6790\u8DEF\u5F84\uFF1A${startPath}`);
|
|
2346
|
-
}
|
|
2347
|
-
try {
|
|
2348
|
-
if (!fs6.existsSync(currentPath)) {
|
|
2349
|
-
throw new Error(`\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A${currentPath}`);
|
|
2350
|
-
}
|
|
2351
|
-
const stat = fs6.statSync(currentPath);
|
|
2352
|
-
if (stat.isFile()) {
|
|
2353
|
-
currentPath = path7.dirname(currentPath);
|
|
2354
|
-
}
|
|
2355
|
-
} catch (e) {
|
|
2356
|
-
throw new Error(`\u8DEF\u5F84\u9A8C\u8BC1\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
2357
|
-
}
|
|
2358
|
-
while (currentPath !== path7.parse(currentPath).root) {
|
|
2359
|
-
try {
|
|
2360
|
-
const pkgJsonPath = path7.join(currentPath, "package.json");
|
|
2361
|
-
if (fs6.existsSync(pkgJsonPath)) {
|
|
2362
|
-
const pkgStat = fs6.statSync(pkgJsonPath);
|
|
2363
|
-
if (pkgStat.isFile()) {
|
|
2364
|
-
return currentPath;
|
|
2365
|
-
}
|
|
2366
|
-
}
|
|
2367
|
-
} catch (e) {
|
|
2368
|
-
}
|
|
2369
|
-
currentPath = path7.dirname(currentPath);
|
|
2370
|
-
}
|
|
2371
|
-
return path7.dirname(startPath);
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
|
-
// core/module/GetModulePath.ts
|
|
2375
|
-
import * as path8 from "path";
|
|
2376
|
-
function getModulePath() {
|
|
2377
|
-
try {
|
|
2378
|
-
const error = new Error();
|
|
2379
|
-
if (error.stack) {
|
|
2380
|
-
console.log("DEBUG STACK:", error.stack);
|
|
2381
|
-
const stackLines = error.stack.split("\n");
|
|
2382
|
-
for (const line of stackLines) {
|
|
2383
|
-
if (!line.includes("at") || line.includes("getModulePath") || line.includes("createModule") || line.includes("CreateModule.ts") || line.includes("ts-node") || line.includes("node:internal")) {
|
|
2384
|
-
continue;
|
|
2385
|
-
}
|
|
2386
|
-
const match = line.match(/\(([^:]+\.ts)/) || line.match(/at\s+([^:]+\.ts)/);
|
|
2387
|
-
if (match && match[1]) {
|
|
2388
|
-
const filePath = match[1];
|
|
2389
|
-
console.log("\u63D0\u53D6\u5230\u7684\u8DEF\u5F84:", filePath);
|
|
2390
|
-
return path8.resolve(filePath);
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
}
|
|
2394
|
-
} catch (e) {
|
|
2395
|
-
}
|
|
2396
|
-
throw new Error("\u65E0\u6CD5\u786E\u5B9A\u6A21\u5757\u8DEF\u5F84\uFF1A\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301 __filename \u4E14\u65E0\u6CD5\u4ECE\u8C03\u7528\u6808\u4E2D\u63D0\u53D6\u8DEF\u5F84");
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
// core/module/CreateModule.ts
|
|
2400
|
-
function createModule(install, callerPath) {
|
|
2401
|
-
if (typeof install !== "function") {
|
|
2402
|
-
throw new Error("\u6A21\u5757\u5B89\u88C5\u51FD\u6570\u5FC5\u987B\u662F\u4E00\u4E2A\u51FD\u6570");
|
|
2403
|
-
}
|
|
2404
|
-
try {
|
|
2405
|
-
const modulePath = callerPath || getModulePath();
|
|
2406
|
-
const packageRoot = findPackageRoot(modulePath);
|
|
2407
|
-
Logger_default.debug(`\u521B\u5EFA\u6A21\u5757\uFF1A\u8DEF\u5F84=${modulePath}\uFF0C\u5305\u6839\u76EE\u5F55=${packageRoot}`);
|
|
2408
|
-
return {
|
|
2409
|
-
install,
|
|
2410
|
-
__modulePath: packageRoot
|
|
2411
|
-
};
|
|
2412
|
-
} catch (error) {
|
|
2413
|
-
Logger_default.error("\u6A21\u5757\u521B\u5EFA\u5931\u8D25\uFF1A", error);
|
|
2414
|
-
throw new Error(`\u6A21\u5757\u521B\u5EFA\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
// utils/AppError.ts
|
|
2419
|
-
function createAppError(code, msg) {
|
|
2420
|
-
const err = new Error(msg);
|
|
2421
|
-
err.statusCode = code;
|
|
2422
|
-
err.isAppError = true;
|
|
2423
|
-
return err;
|
|
2424
|
-
}
|
|
2425
|
-
var Errors = {
|
|
2426
|
-
badRequest: (msg) => createAppError(400, msg),
|
|
2427
|
-
unauthorized: (msg) => createAppError(401, msg),
|
|
2428
|
-
forbidden: (msg) => createAppError(403, msg),
|
|
2429
|
-
notFound: (msg) => createAppError(404, msg)
|
|
2430
|
-
};
|
|
2431
|
-
export {
|
|
2432
|
-
DocManager,
|
|
2433
|
-
Errors,
|
|
2434
|
-
addCron,
|
|
2435
|
-
createApp,
|
|
2436
|
-
createAppError,
|
|
2437
|
-
createModule,
|
|
2438
|
-
dayjs,
|
|
2439
|
-
docManager,
|
|
2440
|
-
initDatabase,
|
|
2441
|
-
initDocManager,
|
|
2442
|
-
logger,
|
|
2443
|
-
precache,
|
|
2444
|
-
prepare,
|
|
2445
|
-
stopAllCron,
|
|
2446
|
-
stopCron,
|
|
2447
|
-
validate
|
|
2448
|
-
};
|
|
2449
|
-
//# sourceMappingURL=index.mjs.map
|