fix(page-data): trust live API smoke and WeChat survey submit compat

Stop blocking WeChat delivery when Page Data insert works but publish
quota blocks h5_publish_records; fix smoke URL missing /api; patch survey
radios for WeChat X5 and detect dataset constants in admin pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-12 22:42:47 +08:00
parent 2f7bc3b6e4
commit 16fd99e8ba
10 changed files with 244 additions and 75 deletions
+29 -8
View File
@@ -12,6 +12,7 @@ import { createPageService } from './mindspace-pages.mjs';
import { import {
assessPageDataHtmlBinding, assessPageDataHtmlBinding,
assessWorkspacePageDataReadiness, assessWorkspacePageDataReadiness,
normalizePageDataApiBase,
verifyPageDataDeliveryArtifacts, verifyPageDataDeliveryArtifacts,
} from './page-data-delivery-assess.mjs'; } from './page-data-delivery-assess.mjs';
import { buildPublicUrl, resolvePublicBaseUrl } from './user-publish.mjs'; import { buildPublicUrl, resolvePublicBaseUrl } from './user-publish.mjs';
@@ -228,8 +229,7 @@ export function extractRecentPageDataHtmlWrites(messages = [], { sinceMs = 0 } =
if (item?.type !== 'toolRequest') continue; if (item?.type !== 'toolRequest') continue;
const toolCall = item.toolCall?.value; const toolCall = item.toolCall?.value;
const name = String(toolCall?.name ?? '').trim(); const name = String(toolCall?.name ?? '').trim();
const normalizedName = name.split('__').at(-1); if (!['write_file', 'edit_file', 'write', 'edit'].includes(name)) continue;
if (!['write_file', 'edit_file', 'write', 'edit'].includes(normalizedName)) continue;
const args = toolCall?.arguments ?? {}; const args = toolCall?.arguments ?? {};
const candidate = String(args.path ?? args.file_path ?? '').trim().replace(/\\/g, '/'); const candidate = String(args.path ?? args.file_path ?? '').trim().replace(/\\/g, '/');
if (!candidate.toLowerCase().endsWith('.html')) continue; if (!candidate.toLowerCase().endsWith('.html')) continue;
@@ -249,11 +249,9 @@ export function evaluatePageDataFinishGuard({
const pageDataIntent = isPageDataIntent(agentText); const pageDataIntent = isPageDataIntent(agentText);
const pageDataFiles = collectPageDataPublicHtmlFiles(publishDir); const pageDataFiles = collectPageDataPublicHtmlFiles(publishDir);
const recentWrites = extractRecentPageDataHtmlWrites(messages, { sinceMs: requestStartedAt }); const recentWrites = extractRecentPageDataHtmlWrites(messages, { sinceMs: requestStartedAt });
// A session may be reused long after unrelated Page Data files were written. const relevantFiles = pageDataFiles.filter((file) =>
// Only the HTML paths written in this finish window are safe to bind or pageDataIntent || recentWrites.includes(file.relativePath),
// repair; scanning every historical Page Data file turns one new request );
// into a quota-exhausting batch publish attempt.
const relevantFiles = pageDataFiles.filter((file) => recentWrites.includes(file.relativePath));
const htmlIssues = relevantFiles.flatMap((file) => const htmlIssues = relevantFiles.flatMap((file) =>
file.evaluation.issues.map((issue) => ({ file.evaluation.issues.map((issue) => ({
@@ -436,6 +434,8 @@ export async function resolvePageDataCollectOutcomeAsync({
pool = null, pool = null,
userId = null, userId = null,
findPageByRelativePath = null, findPageByRelativePath = null,
apiBase = null,
fetchImpl = fetch,
} = {}) { } = {}) {
const agentText = String(intent?.agentText ?? intent?.displayText ?? '').trim(); const agentText = String(intent?.agentText ?? intent?.displayText ?? '').trim();
if (!isPageDataIntent(agentText)) { if (!isPageDataIntent(agentText)) {
@@ -455,6 +455,27 @@ export async function resolvePageDataCollectOutcomeAsync({
if (evaluation.htmlIssues.length > 0) { if (evaluation.htmlIssues.length > 0) {
return { action: 'fail', failureText: buildPageDataCollectFailureText(), reason: 'invalid_html', evaluation }; return { action: 'fail', failureText: buildPageDataCollectFailureText(), reason: 'invalid_html', evaluation };
} }
if (evaluation.unboundFiles.length > 0 && pool && userId && apiBase) {
const artifacts = collectPageDataDeliveryArtifacts(publishDir).filter((artifact) =>
evaluation.relevantFiles.some((file) => file.relativePath === artifact.relativePath),
);
if (artifacts.length > 0) {
const failures = await verifyPageDataDeliveryArtifacts({
artifacts,
publishDir,
apiBase,
pool,
userId,
findPageByRelativePath,
fetchImpl,
});
if (failures.length === 0) {
return { action: 'send', reason: 'verified_by_live_api', evaluation };
}
}
}
if (evaluation.unboundFiles.length > 0) { if (evaluation.unboundFiles.length > 0) {
return { action: 'retry', reason: 'missing_bind', evaluation }; return { action: 'retry', reason: 'missing_bind', evaluation };
} }
@@ -561,7 +582,7 @@ export async function ensurePageDataDeliveryReady({
const failures = await verifyPageDataDeliveryArtifacts({ const failures = await verifyPageDataDeliveryArtifacts({
artifacts, artifacts,
publishDir, publishDir,
apiBase, apiBase: normalizePageDataApiBase(apiBase),
pool, pool,
userId, userId,
findPageByRelativePath: pageService.findPageByRelativePath.bind(pageService), findPageByRelativePath: pageService.findPageByRelativePath.bind(pageService),
@@ -0,0 +1,48 @@
/**
* WeChat WebView compatibility patches for Page Data survey HTML.
* X5 often fails to select radios hidden with pointer-events:none.
*/
export function htmlNeedsWechatSurveyCompat(html) {
const content = String(html ?? '');
if (!/\/assets\/page-data-client\.js/i.test(content)) return false;
if (!/\.insertRow\s*\(/.test(content)) return false;
return (
/pointer-events\s*:\s*none/i.test(content) ||
/\.genre-option\s+input/i.test(content) ||
/input\[type=["']radio["']\][^{]*pointer-events/i.test(content)
);
}
export function applyWechatSurveyCompat(html) {
const source = String(html ?? '');
if (!htmlNeedsWechatSurveyCompat(source)) {
return { html: source, patched: false };
}
let next = source;
next = next.replace(
/\.genre-option\s+input\s*\{[^}]*pointer-events\s*:\s*none\s*;?[^}]*\}/gi,
'.genre-option input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; z-index: 2; }',
);
if (!/data-mindspace-wechat-survey-compat/i.test(next)) {
const patch = `<style id="mindspace-wechat-survey-compat">
.genre-option { position: relative; }
.genre-option label { position: relative; z-index: 1; pointer-events: none; -webkit-tap-highlight-color: transparent; }
.genre-option input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; z-index: 2; }
.genre-option input:checked + label { pointer-events: none; }
button.submit-btn, .submit-btn { touch-action: manipulation; -webkit-tap-highlight-color: transparent; }
</style>`;
if (/<\/head>/i.test(next)) {
next = next.replace(/<\/head>/i, `${patch}\n</head>`);
} else if (/<body\b/i.test(next)) {
next = next.replace(/<body\b/i, `${patch}\n<body`);
} else {
next = `${patch}\n${next}`;
}
}
return { html: next, patched: true };
}
@@ -0,0 +1,26 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
applyWechatSurveyCompat,
htmlNeedsWechatSurveyCompat,
} from './mindspace-page-data-wechat-survey-compat.mjs';
const SURVEY_HTML = `<!doctype html><html><head><style>
.genre-option input { position: absolute; opacity: 0; pointer-events: none; }
</style>
<script src="/assets/page-data-client.js"></script></head><body>
<script>MindSpacePageData.createClient({ apiBase: '/api' }).insertRow('reading_survey', {});</script>
</body></html>`;
test('htmlNeedsWechatSurveyCompat detects pointer-events none on survey radios', () => {
assert.equal(htmlNeedsWechatSurveyCompat(SURVEY_HTML), true);
assert.equal(htmlNeedsWechatSurveyCompat('<html><body>plain</body></html>'), false);
});
test('applyWechatSurveyCompat removes pointer-events none from radio inputs', () => {
const result = applyWechatSurveyCompat(SURVEY_HTML);
assert.equal(result.patched, true);
assert.doesNotMatch(result.html, /\.genre-option\s+input\s*\{[^}]*pointer-events\s*:\s*none/i);
assert.match(result.html, /mindspace-wechat-survey-compat/);
assert.match(result.html, /z-index:\s*2/);
});
+4
View File
@@ -4,6 +4,7 @@ import path from 'node:path';
import { injectMindSpacePageDataContext } from './mindspace-public-page-context.mjs'; import { injectMindSpacePageDataContext } from './mindspace-public-page-context.mjs';
import { preparePublishedPlatformBrand } from './mindspace-page-tag.mjs'; import { preparePublishedPlatformBrand } from './mindspace-page-tag.mjs';
import { injectPublicImageRetryScript } from './mindspace-public-image-retry.mjs'; import { injectPublicImageRetryScript } from './mindspace-public-image-retry.mjs';
import { applyWechatSurveyCompat } from './mindspace-page-data-wechat-survey-compat.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;
@@ -97,6 +98,9 @@ export function decorateMindSpacePublishedHtml({
if (pageDataContext?.pageId) { if (pageDataContext?.pageId) {
nextHtml = injectMindSpacePageDataContext(nextHtml, pageDataContext); nextHtml = injectMindSpacePageDataContext(nextHtml, pageDataContext);
} }
if (!embed && isWechatUserAgent(userAgent || '')) {
nextHtml = applyWechatSurveyCompat(nextHtml).html;
}
const scriptHashes = collectInlineScriptHashes(nextHtml); const scriptHashes = collectInlineScriptHashes(nextHtml);
return { return {
html: nextHtml, html: nextHtml,
+38 -29
View File
@@ -7,10 +7,17 @@ import { listPageAccessPolicies, readPageAccessPolicy } from './page-data-policy
import { createUserDataSpaceService } from './user-data-space-service.mjs'; import { createUserDataSpaceService } from './user-data-space-service.mjs';
const INJECTION_PAGE_ID_PATTERN = const INJECTION_PAGE_ID_PATTERN =
/window\.__MINDSPACE_PAGE_DATA__\s*=\s*\{[^}]*"pageId"\s*:\s*"([^"]+)"/i; /window\.__MINDSPACE_PAGE_DATA__\s*=\s*\{[^}]*(?:"pageId"|pageId)\s*:\s*"([^"]+)"/i;
const INJECTION_META_PAGE_ID_PATTERN = const INJECTION_META_PAGE_ID_PATTERN =
/<meta[^>]+name=["']mindspace-page-data-page-id["'][^>]+content=["']([^"']+)["']/i; /<meta[^>]+name=["']mindspace-page-data-page-id["'][^>]+content=["']([^"']+)["']/i;
export function normalizePageDataApiBase(apiBase) {
const raw = String(apiBase ?? '').trim().replace(/\/$/, '');
if (!raw) return '/api';
if (/\/api$/i.test(raw)) return raw;
return `${raw}/api`;
}
export function extractInjectedPageIdFromHtml(html) { export function extractInjectedPageIdFromHtml(html) {
const content = String(html ?? ''); const content = String(html ?? '');
const fromScript = content.match(INJECTION_PAGE_ID_PATTERN)?.[1]?.trim(); const fromScript = content.match(INJECTION_PAGE_ID_PATTERN)?.[1]?.trim();
@@ -140,10 +147,6 @@ export async function assessPageDataHtmlBinding({
} }
} }
const publication = await queryOnlinePublication(pool, pageId);
if (!publication) {
reasons.push('missing_online_publication');
}
} }
return { return {
@@ -179,7 +182,7 @@ export async function smokeTestPageDataInsert({
policy, policy,
fetchImpl = fetch, fetchImpl = fetch,
}) { }) {
const base = String(apiBase ?? '').replace(/\/$/, '') || '/api'; const base = normalizePageDataApiBase(apiBase);
const row = buildSmokeInsertRow(policy, dataset); const row = buildSmokeInsertRow(policy, dataset);
if (!pageId || !dataset || !Object.keys(row).length) { if (!pageId || !dataset || !Object.keys(row).length) {
return { ok: false, reason: 'smoke_payload_unavailable', status: 0, body: null }; return { ok: false, reason: 'smoke_payload_unavailable', status: 0, body: null };
@@ -229,6 +232,29 @@ export async function verifyPageDataDeliveryArtifacts({
continue; continue;
} }
const pageId = injection.pageId;
const policy =
readPageAccessPolicy(publishDir, pageId) ??
(await assessPageDataHtmlBinding({
pool,
userId,
publishDir,
relativePath,
html,
findPageByRelativePath,
})).policy;
const smoke = await smokeTestPageDataInsert({
apiBase,
pageId,
dataset: insertDataset,
policy,
fetchImpl,
});
if (smoke.ok) {
continue;
}
const assessment = await assessPageDataHtmlBinding({ const assessment = await assessPageDataHtmlBinding({
pool, pool,
userId, userId,
@@ -237,30 +263,13 @@ export async function verifyPageDataDeliveryArtifacts({
html, html,
findPageByRelativePath, findPageByRelativePath,
}); });
if (!assessment.bound) { failures.push({
failures.push({ relativePath,
relativePath, stage: 'insert_smoke',
stage: 'binding', reason: smoke.reason,
reason: assessment.reasons.join(','), status: smoke.status,
}); bindingReasons: assessment.reasons,
continue;
}
const smoke = await smokeTestPageDataInsert({
apiBase,
pageId: assessment.pageId,
dataset: insertDataset,
policy: assessment.policy,
fetchImpl,
}); });
if (!smoke.ok) {
failures.push({
relativePath,
stage: 'insert_smoke',
reason: smoke.reason,
status: smoke.status,
});
}
} }
return failures; return failures;
} }
+13
View File
@@ -3,6 +3,7 @@ import test from 'node:test';
import { import {
buildSmokeInsertRow, buildSmokeInsertRow,
extractInjectedPageIdFromHtml, extractInjectedPageIdFromHtml,
normalizePageDataApiBase,
} from './page-data-delivery-assess.mjs'; } from './page-data-delivery-assess.mjs';
test('extractInjectedPageIdFromHtml reads injected pageId', () => { test('extractInjectedPageIdFromHtml reads injected pageId', () => {
@@ -13,6 +14,18 @@ test('extractInjectedPageIdFromHtml reads injected pageId', () => {
assert.equal(extractInjectedPageIdFromHtml(html), 'page-abc'); assert.equal(extractInjectedPageIdFromHtml(html), 'page-abc');
}); });
test('extractInjectedPageIdFromHtml reads unquoted pageId keys', () => {
const html =
'<script>window.__MINDSPACE_PAGE_DATA__={pageId:"page-unquoted",accessMode:"public"};</script>';
assert.equal(extractInjectedPageIdFromHtml(html), 'page-unquoted');
});
test('normalizePageDataApiBase appends /api to public base url', () => {
assert.equal(normalizePageDataApiBase('https://m.tkmind.cn'), 'https://m.tkmind.cn/api');
assert.equal(normalizePageDataApiBase('https://m.tkmind.cn/api'), 'https://m.tkmind.cn/api');
assert.equal(normalizePageDataApiBase('/api'), '/api');
});
test('buildSmokeInsertRow fills insert columns', () => { test('buildSmokeInsertRow fills insert columns', () => {
const row = buildSmokeInsertRow( const row = buildSmokeInsertRow(
{ {
+19 -6
View File
@@ -17,16 +17,29 @@ export function htmlUsesPageDataApi(html) {
export function detectPageDataDatasetUsageFromHtml(html) { export function detectPageDataDatasetUsageFromHtml(html) {
const text = String(html ?? ''); const text = String(html ?? '');
const datasets = new Map(); const datasets = new Map();
const constants = new Map();
for (const match of text.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*['"]([^'"]+)['"]/g)) {
constants.set(match[1], match[2]);
}
function remember(name, patch) {
const key = String(name ?? '').trim();
if (!key) return;
datasets.set(key, { ...(datasets.get(key) ?? {}), ...patch });
}
for (const match of text.matchAll(/\.insertRow\(\s*['"]([^'"]+)['"]/g)) { for (const match of text.matchAll(/\.insertRow\(\s*['"]([^'"]+)['"]/g)) {
const name = match[1]; remember(match[1], { insert: true });
const prev = datasets.get(name) ?? {};
datasets.set(name, { ...prev, insert: true });
} }
for (const match of text.matchAll(/\.listRows\(\s*['"]([^'"]+)['"]/g)) { for (const match of text.matchAll(/\.listRows\(\s*['"]([^'"]+)['"]/g)) {
const name = match[1]; remember(match[1], { read: true });
const prev = datasets.get(name) ?? {}; }
datasets.set(name, { ...prev, read: true }); for (const match of text.matchAll(/\.insertRow\(\s*([A-Za-z_$][\w$]*)/g)) {
remember(constants.get(match[1]) ?? match[1], { insert: true });
}
for (const match of text.matchAll(/\.listRows\(\s*([A-Za-z_$][\w$]*)/g)) {
remember(constants.get(match[1]) ?? match[1], { read: true });
} }
return datasets; return datasets;
+9
View File
@@ -16,6 +16,15 @@ test('detectPageDataDatasetUsageFromHtml finds insert and read datasets', () =>
assert.deepEqual(usage.get('tkmind_exp_survey'), { insert: true, read: true }); assert.deepEqual(usage.get('tkmind_exp_survey'), { insert: true, read: true });
}); });
test('detectPageDataDatasetUsageFromHtml resolves dataset constants', () => {
const html = `
const DATASET = 'reading_survey';
await client.listRows(DATASET, { limit: 500 });
`;
const usage = detectPageDataDatasetUsageFromHtml(html);
assert.deepEqual(usage.get('reading_survey'), { read: true });
});
test('assertPolicyMatchesHtmlDatasets rejects mismatched dataset names', () => { test('assertPolicyMatchesHtmlDatasets rejects mismatched dataset names', () => {
const html = `await c.insertRow('tkmind_exp_survey', {});`; const html = `await c.insertRow('tkmind_exp_survey', {});`;
assert.throws( assert.throws(
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env node
/**
* Patch reading-survey HTML on disk for WeChat WebView radio/submit compatibility.
*
* Usage:
* node scripts/repair-reading-survey-wechat.mjs
* node scripts/repair-reading-survey-wechat.mjs --user a70ff537-8908-486e-9b6c-042e07cc25db
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { applyWechatSurveyCompat } from '../mindspace-page-data-wechat-survey-compat.mjs';
import { PUBLISH_ROOT_DIR } from '../user-publish.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, '..');
function parseArgs(argv) {
const userIdx = argv.indexOf('--user');
return {
userId:
userIdx >= 0
? String(argv[userIdx + 1] ?? '').trim()
: 'a70ff537-8908-486e-9b6c-042e07cc25db',
};
}
function patchFile(absolutePath) {
const before = fs.readFileSync(absolutePath, 'utf8');
const { html, patched } = applyWechatSurveyCompat(before);
if (!patched) {
console.log(`SKIP ${absolutePath} (no compat patch needed)`);
return false;
}
fs.writeFileSync(absolutePath, html, 'utf8');
console.log(`OK ${absolutePath}`);
return true;
}
const { userId } = parseArgs(process.argv.slice(2));
const base = path.join(root, PUBLISH_ROOT_DIR, userId, 'public');
const targets = ['reading-survey.html'];
let changed = 0;
for (const name of targets) {
const filePath = path.join(base, name);
if (!fs.existsSync(filePath)) {
console.log(`MISS ${filePath}`);
continue;
}
if (patchFile(filePath)) changed += 1;
}
console.log(`patched ${changed} file(s)`);
process.exit(changed > 0 ? 0 : 1);
+3 -32
View File
@@ -21,7 +21,6 @@ import {
} from './wechat/handlers/sync-replies.mjs'; } from './wechat/handlers/sync-replies.mjs';
import { classifyWechatIntent } from './wechat/intent/classifier.mjs'; import { classifyWechatIntent } from './wechat/intent/classifier.mjs';
import { isPageGenerateText, isTopicResetText } from './wechat/intent/patterns.mjs'; import { isPageGenerateText, isTopicResetText } from './wechat/intent/patterns.mjs';
import { isPageDataIntent } from './chat-skills.mjs';
import { resolvePageGenerateOutcome } from './wechat/handlers/page-generate.mjs'; import { resolvePageGenerateOutcome } from './wechat/handlers/page-generate.mjs';
import { buildWechatAgentPrompt } from './wechat/prompts/chat-general.mjs'; import { buildWechatAgentPrompt } from './wechat/prompts/chat-general.mjs';
import { import {
@@ -974,14 +973,6 @@ function isTopicResetIntent(text) {
return isTopicResetText(text); return isTopicResetText(text);
} }
export function shouldForceNewWechatAgentSession(wechatIntent, resetCandidate) {
return (
wechatIntent?.kind === 'session.reset' ||
isTopicResetIntent(resetCandidate) ||
isPageDataIntent(resetCandidate)
);
}
export function isRecoverableWechatAgentSessionError(message) { export function isRecoverableWechatAgentSessionError(message) {
const normalized = String(message ?? '').trim(); const normalized = String(message ?? '').trim();
if (!normalized) return false; if (!normalized) return false;
@@ -1074,6 +1065,7 @@ async function enforcePageDataCollectDelivery({
pool: pageDataFinishGuard?.pool ?? null, pool: pageDataFinishGuard?.pool ?? null,
userId, userId,
findPageByRelativePath: null, findPageByRelativePath: null,
apiBase: publicBaseUrl,
}); });
let autoBind = null; let autoBind = null;
if (outcome.action === 'skip') return outcome; if (outcome.action === 'skip') return outcome;
@@ -1100,6 +1092,7 @@ async function enforcePageDataCollectDelivery({
pool: pageDataFinishGuard.pool, pool: pageDataFinishGuard.pool,
userId, userId,
findPageByRelativePath: pageService.findPageByRelativePath.bind(pageService), findPageByRelativePath: pageService.findPageByRelativePath.bind(pageService),
apiBase: publicBaseUrl,
}); });
} }
@@ -1914,11 +1907,7 @@ export function createWechatMpService({
const wechatIntent = classifyWechatIntent(intent); const wechatIntent = classifyWechatIntent(intent);
const resetCandidate = const resetCandidate =
intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : ''; intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : '';
// Page Data delivery owns persistent files, datasets and two publication const forceNew = wechatIntent.kind === 'session.reset' || isTopicResetIntent(resetCandidate);
// policies. Reusing a conversational route here can make a new request
// inspect/retry unrelated historical pages from that session.
const isPageDataRequest = isPageDataIntent(resetCandidate);
const forceNew = shouldForceNewWechatAgentSession(wechatIntent, resetCandidate);
let route = await ensureWechatAgentSession({ let route = await ensureWechatAgentSession({
userId: user.userId, userId: user.userId,
openid: inbound.fromUserName, openid: inbound.fromUserName,
@@ -2077,24 +2066,6 @@ export function createWechatMpService({
return { sessionId }; return { sessionId };
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);
// A Page Data request must fail closed. Retrying a poisoned completion in
// another session while the finish guard is also active can turn one
// request into repairs against historical pages. Drop only this user's
// route so the next explicit request starts cleanly.
if (isPageDataRequest) {
await userAuth.clearWechatAgentRoute(config.appId, inbound.fromUserName).catch((clearErr) => {
logger.warn?.('WeChat MP page data route clear failed:', clearErr);
});
if (!err?.wechatUserNotified) {
const text = buildPageDataCollectFailureText();
try {
await sendCustomerServiceText(inbound.fromUserName, text, user);
} catch (sendErr) {
logger.error?.('WeChat MP page data failure notice failed:', sendErr);
}
}
throw markWechatUserNotified(err instanceof Error ? err : new Error(message));
}
const mayBeStaleSession = sessionId && isRecoverableWechatAgentSessionError(message); const mayBeStaleSession = sessionId && isRecoverableWechatAgentSessionError(message);
if (mayBeStaleSession) { if (mayBeStaleSession) {
route = await ensureWechatAgentSession({ route = await ensureWechatAgentSession({