Skip to content

🔗 Webhook 模块

Webhook 模块为聊天系统提供异步通知机制,允许服务器主动向客户端推送重要事件和状态更新。这对于长时间运行的任务、断开连接场景以及实时通知至关重要。

📋 概述

Webhook 模块的核心功能:

  • 异步通知: 服务器主动推送事件到客户端
  • 长任务监控: 跟踪长时间运行任务的状态变化
  • 断线重连: 处理连接中断后的状态同步
  • 安全验证: 确保 Webhook 请求的真实性和安全性

🚀 快速开始

基础配置

ts
import { scp } from '@your-org/scp-sdk';

// 配置 Webhook 接收端点
const webhookConfig = {
  endpoint: 'https://your-app.com/api/webhooks/chat',
  secret: 'your-webhook-secret-key',
  events: ['task_completed', 'message_received', 'session_updated'],
  retryPolicy: {
    maxRetries: 3,
    retryDelay: 1000
  }
};

// 注册 Webhook
await scp.chat.webhook.register(webhookConfig);

接收 Webhook 事件

ts
// Express.js 示例
app.post('/api/webhooks/chat', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const payload = req.body;
  
  // 验证签名
  if (!verifyWebhookSignature(payload, signature, webhookConfig.secret)) {
    return res.status(401).send('Invalid signature');
  }
  
  // 处理事件
  const event = JSON.parse(payload);
  handleWebhookEvent(event);
  
  res.status(200).send('OK');
});

📡 使用场景

1. 长时间运行任务通知

ts
// 场景:AI 模型训练任务
const trainingTask = {
  taskId: 'training_001',
  type: 'model_training',
  estimatedDuration: 3600000, // 1小时
  webhookEvents: [
    'task_started',
    'task_progress',
    'task_completed',
    'task_failed'
  ]
};

// 启动任务时注册 Webhook
await scp.chat.startTask(trainingTask);

// Webhook 事件处理
function handleWebhookEvent(event) {
  switch (event.type) {
    case 'task_started':
      updateUI({
        status: 'running',
        message: '模型训练已开始',
        startTime: event.timestamp
      });
      break;
      
    case 'task_progress':
      updateUI({
        progress: event.data.progress,
        message: `训练进度: ${event.data.progress}%`,
        eta: event.data.estimatedCompletion
      });
      break;
      
    case 'task_completed':
      updateUI({
        status: 'completed',
        message: '模型训练完成',
        result: event.data.result,
        duration: event.data.duration
      });
      break;
      
    case 'task_failed':
      updateUI({
        status: 'failed',
        message: '模型训练失败',
        error: event.data.error
      });
      break;
  }
}

2. 断线重连场景

ts
// 处理连接中断后的状态同步
class ChatSessionManager {
  private sessionId: string;
  private lastSyncTime: number;
  
  async handleReconnection() {
    // 请求错过的事件
    const missedEvents = await scp.chat.webhook.getMissedEvents({
      sessionId: this.sessionId,
      since: this.lastSyncTime
    });
    
    // 处理错过的事件
    for (const event of missedEvents) {
      this.handleWebhookEvent(event);
    }
    
    // 更新同步时间
    this.lastSyncTime = Date.now();
  }
  
  private handleWebhookEvent(event: WebhookEvent) {
    switch (event.type) {
      case 'message_received':
        this.addMessageToChat(event.data.message);
        break;
        
      case 'session_updated':
        this.updateSessionState(event.data.session);
        break;
        
      case 'user_joined':
        this.addUserToSession(event.data.user);
        break;
        
      case 'user_left':
        this.removeUserFromSession(event.data.userId);
        break;
    }
  }
}

3. 实时协作通知

ts
// 多用户协作场景
const collaborationEvents = {
  'document_edited': (event) => {
    showNotification({
      type: 'info',
      message: `${event.data.user.name} 正在编辑文档`,
      document: event.data.document
    });
  },
  
  'user_typing': (event) => {
    showTypingIndicator({
      userId: event.data.userId,
      userName: event.data.userName,
      sessionId: event.data.sessionId
    });
  },
  
  'message_reaction': (event) => {
    updateMessageReactions({
      messageId: event.data.messageId,
      reaction: event.data.reaction,
      user: event.data.user
    });
  }
};

