feat(lark-msg): 抽象卡片 & 飞书Body处理
Some checks failed
/ release (push) Failing after 25s

This commit is contained in:
zhaoyingbo 2024-10-12 02:59:48 +00:00
parent 117aa98219
commit 8f8c32a9ec
5 changed files with 457 additions and 158 deletions

View File

@ -21,6 +21,8 @@
"@types/lodash": "*"
},
"dependencies": {
"lodash": "*"
"lodash": "*",
"@egg/logger": "^1.4.3",
"winston": "*"
}
}

View File

@ -0,0 +1,167 @@
import { LarkEvent, LarkAction } from "../types"
class LarkBody {
protected body: LarkEvent.Data | LarkAction.Data
public isEventMsg: boolean = false
public isActionMsg: boolean = false
public msgType?: string
public userId?: string
public msgText: string = ""
public chatType?: string
public mentions?: any
public actionType?: string
public actionValue?: any
public actionOption?: any
public chatId: string = ""
public messageId: string = ""
/**
*
* @param body Action消息体
*/
constructor(body: LarkEvent.Data | LarkAction.Data) {
this.body = body
this.isEventMsg = this.getIsEventMsg(body)
this.isActionMsg = this.getIsActionMsg(body)
if (this.isEventMsg) {
const eventBody = body as LarkEvent.Data
this.msgType = this.getMsgType(eventBody)
this.userId = this.getUserId(eventBody)
this.msgText = this.getMsgText(eventBody)
this.chatType = this.getChatType(eventBody)
this.mentions = this.getMentions(eventBody)
}
if (this.isActionMsg) {
const actionBody = body as LarkAction.Data
this.actionType = this.getActionType(actionBody)
this.actionValue = this.getActionValue(actionBody)
this.actionOption = this.getActionOption(actionBody)
}
this.chatId = this.getChatId(body)
this.messageId = this.getMessageId(body)
}
/**
*
* @param body
* @returns
*/
private getIsEventMsg(body: any): body is LarkEvent.Data {
return body?.header?.event_type === "im.message.receive_v1"
}
/**
* Action消息
* @param body Action消息体
* @returns Action消息
*/
private getIsActionMsg(body: any): body is LarkAction.Data {
return !!body?.action
}
/**
*
* @param body
* @returns
*/
private getMsgType(body: LarkEvent.Data): string | undefined {
return body?.event?.message?.message_type
}
/**
* Id
* @param body
* @returns Id
*/
private getUserId(body: LarkEvent.Data): string | undefined {
return body?.event?.sender?.sender_id?.user_id
}
/**
*
* @param body
* @returns
*/
private getMsgText(body: LarkEvent.Data): string {
try {
const { text }: { text: string } = JSON.parse(body?.event?.message?.content)
// 去掉@_user_1相关的内容例如 '@_user_1 测试' -> '测试'
const textWithoutAt = text.replace(/@_user_\d+/g, "")
return textWithoutAt
} catch {
return ""
}
}
/**
*
* @param body
* @returns
*/
private getChatType(body: LarkEvent.Data): string | undefined {
return body?.event?.message?.chat_type
}
/**
*
* @param body
* @returns
*/
private getMentions(body: LarkEvent.Data): any {
return body?.event?.message?.mentions
}
/**
* Action类型
* @param body Action消息体
* @returns Action类型
*/
private getActionType(body: LarkAction.Data): string | undefined {
return body?.action?.tag
}
/**
* Action参数
* @param body Action消息体
* @returns Action参数
*/
private getActionValue(body: LarkAction.Data): any {
return body?.action?.value
}
/**
* Action选项
* @param body Action消息体
* @returns Action选项
*/
private getActionOption(body: LarkAction.Data): any {
return body?.action?.option
}
/**
* Id
* @param body Action消息体
* @returns Id
*/
private getChatId(body: LarkEvent.Data | LarkAction.Data): string {
if (this.getIsEventMsg(body)) return body?.event?.message?.chat_id
if (this.getIsActionMsg(body)) return body?.open_chat_id
return ""
}
/**
* Id
* @param body Action消息体
* @returns Id
*/
private getMessageId(body: LarkEvent.Data | LarkAction.Data): string {
if (this.getIsEventMsg(body)) return body?.event?.message?.message_id
if (this.getIsActionMsg(body)) return body?.open_message_id
return ""
}
}
export default LarkBody

View File

