fix(page-data): scope WeChat survey delivery when tool calls are hidden
Memind CI / Test, build, and release guards (push) Failing after 3s

Collect request-scoped tool messages in the WeChat SSE stream, infer this
turn Page Data HTML via mtime when messages lack writes, and limit bind and
delivery smoke to those paths so historical already_bound pages cannot block new surveys.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-02 21:41:00 +08:00
parent 762453852f
commit 5ead1abf7f
4 changed files with 142 additions and 25 deletions
+54 -13
View File
@@ -110,18 +110,20 @@ export function rewritePageDataDeliveryLinks(text, artifacts = []) {
function collectPageDataDeliveryRelativePaths(autoBind, relevantRelativePaths = null) {
const deliveryPaths = new Set();
for (const item of autoBind?.bound ?? []) {
if (item?.relativePath) deliveryPaths.add(item.relativePath);
}
const scope = Array.isArray(relevantRelativePaths) && relevantRelativePaths.length > 0
? new Set(relevantRelativePaths)
: null;
const include = (relativePath) => {
if (!relativePath) return;
if (scope && !scope.has(relativePath)) return;
deliveryPaths.add(relativePath);
};
for (const item of autoBind?.bound ?? []) include(item.relativePath);
for (const item of autoBind?.skipped ?? []) {
if (item?.reason === 'already_bound' && item?.relativePath) {
deliveryPaths.add(item.relativePath);
}
if (item?.reason === 'already_bound') include(item.relativePath);
}
if (Array.isArray(relevantRelativePaths)) {
for (const relativePath of relevantRelativePaths) {
if (relativePath) deliveryPaths.add(relativePath);
}
if (scope) {
for (const relativePath of scope) include(relativePath);
}
return deliveryPaths;
}
@@ -339,6 +341,43 @@ export function extractRecentPageDataBindTargets(messages = [], { sinceMs = 0 }
return [...targets];
}
const PAGE_DATA_WORKSPACE_MTIME_LOOKBACK_MS = 2000;
export function resolvePageDataRequestRelativePaths({
publishDir,
messages = [],
requestStartedAt = 0,
} = {}) {
const relevantPaths = new Set([
...extractRecentPageDataHtmlWrites(messages, { sinceMs: requestStartedAt }),
...extractRecentPageDataBindTargets(messages, { sinceMs: requestStartedAt }),
]);
if (relevantPaths.size > 0 || requestStartedAt <= 0) {
return [...relevantPaths];
}
const publicDir = path.join(path.resolve(String(publishDir ?? '')), 'public');
if (!fs.existsSync(publicDir) || !fs.statSync(publicDir).isDirectory()) {
return [];
}
for (const name of fs.readdirSync(publicDir)) {
if (!name.toLowerCase().endsWith('.html')) continue;
const relativePath = `public/${name}`;
const absolutePath = path.join(publicDir, name);
let mtimeMs = 0;
try {
mtimeMs = fs.statSync(absolutePath).mtimeMs;
} catch {
continue;
}
if (mtimeMs < requestStartedAt - PAGE_DATA_WORKSPACE_MTIME_LOOKBACK_MS) continue;
const content = fs.readFileSync(absolutePath, 'utf8');
if (htmlUsesPageDataApi(content)) {
relevantPaths.add(relativePath);
}
}
return [...relevantPaths];
}
function isStructuralPageDataHtmlFile(file) {
return Boolean(file?.evaluation?.usage?.size > 0 || file?.evaluation?.usesPageDataApi);
}
@@ -371,9 +410,11 @@ export function evaluatePageDataFinishGuard({
const pgRequired = isPgRequiredByMessage(messages);
const pageDataIntent = pgRequired || isPageDataIntent(resolvedAgentText);
const pageDataFiles = collectPageDataPublicHtmlFiles(publishDir);
const recentWrites = extractRecentPageDataHtmlWrites(messages, { sinceMs: requestStartedAt });
const recentBinds = extractRecentPageDataBindTargets(messages, { sinceMs: requestStartedAt });
const relevantPaths = new Set([...recentWrites, ...recentBinds]);
const relevantPaths = new Set(resolvePageDataRequestRelativePaths({
publishDir,
messages,
requestStartedAt,
}));
// A reused session/workspace can contain many historical Page Data pages.
// Only files touched or explicitly bound in this request belong to this
// delivery. Scanning the whole workspace lets one stale localStorage page
+60
View File
@@ -15,6 +15,7 @@ import {
extractRecentPageDataHtmlWrites,
inferPageDataBindAccessMode,
maybeAutoBindPageDataHtmlPages,
resolvePageDataRequestRelativePaths,
rewritePageDataDeliveryLinks,
shouldRetryPageDataCollectReply,
} from './mindspace-page-data-finish-guard.mjs';
@@ -381,3 +382,62 @@ test('rewritePageDataDeliveryLinks rewrites publication route urls to MindSpace
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('resolvePageDataRequestRelativePaths falls back to recent workspace html when tool calls are missing', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-mtime-'));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
const requestStartedAt = Date.now();
const historicalPath = path.join(publishDir, 'public', 'historical-survey.html');
const freshPath = path.join(publishDir, 'public', 'fresh-survey.html');
fs.writeFileSync(historicalPath, SURVEY_HTML, 'utf8');
fs.utimesSync(
historicalPath,
new Date(requestStartedAt - 60_000),
new Date(requestStartedAt - 60_000),
);
fs.writeFileSync(freshPath, SURVEY_HTML, 'utf8');
assert.deepEqual(
resolvePageDataRequestRelativePaths({
publishDir,
messages: [],
requestStartedAt,
}),
['public/fresh-survey.html'],
);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('buildPageDataDeliveryArtifactsFromBindResult scopes already_bound pages to this request', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-guard-scope-'));
const previousBase = process.env.H5_PUBLIC_BASE_URL;
process.env.H5_PUBLIC_BASE_URL = 'https://m.tkmind.cn';
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(path.join(publishDir, 'public', 'current-survey.html'), SURVEY_HTML, 'utf8');
fs.writeFileSync(path.join(publishDir, 'public', 'historical-survey.html'), SURVEY_HTML, 'utf8');
const autoBind = {
bound: [],
skipped: [
{ relativePath: 'public/current-survey.html', reason: 'already_bound' },
{ relativePath: 'public/historical-survey.html', reason: 'already_bound' },
],
};
const artifacts = buildPageDataDeliveryArtifactsFromBindResult(
autoBind,
publishDir,
{},
['public/current-survey.html'],
);
assert.deepEqual(
artifacts.map((artifact) => artifact.relativePath),
['public/current-survey.html'],
);
} finally {
if (previousBase == null) delete process.env.H5_PUBLIC_BASE_URL;
else process.env.H5_PUBLIC_BASE_URL = previousBase;
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
+22 -6
View File
@@ -16,6 +16,7 @@ import {
maybeAutoBindPageDataHtmlPages,
preparePageDataAfterFinish,
resolvePageDataCollectOutcomeAsync,
resolvePageDataRequestRelativePaths,
rewritePageDataDeliveryLinks,
} from './mindspace-page-data-finish-guard.mjs';
import {
@@ -253,6 +254,11 @@ export function createMindSpacePublicFinishService({
intent?.displayText ?? '',
),
};
const scopedRelativePaths = resolvePageDataRequestRelativePaths({
publishDir,
messages: normalizedReply.messages,
requestStartedAt,
});
let outcome =
await resolvePageDataCollectOutcomeAsyncFn({
reply: normalizedReply,
@@ -283,9 +289,14 @@ export function createMindSpacePublicFinishService({
pageService.findPageByRelativePath.bind(
pageService,
);
const relevantRelativePaths = (outcome?.evaluation?.relevantFiles ?? [])
.map((file) => file?.relativePath)
.filter(Boolean);
const relevantRelativePaths = [
...new Set([
...scopedRelativePaths,
...(outcome?.evaluation?.relevantFiles ?? [])
.map((file) => file?.relativePath)
.filter(Boolean),
]),
];
autoBind =
await maybeAutoBindPageDataHtmlPagesFn({
pool,
@@ -315,9 +326,14 @@ export function createMindSpacePublicFinishService({
let deliveryCheck = null;
let rewrittenText = normalizedReply.text;
if (outcome?.action === 'send') {
const deliveryRelativePaths = (outcome?.evaluation?.relevantFiles ?? [])
.map((file) => file?.relativePath)
.filter(Boolean);
const deliveryRelativePaths = [
...new Set([
...scopedRelativePaths,
...(outcome?.evaluation?.relevantFiles ?? [])
.map((file) => file?.relativePath)
.filter(Boolean),
]),
];
deliveryArtifacts =
buildPageDataDeliveryArtifactsFromBindResultFn(
autoBind,
+6 -6
View File
@@ -345,14 +345,16 @@ export async function executeSessionReply(
const routingId = event.chat_request_id ?? event.request_id;
if (routingId && routingId !== requestId) continue;
if (event.type === 'Message' && event.message?.metadata?.userVisible) {
if (event.type === 'Message' && event.message) {
const hasActionRequired = event.message.content?.some((item) => item.type === 'actionRequired');
if (hasActionRequired) {
throw new Error('当前回复需要人工确认,公众号通道暂不支持');
}
if (event.message.role === 'assistant') hasScopedAssistantUpdate = true;
messages = pushMessage(messages, event.message);
requestMessages = pushMessage(requestMessages, event.message);
if (event.message?.metadata?.userVisible) {
if (event.message.role === 'assistant') hasScopedAssistantUpdate = true;
messages = pushMessage(messages, event.message);
}
} else if (event.type === 'UpdateConversation') {
// Ignore unscoped snapshots until this request has yielded an assistant update.
// Otherwise a stale session snapshot can overwrite the current reply with a
@@ -1213,9 +1215,7 @@ async function enforcePageDataCollectDelivery({
userId,
reply: {
text: String(reply?.text ?? ''),
messages: Array.isArray(reply?.messages)
? reply.messages
: [],
messages: replyRequestMessages(reply),
},
intent: {
agentText: String(intent?.agentText ?? ''),