/** * Feishu 卡片 Markdown 样式优化 * * 背景: 飞书卡片的 `tag: 'markdown'` 元素对 H1~H3 标题有已知渲染异常 * (字面量显示 `#`/`##`/`###`)。必须降级为 H4/H5 才能正常渲染。 * * 实现参考: openclaw-lark/src/card/markdown-style.ts * * 本文件只做「保证能渲染」的最小预处理,不加 Schema 2.0 专属的 *
间距美化(cardVersion 参数预留以便将来升级)。 */ /** * 对将要放入 `tag: 'markdown'` 元素的内容做安全预处理。 * * - 标题降级: 若原文包含 H1~H3,则 H2~H6 → H5,H1 → H4 * - 代码块内容受保护,不会被降级 * - 连续 3+ 空行压缩为 2 * - 任何内部错误都 fallback 到原文,不阻塞消息发送 * * @param text 原始 markdown * @param cardVersion 卡片 schema 版本,当前只影响是否加段落间距
*/ export function optimizeMarkdownForFeishu(text: string, cardVersion = 1): string { try { return _optimizeMarkdownForFeishu(text, cardVersion) } catch { return text } } function _optimizeMarkdownForFeishu(text: string, cardVersion: number): string { // ── 1. 提取代码块,用占位符保护,处理后再还原 ───────────────────── // 这样代码块内部的 `#` 不会被标题降级误伤 const MARK = '___CB_' const codeBlocks: string[] = [] let r = text.replace(/```[\s\S]*?```/g, (m) => { return `${MARK}${codeBlocks.push(m) - 1}___` }) // ── 2. 标题降级 ──────────────────────────────────────────────────── // 只有当原文档(不是保护后的 r)包含 H1~H3 时才执行降级 // 顺序: 先 H2~H6 → H5,再 H1 → H4 // 若先 H1→H4,####会被后面的 #{2,6} 再次匹配成 H5(变 ##### Title 两次) const hasH1toH3 = /^#{1,3} /m.test(text) if (hasH1toH3) { r = r.replace(/^#{2,6} (.+)$/gm, '##### $1') // H2~H6 → H5 r = r.replace(/^# (.+)$/gm, '#### $1') // H1 → H4 } // ── 3. Schema 2.0 下可额外加段落间距 ──────────────────────────────── // 当前飞书 adapter 走 Schema 1.0 / buildStreamingCard,这一块不启用 if (cardVersion >= 2) { // 连续标题之间补
r = r.replace(/^(#{4,5} .+)\n{1,2}(#{4,5} )/gm, '$1\n
\n$2') } // ── 4. 压缩多余空行(3 个以上连续换行 → 2 个)──────────────────── // 注意: 必须在还原代码块之前做,否则代码块内部的连续换行会被误伤。 // 此时代码块被占位符 `___CB_N___`(单行 token)替代,压缩只影响 // 真正的 markdown 段落间距。 r = r.replace(/\n{3,}/g, '\n\n') // ── 5. 还原代码块 ───────────────────────────────────────────────── // Schema 1.0 路径不在代码块前后加
(维持现状,最小变更) codeBlocks.forEach((block, i) => { r = r.replace(`${MARK}${i}___`, block) }) return r }