@ -0,0 +1,82 @@
// 通用备注信息包含一个纯文本元素内容中包含作者和请求ID的占位符
export const commonNote = {
tag: "note",
elements: [
{
tag: "plain_text",
content: "${xIcon} 功能由${xAuthor}提供支持Rid${requestId}",
},
],
}
// 成功消息的头部信息,包含绿色模板和标题
export const successHeader = {
template: "green",
title: {
content: "${xIcon} 感谢使用 ${xName}",
tag: "plain_text",
},
}
// 错误消息的头部信息,包含红色模板和标题
export const errorHeader = {
template: "red",
title: {
content: "⛔ ${xName} 错误提示",
tag: "plain_text",
},
}
// 待处理消息的头部信息,包含紫色模板和标题
export const pendingHeader = {
template: "purple",
title: {
content: "${xIcon} 感谢使用 ${xName}",
tag: "plain_text",
},
}
// 基础成功卡片包含markdown内容、水平分割线和通用备注信息
export const baseSuccessCard = {
elements: [
{
tag: "markdown",
content: "${content}",
},
{
tag: "hr",
},
commonNote,
],
successHeader,
}
// 基础错误卡片包含markdown内容、水平分割线和通用备注信息
export const baseErrorCard = {
elements: [
{
tag: "markdown",
content: "${content}",
},
{
tag: "hr",
},
commonNote,
],
errorHeader,
}
// 基础待处理卡片包含markdown内容、水平分割线和通用备注信息
export const basePendingCard = {
elements: [
{
tag: "markdown",
content: "${content}",
},
{
tag: "hr",
},
commonNote,
],
pendingHeader,
}

View File

@ -0,0 +1,201 @@
import logger from "@egg/logger"
import _ from "lodash"
import { Logger } from "winston"
import { baseErrorCard, basePendingCard, baseSuccessCard } from "./component";
type FunctionMap = Record<string, { Xname: string; Xauthor: string, Xicon: string }>
type CardMap = Record<string, any>
type TempMap = Record<string, string>
class LarkCard {
protected requestId: string
protected funcName: string
protected functionMap: FunctionMap
protected cardMap: CardMap
protected tempMap: TempMap
protected stringify: boolean
protected logger: Logger
/**
*
* @param funcName
* @param stringify
* @param requestId ID
* @param cardMap
* @param tempMap
* @param functionMap
*/
constructor(
funcName: string,
stringify: boolean,
requestId: string,
cardMap: CardMap,
tempMap: TempMap,
functionMap: FunctionMap,
) {
this.stringify = stringify
this.requestId = requestId
this.funcName = funcName
this.functionMap = functionMap
this.cardMap = {
success: baseSuccessCard,
error: baseErrorCard,
pending: basePendingCard,
...cardMap,
}
this.tempMap = tempMap
this.logger = logger.child({ requestId })
}
/**
*
* @param funcName
*/
setFunction(funcName: string) {
this.funcName = funcName
}
/**
*
* @param stringify
*/
setStringify(stringify: boolean) {
this.stringify = stringify
}
/**
*
* @param func
* @param stringify
* @returns
*/
child(func: string, stringify: boolean = true) {
return new LarkCard(func, stringify, this.requestId, this.cardMap, this.tempMap, this.functionMap)
}
/**
*
* @param cardKey ID
* @param variables
* @returns JSON字符串
*/
genCard(
cardKey: keyof typeof this.cardMap,
variables: { [key: string]: any },
) {
const card = this.cardMap[cardKey]
if (!card) {
this.logger.error(`Card ${cardKey} not found`)
throw new Error(`Card ${cardKey} not found`);
}
const finalVariables: Record<string, any> = {
...variables,
requestId: this.requestId,
...this.functionMap[this.funcName],
}
this.logger.debug(`Card ${cardKey} final variables: ${JSON.stringify(finalVariables)}`)
/**
*
* @param str
* @returns
*/
const replaceVariables = (str: string): string => {
const variablePattern = /\$\{(\w+)\}/g
const matches = str.match(variablePattern)
if (matches && matches.length === 1 && matches[0] === str) {
// 如果字符串中只有一个变量,且整个字符串就是这个变量
const variableName = matches[0].slice(2, -1) // 去掉 ${ 和 }
return finalVariables[variableName] || ""
} else {
// 否则只替换字符串中的变量部分
return str.replace(variablePattern, (_, v) => finalVariables[v] || "")
}
}
/**
*
* @param obj
* @returns
*/
const traverseAndReplace = (obj: any): any => {
if (_.isString(obj)) {
return replaceVariables(obj)
} else if (_.isArray(obj)) {
return obj.map(traverseAndReplace)
} else if (_.isObject(obj)) {
return _.mapValues(obj, traverseAndReplace)
}
return obj
}
const content = traverseAndReplace(card)
this.logger.debug(`Card ${cardKey} final content: ${JSON.stringify(content)}`)
return this.stringify ? JSON.stringify(content) : content
}
/**
*
* @param content
* @returns JSON字符串
*/
genSuccessCard(content: any) {
return this.genCard("success", { content })
}
/**
*
* @param content
* @returns JSON字符串
*/
genErrorCard(content: any) {
return this.genCard("error", { content })
}
/**
*
* @param content
* @returns JSON字符串
*/
genPendingCard(content: any) {
return this.genCard("pending", { content })
}
/**
*
* @param tempKey ID
* @param variables
* @returns JSON字符串
*/
genTempCard(tempKey: string, variables: { [key: string]: any }) {
const tempId = this.tempMap[tempKey]
if (!tempId) {
this.logger.error(`Temp ${tempKey} not found`)
throw new Error(`Temp ${tempKey} not found`);
}
const finalVariables: Record<string, any> = {
...variables,
requestId: this.requestId,
...this.functionMap[this.funcName],
}
this.logger.debug(`Temp ${tempKey} final variables: ${JSON.stringify(finalVariables)}`)
const content = {
type: "template",
data: {
config: {
update_multi: true,
enable_forward: false,
},
template_id: tempId,
template_variable: finalVariables,
},
}
this.logger.debug(`Temp ${tempKey} final content: ${JSON.stringify(content)}`)
return this.stringify ? JSON.stringify(content) : content
}
}
export default LarkCard

