/** * 飞书API客户端模块 * * 功能说明: * - 封装飞书开放平台的所有API调用 * - 支持tenant_access_token和user_access_token * - 提供用户信息、OAuth2认证等功能 * - 统一错误处理和日志记录 * * @module client * @author fmode * @date 2026 */ /** * Tenant Access Token 响应接口 */ export interface TenantAccessTokenResponse { tenantAccessToken: string; expire: number; } /** * App Access Token 响应接口 */ export interface AppAccessTokenResponse { appAccessToken: string; expire: number; } /** * User Access Token 响应接口 */ export interface UserAccessTokenResponse { accessToken: string; refreshToken: string; expiresIn: number; tokenType: string; refreshExpiresIn: number; } /** * 用户信息接口 */ export interface FeishuUserInfo { name: string; enName?: string; avatarUrl: string; avatarThumb?: string; avatarMiddle?: string; avatarBig?: string; openId: string; unionId: string; email?: string; enterpriseEmail?: string; userId: string; mobile?: string; tenantKey?: string; } /** * 登录用户信息响应接口 */ export interface LoginUserInfoResponse { accessToken: string; tokenType: string; expiresIn: number; refreshToken: string; refreshExpiresIn: number; scope: string; } /** * API 转发请求选项接口 */ export interface ForwardRequestOptions { path: string; method?: string; query?: Record; body?: Record; } /** * 飞书API客户端类 * 提供所有飞书API的封装方法 */ export class FeishuClient { private baseUrl: string; private tokenCache: Map; /** * 构造函数 * 初始化飞书API基础URL */ constructor() { this.baseUrl = 'https://open.feishu.cn/open-apis'; this.tokenCache = new Map(); } /** * 获取tenant_access_token(企业自建应用) * * @static * @async * @param appId - 应用ID * @param appSecret - 应用密钥 * @returns Promise * @throws {Error} 获取失败时抛出错误 */ static async getTenantAccessToken(appId: string, appSecret: string): Promise { try { const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({ app_id: appId, app_secret: appSecret }) }); const result = await response.json(); if (result.code !== 0) { throw new Error(`获取tenant_access_token失败: ${result.msg}, code: ${result.code}`); } return { tenantAccessToken: result.tenant_access_token, expire: result.expire }; } catch (error) { console.error('获取tenant_access_token异常:', error); throw error; } } /** * 获取app_access_token(应用商店应用) * * @static * @async * @param appId - 应用ID * @param appSecret - 应用密钥 * @returns Promise * @throws {Error} 获取失败时抛出错误 */ static async getAppAccessToken(appId: string, appSecret: string): Promise { try { const url = 'https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({ app_id: appId, app_secret: appSecret }) }); const result = await response.json(); if (result.code !== 0) { throw new Error(`获取app_access_token失败: ${result.msg}, code: ${result.code}`); } return { appAccessToken: result.app_access_token, expire: result.expire }; } catch (error) { console.error('获取app_access_token异常:', error); throw error; } } /** * OAuth2方式获取用户访问令牌 * * @static * @async * @param appId - 应用ID * @param appSecret - 应用密钥 * @param code - 授权码 * @param grantType - 授权类型 * @returns Promise * @throws {Error} 获取失败时抛出错误 */ static async getUserAccessToken( appId: string, appSecret: string, code: string, grantType: string = 'authorization_code' ): Promise { try { const url = 'https://open.feishu.cn/open-apis/authen/v2/oauth/token'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({ grant_type: grantType, client_id: appId, client_secret: appSecret, code: code }) }); const result = await response.json(); if (result.code !== 0) { throw new Error(`获取user_access_token失败: ${result.error_description || result.msg}, code: ${result.code}`); } return { accessToken: result.access_token, refreshToken: result.refresh_token, expiresIn: result.expires_in, tokenType: result.token_type, refreshExpiresIn: result.refresh_token_expires_in }; } catch (error) { console.error('获取user_access_token异常:', error); throw error; } } /** * 刷新用户访问令牌 * * @static * @async * @param appId - 应用ID * @param appSecret - 应用密钥 * @param refreshToken - 刷新令牌 * @returns Promise * @throws {Error} 刷新失败时抛出错误 */ static async refreshUserAccessToken( appId: string, appSecret: string, refreshToken: string ): Promise { try { const url = 'https://open.feishu.cn/open-apis/authen/v2/oauth/token'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({ grant_type: 'refresh_token', client_id: appId, client_secret: appSecret, refresh_token: refreshToken }) }); const result = await response.json(); if (result.code !== 0) { throw new Error(`刷新user_access_token失败: ${result.error_description || result.msg}, code: ${result.code}`); } return { accessToken: result.access_token, refreshToken: result.refresh_token, expiresIn: result.expires_in, tokenType: result.token_type, refreshExpiresIn: result.refresh_token_expires_in }; } catch (error) { console.error('刷新user_access_token异常:', error); throw error; } } /** * 获取用户信息(使用user_access_token) * * @static * @async * @param userAccessToken - 用户访问令牌 * @returns Promise * @throws {Error} 获取失败时抛出错误 */ static async getUserInfo(userAccessToken: string): Promise { try { const url = 'https://open.feishu.cn/open-apis/authen/v1/user_info'; const response = await fetch(url, { method: 'GET', headers: { 'Authorization': `Bearer ${userAccessToken}`, 'Content-Type': 'application/json; charset=utf-8' } }); const result = await response.json(); if (result.code !== 0) { throw new Error(`获取用户信息失败: ${result.msg}, code: ${result.code}`); } return { name: result.data.name, enName: result.data.en_name, avatarUrl: result.data.avatar_url, avatarThumb: result.data.avatar_thumb, avatarMiddle: result.data.avatar_middle, avatarBig: result.data.avatar_big, openId: result.data.open_id, unionId: result.data.union_id, email: result.data.email, enterpriseEmail: result.data.enterprise_email, userId: result.data.user_id, mobile: result.data.mobile, tenantKey: result.data.tenant_key }; } catch (error) { console.error('获取用户信息异常:', error); throw error; } } /** * 获取登录用户身份(网页免登) * * @static * @async * @param tenantAccessToken - 租户访问令牌 * @param code - 免登授权码 * @returns Promise * @throws {Error} 获取失败时抛出错误 */ static async getLoginUserInfo(tenantAccessToken: string, code: string): Promise { try { const url = 'https://open.feishu.cn/open-apis/authen/v1/access_token'; const response = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${tenantAccessToken}`, 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({ grant_type: 'authorization_code', code: code }) }); const result = await response.json(); if (result.code !== 0) { throw new Error(`获取登录用户身份失败: ${result.msg}, code: ${result.code}`); } return { accessToken: result.data.access_token, tokenType: result.data.token_type, expiresIn: result.data.expires_in, refreshToken: result.data.refresh_token, refreshExpiresIn: result.data.refresh_expires_in, scope: result.data.scope }; } catch (error) { console.error('获取登录用户身份异常:', error); throw error; } } /** * 转发请求到飞书API * * @static * @async * @param accessToken - 访问令牌 * @param options - 请求选项 * @returns Promise * @throws {Error} 请求失败时抛出错误 */ static async forwardRequest(accessToken: string, options: ForwardRequestOptions): Promise { try { const { path, method = 'GET', query, body } = options; let url = `https://open.feishu.cn/open-apis${path}`; if (query && typeof query === 'object') { const queryParams = new URLSearchParams(); Object.keys(query).forEach(key => { if (query[key] !== undefined && query[key] !== null) { queryParams.append(key, query[key]); } }); const queryString = queryParams.toString(); if (queryString) { url += `?${queryString}`; } } const requestOptions: RequestInit = { method: method.toUpperCase(), headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json; charset=utf-8' } }; if (body && typeof body === 'object' && method.toUpperCase() !== 'GET') { requestOptions.body = JSON.stringify(body); } console.log('转发飞书API请求:', { url, method, body }); const response = await fetch(url, requestOptions); const result = await response.json(); console.log('飞书API响应:', result); return result; } catch (error) { console.error('转发请求异常:', error); throw error; } } } /** * 默认导出客户端类 */ export default FeishuClient;