├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── README.zh.md ├── adaptor ├── default.ts ├── express.ts ├── koa-router.ts ├── koa.ts ├── pick-request-data.ts └── services │ ├── __tests__ │ └── challenge.ts │ └── challenge.ts ├── client ├── app-ticket-manager.ts ├── client.ts ├── request-with.ts ├── token-manager.ts ├── types.ts ├── user-access-token.ts └── utils │ ├── __tests__ │ └── format-errors.ts │ ├── format-errors.ts │ └── index.ts ├── code-gen ├── client-template.ts ├── events-template.ts ├── other-event-handles.ts ├── projects │ ├── acs.ts │ ├── admin.ts │ ├── aily.ts │ ├── apaas.ts │ ├── application.ts │ ├── approval.ts │ ├── attendance.ts │ ├── auth.ts │ ├── authen.ts │ ├── aweme_ecosystem.ts │ ├── baike.ts │ ├── base.ts │ ├── bitable.ts │ ├── block.ts │ ├── board.ts │ ├── calendar.ts │ ├── cardkit.ts │ ├── comment_sdk.ts │ ├── compensation.ts │ ├── contact.ts │ ├── content_check.ts │ ├── contract.ts │ ├── corehr.ts │ ├── directory.ts │ ├── docs.ts │ ├── docs_tool.ts │ ├── document_ai.ts │ ├── docx.ts │ ├── drive.ts │ ├── edu.ts │ ├── ehr.ts │ ├── elearning.ts │ ├── event.ts │ ├── exam.ts │ ├── face_detection.ts │ ├── feelgood.ts │ ├── helpdesk.ts │ ├── hire.ts │ ├── human_authentication.ts │ ├── im.ts │ ├── lingo.ts │ ├── mail.ts │ ├── mdm.ts │ ├── meeting_room.ts │ ├── minutes.ts │ ├── moments.ts │ ├── okr.ts │ ├── optical_char_recognition.ts │ ├── passport.ts │ ├── payroll.ts │ ├── people_admin.ts │ ├── people_bytedance.ts │ ├── performance.ts │ ├── personal_settings.ts │ ├── report.ts │ ├── search.ts │ ├── search_in_app.ts │ ├── security_and_compliance.ts │ ├── sheets.ts │ ├── speech_to_text.ts │ ├── spend.ts │ ├── sup_project.ts │ ├── task.ts │ ├── tenant.ts │ ├── translation.ts │ ├── unified_kms_log.ts │ ├── vc.ts │ ├── verification.ts │ └── wiki.ts └── types.ts ├── consts └── index.ts ├── dispatcher ├── card.ts ├── event.ts └── request-handle.ts ├── doc ├── debugger-tip.png ├── deprecated.png └── msg-card.png ├── http └── index.ts ├── index.ts ├── jest.config.ts ├── logger ├── __tests__ │ └── logger-proxy.ts ├── default-logger.ts └── logger-proxy.ts ├── package.json ├── rollup.config.js ├── scene └── aily │ ├── __tests__ │ └── session-cache.ts │ ├── client.ts │ └── session-cache.ts ├── tsconfig.json ├── typings ├── card.ts ├── http.ts └── index.ts ├── utils ├── __tests__ │ ├── assert.ts │ ├── fill-api-path.ts │ ├── format-url.ts │ └── merge-object.ts ├── aes-cipher.ts ├── assert.ts ├── default-cache.ts ├── fill-api-path.ts ├── format-domain.ts ├── format-url.ts ├── index.ts ├── merge-object.ts ├── message-card.ts ├── pick.ts └── string-2-base64.ts ├── ws-client ├── __tests__ │ └── data-cache.ts ├── data-cache.ts ├── enum.ts ├── index.ts ├── proto-buf │ ├── index.ts │ ├── pbbp2.d.ts │ └── pbbp2.js └── ws-config.ts └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | es 2 | lib 3 | types -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | jest: true, 6 | }, 7 | extends: [ 8 | 'airbnb-base', 9 | 'prettier', 10 | ], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | ecmaVersion: 'latest', 14 | sourceType: 'module', 15 | }, 16 | plugins: [ 17 | '@typescript-eslint', 18 | ], 19 | rules: { 20 | 'import/extensions': ['off'], 21 | 'import/prefer-default-export': ['off'], 22 | 'import/no-unresolved': ['off'], 23 | // indent: [2, 4, { SwitchCase: 1, offsetTernaryExpressions: true }], 24 | 'no-restricted-syntax': ['off'], 25 | 'camelcase': ['off'], 26 | 'no-await-in-loop': ['off'], 27 | "no-shadow": "off", 28 | "@typescript-eslint/no-shadow": "error", 29 | "no-unused-vars": "off", 30 | "@typescript-eslint/no-unused-vars": ["error"] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # for logs and certain task outputs 2 | .DS_Store 3 | */.DS_Store 4 | 5 | node_modules 6 | 7 | # for tests 8 | local-tests 9 | .vscode 10 | 11 | # output assets 12 | **/es 13 | **/lib 14 | **/types -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # output assets 4 | es 5 | lib 6 | types 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "semi": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Lark Technologies Pte. Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice, shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /adaptor/default.ts: -------------------------------------------------------------------------------- 1 | import { EventDispatcher } from '@node-sdk/dispatcher/event'; 2 | import { CardActionHandler } from '@node-sdk/dispatcher/card'; 3 | import { pickRequestData } from './pick-request-data'; 4 | import { generateChallenge } from './services/challenge'; 5 | 6 | export const adaptDefault = 7 | ( 8 | path: string, 9 | dispatcher: EventDispatcher | CardActionHandler, 10 | options?: { 11 | autoChallenge?: boolean; 12 | } 13 | ) => 14 | async (req, res) => { 15 | if (req.url !== path) { 16 | return; 17 | } 18 | 19 | const data = Object.assign( 20 | Object.create({ 21 | headers: req.headers, 22 | }), 23 | await pickRequestData(req) 24 | ); 25 | 26 | // 是否自动响应challange事件: 27 | // https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/request-url-configuration-case 28 | const autoChallenge = options?.autoChallenge || false; 29 | if (autoChallenge) { 30 | const { isChallenge, challenge } = generateChallenge(data, { 31 | encryptKey: dispatcher.encryptKey, 32 | }); 33 | 34 | if (isChallenge) { 35 | res.end(JSON.stringify(challenge)); 36 | return; 37 | } 38 | } 39 | 40 | const value = await dispatcher.invoke(data); 41 | 42 | res.end(JSON.stringify(value)); 43 | }; 44 | -------------------------------------------------------------------------------- /adaptor/express.ts: -------------------------------------------------------------------------------- 1 | import { EventDispatcher } from '@node-sdk/dispatcher/event'; 2 | import { CardActionHandler } from '@node-sdk/dispatcher/card'; 3 | import { Logger } from '@node-sdk/typings'; 4 | import { defaultLogger } from '@node-sdk/logger/default-logger'; 5 | import { pickRequestData } from './pick-request-data'; 6 | import { generateChallenge } from './services/challenge'; 7 | 8 | export const adaptExpress = 9 | ( 10 | dispatcher: EventDispatcher | CardActionHandler, 11 | options?: { 12 | logger?: Logger; 13 | autoChallenge?: boolean; 14 | } 15 | ) => 16 | async (req, res) => { 17 | const reqData = await (async () => { 18 | if (req.body) { 19 | return req.body; 20 | } 21 | if (!req.complete) { 22 | const incomingdata = await pickRequestData(req); 23 | return incomingdata; 24 | } 25 | 26 | (options?.logger || defaultLogger).error( 27 | 'unable to obtain request body, if parsed it in other middleware, please manually set in ctx.request.body' 28 | ); 29 | return null; 30 | })(); 31 | 32 | const data = Object.assign( 33 | Object.create({ 34 | headers: req.headers, 35 | }), 36 | reqData 37 | ); 38 | 39 | // 是否自动响应challenge事件: 40 | // https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/request-url-configuration-case 41 | const autoChallenge = options?.autoChallenge || false; 42 | if (autoChallenge) { 43 | const { isChallenge, challenge } = generateChallenge(data, { 44 | encryptKey: dispatcher.encryptKey, 45 | }); 46 | 47 | if (isChallenge) { 48 | res.json(challenge); 49 | return; 50 | } 51 | } 52 | 53 | const value = await dispatcher.invoke(data); 54 | 55 | res.json(value); 56 | }; 57 | -------------------------------------------------------------------------------- /adaptor/koa-router.ts: -------------------------------------------------------------------------------- 1 | import { EventDispatcher } from '@node-sdk/dispatcher/event'; 2 | import { CardActionHandler } from '@node-sdk/dispatcher/card'; 3 | import { Logger } from '@node-sdk/typings'; 4 | import { defaultLogger } from '@node-sdk/logger/default-logger'; 5 | import { pickRequestData } from './pick-request-data'; 6 | import { generateChallenge } from './services/challenge'; 7 | 8 | export const adaptKoaRouter = 9 | ( 10 | dispatcher: EventDispatcher | CardActionHandler, 11 | options?: { 12 | logger?: Logger; 13 | autoChallenge?: boolean; 14 | } 15 | ) => 16 | async (ctx, next) => { 17 | const { req, request } = ctx; 18 | 19 | const reqData = await (async () => { 20 | if (request.body) { 21 | return request.body; 22 | } 23 | if (!req.complete) { 24 | const incomingdata = await pickRequestData(req); 25 | return incomingdata; 26 | } 27 | 28 | (options?.logger || defaultLogger).error( 29 | 'unable to obtain request body, if parsed it in other middleware, please manually set in ctx.request.body' 30 | ); 31 | return null; 32 | })(); 33 | 34 | const data = Object.assign( 35 | Object.create({ 36 | headers: req.headers, 37 | }), 38 | reqData 39 | ); 40 | 41 | // 是否自动响应challange事件: 42 | // https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/request-url-configuration-case 43 | const autoChallenge = options?.autoChallenge || false; 44 | if (autoChallenge) { 45 | const { isChallenge, challenge } = generateChallenge(data, { 46 | encryptKey: dispatcher.encryptKey, 47 | }); 48 | 49 | if (isChallenge) { 50 | ctx.body = challenge; 51 | await next(); 52 | return; 53 | } 54 | } 55 | 56 | const value = await dispatcher.invoke(data); 57 | 58 | ctx.body = JSON.stringify(value); 59 | 60 | await next(); 61 | }; 62 | -------------------------------------------------------------------------------- /adaptor/koa.ts: -------------------------------------------------------------------------------- 1 | import { EventDispatcher } from '@node-sdk/dispatcher/event'; 2 | import { CardActionHandler } from '@node-sdk/dispatcher/card'; 3 | import { Logger } from '@node-sdk/typings'; 4 | import { defaultLogger } from '@node-sdk/logger/default-logger'; 5 | import { pickRequestData } from './pick-request-data'; 6 | import { generateChallenge } from './services/challenge'; 7 | 8 | export const adaptKoa = 9 | ( 10 | path: string, 11 | dispatcher: EventDispatcher | CardActionHandler, 12 | options?: { 13 | logger?: Logger; 14 | autoChallenge?: boolean; 15 | } 16 | ) => 17 | async (ctx, next) => { 18 | const { originalUrl, req, request } = ctx; 19 | if (originalUrl === path) { 20 | const reqData = await (async () => { 21 | if (request.body) { 22 | return request.body; 23 | } 24 | if (!req.complete) { 25 | const incomingdata = await pickRequestData(req); 26 | return incomingdata; 27 | } 28 | (options?.logger || defaultLogger).error( 29 | 'unable to obtain request body, if parsed it in other middleware, please manually set in ctx.request.body' 30 | ); 31 | return null; 32 | })(); 33 | 34 | const data = Object.assign( 35 | Object.create({ 36 | headers: req.headers, 37 | }), 38 | reqData 39 | ); 40 | 41 | // 是否自动响应challange事件: 42 | // https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/request-url-configuration-case 43 | const autoChallenge = options?.autoChallenge || false; 44 | if (autoChallenge) { 45 | const { isChallenge, challenge } = generateChallenge(data, { 46 | encryptKey: dispatcher.encryptKey, 47 | }); 48 | 49 | if (isChallenge) { 50 | ctx.body = challenge; 51 | await next(); 52 | return; 53 | } 54 | } 55 | 56 | const value = await dispatcher.invoke(data); 57 | 58 | ctx.body = JSON.stringify(value); 59 | } 60 | 61 | await next(); 62 | }; 63 | -------------------------------------------------------------------------------- /adaptor/pick-request-data.ts: -------------------------------------------------------------------------------- 1 | export const pickRequestData = (req) => 2 | new Promise((resolve) => { 3 | let chunks = ''; 4 | req.on('data', (chunk) => { 5 | chunks += chunk; 6 | }); 7 | 8 | req.on('end', () => { 9 | try { 10 | const data = JSON.parse(chunks); 11 | resolve(data); 12 | } catch (e) { 13 | resolve(''); 14 | } 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /adaptor/services/__tests__/challenge.ts: -------------------------------------------------------------------------------- 1 | import { generateChallenge } from '../challenge'; 2 | 3 | describe('generateChallenge(data, options)', () => { 4 | it('generate right with normal challenge', () => { 5 | expect( 6 | generateChallenge( 7 | { 8 | challenge: 'ajls384kdjx98XX', 9 | type: 'url_verification', 10 | }, 11 | { 12 | encryptKey: '', 13 | } 14 | ) 15 | ).toEqual({ 16 | isChallenge: true, 17 | challenge: { 18 | challenge: 'ajls384kdjx98XX', 19 | }, 20 | }); 21 | }); 22 | 23 | it('generate right with encrypt challenge', () => { 24 | expect( 25 | generateChallenge( 26 | { 27 | encrypt: 28 | 'tT1xP+ovuc0T3b/rnZX3ax76exqAn0ANQ6/U45GcciC0mgNBc8JtvqOJGNAPMKpgRs2dVU1NYk9VDeqS8T4SYtU0hzNU54aS84WnxvZ3VFfDcy/RyABlNqlUiGyLDzFI0yxdCMT4KR/YEQXlt6nZi50KwRlZ+A75r645KesZuLMljezYyY8VeeXEVjPw35+e', 29 | }, 30 | { 31 | encryptKey: 'mazhe.nerd', 32 | } 33 | ) 34 | ).toEqual({ 35 | isChallenge: true, 36 | challenge: { 37 | challenge: '5634b122-c042-4634-bc7f-b07a3c3e77ad', 38 | }, 39 | }); 40 | }); 41 | 42 | it('generate right with no challenge', () => { 43 | expect( 44 | generateChallenge( 45 | {}, 46 | { 47 | encryptKey: '', 48 | } 49 | ) 50 | ).toEqual({ 51 | isChallenge: false, 52 | challenge: { 53 | challenge: undefined, 54 | }, 55 | }); 56 | }); 57 | 58 | it('should throw in encrypt scene but not pass encryptKey', () => { 59 | expect(() => 60 | generateChallenge( 61 | { 62 | encrypt: 'encrypt', 63 | }, 64 | { 65 | encryptKey: '', 66 | } 67 | ) 68 | ).toThrowError( 69 | 'auto-challenge need encryptKey, please check for missing in dispatcher' 70 | ); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /adaptor/services/challenge.ts: -------------------------------------------------------------------------------- 1 | import { AESCipher } from '@node-sdk/utils/aes-cipher'; 2 | 3 | export const generateChallenge = ( 4 | data: any, 5 | options: { 6 | encryptKey: string; 7 | } 8 | ) => { 9 | if ('encrypt' in data && !options.encryptKey) { 10 | throw new Error( 11 | 'auto-challenge need encryptKey, please check for missing in dispatcher' 12 | ); 13 | } 14 | 15 | const targetData = 16 | 'encrypt' in data 17 | ? JSON.parse( 18 | new AESCipher(options.encryptKey).decrypt(data.encrypt) 19 | ) 20 | : data; 21 | 22 | return { 23 | isChallenge: targetData.type === 'url_verification', 24 | challenge: { 25 | challenge: targetData.challenge, 26 | }, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /client/app-ticket-manager.ts: -------------------------------------------------------------------------------- 1 | import { Cache, Logger, AppType } from '@node-sdk/typings'; 2 | import { CAppTicket } from '@node-sdk/consts'; 3 | import { HttpInstance } from '@node-sdk/typings/http'; 4 | 5 | export interface IParams { 6 | appId: string; 7 | appSecret: string; 8 | cache: Cache; 9 | domain: string; 10 | logger: Logger; 11 | appType: AppType; 12 | httpInstance: HttpInstance; 13 | } 14 | export default class AppTicketManager { 15 | appId: string; 16 | 17 | appSecret: string; 18 | 19 | cache?: Cache; 20 | 21 | domain: string; 22 | 23 | logger: Logger; 24 | 25 | appType: AppType; 26 | 27 | httpInstance: HttpInstance; 28 | 29 | constructor(params: IParams) { 30 | this.appId = params.appId; 31 | this.appSecret = params.appSecret; 32 | this.cache = params.cache; 33 | this.domain = params.domain; 34 | this.logger = params.logger; 35 | this.appType = params.appType; 36 | this.httpInstance = params.httpInstance; 37 | 38 | this.logger.debug('app ticket manager is ready'); 39 | 40 | this.checkAppTicket(); 41 | } 42 | 43 | async checkAppTicket() { 44 | if (this.appType === AppType.ISV) { 45 | const appTicket = await this.cache?.get(CAppTicket, { 46 | namespace: this.appId 47 | }); 48 | if (!appTicket) { 49 | this.requestAppTicket(); 50 | } 51 | } 52 | } 53 | 54 | async requestAppTicket() { 55 | this.logger.debug('request app ticket'); 56 | await this.httpInstance 57 | .post(`${this.domain}/open-apis/auth/v3/app_ticket/resend`, { 58 | app_id: this.appId, 59 | app_secret: this.appSecret, 60 | }) 61 | .catch((e) => { 62 | this.logger.error(e); 63 | }); 64 | } 65 | 66 | async getAppTicket() { 67 | const appTicket = await this.cache?.get(CAppTicket, { 68 | namespace: this.appId 69 | }); 70 | 71 | if (appTicket) { 72 | this.logger.debug('use cache app ticket'); 73 | return appTicket; 74 | } 75 | 76 | await this.requestAppTicket(); 77 | 78 | return undefined; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/client.ts: -------------------------------------------------------------------------------- 1 | import defaultHttpInstance, { AxiosRequestConfig } from '@node-sdk/http'; 2 | import { Cache, AppType, Domain, LoggerLevel, Logger } from '@node-sdk/typings'; 3 | import { 4 | CTenantKey, 5 | CWithHelpdeskAuthorization, 6 | CWithUserAccessToken, 7 | } from '@node-sdk/consts'; 8 | import { 9 | string2Base64, 10 | internalCache, 11 | formatDomain, 12 | assert, 13 | formatUrl, 14 | } from '@node-sdk/utils'; 15 | import RequestTemplate from '@node-sdk/code-gen/client-template'; 16 | import { defaultLogger } from '@node-sdk/logger/default-logger'; 17 | import { LoggerProxy } from '@node-sdk/logger/logger-proxy'; 18 | import { IRequestOptions, IClientParams, IPayload } from './types'; 19 | import { TokenManager } from './token-manager'; 20 | import { HttpInstance } from '@node-sdk/typings/http'; 21 | import { UserAccessToken } from './user-access-token'; 22 | 23 | export class Client extends RequestTemplate { 24 | appId: string = ''; 25 | 26 | appSecret: string = ''; 27 | 28 | logger: Logger; 29 | 30 | helpDeskId?: string = ''; 31 | 32 | helpDeskToken?: string = ''; 33 | 34 | tokenManager: TokenManager; 35 | 36 | cache: Cache; 37 | 38 | disableTokenCache?: boolean; 39 | 40 | appType: AppType = AppType.SelfBuild; 41 | 42 | domain: string; 43 | 44 | httpInstance: HttpInstance; 45 | 46 | userAccessToken: UserAccessToken; 47 | 48 | constructor(params: IClientParams) { 49 | super(); 50 | 51 | this.logger = new LoggerProxy( 52 | params.loggerLevel || LoggerLevel.info, 53 | params.logger || defaultLogger 54 | ); 55 | 56 | this.appId = params.appId; 57 | this.appSecret = params.appSecret; 58 | this.disableTokenCache = params.disableTokenCache; 59 | 60 | assert(!this.appId, () => this.logger.error('appId is needed')); 61 | assert(!this.appSecret, () => this.logger.error('appSecret is needed')); 62 | 63 | this.helpDeskId = params.helpDeskId; 64 | this.helpDeskToken = params.helpDeskToken; 65 | this.appType = params?.appType || AppType.SelfBuild; 66 | 67 | this.domain = formatDomain(params.domain || Domain.Feishu); 68 | this.logger.debug(`use domain url: ${this.domain}`); 69 | 70 | this.cache = params.cache || internalCache; 71 | this.httpInstance = params.httpInstance || defaultHttpInstance; 72 | 73 | this.tokenManager = new TokenManager({ 74 | appId: this.appId, 75 | appSecret: this.appSecret, 76 | cache: this.cache, 77 | domain: this.domain, 78 | logger: this.logger, 79 | appType: this.appType, 80 | httpInstance: this.httpInstance, 81 | }); 82 | 83 | this.userAccessToken = new UserAccessToken({client: this}); 84 | 85 | this.logger.info('client ready'); 86 | } 87 | 88 | async formatPayload( 89 | payload?: IPayload, 90 | options?: IRequestOptions 91 | ): Promise> { 92 | const targetOptions = [ 93 | 'lark', 94 | 'params', 95 | 'data', 96 | 'headers', 97 | 'path', 98 | ].reduce((acc, key) => { 99 | acc[key] = options?.[key] || {}; 100 | return acc; 101 | }, {} as Required); 102 | 103 | const userAccessToken = targetOptions?.lark?.[CWithUserAccessToken]; 104 | 105 | if (userAccessToken) { 106 | this.logger.debug('use passed token'); 107 | targetOptions.headers.Authorization = `Bearer ${userAccessToken}`; 108 | } else if (!this.disableTokenCache) { 109 | const tenantAccessToken = 110 | await this.tokenManager.getTenantAccessToken({ 111 | [CTenantKey]: targetOptions?.lark?.[CTenantKey], 112 | }); 113 | if (tenantAccessToken) { 114 | targetOptions.headers.Authorization = `Bearer ${tenantAccessToken}`; 115 | } else { 116 | this.logger.warn('failed to obtain token'); 117 | } 118 | } 119 | 120 | // helpDeskCredential 121 | const withHelpDeskCredential = targetOptions?.lark?.[CWithHelpdeskAuthorization]; 122 | 123 | if (withHelpDeskCredential) { 124 | this.logger.debug('generate help desk credential'); 125 | const helpDeskCredential = string2Base64( 126 | `${this.helpDeskId}:${this.helpDeskToken}` 127 | ); 128 | targetOptions.headers[ 129 | 'X-Lark-Helpdesk-Authorization' 130 | ] = `Bearer ${helpDeskCredential}`; 131 | } 132 | 133 | return { 134 | params: { ...(payload?.params || {}), ...targetOptions.params }, 135 | headers: { 136 | 'User-Agent': 'oapi-node-sdk/1.0.0', 137 | ...(payload?.headers || {}), 138 | ...targetOptions.headers, 139 | }, 140 | data: { ...(payload?.data || {}), ...targetOptions.data }, 141 | path: { 142 | ...(payload?.path || {}), 143 | ...targetOptions.path, 144 | }, 145 | }; 146 | } 147 | 148 | async request( 149 | payload: AxiosRequestConfig, 150 | options?: IRequestOptions 151 | ) { 152 | const { data, params, headers, url, ...rest } = payload; 153 | const formatPayload = await this.formatPayload( 154 | { 155 | data, 156 | params, 157 | headers, 158 | }, 159 | options 160 | ); 161 | 162 | this.logger.trace(`send request [${payload.method}]: ${payload.url}`); 163 | const res = await this.httpInstance 164 | .request({ 165 | ...rest, 166 | ...{ 167 | url: /^http/.test(url!) 168 | ? url 169 | : `${this.domain}/${formatUrl(url)}`, 170 | headers: formatPayload.headers, 171 | data: formatPayload.data, 172 | params: formatPayload.params, 173 | }, 174 | }) 175 | .catch((e) => { 176 | this.logger.error(e); 177 | throw e; 178 | }); 179 | 180 | return res; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /client/request-with.ts: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge'; 2 | import { 3 | CTenantKey, 4 | CWithHelpdeskAuthorization, 5 | CWithUserAccessToken, 6 | } from '@node-sdk/consts'; 7 | import { IRequestOptions } from './types'; 8 | 9 | export const withAll = (withList: IRequestOptions[]): IRequestOptions => 10 | withList.reduce((acc, cur) => merge(acc, cur), {} as IRequestOptions); 11 | 12 | export const withTenantKey = (tenantKey: string): IRequestOptions => ({ 13 | lark: { 14 | [CTenantKey]: tenantKey, 15 | }, 16 | }); 17 | 18 | export const withHelpDeskCredential = (): IRequestOptions => ({ 19 | lark: { 20 | [CWithHelpdeskAuthorization]: true, 21 | }, 22 | }); 23 | 24 | export const withTenantToken = ( 25 | tenantAccessToken: string 26 | ): IRequestOptions => ({ 27 | headers: { 28 | Authorization: `Bearer ${tenantAccessToken}`, 29 | }, 30 | }); 31 | 32 | export const withUserAccessToken = ( 33 | userAccessToken: string 34 | ): IRequestOptions => ({ 35 | lark: { 36 | [CWithUserAccessToken]: userAccessToken, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /client/token-manager.ts: -------------------------------------------------------------------------------- 1 | import { CTenantKey, CTenantAccessToken } from '@node-sdk/consts'; 2 | import { Cache, AppType, Logger } from '@node-sdk/typings'; 3 | import { assert } from '@node-sdk/utils'; 4 | import AppTicketManager from './app-ticket-manager'; 5 | import { HttpInstance } from '@node-sdk/typings/http'; 6 | 7 | interface IParams { 8 | appId: string; 9 | appSecret: string; 10 | cache: Cache; 11 | domain: string; 12 | logger: Logger; 13 | appType: AppType; 14 | httpInstance: HttpInstance; 15 | } 16 | 17 | export class TokenManager { 18 | appId: string; 19 | 20 | appSecret: string; 21 | 22 | cache: Cache; 23 | 24 | appTicketManager: AppTicketManager; 25 | 26 | domain: string; 27 | 28 | logger: Logger; 29 | 30 | appType: AppType; 31 | 32 | httpInstance: HttpInstance; 33 | 34 | constructor(params: IParams) { 35 | this.appId = params.appId; 36 | this.appSecret = params.appSecret; 37 | this.cache = params.cache; 38 | this.domain = params.domain; 39 | this.logger = params.logger; 40 | this.appType = params.appType; 41 | this.httpInstance = params.httpInstance; 42 | 43 | this.appTicketManager = new AppTicketManager({ 44 | appId: this.appId, 45 | appSecret: this.appSecret, 46 | cache: this.cache, 47 | domain: this.domain, 48 | logger: this.logger, 49 | appType: this.appType, 50 | httpInstance: this.httpInstance, 51 | }); 52 | 53 | this.logger.debug('token manager is ready'); 54 | } 55 | 56 | async getCustomTenantAccessToken() { 57 | const cachedTenantAccessToken = await this.cache?.get( 58 | CTenantAccessToken, 59 | { 60 | namespace: this.appId 61 | } 62 | ); 63 | 64 | if (cachedTenantAccessToken) { 65 | this.logger.debug('use cache token'); 66 | return cachedTenantAccessToken; 67 | } 68 | 69 | this.logger.debug('request token'); 70 | // @ts-ignore 71 | const { tenant_access_token, expire } = await this.httpInstance 72 | .post( 73 | `${this.domain}/open-apis/auth/v3/tenant_access_token/internal`, 74 | { 75 | app_id: this.appId, 76 | app_secret: this.appSecret, 77 | } 78 | ) 79 | .catch((e) => { 80 | this.logger.error(e); 81 | }); 82 | 83 | await this.cache?.set( 84 | CTenantAccessToken, 85 | tenant_access_token, 86 | // Due to the time-consuming network, the expiration time needs to be 3 minutes earlier 87 | new Date().getTime() + expire * 1000 - 3 * 60 * 1000, 88 | { 89 | namespace: this.appId 90 | } 91 | ); 92 | 93 | return tenant_access_token; 94 | } 95 | 96 | async getMarketTenantAccessToken(tenantKey: string) { 97 | if (!tenantKey) { 98 | this.logger.error('market app request need tenant key'); 99 | return undefined; 100 | } 101 | 102 | const tenantAccessToken = await this.cache?.get( 103 | `larkMarketAccessToken${tenantKey}`, 104 | { 105 | namespace: this.appId 106 | } 107 | ); 108 | 109 | if (tenantAccessToken) { 110 | this.logger.debug('use cache token'); 111 | return tenantAccessToken; 112 | } 113 | 114 | this.logger.debug('get app ticket'); 115 | const appTicket = await this.appTicketManager.getAppTicket(); 116 | 117 | if (!appTicket) { 118 | this.logger.warn('no app ticket'); 119 | return undefined; 120 | } 121 | 122 | this.logger.debug('get app access token'); 123 | // 获取app_access_token 124 | // @ts-ignore 125 | const { app_access_token } = await this.httpInstance 126 | .post<{ 127 | app_access_token: string; 128 | }>(`${this.domain}/open-apis/auth/v3/app_access_token`, { 129 | app_id: this.appId, 130 | app_secret: this.appSecret, 131 | app_ticket: appTicket, 132 | }) 133 | .catch((e) => { 134 | this.logger.error(e); 135 | }); 136 | 137 | this.logger.debug('get tenant access token'); 138 | // 获取tenant_access_token 139 | // @ts-ignore 140 | const { tenant_access_token, expire } = await this.httpInstance 141 | .post(`${this.domain}/open-apis/auth/v3/tenant_access_token`, { 142 | app_access_token, 143 | tenant_key: tenantKey, 144 | }) 145 | .catch((e) => { 146 | this.logger.error(e); 147 | }); 148 | 149 | // 设置tenant_access_token 150 | await this.cache.set( 151 | `larkMarketAccessToken${tenantKey}`, 152 | tenant_access_token, 153 | // Due to the time-consuming network, the expiration time needs to be 3 minutes earlier 154 | new Date().getTime() + expire * 1000 - 3 * 60 * 1000, 155 | { 156 | namespace: this.appId 157 | } 158 | ); 159 | 160 | return tenant_access_token; 161 | } 162 | 163 | async getTenantAccessToken(params?: { [CTenantKey]?: string }) { 164 | assert(this.appType === AppType.SelfBuild, async () => { 165 | this.logger.debug('get custom app token'); 166 | }); 167 | assert(this.appType === AppType.ISV, async () => { 168 | this.logger.debug('get market app token '); 169 | }); 170 | 171 | // prettier-ignore 172 | const tenantAccessToken = 173 | this.appType === AppType.SelfBuild 174 | ? await this.getCustomTenantAccessToken() 175 | : await this.getMarketTenantAccessToken(params?.[CTenantKey]!); 176 | 177 | return tenantAccessToken; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /client/types.ts: -------------------------------------------------------------------------------- 1 | import { Cache, AppType, Domain, LoggerLevel, Logger } from '@node-sdk/typings'; 2 | import { 3 | CTenantKey, 4 | CWithHelpdeskAuthorization, 5 | CWithUserAccessToken, 6 | } from '@node-sdk/consts'; 7 | import { HttpInstance } from '@node-sdk/typings/http'; 8 | 9 | // 和axios保持一致 10 | export interface IRequestOptions { 11 | lark?: { 12 | [CTenantKey]?: string; 13 | [CWithHelpdeskAuthorization]?: boolean; 14 | [CWithUserAccessToken]?: string; 15 | }; 16 | params?: Record; 17 | data?: Record; 18 | headers?: Record; 19 | path?: Record; 20 | } 21 | 22 | export interface IClientParams { 23 | appId: string; 24 | appSecret: string; 25 | domain?: Domain | string; 26 | loggerLevel?: LoggerLevel; 27 | logger?: Logger; 28 | cache?: Cache; 29 | disableTokenCache?: boolean; 30 | appType?: AppType; 31 | helpDeskId?: string; 32 | helpDeskToken?: string; 33 | httpInstance?: HttpInstance; 34 | } 35 | 36 | export interface IPayload { 37 | params?: Record; 38 | data?: Record; 39 | headers?: Record; 40 | path?: Record; 41 | } 42 | -------------------------------------------------------------------------------- /client/user-access-token.ts: -------------------------------------------------------------------------------- 1 | import { CUserAccessToken } from '@node-sdk/consts'; 2 | import { mergeObject } from '@node-sdk/utils/merge-object'; 3 | import type { Client } from './client'; 4 | 5 | interface ITokenInfo { 6 | code?: string; 7 | token?: string; 8 | refreshToken?: string; 9 | expiredTime?: number; 10 | } 11 | 12 | export class UserAccessToken { 13 | client: Client; 14 | 15 | constructor(params: { client: Client }) { 16 | this.client = params.client; 17 | } 18 | 19 | private getCacheKey(key: string, options: { namespace?: string }) { 20 | const namespace = options?.namespace || this.client.appId; 21 | return `${namespace}/${CUserAccessToken.toString()}/${key}`; 22 | } 23 | 24 | // the unit of time is seconds 25 | private calibrateTime(time?: number) { 26 | // Due to the time-consuming network, the time needs to be 3 minutes earlier 27 | return new Date().getTime() + (time || 0) * 1000 - 3 * 60 * 1000; 28 | } 29 | 30 | async initWithCode(key2Code: Record, options?: { namespace?: string }) { 31 | const key2Info: Record = {}; 32 | for (const [key, code] of Object.entries(key2Code)) { 33 | const oidcAccessInfo = await this.client.authen.oidcAccessToken.create({ 34 | data: { 35 | grant_type: 'authorization_code', 36 | code 37 | } 38 | }); 39 | 40 | if (oidcAccessInfo.code !== 0) { 41 | // @ts-ignore 42 | this.client.logger.error('init user access token error', key, oidcAccessInfo.msg || oidcAccessInfo.message); 43 | continue; 44 | } 45 | // code expired 46 | if (!oidcAccessInfo.data) { 47 | this.client.logger.error('user access code expired', key, code); 48 | continue; 49 | } 50 | 51 | key2Info[key] = { 52 | code, 53 | token: oidcAccessInfo.data.access_token, 54 | refreshToken: oidcAccessInfo.data.refresh_token, 55 | expiredTime: this.calibrateTime(oidcAccessInfo.data.expires_in) 56 | } 57 | } 58 | await this.update(key2Info, { namespace: options?.namespace}); 59 | return key2Info; 60 | } 61 | 62 | async update(key2Info: Record, options?: { namespace?: string }) { 63 | for (const [key, info] of Object.entries(key2Info)) { 64 | const cacheKey = this.getCacheKey(key, { namespace: options?.namespace}); 65 | const cacheValue = await this.client.cache.get(cacheKey) || {}; 66 | 67 | const { code, token, refreshToken, expiredTime } = info; 68 | const targetValue = mergeObject(cacheValue, { code, token, refreshToken, expiredTime }); 69 | 70 | await this.client.cache.set(cacheKey, targetValue, Infinity); 71 | } 72 | } 73 | 74 | async get(key: string, options?: { namespace?: string }) { 75 | const cacheKey = this.getCacheKey(key, { namespace: options?.namespace}); 76 | const cacheInfo = await this.client.cache.get(cacheKey); 77 | 78 | // cacheInfo是否存在 79 | if (!cacheInfo) { 80 | this.client.logger.error('user access token needs to be initialized or updated first'); 81 | return; 82 | } 83 | 84 | const { token, code, refreshToken, expiredTime } = cacheInfo; 85 | // step1 token存在且未过期 86 | if (token && expiredTime && expiredTime - new Date().getTime() > 0) { 87 | return token; 88 | } 89 | 90 | // step2 refresh token存在,刷新token 91 | if (refreshToken) { 92 | const refreshAccessInfo = await this.client.authen.oidcRefreshAccessToken.create({ 93 | data: { 94 | grant_type: 'refresh_token', 95 | refresh_token: refreshToken 96 | } 97 | }); 98 | 99 | if (refreshAccessInfo.code === 0 && refreshAccessInfo.data) { 100 | await this.update({ 101 | [key]: { 102 | token: refreshAccessInfo.data.access_token, 103 | refreshToken: refreshAccessInfo.data.refresh_token, 104 | expiredTime: this.calibrateTime(refreshAccessInfo.data.expires_in) 105 | } 106 | }); 107 | 108 | return refreshAccessInfo.data.access_token; 109 | } else { 110 | this.client.logger.error('get user access token by refresh token failed.', refreshAccessInfo.msg); 111 | return; 112 | } 113 | } 114 | 115 | // step3 code存在的话,用code重新获取 116 | if (code) { 117 | const oidcAccessInfo = await this.client.authen.oidcAccessToken.create({ 118 | data: { 119 | grant_type: "authorization_code", 120 | code: code 121 | } 122 | }); 123 | 124 | if (oidcAccessInfo.code === 0 && oidcAccessInfo.data) { 125 | await this.update({ 126 | [key]: { 127 | token: oidcAccessInfo.data.access_token, 128 | refreshToken: oidcAccessInfo.data.refresh_token, 129 | expiredTime: this.calibrateTime(oidcAccessInfo.data.expires_in) 130 | } 131 | }) 132 | } else { 133 | this.client.logger.error('get user access token by code failed.', oidcAccessInfo.msg); 134 | } 135 | } 136 | 137 | // step4 重试完毕没结果后,返回undefine 138 | return; 139 | } 140 | } 141 | 142 | -------------------------------------------------------------------------------- /client/utils/__tests__/format-errors.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from '@node-sdk/http'; 2 | import { formatErrors } from '../format-errors'; 3 | 4 | describe('formatErrors', () => { 5 | test('format axios error', () => { 6 | const error = new AxiosError( 7 | 'message', 8 | '400', 9 | { 10 | data: 'config-data', 11 | url: 'config-url', 12 | params: 'config-params', 13 | method: 'config-method', 14 | }, 15 | { 16 | protocol: 'request-protocol', 17 | host: 'request-host', 18 | path: 'request-path', 19 | method: 'request-method', 20 | }, 21 | { 22 | headers: {}, 23 | config: {}, 24 | data: 'response-data', 25 | status: 400, 26 | statusText: 'response-statusText', 27 | } 28 | ); 29 | 30 | expect(formatErrors(error)).toEqual([ 31 | { 32 | message: 'message', 33 | config: { 34 | data: 'config-data', 35 | url: 'config-url', 36 | params: 'config-params', 37 | method: 'config-method', 38 | }, 39 | request: { 40 | protocol: 'request-protocol', 41 | host: 'request-host', 42 | path: 'request-path', 43 | method: 'request-method', 44 | }, 45 | response: { 46 | data: 'response-data', 47 | status: 400, 48 | statusText: 'response-statusText', 49 | }, 50 | }, 51 | 'response-data', 52 | ]); 53 | }); 54 | 55 | test('format right', () => { 56 | expect(formatErrors({ a: 'a' })).toEqual([{ a: 'a' }]); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /client/utils/format-errors.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from '@node-sdk/http'; 2 | import { pick } from '@node-sdk/utils/pick'; 3 | 4 | export const formatErrors = (e: any) => { 5 | if (e instanceof AxiosError) { 6 | const { message, response, request, config } = pick(e, [ 7 | 'message', 8 | 'response', 9 | 'request', 10 | 'config', 11 | ]); 12 | 13 | const filteredErrorInfo = { 14 | message, 15 | config: pick(config, ['data', 'url', 'params', 'method']), 16 | request: pick(request, ['protocol', 'host', 'path', 'method']), 17 | response: pick(response, ['data', 'status', 'statusText']), 18 | }; 19 | 20 | const errors = [filteredErrorInfo]; 21 | const specificError = e?.response?.data; 22 | if (specificError) { 23 | errors.push({ 24 | ...specificError, 25 | ...(specificError.error ? specificError.error : {}) 26 | }); 27 | } 28 | return errors; 29 | } 30 | 31 | return [e]; 32 | }; 33 | -------------------------------------------------------------------------------- /client/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './format-errors'; 2 | -------------------------------------------------------------------------------- /code-gen/projects/aweme_ecosystem.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import authen from "./authen"; 13 | 14 | // auto gen 15 | export default abstract class Client extends authen { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | aweme_ecosystem = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/comment_sdk.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import cardkit from "./cardkit"; 13 | 14 | // auto gen 15 | export default abstract class Client extends cardkit { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | comment_sdk = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/content_check.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import contact from "./contact"; 13 | 14 | // auto gen 15 | export default abstract class Client extends contact { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | content_check = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/contract.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import content_check from "./content_check"; 13 | 14 | // auto gen 15 | export default abstract class Client extends content_check { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | contract = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/docs.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import docs_tool from "./docs_tool"; 13 | 14 | // auto gen 15 | export default abstract class Client extends docs_tool { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | docs = { 35 | v1: { 36 | /** 37 | * content 38 | */ 39 | content: { 40 | /** 41 | * {@link https://open.feishu.cn/api-explorer?project=docs&resource=content&apiName=get&version=v1 click to debug } 42 | * 43 | * {@link https://open.feishu.cn/api-explorer?from=op_doc_tab&apiName=get&project=docs&resource=content&version=v1 document } 44 | * 45 | * 获取云文档正文内容 46 | */ 47 | get: async ( 48 | payload?: { 49 | params: { 50 | doc_token: string; 51 | doc_type: "docx"; 52 | content_type: "markdown"; 53 | lang?: "zh" | "en" | "ja"; 54 | }; 55 | }, 56 | options?: IRequestOptions 57 | ) => { 58 | const { headers, params, data, path } = 59 | await this.formatPayload(payload, options); 60 | 61 | return this.httpInstance 62 | .request< 63 | any, 64 | { 65 | code?: number; 66 | msg?: string; 67 | data?: { content?: string }; 68 | } 69 | >({ 70 | url: fillApiPath( 71 | `${this.domain}/open-apis/docs/v1/content`, 72 | path 73 | ), 74 | method: "GET", 75 | data, 76 | params, 77 | headers, 78 | paramsSerializer: (params) => 79 | stringify(params, { arrayFormat: "repeat" }), 80 | }) 81 | .catch((e) => { 82 | this.logger.error(formatErrors(e)); 83 | throw e; 84 | }); 85 | }, 86 | }, 87 | }, 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /code-gen/projects/docs_tool.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import directory from "./directory"; 13 | 14 | // auto gen 15 | export default abstract class Client extends directory { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | docs_tool = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/edu.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import drive from "./drive"; 13 | 14 | // auto gen 15 | export default abstract class Client extends drive { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | edu = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/elearning.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import ehr from "./ehr"; 13 | 14 | // auto gen 15 | export default abstract class Client extends ehr { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | elearning = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/exam.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import event from "./event"; 13 | 14 | // auto gen 15 | export default abstract class Client extends event { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | exam = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/face_detection.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import exam from "./exam"; 13 | 14 | // auto gen 15 | export default abstract class Client extends exam { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | face_detection = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/feelgood.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import face_detection from "./face_detection"; 13 | 14 | // auto gen 15 | export default abstract class Client extends face_detection { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | feelgood = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/human_authentication.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import hire from "./hire"; 13 | 14 | // auto gen 15 | export default abstract class Client extends hire { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | * 实名认证 33 | */ 34 | human_authentication = { 35 | /** 36 | * 实名认证 37 | */ 38 | identity: { 39 | /** 40 | * {@link https://open.feishu.cn/api-explorer?project=human_authentication&resource=identity&apiName=create&version=v1 click to debug } 41 | * 42 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/human_authentication-v1/identity/create document } 43 | * 44 | * 录入身份信息 45 | * 46 | * 该接口用于录入实名认证的身份信息,在唤起有源活体认证前,需要使用该接口进行实名认证。 47 | * 48 | * 实名认证接口会有计费管理,接入前请联系飞书开放平台工作人员,邮箱:openplatform@bytedance.com。;;仅通过计费申请的应用,才能在[开发者后台](https://open.feishu.cn/app)查找并申请该接口的权限。 49 | */ 50 | create: async ( 51 | payload?: { 52 | data: { 53 | identity_name: string; 54 | identity_code: string; 55 | mobile?: string; 56 | }; 57 | params: { 58 | user_id: string; 59 | user_id_type?: "open_id" | "user_id" | "union_id"; 60 | }; 61 | }, 62 | options?: IRequestOptions 63 | ) => { 64 | const { headers, params, data, path } = 65 | await this.formatPayload(payload, options); 66 | 67 | return this.httpInstance 68 | .request< 69 | any, 70 | { 71 | code?: number; 72 | msg?: string; 73 | data?: { verify_uid: string }; 74 | } 75 | >({ 76 | url: fillApiPath( 77 | `${this.domain}/open-apis/human_authentication/v1/identities`, 78 | path 79 | ), 80 | method: "POST", 81 | data, 82 | params, 83 | headers, 84 | paramsSerializer: (params) => 85 | stringify(params, { arrayFormat: "repeat" }), 86 | }) 87 | .catch((e) => { 88 | this.logger.error(formatErrors(e)); 89 | throw e; 90 | }); 91 | }, 92 | }, 93 | v1: { 94 | /** 95 | * 实名认证 96 | */ 97 | identity: { 98 | /** 99 | * {@link https://open.feishu.cn/api-explorer?project=human_authentication&resource=identity&apiName=create&version=v1 click to debug } 100 | * 101 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/human_authentication-v1/identity/create document } 102 | * 103 | * 录入身份信息 104 | * 105 | * 该接口用于录入实名认证的身份信息,在唤起有源活体认证前,需要使用该接口进行实名认证。 106 | * 107 | * 实名认证接口会有计费管理,接入前请联系飞书开放平台工作人员,邮箱:openplatform@bytedance.com。;;仅通过计费申请的应用,才能在[开发者后台](https://open.feishu.cn/app)查找并申请该接口的权限。 108 | */ 109 | create: async ( 110 | payload?: { 111 | data: { 112 | identity_name: string; 113 | identity_code: string; 114 | mobile?: string; 115 | }; 116 | params: { 117 | user_id: string; 118 | user_id_type?: "open_id" | "user_id" | "union_id"; 119 | }; 120 | }, 121 | options?: IRequestOptions 122 | ) => { 123 | const { headers, params, data, path } = 124 | await this.formatPayload(payload, options); 125 | 126 | return this.httpInstance 127 | .request< 128 | any, 129 | { 130 | code?: number; 131 | msg?: string; 132 | data?: { verify_uid: string }; 133 | } 134 | >({ 135 | url: fillApiPath( 136 | `${this.domain}/open-apis/human_authentication/v1/identities`, 137 | path 138 | ), 139 | method: "POST", 140 | data, 141 | params, 142 | headers, 143 | paramsSerializer: (params) => 144 | stringify(params, { arrayFormat: "repeat" }), 145 | }) 146 | .catch((e) => { 147 | this.logger.error(formatErrors(e)); 148 | throw e; 149 | }); 150 | }, 151 | }, 152 | }, 153 | }; 154 | } 155 | -------------------------------------------------------------------------------- /code-gen/projects/meeting_room.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import mdm from "./mdm"; 13 | 14 | // auto gen 15 | export default abstract class Client extends mdm { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | meeting_room = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/minutes.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import meeting_room from "./meeting_room"; 13 | 14 | // auto gen 15 | export default abstract class Client extends meeting_room { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | minutes = { 35 | v1: { 36 | /** 37 | * minute 38 | */ 39 | minute: { 40 | /** 41 | * {@link https://open.feishu.cn/api-explorer?project=minutes&resource=minute&apiName=get&version=v1 click to debug } 42 | * 43 | * {@link https://open.feishu.cn/api-explorer?from=op_doc_tab&apiName=get&project=minutes&resource=minute&version=v1 document } 44 | * 45 | * 获取妙记的基础概括信息 46 | */ 47 | get: async ( 48 | payload?: { 49 | params?: { 50 | user_id_type?: "user_id" | "union_id" | "open_id"; 51 | }; 52 | path: { minute_token: string }; 53 | }, 54 | options?: IRequestOptions 55 | ) => { 56 | const { headers, params, data, path } = 57 | await this.formatPayload(payload, options); 58 | 59 | return this.httpInstance 60 | .request< 61 | any, 62 | { 63 | code?: number; 64 | msg?: string; 65 | data?: { 66 | minute?: { 67 | token?: string; 68 | owner_id?: string; 69 | create_time?: string; 70 | title?: string; 71 | cover?: string; 72 | duration?: string; 73 | url?: string; 74 | }; 75 | }; 76 | } 77 | >({ 78 | url: fillApiPath( 79 | `${this.domain}/open-apis/minutes/v1/minutes/:minute_token`, 80 | path 81 | ), 82 | method: "GET", 83 | data, 84 | params, 85 | headers, 86 | paramsSerializer: (params) => 87 | stringify(params, { arrayFormat: "repeat" }), 88 | }) 89 | .catch((e) => { 90 | this.logger.error(formatErrors(e)); 91 | throw e; 92 | }); 93 | }, 94 | }, 95 | /** 96 | * minute.media 97 | */ 98 | minuteMedia: { 99 | /** 100 | * {@link https://open.feishu.cn/api-explorer?project=minutes&resource=minute.media&apiName=get&version=v1 click to debug } 101 | * 102 | * {@link https://open.feishu.cn/api-explorer?from=op_doc_tab&apiName=get&project=minutes&resource=minute.media&version=v1 document } 103 | * 104 | * 获取妙记的音视频文件 105 | */ 106 | get: async ( 107 | payload?: { 108 | path: { minute_token: string }; 109 | }, 110 | options?: IRequestOptions 111 | ) => { 112 | const { headers, params, data, path } = 113 | await this.formatPayload(payload, options); 114 | 115 | return this.httpInstance 116 | .request< 117 | any, 118 | { 119 | code?: number; 120 | msg?: string; 121 | data?: { download_url?: string }; 122 | } 123 | >({ 124 | url: fillApiPath( 125 | `${this.domain}/open-apis/minutes/v1/minutes/:minute_token/media`, 126 | path 127 | ), 128 | method: "GET", 129 | data, 130 | params, 131 | headers, 132 | paramsSerializer: (params) => 133 | stringify(params, { arrayFormat: "repeat" }), 134 | }) 135 | .catch((e) => { 136 | this.logger.error(formatErrors(e)); 137 | throw e; 138 | }); 139 | }, 140 | }, 141 | /** 142 | * minute.statistics 143 | */ 144 | minuteStatistics: { 145 | /** 146 | * {@link https://open.feishu.cn/api-explorer?project=minutes&resource=minute.statistics&apiName=get&version=v1 click to debug } 147 | * 148 | * {@link https://open.feishu.cn/api-explorer?from=op_doc_tab&apiName=get&project=minutes&resource=minute.statistics&version=v1 document } 149 | * 150 | * 获取妙记的访问统计数据 151 | */ 152 | get: async ( 153 | payload?: { 154 | params?: { 155 | user_id_type?: "user_id" | "union_id" | "open_id"; 156 | }; 157 | path: { minute_token: string }; 158 | }, 159 | options?: IRequestOptions 160 | ) => { 161 | const { headers, params, data, path } = 162 | await this.formatPayload(payload, options); 163 | 164 | return this.httpInstance 165 | .request< 166 | any, 167 | { 168 | code?: number; 169 | msg?: string; 170 | data?: { 171 | statistics?: { 172 | user_view_count?: string; 173 | page_view_count?: string; 174 | user_view_list?: Array<{ 175 | user_id?: string; 176 | view_time?: string; 177 | }>; 178 | }; 179 | }; 180 | } 181 | >({ 182 | url: fillApiPath( 183 | `${this.domain}/open-apis/minutes/v1/minutes/:minute_token/statistics`, 184 | path 185 | ), 186 | method: "GET", 187 | data, 188 | params, 189 | headers, 190 | paramsSerializer: (params) => 191 | stringify(params, { arrayFormat: "repeat" }), 192 | }) 193 | .catch((e) => { 194 | this.logger.error(formatErrors(e)); 195 | throw e; 196 | }); 197 | }, 198 | }, 199 | /** 200 | * minute.transcript 201 | */ 202 | minuteTranscript: { 203 | /** 204 | * {@link https://open.feishu.cn/api-explorer?project=minutes&resource=minute.transcript&apiName=get&version=v1 click to debug } 205 | * 206 | * {@link https://open.feishu.cn/api-explorer?from=op_doc_tab&apiName=get&project=minutes&resource=minute.transcript&version=v1 document } 207 | * 208 | * 获取妙记的对话文本,成功时返回文件二进制流 209 | */ 210 | get: async ( 211 | payload?: { 212 | params?: { 213 | need_speaker?: boolean; 214 | need_timestamp?: boolean; 215 | file_format?: string; 216 | }; 217 | path: { minute_token: string }; 218 | }, 219 | options?: IRequestOptions 220 | ) => { 221 | const { headers, params, data, path } = 222 | await this.formatPayload(payload, options); 223 | 224 | const res = await this.httpInstance 225 | .request({ 226 | url: fillApiPath( 227 | `${this.domain}/open-apis/minutes/v1/minutes/:minute_token/transcript`, 228 | path 229 | ), 230 | method: "GET", 231 | headers, 232 | data, 233 | params, 234 | responseType: "stream", 235 | paramsSerializer: (params) => 236 | stringify(params, { arrayFormat: "repeat" }), 237 | }) 238 | .catch((e) => { 239 | this.logger.error(formatErrors(e)); 240 | throw e; 241 | }); 242 | 243 | const checkIsReadable = () => { 244 | const consumedError = 245 | "The stream has already been consumed"; 246 | if (!res.readable) { 247 | this.logger.error(consumedError); 248 | throw new Error(consumedError); 249 | } 250 | }; 251 | 252 | return { 253 | writeFile: async (filePath: string) => { 254 | checkIsReadable(); 255 | return new Promise((resolve, reject) => { 256 | const writableStream = 257 | fs.createWriteStream(filePath); 258 | writableStream.on("finish", () => { 259 | resolve(filePath); 260 | }); 261 | writableStream.on("error", (e) => { 262 | reject(e); 263 | }); 264 | res.pipe(writableStream); 265 | }); 266 | }, 267 | getReadableStream: () => { 268 | checkIsReadable(); 269 | return res as Readable; 270 | }, 271 | }; 272 | }, 273 | }, 274 | }, 275 | }; 276 | } 277 | -------------------------------------------------------------------------------- /code-gen/projects/moments.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import minutes from "./minutes"; 13 | 14 | // auto gen 15 | export default abstract class Client extends minutes { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | moments = { 35 | /** 36 | * post 37 | */ 38 | post: { 39 | /** 40 | * {@link https://open.feishu.cn/api-explorer?project=moments&resource=post&apiName=get&version=v1 click to debug } 41 | * 42 | * {@link https://open.feishu.cn/api-explorer?from=op_doc_tab&apiName=get&project=moments&resource=post&version=v1 document } 43 | */ 44 | get: async ( 45 | payload?: { 46 | params?: { 47 | user_id_type?: "user_id" | "union_id" | "open_id"; 48 | }; 49 | path: { post_id: string }; 50 | }, 51 | options?: IRequestOptions 52 | ) => { 53 | const { headers, params, data, path } = 54 | await this.formatPayload(payload, options); 55 | 56 | return this.httpInstance 57 | .request< 58 | any, 59 | { 60 | code?: number; 61 | msg?: string; 62 | data?: { 63 | post?: { 64 | user_id?: string; 65 | content: string; 66 | image_key_list?: Array; 67 | media_file_token?: string; 68 | comment_count?: number; 69 | reaction_set?: { 70 | reactions?: Array<{ 71 | type?: string; 72 | count?: number; 73 | }>; 74 | total_count?: number; 75 | }; 76 | id?: string; 77 | create_time?: string; 78 | media_cover_image_key?: string; 79 | category_ids?: Array; 80 | link?: string; 81 | user_type?: number; 82 | dislike_count?: number; 83 | }; 84 | }; 85 | } 86 | >({ 87 | url: fillApiPath( 88 | `${this.domain}/open-apis/moments/v1/posts/:post_id`, 89 | path 90 | ), 91 | method: "GET", 92 | data, 93 | params, 94 | headers, 95 | paramsSerializer: (params) => 96 | stringify(params, { arrayFormat: "repeat" }), 97 | }) 98 | .catch((e) => { 99 | this.logger.error(formatErrors(e)); 100 | throw e; 101 | }); 102 | }, 103 | }, 104 | v1: { 105 | /** 106 | * post 107 | */ 108 | post: { 109 | /** 110 | * {@link https://open.feishu.cn/api-explorer?project=moments&resource=post&apiName=get&version=v1 click to debug } 111 | * 112 | * {@link https://open.feishu.cn/api-explorer?from=op_doc_tab&apiName=get&project=moments&resource=post&version=v1 document } 113 | */ 114 | get: async ( 115 | payload?: { 116 | params?: { 117 | user_id_type?: "user_id" | "union_id" | "open_id"; 118 | }; 119 | path: { post_id: string }; 120 | }, 121 | options?: IRequestOptions 122 | ) => { 123 | const { headers, params, data, path } = 124 | await this.formatPayload(payload, options); 125 | 126 | return this.httpInstance 127 | .request< 128 | any, 129 | { 130 | code?: number; 131 | msg?: string; 132 | data?: { 133 | post?: { 134 | user_id?: string; 135 | content: string; 136 | image_key_list?: Array; 137 | media_file_token?: string; 138 | comment_count?: number; 139 | reaction_set?: { 140 | reactions?: Array<{ 141 | type?: string; 142 | count?: number; 143 | }>; 144 | total_count?: number; 145 | }; 146 | id?: string; 147 | create_time?: string; 148 | media_cover_image_key?: string; 149 | category_ids?: Array; 150 | link?: string; 151 | user_type?: number; 152 | dislike_count?: number; 153 | }; 154 | }; 155 | } 156 | >({ 157 | url: fillApiPath( 158 | `${this.domain}/open-apis/moments/v1/posts/:post_id`, 159 | path 160 | ), 161 | method: "GET", 162 | data, 163 | params, 164 | headers, 165 | paramsSerializer: (params) => 166 | stringify(params, { arrayFormat: "repeat" }), 167 | }) 168 | .catch((e) => { 169 | this.logger.error(formatErrors(e)); 170 | throw e; 171 | }); 172 | }, 173 | }, 174 | }, 175 | }; 176 | } 177 | -------------------------------------------------------------------------------- /code-gen/projects/optical_char_recognition.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import okr from "./okr"; 13 | 14 | // auto gen 15 | export default abstract class Client extends okr { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | * AI能力 33 | */ 34 | optical_char_recognition = { 35 | /** 36 | * 图片识别 37 | */ 38 | image: { 39 | /** 40 | * {@link https://open.feishu.cn/api-explorer?project=optical_char_recognition&resource=image&apiName=basic_recognize&version=v1 click to debug } 41 | * 42 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/ai/optical_char_recognition-v1/image/basic_recognize document } 43 | * 44 | * 基础图片识别 (OCR) 45 | * 46 | * 可识别图片中的文字,按图片中的区域划分,分段返回文本列表 47 | * 48 | * 单租户限流:20QPS,同租户下的应用没有限流,共享本租户的 20QPS 限流 49 | */ 50 | basicRecognize: async ( 51 | payload?: { 52 | data?: { image?: string }; 53 | }, 54 | options?: IRequestOptions 55 | ) => { 56 | const { headers, params, data, path } = 57 | await this.formatPayload(payload, options); 58 | 59 | return this.httpInstance 60 | .request< 61 | any, 62 | { 63 | code?: number; 64 | msg?: string; 65 | data?: { text_list: Array }; 66 | } 67 | >({ 68 | url: fillApiPath( 69 | `${this.domain}/open-apis/optical_char_recognition/v1/image/basic_recognize`, 70 | path 71 | ), 72 | method: "POST", 73 | data, 74 | params, 75 | headers, 76 | paramsSerializer: (params) => 77 | stringify(params, { arrayFormat: "repeat" }), 78 | }) 79 | .catch((e) => { 80 | this.logger.error(formatErrors(e)); 81 | throw e; 82 | }); 83 | }, 84 | }, 85 | v1: { 86 | /** 87 | * 图片识别 88 | */ 89 | image: { 90 | /** 91 | * {@link https://open.feishu.cn/api-explorer?project=optical_char_recognition&resource=image&apiName=basic_recognize&version=v1 click to debug } 92 | * 93 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/ai/optical_char_recognition-v1/image/basic_recognize document } 94 | * 95 | * 基础图片识别 (OCR) 96 | * 97 | * 可识别图片中的文字,按图片中的区域划分,分段返回文本列表 98 | * 99 | * 单租户限流:20QPS,同租户下的应用没有限流,共享本租户的 20QPS 限流 100 | */ 101 | basicRecognize: async ( 102 | payload?: { 103 | data?: { image?: string }; 104 | }, 105 | options?: IRequestOptions 106 | ) => { 107 | const { headers, params, data, path } = 108 | await this.formatPayload(payload, options); 109 | 110 | return this.httpInstance 111 | .request< 112 | any, 113 | { 114 | code?: number; 115 | msg?: string; 116 | data?: { text_list: Array }; 117 | } 118 | >({ 119 | url: fillApiPath( 120 | `${this.domain}/open-apis/optical_char_recognition/v1/image/basic_recognize`, 121 | path 122 | ), 123 | method: "POST", 124 | data, 125 | params, 126 | headers, 127 | paramsSerializer: (params) => 128 | stringify(params, { arrayFormat: "repeat" }), 129 | }) 130 | .catch((e) => { 131 | this.logger.error(formatErrors(e)); 132 | throw e; 133 | }); 134 | }, 135 | }, 136 | }, 137 | }; 138 | } 139 | -------------------------------------------------------------------------------- /code-gen/projects/passport.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import optical_char_recognition from "./optical_char_recognition"; 13 | 14 | // auto gen 15 | export default abstract class Client extends optical_char_recognition { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | * 帐号 33 | */ 34 | passport = { 35 | /** 36 | * 登录态 37 | */ 38 | session: { 39 | /** 40 | * {@link https://open.feishu.cn/api-explorer?project=passport&resource=session&apiName=logout&version=v1 click to debug } 41 | * 42 | * {@link https://open.feishu.cn/api-explorer?from=op_doc_tab&apiName=logout&project=passport&resource=session&version=v1 document } 43 | */ 44 | logout: async ( 45 | payload?: { 46 | data: { 47 | idp_credential_id?: string; 48 | logout_type: number; 49 | terminal_type?: Array; 50 | user_id?: string; 51 | logout_reason?: number; 52 | sid?: string; 53 | }; 54 | params?: { 55 | user_id_type?: "open_id" | "union_id" | "user_id"; 56 | }; 57 | }, 58 | options?: IRequestOptions 59 | ) => { 60 | const { headers, params, data, path } = 61 | await this.formatPayload(payload, options); 62 | 63 | return this.httpInstance 64 | .request({ 65 | url: fillApiPath( 66 | `${this.domain}/open-apis/passport/v1/sessions/logout`, 67 | path 68 | ), 69 | method: "POST", 70 | data, 71 | params, 72 | headers, 73 | paramsSerializer: (params) => 74 | stringify(params, { arrayFormat: "repeat" }), 75 | }) 76 | .catch((e) => { 77 | this.logger.error(formatErrors(e)); 78 | throw e; 79 | }); 80 | }, 81 | /** 82 | * {@link https://open.feishu.cn/api-explorer?project=passport&resource=session&apiName=query&version=v1 click to debug } 83 | * 84 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/passport-v1/session/query document } 85 | * 86 | * 批量获取用户登录信息(脱敏) 87 | * 88 | * 该接口用于查询用户的登录信息 89 | */ 90 | query: async ( 91 | payload?: { 92 | data?: { user_ids?: Array }; 93 | params?: { 94 | user_id_type?: "open_id" | "union_id" | "user_id"; 95 | }; 96 | }, 97 | options?: IRequestOptions 98 | ) => { 99 | const { headers, params, data, path } = 100 | await this.formatPayload(payload, options); 101 | 102 | return this.httpInstance 103 | .request< 104 | any, 105 | { 106 | code?: number; 107 | msg?: string; 108 | data?: { 109 | mask_sessions?: Array<{ 110 | create_time?: string; 111 | terminal_type?: number; 112 | user_id?: string; 113 | sid?: string; 114 | }>; 115 | }; 116 | } 117 | >({ 118 | url: fillApiPath( 119 | `${this.domain}/open-apis/passport/v1/sessions/query`, 120 | path 121 | ), 122 | method: "POST", 123 | data, 124 | params, 125 | headers, 126 | paramsSerializer: (params) => 127 | stringify(params, { arrayFormat: "repeat" }), 128 | }) 129 | .catch((e) => { 130 | this.logger.error(formatErrors(e)); 131 | throw e; 132 | }); 133 | }, 134 | }, 135 | v1: { 136 | /** 137 | * 登录态 138 | */ 139 | session: { 140 | /** 141 | * {@link https://open.feishu.cn/api-explorer?project=passport&resource=session&apiName=logout&version=v1 click to debug } 142 | * 143 | * {@link https://open.feishu.cn/api-explorer?from=op_doc_tab&apiName=logout&project=passport&resource=session&version=v1 document } 144 | */ 145 | logout: async ( 146 | payload?: { 147 | data: { 148 | idp_credential_id?: string; 149 | logout_type: number; 150 | terminal_type?: Array; 151 | user_id?: string; 152 | logout_reason?: number; 153 | sid?: string; 154 | }; 155 | params?: { 156 | user_id_type?: "open_id" | "union_id" | "user_id"; 157 | }; 158 | }, 159 | options?: IRequestOptions 160 | ) => { 161 | const { headers, params, data, path } = 162 | await this.formatPayload(payload, options); 163 | 164 | return this.httpInstance 165 | .request< 166 | any, 167 | { code?: number; msg?: string; data?: {} } 168 | >({ 169 | url: fillApiPath( 170 | `${this.domain}/open-apis/passport/v1/sessions/logout`, 171 | path 172 | ), 173 | method: "POST", 174 | data, 175 | params, 176 | headers, 177 | paramsSerializer: (params) => 178 | stringify(params, { arrayFormat: "repeat" }), 179 | }) 180 | .catch((e) => { 181 | this.logger.error(formatErrors(e)); 182 | throw e; 183 | }); 184 | }, 185 | /** 186 | * {@link https://open.feishu.cn/api-explorer?project=passport&resource=session&apiName=query&version=v1 click to debug } 187 | * 188 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/passport-v1/session/query document } 189 | * 190 | * 批量获取用户登录信息(脱敏) 191 | * 192 | * 该接口用于查询用户的登录信息 193 | */ 194 | query: async ( 195 | payload?: { 196 | data?: { user_ids?: Array }; 197 | params?: { 198 | user_id_type?: "open_id" | "union_id" | "user_id"; 199 | }; 200 | }, 201 | options?: IRequestOptions 202 | ) => { 203 | const { headers, params, data, path } = 204 | await this.formatPayload(payload, options); 205 | 206 | return this.httpInstance 207 | .request< 208 | any, 209 | { 210 | code?: number; 211 | msg?: string; 212 | data?: { 213 | mask_sessions?: Array<{ 214 | create_time?: string; 215 | terminal_type?: number; 216 | user_id?: string; 217 | sid?: string; 218 | }>; 219 | }; 220 | } 221 | >({ 222 | url: fillApiPath( 223 | `${this.domain}/open-apis/passport/v1/sessions/query`, 224 | path 225 | ), 226 | method: "POST", 227 | data, 228 | params, 229 | headers, 230 | paramsSerializer: (params) => 231 | stringify(params, { arrayFormat: "repeat" }), 232 | }) 233 | .catch((e) => { 234 | this.logger.error(formatErrors(e)); 235 | throw e; 236 | }); 237 | }, 238 | }, 239 | }, 240 | }; 241 | } 242 | -------------------------------------------------------------------------------- /code-gen/projects/people_admin.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import payroll from "./payroll"; 13 | 14 | // auto gen 15 | export default abstract class Client extends payroll { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | people_admin = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/people_bytedance.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import people_admin from "./people_admin"; 13 | 14 | // auto gen 15 | export default abstract class Client extends people_admin { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | people_bytedance = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/search_in_app.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import report from "./report"; 13 | 14 | // auto gen 15 | export default abstract class Client extends report { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | search_in_app = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/security_and_compliance.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import search from "./search"; 13 | 14 | // auto gen 15 | export default abstract class Client extends search { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | security_and_compliance = { 35 | /** 36 | * openapi_log 37 | */ 38 | openapiLog: { 39 | /** 40 | * {@link https://open.feishu.cn/api-explorer?project=security_and_compliance&resource=openapi_log&apiName=list_data&version=v1 click to debug } 41 | * 42 | * {@link https://open.feishu.cn/api-explorer?from=op_doc_tab&apiName=list_data&project=security_and_compliance&resource=openapi_log&version=v1 document } 43 | */ 44 | listData: async ( 45 | payload?: { 46 | data?: { 47 | api_keys?: Array; 48 | start_time?: number; 49 | end_time?: number; 50 | app_id?: string; 51 | page_size?: number; 52 | page_token?: string; 53 | }; 54 | }, 55 | options?: IRequestOptions 56 | ) => { 57 | const { headers, params, data, path } = 58 | await this.formatPayload(payload, options); 59 | 60 | return this.httpInstance 61 | .request< 62 | any, 63 | { 64 | code?: number; 65 | msg?: string; 66 | data?: { 67 | items?: Array<{ 68 | id: string; 69 | api_key: string; 70 | event_time?: number; 71 | app_id?: string; 72 | ip?: string; 73 | log_detail?: { 74 | path?: string; 75 | method?: string; 76 | query_param?: string; 77 | payload?: string; 78 | status_code?: number; 79 | response?: string; 80 | }; 81 | }>; 82 | page_token?: string; 83 | has_more?: boolean; 84 | }; 85 | } 86 | >({ 87 | url: fillApiPath( 88 | `${this.domain}/open-apis/security_and_compliance/v1/openapi_logs/list_data`, 89 | path 90 | ), 91 | method: "POST", 92 | data, 93 | params, 94 | headers, 95 | paramsSerializer: (params) => 96 | stringify(params, { arrayFormat: "repeat" }), 97 | }) 98 | .catch((e) => { 99 | this.logger.error(formatErrors(e)); 100 | throw e; 101 | }); 102 | }, 103 | }, 104 | v1: { 105 | /** 106 | * openapi_log 107 | */ 108 | openapiLog: { 109 | /** 110 | * {@link https://open.feishu.cn/api-explorer?project=security_and_compliance&resource=openapi_log&apiName=list_data&version=v1 click to debug } 111 | * 112 | * {@link https://open.feishu.cn/api-explorer?from=op_doc_tab&apiName=list_data&project=security_and_compliance&resource=openapi_log&version=v1 document } 113 | */ 114 | listData: async ( 115 | payload?: { 116 | data?: { 117 | api_keys?: Array; 118 | start_time?: number; 119 | end_time?: number; 120 | app_id?: string; 121 | page_size?: number; 122 | page_token?: string; 123 | }; 124 | }, 125 | options?: IRequestOptions 126 | ) => { 127 | const { headers, params, data, path } = 128 | await this.formatPayload(payload, options); 129 | 130 | return this.httpInstance 131 | .request< 132 | any, 133 | { 134 | code?: number; 135 | msg?: string; 136 | data?: { 137 | items?: Array<{ 138 | id: string; 139 | api_key: string; 140 | event_time?: number; 141 | app_id?: string; 142 | ip?: string; 143 | log_detail?: { 144 | path?: string; 145 | method?: string; 146 | query_param?: string; 147 | payload?: string; 148 | status_code?: number; 149 | response?: string; 150 | }; 151 | }>; 152 | page_token?: string; 153 | has_more?: boolean; 154 | }; 155 | } 156 | >({ 157 | url: fillApiPath( 158 | `${this.domain}/open-apis/security_and_compliance/v1/openapi_logs/list_data`, 159 | path 160 | ), 161 | method: "POST", 162 | data, 163 | params, 164 | headers, 165 | paramsSerializer: (params) => 166 | stringify(params, { arrayFormat: "repeat" }), 167 | }) 168 | .catch((e) => { 169 | this.logger.error(formatErrors(e)); 170 | throw e; 171 | }); 172 | }, 173 | }, 174 | }, 175 | }; 176 | } 177 | -------------------------------------------------------------------------------- /code-gen/projects/speech_to_text.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import sheets from "./sheets"; 13 | 14 | // auto gen 15 | export default abstract class Client extends sheets { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | * AI能力 33 | */ 34 | speech_to_text = { 35 | /** 36 | * 语音识别 37 | */ 38 | speech: { 39 | /** 40 | * {@link https://open.feishu.cn/api-explorer?project=speech_to_text&resource=speech&apiName=file_recognize&version=v1 click to debug } 41 | * 42 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/ai/speech_to_text-v1/speech/file_recognize document } 43 | * 44 | * 语音文件识别 (ASR) 45 | * 46 | * 语音文件识别接口,上传整段语音文件进行一次性识别。接口适合 60 秒以内音频识别 47 | * 48 | * 单租户限流:20QPS,同租户下的应用没有限流,共享本租户的 20QPS 限流 49 | */ 50 | fileRecognize: async ( 51 | payload?: { 52 | data: { 53 | speech: { speech?: string; speech_key?: string }; 54 | config: { 55 | file_id: string; 56 | format: string; 57 | engine_type: string; 58 | }; 59 | }; 60 | }, 61 | options?: IRequestOptions 62 | ) => { 63 | const { headers, params, data, path } = 64 | await this.formatPayload(payload, options); 65 | 66 | return this.httpInstance 67 | .request< 68 | any, 69 | { 70 | code?: number; 71 | msg?: string; 72 | data?: { recognition_text: string }; 73 | } 74 | >({ 75 | url: fillApiPath( 76 | `${this.domain}/open-apis/speech_to_text/v1/speech/file_recognize`, 77 | path 78 | ), 79 | method: "POST", 80 | data, 81 | params, 82 | headers, 83 | paramsSerializer: (params) => 84 | stringify(params, { arrayFormat: "repeat" }), 85 | }) 86 | .catch((e) => { 87 | this.logger.error(formatErrors(e)); 88 | throw e; 89 | }); 90 | }, 91 | /** 92 | * {@link https://open.feishu.cn/api-explorer?project=speech_to_text&resource=speech&apiName=stream_recognize&version=v1 click to debug } 93 | * 94 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/ai/speech_to_text-v1/speech/stream_recognize document } 95 | * 96 | * 语音流式识别 (ASR) 97 | * 98 | * 语音流式接口,将整个音频文件分片进行传入模型。能够实时返回数据。建议每个音频分片的大小为 100-200ms 99 | * 100 | * 单租户限流:20 路(一个 stream_id 称为一路会话),同租户下的应用没有限流,共享本租户的 20路限流 101 | */ 102 | streamRecognize: async ( 103 | payload?: { 104 | data: { 105 | speech: { speech?: string; speech_key?: string }; 106 | config: { 107 | stream_id: string; 108 | sequence_id: number; 109 | action: number; 110 | format: string; 111 | engine_type: string; 112 | }; 113 | }; 114 | }, 115 | options?: IRequestOptions 116 | ) => { 117 | const { headers, params, data, path } = 118 | await this.formatPayload(payload, options); 119 | 120 | return this.httpInstance 121 | .request< 122 | any, 123 | { 124 | code?: number; 125 | msg?: string; 126 | data?: { 127 | stream_id: string; 128 | sequence_id: number; 129 | recognition_text: string; 130 | }; 131 | } 132 | >({ 133 | url: fillApiPath( 134 | `${this.domain}/open-apis/speech_to_text/v1/speech/stream_recognize`, 135 | path 136 | ), 137 | method: "POST", 138 | data, 139 | params, 140 | headers, 141 | paramsSerializer: (params) => 142 | stringify(params, { arrayFormat: "repeat" }), 143 | }) 144 | .catch((e) => { 145 | this.logger.error(formatErrors(e)); 146 | throw e; 147 | }); 148 | }, 149 | }, 150 | v1: { 151 | /** 152 | * 语音识别 153 | */ 154 | speech: { 155 | /** 156 | * {@link https://open.feishu.cn/api-explorer?project=speech_to_text&resource=speech&apiName=file_recognize&version=v1 click to debug } 157 | * 158 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/ai/speech_to_text-v1/speech/file_recognize document } 159 | * 160 | * 语音文件识别 (ASR) 161 | * 162 | * 语音文件识别接口,上传整段语音文件进行一次性识别。接口适合 60 秒以内音频识别 163 | * 164 | * 单租户限流:20QPS,同租户下的应用没有限流,共享本租户的 20QPS 限流 165 | */ 166 | fileRecognize: async ( 167 | payload?: { 168 | data: { 169 | speech: { speech?: string; speech_key?: string }; 170 | config: { 171 | file_id: string; 172 | format: string; 173 | engine_type: string; 174 | }; 175 | }; 176 | }, 177 | options?: IRequestOptions 178 | ) => { 179 | const { headers, params, data, path } = 180 | await this.formatPayload(payload, options); 181 | 182 | return this.httpInstance 183 | .request< 184 | any, 185 | { 186 | code?: number; 187 | msg?: string; 188 | data?: { recognition_text: string }; 189 | } 190 | >({ 191 | url: fillApiPath( 192 | `${this.domain}/open-apis/speech_to_text/v1/speech/file_recognize`, 193 | path 194 | ), 195 | method: "POST", 196 | data, 197 | params, 198 | headers, 199 | paramsSerializer: (params) => 200 | stringify(params, { arrayFormat: "repeat" }), 201 | }) 202 | .catch((e) => { 203 | this.logger.error(formatErrors(e)); 204 | throw e; 205 | }); 206 | }, 207 | /** 208 | * {@link https://open.feishu.cn/api-explorer?project=speech_to_text&resource=speech&apiName=stream_recognize&version=v1 click to debug } 209 | * 210 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/ai/speech_to_text-v1/speech/stream_recognize document } 211 | * 212 | * 语音流式识别 (ASR) 213 | * 214 | * 语音流式接口,将整个音频文件分片进行传入模型。能够实时返回数据。建议每个音频分片的大小为 100-200ms 215 | * 216 | * 单租户限流:20 路(一个 stream_id 称为一路会话),同租户下的应用没有限流,共享本租户的 20路限流 217 | */ 218 | streamRecognize: async ( 219 | payload?: { 220 | data: { 221 | speech: { speech?: string; speech_key?: string }; 222 | config: { 223 | stream_id: string; 224 | sequence_id: number; 225 | action: number; 226 | format: string; 227 | engine_type: string; 228 | }; 229 | }; 230 | }, 231 | options?: IRequestOptions 232 | ) => { 233 | const { headers, params, data, path } = 234 | await this.formatPayload(payload, options); 235 | 236 | return this.httpInstance 237 | .request< 238 | any, 239 | { 240 | code?: number; 241 | msg?: string; 242 | data?: { 243 | stream_id: string; 244 | sequence_id: number; 245 | recognition_text: string; 246 | }; 247 | } 248 | >({ 249 | url: fillApiPath( 250 | `${this.domain}/open-apis/speech_to_text/v1/speech/stream_recognize`, 251 | path 252 | ), 253 | method: "POST", 254 | data, 255 | params, 256 | headers, 257 | paramsSerializer: (params) => 258 | stringify(params, { arrayFormat: "repeat" }), 259 | }) 260 | .catch((e) => { 261 | this.logger.error(formatErrors(e)); 262 | throw e; 263 | }); 264 | }, 265 | }, 266 | }, 267 | }; 268 | } 269 | -------------------------------------------------------------------------------- /code-gen/projects/spend.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import speech_to_text from "./speech_to_text"; 13 | 14 | // auto gen 15 | export default abstract class Client extends speech_to_text { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | spend = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/sup_project.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import spend from "./spend"; 13 | 14 | // auto gen 15 | export default abstract class Client extends spend { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | sup_project = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/tenant.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import task from "./task"; 13 | 14 | // auto gen 15 | export default abstract class Client extends task { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | * 企业信息 33 | */ 34 | tenant = { 35 | /** 36 | * tenant.product_assign_info 37 | */ 38 | tenantProductAssignInfo: { 39 | /** 40 | * {@link https://open.feishu.cn/api-explorer?project=tenant&resource=tenant.product_assign_info&apiName=query&version=v2 click to debug } 41 | * 42 | * {@link https://open.feishu.cn/api-explorer?from=op_doc_tab&apiName=query&project=tenant&resource=tenant.product_assign_info&version=v2 document } 43 | */ 44 | query: async (payload?: {}, options?: IRequestOptions) => { 45 | const { headers, params, data, path } = 46 | await this.formatPayload(payload, options); 47 | 48 | return this.httpInstance 49 | .request< 50 | any, 51 | { 52 | code?: number; 53 | msg?: string; 54 | data?: { 55 | assign_info_list?: Array<{ 56 | subscription_id?: string; 57 | license_plan_key?: string; 58 | product_name?: string; 59 | i18n_name?: { 60 | zh_cn?: string; 61 | ja_jp?: string; 62 | en_us?: string; 63 | }; 64 | total_seats?: string; 65 | assigned_seats?: string; 66 | start_time?: string; 67 | end_time?: string; 68 | }>; 69 | }; 70 | } 71 | >({ 72 | url: fillApiPath( 73 | `${this.domain}/open-apis/tenant/v2/tenant/assign_info_list/query`, 74 | path 75 | ), 76 | method: "GET", 77 | data, 78 | params, 79 | headers, 80 | paramsSerializer: (params) => 81 | stringify(params, { arrayFormat: "repeat" }), 82 | }) 83 | .catch((e) => { 84 | this.logger.error(formatErrors(e)); 85 | throw e; 86 | }); 87 | }, 88 | }, 89 | /** 90 | * 企业信息 91 | */ 92 | tenant: { 93 | /** 94 | * {@link https://open.feishu.cn/api-explorer?project=tenant&resource=tenant&apiName=query&version=v2 click to debug } 95 | * 96 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/tenant-v2/tenant/query document } 97 | * 98 | * 获取企业信息 99 | * 100 | * 获取企业名称、企业编号等企业信息 101 | * 102 | * 如果ISV应用是企业创建时默认安装,并且180天内企业未打开或使用过此应用,则无法通过此接口获取到企业信息。 103 | */ 104 | query: async (payload?: {}, options?: IRequestOptions) => { 105 | const { headers, params, data, path } = 106 | await this.formatPayload(payload, options); 107 | 108 | return this.httpInstance 109 | .request< 110 | any, 111 | { 112 | code?: number; 113 | msg?: string; 114 | data?: { 115 | tenant?: { 116 | name: string; 117 | display_id: string; 118 | tenant_tag: number; 119 | tenant_key: string; 120 | avatar: { 121 | avatar_origin?: string; 122 | avatar_72?: string; 123 | avatar_240?: string; 124 | avatar_640?: string; 125 | }; 126 | domain?: string; 127 | }; 128 | }; 129 | } 130 | >({ 131 | url: fillApiPath( 132 | `${this.domain}/open-apis/tenant/v2/tenant/query`, 133 | path 134 | ), 135 | method: "GET", 136 | data, 137 | params, 138 | headers, 139 | paramsSerializer: (params) => 140 | stringify(params, { arrayFormat: "repeat" }), 141 | }) 142 | .catch((e) => { 143 | this.logger.error(formatErrors(e)); 144 | throw e; 145 | }); 146 | }, 147 | }, 148 | v2: { 149 | /** 150 | * tenant.product_assign_info 151 | */ 152 | tenantProductAssignInfo: { 153 | /** 154 | * {@link https://open.feishu.cn/api-explorer?project=tenant&resource=tenant.product_assign_info&apiName=query&version=v2 click to debug } 155 | * 156 | * {@link https://open.feishu.cn/api-explorer?from=op_doc_tab&apiName=query&project=tenant&resource=tenant.product_assign_info&version=v2 document } 157 | */ 158 | query: async (payload?: {}, options?: IRequestOptions) => { 159 | const { headers, params, data, path } = 160 | await this.formatPayload(payload, options); 161 | 162 | return this.httpInstance 163 | .request< 164 | any, 165 | { 166 | code?: number; 167 | msg?: string; 168 | data?: { 169 | assign_info_list?: Array<{ 170 | subscription_id?: string; 171 | license_plan_key?: string; 172 | product_name?: string; 173 | i18n_name?: { 174 | zh_cn?: string; 175 | ja_jp?: string; 176 | en_us?: string; 177 | }; 178 | total_seats?: string; 179 | assigned_seats?: string; 180 | start_time?: string; 181 | end_time?: string; 182 | }>; 183 | }; 184 | } 185 | >({ 186 | url: fillApiPath( 187 | `${this.domain}/open-apis/tenant/v2/tenant/assign_info_list/query`, 188 | path 189 | ), 190 | method: "GET", 191 | data, 192 | params, 193 | headers, 194 | paramsSerializer: (params) => 195 | stringify(params, { arrayFormat: "repeat" }), 196 | }) 197 | .catch((e) => { 198 | this.logger.error(formatErrors(e)); 199 | throw e; 200 | }); 201 | }, 202 | }, 203 | /** 204 | * 企业信息 205 | */ 206 | tenant: { 207 | /** 208 | * {@link https://open.feishu.cn/api-explorer?project=tenant&resource=tenant&apiName=query&version=v2 click to debug } 209 | * 210 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/tenant-v2/tenant/query document } 211 | * 212 | * 获取企业信息 213 | * 214 | * 获取企业名称、企业编号等企业信息 215 | * 216 | * 如果ISV应用是企业创建时默认安装,并且180天内企业未打开或使用过此应用,则无法通过此接口获取到企业信息。 217 | */ 218 | query: async (payload?: {}, options?: IRequestOptions) => { 219 | const { headers, params, data, path } = 220 | await this.formatPayload(payload, options); 221 | 222 | return this.httpInstance 223 | .request< 224 | any, 225 | { 226 | code?: number; 227 | msg?: string; 228 | data?: { 229 | tenant?: { 230 | name: string; 231 | display_id: string; 232 | tenant_tag: number; 233 | tenant_key: string; 234 | avatar: { 235 | avatar_origin?: string; 236 | avatar_72?: string; 237 | avatar_240?: string; 238 | avatar_640?: string; 239 | }; 240 | domain?: string; 241 | }; 242 | }; 243 | } 244 | >({ 245 | url: fillApiPath( 246 | `${this.domain}/open-apis/tenant/v2/tenant/query`, 247 | path 248 | ), 249 | method: "GET", 250 | data, 251 | params, 252 | headers, 253 | paramsSerializer: (params) => 254 | stringify(params, { arrayFormat: "repeat" }), 255 | }) 256 | .catch((e) => { 257 | this.logger.error(formatErrors(e)); 258 | throw e; 259 | }); 260 | }, 261 | }, 262 | }, 263 | }; 264 | } 265 | -------------------------------------------------------------------------------- /code-gen/projects/translation.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import tenant from "./tenant"; 13 | 14 | // auto gen 15 | export default abstract class Client extends tenant { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | * AI能力 33 | */ 34 | translation = { 35 | /** 36 | * 文本 37 | */ 38 | text: { 39 | /** 40 | * {@link https://open.feishu.cn/api-explorer?project=translation&resource=text&apiName=detect&version=v1 click to debug } 41 | * 42 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/ai/translation-v1/text/detect document } 43 | * 44 | * 文本语种识别 45 | * 46 | * 机器翻译 (MT),支持 100 多种语言识别,返回符合 ISO 639-1 标准 47 | * 48 | * 单租户限流:20QPS,同租户下的应用没有限流,共享本租户的 20QPS 限流 49 | */ 50 | detect: async ( 51 | payload?: { 52 | data: { text: string }; 53 | }, 54 | options?: IRequestOptions 55 | ) => { 56 | const { headers, params, data, path } = 57 | await this.formatPayload(payload, options); 58 | 59 | return this.httpInstance 60 | .request< 61 | any, 62 | { 63 | code?: number; 64 | msg?: string; 65 | data?: { language: string }; 66 | } 67 | >({ 68 | url: fillApiPath( 69 | `${this.domain}/open-apis/translation/v1/text/detect`, 70 | path 71 | ), 72 | method: "POST", 73 | data, 74 | params, 75 | headers, 76 | paramsSerializer: (params) => 77 | stringify(params, { arrayFormat: "repeat" }), 78 | }) 79 | .catch((e) => { 80 | this.logger.error(formatErrors(e)); 81 | throw e; 82 | }); 83 | }, 84 | /** 85 | * {@link https://open.feishu.cn/api-explorer?project=translation&resource=text&apiName=translate&version=v1 click to debug } 86 | * 87 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/ai/translation-v1/text/translate document } 88 | * 89 | * 文本翻译 90 | * 91 | * 机器翻译 (MT),支持以下语种互译:;"zh": 汉语;;"zh-Hant": 繁体汉语;;"en": 英语;;"ja": 日语;;"ru": 俄语;;"de": 德语;;"fr": 法语;;"it": 意大利语;;"pl": 波兰语;;"th": 泰语;;"hi": 印地语;;"id": 印尼语;;"es": 西班牙语;;"pt": 葡萄牙语;;"ko": 朝鲜语;;"vi": 越南语; 92 | * 93 | * 单租户限流:20QPS,同租户下的应用没有限流,共享本租户的 20QPS 限流 94 | */ 95 | translate: async ( 96 | payload?: { 97 | data: { 98 | source_language: string; 99 | text: string; 100 | target_language: string; 101 | glossary?: Array<{ from: string; to: string }>; 102 | }; 103 | }, 104 | options?: IRequestOptions 105 | ) => { 106 | const { headers, params, data, path } = 107 | await this.formatPayload(payload, options); 108 | 109 | return this.httpInstance 110 | .request< 111 | any, 112 | { code?: number; msg?: string; data?: { text: string } } 113 | >({ 114 | url: fillApiPath( 115 | `${this.domain}/open-apis/translation/v1/text/translate`, 116 | path 117 | ), 118 | method: "POST", 119 | data, 120 | params, 121 | headers, 122 | paramsSerializer: (params) => 123 | stringify(params, { arrayFormat: "repeat" }), 124 | }) 125 | .catch((e) => { 126 | this.logger.error(formatErrors(e)); 127 | throw e; 128 | }); 129 | }, 130 | }, 131 | v1: { 132 | /** 133 | * 文本 134 | */ 135 | text: { 136 | /** 137 | * {@link https://open.feishu.cn/api-explorer?project=translation&resource=text&apiName=detect&version=v1 click to debug } 138 | * 139 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/ai/translation-v1/text/detect document } 140 | * 141 | * 文本语种识别 142 | * 143 | * 机器翻译 (MT),支持 100 多种语言识别,返回符合 ISO 639-1 标准 144 | * 145 | * 单租户限流:20QPS,同租户下的应用没有限流,共享本租户的 20QPS 限流 146 | */ 147 | detect: async ( 148 | payload?: { 149 | data: { text: string }; 150 | }, 151 | options?: IRequestOptions 152 | ) => { 153 | const { headers, params, data, path } = 154 | await this.formatPayload(payload, options); 155 | 156 | return this.httpInstance 157 | .request< 158 | any, 159 | { 160 | code?: number; 161 | msg?: string; 162 | data?: { language: string }; 163 | } 164 | >({ 165 | url: fillApiPath( 166 | `${this.domain}/open-apis/translation/v1/text/detect`, 167 | path 168 | ), 169 | method: "POST", 170 | data, 171 | params, 172 | headers, 173 | paramsSerializer: (params) => 174 | stringify(params, { arrayFormat: "repeat" }), 175 | }) 176 | .catch((e) => { 177 | this.logger.error(formatErrors(e)); 178 | throw e; 179 | }); 180 | }, 181 | /** 182 | * {@link https://open.feishu.cn/api-explorer?project=translation&resource=text&apiName=translate&version=v1 click to debug } 183 | * 184 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/ai/translation-v1/text/translate document } 185 | * 186 | * 文本翻译 187 | * 188 | * 机器翻译 (MT),支持以下语种互译:;"zh": 汉语;;"zh-Hant": 繁体汉语;;"en": 英语;;"ja": 日语;;"ru": 俄语;;"de": 德语;;"fr": 法语;;"it": 意大利语;;"pl": 波兰语;;"th": 泰语;;"hi": 印地语;;"id": 印尼语;;"es": 西班牙语;;"pt": 葡萄牙语;;"ko": 朝鲜语;;"vi": 越南语; 189 | * 190 | * 单租户限流:20QPS,同租户下的应用没有限流,共享本租户的 20QPS 限流 191 | */ 192 | translate: async ( 193 | payload?: { 194 | data: { 195 | source_language: string; 196 | text: string; 197 | target_language: string; 198 | glossary?: Array<{ from: string; to: string }>; 199 | }; 200 | }, 201 | options?: IRequestOptions 202 | ) => { 203 | const { headers, params, data, path } = 204 | await this.formatPayload(payload, options); 205 | 206 | return this.httpInstance 207 | .request< 208 | any, 209 | { 210 | code?: number; 211 | msg?: string; 212 | data?: { text: string }; 213 | } 214 | >({ 215 | url: fillApiPath( 216 | `${this.domain}/open-apis/translation/v1/text/translate`, 217 | path 218 | ), 219 | method: "POST", 220 | data, 221 | params, 222 | headers, 223 | paramsSerializer: (params) => 224 | stringify(params, { arrayFormat: "repeat" }), 225 | }) 226 | .catch((e) => { 227 | this.logger.error(formatErrors(e)); 228 | throw e; 229 | }); 230 | }, 231 | }, 232 | }, 233 | }; 234 | } 235 | -------------------------------------------------------------------------------- /code-gen/projects/unified_kms_log.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import translation from "./translation"; 13 | 14 | // auto gen 15 | export default abstract class Client extends translation { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | 33 | */ 34 | unified_kms_log = {}; 35 | } 36 | -------------------------------------------------------------------------------- /code-gen/projects/verification.ts: -------------------------------------------------------------------------------- 1 | import identity from "lodash.identity"; 2 | import pickBy from "lodash.pickby"; 3 | import fs from "fs"; 4 | import { fillApiPath } from "@node-sdk/utils"; 5 | import { Logger } from "@node-sdk/typings"; 6 | import { formatErrors } from "@node-sdk/client/utils"; 7 | import { IRequestOptions } from "@node-sdk/code-gen/types"; 8 | import { IPayload } from "@node-sdk/client/types"; 9 | import { HttpInstance } from "@node-sdk/typings/http"; 10 | import { Readable } from "stream"; 11 | import { stringify } from "qs"; 12 | import vc from "./vc"; 13 | 14 | // auto gen 15 | export default abstract class Client extends vc { 16 | declare tokenManager; 17 | 18 | declare domain; 19 | 20 | declare logger: Logger; 21 | 22 | declare httpInstance: HttpInstance; 23 | 24 | abstract formatPayload( 25 | // eslint-disable-next-line no-unused-vars 26 | payload?: IPayload, 27 | // eslint-disable-next-line no-unused-vars 28 | options?: IRequestOptions 29 | ): Promise>; 30 | 31 | /** 32 | * 认证信息 33 | */ 34 | verification = { 35 | v1: { 36 | /** 37 | * 认证信息 38 | */ 39 | verification: { 40 | /** 41 | * {@link https://open.feishu.cn/api-explorer?project=verification&resource=verification&apiName=get&version=v1 click to debug } 42 | * 43 | * {@link https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/verification-v1/verification/get document } 44 | * 45 | * 获取认证信息 46 | * 47 | * 获取企业主体名称、是否认证等信息。 48 | */ 49 | get: async (payload?: {}, options?: IRequestOptions) => { 50 | const { headers, params, data, path } = 51 | await this.formatPayload(payload, options); 52 | 53 | return this.httpInstance 54 | .request< 55 | any, 56 | { 57 | code?: number; 58 | msg?: string; 59 | data?: { 60 | verification?: { 61 | name: string; 62 | has_verification: boolean; 63 | }; 64 | }; 65 | } 66 | >({ 67 | url: fillApiPath( 68 | `${this.domain}/open-apis/verification/v1/verification`, 69 | path 70 | ), 71 | method: "GET", 72 | data, 73 | params, 74 | headers, 75 | paramsSerializer: (params) => 76 | stringify(params, { arrayFormat: "repeat" }), 77 | }) 78 | .catch((e) => { 79 | this.logger.error(formatErrors(e)); 80 | throw e; 81 | }); 82 | }, 83 | }, 84 | }, 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /code-gen/types.ts: -------------------------------------------------------------------------------- 1 | import { CTenantKey, CWithHelpdeskAuthorization } from '@node-sdk/consts'; 2 | 3 | export interface IRequestOptions { 4 | lark?: { 5 | [CTenantKey]?: string; 6 | [CWithHelpdeskAuthorization]?: boolean; 7 | }; 8 | params?: Record; 9 | data?: Record; 10 | headers?: Record; 11 | } -------------------------------------------------------------------------------- /consts/index.ts: -------------------------------------------------------------------------------- 1 | export const CEventType = Symbol('event-type'); 2 | export const CTenantKey = Symbol('tenant-key'); 3 | export const CAppTicket = Symbol('app-ticket'); 4 | export const CTenantAccessToken = Symbol('tenant-access-token'); 5 | export const CWithHelpdeskAuthorization = Symbol('with-helpdesk-authorization'); 6 | export const CWithUserAccessToken = Symbol('with-user-access-token'); 7 | export const CUserAccessToken = Symbol('user-access-token'); 8 | export const CAilySessionRecord = Symbol('aily-session-record'); -------------------------------------------------------------------------------- /dispatcher/card.ts: -------------------------------------------------------------------------------- 1 | import { CAppTicket, CEventType } from '@node-sdk/consts'; 2 | import { Cache, Logger, LoggerLevel } from '@node-sdk/typings'; 3 | import { internalCache } from '@node-sdk/utils'; 4 | import { defaultLogger } from '@node-sdk/logger/default-logger'; 5 | import { LoggerProxy } from '@node-sdk/logger/logger-proxy'; 6 | import RequestHandle from './request-handle'; 7 | 8 | export class CardActionHandler { 9 | verificationToken: string = ''; 10 | 11 | encryptKey: string = ''; 12 | 13 | requestHandle?: RequestHandle; 14 | 15 | cardHandler: Function; 16 | 17 | handles: Map = new Map(); 18 | 19 | cache: Cache; 20 | 21 | logger: Logger; 22 | 23 | constructor( 24 | params: { 25 | verificationToken?: string; 26 | encryptKey?: string; 27 | cache?: Cache; 28 | logger?: Logger; 29 | loggerLevel?: LoggerLevel; 30 | }, 31 | cardHandler: Function 32 | ) { 33 | const { verificationToken, encryptKey } = params; 34 | 35 | this.encryptKey = encryptKey || ''; 36 | this.verificationToken = verificationToken || ''; 37 | 38 | this.cardHandler = cardHandler; 39 | 40 | this.logger = new LoggerProxy( 41 | params.loggerLevel || LoggerLevel.info, 42 | params.logger || defaultLogger 43 | ); 44 | 45 | this.requestHandle = new RequestHandle({ 46 | encryptKey, 47 | verificationToken, 48 | logger: this.logger, 49 | }); 50 | 51 | this.cache = params.cache || internalCache; 52 | 53 | this.registerAppTicketHandle(); 54 | 55 | this.logger.info('card-action-handle is ready'); 56 | } 57 | 58 | private registerAppTicketHandle() { 59 | this.register({ 60 | app_ticket: async (data) => { 61 | const { app_ticket, app_id } = data; 62 | 63 | if (app_ticket) { 64 | await this.cache.set(CAppTicket, app_ticket, undefined, { 65 | namespace: app_id 66 | }); 67 | this.logger.debug('set app ticket'); 68 | } else { 69 | this.logger.warn('response not include app ticket'); 70 | } 71 | }, 72 | }); 73 | } 74 | 75 | private register(handles: Record) { 76 | Object.keys(handles).forEach((key) => { 77 | this.handles.set(key, handles[key]); 78 | this.logger.debug(`register ${key} handle`); 79 | }); 80 | 81 | return this; 82 | } 83 | 84 | async invoke(data) { 85 | if (!this.requestHandle?.checkIsCardEventValidated(data)) { 86 | this.logger.warn('verification failed event'); 87 | return undefined; 88 | } 89 | 90 | const targetData = this.requestHandle?.parse(data); 91 | 92 | const type = targetData[CEventType]; 93 | if (this.handles.has(type)) { 94 | try { 95 | const ret = await this.handles.get(type)!(targetData); 96 | return ret; 97 | } catch (e) { 98 | this.logger.error(e); 99 | return undefined; 100 | } 101 | } 102 | 103 | try { 104 | const result = await this.cardHandler(targetData); 105 | this.logger.debug('execute card handle'); 106 | return result; 107 | } catch (e) { 108 | this.logger.error(e); 109 | } 110 | 111 | return undefined; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /dispatcher/event.ts: -------------------------------------------------------------------------------- 1 | import { CEventType, CAppTicket } from '@node-sdk/consts'; 2 | import { Cache, Logger, LoggerLevel } from '@node-sdk/typings'; 3 | import { internalCache } from '@node-sdk/utils'; 4 | import { defaultLogger } from '@node-sdk/logger/default-logger'; 5 | import { LoggerProxy } from '@node-sdk/logger/logger-proxy'; 6 | import { IHandles } from '@node-sdk/code-gen/events-template'; 7 | import RequestHandle from './request-handle'; 8 | 9 | const CAppTicketHandle = 'app_ticket'; 10 | export class EventDispatcher { 11 | verificationToken: string = ''; 12 | 13 | encryptKey: string = ''; 14 | 15 | requestHandle?: RequestHandle; 16 | 17 | handles: Map = new Map(); 18 | 19 | cache: Cache; 20 | 21 | logger: Logger; 22 | 23 | constructor(params: { 24 | verificationToken?: string; 25 | encryptKey?: string; 26 | cache?: Cache; 27 | logger?: Logger; 28 | loggerLevel?: LoggerLevel; 29 | }) { 30 | const { encryptKey, verificationToken } = params; 31 | 32 | this.encryptKey = encryptKey || ''; 33 | this.verificationToken = verificationToken || ''; 34 | 35 | this.logger = new LoggerProxy( 36 | params.loggerLevel || LoggerLevel.info, 37 | params.logger || defaultLogger 38 | ); 39 | 40 | this.requestHandle = new RequestHandle({ 41 | encryptKey, 42 | verificationToken, 43 | logger: this.logger, 44 | }); 45 | 46 | this.cache = params.cache || internalCache; 47 | 48 | this.registerAppTicketHandle(); 49 | 50 | this.logger.info('event-dispatch is ready'); 51 | } 52 | 53 | private registerAppTicketHandle() { 54 | this.register({ 55 | app_ticket: async (data) => { 56 | const { app_ticket, app_id } = data; 57 | 58 | if (app_ticket) { 59 | await this.cache.set(CAppTicket, app_ticket, undefined ,{ 60 | namespace: app_id 61 | }); 62 | this.logger.debug('set app ticket'); 63 | } else { 64 | this.logger.warn('response not include app ticket'); 65 | } 66 | }, 67 | }); 68 | } 69 | 70 | register(handles: IHandles & T) { 71 | Object.keys(handles).forEach((key) => { 72 | if (this.handles.has(key) && key !== CAppTicketHandle) { 73 | this.logger.error(`this ${key} handle is registered`); 74 | } 75 | 76 | this.handles.set(key, handles[key]); 77 | this.logger.debug(`register ${key} handle`); 78 | }); 79 | 80 | return this; 81 | } 82 | 83 | async invoke(data, params?: { needCheck?: boolean }) { 84 | const needCheck = params?.needCheck === false ? false : true; 85 | 86 | if (needCheck && !this.requestHandle?.checkIsEventValidated(data)) { 87 | this.logger.warn('verification failed event'); 88 | return undefined; 89 | } 90 | const targetData = this.requestHandle?.parse(data); 91 | const type = targetData[CEventType]; 92 | if (this.handles.has(type)) { 93 | const ret = await this.handles.get(type)!(targetData); 94 | this.logger.debug(`execute ${type} handle`); 95 | return ret; 96 | } 97 | 98 | this.logger.warn(`no ${type} handle`); 99 | 100 | return `no ${type} event handle`; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /dispatcher/request-handle.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import { CEventType } from '@node-sdk/consts'; 3 | import { AESCipher } from '@node-sdk/utils'; 4 | import { Logger } from '@node-sdk/typings'; 5 | 6 | export default class RequestHandle { 7 | aesCipher?: AESCipher; 8 | 9 | verificationToken?: string; 10 | 11 | encryptKey?: string; 12 | 13 | logger: Logger; 14 | 15 | constructor(params: { 16 | encryptKey?: string; 17 | verificationToken?: string; 18 | logger: Logger; 19 | }) { 20 | const { encryptKey, verificationToken, logger } = params; 21 | 22 | this.verificationToken = verificationToken; 23 | this.encryptKey = encryptKey; 24 | this.logger = logger; 25 | 26 | if (encryptKey) { 27 | this.aesCipher = new AESCipher(encryptKey); 28 | } 29 | } 30 | 31 | parse(data: any) { 32 | const targetData = (() => { 33 | const { encrypt, ...rest } = data || {}; 34 | if (encrypt) { 35 | try { 36 | return { 37 | ...JSON.parse(this.aesCipher?.decrypt(encrypt)!), 38 | ...rest, 39 | }; 40 | } catch (e) { 41 | this.logger.error('parse encrypt data failed'); 42 | return {}; 43 | } 44 | } 45 | 46 | return rest; 47 | })(); 48 | 49 | // v1和v2版事件的区别:https://open.feishu.cn/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM 50 | if ('schema' in targetData) { 51 | const { header, event, ...rest } = targetData; 52 | return { 53 | [CEventType]: targetData?.header?.event_type, 54 | ...rest, 55 | ...header, 56 | ...event, 57 | }; 58 | } 59 | const { event, ...rest } = targetData; 60 | return { 61 | [CEventType]: targetData?.event?.type, 62 | ...event, 63 | ...rest, 64 | }; 65 | } 66 | 67 | checkIsCardEventValidated(data: any): boolean { 68 | /** 69 | * 1. new message card encrypt ('encrypt' in data) 70 | * 2. new message card but not encrypt ('schema' in data) 71 | */ 72 | if ('encrypt' in data || 'schema' in data) { 73 | return this.checkIsEventValidated(data); 74 | } 75 | 76 | const { 77 | 'x-lark-request-timestamp': timestamp, 78 | 'x-lark-request-nonce': nonce, 79 | 'x-lark-signature': signature, 80 | } = data.headers; 81 | 82 | if (!this.verificationToken) { 83 | return true; 84 | } 85 | 86 | const computedSignature = crypto 87 | .createHash('sha1') 88 | .update( 89 | timestamp + 90 | nonce + 91 | this.verificationToken + 92 | JSON.stringify(data) 93 | ) 94 | .digest('hex'); 95 | 96 | return computedSignature === signature; 97 | } 98 | 99 | checkIsEventValidated(data: any): boolean { 100 | if (!this.encryptKey) { 101 | return true; 102 | } 103 | 104 | const { 105 | 'x-lark-request-timestamp': timestamp, 106 | 'x-lark-request-nonce': nonce, 107 | 'x-lark-signature': signature, 108 | } = data.headers; 109 | 110 | const content = 111 | timestamp + nonce + this.encryptKey + JSON.stringify(data); 112 | 113 | const computedSignature = crypto 114 | .createHash('sha256') 115 | .update(content) 116 | .digest('hex'); 117 | 118 | return computedSignature === signature; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /doc/debugger-tip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larksuite/node-sdk/05cbe50202defd233b27796d49dc5b81cbc7310e/doc/debugger-tip.png -------------------------------------------------------------------------------- /doc/deprecated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larksuite/node-sdk/05cbe50202defd233b27796d49dc5b81cbc7310e/doc/deprecated.png -------------------------------------------------------------------------------- /doc/msg-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larksuite/node-sdk/05cbe50202defd233b27796d49dc5b81cbc7310e/doc/msg-card.png -------------------------------------------------------------------------------- /http/index.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from 'axios'; 2 | 3 | const defaultHttpInstance: AxiosInstance = axios.create(); 4 | 5 | defaultHttpInstance.interceptors.request.use( 6 | (req) => { 7 | if (req.headers) { 8 | req.headers['User-Agent'] = 'oapi-node-sdk/1.0.0'; 9 | } 10 | return req; 11 | }, 12 | undefined, 13 | { synchronous: true } 14 | ); 15 | 16 | defaultHttpInstance.interceptors.response.use((resp) => resp.data); 17 | 18 | export { AxiosRequestConfig, AxiosError } from 'axios'; 19 | 20 | export default defaultHttpInstance; 21 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './client/client'; 2 | export * from './client/request-with'; 3 | export * from './dispatcher/event'; 4 | export * from './dispatcher/card'; 5 | export * from './adaptor/default'; 6 | export * from './adaptor/express'; 7 | export * from './adaptor/koa'; 8 | export * from './adaptor/koa-router'; 9 | export { generateChallenge } from './adaptor/services/challenge'; 10 | export * from './typings/card'; 11 | export { AppType, Domain, LoggerLevel, Cache } from './typings'; 12 | export { CAppTicket, CTenantAccessToken } from './consts'; 13 | export { IHandles as EventHandles } from './code-gen/events-template'; 14 | export { AESCipher } from './utils/aes-cipher'; 15 | // default http client & types 16 | export { default as defaultHttpInstance } from './http'; 17 | export { HttpInstance, HttpRequestOptions } from './typings/http'; 18 | export * as messageCard from './utils/message-card'; 19 | export { WSClient } from './ws-client'; 20 | export { Aily } from './scene/aily/client'; -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | const config: Config.InitialOptions = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | transform: { 7 | '^.+\\.ts?$': 'ts-jest', 8 | }, 9 | transformIgnorePatterns: ['/node_modules/'], 10 | testMatch: ['**/__tests__/**/*.ts'], 11 | moduleNameMapper: { 12 | '^@node-sdk/(.*)$': '/$1', 13 | }, 14 | globals: { 15 | 'ts-jest': { 16 | diagnostics: false, 17 | }, 18 | }, 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /logger/__tests__/logger-proxy.ts: -------------------------------------------------------------------------------- 1 | import { LoggerLevel } from '@node-sdk/typings'; 2 | import { defaultLogger } from '../default-logger'; 3 | import { LoggerProxy } from '../logger-proxy'; 4 | 5 | describe('logger proxy', () => { 6 | test('filter work', () => { 7 | const errorSpy = jest.spyOn(defaultLogger, 'error'); 8 | const warnSpy = jest.spyOn(defaultLogger, 'warn'); 9 | const infoSpy = jest.spyOn(defaultLogger, 'info'); 10 | const debugSpy = jest.spyOn(defaultLogger, 'debug'); 11 | const traceSpy = jest.spyOn(defaultLogger, 'trace'); 12 | 13 | /** 14 | * 同级别调用 15 | */ 16 | let loggerProxy = new LoggerProxy(LoggerLevel.info, defaultLogger); 17 | loggerProxy.info('info'); 18 | expect(infoSpy).toBeCalled(); 19 | infoSpy.mockClear(); 20 | 21 | /** 22 | * 低级别调用 23 | */ 24 | loggerProxy = new LoggerProxy(LoggerLevel.warn, defaultLogger); 25 | loggerProxy.debug('debug'); 26 | expect(warnSpy).not.toBeCalled(); 27 | expect(debugSpy).not.toBeCalled(); 28 | debugSpy.mockClear(); 29 | 30 | /** 31 | * 高级别调用 32 | */ 33 | loggerProxy = new LoggerProxy(LoggerLevel.trace, defaultLogger); 34 | loggerProxy.error('error'); 35 | expect(errorSpy).toBeCalled(); 36 | expect(traceSpy).not.toBeCalled(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /logger/default-logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { Logger } from '@node-sdk/typings'; 3 | 4 | export const defaultLogger: Logger = { 5 | error: (...msg: any[]) => { 6 | console.log('[error]:', ...msg); 7 | }, 8 | warn: (...msg: any[]) => { 9 | console.warn('[warn]:', ...msg); 10 | }, 11 | info: (...msg: any[]) => { 12 | console.info('[info]:', ...msg); 13 | }, 14 | debug: (...msg: any[]) => { 15 | console.debug('[debug]:', ...msg); 16 | }, 17 | trace: (...msg: any[]) => { 18 | console.trace('[trace]:', ...msg); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /logger/logger-proxy.ts: -------------------------------------------------------------------------------- 1 | import { LoggerLevel, Logger } from '@node-sdk/typings'; 2 | 3 | export class LoggerProxy { 4 | private level: LoggerLevel; 5 | 6 | private logger: Logger; 7 | 8 | constructor(level: LoggerLevel, logger: Logger) { 9 | this.level = level; 10 | this.logger = logger; 11 | } 12 | 13 | error(...msg: any[]) { 14 | if (this.level >= LoggerLevel.error) { 15 | this.logger.error(msg); 16 | } 17 | } 18 | 19 | warn(...msg: any[]) { 20 | if (this.level >= LoggerLevel.warn) { 21 | this.logger.warn(msg); 22 | } 23 | } 24 | 25 | info(...msg: any[]) { 26 | if (this.level >= LoggerLevel.info) { 27 | this.logger.info(msg); 28 | } 29 | } 30 | 31 | debug(...msg: any[]) { 32 | if (this.level >= LoggerLevel.debug) { 33 | this.logger.debug(msg); 34 | } 35 | } 36 | 37 | trace(...msg: any[]) { 38 | if (this.level >= LoggerLevel.trace) { 39 | this.logger.trace(msg); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@larksuiteoapi/node-sdk", 3 | "version": "1.48.0", 4 | "description": "larksuite open sdk for nodejs", 5 | "keywords": [ 6 | "feishu", 7 | "larksuite", 8 | "sdk", 9 | "nodejs" 10 | ], 11 | "types": "./types", 12 | "main": "./lib/index.js", 13 | "module": "./es/index.js", 14 | "files": [ 15 | "lib", 16 | "es", 17 | "types", 18 | "README.zh.md", 19 | "README.md", 20 | "LICENSE" 21 | ], 22 | "scripts": { 23 | "build": "rm -r lib es types & rollup -c", 24 | "test": "jest", 25 | "test:watch": "jest --watch --testPathPattern=utils/__tests__" 26 | }, 27 | "author": "mazhe.nerd", 28 | "license": "MIT", 29 | "homepage": "https://github.com/larksuite/node-sdk", 30 | "url": "https://github.com/larksuite/node-sdk/issues", 31 | "dependencies": { 32 | "axios": "0.27.2", 33 | "lodash.identity": "^3.0.0", 34 | "lodash.merge": "^4.6.2", 35 | "lodash.pickby": "^4.6.0", 36 | "protobufjs": "^7.2.6", 37 | "qs": "^6.13.0", 38 | "ws": "^8.16.0" 39 | }, 40 | "devDependencies": { 41 | "@koa/router": "^12.0.0", 42 | "@rollup/plugin-alias": "^3.1.9", 43 | "@rollup/plugin-typescript": "^8.3.2", 44 | "@types/jest": "^28.1.3", 45 | "@types/lodash.merge": "^4.6.7", 46 | "@types/lodash.pick": "^4.4.7", 47 | "@types/qs": "^6.9.16", 48 | "@types/ws": "^8.5.10", 49 | "@typescript-eslint/parser": "^5.27.1", 50 | "body-parser": "^1.20.1", 51 | "eslint": "^8.17.0", 52 | "eslint-config-airbnb-base": "^15.0.0", 53 | "eslint-config-prettier": "^8.5.0", 54 | "eslint-plugin-import": "^2.26.0", 55 | "express": "^4.18.2", 56 | "jest": "^28.1.1", 57 | "koa": "^2.13.4", 58 | "koa-body": "^5.0.0", 59 | "prettier": "2.6.2", 60 | "rollup": "^2.75.5", 61 | "rollup-plugin-dts": "^4.2.2", 62 | "rollup-plugin-license": "^2.8.1", 63 | "ts-jest": "^28.0.5", 64 | "ts-node": "^10.8.1", 65 | "tsconfig-paths": "^4.0.0", 66 | "tslib": "^2.4.0", 67 | "typescript": "^4.7.3" 68 | }, 69 | "publishConfig": { 70 | "access": "public" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import alias from '@rollup/plugin-alias'; 3 | import dts from 'rollup-plugin-dts'; 4 | import license from 'rollup-plugin-license'; 5 | import path from 'path'; 6 | import packageJSON from './package.json'; 7 | 8 | export default [{ 9 | input: './index.ts', 10 | external: [...Object.keys(packageJSON.dependencies)], 11 | output: { 12 | dir: 'lib', 13 | format: 'cjs' 14 | }, 15 | plugins: [ 16 | alias({ 17 | entries: [ 18 | {find: '@node-sdk', replacement: path.resolve(__dirname)} 19 | ] 20 | }), 21 | typescript({tsconfig: false, compilerOptions: { target: 'es6' }}), 22 | license({ 23 | banner: { 24 | content: { 25 | file: path.join(__dirname, 'LICENSE') 26 | } 27 | } 28 | }) 29 | ] 30 | }, { 31 | input: './index.ts', 32 | external: [...Object.keys(packageJSON.dependencies)], 33 | output: { 34 | dir: 'es', 35 | format: 'es' 36 | }, 37 | plugins: [ 38 | alias({ 39 | entries: [ 40 | {find: '@node-sdk', replacement: path.resolve(__dirname)} 41 | ] 42 | }), 43 | typescript({tsconfig: false, compilerOptions: { target: 'es6' }}), 44 | license({ 45 | banner: { 46 | content: { 47 | file: path.join(__dirname, 'LICENSE') 48 | } 49 | } 50 | }) 51 | ] 52 | }, { 53 | input: './index.ts', 54 | output: { 55 | dir: 'types', 56 | format: 'es' 57 | }, 58 | plugins: [ 59 | alias({ 60 | entries: [ 61 | {find: '@node-sdk', replacement: path.resolve(__dirname)} 62 | ] 63 | }), 64 | dts({ 65 | compilerOptions: { 66 | noUnusedLocals: false 67 | } 68 | }), 69 | license({ 70 | banner: { 71 | content: { 72 | file: path.join(__dirname, 'LICENSE') 73 | } 74 | } 75 | }) 76 | ] 77 | }]; 78 | -------------------------------------------------------------------------------- /scene/aily/__tests__/session-cache.ts: -------------------------------------------------------------------------------- 1 | import { SessionCache } from '../session-cache'; 2 | 3 | describe('aily session cache', () => { 4 | test('work right', async () => { 5 | const sessionCache = new SessionCache(); 6 | 7 | await sessionCache.set('test', { a: 1 }); 8 | expect(await sessionCache.get('test')).toEqual({a: 1}); 9 | 10 | await sessionCache.set('test', { a: undefined }); 11 | expect(await sessionCache.get('test')).toEqual({a: 1}); 12 | 13 | await sessionCache.set('test', { a: 2, b: 1 }); 14 | expect(await sessionCache.get('test')).toEqual({a: 2, b: 1}); 15 | }); 16 | }) -------------------------------------------------------------------------------- /scene/aily/client.ts: -------------------------------------------------------------------------------- 1 | import { Client } from '@node-sdk/client/client'; 2 | import { IRequestOptions } from '@node-sdk/client/types'; 3 | import { Cache, Logger } from '@node-sdk/typings'; 4 | import { CAilySessionRecord } from '@node-sdk/consts'; 5 | import { SessionCache } from './session-cache'; 6 | 7 | type ElementType = T extends (infer U)[] ? U : T; 8 | type UnpackPromise = T extends Promise ? U : T; 9 | type TClientInstance = typeof Client extends new (...args: any[]) => infer R ? R : any; 10 | type TPickParams any > = Exclude['0'], undefined>; 11 | type TCreateSessionParams = TPickParams; 12 | type TCreateMessageParams = TPickParams; 13 | type TCreateRunParams = TPickParams; 14 | type TMessage = ElementType>['data'], undefined>['messages'], undefined>>; 15 | 16 | interface ICreateParams { 17 | // session 18 | sessionId?: string; 19 | sessionInfo?: TCreateSessionParams['data']; 20 | // message 21 | message: string; 22 | messageInfo?: Omit; 23 | // run 24 | ailyAppId: string; 25 | skillId?: string; 26 | runInfo?: Omit; 27 | } 28 | 29 | export enum EExecStatus { 30 | ERROR = -1, 31 | SUCCESS = 0, 32 | EXPIRED = 1, 33 | CANCELLED = 2, 34 | FAILED = 3, 35 | OTHER = 4 36 | } 37 | 38 | export class Aily { 39 | client: Client; 40 | cache: Cache; 41 | logger: Logger; 42 | 43 | constructor(params: { client: Client, cache?: Cache }) { 44 | this.client = params.client; 45 | this.logger = params.client.logger; 46 | this.cache = params.cache || new SessionCache(); 47 | } 48 | 49 | private async getSessionId(params: { sessionId?: string, payload?:TCreateSessionParams['data'], options?: IRequestOptions }) { 50 | const { sessionId, payload, options } = params; 51 | 52 | const records = await this.getRecords() as Record; 53 | if (sessionId && sessionId in records) { 54 | return records[sessionId]; 55 | } 56 | 57 | const ailySession = await this.client.aily.v1.ailySession.create({ data: payload }, options); 58 | if (ailySession.code === 0 && ailySession.data) { 59 | const ailySessionId = ailySession.data.session?.id; 60 | if (sessionId) { 61 | await this.updateRecords({[sessionId]: ailySessionId!}); 62 | } 63 | return ailySessionId; 64 | } else { 65 | this.logger.error('get aily session id error', ailySession.msg); 66 | return undefined; 67 | } 68 | } 69 | 70 | private async waitReply(params: ICreateParams, options?: IRequestOptions) { 71 | const { sessionId, ailyAppId, message, sessionInfo, messageInfo, runInfo, skillId } = params; 72 | 73 | // step1 get aily session id 74 | const ailySessionId = await this.getSessionId({ sessionId, payload: sessionInfo, options}); 75 | if (!ailySessionId) { 76 | throw EExecStatus.ERROR; 77 | } 78 | 79 | // step2 create message and run 80 | const ailySessionMessage = await this.client.aily.v1.ailySessionAilyMessage.create({ 81 | path: { 82 | aily_session_id: ailySessionId 83 | }, 84 | data: { 85 | ...messageInfo, 86 | content: message, 87 | idempotent_id: `${Date.now()}`, 88 | content_type: 'MDX', 89 | } 90 | }, options); 91 | if (!(ailySessionMessage.code === 0 && ailySessionMessage.data)) { 92 | this.logger.error('create aily message error', ailySessionMessage.msg); 93 | throw EExecStatus.ERROR; 94 | } 95 | 96 | const messageId = ailySessionMessage.data.message?.id; 97 | 98 | const ailySessionRun = await this.client.aily.v1.ailySessionRun.create({ 99 | path: { 100 | aily_session_id: ailySessionId 101 | }, 102 | data: { 103 | app_id: ailyAppId, 104 | skill_id: skillId, 105 | ...runInfo 106 | } 107 | }, options); 108 | if (!(ailySessionRun.code === 0 && ailySessionRun.data)) { 109 | this.logger.error('create aily session run error', ailySessionRun.msg); 110 | throw EExecStatus.ERROR; 111 | } 112 | const runId = ailySessionRun.data.run?.id; 113 | if (!runId) { 114 | this.logger.error('run id is empty'); 115 | throw EExecStatus.ERROR; 116 | } 117 | 118 | // step3 wait run complete 119 | const polling = async () => { 120 | return new Promise((resolve) => { 121 | setTimeout(async () => { 122 | const runStatusInfo = await this.client.aily.v1.ailySessionRun.get({ 123 | path: { 124 | aily_session_id: ailySessionId, 125 | run_id: runId 126 | } 127 | }, options); 128 | 129 | if (!(runStatusInfo.code === 0 && runStatusInfo.data)) { 130 | resolve(3); 131 | return; 132 | } 133 | 134 | const status = runStatusInfo.data?.run?.status; 135 | 136 | switch(status) { 137 | case 'QUEUED': 138 | case 'IN_PROGRESS': await (async () => { 139 | const ret = await polling(); 140 | resolve(ret); 141 | })(); 142 | case 'COMPLETED': resolve(EExecStatus.SUCCESS); 143 | case 'EXPIRED': resolve(EExecStatus.EXPIRED); 144 | case 'CANCELLED': resolve(EExecStatus.CANCELLED); 145 | case 'FAILED': resolve(EExecStatus.FAILED); 146 | default: resolve(EExecStatus.OTHER) 147 | } 148 | }, 500) 149 | }); 150 | } 151 | 152 | const pollingRet = await polling(); 153 | if (pollingRet !== EExecStatus.SUCCESS) { 154 | this.logger.error('aily run error'); 155 | throw pollingRet; 156 | } 157 | 158 | return { 159 | ailySessionId, 160 | runId, 161 | messageId 162 | } 163 | } 164 | 165 | private async create(params: ICreateParams, options?: IRequestOptions) { 166 | const { 167 | ailySessionId, 168 | runId 169 | } = await this.waitReply(params, options); 170 | 171 | let reply: TMessage | undefined; 172 | for await (const items of await this.client.aily.v1.ailySessionAilyMessage.listWithIterator({ 173 | path: { 174 | aily_session_id: ailySessionId, 175 | }, 176 | params: { 177 | run_id: runId, 178 | with_partial_message: false, 179 | } 180 | }, options)) { 181 | if(!items) { 182 | continue; 183 | } 184 | items?.messages?.forEach(message => { 185 | if (message.sender?.sender_type === 'ASSISTANT') { 186 | reply = message; 187 | } 188 | }) 189 | } 190 | if (!reply) { 191 | this.logger.error('no aily reply'); 192 | throw EExecStatus.ERROR; 193 | } 194 | 195 | return reply; 196 | } 197 | 198 | private async createWithStream(params: ICreateParams, options?: IRequestOptions) { 199 | const { 200 | ailySessionId, 201 | runId, 202 | messageId 203 | } = await this.waitReply(params, options); 204 | 205 | const listMessages = () => this.client.aily.v1.ailySessionAilyMessage.listWithIterator({ 206 | path: { 207 | aily_session_id: ailySessionId, 208 | }, 209 | params: { 210 | run_id: runId, 211 | with_partial_message: true, 212 | } 213 | }, options); 214 | 215 | let startOutput = false; 216 | const Iterable = { 217 | async *[Symbol.asyncIterator]() { 218 | for await (const items of await listMessages()) { 219 | if(!items) { 220 | continue; 221 | } 222 | 223 | for (const message of items.messages || []) { 224 | if (startOutput) { 225 | yield message; 226 | 227 | if (message.status === 'COMPLETED') { 228 | return; 229 | } 230 | } else if (message.id === messageId && message.status === 'COMPLETED') { 231 | startOutput = true; 232 | } 233 | } 234 | } 235 | } 236 | } 237 | 238 | return Iterable; 239 | } 240 | 241 | private async getRecords() { 242 | const sessions = await this.cache.get(CAilySessionRecord) || {}; 243 | return sessions; 244 | } 245 | 246 | private async updateRecords(records: Record) { 247 | await this.cache.set(CAilySessionRecord, records); 248 | } 249 | 250 | completions = { 251 | create: this.create.bind(this), 252 | createWithStream: this.createWithStream.bind(this), 253 | sessionRecords: { 254 | get: this.getRecords.bind(this), 255 | update: this.updateRecords.bind(this) 256 | } 257 | } 258 | } -------------------------------------------------------------------------------- /scene/aily/session-cache.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '@node-sdk/typings'; 2 | import { mergeObject } from '@node-sdk/utils/merge-object'; 3 | 4 | export class SessionCache implements Cache { 5 | sessions: Map> = new Map(); 6 | 7 | async set(key: string | Symbol, value: Record) { 8 | const sessions = this.sessions.get(key.toString()) || {}; 9 | const mergedSessions = mergeObject(sessions, value); 10 | this.sessions.set(key.toString(), mergedSessions); 11 | return true; 12 | } 13 | 14 | async get(key: string | Symbol) { 15 | return this.sessions.get(key.toString()); 16 | } 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "downlevelIteration": true, 7 | "alwaysStrict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "removeComments": false, 11 | "strict": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "noImplicitAny": false, 15 | "noImplicitThis": false, 16 | "noEmit": false, 17 | "useDefineForClassFields": true, 18 | "esModuleInterop": true, 19 | "baseUrl": ".", 20 | "emitDeclarationOnly": true, 21 | "declarationDir": "types", 22 | "allowJs": true, 23 | "paths": { 24 | "@node-sdk/*": ["*"] 25 | } 26 | }, 27 | "exclude": ["__tests__", "node_modules"], 28 | "ts-node": { 29 | "transpileOnly": true 30 | }, 31 | } -------------------------------------------------------------------------------- /typings/card.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export interface InteractiveCardActionEvent { 3 | open_id: string; 4 | user_id?: string; 5 | tenant_key: string; 6 | open_message_id: string; 7 | token: string; 8 | action: { 9 | value: Record; 10 | tag: string; 11 | option?: string; 12 | timezone?: string; 13 | }; 14 | } 15 | 16 | export interface InteractiveCard { 17 | config?: InteractiveCardConfig; 18 | header?: InteractiveCardHeader; 19 | elements?: InteractiveCardElement[]; // required, alternative i18n_elements 20 | i18n_elements?: Record; // required, alternative elements 21 | card_link?: InteractiveCardUrlItem; 22 | } 23 | 24 | interface InteractiveCardConfig { 25 | enable_forward?: boolean; 26 | update_multi?: boolean; 27 | wide_screen_mode?: boolean; // deprecated after 2021/03/22 28 | } 29 | 30 | type InteractiveCardHeaderTemplate = 31 | | 'blue' 32 | | 'wathet' 33 | | 'turquoise' 34 | | 'green' 35 | | 'yellow' 36 | | 'orange' 37 | | 'red' 38 | | 'carmine' 39 | | 'violet' 40 | | 'purple' 41 | | 'indigo' 42 | | 'grey'; 43 | interface InteractiveCardHeader { 44 | title: InteractiveCardTitle; 45 | template?: InteractiveCardHeaderTemplate; 46 | } 47 | 48 | export type InteractiveCardElement = 49 | | InteractiveCardDivElement 50 | | InteractiveCardMarkdownElement 51 | | InteractiveCardDividerElement 52 | | InteractiveCardImageElement 53 | | InteractiveCardNoteElement 54 | | InterfaceCardActionElement; 55 | export interface InteractiveCardTitle 56 | extends Omit { 57 | i18n?: Record; 58 | } 59 | export type InteractiveCardTextItem = 60 | | InteractiveCardPlainTextItem 61 | | InteractiveCardLarkMdItem; 62 | export interface InteractiveCardPlainTextItem { 63 | tag: 'plain_text'; 64 | content?: string; 65 | lines?: number; 66 | } 67 | export interface InteractiveCardLarkMdItem { 68 | tag: 'lark_md'; 69 | content?: string; 70 | } 71 | export interface InteractiveCardImageItem { 72 | tag: 'img'; 73 | img_key: string; 74 | alt: InteractiveCardPlainTextItem; 75 | preview?: boolean; 76 | } 77 | export interface InteractiveCardUrlItem { 78 | url: string; 79 | android_url?: string; 80 | ios_url?: string; 81 | pc_url?: string; 82 | } 83 | 84 | export interface InteractiveCardField { 85 | is_short: boolean; 86 | text: InteractiveCardTextItem; 87 | } 88 | 89 | export interface InteractiveCardDivElement { 90 | tag: 'div'; 91 | text: InteractiveCardTextItem; 92 | fields?: InteractiveCardField[]; 93 | extra?: any; 94 | } 95 | 96 | export interface InteractiveCardMarkdownElement { 97 | tag: 'markdown'; 98 | content: string; 99 | text_align?: 'left' | 'center' | 'right'; 100 | href?: Record; 101 | } 102 | 103 | export interface InteractiveCardDividerElement { 104 | tag: 'hr'; 105 | } 106 | 107 | export interface InteractiveCardImageElement extends InteractiveCardImageItem { 108 | title?: InteractiveCardTextItem; 109 | custom_width?: number; 110 | compact_width?: boolean; 111 | mode?: 'crop_center' | 'fit_horizontal'; 112 | } 113 | 114 | export interface InteractiveCardNoteElement { 115 | tag: 'note'; 116 | elements: (InteractiveCardTextItem | InteractiveCardImageItem)[]; 117 | } 118 | 119 | export interface InterfaceCardActionElement { 120 | tag: 'action'; 121 | actions: InteractiveCardActionItem[]; 122 | layout?: 'bisected' | 'trisection' | 'flow'; 123 | } 124 | 125 | export type InteractiveCardActionItem = 126 | | InteractiveCardDatePickerActionItem 127 | | InteractiveCardButtonActionItem 128 | | InteractiveCardOverflowActionItem 129 | | InteractiveCardSelectMenuActionItem; 130 | 131 | interface InteractiveCardActionBaseItem { 132 | confirm?: { 133 | title: InteractiveCardPlainTextItem; 134 | text: InteractiveCardPlainTextItem; 135 | }; 136 | } 137 | interface InteractiveCardActionOptionItem { 138 | text?: InteractiveCardPlainTextItem; 139 | value?: string; 140 | } 141 | 142 | export interface InteractiveCardDatePickerActionItem 143 | extends InteractiveCardActionBaseItem { 144 | tag: 'date_picker' | 'picker_time' | 'picker_datetime'; 145 | initial_date?: string; 146 | initial_time?: string; 147 | initial_datetime?: string; 148 | placeholder?: InteractiveCardPlainTextItem; 149 | value?: Record; 150 | } 151 | 152 | export interface InteractiveCardButtonActionItem 153 | extends InteractiveCardActionBaseItem { 154 | tag: 'button'; 155 | text?: InteractiveCardTextItem; 156 | url?: string; 157 | multi_utl?: InteractiveCardUrlItem; 158 | type?: 'default' | 'primary' | 'danger'; 159 | value?: Record; 160 | } 161 | 162 | export interface InteractiveCardOverflowActionItem 163 | extends InteractiveCardActionBaseItem { 164 | tag: 'overflow'; 165 | options?: (InteractiveCardActionOptionItem & { 166 | url?: string; 167 | multi_utl?: InteractiveCardUrlItem; 168 | })[]; 169 | value?: Record; 170 | } 171 | 172 | export interface InteractiveCardSelectMenuActionItem 173 | extends InteractiveCardActionBaseItem { 174 | tag: 'select_static' | 'select_person'; 175 | placeholder?: InteractiveCardPlainTextItem; 176 | initial_option?: string; 177 | option?: InteractiveCardActionOptionItem[]; 178 | value?: Record; 179 | } 180 | -------------------------------------------------------------------------------- /typings/http.ts: -------------------------------------------------------------------------------- 1 | export interface HttpInstance { 2 | request(opts: HttpRequestOptions): Promise; 3 | get(url: string, opts?: HttpRequestOptions): Promise; 4 | delete(url: string, opts?: HttpRequestOptions): Promise; 5 | head(url: string, opts?: HttpRequestOptions): Promise; 6 | options(url: string, opts?: HttpRequestOptions): Promise; 7 | post(url: string, data?: D, opts?: HttpRequestOptions): Promise; 8 | put(url: string, data?: D, opts?: HttpRequestOptions): Promise; 9 | patch(url: string, data?: D, opts?: HttpRequestOptions): Promise; 10 | } 11 | 12 | export type ResponseType = 13 | | 'arraybuffer' 14 | | 'blob' 15 | | 'document' 16 | | 'json' 17 | | 'text' 18 | | 'stream'; 19 | 20 | export interface HttpRequestOptions { 21 | url?: string; 22 | method?: string; 23 | headers?: Record; 24 | params?: Record; 25 | data?: D; 26 | responseType?: ResponseType; 27 | paramsSerializer?: (params: Record) => string; 28 | } 29 | -------------------------------------------------------------------------------- /typings/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | /* eslint-disable no-unused-vars */ 3 | export interface Cache { 4 | set: ( 5 | key: string | Symbol, 6 | value: any, 7 | expire?: number, 8 | options?: { 9 | namespace?: string 10 | } 11 | ) => Promise; 12 | get: (key: string | Symbol, options?: { 13 | namespace?: string 14 | }) => Promise; 15 | } 16 | 17 | export interface Logger { 18 | error: (...msg: any[]) => void | Promise; 19 | warn: (...msg: any[]) => void | Promise; 20 | info: (...msg: any[]) => void | Promise; 21 | debug: (...msg: any[]) => void | Promise; 22 | trace: (...msg: any[]) => void | Promise; 23 | } 24 | 25 | export enum AppType { 26 | SelfBuild, 27 | ISV, 28 | } 29 | 30 | export enum Domain { 31 | Feishu, 32 | Lark, 33 | } 34 | 35 | export enum LoggerLevel { 36 | fatal, 37 | error, 38 | warn, 39 | info, 40 | debug, 41 | trace, 42 | } 43 | -------------------------------------------------------------------------------- /utils/__tests__/assert.ts: -------------------------------------------------------------------------------- 1 | import { assert } from '../assert'; 2 | 3 | describe('assert', () => { 4 | test('predication is boolean', () => { 5 | const callback = jest.fn(() => {}); 6 | 7 | assert(false, callback); 8 | expect(callback).not.toBeCalled(); 9 | callback.mockClear(); 10 | 11 | assert(true, callback); 12 | expect(callback).toBeCalled(); 13 | }); 14 | 15 | test('predication is function', () => { 16 | const callback = jest.fn(() => {}); 17 | 18 | assert(() => false, callback); 19 | expect(callback).not.toBeCalled(); 20 | callback.mockClear(); 21 | 22 | assert(() => true, callback); 23 | expect(callback).toBeCalled(); 24 | }); 25 | 26 | test('async case', () => { 27 | const callback = jest.fn(async () => {}); 28 | 29 | assert(() => true, callback); 30 | expect(callback).toBeCalled(); 31 | callback.mockClear(); 32 | 33 | assert(false, callback); 34 | expect(callback).not.toBeCalled(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /utils/__tests__/fill-api-path.ts: -------------------------------------------------------------------------------- 1 | import { fillApiPath } from '../fill-api-path'; 2 | 3 | describe('fillApiPath', () => { 4 | test('fill right', () => { 5 | expect(fillApiPath('https://aaa', {})).toBe('https://aaa'); 6 | expect(fillApiPath('https://s:aaa', { aaa: 'b' })).toBe('https://sb'); 7 | expect(fillApiPath('https://s:aaa/:ccc', { aaa: 'b', ccc: 'cc' })).toBe( 8 | 'https://sb/cc' 9 | ); 10 | }); 11 | 12 | test('miss argument', () => { 13 | expect(() => 14 | fillApiPath('http://s:aaa/:bbb', { aaa: '1' }) 15 | ).toThrowError('request miss bbb path argument'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /utils/__tests__/format-url.ts: -------------------------------------------------------------------------------- 1 | import { formatUrl } from '../format-url'; 2 | 3 | describe('format url', () => { 4 | test('format right', () => { 5 | expect(formatUrl('/a/b/c')).toBe('a/b/c'); 6 | expect(formatUrl('a/b/c')).toBe('a/b/c'); 7 | expect(formatUrl('')).toBe(''); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /utils/__tests__/merge-object.ts: -------------------------------------------------------------------------------- 1 | import { mergeObject } from '../merge-object'; 2 | 3 | describe('merge object', () => { 4 | test('normal merge', () => { 5 | expect(mergeObject({a: 1}, {b: 2})).toEqual({a: 1, b: 2}); 6 | }); 7 | 8 | test('intersect merge', () => { 9 | expect(mergeObject({a: 1}, {a: 2})).toEqual({a: 2}); 10 | }); 11 | 12 | test('undefined merge', () => { 13 | expect(mergeObject({a: 1, b: undefined}, { b: 2, c: undefined })).toEqual({a: 1, b: 2}); 14 | expect(mergeObject({a: 1, b: undefined}, { a: 2, c: 3 })).toEqual({a: 2, b: undefined, c: 3}); 15 | }); 16 | }); -------------------------------------------------------------------------------- /utils/aes-cipher.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | export class AESCipher { 4 | key: Buffer; 5 | 6 | constructor(key) { 7 | const hash = crypto.createHash('sha256'); 8 | hash.update(key); 9 | this.key = hash.digest(); 10 | } 11 | 12 | decrypt(encrypt) { 13 | const encryptBuffer = Buffer.from(encrypt, 'base64'); 14 | const decipher = crypto.createDecipheriv( 15 | 'aes-256-cbc', 16 | this.key, 17 | encryptBuffer.slice(0, 16) 18 | ); 19 | let decrypted = decipher.update( 20 | encryptBuffer.slice(16).toString('hex'), 21 | 'hex', 22 | 'utf8' 23 | ); 24 | decrypted += decipher.final('utf8'); 25 | return decrypted; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /utils/assert.ts: -------------------------------------------------------------------------------- 1 | export const assert = async ( 2 | predication: boolean | (() => boolean), 3 | callback: () => any | Promise 4 | ) => { 5 | const isInvoke = 6 | typeof predication === 'function' ? predication() : predication; 7 | 8 | if (isInvoke) { 9 | await callback(); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /utils/default-cache.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '@node-sdk/typings'; 2 | 3 | export class DefaultCache implements Cache { 4 | values: Map< 5 | string | Symbol, 6 | { 7 | value: any; 8 | expiredTime?: number; 9 | } 10 | >; 11 | 12 | constructor() { 13 | this.values = new Map(); 14 | } 15 | 16 | // When there is a namespace, splice the namespace and key to form a new key 17 | private getCacheKey(key: string | Symbol, namespace?: string) { 18 | if (namespace) { 19 | return `${namespace}/${key.toString()}`; 20 | } 21 | return key; 22 | } 23 | 24 | async get(key: string | Symbol, options?: { 25 | namespace?: string 26 | }) { 27 | const cacheKey = this.getCacheKey(key, options?.namespace); 28 | const data = this.values.get(cacheKey); 29 | 30 | if (data) { 31 | const { value, expiredTime } = data; 32 | if (!expiredTime || expiredTime - new Date().getTime() > 0) { 33 | return value; 34 | } 35 | } 36 | 37 | return undefined; 38 | } 39 | 40 | async set(key: string | Symbol, value: string, expiredTime?: number, options?: { 41 | namespace?: string 42 | }) { 43 | const cacheKey = this.getCacheKey(key, options?.namespace); 44 | this.values.set(cacheKey, { 45 | value, 46 | expiredTime, 47 | }); 48 | return true; 49 | } 50 | } 51 | 52 | export const internalCache = new DefaultCache(); 53 | -------------------------------------------------------------------------------- /utils/fill-api-path.ts: -------------------------------------------------------------------------------- 1 | export const fillApiPath = ( 2 | apiPath: string, 3 | pathSupplement: Record = {} 4 | ) => 5 | apiPath.replace(/:([^/]+)/g, (_, $1) => { 6 | if (pathSupplement[$1] !== undefined) { 7 | return pathSupplement[$1]; 8 | } 9 | 10 | throw new Error(`request miss ${$1} path argument`); 11 | }); 12 | -------------------------------------------------------------------------------- /utils/format-domain.ts: -------------------------------------------------------------------------------- 1 | import { Domain } from '@node-sdk/typings'; 2 | 3 | export const formatDomain = (domain: Domain | string): string => { 4 | switch (domain) { 5 | case Domain.Feishu: 6 | return 'https://open.feishu.cn'; 7 | case Domain.Lark: 8 | return 'https://open.larksuite.com'; 9 | default: 10 | return domain; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /utils/format-url.ts: -------------------------------------------------------------------------------- 1 | export const formatUrl = (url?: string) => (url ? url.replace(/^\//, '') : ''); 2 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './string-2-base64'; 2 | export * from './default-cache'; 3 | export * from './aes-cipher'; 4 | export * from './format-domain'; 5 | export * from './fill-api-path'; 6 | export * from './assert'; 7 | export * from './format-url'; 8 | -------------------------------------------------------------------------------- /utils/merge-object.ts: -------------------------------------------------------------------------------- 1 | export const mergeObject = (obj1: Record, obj2: Record) => { 2 | const mergedObject = { ...obj1 }; 3 | 4 | for (let [key, value] of Object.entries(obj2)) { 5 | if (value !== undefined) { 6 | mergedObject[key] = value; 7 | } 8 | } 9 | 10 | return mergedObject; 11 | } -------------------------------------------------------------------------------- /utils/message-card.ts: -------------------------------------------------------------------------------- 1 | export const defaultCard = (variables: { 2 | title: string; 3 | content: string; 4 | }) => { 5 | const { title, content } = variables; 6 | 7 | return JSON.stringify({ 8 | "config": { 9 | "wide_screen_mode": true 10 | }, 11 | "elements": [ 12 | { 13 | "tag": "markdown", 14 | "content": content 15 | } 16 | ], 17 | "header": { 18 | "template": "blue", 19 | "title": { 20 | "content": title, 21 | "tag": "plain_text" 22 | } 23 | } 24 | }) 25 | } -------------------------------------------------------------------------------- /utils/pick.ts: -------------------------------------------------------------------------------- 1 | export const pick = (obj?: T, keys: K[] = []): Pick => { 2 | const result = {} as Pick; 3 | 4 | if (!obj) { 5 | return result; 6 | } 7 | 8 | keys.forEach(key => { 9 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 10 | result[key] = obj[key]; 11 | } 12 | }); 13 | return result; 14 | } -------------------------------------------------------------------------------- /utils/string-2-base64.ts: -------------------------------------------------------------------------------- 1 | export const string2Base64 = (content: string) => 2 | Buffer.from(content).toString('base64'); -------------------------------------------------------------------------------- /ws-client/__tests__/data-cache.ts: -------------------------------------------------------------------------------- 1 | import { DataCache } from '../data-cache'; 2 | 3 | describe('DataCache', () => { 4 | test('mergeData is right when sum = 1', () => { 5 | const dataCache = new DataCache({}); 6 | const text = '{"data":"hello,world"}'; 7 | 8 | const mockData = { 9 | message_id: 'message_id', 10 | sum: 1, 11 | seq: 0, 12 | trace_id: 'trace_id', 13 | data: new TextEncoder().encode(text) 14 | } 15 | 16 | const combined = dataCache.mergeData(mockData); 17 | const { data } = combined!; 18 | expect(data).toEqual("hello,world"); 19 | expect(dataCache.cache.get('message_id')).toBeUndefined(); 20 | }); 21 | 22 | test('mergeData is right when sum > 2', () => { 23 | const dataCache = new DataCache({}); 24 | const text1 = '{"data":"hello,'; 25 | const text2 = 'world"}'; 26 | 27 | const mockData1 = { 28 | message_id: 'message_id', 29 | sum: 2, 30 | seq: 0, 31 | trace_id: 'trace_id', 32 | data: new TextEncoder().encode(text1) 33 | } 34 | 35 | const combined1 = dataCache.mergeData(mockData1); 36 | expect(combined1).toBe(null); 37 | 38 | const mockData2 = { 39 | message_id: 'message_id', 40 | sum: 2, 41 | seq: 1, 42 | trace_id: 'trace_id', 43 | data: new TextEncoder().encode(text2) 44 | } 45 | 46 | const combined2 = dataCache.mergeData(mockData2); 47 | const { data } = combined2!; 48 | expect(data).toEqual("hello,world"); 49 | }); 50 | 51 | test('data is expired', () => { 52 | jest.useFakeTimers(); 53 | 54 | const dataCache = new DataCache({}); 55 | const text = '{"data":"hello,world"}'; 56 | 57 | const mockData = { 58 | message_id: 'message_id', 59 | sum: 2, 60 | seq: 0, 61 | trace_id: 'trace_id', 62 | data: new TextEncoder().encode(text) 63 | } 64 | 65 | dataCache.mergeData(mockData); 66 | 67 | jest.advanceTimersByTime(10000 * 2 + 100); 68 | 69 | expect(dataCache.cache.get('message_id')).toBeUndefined(); 70 | }); 71 | 72 | test('data is lived', () => { 73 | jest.useFakeTimers(); 74 | 75 | const dataCache = new DataCache({}); 76 | const text = '{"data":"hello,world"}'; 77 | 78 | const mockData = { 79 | message_id: 'message_id', 80 | sum: 2, 81 | seq: 0, 82 | trace_id: 'trace_id', 83 | data: new TextEncoder().encode(text) 84 | } 85 | 86 | dataCache.mergeData(mockData); 87 | 88 | jest.advanceTimersByTime(1000 * 5); 89 | 90 | expect(dataCache.cache.get('message_id')).not.toBeUndefined(); 91 | }); 92 | 93 | }) 94 | -------------------------------------------------------------------------------- /ws-client/data-cache.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@node-sdk/typings'; 2 | 3 | export class DataCache { 4 | cache: Map 10 | 11 | logger?: Logger; 12 | 13 | constructor(params: { 14 | logger?: Logger; 15 | }) { 16 | this.cache = new Map(); 17 | this.logger = params.logger; 18 | this.clearAtInterval(); 19 | } 20 | 21 | mergeData(params: { 22 | message_id: string; 23 | sum: number; 24 | seq: number; 25 | trace_id: string; 26 | data: Uint8Array; 27 | }) { 28 | const { message_id, sum, seq, trace_id, data } = params; 29 | 30 | const cache = this.cache.get(message_id); 31 | if (!cache) { 32 | const buffer = new Array(sum).fill(undefined); 33 | buffer[seq] = data; 34 | this.cache.set(message_id, { 35 | buffer, 36 | trace_id, 37 | message_id, 38 | create_time: Date.now() 39 | }); 40 | } else { 41 | cache.buffer[seq] = data; 42 | } 43 | 44 | const mergedCache = this.cache.get(message_id); 45 | if (mergedCache?.buffer.every(item => !!item)) { 46 | const mergedBuffer = mergedCache.buffer.reduce((acc, cur) => { 47 | const combined = new Uint8Array(acc.byteLength + cur.byteLength); 48 | combined.set(acc, 0); 49 | combined.set(cur, acc.length); 50 | 51 | return combined; 52 | }, new Uint8Array([])); 53 | 54 | const string = new TextDecoder("utf-8").decode(mergedBuffer); 55 | const data = JSON.parse(string); 56 | 57 | this.deleteCache(message_id); 58 | 59 | return data; 60 | } 61 | 62 | return null; 63 | } 64 | 65 | private deleteCache(message_id: string) { 66 | this.cache.delete(message_id); 67 | } 68 | 69 | private clearAtInterval() { 70 | // magic number,10s expired 71 | const clearIntervalMs = 10000; 72 | setInterval(() => { 73 | const now = Date.now(); 74 | this.cache.forEach((value, key) => { 75 | const { create_time, trace_id, message_id } = value; 76 | if (now - create_time > clearIntervalMs) { 77 | this.logger?.debug(`${message_id} event data is deleted as expired, trace_id: ${trace_id}`); 78 | this.deleteCache(key); 79 | } 80 | }); 81 | }, clearIntervalMs); 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /ws-client/enum.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorCode { 2 | ok = 0, 3 | system_busy = 1, 4 | forbidden = 403, 5 | auth_failed = 514, 6 | internal_error = 1000040343, 7 | exceed_conn_limit = 1000040350, 8 | } 9 | 10 | export enum FrameType { 11 | control = 0, 12 | data = 1 13 | } 14 | 15 | export enum HeaderKey { 16 | type = "type", 17 | message_id = "message_id", 18 | sum = "sum", 19 | seq = "seq", 20 | trace_id = "trace_id", 21 | biz_rt = "biz_rt", 22 | handshake_status = "handshake-status", 23 | handshake_msg = "handshake-msg", 24 | handshake_autherrcode = "handshake-autherrcode", 25 | } 26 | 27 | export enum MessageType { 28 | event = "event", 29 | card = "card", 30 | ping = "ping", 31 | pong = "pong" 32 | } 33 | 34 | export enum HttpStatusCode { 35 | // 2xx Success 36 | ok = 200, 37 | // 5xx Server errors 38 | internal_server_error = 500, 39 | } -------------------------------------------------------------------------------- /ws-client/proto-buf/index.ts: -------------------------------------------------------------------------------- 1 | import { pbbp2 } from './pbbp2'; 2 | 3 | export const decode = (buffer: Uint8Array) => { 4 | return pbbp2.Frame.decode(buffer); 5 | } 6 | 7 | export const encode = (data: pbbp2.IFrame) => { 8 | return pbbp2.Frame.encode(data); 9 | } -------------------------------------------------------------------------------- /ws-client/proto-buf/pbbp2.d.ts: -------------------------------------------------------------------------------- 1 | import * as $protobuf from "protobufjs"; 2 | import Long = require("long"); 3 | /** Namespace pbbp2. */ 4 | export namespace pbbp2 { 5 | 6 | /** Properties of a Header. */ 7 | interface IHeader { 8 | 9 | /** Header key */ 10 | key: string; 11 | 12 | /** Header value */ 13 | value: string; 14 | } 15 | 16 | /** Represents a Header. */ 17 | class Header implements IHeader { 18 | 19 | /** 20 | * Constructs a new Header. 21 | * @param [properties] Properties to set 22 | */ 23 | constructor(properties?: pbbp2.IHeader); 24 | 25 | /** Header key. */ 26 | public key: string; 27 | 28 | /** Header value. */ 29 | public value: string; 30 | 31 | /** 32 | * Creates a new Header instance using the specified properties. 33 | * @param [properties] Properties to set 34 | * @returns Header instance 35 | */ 36 | public static create(properties?: pbbp2.IHeader): pbbp2.Header; 37 | 38 | /** 39 | * Encodes the specified Header message. Does not implicitly {@link pbbp2.Header.verify|verify} messages. 40 | * @param message Header message or plain object to encode 41 | * @param [writer] Writer to encode to 42 | * @returns Writer 43 | */ 44 | public static encode(message: pbbp2.IHeader, writer?: $protobuf.Writer): $protobuf.Writer; 45 | 46 | /** 47 | * Encodes the specified Header message, length delimited. Does not implicitly {@link pbbp2.Header.verify|verify} messages. 48 | * @param message Header message or plain object to encode 49 | * @param [writer] Writer to encode to 50 | * @returns Writer 51 | */ 52 | public static encodeDelimited(message: pbbp2.IHeader, writer?: $protobuf.Writer): $protobuf.Writer; 53 | 54 | /** 55 | * Decodes a Header message from the specified reader or buffer. 56 | * @param reader Reader or buffer to decode from 57 | * @param [length] Message length if known beforehand 58 | * @returns Header 59 | * @throws {Error} If the payload is not a reader or valid buffer 60 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 61 | */ 62 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): pbbp2.Header; 63 | 64 | /** 65 | * Decodes a Header message from the specified reader or buffer, length delimited. 66 | * @param reader Reader or buffer to decode from 67 | * @returns Header 68 | * @throws {Error} If the payload is not a reader or valid buffer 69 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 70 | */ 71 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): pbbp2.Header; 72 | 73 | /** 74 | * Verifies a Header message. 75 | * @param message Plain object to verify 76 | * @returns `null` if valid, otherwise the reason why it is not 77 | */ 78 | public static verify(message: { [k: string]: any }): (string|null); 79 | 80 | /** 81 | * Creates a Header message from a plain object. Also converts values to their respective internal types. 82 | * @param object Plain object 83 | * @returns Header 84 | */ 85 | public static fromObject(object: { [k: string]: any }): pbbp2.Header; 86 | 87 | /** 88 | * Creates a plain object from a Header message. Also converts values to other types if specified. 89 | * @param message Header 90 | * @param [options] Conversion options 91 | * @returns Plain object 92 | */ 93 | public static toObject(message: pbbp2.Header, options?: $protobuf.IConversionOptions): { [k: string]: any }; 94 | 95 | /** 96 | * Converts this Header to JSON. 97 | * @returns JSON object 98 | */ 99 | public toJSON(): { [k: string]: any }; 100 | 101 | /** 102 | * Gets the default type url for Header 103 | * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") 104 | * @returns The default type url 105 | */ 106 | public static getTypeUrl(typeUrlPrefix?: string): string; 107 | } 108 | 109 | /** Properties of a Frame. */ 110 | interface IFrame { 111 | 112 | /** Frame SeqID */ 113 | SeqID: number; 114 | 115 | /** Frame LogID */ 116 | LogID: number; 117 | 118 | /** Frame service */ 119 | service: number; 120 | 121 | /** Frame method */ 122 | method: number; 123 | 124 | /** Frame headers */ 125 | headers?: (pbbp2.IHeader[]|null); 126 | 127 | /** Frame payloadEncoding */ 128 | payloadEncoding?: (string|null); 129 | 130 | /** Frame payloadType */ 131 | payloadType?: (string|null); 132 | 133 | /** Frame payload */ 134 | payload?: (Uint8Array|null); 135 | 136 | /** Frame LogIDNew */ 137 | LogIDNew?: (string|null); 138 | } 139 | 140 | /** Represents a Frame. */ 141 | class Frame implements IFrame { 142 | 143 | /** 144 | * Constructs a new Frame. 145 | * @param [properties] Properties to set 146 | */ 147 | constructor(properties?: pbbp2.IFrame); 148 | 149 | /** Frame SeqID. */ 150 | public SeqID: number; 151 | 152 | /** Frame LogID. */ 153 | public LogID: number; 154 | 155 | /** Frame service. */ 156 | public service: number; 157 | 158 | /** Frame method. */ 159 | public method: number; 160 | 161 | /** Frame headers. */ 162 | public headers: pbbp2.IHeader[]; 163 | 164 | /** Frame payloadEncoding. */ 165 | public payloadEncoding: string; 166 | 167 | /** Frame payloadType. */ 168 | public payloadType: string; 169 | 170 | /** Frame payload. */ 171 | public payload: Uint8Array; 172 | 173 | /** Frame LogIDNew. */ 174 | public LogIDNew: string; 175 | 176 | /** 177 | * Creates a new Frame instance using the specified properties. 178 | * @param [properties] Properties to set 179 | * @returns Frame instance 180 | */ 181 | public static create(properties?: pbbp2.IFrame): pbbp2.Frame; 182 | 183 | /** 184 | * Encodes the specified Frame message. Does not implicitly {@link pbbp2.Frame.verify|verify} messages. 185 | * @param message Frame message or plain object to encode 186 | * @param [writer] Writer to encode to 187 | * @returns Writer 188 | */ 189 | public static encode(message: pbbp2.IFrame, writer?: $protobuf.Writer): $protobuf.Writer; 190 | 191 | /** 192 | * Encodes the specified Frame message, length delimited. Does not implicitly {@link pbbp2.Frame.verify|verify} messages. 193 | * @param message Frame message or plain object to encode 194 | * @param [writer] Writer to encode to 195 | * @returns Writer 196 | */ 197 | public static encodeDelimited(message: pbbp2.IFrame, writer?: $protobuf.Writer): $protobuf.Writer; 198 | 199 | /** 200 | * Decodes a Frame message from the specified reader or buffer. 201 | * @param reader Reader or buffer to decode from 202 | * @param [length] Message length if known beforehand 203 | * @returns Frame 204 | * @throws {Error} If the payload is not a reader or valid buffer 205 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 206 | */ 207 | public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): pbbp2.Frame; 208 | 209 | /** 210 | * Decodes a Frame message from the specified reader or buffer, length delimited. 211 | * @param reader Reader or buffer to decode from 212 | * @returns Frame 213 | * @throws {Error} If the payload is not a reader or valid buffer 214 | * @throws {$protobuf.util.ProtocolError} If required fields are missing 215 | */ 216 | public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): pbbp2.Frame; 217 | 218 | /** 219 | * Verifies a Frame message. 220 | * @param message Plain object to verify 221 | * @returns `null` if valid, otherwise the reason why it is not 222 | */ 223 | public static verify(message: { [k: string]: any }): (string|null); 224 | 225 | /** 226 | * Creates a Frame message from a plain object. Also converts values to their respective internal types. 227 | * @param object Plain object 228 | * @returns Frame 229 | */ 230 | public static fromObject(object: { [k: string]: any }): pbbp2.Frame; 231 | 232 | /** 233 | * Creates a plain object from a Frame message. Also converts values to other types if specified. 234 | * @param message Frame 235 | * @param [options] Conversion options 236 | * @returns Plain object 237 | */ 238 | public static toObject(message: pbbp2.Frame, options?: $protobuf.IConversionOptions): { [k: string]: any }; 239 | 240 | /** 241 | * Converts this Frame to JSON. 242 | * @returns JSON object 243 | */ 244 | public toJSON(): { [k: string]: any }; 245 | 246 | /** 247 | * Gets the default type url for Frame 248 | * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") 249 | * @returns The default type url 250 | */ 251 | public static getTypeUrl(typeUrlPrefix?: string): string; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /ws-client/ws-config.ts: -------------------------------------------------------------------------------- 1 | import { Domain } from '@node-sdk/typings'; 2 | import WebSocket from 'ws'; 3 | 4 | interface IClientConfig { 5 | appId: string; 6 | appSecret: string; 7 | domain: string | Domain; 8 | } 9 | 10 | interface IWSConfig { 11 | connectUrl: string; 12 | pingInterval: number; 13 | reconnectCount: number; 14 | reconnectInterval: number; 15 | reconnectNonce: number; 16 | deviceId: string; 17 | serviceId: string; 18 | autoReconnect: boolean; 19 | } 20 | 21 | export class WSConfig { 22 | private client: IClientConfig = { 23 | appId: '', 24 | appSecret: '', 25 | domain: Domain.Feishu, 26 | } 27 | 28 | private ws: IWSConfig = { 29 | connectUrl: '', 30 | 31 | pingInterval: 120 * 1000, 32 | reconnectCount: -1, 33 | reconnectInterval: 120 * 1000, 34 | reconnectNonce: 30 * 1000, 35 | 36 | deviceId: '', 37 | serviceId: '', 38 | 39 | autoReconnect: true 40 | }; 41 | 42 | private wsInstance: WebSocket | null = null; 43 | 44 | constructor() {} 45 | 46 | updateClient(clientConfig: Partial) { 47 | Object.assign(this.client, clientConfig); 48 | } 49 | 50 | updateWs(WSConfig: Partial) { 51 | Object.assign(this.ws, WSConfig); 52 | } 53 | 54 | getClient(): IClientConfig; 55 | getClient(key: T): IClientConfig[T]; 56 | getClient(key?: T): IClientConfig | IClientConfig[T] { 57 | if (key === undefined) { 58 | return this.client; 59 | } 60 | return this.client[key]; 61 | } 62 | 63 | getWS(): IWSConfig; 64 | getWS(key: T): IWSConfig[T]; 65 | getWS(key?: T): IWSConfig | IWSConfig[T] { 66 | if (key === undefined) { 67 | return this.ws; 68 | } 69 | return this.ws[key]; 70 | } 71 | 72 | get wsConfigUrl() { 73 | return `${this.getClient("domain")}/callback/ws/endpoint`; 74 | } 75 | 76 | setWSInstance(wsInstance: WebSocket | null) { 77 | this.wsInstance = wsInstance; 78 | } 79 | 80 | getWSInstance() { 81 | return this.wsInstance; 82 | } 83 | } 84 | --------------------------------------------------------------------------------