kook.go 使用与部署指南
从一个会回复 ping 的机器人开始。你需要会一点 Go,准备好 KOOK 账号,然后按前面三节操作。
跑通以后,可以按需要往下看:
- 想发消息、查成员或管理频道,看第 6 节的接口示例。
- 想给机器人添加消息处理逻辑,看第 8 节。
- 想让机器人一直在服务器上运行,看第 12 节。
项目最低支持 Go 1.21。新项目建议安装仍在维护的 Go 版本;如果已经安装,可以在终端运行 go version 查看。
1. 开始前
先认识几个后面会用到的名称:
| 名称 | 含义 |
|---|---|
| Bot | 在开发者平台创建的机器人账号 |
| Bot Token | 机器人的访问凭证,作用和密码相同 |
| Guild | KOOK 里的服务器 |
| Channel | 文字频道、语音频道或频道分组 |
| WebSocket | 机器人主动连接 KOOK,持续接收事件 |
| Webhook | KOOK 把事件发送到你的公网 HTTPS 地址 |
| OAuth | 用户授权你的应用读取自己的账号数据;普通机器人入门暂时用不到 |
第一次运行建议使用 WebSocket。它不需要域名和公网服务器,在本地终端里就能测试。
| 使用场景 | 选择 |
|---|---|
| 本地开发、常驻机器人、实时收消息 | WebSocket |
| 已有公网 Web 服务,希望复用 HTTP 基础设施 | Webhook |
| 需要读取授权用户的数据 | OAuth;它与事件接收方式无关 |
机器人通过 WebSocket 或 Webhook 收消息,通过 HTTP API 发消息。下面的入门程序使用 WebSocket:启动后保持连接,收到 ping 就调用发送接口回复 pong。
2. 创建机器人并加入服务器
- 登录 KOOK 开发者平台,创建一个应用。
- 在应用中启用机器人功能,并为机器人设置名称和头像。
- 打开机器人配置页,复制 Bot Token。稍后把它放进
KOOK_TOKEN环境变量。 - 为入门示例授予“查看文字、语音频道”和“发布消息”权限。管理成员、角色或消息时,再增加对应权限。
- 在开发者平台生成邀请链接,用自己的 KOOK 账号打开链接,把机器人加入一个测试服务器。
- 确认机器人出现在服务器成员列表中,并且能看到用于测试的文字频道。
打开 KOOK,找一个测试用的文字频道,确认机器人已经在服务器里,而且能看到这个频道。后面就在这里发送 ping。
Bot Token 相当于机器人的密码,别发给别人,也别放进公开代码或截图。如果泄露了,到开发者平台重新生成。
3. 运行第一个机器人
3.1 创建项目
打开终端,执行下面四条命令。Windows 可以用 PowerShell。它们会创建项目目录、初始化 Go 项目并安装 SDK:
mkdir my-kook-bot
cd my-kook-bot
go mod init example.com/my-kook-bot
go get github.com/OggEu127/[email protected]
安装命令使用本文示例对应的版本,先照着执行即可。后面的命令都在 my-kook-bot 目录里运行。
接着设置 Bot Token。把下面的中文替换成开发者平台里的 Token,保留外面的引号:
export KOOK_TOKEN='在开发者平台复制的 Bot Token'
Windows PowerShell 使用:
$env:KOOK_TOKEN = '在开发者平台复制的 Bot Token'
3.2 编写机器人
在 my-kook-bot 目录中新建 main.go,把下面的程序完整复制进去。它会在文字频道收到 ping 时回复 pong。
程序里有几个地方后面会经常用到:
NewClientWithError:用 Token 创建客户端,之后通过它调用 KOOK 接口。OnMessage:告诉 SDK 收到哪种消息时运行你的函数。- 两行
OnMessage注册:同时接收普通文本和 KOOK 客户端发来的 KMarkdown。只注册普通文本,会收不到客户端里的ping。 event.TargetID:这条频道消息所在的频道 ID,用它把pong发回同一个频道。- 最后的
<-stop:让程序一直运行,直到你按Ctrl+C。
示例直接在收到消息后发送回复,适合先试通。后面如果加入查数据库、下载文件等耗时操作,再看第 8.4 节的处理建议。
3.3 运行和检查
go run .
终端出现“已上线”后,回到 KOOK,在测试频道发送小写的 ping,不用加引号。看到机器人回复 pong 就跑通了。
按 Ctrl+C 可以退出程序。如果换了一个终端,记得重新设置 KOOK_TOKEN。
| 现象 | 检查 |
|---|---|
提示 KOOK_TOKEN 为空 | 在当前终端重新执行设置 Token 的命令 |
| 返回 401 或 Token 无效 | 重新复制 Bot Token,不要使用 OAuth Token |
| WebSocket 无法连接 | 先确认网络能访问 KOOK,再看终端里的错误信息和 Token |
能连接但收不到 ping | 检查频道权限,并确认同时注册了 MessageTypeText(1)和 MessageTypeKMD(9)处理器 |
| 收到消息但回复失败 | 检查发送消息权限,并查看返回的错误码 |
3.4 ID 从哪里来
KOOK API 使用 ID 标识服务器、频道、用户和消息。收到事件时,可以直接读取这些字段:
event.TargetID:频道消息所在的频道 ID。event.AuthorID:消息发送者的用户 ID。event.MsgID:这条消息的消息 ID。- 系统事件的其他 ID 位于
event.Extra或SystemEvent.Body。
GuildID 要使用 API 返回的服务器 ID。可以从下面的服务器列表里取 Guild.ID,也可以读频道详情里的 Channel.GuildID。客户端界面显示的服务器编号可能不同,直接填进去容易遇到“资源不存在”。
如果要在没有事件的情况下主动发送消息,可以先查询机器人加入的服务器,再查询服务器中的频道:
把这段代码放到第 3.2 节的 main 函数里、连接 WebSocket 之前运行,就能打印频道名称和 ID。这里为了演示取了第一个服务器;机器人加入多个服务器时,要选你实际使用的那个。
4. Client 配置
第 3 节的默认配置已经够入门使用。SDK 默认使用 Bot Token,每次 HTTP 请求最多等待 30 秒,还会限制请求速度,并对符合条件的失败请求重试。
需要调整超时或日志时,可以像下面这样创建客户端。这是代码片段,放在你自己的 main 函数里;记得在文件的 import 中加入 net/http 和 github.com/sirupsen/logrus。
继续使用 NewClientWithError 即可,配置不对时会返回错误,方便你在日志里找到原因。旧的 NewClient 遇到配置错误会 panic。
defer client.Close() 用来在程序退出时释放资源。关闭后再调用接口,会收到 ErrClientClosed。
常用选项:
| 选项 | 用途 |
|---|---|
WithHTTPClient | 设置超时、代理或自定义 Transport |
WithTokenType | 选择 Bot 或 Bearer 鉴权 |
WithLogger | 注入 logrus.Logger |
WithRateLimiter | 注入全局限速器 |
WithoutRateLimit | 关闭 SDK 限速,仅建议测试使用 |
WithRetryConfig | 设置次数、延迟和可重试错误 |
WithoutRetry | 关闭自动重试 |
WithBaseURL | 指向测试服务器,生产环境通常不设置 |
context.WithTimeout 可以给某一次请求单独设置超时。下面这次查询最多等 10 秒,避免网络卡住后一直等下去:
5. 参数与零值
有些参数的类型是 *int、*bool 或 *string。它们可以区分“这次不修改”和“把值改成 0 或空字符串”。
以频道简介为例:
| 写法 | 效果 |
|---|---|
不填写 Topic,或设为 nil | 这次不修改简介 |
Topic: ptr("新的简介") | 改成新的简介 |
Topic: ptr("") | 清空简介 |
下面的 ptr 只是一个小工具,帮你把普通值变成指针。把它放在 main 函数外面,和 main 同一级;下面调用接口的部分放进 main 函数。
角色、消息、邀请等接口里的可选指针参数也用这个规则。看到字段类型带 * 时,可以用 ptr(...) 传值。
6. HTTP 模块总览
Client 按功能划分为这些模块:
这里列出 SDK 的 102 个 HTTP 接口。按模块找就行:发频道消息看 Message,发私信看 DirectMessage,查服务器和成员看 Guild。
这一节都是代码片段,不能单独保存成文件运行。使用时:
- 先按第 3 节创建
client,再按第 4 节创建带超时的ctx。 - 把
guildID、channelID、userID等变量换成自己的 ID,获取方法见第 3.4 节。 - 用到
ptr时,添加第 5 节的小工具函数。 - 像发送消息这样会修改 KOOK 数据的操作,先在测试频道尝试。
表格为了节省宽度,省略了参数类型前的 kook.。写代码时要加上,例如 kook.MessageCreateParams;下方示例都保留了完整写法。
6.1 Gateway
获取 WebSocket 地址。compress 传 0 表示不压缩,传 1 表示 zlib 压缩。
| HTTP 接口 | SDK 方法 |
|---|---|
GET gateway/index | client.Gateway.GetGateway(ctx, GatewayParams{...}) |
6.2 User
用于读取机器人或目标用户,以及 Webhook 模式下控制在线状态。
| HTTP 接口 | SDK 方法 |
|---|---|
GET user/me | client.User.GetMe(ctx) |
GET user/view | client.User.GetUser(ctx, UserViewParams{...}) |
POST user/online | client.User.SetOnline(ctx) |
POST user/offline | client.User.SetOffline(ctx) |
GET user/get-online-status | client.User.GetOnlineStatus(ctx) |
User 已包含身份、头像、会员、机器人状态、手机号掩码、角色、加入时间、活跃时间、直播状态和活动游戏等字段。
6.3 Guild
服务器模块包含列表、详情、成员、昵称、离开、踢人、静音闭麦和助力历史。
| HTTP 接口 | SDK 方法 |
|---|---|
GET guild/list | client.Guild.GetGuildList(ctx, GuildListParams{...}) |
GET guild/view | client.Guild.GetGuildInfo(ctx, GuildViewParams{...}) |
GET guild/user-list | client.Guild.GetGuildMembers(ctx, GuildMembersParams{...}) |
POST guild/nickname | client.Guild.UpdateGuildMemberNickname(ctx, GuildNicknameParams{...}) |
POST guild/leave | client.Guild.LeaveGuild(ctx, GuildLeaveParams{...}) |
POST guild/kickout | client.Guild.KickGuildMember(ctx, GuildKickoutParams{...}) |
GET guild-mute/list | client.Guild.GetGuildMuteList(ctx, GuildMuteListParams{...}) |
POST guild-mute/create | client.Guild.CreateGuildMute(ctx, GuildMuteCreateParams{...}) |
POST guild-mute/delete | client.Guild.DeleteGuildMute(ctx, GuildMuteDeleteParams{...}) |
GET guild-boost/history | client.Guild.GetGuildBoostHistory(ctx, GuildBoostHistoryParams{...}) |
成员过滤示例:
返回值包含 user_count、online_count 和 offline_count。这三个值不保证简单相加相等。
6.4 Channel 与 ChannelUser
用来查询、创建和修改频道,也可以移动语音频道里的用户,或给角色、用户设置频道权限。
| HTTP 接口 | SDK 方法 |
|---|---|
GET channel/list | client.Channel.GetChannelList(ctx, ChannelListParams{...}) |
GET channel/view | client.Channel.View(ctx, ChannelViewParams{...}) |
POST channel/create | client.Channel.CreateChannel(ctx, CreateChannelParams{...}) |
POST channel/update | client.Channel.UpdateChannel(ctx, UpdateChannelParams{...}) |
POST channel/delete | client.Channel.DeleteChannel(ctx, ChannelDeleteParams{...}) |
GET channel/user-list | client.Channel.GetChannelUserList(ctx, ChannelUserListParams{...}) |
POST channel/move-user | client.Channel.MoveUsers(ctx, ChannelMoveUserParams{...}) |
POST channel/kickout | client.Channel.KickoutUser(ctx, ChannelKickoutParams{...}) |
GET channel-role/index | client.Channel.GetChannelRole(ctx, ChannelRoleViewParams{...}) |
POST channel-role/create | client.Channel.CreateChannelRole(ctx, CreateChannelRoleParams{...}) |
POST channel-role/update | client.Channel.UpdateChannelRole(ctx, UpdateChannelRoleParams{...}) |
POST channel-role/sync | client.Channel.SyncChannelRole(ctx, ChannelRoleSyncParams{...}) |
POST channel-role/delete | client.Channel.DeleteChannelRole(ctx, DeleteChannelRoleParams{...}) |
GET channel-user/get-joined-channel | client.ChannelUser.GetJoinedChannels(ctx, JoinedChannelParams{...}) |
获取子频道:
channel, err := client.Channel.View(ctx, kook.ChannelViewParams{
TargetID: channelID,
NeedChildren: ptr(true),
})
创建分组时只会发送 guild_id、name 和 is_category。普通频道可设置 parent_id、类型、人数上限和语音质量。
6.5 Message
频道消息支持列表、详情、发送、更新、删除、回应、Pipe 和置顶。
| HTTP 接口 | SDK 方法 |
|---|---|
GET message/list | client.Message.GetMessageList(ctx, MessageListParams{...}) |
GET message/view | client.Message.GetMessage(ctx, MessageViewParams{...}) |
POST message/create | client.Message.Create(ctx, MessageCreateParams{...}) |
POST message/update | client.Message.Update(ctx, UpdateMessageParams{...}) |
POST message/delete | client.Message.DeleteMessage(ctx, MessageDeleteParams{...}) |
GET message/reaction-list | client.Message.ReactionUsers(ctx, MessageReactionUsersParams{...}) |
POST message/add-reaction | client.Message.AddReaction(ctx, MessageReactionParams{...}) |
POST message/delete-reaction | client.Message.DeleteReaction(ctx, MessageDeleteReactionParams{...}) |
POST message/send-pipemsg | client.Message.SendPipe(ctx, SendPipeMessageParams{...}) |
POST message/pin | client.Message.PinMessage(ctx, MessagePinParams{...}) |
POST message/unpin | client.Message.UnpinMessage(ctx, MessageUnpinParams{...}) |
发送频道消息:
Nonce 可以帮助你对应一次请求和它的结果,但它不会让 KOOK 自动去掉重复消息。如果同一条通知可能被重复发送,需要自己记录是否已经发过。详见 KOOK 消息接口文档。
Pipe 的 TargetID 可省略,此时使用开发者平台中的管道配置:
6.6 UserChat 与 DirectMessage
UserChat 管理私信会话,DirectMessage 管理会话中的消息。
| HTTP 接口 | SDK 方法 |
|---|---|
GET user-chat/list | client.UserChat.GetUserChatList(ctx, UserChatListParams{...}) |
GET user-chat/view | client.UserChat.GetUserChat(ctx, UserChatViewParams{...}) |
POST user-chat/create | client.UserChat.CreateUserChat(ctx, UserChatCreateParams{...}) |
POST user-chat/delete | client.UserChat.DeleteUserChat(ctx, UserChatDeleteParams{...}) |
GET direct-message/list | client.DirectMessage.List(ctx, DirectMessageListParams{...}) |
GET direct-message/view | client.DirectMessage.View(ctx, DirectMessageViewParams{...}) |
POST direct-message/create | client.DirectMessage.Create(ctx, DirectMessageCreateParams{...}) |
POST direct-message/update | client.DirectMessage.Update(ctx, DirectMessageUpdateParams{...}) |
POST direct-message/delete | client.DirectMessage.Delete(ctx, DirectMessageDeleteParams{...}) |
GET direct-message/reaction-list | client.DirectMessage.ReactionUsers(ctx, DirectMessageReactionUsersParams{...}) |
POST direct-message/add-reaction | client.DirectMessage.AddReaction(ctx, DirectMessageReactionParams{...}) |
POST direct-message/delete-reaction | client.DirectMessage.DeleteReaction(ctx, DirectMessageDeleteReactionParams{...}) |
私信列表同时支持 chat_code 或 target_id,并支持 msg_id、flag、page 和 page_size:
6.7 GuildRole
| HTTP 接口 | SDK 方法 |
|---|---|
GET guild-role/list | client.GuildRole.GetRoleList(ctx, RoleListParams{...}) |
POST guild-role/create | client.GuildRole.CreateRole(ctx, CreateRoleParams{...}) |
POST guild-role/update | client.GuildRole.UpdateRole(ctx, UpdateRoleParams{...}) |
POST guild-role/delete | client.GuildRole.DeleteRole(ctx, DeleteRoleParams{...}) |
POST guild-role/grant | client.GuildRole.GrantRole(ctx, GrantRoleParams{...}) |
POST guild-role/revoke | client.GuildRole.RevokeRole(ctx, RevokeRoleParams{...}) |
更新角色时,只有非 nil 的可选字段会发送给 KOOK:
6.8 GuildEmoji
| HTTP 接口 | SDK 方法 |
|---|---|
GET guild-emoji/list | client.GuildEmoji.GetEmojiList(ctx, EmojiListParams{...}) |
POST guild-emoji/create | client.GuildEmoji.CreateEmoji(ctx, EmojiCreateParams{...}) |
POST guild-emoji/update | client.GuildEmoji.UpdateEmoji(ctx, EmojiUpdateParams{...}) |
POST guild-emoji/delete | client.GuildEmoji.DeleteEmoji(ctx, EmojiDeleteParams{...}) |
表情通过 multipart 上传 PNG,文件大小不能超过 256 KiB。参见 KOOK 服务器表情接口:
6.9 Blacklist
| HTTP 接口 | SDK 方法 |
|---|---|
GET blacklist/list | client.Blacklist.GetBlacklistUsers(ctx, BlacklistListParams{...}) |
POST blacklist/create | client.Blacklist.CreateBlacklistUser(ctx, BlacklistCreateParams{...}) |
POST blacklist/delete | client.Blacklist.DeleteBlacklistUser(ctx, BlacklistDeleteParams{...}) |
delMsgDays 最大为 7。执行成员管理前应检查机器人角色位置和权限。
6.10 Invite
| HTTP 接口 | SDK 方法 |
|---|---|
GET invite/list | client.Invite.GetInviteList(ctx, InviteListParams{...}) |
GET invite/invitees | client.Invite.GetInvitees(ctx, InviteeListParams{...}) |
POST invite/create | client.Invite.CreateInvite(ctx, CreateInviteParams{...}) |
POST invite/delete | client.Invite.DeleteInvite(ctx, DeleteInviteParams{...}) |
永久、无限次数邀请:
invite, err := client.Invite.CreateInvite(ctx, kook.CreateInviteParams{
GuildID: guildID,
Duration: ptr(0),
SettingTimes: ptr(-1),
})
删除邀请时可同时传 GuildID 或 ChannelID,帮助 KOOK 确认邀请所属的服务器或频道。
查询受邀用户和留存统计:
Page 是第几页,PageSize 是每页多少条,两者都要大于 0。把已有的邀请码填进 ID,或者把完整邀请链接填进 InviteURL;只填服务器 ID 可能会被 KOOK 拒绝。
上面的日期和状态都是筛选条件,按需要修改或省略。返回的 JoinedTime 和 ActiveTime 是毫秒时间戳。详见 KOOK 邀请接口文档。
6.11 Asset
| HTTP 接口 | SDK 方法 |
|---|---|
POST asset/create | client.Asset.Create(ctx, AssetCreateParams{...}) |
上传图片或文件后,会得到一个 Asset.URL,发送图片消息时可以使用这个地址。文件内容已经读进内存时用 Create;直接上传本地文件可以用 CreateFromPath(ctx, kook.AssetPathParams{Path: "文件路径"}),SDK 会处理上传格式。
6.12 Intimacy
| HTTP 接口 | SDK 方法 |
|---|---|
GET intimacy/index | client.Intimacy.GetIntimacy(ctx, IntimacyViewParams{...}) |
POST intimacy/update | client.Intimacy.UpdateIntimacy(ctx, UpdateIntimacyParams{...}) |
Intimacy 返回亲密度分数、社交信息、形象 URL、读取时间和 ImgList。
6.13 Friend
| HTTP 接口 | SDK 方法 |
|---|---|
GET friend | client.Friend.GetFriendsList(ctx, FriendListParams{...}) |
POST friend/request | client.Friend.SendFriendRequest(ctx, SendFriendRequestParams{...}) |
POST friend/handle-request | client.Friend.HandleFriendRequest(ctx, HandleFriendRequestParams{...}) |
POST friend/delete | client.Friend.DeleteFriend(ctx, DeleteFriendParams{...}) |
POST friend/block | client.Friend.BlockFriend(ctx, BlockFriendParams{...}) |
POST friend/unblock | client.Friend.UnblockFriend(ctx, UnblockFriendParams{...}) |
好友列表项是 FriendRelation,用户对象位于 FriendInfo。服务端返回 block 或 blocked 时都能正常读取。HandleFriendRequest 返回服务端的布尔处理结果。
6.14 Game
| HTTP 接口 | SDK 方法 |
|---|---|
GET game | client.Game.GetGameList(ctx, GameListParams{...}) |
POST game/create | client.Game.CreateGame(ctx, GameCreateParams{...}) |
POST game/update | client.Game.UpdateGame(ctx, GameUpdateParams{...}) |
POST game/delete | client.Game.DeleteGame(ctx, GameDeleteParams{...}) |
POST game/activity | client.Game.AddActivity(ctx, GameActivityParams{...}) |
POST game/delete-activity | client.Game.DeleteActivity(ctx, GameDeleteActivityParams{...}) |
GameActivityParams.DataType 使用 GameActivityTypeGame 或 GameActivityTypeMusic。游戏动态必须传 ID;音乐动态必须传 Singer 和 MusicName,并支持 cloudmusic、qqmusic 和 kugou。
6.15 Badge
| HTTP 接口 | SDK 方法 |
|---|---|
GET badge/guild | client.Badge.GetGuildBadge(ctx, BadgeParams{...}) |
这个接口返回的是图片。下面把返回的图片保存为 badge.png:
BinaryResponse 包含 Data、ContentType、StatusCode 和 ETag。样式 0、1、2 分别表示服务器名、在线人数、在线人数/总人数。
6.16 Thread
| HTTP 接口 | SDK 方法 |
|---|---|
GET category/list | client.Thread.GetThreadCategories(ctx, ThreadCategoryListParams{...}) |
POST thread/create | client.Thread.CreateThread(ctx, CreateThreadParams{...}) |
POST thread/reply | client.Thread.ReplyThread(ctx, ReplyThreadParams{...}) |
GET thread/view | client.Thread.GetThread(ctx, ThreadViewParams{...}) |
GET thread/list | client.Thread.GetThreadList(ctx, GetThreadListParams{...}) |
POST thread/delete | client.Thread.DeleteThread(ctx, ThreadDeleteParams{...}) |
GET thread/post | client.Thread.GetThreadPosts(ctx, GetThreadPostsParams{...}) |
Thread、Post 和 ThreadCategory 包含分区权限、媒体、标签、提及、频道引用、道具引用、时间、删除状态、统计和楼中楼回复。category 返回对象或 ID、媒体 type 返回字符串或数字时都能正常解码。
6.17 Voice
| HTTP 接口 | SDK 方法 |
|---|---|
POST voice/join | client.Voice.JoinVoiceChannel(ctx, VoiceJoinParams{...}) |
GET voice/list | client.Voice.GetJoinedVoiceChannels(ctx) |
POST voice/leave | client.Voice.LeaveVoiceChannel(ctx, VoiceLeaveParams{...}) |
POST voice/keep-alive | client.Voice.KeepAliveVoiceChannel(ctx, VoiceKeepAliveParams{...}) |
加入接口返回 RTP/RTCP 地址、SSRC、Payload Type 和码率。port 与 rtcp_port 无论返回数字还是字符串都能读取。保持连接时按 KOOK 要求周期性调用 keep-alive,不要用高频轮询替代正常推流。
6.18 Template
| HTTP 接口 | SDK 方法 |
|---|---|
GET template/list | client.Template.GetTemplateList(ctx) |
POST template/create | client.Template.CreateTemplate(ctx, params) |
POST template/update | client.Template.UpdateTemplate(ctx, params) |
POST template/delete | client.Template.DeleteTemplate(ctx, DeleteTemplateParams{...}) |
模板更新使用指针,因此可以明确清空测试数据或发送 type=0:
template, err := client.Template.UpdateTemplate(ctx, kook.UpdateTemplateParams{
ID: templateID,
TestData: ptr(""),
Type: ptr(0),
})
7. OAuthClient
普通机器人使用 Bot Token 就够了。如果你要让其他用户授权应用读取他们的账号数据,再用 OAuth。
下面用授权码换取用户的 Access Token。authorizationCode 来自授权后的回调,callbackURL 是你在开发者平台配置的回调地址。SDK 会使用 /api/oauth2/token,并处理这个接口的请求格式。
| HTTP 接口 | SDK 方法 |
|---|---|
POST oauth2/token | kook.NewOAuthClient().ExchangeToken(ctx, OAuthTokenParams{...}) |
返回中使用 expire_in 或 expires_in 时,结果都会写入 ExpiresIn。授权码通常只能使用一次。redirect_uri 必须与发起授权时和开发者平台白名单中的值一致。
使用 OAuth Access Token 创建普通 Client:
8. WebSocket 事件
WebSocket 会让机器人和 KOOK 保持连接,收到新消息时运行你注册的处理函数。第 3 节已经用过它,这里再看如何添加其他处理逻辑。协议细节可查 KOOK WebSocket 文档。
8.1 连接
下面的代码会打印收到的文字消息。把它加到已有客户端的程序中,并像第 3 节一样在连接后等待退出信号,程序才能持续收消息。
Event 的 Go 字段与 JSON 字段对应如下:
| Go 字段 | JSON 字段 | 说明 |
|---|---|---|
S | s | 信令类型 |
SN | sn | 事件序号 |
ChannelType | channel_type | GROUP、PERSON 等来源类型 |
Type | type | 普通消息类型或 255 |
TargetID | target_id | 频道或私聊目标 ID |
AuthorID | author_id | 作者 ID |
Content | content | 原始 JSON 内容 |
Extra | extra | 原始 JSON 扩展数据 |
MsgID | msg_id | 消息 ID |
MsgTimestamp | msg_timestamp | 消息时间戳 |
Nonce | nonce | 请求关联值 |
VerifyToken | verify_token | Webhook 验证字段 |
Content 和 Extra 保存的是原始 JSON。读取普通文字可以用 TextContent();需要读取其他字段时,定义一个结构体,再用 DecodeContent 或 DecodeExtra 解码。
8.2 普通消息类型
| 常量 | 值 | 内容形式 |
|---|---|---|
MessageTypeText | 1 | 字符串 |
MessageTypeImage | 2 | 图片 URL 字符串 |
MessageTypeVideo | 3 | 视频 URL 字符串 |
MessageTypeFile | 4 | 文件 URL 字符串 |
MessageTypeAudio | 8 | 音频 URL 字符串 |
MessageTypeKMD | 9 | KMarkdown 字符串 |
MessageTypeCard | 10 | 卡片 JSON 字符串 |
MessageTypeItem | 12 | 道具对象 |
MessageTypeSystem | 255 | 系统事件,按 extra.type 分发 |
KOOK 客户端里看起来普通的一句文字,发给机器人时也是 KMarkdown,类型为 9。所以文字处理函数要注册两次:一次接收类型 1,一次接收类型 9,像第 8.1 节那样写即可。
TextContent() 会返回原来的文字和格式标记。例如 **hello** 读出来仍是 **hello**,需要自行处理加粗等格式。
频道消息和私信都可能触发这个函数。回复时先看 event.ChannelType:GROUP 是频道消息,用 client.Message.Create;PERSON 是私信,用 client.DirectMessage.Create。第 3 节的例子只回复频道消息,收到私信会直接跳过。
对象型道具消息:
8.3 34 种系统事件
成员加入、频道修改、按钮点击等都属于系统事件。它们的消息类型统一是 255,具体发生了什么写在 extra.type 里。注册这些事件时用 OnSystemEvent,不要用 OnMessage。
| 类别 | SystemEventType 常量与值 |
|---|---|
| 回应 | SystemEventAddedReaction=added_reaction、SystemEventDeletedReaction=deleted_reaction |
| 频道消息 | updated_message、deleted_message、pinned_message、unpinned_message |
| 私信消息 | updated_private_message、deleted_private_message、private_added_reaction、private_deleted_reaction |
| 频道 | added_channel、updated_channel、deleted_channel |
| 服务器成员 | joined_guild、exited_guild、updated_guild_member、guild_member_online、guild_member_offline |
| 角色 | added_role、deleted_role、updated_role |
| 服务器 | updated_guild、deleted_guild、added_block_list、deleted_block_list |
| 表情 | added_emoji、removed_emoji、updated_emoji |
| 语音频道用户 | joined_channel、exited_channel |
| 当前用户 | user_updated、self_joined_guild、self_exited_guild |
| Card 按钮 | message_btn_click |
每个表格值都有同名 Go 常量,例如 updated_guild 对应 kook.SystemEventUpdatedGuild。
服务器信息更新的 Body 与 Guild 字段一致。enable_open 返回 bool 或 int、open_id 返回 string 或 int 时都能正常读取:
SystemEvent.Body 是原始 JSON。处理事件时,可以按自己的业务字段定义结构,再调用 DecodeBody。
监听所有原始事件:
ws.OnAnyEvent(func(event *kook.Event) {
log.Printf("sn=%d type=%d raw_extra=%s", event.SN, event.Type, event.Extra)
})
8.4 断线重连和消息处理速度
SDK 会发送心跳来检查连接是否还在,断线后会尝试重连。每条事件有一个编号 SN,SDK 在处理函数正常返回后记录处理进度,重连时尝试接着收消息。
重连后,同一条消息可能再收到一次。如果收到消息会发通知、加积分或写数据库,可以用 event.MsgID 记录已经处理过的消息,重复收到时跳过。这就是常说的“幂等”。
平时不用调整下面这些参数。遇到大消息、消息积压,或者需要修改超时时间时,再传入 WebSocketOptions:
消息处理函数按顺序执行。一个函数如果花了很久,后面的消息就要排队;排队太多,SDK 会记录错误并尝试重连。
例如,机器人收到消息后需要下载一个大文件,可以先把下载任务放进一个有容量限制的任务队列,再让消息处理函数返回。不要收到一条消息就无限制地启动一个 goroutine,否则消息多时容易耗尽资源。
看到 ErrEventDispatchQueueFull 时,先找是不是某个处理函数太慢。这个错误会出现在后台日志里,不会从已经返回成功的 Connect 再传出来。
ReadLimit 限制一条消息的大小,默认是 8 MiB,解压后的内容也受这个限制。
第一次连接最多尝试 11 次,每次失败后多等 5 秒再试。已经上线后断线,最多自动重连 10 次。重连次数用完就会停下来,所以长期运行时还要检查机器人是否在线,别只看进程有没有退出。
Connect 用来开始连接,Close 用来结束连接。已经关闭的 WebSocket 实例不能再连接,需要新建一个。调用 Close 前,先让你自己的任务处理完。
SDK 只有在保存了网关地址、会话 ID 和大于 0 的处理进度时,才会尝试恢复旧会话。网络中断或程序重启时仍可能漏消息;重要任务要另外保存处理记录,并准备重试。
9. Webhook
如果你已经有一台能从公网访问的服务器,也可以用 Webhook 收消息。KOOK 会把事件 POST 到你配置的地址,程序处理收到的请求。请求格式见 KOOK Webhook 文档。
9.1 接收消息
先在开发者平台选择 Webhook 模式,记下 Verify Token 和 Encrypt Key。服务启动并配好 HTTPS 后,再把完整的 Webhook 地址填到平台里,地址的路径要和程序一致,例如 /kook/webhook。
注册消息处理函数的写法和 WebSocket 相同。下面是启动、监听和退出的代码片段;想先看能直接运行的程序,可以看仓库里的 webhook_bot 示例。
messageHandler 是你自己的文字消息处理函数,可以按第 8.1 节的方式定义。buttonHandler 处理卡片按钮点击,参数是 *kook.SystemEvent;rawHandler 查看所有事件,参数是 *kook.Event。如果暂时用不到按钮或原始事件,可以删掉相应的注册行。
上面的服务监听 127.0.0.1:8080,需要通过第 12.4 节的反向代理让 KOOK 访问到它。SDK 自带的服务器已经设置了请求超时:读取 Header 最多 5 秒,读写最多 10 秒,空闲连接保留 60 秒。
如果接入已有的 HTTP 服务,可以把 handler.HandleRequest 注册到你自己的路由里;退出时先停止接收请求,再调用 handler.Shutdown(ctx)。
9.2 配置和返回状态
平台验证 Webhook 地址时,SDK 会自动响应。收到事件后,也会校验 Token、解密和解压,通常不需要自己写这些代码。
| 配置 | 默认值 |
|---|---|
| 单个请求体大小 | 最多 1 MiB,约 1 MB |
| 解压后的内容大小 | 最多 8 MiB,约 8 MB |
| 等待处理的消息 | 最多 256 条,按顺序处理 |
| 重复消息记录 | 保留 10 分钟,最多 10000 条 |
解压支持 gzip、deflate,以及没有 Content-Encoding Header 的 zlib 数据。解密使用 AES-256-CBC,16、24 字节的密钥会先补零到 32 字节。处理函数发生 panic 时,SDK 会捕获错误并写入日志。
Verify Token 用来确认请求来自 KOOK,不能为空。Encrypt Key 用来解密消息,平台未启用加密时可以留空;非空时必须是 16、24 或 32 字节。填写错误会导致服务创建失败,或收到加密消息时无法解密。
排查请求失败时,可以对照 HTTP 状态:
| 场景 | HTTP 状态 |
|---|---|
| 非 POST 请求 | 405 |
| 请求体或解压数据超限 | 413 |
| Verify Token 不匹配 | 401 |
| 顶层 JSON、压缩或解密失败 | 400 |
| 事件解析或注入的去重器失败 | 500 |
| 队列已满或处理器已关闭 | 503 |
| 事件已入队,或 SN 被去重器判定为重复 | 200 |
收到 200 时,事件可能还在排队,处理函数未必已经执行完。SDK 会先记录消息编号,再放进队列;处理函数报错或 panic 后,不会删除这条记录。KOOK 已经拿到了 200,因此后续业务失败需要你自己处理重试。
普通机器人可以先用默认配置。涉及订单、积分等重要数据时,需要自己把收到的事件写进数据库或可靠的消息队列,再处理和重试;如果要求写入成功后才返回 200,需要自己实现这一层 HTTP 接收逻辑。
SDK 只根据 SN 判断是否重复。如果平台重新使用同一个编号,而旧记录还没过期,新消息也可能被跳过。编号小于或等于 0 时,内建去重器不会记录。
退出时调用 Shutdown(ctx),SDK 会先停止接收请求,再等队列里的消息处理完。超过你设置的时间还没结束,会返回超时错误。
9.3 同时运行多个实例
只运行一个机器人程序时,可以用默认去重器。它的记录放在内存里,程序重启就会消失。
如果同一个机器人的请求会分给多个程序,需要把去重记录放进共享数据库或 Redis,通过 WithWebhookDeduplicator 传给 SDK。这里需要你自己实现下面这个接口:
redisDeduper 代表你写的共享去重器,SDK 没有自带 Redis 实现。CheckAndStore 要把“检查编号”和“记下编号”作为一次操作完成,否则两个程序可能同时处理同一条消息。多个机器人共用存储时,记录也要按机器人分开。
WithWebhookDispatch(256, 1) 的第二个参数只能是 1;第一个参数是队列容量,要大于 0。
10. 错误码与处理
10.1 响应与 KOOKError
普通 HTTP API 返回:
调用接口后,先检查 err。KOOK 返回的接口错误会放进 *kook.KOOKError,里面有错误码、说明和请求 ID。下面的写法可以把这些信息打印出来,方便排查:
10.2 常见错误码
下面列出常见错误及处理方向。不同接口可能返回更具体的业务码,排查时以响应中的 code 和 message 为准。
| 代码 | 含义 | 常见处理 |
|---|---|---|
| 0 | 成功 | 继续处理 data |
| 40000 | 参数错误 | 检查必填字段、类型和 ID |
| 40100 | Token 无效 | 检查 Token 类型,必要时重置 Token |
| 40300 | 权限不足 | 检查机器人权限与角色位置 |
| 40400 | 资源不存在 | 检查服务器、频道、用户或消息 ID |
| 40500 | 请求方法不允许 | 检查 SDK 与 API 版本 |
| 42900 | 请求过于频繁 | 遵循 Retry-After,降低并发 |
| 50000 | KOOK 内部错误 | 有界重试并记录请求 ID |
| 50200 | 网关错误 | 短暂退避重试 |
| 50300 | 服务不可用 | 延迟重试并告警 |
| 50400 | 网关超时 | 检查网络后重试 |
10.3 错误实例与处理
401/40100:
KOOK API错误 [40100]: 认证失败,Token无效
检查是否误把 OAuth Access Token 当成 Bot Token;Bot 使用 TokenTypeBot,OAuth 使用 TokenTypeBearer。
403/40300:
KOOK API错误 [40300]: 权限不足
确认机器人已加入目标服务器、拥有目标频道可见权限,并且机器人最高角色高于要管理的成员或角色。
404/40400:
确认 ID 来自同一个服务器。消息详情使用 msg_id,频道详情使用 target_id,私信详情还需要 chat_code。
429/42900:
429 表示请求太快了。SDK 通常已经按你的配置重试过;如果关闭了重试,也可能直接返回这个错误。先降低请求频率,别在报错后立刻循环调用发送接口。稍后再发时,记得检查这条消息是不是已经发过。
OAuth redirect_uri 错误:
回调地址必须与授权 URL 中使用的值完全相同,包括协议、域名、端口、路径和末尾斜杠。
Webhook 400:
依次检查请求体大小、反向代理是否改写压缩数据、Encrypt Key、PKCS7 填充以及 JSON 格式。
WebSocket 收不到事件:
检查机器人是否在线、是否有频道可见权限、是否同时监听了文本(1)和 KMarkdown(9)、是否误把系统事件注册成普通整数消息,以及处理器是否持续阻塞导致派发队列满。
10.4 重试
默认最多重试 3 次,加上第一次请求,一共最多请求 4 次。失败后会先等 1 秒,再逐次延长等待,最长等 30 秒。如果 KOOK 要求等更久,SDK 会按 Retry-After 等待;取消请求的 context 可以结束等待。
方法策略如下:
| 方法 | 默认自动重试 |
|---|---|
GET、HEAD、OPTIONS、PUT、DELETE | 可重试错误,包括 429、常见 5xx 和识别出的临时网络错误 |
POST、PATCH | 只在服务端明确返回 429 时重试 |
| 其他方法 | 默认不重试 |
下面把重试次数改为 4 次。发送消息等写入操作仍保持 RetryNonIdempotent: false,避免一次操作因为重试执行两遍:
请求超时,有时只是你没收到响应,KOOK 可能已经执行成功了。重新发送消息、创建资源或修改成员前,先确认结果,避免重复操作。
把 RetryNonIdempotent 改为 true 后,POST/PATCH 遇到可重试的网络错误或 5xx 也会重试,使用前要处理好重复执行。需要自定义哪些错误可重试时,可以传入 RetryableError;默认的 IsRetryableError 可能识别不到经过多层包装的网络错误。
11. 测试
11.1 测试自己的机器人
在你创建的 my-kook-bot 目录里运行:
go test ./...
go vet ./...
go build ./...
这三个命令分别检查测试、常见代码问题和编译结果。它们不会运行 kook.go 仓库中的集成测试。
11.2 测试 SDK 仓库
如果只是用 SDK 写机器人,看上一节就够了。下面的命令供修改 SDK 源码时使用,要在 SDK 仓库目录执行。
先做不用连接 KOOK 的检查:
make test
make verify
make test 运行普通离线测试;make verify 会执行格式检查、普通测试、race、go vet、golangci-lint、govulncheck 和全仓库构建。质量工具应使用项目锁定的版本:
golangci-lint 2.12.2
govulncheck 1.1.4
这些检查在本地运行即可。需要自动检查时,可以自己配置 CI;仓库当前没有 GitHub Actions 工作流。Go 版本的维护情况可查 官方发布记录。
要用真实 KOOK 数据检查接口,可以运行 make test-integration。先准备下面这些环境变量;ID 要来自机器人能访问的测试服务器,KOOK_TEST_GUILD_ID 的获取方法见第 3.4 节。缺少变量时,测试会失败:
写入测试使用 make test-mutation。当前测试会在指定文字频道创建、更新并删除一条真实消息,应使用独立测试服务器和测试机器人,并只在明确设置 KOOK_ENABLE_MUTATION_TESTS=1 时运行。
OAuth 测试使用 make test-oauth,并要求:
KOOK_OAUTH_CLIENT_ID
KOOK_OAUTH_CLIENT_SECRET
KOOK_OAUTH_CODE
KOOK_OAUTH_REDIRECT_URI
授权码通常只能使用一次,每次运行 OAuth 测试前需要重新获取。
测试用的 Token 和授权码也要保密,不要提交到仓库。配置 CI 时,先用离线测试;连接真实 KOOK 的测试另外设置凭据和运行条件。
12. Linux 部署
先根据运行环境选择方式:
| 场景 | 方式 |
|---|---|
| 单台 Linux 服务器长期运行 | 编译程序并配置后台运行和开机自启 |
| 已使用容器平台 | 构建 Docker 镜像 |
| 使用 Webhook 接收事件 | 在前两种方式外增加 HTTPS 反向代理 |
12.1 编译机器人
先把第 3 节的机器人项目放到 Linux 服务器上,在项目目录里编译并试运行:
go build -trimpath -o kook-bot .
./kook-bot
生产部署请根据 Go 发布与支持周期选择仍受安全维护的工具链,并固定到经过测试的补丁版本。
运行前要在同一个终端中设置 KOOK_TOKEN。确认机器人能够连接和处理消息后,按 Ctrl+C 退出,再配置后台运行和开机自启。
如果在 macOS、Windows 或另一种 CPU 架构上构建 Linux amd64 程序,使用交叉编译:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o kook-bot .
12.2 配置后台运行和开机自启
关闭终端后还想让机器人继续运行,可以交给 systemd。它会在后台启动程序,也能设置开机启动和异常退出后重启。下面以常见的 Linux systemd 环境为例,系统配置命令需要 sudo 权限。
创建专用用户和目录:
创建只允许管理员读取的环境变量文件:
sudo install -m 600 -o root -g root /dev/null /etc/kook-bot.env
sudoedit /etc/kook-bot.env
文件内容为:
KOOK_TOKEN=你的Token
新建 /etc/systemd/system/kook-bot.service,填入下面的内容。它会以 kookbot 用户运行 /opt/kook-bot/kook-bot,并从刚才的环境文件读取 Token:
启动与查看日志:
sudo systemctl daemon-reload
sudo systemctl enable --now kook-bot
sudo systemctl status kook-bot
sudo journalctl -u kook-bot -f
status 显示 active (running) 表示服务已经启动。修改环境变量或替换二进制后,执行 sudo systemctl restart kook-bot。
12.3 使用 Docker 运行
如果服务器已经在用 Docker,可以在机器人项目根目录新建 Dockerfile,写入下面的内容。前半段负责编译,后半段只放运行所需的程序:
构建镜像使用 Go 官方镜像。升级 Go 时,修改第一行的版本号,再重新构建;需要每次构建都使用完全相同的镜像时,可以进一步固定镜像 digest。
构建和运行:
docker build -t kook-bot .
sudo docker run -d --name kook-bot --restart unless-stopped \
--env-file /etc/kook-bot.env kook-bot
sudo docker logs -f kook-bot
上面沿用了 systemd 示例里的 /etc/kook-bot.env。如果跳过了那一节,先创建这个文件并写入 KOOK_TOKEN=你的Token。它只允许管理员读取,所以运行命令用了 sudo。
使用 rootless Docker 时,把环境文件放在运行 Docker 的用户能读取的位置,修改 --env-file 路径即可。Token 放在环境文件或部署平台的密钥配置里,别写进 Dockerfile。
12.4 配置 Webhook 反向代理
Webhook 必须能被 KOOK 公网访问。常见结构为:
KOOK -> HTTPS 443 (Nginx/Caddy/负载均衡) -> 127.0.0.1:8080/kook/webhook
先把域名解析到服务器,并开放 80 和 443 端口。使用 Caddy 时,可以从下面的配置开始:
bot.example.com {
reverse_proxy /kook/webhook 127.0.0.1:8080
}
把 bot.example.com 换成自己的域名。Caddy 取得 HTTPS 证书后,将 https://bot.example.com/kook/webhook 填入 KOOK 开发者平台。
代理需要原样转发请求体和 Content-Encoding。多实例部署还需要共享 SN 去重存储,避免同一事件被不同实例重复处理。
13. 上线前再检查一下
本地能回复 ping 后,再确认几件事:
- 代码和日志里没有 Token、授权码或私信内容。
- 机器人只拿到了用得上的权限,测试和正式使用的频道分开。
- 请求有超时时间;发送频繁时能处理 429。
- 断线后能发现机器人离线,重复收到消息时不会重复执行重要操作。
- 退出时能等自己的任务完成,并关闭 WebSocket 或 Webhook 服务。
- 按第 11.1 节检查过自己的机器人项目。
使用 Webhook 时,还要配好 HTTPS 和 Verify Token。需要保存的任务先写入数据库,别只留在内存里;同时运行多个实例时,处理好共享去重记录。
兼容性说明
如果你在维护旧机器人,升级时留意下面这些地方。新项目可以先跳过这一小节。
KOOK 的部分字段有时会用不同格式返回,比如数字和字符串。SDK 会处理已经支持的格式;如果解码失败,保留错误日志,方便定位是哪一个字段。
常见的 v1.1.1 旧参数写法仍可使用,新代码按本文的 Params 写法来写。参数数量或类型不对时,会返回 *kook.ValidationError。Client.Role 和 Client.Emoji 是旧名字,分别与 GuildRole 和 GuildEmoji 指向同一个服务。
返回值冲突的方法保留当前 API,并提供以下迁移入口:
UpdateEmojiLegacy
HandleFriendRequestLegacy
UpdateIntimacyLegacy
ReplyThreadLegacy
一些旧接口现在只保留了方法名,调用会返回 ErrUnsupportedEndpoint,不会发送请求。可以用 errors.Is(err, kook.ErrUnsupportedEndpoint) 判断。
旧项目升级后先编译一次。结构体字段、返回值和接口定义有变化的地方,仍可能需要修改。
14. 版本和后续升级
本文示例使用 80804a2 提交。安装命令里的长版本号是 Go 根据这次提交生成的,保持原样即可。把项目的 go.mod 和 go.sum 一起保存,下次安装就能使用同一版本。
升级 SDK 时,先在测试项目里编译,再试一下频道消息、私信和你用到的命令。查看对应版本的 README、测试说明 和 变更记录,确认代码的写法有没有变化。
如果安装旧的 v1.3.0 遇到 checksum mismatch,先使用第 3.1 节的版本。2026-10-15 检查时,旧版本存在校验不一致和源码编译失败的问题,因此本文没有使用它。不要通过关闭 Go 校验来绕过这个错误。
更多平台字段和接口细节,可以查 KOOK 开发者文档。