sms-development

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SMS Development Guide

SMS开发指南

This skill provides guidance for developers working with the
@rytass/sms
base package, including creating new SMS adapters for Taiwan SMS service providers.
本指南为使用
@rytass/sms
基础包的开发者提供指导,包括为中国台湾地区短信服务提供商创建新的SMS Adapter。

Overview

概述

The
@rytass/sms
package defines the core interfaces and types that all SMS adapters must implement. It follows the adapter pattern to provide a unified API across different SMS providers.
@rytass/sms
包定义了所有SMS Adapter必须实现的核心接口和类型。它遵循适配器模式,为不同短信服务提供商提供统一的API。

Architecture

架构

@rytass/sms (Base Package)
    ├── SMSService<Request, SendResponse, MultiTarget>  # Service interface
    ├── SMSRequest                                       # Single message interface
    ├── SMSSendResponse                                  # Response interface
    ├── MultiTargetRequest                              # Batch message interface
    ├── SMSRequestResult                                # Status enum
    └── Taiwan Phone Number Helpers                     # Normalization utilities

@rytass/sms-adapter-*                                   # Provider implementations
    ├── [Provider]SMSService                            # Implements SMSService
    ├── [Provider]SMSRequest                            # Extends SMSRequest
    ├── [Provider]SMSSendResponse                       # Extends SMSSendResponse
    └── [Provider] specific types and errors
@rytass/sms (Base Package)
    ├── SMSService<Request, SendResponse, MultiTarget>  # Service interface
    ├── SMSRequest                                       # Single message interface
    ├── SMSSendResponse                                  # Response interface
    ├── MultiTargetRequest                              # Batch message interface
    ├── SMSRequestResult                                # Status enum
    └── Taiwan Phone Number Helpers                     # Normalization utilities

@rytass/sms-adapter-*                                   # Provider implementations
    ├── [Provider]SMSService                            # Implements SMSService
    ├── [Provider]SMSRequest                            # Extends SMSRequest
    ├── [Provider]SMSSendResponse                       # Extends SMSSendResponse
    └── [Provider] specific types and errors

Installation

安装

bash
npm install @rytass/sms
bash
npm install @rytass/sms

Core Interfaces

核心接口

SMSService

SMSService

The main interface that all SMS adapters must implement:
typescript
/**
 * SMS service interface
 * @template Request - SMS request type extending SMSRequest
 * @template SendResponse - SMS response type extending SMSSendResponse
 * @template MultiTarget - Multi-target request type extending MultiTargetRequest
 */
interface SMSService<
  Request extends SMSRequest,
  SendResponse extends SMSSendResponse,
  MultiTarget extends MultiTargetRequest,
> {
  /**
   * Send multiple SMS messages with different content
   * @param request - Array of SMS requests
   * @returns Promise resolving to array of send responses
   */
  send(request: Request[]): Promise<SendResponse[]>;

  /**
   * Send single SMS message
   * @param request - Single SMS request
   * @returns Promise resolving to send response
   */
  send(request: Request): Promise<SendResponse>;

  /**
   * Send same message to multiple recipients
   * @param request - Multi-target request with recipient list
   * @returns Promise resolving to array of send responses
   */
  send(request: MultiTarget): Promise<SendResponse[]>;
}
所有SMS Adapter必须实现的主接口:
typescript
/**
 * SMS service interface
 * @template Request - SMS request type extending SMSRequest
 * @template SendResponse - SMS response type extending SMSSendResponse
 * @template MultiTarget - Multi-target request type extending MultiTargetRequest
 */
interface SMSService<
  Request extends SMSRequest,
  SendResponse extends SMSSendResponse,
  MultiTarget extends MultiTargetRequest,
> {
  /**
   * Send multiple SMS messages with different content
   * @param request - Array of SMS requests
   * @returns Promise resolving to array of send responses
   */
  send(request: Request[]): Promise<SendResponse[]>;

  /**
   * Send single SMS message
   * @param request - Single SMS request
   * @returns Promise resolving to send response
   */
  send(request: Request): Promise<SendResponse>;

  /**
   * Send same message to multiple recipients
   * @param request - Multi-target request with recipient list
   * @returns Promise resolving to array of send responses
   */
  send(request: MultiTarget): Promise<SendResponse[]>;
}

Base Request Types

基础请求类型

typescript
/**
 * Base SMS request interface
 */
interface SMSRequest {
  /** Recipient mobile phone number */
  mobile: string;

  /** Message text content */
  text: string;
}

/**
 * Multi-target request for sending same message to multiple recipients
 */
interface MultiTargetRequest {
  /** Array of recipient mobile phone numbers */
  mobileList: string[];

  /** Message text content (same for all recipients) */
  text: string;
}
typescript
/**
 * Base SMS request interface
 */
interface SMSRequest {
  /** Recipient mobile phone number */
  mobile: string;

  /** Message text content */
  text: string;
}

/**
 * Multi-target request for sending same message to multiple recipients
 */
interface MultiTargetRequest {
  /** Array of recipient mobile phone numbers */
  mobileList: string[];

  /** Message text content (same for all recipients) */
  text: string;
}

Base Response Types

基础响应类型

typescript
/**
 * SMS request result status
 */
enum SMSRequestResult {
  /** Message sent successfully */
  SUCCESS = 'SUCCESS',

  /** Message delivery failed */
  FAILED = 'FAILED',
}

/**
 * Base SMS send response interface
 */
interface SMSSendResponse {
  /** Unique message identifier (if provided by gateway) */
  messageId?: string;

  /** Delivery status */
  status: SMSRequestResult;

  /** Recipient mobile phone number (normalized) */
  mobile: string;
}
typescript
/**
 * SMS request result status
 */
enum SMSRequestResult {
  /** Message sent successfully */
  SUCCESS = 'SUCCESS',

  /** Message delivery failed */
  FAILED = 'FAILED',
}

/**
 * Base SMS send response interface
 */
interface SMSSendResponse {
  /** Unique message identifier (if provided by gateway) */
  messageId?: string;

  /** Delivery status */
  status: SMSRequestResult;

  /** Recipient mobile phone number (normalized) */
  mobile: string;
}

Taiwan Phone Number Utilities

中国台湾地区手机号工具类

The base package provides utilities for handling Taiwan mobile numbers:
typescript
/**
 * Regular expression for validating Taiwan mobile phone numbers
 * Matches formats:
 * - 09XXXXXXXX (standard)
 * - 0912-345-678 (with dashes)
 * - 0912 345 678 (with spaces)
 * - +8869XXXXXXXX (international)
 * - 8869XXXXXXXX (international without +)
 */
const TAIWAN_PHONE_NUMBER_RE = /^(0|\+?886-?)9\d{2}-?\d{3}-?\d{3}$/;

/**
 * Normalize Taiwan mobile phone number to standard format (09XXXXXXXX)
 * @param mobile - Phone number in any supported format
 * @returns Normalized phone number (09XXXXXXXX)
 *
 * @example
 * normalizedTaiwanMobilePhoneNumber('0987-654-321') // '0987654321'
 * normalizedTaiwanMobilePhoneNumber('+886987654321') // '0987654321'
 * normalizedTaiwanMobilePhoneNumber('886987654321') // '0987654321'
 */
