error-rule-detector.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. /**
  2. * 错误规则检测引擎
  3. *
  4. * 特性:
  5. * - 按规则类型分组缓存(regex/contains/exact)
  6. * - 性能优先的检测顺序(包含 → 精确 → 正则)
  7. * - 单例模式,全局复用
  8. * - 支持热重载
  9. * - ReDoS 风险检测(safe-regex)
  10. * - EventEmitter 驱动的自动缓存刷新
  11. */
  12. import { getActiveErrorRules, type ErrorOverrideResponse } from "@/repository/error-rules";
  13. import { logger } from "@/lib/logger";
  14. import { eventEmitter } from "@/lib/event-emitter";
  15. import { isValidErrorOverrideResponse } from "@/lib/error-override-validator";
  16. import safeRegex from "safe-regex";
  17. /**
  18. * 错误检测结果
  19. */
  20. export interface ErrorDetectionResult {
  21. matched: boolean;
  22. category?: string; // 触发的错误分类
  23. pattern?: string; // 匹配的规则模式
  24. matchType?: string; // 匹配类型(regex/contains/exact)
  25. /** 覆写响应体:如果配置了则用此响应替换原始错误响应 */
  26. overrideResponse?: ErrorOverrideResponse;
  27. /** 覆写状态码:如果配置了则用此状态码替换原始状态码 */
  28. overrideStatusCode?: number;
  29. }
  30. /**
  31. * 缓存的正则规则
  32. */
  33. interface RegexPattern {
  34. pattern: RegExp;
  35. category: string;
  36. description?: string;
  37. overrideResponse?: ErrorOverrideResponse;
  38. overrideStatusCode?: number;
  39. }
  40. /**
  41. * 缓存的包含规则
  42. */
  43. interface ContainsPattern {
  44. text: string;
  45. category: string;
  46. description?: string;
  47. overrideResponse?: ErrorOverrideResponse;
  48. overrideStatusCode?: number;
  49. }
  50. /**
  51. * 缓存的精确规则
  52. */
  53. interface ExactPattern {
  54. text: string;
  55. category: string;
  56. description?: string;
  57. overrideResponse?: ErrorOverrideResponse;
  58. overrideStatusCode?: number;
  59. }
  60. /**
  61. * 错误规则检测缓存类
  62. */
  63. class ErrorRuleDetector {
  64. private regexPatterns: RegexPattern[] = [];
  65. private containsPatterns: ContainsPattern[] = [];
  66. private exactPatterns: Map<string, ExactPattern> = new Map();
  67. private lastReloadTime: number = 0;
  68. private isLoading: boolean = false;
  69. private isInitialized: boolean = false; // 跟踪初始化状态
  70. private initializationPromise: Promise<void> | null = null; // 防止并发初始化竞态
  71. constructor() {
  72. // 监听数据库变更事件,自动刷新缓存
  73. eventEmitter.on("errorRulesUpdated", () => {
  74. this.reload().catch((error) => {
  75. logger.error("[ErrorRuleDetector] Failed to reload cache on event:", error);
  76. });
  77. });
  78. }
  79. /**
  80. * 确保规则已加载(懒加载,首次使用时或显式 reload 时调用)
  81. * 避免在数据库未准备好时过早加载
  82. * 使用 Promise 合并模式防止并发请求时的竞态条件
  83. */
  84. private async ensureInitialized(): Promise<void> {
  85. if (this.isInitialized) {
  86. return;
  87. }
  88. if (!this.initializationPromise) {
  89. this.initializationPromise = this.reload().finally(() => {
  90. this.initializationPromise = null;
  91. });
  92. }
  93. await this.initializationPromise;
  94. }
  95. /**
  96. * 从数据库重新加载错误规则
  97. */
  98. async reload(): Promise<void> {
  99. if (this.isLoading) {
  100. logger.warn("[ErrorRuleDetector] Reload already in progress, skipping");
  101. return;
  102. }
  103. this.isLoading = true;
  104. try {
  105. logger.info("[ErrorRuleDetector] Reloading error rules from database...");
  106. let rules;
  107. try {
  108. rules = await getActiveErrorRules();
  109. } catch (dbError) {
  110. // 优雅处理表不存在的情况(迁移还未执行时)
  111. // 这允许应用在迁移前正常启动,迁移后会自动重载
  112. const errorMessage = (dbError as Error).message || "";
  113. if (errorMessage.includes("relation") && errorMessage.includes("does not exist")) {
  114. logger.warn(
  115. "[ErrorRuleDetector] error_rules table does not exist yet (migration pending), using empty rules"
  116. );
  117. this.lastReloadTime = Date.now();
  118. this.isLoading = false; // 关键:early return 时必须清除 isLoading,否则后续 reload 会被永久阻塞
  119. return;
  120. }
  121. // 其他数据库错误继续抛出
  122. throw dbError;
  123. }
  124. // 使用局部变量收集新数据,避免 reload 期间 detect() 返回空结果
  125. const newRegexPatterns: RegexPattern[] = [];
  126. const newContainsPatterns: ContainsPattern[] = [];
  127. const newExactPatterns = new Map<string, ExactPattern>();
  128. // 按类型分组加载规则
  129. let validRegexCount = 0;
  130. let skippedRedosCount = 0;
  131. let skippedInvalidResponseCount = 0;
  132. for (const rule of rules) {
  133. // 在加载阶段验证 overrideResponse 格式,过滤畸形数据
  134. let validatedOverrideResponse: ErrorOverrideResponse | undefined = undefined;
  135. if (rule.overrideResponse) {
  136. if (isValidErrorOverrideResponse(rule.overrideResponse)) {
  137. validatedOverrideResponse = rule.overrideResponse;
  138. } else {
  139. logger.warn(
  140. `[ErrorRuleDetector] Invalid override_response for rule ${rule.id} (pattern: ${rule.pattern}), skipping response override`
  141. );
  142. skippedInvalidResponseCount++;
  143. }
  144. }
  145. switch (rule.matchType) {
  146. case "contains": {
  147. const lowerText = rule.pattern.toLowerCase();
  148. newContainsPatterns.push({
  149. text: lowerText,
  150. category: rule.category,
  151. description: rule.description ?? undefined,
  152. overrideResponse: validatedOverrideResponse,
  153. overrideStatusCode: rule.overrideStatusCode ?? undefined,
  154. });
  155. break;
  156. }
  157. case "exact": {
  158. const lowerText = rule.pattern.toLowerCase();
  159. newExactPatterns.set(lowerText, {
  160. text: lowerText,
  161. category: rule.category,
  162. description: rule.description ?? undefined,
  163. overrideResponse: validatedOverrideResponse,
  164. overrideStatusCode: rule.overrideStatusCode ?? undefined,
  165. });
  166. break;
  167. }
  168. case "regex": {
  169. // 使用 safe-regex 检测 ReDoS 风险
  170. try {
  171. if (!safeRegex(rule.pattern)) {
  172. logger.warn(
  173. `[ErrorRuleDetector] ReDoS risk detected in pattern: ${rule.pattern}, skipping`
  174. );
  175. skippedRedosCount++;
  176. break;
  177. }
  178. const pattern = new RegExp(rule.pattern, "i");
  179. newRegexPatterns.push({
  180. pattern,
  181. category: rule.category,
  182. description: rule.description ?? undefined,
  183. overrideResponse: validatedOverrideResponse,
  184. overrideStatusCode: rule.overrideStatusCode ?? undefined,
  185. });
  186. validRegexCount++;
  187. } catch (error) {
  188. logger.error(`[ErrorRuleDetector] Invalid regex pattern: ${rule.pattern}`, error);
  189. }
  190. break;
  191. }
  192. default:
  193. logger.warn(`[ErrorRuleDetector] Unknown match type: ${rule.matchType}`);
  194. }
  195. }
  196. // 原子替换:确保 detect() 始终看到一致的数据集
  197. this.regexPatterns = newRegexPatterns;
  198. this.containsPatterns = newContainsPatterns;
  199. this.exactPatterns = newExactPatterns;
  200. this.lastReloadTime = Date.now();
  201. this.isInitialized = true; // 标记为已初始化
  202. const skippedInfo = [
  203. skippedRedosCount > 0 ? `${skippedRedosCount} ReDoS` : "",
  204. skippedInvalidResponseCount > 0 ? `${skippedInvalidResponseCount} invalid response` : "",
  205. ]
  206. .filter(Boolean)
  207. .join(", ");
  208. logger.info(
  209. `[ErrorRuleDetector] Loaded ${rules.length} error rules: ` +
  210. `contains=${newContainsPatterns.length}, exact=${newExactPatterns.size}, ` +
  211. `regex=${validRegexCount}${skippedInfo ? ` (skipped: ${skippedInfo})` : ""}`
  212. );
  213. } catch (error) {
  214. logger.error("[ErrorRuleDetector] Failed to reload error rules:", error);
  215. // 失败时不清空现有缓存,保持降级可用
  216. } finally {
  217. this.isLoading = false;
  218. }
  219. }
  220. /**
  221. * 异步检测错误消息(推荐使用)
  222. * 确保规则已加载后再进行检测
  223. *
  224. * @param errorMessage - 错误消息
  225. * @returns 检测结果
  226. */
  227. async detectAsync(errorMessage: string): Promise<ErrorDetectionResult> {
  228. await this.ensureInitialized();
  229. return this.detect(errorMessage);
  230. }
  231. /**
  232. * 检测错误消息是否匹配任何规则(同步版本)
  233. *
  234. * 注意:如果规则未初始化,会记录警告并返回 false
  235. * 推荐使用 detectAsync() 以确保规则已加载
  236. *
  237. * 检测顺序(性能优先):
  238. * 1. 包含匹配(最快,O(n*m))
  239. * 2. 精确匹配(使用 Set,O(1))
  240. * 3. 正则匹配(最慢,但最灵活)
  241. *
  242. * @param errorMessage - 错误消息
  243. * @returns 检测结果
  244. */
  245. detect(errorMessage: string): ErrorDetectionResult {
  246. if (!errorMessage || errorMessage.length === 0) {
  247. return { matched: false };
  248. }
  249. // 如果未初始化,记录警告
  250. if (!this.isInitialized && !this.isLoading) {
  251. logger.warn(
  252. "[ErrorRuleDetector] detect() called before initialization, results may be incomplete. Consider using detectAsync() instead."
  253. );
  254. }
  255. const lowerMessage = errorMessage.toLowerCase();
  256. const trimmedMessage = lowerMessage.trim();
  257. // 1. 包含匹配(最快)
  258. for (const pattern of this.containsPatterns) {
  259. if (lowerMessage.includes(pattern.text)) {
  260. return {
  261. matched: true,
  262. category: pattern.category,
  263. pattern: pattern.text,
  264. matchType: "contains",
  265. overrideResponse: pattern.overrideResponse,
  266. overrideStatusCode: pattern.overrideStatusCode,
  267. };
  268. }
  269. }
  270. // 2. 精确匹配(O(1) 查询)
  271. const exactMatch = this.exactPatterns.get(trimmedMessage);
  272. if (exactMatch) {
  273. return {
  274. matched: true,
  275. category: exactMatch.category,
  276. pattern: exactMatch.text,
  277. matchType: "exact",
  278. overrideResponse: exactMatch.overrideResponse,
  279. overrideStatusCode: exactMatch.overrideStatusCode,
  280. };
  281. }
  282. // 3. 正则匹配(最慢,但最灵活)
  283. for (const { pattern, category, overrideResponse, overrideStatusCode } of this.regexPatterns) {
  284. if (pattern.test(errorMessage)) {
  285. return {
  286. matched: true,
  287. category,
  288. pattern: pattern.source,
  289. matchType: "regex",
  290. overrideResponse,
  291. overrideStatusCode,
  292. };
  293. }
  294. }
  295. return { matched: false };
  296. }
  297. /**
  298. * 获取缓存统计信息
  299. */
  300. getStats() {
  301. return {
  302. regexCount: this.regexPatterns.length,
  303. containsCount: this.containsPatterns.length,
  304. exactCount: this.exactPatterns.size,
  305. totalCount:
  306. this.regexPatterns.length + this.containsPatterns.length + this.exactPatterns.size,
  307. lastReloadTime: this.lastReloadTime,
  308. isLoading: this.isLoading,
  309. };
  310. }
  311. /**
  312. * 检查是否完成至少一次初始化
  313. *
  314. * 用于避免未加载完成时缓存空结果,导致后续请求无法命中规则
  315. */
  316. hasInitialized(): boolean {
  317. return this.isInitialized;
  318. }
  319. /**
  320. * 检查缓存是否为空
  321. */
  322. isEmpty(): boolean {
  323. return (
  324. this.regexPatterns.length === 0 &&
  325. this.containsPatterns.length === 0 &&
  326. this.exactPatterns.size === 0
  327. );
  328. }
  329. }
  330. /**
  331. * 全局单例导出
  332. */
  333. export const errorRuleDetector = new ErrorRuleDetector();