@lark-apaas/coding-steering 0.1.61-alpha.20261005173705 → 0.1.62
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 +6 -6
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +2 -2
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +49 -25
- package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +7 -7
- package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +5 -1
- package/steering/nestjs-react-fullstack/skills_common/user-identity/SKILL.md +17 -11
- package/steering/nestjs-react-fullstack/skills_local/authz-guide/references/sdk-types.md +2 -2
- package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +11 -1
package/package.json
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/coding-steering",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.62",
|
|
4
4
|
"description": "Stack-specific steering content for miaoda-coding templates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"steering"
|
|
8
8
|
],
|
|
9
|
-
"scripts": {
|
|
10
|
-
"lint:md": "markdownlint 'steering/**/*.md' --ignore 'steering/**/skills/**' --ignore 'steering/**/skills_common/**' --ignore 'steering/**/skills_local/**'"
|
|
11
|
-
},
|
|
12
9
|
"devDependencies": {
|
|
13
10
|
"markdownlint-cli": "^0.47.0"
|
|
14
11
|
},
|
|
@@ -20,5 +17,8 @@
|
|
|
20
17
|
"miaoda",
|
|
21
18
|
"coding-steering"
|
|
22
19
|
],
|
|
23
|
-
"license": "MIT"
|
|
24
|
-
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"lint:md": "markdownlint 'steering/**/*.md' --ignore 'steering/**/skills/**' --ignore 'steering/**/skills_common/**' --ignore 'steering/**/skills_local/**'"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -88,7 +88,7 @@ interface UserSimpleDTO {
|
|
|
88
88
|
larkUserID?: number; // 飞书内部数字 ID,禁用(飞书 API 不认)
|
|
89
89
|
department?: DepartmentSimpleDTO;
|
|
90
90
|
employeeID?: string; // 飞书企业内 user_id,调飞书开放平台 API 用它
|
|
91
|
-
larkID?: string; // 飞书用户全局唯一 ID(larkUserID
|
|
91
|
+
larkID?: string; // 飞书用户全局唯一 ID(larkUserID 别名),禁传飞书 API;转妙搭 userId 用 AuthNPaasService.getBatchMiaodaUserIdsByLarkIds
|
|
92
92
|
miaodaUserID?: string; // 妙搭用户 ID(userID 别名)
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -198,7 +198,7 @@ interface SearchUserEntity {
|
|
|
198
198
|
email?: string;
|
|
199
199
|
userStatus?: number;
|
|
200
200
|
employeeID?: string; // 飞书企业内 user_id,调飞书开放平台 API 用它
|
|
201
|
-
larkID?: string; // larkUserID
|
|
201
|
+
larkID?: string; // larkUserID 别名,禁传飞书 API(转妙搭 userId 见上)
|
|
202
202
|
miaodaUserID?: string; // userID 别名
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -20,7 +20,6 @@ unavailable-agents:
|
|
|
20
20
|
- `client/src/app.tsx`(加路由 / Provider 可改,但**禁止删除其中的平台根组件 / Provider 包裹**,禁止用 `eslint-disable` 关掉平台校验规则)
|
|
21
21
|
- `client/src/components/business-ui/*`
|
|
22
22
|
- `server/modules/view/view.controller.ts`
|
|
23
|
-
- `vite.config.ts`
|
|
24
23
|
|
|
25
24
|
**修改 / 排查上列文件时**:
|
|
26
25
|
|
|
@@ -124,7 +123,6 @@ shared/
|
|
|
124
123
|
- ✅ `@IsString()` + `@IsIn(['tile', 'operator'])` + `type!: 'tile' | 'operator'`
|
|
125
124
|
- **机械核对**:先数 shared alias 字面量个数 N,inline union 和 `@IsIn` 数组都必须恰好 N 个,且每个字面量完全一致;数量或名称不等就是错,不能只写“不得窄于”。
|
|
126
125
|
- 边界:shared 真实 `enum` 用 `@IsEnum(EnumName)`;嵌套 DTO 用 `@ValidateNested()` + `@Type(() => XxxDto)`;普通对象字段用 `@IsObject()`;Date 转换按 `@Type(() => Date)` 处理。禁止用 `@Type(() => String)` 只为绕过 lint。
|
|
127
|
-
- **关联实体 ID 合同**:字段确实表示另一实体的 ID 时,在 `shared/api.interface.ts` 的请求字段旁定义一份关系合同,不能只留下 `<field>Id: string`。合同写清 target entity、single/multi cardinality、required/optional 与 nullability、受支持的 list/search lookup、可读 label 与真实 ID value 的映射、visibility scope,以及 create、update 省略、非空值、显式 clear 的语义。普通 UUID 形状的不透明标识不因此变成关联字段。
|
|
128
126
|
|
|
129
127
|
## 代码质量约束
|
|
130
128
|
|
|
@@ -155,15 +153,15 @@ shared/
|
|
|
155
153
|
## 开发环境与内置服务
|
|
156
154
|
|
|
157
155
|
- **登录/注册/用户系统**:内置,禁止自行实现
|
|
158
|
-
- 项目依赖已安装完整,前后端 devServer
|
|
156
|
+
- 项目依赖已安装完整,前后端 devServer 由平台托管、进程退出后自动拉起,文件变更自动热重载;服务异常时按「dev 服务异常与重启」处理
|
|
159
157
|
- `/api` 和 `/openapi` 代理已配置,前端 TS 严格模式已禁用,后端已启用
|
|
160
|
-
- Rspack
|
|
158
|
+
- 构建工具为 Vite(`vite.config.ts`)或 Rspack(`rspack.config.js`),以项目根实际存在的配置文件为准,已配置好,需要时可以修改(见下方「构建配置可改」)
|
|
161
159
|
- **禁止修改的工程配置文件与目录**:
|
|
162
160
|
- `package.json`、`tsconfig.json`、`jest.config.js`、`.env*`
|
|
163
161
|
- `package-lock.json`、`pnpm-lock.yaml`、`yarn.lock`
|
|
164
|
-
- `webpack.config.js`、`vite.config.js`、`babel.config.js`
|
|
165
162
|
- `.eslintrc.js`、`.prettierrc`
|
|
166
163
|
- `node_modules/`、`.git/`、`dist/`、`build/`
|
|
164
|
+
- **构建配置可改**:`vite.config.ts`、`rspack.config.js`、`webpack.config.js`、`babel.config.js` 允许按需修改。改坏或出现构建 / 启动异常时,先用 `git log -p -- <文件>`、`git diff <commit> -- <文件>` 查看改动历史定位问题,再用 `git restore --source=<commit> -- <文件>` 还原到可用版本(最早一次提交即模板原版);不要凭记忆重写
|
|
167
165
|
|
|
168
166
|
### `__runtime__` 接口在 Agent 验证入口下不可达
|
|
169
167
|
|
|
@@ -226,14 +224,42 @@ shared/
|
|
|
226
224
|
- **测后端接口**:优先用接口测试工具,只传以 `/api` 或 `/openapi` 开头的业务路径;工具会自动拼 `CLIENT_BASE_PATH`、选择正确 dev 端口,并补 CSRF 与用户身份。
|
|
227
225
|
- **必须手写 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 中间件或给接口加白名单。
|
|
228
226
|
- **不要直连 NestJS 端口**:会绕过 dev server / 网关注入,`x-larkgw-suda-webuser` 缺失后 `userContext.userId` 为空,依赖身份的接口会误判。
|
|
229
|
-
-
|
|
227
|
+
- **判断服务是否启动**:用 `/dev/health`(见「dev 服务异常与重启」),或看 `client-devserver` / `server-devserver` 日志的 ready / 编译成功。裸端口访问返回 404 或连不上(只监听 IPv6 等)都不能当服务故障的依据;同一 URL 连续两次同状态码就换排查方向,不要 `sleep` + curl 重试。
|
|
228
|
+
- **裸 curl 页面路径不能判断页面是否正常**:页面 HTML 由 NestJS 渲染,dev server 只把 `Accept: text/html` 的请求转给它;curl 默认的 `*/*` 拿到 404 或 HTML 都不是故障依据。也不要拼入口模块路径来验证页面。
|
|
230
229
|
|
|
231
230
|
## 质量保障流程
|
|
232
231
|
|
|
233
232
|
通用提交前检查(代码检查 / 接口测试 / 读日志确认无错误)见系统任务流程约束;本栈具体落点:
|
|
234
233
|
|
|
235
234
|
- **日志源**:服务端读 `server-devserver` / `server` 日志,客户端读 `client-devserver` 日志
|
|
236
|
-
- **dev
|
|
235
|
+
- **dev 服务异常**:先判据再重启,见下节
|
|
236
|
+
|
|
237
|
+
### dev 服务异常与重启
|
|
238
|
+
|
|
239
|
+
`npm run dev` 由平台托管:进程退出后约 1s 自动重新拉起;改代码靠热重载,不需要重启。重启解决不了编译错误和代码 bug,只用于确认进程挂死的场景。
|
|
240
|
+
|
|
241
|
+
**先判据**:`curl -s localhost:$CLIENT_DEV_PORT/dev/health`(**不带** `$CLIENT_BASE_PATH`)。返回的不是 JSON(dev server 不支持该接口)就跳过这一步,直接做下面的快速判活
|
|
242
|
+
|
|
243
|
+
| 返回 | 含义 | 动作 |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| `backend_failed` | 服务端编译 / 启动失败 | 读 `server-devserver` 日志修代码,**重启无效** |
|
|
246
|
+
| `pending` | 正在启动 | 等 `server-devserver` 出现 `Server running on ...`,不要重启;持续超过约 30s 或日志里已有编译报错,直接读日志修代码,不要继续等 |
|
|
247
|
+
| `ready` | Vite 在监听且 NestJS 端口可连 | 不代表 NestJS 没卡死,接口仍异常时做下面的快速判活 |
|
|
248
|
+
|
|
249
|
+
**快速判活**:`curl -s -m 5 -o /dev/null -w '%{http_code}\n' localhost:$SERVER_PORT/`。返回任意 HTTP 状态码(含 404)= NestJS 在响应;`000` 且超时 = 挂死。只用于判活,测接口仍走接口测试工具;不要等接口测试工具超时(约 120s)再下结论。
|
|
250
|
+
|
|
251
|
+
**现象归属**:
|
|
252
|
+
|
|
253
|
+
- `/api/*` 返回 502,body 含 `Backend service is not running` / `temporarily unavailable`:dev server 连不上 NestJS。先读 `server-devserver` 日志,有报错就修代码(热重载会自行恢复)。刚改过服务端代码时,热重载期间短暂的 502 属正常,隔几秒用快速判活复测一次再下结论
|
|
254
|
+
- 接口无响应 / 超时(不是 502):NestJS 端口在但不处理请求,用快速判活确认后按挂死处理
|
|
255
|
+
- `browser` 日志或用户反馈里出现 `Resource load failed`:只说明某个资源没加载到,是结果不是原因,先按上面几条定位是哪一侧
|
|
256
|
+
|
|
257
|
+
**确认挂死才重启**(日志无报错、现象持续、`/dev/health` 不是 `backend_failed`):
|
|
258
|
+
|
|
259
|
+
- 只重启一侧:`pkill -f "npm run dev:server"` 或 `pkill -f "npm run dev:client"`,另一侧不受影响
|
|
260
|
+
- 两侧都异常:`pkill -f "npm run dev"`,整体由平台重新拉起
|
|
261
|
+
|
|
262
|
+
**重启后验证**:`/dev/health` 回到 `ready` → 接口测试工具打一个关键 `/api` → 截图工具看页面。同一现象重启一次后仍复现就不是挂死:停止重启,回到日志找根因。
|
|
237
263
|
|
|
238
264
|
---
|
|
239
265
|
|
|
@@ -259,7 +285,6 @@ shared/
|
|
|
259
285
|
- `client/src/app.tsx`:顶部追加 `import UsersPage from './pages/users';`,在 `<Routes>` 内加 `<Route index element={<UsersPage />} />`(或其他业务首页组件作为 index 路由),并加 `<Route path="users" element={<UsersPage />} />`
|
|
260
286
|
- `client/src/api/index.ts`:每行 `export * as <namespace> from './<name>';`(用命名空间导出避免跨模块 export 名冲突)
|
|
261
287
|
- 预填完成后才派发业务模块任务;聚合文件引用的符号(`UsersModule` / `UsersPage` / `./users`)在任务产出前暂时悬空,任务完成后自然对齐
|
|
262
|
-
- 派发含关联实体 ID 的业务模块时,把 shared 中同一关系合同完整写入前后端任务。前端仅在合同提供受支持 lookup 时复用已有 Select / Combobox / 业务选择器,显示 label、提交 value 中的真实 ID;required 且无选项时禁用提交并显示空态或依赖态,optional 可留空,显式清空按合同传值。没有受支持 lookup 时报告依赖,不得发明 API 或退回自由文本 ID 输入。
|
|
263
288
|
|
|
264
289
|
**业务模块任务的工作边界**:
|
|
265
290
|
|
|
@@ -430,27 +455,18 @@ await db.select().from(users).where(eq(users.adminUser, userId));
|
|
|
430
455
|
### PostgreSQL SQLSTATE / Drizzle 异常处理(CRITICAL)
|
|
431
456
|
|
|
432
457
|
- 可预期幂等 / 冲突优先用数据库原子语义(`ON CONFLICT`、`UPDATE ... WHERE ... RETURNING`),不要靠异常流兜底。
|
|
433
|
-
- 必须 catch 数据库异常时,**禁止只读顶层 `error.code
|
|
458
|
+
- 必须 catch 数据库异常时,**禁止只读顶层 `error.code`**:Drizzle 可能把原始 PostgreSQL error 放在 `cause` 链里。判断 `23505` 等 SQLSTATE 时用 cause-aware helper:
|
|
434
459
|
|
|
435
460
|
```typescript
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
function hasPostgresSqlState(error: unknown, expected: string): boolean {
|
|
439
|
-
if (!SQLSTATE_PATTERN.test(expected)) return false;
|
|
440
|
-
const seen = new Set<object>();
|
|
461
|
+
function extractPostgresErrorCode(error: unknown): string | undefined {
|
|
441
462
|
let current: unknown = error;
|
|
442
|
-
for (let depth = 0; depth <
|
|
443
|
-
if (seen.has(current)) return false;
|
|
444
|
-
seen.add(current);
|
|
463
|
+
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
|
|
445
464
|
const { code, cause } = current as { code?: unknown; cause?: unknown };
|
|
446
|
-
if (typeof code === "string"
|
|
465
|
+
if (typeof code === "string") return code;
|
|
447
466
|
current = cause;
|
|
448
467
|
}
|
|
449
|
-
return
|
|
468
|
+
return undefined;
|
|
450
469
|
}
|
|
451
|
-
|
|
452
|
-
hasPostgresSqlState({ code: "ERR_WRAPPER", cause: { code: "23505" } }, "23505"); // true
|
|
453
|
-
hasPostgresSqlState({ code: "42501", cause: { code: "23505" } }, "23505"); // true
|
|
454
470
|
```
|
|
455
471
|
|
|
456
472
|
- 只处理明确 SQLSTATE:`23505`(unique violation)可转业务态或 `ConflictException`;`23503` 外键、`42501` 权限 / RLS 按业务语义失败;未知错误继续抛出,禁止吞成成功。
|
|
@@ -458,8 +474,6 @@ await db.select().from(users).where(eq(users.adminUser, userId));
|
|
|
458
474
|
|
|
459
475
|
### 写操作与批量读(CRITICAL)
|
|
460
476
|
|
|
461
|
-
- **关联实体 ID 写边界**:create 或 update 明确提供非空关系值时,service 必须先校验每个值的 UUID 格式,再按合同的 visibility scope 查询 target entity 确实存在,之后才能 insert/update;格式非法或目标缺失返回清晰的 4xx。create 缺少 required 值也返回 4xx;update 省略字段表示保持不变,`null` 仅在合同允许时表示 clear,否则返回 4xx。前端选择器和模板对数据库错误的兜底映射都不能替代这层校验。
|
|
462
|
-
- 若产品要求关联在“校验后并发删除”时仍保持完整性,再设计 FK 或等价数据库约束及删除策略;这不是 malformed-text SQLSTATE `22P02` 的成因,也不要求所有关系一律新增 FK。
|
|
463
477
|
- **PATCH/UPDATE 更新契约**:`PATCH` 和增量 `update` 接口只写入请求 DTO 中明确提供的可选字段,禁止用 `dto.field ?? 默认值`、空数组、空字符串等默认值覆盖未提供字段;用户明确清空时由 DTO 传入清空语义值(如空数组、空字符串或 `null`)并按业务规则写入。业务字段发生更新且接口/页面展示最后修改时间或最后修改人时,不能依赖 `DEFAULT CURRENT_TIMESTAMP` 等建表默认值刷新 UPDATE;必须在同一次 Drizzle update 的 `set(patch)` 调用中维护 `schema.ts` 暴露的更新时间和更新人字段,更新人从 `req.userContext.userId` 传入 service,禁止从前端传递。
|
|
464
478
|
|
|
465
479
|
```typescript
|
|
@@ -739,6 +753,16 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
|
|
|
739
753
|
- SelectItem 的 value 禁止空值,占位用 `<SelectValue placeholder="..." />`
|
|
740
754
|
- 版本锁定、按需导入、唯一图标库 lucide-react
|
|
741
755
|
|
|
756
|
+
### 无障碍:控件名字与状态
|
|
757
|
+
|
|
758
|
+
读屏和自动化验收都靠控件的名字和状态找控件、核对结果,样式和图标它们看不见。表单字段见 `forms-skill`,其余控件按下面写:
|
|
759
|
+
|
|
760
|
+
- 纯图标按钮必须加 `aria-label`,写清动作:`<Button size="icon" aria-label="返回"><ArrowLeft /></Button>`
|
|
761
|
+
- 能点的东西用 `Button` 或 `Link`,不要给 `div`、`Card`、表格行加 `onClick`;整行可点的列表把行里的主标题做成 `Link`,或给行加 `aria-label`
|
|
762
|
+
- 可选中、可切换的卡片用 `RadioGroup`、`ToggleGroup` 或 `Checkbox` 表达选中态;自己写时加 `role="radio"` + `aria-checked` 或 `aria-pressed`。不要只靠边框、颜色、阴影表达选中
|
|
763
|
+
- 图片 `alt` 要能区分同类项(`` alt={`海报方案 ${index + 1}`} ``),纯装饰图写 `alt=""`
|
|
764
|
+
- 列表项里的勾选框加 `aria-label`,带上这一项的名字:`` aria-label={`选择 ${file.name}`} ``
|
|
765
|
+
|
|
742
766
|
## @lark-apaas/client-toolkit
|
|
743
767
|
|
|
744
768
|
**零假设原则**:绝不基于假设使用任何子模块。使用任何 `@lark-apaas/client-toolkit` 子模块前**必须**:① 查询 Skills 或 `steering_doc_search` → ② 等待响应获取文档 → ③ 严格按文档编码。**禁止直接调用 `@lark-apaas/client-toolkit` 任何函数/方法**(不基于查询到的文档)。该库是**前端/浏览器侧 SDK**,仅可在 `client/**`、React 组件、hooks、前端工具函数中使用;**禁止在 `server/**`、NestJS controller/service/module、Node.js runtime、migration、script 中 import 或调用**。
|
|
@@ -872,7 +896,7 @@ return <h1>{data?.title || '未知标题'}</h1>;
|
|
|
872
896
|
| 验证 | zod |
|
|
873
897
|
| 工具函数 | lodash |
|
|
874
898
|
| 样式 | clsx |
|
|
875
|
-
| 用户反馈 | sonner |
|
|
899
|
+
| 用户反馈 | sonner(只用于提交结果、操作反馈;字段校验错误用 FormMessage 显示在字段下方,不用 toast) |
|
|
876
900
|
|
|
877
901
|
|
|
878
902
|
## 滚动分页最佳实践
|
|
@@ -60,11 +60,11 @@ match-template-name: nestjs-react-fullstack
|
|
|
60
60
|
|
|
61
61
|
| 兼容字段 | 标准替代 | 为什么禁用 |
|
|
62
62
|
|---|---|---|
|
|
63
|
-
| **lark_id** | employee_id |
|
|
63
|
+
| **lark_id** | employee_id | 纯数字 LarkID(飞书全局 ID),飞书开放平台 API 无法识别,传入报错或返回空。**唯一例外**:要把 LarkID 转成妙搭 userId 时,用 `AuthNPaasService.getBatchMiaodaUserIdsByLarkIds`(见 `user-identity`) |
|
|
64
64
|
| **lark_department_id** | open_department_id | 纯数字内部 ID,飞书 API 只接受 od- 格式 |
|
|
65
65
|
| **lark_chat_id** | open_chat_id | 纯数字内部 ID,飞书 IM API 只接受 oc_ 格式 |
|
|
66
66
|
|
|
67
|
-
> 搜人返回里同时有 `lark_id` 和 `employee_id`
|
|
67
|
+
> 搜人返回里同时有 `lark_id` 和 `employee_id` 时,调飞书 API **必须取 `employee_id`**;用户提到 lark_id 时先问用途:调飞书 API → 纠正为 employee_id;要转妙搭 userId(或反向)→ 按 `user-identity` 用 `AuthNPaasService` 的 LarkID 方法。
|
|
68
68
|
|
|
69
69
|
### 1.5 废弃名称映射
|
|
70
70
|
|
|
@@ -139,7 +139,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
139
139
|
|
|
140
140
|
### ID 转换接口
|
|
141
141
|
|
|
142
|
-
外部用户 / 取 open_id·union_id
|
|
142
|
+
外部用户 / 取 open_id·union_id:模板内用 `AuthNPaasService.getBatchOpenIds` / `getBatchUnionIds`(反向 `getBatchMiaodaUserIdsByOpenIds` / `ByUnionIds`,见 `user-identity`);模板外才用 [`feishu` › id-convert.md](../../../feishu/references/id-convert.md)。
|
|
143
143
|
|
|
144
144
|
---
|
|
145
145
|
|
|
@@ -160,7 +160,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
160
160
|
|
|
161
161
|
| 字段 | 获取方式 |
|
|
162
162
|
|---|---|
|
|
163
|
-
| open_id / union_id |
|
|
163
|
+
| open_id / union_id | 模板内 `AuthNPaasService.getBatchOpenIds` / `getBatchUnionIds`(`feishuClientId` 传 open_id 所属飞书应用 App ID,见 `user-identity`);模板外 id_convert + 开放平台应用凭证 |
|
|
164
164
|
|
|
165
165
|
### Level 4 — 搜人拿不到的字段(需开放平台通讯录 API + 字段权限)
|
|
166
166
|
|
|
@@ -197,7 +197,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
197
197
|
|---|---|
|
|
198
198
|
| 工号 / 手机号 / 职位 / 性别 / 直属上级 | 搜人带 **needFullFields=true**(Level 2,无需开平,follow admin 权限) |
|
|
199
199
|
| 在职状态 / 入职时间 / 自定义字段 | 引导开平应用 → 通讯录 API(场景 B,Level 4) |
|
|
200
|
-
| open_id / union_id |
|
|
200
|
+
| open_id / union_id | 模板内 `AuthNPaasService`(Level 3),无需引导配置开平应用 |
|
|
201
201
|
| 部门信息 | 直接取 open_department_id |
|
|
202
202
|
| 发消息给某人 | 取 miaoda_user_id 传插件 |
|
|
203
203
|
| 批量同步部门/全员、定时同步 | 引导配置应用 → 通讯录 API(场景 A) |
|
|
@@ -276,7 +276,7 @@ export class TicketService {
|
|
|
276
276
|
---
|
|
277
277
|
|
|
278
278
|
## 七、禁止行为
|
|
279
|
-
1.
|
|
279
|
+
1. **禁用兼容字段**:调飞书 API / 插件不得用 lark_id / lark_department_id / lark_chat_id(LarkID ↔ 妙搭 userId 互转除外,见 `user-identity`)
|
|
280
280
|
2. **禁止混用废弃名**:不得用 userID / suda_user_id / larkUserId 等旧名
|
|
281
281
|
3. **禁止给飞书内置插件传 employee_id**:插件入参必须是 miaoda_user_id
|
|
282
282
|
4. **禁止让用户手动做 ID 转换**:Agent 自动判断并执行
|
|
@@ -290,7 +290,7 @@ export class TicketService {
|
|
|
290
290
|
|---|---|
|
|
291
291
|
| 用户说"获取 user_id" | 追问:妙搭 miaoda_user_id 还是飞书 employee_id? |
|
|
292
292
|
| 用户说要 `lark_user_id` / "飞书用户ID" / "飞书 ID" | 多数指**可用的飞书 user_id = `employee_id`**。搜人/选择器结果里**没有**叫 lark_user_id 的字段、可用的就是 `employee_id`,**别返回禁用的纯数字 `lark_id`**;当前登录用户场景取 `useCurrentUserProfile().lark_user_id`(== employee_id)。歧义时确认"是调飞书 API 用吗",是则一律 employee_id |
|
|
293
|
-
| 用户用了 lark_id |
|
|
293
|
+
| 用户用了 lark_id | 若是传飞书 API:纠正"lark_id 飞书 API 不认,请用 employee_id(搜人已默认返回)";若是要转成妙搭 userId:用 `AuthNPaasService.getBatchMiaodaUserIdsByLarkIds`(见 `user-identity`) |
|
|
294
294
|
| 用户用了 lark_department_id | 纠正:"请用 open_department_id(od- 格式)" |
|
|
295
295
|
| 用户用了 lark_chat_id | 纠正:"请用 open_chat_id(oc_ 格式)" |
|
|
296
296
|
| 外部用户请求 employee_id | 说明外部用户无 employee_id,建议 open_id(需 id_convert) |
|
|
@@ -4,13 +4,17 @@ description: 自动化任务触发器配置与代码开发指南,支持 cron
|
|
|
4
4
|
steering: true
|
|
5
5
|
steering-topic: trigger_guide
|
|
6
6
|
match-template-name: nestjs-react-fullstack
|
|
7
|
+
gate-tools:
|
|
8
|
+
- tool: automation_trigger_manager
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
## 自动化任务配置与代码编写指引
|
|
10
12
|
|
|
11
13
|
### 自动化任务配置
|
|
12
14
|
|
|
13
|
-
1.
|
|
15
|
+
1. `CREATE` 创建的触发器默认是 `inactive`,创建成功不代表任务已经运行。
|
|
16
|
+
2. 用户明确要求任务运行,且代码、发布、权限与接收端均已就绪时,必须对同一个触发器执行 `ENABLE`,随后执行 `GET` 并确认 `Active=true`;未读回成功不得宣告任务已运行或交付完成。
|
|
17
|
+
3. 用户只要求准备任务或运行意图不明确时,保持 `inactive`,并明确告知用户任务尚未运行。
|
|
14
18
|
|
|
15
19
|
### 目录结构
|
|
16
20
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: user-identity
|
|
3
|
-
description: "当前登录用户「我是谁」+ 妙搭 ↔ 飞书 ID 转换。Use when getting current user info/profile, displaying user name/avatar/email, converting miaoda userId ↔ lark_user_id (both directions via AuthNPaasService ID-conversion methods), or reading req.userContext fields (userId/roles/tenantId): useCurrentUserProfile, FeishuID conversion. ⚠️ 按 userId 查他人姓名/头像请用 contacts-service 第六节。触发词:用户身份, 用户信息, 用户资料, 当前用户, userProfile, useCurrentUserProfile, 飞书ID, FeishuID, 飞书用户ID, lark_user_id, 用户ID转换, AuthNPaasService ID 转换, getCurrentUserLarkUserId, getBatchLarkUserIds, getBatchMiaodaUserIds, 飞书ID转妙搭, employee_id 转 userId, 用户上下文, userContext, userContext.roles, 用户角色, 当前用户角色, 获取请求者角色, 展示当前用户, 我是谁"
|
|
3
|
+
description: "当前登录用户「我是谁」+ 妙搭 ↔ 飞书 ID 转换。Use when getting current user info/profile, displaying user name/avatar/email, converting miaoda userId ↔ lark_user_id / LarkID / open_id / union_id (both directions via AuthNPaasService ID-conversion methods), or reading req.userContext fields (userId/roles/tenantId): useCurrentUserProfile, FeishuID conversion. ⚠️ 按 userId 查他人姓名/头像请用 contacts-service 第六节。触发词:用户身份, 用户信息, 用户资料, 当前用户, userProfile, useCurrentUserProfile, 飞书ID, FeishuID, 飞书用户ID, lark_user_id, 用户ID转换, AuthNPaasService ID 转换, getCurrentUserLarkUserId, getBatchLarkUserIds, getBatchMiaodaUserIds, 飞书ID转妙搭, employee_id 转 userId, LarkID, lark_id, larkID, LarkID转妙搭用户ID, lark_id 转 userId, getBatchLarkIds, getBatchMiaodaUserIdsByLarkIds, open_id 转妙搭, union_id 转妙搭, 妙搭ID转open_id, getBatchOpenIds, getBatchUnionIds, getBatchMiaodaUserIdsByOpenIds, getBatchMiaodaUserIdsByUnionIds, 用户上下文, userContext, userContext.roles, 用户角色, 当前用户角色, 获取请求者角色, 展示当前用户, 我是谁"
|
|
4
4
|
steering: true
|
|
5
5
|
steering-topic: user_identity
|
|
6
6
|
match-template-name: nestjs-react-fullstack
|
|
@@ -18,7 +18,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
18
18
|
|
|
19
19
|
## 零、ID 体系警告(CRITICAL)
|
|
20
20
|
|
|
21
|
-
⚠️ "user_id"
|
|
21
|
+
⚠️ "user_id" 这个字面在本项目里指**多个不同的东西**,必须先区分清楚再写代码:
|
|
22
22
|
|
|
23
23
|
**作为身份 ID 字段名(指代具体某个用户的 ID 值):**
|
|
24
24
|
|
|
@@ -26,6 +26,7 @@ match-template-name: nestjs-react-fullstack
|
|
|
26
26
|
|---|---|---|
|
|
27
27
|
| `useCurrentUserProfile().user_id`、`req.userContext.userId` | 妙搭用户 ID(纯数字字符串) | 妙搭 |
|
|
28
28
|
| `useCurrentUserProfile().lark_user_id`、`AuthNPaasService.getCurrentUserLarkUserId()` 返回值 | 飞书 user_id(== `employee_id`,企业内身份,**无固定前缀**) | 飞书 |
|
|
29
|
+
| 搜人返回的 `lark_id`、SDK 类型的 `larkID`、用户说的"LarkID" | **LarkID**:飞书全局用户 ID(纯数字,如 `7311528002225963027`),≠ employee_id,飞书 OpenAPI 不认。⚠️ `lark_user_id` / `getBatchLarkUserIds` 名字带 LarkUserId,值却是 employee_id,**不是** LarkID | 飞书 |
|
|
29
30
|
|
|
30
31
|
**作为 API 参数的取值(不是一个 ID,而是告诉接口"我传入哪种类型的 ID"):**
|
|
31
32
|
|
|
@@ -78,17 +79,20 @@ match-template-name: nestjs-react-fullstack
|
|
|
78
79
|
│ └─ 是 ──→ AuthNPaasService.getBatchLarkUserIds()(第三节)
|
|
79
80
|
│
|
|
80
81
|
├─ 需要 飞书 open_id(`ou_` 开头)/ union_id(`on_` 开头)?
|
|
81
|
-
│ └─
|
|
82
|
-
│
|
|
82
|
+
│ └─ 后端 ──→ `getBatchOpenIds()` / `getBatchUnionIds()`(第三节,convertType 10/11),open_id 属于哪个飞书应用就传 `{ feishuClientId: '<该应用 App ID,cli_ 开头>' }`
|
|
83
|
+
│ (复用 `FeishuService` 源码里的 `FEISHU_APP_ID` **常量**,不是环境变量,别用 `process.env` / `ConfigService` 读;不传 = 妙搭应用视角,拿去调你自己应用的 OpenAPI 会不认)
|
|
83
84
|
│
|
|
84
85
|
├─ 需要把 飞书 open_id / union_id 反查成 妙搭 userId?
|
|
85
|
-
│ └─
|
|
86
|
+
│ └─ 后端 ──→ `getBatchMiaodaUserIdsByOpenIds()` / `getBatchMiaodaUserIdsByUnionIds()`(convertType 20/21),`feishuClientId` 同上;模板外才走 `feishu` skill 的 spark id_convert
|
|
86
87
|
│
|
|
87
88
|
├─ 需要把 飞书 user_id(employee_id)反查成 妙搭 userId?
|
|
88
89
|
│ └─ 后端 ──→ `AuthNPaasService.getBatchMiaodaUserIds()`(第三节,SDK 一步,convertType 31);非模板项目 ──→ `feishu` skill 两步兜底
|
|
89
90
|
│
|
|
91
|
+
├─ **LarkID**(`lark_id` / `larkID`,纯数字全局 ID)↔ 妙搭 userId?(不要用上面 employee_id 的方法)
|
|
92
|
+
│ └─ 后端 ──→ LarkID → 妙搭 userId 用 `getBatchMiaodaUserIdsByLarkIds()`,反向用 `getBatchLarkIds()`(第三节,convertType 41 / 40)
|
|
93
|
+
│
|
|
90
94
|
└─ 需要自定义飞书 ID 转换接口?
|
|
91
|
-
└─ 是 ──→ 注入 AuthNPaasService 编写 Controller
|
|
95
|
+
└─ 是 ──→ 注入 AuthNPaasService 编写 Controller(第四节,适用于所有转换方向)
|
|
92
96
|
```
|
|
93
97
|
|
|
94
98
|
> **相关技能**:接口认证/守卫/401 参见 `authn-guide`;登录/登出/获取用户信息等 Dataloom SDK 操作参见 `client-builtins-user-service`;**搜人 / 人员·部门·群组选择器返回的 ID 处理与字段获取**(employee_id / open_department_id、禁用字段、id_convert)参见 `contacts-service`;**"用户能做什么"**(角色/权限点位鉴权)参见 `authz-guide`。本 skill 只解决**"用户是谁"**(当前登录用户),其 `lark_user_id` == `employee_id`。
|
|
@@ -133,9 +137,9 @@ Controller 将校验后的 `roles` 显式传给 Service。`AuthorizationSDK.role
|
|
|
133
137
|
|
|
134
138
|
妙搭平台的用户 ID(`userId`)与飞书用户 ID 是两套独立体系。调用飞书 OpenAPI 时需要传入某种飞书侧 ID(`open_id` / `union_id` / `user_id` 任选其一,具体通过哪个参数指定取决于 API:消息 API 用 `receive_id_type`,多数其他 API 用 `user_id_type`,文档协作者用 `member_id_type`)。
|
|
135
139
|
|
|
136
|
-
`AuthNPaasService`
|
|
140
|
+
`AuthNPaasService` 支持飞书 ID 与妙搭 userId 双向互转:**飞书 user_id**(即 `employee_id`)正向 `getBatchLarkUserIds` / `getCurrentUserLarkUserId`、反向 `getBatchMiaodaUserIds`;**LarkID**(搜人返回里的 `lark_id`)正向 `getBatchLarkIds`、反向 `getBatchMiaodaUserIdsByLarkIds`;**open_id / union_id** 正向 `getBatchOpenIds` / `getBatchUnionIds`、反向 `getBatchMiaodaUserIdsByOpenIds` / `getBatchMiaodaUserIdsByUnionIds`。
|
|
137
141
|
|
|
138
|
-
>
|
|
142
|
+
> 模板内所有方向都走本 SDK(见下方方法表),**不要**再手写 spark `id_convert`(它要开放平台应用凭证 + 额外权限);只有不在 nestjs-react-fullstack 模板内的项目才参见 `feishu` skill 的 `references/id-convert.md`。
|
|
139
143
|
|
|
140
144
|
### 后端 API
|
|
141
145
|
|
|
@@ -168,6 +172,8 @@ export class MyService {
|
|
|
168
172
|
| `getCurrentUserLarkUserId` | `() → Promise<string \| null>` | 从请求上下文获取当前用户的飞书 ID |
|
|
169
173
|
| `getBatchLarkUserIds` | `(userIds: string[]) → Promise<(string \| null)[]>` | 批量转换,最多 100 个,与输入顺序一一对应 |
|
|
170
174
|
| `getBatchMiaodaUserIds` | `(employeeIds: string[]) → Promise<(string \| null)[]>` | 反向:批量把飞书 user_id(employee_id)转为妙搭 userId,最多 100 个,顺序一一对应,失败项 `null`(底层 convertType 31) |
|
|
175
|
+
| `getBatchLarkIds` / `getBatchMiaodaUserIdsByLarkIds` | `(ids: string[]) → Promise<(string \| null)[]>` | 妙搭 userId → LarkID / LarkID → 妙搭 userId,最多 100 个,顺序一一对应,失败项 `null`(底层 convertType 40 / 41);**不要**把 LarkID 传给 `getBatchMiaodaUserIds`(只收 employee_id) |
|
|
176
|
+
| `getBatchOpenIds` / `getBatchUnionIds` / `getBatchMiaodaUserIdsByOpenIds` / `getBatchMiaodaUserIdsByUnionIds` | `(ids: string[], options?: { feishuClientId?: string }) → Promise<(string \| null)[]>` | 妙搭 userId ↔ open_id / union_id(convertType 10 / 11 / 20 / 21),最多 100 个,顺序一一对应,失败项 `null`;`feishuClientId` = open_id 所属飞书应用的 App ID(`cli_`),不传用妙搭应用 |
|
|
171
177
|
|
|
172
178
|
### 内置接口
|
|
173
179
|
|
|
@@ -241,7 +247,7 @@ export class FeishuIdController {
|
|
|
241
247
|
}
|
|
242
248
|
```
|
|
243
249
|
|
|
244
|
-
>
|
|
250
|
+
> 其他方向同理,把 `getBatchLarkUserIds` 换成对应方法即可:employee_id → 妙搭 userId 用 `getBatchMiaodaUserIds`,LarkID → 妙搭 userId 用 `getBatchMiaodaUserIdsByLarkIds`,妙搭 userId → LarkID 用 `getBatchLarkIds`。
|
|
245
251
|
|
|
246
252
|
前端调用示例:
|
|
247
253
|
|
|
@@ -272,8 +278,8 @@ const { larkUserIds } = await request<{ larkUserIds: (string | null)[] }>({
|
|
|
272
278
|
| 用 `if (!userInfo)` 判断加载状态 | 初始值为空对象(truthy),必须用 `if (!userInfo?.user_id)` |
|
|
273
279
|
| 手动实例化 `AuthNPaasService` | 通过 NestJS 依赖注入获取:`constructor(private readonly authnService: AuthNPaasService)` |
|
|
274
280
|
| 单独注册 `AuthNPaasModule.forRoot()` | 已通过 `PlatformModule.forRoot()` 自动注册,无需手动导入 |
|
|
275
|
-
| 自行调用平台 `/v1/app/{appId}/account/user/convert`
|
|
276
|
-
|
|
|
281
|
+
| 自行调用平台 `/v1/app/{appId}/account/user/convert` 接口,或模板内手写 spark `id_convert` | 必须使用 `AuthNPaasService` 的方法,内置错误处理和可观测性。LarkID / open_id / union_id 方法 TS 报不存在时把 `@lark-apaas/fullstack-nestjs-core` 升到最新,仍没有就**报告阻塞**,不要加直接依赖绕过 |
|
|
282
|
+
| 批量转换方法传入超过 100 个 ID | 分批调用,每批最多 100 个 |
|
|
277
283
|
| 在前端自行封装飞书 ID 转换请求 | 使用 `useCurrentUserProfile()` 获取当前用户飞书 ID;批量转换通过后端接口 |
|
|
278
284
|
|
|
279
285
|
---
|
|
@@ -88,7 +88,7 @@ interface UserSimpleDTO {
|
|
|
88
88
|
larkUserID?: number; // 飞书内部数字 ID,禁用(飞书 API 不认)
|
|
89
89
|
department?: DepartmentSimpleDTO;
|
|
90
90
|
employeeID?: string; // 飞书企业内 user_id,调飞书开放平台 API 用它
|
|
91
|
-
larkID?: string; // 飞书用户全局唯一 ID(larkUserID
|
|
91
|
+
larkID?: string; // 飞书用户全局唯一 ID(larkUserID 别名),禁传飞书 API;转妙搭 userId 用 AuthNPaasService.getBatchMiaodaUserIdsByLarkIds
|
|
92
92
|
miaodaUserID?: string; // 妙搭用户 ID(userID 别名)
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -198,7 +198,7 @@ interface SearchUserEntity {
|
|
|
198
198
|
email?: string;
|
|
199
199
|
userStatus?: number;
|
|
200
200
|
employeeID?: string; // 飞书企业内 user_id,调飞书开放平台 API 用它
|
|
201
|
-
larkID?: string; // larkUserID
|
|
201
|
+
larkID?: string; // larkUserID 别名,禁传飞书 API(转妙搭 userId 见上)
|
|
202
202
|
miaodaUserID?: string; // userID 别名
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -567,6 +567,16 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
|
|
|
567
567
|
- SelectItem 的 value 禁止空值,占位用 `<SelectValue placeholder="..." />`
|
|
568
568
|
- 版本锁定、按需导入、唯一图标库 lucide-react
|
|
569
569
|
|
|
570
|
+
### 无障碍:控件名字与状态
|
|
571
|
+
|
|
572
|
+
读屏和自动化验收都靠控件的名字和状态找控件、核对结果,样式和图标它们看不见。表单字段见 `forms-skill`,其余控件按下面写:
|
|
573
|
+
|
|
574
|
+
- 纯图标按钮必须加 `aria-label`,写清动作:`<Button size="icon" aria-label="返回"><ArrowLeft /></Button>`
|
|
575
|
+
- 能点的东西用 `Button` 或 `Link`,不要给 `div`、`Card`、表格行加 `onClick`;整行可点的列表把行里的主标题做成 `Link`,或给行加 `aria-label`
|
|
576
|
+
- 可选中、可切换的卡片用 `RadioGroup`、`ToggleGroup` 或 `Checkbox` 表达选中态;自己写时加 `role="radio"` + `aria-checked` 或 `aria-pressed`。不要只靠边框、颜色、阴影表达选中
|
|
577
|
+
- 图片 `alt` 要能区分同类项(`` alt={`海报方案 ${index + 1}`} ``),纯装饰图写 `alt=""`
|
|
578
|
+
- 列表项里的勾选框加 `aria-label`,带上这一项的名字:`` aria-label={`选择 ${file.name}`} ``
|
|
579
|
+
|
|
570
580
|
## @lark-apaas/client-toolkit
|
|
571
581
|
|
|
572
582
|
**零假设原则**:绝不基于假设使用任何子模块。使用任何 `@lark-apaas/client-toolkit` 子模块前**必须**:① 先查询本地已加载的 Skills 或对应包文档 → ② 严格按文档编码。**禁止直接调用 `@lark-apaas/client-toolkit` 任何函数/方法**(不基于查询到的文档)。该库**仅可在前端代码中使用,禁止在后端代码中引用**。
|
|
@@ -690,7 +700,7 @@ return <h1>{data?.title || '未知标题'}</h1>;
|
|
|
690
700
|
| 验证 | zod |
|
|
691
701
|
| 工具函数 | lodash |
|
|
692
702
|
| 样式 | clsx |
|
|
693
|
-
| 用户反馈 | sonner
|
|
703
|
+
| 用户反馈 | sonner(只用于提交结果、操作反馈;字段校验错误用 FormMessage 显示在字段下方,不用 toast) |
|
|
694
704
|
|
|
695
705
|
|
|
696
706
|
## 滚动分页最佳实践
|