diff --git a/docs/wechat-draft-publication-standard.md b/docs/wechat-draft-publication-standard.md index 5a8ff05..4b97275 100644 --- a/docs/wechat-draft-publication-standard.md +++ b/docs/wechat-draft-publication-standard.md @@ -23,8 +23,8 @@ 1. **Hero 实图**(若有):与页面 `mindspace-cover.cover` / hero 背景 / 侧车缩略图同源,经 `uploadimg` 上传 2. **页面正文**:按 profile 转换(诗页 / 散文 / 技能模板 / 通用语义提取) -3. **阅读原文**:12px 小胶囊链接,位于正文末尾、关注区之前;有 `publicUrl` 时必须出现 -4. **欢迎关注 TKMind 智趣** + 公众号二维码(无 logo、无额外导语) +3. **阅读原文**:位于正文末尾、关注区之前;有 `publicUrl` 时必须出现。使用虚线引导卡片 + 上下箭头 + 主色按钮(文案仍为「阅读原文」,禁止回退旧版「点击阅读原文」大按钮) +4. **欢迎关注 TKMind 智趣** + 服务号二维码(约 120px,静态资源 `/assets/mp-follow-qrcode.png`,推送时优先 uploadimg 到微信 CDN) 正文超过 `maxContentChars`(20000)时,**先给阅读原文和关注区留额度,再截正文**,禁止先拼再整体 `slice` 把页脚裁掉。公开页失败弹窗必须展示服务端真实原因,不能用「请先登录 MindSpace」兜底掩盖校验失败。 diff --git a/mindspace-wechat-page-draft.test.mjs b/mindspace-wechat-page-draft.test.mjs index 31cec5c..dc9327a 100644 --- a/mindspace-wechat-page-draft.test.mjs +++ b/mindspace-wechat-page-draft.test.mjs @@ -90,6 +90,9 @@ test('daily-news draft includes read original when publicUrl is present', () => const followIdx = article.content.indexOf('欢迎关注 TKMind 智趣'); assert.ok(readIdx >= 0, '缺少阅读原文'); assert.ok(readIdx < followIdx, '阅读原文应在关注区之前'); + assert.match(article.content, /完整网页版更精彩/); + assert.match(article.content, /⬇ ⬇ ⬇/); + assert.match(article.content, /width:120px;height:120px/); assert.match(article.content, /href="https:\/\/m\.tkmind\.cn\/u\/john\/pages\/tkmind-upgrade-sep2026"/); assert.equal(article.contentSourceUrl, 'https://m.tkmind.cn/u/john/pages/tkmind-upgrade-sep2026'); }); diff --git a/public/assets/mp-follow-qrcode.png b/public/assets/mp-follow-qrcode.png new file mode 100644 index 0000000..fa279d7 Binary files /dev/null and b/public/assets/mp-follow-qrcode.png differ diff --git a/public/dev/mp-follow-qrcode.png b/public/dev/mp-follow-qrcode.png index b35751c..fa279d7 100644 Binary files a/public/dev/mp-follow-qrcode.png and b/public/dev/mp-follow-qrcode.png differ diff --git a/public/dev/wechat-draft-layout-preview.html b/public/dev/wechat-draft-layout-preview.html new file mode 100644 index 0000000..06691a2 --- /dev/null +++ b/public/dev/wechat-draft-layout-preview.html @@ -0,0 +1,70 @@ + + + + + + 公众号草稿 layout 样板 · 八月午后 · 一首短诗 + + + +
+
微信草稿箱 · 排版样板(非真实推送)
+
+

八月午后 · 一首短诗

+

