Skip to content

增加了shiro对虚拟线程池的支持,增加了plainText字段了,优化了消息的正则匹配逻辑以可以只匹配纯文本消息#371

Merged
MisakaTAT merged 4 commits into
MisakaTAT:mainfrom
FlanChanXwO:main
Jan 27, 2026

Conversation

@FlanChanXwO

@FlanChanXwO FlanChanXwO commented Jan 26, 2026

Copy link
Copy Markdown
Contributor

在以下配置中进行设置,即可开启虚拟线程池

spring:
  threads:
    virtual:
      enabled: true # 默认是 false,也就是默认采用平台线程,反之使用虚拟线程

新增的plainText字段将利于消息过滤匹配,以及开发者提取纯文本消息

@Shiro
@Component
@Slf4j
public class TestPlugin {

    @AnyMessageHandler
    @MessageHandlerFilter(matchPlainText = true)
    public void testPlugin(AnyMessageEvent event) {
        log.info("testPlugin plainMsg = {}", event.getPlainText()); // testPlugin plainMsg = /test
        log.info("testPlugin msg = {}", event.getMessage()); // testPlugin msg = [CQ:reply,id=14261461]/test
        log.info("testPlugin raw_msg = {}", event.getRawMessage()); // testPlugin raw_msg = [CQ:reply,id=14261461]/test
    }

    @AnyMessageHandler
    @MessageHandlerFilter(reply = ReplyEnum.REPLY_ALL, cmd = "/test", matchPlainText = true)
    public void testPlainPlugin(AnyMessageEvent event) {
        log.info("testPlainTextPlugin plainMsg = {}", event.getPlainText()); // testPlainTextPlugin plainMsg = /test
        log.info("testPlainTextPlugin msg = {}", event.getMessage()); // testPlainTextPlugin msg = [CQ:reply,id=14261461]/test
        log.info("testPlainTextPlugin raw_msg = {}", event.getRawMessage()); // testPlainTextPlugin raw_msg = [CQ:reply,id=14261461]/test
    }

    @AnyMessageHandler
    @MessageHandlerFilter(reply = ReplyEnum.REPLY_ALL, cmd = "\\[CQ:reply,id=\\d+\\]\\s*(/test)") // 不匹配纯文本
    public void testMessagePlugin(AnyMessageEvent event) {
        log.info("testMessagePlugin plainMsg = {}", event.getPlainText());// testMessagePlugin plainMsg = /test
        log.info("testMessagePlugin msg = {}", event.getMessage());// testMessagePlugin msg = [CQ:reply,id=14261461]/test
        log.info("testMessagePlugin raw_msg = {}", event.getRawMessage());// testMessagePlugin raw_msg = [CQ:reply,id=14261461]/test

    }
}

Summary by Sourcery

为纯文本消息提取和可配置的正则匹配方式提供支持(可选择对纯文本或完整消息进行匹配),并为 Shiro 任务新增独立的平台线程池和虚拟线程池执行器。

New Features:

  • 在消息事件上暴露一个 plainText 字段,从解析后的消息数组中派生而来,用于更方便地获取纯文本内容。
  • 允许消息处理器的过滤器选择正则命令是匹配纯文本内容还是完整的原始消息,默认使用纯文本匹配。
  • 为 Shiro 任务提供独立的平台线程和虚拟线程执行器,并由 Spring 的线程配置进行控制。

Enhancements:

  • 添加工具方法,将消息字符串和数组转换为纯文本,忽略 CQ 码。
  • 调整消息转换逻辑,使其能够从 ArrayMsg 元素正确重建消息字符串。

Build:

  • 将项目版本从 2.5.3 升级到 2.5.5

Tests:

  • 新增单元测试,验证 stringToPlainText 能够从包含混合 CQ 码的消息中正确提取出仅包含文本的部分。
Original summary in English

Summary by Sourcery

Add support for plain-text message extraction and configurable regex matching against plain text or full messages, and introduce separate platform and virtual thread pool executors for Shiro tasks.

New Features:

  • Expose a plainText field on message events derived from the parsed message array for easier access to pure text content.
  • Allow message handler filters to choose whether regex commands match against pure text content or the full raw message, defaulting to plain-text matching.
  • Provide separate Shiro task executors for platform and virtual threads controlled by Spring threading configuration.

Enhancements:

  • Add utilities to convert message strings and arrays to pure text, ignoring CQ codes.
  • Adjust message conversion logic to correctly rebuild message strings from ArrayMsg elements.

Build:

  • Bump project version from 2.5.3 to 2.5.5.

Tests:

  • Add a unit test verifying that stringToPlainText correctly extracts only the text portions from mixed CQ-code messages.

- 新增 MessageConverser.arrayToPlainText 和 stringToPlainText 方法,实现消息纯文本提取
- MessageEvent 增加 plainText 字段,自动同步纯文本内容
- MessageHandlerFilter 增加 matchPlainText 配置 (默认开启) ,支持仅匹配纯文本消息
- 优化 filterCheck 方法,支持按需匹配纯文本或原始消息
- 修正 arraysToString 逻辑,完善 CQ 码与文本处理
- 补充相关单元测试,验证纯文本提取与匹配功能
- 升级版本号至 2.5.5
@sourcery-ai

sourcery-ai Bot commented Jan 26, 2026

Copy link
Copy Markdown

Reviewer's Guide

为 Shiro 的任务执行器添加虚拟线程支持,引入消息的派生纯文本视图,并更新消息过滤逻辑,以可选地基于纯文本而非原始 CQ 编码内容进行匹配,同时增加相应测试并提升版本号。

Sequence diagram for message filtering with optional plain-text matching

sequenceDiagram
    actor PluginDeveloper
    participant AnyMessageEvent as AnyMessageEvent
    participant MessageEvent as MessageEvent
    participant MessageConverser as MessageConverser
    participant CommonUtils as CommonUtils
    participant RegexUtils as RegexUtils

    PluginDeveloper->>AnyMessageEvent: send CQ-coded message
    AnyMessageEvent->>MessageEvent: construct from JSON message
    activate MessageEvent
    MessageEvent->>MessageConverser: stringToArray(message)
    MessageConverser-->>MessageEvent: List ArrayMsg
    MessageEvent->>MessageConverser: arrayToPlainText(arrayMsg)
    MessageConverser-->>MessageEvent: plainText
    deactivate MessageEvent

    AnyMessageEvent->>CommonUtils: allFilterCheck(event, selfId, filter)
    activate CommonUtils
    CommonUtils->>CommonUtils: msgExtract(event.getMessage(), event.getArrayMsg(), filter.at(), event.getSelfId())
    CommonUtils-->>CommonUtils: rawMessage

    alt filter.matchPlainText() is true
        CommonUtils->>RegexUtils: matcher(filter.cmd(), event.getPlainText())
    else filter.matchPlainText() is false
        CommonUtils->>RegexUtils: matcher(filter.cmd(), rawMessage)
    end

    RegexUtils-->>CommonUtils: Optional Matcher
    CommonUtils-->>AnyMessageEvent: CheckResult
    deactivate CommonUtils
Loading

Class diagram for updated message processing and filtering

classDiagram
    class MessageEvent {
        - String message
        - String rawMessage
        - List~ArrayMsg~ arrayMsg
        - String plainText
        + String getMessage()
        + String getRawMessage()
        + List~ArrayMsg~ getArrayMsg()
        + String getPlainText()
        - void setMessageFromJson(JsonNode json)
    }

    class MessageConverser {
        - MessageConverser()
        + String arraysToString(List~ArrayMsg~ array)
        + List~ArrayMsg~ stringToArray(String msg)
        + String arrayToPlainText(List~ArrayMsg~ array)
        + String stringToPlainText(String msg)
        - void addTextMsg(List~ArrayMsg~ chain, String text)
    }

    class ArrayMsg {
        + MsgTypeEnum getType()
        + String toCQCode()
        + String getStringData(String key)
    }

    class MsgTypeEnum {
        <<enumeration>>
        text
        image
        other
    }

    class MessageHandlerFilter {
        <<annotation>>
        + boolean at() default false
        + String cmd() default ""
        + boolean invert() default false
        + boolean matchPlainText() default true
    }

    class CommonUtils {
        + CheckResult allFilterCheck(MessageEvent event, long selfId, MessageHandlerFilter filter)
        - CheckResult filterCheck(MessageEvent event, long selfId, MessageHandlerFilter filter)
        - String msgExtract(String message, List~ArrayMsg~ arrayMsg, boolean at, long selfId)
    }

    class RegexUtils {
        + Optional~Matcher~ matcher(String pattern, String input)
    }

    class ShiroTaskPoolConfig {
        - TaskPoolProperties taskPoolProperties
        + ShiroTaskPoolConfig(TaskPoolProperties taskPoolProperties)
        + ThreadPoolTaskExecutor shiroTaskPlatformExecutor()
        + ThreadPoolTaskExecutor shiroTaskVirtualExecutor()
        - ThreadPoolTaskExecutor getThreadPoolTaskExecutor(ThreadFactory factory)
    }

    class TaskPoolProperties {
        + int getCorePoolSize()
        + int getMaxPoolSize()
        + int getQueueCapacity()
        + String getThreadNamePrefix()
        + int getKeepAliveSeconds()
        + boolean isWaitForTasksToCompleteOnShutdown()
        + int getAwaitTerminationSeconds()
    }

    MessageEvent --> MessageConverser : uses
    MessageEvent --> ArrayMsg : contains
    ArrayMsg --> MsgTypeEnum : typed_by
    CommonUtils --> MessageEvent : uses
    CommonUtils --> MessageHandlerFilter : uses
    CommonUtils --> RegexUtils : uses
    ShiroTaskPoolConfig --> TaskPoolProperties : depends_on
    ShiroTaskPoolConfig --> ThreadPoolTaskExecutor : produces
Loading

Flow diagram for conversion from CQ-coded message to plain-text

