Skip to content

feat: integrates KOOK platform adapter#5658

Merged
Soulter merged 37 commits intoAstrBotDevs:masterfrom
shuiping233:feat/astrbot_plugin_kook_adapter
Mar 3, 2026
Merged

feat: integrates KOOK platform adapter#5658
Soulter merged 37 commits intoAstrBotDevs:masterfrom
shuiping233:feat/astrbot_plugin_kook_adapter

Conversation

@shuiping233
Copy link
Contributor

@shuiping233 shuiping233 commented Mar 2, 2026

astrbot_plugin_kook_adapter引入astrbot官方适配器目录中,
相关链接:

Modifications / 改动点

  • 将astrbot_plugin_kook_adapter的实现放入astrbot/core/platform/sources/kook
  • CONFIG_METADATA_2里加了kook适配器的相关配置项
  • astrbot/core/platform/manager.py里实现了kook适配器的正常导入功能
  • This is NOT a breaking change. / 这不是一个破坏性变更。

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

[14:46:43.751] [Core] [INFO] [platform.manager:125]: 载入 kook(kook) 平台适配器 ...
[14:46:43.845] [Core] [DBUG] [platform.register:60]: 平台适配器 kook 已注册
[14:46:45.903] [Core] [INFO] [kook.kook_adapter:62]: [KOOK] 启动KOOK适配器
[14:46:45.903] [Core] [INFO] [core.core_lifecycle:297]: AstrBot 启动完成。
[14:46:45.905] [Core] [INFO] [kook.kook_adapter:98]: [KOOK] 尝试连接KOOK服务器...
[14:46:46.293] [Core] [INFO] [kook.kook_client:92]: [KOOK] 获取gateway成功: wss://nws.kaiheila.cn/gateway?compress=1&token=djsaodjisajdiosajdiojioqwje=
[14:46:46.483] [Core] [INFO] [kook.kook_client:62]: [KOOK] 获取机器人账号ID成功: xxxxxxxxxxxx
[14:46:46.535] [Core] [INFO] [utils.llm_metadata:63]: Successfully fetched metadata for 1675 LLMs.
[14:46:47.139] [Core] [INFO] [kook.kook_client:118]: [KOOK] WebSocket 连接成功
Snipaste_2026-03-02_14-50-24 Snipaste_2026-03-02_14-50-06 image image

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.

Summary by Sourcery

添加一方 KOOK 平台适配器,并将其集成到 AstrBot 的平台系统和配置元数据中。

新功能:

  • 引入 KOOK 平台适配器实现,通过 WebSocket 连接,接收事件,并发送多种消息类型,包括文本、图片、文件、音频、视频和卡片消息。
  • 添加 KOOK 专用客户端、事件处理和类型定义,用于将 KOOK API、消息格式和卡片结构映射到 AstrBot 的内部消息模型。
  • 提供 KOOK 适配器配置模型,包含重连、心跳、失败处理和令牌设置,并在默认配置元数据中暴露相应选项。
  • 在平台管理器中注册 KOOK 适配器,使其能够与现有平台适配器一起启用和加载。

测试:

  • 为 KOOK 事件消息封装逻辑添加单元测试,以覆盖不同 AstrBot 消息组件及其到 KOOK 消息类型的映射。
  • 为 KOOK 卡片消息类型和容器序列化添加单元测试,以验证生成的 KOOK 卡片 JSON 结构。
Original summary in English

Summary by Sourcery

Add a first-party KOOK platform adapter and integrate it into AstrBot's platform system and configuration metadata.

New Features:

  • Introduce a KOOK platform adapter implementation that connects via WebSocket, receives events, and sends various message types including text, images, files, audio, video, and card messages.
  • Add KOOK-specific client, event handling, and type definitions to map KOOK APIs, message formats, and card structures to AstrBot's internal message model.
  • Provide a KOOK adapter configuration model with reconnection, heartbeat, failure handling, and token settings, and expose corresponding options in the default configuration metadata.
  • Register the KOOK adapter in the platform manager so it can be enabled and loaded alongside existing platform adapters.

Tests:

  • Add unit tests for KOOK event message wrapping logic to cover different AstrBot message components and their mapping to KOOK message types.
  • Add unit tests for KOOK card message types and container serialization to validate generated KOOK card JSON structures.

新功能:

  • 添加 KOOK 平台适配器实现,通过 WebSocket 连接 KOOK,处理事件,并发送包括文本、图片、文件、音频、视频和卡片消息在内的消息。
  • 引入 KOOK 专用的客户端、事件和类型定义,将 KOOK 的 API、消息格式和卡片结构映射到 AstrBot 的内部消息模型中。
  • 提供 KOOK 适配器配置模块,涵盖心跳、重连、日志记录、错误处理、性能和安全选项。
  • 在平台管理器和默认配置元数据中注册 KOOK 适配器,使其能够与其他平台一起配置和加载。
Original summary in English

Summary by Sourcery

添加一方 KOOK 平台适配器,并将其集成到 AstrBot 的平台系统和配置元数据中。

新功能:

  • 引入 KOOK 平台适配器实现,通过 WebSocket 连接,接收事件,并发送多种消息类型,包括文本、图片、文件、音频、视频和卡片消息。
  • 添加 KOOK 专用客户端、事件处理和类型定义,用于将 KOOK API、消息格式和卡片结构映射到 AstrBot 的内部消息模型。
  • 提供 KOOK 适配器配置模型,包含重连、心跳、失败处理和令牌设置,并在默认配置元数据中暴露相应选项。
  • 在平台管理器中注册 KOOK 适配器,使其能够与现有平台适配器一起启用和加载。

测试:

  • 为 KOOK 事件消息封装逻辑添加单元测试,以覆盖不同 AstrBot 消息组件及其到 KOOK 消息类型的映射。
  • 为 KOOK 卡片消息类型和容器序列化添加单元测试,以验证生成的 KOOK 卡片 JSON 结构。
Original summary in English

Summary by Sourcery

Add a first-party KOOK platform adapter and integrate it into AstrBot's platform system and configuration metadata.

New Features:

  • Introduce a KOOK platform adapter implementation that connects via WebSocket, receives events, and sends various message types including text, images, files, audio, video, and card messages.
  • Add KOOK-specific client, event handling, and type definitions to map KOOK APIs, message formats, and card structures to AstrBot's internal message model.
  • Provide a KOOK adapter configuration model with reconnection, heartbeat, failure handling, and token settings, and expose corresponding options in the default configuration metadata.
  • Register the KOOK adapter in the platform manager so it can be enabled and loaded alongside existing platform adapters.

Tests:

  • Add unit tests for KOOK event message wrapping logic to cover different AstrBot message components and their mapping to KOOK message types.
  • Add unit tests for KOOK card message types and container serialization to validate generated KOOK card JSON structures.

@dosubot dosubot bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Mar 2, 2026
@gemini-code-assist
Copy link
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 适配器集成到 AstrBot 的官方适配器目录中,显著扩展 AstrBot 的平台兼容性。这一变更使得 AstrBot 能够直接支持 Kook 平台的消息交互,从而为用户提供更广泛的部署和使用选择。核心改动包括添加 Kook 相关的配置、实现适配器的加载机制以及提供完整的 Kook 消息处理和发送功能。

Highlights

  • Kook 适配器集成: 将 astrbot_plugin_kook_adapter 插件正式并入 AstrBot 官方适配器目录,扩展了平台兼容性。
  • 配置更新: 在 astrbot/core/config/default.py 中新增了 Kook 适配器的默认配置项,方便用户启用和配置。
  • 适配器加载机制: 在 astrbot/core/platform/manager.py 中实现了 Kook 适配器的动态导入和加载逻辑。
  • 核心实现文件: 新增了 astrbot/core/platform/sources/kook 目录,包含了 Kook 适配器的核心实现文件,包括客户端、事件处理和类型定义。
