@jayfong/x-server 2.118.0 → 2.119.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/lib/_cjs/cli/api_generator.js +26 -6
  2. package/lib/_cjs/cli/cli.js +0 -0
  3. package/lib/_cjs/cli/dev_util.js +2 -1
  4. package/lib/_cjs/core/define_task.js +2 -2
  5. package/lib/_cjs/core/handler.js +4 -4
  6. package/lib/_cjs/core/server.js +6 -8
  7. package/lib/_cjs/plugins/compress.js +1 -1
  8. package/lib/_cjs/plugins/cookie.js +1 -1
  9. package/lib/_cjs/plugins/form_body_parser.js +1 -1
  10. package/lib/_cjs/plugins/ws_parser.js +1 -1
  11. package/lib/_cjs/plugins/xml_parser.js +1 -1
  12. package/lib/_cjs/services/captcha.js +3 -3
  13. package/lib/_cjs/services/dingtalk.js +2 -2
  14. package/lib/_cjs/services/emoji.js +1 -1
  15. package/lib/_cjs/services/log.js +2 -2
  16. package/lib/_cjs/services/pay.js +5 -6
  17. package/lib/_cjs/services/sensitive_words.js +0 -1
  18. package/lib/cli/api_generator.js +26 -6
  19. package/lib/cli/dev_util.js +2 -1
  20. package/lib/core/ctx_storage.d.ts +1 -1
  21. package/lib/core/define_bus.d.ts +1 -1
  22. package/lib/core/define_cron.d.ts +1 -1
  23. package/lib/core/define_task.js +1 -1
  24. package/lib/core/get_handler_url.js +2 -2
  25. package/lib/core/handler.js +4 -4
  26. package/lib/core/http_method.d.ts +2 -2
  27. package/lib/core/server.d.ts +1 -1
  28. package/lib/core/server.js +6 -8
  29. package/lib/core/types.d.ts +2 -2
  30. package/lib/plugins/base.d.ts +1 -1
  31. package/lib/plugins/compress.d.ts +3 -3
  32. package/lib/plugins/compress.js +1 -1
  33. package/lib/plugins/cookie.d.ts +3 -3
  34. package/lib/plugins/cookie.js +1 -1
  35. package/lib/plugins/cors.d.ts +4 -4
  36. package/lib/plugins/cors.js +1 -2
  37. package/lib/plugins/file_parser.d.ts +3 -3
  38. package/lib/plugins/form_body_parser.d.ts +3 -3
  39. package/lib/plugins/form_body_parser.js +1 -1
  40. package/lib/plugins/ws_parser.d.ts +3 -3
  41. package/lib/plugins/ws_parser.js +1 -1
  42. package/lib/plugins/xml_parser.d.ts +3 -3
  43. package/lib/plugins/xml_parser.js +1 -1
  44. package/lib/services/captcha.d.ts +1 -1
  45. package/lib/services/captcha.js +3 -3
  46. package/lib/services/crypto.d.ts +1 -1
  47. package/lib/services/dingtalk.d.ts +1 -1
  48. package/lib/services/dingtalk.js +1 -1
  49. package/lib/services/dispose.d.ts +1 -1
  50. package/lib/services/emoji.d.ts +1 -1
  51. package/lib/services/emoji.js +1 -1
  52. package/lib/services/html.d.ts +1 -1
  53. package/lib/services/log.d.ts +2 -2
  54. package/lib/services/log.js +1 -1
  55. package/lib/services/mail.d.ts +1 -1
  56. package/lib/services/pay.d.ts +6 -8
  57. package/lib/services/pay.js +3 -4
  58. package/lib/services/rate_limit.d.ts +2 -2
  59. package/lib/services/redis.d.ts +2 -2
  60. package/lib/services/sensitive_words.d.ts +1 -2
  61. package/lib/services/sensitive_words.js +0 -1
  62. package/lib/services/sms.d.ts +1 -1
  63. package/package.json +34 -33