🔒 安全机制

1. 签名验证

ts
import crypto from 'crypto';

// 生成 Webhook 签名
function generateWebhookSignature(payload: string, secret: string): string {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(payload);
  return `sha256=${hmac.digest('hex')}`;
}

// 验证 Webhook 签名
function verifyWebhookSignature(
  payload: string, 
  signature: string, 
  secret: string
): boolean {
  const expectedSignature = generateWebhookSignature(payload, secret);
  
  // 使用时间安全的比较方法
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// 中间件示例
function webhookVerificationMiddleware(secret: string) {
  return (req, res, next) => {
    const signature = req.headers['x-webhook-signature'];
    const payload = req.body;
    
    if (!signature) {
      return res.status(401).json({ error: 'Missing signature' });
    }
    
    if (!verifyWebhookSignature(payload, signature, secret)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    
    next();
  };
}

2. IP 白名单

ts
// IP 白名单配置
const allowedIPs = [
  '192.168.1.0/24',
  '10.0.0.0/8',
  '172.16.0.0/12'
];

function isIPAllowed(clientIP: string): boolean {
  return allowedIPs.some(range => {
    if (range.includes('/')) {
      // CIDR 范围检查
      return isIPInCIDR(

clientIP, range);
    } else {
      // 精确 IP 匹配
      return clientIP === range;
    }
  });
}

// IP 白名单中间件
function ipWhitelistMiddleware(req, res, next) {
  const clientIP = req.ip || req.connection.remoteAddress;
  
  if (!isIPAllowed(clientIP)) {
    return res.status(403).json({ 
      error: 'IP not allowed',
      ip: clientIP 
    });
  }
  
  next();
}

3. 时间戳验证

ts
// 防重放攻击
function verifyTimestamp(timestamp: number, toleranceMs: number = 300000): boolean {
  const now = Date.now();
  const diff = Math.abs(now - timestamp);
  
  return diff <= toleranceMs; // 5分钟容差
}

// 完整的安全验证中间件
function secureWebhookMiddleware(config: {
  secret: string;
  allowedIPs?: string[];
  timestampTolerance?: number;
}) {
  return (req, res, next) => {
    try {
      // 1. IP 白名单检查
      if (config.allowedIPs) {
        const clientIP = req.ip || req.connection.remoteAddress;
        if (!isIPAllowed(clientIP)) {
          return res.status(403).json({ error: 'IP not allowed' });
        }
      }
      
      // 2. 签名验证
      const signature = req.headers['x-webhook-signature'];
      if (!signature || !verifyWebhookSignature(req.body, signature, config.secret)) {
        return res.status(401).json({ error: 'Invalid signature' });
      }
      
      // 3. 时间戳验证
      const timestamp = parseInt(req.headers['x-webhook-timestamp'] as string);
      if (!verifyTimestamp(timestamp, config.timestampTolerance)) {
        return res.status(401).json({ error: 'Invalid timestamp' });
      }
      
      next();
    } catch (error) {
      res.status(400).json({ error: 'Webhook validation failed' });
    }
  };
}

📊 事件类型

1. 任务相关事件

ts
interface TaskEvent {
  type: 'task_started' | 'task_progress' | 'task_completed' | 'task_failed';
  taskId: string;
  timestamp: number;
  data: {
    progress?: number;
    result?: any;
    error?: string;
    estimatedCompletion?: number;
    duration?: number;
  };
}

// 事件处理器
const taskEventHandlers = {
  task_started: (event: TaskEvent) => {
    console.log(`任务 ${event.taskId} 已开始`);
    updateTaskStatus(event.taskId, 'running');
  },
  
  task_progress: (event: TaskEvent) => {
    console.log(`任务 ${event.taskId} 进度: ${event.data.progress}%`);
    updateTaskProgress(event.taskId, event.data.progress);
  },
  
  task_completed: (event: TaskEvent) => {
    console.log(`任务 ${event.taskId} 已完成`);
    updateTaskStatus(event.taskId, 'completed');
    showTaskResult(event.taskId, event.data.result);
  },
  
  task_failed: (event: TaskEvent) => {
    console.error(`任务 ${event.taskId} 失败: ${event.data.error}`);
    updateTaskStatus(event.taskId, 'failed');
    showTaskError(event.taskId, event.data.error);
  }
};

2. 聊天相关事件

ts
interface ChatEvent {
  type: 'message_received' | 'message_updated' | 'message_deleted' | 'typing_indicator';
  sessionId: string;
  userId?: string;
  timestamp: number;
  data: {
    message?: ChatMessage;
    messageId?: string;
    isTyping?: boolean;
  };
}

// 聊天事件处理
const chatEventHandlers = {
  message_received: (event: ChatEvent) => {
    addMessageToChat(event.sessionId, event.data.message);
    playNotificationSound();
  },
  
  message_updated: (event: ChatEvent) => {
    updateMessage(event.data.messageId, event.data.message);
  },
  
  message_deleted: (event: ChatEvent) => {
    removeMessage(event.data.messageId);
  },
  
  typing_indicator: (event: ChatEvent) => {
    showTypingIndicator(event.sessionId, event.userId, event.data.isTyping);
  }
};

3. 系统相关事件

ts
interface SystemEvent {
  type: 'system_maintenance' | 'service_update' | 'rate_limit_warning';
  timestamp: number;
  data: {
    message: string;
    severity: 'info' | 'warning' | 'error';
    maintenanceWindow?: {
      start: number;
      end: number;
    };
    rateLimit?: {
      current: number;
      limit: number;
      resetTime: number;
    };
  };
}

🔄 重试机制

1. 指数退避重试

ts
class WebhookRetryManager {
  private retryQueue: Map<string, RetryItem> = new Map();
  
  async sendWebhook(
    url: string, 
    payload: any, 
    options: {
      maxRetries?: number;
      initialDelay?: number;
      maxDelay?: number;
      backoffMultiplier?: number;
    } = {}
  ) {
    const {
      maxRetries = 3,
      initialDelay = 1000,
      maxDelay = 30000,
      backoffMultiplier = 2
    } = options;
    
    let attempt = 0;
    let delay = initialDelay;
    
    while (attempt <= maxRetries) {
      try {
        const response = await fetch(url, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-Webhook-Signature': generateWebhookSignature(JSON.stringify(payload), this.secret),
            'X-Webhook-Timestamp': Date.now().toString()
          },
          body: JSON.stringify(payload)
        });
        
        if (response.ok) {
          console.log(`Webhook 发送成功: ${url}`);
          return response;
        }
        
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      } catch (error) {
        attempt++;
        
        if (attempt > maxRetries) {
          console.error(`Webhook 发送失败,已达到最大重试次数: ${error.message}`);
          throw error;
        }
        
        console.warn(`Webhook 发送失败,${delay}ms 后重试 (${attempt}/${maxRetries}): ${error.message}`);
        
        await this.sleep(delay);
        delay = Math.min(delay * backoffMultiplier, maxDelay);
      }
    }
  }
  
  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

2. 死信队列处理

ts
class DeadLetterQueue {
  private failedWebhooks: FailedWebhook[] = [];
  
  addFailedWebhook(webhook: FailedWeb

hook) {
    this.failedWebhooks.push({
      ...webhook,
      failedAt: Date.now(),
      retryCount: webhook.retryCount || 0
    });
  }
  
  async processDeadLetterQueue() {
    const now = Date.now();
    const retryableWebhooks = this.failedWebhooks.filter(webhook => {
      // 24小时后重试
      return now - webhook.failedAt > 24 * 60 * 60 * 1000 && webhook.retryCount < 5;
    });
    
    for (const webhook of retryableWebhooks) {
      try {
        await this.retryWebhook(webhook);
        // 成功后从死信队列移除
        this.removeFromDeadLetterQueue(webhook.id);
      } catch (error) {
        // 更新重试次数
        webhook.retryCount++;
        if (webhook.retryCount >= 5) {
          console.error(`Webhook ${webhook.id} 已达到最大重试次数,永久失败`);
          this.removeFromDeadLetterQueue(webhook.id);
        }
      }
    }
  }
}

🛠️ 实际应用示例

1. Next.js 应用集成

ts
// pages/api/webhooks/chat.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { verifyWebhookSignature } from '@/lib/webhook-utils';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }
  
  try {
    // 验证签名
    const signature = req.headers['x-webhook-signature'] as string;
    const isValid = verifyWebhookSignature(
      JSON.stringify(req.body),
      signature,
      process.env.WEBHOOK_SECRET!
    );
    
    if (!isValid) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    
    // 处理事件
    const event = req.body;
    await handleWebhookEvent(event);
    
    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
}

async function handleWebhookEvent(event: any) {
  switch (event.type) {
    case 'task_completed':
      await notifyTaskCompletion(event);
      break;
    case 'message_received':
      await broadcastNewMessage(event);
      break;
    default:
      console.log(`未处理的事件类型: ${event.type}`);
  }
}

2. Vue.js 应用集成

vue
<template>
  <div class="webhook-status">
    <div v-if="webhookConnected" class="status-connected">
      🟢 Webhook 已连接
    </div>
    <div v-else class="status-disconnected">
      🔴 Webhook 未连接
    </div>
    
    <div class="recent-events">
      <h3>最近事件</h3>
      <div v-for="event in recentEvents" :key="event.id" class="event-item">
        <span class="event-type">{{ event.type }}</span>
        <span class="event-time">{{ formatTime(event.timestamp) }}</span>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { scp } from '@your-org/scp-sdk';

const webhookConnected = ref(false);
const recentEvents = ref([]);
let eventSource: EventSource | null = null;

onMounted(async () => {
  // 注册 Webhook
  await registerWebhook();
  
  // 建立 Server-Sent Events 连接用于实时更新
  eventSource = new EventSource('/api/webhook-events');
  eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
    handleRealtimeEvent(data);
  };
});

