@lark-apaas/coding-steering 0.1.6-alpha.8 → 0.1.6-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/steering/design-stack/skills/.gitkeep +0 -0
  3. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +174 -0
  4. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +621 -0
  5. package/steering/nestjs-react-fullstack/skills/authz-guide/references/management-page-spec.md +505 -0
  6. package/steering/nestjs-react-fullstack/skills/authz-guide/references/runtime-role-controller-spec.md +203 -0
  7. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +90 -0
  8. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +216 -0
  9. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +405 -0
  10. package/steering/nestjs-react-fullstack/skills/devops-guide/SKILL.md +119 -0
  11. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +582 -0
  12. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +357 -0
  13. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +513 -0
  14. package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +118 -0
  15. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +177 -0
  16. package/steering/nestjs-react-fullstack/skills/user-management-best-practices/SKILL.md +142 -0
  17. package/steering/design-stack/skills/client-add-aily-web-chat/SKILL.md +0 -139
  18. package/steering/design-stack/skills/client-builtins-user-service/SKILL.md +0 -628
  19. package/steering/design-stack/skills/code-fix/SKILL.md +0 -246
  20. package/steering/design-stack/skills/feishu/SKILL.md +0 -270
  21. package/steering/design-stack/skills/feishu/references/approval.md +0 -214
  22. package/steering/design-stack/skills/feishu/references/attendance.md +0 -163
  23. package/steering/design-stack/skills/feishu/references/bitable.md +0 -309
  24. package/steering/design-stack/skills/feishu/references/calendar.md +0 -190
  25. package/steering/design-stack/skills/feishu/references/contacts.md +0 -160
  26. package/steering/design-stack/skills/feishu/references/doc.md +0 -256
  27. package/steering/design-stack/skills/feishu/references/drive.md +0 -103
  28. package/steering/design-stack/skills/feishu/references/events.md +0 -198
  29. package/steering/design-stack/skills/feishu/references/id-convert.md +0 -128
  30. package/steering/design-stack/skills/feishu/references/messaging.md +0 -207
  31. package/steering/design-stack/skills/feishu/references/oauth.md +0 -164
  32. package/steering/design-stack/skills/feishu/references/perm.md +0 -90
  33. package/steering/design-stack/skills/feishu/references/wiki.md +0 -164
  34. package/steering/design-stack/skills/user-identity/SKILL.md +0 -300