Changelog
  • astrbot/core/config/default.py
    • 新增了 Kook 适配器的默认配置模板,包括 id, type, enable, token 等字段。
  • astrbot/core/platform/manager.py
    • load_platform 异步函数中添加了 case "kook" 逻辑,以动态导入 KookPlatformAdapter
  • astrbot/core/platform/sources/kook/config.py
    • 新增文件,定义了 Kook 适配器的详细配置,包括连接、日志、错误处理、性能和安全相关的设置。
  • astrbot/core/platform/sources/kook/kook_adapter.py
    • 新增文件,实现了 KookPlatformAdapter 类,负责 Kook 平台的消息接收、转换、事件处理以及与 Kook 客户端的交互。
  • astrbot/core/platform/sources/kook/kook_client.py
    • 新增文件,实现了 KookClient 类,用于管理与 Kook WebSocket 网关的连接、心跳机制、消息监听、API 请求(如获取机器人 ID、网关 URL、发送消息、上传文件)以及重连逻辑。
  • astrbot/core/platform/sources/kook/kook_event.py
    • 新增文件,实现了 KookEvent 类,继承自 AstrMessageEvent,负责将 AstrBot 的消息链转换为 Kook 平台可识别的消息格式,并处理不同类型的消息组件(文本、图片、视频、文件、录音、@用户、@所有人、回复、卡片消息)。
  • astrbot/core/platform/sources/kook/kook_types.py
    • 新增文件,定义了 Kook API 路径、Kook 消息类型枚举以及用于构建 Kook 卡片消息的 Pydantic 数据模型。
Activity
  • 此拉取请求由 shuiping233 创建,旨在将 Kook 适配器集成到 AstrBot 官方目录。
  • 拉取请求描述中包含了详细的改动点、验证步骤和运行截图,展示了 Kook 适配器成功启动并连接到服务器的日志。
  • 作者已确认此变更为非破坏性变更,并提供了检查清单的完成状态。
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 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 2, 2026
@dosubot
Copy link

dosubot bot commented Mar 2, 2026

Related Documentation

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

How did I do? Any feedback?  Join Discord

Copy link
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 个问题,并给出了一些高层面的反馈:

  • 新的 kook/config.py 定义了大量配置(心跳、重连、日志等),但这些配置都没有真正被 KookClient 或适配器使用;建议要么通过将这些配置值接入 KookClient(例如通过构造函数/关键字参数),要么删除该文件,以避免产生无用或重复的配置。
  • KookPlatformAdapter.handle_msg 中,唤醒检测依赖于硬编码的 bot_nickname = "astrbot";更稳妥的做法是从 KOOK 或配置中推断机器人的名称/ID,而不是使用常量,这样在不同机器人名称的部署环境中也能正常工作。
  • 存在两处不同的 MessageType 导入(kook_client.py 中从 astrbot.core.platform.message_type 导入,而 kook_event.py 中从 astrbot.core.platform 导入),这可能会造成困惑;建议在新的 KOOK 模块中统一 MessageType 的导入来源。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- 新的 `kook/config.py` 定义了大量配置(心跳、重连、日志等),但这些配置都没有真正被 `KookClient` 或适配器使用;建议要么通过将这些配置值接入 `KookClient`(例如通过构造函数/关键字参数),要么删除该文件,以避免产生无用或重复的配置。
-`KookPlatformAdapter.handle_msg` 中,唤醒检测依赖于硬编码的 `bot_nickname = "astrbot"`;更稳妥的做法是从 KOOK 或配置中推断机器人的名称/ID,而不是使用常量,这样在不同机器人名称的部署环境中也能正常工作。
- 存在两处不同的 `MessageType` 导入(`kook_client.py` 中从 `astrbot.core.platform.message_type` 导入,而 `kook_event.py` 中从 `astrbot.core.platform` 导入),这可能会造成困惑;建议在新的 KOOK 模块中统一 `MessageType` 的导入来源。

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="275-284" />
<code_context>
+    async def reconnect(self):
</code_context>
<issue_to_address>
**issue (bug_risk):** 在关闭 HTTP 客户端后进行重连会复用一个已关闭的 `ClientSession`,这很可能会失败。

在 `reconnect` 中,`await self.close()` 会关闭 `self._http_client`,但 `connect()` 随后调用的 `get_gateway_url`/`get_bot_id` 会使用 `self._http_client.get(...)`,在已关闭的 `ClientSession` 上调用会抛出异常。可以考虑在 `reconnect` 中避免关闭 HTTP 客户端,或者在调用 `connect()` 之前重新创建一个新的 `aiohttp.ClientSession`(或以其他方式确保使用新的会话)。
</issue_to_address>

### Comment 2
<location path="astrbot/core/platform/sources/kook/kook_event.py" line_range="47-56" />
<code_context>
+        self._file_message_counter = 0
</code_context>
<issue_to_address>
**suggestion (bug_risk):** `_file_message_counter` 没有在每次发送时重置,因此跨多条消息的文件上传日志可能会不准确。

由于 `_file_message_counter` 只在 `__init__` 中初始化,在 `send` 中从未重置,一旦你发送过带文件的消息,之后每一次 `send` 都会看到一个非零值,并按有文件上传的情况进行日志记录,即使当前并没有上传文件。应在 `send` 的开始或结束处重置 `_file_message_counter`,这样日志只反映当前消息所携带的文件。

Suggested implementation:

```python
        self.astrbot_message_type: MessageType = message_obj.type
        self._file_message_counter = 0

    def _reset_file_message_counter(self) -> None:
        """Reset the per-send file upload counter.

        This should be called at the start of each send() so that logging for
        file uploads only reflects the files in the current outgoing message.
        """
        self._file_message_counter = 0

    def _warp_message(
        self, index: int, message_component: BaseMessageComponent
    ) -> Coroutine[Any, Any, OrderMessage]:

```

为了完整实现你所描述的行为,确保 `send()`(负责发送消息的方法)在每次发送开始时调用 `self._reset_file_message_counter()`。例如:

```python
    async def send(...):
        self._reset_file_message_counter()
        ...
```

这可以确保 `_file_message_counter` 会在每一条要发送的消息前被重置,从而让关于文件上传的日志以每条消息为粒度,而不是在多条消息之间累积。
</issue_to_address>

### Comment 3
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="96" />
<code_context>
+    """click 代表用户点击的事件,默认为"",代表无任何事件。"""
+
+
+AnyElement = PlainTextElement | KmarkdownElement | ImageElement | ButtonElement | str
+
+
</code_context>
<issue_to_address>
**issue (complexity):** 建议用更简单的基于 protocol 的类型和 dataclass 封装替换大型的 `AnyModule`/`AnyElement` 联合类型以及 `list` 子类容器,在保持现有行为的前提下降低理解成本。

你可以保留完整的卡片模型,同时通过简化类型组合和容器模式来减少认知负担,而不会改变行为。

### 1. 用公共基类替换大型 `AnyModule`/`AnyElement` 联合类型

当前实现:

```python
class KookCardModelBase:
    """卡片模块基类"""
    type: str

@dataclass
class HeaderModule(KookCardModelBase): ...
@dataclass
class SectionModule(KookCardModelBase): ...
# ...

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

AnyElement = PlainTextElement | KmarkdownElement | ImageElement | ButtonElement | str

class KookCardMessage(BaseModel):
    modules: list[AnyModule] = field(default_factory=list)
```

这些联合类型很大,而且由于模块本身是 dataclass 而不是 pydantic model,它们并没有被用于运行时的校验。你可以通过把基类改成合适的 protocol/model,并把 `modules` 声明为该基类类型,而不是显式的联合类型,从而降低复杂度:

```python
from typing import Protocol

class KookCardModule(Protocol):
    type: str

# All your dataclasses implicitly satisfy this protocol:
# @dataclass
# class HeaderModule(KookCardModule): ...

class KookCardMessage(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    type: str = "card"
    theme: ThemeType | None = None
    size: SizeType | None = None
    color: KookCardColor | None = None
    modules: list[KookCardModule] = field(default_factory=list)

    def add_module(self, module: KookCardModule):
        self.modules.append(module)
```

如果你实际上还没有在任何地方使用 `AnyElement`,也可以类似地定义一个 `Element` protocol(或者在不需要的地方直接删掉这个别名),以避免在类型注解中出现大型联合类型。

这样可以保留所有现有的类和行为,同时不再需要在每次新增模块/元素时手动保持 `AnyModule`/`AnyElement` 与之同步。

### 2. 避免让 `KookCardMessageContainer` 继承 `list`

继承 `list` 会带来额外的心智负担,但收益很有限。你可以改成一个带显式 `cards` 字段的简单封装类,同时保持现有的外部行为(`to_json``append`):

当前实现:

