diff --git a/src/AI/image.ts b/src/AI/image.ts index b5e5258..9b72daa 100644 --- a/src/AI/image.ts +++ b/src/AI/image.ts @@ -3,7 +3,7 @@ import { sendITTRequest } from "../service"; import { generateId } from "../utils/utils"; import { logger } from "../logger"; import { AI } from "./AI"; -import { MessageSegment } from "../utils/utils_string"; +import { MessageSegment, parseSpecialTokens } from "../utils/utils_string"; export class Image { static validKeys: (keyof Image)[] = ['id', 'file', 'content']; @@ -257,15 +257,21 @@ ${img.CQCode}`; } static async extractExistingImagesToSave(ctx: seal.MsgContext, ai: AI, s: string): Promise { - const images = []; - const match = s.match(/[<<][\|│|]img:.+?(?:[\|│|][>>]|[\|│|>>])/g); - if (match) { - for (let i = 0; i < match.length; i++) { - const id = match[i].match(/[<<][\|│|]img:(.+?)(?:[\|│|][>>]|[\|│|>>])/)[1]; - const image = await ai.context.findImage(ctx, id); - if (image) { - if (image.type === 'url') await image.urlToBase64(); - images.push(image); + const segs = parseSpecialTokens(s); + const images: Image[] = []; + for (const seg of segs) { + switch (seg.type) { + case 'img': { + const id = seg.content; + const image = await ai.context.findImage(ctx, id); + + if (image) { + if (image.type === 'url') await image.urlToBase64(); + images.push(image); + } else { + logger.warning(`无法找到图片:${id}`); + } + break; } } } diff --git a/src/tool/tool_message.ts b/src/tool/tool_message.ts index 1772b6d..2e42fae 100644 --- a/src/tool/tool_message.ts +++ b/src/tool/tool_message.ts @@ -2,10 +2,12 @@ import { AIManager } from "../AI/AI"; import { ConfigManager } from "../config/configManager"; import { replyToSender, transformMsgIdBack } from "../utils/utils"; import { getCtxAndMsg } from "../utils/utils_seal"; -import { handleReply, MessageSegment, transformArrayToContent } from "../utils/utils_string"; +import { handleReply, MessageSegment, parseSpecialTokens, transformArrayToContent } from "../utils/utils_string"; import { Tool, ToolManager } from "./tool"; -import { CQTYPESALLOW } from "../config/config"; -import { deleteMsg, getGroupMemberInfo, getMsg, netExists } from "../utils/utils_ob11"; +import { CQTYPESALLOW, faceMap } from "../config/config"; +import { deleteMsg, getGroupMemberInfo, getMsg, sendGroupForwardMsg, sendPrivateForwardMsg, netExists } from "../utils/utils_ob11"; +import { logger } from "../logger"; +import { Image } from "../AI/image"; export function registerMessage() { const toolSend = new Tool({ @@ -50,13 +52,17 @@ export function registerMessage() { `来自<${ctx.player.name}>${showNumber ? `(${ctx.player.userId.replace(/^.+:/, '')})` : ``}` : `来自群聊<${ctx.group.groupName}>${showNumber ? `(${ctx.group.groupId.replace(/^.+:/, '')})` : ``}`; - const originalImages = []; - const match = content.match(/[<<][\|│|]img:.+?(?:[\|│|][>>]|[\|│|>>])/g); - if (match) { - for (let i = 0; i < match.length; i++) { - const id = match[i].match(/[<<][\|│|]img:(.+?)(?:[\|│|][>>]|[\|│|>>])/)[1].trim().slice(0, 6); - const image = await ai.context.findImage(ctx, id); - if (image) originalImages.push(image); + const segs = parseSpecialTokens(content); + const originalImages: Image[] = []; + for (const seg of segs) { + switch (seg.type) { + case 'img': { + const id = seg.content; + const image = await ai.context.findImage(ctx, id); + if (image) originalImages.push(image); + else logger.warning(`无法找到图片:${id}`); + break; + } } } @@ -178,4 +184,172 @@ export function registerMessage() { await deleteMsg(epId, transformMsgIdBack(msg_id)); return { content: `已撤回消息${msg_id}`, images: [] }; } -} \ No newline at end of file + + const toolMerge = new Tool({ + type: 'function', + function: { + name: 'send_forward_msg', + description: '发送合并转发消息', + parameters: { + type: 'object', + properties: { + msg_type: { + type: 'string', + description: '消息类型,私聊或群聊', + enum: ['private', 'group'] + }, + name: { + type: 'string', + description: '用户名称或群聊名称' + (ConfigManager.message.showNumber ? '或纯数字QQ号、群号' : '') + ',实际使用时与消息类型对应' + }, + messages: { + type: 'array', + description: '消息节点列表,可以有多个', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: '用户名称' + (ConfigManager.message.showNumber ? '或纯数字QQ号' : '') + }, + nickname: { + type: 'string', + description: '发送者名称,默认与name相同' + }, + content: { + type: 'string', + description: '消息内容' + } + }, + required: ['content'] + } + } + }, + required: ['msg_type', 'name', 'messages'] + } + } + }); + toolMerge.solve = async (ctx, _, ai, args) => { + const { msg_type, name, messages } = args; + + if (!netExists()) return { content: `未找到ob11网络连接依赖,请提示用户安装`, images: [] }; + + const messagesToSend = []; + const images: Image[] = []; + for (const messageItem of messages) { + const segs = parseSpecialTokens(messageItem.content); + const content: MessageSegment[] = []; + for (const seg of segs) { + switch (seg.type) { + case 'text': { + content.push({ + type: 'text', + data: { + text: seg.content + } + }) + break; + } + case 'at': { + const name = seg.content; + const ui = await ai.context.findUserInfo(ctx, name); + if (ui !== null) { + content.push({ + type: 'at', + data: { + qq: ui.id.replace(/^.+:/, "") + } + }) + } else { + logger.warning(`无法找到用户:${name}`); + content.push({ + type: 'text', + data: { + text: ` @${name} ` + } + }) + } + break; + } + case 'quote': { + const msgId = seg.content; + content.push({ + type: 'reply', + data: { id: String(transformMsgIdBack(msgId)) } + }) + break; + } + case 'img': { + const id = seg.content; + const image = await ai.context.findImage(ctx, id); + + if (image) { + if (image.type === 'local') break; + images.push(image); + content.push({ + type: 'image', + data: { file: image.type === 'base64' ? seal.base64ToImage(image.base64) : image.file } + }) + } else { + logger.warning(`无法找到图片:${id}`); + } + break; + } + case 'face': { + const faceId = Object.keys(faceMap).find(key => faceMap[key] === seg.content) || ''; + content.push({ + type: 'face', + data: { id: faceId } + }) + break; + } + } + } + + if (content.length === 0) { + return { content: `消息长度不能为0`, images: [] }; + } + + let userId = ""; + let nickname = messageItem.nickname || ""; + const ui = await ai.context.findUserInfo(ctx, messageItem.name, true); + if (ui !== null) { + userId = ui.id.replace(/^.+:/, ""); + nickname = nickname || ui.name; + } + + messagesToSend.push({ + type: 'node', + data: { + user_id: userId, + nickname: nickname, + content: content + } + }); + } + + const news = null; + const prompt = ""; + const summary = ""; + const source = ""; + + if (msg_type === "private") { + const ui = await ai.context.findUserInfo(ctx, name, true); + if (ui === null) return { content: `未找到<${name}>`, images: [] }; + if (ui.id === ctx.endPoint.userId) return { content: `禁止向自己发送消息`, images: [] }; + + await sendPrivateForwardMsg(ctx.endPoint.userId, ui.id.replace(/^.+:/, ""), messagesToSend, news, prompt, summary, source); + } else if (msg_type === "group") { + const gi = await ai.context.findGroupInfo(ctx, name); + if (gi === null) return { content: `未找到<${name}>`, images: [] }; + + await sendGroupForwardMsg(ctx.endPoint.userId, gi.id.replace(/^.+:/, ""), messagesToSend, news, prompt, summary, source); + } else { + return { content: `未知的消息类型<${msg_type}>`, images: [] }; + } + + return { content: `发送合并消息成功`, images: images }; + } +} + +// TODO: 合并消息嵌套 \ No newline at end of file diff --git a/src/update.ts b/src/update.ts index 1a26928..289b4d5 100644 --- a/src/update.ts +++ b/src/update.ts @@ -3,7 +3,10 @@ export const updateInfo = { "4.12.1": `- 新增按时间搜索记忆 - 新增图片头像ID发送 - 将img命令改为ai子命令 -- 新增render嵌入图片`, +- 新增render嵌入图片 +- 新增tti保存 +- 修复tool关闭状态发生错误反转的问题 +- 新增合并转发tool`, "4.12.0": `- 新增通过名称选择角色设定功能 - 修复获取好友、群聊等列表时的bug - 修复了调用函数时,无需cmdArgs的函数也会报错的问题 diff --git a/src/utils/utils_ob11.ts b/src/utils/utils_ob11.ts index 715fa10..f2d0e5d 100644 --- a/src/utils/utils_ob11.ts +++ b/src/utils/utils_ob11.ts @@ -249,4 +249,52 @@ export async function sendGroupAISound(epId: string, characterId: string, group_ logger.error(`发送群 ${group_id} AI 声聊合成语音失败:${e}`); return; } -} \ No newline at end of file +} + +export async function sendPrivateForwardMsg(epId: string, user_id: string, + messages: MessageSegment[], + news: string[], + prompt: string, + summary: string, + source: string): Promise { + const net = getNet(); + if (!net) return null; + try { + const data = await net.callApi(epId, 'send_private_forward_msg', { + user_id, + messages, + news, + prompt, + summary, + source + }) + return data; + } catch (e) { + logger.error(`发送用户 ${user_id} 转发消息失败:${e}`); + return null; + } +} + +export async function sendGroupForwardMsg(epId: string, group_id: string, + messages: MessageSegment[], + news: string[], + prompt: string, + summary: string, + source: string): Promise { + const net = getNet(); + if (!net) return null; + try { + const data = await net.callApi(epId, 'send_group_forward_msg', { + group_id, + messages, + news, + prompt, + summary, + source + }) + return data; + } catch (e) { + logger.error(`发送群 ${group_id} 转发消息失败:${e}`); + return null; + } +}