onUnmounted(() => {
  if (eventSource) {
    eventSource.close();
  }
});

async function registerWebhook() {
  try {
    await scp.chat.webhook.register({
      endpoint: `${window.location.origin}/api/webhooks/chat`,
      secret: process.env.VUE_APP_WEBHOOK_SECRET,
      events: ['task_completed', 'message_received', 'session_updated']
    });
    webhookConnected.value = true;
  } catch (error) {
    console.error('Webhook 注册失败:', error);
    webhookConnected.value = false;
  }
}

function handleRealtimeEvent(event: any) {
  recentEvents.value.unshift(event);
  if (recentEvents.value.length > 10) {
    recentEvents.value.pop();
  }
}

function formatTime(timestamp: number) {
  return new Date(timestamp).toLocaleTimeString();
}
</script>

3. React 应用集成

tsx
import React, { useState, useEffect, useCallback } from 'react';
import { scp } from '@your-org/scp-sdk';

interface WebhookEvent {
  id: string;
  type: string;
  timestamp: number;
  data: any;
}

export const WebhookManager: React.FC = () => {
  const [isConnected, setIsConnected] = useState(false);
  const [events, setEvents] = useState<WebhookEvent[]>([]);
  const [error, setError] = useState<string | null>(null);

  const handleWebhookEvent = useCallback((event: WebhookEvent) => {
    setEvents(prev => [event, ...prev.slice(0, 9)]);
    
    // 根据事件类型执行相应操作
    switch (event.type) {
      case 'task_completed':
        showNotification('任务完成', event.data.message);
        break;
      case 'message_received':
        updateChatUI(event.data.message);
        break;
      case 'system_maintenance':
        showMaintenanceAlert(event.data);
        break;
    }
  }, []);

  useEffect(() => {
    const initWebhook = async () => {
      try {
        await scp.chat.webhook.register({
          endpoint: `${window.location.origin}/api/webhooks/chat`,
          secret: process.env.REACT_APP_WEBHOOK_SECRET!,
          events: ['*'], // 监听所有事件
          onEvent: handleWebhookEvent
        });
        
        setIsConnected(true);
        setError(null);
      } catch (err) {
        setError(err instanceof Error ? err.message : '未知错误');
        setIsConnected(false);
      }
    };

    initWebhook();

    return () => {
      scp.chat.webhook.unregister();
    };
  }, [handleWebhookEvent]);

  return (
    <div className="webhook-manager">
      <div className={`status ${isConnected ? 'connected' : 'disconnected'}`}>
        {isConnected ? '🟢 已连接' : '🔴 未连接'}
        {error && <span className="error">: {error}</span>}
      </div>
      
      <div className="events-list">
        <h3>最近事件</h3>
        {events.map(event => (
          <div key={event.id} className="event-item">
            <span className="event-type">{event.type}</span>
            <span className="event-time">
              {new Date(event.timestamp).toLocaleTimeString()}
            </span>
            <span className="event-data">
              {JSON.stringify(event.data, null, 2)}
            </span>
          </div>
        ))}
      </div>
    </div>
  );
};

