@lark-apaas/coding-steering 0.1.6-alpha.1 → 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 (39) 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/authn-guide/SKILL.md +122 -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-add-aily-web-chat/SKILL.md +139 -0
  12. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +405 -0
  13. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +628 -0
  14. package/steering/nestjs-react-fullstack/skills/devops-guide/SKILL.md +119 -0
  15. package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +270 -0
  16. package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +214 -0
  17. package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +163 -0
  18. package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +309 -0
  19. package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +190 -0
  20. package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +160 -0
  21. package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +256 -0
  22. package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +103 -0
  23. package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +198 -0
  24. package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +128 -0
  25. package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +207 -0
  26. package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +164 -0
  27. package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +90 -0
  28. package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +164 -0
  29. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +267 -0
  30. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +582 -0
  31. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +357 -0
  32. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +513 -0
  33. package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +118 -0
  34. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +177 -0
  35. package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +452 -0
  36. package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +300 -0
  37. package/steering/nestjs-react-fullstack/skills/user-management-best-practices/SKILL.md +142 -0
  38. package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +232 -0
  39. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +585 -0
@@ -0,0 +1,163 @@
1
+ # 考勤 (Attendance)
2
+
3
+ > 开放平台文档(Markdown 版):https://open.larkoffice.com/document/server-docs/attendance-v1/overview.md
4
+
5
+ 使用 `@larksuiteoapi/node-sdk` 在 NestJS 中查询飞书考勤数据。
6
+
7
+ ## 所需权限
8
+
9
+ | 权限标识 | 说明 |
10
+ |----------|------|
11
+ | `attendance:task:readonly` | 导出打卡数据 |
12
+ | `attendance:rule:readonly` | 导出打卡管理规则(查询考勤组时需要) |
13
+ | `contact:user.employee_id:readonly` | 获取用户 employee_id(open_id 转换必需) |
14
+
15
+ > **重要**:考勤 API 使用 `employee_id` 而非 `open_id`。需先通过通讯录 API 将 `open_id` 转换为 `employee_id`。
16
+
17
+ ## open_id 转 employee_id
18
+
19
+ ```typescript
20
+ // 通过通讯录 API 获取用户的 employee_id
21
+ const user = await client.contact.user.get({
22
+ path: { user_id: 'ou_xxx' },
23
+ params: { user_id_type: 'open_id' },
24
+ });
25
+ const employeeId = user.data?.user?.employee_id; // 如 'abd754f7'
26
+ ```
27
+
28
+ ## 查询打卡结果
29
+
30
+ ```typescript
31
+ const tasks = await client.attendance.userTask.query({
32
+ params: { employee_type: 'employee_id' },
33
+ data: {
34
+ user_ids: ['abd754f7'], // employee_id 列表,最多 50 个
35
+ check_date_from: 20260209, // yyyyMMdd 格式整数
36
+ check_date_to: 20260213,
37
+ },
38
+ });
39
+
40
+ // 遍历结果
41
+ for (const task of tasks.data?.user_task_results || []) {
42
+ console.log(`${task.employee_name} - ${task.day}`);
43
+ for (const record of task.records || []) {
44
+ console.log(` 上班: ${record.check_in_result}, 下班: ${record.check_out_result}`);
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### 日期格式
50
+
51
+ 考勤 API 的日期格式是 `yyyyMMdd` **整数**(不是字符串,不是时间戳):
52
+
53
+ ```typescript
54
+ // 正确
55
+ check_date_from: 20260209
56
+
57
+ // 错误
58
+ check_date_from: '2026-02-09' // 不是字符串
59
+ check_date_from: 1739059200 // 不是 Unix 时间戳
60
+ check_date_from: '20260209' // 不是字符串
61
+ ```
62
+
63
+ ### 打卡状态码
64
+
65
+ | 值 | 含义 |
66
+ |----|------|
67
+ | `Normal` | 正常 |
68
+ | `Late` | 迟到 |
69
+ | `Early` | 早退 |
70
+ | `Lack` | 缺卡 |
71
+ | `Todo` | 未打卡 |
72
+ | `NoNeedCheck` | 无需打卡 |
73
+
74
+ ## 查询补卡记录
75
+
76
+ ```typescript
77
+ const remedys = await client.attendance.userTaskRemedy.query({
78
+ params: { employee_type: 'employee_id' },
79
+ data: {
80
+ user_ids: ['abd754f7'],
81
+ check_time_from: '1738800000', // Unix 秒时间戳字符串
82
+ check_time_to: '1739404800',
83
+ status: 2, // 0=待审批, 1=未通过, 2=已通过, 3=已取消, 4=已撤回
84
+ },
85
+ });
86
+ ```
87
+
88
+ ### 补卡状态码
89
+
90
+ | 值 | 含义 |
91
+ |----|------|
92
+ | 0 | 待审批 |
93
+ | 1 | 未通过 |
94
+ | 2 | 已通过 |
95
+ | 3 | 已取消 |
96
+ | 4 | 已撤回 |
97
+
98
+ ## 查询考勤组
99
+
100
+ ```typescript
101
+ // group_id 从打卡结果中获取
102
+ const group = await client.attendance.group.get({
103
+ path: { group_id: '6737202939523236110' },
104
+ params: { employee_type: 'employee_id' },
105
+ });
106
+ // group.data — { group_name, time_zone, group_type, locations, ... }
107
+ ```
108
+
109
+ ### 考勤组类型
110
+
111
+ | 值 | 含义 |
112
+ |----|------|
113
+ | 0 | 固定班制 |
114
+ | 2 | 排班制 |
115
+ | 3 | 自由班制 |
116
+
117
+ ## 列出考勤组
118
+
119
+ ```typescript
120
+ const groups = await client.attendance.group.list({
121
+ params: { page_size: 20 },
122
+ });
123
+ ```
124
+
125
+ ## 搜索考勤组
126
+
127
+ ```typescript
128
+ const search = await client.attendance.group.search({
129
+ data: { group_name: '产品部' },
130
+ });
131
+ ```
132
+
133
+ ## 典型工作流
134
+
135
+ ### 查看本周考勤
136
+
137
+ 1. **获取 employee_id** → `client.contact.user.get()` 转换 open_id
138
+ 2. **查询打卡结果** → `client.attendance.userTask.query({ data: { user_ids, check_date_from, check_date_to } })`
139
+ 3. 汇总每天上下班打卡状态
140
+ 4. 标记异常(Late/Early/Lack)
141
+
142
+ ### 查看考勤规则
143
+
144
+ 1. **查询打卡结果** → 获取 `group_id`
145
+ 2. **查询考勤组** → `client.attendance.group.get({ path: { group_id } })`
146
+ 3. 查看打卡地点、考勤时间、补卡策略等配置
147
+
148
+ ### 批量查询团队考勤
149
+
150
+ 1. 获取部门成员 → `client.contact.user.findByDepartment()`
151
+ 2. 提取所有成员的 `employee_id`
152
+ 3. 分批查询(每批最多 50 人)→ `client.attendance.userTask.query()`
153
+ 4. 汇总统计异常情况
154
+
155
+ ## Common Mistakes
156
+
157
+ | 错误 | 正确做法 |
158
+ |------|----------|
159
+ | 用 open_id 调考勤 API | 先转换为 employee_id |
160
+ | 日期格式传字符串或时间戳 | 必须是 yyyyMMdd 整数如 `20260209` |
161
+ | 补卡查询时间传整数 | 补卡时间是 Unix 秒时间戳**字符串** |
162
+ | 一次查超过 50 人 | `user_ids` 最多 50 个,需分批 |
163
+ | 未申请 `contact:user.employee_id:readonly` | open_id 转 employee_id 必需此权限 |
@@ -0,0 +1,309 @@
1
+ # 多维表格 (Bitable)
2
+
3
+ > 开放平台文档(Markdown 版):https://open.larkoffice.com/document/server-docs/docs/bitable-v1/bitable-overview.md
4
+
5
+ 使用 `@larksuiteoapi/node-sdk` 在 NestJS 中操作飞书多维表格。
6
+
7
+ ## 所需权限
8
+
9
+ | 权限标识 | 说明 |
10
+ |----------|------|
11
+ | `bitable:app` | 读写多维表格 |
12
+ | `drive:drive` | 访问云空间(创建/列出多维表格时需要) |
13
+
14
+ > 对于已有的多维表格,需确保应用已被添加为协作者:云文档右上角「...」→「更多」→「添加文档应用」。
15
+
16
+ ## 获取 app_token
17
+
18
+ 多维表格的 `app_token` 有三种获取方式:
19
+ 1. **通过 API 创建** → 响应中包含 `app_token`
20
+ 2. **通过 API 列出** → 从文件列表中获取
21
+ 3. **从 URL 提取** → 见下方 URL 解析
22
+
23
+ ### URL 解析
24
+
25
+ 多维表格有两种 URL 格式:
26
+
27
+ | 格式 | URL 示例 | 说明 |
28
+ |------|----------|------|
29
+ | `/base/` | `https://xxx.feishu.cn/base/{app_token}?table=tblXXX` | 直接提取 `app_token` |
30
+ | `/wiki/` | `https://xxx.feishu.cn/wiki/{node_token}?table=tblXXX` | 需先通过 Wiki API 获取 `obj_token` |
31
+
32
+ ```typescript
33
+ // /base/ 格式:直接提取
34
+ const baseUrl = 'https://xxx.feishu.cn/base/ABC123?table=tblXXX';
35
+ const u = new URL(baseUrl);
36
+ const appToken = u.pathname.match(/\/base\/([A-Za-z0-9]+)/)?.[1]; // 'ABC123'
37
+ const tableId = u.searchParams.get('table'); // 'tblXXX'
38
+
39
+ // /wiki/ 格式:需先获取 obj_token
40
+ const wikiUrl = 'https://xxx.feishu.cn/wiki/XYZ789?table=tblXXX';
41
+ const nodeToken = new URL(wikiUrl).pathname.match(/\/wiki\/([A-Za-z0-9]+)/)?.[1];
42
+ const nodeRes = await client.wiki.space.getNode({ params: { token: nodeToken } });
43
+ const appTokenFromWiki = nodeRes.data?.node?.obj_token; // 这才是真正的 app_token
44
+ ```
45
+
46
+ ## 创建多维表格
47
+
48
+ ```typescript
49
+ const res = await client.bitable.app.create({
50
+ data: {
51
+ name: '项目跟踪表',
52
+ // folder_token: 'fldcnxxxxxx', // 可选:目标文件夹
53
+ },
54
+ });
55
+ const appToken = res.data?.app?.app_token;
56
+ const defaultTableId = res.data?.app?.table_id;
57
+ ```
58
+
59
+ ## 获取多维表格信息
60
+
61
+ ```typescript
62
+ const info = await client.bitable.app.get({
63
+ path: { app_token: appToken },
64
+ });
65
+ ```
66
+
67
+ ## 列出数据表
68
+
69
+ ```typescript
70
+ const tables = await client.bitable.appTable.list({
71
+ path: { app_token: appToken },
72
+ params: { page_size: 20 },
73
+ });
74
+ // tables.data?.items — [{table_id, name, revision}, ...]
75
+ ```
76
+
77
+ ## 创建数据表
78
+
79
+ ```typescript
80
+ const newTable = await client.bitable.appTable.create({
81
+ path: { app_token: appToken },
82
+ data: {
83
+ table: {
84
+ name: '新数据表',
85
+ default_view_name: '默认视图',
86
+ fields: [
87
+ { field_name: '任务名称', type: 1 }, // 1 = 多行文本
88
+ { field_name: '状态', type: 3 }, // 3 = 单选
89
+ { field_name: '截止日期', type: 5 }, // 5 = 日期
90
+ ],
91
+ },
92
+ },
93
+ });
94
+ ```
95
+
96
+ ## 列出字段
97
+
98
+ ```typescript
99
+ const fields = await client.bitable.appTableField.list({
100
+ path: { app_token: appToken, table_id: tableId },
101
+ params: { page_size: 100 },
102
+ });
103
+ // fields.data?.items — [{field_id, field_name, type, is_primary, property}, ...]
104
+ ```
105
+
106
+ ## 查询记录
107
+
108
+ ```typescript
109
+ // 使用 search 方法支持 filter 和 sort
110
+ const records = await client.bitable.appTableRecord.search({
111
+ path: { app_token: appToken, table_id: tableId },
112
+ data: {
113
+ field_names: ['任务名称', '负责人', '状态'],
114
+ filter: {
115
+ conjunction: 'and',
116
+ conditions: [
117
+ { field_name: '状态', operator: 'is', value: ['进行中'] },
118
+ ],
119
+ },
120
+ sort: [
121
+ { field_name: '截止日期', desc: false },
122
+ ],
123
+ page_size: 20,
124
+ },
125
+ });
126
+ // records.data?.items — [{record_id, fields: {...}}, ...]
127
+ ```
128
+
129
+ ### filter 语法
130
+
131
+ ```typescript
132
+ // conjunction: 'and' | 'or'
133
+ // operator: 'is' | 'isNot' | 'contains' | 'doesNotContain' | 'isEmpty' | 'isNotEmpty' | 'isGreater' | 'isLess'
134
+ {
135
+ conjunction: 'and',
136
+ conditions: [
137
+ { field_name: '状态', operator: 'is', value: ['进行中'] },
138
+ { field_name: '优先级', operator: 'is', value: ['高'] },
139
+ ],
140
+ }
141
+ ```
142
+
143
+ ## 获取单条记录
144
+
145
+ ```typescript
146
+ const record = await client.bitable.appTableRecord.get({
147
+ path: { app_token: appToken, table_id: tableId, record_id: 'recXXX' },
148
+ });
149
+ ```
150
+
151
+ ## 列出记录(简单查询)
152
+
153
+ ```typescript
154
+ const list = await client.bitable.appTableRecord.list({
155
+ path: { app_token: appToken, table_id: tableId },
156
+ params: {
157
+ page_size: 100,
158
+ // filter: '...', // URL filter 表达式
159
+ // sort: '...', // URL sort 表达式
160
+ },
161
+ });
162
+ ```
163
+
164
+ ## 新增记录
165
+
166
+ ```typescript
167
+ const created = await client.bitable.appTableRecord.create({
168
+ path: { app_token: appToken, table_id: tableId },
169
+ data: {
170
+ fields: {
171
+ '任务名称': '设计数据库表结构',
172
+ '状态': '待开始',
173
+ '优先级': '高',
174
+ '截止日期': 1708300800000, // 毫秒时间戳
175
+ },
176
+ },
177
+ });
178
+ const recordId = created.data?.record?.record_id;
179
+ ```
180
+
181
+ ## 批量新增记录
182
+
183
+ ```typescript
184
+ await client.bitable.appTableRecord.batchCreate({
185
+ path: { app_token: appToken, table_id: tableId },
186
+ data: {
187
+ records: [
188
+ { fields: { '任务名称': '任务1', '状态': '待开始' } },
189
+ { fields: { '任务名称': '任务2', '状态': '待开始' } },
190
+ ],
191
+ },
192
+ });
193
+ ```
194
+
195
+ ## 更新记录
196
+
197
+ ```typescript
198
+ await client.bitable.appTableRecord.update({
199
+ path: { app_token: appToken, table_id: tableId, record_id: recordId },
200
+ data: {
201
+ fields: {
202
+ '状态': '已完成',
203
+ '完成日期': Date.now(), // 毫秒时间戳
204
+ },
205
+ },
206
+ });
207
+ ```
208
+
209
+ ## 删除记录
210
+
211
+ ```typescript
212
+ // 单条删除
213
+ await client.bitable.appTableRecord.delete({
214
+ path: { app_token: appToken, table_id: tableId, record_id: 'recXXX' },
215
+ });
216
+
217
+ // 批量删除
218
+ await client.bitable.appTableRecord.batchDelete({
219
+ path: { app_token: appToken, table_id: tableId },
220
+ data: {
221
+ records: ['recXXX', 'recYYY', 'recZZZ'],
222
+ },
223
+ });
224
+ ```
225
+
226
+ ## 字段 CRUD
227
+
228
+ ### 创建字段
229
+
230
+ ```typescript
231
+ const res = await client.bitable.appTableField.create({
232
+ path: { app_token: appToken, table_id: tableId },
233
+ data: {
234
+ field_name: '优先级',
235
+ type: 3, // SingleSelect
236
+ // property: { options: [{name: '高'}, {name: '中'}, {name: '低'}] }, // 可选
237
+ },
238
+ });
239
+ const fieldId = res.data?.field?.field_id;
240
+ ```
241
+
242
+ ### 更新字段
243
+
244
+ ```typescript
245
+ await client.bitable.appTableField.update({
246
+ path: { app_token: appToken, table_id: tableId, field_id: fieldId },
247
+ data: {
248
+ field_name: '新字段名',
249
+ type: 1, // 字段类型
250
+ },
251
+ });
252
+ ```
253
+
254
+ ### 删除字段
255
+
256
+ ```typescript
257
+ await client.bitable.appTableField.delete({
258
+ path: { app_token: appToken, table_id: tableId, field_id: fieldId },
259
+ });
260
+ ```
261
+
262
+ > **注意**:主字段(`is_primary: true`)不能删除,只能重命名。
263
+
264
+ ## 新建多维表格清理建议
265
+
266
+ 新建的多维表格会自动创建默认字段(单选、日期、附件)和空行。建议创建后清理:
267
+ 1. 删除不需要的默认字段(类型 3/5/17)
268
+ 2. 重命名主字段为有意义的名称
269
+ 3. 批量删除空的占位行
270
+
271
+ ## 字段类型与写入格式
272
+
273
+ | 类型编号 | 字段类型 | 写入格式 | 示例 |
274
+ |----------|----------|----------|------|
275
+ | 1 | 多行文本 | string 或 text 对象数组 | `"Hello"` 或 `[{"text":"Hello","type":"text"}]` |
276
+ | 2 | 数字 | number | `2323.23` |
277
+ | 3 | 单选 | string | `"选项1"` |
278
+ | 4 | 多选 | string[] | `["选项1", "选项2"]` |
279
+ | 5 | 日期 | number(毫秒时间戳) | `1690992000000` |
280
+ | 7 | 复选框 | boolean | `true` |
281
+ | 11 | 人员 | object[] | `[{"id": "ou_xxx"}]` |
282
+ | 13 | 电话号码 | string | `"13800138000"` |
283
+ | 15 | 超链接 | object | `{"text": "链接", "link": "https://..."}` |
284
+ | 17 | 附件 | object[] | `[{"file_token": "xxx"}]` |
285
+ | 18 | 单向关联 | string[] | `["recXXX"]` |
286
+ | 19 | 查找引用 | — | 只读,由关联字段自动计算 |
287
+ | 20 | 公式 | — | 只读,由公式自动计算 |
288
+ | 21 | 双向关联 | string[] | `["recXXX"]` |
289
+ | 22 | 地理位置 | object | `{"location": "北京市朝阳区", "pname": "北京市"}` |
290
+ | 23 | 群组 | string[] | `["oc_xxx"]` |
291
+ | 1001 | 创建时间 | — | 只读,系统自动生成 |
292
+ | 1002 | 修改时间 | — | 只读,系统自动生成 |
293
+ | 1003 | 创建人 | — | 只读,系统自动生成 |
294
+ | 1004 | 修改人 | — | 只读,系统自动生成 |
295
+ | 1005 | 自动编号 | — | 只读,系统自动生成 |
296
+
297
+ ## Common Mistakes
298
+
299
+ | 错误 | 正确做法 |
300
+ |------|----------|
301
+ | 应用无法访问已有多维表格 | 需先添加应用为协作者 |
302
+ | 日期字段传 ISO 字符串 | 必须传毫秒时间戳数字 |
303
+ | 人员字段传字符串 | 必须传 `[{id: 'ou_xxx'}]` 数组格式 |
304
+ | 单选/多选传不存在的选项 | 选项会自动创建,但注意拼写一致 |
305
+ | `app_token` 和 `table_id` 搞混 | `app_token` 是多维表格级别,`table_id` 是数据表级别 |
306
+ | 查询用 `list` 不支持复杂筛选 | 使用 `search` 方法支持 filter/sort |
307
+ | `/wiki/` URL 直接当 app_token 用 | Wiki URL 的 token 是 node_token,需先 `wiki.space.getNode()` 获取 `obj_token` |
308
+ | 删除主字段 | 主字段(`is_primary: true`)不能删除,只能重命名 |
309
+ | 写入只读字段(公式/创建时间等) | 类型 19/20/1001-1005 为只读,不能通过 API 写入 |
@@ -0,0 +1,190 @@
1
+ # 日历与会议室 (Calendar)
2
+
3
+ > 开放平台文档(Markdown 版):https://open.larkoffice.com/document/server-docs/calendar-v4/overview.md
4
+
5
+ 使用 `@larksuiteoapi/node-sdk` 在 NestJS 中管理日程、预约会议室和查询忙闲状态。
6
+
7
+ ## 所需权限
8
+
9
+ | 权限标识 | 说明 |
10
+ |----------|------|
11
+ | `calendar:calendar` | 读写日历及日程信息 |
12
+ | `vc:room:readonly` | 查询/搜索会议室 |
13
+ | `contact:user.employee_id:readonly` | 获取用户 ID(可选) |
14
+
15
+ ## 创建日程
16
+
17
+ ```typescript
18
+ const res = await client.calendar.calendarEvent.create({
19
+ path: { calendar_id: 'primary' }, // 'primary' 表示主日历
20
+ data: {
21
+ summary: '产品评审会议',
22
+ description: 'Q1 产品路线图评审',
23
+ start_time: {
24
+ timestamp: String(Math.floor(new Date('2026-02-13T14:00:00+08:00').getTime() / 1000)),
25
+ },
26
+ end_time: {
27
+ timestamp: String(Math.floor(new Date('2026-02-13T15:00:00+08:00').getTime() / 1000)),
28
+ },
29
+ attendee_ability: 'can_invite_others',
30
+ },
31
+ });
32
+ const eventId = res.data?.event?.event_id;
33
+ ```
34
+
35
+ ## 更新日程
36
+
37
+ ```typescript
38
+ await client.calendar.calendarEvent.patch({
39
+ path: { calendar_id: 'primary', event_id: eventId },
40
+ data: {
41
+ summary: '产品评审会议(更新)',
42
+ end_time: {
43
+ timestamp: String(Math.floor(new Date('2026-02-13T15:30:00+08:00').getTime() / 1000)),
44
+ },
45
+ },
46
+ });
47
+ ```
48
+
49
+ ## 获取日程详情
50
+
51
+ ```typescript
52
+ const detail = await client.calendar.calendarEvent.get({
53
+ path: { calendar_id: 'primary', event_id: eventId },
54
+ });
55
+ ```
56
+
57
+ ## 获取日程列表
58
+
59
+ ```typescript
60
+ const events = await client.calendar.calendarEvent.list({
61
+ path: { calendar_id: 'primary' },
62
+ params: {
63
+ start_time: String(Math.floor(new Date('2026-02-13T00:00:00+08:00').getTime() / 1000)),
64
+ end_time: String(Math.floor(new Date('2026-02-14T00:00:00+08:00').getTime() / 1000)),
65
+ page_size: 50,
66
+ },
67
+ });
68
+ ```
69
+
70
+ ## 删除日程
71
+
72
+ ```typescript
73
+ await client.calendar.calendarEvent.delete({
74
+ path: { calendar_id: 'primary', event_id: eventId },
75
+ });
76
+ ```
77
+
78
+ ## 添加参与人
79
+
80
+ ```typescript
81
+ await client.calendar.calendarEventAttendee.create({
82
+ path: { calendar_id: 'primary', event_id: eventId },
83
+ data: {
84
+ attendees: [
85
+ { type: 'user', user_id: 'ou_xxx1' },
86
+ { type: 'user', user_id: 'ou_xxx2' },
87
+ { type: 'resource', room_id: 'omm_xxx' }, // 预约会议室
88
+ ],
89
+ need_notification: true,
90
+ },
91
+ params: { user_id_type: 'open_id' },
92
+ });
93
+ ```
94
+
95
+ > **会议室预约是异步的**:添加会议室参与人成功不代表预约成功。需后续通过日程参与人列表中会议室的 `rsvp_status` 确认预约状态。
96
+
97
+ ## 获取参与人列表
98
+
99
+ ```typescript
100
+ const attendees = await client.calendar.calendarEventAttendee.list({
101
+ path: { calendar_id: 'primary', event_id: eventId },
102
+ params: { user_id_type: 'open_id', page_size: 50 },
103
+ });
104
+ ```
105
+
106
+ ## 删除参与人
107
+
108
+ ```typescript
109
+ await client.calendar.calendarEventAttendee.batchDelete({
110
+ path: { calendar_id: 'primary', event_id: eventId },
111
+ data: {
112
+ attendee_ids: ['user_xxx'],
113
+ },
114
+ });
115
+ ```
116
+
117
+ ## 忙闲查询
118
+
119
+ ```typescript
120
+ // 注意:方法名是 batch,不是 list
121
+ const freebusy = await client.calendar.freebusy.batch({
122
+ data: {
123
+ time_min: String(Math.floor(new Date('2026-02-13T09:00:00+08:00').getTime() / 1000)),
124
+ time_max: String(Math.floor(new Date('2026-02-13T18:00:00+08:00').getTime() / 1000)),
125
+ user_id: { user_id: 'ou_xxx', id_type: 'open_id' },
126
+ },
127
+ });
128
+ // 返回 freebusy.data?.freebusy_list — 忙碌时间段数组
129
+ ```
130
+
131
+ 查询会议室忙闲时,使用 `room_id` 代替 `user_id`:
132
+
133
+ ```typescript
134
+ const roomFreebusy = await client.calendar.freebusy.batch({
135
+ data: {
136
+ time_min: '...',
137
+ time_max: '...',
138
+ room_id: 'omm_xxx',
139
+ },
140
+ });
141
+ ```
142
+
143
+ ## 会议室列表
144
+
145
+ ```typescript
146
+ const rooms = await client.vc.room.list({
147
+ params: {
148
+ page_size: 20,
149
+ // room_level_id: 'xxx', // 可选:指定层级
150
+ },
151
+ });
152
+ ```
153
+
154
+ ## 搜索会议室
155
+
156
+ ```typescript
157
+ const searchResult = await client.vc.room.search({
158
+ data: {
159
+ query: '大会议室',
160
+ },
161
+ params: { page_size: 10 },
162
+ });
163
+ ```
164
+
165
+ ## 典型工作流
166
+
167
+ ### 预约会议室
168
+
169
+ 1. **搜索会议室** → `client.vc.room.search({ data: { query: '关键词' } })`
170
+ 2. **查询忙闲** → `client.calendar.freebusy.batch({ data: { room_id, time_min, time_max } })`
171
+ 3. **创建日程** → `client.calendar.calendarEvent.create({ ... })`
172
+ 4. **添加会议室** → `client.calendar.calendarEventAttendee.create({ data: { attendees: [{ type: 'resource', room_id }] } })`
173
+ 5. **确认状态** → 查看参与人列表中会议室的 `rsvp_status`
174
+
175
+ ### 安排团队会议
176
+
177
+ 1. 分别查询每位成员忙闲 → `client.calendar.freebusy.batch()`
178
+ 2. 找到共同空闲时间段
179
+ 3. 搜索可用会议室
180
+ 4. 创建日程并添加参与人和会议室
181
+
182
+ ## Common Mistakes
183
+
184
+ | 错误 | 正确做法 |
185
+ |------|----------|
186
+ | 时间传 ISO 字符串 | SDK 需要 Unix 秒时间戳字符串 |
187
+ | 忙闲查询用 `freebusy.list` | 正确方法名是 `freebusy.batch` |
188
+ | 会议室查询用 `calendar.*` | 会议室在 `vc.room.*` 域下 |
189
+ | 以为添加会议室立即生效 | 会议室预约是异步的,需查询 `rsvp_status` |
190
+ | 忘记传 `calendar_id` | path 中必须传 `calendar_id`,主日历用 `'primary'` |