feat(wechat): channel split with stub-safe page delivery
Introduce wechat/ channel package; never send stub HTML links to service-account users.
This commit was merged in pull request #2.
This commit is contained in:
+73
-22
@@ -13,6 +13,14 @@ import { buildPublicUrl, PUBLISH_ROOT_DIR } from './user-publish.mjs';
|
||||
import { buildAutoChatSkillPrefix } from './chat-skills.mjs';
|
||||
import { downloadTemporaryMedia, persistWechatImage } from './wechat-media.mjs';
|
||||
import { buildAckText } from './wechat/ack/ack-provider.mjs';
|
||||
import { classifyWechatIntent } from './wechat/intent/classifier.mjs';
|
||||
import { isPageGenerateText, isTopicResetText } from './wechat/intent/patterns.mjs';
|
||||
import { resolvePageGenerateOutcome } from './wechat/handlers/page-generate.mjs';
|
||||
import {
|
||||
buildPageGenerateAgentPrompt,
|
||||
buildPagePublishFailureText,
|
||||
} from './wechat/prompts/page-generate.mjs';
|
||||
import { selectSendableHtmlArtifacts } from './wechat/verify/page-artifact.mjs';
|
||||
|
||||
const DEFAULT_WECHAT_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/stable_token';
|
||||
const DEFAULT_WECHAT_CUSTOMER_SERVICE_URL =
|
||||
@@ -339,9 +347,7 @@ function extractHtmlWriteTargets(messages = []) {
|
||||
}
|
||||
|
||||
function looksLikeHtmlGenerationIntent(text) {
|
||||
const normalized = String(text ?? '').trim().toLowerCase();
|
||||
if (!normalized) return false;
|
||||
return /(?:生成|创建|做|写|帮我.*(?:生成|创建|做|写)).*(?:html|页面|网页|page|文件)/i.test(normalized);
|
||||
return isPageGenerateText(text);
|
||||
}
|
||||
|
||||
function looksLikeDocxDownloadIntent(text) {
|
||||
@@ -800,10 +806,7 @@ function downgradePrematurePublishClaims(text) {
|
||||
}
|
||||
|
||||
export function buildHtmlPublishFailureText() {
|
||||
return [
|
||||
'这次页面没有按 H5 里的页面技能真正生成成功,所以我先不发不一致的简版页。',
|
||||
'请直接重发一次你的页面需求,我会按和 H5 相同的 `static-page-publish` 技能链路重做。',
|
||||
].join('\n');
|
||||
return buildPagePublishFailureText();
|
||||
}
|
||||
|
||||
export function isHtmlPublishFailureMessage(message) {
|
||||
@@ -906,9 +909,7 @@ function resolveHtmlPublishArtifacts({
|
||||
}
|
||||
|
||||
function selectHtmlPublishArtifacts({ verifiedArtifacts = [], confirmedArtifacts = [] } = {}) {
|
||||
const candidates = verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts;
|
||||
const sendable = nonStubHtmlArtifacts(candidates);
|
||||
return sendable.length > 0 ? sendable : candidates;
|
||||
return selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts });
|
||||
}
|
||||
|
||||
function isStubPublicHtmlArtifact(artifact) {
|
||||
@@ -970,11 +971,7 @@ function isConnectivityTest(text) {
|
||||
}
|
||||
|
||||
function isTopicResetIntent(text) {
|
||||
const normalized = String(text ?? '').trim();
|
||||
return (
|
||||
/^(换(个)?话题|新问题|忽略之前|不管之前|重新开始|reset)$/i.test(normalized) ||
|
||||
/忽略.*之前|不要管.*之前|别管.*之前/.test(normalized)
|
||||
);
|
||||
return isTopicResetText(text);
|
||||
}
|
||||
|
||||
export function isRecoverableWechatAgentSessionError(message) {
|
||||
@@ -1943,9 +1940,10 @@ export function createWechatMpService({
|
||||
};
|
||||
|
||||
const runIntentMessage = async ({ inbound, intent, user }) => {
|
||||
const wechatIntent = classifyWechatIntent(intent);
|
||||
const resetCandidate =
|
||||
intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : '';
|
||||
const forceNew = isTopicResetIntent(resetCandidate);
|
||||
const forceNew = wechatIntent.kind === 'session.reset' || isTopicResetIntent(resetCandidate);
|
||||
let route = await ensureWechatAgentSession({
|
||||
userId: user.userId,
|
||||
openid: inbound.fromUserName,
|
||||
@@ -1993,11 +1991,15 @@ export function createWechatMpService({
|
||||
};
|
||||
try {
|
||||
const requestStartedAt = Date.now();
|
||||
const agentPrompt =
|
||||
wechatIntent.kind === 'page.generate'
|
||||
? buildPageGenerateAgentPrompt(intent, { wantsDocx: wechatIntent.wantsDocx })
|
||||
: buildWechatAgentPrompt(intent);
|
||||
const reply = await executeSessionReply(
|
||||
(pathname, init) => fetchForSession(sessionId, pathname, init),
|
||||
sessionId,
|
||||
requestId,
|
||||
buildWechatAgentPrompt(intent),
|
||||
agentPrompt,
|
||||
buildIntentMetadata(intent),
|
||||
);
|
||||
const workingDir = publishLayout?.publishDir ?? (await userAuth.resolveWorkingDir(user.userId));
|
||||
@@ -2030,7 +2032,32 @@ export function createWechatMpService({
|
||||
hasValidLinkInReply,
|
||||
});
|
||||
const bareCompletionReply = isSuspiciousBareCompletionReply(reply, intent);
|
||||
if (looksLikeHtmlGenerationIntent(intent?.agentText)) {
|
||||
let publishArtifacts = selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts });
|
||||
|
||||
if (wechatIntent.kind === 'page.generate') {
|
||||
const pageOutcome = resolvePageGenerateOutcome({
|
||||
reply,
|
||||
confirmedArtifacts,
|
||||
verifiedArtifacts,
|
||||
suspiciousPublishClaim,
|
||||
bareCompletionReply,
|
||||
htmlGenerationNeedsRetry,
|
||||
replyHasPublicLinks,
|
||||
});
|
||||
if (pageOutcome.action === 'session_retry') {
|
||||
throw new Error('stale_session_poisoned_completion');
|
||||
}
|
||||
if (pageOutcome.action === 'fail') {
|
||||
const text = pageOutcome.failureText ?? buildPagePublishFailureText();
|
||||
try {
|
||||
await sendCustomerServiceText(inbound.fromUserName, text, user);
|
||||
} catch (sendErr) {
|
||||
logger.error?.('WeChat MP page generate failure notice failed:', sendErr);
|
||||
}
|
||||
throw markWechatUserNotified(new Error(text));
|
||||
}
|
||||
publishArtifacts = pageOutcome.artifacts ?? publishArtifacts;
|
||||
} else if (looksLikeHtmlGenerationIntent(intent?.agentText)) {
|
||||
if (
|
||||
suspiciousPublishClaim ||
|
||||
bareCompletionReply ||
|
||||
@@ -2053,7 +2080,6 @@ export function createWechatMpService({
|
||||
if (reply.tokenState) {
|
||||
await userAuth.billSessionUsage(user.userId, sessionId, reply.tokenState, requestId);
|
||||
}
|
||||
const publishArtifacts = selectHtmlPublishArtifacts({ verifiedArtifacts, confirmedArtifacts });
|
||||
const finalizedReply = await maybeAttachPublishedHtmlLink(reply, {
|
||||
workingDir,
|
||||
publicBaseUrl: config.publicBaseUrl,
|
||||
@@ -2080,11 +2106,15 @@ export function createWechatMpService({
|
||||
await rememberWechatUserContext(sessionId, user, { forceBootstrap: route.isNewSession });
|
||||
const retryId = crypto.randomUUID();
|
||||
const retryStartedAt = Date.now();
|
||||
const retryPrompt =
|
||||
wechatIntent.kind === 'page.generate'
|
||||
? buildPageGenerateAgentPrompt(intent, { wantsDocx: wechatIntent.wantsDocx })
|
||||
: buildWechatAgentPrompt(intent);
|
||||
const reply = await executeSessionReply(
|
||||
(pathname, init) => fetchForSession(sessionId, pathname, init),
|
||||
sessionId,
|
||||
retryId,
|
||||
buildWechatAgentPrompt(intent),
|
||||
retryPrompt,
|
||||
buildIntentMetadata(intent),
|
||||
);
|
||||
const workingDir = publishLayout?.publishDir ?? (await userAuth.resolveWorkingDir(user.userId));
|
||||
@@ -2117,7 +2147,29 @@ export function createWechatMpService({
|
||||
hasValidLinkInReply,
|
||||
});
|
||||
const bareCompletionReply = isSuspiciousBareCompletionReply(reply, intent);
|
||||
if (looksLikeHtmlGenerationIntent(intent?.agentText)) {
|
||||
let publishArtifacts = selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts });
|
||||
|
||||
if (wechatIntent.kind === 'page.generate') {
|
||||
const pageOutcome = resolvePageGenerateOutcome({
|
||||
reply,
|
||||
confirmedArtifacts,
|
||||
verifiedArtifacts,
|
||||
suspiciousPublishClaim,
|
||||
bareCompletionReply,
|
||||
htmlGenerationNeedsRetry,
|
||||
replyHasPublicLinks,
|
||||
});
|
||||
if (pageOutcome.action === 'session_retry' || pageOutcome.action === 'fail') {
|
||||
const text = pageOutcome.failureText ?? buildPagePublishFailureText();
|
||||
try {
|
||||
await sendCustomerServiceText(inbound.fromUserName, text, user);
|
||||
} catch (sendErr) {
|
||||
logger.error?.('WeChat MP page generate retry failure notice failed:', sendErr);
|
||||
}
|
||||
throw markWechatUserNotified(new Error(text));
|
||||
}
|
||||
publishArtifacts = pageOutcome.artifacts ?? publishArtifacts;
|
||||
} else if (looksLikeHtmlGenerationIntent(intent?.agentText)) {
|
||||
if (
|
||||
suspiciousPublishClaim ||
|
||||
bareCompletionReply ||
|
||||
@@ -2140,7 +2192,6 @@ export function createWechatMpService({
|
||||
if (reply.tokenState) {
|
||||
await userAuth.billSessionUsage(user.userId, sessionId, reply.tokenState, retryId);
|
||||
}
|
||||
const publishArtifacts = selectHtmlPublishArtifacts({ verifiedArtifacts, confirmedArtifacts });
|
||||
const finalizedReply = await maybeAttachPublishedHtmlLink(reply, {
|
||||
workingDir,
|
||||
publicBaseUrl: config.publicBaseUrl,
|
||||
|
||||
Reference in New Issue
Block a user