@lark-apaas/coding-steering 0.1.21 → 0.1.22-alpha.20260724081248

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 (22) hide show
  1. package/README.md +21 -19
  2. package/package.json +2 -2
  3. package/steering/design-html/skills/animated-video/SKILL.md +2 -4
  4. package/steering/design-html/skills/charts/SKILL.md +3 -5
  5. package/steering/design-html/skills/data-report/SKILL.md +4 -6
  6. package/steering/design-html/skills/frontend-design/SKILL.md +34 -36
  7. package/steering/design-html/skills/hi-fi-design/SKILL.md +2 -4
  8. package/steering/design-html/skills/interactive-prototype/SKILL.md +2 -4
  9. package/steering/design-html/skills/make-a-deck/SKILL.md +94 -112
  10. package/steering/design-html/skills/visual-exposure/SKILL.md +4 -6
  11. package/steering/design-html/skills/wireframe/SKILL.md +5 -7
  12. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +375 -61
  13. package/steering/nestjs-react-fullstack/{skills_common/trigger-guide/references/trigger-lifecycle.md → skills/trigger-guide/SKILL.md} +162 -11
  14. package/steering/nestjs-react-fullstack/{skills_common → skills}/user-identity/SKILL.md +5 -12
  15. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +0 -180
  16. package/steering/nestjs-react-fullstack/skills_local/authz-guide/SKILL.md +0 -196
  17. package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/dynamic-permission-guide.md +0 -643
  18. package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/management-page-spec.md +0 -505
  19. package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/runtime-role-controller-spec.md +0 -203
  20. package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/sdk-examples.md +0 -92
  21. package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/sdk-types.md +0 -229
  22. package/steering/nestjs-react-fullstack/skills_local/client-builtins-user-service/SKILL.md +0 -240
@@ -1,8 +1,36 @@
1
- # 触发器入参类型与代码示例
1
+ ---
2
+ name: trigger-guide
3
+ description: 自动化任务触发器配置与代码开发指南,支持 cron 定时触发器、record_change 数据变更触发器和 webhook 触发器,包含 @Automation/@BindTrigger 装饰器用法和 Crontab 表达式规范。Use when 需要:(1) 创建或配置自动化任务/定时任务,(2) 编写 automation 代码绑定触发器,或其他自动化任务相关开发
4
+ steering: true
5
+ steering-topic: trigger_guide
6
+ match-template-name: nestjs-react-fullstack
7
+ ---
2
8
 
3
- reference 承载 nestjs-react-fullstack 触发器 handler 的入参类型定义、完整代码示例与常见实现场景。先读主 [trigger-guide](../SKILL.md) 了解目录结构、绑定约束与配置要求。
9
+ ## 自动化任务配置与代码编写指引
4
10
 
5
- ## 触发器类型与入参
11
+ ### 自动化任务配置
12
+
13
+ 1. 新建自动化任务触发器时无需 enable(激活),将任务创建好然后开发完代码即可。触发器随后交由用户主动操作、要求开始。
14
+
15
+ ### 目录结构
16
+
17
+ ```text
18
+ server
19
+ └── modules
20
+ └── xxx
21
+ ├── xxx.automation.ts
22
+ ├── xxx.module.ts // 必须在 module 中注册自动化任务类,并且在 app.module.ts 中引用并注册该 module,否则代码将不会生效。
23
+ └── 其他文件(如有的话)
24
+ ```
25
+
26
+ 文件命名规则:{模块名}.automation.ts
27
+
28
+ 注意:
29
+
30
+ 1. 每个模块只应该有一个存放自动化任务逻辑的文件,业务逻辑需要聚合到该文件中。
31
+ 2. 如果该模块只有对应的自动化任务,无需编写 Controller
32
+
33
+ ### 触发器类型与入参
6
34
 
7
35
  触发器类型(`triggerType`)有三种:
8
36
 
@@ -57,11 +85,12 @@ interface WebhookEvent {
57
85
  }
58
86
  ```
59
87
 
60
- `DataChangeEventInput.type` 只定义 `INSERT`、`UPDATE`、`DELETE`,不包含 `UPSERT`。
88
+ ### 指定值限制
89
+ 1. Webhook 触发器不可以设置指定值,并且告知用户。
61
90
 
62
- ## 代码示例
91
+ ### 代码示例
63
92
 
64
- 根据触发器创建后确定的任务名字(应用内唯一),编写并绑定到对应的方法上。使用模板已有的 `@lark-apaas/fullstack-nestjs-core` 聚合入口导入 `Automation` / `BindTrigger`,不要求项目再感知底层 trigger 包。具体代码示例如下:
93
+ 你需要根据 `automation_trigger_manager` 工具返回的自动化任务名字,编写并绑定到对应的方法上。具体代码示例如下:
65
94
 
66
95
  ```typescript
67
96
  // 文件名:demo.automation.ts
@@ -155,11 +184,23 @@ export class DemoAutomationTasksService {
155
184
  }