August · Afternoon 八月午后 蝉声把正午拉得很长, 甜是慢的,光是慢的,连无聊都舍不得结束。 短诗 · 四节 2026 年 8 月 阅读约 40 秒 往 下 正文 切换竖排 壹 蝉声把正午拉得很长, 树荫在墙上,翻了个身。 一杯凉白开浮着半朵云, 风扇数着拍子: 一圈,又一圈 。 贰 阳光跨过门槛, 在地板上停住, 像一封没有署名的信。 猫睡成一座小岛, 梦里自有它的海。 叁 远处有人喊了句什么, 被热风揉软, 落在西瓜红色的边缘。 肆 甜是慢的,光是慢的 , 连一整个下午的无聊,都舍不得结束—— 傍晚终于来了, 它只是把影子拉长了一点, 轻声说:明天见。 一整天的漫长,其实只发生在 八月午后那两个小时里 。 蝉 声音的长度 蝉鸣没有起伏,所以时间失去了刻度。午后之所以长,不是因为无事可做,是因为没有东西在推进。 影 会翻身的树荫 墙上的影子一寸寸移动,是这一首诗里唯一诚实的钟表——它从不报时,只慢慢证明下午正在过去。 猫 睡在光斑里 猫总挑最暖的那块地板睡。它比谁都清楚:夏日的好,是可以用身体去接住的。 瓜 甜是慢的 远处的人

+

August · Afternoon 八月午后 蝉声把正午拉得很长, 甜是慢的,光是慢的,连无聊都舍不得结束。 短诗 · 四节 2026 年 8 月 阅读约 40 秒 往 下 正文 切换竖排 壹 蝉声把正午拉得很长, 树荫在墙上,翻了个身。 一杯凉白开浮着半朵云, 风扇数着拍子: 一圈,又一圈 。 贰 阳光跨过门槛, 在地板上停住, 像一封没有署名的信。 猫睡成一座小岛, 梦里自有它的海。 叁 远处有人喊了句什么, 被热风揉软, 落在西瓜红色的边缘。 肆 甜是慢的,光是慢的 , 连一整个下午的无聊,都舍不得结束—— 傍晚终于来了, 它只是把影子拉长了一点, 轻声说:明天见。 一整天的漫长,其实只发生在 八月午后那两个小时里 。 蝉 声音的长度 蝉鸣没有起伏,所以时间失去了刻度。午后之所以长,不是因为无事可做,是因为没有东西在推进。 影 会翻身的树荫 墙上的影子一寸寸移动,是这一首诗里唯一诚实的钟表——它从不报时,只慢慢证明下午正在过去。 猫 睡在光斑里 猫总挑最暖的那块地板睡。它比谁都清楚:夏日的好,是可以用身体去接住的。 瓜 甜是慢的 远处的人

+

✨ 完整网页版更精彩

动画 · 互动 · 高清大图 · 完整内容

⬇ ⬇ ⬇

📖 阅读原文

👆 点击上方按钮,查看完整精彩页面

➡️ ➡️ ➡️

+

欢迎关注 TKMind 智趣

TKMind 服务号二维码

长按扫描关注

+
+
+
+

阅读原文跳转:https://m.tkmind.cn/MindSpace/1c99b83b-0454-474f-a5d2-129d34506a32/public/august-afternoon.html

+

二维码静态资源:https://m.tkmind.cn/assets/mp-follow-qrcode.png

+

scripts/preview-wechat-draft-layout.mjs 生成,用于确认「阅读原文」引导与关注区二维码尺寸。

+
+ + \ No newline at end of file diff --git a/scripts/preview-wechat-draft-layout.mjs b/scripts/preview-wechat-draft-layout.mjs new file mode 100644 index 0000000..5071664 --- /dev/null +++ b/scripts/preview-wechat-draft-layout.mjs @@ -0,0 +1,144 @@ +#!/usr/bin/env node +/** + * 生成公众号草稿底部 layout 样板页(阅读原文引导 + TKMind 关注二维码)。 + * + * 用法: + * node scripts/preview-wechat-draft-layout.mjs [publicUrl] [outputPath] + */ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + applyWechatDraftArticleLayout, + resolveMpFollowQrcodePublicUrl, +} from '../wechat-draft-article-layout.mjs'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); +const userId = '1c99b83b-0454-474f-a5d2-129d34506a32'; +const defaultPublicUrl = `https://m.tkmind.cn/MindSpace/${userId}/public/august-afternoon.html`; + +async function loadSampleBody() { + const samplePath = path.join(root, 'MindSpace', userId, 'public', 'august-afternoon.html'); + try { + const html = await fs.readFile(samplePath, 'utf8'); + const titleMatch = html.match(/([^<]+)<\/title>/i); + const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i); + const text = String(bodyMatch?.[1] ?? '') + .replace(/<script[\s\S]*?<\/script>/gi, '') + .replace(/<style[\s\S]*?<\/style>/gi, '') + .replace(/<[^>]+>/g, ' ') + .replace(/\s+/g, ' ') + .trim() + .slice(0, 480); + return { + title: titleMatch?.[1]?.trim() || 'MindSpace 页面样板', + excerpt: text || '这是一段示例正文,推送到公众号后会自动转换为微信兼容格式。', + }; + } catch { + return { + title: 'MindSpace 页面样板', + excerpt: '这是一段示例正文。完整网页版包含更丰富的排版、动画与互动内容,请点击下方「阅读原文」查看。', + }; + } +} + +function renderPreviewHtml({ title, excerpt, articleContent, publicUrl, qrcodeUrl }) { + return `<!doctype html> +<html lang="zh-CN"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>公众号草稿 layout 样板 · ${title} + + + +
+
微信草稿箱 · 排版样板(非真实推送)
+
+

