diff --git a/app/api/generate/tts/route.ts b/app/api/generate/tts/route.ts index 9e364b405..304d53188 100644 --- a/app/api/generate/tts/route.ts +++ b/app/api/generate/tts/route.ts @@ -20,6 +20,11 @@ const log = createLogger('TTS API'); export const maxDuration = 30; +function normalizeBaseUrl(baseUrl: string | undefined): string | undefined { + const trimmed = baseUrl?.trim(); + return trimmed ? trimmed.replace(/\/+$/, '') : undefined; +} + export async function POST(req: NextRequest) { let ttsProviderId: string | undefined; let ttsVoice: string | undefined; @@ -69,20 +74,24 @@ export async function POST(req: NextRequest) { ); } - const clientBaseUrl = ttsBaseUrl || undefined; - if (clientBaseUrl) { - const ssrfError = await validateUrlForSSRF(clientBaseUrl); + const clientApiKey = ttsApiKey?.trim() ? ttsApiKey : undefined; + const requestedBaseUrl = normalizeBaseUrl(ttsBaseUrl); + const serverBaseUrl = normalizeBaseUrl(resolveTTSBaseUrl(ttsProviderId)); + const useClientBaseUrl = + !!requestedBaseUrl && + (!!clientApiKey || normalizeBaseUrl(requestedBaseUrl) !== normalizeBaseUrl(serverBaseUrl)); + + if (useClientBaseUrl) { + const ssrfError = await validateUrlForSSRF(requestedBaseUrl); if (ssrfError) { return apiError('INVALID_URL', 403, ssrfError); } } - const apiKey = clientBaseUrl - ? ttsApiKey || '' - : resolveTTSApiKey(ttsProviderId, ttsApiKey || undefined); - const baseUrl = clientBaseUrl - ? clientBaseUrl - : resolveTTSBaseUrl(ttsProviderId, ttsBaseUrl || undefined); + const apiKey = useClientBaseUrl + ? clientApiKey || '' + : resolveTTSApiKey(ttsProviderId, clientApiKey); + const baseUrl = useClientBaseUrl ? requestedBaseUrl : serverBaseUrl || requestedBaseUrl; // Build TTS config const config = { diff --git a/components/settings/tts-settings.tsx b/components/settings/tts-settings.tsx index 010358aa0..9a0641ec7 100644 --- a/components/settings/tts-settings.tsx +++ b/components/settings/tts-settings.tsx @@ -164,9 +164,8 @@ export function TTSSettings({ selectedProviderId }: TTSSettingsProps) { speed: ttsSpeed, apiKey: ttsProvidersConfig[selectedProviderId]?.apiKey, baseUrl: - ttsProvidersConfig[selectedProviderId]?.serverBaseUrl || ttsProvidersConfig[selectedProviderId]?.baseUrl || - providerConfig?.customDefaultBaseUrl || + (!isServerConfigured ? providerConfig?.customDefaultBaseUrl : '') || '', providerOptions, }); diff --git a/lib/generation/scene-generator.ts b/lib/generation/scene-generator.ts index 33d345779..51cf10421 100644 --- a/lib/generation/scene-generator.ts +++ b/lib/generation/scene-generator.ts @@ -924,36 +924,49 @@ async function generatePBLSceneContent( } } +function stripTrailingCodeFence(html: string): string { + return html.replace(/\s*```\s*$/g, '').trim(); +} + /** * Extract HTML document from AI response. * Tries to find ... first, then falls back to code block extraction. */ -function extractHtml(response: string): string | null { +export function extractHtml(response: string): string | null { // Strategy 1: Find complete HTML document - const doctypeStart = response.indexOf(''); - const htmlTagStart = response.indexOf('|'); - if (htmlEnd !== -1) { + const htmlEnd = response.toLowerCase().lastIndexOf(''); + if (htmlEnd >= start) { return response.substring(start, htmlEnd + 7); } + return stripTrailingCodeFence(response.substring(start)); } // Strategy 2: Extract from code block const codeBlockMatch = response.match(/```(?:html)?\s*([\s\S]*?)```/); if (codeBlockMatch) { - const content = codeBlockMatch[1].trim(); + const content = stripTrailingCodeFence(codeBlockMatch[1]); if (content.includes('/i.test(content)) { + return content; + } + } + + // Strategy 4: If response itself looks like HTML const trimmed = response.trim(); - if (trimmed.startsWith(' { }); }); }); + +describe('interactive HTML extraction', () => { + it('accepts an unterminated HTML code block when the model response is truncated', () => { + const html = extractHtml(`\`\`\`html + + +Codex 命令全景图 +
visible widget
`); + + expect(html).toContain(''); + expect(html).toContain('visible widget'); + expect(html).not.toContain('```'); + }); +});