cc-haha/adapters/feishu/markdown-style.ts
程序员阿江(Relakkes) 4ccda92cfe chore(adapters): snapshot pre-attachment WIP (permission card v2, handler serialization, markdown style)
Carries forward ~800 lines of in-flight work that existed in the working
tree before the IM-attachment branch was cut. Kept as a single isolated
commit so later attachment work has a clean diff baseline and so this
WIP can be cherry-picked back to main on its own merits.

Contents:
- ws-bridge: per-chat handler chain serialization; sendPermissionResponse
  now accepts optional `rule: 'always'` for persistent permits
- feishu/index.ts: permission card rewritten to Schema 2.0 with icon,
  cross-dir warning, and ♾️ 永久允许 button; optimizeMarkdownForFeishu
  applied to streaming card
- feishu/markdown-style.ts: new Feishu-specific markdown downgrader
- tests: corresponding bun:test coverage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 15:24:10 +08:00

72 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Feishu 卡片 Markdown 样式优化
*
* 背景: 飞书卡片的 `tag: 'markdown'` 元素对 H1~H3 标题有已知渲染异常
* (字面量显示 `#`/`##`/`###`)。必须降级为 H4/H5 才能正常渲染。
*
* 实现参考: openclaw-lark/src/card/markdown-style.ts
*
* 本文件只做「保证能渲染」的最小预处理,不加 Schema 2.0 专属的
* <br> 间距美化cardVersion 参数预留以便将来升级)。
*/
/**
* 对将要放入 `tag: 'markdown'` 元素的内容做安全预处理。
*
* - 标题降级: 若原文包含 H1~H3则 H2~H6 → H5H1 → H4
* - 代码块内容受保护,不会被降级
* - 连续 3+ 空行压缩为 2
* - 任何内部错误都 fallback 到原文,不阻塞消息发送
*
* @param text 原始 markdown
* @param cardVersion 卡片 schema 版本,当前只影响是否加段落间距 <br>
*/
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) {
// 连续标题之间补 <br>
r = r.replace(/^(#{4,5} .+)\n{1,2}(#{4,5} )/gm, '$1\n<br>\n$2')
}
// ── 4. 压缩多余空行3 个以上连续换行 → 2 个)────────────────────
// 注意: 必须在还原代码块之前做,否则代码块内部的连续换行会被误伤。
// 此时代码块被占位符 `___CB_N___`(单行 token替代压缩只影响
// 真正的 markdown 段落间距。
r = r.replace(/\n{3,}/g, '\n\n')
// ── 5. 还原代码块 ─────────────────────────────────────────────────
// Schema 1.0 路径不在代码块前后加 <br>(维持现状,最小变更)
codeBlocks.forEach((block, i) => {
r = r.replace(`${MARK}${i}___`, block)
})
return r
}