🔒 安全最佳实践

1. 密钥管理

ts
// 环境变量配置
const webhookConfig = {
  // ✅ 好的做法:使用环境变量
  secret: process.env.WEBHOOK_SECRET,
  
  // ❌ 不好的做法:硬编码密钥
  // secret: 'my-secret-key'
};

// 密钥轮换
class WebhookSecretManager {
  private currentSecret: string;
  private previousSecret: string | null = null;
  
  constructor(secret: string) {
    this.currentSecret = secret;
  }
  
  // 轮换密钥
  rotateSecret(newSecret: string) {
    this.previousSecret = this.currentSecret;
    this.currentSecret = newSecret;
  }
  
  // 验证签名(支持新旧密钥)
  verifySignature(payload: string, signature: string): boolean {
    // 先尝试当前密钥
    if (verifyWebhookSignature(payload, signature, this.currentSecret)) {
      return true;
    }
    
    // 如果失败且存在旧密钥,尝试旧密钥
    if (this.previousSecret && 
        verifyWebhookSignature(payload, signature, this.previousSecret)) {
      return true;
    }
    
    return false;
  }
}

2. 速率限制

ts
class WebhookRateLimiter {
  private requests: Map<string, number[]> = new Map();
  
  isAllowed(clientIP: string, maxRequests: number = 100, windowMs: number = 60000): boolean {
    const now = Date.now();
    const windowStart = now - windowMs;
    
    // 获取客户端请求历史
    let clientRequests = this.requests.get(clientIP) || [];
    
    // 清理过期请求
    clientRequests = clientRequests.filter(timestamp => timestamp > windowStart);
    
    // 检查是否超过限制
    if (clientRequests.length >= maxRequests) {
      return false;
    }
    
    // 记录新请求
    clientRequests.push(now);
    this.requests.set(clientIP, clientRequests);
    
    return true;
  }
  