@@ -26,6 +26,7 @@ class ApiGenerator {
26
26
  // /user/index.ts
27
27
  // /post.ts
28
28
  // /user/list.ts
29
+ // category 必须从 handlers 目录结构中推导,后续会直接拼接到 URL 前缀
29
30
  this.categoryUrlRe = /^.+\/src\/handlers\/(?:index\.ts|(.*)\/index\.ts|(.*)\.ts)$/;
30
31
  this.checker = void 0;
31
32
  this.cwd = this.options.cwd || process.cwd();
@@ -36,7 +37,7 @@ class ApiGenerator {
36
37
  const handlersPath = _nodePath.default.join(this.cwd, 'node_modules/.x/handlers.ts');
37
38
  const entryFile = _nodePath.default.join(this.cwd, 'src/generated/handlers.ts');
38
39
 
39
- // 仅使用 ts-morph 初始化项目
40
+ // 仅使用 ts-morph 初始化项目,并把运行时生成的 handlers 聚合文件注入到 Program 中
40
41
  const moProject = new _tsMorph.default.Project({
41
42
  tsConfigFilePath: tsConfigPath
42
43
  });
@@ -45,11 +46,12 @@ class ApiGenerator {
45
46
  const checker = program.getTypeChecker();
46
47
  this.checker = checker;
47
48
  const entrySourceFile = program.getSourceFile(entryFile);
49
+ // handlers.ts 的每个 export 对应一个分类(如 user、post)
48
50
  const moduleSymbol = checker.getSymbolAtLocation(entrySourceFile);
49
51
  const categorySymbols = checker.getExportsOfModule(moduleSymbol);
50
52
  const apiData = [];
51
53
  for (const categorySymbol of categorySymbols) {
52
- // 分类的类型
54
+ // 分类的类型:形如 { list: defineHandler<...>, detail: defineHandler<...> }
53
55
  const categoryType = checker.getTypeOfSymbol(categorySymbol);
54
56
 
55
57
  // 处理器列表
@@ -81,7 +83,8 @@ class ApiGenerator {
81
83
  let handlerPath = [handlerSymbol.getName()];
82
84
  const handlerNode = handlerSymbol.getDeclarations()[0];
83
85
 
84
- // 支持自定义 requestPath
86
+ // 支持自定义 requestPath,仅识别字符串字面量或字符串数组字面量
87
+ // 这里不会执行表达式(例如变量、模板字符串、函数调用)
85
88
  if (
86
89
  // const xx = defineHandler
87
90
  _tsMorph.ts.isVariableDeclaration(handlerNode) && handlerNode.initializer &&
@@ -116,6 +119,9 @@ class ApiGenerator {
116
119
  }
117
120
  this.debug('生成接口: %s ...', handlerUrl);
118
121
  const handlerType = checker.getTypeAtLocation(handlerNode);
122
+
123
+ // 约定 defineHandler 的泛型顺序固定为 <Request, Response, Method>
124
+ // 若上游定义顺序变更,这里会静默解析错误
119
125
  const [requestType, responseType, methodType] = checker.getTypeArguments(handlerType);
120
126
  const handlerCategory = (0, _vtils.pascalCase)(categoryUrl) || 'Index';
121
127
  const handlerDescription = handlerComment.description || handlerUrl.join(', ');
@@ -129,6 +135,8 @@ class ApiGenerator {
129
135
  });
130
136
  const requestDataJsonSchema = this.apiDataToJsonSchema(requestData);
131
137
  const responseDataJsonSchema = this.apiDataToJsonSchema(responseData);
138
+
139
+ // 说明文档按“首段标题 + 空行 + 详情”拆分
132
140
  const handlerDescriptionDividerIndex = handlerDescription.indexOf('\n\n');
133
141
  const apiName = handlerDescriptionDividerIndex === -1 ? handlerDescription : handlerDescription.substring(0, handlerDescriptionDividerIndex);
134
142
  const apiDesc = handlerDescriptionDividerIndex === -1 ? '' : handlerDescription.substring(handlerDescriptionDividerIndex + 2);
@@ -167,6 +175,7 @@ class ApiGenerator {
167
175
  };
168
176
  }
169
177
  const description = _tsMorph.ts.displayPartsToString(symbol.getDocumentationComment(this.checker)).trim();
178
+ // JSDoc tag 统一收敛到 Map,方便按 @private 这类标签做开关
170
179
  const tags = symbol.getJsDocTags().map(item => {
171
180
  return {
172
181
  label: item.name,
@@ -187,6 +196,8 @@ class ApiGenerator {
187
196
  keySymbol = type.symbol || type.aliasSymbol
188
197
  }) {
189
198
  const hasTypeFlag = (flag, _type = type) => (_type.flags & flag) === flag;
199
+
200
+ // valueSymbol 用于识别“实际值类型”,keySymbol 用于保留字段名和字段注释
190
201
  let valueSymbol = type.symbol || type.aliasSymbol;
191
202
  const keySymbolName = keySymbol?.getName() || '';
192
203
  let valueSymbolName = valueSymbol?.getName() || '';
@@ -242,8 +253,8 @@ class ApiGenerator {
242
253
  };
243
254
  }
244
255
 
245
- // 联合类型
246
- // 布尔、枚举也会用这表示
256
+ // 联合类型:布尔字面量、枚举字面量都会走到这里
257
+ // 处理策略:忽略 null/undefined,用第一个成员推断基础类型,同时把所有字面量收集到 enum
247
258
  const rawUnionTypes = type.isUnion() && type.types || [];
248
259
  const unionTypes = rawUnionTypes.filter(item => !hasTypeFlag(_tsMorph.ts.TypeFlags.Null, item) && !hasTypeFlag(_tsMorph.ts.TypeFlags.Undefined, item));
249
260
  const isUnion = !!unionTypes.length;
@@ -274,6 +285,8 @@ class ApiGenerator {
274
285
  const isNumber = hasTypeFlag(_tsMorph.ts.TypeFlags.Number) || isNumberLiteral;
275
286
  const isBoolean = hasTypeFlag(_tsMorph.ts.TypeFlags.Boolean) || hasTypeFlag(_tsMorph.ts.TypeFlags.BooleanLiteral);
276
287
  const isLiteral = isStringLiteral || isNumberLiteral || isBigIntLiteral;
288
+
289
+ // object 兜底:只要不是数组/基础类型,且 TypeScript 视为 Object,就按对象递归
277
290
  const isObject = !isArray && !isString && !isNumber && !isBoolean && hasTypeFlag(_tsMorph.ts.TypeFlags.Object) ||
278
291
  // 交集类型视为对象
279
292
  isIntersection;
@@ -284,6 +297,8 @@ class ApiGenerator {
284
297
  const apiChildren = isArray ? [this.getApiDto({
285
298
  type: this.checker.getTypeArguments(type)[0]
286
299
  })] : isObject ? (0, _vtils.ii)(() => {
300
+ // 这里使用了 TypeChecker 的内部能力来合并交集类型字段
301
+ // 若 TypeScript 内部 API 变化,这里是高风险点
287
302
  const childSymbols = this.checker.getAllPossiblePropertiesOfTypes(intersectionTypes.length ? intersectionTypes : [type]);
288
303
  return childSymbols.map(childSymbol => {
289
304
  const childType = this.checker.getTypeOfSymbol(childSymbol);
@@ -303,6 +318,7 @@ class ApiGenerator {
303
318
  };
304
319
  }
305
320
  apiDataToJsonSchema(apiData, jsonSchema = {}) {
321
+ // 递归把内部 DTO 结构映射到 JSON Schema(以 object/array/primitive 三类分支处理)
306
322
  jsonSchema.description = apiData.desc;
307
323
  if (apiData.type === 'object') {
308
324
  jsonSchema.type = 'object';
@@ -327,7 +343,10 @@ class ApiGenerator {
327
343
  for (const handle of handles) {
328
344
  for (const path of handle.path) {
329
345
  data[handle.category] = data[handle.category] || [];
330
- data[handle.category].push(handle.handlerMethod === 'GET' ? {
346
+ data[handle.category].push(
347
+ // GET -> query;FILE -> form-data;其余 -> json body
348
+ // 此处是与 YApi 结构的协议转换层
349
+ handle.handlerMethod === 'GET' ? {
331
350
  method: handle.method.toUpperCase(),
332
351
  title: handle.name,
333
352
  markdown: handle.desc,
@@ -411,6 +430,7 @@ class ApiGenerator {
411
430
  tags: [handle.category],
412
431
  summary: handle.name,
413
432
  description: handle.desc,
433
+ // 目前统一按 application/json 输出;上传类接口未转为 multipart/form-data
414
434
  requestBody: {
415
435
  content: {
416
436
  'application/json': {
File without changes
@@ -12,7 +12,8 @@ class DevUtil {
12
12
  const tsxPkgFile = require.resolve('tsx/package.json');
13
13
  const tsxJsFile = tsxPkgFile.replace('/package.json', '/dist/cli.js');
14
14
  const tsxCjsFile = tsxPkgFile.replace('/package.json', '/dist/cli.cjs');
15
- return _nodeFs.default.existsSync(tsxCjsFile) ? tsxCjsFile : tsxJsFile;
15
+ const tsxMjsFile = tsxPkgFile.replace('/package.json', '/dist/cli.mjs');
16
+ return _nodeFs.default.existsSync(tsxCjsFile) ? tsxCjsFile : _nodeFs.default.existsSync(tsxMjsFile) ? tsxMjsFile : tsxJsFile;
16
17
  }), options.file], {
17
18
  cwd: options.cwd,
18
19
  stdio: 'inherit'
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  exports.__esModule = true;
5
5
  exports.defineSliceTask = defineSliceTask;
6
6
  exports.defineTask = defineTask;
7
- var _assert = _interopRequireDefault(require("assert"));
7
+ var _nodeAssert = _interopRequireDefault(require("node:assert"));
8
8
  var _bull = _interopRequireDefault(require("bull"));
9
9
  var _vtils = require("vtils");
10
10
  var _x = require("../x");
@@ -46,7 +46,7 @@ function defineSliceTask(options) {
46
46
  const duration = addOptions?.duration != null ? (0, _vtils.ms)(addOptions.duration) : typeof options.duration === 'function' ? (0, _vtils.ms)(options.duration(key)) : options.duration && (0, _vtils.ms)(options.duration);
47
47
  const threshold = addOptions?.threshold || typeof options.threshold && (typeof options.threshold === 'function' ? options.threshold(key) : options.threshold);
48
48
  const thresholdTimeout = addOptions?.thresholdTimeout != null ? (0, _vtils.ms)(addOptions.thresholdTimeout) : typeof options.thresholdTimeout === 'function' ? (0, _vtils.ms)(options.thresholdTimeout(key)) : options.thresholdTimeout && (0, _vtils.ms)(options.thresholdTimeout);
49
- (0, _assert.default)(duration != null || threshold != null, '参数 threshold 和 duration 必须至少设置 1 个');
49
+ (0, _nodeAssert.default)(duration != null || threshold != null, '参数 threshold 和 duration 必须至少设置 1 个');
50
50
  const redisKey = !key ? redisKeyPrefix : `${redisKeyPrefix}_${key}`;
51
51
  const res = await _x.x.redis.multi([['llen', redisKey], ['lpush', redisKey, JSON.stringify(data)]]).exec();
52
52
  const count = parseInt(res[0][1], 10) + 1;
@@ -131,11 +131,11 @@ class Handler {
131
131
  };
132
132
  this.handleWs = async (_data, ctx) => {
133
133
  const dispose = new _dispose.DisposeService();
134
- ctx.ws.socket.on('message', async payload => {
134
+ ctx.ws.on('message', async payload => {
135
135
  try {
136
136
  const _ = JSON.parse(payload.toString('utf8'));
137
137
  const send = data => {
138
- ctx.ws.socket.send(JSON.stringify({
138
+ ctx.ws.send(JSON.stringify({
139
139
  id: _.id,
140
140
  data: data
141
141
  }));
@@ -172,14 +172,14 @@ class Handler {
172
172
  }
173
173
  } catch (err) {
174
174
  const _err = _http_error.HttpError.isHttpError(err) ? err : new _http_error.HttpError.InternalServerError();
175
- ctx.ws.socket.send(JSON.stringify({
175
+ ctx.ws.send(JSON.stringify({
176
176
  statusCode: _err.statusCode,
177
177
  error: _err.name,
178
178
  message: _err.message
179
179
  }));
180
180
  }
181
181
  });
182
- ctx.ws.socket.on('close', () => {
182
+ ctx.ws.on('close', () => {
183
183
  dispose.dispose();
184
184
  });
185
185
  };
@@ -118,7 +118,7 @@ class Server {
118
118
  url,
119
119
  headers: req.headers,
120
120
  setHeader: (k, v) => res.header(k, v),
121
- redirect: (url, permanently) => res.redirect(permanently ? 301 : 302, url),
121
+ redirect: (url, permanently) => res.redirect(url, permanently ? 301 : 302),
122
122
  ws: undefined,
123
123
  req: req,
124
124
  res: res
@@ -188,15 +188,13 @@ class Server {
188
188
  if (Array.isArray(_x2.x.env.APP_STATIC_CONTENT)) {
189
189
  const staticContent = _x2.x.env.APP_STATIC_CONTENT;
190
190
  for (const item of staticContent) {
191
- if (!item.method) {
192
- item.method = 'GET';
193
- }
194
- if (!this.fastify.hasRoute({
195
- method: item.method,
191
+ const methods = (0, _vtils.castArray)(item.method);
192
+ if (!methods.some(method => this.fastify.hasRoute({
193
+ method,
196
194
  url: item.path
197
- })) {
195
+ }))) {
198
196
  this.fastify.route({
199
- method: item.method,
197
+ method: methods,
200
198
  url: item.path,
201
199
  handler: async (_req, res) => {
202
200
  if (!item.contentType && typeof item.content === 'string') {
@@ -12,7 +12,7 @@ class CompressPlugin {
12
12
  this.options = options;
13
13
  }
14
14
  register(fastify) {
15
- fastify.register(_compress.default, this.options);
15
+ fastify.register(_compress.default, this.options ?? {});
16
16
  }
17
17
  }
18
18
  exports.CompressPlugin = CompressPlugin;
@@ -12,7 +12,7 @@ class CookiePlugin {
12
12
  this.options = options;
13
13
  }
14
14
  register(fastify) {
15
- fastify.register(_cookie.default, this.options);
15
+ fastify.register(_cookie.default, this.options ?? {});
16
16
  }
17
17
  }
18
18
  exports.CookiePlugin = CookiePlugin;
@@ -12,7 +12,7 @@ class FormBodyParserPlugin {
12
12
  this.options = options;
13
13
  }
14
14
  register(fastify) {
15
- fastify.register(_formbody.default, this.options);
15
+ fastify.register(_formbody.default, this.options || {});
16
16
  }
17
17
  }
18
18
  exports.FormBodyParserPlugin = FormBodyParserPlugin;
@@ -12,7 +12,7 @@ class WsParserPlugin {
12
12
  this.options = options;
13
13
  }
14
14
  register(fastify) {
15
- fastify.register(_websocket.default, this.options);
15
+ fastify.register(_websocket.default, this.options ?? {});
16
16
  }
17
17
  }
18
18
  exports.WsParserPlugin = WsParserPlugin;
@@ -18,7 +18,7 @@ class XmlParserPlugin {
18
18
  ...parseOptions
19
19
  } = this.options || {};
20
20
  const xmlParser = new _fastXmlParser.XMLParser(parseOptions);
21
- fastify.addContentTypeParser(contentType, (req, payload, done) => {
21
+ fastify.addContentTypeParser(contentType, (_req, payload, done) => {
22
22
  let body = '';
23
23
  const handleError = err => done(err);
24
24
  const handleData = data => body += data;
@@ -13,9 +13,9 @@ class CaptchaService {
13
13
  this.charsetMap = {
14
14
  '09': '0123456789'.split(''),
15
15
  '19': '123456789'.split(''),
16
- 'az': 'abcdefghijklmnopqrstuvwxyz'.split(''),
17
- 'AZ': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
18
- 'aZ': 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
16
+ az: 'abcdefghijklmnopqrstuvwxyz'.split(''),
17
+ AZ: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
18
+ aZ: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
19
19
  '0z': '0123456789abcdefghijklmnopqrstuvwxyz'.split(''),
20
20
  '0Z': '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
21
21
  '1z': '123456789abcdefghijklmnopqrstuvwxyz'.split(''),
@@ -3,7 +3,7 @@
3
3
  var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4
4
  exports.__esModule = true;
5
5
  exports.DingtalkService = void 0;
6
- var _crypto = _interopRequireDefault(require("crypto"));
6
+ var _nodeCrypto = _interopRequireDefault(require("node:crypto"));
7
7
  var _got = _interopRequireDefault(require("got"));
8
8
  class DingtalkService {
9
9
  constructor(options) {
@@ -12,7 +12,7 @@ class DingtalkService {
12
12
  }
13
13
  async send(options) {
14
14
  const timestamp = Date.now();
15
- const sign = _crypto.default.createHmac('sha256', this.options.secret).update(`${timestamp}\n${this.options.secret}`).digest('base64');
15
+ const sign = _nodeCrypto.default.createHmac('sha256', this.options.secret).update(`${timestamp}\n${this.options.secret}`).digest('base64');
16
16
  const data = {
17
17
  msgtype: 'markdown',
18
18
  markdown: {
@@ -3,7 +3,7 @@
3
3
  exports.__esModule = true;
4
4
  exports.EmojiService = void 0;
5
5
  // softbankOrDocomoOrAuOrGoogleToUnified
6
- // prettier-ignore
6
+ // biome-ignore format: x
7
7
  const softbankOrDocomoOrAuOrGoogleToUnified = {
8
8
  "": "#️⃣",
9
9
  "󾠬": "#️⃣",
@@ -3,7 +3,7 @@
3
3
  var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4
4
  exports.__esModule = true;
5
5
  exports.LogService = void 0;
6
- var _path = _interopRequireDefault(require("path"));
6
+ var _nodePath = _interopRequireDefault(require("node:path"));
7
7
  var _fsExtra = _interopRequireDefault(require("fs-extra"));
8
8
  var _vtils = require("vtils");
9
9
  var _date = require("vtils/date");
@@ -68,7 +68,7 @@ class LogService {
68
68
  getLogFile(date) {
69
69
  const year = date.getFullYear();
70
70
  const month = date.getMonth() + 1;
71
- return _path.default.join(_x.x.dataDir, `log/${year}/${month < 10 ? `0${month}` : month}.log`);
71
+ return _nodePath.default.join(_x.x.dataDir, `log/${year}/${month < 10 ? `0${month}` : month}.log`);
72
72
  }
73
73
  get logFile() {
74
74
  return this.getLogFile(new Date());
@@ -3,7 +3,7 @@
3
3
  var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4
4
  exports.__esModule = true;
5
5
  exports.PayService = void 0;
6
- var _crypto = _interopRequireDefault(require("crypto"));
6
+ var _nodeCrypto = _interopRequireDefault(require("node:crypto"));
7
7
  var _alipaySdk = _interopRequireDefault(require("alipay-sdk"));
8
8
  var _form = _interopRequireDefault(require("alipay-sdk/lib/form"));
9
9
  var _got = _interopRequireDefault(require("got"));
@@ -44,9 +44,8 @@ class PayService {
44
44
  };
45
45
  }
46
46
  async prepareWepay(options) {
47
- const appId = options.appId || this.options.wepay.appId;
48
47
  const res = await this.wepayRequest('https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi', {
49
- appid: appId,
48
+ appid: this.options.wepay.appId,
50
49
  mchid: this.options.wepay.merchantId,
51
50
  description: options.goodsName,
52
51
  out_trade_no: options.tradeNumber,
@@ -65,7 +64,7 @@ class PayService {
65
64
  throw new Error(error);
66
65
  }
67
66
  const params = {
68
- appId: appId,
67
+ appId: this.options.wepay.appId,
69
68
  timestamp: String(Math.round(Date.now() / 1000)),
70
69
  nonceStr: Math.random().toString().substr(2, 12),
71
70
  package: `prepay_id=${res.prepay_id}`,
@@ -145,13 +144,13 @@ class PayService {
145
144
  };
146
145
  }
147
146
  wepaySign(data) {
148
- return _crypto.default.createSign('RSA-SHA256').update(data.map(v => `${v}\n`).join('')).sign(this.options.wepay.privateKey, 'base64');
147
+ return _nodeCrypto.default.createSign('RSA-SHA256').update(data.map(v => `${v}\n`).join('')).sign(this.options.wepay.privateKey, 'base64');
149
148
  }
150
149
  wepayDecrypt(payload) {
151
150
  const ciphertextBuffer = Buffer.from(payload.ciphertext, 'base64');
152
151
  const authTag = ciphertextBuffer.slice(ciphertextBuffer.length - 16);
153
152
  const data = ciphertextBuffer.slice(0, ciphertextBuffer.length - 16);
154
- const decipherIv = _crypto.default.createDecipheriv('aes-256-gcm', this.options.wepay.secretKey, payload.nonce);
153
+ const decipherIv = _nodeCrypto.default.createDecipheriv('aes-256-gcm', this.options.wepay.secretKey, payload.nonce);
155
154
  decipherIv.setAuthTag(Buffer.from(authTag));
156
155
  decipherIv.setAAD(Buffer.from(payload.associated_data));
157
156
  const decryptStr = decipherIv.update(data, undefined, 'utf8');
@@ -12,7 +12,6 @@ const toRegExp = (0, _vtils.memoize)(re => new RegExp(re.substring(1, re.length
12
12
  */
13
13
  class SensitiveWordsService {
14
14
  constructor(options) {
15
- this.options = options;
16
15
  this.serviceName = 'sensitiveWords';
17
16
  this.sensitiveWords = [];
18
17
  this.sensitiveWords = (0, _vtils.uniq)(options.words.map(word => word.toLowerCase()));
@@ -21,6 +21,7 @@ export class ApiGenerator {
21
21
  // /user/index.ts
22
22
  // /post.ts
23
23
  // /user/list.ts
24
+ // category 必须从 handlers 目录结构中推导,后续会直接拼接到 URL 前缀
24
25
  this.categoryUrlRe = /^.+\/src\/handlers\/(?:index\.ts|(.*)\/index\.ts|(.*)\.ts)$/;
25
26
  this.checker = void 0;
26
27
  this.cwd = this.options.cwd || process.cwd();
@@ -31,7 +32,7 @@ export class ApiGenerator {
31
32
  const handlersPath = path.join(this.cwd, 'node_modules/.x/handlers.ts');
32
33
  const entryFile = path.join(this.cwd, 'src/generated/handlers.ts');
33
34
 
34
- // 仅使用 ts-morph 初始化项目
35
+ // 仅使用 ts-morph 初始化项目,并把运行时生成的 handlers 聚合文件注入到 Program 中
35
36
  const moProject = new mo.Project({
36
37
  tsConfigFilePath: tsConfigPath
37
38
  });
@@ -40,11 +41,12 @@ export class ApiGenerator {
40
41
  const checker = program.getTypeChecker();
41
42
  this.checker = checker;
42
43
  const entrySourceFile = program.getSourceFile(entryFile);
44
+ // handlers.ts 的每个 export 对应一个分类(如 user、post)
43
45
  const moduleSymbol = checker.getSymbolAtLocation(entrySourceFile);
44
46
  const categorySymbols = checker.getExportsOfModule(moduleSymbol);
45
47
  const apiData = [];
46
48
  for (const categorySymbol of categorySymbols) {
47
- // 分类的类型
49
+ // 分类的类型:形如 { list: defineHandler<...>, detail: defineHandler<...> }
48
50
  const categoryType = checker.getTypeOfSymbol(categorySymbol);
49
51
 
50
52
  // 处理器列表
@@ -76,7 +78,8 @@ export class ApiGenerator {
76
78
  let handlerPath = [handlerSymbol.getName()];
77
79
  const handlerNode = handlerSymbol.getDeclarations()[0];
78
80
 
79
- // 支持自定义 requestPath
81
+ // 支持自定义 requestPath,仅识别字符串字面量或字符串数组字面量
82
+ // 这里不会执行表达式(例如变量、模板字符串、函数调用)
80
83
  if (
81
84
  // const xx = defineHandler
82
85
  ts.isVariableDeclaration(handlerNode) && handlerNode.initializer &&
@@ -111,6 +114,9 @@ export class ApiGenerator {
111
114
  }
112
115
  this.debug('生成接口: %s ...', handlerUrl);
113
116
  const handlerType = checker.getTypeAtLocation(handlerNode);
117
+
118
+ // 约定 defineHandler 的泛型顺序固定为 <Request, Response, Method>
119
+ // 若上游定义顺序变更,这里会静默解析错误
114
120
  const [requestType, responseType, methodType] = checker.getTypeArguments(handlerType);
115
121
  const handlerCategory = pascalCase(categoryUrl) || 'Index';
116
122
  const handlerDescription = handlerComment.description || handlerUrl.join(', ');
@@ -124,6 +130,8 @@ export class ApiGenerator {
124
130
  });
125
131
  const requestDataJsonSchema = this.apiDataToJsonSchema(requestData);
126
132
  const responseDataJsonSchema = this.apiDataToJsonSchema(responseData);
133
+
134
+ // 说明文档按“首段标题 + 空行 + 详情”拆分
127
135
  const handlerDescriptionDividerIndex = handlerDescription.indexOf('\n\n');
128
136
  const apiName = handlerDescriptionDividerIndex === -1 ? handlerDescription : handlerDescription.substring(0, handlerDescriptionDividerIndex);
129
137
  const apiDesc = handlerDescriptionDividerIndex === -1 ? '' : handlerDescription.substring(handlerDescriptionDividerIndex + 2);
@@ -162,6 +170,7 @@ export class ApiGenerator {
162
170
  };
163
171
  }
164
172
  const description = ts.displayPartsToString(symbol.getDocumentationComment(this.checker)).trim();
173
+ // JSDoc tag 统一收敛到 Map,方便按 @private 这类标签做开关
165
174
  const tags = symbol.getJsDocTags().map(item => {
166
175
  return {
167
176
  label: item.name,
@@ -182,6 +191,8 @@ export class ApiGenerator {
182
191
  keySymbol = type.symbol || type.aliasSymbol
183
192
  }) {
184
193
  const hasTypeFlag = (flag, _type = type) => (_type.flags & flag) === flag;
194
+
195
+ // valueSymbol 用于识别“实际值类型”,keySymbol 用于保留字段名和字段注释
185
196
  let valueSymbol = type.symbol || type.aliasSymbol;
186
197
  const keySymbolName = keySymbol?.getName() || '';
187
198
  let valueSymbolName = valueSymbol?.getName() || '';
@@ -237,8 +248,8 @@ export class ApiGenerator {
237
248
  };
238
249
  }
239
250
 
240
- // 联合类型
241
- // 布尔、枚举也会用这表示
251
+ // 联合类型:布尔字面量、枚举字面量都会走到这里
252
+ // 处理策略:忽略 null/undefined,用第一个成员推断基础类型,同时把所有字面量收集到 enum
242
253
  const rawUnionTypes = type.isUnion() && type.types || [];
243
254
  const unionTypes = rawUnionTypes.filter(item => !hasTypeFlag(ts.TypeFlags.Null, item) && !hasTypeFlag(ts.TypeFlags.Undefined, item));
244
255
  const isUnion = !!unionTypes.length;
@@ -269,6 +280,8 @@ export class ApiGenerator {
269
280
  const isNumber = hasTypeFlag(ts.TypeFlags.Number) || isNumberLiteral;
270
281
  const isBoolean = hasTypeFlag(ts.TypeFlags.Boolean) || hasTypeFlag(ts.TypeFlags.BooleanLiteral);
271
282
  const isLiteral = isStringLiteral || isNumberLiteral || isBigIntLiteral;
283
+
284
+ // object 兜底:只要不是数组/基础类型,且 TypeScript 视为 Object,就按对象递归
272
285
  const isObject = !isArray && !isString && !isNumber && !isBoolean && hasTypeFlag(ts.TypeFlags.Object) ||
273
286
  // 交集类型视为对象
274
287
  isIntersection;
@@ -279,6 +292,8 @@ export class ApiGenerator {
279
292
  const apiChildren = isArray ? [this.getApiDto({
280
293
  type: this.checker.getTypeArguments(type)[0]
281
294
  })] : isObject ? ii(() => {
295
+ // 这里使用了 TypeChecker 的内部能力来合并交集类型字段
296
+ // 若 TypeScript 内部 API 变化,这里是高风险点
282
297
  const childSymbols = this.checker.getAllPossiblePropertiesOfTypes(intersectionTypes.length ? intersectionTypes : [type]);
283
298
  return childSymbols.map(childSymbol => {
284
299
  const childType = this.checker.getTypeOfSymbol(childSymbol);
@@ -298,6 +313,7 @@ export class ApiGenerator {
298
313
  };
299
314
  }
300
315
  apiDataToJsonSchema(apiData, jsonSchema = {}) {
316
+ // 递归把内部 DTO 结构映射到 JSON Schema(以 object/array/primitive 三类分支处理)
301
317
  jsonSchema.description = apiData.desc;
302
318
  if (apiData.type === 'object') {
303
319
  jsonSchema.type = 'object';
@@ -322,7 +338,10 @@ export class ApiGenerator {
322
338
  for (const handle of handles) {
323
339
  for (const path of handle.path) {
324
340
  data[handle.category] = data[handle.category] || [];
325
- data[handle.category].push(handle.handlerMethod === 'GET' ? {
341
+ data[handle.category].push(
342
+ // GET -> query;FILE -> form-data;其余 -> json body
343
+ // 此处是与 YApi 结构的协议转换层
344
+ handle.handlerMethod === 'GET' ? {
326
345
  method: handle.method.toUpperCase(),
327
346
  title: handle.name,
328
347
  markdown: handle.desc,
@@ -406,6 +425,7 @@ export class ApiGenerator {
406
425
  tags: [handle.category],
407
426
  summary: handle.name,
408
427
  description: handle.desc,
428
+ // 目前统一按 application/json 输出;上传类接口未转为 multipart/form-data
409
429
  requestBody: {
410
430
  content: {
411
431
  'application/json': {
@@ -7,7 +7,8 @@ export class DevUtil {
7
7
  const tsxPkgFile = require.resolve('tsx/package.json');
8
8
  const tsxJsFile = tsxPkgFile.replace('/package.json', '/dist/cli.js');
9
9
  const tsxCjsFile = tsxPkgFile.replace('/package.json', '/dist/cli.cjs');
10
- return fs.existsSync(tsxCjsFile) ? tsxCjsFile : tsxJsFile;
10
+ const tsxMjsFile = tsxPkgFile.replace('/package.json', '/dist/cli.mjs');
11
+ return fs.existsSync(tsxCjsFile) ? tsxCjsFile : fs.existsSync(tsxMjsFile) ? tsxMjsFile : tsxJsFile;
11
12
  }), options.file], {
12
13
  cwd: options.cwd,
13
14
  stdio: 'inherit'
@@ -1,3 +1,3 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
- import { XHandler } from './types';
2
+ import type { XHandler } from './types';
3
3
  export declare const ctxStorage: AsyncLocalStorage<XHandler.Context>;
@@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events';
2
2
  declare class EventBus extends EventEmitter {
3
3
  onReturnOff(event: string, listener: (...args: any[]) => any): () => any;
4
4
  }
5
- export declare function defineBus<T extends Record<string, any>>(): StrictEventEmitter<EventBus, T, T, "addEventListener" | "removeEventListener", "on" | "addListener" | "once" | "removeListener" | "emit" | "off" | "onReturnOff">;
5
+ export declare function defineBus<T extends Record<string, any>>(): StrictEventEmitter<EventBus, T, T, "addEventListener" | "removeEventListener", "on" | "addListener" | "removeListener" | "once" | "emit" | "off" | "onReturnOff">;
6
6
  declare const assignmentCompatibilityHack: unique symbol;
7
7
  interface TypeRecord<T, U, V> {
8
8
  ' _emitterType'?: T;
@@ -1,2 +1,2 @@
1
- import { XCron } from './types';
1
+ import type { XCron } from './types';
2
2
  export declare function defineCron(options: XCron.Options): XCron.Cron;
@@ -1,4 +1,4 @@
1
- import assert from 'assert';
1
+ import assert from 'node:assert';
2
2
  import Queue from 'bull';
3
3
  import { ms } from 'vtils';
4
4
  import { x } from "../x";
@@ -1,7 +1,7 @@
1
- import { createUrlQueryString } from 'vtils';
2
- import { x } from "../x";
3
1
  // @ts-ignore
4
2
 
3
+ import { createUrlQueryString } from 'vtils';
4
+ import { x } from "../x";
5
5
  export function getHandlerUrl(path, query) {
6
6
  return `${x.env.APP_URL.replace(/\/+$/, '')}${path}${query ? `?${createUrlQueryString(query)}` : ''}`;
7
7
  }
@@ -125,11 +125,11 @@ export class Handler {
125
125
  };
126
126
  this.handleWs = async (_data, ctx) => {
127
127
  const dispose = new DisposeService();
128
- ctx.ws.socket.on('message', async payload => {
128
+ ctx.ws.on('message', async payload => {
129
129
  try {
130
130
  const _ = JSON.parse(payload.toString('utf8'));
131
131
  const send = data => {
132
- ctx.ws.socket.send(JSON.stringify({
132
+ ctx.ws.send(JSON.stringify({
133
133
  id: _.id,
134
134
  data: data
135
135
  }));
@@ -166,14 +166,14 @@ export class Handler {
166
166
  }
167
167
  } catch (err) {
168
168
  const _err = HttpError.isHttpError(err) ? err : new HttpError.InternalServerError();
169
- ctx.ws.socket.send(JSON.stringify({
169
+ ctx.ws.send(JSON.stringify({
170
170
  statusCode: _err.statusCode,
171
171
  error: _err.name,
172
172
  message: _err.message
173
173
  }));
174
174
  }
175
175
  });
176
- ctx.ws.socket.on('close', () => {
176
+ ctx.ws.on('close', () => {
177
177
  dispose.dispose();
178
178
  });
179
179
  };
@@ -1,3 +1,3 @@
1
- import { HTTPMethods } from 'fastify';
2
- import { XHandler } from './types';
1
+ import type { HTTPMethods } from 'fastify';
2
+ import type { XHandler } from './types';
3
3
  export declare const HandlerMethodToHttpMethod: Record<XHandler.Method, HTTPMethods>;
@@ -1,4 +1,4 @@
1
- import { XServer } from './types';
1
+ import type { XServer } from './types';
2
2
  export declare class Server {
3
3
  readonly options: XServer.Options;
4
4
  private fastify;
@@ -113,7 +113,7 @@ export class Server {
113
113
  url,
114
114
  headers: req.headers,
115
115
  setHeader: (k, v) => res.header(k, v),
116
- redirect: (url, permanently) => res.redirect(permanently ? 301 : 302, url),
116
+ redirect: (url, permanently) => res.redirect(url, permanently ? 301 : 302),
117
117
  ws: undefined,
118
118
  req: req,
119
119
  res: res
@@ -183,15 +183,13 @@ export class Server {
183
183
  if (Array.isArray(x.env.APP_STATIC_CONTENT)) {
184
184
  const staticContent = x.env.APP_STATIC_CONTENT;
185
185
  for (const item of staticContent) {
186
- if (!item.method) {
187
- item.method = 'GET';
188
- }
189
- if (!this.fastify.hasRoute({
190
- method: item.method,
186
+ const methods = castArray(item.method);
187
+ if (!methods.some(method => this.fastify.hasRoute({
188
+ method,
191
189
  url: item.path
192
- })) {
190
+ }))) {
193
191
  this.fastify.route({
194
- method: item.method,
192
+ method: methods,
195
193
  url: item.path,
196
194
  handler: async (_req, res) => {
197
195
  if (!item.contentType && typeof item.content === 'string') {
@@ -1,6 +1,5 @@
1
1
  import type { IncomingHttpHeaders } from 'node:http';
2
2
  import type { MultipartFile } from '@fastify/multipart';
3
- import type { SocketStream } from '@fastify/websocket';
4
3
  import type { Queue } from 'bull';
5
4
  import type { CronJob } from 'cron';
6
5
  import type { FastifyReply, FastifyRequest, FastifyServerOptions } from 'fastify';
@@ -8,6 +7,7 @@ import type { MsValue } from 'vtils';
8
7
  import type { AsyncOrSync, LiteralUnion, OneOrMore, RequiredDeep } from 'vtils/types';
9
8
  import type * as vae from 'vtils/vae';
10
9
  import type { yup } from 'vtils/validator';
10
+ import type { WebSocket } from 'ws';
11
11
  import type { BasePlugin } from '../plugins/base';
12
12
  import type { BaseService } from '../services/base';
13
13
  import type { DisposeService } from '../services/dispose';
@@ -92,7 +92,7 @@ export declare namespace XHandler {
92
92
  /**
93
93
  * WS
94
94
  */
95
- ws: SocketStream;
95
+ ws: WebSocket;
96
96
  /**
97
97
  * Fastify Request
98
98
  */
@@ -1,4 +1,4 @@
1
- import { FastifyInstance } from 'fastify';
1
+ import type { FastifyInstance } from 'fastify';
2
2
  export interface BasePlugin {
3
3
  readonly register: (fastify: FastifyInstance<any, any, any, any, any>) => void;
4
4
  }
@@ -1,6 +1,6 @@
1
- import { FastifyCompressOptions } from '@fastify/compress';
2
- import { FastifyInstance } from 'fastify';
3
- import { BasePlugin } from './base';
1
+ import { type FastifyCompressOptions } from '@fastify/compress';
2
+ import type { FastifyInstance } from 'fastify';
3
+ import type { BasePlugin } from './base';
4
4
  export interface CompressPluginOptions extends FastifyCompressOptions {
5
5
  }
6
6
  /**
@@ -7,6 +7,6 @@ export class CompressPlugin {
7
7
  this.options = options;
8
8
  }
9
9
  register(fastify) {
10
- fastify.register(FastifyCompress, this.options);
10
+ fastify.register(FastifyCompress, this.options ?? {});
11
11
  }
12
12
  }
@@ -1,6 +1,6 @@
1
- import { FastifyCookieOptions } from '@fastify/cookie';
2
- import { FastifyInstance } from 'fastify';
3
- import { BasePlugin } from './base';
1
+ import { type FastifyCookieOptions } from '@fastify/cookie';
2
+ import type { FastifyInstance } from 'fastify';
3
+ import type { BasePlugin } from './base';
4
4
  export interface CookiePluginOptions extends FastifyCookieOptions {
5
5
  }
6
6
  /**
@@ -7,6 +7,6 @@ export class CookiePlugin {
7
7
  this.options = options;
8
8
  }
9
9
  register(fastify) {
10
- fastify.register(FastifyCookie, this.options);
10
+ fastify.register(FastifyCookie, this.options ?? {});
11
11
  }
12
12
  }
@@ -1,7 +1,7 @@
1
- import { FastifyCorsOptions } from '@fastify/cors';
2
- import { FastifyInstance } from 'fastify';
3
- import { MsValue } from 'vtils';
4
- import { BasePlugin } from './base';
1
+ import { type FastifyCorsOptions } from '@fastify/cors';
2
+ import type { FastifyInstance } from 'fastify';
3
+ import { type MsValue } from 'vtils';
4
+ import type { BasePlugin } from './base';
5
5
  export interface CorsPluginOptions extends FastifyCorsOptions {
6
6
  /**
7
7
  * 允许的来源,支持位于开头的 * 通配符
@@ -1,6 +1,5 @@
1
1
  import FastifyCors from '@fastify/cors';
2
- import { memoize, omitStrict } from 'vtils';
3
- import { ms } from 'vtils';
2
+ import { memoize, ms, omitStrict } from 'vtils';
4
3
  /**
5
4
  * CORS 支持插件
6
5
  */
@@ -1,6 +1,6 @@
1
- import { FastifyMultipartOptions } from '@fastify/multipart';
2
- import { FastifyInstance } from 'fastify';
3
- import { BasePlugin } from './base';
1
+ import { type FastifyMultipartOptions } from '@fastify/multipart';
2
+ import type { FastifyInstance } from 'fastify';
3
+ import type { BasePlugin } from './base';
4
4
  export { MultipartFile } from '@fastify/multipart';
5
5
  export interface FileParserPluginOptions extends FastifyMultipartOptions {
6
6
  }
@@ -1,6 +1,6 @@
1
- import { FastifyFormbodyOptions } from '@fastify/formbody';
2
- import { BasePlugin } from './base';
3
- import { FastifyInstance } from 'fastify';
1
+ import { type FastifyFormbodyOptions } from '@fastify/formbody';
2
+ import type { FastifyInstance } from 'fastify';
3
+ import type { BasePlugin } from './base';
4
4
  export interface FormBodyParserPluginOptions extends FastifyFormbodyOptions {
5
5
  }
6
6
  /**
@@ -7,6 +7,6 @@ export class FormBodyParserPlugin {
7
7
  this.options = options;
8
8
  }
9
9
  register(fastify) {
10
- fastify.register(FastifyFormBody, this.options);
10
+ fastify.register(FastifyFormBody, this.options || {});
11
11
  }
12
12
  }
@@ -1,6 +1,6 @@
1
- import { WebsocketPluginOptions } from '@fastify/websocket';
2
- import { BasePlugin } from './base';
3
- import { FastifyInstance } from 'fastify';
1
+ import { type WebsocketPluginOptions } from '@fastify/websocket';
2
+ import type { FastifyInstance } from 'fastify';
3
+ import type { BasePlugin } from './base';
4
4
  export interface WsParserPluginOptions extends WebsocketPluginOptions {
5
5
  }
6
6
  /**
@@ -7,6 +7,6 @@ export class WsParserPlugin {
7
7
  this.options = options;
8
8
  }
9
9
  register(fastify) {
10
- fastify.register(FastifyWebsocket, this.options);
10
+ fastify.register(FastifyWebsocket, this.options ?? {});
11
11
  }
12
12
  }
@@ -1,6 +1,6 @@
1
- import { BasePlugin } from './base';
2
- import { FastifyInstance } from 'fastify';
3
- import { X2jOptions } from 'fast-xml-parser';
1
+ import { type X2jOptions } from 'fast-xml-parser';
2
+ import type { FastifyInstance } from 'fastify';
3
+ import type { BasePlugin } from './base';
4
4
  export interface XmlParserPluginOptions extends Partial<X2jOptions> {
5
5
  /**
6
6
  * 要处理的 `Content-Type`
@@ -14,7 +14,7 @@ export class XmlParserPlugin {
14
14
  ...parseOptions
15
15
  } = this.options || {};
16
16
  const xmlParser = new XMLParser(parseOptions);
17
- fastify.addContentTypeParser(contentType, (req, payload, done) => {
17
+ fastify.addContentTypeParser(contentType, (_req, payload, done) => {
18
18
  let body = '';
19
19
  const handleError = err => done(err);
20
20
  const handleData = data => body += data;
@@ -1,5 +1,5 @@
1
1
  import { type MsValue } from 'vtils';
2
- import { BaseService } from './base';
2
+ import type { BaseService } from './base';
3
3
  export type CaptchaServiceGenerateCharset = '09' | '19' | 'az' | 'AZ' | 'aZ' | '0z' | '0Z' | '1z' | '1Z';
4
4
  export interface CaptchaServiceGeneratePayload<T> {
5
5
  /**
@@ -8,9 +8,9 @@ export class CaptchaService {
8
8
  this.charsetMap = {
9
9
  '09': '0123456789'.split(''),
10
10
  '19': '123456789'.split(''),
11
- 'az': 'abcdefghijklmnopqrstuvwxyz'.split(''),
12
- 'AZ': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
13
- 'aZ': 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
11
+ az: 'abcdefghijklmnopqrstuvwxyz'.split(''),
12
+ AZ: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
13
+ aZ: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
14
14
  '0z': '0123456789abcdefghijklmnopqrstuvwxyz'.split(''),
15
15
  '0Z': '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
16
16
  '1z': '123456789abcdefghijklmnopqrstuvwxyz'.split(''),
@@ -1,4 +1,4 @@
1
- import { BaseService } from './base';
1
+ import type { BaseService } from './base';
2
2
  export declare class CryptoService implements BaseService {
3
3
  serviceName: string;
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { BaseService } from './base';
1
+ import type { BaseService } from './base';
2
2
  export interface DingtalkServiceOptions {
3
3
  /**
4
4
  * 访问口令。
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto';
1
+ import crypto from 'node:crypto';
2
2
  import got from 'got';
3
3
  export class DingtalkService {
4
4
  constructor(options) {
@@ -1,4 +1,4 @@
1
- import { BaseService } from './base';
1
+ import type { BaseService } from './base';
2
2
  export interface DisposeServiceOptions {
3
3
  disposeOnExit?: boolean;
4
4
  }
@@ -1,4 +1,4 @@
1
- import { BaseService } from './base';
1
+ import type { BaseService } from './base';
2
2
  export declare class EmojiService implements BaseService {
3
3
  serviceName: string;
4
4
  unify(text: string): string;
@@ -1,5 +1,5 @@
1
1
  // softbankOrDocomoOrAuOrGoogleToUnified
2
- // prettier-ignore
2
+ // biome-ignore format: x
3
3
  const softbankOrDocomoOrAuOrGoogleToUnified = {
4
4
  "": "#️⃣",
5
5
  "󾠬": "#️⃣",
@@ -1,4 +1,4 @@
1
- import { BaseService } from './base';
1
+ import type { BaseService } from './base';
2
2
  export declare class HtmlService implements BaseService {
3
3
  serviceName: string;
4
4
  escape(html: string): string;
@@ -1,5 +1,5 @@
1
- import { Merge } from 'vtils/types';
2
- import { BaseService } from './base';
1
+ import type { Merge } from 'vtils/types';
2
+ import type { BaseService } from './base';
3
3
  export type LogServiceLevel = 'error' | 'info' | 'success';
4
4
  export type LogServiceDesc = Record<string, any> | any[];
5
5
  export type LogServiceWriter = 'file' | 'console';
@@ -1,4 +1,4 @@
1
- import path from 'path';
1
+ import path from 'node:path';
2
2
  import fs from 'fs-extra';
3
3
  import { memoize } from 'vtils';
4
4
  import { formatDate } from 'vtils/date';
@@ -1,5 +1,5 @@
1
1
  import nodemailer from 'nodemailer';
2
- import { BaseService } from './base';
2
+ import type { BaseService } from './base';
3
3
  export interface MailServiceOptions {
4
4
  host: string;
5
5
  port: number;
@@ -1,4 +1,4 @@
1
- import { BaseService } from './base';
1
+ import type { BaseService } from './base';
2
2
  export interface PayServiceOptions {
3
3
  /** 支付宝 */
4
4
  alipay?: {
@@ -9,17 +9,17 @@ export interface PayServiceOptions {
9
9
  /** 支付宝公钥,用于对返回结果验签 */
10
10
  publicKey: string;
11
11
  };
12
- /** 微信支付 */
12
+ /** 微信支付(APIv3),未实现回调验签,因此验证微信支付身份块不需要关心 */
13
13
  wepay?: {
14
14
  /** 公众号 APPID */
15
15
  appId: string;
16
16
  /** 商户 ID */
17
17
  merchantId: string;
18
- /** 商户应用私钥 */
18
+ /** 商户API证书私钥 */
19
19
  privateKey: string;
20
- /** 证书序列号 */
20
+ /** 商户API证书序列号 */
21
21
  certificateSerialNumber: string;
22
- /** 秘钥,用于解密请求平台证书、回调数据 */
22
+ /** 密钥,用于解密回调数据 */
23
23
  secretKey: string;
24
24
  };
25
25
  }
@@ -48,9 +48,7 @@ export interface PayServicePrepareWepayOptions {
48
48
  totalMoney: number;
49
49
  /** 通知地址(POST) */
50
50
  notifyUrl: string;
51
- /** 对应公众号的 appId */
52
- appId?: string;
53
- /** 对应公众号用户的 openid */
51
+ /** 公众号用户的 openid */
54
52
  openid: string;
55
53
  }
56
54
  export interface PayServicePrepareWepayResult {
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto';
1
+ import crypto from 'node:crypto';
2
2
  import AlipaySdk from 'alipay-sdk';
3
3
  import AlipayFormData from 'alipay-sdk/lib/form';
4
4
  import got from 'got';
@@ -39,9 +39,8 @@ export class PayService {
39
39
  };
40
40
  }
41
41
  async prepareWepay(options) {
42
- const appId = options.appId || this.options.wepay.appId;
43
42
  const res = await this.wepayRequest('https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi', {
44
- appid: appId,
43
+ appid: this.options.wepay.appId,
45
44
  mchid: this.options.wepay.merchantId,
46
45
  description: options.goodsName,
47
46
  out_trade_no: options.tradeNumber,
@@ -60,7 +59,7 @@ export class PayService {
60
59
  throw new Error(error);
61
60
  }
62
61
  const params = {
63
- appId: appId,
62
+ appId: this.options.wepay.appId,
64
63
  timestamp: String(Math.round(Date.now() / 1000)),
65
64
  nonceStr: Math.random().toString().substr(2, 12),
66
65
  package: `prepay_id=${res.prepay_id}`,
@@ -1,5 +1,5 @@
1
- import { MsValue } from 'vtils';
2
- import { BaseService } from './base';
1
+ import { type MsValue } from 'vtils';
2
+ import type { BaseService } from './base';
3
3
  export interface RateLimitServiceLimitByCountOptions {
4
4
  key: string;
5
5
  ttl: MsValue;
@@ -1,5 +1,5 @@
1
- import { BaseService } from './base';
2
- import { Redis, RedisOptions } from 'ioredis';
1
+ import { Redis, type RedisOptions } from 'ioredis';
2
+ import type { BaseService } from './base';
3
3
  export interface RedisServiceOptions extends RedisOptions {
4
4
  }
5
5
  export declare class RedisService extends Redis implements BaseService {
@@ -1,4 +1,4 @@
1
- import { BaseService } from './base';
1
+ import type { BaseService } from './base';
2
2
  export interface SensitiveWordsServiceOptions {
3
3
  /** 敏感词列表 */
4
4
  words: string[];
@@ -7,7 +7,6 @@ export interface SensitiveWordsServiceOptions {
7
7
  * 敏感词服务。
8
8
  */
9
9
  export declare class SensitiveWordsService implements BaseService {
10
- private options;
11
10
  serviceName: string;
12
11
  private sensitiveWords;
13
12
  constructor(options: SensitiveWordsServiceOptions);
@@ -8,7 +8,6 @@ const toRegExp = memoize(re => new RegExp(re.substring(1, re.length - 1)), re =>
8
8
  */
9
9
  export class SensitiveWordsService {
10
10
  constructor(options) {
11
- this.options = options;
12
11
  this.serviceName = 'sensitiveWords';
13
12
  this.sensitiveWords = [];
14
13
  this.sensitiveWords = uniq(options.words.map(word => word.toLowerCase()));
@@ -1,4 +1,4 @@
1
- import { BaseService } from './base';
1
+ import type { BaseService } from './base';
2
2
  export type SmsServiceProvider = 'tencentCloud';
3
3
  export type SmsServiceOptions = {
4
4
  provider: 'tencentCloud';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jayfong/x-server",
3
- "version": "2.118.0",
3
+ "version": "2.119.0",
4
4
  "license": "ISC",
5
5
  "sideEffects": false,
6
6
  "main": "lib/_cjs/index.js",
@@ -12,64 +12,55 @@
12
12
  "files": [
13
13
  "lib"
14
14
  ],
15
- "scripts": {
16
- "build": "haoma compile",
17
- "build_test_pkg": "tyn build && rm -rf ./lib_test && mkdir -p ./lib_test && cp -r ./lib ./lib_test/lib && cp ./package.json ./lib_test/package.json && cd ./tests/app && tyn add file:../../lib_test",
18
- "test": "tsc --noEmit -p ./tsconfig.build.json && jest \"$(pwd)/src/\"",
19
- "test_all": "tsc --noEmit -p ./tsconfig.build.json && jest",
20
- "test_api_bot": "cd /Users/admin/Documents/jfWorks/qiqi-bot && DEBUG=api tsx /Users/admin/Documents/jfWorks/x/packages/x-server/src/cli/cli.ts api",
21
- "test_api_qiqi": "cd /Users/admin/Documents/jfWorks/qiqi-server && DEBUG=api tsx /Users/admin/Documents/jfWorks/x/packages/x-server/src/cli/cli.ts api",
22
- "up_prisma": "tpm dlx npm-check-updates '/.*prisma.*/' -i --target semver"
23
- },
24
15
  "dependencies": {
25
- "@jayfong/x-request": "^2.118.0",
26
- "@aws-sdk/client-s3": "^3.1079.0",
27
- "@aws-sdk/s3-request-presigner": "^3.1079.0",
28
- "@fastify/compress": "^7.0.3",
29
- "@fastify/cookie": "^9.4.0",
30
- "@fastify/cors": "^8.3.0",
31
- "@fastify/formbody": "^7.4.0",
32
- "@fastify/multipart": "^7.7.0",
33
- "@fastify/websocket": "^8.1.0",
34
- "@paralleldrive/cuid2": "^3.0.4",
16
+ "@aws-sdk/client-s3": "^3.1166.0",
17
+ "@aws-sdk/s3-request-presigner": "^3.1166.0",
18
+ "@fastify/compress": "^9.3.0",
19
+ "@fastify/cookie": "^11.1.5",
20
+ "@fastify/cors": "^11.4.1",
21
+ "@fastify/formbody": "^9.0.1",
22
+ "@fastify/multipart": "^10.1.8",
23
+ "@fastify/websocket": "^11.3.7",
24
+ "@jayfong/x-request": "2.119.0",
25
+ "@paralleldrive/cuid2": "^3.3.0",
35
26
  "@prisma/adapter-mariadb": "^7.10.1",
36
27
  "@prisma/client": "^7.10.1",
37
28
  "@types/busboy": "^0.3.2",
38
- "@types/http-errors": "^1.8.2",
29
+ "@types/http-errors": "^2.0.5",
39
30
  "@types/jsonwebtoken": "^8.5.8",
40
31
  "@types/nodemailer": "^6.4.4",
41
- "@types/ws": "^8.5.5",
32
+ "@types/ws": "^8.18.2",
42
33
  "alipay-sdk": "^3.2.0",
43
34
  "bufferutil": "^4.0.6",
44
- "bull": "^4.10.4",
35
+ "bull": "^4.16.5",
45
36
  "chokidar": "^3.5.3",
46
37
  "comment-parser": "^1.3.1",
47
38
  "compressing": "^1.5.1",
48
- "cron": "^3.1.6",
39
+ "cron": "^4.4.0",
49
40
  "cuid": "^2.1.8",
50
41
  "debug": "^4.3.4",
51
- "esbuild": "^0.25.5",
42
+ "esbuild": "^0.28.2",
52
43
  "esbuild-register": "^3.6.0",
53
44
  "execa": "^5.1.1",
54
45
  "exit-hook": "^2.2.1",
55
46
  "fast-escape-html": "^1.1.0",
56
- "fast-xml-parser": "^4.2.5",
57
- "fastify": "^4.19.2",
47
+ "fast-xml-parser": "^5.12.0",
48
+ "fastify": "^5.12.8",
58
49
  "fs-extra": "^10.0.1",
59
50
  "globby": "^11",
60
51
  "got": "^11.8.2",
61
- "http-errors": "^2.0.0",
52
+ "http-errors": "^2.0.1",
62
53
  "inquirer": "^12.5.0",
63
54
  "inquirer-autocomplete-standalone": "^0.8.1",
64
55
  "ioredis": "^5.3.2",
65
56
  "jsonwebtoken": "^8.5.1",
66
57
  "lru-cache": "^10.0.0",
67
58
  "lz-string": "^1.4.4",
68
- "mime": "^4.1.0",
59
+ "mime": "^4.1.2",
69
60
  "mini-svg-data-uri": "^1.4.4",
70
61
  "node-ssh": "^13.1.0",
71
62
  "nodemailer": "^6.7.3",
72
- "pino-pretty": "^10.0.1",
63
+ "pino-pretty": "^13.2.0",
73
64
  "pinyin-pro": "^3.26.0",
74
65
  "pirates": "^4.0.6",
75
66
  "prisma": "^7.10.1",
@@ -80,12 +71,12 @@
80
71
  "svg-captcha": "^1.4.0",
81
72
  "tencentcloud-sdk-nodejs-sms": "^4.0.681",
82
73
  "ts-morph": "^27.0.2",
83
- "tsx": "^4.21.0",
74
+ "tsx": "^4.23.15",
84
75
  "unplugin-macros": "^0.16.0",
85
76
  "utf-8-validate": "^5.0.9",
86
77
  "vscode-generate-index-standalone": "^1.7.1",
87
78
  "vtils": "^4.138.0",
88
- "yaml": "^2.3.1",
79
+ "yaml": "^2.9.1",
89
80
  "yargs": "^17.4.1"
90
81
  },
91
82
  "devDependencies": {
@@ -104,5 +95,15 @@
104
95
  },
105
96
  "publishConfig": {
106
97
  "access": "public"
98
+ },
99
+ "scripts": {
100
+ "build": "mv ./node_modules/.x ./node_modules/.x2 || true && haoma compile && mv ./node_modules/.x2 ./node_modules/.x || true",
101
+ "build_test_pkg": "tyn build && rm -rf ./lib_test && mkdir -p ./lib_test && cp -r ./lib ./lib_test/lib && cp ./package.json ./lib_test/package.json && cd ./tests/app && tyn add file:../../lib_test",
102
+ "test": "tsc --noEmit -p ./tsconfig.build.json && jest \"$(pwd)/src/\"",
103
+ "test_all": "tsc --noEmit -p ./tsconfig.build.json && jest",
104
+ "test_api_bot": "cd /Users/admin/Documents/jfWorks/qiqi-bot && DEBUG=api tsx /Users/admin/Documents/jfWorks/x/packages/x-server/src/cli/cli.ts api",
105
+ "test_api_qiqi": "cd /Users/admin/Documents/jfWorks/qiqi-server && DEBUG=api tsx /Users/admin/Documents/jfWorks/x/packages/x-server/src/cli/cli.ts api",
106
+ "up_prisma": "tpm dlx npm-check-updates '/.*prisma.*/' -i --target semver",
107
+ "up_fastify": "tpm dlx npm-check-updates '/.*fastify.*/' -i --target latest"
107
108
  }
108
- }
109
+ }