@@ -1,90 +0,0 @@
1
- # 权限管理 (Permission)
2
-
3
- > 开放平台文档(Markdown 版):https://open.larkoffice.com/document/server-docs/docs/permission/overview
4
-
5
- 使用 `@larksuiteoapi/node-sdk` 在 NestJS 中管理飞书云文档的协作者权限。
6
-
7
- ## 所需权限
8
-
9
- | 权限标识 | 说明 |
10
- |----------|------|
11
- | `drive:permission` | 管理文档/文件的协作者权限 |
12
-
13
- > **敏感操作警告**:权限管理涉及文档访问控制,添加/移除协作者会直接影响用户的文档可见性。操作前请确认目标对象和权限级别。
14
-
15
- ## 列出协作者
16
-
17
- ```typescript
18
- const res = await client.drive.permissionMember.list({
19
- path: { token: 'ABC123' },
20
- params: { type: 'docx' },
21
- });
22
- const members = res.data?.items ?? [];
23
- // members: [{member_type, member_id, perm, name}, ...]
24
- ```
25
-
26
- ## 添加协作者
27
-
28
- ```typescript
29
- const res = await client.drive.permissionMember.create({
30
- path: { token: 'ABC123' },
31
- params: { type: 'docx', need_notification: false },
32
- data: {
33
- member_type: 'email',
34
- member_id: 'user@example.com',
35
- perm: 'edit',
36
- },
37
- });
38
- ```
39
-
40
- ## 移除协作者
41
-
42
- ```typescript
43
- await client.drive.permissionMember.delete({
44
- path: { token: 'ABC123', member_id: 'user@example.com' },
45
- params: { type: 'docx', member_type: 'email' },
46
- });
47
- ```
48
-
49
- ## Token 类型参考
50
-
51
- | 类型 | 说明 |
52
- |------|------|
53
- | `doc` | 旧版文档 |
54
- | `docx` | 新版文档 |
55
- | `sheet` | 电子表格 |
56
- | `bitable` | 多维表格 |
57
- | `folder` | 文件夹 |
58
- | `file` | 上传的文件 |
59
- | `wiki` | 知识库节点 |
60
- | `mindnote` | 思维导图 |
61
-
62
- ## 成员类型参考
63
-
64
- | 类型 | 说明 |
65
- |------|------|
66
- | `email` | 邮箱地址 |
67
- | `openid` | 用户 open_id |
68
- | `userid` | 用户 user_id |
69
- | `unionid` | 用户 union_id |
70
- | `openchat` | 群聊 open_id |
71
- | `opendepartmentid` | 部门 open_id |
72
- | `groupid` | 用户组 ID |
73
- | `wikispaceid` | 知识空间 ID |
74
-
75
- ## 权限级别参考
76
-
77
- | 权限值 | 说明 |
78
- |--------|------|
79
- | `view` | 仅查看 |
80
- | `edit` | 可编辑 |
81
- | `full_access` | 完全访问(可管理权限) |
82
-
83
- ## Common Mistakes
84
-
85
- | 错误 | 正确做法 |
86
- |------|----------|
87
- | token 类型与文件实际类型不匹配 | `type` 参数必须与文件实际类型一致(docx/sheet/bitable 等) |
88
- | 用 wiki URL 的 token 直接操作权限 | Wiki 节点需用 `wiki` 类型,或先获取 `obj_token` 用对应类型 |
89
- | 添加协作者时成员不存在 | 确认 member_id 正确,email 需要是飞书注册邮箱 |
90
- | 移除自身的 full_access 权限 | 文档至少需要一个管理员,避免移除最后一个 full_access 成员 |
@@ -1,164 +0,0 @@
1
- # 知识库 (Wiki)
2
-
3
- > 开放平台文档(Markdown 版):https://open.larkoffice.com/document/server-docs/docs/wiki-v2/wiki-overview
4
-
5
- 使用 `@larksuiteoapi/node-sdk` 在 NestJS 中操作飞书知识库。
6
-
7
- ## 所需权限
8
-
9
- | 权限标识 | 说明 |
10
- |----------|------|
11
- | `wiki:wiki` | 读写知识库 |
12
- | `wiki:wiki:readonly` | 只读知识库 |
13
-
14
- > 机器人需被添加为知识空间成员才能访问:知识空间 → 设置 → 成员管理 → 添加机器人。
15
-
16
- ## 从 URL 提取 Token
17
-
18
- URL 格式:`https://xxx.feishu.cn/wiki/{token}`
19
-
20
- ```typescript
21
- const url = 'https://xxx.feishu.cn/wiki/ABC123def';
22
- const token = new URL(url).pathname.split('/wiki/')[1]; // 'ABC123def'
23
- ```
24
-
25
- ## 列出知识空间
26
-
27
- ```typescript
28
- const res = await client.wiki.space.list({});
29
- const spaces = res.data?.items ?? [];
30
- // spaces: [{space_id, name, description, visibility}, ...]
31
- ```
32
-
33
- > 如果返回空列表,说明机器人未被添加到任何知识空间。
34
-
35
- ## 列出节点
36
-
37
- ```typescript
38
- // 列出空间根节点
39
- const res = await client.wiki.spaceNode.list({
40
- path: { space_id: '7xxx' },
41
- });
42
- const nodes = res.data?.items ?? [];
43
- // nodes: [{node_token, obj_token, obj_type, title, has_child}, ...]
44
-
45
- // 列出子节点
46
- const childRes = await client.wiki.spaceNode.list({
47
- path: { space_id: '7xxx' },
48
- params: { parent_node_token: 'wikcnXXX' },
49
- });
50
- ```
51
-
52
- ## 获取节点详情
53
-
54
- ```typescript
55
- const res = await client.wiki.space.getNode({
56
- params: { token: 'ABC123def' }, // 从 URL 提取的 token
57
- });
58
- const node = res.data?.node;
59
- // node: {node_token, space_id, obj_token, obj_type, title, parent_node_token, has_child, creator}
60
- ```
61
-
62
- > **关键**:返回的 `obj_token` 是实际文档/表格的 token,需用它来调用 docx/bitable 等 API。
63
-
64
- ## 创建节点
65
-
66
- ```typescript
67
- const res = await client.wiki.spaceNode.create({
68
- path: { space_id: '7xxx' },
69
- data: {
70
- obj_type: 'docx', // 节点类型
71
- node_type: 'origin', // 固定值
72
- title: '新页面',
73
- // parent_node_token: 'wikcnXXX', // 可选:父节点
74
- },
75
- });
76
- const node = res.data?.node;
77
- // node: {node_token, obj_token, obj_type, title}
78
- ```
79
-
80
- ### obj_type 取值
81
-
82
- | 值 | 说明 |
83
- |------|------|
84
- | `docx` | 新版文档(默认) |
85
- | `doc` | 旧版文档 |
86
- | `sheet` | 电子表格 |
87
- | `bitable` | 多维表格 |
88
- | `mindnote` | 思维导图 |
89
- | `file` | 文件 |
90
- | `slides` | 幻灯片 |
91
-
92
- ## 移动节点
93
-
94
- ```typescript
95
- await client.wiki.spaceNode.move({
96
- path: { space_id: '7xxx', node_token: 'wikcnXXX' },
97
- data: {
98
- target_space_id: '7yyy', // 目标空间(不传则同空间内移动)
99
- target_parent_token: 'wikcnYYY', // 目标父节点
100
- },
101
- });
102
- ```
103
-
104
- ## 重命名节点
105
-
106
- ```typescript
107
- await client.wiki.spaceNode.updateTitle({
108
- path: { space_id: '7xxx', node_token: 'wikcnXXX' },
109
- data: { title: '新标题' },
110
- });
111
- ```
112
-
113
- ## Wiki-Doc 工作流(关键)
114
-
115
- 知识库页面的内容读写必须通过 docx API,流程:
116
-
117
- ```typescript
118
- // 1. 获取节点详情 → 拿到 obj_token
119
- const nodeRes = await client.wiki.space.getNode({
120
- params: { token: wikiToken },
121
- });
122
- const objToken = nodeRes.data?.node?.obj_token;
123
-
124
- // 2. 用 obj_token 作为 doc_token 读取文档
125
- const contentRes = await client.docx.document.rawContent({
126
- path: { document_id: objToken },
127
- });
128
-
129
- // 3. 用 obj_token 作为 doc_token 写入文档
130
- const convertRes = await client.docx.document.convert({
131
- data: { content_type: 'markdown', content: '# 新内容\n\n正文...' },
132
- });
133
- await client.docx.documentBlockChildren.create({
134
- path: { document_id: objToken, block_id: objToken },
135
- data: { children: convertRes.data?.blocks ?? [] },
136
- });
137
- ```
138
-
139
- > **重要**:不要用 `node_token` 或 URL 中的 `token` 直接调用 docx API,必须用 `getNode()` 返回的 `obj_token`。
140
-
141
- ## 搜索不可用
142
-
143
- Wiki API 不提供搜索功能。获取内容需通过以下方式:
144
-
145
- - 通过 `spaceNode.list()` 浏览节点树
146
- - 通过 `space.getNode()` + URL 中的 token 直接查询
147
-
148
- ## 知识库访问设置
149
-
150
- 机器人需要被添加为知识空间成员才能访问:
151
-
152
- 1. 打开知识空间 → 设置 → 成员管理
153
- 2. 添加机器人应用
154
- 3. 参考:https://open.feishu.cn/document/server-docs/docs/wiki-v2/wiki-qa
155
-
156
- ## Common Mistakes
157
-
158
- | 错误 | 正确做法 |
159
- |------|----------|
160
- | 用 wiki URL 中的 token 直接调用 docx API | 必须先 `getNode()` 获取 `obj_token`,再用 `obj_token` 调用 docx API |
161
- | 列出空间返回空但实际有内容 | 机器人未被添加为空间成员 |
162
- | 尝试搜索知识库内容 | Wiki API 不支持搜索,只能通过 `list` 浏览或 `getNode` 查询 |
163
- | 创建节点忘记传 `node_type: 'origin'` | `node_type` 是必填字段,值固定为 `'origin'` |
164
- | 混淆 `node_token` 和 `obj_token` | `node_token` 是知识库节点标识,`obj_token` 是实际文档标识 |
@@ -1,300 +0,0 @@
1
- ---
2
- name: user-identity
3
- description: "Use when getting current user info/profile, displaying user name/avatar/email, converting miaoda userId to lark_user_id, or reading req.userContext fields (userId/roles/tenantId): useCurrentUserProfile, AuthNPaasService, FeishuID conversion. 触发词:用户身份, 用户信息, 用户资料, 当前用户, userProfile, useCurrentUserProfile, 飞书ID, FeishuID, 飞书用户ID, lark_user_id, 用户ID转换, AuthNPaasService, 用户上下文, userContext, userContext.roles, 用户角色, 当前用户角色, 获取请求者角色, 展示用户, 显示用户, 用户面板, 我是谁, 获取用户"
4
- steering: true
5
- steering-topic: user_identity
6
- match-template-name: design-stack
7
- ---
8
-
9
- # 用户身份与上下文
10
-
11
- 本 skill 专注于**用户身份**:ID 体系、`req.userContext` 字段、`useCurrentUserProfile`、妙搭 ↔ 飞书 ID 转换。
12
-
13
- **接口认证**(`@NeedLogin()` 装饰器、`AuthNPaasGuard` opt-in 模式、公开接口处理、401)请使用 [`authn-guide`](../authn-guide/SKILL.md) skill。
14
-
15
- ## 零、ID 体系警告(CRITICAL)
16
-
17
- ⚠️ "user_id" 这个字面在本项目里指**三个不同的东西**,必须先区分清楚再写代码:
18
-
19
- **作为身份 ID 字段名(指代具体某个用户的 ID 值):**
20
-
21
- | 出现位置 | 实际含义 | 体系 |
22
- |---|---|---|
23
- | `useCurrentUserProfile().user_id`、`req.userContext.userId` | 妙搭用户 ID(纯数字字符串) | 妙搭 |
24
- | `useCurrentUserProfile().lark_user_id`、`AuthNPaasService.getCurrentUserLarkUserId()` 返回值 | 飞书 user_id(== `employee_id`,企业内身份,**无固定前缀**) | 飞书 |
25
-
26
- **作为 API 参数的取值(不是一个 ID,而是告诉接口"我传入哪种类型的 ID"):**
27
-
28
- | 出现位置 | 实际含义 |
29
- |---|---|
30
- | 飞书 OpenAPI 参数 `user_id_type: 'user_id'`(也可取 `'open_id'` / `'union_id'`) | 字符串字面,与上方的飞书 user_id 字段是一致体系但用途不同 |
31
-
32
- **严禁**:把 `useCurrentUserProfile().user_id`(妙搭 ID)直接当飞书 ID 传给飞书 API。飞书 ID 必须通过 `lark_user_id` 字段或 `AuthNPaasService` 获取。
33
-
34
- `useCurrentUserProfile()` 返回的字段中涉及**两套完全独立的 ID 体系**,严禁混用或互相回退:
35
-
36
- | 字段 | 体系 | 格式 | 说明 |
37
- |------|------|------|------|
38
- | `user_id` | 妙搭 | 纯数字字符串 | 妙搭平台用户 ID |
39
- | `lark_user_id` | 飞书 | 字符串 | 飞书 user_id(== employee_id),通过额外异步请求获取 |
40
-
41
- > **严禁**:`useCurrentUserProfile()` 返回值里**没有** `open_id`、`feishu_id`、`openId` 字段——飞书 ID 在这个 Hook 里**只通过 `lark_user_id`** 暴露。如果在本 Hook 的消费代码里看到 `userInfo.open_id` 等写法,属于历史错误,必须改成 `lark_user_id`。
42
- >
43
- > (范围限定:上一条只约束 `useCurrentUserProfile()` 的消费代码。**项目其他场景** —— 例如调 spark `id_convert`、调飞书通讯录 API —— 出现 `open_id` / `union_id` 是正常且必要的,不在禁止之列。)
44
- >
45
- > **严禁**:当 `lark_user_id` 为空时回退到 `user_id` 展示。两套 ID 含义完全不同,回退会误导用户。正确做法是条件渲染:有值则展示,无值则不展示或展示"未关联飞书账号"。
46
-
47
- ---
48
-
49
- ## 一、功能决策树
50
-
51
- ```text
52
- 用户需求
53
-
54
- ├─ 接口认证(@NeedLogin / 公开接口 / 401 / 守卫流程)?
55
- │ └─ 跳到 `authn-guide` skill
56
-
57
- ├─ 需要在服务端读取当前用户 ID / 租户 / 角色等?
58
- │ └─ 是 ──→ req.userContext(第二节)
59
-
60
- ├─ 需要在前端展示当前用户信息(名称/头像/邮箱)?
61
- │ └─ 是 ──→ useCurrentUserProfile()(第三节)
62
-
63
- ├─ 需要获取当前用户的飞书 user_id(即 employee_id,企业内身份)?
64
- │ ├─ 后端 ──→ AuthNPaasService.getCurrentUserLarkUserId()(第三节)
65
- │ └─ 前端 ──→ useCurrentUserProfile().lark_user_id(第三节)
66
-
67
- ├─ 需要批量把 妙搭 userId 转成 飞书 **user_id (employee_id,无前缀)** —— 不是 open_id (`ou_`)、不是 union_id (`on_`)?
68
- │ └─ 是 ──→ AuthNPaasService.getBatchLarkUserIds()(第三节)
69
-
70
- ├─ 需要 飞书 open_id(`ou_` 开头)/ union_id(`on_` 开头)?
71
- │ └─ ⚠️ **AuthNPaasService 不产出 open_id/union_id**,只能 user_id
72
- │ └─ 跳到 `feishu` skill 的 `references/id-convert.md`(spark id_convert type 10/11)
73
-
74
- ├─ 需要把 飞书 open_id / union_id 反查成 妙搭 userId?
75
- │ └─ 跳到 `feishu` skill 的 `references/id-convert.md`(spark id_convert type 20/21)
76
-
77
- ├─ 需要把 飞书 user_id(employee_id)反查成 妙搭 userId?
78
- │ └─ 无单步方案 ──→ `feishu` skill `references/id-convert.md` "反向两步走"
79
-
80
- └─ 需要自定义飞书 ID 转换接口?
81
- └─ 是 ──→ 注入 AuthNPaasService 编写 Controller(第四节,仅适用于 user_id)
82
- ```
83
-
84
- > **相关技能**:接口认证/守卫/401 参见 `authn-guide`;登录/登出/获取用户信息等 Dataloom SDK 操作参见 `client-builtins-user-service`;**"用户能做什么"**(角色/权限点位鉴权)参见 `authz-guide`。本 skill 只解决**"用户是谁"**。
85
-
86
- ---
87
-
88
- ## 二、用户上下文(req.userContext)
89
-
90
- `UserContextMiddleware` 解析 Gateway 注入的 `x-larkgw-suda-webuser` 头,把当前用户上下文挂到 `req.userContext`,所有 Controller 均可通过 `@Req() req: Request` 读取。
91
-
92
- > 完整的解析→守卫流程见 `authn-guide` skill 第二节"认证流程"。
93
-
94
- ### 字段表
95
-
96
- | 字段 | 类型 | 说明 |
97
- |------|------|------|
98
- | `userId` | `string` | 妙搭用户 ID |
99
- | `tenantId` | `number` | 租户 ID |
100
- | `appId` | `string` | 应用 ID |
101
- | `loginUrl` | `string` | 登录跳转 URL |
102
- | `userType` | `string` | 用户类型(如 `_employee`) |
103
- | `env` | `string` | 环境(如 `preview`、`online`) |
104
- | `userName` | `string` | 用户名 |
105
- | `userNameI18n` | `{ zh_cn, en_us, ja_jp }` | 多语言用户名 |
106
- | `isSystemAccount` | `boolean` | 是否系统账号 |
107
- | `roles` | `string[] \| null` | 用户角色列表。**未开启权限服务或用户无角色时为 `null`** |
108
- | `baseUrl` | `string` | 网关内部地址 |
109
-
110
- ### 服务端读取角色示例
111
-
112
- `roles` 字段记录当前用户在应用中的角色列表,可在 Controller 业务逻辑中消费(如根据角色返回不同数据):
113
-
114
- ```typescript
115
- import { Controller, Get, Req } from '@nestjs/common';
116
- import { Request } from 'express'; // ⚠️ Request 类型必须来自 'express',不要 import 自 'http' / 'undici' 或漏掉 import
117
- import { TaskService } from './task.service'; // 按项目实际路径 import 业务 Service
118
-
119
- // req.userContext 的类型由 @lark-apaas/fullstack-nestjs-core 通过 declare module 'express' 自动注入到 Request,无需手动声明
120
- @Controller('api/tasks')
121
- export class TasksController {
122
- constructor(private readonly taskService: TaskService) {}
123
-
124
- @Get()
125
- async listTasks(@Req() req: Request) {
126
- const roles: string[] | null = req.userContext?.roles ?? null;
127
- // roles 示例:['text_editor', 'visitor', 'admin']
128
- // ⚠️ 未开启权限服务或用户无角色时为 null
129
- if (roles?.includes('admin')) {
130
- return this.taskService.findAll();
131
- }
132
- return this.taskService.findByUser(req.userContext?.userId);
133
- }
134
- }
135
- ```
136
-
137
- > **注意**:`roles` 在未开启权限服务或用户无角色时为 `null`,使用前必须做空值处理。
138
-
139
- ---
140
-
141
- ## 三、飞书 ID 转换(FeishuID Converter)
142
-
143
- ### 核心概念
144
-
145
- 妙搭平台的用户 ID(`userId`)与飞书用户 ID 是两套独立体系。调用飞书 OpenAPI 时需要传入某种飞书侧 ID(`open_id` / `union_id` / `user_id` 任选其一,具体通过哪个参数指定取决于 API:消息 API 用 `receive_id_type`,多数其他 API 用 `user_id_type`,文档协作者用 `member_id_type`)。
146
-
147
- `AuthNPaasService` 暴露的飞书 ID 是 **`user_id`**(即 `employee_id`,飞书企业内的用户标识)这一种,且**只支持 妙搭 userId → 飞书 user_id 单向转换**。
148
-
149
- > 如果需要 `open_id` / `union_id`,或者需要"飞书 ID → 妙搭 userId"反向,请改用飞书开放平台 `spark id_convert` 接口,参见 `feishu` skill 的 `references/id-convert.md`。
150
-
151
- ### 后端 API
152
-
153
- ```typescript
154
- import { Injectable } from '@nestjs/common';
155
- import { AuthNPaasService } from '@lark-apaas/fullstack-nestjs-core';
156
-
157
- @Injectable()
158
- export class MyService {
159
- constructor(private readonly authnService: AuthNPaasService) {}
160
-
161
- async example() {
162
- // 获取当前登录用户的飞书 user_id
163
- const larkUserId = await this.authnService.getCurrentUserLarkUserId();
164
- // => '<飞书 user_id>' | null
165
-
166
- // 批量转换(最多 100 个)
167
- const larkUserIds = await this.authnService.getBatchLarkUserIds(['uid1', 'uid2']);
168
- // => ['<飞书 user_id>', null] 顺序与输入对应,失败项为 null
169
- }
170
- }
171
- ```
172
-
173
- | 方法 | 签名 | 说明 |
174
- |------|------|------|
175
- | `getCurrentUserLarkUserId` | `() → Promise<string \| null>` | 从请求上下文获取当前用户的飞书 ID |
176
- | `getBatchLarkUserIds` | `(userIds: string[]) → Promise<(string \| null)[]>` | 批量转换,最多 100 个,与输入顺序一一对应 |
177
-
178
- ### 内置接口
179
-
180
- 模块自动注册了 `GET /api/authnpaas/lark-user-id`,返回当前登录用户的飞书 ID:
181
-
182
- ```json
183
- { "lark_user_id": "<飞书 user_id>" }
184
- ```
185
-
186
- ### 前端获取
187
-
188
- `useCurrentUserProfile()` Hook 已自动调用上述内置接口,返回值中包含 `lark_user_id` 字段:
189
-
190
- > 本节示例只涉及 ID 相关字段(`user_id` / `lark_user_id`)和顺手的 `name`。完整返回值(`name` / `avatar` / `email` / `tenantId` 等)见 `client-builtins-user-service` skill。
191
-
192
- ```typescript
193
- import { useCurrentUserProfile } from '@lark-apaas/client-toolkit/hooks/useCurrentUserProfile';
194
-
195
- const UserInfoPanel = () => {
196
- const userInfo = useCurrentUserProfile();
197
-
198
- if (!userInfo?.user_id) return <div>加载中...</div>;
199
-
200
- return (
201
- <div>
202
- <p>用户名: {userInfo.name}</p>
203
- <p>用户 ID: {userInfo.user_id}</p>
204
- {userInfo.lark_user_id && <p>飞书用户 ID: {userInfo.lark_user_id}</p>}
205
- </div>
206
- );
207
- };
208
- ```
209
-
210
- **注意**:
211
- - `lark_user_id` 通过额外异步请求获取,可能晚于 `user_id` 等基础字段就绪
212
- - 请求失败或用户无对应飞书账号时值为 `undefined`,**必须用条件渲染**
213
- - `useCurrentUserProfile()` 的返回值里**不存在** `open_id`、`feishu_id`、`openId` 字段——在这个 Hook 的消费代码里飞书 ID 唯一字段名是 `lark_user_id`(仅约束本 Hook;项目其他场景调 spark `id_convert` / 通讯录 API 出现 `open_id` 是正常的)
214
-
215
- ---
216
-
217
- ## 四、自定义飞书 ID 转换接口
218
-
219
- 当内置接口不满足需求(如需要批量转换),可注入 `AuthNPaasService` 编写自定义 Controller:
220
-
221
- ```typescript
222
- import { Controller, Get, Post, Body, HttpCode } from '@nestjs/common';
223
- import { AuthNPaasService } from '@lark-apaas/fullstack-nestjs-core';
224
-
225
- // 生产环境建议加 class-validator 装饰器(如 @IsArray() @IsString({ each: true })
226
- // @ArrayMaxSize(100))并启用全局 ValidationPipe;本示例聚焦 AuthNPaas 用法,省略校验
227
- class BatchConvertDto {
228
- userIds!: string[];
229
- }
230
-
231
- @Controller('api/feishu-id')
232
- export class FeishuIdController {
233
- constructor(private readonly authnService: AuthNPaasService) {}
234
-
235
- @Get('current')
236
- async getCurrent() {
237
- const larkUserId = await this.authnService.getCurrentUserLarkUserId();
238
- return { larkUserId };
239
- }
240
-
241
- @Post('batch')
242
- @HttpCode(200)
243
- async batchConvert(@Body() dto: BatchConvertDto) {
244
- const larkUserIds = await this.authnService.getBatchLarkUserIds(dto.userIds);
245
- return { larkUserIds };
246
- }
247
- }
248
- ```
249
-
250
- 前端调用示例:
251
-
252
- ```typescript
253
- // 获取当前用户飞书 ID
254
- const { larkUserId } = await request<{ larkUserId: string | null }>({
255
- url: '/api/feishu-id/current',
256
- method: 'GET',
257
- });
258
-
259
- // 批量转换
260
- const { larkUserIds } = await request<{ larkUserIds: (string | null)[] }>({
261
- url: '/api/feishu-id/batch',
262
- method: 'POST',
263
- data: { userIds: ['uid1', 'uid2', 'uid3'] },
264
- });
265
- ```
266
-
267
- ---
268
-
269
- ## 五、禁止行为清单
270
-
271
- | 禁止行为 | 正确做法 |
272
- |---------|---------|
273
- | 在 `useCurrentUserProfile()` 消费代码里使用 `open_id`、`feishu_id`、`openId` 等字段名 | `useCurrentUserProfile()` 中飞书 ID 的**唯一字段名**是 `lark_user_id`(限本 Hook;调 spark `id_convert` / 通讯录 API 出现 `open_id` 正常) |
274
- | `lark_user_id` 为空时回退到 `user_id` 展示 | 两套 ID 体系完全不同,严禁互相回退。无值时不展示或展示"未关联飞书账号" |
275
- | 前端直接展示 `lark_user_id` 而不处理空值 | `lark_user_id` 可能为 `undefined`(加载中/获取失败/无飞书账号),必须用条件渲染 |
276
- | 用 `if (!userInfo)` 判断加载状态 | 初始值为空对象(truthy),必须用 `if (!userInfo?.user_id)` |
277
- | 手动实例化 `AuthNPaasService` | 通过 NestJS 依赖注入获取:`constructor(private readonly authnService: AuthNPaasService)` |
278
- | 单独注册 `AuthNPaasModule.forRoot()` | 已通过 `PlatformModule.forRoot()` 自动注册,无需手动导入 |
279
- | 自行调用平台 `/v1/app/{appId}/account/user/convert` 接口 | 必须使用 `AuthNPaasService` 的方法,内置错误处理和可观测性 |
280
- | `getBatchLarkUserIds` 传入超过 100 个 ID | 分批调用,每批最多 100 个 |
281
- | 在前端自行封装飞书 ID 转换请求 | 使用 `useCurrentUserProfile()` 获取当前用户飞书 ID;批量转换通过后端接口 |
282
-
283
- ---
284
-
285
- ## 六、常见问题
286
-
287
- > 接口 401 / 登录跳转相关问题见 `authn-guide` skill 第四节"常见问题"。
288
-
289
- ### 飞书 ID 返回 null
290
-
291
- 1. 确认用户已登录(`req.userContext.userId` 存在)
292
- 2. 确认 `appId` 在请求上下文中存在
293
- 3. 检查平台转换接口是否正常(查看后端日志中 `[batchConvertUserIds]` 相关输出)
294
- 4. 部分用户可能无对应飞书账号,此时转换结果为 null 是预期行为
295
-
296
- ### 前端 lark_user_id 一直为 undefined
297
-
298
- 1. 确认后端 `AuthNPaasModule` 已注册(内置接口 `/api/authnpaas/lark-user-id` 可访问)
299
- 2. 检查浏览器开发者工具中该接口的请求和响应
300
- 3. `lark_user_id` 异步加载,确保组件正确处理了加载态