#!/usr/bin/env node /** * 通过 goose 子会话重新生成 MindSpace 页面内容(Studio / RDS 环境测试) * * 用法: * node scripts/regenerate-page-goose-test.mjs [pageId] [parentSessionId] * * 默认:探索世界 · TKMind(john,source_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 与完整内联 CSS(CSP 安全,禁止外部 JS) - 标题「探索世界 · TKMind」,summary 写一句精炼中文 **禁止**读写工作区文件。完成后**只**在回复末尾输出一个 \`\`\`mindspace-page-update\`\`\` 代码块(JSON:title、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); });