@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.
Files changed (44) hide show
  1. package/README.md +11 -2
  2. package/package.json +1 -1
  3. package/steering/design-stack/skills/client-add-aily-web-chat/SKILL.md +139 -0
  4. package/steering/design-stack/skills/client-builtins-user-service/SKILL.md +628 -0
  5. package/steering/design-stack/skills/code-fix/SKILL.md +246 -0
  6. package/steering/design-stack/skills/feishu/SKILL.md +270 -0
  7. package/steering/design-stack/skills/feishu/references/approval.md +214 -0
  8. package/steering/design-stack/skills/feishu/references/attendance.md +163 -0
  9. package/steering/design-stack/skills/feishu/references/bitable.md +309 -0
  10. package/steering/design-stack/skills/feishu/references/calendar.md +190 -0
  11. package/steering/design-stack/skills/feishu/references/contacts.md +160 -0
  12. package/steering/design-stack/skills/feishu/references/doc.md +256 -0
  13. package/steering/design-stack/skills/feishu/references/drive.md +103 -0
  14. package/steering/design-stack/skills/feishu/references/events.md +198 -0
  15. package/steering/design-stack/skills/feishu/references/id-convert.md +128 -0
  16. package/steering/design-stack/skills/feishu/references/messaging.md +207 -0
  17. package/steering/design-stack/skills/feishu/references/oauth.md +164 -0
  18. package/steering/design-stack/skills/feishu/references/perm.md +90 -0
  19. package/steering/design-stack/skills/feishu/references/wiki.md +164 -0
  20. package/steering/design-stack/skills/user-identity/SKILL.md +300 -0
  21. package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +122 -0
  22. package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +139 -0
  23. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +628 -0
  24. package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +270 -0
  25. package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +214 -0
  26. package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +163 -0
  27. package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +309 -0
  28. package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +190 -0
  29. package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +160 -0
  30. package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +256 -0
  31. package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +103 -0
  32. package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +198 -0
  33. package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +128 -0
  34. package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +207 -0
  35. package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +164 -0
  36. package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +90 -0
  37. package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +164 -0
  38. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +267 -0
  39. package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +452 -0
  40. package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +300 -0
  41. package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +246 -0
  42. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +707 -0
  43. package/steering/nestjs-react-fullstack/skills/.gitkeep +0 -0
  44. 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` 自动分页 |