feishu-mcp 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,7 +79,7 @@
79
79
  - ~~**支持mermaid图表**:流程图、时序图等等,丰富文档内容~~ 0.1.11 ✅
80
80
  - ~~**支持表格创建**:创建包含各种块类型的复杂表格,支持样式控制~~ 0.1.2 ✅
81
81
  - ~~**支持飞书多用户user认证**:一人部署,可以多人使用~~ 0.1.3 ✅
82
- - **支持user_access_token自动刷新**:无需频繁授权,提高使用体验
82
+ - ~~**支持user_access_token自动刷新**:无需频繁授权,提高使用体验~~ 0.1.6 ✅
83
83
 
84
84
  ---
85
85
 
@@ -6,7 +6,7 @@ import { registerFeishuBlockTools } from './tools/feishuBlockTools.js';
6
6
  import { registerFeishuFolderTools } from './tools/feishuFolderTools.js';
7
7
  const serverInfo = {
8
8
  name: "Feishu MCP Server",
9
- version: "0.1.5",
9
+ version: "0.1.6",
10
10
  };
11
11
  const serverOptions = {
12
12
  capabilities: { logging: {}, tools: {} },
package/dist/server.js CHANGED
@@ -7,7 +7,7 @@ import { Logger } from './utils/logger.js';
7
7
  import { SSEConnectionManager } from './manager/sseConnectionManager.js';
8
8
  import { FeishuMcp } from './mcp/feishuMcp.js';
9
9
  import { callback } from './services/callbackService.js';
10
- import { UserAuthManager, UserContextManager, getBaseUrl, TokenCacheManager } from './utils/auth/index.js';
10
+ import { UserAuthManager, UserContextManager, getBaseUrl, TokenCacheManager, TokenRefreshManager } from './utils/auth/index.js';
11
11
  export class FeishuMcpServer {
12
12
  constructor() {
13
13
  Object.defineProperty(this, "connectionManager", {
@@ -33,6 +33,10 @@ export class FeishuMcpServer {
33
33
  this.userContextManager = UserContextManager.getInstance();
34
34
  // 初始化TokenCacheManager,确保在启动时从文件加载缓存
35
35
  TokenCacheManager.getInstance();
36
+ // 启动Token自动刷新管理器
37
+ const tokenRefreshManager = TokenRefreshManager.getInstance();
38
+ tokenRefreshManager.start();
39
+ Logger.info('Token自动刷新管理器已在服务器启动时初始化');
36
40
  }
37
41
  async connect(transport) {
38
42
  const server = new FeishuMcp();
@@ -74,6 +74,9 @@ export async function callback(req, res) {
74
74
  if (data.refresh_token_expires_in) {
75
75
  data.refresh_token_expires_at = Math.floor(Date.now() / 1000) + data.refresh_token_expires_in;
76
76
  }
77
+ // 添加client_id和client_secret,用于后续刷新token
78
+ data.client_id = appId;
79
+ data.client_secret = appSecret;
77
80
  // 缓存token信息
78
81
  const refreshTtl = data.refresh_token_expires_in || 3600 * 24 * 365; // 默认1年
79
82
  tokenCacheManager.cacheUserToken(clientKey, data, refreshTtl);
@@ -5,7 +5,7 @@ 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
9
  import axios from 'axios';
10
10
  import FormData from 'form-data';
11
11
  import fs from 'fs';
@@ -38,9 +38,16 @@ export class FeishuApiService extends BaseApiService {
38
38
  writable: true,
39
39
  value: void 0
40
40
  });
41
+ Object.defineProperty(this, "authService", {
42
+ enumerable: true,
43
+ configurable: true,
44
+ writable: true,
45
+ value: void 0
46
+ });
41
47
  this.cacheManager = CacheManager.getInstance();
42
48
  this.blockFactory = BlockFactory.getInstance();
43
49
  this.config = Config.getInstance();
50
+ this.authService = new AuthService();
44
51
  }
45
52
  /**
46
53
  * 获取飞书API服务实例
@@ -83,7 +90,7 @@ export class FeishuApiService extends BaseApiService {
83
90
  }
84
91
  else {
85
92
  // 用户模式:获取用户访问令牌
86
- return this.getUserAccessToken(appId, appSecret, clientKey, userKey);
93
+ return this.authService.getUserAccessToken(clientKey, appId, appSecret);
87
94
  }
88
95
  }
89
96
  /**
@@ -135,88 +142,6 @@ export class FeishuApiService extends BaseApiService {
135
142
  throw new Error('获取租户访问令牌失败: ' + (error instanceof Error ? error.message : String(error)));
136
143
  }
137
144
  }
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
145
  /**
221
146
  * 创建飞书文档
222
147
  * @param title 文档标题
@@ -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
  }
@@ -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';
@@ -417,4 +417,20 @@ export class TokenCacheManager {
417
417
  }, 5 * 60 * 1000);
418
418
  Logger.info('Token缓存清理定时器已启动,每5分钟执行一次');
419
419
  }
420
+ /**
421
+ * 获取所有用户token的key列表(不包含前缀)
422
+ * @returns 用户token的key数组
423
+ */
424
+ getAllUserTokenKeys() {
425
+ const keys = [];
426
+ for (const [key] of this.cache.entries()) {
427
+ if (key.startsWith('user_access_token:')) {
428
+ // 提取clientKey(去掉前缀)
429
+ const clientKey = key.substring('user_access_token:'.length);
430
+ keys.push(clientKey);
431
+ }
432
+ }
433
+ Logger.debug(`获取到 ${keys.length} 个用户token keys`);
434
+ return keys;
435
+ }
420
436
  }
@@ -0,0 +1,172 @@
1
+ import { Logger } from '../logger.js';
2
+ import { TokenCacheManager } from './tokenCacheManager.js';
3
+ import { AuthService } from '../../services/feishuAuthService.js';
4
+ /**
5
+ * Token自动刷新管理器
6
+ * 定期检查并自动刷新即将过期的用户token
7
+ */
8
+ export class TokenRefreshManager {
9
+ /**
10
+ * 私有构造函数,用于单例模式
11
+ */
12
+ constructor() {
13
+ Object.defineProperty(this, "intervalId", {
14
+ enumerable: true,
15
+ configurable: true,
16
+ writable: true,
17
+ value: null
18
+ });
19
+ Object.defineProperty(this, "checkInterval", {
20
+ enumerable: true,
21
+ configurable: true,
22
+ writable: true,
23
+ value: 5 * 60 * 1000
24
+ }); // 5分钟
25
+ Object.defineProperty(this, "isRunning", {
26
+ enumerable: true,
27
+ configurable: true,
28
+ writable: true,
29
+ value: false
30
+ });
31
+ Logger.info('Token刷新管理器已初始化');
32
+ }
33
+ /**
34
+ * 获取TokenRefreshManager实例
35
+ */
36
+ static getInstance() {
37
+ if (!TokenRefreshManager.instance) {
38
+ TokenRefreshManager.instance = new TokenRefreshManager();
39
+ }
40
+ return TokenRefreshManager.instance;
41
+ }
42
+ /**
43
+ * 启动自动刷新检查
44
+ */
45
+ start() {
46
+ if (this.isRunning) {
47
+ Logger.warn('Token刷新管理器已在运行中');
48
+ return;
49
+ }
50
+ Logger.info(`启动Token自动刷新管理器,检查间隔: ${this.checkInterval / 1000}秒`);
51
+ // 立即执行一次检查
52
+ this.checkAndRefreshTokens();
53
+ // 设置定时器
54
+ this.intervalId = setInterval(() => {
55
+ this.checkAndRefreshTokens();
56
+ }, this.checkInterval);
57
+ this.isRunning = true;
58
+ Logger.info('Token自动刷新管理器已启动');
59
+ }
60
+ /**
61
+ * 停止自动刷新检查
62
+ */
63
+ stop() {
64
+ if (!this.isRunning) {
65
+ Logger.warn('Token刷新管理器未在运行');
66
+ return;
67
+ }
68
+ if (this.intervalId) {
69
+ clearInterval(this.intervalId);
70
+ this.intervalId = null;
71
+ }
72
+ this.isRunning = false;
73
+ Logger.info('Token自动刷新管理器已停止');
74
+ }
75
+ /**
76
+ * 检查并刷新即将过期的token
77
+ */
78
+ async checkAndRefreshTokens() {
79
+ try {
80
+ Logger.debug('开始检查需要刷新的token');
81
+ const tokenCacheManager = TokenCacheManager.getInstance();
82
+ // 获取所有用户token
83
+ const allCacheKeys = this.getAllUserTokenKeys();
84
+ let checkedCount = 0;
85
+ let refreshedCount = 0;
86
+ let failedCount = 0;
87
+ for (const clientKey of allCacheKeys) {
88
+ checkedCount++;
89
+ try {
90
+ const tokenStatus = tokenCacheManager.checkUserTokenStatus(clientKey);
91
+ // 检查是否需要刷新:token即将过期(5分钟内)且可以刷新
92
+ if (tokenStatus.shouldRefresh || (tokenStatus.canRefresh && tokenStatus.isExpired)) {
93
+ Logger.info(`检测到需要刷新的token: ${clientKey}`);
94
+ const tokenInfo = tokenCacheManager.getUserTokenInfo(clientKey);
95
+ if (!tokenInfo) {
96
+ Logger.warn(`无法获取token信息: ${clientKey}`);
97
+ failedCount++;
98
+ continue;
99
+ }
100
+ // 验证是否有刷新所需的必要信息
101
+ if (!tokenInfo.refresh_token) {
102
+ Logger.warn(`token没有refresh_token,无法刷新: ${clientKey}`);
103
+ failedCount++;
104
+ continue;
105
+ }
106
+ if (!tokenInfo.client_id || !tokenInfo.client_secret) {
107
+ Logger.warn(`token缺少client_id或client_secret,无法刷新: ${clientKey}`);
108
+ failedCount++;
109
+ continue;
110
+ }
111
+ // 执行刷新,使用AuthService的统一刷新方法
112
+ try {
113
+ const authService = new AuthService();
114
+ await authService.refreshUserToken(clientKey);
115
+ refreshedCount++;
116
+ Logger.info(`token刷新成功: ${clientKey}`);
117
+ }
118
+ catch (error) {
119
+ failedCount++;
120
+ Logger.warn(`token刷新失败: ${clientKey}`, error);
121
+ // 如果刷新失败是因为refresh_token无效,清除缓存
122
+ if (error?.response?.data?.code === 99991669 || error?.message?.includes('refresh_token')) {
123
+ Logger.warn(`refresh_token无效,清除缓存: ${clientKey}`);
124
+ tokenCacheManager.removeUserToken(clientKey);
125
+ }
126
+ }
127
+ }
128
+ else {
129
+ Logger.debug(`token状态正常,无需刷新: ${clientKey}`, {
130
+ isValid: tokenStatus.isValid,
131
+ isExpired: tokenStatus.isExpired,
132
+ canRefresh: tokenStatus.canRefresh,
133
+ shouldRefresh: tokenStatus.shouldRefresh
134
+ });
135
+ }
136
+ }
137
+ catch (error) {
138
+ Logger.error(`检查token时发生错误: ${clientKey}`, error);
139
+ failedCount++;
140
+ }
141
+ }
142
+ if (refreshedCount > 0 || failedCount > 0) {
143
+ Logger.info(`Token刷新检查完成: 检查${checkedCount}个,刷新${refreshedCount}个,失败${failedCount}个`);
144
+ }
145
+ else {
146
+ Logger.debug(`Token刷新检查完成: 检查${checkedCount}个,无需刷新`);
147
+ }
148
+ }
149
+ catch (error) {
150
+ Logger.error('检查并刷新token时发生错误:', error);
151
+ }
152
+ }
153
+ /**
154
+ * 获取所有用户token的key列表
155
+ */
156
+ getAllUserTokenKeys() {
157
+ try {
158
+ const tokenCacheManager = TokenCacheManager.getInstance();
159
+ return tokenCacheManager.getAllUserTokenKeys();
160
+ }
161
+ catch (error) {
162
+ Logger.error('获取所有用户token key时发生错误:', error);
163
+ return [];
164
+ }
165
+ }
166
+ /**
167
+ * 获取运行状态
168
+ */
169
+ isRunningStatus() {
170
+ return this.isRunning;
171
+ }
172
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feishu-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Model Context Protocol server for Feishu integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",