Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  api-client.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

/**
 * Core HTTP client for the QQ Open Platform REST API.
 *
 * Key improvements over the old `src/api.ts#apiRequest`:
 * - `ApiClient` is an **instance** — config (baseUrl, timeout, logger, UA)
 *   is injected via the constructor, eliminating module-level globals.
 * - Throws structured `ApiError` with httpStatus, bizCode, and path fields.
 * - Detects HTML error pages from CDN/gateway and returns user-friendly messages.
 * - `redactBodyKeys` replaces the hardcoded `file_data` redaction.
 */

import { ApiError, type ApiClientConfig, type EngineLogger } from "../types.js";
import { formatErrorMessage } from "../utils/format.js";

const DEFAULT_BASE_URL = "https://api.sgroup.qq.com";
const DEFAULT_TIMEOUT_MS = 30_000;
const FILE_UPLOAD_TIMEOUT_MS = 120_000;

export interface RequestOptions {
  /** Request timeout override in milliseconds. */
  timeoutMs?: number;
  /** Body keys to redact in debug logs (e.g. `['file_data']`). */
  redactBodyKeys?: string[];
}

/**
 * Stateful HTTP client for the QQ Open Platform.
 *
 * Usage:
 * ```ts
 * const client = new ApiClient({ logger, userAgent: 'QQBotPlugin/1.0' });
 * const data = await client.request<{ url: string }>(token, 'GET', '/gateway');
 * ```
 */
export class ApiClient {
  private readonly baseUrl: string;
  private readonly defaultTimeoutMs: number;
  private readonly fileUploadTimeoutMs: number;
  private readonly logger?: EngineLogger;
  private readonly resolveUserAgent: () => string;

  constructor(config: ApiClientConfig = {}) {
    this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
    this.defaultTimeoutMs = config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
    this.fileUploadTimeoutMs = config.fileUploadTimeoutMs ?? FILE_UPLOAD_TIMEOUT_MS;
    this.logger = config.logger;
    const ua = config.userAgent ?? "QQBotPlugin/unknown";
    this.resolveUserAgent = typeof ua === "function" ? ua : () => ua;
  }

  /**
   * Send an authenticated JSON request to the QQ Open Platform.
   *
   * @param accessToken - Bearer token (`QQBot {token}`).
   * @param method - HTTP method.
   * @param path - API path (appended to baseUrl).
   * @param body - Optional JSON body.
   * @param options - Optional request overrides.
   * @returns Parsed JSON response.
   * @throws {ApiError} On HTTP or parse errors.
   */
  async request<T = unknown>(
    accessToken: string,
    method: string,
    path: string,
    body?: unknown,
    options?: RequestOptions,
  ): Promise<T> {
    const url = `${this.baseUrl}${path}`;

    const headers: Record<string, string> = {
      Authorization: `QQBot ${accessToken}`,
      "Content-Type": "application/json",
      "User-Agent": this.resolveUserAgent(),
    };

    const isFileUpload = path.includes("/files");
    const timeout =
      options?.timeoutMs ?? (isFileUpload ? this.fileUploadTimeoutMs : this.defaultTimeoutMs);

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    const fetchInit: RequestInit = {
      method,
      headers,
      signal: controller.signal,
    };

    if (body) {
      fetchInit.body = JSON.stringify(body);
    }

    // Debug logging with optional body redaction.
    this.logger?.debug?.(`[qqbot:api] >>> ${method} ${url} (timeout: ${timeout}ms)`);
    if (body && this.logger?.debug) {
      const logBody = { ...(body as Record<string, unknown>) };
      for (const key of options?.redactBodyKeys ?? ["file_data"]) {
        if (typeof logBody[key] === "string") {
          logBody[key] = `<redacted ${logBody[key].length} chars>`;
        }
      }
      this.logger.debug(`[qqbot:api] >>> Body: ${JSON.stringify(logBody)}`);
    }

    let res: Response;
    try {
      res = await fetch(url, fetchInit);
    } catch (err) {
      clearTimeout(timeoutId);
      if (err instanceof Error && err.name === "AbortError") {
        this.logger?.error?.(`[qqbot:api] <<< Timeout after ${timeout}ms`);
        throw new ApiError(`Request timeout [${path}]: exceeded ${timeout}ms`, 0, path);
      }
      this.logger?.error?.(`[qqbot:api] <<< Network error: ${formatErrorMessage(err)}`);
      throw new ApiError(`Network error [${path}]: ${formatErrorMessage(err)}`, 0, path);
    } finally {
      clearTimeout(timeoutId);
    }

    // Log response status and trace ID.
    const traceId = res.headers.get("x-tps-trace-id") ?? "";
    this.logger?.info?.(
      `[qqbot:api] <<< Status: ${res.status} ${res.statusText}${traceId ? ` | TraceId: ${traceId}` : ""}`,
    );

    let rawBody: string;
    try {
      rawBody = await res.text();
    } catch (err) {
      throw new ApiError(
        `Failed to read response [${path}]: ${formatErrorMessage(err)}`,
        res.status,
        path,
      );
    }
    this.logger?.debug?.(`[qqbot:api] <<< Body: ${rawBody}`);

    // Detect non-JSON responses (HTML gateway errors, CDN rate-limit pages).
    const contentType = res.headers.get("content-type") ?? "";
    const isHtmlResponse = contentType.includes("text/html") || rawBody.trimStart().startsWith("<");

    if (!res.ok) {
      if (isHtmlResponse) {
        const statusHint =
          res.status === 502 || res.status === 503 || res.status === 504
            ? "调用发生异常,请稍候重试"
            : res.status === 429
              ? "请求过于频繁,已被限流"
              : `开放平台返回 HTTP ${res.status}`;
        throw new ApiError(`${statusHint}(${path}),请稍后重试`, res.status, path);
      }

      // JSON error response.
      try {
        const error = JSON.parse(rawBody) as {
          message?: string;
          code?: number;
          err_code?: number;
        };
        const bizCode = error.code ?? error.err_code;
        throw new ApiError(
          `API Error [${path}]: ${error.message ?? rawBody}`,
          res.status,
          path,
          bizCode,
          error.message,
        );
      } catch (parseErr) {
        if (parseErr instanceof ApiError) {
          throw parseErr;
        }
        throw new ApiError(
          `API Error [${path}] HTTP ${res.status}: ${rawBody.slice(0, 200)}`,
          res.status,
          path,
        );
      }
    }

    // Successful response but not JSON (extreme edge case).
    if (isHtmlResponse) {
      throw new ApiError(
        `QQ 服务端返回了非 JSON 响应(${path}),可能是临时故障,请稍后重试`,
        res.status,
        path,
      );
    }

    try {
      return JSON.parse(rawBody) as T;
    } catch {
      throw new ApiError(`开放平台响应格式异常(${path}),请稍后重试`, res.status, path);
    }
  }
}

¤ Dauer der Verarbeitung: 0.28 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge