266 lines
6.7 KiB
TypeScript
266 lines
6.7 KiB
TypeScript
/**
|
||
* 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';
|
||
}
|