diff --git a/wechat-mp.mjs b/wechat-mp.mjs
index 66eddd5..be07606 100644
--- a/wechat-mp.mjs
+++ b/wechat-mp.mjs
@@ -2649,6 +2649,7 @@ export function createWechatMpService({
bareCompletionReply,
htmlGenerationNeedsRetry,
replyHasPublicLinks,
+ topic: wechatIntent?.topic ?? intent?.agentText ?? '',
});
if (pageOutcome.action === 'session_retry') {
throw new Error('stale_session_poisoned_completion');
@@ -2967,6 +2968,7 @@ export function createWechatMpService({
bareCompletionReply,
htmlGenerationNeedsRetry,
replyHasPublicLinks,
+ topic: wechatIntent?.topic ?? intent?.agentText ?? '',
});
if (pageOutcome.action === 'session_retry' || pageOutcome.action === 'fail') {
const text = pageOutcome.failureText ?? buildPagePublishFailureText();
diff --git a/wechat/handlers/page-generate.mjs b/wechat/handlers/page-generate.mjs
index d07546f..c873449 100644
--- a/wechat/handlers/page-generate.mjs
+++ b/wechat/handlers/page-generate.mjs
@@ -1,12 +1,28 @@
import { buildPagePublishFailureText } from '../prompts/page-generate.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';
-export function evaluatePageGenerateSendableArtifacts({ verifiedArtifacts = [], confirmedArtifacts = [] } = {}) {
+export function evaluatePageGenerateSendableArtifacts({
+ verifiedArtifacts = [],
+ confirmedArtifacts = [],
+ repairContext = {},
+} = {}) {
const sendable = selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts });
const verified = sendable.filter((artifact) => verifyPageArtifactContent(artifact).ok);
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,
htmlGenerationNeedsRetry = 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) {
return { action: 'send', artifacts: sendable };
diff --git a/wechat/verify/share-preview-repair.mjs b/wechat/verify/share-preview-repair.mjs
new file mode 100644
index 0000000..efec84d
--- /dev/null
+++ b/wechat/verify/share-preview-repair.mjs
@@ -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(/
]*>([\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 = ``;
+ 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 };
+}
diff --git a/wechat/wechat-channel.test.mjs b/wechat/wechat-channel.test.mjs
index 4d47dc6..b01b04b 100644
--- a/wechat/wechat-channel.test.mjs
+++ b/wechat/wechat-channel.test.mjs
@@ -22,6 +22,7 @@ import {
verifyArtifactSharePreview,
verifySharePreviewMeta,
} from './verify/share-preview.mjs';
+import { repairSharePreviewHtml } from './verify/share-preview-repair.mjs';
test('classifyWechatIntent detects page.generate', () => {
const intent = classifyWechatIntent({
@@ -205,20 +206,30 @@ test('verifySharePreviewMeta requires mindspace-cover, description, and platform
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 = `安吉德清半日攻略${'内容'.repeat(200)}`;
+ 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 file = path.join(dir, 'page.html');
writePreviewReadyHtml(file, { withCover: false });
const outcome = resolvePageGenerateOutcome({
reply: { text: '页面已生成 https://m.tkmind.cn/MindSpace/u/public/page.html' },
confirmedArtifacts: [{ localPath: file, relativePath: 'public/page.html' }],
+ topic: '安吉攻略页面',
});
- assert.equal(outcome.action, 'fail');
- assert.equal(outcome.reason, 'missing_mindspace_cover');
- assert.match(outcome.failureText, /预览元数据/);
+ assert.equal(outcome.action, 'send');
+ assert.equal(outcome.artifacts.length, 1);
+ 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 file = path.join(dir, 'page.html');
writePreviewReadyHtml(file, { withBrand: false });
@@ -226,9 +237,9 @@ test('resolvePageGenerateOutcome fails when html lacks platform brand marker', (
reply: { text: '页面已生成' },
confirmedArtifacts: [{ localPath: file, relativePath: 'public/page.html' }],
});
- assert.equal(outcome.action, 'fail');
- assert.equal(outcome.reason, 'missing_platform_brand');
- assert.match(outcome.failureText, /平台品牌/);
+ assert.equal(outcome.action, 'send');
+ assert.equal(outcome.artifacts.length, 1);
+ assert.equal(hasPlatformBrandMarker(fs.readFileSync(file, 'utf8')), true);
});
test('resolvePageGenerateOutcome sends when share preview meta is present', () => {