/** * 飞书Token管理器模块 * * 功能说明: * - 负责tenant_access_token的获取、缓存、刷新和持久化 * - 支持多层缓存机制(内存+数据库) * - 自动过期检测和刷新 * - 提供Token状态查询接口 * * @module token-manager * @author fmode * @date 2026 */ import type { FeishuAppConfig, FeishuConfig } from './config.ts'; import { FeishuClient } from './client.ts'; import type { TenantAccessTokenResponse } from './client.ts'; /** * Token信息接口 */ export interface TokenInfo { tenantAccessToken: string; expire: number; createdAt: Date; expiredAt: Date; } /** * Token状态接口 */ export interface TokenStatus { appId: string; hasMemoryCache: boolean; hasDatabaseCache: boolean; isExpired: boolean | null; expiredAt?: Date; expire?: number; lastUpdated: number | null; } /** * 缓存项接口 */ interface CacheItem { data: TokenInfo; timestamp: number; } /** * 飞书Token管理器类 * 负责管理所有应用的tenant_access_token */ export class FeishuTokenManager { private memoryCache: Map; private cacheTimeout: number; private refreshThreshold: number; private configManager: any; /** * 构造函数 * 初始化缓存和配置参数 * @param configManager - 配置管理器实例(必填) */ constructor(configManager: FeishuConfig) { this.memoryCache = new Map(); this.cacheTimeout = 3600000; // 1小时 this.refreshThreshold = 300000; // 5分钟 this.configManager = configManager; } /** * 获取tenant_access_token(自动处理缓存和刷新) * * @async * @param appId - 应用ID * @returns Promise tenant_access_token * @throws {Error} 获取失败时抛出错误 */ async getTenantAccessToken(appId: string): Promise { try { // 1. 检查内存缓存 const cachedToken = this.getFromMemoryCache(appId); if (cachedToken && !this.isTokenExpired(cachedToken)) { console.log(`从内存缓存获取tenant_access_token: ${appId}`); return cachedToken.tenantAccessToken; } // 2. 检查数据库缓存 const dbToken = await this.getFromDatabase(appId); if (dbToken && !this.isTokenExpired(dbToken)) { console.log(`从数据库获取tenant_access_token: ${appId}`); this.updateMemoryCache(appId, dbToken); return dbToken.tenantAccessToken; } // 3. 缓存过期或不存在,获取新token console.log(`获取新的tenant_access_token: ${appId}`); const newToken = await this.fetchNewToken(appId); // 4. 持久化token到数据库 await this.persistToken(appId, newToken); // 5. 更新内存缓存 this.updateMemoryCache(appId, newToken); // 6. 记录token获取事件到OpenEvent await this.recordTokenEvent(appId, newToken, 'token_refresh'); return newToken.tenantAccessToken; } catch (error) { console.error(`获取tenant_access_token失败: ${appId}`, error); throw error; } } /** * 从飞书API获取新的tenant_access_token * * @async * @param appId - 应用ID * @returns Promise token信息对象 * @throws {Error} 获取失败时抛出错误 */ async fetchNewToken(appId: string): Promise { const config = this.configManager.getAppConfig(appId); console.log('获取Token配置:', { appId, hasAppSecret: !!config.appSecret }); const result = await FeishuClient.getTenantAccessToken(config.appId, config.appSecret); const tokenInfo: TokenInfo = { tenantAccessToken: result.tenantAccessToken, expire: result.expire, createdAt: new Date(), expiredAt: new Date(Date.now() + (result.expire - 300) * 1000) }; console.log(`成功获取tenant_access_token: ${appId}, 有效期: ${result.expire}秒`); return tokenInfo; } /** * 持久化token到数据库 * * @async * @param appId - 应用ID * @param tokenInfo - token信息对象 * @returns Promise */ async persistToken(appId: string, tokenInfo: TokenInfo): Promise { try { const Parse = (global as any).Parse; if (!Parse) { console.warn('Parse不可用,跳过token持久化'); return; } const OpenEvent = Parse.Object.extend('OpenEvent'); const query = new Parse.Query(OpenEvent); query.equalTo('provider', 'feishu'); query.equalTo('appId', appId); query.equalTo('eventType', 'token_auth'); query.descending('createdAt'); query.limit(1); const existingRecord = await query.first(); if (existingRecord) { existingRecord.set('accessToken', tokenInfo.tenantAccessToken); existingRecord.set('expiredAt', tokenInfo.expiredAt); existingRecord.set('expire', tokenInfo.expire); existingRecord.set('updatedAt', new Date()); existingRecord.set('status', 'active'); existingRecord.set('errorMessage', null); await existingRecord.save(); console.log(`更新token记录到OpenEvent表: ${appId}`); } else { const tokenRecord = new OpenEvent(); tokenRecord.set('provider', 'feishu'); tokenRecord.set('eventType', 'token_auth'); tokenRecord.set('appId', appId); tokenRecord.set('accessToken', tokenInfo.tenantAccessToken); tokenRecord.set('expiredAt', tokenInfo.expiredAt); tokenRecord.set('expire', tokenInfo.expire); tokenRecord.set('status', 'active'); tokenRecord.set('createdAt', new Date()); tokenRecord.set('updatedAt', new Date()); await tokenRecord.save(); console.log(`创建新token记录到OpenEvent表: ${appId}`); } } catch (error) { console.error('持久化token失败:', error); } } /** * 从数据库获取token * * @async * @param appId - 应用ID * @returns Promise */ async getFromDatabase(appId: string): Promise { try { const Parse = (global as any).Parse; if (!Parse) { return null; } const OpenEvent = Parse.Object.extend('OpenEvent'); const query = new Parse.Query(OpenEvent); query.equalTo('provider', 'feishu'); query.equalTo('appId', appId); query.equalTo('eventType', 'token_auth'); query.equalTo('status', 'active'); query.greaterThan('expiredAt', new Date()); query.descending('createdAt'); query.limit(1); const record = await query.first(); if (record) { return { tenantAccessToken: record.get('accessToken'), expire: record.get('expire'), expiredAt: record.get('expiredAt'), createdAt: record.get('createdAt') }; } return null; } catch (error) { console.error('从数据库获取token失败:', error); return null; } } /** * 从内存缓存获取token * * @param appId - 应用ID * @returns TokenInfo | null */ getFromMemoryCache(appId: string): TokenInfo | null { const cached = this.memoryCache.get(appId); if (cached && (Date.now() - cached.timestamp < this.cacheTimeout)) { return cached.data; } return null; } /** * 更新内存缓存 * * @param appId - 应用ID * @param tokenInfo - token信息对象 */ updateMemoryCache(appId: string, tokenInfo: TokenInfo): void { this.memoryCache.set(appId, { data: tokenInfo, timestamp: Date.now() }); } /** * 检查token是否过期 * * @param tokenInfo - token信息对象 * @returns boolean */ isTokenExpired(tokenInfo: TokenInfo): boolean { if (!tokenInfo || !tokenInfo.expiredAt) { return true; } return new Date() >= new Date(tokenInfo.expiredAt.getTime() - this.refreshThreshold); } /** * 记录token相关事件到OpenEvent * * @async * @param appId - 应用ID * @param tokenInfo - token信息 * @param action - 事件类型 * @returns Promise */ async recordTokenEvent(appId: string, tokenInfo: TokenInfo, action: string): Promise { try { const Parse = (global as any).Parse; if (!Parse) { return; } const OpenEvent = Parse.Object.extend('OpenEvent'); const eventRecord = new OpenEvent(); eventRecord.set('provider', 'feishu'); eventRecord.set('eventType', 'token_event'); eventRecord.set('appId', appId); eventRecord.set('action', action); eventRecord.set('status', 'success'); eventRecord.set('expire', tokenInfo.expire); eventRecord.set('expiredAt', tokenInfo.expiredAt); eventRecord.set('createdAt', new Date()); eventRecord.set('updatedAt', new Date()); await eventRecord.save(); console.log(`记录token事件到OpenEvent: ${appId}, action: ${action}`); } catch (error) { console.error('记录token事件失败:', error); } } /** * 清理过期的token记录 * * @async * @returns Promise */ async cleanupExpiredTokens(): Promise { try { const Parse = (global as any).Parse; if (!Parse) { return; } const OpenEvent = Parse.Object.extend('OpenEvent'); const query = new Parse.Query(OpenEvent); query.equalTo('provider', 'feishu'); query.equalTo('eventType', 'token_auth'); query.lessThan('expiredAt', new Date()); const expiredRecords = await query.find(); for (const record of expiredRecords) { record.set('status', 'expired'); record.set('updatedAt', new Date()); await record.save(); } console.log(`清理了 ${expiredRecords.length} 个过期的token记录`); } catch (error) { console.error('清理过期token失败:', error); } } /** * 强制刷新指定应用的token * * @async * @param appId - 应用ID * @returns Promise 新的tenant_access_token * @throws {Error} 刷新失败时抛出错误 */ async forceRefreshToken(appId: string): Promise { try { console.log(`强制刷新token: ${appId}`); this.memoryCache.delete(appId); const newToken = await this.fetchNewToken(appId); await this.persistToken(appId, newToken); this.updateMemoryCache(appId, newToken); await this.recordTokenEvent(appId, newToken, 'token_force_refresh'); return newToken.tenantAccessToken; } catch (error) { console.error(`强制刷新token失败: ${appId}`, error); throw error; } } /** * 获取token状态信息 * * @async * @param appId - 应用ID * @returns Promise * @throws {Error} 获取失败时抛出错误 */ async getTokenStatus(appId: string): Promise { try { const cachedToken = this.getFromMemoryCache(appId); const dbToken = await this.getFromDatabase(appId); return { appId, hasMemoryCache: !!cachedToken, hasDatabaseCache: !!dbToken, isExpired: cachedToken ? this.isTokenExpired(cachedToken) : null, expiredAt: cachedToken?.expiredAt || dbToken?.expiredAt, expire: cachedToken?.expire || dbToken?.expire, lastUpdated: cachedToken ? Date.now() : null }; } catch (error) { console.error(`获取token状态失败: ${appId}`, error); throw error; } } /** * 初始化token管理器 * * @async * @returns Promise */ async initialize(): Promise { await this.cleanupExpiredTokens(); setInterval(() => { this.cleanupExpiredTokens().catch(console.error); }, 30 * 60 * 1000); console.log('飞书Token管理器初始化完成'); } } /** * 默认导出Token管理器类 */ export default FeishuTokenManager;