@lark-apaas/coding-steering 0.1.6-alpha.0 → 0.1.6-alpha.2
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/nestjs-react-fullstack/skills/.gitkeep +0 -0
- package/steering/nestjs-react-fullstack/tech.md +21 -0
- package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +0 -122
- package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +0 -139
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +0 -628
- package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +0 -246
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +0 -707
- package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -270
- package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +0 -214
- package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +0 -163
- package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +0 -309
- package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +0 -190
- package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +0 -160
- package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +0 -256
- package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +0 -103
- package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +0 -198
- package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +0 -128
- package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +0 -207
- package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +0 -164
- package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +0 -90
- package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +0 -164
- package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +0 -267
- package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +0 -452
- package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +0 -300
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
# 消息 (Messaging)
|
|
2
|
-
|
|
3
|
-
> 开放平台文档(Markdown 版):https://open.larkoffice.com/document/server-docs/im-v1/introduction.md
|
|
4
|
-
|
|
5
|
-
使用 `@larksuiteoapi/node-sdk` 在 NestJS 中发送和回复飞书消息。
|
|
6
|
-
|
|
7
|
-
## 所需权限
|
|
8
|
-
|
|
9
|
-
| 权限标识 | 说明 |
|
|
10
|
-
|----------|------|
|
|
11
|
-
| `im:message:send_as_bot` | 以应用身份发送消息 |
|
|
12
|
-
|
|
13
|
-
## 发送消息
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
// 发送文本消息
|
|
17
|
-
const res = await client.im.message.create({
|
|
18
|
-
params: { receive_id_type: 'chat_id' },
|
|
19
|
-
data: {
|
|
20
|
-
receive_id: 'oc_xxx',
|
|
21
|
-
content: JSON.stringify({ text: '你好,这是一条测试消息' }),
|
|
22
|
-
msg_type: 'text',
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
if (res.code !== 0) throw new Error(`[${res.code}] ${res.msg}`);
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### receive_id_type 与 ID 前缀对应关系
|
|
29
|
-
|
|
30
|
-
| receive_id_type | ID 前缀 | 说明 |
|
|
31
|
-
|-----------------|---------|------|
|
|
32
|
-
| `chat_id` | `oc_` | 群聊 ID |
|
|
33
|
-
| `open_id` | `ou_` | 用户 open_id |
|
|
34
|
-
| `union_id` | `on_` | 用户 union_id |
|
|
35
|
-
| `user_id` | 无固定前缀 | 用户 user_id |
|
|
36
|
-
| `email` | - | 用户邮箱 |
|
|
37
|
-
|
|
38
|
-
## 发送富文本消息
|
|
39
|
-
|
|
40
|
-
```typescript
|
|
41
|
-
await client.im.message.create({
|
|
42
|
-
params: { receive_id_type: 'chat_id' },
|
|
43
|
-
data: {
|
|
44
|
-
receive_id: 'oc_xxx',
|
|
45
|
-
content: JSON.stringify({
|
|
46
|
-
zh_cn: {
|
|
47
|
-
title: '项目更新',
|
|
48
|
-
content: [
|
|
49
|
-
[
|
|
50
|
-
{ tag: 'text', text: '版本 2.0 已发布,主要更新:' },
|
|
51
|
-
{ tag: 'a', text: '查看详情', href: 'https://example.com' },
|
|
52
|
-
],
|
|
53
|
-
],
|
|
54
|
-
},
|
|
55
|
-
}),
|
|
56
|
-
msg_type: 'post',
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## 发送卡片消息
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
// 方式 1:JSON 卡片
|
|
65
|
-
await client.im.message.create({
|
|
66
|
-
params: { receive_id_type: 'chat_id' },
|
|
67
|
-
data: {
|
|
68
|
-
receive_id: 'oc_xxx',
|
|
69
|
-
content: JSON.stringify({
|
|
70
|
-
header: {
|
|
71
|
-
template: 'blue',
|
|
72
|
-
title: { content: '卡片标题', tag: 'plain_text' },
|
|
73
|
-
},
|
|
74
|
-
elements: [
|
|
75
|
-
{ tag: 'markdown', content: '**进度更新**\n- 前端:80%\n- 后端:60%' },
|
|
76
|
-
{
|
|
77
|
-
tag: 'action',
|
|
78
|
-
actions: [
|
|
79
|
-
{ tag: 'button', text: { tag: 'plain_text', content: '确认' }, type: 'primary' },
|
|
80
|
-
],
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
}),
|
|
84
|
-
msg_type: 'interactive',
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// 方式 2:SDK 内置默认卡片(快速使用)
|
|
89
|
-
await client.im.message.create({
|
|
90
|
-
params: { receive_id_type: 'chat_id' },
|
|
91
|
-
data: {
|
|
92
|
-
receive_id: 'oc_xxx',
|
|
93
|
-
content: lark.messageCard.defaultCard({ title: '标题', content: '内容' }),
|
|
94
|
-
msg_type: 'interactive',
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// 方式 3:卡片模板(推荐,在卡片搭建工具中配置后使用 template_id)
|
|
99
|
-
await client.im.message.createByCard({
|
|
100
|
-
params: { receive_id_type: 'chat_id' },
|
|
101
|
-
data: {
|
|
102
|
-
receive_id: 'oc_xxx',
|
|
103
|
-
template_id: 'your_template_id',
|
|
104
|
-
template_variable: { title: '标题', content: '正文内容' },
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## 回复消息
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
await client.im.message.reply({
|
|
113
|
-
path: { message_id: 'om_xxx' },
|
|
114
|
-
data: {
|
|
115
|
-
content: JSON.stringify({ text: '收到,我马上处理' }),
|
|
116
|
-
msg_type: 'text',
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## 编辑消息(24h 内)
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
await client.im.message.update({
|
|
125
|
-
path: { message_id: 'om_xxx' },
|
|
126
|
-
data: {
|
|
127
|
-
content: JSON.stringify({ text: '已更新的消息内容' }),
|
|
128
|
-
msg_type: 'text',
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## 更新卡片消息
|
|
134
|
-
|
|
135
|
-
```typescript
|
|
136
|
-
await client.im.message.patch({
|
|
137
|
-
path: { message_id: 'om_xxx' },
|
|
138
|
-
data: {
|
|
139
|
-
content: JSON.stringify({
|
|
140
|
-
elements: [{ tag: 'markdown', content: '已更新的卡片内容' }],
|
|
141
|
-
}),
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
## 消息内容格式
|
|
147
|
-
|
|
148
|
-
### text(文本)
|
|
149
|
-
|
|
150
|
-
```json
|
|
151
|
-
{"text": "你好,这是一条消息"}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
支持 @ 用户:`{"text": "<at user_id=\"ou_xxx\">张三</at> 请查看"}`
|
|
155
|
-
|
|
156
|
-
### post(富文本)
|
|
157
|
-
|
|
158
|
-
```json
|
|
159
|
-
{
|
|
160
|
-
"zh_cn": {
|
|
161
|
-
"title": "标题",
|
|
162
|
-
"content": [
|
|
163
|
-
[
|
|
164
|
-
{"tag": "text", "text": "普通文本"},
|
|
165
|
-
{"tag": "a", "text": "链接文字", "href": "https://example.com"},
|
|
166
|
-
{"tag": "at", "user_id": "ou_xxx"}
|
|
167
|
-
]
|
|
168
|
-
]
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### interactive(卡片)
|
|
174
|
-
|
|
175
|
-
```json
|
|
176
|
-
{
|
|
177
|
-
"header": {
|
|
178
|
-
"template": "blue",
|
|
179
|
-
"title": {"content": "卡片标题", "tag": "plain_text"}
|
|
180
|
-
},
|
|
181
|
-
"elements": [
|
|
182
|
-
{"tag": "markdown", "content": "**标题**\n内容文本"},
|
|
183
|
-
{
|
|
184
|
-
"tag": "action",
|
|
185
|
-
"actions": [
|
|
186
|
-
{"tag": "button", "text": {"tag": "plain_text", "content": "按钮"}, "type": "primary"}
|
|
187
|
-
]
|
|
188
|
-
}
|
|
189
|
-
]
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
## 使用限制
|
|
194
|
-
|
|
195
|
-
- 向同一用户发消息限频:**5 QPS**
|
|
196
|
-
- 向同一群组发消息限频:群内机器人共享 **5 QPS**
|
|
197
|
-
- 文本消息最大 **150 KB**
|
|
198
|
-
- 卡片/富文本消息最大 **30 KB**
|
|
199
|
-
|
|
200
|
-
## Common Mistakes
|
|
201
|
-
|
|
202
|
-
| 错误 | 正确做法 |
|
|
203
|
-
|------|----------|
|
|
204
|
-
| `content` 传对象 | 必须 `JSON.stringify({text: 'hello'})` |
|
|
205
|
-
| 群聊 ID 用 `open_id` 类型 | `oc_` 开头的是 `chat_id` |
|
|
206
|
-
| 富文本 content 不是二维数组 | `content: [[{tag:'text', text:'...'}]]` 外层是行数组 |
|
|
207
|
-
| 忘记开启机器人能力 | 应用能力 → 添加机器人 |
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
# OAuth 用户授权
|
|
2
|
-
|
|
3
|
-
> 开放平台文档(Markdown 版):https://open.larkoffice.com/document/ukTMukTMukTM/uMTNz4yM1MjLzUzM
|
|
4
|
-
|
|
5
|
-
默认的 tenant_access_token 以应用身份调用 API。
|
|
6
|
-
部分场景(个人日历、搜索联系人)需要 user_access_token 代表用户操作。
|
|
7
|
-
|
|
8
|
-
## 授权流程
|
|
9
|
-
|
|
10
|
-
### 1. 构造 redirect_uri 并重定向用户到授权页面
|
|
11
|
-
|
|
12
|
-
```typescript
|
|
13
|
-
// 域名和路径根从运行时获取
|
|
14
|
-
const redirectUri = `https://${req.hostname}${process.env.CLIENT_BASE_PATH}/feishu/oauth/callback`;
|
|
15
|
-
const authorizeUrl = `https://open.feishu.cn/open-apis/authen/v1/authorize?app_id=${FEISHU_APP_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}`;
|
|
16
|
-
// 返回 302 重定向或将 URL 返回给前端
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
### 2. 回调中用 code 换取 token
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
const userId = req.userContext.userId; // 当前登录用户
|
|
23
|
-
await client.userAccessToken.initWithCode({ [userId]: code });
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
### 3. 获取 token(自动 refresh)
|
|
27
|
-
|
|
28
|
-
```typescript
|
|
29
|
-
const token = await client.userAccessToken.get(userId);
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### 4. 携带 user_access_token 调用 API
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
await client.calendar.calendarEvent.create(
|
|
36
|
-
{ data: { summary: '我的日程', ... } },
|
|
37
|
-
lark.withUserAccessToken(token),
|
|
38
|
-
);
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Token 持久化与用户关联
|
|
42
|
-
|
|
43
|
-
SDK 默认将 token 存在内存,重启后丢失。生产环境需持久化到数据库。
|
|
44
|
-
|
|
45
|
-
### 建表
|
|
46
|
-
|
|
47
|
-
```sql
|
|
48
|
-
CREATE TABLE feishu_user_token (
|
|
49
|
-
user_id VARCHAR(64) PRIMARY KEY, -- req.userContext.userId
|
|
50
|
-
open_id VARCHAR(64) NOT NULL, -- 飞书用户标识
|
|
51
|
-
access_token TEXT NOT NULL,
|
|
52
|
-
refresh_token TEXT NOT NULL,
|
|
53
|
-
token_expire_at TIMESTAMPTZ NOT NULL,
|
|
54
|
-
refresh_expire_at TIMESTAMPTZ NOT NULL,
|
|
55
|
-
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
56
|
-
);
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### FeishuOAuthService 示例
|
|
60
|
-
|
|
61
|
-
回调保存和 token 刷新的完整 NestJS Service 实现:
|
|
62
|
-
|
|
63
|
-
- `access_token` 有效期 2 小时,`refresh_token` 有效期 30 天
|
|
64
|
-
- 每次 refresh 后,服务端会同时返回**新的 `refresh_token`**(token rotation),必须用新值覆盖旧值,否则下次刷新会失败
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
import { Injectable, Inject } from '@nestjs/common';
|
|
68
|
-
import { DRIZZLE_DATABASE, type PostgresJsDatabase } from '@lark-apaas/fullstack-nestjs-core';
|
|
69
|
-
import { eq } from 'drizzle-orm';
|
|
70
|
-
import { feishuUserToken } from '@server/database/schema';
|
|
71
|
-
|
|
72
|
-
@Injectable()
|
|
73
|
-
export class FeishuOAuthService {
|
|
74
|
-
private readonly EXPIRY_BUFFER_MS = 3 * 60 * 1000; // 预留 3 分钟 buffer
|
|
75
|
-
|
|
76
|
-
constructor(
|
|
77
|
-
private readonly feishuService: FeishuService,
|
|
78
|
-
@Inject(DRIZZLE_DATABASE) private readonly db: PostgresJsDatabase,
|
|
79
|
-
) {}
|
|
80
|
-
|
|
81
|
-
/** 回调中用 code 换取 token 并持久化(Step 2 之后调用) */
|
|
82
|
-
async handleCallback(userId: string, code: string) {
|
|
83
|
-
const client = this.feishuService.getClient();
|
|
84
|
-
const tokenRes = await client.authen.oidcAccessToken.create({
|
|
85
|
-
data: { grant_type: 'authorization_code', code },
|
|
86
|
-
});
|
|
87
|
-
if (tokenRes.code !== 0 || !tokenRes.data) {
|
|
88
|
-
throw new Error(`Feishu OAuth token error [${tokenRes.code}]: ${tokenRes.msg}`);
|
|
89
|
-
}
|
|
90
|
-
const { access_token, refresh_token, open_id, expires_in, refresh_expires_in } = tokenRes.data;
|
|
91
|
-
const now = Date.now();
|
|
92
|
-
await this.db.insert(feishuUserToken).values({
|
|
93
|
-
userId,
|
|
94
|
-
openId: open_id,
|
|
95
|
-
accessToken: access_token,
|
|
96
|
-
refreshToken: refresh_token,
|
|
97
|
-
tokenExpireAt: new Date(now + expires_in * 1000),
|
|
98
|
-
refreshExpireAt: new Date(now + refresh_expires_in * 1000),
|
|
99
|
-
}).onConflictDoUpdate({
|
|
100
|
-
target: feishuUserToken.userId,
|
|
101
|
-
set: {
|
|
102
|
-
openId: open_id,
|
|
103
|
-
accessToken: access_token,
|
|
104
|
-
refreshToken: refresh_token,
|
|
105
|
-
tokenExpireAt: new Date(now + expires_in * 1000),
|
|
106
|
-
refreshExpireAt: new Date(now + refresh_expires_in * 1000),
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** 获取有效的 user_access_token,自动刷新 */
|
|
112
|
-
async getValidToken(userId: string): Promise<string> {
|
|
113
|
-
const [stored] = await this.db
|
|
114
|
-
.select()
|
|
115
|
-
.from(feishuUserToken)
|
|
116
|
-
.where(eq(feishuUserToken.userId, userId));
|
|
117
|
-
if (!stored) throw new Error('用户未授权飞书,请先完成 OAuth 授权');
|
|
118
|
-
|
|
119
|
-
const now = Date.now();
|
|
120
|
-
|
|
121
|
-
// 1. access_token 未过期 → 直接返回
|
|
122
|
-
if (stored.tokenExpireAt.getTime() - now > this.EXPIRY_BUFFER_MS) {
|
|
123
|
-
return stored.accessToken;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// 2. access_token 过期,但 refresh_token 未过期 → 刷新
|
|
127
|
-
if (stored.refreshExpireAt.getTime() - now > this.EXPIRY_BUFFER_MS) {
|
|
128
|
-
const client = this.feishuService.getClient();
|
|
129
|
-
const res = await client.authen.oidcRefreshAccessToken.create({
|
|
130
|
-
data: { grant_type: 'refresh_token', refresh_token: stored.refreshToken },
|
|
131
|
-
});
|
|
132
|
-
if (res.code !== 0 || !res.data) {
|
|
133
|
-
throw new Error(`Feishu OAuth refresh error [${res.code}]: ${res.msg}`);
|
|
134
|
-
}
|
|
135
|
-
const { access_token, refresh_token, expires_in, refresh_expires_in } = res.data;
|
|
136
|
-
|
|
137
|
-
// 必须同时更新 access_token 和 refresh_token(token rotation)
|
|
138
|
-
await this.db.update(feishuUserToken)
|
|
139
|
-
.set({
|
|
140
|
-
accessToken: access_token,
|
|
141
|
-
refreshToken: refresh_token,
|
|
142
|
-
tokenExpireAt: new Date(now + expires_in * 1000),
|
|
143
|
-
refreshExpireAt: new Date(now + refresh_expires_in * 1000),
|
|
144
|
-
})
|
|
145
|
-
.where(eq(feishuUserToken.userId, userId));
|
|
146
|
-
return access_token;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 3. refresh_token 也过期 → 需要用户重新授权(回到第 1 步)
|
|
150
|
-
throw new Error('飞书授权已过期,请重新授权');
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
#### 在业务代码中使用
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
// 在 Controller 或其他 Service 中注入 FeishuOAuthService
|
|
159
|
-
const token = await this.feishuOAuthService.getValidToken(req.userContext.userId);
|
|
160
|
-
await this.feishuService.getClient().calendar.calendarEvent.create(
|
|
161
|
-
{ data: { summary: '我的日程', ... } },
|
|
162
|
-
lark.withUserAccessToken(token),
|
|
163
|
-
);
|
|
164
|
-
```
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# 权限管理 (Permission)
|
|
2
|
-
|
|
3
|
-
> 开放平台文档(Markdown 版):https://open.larkoffice.com/document/server-docs/docs/permission/overview
|
|
4
|
-
|
|
5
|
-
使用 `@larksuiteoapi/node-sdk` 在 NestJS 中管理飞书云文档的协作者权限。
|
|
6
|
-
|
|
7
|
-
## 所需权限
|
|
8
|
-
|
|
9
|
-
| 权限标识 | 说明 |
|
|
10
|
-
|----------|------|
|
|
11
|
-
| `drive:permission` | 管理文档/文件的协作者权限 |
|
|
12
|
-
|
|
13
|
-
> **敏感操作警告**:权限管理涉及文档访问控制,添加/移除协作者会直接影响用户的文档可见性。操作前请确认目标对象和权限级别。
|
|
14
|
-
|
|
15
|
-
## 列出协作者
|
|
16
|
-
|
|
17
|
-
```typescript
|
|
18
|
-
const res = await client.drive.permissionMember.list({
|
|
19
|
-
path: { token: 'ABC123' },
|
|
20
|
-
params: { type: 'docx' },
|
|
21
|
-
});
|
|
22
|
-
const members = res.data?.items ?? [];
|
|
23
|
-
// members: [{member_type, member_id, perm, name}, ...]
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## 添加协作者
|
|
27
|
-
|
|
28
|
-
```typescript
|
|
29
|
-
const res = await client.drive.permissionMember.create({
|
|
30
|
-
path: { token: 'ABC123' },
|
|
31
|
-
params: { type: 'docx', need_notification: false },
|
|
32
|
-
data: {
|
|
33
|
-
member_type: 'email',
|
|
34
|
-
member_id: 'user@example.com',
|
|
35
|
-
perm: 'edit',
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## 移除协作者
|
|
41
|
-
|
|
42
|
-
```typescript
|
|
43
|
-
await client.drive.permissionMember.delete({
|
|
44
|
-
path: { token: 'ABC123', member_id: 'user@example.com' },
|
|
45
|
-
params: { type: 'docx', member_type: 'email' },
|
|
46
|
-
});
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Token 类型参考
|
|
50
|
-
|
|
51
|
-
| 类型 | 说明 |
|
|
52
|
-
|------|------|
|
|
53
|
-
| `doc` | 旧版文档 |
|
|
54
|
-
| `docx` | 新版文档 |
|
|
55
|
-
| `sheet` | 电子表格 |
|
|
56
|
-
| `bitable` | 多维表格 |
|
|
57
|
-
| `folder` | 文件夹 |
|
|
58
|
-
| `file` | 上传的文件 |
|
|
59
|
-
| `wiki` | 知识库节点 |
|
|
60
|
-
| `mindnote` | 思维导图 |
|
|
61
|
-
|
|
62
|
-
## 成员类型参考
|
|
63
|
-
|
|
64
|
-
| 类型 | 说明 |
|
|
65
|
-
|------|------|
|
|
66
|
-
| `email` | 邮箱地址 |
|
|
67
|
-
| `openid` | 用户 open_id |
|
|
68
|
-
| `userid` | 用户 user_id |
|
|
69
|
-
| `unionid` | 用户 union_id |
|
|
70
|
-
| `openchat` | 群聊 open_id |
|
|
71
|
-
| `opendepartmentid` | 部门 open_id |
|
|
72
|
-
| `groupid` | 用户组 ID |
|
|
73
|
-
| `wikispaceid` | 知识空间 ID |
|
|
74
|
-
|
|
75
|
-
## 权限级别参考
|
|
76
|
-
|
|
77
|
-
| 权限值 | 说明 |
|
|
78
|
-
|--------|------|
|
|
79
|
-
| `view` | 仅查看 |
|
|
80
|
-
| `edit` | 可编辑 |
|
|
81
|
-
| `full_access` | 完全访问(可管理权限) |
|
|
82
|
-
|
|
83
|
-
## Common Mistakes
|
|
84
|
-
|
|
85
|
-
| 错误 | 正确做法 |
|
|
86
|
-
|------|----------|
|
|
87
|
-
| token 类型与文件实际类型不匹配 | `type` 参数必须与文件实际类型一致(docx/sheet/bitable 等) |
|
|
88
|
-
| 用 wiki URL 的 token 直接操作权限 | Wiki 节点需用 `wiki` 类型,或先获取 `obj_token` 用对应类型 |
|
|
89
|
-
| 添加协作者时成员不存在 | 确认 member_id 正确,email 需要是飞书注册邮箱 |
|
|
90
|
-
| 移除自身的 full_access 权限 | 文档至少需要一个管理员,避免移除最后一个 full_access 成员 |
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
# 知识库 (Wiki)
|
|
2
|
-
|
|
3
|
-
> 开放平台文档(Markdown 版):https://open.larkoffice.com/document/server-docs/docs/wiki-v2/wiki-overview
|
|
4
|
-
|
|
5
|
-
使用 `@larksuiteoapi/node-sdk` 在 NestJS 中操作飞书知识库。
|
|
6
|
-
|
|
7
|
-
## 所需权限
|
|
8
|
-
|
|
9
|
-
| 权限标识 | 说明 |
|
|
10
|
-
|----------|------|
|
|
11
|
-
| `wiki:wiki` | 读写知识库 |
|
|
12
|
-
| `wiki:wiki:readonly` | 只读知识库 |
|
|
13
|
-
|
|
14
|
-
> 机器人需被添加为知识空间成员才能访问:知识空间 → 设置 → 成员管理 → 添加机器人。
|
|
15
|
-
|
|
16
|
-
## 从 URL 提取 Token
|
|
17
|
-
|
|
18
|
-
URL 格式:`https://xxx.feishu.cn/wiki/{token}`
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
const url = 'https://xxx.feishu.cn/wiki/ABC123def';
|
|
22
|
-
const token = new URL(url).pathname.split('/wiki/')[1]; // 'ABC123def'
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## 列出知识空间
|
|
26
|
-
|
|
27
|
-
```typescript
|
|
28
|
-
const res = await client.wiki.space.list({});
|
|
29
|
-
const spaces = res.data?.items ?? [];
|
|
30
|
-
// spaces: [{space_id, name, description, visibility}, ...]
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
> 如果返回空列表,说明机器人未被添加到任何知识空间。
|
|
34
|
-
|
|
35
|
-
## 列出节点
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
// 列出空间根节点
|
|
39
|
-
const res = await client.wiki.spaceNode.list({
|
|
40
|
-
path: { space_id: '7xxx' },
|
|
41
|
-
});
|
|
42
|
-
const nodes = res.data?.items ?? [];
|
|
43
|
-
// nodes: [{node_token, obj_token, obj_type, title, has_child}, ...]
|
|
44
|
-
|
|
45
|
-
// 列出子节点
|
|
46
|
-
const childRes = await client.wiki.spaceNode.list({
|
|
47
|
-
path: { space_id: '7xxx' },
|
|
48
|
-
params: { parent_node_token: 'wikcnXXX' },
|
|
49
|
-
});
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## 获取节点详情
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
const res = await client.wiki.space.getNode({
|
|
56
|
-
params: { token: 'ABC123def' }, // 从 URL 提取的 token
|
|
57
|
-
});
|
|
58
|
-
const node = res.data?.node;
|
|
59
|
-
// node: {node_token, space_id, obj_token, obj_type, title, parent_node_token, has_child, creator}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
> **关键**:返回的 `obj_token` 是实际文档/表格的 token,需用它来调用 docx/bitable 等 API。
|
|
63
|
-
|
|
64
|
-
## 创建节点
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
const res = await client.wiki.spaceNode.create({
|
|
68
|
-
path: { space_id: '7xxx' },
|
|
69
|
-
data: {
|
|
70
|
-
obj_type: 'docx', // 节点类型
|
|
71
|
-
node_type: 'origin', // 固定值
|
|
72
|
-
title: '新页面',
|
|
73
|
-
// parent_node_token: 'wikcnXXX', // 可选:父节点
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
const node = res.data?.node;
|
|
77
|
-
// node: {node_token, obj_token, obj_type, title}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### obj_type 取值
|
|
81
|
-
|
|
82
|
-
| 值 | 说明 |
|
|
83
|
-
|------|------|
|
|
84
|
-
| `docx` | 新版文档(默认) |
|
|
85
|
-
| `doc` | 旧版文档 |
|
|
86
|
-
| `sheet` | 电子表格 |
|
|
87
|
-
| `bitable` | 多维表格 |
|
|
88
|
-
| `mindnote` | 思维导图 |
|
|
89
|
-
| `file` | 文件 |
|
|
90
|
-
| `slides` | 幻灯片 |
|
|
91
|
-
|
|
92
|
-
## 移动节点
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
await client.wiki.spaceNode.move({
|
|
96
|
-
path: { space_id: '7xxx', node_token: 'wikcnXXX' },
|
|
97
|
-
data: {
|
|
98
|
-
target_space_id: '7yyy', // 目标空间(不传则同空间内移动)
|
|
99
|
-
target_parent_token: 'wikcnYYY', // 目标父节点
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## 重命名节点
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
await client.wiki.spaceNode.updateTitle({
|
|
108
|
-
path: { space_id: '7xxx', node_token: 'wikcnXXX' },
|
|
109
|
-
data: { title: '新标题' },
|
|
110
|
-
});
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Wiki-Doc 工作流(关键)
|
|
114
|
-
|
|
115
|
-
知识库页面的内容读写必须通过 docx API,流程:
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
// 1. 获取节点详情 → 拿到 obj_token
|
|
119
|
-
const nodeRes = await client.wiki.space.getNode({
|
|
120
|
-
params: { token: wikiToken },
|
|
121
|
-
});
|
|
122
|
-
const objToken = nodeRes.data?.node?.obj_token;
|
|
123
|
-
|
|
124
|
-
// 2. 用 obj_token 作为 doc_token 读取文档
|
|
125
|
-
const contentRes = await client.docx.document.rawContent({
|
|
126
|
-
path: { document_id: objToken },
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// 3. 用 obj_token 作为 doc_token 写入文档
|
|
130
|
-
const convertRes = await client.docx.document.convert({
|
|
131
|
-
data: { content_type: 'markdown', content: '# 新内容\n\n正文...' },
|
|
132
|
-
});
|
|
133
|
-
await client.docx.documentBlockChildren.create({
|
|
134
|
-
path: { document_id: objToken, block_id: objToken },
|
|
135
|
-
data: { children: convertRes.data?.blocks ?? [] },
|
|
136
|
-
});
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
> **重要**:不要用 `node_token` 或 URL 中的 `token` 直接调用 docx API,必须用 `getNode()` 返回的 `obj_token`。
|
|
140
|
-
|
|
141
|
-
## 搜索不可用
|
|
142
|
-
|
|
143
|
-
Wiki API 不提供搜索功能。获取内容需通过以下方式:
|
|
144
|
-
|
|
145
|
-
- 通过 `spaceNode.list()` 浏览节点树
|
|
146
|
-
- 通过 `space.getNode()` + URL 中的 token 直接查询
|
|
147
|
-
|
|
148
|
-
## 知识库访问设置
|
|
149
|
-
|
|
150
|
-
机器人需要被添加为知识空间成员才能访问:
|
|
151
|
-
|
|
152
|
-
1. 打开知识空间 → 设置 → 成员管理
|
|
153
|
-
2. 添加机器人应用
|
|
154
|
-
3. 参考:https://open.feishu.cn/document/server-docs/docs/wiki-v2/wiki-qa
|
|
155
|
-
|
|
156
|
-
## Common Mistakes
|
|
157
|
-
|
|
158
|
-
| 错误 | 正确做法 |
|
|
159
|
-
|------|----------|
|
|
160
|
-
| 用 wiki URL 中的 token 直接调用 docx API | 必须先 `getNode()` 获取 `obj_token`,再用 `obj_token` 调用 docx API |
|
|
161
|
-
| 列出空间返回空但实际有内容 | 机器人未被添加为空间成员 |
|
|
162
|
-
| 尝试搜索知识库内容 | Wiki API 不支持搜索,只能通过 `list` 浏览或 `getNode` 查询 |
|
|
163
|
-
| 创建节点忘记传 `node_type: 'origin'` | `node_type` 是必填字段,值固定为 `'origin'` |
|
|
164
|
-
| 混淆 `node_token` 和 `obj_token` | `node_token` 是知识库节点标识,`obj_token` 是实际文档标识 |
|