Skip to content

perf: Implement Pydantic data models for the KOOK adapter to enhance data retrieval and message schema validation#5719

Merged
Soulter merged 2 commits intoAstrBotDevs:masterfrom
shuiping233:refactor/add_kook_event_data_type
Mar 17, 2026
Merged

perf: Implement Pydantic data models for the KOOK adapter to enhance data retrieval and message schema validation#5719
Soulter merged 2 commits intoAstrBotDevs:masterfrom
shuiping233:refactor/add_kook_event_data_type

Conversation

@shuiping233
Copy link
Copy Markdown
Contributor

@shuiping233 shuiping233 commented Mar 3, 2026

Modifications / 改动点

  • astrbot/core/platform/sources/kook, 将大部分ws消息事件和请求接口定义成pydantic数据类,并将大量字段get方法改成更好使用的类型判断
  • tests/test_kook, pydantic的to_json行为和之前有点不同,所以要跟着修改一下测试用例
  • 修复了kook适配器处理带文件的消息时,没有正确处理(met)xx(met)格式字符串,导致机器人不响应携带了任何文件的的用户消息
  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果


Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。/ My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

由 Sourcery 提供的总结

重构 KOOK 适配器和客户端,使用强类型的 Pydantic 模型来处理 WebSocket 事件、卡片结构和 REST 响应,从而改进消息解析、校验和日志记录,并更新测试以匹配新的序列化行为。

增强内容:

  • 引入用于 KOOK 卡片模块、WebSocket 事件和 API 响应的共享 Pydantic 基类模型和枚举,为 KOOK 数据提供结构化、类型安全的访问。
  • 更新 KOOK 适配器,以消费类型化的 WebSocket 消息事件,简化自发消息过滤逻辑,并改进卡片和 kmarkdown 消息的解析和日志记录。
  • 优化 KOOK 客户端对机器人元数据、网关 URL 获取、WebSocket 信号分发以及心跳负载的处理,使其使用新的类型化模型。
  • 调整 KOOK 事件发送逻辑,使用类型化的模块常量,并在跳过项目时提供更清晰的日志消息。

测试:

  • 扩展 KOOK 测试,用于 WebSocket 事件解析/往返(round‑tripping)的样例(fixtures)和断言,并将现有预期结果调整为适配新的 Pydantic JSON 输出和类型化消息构造方式。
Original summary in English

Summary by Sourcery

Refactor the KOOK adapter and client to use strongly-typed Pydantic models for WebSocket events, card structures, and REST responses, improving message parsing, validation, and logging while updating tests to match the new serialization behavior.

Enhancements:

  • Introduce shared Pydantic base models and enums for KOOK card modules, WebSocket events, and API responses to provide structured, type-safe access to KOOK data.
  • Update the KOOK adapter to consume typed WebSocket message events, simplify self-message filtering, and improve card and kmarkdown message parsing and logging.
  • Refine the KOOK client’s handling of bot metadata, gateway URL retrieval, WebSocket signal dispatching, and heartbeat payloads using the new typed models.
  • Adjust KOOK event sending to use typed module constants and clearer log messages for skipped items.

Tests:

  • Expand KOOK tests with fixtures and assertions for WebSocket event parsing/round‑tripping and adapt existing expectations to the new Pydantic JSON output and typed message construction.
Original summary in English

由 Sourcery 提供的总结

重构 KOOK 适配器和客户端,使用强类型的 Pydantic 模型来处理 WebSocket 事件、卡片结构和 REST 响应,从而改进消息解析、校验和日志记录,并更新测试以匹配新的序列化行为。

增强内容:

  • 引入用于 KOOK 卡片模块、WebSocket 事件和 API 响应的共享 Pydantic 基类模型和枚举,为 KOOK 数据提供结构化、类型安全的访问。
  • 更新 KOOK 适配器,以消费类型化的 WebSocket 消息事件,简化自发消息过滤逻辑,并改进卡片和 kmarkdown 消息的解析和日志记录。
  • 优化 KOOK 客户端对机器人元数据、网关 URL 获取、WebSocket 信号分发以及心跳负载的处理,使其使用新的类型化模型。
  • 调整 KOOK 事件发送逻辑,使用类型化的模块常量,并在跳过项目时提供更清晰的日志消息。

测试:

  • 扩展 KOOK 测试,用于 WebSocket 事件解析/往返(round‑tripping)的样例(fixtures)和断言,并将现有预期结果调整为适配新的 Pydantic JSON 输出和类型化消息构造方式。
Original summary in English

Summary by Sourcery

Refactor the KOOK adapter and client to use strongly-typed Pydantic models for WebSocket events, card structures, and REST responses, improving message parsing, validation, and logging while updating tests to match the new serialization behavior.

Enhancements:

  • Introduce shared Pydantic base models and enums for KOOK card modules, WebSocket events, and API responses to provide structured, type-safe access to KOOK data.
  • Update the KOOK adapter to consume typed WebSocket message events, simplify self-message filtering, and improve card and kmarkdown message parsing and logging.
  • Refine the KOOK client’s handling of bot metadata, gateway URL retrieval, WebSocket signal dispatching, and heartbeat payloads using the new typed models.
  • Adjust KOOK event sending to use typed module constants and clearer log messages for skipped items.

Tests:

  • Expand KOOK tests with fixtures and assertions for WebSocket event parsing/round‑tripping and adapt existing expectations to the new Pydantic JSON output and typed message construction.
Original summary in English

由 Sourcery 提供的总结

重构 KOOK 适配器和客户端,使用强类型的 Pydantic 模型来处理 WebSocket 事件、卡片结构和 REST 响应,从而改进消息解析、校验和日志记录,并更新测试以匹配新的序列化行为。

增强内容:

  • 引入用于 KOOK 卡片模块、WebSocket 事件和 API 响应的共享 Pydantic 基类模型和枚举,为 KOOK 数据提供结构化、类型安全的访问。
  • 更新 KOOK 适配器,以消费类型化的 WebSocket 消息事件,简化自发消息过滤逻辑,并改进卡片和 kmarkdown 消息的解析和日志记录。
  • 优化 KOOK 客户端对机器人元数据、网关 URL 获取、WebSocket 信号分发以及心跳负载的处理,使其使用新的类型化模型。
  • 调整 KOOK 事件发送逻辑,使用类型化的模块常量,并在跳过项目时提供更清晰的日志消息。

测试:

  • 扩展 KOOK 测试,用于 WebSocket 事件解析/往返(round‑tripping)的样例(fixtures)和断言,并将现有预期结果调整为适配新的 Pydantic JSON 输出和类型化消息构造方式。
Original summary in English

Summary by Sourcery

Refactor the KOOK adapter and client to use strongly-typed Pydantic models for WebSocket events, card structures, and REST responses, improving message parsing, validation, and logging while updating tests to match the new serialization behavior.

Enhancements:

  • Introduce shared Pydantic base models and enums for KOOK card modules, WebSocket events, and API responses to provide structured, type-safe access to KOOK data.
  • Update the KOOK adapter to consume typed WebSocket message events, simplify self-message filtering, and improve card and kmarkdown message parsing and logging.
  • Refine the KOOK client’s handling of bot metadata, gateway URL retrieval, WebSocket signal dispatching, and heartbeat payloads using the new typed models.
  • Adjust KOOK event sending to use typed module constants and clearer log messages for skipped items.

Tests:

  • Expand KOOK tests with fixtures and assertions for WebSocket event parsing/round‑tripping and adapt existing expectations to the new Pydantic JSON output and typed message construction.
Original summary in English

由 Sourcery 提供的总结

重构 KOOK 适配器和客户端,使用强类型的 Pydantic 模型来处理 WebSocket 事件、卡片结构和 REST 响应,从而改进消息解析、校验和日志记录,并更新测试以匹配新的序列化行为。

增强内容:

  • 引入用于 KOOK 卡片模块、WebSocket 事件和 API 响应的共享 Pydantic 基类模型和枚举,为 KOOK 数据提供结构化、类型安全的访问。
  • 更新 KOOK 适配器,以消费类型化的 WebSocket 消息事件,简化自发消息过滤逻辑,并改进卡片和 kmarkdown 消息的解析和日志记录。
  • 优化 KOOK 客户端对机器人元数据、网关 URL 获取、WebSocket 信号分发以及心跳负载的处理,使其使用新的类型化模型。
  • 调整 KOOK 事件发送逻辑,使用类型化的模块常量,并在跳过项目时提供更清晰的日志消息。

测试:

  • 扩展 KOOK 测试,用于 WebSocket 事件解析/往返(round‑tripping)的样例(fixtures)和断言,并将现有预期结果调整为适配新的 Pydantic JSON 输出和类型化消息构造方式。
Original summary in English

Summary by Sourcery

Refactor the KOOK adapter and client to use strongly-typed Pydantic models for WebSocket events, card structures, and REST responses, improving message parsing, validation, and logging while updating tests to match the new serialization behavior.

Enhancements:

  • Introduce shared Pydantic base models and enums for KOOK card modules, WebSocket events, and API responses to provide structured, type-safe access to KOOK data.
  • Update the KOOK adapter to consume typed WebSocket message events, simplify self-message filtering, and improve card and kmarkdown message parsing and logging.
  • Refine the KOOK client’s handling of bot metadata, gateway URL retrieval, WebSocket signal dispatching, and heartbeat payloads using the new typed models.
  • Adjust KOOK event sending to use typed module constants and clearer log messages for skipped items.

Tests:

  • Expand KOOK tests with fixtures and assertions for WebSocket event parsing/round‑tripping and adapt existing expectations to the new Pydantic JSON output and typed message construction.

@dosubot dosubot bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Mar 3, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求对KOOK适配器进行了重大重构,通过全面引入Pydantic数据类来处理KOOK平台的WebSocket事件和API响应。这一改进旨在为适配器提供更强大的类型安全、自动数据验证和更清晰的数据结构定义,从而显著提升代码的健壮性、可维护性和开发效率。它简化了消息的解析和构造过程,使得适配器在处理复杂数据时更加可靠和易于理解。

Highlights

  • Pydantic数据类引入: KOOK适配器现在尽可能使用Pydantic数据类来定义WebSocket消息事件和请求接口,以增强数据获取的便利性和消息结构的校验。
  • 消息处理逻辑优化: 重构了消息处理方法,将大量字段的get方法替换为更易用的类型判断,并直接使用Pydantic模型进行数据访问和验证。
  • 测试用例更新: 由于Pydantic的to_json行为与之前有所不同,相应的KOOK测试用例已进行修改以适应新的序列化方式。
Changelog
  • astrbot/core/platform/sources/kook/kook_adapter.py
    • 引入了pydantic库和新的KOOK类型定义。
    • 修改了_should_ignore_event_by_bot_nickname方法,直接使用Pydantic事件对象的author_id
    • 更新了_on_received方法以接收KookMessageEvent,并增加了对系统消息的显式处理。
    • 重构了_parse_kmarkdown_text_message_parse_card_message方法,利用Pydantic模型进行消息解析和验证。
    • 新增了_handle_section_text_handle_image_group辅助方法,以简化卡片消息的解析逻辑。
    • 调整了convert_message方法,使其接受KookMessageEvent并使用Pydantic字段获取消息属性和进行类型检查。
  • astrbot/core/platform/sources/kook/kook_client.py
    • 引入了pydantic库和多个新的KOOK事件/响应类型。
    • bot_name属性更新为bot_nickname,并新增了bot_username属性。
    • 重构了get_bot_infoget_gateway_url方法,使用Pydantic模型解析和验证API响应。
    • 修改了listen方法,将传入的WebSocket消息解析为KookWebsocketEvent Pydantic对象。
    • 更新了_handle_signal_handle_hello_handle_pong_handle_reconnect_handle_resume_ack方法,以处理Pydantic事件对象。
  • astrbot/core/platform/sources/kook/kook_event.py
    • 引入了KookModuleType枚举。
    • handle_audio函数中,将FileModuletype字段更新为KookModuleType.AUDIO
  • astrbot/core/platform/sources/kook/kook_types.py
    • 移除了dataclass装饰器和field导入,全面转向Pydantic BaseModel
    • 引入了KookBaseDataClass作为所有Pydantic模型的基础类,提供了from_dictfrom_jsonto_dictto_json方法。
    • 定义了KookModuleType枚举,用于表示各种卡片模块类型。
    • 更新了现有的卡片消息元素和模块类(如PlainTextElementKmarkdownElementImageElementButtonElementHeaderModuleSectionModule等),使其继承自KookBaseDataClass,并使用Literal类型结合KookModuleType枚举值定义type字段。
    • 修改了AnyModule的定义,使用AnnotatedField(discriminator="type")以实现更好的类型判别。
    • 重构了KookCardMessage,使其继承自KookBaseDataClass,并移除了自定义的to_dict/to_json方法,转而依赖KookBaseDataClass的实现。
    • KookCardMessageContainer添加了from_dict类方法。
    • 引入了新的Pydantic模型,用于KOOK WebSocket信令(KookMessageSignal)、频道类型(KookChannelType)、作者信息(KookAuthor)、KMarkdown内容(KookKMarkdown)、额外数据(KookExtra)以及各种事件类型(KookMessageEventKookHelloEventKookResumeAckEventKookWebsocketEvent)。
    • KookWebsocketEvent中实现了model_validator,用于在解析前将signal注入到data字段中,以便Pydantic正确进行类型判别。
    • 添加了API响应的Pydantic模型:KookApiResponseBaseKookUserMeDataKookUserMeResponsekookGatewayIndexDataKookGatewayIndexResponse
  • tests/test_kook/data/kook_card_data.json
    • 调整了JSON对象内部键的顺序,使其与Pydantic的默认序列化顺序保持一致,特别是将type字段移至开头。
  • tests/test_kook/test_kook_event.py
    • 更新了OrderMessage的实例化方式,使用关键字参数(index=1)而非位置参数。
    • 修改了音频卡片消息的预期JSON字符串,以反映新的Pydantic序列化顺序。
  • tests/test_kook/test_kook_types.py
    • 引入了KookModuleType枚举。
    • test_all_kook_card_type中,更新了FileModule的实例化方式,使用KookModuleType.FILE
Activity
  • 拉取请求由shuiping233创建。
  • 作者提供了详细的更改描述,包括动机和修改点。
  • 作者确认此更改不是破坏性变更。
  • 作者已完成检查清单,表明已进行讨论、测试,未引入新依赖,且未包含恶意代码。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@dosubot
