fix(page-data): scope WeChat survey delivery when tool calls are hidden
Memind CI / Test, build, and release guards (push) Failing after 3s
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:
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
@@ -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 ?? ''),
|
||||
|
||||
Reference in New Issue
Block a user