fix(brand): require TKMind platform marker on wechat page delivery

Backfill missing platform-brand footer at serve time and block service-
account links when the HTML file omits data-mindspace-page-tag.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-04 14:15:42 +08:00
parent cd81c4b2bb
commit b5a88d9746
8 changed files with 79 additions and 14 deletions
+18
View File
@@ -31,6 +31,24 @@ export function ensurePlatformBrandPageTags(html) {
}); });
} }
export function hasPlatformBrandMarker(html) {
return new RegExp(
`${MINDSPACE_PAGE_TAG_ATTR}\\s*=\\s*["']${MINDSPACE_PAGE_TAG_PLATFORM_BRAND}["']`,
'i',
).test(String(html ?? ''));
}
/** Append the platform brand footer when authors omitted data-mindspace-page-tag. */
export function ensurePlatformBrandFooter(html) {
const normalized = ensurePlatformBrandPageTags(html);
if (hasPlatformBrandMarker(normalized)) return normalized;
const footer = `<p ${MINDSPACE_PAGE_TAG_ATTR}="${MINDSPACE_PAGE_TAG_PLATFORM_BRAND}">${PLATFORM_BRAND_TEXT}</p>`;
if (/<\/body>/i.test(normalized)) {
return normalized.replace(/<\/body>/i, `${footer}\n</body>`);
}
return `${normalized}\n${footer}`;
}
export function prepareHtmlPageBrandMarkers(html) { export function prepareHtmlPageBrandMarkers(html) {
return ensurePlatformBrandPageTags(html); return ensurePlatformBrandPageTags(html);
} }
+8
View File
@@ -3,6 +3,7 @@ import test from 'node:test';
import { import {
MINDSPACE_PAGE_TAG_ATTR, MINDSPACE_PAGE_TAG_ATTR,
MINDSPACE_PAGE_TAG_PLATFORM_BRAND, MINDSPACE_PAGE_TAG_PLATFORM_BRAND,
ensurePlatformBrandFooter,
ensurePlatformBrandPageTags, ensurePlatformBrandPageTags,
normalizePlatformBrandHtml, normalizePlatformBrandHtml,
prepareHtmlPageBrandMarkers, prepareHtmlPageBrandMarkers,
@@ -28,3 +29,10 @@ test('prepareHtmlPageBrandMarkers keeps existing marker', () => {
const result = prepareHtmlPageBrandMarkers(html); const result = prepareHtmlPageBrandMarkers(html);
assert.equal(result.match(new RegExp(MINDSPACE_PAGE_TAG_ATTR, 'g'))?.length, 1); assert.equal(result.match(new RegExp(MINDSPACE_PAGE_TAG_ATTR, 'g'))?.length, 1);
}); });
test('ensurePlatformBrandFooter appends brand line when marker is missing', () => {
const html = '<html><body><main>content</main></body></html>';
const result = ensurePlatformBrandFooter(html);
assert.match(result, /data-mindspace-page-tag="platform-brand"/);
assert.match(result, /TKMind · 智趣/);
});
+2
View File
@@ -1,6 +1,7 @@
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { ensurePlatformBrandFooter } from './mindspace-page-tag.mjs';
const INLINE_SCRIPT_PATTERN = /<script\b(?![^>]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi; const INLINE_SCRIPT_PATTERN = /<script\b(?![^>]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi;
@@ -67,6 +68,7 @@ export function decorateMindSpacePublishedHtml({
} }
try { try {
nextHtml = ensurePlatformBrandFooter(nextHtml);
nextHtml = injectOgTags(nextHtml, { nextHtml = injectOgTags(nextHtml, {
origin: context.origin, origin: context.origin,
pageUrl: context.pageUrl, pageUrl: context.pageUrl,
+1 -1
View File
@@ -62,7 +62,7 @@ function inboundXml({
} }
function previewReadyPageHtml({ title = 'Page', subtitle = '测试页面' } = {}) { function previewReadyPageHtml({ title = 'Page', subtitle = '测试页面' } = {}) {
return `<!doctype html><html><head><meta name="description" content="${subtitle}"><meta name="mindspace-cover" content='{"tag":"页面","accent":"#3366cc","accent2":"#112233","subtitle":"${subtitle}"}'><title>${title}</title></head><body><main>${'x'.repeat(600)}</main></body></html>`; return `<!doctype html><html><head><meta name="description" content="${subtitle}"><meta name="mindspace-cover" content='{"tag":"页面","accent":"#3366cc","accent2":"#112233","subtitle":"${subtitle}"}'><title>${title}</title></head><body><main>${'x'.repeat(600)}</main><p data-mindspace-page-tag="platform-brand">TKMind · 智趣</p></body></html>`;
} }
function jsonEscapedPreviewReadyPageHtml(options = {}) { function jsonEscapedPreviewReadyPageHtml(options = {}) {
+9 -3
View File
@@ -1,6 +1,6 @@
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 { filterSharePreviewReadyArtifacts } from '../verify/share-preview.mjs'; import { filterSharePreviewReadyArtifacts, verifyArtifactSharePreview } from '../verify/share-preview.mjs';
export function evaluatePageGenerateSendableArtifacts({ verifiedArtifacts = [], confirmedArtifacts = [] } = {}) { export function evaluatePageGenerateSendableArtifacts({ verifiedArtifacts = [], confirmedArtifacts = [] } = {}) {
const sendable = selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts }); const sendable = selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts });
@@ -48,10 +48,16 @@ export function resolvePageGenerateOutcome({
selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts }), selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts }),
); );
if (previewCandidates.length === 0 && confirmedArtifacts.length > 0) { if (previewCandidates.length === 0 && confirmedArtifacts.length > 0) {
const previewIssue = verifyArtifactSharePreview(
selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts })[0] ?? confirmedArtifacts[0],
);
return { return {
action: 'fail', action: 'fail',
failureText: buildPagePublishFailureText({ missingSharePreview: true }), failureText: buildPagePublishFailureText({
reason: 'missing_share_preview', missingPlatformBrand: previewIssue.reason === 'missing_platform_brand',
missingSharePreview: previewIssue.reason !== 'missing_platform_brand',
}),
reason: previewIssue.reason ?? 'missing_share_preview',
}; };
} }
+8 -2
View File
@@ -28,7 +28,7 @@ export function buildPageGenerateAgentPrompt(intent, { wantsDocx = false } = {})
' - `<meta name="description" content="一句话摘要">`', ' - `<meta name="description" content="一句话摘要">`',
' - `<meta name="mindspace-cover" content=\'{"tag":"主题","accent":"#主色","accent2":"#辅色","subtitle":"卖点","cover":"assets/hero.jpg"}\'>`', ' - `<meta name="mindspace-cover" content=\'{"tag":"主题","accent":"#主色","accent2":"#辅色","subtitle":"卖点","cover":"assets/hero.jpg"}\'>`',
' tag/accent/subtitle 必须与页面主题一致;有 hero 图时 cover 指向同目录 raster 图(jpg/png)。', ' tag/accent/subtitle 必须与页面主题一致;有 hero 图时 cover 指向同目录 raster 图(jpg/png)。',
'6. 页脚加 `<footer data-mindspace-page-tag="platform-brand">` 以显示 TKMind 智趣 品牌。', '6. 页脚加 `<p data-mindspace-page-tag="platform-brand">TKMind · 智趣</p>`(必须,禁止省略或用邮箱/tkmind.ai 代替)。',
'7. 回复里只给一个正式域名链接;没有落盘或缺少上述元数据就不要发链接。', '7. 回复里只给一个正式域名链接;没有落盘或缺少上述元数据就不要发链接。',
'', '',
buildCurrentTimeAgentPrefix({ timezone: scheduleTimezone }), buildCurrentTimeAgentPrefix({ timezone: scheduleTimezone }),
@@ -38,7 +38,13 @@ export function buildPageGenerateAgentPrompt(intent, { wantsDocx = false } = {})
.join('\n'); .join('\n');
} }
export function buildPagePublishFailureText({ missingSharePreview = false } = {}) { export function buildPagePublishFailureText({ missingSharePreview = false, missingPlatformBrand = false } = {}) {
if (missingPlatformBrand) {
return [
'这次页面已落盘,但缺少页脚平台品牌标记(data-mindspace-page-tag="platform-brand"),所以我先不发链接。',
'请直接重发一次完整页面需求,我会重新按 static-page-publish 技能生成带 TKMind · 智趣 品牌页脚的页面。',
].join('\n');
}
if (missingSharePreview) { if (missingSharePreview) {
return [ return [
'这次页面已落盘,但缺少微信分享卡片所需的预览元数据(mindspace-cover 与摘要),所以我先不发链接。', '这次页面已落盘,但缺少微信分享卡片所需的预览元数据(mindspace-cover 与摘要),所以我先不发链接。',
+6
View File
@@ -1,5 +1,8 @@
import fs from 'node:fs'; import fs from 'node:fs';
import { artifactFileExists } from './page-artifact.mjs'; import { artifactFileExists } from './page-artifact.mjs';
import { hasPlatformBrandMarker } from '../../mindspace-page-tag.mjs';
export { hasPlatformBrandMarker };
const RASTER_IMAGE_PATTERN = /\.(png|jpe?g|webp|gif)(?:[?#]|$)/i; const RASTER_IMAGE_PATTERN = /\.(png|jpe?g|webp|gif)(?:[?#]|$)/i;
@@ -48,6 +51,9 @@ export function verifySharePreviewMeta(html) {
if (!hasShareDescription(html)) { if (!hasShareDescription(html)) {
return { ok: false, reason: 'missing_description' }; return { ok: false, reason: 'missing_description' };
} }
if (!hasPlatformBrandMarker(html)) {
return { ok: false, reason: 'missing_platform_brand' };
}
return { ok: true, reason: null }; return { ok: true, reason: null };
} }
+27 -8
View File
@@ -16,6 +16,7 @@ import { buildGreetingText, resolveSyncReply } from './handlers/sync-replies.mjs
import { buildWechatAgentPrompt } from './prompts/chat-general.mjs'; import { buildWechatAgentPrompt } from './prompts/chat-general.mjs';
import { import {
hasMindspaceCoverMeta, hasMindspaceCoverMeta,
hasPlatformBrandMarker,
hasShareDescription, hasShareDescription,
verifyArtifactSharePreview, verifyArtifactSharePreview,
verifySharePreviewMeta, verifySharePreviewMeta,
@@ -120,26 +121,31 @@ test('verifyPageArtifactContent rejects small stub files', () => {
assert.equal(filterSendableHtmlArtifacts([artifact]).length, 0); assert.equal(filterSendableHtmlArtifacts([artifact]).length, 0);
}); });
function writePreviewReadyHtml(file, { withCover = true } = {}) { function writePreviewReadyHtml(file, { withCover = true, withBrand = true } = {}) {
const coverMeta = withCover const coverMeta = withCover
? '<meta name="mindspace-cover" content=\'{"tag":"旅行","accent":"#ff6b35","accent2":"#24243e","subtitle":"测试页面"}\'>' ? '<meta name="mindspace-cover" content=\'{"tag":"旅行","accent":"#ff6b35","accent2":"#24243e","subtitle":"测试页面"}\'>'
: ''; : '';
const brandMeta = withBrand
? '<p data-mindspace-page-tag="platform-brand">TKMind · 智趣</p>'
: '';
fs.writeFileSync( fs.writeFileSync(
file, file,
`<!doctype html><html><head> `<!doctype html><html><head>
<meta name="description" content="一句话摘要"> <meta name="description" content="一句话摘要">
${coverMeta} ${coverMeta}
</head><body><main>${'x'.repeat(600)}</main></body></html>`, </head><body><main>${'x'.repeat(600)}</main>${brandMeta}</body></html>`,
'utf8', 'utf8',
); );
} }
test('verifySharePreviewMeta requires mindspace-cover and description', () => { test('verifySharePreviewMeta requires mindspace-cover, description, and platform brand', () => {
const withBoth = '<meta name="description" content="摘要"><meta name="mindspace-cover" content=\'{"tag":"旅行"}\'>'; const withAll =
assert.equal(verifySharePreviewMeta(withBoth).ok, true); '<meta name="description" content="摘要"><meta name="mindspace-cover" content=\'{"tag":"旅行"}\'><p data-mindspace-page-tag="platform-brand">TKMind · 智趣</p>';
assert.equal(verifySharePreviewMeta(withAll).ok, true);
assert.equal(verifySharePreviewMeta('<meta name="description" content="摘要">').ok, false); assert.equal(verifySharePreviewMeta('<meta name="description" content="摘要">').ok, false);
assert.equal(hasMindspaceCoverMeta(withBoth), true); assert.equal(hasMindspaceCoverMeta(withAll), true);
assert.equal(hasShareDescription(withBoth), true); assert.equal(hasShareDescription(withAll), true);
assert.equal(hasPlatformBrandMarker(withAll), true);
}); });
test('resolvePageGenerateOutcome fails when html lacks share preview meta', () => { test('resolvePageGenerateOutcome fails when html lacks share preview meta', () => {
@@ -151,10 +157,23 @@ test('resolvePageGenerateOutcome fails when html lacks share preview meta', () =
confirmedArtifacts: [{ localPath: file, relativePath: 'public/page.html' }], confirmedArtifacts: [{ localPath: file, relativePath: 'public/page.html' }],
}); });
assert.equal(outcome.action, 'fail'); assert.equal(outcome.action, 'fail');
assert.equal(outcome.reason, 'missing_share_preview'); assert.equal(outcome.reason, 'missing_mindspace_cover');
assert.match(outcome.failureText, /预览元数据/); assert.match(outcome.failureText, /预览元数据/);
}); });
test('resolvePageGenerateOutcome fails 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 });
const outcome = resolvePageGenerateOutcome({
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, /平台品牌/);
});
test('resolvePageGenerateOutcome sends when share preview meta is present', () => { test('resolvePageGenerateOutcome sends when share preview meta is present', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-preview-ok-')); const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-preview-ok-'));
const file = path.join(dir, 'page.html'); const file = path.join(dir, 'page.html');