flowchart TD
    A["Incoming message string or array"] --> B{Type}
    B -->|String| C["MessageConverser.stringToArray(msg)"]
    B -->|List ArrayMsg| D["arrayMsg already present"]

    C --> E["arrayMsg: List ArrayMsg"]
    D --> E

    E --> F["MessageConverser.arrayToPlainText(arrayMsg)"]
    F --> G["Iterate items\nif type == MsgTypeEnum.text\nappend getStringData(text)"]
    G --> H["plainText"]

    H --> I["MessageEvent.plainText field"]
Loading

File-Level Changes

Change Details Files
暴露由 ArrayMsg 链派生的消息纯文本表示,并将其接入 MessageEvent 反序列化流程。
  • 修复 arraysToString 逻辑:对非文本片段追加 CQ 码,对文本片段追加原始文本。
  • 新增 arrayToPlainText,用仅包含文本类型 ArrayMsg 条目构建纯文本字符串。
  • 新增 stringToPlainText 辅助方法,复用 stringToArray 和 arrayToPlainText,从 CQ 消息字符串中提取纯文本。
  • 在 MessageEvent 从字符串或数组 JSON 表示反序列化时,填充新的 plainText 字段。
  • 新增单元测试,验证 stringToPlainText 能去除非文本 CQ 片段,同时保留文本与换行符。
src/main/java/com/mikuac/shiro/common/utils/MessageConverser.java
src/test/java/com/mikuac/shiro/common/utils/MessageConverserTest.java
src/main/java/com/mikuac/shiro/dto/event/message/MessageEvent.java
引入配置,以在 Shiro 任务执行中在平台线程池与虚拟线程池之间进行选择。
  • 将 shiroTaskExecutor 拆分为两个 bean:一个用于平台线程,一个用于虚拟线程,并根据 Spring Boot 的线程模型进行条件装配。
  • 新增共享的 getThreadPoolTaskExecutor 辅助方法,接受 ThreadFactory,并在应用 TaskPoolProperties 之前将其设置到 ThreadPoolTaskExecutor 上。
  • 使用 Thread.ofPlatform() 和 Thread.ofVirtual() 工厂来支撑各自的执行器,并保留现有的 shiro.task-pool.enable-task-pool 属性开关。
src/main/java/com/mikuac/shiro/task/ShiroTaskPoolConfig.java
扩展 MessageHandlerFilter,默认支持基于纯文本消息进行正则匹配,并提供回退到原始消息的选项。
  • 在 MessageHandlerFilter 中新增 matchPlainText 标志,默认值为 true,并在 Javadoc 中说明其行为。
  • 修改 CommonUtils.filterCheck,使其基于处理后的消息文本构建 rawMessage,并根据 matchPlainText 选择正则匹配输入(plainText 或 rawMessage)。
src/main/java/com/mikuac/shiro/annotation/MessageHandlerFilter.java
src/main/java/com/mikuac/shiro/common/utils/CommonUtils.java
使项目元数据与新功能保持一致。
  • 将项目版本号从 2.5.3 提升到 2.5.5。
build.gradle.kts

Possibly linked issues

  • #(unprovided): 该 PR 在启用 Spring 虚拟线程时实现了条件性的虚拟线程 Shiro 执行器,直接响应了该 issue 中的提案。

Tips and commands

Interacting with Sourcery

  • 触发新评审: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的评审评论。
  • 从评审评论生成 GitHub issue: 在评审评论下回复,请 Sourcery 从该评论创建 issue。你也可以回复 @sourcery-ai issue 来基于该评论创建 issue。
  • 生成 pull request 标题: 在 pull request 标题任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文任意位置写上 @sourcery-ai summary,即可在对应位置生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成评审者指南: 在 pull request 中评论 @sourcery-ai guide,即可(重新)生成评审者指南。
  • 一次性解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,可将所有 Sourcery 评论标记为已解决。适用于你已经处理完所有评论且不希望再看到它们时。
  • 移除所有 Sourcery 评审: 在 pull request 中评论 @sourcery-ai dismiss,可移除所有现有的 Sourcery 评审。特别适用于你想在一个全新的评审基础上开始——记得随后评论 @sourcery-ai review 触发新的评审!

Customizing Your Experience

访问你的 dashboard 以:

  • 启用或禁用评审功能,例如 Sourcery 自动生成的 pull request 摘要、评审者指南等。
  • 修改评审语言。
  • 添加、删除或编辑自定义评审指令。
  • 调整其他评审相关设置。

Getting Help

Original review guide in English

Reviewer's Guide

Adds virtual-thread support to Shiro's task executor, introduces a derived plain-text view of messages, and updates message filtering to optionally match against plain text instead of the raw CQ-coded content, along with tests and a version bump.

Sequence diagram for message filtering with optional plain-text matching

