-
Notifications
You must be signed in to change notification settings - Fork 227
feat: onebot 自定义合并转发外显和嵌套合并转发支持 #682
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
审查者指南为 OneBot 多消息/合并转发实现可定制展示元数据和嵌套转发消息支持,包括带深度限制的递归编码,以及用于传递自定义字段的 API 扩展。 嵌套转发消息生成与上传的时序图sequenceDiagram
actor Client
participant SendForwardMsg
participant MessageEncoder_outer as MessageEncoder_outer
participant MessageEncoder_inner as MessageEncoder_inner
participant App as App
participant PMHQ as App_pmhq
Client ->> SendForwardMsg: _handle(payload with messages and display fields)
SendForwardMsg ->> MessageEncoder_outer: new MessageEncoder(ctx, peer, 0)
SendForwardMsg ->> MessageEncoder_outer: generate(nodes, options)
activate MessageEncoder_outer
MessageEncoder_outer ->> MessageEncoder_outer: render(content)
MessageEncoder_outer ->> MessageEncoder_outer: visit(segment type Forward with content)
alt forward has content and depth < MAX_FORWARD_DEPTH
MessageEncoder_outer ->> MessageEncoder_inner: new MessageEncoder(ctx, peer, depth+1)
MessageEncoder_outer ->> MessageEncoder_inner: generate(innerNodes, options)
activate MessageEncoder_inner
MessageEncoder_inner ->> MessageEncoder_inner: render(innerNodes)
MessageEncoder_inner ->> MessageEncoder_inner: visit(segment type Node)
MessageEncoder_inner ->> MessageEncoder_inner: render(node content)
MessageEncoder_inner ->> MessageEncoder_inner: flush()
deactivate MessageEncoder_inner
MessageEncoder_outer ->> PMHQ: uploadForward(peer, innerRaw.multiMsgItems)
PMHQ -->> MessageEncoder_outer: resid
MessageEncoder_outer ->> MessageEncoder_outer: packForwardMessage(resid, options)
MessageEncoder_outer ->> MessageEncoder_outer: append child element and preview
else depth >= MAX_FORWARD_DEPTH
MessageEncoder_outer ->> SendForwardMsg: warn and stop parsing
end
MessageEncoder_outer ->> MessageEncoder_outer: flush()
MessageEncoder_outer -->> SendForwardMsg: MultiMsgRaw
deactivate MessageEncoder_outer
SendForwardMsg ->> PMHQ: uploadForward(peer, raw.multiMsgItems)
PMHQ -->> SendForwardMsg: resid
SendForwardMsg ->> App: sendMessage(ctx, peer, forward element with custom source, news, summary, prompt)
App -->> SendForwardMsg: send result
SendForwardMsg -->> Client: Response with message_id and forward_id
更新后的 OneBot 转发消息编码类图classDiagram
class MessageEncoder {
+static support string[]
-results MsgMessageInput[]
-children MsgElemInput[]
-deleteAfterSentFiles string[]
-isGroup boolean
-tsum number
-preview string
-news NewsItem[]
-name string
-uin number
-depth number
+MessageEncoder(ctx Context, peer Peer, depth number)
+flush() Promise~void~
+visit(segment OB11MessageData) Promise~void~
+packForwardMessage(resid string, options ForwardDisplayOptions) MsgElemInput
+generate(content OB11MessageData[], options ForwardDisplayOptions) Promise~MultiMsgRaw~
-render(content OB11MessageData[]) Promise~void~
}
class ForwardDisplayOptions {
+source string
+news NewsItem[]
+summary string
+prompt string
}
class OB11MessageData {
+type OB11MessageDataType
+data any
}
class OB11MessageNode {
+type OB11MessageDataType
+data OB11MessageNodeData
}
class OB11MessageNodeData {
+content OB11MessageData[]
+uin number
+user_id number
+name string
+nickname string
+source string
+news NewsItem[]
+summary string
+prompt string
}
class SendForwardMsg {
+_handle(payload SendForwardPayload) Promise~Response~
-getMessageNode(msgInfo MsgInfo) Promise~OB11MessageNode~
-handleFakeForwardNode(peer Peer, nodes OB11MessageNode[], options ForwardDisplayOptions) Promise~Response~
-handleForwardNode(peer Peer, nodes OB11MessageNode[], msgInfos MsgInfo[]) Promise~Response~
}
class SendForwardPayload {
+messages OB11MessageNode[]
+message OB11MessageNode[]
+message_type string
+source string
+news NewsItem[]
+summary string
+prompt string
}
class OB11PostSendMsg {
+group_id string
+message OB11MessageMixType
+auto_escape boolean
+source string
+news NewsItem[]
+summary string
+prompt string
}
class AppPMHQ {
+uploadForward(peer Peer, items MultiMsgItem[]) Promise~string~
}
class Context {
+logger Logger
+app App
}
class App {
+pmhq AppPMHQ
+sendMessage(ctx Context, peer Peer, elements MsgElemInput[]) Promise~any~
}
MessageEncoder ..> OB11MessageData
MessageEncoder ..> OB11MessageNode
MessageEncoder ..> ForwardDisplayOptions
SendForwardMsg ..> MessageEncoder : uses
SendForwardMsg ..> SendForwardPayload
SendForwardMsg ..> OB11MessageNode
SendForwardMsg ..> ForwardDisplayOptions
OB11PostSendMsg ..> ForwardDisplayOptions
MessageEncoder ..> Context
Context ..> App
App ..> AppPMHQ
AppPMHQ ..> MultiMsgRaw
文件级变更
提示与命令与 Sourcery 交互
自定义你的体验访问你的 dashboard 以:
获取帮助Original review guide in EnglishReviewer's GuideImplements customizable display metadata and nested forward-message support for OneBot multi-message/merge forwarding, including depth-limited recursive encoding and API extensions to pass custom fields. Sequence diagram for nested forward message generation and uploadsequenceDiagram
actor Client
participant SendForwardMsg
participant MessageEncoder_outer as MessageEncoder_outer
participant MessageEncoder_inner as MessageEncoder_inner
participant App as App
participant PMHQ as App_pmhq
Client ->> SendForwardMsg: _handle(payload with messages and display fields)
SendForwardMsg ->> MessageEncoder_outer: new MessageEncoder(ctx, peer, 0)
SendForwardMsg ->> MessageEncoder_outer: generate(nodes, options)
activate MessageEncoder_outer
MessageEncoder_outer ->> MessageEncoder_outer: render(content)
MessageEncoder_outer ->> MessageEncoder_outer: visit(segment type Forward with content)
alt forward has content and depth < MAX_FORWARD_DEPTH
MessageEncoder_outer ->> MessageEncoder_inner: new MessageEncoder(ctx, peer, depth+1)
MessageEncoder_outer ->> MessageEncoder_inner: generate(innerNodes, options)
activate MessageEncoder_inner
MessageEncoder_inner ->> MessageEncoder_inner: render(innerNodes)
MessageEncoder_inner ->> MessageEncoder_inner: visit(segment type Node)
MessageEncoder_inner ->> MessageEncoder_inner: render(node content)
MessageEncoder_inner ->> MessageEncoder_inner: flush()
deactivate MessageEncoder_inner
MessageEncoder_outer ->> PMHQ: uploadForward(peer, innerRaw.multiMsgItems)
PMHQ -->> MessageEncoder_outer: resid
MessageEncoder_outer ->> MessageEncoder_outer: packForwardMessage(resid, options)
MessageEncoder_outer ->> MessageEncoder_outer: append child element and preview
else depth >= MAX_FORWARD_DEPTH
MessageEncoder_outer ->> SendForwardMsg: warn and stop parsing
end
MessageEncoder_outer ->> MessageEncoder_outer: flush()
MessageEncoder_outer -->> SendForwardMsg: MultiMsgRaw
deactivate MessageEncoder_outer
SendForwardMsg ->> PMHQ: uploadForward(peer, raw.multiMsgItems)
PMHQ -->> SendForwardMsg: resid
SendForwardMsg ->> App: sendMessage(ctx, peer, forward element with custom source, news, summary, prompt)
App -->> SendForwardMsg: send result
SendForwardMsg -->> Client: Response with message_id and forward_id
Class diagram for updated OneBot forward message encodingclassDiagram
class MessageEncoder {
+static support string[]
-results MsgMessageInput[]
-children MsgElemInput[]
-deleteAfterSentFiles string[]
-isGroup boolean
-tsum number
-preview string
-news NewsItem[]
-name string
-uin number
-depth number
+MessageEncoder(ctx Context, peer Peer, depth number)
+flush() Promise~void~
+visit(segment OB11MessageData) Promise~void~
+packForwardMessage(resid string, options ForwardDisplayOptions) MsgElemInput
+generate(content OB11MessageData[], options ForwardDisplayOptions) Promise~MultiMsgRaw~
-render(content OB11MessageData[]) Promise~void~
}
class ForwardDisplayOptions {
+source string
+news NewsItem[]
+summary string
+prompt string
}
class OB11MessageData {
+type OB11MessageDataType
+data any
}
class OB11MessageNode {
+type OB11MessageDataType
+data OB11MessageNodeData
}
class OB11MessageNodeData {
+content OB11MessageData[]
+uin number
+user_id number
+name string
+nickname string
+source string
+news NewsItem[]
+summary string
+prompt string
}
class SendForwardMsg {
+_handle(payload SendForwardPayload) Promise~Response~
-getMessageNode(msgInfo MsgInfo) Promise~OB11MessageNode~
-handleFakeForwardNode(peer Peer, nodes OB11MessageNode[], options ForwardDisplayOptions) Promise~Response~
-handleForwardNode(peer Peer, nodes OB11MessageNode[], msgInfos MsgInfo[]) Promise~Response~
}
class SendForwardPayload {
+messages OB11MessageNode[]
+message OB11MessageNode[]
+message_type string
+source string
+news NewsItem[]
+summary string
+prompt string
}
class OB11PostSendMsg {
+group_id string
+message OB11MessageMixType
+auto_escape boolean
+source string
+news NewsItem[]
+summary string
+prompt string
}
class AppPMHQ {
+uploadForward(peer Peer, items MultiMsgItem[]) Promise~string~
}
class Context {
+logger Logger
+app App
}
class App {
+pmhq AppPMHQ
+sendMessage(ctx Context, peer Peer, elements MsgElemInput[]) Promise~any~
}
MessageEncoder ..> OB11MessageData
MessageEncoder ..> OB11MessageNode
MessageEncoder ..> ForwardDisplayOptions
SendForwardMsg ..> MessageEncoder : uses
SendForwardMsg ..> SendForwardPayload
SendForwardMsg ..> OB11MessageNode
SendForwardMsg ..> ForwardDisplayOptions
OB11PostSendMsg ..> ForwardDisplayOptions
MessageEncoder ..> Context
Context ..> App
App ..> AppPMHQ
AppPMHQ ..> MultiMsgRaw
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey - 我发现了两个问题,并留下了一些整体性的反馈:
visit方法中对嵌套转发的处理在Node和Forward分支都重复了深度检查和构建选项的逻辑;可以考虑提取一个小的辅助函数(例如buildForwardOptions/createNestedForward)来减少重复,并让递归规则更容易理解。- 在
visit的Forward分支中,当提供了content但没有找到任何Node元素时,该方法会记录警告并直接返回,而不会渲染非 node 片段或更新preview;请澄清这是否是预期行为,如果不是,建议处理或明确拒绝这类 payload,而不是静默丢弃其中的内容。 - 针对转发选项,目前在多个地方使用了
as any以及零散的内联类型字面量(例如在nodeData处理和forwardData中);通过使用共享的接口,或扩展OB11MessageNode/OB11MessageData,可以让新增的自定义展示字段(source、news、summary、prompt)在类型上更安全,也更易维护。
AI 代理的提示词
请根据这次代码评审中的评论进行修改:
## 总体评论
- `visit` 方法中对嵌套转发的处理在 `Node` 和 `Forward` 分支都重复了深度检查和构建选项的逻辑;可以考虑提取一个小的辅助函数(例如 `buildForwardOptions` / `createNestedForward`)来减少重复,并让递归规则更容易理解。
- 在 `visit` 的 `Forward` 分支中,当提供了 `content` 但没有找到任何 `Node` 元素时,该方法会记录警告并直接返回,而不会渲染非 node 片段或更新 `preview`;请澄清这是否是预期行为,如果不是,建议处理或明确拒绝这类 payload,而不是静默丢弃其中的内容。
- 针对转发选项,目前在多个地方使用了 `as any` 以及零散的内联类型字面量(例如在 `nodeData` 处理和 `forwardData` 中);通过使用共享的接口,或扩展 `OB11MessageNode`/`OB11MessageData`,可以让新增的自定义展示字段(`source`、`news`、`summary`、`prompt`)在类型上更安全,也更易维护。
## 单独评论
### 评论 1
<location> `src/onebot11/helper/createMultiMessage.ts:209-210` </location>
<code_context>
+
+ // 递归生成内层合并转发
+ 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
</code_context>
<issue_to_address>
**question (bug_risk):** 嵌套 node 的处理会忽略该 node 的 `content` 中的非 node 元素,可能导致数据丢失。
当 `hasNestedNodes` 为 true 时,`innerEncoder` 只接收 `innerNodes`,会忽略 `content` 中的非 node 条目。如果协议允许在同一内容中混合 node 和其他 segment 类型,那么这些非 node 部分在嵌套转发中就会丢失。如果需要保留混合内容,请将完整的 `content` 传入 `innerEncoder.generate`。否则,请在节点中显式文档化或防护这类混合内容,以避免误用。
</issue_to_address>
### 评论 2
<location> `src/onebot11/helper/createMultiMessage.ts:11` </location>
<code_context>
import faceConfig from '@/ntqqapi/helper/face_config.json'
import { InferProtoModelInput } from '@saltify/typeproto'
+// 最大嵌套深度
+const MAX_FORWARD_DEPTH = 3
+
</code_context>
<issue_to_address>
**issue (complexity):** 建议将新的嵌套转发处理逻辑拆分为共享的辅助函数和类型,以去除重复代码,并简化深度检查与转发选项相关的控制流程。
在保留所有新功能的前提下,通过一些小的抽象可以显著降低局部复杂度。
### 1. 集中处理深度检查
`MAX_FORWARD_DEPTH` 的检查在 `Node` 和 `Forward` 分支中重复出现;将其抽到一个辅助方法中可以把递归规则集中到一个位置:
```ts
// class MessageEncoder
private ensureDepthAllowed(context: string): boolean {
if (this.depth >= MAX_FORWARD_DEPTH) {
this.ctx.logger.warn(`合并转发嵌套深度超过 ${MAX_FORWARD_DEPTH} 层,将停止解析:${context}`)
return false
}
return true
}
```
用法示例:
```ts
if (hasNestedNodes) {
if (!this.ensureDepthAllowed('node')) return
// ...
}
} else if (type === OB11MessageDataType.Forward) {
// ...
} else if (forwardData.content) {
if (!this.ensureDepthAllowed('forward')) return
// ...
}
}
```
这样可以删除重复的 `if (this.depth >= MAX_FORWARD_DEPTH)` 分支,并让日志格式保持一致。
### 2. 抽取共享的嵌套转发生成逻辑
`Node` 和 `Forward` 中的内层转发生成逻辑几乎完全相同。提取一个辅助方法可以提升可读性并减少重复:
```ts
// Shared type; see next section for definition
private async createNestedForward(
rawContent: OB11MessageData[],
options: ForwardDisplayOptions,
) {
const normalized = message2List(rawContent)
const innerNodes = normalized.filter(
e => e.type === OB11MessageDataType.Node
) as OB11MessageNode[]
if (innerNodes.length === 0) {
this.ctx.logger.warn('nested forward content 中没有有效的 node 节点')
return
}
const innerEncoder = new MessageEncoder(this.ctx, this.peer, this.depth + 1)
const innerRaw = await innerEncoder.generate(innerNodes, options)
const resid = await this.ctx.app.pmhq.uploadForward(this.peer, innerRaw.multiMsgItems)
this.deleteAfterSentFiles.push(...innerEncoder.deleteAfterSentFiles)
this.children.push(this.packForwardMessage(resid, options))
this.preview += '[聊天记录]'
}
```
然后,两个调用点就会变得简单很多:
```ts
// Node branch
if (hasNestedNodes) {
if (!this.ensureDepthAllowed('node')) return
const nestedOptions = this.extractForwardOptionsFromNode(nodeData)
await this.createNestedForward(content, nestedOptions)
} else {
await this.render(content)
}
// Forward branch
} else if (forwardData.content) {
if (!this.ensureDepthAllowed('forward')) return
await this.createNestedForward(forwardData.content, {
source: forwardData.source,
news: forwardData.news,
summary: forwardData.summary,
prompt: forwardData.prompt,
})
}
```
### 3. 引入共享的 `ForwardDisplayOptions` 类型和提取函数
当前选项的结构在多个位置被重复使用,而且部分是未严格建模的类型。通过定义一个共享类型,并为 node 提供一个选项提取函数,可以去除强制类型转换,并在未来更改时集中修改:
```ts
interface ForwardDisplayOptions {
source?: string
news?: { text: string }[]
summary?: string
prompt?: string
}
```
在函数签名中使用它:
```ts
packForwardMessage(resid: string, options?: ForwardDisplayOptions) { ... }
async generate(content: OB11MessageData[], options?: ForwardDisplayOptions) { ... }
```
并集中处理 node 的选项提取:
```ts
private extractForwardOptionsFromNode(nodeData: OB11MessageNode['data']): ForwardDisplayOptions {
const anyData = nodeData as any
return {
source: anyData.source,
news: anyData.news,
summary: anyData.summary,
prompt: anyData.prompt,
}
}
```
这样 `visit` 中就可以避免随处进行临时的类型断言:
```ts
const nestedOptions = this.extractForwardOptionsFromNode(nodeData)
```
而 `Forward` 分支已经使用了兼容的结构。
### 4. 通过一次性拆分内容来扁平化 `Node` 的处理逻辑
目前代码会先计算 `hasNestedNodes` 再分支;改为将内容一次性拆分为 node 与非 node 段,可以让控制流更线性:
```ts
const content = nodeData.content ? message2List(nodeData.content) : []
const nodeSegments = content.filter(e => e.type === OB11MessageDataType.Node)
const normalSegments = content.filter(e => e.type !== OB11MessageDataType.Node)
if (normalSegments.length) {
await this.render(normalSegments)
}
if (nodeSegments.length) {
if (!this.ensureDepthAllowed('node')) return
const nestedOptions = this.extractForwardOptionsFromNode(nodeData)
await this.createNestedForward(nodeSegments, nestedOptions)
}
```
这样可以将“渲染普通内容”和“处理嵌套转发”分离开来,避免在同一个 `if/else` 分支中混合不同关注点。
### 5. 统一转发的 prompt/preview 行为
`'[聊天记录]'` 和 prompt 的默认值逻辑目前比较分散。既然 `generate` 已经定义了默认值,可以依赖它,并通过让 `createNestedForward` 统一处理 preview 的更新来移除部分分支中额外的 `this.preview += '[聊天记录]'`,同时对基于 `id` 的已有转发在单一位置处理:
```ts
} else if (type === OB11MessageDataType.Forward) {
const forwardData: ForwardDisplayOptions & { id?: string; content?: OB11MessageData[] } = data
if (forwardData.id) {
this.children.push(this.packForwardMessage(forwardData.id, forwardData))
this.preview += '[聊天记录]'
} else if (forwardData.content) {
if (!this.ensureDepthAllowed('forward')) return
await this.createNestedForward(forwardData.content, forwardData)
}
}
```
这样,prompt/preview 的默认值由 `packForwardMessage` / `generate` 统一决定,而转发处理代码只需要决定在何时添加预览标记,而不是在多个位置决定“标记内容是什么”。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进评审质量。
Original comment in English
Hey - I've found 2 issues, and left some high level feedback:
- The nested forward handling in
visitduplicates depth checks and option-building logic for bothNodeandForwardbranches; consider extracting a small helper (e.g.buildForwardOptions/createNestedForward) to reduce repetition and make the recursion rules easier to reason about. - In the
Forwardbranch ofvisit, whencontentis provided but noNodeelements are found, the method logs a warning and returns without rendering non-node segments or updatingpreview; clarify whether this is intended or, if not, process or explicitly reject such payloads instead of silently dropping their content. - There are several
as anyand ad-hoc inline type literals for forward options (e.g. innodeDatahandling andforwardData); tightening these with shared interfaces or extendingOB11MessageNode/OB11MessageDatawill make the new custom display fields (source,news,summary,prompt) safer and easier to maintain.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The nested forward handling in `visit` duplicates depth checks and option-building logic for both `Node` and `Forward` branches; consider extracting a small helper (e.g. `buildForwardOptions` / `createNestedForward`) to reduce repetition and make the recursion rules easier to reason about.
- In the `Forward` branch of `visit`, when `content` is provided but no `Node` elements are found, the method logs a warning and returns without rendering non-node segments or updating `preview`; clarify whether this is intended or, if not, process or explicitly reject such payloads instead of silently dropping their content.
- There are several `as any` and ad-hoc inline type literals for forward options (e.g. in `nodeData` handling and `forwardData`); tightening these with shared interfaces or extending `OB11MessageNode`/`OB11MessageData` will make the new custom display fields (`source`, `news`, `summary`, `prompt`) safer and easier to maintain.
## Individual Comments
### Comment 1
<location> `src/onebot11/helper/createMultiMessage.ts:209-210` </location>
<code_context>
+
+ // 递归生成内层合并转发
+ 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
</code_context>
<issue_to_address>
**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.
</issue_to_address>
### Comment 2
<location> `src/onebot11/helper/createMultiMessage.ts:11` </location>
<code_context>
import faceConfig from '@/ntqqapi/helper/face_config.json'
import { InferProtoModelInput } from '@saltify/typeproto'
+// 最大嵌套深度
+const MAX_FORWARD_DEPTH = 3
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the new nested-forward handling into shared helpers and types to remove duplication and simplify the control flow for depth checks and forward options.
You can keep all the new functionality while shaving off quite a bit of local complexity with some small extra abstractions.
### 1. Centralize depth handling
The `MAX_FORWARD_DEPTH` check is duplicated in both `Node` and `Forward` branches; pulling it into a helper keeps the recursion rules in one place:
```ts
// class MessageEncoder
private ensureDepthAllowed(context: string): boolean {
if (this.depth >= MAX_FORWARD_DEPTH) {
this.ctx.logger.warn(`合并转发嵌套深度超过 ${MAX_FORWARD_DEPTH} 层,将停止解析:${context}`)
return false
}
return true
}
```
Usage:
```ts
if (hasNestedNodes) {
if (!this.ensureDepthAllowed('node')) return
// ...
}
} else if (type === OB11MessageDataType.Forward) {
// ...
} else if (forwardData.content) {
if (!this.ensureDepthAllowed('forward')) return
// ...
}
}
```
This removes the duplicated `if (this.depth >= MAX_FORWARD_DEPTH)` branches and keeps the log format consistent.
### 2. Extract shared nested-forward generation
The inner-forward generation logic is almost identical in `Node` and `Forward`. Extracting a helper keeps the flow readable and reduces duplication:
```ts
// Shared type; see next section for definition
private async createNestedForward(
rawContent: OB11MessageData[],
options: ForwardDisplayOptions,
) {
const normalized = message2List(rawContent)
const innerNodes = normalized.filter(
e => e.type === OB11MessageDataType.Node
) as OB11MessageNode[]
if (innerNodes.length === 0) {
this.ctx.logger.warn('nested forward content 中没有有效的 node 节点')
return
}
const innerEncoder = new MessageEncoder(this.ctx, this.peer, this.depth + 1)
const innerRaw = await innerEncoder.generate(innerNodes, options)
const resid = await this.ctx.app.pmhq.uploadForward(this.peer, innerRaw.multiMsgItems)
this.deleteAfterSentFiles.push(...innerEncoder.deleteAfterSentFiles)
this.children.push(this.packForwardMessage(resid, options))
this.preview += '[聊天记录]'
}
```
Then the two call sites become much simpler:
```ts
// Node branch
if (hasNestedNodes) {
if (!this.ensureDepthAllowed('node')) return
const nestedOptions = this.extractForwardOptionsFromNode(nodeData)
await this.createNestedForward(content, nestedOptions)
} else {
await this.render(content)
}
// Forward branch
} else if (forwardData.content) {
if (!this.ensureDepthAllowed('forward')) return
await this.createNestedForward(forwardData.content, {
source: forwardData.source,
news: forwardData.news,
summary: forwardData.summary,
prompt: forwardData.prompt,
})
}
```
### 3. Introduce a shared `ForwardDisplayOptions` type + extractor
The options shape is currently duplicated and partly untyped. Defining a shared type and an extractor for nodes removes casts and keeps future changes localized:
```ts
interface ForwardDisplayOptions {
source?: string
news?: { text: string }[]
summary?: string
prompt?: string
}
```
Use it in signatures:
```ts
packForwardMessage(resid: string, options?: ForwardDisplayOptions) { ... }
async generate(content: OB11MessageData[], options?: ForwardDisplayOptions) { ... }
```
And centralize node option extraction:
```ts
private extractForwardOptionsFromNode(nodeData: OB11MessageNode['data']): ForwardDisplayOptions {
const anyData = nodeData as any
return {
source: anyData.source,
news: anyData.news,
summary: anyData.summary,
prompt: anyData.prompt,
}
}
```
Then `visit` can avoid ad-hoc casting:
```ts
const nestedOptions = this.extractForwardOptionsFromNode(nodeData)
```
and the `Forward` branch is already using a compatible shape.
### 4. Flatten `Node` handling by splitting content once
Right now you compute `hasNestedNodes` and branch; splitting once into node vs non-node segments can make the control flow more linear:
```ts
const content = nodeData.content ? message2List(nodeData.content) : []
const nodeSegments = content.filter(e => e.type === OB11MessageDataType.Node)
const normalSegments = content.filter(e => e.type !== OB11MessageDataType.Node)
if (normalSegments.length) {
await this.render(normalSegments)
}
if (nodeSegments.length) {
if (!this.ensureDepthAllowed('node')) return
const nestedOptions = this.extractForwardOptionsFromNode(nodeData)
await this.createNestedForward(nodeSegments, nestedOptions)
}
```
This separates “render normal content” from “handle nested forward” and avoids mixing concerns in a single `if/else`.
### 5. Centralize prompt/preview behavior for forwards
`'[聊天记录]'` and prompt defaulting are scattered. Since `generate` already defines defaults, you can rely on it and remove the extra manual `this.preview += '[聊天记录]'` in some branches by letting `createNestedForward` handle preview updates consistently (as in the helper above), and for existing `id`-based forwards, single place:
```ts
} else if (type === OB11MessageDataType.Forward) {
const forwardData: ForwardDisplayOptions & { id?: string; content?: OB11MessageData[] } = data
if (forwardData.id) {
this.children.push(this.packForwardMessage(forwardData.id, forwardData))
this.preview += '[聊天记录]'
} else if (forwardData.content) {
if (!this.ensureDepthAllowed('forward')) return
await this.createNestedForward(forwardData.content, forwardData)
}
}
```
This way, prompt/preview defaults are determined in `packForwardMessage`/`generate`, and forward handling code only decides *when* to append a preview marker, not *what* it is in multiple places.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const innerNodes = content.filter(e => e.type === OB11MessageDataType.Node) as OB11MessageNode[] | ||
| const innerRaw = await innerEncoder.generate(innerNodes, nestedOptions) |
There was a problem hiding this comment.
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.
|
LGTM |
拆分的 #678
API 是和 napcat 兼容的
Summary by Sourcery
在保持与现有转发行为兼容的前提下,为 OneBot v11 实现添加对「可自定义」和「嵌套」合并转发消息的支持。
新功能:
source、news、summary和prompt字段,自定义合并转发消息的外观。增强改进:
MessageEncoder和转发消息处理逻辑,使其同时适用于既有的转发 ID 和动态构建的转发内容,并对空负载进行更安全的处理。Original summary in English
Summary by Sourcery
Add support for customizable and nested merged-forward messages in the OneBot v11 implementation while preserving compatibility with existing forward behavior.
New Features:
Enhancements: