error-rule-detector.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. /**
  2. * 错误规则检测引擎
  3. *
  4. * 特性:
  5. * - 按规则类型分组缓存(regex/contains/exact)
  6. * - 性能优先的检测顺序(包含 → 精确 → 正则)
  7. * - 单例模式,全局复用
  8. * - 支持热重载
  9. * - ReDoS 风险检测(safe-regex)
  10. * - EventEmitter 驱动的自动缓存刷新
  11. */
  12. import { getActiveErrorRules } from "@/repository/error-rules";
  13. import { logger } from "@/lib/logger";
  14. import { eventEmitter } from "@/lib/event-emitter";
  15. import safeRegex from "safe-regex";
  16. /**
  17. * 错误检测结果
  18. */
  19. export interface ErrorDetectionResult {
  20. matched: boolean;
  21. category?: string; // 触发的错误分类
  22. pattern?: string; // 匹配的规则模式
  23. matchType?: string; // 匹配类型(regex/contains/exact)
  24. }
  25. /**
  26. * 缓存的正则规则
  27. */
  28. interface RegexPattern {
  29. pattern: RegExp;
  30. category: string;
  31. description?: string;
  32. }
  33. /**
  34. * 缓存的包含规则
  35. */
  36. interface ContainsPattern {
  37. text: string;
  38. category: string;
  39. description?: string;
  40. }
  41. /**
  42. * 缓存的精确规则
  43. */
  44. interface ExactPattern {
  45. text: string;
  46. category: string;
  47. description?: string;
  48. }
  49. /**
  50. * 错误规则检测缓存类
  51. */
  52. class ErrorRuleDetector {
  53. private regexPatterns: RegexPattern[] = [];
  54. private containsPatterns: ContainsPattern[] = [];
  55. private exactPatterns: Map<string, ExactPattern> = new Map();
  56. private lastReloadTime: number = 0;
  57. private isLoading: boolean = false;
  58. constructor() {
  59. // 初始化时立即加载缓存(异步,不阻塞构造函数)
  60. this.reload().catch((error) => {
  61. logger.error("[ErrorRuleDetector] Failed to initialize cache:", error);
  62. });
  63. // 监听数据库变更事件,自动刷新缓存
  64. eventEmitter.on("errorRulesUpdated", () => {
  65. this.reload().catch((error) => {
  66. logger.error("[ErrorRuleDetector] Failed to reload cache on event:", error);
  67. });
  68. });
  69. }
  70. /**
  71. * 从数据库重新加载错误规则
  72. */
  73. async reload(): Promise<void> {
  74. if (this.isLoading) {
  75. logger.warn("[ErrorRuleDetector] Reload already in progress, skipping");
  76. return;
  77. }
  78. this.isLoading = true;
  79. try {
  80. logger.info("[ErrorRuleDetector] Reloading error rules from database...");
  81. let rules;
  82. try {
  83. rules = await getActiveErrorRules();
  84. } catch (dbError) {
  85. // 优雅处理表不存在的情况(迁移还未执行时)
  86. // 这允许应用在迁移前正常启动,迁移后会自动重载
  87. const errorMessage = (dbError as Error).message || "";
  88. if (errorMessage.includes("relation") && errorMessage.includes("does not exist")) {
  89. logger.warn(
  90. "[ErrorRuleDetector] error_rules table does not exist yet (migration pending), using empty rules"
  91. );
  92. this.lastReloadTime = Date.now();
  93. return;
  94. }
  95. // 其他数据库错误继续抛出
  96. throw dbError;
  97. }
  98. // 清空旧缓存
  99. this.regexPatterns = [];
  100. this.containsPatterns = [];
  101. this.exactPatterns.clear();
  102. // 按类型分组加载规则
  103. let validRegexCount = 0;
  104. let skippedRedosCount = 0;
  105. for (const rule of rules) {
  106. switch (rule.matchType) {
  107. case "contains": {
  108. const lowerText = rule.pattern.toLowerCase();
  109. this.containsPatterns.push({
  110. text: lowerText,
  111. category: rule.category,
  112. description: rule.description ?? undefined,
  113. });
  114. break;
  115. }
  116. case "exact": {
  117. const lowerText = rule.pattern.toLowerCase();
  118. this.exactPatterns.set(lowerText, {
  119. text: lowerText,
  120. category: rule.category,
  121. description: rule.description ?? undefined,
  122. });
  123. break;
  124. }
  125. case "regex": {
  126. // 使用 safe-regex 检测 ReDoS 风险
  127. try {
  128. if (!safeRegex(rule.pattern)) {
  129. logger.warn(
  130. `[ErrorRuleDetector] ReDoS risk detected in pattern: ${rule.pattern}, skipping`
  131. );
  132. skippedRedosCount++;
  133. break;
  134. }
  135. const pattern = new RegExp(rule.pattern, "i");
  136. this.regexPatterns.push({
  137. pattern,
  138. category: rule.category,
  139. description: rule.description ?? undefined,
  140. });
  141. validRegexCount++;
  142. } catch (error) {
  143. logger.error(`[ErrorRuleDetector] Invalid regex pattern: ${rule.pattern}`, error);
  144. }
  145. break;
  146. }
  147. default:
  148. logger.warn(`[ErrorRuleDetector] Unknown match type: ${rule.matchType}`);
  149. }
  150. }
  151. this.lastReloadTime = Date.now();
  152. logger.info(
  153. `[ErrorRuleDetector] Loaded ${rules.length} error rules: ` +
  154. `contains=${this.containsPatterns.length}, exact=${this.exactPatterns.size}, ` +
  155. `regex=${validRegexCount}${skippedRedosCount > 0 ? ` (skipped ${skippedRedosCount} ReDoS)` : ""}`
  156. );
  157. } catch (error) {
  158. logger.error("[ErrorRuleDetector] Failed to reload error rules:", error);
  159. // 失败时不清空现有缓存,保持降级可用
  160. } finally {
  161. this.isLoading = false;
  162. }
  163. }
  164. /**
  165. * 检测错误消息是否匹配任何规则
  166. *
  167. * 检测顺序(性能优先):
  168. * 1. 包含匹配(最快,O(n*m))
  169. * 2. 精确匹配(使用 Set,O(1))
  170. * 3. 正则匹配(最慢,但最灵活)
  171. *
  172. * @param errorMessage - 错误消息
  173. * @returns 检测结果
  174. */
  175. detect(errorMessage: string): ErrorDetectionResult {
  176. if (!errorMessage || errorMessage.length === 0) {
  177. return { matched: false };
  178. }
  179. const lowerMessage = errorMessage.toLowerCase();
  180. const trimmedMessage = lowerMessage.trim();
  181. // 1. 包含匹配(最快)
  182. for (const pattern of this.containsPatterns) {
  183. if (lowerMessage.includes(pattern.text)) {
  184. return {
  185. matched: true,
  186. category: pattern.category,
  187. pattern: pattern.text,
  188. matchType: "contains",
  189. };
  190. }
  191. }
  192. // 2. 精确匹配(O(1) 查询)
  193. const exactMatch = this.exactPatterns.get(trimmedMessage);
  194. if (exactMatch) {
  195. return {
  196. matched: true,
  197. category: exactMatch.category,
  198. pattern: exactMatch.text,
  199. matchType: "exact",
  200. };
  201. }
  202. // 3. 正则匹配(最慢,但最灵活)
  203. for (const { pattern, category } of this.regexPatterns) {
  204. if (pattern.test(errorMessage)) {
  205. return {
  206. matched: true,
  207. category,
  208. pattern: pattern.source,
  209. matchType: "regex",
  210. };
  211. }
  212. }
  213. return { matched: false };
  214. }
  215. /**
  216. * 获取缓存统计信息
  217. */
  218. getStats() {
  219. return {
  220. regexCount: this.regexPatterns.length,
  221. containsCount: this.containsPatterns.length,
  222. exactCount: this.exactPatterns.size,
  223. totalCount:
  224. this.regexPatterns.length + this.containsPatterns.length + this.exactPatterns.size,
  225. lastReloadTime: this.lastReloadTime,
  226. isLoading: this.isLoading,
  227. };
  228. }
  229. /**
  230. * 检查缓存是否为空
  231. */
  232. isEmpty(): boolean {
  233. return (
  234. this.regexPatterns.length === 0 &&
  235. this.containsPatterns.length === 0 &&
  236. this.exactPatterns.size === 0
  237. );
  238. }
  239. }
  240. /**
  241. * 全局单例导出
  242. */
  243. export const errorRuleDetector = new ErrorRuleDetector();