Kookプラットフォームの機能ドキュメント
KookAdapter は、Kook(開黒啦)Bot WebSocket プロトコルに基づいて構築されたアダプタであり、Kook のすべての機能モジュールを統合し、一貫したイベント処理とメッセージ操作インターフェースを提供します。
ドキュメント情報
- 対応モジュールバージョン: 4.1.0
- メンテナー: ShanFish
基本情報
- プラットフォーム概要: Kook(旧称:開黒啦)は、テキスト、音声、ビデオ通話に対応したコミュニティプラットフォームであり、完全な Bot 開発インターフェースを提供しています。
- アダプタ名: KookAdapter
- マルチアカウント対応: 複数の Kook ボットを同時に設定できます
- 接続方法: WebSocket 長時間接続(Kook ゲートウェイ経由)
- 認証方法: Bot Token に基づく認証
- チェーン修飾子対応:
.Reply()、.At()、.AtAll()などのチェーン修飾メソッドに対応しています - OneBot12互換: OneBot12 形式のメッセージ送信に対応しています
設定の説明
KookAdapter は、複数のアカウントをサポートしており、各アカウントは独立した Kook ロボットに対応します。
# config.toml
# アカウント1
[KookAdapter.accounts.default]
token = "YOUR_BOT_TOKEN" # Kook Bot Token(必須、形式: Bot xxx/xxx)
bot_id = "" # Bot ユーザーID(任意、空欄の場合は token から解析)
compress = true # WebSocket 圧縮を有効にするか(任意、デフォルトは true)
enabled = true # 有効にするか(任意、デフォルトはtrue)
# アカウント2
[KookAdapter.accounts.bot2]
token = "ANOTHER_BOT_TOKEN"
bot_id = ""
enabled = true
旧設定の互換性:
[KookAdapter]に token を含む旧の単一アカウント設定が検出された場合、accounts.defaultに自動的に移行されます。
各アカウントの設定項目の説明:
token:Kook Bot のトークン(必須)、Kook開発者センター から取得し、形式はBot xxx/xxxです。bot_id:Bot のユーザーID(任意)、空欄の場合は、トークンから自動的に解析されます。正確さを保証するため、手動で入力することを推奨します。compress:WebSocket データ圧縮を有効にするか(任意、デフォルトはtrue)、有効にすると zlib でデータを解凍します。enabled:このアカウントを有効にするか(任意、デフォルトは true)
API 環境:
- Kook API 基本アドレス:
https://www.kookapp.cn/api/v3 - WebSocket ゲートウェイは API を介して動的に取得されます:
POST /gateway/index
v5 フレームワークの更新(4.1.0)
このアダプタは v5 フレームワークに準拠した更新を完了しました(段階的なアップグレード、API は互換性を保つ)。
- BaseConverter の継承:コンバーターの共通フィールドはフレームワークの
build_base_eventによって構築されます。 - Api DSL:標準的な API アクションのマッピング(下記を参照)
- 標準的な keyboard セグメント:
{"type": "keyboard", "data": {"rows": [[{"label", "type": "callback|link", "data"}]]}}テキストとキーボードは自動的に Kook カードメッセージ(section + action-group)に組み合わされます。.Keyboard(rows) 修飾子は一般的な構造を受け付けます。 - spawn_background でのタスクの所属:接続タスクは
untime.spawn_backgroundを使用します。 - フレームワークのソフト依存:ErisPulse>=2.7.1 の実行時検出と警告メッセージを出力します。起動時にバージョンのログを出力します。
標準的な API アクション
from ErisPulse import sdk
kook = sdk.adapter.get("kook")
result = await kook.Api.get_self_info() # GET /users/@me
result = await kook.Api.get_user_info(user_id) # POST /user/view
result = await kook.Api.get_guild_info(guild_id) # POST /guild/view
result = await kook.Api.get_guild_list() # POST /guild/list
result = await kook.Api.get_channel_info(channel_id) # POST /channel/view
result = await kook.Api.get_channel_list(guild_id) # POST /channel/list
await kook.Api.delete_message(msg_id) # POST /message/delete
result = await kook.Api.Using("main").get_self_info()
ボタン(keyboard)
rows = [[{"label": "オプションA", "type": "callback", "data": "vote:A"},
{"label": "公式サイト", "type": "link", "data": "https://example.com"}]]
await kook.Send.To("channel", channel_id).Keyboard(rows).Text("選択してください")
# テキストとボタンは自動的に Kook カードメッセージ(section + action-group)に組み合わされます。
# ボタンのクリックコールバック(標準的なフィールド)
from ErisPulse.Core.Event import notice
@notice.on_notice()
async def handle_button(event):
if event.get("platform") == "kook" and event.get("button_data"):
data = event["button_data"]
支援されるメッセージ送信タイプ
すべての送信メソッドは、メソッドチェーン構文で実装されています。たとえば、次のように使用します:
from ErisPulse.Core import adapter
kook = adapter.get("kook")
await kook.Send.To("group", channel_id).Text("Hello World!")
サポートされている送信タイプは以下の通りです:
.Text(text: str):純粋なテキストメッセージを送信します。.Image(file: bytes | str):画像メッセージを送信します。ファイルパス、URL、バイナリデータをサポートします。.Video(file: bytes | str):動画メッセージを送信します。ファイルパス、URL、バイナリデータをサポートします。.File(file: bytes | str, filename: str = None):ファイルメッセージを送信します。ファイルパス、URL、バイナリデータをサポートします。.Voice(file: bytes | str):音声メッセージを送信します。ファイルパス、URL、バイナリデータをサポートします。.Markdown(text: str):KMarkdown形式のメッセージを送信します。.Card(card_data: dict):カードメッセージ(CardMessage)を送信します。.Raw_ob12(message: List[Dict], **kwargs):OneBot12形式のメッセージを送信します。
チェイン修飾メソッド(複数組み合わせて使用可能)
チェイン修飾メソッドは self を返し、メソッドチェーンで呼び出すことができます。最終的な送信メソッドの前に呼び出す必要があります:
.Reply(message_id: str):指定したメッセージに返信(引用)します。.At(user_id: str):指定したユーザーを@します。複数回呼び出すことで複数のユーザーを@できます。.AtAll():全員を@します。
チェイン呼び出しの例
# 基本的な送信
await kook.Send.To("group", channel_id).Text("Hello")
# メッセージの返信
await kook.Send.To("group", channel_id).Reply(msg_id).Text("返信メッセージ")
# ユーザーを@する
await kook.Send.To("group", channel_id).At("user_id").Text("こんにちは")
# 複数のユーザーを@する
await kook.Send.To("group", channel_id).At("user1").At("user2").Text("複数ユーザー@")
# 全員を@する
await kook.Send.To("group", channel_id).AtAll().Text("お知らせ")
# 修飾メソッドを組み合わせる
await kook.Send.To("group", channel_id).Reply(msg_id).At("user_id").Text("複合メッセージ")
OneBot12メッセージのサポート
アダプタは、OneBot12形式のメッセージを送信する機能をサポートしており、プラットフォーム間のメッセージ互換性を確保します。
# OneBot12形式のメッセージを送信する
ob12_msg = [{"type": "text", "data": {"text": "Hello"}}]
await kook.Send.To("group", channel_id).Raw_ob12(ob12_msg)
# 修飾メソッドと組み合わせる
ob12_msg = [{"type": "text", "data": {"text": "返信メッセージ"}}]
await kook.Send.To("group", channel_id).Reply(msg_id).Raw_ob12(ob12_msg)
# Raw_ob12でmentionとreplyメッセージセグメントを使用する
ob12_msg = [
{"type": "text", "data": {"text": "Hello "}},
{"type": "mention", "data": {"user_id": "user_id"}},
{"type": "reply", "data": {"message_id": "msg_id"}}
]
await kook.Send.To("group", channel_id).Raw_ob12(ob12_msg)
追加操作メソッド
メッセージ送信以外にも、Kookアダプタは以下の操作をサポートしています:
# メッセージの編集(KMarkdown type=9 と CardMessage type=10 にのみ対応)
await kook.Send.To("group", channel_id).Edit(msg_id, "**更新後の内容**")
# メッセージの撤回
await kook.Send.To("group", channel_id).Recall(msg_id)
# ファイルのアップロード(ファイルURLの取得)
result = await kook.Send.Upload("C:/path/to/file.jpg")
file_url = result["data"]["url"]
送信メソッドの戻り値
すべての送信メソッドは Task オブジェクトを返します。これを直接 await することで送信結果を取得できます。返り値は ErisPulse アダプタの標準化された返り値規格に従います:
{
"status": "ok", // 実行ステータス: "ok" または "failed"
"retcode": 0, // 戻りコード(Kook API の code)
"data": {...}, // 応答データ
"message_id": "xxx", // メッセージID
"message": "", // エラーメッセージ
"kook_raw": {...} // 元の応答データ
}
エラーコードの説明
| retcode | 説明 |
|---|---|
| 0 | 成功 |
| 40100 | Token が無効または提供されていない |
| 40101 | Token が期限切れ |
| 40102 | Token が Bot と一致しない |
| 40103 | 権限が不足している |
| 40000 | パラメータエラー |
| 40400 | 対象が存在しない |
| 40300 | 操作権限がありません |
| 50000 | サーバ内部エラー |
| -1 | アダプタ内部エラー |
特有イベントタイプ
このプラットフォームの機能を使用するには、platform=="kook" の検証が必要です。
核心的な違い
- チャンネルシステム:Kook はサーバー(Guild)とチャンネル(Channel)の2層構造を使用し、チャンネルがメッセージの基本的な送信先となります。
- メッセージタイプ:Kook はテキスト(1)、画像(2)、動画(3)、ファイル(4)、音声(8)、KMarkdown(9)、カードメッセージ(10)など、多様なメッセージタイプをサポートしています。
- プライベートメッセージシステム:Kook はチャンネルメッセージとプライベートメッセージを区別し、異なる API エンドポイントを使用します。
- メッセージの順序番号:Kook の WebSocket は
sn番号を使用してメッセージの順序性を保証し、メッセージの一時保存や順序の乱れを再配置することをサポートします。 - メッセージの編集と撤回:既に送信されたメッセージ(KMarkdown および CardMessage に限る)を編集し、メッセージを撤回することができます。
拡張フィールド
- すべての固有フィールドは
kook_で始まるプレフィックスで識別されます。 - 保持された元のデータは
kook_rawフィールドに格納されます。 kook_raw_typeは元の Kook メッセージタイプの番号を示します(例:1はテキスト、255は通知イベント)。
特殊フィールドの例
# チャンネルのテキストメッセージ
{
"type": "message",
"detail_type": "group",
"user_id": "ユーザーID",
"group_id": "チャンネルID",
"channel_id": "チャンネルID",
"message_id": "メッセージID",
"kook_raw": {...},
"kook_raw_type": "1",
"message": [
{"type": "text", "data": {"text": "Hello"}}
],
"alt_message": "Hello"
}
# 画像付きのメッセージ
{
"type": "message",
"detail_type": "group",
"user_id": "ユーザーID",
"group_id": "チャンネルID",
"channel_id": "チャンネルID",
"message_id": "メッセージID",
"kook_raw": {...},
"kook_raw_type": "2",
"message": [
{"type": "image", "data": {"file": "画像URL", "url": "画像URL"}}
],
"alt_message": "画像の内容"
}
# KMarkdownメッセージ
{
"type": "message",
"detail_type": "group",
"user_id": "ユーザーID",
"group_id": "チャンネルID",
"message_id": "メッセージID",
"kook_raw": {...},
"kook_raw_type": "9",
"message": [
{"type": "text", "data": {"text": "解析後の純粋なテキスト"}}
]
}
# カードメッセージ
{
"type": "message",
"detail_type": "group",
"user_id": "ユーザーID",
"group_id": "チャンネルID",
"message_id": "メッセージID",
"kook_raw": {...},
"kook_raw_type": "10",
"message": [
{"type": "json", "data": {"data": "カードのJSON内容"}}
]
}
# プライベートチャットメッセージ
{
"type": "message",
"detail_type": "private",
"user_id": "ユーザーID",
"message_id": "メッセージID",
"kook_raw": {...},
"kook_raw_type": "1",
"message": [
{"type": "text", "data": {"text": "プライベートチャットの内容"}}
]
}
メッセージセグメントタイプ
Kook のメッセージタイプは type フィールドに基づいて対応するメッセージセグメントに自動的に変換されます:
| Kook type | 変換タイプ | 説明 |
|---|---|---|
| 1 | text |
テキストメッセージ |
| 2 | image |
画像メッセージ |
| 3 | video |
動画メッセージ |
| 4 | file |
ファイルメッセージ |
| 8 | record |
音声メッセージ |
| 9 | text |
KMarkdownメッセージ(純粋なテキスト内容を抽出) |
| 10 | json |
カードメッセージ(元のJSON) |
メッセージセグメントの構造例:
{
"type": "image",
"data": {
"file": "画像URL",
"url": "画像URL"
}
}
Mentionメッセージセグメント
メッセージに @ 情報が含まれる場合、メッセージセグメントの前に mention メッセージセグメントが挿入されます:
{
"type": "mention",
"data": {
"user_id": "メンションされたユーザーID"
}
}
mention_allメッセージセグメント
メッセージが @全員 の場合、mention_all メッセージセグメントが挿入されます:
{
"type": "mention_all",
"data": {}
}
WebSocket接続
接続フロー
- Bot Tokenを使用して
POST /gateway/indexを呼び出すことでWebSocketゲートウェイのアドレスを取得します。 - WebSocketゲートウェイに接続します。
- HELLO(s=1)シグナルを受信し、接続状態を確認します。
- ハートビートループを開始します(PING、s=2、30秒ごとに1回)。
- メッセージイベント(s=0)を受信し、sn番号を使用して順序を保証します。
- ハートビート応答PONG(s=3)を受信します。
シグナルタイプ
| シグナル | s値 | 説明 |
|---|---|---|
| HELLO | 1 | サーバーからの歓迎シグナル、接続成功後に受信します。 |
| PING | 2 | クライアントのハートビート、30秒ごとに現在のsnを含めて送信します。 |
| PONG | 3 | ハートビート応答 |
| RESUME | 4 | 接続の復元シグナル、snを含んでセッションを復元します。 |
| RECONNECT | 5 | サーバーからの再接続要求、ゲートウェイを再取得する必要があります。 |
| RESUME_ACK | 6 | RESUMEの成功応答 |
断線再接続
- 接続が異常終了した後、アダプターは自動的に再接続を試みます。
- 以前に
sn > 0が存在する場合、まずRESUME(s=4)を使って接続を復元しようとします。 - RESUMEが失敗した場合、snとメッセージキューをリセットし、完全な接続(HELLOフロー)からやり直します。
- RECONNECT(s=5)シグナルを受信した場合、状態をクリアして再接続します。
メッセージ番号メカニズム
Kook WebSocketはsn(増加する番号)を使用してメッセージの順序を保証します:
- 各メッセージイベント(s=0)を受信するたびに、snは1増加します。
- 受信したメッセージのsnが連続していない場合、一時保存モードに入ります。
- 一時保存中のメッセージはsn順に並べ替えられ、欠落したメッセージが到着した後に順序通りに処理されます。
- 一時保存区がクリアされた後、自動的に一時保存モードから抜けます。
使用例
チャンネルメッセージの処理
from ErisPulse.Core.Event import message
from ErisPulse import sdk
kook = sdk.adapter.get("kook")
@message.on_message()
async def handle_group_msg(event):
if event.get("platform") != "kook":
return
if event.get("detail_type") != "group":
return
text = event.get_text()
channel_id = event.get("group_id")
if text == "hello":
await kook.Send.To("group", channel_id).Text("Hello!")
プライベートメッセージの処理
@message.on_message()
async def handle_private_msg(event):
if event.get("platform") != "kook":
return
if event.get("detail_type") != "private":
return
text = event.get_text()
user_id = event.get("user_id")
await kook.Send.To("user", user_id).Text(f"あなたが言った: {text}")
通知イベントの処理(絵文字反応など)
from ErisPulse.Core.Event import notice
@notice.on_notice()
async def handle_notice(event):
if event.get("platform") != "kook":
return
sub_type = event.get("sub_type")
if sub_type == "added_reaction":
emoji = event.get("emoji", {})
user_id = event.get("user_id")
msg_id = event.get("message_id")
print(f"ユーザー {user_id} がメッセージ {msg_id} に絵文字反応を追加しました")
elif sub_type == "deleted_reaction":
emoji = event.get("emoji", {})
user_id = event.get("user_id")
msg_id = event.get("message_id")
print(f"ユーザー {user_id} がメッセージ {msg_id} の絵文字反応を削除しました")
メディアメッセージの送信
# 画像の送信(URL)
await kook.Send.To("group", channel_id).Image("https://example.com/image.png")
# 画像の送信(バイナリ)
with open("image.png", "rb") as f:
image_bytes = f.read()
await kook.Send.To("group", channel_id).Image(image_bytes)
# ビデオの送信
await kook.Send.To("group", channel_id).Video("https://example.com/video.mp4")
# ファイルの送信
await kook.Send.To("group", channel_id).File("https://example.com/file.pdf", filename="document.pdf")
# 音声の送信
await kook.Send.To("group", channel_id).Voice("https://example.com/voice.mp3")
KMarkdown とカードメッセージの送信
# KMarkdown
await kook.Send.To("group", channel_id).Markdown("**太字** *斜体* [リンク](https://example.com)")
# カードメッセージ
card = {
"type": "card",
"theme": "primary",
"size": "lg",
"modules": [
{"type": "header", "text": {"type": "plain-text", "content": "タイトル"}},
{"type": "section", "text": {"type": "kmarkdown", "content": "内容"}}
]
}
await kook.Send.To("group", channel_id).Card(card)
メッセージの編集と撤回
# メッセージの送信
result = await kook.Send.To("group", channel_id).Markdown("**元の内容**")
msg_id = result["data"]["msg_id"]
# メッセージの編集(KMarkdown と CardMessage にのみ対応)
await kook.Send.To("group", channel_id).Edit(msg_id, "**更新後の内容**")
# メッセージの撤回
await kook.Send.To("group", channel_id).Recall(msg_id)
プライベートメッセージの編集と削除通知の処理
@notice.on_notice()
async def handle_private_notice(event):
if event.get("platform") != "kook":
return
sub_type = event.get("sub_type")
if sub_type == "updated_private_message":
msg_id = event.get("message_id")
content = event.get("content")
print(f"プライベートメッセージが更新されました: {msg_id}, 新しい内容: {content}")
elif sub_type == "deleted_private_message":
msg_id = event.get("message_id")
print(f"プライベートメッセージが削除されました: {msg_id}")