function normalizedTaiwanMobilePhoneNumber(mobile: string): string;
基础包提供了处理中国台湾地区手机号的工具:
typescript
/**
 * Regular expression for validating Taiwan mobile phone numbers
 * Matches formats:
 * - 09XXXXXXXX (standard)
 * - 0912-345-678 (with dashes)
 * - 0912 345 678 (with spaces)
 * - +8869XXXXXXXX (international)
 * - 8869XXXXXXXX (international without +)
 */
const TAIWAN_PHONE_NUMBER_RE = /^(0|\+?886-?)9\d{2}-?\d{3}-?\d{3}$/;

/**
 * Normalize Taiwan mobile phone number to standard format (09XXXXXXXX)
 * @param mobile - Phone number in any supported format
 * @returns Normalized phone number (09XXXXXXXX)
 *
 * @example
 * normalizedTaiwanMobilePhoneNumber('0987-654-321') // '0987654321'
 * normalizedTaiwanMobilePhoneNumber('+886987654321') // '0987654321'
 * normalizedTaiwanMobilePhoneNumber('886987654321') // '0987654321'
 */
function normalizedTaiwanMobilePhoneNumber(mobile: string): string;

Implementation Guide

实现指南

Step 1: Define Provider-Specific Types

步骤1:定义服务商专属类型

Create interfaces extending the base types:
typescript
import {
  SMSRequest,
  SMSSendResponse,
  MultiTargetRequest,
  SMSRequestResult,
} from '@rytass/sms';

/**
 * Provider-specific initialization options
 */
export interface MyProviderSMSRequestInit {
  /** API username/account ID */
  username: string;

  /** API password/secret key */
  password: string;

  /** API base URL (optional, defaults to production) */
  baseUrl?: string;

  /** Restrict to Taiwan mobile numbers only */
  onlyTaiwanMobileNumber?: boolean;

  // Add any other provider-specific config
}

/**
 * Provider-specific error codes
 */
export enum MyProviderError {
  /** Invalid credentials */
  INVALID_CREDENTIALS = -1,

  /** Invalid phone number format */
  FORMAT_ERROR = -2,

  /** Insufficient account balance */
  INSUFFICIENT_BALANCE = -3,

  /** Rate limit exceeded */
  RATE_LIMIT_EXCEEDED = -4,

  /** Unknown error */
  UNKNOWN = -99,
}

/**
 * Provider-specific SMS request
 * Extends base SMSRequest with additional fields if needed
 */
export interface MyProviderSMSRequest extends SMSRequest {
  mobile: string;
  text: string;

  // Add provider-specific fields if needed
  // priority?: 'low' | 'normal' | 'high';
  // scheduledTime?: Date;
}

/**
 * Provider-specific send response
 * Extends base SMSSendResponse with additional fields
 */
export interface MyProviderSMSSendResponse extends SMSSendResponse {
  messageId?: string;
  status: SMSRequestResult;
  mobile: string;

  /** Error message if delivery failed */
  errorMessage?: string;

  /** Provider-specific error code */
  errorCode?: MyProviderError;

  // Add provider-specific fields if needed
  // cost?: number;
  // remainingBalance?: number;
}

/**
 * Provider-specific multi-target request
 */
export interface MyProviderSMSMultiTargetRequest extends MultiTargetRequest {
  mobileList: string[];
  text: string;
}
创建继承基础类型的接口:
typescript
import {
  SMSRequest,
  SMSSendResponse,
  MultiTargetRequest,
  SMSRequestResult,
} from '@rytass/sms';

/**
 * Provider-specific initialization options
 */
export interface MyProviderSMSRequestInit {
  /** API username/account ID */
  username: string;

  /** API password/secret key */
  password: string;

  /** API base URL (optional, defaults to production) */
  baseUrl?: string;

  /** Restrict to Taiwan mobile numbers only */
  onlyTaiwanMobileNumber?: boolean;

  // Add any other provider-specific config
}

/**
 * Provider-specific error codes
 */
export enum MyProviderError {
  /** Invalid credentials */
  INVALID_CREDENTIALS = -1,

  /** Invalid phone number format */
  FORMAT_ERROR = -2,

  /** Insufficient account balance */
  INSUFFICIENT_BALANCE = -3,

  /** Rate limit exceeded */
  RATE_LIMIT_EXCEEDED = -4,

  /** Unknown error */
  UNKNOWN = -99,
}

/**
 * Provider-specific SMS request
 * Extends base SMSRequest with additional fields if needed
 */
export interface MyProviderSMSRequest extends SMSRequest {
  mobile: string;
  text: string;

  // Add provider-specific fields if needed
  // priority?: 'low' | 'normal' | 'high';
  // scheduledTime?: Date;
}

/**
 * Provider-specific send response
 * Extends base SMSSendResponse with additional fields
 */
export interface MyProviderSMSSendResponse extends SMSSendResponse {
  messageId?: string;
  status: SMSRequestResult;
  mobile: string;

  /** Error message if delivery failed */
  errorMessage?: string;

  /** Provider-specific error code */
  errorCode?: MyProviderError;

  // Add provider-specific fields if needed
  // cost?: number;
  // remainingBalance?: number;
}

/**
 * Provider-specific multi-target request
 */
export interface MyProviderSMSMultiTargetRequest extends MultiTargetRequest {
  mobileList: string[];
  text: string;
}

Step 2: Implement the SMS Service

步骤2:实现SMS服务

typescript
import {
  SMSService,
  SMSRequestResult,
  normalizedTaiwanMobilePhoneNumber,
  TAIWAN_PHONE_NUMBER_RE,
} from '@rytass/sms';
import axios from 'axios';

/**
 * SMS service implementation for MyProvider
 * Implements the SMSService interface with provider-specific logic
 */
export class SMSServiceMyProvider implements SMSService<
  MyProviderSMSRequest,
  MyProviderSMSSendResponse,
  MyProviderSMSMultiTargetRequest
