Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions src/onebot11/action/go-cqhttp/SendForwardMsg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -35,8 +40,8 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
})

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
Expand Down Expand Up @@ -122,7 +127,12 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
}
}

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) {
Expand Down Expand Up @@ -162,11 +172,17 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
})
}

private async handleFakeForwardNode(peer: Peer, nodes: OB11MessageNode[]): Promise<Response> {
private async handleFakeForwardNode(peer: Peer, nodes: OB11MessageNode[], options?: {
source?: string
news?: { text: string }[]
summary?: string
prompt?: string
}): Promise<Response> {
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,
Expand All @@ -181,7 +197,7 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
type: 'normal',
width: 300,
},
desc: '[聊天记录]',
desc: prompt,
extra: JSON.stringify({
filename: uuid,
tsum: raw.tsum,
Expand All @@ -195,7 +211,7 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
uniseq: uuid,
},
},
prompt: '[聊天记录]',
prompt,
ver: '0.0.0.5',
view: 'contact',
}),
Expand Down
117 changes: 99 additions & 18 deletions src/onebot11/helper/createMultiMessage.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Msg.Message>[]
children: InferProtoModelInput<typeof Msg.Elem>[]
deleteAfterSentFiles: string[]
Expand All @@ -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 = []
Expand All @@ -30,6 +34,7 @@ export class MessageEncoder {
this.tsum = 0
this.preview = ''
this.news = []
this.depth = depth
}

async flush() {
Expand Down Expand Up @@ -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: {
Expand All @@ -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'
})
Expand All @@ -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)
Comment on lines +209 to +210
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (bug_risk): 嵌套 node 的处理会忽略该 node 的 content 中的非 node 元素,可能导致数据丢失。

hasNestedNodes 为 true 时,innerEncoder 只接收 innerNodes,会忽略 content 中的非 node 条目。如果协议允许在同一内容中混合 node 和其他 segment 类型,那么这些非 node 部分在嵌套转发中就会丢失。如果需要保留混合内容,请将完整的 content 传入 innerEncoder.generate。否则,请在节点中显式文档化或防护这类混合内容,以避免误用。

Original comment in English

question (bug_risk): Nested node handling ignores non-node elements in a node’s content, which may drop data.

When hasNestedNodes is true, innerEncoder receives only innerNodes and ignores any non-node entries in content. If the protocol permits mixing nodes with other segment types, those non-node parts will be lost in nested forwards. If mixed content should be preserved, pass the full content into innerEncoder.generate. Otherwise, explicitly document or guard against mixed content in nodes.


// 上传内层合并转发,获取 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({
Expand Down Expand Up @@ -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 += '[聊天记录]'
}
}
Expand All @@ -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: [{
Expand All @@ -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 ?? '[聊天记录]'
}
}
}
5 changes: 5 additions & 0 deletions src/onebot11/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading