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:
@@ -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,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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user