@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.
- package/README.md +11 -2
- package/package.json +1 -1
- package/steering/design-stack/skills/.gitkeep +0 -0
- package/steering/nestjs-react-fullstack/skills/.gitkeep +0 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +174 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +621 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/management-page-spec.md +505 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/runtime-role-controller-spec.md +203 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +90 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +216 -0
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +405 -0
- package/steering/nestjs-react-fullstack/skills/devops-guide/SKILL.md +119 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +582 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +357 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +513 -0
- package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +118 -0
- package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +177 -0
- package/steering/nestjs-react-fullstack/skills/user-management-best-practices/SKILL.md +142 -0
- package/steering/nestjs-react-fullstack/{skills → skills_local}/code-fix/SKILL.md +5 -19
- package/steering/nestjs-react-fullstack/{skills → skills_local}/coding-guide/SKILL.md +26 -148
- 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
|
|
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
|
|
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
|
-
###
|
|
156
|
+
### 查看本地运行日志
|
|
160
157
|
|
|
161
|
-
|
|
158
|
+
**适用场景**:本地开发期 API 调用异常、控制台报错、后端进程行为异常。
|
|
162
159
|
|
|
163
|
-
|
|
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
|
|
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
|
|
40
|
-
│ ├── schema.ts # Drizzle ORM 数据库 Schema
|
|
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.
|
|
166
|
-
3. dev
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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.
|
|
475
|
-
2.
|
|
476
|
-
3.
|
|
477
|
-
4. API 测试返回 HTML
|
|
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` 子模块前**必须**:①
|
|
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 }
|
|
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.
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
|