feishu-mcp 0.1.5 → 0.1.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.
@@ -5,7 +5,8 @@ import { CacheManager } from '../utils/cache.js';
5
5
  import { ParamUtils } from '../utils/paramUtils.js';
6
6
  import { BlockFactory, BlockType } from './blockFactory.js';
7
7
  import { AuthUtils, TokenCacheManager } from '../utils/auth/index.js';
8
- import { AuthRequiredError } from '../utils/error.js';
8
+ import { AuthService } from './feishuAuthService.js';
9
+ import { ScopeInsufficientError } from '../utils/error.js';
9
10
  import axios from 'axios';
10
11
  import FormData from 'form-data';
11
12
  import fs from 'fs';
@@ -38,9 +39,16 @@ export class FeishuApiService extends BaseApiService {
38
39
  writable: true,
39
40
  value: void 0
40
41
  });
42
+ Object.defineProperty(this, "authService", {
43
+ enumerable: true,
44
+ configurable: true,
45
+ writable: true,
46
+ value: void 0
47
+ });
41
48
  this.cacheManager = CacheManager.getInstance();
42
49
  this.blockFactory = BlockFactory.getInstance();
43
50
  this.config = Config.getInstance();
51
+ this.authService = new AuthService();
44
52
  }
45
53
  /**
46
54
  * 获取飞书API服务实例
@@ -77,13 +85,229 @@ export class FeishuApiService extends BaseApiService {
77
85
  // 生成客户端缓存键
78
86
  const clientKey = AuthUtils.generateClientKey(userKey);
79
87
  Logger.debug(`[FeishuApiService] 获取访问令牌,userKey: ${userKey}, clientKey: ${clientKey}, authType: ${authType}`);
88
+ // 在使用token之前先校验scope(使用appId+appSecret获取临时tenant token来调用scope接口)
89
+ await this.validateScopeWithVersion(appId, appSecret, authType);
90
+ // 校验通过后,获取实际的token
80
91
  if (authType === 'tenant') {
81
92
  // 租户模式:获取租户访问令牌
82
- return this.getTenantAccessToken(appId, appSecret, clientKey);
93
+ return await this.getTenantAccessToken(appId, appSecret, clientKey);
83
94
  }
84
95
  else {
85
96
  // 用户模式:获取用户访问令牌
86
- return this.getUserAccessToken(appId, appSecret, clientKey, userKey);
97
+ return await this.authService.getUserAccessToken(clientKey, appId, appSecret);
98
+ }
99
+ }
100
+ /**
101
+ * 获取应用权限范围
102
+ * @param accessToken 访问令牌
103
+ * @param authType 认证类型(tenant或user)
104
+ * @returns 应用权限范围列表
105
+ */
106
+ async getApplicationScopes(accessToken, authType) {
107
+ try {
108
+ const endpoint = '/application/v6/scopes';
109
+ const headers = {
110
+ 'Authorization': `Bearer ${accessToken}`,
111
+ 'Content-Type': 'application/json'
112
+ };
113
+ Logger.debug('请求应用权限范围:', endpoint);
114
+ const response = await axios.get(`${this.getBaseUrl()}${endpoint}`, { headers });
115
+ const data = response.data;
116
+ if (data.code !== 0) {
117
+ throw new Error(`获取应用权限范围失败:${data.msg || '未知错误'} (错误码: ${data.code})`);
118
+ }
119
+ // 提取权限列表
120
+ // API返回格式: { "data": { "scopes": [{ "grant_status": 1, "scope_name": "...", "scope_type": "tenant"|"user" }] } }
121
+ const scopes = [];
122
+ if (data.data && Array.isArray(data.data.scopes)) {
123
+ // 根据authType过滤,只取已授权的scope(grant_status === 1)
124
+ for (const scopeItem of data.data.scopes) {
125
+ if (scopeItem.grant_status === 1 && scopeItem.scope_type === authType && scopeItem.scope_name) {
126
+ scopes.push(scopeItem.scope_name);
127
+ }
128
+ }
129
+ }
130
+ Logger.debug(`获取应用权限范围成功,共 ${scopes.length} 个${authType}权限`);
131
+ return scopes;
132
+ }
133
+ catch (error) {
134
+ Logger.error('获取应用权限范围失败:', error);
135
+ throw new Error('获取应用权限范围失败: ' + (error instanceof Error ? error.message : String(error)));
136
+ }
137
+ }
138
+ /**
139
+ * 校验scope权限是否充足
140
+ * @param requiredScopes 所需的权限列表
141
+ * @param actualScopes 实际的权限列表
142
+ * @returns 是否权限充足,以及缺失的权限列表
143
+ */
144
+ validateScopes(requiredScopes, actualScopes) {
145
+ const actualScopesSet = new Set(actualScopes);
146
+ const missingScopes = [];
147
+ for (const requiredScope of requiredScopes) {
148
+ if (!actualScopesSet.has(requiredScope)) {
149
+ missingScopes.push(requiredScope);
150
+ }
151
+ }
152
+ return {
153
+ isValid: missingScopes.length === 0,
154
+ missingScopes
155
+ };
156
+ }
157
+ /**
158
+ * 获取所需的scope列表(根据认证类型)
159
+ * @param authType 认证类型
160
+ * @returns 所需的scope列表
161
+ */
162
+ getRequiredScopes(authType) {
163
+ // 根据FEISHU_CONFIG.md中定义的权限列表,与用户提供的配置保持一致
164
+ const tenantScopes = [
165
+ "docx:document.block:convert",
166
+ "base:app:read",
167
+ "bitable:app",
168
+ "bitable:app:readonly",
169
+ "board:whiteboard:node:create",
170
+ "board:whiteboard:node:read",
171
+ "contact:user.employee_id:readonly",
172
+ "docs:document.content:read",
173
+ "docx:document",
174
+ "docx:document:create",
175
+ "docx:document:readonly",
176
+ "drive:drive",
177
+ "drive:drive:readonly",
178
+ "drive:file",
179
+ "drive:file:upload",
180
+ "sheets:spreadsheet",
181
+ "sheets:spreadsheet:readonly",
182
+ "space:document:retrieve",
183
+ "space:folder:create",
184
+ "wiki:space:read",
185
+ "wiki:space:retrieve",
186
+ "wiki:wiki",
187
+ "wiki:wiki:readonly"
188
+ ];
189
+ const userScopes = [
190
+ ...tenantScopes,
191
+ 'offline_access'
192
+ ];
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`;
218
+ Logger.error(errorMessage);
219
+ throw new ScopeInsufficientError(missingScopes, errorMessage);
220
+ }
221
+ /**
222
+ * 生成应用级别的scope校验key(基于appId、appSecret和authType)
223
+ * @param appId 应用ID
224
+ * @param appSecret 应用密钥
225
+ * @param authType 认证类型(tenant或user)
226
+ * @returns scope校验key
227
+ */
228
+ generateScopeKey(appId, appSecret, authType) {
229
+ // 使用appId、appSecret和authType生成唯一的key,用于scope版本管理
230
+ // 包含authType是因为tenant和user的权限列表不同,需要分开校验
231
+ return `app:${appId}:${appSecret.substring(0, 8)}:${authType}`;
232
+ }
233
+ /**
234
+ * 获取临时租户访问令牌(用于scope校验)
235
+ * @param appId 应用ID
236
+ * @param appSecret 应用密钥
237
+ * @returns 租户访问令牌
238
+ */
239
+ async getTempTenantTokenForScope(appId, appSecret) {
240
+ try {
241
+ const requestData = {
242
+ app_id: appId,
243
+ app_secret: appSecret,
244
+ };
245
+ const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
246
+ const headers = { 'Content-Type': 'application/json' };
247
+ Logger.debug('获取临时租户token用于scope校验:', url);
248
+ const response = await axios.post(url, requestData, { headers });
249
+ const data = response.data;
250
+ if (data.code !== 0) {
251
+ throw new Error(`获取临时租户访问令牌失败:${data.msg || '未知错误'} (错误码: ${data.code})`);
252
+ }
253
+ if (!data.tenant_access_token) {
254
+ throw new Error('获取临时租户访问令牌失败:响应中没有token');
255
+ }
256
+ Logger.debug('临时租户token获取成功,用于scope校验');
257
+ return data.tenant_access_token;
258
+ }
259
+ catch (error) {
260
+ Logger.error('获取临时租户访问令牌失败:', error);
261
+ throw new Error('获取临时租户访问令牌失败: ' + (error instanceof Error ? error.message : String(error)));
262
+ }
263
+ }
264
+ /**
265
+ * 校验scope权限(带版本管理)
266
+ * @param appId 应用ID
267
+ * @param appSecret 应用密钥
268
+ * @param authType 认证类型
269
+ */
270
+ async validateScopeWithVersion(appId, appSecret, authType) {
271
+ const tokenCacheManager = TokenCacheManager.getInstance();
272
+ // 生成应用级别的scope校验key(包含authType,因为tenant和user权限不同)
273
+ const scopeKey = this.generateScopeKey(appId, appSecret, authType);
274
+ const scopeVersion = '1.0.0'; // 当前scope版本号,可以根据需要更新
275
+ // 检查是否需要校验
276
+ if (!tokenCacheManager.shouldValidateScope(scopeKey, scopeVersion)) {
277
+ Logger.debug(`Scope版本已校验过,跳过校验: ${scopeKey}`);
278
+ return;
279
+ }
280
+ Logger.info(`开始校验scope权限,版本: ${scopeVersion}, scopeKey: ${scopeKey}`);
281
+ try {
282
+ // 使用appId和appSecret获取临时tenant token来调用scope接口
283
+ const tempTenantToken = await this.getTempTenantTokenForScope(appId, appSecret);
284
+ // 获取实际权限范围(使用tenant token,但根据authType过滤scope_type)
285
+ const actualScopes = await this.getApplicationScopes(tempTenantToken, authType);
286
+ // 获取当前版本所需的scope列表
287
+ const requiredScopes = this.getRequiredScopes(authType);
288
+ // 校验权限
289
+ const validationResult = this.validateScopes(requiredScopes, actualScopes);
290
+ if (!validationResult.isValid) {
291
+ // 权限不足,抛出错误
292
+ this.throwScopeInsufficientError(validationResult.missingScopes);
293
+ }
294
+ // 权限充足,保存版本信息
295
+ const scopeVersionInfo = {
296
+ scopeVersion,
297
+ scopeList: requiredScopes,
298
+ validatedAt: Math.floor(Date.now() / 1000),
299
+ validatedVersion: scopeVersion
300
+ };
301
+ tokenCacheManager.saveScopeVersionInfo(scopeKey, scopeVersionInfo);
302
+ Logger.info(`Scope权限校验成功,版本: ${scopeVersion}`);
303
+ }
304
+ catch (error) {
305
+ // 如果是权限不足错误,需要重新抛出,中断流程
306
+ if (error instanceof ScopeInsufficientError) {
307
+ throw error;
308
+ }
309
+ // 如果获取权限范围失败(网络错误、API调用失败等),记录警告但不阻止token使用
310
+ Logger.warn(`Scope权限校验失败,但继续使用token: ${error instanceof Error ? error.message : String(error)}`);
87
311
  }
88
312
  }
89
313
  /**
@@ -135,88 +359,6 @@ export class FeishuApiService extends BaseApiService {
135
359
  throw new Error('获取租户访问令牌失败: ' + (error instanceof Error ? error.message : String(error)));
136
360
  }
137
361
  }
138
- /**
139
- * 获取用户访问令牌
140
- * @param appId 应用ID
141
- * @param appSecret 应用密钥
142
- * @param clientKey 客户端缓存键
143
- * @param userKey 用户标识
144
- * @returns 用户访问令牌
145
- */
146
- async getUserAccessToken(appId, appSecret, clientKey, _userKey) {
147
- const tokenCacheManager = TokenCacheManager.getInstance();
148
- // 检查用户token状态
149
- const tokenStatus = tokenCacheManager.checkUserTokenStatus(clientKey);
150
- Logger.debug(`用户token状态:`, tokenStatus);
151
- if (tokenStatus.isValid && !tokenStatus.shouldRefresh) {
152
- // token有效且不需要刷新,直接返回
153
- const cachedToken = tokenCacheManager.getUserToken(clientKey);
154
- if (cachedToken) {
155
- Logger.debug('使用缓存的用户访问令牌');
156
- return cachedToken;
157
- }
158
- }
159
- if (tokenStatus.canRefresh && (tokenStatus.isExpired || tokenStatus.shouldRefresh)) {
160
- // 可以刷新token
161
- Logger.info('尝试刷新用户访问令牌');
162
- try {
163
- const tokenInfo = tokenCacheManager.getUserTokenInfo(clientKey);
164
- if (tokenInfo && tokenInfo.refresh_token) {
165
- const refreshedToken = await this.refreshUserToken(tokenInfo.refresh_token, clientKey, appId, appSecret);
166
- if (refreshedToken && refreshedToken.access_token) {
167
- Logger.info('用户访问令牌刷新成功');
168
- return refreshedToken.access_token;
169
- }
170
- }
171
- }
172
- catch (error) {
173
- Logger.warn('刷新用户访问令牌失败:', error);
174
- // 刷新失败,清除缓存,需要重新授权
175
- tokenCacheManager.removeUserToken(clientKey);
176
- }
177
- }
178
- // 没有有效的token或刷新失败,需要用户授权
179
- Logger.warn('没有有效的用户token,需要用户授权');
180
- throw new AuthRequiredError('user', '需要用户授权');
181
- }
182
- /**
183
- * 刷新用户访问令牌
184
- * @param refreshToken 刷新令牌
185
- * @param clientKey 客户端缓存键
186
- * @param appId 应用ID
187
- * @param appSecret 应用密钥
188
- * @returns 刷新后的token信息
189
- */
190
- async refreshUserToken(refreshToken, clientKey, appId, appSecret) {
191
- const tokenCacheManager = TokenCacheManager.getInstance();
192
- const body = {
193
- grant_type: 'refresh_token',
194
- client_id: appId,
195
- client_secret: appSecret,
196
- refresh_token: refreshToken
197
- };
198
- Logger.debug('刷新用户访问令牌请求:', body);
199
- const response = await axios.post('https://open.feishu.cn/open-apis/authen/v2/oauth/token', body, {
200
- headers: { 'Content-Type': 'application/json' }
201
- });
202
- const data = response.data;
203
- if (data && data.access_token && data.expires_in) {
204
- // 计算过期时间戳
205
- data.expires_at = Math.floor(Date.now() / 1000) + data.expires_in;
206
- if (data.refresh_token_expires_in) {
207
- data.refresh_token_expires_at = Math.floor(Date.now() / 1000) + data.refresh_token_expires_in;
208
- }
209
- // 缓存新的token信息
210
- const refreshTtl = data.refresh_token_expires_in || 3600 * 24 * 365; // 默认1年
211
- tokenCacheManager.cacheUserToken(clientKey, data, refreshTtl);
212
- Logger.info('用户访问令牌刷新并缓存成功');
213
- return data;
214
- }
215
- else {
216
- Logger.warn('刷新用户访问令牌失败:', data);
217
- throw new Error('刷新用户访问令牌失败');
218
- }
219
- }
220
362
  /**
221
363
  * 创建飞书文档
222
364
  * @param title 文档标题
@@ -820,6 +962,21 @@ export class FeishuApiService extends BaseApiService {
820
962
  };
821
963
  }
822
964
  break;
965
+ case BlockType.WHITEBOARD:
966
+ if ('whiteboard' in options && options.whiteboard) {
967
+ const whiteboardOptions = options.whiteboard;
968
+ blockConfig.options = {
969
+ align: (whiteboardOptions.align === 1 || whiteboardOptions.align === 2 || whiteboardOptions.align === 3)
970
+ ? whiteboardOptions.align : 1
971
+ };
972
+ }
973
+ else {
974
+ // 默认画板块选项
975
+ blockConfig.options = {
976
+ align: 1
977
+ };
978
+ }
979
+ break;
823
980
  default:
824
981
  Logger.warn(`未知的块类型: ${blockType},尝试作为标准类型处理`);
825
982
  if ('text' in options) {
@@ -890,6 +1047,14 @@ export class FeishuApiService extends BaseApiService {
890
1047
  code: mermaidConfig.code,
891
1048
  };
892
1049
  }
1050
+ else if ("whiteboard" in options) {
1051
+ blockConfig.type = BlockType.WHITEBOARD;
1052
+ const whiteboardConfig = options.whiteboard;
1053
+ blockConfig.options = {
1054
+ align: (whiteboardConfig.align === 1 || whiteboardConfig.align === 2 || whiteboardConfig.align === 3)
1055
+ ? whiteboardConfig.align : 1
1056
+ };
1057
+ }
893
1058
  break;
894
1059
  }
895
1060
  // 记录调试信息
@@ -1237,6 +1402,36 @@ export class FeishuApiService extends BaseApiService {
1237
1402
  return Buffer.from([]); // 永远不会执行到这里
1238
1403
  }
1239
1404
  }
1405
+ /**
1406
+ * 在画板中创建图表节点(支持 PlantUML 和 Mermaid)
1407
+ * @param whiteboardId 画板ID(token)
1408
+ * @param code 图表代码(PlantUML 或 Mermaid)
1409
+ * @param syntaxType 语法类型:1=PlantUML, 2=Mermaid
1410
+ * @returns 创建结果
1411
+ */
1412
+ async createDiagramNode(whiteboardId, code, syntaxType) {
1413
+ try {
1414
+ const normalizedWhiteboardId = ParamUtils.processWhiteboardId(whiteboardId);
1415
+ const endpoint = `/board/v1/whiteboards/${normalizedWhiteboardId}/nodes/plantuml`;
1416
+ const syntaxTypeName = syntaxType === 1 ? 'PlantUML' : 'Mermaid';
1417
+ Logger.info(`开始在画板中创建 ${syntaxTypeName} 节点,画板ID: ${normalizedWhiteboardId}`);
1418
+ Logger.debug(`${syntaxTypeName} 代码: ${code.substring(0, 200)}...`);
1419
+ const payload = {
1420
+ plant_uml_code: code,
1421
+ style_type: 1, // 画板样式(默认为2 经典样式) 示例值:1 可选值有: 1:画板样式(解析之后为多个画板节点,粘贴到画板中,不可对语法进行二次编辑) 2:经典样式(解析之后为一张图片,粘贴到画板中,可对语法进行二次编辑)(只有PlantUml语法支持经典样式
1422
+ syntax_type: syntaxType
1423
+ };
1424
+ Logger.debug(`请求载荷: ${JSON.stringify(payload, null, 2)}`);
1425
+ const response = await this.post(endpoint, payload);
1426
+ Logger.info(`${syntaxTypeName} 节点创建成功`);
1427
+ return response;
1428
+ }
1429
+ catch (error) {
1430
+ const syntaxTypeName = syntaxType === 1 ? 'PlantUML' : 'Mermaid';
1431
+ Logger.error(`创建 ${syntaxTypeName} 节点失败,画板ID: ${whiteboardId}`, error);
1432
+ this.handleApiError(error, `创建 ${syntaxTypeName} 节点失败`);
1433
+ }
1434
+ }
1240
1435
  /**
1241
1436
  * 从路径或URL获取图片的Base64编码
1242
1437
  * @param imagePathOrUrl 图片路径或URL
@@ -1,6 +1,8 @@
1
1
  import axios from 'axios';
2
2
  import { Config } from '../utils/config.js';
3
3
  import { Logger } from '../utils/logger.js';
4
+ import { TokenCacheManager } from '../utils/auth/tokenCacheManager.js';
5
+ import { AuthRequiredError } from '../utils/error.js';
4
6
  export class AuthService {
5
7
  constructor() {
6
8
  Object.defineProperty(this, "config", {
@@ -45,4 +47,109 @@ export class AuthService {
45
47
  Logger.debug('[AuthService] getUserTokenByCode response', data);
46
48
  return data;
47
49
  }
50
+ /**
51
+ * 刷新用户访问令牌
52
+ * 从缓存中获取token信息并刷新,如果缓存中没有必要信息则使用传入的备用参数
53
+ * @param clientKey 客户端缓存键
54
+ * @param appId 应用ID(可选,如果tokenInfo中没有则使用此参数)
55
+ * @param appSecret 应用密钥(可选,如果tokenInfo中没有则使用此参数)
56
+ * @returns 刷新后的token信息
57
+ * @throws 如果无法获取必要的刷新信息则抛出错误
58
+ */
59
+ async refreshUserToken(clientKey, appId, appSecret) {
60
+ const tokenCacheManager = TokenCacheManager.getInstance();
61
+ // 从缓存中获取token信息
62
+ const tokenInfo = tokenCacheManager.getUserTokenInfo(clientKey);
63
+ if (!tokenInfo) {
64
+ throw new Error(`无法获取token信息: ${clientKey}`);
65
+ }
66
+ // 获取刷新所需的必要信息
67
+ const actualRefreshToken = tokenInfo.refresh_token;
68
+ const actualAppId = tokenInfo.client_id || appId;
69
+ const actualAppSecret = tokenInfo.client_secret || appSecret;
70
+ // 验证必要参数
71
+ if (!actualRefreshToken) {
72
+ throw new Error('无法获取refresh_token,无法刷新用户访问令牌');
73
+ }
74
+ if (!actualAppId || !actualAppSecret) {
75
+ throw new Error('无法获取client_id或client_secret,无法刷新用户访问令牌');
76
+ }
77
+ const body = {
78
+ grant_type: 'refresh_token',
79
+ client_id: actualAppId,
80
+ client_secret: actualAppSecret,
81
+ refresh_token: actualRefreshToken
82
+ };
83
+ Logger.debug('[AuthService] 刷新用户访问令牌请求:', {
84
+ clientKey,
85
+ client_id: actualAppId,
86
+ has_refresh_token: !!actualRefreshToken
87
+ });
88
+ const response = await axios.post('https://open.feishu.cn/open-apis/authen/v2/oauth/token', body, {
89
+ headers: { 'Content-Type': 'application/json' }
90
+ });
91
+ const data = response.data;
92
+ if (data && data.access_token && data.expires_in) {
93
+ // 计算过期时间戳
94
+ data.expires_at = Math.floor(Date.now() / 1000) + data.expires_in;
95
+ if (data.refresh_token_expires_in) {
96
+ data.refresh_token_expires_at = Math.floor(Date.now() / 1000) + data.refresh_token_expires_in;
97
+ }
98
+ // 保留client_id和client_secret(优先使用tokenInfo中的,如果没有则使用实际使用的参数)
99
+ data.client_id = actualAppId;
100
+ data.client_secret = actualAppSecret;
101
+ // 缓存新的token信息
102
+ const refreshTtl = data.refresh_token_expires_in || 3600 * 24 * 365; // 默认1年
103
+ tokenCacheManager.cacheUserToken(clientKey, data, refreshTtl);
104
+ Logger.info(`[AuthService] 用户访问令牌刷新并缓存成功: ${clientKey}`);
105
+ return data;
106
+ }
107
+ else {
108
+ Logger.warn('[AuthService] 刷新用户访问令牌失败:', data);
109
+ throw new Error('刷新用户访问令牌失败');
110
+ }
111
+ }
112
+ /**
113
+ * 获取用户访问令牌
114
+ * 检查token状态,如果有效则返回缓存的token,如果过期则尝试刷新
115
+ * @param clientKey 客户端缓存键
116
+ * @param appId 应用ID(可选,如果tokenInfo中没有则使用此参数)
117
+ * @param appSecret 应用密钥(可选,如果tokenInfo中没有则使用此参数)
118
+ * @returns 用户访问令牌
119
+ * @throws 如果无法获取有效的token则抛出AuthRequiredError
120
+ */
121
+ async getUserAccessToken(clientKey, appId, appSecret) {
122
+ const tokenCacheManager = TokenCacheManager.getInstance();
123
+ // 检查用户token状态
124
+ const tokenStatus = tokenCacheManager.checkUserTokenStatus(clientKey);
125
+ Logger.debug(`[AuthService] 用户token状态:`, tokenStatus);
126
+ if (tokenStatus.isValid && !tokenStatus.shouldRefresh) {
127
+ // token有效且不需要刷新,直接返回
128
+ const cachedToken = tokenCacheManager.getUserToken(clientKey);
129
+ if (cachedToken) {
130
+ Logger.debug('[AuthService] 使用缓存的用户访问令牌');
131
+ return cachedToken;
132
+ }
133
+ }
134
+ if (tokenStatus.canRefresh && (tokenStatus.isExpired || tokenStatus.shouldRefresh)) {
135
+ // 可以刷新token
136
+ Logger.info('[AuthService] 尝试刷新用户访问令牌');
137
+ try {
138
+ // 使用统一的刷新方法,它会自动从缓存中获取必要信息
139
+ const refreshedToken = await this.refreshUserToken(clientKey, appId, appSecret);
140
+ if (refreshedToken && refreshedToken.access_token) {
141
+ Logger.info('[AuthService] 用户访问令牌刷新成功');
142
+ return refreshedToken.access_token;
143
+ }
144
+ }
145
+ catch (error) {
146
+ Logger.warn('[AuthService] 刷新用户访问令牌失败:', error);
147
+ // 刷新失败,清除缓存,需要重新授权
148
+ tokenCacheManager.removeUserToken(clientKey);
149
+ }
150
+ }
151
+ // 没有有效的token或刷新失败,需要用户授权
152
+ Logger.warn('[AuthService] 没有有效的用户token,需要用户授权');
153
+ throw new AuthRequiredError('user', '需要用户授权');
154
+ }
48
155
  }
@@ -103,9 +103,10 @@ export const ListBlockSchema = z.object({
103
103
  align: AlignSchemaWithValidation,
104
104
  });
105
105
  // 块类型枚举 - 用于批量创建块工具
106
- export const BlockTypeEnum = z.string().describe("Block type (required). Supports: 'text', 'code', 'heading', 'list', 'image','mermaid',as well as 'heading1' through 'heading9'. " +
106
+ export const BlockTypeEnum = z.string().describe("Block type (required). Supports: 'text', 'code', 'heading', 'list', 'image','mermaid','whiteboard',as well as 'heading1' through 'heading9'. " +
107
107
  "For headings, we recommend using 'heading' with level property, but 'heading1'-'heading9' are also supported. " +
108
108
  "For images, use 'image' to create empty image blocks that can be filled later. " +
109
+ "For whiteboards, use 'whiteboard' to create empty whiteboard blocks that return a token for filling content. " +
109
110
  "For text blocks, you can include both regular text and equation elements in the same block.");
110
111
  // 图片宽度参数定义
111
112
  export const ImageWidthSchema = z.number().optional().describe('Image width in pixels (optional). If not provided, the original image width will be used.');
@@ -124,6 +125,12 @@ export const MermaidCodeSchema = z.string().describe('Mermaid code (required). T
124
125
  export const MermaidBlockSchema = z.object({
125
126
  code: MermaidCodeSchema,
126
127
  });
128
+ // 画板对齐方式参数定义
129
+ export const WhiteboardAlignSchema = z.number().optional().default(2).describe('Whiteboard alignment: 1 for left, 2 for center (default), 3 for right.');
130
+ // 画板块内容定义 - 用于批量创建块工具
131
+ export const WhiteboardBlockSchema = z.object({
132
+ align: WhiteboardAlignSchema,
133
+ });
127
134
  // 块配置定义 - 用于批量创建块工具
128
135
  export const BlockConfigSchema = z.object({
129
136
  blockType: BlockTypeEnum,
@@ -134,6 +141,7 @@ export const BlockConfigSchema = z.object({
134
141
  z.object({ list: ListBlockSchema }).describe("List block options. Used when blockType is 'list'."),
135
142
  z.object({ image: ImageBlockSchema }).describe("Image block options. Used when blockType is 'image'. Creates empty image blocks."),
136
143
  z.object({ mermaid: MermaidBlockSchema }).describe("Mermaid block options. Used when blockType is 'mermaid'."),
144
+ z.object({ whiteboard: WhiteboardBlockSchema }).describe("Whiteboard block options. Used when blockType is 'whiteboard'. Creates empty whiteboard blocks that return a token for filling content."),
137
145
  z.record(z.any()).describe("Fallback for any other block options")
138
146
  ]).describe('Options for the specific block type. Provide the corresponding options object based on blockType.'),
139
147
  });
@@ -196,5 +204,26 @@ export const ImagesArraySchema = z.array(z.object({
196
204
  export const WhiteboardIdSchema = z.string().describe('Whiteboard ID (required). This is the token value from the board.token field when getting document blocks.\n' +
197
205
  'When you find a block with block_type: 43, the whiteboard ID is located in board.token field.\n' +
198
206
  'Example: "EPJKwvY5ghe3pVbKj9RcT2msnBX"');
207
+ // 画板代码参数定义(支持 PlantUML 和 Mermaid)
208
+ export const WhiteboardCodeSchema = z.string().describe('Diagram code (required). The complete diagram code to create in the whiteboard.\n' +
209
+ 'Supports both PlantUML and Mermaid formats.\n' +
210
+ 'PlantUML example: "@startuml\nAlice -> Bob: Hello\n@enduml"\n' +
211
+ 'Mermaid example: "graph TD\nA[Start] --> B[End]"');
212
+ // 语法类型参数定义
213
+ export const SyntaxTypeSchema = z.number().describe('Syntax type (required). Specifies the diagram syntax format.\n' +
214
+ '1: PlantUML syntax\n' +
215
+ '2: Mermaid syntax');
216
+ // 画板内容配置定义(包含画板ID和内容配置)
217
+ export const WhiteboardContentSchema = z.object({
218
+ whiteboardId: WhiteboardIdSchema,
219
+ code: WhiteboardCodeSchema,
220
+ syntax_type: SyntaxTypeSchema,
221
+ }).describe('Whiteboard content configuration. Contains the whiteboard ID, diagram code and syntax type.\n' +
222
+ 'whiteboardId: The token value from board.token field when creating whiteboard block (required)\n' +
223
+ 'code: The diagram code (PlantUML or Mermaid format) (required)\n' +
224
+ 'syntax_type: 1 for PlantUML, 2 for Mermaid (required)');
225
+ // 批量填充画板数组定义
226
+ export const WhiteboardFillArraySchema = z.array(WhiteboardContentSchema).describe('Array of whiteboard fill items (required). Each item must include whiteboardId, code and syntax_type.\n' +
227
+ 'Example: [{whiteboardId:"token1", code:"@startuml...", syntax_type:1}, {whiteboardId:"token2", code:"graph TD...", syntax_type:2}]');
199
228
  // 文档标题参数定义
200
229
  export const DocumentTitleSchema = z.string().describe('Document title (required). This will be displayed in the Feishu document list and document header.');
@@ -2,3 +2,4 @@ export { UserContextManager, getBaseUrl } from './userContextManager.js';
2
2
  export { UserAuthManager } from './userAuthManager.js';
3
3
  export { TokenCacheManager } from './tokenCacheManager.js';
4
4
  export { AuthUtils } from './authUtils.js';
5
+ export { TokenRefreshManager } from './tokenRefreshManager.js';