@lark-apaas/coding-steering 0.1.6-alpha.0 → 0.1.6-alpha.2

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 (25) hide show
  1. package/package.json +1 -1
  2. package/steering/nestjs-react-fullstack/skills/.gitkeep +0 -0
  3. package/steering/nestjs-react-fullstack/tech.md +21 -0
  4. package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +0 -122
  5. package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +0 -139
  6. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +0 -628
  7. package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +0 -246
  8. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +0 -707
  9. package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -270
  10. package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +0 -214
  11. package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +0 -163
  12. package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +0 -309
  13. package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +0 -190
  14. package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +0 -160
  15. package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +0 -256
  16. package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +0 -103
  17. package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +0 -198
  18. package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +0 -128
  19. package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +0 -207
  20. package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +0 -164
  21. package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +0 -90
  22. package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +0 -164
  23. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +0 -267
  24. package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +0 -452
  25. package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +0 -300
@@ -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: nestjs-react-fullstack
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` 异步加载,确保组件正确处理了加载态