```python
class KookCardMessageContainer(list[KookCardMessage]):
    """卡片消息容器(列表),此类型可以直接to_json后发送出去"""

    def append(self, object: KookCardMessage) -> None:
        return super().append(object)

    def to_json(self, indent: int | None = None, ensure_ascii: bool = True) -> str:
        return json.dumps(
            [i.to_dict() for i in self], indent=indent, ensure_ascii=ensure_ascii
        )
```

简化方案:

```python
from dataclasses import dataclass, field

@dataclass
class KookCardMessageContainer:
    cards: list[KookCardMessage] = field(default_factory=list)

    def append(self, card: KookCardMessage) -> None:
        self.cards.append(card)

    def to_json(self, indent: int | None = None, ensure_ascii: bool = True) -> str:
        return json.dumps(
            [card.to_dict() for card in self.cards],
            indent=indent,
            ensure_ascii=ensure_ascii,
        )
```

现有调用代码:

```python
container = KookCardMessageContainer()
container.append(card)
payload = container.to_json()
```

仍然可以按原样工作,但这个类型现在是一个简单、语义清晰的容器,而不是一个特化的 `list` 子类。
</issue_to_address>

### Comment 4
<location path="astrbot/core/platform/sources/kook/config.py" line_range="6" />
<code_context>
+包含连接参数、重连策略、心跳设置等配置项
+"""
+
+# 连接配置
+CONNECTION_CONFIG = {
+    # 心跳配置
</code_context>
<issue_to_address>
**issue (complexity):** 建议将多个独立的配置字典和 getter 函数整合为一个带命名空间的 `CONFIG`,并通过一个统一的访问函数对外暴露,以简化配置的外部接口。

你可以在不删除任何配置项的前提下,通过把多个全局配置和 getter 合并为单一的、带命名空间的配置对象,来降低整体的配置复杂度。

与其暴露 5 个独立的字典和 3+ 个 getter 函数,不如保留所有键,但将它们归到一个 `CONFIG` 下,并使用单一访问 API:

```python
# config.py

CONFIG = {
    "connection": {
        "heartbeat_interval": 30,
        "heartbeat_timeout": 6,
        "max_heartbeat_failures": 3,
        "initial_reconnect_delay": 1,
        "max_reconnect_delay": 60,
        "max_consecutive_failures": 5,
        "websocket_timeout": 10,
        "connection_timeout": 30,
        "enable_compression": True,
        "max_message_size": 1024 * 1024,
    },
    "logging": {
        "level": "INFO",
        "format": "[KOOK] %(message)s",
        "enable_heartbeat_logs": False,
        "enable_message_logs": False,
    },
    "error_handling": {
        "retry_on_network_error": True,
        "retry_on_token_expired": True,
        "max_retry_attempts": 3,
        "retry_delay_base": 2,
    },
    "performance": {
        "enable_message_buffering": True,
        "buffer_size": 100,
        "enable_connection_pooling": True,
        "max_concurrent_requests": 10,
    },
    "security": {
        "verify_ssl": True,
        "enable_rate_limiting": True,
        "rate_limit_requests": 100,
        "rate_limit_window": 60,
    },
}


def get_config(section: str | None = None):
    """获取配置;section 为 None 时返回完整配置"""
    if section is None:
        return CONFIG
    return CONFIG[section]
```

使用方式变为:

```python
connection_cfg = get_config("connection")
heartbeat_interval = connection_cfg["heartbeat_interval"]
```

这样可以在保留当前所有配置项和默认值的同时:

- 将对外的公共 API 从多个全局字典/getter 收缩为一个易于理解的入口。
- 降低读者的概念负担:只需要理解一个带分区的配置对象,而不是许多顶层符号。
- 让你更容易逐步将所需的配置节/键接入 `KookClient`/`KookPlatformAdapter`,而不会给人这些顶层字典已经全部被使用的错觉。
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

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

  • The new kook/config.py defines extensive configuration (heartbeat, reconnect, logging, etc.) but none of it is actually used by KookClient or the adapter; consider either wiring these values into KookClient (e.g., via constructor/kwargs) or removing the file to avoid dead/duplicated configuration.
  • In KookPlatformAdapter.handle_msg, the wakeup detection relies on a hardcoded bot_nickname = "astrbot"; it would be more robust to infer the bot's name/ID from KOOK or config instead of a constant so deployments with different bot names still work correctly.
  • There are two different MessageType imports (astrbot.core.platform.message_type in kook_client.py and astrbot.core.platform in kook_event.py), which can be confusing; consider unifying the import source for MessageType across the new KOOK modules.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `kook/config.py` defines extensive configuration (heartbeat, reconnect, logging, etc.) but none of it is actually used by `KookClient` or the adapter; consider either wiring these values into `KookClient` (e.g., via constructor/kwargs) or removing the file to avoid dead/duplicated configuration.
- In `KookPlatformAdapter.handle_msg`, the wakeup detection relies on a hardcoded `bot_nickname = "astrbot"`; it would be more robust to infer the bot's name/ID from KOOK or config instead of a constant so deployments with different bot names still work correctly.
- There are two different `MessageType` imports (`astrbot.core.platform.message_type` in `kook_client.py` and `astrbot.core.platform` in `kook_event.py`), which can be confusing; consider unifying the import source for `MessageType` across the new KOOK modules.

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="275-284" />
<code_context>
+    async def reconnect(self):
</code_context>
<issue_to_address>
**issue (bug_risk):** Reconnecting after closing the HTTP client will use a closed `ClientSession` and likely fail.

In `reconnect`, `await self.close()` shuts down `self._http_client`, but `connect()` then calls `get_gateway_url`/`get_bot_id`, which use `self._http_client.get(...)` and will raise on a closed `ClientSession`. Either avoid closing the HTTP client in `reconnect`, or recreate a fresh `aiohttp.ClientSession` (or otherwise ensure a new session) before calling `connect()`.
</issue_to_address>

### Comment 2
<location path="astrbot/core/platform/sources/kook/kook_event.py" line_range="47-56" />
<code_context>
+        self._file_message_counter = 0
</code_context>
<issue_to_address>
**suggestion (bug_risk):** `_file_message_counter` is never reset per send, so logging about file uploads can be incorrect across messages.

Because `_file_message_counter` is only initialized in `__init__` and never reset in `send`, once you’ve sent a message with files, every later `send` will see a non-zero value and log as if it’s uploading files even when it isn’t. Reset `_file_message_counter` at the start or end of `send` so the log reflects only the current message’s files.

Suggested implementation:

```python
        self.astrbot_message_type: MessageType = message_obj.type
        self._file_message_counter = 0

    def _reset_file_message_counter(self) -> None:
        """Reset the per-send file upload counter.

        This should be called at the start of each send() so that logging for
        file uploads only reflects the files in the current outgoing message.
        """
        self._file_message_counter = 0

    def _warp_message(
        self, index: int, message_component: BaseMessageComponent
    ) -> Coroutine[Any, Any, OrderMessage]:

```

To fully implement the behavior you described, ensure that `send()` (the method responsible for sending a message) calls `self._reset_file_message_counter()` at the start of each send. For example:

```python
    async def send(...):
        self._reset_file_message_counter()
        ...
```

This guarantees that `_file_message_counter` is reset for every outgoing message, so any logging about file uploads is accurate per message rather than accumulating across messages.
</issue_to_address>

### Comment 3
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="96" />
<code_context>
+    """click 代表用户点击的事件,默认为"",代表无任何事件。"""
+
+
+AnyElement = PlainTextElement | KmarkdownElement | ImageElement | ButtonElement | str
+
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider replacing the large `AnyModule`/`AnyElement` unions and the `list` subclass container with simpler protocol-based types and a dataclass wrapper to lower cognitive overhead while preserving behavior.

You can keep the full card model but reduce the cognitive load by simplifying the type plumbing and container pattern without changing behavior.

### 1. Drop the big `AnyModule`/`AnyElement` unions in favor of a common base

Right now:

```python
class KookCardModelBase:
    """卡片模块基类"""
    type: str

@dataclass
class HeaderModule(KookCardModelBase): ...
@dataclass
class SectionModule(KookCardModelBase): ...
# ...

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

AnyElement = PlainTextElement | KmarkdownElement | ImageElement | ButtonElement | str

class KookCardMessage(BaseModel):
    modules: list[AnyModule] = field(default_factory=list)
```

The union types are large, and they’re not leveraged for runtime validation (since the modules themselves are dataclasses, not pydantic models). You can reduce complexity by making the base class a proper protocol/model and typing `modules` as the base instead of an explicit union:

```python
from typing import Protocol

class KookCardModule(Protocol):
    type: str

# All your dataclasses implicitly satisfy this protocol:
# @dataclass
# class HeaderModule(KookCardModule): ...

class KookCardMessage(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    type: str = "card"
    theme: ThemeType | None = None
    size: SizeType | None = None
    color: KookCardColor | None = None
    modules: list[KookCardModule] = field(default_factory=list)

    def add_module(self, module: KookCardModule):
        self.modules.append(module)
```

If you don’t actually use `AnyElement` anywhere yet, you can similarly define an `Element` protocol (or just drop the alias where it isn’t needed) to avoid the large union in the type hints.

This preserves all existing classes and behavior, but removes the need to keep `AnyModule`/`AnyElement` synchronized with every new module/element you add.

### 2. Avoid subclassing `list` for `KookCardMessageContainer`

Subclassing `list` adds mental overhead for little benefit. You can make this a simple wrapper with an explicit `cards` field while keeping the same external behavior (`to_json` and `append`):

Current:

```python
class KookCardMessageContainer(list[KookCardMessage]):
    """卡片消息容器(列表),此类型可以直接to_json后发送出去"""

    def append(self, object: KookCardMessage) -> None:
        return super().append(object)

    def to_json(self, indent: int | None = None, ensure_ascii: bool = True) -> str:
        return json.dumps(
            [i.to_dict() for i in self], indent=indent, ensure_ascii=ensure_ascii
        )
```

Simplified:

```python
from dataclasses import dataclass, field

@dataclass
class KookCardMessageContainer:
    cards: list[KookCardMessage] = field(default_factory=list)

    def append(self, card: KookCardMessage) -> None:
        self.cards.append(card)

    def to_json(self, indent: int | None = None, ensure_ascii: bool = True) -> str:
        return json.dumps(
            [card.to_dict() for card in self.cards],
            indent=indent,
            ensure_ascii=ensure_ascii,
        )
```

Existing call sites that did:

```python
container = KookCardMessageContainer()
container.append(card)
payload = container.to_json()
```

still work the same, but the type is now a simple, self-describing container instead of a specialized `list`.
</issue_to_address>

### Comment 4
<location path="astrbot/core/platform/sources/kook/config.py" line_range="6" />
<code_context>
+包含连接参数、重连策略、心跳设置等配置项
+"""
+
+# 连接配置
+CONNECTION_CONFIG = {
+    # 心跳配置
</code_context>
<issue_to_address>
**issue (complexity):** Consider consolidating the separate configuration dictionaries and getters into a single namespaced `CONFIG` with one access function to simplify the exposed configuration surface.

You can reduce the perceived/configuration surface complexity without removing any of the options by consolidating the multiple globals and getters into a single, namespaced config object.

Instead of exposing 5 separate dicts and 3+ getter functions, you can keep all keys but group them under one `CONFIG` with a single access API:

```python
# config.py

CONFIG = {
    "connection": {
        "heartbeat_interval": 30,
        "heartbeat_timeout": 6,
        "max_heartbeat_failures": 3,
        "initial_reconnect_delay": 1,
        "max_reconnect_delay": 60,
        "max_consecutive_failures": 5,
        "websocket_timeout": 10,
        "connection_timeout": 30,
        "enable_compression": True,
        "max_message_size": 1024 * 1024,
    },
    "logging": {
        "level": "INFO",
        "format": "[KOOK] %(message)s",
        "enable_heartbeat_logs": False,
        "enable_message_logs": False,
    },
    "error_handling": {
        "retry_on_network_error": True,
        "retry_on_token_expired": True,
        "max_retry_attempts": 3,
        "retry_delay_base": 2,
    },
    "performance": {
        "enable_message_buffering": True,
        "buffer_size": 100,
        "enable_connection_pooling": True,
        "max_concurrent_requests": 10,
    },
    "security": {
        "verify_ssl": True,
        "enable_rate_limiting": True,
        "rate_limit_requests": 100,
        "rate_limit_window": 60,
    },
}


def get_config(section: str | None = None):
    """获取配置;section 为 None 时返回完整配置"""
    if section is None:
        return CONFIG
    return CONFIG[section]
```

Usage becomes:

```python
connection_cfg = get_config("connection")
heartbeat_interval = connection_cfg["heartbeat_interval"]
```

This keeps all current configuration options and values intact but:

- Shrinks the public API from multiple globals/getters to a single, easy-to-understand entry point.
- Reduces the conceptual overhead for readers: one config object with sections, instead of many top-level symbols.
- Makes it easier to gradually wire only the needed sections/keys into `KookClient`/`KookPlatformAdapter` without implying that each top-level dict is already fully honored.
</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.

Copy link
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 successfully integrates the KOOK adapter into AstrBot. However, a high-severity Path Traversal / Local File Inclusion (LFI) vulnerability was identified in the handling of card messages and asset uploads, where untrusted image sources from received messages could allow reading arbitrary files from the bot's host system. It is recommended to validate external URLs and restrict local file access. Furthermore, improvements are needed for hardcoded configurations, error handling, a serious base64 parsing bug, and some naming issues to enhance the code's robustness, maintainability, and configurability.

@shuiping233
Copy link
Contributor Author

@Soulter 请问一下,如果我这边有关于适配器的单元测试,那这些单测应该放哪里呢,还是说单测先不放进来?

@Soulter
Copy link
Member

Soulter commented Mar 2, 2026

@Soulter 请问一下,如果我这边有关于适配器的单元测试,那这些单测应该放哪里呢,还是说单测先不放进来?

放到tests下就行了

@shuiping233
Copy link
Contributor Author

@Soulter 请问一下,如果我这边有关于适配器的单元测试,那这些单测应该放哪里呢,还是说单测先不放进来?

放到tests下就行了

那要不我开一个tests/test_platform_adapters/ ? 这边单测是有几个文件来着)

