Files
qqbot/src/outbound.ts
sliverp 869519de7c feat: 支持主动发送消息和附件处理
新增 `sendProactiveC2CMessage` 和 `sendProactiveGroupMessage` API,支持无需 msg_id 的主动消息发送(有月限额)。在网关中增加附件处理逻辑,支持解析图片和普通附件,并将附件信息传递给上下文。同时优化了 `sendText` 的目标地址解析逻辑,支持 `group:` 和 `channel:` 前缀,并新增 `sendProactiveMessage` 方法。
2026-01-29 16:17:43 +08:00

115 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ResolvedQQBotAccount } from "./types.js";
import {
getAccessToken,
sendC2CMessage,
sendChannelMessage,
sendGroupMessage,
sendProactiveC2CMessage,
sendProactiveGroupMessage,
} from "./api.js";
export interface OutboundContext {
to: string;
text: string;
accountId?: string | null;
replyToId?: string | null;
account: ResolvedQQBotAccount;
}
export interface OutboundResult {
channel: string;
messageId?: string;
timestamp?: string | number;
error?: string;
}
/**
* 解析目标地址
* 格式:
* - openid (32位十六进制) -> C2C 单聊
* - group:xxx -> 群聊
* - channel:xxx -> 频道
* - 纯数字 -> 频道
*/
function parseTarget(to: string): { type: "c2c" | "group" | "channel"; id: string } {
if (to.startsWith("group:")) {
return { type: "group", id: to.slice(6) };
}
if (to.startsWith("channel:")) {
return { type: "channel", id: to.slice(8) };
}
// openid 通常是 32 位十六进制
if (/^[A-F0-9]{32}$/i.test(to)) {
return { type: "c2c", id: to };
}
// 默认当作频道 ID
return { type: "channel", id: to };
}
/**
* 发送文本消息(被动回复,需要 replyToId
*/
export async function sendText(ctx: OutboundContext): Promise<OutboundResult> {
const { to, text, replyToId, account } = ctx;
if (!account.appId || !account.clientSecret) {
return { channel: "qqbot", error: "QQBot not configured (missing appId or clientSecret)" };
}
try {
const accessToken = await getAccessToken(account.appId, account.clientSecret);
const target = parseTarget(to);
if (target.type === "c2c") {
const result = await sendC2CMessage(accessToken, target.id, text, replyToId ?? undefined);
return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
} else if (target.type === "group") {
const result = await sendGroupMessage(accessToken, target.id, text, replyToId ?? undefined);
return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
} else {
const result = await sendChannelMessage(accessToken, target.id, text, replyToId ?? undefined);
return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { channel: "qqbot", error: message };
}
}
/**
* 主动发送消息(不需要 replyToId有配额限制每月 4 条/用户/群)
*
* @param account - 账户配置
* @param to - 目标地址格式openid单聊或 group:xxx群聊
* @param text - 消息内容
*/
export async function sendProactiveMessage(
account: ResolvedQQBotAccount,
to: string,
text: string
): Promise<OutboundResult> {
if (!account.appId || !account.clientSecret) {
return { channel: "qqbot", error: "QQBot not configured (missing appId or clientSecret)" };
}
try {
const accessToken = await getAccessToken(account.appId, account.clientSecret);
const target = parseTarget(to);
if (target.type === "c2c") {
const result = await sendProactiveC2CMessage(accessToken, target.id, text);
return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
} else if (target.type === "group") {
const result = await sendProactiveGroupMessage(accessToken, target.id, text);
return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
} else {
// 频道暂不支持主动消息,使用普通发送
const result = await sendChannelMessage(accessToken, target.id, text);
return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp };
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { channel: "qqbot", error: message };
}
}