feishu-mcp 0.2.2 → 0.2.7
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 +356 -266
- package/dist/cli/auth.js +117 -0
- package/dist/cli/commands/auth.js +141 -0
- package/dist/cli/commands/config.js +86 -0
- package/dist/cli/commands/guide.js +68 -0
- package/dist/cli/dispatcher.js +96 -0
- package/dist/cli/index.js +95 -0
- package/dist/manager/sseConnectionManager.js +2 -0
- package/dist/mcp/feishuMcp.js +25 -25
- package/dist/mcp/tools/blockTools.js +295 -0
- package/dist/mcp/tools/documentTools.js +105 -0
- package/dist/mcp/tools/feishuBlockTools.js +1 -285
- package/dist/mcp/tools/feishuTools.js +1 -67
- package/dist/mcp/tools/folderTools.js +99 -0
- package/dist/mcp/tools/toolHelpers.js +155 -0
- package/dist/modules/FeatureModule.js +1 -0
- package/dist/modules/ModuleRegistry.js +63 -0
- package/dist/modules/calendar/index.js +11 -0
- package/dist/modules/calendar/services/FeishuCalendarService.js +6 -0
- package/dist/modules/calendar/tools/calendarTools.js +6 -0
- package/dist/modules/document/index.js +15 -0
- package/dist/modules/document/services/FeishuBlockService.js +410 -0
- package/dist/modules/document/services/FeishuDocumentService.js +110 -0
- package/dist/modules/document/services/FeishuFoldService.js +187 -0
- package/dist/modules/document/services/FeishuSearchService.js +232 -0
- package/dist/modules/document/services/FeishuWhiteboardService.js +80 -0
- package/dist/modules/document/services/blockFactory.js +521 -0
- package/dist/modules/document/toolApi/blockToolApi.js +160 -0
- package/dist/modules/document/toolApi/documentToolApi.js +65 -0
- package/dist/modules/document/toolApi/folderToolApi.js +73 -0
- package/dist/modules/document/toolApi/index.js +3 -0
- package/dist/modules/document/tools/blockTools.js +138 -0
- package/dist/modules/document/tools/documentTools.js +64 -0
- package/dist/modules/document/tools/folderTools.js +46 -0
- package/dist/modules/document/tools/toolHelpers.js +155 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/member/index.js +11 -0
- package/dist/modules/member/services/FeishuMemberService.js +41 -0
- package/dist/modules/member/toolApi/index.js +1 -0
- package/dist/modules/member/toolApi/memberToolApi.js +54 -0
- package/dist/modules/member/tools/memberTools.js +26 -0
- package/dist/modules/task/index.js +11 -0
- package/dist/modules/task/services/FeishuTaskService.js +271 -0
- package/dist/modules/task/toolApi/__tests__/taskToolApi.test.js +98 -0
- package/dist/modules/task/toolApi/index.js +1 -0
- package/dist/modules/task/toolApi/taskToolApi.js +128 -0
- package/dist/modules/task/tools/taskTools.js +93 -0
- package/dist/server.js +43 -24
- package/dist/services/baseService.js +11 -2
- package/dist/services/blockFactory.js +167 -0
- package/dist/services/callbackService.js +1 -1
- package/dist/services/constants/feishuScopes.js +94 -0
- package/dist/services/feishu/FeishuBaseApiService.js +47 -0
- package/dist/services/feishu/FeishuBlockService.js +410 -0
- package/dist/services/feishu/FeishuDocumentBlockService.js +403 -0
- package/dist/services/feishu/FeishuDocumentService.js +110 -0
- package/dist/services/feishu/FeishuDriveService.js +62 -0
- package/dist/services/feishu/FeishuFoldService.js +187 -0
- package/dist/services/feishu/FeishuImageService.js +219 -0
- package/dist/services/feishu/FeishuScopeValidator.js +177 -0
- package/dist/services/feishu/FeishuSearchService.js +232 -0
- package/dist/services/feishu/FeishuWhiteboardService.js +80 -0
- package/dist/services/feishu/FeishuWikiService.js +134 -0
- package/dist/services/feishuApiService.js +246 -1760
- package/dist/services/feishuAuthService.js +43 -0
- package/dist/types/documentSchema.js +232 -0
- package/dist/types/feishuSchema.js +54 -77
- package/dist/types/memberSchema.js +35 -0
- package/dist/types/taskSchema.js +166 -0
- package/dist/utils/auth/tokenCacheManager.js +16 -3
- package/dist/utils/auth/tokenRefreshManager.js +2 -1
- package/dist/utils/config.js +47 -0
- package/dist/utils/document.js +116 -116
- package/dist/utils/error.js +0 -11
- package/dist/utils/paramUtils.js +0 -31
- package/package.json +77 -76
|
@@ -1,1891 +1,377 @@
|
|
|
1
|
-
import { BaseApiService } from './baseService.js';
|
|
2
|
-
import { Logger } from '../utils/logger.js';
|
|
3
|
-
import { Config } from '../utils/config.js';
|
|
4
|
-
import { ParamUtils } from '../utils/paramUtils.js';
|
|
5
|
-
import { BlockFactory, BlockType } from './blockFactory.js';
|
|
6
|
-
import { AuthUtils, TokenCacheManager } from '../utils/auth/index.js';
|
|
7
1
|
import { AuthService } from './feishuAuthService.js';
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
2
|
+
import { FeishuDocumentService } from '../modules/document/services/FeishuDocumentService.js';
|
|
3
|
+
import { FeishuBlockService } from '../modules/document/services/FeishuBlockService.js';
|
|
4
|
+
import { FeishuFoldService } from '../modules/document/services/FeishuFoldService.js';
|
|
5
|
+
import { FeishuSearchService } from '../modules/document/services/FeishuSearchService.js';
|
|
6
|
+
import { FeishuWhiteboardService } from '../modules/document/services/FeishuWhiteboardService.js';
|
|
7
|
+
import { FeishuTaskService, } from '../modules/task/services/FeishuTaskService.js';
|
|
8
|
+
import { FeishuCalendarService } from '../modules/calendar/services/FeishuCalendarService.js';
|
|
9
|
+
import { FeishuMemberService } from '../modules/member/services/FeishuMemberService.js';
|
|
13
10
|
/**
|
|
14
|
-
* 飞书API
|
|
15
|
-
*
|
|
11
|
+
* 飞书 API 服务门面(Facade)
|
|
12
|
+
*
|
|
13
|
+
* 统一对外入口,持有并编排各领域服务实例。
|
|
14
|
+
* 所有 public 方法均委托给对应的领域服务,本类不直接发起 HTTP 请求。
|
|
15
|
+
* 采用单例模式,通过 {@link getInstance} 获取实例。
|
|
16
|
+
*
|
|
17
|
+
* 领域服务对应关系:
|
|
18
|
+
* - 文档操作 → {@link FeishuDocumentService}
|
|
19
|
+
* - 块/图片操作 → {@link FeishuBlockService}
|
|
20
|
+
* - 文件夹/知识空间操作 → {@link FeishuFoldService}
|
|
21
|
+
* - 搜索 → {@link FeishuSearchService}
|
|
22
|
+
* - 画板 → {@link FeishuWhiteboardService}
|
|
23
|
+
* - 任务 → {@link FeishuTaskService}
|
|
24
|
+
* - 日历 → {@link FeishuCalendarService}
|
|
25
|
+
* - 成员/通讯录 → {@link FeishuMemberService}
|
|
16
26
|
*/
|
|
17
|
-
export class FeishuApiService
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
export class FeishuApiService {
|
|
28
|
+
constructor(documentService, blockService, foldService, searchService, whiteboardService, taskService, calendarService, memberService) {
|
|
29
|
+
Object.defineProperty(this, "documentService", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: documentService
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(this, "blockService", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
value: blockService
|
|
40
|
+
});
|
|
41
|
+
Object.defineProperty(this, "foldService", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
configurable: true,
|
|
44
|
+
writable: true,
|
|
45
|
+
value: foldService
|
|
46
|
+
});
|
|
47
|
+
Object.defineProperty(this, "searchService", {
|
|
48
|
+
enumerable: true,
|
|
49
|
+
configurable: true,
|
|
50
|
+
writable: true,
|
|
51
|
+
value: searchService
|
|
52
|
+
});
|
|
53
|
+
Object.defineProperty(this, "whiteboardService", {
|
|
54
|
+
enumerable: true,
|
|
55
|
+
configurable: true,
|
|
56
|
+
writable: true,
|
|
57
|
+
value: whiteboardService
|
|
58
|
+
});
|
|
59
|
+
Object.defineProperty(this, "taskService", {
|
|
24
60
|
enumerable: true,
|
|
25
61
|
configurable: true,
|
|
26
62
|
writable: true,
|
|
27
|
-
value:
|
|
63
|
+
value: taskService
|
|
28
64
|
});
|
|
29
|
-
Object.defineProperty(this, "
|
|
65
|
+
Object.defineProperty(this, "calendarService", {
|
|
30
66
|
enumerable: true,
|
|
31
67
|
configurable: true,
|
|
32
68
|
writable: true,
|
|
33
|
-
value:
|
|
69
|
+
value: calendarService
|
|
34
70
|
});
|
|
35
|
-
Object.defineProperty(this, "
|
|
71
|
+
Object.defineProperty(this, "memberService", {
|
|
36
72
|
enumerable: true,
|
|
37
73
|
configurable: true,
|
|
38
74
|
writable: true,
|
|
39
|
-
value:
|
|
75
|
+
value: memberService
|
|
40
76
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
77
|
+
}
|
|
78
|
+
/** 组装所有领域服务并返回 FeishuApiService 新实例 */
|
|
79
|
+
static createInstance() {
|
|
80
|
+
const authService = new AuthService();
|
|
81
|
+
const documentService = new FeishuDocumentService(authService);
|
|
82
|
+
const blockService = new FeishuBlockService(authService);
|
|
83
|
+
const foldService = new FeishuFoldService(authService);
|
|
84
|
+
const searchService = new FeishuSearchService(authService);
|
|
85
|
+
const whiteboardService = new FeishuWhiteboardService(authService);
|
|
86
|
+
const taskService = new FeishuTaskService(authService);
|
|
87
|
+
const calendarService = new FeishuCalendarService(authService);
|
|
88
|
+
const memberService = new FeishuMemberService(authService);
|
|
89
|
+
return new FeishuApiService(documentService, blockService, foldService, searchService, whiteboardService, taskService, calendarService, memberService);
|
|
44
90
|
}
|
|
45
91
|
/**
|
|
46
|
-
*
|
|
47
|
-
* @returns
|
|
92
|
+
* 获取 FeishuApiService 单例
|
|
93
|
+
* @returns 全局唯一的 FeishuApiService 实例
|
|
48
94
|
*/
|
|
49
95
|
static getInstance() {
|
|
50
96
|
if (!FeishuApiService.instance) {
|
|
51
|
-
FeishuApiService.instance =
|
|
97
|
+
FeishuApiService.instance = FeishuApiService.createInstance();
|
|
52
98
|
}
|
|
53
99
|
return FeishuApiService.instance;
|
|
54
100
|
}
|
|
55
|
-
|
|
56
|
-
* 获取API基础URL
|
|
57
|
-
* @returns API基础URL
|
|
58
|
-
*/
|
|
59
|
-
getBaseUrl() {
|
|
60
|
-
return this.config.feishu.baseUrl;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* 获取API认证端点
|
|
64
|
-
* @returns 认证端点URL
|
|
65
|
-
*/
|
|
66
|
-
getAuthEndpoint() {
|
|
67
|
-
return '/auth/v3/tenant_access_token/internal';
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* 获取访问令牌
|
|
71
|
-
* @param userKey 用户标识(可选)
|
|
72
|
-
* @returns 访问令牌
|
|
73
|
-
* @throws 如果获取令牌失败则抛出错误
|
|
74
|
-
*/
|
|
75
|
-
async getAccessToken(userKey) {
|
|
76
|
-
const { appId, appSecret, authType } = this.config.feishu;
|
|
77
|
-
// 生成客户端缓存键
|
|
78
|
-
const clientKey = AuthUtils.generateClientKey(userKey);
|
|
79
|
-
Logger.debug(`[FeishuApiService] 获取访问令牌,userKey: ${userKey}, clientKey: ${clientKey}, authType: ${authType}`);
|
|
80
|
-
// 在使用token之前先校验scope(使用appId+appSecret获取临时tenant token来调用scope接口)
|
|
81
|
-
// 根据配置决定是否执行权限检查
|
|
82
|
-
if (this.config.feishu.enableScopeValidation) {
|
|
83
|
-
await this.validateScopeWithVersion(appId, appSecret, authType);
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
Logger.debug('权限检查已禁用,跳过scope校验');
|
|
87
|
-
}
|
|
88
|
-
// 校验通过后,获取实际的token
|
|
89
|
-
if (authType === 'tenant') {
|
|
90
|
-
// 租户模式:获取租户访问令牌
|
|
91
|
-
return await this.getTenantAccessToken(appId, appSecret, clientKey);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
// 用户模式:获取用户访问令牌
|
|
95
|
-
return await this.authService.getUserAccessToken(clientKey, appId, appSecret);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* 获取应用权限范围
|
|
100
|
-
* @param accessToken 访问令牌
|
|
101
|
-
* @param authType 认证类型(tenant或user)
|
|
102
|
-
* @returns 应用权限范围列表
|
|
103
|
-
*/
|
|
104
|
-
async getApplicationScopes(accessToken, authType) {
|
|
105
|
-
try {
|
|
106
|
-
const endpoint = '/application/v6/scopes';
|
|
107
|
-
const headers = {
|
|
108
|
-
'Authorization': `Bearer ${accessToken}`,
|
|
109
|
-
'Content-Type': 'application/json'
|
|
110
|
-
};
|
|
111
|
-
Logger.debug('请求应用权限范围:', endpoint);
|
|
112
|
-
const response = await axios.get(`${this.getBaseUrl()}${endpoint}`, { headers });
|
|
113
|
-
const data = response.data;
|
|
114
|
-
if (data.code !== 0) {
|
|
115
|
-
throw new Error(`获取应用权限范围失败:${data.msg || '未知错误'} (错误码: ${data.code})`);
|
|
116
|
-
}
|
|
117
|
-
// 提取权限列表
|
|
118
|
-
// API返回格式: { "data": { "scopes": [{ "grant_status": 1, "scope_name": "...", "scope_type": "tenant"|"user" }] } }
|
|
119
|
-
const scopes = [];
|
|
120
|
-
if (data.data && Array.isArray(data.data.scopes)) {
|
|
121
|
-
// 根据authType过滤,只取已授权的scope(grant_status === 1)
|
|
122
|
-
for (const scopeItem of data.data.scopes) {
|
|
123
|
-
if (scopeItem.grant_status === 1 && scopeItem.scope_type === authType && scopeItem.scope_name) {
|
|
124
|
-
scopes.push(scopeItem.scope_name);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
Logger.debug(`获取应用权限范围成功,共 ${scopes.length} 个${authType}权限`);
|
|
129
|
-
return scopes;
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
Logger.error('获取应用权限范围失败:', error);
|
|
133
|
-
throw new Error('获取应用权限范围失败: ' + (error instanceof Error ? error.message : String(error)));
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* 校验scope权限是否充足
|
|
138
|
-
* @param requiredScopes 所需的权限列表
|
|
139
|
-
* @param actualScopes 实际的权限列表
|
|
140
|
-
* @returns 是否权限充足,以及缺失的权限列表
|
|
141
|
-
*/
|
|
142
|
-
validateScopes(requiredScopes, actualScopes) {
|
|
143
|
-
const actualScopesSet = new Set(actualScopes);
|
|
144
|
-
const missingScopes = [];
|
|
145
|
-
for (const requiredScope of requiredScopes) {
|
|
146
|
-
if (!actualScopesSet.has(requiredScope)) {
|
|
147
|
-
missingScopes.push(requiredScope);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
isValid: missingScopes.length === 0,
|
|
152
|
-
missingScopes
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* 获取所需的scope列表(根据认证类型)
|
|
157
|
-
* @param authType 认证类型
|
|
158
|
-
* @returns 所需的scope列表
|
|
159
|
-
*/
|
|
160
|
-
getRequiredScopes(authType) {
|
|
161
|
-
// 根据FEISHU_CONFIG.md中定义的权限列表,与用户提供的配置保持一致
|
|
162
|
-
const tenantScopes = [
|
|
163
|
-
"docx:document.block:convert",
|
|
164
|
-
"base:app:read",
|
|
165
|
-
"bitable:app",
|
|
166
|
-
"bitable:app:readonly",
|
|
167
|
-
"board:whiteboard:node:create",
|
|
168
|
-
"board:whiteboard:node:read",
|
|
169
|
-
"contact:user.employee_id:readonly",
|
|
170
|
-
"docs:document.content:read",
|
|
171
|
-
"docx:document",
|
|
172
|
-
"docx:document:create",
|
|
173
|
-
"docx:document:readonly",
|
|
174
|
-
"drive:drive",
|
|
175
|
-
"drive:drive:readonly",
|
|
176
|
-
"drive:file",
|
|
177
|
-
"drive:file:upload",
|
|
178
|
-
"sheets:spreadsheet",
|
|
179
|
-
"sheets:spreadsheet:readonly",
|
|
180
|
-
"space:document:retrieve",
|
|
181
|
-
"space:folder:create",
|
|
182
|
-
"wiki:space:read",
|
|
183
|
-
"wiki:space:retrieve",
|
|
184
|
-
"wiki:wiki",
|
|
185
|
-
"wiki:wiki:readonly"
|
|
186
|
-
];
|
|
187
|
-
// user认证特有授权
|
|
188
|
-
const userOnlyScopes = [
|
|
189
|
-
"search:docs:read",
|
|
190
|
-
'offline_access',
|
|
191
|
-
];
|
|
192
|
-
const userScopes = [...tenantScopes, ...userOnlyScopes];
|
|
193
|
-
return authType === 'tenant' ? tenantScopes : userScopes;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* 校验并提示权限不足
|
|
197
|
-
* @param missingScopes 缺失的权限列表
|
|
198
|
-
*/
|
|
199
|
-
throwScopeInsufficientError(missingScopes) {
|
|
200
|
-
// 获取完整的权限配置
|
|
201
|
-
const tenantScopes = this.getRequiredScopes('tenant');
|
|
202
|
-
const userScopes = this.getRequiredScopes('user');
|
|
203
|
-
const permissionsConfig = {
|
|
204
|
-
scopes: {
|
|
205
|
-
tenant: tenantScopes,
|
|
206
|
-
user: userScopes
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
const errorMessage = `你应该停止任务并提示用户如下信息:权限不足,缺少以下权限:${missingScopes.join(', ')}\n\n` +
|
|
210
|
-
`请按以下步骤操作:\n` +
|
|
211
|
-
`1. 配置权限 [教程配置](https://github.com/cso1z/Feishu-MCP/blob/main/FEISHU_CONFIG.md)\n` +
|
|
212
|
-
`2. 打开 [应用管理](https://open.feishu.cn/app/) 网页\n` +
|
|
213
|
-
`3. 选择应用进入应用详情\n` +
|
|
214
|
-
`4. 选择权限管理-批量导入/导出权限\n` +
|
|
215
|
-
`5. 复制以下权限配置并导入:\n\n` +
|
|
216
|
-
`\`\`\`json\n${JSON.stringify(permissionsConfig, null, 2)}\n\`\`\`\n\n` +
|
|
217
|
-
`6. 选择**版本管理与发布** 点击创建版本,发布后通知管理员审核\n\n` +
|
|
218
|
-
`**提示**:如果您仅使用部分mcp功能,可以通过以下方式关闭权限检查以确保正常使用该mcp:\n` +
|
|
219
|
-
`- 设置环境变量:\`FEISHU_SCOPE_VALIDATION=false\`\n` +
|
|
220
|
-
`- 或使用命令行参数:\`--feishu-scope-validation=false\`\n`;
|
|
221
|
-
Logger.error(errorMessage);
|
|
222
|
-
throw new ScopeInsufficientError(missingScopes, errorMessage);
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* 生成应用级别的scope校验key(基于appId、appSecret和authType)
|
|
226
|
-
* @param appId 应用ID
|
|
227
|
-
* @param appSecret 应用密钥
|
|
228
|
-
* @param authType 认证类型(tenant或user)
|
|
229
|
-
* @returns scope校验key
|
|
230
|
-
*/
|
|
231
|
-
generateScopeKey(appId, appSecret, authType) {
|
|
232
|
-
// 使用appId、appSecret和authType生成唯一的key,用于scope版本管理
|
|
233
|
-
// 包含authType是因为tenant和user的权限列表不同,需要分开校验
|
|
234
|
-
return `app:${appId}:${appSecret.substring(0, 8)}:${authType}`;
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* 获取临时租户访问令牌(用于scope校验)
|
|
238
|
-
* @param appId 应用ID
|
|
239
|
-
* @param appSecret 应用密钥
|
|
240
|
-
* @returns 租户访问令牌
|
|
241
|
-
*/
|
|
242
|
-
async getTempTenantTokenForScope(appId, appSecret) {
|
|
243
|
-
try {
|
|
244
|
-
const requestData = {
|
|
245
|
-
app_id: appId,
|
|
246
|
-
app_secret: appSecret,
|
|
247
|
-
};
|
|
248
|
-
const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
|
|
249
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
250
|
-
Logger.debug('获取临时租户token用于scope校验:', url);
|
|
251
|
-
const response = await axios.post(url, requestData, { headers });
|
|
252
|
-
const data = response.data;
|
|
253
|
-
if (data.code !== 0) {
|
|
254
|
-
throw new Error(`获取临时租户访问令牌失败:${data.msg || '未知错误'} (错误码: ${data.code})`);
|
|
255
|
-
}
|
|
256
|
-
if (!data.tenant_access_token) {
|
|
257
|
-
throw new Error('获取临时租户访问令牌失败:响应中没有token');
|
|
258
|
-
}
|
|
259
|
-
Logger.debug('临时租户token获取成功,用于scope校验');
|
|
260
|
-
return data.tenant_access_token;
|
|
261
|
-
}
|
|
262
|
-
catch (error) {
|
|
263
|
-
Logger.error('获取临时租户访问令牌失败:', error);
|
|
264
|
-
throw new Error('获取临时租户访问令牌失败: ' + (error instanceof Error ? error.message : String(error)));
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* 校验scope权限(带版本管理)
|
|
269
|
-
* @param appId 应用ID
|
|
270
|
-
* @param appSecret 应用密钥
|
|
271
|
-
* @param authType 认证类型
|
|
272
|
-
*/
|
|
273
|
-
async validateScopeWithVersion(appId, appSecret, authType) {
|
|
274
|
-
const tokenCacheManager = TokenCacheManager.getInstance();
|
|
275
|
-
// 生成应用级别的scope校验key(包含authType,因为tenant和user权限不同)
|
|
276
|
-
const scopeKey = this.generateScopeKey(appId, appSecret, authType);
|
|
277
|
-
const scopeVersion = '2.0.0'; // 当前scope版本号,可以根据需要更新
|
|
278
|
-
// 检查是否需要校验
|
|
279
|
-
if (!tokenCacheManager.shouldValidateScope(scopeKey, scopeVersion)) {
|
|
280
|
-
Logger.debug(`Scope版本已校验过,跳过校验: ${scopeKey}`);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
Logger.info(`开始校验scope权限,版本: ${scopeVersion}, scopeKey: ${scopeKey}`);
|
|
284
|
-
try {
|
|
285
|
-
// 使用appId和appSecret获取临时tenant token来调用scope接口
|
|
286
|
-
const tempTenantToken = await this.getTempTenantTokenForScope(appId, appSecret);
|
|
287
|
-
// 获取实际权限范围(使用tenant token,但根据authType过滤scope_type)
|
|
288
|
-
const actualScopes = await this.getApplicationScopes(tempTenantToken, authType);
|
|
289
|
-
// 获取当前版本所需的scope列表
|
|
290
|
-
const requiredScopes = this.getRequiredScopes(authType);
|
|
291
|
-
// 校验权限
|
|
292
|
-
const validationResult = this.validateScopes(requiredScopes, actualScopes);
|
|
293
|
-
if (!validationResult.isValid) {
|
|
294
|
-
// 权限不足,抛出错误
|
|
295
|
-
this.throwScopeInsufficientError(validationResult.missingScopes);
|
|
296
|
-
}
|
|
297
|
-
// 权限充足,保存版本信息
|
|
298
|
-
const scopeVersionInfo = {
|
|
299
|
-
scopeVersion,
|
|
300
|
-
scopeList: requiredScopes,
|
|
301
|
-
validatedAt: Math.floor(Date.now() / 1000),
|
|
302
|
-
validatedVersion: scopeVersion
|
|
303
|
-
};
|
|
304
|
-
tokenCacheManager.saveScopeVersionInfo(scopeKey, scopeVersionInfo);
|
|
305
|
-
Logger.info(`Scope权限校验成功,版本: ${scopeVersion}`);
|
|
306
|
-
}
|
|
307
|
-
catch (error) {
|
|
308
|
-
// 如果是权限不足错误,需要重新抛出,中断流程
|
|
309
|
-
if (error instanceof ScopeInsufficientError) {
|
|
310
|
-
throw error;
|
|
311
|
-
}
|
|
312
|
-
// 如果获取权限范围失败(网络错误、API调用失败等),记录警告但不阻止token使用
|
|
313
|
-
Logger.warn(`Scope权限校验失败,但继续使用token: ${error instanceof Error ? error.message : String(error)}`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* 获取租户访问令牌
|
|
318
|
-
* @param appId 应用ID
|
|
319
|
-
* @param appSecret 应用密钥
|
|
320
|
-
* @param clientKey 客户端缓存键
|
|
321
|
-
* @returns 租户访问令牌
|
|
322
|
-
*/
|
|
323
|
-
async getTenantAccessToken(appId, appSecret, clientKey) {
|
|
324
|
-
const tokenCacheManager = TokenCacheManager.getInstance();
|
|
325
|
-
// 尝试从缓存获取租户token
|
|
326
|
-
const cachedToken = tokenCacheManager.getTenantToken(clientKey);
|
|
327
|
-
if (cachedToken) {
|
|
328
|
-
Logger.debug('使用缓存的租户访问令牌');
|
|
329
|
-
return cachedToken;
|
|
330
|
-
}
|
|
331
|
-
// 缓存中没有token,请求新的租户token
|
|
332
|
-
Logger.info('缓存中没有租户token,请求新的租户访问令牌');
|
|
333
|
-
try {
|
|
334
|
-
const requestData = {
|
|
335
|
-
app_id: appId,
|
|
336
|
-
app_secret: appSecret,
|
|
337
|
-
};
|
|
338
|
-
const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
|
|
339
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
340
|
-
Logger.debug('请求租户访问令牌:', url, requestData);
|
|
341
|
-
const response = await axios.post(url, requestData, { headers });
|
|
342
|
-
const data = response.data;
|
|
343
|
-
if (data.code !== 0) {
|
|
344
|
-
throw new Error(`获取租户访问令牌失败:${data.msg || '未知错误'} (错误码: ${data.code})`);
|
|
345
|
-
}
|
|
346
|
-
if (!data.tenant_access_token) {
|
|
347
|
-
throw new Error('获取租户访问令牌失败:响应中没有token');
|
|
348
|
-
}
|
|
349
|
-
// 计算绝对过期时间戳
|
|
350
|
-
const expire_at = Math.floor(Date.now() / 1000) + (data.expire || 0);
|
|
351
|
-
const tokenInfo = {
|
|
352
|
-
app_access_token: data.tenant_access_token,
|
|
353
|
-
expires_at: expire_at
|
|
354
|
-
};
|
|
355
|
-
// 缓存租户token
|
|
356
|
-
tokenCacheManager.cacheTenantToken(clientKey, tokenInfo, data.expire);
|
|
357
|
-
Logger.info('租户访问令牌获取并缓存成功');
|
|
358
|
-
return data.tenant_access_token;
|
|
359
|
-
}
|
|
360
|
-
catch (error) {
|
|
361
|
-
Logger.error('获取租户访问令牌失败:', error);
|
|
362
|
-
throw new Error('获取租户访问令牌失败: ' + (error instanceof Error ? error.message : String(error)));
|
|
363
|
-
}
|
|
364
|
-
}
|
|
101
|
+
// ─── 文档服务委托 ─────────────────────────────────────────────────
|
|
365
102
|
/**
|
|
366
103
|
* 创建飞书文档
|
|
367
|
-
* @
|
|
368
|
-
* @param folderToken 文件夹Token
|
|
369
|
-
* @returns 创建的文档信息
|
|
104
|
+
* @see FeishuDocumentService.createDocument
|
|
370
105
|
*/
|
|
371
106
|
async createDocument(title, folderToken) {
|
|
372
|
-
|
|
373
|
-
const endpoint = '/docx/v1/documents';
|
|
374
|
-
const payload = {
|
|
375
|
-
title,
|
|
376
|
-
folder_token: folderToken
|
|
377
|
-
};
|
|
378
|
-
const response = await this.post(endpoint, payload);
|
|
379
|
-
return response;
|
|
380
|
-
}
|
|
381
|
-
catch (error) {
|
|
382
|
-
this.handleApiError(error, '创建飞书文档失败');
|
|
383
|
-
}
|
|
107
|
+
return this.documentService.createDocument(title, folderToken);
|
|
384
108
|
}
|
|
385
109
|
/**
|
|
386
|
-
*
|
|
387
|
-
* @
|
|
388
|
-
* @param documentType 文档类型(可选),'document' 或 'wiki',如果不指定则自动检测
|
|
389
|
-
* @returns 文档信息或Wiki节点信息
|
|
110
|
+
* 获取文档信息,支持普通文档和 Wiki 文档
|
|
111
|
+
* @see FeishuDocumentService.getDocumentInfo
|
|
390
112
|
*/
|
|
391
113
|
async getDocumentInfo(documentId, documentType) {
|
|
392
|
-
|
|
393
|
-
let isWikiLink;
|
|
394
|
-
// 如果明确指定了类型,使用指定的类型
|
|
395
|
-
if (documentType === 'wiki') {
|
|
396
|
-
isWikiLink = true;
|
|
397
|
-
}
|
|
398
|
-
else if (documentType === 'document') {
|
|
399
|
-
isWikiLink = false;
|
|
400
|
-
}
|
|
401
|
-
else {
|
|
402
|
-
// 自动检测:检查是否是Wiki链接(包含 /wiki/ 路径)
|
|
403
|
-
isWikiLink = documentId.includes('/wiki/');
|
|
404
|
-
}
|
|
405
|
-
if (isWikiLink) {
|
|
406
|
-
// 处理Wiki文档
|
|
407
|
-
const wikiToken = ParamUtils.processWikiToken(documentId);
|
|
408
|
-
const endpoint = `/wiki/v2/spaces/get_node`;
|
|
409
|
-
const params = { token: wikiToken, obj_type: 'wiki' };
|
|
410
|
-
const response = await this.get(endpoint, params);
|
|
411
|
-
if (!response.node || !response.node.obj_token) {
|
|
412
|
-
throw new Error(`无法从Wiki节点获取文档ID: ${wikiToken}`);
|
|
413
|
-
}
|
|
414
|
-
const node = response.node;
|
|
415
|
-
const docId = node.obj_token;
|
|
416
|
-
// 构建返回对象,包含完整节点信息和 documentId 字段
|
|
417
|
-
const result = {
|
|
418
|
-
...node,
|
|
419
|
-
documentId: docId, // 添加 documentId 字段作为 obj_token 的别名
|
|
420
|
-
_type: 'wiki', // 标识这是Wiki文档
|
|
421
|
-
};
|
|
422
|
-
Logger.debug(`获取Wiki文档信息: ${wikiToken} -> documentId: ${docId}, space_id: ${node.space_id}, node_token: ${node.node_token}`);
|
|
423
|
-
return result;
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
426
|
-
// 处理普通文档
|
|
427
|
-
const normalizedDocId = ParamUtils.processDocumentId(documentId);
|
|
428
|
-
const endpoint = `/docx/v1/documents/${normalizedDocId}`;
|
|
429
|
-
const response = await this.get(endpoint);
|
|
430
|
-
const result = {
|
|
431
|
-
...response,
|
|
432
|
-
_type: 'document', // 标识这是普通文档
|
|
433
|
-
};
|
|
434
|
-
Logger.debug(`获取普通文档信息: ${normalizedDocId}`);
|
|
435
|
-
return result;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
catch (error) {
|
|
439
|
-
this.handleApiError(error, '获取文档信息失败');
|
|
440
|
-
}
|
|
114
|
+
return this.documentService.getDocumentInfo(documentId, documentType);
|
|
441
115
|
}
|
|
442
116
|
/**
|
|
443
|
-
*
|
|
444
|
-
* @
|
|
445
|
-
* @param lang 语言代码,0为中文,1为英文
|
|
446
|
-
* @returns 文档内容
|
|
117
|
+
* 获取文档的纯文本内容
|
|
118
|
+
* @see FeishuDocumentService.getDocumentContent
|
|
447
119
|
*/
|
|
448
120
|
async getDocumentContent(documentId, lang = 0) {
|
|
449
|
-
|
|
450
|
-
const normalizedDocId = ParamUtils.processDocumentId(documentId);
|
|
451
|
-
const endpoint = `/docx/v1/documents/${normalizedDocId}/raw_content`;
|
|
452
|
-
const params = { lang };
|
|
453
|
-
const response = await this.get(endpoint, params);
|
|
454
|
-
return response.content;
|
|
455
|
-
}
|
|
456
|
-
catch (error) {
|
|
457
|
-
this.handleApiError(error, '获取文档内容失败');
|
|
458
|
-
}
|
|
121
|
+
return this.documentService.getDocumentContent(documentId, lang);
|
|
459
122
|
}
|
|
460
123
|
/**
|
|
461
|
-
*
|
|
462
|
-
* @
|
|
463
|
-
* @param pageSize 每页块数量
|
|
464
|
-
* @returns 文档块数组
|
|
124
|
+
* 获取文档的所有块结构(自动分页)
|
|
125
|
+
* @see FeishuDocumentService.getDocumentBlocks
|
|
465
126
|
*/
|
|
466
127
|
async getDocumentBlocks(documentId, pageSize = 500) {
|
|
467
|
-
|
|
468
|
-
const normalizedDocId = ParamUtils.processDocumentId(documentId);
|
|
469
|
-
const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks`;
|
|
470
|
-
let pageToken = '';
|
|
471
|
-
let allBlocks = [];
|
|
472
|
-
// 分页获取所有块
|
|
473
|
-
do {
|
|
474
|
-
const params = {
|
|
475
|
-
page_size: pageSize,
|
|
476
|
-
document_revision_id: -1
|
|
477
|
-
};
|
|
478
|
-
if (pageToken) {
|
|
479
|
-
params.page_token = pageToken;
|
|
480
|
-
}
|
|
481
|
-
const response = await this.get(endpoint, params);
|
|
482
|
-
const blocks = response.items || [];
|
|
483
|
-
allBlocks = [...allBlocks, ...blocks];
|
|
484
|
-
pageToken = response.page_token;
|
|
485
|
-
} while (pageToken);
|
|
486
|
-
return allBlocks;
|
|
487
|
-
}
|
|
488
|
-
catch (error) {
|
|
489
|
-
this.handleApiError(error, '获取文档块结构失败');
|
|
490
|
-
}
|
|
128
|
+
return this.documentService.getDocumentBlocks(documentId, pageSize);
|
|
491
129
|
}
|
|
130
|
+
// ─── 块服务委托 ───────────────────────────────────────────────────
|
|
492
131
|
/**
|
|
493
|
-
*
|
|
494
|
-
* @
|
|
495
|
-
* @param blockId 块ID
|
|
496
|
-
* @returns 块内容
|
|
497
|
-
*/
|
|
498
|
-
async getBlockContent(documentId, blockId) {
|
|
499
|
-
try {
|
|
500
|
-
const normalizedDocId = ParamUtils.processDocumentId(documentId);
|
|
501
|
-
const safeBlockId = ParamUtils.processBlockId(blockId);
|
|
502
|
-
const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${safeBlockId}`;
|
|
503
|
-
const params = { document_revision_id: -1 };
|
|
504
|
-
const response = await this.get(endpoint, params);
|
|
505
|
-
return response;
|
|
506
|
-
}
|
|
507
|
-
catch (error) {
|
|
508
|
-
this.handleApiError(error, '获取块内容失败');
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* 更新块文本内容
|
|
513
|
-
* @param documentId 文档ID或URL
|
|
514
|
-
* @param blockId 块ID
|
|
515
|
-
* @param textElements 文本元素数组,支持普通文本和公式元素
|
|
516
|
-
* @returns 更新结果
|
|
132
|
+
* 更新块的文本内容,支持普通文本与行内公式混排
|
|
133
|
+
* @see FeishuBlockService.updateBlockTextContent
|
|
517
134
|
*/
|
|
518
135
|
async updateBlockTextContent(documentId, blockId, textElements) {
|
|
519
|
-
|
|
520
|
-
const docId = ParamUtils.processDocumentId(documentId);
|
|
521
|
-
const endpoint = `/docx/v1/documents/${docId}/blocks/${blockId}?document_revision_id=-1`;
|
|
522
|
-
Logger.debug(`准备请求API端点: ${endpoint}`);
|
|
523
|
-
const elements = textElements.map(item => {
|
|
524
|
-
if (item.equation !== undefined) {
|
|
525
|
-
return {
|
|
526
|
-
equation: {
|
|
527
|
-
content: item.equation,
|
|
528
|
-
text_element_style: BlockFactory.applyDefaultTextStyle(item.style)
|
|
529
|
-
}
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
return {
|
|
534
|
-
text_run: {
|
|
535
|
-
content: item.text || '',
|
|
536
|
-
text_element_style: BlockFactory.applyDefaultTextStyle(item.style)
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
});
|
|
541
|
-
const data = {
|
|
542
|
-
update_text_elements: {
|
|
543
|
-
elements: elements
|
|
544
|
-
}
|
|
545
|
-
};
|
|
546
|
-
Logger.debug(`请求数据: ${JSON.stringify(data, null, 2)}`);
|
|
547
|
-
const response = await this.patch(endpoint, data);
|
|
548
|
-
return response;
|
|
549
|
-
}
|
|
550
|
-
catch (error) {
|
|
551
|
-
this.handleApiError(error, '更新块文本内容失败');
|
|
552
|
-
return null; // 永远不会执行到这里
|
|
553
|
-
}
|
|
136
|
+
return this.blockService.updateBlockTextContent(documentId, blockId, textElements);
|
|
554
137
|
}
|
|
555
138
|
/**
|
|
556
|
-
*
|
|
557
|
-
* @
|
|
558
|
-
* @param parentBlockId 父块ID
|
|
559
|
-
* @param blockContent 块内容
|
|
560
|
-
* @param index 插入位置索引
|
|
561
|
-
* @returns 创建结果
|
|
139
|
+
* 批量更新多个块的文本内容(一次 API 调用)
|
|
140
|
+
* @see FeishuBlockService.batchUpdateBlocksTextContent
|
|
562
141
|
*/
|
|
563
|
-
async
|
|
564
|
-
|
|
565
|
-
const normalizedDocId = ParamUtils.processDocumentId(documentId);
|
|
566
|
-
const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children?document_revision_id=-1`;
|
|
567
|
-
Logger.debug(`准备请求API端点: ${endpoint}`);
|
|
568
|
-
const payload = {
|
|
569
|
-
children: [blockContent],
|
|
570
|
-
index
|
|
571
|
-
};
|
|
572
|
-
Logger.debug(`请求数据: ${JSON.stringify(payload, null, 2)}`);
|
|
573
|
-
const response = await this.post(endpoint, payload);
|
|
574
|
-
return response;
|
|
575
|
-
}
|
|
576
|
-
catch (error) {
|
|
577
|
-
this.handleApiError(error, '创建文档块失败');
|
|
578
|
-
return null; // 永远不会执行到这里
|
|
579
|
-
}
|
|
142
|
+
async batchUpdateBlocksTextContent(documentId, updates) {
|
|
143
|
+
return this.blockService.batchUpdateBlocksTextContent(documentId, updates);
|
|
580
144
|
}
|
|
581
145
|
/**
|
|
582
|
-
*
|
|
583
|
-
* @
|
|
584
|
-
* @param parentBlockId 父块ID
|
|
585
|
-
* @param blockContents 块内容数组
|
|
586
|
-
* @param index 起始插入位置索引
|
|
587
|
-
* @returns 创建结果
|
|
146
|
+
* 在指定父块下批量创建多个子块
|
|
147
|
+
* @see FeishuBlockService.createDocumentBlocks
|
|
588
148
|
*/
|
|
589
149
|
async createDocumentBlocks(documentId, parentBlockId, blockContents, index = 0) {
|
|
590
|
-
|
|
591
|
-
const normalizedDocId = ParamUtils.processDocumentId(documentId);
|
|
592
|
-
const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children?document_revision_id=-1`;
|
|
593
|
-
Logger.debug(`准备请求API端点: ${endpoint}`);
|
|
594
|
-
const payload = {
|
|
595
|
-
children: blockContents,
|
|
596
|
-
index
|
|
597
|
-
};
|
|
598
|
-
Logger.debug(`请求数据: ${JSON.stringify(payload, null, 2)}`);
|
|
599
|
-
const response = await this.post(endpoint, payload);
|
|
600
|
-
return response;
|
|
601
|
-
}
|
|
602
|
-
catch (error) {
|
|
603
|
-
this.handleApiError(error, '批量创建文档块失败');
|
|
604
|
-
return null; // 永远不会执行到这里
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
/**
|
|
608
|
-
* 创建文本块
|
|
609
|
-
* @param documentId 文档ID或URL
|
|
610
|
-
* @param parentBlockId 父块ID
|
|
611
|
-
* @param textContents 文本内容数组,支持普通文本和公式元素
|
|
612
|
-
* @param align 对齐方式,1为左对齐,2为居中,3为右对齐
|
|
613
|
-
* @param index 插入位置索引
|
|
614
|
-
* @returns 创建结果
|
|
615
|
-
*/
|
|
616
|
-
async createTextBlock(documentId, parentBlockId, textContents, align = 1, index = 0) {
|
|
617
|
-
// 处理文本内容样式,支持普通文本和公式元素
|
|
618
|
-
const processedTextContents = textContents.map(item => {
|
|
619
|
-
if (item.equation !== undefined) {
|
|
620
|
-
return {
|
|
621
|
-
equation: item.equation,
|
|
622
|
-
style: BlockFactory.applyDefaultTextStyle(item.style)
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
else {
|
|
626
|
-
return {
|
|
627
|
-
text: item.text || '',
|
|
628
|
-
style: BlockFactory.applyDefaultTextStyle(item.style)
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
const blockContent = this.blockFactory.createTextBlock({
|
|
633
|
-
textContents: processedTextContents,
|
|
634
|
-
align
|
|
635
|
-
});
|
|
636
|
-
return this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* 创建代码块
|
|
640
|
-
* @param documentId 文档ID或URL
|
|
641
|
-
* @param parentBlockId 父块ID
|
|
642
|
-
* @param code 代码内容
|
|
643
|
-
* @param language 语言代码
|
|
644
|
-
* @param wrap 是否自动换行
|
|
645
|
-
* @param index 插入位置索引
|
|
646
|
-
* @returns 创建结果
|
|
647
|
-
*/
|
|
648
|
-
async createCodeBlock(documentId, parentBlockId, code, language = 0, wrap = false, index = 0) {
|
|
649
|
-
const blockContent = this.blockFactory.createCodeBlock({
|
|
650
|
-
code,
|
|
651
|
-
language,
|
|
652
|
-
wrap
|
|
653
|
-
});
|
|
654
|
-
return this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
|
|
655
|
-
}
|
|
656
|
-
/**
|
|
657
|
-
* 创建标题块
|
|
658
|
-
* @param documentId 文档ID或URL
|
|
659
|
-
* @param parentBlockId 父块ID
|
|
660
|
-
* @param text 标题文本
|
|
661
|
-
* @param level 标题级别,1-9
|
|
662
|
-
* @param index 插入位置索引
|
|
663
|
-
* @param align 对齐方式,1为左对齐,2为居中,3为右对齐
|
|
664
|
-
* @returns 创建结果
|
|
665
|
-
*/
|
|
666
|
-
async createHeadingBlock(documentId, parentBlockId, text, level = 1, index = 0, align = 1) {
|
|
667
|
-
const blockContent = this.blockFactory.createHeadingBlock({
|
|
668
|
-
text,
|
|
669
|
-
level,
|
|
670
|
-
align
|
|
671
|
-
});
|
|
672
|
-
return this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
|
|
673
|
-
}
|
|
674
|
-
/**
|
|
675
|
-
* 创建列表块
|
|
676
|
-
* @param documentId 文档ID或URL
|
|
677
|
-
* @param parentBlockId 父块ID
|
|
678
|
-
* @param text 列表项文本
|
|
679
|
-
* @param isOrdered 是否是有序列表
|
|
680
|
-
* @param index 插入位置索引
|
|
681
|
-
* @param align 对齐方式,1为左对齐,2为居中,3为右对齐
|
|
682
|
-
* @returns 创建结果
|
|
683
|
-
*/
|
|
684
|
-
async createListBlock(documentId, parentBlockId, text, isOrdered = false, index = 0, align = 1) {
|
|
685
|
-
const blockContent = this.blockFactory.createListBlock({
|
|
686
|
-
text,
|
|
687
|
-
isOrdered,
|
|
688
|
-
align
|
|
689
|
-
});
|
|
690
|
-
return this.createDocumentBlock(documentId, parentBlockId, blockContent, index);
|
|
150
|
+
return this.blockService.createDocumentBlocks(documentId, parentBlockId, blockContents, index);
|
|
691
151
|
}
|
|
692
152
|
/**
|
|
693
|
-
*
|
|
694
|
-
* @
|
|
695
|
-
* @param parentBlockId 父块ID
|
|
696
|
-
* @param mermaidCode Mermaid代码
|
|
697
|
-
* @param index 插入位置索引
|
|
698
|
-
* @returns 创建结果
|
|
699
|
-
*/
|
|
700
|
-
async createMermaidBlock(documentId, parentBlockId, mermaidCode, index = 0) {
|
|
701
|
-
const normalizedDocId = ParamUtils.processDocumentId(documentId);
|
|
702
|
-
const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children?document_revision_id=-1`;
|
|
703
|
-
const blockContent = {
|
|
704
|
-
block_type: 40,
|
|
705
|
-
add_ons: {
|
|
706
|
-
component_id: "",
|
|
707
|
-
component_type_id: "blk_631fefbbae02400430b8f9f4",
|
|
708
|
-
record: JSON.stringify({
|
|
709
|
-
data: mermaidCode,
|
|
710
|
-
})
|
|
711
|
-
}
|
|
712
|
-
};
|
|
713
|
-
const payload = {
|
|
714
|
-
children: [blockContent],
|
|
715
|
-
index
|
|
716
|
-
};
|
|
717
|
-
Logger.info(`请求创建Mermaid块: ${JSON.stringify(payload).slice(0, 500)}...`);
|
|
718
|
-
const response = await this.post(endpoint, payload);
|
|
719
|
-
return response;
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* 创建表格块
|
|
723
|
-
* @param documentId 文档ID或URL
|
|
724
|
-
* @param parentBlockId 父块ID
|
|
725
|
-
* @param tableConfig 表格配置
|
|
726
|
-
* @param index 插入位置索引
|
|
727
|
-
* @returns 创建结果
|
|
153
|
+
* 创建表格块,支持自定义单元格内容
|
|
154
|
+
* @see FeishuBlockService.createTableBlock
|
|
728
155
|
*/
|
|
729
156
|
async createTableBlock(documentId, parentBlockId, tableConfig, index = 0) {
|
|
730
|
-
|
|
731
|
-
const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/descendant?document_revision_id=-1`;
|
|
732
|
-
// 处理表格配置,为每个单元格创建正确的内容块
|
|
733
|
-
const processedTableConfig = {
|
|
734
|
-
...tableConfig,
|
|
735
|
-
cells: tableConfig.cells?.map(cell => ({
|
|
736
|
-
...cell,
|
|
737
|
-
content: this.createBlockContent(cell.content.blockType, cell.content.options)
|
|
738
|
-
}))
|
|
739
|
-
};
|
|
740
|
-
// 使用 BlockFactory 创建表格块内容
|
|
741
|
-
const tableStructure = this.blockFactory.createTableBlock(processedTableConfig);
|
|
742
|
-
const payload = {
|
|
743
|
-
children_id: tableStructure.children_id,
|
|
744
|
-
descendants: tableStructure.descendants,
|
|
745
|
-
index
|
|
746
|
-
};
|
|
747
|
-
Logger.info(`请求创建表格块: ${tableConfig.rowSize}x${tableConfig.columnSize},单元格数量: ${tableConfig.cells?.length || 0}`);
|
|
748
|
-
const response = await this.post(endpoint, payload);
|
|
749
|
-
// 创建表格成功后,获取单元格中的图片token
|
|
750
|
-
const imageTokens = await this.extractImageTokensFromTable(response, tableStructure.imageBlocks);
|
|
751
|
-
return {
|
|
752
|
-
...response,
|
|
753
|
-
imageTokens: imageTokens
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* 从表格中提取图片块信息(优化版本)
|
|
758
|
-
* @param tableResponse 创建表格的响应数据
|
|
759
|
-
* @param cells 表格配置,包含原始cells信息
|
|
760
|
-
* @returns 图片块信息数组,包含坐标和块ID信息
|
|
761
|
-
*/
|
|
762
|
-
async extractImageTokensFromTable(tableResponse, cells) {
|
|
763
|
-
try {
|
|
764
|
-
const imageTokens = [];
|
|
765
|
-
Logger.info(`tableResponse: ${JSON.stringify(tableResponse)}`);
|
|
766
|
-
// 判断 cells 是否为空
|
|
767
|
-
if (!cells || cells.length === 0) {
|
|
768
|
-
Logger.info('表格中没有图片单元格,跳过图片块信息提取');
|
|
769
|
-
return imageTokens;
|
|
770
|
-
}
|
|
771
|
-
// 创建 localBlockId 到 block_id 的映射
|
|
772
|
-
const blockIdMap = new Map();
|
|
773
|
-
if (tableResponse && tableResponse.block_id_relations) {
|
|
774
|
-
for (const relation of tableResponse.block_id_relations) {
|
|
775
|
-
blockIdMap.set(relation.temporary_block_id, relation.block_id);
|
|
776
|
-
}
|
|
777
|
-
Logger.debug(`创建了 ${blockIdMap.size} 个块ID映射关系`);
|
|
778
|
-
}
|
|
779
|
-
// 遍历所有图片单元格
|
|
780
|
-
for (const cell of cells) {
|
|
781
|
-
const { coordinate, localBlockId } = cell;
|
|
782
|
-
const { row, column } = coordinate;
|
|
783
|
-
// 根据 localBlockId 在创建表格的返回数据中找到 block_id
|
|
784
|
-
const blockId = blockIdMap.get(localBlockId);
|
|
785
|
-
if (!blockId) {
|
|
786
|
-
Logger.warn(`未找到 localBlockId ${localBlockId} 对应的 block_id`);
|
|
787
|
-
continue;
|
|
788
|
-
}
|
|
789
|
-
Logger.debug(`处理单元格 (${row}, ${column}),localBlockId: ${localBlockId},blockId: ${blockId}`);
|
|
790
|
-
// 直接添加块信息
|
|
791
|
-
imageTokens.push({
|
|
792
|
-
row,
|
|
793
|
-
column,
|
|
794
|
-
blockId
|
|
795
|
-
});
|
|
796
|
-
Logger.info(`提取到图片块信息: 位置(${row}, ${column}),blockId: ${blockId}`);
|
|
797
|
-
}
|
|
798
|
-
Logger.info(`成功提取 ${imageTokens.length} 个图片块信息`);
|
|
799
|
-
return imageTokens;
|
|
800
|
-
}
|
|
801
|
-
catch (error) {
|
|
802
|
-
Logger.error(`提取表格图片块信息失败: ${error}`);
|
|
803
|
-
return [];
|
|
804
|
-
}
|
|
157
|
+
return this.blockService.createTableBlock(documentId, parentBlockId, tableConfig, index);
|
|
805
158
|
}
|
|
806
159
|
/**
|
|
807
|
-
*
|
|
808
|
-
* @
|
|
809
|
-
* @param parentBlockId 父块ID(通常是文档ID)
|
|
810
|
-
* @param startIndex 起始索引
|
|
811
|
-
* @param endIndex 结束索引
|
|
812
|
-
* @returns 操作结果
|
|
160
|
+
* 批量删除指定父块下的连续子块(按索引范围)
|
|
161
|
+
* @see FeishuBlockService.deleteDocumentBlocks
|
|
813
162
|
*/
|
|
814
163
|
async deleteDocumentBlocks(documentId, parentBlockId, startIndex, endIndex) {
|
|
815
|
-
|
|
816
|
-
const normalizedDocId = ParamUtils.processDocumentId(documentId);
|
|
817
|
-
const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/children/batch_delete`;
|
|
818
|
-
// 确保索引有效
|
|
819
|
-
if (startIndex < 0 || endIndex < startIndex) {
|
|
820
|
-
throw new Error('无效的索引范围:起始索引必须大于等于0,结束索引必须大于等于起始索引');
|
|
821
|
-
}
|
|
822
|
-
const payload = {
|
|
823
|
-
start_index: startIndex,
|
|
824
|
-
end_index: endIndex
|
|
825
|
-
};
|
|
826
|
-
Logger.info(`开始删除文档块,文档ID: ${normalizedDocId},父块ID: ${parentBlockId},索引范围: ${startIndex}-${endIndex}`);
|
|
827
|
-
const response = await this.delete(endpoint, payload);
|
|
828
|
-
Logger.info('文档块删除成功');
|
|
829
|
-
return response;
|
|
830
|
-
}
|
|
831
|
-
catch (error) {
|
|
832
|
-
this.handleApiError(error, '删除文档块失败');
|
|
833
|
-
}
|
|
164
|
+
return this.blockService.deleteDocumentBlocks(documentId, parentBlockId, startIndex, endIndex);
|
|
834
165
|
}
|
|
835
166
|
/**
|
|
836
|
-
*
|
|
837
|
-
* @
|
|
838
|
-
* @param parentBlockId 父块ID
|
|
839
|
-
* @param blockIndex 块索引
|
|
840
|
-
* @returns 操作结果
|
|
167
|
+
* 根据块类型字符串和选项对象创建块内容对象
|
|
168
|
+
* @see FeishuBlockService.createBlockContent
|
|
841
169
|
*/
|
|
842
|
-
|
|
843
|
-
return this.
|
|
170
|
+
createBlockContent(blockType, options) {
|
|
171
|
+
return this.blockService.createBlockContent(blockType, options);
|
|
844
172
|
}
|
|
845
173
|
/**
|
|
846
|
-
*
|
|
847
|
-
* @
|
|
848
|
-
* @returns 文档ID
|
|
849
|
-
*/
|
|
850
|
-
// public async convertWikiToDocumentId(wikiUrl: string): Promise<string> {
|
|
851
|
-
// try {
|
|
852
|
-
// const wikiToken = ParamUtils.processWikiToken(wikiUrl);
|
|
853
|
-
//
|
|
854
|
-
// // 获取Wiki节点信息
|
|
855
|
-
// const endpoint = `/wiki/v2/spaces/get_node`;
|
|
856
|
-
// const params = { token: wikiToken, obj_type: 'wiki' };
|
|
857
|
-
// const response = await this.get(endpoint, params);
|
|
858
|
-
//
|
|
859
|
-
// if (!response.node || !response.node.obj_token) {
|
|
860
|
-
// throw new Error(`无法从Wiki节点获取文档ID: ${wikiToken}`);
|
|
861
|
-
// }
|
|
862
|
-
//
|
|
863
|
-
// const documentId = response.node.obj_token;
|
|
864
|
-
//
|
|
865
|
-
// Logger.debug(`Wiki转换为文档ID: ${wikiToken} -> ${documentId}`);
|
|
866
|
-
// return documentId;
|
|
867
|
-
// } catch (error) {
|
|
868
|
-
// this.handleApiError(error, 'Wiki转换为文档ID失败');
|
|
869
|
-
// return ''; // 永远不会执行到这里
|
|
870
|
-
// }
|
|
871
|
-
// }
|
|
872
|
-
/**
|
|
873
|
-
* 获取BlockFactory实例
|
|
874
|
-
* @returns BlockFactory实例
|
|
174
|
+
* 获取 BlockFactory 单例实例
|
|
175
|
+
* @see FeishuBlockService.getBlockFactory
|
|
875
176
|
*/
|
|
876
177
|
getBlockFactory() {
|
|
877
|
-
return this.
|
|
178
|
+
return this.blockService.getBlockFactory();
|
|
878
179
|
}
|
|
180
|
+
// ─── 文件夹 / 知识空间服务委托 ───────────────────────────────────
|
|
879
181
|
/**
|
|
880
|
-
*
|
|
881
|
-
* @
|
|
882
|
-
* @param options 块选项
|
|
883
|
-
* @returns 块内容对象
|
|
182
|
+
* 获取当前用户根文件夹的元数据信息
|
|
183
|
+
* @see FeishuFoldService.getRootFolderInfo
|
|
884
184
|
*/
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
// 处理特殊的heading标题格式,如heading1, heading2等
|
|
888
|
-
if (typeof blockType === 'string' && blockType.startsWith('heading')) {
|
|
889
|
-
// 使用正则表达式匹配"heading"后跟1-9的数字格式
|
|
890
|
-
const headingMatch = blockType.match(/^heading([1-9])$/);
|
|
891
|
-
if (headingMatch) {
|
|
892
|
-
// 提取数字部分,例如从"heading1"中提取"1"
|
|
893
|
-
const level = parseInt(headingMatch[1], 10);
|
|
894
|
-
// 额外的安全检查,确保level在1-9范围内
|
|
895
|
-
if (level >= 1 && level <= 9) {
|
|
896
|
-
// 使用level参数创建标题块
|
|
897
|
-
if (!options || Object.keys(options).length === 0) {
|
|
898
|
-
// 没有提供选项时创建默认选项
|
|
899
|
-
options = { heading: { level, content: '', align: 1 } };
|
|
900
|
-
}
|
|
901
|
-
else if (!('heading' in options)) {
|
|
902
|
-
// 提供了选项但没有heading字段
|
|
903
|
-
options = { heading: { level, content: '', align: 1 } };
|
|
904
|
-
}
|
|
905
|
-
else if (options.heading && !('level' in options.heading)) {
|
|
906
|
-
// 提供了heading但没有level字段
|
|
907
|
-
options.heading.level = level;
|
|
908
|
-
}
|
|
909
|
-
blockType = BlockType.HEADING; // 将blockType转为标准的heading类型
|
|
910
|
-
Logger.info(`转换特殊标题格式: ${blockType}${level} -> standard heading with level=${level}`);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
// 使用枚举类型来避免字符串错误
|
|
915
|
-
const blockTypeEnum = blockType;
|
|
916
|
-
// 构建块配置
|
|
917
|
-
const blockConfig = {
|
|
918
|
-
type: blockTypeEnum,
|
|
919
|
-
options: {}
|
|
920
|
-
};
|
|
921
|
-
// 根据块类型处理不同的选项
|
|
922
|
-
switch (blockTypeEnum) {
|
|
923
|
-
case BlockType.TEXT:
|
|
924
|
-
if ('text' in options && options.text) {
|
|
925
|
-
const textOptions = options.text;
|
|
926
|
-
// 处理文本样式,应用默认样式,支持普通文本和公式元素
|
|
927
|
-
const textStyles = textOptions.textStyles || [];
|
|
928
|
-
const processedTextStyles = textStyles.map((item) => {
|
|
929
|
-
if (item.equation !== undefined) {
|
|
930
|
-
return {
|
|
931
|
-
equation: item.equation,
|
|
932
|
-
style: BlockFactory.applyDefaultTextStyle(item.style)
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
else {
|
|
936
|
-
return {
|
|
937
|
-
text: item.text || '',
|
|
938
|
-
style: BlockFactory.applyDefaultTextStyle(item.style)
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
});
|
|
942
|
-
blockConfig.options = {
|
|
943
|
-
textContents: processedTextStyles,
|
|
944
|
-
align: textOptions.align || 1
|
|
945
|
-
};
|
|
946
|
-
}
|
|
947
|
-
break;
|
|
948
|
-
case BlockType.CODE:
|
|
949
|
-
if ('code' in options && options.code) {
|
|
950
|
-
const codeOptions = options.code;
|
|
951
|
-
blockConfig.options = {
|
|
952
|
-
code: codeOptions.code || '',
|
|
953
|
-
language: codeOptions.language === 0 ? 0 : (codeOptions.language || 0),
|
|
954
|
-
wrap: codeOptions.wrap || false
|
|
955
|
-
};
|
|
956
|
-
}
|
|
957
|
-
break;
|
|
958
|
-
case BlockType.HEADING:
|
|
959
|
-
if ('heading' in options && options.heading) {
|
|
960
|
-
const headingOptions = options.heading;
|
|
961
|
-
blockConfig.options = {
|
|
962
|
-
text: headingOptions.content || '',
|
|
963
|
-
level: headingOptions.level || 1,
|
|
964
|
-
align: (headingOptions.align === 1 || headingOptions.align === 2 || headingOptions.align === 3)
|
|
965
|
-
? headingOptions.align : 1
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
break;
|
|
969
|
-
case BlockType.LIST:
|
|
970
|
-
if ('list' in options && options.list) {
|
|
971
|
-
const listOptions = options.list;
|
|
972
|
-
blockConfig.options = {
|
|
973
|
-
text: listOptions.content || '',
|
|
974
|
-
isOrdered: listOptions.isOrdered || false,
|
|
975
|
-
align: (listOptions.align === 1 || listOptions.align === 2 || listOptions.align === 3)
|
|
976
|
-
? listOptions.align : 1
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
break;
|
|
980
|
-
case BlockType.IMAGE:
|
|
981
|
-
if ('image' in options && options.image) {
|
|
982
|
-
const imageOptions = options.image;
|
|
983
|
-
blockConfig.options = {
|
|
984
|
-
width: imageOptions.width || 100,
|
|
985
|
-
height: imageOptions.height || 100
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
|
-
else {
|
|
989
|
-
// 默认图片块选项
|
|
990
|
-
blockConfig.options = {
|
|
991
|
-
width: 100,
|
|
992
|
-
height: 100
|
|
993
|
-
};
|
|
994
|
-
}
|
|
995
|
-
break;
|
|
996
|
-
case BlockType.MERMAID:
|
|
997
|
-
if ('mermaid' in options && options.mermaid) {
|
|
998
|
-
const mermaidOptions = options.mermaid;
|
|
999
|
-
blockConfig.options = {
|
|
1000
|
-
code: mermaidOptions.code,
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
break;
|
|
1004
|
-
case BlockType.WHITEBOARD:
|
|
1005
|
-
if ('whiteboard' in options && options.whiteboard) {
|
|
1006
|
-
const whiteboardOptions = options.whiteboard;
|
|
1007
|
-
blockConfig.options = {
|
|
1008
|
-
align: (whiteboardOptions.align === 1 || whiteboardOptions.align === 2 || whiteboardOptions.align === 3)
|
|
1009
|
-
? whiteboardOptions.align : 1
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
else {
|
|
1013
|
-
// 默认画板块选项
|
|
1014
|
-
blockConfig.options = {
|
|
1015
|
-
align: 1
|
|
1016
|
-
};
|
|
1017
|
-
}
|
|
1018
|
-
break;
|
|
1019
|
-
default:
|
|
1020
|
-
Logger.warn(`未知的块类型: ${blockType},尝试作为标准类型处理`);
|
|
1021
|
-
if ('text' in options) {
|
|
1022
|
-
blockConfig.type = BlockType.TEXT;
|
|
1023
|
-
const textOptions = options.text;
|
|
1024
|
-
// 处理文本样式,应用默认样式,支持普通文本和公式元素
|
|
1025
|
-
const textStyles = textOptions.textStyles || [];
|
|
1026
|
-
const processedTextStyles = textStyles.map((item) => {
|
|
1027
|
-
if (item.equation !== undefined) {
|
|
1028
|
-
return {
|
|
1029
|
-
equation: item.equation,
|
|
1030
|
-
style: BlockFactory.applyDefaultTextStyle(item.style)
|
|
1031
|
-
};
|
|
1032
|
-
}
|
|
1033
|
-
else {
|
|
1034
|
-
return {
|
|
1035
|
-
text: item.text || '',
|
|
1036
|
-
style: BlockFactory.applyDefaultTextStyle(item.style)
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
});
|
|
1040
|
-
blockConfig.options = {
|
|
1041
|
-
textContents: processedTextStyles,
|
|
1042
|
-
align: textOptions.align || 1
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
else if ('code' in options) {
|
|
1046
|
-
blockConfig.type = BlockType.CODE;
|
|
1047
|
-
const codeOptions = options.code;
|
|
1048
|
-
blockConfig.options = {
|
|
1049
|
-
code: codeOptions.code || '',
|
|
1050
|
-
language: codeOptions.language === 0 ? 0 : (codeOptions.language || 0),
|
|
1051
|
-
wrap: codeOptions.wrap || false
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
|
-
else if ('heading' in options) {
|
|
1055
|
-
blockConfig.type = BlockType.HEADING;
|
|
1056
|
-
const headingOptions = options.heading;
|
|
1057
|
-
blockConfig.options = {
|
|
1058
|
-
text: headingOptions.content || '',
|
|
1059
|
-
level: headingOptions.level || 1,
|
|
1060
|
-
align: (headingOptions.align === 1 || headingOptions.align === 2 || headingOptions.align === 3)
|
|
1061
|
-
? headingOptions.align : 1
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
else if ('list' in options) {
|
|
1065
|
-
blockConfig.type = BlockType.LIST;
|
|
1066
|
-
const listOptions = options.list;
|
|
1067
|
-
blockConfig.options = {
|
|
1068
|
-
text: listOptions.content || '',
|
|
1069
|
-
isOrdered: listOptions.isOrdered || false,
|
|
1070
|
-
align: (listOptions.align === 1 || listOptions.align === 2 || listOptions.align === 3)
|
|
1071
|
-
? listOptions.align : 1
|
|
1072
|
-
};
|
|
1073
|
-
}
|
|
1074
|
-
else if ('image' in options) {
|
|
1075
|
-
blockConfig.type = BlockType.IMAGE;
|
|
1076
|
-
const imageOptions = options.image;
|
|
1077
|
-
blockConfig.options = {
|
|
1078
|
-
width: imageOptions.width || 100,
|
|
1079
|
-
height: imageOptions.height || 100
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
else if ("mermaid" in options) {
|
|
1083
|
-
blockConfig.type = BlockType.MERMAID;
|
|
1084
|
-
const mermaidConfig = options.mermaid;
|
|
1085
|
-
blockConfig.options = {
|
|
1086
|
-
code: mermaidConfig.code,
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
else if ("whiteboard" in options) {
|
|
1090
|
-
blockConfig.type = BlockType.WHITEBOARD;
|
|
1091
|
-
const whiteboardConfig = options.whiteboard;
|
|
1092
|
-
blockConfig.options = {
|
|
1093
|
-
align: (whiteboardConfig.align === 1 || whiteboardConfig.align === 2 || whiteboardConfig.align === 3)
|
|
1094
|
-
? whiteboardConfig.align : 1
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
break;
|
|
1098
|
-
}
|
|
1099
|
-
// 记录调试信息
|
|
1100
|
-
Logger.debug(`创建块内容: 类型=${blockConfig.type}, 选项=${JSON.stringify(blockConfig.options)}`);
|
|
1101
|
-
// 使用BlockFactory创建块
|
|
1102
|
-
return this.blockFactory.createBlock(blockConfig.type, blockConfig.options);
|
|
1103
|
-
}
|
|
1104
|
-
catch (error) {
|
|
1105
|
-
Logger.error(`创建块内容对象失败: ${error}`);
|
|
1106
|
-
return null;
|
|
1107
|
-
}
|
|
185
|
+
async getRootFolderInfo() {
|
|
186
|
+
return this.foldService.getRootFolderInfo();
|
|
1108
187
|
}
|
|
1109
188
|
/**
|
|
1110
|
-
*
|
|
1111
|
-
* @
|
|
1112
|
-
* @param extra 额外参数,可选
|
|
1113
|
-
* @returns 图片二进制数据
|
|
189
|
+
* 获取指定文件夹内的文件和子文件夹列表
|
|
190
|
+
* @see FeishuFoldService.getFolderFileList
|
|
1114
191
|
*/
|
|
1115
|
-
async
|
|
1116
|
-
|
|
1117
|
-
Logger.info(`开始获取图片资源,媒体ID: ${mediaId}`);
|
|
1118
|
-
if (!mediaId) {
|
|
1119
|
-
throw new Error('媒体ID不能为空');
|
|
1120
|
-
}
|
|
1121
|
-
const endpoint = `/drive/v1/medias/${mediaId}/download`;
|
|
1122
|
-
const params = {};
|
|
1123
|
-
if (extra) {
|
|
1124
|
-
params.extra = extra;
|
|
1125
|
-
}
|
|
1126
|
-
// 使用通用的request方法获取二进制响应
|
|
1127
|
-
const response = await this.request(endpoint, 'GET', params, true, {}, 'arraybuffer');
|
|
1128
|
-
const imageBuffer = Buffer.from(response);
|
|
1129
|
-
Logger.info(`图片资源获取成功,大小: ${imageBuffer.length} 字节`);
|
|
1130
|
-
return imageBuffer;
|
|
1131
|
-
}
|
|
1132
|
-
catch (error) {
|
|
1133
|
-
this.handleApiError(error, '获取图片资源失败');
|
|
1134
|
-
return Buffer.from([]); // 永远不会执行到这里
|
|
1135
|
-
}
|
|
192
|
+
async getFolderFileList(folderToken, orderBy = 'EditedTime', direction = 'DESC') {
|
|
193
|
+
return this.foldService.getFolderFileList(folderToken, orderBy, direction);
|
|
1136
194
|
}
|
|
1137
195
|
/**
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
* @returns 根文件夹信息
|
|
196
|
+
* 在指定文件夹下创建子文件夹
|
|
197
|
+
* @see FeishuFoldService.createFolder
|
|
1141
198
|
*/
|
|
1142
|
-
async
|
|
1143
|
-
|
|
1144
|
-
const endpoint = '/drive/explorer/v2/root_folder/meta';
|
|
1145
|
-
const response = await this.get(endpoint);
|
|
1146
|
-
Logger.debug('获取根文件夹信息成功:', response);
|
|
1147
|
-
return response;
|
|
1148
|
-
}
|
|
1149
|
-
catch (error) {
|
|
1150
|
-
this.handleApiError(error, '获取飞书根文件夹信息失败');
|
|
1151
|
-
}
|
|
199
|
+
async createFolder(folderToken, name) {
|
|
200
|
+
return this.foldService.createFolder(folderToken, name);
|
|
1152
201
|
}
|
|
1153
202
|
/**
|
|
1154
|
-
*
|
|
1155
|
-
* @
|
|
1156
|
-
* @returns 所有知识空间列表(仅包含 items 数组,不包含 has_more 和 page_token)
|
|
203
|
+
* 获取所有知识空间列表(自动分页)
|
|
204
|
+
* @see FeishuFoldService.getAllWikiSpacesList
|
|
1157
205
|
*/
|
|
1158
206
|
async getAllWikiSpacesList(pageSize = 20) {
|
|
1159
|
-
|
|
1160
|
-
Logger.info(`开始获取所有知识空间列表,每页数量: ${pageSize}`);
|
|
1161
|
-
const endpoint = '/wiki/v2/spaces';
|
|
1162
|
-
let allItems = [];
|
|
1163
|
-
let pageToken = undefined;
|
|
1164
|
-
let hasMore = true;
|
|
1165
|
-
// 循环获取所有页的数据
|
|
1166
|
-
while (hasMore) {
|
|
1167
|
-
const params = { page_size: pageSize };
|
|
1168
|
-
if (pageToken) {
|
|
1169
|
-
params.page_token = pageToken;
|
|
1170
|
-
}
|
|
1171
|
-
Logger.debug(`请求知识空间列表,page_token: ${pageToken || 'null'}, page_size: ${pageSize}`);
|
|
1172
|
-
const response = await this.get(endpoint, params);
|
|
1173
|
-
if (response && response.items) {
|
|
1174
|
-
const newItems = response.items;
|
|
1175
|
-
allItems = [...allItems, ...newItems];
|
|
1176
|
-
hasMore = response.has_more || false;
|
|
1177
|
-
pageToken = response.page_token;
|
|
1178
|
-
Logger.debug(`当前页获取到 ${newItems.length} 个知识空间,累计 ${allItems.length} 个,hasMore: ${hasMore}`);
|
|
1179
|
-
}
|
|
1180
|
-
else {
|
|
1181
|
-
hasMore = false;
|
|
1182
|
-
Logger.warn('知识空间列表响应格式异常:', JSON.stringify(response, null, 2));
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
Logger.info(`知识空间列表获取完成,共 ${allItems.length} 个空间`);
|
|
1186
|
-
return allItems; // 直接返回数组,不包装在 items 中
|
|
1187
|
-
}
|
|
1188
|
-
catch (error) {
|
|
1189
|
-
this.handleApiError(error, '获取知识空间列表失败');
|
|
1190
|
-
}
|
|
207
|
+
return this.foldService.getAllWikiSpacesList(pageSize);
|
|
1191
208
|
}
|
|
1192
209
|
/**
|
|
1193
|
-
*
|
|
1194
|
-
* @
|
|
1195
|
-
* @param parentNodeToken 父节点Token(可选,为空时获取根节点)
|
|
1196
|
-
* @param pageSize 每页数量,默认20
|
|
1197
|
-
* @returns 所有子节点列表(仅包含 items 数组,不包含 has_more 和 page_token)
|
|
210
|
+
* 获取指定知识空间下的所有子节点(自动分页)
|
|
211
|
+
* @see FeishuFoldService.getAllWikiSpaceNodes
|
|
1198
212
|
*/
|
|
1199
213
|
async getAllWikiSpaceNodes(spaceId, parentNodeToken, pageSize = 20) {
|
|
1200
|
-
|
|
1201
|
-
Logger.info(`开始获取知识空间子节点列表,space_id: ${spaceId}, parent_node_token: ${parentNodeToken || 'null'}, 每页数量: ${pageSize}`);
|
|
1202
|
-
const endpoint = `/wiki/v2/spaces/${spaceId}/nodes`;
|
|
1203
|
-
let allItems = [];
|
|
1204
|
-
let pageToken = undefined;
|
|
1205
|
-
let hasMore = true;
|
|
1206
|
-
// 循环获取所有页的数据
|
|
1207
|
-
while (hasMore) {
|
|
1208
|
-
const params = { page_size: pageSize };
|
|
1209
|
-
if (parentNodeToken) {
|
|
1210
|
-
params.parent_node_token = parentNodeToken;
|
|
1211
|
-
}
|
|
1212
|
-
if (pageToken) {
|
|
1213
|
-
params.page_token = pageToken;
|
|
1214
|
-
}
|
|
1215
|
-
Logger.debug(`请求知识空间子节点列表,page_token: ${pageToken || 'null'}, page_size: ${pageSize}`);
|
|
1216
|
-
const response = await this.get(endpoint, params);
|
|
1217
|
-
if (response && response.items) {
|
|
1218
|
-
const newItems = response.items;
|
|
1219
|
-
allItems = [...allItems, ...newItems];
|
|
1220
|
-
hasMore = response.has_more || false;
|
|
1221
|
-
pageToken = response.page_token;
|
|
1222
|
-
Logger.debug(`当前页获取到 ${newItems.length} 个子节点,累计 ${allItems.length} 个,hasMore: ${hasMore}`);
|
|
1223
|
-
}
|
|
1224
|
-
else {
|
|
1225
|
-
hasMore = false;
|
|
1226
|
-
Logger.warn('知识空间子节点列表响应格式异常:', JSON.stringify(response, null, 2));
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
Logger.info(`知识空间子节点列表获取完成,共 ${allItems.length} 个节点`);
|
|
1230
|
-
return allItems; // 直接返回数组,不包装在 items 中
|
|
1231
|
-
}
|
|
1232
|
-
catch (error) {
|
|
1233
|
-
this.handleApiError(error, '获取知识空间子节点列表失败');
|
|
1234
|
-
}
|
|
214
|
+
return this.foldService.getAllWikiSpaceNodes(spaceId, parentNodeToken, pageSize);
|
|
1235
215
|
}
|
|
1236
216
|
/**
|
|
1237
|
-
*
|
|
1238
|
-
* @
|
|
1239
|
-
* @param lang 语言(仅当 spaceId 为 'my_library' 时有效),默认'en'
|
|
1240
|
-
* @returns 知识空间信息
|
|
217
|
+
* 获取指定知识空间的详细信息
|
|
218
|
+
* @see FeishuFoldService.getWikiSpaceInfo
|
|
1241
219
|
*/
|
|
1242
220
|
async getWikiSpaceInfo(spaceId, lang = 'en') {
|
|
1243
|
-
|
|
1244
|
-
const endpoint = `/wiki/v2/spaces/${spaceId}`;
|
|
1245
|
-
const params = {};
|
|
1246
|
-
// 当 spaceId 为 'my_library' 时,添加 lang 参数
|
|
1247
|
-
if (spaceId === 'my_library') {
|
|
1248
|
-
params.lang = lang;
|
|
1249
|
-
}
|
|
1250
|
-
const response = await this.get(endpoint, params);
|
|
1251
|
-
Logger.debug(`获取知识空间信息成功 (space_id: ${spaceId}):`, response);
|
|
1252
|
-
// 如果响应中包含 space 字段,直接返回 space 对象;否则返回整个响应
|
|
1253
|
-
if (response && response.space) {
|
|
1254
|
-
return response.space;
|
|
1255
|
-
}
|
|
1256
|
-
return response;
|
|
1257
|
-
}
|
|
1258
|
-
catch (error) {
|
|
1259
|
-
this.handleApiError(error, `获取知识空间信息失败 (space_id: ${spaceId})`);
|
|
1260
|
-
}
|
|
221
|
+
return this.foldService.getWikiSpaceInfo(spaceId, lang);
|
|
1261
222
|
}
|
|
1262
223
|
/**
|
|
1263
|
-
*
|
|
1264
|
-
* @
|
|
1265
|
-
* @param title 节点标题
|
|
1266
|
-
* @param parentNodeToken 父节点Token(可选,为空时在根节点下创建)
|
|
1267
|
-
* @returns 创建的节点信息,包含 node_token(节点ID)和 obj_token(文档ID)
|
|
224
|
+
* 在知识空间中创建文档节点
|
|
225
|
+
* @see FeishuFoldService.createWikiSpaceNode
|
|
1268
226
|
*/
|
|
1269
227
|
async createWikiSpaceNode(spaceId, title, parentNodeToken) {
|
|
1270
|
-
|
|
1271
|
-
Logger.info(`开始创建知识空间节点,space_id: ${spaceId}, title: ${title}, parent_node_token: ${parentNodeToken || 'null(根节点)'}`);
|
|
1272
|
-
const endpoint = `/wiki/v2/spaces/${spaceId}/nodes`;
|
|
1273
|
-
const payload = {
|
|
1274
|
-
title,
|
|
1275
|
-
obj_type: 'docx',
|
|
1276
|
-
node_type: 'origin',
|
|
1277
|
-
};
|
|
1278
|
-
if (parentNodeToken) {
|
|
1279
|
-
payload.parent_node_token = parentNodeToken;
|
|
1280
|
-
}
|
|
1281
|
-
const response = await this.post(endpoint, payload);
|
|
1282
|
-
// 提取 node 对象,统一返回格式
|
|
1283
|
-
if (response && response.data && response.data.node) {
|
|
1284
|
-
const node = response.data.node;
|
|
1285
|
-
Logger.info(`知识空间节点创建成功,node_token: ${node.node_token}, obj_token: ${node.obj_token}`);
|
|
1286
|
-
return node;
|
|
1287
|
-
}
|
|
1288
|
-
Logger.info(`知识空间节点创建成功`);
|
|
1289
|
-
return response;
|
|
1290
|
-
}
|
|
1291
|
-
catch (error) {
|
|
1292
|
-
this.handleApiError(error, '创建知识空间节点失败');
|
|
1293
|
-
}
|
|
228
|
+
return this.foldService.createWikiSpaceNode(spaceId, title, parentNodeToken);
|
|
1294
229
|
}
|
|
230
|
+
// ─── Search 服务委托 ──────────────────────────────────────────────
|
|
1295
231
|
/**
|
|
1296
|
-
*
|
|
1297
|
-
* @
|
|
1298
|
-
* @param orderBy 排序方式,默认按修改时间排序
|
|
1299
|
-
* @param direction 排序方向,默认降序
|
|
1300
|
-
* @returns 文件清单信息
|
|
1301
|
-
*/
|
|
1302
|
-
async getFolderFileList(folderToken, orderBy = 'EditedTime', direction = 'DESC') {
|
|
1303
|
-
try {
|
|
1304
|
-
const endpoint = '/drive/v1/files';
|
|
1305
|
-
const params = {
|
|
1306
|
-
folder_token: folderToken,
|
|
1307
|
-
order_by: orderBy,
|
|
1308
|
-
direction: direction
|
|
1309
|
-
};
|
|
1310
|
-
const response = await this.get(endpoint, params);
|
|
1311
|
-
Logger.debug(`获取文件夹(${folderToken})中的文件清单成功,文件数量: ${response.files?.length || 0}`);
|
|
1312
|
-
return response;
|
|
1313
|
-
}
|
|
1314
|
-
catch (error) {
|
|
1315
|
-
this.handleApiError(error, '获取文件夹中的文件清单失败');
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
/**
|
|
1319
|
-
* 创建文件夹
|
|
1320
|
-
* @param folderToken 父文件夹Token
|
|
1321
|
-
* @param name 文件夹名称
|
|
1322
|
-
* @returns 创建的文件夹信息
|
|
1323
|
-
*/
|
|
1324
|
-
async createFolder(folderToken, name) {
|
|
1325
|
-
try {
|
|
1326
|
-
const endpoint = '/drive/v1/files/create_folder';
|
|
1327
|
-
const payload = {
|
|
1328
|
-
folder_token: folderToken,
|
|
1329
|
-
name: name
|
|
1330
|
-
};
|
|
1331
|
-
const response = await this.post(endpoint, payload);
|
|
1332
|
-
Logger.debug(`文件夹创建成功, token: ${response.token}, url: ${response.url}`);
|
|
1333
|
-
return response;
|
|
1334
|
-
}
|
|
1335
|
-
catch (error) {
|
|
1336
|
-
this.handleApiError(error, '创建文件夹失败');
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
/**
|
|
1340
|
-
* 搜索飞书文档(支持分页和轮询)
|
|
1341
|
-
* @param searchKey 搜索关键字
|
|
1342
|
-
* @param maxSize 最大返回数量,如果未指定则只返回一页
|
|
1343
|
-
* @param offset 偏移量,用于分页,默认0
|
|
1344
|
-
* @returns 搜索结果,包含数据和分页信息
|
|
232
|
+
* 搜索飞书文档,支持分页
|
|
233
|
+
* @see FeishuSearchService.searchDocuments
|
|
1345
234
|
*/
|
|
1346
235
|
async searchDocuments(searchKey, maxSize, offset = 0) {
|
|
1347
|
-
|
|
1348
|
-
Logger.info(`开始搜索文档,关键字: ${searchKey}, maxSize: ${maxSize || '未指定'}, offset: ${offset}`);
|
|
1349
|
-
const endpoint = `/suite/docs-api/search/object`;
|
|
1350
|
-
const PAGE_SIZE = 50; // 文档API固定使用50
|
|
1351
|
-
const allResults = [];
|
|
1352
|
-
let currentOffset = offset;
|
|
1353
|
-
let hasMore = true;
|
|
1354
|
-
// 如果指定了maxSize,轮询获取直到满足maxSize或没有更多数据
|
|
1355
|
-
while (hasMore && (maxSize === undefined || allResults.length < maxSize)) {
|
|
1356
|
-
const payload = {
|
|
1357
|
-
search_key: searchKey,
|
|
1358
|
-
docs_types: ["doc"],
|
|
1359
|
-
count: PAGE_SIZE,
|
|
1360
|
-
offset: currentOffset
|
|
1361
|
-
};
|
|
1362
|
-
Logger.debug(`请求搜索文档,offset: ${currentOffset}, count: ${PAGE_SIZE}`);
|
|
1363
|
-
const response = await this.post(endpoint, payload);
|
|
1364
|
-
Logger.debug('搜索响应:', JSON.stringify(response, null, 2));
|
|
1365
|
-
if (response && response.docs_entities) {
|
|
1366
|
-
const resultCount = response.docs_entities.length;
|
|
1367
|
-
const apiHasMore = response.has_more || false;
|
|
1368
|
-
// 更新offset
|
|
1369
|
-
currentOffset += resultCount;
|
|
1370
|
-
if (resultCount > 0) {
|
|
1371
|
-
allResults.push(...response.docs_entities);
|
|
1372
|
-
hasMore = apiHasMore; // 保持API返回的hasMore
|
|
1373
|
-
// 如果指定了maxSize,只取需要的数量
|
|
1374
|
-
if (maxSize == undefined || allResults.length >= maxSize) {
|
|
1375
|
-
// 如果已经达到maxSize,停止轮询,但保持API返回的hasMore值
|
|
1376
|
-
Logger.debug(`已达到maxSize ${maxSize},停止获取,但API还有更多: ${hasMore}`);
|
|
1377
|
-
break; // 停止轮询
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
else {
|
|
1381
|
-
hasMore = false;
|
|
1382
|
-
}
|
|
1383
|
-
Logger.debug(`文档搜索进度: 已获取 ${allResults.length} 条,hasMore: ${hasMore}`);
|
|
1384
|
-
}
|
|
1385
|
-
else {
|
|
1386
|
-
Logger.warn('搜索响应格式异常:', JSON.stringify(response, null, 2));
|
|
1387
|
-
hasMore = false;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
const resultCount = allResults.length;
|
|
1391
|
-
Logger.info(`文档搜索完成,找到 ${resultCount} 个结果${maxSize ? `(maxSize: ${maxSize})` : ''}`);
|
|
1392
|
-
return {
|
|
1393
|
-
items: allResults,
|
|
1394
|
-
hasMore: hasMore,
|
|
1395
|
-
nextOffset: currentOffset
|
|
1396
|
-
};
|
|
1397
|
-
}
|
|
1398
|
-
catch (error) {
|
|
1399
|
-
this.handleApiError(error, '搜索文档失败');
|
|
1400
|
-
throw error;
|
|
1401
|
-
}
|
|
236
|
+
return this.searchService.searchDocuments(searchKey, maxSize, offset);
|
|
1402
237
|
}
|
|
1403
238
|
/**
|
|
1404
|
-
*
|
|
1405
|
-
* @
|
|
1406
|
-
* @param maxSize 最大返回数量,如果未指定则只返回一页
|
|
1407
|
-
* @param pageToken 分页token,用于获取下一页,可选
|
|
1408
|
-
* @returns 搜索结果,包含数据和分页信息
|
|
239
|
+
* 搜索飞书知识库节点,支持分页
|
|
240
|
+
* @see FeishuSearchService.searchWikiNodes
|
|
1409
241
|
*/
|
|
1410
242
|
async searchWikiNodes(query, maxSize, pageToken) {
|
|
1411
|
-
|
|
1412
|
-
Logger.info(`开始搜索知识库,关键字: ${query}, maxSize: ${maxSize || '未指定'}, pageToken: ${pageToken || '无'}`);
|
|
1413
|
-
const endpoint = `/wiki/v1/nodes/search`;
|
|
1414
|
-
const PAGE_SIZE = 20; // Wiki API每页固定使用20
|
|
1415
|
-
const allResults = [];
|
|
1416
|
-
let currentPageToken = pageToken;
|
|
1417
|
-
let hasMore = true;
|
|
1418
|
-
// 如果指定了maxSize,轮询获取直到满足maxSize或没有更多数据
|
|
1419
|
-
while (hasMore && (maxSize === undefined || allResults.length < maxSize)) {
|
|
1420
|
-
const size = Math.min(PAGE_SIZE, 100); // Wiki API最大支持100
|
|
1421
|
-
let url = `${endpoint}?page_size=${size}`;
|
|
1422
|
-
if (currentPageToken) {
|
|
1423
|
-
url += `&page_token=${currentPageToken}`;
|
|
1424
|
-
}
|
|
1425
|
-
const payload = {
|
|
1426
|
-
query: query
|
|
1427
|
-
};
|
|
1428
|
-
Logger.debug(`请求搜索知识库,pageSize: ${size}, pageToken: ${currentPageToken || '无'}`);
|
|
1429
|
-
const response = await this.post(url, payload);
|
|
1430
|
-
Logger.debug('知识库搜索响应:', JSON.stringify(response, null, 2));
|
|
1431
|
-
// baseService的post方法已经提取了response.data.data,所以response直接就是data字段的内容
|
|
1432
|
-
if (response && response.items) {
|
|
1433
|
-
const resultCount = response.items?.length || 0;
|
|
1434
|
-
const apiHasMore = response.has_more || false;
|
|
1435
|
-
currentPageToken = response.page_token || null;
|
|
1436
|
-
if (resultCount > 0) {
|
|
1437
|
-
allResults.push(...response.items);
|
|
1438
|
-
hasMore = apiHasMore; // 保持API返回的hasMore,以便下次调用可以继续
|
|
1439
|
-
if (maxSize !== undefined) {
|
|
1440
|
-
// 如果已经达到maxSize,停止轮询,但保持API返回的hasMore值
|
|
1441
|
-
if (allResults.length >= maxSize) {
|
|
1442
|
-
Logger.debug(`已达到maxSize ${maxSize},停止获取,但API还有更多: ${hasMore}`);
|
|
1443
|
-
break; // 停止轮询
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
else {
|
|
1447
|
-
break; // 只返回一页
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
else {
|
|
1451
|
-
hasMore = false;
|
|
1452
|
-
}
|
|
1453
|
-
Logger.debug(`知识库搜索进度: 已获取 ${allResults.length} 条,hasMore: ${hasMore}`);
|
|
1454
|
-
}
|
|
1455
|
-
else {
|
|
1456
|
-
Logger.warn('知识库搜索响应格式异常:', JSON.stringify(response, null, 2));
|
|
1457
|
-
hasMore = false;
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
const resultCount = allResults.length;
|
|
1461
|
-
Logger.info(`知识库搜索完成,找到 ${resultCount} 个结果${maxSize ? `(maxSize: ${maxSize})` : ''}`);
|
|
1462
|
-
return {
|
|
1463
|
-
items: allResults,
|
|
1464
|
-
hasMore: hasMore,
|
|
1465
|
-
pageToken: currentPageToken,
|
|
1466
|
-
count: resultCount
|
|
1467
|
-
};
|
|
1468
|
-
}
|
|
1469
|
-
catch (error) {
|
|
1470
|
-
this.handleApiError(error, '搜索知识库失败');
|
|
1471
|
-
throw error;
|
|
1472
|
-
}
|
|
243
|
+
return this.searchService.searchWikiNodes(query, maxSize, pageToken);
|
|
1473
244
|
}
|
|
1474
245
|
/**
|
|
1475
|
-
*
|
|
1476
|
-
* @
|
|
1477
|
-
* @param searchType 搜索类型:'document' | 'wiki' | 'both',默认'both'
|
|
1478
|
-
* @param offset 文档搜索的偏移量,可选(用于分页)
|
|
1479
|
-
* @param pageToken 知识库搜索的分页token,可选(用于分页)
|
|
1480
|
-
* @returns 搜索结果,包含documents、wikis和分页信息
|
|
246
|
+
* 统一搜索入口,可同时搜索文档和知识库节点
|
|
247
|
+
* @see FeishuSearchService.search
|
|
1481
248
|
*/
|
|
1482
249
|
async search(searchKey, searchType = 'both', offset, pageToken) {
|
|
1483
|
-
|
|
1484
|
-
// wiki搜索不支持tenant认证,如果是tenant模式则强制使用document搜索
|
|
1485
|
-
if (this.config.feishu.authType === 'tenant' && (searchType === 'wiki' || searchType === 'both')) {
|
|
1486
|
-
Logger.info(`租户认证模式下wiki搜索不支持,强制将searchType从 ${searchType} 修改为 document`);
|
|
1487
|
-
searchType = 'document';
|
|
1488
|
-
}
|
|
1489
|
-
const MAX_TOTAL_RESULTS = 100; // 总共最多200条(文档+wiki合计)
|
|
1490
|
-
const docOffset = offset ?? 0;
|
|
1491
|
-
Logger.info(`开始统一搜索,关键字: ${searchKey}, 类型: ${searchType}, offset: ${docOffset}, pageToken: ${pageToken || '无'}`);
|
|
1492
|
-
const documents = [];
|
|
1493
|
-
const wikis = [];
|
|
1494
|
-
// 用于生成分页指导的内部变量
|
|
1495
|
-
let documentOffset = docOffset;
|
|
1496
|
-
let wikiPageToken = null;
|
|
1497
|
-
let documentHasMore = false;
|
|
1498
|
-
let wikiHasMore = false;
|
|
1499
|
-
// 搜索文档
|
|
1500
|
-
if (searchType === 'document' || searchType === 'both') {
|
|
1501
|
-
// 计算文档的最大数量(不超过总限制)
|
|
1502
|
-
const maxDocCount = MAX_TOTAL_RESULTS;
|
|
1503
|
-
const docResult = await this.searchDocuments(searchKey, maxDocCount, docOffset);
|
|
1504
|
-
if (docResult.items && docResult.items.length > 0) {
|
|
1505
|
-
documents.push(...docResult.items);
|
|
1506
|
-
documentOffset = docResult.nextOffset;
|
|
1507
|
-
documentHasMore = docResult.hasMore;
|
|
1508
|
-
Logger.debug(`文档搜索: 获取 ${docResult.items.length} 条,新offset: ${documentOffset}, hasMore: ${documentHasMore}`);
|
|
1509
|
-
}
|
|
1510
|
-
else {
|
|
1511
|
-
documentHasMore = false;
|
|
1512
|
-
Logger.debug('文档搜索: 无结果');
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
// 搜索知识库(仅在文档+wiki总数未达到100条时继续)
|
|
1516
|
-
if (searchType === 'wiki' || searchType === 'both') {
|
|
1517
|
-
const currentDocCount = documents.length;
|
|
1518
|
-
const remainingCount = MAX_TOTAL_RESULTS - currentDocCount;
|
|
1519
|
-
// 如果还有剩余空间,获取知识库
|
|
1520
|
-
if (remainingCount > 0) {
|
|
1521
|
-
const wikiResult = await this.searchWikiNodes(searchKey, remainingCount, pageToken);
|
|
1522
|
-
if (wikiResult.items && wikiResult.items.length > 0) {
|
|
1523
|
-
wikis.push(...wikiResult.items);
|
|
1524
|
-
wikiPageToken = wikiResult.pageToken;
|
|
1525
|
-
wikiHasMore = wikiResult.hasMore;
|
|
1526
|
-
Logger.debug(`知识库搜索: 获取 ${wikiResult.items.length} 条,pageToken: ${wikiPageToken || '无'}, hasMore: ${wikiHasMore}`);
|
|
1527
|
-
}
|
|
1528
|
-
else {
|
|
1529
|
-
wikiHasMore = false;
|
|
1530
|
-
Logger.debug('知识库搜索: 无结果');
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
else {
|
|
1534
|
-
Logger.info(`已达到总限制 ${MAX_TOTAL_RESULTS} 条,不再获取知识库`);
|
|
1535
|
-
wikiHasMore = true;
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
// 生成分页指导信息
|
|
1539
|
-
const paginationGuide = this.generatePaginationGuide(searchType, documentHasMore, wikiHasMore, documentOffset, wikiPageToken);
|
|
1540
|
-
const total = documents.length + wikis.length;
|
|
1541
|
-
const hasMore = documentHasMore || wikiHasMore;
|
|
1542
|
-
Logger.info(`统一搜索完成,文档: ${documents.length} 条, 知识库: ${wikis.length} 条, 总计: ${total} 条, hasMore: ${hasMore}`);
|
|
1543
|
-
// 只返回必要字段,根据搜索类型动态添加
|
|
1544
|
-
const result = {
|
|
1545
|
-
paginationGuide
|
|
1546
|
-
};
|
|
1547
|
-
if (searchType === 'document' || searchType === 'both') {
|
|
1548
|
-
result.documents = documents;
|
|
1549
|
-
}
|
|
1550
|
-
if (searchType === 'wiki' || searchType === 'both') {
|
|
1551
|
-
result.wikis = wikis;
|
|
1552
|
-
}
|
|
1553
|
-
return result;
|
|
1554
|
-
}
|
|
1555
|
-
catch (error) {
|
|
1556
|
-
this.handleApiError(error, '统一搜索失败');
|
|
1557
|
-
throw error;
|
|
1558
|
-
}
|
|
250
|
+
return this.searchService.search(searchKey, searchType, offset, pageToken);
|
|
1559
251
|
}
|
|
252
|
+
// ─── 图片块操作委托 ───────────────────────────────────────────────
|
|
1560
253
|
/**
|
|
1561
|
-
*
|
|
1562
|
-
* @
|
|
1563
|
-
* @param documentHasMore 文档是否还有更多
|
|
1564
|
-
* @param wikiHasMore 知识库是否还有更多
|
|
1565
|
-
* @param documentOffset 文档的下一offset
|
|
1566
|
-
* @param wikiPageToken 知识库的下一页token
|
|
1567
|
-
* @returns 分页指导信息
|
|
254
|
+
* 将本地路径或远程 URL 的图片转换为 Base64 及文件名
|
|
255
|
+
* @see FeishuBlockService.getImageBase64FromPathOrUrl
|
|
1568
256
|
*/
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
hasMore: documentHasMore || wikiHasMore,
|
|
1572
|
-
description: ''
|
|
1573
|
-
};
|
|
1574
|
-
if (!guide.hasMore) {
|
|
1575
|
-
guide.description = '没有更多结果了';
|
|
1576
|
-
return guide;
|
|
1577
|
-
}
|
|
1578
|
-
// 根据搜索类型和hasMore状态生成指导
|
|
1579
|
-
if (searchType === 'document') {
|
|
1580
|
-
if (documentHasMore) {
|
|
1581
|
-
guide.nextPageParams = {
|
|
1582
|
-
searchType: 'document',
|
|
1583
|
-
offset: documentOffset
|
|
1584
|
-
};
|
|
1585
|
-
guide.description = `请使用 search_feishu_documents工具获取下一页,searchType = document offset=${documentOffset} 获取文档的下一页`;
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
else if (searchType === 'wiki') {
|
|
1589
|
-
if (wikiHasMore && wikiPageToken) {
|
|
1590
|
-
guide.nextPageParams = {
|
|
1591
|
-
searchType: 'wiki',
|
|
1592
|
-
pageToken: wikiPageToken
|
|
1593
|
-
};
|
|
1594
|
-
guide.description = `请使用 search_feishu_documents工具获取下一页,searchType = wiki pageToken="${wikiPageToken}" 获取知识库的下一页`;
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
else if (searchType === 'both') {
|
|
1598
|
-
// both类型:优先返回文档的下一页,如果文档没有更多了,再返回知识库的下一页
|
|
1599
|
-
if (documentHasMore) {
|
|
1600
|
-
guide.nextPageParams = {
|
|
1601
|
-
searchType: 'both',
|
|
1602
|
-
offset: documentOffset
|
|
1603
|
-
};
|
|
1604
|
-
guide.description = `请使用 search_feishu_documents工具获取下一页,searchType = both offset=${documentOffset} 获取文档的下一页`;
|
|
1605
|
-
}
|
|
1606
|
-
else if (wikiHasMore && wikiPageToken) {
|
|
1607
|
-
guide.nextPageParams = {
|
|
1608
|
-
searchType: 'wiki',
|
|
1609
|
-
pageToken: wikiPageToken
|
|
1610
|
-
};
|
|
1611
|
-
guide.description = `请使用 search_feishu_documents工具获取下一页,searchType = wiki pageToken="${wikiPageToken}" 获取知识库的下一页wiki结果`;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
return guide;
|
|
257
|
+
async getImageBase64FromPathOrUrl(imagePathOrUrl) {
|
|
258
|
+
return this.blockService.getImageBase64FromPathOrUrl(imagePathOrUrl);
|
|
1615
259
|
}
|
|
1616
260
|
/**
|
|
1617
|
-
*
|
|
1618
|
-
* @
|
|
1619
|
-
* @param fileName 图片文件名,如果不提供则自动生成
|
|
1620
|
-
* @param parentBlockId 图片块ID
|
|
1621
|
-
* @returns 上传结果,包含file_token
|
|
261
|
+
* 下载飞书图片素材,返回二进制数据
|
|
262
|
+
* @see FeishuBlockService.getImageResource
|
|
1622
263
|
*/
|
|
1623
|
-
async
|
|
1624
|
-
|
|
1625
|
-
const endpoint = '/drive/v1/medias/upload_all';
|
|
1626
|
-
// 将Base64转换为Buffer
|
|
1627
|
-
const imageBuffer = Buffer.from(imageBase64, 'base64');
|
|
1628
|
-
const imageSize = imageBuffer.length;
|
|
1629
|
-
// 如果没有提供文件名,根据Base64数据生成默认文件名
|
|
1630
|
-
if (!fileName) {
|
|
1631
|
-
// 简单检测图片格式
|
|
1632
|
-
if (imageBase64.startsWith('/9j/')) {
|
|
1633
|
-
fileName = `image_${Date.now()}.jpg`;
|
|
1634
|
-
}
|
|
1635
|
-
else if (imageBase64.startsWith('iVBORw0KGgo')) {
|
|
1636
|
-
fileName = `image_${Date.now()}.png`;
|
|
1637
|
-
}
|
|
1638
|
-
else if (imageBase64.startsWith('R0lGODlh')) {
|
|
1639
|
-
fileName = `image_${Date.now()}.gif`;
|
|
1640
|
-
}
|
|
1641
|
-
else {
|
|
1642
|
-
fileName = `image_${Date.now()}.png`; // 默认PNG格式
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
Logger.info(`开始上传图片素材,文件名: ${fileName},大小: ${imageSize} 字节,关联块ID: ${parentBlockId}`);
|
|
1646
|
-
// 验证图片大小(可选的业务检查)
|
|
1647
|
-
if (imageSize > 20 * 1024 * 1024) {
|
|
1648
|
-
// 20MB限制
|
|
1649
|
-
Logger.warn(`图片文件过大: ${imageSize} 字节,建议小于20MB`);
|
|
1650
|
-
}
|
|
1651
|
-
// 使用FormData构建multipart/form-data请求
|
|
1652
|
-
const formData = new FormData();
|
|
1653
|
-
// file字段传递图片的二进制数据流
|
|
1654
|
-
// Buffer是Node.js中的二进制数据类型,form-data库会将其作为文件流处理
|
|
1655
|
-
formData.append('file', imageBuffer, {
|
|
1656
|
-
filename: fileName,
|
|
1657
|
-
contentType: this.getMimeTypeFromFileName(fileName),
|
|
1658
|
-
knownLength: imageSize, // 明确指定文件大小,避免流读取问题
|
|
1659
|
-
});
|
|
1660
|
-
// 飞书API要求的其他表单字段
|
|
1661
|
-
formData.append('file_name', fileName);
|
|
1662
|
-
formData.append('parent_type', 'docx_image'); // 固定值:文档图片类型
|
|
1663
|
-
formData.append('parent_node', parentBlockId); // 关联的图片块ID
|
|
1664
|
-
formData.append('size', imageSize.toString()); // 文件大小(字节,字符串格式)
|
|
1665
|
-
// 使用通用的post方法发送请求
|
|
1666
|
-
const response = await this.post(endpoint, formData);
|
|
1667
|
-
Logger.info(`图片素材上传成功,file_token: ${response.file_token}`);
|
|
1668
|
-
return response;
|
|
1669
|
-
}
|
|
1670
|
-
catch (error) {
|
|
1671
|
-
this.handleApiError(error, '上传图片素材失败');
|
|
1672
|
-
}
|
|
264
|
+
async getImageResource(mediaId, extra = '') {
|
|
265
|
+
return this.blockService.getImageResource(mediaId, extra);
|
|
1673
266
|
}
|
|
1674
267
|
/**
|
|
1675
|
-
*
|
|
1676
|
-
* @
|
|
1677
|
-
* @param imageBlockId 图片块ID
|
|
1678
|
-
* @param fileToken 图片素材的file_token
|
|
1679
|
-
* @returns 设置结果
|
|
268
|
+
* 将图片素材上传到飞书云端
|
|
269
|
+
* @see FeishuBlockService.uploadImageMedia
|
|
1680
270
|
*/
|
|
1681
|
-
async
|
|
1682
|
-
|
|
1683
|
-
const normalizedDocId = ParamUtils.processDocumentId(documentId);
|
|
1684
|
-
const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${imageBlockId}`;
|
|
1685
|
-
const payload = {
|
|
1686
|
-
replace_image: {
|
|
1687
|
-
token: fileToken,
|
|
1688
|
-
},
|
|
1689
|
-
};
|
|
1690
|
-
Logger.info(`开始设置图片块内容,文档ID: ${normalizedDocId},块ID: ${imageBlockId},file_token: ${fileToken}`);
|
|
1691
|
-
const response = await this.patch(endpoint, payload);
|
|
1692
|
-
Logger.info('图片块内容设置成功');
|
|
1693
|
-
return response;
|
|
1694
|
-
}
|
|
1695
|
-
catch (error) {
|
|
1696
|
-
this.handleApiError(error, '设置图片块内容失败');
|
|
1697
|
-
}
|
|
271
|
+
async uploadImageMedia(imageBase64, fileName, parentBlockId) {
|
|
272
|
+
return this.blockService.uploadImageMedia(imageBase64, fileName, parentBlockId);
|
|
1698
273
|
}
|
|
1699
274
|
/**
|
|
1700
|
-
*
|
|
1701
|
-
* @
|
|
1702
|
-
* @param parentBlockId 父块ID
|
|
1703
|
-
* @param imagePathOrUrl 图片路径或URL
|
|
1704
|
-
* @param options 图片选项
|
|
1705
|
-
* @returns 创建结果
|
|
275
|
+
* 将已上传的图片素材绑定到指定图片块
|
|
276
|
+
* @see FeishuBlockService.setImageBlockContent
|
|
1706
277
|
*/
|
|
1707
|
-
async
|
|
1708
|
-
|
|
1709
|
-
const { fileName: providedFileName, width, height, index = 0 } = options;
|
|
1710
|
-
Logger.info(`开始创建图片块,文档ID: ${documentId},父块ID: ${parentBlockId},图片源: ${imagePathOrUrl},插入位置: ${index}`);
|
|
1711
|
-
// 从路径或URL获取图片的Base64编码
|
|
1712
|
-
const { base64: imageBase64, fileName: detectedFileName } = await this.getImageBase64FromPathOrUrl(imagePathOrUrl);
|
|
1713
|
-
// 使用提供的文件名或检测到的文件名
|
|
1714
|
-
const finalFileName = providedFileName || detectedFileName;
|
|
1715
|
-
// 第1步:创建空图片块
|
|
1716
|
-
Logger.info('第1步:创建空图片块');
|
|
1717
|
-
const imageBlockContent = this.blockFactory.createImageBlock({
|
|
1718
|
-
width,
|
|
1719
|
-
height,
|
|
1720
|
-
});
|
|
1721
|
-
const createBlockResult = await this.createDocumentBlock(documentId, parentBlockId, imageBlockContent, index);
|
|
1722
|
-
if (!createBlockResult?.children?.[0]?.block_id) {
|
|
1723
|
-
throw new Error('创建空图片块失败:无法获取块ID');
|
|
1724
|
-
}
|
|
1725
|
-
const imageBlockId = createBlockResult.children[0].block_id;
|
|
1726
|
-
Logger.info(`空图片块创建成功,块ID: ${imageBlockId}`);
|
|
1727
|
-
// 第2步:上传图片素材
|
|
1728
|
-
Logger.info('第2步:上传图片素材');
|
|
1729
|
-
const uploadResult = await this.uploadImageMedia(imageBase64, finalFileName, imageBlockId);
|
|
1730
|
-
if (!uploadResult?.file_token) {
|
|
1731
|
-
throw new Error('上传图片素材失败:无法获取file_token');
|
|
1732
|
-
}
|
|
1733
|
-
Logger.info(`图片素材上传成功,file_token: ${uploadResult.file_token}`);
|
|
1734
|
-
// 第3步:设置图片块内容
|
|
1735
|
-
Logger.info('第3步:设置图片块内容');
|
|
1736
|
-
const setContentResult = await this.setImageBlockContent(documentId, imageBlockId, uploadResult.file_token);
|
|
1737
|
-
Logger.info('图片块创建完成');
|
|
1738
|
-
// 返回综合结果
|
|
1739
|
-
return {
|
|
1740
|
-
imageBlock: createBlockResult.children[0],
|
|
1741
|
-
imageBlockId: imageBlockId,
|
|
1742
|
-
fileToken: uploadResult.file_token,
|
|
1743
|
-
uploadResult: uploadResult,
|
|
1744
|
-
setContentResult: setContentResult,
|
|
1745
|
-
documentRevisionId: setContentResult.document_revision_id ||
|
|
1746
|
-
createBlockResult.document_revision_id,
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1749
|
-
catch (error) {
|
|
1750
|
-
this.handleApiError(error, '创建图片块失败');
|
|
1751
|
-
}
|
|
278
|
+
async setImageBlockContent(documentId, imageBlockId, fileToken) {
|
|
279
|
+
return this.blockService.setImageBlockContent(documentId, imageBlockId, fileToken);
|
|
1752
280
|
}
|
|
1753
281
|
/**
|
|
1754
|
-
*
|
|
1755
|
-
* @
|
|
1756
|
-
* @returns MIME类型
|
|
282
|
+
* 完整创建图片块(创建空块 → 上传素材 → 绑定),支持本地路径和 URL
|
|
283
|
+
* @see FeishuBlockService.createImageBlock
|
|
1757
284
|
*/
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
switch (extension) {
|
|
1761
|
-
case 'jpg':
|
|
1762
|
-
case 'jpeg':
|
|
1763
|
-
return 'image/jpeg';
|
|
1764
|
-
case 'png':
|
|
1765
|
-
return 'image/png';
|
|
1766
|
-
case 'gif':
|
|
1767
|
-
return 'image/gif';
|
|
1768
|
-
case 'webp':
|
|
1769
|
-
return 'image/webp';
|
|
1770
|
-
case 'bmp':
|
|
1771
|
-
return 'image/bmp';
|
|
1772
|
-
case 'svg':
|
|
1773
|
-
return 'image/svg+xml';
|
|
1774
|
-
default:
|
|
1775
|
-
return 'image/png'; // 默认PNG
|
|
1776
|
-
}
|
|
285
|
+
async createImageBlock(documentId, parentBlockId, imagePathOrUrl, options = {}) {
|
|
286
|
+
return this.blockService.createImageBlock(documentId, parentBlockId, imagePathOrUrl, options);
|
|
1777
287
|
}
|
|
288
|
+
// ─── Whiteboard 服务委托 ──────────────────────────────────────────
|
|
1778
289
|
/**
|
|
1779
|
-
*
|
|
1780
|
-
* @
|
|
1781
|
-
* @returns 画板节点数据
|
|
290
|
+
* 获取画板的所有节点内容
|
|
291
|
+
* @see FeishuWhiteboardService.getWhiteboardContent
|
|
1782
292
|
*/
|
|
1783
293
|
async getWhiteboardContent(whiteboardId) {
|
|
1784
|
-
|
|
1785
|
-
const normalizedWhiteboardId = ParamUtils.processWhiteboardId(whiteboardId);
|
|
1786
|
-
const endpoint = `/board/v1/whiteboards/${normalizedWhiteboardId}/nodes`;
|
|
1787
|
-
Logger.info(`开始获取画板内容,画板ID: ${normalizedWhiteboardId}`);
|
|
1788
|
-
const response = await this.get(endpoint);
|
|
1789
|
-
Logger.info(`画板内容获取成功,节点数量: ${response.nodes?.length || 0}`);
|
|
1790
|
-
return response;
|
|
1791
|
-
}
|
|
1792
|
-
catch (error) {
|
|
1793
|
-
this.handleApiError(error, '获取画板内容失败');
|
|
1794
|
-
}
|
|
294
|
+
return this.whiteboardService.getWhiteboardContent(whiteboardId);
|
|
1795
295
|
}
|
|
1796
296
|
/**
|
|
1797
|
-
*
|
|
1798
|
-
* @
|
|
1799
|
-
* @returns 画板缩略图的二进制数据
|
|
297
|
+
* 获取画板缩略图,返回二进制数据
|
|
298
|
+
* @see FeishuWhiteboardService.getWhiteboardThumbnail
|
|
1800
299
|
*/
|
|
1801
300
|
async getWhiteboardThumbnail(whiteboardId) {
|
|
1802
|
-
|
|
1803
|
-
const normalizedWhiteboardId = ParamUtils.processWhiteboardId(whiteboardId);
|
|
1804
|
-
const endpoint = `/board/v1/whiteboards/${normalizedWhiteboardId}/download_as_image`;
|
|
1805
|
-
Logger.info(`开始获取画板缩略图,画板ID: ${normalizedWhiteboardId}`);
|
|
1806
|
-
// 使用通用的request方法获取二进制响应
|
|
1807
|
-
const response = await this.request(endpoint, 'GET', {}, true, {}, 'arraybuffer');
|
|
1808
|
-
const thumbnailBuffer = Buffer.from(response);
|
|
1809
|
-
Logger.info(`画板缩略图获取成功,大小: ${thumbnailBuffer.length} 字节`);
|
|
1810
|
-
return thumbnailBuffer;
|
|
1811
|
-
}
|
|
1812
|
-
catch (error) {
|
|
1813
|
-
this.handleApiError(error, '获取画板缩略图失败');
|
|
1814
|
-
return Buffer.from([]); // 永远不会执行到这里
|
|
1815
|
-
}
|
|
301
|
+
return this.whiteboardService.getWhiteboardThumbnail(whiteboardId);
|
|
1816
302
|
}
|
|
1817
303
|
/**
|
|
1818
|
-
* 在画板中创建图表节点(支持 PlantUML
|
|
1819
|
-
* @
|
|
1820
|
-
* @param code 图表代码(PlantUML 或 Mermaid)
|
|
1821
|
-
* @param syntaxType 语法类型:1=PlantUML, 2=Mermaid
|
|
1822
|
-
* @returns 创建结果
|
|
304
|
+
* 在画板中创建图表节点(支持 PlantUML / Mermaid)
|
|
305
|
+
* @see FeishuWhiteboardService.createDiagramNode
|
|
1823
306
|
*/
|
|
1824
307
|
async createDiagramNode(whiteboardId, code, syntaxType) {
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
308
|
+
return this.whiteboardService.createDiagramNode(whiteboardId, code, syntaxType);
|
|
309
|
+
}
|
|
310
|
+
// ─── 任务服务委托 ─────────────────────────────────────────────────
|
|
311
|
+
/** @see FeishuTaskService.createTask */
|
|
312
|
+
async createTask(params) {
|
|
313
|
+
return this.taskService.createTask(params);
|
|
314
|
+
}
|
|
315
|
+
/** @see FeishuTaskService.createTasksNested. Supports multi-level subTasks. */
|
|
316
|
+
async createTasksNested(rootItems, options) {
|
|
317
|
+
return this.taskService.createTasksNested(rootItems, options);
|
|
318
|
+
}
|
|
319
|
+
/** @see FeishuTaskService.updateTask */
|
|
320
|
+
async updateTask(taskGuid, params) {
|
|
321
|
+
return this.taskService.updateTask(taskGuid, params);
|
|
322
|
+
}
|
|
323
|
+
/** @see FeishuTaskService.addTaskMembers */
|
|
324
|
+
async addTaskMembers(taskGuid, members) {
|
|
325
|
+
return this.taskService.addTaskMembers(taskGuid, members);
|
|
326
|
+
}
|
|
327
|
+
/** @see FeishuTaskService.removeTaskMembers */
|
|
328
|
+
async removeTaskMembers(taskGuid, members) {
|
|
329
|
+
return this.taskService.removeTaskMembers(taskGuid, members);
|
|
330
|
+
}
|
|
331
|
+
/** @see FeishuTaskService.addTaskReminder. Task must have due; only one reminder per task. */
|
|
332
|
+
async addTaskReminder(taskGuid, relativeFireMinute) {
|
|
333
|
+
return this.taskService.addTaskReminder(taskGuid, relativeFireMinute);
|
|
334
|
+
}
|
|
335
|
+
/** @see FeishuTaskService.removeTaskReminders */
|
|
336
|
+
async removeTaskReminders(taskGuid, reminderIds) {
|
|
337
|
+
return this.taskService.removeTaskReminders(taskGuid, reminderIds);
|
|
338
|
+
}
|
|
339
|
+
/** @see FeishuTaskService.listTasksTwoPages. Lists "my_tasks" (我负责的), 2 pages (up to 100 items), slimmed fields. Requires user token. */
|
|
340
|
+
async listTasks(pageToken, completed) {
|
|
341
|
+
return this.taskService.listTasksTwoPages(pageToken, completed);
|
|
342
|
+
}
|
|
343
|
+
/** @see FeishuTaskService.deleteTask */
|
|
344
|
+
async deleteTask(taskGuid) {
|
|
345
|
+
return this.taskService.deleteTask(taskGuid);
|
|
346
|
+
}
|
|
347
|
+
/** 批量删除任务。逐条调用 deleteTask,返回已删除的 guid 与每项错误。 */
|
|
348
|
+
async deleteTasks(taskGuids) {
|
|
349
|
+
const deleted = [];
|
|
350
|
+
const errors = [];
|
|
351
|
+
for (const guid of taskGuids) {
|
|
352
|
+
try {
|
|
353
|
+
await this.taskService.deleteTask(guid);
|
|
354
|
+
deleted.push(guid);
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
358
|
+
errors.push({ taskGuid: guid, error: msg });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { deleted, errors };
|
|
362
|
+
}
|
|
363
|
+
// ─── 日历服务委托 ─────────────────────────────────────────────────
|
|
364
|
+
/** @see FeishuCalendarService - 供 calendarTools 等调用 */
|
|
365
|
+
getCalendarService() {
|
|
366
|
+
return this.calendarService;
|
|
367
|
+
}
|
|
368
|
+
// ─── 成员搜索服务委托 ──────────────────────────────────────────────
|
|
369
|
+
/** @see FeishuMemberService.searchUsers */
|
|
370
|
+
async searchUsers(query, pageToken) {
|
|
371
|
+
return this.memberService.searchUsers(query, pageToken);
|
|
372
|
+
}
|
|
373
|
+
/** @see FeishuMemberService.batchGetUsers */
|
|
374
|
+
async getUsersBatch(userIds, userIdType = 'open_id') {
|
|
375
|
+
return this.memberService.batchGetUsers(userIds, userIdType);
|
|
1890
376
|
}
|
|
1891
377
|
}
|