diff --git a/wechat-daily-news-inline.mjs b/wechat-daily-news-inline.mjs index f3c9b96..b8e431d 100644 --- a/wechat-daily-news-inline.mjs +++ b/wechat-daily-news-inline.mjs @@ -2,6 +2,7 @@ import { extractPageTitle } from './wechat/verify/share-preview-repair.mjs'; const MAX_WECHAT_CONTENT_CHARS = 20000; const MAX_WECHAT_CONTENT_TARGET_CHARS = 19600; +const DEFAULT_PORTAL_URL = 'https://m.tkmind.cn'; const CARD_VARIANTS = { 'highlight-box': { @@ -128,7 +129,7 @@ function renderHero(html) { const dateBadge = sanitizeInlineHtml(String(html).match(/
]*>([\s\S]*?)<\/div>/i)?.[1] ?? ''); const subtitle = sanitizeInlineHtml(String(html).match(/
]*>([\s\S]*?)<\/div>/i)?.[1] ?? ''); return [ - '
', + '
', `

${title}

`, dateBadge ? `

${dateBadge}

` : '', subtitle ? `

${subtitle}

` : '', @@ -136,6 +137,39 @@ function renderHero(html) { ].filter(Boolean).join(''); } +function renderReadOriginalTop(publicUrl) { + if (!publicUrl) return ''; + return [ + '
', + `📖 点击阅读原文`, + '

查看完整排版、图片与更多栏目

', + '
', + ].join(''); +} + +function renderCreatePortalCTA(portalUrl = DEFAULT_PORTAL_URL) { + return `

✨ 一起创作 → 点我

`; +} + +function renderFollowMpSection(qrcodeImageUrl = '') { + if (qrcodeImageUrl) { + return [ + '
', + '

📣 关注 TKMind 服务号

', + '

长按扫描关注,每日早报不错过

', + `TKMind 服务号二维码`, + '

长按扫描关注

', + '
', + ].join(''); + } + return [ + '
', + '

📣 关注 TKMind 服务号

', + '

推送草稿时将嵌入公众号二维码

', + '
', + ].join(''); +} + function renderWeatherSection(sectionHtml) { const title = extractInnerHtml(sectionHtml, /
]*>([\s\S]*?)<\/div>/i); const summary = extractInnerHtml(sectionHtml, /
]*>([\s\S]*?)<\/div>/i); @@ -253,9 +287,23 @@ function resolveSectionCardLimit(section, sectionCardLimits) { return sectionCardLimits[section.id] ?? 3; } -function buildWechatInlineContent(html, { publicUrl = '', trimProfile = TRIM_PROFILES[0] } = {}) { +function estimateFixedLayoutLength({ publicUrl = '', qrcodeImageUrl = '', portalUrl = DEFAULT_PORTAL_URL } = {}) { + return ( + (publicUrl ? renderReadOriginalTop(publicUrl).length : 0) + + renderFollowMpSection(qrcodeImageUrl).length + + renderCreatePortalCTA(portalUrl).length + ); +} + +function buildWechatInlineContent(html, { + publicUrl = '', + qrcodeImageUrl = '', + portalUrl = DEFAULT_PORTAL_URL, + trimProfile = TRIM_PROFILES[0], + includeFixedLayout = true, +} = {}) { const sections = splitSections(html); - const parts = [renderHero(html)]; + const bodyParts = [renderHero(html)]; for (const section of sections) { if (trimProfile.skipHistory && shouldSkipSection(section, trimProfile)) { @@ -266,26 +314,27 @@ function buildWechatInlineContent(html, { publicUrl = '', trimProfile = TRIM_PRO } if (section.id === 'weather') { if (!trimProfile.skipWeather) { - parts.push(renderWeatherSection(section.html)); + bodyParts.push(renderWeatherSection(section.html)); } continue; } - parts.push(renderGenericSection(section.html, { + bodyParts.push(renderGenericSection(section.html, { maxCards: resolveSectionCardLimit(section, trimProfile.sectionCardLimits), maxBodyChars: trimProfile.maxBodyChars, })); } - parts.push(renderFooter(html)); + bodyParts.push(renderFooter(html)); - if (publicUrl) { - parts.push( - `

✨ 一起创作 → 点击阅读原文

`, - ); + const ordered = [...bodyParts]; + if (includeFixedLayout) { + if (publicUrl) ordered.splice(1, 0, renderReadOriginalTop(publicUrl)); + ordered.push(renderFollowMpSection(qrcodeImageUrl)); + ordered.push(renderCreatePortalCTA(portalUrl)); } return { - content: parts.join(''), + content: ordered.join(''), sectionCount: sections.length, renderedSectionCount: sections.filter((section) => { if (trimProfile.skipHistory && shouldSkipSection(section, trimProfile)) return false; @@ -305,11 +354,12 @@ function shouldSkipSection(section, trimProfile) { if (!section) return true; if (['history', 'knowledge'].includes(section.id)) return true; if (trimProfile?.skipSectionIds?.includes(section.id)) return true; - return /历史上的今天/u.test(section.title); + return /历史上的今天|近7天新闻早报/u.test(section.title); } function renderGenericSection(sectionHtml, { maxCards = null, maxBodyChars = null } = {}) { const title = extractInnerHtml(sectionHtml, /
]*>([\s\S]*?)<\/div>/i); + if (/近7天新闻早报/u.test(stripInlineText(title))) return ''; const cards = sectionHtml.split(/
= TRIM_PROFILES.length; - if (built.content.length <= MAX_WECHAT_CONTENT_TARGET_CHARS) { + if (built.content.length <= trimTargetChars) { break; } } + let selectedProfile = allProfiles[trimLevel] ?? allProfiles[allProfiles.length - 1]; + if (built) { + built = buildWechatInlineContent(html, { + publicUrl, + qrcodeImageUrl, + portalUrl, + trimProfile: selectedProfile, + includeFixedLayout: true, + }); + while (built.content.length > MAX_WECHAT_CONTENT_CHARS && (selectedProfile.maxBodyChars ?? 120) > 20) { + selectedProfile = { + ...selectedProfile, + skipWeather: true, + maxBodyChars: Math.max(20, (selectedProfile.maxBodyChars ?? 64) - 8), + sectionCardLimits: { + ...(selectedProfile.sectionCardLimits ?? {}), + headlines: Math.max(2, selectedProfile.sectionCardLimits?.headlines ?? 3), + world: Math.min(2, selectedProfile.sectionCardLimits?.world ?? 2), + domestic: Math.min(2, selectedProfile.sectionCardLimits?.domestic ?? 2), + google: 1, + finance: 2, + tech: 2, + sports: 1, + technews: 0, + hot: 0, + }, + }; + built = buildWechatInlineContent(html, { + publicUrl, + qrcodeImageUrl, + portalUrl, + trimProfile: selectedProfile, + includeFixedLayout: true, + }); + usedFallback = true; + } + } + if (!built || built.content.length > MAX_WECHAT_CONTENT_CHARS) { throw new Error( `微信正文超过 ${MAX_WECHAT_CONTENT_CHARS} 字符(当前 ${built?.content.length ?? 0}),请缩短 HTML 页面后重试`, diff --git a/wechat-news-morning-draft.mjs b/wechat-news-morning-draft.mjs index a8fff96..c4dc81b 100644 --- a/wechat-news-morning-draft.mjs +++ b/wechat-news-morning-draft.mjs @@ -17,6 +17,7 @@ const RUNS_TABLE = 'h5_wechat_news_draft_runs'; const DEFAULT_WECHAT_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/stable_token'; const DEFAULT_DRAFT_ADD_URL = 'https://api.weixin.qq.com/cgi-bin/draft/add'; const DEFAULT_MATERIAL_ADD_URL = 'https://api.weixin.qq.com/cgi-bin/material/add_material'; +const DEFAULT_UPLOAD_IMG_URL = 'https://api.weixin.qq.com/cgi-bin/media/uploadimg'; const MAX_THUMB_BYTES = 64 * 1024; export const NEWS_MORNING_TEMPLATE_VERSION = '2026-09-10'; @@ -211,11 +212,46 @@ export function extractDailyNewsArticleMeta(html) { return { title: title.slice(0, 64), digest }; } +export function resolveMpFollowQrcodePath(memindLibRoot = process.cwd()) { + const candidates = [ + path.join(memindLibRoot, 'wechat/assets/mp-follow-qrcode.png'), + path.join(process.cwd(), 'wechat/assets/mp-follow-qrcode.png'), + ]; + return candidates.find((candidate) => fs.existsSync(candidate)) ?? null; +} + +export async function uploadWechatArticleContentImage( + accessToken, + imageBuffer, + { wechatFetch = undiciFetch, uploadUrl = DEFAULT_UPLOAD_IMG_URL, filename = 'content.png' } = {}, +) { + if (!accessToken) throw new Error('缺少微信 access_token'); + if (!Buffer.isBuffer(imageBuffer) || imageBuffer.length === 0) { + throw new Error('正文图片为空'); + } + const form = new FormData(); + form.append( + 'media', + new Blob([imageBuffer], { type: 'image/png' }), + filename, + ); + const endpoint = new URL(uploadUrl); + endpoint.searchParams.set('access_token', accessToken); + const payload = await readJsonResponse( + await wechatFetch(endpoint.toString(), { method: 'POST', body: form }), + ); + if (Number(payload?.errcode ?? 0) !== 0 || !String(payload?.url ?? '').trim()) { + throw new Error(payload?.errmsg || '上传微信正文图片失败'); + } + return String(payload.url); +} + export function buildDailyNewsWechatDraftArticle({ html, publicUrl, + qrcodeImageUrl = '', } = {}) { - const article = convertDailyNewsHtmlToWechatInlineArticle(html, { publicUrl }); + const article = convertDailyNewsHtmlToWechatInlineArticle(html, { publicUrl, qrcodeImageUrl }); return { ...article, cardCount: extractCardArticles(html).length, @@ -223,6 +259,27 @@ export function buildDailyNewsWechatDraftArticle({ }; } +export async function buildDailyNewsWechatDraftArticleForPush({ + html, + publicUrl, + accessToken = null, + wechatFetch = undiciFetch, + memindLibRoot = process.cwd(), +} = {}) { + let qrcodeImageUrl = ''; + if (accessToken) { + const qrcodePath = resolveMpFollowQrcodePath(memindLibRoot); + if (qrcodePath) { + qrcodeImageUrl = await uploadWechatArticleContentImage( + accessToken, + fs.readFileSync(qrcodePath), + { wechatFetch, filename: 'mp-follow-qrcode.png' }, + ); + } + } + return buildDailyNewsWechatDraftArticle({ html, publicUrl, qrcodeImageUrl }); +} + export function convertNewsPageHtmlToWechatArticle(html, { publicUrl = '' } = {}) { const dailyNews = isDailyNewsFormat(html); if (dailyNews) { @@ -700,9 +757,12 @@ export function createWechatNewsMorningDraftService( const thumbMediaId = await uploadWechatPermanentThumb(accessToken, thumbBuffer, { wechatFetch }); const html = fs.readFileSync(page.localPath, 'utf8'); const article = isDailyNewsFormat(html) - ? buildDailyNewsWechatDraftArticle({ + ? await buildDailyNewsWechatDraftArticleForPush({ html, publicUrl: preview.page.publicUrl, + accessToken, + wechatFetch, + memindLibRoot, }) : preview.article; const draft = await addWechatDraftArticle( diff --git a/wechat-news-morning-draft.test.mjs b/wechat-news-morning-draft.test.mjs index 4924283..063ea40 100644 --- a/wechat-news-morning-draft.test.mjs +++ b/wechat-news-morning-draft.test.mjs @@ -91,6 +91,9 @@ test('convertNewsPageHtmlToWechatArticle converts daily-news to inline html', () assert.match(article.content, /每日新闻早报/u); assert.match(article.content, /示例热点/u); assert.match(article.content, /点击阅读原文/u); + assert.match(article.content, /一起创作/u); + assert.match(article.content, /点我/u); + assert.doesNotMatch(article.content, /近7天新闻早报/u); }); test('buildDailyNewsWechatHtmlContent preserves style and body markup', () => { diff --git a/wechat/assets/mp-follow-qrcode.png b/wechat/assets/mp-follow-qrcode.png new file mode 100644 index 0000000..b35751c Binary files /dev/null and b/wechat/assets/mp-follow-qrcode.png differ