> {
  private readonly username: string;
  private readonly password: string;
  private readonly baseUrl: string;
  private readonly onlyTaiwanMobileNumber: boolean;

  /**
   * Initialize SMS service
   * @param options - Provider configuration options
   */
  constructor(options: MyProviderSMSRequestInit) {
    this.username = options.username;
    this.password = options.password;
    this.baseUrl = options.baseUrl || 'https://api.myprovider.com';
    this.onlyTaiwanMobileNumber = options.onlyTaiwanMobileNumber || false;
  }

  /**
   * Send SMS message(s)
   * Handles three sending patterns:
   * 1. Single SMS to one recipient
   * 2. Multiple SMS with different messages
   * 3. Same message to multiple recipients
   */
  async send(requests: MyProviderSMSRequest[]): Promise<MyProviderSMSSendResponse[]>;
  async send(request: MyProviderSMSRequest): Promise<MyProviderSMSSendResponse>;
  async send(request: MyProviderSMSMultiTargetRequest): Promise<MyProviderSMSSendResponse[]>;

  async send(
    requests: MyProviderSMSMultiTargetRequest | MyProviderSMSRequest | MyProviderSMSRequest[],
  ): Promise<MyProviderSMSSendResponse | MyProviderSMSSendResponse[]> {
    // Validate input
    if (
      (Array.isArray(requests) && !requests.length) ||
      ((requests as MyProviderSMSMultiTargetRequest).mobileList &&
        !(requests as MyProviderSMSMultiTargetRequest).mobileList?.length)
    ) {
      throw new Error('No target provided.');
    }

    // Process and validate phone numbers
    const processedRequests = this.processRequests(requests);

    // Send to provider API
    const results = await this.sendToProvider(processedRequests);

    // Return results in appropriate format
    return this.formatResults(requests, results);
  }

  /**
   * Process and validate phone numbers
   * @param requests - Raw requests
   * @returns Processed requests with normalized phone numbers
   */
  private processRequests(
    requests: MyProviderSMSMultiTargetRequest | MyProviderSMSRequest | MyProviderSMSRequest[],
  ): Array<{ mobile: string; text: string }> {
    const requestArray = Array.isArray(requests) ? requests : [requests];
    const processed: Array<{ mobile: string; text: string }> = [];

    for (const request of requestArray) {
      if ((request as MyProviderSMSMultiTargetRequest).mobileList) {
        // Multi-target request
        const multiTarget = request as MyProviderSMSMultiTargetRequest;

        for (const mobile of multiTarget.mobileList) {
          const normalizedMobile = this.validateAndNormalizeMobile(mobile);
          processed.push({
            mobile: normalizedMobile,
            text: multiTarget.text,
          });
        }
      } else {
        // Single request
        const singleRequest = request as MyProviderSMSRequest;
        const normalizedMobile = this.validateAndNormalizeMobile(singleRequest.mobile);
        processed.push({
          mobile: normalizedMobile,
          text: singleRequest.text,
        });
      }
    }

    return processed;
  }

  /**
   * Validate and normalize phone number
   * @param mobile - Raw phone number
   * @returns Normalized phone number
   * @throws Error if number is invalid and onlyTaiwanMobileNumber is true
   */
  private validateAndNormalizeMobile(mobile: string): string {
    // Check if Taiwan number
    if (TAIWAN_PHONE_NUMBER_RE.test(mobile)) {
      return normalizedTaiwanMobilePhoneNumber(mobile);
    }

    // If strict Taiwan-only mode, reject non-Taiwan numbers
    if (this.onlyTaiwanMobileNumber) {
      throw new Error(
        `${mobile} is not taiwan mobile phone (\`onlyTaiwanMobileNumber\` option is true)`
      );
    }

    // Return as-is for international numbers
    return mobile;
  }

  /**
   * Send requests to provider API
   * @param requests - Processed requests
   * @returns API responses
   */
  private async sendToProvider(
    requests: Array<{ mobile: string; text: string }>,
  ): Promise<Map<string, MyProviderSMSSendResponse>> {
    // Group requests by message text for batch optimization
    const batches = this.groupByMessage(requests);
    const results = new Map<string, MyProviderSMSSendResponse>();

    // Send each batch
    for (const [message, mobileList] of batches.entries()) {
      try {
        // Call provider API
        const response = await this.callProviderAPI(mobileList, message);

        // Process API response
        const batchResults = this.parseAPIResponse(response, mobileList, message);

        // Store results
        for (const [key, result] of batchResults.entries()) {
          results.set(key, result);
        }
      } catch (error) {
        // Handle API errors
        for (const mobile of mobileList) {
          results.set(`${message}:${mobile}`, {
            status: SMSRequestResult.FAILED,
            mobile,
            errorMessage: error.message,
            errorCode: MyProviderError.UNKNOWN,
          });
        }
      }
    }

    return results;
  }

  /**
   * Group requests by message text for batch sending
   * @param requests - Array of requests
   * @returns Map of message text to mobile numbers
   */
  private groupByMessage(
    requests: Array<{ mobile: string; text: string }>,
  ): Map<string, string[]> {
    const batches = new Map<string, string[]>();

    for (const request of requests) {
      const existing = batches.get(request.text) || [];
      batches.set(request.text, [...existing, request.mobile]);
    }

    return batches;
  }

  /**
   * Call provider API
   * IMPORTANT: Implement this method according to your provider's API specification
   * @param mobileList - Array of phone numbers
   * @param message - Message text
   * @returns API response
   *
   * NOTE: Every8D uses the following API specification:
   * - Endpoint: `${baseUrl}/API21/HTTP/SendSMS.ashx`
   * - Method: POST with application/x-www-form-urlencoded
   * - Parameters: UID (username), PWD (password), MSG (message), DEST (comma-separated mobiles)
   * - Response: CSV format: "credit,sent,cost,unsent,batchId" or "errorCode,errorMessage"
   */
  private async callProviderAPI(
    mobileList: string[],
    message: string,
  ): Promise<any> {
    // Example implementation using Every8D API format
    // Other providers may use different endpoints and parameters
    const { data } = await axios.post(
      `${this.baseUrl}/API21/HTTP/SendSMS.ashx`,
      new URLSearchParams({
        UID: this.username,    // Every8D uses UID for username
        PWD: this.password,    // Every8D uses PWD for password
        MSG: message,          // Every8D uses MSG for message content
        DEST: mobileList.join(','), // Every8D uses DEST for comma-separated recipients
      }),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
      }
    );

    return data;
  }

  /**
   * Parse provider API response
   * IMPORTANT: Implement this method according to your provider's response format
   * @param response - API response data
   * @param mobileList - Array of phone numbers
   * @param message - Message text
   * @returns Map of results keyed by "message:mobile"
   */
  private parseAPIResponse(
    response: any,
    mobileList: string[],
    message: string,
  ): Map<string, MyProviderSMSSendResponse> {
    const results = new Map<string, MyProviderSMSSendResponse>();

    // Example parsing - REPLACE WITH ACTUAL PROVIDER RESPONSE PARSING
    if (response.success) {
      // All succeeded
      for (const mobile of mobileList) {
        results.set(`${message}:${mobile}`, {
          messageId: response.messageId,
          status: SMSRequestResult.SUCCESS,
          mobile,
        });
      }
    } else {
      // All failed
      for (const mobile of mobileList) {
        results.set(`${message}:${mobile}`, {
          status: SMSRequestResult.FAILED,
          mobile,
          errorMessage: response.errorMessage,
          errorCode: response.errorCode,
        });
      }
    }

    return results;
  }

  /**
   * Format results based on original request type
   * @param originalRequest - Original request
   * @param results - Processed results map
   * @returns Formatted response(s)
   */
  private formatResults(
    originalRequest: MyProviderSMSMultiTargetRequest | MyProviderSMSRequest | MyProviderSMSRequest[],
    results: Map<string, MyProviderSMSSendResponse>,
  ): MyProviderSMSSendResponse | MyProviderSMSSendResponse[] {
    // Multi-target request
    if ((originalRequest as MyProviderSMSMultiTargetRequest).mobileList) {
      const multiTarget = originalRequest as MyProviderSMSMultiTargetRequest;

      return multiTarget.mobileList.map(mobile => {
        const normalizedMobile = TAIWAN_PHONE_NUMBER_RE.test(mobile)
          ? normalizedTaiwanMobilePhoneNumber(mobile)
          : mobile;

        return results.get(`${multiTarget.text}:${normalizedMobile}`)!;
      });
    }

    // Array of requests
    if (Array.isArray(originalRequest)) {
      return originalRequest.map(request => {
        const normalizedMobile = TAIWAN_PHONE_NUMBER_RE.test(request.mobile)
          ? normalizedTaiwanMobilePhoneNumber(request.mobile)
          : request.mobile;

        return results.get(`${request.text}:${normalizedMobile}`)!;
      });
    }

    // Single request
    const singleRequest = originalRequest as MyProviderSMSRequest;
    const normalizedMobile = TAIWAN_PHONE_NUMBER_RE.test(singleRequest.mobile)
      ? normalizedTaiwanMobilePhoneNumber(singleRequest.mobile)
      : singleRequest.mobile;

    return results.get(`${singleRequest.text}:${normalizedMobile}`)!;
  }
}
typescript
import {
  SMSService,
  SMSRequestResult,
  normalizedTaiwanMobilePhoneNumber,
  TAIWAN_PHONE_NUMBER_RE,
} from '@rytass/sms';
import axios from 'axios';

