feat: 支持主动发送消息和附件处理

新增 `sendProactiveC2CMessage` 和 `sendProactiveGroupMessage` API,支持无需 msg_id 的主动消息发送(有月限额)。在网关中增加附件处理逻辑,支持解析图片和普通附件,并将附件信息传递给上下文。同时优化了 `sendText` 的目标地址解析逻辑,支持 `group:` 和 `channel:` 前缀,并新增 `sendProactiveMessage` 方法。
This commit is contained in:
sliverp
2026-01-29 16:17:06 +08:00
parent af31a001e9
commit 869519de7c
4 changed files with 170 additions and 10 deletions

View File

@@ -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) {