Files
memind/scripts/regenerate-page-goose-test.mjs
john 9b4a25799f Add smart ACK provider for WeChat MP replies
Replace fixed ackText with a rule-based AckProvider that picks
response templates by message type and intent (translate, summary,
rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync,
zero I/O, auto-falls back to config.ackText on any error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 15:19:03 +08:00

287 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 通过 goose 子会话重新生成 MindSpace 页面内容(Studio / RDS 环境测试)
*
* 用法:
* node scripts/regenerate-page-goose-test.mjs [pageId] [parentSessionId]
*
* 默认:探索世界 · TKMindjohnsource_session 20260614_70
*/
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { Readable } from 'node:stream';
import { fileURLToPath } from 'node:url';
import { Agent, fetch as undiciFetch } from 'undici';
import { createDbPool } from '../db.mjs';
import { createUserAuth } from '../user-auth.mjs';
import { createPageService } from '../mindspace-pages.mjs';
import { createPageLiveEditService } from '../mindspace-page-live-edit.mjs';
import { createPageEditSessionService } from '../mindspace-page-edit-session.mjs';
import { createLlmProviderService } from '../llm-providers.mjs';
import { extractMindSpacePagePatch } from '../mindspace-page-patch.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
function loadEnvFile(filePath) {
if (!fs.existsSync(filePath)) return;
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq < 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1).trim();
if (!process.env[key]) process.env[key] = value;
}
}
loadEnvFile(path.join(root, '.env'));
const PAGE_ID = process.argv[2] ?? '44a8d4f5-7dda-4e0c-973a-c580bf2cb82b';
const PARENT_SESSION = process.argv[3] ?? '20260614_70';
const USERNAME = 'john';
const apiTarget = process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006';
const apiSecret = process.env.TKMIND_SERVER__SECRET_KEY ?? '';
const h5ApiBase = (process.env.H5_PAGE_PATCH_BASE ?? 'http://127.0.0.1:8081').replace(/\/$/, '');
const insecureDispatcher = new Agent({ connect: { rejectUnauthorized: false } });
function isHttpsTarget(target) {
return target.startsWith('https://');
}
function createApiFetch() {
return async (pathname, init = {}) => {
const url = new URL(pathname, apiTarget);
const headers = {
...(init.headers ?? {}),
'X-Secret-Key': apiSecret,
};
if (init.body && !headers['Content-Type']) {
headers['Content-Type'] = 'application/json';
}
return undiciFetch(url, {
...init,
headers,
dispatcher: isHttpsTarget(apiTarget) ? insecureDispatcher : undefined,
});
};
}
async function executeSessionReply(apiFetch, sessionId, requestId, prompt) {
const eventsResponse = await apiFetch(`/sessions/${sessionId}/events`, {
method: 'GET',
headers: { Accept: 'text/event-stream' },
});
if (!eventsResponse.ok || !eventsResponse.body) {
const text = await eventsResponse.text().catch(() => '');
throw new Error(text || '无法建立事件流');
}
const replyResponse = await apiFetch(`/sessions/${sessionId}/reply`, {
method: 'POST',
body: JSON.stringify({
request_id: requestId,
user_message: {
role: 'user',
created: Date.now(),
content: [{ type: 'text', text: prompt }],
metadata: { userVisible: true, agentVisible: true },
},
}),
});
if (!replyResponse.ok) {
const text = await replyResponse.text().catch(() => '');
throw new Error(text || 'reply 失败');
}
replyResponse.body?.cancel?.();
const reader = Readable.fromWeb(eventsResponse.body);
const decoder = new TextDecoder();
let buffer = '';
let messages = [];
const pushMessage = (list, message) => {
const idx = list.findIndex((item) => item.id === message.id);
if (idx >= 0) {
const next = [...list];
next[idx] = message;
return next;
}
return [...list, message];
};
const messageVisibleText = (message) =>
(message?.content ?? [])
.filter((item) => item.type === 'text')
.map((item) => item.text)
.join('\n')
.trim();
for await (const chunk of reader) {
buffer += decoder.decode(chunk, { stream: true });
const frames = buffer.split('\n\n');
buffer = frames.pop() ?? '';
for (const frame of frames) {
let data = '';
for (const line of frame.split('\n')) {
if (line.startsWith('data:')) data += line.slice(5).trim();
}
if (!data) continue;
let event;
try {
event = JSON.parse(data);
} catch {
continue;
}
const routingId = event.chat_request_id ?? event.request_id;
if (routingId && routingId !== requestId) continue;
if (event.type === 'Message' && event.message?.metadata?.userVisible) {
messages = pushMessage(messages, event.message);
} else if (event.type === 'UpdateConversation') {
messages = (event.conversation ?? []).filter((item) => item.metadata?.userVisible);
} else if (event.type === 'Error') {
throw new Error(event.error || 'goose 执行失败');
} else if (event.type === 'Finish') {
const assistantTexts = messages
.filter((item) => item.role === 'assistant')
.map((item) => messageVisibleText(item))
.filter(Boolean);
const combined = assistantTexts.join('\n\n');
const withPatch = [...assistantTexts].reverse().find((text) => text.includes('mindspace-page-update'));
return { text: withPatch ?? combined ?? messageVisibleText([...messages].reverse().find((item) => item.role === 'assistant')) };
}
}
}
throw new Error('goose 回复未完成');
}
async function main() {
const pool = createDbPool();
const userAuth = createUserAuth(pool, { h5Root: root });
const pageService = createPageService(pool, {
h5Root: root,
storageRoot: process.env.MINDSPACE_STORAGE_ROOT ?? path.join(root, 'data', 'mindspace'),
});
const pageLiveEdit = createPageLiveEditService({
pageService,
resolveUserIdForAgentSession: async (sessionId) => {
const [rows] = await pool.query(
`SELECT user_id FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`,
[sessionId],
);
return rows[0]?.user_id ?? null;
},
});
const llmProviderService = createLlmProviderService(pool, {
apiTarget,
apiSecret,
encryptionKey:
process.env.H5_SETTINGS_ENCRYPTION_KEY ?? process.env.TKMIND_SERVER__SECRET_KEY ?? '',
});
const pageEditSession = createPageEditSessionService({
apiTarget,
apiSecret,
userAuth,
pageService,
pageLiveEdit,
llmProviderService,
});
const apiFetch = createApiFetch();
const [userRows] = await pool.query(`SELECT id FROM h5_users WHERE username = ? LIMIT 1`, [
USERNAME,
]);
const userId = userRows[0]?.id;
if (!userId) throw new Error(`用户 ${USERNAME} 不存在`);
const pageBefore = await pageService.getPage(userId, PAGE_ID);
console.log('目标页面:', pageBefore.title, `(v${pageBefore.versionNo})`);
console.log('父 goose 会话:', PARENT_SESSION);
const forked = await pageEditSession.forkSession({
userId,
pageId: PAGE_ID,
parentSessionId: PARENT_SESSION,
h5ApiBase,
});
console.log('✓ 子会话已 fork:', forked.sessionId);
const contentPreview = String(pageBefore.content ?? '').slice(0, 12000);
const prompt = `请基于下方【当前页面 HTML】全面升级为更精美的旅行主题落地页:
- 深色沉浸式 Hero + 玻璃拟态卡片 + 渐变强调文字
- 保留瑞士、马尔代夫、京都三个目的地
- 必须包含 mindspace-cover meta 与完整内联 CSSCSP 安全,禁止外部 JS)
- 标题「探索世界 · TKMind」,summary 写一句精炼中文
**禁止**读写工作区文件。完成后**只**在回复末尾输出一个 \`\`\`mindspace-page-update\`\`\` 代码块(JSONtitle、summary、content 完整 HTML)。
【当前页面 HTML】
${contentPreview}`;
const requestId = crypto.randomUUID();
console.log('→ 向 goose 发送重生成指令…');
const reply = await executeSessionReply(apiFetch, forked.sessionId, requestId, prompt);
console.log('✓ goose 回复完成,文本长度:', reply.text.length);
console.log(reply.text.slice(-1200));
let pageAfter = await pageService.getPage(userId, PAGE_ID);
let changed =
pageAfter.versionNo !== pageBefore.versionNo ||
pageAfter.updatedAt !== pageBefore.updatedAt ||
pageAfter.content !== pageBefore.content;
if (!changed) {
const patch = extractMindSpacePagePatch(reply.text);
if (patch) {
console.log('→ 从 mindspace-page-update 块应用补丁…');
await pageLiveEdit.applyAgentPatch({
sessionId: forked.sessionId,
pageId: PAGE_ID,
...patch,
changeNote: 'goose 重生成精美旅行页',
});
pageAfter = await pageService.getPage(userId, PAGE_ID);
changed =
pageAfter.versionNo !== pageBefore.versionNo ||
pageAfter.updatedAt !== pageBefore.updatedAt ||
pageAfter.content !== pageBefore.content;
}
}
console.log('\n结果:');
console.log(' 版本:', pageBefore.versionNo, '→', pageAfter.versionNo);
console.log(' 摘要:', pageAfter.summary?.slice(0, 120));
console.log(' 内容已变更:', changed ? '是' : '否');
const publicBase = (process.env.H5_PUBLIC_BASE_URL ?? 'https://58.38.22.103').replace(/\/$/, '');
const [pubRows] = await pool.query(
`SELECT url_slug, status FROM h5_publish_records WHERE id = (SELECT current_publish_id FROM h5_page_records WHERE id = ?)`,
[PAGE_ID],
);
const slug = pubRows[0]?.url_slug;
if (slug) {
console.log(' 公开预览:', `${publicBase}/p/${slug}`);
}
await pageEditSession.closeSession({
userId,
sessionId: forked.sessionId,
pageId: PAGE_ID,
parentSessionId: PARENT_SESSION,
summary: '已通过子会话重生成旅行主题页视觉。',
});
await pool.end();
if (!changed) {
console.error('\n⚠ 页面内容未检测到变更,请检查 goose 是否调用了 mindspace_page_patch');
process.exit(1);
}
console.log('\n✅ 测试完成');
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack : error);
process.exit(1);
});