feat(publish/dongchedi): publish via page context with cover and conservative HTML

懂车帝发布闸现走 Playwright/WebContents 内的 fetch,附带 CSRF token、自动
draft+commit 两步保存,并在前置流程中把正文 HTML 收敛到 p/strong/img、表格
降级为段落、过滤本地素材 URL。同步把懂车帝纳入封面强制平台,更新中英文
提示与错误码文案。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 18:35:36 +08:00
parent bf7594ccd8
commit 4c4795e029
10 changed files with 684 additions and 106 deletions
@@ -0,0 +1,104 @@
import { renderTablesAsParagraphRows } from "../../../../../packages/publisher-platforms/src/baijiahao";
export function prepareDongchediArticleHtml(html: string, title = ""): string {
let next = renderTablesAsParagraphRows(html).trim();
next = next.replace(/<hr\b[^>]*\/?>/gi, "");
next = normalizeDongchediImageTags(next);
next = next.replace(/<h[1-6]\b[^>]*>([\s\S]*?)<\/h[1-6]>/gi, (_match, content: string) => {
const text = normalizeInlineContent(content);
return text ? `<p><strong>${text}</strong></p>` : "";
});
next = next.replace(/<li\b[^>]*>([\s\S]*?)<\/li>/gi, (_match, content: string) => {
const text = normalizeInlineContent(content);
return text ? `<p>${text}</p>` : "";
});
next = next.replace(/<\/?(?:ol|ul)\b[^>]*>/gi, "");
next = next.replace(/<p\b[^>]*>/gi, "<p>");
next = next.replace(/<(strong|b)\b[^>]*>/gi, "<strong>").replace(/<\/b>/gi, "</strong>");
next = next.replace(/<br\s*\/?>/gi, " ");
next = next.replace(/<(?!\/?(?:p|strong|img)\b)[^>]+>/gi, "");
next = next.replace(/<p>\s*<\/p>/gi, "");
next = next.replace(/\s+/g, " ");
next = next.replace(/>\s+</g, "><");
next = stripLeadingDuplicateTitleParagraph(next, title);
return next.trim();
}
export function countDongchediContentWordCount(html: string): number {
const text = decodeDongchediHtmlEntities(html)
.replace(/<[^>]*>/g, "")
.replace(/\s+/g, "")
.trim();
return [...text].length;
}
function decodeDongchediHtmlEntities(value: string): string {
return value
.replace(/&nbsp;/gi, " ")
.replace(/&amp;/gi, "&")
.replace(/&lt;/gi, "<")
.replace(/&gt;/gi, ">")
.replace(/&quot;/gi, "\"")
.replace(/&#39;|&apos;/gi, "'")
.replace(/&#(\d+);/g, (_match, code: string) => {
const value = Number.parseInt(code, 10);
return Number.isFinite(value) && value >= 0 && value <= 0x10FFFF ? String.fromCodePoint(value) : "";
})
.replace(/&#x([0-9a-f]+);/gi, (_match, code: string) => {
const value = Number.parseInt(code, 16);
return Number.isFinite(value) && value >= 0 && value <= 0x10FFFF ? String.fromCodePoint(value) : "";
});
}
function normalizeDongchediImageTags(html: string): string {
return html.replace(/<img\b[^>]*>/gi, (imageTag: string) => {
const src = extractAttribute(imageTag, "src");
if (!src) {
return "";
}
const alt = extractAttribute(imageTag, "alt") ?? "";
return `<img src="${escapeAttribute(src)}" alt="${escapeAttribute(alt)}" />`;
});
}
function normalizeInlineContent(value: string): string {
return value
.replace(/<br\s*\/?>/gi, " ")
.replace(/<\/?(?:p|div|section|article|header|footer|ol|ul|li|blockquote|h[1-6])\b[^>]*>/gi, " ")
.replace(/\s+/g, " ")
.trim();
}
function stripLeadingDuplicateTitleParagraph(html: string, title: string): string {
const normalizedTitle = normalizeComparableText(title);
if (!normalizedTitle) {
return html;
}
return html.replace(/^\s*<p>([\s\S]*?)<\/p>/i, (match, paragraphContent: string) => {
return normalizeComparableText(paragraphContent) === normalizedTitle ? "" : match;
});
}
function normalizeComparableText(value: string): string {
return decodeDongchediHtmlEntities(value)
.replace(/<[^>]*>/g, "")
.replace(/\s+/g, "")
.trim();
}
function extractAttribute(sourceTag: string, attributeName: string): string | null {
const matcher = new RegExp(`\\s${attributeName}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'>]+))`, "i");
const match = matcher.exec(sourceTag);
const value = match?.[1] ?? match?.[2] ?? match?.[3] ?? "";
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
function escapeAttribute(value: string): string {
return value
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}