Files
qqbot/src/utils/payload.ts
2026-02-03 14:41:35 +08:00

266 lines
6.7 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.
/**
* QQBot 结构化消息载荷工具
*
* 用于处理 AI 输出的结构化消息载荷,包括:
* - 定时提醒载荷 (cron_reminder)
* - 媒体消息载荷 (media)
*/
// ============================================
// 类型定义
// ============================================
/**
* 定时提醒载荷
*/
export interface CronReminderPayload {
type: 'cron_reminder';
/** 提醒内容 */
content: string;
/** 目标类型c2c (私聊) 或 group (群聊) */
targetType: 'c2c' | 'group';
/** 目标地址user_openid 或 group_openid */
targetAddress: string;
/** 原始消息 ID可选 */
originalMessageId?: string;
}
/**
* 媒体消息载荷
*/
export interface MediaPayload {
type: 'media';
/** 媒体类型image, audio, video */
mediaType: 'image' | 'audio' | 'video';
/** 来源类型url 或 file */
source: 'url' | 'file';
/** 媒体路径或 URL */
path: string;
/** 媒体描述(可选) */
caption?: string;
}
/**
* QQBot 载荷联合类型
*/
export type QQBotPayload = CronReminderPayload | MediaPayload;
/**
* 解析结果
*/
export interface ParseResult {
/** 是否为结构化载荷 */
isPayload: boolean;
/** 解析后的载荷对象(如果是结构化载荷) */
payload?: QQBotPayload;
/** 原始文本(如果不是结构化载荷) */
text?: string;
/** 解析错误信息(如果解析失败) */
error?: string;
}
// ============================================
// 常量定义
// ============================================
/** AI 输出的结构化载荷前缀 */
const PAYLOAD_PREFIX = 'QQBOT_PAYLOAD:';
/** Cron 消息存储的前缀 */
const CRON_PREFIX = 'QQBOT_CRON:';
// ============================================
// 解析函数
// ============================================
/**
* 解析 AI 输出的结构化载荷
*
* 检测消息是否以 QQBOT_PAYLOAD: 前缀开头,如果是则提取并解析 JSON
*
* @param text AI 输出的原始文本
* @returns 解析结果
*
* @example
* const result = parseQQBotPayload('QQBOT_PAYLOAD:\n{"type": "media", "mediaType": "image", ...}');
* if (result.isPayload && result.payload) {
* // 处理结构化载荷
* }
*/
export function parseQQBotPayload(text: string): ParseResult {
const trimmedText = text.trim();
// 检查是否以 QQBOT_PAYLOAD: 开头
if (!trimmedText.startsWith(PAYLOAD_PREFIX)) {
return {
isPayload: false,
text: text
};
}
// 提取 JSON 内容(去掉前缀)
const jsonContent = trimmedText.slice(PAYLOAD_PREFIX.length).trim();
if (!jsonContent) {
return {
isPayload: true,
error: '载荷内容为空'
};
}
try {
const payload = JSON.parse(jsonContent) as QQBotPayload;
// 验证必要字段
if (!payload.type) {
return {
isPayload: true,
error: '载荷缺少 type 字段'
};
}
// 根据 type 进行额外验证
if (payload.type === 'cron_reminder') {
if (!payload.content || !payload.targetType || !payload.targetAddress) {
return {
isPayload: true,
error: 'cron_reminder 载荷缺少必要字段 (content, targetType, targetAddress)'
};
}
} else if (payload.type === 'media') {
if (!payload.mediaType || !payload.source || !payload.path) {
return {
isPayload: true,
error: 'media 载荷缺少必要字段 (mediaType, source, path)'
};
}
}
return {
isPayload: true,
payload
};
} catch (e) {
return {
isPayload: true,
error: `JSON 解析失败: ${e instanceof Error ? e.message : String(e)}`
};
}
}
// ============================================
// Cron 编码/解码函数
// ============================================
/**
* 将定时提醒载荷编码为 Cron 消息格式
*
* 将 JSON 编码为 Base64并添加 QQBOT_CRON: 前缀
*
* @param payload 定时提醒载荷
* @returns 编码后的消息字符串,格式为 QQBOT_CRON:{base64}
*
* @example
* const message = encodePayloadForCron({
* type: 'cron_reminder',
* content: '喝水时间到!',
* targetType: 'c2c',
* targetAddress: 'user_openid_xxx'
* });
* // 返回: QQBOT_CRON:eyJ0eXBlIjoiY3Jvbl9yZW1pbmRlciIs...
*/
export function encodePayloadForCron(payload: CronReminderPayload): string {
const jsonString = JSON.stringify(payload);
const base64 = Buffer.from(jsonString, 'utf-8').toString('base64');
return `${CRON_PREFIX}${base64}`;
}
/**
* 解码 Cron 消息中的载荷
*
* 检测 QQBOT_CRON: 前缀,解码 Base64 并解析 JSON
*
* @param message Cron 触发时收到的消息
* @returns 解码结果,包含是否为 Cron 载荷、解析后的载荷对象或错误信息
*
* @example
* const result = decodeCronPayload('QQBOT_CRON:eyJ0eXBlIjoiY3Jvbl9yZW1pbmRlciIs...');
* if (result.isCronPayload && result.payload) {
* // 处理定时提醒
* }
*/
export function decodeCronPayload(message: string): {
isCronPayload: boolean;
payload?: CronReminderPayload;
error?: string;
} {
const trimmedMessage = message.trim();
// 检查是否以 QQBOT_CRON: 开头
if (!trimmedMessage.startsWith(CRON_PREFIX)) {
return {
isCronPayload: false
};
}
// 提取 Base64 内容
const base64Content = trimmedMessage.slice(CRON_PREFIX.length);
if (!base64Content) {
return {
isCronPayload: true,
error: 'Cron 载荷内容为空'
};
}
try {
// Base64 解码
const jsonString = Buffer.from(base64Content, 'base64').toString('utf-8');
const payload = JSON.parse(jsonString) as CronReminderPayload;
// 验证类型
if (payload.type !== 'cron_reminder') {
return {
isCronPayload: true,
error: `期望 type 为 cron_reminder实际为 ${payload.type}`
};
}
// 验证必要字段
if (!payload.content || !payload.targetType || !payload.targetAddress) {
return {
isCronPayload: true,
error: 'Cron 载荷缺少必要字段'
};
}
return {
isCronPayload: true,
payload
};
} catch (e) {
return {
isCronPayload: true,
error: `Cron 载荷解码失败: ${e instanceof Error ? e.message : String(e)}`
};
}
}
// ============================================
// 辅助函数
// ============================================
/**
* 判断载荷是否为定时提醒类型
*/
export function isCronReminderPayload(payload: QQBotPayload): payload is CronReminderPayload {
return payload.type === 'cron_reminder';
}
/**
* 判断载荷是否为媒体消息类型
*/
export function isMediaPayload(payload: QQBotPayload): payload is MediaPayload {
return payload.type === 'media';
}