2025-09-03 20:26:24 +08:00

391 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
ChatMessage,
MessageBlock,
FetchMessagesRequest,
SendMessageRequest,
ChatConfig,
ApiResponse,
RealApiMessage,
ApiMessageContent,
MessagesResponse,
FunctionName,
ProjectInit,
ScriptSummary,
CharacterGeneration,
SketchGeneration,
ShotSketchGeneration,
ShotVideoGeneration
} from "./types";
import { post } from "@/api/request";
// 空消息 默认展示
const EMPTY_MESSAGES: RealApiMessage[] = [
{
id: 1,
role: 'assistant',
content: JSON.stringify([{
type: 'text',
content: '🌟Welcome to MovieFlow 🎬✨\nTell me your idea💡\nI am your AI assistant🤖, I can help you:\n🎭 Generate actor images\n📽 Generate scene & shot sketches\n🎞 Complete video creation\n\nLet\'s start our creative journey together!❤️'
}]),
created_at: new Date().toISOString(),
function_name: undefined,
custom_data: undefined,
status: 'success',
intent_type: 'function_call'
}
];
// 用户积分不足消息
const NoEnoughCreditsMessageBlocks: MessageBlock[] = [
{
type: 'text',
text: 'Insufficient credits.'
},
{
type: 'link',
text: 'Upgrade to continue.',
url: '/pricing'
}
];
/**
* 类型守卫函数
*/
function isProjectInit(data: any): data is ProjectInit {
return data && 'project_data' in data;
}
function isScriptSummary(data: any): data is ScriptSummary {
return data && 'summary' in data;
}
function isCharacterGeneration(data: any): data is CharacterGeneration {
return data && 'character_name' in data && 'image_path' in data && 'completed_count' in data && 'total_count' in data;
}
function isSketchGeneration(data: any): data is SketchGeneration {
return data && 'sketch_name' in data && 'image_path' in data && 'completed_count' in data && 'total_count' in data;
}
function isShotSketchGeneration(data: any): data is ShotSketchGeneration {
return data && 'shot_type' in data && 'atmosphere' in data && 'key_action' in data && 'url' in data && 'completed_count' in data && 'total_count' in data;
}
function isShotVideoGeneration(data: any): data is ShotVideoGeneration {
return data && 'prompt_json' in data && 'urls' in data && 'completed_count' in data && 'total_count' in data;
}
function displayText(title: string, content: string, isLast: boolean = false) {
return `${content ? `${title}: ${content}${isLast ? '' : '\n'}` : ''}`
}
/**
* 系统消息转换为blocks数组
*/
function transformSystemMessage(
functionName: FunctionName,
content: string,
customData: ProjectInit | ScriptSummary | CharacterGeneration | SketchGeneration | ShotSketchGeneration | ShotVideoGeneration
): MessageBlock[] {
let blocks: MessageBlock[] = [];
switch (functionName) {
case 'create_project':
if (isProjectInit(customData)) {
blocks = [{
type: 'text',
text: `🎬 According to your input "${customData.project_data.script}", I have completed the initialization of the project.\n${content}`
}];
}
break;
case 'generate_script_summary':
if (isScriptSummary(customData)) {
blocks = [
{ type: 'text', text: `🎬 I have completed the script summary generation.\n\n${customData.summary}\n\n${content}` }
];
}
break;
case 'generate_character':
if (isCharacterGeneration(customData)) {
blocks = [{
type: 'text',
text: `🎭 Actor "${customData.character_name}" is ready.`
}, {
type: 'image',
url: customData.image_path
}, {
type: 'text',
text: 'The actor image is for reference only, and can be adjusted after the video is generated.'
}, {
type: 'progress',
value: customData.completed_count,
total: customData.total_count,
label: `Completed ${customData.completed_count} actors, total ${customData.total_count} actors`
}, {
type: 'text',
text: `\n${content}`
}];
}
break;
case 'generate_sketch':
if (isSketchGeneration(customData)) {
blocks = [{
type: 'text',
text: `🎨 Scene "${customData.sketch_name}" reference image generated \n`
}, {
type: 'image',
url: customData.image_path
}, {
type: 'text',
text: 'The scene image is for reference only, and can be adjusted after the video is generated.'
}, {
type: 'progress',
value: customData.completed_count,
total: customData.total_count,
label: `Completed ${customData.completed_count} scenes, total ${customData.total_count} scenes`
}, {
type: 'text',
text: `\n${content}`
}];
}
break;
case 'generate_shot_sketch':
if (isShotSketchGeneration(customData)) {
blocks = [{
type: 'text',
text: `🎬 Storyboard static frame generation \n${displayText('Shot type', customData.shot_type)}${displayText('Atmosphere', customData.atmosphere)}${displayText('Key action', customData.key_action, true)}`
}, {
type: 'image',
url: customData.url
}, {
type: 'text',
text: 'The storyboard static frame image is for reference only, and can be adjusted after the video is generated.'
}, {
type: 'progress',
value: customData.completed_count,
total: customData.total_count,
label: `Completed ${customData.completed_count} storyboard static frames, total ${customData.total_count} storyboard static frames`
}, {
type: 'text',
text: `\n${content}`
}];
}
break;
case 'generate_video':
if (isShotVideoGeneration(customData)) {
blocks.push({
type: 'text',
text: `🎬 There are ${customData.urls.length} videos in this shot. \nCore atmosphere: ${customData.prompt_json.core_atmosphere}`
});
customData.urls.forEach((url: string) => {
blocks.push({
type: 'video',
url: url
});
});
blocks.push({
type: 'text',
text: 'You can edit the video on the editing line later.'
}, {
type: 'progress',
value: customData.completed_count,
total: customData.total_count,
label: `Completed ${customData.completed_count} shots, total ${customData.total_count} shots`
}, {
type: 'text',
text: `\n${content}`
})
}
break;
}
return blocks;
}
/**
* 将API响应转换为ChatMessage格式
*/
function transformMessage(apiMessage: RealApiMessage): ChatMessage {
try {
const { id, role, content, created_at, function_name, custom_data, status, intent_type } = apiMessage;
let message: ChatMessage = {
id: id ? id.toString() : Date.now().toString(),
role: role,
createdAt: new Date(created_at).getTime(),
blocks: [],
chatType: intent_type,
status: status || 'success',
};
const errorMessage = custom_data?.error_message;
if (errorMessage && errorMessage === 'no enough credits') {
message.blocks = NoEnoughCreditsMessageBlocks;
} else {
if (role === 'assistant' || role === 'user') {
try {
const contentObj = JSON.parse(content);
const contentArray = Array.isArray(contentObj) ? contentObj : [contentObj];
contentArray.forEach((c: ApiMessageContent) => {
if (c.type === "text") {
message.blocks.push({ type: "text", text: c.content });
} else if (c.type === "image") {
message.blocks.push({ type: "image", url: c.content });
} else if (c.type === "video") {
message.blocks.push({ type: "video", url: c.content });
} else if (c.type === "audio") {
message.blocks.push({ type: "audio", url: c.content });
} else if (c.type === "link") {
message.blocks.push({ type: "link", text: c.content, url: c.url || '' });
}
});
// todo: 需要确认是否需要添加applyButton
// if (role === 'assistant' && function_name === 'modify_video_with_runway') {
// message.blocks.push({ type: "applyButton", text: "Apply" });
// }
} catch (error) {
// 如果 JSON 解析失败,将整个 content 作为文本内容
message.blocks.push({ type: "text", text: content });
}
} else if (role === 'system' && function_name && custom_data) {
// 处理系统消息
message.blocks = transformSystemMessage(function_name, content, custom_data);
} else {
message.blocks.push({ type: "text", text: content });
}
}
// 如果没有有效的 blocks至少添加一个文本块
if (message.blocks.length === 0) {
message.blocks.push({ type: "text", text: "No content" });
}
return message;
} catch (error) {
console.error("Failed to transform message format:", error, apiMessage);
// 返回一个带有错误信息的消息
return {
id: new Date().getTime().toString(),
role: apiMessage.role,
createdAt: new Date(apiMessage.created_at).getTime(),
blocks: [{ type: "text", text: "Message format error" }],
chatType: 'chat',
status: 'error',
};
}
}
/**
* 获取消息列表
*/
export async function fetchMessages(
config: ChatConfig,
offset: number = 0,
limit: number = 50
): Promise<{
messages: ChatMessage[],
hasMore: boolean,
totalCount: number,
}> {
const request: FetchMessagesRequest = {
session_id: `project_${config.projectId}_user_${config.userId}`,
limit,
offset,
};
try {
console.log('Send history message request:', request);
const response = await post<ApiResponse<MessagesResponse>>("/intelligent/history", request);
console.log('Receive history message response:', response);
// 确保 response.data 和 messages 存在
if (!response.data || !response.data.messages) {
console.error('History message response format error:', response);
return {
messages: [],
hasMore: false,
totalCount: 0
};
}
// 转换消息并按时间排序
if (response.data.messages.length === 0) {
return {
messages: EMPTY_MESSAGES.map(transformMessage),
hasMore: false,
totalCount: 0
};
}
return {
messages: response.data.messages
.map(transformMessage)
.sort((a, b) => Number(a.id) - Number(b.id)),
hasMore: response.data.has_more,
totalCount: response.data.total_count
};
} catch (error) {
console.error("Failed to get message history:", error);
throw error;
}
}
/**
* 发送新消息
*/
export async function sendMessage(
blocks: MessageBlock[],
config: ChatConfig,
videoId?: string
): Promise<void> {
// 提取文本、图片和视频
const textBlocks = blocks.filter(b => b.type === "text");
const imageBlocks = blocks.filter(b => b.type === "image");
const videoBlocks = blocks.filter(b => b.type === "video");
const request: SendMessageRequest = {
session_id: `project_${config.projectId}_user_${config.userId}`,
user_input: textBlocks.map(b => (b as { text: string }).text).join("\n"),
project_id: config.projectId,
user_id: config.userId.toString(),
};
// 如果有图片添加第一张图片的URL
if (imageBlocks.length > 0) {
request.image_url = (imageBlocks[0] as { url: string }).url;
}
// 如果有视频添加视频URL
if (videoBlocks.length > 0) {
request.video_url = (videoBlocks[0] as { url: string }).url;
}
// 如果有视频ID添加到请求中
if (videoId) {
request.video_id = videoId;
}
try {
console.log('Send message request:', request);
await post<ApiResponse<RealApiMessage>>("/intelligent/chat", request);
} catch (error) {
console.error("Send message failed:", error);
throw error;
}
}
/**
* 重试发送消息
*/
export async function retryMessage(
messageId: string,
config: ChatConfig
): Promise<void> {
// TODO: 实现实际的重试逻辑,可能需要保存原始消息内容
// 这里简单重用发送消息的接口
return sendMessage([{ type: "text", text: "Retry message" }], config);
}