View File

@ -2,164 +2,11 @@ import _ from "lodash"
import { LarkAction, LarkEvent } from "./types"
/**
*
* @param body
* @returns
*/
export const getIsEventMsg = (body: any): body is LarkEvent.Data => {
return body?.header?.event_type === "im.message.receive_v1"
}
import * as CardComponent from "./Card/component"
/**
*
* @param body
* @returns
*/
export const getMsgType = (body: LarkEvent.Data) => {
return body?.event?.message?.message_type
}
import LarkCard from "./Card"
/**
* Id
* @param body
* @returns Id
*/
export const getUserId = (body: LarkEvent.Data) => {
return body?.event?.sender?.sender_id?.user_id
}
/**
*
* @param body
* @returns
*/
export const getMsgText = (body: LarkEvent.Data) => {
try {
const { text }: { text: string } = JSON.parse(body?.event?.message?.content)
// 去掉@_user_1相关的内容例如 '@_user_1 测试' -> '测试'
const textWithoutAt = text.replace(/@_user_\d+/g, "")
return textWithoutAt
} catch {
return ""
}
}
/**
*
* @param body
* @returns
*/
export const getChatType = (body: LarkEvent.Data) => {
return body?.event?.message?.chat_type
}
/**
*
* @param body
* @returns
*/
export const getMentions = (body: LarkEvent.Data) => {
return body?.event?.message?.mentions
}
/**
* Action消息
* @param body Action消息体
* @returns Action消息
*/
export const getIsActionMsg = (body: any): body is LarkAction.Data => {
return !!body?.action
}
/**
* Action类型
* @param body Action消息体
* @returns Action类型
*/
export const getActionType = (body: LarkAction.Data) => {
return body?.action?.tag
}
/**
* Action参数
* @param body Action消息体
* @returns Action参数
*/
export const getActionValue = (body: LarkAction.Data) => {
return body?.action?.value
}
/**
* Action选项
* @param body Action消息体
* @returns Action选项
*/
export const getActionOption = (body: LarkAction.Data) => {
return body?.action?.option
}
/**
* Id
* @param body
* @returns Id
*/
export const getChatId = (body: LarkEvent.Data | LarkAction.Data) => {
if (getIsEventMsg(body)) return body?.event?.message?.chat_id
if (getIsActionMsg(body)) return body?.open_chat_id
return ""
}
/**
* Id
* @param body
* @returns Id
*/
export const getMessageId = (body: LarkEvent.Data | LarkAction.Data) => {
if (getIsEventMsg(body)) return body?.event?.message?.message_id
if (getIsActionMsg(body)) return body?.open_message_id
return ""
}
/**
*
* @param {any} card -
* @param {object} variables -
* @param {boolean} [stringify=true] - JSON
*/
export const genCardMsg = (
card: any,
variables: { [key: string]: any },
stringify: boolean = true
): any => {
const replaceVariables = (str: string): string => {
const variablePattern = /\$\{(\w+)\}/g
const matches = str.match(variablePattern)
if (matches && matches.length === 1 && matches[0] === str) {
// 如果字符串中只有一个变量,且整个字符串就是这个变量
const variableName = matches[0].slice(2, -1) // 去掉 ${ 和 }
return variables[variableName] || ""
} else {
// 否则只替换字符串中的变量部分
return str.replace(variablePattern, (_, v) => variables[v] || "")
}
}
const traverseAndReplace = (obj: any): any => {
if (_.isString(obj)) {
return replaceVariables(obj)
} else if (_.isArray(obj)) {
return obj.map(traverseAndReplace)
} else if (_.isObject(obj)) {
return _.mapValues(obj, traverseAndReplace)
}
return obj
}
const updatedCard = traverseAndReplace(card)
return stringify ? JSON.stringify(updatedCard) : updatedCard
}
import LarkBody from "./Body"
/**
* Options
@ -174,4 +21,4 @@ export const genCardOptions = (options: Record<string, string>) => {
}))
}
export { LarkAction, LarkEvent }
export { LarkAction, LarkEvent, CardComponent, LarkCard, LarkBody }