Copy link
Copy Markdown

dosubot bot commented Mar 3, 2026

Related Documentation

Checked 1 published document(s) in 1 knowledge base(s). No updates required.

How did I do? Any feedback?  Join Discord

@dosubot dosubot bot added the area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. label Mar 3, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

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

  • _parse_card_message 中,当 KookCardMessageContainer.from_dict 抛出 ValidationError 时,你目前只是记录了错误然后继续执行,这会导致 card_list 未定义并在运行时出错;建议在记录日志后立刻返回一个安全的兜底值(例如 return [], "")。
  • KookCardMessageContainer.from_dict 当前返回的是一个裸的 list[KookCardMessage],而不是 KookCardMessageContainer 实例,这对于一个定义在该类型上的 classmethod 来说有些出乎意料;建议返回 cls(card_list),以便调用方始终获得容器类型。
  • kookGatewayIndexData 的命名(首字母小写)与其他 Pydantic 模型(如 KookUserMeData 等)不一致;建议重命名为 KookGatewayIndexData 以保持清晰和一致性。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_parse_card_message`, when `KookCardMessageContainer.from_dict` raises a `ValidationError` you only log the error and continue, which will leave `card_list` undefined and lead to a runtime error; consider returning a safe fallback (e.g., `return [], ""`) immediately after logging.
- `KookCardMessageContainer.from_dict` currently returns a bare `list[KookCardMessage]` rather than an instance of `KookCardMessageContainer`, which is surprising for a classmethod on that type; consider returning `cls(card_list)` so callers consistently get the container type.
- The naming of `kookGatewayIndexData` (lowercase leading letter) is inconsistent with the rest of the Pydantic models (`KookUserMeData`, etc.); consider renaming it to `KookGatewayIndexData` for clarity and consistency.

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/kook/kook_adapter.py" line_range="276-277" />
<code_context>
+        try:
+            card_list = KookCardMessageContainer.from_dict(json.loads(content))
+
+        except pydantic.ValidationError as e:
+            logger.error(f"[KOOK] 解析卡片消息错误: {e}")
+            logger.error(f"[KOOK] 原始消息内容: {data.to_json()}")

</code_context>
<issue_to_address>
**issue (bug_risk):** Catching pydantic.ValidationError here will currently raise NameError and also leaves card_list undefined.

Consider importing `pydantic` in this module and returning early from `_parse_card_message` on validation failure (e.g. `return [], ""`) so later code doesn’t iterate over an undefined `card_list`. Alternatively, ensure the exception handling path always initializes `card_list` before it is used.
</issue_to_address>

### Comment 2
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="253-254" />
<code_context>
-        return json.dumps(self.to_dict(), indent=indent, ensure_ascii=ensure_ascii)
-

 class KookCardMessageContainer(list[KookCardMessage]):
     """卡片消息容器(列表),此类型可以直接to_json后发送出去"""
</code_context>
<issue_to_address>
**suggestion (bug_risk):** from_dict on KookCardMessageContainer returns a plain list instead of an instance of cls, which can be surprising.

`from_dict` currently returns a plain `list[KookCardMessage]` (`[KookCardMessage.from_dict(item) for item in raw_data]`), which conflicts with being defined on `KookCardMessageContainer` and with the type hint/`to_json` behavior. This inconsistency can cause subtle type/behavior issues. Consider returning a `KookCardMessageContainer` instance instead, e.g. `cls(KookCardMessage.from_dict(item) for item in raw_data)`.
</issue_to_address>

### Comment 3
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="220" />
<code_context>

 # 所有模块的联合类型
-AnyModule = (
+AnyModule = Annotated[
     HeaderModule
     | SectionModule
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new KOOK models by removing unnecessary discriminated unions and base-class abstractions while keeping the same typing and behavior.

You can reduce the new abstraction/indirection without losing any of the new functionality.

### 1. Drop discriminated `AnyModule` while keeping enums/literals

You already have `KookModuleType` and per‑module `type: Literal[... ]`, so you don’t actually need a discriminated union here. Pydantic will still serialize/deserialize correctly as long as the field names/types match KOOK’s payloads.

Current:

```python
AnyModule = Annotated[
    HeaderModule
    | SectionModule
    | ImageGroupModule
    | ContainerModule
    | ActionGroupModule
    | ContextModule
    | DividerModule
    | FileModule
    | CountdownModule
    | InviteModule,
    Field(discriminator="type"),
]
```

This can be simplified to:

```python
AnyModule = (
    HeaderModule
    | SectionModule
    | ImageGroupModule
    | ContainerModule
    | ActionGroupModule
    | ContextModule
    | DividerModule
    | FileModule
    | CountdownModule
    | InviteModule
)
```

`KookCardMessage.modules: list[AnyModule]` will still validate/serialize fine because each concrete module has a fixed `type` literal; the only thing you lose is automatic discriminated union resolution, which you’re not relying on anywhere else.

### 2. Simplify `KookWebsocketEvent` by moving parsing to the client

The discriminated union + pre‑validator on `KookWebsocketEvent` introduces quite a bit of protocol‑specific cleverness into the type layer:

```python
class KookWebsocketEvent(KookBaseDataClass):
    signal: KookMessageSignal = Field(..., alias="s")
    data: Annotated[
        KookMessageEvent | KookHelloEvent | KookResumeAckEvent | None,
        Field(discriminator="signal"),
    ] = Field(None, alias="d")

    @model_validator(mode="before")
    @classmethod
    def _inject_signal_into_data(cls, data: Any) -> Any:
        ...
```

Since you still match on `event.signal` in the client, you can simplify `KookWebsocketEvent` to treat `data` as an untyped payload and do the per‑signal parsing where you already branch:

```python
class KookWebsocketEvent(KookBaseDataClass):
    signal: KookMessageSignal = Field(..., alias="s")
    d: dict[str, Any] | None = Field(None, alias="d")
    sn: int | None = None
```

Then, in your client code:

```python
async def _handle_signal(self, raw_event: dict[str, Any]) -> None:
    event = KookWebsocketEvent.from_dict(raw_event)

    match event.signal:
        case KookMessageSignal.MESSAGE:
            msg = KookMessageEvent.from_dict(event.d or {})
            # handle message
        case KookMessageSignal.HELLO:
            hello = KookHelloEvent.from_dict(event.d or {})
            # handle hello
        case KookMessageSignal.RESUME_ACK:
            resume = KookResumeAckEvent.from_dict(event.d or {})
            # handle resume-ack
        # ...
```

You retain the same strong typing for `KookMessageEvent`, `KookHelloEvent`, `KookResumeAckEvent`, but the WebSocket envelope becomes a straightforward representation of the raw protocol, without discriminator plumbing or mutation in a pre‑validator.

### 3. Narrow the use of `KookBaseDataClass` to where it adds value

Right now `KookBaseDataClass` mostly renames `model_validate`/`model_dump`/`model_dump_json` and turns on `extra="allow"` globally:

```python
class KookBaseDataClass(BaseModel):
    model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)

    @classmethod
    def from_dict(cls, raw_data: dict):
        return cls.model_validate(raw_data)

    @classmethod
    def from_json(cls, raw_data: str | bytes | bytearray):
        return cls.model_validate_json(raw_data)

    def to_dict(self, by_alias=False, exclude_none=True) -> dict:
        return self.model_dump(by_alias=by_alias, exclude_none=exclude_none)

    def to_json(...):
        return self.model_dump_json(...)
```

If you want to keep the friendlier `from_*`/`to_*` names (they’re convenient for HTTP/WS payloads), consider:

* Using `KookBaseDataClass` only for **API/WS payload models** (`KookMessageEvent`, `KookUserMeResponse`, etc.).
* Letting card models (`KookCardMessage`, `KookCardModelBase` and friends) inherit directly from `BaseModel` since they already have simple usage and don’t need `extra="allow"`:

```python
class KookCardModelBase(BaseModel):
    type: str

class KookCardMessage(BaseModel):
    type: Literal[KookModuleType.CARD] = KookModuleType.CARD
    # ...
```

This reduces one layer of inheritance/indirection in the hot path for card handling, while keeping the convenience helpers where they actually simplify call‑sites (HTTP and WebSocket parsing).
</issue_to_address>

### Comment 4
<location path="astrbot/core/platform/sources/kook/kook_adapter.py" line_range="268" />
<code_context>

-    def _parse_card_message(self, data: dict) -> tuple[list, str]:
-        content = data.get("content", "[]")
+    def _parse_card_message(self, data: KookMessageEvent) -> tuple[list, str]:
+        content = data.content
         if not isinstance(content, str):
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying `_parse_card_message` to a single linear traversal with a generic image-extraction helper so the hot path depends only on a few core fields instead of many module-specific helpers and pattern matches.

You can keep the typed models and still reduce complexity/coupling a bit in the hot path with small, local changes.

### 1. Make `_parse_card_message` robust and more linear

Right now, if `KookCardMessageContainer.from_dict` fails, `card_list` is undefined and the function continues. Also, the pattern-matching + helpers cause a lot of jumping for a very narrow extraction (text + image URLs).

You can keep the typed models but make the traversal more linear and self‑contained:

```python
def _parse_card_message(self, data: KookMessageEvent) -> tuple[list, str]:
    content = data.content
    if not isinstance(content, str):
        content = str(content)

    try:
        card_list = KookCardMessageContainer.from_dict(json.loads(content))
    except pydantic.ValidationError as e:
        logger.error(f"[KOOK] 解析卡片消息错误: {e}")
        logger.error(f"[KOOK] 原始消息内容: {data.to_json()}")
        # 明确失败返回,避免 card_list 未定义
        return [Plain(text="[卡片消息解析失败]")], "[卡片消息解析失败]"

    text_parts: list[str] = []
    images: list[str] = []

    for card in card_list:
        for module in card.modules:
            module_type = module.type

            if module_type == "section":
                # inline 原来的 _handle_section_text
                text = getattr(module, "text", None)
                if isinstance(text, (KmarkdownElement, PlainTextElement)):
                    if text.content:
                        text_parts.append(text.content)

            elif module_type in ("container", "image-group"):
                # 复用一个简洁的图片提取 helper(见下)
                images.extend(self._extract_image_urls(module.elements))

            elif module_type == "header":
                header_text = getattr(getattr(module, "text", None), "content", None)
                if header_text:
                    text_parts.append(header_text)
            else:
                logger.debug(f"[KOOK] 跳过或未处理模块: {module_type}")

    text = "".join(text_parts)
    message: list = []
    if text:
        message.append(Plain(text=text))
    for img_url in images:
        message.append(Image(file=img_url))
    return message, text
```

### 2. Replace two module‑specific helpers with one focused image extractor

Instead of `_handle_section_text` and `_handle_image_group(module: ContainerModule | ImageGroupModule)`, you can keep a single helper that only depends on the *elements*, not on the specific module classes. This keeps the adapter less tightly coupled to the full card schema:

```python
def _extract_image_urls(self, elements) -> list[str]:
    """从 elements 中提取合法图片 URL,不关心具体 module 类型。"""
    valid_urls: list[str] = []
    for el in elements or []:
        image_src = getattr(el, "src", None)
        if not isinstance(image_src, str):
            logger.warning(
                f'[KOOK] 处理卡片中的图片时发生错误,图片url "{image_src}" 应该为str类型, 而不是 "{type(image_src)}" '
            )
            continue
        if not image_src.startswith(("http://", "https://")):
            logger.warning(f"[KOOK] 屏蔽非http图片url: {image_src}")
            continue
        valid_urls.append(image_src)
    return valid_urls
```

This way:

- The hot path only cares about `module.type`, `module.text.content`, and `elements[].src`, similar to the original dict-based version.
- You still benefit from typed models and validation, but the adapter logic is less tightly coupled to every specific module class.
- The number of helper methods shrinks and the control flow becomes easier to read without losing functionality.
</issue_to_address>

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

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

  • In _parse_card_message, when KookCardMessageContainer.from_dict raises a ValidationError you only log the error and continue, which will leave card_list undefined and lead to a runtime error; consider returning a safe fallback (e.g., return [], "") immediately after logging.
  • KookCardMessageContainer.from_dict currently returns a bare list[KookCardMessage] rather than an instance of KookCardMessageContainer, which is surprising for a classmethod on that type; consider returning cls(card_list) so callers consistently get the container type.
  • The naming of kookGatewayIndexData (lowercase leading letter) is inconsistent with the rest of the Pydantic models (KookUserMeData, etc.); consider renaming it to KookGatewayIndexData for clarity and consistency.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_parse_card_message`, when `KookCardMessageContainer.from_dict` raises a `ValidationError` you only log the error and continue, which will leave `card_list` undefined and lead to a runtime error; consider returning a safe fallback (e.g., `return [], ""`) immediately after logging.
- `KookCardMessageContainer.from_dict` currently returns a bare `list[KookCardMessage]` rather than an instance of `KookCardMessageContainer`, which is surprising for a classmethod on that type; consider returning `cls(card_list)` so callers consistently get the container type.
- The naming of `kookGatewayIndexData` (lowercase leading letter) is inconsistent with the rest of the Pydantic models (`KookUserMeData`, etc.); consider renaming it to `KookGatewayIndexData` for clarity and consistency.

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/kook/kook_adapter.py" line_range="276-277" />
<code_context>
+        try:
+            card_list = KookCardMessageContainer.from_dict(json.loads(content))
+
+        except pydantic.ValidationError as e:
+            logger.error(f"[KOOK] 解析卡片消息错误: {e}")
+            logger.error(f"[KOOK] 原始消息内容: {data.to_json()}")

</code_context>
<issue_to_address>
**issue (bug_risk):** Catching pydantic.ValidationError here will currently raise NameError and also leaves card_list undefined.

Consider importing `pydantic` in this module and returning early from `_parse_card_message` on validation failure (e.g. `return [], ""`) so later code doesn’t iterate over an undefined `card_list`. Alternatively, ensure the exception handling path always initializes `card_list` before it is used.
</issue_to_address>

### Comment 2
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="253-254" />
<code_context>
-        return json.dumps(self.to_dict(), indent=indent, ensure_ascii=ensure_ascii)
-

 class KookCardMessageContainer(list[KookCardMessage]):
     """卡片消息容器(列表),此类型可以直接to_json后发送出去"""
</code_context>
<issue_to_address>
**suggestion (bug_risk):** from_dict on KookCardMessageContainer returns a plain list instead of an instance of cls, which can be surprising.

