easy-feishu-card
v0.4.5
Published
生成一张飞书卡片,你本不应该知道那么多。写一份 YAML,编译为标准飞书卡片 JSON(schema 2.0),可选发送到聊天。
Readme
easy-feishu-card
生成一张飞书卡片,你本不应该知道那么多。
用一份 YAML 描述飞书卡片,编译成标准飞书卡片 JSON(schema 2.0),可选发送到聊天。
飞书原生卡片 JSON 嵌套深、样板多、容易写错结构。本项目在它之上加了一层更高抽象、约束更强、可测试的 YAML 方言(DSL):你(或 LLM)只写 YAML,编译器兜住正确性——设默认值、做别名与字号映射、拦住非法结构。剩下的飞书细节,你本不应该知道那么多。
产品形态上它是「一份提示词 + 一个 CLI」:
- 提示词 =
prompt/card-dsl-spec.md,这门 YAML 方言的语言规范(组件、语法、硬性规则)。教会 LLM 怎么写。也从库导出为字符串CARD_DSL_SPEC,可直接拼进 agent 的 system prompt。 - CLI / 库 = 把 YAML 编译为飞书 JSON、发送、读回验证的确定性工具链。保证写出来的东西一定对、能发。
代码形态上它是「DSL + 转译器」:compile/ 是一个前端(parse → AST)/后端(AST → 飞书 JSON)解耦的 source-to-source 编译器。
安装
npm install # Node >= 18(用到全局 fetch)发送/验证需要环境变量:
| 变量 | 用途 |
|---|---|
| FEISHU_APP_ID | 应用 ID |
| FEISHU_APP_SECRET | 应用密钥 |
| FEISHU_CHAT_ID | 目标聊天(发送用) |
编译(compile)不需要发送类环境变量;但默认会联网访问 enumless 网关解析 emoji/icon(无网时那些标记按“解析不到”删除)。要纯离线编译请用库接口传 { resolve: false }。
命令行用法
node bin/easy-feishu-card.mjs compile card.yaml # YAML → 卡片 JSON(打印到 stdout;默认联网解析 emoji/icon)
node bin/easy-feishu-card.mjs compile card.yaml -o out.json # 写入文件
node bin/easy-feishu-card.mjs send card.yaml # 编译(+上传图片)→ 发送,打印 message_id
node bin/easy-feishu-card.mjs render card.yaml # 发送后自动读回对比,验证飞书完整接受
node bin/easy-feishu-card.mjs verify <message_id> expected.json # 读回某条消息 → 与期望 JSON 对比差异render = send + 读回验证,是「飞书是否原样接受了我们发送的卡片」的往返检查。
作为库使用(JS / TS)
CLI 只是薄壳;核心能力都是可 import 的纯函数/类。从包入口导入:
import { compile, FeishuClient, resolveImages, resolveEmojis, resolveIcons, diffCards } from 'easy-feishu-card';
// 1) 编译:异步。默认联网把 :token: 表情、standard_icon 的 token 语义解析为真实
// 飞书 key/token(解析不到则删除)。保留飞书风格信封 { code, msg, data },失败不抛异常。
const { code, msg, data: card } = await compile({ content: yamlString });
if (code !== 0) throw new Error(msg);
// 2) 发送(需要 FEISHU_APP_ID / FEISHU_APP_SECRET)
const client = new FeishuClient(); // 凭证从 env 读,也可传 { appId, appSecret, token }
await resolveImages(card, client); // 上传卡片内带 URL/本地路径的图片,换成 img_key(emoji/icon 已在 compile 阶段解析)
const messageId = await client.sendCard({ chatId: process.env.FEISHU_CHAT_ID, card });
// 3) 读回验证(可选)
const readback = await client.getRawCard(messageId);需要纯离线、确定性输出(不联网、原样保留 :token: 与图标)时,传 { resolve: false }:
const { data: card } = await compile({ content: yamlString, resolve: false });也可用只含编译逻辑的子路径入口(同样支持 resolve 选项):
import { compile } from 'easy-feishu-card/compile';TypeScript 用户有随包的 index.d.ts,compile(...) 返回 Promise<CompileResult>,data 为 Card。
导出一览
| 导出 | 签名 | 说明 |
|---|---|---|
| compile | ({ content, resolve?, fetch? }) => Promise<{ code, msg, data }> | YAML 方言 → 飞书卡片 DSL。异步,默认联网语义解析 emoji/icon(resolve:false 则纯离线原样保留)。 |
| FeishuClient | new (opts?) | 轻量飞书 IM 客户端:sendCard / getRawCard / uploadImage / token。 |
| resolveImages | (card, client) => Promise<card> | 上传卡片内待解析的图片,原地替换为 img_key。仅发送路径需要。 |
| resolveEmojis | (card, opts?) => Promise<card> | 把 richtext 里的 :token: 语义检索为真实飞书表情 key(英文词效果最佳),原地改写。低分/无匹配/离线则删除标记并 warn,结果写 .emoji-cache.json。compile 默认已调用;仅在 compile({resolve:false}) 后想手动解析时才需单独调。 |
| resolveIcons | (card, opts?) => Promise<card> | 把 standard_icon 的 token 语义检索为真实图标 token。validate-then-replace:已存在的保留,未知的按最近匹配替换并尽量保留风格(outlined/filled/colorful)。低分/无匹配/离线则删除图标节点并 warn,结果写 .icon-cache.json。compile 默认已调用;仅在 compile({resolve:false}) 后想手动解析时才需单独调。 |
| diffCards | (generated, readback) => { equal, diffs } | 语义对比生成卡片与读回卡片。 |
| normalizeCard | (card) => card | 标准化(剥离编辑器默认值、归一 img_key),供往返对比。 |
编写 YAML
完整的组件、富文本语法、硬性规则见 prompt/card-dsl-spec.md(这门方言的语言规范,也从库导出为 CARD_DSL_SPEC)。设计取舍见 DESIGN.md。最小示例:
header:
color: blue
title: 周报 · 第25周
subtitle: 2026-06-20 ~ 06-26
config:
summary: 周报已生成
body:
- richtext: "**本周核心指标**:完成 29 单"
- hr:
- linkbtn: {text: 查看详情, type: primary, url: https://feishu.cn/x}开发
npm test # 一致性测试(conformance/ 下 .yaml 输入配 .dsl.json 期望输出)
npm run regen # 重新生成一致性 fixtures
node tests/smoke-exports.mjs # 库导出冒烟测试conformance/*.yaml ↔ conformance/*.dsl.json 锁死「同一份 YAML 永远编译出同一个 JSON」的语义契约。新增组件见 compile/blocks/NEW-COMPONENT.md。
架构
YAML 方言
└─ compile/yaml-source.mjs 前端:切分 header / config / body
└─ compile/to-ast.mjs 前端:元素列表 → AST(与输入格式无关的中间表示)
└─ compile/render.mjs 后端:AST → 飞书卡片 DSL
+ compile/blocks/* 各组件的字段映射
publish/ FeishuClient、图片上传解析(运行时 / IO)
verify/ 读回对比(往返验证)
bin/ CLI(以上全部之上的薄壳)渲染层与输入格式无关——理论上可替换前端(如另一种语法)复用同一后端。
