feat(net-tool): 支持飞书接口
All checks were successful
/ release (push) Successful in 28s

This commit is contained in:
zhaoyingbo 2024-11-24 03:28:29 +00:00
parent b441f3132a
commit 16d2b2ba79
11 changed files with 745 additions and 5 deletions

View File

@ -1,7 +1,8 @@
import GitlabService from "./gitlabServer"
import LarkService from "./larkServer"
import NetTool from "./netTool"
import NetToolBase from "./netTool/base"
import type { Gitlab, NetErrorDetail, NetRequestParams } from "./types"
import NetToolBase, { NetError } from "./netTool/base"
import type { Gitlab, Lark, NetErrorDetail, NetRequestParams } from "./types"
export { GitlabService, NetTool, NetToolBase }
export type { Gitlab, NetErrorDetail, NetRequestParams }
export { GitlabService, LarkService, NetError, NetTool, NetToolBase }
export type { Gitlab, Lark, NetErrorDetail, NetRequestParams }

View File

@ -0,0 +1,15 @@
import LarkBaseService from "./base"
class LarkAuthService extends LarkBaseService {
getAk(appId: string, appSecret: string) {
return this.post<{ tenant_access_token: string; code: number }>(
"/auth/v3/tenant_access_token/internal",
{
app_id: appId,
app_secret: appSecret,
}
)
}
}
export default LarkAuthService

View File

@ -0,0 +1,28 @@
import NetToolBase, { NetError } from "../netTool/base"
class LarkBaseService extends NetToolBase {
constructor(getToken: () => Promise<string>, requestId: string) {
super({
prefix: "https://open.f.mioffice.cn/open-apis",
requestId,
getHeaders: async () => ({
Authorization: `Bearer ${await getToken()}`,
}),
})
}
protected async request<T = any>(params: any): Promise<T> {
return super.request<T>(params).catch((error: NetError) => {
const res = {
httpStatus: error.response?.status,
code: error.code,
data: error.data,
message: error.message,
} as T
this.logger.error(`larkNetTool catch error: ${JSON.stringify(res)}`)
return res
})
}
}
export default LarkBaseService

View File

@ -0,0 +1,42 @@
import { Lark } from "../types"
import LarkBaseService from "./base"
class LarkChatService extends LarkBaseService {
/**
*
*/
async getInnerList() {
const path = "/im/v1/chats"
const chatList = []
let hasMore = true
let pageToken = ""
while (hasMore) {
const { data, code } = await this.get<
Lark.BaseListRes<Lark.ChatGroupData>
>(path, {
page_size: 100,
page_token: pageToken,
})
if (code !== 0) break
chatList.push(...data.items)
hasMore = data.has_more
pageToken = data.page_token
}
return {
code: 0,
data: chatList,
message: "ok",
}
}
/**
*
* @param chatId ID
*/
async getChatInfo(chatId: string) {
const path = `/im/v1/chats/${chatId}`
return this.get<Lark.BaseRes<Lark.ChatGroupData>>(path)
}
}
export default LarkChatService

View File