/**
 * SMS service implementation for MyProvider
 * Implements the SMSService interface with provider-specific logic
 */
export class SMSServiceMyProvider implements SMSService<
  MyProviderSMSRequest,
  MyProviderSMSSendResponse,
  MyProviderSMSMultiTargetRequest
> {
  private readonly username: string;
  private readonly password: string;
  private readonly baseUrl: string;
  private readonly onlyTaiwanMobileNumber: boolean;

  /**
   * Initialize SMS service
   * @param options - Provider configuration options
   */
  constructor(options: MyProviderSMSRequestInit) {
    this.username = options.username;
    this.password = options.password;
    this.baseUrl = options.baseUrl || 'https://api.myprovider.com';
    this.onlyTaiwanMobileNumber = options.onlyTaiwanMobileNumber || false;
  }

  /**
   * Send SMS message(s)
   * Handles three sending patterns:
   * 1. Single SMS to one recipient
   * 2. Multiple SMS with different messages
   * 3. Same message to multiple recipients
   */
  async send(requests: MyProviderSMSRequest[]): Promise<MyProviderSMSSendResponse[]>;
  async send(request: MyProviderSMSRequest): Promise<MyProviderSMSSendResponse>;
  async send(request: MyProviderSMSMultiTargetRequest): Promise<MyProviderSMSSendResponse[]>;

  async send(
    requests: MyProviderSMSMultiTargetRequest | MyProviderSMSRequest | MyProviderSMSRequest[],
  ): Promise<MyProviderSMSSendResponse | MyProviderSMSSendResponse[]> {
    // Validate input
    if (
      (Array.isArray(requests) && !requests.length) ||
      ((requests as MyProviderSMSMultiTargetRequest).mobileList &&
        !(requests as MyProviderSMSMultiTargetRequest).mobileList?.length)
    ) {
      throw new Error('No target provided.');
    }

    // Process and validate phone numbers
    const processedRequests = this.processRequests(requests);

    // Send to provider API
    const results = await this.sendToProvider(processedRequests);

    // Return results in appropriate format
    return this.formatResults(requests, results);
  }

  /**
   * Process and validate phone numbers
   * @param requests - Raw requests
   * @returns Processed requests with normalized phone numbers
   */
  private processRequests(
    requests: MyProviderSMSMultiTargetRequest | MyProviderSMSRequest | MyProviderSMSRequest[],
  ): Array<{ mobile: string; text: string }> {
    const requestArray = Array.isArray(requests) ? requests : [requests];
    const processed: Array<{ mobile: string; text: string }> = [];

    for (const request of requestArray) {
      if ((request as MyProviderSMSMultiTargetRequest).mobileList) {
        // Multi-target request
        const multiTarget = request as MyProviderSMSMultiTargetRequest;

        for (const mobile of multiTarget.mobileList) {
          const normalizedMobile = this.validateAndNormalizeMobile(mobile);
          processed.push({
            mobile: normalizedMobile,
            text: multiTarget.text,
          });
        }
      } else {
        // Single request
        const singleRequest = request as MyProviderSMSRequest;
        const normalizedMobile = this.validateAndNormalizeMobile(singleRequest.mobile);
        processed.push({
          mobile: normalizedMobile,
          text: singleRequest.text,
        });
      }
    }

    return processed;
  }

  /**
   * Validate and normalize phone number
   * @param mobile - Raw phone number
   * @returns Normalized phone number
   * @throws Error if number is invalid and onlyTaiwanMobileNumber is true
   */
  private validateAndNormalizeMobile(mobile: string): string {
    // Check if Taiwan number
    if (TAIWAN_PHONE_NUMBER_RE.test(mobile)) {
      return normalizedTaiwanMobilePhoneNumber(mobile);
    }

    // If strict Taiwan-only mode, reject non-Taiwan numbers
    if (this.onlyTaiwanMobileNumber) {
      throw new Error(
        `${mobile} is not taiwan mobile phone (\`onlyTaiwanMobileNumber\` option is true)`
      );
    }

    // Return as-is for international numbers
    return mobile;
  }

  /**
   * Send requests to provider API
   * @param requests - Processed requests
   * @returns API responses
   */
  private async sendToProvider(
    requests: Array<{ mobile: string; text: string }>,
  ): Promise<Map<string, MyProviderSMSSendResponse>> {
    // Group requests by message text for batch optimization
    const batches = this.groupByMessage(requests);
    const results = new Map<string, MyProviderSMSSendResponse>();

    // Send each batch
    for (const [message, mobileList] of batches.entries()) {
      try {
        // Call provider API
        const response = await this.callProviderAPI(mobileList, message);

        // Process API response
        const batchResults = this.parseAPIResponse(response, mobileList, message);

        // Store results
        for (const [key, result] of batchResults.entries()) {
          results.set(key, result);
        }
      } catch (error) {
        // Handle API errors
        for (const mobile of mobileList) {
          results.set(`${message}:${mobile}`, {
            status: SMSRequestResult.FAILED,
            mobile,
            errorMessage: error.message,
            errorCode: MyProviderError.UNKNOWN,
          });
        }
      }
    }

    return results;
  }

  /**
   * Group requests by message text for batch sending
   * @param requests - Array of requests
   * @returns Map of message text to mobile numbers
   */
  private groupByMessage(
    requests: Array<{ mobile: string; text: string }>,
  ): Map<string, string[]> {
    const batches = new Map<string, string[]>();

    for (const request of requests) {
      const existing = batches.get(request.text) || [];
      batches.set(request.text, [...existing, request.mobile]);
    }

    return batches;
  }

  /**
   * Call provider API
   * IMPORTANT: Implement this method according to your provider's API specification
   * @param mobileList - Array of phone numbers
   * @param message - Message text
   * @returns API response
   *
   * NOTE: Every8D uses the following API specification:
   * - Endpoint: `${baseUrl}/API21/HTTP/SendSMS.ashx`
   * - Method: POST with application/x-www-form-urlencoded
   * - Parameters: UID (username), PWD (password), MSG (message), DEST (comma-separated mobiles)
   * - Response: CSV format: "credit,sent,cost,unsent,batchId" or "errorCode,errorMessage"
   */
  private async callProviderAPI(
    mobileList: string[],
    message: string,
  ): Promise<any> {
    // Example implementation using Every8D API format
    // Other providers may use different endpoints and parameters
    const { data } = await axios.post(
      `${this.baseUrl}/API21/HTTP/SendSMS.ashx`,
      new URLSearchParams({
        UID: this.username,    // Every8D uses UID for username
        PWD: this.password,    // Every8D uses PWD for password
        MSG: message,          // Every8D uses MSG for message content
        DEST: mobileList.join(','), // Every8D uses DEST for comma-separated recipients
      }),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
      }
    );

    return data;
  }

  /**
   * Parse provider API response
   * IMPORTANT: Implement this method according to your provider's response format
   * @param response - API response data
   * @param mobileList - Array of phone numbers
   * @param message - Message text
   * @returns Map of results keyed by "message:mobile"
   */
  private parseAPIResponse(
    response: any,
    mobileList: string[],
    message: string,
  ): Map<string, MyProviderSMSSendResponse> {
    const results = new Map<string, MyProviderSMSSendResponse>();

    // Example parsing - REPLACE WITH ACTUAL PROVIDER RESPONSE PARSING
    if (response.success) {
      // All succeeded
      for (const mobile of mobileList) {
        results.set(`${message}:${mobile}`, {
          messageId: response.messageId,
          status: SMSRequestResult.SUCCESS,
          mobile,
        });
      }
    } else {
      // All failed
      for (const mobile of mobileList) {
        results.set(`${message}:${mobile}`, {
          status: SMSRequestResult.FAILED,
          mobile,
          errorMessage: response.errorMessage,
          errorCode: response.errorCode,
        });
      }
    }

    return results;
  }

  /**
   * Format results based on original request type
   * @param originalRequest - Original request
   * @param results - Processed results map
   * @returns Formatted response(s)
   */
  private formatResults(
    originalRequest: MyProviderSMSMultiTargetRequest | MyProviderSMSRequest | MyProviderSMSRequest[],
    results: Map<string, MyProviderSMSSendResponse>,
  ): MyProviderSMSSendResponse | MyProviderSMSSendResponse[] {
    // Multi-target request
    if ((originalRequest as MyProviderSMSMultiTargetRequest).mobileList) {
      const multiTarget = originalRequest as MyProviderSMSMultiTargetRequest;

      return multiTarget.mobileList.map(mobile => {
        const normalizedMobile = TAIWAN_PHONE_NUMBER_RE.test(mobile)
          ? normalizedTaiwanMobilePhoneNumber(mobile)
          : mobile;

        return results.get(`${multiTarget.text}:${normalizedMobile}`)!;
      });
    }

    // Array of requests
    if (Array.isArray(originalRequest)) {
      return originalRequest.map(request => {
        const normalizedMobile = TAIWAN_PHONE_NUMBER_RE.test(request.mobile)
          ? normalizedTaiwanMobilePhoneNumber(request.mobile)
          : request.mobile;

        return results.get(`${request.text}:${normalizedMobile}`)!;
      });
    }

    // Single request
    const singleRequest = originalRequest as MyProviderSMSRequest;
    const normalizedMobile = TAIWAN_PHONE_NUMBER_RE.test(singleRequest.mobile)
      ? normalizedTaiwanMobilePhoneNumber(singleRequest.mobile)
      : singleRequest.mobile;

    return results.get(`${singleRequest.text}:${normalizedMobile}`)!;
  }
}

