/**
* 将 Markdown 内容转换为企业微信 textcard 文本卡片格式。
*
* textcard 格式(官方文档):
* {
* "touser": "USERID",
* "msgtype": "textcard",
* "agentid": 1,
* "textcard": {
* "title": "标题",
* "description": "
时间
描述内容
高亮
",
* "url": "https://...",
* "btntxt": "更多"
* }
* }
*/
export interface TextcardPayload {
title: string;
description: string;
url?: string;
btntxt?: string;
}
/**
* 将 Markdown 文本转换为 textcard payload。
* - 提取第一个 `# 标题` 作为 title(去掉 #)
* - 剩余内容转为纯文本 description
* - 图片转为 `[图片]` 文本
* - 链接保留 `[文字](url)` 显示为 "文字: url"
* - **粗体** 转为纯文本高亮(在 description 中保留)
* - description 最大 512 字符
*/
export function toTextcardV1(markdown: string): TextcardPayload {
if (!markdown) {
return { title: "通知", description: "" };
}
const lines = markdown.split("\n");
let title = "通知";
let bodyLines: string[] = [];
let titleFound = false;
for (const line of lines) {
const trimmed = line.trim();
// 提取第一个 # 标题
if (!titleFound && /^#\s/.test(trimmed)) {
title = trimmed.replace(/^#+\s*/, "").trim();
titleFound = true;
continue;
}
// 跳过 bare | 行(表格分隔行),但保留其他内容
if (/^\s*\|[-:|\s]+\|\s*$/.test(trimmed)) {
continue;
}
bodyLines.push(line);
}
// 处理 body 内容
let description = processBodyForTextcard(bodyLines.join("\n"));
// 截断到 512 字符
const MAX_DESC = 512;
if (description.length > MAX_DESC) {
description = description.slice(0, MAX_DESC - 6) + "...(更多)";
}
return {
title,
description,
url: "",
btntxt: "详情",
};
}
function processBodyForTextcard(text: string): string {
if (!text) return "";
// 移除 < > HTML 标签
let result = text.replace(/<[^>]+>/g, "");
// 图片:  → [图片]
result = result.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt) => {
const safeAlt = alt?.trim() || "图片";
return `[图片:${safeAlt}]`;
});
result = result.replace(/!\[([^\]]*)\]\[[^\]]*\]/g, (_, alt) => {
const safeAlt = alt?.trim() || "图片";
return `[图片:${safeAlt}]`;
});
// 链接: [文字](url) → 文字: url
result = result.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, (_, label, url) => {
return `${label}: ${url}`;
});
// 相对链接保留为纯文字
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1");
// 粗体 **text** → 保留纯文本
result = result.replace(/\*\*([^*]+)\*\*/g, "$1");
// 斜体 *text* → 保留纯文本
result = result.replace(/\*([^*\n]+)\*/g, "$1");
// 行内代码 `code` → 保留 code
result = result.replace(/`([^`\n]+)`/g, "$1");
// 代码块( fenced / indented )→ 移除
result = result.replace(/```[\s\S]*?```/g, "");
result = result.replace(/(?:^ .*$\n?)+/gm, "");
// 引用块 > text → 保留 text
result = result.replace(/^>\s?/gm, "");
// 任务列表 - [x] / - [ ] → 移除标记
result = result.replace(/^(\s*[-*+])\s+\[[x ]\]\s+/gm, "$1 ");
// 标题 ## / ### 等(首层已用于 title)→ 去掉 # 保留文字
result = result.replace(/^#{1,6}\s+/gm, "");
// 水平线 → 移除
result = result.replace(/^\s*([-*_])\s*\1{2,}\s*$/gm, "");
// 清理多余空白
result = result.replace(/\n{3,}/g, "\n\n").trim();
return result;
}