  // 清理过期数据
  cleanup() {
    const now = Date.now();
    const oneHourAgo = now - 60 * 60 * 1000;
    
    for (const [clientIP, requests] of this.requests.entries()) {
      const validRequests = requests.filter(timestamp => timestamp > oneHourAgo);
      
      if (validRequests.length === 0) {
        this.requests.delete(clientIP);
      } else {
        this.requests.set(clientIP, validRequests);
      }
    }
  }
}

// 使用中间件
const rateLimiter = new WebhookRateLimiter();

function rateLimitMiddleware(req, res, next) {
  const clientIP = req.ip || req.connection.remoteAddress;
  
  if (!rateLimiter.isAllowed(clientIP)) {
    return res.status(429).json({
      error: 'Too many requests',
      retryAfter: 60
    });
  }
  
  next();
}

3. 请求日志和监控

ts
class WebhookLogger {
  private logs: WebhookLog[] = [];
  
  logRequest(req: any, res: any, processingTime: number) {
    const log: WebhookLog = {
      timestamp: Date.now(),
      clientIP: req.ip || req.connection.remoteAddress,
      userAgent: req.headers['user-agent'],
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      processingTime,
      payloadSize: req.headers['content-length'] || 0,
      signature: req.headers['x-webhook-signature'] ? 'present' : 'missing'
    };
    
    this.logs.push(log);
    
    // 保持最近1000条日志
    if (this.logs.length > 1000) {
      this.logs.shift();
    }
    
    // 异常请求告警
    if (res.statusCode >= 400) {
      this.alertOnSuspiciousActivity(log);
    }
  }
  