Step 3: Export Public API

步骤3:导出公共API

typescript
// index.ts
export { SMSServiceMyProvider } from './sms-service-my-provider';
export * from './typings';
typescript
// index.ts
export { SMSServiceMyProvider } from './sms-service-my-provider';
export * from './typings';

Step 4: Add Tests

步骤4:添加测试

typescript
// __tests__/sms-service-my-provider.spec.ts
import { SMSServiceMyProvider } from '../src/sms-service-my-provider';
import { SMSRequestResult } from '@rytass/sms';

describe('SMSServiceMyProvider', () => {
  let smsService: SMSServiceMyProvider;

  beforeEach(() => {
    smsService = new SMSServiceMyProvider({
      username: 'test-username',
      password: 'test-password',
      onlyTaiwanMobileNumber: true,
    });
  });

  describe('send - single SMS', () => {
    it('should send single SMS successfully', async () => {
      const result = await smsService.send({
        mobile: '0987654321',
        text: 'Test message',
      });

      expect(result.status).toBe(SMSRequestResult.SUCCESS);
      expect(result.mobile).toBe('0987654321');
      expect(result.messageId).toBeDefined();
    });

    it('should normalize Taiwan phone number', async () => {
      const result = await smsService.send({
        mobile: '+886987654321',
        text: 'Test message',
      });

      expect(result.mobile).toBe('0987654321');
    });

    it('should reject non-Taiwan number when onlyTaiwanMobileNumber is true', async () => {
      await expect(
        smsService.send({
          mobile: '+1234567890',
          text: 'Test message',
        })
      ).rejects.toThrow('is not taiwan mobile phone');
    });
  });

  describe('send - batch SMS', () => {
    it('should send same message to multiple recipients', async () => {
      const results = await smsService.send({
        mobileList: ['0987654321', '0912345678', '0923456789'],
        text: 'Batch message',
      });

      expect(results).toHaveLength(3);
      expect(results.every(r => r.status === SMSRequestResult.SUCCESS)).toBe(true);
    });

    it('should send different messages to multiple recipients', async () => {
      const results = await smsService.send([
        { mobile: '0987654321', text: 'Message 1' },
        { mobile: '0912345678', text: 'Message 2' },
        { mobile: '0923456789', text: 'Message 3' },
      ]);

      expect(results).toHaveLength(3);
      expect(results.every(r => r.status === SMSRequestResult.SUCCESS)).toBe(true);
    });
  });

  describe('error handling', () => {
    it('should handle API errors gracefully', async () => {
      // Mock API error
      // ... test implementation
    });

    it('should return FAILED status on delivery failure', async () => {
      // Mock failed delivery
      // ... test implementation
    });
  });
});
typescript
// __tests__/sms-service-my-provider.spec.ts
import { SMSServiceMyProvider } from '../src/sms-service-my-provider';
import { SMSRequestResult } from '@rytass/sms';