156
185
  ```
157
186
 
158
- ## 技术实现路径参考
187
+ ### 任务代码实现约束
188
+
189
+ 1. 执行自动化任务时无法获取用户信息。依赖用户信息的场景,实现路径如下:
190
+ - 需要查询数据库中的特定数据,给用户发消息:数据库中需要存储用户 id,使用从数据库中查询到的用户 id 进行后续操作
191
+ - 需要调用飞书能力给用户发消息:飞书能力不应该接受用户信息作为参数,而是应该在飞书能力配置里要求用户自己预先指定
192
+
193
+ 2. 入参解析规范(仅 record_change 和 webhook 触发器):
194
+ - 有入参的触发器方法签名为 `async methodName(event: TaskHandlerArgs)`,`cron` 触发器无入参
195
+ - `content.input` 是 JSON 字符串,先用 `typeof input === 'string'` 检查类型,再用 `JSON.parse()` 解析,需添加 try-catch 错误处理
196
+ - `record_change`:根据操作类型获取数据:INSERT/UPDATE 使用 `after` 字段,DELETE 使用 `before` 字段
197
+ - `webhook`:从 `method`、`path`、`query`、`headers`、`body` 中按需取用;`body` 本身也是 JSON 字符串,需要时再次 `JSON.parse()` 解析;`query` 和 `headers` 的值均为 `string[]`
198
+
199
+ ### 技术实现路径参考
159
200
 
160
201
  以下是一些常见需求的推荐实现路径,帮助你在平台能力限制下找到合理的技术方案。
161
202
 
162
- ### 场景一:用户需要管理页面控制定时任务的启停
203
+ #### 场景一:用户需要管理页面控制定时任务的启停
163
204
 
164
205
  平台侧不支持通过 API 动态启停触发器。推荐方案:**平台定时触发器始终保持开启,在任务执行时查询数据库中的开关状态,决定是否真正执行业务逻辑。**
165
206
 
@@ -192,7 +233,7 @@ export class ReportAutomationService {
192
233
  }
193
234
  ```
194
235
 
195
- ### 场景二:定时任务需要将结果通知给特定用户
236
+ #### 场景二:定时任务需要将结果通知给特定用户
196
237
 
197
238
  自动化任务执行时无法获取当前用户上下文。推荐方案:**在数据库中预存需要通知的用户 ID,任务执行时从数据库查询目标用户,再调用飞书插件发送通知。**
198
239
 
@@ -226,7 +267,7 @@ export class NotifyAutomationService {
226
267
  }
227
268
  ```
228
269
 
229
- ### 场景三:记录变更触发器需要做防抖/去重
270
+ #### 场景三:记录变更触发器需要做防抖/去重
230
271
 
231
272
  高频数据变更场景下,同一条记录可能短时间内触发多次。推荐方案:**利用数据库记录最近一次处理时间戳,对比 event 时间戳进行去重。**
232
273
 
@@ -253,7 +294,7 @@ async handleOrderChange(event: TaskHandlerArgs) {
253
294
  }
254
295
  ```
255
296
 
256
- ### 场景四:用户需要自定义定时任务的触发时间
297
+ #### 场景四:用户需要自定义定时任务的触发时间
257
298
 
258
299
  平台侧的 cron 表达式在触发器创建后无法由用户动态修改。推荐方案:**平台设置一个固定的高频定时器(如每 30 分钟执行一次),在任务执行时从数据库读取用户配置的触发时间,判断当前是否命中再决定是否执行。**
259
300
 
