forked from 77media/video-flow
554 lines
16 KiB
TypeScript
554 lines
16 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";
|
||
|
||
// Mock 数据
|
||
const MOCK_MESSAGES: RealApiMessage[] = [
|
||
// 用户发送剧本
|
||
{
|
||
id: 1,
|
||
role: 'user',
|
||
content: JSON.stringify([{
|
||
type: 'text',
|
||
content: '我想拍一个关于一个小女孩和她的机器人朋友的故事,故事发生在未来世界。'
|
||
}]),
|
||
created_at: '2024-03-20T10:00:00Z',
|
||
function_name: undefined,
|
||
custom_data: undefined,
|
||
status: 'success',
|
||
intent_type: 'chat'
|
||
},
|
||
// 项目初始化
|
||
{
|
||
id: 2,
|
||
role: 'system',
|
||
content: '我会帮您创建一个温馨感人的科幻短片,讲述人工智能与人类情感的故事。',
|
||
created_at: '2024-03-20T10:00:10Z',
|
||
function_name: 'create_project',
|
||
custom_data: {
|
||
project_data: {
|
||
script: '小女孩和机器人朋友的故事'
|
||
}
|
||
},
|
||
status: 'success',
|
||
intent_type: 'procedure'
|
||
},
|
||
// 剧本总结
|
||
{
|
||
id: 3,
|
||
role: 'system',
|
||
content: '故事概要:在2045年的未来城市,10岁的小女孩艾米丽收到了一个特别的生日礼物——一个具有高度情感智能的机器人伙伴"小星"。随着时间推移,他们建立了深厚的友谊。当小星因能源耗尽即将永久关闭时,艾米丽想尽办法寻找解决方案,最终通过她的坚持和创意,成功为小星找到了新的能源,让这段跨越人机界限的友谊得以延续。',
|
||
created_at: '2024-03-20T10:01:00Z',
|
||
function_name: 'generate_script_summary',
|
||
custom_data: {
|
||
summary: '一个关于友谊和希望的温暖故事'
|
||
},
|
||
status: 'success',
|
||
intent_type: 'procedure'
|
||
},
|
||
// 角色生成 - 艾米丽
|
||
{
|
||
id: 4,
|
||
role: 'system',
|
||
content: '主角艾米丽的形象已生成',
|
||
created_at: '2024-03-20T10:02:00Z',
|
||
function_name: 'generate_character',
|
||
custom_data: {
|
||
character_name: '艾米丽',
|
||
image_path: 'https://picsum.photos/seed/emily/300/400',
|
||
completed_count: 1,
|
||
total_count: 2
|
||
},
|
||
status: 'success',
|
||
intent_type: 'procedure'
|
||
},
|
||
// 角色生成 - 小星
|
||
{
|
||
id: 5,
|
||
role: 'system',
|
||
content: '机器人小星的形象已生成',
|
||
created_at: '2024-03-20T10:03:00Z',
|
||
function_name: 'generate_character',
|
||
custom_data: {
|
||
character_name: '小星',
|
||
image_path: 'https://picsum.photos/seed/robot/300/400',
|
||
completed_count: 2,
|
||
total_count: 2
|
||
},
|
||
status: 'success',
|
||
intent_type: 'procedure'
|
||
},
|
||
// 场景生成 - 未来城市
|
||
{
|
||
id: 6,
|
||
role: 'system',
|
||
content: '未来城市场景设计完成',
|
||
created_at: '2024-03-20T10:04:00Z',
|
||
function_name: 'generate_sketch',
|
||
custom_data: {
|
||
sketch_name: '未来城市街景',
|
||
image_path: 'https://picsum.photos/seed/city/600/400',
|
||
completed_count: 1,
|
||
total_count: 3
|
||
},
|
||
status: 'success',
|
||
intent_type: 'procedure'
|
||
},
|
||
// 场景生成 - 艾米丽的房间
|
||
{
|
||
id: 7,
|
||
role: 'system',
|
||
content: '艾米丽的未来风格卧室设计完成',
|
||
created_at: '2024-03-20T10:05:00Z',
|
||
function_name: 'generate_sketch',
|
||
custom_data: {
|
||
sketch_name: '艾米丽的卧室',
|
||
image_path: 'https://picsum.photos/seed/room/600/400',
|
||
completed_count: 2,
|
||
total_count: 3
|
||
},
|
||
status: 'success',
|
||
intent_type: 'procedure'
|
||
},
|
||
// 场景生成 - 实验室
|
||
{
|
||
id: 8,
|
||
role: 'system',
|
||
content: '高科技实验室场景设计完成',
|
||
created_at: '2024-03-20T10:06:00Z',
|
||
function_name: 'generate_sketch',
|
||
custom_data: {
|
||
sketch_name: '未来实验室',
|
||
image_path: 'https://picsum.photos/seed/lab/600/400',
|
||
completed_count: 3,
|
||
total_count: 3
|
||
},
|
||
status: 'success',
|
||
intent_type: 'procedure'
|
||
},
|
||
// 分镜生成 - 相遇
|
||
{
|
||
id: 9,
|
||
role: 'system',
|
||
content: '第一个分镜:艾米丽收到礼物时的场景',
|
||
created_at: '2024-03-20T10:07:00Z',
|
||
function_name: 'generate_shot_sketch',
|
||
custom_data: {
|
||
shot_type: '中景',
|
||
atmosphere: '温馨、期待',
|
||
key_action: '艾米丽惊喜地打开礼物盒,小星缓缓启动',
|
||
url: 'https://picsum.photos/seed/shot1/600/400',
|
||
completed_count: 1,
|
||
total_count: 3
|
||
},
|
||
status: 'success',
|
||
intent_type: 'procedure'
|
||
},
|
||
// 分镜生成 - 危机
|
||
{
|
||
id: 10,
|
||
role: 'system',
|
||
content: '第二个分镜:小星能源耗尽的场景',
|
||
created_at: '2024-03-20T10:08:00Z',
|
||
function_name: 'generate_shot_sketch',
|
||
custom_data: {
|
||
shot_type: '特写',
|
||
atmosphere: '紧张、担忧',
|
||
key_action: '小星的能源指示灯闪烁微弱,艾米丽神情焦急',
|
||
url: 'https://picsum.photos/seed/shot2/600/400',
|
||
completed_count: 2,
|
||
total_count: 3
|
||
},
|
||
status: 'success',
|
||
intent_type: 'procedure'
|
||
},
|
||
// 分镜生成 - 解决
|
||
{
|
||
id: 11,
|
||
role: 'system',
|
||
content: '第三个分镜:找到新能源解决方案的场景',
|
||
created_at: '2024-03-20T10:09:00Z',
|
||
function_name: 'generate_shot_sketch',
|
||
custom_data: {
|
||
shot_type: '全景',
|
||
atmosphere: '欢欣、胜利',
|
||
key_action: '实验室中艾米丽成功激活新能源,小星重新焕发活力',
|
||
url: 'https://picsum.photos/seed/shot3/600/400',
|
||
completed_count: 3,
|
||
total_count: 3
|
||
},
|
||
status: 'success',
|
||
intent_type: 'procedure'
|
||
},
|
||
// 分镜视频生成
|
||
{
|
||
id: 11.1,
|
||
role: 'system',
|
||
content: '分镜视频生成完成',
|
||
created_at: '2024-03-20T10:10:00Z',
|
||
function_name: 'generate_video',
|
||
custom_data: {
|
||
prompt_json: {
|
||
core_atmosphere: '欢欣、胜利',
|
||
},
|
||
urls: ['https://cdn.qikongjian.com/faces/1755798635_facefusion_output_1755798635.mp4'],
|
||
completed_count: 1,
|
||
total_count: 1
|
||
},
|
||
status: 'success',
|
||
intent_type: 'procedure'
|
||
},
|
||
// 用户反馈
|
||
{
|
||
id: 12,
|
||
role: 'user',
|
||
content: JSON.stringify([{
|
||
type: 'text',
|
||
content: '这个故事设计太棒了!特别喜欢艾米丽和小星的互动场景。'
|
||
}]),
|
||
created_at: '2024-03-20T10:10:00Z',
|
||
function_name: undefined,
|
||
custom_data: undefined,
|
||
status: 'success',
|
||
intent_type: 'function_call'
|
||
},
|
||
// 助手回复
|
||
{
|
||
id: 13,
|
||
role: 'assistant',
|
||
content: JSON.stringify([{
|
||
type: 'text',
|
||
content: '谢谢您的肯定!我们可以继续优化任何场景或角色设计,您觉得有什么地方需要调整吗?'
|
||
}]),
|
||
created_at: '2024-03-20T10:10:10Z',
|
||
function_name: undefined,
|
||
custom_data: undefined,
|
||
status: 'success',
|
||
intent_type: 'function_call'
|
||
}
|
||
];
|
||
|
||
/**
|
||
* 类型守卫函数
|
||
*/
|
||
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\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\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\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\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\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',
|
||
};
|
||
|
||
if (role === 'assistant' || role === 'user') {
|
||
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 (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: MOCK_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
|
||
): Promise<void> {
|
||
// 提取文本和图片
|
||
const textBlocks = blocks.filter(b => b.type === "text");
|
||
const imageBlocks = blocks.filter(b => b.type === "image");
|
||
|
||
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;
|
||
}
|
||
|
||
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);
|
||
} |