Operit ToolPkg:把飞书自建应用的双向对话机器人能力,整理成 Operit 可调用的工具。 支持 WebSocket 长连接收消息、消息队列读取、C2C/群消息发送、多模型 API 配置切换。
| 工具 | 说明 |
|---|---|
feishu_bot_configure |
保存 AppID / AppSecret / 模型配置,可自动重启服务 |
feishu_bot_status |
读取配置摘要、服务状态、消息队列积压 |
feishu_bot_service_start |
启动 WebSocket 收消息服务(支持强制重启换票) |
feishu_bot_service_stop |
停止后台服务 |
feishu_bot_receive_events |
读取事件队列(默认消费) |
feishu_bot_clear_events |
清空事件队列 |
feishu_bot_test_connection |
验证凭证并获取 WS 端点 |
feishu_bot_send_text_message |
发送文本消息 / 被动回复 |
内置能力:
- 🔌 异步解耦:首帧 ACK 立即回(
biz_rt=1),模型生成后异步发送 + 完成 ACK,规避飞书 ~19s 重投窗口 - 🔁 消息去重(可持久化):服务端固定重投策略下,
[Dedup]兜底保证只回复一次;去重表落盘至feishu_gateway.dedup.json,进程重启后仍能拦截历史重投 - 🔇 日志级别可控:默认
INFO,可用--log-level参数或FEISHU_LOG_LEVEL环境变量临时开启DEBUG排查 - 🔄 多模型热切换:DeepSeek / GLM / LongCat 等任意 OpenAI 兼容端点,改配置即生效
- 🧩 Operit 模型库联动:传
model_config_id自动同步对应 API Key
- 在 飞书开放平台 创建自建应用
- 开启权限:
im:message、im:message:send_as_bot - 事件订阅选择长连接方式
- 记录
AppID/AppSecret
feishu_bot_service_start(restart: true)
| 模型 | 端点 | 端到端延迟 | 备注 |
|---|---|---|---|
glm-4-flash |
open.bigmodel.cn/api/paas/v4 |
~2s | 免费可用 |
deepseek-v4-flash |
api.deepseek.com/v1 |
~3s | 直连最快 |
LongCat-2.0 |
api.longcat.chat/openai/v1 |
~4s | 推理模型(带思维链) |
切换模型时必须同时显式传
model_endpoint+model_name,否则只换model_config_id会导致 key/endpoint 错配。
现象:直接编辑 feishu_config.json 里的 model_name,重启网关服务后,进程启动参数仍是旧值。
根因:包运行时读配置走内存缓存,一旦加载就不再回读文件:
async function readCachedJsonStoreAsync(path, sanitize) {
const entry = getCachedJsonStoreEntry(path);
if (!entry.loaded) { // ← 只在首次加载时读文件
const raw = await readJsonObjectFileAsync(path);
entry.value = ...; entry.loaded = true;
}
return cloneJsonObject(entry.value); // ← 之后永远返回内存快照
}Operit 主进程一直存活,所以缓存里的值永不刷新。重启飞书网关子进程也没用,因为缓存属于主进程。
正确做法:✅ 通过 feishu_bot_configure 工具修改(走 updatePersistedConfigAsync,同时更新内存 + 落盘);❌ 不要直接编辑 config 文件。
现象:feishu_bot_configure 后服务重连,报:
WS 错误: Handshake status 401 Unauthorized
handshake-autherrcode: 1000040348 (auth resp code error)
退出码反复重连,退避 3→6→12s,永不成功。
根因:每次调 configure 都会重新申请 WS 端点(新 ticket),但旧连接仍持有旧 ticket。飞书 ticket 是一次性的,旧连接被踢 + 新 ticket 冲突 → 授权链断裂,票据作废。
正确做法:配置变更后用 feishu_bot_service_start(restart: true) 强制全新启动,它会重新申请 ticket。(单纯 stop 可能被守护进程拉活,杀不干净)
根因(feishu_runtime.js):
const hit = ds.entries.find(e => e.id === targetConfigId);
if (hit && hit.apiKey) {
await updatePersistedConfigAsync({ model_api_key: hit.apiKey }); // 只写 key!
}切模型时若只传 model_config_id,会出现 endpoint 是旧的、key 是新的 的错配,请求必然失败。
正确做法:切换时同时传 model_endpoint + model_name + model_config_id 三件套。
以下为实测中观察到的平台侧/工具调用层行为,不影响功能使用,但需按规避方式操作。
现象:以下调用偶发返回空白错误,无有效信息:
feishu_bot_service_stop—— 无参数工具易触发feishu_bot_configure传入含uuid格式的model_config_id时- 传入超长参数值时
feishu_bot_configure携带test_connection: true时
推测原因:平台工具调用层对无参调用 / uuid 参数 / 超长值存在 schema 过滤或校验限制,具体规则未公开。
规避方式:
| 场景 | 规避 |
|---|---|
| 需要停服务 | 改用 feishu_bot_service_start(restart: true) 一步完成停+启 |
| 切模型(含 uuid id) | 分步调用:先只传 model_config_id + model_name,避免一次传三个字段 |
| 想测连接 | 去掉 test_connection,改用 feishu_bot_test_connection 单独测 |
| 仍失败 | 简化参数(减少字段数)后重试,通常可绕过 |
现象:调用 stop 后服务很快又被拉起。
原因:包内守护逻辑 ensureFeishuServiceStarted 会在检测到服务停止时自动重启;此外网关进程运行在 proot 容器内,宿主侧 ps/kill 看不到其真实 PID,无法从外部强杀。
规避方式:不要依赖 stop,直接用 feishu_bot_service_start(restart: true) 显式重启。
现象:用户发一条消息,机器人反复推送同一问答,跨多个时段不停重复(如几十秒重投一次,持续数十分钟)。
根因:飞书长连接协议要求 ACK 帧的 SeqID 与请求帧一一对应。原实现在三处 send_frame 调用点(ping / 首帧 ACK / 完成 ACK)均未传 seq_id,导致 ACK 的 SeqID 恒为 0,飞书判定事件未确认,按递增间隔持续重投。
修复:三处 send_frame 补齐 seq_id=seq_id, log_id=log_id,ACK 回填请求帧的真实 SeqID。
实证:修复后实测多条消息,frame.SeqID 均为非 0 且 ACK 正确回填(如 1524829487 / 1524981587 / 1525211493),event_frames 正常增长后稳定,每条消息只被处理一次、只回复一次。
现象:去重表仅在内存中,进程重启(或网关 recycling)后清零,历史重投无法被拦截。
修复:新增 _load_dedup() / _save_dedup(),启动时从 feishu_gateway.dedup.json 载入,记录新事件后原子落盘(写 .tmp 再 os.replace);容错缺失文件与损坏 JSON,max_processed_events=500 LRU 淘汰最旧。
实证:进程重启日志出现 [Dedup] 已从磁盘载入 N 条历史去重记录,并在真实环境中成功拦截了一次飞书重投([Dedup] 重复事件已忽略)。
现象:[ACK-DIAG] event frame headers={...} 单行超长(含完整帧头),在 INFO 级别下大量输出。
修复:该行由 log.info 降为 log.debug;日志级别由写死的 DEBUG 改为默认 INFO,可通过 --log-level 或 FEISHU_LOG_LEVEL 覆盖。
.
├── manifest.json # ToolPkg 清单
├── src/ # TypeScript 源码
│ ├── main.ts
│ ├── packages/feishu_bot.ts # 工具入口
│ ├── shared/
│ │ ├── feishu_common.ts # 常量 / 工具函数
│ │ ├── feishu_openapi.ts # 飞书 OpenAPI 封装
│ │ ├── feishu_runtime.ts # configure 逻辑 + DataStore 读取
│ │ ├── feishu_service.ts # 网关进程生命周期
│ │ └── feishu_state.ts # 配置读写(含内存缓存)
│ └── ui/feishu_settings/index.ui.ts
├── dist/ # 编译产物(与 src 同步)
└── resources/
└── feishu_gateway_service.py # 网关服务(WS 长连接 + 模型调用)
⚠️ 本项目不包含任何真实凭据;示例中的cli_xxxx/ API Key 均为占位符- 配置文件位于
ToolPkg.getConfigDir("com.operit.feishu_bundle")/feishu_config.json,请勿提交至公开仓库 - 建议在
.gitignore中排除feishu_config.json、*.log、__pycache__/
基于 Operit 平台 ToolPkg 体系开发。