codexmobile-relay / server /image-generator.js
Codex
deploy: CodexMobile Relay
90f0300
Raw
History Blame Contribute Delete
8.69 kB
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;
}