d8eaef3fbd
Add public-page WeChat draft button, publication standard conversion for poem/prose/generic pages, and quick push APIs. Default Studio release SSH targets to john@180.159.29.143 with 105 edge upstream via 10.10.0.2. Co-authored-by: Cursor <cursoragent@cursor.com>
1081 lines
38 KiB
JavaScript
1081 lines
38 KiB
JavaScript
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fetch as undiciFetch } from 'undici';
|
||
import sharp from 'sharp';
|
||
import {
|
||
buildPublicUrl,
|
||
PUBLISH_ROOT_DIR,
|
||
PUBLIC_ZONE_DIR,
|
||
resolvePublicBaseUrl,
|
||
} from './user-publish.mjs';
|
||
import { extractMindspaceCoverPath } from './wechat/verify/generated-thumbnail.mjs';
|
||
import { extractPageTitle } from './wechat/verify/share-preview-repair.mjs';
|
||
import { readWorkspacePublishHtml } from './mindspace-workspace-path.mjs';
|
||
import { resolvePageWorkspaceRelativePath } from './mindspace-workspace-relative-path.mjs';
|
||
import { fetchWechatMpAccessToken } from './mindspace-wechat-mp-config.mjs';
|
||
import {
|
||
addWechatDraftArticle,
|
||
buildDailyNewsWechatDraftArticleForPush,
|
||
convertNewsPageHtmlToWechatArticle,
|
||
isDailyNewsFormat,
|
||
uploadWechatArticleContentImage,
|
||
uploadWechatPermanentThumb,
|
||
} from './wechat-news-morning-draft.mjs';
|
||
import { finalizeWechatDraftArticleForPush } from './wechat-draft-article-layout.mjs';
|
||
import {
|
||
verifyWechatDraftCoverResolution,
|
||
verifyWechatDraftPublicationContent,
|
||
WECHAT_DRAFT_PUBLICATION_STANDARD_VERSION,
|
||
} from './wechat-draft-publication-standard.mjs';
|
||
|
||
export { WECHAT_DRAFT_PUBLICATION_STANDARD_VERSION };
|
||
|
||
const RUNS_TABLE = 'h5_user_wechat_page_draft_runs';
|
||
const DEFAULT_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/stable_token';
|
||
|
||
function escapeHtml(value) {
|
||
return String(value ?? '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
function extractMetaDescription(html) {
|
||
const match = String(html ?? '').match(
|
||
/<meta[^>]+name=["']description["'][^>]+content=["']([^"']*)["']/i,
|
||
);
|
||
return match?.[1]?.trim() ?? '';
|
||
}
|
||
|
||
function stripHtml(value) {
|
||
return String(value ?? '')
|
||
.replace(/<br\s*\/?>/gi, '\n')
|
||
.replace(/<[^>]+>/g, '')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
function inlineHtmlToWechatText(value) {
|
||
return String(value ?? '')
|
||
.replace(/<br\s*\/?>/gi, '\n')
|
||
.replace(/<em[^>]*>([\s\S]*?)<\/em>/gi, '<span style="color:#d98f4a;font-style:normal;">$1</span>')
|
||
.replace(/<strong[^>]*>([\s\S]*?)<\/strong>/gi, '<strong>$1</strong>')
|
||
.replace(/<b[^>]*>([\s\S]*?)<\/b>/gi, '<strong>$1</strong>')
|
||
.replace(/<[^>]+>/g, '')
|
||
.replace(/\n{3,}/g, '\n\n')
|
||
.trim();
|
||
}
|
||
|
||
function stripLegacyReadOriginalFooter(content) {
|
||
return String(content ?? '')
|
||
.replace(/<p[^>]*>\s*阅读原文[::][\s\S]*?<\/p>/gi, '')
|
||
.trim();
|
||
}
|
||
|
||
export function isNewsHotspotFormat(html) {
|
||
const source = String(html ?? '');
|
||
if (isDailyNewsFormat(source)) return false;
|
||
if (/news-hotspot|今日新闻热点/u.test(source)) return true;
|
||
if (!/class="card"/.test(source)) return false;
|
||
return /<h2[^>]*>/.test(source)
|
||
&& (/<span class="tag"/.test(source) || /<div class="k"/.test(source));
|
||
}
|
||
|
||
function normalizeImageRef(value) {
|
||
const ref = String(value ?? '').trim();
|
||
if (!ref || /^data:/i.test(ref)) return '';
|
||
return ref;
|
||
}
|
||
|
||
function resolveImageFilePath(imageRef, publishDir, htmlRelativePath = '') {
|
||
const ref = normalizeImageRef(imageRef);
|
||
if (!ref) return null;
|
||
if (/^https?:\/\//i.test(ref)) return ref;
|
||
const htmlDir = htmlRelativePath
|
||
? path.dirname(path.join(publishDir, htmlRelativePath))
|
||
: publishDir;
|
||
const publicDir = path.join(publishDir, PUBLIC_ZONE_DIR);
|
||
const candidates = [
|
||
path.resolve(htmlDir, ref.replace(/^\.\//, '')),
|
||
path.resolve(publicDir, ref.replace(/^\.\//, '')),
|
||
path.resolve(publishDir, ref.replace(/^\.\//, '')),
|
||
path.resolve(publishDir, ref.replace(/^\/+/, '')),
|
||
];
|
||
for (const candidate of candidates) {
|
||
if (/^https?:\/\//i.test(candidate)) return candidate;
|
||
if (fs.existsSync(candidate)) return candidate;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function normalizeWechatImageUrl(imageUrl) {
|
||
return String(imageUrl ?? '').trim().replace(/^http:\/\//i, 'https://');
|
||
}
|
||
|
||
function renderWechatImage(imageUrl, alt = '') {
|
||
const safeUrl = normalizeWechatImageUrl(imageUrl);
|
||
if (!safeUrl) return '';
|
||
return [
|
||
'<section style="margin:0 0 16px;text-align:center;">',
|
||
`<img src="${safeUrl}" alt="${escapeHtml(alt)}" style="width:100%;max-width:100%;display:block;margin:0 auto;border-radius:10px;" />`,
|
||
'</section>',
|
||
].join('');
|
||
}
|
||
|
||
function resolveSidecarThumbnailPaths(publishDir, htmlRelativePath) {
|
||
if (!publishDir || !htmlRelativePath) return [];
|
||
const htmlPath = path.join(publishDir, htmlRelativePath);
|
||
const pngPath = htmlPath.replace(/\.html$/i, '.thumbnail.png');
|
||
const svgPath = htmlPath.replace(/\.html$/i, '.thumbnail.svg');
|
||
if (fs.existsSync(pngPath)) return [pngPath];
|
||
if (fs.existsSync(svgPath)) return [svgPath];
|
||
return [];
|
||
}
|
||
|
||
function collectPageImageCandidatesFromHtml(html, { publishDir = '', htmlRelativePath = '' } = {}) {
|
||
const source = String(html ?? '');
|
||
const refs = [];
|
||
const coverPath = extractMindspaceCoverPath(source);
|
||
if (coverPath) refs.push(coverPath);
|
||
for (const match of source.matchAll(/background-image:\s*url\(["']?([^"')]+)["']?\)/gi)) {
|
||
refs.push(match[1]);
|
||
}
|
||
for (const match of source.matchAll(/<img[^>]+src=["']([^"']+)["']/gi)) {
|
||
refs.push(match[1]);
|
||
}
|
||
const seen = new Set();
|
||
const resolved = [];
|
||
for (const ref of refs) {
|
||
const normalized = normalizeImageRef(ref);
|
||
if (!normalized || seen.has(normalized)) continue;
|
||
seen.add(normalized);
|
||
resolved.push({
|
||
ref: normalized,
|
||
absPath: resolveImageFilePath(normalized, publishDir, htmlRelativePath),
|
||
});
|
||
}
|
||
return resolved;
|
||
}
|
||
|
||
export function collectPageImageCandidates(html, { publishDir = '', htmlRelativePath = '' } = {}) {
|
||
const resolved = collectPageImageCandidatesFromHtml(html, { publishDir, htmlRelativePath });
|
||
if (
|
||
htmlRelativePath
|
||
&& publishDir
|
||
&& !resolveHeroImagePath(html, { publishDir, htmlRelativePath })
|
||
) {
|
||
const seen = new Set(resolved.map((item) => item.ref));
|
||
for (const sidecarPath of resolveSidecarThumbnailPaths(publishDir, htmlRelativePath)) {
|
||
const ref = path.basename(sidecarPath);
|
||
if (seen.has(ref)) continue;
|
||
seen.add(ref);
|
||
resolved.push({ ref, absPath: sidecarPath });
|
||
}
|
||
}
|
||
return resolved;
|
||
}
|
||
|
||
function countBodyImages(html) {
|
||
return collectPageImageCandidates(html).length;
|
||
}
|
||
|
||
function extractBodyHtml(source) {
|
||
const match = String(source ?? '').match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
||
return match ? match[1] : String(source ?? '');
|
||
}
|
||
|
||
function stripMindspaceDeliveryChrome(source) {
|
||
let body = extractBodyHtml(source);
|
||
body = body.replace(/<style id="mindspace-public-share-style">[\s\S]*?<\/script>\s*/i, '');
|
||
body = body.replace(/<script[\s\S]*?<\/script>/gi, '');
|
||
body = body.replace(/<style[\s\S]*?<\/style>/gi, '');
|
||
body = body.replace(/<link[^>]*>/gi, '');
|
||
body = body.replace(/<div[^>]*data-mindspace-wechat-dialog[\s\S]*?<\/div>\s*/gi, '');
|
||
body = body.replace(/<div[^>]*data-mindspace-public-share[\s\S]*?<\/div>\s*/gi, '');
|
||
body = body.replace(/<small[^>]*aria-live="polite"[^>]*>[\s\S]*?<\/small>/gi, '');
|
||
return body.trim();
|
||
}
|
||
|
||
function isMindspaceBoilerplateText(text) {
|
||
const normalized = stripHtml(text);
|
||
if (!normalized) return true;
|
||
if (/^(TKMind · 智趣|AI 配图服务暂时不可用)$/u.test(normalized)) return true;
|
||
if (/^(取消|确认推送|知道了|关闭|保存长图|公开分享|公众号|发布 Plaza)$/u.test(normalized)) return true;
|
||
return false;
|
||
}
|
||
|
||
function resolveMindspaceMainContentHtml(body) {
|
||
const main = body.match(/<main[^>]*>([\s\S]*?)<\/main>/i)?.[1];
|
||
if (main) return main;
|
||
const article = body.match(/<article[^>]*>([\s\S]*?)<\/article>/i)?.[1];
|
||
if (article) return article;
|
||
const page = body.match(/<div class="page"[^>]*>([\s\S]*?)<\/div>/i)?.[1];
|
||
if (page) return page;
|
||
const content = body.match(/<div class="content"[^>]*>([\s\S]*?)<\/div>/i)?.[1];
|
||
if (content) return content;
|
||
return body;
|
||
}
|
||
|
||
function appendParagraphsFromHtml(html, blocks, seen) {
|
||
for (const match of String(html ?? '').matchAll(/<p[^>]*>([\s\S]*?)<\/p>/gi)) {
|
||
const text = stripHtml(match[1]);
|
||
if (!text || isMindspaceBoilerplateText(text) || seen.has(text)) continue;
|
||
seen.add(text);
|
||
blocks.push(
|
||
`<p style="margin:0 0 16px;font-size:16px;line-height:1.9;color:#333;text-align:justify;">${escapeHtml(text)}</p>`,
|
||
);
|
||
}
|
||
}
|
||
|
||
function appendListsFromHtml(html, blocks, seen) {
|
||
for (const match of String(html ?? '').matchAll(/<li[^>]*>([\s\S]*?)<\/li>/gi)) {
|
||
const text = stripHtml(match[1]);
|
||
if (!text || isMindspaceBoilerplateText(text) || seen.has(text)) continue;
|
||
seen.add(text);
|
||
blocks.push(
|
||
`<p style="margin:0 0 10px;font-size:15px;line-height:1.8;color:#444;padding-left:12px;">• ${escapeHtml(text)}</p>`,
|
||
);
|
||
}
|
||
}
|
||
|
||
function appendBlockquotesFromHtml(html, blocks, seen) {
|
||
for (const match of String(html ?? '').matchAll(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi)) {
|
||
const text = stripHtml(match[1]);
|
||
if (!text || isMindspaceBoilerplateText(text) || seen.has(text)) continue;
|
||
seen.add(text);
|
||
blocks.push(
|
||
`<blockquote style="margin:0 0 16px;padding:12px 16px;border-left:3px solid #d98f4a;background:#faf7f1;color:#2a1a12;font-size:15px;line-height:1.8;">${escapeHtml(text)}</blockquote>`,
|
||
);
|
||
}
|
||
}
|
||
|
||
function appendFallbackMindspaceArticleSections(sections, source, { skipTexts = new Set() } = {}) {
|
||
let body = stripMindspaceDeliveryChrome(source);
|
||
body = body
|
||
.replace(/<header class="hero"[^>]*>[\s\S]*?<\/header>/gi, '')
|
||
.replace(/<div class="hero"[^>]*>[\s\S]*?<\/div>/gi, '')
|
||
.replace(/<div class="title-block"[^>]*>[\s\S]*?<\/div>/gi, '')
|
||
.replace(/<div class="divider"[^>]*>\s*<\/div>/gi, '')
|
||
.replace(/<div class="tags"[^>]*>[\s\S]*?<\/div>/gi, '')
|
||
.replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, '');
|
||
|
||
const contentHtml = resolveMindspaceMainContentHtml(body);
|
||
const seen = new Set([...skipTexts].map((text) => stripHtml(text)).filter(Boolean));
|
||
const blocks = [];
|
||
|
||
for (const match of contentHtml.matchAll(/<h2[^>]*>([\s\S]*?)<\/h2>([\s\S]*?)(?=<h2|<h3|$)/gi)) {
|
||
const heading = stripHtml(match[1]);
|
||
if (!heading || seen.has(heading)) continue;
|
||
seen.add(heading);
|
||
blocks.push(
|
||
`<p style="margin:0 0 10px;font-size:17px;font-weight:700;color:#2a1a12;">${escapeHtml(heading)}</p>`,
|
||
);
|
||
appendParagraphsFromHtml(match[2], blocks, seen);
|
||
appendListsFromHtml(match[2], blocks, seen);
|
||
}
|
||
|
||
for (const match of contentHtml.matchAll(/<h3[^>]*>([\s\S]*?)<\/h3>([\s\S]*?)(?=<h[23]|$)/gi)) {
|
||
const heading = stripHtml(match[1]);
|
||
if (!heading || seen.has(heading)) continue;
|
||
seen.add(heading);
|
||
blocks.push(
|
||
`<p style="margin:0 0 8px;font-size:15px;font-weight:700;color:#444;">${escapeHtml(heading)}</p>`,
|
||
);
|
||
appendParagraphsFromHtml(match[2], blocks, seen);
|
||
appendListsFromHtml(match[2], blocks, seen);
|
||
}
|
||
|
||
appendParagraphsFromHtml(contentHtml, blocks, seen);
|
||
appendListsFromHtml(contentHtml, blocks, seen);
|
||
appendBlockquotesFromHtml(contentHtml, blocks, seen);
|
||
|
||
if (blocks.length === 0) return;
|
||
sections.push('<section style="margin:0 0 20px;">');
|
||
sections.push(...blocks);
|
||
sections.push('</section>');
|
||
}
|
||
|
||
export function convertRichMindSpacePageHtmlToWechatArticle(
|
||
html,
|
||
{ pageTitle = '', pageSummary = '', imageUrlMap = new Map() } = {},
|
||
) {
|
||
const source = String(html ?? '');
|
||
const title = (extractPageTitle(source) || pageTitle || 'TKMind 页面').slice(0, 64);
|
||
const digest = (extractMetaDescription(source) || pageSummary || title).slice(0, 120);
|
||
const sections = [];
|
||
|
||
const heroTitle = stripHtml(
|
||
source.match(/<header class="hero"[^>]*>[\s\S]*?<h1[^>]*>([\s\S]*?)<\/h1>/i)?.[1]
|
||
?? source.match(/<article[^>]*>[\s\S]*?<h1[^>]*>([\s\S]*?)<\/h1>/i)?.[1]
|
||
?? '',
|
||
);
|
||
const heroLead = inlineHtmlToWechatText(
|
||
source.match(/<header class="hero"[^>]*>[\s\S]*?<p class="lead"[^>]*>([\s\S]*?)<\/p>/i)?.[1]
|
||
?? source.match(/<p class="pitch-sub"[^>]*>([\s\S]*?)<\/p>/i)?.[1]
|
||
?? '',
|
||
);
|
||
const titleBlockTitle = stripHtml(
|
||
source.match(/<div class="title-block"[^>]*>[\s\S]*?<h1[^>]*class="title"[^>]*>([\s\S]*?)<\/h1>/i)?.[1]
|
||
?? source.match(/<h1[^>]*class="title"[^>]*>([\s\S]*?)<\/h1>/i)?.[1]
|
||
?? '',
|
||
);
|
||
const titleBlockSubtitle = stripHtml(
|
||
source.match(/<div class="title-block"[^>]*>[\s\S]*?<p[^>]*class="subtitle"[^>]*>([\s\S]*?)<\/p>/i)?.[1] ?? '',
|
||
);
|
||
const displayTitle = heroTitle || titleBlockTitle || title;
|
||
const displayLead = heroLead || titleBlockSubtitle;
|
||
const coverRef = extractMindspaceCoverPath(source);
|
||
const sidecarThumbRef = [...imageUrlMap.keys()].find((ref) => /\.thumbnail\.(png|svg)$/i.test(ref)) ?? '';
|
||
const heroImageUrl = normalizeWechatImageUrl(
|
||
imageUrlMap.get(coverRef)
|
||
?? (sidecarThumbRef ? imageUrlMap.get(sidecarThumbRef) : undefined)
|
||
?? imageUrlMap.values().next().value
|
||
?? '',
|
||
);
|
||
|
||
if (heroImageUrl) {
|
||
sections.push(renderWechatImage(heroImageUrl, displayTitle));
|
||
}
|
||
|
||
sections.push([
|
||
'<section style="text-align:center;padding:20px 14px 16px;background:linear-gradient(135deg,#2a1a12,#4a3020);color:#fff;border-radius:12px;margin:0 0 16px;">',
|
||
`<p style="margin:0 0 8px;font-size:22px;font-weight:700;line-height:1.35;">${escapeHtml(displayTitle)}</p>`,
|
||
displayLead
|
||
? `<p style="margin:0;font-size:14px;opacity:.88;line-height:1.8;">${displayLead.replace(/\n/g, '<br/>')}</p>`
|
||
: '',
|
||
'</section>',
|
||
].filter(Boolean).join(''));
|
||
|
||
const summary = stripHtml(pageSummary);
|
||
if (summary && summary !== stripHtml(displayLead) && summary !== displayTitle) {
|
||
sections.push(
|
||
`<p style="margin:0 0 16px;font-size:14px;color:#666;line-height:1.8;">${escapeHtml(summary)}</p>`,
|
||
);
|
||
}
|
||
|
||
const proseBlocks = [...source.matchAll(/<div class="prose"[^>]*>([\s\S]*?)<\/div>/gi)];
|
||
if (proseBlocks.length > 0) {
|
||
sections.push('<section style="margin:0 0 20px;">');
|
||
for (const [, block] of proseBlocks) {
|
||
for (const [, paragraph] of block.matchAll(/<p[^>]*>([\s\S]*?)<\/p>/gi)) {
|
||
const text = stripHtml(paragraph);
|
||
if (!text) continue;
|
||
sections.push(
|
||
`<p style="margin:0 0 16px;font-size:16px;line-height:2;color:#5c3a1e;text-align:justify;">${escapeHtml(text)}</p>`,
|
||
);
|
||
}
|
||
}
|
||
sections.push('</section>');
|
||
}
|
||
|
||
const tagBlocks = [...source.matchAll(/<div class="tags"[^>]*>([\s\S]*?)<\/div>/gi)];
|
||
if (tagBlocks.length > 0) {
|
||
const tags = tagBlocks.flatMap(([, block]) =>
|
||
[...block.matchAll(/<span class="tag"[^>]*>([\s\S]*?)<\/span>/gi)]
|
||
.map(([, tag]) => stripHtml(tag))
|
||
.filter(Boolean),
|
||
);
|
||
if (tags.length > 0) {
|
||
sections.push(
|
||
`<p style="margin:0 0 20px;font-size:13px;line-height:1.8;color:#8b6b4a;text-align:center;">${tags.map((tag) => escapeHtml(tag)).join(' · ')}</p>`,
|
||
);
|
||
}
|
||
}
|
||
|
||
const stanzaBlocks = [...source.matchAll(/<div class="stanza"[^>]*>([\s\S]*?)<\/div>/gi)];
|
||
if (stanzaBlocks.length > 0) {
|
||
sections.push('<section style="margin:0 0 20px;">');
|
||
sections.push('<p style="margin:0 0 12px;font-size:13px;font-weight:600;color:#888;letter-spacing:.2em;">正文</p>');
|
||
for (const [, block] of stanzaBlocks) {
|
||
const num = stripHtml(block.match(/<span class="num"[^>]*>([\s\S]*?)<\/span>/i)?.[1] ?? '');
|
||
const poem = inlineHtmlToWechatText(block.match(/<p[^>]*>([\s\S]*?)<\/p>/i)?.[1] ?? '');
|
||
if (!poem) continue;
|
||
sections.push([
|
||
'<div style="margin:0 0 18px;padding:14px 12px;background:#faf7f1;border-radius:10px;">',
|
||
num ? `<p style="margin:0 0 8px;font-size:11px;color:#d98f4a;letter-spacing:.24em;">${escapeHtml(num)}</p>` : '',
|
||
`<p style="margin:0;font-size:16px;line-height:2;color:#2a1a12;white-space:pre-wrap;">${poem.replace(/\n/g, '<br/>')}</p>`,
|
||
'</div>',
|
||
].filter(Boolean).join(''));
|
||
}
|
||
sections.push('</section>');
|
||
}
|
||
|
||
const finalText = inlineHtmlToWechatText(
|
||
source.match(/<div class="final"[^>]*>([\s\S]*?)<\/div>/i)?.[1] ?? '',
|
||
);
|
||
if (finalText) {
|
||
sections.push(
|
||
`<p style="margin:0 0 20px;font-size:15px;line-height:1.9;color:#666;text-align:center;">${finalText.replace(/\n/g, '<br/>')}</p>`,
|
||
);
|
||
}
|
||
|
||
const cardBlocks = [...source.matchAll(/<div class="card"[^>]*>([\s\S]*?)<\/div>/gi)];
|
||
const imageryCards = cardBlocks.filter(([, block]) =>
|
||
/<span class="glyph"/.test(block) || /<h3[^>]*>/.test(block),
|
||
);
|
||
if (imageryCards.length > 0) {
|
||
sections.push('<section style="margin:0 0 20px;">');
|
||
sections.push('<p style="margin:0 0 12px;font-size:13px;font-weight:600;color:#888;letter-spacing:.2em;">意象</p>');
|
||
for (const [, block] of imageryCards) {
|
||
const glyph = stripHtml(block.match(/<span class="glyph"[^>]*>([\s\S]*?)<\/span>/i)?.[1] ?? '');
|
||
const cardTitle = stripHtml(block.match(/<h3[^>]*>([\s\S]*?)<\/h3>/i)?.[1] ?? '');
|
||
const cardBody = stripHtml(block.match(/<p[^>]*>([\s\S]*?)<\/p>/i)?.[1] ?? '');
|
||
sections.push([
|
||
'<div style="margin:0 0 14px;padding:16px;background:#fff;border:1px solid #eee;border-radius:10px;">',
|
||
glyph ? `<p style="margin:0 0 8px;font-size:24px;color:#d98f4a;">${escapeHtml(glyph)}</p>` : '',
|
||
cardTitle ? `<p style="margin:0 0 8px;font-size:15px;font-weight:700;color:#2a1a12;">${escapeHtml(cardTitle)}</p>` : '',
|
||
cardBody ? `<p style="margin:0;font-size:14px;line-height:1.8;color:#666;">${escapeHtml(cardBody)}</p>` : '',
|
||
'</div>',
|
||
].filter(Boolean).join(''));
|
||
}
|
||
sections.push('</section>');
|
||
}
|
||
|
||
const noteBlock = source.match(/<section class="note"[^>]*>([\s\S]*?)<\/section>/i)?.[1] ?? '';
|
||
if (noteBlock) {
|
||
sections.push('<section style="margin:0 0 8px;">');
|
||
sections.push('<p style="margin:0 0 12px;font-size:13px;font-weight:600;color:#888;letter-spacing:.2em;">创作手记</p>');
|
||
const noteTitle = stripHtml(noteBlock.match(/<h2[^>]*>([\s\S]*?)<\/h2>/i)?.[1] ?? '');
|
||
if (noteTitle) {
|
||
sections.push(`<p style="margin:0 0 12px;font-size:16px;font-weight:700;color:#2a1a12;">${escapeHtml(noteTitle)}</p>`);
|
||
}
|
||
for (const [, paragraph] of noteBlock.matchAll(/<p[^>]*>([\s\S]*?)<\/p>/gi)) {
|
||
const text = stripHtml(paragraph);
|
||
if (!text) continue;
|
||
sections.push(`<p style="margin:0 0 12px;font-size:14px;line-height:1.9;color:#555;">${escapeHtml(text)}</p>`);
|
||
}
|
||
const quote = stripHtml(noteBlock.match(/<div class="quote"[^>]*>([\s\S]*?)<\/div>/i)?.[1] ?? '');
|
||
if (quote) {
|
||
sections.push(
|
||
`<blockquote style="margin:0;padding:12px 16px;border-left:3px solid #d98f4a;background:#faf7f1;color:#2a1a12;font-size:15px;line-height:1.8;">${escapeHtml(quote)}</blockquote>`,
|
||
);
|
||
}
|
||
sections.push('</section>');
|
||
}
|
||
|
||
const specializedContentExtracted =
|
||
proseBlocks.length > 0
|
||
|| stanzaBlocks.length > 0
|
||
|| Boolean(finalText)
|
||
|| imageryCards.length > 0
|
||
|| Boolean(noteBlock);
|
||
if (!specializedContentExtracted) {
|
||
appendFallbackMindspaceArticleSections(sections, source, {
|
||
skipTexts: new Set([displayTitle, displayLead, summary].filter(Boolean)),
|
||
});
|
||
}
|
||
|
||
for (const match of source.matchAll(/<img[^>]+src=["']([^"']+)["']/gi)) {
|
||
const ref = normalizeImageRef(match[1]);
|
||
const uploaded = normalizeWechatImageUrl(imageUrlMap.get(ref));
|
||
if (uploaded && uploaded !== heroImageUrl) {
|
||
sections.push(renderWechatImage(uploaded));
|
||
}
|
||
}
|
||
|
||
return {
|
||
title: displayTitle.slice(0, 64),
|
||
digest,
|
||
content: sections.join('\n'),
|
||
imageCount: imageUrlMap.size,
|
||
};
|
||
}
|
||
|
||
export function resolveWechatDraftProfile(html) {
|
||
const source = String(html ?? '');
|
||
if (isDailyNewsFormat(source)) return 'daily-news';
|
||
if (isNewsHotspotFormat(source)) return 'news-hotspot';
|
||
if (/name=["']mindspace-cover["']/i.test(source)) return 'mindspace-page';
|
||
if (/data-mindspace-page-tag=["']platform-brand["']/i.test(source)) return 'mindspace-page';
|
||
if (/class="stanza"/.test(source) || /class="hero"/.test(source)) return 'mindspace-page';
|
||
return 'generic';
|
||
}
|
||
|
||
export function validatePageForWechatDraft(
|
||
html,
|
||
{ pageTitle = '', pageSummary = '', publicUrl = '' } = {},
|
||
) {
|
||
const source = String(html ?? '').trim();
|
||
const profile = resolveWechatDraftProfile(source);
|
||
const warnings = [];
|
||
const blockers = [];
|
||
|
||
if (!source) {
|
||
blockers.push('页面 HTML 为空,无法推送草稿。');
|
||
}
|
||
if (!stripHtml(pageTitle || extractPageTitle(source))) {
|
||
blockers.push('缺少页面标题。');
|
||
}
|
||
if (!publicUrl) {
|
||
warnings.push('页面尚未公开或缺少阅读原文链接,草稿中将省略「阅读原文」。');
|
||
}
|
||
if (countBodyImages(source) === 0 && profile === 'mindspace-page') {
|
||
warnings.push('页面未检测到可上传图片,草稿将以文字为主。');
|
||
}
|
||
if (profile === 'mindspace-page') {
|
||
warnings.push('MindSpace 页面结构转换后视觉可能与原页略有差异。');
|
||
}
|
||
|
||
return {
|
||
ok: blockers.length === 0,
|
||
profile,
|
||
warnings,
|
||
blockers,
|
||
};
|
||
}
|
||
|
||
async function uploadPageImagesForWechat(
|
||
accessToken,
|
||
html,
|
||
{ publishDir = '', htmlRelativePath = '', wechatFetch = undiciFetch } = {},
|
||
) {
|
||
const imageUrlMap = new Map();
|
||
const candidates = collectPageImageCandidates(html, { publishDir, htmlRelativePath });
|
||
for (const { ref, absPath } of candidates) {
|
||
if (!absPath || imageUrlMap.has(ref)) continue;
|
||
if (/^https?:\/\//i.test(absPath)) {
|
||
imageUrlMap.set(ref, absPath);
|
||
continue;
|
||
}
|
||
let buffer = fs.readFileSync(absPath);
|
||
if (/\.(webp|svg)$/i.test(absPath)) {
|
||
buffer = await sharp(buffer).png().toBuffer();
|
||
}
|
||
const uploaded = normalizeWechatImageUrl(
|
||
await uploadWechatArticleContentImage(
|
||
accessToken,
|
||
buffer,
|
||
{ wechatFetch, filename: `${path.basename(absPath).replace(/\.(webp|svg)$/i, '')}.png` },
|
||
),
|
||
);
|
||
imageUrlMap.set(ref, uploaded);
|
||
}
|
||
return imageUrlMap;
|
||
}
|
||
|
||
export async function buildMindSpacePageWechatDraftArticleForPush({
|
||
html,
|
||
publicUrl = '',
|
||
pageTitle = '',
|
||
pageSummary = '',
|
||
accessToken = null,
|
||
publishDir = '',
|
||
htmlRelativePath = '',
|
||
wechatFetch = undiciFetch,
|
||
memindLibRoot = process.cwd(),
|
||
} = {}) {
|
||
const imageUrlMap = accessToken
|
||
? await uploadPageImagesForWechat(accessToken, html, { publishDir, htmlRelativePath, wechatFetch })
|
||
: new Map();
|
||
const profile = resolveWechatDraftProfile(html);
|
||
let article;
|
||
if (profile === 'news-hotspot') {
|
||
article = convertNewsPageHtmlToWechatArticle(html, { publicUrl });
|
||
article.content = stripLegacyReadOriginalFooter(article.content);
|
||
} else if (profile === 'mindspace-page') {
|
||
article = convertRichMindSpacePageHtmlToWechatArticle(html, {
|
||
pageTitle,
|
||
pageSummary,
|
||
imageUrlMap,
|
||
});
|
||
} else {
|
||
article = convertGenericPageHtmlToWechatArticle(html, {
|
||
pageTitle,
|
||
pageSummary,
|
||
});
|
||
article.content = stripLegacyReadOriginalFooter(article.content);
|
||
}
|
||
const finalized = await finalizeWechatDraftArticleForPush({
|
||
content: article.content,
|
||
publicUrl,
|
||
accessToken,
|
||
wechatFetch,
|
||
memindLibRoot,
|
||
});
|
||
return {
|
||
...article,
|
||
content: finalized.content,
|
||
contentSourceUrl: publicUrl || undefined,
|
||
uploadedImageCount: imageUrlMap.size,
|
||
};
|
||
}
|
||
|
||
export async function buildWechatDraftPublicationBundleForPush({
|
||
html,
|
||
publicUrl = '',
|
||
pageTitle = '',
|
||
pageSummary = '',
|
||
accessToken = null,
|
||
publishDir = '',
|
||
htmlRelativePath = '',
|
||
h5Root = process.cwd(),
|
||
userId = '',
|
||
workspaceRelativePath = htmlRelativePath,
|
||
wechatFetch = undiciFetch,
|
||
memindLibRoot = process.cwd(),
|
||
} = {}) {
|
||
const profile = resolveWechatDraftProfile(html);
|
||
const article = isDailyNewsFormat(html)
|
||
? await buildDailyNewsWechatDraftArticleForPush({
|
||
html,
|
||
publicUrl,
|
||
accessToken,
|
||
wechatFetch,
|
||
memindLibRoot,
|
||
publishDir,
|
||
})
|
||
: await buildMindSpacePageWechatDraftArticleForPush({
|
||
html,
|
||
publicUrl,
|
||
pageTitle,
|
||
pageSummary,
|
||
accessToken,
|
||
publishDir,
|
||
htmlRelativePath,
|
||
wechatFetch,
|
||
memindLibRoot,
|
||
});
|
||
const thumbResolved = resolveWechatDraftThumbPath(html, {
|
||
h5Root,
|
||
userId,
|
||
workspaceRelativePath,
|
||
publishDir,
|
||
});
|
||
const contentCheck = verifyWechatDraftPublicationContent(article.content, { publicUrl });
|
||
const coverCheck = verifyWechatDraftCoverResolution(thumbResolved);
|
||
if (!contentCheck.ok || !coverCheck.ok) {
|
||
throw Object.assign(
|
||
new Error(`草稿不符合推送标准 ${WECHAT_DRAFT_PUBLICATION_STANDARD_VERSION}`),
|
||
{
|
||
code: 'wechat_draft_standard_violation',
|
||
contentIssues: contentCheck.issues,
|
||
coverIssues: coverCheck.issues,
|
||
},
|
||
);
|
||
}
|
||
return {
|
||
article,
|
||
thumbResolved,
|
||
profile,
|
||
standardVersion: WECHAT_DRAFT_PUBLICATION_STANDARD_VERSION,
|
||
};
|
||
}
|
||
|
||
export function convertGenericPageHtmlToWechatArticle(
|
||
html,
|
||
{ publicUrl = '', pageTitle = '', pageSummary = '' } = {},
|
||
) {
|
||
const title = (extractPageTitle(html) || pageTitle || 'TKMind 页面').slice(0, 64);
|
||
const digest = (extractMetaDescription(html) || pageSummary || title).slice(0, 120);
|
||
const sections = [];
|
||
const summary = stripHtml(pageSummary);
|
||
if (summary) {
|
||
sections.push(
|
||
`<p style="margin:0 0 16px;font-size:14px;color:#666;line-height:1.8;">${escapeHtml(summary)}</p>`,
|
||
);
|
||
}
|
||
appendFallbackMindspaceArticleSections(sections, html, {
|
||
skipTexts: new Set([title, summary].filter(Boolean)),
|
||
});
|
||
if (sections.length === (summary ? 1 : 0)) {
|
||
sections.push(
|
||
`<p style="margin:0;font-size:15px;line-height:1.8;color:#333;">${escapeHtml(title)}</p>`,
|
||
);
|
||
}
|
||
return {
|
||
title,
|
||
digest,
|
||
content: sections.join('\n').slice(0, 20000),
|
||
contentSourceUrl: publicUrl || undefined,
|
||
};
|
||
}
|
||
|
||
export function convertPageHtmlToWechatArticle(
|
||
html,
|
||
{ publicUrl = '', pageTitle = '', pageSummary = '' } = {},
|
||
) {
|
||
if (isDailyNewsFormat(html)) {
|
||
return convertNewsPageHtmlToWechatArticle(html, { publicUrl });
|
||
}
|
||
if (isNewsHotspotFormat(html)) {
|
||
return convertNewsPageHtmlToWechatArticle(html, { publicUrl });
|
||
}
|
||
if (resolveWechatDraftProfile(html) === 'mindspace-page') {
|
||
return convertRichMindSpacePageHtmlToWechatArticle(html, { pageTitle, pageSummary });
|
||
}
|
||
return convertGenericPageHtmlToWechatArticle(html, { publicUrl, pageTitle, pageSummary });
|
||
}
|
||
|
||
async function ensureRunsTable(pool) {
|
||
await pool.query(`
|
||
CREATE TABLE IF NOT EXISTS ${RUNS_TABLE} (
|
||
id CHAR(36) PRIMARY KEY,
|
||
user_id CHAR(36) NOT NULL,
|
||
page_id CHAR(36) NOT NULL,
|
||
status VARCHAR(16) NOT NULL,
|
||
page_title VARCHAR(255) NULL,
|
||
page_url VARCHAR(512) NULL,
|
||
draft_media_id VARCHAR(128) NULL,
|
||
error_message TEXT NULL,
|
||
triggered_by CHAR(36) NULL,
|
||
created_at BIGINT NOT NULL,
|
||
KEY idx_wechat_page_draft_user (user_id, created_at),
|
||
KEY idx_wechat_page_draft_page (page_id, created_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||
`);
|
||
}
|
||
|
||
function cryptoRandomId() {
|
||
return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||
}
|
||
|
||
function resolveThumbPath(h5Root, userId, workspaceRelativePath) {
|
||
if (!h5Root || !userId || !workspaceRelativePath) return null;
|
||
const parts = String(workspaceRelativePath)
|
||
.replace(/^\/+/, '')
|
||
.split('/')
|
||
.filter(Boolean);
|
||
const baseParts = parts[0]?.toLowerCase() === PUBLIC_ZONE_DIR
|
||
? parts
|
||
: [PUBLIC_ZONE_DIR, ...parts];
|
||
const htmlPath = path.join(
|
||
h5Root,
|
||
PUBLISH_ROOT_DIR,
|
||
String(userId),
|
||
...baseParts,
|
||
);
|
||
const pngPath = htmlPath.replace(/\.html$/i, '.thumbnail.png');
|
||
if (fs.existsSync(pngPath)) return pngPath;
|
||
const svgPath = htmlPath.replace(/\.html$/i, '.thumbnail.svg');
|
||
if (fs.existsSync(svgPath)) return svgPath;
|
||
return null;
|
||
}
|
||
|
||
function isWechatDraftDecorativeImageRef(ref = '') {
|
||
return /mp-follow-qrcode|tkmind-icon|\/brand\//i.test(String(ref ?? ''));
|
||
}
|
||
|
||
export function resolveHeroImagePath(html, { publishDir = '', htmlRelativePath = '' } = {}) {
|
||
const coverRef = extractMindspaceCoverPath(html);
|
||
if (coverRef && !/\.svg$/i.test(coverRef)) {
|
||
const coverPath = resolveImageFilePath(coverRef, publishDir, htmlRelativePath);
|
||
if (coverPath && !/\.svg$/i.test(coverPath)) {
|
||
return coverPath;
|
||
}
|
||
}
|
||
const candidates = collectPageImageCandidatesFromHtml(html, { publishDir, htmlRelativePath });
|
||
for (const { ref, absPath } of candidates) {
|
||
if (!absPath || /\.svg$/i.test(absPath) || isWechatDraftDecorativeImageRef(ref)) {
|
||
continue;
|
||
}
|
||
return absPath;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function resolveRasterThumbSidecarPath(h5Root, userId, workspaceRelativePath) {
|
||
const sidecarPath = resolveThumbPath(h5Root, userId, workspaceRelativePath);
|
||
if (sidecarPath && /\.(png|svg)$/i.test(sidecarPath)) {
|
||
return sidecarPath;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export function resolveWechatDraftThumbPath(
|
||
html,
|
||
{ h5Root, userId, workspaceRelativePath, publishDir } = {},
|
||
) {
|
||
const heroPath = resolveHeroImagePath(html, {
|
||
publishDir,
|
||
htmlRelativePath: workspaceRelativePath,
|
||
});
|
||
if (heroPath) {
|
||
return { source: 'hero', path: heroPath };
|
||
}
|
||
const sidecarPath = resolveRasterThumbSidecarPath(h5Root, userId, workspaceRelativePath);
|
||
if (sidecarPath) {
|
||
return { source: 'thumbnail-sidecar', path: sidecarPath };
|
||
}
|
||
return { source: 'placeholder', path: null };
|
||
}
|
||
|
||
async function loadThumbBuffer(thumbPath, { wechatFetch = undiciFetch } = {}) {
|
||
if (!thumbPath) {
|
||
return sharp(
|
||
Buffer.from(
|
||
'<svg xmlns="http://www.w3.org/2000/svg" width="900" height="900"><rect width="900" height="900" fill="#1a1a2e"/><text x="50%" y="50%" fill="#fff" font-size="42" text-anchor="middle" dominant-baseline="middle">TKMind</text></svg>',
|
||
),
|
||
)
|
||
.png()
|
||
.toBuffer();
|
||
}
|
||
|
||
let raw;
|
||
if (/^https?:\/\//i.test(thumbPath)) {
|
||
const response = await wechatFetch(thumbPath);
|
||
if (!response.ok) {
|
||
throw new Error(`封面图片下载失败: ${thumbPath}`);
|
||
}
|
||
raw = Buffer.from(await response.arrayBuffer());
|
||
} else {
|
||
raw = fs.readFileSync(thumbPath);
|
||
}
|
||
|
||
return sharp(raw).resize(900, 900, { fit: 'cover' }).png().toBuffer();
|
||
}
|
||
|
||
export function resolvePagePublicUrl(
|
||
page,
|
||
{
|
||
userId = '',
|
||
workspaceRelativePath = '',
|
||
publication = null,
|
||
publicBaseUrl = '',
|
||
} = {},
|
||
) {
|
||
const resolved = String(
|
||
publication?.publicUrl
|
||
?? publication?.public_url
|
||
?? page?.publication?.publicUrl
|
||
?? page?.publicationUrl
|
||
?? page?.workspacePublicUrl
|
||
?? '',
|
||
).trim();
|
||
if (resolved) return resolved;
|
||
const relativePath = String(
|
||
workspaceRelativePath
|
||
?? page?.workspaceRelativePath
|
||
?? resolvePageWorkspaceRelativePath(page)
|
||
?? '',
|
||
).trim();
|
||
if (relativePath && userId && publicBaseUrl) {
|
||
return buildPublicUrl(publicBaseUrl, userId, relativePath);
|
||
}
|
||
return '';
|
||
}
|
||
|
||
export function createMindSpaceWechatPageDraftService(
|
||
pool,
|
||
{
|
||
getMindSpacePages = () => null,
|
||
getMindSpacePublications = () => null,
|
||
getWechatMpConfig = () => null,
|
||
h5Root = process.cwd(),
|
||
memindLibRoot = h5Root,
|
||
env = process.env,
|
||
tokenUrl = env.H5_WECHAT_MP_TOKEN_URL?.trim() || DEFAULT_TOKEN_URL,
|
||
wechatFetch = undiciFetch,
|
||
} = {},
|
||
) {
|
||
let ensurePromise = null;
|
||
const accessTokenCache = new Map();
|
||
|
||
async function ensureReady() {
|
||
if (!ensurePromise) {
|
||
ensurePromise = ensureRunsTable(pool);
|
||
}
|
||
await ensurePromise;
|
||
}
|
||
|
||
async function getAccessToken(userId) {
|
||
const configService = getWechatMpConfig?.();
|
||
if (!configService) throw new Error('公众号配置服务未启用');
|
||
const credentials = await configService.getCredentials(userId);
|
||
if (!credentials?.appId || !credentials?.appSecret) {
|
||
throw Object.assign(new Error('请先在 M 配置中填写公众号 AppID 和 AppSecret'), {
|
||
code: 'wechat_mp_not_configured',
|
||
});
|
||
}
|
||
const cacheKey = `${userId}:${credentials.appId}`;
|
||
const cached = accessTokenCache.get(cacheKey);
|
||
if (cached && cached.expiresAt > Date.now() + 60_000) {
|
||
return { accessToken: cached.token, credentials };
|
||
}
|
||
const token = await fetchWechatMpAccessToken(credentials, { tokenUrl, wechatFetch });
|
||
accessTokenCache.set(cacheKey, {
|
||
token: token.accessToken,
|
||
expiresAt: Date.now() + token.expiresIn * 1000,
|
||
});
|
||
return { accessToken: token.accessToken, credentials };
|
||
}
|
||
|
||
async function recordRun(payload) {
|
||
await ensureReady();
|
||
const id = cryptoRandomId();
|
||
const now = Date.now();
|
||
await pool.query(
|
||
`INSERT INTO ${RUNS_TABLE}
|
||
(id, user_id, page_id, status, page_title, page_url, draft_media_id, error_message, triggered_by, created_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
[
|
||
id,
|
||
payload.userId,
|
||
payload.pageId,
|
||
payload.status,
|
||
payload.pageTitle ?? null,
|
||
payload.pageUrl ?? null,
|
||
payload.draftMediaId ?? null,
|
||
payload.errorMessage ?? null,
|
||
payload.triggeredBy ?? null,
|
||
now,
|
||
],
|
||
);
|
||
return {
|
||
id,
|
||
status: payload.status,
|
||
pageId: payload.pageId,
|
||
pageTitle: payload.pageTitle ?? null,
|
||
pageUrl: payload.pageUrl ?? null,
|
||
draftMediaId: payload.draftMediaId ?? null,
|
||
errorMessage: payload.errorMessage ?? null,
|
||
triggeredBy: payload.triggeredBy ?? null,
|
||
createdAt: now,
|
||
};
|
||
}
|
||
|
||
async function resolvePageHtml(userId, pageId) {
|
||
const pages = getMindSpacePages?.();
|
||
if (!pages?.renderPreview || !pages?.getPage) {
|
||
throw new Error('MindSpace 页面服务未启用');
|
||
}
|
||
const page = await pages.getPage(userId, pageId);
|
||
const { html } = await pages.renderPreview(userId, pageId);
|
||
const workspaceRelativePath = resolvePageWorkspaceRelativePath(page)
|
||
?? page.workspaceRelativePath
|
||
?? '';
|
||
const publications = getMindSpacePublications?.();
|
||
const publication = publications?.getCurrent
|
||
? await publications.getCurrent(userId, pageId).catch(() => null)
|
||
: null;
|
||
const publicBaseUrl = resolvePublicBaseUrl(env);
|
||
const publicUrl = resolvePagePublicUrl(page, {
|
||
userId,
|
||
workspaceRelativePath,
|
||
publication,
|
||
publicBaseUrl,
|
||
});
|
||
let sourceHtml = html;
|
||
if (workspaceRelativePath) {
|
||
const workspaceHtml = await readWorkspacePublishHtml(h5Root, userId, workspaceRelativePath);
|
||
if (workspaceHtml?.trim()) {
|
||
sourceHtml = workspaceHtml;
|
||
}
|
||
}
|
||
return {
|
||
page,
|
||
html: sourceHtml,
|
||
publicUrl,
|
||
workspaceRelativePath,
|
||
};
|
||
}
|
||
|
||
return {
|
||
async pushPageDraft(userId, pageId, { triggeredBy = null } = {}) {
|
||
let preview = null;
|
||
try {
|
||
preview = await resolvePageHtml(userId, pageId);
|
||
const { accessToken, credentials } = await getAccessToken(userId);
|
||
const publishDir = path.join(h5Root, PUBLISH_ROOT_DIR, String(userId));
|
||
const bundle = await buildWechatDraftPublicationBundleForPush({
|
||
html: preview.html,
|
||
publicUrl: preview.publicUrl,
|
||
pageTitle: preview.page.title,
|
||
pageSummary: preview.page.summary,
|
||
accessToken,
|
||
publishDir,
|
||
htmlRelativePath: preview.workspaceRelativePath ?? '',
|
||
h5Root,
|
||
userId,
|
||
workspaceRelativePath: preview.workspaceRelativePath,
|
||
wechatFetch,
|
||
memindLibRoot,
|
||
});
|
||
const { article, thumbResolved } = bundle;
|
||
const thumbBuffer = await loadThumbBuffer(thumbResolved.path, { wechatFetch });
|
||
const thumbMediaId = await uploadWechatPermanentThumb(accessToken, thumbBuffer, { wechatFetch });
|
||
const draft = await addWechatDraftArticle(
|
||
accessToken,
|
||
{
|
||
title: article.title,
|
||
author: credentials.author || 'TKMind',
|
||
digest: article.digest,
|
||
content: article.content,
|
||
content_source_url: article.contentSourceUrl,
|
||
thumb_media_id: thumbMediaId,
|
||
need_open_comment: 0,
|
||
only_fans_can_comment: 0,
|
||
},
|
||
{ wechatFetch },
|
||
);
|
||
const run = await recordRun({
|
||
userId,
|
||
pageId,
|
||
status: 'success',
|
||
pageTitle: preview.page.title,
|
||
pageUrl: preview.publicUrl,
|
||
draftMediaId: draft.draftMediaId,
|
||
triggeredBy,
|
||
});
|
||
return {
|
||
ok: true,
|
||
draftMediaId: draft.draftMediaId,
|
||
pageTitle: preview.page.title,
|
||
pageUrl: preview.publicUrl,
|
||
standardVersion: bundle.standardVersion,
|
||
coverSource: thumbResolved.source,
|
||
run,
|
||
};
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
const run = await recordRun({
|
||
userId,
|
||
pageId,
|
||
status: 'failed',
|
||
pageTitle: preview?.page?.title ?? null,
|
||
pageUrl: preview?.publicUrl ?? null,
|
||
errorMessage: message,
|
||
triggeredBy,
|
||
});
|
||
throw Object.assign(new Error(message), {
|
||
code: error?.code ?? 'wechat_draft_push_failed',
|
||
run,
|
||
});
|
||
}
|
||
},
|
||
|
||
async listRuns(userId, { pageId = null, limit = 20 } = {}) {
|
||
await ensureReady();
|
||
const safeLimit = Math.min(Math.max(Number(limit) || 20, 1), 100);
|
||
const params = [userId];
|
||
let sql = `
|
||
SELECT id, page_id AS pageId, status, page_title AS pageTitle, page_url AS pageUrl,
|
||
draft_media_id AS draftMediaId, error_message AS errorMessage,
|
||
triggered_by AS triggeredBy, created_at AS createdAt
|
||
FROM ${RUNS_TABLE}
|
||
WHERE user_id = ?`;
|
||
if (pageId) {
|
||
sql += ' AND page_id = ?';
|
||
params.push(pageId);
|
||
}
|
||
sql += ' ORDER BY created_at DESC LIMIT ?';
|
||
params.push(safeLimit);
|
||
const [rows] = await pool.query(sql, params);
|
||
return rows.map((row) => ({
|
||
...row,
|
||
createdAt: Number(row.createdAt ?? 0),
|
||
}));
|
||
},
|
||
};
|
||
}
|
||
|
||
export const mindspaceWechatPageDraftInternals = {
|
||
convertGenericPageHtmlToWechatArticle,
|
||
convertPageHtmlToWechatArticle,
|
||
convertRichMindSpacePageHtmlToWechatArticle,
|
||
collectPageImageCandidates,
|
||
isNewsHotspotFormat,
|
||
resolveWechatDraftThumbPath,
|
||
resolveHeroImagePath,
|
||
};
|