describe('SMSServiceMyProvider', () => {
  let smsService: SMSServiceMyProvider;

  beforeEach(() => {
    smsService = new SMSServiceMyProvider({
      username: 'test-username',
      password: 'test-password',
      onlyTaiwanMobileNumber: true,
    });
  });

  describe('send - single SMS', () => {
    it('should send single SMS successfully', async () => {
      const result = await smsService.send({
        mobile: '0987654321',
        text: 'Test message',
      });

      expect(result.status).toBe(SMSRequestResult.SUCCESS);
      expect(result.mobile).toBe('0987654321');
      expect(result.messageId).toBeDefined();
    });

    it('should normalize Taiwan phone number', async () => {
      const result = await smsService.send({
        mobile: '+886987654321',
        text: 'Test message',
      });

      expect(result.mobile).toBe('0987654321');
    });

    it('should reject non-Taiwan number when onlyTaiwanMobileNumber is true', async () => {
      await expect(
        smsService.send({
          mobile: '+1234567890',
          text: 'Test message',
        })
      ).rejects.toThrow('is not taiwan mobile phone');
    });
  });

  describe('send - batch SMS', () => {
    it('should send same message to multiple recipients', async () => {
      const results = await smsService.send({
        mobileList: ['0987654321', '0912345678', '0923456789'],
        text: 'Batch message',
      });

      expect(results).toHaveLength(3);
      expect(results.every(r => r.status === SMSRequestResult.SUCCESS)).toBe(true);
    });

    it('should send different messages to multiple recipients', async () => {
      const results = await smsService.send([
        { mobile: '0987654321', text: 'Message 1' },
        { mobile: '0912345678', text: 'Message 2' },
        { mobile: '0923456789', text: 'Message 3' },
      ]);

      expect(results).toHaveLength(3);
      expect(results.every(r => r.status === SMSRequestResult.SUCCESS)).toBe(true);
    });
  });

  describe('error handling', () => {
    it('should handle API errors gracefully', async () => {
      // Mock API error
      // ... test implementation
    });

    it('should return FAILED status on delivery failure', async () => {
      // Mock failed delivery
      // ... test implementation
    });
  });
});

Implementation Checklist

实现检查清单

When implementing a new SMS adapter, ensure:
当实现新的SMS Adapter时,请确保:

Required Features

必备功能

  • Interface Implementation: Implements
    SMSService<Request, SendResponse, MultiTarget>
  • Single SMS: Supports sending single SMS to one recipient
  • Batch SMS: Supports sending same message to multiple recipients
  • Multi-target: Supports sending different messages to multiple recipients
  • Phone Validation: Validates phone numbers before sending
  • Number Normalization: Uses
    normalizedTaiwanMobilePhoneNumber()
    for Taiwan numbers
  • Error Handling: Returns appropriate error codes and messages
  • Type Safety: All methods have proper TypeScript types
  • 接口实现:实现
    SMSService<Request, SendResponse, MultiTarget>
  • 单条短信:支持向单个接收者发送单条短信
  • 批量短信:支持向多个接收者发送同一条短信
  • 多目标发送:支持向多个接收者发送不同短信
  • 手机号验证:发送前验证手机号
  • 号码规范化:对中国台湾地区号码使用
    normalizedTaiwanMobilePhoneNumber()
  • 错误处理:返回合适的错误码和信息
  • 类型安全:所有方法具备正确的TypeScript类型

Recommended Features

推荐功能

  • Taiwan Number Support: Uses
    TAIWAN_PHONE_NUMBER_RE
    for validation
  • Strict Mode: Implements
    onlyTaiwanMobileNumber
    option
  • International Support: Handles international numbers when strict mode is disabled
  • Message Batching: Groups messages for efficient API calls
  • Rate Limiting: Implements rate limiting if required by provider
  • Retry Logic: Implements retry for transient failures
  • Logging: Logs API calls and errors for debugging
  • Documentation: Includes comprehensive JSDoc comments
  • 中国台湾地区号码支持:使用
    TAIWAN_PHONE_NUMBER_RE
    进行验证
  • 严格模式:实现
    onlyTaiwanMobileNumber
    选项
  • 国际号码支持:禁用严格模式时处理国际号码
  • 消息批量处理:对消息进行分组以优化API调用
  • 速率限制:根据服务商要求实现速率限制
  • 重试逻辑:针对临时故障实现重试机制
  • 日志记录:记录API调用和错误以便调试
  • 文档:包含全面的JSDoc注释

Quality Assurance

质量保证

  • Unit Tests: Comprehensive test coverage (>80%)
  • Integration Tests: Tests against provider API (staging environment)
  • Error Cases: Tests all error scenarios
  • Edge Cases: Tests edge cases (empty lists, invalid numbers, etc.)
  • Performance: Tests batch performance with large recipient lists
  • README: Complete README with examples and API reference
  • 单元测试:全面的测试覆盖率(>80%)
  • 集成测试:针对服务商API( staging环境)进行测试
  • 错误场景:测试所有错误场景
  • 边缘情况:测试边缘情况(空列表、无效号码等)
  • 性能:测试大量接收者列表的批量处理性能
  • README:包含示例和API参考的完整README

Best Practices

最佳实践

1. Phone Number Handling

1. 手机号处理

typescript
// ✅ GOOD: Use provided utilities
import { normalizedTaiwanMobilePhoneNumber, TAIWAN_PHONE_NUMBER_RE } from '@rytass/sms';

private validateMobile(mobile: string): string {
  if (TAIWAN_PHONE_NUMBER_RE.test(mobile)) {
    return normalizedTaiwanMobilePhoneNumber(mobile);
  }

  if (this.onlyTaiwanMobileNumber) {
    throw new Error(`Invalid Taiwan mobile number: ${mobile}`);
  }

  return mobile;
}

// ❌ BAD: Custom regex without normalization
private validateMobile(mobile: string): string {
  if (!/^09\d{8}$/.test(mobile)) {
    throw new Error('Invalid number');
  }
  return mobile;
}
typescript
// ✅ 推荐:使用提供的工具类
import { normalizedTaiwanMobilePhoneNumber, TAIWAN_PHONE_NUMBER_RE } from '@rytass/sms';

private validateMobile(mobile: string): string {
  if (TAIWAN_PHONE_NUMBER_RE.test(mobile)) {
    return normalizedTaiwanMobilePhoneNumber(mobile);
  }

  if (this.onlyTaiwanMobileNumber) {
    throw new Error(`Invalid Taiwan mobile number: ${mobile}`);
  }

  return mobile;
}

// ❌ 不推荐:自定义正则且不做规范化
private validateMobile(mobile: string): string {
  if (!/^09\d{8}$/.test(mobile)) {
    throw new Error('Invalid number');
  }
  return mobile;
}

2. Error Handling

2. 错误处理

typescript
// ✅ GOOD: Detailed error information
catch (error) {
  return {
    status: SMSRequestResult.FAILED,
    mobile,
    errorMessage: error.response?.data?.message || error.message,
    errorCode: this.mapProviderErrorCode(error.response?.data?.code),
  };
}

// ❌ BAD: Generic error without details
catch (error) {
  return {
    status: SMSRequestResult.FAILED,
    mobile,
  };
}
typescript
// ✅ 推荐:返回详细错误信息
catch (error) {
  return {
    status: SMSRequestResult.FAILED,
    mobile,
    errorMessage: error.response?.data?.message || error.message,
    errorCode: this.mapProviderErrorCode(error.response?.data?.code),
  };
}