sequenceDiagram
    actor PluginDeveloper
    participant AnyMessageEvent as AnyMessageEvent
    participant MessageEvent as MessageEvent
    participant MessageConverser as MessageConverser
    participant CommonUtils as CommonUtils
    participant RegexUtils as RegexUtils

    PluginDeveloper->>AnyMessageEvent: send CQ-coded message
    AnyMessageEvent->>MessageEvent: construct from JSON message
    activate MessageEvent
    MessageEvent->>MessageConverser: stringToArray(message)
    MessageConverser-->>MessageEvent: List ArrayMsg
    MessageEvent->>MessageConverser: arrayToPlainText(arrayMsg)
    MessageConverser-->>MessageEvent: plainText
    deactivate MessageEvent

    AnyMessageEvent->>CommonUtils: allFilterCheck(event, selfId, filter)
    activate CommonUtils
    CommonUtils->>CommonUtils: msgExtract(event.getMessage(), event.getArrayMsg(), filter.at(), event.getSelfId())
    CommonUtils-->>CommonUtils: rawMessage

    alt filter.matchPlainText() is true
        CommonUtils->>RegexUtils: matcher(filter.cmd(), event.getPlainText())
    else filter.matchPlainText() is false
        CommonUtils->>RegexUtils: matcher(filter.cmd(), rawMessage)
    end

    RegexUtils-->>CommonUtils: Optional Matcher
    CommonUtils-->>AnyMessageEvent: CheckResult
    deactivate CommonUtils
Loading

Class diagram for updated message processing and filtering

classDiagram
    class MessageEvent {
        - String message
        - String rawMessage
        - List~ArrayMsg~ arrayMsg
        - String plainText
        + String getMessage()
        + String getRawMessage()
        + List~ArrayMsg~ getArrayMsg()
        + String getPlainText()
        - void setMessageFromJson(JsonNode json)
    }

    class MessageConverser {
        - MessageConverser()
        + String arraysToString(List~ArrayMsg~ array)
        + List~ArrayMsg~ stringToArray(String msg)
        + String arrayToPlainText(List~ArrayMsg~ array)
        + String stringToPlainText(String msg)
        - void addTextMsg(List~ArrayMsg~ chain, String text)
    }

    class ArrayMsg {
        + MsgTypeEnum getType()
        + String toCQCode()
        + String getStringData(String key)
    }

    class MsgTypeEnum {
        <<enumeration>>
        text
        image
        other
    }

    class MessageHandlerFilter {
        <<annotation>>
        + boolean at() default false
        + String cmd() default ""
        + boolean invert() default false
        + boolean matchPlainText() default true
    }

    class CommonUtils {
        + CheckResult allFilterCheck(MessageEvent event, long selfId, MessageHandlerFilter filter)
        - CheckResult filterCheck(MessageEvent event, long selfId, MessageHandlerFilter filter)
        - String msgExtract(String message, List~ArrayMsg~ arrayMsg, boolean at, long selfId)
    }

    class RegexUtils {
        + Optional~Matcher~ matcher(String pattern, String input)
    }

    class ShiroTaskPoolConfig {
        - TaskPoolProperties taskPoolProperties
        + ShiroTaskPoolConfig(TaskPoolProperties taskPoolProperties)
        + ThreadPoolTaskExecutor shiroTaskPlatformExecutor()
        + ThreadPoolTaskExecutor shiroTaskVirtualExecutor()
        - ThreadPoolTaskExecutor getThreadPoolTaskExecutor(ThreadFactory factory)
    }

    class TaskPoolProperties {
        + int getCorePoolSize()
        + int getMaxPoolSize()
        + int getQueueCapacity()
        + String getThreadNamePrefix()
        + int getKeepAliveSeconds()
        + boolean isWaitForTasksToCompleteOnShutdown()
        + int getAwaitTerminationSeconds()
    }

    MessageEvent --> MessageConverser : uses
    MessageEvent --> ArrayMsg : contains
    ArrayMsg --> MsgTypeEnum : typed_by
    CommonUtils --> MessageEvent : uses
    CommonUtils --> MessageHandlerFilter : uses
    CommonUtils --> RegexUtils : uses
    ShiroTaskPoolConfig --> TaskPoolProperties : depends_on
    ShiroTaskPoolConfig --> ThreadPoolTaskExecutor : produces
Loading

Flow diagram for conversion from CQ-coded message to plain-text

flowchart TD
    A["Incoming message string or array"] --> B{Type}
    B -->|String| C["MessageConverser.stringToArray(msg)"]
    B -->|List ArrayMsg| D["arrayMsg already present"]

    C --> E["arrayMsg: List ArrayMsg"]
    D --> E

    E --> F["MessageConverser.arrayToPlainText(arrayMsg)"]
    F --> G["Iterate items\nif type == MsgTypeEnum.text\nappend getStringData(text)"]
    G --> H["plainText"]

    H --> I["MessageEvent.plainText field"]
Loading

File-Level Changes

