feishu-mcp 0.1.1 → 0.1.3

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.
@@ -246,4 +246,92 @@ export class BlockFactory {
246
246
  },
247
247
  };
248
248
  }
249
+ /**
250
+ * 创建表格块
251
+ * @param options 表格块选项
252
+ * @returns 表格块内容对象
253
+ */
254
+ createTableBlock(options) {
255
+ const { columnSize, rowSize, cells = [] } = options;
256
+ // 生成表格ID
257
+ const tableId = `table_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
258
+ const imageBlocks = Array();
259
+ // 创建表格单元格
260
+ const tableCells = [];
261
+ const descendants = [];
262
+ for (let row = 0; row < rowSize; row++) {
263
+ for (let col = 0; col < columnSize; col++) {
264
+ const cellId = `table_cell${row}_${col}`;
265
+ // 查找是否有配置的单元格内容
266
+ const cellConfigs = cells.filter(cell => cell.coordinate.row === row && cell.coordinate.column === col);
267
+ // 创建单元格内容
268
+ const cellContentBlocks = [];
269
+ const cellContentIds = [];
270
+ if (cellConfigs.length > 0) {
271
+ // 处理多个内容块
272
+ cellConfigs.forEach((cellConfig, index) => {
273
+ const cellContentId = `${cellId}_child_${index}`;
274
+ const cellContentBlock = {
275
+ block_id: cellContentId,
276
+ ...cellConfig.content,
277
+ children: []
278
+ };
279
+ cellContentBlocks.push(cellContentBlock);
280
+ cellContentIds.push(cellContentId);
281
+ Logger.info(`处理块:${JSON.stringify(cellConfig)} ${index}`);
282
+ if (cellConfig.content.block_type === 27) {
283
+ //把图片块保存起来,用于后续获取该图片块的token
284
+ imageBlocks.push({
285
+ coordinate: cellConfig.coordinate,
286
+ localBlockId: cellContentId,
287
+ });
288
+ }
289
+ });
290
+ }
291
+ else {
292
+ // 创建空的文本块
293
+ const cellContentId = `${cellId}_child`;
294
+ const cellContentBlock = {
295
+ block_id: cellContentId,
296
+ ...this.createTextBlock({
297
+ textContents: [{ text: "" }]
298
+ }),
299
+ children: []
300
+ };
301
+ cellContentBlocks.push(cellContentBlock);
302
+ cellContentIds.push(cellContentId);
303
+ }
304
+ // 创建表格单元格块
305
+ const tableCell = {
306
+ block_id: cellId,
307
+ block_type: 32, // 表格单元格类型
308
+ table_cell: {},
309
+ children: cellContentIds
310
+ };
311
+ tableCells.push(cellId);
312
+ descendants.push(tableCell);
313
+ descendants.push(...cellContentBlocks);
314
+ }
315
+ }
316
+ // 创建表格主体
317
+ const tableBlock = {
318
+ block_id: tableId,
319
+ block_type: 31, // 表格块类型
320
+ table: {
321
+ property: {
322
+ row_size: rowSize,
323
+ column_size: columnSize
324
+ }
325
+ },
326
+ children: tableCells
327
+ };
328
+ descendants.unshift(tableBlock);
329
+ // 过滤并记录 block_type 为 27 的元素
330
+ Logger.info(`发现 ${imageBlocks.length} 个图片块 (block_type: 27): ${JSON.stringify(imageBlocks)}`);
331
+ return {
332
+ children_id: [tableId],
333
+ descendants: descendants,
334
+ imageBlocks: imageBlocks
335
+ };
336
+ }
249
337
  }
@@ -1,7 +1,7 @@
1
1
  import { AuthService } from './feishuAuthService.js';
2
2
  import { Config } from '../utils/config.js';
3
- import { CacheManager } from '../utils/cache.js';
4
3
  import { renderFeishuAuthResultHtml } from '../utils/document.js';
4
+ import { AuthUtils, TokenCacheManager } from '../utils/auth';
5
5
  // 通用响应码
6
6
  const CODE = {
7
7
  SUCCESS: 0,
@@ -29,22 +29,34 @@ export async function callback(req, res) {
29
29
  console.log('[callback] 缺少code参数');
30
30
  return sendFail(res, '缺少code参数', CODE.PARAM_ERROR);
31
31
  }
32
- // 校验state(clientKey)
33
- const client_id = config.feishu.appId;
34
- const client_secret = config.feishu.appSecret;
35
- const expectedClientKey = await CacheManager.getClientKey(client_id, client_secret);
36
- if (state !== expectedClientKey) {
37
- console.log('[callback] state(clientKey)不匹配');
38
- return sendFail(res, 'state(clientKey)不匹配', CODE.PARAM_ERROR);
32
+ if (!state) {
33
+ console.log('[callback] 缺少state参数');
34
+ return sendFail(res, '缺少state参数', CODE.PARAM_ERROR);
39
35
  }
40
- const redirect_uri = `http://localhost:${config.server.port}/callback`;
36
+ // 解析state参数
37
+ const stateData = AuthUtils.decodeState(state);
38
+ if (!stateData) {
39
+ console.log('[callback] state参数解析失败');
40
+ return sendFail(res, 'state参数格式错误', CODE.PARAM_ERROR);
41
+ }
42
+ const { appId, appSecret, clientKey, redirectUri } = stateData;
43
+ console.log(`[callback] 解析state成功:`, { appId, clientKey, redirectUri });
44
+ // 验证state中的appId和appSecret是否与配置匹配
45
+ const configAppId = config.feishu.appId;
46
+ const configAppSecret = config.feishu.appSecret;
47
+ if (appId !== configAppId || appSecret !== configAppSecret) {
48
+ console.log('[callback] state中的appId或appSecret与配置不匹配');
49
+ return sendFail(res, 'state参数验证失败', CODE.PARAM_ERROR);
50
+ }
51
+ // 使用从state中解析的redirect_uri,如果没有则使用默认值
52
+ const redirect_uri = redirectUri || `http://localhost:${config.server.port}/callback`;
41
53
  const session = req.session;
42
54
  const code_verifier = session?.code_verifier || undefined;
43
55
  try {
44
56
  // 获取 user_access_token
45
57
  const tokenResp = await authService.getUserTokenByCode({
46
- client_id,
47
- client_secret,
58
+ client_id: appId,
59
+ client_secret: appSecret,
48
60
  code,
49
61
  redirect_uri,
50
62
  code_verifier
@@ -54,6 +66,19 @@ export async function callback(req, res) {
54
66
  if (!data || data.code !== 0 || !data.access_token) {
55
67
  return sendFail(res, `获取 access_token 失败,飞书返回: ${JSON.stringify(tokenResp)}`, CODE.CUSTOM);
56
68
  }
69
+ // 使用TokenCacheManager缓存token信息
70
+ const tokenCacheManager = TokenCacheManager.getInstance();
71
+ if (data.access_token && data.expires_in) {
72
+ // 计算过期时间戳
73
+ data.expires_at = Math.floor(Date.now() / 1000) + data.expires_in;
74
+ if (data.refresh_token_expires_in) {
75
+ data.refresh_token_expires_at = Math.floor(Date.now() / 1000) + data.refresh_token_expires_in;
76
+ }
77
+ // 缓存token信息
78
+ const refreshTtl = data.refresh_token_expires_in || 3600 * 24 * 365; // 默认1年
79
+ tokenCacheManager.cacheUserToken(clientKey, data, refreshTtl);
80
+ console.log(`[callback] token已缓存到clientKey: ${clientKey}`);
81
+ }
57
82
  // 获取用户信息
58
83
  const access_token = data.access_token;
59
84
  let userInfo = null;
@@ -61,20 +86,10 @@ export async function callback(req, res) {
61
86
  userInfo = await authService.getUserInfo(access_token);
62
87
  console.log('[callback] feishu userInfo:', userInfo);
63
88
  }
64
- return sendSuccess(res, { ...data, userInfo });
89
+ return sendSuccess(res, { ...data, userInfo, clientKey });
65
90
  }
66
91
  catch (e) {
67
92
  console.error('[callback] 请求飞书token或用户信息失败:', e);
68
93
  return sendFail(res, `请求飞书token或用户信息失败: ${e}`, CODE.CUSTOM);
69
94
  }
70
95
  }
71
- export async function getTokenByParams({ client_id, client_secret, token_type }) {
72
- const authService = new AuthService();
73
- if (client_id)
74
- authService.config.feishu.appId = client_id;
75
- if (client_secret)
76
- authService.config.feishu.appSecret = client_secret;
77
- if (token_type)
78
- authService.config.feishu.authType = token_type === 'user' ? 'user' : 'tenant';
79
- return await authService.getToken();
80
- }
@@ -4,6 +4,8 @@ import { Config } from '../utils/config.js';
4
4
  import { CacheManager } from '../utils/cache.js';
5
5
  import { ParamUtils } from '../utils/paramUtils.js';
6
6
  import { BlockFactory, BlockType } from './blockFactory.js';
7
+ import { AuthUtils, TokenCacheManager } from '../utils/auth';
8
+ import { AuthRequiredError } from '../utils/error.js';
7
9
  import axios from 'axios';
8
10
  import FormData from 'form-data';
9
11
  import fs from 'fs';
@@ -66,35 +68,154 @@ export class FeishuApiService extends BaseApiService {
66
68
  }
67
69
  /**
68
70
  * 获取访问令牌
71
+ * @param userKey 用户标识(可选)
69
72
  * @returns 访问令牌
70
73
  * @throws 如果获取令牌失败则抛出错误
71
74
  */
72
- async getAccessToken() {
73
- // 尝试从缓存获取
74
- const cachedToken = this.cacheManager.getToken();
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
+ if (authType === 'tenant') {
81
+ // 租户模式:获取租户访问令牌
82
+ return this.getTenantAccessToken(appId, appSecret, clientKey);
83
+ }
84
+ else {
85
+ // 用户模式:获取用户访问令牌
86
+ return this.getUserAccessToken(appId, appSecret, clientKey, userKey);
87
+ }
88
+ }
89
+ /**
90
+ * 获取租户访问令牌
91
+ * @param appId 应用ID
92
+ * @param appSecret 应用密钥
93
+ * @param clientKey 客户端缓存键
94
+ * @returns 租户访问令牌
95
+ */
96
+ async getTenantAccessToken(appId, appSecret, clientKey) {
97
+ const tokenCacheManager = TokenCacheManager.getInstance();
98
+ // 尝试从缓存获取租户token
99
+ const cachedToken = tokenCacheManager.getTenantToken(clientKey);
75
100
  if (cachedToken) {
76
- Logger.debug('使用缓存的访问令牌');
101
+ Logger.debug('使用缓存的租户访问令牌');
77
102
  return cachedToken;
78
103
  }
79
- // 通过HTTP请求调用配置的tokenEndpoint接口
80
- const { appId, appSecret, authType, tokenEndpoint } = this.config.feishu;
81
- const params = new URLSearchParams({
104
+ // 缓存中没有token,请求新的租户token
105
+ Logger.info('缓存中没有租户token,请求新的租户访问令牌');
106
+ try {
107
+ const requestData = {
108
+ app_id: appId,
109
+ app_secret: appSecret,
110
+ };
111
+ const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
112
+ const headers = { 'Content-Type': 'application/json' };
113
+ Logger.debug('请求租户访问令牌:', url, requestData);
114
+ const response = await axios.post(url, requestData, { headers });
115
+ const data = response.data;
116
+ if (data.code !== 0) {
117
+ throw new Error(`获取租户访问令牌失败:${data.msg || '未知错误'} (错误码: ${data.code})`);
118
+ }
119
+ if (!data.tenant_access_token) {
120
+ throw new Error('获取租户访问令牌失败:响应中没有token');
121
+ }
122
+ // 计算绝对过期时间戳
123
+ const expire_at = Math.floor(Date.now() / 1000) + (data.expire || 0);
124
+ const tokenInfo = {
125
+ app_access_token: data.tenant_access_token,
126
+ expires_at: expire_at
127
+ };
128
+ // 缓存租户token
129
+ tokenCacheManager.cacheTenantToken(clientKey, tokenInfo, data.expire);
130
+ Logger.info('租户访问令牌获取并缓存成功');
131
+ return data.tenant_access_token;
132
+ }
133
+ catch (error) {
134
+ Logger.error('获取租户访问令牌失败:', error);
135
+ throw new Error('获取租户访问令牌失败: ' + (error instanceof Error ? error.message : String(error)));
136
+ }
137
+ }
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',
82
194
  client_id: appId,
83
195
  client_secret: appSecret,
84
- token_type: authType
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' }
85
201
  });
86
- const url = `${tokenEndpoint}?${params.toString()}`;
87
- const response = await axios.get(url);
88
- const tokenResult = response.data?.data;
89
- if (tokenResult && tokenResult.access_token) {
90
- Logger.debug('使用Http的访问令牌');
91
- CacheManager.getInstance().cacheToken(tokenResult.access_token, tokenResult.expires_in);
92
- return tokenResult.access_token;
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;
93
214
  }
94
- if (tokenResult && tokenResult.needAuth && tokenResult.url) {
95
- throw new Error(`请在浏览器打开以下链接进行授权:\n\n[点击授权](${tokenResult.url})`);
215
+ else {
216
+ Logger.warn('刷新用户访问令牌失败:', data);
217
+ throw new Error('刷新用户访问令牌失败');
96
218
  }
97
- throw new Error('无法获取有效的access_token');
98
219
  }
99
220
  /**
100
221
  * 创建飞书文档
@@ -411,6 +532,91 @@ export class FeishuApiService extends BaseApiService {
411
532
  const response = await this.post(endpoint, payload);
412
533
  return response;
413
534
  }
535
+ /**
536
+ * 创建表格块
537
+ * @param documentId 文档ID或URL
538
+ * @param parentBlockId 父块ID
539
+ * @param tableConfig 表格配置
540
+ * @param index 插入位置索引
541
+ * @returns 创建结果
542
+ */
543
+ async createTableBlock(documentId, parentBlockId, tableConfig, index = 0) {
544
+ const normalizedDocId = ParamUtils.processDocumentId(documentId);
545
+ const endpoint = `/docx/v1/documents/${normalizedDocId}/blocks/${parentBlockId}/descendant?document_revision_id=-1`;
546
+ // 处理表格配置,为每个单元格创建正确的内容块
547
+ const processedTableConfig = {
548
+ ...tableConfig,
549
+ cells: tableConfig.cells?.map(cell => ({
550
+ ...cell,
551
+ content: this.createBlockContent(cell.content.blockType, cell.content.options)
552
+ }))
553
+ };
554
+ // 使用 BlockFactory 创建表格块内容
555
+ const tableStructure = this.blockFactory.createTableBlock(processedTableConfig);
556
+ const payload = {
557
+ children_id: tableStructure.children_id,
558
+ descendants: tableStructure.descendants,
559
+ index
560
+ };
561
+ Logger.info(`请求创建表格块: ${tableConfig.rowSize}x${tableConfig.columnSize},单元格数量: ${tableConfig.cells?.length || 0}`);
562
+ const response = await this.post(endpoint, payload);
563
+ // 创建表格成功后,获取单元格中的图片token
564
+ const imageTokens = await this.extractImageTokensFromTable(response, tableStructure.imageBlocks);
565
+ return {
566
+ ...response,
567
+ imageTokens: imageTokens
568
+ };
569
+ }
570
+ /**
571
+ * 从表格中提取图片块信息(优化版本)
572
+ * @param tableResponse 创建表格的响应数据
573
+ * @param cells 表格配置,包含原始cells信息
574
+ * @returns 图片块信息数组,包含坐标和块ID信息
575
+ */
576
+ async extractImageTokensFromTable(tableResponse, cells) {
577
+ try {
578
+ const imageTokens = [];
579
+ Logger.info(`tableResponse: ${JSON.stringify(tableResponse)}`);
580
+ // 判断 cells 是否为空
581
+ if (!cells || cells.length === 0) {
582
+ Logger.info('表格中没有图片单元格,跳过图片块信息提取');
583
+ return imageTokens;
584
+ }
585
+ // 创建 localBlockId 到 block_id 的映射
586
+ const blockIdMap = new Map();
587
+ if (tableResponse && tableResponse.block_id_relations) {
588
+ for (const relation of tableResponse.block_id_relations) {
589
+ blockIdMap.set(relation.temporary_block_id, relation.block_id);
590
+ }
591
+ Logger.debug(`创建了 ${blockIdMap.size} 个块ID映射关系`);
592
+ }
593
+ // 遍历所有图片单元格
594
+ for (const cell of cells) {
595
+ const { coordinate, localBlockId } = cell;
596
+ const { row, column } = coordinate;
597
+ // 根据 localBlockId 在创建表格的返回数据中找到 block_id
598
+ const blockId = blockIdMap.get(localBlockId);
599
+ if (!blockId) {
600
+ Logger.warn(`未找到 localBlockId ${localBlockId} 对应的 block_id`);
601
+ continue;
602
+ }
603
+ Logger.debug(`处理单元格 (${row}, ${column}),localBlockId: ${localBlockId},blockId: ${blockId}`);
604
+ // 直接添加块信息
605
+ imageTokens.push({
606
+ row,
607
+ column,
608
+ blockId
609
+ });
610
+ Logger.info(`提取到图片块信息: 位置(${row}, ${column}),blockId: ${blockId}`);
611
+ }
612
+ Logger.info(`成功提取 ${imageTokens.length} 个图片块信息`);
613
+ return imageTokens;
614
+ }
615
+ catch (error) {
616
+ Logger.error(`提取表格图片块信息失败: ${error}`);
617
+ return [];
618
+ }
619
+ }
414
620
  /**
415
621
  * 删除文档中的块,支持批量删除
416
622
  * @param documentId 文档ID或URL
@@ -793,7 +999,7 @@ export class FeishuApiService extends BaseApiService {
793
999
  async searchDocuments(searchKey, count = 50) {
794
1000
  try {
795
1001
  Logger.info(`开始搜索文档,关键字: ${searchKey}`);
796
- const endpoint = `//suite/docs-api/search/object`;
1002
+ const endpoint = `/suite/docs-api/search/object`;
797
1003
  let offset = 0;
798
1004
  let allResults = [];
799
1005
  let hasMore = true;
@@ -1,6 +1,5 @@
1
1
  import axios from 'axios';
2
2
  import { Config } from '../utils/config.js';
3
- import { CacheManager } from '../utils/cache.js';
4
3
  import { Logger } from '../utils/logger.js';
5
4
  export class AuthService {
6
5
  constructor() {
@@ -10,129 +9,6 @@ export class AuthService {
10
9
  writable: true,
11
10
  value: Config.getInstance()
12
11
  });
13
- Object.defineProperty(this, "cache", {
14
- enumerable: true,
15
- configurable: true,
16
- writable: true,
17
- value: CacheManager.getInstance()
18
- });
19
- }
20
- // 获取token主入口
21
- async getToken(options) {
22
- Logger.warn('[AuthService] getToken called', options);
23
- const config = this.config.feishu;
24
- const client_id = options?.client_id || config.appId;
25
- const client_secret = options?.client_secret || config.appSecret;
26
- const authType = options?.authType || config.authType;
27
- const clientKey = await CacheManager.getClientKey(client_id, client_secret);
28
- Logger.warn('[AuthService] getToken resolved clientKey', clientKey, 'authType', authType);
29
- if (authType === 'tenant') {
30
- return this.getTenantToken(client_id, client_secret, clientKey);
31
- }
32
- else {
33
- let tokenObj = this.cache.getUserToken(clientKey);
34
- const now = Date.now() / 1000;
35
- if (!tokenObj || tokenObj.refresh_token_expires_at < now) {
36
- Logger.warn('[AuthService] No user token in cache, need user auth', clientKey);
37
- // 返回授权链接
38
- const redirect_uri = encodeURIComponent(`http://localhost:${this.config.server.port}/callback`);
39
- const scope = encodeURIComponent('base:app:read bitable:app bitable:app:readonly board:whiteboard:node:read contact:user.employee_id:readonly docs:document.content:read docx:document docx:document.block:convert docx:document:create docx:document:readonly drive:drive drive:drive:readonly drive:file drive:file:upload sheets:spreadsheet sheets:spreadsheet:readonly space:document:retrieve space:folder:create wiki:space:read wiki:space:retrieve wiki:wiki wiki:wiki:readonly offline_access');
40
- const state = clientKey;
41
- const url = `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${client_id}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}`;
42
- return { needAuth: true, url };
43
- }
44
- Logger.debug('[AuthService] User token found in cache', tokenObj);
45
- if (tokenObj.expires_at && tokenObj.expires_at < now) {
46
- Logger.warn('[AuthService] User token expired, try refresh', tokenObj);
47
- if (tokenObj.refresh_token) {
48
- tokenObj = await this.refreshUserToken(tokenObj.refresh_token, clientKey, client_id, client_secret);
49
- }
50
- else {
51
- Logger.warn('[AuthService] No refresh_token, clear cache and require re-auth', clientKey);
52
- this.cache.cacheUserToken(clientKey, null, 0);
53
- return { needAuth: true, url: '请重新授权' };
54
- }
55
- }
56
- Logger.warn('[AuthService] Return user access_token', tokenObj.access_token);
57
- // 计算剩余有效期(秒)
58
- const expires_in = tokenObj.expires_at ? Math.max(tokenObj.expires_at - now, 0) : undefined;
59
- return { access_token: tokenObj.access_token, expires_in, ...tokenObj };
60
- }
61
- }
62
- // 获取tenant_access_token
63
- async getTenantToken(client_id, client_secret, clientKey) {
64
- Logger.warn('[AuthService] getTenantToken called', { client_id, clientKey });
65
- // 尝试从缓存获取
66
- const cacheKey = clientKey;
67
- const cachedTokenObj = this.cache.getTenantToken(cacheKey);
68
- if (cachedTokenObj) {
69
- Logger.warn('[AuthService] Tenant token cache hit', cacheKey);
70
- const { tenant_access_token, expire_at } = cachedTokenObj;
71
- const now = Math.floor(Date.now() / 1000);
72
- const expires_in = expire_at ? Math.max(expire_at - now, 0) : undefined;
73
- return { access_token: tenant_access_token, expires_in };
74
- }
75
- try {
76
- const requestData = {
77
- app_id: client_id,
78
- app_secret: client_secret,
79
- };
80
- const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
81
- const headers = { 'Content-Type': 'application/json' };
82
- Logger.debug('[AuthService] Requesting tenant_access_token', url, requestData);
83
- const response = await axios.post(url, requestData, { headers });
84
- const data = response.data;
85
- Logger.debug('[AuthService] tenant_access_token response', data);
86
- if (!data || typeof data !== 'object') {
87
- Logger.error('[AuthService] tenant_access_token invalid response', data);
88
- throw new Error('获取飞书访问令牌失败:响应格式无效');
89
- }
90
- if (data.code !== 0) {
91
- Logger.error('[AuthService] tenant_access_token error', data);
92
- throw new Error(`获取飞书访问令牌失败:${data.msg || '未知错误'} (错误码: ${data.code})`);
93
- }
94
- if (!data.tenant_access_token) {
95
- Logger.error('[AuthService] tenant_access_token missing in response', data);
96
- throw new Error('获取飞书访问令牌失败:响应中没有token');
97
- }
98
- // 计算绝对过期时间戳
99
- const expire_at = Math.floor(Date.now() / 1000) + (data.expire || 0);
100
- const tokenObj = {
101
- tenant_access_token: data.tenant_access_token,
102
- expire_at
103
- };
104
- this.cache.cacheTenantToken(cacheKey, tokenObj, data.expire);
105
- Logger.warn('[AuthService] tenant_access_token cached', cacheKey);
106
- // 返回token对象和expires_in
107
- return { access_token: data.tenant_access_token, expires_in: data.expire, expire_at };
108
- }
109
- catch (error) {
110
- Logger.error('[AuthService] getTenantToken error', error);
111
- throw new Error('获取飞书访问令牌失败: ' + (error instanceof Error ? error.message : String(error)));
112
- }
113
- }
114
- // 刷新user_access_token
115
- async refreshUserToken(refresh_token, clientKey, client_id, client_secret) {
116
- Logger.warn('[AuthService] refreshUserToken called', { clientKey });
117
- const body = {
118
- grant_type: 'refresh_token',
119
- client_id,
120
- client_secret,
121
- refresh_token
122
- };
123
- Logger.debug('[AuthService] refreshUserToken request', body);
124
- const response = await axios.post('https://open.feishu.cn/open-apis/authen/v2/oauth/token', body, { headers: { 'Content-Type': 'application/json' } });
125
- const data = response.data;
126
- Logger.debug('[AuthService] refreshUserToken response', data);
127
- if (data && data.access_token && data.expires_in) {
128
- data.expires_in = Math.floor(Date.now() / 1000) + data.expires_in;
129
- this.cache.cacheUserToken(clientKey, data, data.expires_in);
130
- Logger.warn('[AuthService] Refreshed user_access_token cached', clientKey);
131
- }
132
- else {
133
- Logger.warn('[AuthService] refreshUserToken failed', data);
134
- }
135
- return data;
136
12
  }
137
13
  // 获取用户信息
138
14
  async getUserInfo(access_token) {
@@ -150,7 +26,6 @@ export class AuthService {
150
26
  // 通过授权码换取user_access_token
151
27
  async getUserTokenByCode({ client_id, client_secret, code, redirect_uri, code_verifier }) {
152
28
  Logger.warn('[AuthService] getUserTokenByCode called', { client_id, code, redirect_uri });
153
- const clientKey = await CacheManager.getClientKey(client_id, client_secret);
154
29
  const body = {
155
30
  grant_type: 'authorization_code',
156
31
  client_id,
@@ -168,18 +43,6 @@ export class AuthService {
168
43
  });
169
44
  const data = await response.json();
170
45
  Logger.debug('[AuthService] getUserTokenByCode response', data);
171
- // 缓存user_access_token
172
- if (data && data.access_token && data.expires_in) {
173
- data.expires_at = Math.floor(Date.now() / 1000) + data.expires_in;
174
- data.refresh_token_expires_at = Math.floor(Date.now() / 1000) + data.refresh_token_expires_in;
175
- // 缓存时间应为 refresh_token 的有效期,防止缓存被提前清理
176
- const refreshTtl = data.refresh_expires_in || 3600 * 24 * 365; // 默认1年
177
- this.cache.cacheUserToken(clientKey, data, refreshTtl);
178
- Logger.warn('[AuthService] user_access_token cached', clientKey, 'refreshTtl', refreshTtl);
179
- }
180
- else {
181
- Logger.warn('[AuthService] getUserTokenByCode failed', data);
182
- }
183
46
  return data;
184
47
  }
185
48
  }
@@ -137,6 +137,33 @@ export const BlockConfigSchema = z.object({
137
137
  z.record(z.any()).describe("Fallback for any other block options")
138
138
  ]).describe('Options for the specific block type. Provide the corresponding options object based on blockType.'),
139
139
  });
140
+ // 表格列数参数定义
141
+ export const TableColumnSizeSchema = z.number().min(1).describe('Table column size (required). The number of columns in the table. Must be at least 1.');
142
+ // 表格行数参数定义
143
+ export const TableRowSizeSchema = z.number().min(1).describe('Table row size (required). The number of rows in the table. Must be at least 1.');
144
+ // 表格单元格坐标参数定义
145
+ export const TableCellCoordinateSchema = z.object({
146
+ row: z.number().min(0).describe('Row coordinate (0-based). The row position of the cell in the table.'),
147
+ column: z.number().min(0).describe('Column coordinate (0-based). The column position of the cell in the table.')
148
+ });
149
+ // 表格单元格内容配置定义
150
+ export const TableCellContentSchema = z.object({
151
+ coordinate: TableCellCoordinateSchema,
152
+ content: BlockConfigSchema
153
+ });
154
+ // 表格创建参数定义 - 专门用于创建表格块工具
155
+ export const TableCreateSchema = z.object({
156
+ columnSize: TableColumnSizeSchema,
157
+ rowSize: TableRowSizeSchema,
158
+ cells: z.array(TableCellContentSchema).optional().describe('Array of cell configurations (optional). Each cell specifies its position (row, column) and content block configuration. ' +
159
+ 'If not provided, empty text blocks will be created for all cells. ' +
160
+ 'IMPORTANT: Multiple cells can have the same coordinates (row, column) - when this happens, ' +
161
+ 'the content blocks will be added sequentially to the same cell, allowing you to create rich content ' +
162
+ 'with multiple blocks (text, code, images, etc.) within a single cell. ' +
163
+ 'Example: [{coordinate:{row:0,column:0}, content:{blockType:"text", options:{text:{textStyles:[{text:"Header"}]}}}, ' +
164
+ '{coordinate:{row:0,column:0}, content:{blockType:"code", options:{code:{code:"console.log(\'hello\')", language:30}}}}] ' +
165
+ 'will add both a text block and a code block to cell (0,0).')
166
+ });
140
167
  // 媒体ID参数定义
141
168
  export const MediaIdSchema = z.string().describe('Media ID (required). The unique identifier for a media resource (image, file, etc.) in Feishu. ' +
142
169
  'Usually obtained from image blocks or file references in documents. ' +