  private alertOnSuspiciousActivity(log: WebhookLog) {
    // 检测可能的攻击模式
    const recentFailures = this.logs.filter(l => 
      l.clientIP === log.clientIP && 
      l.statusCode >= 400 && 
      Date.now() - l.timestamp < 60000 // 最近1分钟
    );
    
    if (recentFailures.length > 10) {
      console.warn(`可能的攻击检测: IP ${log.clientIP} 在1分钟内失败请求 ${recentFailures.length} 次`);
      // 这里可以集成告警系统
      this.sendAlert({
        type: 'suspicious_activity',
        clientIP: log.clientIP,
        failureCount: recentFailures.length,
        timeWindow: '1分钟'
      });
    }
  }
  
  private sendAlert(alert: any) {
    // 集成告警系统(如邮件、Slack、钉钉等)
    console.log('🚨 安全告警:', alert);
  }
}

🐛 故障排除

常见问题

Q1: Webhook 请求失败,返回 401 错误

可能原因:

  • 签名验证失败
  • 时间戳过期
  • 密钥配置错误

解决方案:

ts
// 调试签名验证
function debugSignatureVerification(payload: string, signature: string, secret: string) {
  console.log('调试信息:');
  console.log('Payload:', payload);
  console.log('Received signature:', signature);
  
  const expectedSignature = generateWebhookSignature(payload, secret);
  console.log('Expected signature:', expectedSignature);
  
  const isValid = signature === expectedSignature;
  console.log('Signature valid:', isValid);
  
  if (!isValid) {
    console.log('可能的问题:');
    console.log('1. 检查密钥是否正确');
    console.log('2. 检查 payload 是否被修改');
    console.log('3. 检查签名算法是否一致');
  }
  
  return isValid;
}

Q2: Webhook 接收到重复事件

解决方案:

ts
class WebhookDeduplicator {
  private processedEvents: Set<string> = new Set();
  private eventTTL: number = 5 * 60 * 1000; // 5分钟
  
  isDuplicate(eventId: string):
 boolean {
    return this.processedEvents.has(eventId);
  }
  
  markAsProcessed(eventId: string) {
    this.processedEvents.add(eventId);
    
    // 定期清理过期事件ID
    setTimeout(() => {
      this.processedEvents.delete(eventId);
    }, this.eventTTL);
  }
  
  processEvent(event: any) {
    if (this.isDuplicate(event.id)) {
      console.log(`跳过重复事件: ${event.id}`);
      return false;
    }
    
    this.markAsProcessed(event.id);
    return true;
  }
}

Q3: Webhook 处理超时

解决方案:

ts
// 设置处理超时
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
  return Promise.race([
    promise,
    new Promise<T>((_, reject) => {
      setTimeout(() => reject(new Error(`处理超时 (${timeoutMs}ms)`)), timeoutMs);
    })
  ]);
}

// 异步处理 Webhook
async function handleWebhookAsync(event: any) {
  try {
    // 快速响应客户端
    res.status(200).send('OK');
    
    // 异步处理事件
    await withTimeout(processWebhookEvent(event), 30000);
  } catch (error) {
    console.error('Webhook 处理失败:', error);
    // 记录到错误日志或重试队列
  }
}

