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.
Files changed (77) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +356 -266
  3. package/dist/cli/auth.js +117 -0
  4. package/dist/cli/commands/auth.js +141 -0
  5. package/dist/cli/commands/config.js +86 -0
  6. package/dist/cli/commands/guide.js +68 -0
  7. package/dist/cli/dispatcher.js +96 -0
  8. package/dist/cli/index.js +95 -0
  9. package/dist/manager/sseConnectionManager.js +2 -0
  10. package/dist/mcp/feishuMcp.js +25 -25
  11. package/dist/mcp/tools/blockTools.js +295 -0
  12. package/dist/mcp/tools/documentTools.js +105 -0
  13. package/dist/mcp/tools/feishuBlockTools.js +1 -285
  14. package/dist/mcp/tools/feishuTools.js +1 -67
  15. package/dist/mcp/tools/folderTools.js +99 -0
  16. package/dist/mcp/tools/toolHelpers.js +155 -0
  17. package/dist/modules/FeatureModule.js +1 -0
  18. package/dist/modules/ModuleRegistry.js +63 -0
  19. package/dist/modules/calendar/index.js +11 -0
  20. package/dist/modules/calendar/services/FeishuCalendarService.js +6 -0
  21. package/dist/modules/calendar/tools/calendarTools.js +6 -0
  22. package/dist/modules/document/index.js +15 -0
  23. package/dist/modules/document/services/FeishuBlockService.js +410 -0
  24. package/dist/modules/document/services/FeishuDocumentService.js +110 -0
  25. package/dist/modules/document/services/FeishuFoldService.js +187 -0
  26. package/dist/modules/document/services/FeishuSearchService.js +232 -0
  27. package/dist/modules/document/services/FeishuWhiteboardService.js +80 -0
  28. package/dist/modules/document/services/blockFactory.js +521 -0
  29. package/dist/modules/document/toolApi/blockToolApi.js +160 -0
  30. package/dist/modules/document/toolApi/documentToolApi.js +65 -0
  31. package/dist/modules/document/toolApi/folderToolApi.js +73 -0
  32. package/dist/modules/document/toolApi/index.js +3 -0
  33. package/dist/modules/document/tools/blockTools.js +138 -0
  34. package/dist/modules/document/tools/documentTools.js +64 -0
  35. package/dist/modules/document/tools/folderTools.js +46 -0
  36. package/dist/modules/document/tools/toolHelpers.js +155 -0
  37. package/dist/modules/index.js +5 -0
  38. package/dist/modules/member/index.js +11 -0
  39. package/dist/modules/member/services/FeishuMemberService.js +41 -0
  40. package/dist/modules/member/toolApi/index.js +1 -0
  41. package/dist/modules/member/toolApi/memberToolApi.js +54 -0
  42. package/dist/modules/member/tools/memberTools.js +26 -0
  43. package/dist/modules/task/index.js +11 -0
  44. package/dist/modules/task/services/FeishuTaskService.js +271 -0
  45. package/dist/modules/task/toolApi/__tests__/taskToolApi.test.js +98 -0
  46. package/dist/modules/task/toolApi/index.js +1 -0
  47. package/dist/modules/task/toolApi/taskToolApi.js +128 -0
  48. package/dist/modules/task/tools/taskTools.js +93 -0
  49. package/dist/server.js +43 -24
  50. package/dist/services/baseService.js +11 -2
  51. package/dist/services/blockFactory.js +167 -0
  52. package/dist/services/callbackService.js +1 -1
  53. package/dist/services/constants/feishuScopes.js +94 -0
  54. package/dist/services/feishu/FeishuBaseApiService.js +47 -0
  55. package/dist/services/feishu/FeishuBlockService.js +410 -0
  56. package/dist/services/feishu/FeishuDocumentBlockService.js +403 -0
  57. package/dist/services/feishu/FeishuDocumentService.js +110 -0
  58. package/dist/services/feishu/FeishuDriveService.js +62 -0
  59. package/dist/services/feishu/FeishuFoldService.js +187 -0
  60. package/dist/services/feishu/FeishuImageService.js +219 -0
  61. package/dist/services/feishu/FeishuScopeValidator.js +177 -0
  62. package/dist/services/feishu/FeishuSearchService.js +232 -0
  63. package/dist/services/feishu/FeishuWhiteboardService.js +80 -0
  64. package/dist/services/feishu/FeishuWikiService.js +134 -0
  65. package/dist/services/feishuApiService.js +246 -1760
  66. package/dist/services/feishuAuthService.js +43 -0
  67. package/dist/types/documentSchema.js +232 -0
  68. package/dist/types/feishuSchema.js +54 -77
  69. package/dist/types/memberSchema.js +35 -0
  70. package/dist/types/taskSchema.js +166 -0
  71. package/dist/utils/auth/tokenCacheManager.js +16 -3
  72. package/dist/utils/auth/tokenRefreshManager.js +2 -1
  73. package/dist/utils/config.js +47 -0
  74. package/dist/utils/document.js +116 -116
  75. package/dist/utils/error.js +0 -11
  76. package/dist/utils/paramUtils.js +0 -31
  77. 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 { ScopeInsufficientError } from '../utils/error.js';
