fix: add missing payload.ts file
This commit is contained in:
265
src/utils/payload.ts
Normal file
265
src/utils/payload.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user