diff --git a/server/dukang-api/src/modules/common/client-error.service.ts b/server/dukang-api/src/modules/common/client-error.service.ts index 15f4215..2e7a814 100644 --- a/server/dukang-api/src/modules/common/client-error.service.ts +++ b/server/dukang-api/src/modules/common/client-error.service.ts @@ -23,6 +23,14 @@ export class ClientErrorService { const stack = dto.stack?.trim().slice(0, 4000); const pagePath = dto.pagePath?.trim().slice(0, 128); const fingerprint = `${dto.level}|${dto.category}|${message.slice(0, 120)}`; + const anonymous = user?.actorId == null; + const skipWecom = shouldSkipWecomClientErrorAlert({ + message, + pagePath, + clientApp, + anonymous, + category: dto.category, + }); const logLine = { level: dto.level, @@ -34,6 +42,7 @@ export class ClientErrorService { actorId: user?.actorId != null ? String(user.actorId) : undefined, stack: stack?.slice(0, 800), extra: dto.extra, + skipWecom, }; if (dto.level === 'fatal') { @@ -61,6 +70,7 @@ export class ClientErrorService { actorType: user?.actorType ?? null, actorId: user?.actorId != null ? String(user.actorId) : null, storeId: user?.storeId != null ? String(user.storeId) : null, + skipWecom, ...(dto.extra && typeof dto.extra === 'object' ? { clientExtra: dto.extra } : {}), } as Prisma.InputJsonValue, }, @@ -71,7 +81,7 @@ export class ClientErrorService { ); } - if (WECOM_LEVELS.has(dto.level)) { + if (WECOM_LEVELS.has(dto.level) && !skipWecom) { const alertLevel: AlertLevel = dto.level === 'fatal' ? 'P0' : 'P1'; this.alert.notify({ level: alertLevel, @@ -91,12 +101,56 @@ export class ClientErrorService { dedupeKey: `client_error|${fingerprint}`, dedupeTtlSec: 600, }); + } else if (skipWecom && WECOM_LEVELS.has(dto.level)) { + this.logger.log( + `[client_error] skip WeCom alert (likely mini-program audit noise): ${message.slice(0, 160)}`, + ); } return { ok: true }; } } +/** + * 过滤企微推送:微信小程序审核机 / 自动化探测常见噪声。 + * 仍落库与写服务端日志,仅跳过 webhook。 + * + * 典型特征(与本次协议页报错一致): + * - 匿名 USER_MINI + * - navigateTo/redirectTo 等 page … is not found(常带 .html,Taro H5 路径形态) + */ +export function shouldSkipWecomClientErrorAlert(input: { + message: string; + pagePath?: string | null; + clientApp: string; + anonymous: boolean; + category?: string; +}): boolean { + const msg = input.message || ''; + const app = input.clientApp || ''; + const isMini = app === 'USER_MINI' || app === 'PARTNER_MINI' || app === 'HQ_MINI'; + + // 路由页不存在:审核机点协议/隐私链接触发最常见 + const isNavPageMissing = + /(navigateTo|redirectTo|reLaunch|switchTab):fail/i.test(msg) && + /is not found/i.test(msg); + + // Taro 把路径拼成 *.html 的形态,几乎不可能是真·原生页路径 + const isTaroHtmlPagePath = + /\.html(\b|"|')/i.test(msg) && /is not found|page /i.test(msg); + + if (isMini && input.anonymous && (isNavPageMissing || isTaroHtmlPagePath)) { + return true; + } + + // 即使带登录态,纯 *.html not found 也视为框架/探测噪声 + if (isMini && isTaroHtmlPagePath) { + return true; + } + + return false; +} + function isClientApp(v: string): v is ClientApp { return [ 'USER_MINI',