fix(wechat): 长文推送先留页脚,失败弹窗展示真实原因
Memind CI / Test, build, and release guards (push) Has been cancelled

超过 2 万字时不再把阅读原文和关注区裁掉;公开页错误态不再误报需要登录 MindSpace。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
John
2026-09-12 12:51:06 +08:00
parent 47cbb4615b
commit 014c3b7981
8 changed files with 102 additions and 17 deletions
@@ -26,6 +26,8 @@
3. **阅读原文**:12px 小胶囊链接,位于正文末尾、关注区之前;有 `publicUrl` 时必须出现
4. **欢迎关注 TKMind 智趣** + 公众号二维码(无 logo、无额外导语)
正文超过 `maxContentChars`(20000)时,**先给阅读原文和关注区留额度,再截正文**,禁止先拼再整体 `slice` 把页脚裁掉。公开页失败弹窗必须展示服务端真实原因,不能用「请先登录 MindSpace」兜底掩盖校验失败。
## 封面(thumb_media_id
优先级:
+6 -4
View File
@@ -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 } = {}) {
</div>
<div data-wechat-view="error" hidden>
<h3>操作失败</h3>
<p data-wechat-hint>请稍后重试,或先登录 MindSpace。</p>
<p data-wechat-hint>推送失败,请稍后重试。</p>
<div data-mindspace-public-share-dialog-actions">
<button type="button" data-action="wechat-error-close">关闭</button>
</div>
+7
View File
@@ -20,6 +20,13 @@ test('injectPublicFileShareButton adds plaza entry and confirm dialog', () => {
assert.match(result.html, /apiErrorBody/);
assert.match(result.html, /redirectToMindSpaceLogin/);
assert.match(result.html, /auth\/wechat\/authorize\?intent=login/);
assert.match(result.html, /setWechatErrorHint/);
assert.match(result.html, /\[data-wechat-view="error"\] \[data-wechat-hint\]/);
assert.match(result.html, /推送失败,请稍后重试。/);
assert.doesNotMatch(
result.html,
/data-wechat-view="error"[\s\S]*请稍后重试,或先登录 MindSpace/,
);
assert.match(result.html, /ALREADY_PUBLISHED/);
assert.equal(result.scriptHashes.length, 1);
});
+12 -2
View File
@@ -645,12 +645,22 @@ export async function buildWechatDraftPublicationBundleForPush({
const contentCheck = verifyWechatDraftPublicationContent(article.content, { publicUrl });
const coverCheck = verifyWechatDraftCoverResolution(thumbResolved);
if (!contentCheck.ok || !coverCheck.ok) {
const issues = [...contentCheck.issues, ...coverCheck.issues];
throw Object.assign(
new Error(`草稿不符合推送标准 ${WECHAT_DRAFT_PUBLICATION_STANDARD_VERSION}`),
new Error(
`草稿不符合推送标准 ${WECHAT_DRAFT_PUBLICATION_STANDARD_VERSION}${
issues.length ? `${issues.join('')}` : ''
}`,
),
{
code: 'wechat_draft_standard_violation',
contentIssues: contentCheck.issues,
coverIssues: coverCheck.issues,
details: {
contentIssues: contentCheck.issues,
coverIssues: coverCheck.issues,
standardVersion: WECHAT_DRAFT_PUBLICATION_STANDARD_VERSION,
},
},
);
}
@@ -686,7 +696,7 @@ export function convertGenericPageHtmlToWechatArticle(
return {
title,
digest,
content: sections.join('\n').slice(0, 20000),
content: sections.join('\n'),
contentSourceUrl: publicUrl || undefined,
};
}
+20 -3
View File
@@ -107,6 +107,17 @@ test('applyWechatDraftArticleLayout adds read original and follow footer', () =>
assert.ok(content.indexOf('阅读原文') < content.indexOf('欢迎关注'));
});
test('applyWechatDraftArticleLayout clips long body instead of required footer', () => {
const content = applyWechatDraftArticleLayout(`<p>${'超长正文'.repeat(5000)}</p>`, {
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 = `<p>${'很长'.repeat(12000)}</p>`;
const article = mindspaceWechatPageDraftInternals.convertGenericPageHtmlToWechatArticle(
`<html><body>${longBody}</body></html>`,
{ 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', () => {
+1
View File
@@ -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;
+27 -4
View File
@@ -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()) {
+27 -4
View File
@@ -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 = `<p>${'一年级语文'.repeat(4000)}</p>`;
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('<img');
assert.ok(heroIdx < readIdx, 'hero 应在阅读原文之前');
assert.ok(readIdx >= 0, '缺少阅读原文');
assert.ok(readIdx < followIdx, '阅读原文应在关注区之前');
if (AUGUST_HTML.includes('蝉声把正午拉得很长')) {
assert.match(article.content, /蝉声把正午拉得很长/);
const heroIdx = article.content.indexOf('<img');
assert.ok(heroIdx < readIdx, 'hero 应在阅读原文之前');
}
});