@@ -299,3 +340,113 @@ export class ScheduleAutomationService {
299
340
  ```
300
341
 
301
342
  > 注意:由于平台最小调度间隔为 30 分钟,用户可配置的时间精度也应限制为 30 分钟的整数倍(如 `09:00`、`09:30`),前端做好校验提示。
343
+
344
+ ## Crontab 表达式规范
345
+
346
+ ### 基本结构
347
+
348
+ Crontab 表达式由 5 个字段组成:`<minute> <hour> <day> <month> <week>`
349
+
350
+ ### 字段说明
351
+
352
+ 1. **minute(分钟)**:0-59 的整数
353
+ 2. **hour(小时)**:0-23 的整数
354
+ 3. **day(日期)**:1-31 的整数,或大写字母 `L` 表示月份的最后一天
355
+ 4. **month(月份)**:1-12 的整数
356
+ 5. **week(星期)**:0-6 的整数,其中 0 表示星期天
357
+
358
+ ### 特殊字符
359
+
360
+ - **星号 `*`**:表示所有可能的值(每)
361
+ - 例:`* * * * *` 表示每分钟
362
+ - **逗号 `,`**:表示列表范围
363
+ - 例:`1,2,3 * * * *` 表示每小时的第 1、2、3 分钟
364
+ - **中杠 `-`**:表示数值范围
365
+ - 例:`1-10 * * * *` 表示每小时的第 1 到 10 分钟
366
+ - **正斜线 `/`**:表示间隔频率
367
+ - 例:`0 10-18/2 * * *` 表示每天 10 点到 18 点,每隔 2 小时执行
368
+
369
+ ## 输出要求
370
+
371
+ 1. 必须以 JSON 格式输出
372
+ 2. JSON 包含两个字段:
373
+ - `expression`:Crontab 表达式字符串
374
+ - `explanation`:中文说明,简要描述执行时间
375
+ 3. 如果用户描述不清晰,请询问具体细节
376
+
377
+ ## 示例
378
+
379
+ **用户输入**:每天早上 8 点执行
380
+
381
+ **输出**:
382
+
383
+ ```json
384
+ {
385
+ "expression": "0 8 * * *",
386
+ "explanation": "每天早上 8:00 执行"
387
+ }
388
+ ```
389
+
390
+ **用户输入**:每周一到周五的上午 9 点和下午 6 点执行
391
+
392
+ **输出**:
393
+
394
+ ```json
395
+ {
396
+ "expression": "0 9,18 * * 1-5",
397
+ "explanation": "每周一至周五的 9:00 和 18:00 执行"
398
+ }
399
+ ```
400
+
401
+ **用户输入**:每隔 30 分钟执行一次
402
+
403
+ **输出**:
404
+
405
+ ```json
406
+ {
407
+ "expression": "*/30 * * * *",
408
+ "explanation": "每隔 30 分钟执行一次"
409
+ }
410
+ ```
411
+
412
+ **用户输入**:每月最后一天的晚上 11 点执行
413
+
414
+ **输出**:
415
+
416
+ ```json
417
+ {
418
+ "expression": "0 23 L * *",
419
+ "explanation": "每月最后一天的 23:00 执行"
420
+ }
421
+ ```
422
+
423
+ **用户输入**:每个工作日的每小时第 15 和 45 分钟执行
424
+
425
+ **输出**:
426
+
427
+ ```json
428
+ {
429
+ "expression": "15,45 * * * 1-5",
430
+ "explanation": "每周一至周五,每小时的第 15 和 45 分钟执行"
431
+ }
432
+ ```
433
+
434
+ **用户输入**:每天上午 10 点到下午 6 点,每隔 2 小时执行
435
+
436
+ **输出**:
437
+
438
+ ```json
439
+ {
440
+ "expression": "0 10-18/2 * * *",
441
+ "explanation": "每天 10:00、12:00、14:00、16:00、18:00 执行"
442
+ }
443
+ ```
444
+
445
+ ## 注意事项
446
+
447
+ - 星期字段:0 和 7 都可以表示星期天(但本规范使用 0)
448
+ - 时间采用 24 小时制
449
+ - 月份和星期都从较小的数字开始计数
450
+ - 确保生成的表达式符合实际日历逻辑
451
+ - 由于技术限制,最小间隔为 30 分钟,如用户要求有误请直接拒绝用户并给出原因
452
+ - 输出必须是有效的 JSON 格式
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: user-identity
3
- description: "Use when getting current user info/profile, displaying user name/avatar/email, converting miaoda userId lark_user_id (both directions via AuthNPaasService), or reading req.userContext fields (userId/roles/tenantId): useCurrentUserProfile, AuthNPaasService, FeishuID conversion. 触发词:用户身份, 用户信息, 用户资料, 当前用户, userProfile, useCurrentUserProfile, 飞书ID, FeishuID, 飞书用户ID, lark_user_id, 用户ID转换, AuthNPaasService, getBatchMiaodaUserIds, 飞书ID转妙搭, employee_id 转 userId, 用户上下文, userContext, userContext.roles, 用户角色, 当前用户角色, 获取请求者角色, 展示用户, 显示用户, 用户面板, 我是谁, 获取用户"
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
4
  steering: true
5
5
  steering-topic: user_identity
6
6
  match-template-name: nestjs-react-fullstack
@@ -77,7 +77,7 @@ match-template-name: nestjs-react-fullstack
77
77
  │ └─ 跳到 `feishu` skill 的 `references/id-convert.md`(spark id_convert type 20/21)
78
78
 
79
79
  ├─ 需要把 飞书 user_id(employee_id)反查成 妙搭 userId?
80
- │ └─ 后端 ──→ `AuthNPaasService.getBatchMiaodaUserIds()`(第三节,SDK 一步,convertType 31);非模板项目 ──→ `feishu` skill 两步兜底
80
+ │ └─ 无单步方案 ──→ `feishu` skill `references/id-convert.md` "反向两步走"
81
81
 
82
82
  └─ 需要自定义飞书 ID 转换接口?
83
83
  └─ 是 ──→ 注入 AuthNPaasService 编写 Controller(第四节,仅适用于 user_id)
@@ -102,7 +102,7 @@ match-template-name: nestjs-react-fullstack
102
102
  | `appId` | `string` | 应用 ID |
103
103
  | `loginUrl` | `string` | 登录跳转 URL |
104
104
  | `userType` | `string` | 用户类型(如 `_employee`) |
105
- | `env` | `string` | 环境(`preview` 预览态、`runtime` 发布运行态) |
105
+ | `env` | `string` | 环境(如 `preview`、`online`) |
106
106
  | `userName` | `string` | 用户名 |
107
107
  | `userNameI18n` | `{ zh_cn, en_us, ja_jp }` | 多语言用户名 |
108
108
  | `isSystemAccount` | `boolean` | 是否系统账号 |
@@ -146,9 +146,9 @@ export class TasksController {
146
146
 
147
147
  妙搭平台的用户 ID(`userId`)与飞书用户 ID 是两套独立体系。调用飞书 OpenAPI 时需要传入某种飞书侧 ID(`open_id` / `union_id` / `user_id` 任选其一,具体通过哪个参数指定取决于 API:消息 API 用 `receive_id_type`,多数其他 API 用 `user_id_type`,文档协作者用 `member_id_type`)。
148
148
 
149
- `AuthNPaasService` 暴露的飞书 ID 是 **`user_id`**(即 `employee_id`,飞书企业内的用户标识)这一种,支持 **妙搭 userId 飞书 user_id 双向**转换(正向 `getBatchLarkUserIds`/`getCurrentUserLarkUserId`,反向 `getBatchMiaodaUserIds`)。
149
+ `AuthNPaasService` 暴露的飞书 ID 是 **`user_id`**(即 `employee_id`,飞书企业内的用户标识)这一种,且**只支持 妙搭 userId 飞书 user_id 单向转换**。
150
150
 
151
- > 如果需要 `open_id` / `union_id`(无论正反向),请改用飞书开放平台 `spark id_convert` 接口,参见 `feishu` skill 的 `references/id-convert.md`。`employee_id` ↔ 妙搭 userId 双向都在本 SDK 内(见下方方法表)。
151
+ > 如果需要 `open_id` / `union_id`,或者需要"飞书 ID → 妙搭 userId"反向,请改用飞书开放平台 `spark id_convert` 接口,参见 `feishu` skill 的 `references/id-convert.md`。
152
152
 
153
153
  ### 后端 API
154
154
 
@@ -168,10 +168,6 @@ export class MyService {
168
168
  // 批量转换(最多 100 个)
169
169
  const larkUserIds = await this.authnService.getBatchLarkUserIds(['uid1', 'uid2']);
170
170
  // => ['<飞书 user_id>', null] 顺序与输入对应,失败项为 null
171
-
172
- // 反向:飞书 user_id(employee_id) → 妙搭 userId
173
- const miaodaUserIds = await this.authnService.getBatchMiaodaUserIds(['emp1', 'emp2']);
174
- // => ['<妙搭 userId>', null] 顺序与输入对应,失败项为 null
175
171
  }
176
172
  }
177
173
  ```
@@ -180,7 +176,6 @@ export class MyService {
180
176
  |------|------|------|
181
177
  | `getCurrentUserLarkUserId` | `() → Promise<string \| null>` | 从请求上下文获取当前用户的飞书 ID |
182
178
  | `getBatchLarkUserIds` | `(userIds: string[]) → Promise<(string \| null)[]>` | 批量转换,最多 100 个,与输入顺序一一对应 |
183
- | `getBatchMiaodaUserIds` | `(employeeIds: string[]) → Promise<(string \| null)[]>` | 反向:批量把飞书 user_id(employee_id)转为妙搭 userId,最多 100 个,顺序一一对应,失败项 `null`(底层 convertType 31) |
184
179
 
185
180
  ### 内置接口
186
181
 
@@ -254,8 +249,6 @@ export class FeishuIdController {
254
249
  }
255
250
  ```
256
251
 
257
- > 反向转换(employee_id → 妙搭 userId)同理,把 `getBatchLarkUserIds` 换成 `getBatchMiaodaUserIds` 即可,无需新增 Controller。
258
-
259
252
  前端调用示例:
260
253
 
261
254
  ```typescript
@@ -1,180 +0,0 @@
1
- ---
2
- name: trigger-guide
3
- description: 自动化任务触发器代码开发指南,支持 cron 定时触发器、record_change 数据变更触发器和 webhook 触发器,包含 @Automation/@BindTrigger 装饰器用法、handler 入参解析和 Crontab 表达式规范。Use when 需要:(1) 为已创建的自动化任务/定时任务编写业务 handler,(2) 编写 automation 代码绑定触发器,或其他自动化任务相关开发
4
- steering: true
5
- steering-topic: trigger_guide
6
- match-template-name: nestjs-react-fullstack
7
- ---
8
-
9
- ## 自动化任务配置与代码编写指引
10
-
11
- ### 自动化任务配置
12
-
13
- 1. 新建自动化任务触发器时无需 enable(激活),将任务创建好然后开发完代码即可。触发器随后交由用户主动操作、要求开始。
14
-
15
- ### 目录结构
16
-
17
- ```text
18
- server
19
- └── modules
20
- └── xxx
21
- ├── xxx.automation.ts
22
- ├── xxx.module.ts // 必须在 module 中注册自动化任务类,并且在 app.module.ts 中引用并注册该 module,否则代码将不会生效。
23
- └── 其他文件(如有的话)
24
- ```
25
-
26
- 文件命名规则:{模块名}.automation.ts
27
-
28
- 注意:
29
-
30
- 1. 每个模块只应该有一个存放自动化任务逻辑的文件,业务逻辑需要聚合到该文件中。
31
- 2. 如果该模块只有对应的自动化任务,无需编写 Controller
32
-
33
- ### 触发器类型
34
-
35
- 触发器类型(`triggerType`)有三种:
36
-
37
- - `record_change`:记录变更触发器,**有入参**
38
- - `cron`:定时触发器,**无入参**
39
- - `webhook`:Webhook 触发器,**有入参**
40
-
41
- 各触发器 handler 的入参类型定义(`TaskHandlerArgs`、`DataChangeEventInput`、`WebhookEvent`)见 [触发器入参类型与代码示例](references/trigger-lifecycle.md)。
42
-
43
- ### 指定值限制
44
-
45
- 1. Webhook 触发器不可以设置指定值,并且告知用户。
46
-
47
- ### 代码绑定
48
-
49
- 你需要根据触发器创建后确定的自动化任务名字(应用内唯一),编写并绑定到对应的方法上:`@BindTrigger('<任务名字>')` 中的名字必须与创建触发器时确定的名字逐字相同,不能用 trigger ID 或方法名代替。`@Automation()` 标记的类需注册为对应 `<module>.module.ts` 的 provider,且该 module 必须被 `server/app.module.ts` 直接或传递 import,否则装饰器不会生效。完整代码示例见 [触发器入参类型与代码示例](references/trigger-lifecycle.md)。
50
-
51
- ### 任务代码实现约束
52
-
53
- 1. 执行自动化任务时无法获取用户信息。依赖用户信息的场景,实现路径如下:
54
- - 需要查询数据库中的特定数据,给用户发消息:数据库中需要存储用户 id,使用从数据库中查询到的用户 id 进行后续操作
55
- - 需要调用飞书能力给用户发消息:飞书能力不应该接受用户信息作为参数,而是应该在飞书能力配置里要求用户自己预先指定
56
-
57
- 2. 入参解析规范(仅 record_change 和 webhook 触发器):
58
- - 有入参的触发器方法签名为 `async methodName(event: TaskHandlerArgs)`,`cron` 触发器无入参
59
- - `content.input` 是 JSON 字符串,先用 `typeof input === 'string'` 检查类型,再用 `JSON.parse()` 解析,需添加 try-catch 错误处理
60
- - `record_change`:根据操作类型获取数据:INSERT/UPDATE 使用 `after` 字段,DELETE 使用 `before` 字段
61
- - `webhook`:从 `method`、`path`、`query`、`headers`、`body` 中按需取用;`body` 本身也是 JSON 字符串,需要时再次 `JSON.parse()` 解析;`query` 和 `headers` 的值均为 `string[]`
62
-
63
- ### 技术实现路径参考
64
-
65
- 以下常见需求的推荐实现路径,帮助你在平台能力限制下找到合理的技术方案;完整代码见 [触发器入参类型与代码示例](references/trigger-lifecycle.md)。
66
-
67
- - **场景一:管理页面控制定时任务启停** —— 平台侧不支持通过 API 动态启停触发器;定时触发器始终保持开启,在任务执行时查询数据库中的开关状态决定是否执行。
68
- - **场景二:定时任务通知特定用户** —— 任务执行时无法获取用户上下文;在数据库预存目标用户 ID,执行时查询再调用飞书插件发送。
69
- - **场景三:记录变更触发器防抖/去重** —— 利用数据库记录最近一次处理时间戳,对比 event 时间戳进行去重。
70
- - **场景四:自定义定时任务触发时间** —— cron 创建后不可动态改;平台设固定高频定时器(如每 30 分钟),执行时读数据库配置判断是否命中。
71
-
72
- ## Crontab 表达式规范
73
-
74
- ### 基本结构
75
-
76
- Crontab 表达式由 5 个字段组成:`<minute> <hour> <day> <month> <week>`
77
-
78
- ### 字段说明
79
-
80
- 1. **minute(分钟)**:0-59 的整数
81
- 2. **hour(小时)**:0-23 的整数
82
- 3. **day(日期)**:1-31 的整数,或大写字母 `L` 表示月份的最后一天
83
- 4. **month(月份)**:1-12 的整数
84
- 5. **week(星期)**:0-6 的整数,其中 0 表示星期天
85
-
86
- ### 特殊字符
87
-
88
- - **星号 `*`**:表示所有可能的值(每)
89
- - 例:`* * * * *` 表示每分钟
90
- - **逗号 `,`**:表示列表范围
91
- - 例:`1,2,3 * * * *` 表示每小时的第 1、2、3 分钟
92
- - **中杠 `-`**:表示数值范围
93
- - 例:`1-10 * * * *` 表示每小时的第 1 到 10 分钟
94
- - **正斜线 `/`**:表示间隔频率
95
- - 例:`0 10-18/2 * * *` 表示每天 10 点到 18 点,每隔 2 小时执行
96
-
97
- ## 输出要求
98
-
99
- 1. 必须以 JSON 格式输出
100
- 2. JSON 包含两个字段:
101
- - `expression`:Crontab 表达式字符串
102
- - `explanation`:中文说明,简要描述执行时间
103
- 3. 如果用户描述不清晰,请询问具体细节
104
-
105
- ## 示例
106
-
107
- **用户输入**:每天早上 8 点执行
108
-
109
- **输出**:
110
-
111
- ```json
112
- {
113
- "expression": "0 8 * * *",
114
- "explanation": "每天早上 8:00 执行"
115
- }
116
- ```
117
-
118
- **用户输入**:每周一到周五的上午 9 点和下午 6 点执行
119
-
120
- **输出**:
121
-
122
- ```json
123
- {
124
- "expression": "0 9,18 * * 1-5",
125
- "explanation": "每周一至周五的 9:00 和 18:00 执行"
126
- }
127
- ```
128
-
129
- **用户输入**:每隔 30 分钟执行一次
130
-
131
- **输出**:
132
-
133
- ```json
134
- {
135
- "expression": "*/30 * * * *",
136
- "explanation": "每隔 30 分钟执行一次"
137
- }
138
- ```
139
-
140
- **用户输入**:每月最后一天的晚上 11 点执行
141
-
142
- **输出**:
143
-
144
- ```json
145
- {
146
- "expression": "0 23 L * *",
147
- "explanation": "每月最后一天的 23:00 执行"
148
- }
149
- ```
150
-
151
- **用户输入**:每个工作日的每小时第 15 和 45 分钟执行
152
-
153
- **输出**:
154
-
155
- ```json
156
- {
157
- "expression": "15,45 * * * 1-5",
158
- "explanation": "每周一至周五,每小时的第 15 和 45 分钟执行"
159
- }
160
- ```
161
-
162
- **用户输入**:每天上午 10 点到下午 6 点,每隔 2 小时执行
163
-
164
- **输出**:
165
-
166
- ```json
167
- {
168
- "expression": "0 10-18/2 * * *",
169
- "explanation": "每天 10:00、12:00、14:00、16:00、18:00 执行"
170
- }
171
- ```
172
-
173
- ## 注意事项
174
-
175
- - 星期字段:0 和 7 都可以表示星期天(但本规范使用 0)
176
- - 时间采用 24 小时制
177
- - 月份和星期都从较小的数字开始计数
178
- - 确保生成的表达式符合实际日历逻辑
179
- - 由于技术限制,最小间隔为 30 分钟,如用户要求有误请直接拒绝用户并给出原因
180
- - 输出必须是有效的 JSON 格式
@@ -1,196 +0,0 @@
1
- ---
2
- name: authz-guide
3
- description: "Use when writing permission control code with CanRole/@Can/useCan decorator, managing roles/members at runtime via AuthorizationSDK, implementing dynamic permission-point-based auth, designing RBAC role/permission system, debugging 403 errors, or checking role panel entry. 触发词:权限控制, CanRole, @Can, useCan, RBAC, 403, permission, access control, 鉴权代码, 角色面板, 运行时角色, 成员管理, AuthorizationSDK, 权限点位, 动态鉴权, 权限配置, authz_permissions, authz_role_permissions, IPermissionResolver, 设计权限体系, 开启权限服务, 规划角色, 开启角色服务, 设计角色"
4
- steering: true
5
- steering-topic: authz_guide
6
- match-template-name: nestjs-react-fullstack
7
- ---
8
-
9
- # RBAC 权限编码指南
10
-
11
- ## 本地 lark-cli 开发适配
12
-
13
- 本节只规定 `apps init` 后的本地工程识别、平台资源核验和验证流程,**不改变下方既有的权限模式、模板代码或页面规格**。
14
-
15
- 1. 以当前 `apps init` 工程为唯一项目根,先读取 `.spark/meta.json` 获取真实 `app_id`,再读取工程内 `.agents/skills/authz-guide/SKILL.md` 及当前模式引用的 reference。不得用目录名、用户口述的 ID 或工作区外同名 skill 代替。
16
- 2. 应用源码仍按本 skill 使用 `CanRole`、`AuthorizationSDK`、`@Can` / `<Can>` 实现;`lark-cli` 只负责平台角色、成员和应用数据库等外部资源操作,不能替代模板代码。
17
- 3. 代码或 SQL 使用具体角色标识前,先执行 `lark-cli apps +role-list --app-id <app_id> --as user --format json`;对每个已存在的目标 `role_id` 再执行 `lark-cli apps +role-get --app-id <app_id> --role-id <role_id> --as user --format json`。禁止把示例中的 `admin`、`editor` 等名称直接当作真实 `role_id`;新建角色则使用创建响应返回的 `role_id` 并独立回读。
18
- 4. 平台角色查询和变更统一使用 `lark-cli apps +role-*` / `+role-member-*`。真实写操作遵守命令自身的确认与回读要求;诊断和预演不得声称平台状态已经改变。
19
- 5. 收尾按项目 `package.json` 的真实脚本运行 typecheck、测试和 build,并确认新增页面、Controller、Module 已进入实际 router/bootstrap 注册链;只创建未接线文件不算完成。
20
-
21
- 如果请求只操作平台角色或成员、完全不修改应用源码,应停止使用本 skill,改用当前环境中的 `lark-apps` skill;只有应用代码改造才继续执行下方模板。
22
-
23
- ## 零、模式决策与实施
24
-
25
- ### 决策总表
26
-
27
- | 信号 | 先 DESIGN? | 模式 | 实施章节 | CanRole |
28
- |------|------------|------|---------|---------|
29
- | "加权限控制"、"按角色控制可见性" | 否 | 静态角色鉴权 | 第一节 | ✅ 使用 |
30
- | "应用内管理角色成员"、"角色管理页面" | ✅ 是 | 静态 + 运行态角色管理 | 第一节 + 第二节 | ✅ 使用 |
31
- | "开启权限服务"、"设计权限体系"、"规划角色" | ✅ 是 | 按方案决定 | 按方案 | 按方案 |
32
- | "动态配置权限"、"无需改代码调整权限"、"权限点位" | ✅ 是 | 动态权限点位(新建) | 第二节 + 第三节 | ⛔ 禁止 |
33
- | "升级为动态权限"、"CanRole 迁移到 Can" | ✅ 是 | 动态权限点位(升级) | 第三节(升级分支) | ⛔ 禁止 |
34
-
35
- > **⛔ 互斥硬规则**:选择动态权限点位鉴权后,**全部业务鉴权和管理 API 鉴权必须用 `@Can`/`<Can>`**,禁止混用 `CanRole`。需求明确需要动态权限时,禁止先用 `CanRole` 实现再升级为 `@Can`,直接进入第三节,一步到位。
36
-
37
- > **⛔ 点位全覆盖**:编写鉴权代码时,必须对照权限设计方案的**功能权限表**,将每个权限点位逐一落实到对应的后端 API(`@CanRole` / `@Can`)和前端入口(`<CanRole>` / `<Can>`),完成后逐行核对确认无遗漏。
38
-
39
- > **⛔ 角色来源一律是平台,禁止另搞一套平台不认的权限系统**:多级审批、按城市/部门分权、多角色组合等复杂授权,角色必须是**平台真实角色**(先用 `lark-cli apps +role-list` 查询;需要创建时使用 `+role-create`,并让返回的 `role_id` 与代码对齐)。**消费平台角色的方式不限**——`@CanRole`/`@Can` 是语法糖;在 service 里读 `req.userContext.roles` 再判断是否包含已核验的角色标识,同样是走平台机制(roles 来自平台),按场景选用即可,**不强制每个 API 都用 `@CanRole`**(注解 cover 不了的细粒度场景就读 `userContext.roles` 自行判断)。**真正要禁止的是绕开平台另起炉灶**:自建任何平行的角色/权限存储(自建角色表 / 角色字段 / 用户名单,不限具体命名)、引用平台上不存在的角色标识——这些 dev 看似能跑、线上必失效。复杂度用「**平台角色(组合)+ 数据层行级过滤**」表达——例如"审核人只审本城市"=`reviewer` 角色把门 + service 按 `cityBranchId` 过滤;"多级审批"=每级一个平台角色 + 把"当前在第几级"存成业务数据状态。
40
-
41
- ### DESIGN 前置步骤
42
-
43
- "开启权限服务"/"设计权限体系"/"规划角色"/"升级鉴权模式"/"开启角色服务" → **必须先结合现有代码和平台真实角色产出结构化权限设计方案**,用户确认后再实施。日常"给某功能加权限"不触发 DESIGN。
44
-
45
- > **本地命令**:角色查询、创建、更新、成员维护和用户角色匹配分别使用 `lark-cli apps +role-*`、`+role-member-*`、`+role-match-list`;命令参数以当前 `lark-apps` skill 和 `--help` 为准。
46
-
47
- ### 403 统一处理(所有模式通用)
48
-
49
- 403 进入 response 分支还是 reject 分支取决于当前工程 `axiosForBackend` 的 `validateStatus` 和拦截器合同,**禁止断言 catch 永远捕获不到 403**。先读取工程内真实请求封装;在统一 API 层同时按其实际合同识别 403,业务组件只消费归一化后的无权限错误。
50
-
51
- 若当前请求器会 resolve 403,则在前端统一请求层按 response 检查并抛错,**禁止**在业务组件中单独处理:
52
-
53
- ```typescript
54
- const response = await axiosForBackend(config);
55
- if (response.status === 403) throw new Error('无操作权限,请联系管理员分配角色');
56
- ```
57
-
58
- 若请求器会 reject 403,则在同一 API 层从结构化错误中读取 HTTP status 后转换,并保留原始 cause;不要在页面 catch 中用错误字符串猜测。
59
-
60
- 本地只读排查 403 时,读取 `.spark/meta.json` 和真实 handler/policy 后,依次用 `+role-list`、`+role-match-list --user-id <open_id>`、`+role-get --role-id <required_role_id>` 对比“代码要求角色”和“用户实际命中角色”。只有代码绑定链和平台结果都能对上时才下根因结论,不为排查自动创建角色、添加成员或修改可见范围。
61
-
62
- ### 平台的角色面板入口
63
-
64
- **只允许在对话中给其入口链接,严禁写到代码中**:`[角色面板](BaseURL?openPanel=auth)` 或 `[角色面板](?openPanel=auth)`
65
-
66
- ---
67
-
68
- ## 一、静态角色鉴权
69
-
70
- ### 核心原则
71
-
72
- 1. 系统已内置角色权限表,**无需也禁止自建任何平行的角色/权限存储**(自建角色表 / 角色字段 / 用户名单等,不限具体命名)——@CanRole 只读平台角色,应用自建的存储对鉴权无效
73
- 2. 统一使用 `useAuth()` 获取 `{ ability, isLoading }`,**禁止** `useAuthAbility` / `useCanRole`(已移除)
74
- 3. 必须处理 `isLoading`——加载期间 `ability.can()` 返回 false,不检查会误判无权限
75
- 4. 创建角色后必须编写鉴权代码,切忌只创建角色不编写代码
76
- 5. **`@CanRole([X])`/`<CanRole roles={[X]}>` 里的 X 必须等于平台实际角色标识,不能凭语义臆造**——需要稳定标识时用 `lark-cli apps +role-create --app-id <app_id> --name <name> --role-id <X> --as user` 创建并回读,已有角色先用 `+role-list` / `+role-get` 取得真实 `role_id` 再写代码。若代码写 `'admin'`/`'reviewer'` 但平台分配给用户的角色标识是 `role_xxx`,两者对不上 → 线上恒 403(日志可见 `用户角色 [role_xxx], 需要 [admin, reviewer]`)
77
-
78
- ### 前端
79
-
80
- ```typescript
81
- import { CanRole, useAuth, ROLE_SUBJECT } from '@lark-apaas/client-toolkit/auth';
82
-
83
- // 组件级 —— CanRole 内置 isLoading 保护,fallback 仅用于加载态占位(Skeleton/Spinner)
84
- // ⛔ fallback 禁止传入 <Navigate> 等重定向组件
85
- <CanRole roles={['admin', 'super_admin']} fallback={<MenuSkeleton />}>
86
- <NavLink to="/admin">后台管理</NavLink>
87
- </CanRole>
88
-
89
- // 路由级 —— 必须用 ProtectedRoute + useAuth,禁止用 CanRole 做路由守卫
90
- const ProtectedRoute: React.FC<{ children: React.ReactNode; requiredRoles: string[] }> = ({ children, requiredRoles }) => {
91
- const { ability, isLoading } = useAuth();
92
- if (isLoading) return <Loading />;
93
- const hasPermission = requiredRoles.some((role) => ability.can(role, ROLE_SUBJECT));
94
- return hasPermission ? <>{children}</> : <Navigate to="/unauthorized" replace />;
95
- };
96
- ```
97
-
98
- ### 后端
99
-
100
- ```typescript
101
- import { CanRole } from '@lark-apaas/fullstack-nestjs-core';
102
-
103
- @CanRole(['admin']) // 单角色
104
- @CanRole(['admin', 'editor']) // 多角色(OR 逻辑:任一即可)
105
- ```
106
-
107
- ---
108
-
109
- ## 二、运行态角色管理(AuthorizationSDK)
110
-
111
- **前置条件**:应用已开启角色服务。
112
-
113
- ### 核心原则
114
-
115
- 1. 必须通过 `AuthorizationSDK` 操作,禁止自行封装 HTTP 或操作数据库
116
- 2. 通过 NestJS 依赖注入获取实例(`constructor(private readonly authzSDK: AuthorizationSDK)`),禁止手动实例化
117
- 3. 先根据 SDK 出入参签名定义接口规格(DTO / API Path / 请求方式),再写 Controller 和前端代码
118
- 4. 管理页面严格对标规格,禁止自由发挥
119
-
120
- ### 实施指引
121
-
122
- | 内容 | 参考文档 |
123
- |------|---------|
124
- | Controller 注入 + DTO | [runtime-role-controller-spec.md](references/runtime-role-controller-spec.md) |
125
- | Shared 类型定义 | [runtime-role-controller-spec.md § Shared 类型](references/runtime-role-controller-spec.md) |
126
- | 管理页面 UI(从 Step 0 开始,禁止跳步) | [management-page-spec.md](references/management-page-spec.md) |
127
- | SDK 完整类型 | [sdk-types.md](references/sdk-types.md) |
128
- | SDK 调用示例 | [sdk-examples.md](references/sdk-examples.md) |
129
-
130
- **关键约束**:
131
- - 成员按类型分组传递(`MemberMutationData`),不是扁平数组
132
- - `allEmployees`/`public` 只读,包含「企业全员」或「互联网公开」的角色不支持删除
133
- - `userID` 可不传,默认为当前登录用户
134
-
135
- ---
136
-
137
- ## 三、动态权限点位鉴权(`@Can` + `<Can>`)
138
-
139
- **适用场景**:运行时配置「哪个角色拥有哪些权限」,无需改代码调整权限策略。
140
-
141
- | | 静态角色鉴权 | 动态权限点位鉴权 |
142
- |---|---|---|
143
- | 判断依据 | 用户是否属于某角色 | 角色是否拥有某权限点位 |
144
- | 配置方式 | 代码硬编码角色名 | 运行时管理页面配置 |
145
- | 后端 | `@CanRole(['admin'])` | `@Can('create', 'Task')` |
146
- | 前端 | `<CanRole roles={[...]}>` | `<Can action="read" subject="Task">` |
147
- | 数据存储 | 平台角色 API | 平台角色 API + 业务库 `authz_permissions` 和 `authz_role_permissions` 表 |
148
-
149
- **必须严格遵循 [dynamic-permission-guide.md](references/dynamic-permission-guide.md) 实施,从 Step 0 开始逐步执行。** 禁止跳步或自由发挥。
150
-
151
-
152
- ---
153
-
154
- ## 四、禁止行为清单
155
-
156
- | 禁止行为 | 正确做法 |
157
- |----------|----------|
158
- | 动态权限模式下使用 `CanRole`/`@CanRole`/`<CanRole>` | 全部用 `@Can`/`<Can>`,grep 确认零残留 |
159
- | 需要动态权限时先落 CanRole 再升级 | 直接用 `@Can`/`<Can>`,一步到位 |
160
- | 使用 `useAuthAbility` 或 `useCanRole` | 已移除,统一用 `useAuth()` |
161
- | 不检查 `isLoading` 直接判断权限 | 必须先判断 `isLoading`,加载期间显示 Loading |
162
- | `fallback` 中使用 `<Navigate>` 或重定向 | `fallback` 仅用于加载态占位(Skeleton/Spinner) |
163
- | 绕过 AuthorizationSDK 自行封装接口 | 必须通过 SDK 操作运行时角色和权限点位 |
164
- | 升级权限体系时未完成 DESIGN 就编码 | 先基于现有代码和平台角色产出方案,确认后再动手 |
165
- | 在业务组件中单独处理 403 | API 层统一拦截 403 |
166
- | 自建任意平行的角色/权限存储(自建角色表 / 角色字段 / 用户名单,不限具体命名)来做鉴权判断 | @CanRole 只认平台角色,应用自建的存储不会被鉴权读取;角色一律用平台真实角色 |
167
- | 复杂授权就**另起炉灶搞一套平台不认的权限**(自建角色表 / 引用平台没 create 过的标识 / 写死 user_id 名单) | 角色一律用平台真实角色;消费方式不限(`@CanRole` 或读 `req.userContext.roles` 都行),复杂度用「平台角色 + 数据层行级过滤」表达 |
168
- | `@CanRole([X])` 的 X 凭语义臆造、与平台真实角色标识不一致 | 用 `lark-cli apps +role-list` / `+role-get` 获取真实 `role_id`;需要稳定 ID 时创建角色显式传 `--role-id` |
169
- | 未读取请求器合同就断言 403 只会进入 response 或 catch | 按实际 `validateStatus` / 拦截器合同在统一 API 层处理两种交付方式 |
170
-
171
- ---
172
-
173
- ## 五、常见问题
174
-
175
- | 问题 | 处理方式 |
176
- |------|---------|
177
- | 403 错误 | 明确告知是无权限报错;用 `+role-match-list` 查询用户实际命中角色,并用 `+role-get` 核验代码要求的角色 |
178
- | 线上 403 但 dev 正常 / 用户"已授权"仍 403 | 先核对两件事:(1) `+role-match-list` 返回的真实 `role_id` 是否等于代码 `@CanRole` 里的字符串;(2) 是否另搞了一套平台不认的权限(自建角色表 / 引用平台不存在的标识 / user_id 名单)绕开平台。再结合 handler/policy 的真实绑定链判断根因 |
179
- | 开发环境授权不生效 | 用 `+role-match-list` 和 `+role-member-list` 核对平台真实状态,不尝试本地模拟角色 |
180
- | 用户要求管理角色 | 开发态:`[角色面板](BaseURL?openPanel=auth)`;运行态:按第二节实施 |
181
-
182
- ---
183
-
184
- ## 六、自查清单
185
-
186
- - [ ] 创建角色后编写了对应鉴权代码
187
- - [ ] 对照功能权限表,每个点位均已落实到后端 API 和前端入口,无遗漏
188
- - [ ] 前端从 `@lark-apaas/client-toolkit/auth` 导入
189
- - [ ] `useAuth()` 已处理 `isLoading` 状态
190
- - [ ] 前端 API 层统一处理了 403
191
- - [ ] 前后端权限规则一致
192
- - [ ] 开发完成后已用 `+role-match-list` 核对测试用户的真实角色;需要人工授权时给出角色面板入口
193
- - [ ] 动态权限:通过 `PlatformModule.forRoot({ authz })` 注册 resolver,未单独注册 `AuthZPaasModule`
194
- - [ ] 动态权限:grep 确认 `CanRole`/`useCanRole`/`@CanRole`/`<CanRole>` 零残留
195
- - [ ] 运行态:管理页面对照 [management-page-spec.md 检查表](references/management-page-spec.md)
196
- - [ ] 接口端到端测试通过