Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
/**
* Message sending API for the QQ Open Platform.
*
* Key design improvements:
* - Unified `sendMessage(scope, ...)` replaces `sendC2CMessage` + `sendGroupMessage`.
* - `onMessageSent` hook is scoped to the instance, not a module-level global.
* - Markdown support flag is per-instance, not a global Map.
*/
import type {
ChatScope,
MessageResponse,
OutboundMeta,
EngineLogger,
InlineKeyboard,
} from "../types.js";
import { formatErrorMessage } from "../utils/format.js";
import { ApiClient } from "./api-client.js";
import {
messagePath,
channelMessagePath,
dmMessagePath,
gatewayPath,
interactionPath,
getNextMsgSeq,
} from "./routes.js";
import { TokenManager } from "./token.js";
export interface MessageApiConfig {
/** Whether the QQ Bot has markdown permission. */
markdownSupport: boolean;
/** Logger for diagnostics. */
logger?: EngineLogger;
}
type OnMessageSentCallback = (refIdx: string, meta: OutboundMeta) => void;
/**
* Message sending module.
*
* Usage:
* ```ts
* const api = new MessageApi(client, tokenMgr, { markdownSupport: true });
* await api.sendMessage('c2c', openid, 'Hello!', { appId, clientSecret, msgId });
* ```
*/
export class MessageApi {
private readonly client: ApiClient;
private readonly tokenManager: TokenManager;
private readonly markdownSupport: boolean;
private readonly logger?: EngineLogger;
private messageSentHook: OnMessageSentCallback | null = null;
constructor(client: ApiClient, tokenManager: TokenManager, config: MessageApiConfig) {
this.client = client;
this.tokenManager = tokenManager;
this.markdownSupport = config.markdownSupport;
this.logger = config.logger;
}
/** Register a callback invoked when a sent message returns a ref_idx. */
onMessageSent(callback: OnMessageSentCallback): void {
this.messageSentHook = callback;
}
/**
* Notify the registered hook about a sent message.
* Use this for media sends that bypass `sendAndNotify`.
*/
notifyMessageSent(refIdx: string, meta: OutboundMeta): void {
if (this.messageSentHook) {
try {
this.messageSentHook(refIdx, meta);
} catch (err) {
this.logger?.error?.(
`[qqbot:messages] onMessageSent hook error: ${formatErrorMessage(err)}`,
);
}
}
}
// ---- Unified message sending ----
/**
* Send a text message to a C2C or Group target.
*
* Automatically constructs the correct path, body format (markdown vs plain),
* and message sequence number.
*/
async sendMessage(
scope: ChatScope,
targetId: string,
content: string,
creds: Credentials,
opts?: {
msgId?: string;
messageReference?: string;
inlineKeyboard?: InlineKeyboard;
},
): Promise<MessageResponse> {
const token = await this.tokenManager.getAccessToken(creds.appId, creds.clientSecret);
const msgSeq = opts?.msgId ? getNextMsgSeq(opts.msgId) : 1;
const body = this.buildMessageBody(
content,
opts?.msgId,
msgSeq,
opts?.messageReference,
opts?.inlineKeyboard,
);
const path = messagePath(scope, targetId);
return this.sendAndNotify(creds.appId, token, "POST", path, body, { text: content });
}
/** Send a proactive (no msgId) message to a C2C or Group target. */
async sendProactiveMessage(
scope: ChatScope,
targetId: string,
content: string,
creds: Credentials,
): Promise<MessageResponse> {
if (!content?.trim()) {
throw new Error("Proactive message content must not be empty");
}
const token = await this.tokenManager.getAccessToken(creds.appId, creds.clientSecret);
const body = this.buildProactiveBody(content);
const path = messagePath(scope, targetId);
return this.sendAndNotify(creds.appId, token, "POST", path, body, { text: content });
}
// ---- Channel / DM ----
/** Send a channel message. */
async sendChannelMessage(opts: {
channelId: string;
content: string;
creds: Credentials;
msgId?: string;
}): Promise<MessageResponse> {
const token = await this.tokenManager.getAccessToken(opts.creds.appId, opts.creds.clientSecret);
return this.client.request<MessageResponse>(token, "POST", channelMessagePath(opts.channelId), {
content: opts.content,
...(opts.msgId ? { msg_id: opts.msgId } : {}),
});
}
/** Send a DM (guild direct message). */
async sendDmMessage(opts: {
guildId: string;
content: string;
creds: Credentials;
msgId?: string;
}): Promise<MessageResponse> {
const token = await this.tokenManager.getAccessToken(opts.creds.appId, opts.creds.clientSecret);
return this.client.request<MessageResponse>(token, "POST", dmMessagePath(opts.guildId), {
content: opts.content,
...(opts.msgId ? { msg_id: opts.msgId } : {}),
});
}
// ---- C2C Input Notify ----
/** Send a typing indicator to a C2C user. */
async sendInputNotify(opts: {
openid: string;
creds: Credentials;
msgId?: string;
inputSecond?: number;
}): Promise<{ refIdx?: string }> {
const inputSecond = opts.inputSecond ?? 60;
const token = await this.tokenManager.getAccessToken(opts.creds.appId, opts.creds.clientSecret);
const msgSeq = opts.msgId ? getNextMsgSeq(opts.msgId) : 1;
const response = await this.client.request<{ ext_info?: { ref_idx?: string } }>(
token,
"POST",
messagePath("c2c", opts.openid),
{
msg_type: 6,
input_notify: { input_type: 1, input_second: inputSecond },
msg_seq: msgSeq,
...(opts.msgId ? { msg_id: opts.msgId } : {}),
},
);
return { refIdx: response.ext_info?.ref_idx };
}
// ---- Interaction ----
/** Acknowledge an INTERACTION_CREATE event. */
async acknowledgeInteraction(
interactionId: string,
creds: Credentials,
code: 0 | 1 | 2 | 3 | 4 | 5 = 0,
): Promise<void> {
const token = await this.tokenManager.getAccessToken(creds.appId, creds.clientSecret);
await this.client.request(token, "PUT", interactionPath(interactionId), { code });
}
// ---- Gateway ----
/** Get the WebSocket gateway URL. */
async getGatewayUrl(creds: Credentials): Promise<string> {
const token = await this.tokenManager.getAccessToken(creds.appId, creds.clientSecret);
const data = await this.client.request<{ url: string }>(token, "GET", gatewayPath());
return data.url;
}
// ---- Internal ----
private async sendAndNotify(
appId: string,
accessToken: string,
method: string,
path: string,
body: unknown,
meta: OutboundMeta,
): Promise<MessageResponse> {
const result = await this.client.request<MessageResponse>(accessToken, method, path, body);
if (result.ext_info?.ref_idx && this.messageSentHook) {
try {
this.messageSentHook(result.ext_info.ref_idx, meta);
} catch (err) {
this.logger?.error?.(
`[qqbot:messages] onMessageSent hook error: ${formatErrorMessage(err)}`,
);
}
}
return result;
}
private buildMessageBody(
content: string,
msgId: string | undefined,
msgSeq: number,
messageReference?: string,
inlineKeyboard?: InlineKeyboard,
): Record<string, unknown> {
const body: Record<string, unknown> = this.markdownSupport
? { markdown: { content }, msg_type: 2, msg_seq: msgSeq }
: { content, msg_type: 0, msg_seq: msgSeq };
if (msgId) {
body.msg_id = msgId;
}
if (messageReference && !this.markdownSupport) {
body.message_reference = { message_id: messageReference };
}
if (inlineKeyboard) {
body.keyboard = inlineKeyboard;
}
return body;
}
private buildProactiveBody(content: string): Record<string, unknown> {
return this.markdownSupport ? { markdown: { content }, msg_type: 2 } : { content, msg_type: 0 };
}
}
// ---- Shared helpers ----
/** Credentials needed to authenticate API requests. */
export interface Credentials {
appId: string;
clientSecret: string;
}
// Re-export getNextMsgSeq for consumers that import from messages.ts.
export { getNextMsgSeq } from "./routes.js";
¤ Dauer der Verarbeitung: 0.0 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|