// ❌ 不推荐:仅返回通用错误
catch (error) {
  return {
    status: SMSRequestResult.FAILED,
    mobile,
  };
}

3. Batch Optimization

3. 批量优化

typescript
// ✅ GOOD: Group by message for efficiency
private groupByMessage(requests: Array<{ mobile: string; text: string }>) {
  const batches = new Map<string, string[]>();

  for (const request of requests) {
    const existing = batches.get(request.text) || [];
    batches.set(request.text, [...existing, request.mobile]);
  }

  return batches;
}

// ❌ BAD: Send each message individually
for (const request of requests) {
  await this.callAPI(request.mobile, request.text);
}
typescript
// ✅ 推荐:按消息内容分组以提升效率
private groupByMessage(requests: Array<{ mobile: string; text: string }>) {
  const batches = new Map<string, string[]>();

  for (const request of requests) {
    const existing = batches.get(request.text) || [];
    batches.set(request.text, [...existing, request.mobile]);
  }

  return batches;
}

// ❌ 不推荐:逐个发送每条消息
for (const request of requests) {
  await this.callAPI(request.mobile, request.text);
}

4. Type Safety

4. 类型安全

typescript
// ✅ GOOD: Strict typing with generics
export class SMSServiceMyProvider implements SMSService<
  MyProviderSMSRequest,
  MyProviderSMSSendResponse,
  MyProviderSMSMultiTargetRequest
> {
  // ...
}

// ❌ BAD: Using any or loose typing
export class SMSServiceMyProvider {
  async send(request: any): Promise<any> {
    // ...
  }
}
typescript
// ✅ 推荐:使用泛型实现严格类型
ewport class SMSServiceMyProvider implements SMSService<
  MyProviderSMSRequest,
  MyProviderSMSSendResponse,
  MyProviderSMSMultiTargetRequest
> {
  // ...
}

// ❌ 不推荐:使用any或松散类型
export class SMSServiceMyProvider {
  async send(request: any): Promise<any> {
    // ...
  }
}

5. Configuration

5. 配置管理

typescript
// ✅ GOOD: Environment-based configuration
export class SMSServiceMyProvider {
  constructor(options: MyProviderSMSRequestInit) {
    this.baseUrl = options.baseUrl || this.getDefaultBaseUrl();
  }

  private getDefaultBaseUrl(): string {
    return process.env.NODE_ENV === 'production'
      ? 'https://api.myprovider.com'
      : 'https://api-staging.myprovider.com';
  }
}

// ❌ BAD: Hardcoded production URL
export class SMSServiceMyProvider {
  private baseUrl = 'https://api.myprovider.com';
}
typescript
// ✅ 推荐:基于环境的配置
export class SMSServiceMyProvider {
  constructor(options: MyProviderSMSRequestInit) {
    this.baseUrl = options.baseUrl || this.getDefaultBaseUrl();
  }

  private getDefaultBaseUrl(): string {
    return process.env.NODE_ENV === 'production'
      ? 'https://api.myprovider.com'
      : 'https://api-staging.myprovider.com';
  }
}

// ❌ 不推荐:硬编码生产环境URL
export class SMSServiceMyProvider {
  private baseUrl = 'https://api.myprovider.com';
}

API Reference

API参考

Base Package Exports

基础包导出

typescript
// Types
export {
  SMSService,          // Service interface
  SMSRequest,          // Single message interface
  SMSSendResponse,     // Response interface
  MultiTargetRequest,  // Batch message interface
  SMSRequestResult,    // Status enum
};

// Utilities
export {
  TAIWAN_PHONE_NUMBER_RE,                   // Taiwan number regex
  normalizedTaiwanMobilePhoneNumber,        // Normalization function
};
typescript
// 类型
export {
  SMSService,          // Service interface
  SMSRequest,          // Single message interface
  SMSSendResponse,     // Response interface
  MultiTargetRequest,  // Batch message interface
  SMSRequestResult,    // Status enum
};

// 工具类
export {
  TAIWAN_PHONE_NUMBER_RE,                   // Taiwan number regex
  normalizedTaiwanMobilePhoneNumber,        // Normalization function
};

Taiwan Number Validation

中国台湾地区号码验证

Supported Formats:
  • 09XXXXXXXX
    - Standard Taiwan format
  • 0912-345-678
    - With dashes
  • 0912 345 678
    - With spaces
  • +8869XXXXXXXX
    - International with +
  • 8869XXXXXXXX
    - International without +
Normalization Output:
  • Always returns
    09XXXXXXXX
    format
  • Removes dashes, spaces, and country code
  • Converts
    +886
    or
    886
    prefix to
    0
支持格式:
  • 09XXXXXXXX
    - 中国台湾地区标准格式
  • 0912-345-678
    - 带连字符
  • 0912 345 678
    - 带空格
  • +8869XXXXXXXX
    - 国际格式(带+)
  • 8869XXXXXXXX
    - 国际格式(不带+)
规范化输出:
  • 始终返回
    09XXXXXXXX
    格式
  • 移除连字符、空格和国家码
  • +886
    886
    前缀转换为
    0

Examples

示例

Minimal Adapter Implementation

最简Adapter实现

typescript
import {
  SMSService,
  SMSRequest,
  SMSSendResponse,
  MultiTargetRequest,
  SMSRequestResult,
} from '@rytass/sms';

interface SimpleSMSRequest extends SMSRequest {
  mobile: string;
  text: string;
}

interface SimpleSMSResponse extends SMSSendResponse {
  messageId?: string;
  status: SMSRequestResult;
  mobile: string;
}

interface SimpleMultiTargetRequest extends MultiTargetRequest {
  mobileList: string[];
  text: string;
}

export class SimpleSMSService implements SMSService<
  SimpleSMSRequest,
  SimpleSMSResponse,
  SimpleMultiTargetRequest
> {
  async send(requests: SimpleSMSRequest[]): Promise<SimpleSMSResponse[]>;
  async send(request: SimpleSMSRequest): Promise<SimpleSMSResponse>;
  async send(request: SimpleMultiTargetRequest): Promise<SimpleSMSResponse[]>;

  async send(request: any): Promise<any> {
    // Minimal implementation
    if (Array.isArray(request)) {
      return Promise.all(request.map(r => this.sendSingle(r)));
    }

    if (request.mobileList) {
      return Promise.all(
        request.mobileList.map(mobile =>
          this.sendSingle({ mobile, text: request.text })
        )
      );
    }

    return this.sendSingle(request);
  }

  private async sendSingle(request: SimpleSMSRequest): Promise<SimpleSMSResponse> {
    // Call provider API
    // Return response
    return {
      messageId: 'MSG-' + Date.now(),
      status: SMSRequestResult.SUCCESS,
      mobile: request.mobile,
    };
  }
}
typescript
import {
  SMSService,
  SMSRequest,
  SMSSendResponse,
  MultiTargetRequest,
  SMSRequestResult,
} from '@rytass/sms';

interface SimpleSMSRequest extends SMSRequest {
  mobile: string;
  text: string;
}

