#!/usr/bin/env node /** * Direct goosed page smoke on TKMIND_API_TARGET (default https://127.0.0.1:18006). */ import fs from 'node:fs'; import path from 'node:path'; import { Readable } from 'node:stream'; import { fileURLToPath } from 'node:url'; import { Agent, fetch } from 'undici'; import { buildChatSkillPrompt } from '../chat-skills.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 secret = process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret'; const base = process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006'; const workingDir = path.resolve( process.argv[2] ?? path.join(root, '.release-gate/local/goosed-direct-page'), ); const targetHtml = process.argv[3] ?? 'public/suzhou-goosed-direct.html'; const provider = process.argv[4] ?? process.env.GOOSED_PAGE_TEST_PROVIDER ?? 'custom_tkmind_relay_deepseek'; const model = process.argv[5] ?? process.env.GOOSED_PAGE_TEST_MODEL ?? 'deepseek-chat'; const timeoutMs = Number(process.env.GOOSED_PAGE_TEST_TIMEOUT_MS ?? 600_000); const dispatcher = new Agent({ connect: { rejectUnauthorized: false } }); async function apiFetch(pathname, init = {}) { const headers = { ...(init.headers ?? {}), 'X-Secret-Key': secret, }; if (init.body && !headers['Content-Type']) { headers['Content-Type'] = 'application/json'; } return fetch(`${base}${pathname}`, { ...init, headers, dispatcher, }); } async function apiJson(pathname, body) { const response = await apiFetch(pathname, { method: 'POST', body: JSON.stringify(body), }); const text = await response.text(); if (!response.ok) throw new Error(`${pathname} ${response.status}: ${text.slice(0, 800)}`); if (!text.trim()) return {}; return JSON.parse(text); } function messageVisibleText(message) { return (message?.content ?? []) .filter((item) => item?.type === 'text') .map((item) => String(item.text ?? '')) .join('\n') .trim(); } async function executeSessionReply(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 || '无法建立 goosed 事件流'); } 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, displayText: prompt }, }, }), }); 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 = []; let finishSeen = false; let errorText = ''; const pushMessage = (list, message) => { const index = list.findIndex((item) => item.id === message.id); if (index >= 0) { const next = [...list]; next[index] = message; return next; } return [...list, message]; }; const deadline = Date.now() + timeoutMs; for await (const chunk of reader) { if (Date.now() > deadline) throw new Error(`goosed 事件流超时 ${timeoutMs}ms`); 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 !== false) { messages = pushMessage(messages, event.message); } else if (event.type === 'UpdateConversation') { messages = (event.conversation ?? []).filter((item) => item.metadata?.userVisible !== false); } else if (event.type === 'Error') { errorText = String(event.error ?? event.message ?? 'goose 执行失败'); throw new Error(errorText); } else if (event.type === 'Finish') { finishSeen = true; break; } } if (finishSeen) break; } const assistantTexts = messages .filter((item) => item.role === 'assistant') .map((item) => messageVisibleText(item)) .filter(Boolean); return { finishSeen, combined: assistantTexts.join('\n\n').trim(), toolCalls: messages.flatMap((item) => (item.content ?? []) .filter((part) => part?.type === 'toolRequest' || part?.type === 'toolResponse') .map((part) => ({ role: item.role, type: part.type, name: part.toolCall?.value?.name ?? part.toolResponse?.value?.name ?? part.name ?? null, }))), }; } async function main() { fs.mkdirSync(path.join(workingDir, 'public'), { recursive: true }); console.log(`goosed base: ${base}`); console.log(`working_dir: ${workingDir}`); console.log(`target: ${targetHtml}`); console.log(`provider: ${provider} / ${model}`); const start = await apiJson('/agent/start', { working_dir: workingDir }); const sessionId = start.id; console.log(`session: ${sessionId}`); await apiJson('/agent/update_provider', { session_id: sessionId, provider, model, }); const skillPrefix = buildChatSkillPrompt('generate-page', 'static-page-publish'); const userText = `${skillPrefix}请帮我做一个全新的苏州一日游攻略页面,保存为 ${targetHtml},不要修改或复用已有页面,做完直接给我链接。`; const requestId = crypto.randomUUID(); const reply = await executeSessionReply(sessionId, requestId, userText); const htmlPath = path.join(workingDir, targetHtml); const htmlExists = fs.existsSync(htmlPath); const htmlSize = htmlExists ? fs.statSync(htmlPath).size : 0; console.log('\n=== result ==='); console.log(`finish_seen: ${reply.finishSeen}`); console.log(`html_exists: ${htmlExists}`); console.log(`html_bytes: ${htmlSize}`); console.log(`tool_calls: ${reply.toolCalls.length}`); for (const call of reply.toolCalls.slice(-12)) { console.log(` - ${call.role} ${call.type} ${call.name ?? ''}`); } if (reply.combined) { console.log('\n--- assistant excerpt ---'); console.log(reply.combined.slice(-1500)); } if (!htmlExists || htmlSize <= 0) { console.error('\nFAIL: goosed did not materialize target HTML in working_dir'); process.exit(1); } const html = fs.readFileSync(htmlPath, 'utf8'); if (!/苏州/u.test(html)) { console.error('\nFAIL: generated HTML missing expected keyword 苏州'); process.exit(1); } console.log('\nPASS: goosed wrote deliverable HTML on 18006'); } main().catch((error) => { console.error(error instanceof Error ? error.stack ?? error.message : error); process.exit(1); });