@lark-apaas/coding-steering 0.1.59 → 0.1.60
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.
- package/package.json +1 -1
- package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +38 -5
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/runtime-role-controller-spec.md +6 -8
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +6 -1
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +6 -3
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +1 -1
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +1 -1
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +76 -75
- package/steering/nestjs-react-fullstack/skills_common/user-identity/SKILL.md +9 -28
- package/steering/nestjs-react-fullstack/skills_local/authz-guide/SKILL.md +38 -5
- package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/runtime-role-controller-spec.md +6 -8
- package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/sdk-examples.md +6 -1
- package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/sdk-types.md +6 -3
- package/steering/nestjs-react-fullstack/skills_local/client-builtins-user-service/SKILL.md +1 -1
- package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +56 -65
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: authz-guide
|
|
3
|
-
description: "Use
|
|
3
|
+
description: "Use for application authorization work: deciding whether backend/admin features need role-based access, reading current-user or effective-requester roles, filtering data by role, static or dynamic RBAC, permission-point checks, role/member management, role-panel workflows, and diagnosing 403 errors; 后台功能名本身不足以授权启用 RBAC 或新建角色。触发词:权限控制, 当前用户角色, 请求者有效角色, 按角色过滤, 应用角色目录, 角色管理, 成员管理, 静态鉴权, 动态鉴权, 权限点位, 权限配置, RBAC, 403, access control, 角色面板, 设计权限体系, 开启权限服务, 规划角色, 开启角色服务, CanRole, @Can, useCan, 鉴权代码, AuthorizationSDK, 运行时角色"
|
|
4
4
|
steering: true
|
|
5
5
|
steering-topic: authz_guide
|
|
6
6
|
match-template-name: nestjs-react-fullstack
|
|
@@ -8,10 +8,32 @@ match-template-name: nestjs-react-fullstack
|
|
|
8
8
|
|
|
9
9
|
# RBAC 权限编码指南
|
|
10
10
|
|
|
11
|
+
## 当前用户角色与应用角色目录合同
|
|
12
|
+
|
|
13
|
+
1. 当前登录用户角色或按角色过滤:服务端直接读 `req.userContext.roles`;禁止新增当前用户角色 SDK、注入 `PermissionService`、复制内部权限接口或从角色目录回源。
|
|
14
|
+
2. `roles !== undefined` 表示事实已提供:非空数组是命中的角色 ID,`[]` 是明确无角色。`roles === undefined` 表示事实缺失;依赖角色的操作须拒绝或报错,禁止用 `if (!roles)`、`roles || []` 混淆两者。
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { ForbiddenException } from '@nestjs/common';
|
|
18
|
+
import type { Request } from 'express';
|
|
19
|
+
|
|
20
|
+
function getCurrentUserRoles(req: Request): string[] {
|
|
21
|
+
const roles = req.userContext.roles;
|
|
22
|
+
if (roles === undefined) {
|
|
23
|
+
throw new ForbiddenException('当前请求缺少用户角色上下文');
|
|
24
|
+
}
|
|
25
|
+
return roles;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
3. Controller 将校验后的 `roles` 显式传给 Service。无登录用户的 trigger/系统任务须用独立授权模型,禁止伪造角色。
|
|
30
|
+
4. `AuthorizationSDK.roles.list()` 仅列应用角色目录,返回顶层 `ForceRoleDTO[]`(直接遍历,无 `.data`),不查询当前或目标用户角色。`userID` 是妙搭/Force 十进制 I64 请求身份,须为 string;不是目标用户筛选条件或飞书 `user_id` / `open_id`,前端不传。
|
|
11
31
|
## 零、模式决策与实施
|
|
12
32
|
|
|
13
33
|
### 决策总表
|
|
14
34
|
|
|
35
|
+
> **先核访问政策**:后台功能名称本身不授权启用 RBAC 或新建角色。先核对已有访问政策和用户要求;角色范围不明时先澄清,可继续实现不依赖新角色的功能,但不能把未鉴权 API 宣称为安全。确需鉴权时按下表选模式,先用当前沙箱权限管理 skill 查询平台真实角色标识(bizID);仅获授权且确有需要时创建角色,再落实前后端权限规则。
|
|
36
|
+
|
|
15
37
|
| 信号 | 先 DESIGN? | 模式 | 实施章节 | CanRole |
|
|
16
38
|
|------|------------|------|---------|---------|
|
|
17
39
|
| "加权限控制"、"按角色控制可见性" | 否 | 静态角色鉴权 | 第一节 | ✅ 使用 |
|
|
@@ -24,7 +46,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
24
46
|
|
|
25
47
|
> **⛔ 点位全覆盖**:编写鉴权代码时,必须对照权限设计方案的**功能权限表**,将每个权限点位逐一落实到对应的后端 API(`@CanRole` / `@Can`)和前端入口(`<CanRole>` / `<Can>`),完成后逐行核对确认无遗漏。
|
|
26
48
|
|
|
27
|
-
> **⛔ 角色来源一律是平台,禁止另搞一套平台不认的权限系统**:多级审批、按城市/部门分权、多角色组合等复杂授权,角色必须是**平台真实角色**(先 `role create` / `role list`,标识与代码对齐)。**消费平台角色的方式不限**——`@CanRole`/`@Can`
|
|
49
|
+
> **⛔ 角色来源一律是平台,禁止另搞一套平台不认的权限系统**:多级审批、按城市/部门分权、多角色组合等复杂授权,角色必须是**平台真实角色**(先 `role create` / `role list`,标识与代码对齐)。**消费平台角色的方式不限**——`@CanRole`/`@Can` 是语法糖;需要细粒度过滤时,由 Controller 读取并校验 `req.userContext.roles`,再把 `roles` 显式传给 Service,Service 只消费该参数。**不强制每个 API 都用 `@CanRole`**。**真正要禁止的是绕开平台另起炉灶**:自建任何平行的角色/权限存储(自建角色表 / 角色字段 / 用户名单,不限具体命名)、引用平台上不存在(没 create 过)的角色标识——这些 dev 看似能跑、线上必失效。复杂度用「**平台角色(组合)+ 数据层行级过滤**」表达——例如"审核人只审本城市"=`reviewer` 角色把门 + service 按 `cityBranchId` 过滤;"多级审批"=每级一个平台角色 + 把"当前在第几级"存成业务数据状态。
|
|
28
50
|
|
|
29
51
|
### DESIGN 前置步骤
|
|
30
52
|
|
|
@@ -46,6 +68,10 @@ if (response.status === 403) throw new Error('无操作权限,请联系管理
|
|
|
46
68
|
|
|
47
69
|
(403 落 catch 会被吞成通用错误提示如'操作失败请稍后重试',掩盖真实无权限信号,导致排查方向跑偏)
|
|
48
70
|
|
|
71
|
+
### 已要求鉴权但接口返回 200
|
|
72
|
+
|
|
73
|
+
一次 200 不证明守卫缺失。先核请求命中的 handler 及已选模式的方法级 `@CanRole`/`@Can` 元数据、测试身份和当前沙箱权限管理 skill 能提供的角色匹配结果,再核预览环境、应用是否接入 `PlatformModule.forRoot()` 及已安装模块的全局守卫 provider;按查明的缺口修复绑定或装配,不能因一次 200 加第二份 `APP_GUARD`。当前通道无法核实身份时保留未知,不推断线上会 403。
|
|
74
|
+
|
|
49
75
|
### 平台的角色面板入口
|
|
50
76
|
|
|
51
77
|
**只允许在对话中给其入口链接,严禁写到代码中**:`[角色面板](BaseURL?openPanel=auth)` 或 `[角色面板](?openPanel=auth)`
|
|
@@ -85,12 +111,19 @@ const ProtectedRoute: React.FC<{ children: React.ReactNode; requiredRoles: strin
|
|
|
85
111
|
### 后端
|
|
86
112
|
|
|
87
113
|
```typescript
|
|
114
|
+
import { Controller, Get } from '@nestjs/common';
|
|
88
115
|
import { CanRole } from '@lark-apaas/fullstack-nestjs-core';
|
|
89
116
|
|
|
90
|
-
@
|
|
91
|
-
|
|
117
|
+
@Controller('resources')
|
|
118
|
+
export class ResourcesController {
|
|
119
|
+
@Get()
|
|
120
|
+
@CanRole(['<平台实际角色标识>']) // 示例占位;替换为已核验的 bizID
|
|
121
|
+
list() { return []; }
|
|
122
|
+
}
|
|
92
123
|
```
|
|
93
124
|
|
|
125
|
+
`CanRole` 返回 `MethodDecorator`,只标在目标 handler 方法上,不能放在 Controller 类上;多个受限 handler 分别标注,多角色数组为 OR 逻辑。工程接入 `PlatformModule.forRoot()` 后,内部 `AuthZPaasModule.forRoot()` 已通过 `APP_GUARD` 注册全局 `AuthZPaasGuard`;不要重复注册守卫或直接导入应用未声明的传递依赖。
|
|
126
|
+
|
|
94
127
|
---
|
|
95
128
|
|
|
96
129
|
## 二、运行态角色管理(AuthorizationSDK)
|
|
@@ -117,7 +150,7 @@ import { CanRole } from '@lark-apaas/fullstack-nestjs-core';
|
|
|
117
150
|
**关键约束**:
|
|
118
151
|
- 成员按类型分组传递(`MemberMutationData`),不是扁平数组
|
|
119
152
|
- `allEmployees`/`public` 只读,包含「企业全员」或「互联网公开」的角色不支持删除
|
|
120
|
-
- `userID`
|
|
153
|
+
- 当前用户角色来源、角色目录返回层级与 `userID` 边界以开头的“当前用户角色与应用角色目录合同”为准
|
|
121
154
|
|
|
122
155
|
---
|
|
123
156
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# 运行态角色管理 Controller 实施规格
|
|
2
2
|
|
|
3
3
|
> **前置条件**:应用必须已开启角色服务。
|
|
4
|
+
>
|
|
5
|
+
> 本规格暴露应用角色目录管理接口,不查询当前用户命中的角色。SDK 从请求上下文取得调用者身份;Controller DTO 与前端请求都不暴露 `userID`。
|
|
4
6
|
|
|
5
7
|
---
|
|
6
8
|
|
|
@@ -19,24 +21,21 @@ constructor(private readonly authzSDK: AuthorizationSDK) {}
|
|
|
19
21
|
```typescript
|
|
20
22
|
import type { MemberMutationData, FilterParams, MemberType } from '@lark-apaas/fullstack-nestjs-core';
|
|
21
23
|
|
|
24
|
+
// 调用者 userID 由 AuthorizationSDK 从已认证的请求上下文取得;公开 DTO 不允许前端覆盖。
|
|
22
25
|
export class CreateRoleDto {
|
|
23
26
|
role: { name: string; description?: string; bizID: string };
|
|
24
|
-
userID?: string; // 可不传,默认为当前登录用户
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export class UpdateRoleDto {
|
|
28
30
|
role: { name?: string; description?: string };
|
|
29
|
-
userID?: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export class AddMembersDto {
|
|
33
34
|
members: MemberMutationData;
|
|
34
|
-
userID?: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export class RemoveMembersDto {
|
|
38
38
|
members: MemberMutationData;
|
|
39
|
-
userID?: string;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
export class SearchDto {
|
|
@@ -46,7 +45,6 @@ export class SearchDto {
|
|
|
46
45
|
includeExternalGroup?: boolean;
|
|
47
46
|
pageSize?: number;
|
|
48
47
|
page?: number;
|
|
49
|
-
userID?: string;
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
export class ListMembersQueryDto {
|
|
@@ -62,14 +60,14 @@ export class ListMembersQueryDto {
|
|
|
62
60
|
|
|
63
61
|
```typescript
|
|
64
62
|
import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';
|
|
65
|
-
import { AuthorizationSDK } from '@lark-apaas/fullstack-nestjs-core';
|
|
63
|
+
import { AuthorizationSDK, type ForceRoleDTO } from '@lark-apaas/fullstack-nestjs-core';
|
|
66
64
|
|
|
67
65
|
@Controller('api/role_manager')
|
|
68
66
|
export class AuthorizationController {
|
|
69
67
|
constructor(private readonly authzSDK: AuthorizationSDK) {}
|
|
70
68
|
|
|
71
69
|
@Get('roles')
|
|
72
|
-
listRoles() { return this.authzSDK.roles.list(); }
|
|
70
|
+
listRoles(): Promise<ForceRoleDTO[]> { return this.authzSDK.roles.list(); }
|
|
73
71
|
|
|
74
72
|
@Get('roles/:bizID')
|
|
75
73
|
getRole(@Param('bizID') bizID: string) { return this.authzSDK.roles.get(bizID); }
|
|
@@ -163,7 +161,7 @@ export interface SearchMembersRequest {
|
|
|
163
161
|
}
|
|
164
162
|
```
|
|
165
163
|
|
|
166
|
-
>
|
|
164
|
+
> **注意**:请求 DTO 均省略 `userID`。`AuthorizationSDK` 从后端请求上下文取得调用者身份,前端不得传递 `userID`。
|
|
167
165
|
|
|
168
166
|
### 前端 API 层引用示例
|
|
169
167
|
|
|
@@ -7,8 +7,13 @@ import { logger } from "@lark-apaas/client-toolkit/logger";
|
|
|
7
7
|
|
|
8
8
|
// ===================== 角色管理 =====================
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// 获取应用全部角色定义,needMember=true 会在每个角色中附带成员数据
|
|
11
|
+
// 返回顶层 ForceRoleDTO[];直接遍历,不读取 roles.data
|
|
11
12
|
const roles: ForceRoleDTO[] = await sdk.roles.list({ needMember: true });
|
|
13
|
+
for (const roleDefinition of roles) {
|
|
14
|
+
logger.info('角色定义:', roleDefinition.bizID, roleDefinition.name);
|
|
15
|
+
}
|
|
16
|
+
// 应用角色目录不表示当前用户命中了这些角色
|
|
12
17
|
|
|
13
18
|
// 获取单个角色详情
|
|
14
19
|
const role: ForceRoleDTO = await sdk.roles.get('editor');
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> 所有类型均从 `@lark-apaas/fullstack-nestjs-core` 导入。
|
|
4
4
|
>
|
|
5
|
-
>
|
|
5
|
+
> **角色来源**:`req.userContext.roles` 给出当前用户命中的角色 ID;`AuthorizationSDK.roles.list()` 给出应用角色目录,返回顶层 `ForceRoleDTO[]`,不能推断当前用户角色,也不能读取 `.data`。
|
|
6
|
+
>
|
|
7
|
+
> **ID 字段语义**:`ListRolesParams.userID` 是妙搭/Force 十进制 I64 请求身份,必须保持为 string;不是目标用户筛选条件,也不是飞书 `user_id` / `open_id`。其他飞书 API 身份字段见 [`contacts-service`](../../contacts-service/SKILL.md) skill;下表 `larkUserID`/`larkDepartmentID` 为纯数字内部 ID(禁用),飞书 API 用 `employeeID`/`openDepartmentID`/`openChatID`。
|
|
6
8
|
|
|
7
9
|
```typescript
|
|
8
10
|
// ===================== 通用 =====================
|
|
@@ -42,8 +44,9 @@ interface ForceRoleDTO {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
interface ListRolesParams {
|
|
45
|
-
needMember?: boolean;
|
|
46
|
-
|
|
47
|
+
needMember?: boolean; // 是否返回成员信息,默认 true
|
|
48
|
+
// 妙搭/Force 十进制 I64 请求身份,保持 string;非目标用户筛选,禁传飞书 ID
|
|
49
|
+
userID?: string;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
interface CreateRoleParams {
|
package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md
CHANGED
|
@@ -24,7 +24,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
24
24
|
## 使用注意
|
|
25
25
|
- **入口边界**:本 SDK 是**应用前端代码**读写应用存储的唯一入口。Agent 自己在对话 / 开发中上传、灌数据或调试文件,先加载应用文件存储操作 skill(按「应用文件存储 / 文件上传 / 文件下载」召回),用其 CLI 命令操作,具体命令一律以该 skill 为准——二者操作同一个应用存储桶,但**该 CLI 命令只供 Agent 在沙箱终端使用,禁止写进页面代码**,页面里一律用本 SDK。
|
|
26
26
|
- @lark-apaas/client-toolkit/dataloom 这个SDK只适用于前端调用,禁止在服务端调用
|
|
27
|
-
-
|
|
27
|
+
- **上传在 Agent 侧验不了**:`uploadFile` 走平台 runtime 接口,Agent 的验证入口不经平台网关,在沙箱里点上传恒 404;**用户在预览态或发布运行态访问时正常**。按本 skill 写法即为正确,不要为此改代码,也不要据此判断上传能力不可用。开发期改用接口测试打你自己写的后端接口,或复用库里已有的文件 URL。详见 coding-guide「`__runtime__` 接口在 Agent 验证入口下不可达」。
|
|
28
28
|
- 上传成功后,普通 `text` / `varchar` / `JSON` 业务字段默认保存 `data.download_url`,并把它作为页面展示 / 下载 URL
|
|
29
29
|
- **⚠️ 场景区分(重要)**:`dataloom.storage` 仅适用于需要持久化存储文件或获取 `download_url` 保存到数据库的场景。如果文件仅作为插件输入(传给 `capabilityClient`),**必须直接传 File/Blob 对象,禁止先走 dataloom 上传再传 URL**;插件调用(capability)不属于 dataloom,详见 plugin-guide
|
|
30
30
|
- **Server 侧 AI capability 边界**:文件已上传并持久化后,若由后端调用 `CapabilityService` 做文档解析/图片理解,不要把前端展示用 `download_url` 当插件入参;把 `file_path` 和 `bucket_id` 传给后端,由后端 `FileService` 签成临时 http(s) URL。
|
|
@@ -14,7 +14,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
14
14
|
|
|
15
15
|
> **边界说明**:`authClient.session` 仅用于用户登录/登出/获取用户信息等鉴权操作。**插件调用(capability)不属于账户 SDK**,须使用独立的 `capabilityClient`(参见 plugin-guide)。
|
|
16
16
|
>
|
|
17
|
-
>
|
|
17
|
+
> **Agent 验证入口限制**:本 skill 的能力底层走平台 runtime 接口(`__runtime__/api/v1/account/*`),Agent 的验证入口不经平台网关,因此**在 Agent 侧一律打不通**;**用户在预览态或发布运行态访问时正常**。看到 404 或选人列表为空时,禁止改 `business-ui` 组件、禁止加兜底请求或换自研控件绕过,也不要据此判断该能力在用户侧不可用;开发期验证「按当前用户过滤 / 展示姓名」改由服务端出数据(`req.userContext` / `AuthNPaasService`)。症状、端点对照与完整处置见 coding-guide「`__runtime__` 接口在 Agent 验证入口下不可达」。
|
|
18
18
|
>
|
|
19
19
|
> **运行时边界**:本 skill 所有能力(`authClient`、`useCurrentUserProfile`、UserSelect/UserDisplay 等)仅限前端代码使用,**严禁在 `server/**` 中 import**。服务端获取用户身份用 `req.userContext` / `AuthNPaasService`(见 `user-identity` skill),完整边界规则见 coding-guide。
|
|
20
20
|
|
|
@@ -43,62 +43,43 @@ unavailable-agents:
|
|
|
43
43
|
|
|
44
44
|
## 后端结构 (`server/`)
|
|
45
45
|
|
|
46
|
-
基于 MVCS 架构,**禁止新增一级目录**。
|
|
47
|
-
|
|
48
46
|
```
|
|
49
|
-
server/
|
|
50
|
-
├── main.ts
|
|
51
|
-
├── app.module.ts
|
|
52
|
-
├──
|
|
53
|
-
│
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
│ └──
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
├── database/ # Drizzle ORM 数据库相关。应用仅能进行 DML 操作。若用户需求设计表结构变更,DDL 相关操作先加载数据库操作 skill(按「应用数据库 / 建表 / 改表 / SQL」召回),按其中的执行通道与建表规范操作。
|
|
61
|
-
│ ├── schema.ts # Drizzle ORM 数据库 Schema 定义。会在执行完 DDL 操作后自动生成,必须从该文件导入数据库类型,禁止自行编写 schema 文件。如有需要可调用 CodeGen 工具手动生成。
|
|
62
|
-
└── common/ # 共享工具和接口
|
|
63
|
-
│ ├── filters/ # 通用错误处理。
|
|
64
|
-
│ ├── constants/ # 通用常量。
|
|
65
|
-
│ └── utils/ # 通用工具方法。
|
|
66
|
-
shared/ # 前后端共享的目录
|
|
67
|
-
│ └── api.interface.ts # 前后端共享的类型定义
|
|
47
|
+
server/
|
|
48
|
+
├── main.ts # 入口,平台关键文件(见顶部红线)
|
|
49
|
+
├── app.module.ts # 根模块。新 module 必须在此注册,且放在 ViewModule 之前(ViewModule 是 fallback 路由,必须在最后)
|
|
50
|
+
├── modules/
|
|
51
|
+
│ ├── view/ # 平台模板渲染模块,禁止修改
|
|
52
|
+
│ └── hello/ # 示例模块(controller / module / service),可参考
|
|
53
|
+
├── database/ # 应用只做 DML;表结构变更(DDL)先加载数据库操作 skill(按「应用数据库 / 建表 / 改表 / SQL」召回),按其执行通道与建表规范操作
|
|
54
|
+
│ └── schema.ts # DDL 后自动生成(如有需要可调用 CodeGen 工具手动生成)。表类型只能从这里导入,禁止手写或修改
|
|
55
|
+
└── common/ # 通用 filters / constants / interfaces,通用工具方法(utils)也放这里
|
|
56
|
+
shared/
|
|
57
|
+
└── api.interface.ts # 前后端共享类型
|
|
68
58
|
```
|
|
69
59
|
|
|
70
60
|
## 前端结构 (`client/`)
|
|
71
61
|
|
|
72
62
|
```
|
|
73
63
|
client/
|
|
74
|
-
├── index.html
|
|
75
|
-
├── public/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
│
|
|
85
|
-
│ │ | ├── HomePage.tsx # 页面文件
|
|
86
|
-
│ │ | └── HomeComponentA.tsx # 页面依赖的组件(非必须,除非页面文件大于 500 行,否则组件应该写在页面文件内)
|
|
87
|
-
│ │ └── NotFound # 404 页面所在目录
|
|
88
|
-
| │ | └── NotFound.tsx # 404 页面文件
|
|
89
|
-
│ ├── components/ # 可复用的 UI 组件
|
|
90
|
-
│ │ ├── ui/ # shadcn/ui 组件[Use the components for functionality, but heavily style them.]
|
|
91
|
-
│ │ │ ├── README.md # shadcn/ui 组件库使用说明 [请务必参考该文档进行组件使用]
|
|
64
|
+
├── index.html # HTML 模板,平台关键文件(见顶部红线)
|
|
65
|
+
├── public/ # 静态资源
|
|
66
|
+
└── src/
|
|
67
|
+
├── index.tsx # 入口,平台关键文件(见顶部红线)
|
|
68
|
+
├── index.css # 全局样式
|
|
69
|
+
├── tailwind-theme.css # tailwind 主题变量
|
|
70
|
+
├── app.tsx # 路由定义
|
|
71
|
+
├── api/index.ts # 前端请求出口。按单文件还是按模块拆分自行决定;已有内容先读再改
|
|
72
|
+
├── pages/ # 页面
|
|
73
|
+
├── components/
|
|
74
|
+
│ ├── ui/ # shadcn/ui,使用前读 ui/README.md(Use the components for functionality, but heavily style them.)
|
|
92
75
|
{% if projectMeta['flags']['supportBusinessUser'] %}
|
|
93
|
-
│
|
|
94
|
-
│ │ │ ├── README.md # 用户展示,用户头像展示,部门选择展示逻辑必须使用 [请务必参考该文档进行组件使用],禁止直接使用avatar组件等方式自行实现
|
|
76
|
+
│ ├── business-ui/ # 用户/部门选择与展示,必须使用(禁止用 avatar 等自行实现),先读 README.md
|
|
95
77
|
{% endif %}
|
|
96
|
-
│
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
│ └── api.interface.ts # 前后端共享的类型定义
|
|
78
|
+
│ └── Layout.tsx # 布局
|
|
79
|
+
├── hooks/
|
|
80
|
+
└── utils/
|
|
81
|
+
shared/
|
|
82
|
+
└── api.interface.ts # 前后端共享类型
|
|
102
83
|
```
|
|
103
84
|
|
|
104
85
|
---
|
|
@@ -111,7 +92,6 @@ shared/ # 前后端共享的目录
|
|
|
111
92
|
- **路径别名**:`@client/` → `client/`;`@server/` → `server/`;`@shared/` → `shared/`。优先使用别名
|
|
112
93
|
- **环境配置**:不支持配置文件,写在代码中即可
|
|
113
94
|
- **命名约定**:Class → PascalCase,函数/变量 → camelCase,常量 → UPPER_CASE
|
|
114
|
-
- **跨子组件共享常量加语义前缀**:父级桶导出聚合多子组件时,子组件中相同语义的常量必须前缀化(如 `USER_DEFAULTS` / `ORDER_DEFAULTS`),禁止跨子组件复用裸通用名(`DEFAULT_CONFIG` / `STYLES` / `OPTIONS`),避免 barrel re-export 触发 `no-redeclare`
|
|
115
95
|
- **类型约束**:
|
|
116
96
|
- 使用 interface 定义类型,type 用于复杂类型。禁止 `any`
|
|
117
97
|
- 禁止自定义与 TypeScript 内置工具类型同名的类型(如 `Record`, `Omit`)
|
|
@@ -149,9 +129,8 @@ shared/ # 前后端共享的目录
|
|
|
149
129
|
|
|
150
130
|
- **代码行长度**:单行不超过 80-120 字符,import 成员多时及时换行
|
|
151
131
|
- **嵌套层级**:不超过 4 层,用 early return 减少嵌套
|
|
152
|
-
-
|
|
153
|
-
-
|
|
154
|
-
- **大型闭包风险**:组件内大型回调/处理函数(如 handleApplyResults)应拆到独立文件(如 `apply-results.ts`),避免 Vite React Refresh 循环引用导致白屏
|
|
132
|
+
- **单文件规模**:避免上帝文件,文件过大时按功能拆分,拆分方式自行决定
|
|
133
|
+
- **避免循环引用**:文件之间不要形成循环 import(如组件文件与其拆出的工具/处理函数文件互相引用),否则 Vite React Refresh 会失效甚至白屏;共享逻辑抽到单向依赖的独立文件
|
|
155
134
|
- **将风格问题视为编码错误**:项目使用 `@eslint/js` + `typescript-eslint` 推荐配置
|
|
156
135
|
- **ESLint 规范**(`@eslint/js` + `typescript-eslint`):
|
|
157
136
|
- 正则控制字符用 unicode 标志:`/\x1b\[/u`
|
|
@@ -170,7 +149,7 @@ shared/ # 前后端共享的目录
|
|
|
170
149
|
- **组件**:PascalCase(`HelloWorld.tsx`)
|
|
171
150
|
- **模块**:kebab-case 文件夹,PascalCase 文件(`hello/hello.controller.ts`)
|
|
172
151
|
- **配置**:dot.case(`app.config.ts`)
|
|
173
|
-
-
|
|
152
|
+
- **导入导出**:跨目录引用优先用路径别名;同一符号不要同时行内 `export` 和底部 `export { }` 双重导出
|
|
174
153
|
|
|
175
154
|
## 开发环境与内置服务
|
|
176
155
|
|
|
@@ -185,41 +164,64 @@ shared/ # 前后端共享的目录
|
|
|
185
164
|
- `.eslintrc.js`、`.prettierrc`
|
|
186
165
|
- `node_modules/`、`.git/`、`dist/`、`build/`
|
|
187
166
|
|
|
188
|
-
###
|
|
167
|
+
### `__runtime__` 接口在 Agent 验证入口下不可达
|
|
168
|
+
|
|
169
|
+
**判据**:请求路径含 `__runtime__`,或不以 `/api` / `/openapi` 开头——在 Agent 这里一定拿不到正常响应。不必重试,也不要换请求方法再试。
|
|
170
|
+
|
|
171
|
+
**症状**:POST 返回 404;GET 落到 Vite 的 SPA fallback,拿回 200 + `index.html`(业务侧表现为拿到 HTML 而非 JSON、解析失败)。两者差别只来自 Vite fallback,与端点本身无关。
|
|
189
172
|
|
|
190
|
-
|
|
173
|
+
**为什么**:`__runtime__` 的实现在平台服务上,不在你的应用里,靠平台网关转发过去——这层转发是隐式的,应用代码里看不到。而你的请求一律发往沙箱内 `localhost`(截图与 E2E 打 `localhost:8080`,接口测试工具打 `127.0.0.1:<CLIENT_DEV_PORT>`,手写 curl 同理),这条链路上没有网关。
|
|
191
174
|
|
|
192
|
-
|
|
175
|
+
| | 用户访问(`env=preview` / `env=runtime`) | Agent 验证入口 |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| 请求去向 | 浏览器 → 预览 / 运行域名 → **平台网关** → 平台服务 | 工具 → `localhost` → 本地 dev server |
|
|
178
|
+
| `__runtime__/*` | 网关按路径 + 方法转发,**正常返回** | 无此路由 → 404 / `index.html` |
|
|
179
|
+
| `/api/*`(你写的接口) | 正常 | **正常,不受影响** |
|
|
180
|
+
| 身份 | 网关注入 | 工具补 CSRF 与用户身份;`req.userContext` 可用 |
|
|
193
181
|
|
|
194
|
-
|
|
182
|
+
也就是说:平台能力(选人、上传、身份、埋点)在 Agent 侧一律验不了,但用户侧是好的,与应用是否发布无关。
|
|
183
|
+
|
|
184
|
+
看到这类 404 / HTML 时,先在下表确认是谁发的请求(非穷尽,表外按上面的判据兜底):
|
|
195
185
|
|
|
196
186
|
| 端点 | 方法 | 谁会打到它 |
|
|
197
187
|
|------|------|-----------|
|
|
198
|
-
| `api/v1/permissions/roles` |
|
|
199
|
-
| `api/v1/observability/{logs,traces,metrics}/collect
|
|
188
|
+
| `api/v1/permissions/roles` | POST | `AppContainer` **每次加载应用自动打**,不需要你写任何代码 |
|
|
189
|
+
| `api/v1/observability/{logs,traces,metrics}/collect` | POST | 平台埋点上报,**自动打** |
|
|
190
|
+
| `api/v1/observability/current_server_timestamp` | GET | 平台埋点上报,**自动打** |
|
|
200
191
|
| `api/v1/account/login/user` | POST | `useCurrentUserProfile()` → `authClient.session.getUserInfo()` |
|
|
201
|
-
| `api/v1/account/search_user`、`list_users`、`
|
|
192
|
+
| `api/v1/account/search_user`、`list_users`、`search_department`、`convert_lark_user` | POST | `business-ui` 的 UserSelect / DepartmentSelect |
|
|
193
|
+
| `api/v1/account/user_profile` | GET | `business-ui` 的 UserDisplay |
|
|
202
194
|
| `api/v1/account/search`、`api/v1/account/chat/list_chats` | POST | ChatSelect 选群组件 |
|
|
203
|
-
| `api/v1/studio/user/profile` |
|
|
195
|
+
| `api/v1/studio/user/profile` | POST | `getUserProfile()` |
|
|
204
196
|
| `api/v1/storage/object/<bucket>/pre_upload` | POST | `dataloom.storage.uploadFile()` 前端上传 |
|
|
205
197
|
| `api/v1/studio/plugins/tmp_files/acquire_upload_url`、`acquire_download_url` | POST | `capabilityClient` 的文件类入参 |
|
|
206
198
|
|
|
207
199
|
> `useCurrentUserProfile()` 打的是 `account/login/user`,**不是** `user_profile`/`search_user` 那一排;它另外还打一个 `GET /api/authnpaas/lark-user-id`,那条走 `/api`、被代理、正常。
|
|
208
200
|
|
|
209
|
-
|
|
201
|
+
**不要做**:
|
|
202
|
+
|
|
203
|
+
- 改 `client/src/components/business-ui/**`(平台保护文件)
|
|
204
|
+
- 给组件加兜底请求、换自研控件、改造业务方案来绕开这些能力——它们在用户侧工作正常,换掉会让应用少掉本来可用的功能
|
|
205
|
+
- 据此判断该能力在用户侧不可用,或把它写成产品能力缺陷;如实说明「该部分未在开发态验证」是允许的
|
|
206
|
+
- 拿接口测试工具去打 `__runtime__` 路径——该工具只受理 `/api`、`/openapi`,请求在参数校验阶段就被拒,白跑一轮
|
|
210
207
|
|
|
211
|
-
|
|
212
|
-
- 需要在开发期验证「按当前用户过滤」「展示用户姓名」这类逻辑:改由服务端出数据。服务端的 `req.userContext` 和 `AuthNPaasService` 由网关注入,**不受此限制**,始终可用(见 `user-identity` / `contacts-service` skill)。`useCurrentUserProfile()` 在这里长期停在加载态就是这个原因
|
|
213
|
-
- 需要在开发期验证上传链路:改用接口测试直接打后端接口,或复用库里已有的文件 URL,不要在浏览器里点上传
|
|
214
|
-
- 依赖上述端点的 E2E 用例不要写成浏览器交互 Case,改走接口层往返验证
|
|
208
|
+
**改为做**:
|
|
215
209
|
|
|
216
|
-
|
|
210
|
+
- 验证「按当前用户过滤」「展示用户姓名」这类逻辑:改由服务端出数据。`req.userContext` 和 `AuthNPaasService` 在 NestJS 侧可用,**不受此限制**(见 `user-identity` / `contacts-service` skill)。`useCurrentUserProfile()` 在这里长期停在加载态就是这个原因
|
|
211
|
+
- 验证上传链路:用接口测试打你自己写的后端接口(`/api/*` 正常可用),或复用库里已有的文件 URL
|
|
212
|
+
- E2E 用例改为验证服务端替代路径,不写成浏览器交互 Case
|
|
213
|
+
|
|
214
|
+
另外,`UserSelect` 弹层是**搜索驱动**的:不输关键词时列表为空属预期行为。
|
|
215
|
+
|
|
216
|
+
> 根因是 Agent 工具链缺一条经平台网关的验证通路(本地 `dev:local` 已有可用实现,沙箱模式未挂)。在通路补上之前,本节的纪律是唯一护栏。
|
|
217
217
|
|
|
218
218
|
## 应用访问入口(404 / 白屏先看这里)
|
|
219
219
|
|
|
220
220
|
沙箱内页面入口、API 入口、NestJS 端口不是一回事,禁止用 `5173/3000/8001/8080` 枚举猜测。
|
|
221
221
|
|
|
222
|
-
|
|
222
|
+
**通用规则**:Agent 的全部验证入口(截图 / E2E / 接口测试 / 手写 curl)都在沙箱内部,不论直连 vite 还是经沙箱内 nginx 8080,都不经过平台网关。凡是由平台网关按路径接管的接口,在 Agent 侧一律不可达,与应用是否发布无关(实例见「`__runtime__` 接口在 Agent 验证入口下不可达」)。
|
|
223
|
+
|
|
224
|
+
- **看页面 / 白屏 / 交互**:优先用截图、视觉检查或 E2E 工具,并只传相对路径;工具会打开沙箱内预览入口(E2E 走沙箱内 nginx 8080,不是平台网关)。不要用 `curl` 拿 HTML 判断页面是否渲染。
|
|
223
225
|
- **测后端接口**:优先用接口测试工具,只传以 `/api` 或 `/openapi` 开头的业务路径;工具会自动拼 `CLIENT_BASE_PATH`、选择正确 dev 端口,并补 CSRF 与用户身份。
|
|
224
226
|
- **必须手写 curl 调 `/api/*` 时**:打 client dev server,不要打 NestJS `SERVER_PORT`;URL 必须带 `$CLIENT_BASE_PATH`,并同时带 `Cookie: suda-csrf-token=<X>` 与 `X-Suda-Csrf-Token: <X>`(两值字面相等)。漏 basePath 是 404,漏 CSRF 是 403,都不是业务代码 bug,不要为此改 csrf 中间件或给接口加白名单。
|
|
225
227
|
- **不要直连 NestJS 端口**:会绕过 dev server / 网关注入,`x-larkgw-suda-webuser` 缺失后 `userContext.userId` 为空,依赖身份的接口会误判。
|
|
@@ -316,7 +318,11 @@ export class TestService {
|
|
|
316
318
|
}
|
|
317
319
|
```
|
|
318
320
|
|
|
321
|
+
- **数据库身份由平台管理**:平台会在每次 Drizzle 查询前注入当前请求的用户身份和数据库角色。业务代码不得执行 `SET app.user_id`、`SET LOCAL app.user_id`、`SET ROLE`,也不得调用身份注入内部实现或伪造请求、用户 ID、workspace role;这些设置不会传递给后续查询,且可能被平台覆盖
|
|
322
|
+
- **按请求实际身份设计 RLS**:登录请求使用 `req.userContext`;允许未登录访问时,为对应表配置 `anon` RLS policy。API Key、Webhook、Cron 等没有登录用户的入口不得冒充用户;现有 RLS 无法安全表达其权限时,停止该身份方案并说明能力限制
|
|
319
323
|
- **每次编写或修改数据库操作代码前,必须重新读取 `server/database/schema.ts`**——该文件由系统在 DDL 执行后自动重新生成,内容随时可能变化。禁止凭记忆或之前读取的版本编写字段名和类型,否则会引用不存在的字段导致 TS2339 批量报错
|
|
324
|
+
- `schema.ts` 是数据库结构的生成镜像,可用于字段、类型、普通索引及其完整表达的普通唯一结构;它不保留条件索引、表达式索引、CHECK 等复杂结构的全部语义。判断这类结构或复杂唯一性、约束的精确语义时,必须在首次下结论或编写依赖该语义的代码前,加载数据库操作 skill,并按其说明读取目标表的完整 DDL。不得只依据 `schema.ts`、代码或普通数据查询推断
|
|
325
|
+
- 禁止手改生成的 `schema.ts` 补回缺失的数据库语义;需要改变结构时,按数据库操作 skill 执行 DDL,再重新生成并读取 `schema.ts`
|
|
320
326
|
- **事务**:多条写操作需要整体成功/失败时(如扣库存 + 写审批流水),用 `db.transaction` 包裹;单条语句能完成的操作不要套事务
|
|
321
327
|
- 条件查询用三元分支,禁止 `let query` 重赋值(类型不兼容):
|
|
322
328
|
|
|
@@ -510,7 +516,6 @@ await db.select().from(users).where(eq(users.adminUser, userId));
|
|
|
510
516
|
4. **前后端一致性**(CRITICAL):
|
|
511
517
|
- 前端 API 调用的 HTTP method 和 path **必须**与后端 Controller 装饰器完全匹配(`GET≠POST` → 405)
|
|
512
518
|
- 后端 Service 返回值结构**必须**与 `shared/api.interface.ts` 中定义的响应类型完全匹配(禁止后端直接返回数组而 shared 定义为 `{items: T[]}`)
|
|
513
|
-
- 编写前端 API 时必须先读取对应 Controller 确认 method 和路径
|
|
514
519
|
5. **路由注册验证**:创建新 Controller 后必须用接口测试工具验证路由是否生效。**遇到 404 排查路径**:① 检查 `@Controller(...)` 是否以 `api/` 或 `openapi/` 开头 ② 检查 Module 是否在 `app.module.ts` 注册 ③ 检查静态路由是否在动态路由 `/:id` 之前
|
|
515
520
|
6. **@Query/@Param 类型转换**:默认 string,必须在 controller 层手动转换(如 `parseInt(limit, 10)`)
|
|
516
521
|
7. **写接口鉴权**:POST/PUT/PATCH/DELETE 不一刀切。个人/后台/依赖当前用户的写入加 `@NeedLogin()`;未登录访客表单/报名/反馈可公开,但每张被写入的 RLS 表都要有 `FOR INSERT TO anon WITH CHECK (true)` 或等价 Drizzle `withCheck`,并用未登录 HTTP POST 验证。`FOR SELECT TO anon` 只读、`FOR INSERT TO authenticated` 只登录态;公开只读页的增删改仍按登录态处理。
|
|
@@ -631,8 +636,8 @@ async createArticle(@Req() req: Request, @Body() dto: CreateArticleDto) {
|
|
|
631
636
|
|
|
632
637
|
1. 使用 API 测试工具测试不依赖用户信息的接口
|
|
633
638
|
2. 积极使用 `read_logs` 工具,如无有效日志可增加 logger 打印
|
|
634
|
-
3. API 测试返回 HTML 内容时:检查模块注册顺序、路由顺序、请求路径。禁止修改内置 ViewController。若路径含 `__runtime__
|
|
635
|
-
4. 请求路径含 `__runtime__` 的 404(POST)或 200 + HTML(GET
|
|
639
|
+
3. API 测试返回 HTML 内容时:检查模块注册顺序、路由顺序、请求路径。禁止修改内置 ViewController。若路径含 `__runtime__`,见下一条
|
|
640
|
+
4. 请求路径含 `__runtime__` 的 404(POST)或 200 + HTML(GET):Agent 的验证入口不经平台网关所致,用户侧不受影响,见「`__runtime__` 接口在 Agent 验证入口下不可达」
|
|
636
641
|
5. 服务端启动即崩、`/api` 全部 502 且报 `UnknownDependenciesException ... argument Function at index [0]`:查构造函数有没有空参 `@Inject()`(见 plugin-guide),不要先去改 Module 的 imports / providers
|
|
637
642
|
|
|
638
643
|
---
|
|
@@ -668,7 +673,7 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
|
|
|
668
673
|
// ✅ axiosForBackend.get('/api/users') / .post(...) 等实例方法(axiosForBackend 是 axios.create 返回的实例)
|
|
669
674
|
```
|
|
670
675
|
|
|
671
|
-
-
|
|
676
|
+
- **前后端联调**:禁止对后端接口请求做兜底和过度封装
|
|
672
677
|
- **全栈项目禁止 mock 数据**:只要有服务端接口就当全栈对待,严禁 mock。纯前端项目(静态展示、无后端)可以使用 mock
|
|
673
678
|
- **提交前检查 mock 残留**:禁止硬编码用户 ID、项目 ID 等 mock 数据。提交前确认无 `"user_xxx"`、`"project_1"` 等占位值
|
|
674
679
|
|
|
@@ -678,10 +683,6 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
|
|
|
678
683
|
- **禁止静默处理异常**:显示明确错误信息,禁止掩盖问题
|
|
679
684
|
- **无内置多语言/深浅色切换**:如需要需自行实现
|
|
680
685
|
|
|
681
|
-
## 工程规范
|
|
682
|
-
|
|
683
|
-
- **glob 文件查找**:目录存在嵌套,glob 前端页面时必须使用 `client/src/pages/**`
|
|
684
|
-
|
|
685
686
|
## 官方内置组件
|
|
686
687
|
|
|
687
688
|
| 组件 | 来源 | 用途 |
|
|
@@ -60,9 +60,13 @@ match-template-name: nestjs-react-fullstack
|
|
|
60
60
|
├─ 接口认证(@NeedLogin / 公开接口 / 401 / 守卫流程)?
|
|
61
61
|
│ └─ 跳到 `authn-guide` skill
|
|
62
62
|
│
|
|
63
|
-
├─ 需要在服务端读取当前用户 ID /
|
|
63
|
+
├─ 需要在服务端读取当前用户 ID / 租户?
|
|
64
64
|
│ └─ 是 ──→ req.userContext(第二节)
|
|
65
65
|
│
|
|
66
|
+
├─ 需要当前用户角色 / 请求者有效角色 / 按角色过滤?
|
|
67
|
+
│ ├─ req.userContext.roles !== undefined ──→ 直接使用已注入角色
|
|
68
|
+
│ └─ req.userContext.roles === undefined ──→ 明确拒绝或报错(第二节)
|
|
69
|
+
│
|
|
66
70
|
├─ 需要在前端展示当前用户信息(名称/头像/邮箱)?
|
|
67
71
|
│ └─ 是 ──→ useCurrentUserProfile()(第三节)
|
|
68
72
|
│
|
|
@@ -112,37 +116,14 @@ match-template-name: nestjs-react-fullstack
|
|
|
112
116
|
| `userName` | `string` | 用户名 |
|
|
113
117
|
| `userNameI18n` | `{ zh_cn, en_us, ja_jp }` | 多语言用户名 |
|
|
114
118
|
| `isSystemAccount` | `boolean` | 是否系统账号 |
|
|
115
|
-
| `roles` | `string[] \|
|
|
119
|
+
| `roles` | `string[] \| undefined` | 当前用户角色 ID;缺失处理见下文 |
|
|
116
120
|
| `baseUrl` | `string` | 网关内部地址 |
|
|
117
121
|
|
|
118
|
-
###
|
|
119
|
-
|
|
120
|
-
`roles` 字段记录当前用户在应用中的角色列表,可在 Controller 业务逻辑中消费(如根据角色返回不同数据):
|
|
122
|
+
### 服务端读取当前用户角色
|
|
121
123
|
|
|
122
|
-
|
|
123
|
-
import { Controller, Get, Req } from '@nestjs/common';
|
|
124
|
-
import { Request } from 'express'; // ⚠️ Request 类型必须来自 'express',不要 import 自 'http' / 'undici' 或漏掉 import
|
|
125
|
-
import { TaskService } from './task.service'; // 按项目实际路径 import 业务 Service
|
|
126
|
-
|
|
127
|
-
// req.userContext 的类型由 @lark-apaas/fullstack-nestjs-core 通过 declare module 'express' 自动注入到 Request,无需手动声明
|
|
128
|
-
@Controller('api/tasks')
|
|
129
|
-
export class TasksController {
|
|
130
|
-
constructor(private readonly taskService: TaskService) {}
|
|
131
|
-
|
|
132
|
-
@Get()
|
|
133
|
-
async listTasks(@Req() req: Request) {
|
|
134
|
-
const roles: string[] | null = req.userContext?.roles ?? null;
|
|
135
|
-
// roles 示例:['text_editor', 'visitor', 'admin']
|
|
136
|
-
// ⚠️ 未开启权限服务或用户无角色时为 null
|
|
137
|
-
if (roles?.includes('admin')) {
|
|
138
|
-
return this.taskService.findAll();
|
|
139
|
-
}
|
|
140
|
-
return this.taskService.findByUser(req.userContext?.userId);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
```
|
|
124
|
+
标准登录请求读 `req.userContext.roles`:`roles !== undefined`(含 `[]`)表示事实已提供,`[]` 是明确无角色;`roles === undefined` 表示事实缺失,依赖角色的操作须拒绝或报错。禁止从角色目录或 `PermissionService` 回源、复制内部接口、用空数组兜底。
|
|
144
125
|
|
|
145
|
-
|
|
126
|
+
Controller 将校验后的 `roles` 显式传给 Service。`AuthorizationSDK.roles.list()` 只列应用角色目录,不查询当前用户角色。无登录用户的 trigger/系统任务须用独立授权模型,禁止伪造角色。
|
|
146
127
|
|
|
147
128
|
---
|
|
148
129
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: authz-guide
|
|
3
|
-
description: "Use
|
|
3
|
+
description: "Use for application authorization work: deciding whether backend/admin features need role-based access, reading current-user or effective-requester roles, filtering data by role, static or dynamic RBAC, permission-point checks, role/member management, role-panel workflows, and diagnosing 403 errors; 后台功能名本身不足以授权启用 RBAC 或新建角色。触发词:权限控制, 当前用户角色, 请求者有效角色, 按角色过滤, 应用角色目录, 角色管理, 成员管理, 静态鉴权, 动态鉴权, 权限点位, 权限配置, RBAC, 403, access control, 角色面板, 设计权限体系, 开启权限服务, 规划角色, 开启角色服务, CanRole, @Can, useCan, 鉴权代码, AuthorizationSDK, 运行时角色"
|
|
4
4
|
steering: true
|
|
5
5
|
steering-topic: authz_guide
|
|
6
6
|
match-template-name: nestjs-react-fullstack
|
|
@@ -8,6 +8,26 @@ match-template-name: nestjs-react-fullstack
|
|
|
8
8
|
|
|
9
9
|
# RBAC 权限编码指南
|
|
10
10
|
|
|
11
|
+
## 当前用户角色与应用角色目录合同
|
|
12
|
+
|
|
13
|
+
1. 当前登录用户角色或按角色过滤:服务端直接读 `req.userContext.roles`;禁止新增当前用户角色 SDK、注入 `PermissionService`、复制内部权限接口或从角色目录回源。
|
|
14
|
+
2. `roles !== undefined` 表示事实已提供:非空数组是命中的角色 ID,`[]` 是明确无角色。`roles === undefined` 表示事实缺失;依赖角色的操作须拒绝或报错,禁止用 `if (!roles)`、`roles || []` 混淆两者。
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { ForbiddenException } from '@nestjs/common';
|
|
18
|
+
import type { Request } from 'express';
|
|
19
|
+
|
|
20
|
+
function getCurrentUserRoles(req: Request): string[] {
|
|
21
|
+
const roles = req.userContext.roles;
|
|
22
|
+
if (roles === undefined) {
|
|
23
|
+
throw new ForbiddenException('当前请求缺少用户角色上下文');
|
|
24
|
+
}
|
|
25
|
+
return roles;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
3. Controller 将校验后的 `roles` 显式传给 Service。无登录用户的 trigger/系统任务须用独立授权模型,禁止伪造角色。
|
|
30
|
+
4. `AuthorizationSDK.roles.list()` 仅列应用角色目录,返回顶层 `ForceRoleDTO[]`(直接遍历,无 `.data`),不查询当前或目标用户角色。`userID` 是妙搭/Force 十进制 I64 请求身份,须为 string;不是目标用户筛选条件或飞书 `user_id` / `open_id`,前端不传。
|
|
11
31
|
## 本地 lark-cli 开发适配
|
|
12
32
|
|
|
13
33
|
本节只规定 `apps init` 后的本地工程识别、平台资源核验和验证流程,**不改变下方既有的权限模式、模板代码或页面规格**。
|
|
@@ -24,6 +44,8 @@ match-template-name: nestjs-react-fullstack
|
|
|
24
44
|
|
|
25
45
|
### 决策总表
|
|
26
46
|
|
|
47
|
+
> **先核访问政策**:后台功能名称本身不授权启用 RBAC 或新建角色。先核对已有访问政策和用户要求;角色范围不明时先澄清,可继续实现不依赖新角色的功能,但不能把未鉴权 API 宣称为安全。确需鉴权时按下表选模式,先用 `lark-cli apps +role-list` / `+role-get` 查询平台真实 `role_id`;仅获授权且确有需要时创建角色,再落实前后端权限规则。
|
|
48
|
+
|
|
27
49
|
| 信号 | 先 DESIGN? | 模式 | 实施章节 | CanRole |
|
|
28
50
|
|------|------------|------|---------|---------|
|
|
29
51
|
| "加权限控制"、"按角色控制可见性" | 否 | 静态角色鉴权 | 第一节 | ✅ 使用 |
|
|
@@ -36,7 +58,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
36
58
|
|
|
37
59
|
> **⛔ 点位全覆盖**:编写鉴权代码时,必须对照权限设计方案的**功能权限表**,将每个权限点位逐一落实到对应的后端 API(`@CanRole` / `@Can`)和前端入口(`<CanRole>` / `<Can>`),完成后逐行核对确认无遗漏。
|
|
38
60
|
|
|
39
|
-
> **⛔ 角色来源一律是平台,禁止另搞一套平台不认的权限系统**:多级审批、按城市/部门分权、多角色组合等复杂授权,角色必须是**平台真实角色**(先用 `lark-cli apps +role-list` 查询;需要创建时使用 `+role-create`,并让返回的 `role_id` 与代码对齐)。**消费平台角色的方式不限**——`@CanRole`/`@Can`
|
|
61
|
+
> **⛔ 角色来源一律是平台,禁止另搞一套平台不认的权限系统**:多级审批、按城市/部门分权、多角色组合等复杂授权,角色必须是**平台真实角色**(先用 `lark-cli apps +role-list` 查询;需要创建时使用 `+role-create`,并让返回的 `role_id` 与代码对齐)。**消费平台角色的方式不限**——`@CanRole`/`@Can` 是语法糖;需要细粒度过滤时,由 Controller 读取并校验 `req.userContext.roles`,再把 `roles` 显式传给 Service,Service 只消费该参数。**不强制每个 API 都用 `@CanRole`**。**真正要禁止的是绕开平台另起炉灶**:自建任何平行的角色/权限存储(自建角色表 / 角色字段 / 用户名单,不限具体命名)、引用平台上不存在的角色标识——这些 dev 看似能跑、线上必失效。复杂度用「**平台角色(组合)+ 数据层行级过滤**」表达——例如"审核人只审本城市"=`reviewer` 角色把门 + service 按 `cityBranchId` 过滤;"多级审批"=每级一个平台角色 + 把"当前在第几级"存成业务数据状态。
|
|
40
62
|
|
|
41
63
|
### DESIGN 前置步骤
|
|
42
64
|
|
|
@@ -59,6 +81,10 @@ if (response.status === 403) throw new Error('无操作权限,请联系管理
|
|
|
59
81
|
|
|
60
82
|
本地只读排查 403 时,读取 `.spark/meta.json` 和真实 handler/policy 后,依次用 `+role-list`、`+role-match-list --user-id <open_id>`、`+role-get --role-id <required_role_id>` 对比“代码要求角色”和“用户实际命中角色”。只有代码绑定链和平台结果都能对上时才下根因结论,不为排查自动创建角色、添加成员或修改可见范围。
|
|
61
83
|
|
|
84
|
+
### 已要求鉴权但接口返回 200
|
|
85
|
+
|
|
86
|
+
一次 200 不证明守卫缺失。先核请求命中的 handler 及已选模式的方法级 `@CanRole`/`@Can` 元数据、测试身份和 `lark-cli apps +role-match-list` 返回的实际命中角色,再核预览环境、应用是否接入 `PlatformModule.forRoot()` 及已安装模块的全局守卫 provider;按查明的缺口修复绑定或装配,不能因一次 200 加第二份 `APP_GUARD`。身份未核实前不推断线上会 403。
|
|
87
|
+
|
|
62
88
|
### 平台的角色面板入口
|
|
63
89
|
|
|
64
90
|
**只允许在对话中给其入口链接,严禁写到代码中**:`[角色面板](BaseURL?openPanel=auth)` 或 `[角色面板](?openPanel=auth)`
|
|
@@ -98,12 +124,19 @@ const ProtectedRoute: React.FC<{ children: React.ReactNode; requiredRoles: strin
|
|
|
98
124
|
### 后端
|
|
99
125
|
|
|
100
126
|
```typescript
|
|
127
|
+
import { Controller, Get } from '@nestjs/common';
|
|
101
128
|
import { CanRole } from '@lark-apaas/fullstack-nestjs-core';
|
|
102
129
|
|
|
103
|
-
@
|
|
104
|
-
|
|
130
|
+
@Controller('resources')
|
|
131
|
+
export class ResourcesController {
|
|
132
|
+
@Get()
|
|
133
|
+
@CanRole(['<平台实际角色标识>']) // 示例占位;替换为已核验的 role_id
|
|
134
|
+
list() { return []; }
|
|
135
|
+
}
|
|
105
136
|
```
|
|
106
137
|
|
|
138
|
+
`CanRole` 返回 `MethodDecorator`,只标在目标 handler 方法上,不能放在 Controller 类上;多个受限 handler 分别标注,多角色数组为 OR 逻辑。工程接入 `PlatformModule.forRoot()` 后,内部 `AuthZPaasModule.forRoot()` 已通过 `APP_GUARD` 注册全局 `AuthZPaasGuard`;不要重复注册守卫或直接导入应用未声明的传递依赖。
|
|
139
|
+
|
|
107
140
|
---
|
|
108
141
|
|
|
109
142
|
## 二、运行态角色管理(AuthorizationSDK)
|
|
@@ -130,7 +163,7 @@ import { CanRole } from '@lark-apaas/fullstack-nestjs-core';
|
|
|
130
163
|
**关键约束**:
|
|
131
164
|
- 成员按类型分组传递(`MemberMutationData`),不是扁平数组
|
|
132
165
|
- `allEmployees`/`public` 只读,包含「企业全员」或「互联网公开」的角色不支持删除
|
|
133
|
-
- `userID`
|
|
166
|
+
- 当前用户角色来源、角色目录返回层级与 `userID` 边界以开头的“当前用户角色与应用角色目录合同”为准
|
|
134
167
|
|
|
135
168
|
---
|
|
136
169
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# 运行态角色管理 Controller 实施规格
|
|
2
2
|
|
|
3
3
|
> **前置条件**:应用必须已开启角色服务。
|
|
4
|
+
>
|
|
5
|
+
> 本规格暴露应用角色目录管理接口,不查询当前用户命中的角色。SDK 从请求上下文取得调用者身份;Controller DTO 与前端请求都不暴露 `userID`。
|
|
4
6
|
|
|
5
7
|
---
|
|
6
8
|
|
|
@@ -19,24 +21,21 @@ constructor(private readonly authzSDK: AuthorizationSDK) {}
|
|
|
19
21
|
```typescript
|
|
20
22
|
import type { MemberMutationData, FilterParams, MemberType } from '@lark-apaas/fullstack-nestjs-core';
|
|
21
23
|
|
|
24
|
+
// 调用者 userID 由 AuthorizationSDK 从已认证的请求上下文取得;公开 DTO 不允许前端覆盖。
|
|
22
25
|
export class CreateRoleDto {
|
|
23
26
|
role: { name: string; description?: string; bizID: string };
|
|
24
|
-
userID?: string; // 可不传,默认为当前登录用户
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export class UpdateRoleDto {
|
|
28
30
|
role: { name?: string; description?: string };
|
|
29
|
-
userID?: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export class AddMembersDto {
|
|
33
34
|
members: MemberMutationData;
|
|
34
|
-
userID?: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export class RemoveMembersDto {
|
|
38
38
|
members: MemberMutationData;
|
|
39
|
-
userID?: string;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
export class SearchDto {
|
|
@@ -46,7 +45,6 @@ export class SearchDto {
|
|
|
46
45
|
includeExternalGroup?: boolean;
|
|
47
46
|
pageSize?: number;
|
|
48
47
|
page?: number;
|
|
49
|
-
userID?: string;
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
export class ListMembersQueryDto {
|
|
@@ -62,14 +60,14 @@ export class ListMembersQueryDto {
|
|
|
62
60
|
|
|
63
61
|
```typescript
|
|
64
62
|
import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';
|
|
65
|
-
import { AuthorizationSDK } from '@lark-apaas/fullstack-nestjs-core';
|
|
63
|
+
import { AuthorizationSDK, type ForceRoleDTO } from '@lark-apaas/fullstack-nestjs-core';
|
|
66
64
|
|
|
67
65
|
@Controller('api/role_manager')
|
|
68
66
|
export class AuthorizationController {
|
|
69
67
|
constructor(private readonly authzSDK: AuthorizationSDK) {}
|
|
70
68
|
|
|
71
69
|
@Get('roles')
|
|
72
|
-
listRoles() { return this.authzSDK.roles.list(); }
|
|
70
|
+
listRoles(): Promise<ForceRoleDTO[]> { return this.authzSDK.roles.list(); }
|
|
73
71
|
|
|
74
72
|
@Get('roles/:bizID')
|
|
75
73
|
getRole(@Param('bizID') bizID: string) { return this.authzSDK.roles.get(bizID); }
|
|
@@ -163,7 +161,7 @@ export interface SearchMembersRequest {
|
|
|
163
161
|
}
|
|
164
162
|
```
|
|
165
163
|
|
|
166
|
-
>
|
|
164
|
+
> **注意**:请求 DTO 均省略 `userID`。`AuthorizationSDK` 从后端请求上下文取得调用者身份,前端不得传递 `userID`。
|
|
167
165
|
|
|
168
166
|
### 前端 API 层引用示例
|
|
169
167
|
|
|
@@ -7,8 +7,13 @@ import { logger } from "@lark-apaas/client-toolkit/logger";
|
|
|
7
7
|
|
|
8
8
|
// ===================== 角色管理 =====================
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// 获取应用全部角色定义,needMember=true 会在每个角色中附带成员数据
|
|
11
|
+
// 返回顶层 ForceRoleDTO[];直接遍历,不读取 roles.data
|
|
11
12
|
const roles: ForceRoleDTO[] = await sdk.roles.list({ needMember: true });
|
|
13
|
+
for (const roleDefinition of roles) {
|
|
14
|
+
logger.info('角色定义:', roleDefinition.bizID, roleDefinition.name);
|
|
15
|
+
}
|
|
16
|
+
// 应用角色目录不表示当前用户命中了这些角色
|
|
12
17
|
|
|
13
18
|
// 获取单个角色详情
|
|
14
19
|
const role: ForceRoleDTO = await sdk.roles.get('editor');
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> 所有类型均从 `@lark-apaas/fullstack-nestjs-core` 导入。
|
|
4
4
|
>
|
|
5
|
-
>
|
|
5
|
+
> **角色来源**:`req.userContext.roles` 给出当前用户命中的角色 ID;`AuthorizationSDK.roles.list()` 给出应用角色目录,返回顶层 `ForceRoleDTO[]`,不能推断当前用户角色,也不能读取 `.data`。
|
|
6
|
+
>
|
|
7
|
+
> **ID 字段语义**:`ListRolesParams.userID` 是妙搭/Force 十进制 I64 请求身份,必须保持为 string;不是目标用户筛选条件,也不是飞书 `user_id` / `open_id`。其他飞书 API 身份字段见 [`contacts-service`](../../contacts-service/SKILL.md) skill;下表 `larkUserID`/`larkDepartmentID` 为纯数字内部 ID(禁用),飞书 API 用 `employeeID`/`openDepartmentID`/`openChatID`。
|
|
6
8
|
|
|
7
9
|
```typescript
|
|
8
10
|
// ===================== 通用 =====================
|
|
@@ -42,8 +44,9 @@ interface ForceRoleDTO {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
interface ListRolesParams {
|
|
45
|
-
needMember?: boolean;
|
|
46
|
-
|
|
47
|
+
needMember?: boolean; // 是否返回成员信息,默认 true
|
|
48
|
+
// 妙搭/Force 十进制 I64 请求身份,保持 string;非目标用户筛选,禁传飞书 ID
|
|
49
|
+
userID?: string;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
interface CreateRoleParams {
|
|
@@ -14,7 +14,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
14
14
|
|
|
15
15
|
> **边界说明**:`authClient.session` 仅用于用户登录/登出/获取用户信息等鉴权操作。**插件调用(capability)不属于账户 SDK**,须使用独立的 `capabilityClient`(参见 plugin-guide)。
|
|
16
16
|
>
|
|
17
|
-
> **本地 dev 限制**:本 skill
|
|
17
|
+
> **本地 dev 限制**:本 skill 的能力底层走平台 runtime 接口(`__runtime__/api/v1/account/*`),由平台网关接管。**判据先看 `.env.local` 有没有 `MIAODA_DEV_PLATFORM_BASE`**:有,说明 `env pull` 成功、dev-proxy 会把请求反代到经过网关的沙箱公网入口,这条链路是通的;没有(env pull 失败或未装 lark-cli,启动脚本只 warn 不中断)则反代没挂上,表现为 POST 404 / GET 返回 200 + index.html。缺失时先重跑 `env pull`;确实拉不到再改由服务端出数据(`req.userContext` / `AuthNPaasService`),禁止改 `business-ui` 组件或换自研控件绕过。端点对照见 coding-guide 同名小节。
|
|
18
18
|
>
|
|
19
19
|
> **运行时边界**:本 skill 所有能力(`authClient`、`useCurrentUserProfile`、UserSelect/UserDisplay 等)仅限前端代码使用,**严禁在 `server/**` 中 import**。服务端获取用户身份用 `req.userContext` / `AuthNPaasService`(见 `user-identity` skill),完整边界规则见 coding-guide。
|
|
20
20
|
|
|
@@ -18,60 +18,41 @@ description: '项目全局编码规范,必须在任何代码编写、阅读、
|
|
|
18
18
|
|
|
19
19
|
## 后端结构 (`server/`)
|
|
20
20
|
|
|
21
|
-
基于 MVCS 架构,**禁止新增一级目录**。
|
|
22
|
-
|
|
23
21
|
```
|
|
24
|
-
server/
|
|
25
|
-
├── main.ts
|
|
26
|
-
├── app.module.ts
|
|
27
|
-
├──
|
|
28
|
-
│
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
│ └──
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
├── database/ # Drizzle ORM 数据库相关。表结构变更(DDL)用 `lark-cli apps +db-execute` 执行,再 npm run gen:db-schema 根据数据库重新生成 schema.ts。
|
|
36
|
-
│ ├── schema.ts # Drizzle ORM 数据库 Schema 定义。必须从该文件导入数据库类型,禁止自行编写或修改 — 由 npm run gen:db-schema 根据数据库重新生成。
|
|
37
|
-
└── common/ # 共享工具和接口
|
|
38
|
-
│ ├── filters/ # 通用错误处理。
|
|
39
|
-
│ ├── constants/ # 通用常量。
|
|
40
|
-
│ └── utils/ # 通用工具方法。
|
|
41
|
-
shared/ # 前后端共享的目录
|
|
42
|
-
│ └── api.interface.ts # 前后端共享的类型定义
|
|
22
|
+
server/
|
|
23
|
+
├── main.ts # 入口,内容不可改动
|
|
24
|
+
├── app.module.ts # 根模块。新 module 必须在此注册,且放在 ViewModule 之前(ViewModule 是 fallback 路由,必须在最后)
|
|
25
|
+
├── modules/
|
|
26
|
+
│ ├── view/ # 平台模板渲染模块,禁止修改
|
|
27
|
+
│ └── hello/ # 示例模块(controller / module / service),可参考
|
|
28
|
+
├── database/ # 表结构变更(DDL)用 `lark-cli apps +db-execute` 执行,再 npm run gen:db-schema 重新生成 schema.ts
|
|
29
|
+
│ └── schema.ts # 表类型只能从这里导入,禁止手写或修改 — 由 npm run gen:db-schema 根据数据库重新生成
|
|
30
|
+
└── common/ # 通用 filters / constants / interfaces,通用工具方法(utils)也放这里
|
|
31
|
+
shared/
|
|
32
|
+
└── api.interface.ts # 前后端共享类型
|
|
43
33
|
```
|
|
44
34
|
|
|
45
35
|
## 前端结构 (`client/`)
|
|
46
36
|
|
|
47
37
|
```
|
|
48
38
|
client/
|
|
49
|
-
├── index.html
|
|
50
|
-
├── public/
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
│
|
|
60
|
-
│
|
|
61
|
-
│
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
│ │ │ ├── README.md # shadcn/ui 组件库使用说明 [请务必参考该文档进行组件使用]
|
|
67
|
-
│ │ ├── business-ui/ # 封装好的用户,部门选择组件以及展示组件
|
|
68
|
-
│ │ │ ├── README.md # 用户展示,用户头像展示,部门选择展示逻辑必须使用 [请务必参考该文档进行组件使用],禁止直接使用avatar组件等方式自行实现
|
|
69
|
-
│ │ ├── Layout.tsx # 布局包装器
|
|
70
|
-
│ ├── hooks/ # 自定义 React hooks
|
|
71
|
-
│ └── utils/ # 工具函数
|
|
72
|
-
│ │ └── config.ts # 应用初始化执行的配置
|
|
73
|
-
shared/ # 前后端共享的目录
|
|
74
|
-
│ └── api.interface.ts # 前后端共享的类型定义
|
|
39
|
+
├── index.html # HTML 模板
|
|
40
|
+
├── public/ # 静态资源
|
|
41
|
+
└── src/
|
|
42
|
+
├── index.tsx # 入口,内容不可改动
|
|
43
|
+
├── index.css # 全局样式
|
|
44
|
+
├── tailwind-theme.css # tailwind 主题变量
|
|
45
|
+
├── app.tsx # 路由定义
|
|
46
|
+
├── api/index.ts # 前端请求出口。按单文件还是按模块拆分自行决定;已有内容先读再改
|
|
47
|
+
├── pages/ # 页面
|
|
48
|
+
├── components/
|
|
49
|
+
│ ├── ui/ # shadcn/ui,使用前读 ui/README.md(Use the components for functionality, but heavily style them.)
|
|
50
|
+
│ ├── business-ui/ # 用户/部门选择与展示,必须使用(禁止用 avatar 等自行实现),先读 README.md
|
|
51
|
+
│ └── Layout.tsx # 布局
|
|
52
|
+
├── hooks/
|
|
53
|
+
└── utils/
|
|
54
|
+
shared/
|
|
55
|
+
└── api.interface.ts # 前后端共享类型
|
|
75
56
|
```
|
|
76
57
|
|
|
77
58
|
---
|
|
@@ -84,7 +65,6 @@ shared/ # 前后端共享的目录
|
|
|
84
65
|
- **路径别名**:`@client/` → `client/`;`@server/` → `server/`;`@shared/` → `shared/`。优先使用别名
|
|
85
66
|
- **环境配置**:不支持配置文件,写在代码中即可
|
|
86
67
|
- **命名约定**:Class → PascalCase,函数/变量 → camelCase,常量 → UPPER_CASE
|
|
87
|
-
- **跨子组件共享常量加语义前缀**:父级桶导出聚合多子组件时,子组件中相同语义的常量必须前缀化(如 `USER_DEFAULTS` / `ORDER_DEFAULTS`),禁止跨子组件复用裸通用名(`DEFAULT_CONFIG` / `STYLES` / `OPTIONS`),避免 barrel re-export 触发 `no-redeclare`
|
|
88
68
|
- **类型约束**:
|
|
89
69
|
- 使用 interface 定义类型,type 用于复杂类型。禁止 `any`
|
|
90
70
|
- 禁止自定义与 TypeScript 内置工具类型同名的类型(如 `Record`, `Omit`)
|
|
@@ -122,8 +102,8 @@ shared/ # 前后端共享的目录
|
|
|
122
102
|
|
|
123
103
|
- **代码行长度**:单行不超过 80-120 字符,import 成员多时及时换行
|
|
124
104
|
- **嵌套层级**:不超过 4 层,用 early return 减少嵌套
|
|
125
|
-
-
|
|
126
|
-
-
|
|
105
|
+
- **单文件规模**:避免上帝文件,文件过大时按功能拆分,拆分方式自行决定
|
|
106
|
+
- **避免循环引用**:文件之间不要形成循环 import(如组件文件与其拆出的工具/处理函数文件互相引用),否则 Vite React Refresh 会失效甚至白屏;共享逻辑抽到单向依赖的独立文件
|
|
127
107
|
- **将风格问题视为编码错误**:项目使用 `@eslint/js` + `typescript-eslint` 推荐配置
|
|
128
108
|
- **ESLint 规范**(`@eslint/js` + `typescript-eslint`):
|
|
129
109
|
- 正则控制字符用 unicode 标志:`/\x1b\[/u`
|
|
@@ -143,7 +123,7 @@ shared/ # 前后端共享的目录
|
|
|
143
123
|
- **组件**:PascalCase(`HelloWorld.tsx`)
|
|
144
124
|
- **模块**:kebab-case 文件夹,PascalCase 文件(`hello/hello.controller.ts`)
|
|
145
125
|
- **配置**:dot.case(`app.config.ts`)
|
|
146
|
-
-
|
|
126
|
+
- **导入导出**:跨目录引用优先用路径别名;同一符号不要同时行内 `export` 和底部 `export { }` 双重导出
|
|
147
127
|
|
|
148
128
|
## 开发环境与内置服务
|
|
149
129
|
|
|
@@ -154,32 +134,45 @@ shared/ # 前后端共享的目录
|
|
|
154
134
|
|
|
155
135
|
### 平台 runtime 接口(`__runtime__`)在本地要靠 env pull 才通
|
|
156
136
|
|
|
157
|
-
|
|
137
|
+
**判据**:看 `.env.local` 里有没有 `MIAODA_DEV_PLATFORM_BASE`(或 legacy `SANDBOX_PUBLIC_URL`)。有,这类接口就是通的;没有,一律打不通,先重跑 `env pull`,不要改代码。
|
|
138
|
+
|
|
139
|
+
**症状**(反代没挂上时):POST 返回 404;GET 落到 Vite 的 SPA fallback,返回 200 + `index.html`,业务侧表现为拿到 HTML 而非 JSON、解析失败。
|
|
140
|
+
|
|
141
|
+
**为什么**:`__runtime__` 的实现在平台服务上,不在你的应用里,靠平台网关转发过去——这层转发是隐式的,应用代码里看不到。你本地访问的是 `http://localhost:<port>`,这条链路上本来没有网关,dev server 自己只代理 `/api`、`/openapi`、`/__innerapi__`。
|
|
158
142
|
|
|
159
|
-
|
|
143
|
+
`npm run dev:local` 把这条转发**显式接回去**:`lark-cli apps +env-pull` 拉到的 `MIAODA_DEV_PLATFORM_BASE` 让 dev-proxy 把发到 localhost 的 `__runtime__` 请求反代到沙箱公网入口,那个入口同样要过平台网关,所以接回去之后本地行为与用户访问一致,与应用是否发布无关。**env pull 失败、未装 lark-cli、或 `.spark/meta.json` 缺 app_id 时启动脚本只 warn 不中断**,反代没挂上,这条转发就断了。
|
|
160
144
|
|
|
161
145
|
常踩到的端点:
|
|
162
146
|
|
|
163
147
|
| 端点 | 方法 | 谁会打到它 |
|
|
164
148
|
|------|------|-----------|
|
|
165
|
-
| `api/v1/permissions/roles` |
|
|
166
|
-
| `api/v1/observability/{logs,traces,metrics}/collect
|
|
149
|
+
| `api/v1/permissions/roles` | POST | `AppContainer` 每次加载应用自动打 |
|
|
150
|
+
| `api/v1/observability/{logs,traces,metrics}/collect` | POST | 平台埋点上报,自动打 |
|
|
151
|
+
| `api/v1/observability/current_server_timestamp` | GET | 平台埋点上报,自动打 |
|
|
167
152
|
| `api/v1/account/login/user` | POST | `useCurrentUserProfile()` → `authClient.session.getUserInfo()` |
|
|
168
|
-
| `api/v1/account/search_user`、`list_users`、`
|
|
153
|
+
| `api/v1/account/search_user`、`list_users`、`search_department`、`convert_lark_user` | POST | `business-ui` 的 UserSelect / DepartmentSelect |
|
|
154
|
+
| `api/v1/account/user_profile` | GET | `business-ui` 的 UserDisplay |
|
|
169
155
|
| `api/v1/account/search`、`api/v1/account/chat/list_chats` | POST | ChatSelect 选群组件 |
|
|
170
|
-
| `api/v1/studio/user/profile` |
|
|
156
|
+
| `api/v1/studio/user/profile` | POST | `getUserProfile()` |
|
|
171
157
|
| `api/v1/storage/object/<bucket>/pre_upload` | POST | `dataloom.storage.uploadFile()` 前端上传 |
|
|
172
158
|
| `api/v1/studio/plugins/tmp_files/acquire_upload_url`、`acquire_download_url` | POST | `capabilityClient` 的文件类入参 |
|
|
173
159
|
|
|
174
160
|
> `useCurrentUserProfile()` 打的是 `account/login/user`,**不是** `user_profile`/`search_user` 那一排;它另外还打一个 `GET /api/authnpaas/lark-user-id`,那条走 `/api`、被代理、正常。
|
|
175
161
|
|
|
176
|
-
|
|
162
|
+
`env pull` 确实拉不到时——
|
|
163
|
+
|
|
164
|
+
**不要做**:
|
|
165
|
+
|
|
166
|
+
- 改 `client/src/components/business-ui/**`(平台保护文件)
|
|
167
|
+
- 给组件加兜底请求、改用自研选人控件绕过——请求经过平台网关时这些端点是正常的,换掉会让应用少掉本来可用的功能
|
|
168
|
+
- 据此判断该能力在用户侧不可用
|
|
169
|
+
|
|
170
|
+
**改为做**:
|
|
177
171
|
|
|
178
|
-
-
|
|
179
|
-
-
|
|
180
|
-
- 需要验证上传链路:改用接口测试直接打后端接口,或复用库里已有的文件 URL
|
|
172
|
+
- 验证「按当前用户过滤」「展示用户姓名」这类逻辑:改由服务端出数据。`req.userContext` 和 `AuthNPaasService` 在 NestJS 侧可用,不受此限制(见 `user-identity` / `contacts-service` skill)。`useCurrentUserProfile()` 长期停在加载态就是这个原因
|
|
173
|
+
- 验证上传链路:用接口测试打你自己写的后端接口,或复用库里已有的文件 URL
|
|
181
174
|
|
|
182
|
-
另外,`UserSelect`
|
|
175
|
+
另外,`UserSelect` 弹层是**搜索驱动**的:不输关键词时列表为空属预期行为。
|
|
183
176
|
|
|
184
177
|
### 服务端启动即崩、`/api` 全部 502
|
|
185
178
|
|
|
@@ -373,7 +366,6 @@ await db.select().from(users).where(eq(users.adminUser, userId));
|
|
|
373
366
|
4. **前后端一致性**(CRITICAL):
|
|
374
367
|
- 前端 API 调用的 HTTP method 和 path **必须**与后端 Controller 装饰器完全匹配(`GET≠POST` → 405)
|
|
375
368
|
- 后端 Service 返回值结构**必须**与 `shared/api.interface.ts` 中定义的响应类型完全匹配(禁止后端直接返回数组而 shared 定义为 `{items: T[]}`)
|
|
376
|
-
- 编写前端 API 时必须先读取对应 Controller 确认 method 和路径
|
|
377
369
|
5. **路由注册验证**:创建新 Controller 后必须用接口测试工具验证路由是否生效。**遇到 404 排查路径**:① 检查 `@Controller(...)` 是否以 `api/` 或 `openapi/` 开头 ② 检查 Module 是否在 `app.module.ts` 注册 ③ 检查静态路由是否在动态路由 `/:id` 之前
|
|
378
370
|
6. **@Query/@Param 类型转换**:默认 string,必须在 controller 层手动转换(如 `parseInt(limit, 10)`)
|
|
379
371
|
7. **写操作加 @NeedLogin**:POST/PUT/PATCH/DELETE 接口显式加 `@NeedLogin()` 装饰器:
|
|
@@ -533,7 +525,7 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
|
|
|
533
525
|
// ❌ axiosForBackend.get(...) / .post(...) / .put(...) / .delete(...) ← 不是 axios 实例,所有实例方法都会 TypeError
|
|
534
526
|
```
|
|
535
527
|
|
|
536
|
-
-
|
|
528
|
+
- **前后端联调**:禁止对后端接口请求做兜底和过度封装
|
|
537
529
|
- **全栈项目禁止 mock 数据**:只要有服务端接口就当全栈对待,严禁 mock。纯前端项目(静态展示、无后端)可以使用 mock
|
|
538
530
|
- **提交前检查 mock 残留**:禁止硬编码用户 ID、项目 ID 等 mock 数据。提交前确认无 `"user_xxx"`、`"project_1"` 等占位值
|
|
539
531
|
|
|
@@ -545,7 +537,6 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
|
|
|
545
537
|
|
|
546
538
|
## 工程规范
|
|
547
539
|
|
|
548
|
-
- **glob 文件查找**:目录存在嵌套,glob 前端页面时必须使用 `client/src/pages/**`
|
|
549
540
|
- **先看后写**:修改文件前先理解已有代码风格,模仿现有 pattern 和库的使用方式
|
|
550
541
|
|
|
551
542
|
## 官方内置组件
|