token-manager.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. /**
  2. * 飞书Token管理器模块
  3. *
  4. * 功能说明:
  5. * - 负责tenant_access_token的获取、缓存、刷新和持久化
  6. * - 支持多层缓存机制(内存+数据库)
  7. * - 自动过期检测和刷新
  8. * - 提供Token状态查询接口
  9. *
  10. * @module token-manager
  11. * @author fmode
  12. * @date 2026
  13. */
  14. import type { FeishuAppConfig, FeishuConfig } from './config.ts';
  15. import { FeishuClient } from './client.ts';
  16. import type { TenantAccessTokenResponse } from './client.ts';
  17. /**
  18. * Token信息接口
  19. */
  20. export interface TokenInfo {
  21. tenantAccessToken: string;
  22. expire: number;
  23. createdAt: Date;
  24. expiredAt: Date;
  25. }
  26. /**
  27. * Token状态接口
  28. */
  29. export interface TokenStatus {
  30. appId: string;
  31. hasMemoryCache: boolean;
  32. hasDatabaseCache: boolean;
  33. isExpired: boolean | null;
  34. expiredAt?: Date;
  35. expire?: number;
  36. lastUpdated: number | null;
  37. }
  38. /**
  39. * 缓存项接口
  40. */
  41. interface CacheItem {
  42. data: TokenInfo;
  43. timestamp: number;
  44. }
  45. /**
  46. * 飞书Token管理器类
  47. * 负责管理所有应用的tenant_access_token
  48. */
  49. export class FeishuTokenManager {
  50. private memoryCache: Map<string, CacheItem>;
  51. private cacheTimeout: number;
  52. private refreshThreshold: number;
  53. private configManager: any;
  54. /**
  55. * 构造函数
  56. * 初始化缓存和配置参数
  57. * @param configManager - 配置管理器实例(必填)
  58. */
  59. constructor(configManager: FeishuConfig) {
  60. this.memoryCache = new Map<string, CacheItem>();
  61. this.cacheTimeout = 3600000; // 1小时
  62. this.refreshThreshold = 300000; // 5分钟
  63. this.configManager = configManager;
  64. }
  65. /**
  66. * 获取tenant_access_token(自动处理缓存和刷新)
  67. *
  68. * @async
  69. * @param appId - 应用ID
  70. * @returns Promise<string> tenant_access_token
  71. * @throws {Error} 获取失败时抛出错误
  72. */
  73. async getTenantAccessToken(appId: string): Promise<string> {
  74. try {
  75. // 1. 检查内存缓存
  76. const cachedToken = this.getFromMemoryCache(appId);
  77. if (cachedToken && !this.isTokenExpired(cachedToken)) {
  78. console.log(`从内存缓存获取tenant_access_token: ${appId}`);
  79. return cachedToken.tenantAccessToken;
  80. }
  81. // 2. 检查数据库缓存
  82. const dbToken = await this.getFromDatabase(appId);
  83. if (dbToken && !this.isTokenExpired(dbToken)) {
  84. console.log(`从数据库获取tenant_access_token: ${appId}`);
  85. this.updateMemoryCache(appId, dbToken);
  86. return dbToken.tenantAccessToken;
  87. }
  88. // 3. 缓存过期或不存在,获取新token
  89. console.log(`获取新的tenant_access_token: ${appId}`);
  90. const newToken = await this.fetchNewToken(appId);
  91. // 4. 持久化token到数据库
  92. await this.persistToken(appId, newToken);
  93. // 5. 更新内存缓存
  94. this.updateMemoryCache(appId, newToken);
  95. // 6. 记录token获取事件到OpenEvent
  96. await this.recordTokenEvent(appId, newToken, 'token_refresh');
  97. return newToken.tenantAccessToken;
  98. } catch (error) {
  99. console.error(`获取tenant_access_token失败: ${appId}`, error);
  100. throw error;
  101. }
  102. }
  103. /**
  104. * 从飞书API获取新的tenant_access_token
  105. *
  106. * @async
  107. * @param appId - 应用ID
  108. * @returns Promise<TokenInfo> token信息对象
  109. * @throws {Error} 获取失败时抛出错误
  110. */
  111. async fetchNewToken(appId: string): Promise<TokenInfo> {
  112. const config = this.configManager.getAppConfig(appId);
  113. console.log('获取Token配置:', {
  114. appId,
  115. hasAppSecret: !!config.appSecret
  116. });
  117. const result = await FeishuClient.getTenantAccessToken(config.appId, config.appSecret);
  118. const tokenInfo: TokenInfo = {
  119. tenantAccessToken: result.tenantAccessToken,
  120. expire: result.expire,
  121. createdAt: new Date(),
  122. expiredAt: new Date(Date.now() + (result.expire - 300) * 1000)
  123. };
  124. console.log(`成功获取tenant_access_token: ${appId}, 有效期: ${result.expire}秒`);
  125. return tokenInfo;
  126. }
  127. /**
  128. * 持久化token到数据库
  129. *
  130. * @async
  131. * @param appId - 应用ID
  132. * @param tokenInfo - token信息对象
  133. * @returns Promise<void>
  134. */
  135. async persistToken(appId: string, tokenInfo: TokenInfo): Promise<void> {
  136. try {
  137. const Parse = (global as any).Parse;
  138. if (!Parse) {
  139. console.warn('Parse不可用,跳过token持久化');
  140. return;
  141. }
  142. const OpenEvent = Parse.Object.extend('OpenEvent');
  143. const query = new Parse.Query(OpenEvent);
  144. query.equalTo('provider', 'feishu');
  145. query.equalTo('appId', appId);
  146. query.equalTo('eventType', 'token_auth');
  147. query.descending('createdAt');
  148. query.limit(1);
  149. const existingRecord = await query.first();
  150. if (existingRecord) {
  151. existingRecord.set('accessToken', tokenInfo.tenantAccessToken);
  152. existingRecord.set('expiredAt', tokenInfo.expiredAt);
  153. existingRecord.set('expire', tokenInfo.expire);
  154. existingRecord.set('updatedAt', new Date());
  155. existingRecord.set('status', 'active');
  156. existingRecord.set('errorMessage', null);
  157. await existingRecord.save();
  158. console.log(`更新token记录到OpenEvent表: ${appId}`);
  159. } else {
  160. const tokenRecord = new OpenEvent();
  161. tokenRecord.set('provider', 'feishu');
  162. tokenRecord.set('eventType', 'token_auth');
  163. tokenRecord.set('appId', appId);
  164. tokenRecord.set('accessToken', tokenInfo.tenantAccessToken);
  165. tokenRecord.set('expiredAt', tokenInfo.expiredAt);
  166. tokenRecord.set('expire', tokenInfo.expire);
  167. tokenRecord.set('status', 'active');
  168. tokenRecord.set('createdAt', new Date());
  169. tokenRecord.set('updatedAt', new Date());
  170. await tokenRecord.save();
  171. console.log(`创建新token记录到OpenEvent表: ${appId}`);
  172. }
  173. } catch (error) {
  174. console.error('持久化token失败:', error);
  175. }
  176. }
  177. /**
  178. * 从数据库获取token
  179. *
  180. * @async
  181. * @param appId - 应用ID
  182. * @returns Promise<TokenInfo | null>
  183. */
  184. async getFromDatabase(appId: string): Promise<TokenInfo | null> {
  185. try {
  186. const Parse = (global as any).Parse;
  187. if (!Parse) {
  188. return null;
  189. }
  190. const OpenEvent = Parse.Object.extend('OpenEvent');
  191. const query = new Parse.Query(OpenEvent);
  192. query.equalTo('provider', 'feishu');
  193. query.equalTo('appId', appId);
  194. query.equalTo('eventType', 'token_auth');
  195. query.equalTo('status', 'active');
  196. query.greaterThan('expiredAt', new Date());
  197. query.descending('createdAt');
  198. query.limit(1);
  199. const record = await query.first();
  200. if (record) {
  201. return {
  202. tenantAccessToken: record.get('accessToken'),
  203. expire: record.get('expire'),
  204. expiredAt: record.get('expiredAt'),
  205. createdAt: record.get('createdAt')
  206. };
  207. }
  208. return null;
  209. } catch (error) {
  210. console.error('从数据库获取token失败:', error);
  211. return null;
  212. }
  213. }
  214. /**
  215. * 从内存缓存获取token
  216. *
  217. * @param appId - 应用ID
  218. * @returns TokenInfo | null
  219. */
  220. getFromMemoryCache(appId: string): TokenInfo | null {
  221. const cached = this.memoryCache.get(appId);
  222. if (cached && (Date.now() - cached.timestamp < this.cacheTimeout)) {
  223. return cached.data;
  224. }
  225. return null;
  226. }
  227. /**
  228. * 更新内存缓存
  229. *
  230. * @param appId - 应用ID
  231. * @param tokenInfo - token信息对象
  232. */
  233. updateMemoryCache(appId: string, tokenInfo: TokenInfo): void {
  234. this.memoryCache.set(appId, {
  235. data: tokenInfo,
  236. timestamp: Date.now()
  237. });
  238. }
  239. /**
  240. * 检查token是否过期
  241. *
  242. * @param tokenInfo - token信息对象
  243. * @returns boolean
  244. */
  245. isTokenExpired(tokenInfo: TokenInfo): boolean {
  246. if (!tokenInfo || !tokenInfo.expiredAt) {
  247. return true;
  248. }
  249. return new Date() >= new Date(tokenInfo.expiredAt.getTime() - this.refreshThreshold);
  250. }
  251. /**
  252. * 记录token相关事件到OpenEvent
  253. *
  254. * @async
  255. * @param appId - 应用ID
  256. * @param tokenInfo - token信息
  257. * @param action - 事件类型
  258. * @returns Promise<void>
  259. */
  260. async recordTokenEvent(appId: string, tokenInfo: TokenInfo, action: string): Promise<void> {
  261. try {
  262. const Parse = (global as any).Parse;
  263. if (!Parse) {
  264. return;
  265. }
  266. const OpenEvent = Parse.Object.extend('OpenEvent');
  267. const eventRecord = new OpenEvent();
  268. eventRecord.set('provider', 'feishu');
  269. eventRecord.set('eventType', 'token_event');
  270. eventRecord.set('appId', appId);
  271. eventRecord.set('action', action);
  272. eventRecord.set('status', 'success');
  273. eventRecord.set('expire', tokenInfo.expire);
  274. eventRecord.set('expiredAt', tokenInfo.expiredAt);
  275. eventRecord.set('createdAt', new Date());
  276. eventRecord.set('updatedAt', new Date());
  277. await eventRecord.save();
  278. console.log(`记录token事件到OpenEvent: ${appId}, action: ${action}`);
  279. } catch (error) {
  280. console.error('记录token事件失败:', error);
  281. }
  282. }
  283. /**
  284. * 清理过期的token记录
  285. *
  286. * @async
  287. * @returns Promise<void>
  288. */
  289. async cleanupExpiredTokens(): Promise<void> {
  290. try {
  291. const Parse = (global as any).Parse;
  292. if (!Parse) {
  293. return;
  294. }
  295. const OpenEvent = Parse.Object.extend('OpenEvent');
  296. const query = new Parse.Query(OpenEvent);
  297. query.equalTo('provider', 'feishu');
  298. query.equalTo('eventType', 'token_auth');
  299. query.lessThan('expiredAt', new Date());
  300. const expiredRecords = await query.find();
  301. for (const record of expiredRecords) {
  302. record.set('status', 'expired');
  303. record.set('updatedAt', new Date());
  304. await record.save();
  305. }
  306. console.log(`清理了 ${expiredRecords.length} 个过期的token记录`);
  307. } catch (error) {
  308. console.error('清理过期token失败:', error);
  309. }
  310. }
  311. /**
  312. * 强制刷新指定应用的token
  313. *
  314. * @async
  315. * @param appId - 应用ID
  316. * @returns Promise<string> 新的tenant_access_token
  317. * @throws {Error} 刷新失败时抛出错误
  318. */
  319. async forceRefreshToken(appId: string): Promise<string> {
  320. try {
  321. console.log(`强制刷新token: ${appId}`);
  322. this.memoryCache.delete(appId);
  323. const newToken = await this.fetchNewToken(appId);
  324. await this.persistToken(appId, newToken);
  325. this.updateMemoryCache(appId, newToken);
  326. await this.recordTokenEvent(appId, newToken, 'token_force_refresh');
  327. return newToken.tenantAccessToken;
  328. } catch (error) {
  329. console.error(`强制刷新token失败: ${appId}`, error);
  330. throw error;
  331. }
  332. }
  333. /**
  334. * 获取token状态信息
  335. *
  336. * @async
  337. * @param appId - 应用ID
  338. * @returns Promise<TokenStatus>
  339. * @throws {Error} 获取失败时抛出错误
  340. */
  341. async getTokenStatus(appId: string): Promise<TokenStatus> {
  342. try {
  343. const cachedToken = this.getFromMemoryCache(appId);
  344. const dbToken = await this.getFromDatabase(appId);
  345. return {
  346. appId,
  347. hasMemoryCache: !!cachedToken,
  348. hasDatabaseCache: !!dbToken,
  349. isExpired: cachedToken ? this.isTokenExpired(cachedToken) : null,
  350. expiredAt: cachedToken?.expiredAt || dbToken?.expiredAt,
  351. expire: cachedToken?.expire || dbToken?.expire,
  352. lastUpdated: cachedToken ? Date.now() : null
  353. };
  354. } catch (error) {
  355. console.error(`获取token状态失败: ${appId}`, error);
  356. throw error;
  357. }
  358. }
  359. /**
  360. * 初始化token管理器
  361. *
  362. * @async
  363. * @returns Promise<void>
  364. */
  365. async initialize(): Promise<void> {
  366. await this.cleanupExpiredTokens();
  367. setInterval(() => {
  368. this.cleanupExpiredTokens().catch(console.error);
  369. }, 30 * 60 * 1000);
  370. console.log('飞书Token管理器初始化完成');
  371. }
  372. }
  373. /**
  374. * 默认导出Token管理器类
  375. */
  376. export default FeishuTokenManager;