b8bcbcb764
让定时任务按新版式推送草稿:热门旅游城市天气、无 Hero logo,并约束生成页必须写城市天气卡。 Co-authored-by: Cursor <cursoragent@cursor.com>
161 lines
6.2 KiB
JavaScript
161 lines
6.2 KiB
JavaScript
/**
|
|
* 公众号草稿统一 layout:阅读原文 + 底部关注区。
|
|
* 标准定义见 wechat-draft-publication-standard.mjs 与 docs/wechat-draft-publication-standard.md
|
|
*/
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fetch as undiciFetch } from 'undici';
|
|
import { PLATFORM_BRAND_ICON_PATH } from './mindspace-og-tags.mjs';
|
|
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;
|
|
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,
|
|
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 '';
|
|
return [
|
|
'<section style="text-align:center;margin:28px 0 14px;padding:24px 18px;background:#f4f8ff;border:1px dashed #1a4a8a;border-radius:16px">',
|
|
'<p style="margin:0;font-size:15px;font-weight:700;color:#0d2b5c;line-height:1.6">完整网页版更精彩</p>',
|
|
'<p style="margin:8px 0 0;font-size:12px;color:#3a5f92;line-height:1.6">全部资讯 · 高清配图 · 来源直达 · 历史早报</p>',
|
|
'<p style="margin:15px 0 0;font-size:19px;line-height:1;color:#1a4a8a;letter-spacing:8px">⬇ ⬇ ⬇</p>',
|
|
'<p style="margin:15px 0 0">',
|
|
`<a href="${publicUrl}" style="display:inline-block;padding:13px 36px;border-radius:999px;background:linear-gradient(135deg,#0d2b5c,#1a4a8a);color:#fff;font-size:16px;font-weight:700;text-decoration:none">📖 阅读原文</a>`,
|
|
'</p>',
|
|
'<p style="margin:14px 0 0;font-size:12px;color:#3a5f92;line-height:1.5">点击上方按钮,查看完整早报</p>',
|
|
'</section>',
|
|
].join('');
|
|
}
|
|
|
|
/** @deprecated 保留别名,避免外部引用断裂 */
|
|
export const renderReadOriginalTop = renderReadOriginalLink;
|
|
|
|
export function renderFollowTkMindSection({
|
|
qrcodeImageUrl = '',
|
|
} = {}) {
|
|
const qrcodeBlock = qrcodeImageUrl
|
|
? `<img src="${qrcodeImageUrl}" alt="TKMind 服务号二维码" style="width:${FOLLOW_QRCODE_SIZE_PX}px;height:${FOLLOW_QRCODE_SIZE_PX}px;display:block;margin:14px auto 0;border-radius:10px" />`
|
|
: '';
|
|
return [
|
|
'<section style="text-align:center;padding:24px 16px;margin:18px 0 8px;background:#fff5f6;border-radius:14px">',
|
|
'<p style="margin:0;font-size:16px;font-weight:700;color:#c8102e;line-height:1.5">欢迎关注 TKMind 智趣</p>',
|
|
'<p style="margin:8px 0 0;font-size:12px;color:#9b9b9b;line-height:1.6">每日早报 · 智能创作 · 一键成页</p>',
|
|
qrcodeBlock,
|
|
qrcodeImageUrl
|
|
? '<p style="margin:12px 0 0;font-size:12px;color:#9b9b9b">长按扫描关注</p>'
|
|
: '<p style="margin:12px 0 0;font-size:12px;color:#9b9b9b">关注 TKMind 服务号</p>',
|
|
'</section>',
|
|
].filter(Boolean).join('');
|
|
}
|
|
|
|
export function applyWechatDraftArticleLayout(
|
|
content,
|
|
{ publicUrl = '', qrcodeImageUrl = '' } = {},
|
|
) {
|
|
const footerParts = [];
|
|
if (publicUrl) {
|
|
footerParts.push(renderReadOriginalLink(publicUrl));
|
|
}
|
|
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()) {
|
|
const candidates = [
|
|
path.join(memindLibRoot, 'public', PLATFORM_BRAND_ICON_PATH.replace(/^\//, '')),
|
|
path.join(memindLibRoot, 'dist', PLATFORM_BRAND_ICON_PATH.replace(/^\//, '')),
|
|
path.join(process.cwd(), 'public', PLATFORM_BRAND_ICON_PATH.replace(/^\//, '')),
|
|
];
|
|
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 (!qrcodePath) {
|
|
return { qrcodeImageUrl };
|
|
}
|
|
|
|
if (!accessToken) {
|
|
return { qrcodeImageUrl: publicFallbackUrl };
|
|
}
|
|
|
|
try {
|
|
qrcodeImageUrl = await uploadWechatArticleContentImage(
|
|
accessToken,
|
|
fs.readFileSync(qrcodePath),
|
|
{ wechatFetch, filename: 'mp-follow-qrcode.png' },
|
|
);
|
|
} catch {
|
|
qrcodeImageUrl = publicFallbackUrl;
|
|
}
|
|
|
|
return { qrcodeImageUrl };
|
|
}
|
|
|
|
export async function finalizeWechatDraftArticleForPush({
|
|
content,
|
|
publicUrl = '',
|
|
accessToken = null,
|
|
wechatFetch = undiciFetch,
|
|
memindLibRoot = process.cwd(),
|
|
} = {}) {
|
|
const brandAssets = await uploadWechatDraftBrandAssets({
|
|
accessToken,
|
|
wechatFetch,
|
|
memindLibRoot,
|
|
});
|
|
return {
|
|
content: applyWechatDraftArticleLayout(content, {
|
|
publicUrl,
|
|
...brandAssets,
|
|
}),
|
|
...brandAssets,
|
|
};
|
|
}
|