diff --git a/src/milky/api/message.ts b/src/milky/api/message.ts index 19e24176..2c156d34 100644 --- a/src/milky/api/message.ts +++ b/src/milky/api/message.ts @@ -44,13 +44,27 @@ const SendPrivateMessage = defineApi( let result: RawMessage if (payload.message[0].type === 'forward') { + const forwardData = payload.message[0].data as { + messages: OutgoingForwardedMessage[] + title?: string + preview?: { text: string }[] + summary?: string + prompt?: string + } const raw = await transformOutgoingForwardMessages( ctx, - payload.message[0].data.messages as OutgoingForwardedMessage[], - peer + forwardData.messages, + peer, + { + title: forwardData.title, + preview: forwardData.preview, + summary: forwardData.summary, + prompt: forwardData.prompt + } ) const resid = await ctx.app.pmhq.uploadForward(peer, raw.multiMsgItems) const uuid = randomUUID() + const prompt = raw.prompt result = await ctx.app.sendMessage(ctx, peer, [{ elementType: 10, elementId: '', @@ -64,7 +78,7 @@ const SendPrivateMessage = defineApi( type: 'normal', width: 300, }, - desc: '[聊天记录]', + desc: prompt, extra: JSON.stringify({ filename: uuid, tsum: raw.tsum, @@ -78,7 +92,7 @@ const SendPrivateMessage = defineApi( uniseq: uuid, }, }, - prompt: '[聊天记录]', + prompt, ver: '0.0.0.5', view: 'contact', }), @@ -117,13 +131,27 @@ const SendGroupMessage = defineApi( let result: RawMessage if (payload.message[0].type === 'forward') { + const forwardData = payload.message[0].data as { + messages: OutgoingForwardedMessage[] + title?: string + preview?: { text: string }[] + summary?: string + prompt?: string + } const raw = await transformOutgoingForwardMessages( ctx, - payload.message[0].data.messages as OutgoingForwardedMessage[], - peer + forwardData.messages, + peer, + { + title: forwardData.title, + preview: forwardData.preview, + summary: forwardData.summary, + prompt: forwardData.prompt + } ) const resid = await ctx.app.pmhq.uploadForward(peer, raw.multiMsgItems) const uuid = randomUUID() + const prompt = raw.prompt result = await ctx.app.sendMessage(ctx, peer, [{ elementType: 10, elementId: '', @@ -137,7 +165,7 @@ const SendGroupMessage = defineApi( type: 'normal', width: 300, }, - desc: '[聊天记录]', + desc: prompt, extra: JSON.stringify({ filename: uuid, tsum: raw.tsum, @@ -151,7 +179,7 @@ const SendGroupMessage = defineApi( uniseq: uuid, }, }, - prompt: '[聊天记录]', + prompt, ver: '0.0.0.5', view: 'contact', }), diff --git a/src/milky/transform/message/outgoing.ts b/src/milky/transform/message/outgoing.ts index d7939ee8..b1780664 100644 --- a/src/milky/transform/message/outgoing.ts +++ b/src/milky/transform/message/outgoing.ts @@ -90,10 +90,16 @@ export async function transformOutgoingMessage( export async function transformOutgoingForwardMessages( ctx: Context, messages: OutgoingForwardedMessage[], - peer: Peer + peer: Peer, + options?: { + title?: string + preview?: { text: string }[] + summary?: string + prompt?: string + } ) { const encoder = new ForwardMessageEncoder(ctx, peer) - return await encoder.generate(messages) + return await encoder.generate(messages, options) } class ForwardMessageEncoder { @@ -304,7 +310,12 @@ class ForwardMessageEncoder { } } - async generate(content: OutgoingForwardedMessage[]) { + async generate(content: OutgoingForwardedMessage[], options?: { + title?: string + preview?: { text: string }[] + summary?: string + prompt?: string + }) { await this.render(content) return { multiMsgItems: [{ @@ -314,9 +325,10 @@ class ForwardMessageEncoder { } }], tsum: this.tsum, - source: this.isGroup ? '群聊的聊天记录' : '聊天记录', - summary: `查看${this.tsum}条转发消息`, - news: this.news + source: options?.title ?? (this.isGroup ? '群聊的聊天记录' : '聊天记录'), + summary: options?.summary ?? `查看${this.tsum}条转发消息`, + news: options?.preview ?? this.news, + prompt: options?.prompt ?? '[聊天记录]' } } } diff --git a/src/onebot11/action/go-cqhttp/SendForwardMsg.ts b/src/onebot11/action/go-cqhttp/SendForwardMsg.ts index 0b92714e..c8497fbf 100644 --- a/src/onebot11/action/go-cqhttp/SendForwardMsg.ts +++ b/src/onebot11/action/go-cqhttp/SendForwardMsg.ts @@ -17,6 +17,11 @@ interface Payload { messages?: OB11MessageNode[] message?: OB11MessageNode[] message_type?: 'group' | 'private' + // 合并转发自定义外显 + source?: string + news?: { text: string }[] + summary?: string + prompt?: string } interface Response { @@ -35,8 +40,8 @@ export class SendForwardMsg extends BaseAction { }) protected async _handle(payload: Payload) { - const messages = payload.messages ?? payload.message - if (!messages) { + const messages = (payload.messages?.length ? payload.messages : null) ?? payload.message + if (!messages || messages.length === 0) { throw new Error('未指定消息内容') } let contextMode = CreatePeerMode.Normal @@ -122,7 +127,12 @@ export class SendForwardMsg extends BaseAction { } } - return fake ? await this.handleFakeForwardNode(peer, nodes) : await this.handleForwardNode(peer, nodes, msgInfos) + return fake ? await this.handleFakeForwardNode(peer, nodes, { + source: payload.source, + news: payload.news, + summary: payload.summary, + prompt: payload.prompt + }) : await this.handleForwardNode(peer, nodes, msgInfos) } private async getMessageNode(msgInfo: MsgInfo) { @@ -162,11 +172,17 @@ export class SendForwardMsg extends BaseAction { }) } - private async handleFakeForwardNode(peer: Peer, nodes: OB11MessageNode[]): Promise { + private async handleFakeForwardNode(peer: Peer, nodes: OB11MessageNode[], options?: { + source?: string + news?: { text: string }[] + summary?: string + prompt?: string + }): Promise { const encoder = new MessageEncoder(this.ctx, peer) - const raw = await encoder.generate(nodes) + const raw = await encoder.generate(nodes, options) const resid = await this.ctx.app.pmhq.uploadForward(peer, raw.multiMsgItems) const uuid = randomUUID() + const prompt = options?.prompt ?? '[聊天记录]' try { const msg = await this.ctx.app.sendMessage(this.ctx, peer, [{ elementType: 10, @@ -181,7 +197,7 @@ export class SendForwardMsg extends BaseAction { type: 'normal', width: 300, }, - desc: '[聊天记录]', + desc: prompt, extra: JSON.stringify({ filename: uuid, tsum: raw.tsum, @@ -195,7 +211,7 @@ export class SendForwardMsg extends BaseAction { uniseq: uuid, }, }, - prompt: '[聊天记录]', + prompt, ver: '0.0.0.5', view: 'contact', }), diff --git a/src/onebot11/helper/createMultiMessage.ts b/src/onebot11/helper/createMultiMessage.ts index 022765f2..e9f9ebe9 100644 --- a/src/onebot11/helper/createMultiMessage.ts +++ b/src/onebot11/helper/createMultiMessage.ts @@ -1,15 +1,18 @@ import { Context } from 'cordis' -import { OB11MessageData, OB11MessageDataType } from '../types' +import { OB11MessageData, OB11MessageDataType, OB11MessageNode } from '../types' import { Msg, Media } from '@/ntqqapi/proto' -import { handleOb11RichMedia } from './createMessage' +import { handleOb11RichMedia, message2List } from './createMessage' import { selfInfo } from '@/common/globalVars' import { ElementType, Peer, RichMediaUploadCompleteNotify } from '@/ntqqapi/types' import { deflateSync } from 'node:zlib' import faceConfig from '@/ntqqapi/helper/face_config.json' import { InferProtoModelInput } from '@saltify/typeproto' +// 最大嵌套深度 +const MAX_FORWARD_DEPTH = 3 + export class MessageEncoder { - static support = ['text', 'face', 'image', 'markdown', 'forward'] + static support = ['text', 'face', 'image', 'markdown', 'forward', 'node'] results: InferProtoModelInput[] children: InferProtoModelInput[] deleteAfterSentFiles: string[] @@ -20,8 +23,9 @@ export class MessageEncoder { news: { text: string }[] name?: string uin?: number + depth: number = 0 - constructor(private ctx: Context, private peer: Peer) { + constructor(private ctx: Context, private peer: Peer, depth: number = 0) { this.results = [] this.children = [] this.deleteAfterSentFiles = [] @@ -30,6 +34,7 @@ export class MessageEncoder { this.tsum = 0 this.preview = '' this.news = [] + this.depth = depth } async flush() { @@ -136,8 +141,9 @@ export class MessageEncoder { } } - packForwardMessage(resid: string) { + packForwardMessage(resid: string, options?: { source?: string; news?: { text: string }[]; summary?: string; prompt?: string }) { const uuid = crypto.randomUUID() + const prompt = options?.prompt ?? '[聊天记录]' const content = JSON.stringify({ app: 'com.tencent.multimsg', config: { @@ -147,23 +153,23 @@ export class MessageEncoder { type: 'normal', width: 300 }, - desc: '[聊天记录]', + desc: prompt, extra: JSON.stringify({ filename: uuid, tsum: 0, }), meta: { detail: { - news: [{ + news: options?.news ?? [{ text: '查看转发消息' }], resid, - source: '聊天记录', - summary: '查看转发消息', + source: options?.source ?? '聊天记录', + summary: options?.summary ?? '查看转发消息', uniseq: uuid, } }, - prompt: '[聊天记录]', + prompt, ver: '0.0.0.5', view: 'contact' }) @@ -177,10 +183,49 @@ export class MessageEncoder { async visit(segment: OB11MessageData) { const { type, data } = segment if (type === OB11MessageDataType.Node) { - await this.render(data.content as OB11MessageData[]) - const id = data.uin ?? data.user_id + const nodeData = data as OB11MessageNode['data'] + const content = nodeData.content ? message2List(nodeData.content) : [] + + // 检查 content 中是否包含嵌套的 node 节点 + const hasNestedNodes = content.some(e => e.type === OB11MessageDataType.Node) + + if (hasNestedNodes) { + // 递归处理嵌套的合并转发 + if (this.depth >= MAX_FORWARD_DEPTH) { + this.ctx.logger.warn(`合并转发嵌套深度超过 ${MAX_FORWARD_DEPTH} 层,将停止解析`) + return + } + + // 提取嵌套节点的自定义外显参数 + const nestedOptions = { + source: (nodeData as any).source, + news: (nodeData as any).news, + summary: (nodeData as any).summary, + prompt: (nodeData as any).prompt, + } + + // 递归生成内层合并转发 + const innerEncoder = new MessageEncoder(this.ctx, this.peer, this.depth + 1) + const innerNodes = content.filter(e => e.type === OB11MessageDataType.Node) as OB11MessageNode[] + const innerRaw = await innerEncoder.generate(innerNodes, nestedOptions) + + // 上传内层合并转发,获取 resid + const resid = await this.ctx.app.pmhq.uploadForward(this.peer, innerRaw.multiMsgItems) + + // 合并内层的待删除文件 + this.deleteAfterSentFiles.push(...innerEncoder.deleteAfterSentFiles) + + // 将内层合并转发作为当前节点的内容 + this.children.push(this.packForwardMessage(resid, nestedOptions)) + this.preview += '[聊天记录]' + } else { + // 普通节点,直接渲染内容 + await this.render(content) + } + + const id = nodeData.uin ?? nodeData.user_id this.uin = id ? +id : undefined - this.name = data.name ?? data.nickname + this.name = nodeData.name ?? nodeData.nickname await this.flush() } else if (type === OB11MessageDataType.Text) { this.children.push({ @@ -211,7 +256,37 @@ export class MessageEncoder { this.preview += busiType === 1 ? '[动画表情]' : '[图片]' this.deleteAfterSentFiles.push(path) } else if (type === OB11MessageDataType.Forward) { - this.children.push(this.packForwardMessage(data.id)) + // 处理 forward 类型:支持 id(已有 resid)或 content(嵌套节点) + const forwardData = data as { id?: string; content?: OB11MessageData[]; source?: string; news?: { text: string }[]; summary?: string; prompt?: string } + + if (forwardData.id) { + this.children.push(this.packForwardMessage(forwardData.id, forwardData)) + } else if (forwardData.content) { + if (this.depth >= MAX_FORWARD_DEPTH) { + this.ctx.logger.warn(`合并转发嵌套深度超过 ${MAX_FORWARD_DEPTH} 层,将停止解析`) + return + } + + const nestedContent = message2List(forwardData.content) + const innerEncoder = new MessageEncoder(this.ctx, this.peer, this.depth + 1) + const innerNodes = nestedContent.filter(e => e.type === OB11MessageDataType.Node) as OB11MessageNode[] + + if (innerNodes.length === 0) { + this.ctx.logger.warn('forward content 中没有有效的 node 节点') + return + } + + const innerRaw = await innerEncoder.generate(innerNodes, { + source: forwardData.source, + news: forwardData.news, + summary: forwardData.summary, + prompt: forwardData.prompt, + }) + + const resid = await this.ctx.app.pmhq.uploadForward(this.peer, innerRaw.multiMsgItems) + this.deleteAfterSentFiles.push(...innerEncoder.deleteAfterSentFiles) + this.children.push(this.packForwardMessage(resid, forwardData)) + } this.preview += '[聊天记录]' } } @@ -222,7 +297,12 @@ export class MessageEncoder { } } - async generate(content: OB11MessageData[]) { + async generate(content: OB11MessageData[], options?: { + source?: string + news?: { text: string }[] + summary?: string + prompt?: string + }) { await this.render(content) return { multiMsgItems: [{ @@ -232,9 +312,10 @@ export class MessageEncoder { } }], tsum: this.tsum, - source: this.isGroup ? '群聊的聊天记录' : '聊天记录', - summary: `查看${this.tsum}条转发消息`, - news: this.news + source: options?.source ?? (this.isGroup ? '群聊的聊天记录' : '聊天记录'), + summary: options?.summary ?? `查看${this.tsum}条转发消息`, + news: options?.news ?? this.news, + prompt: options?.prompt ?? '[聊天记录]' } } } diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index b892f9c5..1f252628 100644 --- a/src/onebot11/types.ts +++ b/src/onebot11/types.ts @@ -378,6 +378,11 @@ export interface OB11PostSendMsg { group_id?: string | number message: OB11MessageMixType auto_escape?: boolean + // 合并转发自定义外显 + source?: string + news?: { text: string }[] + summary?: string + prompt?: string } export interface OB11Version {