Change Details Files
Expose plain-text representation of messages derived from ArrayMsg chains and wire it into MessageEvent deserialization.
  • Fix arraysToString logic to append CQ codes for non-text segments and raw text for text segments.
  • Add arrayToPlainText to build a pure-text string from only text-type ArrayMsg entries.
  • Add stringToPlainText helper that reuses stringToArray and arrayToPlainText to extract pure text from a CQ message string.
  • Populate a new plainText field in MessageEvent whenever the message is deserialized from either string or array JSON representations.
  • Add a unit test to verify that stringToPlainText strips non-text CQ segments while preserving text and newlines.
src/main/java/com/mikuac/shiro/common/utils/MessageConverser.java
src/test/java/com/mikuac/shiro/common/utils/MessageConverserTest.java
src/main/java/com/mikuac/shiro/dto/event/message/MessageEvent.java
Introduce configuration to choose between platform and virtual thread pools for Shiro task execution.
  • Split shiroTaskExecutor into two beans: one for platform threads and one for virtual threads, conditioned on Spring Boot's Threading model.
  • Add a shared getThreadPoolTaskExecutor helper that accepts a ThreadFactory and sets it on ThreadPoolTaskExecutor before applying TaskPoolProperties.
  • Use Thread.ofPlatform() and Thread.ofVirtual() factories to back the respective executors, and keep the existing shiro.task-pool.enable-task-pool property gate.
src/main/java/com/mikuac/shiro/task/ShiroTaskPoolConfig.java
Extend MessageHandlerFilter to support regex matching against plain-text messages by default, with an option to fall back to raw messages.
  • Add a matchPlainText flag to MessageHandlerFilter with default true and Javadoc explaining the behavior.
  • Change CommonUtils.filterCheck to build rawMessage from the processed message text instead of rawMessage and to select the regex input based on matchPlainText (plainText vs rawMessage).
src/main/java/com/mikuac/shiro/annotation/MessageHandlerFilter.java
src/main/java/com/mikuac/shiro/common/utils/CommonUtils.java
Align project metadata with the new functionality.
  • Bump project version from 2.5.3 to 2.5.5.
build.gradle.kts

Possibly linked issues

  • #(unprovided): PR implements conditional virtual-thread Shiro executor when Spring virtual threads enabled, directly addressing the issue’s proposal

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - 我发现了两个问题,并给出了一些整体性的反馈:

  • MessageConverser.arraysToString 中的条件从 if (MsgTypeEnum.text.equals(item.getType())) 改成 if (!MsgTypeEnum.text.equals(item.getType())) 实际上反转了原有行为,而且看起来并非有意为之:现在把“非文本”当作默认分支处理;请确认这一改动是否是刻意设计的,如不是,请恢复原始条件。
  • MessageConverser.arrayToPlainText 中,建议使用 MsgTypeEnum.text.toString()(或一个共享常量)来代替 getStringData 中硬编码的 "text" 字面量,以保持基于枚举的 key 使用方式一致,降低将来枚举名变更时引发问题的风险。
给 AI Agent 的提示词
请根据本次代码审查中的评论进行修改:

## 总体评论
-`MessageConverser.arraysToString` 中的条件从 `if (MsgTypeEnum.text.equals(item.getType()))` 改成 `if (!MsgTypeEnum.text.equals(item.getType()))` 实际上反转了原有行为,而且看起来并非有意为之:现在把“非文本”当作默认分支处理;请确认这一改动是否是刻意设计的,如不是,请恢复原始条件。
-`MessageConverser.arrayToPlainText` 中,建议使用 `MsgTypeEnum.text.toString()`(或一个共享常量)来代替 `getStringData` 中硬编码的 "text" 字面量,以保持基于枚举的 key 使用方式一致,降低将来枚举名变更时引发问题的风险。

## 逐条评论

