open-feishu-card
v0.5.0
Published
独立、零依赖的飞书 (Lark) Universal Card 渲染器,支持 Node/SSR 与浏览器,输入飞书 RAW 卡片数据或公开 DSL(JSON Schema 2.0,内置 DSL→RAW 转换),输出 HTML 字符串或 DOM。
Maintainers
Readme
open-feishu-card
独立、零运行时依赖的飞书 (Lark) Universal Card 渲染器。
- ✅ 输入飞书 RAW 卡片数据(服务端归一化后的
json_card),输出 HTML 字符串或 DOM - ✅ 同时适用于 Node / SSR 和浏览器
- ✅ TypeScript 编写,自带类型定义
- ✅ ESM + CJS 双格式
- ✅ header 高光纹理图已 base64 内联,无外部资源依赖
渲染规则(颜色、字号、间距、header 模板色等)均逆向自网页版飞书引擎,力求视觉还原。
安装
npm i open-feishu-card
# 或 pnpm add open-feishu-card / yarn add open-feishu-card使用
Node / SSR —— 渲染成 HTML 字符串
import { renderCardToString } from 'open-feishu-card';
const html = renderCardToString(cardRawJson);
// 把 html 塞进页面/邮件/截图服务即可样式表需一并引入(任选其一):
// 打包器里直接 import
import 'open-feishu-card/styles.css';<!-- 或在 HTML 里链接构建产物 -->
<link rel="stylesheet" href="node_modules/open-feishu-card/dist/styles.css" />浏览器 —— 渲染成 DOM
import { renderCard } from 'open-feishu-card';
import 'open-feishu-card/styles.css';
const el = renderCard(cardRawJson); // 返回 Node
document.getElementById('app')!.appendChild(el);API
| 导出 | 说明 |
|---|---|
| renderCardToString(card, options?) | 渲染为 HTML 字符串(Node/SSR,同步) |
| renderCard(card, options?) | 渲染为 DOM 节点(浏览器;Node 端会抛错,同步) |
| renderDslToString(dsl, options?) | 输入公开 DSL,渲染为 HTML 字符串(= renderCardToString(dslToRaw(dsl))) |
| renderDsl(dsl, options?) | 输入公开 DSL,渲染为 DOM 节点 |
| dslToRaw(dsl) | 公开 DSL(JSON Schema 2.0)→ RAW(纯函数、同步、零依赖) |
| parseMarkdown(content) | markdown 字符串 → RAW 子元素树(dslToRaw 内部用,也可单独调) |
| renderCardToStringAsync(input, options?) | 异步渲染 HTML 字符串,支持用 token 自动拉图 |
| renderCardAsync(input, options?) | 异步渲染 DOM 节点,支持用 token 自动拉图 |
| fetchCardMessage(messageId, options) | 按 message_id 拉取整条卡片消息(含 json_card/json_attachment) |
| registerComponent(tag, fn) | 注册/覆盖某个 tag 的渲染组件 |
| getComponent(tag) | 取某 tag 的组件 |
| h(tag, props, ...children) | 轻量 VNode 构造器(写自定义组件用) |
| HEADER_BG_LIGHT / HEADER_BG_DARK | header 高光纹理的 base64 data URI |
| ICON_FONT_CSS | 图标字体完整 CSS(@font-face 内联 base64 + 每图标 content 规则),用于自助注入 |
| ICON_CODEPOINTS | 图标名 -> 私有区码点 映射(如 warning_outlined: "fa0c"),共 2484 个 |
RenderOptions
interface RenderOptions {
document?: Document; // 浏览器 document(默认取全局)
warnOnUnknownTag?: boolean; // 未注册 tag 时打印警告
resolveImage?: (p) => string; // 自定义图片地址解析
imageBaseURL?: string; // 最终 src = imageBaseURL + imageID
}输入格式:RAW(json_card)
接受 { json_card: {...} }(飞书 card.raw.json 顶层)或直接传 json_card 对象。
元素统一形如 { tag, id?, property },正文在 body.property.elements。
注:开发者编写的 DSL 格式(
body.elements+ markdown 字符串)也可直接渲染, 见下方「输入格式:DSL」——内部由dslToRaw转成 RAW,无需经飞书服务端。
输入格式:DSL(公开 JSON Schema 2.0)
开发者手写、im.message.create 用来发卡片的那套公开 DSL 也能直接渲染:
import { renderDslToString } from 'open-feishu-card';
import 'open-feishu-card/styles.css';
const html = renderDslToString({
schema: '2.0',
header: { template: 'green', title: '标题' }, // 文本可直接写字符串
body: {
elements: [
{ tag: 'markdown', content: '**加粗** 与 `代码` 与 [链接](https://feishu.cn)' },
{ tag: 'hr' },
],
},
});与 RAW 的差异(dslToRaw 负责抹平,纯函数、同步、零依赖):
- markdown 是字符串(
content: "**x**"),内部解析成 RAW 的子元素树(heading/list/link/code_span/<font>/<at>/<text_tag>等); - 元素字段扁平、
snake_case(text_align/background_style/user_id…),转成渲染器认得的形态; padding/margin是 CSS 简写字符串("12px 8px"),转成四向对象;- 未知 tag / 字段原样透传(配合
warnOnUnknownTag温和跳过)。
已覆盖 markdown 常用子集与本渲染器支持的全部 tag;
table、嵌套form等罕见构造走透传, 后续按需补齐。需要「飞书能接收的合法 raw envelope」而非「能渲染的形态」时,仍应走真实发送链路。
支持的 tag
| 分类 | tag |
|---|---|
| 结构 | body、card_header、div、note |
| 布局 | column_set、column |
| 文本 | markdown、plain_text、br、hr |
| markdown 子元素 | heading、blockquote、list、code_block、code_span、emoji |
| 内容/交互 | img、button、action、interactive_container、at、at_all、link、text_tag |
| 图标 | standard_icon、ud_icon(见下方「图标」一节) |
未注册的 tag 会被温和跳过(传 { warnOnUnknownTag: true } 可打印警告)。
图标
standard_icon / ud_icon 复刻网页版飞书的图标字体机制:空的 <i class="ud-icon_<token>">
靠内联 woff2 字体的 :before{content} 画出 glyph。property 取 token(如 warning_outlined)、
size(small/normal/large1~large4,默认 16px)、color(命名色 red/green/blue… 或直接
#hex/rgb();缺省继承父级文字色)。*_colorful 结尾的彩色图标字体自带色,不受 color 覆盖。
字体(2484 个图标、约 160KB woff2)已 base64 内联,零外部依赖:
浏览器(
renderCard):首次渲染图标时自动注入一次<style>,无需额外引样式。Node / SSR(
renderCardToString):无 DOM 不自动注入,需引入图标字体 CSS:import 'open-feishu-card/icon-font.css'; // 打包器<link rel="stylesheet" href="node_modules/open-feishu-card/dist/icon-font.css" />或在运行时把导出的
ICON_FONT_CSS字符串自行注入页面。
更新图标字体:跑
node scripts/gen-icon-font.mjs(从飞书资源重新抽取,重写src/icon-font.ts+src/icon-font.css)。
图片地址解析
RAW 里图片通常只有 imageID(飞书图床 key),不是直链:
renderCardToString(card, {
resolveImage: (p) => myCDN(p.imageID), // 完全自定义
// 或
imageBaseURL: 'https://my-host/img/', // 拼前缀
});若 property 自带 src/url 则直接用;都没有时渲染为占位框。
异步渲染 / 自动拉图
飞书 RAW 卡片里的 img 元素只带局部序号 imageID(如 "19"),不是图片直链;真实图床 key(img_v3_...)在消息的 json_attachment.images 里,需要再用 access token 调接口换二进制。renderCardToStringAsync / renderCardAsync 把这套流程内置了:
- 传了
accessToken→ 渲染器自动拉取图片并内联为 base64 data URI; - 不传
accessToken→ 图片渲染成占位图(其余内容照常渲染)。
输入有两种形态:
import { renderCardToStringAsync } from 'open-feishu-card';
// 形态 1:只给 message_id —— 渲染器自己把整条消息拉回来再渲染(需 token)
const html1 = await renderCardToStringAsync('om_xxx', { accessToken: 't-xxx' });
// 形态 2:已有卡片数据(接口 content 解包后的形态)—— 直接渲染
// 传 json_attachment + messageId + token 才能内联图片;否则图片走占位
const html2 = await renderCardToStringAsync(
{ json_card, json_attachment, messageId: 'om_xxx' },
{ accessToken: 't-xxx' },
);只传
message_id但缺accessToken会抛错——拉取消息本身就需要鉴权。
AsyncRenderOptions
在 RenderOptions 基础上扩展:
interface AsyncRenderOptions extends RenderOptions {
accessToken?: string; // 可选:有则拉图内联,无则占位
messageId?: string; // 输入是卡片对象时,用它拼图片接口 URL
fetch?: typeof fetch; // 注入自定义 fetch(默认全局 fetch;便于代理 / 单测 mock)
apiBaseURL?: string; // 默认 https://open.feishu.cn/open-apis/im/v1
}运行环境
- 异步功能依赖全局
fetch(Node 18+ / 现代浏览器原生支持)。Node 16 用户:同步渲染不受影响;异步功能请升级运行时,或通过options.fetch注入实现。 - 浏览器场景注意 CORS:浏览器直连
open.feishu.cn会被跨域拦截,且会暴露 token。正确做法是让服务端做代理转发(用apiBaseURL指向你的代理),或在服务端预渲染(renderCardToStringAsync)后把 HTML 下发给前端。
扩展自定义元素
import { registerComponent, h } from 'open-feishu-card';
registerComponent('my_widget', (el, ctx) =>
h('div', { className: 'my-widget' }, el.property?.content ?? '')
);暗色主题
给根节点加 theme-dark,或自行覆盖样式变量:
const el = renderCard(card);
el.classList.add('theme-dark');开发
pnpm install
pnpm build # tsup 产出 dist/(esm + cjs + d.ts + styles.css)
pnpm test # node --test(含异步拉图的 mock-fetch 测试,无需联网、无需凭据)
pnpm example # 渲染示例到 examples/output.html测试用注入的 mock fetch,不需要任何凭据或联网。
让 pnpm example 联网拉取真实卡片并自动内联图片——把凭据放进 .env(已被 gitignore):
cp .env.example .env
# 编辑 .env,填入自建应用的 App ID / Secret(开发者后台 -> 凭证与基础信息)
# FEISHU_APP_ID=cli_xxx
# FEISHU_APP_SECRET=xxx
pnpm example # 自动用 App ID/Secret 换取 tenant_access_token,无需手动给 token不配 .env 也能跑:用内置兜底卡片,图片渲染成占位图。凭据解析优先级:FEISHU_TOKEN(直接给的 token)> App ID + Secret 自动换取。
License
MIT
