client.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. /**
  2. * 飞书API客户端模块
  3. *
  4. * 功能说明:
  5. * - 封装飞书开放平台的所有API调用
  6. * - 支持tenant_access_token和user_access_token
  7. * - 提供用户信息、OAuth2认证等功能
  8. * - 统一错误处理和日志记录
  9. *
  10. * @module client
  11. * @author fmode
  12. * @date 2026
  13. */
  14. /**
  15. * Tenant Access Token 响应接口
  16. */
  17. export interface TenantAccessTokenResponse {
  18. tenantAccessToken: string;
  19. expire: number;
  20. }
  21. /**
  22. * App Access Token 响应接口
  23. */
  24. export interface AppAccessTokenResponse {
  25. appAccessToken: string;
  26. expire: number;
  27. }
  28. /**
  29. * User Access Token 响应接口
  30. */
  31. export interface UserAccessTokenResponse {
  32. accessToken: string;
  33. refreshToken: string;
  34. expiresIn: number;
  35. tokenType: string;
  36. refreshExpiresIn: number;
  37. }
  38. /**
  39. * 用户信息接口
  40. */
  41. export interface FeishuUserInfo {
  42. name: string;
  43. enName?: string;
  44. avatarUrl: string;
  45. avatarThumb?: string;
  46. avatarMiddle?: string;
  47. avatarBig?: string;
  48. openId: string;
  49. unionId: string;
  50. email?: string;
  51. enterpriseEmail?: string;
  52. userId: string;
  53. mobile?: string;
  54. tenantKey?: string;
  55. }
  56. /**
  57. * 登录用户信息响应接口
  58. */
  59. export interface LoginUserInfoResponse {
  60. accessToken: string;
  61. tokenType: string;
  62. expiresIn: number;
  63. refreshToken: string;
  64. refreshExpiresIn: number;
  65. scope: string;
  66. }
  67. /**
  68. * API 转发请求选项接口
  69. */
  70. export interface ForwardRequestOptions {
  71. path: string;
  72. method?: string;
  73. query?: Record<string, any>;
  74. body?: Record<string, any>;
  75. }
  76. /**
  77. * 飞书API客户端类
  78. * 提供所有飞书API的封装方法
  79. */
  80. export class FeishuClient {
  81. private baseUrl: string;
  82. private tokenCache: Map<string, any>;
  83. /**
  84. * 构造函数
  85. * 初始化飞书API基础URL
  86. */
  87. constructor() {
  88. this.baseUrl = 'https://open.feishu.cn/open-apis';
  89. this.tokenCache = new Map<string, any>();
  90. }
  91. /**
  92. * 获取tenant_access_token(企业自建应用)
  93. *
  94. * @static
  95. * @async
  96. * @param appId - 应用ID
  97. * @param appSecret - 应用密钥
  98. * @returns Promise<TenantAccessTokenResponse>
  99. * @throws {Error} 获取失败时抛出错误
  100. */
  101. static async getTenantAccessToken(appId: string, appSecret: string): Promise<TenantAccessTokenResponse> {
  102. try {
  103. const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
  104. const response = await fetch(url, {
  105. method: 'POST',
  106. headers: {
  107. 'Content-Type': 'application/json; charset=utf-8'
  108. },
  109. body: JSON.stringify({
  110. app_id: appId,
  111. app_secret: appSecret
  112. })
  113. });
  114. const result = await response.json();
  115. if (result.code !== 0) {
  116. throw new Error(`获取tenant_access_token失败: ${result.msg}, code: ${result.code}`);
  117. }
  118. return {
  119. tenantAccessToken: result.tenant_access_token,
  120. expire: result.expire
  121. };
  122. } catch (error) {
  123. console.error('获取tenant_access_token异常:', error);
  124. throw error;
  125. }
  126. }
  127. /**
  128. * 获取app_access_token(应用商店应用)
  129. *
  130. * @static
  131. * @async
  132. * @param appId - 应用ID
  133. * @param appSecret - 应用密钥
  134. * @returns Promise<AppAccessTokenResponse>
  135. * @throws {Error} 获取失败时抛出错误
  136. */
  137. static async getAppAccessToken(appId: string, appSecret: string): Promise<AppAccessTokenResponse> {
  138. try {
  139. const url = 'https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal';
  140. const response = await fetch(url, {
  141. method: 'POST',
  142. headers: {
  143. 'Content-Type': 'application/json; charset=utf-8'
  144. },
  145. body: JSON.stringify({
  146. app_id: appId,
  147. app_secret: appSecret
  148. })
  149. });
  150. const result = await response.json();
  151. if (result.code !== 0) {
  152. throw new Error(`获取app_access_token失败: ${result.msg}, code: ${result.code}`);
  153. }
  154. return {
  155. appAccessToken: result.app_access_token,
  156. expire: result.expire
  157. };
  158. } catch (error) {
  159. console.error('获取app_access_token异常:', error);
  160. throw error;
  161. }
  162. }
  163. /**
  164. * OAuth2方式获取用户访问令牌
  165. *
  166. * @static
  167. * @async
  168. * @param appId - 应用ID
  169. * @param appSecret - 应用密钥
  170. * @param code - 授权码
  171. * @param grantType - 授权类型
  172. * @returns Promise<UserAccessTokenResponse>
  173. * @throws {Error} 获取失败时抛出错误
  174. */
  175. static async getUserAccessToken(
  176. appId: string,
  177. appSecret: string,
  178. code: string,
  179. grantType: string = 'authorization_code'
  180. ): Promise<UserAccessTokenResponse> {
  181. try {
  182. const url = 'https://open.feishu.cn/open-apis/authen/v2/oauth/token';
  183. const response = await fetch(url, {
  184. method: 'POST',
  185. headers: {
  186. 'Content-Type': 'application/json; charset=utf-8'
  187. },
  188. body: JSON.stringify({
  189. grant_type: grantType,
  190. client_id: appId,
  191. client_secret: appSecret,
  192. code: code
  193. })
  194. });
  195. const result = await response.json();
  196. if (result.code !== 0) {
  197. throw new Error(`获取user_access_token失败: ${result.error_description || result.msg}, code: ${result.code}`);
  198. }
  199. return {
  200. accessToken: result.access_token,
  201. refreshToken: result.refresh_token,
  202. expiresIn: result.expires_in,
  203. tokenType: result.token_type,
  204. refreshExpiresIn: result.refresh_token_expires_in
  205. };
  206. } catch (error) {
  207. console.error('获取user_access_token异常:', error);
  208. throw error;
  209. }
  210. }
  211. /**
  212. * 刷新用户访问令牌
  213. *
  214. * @static
  215. * @async
  216. * @param appId - 应用ID
  217. * @param appSecret - 应用密钥
  218. * @param refreshToken - 刷新令牌
  219. * @returns Promise<UserAccessTokenResponse>
  220. * @throws {Error} 刷新失败时抛出错误
  221. */
  222. static async refreshUserAccessToken(
  223. appId: string,
  224. appSecret: string,
  225. refreshToken: string
  226. ): Promise<UserAccessTokenResponse> {
  227. try {
  228. const url = 'https://open.feishu.cn/open-apis/authen/v2/oauth/token';
  229. const response = await fetch(url, {
  230. method: 'POST',
  231. headers: {
  232. 'Content-Type': 'application/json; charset=utf-8'
  233. },
  234. body: JSON.stringify({
  235. grant_type: 'refresh_token',
  236. client_id: appId,
  237. client_secret: appSecret,
  238. refresh_token: refreshToken
  239. })
  240. });
  241. const result = await response.json();
  242. if (result.code !== 0) {
  243. throw new Error(`刷新user_access_token失败: ${result.error_description || result.msg}, code: ${result.code}`);
  244. }
  245. return {
  246. accessToken: result.access_token,
  247. refreshToken: result.refresh_token,
  248. expiresIn: result.expires_in,
  249. tokenType: result.token_type,
  250. refreshExpiresIn: result.refresh_token_expires_in
  251. };
  252. } catch (error) {
  253. console.error('刷新user_access_token异常:', error);
  254. throw error;
  255. }
  256. }
  257. /**
  258. * 获取用户信息(使用user_access_token)
  259. *
  260. * @static
  261. * @async
  262. * @param userAccessToken - 用户访问令牌
  263. * @returns Promise<FeishuUserInfo>
  264. * @throws {Error} 获取失败时抛出错误
  265. */
  266. static async getUserInfo(userAccessToken: string): Promise<FeishuUserInfo> {
  267. try {
  268. const url = 'https://open.feishu.cn/open-apis/authen/v1/user_info';
  269. const response = await fetch(url, {
  270. method: 'GET',
  271. headers: {
  272. 'Authorization': `Bearer ${userAccessToken}`,
  273. 'Content-Type': 'application/json; charset=utf-8'
  274. }
  275. });
  276. const result = await response.json();
  277. if (result.code !== 0) {
  278. throw new Error(`获取用户信息失败: ${result.msg}, code: ${result.code}`);
  279. }
  280. return {
  281. name: result.data.name,
  282. enName: result.data.en_name,
  283. avatarUrl: result.data.avatar_url,
  284. avatarThumb: result.data.avatar_thumb,
  285. avatarMiddle: result.data.avatar_middle,
  286. avatarBig: result.data.avatar_big,
  287. openId: result.data.open_id,
  288. unionId: result.data.union_id,
  289. email: result.data.email,
  290. enterpriseEmail: result.data.enterprise_email,
  291. userId: result.data.user_id,
  292. mobile: result.data.mobile,
  293. tenantKey: result.data.tenant_key
  294. };
  295. } catch (error) {
  296. console.error('获取用户信息异常:', error);
  297. throw error;
  298. }
  299. }
  300. /**
  301. * 获取登录用户身份(网页免登)
  302. *
  303. * @static
  304. * @async
  305. * @param tenantAccessToken - 租户访问令牌
  306. * @param code - 免登授权码
  307. * @returns Promise<LoginUserInfoResponse>
  308. * @throws {Error} 获取失败时抛出错误
  309. */
  310. static async getLoginUserInfo(tenantAccessToken: string, code: string): Promise<LoginUserInfoResponse> {
  311. try {
  312. const url = 'https://open.feishu.cn/open-apis/authen/v1/access_token';
  313. const response = await fetch(url, {
  314. method: 'POST',
  315. headers: {
  316. 'Authorization': `Bearer ${tenantAccessToken}`,
  317. 'Content-Type': 'application/json; charset=utf-8'
  318. },
  319. body: JSON.stringify({
  320. grant_type: 'authorization_code',
  321. code: code
  322. })
  323. });
  324. const result = await response.json();
  325. if (result.code !== 0) {
  326. throw new Error(`获取登录用户身份失败: ${result.msg}, code: ${result.code}`);
  327. }
  328. return {
  329. accessToken: result.data.access_token,
  330. tokenType: result.data.token_type,
  331. expiresIn: result.data.expires_in,
  332. refreshToken: result.data.refresh_token,
  333. refreshExpiresIn: result.data.refresh_expires_in,
  334. scope: result.data.scope
  335. };
  336. } catch (error) {
  337. console.error('获取登录用户身份异常:', error);
  338. throw error;
  339. }
  340. }
  341. /**
  342. * 转发请求到飞书API
  343. *
  344. * @static
  345. * @async
  346. * @param accessToken - 访问令牌
  347. * @param options - 请求选项
  348. * @returns Promise<any>
  349. * @throws {Error} 请求失败时抛出错误
  350. */
  351. static async forwardRequest(accessToken: string, options: ForwardRequestOptions): Promise<any> {
  352. try {
  353. const { path, method = 'GET', query, body } = options;
  354. let url = `https://open.feishu.cn/open-apis${path}`;
  355. if (query && typeof query === 'object') {
  356. const queryParams = new URLSearchParams();
  357. Object.keys(query).forEach(key => {
  358. if (query[key] !== undefined && query[key] !== null) {
  359. queryParams.append(key, query[key]);
  360. }
  361. });
  362. const queryString = queryParams.toString();
  363. if (queryString) {
  364. url += `?${queryString}`;
  365. }
  366. }
  367. const requestOptions: RequestInit = {
  368. method: method.toUpperCase(),
  369. headers: {
  370. 'Authorization': `Bearer ${accessToken}`,
  371. 'Content-Type': 'application/json; charset=utf-8'
  372. }
  373. };
  374. if (body && typeof body === 'object' && method.toUpperCase() !== 'GET') {
  375. requestOptions.body = JSON.stringify(body);
  376. }
  377. console.log('转发飞书API请求:', { url, method, body });
  378. const response = await fetch(url, requestOptions);
  379. const result = await response.json();
  380. console.log('飞书API响应:', result);
  381. return result;
  382. } catch (error) {
  383. console.error('转发请求异常:', error);
  384. throw error;
  385. }
  386. }
  387. }
  388. /**
  389. * 默认导出客户端类
  390. */
  391. export default FeishuClient;