feat(qqbot): 添加图片发送功能及优化定时任务载荷格式
新增功能: - 新增 qqbot-media 技能,支持 <qqimg> 标签发送本地图片 - 添加图片尺寸检测工具 (image-size.ts),自动识别常见图片格式 - 支持将本地图片上传至 QQ 富媒体服务器 优化改进: - 定时任务支持结构化 JSON 载荷格式 - 优化 <qqimg> 标签正则表达式,避免误匹配反引号内的说明文字 - 完善消息处理流程和错误处理 文件变更: - src/gateway.ts: 添加图片处理、上传逻辑 - src/outbound.ts: 增强外发消息能力 - src/utils/image-size.ts: 新增图片尺寸解析工具 - skills/qqbot-media/SKILL.md: 新增图片功能说明文档 - skills/qqbot-cron/SKILL.md: 补充结构化载荷说明
This commit is contained in:
@@ -695,28 +695,61 @@ openclaw cron add \\
|
|||||||
if (qqimgMatches.length > 0) {
|
if (qqimgMatches.length > 0) {
|
||||||
log?.info(`[qqbot:${account.accountId}] Detected ${qqimgMatches.length} <qqimg> tag(s)`);
|
log?.info(`[qqbot:${account.accountId}] Detected ${qqimgMatches.length} <qqimg> tag(s)`);
|
||||||
|
|
||||||
// 提取标签外的文本(作为描述发送)
|
// 构建发送队列:根据内容在原文中的实际位置顺序发送
|
||||||
let textWithoutTags = replyText;
|
// type: 'text' | 'image', content: 文本内容或图片路径
|
||||||
const imagePaths: string[] = [];
|
const sendQueue: Array<{ type: "text" | "image"; content: string }> = [];
|
||||||
|
|
||||||
for (const match of qqimgMatches) {
|
let lastIndex = 0;
|
||||||
const fullMatch = match[0];
|
// 使用新的正则来获取带索引的匹配结果
|
||||||
|
const qqimgRegexWithIndex = /<qqimg>([^<>]+)<\/qqimg>/gi;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = qqimgRegexWithIndex.exec(replyText)) !== null) {
|
||||||
|
// 添加标签前的文本
|
||||||
|
const textBefore = replyText.slice(lastIndex, match.index).replace(/\n{3,}/g, "\n\n").trim();
|
||||||
|
if (textBefore) {
|
||||||
|
sendQueue.push({ type: "text", content: filterInternalMarkers(textBefore) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加图片
|
||||||
const imagePath = match[1]?.trim();
|
const imagePath = match[1]?.trim();
|
||||||
|
|
||||||
if (imagePath) {
|
if (imagePath) {
|
||||||
imagePaths.push(imagePath);
|
sendQueue.push({ type: "image", content: imagePath });
|
||||||
log?.info(`[qqbot:${account.accountId}] Found image path in <qqimg>: ${imagePath}`);
|
log?.info(`[qqbot:${account.accountId}] Found image path in <qqimg>: ${imagePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从文本中移除标签
|
lastIndex = match.index + match[0].length;
|
||||||
textWithoutTags = textWithoutTags.replace(fullMatch, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理多余空行
|
// 添加最后一个标签后的文本
|
||||||
textWithoutTags = textWithoutTags.replace(/\n{3,}/g, "\n\n").trim();
|
const textAfter = replyText.slice(lastIndex).replace(/\n{3,}/g, "\n\n").trim();
|
||||||
|
if (textAfter) {
|
||||||
|
sendQueue.push({ type: "text", content: filterInternalMarkers(textAfter) });
|
||||||
|
}
|
||||||
|
|
||||||
|
log?.info(`[qqbot:${account.accountId}] Send queue: ${sendQueue.map(item => item.type).join(" -> ")}`);
|
||||||
|
|
||||||
|
// 按顺序发送
|
||||||
|
for (const item of sendQueue) {
|
||||||
|
if (item.type === "text") {
|
||||||
|
// 发送文本
|
||||||
|
try {
|
||||||
|
await sendWithTokenRetry(async (token) => {
|
||||||
|
if (event.type === "c2c") {
|
||||||
|
await sendC2CMessage(token, event.senderId, item.content, event.messageId);
|
||||||
|
} else if (event.type === "group" && event.groupOpenid) {
|
||||||
|
await sendGroupMessage(token, event.groupOpenid, item.content, event.messageId);
|
||||||
|
} else if (event.channelId) {
|
||||||
|
await sendChannelMessage(token, event.channelId, item.content, event.messageId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log?.info(`[qqbot:${account.accountId}] Sent text: ${item.content.slice(0, 50)}...`);
|
||||||
|
} catch (err) {
|
||||||
|
log?.error(`[qqbot:${account.accountId}] Failed to send text: ${err}`);
|
||||||
|
}
|
||||||
|
} else if (item.type === "image") {
|
||||||
// 发送图片
|
// 发送图片
|
||||||
for (const imagePath of imagePaths) {
|
const imagePath = item.content;
|
||||||
try {
|
try {
|
||||||
let imageUrl = imagePath;
|
let imageUrl = imagePath;
|
||||||
|
|
||||||
@@ -778,24 +811,6 @@ openclaw cron add \\
|
|||||||
await sendErrorMessage(`发送图片失败: ${err}`);
|
await sendErrorMessage(`发送图片失败: ${err}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送剩余的文本(如果有)
|
|
||||||
if (textWithoutTags) {
|
|
||||||
textWithoutTags = filterInternalMarkers(textWithoutTags);
|
|
||||||
try {
|
|
||||||
await sendWithTokenRetry(async (token) => {
|
|
||||||
if (event.type === "c2c") {
|
|
||||||
await sendC2CMessage(token, event.senderId, textWithoutTags, event.messageId);
|
|
||||||
} else if (event.type === "group" && event.groupOpenid) {
|
|
||||||
await sendGroupMessage(token, event.groupOpenid, textWithoutTags, event.messageId);
|
|
||||||
} else if (event.channelId) {
|
|
||||||
await sendChannelMessage(token, event.channelId, textWithoutTags, event.messageId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
log?.info(`[qqbot:${account.accountId}] Sent caption text: ${textWithoutTags.slice(0, 50)}...`);
|
|
||||||
} catch (err) {
|
|
||||||
log?.error(`[qqbot:${account.accountId}] Failed to send caption text: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录活动并返回
|
// 记录活动并返回
|
||||||
|
|||||||
Reference in New Issue
Block a user