npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

open-feishu-card

v0.14.5

Published

独立、零依赖的飞书 (Lark) Universal Card 渲染器,支持 Node/SSR 与浏览器,输入飞书 RAW 卡片数据或公开 DSL(JSON Schema 2.0,内置 DSL→RAW 转换),输出 HTML 字符串或 DOM。

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';

const el = renderCard(cardRawJson);       // 返回 Node
document.getElementById('app')!.appendChild(el);

浏览器端无需引任何样式:renderCard 会把布局、图标字体、emoji 三类 CSS 自动注入 页面 <head>(幂等,多张卡片只注一次)。手动引样式仅 Node / SSR(renderCardToString)才需要。

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/mentions;并把 mentions/at_users 合并进 persons) | | mergeMentionsIntoPersons(attachment, mentions?) | 把信封 mentions / at_users 合并进 json_attachment.persons(fetchCardMessage 已内置) | | 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(:KEY: 短码,见下方「Emoji」一节) | | 内容/交互 | img、button、action、interactive_container、checker(勾选器)、at、at_all、link、text_tag | | 图标 | standard_icon、ud_icon(见下方「图标」一节) | | 数据 | table(表格)、chart(图表,需 VChart 水合,见下方「图表与表格」一节) |

card_header 的 text_tag_list 会渲染成标题右侧的彩色标签(复用 text_tag,色板同 header 模板)。

未注册的 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)。

Emoji

markdown 里的 :KEY: 短码(如 :Fire:、:OK:、:Trophy:,大小写敏感)复刻网页版飞书的表情雪碧图机制: 渲染成 <i class="larkw-emoji__img--emoji-<name>">,靠单张雪碧图 + background-position 出图(270 个短码、291 个定位)。 不认识的短码(如 :NOTAREALKEY:)原样保留字面文本,与飞书一致。

雪碧图(665KB)走飞书 CDN(不内联),但定位 CSS 需注入:

  • 浏览器(renderCard):首次渲染 emoji 时自动注入一次 <style>,无需额外引样式。

  • Node / SSR(renderCardToString):无 DOM 不自动注入,需引入 emoji CSS:

    import 'open-feishu-card/emoji.css';                // 打包器
    <link rel="stylesheet" href="node_modules/open-feishu-card/dist/emoji.css" />

    或在运行时把导出的 EMOJI_SPRITE_CSS 字符串自行注入页面。

更新表情:跑 node scripts/gen-emoji.mjs(从飞书资源重新抽取,重写 src/emoji-data.ts + src/emoji.css)。

图表与表格

卡片里的 table、chart 均已支持(DSL / RAW 皆可)。

table(表格) 是纯 HTML,开箱即用、零额外依赖:columns 定义列 (name / display_name / width / data_type / horizontal_align / format), rows 是 { 列name: 值 } 数组。data_type 支持 text / number(format.precision 定小数位)/ options(值为 [{ text, color }],渲染成彩色标签)/ lark_md·markdown(单元格内飞书 markdown, 含 <font> / **加粗** 等;DSL 写字符串,RAW 为已解析的 markdown 元素树);header_style 控表头底色/加粗/对齐, row_height(low/middle/high)控行高,page_size 只显示前 N 行。

chart(图表) 复刻网页版飞书——真机用 VChart 画到 <canvas>。 本渲染器保持同步「输出字符串/DOM」模型,不在渲染期跑 VChart(那要拉数百 KB 图形库、且依赖真实 DOM 测量)。因此 chart 组件只输出一个挂载容器,把 VChart 规格塞进 data-feishu-chart-spec 属性,真正画图交给运行期水合(chart_spec 本身即合法 VChart spec,原样透传):

// 浏览器:先渲染卡片,再用自行引入的 VChart 水合全部图表
import { renderCard, hydrateFeishuCharts } from 'open-feishu-card';
import { VChart } from '@visactor/vchart';

const el = renderCard(card);
document.body.appendChild(el);
hydrateFeishuCharts(el, VChart);   // 扫描 el 下所有图表容器并画图(幂等)
// SSR:把 hydrate 脚本连同 VChart CDN 一起塞进页面,加载后自动扫描并画图
import { renderCardToString, feishuChartHydrateScript, feishuCardStyleTag } from 'open-feishu-card';

const page = `<!DOCTYPE html><html><head>${feishuCardStyleTag()}</head><body>
${renderCardToString(card)}
<script src="https://unpkg.com/@visactor/vchart/build/index.min.js"></script>
${feishuChartHydrateScript()}
</body></html>`;

未水合(页面没引 VChart)时,图表位置显示「图表加载中…」占位框,其余内容照常渲染。 chart 组件本身零依赖——只有真要画图才需要 VChart,且由你决定怎么引(打包 or CDN)。

| 导出 | 说明 | |---|---| | hydrateFeishuCharts(root?, VChart?) | 扫描 root(默认 document)下所有图表容器并画图(幂等),返回成功数。VChart 缺省取 window.VChart | | renderFeishuChart(mount, spec, VChart) | 水合单张图:把 VChart 规格画进指定挂载节点 | | feishuChartHydrateScript(opts?) | SSR:返回自执行 <script>,页面加载后自动画图(依赖全局 VChart) | | feishuChartHydrateJs(vchartGlobal?) | 同上,但只返回脚本正文(不含 <script> 包裹,便于加 nonce) | | CHART_PALETTE | 默认序列色板(VChart 默认主题分类色,spec 未显式给 color 时注入) |

一次性引入全部样式(SSR 推荐)

浏览器端(renderCard / renderDsl)布局、图标、emoji 三类样式都会自动注入 <style>(幂等), 无需引任何样式。以下仅 Node / SSR 需要。

Node / SSR(renderCardToString*)无 DOM、不自动注入,需自行把 CSS 送进页面。与其分别引 styles.css + icon-font.css + emoji.css 三个文件(库以后加资源还得跟着改),可以用一份聚合样式:

// 运行时字符串(推荐):页面级注入一次,多张卡片共享
import { renderCardToString, ALL_CSS, feishuCardStyleTag } from 'open-feishu-card';

const page = `<!DOCTYPE html><html><head>${feishuCardStyleTag()}</head>
<body>${renderCardToString(cardA)}${renderCardToString(cardB)}</body></html>`;
// feishuCardStyleTag() = <style>ALL_CSS</style>;也可直接用 ALL_CSS 字符串自行包裹
import 'open-feishu-card/all.css';   // 或打包器 import 聚合文件
<link rel="stylesheet" href="node_modules/open-feishu-card/dist/all.css" />

ALL_CSS = 布局 + 图标字体 + emoji 雪碧三份合一。以后库再新增资源,聚合导出会自动包含,消费者代码零改动。

图片地址解析

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 会抛错——拉取消息本身就需要鉴权。

@ 提及与 mentions

fetchCardMessage 请求带 card_msg_content_type=raw_card_content&user_id_type=open_id,除解包 body.content 外,还会读取消息信封上的 data.items[0].mentions(不是 content 内字段):

const msg = await fetchCardMessage('om_xxx', { accessToken: 't-xxx' });
// msg.mentions: [{ id: 'ou_...', id_type: 'open_id', key: '@_user_1', name: '赵天雄' }]
// msg.json_attachment.persons 已合并 mentions + at_users,at 可直接出真名
  • mentions:来自消息信封;at_users 仍在 json_attachment 内。
  • 库内会把二者写入 json_attachment.persons(ou_... 与 @_user_N 别名键都写),现有 at 查表逻辑无需改动。
  • 因此 renderCardToStringAsync('om_xxx', { accessToken }) 对含 @ 的卡片会显示真实姓名,不必再传 options.persons。

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