feat: 支持主动发送消息和附件处理
新增 `sendProactiveC2CMessage` 和 `sendProactiveGroupMessage` API,支持无需 msg_id 的主动消息发送(有月限额)。在网关中增加附件处理逻辑,支持解析图片和普通附件,并将附件信息传递给上下文。同时优化了 `sendText` 的目标地址解析逻辑,支持 `group:` 和 `channel:` 前缀,并新增 `sendProactiveMessage` 方法。
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import type { ResolvedQQBotAccount } from "./types.js";
|
||||
import { getAccessToken, sendC2CMessage, sendChannelMessage } from "./api.js";
|
||||
import {
|
||||
getAccessToken,
|
||||
sendC2CMessage,
|
||||
sendChannelMessage,
|
||||
sendGroupMessage,
|
||||
sendProactiveC2CMessage,
|
||||
sendProactiveGroupMessage,
|
||||
} from "./api.js";
|
||||
|
||||
export interface OutboundContext {
|
||||
to: string;
|
||||
@@ -17,7 +24,30 @@ export interface OutboundResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送文本消息
|
||||
* 解析目标地址
|
||||
* 格式:
|
||||
* - 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;
|
||||
@@ -28,16 +58,53 @@ export async function sendText(ctx: OutboundContext): Promise<OutboundResult> {
|
||||
|
||||
try {
|
||||
const accessToken = await getAccessToken(account.appId, account.clientSecret);
|
||||
const target = parseTarget(to);
|
||||
|
||||
// 判断目标类型:openid (C2C) 或 channel_id (频道)
|
||||
// openid 通常是 32 位十六进制,channel_id 通常是数字
|
||||
const isC2C = /^[A-F0-9]{32}$/i.test(to);
|
||||
|
||||
if (isC2C) {
|
||||
const result = await sendC2CMessage(accessToken, to, text, replyToId ?? undefined);
|
||||
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, to, text, replyToId ?? undefined);
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user