2025-08-27 20:11:08 +08:00

381 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;
}
/**
* 系统消息转换为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: `🎬 根据您输入的 "${customData.project_data.script}",我已完成项目的初始化。\n${content}`
}];
}
break;
case 'generate_script_summary':
if (isScriptSummary(customData)) {
blocks = [
{ type: 'text', text: `🎬 剧本摘要生成完成\n\n${customData.summary}\n\n${content}` }
];
}
break;
case 'generate_character':
if (isCharacterGeneration(customData)) {
blocks = [{
type: 'text',
text: `🎭 演员 "${customData.character_name}" 已就位`
}, {
type: 'image',
url: customData.image_path
}, {
type: 'text',
text: '图片中演员形象仅供参考,后续可根据视频生成后进行调整。'
}, {
type: 'progress',
value: customData.completed_count,
total: customData.total_count,
label: `已完成 ${customData.completed_count} 个演员,共有 ${customData.total_count}`
}, {
type: 'text',
text: `\n${content}`
}];
}
break;
case 'generate_sketch':
if (isSketchGeneration(customData)) {
blocks = [{
type: 'text',
text: `🎨 场景 "${customData.sketch_name}" 参考图片已生成 \n`
}, {
type: 'image',
url: customData.image_path
}, {
type: 'text',
text: '图片中场景仅供参考,后续可根据视频生成后进行调整。'
}, {
type: 'progress',
value: customData.completed_count,
total: customData.total_count,
label: `已完成 ${customData.completed_count} 个场景,共有 ${customData.total_count}`
}, {
type: 'text',
text: `\n${content}`
}];
}
break;
case 'generate_shot_sketch':
if (isShotSketchGeneration(customData)) {
blocks = [{
type: 'text',
text: `🎬 故事板静帧生成 \n镜头类型${customData.shot_type}\n氛围${customData.atmosphere}\n关键动作${customData.key_action}`
}, {
type: 'image',
url: customData.url
}, {
type: 'text',
text: '图片中故事板静帧仅供参考,后续可根据视频生成后进行调整。'
}, {
type: 'progress',
value: customData.completed_count,
total: customData.total_count,
label: `已完成 ${customData.completed_count} 个故事板静帧,共有 ${customData.total_count}`
}, {
type: 'text',
text: `\n${content}`
}];
}
break;
case 'generate_video':
if (isShotVideoGeneration(customData)) {
blocks.push({
type: 'text',
text: `🎬 该分镜下包含${customData.urls.length} 个视频。 \n核心氛围${customData.prompt_json.core_atmosphere}`
});
customData.urls.forEach((url: string) => {
blocks.push({
type: 'video',
url: url
});
});
blocks.push({
type: 'text',
text: '后续可在剪辑线上进行编辑。'
}, {
type: 'progress',
value: customData.completed_count,
total: customData.total_count,
label: `已完成 ${customData.completed_count} 个分镜,共有 ${customData.total_count} 个分镜`
}, {
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: "无内容" });
}
return message;
} catch (error) {
console.error("转换消息格式失败:", error, apiMessage);
// 返回一个带有错误信息的消息
return {
id: new Date().getTime().toString(),
role: apiMessage.role,
createdAt: new Date(apiMessage.created_at).getTime(),
blocks: [{ type: "text", text: "消息格式错误" }],
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('发送历史消息请求:', request);
const response = await post<ApiResponse<MessagesResponse>>("/intelligent/history", request);
console.log('收到历史消息响应:', response);
// 确保 response.data 和 messages 存在
if (!response.data || !response.data.messages) {
console.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("获取消息历史失败:", 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('发送消息请求:', request);
await post<ApiResponse<RealApiMessage>>("/intelligent/chat", request);
} catch (error) {
console.error("发送消息失败:", error);
throw error;
}
}
/**
* 重试发送消息
*/
export async function retryMessage(
messageId: string,
config: ChatConfig
): Promise<void> {
// TODO: 实现实际的重试逻辑,可能需要保存原始消息内容
// 这里简单重用发送消息的接口
return sendMessage([{ type: "text", text: "重试消息" }], config);
}