${title}

+

${excerpt}

+ ${articleContent} +
+
+
+

阅读原文跳转:${publicUrl}

+

二维码静态资源:${qrcodeUrl}

+

scripts/preview-wechat-draft-layout.mjs 生成,用于确认「阅读原文」引导与关注区二维码尺寸。

+
+ +`; +} + +async function main() { + const publicUrl = process.argv[2]?.trim() || defaultPublicUrl; + const outputPath = path.resolve( + process.argv[3] || path.join(root, 'public/dev/wechat-draft-layout-preview.html'), + ); + const sample = await loadSampleBody(); + const qrcodeUrl = resolveMpFollowQrcodePublicUrl(process.env); + const articleContent = applyWechatDraftArticleLayout( + `

${sample.excerpt}

`, + { publicUrl, qrcodeImageUrl: qrcodeUrl }, + ); + const html = renderPreviewHtml({ + title: sample.title, + excerpt: sample.excerpt, + articleContent, + publicUrl, + qrcodeUrl, + }); + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await fs.writeFile(outputPath, html, 'utf8'); + console.log(`样板页已写入 ${outputPath}`); + console.log(`publicUrl: ${publicUrl}`); + console.log(`qrcode: ${qrcodeUrl}`); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/wechat-daily-news-inline.mjs b/wechat-daily-news-inline.mjs index ccbc2b2..de10e34 100644 --- a/wechat-daily-news-inline.mjs +++ b/wechat-daily-news-inline.mjs @@ -4,6 +4,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 FOLLOW_QRCODE_SIZE_PX = 120; const CARD_VARIANTS = { 'highlight-box': { @@ -152,9 +153,16 @@ function renderHero(html, imageUrlMap = new Map()) { function renderReadOriginalLink(publicUrl) { if (!publicUrl) return ''; return [ - '

', - `📖 阅读原文`, + '

', + '

✨ 完整网页版更精彩

', + '

动画 · 互动 · 高清大图 · 完整内容

', + '

⬇ ⬇ ⬇

', + '

', + `📖 阅读原文`, '

', + '

👆 点击上方按钮,查看完整精彩页面

', + '

➡️ ➡️ ➡️

', + '
', ].join(''); } @@ -167,7 +175,7 @@ function renderFollowMpSection(qrcodeImageUrl = '') { return [ '
', '

欢迎关注 TKMind 智趣

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

长按扫描关注

', '
', ].join(''); diff --git a/wechat-draft-article-layout.mjs b/wechat-draft-article-layout.mjs index 0409cc7..88509b5 100644 --- a/wechat-draft-article-layout.mjs +++ b/wechat-draft-article-layout.mjs @@ -14,6 +14,13 @@ import { WECHAT_DRAFT_PUBLICATION_STANDARD } from './wechat-draft-publication-st const WECHAT_DRAFT_CONTENT_CHAR_LIMIT = WECHAT_DRAFT_PUBLICATION_STANDARD.limits.maxContentChars; +const FOLLOW_QRCODE_SIZE_PX = 120; +const MP_FOLLOW_QRCODE_PUBLIC_PATH = '/assets/mp-follow-qrcode.png'; + +export function resolveMpFollowQrcodePublicUrl(env = process.env) { + const base = String(env.H5_PUBLIC_BASE_URL ?? 'https://m.tkmind.cn').replace(/\/$/, ''); + return `${base}${MP_FOLLOW_QRCODE_PUBLIC_PATH}`; +} export function clipWechatDraftBodyToFitFooter( body, @@ -35,9 +42,16 @@ export function clipWechatDraftBodyToFitFooter( export function renderReadOriginalLink(publicUrl) { if (!publicUrl) return ''; return [ - '

', - `📖 阅读原文`, + '

', + '

✨ 完整网页版更精彩

', + '

动画 · 互动 · 高清大图 · 完整内容

', + '

⬇ ⬇ ⬇

', + '

', + `📖 阅读原文`, '

', + '

👆 点击上方按钮,查看完整精彩页面

', + '

➡️ ➡️ ➡️

', + '
', ].join(''); } @@ -48,7 +62,7 @@ export function renderFollowTkMindSection({ qrcodeImageUrl = '', } = {}) { const qrcodeBlock = qrcodeImageUrl - ? `TKMind 服务号二维码` + ? `TKMind 服务号二维码` : ''; return [ '
', @@ -84,23 +98,41 @@ export function resolveTkMindBrandIconPath(memindLibRoot = process.cwd()) { return candidates.find((candidate) => fs.existsSync(candidate)) ?? null; } +export function resolveMpFollowQrcodeAssetPath(memindLibRoot = process.cwd()) { + const fromHelper = resolveMpFollowQrcodePath(memindLibRoot); + if (fromHelper) return fromHelper; + const publicAsset = path.join(memindLibRoot, 'public', MP_FOLLOW_QRCODE_PUBLIC_PATH.replace(/^\//, '')); + if (fs.existsSync(publicAsset)) return publicAsset; + const cwdPublicAsset = path.join(process.cwd(), 'public', MP_FOLLOW_QRCODE_PUBLIC_PATH.replace(/^\//, '')); + return fs.existsSync(cwdPublicAsset) ? cwdPublicAsset : null; +} + export async function uploadWechatDraftBrandAssets({ accessToken, wechatFetch = undiciFetch, memindLibRoot = process.cwd(), + env = process.env, } = {}) { + const qrcodePath = resolveMpFollowQrcodeAssetPath(memindLibRoot); + const publicFallbackUrl = resolveMpFollowQrcodePublicUrl(env); let qrcodeImageUrl = ''; - if (!accessToken) { + + if (!qrcodePath) { return { qrcodeImageUrl }; } - const qrcodePath = resolveMpFollowQrcodePath(memindLibRoot); - if (qrcodePath) { + if (!accessToken) { + return { qrcodeImageUrl: publicFallbackUrl }; + } + + try { qrcodeImageUrl = await uploadWechatArticleContentImage( accessToken, fs.readFileSync(qrcodePath), { wechatFetch, filename: 'mp-follow-qrcode.png' }, ); + } catch { + qrcodeImageUrl = publicFallbackUrl; } return { qrcodeImageUrl }; diff --git a/wechat-draft-publication-standard.mjs b/wechat-draft-publication-standard.mjs index a613396..255e1e7 100644 --- a/wechat-draft-publication-standard.mjs +++ b/wechat-draft-publication-standard.mjs @@ -19,7 +19,7 @@ export const WECHAT_DRAFT_PUBLICATION_STANDARD = Object.freeze({ readOriginal: Object.freeze({ position: 'after-content-before-footer', label: '阅读原文', - style: 'compact-link-12px', + style: 'guided-card-with-arrows', forbidLegacyLargeButton: true, }), followFooter: Object.freeze({ diff --git a/wechat-draft-publication-standard.test.mjs b/wechat-draft-publication-standard.test.mjs index 20a1151..479b660 100644 --- a/wechat-draft-publication-standard.test.mjs +++ b/wechat-draft-publication-standard.test.mjs @@ -5,6 +5,7 @@ import test from 'node:test'; import { applyWechatDraftArticleLayout, clipWechatDraftBodyToFitFooter, + resolveMpFollowQrcodePublicUrl, } from './wechat-draft-article-layout.mjs'; import { buildMindSpacePageWechatDraftArticleForPush, @@ -29,6 +30,27 @@ test('standard version is pinned', () => { assert.equal(WECHAT_DRAFT_PUBLICATION_STANDARD.cover.priority[0], 'hero-raster'); }); +test('applyWechatDraftArticleLayout renders guided read-original section and smaller qrcode', () => { + const content = applyWechatDraftArticleLayout('

正文

', { + publicUrl: PUBLIC_URL, + qrcodeImageUrl: 'https://m.tkmind.cn/assets/mp-follow-qrcode.png', + }); + assert.match(content, /完整网页版更精彩/); + assert.match(content, /⬇ ⬇ ⬇/); + assert.match(content, /点击上方按钮,查看完整精彩页面/); + assert.match(content, /width:120px;height:120px/); + assert.match(content, /📖 阅读原文/); + const check = verifyWechatDraftPublicationContent(content, { publicUrl: PUBLIC_URL }); + assert.equal(check.ok, true, check.issues.join('; ')); +}); + +test('resolveMpFollowQrcodePublicUrl points to platform asset path', () => { + assert.equal( + resolveMpFollowQrcodePublicUrl({ H5_PUBLIC_BASE_URL: 'https://m.tkmind.cn' }), + 'https://m.tkmind.cn/assets/mp-follow-qrcode.png', + ); +}); + test('verifyWechatDraftPublicationContent rejects legacy large read button', () => { const bad = applyWechatDraftArticleLayout('

正文

', { publicUrl: PUBLIC_URL, @@ -81,13 +103,13 @@ test('august-afternoon article layout matches publication standard', async () => }); const check = verifyWechatDraftPublicationContent(article.content, { publicUrl: PUBLIC_URL }); assert.equal(check.ok, true, check.issues.join('; ')); + assert.match(article.content, /https:\/\/m\.tkmind\.cn\/assets\/mp-follow-qrcode\.png/); const readIdx = article.content.indexOf('阅读原文'); const followIdx = article.content.indexOf('欢迎关注 TKMind 智趣'); assert.ok(readIdx >= 0, '缺少阅读原文'); assert.ok(readIdx < followIdx, '阅读原文应在关注区之前'); if (AUGUST_HTML.includes('蝉声把正午拉得很长')) { assert.match(article.content, /蝉声把正午拉得很长/); - const heroIdx = article.content.indexOf(' fs.existsSync(candidate)) ?? null; } @@ -442,10 +444,10 @@ export async function buildDailyNewsWechatDraftArticleForPush({ }); } -export function convertNewsPageHtmlToWechatArticle(html, { publicUrl = '' } = {}) { +export function convertNewsPageHtmlToWechatArticle(html, { publicUrl = '', qrcodeImageUrl = '' } = {}) { const dailyNews = isDailyNewsFormat(html); if (dailyNews) { - return buildDailyNewsWechatDraftArticle({ html, publicUrl }); + return buildDailyNewsWechatDraftArticle({ html, publicUrl, qrcodeImageUrl }); } const title = extractPageTitle(html) || '今日新闻热点分析'; const digest = (extractMetaDescription(html) || title).slice(0, 120); diff --git a/wechat-news-morning-draft.test.mjs b/wechat-news-morning-draft.test.mjs index f70cc3e..c9b6f76 100644 --- a/wechat-news-morning-draft.test.mjs +++ b/wechat-news-morning-draft.test.mjs @@ -85,12 +85,17 @@ function createPool(seedRow = null) { test('convertNewsPageHtmlToWechatArticle converts daily-news to inline html', () => { const article = convertNewsPageHtmlToWechatArticle(SAMPLE_HTML, { publicUrl: 'https://m.tkmind.cn/MindSpace/demo/public/daily-news-0910.html', + qrcodeImageUrl: 'https://m.tkmind.cn/assets/mp-follow-qrcode.png', }); assert.match(article.title, /2026年9月10日/u); assert.equal(article.contentMode, '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.match(article.content, /width:120px;height:120px/u); + assert.match(article.content, /mp-follow-qrcode\.png/u); assert.match(article.content, /一起创作/u); assert.match(article.content, /点我/u); assert.doesNotMatch(article.content, /近7天新闻早报/u); diff --git a/wechat/assets/mp-follow-qrcode.png b/wechat/assets/mp-follow-qrcode.png index b35751c..fa279d7 100644 Binary files a/wechat/assets/mp-follow-qrcode.png and b/wechat/assets/mp-follow-qrcode.png differ