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

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 (55) hide show
  1. package/package.json +1 -1
  2. package/steering/design-stack/skills/.gitkeep +0 -0
  3. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +174 -0
  4. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +627 -0
  5. package/steering/nestjs-react-fullstack/skills/authz-guide/references/management-page-spec.md +508 -0
  6. package/steering/nestjs-react-fullstack/skills/authz-guide/references/runtime-role-controller-spec.md +203 -0
  7. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +90 -0
  8. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +216 -0
  9. package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +1 -1
  10. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +431 -0
  11. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +39 -127
  12. package/steering/nestjs-react-fullstack/skills/devops-guide/SKILL.md +119 -0
  13. package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -1
  14. package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +1 -1
  15. package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +1 -1
  16. package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +3 -1
  17. package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +1 -1
  18. package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +1 -1
  19. package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +2 -1
  20. package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +2 -1
  21. package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +2 -1
  22. package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +2 -2
  23. package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +1 -1
  24. package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +2 -1
  25. package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +2 -1
  26. package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +3 -2
  27. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +1 -0
  28. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +601 -0
  29. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +360 -0
  30. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +515 -0
  31. package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +118 -0
  32. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +177 -0
  33. package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +1 -0
  34. package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +1 -0
  35. package/steering/nestjs-react-fullstack/skills/user-management-best-practices/SKILL.md +142 -0
  36. package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +42 -28
  37. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +37 -149
  38. package/steering/design-stack/skills/client-add-aily-web-chat/SKILL.md +0 -139
  39. package/steering/design-stack/skills/client-builtins-user-service/SKILL.md +0 -628
  40. package/steering/design-stack/skills/code-fix/SKILL.md +0 -246
  41. package/steering/design-stack/skills/feishu/SKILL.md +0 -270
  42. package/steering/design-stack/skills/feishu/references/approval.md +0 -214
  43. package/steering/design-stack/skills/feishu/references/attendance.md +0 -163
  44. package/steering/design-stack/skills/feishu/references/bitable.md +0 -309
  45. package/steering/design-stack/skills/feishu/references/calendar.md +0 -190
  46. package/steering/design-stack/skills/feishu/references/contacts.md +0 -160
  47. package/steering/design-stack/skills/feishu/references/doc.md +0 -256
  48. package/steering/design-stack/skills/feishu/references/drive.md +0 -103
  49. package/steering/design-stack/skills/feishu/references/events.md +0 -198
  50. package/steering/design-stack/skills/feishu/references/id-convert.md +0 -128
  51. package/steering/design-stack/skills/feishu/references/messaging.md +0 -207
  52. package/steering/design-stack/skills/feishu/references/oauth.md +0 -164
  53. package/steering/design-stack/skills/feishu/references/perm.md +0 -90
  54. package/steering/design-stack/skills/feishu/references/wiki.md +0 -164
  55. package/steering/design-stack/skills/user-identity/SKILL.md +0 -300
@@ -0,0 +1,515 @@
1
+ # 飞书多维表格 (feishu-bitable)
2
+
3
+ 本插件提供了一整套与飞书多维表格进行交互(CRUD + 聚合查询)的接口。
4
+
5
+ > ⚠️ **前端禁止手写 OAuth**:`feishu-bitable` 内置授权机制,前端通过 `capabilityClient.load(<pluginInstanceId>).call(...)` 调用即可,**禁止**在前端拼 `https://open.feishu.cn/open-apis/authen/v1/...` URL 触发用户跳转——多维表格 `appToken` 不是飞书自建应用的 OAuth `app_id`,混用会让用户看到"请求非法"。详见 SKILL.md `## 外部服务插件 fallback 规则`。
6
+
7
+ > ⚠️ **业务功能禁用 mock 同步**:依赖本插件的同步函数(如 `syncToFeishu` / `batchSync`)**禁止**用 `setTimeout` + 直接 `successCount = totalRows` 假装写入;plugin 未配置时应明确 `toast.error` + 配置入口指引,不许编一个"假成功"。
8
+
9
+ ## 开发者注意事项
10
+
11
+ **本文档是为 妙搭助手 设计的技术指南。**
12
+
13
+ 在查看本插件的 Action 时,你将同时获得两部分信息:
14
+
15
+ 1. **本文档 (README)**: 提供高级指引、业务逻辑、使用场景、重要约束。
16
+ 2. **Action 的 `inputSchema` 和 `outputSchema`**: 提供精确的 JSON Schema 格式的输入/输出结构。
17
+
18
+ **请遵循以下原则:**
19
+
20
+ - **以 `Schema` 为结构基准**:严格按照 `inputSchema` 构建你的输入对象,并参照 `outputSchema` 解析返回结果。
21
+ - **以 `README` 为语义权威**:`README` 中的描述包含了 `Schema` 无法表达的关键信息。**必须优先遵循 `README` 中的指引和约束。**
22
+
23
+ ---
24
+
25
+ ## 重要提醒
26
+
27
+ **禁止修改插件配置中的任何字段!即使出现字段类型错误或 Action 执行错误也不能修改。**
28
+
29
+ 使用前需读取配置中的 `fields` 字段,了解可操作的字段及其 `bizType`。
30
+
31
+ ## 字段使用规范
32
+
33
+ 1. **使用字段名称而非 ID**:所有操作必须使用 `fields.name`(字段名称),禁止使用字段 ID
34
+
35
+ 2. **选项字段必须严格匹配 enumValues**:`SingleSelect` 和 `MultiSelect` 类型的选项值必须完全参考配置中的 `enumValues`,禁止自行编造
36
+
37
+ ```json
38
+ // 示例:假设字段配置如下
39
+ {
40
+ "name": "状态",
41
+ "bizType": "SingleSelect",
42
+ "enumValues": ["待处理", "进行中", "已完成"]
43
+ }
44
+ // ✅ 正确:使用 enumValues 中的值
45
+ { "状态": "进行中" }
46
+ // ❌ 错误:使用不存在的选项
47
+ { "状态": "处理中" }
48
+ ```
49
+
50
+ 3. **字段名称可能包含中文或特殊字符**:必须使用括号表示法,禁止点表示法
51
+
52
+ ```javascript
53
+ // ✅ 正确
54
+ record['状态']
55
+ record['v1.0版本']
56
+ { '状态': '进行中', 'v1.0版本': '已发布' }
57
+
58
+ // ❌ 错误
59
+ record.状态 // 语法错误
60
+ record.v1.0版本 // 会被错误解析
61
+ ```
62
+
63
+ 4. **特殊字段需特化展示**:Progress(进度条)、Currency(货币符号)、Rating(星级)、DateTime(格式化日期)、Checkbox(勾选图标)、User(头像+名称)、Barcode(条码图形)
64
+ 5. **特殊字段需专用录入组件**:SingleSelect/MultiSelect(下拉选择器)、Checkbox(开关)、DateTime(日期选择器)、User(人员选择器)
65
+
66
+ ---
67
+
68
+ ## BizType 字段类型对照表
69
+
70
+ | 字段类型 | BizType | 字段类型 | BizType | 字段类型 | BizType |
71
+ | -------- | -------------- | ------------ | -------------- | -------- | ------------- |
72
+ | 文本 | `Text` | 单选 | `SingleSelect` | 附件 | `Attachment` |
73
+ | 邮箱 | `Email` | 多选 | `MultiSelect` | 单向关联 | `SingleLink` |
74
+ | 条码 | `Barcode` | 日期 | `DateTime` | 双向关联 | `DuplexLink` |
75
+ | 数字 | `Number` | 复选框 | `Checkbox` | 查找引用 | `Lookup` |
76
+ | 进度 | `Progress` | 人员 | `User` | 公式 | `Formula` |
77
+ | 货币 | `Currency` | 电话号码 | `Phone` | 地理位置 | `Location` |
78
+ | 评分 | `Rating` | 超链接 | `Url` | 群组 | `GroupChat` |
79
+ | 创建时间 | `CreatedTime` | 最后更新时间 | `ModifiedTime` | 创建人 | `CreatedUser` |
80
+ | 修改人 | `ModifiedUser` | 自动编号 | `AutoNumber` | 按钮 | `Button` |
81
+
82
+ ---
83
+
84
+ ## BizType 读写格式对照表
85
+
86
+ **重要**:读取和写入时的数据格式可能不同,必须严格按照下表处理。
87
+
88
+ ### 可写字段
89
+
90
+ | BizType | 写入格式 | 读取格式 | 说明 |
91
+ | --------------------------------------- | ---------------- | ------------------ | ------------------------ |
92
+ | `Text`/`Email`/`Barcode` | `string` | `{ text: string }` | 写入纯字符串,读取为对象 |
93
+ | `Phone` | `string` | `string` | 读写均为字符串 |
94
+ | `Number`/`Progress`/`Currency`/`Rating` | `number` | `number` | 数值类型 |
95
+ | `SingleSelect` | `string` | `string` | 必须匹配 enumValues |
96
+ | `MultiSelect` | `string[]` | `string[]` | 必须匹配 enumValues |
97
+ | `DateTime` | `number` | `number` | Unix 时间戳(毫秒) |
98
+ | `Checkbox` | `boolean` | `boolean \| null` | 读取时可能为 null |
99
+ | `User` | `number[]` | `number[]` | suda user id 数组 |
100
+ | `Url` | `{ text, link }` | `{ text, link }` | 读写格式一致 |
101
+ | `Attachment` | `string[]` | 复杂对象 | 写入文件 URL 数组 |
102
+
103
+ ### 只读字段
104
+
105
+ | BizType | 读取格式 |
106
+ | ---------------------------- | ---------------------------------------------- |
107
+ | `CreatedTime`/`ModifiedTime` | `number`(时间戳) |
108
+ | `CreatedUser`/`ModifiedUser` | `number[]`(user id 数组) |
109
+ | `AutoNumber` | `{ text: string }` |
110
+ | `Formula` | `{ bizType: string, value: any }` |
111
+ | `Location` | `{ fullAddress, provinceName, cityName, ... }` |
112
+ | `GroupChat` | `[{ groupID, displayName, avatarImageUrl }]` |
113
+
114
+ ---
115
+
116
+ ## Actions 概览
117
+
118
+ | Action | 说明 |
119
+ | -------------------- | ------------------------------ |
120
+ | `searchRecords` | 搜索记录,支持筛选、排序、分页 |
121
+ | `getRecord` | 根据 ID 查询单条记录 |
122
+ | `batchAddRecords` | 批量新增记录(最多 500 条) |
123
+ | `batchUpdateRecords` | 批量更新记录(最多 500 条) |
124
+ | `deleteRecords` | 批量删除记录(最多 500 条) |
125
+ | `aggregateQuery` | 聚合查询,支持分组、聚合计算、筛选、排序、分页 |
126
+
127
+ > 注意:分析/统计类场景应优先使用 `aggregateQuery`,不要用 `searchRecords` 模拟统计。
128
+
129
+ ### Action 选择决策树
130
+
131
+ ```
132
+ 需要什么?
133
+ ├─ 查看/编辑单条记录 → getRecord / batchUpdateRecords
134
+ ├─ 列表分页浏览 → searchRecords(游标分页,pageToken 翻页)
135
+ ├─ 按条件筛选少量记录 → searchRecords + filter + pageSize
136
+ ├─ 排行榜 / TOP N → searchRecords + sort + pageSize=N
137
+ ├─ 统计数值(计数/求和/平均/最大最小) → aggregateQuery
138
+ ├─ 分组统计(按部门/状态等维度) → aggregateQuery + dimensions
139
+ ├─ 统计 + 明细下钻 → 聚合用 aggregateQuery,下钻用 searchRecords + filter
140
+ ├─ 新增数据 → batchAddRecords
141
+ └─ 删除数据 → deleteRecords
142
+
143
+ ⚠️ 禁止模式:
144
+ ❌ searchRecords 拉全量 → 内存 reduce/filter 做统计 → 必须用 aggregateQuery
145
+ ❌ searchRecords 拉全量 → sort → slice 取 TOP N → 必须用 searchRecords + sort + pageSize
146
+ ❌ searchRecords 拉全量 → find 找单条 → 必须用 getRecord
147
+ ```
148
+
149
+ ## 数据分析类Action补充说明
150
+
151
+ `aggregateQuery` 用于分组聚合统计,支持 dimensions(分组维度)和 measures(聚合计算)。
152
+
153
+ ### 输入参数
154
+
155
+ - **dimensions 支持的 bizType**:`Text`、`Email`、`Barcode`、`Number`、`Progress`、`Currency`、`Rating`、`SingleSelect`、`MultiSelect`、`DateTime`、`CreatedTime`、`ModifiedTime`、`Checkbox`、`User`、`CreatedUser`、`ModifiedUser`
156
+
157
+ ### 输出结构
158
+
159
+ - 返回 `{ result, hasMore, pageToken }`
160
+ - `result` 是聚合结果数组,每个元素是对象,key 对应 dimensions/measures 中声明字段,value 结构为 `{ value: <actual_value> }`
161
+
162
+ ### dimensions value 类型(常见)
163
+
164
+ - `Text` / `Email` / `Barcode`: `{ text: string }`
165
+ - `Number`: `string`
166
+ - `Progress`: `string`
167
+ - `Currency`: `string`
168
+ - `Rating`: `string`
169
+ - `SingleSelect`: `string`
170
+ - `MultiSelect`: `string[]`
171
+ - `DateTime` / `CreatedTime` / `ModifiedTime`: `string`(按多维表格配置格式)
172
+ - `Checkbox`: `string`(`"true"` / `"false"`)
173
+ - `User`: `Array<{ id, name }>`
174
+ - `CreatedUser` / `ModifiedUser`: `{ id, name }`
175
+
176
+ ### measures value 类型
177
+
178
+ - 所有 measures 的 `value` 都是 `number`
179
+
180
+ ### measures 支持的 bizType
181
+
182
+ | aggregation | 支持的 bizType | 不支持 |
183
+ |-------------|---------------|--------|
184
+ | `count` | 所有类型(但只统计非空值,统计总行数建议用不会为空的 Number 字段) | - |
185
+ | `sum` / `avg` | `Number`、`Currency`、`Progress`、`Rating` | **`Formula`**、`Text` |
186
+ | `max` / `min` | `Number`、`Currency`、`DateTime` | `Formula`、`Text` |
187
+
188
+ ### filter
189
+
190
+ aggregateQuery 的 filter 格式与 searchRecords **完全相同**,遵循下方"搜索过滤条件规则"章节的所有规则。
191
+
192
+ ### expandArrayDimension
193
+
194
+ - `expandArrayDimension=true` 时会先展开数组维度再聚合统计
195
+ - 典型场景:`MultiSelect`、`User`
196
+
197
+ ---
198
+
199
+ ## 代码示例
200
+
201
+ ```typescript
202
+ import { capabilityClient } from '@lark-apaas/client-toolkit';
203
+
204
+ // 搜索记录
205
+ const response = await capabilityClient.load('plugin_instance_id').call<{
206
+ records: Array<{ id: string; record: Record<string, any> }>;
207
+ hasMore: boolean;
208
+ pageToken?: string;
209
+ total: number;
210
+ }>('searchRecords', {
211
+ filter: {
212
+ conjunction: 'and',
213
+ conditions: [{ fieldName: '状态', operator: 'is', value: ['进行中'] }],
214
+ },
215
+ pageSize: 20,
216
+ });
217
+
218
+ // ⚠️ 响应数据直接从 response 访问,不要加 .data 或 .output 前缀
219
+ // ✅ 正确:response.records / response.hasMore / response.total / response.pageToken
220
+ // ❌ 错误:response.data.records / response.data.output.records
221
+ const { records, hasMore, total, pageToken } = response;
222
+ console.log(`共 ${total} 条,本页 ${records.length} 条,还有更多: ${hasMore}`);
223
+
224
+ // 获取单条记录
225
+ const detail = await capabilityClient.load('plugin_instance_id').call<{
226
+ id: string;
227
+ record: Record<string, any>;
228
+ }>('getRecord', { recordID: 'recXXX' });
229
+ // ✅ 正确:detail.id / detail.record
230
+ // ❌ 错误:detail.data.output.id
231
+ const title = detail.record['标题']?.text;
232
+
233
+ // 批量新增(注意:写入时用写入格式,字段名必须用引号包裹)
234
+ await capabilityClient.load('plugin_instance_id').call('batchAddRecords', {
235
+ records: [
236
+ {
237
+ record: {
238
+ 文本字段: '内容', // Text: string(非 { text: string })
239
+ 数字字段: 100, // Number: number
240
+ 单选字段: '选项A', // SingleSelect: string(必须匹配 enumValues)
241
+ 多选字段: ['标签1', '标签2'], // MultiSelect: string[]
242
+ 复选框: true, // Checkbox: boolean
243
+ 日期字段: Date.now(), // DateTime: number (毫秒时间戳)
244
+ 人员字段: [123456, 789012], // User: number[] (suda user id)
245
+ 超链接: { text: '链接文本', link: 'https://example.com' }, // Url
246
+ },
247
+ },
248
+ ],
249
+ });
250
+
251
+ // 批量更新
252
+ await capabilityClient.load('plugin_instance_id').call('batchUpdateRecords', {
253
+ records: [{ id: 'record_id', record: { 文本字段: '新内容' } }],
254
+ });
255
+
256
+ // 删除记录
257
+ await capabilityClient
258
+ .load('plugin_instance_id')
259
+ .call('deleteRecords', { recordIDs: ['id1', 'id2'] });
260
+
261
+ // 读取时注意格式差异
262
+ const textValue = record.record['文本字段']?.text; // 读取时是 { text: string }
263
+ const numberValue = record.record['数字字段']; // number
264
+ const userIds = record.record['人员字段']; // number[]
265
+
266
+ // 聚合查询
267
+ const aggResp = await capabilityClient
268
+ .load('plugin_instance_id')
269
+ .call<{ result: Array<Record<string, { value: unknown }>> }>('aggregateQuery', {
270
+ dimensions: ['region'],
271
+ measures: [{ fieldName: 'order_id', aggregation: 'count', alias: 'order_count' }],
272
+ sort: [{ fieldName: 'region', desc: false }],
273
+ pageSize: 100,
274
+ });
275
+
276
+ const chartData =
277
+ aggResp.result?.map((item) => ({
278
+ region: String(item.region?.value || ''),
279
+ order_count: Number(item.order_count?.value || 0),
280
+ })) || [];
281
+ ```
282
+
283
+ ## 代码实现注意事项
284
+
285
+ 1. **响应数据直接访问,禁止猜测嵌套路径**。`capabilityClient.call()` 返回的就是最终数据,直接用 `response.records`、`response.total` 等。**禁止**加 `.data`、`.output`、`.data.output` 等前缀。如果不确定响应结构,先用 `console.log` 打印完整响应再编码,禁止凭猜测修改数据路径。
286
+ 2. 在进行写入操作时,比如确认提交时,应该有loading效果,禁止用户重复点击。
287
+ 3. 如果capabilityClient call失败,命中了error逻辑,要打印日志,同时把error message提示给用户。
288
+ 4. 在进行更新操作时,只更新需要更新的字段即可,不需要对全量字段进行更新。
289
+ 5. **分页必须使用游标分页,禁止全量拉取后切片**。先拉全部数据再 `slice` 会导致性能极差、浪费带宽和内存。每次只拉当前页数据,通过 `pageToken` 翻页:
290
+
291
+ ```typescript
292
+ const result = await capabilityClient.load(pluginInstanceId).call('searchRecords', {
293
+ pageSize: 10,
294
+ pageToken: cursor, // 上一页返回的 pageToken
295
+ });
296
+ // 返回:{ records, pageToken(下一页游标), hasMore, total }
297
+ ```
298
+
299
+ ---
300
+
301
+ ## 推荐 UI 组件
302
+
303
+ | BizType | 展示 | 录入 |
304
+ | ---------------------------- | --------------------------------------------------------------------- | ------------------------------------------- |
305
+ | `User` | `<UserDisplay users={ids.map(id => ({ user_id: id.toString() }))} />` | `<UserSelect multiple />` |
306
+ | `Url` | `<Hyperlink value={val} readOnly />` | `<Hyperlink readOnly={false} />` |
307
+ | `SingleSelect`/`MultiSelect` | 文本/标签 | `<Select>` 选项来自 enumValues |
308
+ | `DateTime` | `new Date(ts).toLocaleDateString()` | `<Input type="date" />` |
309
+ | `Checkbox` | `value ? '是' : '否'` | `<Checkbox />` |
310
+ | `Currency` | `¥${value.toFixed(2)}` | `<Input type="number" />` |
311
+ | `Progress` | `${value}%` 或进度条 | `<Input type="number" min={0} max={100} />` |
312
+ | `Rating` | `'★'.repeat(value) + '☆'.repeat(5-value)` | `<Input type="number" min={0} max={5} />` |
313
+
314
+ ### 组件代码示例
315
+
316
+ ```tsx
317
+ // 人员展示
318
+ <UserDisplay
319
+ users={record['人员字段']?.map(userId => ({ user_id: userId.toString() })) || []}
320
+ size="small"
321
+ showLabel={true}
322
+ />
323
+
324
+ // 人员录入(多选)
325
+ <UserSelect
326
+ multiple
327
+ value={userIds.map(String)}
328
+ onChange={(val) => setUserIds(val.map(Number))}
329
+ placeholder="请选择人员"
330
+ />
331
+
332
+ // 人员录入(单选)— 注意:单选时 value 是 string | null,不是数组
333
+ // 不要传 accountType="lark",否则返回 larkUserID 与多维表格的 userID 不匹配
334
+ <UserSelect
335
+ value={formData.assignee || null}
336
+ onChange={(val) => setFormData({ ...formData, assignee: val || '' })}
337
+ placeholder="请选择负责人"
338
+ />
339
+
340
+ // 超链接展示/录入
341
+ <Hyperlink
342
+ value={record['超链接']}
343
+ onChange={(val) => setHyperlink(val)}
344
+ readOnly={false}
345
+ placeholder={{ text: "链接文本", link: "https://example.com" }}
346
+ />
347
+
348
+ // 单选下拉(选项必须来自 enumValues)
349
+ <Select value={value} onValueChange={setValue}>
350
+ <SelectTrigger><SelectValue placeholder="请选择" /></SelectTrigger>
351
+ <SelectContent>
352
+ {enumValues.map(opt => (
353
+ <SelectItem key={opt} value={opt}>{opt}</SelectItem>
354
+ ))}
355
+ </SelectContent>
356
+ </Select>
357
+ ```
358
+
359
+ ---
360
+
361
+ ## 表格列渲染示例
362
+
363
+ ```tsx
364
+ const columns = [
365
+ // 文本字段 - 注意读取格式是 { text: string }
366
+ { title: '文本', dataIndex: ['record', '文本字段', 'text'], key: 'text' },
367
+ // 人员字段 - 使用 UserDisplay 组件
368
+ {
369
+ title: '负责人',
370
+ key: 'user',
371
+ render: (_, record) => (
372
+ <UserDisplay
373
+ users={record.record['人员字段']?.map((id) => ({ user_id: id.toString() })) || []}
374
+ size="small"
375
+ />
376
+ ),
377
+ },
378
+ // 日期字段 - 格式化显示
379
+ {
380
+ title: '创建日期',
381
+ dataIndex: ['record', '日期字段'],
382
+ key: 'date',
383
+ render: (v) => (v ? new Date(v).toLocaleDateString() : '-'),
384
+ },
385
+ // 货币 - 格式化显示
386
+ {
387
+ title: '金额',
388
+ dataIndex: ['record', '货币字段'],
389
+ key: 'currency',
390
+ render: (v) => (v != null ? `¥${v.toFixed(2)}` : '-'),
391
+ },
392
+ // 超链接 - 使用 Hyperlink 组件
393
+ {
394
+ title: '链接',
395
+ key: 'hyperlink',
396
+ render: (_, record) => <Hyperlink value={record.record['超链接']} readOnly />,
397
+ },
398
+ // 多选 - 逗号分隔显示
399
+ {
400
+ title: '标签',
401
+ dataIndex: ['record', '多选字段'],
402
+ key: 'multiSelect',
403
+ render: (v) => v?.join(', ') || '-',
404
+ },
405
+ ];
406
+ ```
407
+
408
+ ---
409
+
410
+ ## 搜索过滤条件规则
411
+
412
+ ### 核心规则
413
+
414
+ 1. **value 始终是字符串数组**:数字需转为字符串,如 `["23.4"]`
415
+ 2. **空值判断**:`isEmpty`/`isNotEmpty` 时 value 填 `[]`
416
+ 3. **Formula 字段不支持筛选**
417
+ 4. **Text 字段 `is`/`isNot` 的 value 只能传一个元素**(见下方常见错误)
418
+
419
+ ### 各 BizType 的 value 格式与支持的操作符
420
+
421
+ | BizType | value 格式 | 支持的操作符 |
422
+ | ---------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
423
+ | `Text`/`Email`/`Barcode`/`Phone` | `["内容"]`(只能一个元素) | `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty` |
424
+ | `Number`/`Currency`/`Progress`/`Rating`/`AutoNumber` | `["23.4"]`(数字转字符串) | `is`, `isNot`, `isGreater`, `isGreaterEqual`, `isLess`, `isLessEqual`, `isEmpty`, `isNotEmpty` |
425
+ | `SingleSelect`/`MultiSelect` | `["选项A", "选项B"]`(`is`/`isNot` 填一个,其他可多个) | `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty` |
426
+ | `Checkbox` | `["true"]` 或 `["false"]` | `is`(仅此一个) |
427
+ | `User`/`CreatedUser`/`ModifiedUser` | `["123456"]`(suda user id) | `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty` |
428
+ | `Url` | `["显示名称"]`(填显示文本,非 URL) | `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty` |
429
+ | `GroupChat` | `["328472133423"]`(群组 ID) | `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty` |
430
+ | `Location` | `["地址文本"]`(只能一个元素) | `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty` |
431
+ | `Attachment` | `[]`(仅支持空值判断) | `isEmpty`, `isNotEmpty` |
432
+ | `DateTime`/`CreatedTime`/`ModifiedTime` | 见下方 | `is`, `isEmpty`, `isNotEmpty`, `isGreater`, `isLess` |
433
+
434
+ ### 日期字段 value
435
+
436
+ | value | 含义 | operator 限制 |
437
+ | ------------------------------------------------------------------------- | ---------------------- | ------------- |
438
+ | `["ExactDate", "1702449755000"]` | 具体日期(毫秒时间戳) | - |
439
+ | `["Today"]`/`["Tomorrow"]`/`["Yesterday"]` | 今天/明天/昨天 | - |
440
+ | `["CurrentWeek"]`/`["LastWeek"]`/`["CurrentMonth"]`/`["LastMonth"]` | 本周/上周/本月/上月 | 仅 `is` |
441
+ | `["TheLastWeek"]`/`["TheNextWeek"]`/`["TheLastMonth"]`/`["TheNextMonth"]` | 过去/未来七天/三十天 | 仅 `is` |
442
+
443
+ ### ⚠️ 常见错误(必读)
444
+
445
+ **1. Text 字段 `is`/`isNot` 传多个 value → 查询返回空结果**
446
+
447
+ ```typescript
448
+ // ❌ 错误:Text 类型 is 只能传一个元素,传多个会静默返回空
449
+ { fieldName: '状态', operator: 'is', value: ['红色', '黑色'] }
450
+
451
+ // ✅ 排除少数值:多条 isNot + conjunction: 'and'
452
+ {
453
+ conjunction: 'and',
454
+ conditions: [
455
+ { fieldName: '进展', operator: 'isNot', value: ['已完成'] },
456
+ { fieldName: '进展', operator: 'isNot', value: ['已终止'] },
457
+ ],
458
+ }
459
+
460
+ // ✅ 匹配多个值:用 contains 模糊匹配(适合有公共子串的场景)
461
+ { fieldName: '进展', operator: 'contains', value: ['付款'] } // 匹配"付款审批"和"付款动作"
462
+ ```
463
+
464
+ **2. Formula 字段不支持 filter 和 sum/avg 聚合**
465
+
466
+ 同一数据常有两个字段:原始 Number 字段(如 `金额`,单位元)和计算 Formula 字段(如 `金额(万元)`)。聚合和过滤**必须用 Number 字段**,在代码里做单位转换:
467
+
468
+ ```typescript
469
+ // ❌ 错误:金额(万元) 是 Formula,不能聚合也不能过滤
470
+ { fieldName: '金额(万元)', aggregation: 'sum' }
471
+ { fieldName: '金额(万元)', operator: 'isGreater', value: ['0'] }
472
+
473
+ // ✅ 正确:用 Number 类型的 金额 字段,代码里除以 10000 转万元
474
+ { fieldName: '金额', aggregation: 'sum', alias: 'total_amount' }
475
+ // 结果处理:const amountWan = totalAmount / 10000;
476
+ ```
477
+
478
+ **3. aggregateQuery 的 `isNot` 会排掉字段为空/null 的记录**
479
+
480
+ 与 searchRecords 不同,aggregateQuery 中 `isNot` 会把字段值为空的记录也排除。如果很多记录的该字段为空,结果会远少于预期甚至为 0。
481
+
482
+ 解决方案:不在 filter 里用 `isNot` 排除 Text 值,改为在 dimensions 里加上该字段,在结果侧用 if 跳过不要的分组:
483
+
484
+ ```typescript
485
+ // ❌ 错误:isNot 会把「目前进展」为空的记录也排掉
486
+ filter: { conditions: [{ fieldName: '目前进展', operator: 'isNot', value: ['已完成'] }] }
487
+
488
+ // ✅ 正确:不加 filter,在 dimensions 里加「目前进展」,结果侧排除
489
+ dimensions: ['当前状态', '目前进展'],
490
+ // 解析时:if (progress === '已完成' || progress === '已终止') continue;
491
+ ```
492
+
493
+ **4. count 聚合只统计非空值** — 统计总行数时用不会为空的 Number 字段(如"行号"),不要用可能为空的 Text 字段
494
+
495
+ ### 筛选条件构建示例
496
+
497
+ ```typescript
498
+ const filter = {
499
+ conjunction: 'and', // 或 'or'
500
+ conditions: [
501
+ { fieldName: '标题', operator: 'contains', value: ['关键词'] }, // 文本包含
502
+ { fieldName: '状态', operator: 'is', value: ['进行中'] }, // 单选匹配
503
+ { fieldName: '金额', operator: 'isGreater', value: ['1000'] }, // 数字比较
504
+ { fieldName: '创建日期', operator: 'is', value: ['Today'] }, // 日期-今天
505
+ { fieldName: '截止日期', operator: 'isLess', value: ['ExactDate', Date.now().toString()] }, // 日期-具体
506
+ { fieldName: '负责人', operator: 'is', value: ['123456'] }, // 人员筛选
507
+ { fieldName: '备注', operator: 'isEmpty', value: [] }, // 空值检查
508
+ { fieldName: '已归档', operator: 'is', value: ['true'] }, // 复选框
509
+ ],
510
+ };
511
+
512
+ const response = await capabilityClient
513
+ .load('plugin_instance_id')
514
+ .call<SearchResponse>('searchRecords', { filter, pageSize: 20 });
515
+ ```
@@ -0,0 +1,118 @@
1
+ ---
2
+ name: react-hook-best-practices
3
+ description: "在编写 React Hooks 时遇到闭包陷阱、冗余 Effect、派生状态管理、useEffect 死循环(含「Maximum update depth exceeded」/「循环渲染」/「无限循环」/「页面卡死」等症状)等问题时使用。涵盖 React 19 的 use()、useActionState、useOptimistic 等新特性,以及依赖数组管理、事件处理 vs Effect 等现代模式。React Hooks best practices for stale closures, redundant Effects, derived state, and infinite loop / Maximum update depth diagnosis."
4
+ steering: true
5
+ steering-topic: react_hook_best_practices
6
+ match-template-name: nestjs-react-fullstack
7
+ ---
8
+
9
+ # React Hook 最佳实践 (React 19+)
10
+
11
+ ## ⚡️ 核心原则 (TL;DR)
12
+
13
+ 1. **优先 React 19 新特性**: 用 `use()` 读取异步数据,用 `useActionState` 管理表单,替代繁琐的 `useEffect` + `useState`。
14
+ 2. **拒绝冗余 State**: 能计算得到的变量(派生状态),绝不存入 State,直接计算 or `useMemo`。
15
+ 3. **事件驱动 > Effects**: 用户交互(点击、提交)产生的逻辑写在事件处理函数中,`useEffect` 仅用于同步外部系统(订阅、DOM)。
16
+ 4. **依赖诚实**: `useEffect/useCallback/useMemo` 的依赖数组必须包含所有引用的响应式变量,禁止欺骗 Linter。
17
+
18
+ ---
19
+
20
+ ## 🚫 关键禁忌与陷阱 (Critical Anti-Patterns)
21
+
22
+ | ❌ 错误模式 | ✅ 正确做法 | 说明 |
23
+ | --------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
24
+ | **依赖数组撒谎** | **诚实的依赖数组** | 漏写依赖会导致闭包陷阱(读到旧值)。若不希望频繁触发,用 `useRef` 或拆分逻辑。 |
25
+ | **useEffect 获取数据** | **use() / useActionState** | React 19 中,数据获取应配合 Suspense 或 Action,而非手动管理 loading/error。 |
26
+ | **useCallback 依赖 loading** | **Functional Updates / 移除依赖** | 若 `useCallback` 修改 `loading` 又依赖 `loading`,会导致引用不稳定,触发 `useEffect` 循环或双重请求。 |
27
+ | **useEffect 同步 Props 到 State** | **派生状态 / key** | 直接在 Render 中计算;若需重置状态,给组件加 `key` prop。 |
28
+ | **useEffect 回调直接 async** | **内部定义 async 函数** | `useEffect(async () => ...)` 返回 Promise 会破坏清理机制。 |
29
+ | **手动管理 Form Loading** | **useActionState** | 自动处理 pending/error,减少样板代码。 |
30
+ | **遗漏副作用清理** | **返回清理函数** | 监听器、定时器、订阅必须在 `return () => ...` 中清除。 |
31
+ | **Context value 引用不稳定** | **ref 持值 + 稳定 getter** | `<Provider value={useMemo(() => ({ inst }), [inst])}>` 中若 `inst` 是会更新的实例,每帧 identity 翻 → 所有 consumer 重渲染 → 子组件 ref-callback 链触发 setState → 死循环。改用 `ref.current = inst` + 一次性 `useMemo(() => ({ get: () => ref.current }), [])`。 |
32
+ | **selector / hook 返回 `.bind()` 或新对象字面量** | **直接传引用,不 bind** | `useStore({ selector })` / `useContextSelector` 类用 shallow equality bailout。`selector: ctx => ({ fn: ctx.fn.bind(ctx) })` 每帧产生新函数 → 所有 consumer 重渲染 → setRef 链路放大成死循环。直接传 `ctx.fn`,consumer 自己访问。 |
33
+ | **callback ref 在 render 中创建** | **稳定引用 + useCallback** | `ref={(node) => { map[key] = node }}` 每次 render 新函数 → React commit 阶段先调旧 ref(null) 再调新 ref(node) → 若 callback 含 setState 即死循环。同理 `useMemo(..., [模板字符串/字面量/.map结果])` 让 memo 失效。 |
34
+
35
+ > **死循环根因模式**(针对上方最后 3 条 anti-pattern):报错栈含 `Maximum update depth exceeded` / `setRef` 内调 `dispatchSetState` / `Array.map` + `setRef`(`composeRefs` / `Slot` 链路)时,真根因**始终在 ref provider 一侧**——hook / context 返回值不稳定,被 ref 消费方放大成死循环。不要在 ref 接收方做兜底(关闭自动 focus、外层条件渲染、移除 `asChild`、加 unique key 等都不会修好),按上方 3 条 anti-pattern 追到 ref provider 上游。
36
+
37
+ #### Safe Destructuring (安全解构)
38
+
39
+ **禁止**对可能为空的 Hook 返回值(如 `useActionState` 的 state 或 `use` 读取的异步数据)直接进行深层解构。
40
+
41
+ ```typescript
42
+ // ❌ Anti-pattern
43
+ const [state, action] = useActionState(updateUser, null);
44
+ const {
45
+ data: { name },
46
+ } = state; // 如果 state 初始为 null,直接崩溃
47
+
48
+ // ✅ Best Practice
49
+ const [state, action] = useActionState(updateUser, null);
50
+ const name = state?.data?.name ?? "Guest";
51
+ ```
52
+
53
+ ---
54
+
55
+ ## 🆕 React 19 现代 Hooks 速查
56
+
57
+ | Hook | 场景 | 示例模式 |
58
+ | ------------------ | ---------------------- | -------------------------------------------------------------- |
59
+ | **use(Promise)** | **读取异步数据** | `const data = use(fetchData(id))` (需配合 `<Suspense>`) |
60
+ | **useActionState** | **表单提交/Mutation** | `const [state, action, isPending] = useActionState(fn, null)` |
61
+ | **useOptimistic** | **乐观 UI 更新** | `const [optTodos, addOpt] = useOptimistic(todos, reducer)` |
62
+ | **useFormStatus** | **子组件获取表单状态** | `const { pending } = useFormStatus()` (仅限 `<form>` 内部组件) |
63
+ | **useTransition** | **非阻塞 UI 更新** | `startTransition(() => setFilter(value))` |
64
+
65
+ ### 代码对比:异步数据获取
66
+
67
+ **🔴 旧模式 (React 18-)**:
68
+
69
+ ```typescript
70
+ const [data, setData] = useState(null);
71
+ const [loading, setLoading] = useState(true);
72
+ useEffect(() => {
73
+ fetch(`/api/user/${id}`).then(d => { setData(d); setLoading(false); });
74
+ }, [id]);
75
+ if (loading) return <Spinner />;
76
+ ```
77
+
78
+ **🟢 新模式 (React 19+)**:
79
+
80
+ ```typescript
81
+ // 结合 Suspense 使用
82
+ const data = use(fetchUserPromise(id)); // 支持条件调用!
83
+ return <div>{data.name}</div>;
84
+ ```
85
+
86
+ ---
87
+
88
+ ## 🧠 思维模型:State vs Effect
89
+
90
+ ### 1. 派生状态 (Derived State)
91
+
92
+ **规则**: 如果一个值可以由现有的 props 或 state 计算得出,**不要**把它放入 state。
93
+
94
+ - ❌ `const [fullName, setFullName] = useState('')` + `useEffect` 更新
95
+ - ✅ `const fullName = ${firstName} ${lastName}`
96
+ - ✅ (昂贵计算) `const list = useMemo(() => sort(items), [items])`
97
+
98
+ ### 2. Effect 的正确归宿
99
+
100
+ `useEffect` 是用来**同步** React 之外的东西(非 React 组件状态)的。
101
+
102
+ - **用户点击加载数据?** -> ❌ Effect -> ✅ Event Handler (`onClick`)
103
+ - **表单提交?** -> ❌ Effect -> ✅ `useActionState` / Event Handler
104
+ - **WebSocket 连接?** -> ✅ `useEffect` (因为要保持连接同步)
105
+ - **DOM 元素测量?** -> ✅ `useLayoutEffect` / `useEffect`
106
+
107
+ ---
108
+
109
+ ## ✅ 代码审查清单 (Checklist)
110
+
111
+ 在提交代码前,请自查:
112
+
113
+ - [ ] **Hooks 规则**: 顶层调用,不嵌套在循环/条件中(`use()` 除外)。
114
+ - [ ] **依赖数组**: `useEffect`, `useMemo`, `useCallback` 包含所有外部变量。
115
+ - [ ] **清理工作**: `useEffect` 中是否清理了定时器/订阅?
116
+ - [ ] **竞态处理**: 异步 Effect 是否处理了组件卸载或 id 变更的情况?(如 `ignore` 标志)。
117
+ - [ ] **引用稳定**: `Context` value 或自定义 Hook 返回的对象,是否做了 `useMemo` 缓存?
118
+ - [ ] **React 19 升级**: 是否还在手动写 `loading` state?能否用 `useActionState` 或 `Suspense` 替代?