### Comment 1
<location> `src/main/java/com/mikuac/shiro/common/utils/MessageConverser.java:21-24` </location>
<code_context>
     public static String arraysToString(List<ArrayMsg> array) {
         StringBuilder builder = new StringBuilder();
         for (ArrayMsg item : array) {
-            if (MsgTypeEnum.text.equals(item.getType())) {
+            if (!MsgTypeEnum.text.equals(item.getType())) {
                 builder.append(item.toCQCode());
</code_context>

<issue_to_address>
**suggestion:** 避免为 key 硬编码 "text" 字面量,改为复用枚举值以保持一致性。

该方法在此处使用 `item.getStringData("text")`,但在类的其他位置却使用 `item.getStringData(MsgTypeEnum.text.toString())`。一旦枚举名或底层 key 发生变化,这两处行为就会出现差异。建议这里也统一使用 `MsgTypeEnum.text.toString()`,从而在行为上保持一致,并在重构时更加健壮。

建议实现如下:

```java
    public static String arraysToString(List<ArrayMsg> array) {
        StringBuilder builder = new StringBuilder();
        for (ArrayMsg item : array) {
            if (!MsgTypeEnum.text.equals(item.getType())) {
                builder.append(item.toCQCode());
            } else {
                builder.append(item.getStringData(MsgTypeEnum.text.toString()));
            }
        }
        return builder.toString();
    }

```

如果在 `MessageConverser.java`(或相关类)中还有其他 `item.getStringData("text")` 的使用位置,也建议一并替换为 `item.getStringData(MsgTypeEnum.text.toString())`,以保持行为一致,并在未来对枚举进行重构时更为稳健。
</issue_to_address>

### Comment 2
<location> `src/test/java/com/mikuac/shiro/common/utils/MessageConverserTest.java:183-188` </location>
<code_context>
         assertEquals(originalResult, optimizedResult);
     }

+    @Test
+    void testPlainMsgTest() {
+        val msg = "[CQ:at,qq=1122334455]测试消息1[CQ:face,id=1]测试消息2[CQ:video,file=https://test.com/1.mp4][CQ:image,file=test1.image,url=https://test.com/1.jpg]\n[CQ:image,file=test2.image,url=https://test.com/2.jpg]";
+        val expected = "测试消息1测试消息2\n";
+        val plainText = MessageConverser.stringToPlainText(msg);
+        assertEquals(expected, plainText);
+    }
+
</code_context>

<issue_to_address>
**suggestion (testing):**`stringToPlainText``arrayToPlainText` 增加更多边界场景测试用例。

新的 `testPlainMsgTest` 覆盖了一个较复杂的场景,但这些工具方法还可以通过一些更加针对性的测试获益:

- 只包含 **CQ 码且没有任何文本** 的情况(纯文本结果应为空字符串)。
- 只包含 **纯文本** 且没有 CQ 码的情况,以验证这是一个 no-op。
- 文本 **前后包裹 CQ 码** 且使用不同分隔符(空格/换行)的情况,以验证格式是否正确保留。
- 直接针对 `arrayToPlainText(List<ArrayMsg>)` 编写单元测试,手动构造 `ArrayMsg` 实例,避免潜在问题被 `stringToArray` 的中间步骤掩盖。

这些测试有助于在你期望支持的主要消息模式下验证 `plainText` 的行为。

建议实现如下:

```java
    @Test
    void testPlainMsgTest() {
        val msg = "[CQ:at,qq=1122334455]测试消息1[CQ:face,id=1]测试消息2[CQ:video,file=https://test.com/1.mp4][CQ:image,file=test1.image,url=https://test.com/1.jpg]\n[CQ:image,file=test2.image,url=https://test.com/2.jpg]";
        val expected = "测试消息1测试消息2\n";
        val plainText = MessageConverser.stringToPlainText(msg);
        assertEquals(expected, plainText);
    }

    /**
     * 仅包含 CQ 码、没有任何纯文本,结果应为空字符串
     */
    @Test
    void testStringToPlainTextOnlyCQCodes() {
        val msg = "[CQ:at,qq=1122334455][CQ:face,id=1][CQ:image,file=test.image,url=https://test.com/1.jpg]";
        val expected = "";
        val plainText = MessageConverser.stringToPlainText(msg);
        assertEquals(expected, plainText);
    }

    /**
     * 仅包含纯文本,stringToPlainText 应为 no-op
     */
    @Test
    void testStringToPlainTextOnlyText() {
        val msg = "这是一个纯文本消息 without any CQ codes.\n第二行文本。";
        val expected = msg;
        val plainText = MessageConverser.stringToPlainText(msg);
        assertEquals(expected, plainText);
    }

    /**
     * 文本前后存在 CQ 码与不同分隔符,确保格式(空格/换行)被保留
     */
    @Test
    void testStringToPlainTextLeadingAndTrailingCQCodes() {
        val msg = "[CQ:at,qq=1122334455] 前置文本 [CQ:face,id=1]\n后置文本 [CQ:image,file=test.image,url=https://test.com/1.jpg]";
        // 预期只去掉 CQ 码,本身的空格和换行应当保留
        val expected = " 前置文本 \n后置文本 ";
        val plainText = MessageConverser.stringToPlainText(msg);
        assertEquals(expected, plainText);
    }

    /**
     * 直接使用手动构造的 ArrayMsg 列表测试 arrayToPlainText,
     * 避免 stringToArray 中间步骤掩盖潜在问题
     */
    @Test
    void testArrayToPlainTextDirectly() {
        val arrayMsgs = new java.util.ArrayList<ArrayMsg>();

        // 非文本类型(如 at),不应该出现在 plain text 中
        {
            val atMsg = new ArrayMsg();
            atMsg.setType("at");
            atMsg.setText(null);
            arrayMsgs.add(atMsg);
        }

        // 第一段纯文本
        {
            val textMsg1 = new ArrayMsg();
            textMsg1.setType("text");
            textMsg1.setText("测试消息1");
            arrayMsgs.add(textMsg1);
        }

        // 表情类 CQ 码,假设不参与 plain text 输出
        {
            val faceMsg = new ArrayMsg();
            faceMsg.setType("face");
            faceMsg.setText(null);
            arrayMsgs.add(faceMsg);
        }

        // 第二段纯文本(带换行)
        {
            val textMsg2 = new ArrayMsg();
            textMsg2.setType("text");
            textMsg2.setText("测试消息2\n");
            arrayMsgs.add(textMsg2);
        }

        // 图片/视频等富媒体消息
        {
            val imageMsg = new ArrayMsg();
            imageMsg.setType("image");
            imageMsg.setText(null);
            arrayMsgs.add(imageMsg);
        }

        val expected = "测试消息1测试消息2\n";
        val plainText = MessageConverser.arrayToPlainText(arrayMsgs);
        assertEquals(expected, plainText);
    }

    @Test
    void rawToArrayMsg2Test() {

```

1. 确认 `ArrayMsg` 类的包名和 API- 如果当前测试文件尚未导入 `ArrayMsg`,需要在文件顶部添加正确的 `import`,如 `import com.mikuac.shiro.dto.event.message.ArrayMsg;`(请根据实际项目结构调整)。
   - 如果 `ArrayMsg` 不是通过无参构造 + `setType`/`setText` 方式使用,而是有构造函数或 builder,请将 `new ArrayMsg(); setType(...); setText(...);` 改成项目中实际使用的构建方式。
2. 如果 `arrayToPlainText` 的语义与这里的假设不同(例如会保留某些非 text 类型的内容,或对换行有特殊处理),请同步调整各测试中的 `expected` 字符串,以匹配实际行为。
</issue_to_address>

Sourcery 对开源项目免费 —— 如果你觉得这些 review 有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的代码审查。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • The change in MessageConverser.arraysToString from if (MsgTypeEnum.text.equals(item.getType())) to if (!MsgTypeEnum.text.equals(item.getType())) inverts the previous behavior and looks unintended, as it now treats non-text as the default branch; please verify and restore the original condition if this wasn’t deliberate.
  • In MessageConverser.arrayToPlainText, consider using MsgTypeEnum.text.toString() (or a shared constant) instead of the hard-coded "text" literal for getStringData to keep the enum-based key usage consistent and reduce risk of breakage if the enum name changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The change in `MessageConverser.arraysToString` from `if (MsgTypeEnum.text.equals(item.getType()))` to `if (!MsgTypeEnum.text.equals(item.getType()))` inverts the previous behavior and looks unintended, as it now treats non-text as the default branch; please verify and restore the original condition if this wasn’t deliberate.
- In `MessageConverser.arrayToPlainText`, consider using `MsgTypeEnum.text.toString()` (or a shared constant) instead of the hard-coded "text" literal for `getStringData` to keep the enum-based key usage consistent and reduce risk of breakage if the enum name changes.

## Individual Comments

### Comment 1
<location> `src/main/java/com/mikuac/shiro/common/utils/MessageConverser.java:21-24` </location>
<code_context>
     public static String arraysToString(List<ArrayMsg> array) {
         StringBuilder builder = new StringBuilder();
         for (ArrayMsg item : array) {
-            if (MsgTypeEnum.text.equals(item.getType())) {
+            if (!MsgTypeEnum.text.equals(item.getType())) {
                 builder.append(item.toCQCode());
</code_context>

<issue_to_address>
**suggestion:** Avoid hardcoded "text" literal for key and reuse the enum value for consistency.

This method mixes `item.getStringData("text")` with `item.getStringData(MsgTypeEnum.text.toString())` used elsewhere in the class. If the enum name or underlying key changes, this will behave differently. Use `MsgTypeEnum.text.toString()` here as well to keep behavior consistent and robust against refactors.

Suggested implementation:

```java
    public static String arraysToString(List<ArrayMsg> array) {
        StringBuilder builder = new StringBuilder();
        for (ArrayMsg item : array) {
            if (!MsgTypeEnum.text.equals(item.getType())) {
                builder.append(item.toCQCode());
            } else {
                builder.append(item.getStringData(MsgTypeEnum.text.toString()));
            }
        }
        return builder.toString();
    }

```

If there are any other occurrences of `item.getStringData("text")` elsewhere in `MessageConverser.java` (or related classes), they should also be updated to `item.getStringData(MsgTypeEnum.text.toString())` to keep the behavior consistent and robust against future enum refactors.
</issue_to_address>

### Comment 2
<location> `src/test/java/com/mikuac/shiro/common/utils/MessageConverserTest.java:183-188` </location>
<code_context>
         assertEquals(originalResult, optimizedResult);
     }

+    @Test
+    void testPlainMsgTest() {
+        val msg = "[CQ:at,qq=1122334455]测试消息1[CQ:face,id=1]测试消息2[CQ:video,file=https://test.com/1.mp4][CQ:image,file=test1.image,url=https://test.com/1.jpg]\n[CQ:image,file=test2.image,url=https://test.com/2.jpg]";
+        val expected = "测试消息1测试消息2\n";
+        val plainText = MessageConverser.stringToPlainText(msg);
+        assertEquals(expected, plainText);
+    }
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add more edge-case coverage for `stringToPlainText` and `arrayToPlainText`.

The new `testPlainMsgTest` covers one complex scenario, but the utilities would benefit from a few more targeted tests:

- A case with **only CQ codes and no text** (plain text should be an empty string).
- A case with **only text** and no CQ codes to confirm it’s a no-op.
- A case with **leading/trailing CQ codes around text** with different separators (spaces/newlines) to confirm formatting.
- A direct unit test for `arrayToPlainText(List<ArrayMsg>)` using manually constructed `ArrayMsg` instances, so issues aren’t hidden by `stringToArray`.

These will help validate `plainText` across the main message patterns you expect to handle.

Suggested implementation:

```java
    @Test
    void testPlainMsgTest() {
        val msg = "[CQ:at,qq=1122334455]测试消息1[CQ:face,id=1]测试消息2[CQ:video,file=https://test.com/1.mp4][CQ:image,file=test1.image,url=https://test.com/1.jpg]\n[CQ:image,file=test2.image,url=https://test.com/2.jpg]";
        val expected = "测试消息1测试消息2\n";
        val plainText = MessageConverser.stringToPlainText(msg);
        assertEquals(expected, plainText);
    }

    /**
     * 仅包含 CQ 码、没有任何纯文本,结果应为空字符串
     */
    @Test
    void testStringToPlainTextOnlyCQCodes() {
        val msg = "[CQ:at,qq=1122334455][CQ:face,id=1][CQ:image,file=test.image,url=https://test.com/1.jpg]";
        val expected = "";
        val plainText = MessageConverser.stringToPlainText(msg);
        assertEquals(expected, plainText);
    }

    /**
     * 仅包含纯文本,stringToPlainText 应为 no-op
     */
    @Test
    void testStringToPlainTextOnlyText() {
        val msg = "这是一个纯文本消息 without any CQ codes.\n第二行文本。";
        val expected = msg;
        val plainText = MessageConverser.stringToPlainText(msg);
        assertEquals(expected, plainText);
    }

    /**
     * 文本前后存在 CQ 码与不同分隔符,确保格式(空格/换行)被保留
     */
    @Test
    void testStringToPlainTextLeadingAndTrailingCQCodes() {
        val msg = "[CQ:at,qq=1122334455] 前置文本 [CQ:face,id=1]\n后置文本 [CQ:image,file=test.image,url=https://test.com/1.jpg]";
        // 预期只去掉 CQ 码,本身的空格和换行应当保留
        val expected = " 前置文本 \n后置文本 ";
        val plainText = MessageConverser.stringToPlainText(msg);
        assertEquals(expected, plainText);
    }

    /**
     * 直接使用手动构造的 ArrayMsg 列表测试 arrayToPlainText,
     * 避免 stringToArray 中间步骤掩盖潜在问题
     */
    @Test
    void testArrayToPlainTextDirectly() {
        val arrayMsgs = new java.util.ArrayList<ArrayMsg>();

        // 非文本类型(如 at),不应该出现在 plain text 中
        {
            val atMsg = new ArrayMsg();
            atMsg.setType("at");
            atMsg.setText(null);
            arrayMsgs.add(atMsg);
        }

        // 第一段纯文本
        {
            val textMsg1 = new ArrayMsg();
            textMsg1.setType("text");
            textMsg1.setText("测试消息1");
            arrayMsgs.add(textMsg1);
        }

        // 表情类 CQ 码,假设不参与 plain text 输出
        {
            val faceMsg = new ArrayMsg();
            faceMsg.setType("face");
            faceMsg.setText(null);
            arrayMsgs.add(faceMsg);
        }

        // 第二段纯文本(带换行)
        {
            val textMsg2 = new ArrayMsg();
            textMsg2.setType("text");
            textMsg2.setText("测试消息2\n");
            arrayMsgs.add(textMsg2);
        }

        // 图片/视频等富媒体消息
        {
            val imageMsg = new ArrayMsg();
            imageMsg.setType("image");
            imageMsg.setText(null);
            arrayMsgs.add(imageMsg);
        }

        val expected = "测试消息1测试消息2\n";
        val plainText = MessageConverser.arrayToPlainText(arrayMsgs);
        assertEquals(expected, plainText);
    }

    @Test
    void rawToArrayMsg2Test() {

```

1. 确认 `ArrayMsg` 类的包名和 API- 如果当前测试文件尚未导入 `ArrayMsg`,需要在文件顶部添加正确的 `import`,如 `import com.mikuac.shiro.dto.event.message.ArrayMsg;`(请根据实际项目结构调整)。
   - 如果 `ArrayMsg` 不是通过无参构造 + `setType`/`setText` 方式使用,而是有构造函数或 builder,请将 `new ArrayMsg(); setType(...); setText(...);` 改成项目中实际使用的构建方式。
2. 如果 `arrayToPlainText` 的语义与这里的假设不同(例如会保留某些非 text 类型的内容,或对换行有特殊处理),请同步调整各测试中的 `expected` 字符串,以匹配实际行为。
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/main/java/com/mikuac/shiro/common/utils/MessageConverser.java
Comment thread src/main/java/com/mikuac/shiro/annotation/MessageHandlerFilter.java Outdated
@MisakaTAT
MisakaTAT merged commit 9140b8c into MisakaTAT:main Jan 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants