- Octo产品管家Q13源码级回答(2026-09-22)
- octo-server源码:pkg/errcode/bot_api.go、pkg/httperr/respond.go、modules/bot_api/auth.go、pkg/cardmsg/、modules/bot_api/ratelimit.go、modules/bot_api/file.go
Bot API 错误码与排障 v1.0
本文档基于octo-server源码核查(`pkg/errcode/bot_api.go`、`pkg/httperr/respond.go`等),供Bot/Agent集成开发者参考。
所有错误码和限制均为源码级确认。标注⚠️的为待复核/待Owner补充项。
租户隔离唯一单元是Space,无organization概念。
一、错误响应统一格式
1.1 标准错误结构【✅源码确认 `pkg/httperr/respond.go`】
{
"code": "err.server.bot_api.request_invalid",
"message": "Request body is invalid JSON",
"error": {
"http_status": 400
}
}
- code:错误码字符串,格式为
err.server.bot_api.<具体错误>,用于程序化判断
- message:人类可读的错误描述
- error.http_status:HTTP状态码
1.2 成功响应
二、完整错误码对照表
2.1 400 Bad Request 系列
| code |
触发场景 |
排查建议 |
request_invalid |
请求体JSON格式错误(BindJSON失败)、缺少必填字段、字段类型错误 |
检查Content-Type是否为application/json,JSON格式是否合法,必填字段是否都传了 |
limit_exceeded |
批量操作超限(members>200 或 message_ids>100) |
分批请求,members≤200,message_ids≤100 |
content_too_large |
消息内容超过字段上限 |
各字段具体上限【✅源码确认 A-4补证 2026-09-22】:群MD/Thread MD默认 10240字节(group.GetGroupMdMaxSize(),env TS_GROUPMDMAXSIZE可调);语音转写上下文 10000字符(rune计);OBO persona_prompt 4096字节(oboPersonaPromptMaxBytes)。错误响应的Details携带field/max_size具体值 |
file_too_large |
上传文件超过大小限制 |
默认上限100MB,硬顶512MB(system_setting: file.max_size_kb可配置) |
file_type_unsupported |
文件类型不在白名单、命中扩展名黑名单、魔数(magic number)不匹配 |
检查文件扩展名和实际文件格式,空扩展名会被拒绝 |
member_not_human |
尝试将非人类用户(如Bot)添加为普通群成员 |
检查成员UID是否为真实用户 |
# 1. 未设置Content-Type
curl -X POST http:///v1/bot/messages/send \
-H "Authorization: Bearer " \
# ❌ 缺少 -H "Content-Type: application/json"
# 2. JSON语法错误(逗号、引号等)
# 3. 字段名拼写错误(如recv_id写成recvId)
# 4. 必填字段为空字符串
2.2 401 Unauthorized【✅源码确认 `modules/bot_api/auth.go`】
| code |
触发场景 |
排查建议 |
ErrBotAPIAuthFailed |
鉴权失败(单一反枚举码) |
见下方详细说明 |
请求进入
↓
从 Authorization: Bearer 提取token
↓
根据token前缀路由:
├─ app_ 前缀 → App Bot鉴权
├─ bf_ 前缀 / legacy格式 → User Bot鉴权
└─ 无前缀/无法识别 → 401
↓
查库验证token有效性
├─ 有效 → 注入botInfo到context,继续处理
└─ 无效/过期 → 401 ErrBotAPIAuthFailed
# 1. 确认header格式正确
curl -s -H "Authorization: Bearer " \
http:///v1/bot/info
# 2. 确认token前缀匹配Bot类型
# App Bot token: app_xxxxxxx
# User Bot token: bf_xxxxxxx (或legacy格式)
# 3. 确认token未过期、未被撤销
2.3 403 Forbidden 系列
| code |
触发场景 |
解决方案 |
not_group_member |
Bot不是该群组成员 |
通过API或管理后台将Bot加入群组 |
not_group_admin |
需要群组管理员权限的操作(踢人、改群信息、解散等) |
将Bot设为群组管理员 |
group_disbanded |
目标群组已解散 |
使用有效的群组ID |
not_space_member |
Bot/用户不在目标Space中 |
将Bot/用户添加到对应Space |
app_bot_dm_only |
App Bot尝试在群组中发消息(仅支持私信场景) |
使用支持群组的Bot类型,或改用私信 |
not_friend |
尝试给非好友发私信(非好友关系限制场景) |
先添加好友关系 |
message_edit_forbidden |
无权限编辑该消息(非消息发送者/超出编辑时限) |
只能编辑自己发送的消息,注意编辑时限 |
bot_unavailable |
App Bot未发布(草稿状态) |
在管理后台/插件市场发布Bot后使用 |
2.4 404 Not Found 系列
| code |
触发场景 |
排查建议 |
group_not_found |
群组ID不存在 |
确认groupID正确,群组未被删除 |
message_not_found |
消息ID不存在 |
确认messageID正确,消息未被撤回/删除 |
user_not_found |
用户UID不存在 |
确认userID正确,用户在当前Space中 |
2.5 413 Payload Too Large
| code |
触发场景 |
排查建议 |
payload_too_large |
请求体过大(语音转写代理等场景) |
减小请求体大小,注意Nginx层也可能有限制 |
2.6 429 Too Many Requests【✅源码确认 `modules/bot_api/ratelimit.go`】
HTTP/1.1 429 Too Many Requests
Retry-After: 5
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Scope: business
2.7 500 Internal Server Error
| code |
触发场景 |
排查建议 |
im_token_failed 等 |
服务内部错误(IM token获取失败等) |
响应消息脱敏,服务端日志有详细错误信息,联系运维查看服务端日志 |
# 查看服务端500相关日志
docker compose logs octo-server | grep -iE "error|panic|im_token"
# K8s: kubectl logs -n octo | grep -iE "error|panic"
三、卡片消息(type=17)发送失败排障【✅源码确认 `pkg/cardmsg/`】
3.1 卡片硬限制清单
| 限制项 |
阈值 |
超限时错误 |
说明 |
| 整包大小 |
512 KiB |
ErrCardTooLarge |
卡片JSON序列化后的总大小 |
| 节点数 |
≤ 200 |
ErrCardTooManyNodes |
卡片中所有元素节点总数 |
| 嵌套深度 |
≤ 16 |
(校验失败) |
元素嵌套层数上限 |
| card_version |
必须 = "1.5" |
ErrCardProfileUnsupported |
版本不匹配直接拒绝 |
| 元素/动作白名单 |
必须在白名单内 |
(校验失败) |
不支持的元素类型 |
| URL格式 |
必须合法 |
(校验失败) |
URL格式校验 |
3.2 卡片排障步骤
# 1. 检查卡片JSON大小
cat card.json | wc -c # 单位字节,512KiB = 524288字节
# 2. 检查card_version
grep card_version card.json
# 必须是 "card_version": "1.5"
# 3. 简化卡片测试
# 先用最简单的卡片模板测试连通性,逐步添加元素定位问题
3.3 最简测试卡片
{
"type": 17,
"card_version": "1.5",
"version": "1.5",
"body": {
"elements": [
{
"type": "text",
"text": "测试卡片",
"color": "#333333"
}
]
}
}
3.4 注意事项
- 无独立校验端点:卡片校验在消息发送入口内联执行,不提供单独的validate接口
- 建议在开发环境先发送测试消息验证卡片格式
- 卡片渲染兼容性需在各端(Web/iOS/Android)实际测试
四、限流机制与重试策略【✅源码确认】
4.1 限流架构
| 维度 |
说明 |
| 算法 |
令牌桶(Token Bucket),Redis+Lua原子实现 |
| 隔离维度 |
按robotID隔离(一个Bot被限流不影响其他Bot) |
| 限流类别 |
business(业务请求)/ heartbeat(心跳/长轮询)/ register(注册) |
4.2 默认限流参数【✅源码确认 `modules/bot_api/ratelimit.go` + `modules/common/system_settings.go:1382-1400`,2026-09-22 A-2补证】
| 类别 |
默认RPS |
默认Burst |
说明 |
| business |
20.0 |
200 |
单Bot业务端点令牌桶 |
| heartbeat |
1.0 |
10 |
下界硬约束:心跳key TTL 60s,低于此一次限流就断联 |
| register |
0.5 |
10 |
自愈链路最后一环,未鉴权写入口 |
| 类别 |
默认RPS |
默认Burst |
env |
| register IP |
100 |
500 |
OCTO_BOT_RATELIMIT_REGISTER_IP_RPS/BURST |
| heartbeat IP |
500 |
1500 |
OCTO_BOT_RATELIMIT_HEARTBEAT_IP_RPS/BURST |
| batch users IP |
20 |
40 |
OCTO_BOT_RATELIMIT_USERS_BATCH_IP_RPS/BURST |
per-bot层参数env:`OCTO_BOT_RATELIMIT_{BUSINESS,HEARTBEAT,REGISTER}_RPS/BURST`;TTL由Lua从参数推ceil(burst/rps×2)。
⚠️ 限流参数可通过环境变量热调整,不需要重启服务。
4.3 429响应头
| 头 |
含义 |
Retry-After |
建议等待秒数 |
X-RateLimit-Limit |
当前窗口的请求限额 |
X-RateLimit-Remaining |
当前窗口剩余配额 |
X-RateLimit-Scope |
限流类别(business/heartbeat/register) |
4.4 推荐重试策略
import time
import requests
def bot_api_call(url, headers, data, max_retries=3):
for attempt in range(max_retries):
resp = requests.post(url, headers=headers, json=data)
if resp.status_code == 200:
return resp.json()
if resp.status_code == 429:
retry_after = int(resp.headers.get('Retry-After', 5))
# 指数退避: retry_after * 2^attempt
wait = retry_after * (2 ** attempt)
print(f"限流,等待{wait}秒后重试...")
time.sleep(wait)
continue
# 其他错误直接抛出
resp.raise_for_status()
raise Exception("重试次数耗尽")
4.5 避免限流的最佳实践
- 使用批量接口:`POST /v1/bot/users/batch`支持批量操作(members≤200, message_ids≤100),避免逐条发送
- 控制并发:限制并发请求数,不要开过多协程/线程
- 监控限流头:根据`X-RateLimit-Remaining`动态调整发送速率
- 错峰发送:避免在短时间内集中发送大量消息
- 消息合并:将多条短消息合并为一条结构化消息(如使用卡片)
五、长轮询事件接口排障【✅源码确认】
5.1 长轮询事件保留窗口TTL【✅源码确认 A-1补证 2026-09-22】
- 事件队列(ZSet)TTL =
Robot.MessageExpire,默认 7天(octo-lib config.go:439 time.Hour*24*7;tsdd.yaml注释示例messageExpire: 7d)
- 事件写入后对整个队列key执行
EXPIRE(key, MessageExpire)(robot/api.go:296)——即队列TTL随每次新事件写入刷新
- 门铃key TTL =
BellTTL = 5分钟(pkg/botevent/bell.go:38,只需活过最长30s hold)
- 长轮询 hold 上限 =
maxEventWaitSeconds = 30s(events_wait.go:86)
5.2 接口信息
| 项目 |
值 |
| 方法 |
POST(不是GET) |
| 路径 |
/v1/bot/events |
| 断点参数 |
event_id(int64,exclusive,单调递增) |
| limit参数 |
默认20,上限100 |
| wait参数 |
长轮询等待秒数,服务端clamp到[0, 30]秒 |
5.3 常见问题
| 问题 |
原因 |
解决方案 |
| 立即返回空数组 |
有backlog时"先读后等",不进入hold |
使用返回的最新event_id继续轮询 |
| GET方法返回404 |
接口只支持POST |
改用POST |
| 重复收到同一事件 |
event_id使用inclusive而非exclusive |
event_id传上次收到的最后一个事件ID(exclusive) |
| 轮询断开后丢失事件 |
断点续传窗口有限 |
⚠️事件保留TTL待Owner补充,建议断线后尽快重连 |
| wait参数设置>30s无效果 |
服务端clamp到30s |
客户端设置wait≤30,建议10秒 |
5.4 正确的长轮询实现
event_id = 0
while True:
try:
resp = requests.post(
f"{host}/v1/bot/events",
headers={"Authorization": f"Bearer {token}"},
json={"event_id": event_id, "limit": 20, "wait": 10},
timeout=15 # 客户端硬超时 > wait时间
)
events = resp.json()
for event in events.get("events", []):
process_event(event)
event_id = event["event_id"] # 更新断点
except requests.Timeout:
continue # 超时直接重试
except Exception as e:
time.sleep(2) # 出错后等2秒重试
六、媒体上传排障【✅源码确认 `modules/bot_api/file.go`】
6.1 上传路径【✅源码确认 bot_api.go:447-451 路由注册,A-5补证 2026-09-22】
| 路径 |
方法 |
用途 |
适用场景 |
/v1/bot/file/upload(另有别名/v1/bot/upload) |
POST |
服务端中转直传 |
小文件、简单集成 |
/v1/bot/upload/credentials |
GET |
获取STS临时密钥,客户端直传COS |
大文件、高并发 |
/v1/bot/upload/presigned |
GET |
预签名URL直接上传 |
与credentials二选一 |
下载/v1/bot/file/download/*path |
GET |
302跳转到presigned URL |
文件下载 |
6.2 ✅端点差异已解决(A-5补证 2026-09-22)
已确认:源码中两个端点同时存在(`bot_api.go:450-451`):`GET /v1/bot/upload/credentials`(STS临时密钥)与 `GET /v1/bot/upload/presigned`(预签名URL)均已注册,不是命名冲突,是两条不同的上传路径。之前"文档写presigned、代码是credentials"的差异是因为两者都真实存在。集成时任选其一即可,建议优先credentials(STS)。
6.3 上传限制【✅源码确认 `modules/file/policy.go` + `const.go:183-239`,A-3补证 2026-09-22】
| 限制项 |
值 |
说明 |
| 文件大小 |
默认100MB,硬顶512MB |
由system_setting file.max_size_kb控制,MaxUploadSize()是全部7个检查点唯一真源 |
| 扩展名 |
白名单约70种+黑名单31种双门 |
空扩展名直接拒绝;大小写归一化 |
| 魔数校验 |
有 |
文件实际内容必须匹配扩展名类型 |
- 图片:.jpg/.jpeg/.png/.gif/.bmp/.webp/.ico/.heic/.heif/.tiff/.tif
- 文档:.pdf/.doc/.docx/.xls/.xlsx/.ppt/.pptx/.txt/.csv/.rtf/.odt/.ods/.odp/.md/.html/.htm
- iWork:.key/.numbers/.pages;电子书:.epub/.mobi
- 纯文本/数据:.toml/.ini/.log/.tsv/.ndjson;字幕:.srt/.vtt/.ass
- 音频:.mp3/.wav/.aac/.flac/.ogg/.wma/.m4a/.amr/.opus/.aiff
- 视频:.mp4/.avi/.mov/.wmv/.flv/.mkv/.webm/.m4v
- 压缩包:.zip/.rar/.7z/.tar/.gz/.bz2/.xz
- 其他:.json/.xml/.yaml/.yml;安装包:.dmg/.pkg/.deb/.rpm/.appimage
部署可通过 system_setting / env(`DM_FILE_EXTRA_ALLOWED`/`DM_FILE_EXTRA_BLOCKED`)追加白/黑名单:allowed = (base ∪ extra) − blocked,黑名单永远压过放开。客户端可从 `/v1/common/appconfig` 拉当前生效白名单(仅下发allowed不下发blocked,防探测)。
6.4 上传常见错误
| 错误 |
原因 |
解决方案 |
| file_too_large (400) |
文件超过大小限制 |
压缩文件或调整file.max_size_kb配置 |
| file_type_unsupported (400) |
扩展名不支持或魔数不匹配 |
确认文件格式正确、扩展名匹配 |
| 上传到Nginx返回413 |
Nginx client_max_body_size限制 |
在Nginx配置中增大client_max_body_size |
| STS凭证获取失败 |
MinIO/COS配置问题 |
检查对象存储配置和密钥 |
七、鉴权流程详解【✅源码确认 `modules/bot_api/auth.go`】
7.1 鉴权流程图
HTTP请求到达
↓
提取 Authorization: Bearer
│
├─ 无Authorization头 → 401
├─ token格式错误 → 401
│
├─ token前缀 "app_" → App Bot鉴权路径
│ ├─ 查库验证App Bot token
│ ├─ 验证Bot状态(已发布?)
│ └─ 通过 → 注入botInfo到context
│
├─ token前缀 "bf_" / legacy → User Bot鉴权路径
│ ├─ 查库验证User Bot token
│ └─ 通过 → 注入botInfo到context
│
└─ 前缀无法识别 → 401
│
└─ token无效/过期/未发布 → 401(统一ErrBotAPIAuthFailed)
7.2 Bot类型对比
| 特性 |
App Bot |
User Bot |
| Token前缀 |
app_ |
bf_ / legacy |
| 群组消息 |
⚠️ app_bot_dm_only时仅支持私信 |
支持 |
| 发布要求 |
必须发布才能使用(bot_unavailable) |
无需发布 |
| 插件市场 |
在插件市场发布 |
不经过插件市场 |
八、常见集成错误案例
案例1:发送消息返回401
# 错误请求(缺少Bearer前缀)
curl -X POST http://octo.example.com/v1/bot/messages/send \
-H "Authorization: app_xxxxx" \
-H "Content-Type: application/json" \
-d '{"recv_id": "xxx", "content": "hello"}'
# → 401 ErrBotAPIAuthFailed
# 正确请求
curl -X POST http://octo.example.com/v1/bot/messages/send \
-H "Authorization: Bearer app_xxxxx" \
-H "Content-Type: application/json" \
-d '{"recv_id": "xxx", "content": "hello"}'
# → 200 OK
案例2:卡片消息发送失败
// 错误:card_version不是1.5
{
"type": 17,
"card_version": "1.0",
...
}
// → ErrCardProfileUnsupported
// 正确
{
"type": 17,
"card_version": "1.5",
"version": "1.5",
...
}
案例3:批量添加成员超限
# 错误:members数量201人
curl -X POST http://octo.example.com/v1/bot/groups/members/add \
-H "Authorization: Bearer " \
-d '{"group_id":"xxx", "member_ids":["u1","u2",...,"u201"]}'
# → 400 limit_exceeded
# 正确:分批,每批≤200人
案例4:长轮询用GET方法
# 错误:使用GET
curl http://octo.example.com/v1/bot/events?event_id=0
# → 404 Not Found
# 正确:使用POST
curl -X POST http://octo.example.com/v1/bot/events \
-H "Authorization: Bearer " \
-d '{"event_id":0, "limit":20, "wait":10}'
# → 200 OK
九、错误码快速查找索引
| 场景 |
HTTP码 |
code |
章节 |
| Token错误 |
401 |
ErrBotAPIAuthFailed |
2.2 |
| Bot未发布 |
403 |
bot_unavailable |
2.3 |
| 不在群里 |
403 |
not_group_member |
2.3 |
| 不是群管理员 |
403 |
not_group_admin |
2.3 |
| 群已解散 |
403 |
group_disbanded |
2.3 |
| 不在Space里 |
403 |
not_space_member |
2.3 |
| App Bot仅私信 |
403 |
app_bot_dm_only |
2.3 |
| 非好友 |
403 |
not_friend |
2.3 |
| 无编辑权限 |
403 |
message_edit_forbidden |
2.3 |
| 群不存在 |
404 |
group_not_found |
2.4 |
| 消息不存在 |
404 |
message_not_found |
2.4 |
| 用户不存在 |
404 |
user_not_found |
2.4 |
| JSON格式错 |
400 |
request_invalid |
2.1 |
| 批量超限 |
400 |
limit_exceeded |
2.1 |
| 文件过大 |
400 |
file_too_large |
2.1/6.4 |
| 文件类型错 |
400 |
file_type_unsupported |
2.1/6.4 |
| 内容过大 |
400 |
content_too_large |
2.1 |
| 请求体过大 |
413 |
payload_too_large |
2.5 |
| 限流 |
429 |
(读Retry-After) |
四 |
| 卡片过大 |
400 |
ErrCardTooLarge |
三 |
| 卡片节点过多 |
400 |
ErrCardTooManyNodes |
三 |
| 卡片版本错 |
400 |
ErrCardProfileUnsupported |
三 |
| 服务内部错误 |
500 |
im_token_failed等 |
2.7 |
十、待确认项更新状态(2026-09-22 A类源码补证后)
| 项目 |
状态 |
说明 |
| ~~business/heartbeat默认限流参数~~ |
✅已确认 |
per-bot层:business 20/200、heartbeat 1/10、register 0.5/10(默认dry-run观察模式);per-IP层:register 100/500、heartbeat 500/1500、batch 20/40 |
| ~~上传端点命名差异~~ |
✅已解决 |
credentials与presigned两端点同时存在(bot_api.go:450-451),是两条路径非命名冲突 |
| ~~长轮询事件保留TTL~~ |
✅已确认 |
默认7天(Robot.MessageExpire),队列key随新事件写入刷新TTL |
| ~~文件类型白名单完整列表~~ |
✅已确认 |
白名单约70种+黑名单31种,全文见§6.3 |
| ~~消息内容content_too_large阈值~~ |
✅已确认 |
群MD 10240B/语音上下文 10000rune/OBO persona 4096B,详见§2.1 |
| ~~Bot token过期策略~~ |
✅已确认 |
token无过期时间字段(robot表bot_token不过期,查询仅校验status=1),仅手动重置或Bot删除失效 |
| ~~metrics端点~~ |
✅已确认 |
独立抓取服务器,默认:9090,env DM_METRICS_ADDR可调,见监控文档 |
| Webhook回调失败重试机制 |
⚠️待补充 |
平台级webhook非Bot主通道 |
| 各端版本兼容矩阵 |
⚠️待Owner |
官方release notes |