feishu-mcp 0.0.15 → 0.0.16
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/LICENSE +21 -21
- package/README.md +257 -251
- package/dist/cli.js +0 -0
- package/dist/config.js +26 -0
- package/dist/mcp/tools/feishuBlockTools.js +202 -252
- package/dist/mcp/tools/feishuFolderTools.js +22 -28
- package/dist/mcp/tools/feishuTools.js +54 -68
- package/dist/services/feishu.js +495 -0
- package/dist/services/feishuBlockService.js +179 -0
- package/dist/services/feishuBlocks.js +135 -0
- package/dist/services/feishuService.js +475 -0
- package/dist/types/feishuSchema.js +6 -15
- package/package.json +75 -75
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
import { formatErrorMessage } from '../../utils/error.js';
|
|
3
3
|
import { Logger } from '../../utils/logger.js';
|
|
4
|
-
import { DocumentIdSchema,
|
|
5
|
-
// BlockIdSchema,
|
|
6
|
-
SearchKeySchema, WhiteboardIdSchema, DocumentTitleSchema, FolderTokenSchema, } from '../../types/feishuSchema.js';
|
|
4
|
+
import { DocumentIdSchema, BlockIdSchema, SearchKeySchema, WhiteboardIdSchema, } from '../../types/feishuSchema.js';
|
|
7
5
|
/**
|
|
8
6
|
* 注册飞书相关的MCP工具
|
|
9
7
|
* @param server MCP服务器实例
|
|
@@ -12,8 +10,8 @@ SearchKeySchema, WhiteboardIdSchema, DocumentTitleSchema, FolderTokenSchema, } f
|
|
|
12
10
|
export function registerFeishuTools(server, feishuService) {
|
|
13
11
|
// 添加创建飞书文档工具
|
|
14
12
|
server.tool('create_feishu_document', 'Creates a new Feishu document and returns its information. Use this tool when you need to create a document from scratch with a specific title and folder location.', {
|
|
15
|
-
title:
|
|
16
|
-
folderToken:
|
|
13
|
+
title: z.string().describe('Document title (required). This will be displayed in the Feishu document list and document header.'),
|
|
14
|
+
folderToken: z.string().describe('Folder token (required). Specifies where to create the document. Format is an alphanumeric string like "doxcnOu1ZKYH4RtX1Y5XwL5WGRh".'),
|
|
17
15
|
}, async ({ title, folderToken }) => {
|
|
18
16
|
try {
|
|
19
17
|
Logger.info(`开始创建飞书文档,标题: ${title}${folderToken ? `,文件夹Token: ${folderToken}` : ',使用默认文件夹'}`);
|
|
@@ -60,37 +58,31 @@ export function registerFeishuTools(server, feishuService) {
|
|
|
60
58
|
}
|
|
61
59
|
});
|
|
62
60
|
// 添加获取飞书文档内容工具
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// return {
|
|
89
|
-
// content: [{ type: 'text', text: `获取飞书文档内容失败: ${errorMessage}` }],
|
|
90
|
-
// };
|
|
91
|
-
// }
|
|
92
|
-
// },
|
|
93
|
-
// );
|
|
61
|
+
server.tool('get_feishu_document_content', 'Retrieves the plain text content of a Feishu document. Ideal for content analysis, processing, or when you need to extract text without formatting. The content maintains the document structure but without styling. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx) you must first use convert_feishu_wiki_to_document_id tool to obtain a compatible document ID.', {
|
|
62
|
+
documentId: DocumentIdSchema,
|
|
63
|
+
lang: z.number().optional().default(0).describe('Language code (optional). Default is 0 (Chinese). Use 1 for English if available.'),
|
|
64
|
+
}, async ({ documentId, lang }) => {
|
|
65
|
+
try {
|
|
66
|
+
if (!feishuService) {
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: 'text', text: 'Feishu service is not initialized. Please check the configuration' }],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
Logger.info(`开始获取飞书文档内容,文档ID: ${documentId},语言: ${lang}`);
|
|
72
|
+
const content = await feishuService.getDocumentContent(documentId, lang);
|
|
73
|
+
Logger.info(`飞书文档内容获取成功,内容长度: ${content.length}字符`);
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: 'text', text: content }],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
Logger.error(`获取飞书文档内容失败:`, error);
|
|
80
|
+
const errorMessage = formatErrorMessage(error);
|
|
81
|
+
return {
|
|
82
|
+
content: [{ type: 'text', text: `获取飞书文档内容失败: ${errorMessage}` }],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
});
|
|
94
86
|
// 添加获取飞书文档块工具
|
|
95
87
|
server.tool('get_feishu_document_blocks', 'Retrieves the block structure information of a Feishu document. Essential to use before inserting content to understand document structure and determine correct insertion positions. Returns a detailed hierarchy of blocks with their IDs, types, and content. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx) you must first use convert_feishu_wiki_to_document_id tool to obtain a compatible document ID.', {
|
|
96
88
|
documentId: DocumentIdSchema,
|
|
@@ -135,37 +127,31 @@ export function registerFeishuTools(server, feishuService) {
|
|
|
135
127
|
}
|
|
136
128
|
});
|
|
137
129
|
// 添加获取块内容工具
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// return {
|
|
164
|
-
// content: [{ type: 'text', text: `获取飞书块内容失败: ${errorMessage}` }],
|
|
165
|
-
// };
|
|
166
|
-
// }
|
|
167
|
-
// },
|
|
168
|
-
// );
|
|
130
|
+
server.tool('get_feishu_block_content', 'Retrieves the detailed content and structure of a specific block in a Feishu document. Useful for inspecting block properties, formatting, and content, especially before making updates or for debugging purposes. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx) you must first use convert_feishu_wiki_to_document_id tool to obtain a compatible document ID.', {
|
|
131
|
+
documentId: DocumentIdSchema,
|
|
132
|
+
blockId: BlockIdSchema,
|
|
133
|
+
}, async ({ documentId, blockId }) => {
|
|
134
|
+
try {
|
|
135
|
+
if (!feishuService) {
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
Logger.info(`开始获取飞书块内容,文档ID: ${documentId},块ID: ${blockId}`);
|
|
141
|
+
const blockContent = await feishuService.getBlockContent(documentId, blockId);
|
|
142
|
+
Logger.info(`飞书块内容获取成功,块类型: ${blockContent.block_type}`);
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: 'text', text: JSON.stringify(blockContent, null, 2) }],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
Logger.error(`获取飞书块内容失败:`, error);
|
|
149
|
+
const errorMessage = formatErrorMessage(error);
|
|
150
|
+
return {
|
|
151
|
+
content: [{ type: 'text', text: `获取飞书块内容失败: ${errorMessage}` }],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
});
|
|
169
155
|
// 添加搜索文档工具
|
|
170
156
|
server.tool('search_feishu_documents', 'Searches for documents in Feishu. Supports keyword-based search and returns document information including title, type, and owner. Use this tool to find specific content or related documents in your document library.', {
|
|
171
157
|
searchKey: SearchKeySchema,
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import axios, { AxiosError } from "axios";
|
|
2
|
+
import { Logger } from "../server.js";
|
|
3
|
+
export class FeishuService {
|
|
4
|
+
constructor(appId, appSecret) {
|
|
5
|
+
Object.defineProperty(this, "appId", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
value: void 0
|
|
10
|
+
});
|
|
11
|
+
Object.defineProperty(this, "appSecret", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: void 0
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(this, "baseUrl", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: "https://open.feishu.cn/open-apis"
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(this, "accessToken", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: null
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(this, "tokenExpireTime", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: null
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(this, "MAX_TOKEN_LIFETIME", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
value: 2 * 60 * 60 * 1000
|
|
40
|
+
}); // 2小时的毫秒数
|
|
41
|
+
this.appId = appId;
|
|
42
|
+
this.appSecret = appSecret;
|
|
43
|
+
}
|
|
44
|
+
isTokenExpired() {
|
|
45
|
+
if (!this.accessToken || !this.tokenExpireTime)
|
|
46
|
+
return true;
|
|
47
|
+
return Date.now() >= this.tokenExpireTime;
|
|
48
|
+
}
|
|
49
|
+
async getAccessToken() {
|
|
50
|
+
if (this.accessToken && !this.isTokenExpired()) {
|
|
51
|
+
Logger.log('使用现有访问令牌,未过期');
|
|
52
|
+
return this.accessToken;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const url = `${this.baseUrl}/auth/v3/tenant_access_token/internal`;
|
|
56
|
+
const requestData = {
|
|
57
|
+
app_id: this.appId,
|
|
58
|
+
app_secret: this.appSecret,
|
|
59
|
+
};
|
|
60
|
+
Logger.log('开始获取新的访问令牌...');
|
|
61
|
+
Logger.log(`请求URL: ${url}`);
|
|
62
|
+
Logger.log(`请求方法: POST`);
|
|
63
|
+
Logger.log(`请求数据: ${JSON.stringify(requestData, null, 2)}`);
|
|
64
|
+
const response = await axios.post(url, requestData);
|
|
65
|
+
Logger.log(`响应状态码: ${response.status}`);
|
|
66
|
+
Logger.log(`响应头: ${JSON.stringify(response.headers, null, 2)}`);
|
|
67
|
+
Logger.log(`响应数据: ${JSON.stringify(response.data, null, 2)}`);
|
|
68
|
+
if (response.data.code !== 0) {
|
|
69
|
+
Logger.error(`获取访问令牌失败,错误码: ${response.data.code}, 错误信息: ${response.data.msg}`);
|
|
70
|
+
throw {
|
|
71
|
+
status: response.status,
|
|
72
|
+
err: response.data.msg || "Unknown error",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
this.accessToken = response.data.tenant_access_token;
|
|
76
|
+
this.tokenExpireTime = Date.now() + Math.min(response.data.expire * 1000, this.MAX_TOKEN_LIFETIME);
|
|
77
|
+
Logger.log(`成功获取新的访问令牌,有效期: ${response.data.expire} 秒`);
|
|
78
|
+
return this.accessToken; // 使用类型断言确保返回类型为string
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof AxiosError && error.response) {
|
|
82
|
+
Logger.error(`获取访问令牌请求失败:`);
|
|
83
|
+
Logger.error(`状态码: ${error.response.status}`);
|
|
84
|
+
Logger.error(`响应头: ${JSON.stringify(error.response.headers, null, 2)}`);
|
|
85
|
+
Logger.error(`响应数据: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
86
|
+
throw {
|
|
87
|
+
status: error.response.status,
|
|
88
|
+
err: error.response.data?.msg || "Unknown error",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
Logger.error('获取访问令牌时发生未知错误:', error);
|
|
92
|
+
throw new Error("Failed to get Feishu access token");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async request(endpoint, method = "GET", data) {
|
|
96
|
+
try {
|
|
97
|
+
const accessToken = await this.getAccessToken();
|
|
98
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
99
|
+
const headers = {
|
|
100
|
+
Authorization: `Bearer ${accessToken}`,
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
};
|
|
103
|
+
Logger.log('准备发送请求:');
|
|
104
|
+
Logger.log(`请求URL: ${url}`);
|
|
105
|
+
Logger.log(`请求方法: ${method}`);
|
|
106
|
+
Logger.log(`请求头: ${JSON.stringify(headers, null, 2)}`);
|
|
107
|
+
if (data) {
|
|
108
|
+
Logger.log(`请求数据: ${JSON.stringify(data, null, 2)}`);
|
|
109
|
+
}
|
|
110
|
+
const response = await axios({
|
|
111
|
+
method,
|
|
112
|
+
url,
|
|
113
|
+
headers,
|
|
114
|
+
data,
|
|
115
|
+
});
|
|
116
|
+
Logger.log('收到响应:');
|
|
117
|
+
Logger.log(`响应状态码: ${response.status}`);
|
|
118
|
+
Logger.log(`响应头: ${JSON.stringify(response.headers, null, 2)}`);
|
|
119
|
+
Logger.log(`响应数据: ${JSON.stringify(response.data, null, 2)}`);
|
|
120
|
+
return response.data;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error instanceof AxiosError && error.response) {
|
|
124
|
+
Logger.error(`请求失败:`);
|
|
125
|
+
Logger.error(`状态码: ${error.response.status}`);
|
|
126
|
+
Logger.error(`响应头: ${JSON.stringify(error.response.headers, null, 2)}`);
|
|
127
|
+
Logger.error(`响应数据: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
128
|
+
throw {
|
|
129
|
+
status: error.response.status,
|
|
130
|
+
err: error.response.data?.msg || "Unknown error",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
Logger.error('发送请求时发生未知错误:', error);
|
|
134
|
+
throw new Error("Failed to make request to Feishu API");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// 创建新文档
|
|
138
|
+
async createDocument(title, folderToken) {
|
|
139
|
+
try {
|
|
140
|
+
Logger.log(`开始创建飞书文档,标题: ${title}${folderToken ? `,文件夹Token: ${folderToken}` : ',根目录'}`);
|
|
141
|
+
const endpoint = '/docx/v1/documents';
|
|
142
|
+
const data = {
|
|
143
|
+
title: title,
|
|
144
|
+
};
|
|
145
|
+
if (folderToken) {
|
|
146
|
+
data.folder_token = folderToken;
|
|
147
|
+
}
|
|
148
|
+
Logger.log(`准备请求API端点: ${endpoint}`);
|
|
149
|
+
Logger.log(`请求数据: ${JSON.stringify(data, null, 2)}`);
|
|
150
|
+
const response = await this.request(endpoint, 'POST', data);
|
|
151
|
+
if (response.code !== 0) {
|
|
152
|
+
throw new Error(`创建文档失败: ${response.msg}`);
|
|
153
|
+
}
|
|
154
|
+
const docInfo = response.data?.document;
|
|
155
|
+
Logger.log(`文档创建成功,文档ID: ${docInfo?.document_id}`);
|
|
156
|
+
Logger.log(`文档详情: ${JSON.stringify(docInfo, null, 2)}`);
|
|
157
|
+
return docInfo;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
Logger.error(`创建文档失败:`, error);
|
|
161
|
+
if (error instanceof AxiosError) {
|
|
162
|
+
Logger.error(`请求URL: ${error.config?.url}`);
|
|
163
|
+
Logger.error(`请求方法: ${error.config?.method?.toUpperCase()}`);
|
|
164
|
+
Logger.error(`状态码: ${error.response?.status}`);
|
|
165
|
+
if (error.response?.data) {
|
|
166
|
+
Logger.error(`错误详情: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// 获取文档信息
|
|
173
|
+
async getDocumentInfo(documentId) {
|
|
174
|
+
try {
|
|
175
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
176
|
+
if (!docId) {
|
|
177
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
178
|
+
}
|
|
179
|
+
Logger.log(`开始获取文档信息,文档ID: ${docId}`);
|
|
180
|
+
const endpoint = `/docx/v1/documents/${docId}`;
|
|
181
|
+
Logger.log(`准备请求API端点: ${endpoint}`);
|
|
182
|
+
const response = await this.request(endpoint);
|
|
183
|
+
if (response.code !== 0) {
|
|
184
|
+
throw new Error(`获取文档信息失败: ${response.msg}`);
|
|
185
|
+
}
|
|
186
|
+
const docInfo = response.data?.document;
|
|
187
|
+
Logger.log(`文档信息获取成功: ${JSON.stringify(docInfo, null, 2)}`);
|
|
188
|
+
if (!docInfo) {
|
|
189
|
+
throw new Error(`获取文档信息失败: 返回的文档信息为空`);
|
|
190
|
+
}
|
|
191
|
+
return docInfo;
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
Logger.error(`获取文档信息失败:`, error);
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// 获取文档纯文本内容
|
|
199
|
+
async getDocumentContent(documentId, lang = 0) {
|
|
200
|
+
try {
|
|
201
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
202
|
+
if (!docId) {
|
|
203
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
204
|
+
}
|
|
205
|
+
Logger.log(`开始获取文档内容,文档ID: ${docId},语言: ${lang}`);
|
|
206
|
+
const endpoint = `/docx/v1/documents/${docId}/raw_content?lang=${lang}`;
|
|
207
|
+
Logger.log(`准备请求API端点: ${endpoint}`);
|
|
208
|
+
const response = await this.request(endpoint);
|
|
209
|
+
if (response.code !== 0) {
|
|
210
|
+
throw new Error(`获取文档内容失败: ${response.msg}`);
|
|
211
|
+
}
|
|
212
|
+
Logger.log(`文档内容获取成功,长度: ${response.data?.content?.length || 0}字符`);
|
|
213
|
+
return response.data?.content || '';
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
Logger.error(`获取文档内容失败:`, error);
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// 获取文档块
|
|
221
|
+
async getDocumentBlocks(documentId, pageSize = 500) {
|
|
222
|
+
try {
|
|
223
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
224
|
+
if (!docId) {
|
|
225
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
226
|
+
}
|
|
227
|
+
Logger.log(`开始获取文档块,文档ID: ${docId},页大小: ${pageSize}`);
|
|
228
|
+
const endpoint = `/docx/v1/documents/${docId}/blocks?document_revision_id=-1&page_size=${pageSize}`;
|
|
229
|
+
Logger.log(`准备请求API端点: ${endpoint}`);
|
|
230
|
+
const response = await this.request(endpoint);
|
|
231
|
+
if (response.code !== 0) {
|
|
232
|
+
throw new Error(`获取文档块失败: ${response.msg}`);
|
|
233
|
+
}
|
|
234
|
+
const blocks = response.data?.items || [];
|
|
235
|
+
Logger.log(`文档块获取成功,共 ${blocks.length} 个块`);
|
|
236
|
+
return blocks;
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
Logger.error(`获取文档块失败:`, error);
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// 创建代码块
|
|
244
|
+
async createCodeBlock(documentId, parentBlockId, code, language = 0, wrap = false, index = 0) {
|
|
245
|
+
try {
|
|
246
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
247
|
+
if (!docId) {
|
|
248
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
249
|
+
}
|
|
250
|
+
Logger.log(`开始创建代码块,文档ID: ${docId},父块ID: ${parentBlockId},语言: ${language},自动换行: ${wrap},插入位置: ${index}`);
|
|
251
|
+
const blockContent = {
|
|
252
|
+
block_type: 14, // 14表示代码块
|
|
253
|
+
code: {
|
|
254
|
+
elements: [
|
|
255
|
+
{
|
|
256
|
+
text_run: {
|
|
257
|
+
content: code,
|
|
258
|
+
text_element_style: {
|
|
259
|
+
bold: false,
|
|
260
|
+
inline_code: false,
|
|
261
|
+
italic: false,
|
|
262
|
+
strikethrough: false,
|
|
263
|
+
underline: false
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
],
|
|
268
|
+
style: {
|
|
269
|
+
language: language,
|
|
270
|
+
wrap: wrap
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
Logger.log(`代码块内容: ${JSON.stringify(blockContent, null, 2)}`);
|
|
275
|
+
return await this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
Logger.error(`创建代码块失败:`, error);
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// 创建文本块
|
|
283
|
+
async createTextBlock(documentId, parentBlockId, textContents, align = 1, index = 0) {
|
|
284
|
+
try {
|
|
285
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
286
|
+
if (!docId) {
|
|
287
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
288
|
+
}
|
|
289
|
+
Logger.log(`开始创建文本块,文档ID: ${docId},父块ID: ${parentBlockId},对齐方式: ${align},插入位置: ${index}`);
|
|
290
|
+
const blockContent = {
|
|
291
|
+
block_type: 2, // 2表示文本块
|
|
292
|
+
text: {
|
|
293
|
+
elements: textContents.map(content => ({
|
|
294
|
+
text_run: {
|
|
295
|
+
content: content.text,
|
|
296
|
+
text_element_style: content.style || {}
|
|
297
|
+
}
|
|
298
|
+
})),
|
|
299
|
+
style: {
|
|
300
|
+
align: align // 1 居左,2 居中,3 居右
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
Logger.log(`文本块内容: ${JSON.stringify(blockContent, null, 2)}`);
|
|
305
|
+
return await this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
Logger.error(`创建文本块失败:`, error);
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// 创建文档块
|
|
313
|
+
async createDocumentBlock(documentId, parentBlockId, blockContent, index = 0) {
|
|
314
|
+
try {
|
|
315
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
316
|
+
if (!docId) {
|
|
317
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
318
|
+
}
|
|
319
|
+
Logger.log(`开始创建文档块,文档ID: ${docId},父块ID: ${parentBlockId},插入位置: ${index}`);
|
|
320
|
+
const endpoint = `/docx/v1/documents/${docId}/blocks/${parentBlockId}/children?document_revision_id=-1`;
|
|
321
|
+
Logger.log(`准备请求API端点: ${endpoint}`);
|
|
322
|
+
const data = {
|
|
323
|
+
children: [blockContent],
|
|
324
|
+
index: index
|
|
325
|
+
};
|
|
326
|
+
Logger.log(`请求数据: ${JSON.stringify(data, null, 2)}`);
|
|
327
|
+
const response = await this.request(endpoint, 'POST', data);
|
|
328
|
+
if (response.code !== 0) {
|
|
329
|
+
throw new Error(`创建文档块失败: ${response.msg}`);
|
|
330
|
+
}
|
|
331
|
+
Logger.log(`文档块创建成功: ${JSON.stringify(response.data, null, 2)}`);
|
|
332
|
+
return response.data;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
Logger.error(`创建文档块失败:`, error);
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// 创建文本块内容
|
|
340
|
+
createTextBlockContent(textContents, align = 1) {
|
|
341
|
+
return {
|
|
342
|
+
block_type: 2, // 2表示文本块
|
|
343
|
+
text: {
|
|
344
|
+
elements: textContents.map(content => ({
|
|
345
|
+
text_run: {
|
|
346
|
+
content: content.text,
|
|
347
|
+
text_element_style: content.style || {}
|
|
348
|
+
}
|
|
349
|
+
})),
|
|
350
|
+
style: {
|
|
351
|
+
align: align // 1 居左,2 居中,3 居右
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
// 创建代码块内容
|
|
357
|
+
createCodeBlockContent(code, language = 0, wrap = false) {
|
|
358
|
+
return {
|
|
359
|
+
block_type: 14, // 14表示代码块
|
|
360
|
+
code: {
|
|
361
|
+
elements: [
|
|
362
|
+
{
|
|
363
|
+
text_run: {
|
|
364
|
+
content: code,
|
|
365
|
+
text_element_style: {
|
|
366
|
+
bold: false,
|
|
367
|
+
inline_code: false,
|
|
368
|
+
italic: false,
|
|
369
|
+
strikethrough: false,
|
|
370
|
+
underline: false
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
],
|
|
375
|
+
style: {
|
|
376
|
+
language: language,
|
|
377
|
+
wrap: wrap
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
// 创建标题块内容
|
|
383
|
+
createHeadingBlockContent(text, level = 1, align = 1) {
|
|
384
|
+
// 确保标题级别在有效范围内(1-9)
|
|
385
|
+
const safeLevel = Math.max(1, Math.min(9, level));
|
|
386
|
+
// 根据标题级别设置block_type和对应的属性名
|
|
387
|
+
// 飞书API中,一级标题的block_type为3,二级标题为4,以此类推
|
|
388
|
+
const blockType = 2 + safeLevel; // 一级标题为3,二级标题为4,以此类推
|
|
389
|
+
const headingKey = `heading${safeLevel}`; // heading1, heading2, ...
|
|
390
|
+
// 构建块内容
|
|
391
|
+
const blockContent = {
|
|
392
|
+
block_type: blockType
|
|
393
|
+
};
|
|
394
|
+
// 设置对应级别的标题属性
|
|
395
|
+
blockContent[headingKey] = {
|
|
396
|
+
elements: [
|
|
397
|
+
{
|
|
398
|
+
text_run: {
|
|
399
|
+
content: text,
|
|
400
|
+
text_element_style: {}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
],
|
|
404
|
+
style: {
|
|
405
|
+
align: align,
|
|
406
|
+
folded: false
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
return blockContent;
|
|
410
|
+
}
|
|
411
|
+
// 批量创建文档块
|
|
412
|
+
async createDocumentBlocks(documentId, parentBlockId, blockContents, index = 0) {
|
|
413
|
+
try {
|
|
414
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
415
|
+
if (!docId) {
|
|
416
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
417
|
+
}
|
|
418
|
+
Logger.log(`开始批量创建文档块,文档ID: ${docId},父块ID: ${parentBlockId},块数量: ${blockContents.length},插入位置: ${index}`);
|
|
419
|
+
const endpoint = `/docx/v1/documents/${docId}/blocks/${parentBlockId}/children?document_revision_id=-1`;
|
|
420
|
+
Logger.log(`准备请求API端点: ${endpoint}`);
|
|
421
|
+
const data = {
|
|
422
|
+
children: blockContents,
|
|
423
|
+
index: index
|
|
424
|
+
};
|
|
425
|
+
Logger.log(`请求数据: ${JSON.stringify(data, null, 2)}`);
|
|
426
|
+
const response = await this.request(endpoint, 'POST', data);
|
|
427
|
+
if (response.code !== 0) {
|
|
428
|
+
throw new Error(`批量创建文档块失败: ${response.msg}`);
|
|
429
|
+
}
|
|
430
|
+
Logger.log(`批量文档块创建成功: ${JSON.stringify(response.data, null, 2)}`);
|
|
431
|
+
return response.data;
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
Logger.error(`批量创建文档块失败:`, error);
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// 创建标题块
|
|
439
|
+
async createHeadingBlock(documentId, parentBlockId, text, level = 1, index = 0, align = 1) {
|
|
440
|
+
try {
|
|
441
|
+
const docId = this.extractDocIdFromUrl(documentId);
|
|
442
|
+
if (!docId) {
|
|
443
|
+
throw new Error(`无效的文档ID: ${documentId}`);
|
|
444
|
+
}
|
|
445
|
+
Logger.log(`开始创建标题块,文档ID: ${docId},父块ID: ${parentBlockId},标题级别: ${level},插入位置: ${index}`);
|
|
446
|
+
// 确保标题级别在有效范围内(1-9)
|
|
447
|
+
const safeLevel = Math.max(1, Math.min(9, level));
|
|
448
|
+
// 根据标题级别设置block_type和对应的属性名
|
|
449
|
+
// 飞书API中,一级标题的block_type为3,二级标题为4,以此类推
|
|
450
|
+
const blockType = 2 + safeLevel; // 一级标题为3,二级标题为4,以此类推
|
|
451
|
+
const headingKey = `heading${safeLevel}`; // heading1, heading2, ...
|
|
452
|
+
// 构建块内容
|
|
453
|
+
const blockContent = {
|
|
454
|
+
block_type: blockType
|
|
455
|
+
};
|
|
456
|
+
// 设置对应级别的标题属性
|
|
457
|
+
blockContent[headingKey] = {
|
|
458
|
+
elements: [
|
|
459
|
+
{
|
|
460
|
+
text_run: {
|
|
461
|
+
content: text,
|
|
462
|
+
text_element_style: {}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
],
|
|
466
|
+
style: {
|
|
467
|
+
align: align,
|
|
468
|
+
folded: false
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
Logger.log(`标题块内容: ${JSON.stringify(blockContent, null, 2)}`);
|
|
472
|
+
return await this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
Logger.error(`创建标题块失败:`, error);
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
extractDocIdFromUrl(url) {
|
|
480
|
+
// 处理飞书文档 URL,提取文档 ID
|
|
481
|
+
// 支持多种URL格式
|
|
482
|
+
// 1. 标准文档URL格式: https://xxx.feishu.cn/docs/xxx 或 https://xxx.feishu.cn/docx/xxx
|
|
483
|
+
const docxMatch = url.match(/\/docx\/(\w+)/); // 匹配 docx 格式
|
|
484
|
+
const docsMatch = url.match(/\/docs\/(\w+)/); // 匹配 docs 格式
|
|
485
|
+
// 2. API URL格式: https://open.feishu.cn/open-apis/doc/v2/documents/xxx
|
|
486
|
+
const apiMatch = url.match(/\/documents\/([\w-]+)/); // 匹配 API URL 格式
|
|
487
|
+
// 3. 直接使用文档ID
|
|
488
|
+
const directIdMatch = url.match(/^([\w-]+)$/); // 如果直接传入了文档ID
|
|
489
|
+
// 按优先级返回匹配结果
|
|
490
|
+
return docxMatch ? docxMatch[1] :
|
|
491
|
+
docsMatch ? docsMatch[1] :
|
|
492
|
+
apiMatch ? apiMatch[1] :
|
|
493
|
+
directIdMatch ? directIdMatch[1] : null;
|
|
494
|
+
}
|
|
495
|
+
}
|