import { Injectable } from '@nestjs/common'; import { redactSensitive } from '../../common/security/redact-sensitive'; import { DeployRunSummary } from '../../deploy-runs/deploy-run.types'; import { WeComClient } from './wecom.client'; export type DeployNotificationEvent = | 'created' | 'queued' | 'succeeded' | 'failed' | 'canceled' | 'retried'; export type DeployNotificationResult = { status: 'sent' | 'skipped' | 'failed'; channel: 'wecom'; reason?: string; }; /** * 统一封装发布单企微通知,保证 webhook 只存在于后端配置边界内。 */ @Injectable() export class DeployNotificationService { constructor(private readonly weComClient: WeComClient) {} async sendDeployRunEvent( run: DeployRunSummary, event: DeployNotificationEvent, ): Promise { const health = this.weComClient.healthSummary(); if (health.status === 'not_configured') { return { status: 'skipped', channel: 'wecom', reason: health.message ?? 'WeCom webhook is not configured', }; } try { await this.weComClient.sendMarkdown({ title: this.titleFor(event), markdown: this.markdownFor(run, event), }); return { status: 'sent', channel: 'wecom' }; } catch (error) { return { status: 'failed', channel: 'wecom', reason: error instanceof Error ? redactSensitive({ message: error.message }).message : 'Unknown WeCom notification error', }; } } private titleFor(event: DeployNotificationEvent): string { if (event === 'canceled') { return 'DevOps 发布已取消'; } if (event === 'queued') { return 'DevOps 发布已进入 Jenkins 队列'; } if (event === 'succeeded') { return 'DevOps 发布成功'; } if (event === 'failed') { return 'DevOps 发布失败'; } if (event === 'retried') { return 'DevOps 发布已重试'; } return 'DevOps 发布已创建'; } private markdownFor( run: DeployRunSummary, event: DeployNotificationEvent, ): string { return [ `> 事件:${this.titleFor(event)}`, `> 项目:${run.projectKey}`, `> 环境:${run.environment}`, `> Ref:${run.ref}`, `> 状态:${run.status}`, `> 操作者:${run.operator}`, `> Run ID:${run.id}`, run.jenkinsQueueId ? `> Jenkins Queue:${run.jenkinsQueueId}` : undefined, run.jenkinsBuildNumber ? `> Jenkins Build:#${run.jenkinsBuildNumber}` : undefined, run.failureSummary ? `> 失败摘要:${run.failureSummary}` : undefined, ] .filter(Boolean) .join('\n'); } }