UNPKG

feishu-mcp

Version:

Model Context Protocol server for Feishu integration

597 lines (596 loc) 22.2 kB
import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { Logger } from '../logger.js'; import { migrateLegacyTokenCacheIfNeeded } from './legacyCacheMigration.js'; /** * Token缓存管理器 * 专门处理用户token和租户token的缓存管理 */ export class TokenCacheManager { /** * 私有构造函数,用于单例模式 */ constructor() { Object.defineProperty(this, "cache", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "userTokenCacheFile", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "tenantTokenCacheFile", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "scopeVersionCacheFile", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "cacheDir", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "cleanupTimerId", { enumerable: true, configurable: true, writable: true, value: null }); this.cache = new Map(); this.cacheDir = this.resolveCacheDir(); this.userTokenCacheFile = path.join(this.cacheDir, 'user_token_cache.json'); this.tenantTokenCacheFile = path.join(this.cacheDir, 'tenant_token_cache.json'); this.scopeVersionCacheFile = path.join(this.cacheDir, 'scope_version_cache.json'); Logger.info(`Token缓存目录: ${this.cacheDir}`); migrateLegacyTokenCacheIfNeeded(this.cacheDir, [ this.userTokenCacheFile, this.tenantTokenCacheFile, this.scopeVersionCacheFile, ]); this.loadTokenCaches(); this.startCacheCleanupTimer(); } /** * 解析并确保可写的缓存目录 */ resolveCacheDir() { const candidates = [ path.join(os.homedir(), '.cache', 'feishu-mcp'), path.join(os.homedir(), '.feishu-mcp'), ]; for (const dir of candidates) { try { fs.mkdirSync(dir, { recursive: true }); fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK); return dir; } catch (error) { Logger.warn(`缓存目录不可用,尝试下一个: ${dir}`, error); } } throw new Error('无法找到可读写的缓存目录'); } /** * 获取TokenCacheManager实例 */ static getInstance() { if (!TokenCacheManager.instance) { TokenCacheManager.instance = new TokenCacheManager(); } return TokenCacheManager.instance; } /** * 系统启动时从本地文件缓存中读取token记录 */ loadTokenCaches() { this.loadUserTokenCache(); this.loadTenantTokenCache(); this.loadScopeVersionCache(); } /** * 加载用户token缓存 */ loadUserTokenCache() { if (fs.existsSync(this.userTokenCacheFile)) { try { const raw = fs.readFileSync(this.userTokenCacheFile, 'utf-8'); const cacheData = JSON.parse(raw); let loadedCount = 0; for (const key in cacheData) { if (key.startsWith('user_access_token:')) { this.cache.set(key, cacheData[key]); loadedCount++; } } Logger.info(`已加载用户token缓存,共 ${loadedCount} 条记录`); } catch (error) { Logger.warn('加载用户token缓存失败:', error); } } else { Logger.info('用户token缓存文件不存在,将创建新的缓存'); } } /** * 加载租户token缓存 */ loadTenantTokenCache() { if (fs.existsSync(this.tenantTokenCacheFile)) { try { const raw = fs.readFileSync(this.tenantTokenCacheFile, 'utf-8'); const cacheData = JSON.parse(raw); let loadedCount = 0; for (const key in cacheData) { if (key.startsWith('tenant_access_token:')) { this.cache.set(key, cacheData[key]); loadedCount++; } } Logger.info(`已加载租户token缓存,共 ${loadedCount} 条记录`); } catch (error) { Logger.warn('加载租户token缓存失败:', error); } } } /** * 根据key获取完整的用户token信息 * @param key 缓存键 * @returns 完整的用户token信息对象,如果未找到或refresh_token过期则返回null */ getUserTokenInfo(key) { const cacheKey = `user_access_token:${key}`; let cacheItem = this.cache.get(cacheKey); // 内存未命中时,尝试从文件重新加载(应对多进程 stdio 场景:callback 在进程 A,API 调用在进程 B) if (!cacheItem && fs.existsSync(this.userTokenCacheFile)) { this.loadUserTokenCache(); cacheItem = this.cache.get(cacheKey); } if (!cacheItem) { Logger.debug(`用户token信息未找到: ${key}`); return null; } const tokenInfo = cacheItem.data; const now = Math.floor(Date.now() / 1000); // 检查refresh_token是否过期(如果有的话) if (tokenInfo.refresh_token && tokenInfo.refresh_token_expires_at) { if (tokenInfo.refresh_token_expires_at < now) { Logger.debug(`用户token的refresh_token已过期,从缓存中删除: ${key}`); this.cache.delete(cacheKey); this.saveUserTokenCache(); return null; } } else { // 如果没有refresh_token信息,检查缓存本身是否过期 if (Date.now() > cacheItem.expiresAt) { Logger.debug(`用户token缓存已过期: ${key}`); this.cache.delete(cacheKey); this.saveUserTokenCache(); return null; } } Logger.debug(`获取用户token信息成功: ${key}`); return tokenInfo; } /** * 根据key获取用户的access_token值 * @param key 缓存键 * @returns access_token字符串,如果未找到或已过期则返回null */ getUserToken(key) { const tokenInfo = this.getUserTokenInfo(key); return tokenInfo ? tokenInfo.access_token : null; } /** * 根据key获取租户token信息 * @param key 缓存键 * @returns 租户token信息,如果未找到或已过期则返回null */ getTenantTokenInfo(key) { const cacheKey = `tenant_access_token:${key}`; const cacheItem = this.cache.get(cacheKey); if (!cacheItem) { Logger.debug(`租户token信息未找到: ${key}`); return null; } // 检查是否过期 if (Date.now() > cacheItem.expiresAt) { Logger.debug(`租户token信息已过期: ${key}`); this.cache.delete(cacheKey); this.saveTenantTokenCache(); return null; } Logger.debug(`获取租户token信息成功: ${key}`); return cacheItem.data; } /** * 删除租户token * @param key 缓存键 * @returns 是否成功删除 */ removeTenantToken(key) { const cacheKey = `tenant_access_token:${key}`; const result = this.cache.delete(cacheKey); if (result) { this.saveTenantTokenCache(); Logger.debug(`租户token删除成功: ${key}`); } return result; } /** * 根据key获取租户的access_token值 * @param key 缓存键 * @returns app_access_token字符串,如果未找到或已过期则返回null */ getTenantToken(key) { const tokenInfo = this.getTenantTokenInfo(key); return tokenInfo ? tokenInfo.app_access_token : null; } /** * 缓存用户token信息 * @param key 缓存键 * @param tokenInfo 用户token信息 * @param customTtl 自定义TTL(秒),如果不提供则使用refresh_token的过期时间 * @returns 是否成功缓存 */ cacheUserToken(key, tokenInfo, customTtl) { try { const now = Date.now(); const cacheKey = `user_access_token:${key}`; // 计算过期时间 - 优先使用refresh_token的过期时间,确保可以刷新 let expiresAt; if (customTtl) { expiresAt = now + (customTtl * 1000); } else if (tokenInfo.refresh_token_expires_at) { // 使用refresh_token的过期时间,确保在refresh_token有效期内缓存不会被清除 expiresAt = tokenInfo.refresh_token_expires_at * 1000; // 转换为毫秒 Logger.debug(`使用refresh_token过期时间作为缓存过期时间: ${new Date(expiresAt).toISOString()}`); } else if (tokenInfo.expires_at) { // 如果没有refresh_token_expires_at信息,降级使用access_token的过期时间 expiresAt = tokenInfo.expires_at * 1000; Logger.warn(`没有refresh_token过期时间戳,使用access_token过期时间: ${new Date(expiresAt).toISOString()}`); } else { // 最后的降级方案:如果没有任何过期时间信息,设置默认的2小时过期 expiresAt = now + (2 * 60 * 60 * 1000); // 2小时 Logger.warn(`没有过期时间信息,使用默认2小时作为缓存过期时间`); } const cacheItem = { data: tokenInfo, timestamp: now, expiresAt: expiresAt }; this.cache.set(cacheKey, cacheItem); this.saveUserTokenCache(); Logger.debug(`用户token缓存成功: ${key}, 缓存过期时间: ${new Date(expiresAt).toISOString()}`); return true; } catch (error) { Logger.error(`缓存用户token失败: ${key}`, error); return false; } } /** * 缓存租户token信息 * @param key 缓存键 * @param tokenInfo 租户token信息 * @param customTtl 自定义TTL(秒),如果不提供则使用token本身的过期时间 * @returns 是否成功缓存 */ cacheTenantToken(key, tokenInfo, customTtl) { try { const now = Date.now(); const cacheKey = `tenant_access_token:${key}`; // 计算过期时间 let expiresAt; if (customTtl) { expiresAt = now + (customTtl * 1000); } else if (tokenInfo.expires_at) { expiresAt = tokenInfo.expires_at * 1000; // 转换为毫秒 } else { // 如果没有过期时间信息,设置默认的2小时过期 expiresAt = now + (2 * 60 * 60 * 1000); Logger.warn(`租户token没有过期时间信息,使用默认2小时`); } const cacheItem = { data: tokenInfo, timestamp: now, expiresAt: expiresAt }; this.cache.set(cacheKey, cacheItem); this.saveTenantTokenCache(); Logger.debug(`租户token缓存成功: ${key}, 过期时间: ${new Date(expiresAt).toISOString()}`); return true; } catch (error) { Logger.error(`缓存租户token失败: ${key}`, error); return false; } } /** * 检查用户token状态 * @param key 缓存键 * @returns token状态信息 */ checkUserTokenStatus(key) { const tokenInfo = this.getUserTokenInfo(key); if (!tokenInfo) { return { isValid: false, isExpired: true, canRefresh: false, shouldRefresh: false }; } const now = Math.floor(Date.now() / 1000); const isExpired = tokenInfo.expires_at ? tokenInfo.expires_at < now : false; const timeToExpiry = tokenInfo.expires_at ? Math.max(0, tokenInfo.expires_at - now) : 0; // 判断是否可以刷新 const canRefresh = !!(tokenInfo.refresh_token && tokenInfo.refresh_token_expires_at && tokenInfo.refresh_token_expires_at > now); // 判断是否应该提前刷新(提前5分钟) const shouldRefresh = timeToExpiry > 0 && timeToExpiry < 300 && canRefresh; return { isValid: !isExpired, isExpired, canRefresh, shouldRefresh }; } /** * 删除用户token * @param key 缓存键 * @returns 是否成功删除 */ removeUserToken(key) { const cacheKey = `user_access_token:${key}`; const result = this.cache.delete(cacheKey); if (result) { this.saveUserTokenCache(); Logger.debug(`用户token删除成功: ${key}`); } return result; } /** * 保存用户token缓存到文件 */ saveUserTokenCache() { const cacheData = {}; for (const [key, value] of this.cache.entries()) { if (key.startsWith('user_access_token:')) { cacheData[key] = value; } } try { fs.writeFileSync(this.userTokenCacheFile, JSON.stringify(cacheData, null, 2), 'utf-8'); Logger.debug('用户token缓存已保存到文件'); } catch (error) { Logger.warn('保存用户token缓存失败:', error); } } /** * 保存租户token缓存到文件 */ saveTenantTokenCache() { const cacheData = {}; for (const [key, value] of this.cache.entries()) { if (key.startsWith('tenant_access_token:')) { cacheData[key] = value; } } try { fs.writeFileSync(this.tenantTokenCacheFile, JSON.stringify(cacheData, null, 2), 'utf-8'); Logger.debug('租户token缓存已保存到文件'); } catch (error) { Logger.warn('保存租户token缓存失败:', error); } } /** * 清理过期缓存 * 对于用户token,只有在refresh_token过期时才清理 * 对于租户token,按缓存过期时间清理 * @returns 清理的数量 */ cleanExpiredTokens() { const now = Date.now(); const nowSeconds = Math.floor(now / 1000); let cleanedCount = 0; const keysToDelete = []; for (const [key, cacheItem] of this.cache.entries()) { let shouldDelete = false; if (key.startsWith('user_access_token:')) { // 用户token:检查refresh_token是否过期 const tokenInfo = cacheItem.data; if (tokenInfo.refresh_token && tokenInfo.refresh_token_expires_at) { // 有refresh_token,只有refresh_token过期才删除 shouldDelete = tokenInfo.refresh_token_expires_at < nowSeconds; if (shouldDelete) { Logger.debug(`清理用户token - refresh_token已过期: ${key}`); } } else { // 没有refresh_token,按缓存过期时间删除 shouldDelete = cacheItem.expiresAt <= now; if (shouldDelete) { Logger.debug(`清理用户token - 无refresh_token且缓存过期: ${key}`); } } } else { // 租户token或其他类型:按缓存过期时间删除 shouldDelete = cacheItem.expiresAt <= now; if (shouldDelete) { Logger.debug(`清理过期缓存: ${key}`); } } if (shouldDelete) { keysToDelete.push(key); } } // 批量删除 keysToDelete.forEach(key => { this.cache.delete(key); cleanedCount++; }); if (cleanedCount > 0) { // 分别保存用户和租户缓存 this.saveUserTokenCache(); this.saveTenantTokenCache(); Logger.info(`清理过期token,删除了 ${cleanedCount} 条记录`); } return cleanedCount; } /** * 启动缓存清理定时器 */ startCacheCleanupTimer() { // 每5分钟清理一次过期缓存 this.cleanupTimerId = setInterval(() => { this.cleanExpiredTokens(); }, 5 * 60 * 1000); // 不阻止进程在 stdio 模式下自然退出 this.cleanupTimerId.unref(); Logger.info('Token缓存清理定时器已启动,每5分钟执行一次'); } /** * 获取所有用户token的key列表(不包含前缀) * @returns 用户token的key数组 */ getAllUserTokenKeys() { const keys = []; for (const [key] of this.cache.entries()) { if (key.startsWith('user_access_token:')) { // 提取clientKey(去掉前缀) const clientKey = key.substring('user_access_token:'.length); keys.push(clientKey); } } Logger.debug(`获取到 ${keys.length} 个用户token keys`); return keys; } /** * 获取scope版本信息 * @param clientKey 客户端缓存键 * @returns scope版本信息,如果未找到则返回null */ getScopeVersionInfo(clientKey) { const cacheKey = `scope_version:${clientKey}`; const cacheItem = this.cache.get(cacheKey); if (!cacheItem) { Logger.debug(`Scope版本信息未找到: ${clientKey}`); return null; } Logger.debug(`获取Scope版本信息成功: ${clientKey}`); return cacheItem.data; } /** * 保存scope版本信息 * @param clientKey 客户端缓存键 * @param scopeVersionInfo scope版本信息 * @returns 是否成功保存 */ saveScopeVersionInfo(clientKey, scopeVersionInfo) { try { const cacheKey = `scope_version:${clientKey}`; const now = Date.now(); // scope版本信息永久有效,不设置过期时间 const cacheItem = { data: scopeVersionInfo, timestamp: now, expiresAt: Number.MAX_SAFE_INTEGER // 永久有效 }; this.cache.set(cacheKey, cacheItem); this.saveScopeVersionCache(); Logger.debug(`Scope版本信息保存成功: ${clientKey}, 版本: ${scopeVersionInfo.scopeVersion}`); return true; } catch (error) { Logger.error(`保存Scope版本信息失败: ${clientKey}`, error); return false; } } /** * 检查scope版本是否需要校验 * @param clientKey 客户端缓存键 * @param currentScopeVersion 当前scope版本号 * @returns 是否需要校验 */ shouldValidateScope(clientKey, currentScopeVersion) { const scopeVersionInfo = this.getScopeVersionInfo(clientKey); if (!scopeVersionInfo) { Logger.debug(`Scope版本信息不存在,需要校验: ${clientKey}`); return true; } // 如果版本号不同,需要重新校验 if (scopeVersionInfo.validatedVersion !== currentScopeVersion) { Logger.debug(`Scope版本号已更新,需要重新校验: ${clientKey}, 旧版本: ${scopeVersionInfo.validatedVersion}, 新版本: ${currentScopeVersion}`); return true; } Logger.debug(`Scope版本已校验过,无需重复校验: ${clientKey}, 版本: ${currentScopeVersion}`); return false; } /** * 加载scope版本缓存 */ loadScopeVersionCache() { if (fs.existsSync(this.scopeVersionCacheFile)) { try { const raw = fs.readFileSync(this.scopeVersionCacheFile, 'utf-8'); const cacheData = JSON.parse(raw); let loadedCount = 0; for (const key in cacheData) { if (key.startsWith('scope_version:')) { this.cache.set(key, cacheData[key]); loadedCount++; } } Logger.info(`已加载Scope版本缓存,共 ${loadedCount} 条记录`); } catch (error) { Logger.warn('加载Scope版本缓存失败:', error); } } else { Logger.info('Scope版本缓存文件不存在,将创建新的缓存'); } } /** * 保存scope版本缓存到文件 */ saveScopeVersionCache() { const cacheData = {}; for (const [key, value] of this.cache.entries()) { if (key.startsWith('scope_version:')) { cacheData[key] = value; } } try { fs.writeFileSync(this.scopeVersionCacheFile, JSON.stringify(cacheData, null, 2), 'utf-8'); Logger.debug('Scope版本缓存已保存到文件'); } catch (error) { Logger.warn('保存Scope版本缓存失败:', error); } } }