From 7b30ac12e1692130c67f2ea3ccd4e95981978469 Mon Sep 17 00:00:00 2001 From: Shigma Date: Wed, 30 Oct 2024 13:51:02 +0800 Subject: [PATCH] feat(lark): init card support --- adapters/lark/src/message.ts | 44 +++++--- adapters/lark/src/types/message/content.ts | 118 ++++++++++++++++++++- adapters/lark/src/types/message/index.ts | 20 ++-- 3 files changed, 154 insertions(+), 28 deletions(-) diff --git a/adapters/lark/src/message.ts b/adapters/lark/src/message.ts index 47cfc82f..77af9782 100644 --- a/adapters/lark/src/message.ts +++ b/adapters/lark/src/message.ts @@ -7,14 +7,7 @@ export class LarkMessageEncoder extends MessageEnco private quote: string | undefined private textContent = '' private richContent: MessageComponent.RichText.Paragraph[] = [] - private cardElements: MessageComponent.Card[] | undefined - private richElements: MessageComponent.RichText.InlineElement[] | undefined - - private flushRich() { - if (!this.textContent) return - this.richContent.push([{ tag: 'md', text: this.textContent }]) - this.textContent = '' - } + private cardElements: MessageComponent.Card.Element[] | undefined async post(data?: any) { try { @@ -47,15 +40,23 @@ export class LarkMessageEncoder extends MessageEnco } } + private flushText() { + if (!this.textContent) return + this.richContent.push([{ tag: 'md', text: this.textContent }]) + this.cardElements?.push({ tag: 'markdown', content: this.textContent }) + this.textContent = '' + } + async flush() { - if (this.textContent === '' && !this.cardElements && !this.richContent.length) return + this.flushText() + if (!this.cardElements && !this.richContent.length) return if (this.cardElements) { await this.post({ msg_type: 'interactive', elements: this.cardElements, }) - } else if (this.richContent.length) { + } else { await this.post({ msg_type: 'post', content: JSON.stringify({ zh_cn: this.richContent }), @@ -65,7 +66,6 @@ export class LarkMessageEncoder extends MessageEnco // reset cached content this.quote = undefined this.textContent = '' - this.richElements = undefined this.richContent = [] this.cardElements = undefined } @@ -79,13 +79,17 @@ export class LarkMessageEncoder extends MessageEnco return image_key } - async sendFile(_type: 'video' | 'audio' | 'file', url: string) { + async sendFile(_type: 'video' | 'audio' | 'file', attrs: any) { + const url = attrs.src || attrs.url const payload = new FormData() - const { filename, type, data } = await this.bot.assetsQuester.file(url) payload.append('file', new Blob([data], { type }), filename) payload.append('file_name', filename) + if (attrs.duration) { + payload.append('duration', attrs.duration) + } + if (_type === 'audio') { // FIXME: only support opus payload.append('file_type', 'opus') @@ -94,7 +98,7 @@ export class LarkMessageEncoder extends MessageEnco payload.append('file_type', 'mp4') } else { const ext = filename.split('.').pop() - if (['xls', 'ppt', 'pdf'].includes(ext)) { + if (['doc', 'xls', 'ppt', 'pdf'].includes(ext)) { payload.append('file_type', ext) } else { payload.append('file_type', 'stream') @@ -135,14 +139,18 @@ export class LarkMessageEncoder extends MessageEnco } else if (type === 'img' || type === 'image') { const image_key = await this.createImage(attrs.src || attrs.url) this.textContent += `![${attrs.alt ?? '图片'}](${image_key})` - this.flushRich() + this.flushText() this.richContent.push([{ tag: 'img', image_key }]) } else if (['video', 'audio', 'file'].includes(type)) { await this.flush() - await this.sendFile(type as any, attrs.src || attrs.url) + await this.sendFile(type as any, attrs) } else if (type === 'figure' || type === 'message') { await this.flush() await this.render(children, true) + } else if (type === 'hr' || type === 'lark:hr' || type === 'feishu:hr') { + this.flushText() + this.richContent.push([{ tag: 'hr' }]) + this.cardElements?.push({ tag: 'hr' }) } else if (type.startsWith('lark:') || type.startsWith('feishu:')) { const tag = type.slice(type.split(':', 1)[0].length + 1) if (tag === 'share-chat') { @@ -169,6 +177,10 @@ export class LarkMessageEncoder extends MessageEnco }), }) this.textContent = '' + } else if (tag === 'card') { + await this.flush() + this.cardElements = [] + await this.render(children, true) } } else { await this.render(children) diff --git a/adapters/lark/src/types/message/content.ts b/adapters/lark/src/types/message/content.ts index 575c6e25..e4613222 100644 --- a/adapters/lark/src/types/message/content.ts +++ b/adapters/lark/src/types/message/content.ts @@ -115,7 +115,7 @@ export namespace MessageComponent { text: string } - export interface HRElement extends BaseElement<'hr'> {} + export interface HorizontalRuleElement extends BaseElement<'hr'> {} export interface MarkdownElement extends BaseElement<'md'> { text: string @@ -132,10 +132,124 @@ export namespace MessageComponent { | ImageElement | MediaElement | CodeBlockElement - | HRElement + | HorizontalRuleElement export type Paragraph = | InlineElement[] | [BlockElement] } + + export interface Card { + config: Card.Config + card_link?: Card.URLs + elements?: Card.Element[] + } + + export namespace Card { + /** @see https://open.larksuite.com/document/common-capabilities/message-card/getting-started/card-structure/card-configuration */ + export interface Config { + enable_forward?: boolean + update_multi?: boolean + } + + export interface URLs { + url: string + pc_url?: string + ios_url?: string + android_url?: string + } + + /** @see https://open.larksuite.com/document/common-capabilities/message-card/message-cards-content/card-header */ + export interface Header { + title: PlainTextElement + subtitle?: PlainTextElement + template?: Header.Template + icon?: CustomIconElement + ud_icon?: StandardIconElement + text_tag_list?: TextTagElement[] + i18n_text_tag_list?: Record + } + + export namespace Header { + export type Template = 'blue' | 'wathet' | 'turquoise' | 'green' | 'yellow' | 'orange' | 'red' | 'carmine' | 'violet' | 'purple' | 'indigo' | 'grey' | 'default' + } + + export interface BaseElement { + tag: T + } + + export type TextSize = + | 'heading-0' | 'heading-1' | 'heading-2' | 'heading-3' | 'heading-4' | 'heading' + | 'normal' | 'notation' | 'xxxx-large' | 'xxx-large' | 'xx-large' | 'x-large' | 'large' | 'medium' | 'small' | 'x-small' + + export type TextAlign = 'left' | 'center' | 'right' + + export interface PlainTextElement extends BaseElement<'plain_text'> { + content: string + i18n?: Record + text_size?: TextSize + text_color?: string + text_align?: TextAlign + lines?: number + icon?: IconElement + } + + export type IconElement = StandardIconElement | CustomIconElement + + export interface CustomIconElement extends BaseElement<'custom_icon'> { + img_key: string + } + + export interface StandardIconElement extends BaseElement<'standard_icon'> { + token: string + color?: string + } + + export interface TextTagElement extends BaseElement<'text_tag'> { + text: PlainTextElement + color: TextTagElement.Color + } + + export namespace TextTagElement { + export type Color = 'neutral' | 'blue' | 'torqoise' | 'lime' | 'orange' | 'violet' | 'indigo' | 'wathet' | 'green' | 'yellow' | 'red' | 'purple' | 'carmine' + } + + export interface ImageElement extends BaseElement<'image'> { + img_key: string + alt?: PlainTextElement + title?: PlainTextElement + custom_width?: number + compact_width?: boolean + mode?: 'crop_center' | 'fit_horizontal' | 'large' | 'medium' | 'small' | 'tiny' + preview?: boolean + } + + export interface HorizontalRuleElement extends BaseElement<'hr'> {} + + export interface ParagraphElement extends BaseElement<'div'> { + text?: PlainTextElement + } + + export interface MarkdownElement extends BaseElement<'markdown'> { + content: string + text_size?: TextSize + text_align?: TextAlign + href?: Record + } + + export interface HorizontalRuleElement extends BaseElement<'hr'> {} + + export type Element = + | ParagraphElement + | MarkdownElement + | HorizontalRuleElement + } + + export interface Template { + type: 'template' + data: { + template_id: string + template_variable: object + } + } } diff --git a/adapters/lark/src/types/message/index.ts b/adapters/lark/src/types/message/index.ts index 75224b15..6bbc3cb2 100644 --- a/adapters/lark/src/types/message/index.ts +++ b/adapters/lark/src/types/message/index.ts @@ -1,20 +1,20 @@ import { Lark } from '..' -import { MessageContent } from './content' +import { MessageComponent } from './content' export * from './content' export type MessageType = 'text' | 'post' | 'image' | 'file' | 'audio' | 'media' | 'sticker' | 'interactive' | 'share_chat' | 'share_user' export interface MessageContentMap { - 'text': MessageContent.Text - 'post': MessageContent.RichText - 'image': MessageContent.Image - 'file': MessageContent.File - 'audio': MessageContent.Audio - 'media': MessageContent.Media - 'sticker': MessageContent.Sticker - 'share_chat': MessageContent.ShareChat - 'share_user': MessageContent.ShareUser + 'text': MessageComponent.Text + 'post': MessageComponent.RichText + 'image': MessageComponent.Image + 'file': MessageComponent.File + 'audio': MessageComponent.Audio + 'media': MessageComponent.Media + 'sticker': MessageComponent.Sticker + 'share_chat': MessageComponent.ShareChat + 'share_user': MessageComponent.ShareUser } export type MessageContentType = T extends keyof MessageContentMap ? MessageContentMap[T] : any