| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- /**
- * 飞书 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<TokenInfo> {
- 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<any> {
- 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<TokenInfo | null> {
- 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<void> {
- 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<string> {
- 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<string> {
- console.log('强制刷新 token');
- const tokenInfo = await this.fetchTokenFromFeishu();
- await this.saveTokenToDatabase(tokenInfo);
- return tokenInfo.accessToken;
- }
- }
- /**
- * 获取 Token 管理器实例
- */
- export function getTokenManager(): TokenManager {
- return TokenManager.getInstance();
- }
|