9b4a25799f
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>
287 lines
10 KiB
JavaScript
287 lines
10 KiB
JavaScript
#!/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);
|
||
});
|