diff --git a/docs/wechat-draft-publication-standard.md b/docs/wechat-draft-publication-standard.md index c9d2a82..5a8ff05 100644 --- a/docs/wechat-draft-publication-standard.md +++ b/docs/wechat-draft-publication-standard.md @@ -26,6 +26,8 @@ 3. **阅读原文**:12px 小胶囊链接,位于正文末尾、关注区之前;有 `publicUrl` 时必须出现 4. **欢迎关注 TKMind 智趣** + 公众号二维码(无 logo、无额外导语) +正文超过 `maxContentChars`(20000)时,**先给阅读原文和关注区留额度,再截正文**,禁止先拼再整体 `slice` 把页脚裁掉。公开页失败弹窗必须展示服务端真实原因,不能用「请先登录 MindSpace」兜底掩盖校验失败。 + ## 封面(thumb_media_id) 优先级: diff --git a/mindspace-public-share-widget.mjs b/mindspace-public-share-widget.mjs index a6a059b..eb57da9 100644 --- a/mindspace-public-share-widget.mjs +++ b/mindspace-public-share-widget.mjs @@ -121,6 +121,7 @@ var wechatBlockedClose=wechatDialog.querySelector('[data-action="wechat-blocked- var wechatForbiddenClose=wechatDialog.querySelector('[data-action="wechat-forbidden-close"]'); var wechatDraftId=wechatDialog.querySelector('[data-wechat-draft-id]'); var wechatHint=wechatDialog.querySelector('[data-wechat-view="confirm"] [data-wechat-hint]'); +var wechatErrorHint=wechatDialog.querySelector('[data-wechat-view="error"] [data-wechat-hint]'); var wechatViews={ confirm:wechatDialog.querySelector('[data-wechat-view="confirm"]'), loading:wechatDialog.querySelector('[data-wechat-view="loading"]'), @@ -141,6 +142,7 @@ void refreshWechatDialog(); } function closeWechatDialog(){wechatDialog.hidden=true;showWechatView('confirm');} function setWechatHint(text){if(wechatHint)wechatHint.textContent=text||'';} +function setWechatErrorHint(text){if(wechatErrorHint)wechatErrorHint.textContent=text||'推送失败,请稍后重试。';} async function refreshWechatDialog(){ if(wechatChecking)return; var relativePath=workspaceRelativePath(); @@ -157,12 +159,12 @@ throw new Error(errBody.message||'无法检查公众号草稿状态'); var data=payload.data||{}; if(!data.configured){showWechatView('blocked');setWechatHint('请先在 M 配置中绑定公众号 AppID 和 AppSecret。');return;} var blockers=(data.validation&&data.validation.blockers)||[]; -if(!data.canPush){showWechatView('error');setWechatHint(blockers[0]||'当前页面暂不符合推送要求');return;} +if(!data.canPush){showWechatView('error');setWechatErrorHint(blockers[0]||'当前页面暂不符合推送要求');return;} setWechatHint(data.pageTitle||'当前页面'); showWechatView('confirm'); }catch(e){ showWechatView('error'); -setWechatHint(e&&e.message?e.message:'无法检查公众号草稿状态'); +setWechatErrorHint(e&&e.message?e.message:'无法检查公众号草稿状态'); }finally{wechatChecking=false;} } wechatButton.addEventListener('click',openWechatDialog); @@ -194,7 +196,7 @@ if(wechatDraftId){wechatDraftId.textContent=data.draftMediaId?'草稿 media_id setStatus('已推送到公众号草稿箱',false,true); }catch(e){ showWechatView('error'); -setWechatHint(e&&e.message?e.message:'推送失败'); +setWechatErrorHint(e&&e.message?e.message:'推送失败'); }finally{pushing=false;wechatConfirm.disabled=false;} }); } @@ -307,7 +309,7 @@ export function injectPublicFileShareButton(html, { isOwner = true } = {}) {
请稍后重试,或先登录 MindSpace。
+推送失败,请稍后重试。
${'超长正文'.repeat(5000)}
`, { + publicUrl: 'https://m.tkmind.cn/u/demo/pages/long.html', + qrcodeImageUrl: 'https://mmbiz.qpic.cn/qrcode.png', + }); + assert.ok(content.length <= 20000); + assert.match(content, /阅读原文/); + assert.match(content, /欢迎关注 TKMind 智趣/); + assert.ok(content.indexOf('阅读原文') < content.indexOf('欢迎关注')); +}); + test('convertPageHtmlToWechatArticle uses rich mindspace converter', () => { const article = convertPageHtmlToWechatArticle(POEM_HTML, { pageTitle: '测试页面', @@ -191,13 +202,19 @@ test('collectPageImageCandidates adds sidecar thumbnail when hero image missing' assert.ok(candidates.some((item) => /\.thumbnail\.(png|svg)$/i.test(item.ref ?? ''))); }); -test('convertGenericPageHtmlToWechatArticle truncates long content', () => { +test('convertGenericPageHtmlToWechatArticle leaves length clipping to layout', () => { const longBody = `${'很长'.repeat(12000)}
`; const article = mindspaceWechatPageDraftInternals.convertGenericPageHtmlToWechatArticle( `${longBody}`, - { pageTitle: '长文' }, + { pageTitle: '长文', publicUrl: 'https://m.tkmind.cn/u/demo/pages/long.html' }, ); - assert.ok(article.content.length <= 20000); + const laidOut = applyWechatDraftArticleLayout(article.content, { + publicUrl: 'https://m.tkmind.cn/u/demo/pages/long.html', + }); + assert.ok(article.content.length > 20000); + assert.ok(laidOut.length <= 20000); + assert.match(laidOut, /阅读原文/); + assert.match(laidOut, /欢迎关注 TKMind 智趣/); }); test('resolveWechatDraftThumbPath prefers hero image over thumbnail sidecar', () => { diff --git a/server.mjs b/server.mjs index 2185cb2..f836c9d 100644 --- a/server.mjs +++ b/server.mjs @@ -1083,6 +1083,7 @@ function mindSpaceError(res, req, error) { wechat_mp_not_configured: 400, invalid_wechat_mp_config: 400, wechat_draft_push_failed: 502, + wechat_draft_standard_violation: 422, }; const code = error?.code ?? 'internal_error'; const status = statusByCode[code] ?? 500; diff --git a/wechat-draft-article-layout.mjs b/wechat-draft-article-layout.mjs index cb1e8aa..0409cc7 100644 --- a/wechat-draft-article-layout.mjs +++ b/wechat-draft-article-layout.mjs @@ -10,6 +10,27 @@ import { resolveMpFollowQrcodePath, uploadWechatArticleContentImage, } from './wechat-news-morning-draft.mjs'; +import { WECHAT_DRAFT_PUBLICATION_STANDARD } from './wechat-draft-publication-standard.mjs'; + +const WECHAT_DRAFT_CONTENT_CHAR_LIMIT = + WECHAT_DRAFT_PUBLICATION_STANDARD.limits.maxContentChars; + +export function clipWechatDraftBodyToFitFooter( + body, + footer, + limit = WECHAT_DRAFT_CONTENT_CHAR_LIMIT, +) { + const source = String(body ?? ''); + const suffix = String(footer ?? ''); + const separator = source && suffix ? '\n' : ''; + const maxBody = Math.max(0, Number(limit) - suffix.length - separator.length); + if (source.length <= maxBody) return source; + let clipped = source.slice(0, maxBody); + const lastLt = clipped.lastIndexOf('<'); + const lastGt = clipped.lastIndexOf('>'); + if (lastLt > lastGt) clipped = clipped.slice(0, lastLt); + return clipped.trimEnd(); +} export function renderReadOriginalLink(publicUrl) { if (!publicUrl) return ''; @@ -44,12 +65,14 @@ export function applyWechatDraftArticleLayout( content, { publicUrl = '', qrcodeImageUrl = '' } = {}, ) { - const parts = [String(content ?? '').trim()]; + const footerParts = []; if (publicUrl) { - parts.push(renderReadOriginalLink(publicUrl)); + footerParts.push(renderReadOriginalLink(publicUrl)); } - parts.push(renderFollowTkMindSection({ qrcodeImageUrl })); - return parts.filter(Boolean).join('\n').slice(0, 20000); + footerParts.push(renderFollowTkMindSection({ qrcodeImageUrl })); + const footer = footerParts.filter(Boolean).join('\n'); + const body = clipWechatDraftBodyToFitFooter(String(content ?? '').trim(), footer); + return [body, footer].filter(Boolean).join(body && footer ? '\n' : ''); } export function resolveTkMindBrandIconPath(memindLibRoot = process.cwd()) { diff --git a/wechat-draft-publication-standard.test.mjs b/wechat-draft-publication-standard.test.mjs index bb5a718..20a1151 100644 --- a/wechat-draft-publication-standard.test.mjs +++ b/wechat-draft-publication-standard.test.mjs @@ -2,7 +2,10 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import test from 'node:test'; -import { applyWechatDraftArticleLayout } from './wechat-draft-article-layout.mjs'; +import { + applyWechatDraftArticleLayout, + clipWechatDraftBodyToFitFooter, +} from './wechat-draft-article-layout.mjs'; import { buildMindSpacePageWechatDraftArticleForPush, resolveWechatDraftThumbPath, @@ -49,6 +52,23 @@ test('august-afternoon cover resolves to hero raster', () => { assert.equal(coverCheck.ok, true); }); +test('applyWechatDraftArticleLayout keeps required footer when body exceeds 20000 chars', () => { + const longBody = `${'一年级语文'.repeat(4000)}
`; + const content = applyWechatDraftArticleLayout(longBody, { + publicUrl: PUBLIC_URL, + qrcodeImageUrl: 'https://mmbiz.qpic.cn/qrcode.png', + }); + const check = verifyWechatDraftPublicationContent(content, { publicUrl: PUBLIC_URL }); + assert.equal(check.ok, true, check.issues.join('; ')); + assert.ok(content.length <= WECHAT_DRAFT_PUBLICATION_STANDARD.limits.maxContentChars); + assert.ok(content.indexOf('一年级语文') >= 0); + assert.ok(content.indexOf('阅读原文') < content.indexOf('欢迎关注 TKMind 智趣')); + assert.equal( + clipWechatDraftBodyToFitFooter(longBody, 'FOOTER', 20).length + 'FOOTER'.length + 1, + 20, + ); +}); + test('august-afternoon article layout matches publication standard', async () => { const article = await buildMindSpacePageWechatDraftArticleForPush({ html: AUGUST_HTML, @@ -61,10 +81,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, /蝉声把正午拉得很长/); const readIdx = article.content.indexOf('阅读原文'); const followIdx = article.content.indexOf('欢迎关注 TKMind 智趣'); - const heroIdx = article.content.indexOf('