@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.
- package/package.json +1 -1
- package/steering/design-stack/skills/.gitkeep +0 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +174 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +627 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/management-page-spec.md +508 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/runtime-role-controller-spec.md +203 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +90 -0
- package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +216 -0
- package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +1 -1
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +431 -0
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +39 -127
- package/steering/nestjs-react-fullstack/skills/devops-guide/SKILL.md +119 -0
- package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -1
- package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +1 -1
- package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +1 -1
- package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +3 -1
- package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +1 -1
- package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +1 -1
- package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +2 -1
- package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +2 -1
- package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +2 -1
- package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +2 -2
- package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +1 -1
- package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +2 -1
- package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +2 -1
- package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +3 -2
- package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +1 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +601 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +360 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +515 -0
- package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +118 -0
- package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +177 -0
- package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +1 -0
- package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +1 -0
- package/steering/nestjs-react-fullstack/skills/user-management-best-practices/SKILL.md +142 -0
- package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +42 -28
- package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +37 -149
- package/steering/design-stack/skills/client-add-aily-web-chat/SKILL.md +0 -139
- package/steering/design-stack/skills/client-builtins-user-service/SKILL.md +0 -628
- package/steering/design-stack/skills/code-fix/SKILL.md +0 -246
- package/steering/design-stack/skills/feishu/SKILL.md +0 -270
- package/steering/design-stack/skills/feishu/references/approval.md +0 -214
- package/steering/design-stack/skills/feishu/references/attendance.md +0 -163
- package/steering/design-stack/skills/feishu/references/bitable.md +0 -309
- package/steering/design-stack/skills/feishu/references/calendar.md +0 -190
- package/steering/design-stack/skills/feishu/references/contacts.md +0 -160
- package/steering/design-stack/skills/feishu/references/doc.md +0 -256
- package/steering/design-stack/skills/feishu/references/drive.md +0 -103
- package/steering/design-stack/skills/feishu/references/events.md +0 -198
- package/steering/design-stack/skills/feishu/references/id-convert.md +0 -128
- package/steering/design-stack/skills/feishu/references/messaging.md +0 -207
- package/steering/design-stack/skills/feishu/references/oauth.md +0 -164
- package/steering/design-stack/skills/feishu/references/perm.md +0 -90
- package/steering/design-stack/skills/feishu/references/wiki.md +0 -164
- package/steering/design-stack/skills/user-identity/SKILL.md +0 -300
|
@@ -1,190 +0,0 @@
|
|
|
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'` |
|
|
@@ -1,160 +0,0 @@
|
|
|
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` 自动分页 |
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
# 云文档 (Document)
|
|
2
|
-
|
|
3
|
-
> 开放平台文档(Markdown 版):https://open.larkoffice.com/document/server-docs/docs/docs-overview
|
|
4
|
-
|
|
5
|
-
使用 `@larksuiteoapi/node-sdk` 在 NestJS 中操作飞书云文档。
|
|
6
|
-
|
|
7
|
-
## 所需权限
|
|
8
|
-
|
|
9
|
-
| 权限标识 | 说明 |
|
|
10
|
-
|----------|------|
|
|
11
|
-
| `docx:document` | 读写新版文档 |
|
|
12
|
-
| `docx:document:readonly` | 只读新版文档 |
|
|
13
|
-
| `docx:document.block:convert` | Markdown 转 Block(写入/追加需要) |
|
|
14
|
-
| `drive:drive` | 访问云空间(创建文档到指定文件夹时需要) |
|
|
15
|
-
|
|
16
|
-
> 对于已有的文档,需确保应用已被添加为协作者:文档右上角「...」→「更多」→「添加文档应用」。
|
|
17
|
-
|
|
18
|
-
## 从 URL 提取 Token
|
|
19
|
-
|
|
20
|
-
URL 格式:`https://xxx.feishu.cn/docx/{doc_token}`
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
23
|
-
const url = 'https://xxx.feishu.cn/docx/ABC123def';
|
|
24
|
-
const docToken = new URL(url).pathname.split('/docx/')[1]; // 'ABC123def'
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## 创建文档
|
|
28
|
-
|
|
29
|
-
```typescript
|
|
30
|
-
const res = await client.docx.document.create({
|
|
31
|
-
data: {
|
|
32
|
-
title: '新文档',
|
|
33
|
-
// folder_token: 'fldcnXXX', // 可选:目标文件夹
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
const docId = res.data?.document?.document_id;
|
|
37
|
-
const docUrl = `https://feishu.cn/docx/${docId}`;
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## 读取文档
|
|
41
|
-
|
|
42
|
-
### 读取工作流
|
|
43
|
-
|
|
44
|
-
1. **先获取纯文本** — 快速了解文档内容
|
|
45
|
-
2. **检查 hint** — 如果返回中包含 `hint` 字段,说明文档含有表格、图片等结构化内容
|
|
46
|
-
3. **获取 Block 列表** — 需要结构化内容时使用 `documentBlock.list()`
|
|
47
|
-
|
|
48
|
-
### 读取纯文本
|
|
49
|
-
|
|
50
|
-
```typescript
|
|
51
|
-
const [contentRes, infoRes] = await Promise.all([
|
|
52
|
-
client.docx.document.rawContent({ path: { document_id: docToken } }),
|
|
53
|
-
client.docx.document.get({ path: { document_id: docToken } }),
|
|
54
|
-
]);
|
|
55
|
-
const title = infoRes.data?.document?.title;
|
|
56
|
-
const content = contentRes.data?.content;
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### 读取 Block 列表(结构化内容)
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
const blocksRes = await client.docx.documentBlock.list({
|
|
63
|
-
path: { document_id: docToken },
|
|
64
|
-
});
|
|
65
|
-
const blocks = blocksRes.data?.items ?? [];
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## 写入文档(替换全部内容)
|
|
69
|
-
|
|
70
|
-
工作流:清空现有内容 → 转换 Markdown → 插入新 Block
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
// 1. 清空文档内容(保留 Page block)
|
|
74
|
-
const existing = await client.docx.documentBlock.list({
|
|
75
|
-
path: { document_id: docToken },
|
|
76
|
-
});
|
|
77
|
-
const childIds = existing.data?.items
|
|
78
|
-
?.filter(b => b.parent_id === docToken && b.block_type !== 1)
|
|
79
|
-
.map(b => b.block_id) ?? [];
|
|
80
|
-
|
|
81
|
-
if (childIds.length > 0) {
|
|
82
|
-
await client.docx.documentBlockChildren.batchDelete({
|
|
83
|
-
path: { document_id: docToken, block_id: docToken },
|
|
84
|
-
data: { start_index: 0, end_index: childIds.length },
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// 2. 转换 Markdown 为 Block
|
|
89
|
-
const convertRes = await client.docx.document.convert({
|
|
90
|
-
data: { content_type: 'markdown', content: markdownContent },
|
|
91
|
-
});
|
|
92
|
-
const blocks = convertRes.data?.blocks ?? [];
|
|
93
|
-
|
|
94
|
-
// 3. 插入 Block
|
|
95
|
-
await client.docx.documentBlockChildren.create({
|
|
96
|
-
path: { document_id: docToken, block_id: docToken },
|
|
97
|
-
data: { children: blocks },
|
|
98
|
-
});
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## 追加内容
|
|
102
|
-
|
|
103
|
-
与写入相同,但跳过清空步骤:
|
|
104
|
-
|
|
105
|
-
```typescript
|
|
106
|
-
// 转换 + 插入(不清空)
|
|
107
|
-
const convertRes = await client.docx.document.convert({
|
|
108
|
-
data: { content_type: 'markdown', content: markdownContent },
|
|
109
|
-
});
|
|
110
|
-
await client.docx.documentBlockChildren.create({
|
|
111
|
-
path: { document_id: docToken, block_id: docToken },
|
|
112
|
-
data: { children: convertRes.data?.blocks ?? [] },
|
|
113
|
-
});
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## Block 操作
|
|
117
|
-
|
|
118
|
-
### 获取单个 Block
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
const res = await client.docx.documentBlock.get({
|
|
122
|
-
path: { document_id: docToken, block_id: blockId },
|
|
123
|
-
});
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### 更新 Block 文本
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
await client.docx.documentBlock.patch({
|
|
130
|
-
path: { document_id: docToken, block_id: blockId },
|
|
131
|
-
data: {
|
|
132
|
-
update_text_elements: {
|
|
133
|
-
elements: [{ text_run: { content: '新文本内容' } }],
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### 删除 Block
|
|
140
|
-
|
|
141
|
-
```typescript
|
|
142
|
-
// 需要知道 block 在父容器中的 index
|
|
143
|
-
const children = await client.docx.documentBlockChildren.get({
|
|
144
|
-
path: { document_id: docToken, block_id: parentId },
|
|
145
|
-
});
|
|
146
|
-
const index = children.data?.items?.findIndex(item => item.block_id === blockId);
|
|
147
|
-
|
|
148
|
-
await client.docx.documentBlockChildren.batchDelete({
|
|
149
|
-
path: { document_id: docToken, block_id: parentId },
|
|
150
|
-
data: { start_index: index, end_index: index + 1 },
|
|
151
|
-
});
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## 图片上传
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
import { Readable } from 'stream';
|
|
158
|
-
|
|
159
|
-
// 1. 上传图片到文档
|
|
160
|
-
const uploadRes = await client.drive.media.uploadAll({
|
|
161
|
-
data: {
|
|
162
|
-
file_name: 'image.png',
|
|
163
|
-
parent_type: 'docx_image',
|
|
164
|
-
parent_node: blockId, // 图片 Block 的 block_id
|
|
165
|
-
size: imageBuffer.length,
|
|
166
|
-
file: Readable.from(imageBuffer) as any,
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
if (uploadRes.code !== 0) {
|
|
170
|
-
throw new Error(`Image upload failed [${uploadRes.code}]: ${uploadRes.msg}`);
|
|
171
|
-
}
|
|
172
|
-
const fileToken = uploadRes.data?.file_token;
|
|
173
|
-
|
|
174
|
-
// 2. 替换图片 Block 的内容
|
|
175
|
-
await client.docx.documentBlock.patch({
|
|
176
|
-
path: { document_id: docToken, block_id: blockId },
|
|
177
|
-
data: {
|
|
178
|
-
replace_image: { token: fileToken },
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
## Block 类型参考
|
|
184
|
-
|
|
185
|
-
| block_type | 名称 | 说明 | 可编辑 |
|
|
186
|
-
|------------|------|------|--------|
|
|
187
|
-
| 1 | Page | 文档根节点(包含标题) | 否 |
|
|
188
|
-
| 2 | Text | 纯文本段落 | 是 |
|
|
189
|
-
| 3 | Heading1 | 一级标题 | 是 |
|
|
190
|
-
| 4 | Heading2 | 二级标题 | 是 |
|
|
191
|
-
| 5 | Heading3 | 三级标题 | 是 |
|
|
192
|
-
| 6 | Heading4 | 四级标题 | 是 |
|
|
193
|
-
| 7 | Heading5 | 五级标题 | 是 |
|
|
194
|
-
| 8 | Heading6 | 六级标题 | 是 |
|
|
195
|
-
| 9 | Heading7 | 七级标题 | 是 |
|
|
196
|
-
| 10 | Heading8 | 八级标题 | 是 |
|
|
197
|
-
| 11 | Heading9 | 九级标题 | 是 |
|
|
198
|
-
| 12 | Bullet | 无序列表项 | 是 |
|
|
199
|
-
| 13 | Ordered | 有序列表项 | 是 |
|
|
200
|
-
| 14 | Code | 代码块 | 是 |
|
|
201
|
-
| 15 | Quote | 引用块 | 是 |
|
|
202
|
-
| 16 | Equation | LaTeX 公式 | 部分 |
|
|
203
|
-
| 17 | Todo | 任务/复选框 | 是 |
|
|
204
|
-
| 18 | Bitable | 多维表格嵌入 | 否 |
|
|
205
|
-
| 19 | Callout | 高亮块 | 是 |
|
|
206
|
-
| 20 | ChatCard | 会话卡片嵌入 | 否 |
|
|
207
|
-
| 21 | Diagram | 绘图嵌入 | 否 |
|
|
208
|
-
| 22 | Divider | 分割线 | 否 |
|
|
209
|
-
| 23 | File | 文件附件 | 否 |
|
|
210
|
-
| 24 | Grid | 分栏布局容器 | 否 |
|
|
211
|
-
| 25 | GridColumn | 分栏列 | 否 |
|
|
212
|
-
| 26 | Iframe | 内嵌网页 | 否 |
|
|
213
|
-
| 27 | Image | 图片 | 部分(replace_image) |
|
|
214
|
-
| 28 | ISV | 第三方小组件 | 否 |
|
|
215
|
-
| 29 | MindnoteBlock | 思维导图嵌入 | 否 |
|
|
216
|
-
| 30 | Sheet | 电子表格嵌入 | 否 |
|
|
217
|
-
| 31 | Table | 表格 | 部分(仅读取,无法通过 API 创建) |
|
|
218
|
-
| 32 | TableCell | 表格单元格 | 是 |
|
|
219
|
-
| 33 | View | 视图嵌入 | 否 |
|
|
220
|
-
| 34 | Undefined | 未知类型 | 否 |
|
|
221
|
-
| 35 | QuoteContainer | 引用容器 | 否 |
|
|
222
|
-
| 36 | Task | 飞书任务集成 | 否 |
|
|
223
|
-
| 37 | OKR | OKR 集成 | 否 |
|
|
224
|
-
| 38 | OKRObjective | OKR 目标 | 否 |
|
|
225
|
-
| 39 | OKRKeyResult | OKR 关键结果 | 否 |
|
|
226
|
-
| 40 | OKRProgress | OKR 进展 | 否 |
|
|
227
|
-
| 41 | AddOns | 扩展块 | 否 |
|
|
228
|
-
| 42 | JiraIssue | Jira Issue 嵌入 | 否 |
|
|
229
|
-
| 43 | WikiCatalog | 知识库目录 | 否 |
|
|
230
|
-
| 44 | Board | 画板嵌入 | 否 |
|
|
231
|
-
| 45 | Agenda | 议程块 | 否 |
|
|
232
|
-
| 46 | AgendaItem | 议程项 | 否 |
|
|
233
|
-
| 47 | AgendaItemTitle | 议程项标题 | 否 |
|
|
234
|
-
| 48 | SyncedBlock | 同步块引用 | 否 |
|
|
235
|
-
|
|
236
|
-
> 可编辑的文本类 block(2-17, 19)通过 `documentBlock.patch()` 的 `update_text_elements` 更新;容器类 block(24, 25, 35)需编辑其子 block。
|
|
237
|
-
|
|
238
|
-
## Markdown 写入限制
|
|
239
|
-
|
|
240
|
-
- **表格不支持**:Markdown 中的表格无法通过 `document.convert()` → `documentBlockChildren.create()` 创建(error 1770029)
|
|
241
|
-
- 支持的 Markdown 元素:标题、列表、代码块、引用、链接、图片(`` 自动上传)、加粗/斜体/删除线
|
|
242
|
-
|
|
243
|
-
## Common Mistakes
|
|
244
|
-
|
|
245
|
-
| 错误 | 正确做法 |
|
|
246
|
-
|------|----------|
|
|
247
|
-
| 直接用 wiki token 调用 docx API | Wiki 页面需先通过 `wiki.space.getNode()` 获取 `obj_token`,再用 `obj_token` 作为 `doc_token` |
|
|
248
|
-
| 写入时未清空旧内容导致重复 | 写入前先调用 `documentBlockChildren.batchDelete()` 清空 |
|
|
249
|
-
| 删除 Block 传 block_id 而非 index | `batchDelete` 使用 `start_index` 和 `end_index`,非 block_id |
|
|
250
|
-
| 尝试通过 API 创建表格 Block | Table block 无法通过 `documentBlockChildren.create()` 创建 |
|
|
251
|
-
| `document.convert()` 缺少权限 | 需要 `docx:document.block:convert` 权限 |
|
|
252
|
-
| 图片上传 parent_type 填错 | 文档图片必须用 `docx_image`,不是 `doc_image` |
|
|
253
|
-
| 更新文本时覆盖了富文本样式 | `update_text_elements` 会替换整个文本内容,包括样式 |
|
|
254
|
-
| 并发写入同一文档 | 飞书文档不支持并发写入,需串行操作 |
|
|
255
|
-
| 忘记检查 `res.code !== 0` | 所有 API 调用都需检查返回码 |
|
|
256
|
-
| 清空文档时删除了 Page block | `block_type !== 1` 的才能删除,Page block 是根节点 |
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# 云空间 (Drive)
|
|
2
|
-
|
|
3
|
-
> 开放平台文档(Markdown 版):https://open.larkoffice.com/document/server-docs/docs/drive-v1/introduction
|
|
4
|
-
|
|
5
|
-
使用 `@larksuiteoapi/node-sdk` 在 NestJS 中管理飞书云空间文件和文件夹。
|
|
6
|
-
|
|
7
|
-
## 所需权限
|
|
8
|
-
|
|
9
|
-
| 权限标识 | 说明 |
|
|
10
|
-
|----------|------|
|
|
11
|
-
| `drive:drive` | 读写云空间(创建、移动、删除) |
|
|
12
|
-
| `drive:drive:readonly` | 只读云空间(列出、查看) |
|
|
13
|
-
|
|
14
|
-
## 从 URL 提取 Token
|
|
15
|
-
|
|
16
|
-
文件夹 URL 格式:`https://xxx.feishu.cn/drive/folder/{folder_token}`
|
|
17
|
-
|
|
18
|
-
```typescript
|
|
19
|
-
const url = 'https://xxx.feishu.cn/drive/folder/ABC123';
|
|
20
|
-
const folderToken = new URL(url).pathname.split('/drive/folder/')[1]; // 'ABC123'
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## 列出文件夹内容
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
// 列出指定文件夹
|
|
27
|
-
const res = await client.drive.file.list({
|
|
28
|
-
params: { folder_token: 'fldcnXXX' },
|
|
29
|
-
});
|
|
30
|
-
const files = res.data?.files ?? [];
|
|
31
|
-
// files: [{token, name, type, url, created_time, modified_time, owner_id}, ...]
|
|
32
|
-
|
|
33
|
-
// 列出根目录(不传 folder_token)
|
|
34
|
-
const rootRes = await client.drive.file.list({
|
|
35
|
-
params: {},
|
|
36
|
-
});
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## 创建文件夹
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
const res = await client.drive.file.createFolder({
|
|
43
|
-
data: {
|
|
44
|
-
name: '新文件夹',
|
|
45
|
-
folder_token: 'fldcnXXX', // 父文件夹 token
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
const folderToken = res.data?.token;
|
|
49
|
-
const folderUrl = res.data?.url;
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## 移动文件
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
const res = await client.drive.file.move({
|
|
56
|
-
path: { file_token: 'ABC123' },
|
|
57
|
-
data: {
|
|
58
|
-
type: 'docx', // 文件类型
|
|
59
|
-
folder_token: 'fldcnXXX', // 目标文件夹
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## 删除文件
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
const res = await client.drive.file.delete({
|
|
68
|
-
path: { file_token: 'ABC123' },
|
|
69
|
-
params: {
|
|
70
|
-
type: 'docx', // 文件类型
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## 文件类型参考
|
|
76
|
-
|
|
77
|
-
| 类型 | 说明 |
|
|
78
|
-
|------|------|
|
|
79
|
-
| `doc` | 旧版文档 |
|
|
80
|
-
| `docx` | 新版文档 |
|
|
81
|
-
| `sheet` | 电子表格 |
|
|
82
|
-
| `bitable` | 多维表格 |
|
|
83
|
-
| `folder` | 文件夹 |
|
|
84
|
-
| `file` | 上传的文件 |
|
|
85
|
-
| `mindnote` | 思维导图 |
|
|
86
|
-
| `shortcut` | 快捷方式 |
|
|
87
|
-
|
|
88
|
-
## 机器人根文件夹限制
|
|
89
|
-
|
|
90
|
-
飞书机器人使用 `tenant_access_token`,没有自己的「我的空间」根文件夹。这意味着:
|
|
91
|
-
|
|
92
|
-
- 不指定 `folder_token` 创建文件夹会失败(400 错误)
|
|
93
|
-
- 机器人只能访问**已共享给它**的文件和文件夹
|
|
94
|
-
- **解决方案**:用户先手动创建一个文件夹并共享给机器人,机器人就可以在其中创建子文件夹和文件
|
|
95
|
-
|
|
96
|
-
## Common Mistakes
|
|
97
|
-
|
|
98
|
-
| 错误 | 正确做法 |
|
|
99
|
-
|------|----------|
|
|
100
|
-
| 机器人不指定 folder_token 创建文件夹 | 必须指定一个已共享给机器人的 folder_token |
|
|
101
|
-
| move/delete 时 type 参数搞错 | type 必须与文件实际类型一致 |
|
|
102
|
-
| 用 folder_token 当 file_token | `folder_token` 用于列出目录,`file_token` 用于移动/删除 |
|
|
103
|
-
| 文件不在机器人可访问范围内 | 需先将文件/文件夹共享给机器人应用 |
|