Files
memind/scripts/help-remediate-page-delivery.mjs
T
john 8f91c52c67 feat(help): add cursor help escalation worker and chat bridge
Introduce help escalation persistence, cursor help worker processing,
optional OpenAI-compatible chat bridge, and page delivery remediation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 09:33:49 +08:00

190 lines
6.6 KiB
JavaScript

#!/usr/bin/env node
/**
* Help escalation remediation: fix delivery contract + notify user + optional Goose resend.
*
* Usage:
* node scripts/help-remediate-page-delivery.mjs \
* --user-id=<uuid> --session-id=<goose-session> --page=public/foo.html [--via-goose]
*/
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { setTimeout as sleep } from 'node:timers/promises';
import { createDbPool } from '../db.mjs';
import {
getPageDeliveryContract,
normalizeDeliveryRelativePath,
releaseMaterializedPageDeliveryContracts,
} from '../mindspace-delivery-contract.mjs';
import { createMindSpacePublicPageUrl } from '../mindspace-canonical-url.mjs';
import { createScheduleService } from '../schedule-service.mjs';
import { PUBLISH_ROOT_DIR, resolvePageDataDeliveryBaseUrl } from '../user-publish.mjs';
import { loadH5Environment } from './load-env.mjs';
import {
createAgentRun,
extractAssistantTexts,
extractPublicLinks,
getSession,
loginViaApi,
resolvePortalBase,
waitForAssistantGrowth,
waitForRunTerminal,
} from './scenario-test-lib.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
loadH5Environment(path.dirname(fileURLToPath(import.meta.url)));
function parseArgs(argv) {
const options = {
userId: '',
sessionId: '',
page: '',
viaGoose: false,
port: Number(process.env.H5_PORT ?? 8081),
username: process.env.RELEASE_GATE_SCENARIO_USERNAME ?? 'john',
password: process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? '888888',
};
for (const arg of argv) {
if (arg.startsWith('--user-id=')) options.userId = arg.slice('--user-id='.length).trim();
else if (arg.startsWith('--session-id=')) options.sessionId = arg.slice('--session-id='.length).trim();
else if (arg.startsWith('--page=')) options.page = arg.slice('--page='.length).trim();
else if (arg.startsWith('--port=')) options.port = Number(arg.slice('--port='.length));
else if (arg === '--via-goose') options.viaGoose = true;
else if (arg === '--help' || arg === '-h') {
console.log(`Usage: node scripts/help-remediate-page-delivery.mjs \\
--user-id=<uuid> --session-id=<session> --page=public/foo.html [--via-goose]`);
process.exit(0);
}
}
return options;
}
function pageTitleFromPath(relativePath) {
const base = path.basename(relativePath, '.html');
return base.replace(/[-_]+/g, ' ').trim() || '页面';
}
async function main() {
const options = parseArgs(process.argv.slice(2));
const relativePath = normalizeDeliveryRelativePath(options.page);
if (!options.userId || !relativePath) {
console.error('缺少 --user-id 或有效 --page=public/xxx.html');
process.exit(1);
}
const publishDir = path.join(root, PUBLISH_ROOT_DIR, options.userId);
const htmlPath = path.join(publishDir, relativePath);
if (!fs.existsSync(htmlPath)) {
console.error(`页面文件不存在: ${htmlPath}`);
process.exit(1);
}
const pool = createDbPool();
const scheduleService = createScheduleService(pool, {
defaultTimezone: process.env.MEMIND_DEFAULT_TIMEZONE ?? 'Asia/Shanghai',
});
try {
const released = await releaseMaterializedPageDeliveryContracts({
pool,
userId: options.userId,
relativePaths: [relativePath],
publishDir,
});
const contract = await getPageDeliveryContract({
pool,
userId: options.userId,
relativePath,
});
const publicBaseUrl = resolvePageDataDeliveryBaseUrl(process.env);
const pageUrl = createMindSpacePublicPageUrl({
publicBaseUrl,
ownerKey: options.userId,
filename: relativePath.replace(/^public\//, ''),
});
const pageTitle = pageTitleFromPath(relativePath);
const userMessage = [
'工程师已修复页面交付,请打开链接:',
`[${pageTitle}](${pageUrl})`,
'',
`页面地址:${pageUrl}`,
].join('\n');
let gooseReply = null;
if (options.viaGoose && options.sessionId) {
const baseUrl = resolvePortalBase(options.port);
const reporter = { pass() {}, fail() {} };
const auth = await loginViaApi(baseUrl, {
username: options.username,
password: options.password,
}, reporter);
if (auth.user?.id && auth.user.id !== options.userId) {
console.warn('[help-remediate] 登录用户与 --user-id 不一致,仍继续');
}
const before = await getSession(baseUrl, auth.cookie, options.sessionId);
const beforeTexts = before.ok ? extractAssistantTexts(before.session) : [];
const followUpText = '请把刚才生成的诗歌 HTML 页面链接发给我';
const run = await createAgentRun(baseUrl, auth.cookie, {
message: followUpText,
sessionId: options.sessionId,
});
const terminal = await waitForRunTerminal(
baseUrl,
auth.cookie,
run.runId,
Number(process.env.MEMIND_HELP_REMEDIATE_GOOSE_TIMEOUT_MS ?? 600_000),
);
if (terminal.status !== 'succeeded') {
throw new Error(`Goose 补救 run 失败: ${terminal.error ?? terminal.status}`);
}
const reply = await waitForAssistantGrowth(baseUrl, auth.cookie, options.sessionId, {
previousCount: beforeTexts.length,
previousCombinedLength: beforeTexts.join('\n').length,
minChars: 5,
timeoutMs: 120_000,
});
gooseReply = reply?.combined ?? null;
const links = extractPublicLinks(gooseReply ?? '', baseUrl);
if (!links.some((url) => url.includes(relativePath.replace(/^public\//, '')))) {
console.warn('[help-remediate] Goose 回复未含预期页面链接,将依赖通知补发');
}
}
await scheduleService.createUserNotification({
userId: options.userId,
channel: 'web',
notificationType: 'help_escalation_result',
title: '工程师已补发页面链接',
body: userMessage,
data: {
pageUrl,
relativePath,
sessionId: options.sessionId || null,
remediatedBy: 'help-remediate-page-delivery',
},
});
const result = {
status: 'succeeded',
userMessage: `页面链接已修复并补发。\n\n${userMessage}`,
pageUrl,
relativePath,
contractStatus: contract?.status ?? null,
releasedPaths: released,
gooseReplyPreview: gooseReply ? gooseReply.slice(0, 400) : null,
};
console.log(JSON.stringify(result, null, 2));
} finally {
await pool.end();
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack ?? error.message : error);
process.exit(1);
});