import { randomBytes, randomUUID } from 'node:crypto' import { MessageItemType, MessageState, MessageType, type BaseInfo, type GetConfigResp, type GetUpdatesReq, type GetUpdatesResp, type SendMessageReq, type SendTypingReq } from './types.js' export const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com' export const CHANNEL_VERSION = '1.0.0' export interface QrCodeResponse { qrcode: string qrcode_img_content: string } export interface QrStatusResponse { status: 'wait' | 'scaned' | 'confirmed' | 'expired' bot_token?: string ilink_bot_id?: string ilink_user_id?: string baseurl?: string } export class ApiError extends Error { readonly status: number readonly code?: number readonly payload?: unknown constructor(message: string, options: { status: number; code?: number; payload?: unknown }) { super(message) this.name = 'ApiError' this.status = options.status this.code = options.code this.payload = options.payload } } function normalizeBaseUrl(baseUrl: string): string { return baseUrl.replace(/\/+$/, '') } function buildBaseInfo(): BaseInfo { return { channel_version: CHANNEL_VERSION } } async function parseJsonResponse(response: Response, label: string): Promise { const text = await response.text() const payload = text ? JSON.parse(text) as T : ({} as T) if (!response.ok) { const body = payload as { errmsg?: string; errcode?: number } | null throw new ApiError(body?.errmsg ?? `${label} failed with HTTP ${response.status}`, { status: response.status, code: body?.errcode, payload }) } const body = payload as { ret?: number; errcode?: number; errmsg?: string } | null if (typeof body?.ret === 'number' && body.ret !== 0) { throw new ApiError(body.errmsg ?? `${label} failed`, { status: response.status, code: body.errcode ?? body.ret, payload }) } return payload } export function randomWechatUin(): string { const value = randomBytes(4).readUInt32BE(0) return Buffer.from(String(value), 'utf8').toString('base64') } export function buildHeaders(token: string): Record { return { 'Content-Type': 'application/json', AuthorizationType: 'ilink_bot_token', Authorization: `Bearer ${token}`, 'X-WECHAT-UIN': randomWechatUin() } } export async function apiFetch( baseUrl: string, endpoint: string, body: unknown, token: string, timeoutMs = 40_000, signal?: AbortSignal ): Promise { const url = new URL(endpoint, `${normalizeBaseUrl(baseUrl)}/`) const timeoutSignal = AbortSignal.timeout(timeoutMs) const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal const response = await fetch(url, { method: 'POST', headers: buildHeaders(token), body: JSON.stringify(body), signal: requestSignal }) return parseJsonResponse(response, endpoint) } export async function apiGet( baseUrl: string, path: string, headers: Record = {} ): Promise { const url = new URL(path, `${normalizeBaseUrl(baseUrl)}/`) const response = await fetch(url, { method: 'GET', headers }) return parseJsonResponse(response, path) } export async function getUpdates( baseUrl: string, token: string, cursor: string, signal?: AbortSignal ): Promise { const body: GetUpdatesReq = { get_updates_buf: cursor, base_info: buildBaseInfo() } return apiFetch(baseUrl, '/ilink/bot/getupdates', body, token, 40_000, signal) } export async function sendMessage( baseUrl: string, token: string, msg: SendMessageReq['msg'] ): Promise> { return apiFetch>( baseUrl, '/ilink/bot/sendmessage', { msg, base_info: buildBaseInfo() }, token, 15_000 ) } export async function getConfig( baseUrl: string, token: string, userId: string, contextToken: string ): Promise { return apiFetch( baseUrl, '/ilink/bot/getconfig', { ilink_user_id: userId, context_token: contextToken, base_info: buildBaseInfo() }, token, 15_000 ) } export async function sendTyping( baseUrl: string, token: string, userId: string, ticket: string, status: SendTypingReq['status'] ): Promise> { const body: SendTypingReq = { ilink_user_id: userId, typing_ticket: ticket, status, base_info: buildBaseInfo() } return apiFetch>(baseUrl, '/ilink/bot/sendtyping', body, token, 15_000) } export async function fetchQrCode(baseUrl: string = DEFAULT_BASE_URL): Promise { return apiGet(baseUrl, '/ilink/bot/get_bot_qrcode?bot_type=3') } export async function getQrCodeStatus( qrcode: string, baseUrl: string = DEFAULT_BASE_URL ): Promise { return apiGet( baseUrl, `/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`, { 'iLink-App-ClientVersion': '1' } ) } export function buildTextMessage( userId: string, contextToken: string, text: string ): SendMessageReq['msg'] { return { from_user_id: '', to_user_id: userId, client_id: randomUUID(), message_type: MessageType.BOT, message_state: MessageState.FINISH, context_token: contextToken, item_list: [ { type: MessageItemType.TEXT, text_item: { text } } ] } }