interface SimpleSMSResponse extends SMSSendResponse {
  messageId?: string;
  status: SMSRequestResult;
  mobile: string;
}

interface SimpleMultiTargetRequest extends MultiTargetRequest {
  mobileList: string[];
  text: string;
}

export class SimpleSMSService implements SMSService<
  SimpleSMSRequest,
  SimpleSMSResponse,
  SimpleMultiTargetRequest
> {
  async send(requests: SimpleSMSRequest[]): Promise<SimpleSMSResponse[]>;
  async send(request: SimpleSMSRequest): Promise<SimpleSMSResponse>;
  async send(request: SimpleMultiTargetRequest): Promise<SimpleSMSResponse[]>;

  async send(request: any): Promise<any> {
    // Minimal implementation
    if (Array.isArray(request)) {
      return Promise.all(request.map(r => this.sendSingle(r)));
    }

    if (request.mobileList) {
      return Promise.all(
        request.mobileList.map(mobile =>
          this.sendSingle({ mobile, text: request.text })
        )
      );
    }

    return this.sendSingle(request);
  }

  private async sendSingle(request: SimpleSMSRequest): Promise<SimpleSMSResponse> {
    // Call provider API
    // Return response
    return {
      messageId: 'MSG-' + Date.now(),
      status: SMSRequestResult.SUCCESS,
      mobile: request.mobile,
    };
  }
}

Common Provider Patterns

常见服务商模式

HTTP-based API

基于HTTP的API

Most Taiwan SMS providers use HTTP POST:
typescript
private async callProviderAPI(mobile: string, text: string): Promise<any> {
  const { data } = await axios.post(
    `${this.baseUrl}/api/sms/send`,
    new URLSearchParams({
      username: this.username,
      password: this.password,
      mobile: mobile,
      message: text,
    }),
    {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    }
  );

  return data;
}
大多数中国台湾地区短信服务商使用HTTP POST:
typescript
private async callProviderAPI(mobile: string, text: string): Promise<any> {
  const { data } = await axios.post(
    `${this.baseUrl}/api/sms/send`,
    new URLSearchParams({
      username: this.username,
      password: this.password,
      mobile: mobile,
      message: text,
    }),
    {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    }
  );

  return data;
}

CSV Response Format

CSV响应格式

Some providers return CSV responses:
typescript
private parseCSVResponse(data: string): {
  messageId: string;
  status: SMSRequestResult;
} {
  const [credit, sent, cost, unsent, messageId] = data.split(',');

  return {
    messageId,
    status: messageId ? SMSRequestResult.SUCCESS : SMSRequestResult.FAILED,
  };
}
部分服务商返回CSV格式响应:
typescript
private parseCSVResponse(data: string): {
  messageId: string;
  status: SMSRequestResult;
} {
  const [credit, sent, cost, unsent, messageId] = data.split(',');

  return {
    messageId,
    status: messageId ? SMSRequestResult.SUCCESS : SMSRequestResult.FAILED,
  };
}

Signature-based Authentication

基于签名的认证

Some providers require request signatures:
typescript
import crypto from 'crypto';

private generateSignature(params: Record<string, string>): string {
  const sorted = Object.keys(params)
    .sort()
    .map(key => `${key}=${params[key]}`)
    .join('&');

  return crypto
    .createHash('md5')
    .update(sorted + this.secretKey)
    .digest('hex');
}
部分服务商要求请求签名:
typescript
import crypto from 'crypto';

private generateSignature(params: Record<string, string>): string {
  const sorted = Object.keys(params)
    .sort()
    .map(key => `${key}=${params[key]}`)
    .join('&');

  return crypto
    .createHash('md5')
    .update(sorted + this.secretKey)
    .digest('hex');
}

Troubleshooting

故障排除

Common Issues

常见问题

TypeScript Errors:
typescript
// Error: Type 'X' is not assignable to type 'SMSService<...>'
// Solution: Ensure all three send() overloads are implemented

async send(requests: MyProviderSMSRequest[]): Promise<MyProviderSMSSendResponse[]>;
async send(request: MyProviderSMSRequest): Promise<MyProviderSMSSendResponse>;
async send(request: MyProviderSMSMultiTargetRequest): Promise<MyProviderSMSSendResponse[]>;
Phone Number Validation:
typescript
// Issue: Numbers not being normalized correctly
// Solution: Use TAIWAN_PHONE_NUMBER_RE before normalizing

if (TAIWAN_PHONE_NUMBER_RE.test(mobile)) {
  mobile = normalizedTaiwanMobilePhoneNumber(mobile);
}
Batch Optimization:
typescript
// Issue: Too many API calls
// Solution: Group requests by message text

const batches = requests.reduce((map, req) => {
  const list = map.get(req.text) || [];
  map.set(req.text, [...list, req.mobile]);
  return map;
}, new Map<string, string[]>());
TypeScript错误:
typescript
// 错误:Type 'X' is not assignable to type 'SMSService<...>'
// 解决方案:确保实现了全部三个send()重载

async send(requests: MyProviderSMSRequest[]): Promise<MyProviderSMSSendResponse[]>;
async send(request: MyProviderSMSRequest): Promise<MyProviderSMSSendResponse>;
async send(request: MyProviderSMSMultiTargetRequest): Promise<MyProviderSMSSendResponse[]>;
手机号验证问题:
typescript
// 问题:号码未被正确规范化
// 解决方案:规范化前使用TAIWAN_PHONE_NUMBER_RE验证

if (TAIWAN_PHONE_NUMBER_RE.test(mobile)) {
  mobile = normalizedTaiwanMobilePhoneNumber(mobile);
}
批量优化问题:
typescript
// 问题:API调用次数过多
// 解决方案:按消息内容分组请求

const batches = requests.reduce((map, req) => {
  const list = map.get(req.text) || [];
  map.set(req.text, [...list, req.mobile]);
  return map;
}, new Map<string, string[]>());

Publishing Checklist

发布检查清单

Before publishing your adapter:
  • Package name follows convention:
    @rytass/sms-adapter-{provider}
  • Peer dependency on
    @rytass/sms
    is declared
  • All exports are properly typed
  • README includes installation, usage, and examples
  • Tests pass with >80% coverage
  • TypeScript builds without errors
  • Package builds with
    npm run build
  • Version follows semantic versioning
  • CHANGELOG.md is updated
  • License is included (MIT recommended)
发布Adapter前:
  • 包名遵循约定:
    @rytass/sms-adapter-{provider}
  • 声明对
    @rytass/sms
    的peer依赖
  • 所有导出具备正确类型
  • README包含安装、使用方法和示例
  • 测试通过率>80%
  • TypeScript编译无错误
  • 可通过
    npm run build
    构建包
  • 版本遵循语义化版本规范
  • 更新CHANGELOG.md
  • 包含许可证(推荐MIT)

Resources

资源

For reference implementations:
  • Every8D Adapter - Complete reference implementation (互動資通 / Interactive Communications)
  • Base Package - Core interfaces and utilities
For usage guidance:
参考实现:
  • Every8D Adapter - 完整参考实现(互动资通 / Interactive Communications)
  • Base Package - 核心接口和工具类
使用指南: