import { Injectable } from '@nestjs/common'; import * as http from 'node:http'; import * as https from 'node:https'; export type QqbotPluginHttpClientRequest = { body?: Buffer | string; context?: string; failureMessage?: (statusCode: number) => string; headers?: Record; invalidJsonMessage?: string; method?: string; timeoutMessage?: string; timeoutMs?: number; url: string | URL; }; @Injectable() export class QqbotPluginHttpClientService { /** * 执行 QQBot 插件平台流程。 * @param input - input 输入;使用 `invalidJsonMessage`、`context` 字段生成结果。 * @returns 异步完成后的 QQBot 插件平台结果。 */ async requestJson(input: QqbotPluginHttpClientRequest): Promise { const body = await this.requestText(input); try { return JSON.parse(body) as T; } catch { throw new Error( input.invalidJsonMessage || `${input.context || '插件 HTTP 接口'}返回不是合法 JSON`, ); } } /** * 执行 QQBot 插件平台流程。 * @param input - input 输入;使用 `url`、`method`、`timeoutMs`、`context` 字段生成结果。 * @returns QQBot 插件平台渲染后的图片、画布或文本。 */ requestBuffer(input: QqbotPluginHttpClientRequest): Promise { const url = input.url instanceof URL ? input.url : new URL(input.url); const method = input.method || 'GET'; const timeoutMs = input.timeoutMs || 8000; const context = input.context || '插件 HTTP 接口'; return new Promise((resolve, reject) => { const client = url.protocol === 'http:' ? http : https; const request = client.request( url, { headers: { Accept: '*/*', 'User-Agent': 'kt-template-online-api/qqbot-plugin', ...(input.headers || {}), }, method, timeout: timeoutMs, }, (response) => { const chunks: Buffer[] = []; response.on('data', (chunk) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }); response.on('end', () => { const statusCode = response.statusCode || 500; if (statusCode >= 400) { reject( createPluginHttpError( input.failureMessage?.(statusCode) || `${context}请求失败:${statusCode}`, statusCode, ), ); return; } resolve(Buffer.concat(chunks)); }); }, ); request.on('timeout', () => { request.destroy( new Error(input.timeoutMessage || `${context}请求超时`), ); }); request.on('error', reject); if (input.body) request.write(input.body); request.end(); }); } /** * 执行 QQBot 插件平台流程。 * @param input - input 输入;使用 `url`、`method`、`timeoutMs`、`context` 字段生成结果。 * @returns QQBot 插件平台渲染后的图片、画布或文本。 */ requestText(input: QqbotPluginHttpClientRequest): Promise { const url = input.url instanceof URL ? input.url : new URL(input.url); const method = input.method || 'GET'; const timeoutMs = input.timeoutMs || 8000; const context = input.context || '插件 HTTP 接口'; return new Promise((resolve, reject) => { const client = url.protocol === 'http:' ? http : https; const request = client.request( url, { headers: { Accept: 'application/json', 'User-Agent': 'kt-template-online-api/qqbot-plugin', ...(input.headers || {}), }, method, timeout: timeoutMs, }, (response) => { let responseBody = ''; response.setEncoding('utf8'); response.on('data', (chunk) => { responseBody += chunk; }); response.on('end', () => { const statusCode = response.statusCode || 500; if (statusCode >= 400) { reject( createPluginHttpError( input.failureMessage?.(statusCode) || `${context}请求失败:${statusCode}`, statusCode, ), ); return; } resolve(responseBody); }); }, ); request.on('timeout', () => { request.destroy( new Error(input.timeoutMessage || `${context}请求超时`), ); }); request.on('error', reject); if (input.body) request.write(input.body); request.end(); }); } } /** * 创建 QQBot 插件平台对象或配置。 * @param message - message 输入;驱动 `Object.assign()` 的 插件平台步骤。 * @param statusCode - statusCode 输入;生成 插件平台对象。 */ function createPluginHttpError(message: string, statusCode: number) { return Object.assign(new Error(message), { response: { status: statusCode, }, statusCode, }); }