Spaces:
Running
Running
| import crypto from 'node:crypto'; | |
| import { readMobileSessionMessages, registerMobileSession } from './mobile-session-index.js'; | |
| import { | |
| IMAGE_FALLBACK_MAX_ATTEMPTS, | |
| IMAGE_MAX_ATTEMPTS, | |
| IMAGE_RETRY_BASE_DELAY_MS, | |
| buildFallbackGenerationPrompt, | |
| canFallbackEditToGeneration, | |
| detectImageIntent, | |
| isImageRequest, | |
| isTransientImageError, | |
| requestImageGeneration, | |
| safeErrorMessage, | |
| sleep | |
| } from './image-generator-api.js'; | |
| import { appendMobileMessages, buildAssistantContent, emitStatus, saveGeneratedImages } from './image-generator-store.js'; | |
| export { GENERATED_ROOT, isImageRequest } from './image-generator-api.js'; | |
| export async function runImageTurn({ | |
| sessionId, | |
| previousSessionId, | |
| projectPath, | |
| message, | |
| attachments = [], | |
| config, | |
| turnId, | |
| persistMobileSession = false | |
| }, emit) { | |
| const finalSessionId = sessionId || `mobile-image-${crypto.randomUUID()}`; | |
| const startedAt = new Date().toISOString(); | |
| if (previousSessionId && previousSessionId !== finalSessionId) { | |
| emit({ | |
| type: 'thread-started', | |
| sessionId: finalSessionId, | |
| previousSessionId, | |
| turnId, | |
| projectPath, | |
| startedAt | |
| }); | |
| } | |
| emit({ | |
| type: 'chat-started', | |
| sessionId: finalSessionId, | |
| previousSessionId, | |
| turnId, | |
| projectPath, | |
| startedAt | |
| }); | |
| emitStatus(emit, { | |
| sessionId: finalSessionId, | |
| turnId, | |
| kind: 'image_generation_call', | |
| status: 'running', | |
| label: attachments.some((attachment) => attachment.kind === 'image') ? '正在编辑图片' : '正在生成图片', | |
| detail: '' | |
| }); | |
| if (persistMobileSession) { | |
| await appendMobileMessages({ | |
| sessionId: finalSessionId, | |
| projectPath, | |
| title: message.slice(0, 52) || 'Image task', | |
| summary: message || 'Image task', | |
| updatedAt: startedAt, | |
| messages: [ | |
| { | |
| id: `user-${turnId}`, | |
| role: 'user', | |
| content: message, | |
| timestamp: startedAt | |
| } | |
| ] | |
| }); | |
| } | |
| try { | |
| const primaryIntent = detectImageIntent(message, attachments) || 'generate'; | |
| let result = null; | |
| let lastError = null; | |
| for (let attempt = 1; attempt <= IMAGE_MAX_ATTEMPTS; attempt += 1) { | |
| try { | |
| result = await requestImageGeneration({ prompt: message, attachments, config }); | |
| break; | |
| } catch (error) { | |
| lastError = error; | |
| const canFallback = canFallbackEditToGeneration({ intent: primaryIntent, attachments, error }); | |
| if (attempt >= IMAGE_MAX_ATTEMPTS && canFallback) { | |
| break; | |
| } | |
| if (attempt >= IMAGE_MAX_ATTEMPTS || !isTransientImageError(error)) { | |
| throw error; | |
| } | |
| const detail = safeErrorMessage(error); | |
| emitStatus(emit, { | |
| sessionId: finalSessionId, | |
| turnId, | |
| kind: 'image_generation_call', | |
| status: 'running', | |
| label: `图片接口断流,正在重试 ${attempt + 1}/${IMAGE_MAX_ATTEMPTS}`, | |
| detail | |
| }); | |
| emit({ | |
| type: 'activity-update', | |
| sessionId: finalSessionId, | |
| turnId, | |
| messageId: `retry-${turnId}-${attempt}`, | |
| kind: 'image_generation_call', | |
| label: `图片接口断流,正在重试 ${attempt + 1}/${IMAGE_MAX_ATTEMPTS}`, | |
| status: 'running', | |
| detail, | |
| timestamp: new Date().toISOString() | |
| }); | |
| await sleep(IMAGE_RETRY_BASE_DELAY_MS * attempt); | |
| } | |
| } | |
| if (!result && canFallbackEditToGeneration({ intent: primaryIntent, attachments, error: lastError })) { | |
| const fallbackPrompt = buildFallbackGenerationPrompt(message, attachments, lastError); | |
| const fallbackDetail = safeErrorMessage(lastError); | |
| emitStatus(emit, { | |
| sessionId: finalSessionId, | |
| turnId, | |
| kind: 'image_generation_call', | |
| status: 'running', | |
| label: '图片编辑接口断流,正在改为重绘出图', | |
| detail: fallbackDetail | |
| }); | |
| emit({ | |
| type: 'activity-update', | |
| sessionId: finalSessionId, | |
| turnId, | |
| messageId: `fallback-${turnId}`, | |
| kind: 'image_generation_call', | |
| label: '图片编辑接口断流,正在改为重绘出图', | |
| status: 'running', | |
| detail: fallbackDetail, | |
| timestamp: new Date().toISOString() | |
| }); | |
| for (let attempt = 1; attempt <= IMAGE_FALLBACK_MAX_ATTEMPTS; attempt += 1) { | |
| try { | |
| result = await requestImageGeneration({ | |
| prompt: fallbackPrompt, | |
| attachments: [], | |
| config, | |
| forceGenerate: true | |
| }); | |
| result.fallbackFromEdit = true; | |
| result.originalError = fallbackDetail; | |
| break; | |
| } catch (error) { | |
| lastError = error; | |
| if (attempt >= IMAGE_FALLBACK_MAX_ATTEMPTS || !isTransientImageError(error)) { | |
| throw error; | |
| } | |
| const detail = safeErrorMessage(error); | |
| emitStatus(emit, { | |
| sessionId: finalSessionId, | |
| turnId, | |
| kind: 'image_generation_call', | |
| status: 'running', | |
| label: `重绘出图断流,正在重试 ${attempt + 1}/${IMAGE_FALLBACK_MAX_ATTEMPTS}`, | |
| detail | |
| }); | |
| await sleep(IMAGE_RETRY_BASE_DELAY_MS * attempt); | |
| } | |
| } | |
| } | |
| if (!result) { | |
| throw lastError || new Error('图片生成失败'); | |
| } | |
| const savedImages = await saveGeneratedImages(result.images); | |
| let assistantContent = buildAssistantContent(savedImages); | |
| if (result.fallbackFromEdit) { | |
| assistantContent = `图片编辑接口连续断流,已自动改为重绘出图。\n\n${assistantContent}`; | |
| } | |
| const completedAt = new Date().toISOString(); | |
| emit({ | |
| type: 'activity-update', | |
| sessionId: finalSessionId, | |
| turnId, | |
| messageId: `activity-${turnId}`, | |
| kind: 'image_generation_call', | |
| label: result.fallbackFromEdit ? '重绘出图完成' : result.intent === 'edit' ? '图片编辑完成' : '图片生成完成', | |
| status: 'completed', | |
| detail: `model: ${result.model}`, | |
| timestamp: completedAt | |
| }); | |
| emit({ | |
| type: 'assistant-update', | |
| sessionId: finalSessionId, | |
| previousSessionId, | |
| turnId, | |
| messageId: `assistant-${turnId}`, | |
| role: 'assistant', | |
| kind: 'image_generation_result', | |
| content: assistantContent, | |
| done: true | |
| }); | |
| if (persistMobileSession) { | |
| const existingMessages = await readMobileSessionMessages(finalSessionId); | |
| await registerMobileSession({ | |
| id: finalSessionId, | |
| projectPath, | |
| title: message.slice(0, 52) || '图片生成', | |
| summary: message || '图片生成', | |
| updatedAt: completedAt, | |
| messages: [ | |
| ...existingMessages, | |
| { | |
| id: `user-${turnId}`, | |
| role: 'user', | |
| content: message, | |
| timestamp: startedAt | |
| }, | |
| { | |
| id: `assistant-${turnId}`, | |
| role: 'assistant', | |
| content: assistantContent, | |
| timestamp: completedAt | |
| } | |
| ] | |
| }); | |
| } | |
| emit({ | |
| type: 'chat-complete', | |
| sessionId: finalSessionId, | |
| previousSessionId, | |
| turnId, | |
| usage: result.usage, | |
| hadAssistantText: true, | |
| completedAt | |
| }); | |
| } catch (error) { | |
| const messageText = safeErrorMessage(error); | |
| console.error('[image] Generation failed:', messageText); | |
| if (persistMobileSession) { | |
| const failedAt = new Date().toISOString(); | |
| await appendMobileMessages({ | |
| sessionId: finalSessionId, | |
| projectPath, | |
| title: message.slice(0, 52) || 'Image task', | |
| summary: message || 'Image task', | |
| updatedAt: failedAt, | |
| messages: [ | |
| { | |
| id: `assistant-${turnId}`, | |
| role: 'assistant', | |
| content: `Image task failed: ${messageText}`, | |
| timestamp: failedAt | |
| } | |
| ] | |
| }); | |
| } | |
| emit({ | |
| type: 'activity-update', | |
| sessionId: finalSessionId, | |
| turnId, | |
| messageId: `activity-${turnId}`, | |
| kind: 'image_generation_call', | |
| label: '图片生成失败', | |
| status: 'failed', | |
| detail: messageText, | |
| error: messageText, | |
| timestamp: new Date().toISOString() | |
| }); | |
| emitStatus(emit, { | |
| sessionId: finalSessionId, | |
| turnId, | |
| kind: 'image_generation_call', | |
| status: 'failed', | |
| label: '图片生成失败', | |
| detail: messageText | |
| }); | |
| emit({ | |
| type: 'chat-error', | |
| sessionId: finalSessionId, | |
| previousSessionId, | |
| turnId, | |
| error: messageText | |
| }); | |
| } | |
| return finalSessionId; | |
| } | |