@lark-apaas/coding-steering 0.1.14 → 0.1.15

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 (26) hide show
  1. package/package.json +1 -1
  2. package/steering/nestjs-react-fullstack/skills/anycross-forward/SKILL.md +180 -0
  3. package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +124 -0
  4. package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +3 -0
  5. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +11 -2
  6. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +0 -6
  7. package/steering/nestjs-react-fullstack/skills/authz-guide/references/management-page-spec.md +1 -4
  8. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +5 -3
  9. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +17 -4
  10. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +4 -31
  11. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +226 -116
  12. package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +319 -0
  13. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +666 -0
  14. package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +222 -0
  15. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +0 -1
  16. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +87 -36
  17. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +1 -4
  18. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +5 -6
  19. package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +5 -4
  20. package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +0 -1
  21. package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +3 -2
  22. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +86 -87
  23. package/steering/nestjs-react-fullstack/skills_local/openapi-guide/SKILL.md +262 -0
  24. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +580 -0
  25. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/references/plugin-coding-guide.md +322 -0
  26. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/scripts/plugin-hydrate.js +152 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/coding-steering",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Stack-specific steering content for miaoda-coding templates",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,180 @@
1
+ ---
2
+ name: anycross-forward
3
+ description: "Use when generating client-side code that calls an internal / intranet HTTP API through the platform's built-in `/api/__platform__/http-forward` endpoint. 触发词:anycross, http-forward, http forwarder, 内网转发, 内网接口, 内网 API, 请求内网, 请求内网接口, 调用内网接口, 转发到内网, 请求内部接口"
4
+ steering: true
5
+ steering-topic: anycross_forward
6
+ steering-inclusion: manual
7
+ match-template-name: nestjs-react-fullstack
8
+ available-agents:
9
+ - Code
10
+ - Worker
11
+ - DevOps
12
+ - Reviewer
13
+ unavailable-agents:
14
+ - AppInit
15
+ ---
16
+
17
+ # anycross-forward 内网请求转发指南
18
+
19
+ 通过平台内置 `/api/__platform__/http-forward` 端点,从前端把请求转发到客户内网地址。后端 SDK 通过 `PlatformModule.forRoot()` **自动装配**,业务零接入;前端用 `axiosForBackend` 调用即可。
20
+
21
+ > SDK 不感知代理 / 鉴权 / 隧道。实际通路由部署环境(沙箱 mihomo + iptables)处理,生成代码里**不要**拼 `proxy_group_id` / `jwtToken` / `anycross-host`。
22
+
23
+ ## 一、决策树
24
+
25
+ ```text
26
+ 需要从前端发请求?
27
+
28
+ ├─ 目标是平台内置 capability / Dataloom?
29
+ │ └─ 是 ──→ 用对应 SDK,不走 forwarder
30
+
31
+ ├─ 目标是公网公共 API(github.com 等)?
32
+ │ └─ 是 ──→ 直接 fetch / axios(不走 forwarder)
33
+
34
+ ├─ 目标是客户内网 HTTP/HTTPS API?
35
+ │ └─ 是 ──→ ✅ 用 forwarder:axiosForBackend + `/api/__platform__/http-forward`
36
+
37
+ └─ 目标是 ws:// / file:// / 二进制流 / FormData?
38
+ └─ 是 ──→ ❌ 当前版本不支持,回退方案见"限制"小节
39
+ ```
40
+
41
+ ## 二、Quick reference
42
+
43
+ | 项 | 值 |
44
+ | ------------ | ---------------------------------------------------------------------------- |
45
+ | 端点路径 | `ALL /api/__platform__/http-forward` |
46
+ | Query 参数 | `targetUrl` (必填,仅 `http://` / `https://`) |
47
+ | 前端 SDK | `axiosForBackend` from `@lark-apaas/client-toolkit/utils/getAxiosForBackend` |
48
+ | HTTP method | 透传:GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS |
49
+ | Body 形态 | string / JSON-able object(axios 自动序列化) / null |
50
+ | Headers | 自动透传(剔除 hop-by-hop / accept-encoding / host / content-length) |
51
+ | 响应 | 上游 `status` / `headers` / `data` 原样投射 |
52
+ | 默认超时 | 30000 ms(可在 `PlatformModule.forRoot({ httpForwarder })` 改) |
53
+ | 默认响应上限 | 10485760 bytes (10 MB) |
54
+ | 鉴权 | SDK **不内置**;业务方在路由上加 `@NeedLogin()` 等 guard |
55
+
56
+ ## 三、标准前端代码模板
57
+
58
+ ```ts
59
+ // client/src/api/corp-api.ts
60
+ import { axiosForBackend } from "@lark-apaas/client-toolkit/utils/getAxiosForBackend";
61
+
62
+ const FORWARD = "/api/__platform__/http-forward";
63
+ const BASE = "http://api.corp.com/v1";
64
+
65
+ // GET — 拉用户列表
66
+ export async function fetchUsers() {
67
+ const res = await axiosForBackend.get(FORWARD, {
68
+ params: { targetUrl: `${BASE}/users` },
69
+ });
70
+ return res.data; // 上游真实响应 data
71
+ }
72
+
73
+ // POST — 创建订单,body 直接传对象
74
+ export async function createOrder(payload: { qty: number }) {
75
+ const res = await axiosForBackend.post(FORWARD, payload, {
76
+ params: { targetUrl: `${BASE}/orders` },
77
+ headers: { "X-Tenant": "demo" }, // 自定义 headers 自动透传上游
78
+ });
79
+ return res.data;
80
+ }
81
+
82
+ // DELETE — 路径参数拼在 targetUrl 上
83
+ export async function deleteOrder(id: string) {
84
+ await axiosForBackend.delete(FORWARD, {
85
+ params: { targetUrl: `${BASE}/orders/${id}` },
86
+ });
87
+ }
88
+ ```
89
+
90
+ ## 四、错误处理模板
91
+
92
+ > ⚠️ 模板的 `GlobalExceptionFilter`(`server/common/filters/exception.filter.ts`)会把 forwarder 抛的 `HttpException` 重新打包为 `{ error: { code: 'BAD_REQUEST' | 'BAD_GATEWAY' | ..., message, details } }`:顶层 `error.code` 是 HTTP 状态映射,**forwarder 自己的业务码(`INVALID_REQUEST` / `UPSTREAM_TIMEOUT` 等)被 `JSON.stringify` 进 `error.details`**,需要 `JSON.parse` 才能拿到。
93
+
94
+ ```ts
95
+ import { axiosForBackend } from "@lark-apaas/client-toolkit/utils/getAxiosForBackend";
96
+ import type { AxiosError } from "axios";
97
+
98
+ type ForwarderInner = { code?: string; message?: string };
99
+ type ApiError = {
100
+ error?: { code?: string; message?: string; details?: string };
101
+ };
102
+
103
+ function parseForwarder(err: unknown): ForwarderInner | undefined {
104
+ const details = (err as AxiosError<ApiError>)?.response?.data?.error?.details;
105
+ if (typeof details !== "string") return undefined;
106
+ try {
107
+ return JSON.parse(details) as ForwarderInner;
108
+ } catch {
109
+ return undefined;
110
+ }
111
+ }
112
+
113
+ export async function safeForward(targetUrl: string) {
114
+ try {
115
+ const res = await axiosForBackend.get("/api/__platform__/http-forward", {
116
+ params: { targetUrl },
117
+ });
118
+ // 上游 4xx/5xx 也走这里:res.status 是上游真实 status
119
+ return { ok: true, status: res.status, data: res.data };
120
+ } catch (err) {
121
+ // 只有 forwarder 自身错才走 catch
122
+ const inner = parseForwarder(err);
123
+ switch (inner?.code) {
124
+ case "INVALID_REQUEST":
125
+ case "INVALID_TARGET_PROTOCOL":
126
+ // targetUrl 缺失 / 格式非法 / 协议非 http(s)
127
+ return { ok: false, reason: "targetUrl 配置错误" };
128
+ case "UPSTREAM_TIMEOUT":
129
+ return { ok: false, reason: "上游 30s 超时" };
130
+ case "UPSTREAM_UNREACHABLE":
131
+ // DNS 解析失败 / 连接拒绝 / 网络层不可达;
132
+ // inner.message 形如 "upstream unreachable (ENOTFOUND)",括号里是 cause code
133
+ return { ok: false, reason: inner.message ?? "上游不可达" };
134
+ case "RESPONSE_TOO_LARGE":
135
+ return { ok: false, reason: "上游响应 > 10MB" };
136
+ default:
137
+ throw err;
138
+ }
139
+ }
140
+ }
141
+ ```
142
+
143
+ ## 五、生成代码必须遵守的 6 条
144
+
145
+ 1. **路径恒为 `/api/__platform__/http-forward`**,HTTP method 来自调用方,service 透传给上游。
146
+ 2. **`targetUrl` 永远放 `params`**,不要拼到 URL 字符串里、不要放 body、不要放 header。
147
+ 3. **服务端零接入**——`PlatformModule.forRoot()` 已自动装配 `HttpForwarderModule`,**不要**在用户 `AppModule` 里再写 `HttpForwarderModule.forRoot()`。
148
+ 4. **前端只用 `axiosForBackend`**,它已带 401 → `x-login-url` 自动跳转拦截器,**不要**再实现登录跳转。
149
+ 5. **响应原样投射**:`res.status` / `res.headers` / `res.data` 都来自上游真实响应。forwarder 自身错被模板 `GlobalExceptionFilter` 包成 `error.response.data.error`,业务码需从 `error.details`(JSON 字符串)里 parse 出来,详见第四节。
150
+ 6. **headers 直接走 `headers: {...}`**,无需手剔 `host` / `content-length` / hop-by-hop,service 端会处理。
151
+
152
+ ## 六、Common Mistakes
153
+
154
+ | ❌ 错误 | ✅ 正确 |
155
+ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
156
+ | `axiosForBackend.post('/api/__platform__/http-forward?targetUrl=...')` 手拼 query | `params: { targetUrl }`,让 axios 做 URL 编码 |
157
+ | `axiosForBackend.post(targetUrl, body)` 把 targetUrl 当路径 | 路径永远 `/api/__platform__/http-forward`,targetUrl 走 `params` |
158
+ | 用裸 `axios.get('/api/__platform__/http-forward', ...)`(直接 import 'axios') | 用 `axiosForBackend`(带 baseURL / 401 拦截器) |
159
+ | body 传 `FormData` / `Blob` / 流 | v0.1.0 仅支持 string / JSON-able;二进制走 base64 字符串 |
160
+ | 上游 4xx/5xx 被当作转发失败 | 上游响应**原样投射**到 `res`;只有 forwarder 自身错才进 catch,业务码在 `error.response.data.error.details`(需 `JSON.parse`) |
161
+ | 用户 `AppModule` 手写 `HttpForwarderModule.forRoot()` | 已被 `PlatformModule` 自动装配,重复注册冲突 |
162
+ | 拼 `?targetUrl=...&proxy_group_id=xxx&jwt=yyy` 等自定义参数 | 部署环境接管路由,SDK 不感知;唯一参数是 `targetUrl` |
163
+ | `targetUrl: 'ws://...'` / `'file://...'` | 仅 `http://` / `https://`,否则 `INVALID_TARGET_PROTOCOL` |
164
+ | 在生成代码里手动加 `Authorization` 模拟 anycross | 部署环境处理鉴权,业务代码只关心**业务**header |
165
+ | 假设响应可流式读取 | 当前版本响应统一 utf-8 string,不流式 |
166
+
167
+ ## 七、限制(v0.1.0)
168
+
169
+ - **Body**:仅 `string` / JSON-able object / `null`;不支持 binary / FormData / 流
170
+ - **响应**:统一 utf-8 string;不支持流式 / 二进制下载
171
+ - **不做请求重试**(透传语义,重试由业务方决定)
172
+ - **超时 / 响应上限可配**:业务方可在 `PlatformModule.forRoot({ httpForwarder: { requestTimeoutMs, maxResponseBytes } })` 处覆盖默认值
173
+ - **错误码 contract**:`INVALID_REQUEST` / `INVALID_TARGET_PROTOCOL` / `UPSTREAM_UNREACHABLE` / `UPSTREAM_TIMEOUT` / `RESPONSE_TOO_LARGE`
174
+
175
+ ## 八、相关
176
+
177
+ - 后端 SDK 实现:`@lark-apaas/nestjs-http-forwarder`
178
+ - 前端 SDK:`axiosForBackend` from `@lark-apaas/client-toolkit/utils/getAxiosForBackend`
179
+ - 自动装配点:`@lark-apaas/nestjs-core` 的 `PlatformModule`
180
+ - 鉴权约束:参见 `user-identity`——forwarder 不内置鉴权,路由级 `@NeedLogin()` 仍生效
@@ -0,0 +1,124 @@
1
+ ---
2
+ name: app-init-feasibility-guide
3
+ description: "Use when AppInit or SpecDoc needs a lightweight WHAT-level feasibility and capability-boundary guide for nestjs-react-fullstack apps: feasibility judgment, platform capability boundaries, architecture mode choice, data/integration assumptions, and spec handoff signals. 触发词:AppInit, SpecDoc, 初始化判断, 需求可行性, 能力边界, 轻量可行性指南, WHAT规范, 应用初始化, 规格文档"
4
+ steering: true
5
+ steering-inclusion: always
6
+ steering-topic: tech
7
+ match-template-name: nestjs-react-fullstack
8
+ available-agents:
9
+ - AppInit
10
+ - SpecDoc
11
+ ---
12
+
13
+ # AppInit / SpecDoc 可行性与规格指南
14
+
15
+ 本 skill 只提供应用初始化和规格编写所需的 WHAT 级判断信息:需求能否做、应选哪类能力、哪些地方要澄清。不要在 AppInit 或 SpecDoc 阶段加载完整 `coding-guide` 或展开实现细节。
16
+
17
+ ## Quick Reference
18
+
19
+ | 判断项 | 结论 |
20
+ |--------|------|
21
+ | 企业管理、流程、台账、数据看板、内容工具 | 适合全栈应用 |
22
+ | 静态展示、活动页、无后端状态 | 可做纯前端 |
23
+ | 需要持久化业务数据、权限、通知、统计 | 选全栈 |
24
+ | 飞书集成、AI 能力(生文/生图/识别) | 通过平台插件接入 |
25
+ | 用户身份、角色权限、文件存储 | 平台内置能力 |
26
+ | 实时音视频、长连接、WebSocket/SSE | 不适合,需改交替方案 |
27
+ | 原生客户端、浏览器插件、系统级后台服务 | 超出模板能力边界 |
28
+
29
+ ## Feasibility Decision
30
+
31
+ ```text
32
+ 用户需求
33
+
34
+ ├─ 是企业应用/业务流程/数据管理/看板/内容工具?
35
+ │ └─ 是:通常可实现
36
+
37
+ ├─ 仅需页面展示或前端交互,无持久化业务状态?
38
+ │ └─ 是:纯前端即可
39
+
40
+ ├─ 需要数据持久化、用户隔离、权限、后端集成或定时任务?
41
+ │ └─ 是:需要全栈
42
+
43
+ ├─ 依赖实时双向通信、音视频、游戏级同步或长连接?
44
+ │ └─ 是:标记为不适合;建议改为轮询、异步任务或非实时替代
45
+
46
+ └─ 依赖外部系统但接口、凭证、数据源不明确?
47
+ └─ 需要澄清或在规格中标记假设
48
+ ```
49
+
50
+ ## Platform Capabilities
51
+
52
+ | 能力域 | 可用于规划的范围 |
53
+ |--------|------------------|
54
+ | 前端应用 | 页面、路由、表单、表格、图表、文件预览、交互组件 |
55
+ | 后端服务 | HTTP JSON API:CRUD、聚合统计、业务规则、外部 API 编排 |
56
+ | 数据库 | 应用自有业务数据的持久化、关系建模、查询与统计 |
57
+ | 用户身份 | 登录、当前用户、用户信息展示 |
58
+ | 权限 | 角色可见性、操作权限点位 |
59
+ | 文件 | 前端上传至平台文件服务,后端保存元信息 |
60
+ | 插件集成 | 飞书多维表格、消息、群组、AI 能力等通过插件接入 |
61
+ | 自动化 | 定时任务、数据变更触发、webhook、审批触发 |
62
+ | OpenAPI | 对外 HTTP 接口,需明确鉴权与数据边界 |
63
+ | 内网接口 | 客户内网 HTTP 接口转发,需用户提供目标接口与访问条件 |
64
+
65
+ ## Capability Boundaries
66
+
67
+ | 不适合直接承诺 | 可替代方向 |
68
+ |---------------|------------|
69
+ | WebSocket、SSE、持续流式响应 | 短轮询、任务状态查询、一次性返回 |
70
+ | 实时多人编辑、毫秒级协同 | 弱实时刷新、提交后同步 |
71
+ | 原生 App、桌面端、浏览器插件 | Web 应用、响应式页面 |
72
+ | 服务端持久写本地文件 | 平台文件服务、数据库、临时 `/tmp` |
73
+ | 无凭证调用受限第三方系统 | 要求用户提供 API、凭证或授权方式 |
74
+ | 自建账号体系绕过平台登录 | 使用平台内置身份与权限 |
75
+ | 多语言 i18n、深浅色主题切换(非平台内置) | 需自行实现并计入工作量;规格中先确认是否必要 |
76
+
77
+ ## Architecture Choice
78
+
79
+ | 需求特征 | 建议模式 |
80
+ |----------|----------|
81
+ | 无持久化状态、纯展示或前端工具 | 纯前端 |
82
+ | 需要数据存储、用户隔离、后端逻辑 | 全栈 |
83
+ | 涉及文件上传与管理 | 全栈 + 文件服务 |
84
+ | 以飞书多维表格为数据源 | 插件优先;复杂查询场景评估是否同步至本地库 |
85
+ | 需要飞书通知、群组、AI 能力 | 插件集成,标记插件实例需求 |
86
+ | 需要定时或事件驱动的后台处理 | 全栈 + 自动化任务 |
87
+
88
+ ## Data Source Choice
89
+
90
+ | 数据来源 | 规划原则 |
91
+ |----------|----------|
92
+ | 应用自身创建和维护的数据 | 本地数据库 |
93
+ | 飞书多维表格为既有事实源 | 通过多维表格插件读写 |
94
+ | 多维表格上的统计看板 | 优先使用插件聚合能力,避免全量拉取后计算 |
95
+ | 需要跨表关联、全文搜索、高频读写 | 规划同步至本地数据库后查询 |
96
+ | 外部系统 API | 明确接口、鉴权、网络可达性与失败策略 |
97
+
98
+ ## AppInit Checklist
99
+
100
+ - 判断需求可行性(可行 / 部分可行 / 不建议),给出一句原因。
101
+ - 明确应用模式:纯前端或全栈。
102
+ - 识别需求中缺失的关键信息:数据源、权限角色、外部依赖。
103
+ - 对不支持的能力给出可替代方向,不包装平台边界。
104
+ - 只做 WHAT 级判断,不输出文件结构、技术选型、代码片段。
105
+
106
+ ## SpecDoc Checklist
107
+
108
+ - 页面清单:每页目标、主要操作与状态。
109
+ - 数据对象:实体、关键字段、筛选与统计口径。
110
+ - 角色权限:查看、创建、编辑、删除、导出、配置等操作的分配。
111
+ - 集成点:飞书、多维表格、AI、内网接口、OpenAPI、自动化触发。
112
+ - 边界与限制:不支持的能力、外部依赖、数据权限边界。
113
+ - 交给 Code agent 时标明需要加载的专项 skill(表格、表单、图表、插件、权限、文件、自动化)。
114
+
115
+ ## Common Mistakes
116
+
117
+ | 错误 | 正确做法 |
118
+ |------|----------|
119
+ | AppInit 阶段读取完整 `coding-guide` | 使用本轻量规范即可 |
120
+ | 把 HOW 细节写进需求判断 | 只判断可行性、模式、边界和风险 |
121
+ | 对长连接、原生端、实时协作直接承诺 | 标记不适合并给替代方向 |
122
+ | 数据源不清时默认建表或默认接多维表格 | 先澄清事实源,或在规格中写明假设 |
123
+ | 插件能力只写"可以调用" | 标明需要插件实例、授权或用户确认的数据源 |
124
+ | 忽略权限和身份 | 规格中至少列出角色与关键操作权限 |
@@ -98,6 +98,9 @@ export class ApiController {
98
98
  | 用 `@Public()` 标记公开接口 | `@Public()` 是 no-op;公开接口不需要任何装饰器(opt-in 模式默认放行未标 `@NeedLogin()` 的接口) |
99
99
  | 在未标 `@NeedLogin()` 的接口里依赖 `req.userContext.userId` 做业务判断 | 该接口默认放行未登录请求,`userId` 可能为 `undefined`,必须先判空 |
100
100
  | 业务层自行返回 401 实现"未登录跳转" | 用 `@NeedLogin()` 让守卫统一处理,确保 `x-login-url` 头被正确写入 |
101
+ | 自建认证传输:自签 JWT 走自定义 `Authorization` / `X-Auth-Token` 头、自定义 cookie、query token | 这些头在妙搭网关转发时**不透传**(白名单只放 `x-suda-csrf-token` / `x-larkgw-suda-webuser` + 标准头),后端必拿不到 → 401,且易被误诊为网关故障。任何自有账号体系/自分配账号需求,身份一律取 `req.userContext`(网关注入,解析自 `x-larkgw-suda-webuser`)、用 `@NeedLogin()` 认证 + 业务角色表授权,不要自签 JWT 走自定义传输 |
102
+
103
+ > ⚠️ **"去飞书化 / 自分配账号"需求专项**:哪怕用户要"完全自己的账号体系",登录态仍走平台 `x-larkgw-suda-webuser` 身份,自建的只是"业务角色/资料表"。自签 token 走任何自定义传输通道都会被网关白名单拦掉(必踩 401)。
101
104
 
102
105
  ---
103
106
 
@@ -24,6 +24,8 @@ match-template-name: nestjs-react-fullstack
24
24
 
25
25
  > **⛔ 点位全覆盖**:编写鉴权代码时,必须对照权限设计方案的**功能权限表**,将每个权限点位逐一落实到对应的后端 API(`@CanRole` / `@Can`)和前端入口(`<CanRole>` / `<Can>`),完成后逐行核对确认无遗漏。
26
26
 
27
+ > **⛔ 角色来源一律是平台,禁止另搞一套平台不认的权限系统**:多级审批、按城市/部门分权、多角色组合等复杂授权,角色必须是**平台真实角色**(先 `role create` / `role list`,标识与代码对齐)。**消费平台角色的方式不限**——`@CanRole`/`@Can` 是语法糖;在 service 里读 `req.userContext.roles` 再 `roles.includes('admin')` 同样是走平台机制(roles 来自平台),按场景选用即可,**不强制每个 API 都用 `@CanRole`**(注解 cover 不了的细粒度场景就读 `userContext.roles` 自行判断)。**真正要禁止的是绕开平台另起炉灶**:自建任何平行的角色/权限存储(自建角色表 / 角色字段 / 用户名单,不限具体命名)、引用平台上不存在(没 create 过)的角色标识——这些 dev 看似能跑、线上必失效。复杂度用「**平台角色(组合)+ 数据层行级过滤**」表达——例如"审核人只审本城市"=`reviewer` 角色把门 + service 按 `cityBranchId` 过滤;"多级审批"=每级一个平台角色 + 把"当前在第几级"存成业务数据状态。
28
+
27
29
  ### DESIGN 前置步骤
28
30
 
29
31
  "开启权限服务"/"设计权限体系"/"规划角色"/"升级鉴权模式"/"开启角色服务" → **必须先调用 `rbac_role_manager` tool 的 DESIGN action**,用户确认后再实施。日常"给某功能加权限"不触发 DESIGN。
@@ -42,6 +44,8 @@ const response = await axiosForBackend(config);
42
44
  if (response.status === 403) throw new Error('无操作权限,请联系管理员分配角色');
43
45
  ```
44
46
 
47
+ (403 落 catch 会被吞成通用错误提示如'操作失败请稍后重试',掩盖真实无权限信号,导致排查方向跑偏)
48
+
45
49
  ### 平台的角色面板入口
46
50
 
47
51
  **只允许在对话中给其入口链接,严禁写到代码中**:`[角色面板](BaseURL?openPanel=auth)` 或 `[角色面板](?openPanel=auth)`
@@ -52,10 +56,11 @@ if (response.status === 403) throw new Error('无操作权限,请联系管理
52
56
 
53
57
  ### 核心原则
54
58
 
55
- 1. 系统已内置角色权限表,无需建表
59
+ 1. 系统已内置角色权限表,**无需也禁止自建任何平行的角色/权限存储**(自建角色表 / 角色字段 / 用户名单等,不限具体命名)——@CanRole 只读平台角色,应用自建的存储对鉴权无效
56
60
  2. 统一使用 `useAuth()` 获取 `{ ability, isLoading }`,**禁止** `useAuthAbility` / `useCanRole`(已移除)
57
61
  3. 必须处理 `isLoading`——加载期间 `ability.can()` 返回 false,不检查会误判无权限
58
62
  4. 创建角色后必须编写鉴权代码,切忌只创建角色不编写代码
63
+ 5. **`@CanRole([X])`/`<CanRole roles={[X]}>` 里的 X 必须等于平台实际角色标识(bizID),不能凭语义臆造**——创建角色时用 `miaoda-auth-cli role create --biz-id <X>` 把标识固定成代码要用的值,或先 `miaoda-auth-cli role list` 取实际标识再写代码。若代码写 `'admin'`/`'reviewer'` 但平台分配给用户的角色标识是 `role_xxx`,两者对不上 → 线上恒 403(日志可见 `用户角色 [role_xxx], 需要 [admin, reviewer]`)
59
64
 
60
65
  ### 前端
61
66
 
@@ -110,7 +115,6 @@ import { CanRole } from '@lark-apaas/fullstack-nestjs-core';
110
115
  | SDK 调用示例 | [sdk-examples.md](references/sdk-examples.md) |
111
116
 
112
117
  **关键约束**:
113
-
114
118
  - 成员按类型分组传递(`MemberMutationData`),不是扁平数组
115
119
  - `allEmployees`/`public` 只读,包含「企业全员」或「互联网公开」的角色不支持删除
116
120
  - `userID` 可不传,默认为当前登录用户
@@ -131,6 +135,7 @@ import { CanRole } from '@lark-apaas/fullstack-nestjs-core';
131
135
 
132
136
  **必须严格遵循 [dynamic-permission-guide.md](references/dynamic-permission-guide.md) 实施,从 Step 0 开始逐步执行。** 禁止跳步或自由发挥。
133
137
 
138
+
134
139
  ---
135
140
 
136
141
  ## 四、禁止行为清单
@@ -145,6 +150,9 @@ import { CanRole } from '@lark-apaas/fullstack-nestjs-core';
145
150
  | 绕过 AuthorizationSDK 自行封装接口 | 必须通过 SDK 操作运行时角色和权限点位 |
146
151
  | 升级权限体系时未调用 DESIGN 就编码 | 先调用 DESIGN 产出方案,确认后再动手 |
147
152
  | 在业务组件中单独处理 403 | API 层统一拦截 403 |
153
+ | 自建任意平行的角色/权限存储(自建角色表 / 角色字段 / 用户名单,不限具体命名)来做鉴权判断 | @CanRole 只认平台角色,应用自建的存储不会被鉴权读取;角色一律用平台真实角色 |
154
+ | 复杂授权就**另起炉灶搞一套平台不认的权限**(自建角色表 / 引用平台没 create 过的标识 / 写死 user_id 名单) | 角色一律用平台真实角色;消费方式不限(`@CanRole` 或读 `req.userContext.roles` 都行),复杂度用「平台角色 + 数据层行级过滤」表达 |
155
+ | `@CanRole([X])` 的 X 凭语义臆造、与平台角色标识(bizID)不一致 | X 必须 = 平台角色 bizID;`role create --biz-id` 固定标识或 `role list` 取实际标识再写代码 |
148
156
  | 在 `.catch()` 或 catch 块中处理 403 | 403 不触发异常,必须在 response 层面检查 `status` |
149
157
 
150
158
  ---
@@ -154,6 +162,7 @@ import { CanRole } from '@lark-apaas/fullstack-nestjs-core';
154
162
  | 问题 | 处理方式 |
155
163
  |------|---------|
156
164
  | 403 错误 | 明确告知是无权限报错,确认是否符合预期;用 `miaoda-auth-cli` MOCK 模拟角色调试 |
165
+ | 线上 403 但 dev 正常 / 用户"已授权"仍 403 | 先核对两件事:(1) 角色面板授给用户的角色**标识(bizID)** 是否等于代码 `@CanRole` 里的字符串(`miaoda-auth-cli role list` 取实际标识比对);(2) 是否另搞了一套平台不认的权限(自建角色表 / 引用平台没 create 过的标识 / user_id 名单)绕开平台。两者任一不符即解释 403 |
157
166
  | 开发环境授权不生效 | 引导用户使用 `miaoda-auth-cli` MOCK 功能 |
158
167
  | 用户要求管理角色 | 开发态:`[角色面板](BaseURL?openPanel=auth)`;运行态:按第二节实施 |
159
168
 
@@ -25,7 +25,6 @@
25
25
  > **前端无需手动配置权限点位获取**:auth-sdk 的 `AuthProvider` 会自动请求内置端点获取当前用户的权限点位,业务侧不需要传任何额外配置。
26
26
 
27
27
  **关键约束**:
28
-
29
28
  - 权限点位(`authz_permissions` 表)的增删改**由 Agent 通过 DDL 操作**,确保与代码中的 `@Can`/`<Can>` 保持一致
30
29
  - 管理页面**只负责角色-点位映射**的勾选/取消,不提供权限点位的 CRUD
31
30
  - **⛔ 启用动态权限后禁止使用 CanRole**:所有鉴权(包括管理 API)统一使用 `@Can`/`<Can>`,不允许 CanRole 与 Can 混用
@@ -354,7 +353,6 @@ import { Can } from '@lark-apaas/fullstack-nestjs-core';
354
353
  ```
355
354
 
356
355
  **关键约束**:
357
-
358
356
  - **⛔ 动态权限模式下禁止 `@CanRole`**,所有鉴权(含管理 API)统一用 `@Can`
359
357
  - 未注入 `permissionResolver` 时使用 `@Can` 会抛出 500 错误
360
358
  - 多权限叠加装饰器:`@Can('read', 'Task') @Can('read', 'User')`
@@ -366,7 +364,6 @@ import { Can } from '@lark-apaas/fullstack-nestjs-core';
366
364
  ### 前端 API 函数
367
365
 
368
366
  在 `client/src/api/index.ts` 中添加**管理页面所需**的请求函数:
369
-
370
367
  - 权限点位查询 API(listPermissions)
371
368
  - 角色-权限映射 CRUD API(listRoleMappings / createRoleMapping / deleteRoleMapping)
372
369
 
@@ -563,7 +560,6 @@ function ConfigPermissionsDialog({ role, permissions, mappings, open, onOpenChan
563
560
  ```
564
561
 
565
562
  **布局规则**:
566
-
567
563
  - 按 `subject` 分组为可展开/收起的 Accordion 卡片,标题 `Subject中文名 (已开启/总数)`
568
564
  - 有已勾选权限的 Subject 默认展开,全未勾选的默认收起
569
565
  - 每行:`Checkbox + description + action:subject badge(置灰小字)`
@@ -598,11 +594,9 @@ SDK 内部统一将平台错误转为 `HttpException`,Controller 无需 try-ca
598
594
 
599
595
  1. 确认编译通过
600
596
  2. grep 确认 CanRole 零残留:
601
-
602
597
  ```bash
603
598
  grep -r "CanRole\|useCanRole\|ROLE_SUBJECT" --include="*.ts" --include="*.tsx" server/ client/ shared/
604
599
  ```
605
-
606
600
  ⛔ 结果必须为空,否则回到 Step 5 继续替换
607
601
  3. 逐项对照下方「实现检查表」
608
602
 
@@ -79,7 +79,6 @@ Step 5: 验证
79
79
  ### 操作列
80
80
 
81
81
  每行包含:
82
-
83
82
  - **编辑成员** 按钮(文字按钮)
84
83
  - **更多操作** 下拉菜单(`...` 按钮,DropdownMenu),包含:
85
84
  - 配置权限(仅动态权限点位模式下显示)
@@ -187,6 +186,7 @@ const members: MemberMutationData = {
187
186
  await addRoleMembers(bizID, { members });
188
187
  ```
189
188
 
189
+
190
190
  ### 4. 编辑角色信息
191
191
 
192
192
  Dialog 弹窗,编辑角色名称和描述。调用 `sdk.roles.update(bizID, { role: { name, description } })`。
@@ -194,7 +194,6 @@ Dialog 弹窗,编辑角色名称和描述。调用 `sdk.roles.update(bizID, {
194
194
  ### 5. 删除角色
195
195
 
196
196
  删除前必须:
197
-
198
197
  1. 检查 `role.roleMembers?.allEmployees` 或 `role.roleMembers?.public` 是否为 `true`
199
198
  2. 若为 `true`,**禁用删除按钮**并通过 tooltip 提示「包含企业全员/互联网公开的角色不支持删除」
200
199
  3. 若可删除,弹出二次确认对话框,确认后调用 `sdk.roles.delete(bizID)`
@@ -474,7 +473,6 @@ SDK 内部统一将平台错误转为 `HttpException`,Controller 无需 try-ca
474
473
  | 权限点位 | `permissions`(自定义渲染) | 250 | 展示该角色绑定的权限点位 |
475
474
 
476
475
  渲染规则:
477
-
478
476
  - 每个点位优先展示中文说明(`description`),`action:subject` 作为辅助置灰小字
479
477
  - 最多展示 3 个,超出部分用 `+N` Badge + HoverCard 展示剩余
480
478
  - 无绑定点位时显示 `--`
@@ -484,7 +482,6 @@ SDK 内部统一将平台错误转为 `HttpException`,Controller 无需 try-ca
484
482
  在「更多操作」`...` 下拉菜单中(与「编辑角色信息」「删除角色」同级)新增「配置权限」菜单项,**禁止外露为独立按钮**(与 § 操作列 的排版硬规则对齐)。
485
483
 
486
484
  点击后打开 Dialog,列出所有权限点位,每行一个 Checkbox + `action:subject` 标识 + 描述:
487
-
488
485
  - **勾选**:调用 `createRoleMapping({ roleKey, permissionId })`
489
486
  - **取消勾选**:找到映射记录,调用 `deleteRoleMapping(id)`
490
487
  - 操作即时生效,无需保存按钮
@@ -3,6 +3,8 @@
3
3
  > 完整类型定义见 [sdk-types.md](./sdk-types.md)
4
4
 
5
5
  ```typescript
6
+ import { logger } from "@lark-apaas/client-toolkit/logger";
7
+
6
8
  // ===================== 角色管理 =====================
7
9
 
8
10
  // 获取所有角色列表,needMember=true 会在每个角色中附带成员数据
@@ -79,12 +81,12 @@ const searchRes: SearchResponse = await sdk.search.search({
79
81
 
80
82
  // 响应按类型分组,分别遍历
81
83
  searchRes.result.userResult?.items?.forEach(u =>
82
- console.log('用户:', u.name, u.userID, u.avatar),
84
+ logger.info('用户:', u.name, u.userID, u.avatar),
83
85
  );
84
86
  searchRes.result.departmentResult?.items?.forEach(d =>
85
- console.log('部门:', d.name, d.departmentID),
87
+ logger.info('部门:', d.name, d.departmentID),
86
88
  );
87
89
  searchRes.result.chatResult?.items?.forEach(c =>
88
- console.log('群组:', c.name, c.chatID, '成员数:', c.userCount),
90
+ logger.info('群组:', c.name, c.chatID, '成员数:', c.userCount),
89
91
  );
90
92
  ```
@@ -1,6 +1,8 @@
1
1
  # AuthorizationSDK 类型定义
2
2
 
3
3
  > 所有类型均从 `@lark-apaas/fullstack-nestjs-core` 导入。
4
+ >
5
+ > **ID 字段语义**(哪个用于飞书 API、哪个禁用、employee_id/open_department_id/open_chat_id 含义)见 [`contacts-service`](../../contacts-service/SKILL.md) skill。下表 `larkUserID`/`larkDepartmentID` 为纯数字内部 ID(禁用),飞书 API 用 `employeeID`/`openDepartmentID`/`openChatID`。
4
6
 
5
7
  ```typescript
6
8
  // ===================== 通用 =====================
@@ -75,25 +77,30 @@ type MemberType =
75
77
  | 'AllEmployee';
76
78
 
77
79
  interface UserSimpleDTO {
78
- userID?: string; // 用户 ID
80
+ userID?: string; // 妙搭用户 ID(入库/插件用它)
79
81
  name?: I18nText;
80
82
  avatar?: string;
81
83
  email?: string;
82
84
  userType?: string;
83
- larkUserID?: number;
85
+ larkUserID?: number; // 飞书内部数字 ID,禁用(飞书 API 不认)
84
86
  department?: DepartmentSimpleDTO;
87
+ employeeID?: string; // 飞书企业内 user_id,调飞书开放平台 API 用它
88
+ larkID?: string; // 飞书用户全局唯一 ID(larkUserID 别名),禁用
89
+ miaodaUserID?: string; // 妙搭用户 ID(userID 别名)
85
90
  }
86
91
 
87
92
  interface DepartmentSimpleDTO {
88
93
  departmentID?: number;
89
- larkDepartmentID?: number;
94
+ larkDepartmentID?: number; // 纯数字内部 ID,禁用
90
95
  name?: I18nText; // SDK 归一化后统一为 { zh_cn, en_us } 格式
96
+ openDepartmentID?: string; // 飞书部门 open id(od- 开头),部门统一用它
91
97
  }
92
98
 
93
99
  /** 查询响应中的部门(SDK 已将平台的数组格式归一化为 I18nText) */
94
100
  interface DepartmentDTO {
95
101
  id?: string; // 部门 ID
96
102
  name?: I18nText; // 部门名称,SDK 归一化后统一为 { zh_cn, en_us } 格式
103
+ openDepartmentID?: string; // 飞书部门 open id(od- 开头);需后端确认成员部门是否返回
97
104
  }
98
105
 
99
106
  /** 变更入参中的部门(提交接口只需 id) */
@@ -106,6 +113,7 @@ interface ChatSimpleDTO {
106
113
  name?: I18nText;
107
114
  avatar?: string;
108
115
  isExternal?: boolean;
116
+ openChatID?: string; // 飞书群组 open id(oc_ 开头),群组统一用它
109
117
  }
110
118
 
111
119
  interface PresetGroupDTO {
@@ -179,19 +187,23 @@ interface SearchParams {
179
187
 
180
188
  interface SearchUserEntity {
181
189
  userID?: string;
182
- larkUserID?: number;
190
+ larkUserID?: number; // 纯数字内部 ID,禁用
183
191
  name?: I18nText;
184
192
  avatar?: string;
185
193
  department?: DepartmentSimpleDTO; // 用户所属部门(name 已归一化)
186
194
  userType?: string;
187
195
  email?: string;
188
196
  userStatus?: number;
197
+ employeeID?: string; // 飞书企业内 user_id,调飞书开放平台 API 用它
198
+ larkID?: string; // larkUserID 别名,禁用
199
+ miaodaUserID?: string; // userID 别名
189
200
  }
190
201
 
191
202
  interface DepartmentEntity {
192
203
  departmentID?: number;
193
204
  larkDepartmentID?: number;
194
205
  name?: I18nText; // SDK 归一化后统一为 { zh_cn, en_us } 格式
206
+ openDepartmentID?: string; // 飞书部门 open id(od- 开头)
195
207
  }
196
208
 
197
209
  interface SearchChatEntity {
@@ -200,6 +212,7 @@ interface SearchChatEntity {
200
212
  avatar?: string;
201
213
  isExternal?: boolean;
202
214
  userCount?: number;
215
+ openChatID?: string; // 飞书群组 open id(oc_ 开头)
203
216
  }
204
217
 
205
218
  interface SearchResult {