源码副本已存档至
raw/repos/feishu-ids/(feishu_ids.js + README.md)。 单文件 Node.js 18+ 脚本,零第三方依赖(仅用内置 fetch):通过飞书自建应用凭证查询用户 user_id/open_id 和群 chat_id。
用法
node feishu_ids.js \
--app-id <larkAppId> --app-secret <larkAppSecret> \
[--user-name <姓名>] [--mobile <手机号>] [--chat-name <群名>]凭证与查询条件也可走环境变量:LARK_APP_ID / LARK_APP_SECRET / FEISHU_USER_NAME / FEISHU_MOBILE / FEISHU_CHAT_NAME。
飞书 ID 体系(本工具要解决的核心问题)
| ID | 前缀 | 作用域 | 说明 |
|---|---|---|---|
user_id | 无前缀 | 租户内唯一 | 租户内标识用户,跨应用通用 |
open_id | ou_ | 应用内唯一 | 同一用户在不同应用里 open_id 不同,机器人消息/事件里拿到的就是它 |
union_id | on_ | 开发者内唯一 | 同一开发者的多个应用间打通 |
chat_id | oc_ | 群 | 即通常说的 group_id,机器人发消息/管理群的凭据 |
botmux 机器人协作时 @ 人用的就是 open_id(--mention ou_xxx),本脚本是拿到这些 ID 的最快途径(见 botmux)。
鉴权:tenant_access_token
// POST /auth/v3/tenant_access_token/internal { app_id, app_secret }
// 返回 { code, msg, tenant_access_token, expire } —— 注意没有 data 字段自建应用(internal)模式换取租户级 token,有效期 2 小时,脚本每次运行重新获取,不做缓存。后续所有请求带 Authorization: Bearer <token>。
通用请求封装与飞书成功约定
async function api(method, path, body) {
const res = await fetch(BASE + path, { ... });
const data = await res.json();
if (data.code !== 0) { ... throw err with err.code ... }
return data.data || {};
}飞书 OpenAPI 约定:HTTP 200 不代表成功,必须 body.code === 0。错误码在 code/msg 中,脚本把 code 挂到异常对象上供上层做分支(权限降级就靠它)。
查用户:两级降级策略
优先 —— 手机号精确换取(需权限 contact:user.id:readonly):
// POST /contact/v3/users/batch_get_id?user_id_type=user_id|open_id
// body: { mobiles: ["13800000000"] }对 user_id、open_id 两种类型各调一次拿全。遇到权限错误码 99991672/99991663 返回 null,触发降级。
降级 —— 遍历可见范围按姓名/手机号匹配(需权限 contact:user.base:readonly):
// GET /contact/v3/scopes → 应用可见范围内全部用户 open_id 列表
// GET /contact/v3/users/{open_id} → 逐个取详情,name 精确匹配或 mobile 尾部匹配手机号匹配用 u.mobile.replace(/[^0-9]/g, "").endsWith(mobileNum) 容忍国际区号前缀(如 +86)。单个用户查详情失败跳过不中断。
关键边界:只能查到应用「可用范围」内的用户(飞书默认仅创建者可见,需去开放平台扩可用范围)。
查群:分页拉取 + 群名匹配
// GET /im/v1/chats?page_size=100[&page_token=...] (需权限 im:chat:readonly)
do {
const data = await api("GET", `/im/v1/chats?page_size=100${...}`);
chats.push(...(data.items || []));
pageToken = data.has_more ? data.page_token : "";
} while (pageToken);飞书列表接口统一分页协议:page_size + page_token 请求,has_more + page_token 响应,do-while 翻到底。
关键边界:只返回机器人已加入的群;查不到时脚本会列出机器人当前所有群并提示「先把机器人拉进目标群」——这是 API 限制,无降级方案。
所需权限
| 权限 | 用途 | 必需? |
|---|---|---|
contact:user.id:readonly | 手机号/邮箱换取 user_id | 否(缺失自动降级) |
contact:user.base:readonly | 遍历可见范围用户拿详情 | 是(降级方案依赖) |
im:chat:readonly | 列出机器人所在群 | 是 |
权限修改后需在开放平台发布新版本才生效。
常见错误码
| code | 含义 | 处理 |
|---|---|---|
99991672 | Access denied,权限未开通 | 按提示链接申请权限并发布版本 |
99991663 / 99991661 | token 缺失/权限校验失败 | 脚本重新取 token / 走降级 |
| 查不到用户 | 不在应用可用范围 | 开放平台扩「可用范围」 |
| 查不到群 | 机器人未入群 | 拉机器人进群后重查 |
关键技术点小结
| 技术点 | 实现 | 为什么这么做 |
|---|---|---|
| 鉴权 | internal 模式 tenant_access_token | 自建应用最简路径,无需用户授权流程 |
| 成功判断 | body.code === 0 而非 HTTP 状态码 | 飞书 OpenAPI 统一约定 |
| 权限降级 | 捕获 99991672/99991663 转 null → 换方案 | 不同租户开通权限不一,脚本自适应 |
| ID 获取 | batch_get_id 两次调用(user_id/open_id 两种 user_id_type) | 该接口一次只返回一种类型 |
| 分页 | do-while + page_token/has_more | 飞书列表接口标准协议 |
| 零依赖 | Node 18+ 内置 fetch | 运维脚本免 npm install |
相关页面
- botmux — 飞书话题群桥接,@mention 用 open_id,本脚本可查 open_id
- web-terminal-pty — code-reading 目录首篇(源码讲解)