`from_dict` currently returns a plain `list[KookCardMessage]` (`[KookCardMessage.from_dict(item) for item in raw_data]`), which conflicts with being defined on `KookCardMessageContainer` and with the type hint/`to_json` behavior. This inconsistency can cause subtle type/behavior issues. Consider returning a `KookCardMessageContainer` instance instead, e.g. `cls(KookCardMessage.from_dict(item) for item in raw_data)`.
</issue_to_address>

### Comment 3
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="220" />
<code_context>

 # 所有模块的联合类型
-AnyModule = (
+AnyModule = Annotated[
     HeaderModule
     | SectionModule
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new KOOK models by removing unnecessary discriminated unions and base-class abstractions while keeping the same typing and behavior.

You can reduce the new abstraction/indirection without losing any of the new functionality.

### 1. Drop discriminated `AnyModule` while keeping enums/literals

You already have `KookModuleType` and per‑module `type: Literal[... ]`, so you don’t actually need a discriminated union here. Pydantic will still serialize/deserialize correctly as long as the field names/types match KOOK’s payloads.

Current:

```python
AnyModule = Annotated[
    HeaderModule
    | SectionModule
    | ImageGroupModule
    | ContainerModule
    | ActionGroupModule
    | ContextModule
    | DividerModule
    | FileModule
    | CountdownModule
    | InviteModule,
    Field(discriminator="type"),
]
```

This can be simplified to:

```python
AnyModule = (
    HeaderModule
    | SectionModule
    | ImageGroupModule
    | ContainerModule
    | ActionGroupModule
    | ContextModule
    | DividerModule
    | FileModule
    | CountdownModule
    | InviteModule
)
```

`KookCardMessage.modules: list[AnyModule]` will still validate/serialize fine because each concrete module has a fixed `type` literal; the only thing you lose is automatic discriminated union resolution, which you’re not relying on anywhere else.

### 2. Simplify `KookWebsocketEvent` by moving parsing to the client

The discriminated union + pre‑validator on `KookWebsocketEvent` introduces quite a bit of protocol‑specific cleverness into the type layer:

```python
class KookWebsocketEvent(KookBaseDataClass):
    signal: KookMessageSignal = Field(..., alias="s")
    data: Annotated[
        KookMessageEvent | KookHelloEvent | KookResumeAckEvent | None,
        Field(discriminator="signal"),
    ] = Field(None, alias="d")

    @model_validator(mode="before")
    @classmethod
    def _inject_signal_into_data(cls, data: Any) -> Any:
        ...
```

Since you still match on `event.signal` in the client, you can simplify `KookWebsocketEvent` to treat `data` as an untyped payload and do the per‑signal parsing where you already branch:

```python
class KookWebsocketEvent(KookBaseDataClass):
    signal: KookMessageSignal = Field(..., alias="s")
    d: dict[str, Any] | None = Field(None, alias="d")
    sn: int | None = None
```

Then, in your client code:

```python
async def _handle_signal(self, raw_event: dict[str, Any]) -> None:
    event = KookWebsocketEvent.from_dict(raw_event)

    match event.signal:
        case KookMessageSignal.MESSAGE:
            msg = KookMessageEvent.from_dict(event.d or {})
            # handle message
        case KookMessageSignal.HELLO:
            hello = KookHelloEvent.from_dict(event.d or {})
            # handle hello
        case KookMessageSignal.RESUME_ACK:
            resume = KookResumeAckEvent.from_dict(event.d or {})
            # handle resume-ack
        # ...
```

You retain the same strong typing for `KookMessageEvent`, `KookHelloEvent`, `KookResumeAckEvent`, but the WebSocket envelope becomes a straightforward representation of the raw protocol, without discriminator plumbing or mutation in a pre‑validator.

### 3. Narrow the use of `KookBaseDataClass` to where it adds value

Right now `KookBaseDataClass` mostly renames `model_validate`/`model_dump`/`model_dump_json` and turns on `extra="allow"` globally:

```python
class KookBaseDataClass(BaseModel):
    model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)

    @classmethod
    def from_dict(cls, raw_data: dict):
        return cls.model_validate(raw_data)

    @classmethod
    def from_json(cls, raw_data: str | bytes | bytearray):
        return cls.model_validate_json(raw_data)

    def to_dict(self, by_alias=False, exclude_none=True) -> dict:
        return self.model_dump(by_alias=by_alias, exclude_none=exclude_none)

    def to_json(...):
        return self.model_dump_json(...)
```

If you want to keep the friendlier `from_*`/`to_*` names (they’re convenient for HTTP/WS payloads), consider:

* Using `KookBaseDataClass` only for **API/WS payload models** (`KookMessageEvent`, `KookUserMeResponse`, etc.).
* Letting card models (`KookCardMessage`, `KookCardModelBase` and friends) inherit directly from `BaseModel` since they already have simple usage and don’t need `extra="allow"`:

```python
class KookCardModelBase(BaseModel):
    type: str

class KookCardMessage(BaseModel):
    type: Literal[KookModuleType.CARD] = KookModuleType.CARD
    # ...
```

This reduces one layer of inheritance/indirection in the hot path for card handling, while keeping the convenience helpers where they actually simplify call‑sites (HTTP and WebSocket parsing).
</issue_to_address>

### Comment 4
<location path="astrbot/core/platform/sources/kook/kook_adapter.py" line_range="268" />
<code_context>

-    def _parse_card_message(self, data: dict) -> tuple[list, str]:
-        content = data.get("content", "[]")
+    def _parse_card_message(self, data: KookMessageEvent) -> tuple[list, str]:
+        content = data.content
         if not isinstance(content, str):
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying `_parse_card_message` to a single linear traversal with a generic image-extraction helper so the hot path depends only on a few core fields instead of many module-specific helpers and pattern matches.

You can keep the typed models and still reduce complexity/coupling a bit in the hot path with small, local changes.

### 1. Make `_parse_card_message` robust and more linear

Right now, if `KookCardMessageContainer.from_dict` fails, `card_list` is undefined and the function continues. Also, the pattern-matching + helpers cause a lot of jumping for a very narrow extraction (text + image URLs).

You can keep the typed models but make the traversal more linear and self‑contained:

```python
def _parse_card_message(self, data: KookMessageEvent) -> tuple[list, str]:
    content = data.content
    if not isinstance(content, str):
        content = str(content)

    try:
        card_list = KookCardMessageContainer.from_dict(json.loads(content))
    except pydantic.ValidationError as e:
        logger.error(f"[KOOK] 解析卡片消息错误: {e}")
        logger.error(f"[KOOK] 原始消息内容: {data.to_json()}")
        # 明确失败返回,避免 card_list 未定义
        return [Plain(text="[卡片消息解析失败]")], "[卡片消息解析失败]"

    text_parts: list[str] = []
    images: list[str] = []

    for card in card_list:
        for module in card.modules:
            module_type = module.type

            if module_type == "section":
                # inline 原来的 _handle_section_text
                text = getattr(module, "text", None)
                if isinstance(text, (KmarkdownElement, PlainTextElement)):
                    if text.content:
                        text_parts.append(text.content)

            elif module_type in ("container", "image-group"):
                # 复用一个简洁的图片提取 helper(见下)
                images.extend(self._extract_image_urls(module.elements))

            elif module_type == "header":
                header_text = getattr(getattr(module, "text", None), "content", None)
                if header_text:
                    text_parts.append(header_text)
            else:
                logger.debug(f"[KOOK] 跳过或未处理模块: {module_type}")

    text = "".join(text_parts)
    message: list = []
    if text:
        message.append(Plain(text=text))
    for img_url in images:
        message.append(Image(file=img_url))
    return message, text
```

### 2. Replace two module‑specific helpers with one focused image extractor

Instead of `_handle_section_text` and `_handle_image_group(module: ContainerModule | ImageGroupModule)`, you can keep a single helper that only depends on the *elements*, not on the specific module classes. This keeps the adapter less tightly coupled to the full card schema:

```python
def _extract_image_urls(self, elements) -> list[str]:
    """从 elements 中提取合法图片 URL,不关心具体 module 类型。"""
    valid_urls: list[str] = []
    for el in elements or []:
        image_src = getattr(el, "src", None)
        if not isinstance(image_src, str):
            logger.warning(
                f'[KOOK] 处理卡片中的图片时发生错误,图片url "{image_src}" 应该为str类型, 而不是 "{type(image_src)}" '
            )
            continue
        if not image_src.startswith(("http://", "https://")):
            logger.warning(f"[KOOK] 屏蔽非http图片url: {image_src}")
            continue
        valid_urls.append(image_src)
    return valid_urls
```

This way:

- The hot path only cares about `module.type`, `module.text.content`, and `elements[].src`, similar to the original dict-based version.
- You still benefit from typed models and validation, but the adapter logic is less tightly coupled to every specific module class.
- The number of helper methods shrinks and the control flow becomes easier to read without losing functionality.
</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 astrbot/core/platform/sources/kook/kook_adapter.py Outdated
Comment thread astrbot/core/platform/sources/kook/kook_types.py
Comment thread astrbot/core/platform/sources/kook/kook_types.py
Comment thread astrbot/core/platform/sources/kook/kook_adapter.py Outdated
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

这次重构做得非常出色,通过引入 Pydantic 数据类,极大地提升了 KOOK 适配器的代码质量、可读性和健壮性。对 WebSocket 事件和 API 响应使用强类型模型,特别是利用 Pydantic 的可区分联合(Discriminated Unions)来处理不同类型的事件和卡片模块,是非常好的实践。这使得数据校验和访问更加安全和直观。代码整体结构更清晰,也更容易维护和扩展。我在代码中发现了一些可以进一步优化的小地方,请查看具体的审查评论。

Comment thread astrbot/core/platform/sources/kook/kook_adapter.py Outdated
Comment thread astrbot/core/platform/sources/kook/kook_client.py Outdated
Comment thread astrbot/core/platform/sources/kook/kook_types.py Outdated
@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Mar 4, 2026
@shuiping233 shuiping233 force-pushed the refactor/add_kook_event_data_type branch from 2eae5a7 to e803d27 Compare March 4, 2026 03:36
@shuiping233
Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@shuiping233
Copy link
Copy Markdown
Contributor Author

@gemini-code-assist /gemini review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

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

  • _send_ping 中,你使用 s=KookMessageSignal.PING 来构造 KookWebsocketEvent;为避免依赖基于别名的初始化行为,建议改为传入 signal=KookMessageSignal.PING,这样可以让代码在 Pydantic 配置变化时更加稳健。
  • get_bot_infoget_gateway_urlpydantic.ValidationError 分支里,你在上下文管理器内部已经调用了 await resp.json() 消费了响应体,又在之后调用 await resp.text();这可能会失败或返回空数据。更安全的做法是先捕获一次原始 body,然后同时用于解析和错误日志。
供 AI Agent 使用的提示词
Please address the comments from this code review:

## Overall Comments
-`_send_ping` 中,你使用 `s=KookMessageSignal.PING` 来构造 `KookWebsocketEvent`;为避免依赖基于别名的初始化行为,建议改为传入 `signal=KookMessageSignal.PING`,这样可以让代码在 Pydantic 配置变化时更加稳健。
-`get_bot_info``get_gateway_url``pydantic.ValidationError` 分支里,你在上下文管理器内部已经调用了 `await resp.json()` 消费了响应体,又在之后调用 `await resp.text()`;这可能会失败或返回空数据。更安全的做法是先捕获一次原始 body,然后同时用于解析和错误日志。

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="97-98" />
<code_context>
+                logger.info(f"[KOOK] 获取机器人名称成功: {self._bot_nickname}")

                 return bot_id
+        except pydantic.ValidationError as e:
+            logger.error(f"[KOOK] 获取机器人账号信息失败, 响应数据格式错误: \n{e}")
+            logger.error(f"[KOOK] 响应内容: {await resp.text()}")
+
</code_context>
<issue_to_address>
**issue (bug_risk):**`ValidationError` 处理器中使用 `resp` 可能会抛出 `UnboundLocalError`,并且可能会尝试从已关闭的响应中读取数据。

在 `except pydantic.ValidationError` 块中你调用了 `await resp.text()`,但 `resp` 是在上面的 `async with` 块中创建的。如果在 `from_dict(await resp.json())` 之前或执行过程中发生了校验失败,`async with` 可能已经退出,此时 `resp` 要么根本没有绑定,要么已经被关闭,从而导致 `UnboundLocalError`,或者尝试从已关闭的流中读取数据。

更安全的模式是:在 `async with` 块内部读取并保存响应 body,然后再进行校验,这样原始文本在 `except` 处理中仍然可用,例如:

```python
raw_text = await resp.text()
try:
    resp_content = KookUserMeResponse.from_json(raw_text)
except pydantic.ValidationError as e:
    logger.error(...)
    logger.error(f"[KOOK] 响应内容: {raw_text}")
    return ""
```

同时应完全避免在 `async with` 作用域之外使用 `resp`。
</issue_to_address>

### Comment 2
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="337-342" />
<code_context>
         try:
-            ping_data = {"s": 2, "sn": self.last_sn}
-            await self.ws.send(json.dumps(ping_data))  # type: ignore
+            ping_data = KookWebsocketEvent(
+                s=KookMessageSignal.PING,
+                d=None,
+                sn=self.last_sn,
+            )
+            await self.ws.send(ping_data.to_json())
         except Exception as e:
             logger.error(f"[KOOK] 发送心跳失败: {e}")
</code_context>
<issue_to_address>
**issue (bug_risk):** `KookWebsocketEvent` 构造函数是用 `s=` 调用的,而不是使用模型字段名,这很可能导致 `TypeError`。

在 `KookWebsocketEvent` 中,该字段定义如下:

```python
signal: KookMessageSignal = Field(..., alias="s")
```

在 Pydantic v2 中,构造模型时必须使用字段名(`signal`),而不是别名(`s`)。使用 `s=` 会抛出 `TypeError: __init__() got an unexpected keyword argument 's'`。

应将构造更新为:

```python
ping_data = KookWebsocketEvent(
    signal=KookMessageSignal.PING,
    data=None,
    sn=self.last_sn,
)
```

在调用 `to_json()` 时,别名仍然会被正确应用。
</issue_to_address>

### Comment 3
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="190-194" />
<code_context>
             while self.running:
                 try:
-                    msg = await asyncio.wait_for(self.ws.recv(), timeout=10)  # type: ignore
+                    if self.ws is None:
+                        logger.warning(
+                            "[KOOK] 尚未连接kook WebSocket服务器, 跳过消息监听流程"
+                        )
+                        continue
+                    msg = await asyncio.wait_for(self.ws.recv(), timeout=10)

</code_context>
<issue_to_address>
**issue (performance):**`self.ws``None` 时,监听循环会在没有任何延迟的情况下持续运行,导致频繁日志输出和不必要的 CPU 占用。

在这个分支中,`self.ws is None` 时会记录一条 warning 然后立即 `continue`,而此时 `running` 仍为 `True`,从而形成一个忙等循环,疯狂刷日志并浪费 CPU。可以在 `continue` 之前添加一个短暂的 `await asyncio.sleep(...)`,或者在这里直接跳出循环,让重连机制来处理。
</issue_to_address>

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

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

  • In _send_ping, you construct KookWebsocketEvent with s=KookMessageSignal.PING; to avoid relying on alias-based initialization behavior, consider passing signal=KookMessageSignal.PING instead so the code is robust against Pydantic config changes.
  • In the pydantic.ValidationError branches of get_bot_info and get_gateway_url, you call await resp.text() after having already consumed await resp.json() inside the context manager; this can fail or return empty data, so it would be safer to capture the raw body once and reuse it for both parsing and error logging.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_send_ping`, you construct `KookWebsocketEvent` with `s=KookMessageSignal.PING`; to avoid relying on alias-based initialization behavior, consider passing `signal=KookMessageSignal.PING` instead so the code is robust against Pydantic config changes.
- In the `pydantic.ValidationError` branches of `get_bot_info` and `get_gateway_url`, you call `await resp.text()` after having already consumed `await resp.json()` inside the context manager; this can fail or return empty data, so it would be safer to capture the raw body once and reuse it for both parsing and error logging.

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="97-98" />
<code_context>
+                logger.info(f"[KOOK] 获取机器人名称成功: {self._bot_nickname}")

                 return bot_id
+        except pydantic.ValidationError as e:
+            logger.error(f"[KOOK] 获取机器人账号信息失败, 响应数据格式错误: \n{e}")
+            logger.error(f"[KOOK] 响应内容: {await resp.text()}")
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `resp` inside the `ValidationError` handler can raise an `UnboundLocalError` and may try to read from a closed response.

In the `except pydantic.ValidationError` block you call `await resp.text()`, but `resp` is created inside the `async with` block above. If validation fails before or during `from_dict(await resp.json())`, the `async with` may already have exited and `resp` may either not be bound at all or already be closed, causing an `UnboundLocalError` or attempts to read from a closed stream.

A safer pattern is to read and store the response body inside the `async with` block and then validate it, so the raw text is still available in the `except` handler, for example:

```python
raw_text = await resp.text()
try:
    resp_content = KookUserMeResponse.from_json(raw_text)
except pydantic.ValidationError as e:
    logger.error(...)
    logger.error(f"[KOOK] 响应内容: {raw_text}")
    return ""
```

Also avoid using `resp` outside the `async with` scope entirely.
</issue_to_address>

### Comment 2
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="337-342" />
<code_context>
         try:
-            ping_data = {"s": 2, "sn": self.last_sn}
-            await self.ws.send(json.dumps(ping_data))  # type: ignore
+            ping_data = KookWebsocketEvent(
+                s=KookMessageSignal.PING,
+                d=None,
+                sn=self.last_sn,
+            )
+            await self.ws.send(ping_data.to_json())
         except Exception as e:
             logger.error(f"[KOOK] 发送心跳失败: {e}")
</code_context>
<issue_to_address>
**issue (bug_risk):** The `KookWebsocketEvent` constructor is called with `s=` instead of the model field name, which is likely to raise a `TypeError`.

In `KookWebsocketEvent`, the field is defined as:

```python
signal: KookMessageSignal = Field(..., alias="s")
```

With Pydantic v2 you must use the field name (`signal`), not the alias (`s`), when constructing the model. Using `s=` will raise `TypeError: __init__() got an unexpected keyword argument 's'`.

Update the construction to:

```python
ping_data = KookWebsocketEvent(
    signal=KookMessageSignal.PING,
    data=None,
    sn=self.last_sn,
)
```

The alias will still be applied when calling `to_json()`.
</issue_to_address>

### Comment 3
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="190-194" />
<code_context>
             while self.running:
                 try:
-                    msg = await asyncio.wait_for(self.ws.recv(), timeout=10)  # type: ignore
+                    if self.ws is None:
+                        logger.warning(
+                            "[KOOK] 尚未连接kook WebSocket服务器, 跳过消息监听流程"
+                        )
+                        continue
+                    msg = await asyncio.wait_for(self.ws.recv(), timeout=10)

</code_context>
<issue_to_address>
**issue (performance):** When `self.ws` is `None`, the listen loop continues without any delay, causing a tight log loop and unnecessary CPU usage.

In this branch, `self.ws is None` logs a warning and immediately `continue`s while `running` stays `True`, creating a busy loop that spams logs and wastes CPU. Add a short `await asyncio.sleep(...)` before continuing, or exit the loop here and let a reconnect mechanism handle it.
</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 astrbot/core/platform/sources/kook/kook_client.py Outdated
Comment thread astrbot/core/platform/sources/kook/kook_client.py
Comment thread astrbot/core/platform/sources/kook/kook_client.py Outdated
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

本次 PR 对 KOOK 适配器进行了大规模的重构,全面采用 Pydantic 数据类来处理 WebSocket 事件和 API 响应。这是一个非常出色的改进,极大地增强了代码的健壮性、可读性和可维护性。通过使用 Pydantic 的类型校验和序列化功能,代码变得更加清晰和可靠。

主要改动包括:

  • 将所有 KOOK 的数据结构(如消息事件、卡片消息、API 响应等)都定义为 Pydantic 模型。
  • 利用 Pydantic 的可辨识联合(discriminated unions)来优雅地处理不同类型的 WebSocket 事件。
  • 简化了消息处理逻辑,直接访问模型属性而不是使用 get() 方法,并增加了更健壮的错误处理。
  • 相应地更新了测试用例,并增加了新的测试数据来覆盖新的数据模型。

整体来看,这是一次高质量的重构。我只发现了一些可以微调的小细节,已在代码中提出。感谢你的贡献!

@shuiping233
Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@shuiping233
Copy link
Copy Markdown
Contributor Author

@gemini-code-assist /gemini review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 3 个问题,并给出了一些总体反馈:

  • 忽略机器人自身发送消息的逻辑,从对比配置中的 bot_nickname 改成了对比 author_idself.client.bot_id;如果你仍然需要或依赖基于昵称的过滤逻辑,建议要么保留之前的行为,要么在配置里明确废弃该选项,以避免出现静默的行为变更。
  • 若干 BaseModel 字段使用了可变默认值,比如 roles: list[int] = []mention: list[str] = [] 等;为了清晰起见并遵循 pydantic 的最佳实践,建议改用 Field(default_factory=list)(例如 KookAuthorKookKMarkdownKookExtraKookUserMeData 中的相关字段)。
给 AI Agent 的提示词
Please address the comments from this code review:

## Overall Comments
- The logic for ignoring self-sent messages has changed from comparing `bot_nickname` in config to comparing `author_id` with `self.client.bot_id`; if nickname-based filtering is still desired or relied upon, consider either preserving that behavior or explicitly deprecating the config option to avoid silent behavior changes.
- Several `BaseModel` fields use mutable defaults like `roles: list[int] = []`, `mention: list[str] = []`, etc.; for clarity and to follow pydantic best practices, consider switching these to `Field(default_factory=list)` (e.g. in `KookAuthor`, `KookKMarkdown`, `KookExtra`, `KookUserMeData`).

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="319-328" />
<code_context>
+    BROADCAST = "BROADCAST"
+
+
+class KookAuthor(KookBaseDataClass):
+    id: str
+    username: str
+    identify_num: str
+    nickname: str
+    bot: bool
+    online: bool
+    avatar: str | None = None
+    vip_avatar: str | None = None
+    status: int
+    roles: list[int] = []
+
+
</code_context>
<issue_to_address>
**suggestion:** Using mutable list literals as defaults in Pydantic models can be brittle; `Field(default_factory=list)` is safer.

For the fields using `[]` (e.g. `KookAuthor.roles`, `KookKMarkdown.mention_part`, `KookExtra.mention`), switch to `Field(default_factory=list)`. This avoids relying on Pydantic’s current deep-copy behavior and keeps the models safer and more portable (e.g. if later converted to dataclasses or used outside Pydantic).

Suggested implementation:

```python
class KookAuthor(KookBaseDataClass):
    id: str
    username: str
    identify_num: str
    nickname: str
    bot: bool
    online: bool
    avatar: str | None = None
    vip_avatar: str | None = None
    status: int
    roles: list[int] = Field(default_factory=list)

```

In the same file (`astrbot/core/platform/sources/kook/kook_types.py`), you should apply the same pattern to other list-typed fields that currently use `[]` as a default:
1. Change `KookKMarkdown.mention_part: list[...] = []` to `KookKMarkdown.mention_part: list[...] = Field(default_factory=list)`.
2. Change `KookExtra.mention: list[...] = []` to `KookExtra.mention: list[...] = Field(default_factory=list)`.

Make sure `Field` is imported from `pydantic` at the top (it already appears in the snippet you provided).
</issue_to_address>

### Comment 2
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="120" />
<code_context>


-class KookCardModelBase:
+class KookCardModelBase(KookBaseDataClass):
     """卡片模块基类"""

</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the card module `type` definitions by using the `KookModuleType` enum directly as the discriminator instead of repeating `Enum + Literal[...] + default` for each model.

You can reduce a fair bit of boilerplate in the card models by removing the `Enum + Literal + default` duplication while keeping the discriminated union and all current behavior.

Pydantic v2 supports `Enum` values as discriminators directly, so you can:

* type `type` as `KookModuleType` instead of `Literal[...]`
* set the enum default as the value
* keep `Field(discriminator="type")` unchanged

This removes the triple‑specification (Enum member, `Literal[...]`, default) and simplifies `KookCardModelBase`.

### 1. Simplify `KookCardModelBase`

```python
class KookCardModelBase(KookBaseDataClass):
    """卡片模块基类"""

    type: KookModuleType
```

### 2. Simplify each module’s `type` field

Example: `PlainTextElement`, `SectionModule`, `FileModule`:

```python
class PlainTextElement(KookCardModelBase):
    content: str
    type: KookModuleType = KookModuleType.PLAIN_TEXT
    emoji: bool = True


class SectionModule(KookCardModelBase):
    text: PlainTextElement | KmarkdownElement | ParagraphStructure
    type: KookModuleType = KookModuleType.SECTION
    mode: SectionMode = "left"
    accessory: ImageElement | ButtonElement | None = None


class FileModule(KookCardModelBase):
    src: str
    title: str = ""
    type: KookModuleType = KookModuleType.FILE
    cover: str | None = None
    """cover 仅音频有效, 是音频的封面图"""
```

For `FileModule`, if you still want to accept `"file" | "audio" | "video"` on input, you can keep that in the enum (`FILE`, `AUDIO`, `VIDEO`) and let Pydantic coerce incoming strings to the enum; the discriminator will still work.

### 3. Keep the discriminated union unchanged

`AnyModule` can stay as is; it will work with `KookModuleType` directly:

```python
AnyModule = Annotated[
    HeaderModule
    | SectionModule
    | ImageGroupModule
    | ContainerModule
    | ActionGroupModule
    | ContextModule
    | DividerModule
    | FileModule
    | CountdownModule
    | InviteModule,
    Field(discriminator="type"),
]
```

This change:

* preserves the discriminated union behavior and serialization format,
* keeps `KookModuleType` (so you still have enum‑backed safety),
* but removes all the repeated `Literal[...] = EnumMember` boilerplate and the extra cognitive load of three sources of truth for each module type.
</issue_to_address>

### Comment 3
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="69" />
<code_context>
+    def bot_username(self):
+        return self._bot_username
+
+    async def get_bot_info(self):
+        """获取机器人账号信息"""
         url = KookApiPaths.USER_ME
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting a shared helper to parse HTTP responses into Pydantic models so both methods reuse the same validation and logging logic and avoid double-reading the body.

You can keep the stronger typing while reducing duplication and complexity around HTTP parsing by centralizing the Pydantic handling (and also avoid double‑reading the response body).

### 1. Factor out a generic HTTP → Pydantic helper

Right now:

- `get_bot_info`:
  - does `await resp.json()`
  - calls `KookUserMeResponse.from_dict(...)` in a nested try/except
  - logs and then calls `await resp.text()` again in the exception path
- `get_gateway_url`:
  - does `await resp.json()`
  - calls `KookGatewayIndexResponse.from_dict(...)`
  - has a separate `except pydantic.ValidationError` that also tries to log `await resp.text()`

This is a lot of ceremony per call, and double‑reading the body can be fragile.

You can extract a small helper that:

- reads the raw response body once,
- parses it into the given Pydantic model,
- does consistent logging for status/validation errors.

For example:

```python
# inside KookClient

async def _parse_response(
    self,
    resp: aiohttp.ClientResponse,
    model: type[pydantic.BaseModel],
    context: str,
):
    raw = await resp.text()
    if resp.status != 200:
        logger.error(f"[KOOK] {context}失败,状态码: {resp.status} , {raw}")
        return None

    try:
        return model.model_validate_json(raw)
    except pydantic.ValidationError as e:
        logger.error(f"[KOOK] {context}失败, 响应数据格式错误:\n{e}")
        logger.error(f"[KOOK] 响应内容: {raw}")
        return None
```

Then your call sites become much simpler and symmetric:

```python
async def get_bot_info(self):
    url = KookApiPaths.USER_ME
    try:
        async with self._http_client.get(url) as resp:
            resp_content = await self._parse_response(
                resp, KookUserMeResponse, "获取机器人账号信息"
            )
            if resp_content is None or not resp_content.success():
                if resp_content is not None:
                    logger.error(
                        f"[KOOK] 获取机器人账号信息失败: {resp_content.model_dump_json()}"
                    )
                return

            self._bot_id = resp_content.data.id
            self._bot_nickname = resp_content.data.nickname
            self._bot_username = resp_content.data.username
            logger.info(f"[KOOK] 获取机器人账号ID成功: {self._bot_id}")
            logger.info(f"[KOOK] 获取机器人名称成功: {self._bot_nickname}")
    except Exception as e:
        logger.error(f"[KOOK] 获取机器人账号信息异常: {e}")
```

```python
async def get_gateway_url(self, resume=False, sn=0, session_id=None):
    url = KookApiPaths.GATEWAY_INDEX
    params = {}
    if resume:
        params["resume"] = 1
        params["sn"] = sn
        if session_id:
            params["session_id"] = session_id

    try:
        async with self._http_client.get(url, params=params) as resp:
            resp_content = await self._parse_response(
                resp, KookGatewayIndexResponse, "获取gateway"
            )
            if resp_content is None or not resp_content.success():
                if resp_content is not None:
                    logger.error(f"[KOOK] 获取gateway失败: {resp_content}")
                return None

            gateway_url: str = resp_content.data.url
            logger.info(f"[KOOK] 获取gateway成功: {gateway_url.split('?')[0]}")
            return gateway_url
    except Exception as e:
        logger.error(f"[KOOK] 获取gateway异常: {e}")
        return None
```

This keeps all the Pydantic validation and structured logging, but removes repetitive try/except blocks and error‑logging boilerplate from each API method, making the code easier to follow and change.

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

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

  • The logic for ignoring self-sent messages has changed from comparing bot_nickname in config to comparing author_id with self.client.bot_id; if nickname-based filtering is still desired or relied upon, consider either preserving that behavior or explicitly deprecating the config option to avoid silent behavior changes.
  • Several BaseModel fields use mutable defaults like roles: list[int] = [], mention: list[str] = [], etc.; for clarity and to follow pydantic best practices, consider switching these to Field(default_factory=list) (e.g. in KookAuthor, KookKMarkdown, KookExtra, KookUserMeData).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The logic for ignoring self-sent messages has changed from comparing `bot_nickname` in config to comparing `author_id` with `self.client.bot_id`; if nickname-based filtering is still desired or relied upon, consider either preserving that behavior or explicitly deprecating the config option to avoid silent behavior changes.
- Several `BaseModel` fields use mutable defaults like `roles: list[int] = []`, `mention: list[str] = []`, etc.; for clarity and to follow pydantic best practices, consider switching these to `Field(default_factory=list)` (e.g. in `KookAuthor`, `KookKMarkdown`, `KookExtra`, `KookUserMeData`).

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="319-328" />
<code_context>
+    BROADCAST = "BROADCAST"
+
+
+class KookAuthor(KookBaseDataClass):
+    id: str
+    username: str
+    identify_num: str
+    nickname: str
+    bot: bool
+    online: bool
+    avatar: str | None = None
+    vip_avatar: str | None = None
+    status: int
+    roles: list[int] = []
+
+
</code_context>
<issue_to_address>
**suggestion:** Using mutable list literals as defaults in Pydantic models can be brittle; `Field(default_factory=list)` is safer.

For the fields using `[]` (e.g. `KookAuthor.roles`, `KookKMarkdown.mention_part`, `KookExtra.mention`), switch to `Field(default_factory=list)`. This avoids relying on Pydantic’s current deep-copy behavior and keeps the models safer and more portable (e.g. if later converted to dataclasses or used outside Pydantic).

Suggested implementation:

```python
class KookAuthor(KookBaseDataClass):
    id: str
    username: str
    identify_num: str
    nickname: str
    bot: bool
    online: bool
    avatar: str | None = None
    vip_avatar: str | None = None
    status: int
    roles: list[int] = Field(default_factory=list)

```

In the same file (`astrbot/core/platform/sources/kook/kook_types.py`), you should apply the same pattern to other list-typed fields that currently use `[]` as a default:
1. Change `KookKMarkdown.mention_part: list[...] = []` to `KookKMarkdown.mention_part: list[...] = Field(default_factory=list)`.
2. Change `KookExtra.mention: list[...] = []` to `KookExtra.mention: list[...] = Field(default_factory=list)`.

Make sure `Field` is imported from `pydantic` at the top (it already appears in the snippet you provided).
</issue_to_address>

### Comment 2
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="120" />
<code_context>


-class KookCardModelBase:
+class KookCardModelBase(KookBaseDataClass):
     """卡片模块基类"""

</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the card module `type` definitions by using the `KookModuleType` enum directly as the discriminator instead of repeating `Enum + Literal[...] + default` for each model.

You can reduce a fair bit of boilerplate in the card models by removing the `Enum + Literal + default` duplication while keeping the discriminated union and all current behavior.

Pydantic v2 supports `Enum` values as discriminators directly, so you can:

* type `type` as `KookModuleType` instead of `Literal[...]`
* set the enum default as the value
* keep `Field(discriminator="type")` unchanged

This removes the triple‑specification (Enum member, `Literal[...]`, default) and simplifies `KookCardModelBase`.

### 1. Simplify `KookCardModelBase`

```python
class KookCardModelBase(KookBaseDataClass):
    """卡片模块基类"""

    type: KookModuleType
```

### 2. Simplify each module’s `type` field

Example: `PlainTextElement`, `SectionModule`, `FileModule`:

```python
class PlainTextElement(KookCardModelBase):
    content: str
    type: KookModuleType = KookModuleType.PLAIN_TEXT
    emoji: bool = True


class SectionModule(KookCardModelBase):
    text: PlainTextElement | KmarkdownElement | ParagraphStructure
    type: KookModuleType = KookModuleType.SECTION
    mode: SectionMode = "left"
    accessory: ImageElement | ButtonElement | None = None


class FileModule(KookCardModelBase):
    src: str
    title: str = ""
    type: KookModuleType = KookModuleType.FILE
    cover: str | None = None
    """cover 仅音频有效, 是音频的封面图"""
```

For `FileModule`, if you still want to accept `"file" | "audio" | "video"` on input, you can keep that in the enum (`FILE`, `AUDIO`, `VIDEO`) and let Pydantic coerce incoming strings to the enum; the discriminator will still work.

### 3. Keep the discriminated union unchanged

`AnyModule` can stay as is; it will work with `KookModuleType` directly:

```python
AnyModule = Annotated[
    HeaderModule
    | SectionModule
    | ImageGroupModule
    | ContainerModule
    | ActionGroupModule
    | ContextModule
    | DividerModule
    | FileModule
    | CountdownModule
    | InviteModule,
    Field(discriminator="type"),
]
```

This change:

* preserves the discriminated union behavior and serialization format,
* keeps `KookModuleType` (so you still have enum‑backed safety),
* but removes all the repeated `Literal[...] = EnumMember` boilerplate and the extra cognitive load of three sources of truth for each module type.
</issue_to_address>

### Comment 3
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="69" />
<code_context>
+    def bot_username(self):
+        return self._bot_username
+
+    async def get_bot_info(self):
+        """获取机器人账号信息"""
         url = KookApiPaths.USER_ME
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting a shared helper to parse HTTP responses into Pydantic models so both methods reuse the same validation and logging logic and avoid double-reading the body.

You can keep the stronger typing while reducing duplication and complexity around HTTP parsing by centralizing the Pydantic handling (and also avoid double‑reading the response body).

### 1. Factor out a generic HTTP → Pydantic helper

Right now:

- `get_bot_info`:
  - does `await resp.json()`
  - calls `KookUserMeResponse.from_dict(...)` in a nested try/except
  - logs and then calls `await resp.text()` again in the exception path
- `get_gateway_url`:
  - does `await resp.json()`
  - calls `KookGatewayIndexResponse.from_dict(...)`
  - has a separate `except pydantic.ValidationError` that also tries to log `await resp.text()`

This is a lot of ceremony per call, and double‑reading the body can be fragile.

You can extract a small helper that:

- reads the raw response body once,
- parses it into the given Pydantic model,
- does consistent logging for status/validation errors.

For example:

```python
# inside KookClient

async def _parse_response(
    self,
    resp: aiohttp.ClientResponse,
    model: type[pydantic.BaseModel],
    context: str,
):
    raw = await resp.text()
    if resp.status != 200:
        logger.error(f"[KOOK] {context}失败,状态码: {resp.status} , {raw}")
        return None

    try:
        return model.model_validate_json(raw)
    except pydantic.ValidationError as e:
        logger.error(f"[KOOK] {context}失败, 响应数据格式错误:\n{e}")
        logger.error(f"[KOOK] 响应内容: {raw}")
        return None
```

Then your call sites become much simpler and symmetric:

```python
async def get_bot_info(self):
    url = KookApiPaths.USER_ME
    try:
        async with self._http_client.get(url) as resp:
            resp_content = await self._parse_response(
                resp, KookUserMeResponse, "获取机器人账号信息"
            )
            if resp_content is None or not resp_content.success():
                if resp_content is not None:
                    logger.error(
                        f"[KOOK] 获取机器人账号信息失败: {resp_content.model_dump_json()}"
                    )
                return

            self._bot_id = resp_content.data.id
            self._bot_nickname = resp_content.data.nickname
            self._bot_username = resp_content.data.username
            logger.info(f"[KOOK] 获取机器人账号ID成功: {self._bot_id}")
            logger.info(f"[KOOK] 获取机器人名称成功: {self._bot_nickname}")
    except Exception as e:
        logger.error(f"[KOOK] 获取机器人账号信息异常: {e}")
```

```python
async def get_gateway_url(self, resume=False, sn=0, session_id=None):
    url = KookApiPaths.GATEWAY_INDEX
    params = {}
    if resume:
        params["resume"] = 1
        params["sn"] = sn
        if session_id:
            params["session_id"] = session_id

    try:
        async with self._http_client.get(url, params=params) as resp:
            resp_content = await self._parse_response(
                resp, KookGatewayIndexResponse, "获取gateway"
            )
            if resp_content is None or not resp_content.success():
                if resp_content is not None:
                    logger.error(f"[KOOK] 获取gateway失败: {resp_content}")
                return None

            gateway_url: str = resp_content.data.url
            logger.info(f"[KOOK] 获取gateway成功: {gateway_url.split('?')[0]}")
            return gateway_url
    except Exception as e:
        logger.error(f"[KOOK] 获取gateway异常: {e}")
        return None
```

This keeps all the Pydantic validation and structured logging, but removes repetitive try/except blocks and error‑logging boilerplate from each API method, making the code easier to follow and change.
</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 astrbot/core/platform/sources/kook/kook_types.py


class KookCardModelBase:
class KookCardModelBase(KookBaseDataClass):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (complexity): 建议直接使用 KookModuleType 枚举作为区分字段(discriminator),从而简化卡片模块中 type 字段的定义,避免在每个模型上重复维护 Enum + Literal[...] + default 三套信息。

通过去掉 Enum + Literal + default 这层重复,同时保持区分联合(discriminated union)和现有行为,可以显著减少卡片模型里的样板代码。

Pydantic v2 支持直接使用 Enum 值作为 discriminator,因此可以:

  • type 的类型从 Literal[...] 改为 KookModuleType
  • 用枚举值作为默认值
  • 保持 Field(discriminator="type") 不变

这样就删除了“枚举成员 + Literal[...] + 默认值”这三重声明,只保留更简单的 KookCardModelBase 定义。

1. 简化 KookCardModelBase

class KookCardModelBase(KookBaseDataClass):
    """卡片模块基类"""

    type: KookModuleType

2. 简化各模块的 type 字段

示例:PlainTextElementSectionModuleFileModule

class PlainTextElement(KookCardModelBase):
    content: str
    type: KookModuleType = KookModuleType.PLAIN_TEXT
    emoji: bool = True


class SectionModule(KookCardModelBase):
    text: PlainTextElement | KmarkdownElement | ParagraphStructure
    type: KookModuleType = KookModuleType.SECTION
    mode: SectionMode = "left"
    accessory: ImageElement | ButtonElement | None = None


class FileModule(KookCardModelBase):
    src: str
    title: str = ""
    type: KookModuleType = KookModuleType.FILE
    cover: str | None = None
    """cover 仅音频有效, 是音频的封面图"""

对于 FileModule,如果你仍希望在输入上接受 "file" | "audio" | "video" 这样的字符串,可以把这些值保留在枚举中(FILEAUDIOVIDEO),然后让 Pydantic 来把传入字符串转换为枚举值;discriminator 仍然能正常工作。

3. 保持区分联合定义不变

AnyModule 可以保持原样,它可以直接与 KookModuleType 一起工作:

AnyModule = Annotated[
    HeaderModule
    | SectionModule
    | ImageGroupModule
    | ContainerModule
    | ActionGroupModule
    | ContextModule
    | DividerModule
    | FileModule
    | CountdownModule
    | InviteModule,
    Field(discriminator="type"),
]

这一改动:

  • 保留了区分联合的行为和序列化格式;
  • 保留了 KookModuleType(仍然有枚举带来的类型安全);
  • 但去掉了所有 Literal[...] = EnumMember 形式的重复样板,减少了为每个模块维护三份“单一事实源”的负担。
Original comment in English

issue (complexity): Consider simplifying the card module type definitions by using the KookModuleType enum directly as the discriminator instead of repeating Enum + Literal[...] + default for each model.

You can reduce a fair bit of boilerplate in the card models by removing the Enum + Literal + default duplication while keeping the discriminated union and all current behavior.

Pydantic v2 supports Enum values as discriminators directly, so you can:

  • type type as KookModuleType instead of Literal[...]
  • set the enum default as the value
  • keep Field(discriminator="type") unchanged

This removes the triple‑specification (Enum member, Literal[...], default) and simplifies KookCardModelBase.

1. Simplify KookCardModelBase

class KookCardModelBase(KookBaseDataClass):
    """卡片模块基类"""

    type: KookModuleType

2. Simplify each module’s type field

Example: PlainTextElement, SectionModule, FileModule:

class PlainTextElement(KookCardModelBase):
    content: str
    type: KookModuleType = KookModuleType.PLAIN_TEXT
    emoji: bool = True


class SectionModule(KookCardModelBase):
    text: PlainTextElement | KmarkdownElement | ParagraphStructure
    type: KookModuleType = KookModuleType.SECTION
    mode: SectionMode = "left"
    accessory: ImageElement | ButtonElement | None = None


class FileModule(KookCardModelBase):
    src: str
    title: str = ""
    type: KookModuleType = KookModuleType.FILE
    cover: str | None = None
    """cover 仅音频有效, 是音频的封面图"""

For FileModule, if you still want to accept "file" | "audio" | "video" on input, you can keep that in the enum (FILE, AUDIO, VIDEO) and let Pydantic coerce incoming strings to the enum; the discriminator will still work.

3. Keep the discriminated union unchanged

AnyModule can stay as is; it will work with KookModuleType directly:

AnyModule = Annotated[
    HeaderModule
    | SectionModule
    | ImageGroupModule
    | ContainerModule
    | ActionGroupModule
    | ContextModule
    | DividerModule
    | FileModule
    | CountdownModule
    | InviteModule,
    Field(discriminator="type"),
]

This change:

  • preserves the discriminated union behavior and serialization format,
  • keeps KookModuleType (so you still have enum‑backed safety),
  • but removes all the repeated Literal[...] = EnumMember boilerplate and the extra cognitive load of three sources of truth for each module type.

Comment thread astrbot/core/platform/sources/kook/kook_client.py Outdated
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This PR extensively refactors the KOOK adapter, fully introducing Pydantic data classes to handle WebSocket events and API responses. This is an excellent improvement, significantly enhancing code type safety, readability, and robustness, while reducing redundant code and improving error handling. A critical security issue was identified where sensitive information, such as verifyToken, could be leaked into logs during a validation error. Additionally, some minor issues were found that could make the code more concise and clear, with specific suggestions provided in the review comments. The rest of the changes appear to follow security best practices, including improved bot identification and robust card message parsing.


except pydantic.ValidationError as e:
logger.error(f"[KOOK] 解析WebSocket事件数据格式失败: \n{e}")
logger.error(f"[KOOK] 原始响应内容: {msg}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The listen method logs the raw WebSocket message msg when a pydantic.ValidationError occurs. This raw message contains sensitive information such as the verifyToken and potentially the encryptKey in the top-level extra field. Logging these secrets can lead to unauthorized access if logs are compromised. It is recommended to sanitize the message by masking or removing sensitive fields before logging.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

有点屏蔽敏感内容太麻烦,而且适配器的其他消息在debug下也会打印原始响应内容,要改的话那要改的可多了.jpg

Comment thread astrbot/core/platform/sources/kook/kook_adapter.py
Comment thread astrbot/core/platform/sources/kook/kook_client.py Outdated
@shuiping233
Copy link
Copy Markdown
Contributor Author

ok,这个pr我觉得应该是弄完了)

@shuiping233 shuiping233 force-pushed the refactor/add_kook_event_data_type branch 3 times, most recently from 351ffb1 to 6633cf6 Compare March 4, 2026 08:01
@shuiping233 shuiping233 force-pushed the refactor/add_kook_event_data_type branch from dd44b74 to 4b8b76c Compare March 5, 2026 04:31
@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:XXL This PR changes 1000+ lines, ignoring generated files. labels Mar 5, 2026
@shuiping233 shuiping233 force-pushed the refactor/add_kook_event_data_type branch from 4b8b76c to a91f57d Compare March 10, 2026 12:46
@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Mar 10, 2026
@shuiping233 shuiping233 force-pushed the refactor/add_kook_event_data_type branch from a91f57d to d2852db Compare March 10, 2026 12:47
@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Mar 17, 2026
@Soulter Soulter changed the title refactor: 给kook适配器尽可能覆盖pydantic数据类,使其更容易获取数据和校验消息结构 perf: Implement Pydantic data models for the KOOK adapter to enhance data retrieval and message schema validation Mar 17, 2026
@Soulter Soulter merged commit f5ba1a0 into AstrBotDevs:master Mar 17, 2026
6 checks passed
@shuiping233 shuiping233 deleted the refactor/add_kook_event_data_type branch March 17, 2026 10:51
shuiping233 added a commit to shuiping233/AstrBot that referenced this pull request Mar 17, 2026
我那时候搓 AstrBotDevs#5719 的时候 AstrBotDevs#5729 已经合并了, 既然ruff的py限制版本里是`3.12`,那我那时候干脆用的StrEnum,现在发现那个pr revert了,那我也降级回旧Enum写法好了
Soulter pushed a commit that referenced this pull request Mar 17, 2026
…compatibility (#6512)

我那时候搓 #5719 的时候 #5729 已经合并了, 既然ruff的py限制版本里是`3.12`,那我那时候干脆用的StrEnum,现在发现那个pr revert了,那我也降级回旧Enum写法好了
Astral-Yang pushed a commit to Astral-Yang/AstrBot that referenced this pull request Mar 17, 2026
…compatibility (AstrBotDevs#6512)

我那时候搓 AstrBotDevs#5719 的时候 AstrBotDevs#5729 已经合并了, 既然ruff的py限制版本里是`3.12`,那我那时候干脆用的StrEnum,现在发现那个pr revert了,那我也降级回旧Enum写法好了
KBVsent pushed a commit to KBVsent/AstrBot that referenced this pull request Mar 19, 2026
…data retrieval and message schema validation (AstrBotDevs#5719)

* refactor: 给kook适配器添加kook事件数据类

* format: 使用StrEnum替换kook适配器中的(str,enum)
KBVsent pushed a commit to KBVsent/AstrBot that referenced this pull request Mar 19, 2026
…compatibility (AstrBotDevs#6512)

我那时候搓 AstrBotDevs#5719 的时候 AstrBotDevs#5729 已经合并了, 既然ruff的py限制版本里是`3.12`,那我那时候干脆用的StrEnum,现在发现那个pr revert了,那我也降级回旧Enum写法好了
KBVsent pushed a commit to KBVsent/AstrBot that referenced this pull request Mar 21, 2026
…data retrieval and message schema validation (AstrBotDevs#5719)

* refactor: 给kook适配器添加kook事件数据类

* format: 使用StrEnum替换kook适配器中的(str,enum)
KBVsent pushed a commit to KBVsent/AstrBot that referenced this pull request Mar 21, 2026
…compatibility (AstrBotDevs#6512)

我那时候搓 AstrBotDevs#5719 的时候 AstrBotDevs#5729 已经合并了, 既然ruff的py限制版本里是`3.12`,那我那时候干脆用的StrEnum,现在发现那个pr revert了,那我也降级回旧Enum写法好了
xkeyC added a commit to xkeyC/AstrBot that referenced this pull request Mar 28, 2026
* perf: onebot, satori docs improvement

* ci: add pr check

* chore: Delete .github/workflows/pr-checklist-check.yml

* feat: localize session management group & interval method texts (AstrBotDevs#6471)

* fix(ui): localize session management group texts

Replace hardcoded Chinese strings in SessionManagementPage with i18n
lookups for group management labels, dialogs, and action feedback.

Add and align translation keys in en-US, ru-RU, and zh-CN for group
management and batch operation messages to ensure consistent multilingual
UI behavior.

* fix(ui): localize interval method hint text

* fix: SQLite 'database is locked' by adding busy timeout (AstrBotDevs#6474)

The async engine is created without a busy timeout, so concurrent
writes (agent responses, metrics, session updates) fail instantly
with 'database is locked' instead of waiting for the lock.

Add connect_args={'timeout': 30} for SQLite engines so the driver
waits up to 30 seconds for the write lock. Combined with the existing
WAL journal mode, this handles the typical concurrent write bursts
from agent + metrics + session operations.

Fixes AstrBotDevs#6443

* fix: parse multiline frontmatter description in SKILL.md (AstrBotDevs#6460)

* fix(skills): support multiline frontmatter descriptions

* fix(skills): 修复多行 frontmatter 描述解析

* style(skills): clean up frontmatter parser follow-ups

---------

Co-authored-by: RhoninSeiei <RhoninSeiei@users.noreply.github.com>

* chore(deps): bump the github-actions group with 2 updates (AstrBotDevs#6461)

Bumps the github-actions group with 2 updates: [ncipollo/release-action](https://github.com/ncipollo/release-action) and [actions/github-script](https://github.com/actions/github-script).


Updates `ncipollo/release-action` from 1.20.0 to 1.21.0
- [Release notes](https://github.com/ncipollo/release-action/releases)
- [Commits](ncipollo/release-action@v1.20.0...v1.21.0)

Updates `actions/github-script` from 7 to 8
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](actions/github-script@v7...v8)

---
updated-dependencies:
- dependency-name: ncipollo/release-action
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore: remove deprecated version field from compose.yml (AstrBotDevs#5495)

The version field is no longer required in Docker Compose v2 and has been deprecated.

* fix: reading skills on Windows (AstrBotDevs#6490)

There is an issue with reading the skill directory on the Windows system, which results in a high probability of files under the skill directory being unrecognizable, now fix it.

* fix: subagent lookup failure when using default persona (AstrBotDevs#5672)

* fix: resolve subagent persona lookup for 'default' and unify resolution logic

- Add PersonaManager.get_persona_v3_by_id() to centralize v3 persona resolution
- Handle 'default' persona_id mapping to DEFAULT_PERSONALITY in subagent orchestrator
- Fix HandoffTool.default_description using agent_name parameter correctly
- Add tests for default persona in subagent config and tool deduplication

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: simplify get_default_persona_v3 using get_persona_v3_by_id

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* fix: register_agent decorator NameError (AstrBotDevs#5765)

* fix: 修改 register_agent 以避免运行时导入 AstrAgentContext

* test: improve register_agent test robustness

- Add fixture for llm_tools cleanup to avoid test interference
- Use multiple import patterns to make guard more robust to refactors
- Add assertion to verify decorated coroutine is wired as handoff handler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 删除测试文件: 移除 register_agent 装饰器的运行时行为测试

---------

Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Soulter <905617992@qq.com>

* fix: only pass dimensions when explicitly configured in embedding config (AstrBotDevs#6432)

* fix: only pass dimensions param when explicitly configured

Models like bge-m3 don't support the dimensions parameter in the
embedding API, causing HTTP 400 errors. Previously dimensions was
always sent with a default value of 1024, even when the user never
configured it. Now dimensions is only included in the request when
embedding_dimensions is explicitly set in provider config.

Closes AstrBotDevs#6421

Signed-off-by: JiangNan <1394485448@qq.com>

* fix: handle invalid dimensions config and align get_dim return

- Add try-except around int() conversion in _embedding_kwargs to
  gracefully handle invalid embedding_dimensions config values
- Update get_dim() to return 0 when embedding_dimensions is not
  explicitly configured, so callers know dimensions weren't specified
  and can handle it accordingly
- Both methods now share consistent logic for reading the config

Signed-off-by: JiangNan <1394485448@qq.com>

* fix: improve logging for invalid embedding_dimensions configuration

---------

Signed-off-by: JiangNan <1394485448@qq.com>
Co-authored-by: Soulter <905617992@qq.com>

* perf: Implement Pydantic data models for the KOOK adapter to enhance data retrieval and message schema validation (AstrBotDevs#5719)

* refactor: 给kook适配器添加kook事件数据类

* format: 使用StrEnum替换kook适配器中的(str,enum)

* docs: add aiocqhttp and satori protocol documentation; remove outdated lagrange and napcat guides

* refactor: downgrade StrEnum to (str, Enum) in kook_type for backward compatibility  (AstrBotDevs#6512)

我那时候搓 AstrBotDevs#5719 的时候 AstrBotDevs#5729 已经合并了, 既然ruff的py限制版本里是`3.12`,那我那时候干脆用的StrEnum,现在发现那个pr revert了,那我也降级回旧Enum写法好了

* feat: install plugin using metadata name and validate importable identifiers (AstrBotDevs#6530)

* feat: install plugin using metadata name and validate importable identifiers

* fix: cleanup temporary upload extraction directory on plugin install failure

* Update astrbot/core/star/star_manager.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix: avoid unnecessary install when repository directory already exists

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix: restrict workflows to upstream repo (AstrBotDevs#6531)

* Clarify FileUpload/DownloadTool descriptions to fix LLM tool selection (AstrBotDevs#6527)

Multiple models (Gemini 3, GPT-5.2, Claude Sonnet, Kimi K2.5) consistently
pick FileDownloadTool when they should pick FileUploadTool. The old
descriptions used "upload/download" which is ambiguous from the LLM's
perspective — it doesn't know which side is "local" vs "remote".

Rewrite descriptions to use explicit directional language:
- Upload: "Transfer FROM host INTO sandbox" + "when user sends a file"
- Download: "Transfer FROM sandbox OUT to host" + "ONLY when user asks
  to retrieve/export"

Also improve parameter descriptions with the same directional clarity.

Fixes AstrBotDevs#6497

Co-authored-by: Yufeng He <40085740+universeplayer@users.noreply.github.com>

* perf(dashboard): subset MDI icon font and self-host Google Fonts (AstrBotDevs#6532)

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font cr fix

* chore: update lockfile

* enhance:更改未完成更新的文档用词问题(多处“消息平台”已更名为“机器人”) (AstrBotDevs#6568)

* Update kubernetes.md

* Update discord.md

* Update kubernetes.md

* Update AstrBot setup instructions in Kubernetes doc

* fix: set packaged Windows runtime build env for pip native builds (AstrBotDevs#6575)

* Fix Windows packaged runtime pip build env

* test(pip): cover packaged runtime env injection edges

* refactor(pip): tighten packaged runtime env handling

* test(pip): cover missing runtime build dirs

* fix(pip): build runtime env inside locked section

* test(pip): expand windows path normalization coverage

* refactor(pip): build runtime env from snapshots

* fix(pip): preserve windows env key semantics

* refactor(pip): simplify windows runtime env handling

Keep the in-process pip environment mutation and case-insensitive INCLUDE/LIB handling localized so packaged Windows builds are easier to follow. Add a UNC no-op regression case to guard path normalization.

* refactor(pip): streamline runtime env mutation helpers

Keep packaged Windows pip environment handling easier to follow by reusing a temporary environment context manager, isolating case-insensitive INCLUDE/LIB lookup, and documenting native path normalization behavior.

* feat (doc) : Add doc for shipyard-neo sandbox driver (AstrBotDevs#6590)

* fix(ui): localize session management group texts

Replace hardcoded Chinese strings in SessionManagementPage with i18n
lookups for group management labels, dialogs, and action feedback.

Add and align translation keys in en-US, ru-RU, and zh-CN for group
management and batch operation messages to ensure consistent multilingual
UI behavior.

* fix(ui): localize interval method hint text

* docs(sandbox): document shipyard neo setup

Expand the Chinese sandbox guide to cover Shipyard Neo as the
recommended driver and distinguish it from legacy Shipyard.

Add deployment and configuration guidance for standalone and
compose-based setups, include a full annotated config example,
and clarify profile selection, TTL behavior, workspace paths,
and persistence semantics.

* docs(sandbox): recommend standalone shipyard neo

Clarify that Shipyard Neo is best deployed on a separate,
better-provisioned host for long-term use.

Update the setup steps and AstrBot connection guidance, and
remove the earlier combined Docker Compose deployment flow.

* docs(sandbox): expand shipyard neo guide

Document Shipyard Neo as the recommended sandbox driver and
clarify how it differs from the legacy Shipyard setup.

Add guidance for deployment, performance requirements, Bay
configuration, profile selection, TTL behavior, workspace
persistence, and browser capability support.

Also reorganize the sandbox configuration section and keep the
legacy Shipyard instructions for compatibility.

* docs(sandbox): fix shipyard neo doc links

Update the sandbox guides in English and Chinese to link
directly to the upstream `config.yaml` example.

Replace duplicated TTL and persistence notes with references
to the dedicated sections to keep the guide concise and easier
to maintain.

* docs(sandbox): clarify section references in guides (AstrBotDevs#6591)

* fix: prevent wecom ai bot long connection replies from disappearing (AstrBotDevs#6606)

* fix: prevent empty fallback replies from clearing wecom ai bot output

* fix: 优化消息发送逻辑,避免发送空消息

---------

Co-authored-by: shijianhuai <shijianhuai@simuwang.com>
Co-authored-by: Soulter <905617992@qq.com>

* fix(wecom-aibot): significantly improve streaming readability and speed via add throttling (AstrBotDevs#6610)

* fix(wecom-ai): add 0.5s interval for streaming responses

* fix(wecom-ai): correct event type checking and add spacing in WecomAIBotMessageEvent

* feat: context token counting support for multimodal content (images, audio, and chain-of-thought) (AstrBotDevs#6596)

EstimateTokenCounter 之前只计算 TextPart,完全忽略 ImageURLPart、
AudioURLPart 和 ThinkPart。多模态对话中图片占 500-2000 token,
不被计入会导致 context 压缩触发过晚,API 先报 context_length_exceeded。

改动:
- ImageURLPart 按 765 token 估算(OpenAI vision 低/高分辨率中位数)
- AudioURLPart 按 500 token 估算
- ThinkPart 的文本内容正常计算
- 10 个新测试覆盖各类型单独和混合场景

Co-authored-by: Yufeng He <40085740+universeplayer@users.noreply.github.com>

* fix(openai): Token usage not working when using MoonshotAI official API (AstrBotDevs#6618)

fixes: AstrBotDevs#6614

* fix: update hint for ID whitelist configuration to clarify behavior when empty (AstrBotDevs#6611)

* fix: update hint for ID whitelist configuration to clarify behavior when empty

* fix: update whitelist hint

---------

Co-authored-by: machina <1531829828@qq.com>
Co-authored-by: Soulter <905617992@qq.com>

* fix: 截断器丢失唯一 user 消息导致智谱等 provider 返回 400 (AstrBotDevs#6581)

* fix: 截断器丢失唯一 user 消息导致 API 400

修复 AstrBotDevs#6196

当对话只有一条 user 消息(长 tool chain 场景:system → user → assistant
→ tool → assistant → tool → ...),三个截断方法都会把这条 user 消息丢掉,
导致智谱、Gemini 等要求 user 消息的 provider 返回 400。

改动:
- 提取 `_split_system_rest()` 去掉三个方法里重复的 system/non-system 拆分
- 新增 `_ensure_user_message()`:截断后如果没有 user 了,从原始消息里补回
  第一条 user,避免违反 API 格式要求
- 删掉 `truncate_by_dropping_oldest_turns` 里把没有 user 就清空全部消息的逻辑
- 5 个新测试覆盖单 user + 长 tool chain 场景,3 个旧测试更新断言

* style: format code

---------

Co-authored-by: Yufeng He <40085740+universeplayer@users.noreply.github.com>
Co-authored-by: RC-CHN <1051989940@qq.com>

* fix: prevent truncation logic from removing the only user message in long tool-calling conversations (AstrBotDevs#6198)

* fix: 压缩算法删除 user 消息 Bug 修复

* perf: improve truncate algo

---------

Co-authored-by: Soulter <905617992@qq.com>

* feat: add Kimi Coding Plan provider with Anthropic API compatibility (AstrBotDevs#6559)

* Add Kimi Code provider

* Add icon mapping for Kimi Code provider

* Clarify Kimi CodingPlan provider labeling

* Refine Kimi Code header handling

* modified docker compose

* fix: correct Kimi Coding Plan label and update API base URL

---------

Co-authored-by: Soulter <905617992@qq.com>

* fix(openai): improve logging for proxy and API base configuration (AstrBotDevs#6669)

fix: AstrBotDevs#6558

* fix(dashboard): simplify persona selector layout for mobile screens (AstrBotDevs#5907)

* fix: Follow-up logic persists after /stop trigger (AstrBotDevs#6656)

/stop 设置 agent_stop_requested 标记,但 runner 直到当前工具调用
超时才从 _ACTIVE_AGENT_RUNNERS 注销。在此窗口期内,用户发的新消息
被 try_capture_follow_up() 当作 follow-up 吞掉。

在 follow-up 捕获前检查 stop 标记:一旦用户请求停止,就不再把后续
消息注入到正在终止的 agent 上下文中。

Fixes AstrBotDevs#6626

* fix: auto-restart telegram polling loop on failure (AstrBotDevs#6648)

* fix: auto-restart telegram polling loop on failure (AstrBotDevs#373)

* fix: auto-restart telegram polling loop on failure

* fix: harden telegram polling restart lifecycle

* fix(telegram): 根据建议优化轮询鲁棒性并处理 Token 失效错误

* fix: 补全配置元数据及 i18n

* feat: add xiaomi MiMo TTS & STT providers (AstrBotDevs#6643)

* feat: add mimo tts provider support

* fix: handle empty mimo tts choices

* feat: add mimo stt provider support

* chore: rename "OpenAI" provider to "OpenAI Compatible" (AstrBotDevs#6707)

* fix: prevent accidental removal of MCP external tools due to name collisions with disabled built-in tools (AstrBotDevs#5925)

* fix: 解决 MCP 工具与内置工具重名时的连坐问题

- 修改 get_func 方法:优先返回已激活的工具
- 修改 get_full_tool_set 方法:使用 add_tool 防止同名冲突
- 修改 add_tool 方法:优先保留已激活的工具

Fixes AstrBotDevs#5821

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: address PR review feedback for tool conflict resolution

- Fix inconsistency: get_func now uses reversed() to match ToolSet.add_tool's
  "last-active-wins" logic, preventing potential "tool hijacking" issues
- Improve readability: replace double negative condition with clearer logic
- Add compatibility: use getattr with default for tools without 'active' attribute
- Remove unnecessary deepcopy: MCPTool runtime objects should not be deep copied
- Update docstring: accurately describe the actual tool resolution behavior

Addresses review comments from sourcery-ai, gemini-code-assist, and Copilot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add tests for tool conflict resolution (issue AstrBotDevs#5821)

Add comprehensive tests for ToolSet.add_tool, get_func, and get_full_tool_set
to verify the conflict resolution behavior when MCP tools share names with
built-in tools.

Test cases:
- ToolSet.add_tool: active/inactive priority, last-one-wins for same state
- get_func: returns last active tool, fallback to last matching tool
- get_full_tool_set: deduplication logic, no deepcopy, MCP overrides disabled builtin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复工具冲突处理逻辑,确保未激活工具不被错误移除

---------

Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add a toggle to disable thinking mode in Ollama (AstrBotDevs#5941)

* feat: add ollama thinking toggle

* fix: simplify hint for ollama_disable_thinking configuration

---------

Co-authored-by: Gargantua <22532097@zju.edu.cn>
Co-authored-by: Soulter <905617992@qq.com>

* fix: preserve PATHEXT for stdio mcp servers on windows (AstrBotDevs#5822)

* fix: preserve PATHEXT for stdio mcp servers on windows

* chore: delete test_mcp_client.py

---------

Co-authored-by: Soulter <905617992@qq.com>

* fix(core): interrupt subagent tool waits on stop (AstrBotDevs#5850)

* fix(core): interrupt subagent tool waits on stop

* test: relax subagent handoff timeout

* test: cover stop-aware tool interruption

* refactor: unify runner stop state

* refactor: simplify tool executor interruption

* fix: preserve tool interruption propagation

* refactor: tighten interruption helpers

---------

Co-authored-by: idiotsj <idiotsj@users.noreply.github.com>

* fix(agent): reject follow-up messages after stop request (AstrBotDevs#6704)

* fix: reject follow-up messages after stop requested (AstrBotDevs#6626)

Once a user sends /stop, follow-up messages should no longer be
accepted for that runner. Previously, there was a race window where
messages sent after stop could still be queued as follow-ups.

This fix gates the follow_up() method to check both done() and
_stop_requested before accepting a new follow-up message.

Acceptance criteria met:
- After /stop, later follow-up messages return None (rejected)
- Post-stop follow-ups are not added to _pending_follow_ups
- No post-stop text is injected into tool results
- Graceful-stop behavior otherwise unchanged
- Follow-ups submitted before stop retain current behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: add regression tests for issue AstrBotDevs#6626 follow-up rejection

Add focused tests that verify the complete tool-result injection path
for follow-up messages after stop is requested:

- test_follow_up_rejected_and_runner_stops_without_execution: Verifies
  that when stop is requested before any execution, follow-ups are
  rejected and the runner stops gracefully without executing tools.

- test_follow_up_merged_into_tool_result_before_stop: Verifies that
  follow-ups queued before stop are properly merged into tool results
  via _merge_follow_up_notice().

- test_follow_up_after_stop_not_merged_into_tool_result: Regression
  test that simulates the race condition from issue AstrBotDevs#6626. Verifies
  that only pre-stop follow-ups are merged into tool results, and
  post-stop follow-ups are rejected at the admission point.

These tests validate the fix in ToolLoopAgentRunner.follow_up() that
checks both self.done() and self._stop_requested before accepting
new follow-up messages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(agent): update stop request check in ToolLoopAgentRunner

---------

Co-authored-by: ccsang <ccsang@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Soulter <905617992@qq.com>

* fix: skills-like re-query missing extra_user_content_parts causes image_caption not to be injected (AstrBotDevs#6710)

当使用 skills-like tool mode 时,_resolve_tool_exec 的 re-query 调用没有
传递 extra_user_content_parts,导致图片描述等附加内容丢失。

fixes AstrBotDevs#6702

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* perf(webchat): enhance message handling with proactive saving and streaming completion (AstrBotDevs#6698)

* fix(config): respect disabled system functions in web search tools (AstrBotDevs#6584)

Co-authored-by: BillionClaw <billionclaw@cl OSS.dev>

* fix(agent): pass tool_call_timeout to subagent handsoff, cron and background task execution, and increase default timeout from 60 to 120 (AstrBotDevs#6713)

* fix(agent): pass tool_call_timeout to SubAgent handoff execution

- Add tool_call_timeout parameter to _execute_handoff method
- Pass run_context.tool_call_timeout to ctx.tool_loop_agent
- Add unit test to verify tool_call_timeout is correctly passed
- Fixes AstrBotDevs#6711: SubAgent MCP tool call timeout now respects configured timeout

The SubAgent handoff execution was using the default 60-second timeout
instead of the configured tool_call_timeout from provider settings.
This change ensures that SubAgent MCP tool calls respect the user's
configured timeout settings.

* test: add unit test for tool_call_timeout in SubAgent handoff

* fix: restore deleted test and fix test assertion

- Restore test_collect_handoff_image_urls_filters_extensionless_missing_event_file
- Fix test_collect_handoff_image_urls_keeps_extensionless_existing_event_file assertion
- Keep new test_execute_handoff_passes_tool_call_timeout_to_tool_loop_agent

* refactor: simplify tool_call_timeout passing in _execute_handoff

- Pass run_context.tool_call_timeout directly to ctx.tool_loop_agent
- Remove unnecessary local variable assignment
- Addresses review feedback from Sourcery AI

* fix(config): increase default tool call timeout from 60 to 120 seconds

---------

Co-authored-by: LehaoLin <linlehao@cuhk.edu.cn>
Co-authored-by: Soulter <905617992@qq.com>

* docs: update README.md to add separator in links section

* fix(skills): use actual sandbox path from cache instead of hardcoded workspace root (AstrBotDevs#6331)

* fix(skills): use actual sandbox path from cache instead of hardcoded workspace root

Fixes AstrBotDevs#6273

When using Shipyard booter, the sandbox workspace directory is
`/home/ship_{session_id}/workspace/` instead of the hardcoded `/workspace`.
This caused Agent to fail reading SKILL.md files with 'No such file or directory'.

Changes:
- In build_skills_prompt: prefer skill.path (from sandbox cache) over
  hardcoded SANDBOX_WORKSPACE_ROOT for sandbox_only skills
- In list_skills: always prefer sandbox_cached_paths over hardcoded path
  for sandbox_only skills

The actual path is resolved at sandbox scan time via Path.resolve() in
_build_scan_command, which returns the correct absolute path based on
the sandbox's actual working directory.

* docs: add comment explaining show_sandbox_path behavior for sandbox_only skills

Address Sourcery AI review comment:
- Clarify that show_sandbox_path is implicitly True for sandbox_only skills
- Explain why the flag is effectively ignored (no local path exists)

* refactor: simplify path_str fallback using or operator

Address review feedback: use single-line fallback instead of if-not pattern.

* style: format skill_manager.py with ruff

Fix ruff format-check failure

* fix(skills): sanitize cached sandbox skill paths

Normalize sandbox cache paths before reading or writing them so invalid,
empty, or mismatched entries fall back to a safe default SKILL.md path.

This avoids using malformed cached paths, keeps path rendering
consistent, and ensures sandbox skill listings always point to the
expected workspace location.

---------

Co-authored-by: ccsang <ccsang@users.noreply.github.com>
Co-authored-by: RC-CHN <1051989940@qq.com>

* fix: ensure Gemini array schemas always include items (AstrBotDevs#6051)

Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>

* fix(webchat): render standalone HTML replies as code (AstrBotDevs#6074)

Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* fix: fall back on Windows skill file encodings (AstrBotDevs#6058)

Co-authored-by: stablegenius49 <185121704+stablegenius49@users.noreply.github.com>

* fix(lark): Defer card creation and renew on tool call break (AstrBotDevs#6743)

* fix(lark): defer streaming card creation and renew card on tool call break

- Defer CardKit streaming card creation until the first text token
  arrives, preventing an empty card from rendering before content.
- Handle `type="break"` signal in send_streaming: close the current
  card and lazily create a new one for post-tool-call text, so the
  new card appears below the tool status message in correct order.
- Only emit "break" signal when show_tool_use is enabled; when tool
  output is hidden, the AI response continues on the same card.

* style: format ruff

* fix: cr bug

* fix: cr

* fix: convert Feishu opus files for Whisper API STT (AstrBotDevs#6078)

* fix: convert lark opus files for whisper api

* chore: ruff format

---------

Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>
Co-authored-by: Soulter <905617992@qq.com>

* fix: skip empty knowledge-base embedding batches (AstrBotDevs#6106)

Co-authored-by: stablegenius49 <185121704+stablegenius49@users.noreply.github.com>

* feat(skill_manager): normalize and rename legacy skill markdown files to `SKILL.md` (AstrBotDevs#6757)

* feat(skill_manager): normalize and rename legacy skill markdown files to `SKILL.md`

* fix(vec_db): format debug log message for empty batch insert

* feat(extension): add category filtering for market plugins and enhance UI components (AstrBotDevs#6762)

* chore: bump version to 4.21.0

* feat: supports weixin personal account (AstrBotDevs#6777)

* feat: supports weixin personal account

* feat(weixin): update documentation for personal WeChat integration and add QR code image

* feat(weixin): refactor send method to streamline message handling

* fix(weixin): correct AES key encoding in media payload construction

* feat(weixin): update weixin_oc_base_url description for clarity in config metadata

* feat(weixin): enhance WeChat integration with QR code support and configuration updates

* feat(weixin): implement WeixinOCClient for improved media handling and API requests

* feat(platform): update platform status refresh interval to 5 seconds

* fix(platform.tg_adapter): import Forbidden instead of deprecated Unauthorized (AstrBotDevs#6765) (AstrBotDevs#6769)

* feat: skip search when the entire knowledge base is empty (AstrBotDevs#6750)

* feat:增加知识库全为空时的跳过检索

* apply bot suggestions

* style:reformat code

* feat: fix preserve escaped newlines in frontmatter & update tests & ci workflows (AstrBotDevs#6783)

* Feat(webui): support pinning and dragging for installed plugins (AstrBotDevs#6649) (AstrBotDevs#6776)

* refactor(persona): replace local folder components with shared folder components

* feat(webui): implement draggable reordering with animation for pinned plugins

* refactor(webui): extract PinnedPluginItem into a standalone component

* fix: handle potential None values for token usage metrics in OpenAI provider (AstrBotDevs#6788)

Such as: unsupported operand type(s) for -: 'int' and 'NoneType'

fixes: AstrBotDevs#6772

* feat: supports image compressing (AstrBotDevs#6794)

* feat: supports image compressing (AstrBotDevs#6463)

Co-authored-by: Soulter <905617992@qq.com>

* feat: 增加图像压缩最大尺寸至1280

* Update astrbot/core/astr_main_agent.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* feat: 增强临时文件管理,添加图像压缩路径跟踪与清理功能

* feat: 更新图片压缩功能提示,移除对 chat_completion 提供商的限制说明

---------

Co-authored-by: Chen <42998804+a61995987@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix: keep all CallToolResult content items (AstrBotDevs#6149)

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* chore: bump version to 4.22.0

* docs: update wechat app version requirements for WeChat adapter and add instructions for profile photo/remark modifications

* chore: gitignore .env warker.js

* fix: remove privacy data from test case (AstrBotDevs#6803)

Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>

* fix: align mimo tts style payload with official docs (AstrBotDevs#6814)

* feat(dashboard): add log and cache cleanup in settings (AstrBotDevs#6822)

* feat(dashboard): add log and cache cleanup in settings

* refactor: simplify storage cleaner log config handling

* fix: Repair abnormal indentation

* fix(storage): harden cleanup config handling

Use typed config value access to avoid treating invalid values as
enabled flags or log paths during storage cleanup.

Also stop exposing raw backend exceptions in the dashboard storage
status API and direct users to server logs for details.

---------

Co-authored-by: RC-CHN <1051989940@qq.com>

* fix(t2i): sync active template across all configs (AstrBotDevs#6824)

* fix(t2i): sync active template across all configs

apply template activation and reset to every config profile instead of only
the default one, and reload each pipeline scheduler so changes take effect
consistently in multi-config setups

add a dashboard test that creates extra configs and verifies active template
updates and scheduler reload coverage across all config ids

* fix(t2i): reload all schedulers on template changes

extract a shared helper to reload pipeline schedulers for every config.
when syncing or resetting the active template, persist each config and
then reload all schedulers to keep mappings consistent.

also reload all schedulers when updating the currently active template,
and add dashboard tests to verify cross-config sync and scheduler
replacement behavior.

* fix: cannot use tools in siliconflow provider (AstrBotDevs#6829)

* fix: cannot use tools in siliconflow provider

* fix: handle empty choices in ChatCompletionStreamState

* fix: correct voice message support status in WeChat adapter documentation

* feat(lark): add collapsible reasoning panel support and enhance message handling (AstrBotDevs#6831)

* feat(lark): add collapsible reasoning panel support and enhance message handling

* feat(lark): refactor collapsible panel creation for improved readability and maintainability

* chore: ruff format

* perf: validate config_path before checking existence (AstrBotDevs#6722)

Add a check for empty config_path in check_exist method

* chore(deps): bump pnpm/action-setup in the github-actions group (AstrBotDevs#6862)

Bumps the github-actions group with 1 update: [pnpm/action-setup](https://github.com/pnpm/action-setup).


Updates `pnpm/action-setup` from 4.4.0 to 5.0.0
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](pnpm/action-setup@v4.4.0...v5.0.0)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: wrong index in ObjectEditor updateKey causing false 'key exists' error

* fix: wrong index in ObjectEditor updateKey causing false 'key exists' error

* fix: same index mismatch issue in updateJSON

* fix(ui): stabilize ObjectEditor pair keys

Use generated ids for key-value pairs instead of array indexes to
prevent mismatch issues during editing and rendering.

Also replace duplicate-key alerts with toast warnings for a more
consistent UI experience.

---------

Co-authored-by: RC-CHN <1051989940@qq.com>

* feat(api): add GET file endpoint and update file route to support multiple methods (AstrBotDevs#6874)

* fix(openapi): rename route view function

* fix(ui): include vuetify radiobox icons (AstrBotDevs#6892)

Add the radiobox icons used indirectly by Vuetify internals
to the required MDI subset so they are kept during font
generation.

Regenerate the subset CSS and font files to prevent missing
radio button icons at runtime.

* fix(tests): update scanUsedIcons tests to include required radio icons (AstrBotDevs#6894)

* doc: Update docs/zh/platform/lark.md (AstrBotDevs#6897)

* 补充飞书配置群聊机器人的部分

- 移除了 im:message:send 权限,因为似乎飞书已经移除了该权限
- 新增关于飞书群聊如何配置权限的部分

* Update docs/zh/platform/lark.md

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Feat(webui): show plugin author on cards & pinned item (AstrBotDevs#5802) (AstrBotDevs#6875)

* feat: 为卡片视图增加作者信息

* feat:置顶列表面板新增作者名称与插件名称

* docs(compshare): correct typos (AstrBotDevs#6878)

* Fix(WebUi): allow batch resetting provider config to "follow" (iss#6749) (AstrBotDevs#6825)

* feat(webui): use explicit 'follow' status for provider settings and improve batch operation logic

* fix: allow batch resetting provider config to "follow config"

* fix(AstrBotDevs#6749): use a unique constant for 'follow' status to avoid collisions with provider IDs

* fix: remove config.use_reloader = True

* refactor(ui): extract follow config sentinel constant

---------

Co-authored-by: RC-CHN <1051989940@qq.com>

* fix: keep weixin_oc polling after inbound timeouts (AstrBotDevs#6915)

* fix: keep weixin_oc polling after inbound timeouts

* Delete tests/test_weixin_oc_adapter.py

---------

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* fix(i18n): update OpenAI embedding hint for better compatibility guidance

fixes: AstrBotDevs#6855

* feat: auto-append /v1 to embedding_api_base in OpenAI embedding provider (AstrBotDevs#6863)

* fix: auto-append /v1 to embedding_api_base in OpenAI embedding provider (AstrBotDevs#6855)

When users configure `embedding_api_base` without the `/v1` suffix,
the OpenAI SDK does not auto-complete it, causing request path errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: ensure API base URL for OpenAI embedding ends with /v1 or /v4

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Soulter <905617992@qq.com>

* Fix payload handling for msg_id in QQ API (AstrBotDevs#6604)

Remove msg_id from payload to prevent errors with proactive tool-call path and avoid permission issues.

Co-authored-by: Naer <88199249+V-YOP@users.noreply.github.com>

* fix(provider): add missing index field to streaming tool_call deltas (AstrBotDevs#6661) (AstrBotDevs#6692)

* fix(provider): add missing index field to streaming tool_call deltas

- Fix AstrBotDevs#6661: Streaming tool_call arguments lost when OpenAI-compatible proxy omits index field
- Gemini and some proxies (e.g. Continue) don't include index field in tool_call deltas
- Add default index=0 when missing to prevent ChatCompletionStreamState.handle_chunk() from rejecting chunks

Fixes AstrBotDevs#6661

* fix(provider): use enumerate for multi-tool-call index assignment

- Use enumerate() to assign correct index based on list position
- Iterate over all choices (not just the first) for completeness
- Addresses review feedback from sourcery-ai and gemini-code-assist

---------

Co-authored-by: Yaohua-Leo <3067173925@qq.com>
Co-authored-by: Soulter <905617992@qq.com>

* feat(skills): enhance skill installation to support multiple top-level folders and add duplicate handling, and Chinese skill name support (AstrBotDevs#6952)

* feat(skills): enhance skill installation to support multiple top-level folders and add duplicate handling

closes: AstrBotDevs#6949

* refactor(skill_manager): streamline skill name normalization and validation logic

* fix(skill_manager): update skill name regex to allow underscores in skill names

* fix(skill_manager): improve skill name normalization and validation logic

* chore: bump version to 4.22.1

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: JiangNan <1394485448@qq.com>
Co-authored-by: Soulter <905617992@qq.com>
Co-authored-by: LIghtJUNction <lightjunction.me@gmail.com>
Co-authored-by: Ruochen Pan <1051989940@qq.com>
Co-authored-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
Co-authored-by: Rhonin Wang <33801807+RhoninSeiei@users.noreply.github.com>
Co-authored-by: RhoninSeiei <RhoninSeiei@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: YYMa <118096301+YuanyuanMa03@users.noreply.github.com>
Co-authored-by: linzhengtian <907305684@qq.com>
Co-authored-by: whatevertogo <149563971+whatevertogo@users.noreply.github.com>
Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
Co-authored-by: jnMetaCode <1394485448@qq.com>
Co-authored-by: shuiping233 <49360196+shuiping233@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: 鸦羽 <Raven95676@gmail.com>
Co-authored-by: Yufeng He <40085740+universeplayer@users.noreply.github.com>
Co-authored-by: camera-2018 <40380042+camera-2018@users.noreply.github.com>
Co-authored-by: 糯米茨 <143102889+nuomicici@users.noreply.github.com>
Co-authored-by: エイカク <1259085392z@gmail.com>
Co-authored-by: Ruochen Pan <sorainygreen@gmail.com>
Co-authored-by: Scofield <59475095+shijianhuai@users.noreply.github.com>
Co-authored-by: shijianhuai <shijianhuai@simuwang.com>
Co-authored-by: machina <53079908+machinad@users.noreply.github.com>
Co-authored-by: machina <1531829828@qq.com>
Co-authored-by: leonforcode <leonbeyourside01@gmail.com>
Co-authored-by: daniel5u <danielsuuuuuu@gmail.com>
Co-authored-by: letr <123731298+letr007@users.noreply.github.com>
Co-authored-by: Helian Nuits <sxp20061207@163.com>
Co-authored-by: RichardLiu <97330937+RichardLiuda@users.noreply.github.com>
Co-authored-by: _Kerman <kermanx@qq.com>
Co-authored-by: Gargantua <124801228+catDforD@users.noreply.github.com>
Co-authored-by: Gargantua <22532097@zju.edu.cn>
Co-authored-by: 晴空 <3103908461@qq.com>
Co-authored-by: SJ <idiotgyz@gmail.com>
Co-authored-by: idiotsj <idiotsj@users.noreply.github.com>
Co-authored-by: qingyun <codingtsunami@gmail.com>
Co-authored-by: ccsang <ccsang@users.noreply.github.com>
Co-authored-by: BillionToken <hydr0codone@proton.me>
Co-authored-by: BillionClaw <billionclaw@cl OSS.dev>
Co-authored-by: LIU Yaohua <12531035@mail.sustech.edu.cn>
Co-authored-by: LehaoLin <linlehao@cuhk.edu.cn>
Co-authored-by: Stable Genius <stablegenius043@gmail.com>
Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>
Co-authored-by: stablegenius49 <185121704+stablegenius49@users.noreply.github.com>
Co-authored-by: Lockinwize Lolite <mzwing@mzwing.eu.org>
Co-authored-by: Waterwzy <2916963017@qq.com>
Co-authored-by: M1LKT <144798909+M1LKT@users.noreply.github.com>
Co-authored-by: Chen <42998804+a61995987@users.noreply.github.com>
Co-authored-by: Frank <97429702+tsubasakong@users.noreply.github.com>
Co-authored-by: bread <104435263+bread-ovO@users.noreply.github.com>
Co-authored-by: Stardust <1441308506a@gmail.com>
Co-authored-by: Vorest <147138388+Vorest3679@users.noreply.github.com>
Co-authored-by: GH <BoneAsh@iCloud.com>
Co-authored-by: Zeng Qingwen <143274079+fishwww-ww@users.noreply.github.com>
Co-authored-by: Rainor_da! <51012640+1zzxy1@users.noreply.github.com>
Co-authored-by: Izayoi9 <105905446+Izayoi9@users.noreply.github.com>
Co-authored-by: naer-lily <88199249+naer-lily@users.noreply.github.com>
Co-authored-by: Naer <88199249+V-YOP@users.noreply.github.com>
Co-authored-by: Yaohua-Leo <3067173925@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. lgtm This PR has been approved by a maintainer size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants