@lark-apaas/coding-steering 0.1.6-alpha.3 → 0.1.6-alpha.5
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/client-add-aily-web-chat/SKILL.md +139 -0
- package/steering/design-stack/skills/client-builtins-user-service/SKILL.md +628 -0
- package/steering/design-stack/skills/code-fix/SKILL.md +246 -0
- package/steering/design-stack/skills/feishu/SKILL.md +270 -0
- package/steering/design-stack/skills/feishu/references/approval.md +214 -0
- package/steering/design-stack/skills/feishu/references/attendance.md +163 -0
- package/steering/design-stack/skills/feishu/references/bitable.md +309 -0
- package/steering/design-stack/skills/feishu/references/calendar.md +190 -0
- package/steering/design-stack/skills/feishu/references/contacts.md +160 -0
- package/steering/design-stack/skills/feishu/references/doc.md +256 -0
- package/steering/design-stack/skills/feishu/references/drive.md +103 -0
- package/steering/design-stack/skills/feishu/references/events.md +198 -0
- package/steering/design-stack/skills/feishu/references/id-convert.md +128 -0
- package/steering/design-stack/skills/feishu/references/messaging.md +207 -0
- package/steering/design-stack/skills/feishu/references/oauth.md +164 -0
- package/steering/design-stack/skills/feishu/references/perm.md +90 -0
- package/steering/design-stack/skills/feishu/references/wiki.md +164 -0
- package/steering/design-stack/skills/user-identity/SKILL.md +300 -0
- package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +122 -0
- package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +139 -0
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +628 -0
- package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +270 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +214 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +163 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +309 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +190 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +160 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +256 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +103 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +198 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +128 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +207 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +164 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +90 -0
- package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +164 -0
- package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +267 -0
- package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +452 -0
- package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +300 -0
- package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +246 -0
- package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +707 -0
- package/steering/nestjs-react-fullstack/skills/.gitkeep +0 -0
- package/steering/nestjs-react-fullstack/tech.md +0 -21
|
@@ -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'` |
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# 通讯录 (Contacts)
|
|
2
|
+
|
|
3
|
+
> 开放平台文档(Markdown 版):https://open.larkoffice.com/document/server-docs/contact-v3/resources.md
|
|
4
|
+
|
|
5
|
+
使用 `@larksuiteoapi/node-sdk` 在 NestJS 中查询飞书通讯录用户和部门信息。
|
|
6
|
+
|
|
7
|
+
## 所需权限
|
|
8
|
+
|
|
9
|
+
| 权限标识 | 说明 |
|
|
10
|
+
|----------|------|
|
|
11
|
+
| `contact:contact.base:readonly` | 读取通讯录基本信息 |
|
|
12
|
+
| `contact:user.base:readonly` | 获取用户基础信息 |
|
|
13
|
+
| `contact:department.base:readonly` | 获取部门基础信息 |
|
|
14
|
+
| `contact:user.employee_id:readonly` | 获取用户 employee_id(可选) |
|
|
15
|
+
|
|
16
|
+
> **通讯录权限范围**:必须在安全设置中配置,否则 API 只能查到权限范围内的用户/部门。建议设为「全部成员」。
|
|
17
|
+
|
|
18
|
+
## 获取用户信息
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
const user = await client.contact.user.get({
|
|
22
|
+
path: { user_id: 'ou_xxx' },
|
|
23
|
+
params: { user_id_type: 'open_id' },
|
|
24
|
+
});
|
|
25
|
+
// user.data?.user — { name, en_name, email, mobile, department_ids, status, ... }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 用户 ID 类型说明
|
|
29
|
+
|
|
30
|
+
| user_id_type | 说明 | 示例 |
|
|
31
|
+
|--------------|------|------|
|
|
32
|
+
| `open_id` | 应用级唯一标识 | `ou_xxx`(默认) |
|
|
33
|
+
| `union_id` | 跨应用统一标识 | `on_xxx` |
|
|
34
|
+
| `user_id` | 企业内用户 ID | 无固定前缀 |
|
|
35
|
+
|
|
36
|
+
## 列出部门下的用户
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const users = await client.contact.user.findByDepartment({
|
|
40
|
+
params: {
|
|
41
|
+
department_id: 'od_xxx',
|
|
42
|
+
page_size: 50,
|
|
43
|
+
department_id_type: 'open_department_id',
|
|
44
|
+
user_id_type: 'open_id',
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
// users.data?.items — [{user_id, name, email, ...}, ...]
|
|
48
|
+
// users.data?.has_more, users.data?.page_token — 用于分页
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 分页遍历所有成员
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
for await (const items of client.contact.user.listWithIterator({
|
|
55
|
+
params: {
|
|
56
|
+
department_id: 'od_xxx',
|
|
57
|
+
page_size: 50,
|
|
58
|
+
department_id_type: 'open_department_id',
|
|
59
|
+
user_id_type: 'open_id',
|
|
60
|
+
},
|
|
61
|
+
})) {
|
|
62
|
+
for (const user of items?.items || []) {
|
|
63
|
+
console.log(user.name, user.open_id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 搜索用户
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
const searchResult = await client.contact.user.search({
|
|
72
|
+
data: { query: '张三' },
|
|
73
|
+
params: {
|
|
74
|
+
user_id_type: 'open_id',
|
|
75
|
+
page_size: 20,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
// searchResult.data?.items — 匹配的用户列表
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> 搜索用户需要 `user_access_token`(用户授权),不支持 `tenant_access_token`。如果只有应用凭证,使用 `findByDepartment` 遍历 + 本地过滤替代。
|
|
82
|
+
|
|
83
|
+
## 获取部门信息
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const dept = await client.contact.department.get({
|
|
87
|
+
path: { department_id: 'od_xxx' },
|
|
88
|
+
params: { department_id_type: 'open_department_id' },
|
|
89
|
+
});
|
|
90
|
+
// dept.data?.department — { name, parent_department_id, leader_user_id, member_count, ... }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 列出子部门
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
const children = await client.contact.department.children({
|
|
97
|
+
path: { department_id: 'od_xxx' },
|
|
98
|
+
params: {
|
|
99
|
+
department_id_type: 'open_department_id',
|
|
100
|
+
page_size: 50,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
// children.data?.items — [{department_id, name, member_count, ...}, ...]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 搜索部门
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
const deptSearch = await client.contact.department.search({
|
|
110
|
+
data: { query: '产品' },
|
|
111
|
+
params: {
|
|
112
|
+
department_id_type: 'open_department_id',
|
|
113
|
+
page_size: 20,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
> 搜索部门同样需要 `user_access_token`。应用凭证可用 `department.children()` 遍历代替。
|
|
119
|
+
|
|
120
|
+
## 列出所有部门(从根开始)
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const rootDepts = await client.contact.department.list({
|
|
124
|
+
params: {
|
|
125
|
+
parent_department_id: '0', // 0 表示根部门
|
|
126
|
+
page_size: 50,
|
|
127
|
+
department_id_type: 'open_department_id',
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 典型工作流
|
|
133
|
+
|
|
134
|
+
### 查找某部门所有成员
|
|
135
|
+
|
|
136
|
+
1. **搜索部门** → `client.contact.department.search({ data: { query: '市场部' } })`
|
|
137
|
+
- 或遍历子部门 → `client.contact.department.children()`
|
|
138
|
+
2. **获取部门成员** → `client.contact.user.findByDepartment({ params: { department_id } })`
|
|
139
|
+
|
|
140
|
+
### 查找用户所在部门
|
|
141
|
+
|
|
142
|
+
1. **获取用户信息** → `client.contact.user.get({ path: { user_id } })`
|
|
143
|
+
2. 从返回的 `department_ids` 获取部门 ID
|
|
144
|
+
3. **获取部门详情** → `client.contact.department.get({ path: { department_id } })`
|
|
145
|
+
|
|
146
|
+
### 遍历整个组织架构
|
|
147
|
+
|
|
148
|
+
1. 从根部门开始 → `client.contact.department.list({ params: { parent_department_id: '0' } })`
|
|
149
|
+
2. 递归获取子部门 → `client.contact.department.children()`
|
|
150
|
+
3. 获取每个部门成员 → `client.contact.user.findByDepartment()`
|
|
151
|
+
|
|
152
|
+
## Common Mistakes
|
|
153
|
+
|
|
154
|
+
| 错误 | 正确做法 |
|
|
155
|
+
|------|----------|
|
|
156
|
+
| 未配置通讯录权限范围 | 安全设置 → 通讯录权限范围 → 全部成员 |
|
|
157
|
+
| 用 `tenant_access_token` 搜索用户 | `user.search()` 需 `user_access_token`,否则用 `findByDepartment` |
|
|
158
|
+
| 部门 ID 类型不匹配 | `od_` 开头用 `open_department_id`,纯数字用 `department_id` |
|
|
159
|
+
| 忘记传 `user_id_type` 参数 | 不传默认 `open_id`,注意和实际传入的 ID 类型一致 |
|
|
160
|
+
| 分页获取不完整 | 检查 `has_more`,使用 `listWithIterator` 自动分页 |
|