forked from 77media/video-flow
381 lines
12 KiB
TypeScript
381 lines
12 KiB
TypeScript
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;
|
||
}
|
||
|
||
/**
|
||
* 系统消息转换为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 \nShot type: ${customData.shot_type}\nAtmosphere: ${customData.atmosphere}\nKey action: ${customData.key_action}`
|
||
}, {
|
||
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, error_message } = 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',
|
||
};
|
||
|
||
if (error_message && error_message === '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 || '' });
|
||
}
|
||
});
|
||
} 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);
|
||
} |