Q4: 网络连接问题

解决方案:

ts
// 健康检查端点
app.get('/api/webhooks/health', (req, res) => {
  res.status(200).json({
    status: 'healthy',
    timestamp: Date.now(),
    version: process.env.APP_VERSION
  });
});

// 连接测试工具
class WebhookConnectivityTester {
  async testConnection(webhookUrl: string): Promise<boolean> {
    try {
      const response = await fetch(`${webhookUrl}/health`, {
        method: 'GET',
        timeout: 5000
      });
      
      return response.ok;
    } catch (error) {
      console.error(`连接测试失败: ${error.message}`);
      return false;
    }
  }
  
  async testWebhookDelivery(webhookUrl: string, secret: string): Promise<boolean> {
    const testPayload = {
      type: 'test_event',
      timestamp: Date.now(),
      data: { message: 'This is a test webhook' }
    };
    
    try {
      const response = await fetch(webhookUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Webhook-Signature': generateWebhookSignature(JSON.stringify(testPayload), secret)
        },
        body: JSON.stringify(testPayload),
        timeout: 10000
      });
      
      return response.ok;
    } catch (error) {
      console.error(`Webhook 投递测试失败: ${error.message}`);
      return false;
    }
  }
}

📊 监控和分析

性能监控

ts
class WebhookMetrics {
  private metrics = {
    totalRequests: 0,
    successfulRequests: 0,
    failedRequests: 0,
    averageProcessingTime: 0,
    processingTimes: [] as number[]
  };
  
  recordRequest(processingTime: number, success: boolean) {
    this.metrics.totalRequests++;
    
    if (success) {
      this.metrics.successfulRequests++;
    } else {
      this.metrics.failedRequests++;
    }
    
    // 记录处理时间
    this.metrics.processingTimes.push(processingTime);
    
    // 保持最近1000次请求的数据
    if (this.metrics.processingTimes.length > 1000) {
      this.metrics.processingTimes.shift();
    }
    
    // 计算平均处理时间
    this.metrics.averageProcessingTime = 
      this.metrics.processingTimes.reduce((a, b) => a + b, 0) / 
      this.metrics.processingTimes.length;
  }
  
  getMetrics() {
    const successRate = this.metrics.totalRequests > 0 ? 
      (this.metrics.successfulRequests / this.metrics.totalRequests) * 100 : 0;
    
    return {
      ...this.metrics,
      successRate,
      p95ProcessingTime: this.calculatePercentile(95),
      p99ProcessingTime: this.calculatePercentile(99)
    };
  }
  
  private calculatePercentile(percentile: number): number {
    if (this.metrics.processingTimes.length === 0) return 0;
    
    const sorted = [...this.metrics.processingTimes].sort((a, b) => a - b);
    const index = Math.ceil((percentile / 100) * sorted.length) - 1;
    return sorted[index];
  }
}

// 监控端点
app.get('/api/webhooks/metrics', (req, res) => {
  const metrics = webhookMetrics.getMetrics();
  res.json(metrics);
});

📖 参考资源

相关协议和标准

相关文档

🎯 总结

Webhook 模块为聊天系统提供了强大的异步通知能力,通过合理的设计和实现,可以:

  1. 提升用户体验: 实时推送重要事件和状态更新
  2. 增强系统可靠性: 处理长时间运行任务和断线重连场景
  3. 保证安全性: 通过签名验证、IP 白名单等机制确保安全
  4. 优化性能: 通过重试机制、速率限制等提升系统稳定性

关键要点:

  • 始终验证 Webhook 请求的真实性
  • 实现幂等性处理避免重复执行
  • 设置合理的超时和重试机制
  • 监控和日志记录用于故障排除
  • 遵循安全最佳实践保护系统安全

通过遵循这些最佳实践,你可以构建出可靠、安全且高效的 Webhook 系统。

Released under the MIT License.