@Soulter
Copy link
Member

Soulter commented Mar 2, 2026

@Soulter 请问一下,如果我这边有关于适配器的单元测试,那这些单测应该放哪里呢,还是说单测先不放进来?

放到tests下就行了

那要不我开一个tests/test_platform_adapters/ ? 这边单测是有几个文件来着)

只包含 kook 相关测试吗?先放在 tests/test_kook/

@shuiping233
Copy link
Contributor Author

@Soulter 请问一下,如果我这边有关于适配器的单元测试,那这些单测应该放哪里呢,还是说单测先不放进来?

放到tests下就行了

那要不我开一个tests/test_platform_adapters/ ? 这边单测是有几个文件来着)

只包含 kook 相关测试吗?先放在 tests/test_kook/

好的)

@shuiping233
Copy link
Contributor Author

shuiping233 commented Mar 2, 2026

@gemini-code-assist /gemini review

@shuiping233
Copy link
Contributor Author

@sourcery-ai review

Copy link
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 个问题,并给出了一些整体性的反馈:

  • KookClient.upload_asset 中,用于记录 target_path.as_posix()except 代码块在 target_path 被保证定义之前就会执行,这会额外抛出 UnboundLocalError —— 建议单独定义一个原始路径字符串变量,并在异常处理程序中避免引用 target_path
  • KookConfig 中定义并通过 CONFIG_METADATA_2 暴露的多个配置字段(例如 reconnect_delaymax_reconnect_delaymax_retry_delay)实际上并没有在 _main_loopKookClient 的重连逻辑中使用,目前的实现是硬编码的指数退避;建议要么把这些配置接入退避计算逻辑,要么移除它们,以避免产生令人困惑且无效的配置项。
给 AI Agents 的提示
Please address the comments from this code review:

## Overall Comments
- In `KookClient.upload_asset`, the `except` block that logs `target_path.as_posix()` executes before `target_path` is guaranteed to be defined, which will raise an additional `UnboundLocalError` — define a separate variable for the raw path string and avoid referencing `target_path` inside the exception handler.
- Several configuration fields defined in `KookConfig` and exposed via `CONFIG_METADATA_2` (e.g. `reconnect_delay`, `max_reconnect_delay`, `max_retry_delay`) are not actually used in the reconnect logic in `_main_loop` or `KookClient`, which currently hard-codes an exponential backoff; consider wiring these values into the backoff calculation or removing them to avoid confusing, ineffective settings.

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="60-69" />
<code_context>
+            else:
+                file_url = file_url.removeprefix("file://")
+
+            try:
+                target_path = Path(file_url)
+                target_path = target_path.resolve()
+            except Exception as exp:
+                logger.error(
+                    f'[KOOK] 获取文件 "{target_path.as_posix()}" 绝对路径失败: "{exp}"'
</code_context>
<issue_to_address>
**issue (bug_risk):** Exception handler in file:// branch uses target_path before it is guaranteed to be defined.

If `Path(file_url)` or `.resolve()` fails, `target_path` is never set, so the `except` block’s use of `target_path` will raise an `UnboundLocalError` and hide the original exception. Use `file_url` (or another variable defined before the `try`) in the log and user-facing message instead of `target_path` when path resolution fails.
</issue_to_address>

### Comment 2
<location path="tests/test_kook/shared.py" line_range="4" />
<code_context>
+from pathlib import Path
+
+
+TEST_DATA_DIR = Path("./tests/test_kook/data")
</code_context>
<issue_to_address>
**nitpick (testing):** Use a path relative to this file instead of the current working directory for test data

Using `Path("./tests/test_kook/data")` ties this to the repo root and can break when tests are run from other working directories. Prefer something like `TEST_DATA_DIR = Path(__file__).parent / "data"` so the path is resolved relative to this file instead.
</issue_to_address>

### Comment 3
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="55" />
<code_context>
+    """16 进制色值"""
+
+
+class KookCardModelBase:
+    """卡片模块基类"""
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider trimming the Kook card type definitions in this file down to only the currently used pieces (e.g., FileModule and a minimal KookCardMessage) and moving the full schema to a dedicated module for future use.

You can reduce complexity here by scoping the card model to only what’s currently needed, and deferring the full schema until it’s actually used.

Concretely:

1. **Keep only used module types in this file**

Right now, only `FileModule` is used. You can keep the rest of the structure the same, but trim unused module/element types and the broad union:

```python
# Keep
@dataclass
class FileModule(KookCardModelBase):
    src: str
    title: str = ""
    type: Literal["file", "audio", "video"] = "file"
    cover: str | None = None

# Replace AnyModule with a minimal union for now
AnyModule = FileModule
```

This keeps the public surface compatible (`AnyModule` still exists) but avoids the cognitive load of unused modules.

2. **Move the full schema to a dedicated module when needed**

If you want to preserve the full type model for future work, move it out of this integration file into a separate module, and only import what’s needed:

```python
# kook_card_types.py
from pydantic.dataclasses import dataclass

class KookCardModelBase:
    type: str

@dataclass
class FileModule(KookCardModelBase):
    src: str
    title: str = ""
    type: Literal["file", "audio", "video"] = "file"
    cover: str | None = None

# keep other modules here but do not re-export them until they’re used
__all__ = ["FileModule"]
```

```python
# current file
from .kook_card_types import FileModule

AnyModule = FileModule  # or re-import broader unions later when needed
```

3. **Optionally simplify `KookCardMessage` if only file/audio cards are used**

If the only current usage is sending a single audio/file module, you can simplify the model further:

```python
class KookCardMessage(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    type: str = "card"
    modules: list[FileModule] = field(default_factory=list)

    def add_file(self, src: str, *, title: str = "", cover: str | None = None):
        self.modules.append(FileModule(src=src, title=title, cover=cover))
```

This still supports the current behavior but expresses the actual constraints in use, and you can generalize back to `AnyModule` when more module types are added.
</issue_to_address>

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

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

  • In KookClient.upload_asset, the except block that logs target_path.as_posix() executes before target_path is guaranteed to be defined, which will raise an additional UnboundLocalError — define a separate variable for the raw path string and avoid referencing target_path inside the exception handler.
  • Several configuration fields defined in KookConfig and exposed via CONFIG_METADATA_2 (e.g. reconnect_delay, max_reconnect_delay, max_retry_delay) are not actually used in the reconnect logic in _main_loop or KookClient, which currently hard-codes an exponential backoff; consider wiring these values into the backoff calculation or removing them to avoid confusing, ineffective settings.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `KookClient.upload_asset`, the `except` block that logs `target_path.as_posix()` executes before `target_path` is guaranteed to be defined, which will raise an additional `UnboundLocalError` — define a separate variable for the raw path string and avoid referencing `target_path` inside the exception handler.
- Several configuration fields defined in `KookConfig` and exposed via `CONFIG_METADATA_2` (e.g. `reconnect_delay`, `max_reconnect_delay`, `max_retry_delay`) are not actually used in the reconnect logic in `_main_loop` or `KookClient`, which currently hard-codes an exponential backoff; consider wiring these values into the backoff calculation or removing them to avoid confusing, ineffective settings.

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="60-69" />
<code_context>
+            else:
+                file_url = file_url.removeprefix("file://")
+
+            try:
+                target_path = Path(file_url)
+                target_path = target_path.resolve()
+            except Exception as exp:
+                logger.error(
+                    f'[KOOK] 获取文件 "{target_path.as_posix()}" 绝对路径失败: "{exp}"'
</code_context>
<issue_to_address>
**issue (bug_risk):** Exception handler in file:// branch uses target_path before it is guaranteed to be defined.

If `Path(file_url)` or `.resolve()` fails, `target_path` is never set, so the `except` block’s use of `target_path` will raise an `UnboundLocalError` and hide the original exception. Use `file_url` (or another variable defined before the `try`) in the log and user-facing message instead of `target_path` when path resolution fails.
</issue_to_address>

### Comment 2
<location path="tests/test_kook/shared.py" line_range="4" />
<code_context>
+from pathlib import Path
+
+
+TEST_DATA_DIR = Path("./tests/test_kook/data")
</code_context>
<issue_to_address>
**nitpick (testing):** Use a path relative to this file instead of the current working directory for test data

Using `Path("./tests/test_kook/data")` ties this to the repo root and can break when tests are run from other working directories. Prefer something like `TEST_DATA_DIR = Path(__file__).parent / "data"` so the path is resolved relative to this file instead.
</issue_to_address>

### Comment 3
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="55" />
<code_context>
+    """16 进制色值"""
+
+
+class KookCardModelBase:
+    """卡片模块基类"""
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider trimming the Kook card type definitions in this file down to only the currently used pieces (e.g., FileModule and a minimal KookCardMessage) and moving the full schema to a dedicated module for future use.

You can reduce complexity here by scoping the card model to only what’s currently needed, and deferring the full schema until it’s actually used.

Concretely:

1. **Keep only used module types in this file**

Right now, only `FileModule` is used. You can keep the rest of the structure the same, but trim unused module/element types and the broad union:

```python
# Keep
@dataclass
class FileModule(KookCardModelBase):
    src: str
    title: str = ""
    type: Literal["file", "audio", "video"] = "file"
    cover: str | None = None

# Replace AnyModule with a minimal union for now
AnyModule = FileModule
```

This keeps the public surface compatible (`AnyModule` still exists) but avoids the cognitive load of unused modules.

2. **Move the full schema to a dedicated module when needed**

If you want to preserve the full type model for future work, move it out of this integration file into a separate module, and only import what’s needed:

```python
# kook_card_types.py
from pydantic.dataclasses import dataclass

class KookCardModelBase:
    type: str

@dataclass
class FileModule(KookCardModelBase):
    src: str
    title: str = ""
    type: Literal["file", "audio", "video"] = "file"
    cover: str | None = None

# keep other modules here but do not re-export them until they’re used
__all__ = ["FileModule"]
```

```python
# current file
from .kook_card_types import FileModule

AnyModule = FileModule  # or re-import broader unions later when needed
```

3. **Optionally simplify `KookCardMessage` if only file/audio cards are used**

If the only current usage is sending a single audio/file module, you can simplify the model further:

```python
class KookCardMessage(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    type: str = "card"
    modules: list[FileModule] = field(default_factory=list)

    def add_file(self, src: str, *, title: str = "", cover: str | None = None):
        self.modules.append(FileModule(src=src, title=title, cover=cover))
```

This still supports the current behavior but expresses the actual constraints in use, and you can generalize back to `AnyModule` when more module types are added.
</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.

Copy link
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 successfully integrates the KOOK adapter into AstrBot's core code, demonstrating high implementation quality with complete connection, heartbeat, reconnection logic, and proper adaptation to KOOK message formats. The clear code structure and inclusion of test cases are excellent practices. A security audit identified sensitive information leakage where the bot's authentication token is logged at DEBUG level during initialization and at INFO level when logging the gateway URL. This credential exposure needs to be remediated by sanitizing the logged data. Additionally, I've identified a few areas for improvement: there's a risk of improper handling of falsy values in the configuration file parsing logic, duplicate metric reporting in the send_by_session method, and an unused attribute during adapter initialization. Specific suggestions for these points are provided in the code comments. All original comments remain valid as they do not contradict the provided rule regarding asyncio atomicity.

@shuiping233
Copy link
Contributor Author

@gemini-code-assist /gemini review

@shuiping233
Copy link
Contributor Author

@sourcery-ai review

Copy link
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 适配器并入官方适配器目录,是一项重要的功能新增。整体实现非常完整,涵盖了配置、客户端逻辑、事件处理、国际化以及单元测试,代码质量很高。尤其是在 KookClient 的实现中,对心跳、重连机制以及文件上传安全性的处理都非常周到和健壮。我发现一处可以改进的地方,即关于 KookPlatformAdapter 中客户端的初始化生命周期,调整后可以使代码更加稳健。总体而言,这是一次出色的贡献。

Copy link
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 个问题,并给出了一些总体反馈:

  • KookPlatformAdapter._main_loop 中的重连逻辑目前是写死的指数退避(2**consecutive_failures,上限固定为 60 秒),并且忽略了 KookConfig 中的 reconnect_delay / max_reconnect_delay,如果基于这些配置字段来计算退避时间,会更清晰也更便于控制。
  • KookClient.connect 暴露了 resume 参数并跟踪 session_id / last_sn,但适配器从未调用过 connect(resume=True),也没有在重连流程中实现 resume 分支,这导致与 resume 相关的代码实际上处于死代码状态;建议要么在重连路径中接入 resume,要么移除未使用的参数/状态,以简化客户端。
面向 AI Agent 的提示
请根据这次代码评审中的评论进行修改:

## 总体评论
- `KookPlatformAdapter._main_loop` 中的重连逻辑目前是写死的指数退避(`2**consecutive_failures`,上限固定为 60 秒),并且忽略了 `KookConfig` 中的 `reconnect_delay` / `max_reconnect_delay`,如果基于这些配置字段来计算退避时间,会更清晰也更便于控制。
- `KookClient.connect` 暴露了 `resume` 参数并跟踪 `session_id` / `last_sn`,但适配器从未调用过 `connect(resume=True)`,也没有在重连流程中实现 resume 分支,这导致与 resume 相关的代码实际上处于死代码状态;建议要么在重连路径中接入 resume,要么移除未使用的参数/状态,以简化客户端。

## 具体评论

### 评论 1
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="258-259" />
<code_context>
+        """心跳循环"""
+        while self.running:
+            try:
+                # 随机化心跳间隔 (30±5秒)
+                interval = self.config.heartbeat_interval + random.randint(-5, 5)
+                await asyncio.sleep(interval)
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 随机化心跳间隔可能变为非正数,并且忽略了已配置的重连延迟。

随机化间隔 `config.heartbeat_interval + randint(-5, 5)``heartbeat_interval < 5` 时可能变为 0 或负数,从而导致接近忙循环的情况。可以通过将结果限制在一个最小值(例如 `interval = max(1, ...)`)来避免这个问题。另外,`KookConfig` 中的 `reconnect_delay` / `max_reconnect_delay` 从未被使用,即使 `_handle_hello` 的注释中提到了重连延迟;建议要么将它们整合进重连逻辑,要么如果不打算实现退避重连,就移除这些配置字段及相关注释,以避免困惑。

建议实现:

```python
    async def _heartbeat_loop(self):
        """心跳循环"""
        while self.running:
            try:
                # 随机化心跳间隔 (基于配置的 heartbeat_interval,加入 ±5 秒抖动),并保证为正数
                base_interval = getattr(self.config, "heartbeat_interval", 30)
                jitter = random.randint(-5, 5)
                interval = max(1, base_interval + jitter)
                await asyncio.sleep(interval)

import asyncio
import random
import aiofiles

```

```python
import aiohttp
import websockets

```

1. 检查重连逻辑(很可能在实现 `_handle_hello` 或 websocket 重连的地方),然后:
   - 使用 `KookConfig.reconnect_delay` / `KookConfig.max_reconnect_delay` 来实现重连尝试之间递增的退避时间,或者
   - 如果不需要退避重连,则移除这些配置字段以及所有引用它们的注释。
2. 如果在每次循环迭代中需要发送心跳负载(例如通过 websocket 发送),请确保发送调用放在 `_heartbeat_loop` 中的 `await asyncio.sleep(interval)` 之后。
</issue_to_address>

### 评论 2
<location path="astrbot/core/platform/sources/kook/kook_event.py" line_range="134-146" />
<code_context>
+                return handle_plain(index, "(met)all(met)")
+            case Reply():
+                return handle_plain(index, "", reply_id=message_component.id)
+            case Json():
+                json_data = message_component.data
+                # kook卡片json外层得是一个列表
+                if isinstance(json_data, dict):
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 当 Json 组件中已经包含 JSON 字符串时,存在二次编码的风险。

如果某个插件传入的是已经序列化好的 JSON 字符串(例如他们事先构建好的 KOOK 卡片),当前逻辑会再次对其执行 `json.dumps`,从而发送一个被加了引号的 JSON 字符串,而不是预期的卡片数组。可以考虑在检测到输入是 `str` 时直接透传,仅在输入为 dict/list(或其他 Python 结构)时再做封装/序列化,以避免二次编码。

```suggestion
            case Json():
                json_data = message_component.data
                # 如果插件已经传入预序列化的 JSON 字符串(例如完整的 KOOK 卡片数组),直接透传,避免二次编码
                if isinstance(json_data, str):
                    encoded_json = json_data
                else:
                    # kook卡片json外层得是一个列表
                    if isinstance(json_data, dict):
                        json_data = [json_data]
                    # 考虑到kook可能会更改消息结构,为了能让插件开发者
                    # 自行根据kook文档描述填卡片json内容,故不做模型校验
                    # KookCardMessage().model_validate(message_component.data).to_json(),
                    encoded_json = json.dumps(json_data)

                return handle_plain(
                    index,
                    text=encoded_json,
                    type=KookMessageType.CARD,
                )
```
</issue_to_address>

### 评论 3
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="10" />
<code_context>
+from pydantic.dataclasses import dataclass
+
+
+class KookApiPaths:
+    """Kook Api 路径"""
+
</code_context>
<issue_to_address>
**issue (complexity):** 建议将核心的卡片/消息 API 与详细的模块定义拆分开来,并使用一个简单的 Protocol 来替代大的 union,这样主 Kook 卡片模型可以更小、更不与每个模块类型强耦合。

你可以在不损失任何功能的前提下,通过收紧“核心”接口并将 `KookCardMessage` 与具体模块类型解耦,来降低这个抽象层看起来的“重量感”。

### 1. 收窄核心模块接口

目前这个文件在同一个地方定义了所有模块 + union + literal,即使实际上只用到了其中一部分。你可以在保留现有功能的情况下,通过拆分“核心定义”和“扩展定义”,让这个文件更容易浏览。

例如:

```python
# kook_card_core.py  (only what is used today)

from enum import IntEnum
from typing import Protocol, Literal
from pydantic import BaseModel, ConfigDict
from pydantic.dataclasses import dataclass
from dataclasses import field
import json


class KookApiPaths:
    BASE_URL = "https://www.kookapp.cn"
    API_VERSION_PATH = "/api/v3"
    USER_ME = f"{BASE_URL}{API_VERSION_PATH}/user/me"
    GATEWAY_INDEX = f"{BASE_URL}{API_VERSION_PATH}/gateway/index"
    ASSET_CREATE = f"{BASE_URL}{API_VERSION_PATH}/asset/create"
    CHANNEL_MESSAGE_CREATE = f"{BASE_URL}{API_VERSION_PATH}/message/create"
    DIRECT_MESSAGE_CREATE = f"{BASE_URL}{API_VERSION_PATH}/direct-message/create"


class KookMessageType(IntEnum):
    TEXT = 1
    IMAGE = 2
    VIDEO = 3
    FILE = 4
    AUDIO = 8
    KMARKDOWN = 9
    CARD = 10
    SYSTEM = 255


ThemeType = Literal["primary", "success", "danger", "warning", "info", "secondary", "none", "invisible"]
SizeType = Literal["xs", "sm", "md", "lg"]


class BaseCardModule(Protocol):
    type: str  # structural typing; all your dataclass modules already have this


@dataclass
class FileModule:
    src: str
    title: str = ""
    type: Literal["file", "audio", "video"] = "file"
    cover: str | None = None


class KookCardMessage(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)

    type: str = "card"
    theme: ThemeType | None = None
    size: SizeType | None = None
    color: str | None = None
    modules: list[BaseCardModule] = field(default_factory=list)

    def add_module(self, module: BaseCardModule) -> None:
        self.modules.append(module)

    def to_dict(self, exclude_none: bool = True):
        return self.model_dump(exclude_none=exclude_none)

    def to_json(self, indent: int | None = None, ensure_ascii: bool = True):
        return json.dumps(self.to_dict(), indent=indent, ensure_ascii=ensure_ascii)


class KookCardMessageContainer(list[KookCardMessage]):
    def to_json(self, indent: int | None = None, ensure_ascii: bool = True) -> str:
        return json.dumps([i.to_dict() for i in self], indent=indent, ensure_ascii=ensure_ascii)


@dataclass
class OrderMessage:
    index: int
    text: str
    type: KookMessageType
    reply_id: str | int = ""
```

然后把所有详细的模块 dataclass(`HeaderModule``SectionModule` 等)移到一个“扩展 DSL” 文件中:

```python
# kook_card_modules.py

from pydantic.dataclasses import dataclass
from .kook_card_core import BaseCardModule, ThemeType, SizeType

# 将你现有的所有 @dataclass 模块放到这里,保持原样:
# PlainTextElement, KmarkdownElement, ImageElement, ButtonElement,
# HeaderModule, SectionModule, ImageGroupModule, ContainerModule, etc.
```

未来任何需要更丰富卡片结构的代码可以从 `kook_card_modules` 中导入,而主适配器/事件代码只需要面对精简的 `kook_card_core` API。

### 2. 用结构化类型替代 `AnyModule`

因为你目前并没有对照 KOOK 的 schema 做严格校验,同时你希望保持灵活性,那么像 `AnyModule = HeaderModule | ... | InviteModule` 这样的大 union 带来的价值其实有限,反而让核心消息模型与每个具体模块类型形成强耦合。

改用一个简单的 Protocol,可以在不引入联合类型爆炸的情况下,保留类型检查的好处:

```python
from typing import Protocol

class BaseCardModule(Protocol):
    type: str
    # 如果以后需要,也可以在这里补充一些公共字段
```

然后:

```python
class KookCardMessage(BaseModel):
    ...
    modules: list[BaseCardModule] = field(default_factory=list)

    def add_module(self, module: BaseCardModule) -> None:
        self.modules.append(module)
```

你现有的 dataclass 模块已经符合这个 Protocol(它们都有 `type: str`,并且可以通过 `model_dump` 序列化为 JSON),所以行为上不会有变化,但 `KookCardMessage` 不再与每一个具体模块类型紧紧绑在一起。

这两个改动可以在保持现有功能完整的前提下,让这个文件更轻量、更易理解和维护,也能让未来对单个模块的改动变得不那么“高风险”。
</issue_to_address>

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

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

  • The reconnection logic in KookPlatformAdapter._main_loop currently hardcodes an exponential backoff (2**consecutive_failures with a fixed 60s cap) and ignores reconnect_delay/max_reconnect_delay from KookConfig, so it would be clearer and more controllable to base the backoff calculation on those config fields.
  • KookClient.connect exposes a resume parameter and tracks session_id/last_sn, but the adapter never calls connect(resume=True) nor implements a resume branch in the reconnection flow, which makes the resume-related code effectively dead; consider either wiring resume into the reconnect path or removing the unused parameter/state to simplify the client.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The reconnection logic in `KookPlatformAdapter._main_loop` currently hardcodes an exponential backoff (`2**consecutive_failures` with a fixed 60s cap) and ignores `reconnect_delay`/`max_reconnect_delay` from `KookConfig`, so it would be clearer and more controllable to base the backoff calculation on those config fields.
- `KookClient.connect` exposes a `resume` parameter and tracks `session_id`/`last_sn`, but the adapter never calls `connect(resume=True)` nor implements a resume branch in the reconnection flow, which makes the resume-related code effectively dead; consider either wiring resume into the reconnect path or removing the unused parameter/state to simplify the client.

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/kook/kook_client.py" line_range="258-259" />
<code_context>
+        """心跳循环"""
+        while self.running:
+            try:
+                # 随机化心跳间隔 (30±5秒)
+                interval = self.config.heartbeat_interval + random.randint(-5, 5)
+                await asyncio.sleep(interval)
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Randomized heartbeat interval can go non‑positive and ignores configured reconnect delays.

The randomized interval `config.heartbeat_interval + randint(-5, 5)` can become 0 or negative when `heartbeat_interval < 5`, leading to effectively busy loops. It would help to clamp the result to a minimum (e.g. `interval = max(1, ...)`). Also, `reconnect_delay`/`max_reconnect_delay` in `KookConfig` are never used despite the `_handle_hello` comment mentioning reconnect delay; consider either integrating them into the reconnection logic or removing them to avoid confusion.

Suggested implementation:

```python
    async def _heartbeat_loop(self):
        """心跳循环"""
        while self.running:
            try:
                # 随机化心跳间隔 (基于配置的 heartbeat_interval,加入 ±5 秒抖动),并保证为正数
                base_interval = getattr(self.config, "heartbeat_interval", 30)
                jitter = random.randint(-5, 5)
                interval = max(1, base_interval + jitter)
                await asyncio.sleep(interval)

import asyncio
import random
import aiofiles

```

```python
import aiohttp
import websockets

```

1. Review the reconnection logic (likely where `_handle_hello` or websocket reconnect is implemented) and either:
   - Use `KookConfig.reconnect_delay` / `KookConfig.max_reconnect_delay` to implement an increasing backoff between reconnect attempts, or
   - Remove these configuration fields and any comments referencing them, if reconnection backoff is not desired.
2. If a heartbeat payload needs to be sent on each loop iteration (e.g. via websocket send), ensure that send call is placed after the `await asyncio.sleep(interval)` in `_heartbeat_loop`.
</issue_to_address>

### Comment 2
<location path="astrbot/core/platform/sources/kook/kook_event.py" line_range="134-146" />
<code_context>
+                return handle_plain(index, "(met)all(met)")
+            case Reply():
+                return handle_plain(index, "", reply_id=message_component.id)
+            case Json():
+                json_data = message_component.data
+                # kook卡片json外层得是一个列表
+                if isinstance(json_data, dict):
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Double-encoding risk when sending Json components that already contain JSON strings.

If a plugin passes a pre‑serialized JSON string (e.g., a KOOK card they've already built), this logic will `json.dumps` it again and send a quoted JSON string instead of the expected card array. Consider detecting `str` and passing it through unchanged, and only wrapping/dumping when the input is a dict/list (or other Python structures) to avoid double‑encoding.

```suggestion
            case Json():
                json_data = message_component.data
                # 如果插件已经传入预序列化的 JSON 字符串(例如完整的 KOOK 卡片数组),直接透传,避免二次编码
                if isinstance(json_data, str):
                    encoded_json = json_data
                else:
                    # kook卡片json外层得是一个列表
                    if isinstance(json_data, dict):
                        json_data = [json_data]
                    # 考虑到kook可能会更改消息结构,为了能让插件开发者
                    # 自行根据kook文档描述填卡片json内容,故不做模型校验
                    # KookCardMessage().model_validate(message_component.data).to_json(),
                    encoded_json = json.dumps(json_data)

                return handle_plain(
                    index,
                    text=encoded_json,
                    type=KookMessageType.CARD,
                )
```
</issue_to_address>

### Comment 3
<location path="astrbot/core/platform/sources/kook/kook_types.py" line_range="10" />
<code_context>
+from pydantic.dataclasses import dataclass
+
+
+class KookApiPaths:
+    """Kook Api 路径"""
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider splitting the core card/message API from the detailed module definitions and using a simple protocol instead of a large union so the main Kook card model stays smaller and less coupled to every module type.

You can reduce the perceived heaviness of this abstraction without losing any functionality by tightening the “core” surface and decoupling `KookCardMessage` from knowing every module type.

### 1. Narrow the core module surface

Right now this file defines all modules + unions + literals in one place, even though only a subset is used. You can keep the current functionality but make this file easier to scan by splitting *core* vs *extended* definitions.

For example:

```python
# kook_card_core.py  (only what is used today)

from enum import IntEnum
from typing import Protocol, Literal
from pydantic import BaseModel, ConfigDict
from pydantic.dataclasses import dataclass
from dataclasses import field
import json


class KookApiPaths:
    BASE_URL = "https://www.kookapp.cn"
    API_VERSION_PATH = "/api/v3"
    USER_ME = f"{BASE_URL}{API_VERSION_PATH}/user/me"
    GATEWAY_INDEX = f"{BASE_URL}{API_VERSION_PATH}/gateway/index"
    ASSET_CREATE = f"{BASE_URL}{API_VERSION_PATH}/asset/create"
    CHANNEL_MESSAGE_CREATE = f"{BASE_URL}{API_VERSION_PATH}/message/create"
    DIRECT_MESSAGE_CREATE = f"{BASE_URL}{API_VERSION_PATH}/direct-message/create"


class KookMessageType(IntEnum):
    TEXT = 1
    IMAGE = 2
    VIDEO = 3
    FILE = 4
    AUDIO = 8
    KMARKDOWN = 9
    CARD = 10
    SYSTEM = 255


ThemeType = Literal["primary", "success", "danger", "warning", "info", "secondary", "none", "invisible"]
SizeType = Literal["xs", "sm", "md", "lg"]


class BaseCardModule(Protocol):
    type: str  # structural typing; all your dataclass modules already have this


@dataclass
class FileModule:
    src: str
    title: str = ""
    type: Literal["file", "audio", "video"] = "file"
    cover: str | None = None


class KookCardMessage(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)

    type: str = "card"
    theme: ThemeType | None = None
    size: SizeType | None = None
    color: str | None = None
    modules: list[BaseCardModule] = field(default_factory=list)

    def add_module(self, module: BaseCardModule) -> None:
        self.modules.append(module)

    def to_dict(self, exclude_none: bool = True):
        return self.model_dump(exclude_none=exclude_none)

    def to_json(self, indent: int | None = None, ensure_ascii: bool = True):
        return json.dumps(self.to_dict(), indent=indent, ensure_ascii=ensure_ascii)


class KookCardMessageContainer(list[KookCardMessage]):
    def to_json(self, indent: int | None = None, ensure_ascii: bool = True) -> str:
        return json.dumps([i.to_dict() for i in self], indent=indent, ensure_ascii=ensure_ascii)


@dataclass
class OrderMessage:
    index: int
    text: str
    type: KookMessageType
    reply_id: str | int = ""
```

Then move all the detailed module dataclasses (`HeaderModule`, `SectionModule`, etc.) into an “extended DSL” file:

```python
# kook_card_modules.py

from pydantic.dataclasses import dataclass
from .kook_card_core import BaseCardModule, ThemeType, SizeType

# all the existing @dataclass modules here exactly as you have them:
# PlainTextElement, KmarkdownElement, ImageElement, ButtonElement,
# HeaderModule, SectionModule, ImageGroupModule, ContainerModule, etc.
```

Any future code that needs richer card structures can import from `kook_card_modules`, but the main adapter/event code only sees the compact `kook_card_core` API.

### 2. Replace `AnyModule` with structural typing

Because you’re not validating against KOOK’s schema and you want flexibility, a broad union like `AnyModule = HeaderModule | ... | InviteModule` doesn’t buy much and tightly couples the core message model to every concrete module type.

Switching to a simple protocol keeps typing goodness without the explosion of unions:

```python
from typing import Protocol

class BaseCardModule(Protocol):
    type: str
    # you can optionally add common attributes if you ever need them
```

And then:

```python
class KookCardMessage(BaseModel):
    ...
    modules: list[BaseCardModule] = field(default_factory=list)

    def add_module(self, module: BaseCardModule) -> None:
        self.modules.append(module)
```

Your existing dataclass modules already conform to this protocol (they all have `type: str` and are JSON-serializable via `model_dump`), so no behavior changes, but `KookCardMessage` is no longer entangled with every specific module type.

These two changes keep all current features intact, but make the file much lighter to understand and maintain, and make future changes to individual modules feel less risky.
</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.

@shuiping233 shuiping233 force-pushed the feat/astrbot_plugin_kook_adapter branch from 4f6c261 to e27a3ad Compare March 2, 2026 11:56
@shuiping233
Copy link
Contributor Author

shuiping233 commented Mar 2, 2026

ok,修完bug了,经过手动测试是能用了,至于原有kook适配器代码的写的比较不是很令人愉悦的部分,后面再说吧)
单测部分也得后面有空才能补了)

@Soulter 如果有空的话,请帮忙review一下,虽然我应该吧pr拆开的,毕竟代码量太多确实不好人工审核(
下次一定注意.jpg

@shuiping233 shuiping233 force-pushed the feat/astrbot_plugin_kook_adapter branch from ce40c8c to cb2d83a Compare March 2, 2026 12:22
shuiping233 added a commit to shuiping233/astrbot_plugin_kook_adapter that referenced this pull request Mar 2, 2026
@shuiping233 shuiping233 force-pushed the feat/astrbot_plugin_kook_adapter branch from 96c179c to dace3f0 Compare March 2, 2026 14:43
@Soulter
Copy link
Member

Soulter commented Mar 3, 2026

上传文件会提示 “资源不存在”,这个你有遇到吗

发送kook消息类型 "FILE" 失败: {'code': 40000, 'message': '资源不存在', 'data': {}}

@shuiping233
Copy link
Contributor Author

上传文件会提示 “资源不存在”,这个你有遇到吗

发送kook消息类型 "FILE" 失败: {'code': 40000, 'message': '资源不存在', 'data': {}}

资源不存在我遇到过,我这里的情况是,发了一个外联http的svg到hook那边,他乎返回这个,似乎是因为他那边解析不了svg,但是我这边是image类型的,file类型的不存在我还没遇到过)

image image

@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Mar 3, 2026
b64_str = file_url.removeprefix("base64://")
bytes_data = base64.b64decode(b64_str)

elif file_url.startswith("file://") or os.path.exists(file_url):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

这里我测试debug了一下,我这边插件放了一个File对象发送去,然后file_url的值是没有file://开头的, File.get_file()去掉了了file://头,假设这里激进一点有可能会有file://开头的url进来,那是不是还要处理一下可能的file:///三个斜杠的情况? astrbot有没有utils函数处理这个协议头来着)

chain = []
        chain.extend(
            [
                Plain("测试发送文件"),
                File(
                    "unknown",
                    file="D:/game/SteamLibrary/steamapps/common/RunningWithRifles/media/packages/vanilla/sounds/dancing_shot5.wav",
                ),
            ]
        )
        yield event.chain_result(chain)

Copy link
Member

Choose a reason for hiding this comment

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

file_url = file_url.removeprefix("file://") 前加一个 file_url = file_url.removeprefix("file:///") 这样可以解决?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

应该可以)

Copy link
Member

Choose a reason for hiding this comment

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

加协议头 file:/// 是历史原因了。。屎山。。onebot。。

@Soulter Soulter changed the title feat: 将kook适配器插件并入astrbot官方适配器目录中 feat: integrates KOOK platform adapter Mar 3, 2026
@Soulter Soulter merged commit 866e546 into AstrBotDevs:master Mar 3, 2026
5 checks passed
"type": "string",
"hint": "必填项。从 KOOK 开发者平台获取的机器人 Token。",
},
"kook_bot_nickname": {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

机器人的名字要修改的话应该是在kook机器人控制台里改的,KookClient里有个get_bot_info能通过kook接口拿到自身bot_idbot_name,我不知道为什么明明我拿了bot_name却没拿来用上,但是有bot_name的话应该就不用用户手动填kook_bot_nickname进来了吧,也省的用户改了kook那边机器人名称,但是忘了改kook适配器配置)

@astrbot-doc-agent
Copy link

No docs changes were generated in this run (docs repo had no updates).

Docs repo: AstrBotDevs/AstrBot-docs
Trigger: PR merged


AI change summary (not committed):

  • 新增 KOOK 平台适配器的配置与接入说明文档。
  • i18n: TODO(未检测到对应英文文档变更)。

Experimental bot notice:

  • This output is generated by AstrBot-Doc-Agent for review only.
  • It does not represent the final documentation form.

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