| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- /**
- * 飞书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<string, CacheItem>;
- private cacheTimeout: number;
- private refreshThreshold: number;
- private configManager: any;
- /**
- * 构造函数
- * 初始化缓存和配置参数
- * @param configManager - 配置管理器实例(必填)
- */
- constructor(configManager: FeishuConfig) {
- this.memoryCache = new Map<string, CacheItem>();
- this.cacheTimeout = 3600000; // 1小时
- this.refreshThreshold = 300000; // 5分钟
- this.configManager = configManager;
- }
- /**
- * 获取tenant_access_token(自动处理缓存和刷新)
- *
- * @async
- * @param appId - 应用ID
- * @returns Promise<string> tenant_access_token
- * @throws {Error} 获取失败时抛出错误
- */
- async getTenantAccessToken(appId: string): Promise<string> {
- 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<TokenInfo> token信息对象
- * @throws {Error} 获取失败时抛出错误
- */
- async fetchNewToken(appId: string): Promise<TokenInfo> {
- 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<void>
- */
- async persistToken(appId: string, tokenInfo: TokenInfo): Promise<void> {
- 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<TokenInfo | null>
- */
- async getFromDatabase(appId: string): Promise<TokenInfo | null> {
- 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<void>
- */
- async recordTokenEvent(appId: string, tokenInfo: TokenInfo, action: string): Promise<void> {
- 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<void>
- */
- async cleanupExpiredTokens(): Promise<void> {
- 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<string> 新的tenant_access_token
- * @throws {Error} 刷新失败时抛出错误
- */
- async forceRefreshToken(appId: string): Promise<string> {
- 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<TokenStatus>
- * @throws {Error} 获取失败时抛出错误
- */
- async getTokenStatus(appId: string): Promise<TokenStatus> {
- 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<void>
- */
- async initialize(): Promise<void> {
- await this.cleanupExpiredTokens();
- setInterval(() => {
- this.cleanupExpiredTokens().catch(console.error);
- }, 30 * 60 * 1000);
- console.log('飞书Token管理器初始化完成');
- }
- }
- /**
- * 默认导出Token管理器类
- */
- export default FeishuTokenManager;
|