9
- import axios from 'axios';
10
- import FormData from 'form-data';
11
- import fs from 'fs';
12
- import path from 'path';
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
- * 提供飞书API的所有基础操作,包括认证、请求和缓存管理
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 extends BaseApiService {
18
- /**
19
- * 私有构造函数,用于单例模式
20
- */
21
- constructor() {
22
- super();
23
- Object.defineProperty(this, "blockFactory", {
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: void 0
63
+ value: taskService
28
64
  });
29
- Object.defineProperty(this, "config", {
65
+ Object.defineProperty(this, "calendarService", {
30
66
  enumerable: true,
31
67
  configurable: true,
32
68
  writable: true,
33
- value: void 0
69
+ value: calendarService
34
70
  });
35
- Object.defineProperty(this, "authService", {
71
+ Object.defineProperty(this, "memberService", {
36
72
  enumerable: true,
37
73
  configurable: true,
38
74
  writable: true,
39
- value: void 0
75
+ value: memberService
40
76
  });
41
- this.blockFactory = BlockFactory.getInstance();
42
- this.config = Config.getInstance();
43
- this.authService = new AuthService();
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
- * 获取飞书API服务实例
47
- * @returns 飞书API服务实例
92
+ * 获取 FeishuApiService 单例
93
+ * @returns 全局唯一的 FeishuApiService 实例
48
94
  */
49
95
  static getInstance() {
50
96
  if (!FeishuApiService.instance) {
51
- FeishuApiService.instance = new FeishuApiService();
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
- * @param title 文档标题
368
- * @param folderToken 文件夹Token
369
- * @returns 创建的文档信息
104
+ * @see FeishuDocumentService.createDocument
370
105
  */
371
106
  async createDocument(title, folderToken) {
372
- try {
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
- * 获取文档信息(支持普通文档和Wiki文档)
387
- * @param documentId 文档ID或URL(支持Wiki链接)
388
- * @param documentType 文档类型(可选),'document' 或 'wiki',如果不指定则自动检测
389
- * @returns 文档信息或Wiki节点信息
110
+ * 获取文档信息,支持普通文档和 Wiki 文档
111
+ * @see FeishuDocumentService.getDocumentInfo
390
112
  */
391
113
  async getDocumentInfo(documentId, documentType) {
392
- try {
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
- * @param documentId 文档ID或URL
445
- * @param lang 语言代码,0为中文,1为英文
446
- * @returns 文档内容
117
+ * 获取文档的纯文本内容
118
+ * @see FeishuDocumentService.getDocumentContent
447
119
  */
448
120
  async getDocumentContent(documentId, lang = 0) {
449
- try {
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
- * @param documentId 文档ID或URL
463
- * @param pageSize 每页块数量
464
- * @returns 文档块数组
124
+ * 获取文档的所有块结构(自动分页)
125
+ * @see FeishuDocumentService.getDocumentBlocks
465
126
  */
466
127
  async getDocumentBlocks(documentId, pageSize = 500) {
467
- try {
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
- * @param documentId 文档ID或URL
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
- try {
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
- * @param documentId 文档ID或URL
558
- * @param parentBlockId 父块ID
559
- * @param blockContent 块内容
560
- * @param index 插入位置索引
561
- * @returns 创建结果
139
+ * 批量更新多个块的文本内容(一次 API 调用)
140
+ * @see FeishuBlockService.batchUpdateBlocksTextContent
562
141
  */
563
- async createDocumentBlock(documentId, parentBlockId, blockContent, index = 0) {
564
- try {
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
- * @param documentId 文档ID或URL
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
- try {
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
- * 创建Mermaid块
694
- * @param documentId 文档ID或URL
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
- const normalizedDocId = ParamUtils.processDocumentId(documentId);
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
- * @param documentId 文档ID或URL
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
- try {
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
- * @param documentId 文档ID或URL
838
- * @param parentBlockId 父块ID
839
- * @param blockIndex 块索引
840
- * @returns 操作结果
167
+ * 根据块类型字符串和选项对象创建块内容对象
168
+ * @see FeishuBlockService.createBlockContent
841
169
  */
842
- async deleteDocumentBlock(documentId, parentBlockId, blockIndex) {
843
- return this.deleteDocumentBlocks(documentId, parentBlockId, blockIndex, blockIndex + 1);
170
+ createBlockContent(blockType, options) {
171
+ return this.blockService.createBlockContent(blockType, options);
844
172
  }
845
173
  /**
846
- * 将飞书Wiki链接转换为文档ID
847
- * @param wikiUrl Wiki链接或Token
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.blockFactory;
178
+ return this.blockService.getBlockFactory();
878
179
  }
180
+ // ─── 文件夹 / 知识空间服务委托 ───────────────────────────────────
879
181
  /**
880
- * 创建块内容对象
881
- * @param blockType 块类型
882
- * @param options 块选项
883
- * @returns 块内容对象
182
+ * 获取当前用户根文件夹的元数据信息
183
+ * @see FeishuFoldService.getRootFolderInfo
884
184
  */
885
- createBlockContent(blockType, options) {
886
- try {
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
- * @param mediaId 图片媒体ID
1112
- * @param extra 额外参数,可选
1113
- * @returns 图片二进制数据
189
+ * 获取指定文件夹内的文件和子文件夹列表
190
+ * @see FeishuFoldService.getFolderFileList
1114
191
  */
1115
- async getImageResource(mediaId, extra = '') {
1116
- try {
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
- * 获取用户的根文件夹的元数据信息,包括token、id和用户id
1140
- * @returns 根文件夹信息
196
+ * 在指定文件夹下创建子文件夹
197
+ * @see FeishuFoldService.createFolder
1141
198
  */
1142
- async getRootFolderInfo() {
1143
- try {
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
- * @param pageSize 每页数量,默认20
1156
- * @returns 所有知识空间列表(仅包含 items 数组,不包含 has_more 和 page_token)
203
+ * 获取所有知识空间列表(自动分页)
204
+ * @see FeishuFoldService.getAllWikiSpacesList
1157
205
  */
1158
206
  async getAllWikiSpacesList(pageSize = 20) {
1159
- try {
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
- * @param spaceId 知识空间ID
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
- try {
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
- * @param spaceId 知识空间ID,传入 'my_library' 时获取"我的知识库"
1239
- * @param lang 语言(仅当 spaceId 为 'my_library' 时有效),默认'en'
1240
- * @returns 知识空间信息
217
+ * 获取指定知识空间的详细信息
218
+ * @see FeishuFoldService.getWikiSpaceInfo
1241
219
  */
1242
220
  async getWikiSpaceInfo(spaceId, lang = 'en') {
1243
- try {
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
- * @param spaceId 知识空间ID
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
- try {
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
- * @param folderToken 文件夹Token
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
- try {
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
- * 搜索Wiki知识库节点(支持分页和轮询)
1405
- * @param query 搜索关键字
1406
- * @param maxSize 最大返回数量,如果未指定则只返回一页
1407
- * @param pageToken 分页token,用于获取下一页,可选
1408
- * @returns 搜索结果,包含数据和分页信息
239
+ * 搜索飞书知识库节点,支持分页
240
+ * @see FeishuSearchService.searchWikiNodes
1409
241
  */
1410
242
  async searchWikiNodes(query, maxSize, pageToken) {
1411
- try {
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
- * @param searchKey 搜索关键字
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
- try {
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
- * @param searchType 搜索类型
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
- generatePaginationGuide(searchType, documentHasMore, wikiHasMore, documentOffset, wikiPageToken) {
1570
- const guide = {
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
- * @param imageBase64 图片的Base64编码
1619
- * @param fileName 图片文件名,如果不提供则自动生成
1620
- * @param parentBlockId 图片块ID
1621
- * @returns 上传结果,包含file_token
261
+ * 下载飞书图片素材,返回二进制数据
262
+ * @see FeishuBlockService.getImageResource
1622
263
  */
1623
- async uploadImageMedia(imageBase64, fileName, parentBlockId) {
1624
- try {
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
- * @param documentId 文档ID
1677
- * @param imageBlockId 图片块ID
1678
- * @param fileToken 图片素材的file_token
1679
- * @returns 设置结果
268
+ * 将图片素材上传到飞书云端
269
+ * @see FeishuBlockService.uploadImageMedia
1680
270
  */
1681
- async setImageBlockContent(documentId, imageBlockId, fileToken) {
1682
- try {
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
- * @param documentId 文档ID
1702
- * @param parentBlockId 父块ID
1703
- * @param imagePathOrUrl 图片路径或URL
1704
- * @param options 图片选项
1705
- * @returns 创建结果
275
+ * 将已上传的图片素材绑定到指定图片块
276
+ * @see FeishuBlockService.setImageBlockContent
1706
277
  */
1707
- async createImageBlock(documentId, parentBlockId, imagePathOrUrl, options = {}) {
1708
- try {
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
- * 根据文件名获取MIME类型
1755
- * @param fileName 文件名
1756
- * @returns MIME类型
282
+ * 完整创建图片块(创建空块 → 上传素材 → 绑定),支持本地路径和 URL
283
+ * @see FeishuBlockService.createImageBlock
1757
284
  */
1758
- getMimeTypeFromFileName(fileName) {
1759
- const extension = fileName.toLowerCase().split('.').pop();
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
- * @param whiteboardId 画板ID或URL
1781
- * @returns 画板节点数据
290
+ * 获取画板的所有节点内容
291
+ * @see FeishuWhiteboardService.getWhiteboardContent
1782
292
  */
1783
293
  async getWhiteboardContent(whiteboardId) {
1784
- try {
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
- * @param whiteboardId 画板ID或URL
1799
- * @returns 画板缩略图的二进制数据
297
+ * 获取画板缩略图,返回二进制数据
298
+ * @see FeishuWhiteboardService.getWhiteboardThumbnail
1800
299
  */
1801
300
  async getWhiteboardThumbnail(whiteboardId) {
1802
- try {
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 Mermaid)
1819
- * @param whiteboardId 画板ID(token)
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
- try {
1826
- const normalizedWhiteboardId = ParamUtils.processWhiteboardId(whiteboardId);
1827
- const endpoint = `/board/v1/whiteboards/${normalizedWhiteboardId}/nodes/plantuml`;
1828
- const syntaxTypeName = syntaxType === 1 ? 'PlantUML' : 'Mermaid';
1829
- Logger.info(`开始在画板中创建 ${syntaxTypeName} 节点,画板ID: ${normalizedWhiteboardId}`);
1830
- Logger.debug(`${syntaxTypeName} 代码: ${code.substring(0, 200)}...`);
1831
- const payload = {
1832
- plant_uml_code: code,
1833
- style_type: 1, // 画板样式(默认为2 经典样式) 示例值:1 可选值有: 1:画板样式(解析之后为多个画板节点,粘贴到画板中,不可对语法进行二次编辑) 2:经典样式(解析之后为一张图片,粘贴到画板中,可对语法进行二次编辑)(只有PlantUml语法支持经典样式
1834
- syntax_type: syntaxType
1835
- };
1836
- Logger.debug(`请求载荷: ${JSON.stringify(payload, null, 2)}`);
1837
- const response = await this.post(endpoint, payload);
1838
- Logger.info(`${syntaxTypeName} 节点创建成功`);
1839
- return response;
1840
- }
1841
- catch (error) {
1842
- const syntaxTypeName = syntaxType === 1 ? 'PlantUML' : 'Mermaid';
1843
- Logger.error(`创建 ${syntaxTypeName} 节点失败,画板ID: ${whiteboardId}`, error);
1844
- this.handleApiError(error, `创建 ${syntaxTypeName} 节点失败`);
1845
- }
1846
- }
1847
- /**
1848
- * 从路径或URL获取图片的Base64编码
1849
- * @param imagePathOrUrl 图片路径或URL
1850
- * @returns 图片的Base64编码和文件名
1851
- */
1852
- async getImageBase64FromPathOrUrl(imagePathOrUrl) {
1853
- try {
1854
- let imageBuffer;
1855
- let fileName;
1856
- // 判断是否为HTTP/HTTPS URL
1857
- if (imagePathOrUrl.startsWith('http://') || imagePathOrUrl.startsWith('https://')) {
1858
- Logger.info(`从URL获取图片: ${imagePathOrUrl}`);
1859
- // 从URL下载图片
1860
- const response = await axios.get(imagePathOrUrl, {
1861
- responseType: 'arraybuffer',
1862
- timeout: 30000, // 30秒超时
1863
- });
1864
- imageBuffer = Buffer.from(response.data);
1865
- // 从URL中提取文件名
1866
- const urlPath = new URL(imagePathOrUrl).pathname;
1867
- fileName = path.basename(urlPath) || `image_${Date.now()}.png`;
1868
- Logger.info(`从URL成功获取图片,大小: ${imageBuffer.length} 字节,文件名: ${fileName}`);
1869
- }
1870
- else {
1871
- // 本地文件路径
1872
- Logger.info(`从本地路径读取图片: ${imagePathOrUrl}`);
1873
- // 检查文件是否存在
1874
- if (!fs.existsSync(imagePathOrUrl)) {
1875
- throw new Error(`图片文件不存在: ${imagePathOrUrl}`);
1876
- }
1877
- // 读取文件
1878
- imageBuffer = fs.readFileSync(imagePathOrUrl);
1879
- fileName = path.basename(imagePathOrUrl);
1880
- Logger.info(`从本地路径成功读取图片,大小: ${imageBuffer.length} 字节,文件名: ${fileName}`);
1881
- }
1882
- // 转换为Base64
1883
- const base64 = imageBuffer.toString('base64');
1884
- return { base64, fileName };
1885
- }
1886
- catch (error) {
1887
- Logger.error(`获取图片失败: ${error}`);
1888
- throw new Error(`获取图片失败: ${error instanceof Error ? error.message : String(error)}`);
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
  }