fix(wechat): complement page link delivery when reply references public html
Memind CI / Test, build, and release guards (push) Failing after 7s
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:
@@ -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
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user