fix(wechat): auto-repair missing share preview meta before page delivery
Memind CI / Test, build, and release guards (push) Failing after 3s
Memind CI / Test, build, and release guards (push) Failing after 3s
Pages with real HTML content but missing description, mindspace-cover, or platform brand markers are patched in-place so WeChat link delivery no longer fails after successful generation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2649,6 +2649,7 @@ export function createWechatMpService({
|
|||||||
bareCompletionReply,
|
bareCompletionReply,
|
||||||
htmlGenerationNeedsRetry,
|
htmlGenerationNeedsRetry,
|
||||||
replyHasPublicLinks,
|
replyHasPublicLinks,
|
||||||
|
topic: wechatIntent?.topic ?? intent?.agentText ?? '',
|
||||||
});
|
});
|
||||||
if (pageOutcome.action === 'session_retry') {
|
if (pageOutcome.action === 'session_retry') {
|
||||||
throw new Error('stale_session_poisoned_completion');
|
throw new Error('stale_session_poisoned_completion');
|
||||||
@@ -2967,6 +2968,7 @@ export function createWechatMpService({
|
|||||||
bareCompletionReply,
|
bareCompletionReply,
|
||||||
htmlGenerationNeedsRetry,
|
htmlGenerationNeedsRetry,
|
||||||
replyHasPublicLinks,
|
replyHasPublicLinks,
|
||||||
|
topic: wechatIntent?.topic ?? intent?.agentText ?? '',
|
||||||
});
|
});
|
||||||
if (pageOutcome.action === 'session_retry' || pageOutcome.action === 'fail') {
|
if (pageOutcome.action === 'session_retry' || pageOutcome.action === 'fail') {
|
||||||
const text = pageOutcome.failureText ?? buildPagePublishFailureText();
|
const text = pageOutcome.failureText ?? buildPagePublishFailureText();
|
||||||
|
|||||||
@@ -1,12 +1,28 @@
|
|||||||
import { buildPagePublishFailureText } from '../prompts/page-generate.mjs';
|
import { buildPagePublishFailureText } from '../prompts/page-generate.mjs';
|
||||||
import { selectSendableHtmlArtifacts, verifyPageArtifactContent } from '../verify/page-artifact.mjs';
|
import { selectSendableHtmlArtifacts, verifyPageArtifactContent } from '../verify/page-artifact.mjs';
|
||||||
|
import { repairArtifactSharePreview } from '../verify/share-preview-repair.mjs';
|
||||||
import { filterSharePreviewReadyArtifacts, verifyArtifactSharePreview } from '../verify/share-preview.mjs';
|
import { filterSharePreviewReadyArtifacts, verifyArtifactSharePreview } from '../verify/share-preview.mjs';
|
||||||
|
|
||||||
export function evaluatePageGenerateSendableArtifacts({ verifiedArtifacts = [], confirmedArtifacts = [] } = {}) {
|
export function evaluatePageGenerateSendableArtifacts({
|
||||||
|
verifiedArtifacts = [],
|
||||||
|
confirmedArtifacts = [],
|
||||||
|
repairContext = {},
|
||||||
|
} = {}) {
|
||||||
const sendable = selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts });
|
const sendable = selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts });
|
||||||
const verified = sendable.filter((artifact) => verifyPageArtifactContent(artifact).ok);
|
const verified = sendable.filter((artifact) => verifyPageArtifactContent(artifact).ok);
|
||||||
const candidates = verified.length > 0 ? verified : sendable;
|
const candidates = verified.length > 0 ? verified : sendable;
|
||||||
return filterSharePreviewReadyArtifacts(candidates);
|
const ready = filterSharePreviewReadyArtifacts(candidates);
|
||||||
|
if (ready.length > 0) return ready;
|
||||||
|
|
||||||
|
const repaired = [];
|
||||||
|
for (const artifact of candidates) {
|
||||||
|
if (!verifyPageArtifactContent(artifact).ok) continue;
|
||||||
|
const result = repairArtifactSharePreview(artifact, repairContext);
|
||||||
|
if (result.ok && verifyArtifactSharePreview(artifact).ok) {
|
||||||
|
repaired.push(artifact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return repaired;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,8 +37,18 @@ export function resolvePageGenerateOutcome({
|
|||||||
bareCompletionReply = false,
|
bareCompletionReply = false,
|
||||||
htmlGenerationNeedsRetry = false,
|
htmlGenerationNeedsRetry = false,
|
||||||
replyHasPublicLinks = false,
|
replyHasPublicLinks = false,
|
||||||
|
topic = '',
|
||||||
|
repairContext = {},
|
||||||
}) {
|
}) {
|
||||||
const sendable = evaluatePageGenerateSendableArtifacts({ verifiedArtifacts, confirmedArtifacts });
|
const context = {
|
||||||
|
topic: String(topic || repairContext.topic || '').trim(),
|
||||||
|
...repairContext,
|
||||||
|
};
|
||||||
|
const sendable = evaluatePageGenerateSendableArtifacts({
|
||||||
|
verifiedArtifacts,
|
||||||
|
confirmedArtifacts,
|
||||||
|
repairContext: context,
|
||||||
|
});
|
||||||
|
|
||||||
if (sendable.length > 0) {
|
if (sendable.length > 0) {
|
||||||
return { action: 'send', artifacts: sendable };
|
return { action: 'send', artifacts: sendable };
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import { injectBeforeDocumentClosingHead } from '../../html-document-injection.mjs';
|
||||||
|
import { normalizeCoverMetaSuggestion, upsertMindspaceCoverMeta } from '../../mindspace-cover-meta.mjs';
|
||||||
|
import { preparePublishedPlatformBrand } from '../../mindspace-page-tag.mjs';
|
||||||
|
import { artifactFileExists, verifyPageArtifactContent } from './page-artifact.mjs';
|
||||||
|
import {
|
||||||
|
hasMindspaceCoverMeta,
|
||||||
|
hasPlatformBrandMarker,
|
||||||
|
hasShareDescription,
|
||||||
|
verifyArtifactSharePreview,
|
||||||
|
verifySharePreviewMeta,
|
||||||
|
} from './share-preview.mjs';
|
||||||
|
|
||||||
|
function escapeMetaAttribute(value) {
|
||||||
|
return String(value ?? '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll('<', '<');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractPageTitle(html) {
|
||||||
|
const match = String(html ?? '').match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
||||||
|
return match?.[1]?.replace(/<[^>]+>/g, '').trim() ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inferCoverMeta({ title = '', topic = '' } = {}) {
|
||||||
|
const source = `${title} ${topic}`.trim();
|
||||||
|
let tag = '报告';
|
||||||
|
let emoji = '📄';
|
||||||
|
let accent = '#667eea';
|
||||||
|
let accent2 = '#764ba2';
|
||||||
|
if (/攻略|旅行|游记|半日|地图|景点|路线/.test(source)) {
|
||||||
|
tag = '旅行';
|
||||||
|
emoji = '🏞️';
|
||||||
|
accent = '#2e7d32';
|
||||||
|
accent2 = '#1565c0';
|
||||||
|
} else if (/新闻|早报|热点|资讯|日报/.test(source)) {
|
||||||
|
tag = '资讯';
|
||||||
|
emoji = '📰';
|
||||||
|
accent = '#2563eb';
|
||||||
|
accent2 = '#0f172a';
|
||||||
|
} else if (/诗|诗词|散文|文学/.test(source)) {
|
||||||
|
tag = '文艺';
|
||||||
|
emoji = '🌿';
|
||||||
|
accent = '#2d5016';
|
||||||
|
accent2 = '#1a2a3a';
|
||||||
|
}
|
||||||
|
const subtitle = (title || topic || 'MindSpace 页面').trim().slice(0, 80);
|
||||||
|
return normalizeCoverMetaSuggestion({ tag, emoji, accent, accent2, subtitle, cover: '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function repairSharePreviewHtml(html, { title = '', topic = '' } = {}) {
|
||||||
|
let next = String(html ?? '');
|
||||||
|
const resolvedTitle = String(title || extractPageTitle(next) || '').trim();
|
||||||
|
const summary = (topic || resolvedTitle || 'MindSpace 页面').trim().slice(0, 160);
|
||||||
|
|
||||||
|
if (!hasShareDescription(next)) {
|
||||||
|
const meta = `<meta name="description" content="${escapeMetaAttribute(summary)}">`;
|
||||||
|
next = injectBeforeDocumentClosingHead(next, `${meta}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMindspaceCoverMeta(next)) {
|
||||||
|
next = upsertMindspaceCoverMeta(next, inferCoverMeta({ title: resolvedTitle, topic }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPlatformBrandMarker(next)) {
|
||||||
|
next = preparePublishedPlatformBrand(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function repairArtifactSharePreview(artifact, { topic = '' } = {}) {
|
||||||
|
if (!artifactFileExists(artifact)) {
|
||||||
|
return { ok: false, reason: 'missing_file', changes: [] };
|
||||||
|
}
|
||||||
|
if (!verifyPageArtifactContent(artifact).ok) {
|
||||||
|
return { ok: false, reason: 'not_repairable_artifact', changes: [] };
|
||||||
|
}
|
||||||
|
const localPath = String(artifact.localPath ?? '').trim();
|
||||||
|
if (!localPath) {
|
||||||
|
return { ok: false, reason: 'missing_local_path', changes: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const before = fs.readFileSync(localPath, 'utf8');
|
||||||
|
if (verifySharePreviewMeta(before).ok) {
|
||||||
|
return { ok: true, reason: null, changes: [], alreadyReady: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = extractPageTitle(before);
|
||||||
|
const after = repairSharePreviewHtml(before, { title, topic });
|
||||||
|
if (after === before) {
|
||||||
|
return { ok: false, reason: 'repair_noop', changes: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const changes = [];
|
||||||
|
if (!hasShareDescription(before) && hasShareDescription(after)) changes.push('description');
|
||||||
|
if (!hasMindspaceCoverMeta(before) && hasMindspaceCoverMeta(after)) changes.push('mindspace_cover');
|
||||||
|
if (!hasPlatformBrandMarker(before) && hasPlatformBrandMarker(after)) changes.push('platform_brand');
|
||||||
|
|
||||||
|
fs.writeFileSync(localPath, after, 'utf8');
|
||||||
|
const verified = verifyArtifactSharePreview(artifact);
|
||||||
|
if (!verified.ok) {
|
||||||
|
return { ok: false, reason: verified.reason ?? 'repair_incomplete', changes };
|
||||||
|
}
|
||||||
|
return { ok: true, reason: null, changes };
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
verifyArtifactSharePreview,
|
verifyArtifactSharePreview,
|
||||||
verifySharePreviewMeta,
|
verifySharePreviewMeta,
|
||||||
} from './verify/share-preview.mjs';
|
} from './verify/share-preview.mjs';
|
||||||
|
import { repairSharePreviewHtml } from './verify/share-preview-repair.mjs';
|
||||||
|
|
||||||
test('classifyWechatIntent detects page.generate', () => {
|
test('classifyWechatIntent detects page.generate', () => {
|
||||||
const intent = classifyWechatIntent({
|
const intent = classifyWechatIntent({
|
||||||
@@ -205,20 +206,30 @@ test('verifySharePreviewMeta requires mindspace-cover, description, and platform
|
|||||||
assert.equal(hasPlatformBrandMarker(withAll), true);
|
assert.equal(hasPlatformBrandMarker(withAll), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolvePageGenerateOutcome fails when html lacks share preview meta', () => {
|
test('repairSharePreviewHtml injects missing description, cover, and platform brand', () => {
|
||||||
|
const html = `<!doctype html><html><head><title>安吉德清半日攻略</title></head><body><main>${'内容'.repeat(200)}</main></body></html>`;
|
||||||
|
const repaired = repairSharePreviewHtml(html, { topic: '帮我把这个安吉攻略生成页面' });
|
||||||
|
assert.equal(verifySharePreviewMeta(repaired).ok, true);
|
||||||
|
assert.match(repaired, /name="description"/);
|
||||||
|
assert.match(repaired, /name="mindspace-cover"/);
|
||||||
|
assert.match(repaired, /data-mindspace-page-tag="platform-brand"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolvePageGenerateOutcome repairs and sends when html lacks share preview meta', () => {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-preview-'));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-preview-'));
|
||||||
const file = path.join(dir, 'page.html');
|
const file = path.join(dir, 'page.html');
|
||||||
writePreviewReadyHtml(file, { withCover: false });
|
writePreviewReadyHtml(file, { withCover: false });
|
||||||
const outcome = resolvePageGenerateOutcome({
|
const outcome = resolvePageGenerateOutcome({
|
||||||
reply: { text: '页面已生成 https://m.tkmind.cn/MindSpace/u/public/page.html' },
|
reply: { text: '页面已生成 https://m.tkmind.cn/MindSpace/u/public/page.html' },
|
||||||
confirmedArtifacts: [{ localPath: file, relativePath: 'public/page.html' }],
|
confirmedArtifacts: [{ localPath: file, relativePath: 'public/page.html' }],
|
||||||
|
topic: '安吉攻略页面',
|
||||||
});
|
});
|
||||||
assert.equal(outcome.action, 'fail');
|
assert.equal(outcome.action, 'send');
|
||||||
assert.equal(outcome.reason, 'missing_mindspace_cover');
|
assert.equal(outcome.artifacts.length, 1);
|
||||||
assert.match(outcome.failureText, /预览元数据/);
|
assert.equal(verifyArtifactSharePreview({ localPath: file }).ok, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolvePageGenerateOutcome fails when html lacks platform brand marker', () => {
|
test('resolvePageGenerateOutcome repairs and sends when html lacks platform brand marker', () => {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-brand-'));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-brand-'));
|
||||||
const file = path.join(dir, 'page.html');
|
const file = path.join(dir, 'page.html');
|
||||||
writePreviewReadyHtml(file, { withBrand: false });
|
writePreviewReadyHtml(file, { withBrand: false });
|
||||||
@@ -226,9 +237,9 @@ test('resolvePageGenerateOutcome fails when html lacks platform brand marker', (
|
|||||||
reply: { text: '页面已生成' },
|
reply: { text: '页面已生成' },
|
||||||
confirmedArtifacts: [{ localPath: file, relativePath: 'public/page.html' }],
|
confirmedArtifacts: [{ localPath: file, relativePath: 'public/page.html' }],
|
||||||
});
|
});
|
||||||
assert.equal(outcome.action, 'fail');
|
assert.equal(outcome.action, 'send');
|
||||||
assert.equal(outcome.reason, 'missing_platform_brand');
|
assert.equal(outcome.artifacts.length, 1);
|
||||||
assert.match(outcome.failureText, /平台品牌/);
|
assert.equal(hasPlatformBrandMarker(fs.readFileSync(file, 'utf8')), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolvePageGenerateOutcome sends when share preview meta is present', () => {
|
test('resolvePageGenerateOutcome sends when share preview meta is present', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user