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

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 (21) hide show
  1. package/README.md +11 -2
  2. package/package.json +1 -1
  3. package/steering/design-stack/skills/.gitkeep +0 -0
  4. package/steering/nestjs-react-fullstack/skills/.gitkeep +0 -0
  5. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +174 -0
  6. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +621 -0
  7. package/steering/nestjs-react-fullstack/skills/authz-guide/references/management-page-spec.md +505 -0
  8. package/steering/nestjs-react-fullstack/skills/authz-guide/references/runtime-role-controller-spec.md +203 -0
  9. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +90 -0
  10. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +216 -0
  11. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +405 -0
  12. package/steering/nestjs-react-fullstack/skills/devops-guide/SKILL.md +119 -0
  13. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +582 -0
  14. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +357 -0
  15. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +513 -0
  16. package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +118 -0
  17. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +177 -0
  18. package/steering/nestjs-react-fullstack/skills/user-management-best-practices/SKILL.md +142 -0
  19. package/steering/nestjs-react-fullstack/{skills → skills_local}/code-fix/SKILL.md +5 -19
  20. package/steering/nestjs-react-fullstack/{skills → skills_local}/coding-guide/SKILL.md +26 -148
  21. package/steering/nestjs-react-fullstack/tech.md +21 -0
@@ -0,0 +1,177 @@
1
+ ---
2
+ name: server-builtins-file-storage-service
3
+ description: "Use when server-side code needs to upload or download files programmatically, such as file format conversion, automated report generation, or background tasks that produce files. NOT for frontend file operations. 触发词:服务端文件上传, 服务端文件下载, FileService, nestjs file, 后端文件处理, 文件格式转换, 生成文件上传"
4
+ steering: true
5
+ steering-topic: server_builtins_file_storage_service
6
+ match-template-name: nestjs-react-fullstack
7
+ ---
8
+
9
+ # NestJS File Service SDK
10
+
11
+ 在 NestJS 服务端代码中使用 `FileService` 进行文件上传、下载、删除和管理。
12
+
13
+ > **使用注意**: 仅用于服务端文件处理场景,非必要文件上传下载场景请使用前端 `client-builtins-file-storage-service` skill。
14
+ > CLI 调试场景请参考 `file-storage` skill。
15
+
16
+ ## 使用场景
17
+
18
+ | 场景 | 说明 |
19
+ |------|------|
20
+ | 服务端文件处理 | 下载文件到本地处理后重新上传,如图片格式转换、docx 格式转换等 |
21
+ | 自动化任务生成文件 | 定时任务或事件触发后,根据数据库/插件内容生成文件并上传,如定时生成 PDF 报告、图片海报等 |
22
+
23
+ ## Quick Reference
24
+
25
+ | 方法 | 说明 | 返回值 |
26
+ |------|------|--------|
27
+ | `upload(file, options?)` | 上传文件 | `FileMeta` |
28
+ | `download(path)` | 下载文件(≤50MB) | `FileDownloadBuilder` |
29
+ | `remove(filePaths)` | 删除文件 | `RemoveResponse` |
30
+ | `getFileMetadata(filePath)` | 获取文件元信息 | `FileMeta` |
31
+
32
+ ## 注入 FileService
33
+
34
+ ```typescript
35
+ import { FileService } from '@lark-apaas/fullstack-nestjs-core';
36
+
37
+ @Injectable()
38
+ export class MyService {
39
+ constructor(private readonly fileService: FileService) {}
40
+ }
41
+ ```
42
+
43
+ ## 公共类型
44
+
45
+ ```typescript
46
+ interface FileMeta {
47
+ id: string;
48
+ name: string;
49
+ filePath: string;
50
+ metadata: { contentLength: string; mimeType: string };
51
+ downloadURL: string;
52
+ createdAt: string;
53
+ updatedAt: string;
54
+ bucketID: string;
55
+ }
56
+ ```
57
+
58
+ ## 核心操作
59
+
60
+ ### 1. 上传文件
61
+
62
+ **入参:**
63
+
64
+ | 参数 | 类型 | 必需 | 说明 |
65
+ |------|------|------|------|
66
+ | `file` | `FileBody` | 是 | 文件内容(支持 `Buffer`、`ReadableStream`、`ArrayBuffer`、`Blob`、`string` 等) |
67
+ | `options` | `UploadOptions` | 否 | 上传选项 |
68
+
69
+ **UploadOptions:**
70
+
71
+ | 字段 | 类型 | 必需 | 说明 |
72
+ |------|------|------|------|
73
+ | `fileName` | `string` | 否 | 文件名称 |
74
+ | `contentType` | `string` | 否 | MIME 类型 |
75
+ | `cacheControl` | `string \| number` | 否 | 缓存控制 |
76
+ | `upsert` | `boolean` | 否 | 是否覆盖已有文件 |
77
+
78
+ **返回值:** `FileMeta`
79
+
80
+ **示例:**
81
+
82
+ ```typescript
83
+ // 基本上传
84
+ const result = await this.fileService.upload(buffer);
85
+
86
+ // 带选项上传
87
+ const result = await this.fileService.upload(fileBody, {
88
+ fileName: 'report.pdf',
89
+ contentType: 'application/pdf',
90
+ upsert: false,
91
+ });
92
+ ```
93
+
94
+ ### 2. 下载文件
95
+
96
+ **入参:**
97
+
98
+ | 参数 | 类型 | 必需 | 说明 |
99
+ |------|------|------|------|
100
+ | `path` | `string` | 是 | downloadURL 或文件存储路径 |
101
+
102
+ **返回值 `DownloadResult`:**
103
+
104
+ | 字段 | 类型 | 说明 |
105
+ |------|------|------|
106
+ | `content` | `Blob`(默认)/ `ReadableStream`(流式) | 文件内容 |
107
+ | `metadata` | `FileMeta` | 文件元信息 |
108
+
109
+ > **推荐始终使用 `.asStream()`**,避免大文件导致内存溢出。直接 `await download()` 有 50MB 限制。
110
+
111
+ **示例:**
112
+
113
+ ```typescript
114
+ // 推荐:使用 downloadURL + 流式下载
115
+ const downloadURL = fileMeta.downloadURL; // 从上传返回值或数据库获取
116
+ const { content, metadata } = await this.fileService
117
+ .download(downloadURL)
118
+ .asStream();
119
+
120
+ // 也可以使用文件存储路径
121
+ const { content, metadata } = await this.fileService
122
+ .download('file.pdf')
123
+ .asStream();
124
+
125
+ // 不推荐:Blob 下载(限制 50MB,会将整个文件加载到内存)
126
+ const { content, metadata } = await this.fileService.download(downloadURL);
127
+ ```
128
+
129
+ ### 3. 删除文件
130
+
131
+ **入参:**
132
+
133
+ | 参数 | 类型 | 必需 | 说明 |
134
+ |------|------|------|------|
135
+ | `filePaths` | `string[]` | 是 | downloadURL 或文件存储路径数组 |
136
+
137
+ **返回值:** `FileMeta[]` - 删除成功的文件元信息数组
138
+
139
+ **示例:**
140
+
141
+ ```typescript
142
+ // 推荐:使用 downloadURL
143
+ const result = await this.fileService.remove([fileMeta.downloadURL]);
144
+
145
+ // 也可以使用文件存储路径
146
+ const result = await this.fileService.remove(['file1.pdf', 'file2.png']);
147
+ ```
148
+
149
+ ### 4. 获取文件元信息
150
+
151
+ **入参:**
152
+
153
+ | 参数 | 类型 | 必需 | 说明 |
154
+ |------|------|------|------|
155
+ | `filePath` | `string` | 是 | downloadURL 或文件存储路径 |
156
+
157
+ **返回值:** `FileMeta | null` - 文件元信息或 `null` 表示文件不存在或删除失败
158
+
159
+ **示例:**
160
+
161
+ ```typescript
162
+ // 推荐:使用 downloadURL
163
+ const meta = await this.fileService.getFileMetadata(fileMeta.downloadURL);
164
+
165
+ // 也可以使用文件存储路径
166
+ const meta = await this.fileService.getFileMetadata('file.pdf');
167
+ ```
168
+
169
+ ## Common Mistakes
170
+
171
+ | 错误 | 正确做法 |
172
+ |------|----------|
173
+ | 直接 `await download()` 导致内存溢出 | 始终使用 `.asStream()` 流式下载 |
174
+ | 忘记设置 `contentType` 导致浏览器无法预览 | 上传时明确指定 `contentType` |
175
+ | 直接 `new FileService()` 手动实例化 | 通过 NestJS DI 注入 `FileService` |
176
+ | 在异步回调中调用 `download()` 导致上下文丢失 | 在请求处理函数中立即调用,SDK 内部已处理上下文捕获 |
177
+ | 删除时传单个字符串 | `remove()` 参数为 `string[]` 数组 |
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: user-management-best-practices
3
+ description: "Use when 决定是否创建用户表、选择用户唯一标识(user_id vs email/手机号)、设计 user_profile 字段与 UNIQUE 约束、处理防重复提交/upsert 去重,或涉及问卷、抽奖、报名、投票等用户提交场景的表结构设计。触发词:用户管理, 用户表设计, user_id, email, user_profile, 去重, 防重复提交, 批量导入用户, upsert, UNIQUE约束, UserSelect, UserDisplay, user management, deduplication"
4
+ steering: true
5
+ steering-topic: user_management_best_practices
6
+ match-template-name: nestjs-react-fullstack
7
+ ---
8
+
9
+ # 用户管理最佳实践指南
10
+
11
+ ## 概述
12
+
13
+ 本指南适用于生成涉及用户管理、问卷、抽奖、报名、投票等用户提交场景的代码。
14
+
15
+ **核心问题:**
16
+
17
+ - 妙搭 Agent 无法获取企业的所有用户数据,需要构建应用维度的用户表
18
+ - 企业无法显式感知飞书的 user_id(不透明的数字字符串,如 "1847292357012580"),无法批量导入飞书用户到应用的用户表
19
+ - 推荐使用邮箱或者手机号作为业务层唯一标识(不是 user_id)
20
+ - UserSelect 返回的 IUserProfile 对象已包含 email 和 phone_number 字段,可直接使用
21
+
22
+ **本指南解决:**
23
+
24
+ - 何时使用 UserSelect 组件 vs 何时创建用户表
25
+ - 如何设计用户业务表(正确的唯一约束)
26
+ - 防止重复提交的正确实现方式
27
+
28
+ ---
29
+
30
+ ## 核心原则:禁止事项
31
+
32
+ | 禁止操作 | 说明 | 正确做法 |
33
+ | ------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
34
+ | 不推荐实现批量导入用户的功能 | Agent 无法获取企业全量用户,批量 INSERT users 表行不通 | 推荐使用 UserSelect 组件动态选择用户 |
35
+ | 不推荐用 user_id 作为去重逻辑 | 企业不感知飞书用户 id,无法感知是谁 | 使用 email 或者手机号做唯一约束(UNIQUE(email)),不要使用姓名,可能会重复 |
36
+ | 禁止编造 user_id | Mock 数据中不能随意编造 user_id 值 | 必须使用 SQL skill 规定的测试用户列表 |
37
+ | 禁止全量查询用户列表 | 全量拉取数据会导致接口超时或 OOM | 使用后端分页查询(skip/take) |
38
+ | 禁止前端分页用户列表 | 前端分页需要全量拉取数据,导致性能问题 | 使用后端分页,前端仅控制页码 |
39
+ | 禁止只存 user_id 不存 email or 手机号 | 业务层无法用 user_id 做防重和查询 | 存储 user_profile 字段(已包含 email/phone),并在表中单独添加 email 或 phone 列用于唯一约束 |
40
+
41
+ ---
42
+
43
+ ## 决策树示例:何时创建用户表 vs 何时使用 user_profile
44
+
45
+ ```text
46
+ 需要记录用户相关业务数据?
47
+
48
+ ├─ 仅需要识别"谁"执行操作(发帖人、评论者)
49
+ │ └─ ✅ 直接使用 user_profile 类型字段
50
+ │ 示例:posts.author(user_profile)
51
+
52
+ └─ 需要存储业务特定的用户数据
53
+
54
+ ├─ 数据与具体业务资源强绑定(问卷提交、抽奖记录)
55
+ │ └─ ✅ 创建业务表 + user_profile 字段 + email 唯一约束
56
+ │ 示例:questionnaire_responses (id, respondent, email, UNIQUE(questionnaire_id, email))
57
+
58
+ └─ 数据代表"用户在系统中的身份"(员工、学生、会员)
59
+ └─ ✅ 创建人员实体表,user_profile 作为主键
60
+ 示例:employees (employee_profile PRIMARY KEY, department, position)
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 用户列表查询与性能注意事项
66
+
67
+ ### UserSelect vs 分页列表的选择
68
+
69
+ 根据场景选择正确的用户选择方式:
70
+
71
+ | 场景 | 使用组件 | 数据加载方式 | 典型数量 |
72
+ | ------------------ | ---------------- | ------------ | -------- |
73
+ | 表单选择用户 | UserSelect | 搜索驱动 | 1-10 |
74
+ | 用户管理后台 | Table + 后端分页 | 分页加载 | 100+ |
75
+ | 问卷提交记录列表 | Table + 后端分页 | 分页加载 | 100+ |
76
+ | 抽奖参与者名单展示 | Table + 后端分页 | 分页加载 | 100+ |
77
+
78
+ - **UserSelect 组件**:搜索驱动,返回完整 IUserProfile 对象(含 email、phone_number)。详细用法参考 **client-builtins-user-service**
79
+ - **Table + 后端分页**:详细实现参考 **table-skill**
80
+
81
+ ### 分页实现
82
+
83
+ 用户列表必须使用后端分页,禁止前端全量拉取。具体实现参考 **table-skill**。
84
+
85
+ ### 动态创建用户记录
86
+
87
+ **何时动态创建:**
88
+
89
+ 1. **首次提交场景**:用户首次提交问卷/参与抽奖时,使用 `upsert` 方法
90
+ 2. **首次登录场景**:用户首次登录系统时,创建用户记录
91
+
92
+ ## 常见错误速查表
93
+
94
+ | 错误模式 | 问题 | 正确做法 | 影响 |
95
+ | ----------------------- | ---------------------- | ------------------------------------------------------------ | ----------- |
96
+ | 批量 INSERT users | Agent 无法获取全量用户 | 使用 UserSelect 组件 | 🔴 CRITICAL |
97
+ | `UNIQUE(user_id)` | 企业不认识 user_id | `UNIQUE(email)` | 🔴 CRITICAL |
98
+ | 编造 user_id | Mock 数据无效 | 使用测试用户列表 | 🔴 CRITICAL |
99
+ | 全量查询用户列表 | 接口超时或 OOM | 使用后端分页(skip/take) | 🔴 CRITICAL |
100
+ | 前端分页(全量拉取) | 首次加载超时 | 使用后端分页 | 🔴 CRITICAL |
101
+ | 只存 user_id 不存 email | 无法做业务层防重 | 存储 user_profile(已含 email),并添加 email 列用于唯一约束 | 🟠 BUG |
102
+ | 直接显示 user_id | 用户体验差 | 使用 UserDisplay 组件 | 🟡 UX |
103
+ | 创建 users 基础表 | 无法同步飞书数据 | 使用 user_profile 类型 | 🟠 DESIGN |
104
+
105
+ ## 相关技能参考
106
+
107
+ - **client-builtins-user-service**: UserSelect 详细用法、IUserProfile 类型定义、UserDisplay 组件、useCurrentUserProfile hook
108
+ - **sql**: user_profile 类型约束、JSONB 验证规则、测试用户列表
109
+ - **table-skill**: Table 组件使用、后端分页实现、前端分页最佳实践
110
+ - **authz-guide**: 结合基于角色的访问控制(如:只允许用户查看/编辑自己的提交)
111
+
112
+ ## 自查清单
113
+
114
+ ### 表设计
115
+
116
+ - [ ] 判断是否需要创建用户表(参考决策树)
117
+ - [ ] 人员实体表使用 `user_profile PRIMARY KEY`
118
+ - [ ] 业务表使用 `id UUID PRIMARY KEY` + `user_profile` 字段
119
+ - [ ] 业务表包含 `email VARCHAR(255)` or `phone VARCHAR(255)` 字段
120
+ - [ ] 设置了正确的唯一约束(使用 email or 手机号,如 `UNIQUE(resource_id, email)`)
121
+
122
+ ### 前端实现
123
+
124
+ - [ ] 使用 UserSelect 组件而非手动输入
125
+ - [ ] 使用 UserDisplay 展示用户(不直接显示 user_id)
126
+ - [ ] 用户列表使用后端分页(不全量拉取)
127
+ - [ ] Table 组件的 onChange 触发后端请求
128
+
129
+ ### 后端实现
130
+
131
+ - [ ] Entity 定义了正确的唯一约束(`@Unique(['resourceId', 'email'])` or `@Unique(['resourceId', 'phone'])`)
132
+ - [ ] 错误处理包含唯一约束冲突(409 Conflict)
133
+ - [ ] 存储 user_profile 字段(已包含 email/phone)
134
+ - [ ] 在表中单独添加 email 或 phone 列,用于唯一约束和索引查询
135
+ - [ ] 列表接口使用 skip/take 分页查询
136
+ - [ ] 返回 pagination 对象(page, pageSize, total)
137
+
138
+ ### Mock 数据
139
+
140
+ - [ ] user_id 使用测试用户列表(不编造)
141
+ - [ ] user_profile 中的 email/phone 与 user_id 对应正确
142
+ - [ ] 测试了重复提交场景(唯一约束生效)
@@ -1,9 +1,6 @@
1
1
  ---
2
2
  name: code-fix
3
- description: Use when encountering code errors such as import failures, TypeScript/Dto type mismatches, JSX syntax issues, API call exceptions (traceid troubleshooting), production log troubleshooting that should route through miaoda-cli, route 404 errors, or **lucide-react icon not found / duplicate identifier / barrel-export naming conflicts**. 触发词:导入错误, 模块解析失败, 类型错误, Dto不匹配, JSX语法, API异常, traceid, 线上日志, 线上日志查询, 查询线上日志, 路由404, code fix, debugging, lucide-react import error, icon not found, 图标不存在, Cannot find name, 标识符重复, no-redeclare, export 冲突, 桶导出冲突, dual export, "请修复错误" 通用排错
4
- steering: true
5
- steering-topic: code_fix
6
- match-template-name: nestjs-react-fullstack
3
+ description: Use when encountering code errors such as import failures, TypeScript/Dto type mismatches, JSX syntax issues, API call exceptions, route 404 errors, or **lucide-react icon not found / duplicate identifier / barrel-export naming conflicts**. 触发词:导入错误, 模块解析失败, 类型错误, Dto不匹配, JSX语法, API异常, 路由404, code fix, debugging, lucide-react import error, icon not found, 图标不存在, Cannot find name, 标识符重复, no-redeclare, export 冲突, 桶导出冲突, dual export, "请修复错误" 通用排错
7
4
  ---
8
5
 
9
6
 
@@ -70,7 +67,7 @@ import { ListPlus, Cake, Home, Building, Twitter } from "lucide-react";
70
67
 
71
68
  1. **不确定就查**:图标名不在你已知列表里 → 先 `Read packages/client/lucide-react/iconMappings.json`(或 lucide-react 包的 d.ts 导出列表)确认存在,再写 import
72
69
  2. **替换图标必须连同 import 列表一起检查**:把旧图标替换成新图标时,**必须先 grep 当前文件 import 列表**确认新名未已 import,避免触发 LSP "标识符重复" / `no-redeclare`
73
- 3. **LSP 警告是硬约束**:multi_edit / 写代码后看到 LSP 任意 "Cannot find name" / "标识符重复" → **必须先修完警告才能 commit**,禁止带 LSP 错误跑 `commit_task`
70
+ 3. **LSP 警告是硬约束**:写代码后看到 LSP 任意 "Cannot find name" / "标识符重复" → **必须先修完警告才能 commit**,禁止带 LSP 错误提交
74
71
 
75
72
  ### 同名 export 重复(修复 SOP)
76
73
 
@@ -156,22 +153,11 @@ Module '"@client/src/api/gen"' has no exported member named 'DiscrepancyResponse
156
153
 
157
154
  ## 调用生成的 API 异常
158
155
 
159
- ### 需要查询线上日志时使用 miaoda-cli
156
+ ### 查看本地运行日志
160
157
 
161
- **适用场景**:仅当用户提供 traceid、logid、线上报错、发布错误日志、线上运行日志、线上链路追踪,或排查必须依赖线上前端/后端日志。
158
+ **适用场景**:本地开发期 API 调用异常、控制台报错、后端进程行为异常。
162
159
 
163
- **处理要求**:引导并使用 `miaoda-cli` skill 查询线上日志。优先通过 `miaoda observability log/trace` 查询线上运行日志与链路追踪;排查发布错误时使用 `miaoda deploy error-log`。本地开发日志、构建日志、测试日志、浏览器控制台日志不在此范围内,按当前技能或对应工具排查。拿到线上日志后再回到本技能继续定位与修复代码问题。
164
-
165
- ### 用户提供了 traceid,排查错误
166
-
167
- **解决方案**:使用 `miaoda-cli` skill 读取线上前端与后端日志,获取详细错误
168
-
169
- **排查示例**:
170
-
171
- 用户输入:8ae6724e-277e-4d51-afbf-b524b654f27f 看看这个 trace 为什么报错了
172
- 排查路径:
173
- 1. 使用 `miaoda-cli` skill 查询线上服务端与 trace 日志
174
- 2. 根据服务端日志中对应的日志内容修复对应代码逻辑
160
+ **处理要求**:本地日志由 `scripts/dev.js` 落到 `logs/` 目录(`dev.log`、`server.log` 及对应的 `.std.log`),直接 `cat` / `tail -F` 查看;浏览器侧错误看 DevTools Console。本地版**不接管线上日志/traceid 排查链路**(那条路径由发布平台和 oncall 工具承接)。
175
161
 
176
162
  ### 调用 API 客户端时后端返回异常
177
163
 
@@ -1,11 +1,6 @@
1
1
  ---
2
2
  name: coding-guide
3
- description: "项目全局编码规范,必须在任何代码编写、阅读、修改、排查前加载。覆盖:项目结构、目录组织、文件命名、NestJS 后端(MVCS/Drizzle ORM/API 规范/异常处理/用户上下文)、React 19 前端(shadcn/ui/Tailwind/路由/样式/组件规范)、前后端联调(shared 类型/axiosForBackend)、数据库操作、插件能力、质量保障流程、日志规范。Use when: 编写任意前端或后端代码、新建页面或模块、修改接口或数据库操作、排查编译错误或运行时问题、代码审查、理解项目架构。"
4
- steering: true
5
- steering-inclusion: always
6
- steering-topic: coding_guide
7
- match-template-name: nestjs-react-fullstack
8
- control-by-feature-ab: true
3
+ description: "项目全局编码规范,必须在任何代码编写、阅读、修改、排查前加载。覆盖:项目结构、目录组织、文件命名、NestJS 后端(MVCS/Drizzle ORM/API 规范/异常处理/用户上下文)、React 19 前端(shadcn/ui/Tailwind/路由/样式/组件规范)、前后端联调(shared 类型/axiosForBackend)、数据库操作、质量保障流程、日志规范。Use when: 编写任意前端或后端代码、新建页面或模块、修改接口或数据库操作、排查编译错误或运行时问题、代码审查、理解项目架构。"
9
4
  ---
10
5
  # 项目结构
11
6
 
@@ -36,8 +31,8 @@ server/ # 符合 NestJS 项目基本规范
36
31
  │ ├── hello.controller.ts # controller 示例
37
32
  │ ├── hello.module.ts # module 示例
38
33
  │ └── hello.service.ts # service 示例(可选)
39
- ├── database/ # Drizzle ORM 数据库相关。应用仅能进行 DML 操作。若用户需求设计表结构变更,DDL 相关操作需要使用 `ddl_sql` 工具进行。
40
- │ ├── schema.ts # Drizzle ORM 数据库 Schema 定义。会在执行完 DDL 操作后自动生成,必须从该文件导入数据库类型,禁止自行编写 schema 文件。如有需要可调用 CodeGen 工具手动生成。
34
+ ├── database/ # Drizzle ORM 数据库相关。表结构变更需走 drizzle-kit migration 流程,不要手写 SQL DDL。
35
+ │ ├── schema.ts # Drizzle ORM 数据库 Schema 定义。必须从该文件导入数据库类型,禁止自行编写或修改 schema 文件 应通过 drizzle migration 生成。
41
36
  └── common/ # 共享工具和接口
42
37
  │ ├── filters/ # 通用错误处理。
43
38
  │ ├── constants/ # 通用常量。
@@ -68,10 +63,8 @@ client/
68
63
  │ ├── components/ # 可复用的 UI 组件
69
64
  │ │ ├── ui/ # shadcn/ui 组件[Use the components for functionality, but heavily style them.]
70
65
  │ │ │ ├── README.md # shadcn/ui 组件库使用说明 [请务必参考该文档进行组件使用]
71
- {% if projectMeta['flags']['supportBusinessUser'] %}
72
66
  │ │ ├── business-ui/ # 封装好的用户,部门选择组件以及展示组件
73
67
  │ │ │ ├── README.md # 用户展示,用户头像展示,部门选择展示逻辑必须使用 [请务必参考该文档进行组件使用],禁止直接使用avatar组件等方式自行实现
74
- {% endif %}
75
68
  │ │ ├── Layout.tsx # 布局包装器
76
69
  │ ├── hooks/ # 自定义 React hooks
77
70
  │ └── utils/ # 工具函数
@@ -160,10 +153,10 @@ shared/ # 前后端共享的目录
160
153
  2. 改动服务端代码 → 进行接口测试,确保新增接口测试通过
161
154
  3. 提交前 **必须** 读取相关日志确认无错误(服务端: server/server-devserver 日志;客户端: client-devserver 日志)
162
155
 
163
- 如果用户反馈编译失败、服务无法启动:
164
- 1. 调用代码检查工具
165
- 2. 读取 server-devserver client-devserver 日志
166
- 3. dev 服务无响应时可 `pkill -f "dev:server"` / `pkill -f "dev:client"` 触发重启
156
+ 如果用户反馈编译失败、服务无法启动:
157
+ 1. 跑 `tsc --noEmit` / `eslint` 等代码检查
158
+ 2. 查看 `logs/server.log` / `logs/server.std.log` / `logs/dev.log` 找具体错误
159
+ 3. dev 服务无响应:在跑 `npm run dev` 的终端 Ctrl+C 后重新启动即可
167
160
 
168
161
  ---
169
162
 
@@ -209,9 +202,8 @@ shared/ # 前后端共享的目录
209
202
 
210
203
  - **@Injectable() Service 注册**:创建 `@Injectable()` Service → 在对应 Module 的 `providers` 中注册(这条在模块目录内部完成,与聚合文件无关)
211
204
  - **三方集成**:调用第三方 API 需在后端实现,使用 @nestjs/axios
212
- - **能力边界**:服务端不支持文件上传(FaaS 限制),前端用 dataloom SDK 上传,服务端仅保存元信息
205
+ - **文件上传**:前端用 dataloom SDK 上传,服务端仅保存元信息(不要在服务端处理 multipart/form-data)
213
206
  - **环境判断**:`process.env.NODE_ENV === "production"` 表示生产环境
214
- - **文件系统**:**严禁使用 `fs` 写入非 `/tmp` 路径**(无状态 FaaS 容器)
215
207
 
216
208
  ## 日志约定
217
209
 
@@ -286,8 +278,8 @@ await db.select().from(users).where(eq(users.adminUser, userId));
286
278
  ### 数据库使用强约束
287
279
 
288
280
  - 仅通过 schema.ts 暴露的客户端和类型读写,禁止手写 SQL/临时类型
289
- - **变更流程(CRITICAL)**:`execute_sql` 执行 DDL系统自动 codegen → **立即重新读取 schema.ts** → 再编写业务代码。跳过重新读取直接编码是 TS2339 批量错误的主要根因
290
- - 连接失败/SSL 错误:立即停机并提示用户联系技术支持
281
+ - **变更流程(CRITICAL)**:通过 drizzle-kit migration 修改表结构 重新生成 schema.ts → **立即重新读取 schema.ts** → 再编写业务代码。跳过重新读取直接编码是 TS2339 批量错误的主要根因
282
+ - 连接失败/SSL 错误:先检查 `.env.local` 里 `SUDA_DATABASE_URL` 是否正确(走过 `lark-cli apps env pull`),再排查网络/代理
291
283
 
292
284
  ## API 规范
293
285
 
@@ -341,115 +333,11 @@ async createArticle(@Req() req: Request, @Body() dto: CreateArticleDto) {
341
333
 
342
334
  ## 插件能力(Plugin)
343
335
 
344
- ### 召回规则(强制执行)
345
-
346
- 当用户需求涉及以下关键词时,**必须查询相应 Skill 获取开发指南**:
347
-
348
- | 关键词 | 对应能力 |
349
- |-------|---------|
350
- | 多维表格、飞书表格、Base、bitable | 飞书多维表格增删改查 |
351
- | 飞书消息、发送通知、消息推送 | 发送飞书消息 |
352
- | 飞书群、创建群组、群聊 | 创建飞书群组 |
353
- | AI生文、AI写作、文本生成 | AI智能生文 |
354
- | AI总结、文本摘要、内容提炼 | AI文本总结 |
355
- | AI翻译、多语言翻译、语言互译 | AI智能翻译 |
356
- | AI分类、文本分类、自动归类 | AI智能分类 |
357
- | 文本转JSON、结构化提取、文本解析 | AI文本转JSON |
358
- | AI生图、图片生成、智能配图 | AI智能生图 |
359
- | 图生图、图片编辑、图片修改 | AI图片编辑 |
360
- | 图片理解、图片识别、图片分析 | AI图片理解 |
361
- | 图片转JSON、图片提取、图片结构化 | AI图片转JSON |
362
- | 图片对比、图片比较、图片差异 | AI图片对比 |
363
- | 抠图、去背景、去水印、去文字 | AI智能抠图 |
364
- | 背景替换、换背景、背景合成 | AI背景替换 |
365
- | 文档解析、PDF解析、文件提取 | AI文档解析 |
366
- | 搜索总结、网页搜索、搜索摘要 | AI搜索总结 |
367
- | 语音合成、文本转语音、TTS | AI语音合成 |
368
- | 语音识别、音频转文字、STT | AI语音转文字 |
369
- | 插件实例、PluginInstance、Capability | 通用插件调用 |
370
-
371
- ### 插件配置完整性(CRITICAL)
372
-
373
- 调用 `capabilityClient.load(pluginInstanceId)` 前**必须确认**:
374
- 1. `server/capabilities/` 下存在对应 `.json` 配置文件
375
- 2. 配置 `id` 与代码中 `pluginInstanceId` 完全匹配
376
- 3. 不存在则**必须先用 `plugin_instance` 工具创建配置**
336
+ 本地开发态**暂不支持** PluginInstance / capability 类插件能力的创建与调用 — 这套链路依赖云端 agent 工具(`plugin_instance` / `get_plugin_ai_json`)与平台下发的 `server/capabilities/` 配置,本地没有等价物。
377
337
 
378
- ```typescript
379
- // 调用不存在的插件(CapabilityNotFoundError 开发态 Top 错误)
380
- capabilityClient.load('feishu_monthly_demand'); // server/capabilities/ 下无此配置
381
-
382
- // ✅ load 失败时停止后续请求
383
- try {
384
- const result = await plugin.call('read_records', input);
385
- } catch (err) {
386
- if (err.name === 'CapabilityNotFoundError') { setConfigError(true); return; }
387
- throw err;
388
- }
389
- ```
390
- **缓存 load 结果**,避免重复 load 放大错误请求(单应用可达每页 4-6 次失败)。
391
-
392
- ### 关键区分:多维表格 vs 数据库表
393
-
394
- | 场景 | 判断 | 操作 |
395
- |-----|------|-----|
396
- | 明确提到"多维表格/飞书/Base/bitable" | 飞书平台外部服务 | 召回 `plugin_guide` |
397
- | "建表/DDL/schema变更" | 应用内数据库结构 | 使用 `ddl_sql` 工具 |
398
- | "往XX表插入数据"(无明确来源) | 检查 `schema.ts` | 有定义→Drizzle ORM;无定义→询问用户确认 |
399
-
400
- ### 多维表格应用的数据架构选择
401
-
402
- 当应用需要读取飞书多维表格数据时,根据**查询模式**选择架构:
403
-
404
- | 查询模式 | 架构选择 | 实现方式 |
405
- |---------|---------|---------|
406
- | 展示/编辑单条记录 | 纯插件 | `getRecord` / `batchUpdateRecords` |
407
- | 列表分页浏览 | 纯插件 | `searchRecords` + `pageToken` 游标分页 |
408
- | 统计聚合(计数/求和/平均) | **纯插件 + aggregateQuery** | 禁止 searchRecords 全量拉取后内存计算 |
409
- | 统计 + 明细下钻 | 纯插件 | 聚合用 `aggregateQuery`,下钻用 `searchRecords` + filter |
410
- | 排行榜 / TOP N | 纯插件 | `searchRecords` + sort + pageSize=N |
411
- | 复杂排序/多表关联/全文搜索 | 插件同步 + 本地数据库 | 定时/Webhook 同步到 postgres,复杂查询走数据库 |
412
- | 高频写入 + 读取 | 本地数据库为主 | 多维表格仅作展示/备份 |
413
-
414
- **快捷判断:**
415
- - 用户说"仪表盘/dashboard/驾驶舱/看板/统计" → **必须用 `aggregateQuery`**
416
- - 用户说"列表/明细/详情" → `searchRecords` 分页
417
- - 用户说"排行榜/TOP N" → `searchRecords` + sort + pageSize=N
418
- - 需要跨表关联或复杂计算 → 建本地数据库表 + 同步
419
-
420
- ### 使用方式
421
- 1. **复用优先**:先查询插件的 Skills,查看已有的候选插件实例 `available_plugin_instances`。
422
- 2. **查 Runtime Spec**:对候选插件实例调用 `get_plugin_ai_json(pluginInstanceId)`,以返回的 `actions[].key / inputSchema / outputSchema / outputMode` 作为**唯一权威**。
423
- 3. **不能复用才创建/更新**:通过 `plugin_instance` 工具 CREATE/UPDATE 生成或调整单文件 PluginInstance 配置。**绝对禁止**使用 `multi_edit` 工具和 `write` 工具来直接修改 `server/capabilities/` 目录下的内容。
424
- 4. **生成调用代码**:必须先调用 `get_plugin_ai_json(pluginInstanceId)` 获取输入输出要求,严格按 schema 与 outputMode 生成调用逻辑。
425
- > 详细用法、配置格式、代码示例请参阅 `plugin_guide` 文档。
426
-
427
- ### 插件调用 API 签名
428
-
429
- ```typescript
430
- // Client 侧
431
- capabilityClient.load(pluginInstanceId).call(actionKey, input) // 非流式
432
- capabilityClient.load(pluginInstanceId).callStream(actionKey, input) // 流式
433
- // Server 侧
434
- capabilityService.load(pluginInstanceId).call(actionKey, input)
435
- ```
436
-
437
- | 参数 | 说明 |
438
- |------|------|
439
- | `pluginInstanceId` | 插件实例 ID,如 `'booking_notification_feishu'` |
440
- | `actionKey` | 来自 `get_plugin_ai_json` 返回的 `actions[].key` |
441
- | `input` | 结构必须符合 `actions[].inputSchema` |
442
-
443
- ```typescript
444
- // ❌ 错误:把参数对象作为 actionKey 传入
445
- plugin.call(JSON.stringify({ meeting_title: '...' }));
446
- // ❌ 错误:漏掉 actionKey,直接传参数对象
447
- plugin.call({ meeting_title: '...' });
448
- // ✅ 正确:actionKey + input 分开传
449
- plugin.call('send_feishu_message', { meeting_title: '...' });
450
- ```
451
-
452
- 使用方式:复用优先 → 查 Runtime Spec(`get_plugin_ai_json`)→ 不能复用才创建 → 严格按 schema 生成调用代码。**绝对禁止**用 `multi_edit`/`write` 工具直接修改 `server/capabilities/` 目录下的内容。
338
+ 如果业务用到飞书多维表格、AI 生文/翻译/分类、消息推送等"插件能力"覆盖的场景:
339
+ - **数据存储/查询类**(多维表格 CRUD、列表分页、聚合统计):优先用本应用自带的 Postgres + Drizzle,数据可在云端运行时再同步飞书 Base
340
+ - **AI / 飞书消息类**:在云端发布后由平台插件链路承接;本地开发时可先用接口 mock 桩占位
453
341
 
454
342
  ## 自动化任务
455
343
 
@@ -471,10 +359,10 @@ plugin.call('send_feishu_message', { meeting_title: '...' });
471
359
 
472
360
  ## 问题排查指引
473
361
 
474
- 1. 使用 API 测试工具测试不依赖用户信息的接口
475
- 2. 积极使用 `read_logs` 工具,如无有效日志可增加 logger 打印
476
- 3. DevServer 停止响应时 `pkill` 对应进程触发重启
477
- 4. API 测试返回 HTML 内容时:检查模块注册顺序、路由顺序、请求路径。禁止修改内置 ViewController
362
+ 1. `curl` / 浏览器 DevTools / 第三方 HTTP 客户端测试不依赖用户信息的接口
363
+ 2. 查看 `logs/server.log` / `logs/server.std.log` 找后端报错;无有效日志时主动补 logger 打印
364
+ 3. dev 服务无响应:在跑 `npm run dev` 的终端 Ctrl+C 后重新启动
365
+ 4. API 测试返回 HTML 内容时:检查模块注册顺序、路由顺序、请求路径。禁止修改内置 ViewController
478
366
 
479
367
  ---
480
368
 
@@ -489,13 +377,9 @@ plugin.call('send_feishu_message', { meeting_title: '...' });
489
377
  - **图表**: ReactECharts,**开发前必须调用 `/charts-skill`**
490
378
  - **图标**: Lucide React(唯一图标库,禁止 Emoji 和其他图标库)
491
379
  - **表格/表单/图表**: 见下方"组件 Skill 召回规则",开发前必须先调用对应 Skill
492
- {% if projectMeta['flags']['supportBusinessUser'] %}
493
380
  - **用户**: 用户信息展示/选择必须用 `business-ui` 组件(阅读 README.md),禁止直接展示 userId
494
- {% endif %}
495
- {% if projectMeta['flags']['supportTiptapAndStreamdown'] %}
496
381
  - **富文本**: `business-ui/tiptap-editor`(阅读 README.md)
497
382
  - **Markdown 渲染**: `components/ui/streamdown`(内置 prose 排版)
498
- {% endif %}
499
383
 
500
384
  ## API 请求
501
385
 
@@ -527,13 +411,9 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
527
411
  | 组件 | 来源 | 用途 |
528
412
  |------|------|------|
529
413
  | Table | `@lark-apaas/client-toolkit/antd-table` | 数据表格,**先调 `/table-skill`** |
530
- {% if projectMeta['flags']['supportBusinessUser'] %}
531
414
  | UserSelect/UserDisplay/UserProfile/DepartmentSelect | `business-ui/*` | 用户/部门选择展示 |
532
- {% endif %}
533
- {% if projectMeta['flags']['supportTiptapAndStreamdown'] %}
534
415
  | TiptapEditorComplete | `business-ui/tiptap-editor` | 富文本编辑器 |
535
416
  | Streamdown | `components/ui/streamdown` | Markdown/流式渲染 |
536
- {% endif %}
537
417
 
538
418
  ### 组件 Skill 召回规则(强制执行)
539
419
 
@@ -557,7 +437,7 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
557
437
 
558
438
  ## @lark-apaas/client-toolkit
559
439
 
560
- **零假设原则**:绝不基于假设使用任何子模块。使用任何 `@lark-apaas/client-toolkit` 子模块前**必须**:① 查询 Skills `steering_doc_search` → ② 等待响应获取文档 → ③ 严格按文档编码。**禁止直接调用 `@lark-apaas/client-toolkit` 任何函数/方法**(不基于查询到的文档)。该库**仅可在前端代码中使用,禁止在后端代码中引用**。
440
+ **零假设原则**:绝不基于假设使用任何子模块。使用任何 `@lark-apaas/client-toolkit` 子模块前**必须**:① 先查询本地已加载的 Skills 或对应包文档 → ② 严格按文档编码。**禁止直接调用 `@lark-apaas/client-toolkit` 任何函数/方法**(不基于查询到的文档)。该库**仅可在前端代码中使用,禁止在后端代码中引用**。
561
441
 
562
442
  | 功能 | 导入路径 |
563
443
  |------|----------|
@@ -571,7 +451,7 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
571
451
 
572
452
  **useCurrentUserProfile** 返回 `Partial<IUserProfile>`,初始为空对象:访问字段用 `?.`,判断加载完成用 `if (!userInfo?.user_id)`。
573
453
 
574
- **UserService.listUsersByIds**: 根据用户 ID 获取用户详细信息(姓名/头像/邮箱/部门)时**必须使用**,**禁止自行实现用户信息查询逻辑**。非组件上下文需要批量获取用户信息时使用。返回 `result.data.userInfoMap: Record<string, UserInfo>`,UserInfo 含 `userID: string`, `name: I18nText { zh_cn, en_us?, ja_jp? }`, `avatar: string`, `email?: string`, `userType: "_employee"|"_externalUser"|"_anonymousUser"`, `department: { departmentID, name: I18nText }`。{% if projectMeta['flags']['supportBusinessUser'] %}展示场景仍优先用 UserDisplay/UserProfile 组件。{% endif %}
454
+ **UserService.listUsersByIds**: 根据用户 ID 获取用户详细信息(姓名/头像/邮箱/部门)时**必须使用**,**禁止自行实现用户信息查询逻辑**。非组件上下文需要批量获取用户信息时使用。返回 `result.data.userInfoMap: Record<string, UserInfo>`,UserInfo 含 `userID: string`, `name: I18nText { zh_cn, en_us?, ja_jp? }`, `avatar: string`, `email?: string`, `userType: "_employee"|"_externalUser"|"_anonymousUser"`, `department: { departmentID, name: I18nText }`。展示场景仍优先用 UserDisplay/UserProfile 组件。
575
455
 
576
456
  > ⚠️ `UserInfo` 不含任何飞书 ID 字段(`lark_user_id` / `open_id` / `union_id` 都没有)。需要飞书 ID 一律见 `user-identity` skill(当前用户、任意用户 user_id、open_id、union_id 各种场景都在那里)。
577
457
 
@@ -661,18 +541,16 @@ return <h1>{data?.title || '未知标题'}</h1>;
661
541
  ## 图片规范
662
542
 
663
543
  - **展示**:必须用 `@client/src/components/ui/image` 组件(非原生 `<img>`),响应式设 `sizes`,固定宽设 `width`
664
- {% if projectMeta['flags']['supportBusinessUser'] %}
665
544
  - 用户头像用 business-ui 组件
666
- {% endif %}
667
545
 
668
546
  ### 图片资源优先级
669
547
 
670
- 1. 用户上传图片(最高)
671
- 2. `generate_image`(主要方案)— 每个位置独立图片,禁止重复。提示词格式:`[主体] + [环境] + [风格] + [色彩] + [无文字说明]`
672
- - **必须包含**:「无文字、无字母、无符号」明确说明
673
- - **禁止出现**:① 使用场景("适合移动端")② 功能描述("适合/用于/作为")③ 目标用途("品牌展示/课程封面")④ 观众定位("吸引年轻人")⑤ 预期效果("视觉冲击力/引人注目")⑥ 媒介格式("移动端/印刷品")
674
- 3. 内置 Avatar(仅头像)— `@client/src/utils/img-resources/avatar-placeholders.ts`
675
- 4. Picsum(临时)— `https://picsum.photos/seed/${seed}/${w}/${h}`(必须带 seed)
548
+ 1. 用户上传图片(最高)
549
+ 2. 用户自带的素材库 / 设计资源 放到 `client/public/` CDN
550
+ 3. 内置 Avatar(仅头像)— `@client/src/utils/img-resources/avatar-placeholders.ts`
551
+ 4. Picsum(临时)— `https://picsum.photos/seed/${seed}/${w}/${h}`(必须带 seed)
552
+
553
+ > 本地开发态不接 AI 图片生成,需要 AI 生图请走云端发布后由平台插件链路承接
676
554
 
677
555
  ## 核心功能依赖
678
556