@ -0,0 +1,141 @@
import { Lark } from "../types"
import LarkBaseService from "./base"
class LarkDriveService extends LarkBaseService {
/**
*
*
* @param docTokens -
* @param docType - "doc"
* @param userIdType - ID类型 "user_id"
* @returns
*/
async batchGetMeta(
docTokens: string[],
docType = "doc",
userIdType = "user_id"
) {
const path = "/drive/v1/metas/batch_query"
// 如果docTokens长度超出150需要分批请求
const docTokensLen = docTokens.length
const maxLen = 150
const requestMap = Array.from(
{ length: Math.ceil(docTokensLen / maxLen) },
(_, index) => {
const start = index * maxLen
const docTokensSlice = docTokens.slice(start, start + maxLen)
const data = {
request_docs: docTokensSlice.map((id) => ({
doc_token: id,
doc_type: docType,
})),
}
return this.post<Lark.BatchDocMetaRes>(path, data, {
user_id_type: userIdType,
})
}
)
const responses = await Promise.all(requestMap)
const metas = responses.flatMap((res) => {
return res.data?.metas || []
})
const failedList = responses.flatMap((res) => {
return res.data?.failed_list || []
})
return {
code: 0,
data: {
metas,
failedList,
},
message: "success",
}
}
/**
*
*
* @param folderToken -
* @returns
*/
async listFiles(folderToken: string) {
const path = "/drive/v1/files"
return this.get<Lark.BaseRes>(path, {
folder_token: folderToken,
})
}
/**
*
*
* @param folderToken -
* @param fileName -
* @param fileType -
* @returns Promise
*/
async createFile(
folderToken: string,
fileName: string,
fileType: "doc" | "sheet" | "bitable"
) {
const path = `/drive/explorer/v2/file/${folderToken}`
return this.post<Lark.BaseRes>(path, {
title: fileName,
type: fileType,
})
}
/**
*
*
* @param folderToken -
* @param fileToken -
* @param fileName -
* @param fileType -
* @returns Promise
*/
async copyFile(
folderToken: string,
fileToken: string,
fileName: string,
fileType: Lark.FileType
) {
const path = `/drive/v1/files/${fileToken}/copy`
return this.post<Lark.BaseRes<Lark.CopyFileData>>(path, {
type: fileType,
folder_token: folderToken,
name: fileName,
})
}
/**
*
*
* @param fileToken -
* @returns Promise
*/
async addCollaborator(
fileToken: string,
fileType: Lark.FileType,
memberType: "userid" | "openchat",
memberId: string,
perm: "view" | "edit" | "full_access"
) {
const path = `/drive/v1/permissions/${fileToken}/members`
return this.post<Lark.BaseRes>(
path,
{
member_type: memberType,
member_id: memberId,
perm,
},
{
type: fileType,
}
)
}
}
export default LarkDriveService

View File

@ -0,0 +1,32 @@
import LarkAuthService from "./auth"
import LarkChatService from "./chat"
import LarkDriveService from "./drive"
import LarkMessageService from "./message"
import LarkSheetService from "./sheet"
import LarkUserService from "./user"
class LarkService {
drive: LarkDriveService
message: LarkMessageService
user: LarkUserService
sheet: LarkSheetService
auth: LarkAuthService
chat: LarkChatService
requestId: string
constructor(getToken: () => Promise<string>, requestId: string) {
this.drive = new LarkDriveService(getToken, requestId)
this.message = new LarkMessageService(getToken, requestId)
this.user = new LarkUserService(getToken, requestId)
this.sheet = new LarkSheetService(getToken, requestId)
this.auth = new LarkAuthService(getToken, requestId)
this.chat = new LarkChatService(getToken, requestId)
this.requestId = requestId
}
child(getToken: () => Promise<string>, requestId?: string) {
return new LarkService(getToken, requestId || this.requestId)
}
}
export default LarkService

View File

@ -0,0 +1,102 @@
import { Lark } from "../types"
import LarkBaseService from "./base"
class LarkMessageService extends LarkBaseService {
/**
*
* @param receiveIdType id类型 open_id/user_id/union_id/email/chat_id
* @param receiveId IDID类型应与查询参数receiveIdType
* @param msgType textpostimagefileaudiomediastickerinteractiveshare_chatshare_user
* @param content JSON结构序列化后的字符串msgType对应不同内容
*/
async send(
receiveIdType: Lark.ReceiveIDType,
receiveId: string,
msgType: Lark.MsgType,
content: string | Record<string, any>
) {
const path = `/im/v1/messages?receive_id_type=${receiveIdType}`
if (typeof content === "object") {
content = JSON.stringify(content)
}
if (msgType === "text" && !content.includes('"text"')) {
content = JSON.stringify({ text: content })
}
return this.post<Lark.BaseRes<{ message_id: string }>>(path, {
receive_id: receiveId,
msg_type: msgType,
content,
})
}
/**
*
* @param receiveId IDID类型应与查询参数receiveIdType
* @param content
*/
async sendCard2Chat(
receiveId: string,
content: string | Record<string, any>
) {
return this.send("chat_id", receiveId, "interactive", content)
}
/**
*
* @param receiveId IDID类型应与查询参数receiveIdType
* @param content
*/
async sendText2Chat(receiveId: string, content: string) {
return this.send("chat_id", receiveId, "text", content)
}
/**
*
* @param messageId id
* @param content JSON结构序列化后的字符串msgType对应不同内容
*/
async update(messageId: string, content: string | Record<string, any>) {
const path = `/im/v1/messages/${messageId}`
if (typeof content === "object") {
content = JSON.stringify(content)
}
return this.patch<Lark.BaseRes>(path, { content })
}
/**
*
* @param chatId ID
* @param startTime
* @param endTime
*/
async getHistory(chatId: string, startTime: string, endTime: string) {
const path = `/im/v1/messages`
const messageList = [] as Lark.MessageData[]
let hasMore = true
let pageToken = ""
while (hasMore) {
const { code, data } = await this.get<Lark.BaseListRes<Lark.MessageData>>(
path,
{
container_id_type: "chat",
container_id: chatId,
start_time: startTime,
end_time: endTime,
page_size: 50,
page_token: pageToken,
}
)
if (code !== 0) break
messageList.push(...data.items)
hasMore = data.has_more
pageToken = data.page_token
}
return {
code: 0,
data: messageList,
message: "ok",
}
}
}
export default LarkMessageService

View File

@ -0,0 +1,103 @@
import { Lark } from "../types"
import LarkBaseService from "./base"
class LarkSheetService extends LarkBaseService {
/**
*
* @param sheetToken
* @param range
* @param values
* @returns Promise
*/
async insertRows(sheetToken: string, range: string, values: string[][]) {
const path = `/sheets/v2/spreadsheets/${sheetToken}/values_append?insertDataOption=INSERT_ROWS`
return this.post<Lark.BaseRes>(path, {
valueRange: {
range,
values,
},
})
}
/**
*
* @param sheetToken
* @param range
* @returns Promise
*/
async getRange(sheetToken: string, range: string) {
const path = `/sheets/v2/spreadsheets/${sheetToken}/values/${range}?valueRenderOption=ToString`
return this.get<Lark.SpreadsheetRes>(path)
}
/**
*
* @param appToken
* @returns Promise
*/
async getTables(appToken: string) {
const path = `/bitable/v1/apps/${appToken}/tables`
const tableList = [] as Lark.TableData[]
let hasMore = true
let pageToken = ""
while (hasMore) {
const { data, code } = await this.get<Lark.BaseListRes<Lark.TableData>>(
path,
{
page_size: 100,
page_token: pageToken,
}
)
if (code !== 0) break
tableList.push(...data.items)
hasMore = data.has_more
pageToken = data.page_token
}
return {
code: 0,
data: tableList,
message: "ok",
}
}
/**
*
* @param appToken
* @param tableId ID
* @returns Promise
*/
async getViews(appToken: string, tableId: string) {
const path = `/bitable/v1/apps/${appToken}/tables/${tableId}/views`
let has_more = true
const res = [] as Lark.ViewData[]
while (has_more) {
const { data, code } = await this.get<Lark.BaseListRes<Lark.ViewData>>(
path,
{
page_size: 100,
}
)
if (code !== 0) break
res.push(...data.items)
has_more = data.has_more
}
return {
code: 0,
data: res,
message: "ok",
}
}
/**
* ()
* @param appToken
* @param tableId ID
* @returns Promise
*/
async getRecords(appToken: string, tableId: string) {
const path = `/bitable/v1/apps/${appToken}/tables/${tableId}/records`
return this.get<Lark.BaseRes>(path)
}
}
export default LarkSheetService

View File

@ -0,0 +1,72 @@
import { Lark } from "../types"
import LarkBaseService from "./base"
class LarkUserService extends LarkBaseService {
/**
*
* @param code
* @returns
*/
async code2Login(code: string) {
const path = `/mina/v2/tokenLoginValidate`
return this.post<Lark.UserSessionRes>(path, { code })
}
/**
*
* @param userId ID
* @param userIdType ID类型
* @returns
*/
async getOne(userId: string, userIdType: "open_id" | "user_id") {
const path = `/contact/v3/users/${userId}`
return this.get<Lark.UserInfoRes>(path, {
user_id_type: userIdType,
})
}
/**
*
* @param userIds ID数组
* @param userIdType ID类型
* @returns
*/
async batchGet(
userIds: string[],
userIdType: "open_id" | "user_id" = "open_id"
) {
const path = `/contact/v3/users/batch`
// 如果user_id长度超出50需要分批请求,
const userCount = userIds.length
const maxLen = 50
const requestMap = Array.from(
{ length: Math.ceil(userCount / maxLen) },
(_, index) => {
const start = index * maxLen
const getParams = `${userIds
.slice(start, start + maxLen)
.map((id) => `user_ids=${id}`)
.join("&")}&user_id_type=${userIdType}`
return this.get<Lark.BatchUserInfoRes>(path, getParams)
}
)
const responses = await Promise.all(requestMap)
const items = responses.flatMap((res) => {
return res.data?.items || []
})
return {
code: 0,
data: {
items,
},
message: "success",
}
}
}
export default LarkUserService

View File

@ -1,4 +1,5 @@
import type { Gitlab } from "./gitlab"
import type { Lark } from "./lark"
export interface NetRequestParams {
url: string
@ -15,4 +16,4 @@ export interface NetErrorDetail {
data?: any
}
export type { Gitlab }
export type { Gitlab, Lark }

View File

@ -0,0 +1,203 @@
export namespace Lark {
export interface UserSession {
/**
* 访
*/
access_token: string
/**
* ID
*/
employee_id: string
/**
*
*/
expires_in: number
/**
* ID
*/
open_id: string
/**
*
*/
refresh_token: string
/**
*
*/
session_key: string
/**
*
*/
tenant_key: string
/**
* ID
*/
union_id: string
}
export interface SuccessDocMeta {
doc_token: string
doc_type: string
title: string
owner_id: string
create_time: string
latest_modify_user: string
latest_modify_time: string
url: string
sec_label_name: string
}
export interface FailedDocMeta {
token: string
code: number
}
export interface ValueRange {
majorDimension: string // 插入维度
range: string // 返回数据的范围,为空时表示查询范围没有数据
revision: number // sheet 的版本号
values: Array<Array<any>> // 查询得到的值
}
export interface SpreadsheetData {
revision: number // sheet 的版本号
spreadsheetToken: string // spreadsheet 的 token
valueRange: ValueRange // 值与范围
}
export interface CopyFileData {
file: {
name: string
parent_token: string
token: string
type: "doc" | "sheet" | "bitable"
url: string
}
}
export interface TableData {
table_id: string
revision: number
name: string
}
export interface ViewData {
view_id: string
view_name: string
view_public_level: "Public" | "Locked" | "Private"
view_type: string
view_private_owner_id?: string
}
export interface MessageData {
message_id: string
root_id: string
parent_id: string
msg_type: MsgType
create_time: string
update_time: string
deleted: boolean
updated: boolean
chat_id: string
sender: {
id: string
id_type: "open_id" | "app_id"
sender_type: "user" | "app"
}
body: {
content: string
}
mentions: any[]
upper_message_id: string
}
export interface ChatGroupData {
avatar: string
chat_id: string
description: string
external: boolean
name: string
owner_id: string
owner_id_type: "open_id" | "user_id"
tenant_key: string
}
export interface BaseRes<T = any> {
code: number
data: T
// 在错误处理中msg会被赋值为message
message: string
}
export interface BaseListRes<T = any> extends BaseRes {
data: {
has_more: boolean
page_token: string
total: number
items: T[]
}
}
export interface SpreadsheetRes extends BaseRes {
data: SpreadsheetData
}
export interface UserSessionRes extends BaseRes {
data: UserSession
}
export interface UserInfoRes extends BaseRes {
data: {
user: any
}
}
export interface BatchUserInfoRes extends BaseRes {
data: {
items: any[]
}
}
export interface BatchDocMetaRes extends BaseRes {
data: {
metas: SuccessDocMeta[]
failed_list: FailedDocMeta[]
}
}
export type ReceiveIDType =
| "open_id"
| "user_id"
| "union_id"
| "email"
| "chat_id"
export type MsgType =
| "text"
| "post"
| "image"
| "file"
| "audio"
| "media"
| "sticker"
| "interactive"
| "share_chat"
| "share_user"
export type FileType =
| "doc"
| "sheet"
| "bitable"
| "file"
| "wiki"
| "docx"
| "folder"
| "mindnote"
| "minutes"
}