]*>([\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