fix(wechat): complement page link delivery when reply references public html
Memind CI / Test, build, and release guards (push) Failing after 7s

Intent routing and reply content now both trigger HTML delivery: public links,
public/*.html paths, or html writes in the agent reply enable artifact recovery,
link attachment, and fail-closed when the claimed page is missing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-02 10:00:05 +08:00
parent 3e8cdfdda3
commit 3547cf8ef0
3 changed files with 227 additions and 16 deletions
+28 -3
View File
@@ -32,6 +32,31 @@ function replyMessages(reply) {
: []; : [];
} }
function messageVisibleText(message) {
if (!message?.content) return '';
return message.content
.filter((item) => item.type === 'text' && typeof item.text === 'string')
.map((item) => item.text)
.join('');
}
function collectReplyLinkScanText(reply) {
const parts = [];
const seen = new Set();
const append = (text) => {
const normalized = String(text ?? '').trim();
if (!normalized || seen.has(normalized)) return;
seen.add(normalized);
parts.push(normalized);
};
append(reply?.text);
for (const message of replyMessages(reply)) {
if (message?.role !== 'assistant') continue;
append(messageVisibleText(message));
}
return parts.join('\n');
}
function extractHtmlWriteTargets(messages = []) { function extractHtmlWriteTargets(messages = []) {
const targets = new Set(); const targets = new Set();
for (const message of messages) { for (const message of messages) {
@@ -396,7 +421,7 @@ export function prepareWechatHtmlDeliveryAtWorkspace({
? collectRecentArtifacts({ ? collectRecentArtifacts({
publishDir, publishDir,
buildCanonicalUrl, buildCanonicalUrl,
replyText: reply?.text, replyText: collectReplyLinkScanText(reply),
requestStartedAt, requestStartedAt,
}) })
: []; : [];
@@ -421,7 +446,7 @@ export function prepareWechatHtmlDeliveryAtWorkspace({
}) ?? artifact, }) ?? artifact,
); );
const linkedFilenames = const linkedFilenames =
collectLinkedHtmlFilenames(reply?.text); collectLinkedHtmlFilenames(collectReplyLinkScanText(reply));
const matchedArtifacts = const matchedArtifacts =
linkedFilenames.size > 0 linkedFilenames.size > 0
? confirmedArtifacts.filter((artifact) => ? confirmedArtifacts.filter((artifact) =>
@@ -433,7 +458,7 @@ export function prepareWechatHtmlDeliveryAtWorkspace({
) )
: confirmedArtifacts; : confirmedArtifacts;
const validReplyUrls = buildValidReplyUrls( const validReplyUrls = buildValidReplyUrls(
reply?.text, collectReplyLinkScanText(reply),
confirmedArtifacts, confirmedArtifacts,
); );
return { return {
+97 -13
View File
@@ -565,6 +565,38 @@ function hasAnyPublicHtmlLink(text) {
return [...String(text ?? '').matchAll(PUBLIC_HTML_LINK_PATTERN)].length > 0; return [...String(text ?? '').matchAll(PUBLIC_HTML_LINK_PATTERN)].length > 0;
} }
const PUBLIC_HTML_PATH_REFERENCE_PATTERN =
/(?:^|[\s`'"])(public\/[a-z0-9._-]+\.html)\b/i;
function hasPublicHtmlPathReference(text) {
return PUBLIC_HTML_PATH_REFERENCE_PATTERN.test(String(text ?? ''));
}
export function hasAnyPublicHtmlLinkInReply(reply) {
return collectWechatAgentReplyVisibleTexts(reply).some((text) => hasAnyPublicHtmlLink(text));
}
function hasPublicHtmlPathReferenceInReply(reply) {
return collectWechatAgentReplyVisibleTexts(reply).some((text) => hasPublicHtmlPathReference(text));
}
export function replyImpliesPublicHtmlDelivery(reply) {
if (hasAnyPublicHtmlLinkInReply(reply)) return true;
if (hasPublicHtmlPathReferenceInReply(reply)) return true;
return extractHtmlWriteTargets(replyRequestMessages(reply)).length > 0;
}
export function shouldFailClosedOnMissingPublicHtmlDelivery({
reply,
confirmedArtifacts = [],
hasValidLinkInReply = false,
} = {}) {
if (!replyImpliesPublicHtmlDelivery(reply)) return false;
if (nonStubHtmlArtifacts(confirmedArtifacts).length > 0) return false;
if (hasValidLinkInReply) return false;
return true;
}
function collectPublicHtmlLinkFilenames(text) { function collectPublicHtmlLinkFilenames(text) {
const filenames = new Set(); const filenames = new Set();
for (const match of String(text ?? '').matchAll(PUBLIC_HTML_LINK_PATTERN)) { for (const match of String(text ?? '').matchAll(PUBLIC_HTML_LINK_PATTERN)) {
@@ -857,10 +889,7 @@ async function resolveHtmlPublishArtifacts({
intent?.agentText, intent?.agentText,
) || ) ||
isWechatPageDataTask(intent?.agentText) || isWechatPageDataTask(intent?.agentText) ||
hasAnyPublicHtmlLink(reply?.text) || replyImpliesPublicHtmlDelivery(reply);
extractHtmlWriteTargets(
replyRequestMessages(reply),
).length > 0;
if ( if (
typeof prepareDelivery !== 'function' typeof prepareDelivery !== 'function'
) { ) {
@@ -961,11 +990,26 @@ export function shouldRetryHtmlGenerationReply({
confirmedArtifacts = [], confirmedArtifacts = [],
hasValidLinkInReply = false, hasValidLinkInReply = false,
}) { }) {
if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false;
if (nonStubHtmlArtifacts(confirmedArtifacts).length > 0) return false; if (nonStubHtmlArtifacts(confirmedArtifacts).length > 0) return false;
const replyHasPublicLinks = hasAnyPublicHtmlLink(reply?.text); const replyHasPublicLinks = hasAnyPublicHtmlLinkInReply(reply);
const replyImpliesDelivery = replyImpliesPublicHtmlDelivery(reply);
if (replyImpliesDelivery) {
if (replyHasPublicLinks && !hasValidLinkInReply) return true;
if (
extractHtmlWriteTargets(replyRequestMessages(reply)).length > 0
&& confirmedArtifacts.length === 0
) {
return true;
}
if (hasPublicHtmlPathReferenceInReply(reply) && confirmedArtifacts.length === 0) {
return true;
}
}
if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false;
if (replyHasPublicLinks) { if (replyHasPublicLinks) {
if (!hasValidLinkInReply) return true; if (!hasValidLinkInReply) return true;
if (isMissingRequiredPublishSkill(reply, intent)) return true; if (isMissingRequiredPublishSkill(reply, intent)) return true;
@@ -2650,6 +2694,9 @@ export function createWechatMpService({
error.code = 'WECHAT_IMAGE_GENERATION_REQUIRED'; error.code = 'WECHAT_IMAGE_GENERATION_REQUIRED';
throw error; throw error;
} }
const replyImpliesHtmlDelivery = replyImpliesPublicHtmlDelivery(reply);
const effectiveHtmlDelivery = htmlArtifactDeliveryExpected || replyImpliesHtmlDelivery;
const allowRecentForDelivery = allowHtmlArtifactRecovery || replyImpliesHtmlDelivery;
const { const {
publishedArtifacts, publishedArtifacts,
expectedArtifacts, expectedArtifacts,
@@ -2665,7 +2712,7 @@ export function createWechatMpService({
userId: user.userId, userId: user.userId,
sessionId, sessionId,
onPageGenerated, onPageGenerated,
allowRecentArtifacts: allowHtmlArtifactRecovery, allowRecentArtifacts: allowRecentForDelivery,
htmlDeliveryAuthority, htmlDeliveryAuthority,
}); });
confirmedArtifacts = resolvedConfirmedArtifacts; confirmedArtifacts = resolvedConfirmedArtifacts;
@@ -2686,7 +2733,7 @@ export function createWechatMpService({
linkExistsForRequest, linkExistsForRequest,
{ confirmedArtifacts }, { confirmedArtifacts },
); );
const replyHasPublicLinks = hasAnyPublicHtmlLink(reply?.text); const replyHasPublicLinks = hasAnyPublicHtmlLinkInReply(reply);
const suspiciousPublishClaim = const suspiciousPublishClaim =
expectedArtifacts.length === 0 && expectedArtifacts.length === 0 &&
recentArtifacts.length === 0 && recentArtifacts.length === 0 &&
@@ -2699,7 +2746,7 @@ export function createWechatMpService({
}); });
const bareCompletionReply = isSuspiciousBareCompletionReply(reply, intent); const bareCompletionReply = isSuspiciousBareCompletionReply(reply, intent);
publishArtifacts = publishArtifacts =
htmlArtifactDeliveryExpected || publishedArtifacts.length > 0 effectiveHtmlDelivery || publishedArtifacts.length > 0
? selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts }) ? selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts })
: []; : [];
@@ -2765,6 +2812,23 @@ export function createWechatMpService({
} }
throw markWechatUserNotified(new Error(text)); throw markWechatUserNotified(new Error(text));
} }
} else if (
replyImpliesHtmlDelivery &&
shouldFailClosedOnMissingPublicHtmlDelivery({
reply,
confirmedArtifacts,
hasValidLinkInReply,
})
) {
const text = buildHtmlPublishFailureText();
try {
await sendWechatFailureNotice(inbound.fromUserName, text, user, {
sourceMsgId: intent.msgId,
});
} catch (sendErr) {
logger.error?.('WeChat MP reply-implied html delivery failure notice failed:', sendErr);
}
throw markWechatUserNotified(new Error(text));
} else if (htmlGenerationNeedsRetry || suspiciousPublishClaim) { } else if (htmlGenerationNeedsRetry || suspiciousPublishClaim) {
throw new Error('stale_session_poisoned_completion'); throw new Error('stale_session_poisoned_completion');
} }
@@ -2998,6 +3062,9 @@ export function createWechatMpService({
error.code = 'WECHAT_IMAGE_GENERATION_REQUIRED'; error.code = 'WECHAT_IMAGE_GENERATION_REQUIRED';
throw error; throw error;
} }
const replyImpliesHtmlDelivery = replyImpliesPublicHtmlDelivery(reply);
const effectiveHtmlDelivery = htmlArtifactDeliveryExpected || replyImpliesHtmlDelivery;
const allowRecentForDelivery = allowHtmlArtifactRecovery || replyImpliesHtmlDelivery;
const { const {
publishedArtifacts, publishedArtifacts,
verifiedArtifacts, verifiedArtifacts,
@@ -3013,7 +3080,7 @@ export function createWechatMpService({
userId: user.userId, userId: user.userId,
sessionId, sessionId,
onPageGenerated, onPageGenerated,
allowRecentArtifacts: allowHtmlArtifactRecovery, allowRecentArtifacts: allowRecentForDelivery,
htmlDeliveryAuthority, htmlDeliveryAuthority,
}); });
const linkExistsForRequest = const linkExistsForRequest =
@@ -3032,7 +3099,7 @@ export function createWechatMpService({
linkExistsForRequest, linkExistsForRequest,
{ confirmedArtifacts }, { confirmedArtifacts },
); );
const replyHasPublicLinks = hasAnyPublicHtmlLink(reply?.text); const replyHasPublicLinks = hasAnyPublicHtmlLinkInReply(reply);
const suspiciousPublishClaim = const suspiciousPublishClaim =
expectedArtifacts.length === 0 && expectedArtifacts.length === 0 &&
recentArtifacts.length === 0 && recentArtifacts.length === 0 &&
@@ -3045,7 +3112,7 @@ export function createWechatMpService({
}); });
const bareCompletionReply = isSuspiciousBareCompletionReply(reply, intent); const bareCompletionReply = isSuspiciousBareCompletionReply(reply, intent);
let publishArtifacts = let publishArtifacts =
htmlArtifactDeliveryExpected || publishedArtifacts.length > 0 effectiveHtmlDelivery || publishedArtifacts.length > 0
? selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts }) ? selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts })
: []; : [];
@@ -3094,6 +3161,23 @@ export function createWechatMpService({
} }
throw markWechatUserNotified(new Error(text)); throw markWechatUserNotified(new Error(text));
} }
} else if (
replyImpliesHtmlDelivery &&
shouldFailClosedOnMissingPublicHtmlDelivery({
reply,
confirmedArtifacts,
hasValidLinkInReply,
})
) {
const text = buildHtmlPublishFailureText();
try {
await sendWechatFailureNotice(inbound.fromUserName, text, user, {
sourceMsgId: intent.msgId,
});
} catch (sendErr) {
logger.error?.('WeChat MP reply-implied html delivery retry failure notice failed:', sendErr);
}
throw markWechatUserNotified(new Error(text));
} else if (htmlGenerationNeedsRetry || suspiciousPublishClaim) { } else if (htmlGenerationNeedsRetry || suspiciousPublishClaim) {
throw new Error(buildHtmlPublishFailureText()); throw new Error(buildHtmlPublishFailureText());
} }
+102
View File
@@ -23,6 +23,9 @@ import {
shouldRotateUnlinkedWechatRoute, shouldRotateUnlinkedWechatRoute,
shouldRetryHtmlGenerationReply, shouldRetryHtmlGenerationReply,
shouldForceNewWechatAgentSession, shouldForceNewWechatAgentSession,
hasAnyPublicHtmlLinkInReply,
replyImpliesPublicHtmlDelivery,
shouldFailClosedOnMissingPublicHtmlDelivery,
splitWechatText, splitWechatText,
verifyWechatMpSignature, verifyWechatMpSignature,
verifyWechatMpUrlChallenge, verifyWechatMpUrlChallenge,
@@ -728,6 +731,105 @@ test('shouldRetryHtmlGenerationReply allows normal H5 agent text without fake pa
); );
}); });
test('replyImpliesPublicHtmlDelivery detects public links in assistant messages for chat.general', () => {
const reply = {
text: '以下是专题报告摘要。',
messages: [
{
role: 'assistant',
content: [
{
type: 'text',
text: '完整报告:https://m.tkmind.cn/MindSpace/user-1/public/ai-out-of-control-report.html',
},
],
},
],
};
assert.equal(replyImpliesPublicHtmlDelivery(reply), true);
assert.equal(hasAnyPublicHtmlLinkInReply(reply), true);
});
test('shouldRetryHtmlGenerationReply fails closed for chat.general when reply claims missing public html', () => {
const intent = { agentText: '帮我深度搜索一下AI失控话题' };
const reply = {
text: '报告已生成。',
messages: [
{
role: 'assistant',
content: [
{
type: 'text',
text: '查看页面:https://m.tkmind.cn/MindSpace/user-1/public/ai-out-of-control-report.html',
},
],
},
],
};
assert.equal(
shouldRetryHtmlGenerationReply({
reply,
intent,
confirmedArtifacts: [],
hasValidLinkInReply: false,
}),
true,
);
assert.equal(
shouldFailClosedOnMissingPublicHtmlDelivery({
reply,
confirmedArtifacts: [],
hasValidLinkInReply: false,
}),
true,
);
});
test('replyImpliesPublicHtmlDelivery attaches delivery when confirmed artifact exists', async (t) => {
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-reply-implied-');
t.after(() => {
fs.rmSync(workspaceRoot, { recursive: true, force: true });
});
const htmlPath = `${workspaceRoot}/public/ai-out-of-control-report.html`;
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
fs.writeFileSync(htmlPath, '<!doctype html><title>AI Out Of Control</title><body>report</body>');
const reply = {
text: '以下是 AI 失控专题摘要。',
messages: [
{
role: 'assistant',
content: [
{
type: 'text',
text: '完整报告见 public/ai-out-of-control-report.html',
},
],
},
],
};
assert.equal(replyImpliesPublicHtmlDelivery(reply), true);
const prepared = prepareWechatHtmlDeliveryAtWorkspace({
reply,
publishDir: workspaceRoot,
buildCanonicalUrl: (relativePath) =>
`https://m.tkmind.cn/MindSpace/user-1/${relativePath}`,
allowRecentArtifacts: true,
});
assert.equal(prepared.confirmedArtifacts.length, 1);
assert.equal(
shouldFailClosedOnMissingPublicHtmlDelivery({
reply,
confirmedArtifacts: prepared.confirmedArtifacts,
hasValidLinkInReply: false,
}),
false,
);
const text = await maybeAttachPublishedHtmlLink(reply, {
artifacts: prepared.confirmedArtifacts,
});
assert.match(text, /https:\/\/m\.tkmind\.cn\/MindSpace\/user-1\/public\/ai-out-of-control-report\.html/);
});
test('wechat mp service rejects blocked publish claims that did not use the H5 page skill', async () => { test('wechat mp service rejects blocked publish claims that did not use the H5 page skill', async () => {
const wechatCalls = []; const wechatCalls = [];
const service = createBoundWechatService({ const service = createBoundWechatService({