/** * 飞书 Token 管理模块 * 负责获取、保存、刷新 tenant_access_token */ import { getFeishuConfig, validateConfig } from './config'; // 飞书 API 端点 const FEISHU_API_BASE = 'https://open.feishu.cn/open-apis'; const TOKEN_API = `${FEISHU_API_BASE}/auth/v3/tenant_access_token/internal`; // Token 信息接口 interface TokenInfo { accessToken: string; expireTime: number; // 过期时间戳(毫秒) } // 飞书 Token API 响应 interface FeishuTokenResponse { code: number; msg: string; tenant_access_token?: string; expire?: number; // 有效期(秒) } /** * Token 管理器类 */ export class TokenManager { private Parse: any; private static instance: TokenManager; constructor() { // 使用全局的 Parse 实例 this.Parse = (globalThis as any).Parse; if (!this.Parse) { throw new Error('Parse is not initialized'); } } /** * 获取单例实例 */ static getInstance(): TokenManager { if (!TokenManager.instance) { TokenManager.instance = new TokenManager(); } return TokenManager.instance; } /** * 从飞书 API 获取新的 access token */ private async fetchTokenFromFeishu(): Promise { const config = getFeishuConfig(); if (!validateConfig(config)) { throw new Error('飞书配置无效:请设置 FEISHU_APP_ID 和 FEISHU_APP_SECRET 环境变量'); } const response = await fetch(TOKEN_API, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ app_id: config.app_id, app_secret: config.app_secret }) }); if (!response.ok) { throw new Error(`获取飞书 token 失败: ${response.status} ${response.statusText}`); } const data: FeishuTokenResponse = await response.json(); if (data.code !== 0) { throw new Error(`获取飞书 token 失败: ${data.msg}`); } if (!data.tenant_access_token || !data.expire) { throw new Error('飞书 API 返回数据格式错误'); } // 计算过期时间(提前 5 分钟过期,避免边界情况) const expireTime = Date.now() + (data.expire - 300) * 1000; return { accessToken: data.tenant_access_token, expireTime }; } /** * 从数据库获取 Store 配置 */ private async getStoreConfig(): Promise { try { const query = new this.Parse.Query('Store'); query.equalTo('objectId', 'config'); const store = await query.first({ useMasterKey: true }); return store; } catch (error) { console.error('获取 Store 配置失败:', error); return null; } } /** * 从数据库读取 token */ private async getTokenFromDatabase(): Promise { try { const store = await this.getStoreConfig(); if (!store) { return null; } const config = store.get('config'); if (!config || !config.feishu || !config.feishu.accessToken || !config.feishu.expireTime) { return null; } return { accessToken: config.feishu.accessToken, expireTime: config.feishu.expireTime }; } catch (error) { console.error('从数据库读取 token 失败:', error); return null; } } /** * 保存 token 到数据库 */ private async saveTokenToDatabase(tokenInfo: TokenInfo): Promise { try { let store = await this.getStoreConfig(); if (!store) { // 创建新的 Store 记录 const StoreClass = this.Parse.Object.extend('Store'); store = new StoreClass(); store.id = 'config'; } // 获取现有的 config let config = store.get('config') || {}; // 更新 feishu 配置 config.feishu = { accessToken: tokenInfo.accessToken, expireTime: tokenInfo.expireTime }; // 保存到数据库 store.set('config', config); await store.save(null, { useMasterKey: true }); console.log('Token 已保存到数据库'); } catch (error) { console.error('保存 token 到数据库失败:', error); throw error; } } /** * 检查 token 是否过期 */ private isTokenExpired(tokenInfo: TokenInfo): boolean { return Date.now() >= tokenInfo.expireTime; } /** * 获取有效的 access token * 优先从数据库读取,如果不存在或已过期则重新获取 */ async getAccessToken(): Promise { try { // 1. 尝试从数据库读取 let tokenInfo = await this.getTokenFromDatabase(); // 2. 检查是否存在且未过期 if (tokenInfo && !this.isTokenExpired(tokenInfo)) { console.log('使用数据库中的 token'); return tokenInfo.accessToken; } // 3. 从飞书 API 获取新 token console.log('从飞书 API 获取新 token'); tokenInfo = await this.fetchTokenFromFeishu(); // 4. 保存到数据库 await this.saveTokenToDatabase(tokenInfo); return tokenInfo.accessToken; } catch (error) { console.error('获取 access token 失败:', error); throw error; } } /** * 强制刷新 token */ async refreshToken(): Promise { console.log('强制刷新 token'); const tokenInfo = await this.fetchTokenFromFeishu(); await this.saveTokenToDatabase(tokenInfo); return tokenInfo.accessToken; } } /** * 获取 Token 管理器实例 */ export function getTokenManager(): TokenManager { return TokenManager.getInstance(); }