feat: 添加 VEO3 场景写作工具并优化文件处理逻辑
- 创建 VEO3SceneWriterTool 页面组件,集成聊天界面和文件选择功能 - 添加 veo3SceneWriterService 服务层,封装与 Rust 后端的通信逻辑 - 实现 Tauri 命令支持,调用 veo3-scene-writer crate - 更新工具数据配置,添加 VEO3 场景写作工具 - 优化文件处理逻辑:JSON/TXT 文件读取内容作为消息,图片文件作为附件 - 支持多种文本格式:.json, .txt, .md, .yaml, .yml, .toml - 提供专业的影视场景提示词生成功能
This commit is contained in:
@@ -41,6 +41,7 @@ pub fn run() {
|
|||||||
.manage(commands::image_editing_commands::ImageEditingState::new())
|
.manage(commands::image_editing_commands::ImageEditingState::new())
|
||||||
.manage(commands::tvai_commands::TvaiState::new())
|
.manage(commands::tvai_commands::TvaiState::new())
|
||||||
.manage(commands::veo3_actor_define_commands::Veo3ActorDefineSessionManager::new(std::collections::HashMap::new()))
|
.manage(commands::veo3_actor_define_commands::Veo3ActorDefineSessionManager::new(std::collections::HashMap::new()))
|
||||||
|
.manage(commands::veo3_actor_define_commands::Veo3SceneWriterSessionManager::new(std::collections::HashMap::new()))
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
commands::project_commands::create_project,
|
commands::project_commands::create_project,
|
||||||
commands::project_commands::get_all_projects,
|
commands::project_commands::get_all_projects,
|
||||||
@@ -790,7 +791,16 @@ pub fn run() {
|
|||||||
commands::veo3_actor_define_commands::veo3_actor_define_clear_conversation,
|
commands::veo3_actor_define_commands::veo3_actor_define_clear_conversation,
|
||||||
commands::veo3_actor_define_commands::veo3_actor_define_remove_session,
|
commands::veo3_actor_define_commands::veo3_actor_define_remove_session,
|
||||||
commands::veo3_actor_define_commands::veo3_actor_define_get_active_sessions,
|
commands::veo3_actor_define_commands::veo3_actor_define_get_active_sessions,
|
||||||
commands::veo3_actor_define_commands::veo3_actor_define_create_actor_file
|
commands::veo3_actor_define_commands::veo3_actor_define_create_actor_file,
|
||||||
|
|
||||||
|
// VEO3 场景写作命令
|
||||||
|
commands::veo3_actor_define_commands::veo3_scene_writer_check_availability,
|
||||||
|
commands::veo3_actor_define_commands::veo3_scene_writer_send_message,
|
||||||
|
commands::veo3_actor_define_commands::veo3_scene_writer_send_message_with_attachments,
|
||||||
|
commands::veo3_actor_define_commands::veo3_scene_writer_get_conversation_history,
|
||||||
|
commands::veo3_actor_define_commands::veo3_scene_writer_clear_conversation,
|
||||||
|
commands::veo3_actor_define_commands::veo3_scene_writer_remove_session,
|
||||||
|
commands::veo3_actor_define_commands::veo3_scene_writer_get_active_sessions
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
// 初始化日志系统
|
// 初始化日志系统
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use tauri::State;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use chrono;
|
use chrono;
|
||||||
use veo3_scene_writer::Veo3ActorDefine;
|
use veo3_scene_writer::{Veo3ActorDefine, Veo3SceneWriter};
|
||||||
use crate::infrastructure::tolerant_json_parser::TolerantJsonParser;
|
use crate::infrastructure::tolerant_json_parser::TolerantJsonParser;
|
||||||
|
|
||||||
/// VEO3 角色定义响应结构
|
/// VEO3 角色定义响应结构
|
||||||
@@ -20,6 +20,9 @@ pub struct Veo3ActorDefineResponse {
|
|||||||
/// VEO3 角色定义会话管理器
|
/// VEO3 角色定义会话管理器
|
||||||
pub type Veo3ActorDefineSessionManager = Mutex<HashMap<String, Veo3ActorDefine>>;
|
pub type Veo3ActorDefineSessionManager = Mutex<HashMap<String, Veo3ActorDefine>>;
|
||||||
|
|
||||||
|
/// VEO3 场景写作会话管理器
|
||||||
|
pub type Veo3SceneWriterSessionManager = Mutex<HashMap<String, Veo3SceneWriter>>;
|
||||||
|
|
||||||
/// 检查 VEO3 角色定义服务可用性
|
/// 检查 VEO3 角色定义服务可用性
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn veo3_actor_define_check_availability() -> Result<(), String> {
|
pub async fn veo3_actor_define_check_availability() -> Result<(), String> {
|
||||||
@@ -305,6 +308,168 @@ pub async fn veo3_actor_define_create_actor_file(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== VEO3 场景写作命令 ====================
|
||||||
|
|
||||||
|
/// 检查 VEO3 场景写作服务可用性
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn veo3_scene_writer_check_availability() -> Result<(), String> {
|
||||||
|
// 尝试创建一个实例来检查服务是否可用
|
||||||
|
match Veo3SceneWriter::new().await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(format!("VEO3 场景写作服务不可用: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送消息到 VEO3 场景写作工具
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn veo3_scene_writer_send_message(
|
||||||
|
session_id: String,
|
||||||
|
message: String,
|
||||||
|
session_manager: State<'_, Veo3SceneWriterSessionManager>,
|
||||||
|
) -> Result<Veo3ActorDefineResponse, String> {
|
||||||
|
// 检查会话是否存在,如果不存在则创建
|
||||||
|
let session_exists = {
|
||||||
|
let sessions = session_manager.lock().await;
|
||||||
|
sessions.contains_key(&session_id)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !session_exists {
|
||||||
|
let new_scene_writer = Veo3SceneWriter::new()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("创建 VEO3 场景写作实例失败: {}", e))?;
|
||||||
|
|
||||||
|
let mut sessions = session_manager.lock().await;
|
||||||
|
sessions.insert(session_id.clone(), new_scene_writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
let (response_result, conversation_history) = {
|
||||||
|
let mut sessions = session_manager.lock().await;
|
||||||
|
let scene_writer = sessions.get_mut(&session_id).unwrap();
|
||||||
|
|
||||||
|
let response = scene_writer.send_message(&message).await;
|
||||||
|
let history = scene_writer.get_conversation_history().clone();
|
||||||
|
(response, history)
|
||||||
|
};
|
||||||
|
|
||||||
|
match response_result {
|
||||||
|
Ok(response) => Ok(Veo3ActorDefineResponse {
|
||||||
|
success: true,
|
||||||
|
content: response,
|
||||||
|
error: None,
|
||||||
|
conversation_history: Some(conversation_history),
|
||||||
|
}),
|
||||||
|
Err(e) => Ok(Veo3ActorDefineResponse {
|
||||||
|
success: false,
|
||||||
|
content: String::new(),
|
||||||
|
error: Some(format!("发送消息失败: {}", e)),
|
||||||
|
conversation_history: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送带附件的消息到 VEO3 场景写作工具
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn veo3_scene_writer_send_message_with_attachments(
|
||||||
|
session_id: String,
|
||||||
|
message: String,
|
||||||
|
attachment_paths: Vec<String>,
|
||||||
|
session_manager: State<'_, Veo3SceneWriterSessionManager>,
|
||||||
|
) -> Result<Veo3ActorDefineResponse, String> {
|
||||||
|
// 检查会话是否存在,如果不存在则创建
|
||||||
|
let session_exists = {
|
||||||
|
let sessions = session_manager.lock().await;
|
||||||
|
sessions.contains_key(&session_id)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !session_exists {
|
||||||
|
let new_scene_writer = Veo3SceneWriter::new()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("创建 VEO3 场景写作实例失败: {}", e))?;
|
||||||
|
|
||||||
|
let mut sessions = session_manager.lock().await;
|
||||||
|
sessions.insert(session_id.clone(), new_scene_writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换路径为字符串引用
|
||||||
|
let attachment_refs: Vec<&str> = attachment_paths.iter().map(|s| s.as_str()).collect();
|
||||||
|
|
||||||
|
// 发送带附件的消息
|
||||||
|
let (response_result, conversation_history) = {
|
||||||
|
let mut sessions = session_manager.lock().await;
|
||||||
|
let scene_writer = sessions.get_mut(&session_id).unwrap();
|
||||||
|
|
||||||
|
let response = scene_writer.send_message_with_attachments(&message, attachment_refs).await;
|
||||||
|
let history = scene_writer.get_conversation_history().clone();
|
||||||
|
(response, history)
|
||||||
|
};
|
||||||
|
|
||||||
|
match response_result {
|
||||||
|
Ok(response) => Ok(Veo3ActorDefineResponse {
|
||||||
|
success: true,
|
||||||
|
content: response,
|
||||||
|
error: None,
|
||||||
|
conversation_history: Some(conversation_history),
|
||||||
|
}),
|
||||||
|
Err(e) => Ok(Veo3ActorDefineResponse {
|
||||||
|
success: false,
|
||||||
|
content: String::new(),
|
||||||
|
error: Some(format!("发送带附件消息失败: {}", e)),
|
||||||
|
conversation_history: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 VEO3 场景写作会话历史记录
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn veo3_scene_writer_get_conversation_history(
|
||||||
|
session_id: String,
|
||||||
|
session_manager: State<'_, Veo3SceneWriterSessionManager>,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
let sessions = session_manager.lock().await;
|
||||||
|
|
||||||
|
if let Some(scene_writer) = sessions.get(&session_id) {
|
||||||
|
Ok(scene_writer.get_conversation_history().clone())
|
||||||
|
} else {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清除 VEO3 场景写作会话历史记录
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn veo3_scene_writer_clear_conversation(
|
||||||
|
session_id: String,
|
||||||
|
session_manager: State<'_, Veo3SceneWriterSessionManager>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut sessions = session_manager.lock().await;
|
||||||
|
|
||||||
|
if let Some(scene_writer) = sessions.get_mut(&session_id) {
|
||||||
|
scene_writer.clear_conversation();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除 VEO3 场景写作会话
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn veo3_scene_writer_remove_session(
|
||||||
|
session_id: String,
|
||||||
|
session_manager: State<'_, Veo3SceneWriterSessionManager>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut sessions = session_manager.lock().await;
|
||||||
|
sessions.remove(&session_id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取所有活跃的 VEO3 场景写作会话ID
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn veo3_scene_writer_get_active_sessions(
|
||||||
|
session_manager: State<'_, Veo3SceneWriterSessionManager>,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
let sessions = session_manager.lock().await;
|
||||||
|
Ok(sessions.keys().cloned().collect())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -319,6 +484,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_veo3_scene_writer_check_availability() {
|
||||||
|
// 注意:这个测试需要有效的 API 配置才能通过
|
||||||
|
if std::env::var("GEMINI_BEARER_TOKEN").is_ok() {
|
||||||
|
let result = veo3_scene_writer_check_availability().await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_session_management() {
|
async fn test_session_management() {
|
||||||
let session_manager = Arc::new(Veo3ActorDefineSessionManager::new(HashMap::new()));
|
let session_manager = Arc::new(Veo3ActorDefineSessionManager::new(HashMap::new()));
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { EnrichedAnalysisDemo } from './pages/tools/EnrichedAnalysisDemo';
|
|||||||
import AiModelFaceHairFixTool from './pages/tools/AiModelFaceHairFixTool';
|
import AiModelFaceHairFixTool from './pages/tools/AiModelFaceHairFixTool';
|
||||||
import TopazVideoAITool from './pages/tools/TopazVideoAITool';
|
import TopazVideoAITool from './pages/tools/TopazVideoAITool';
|
||||||
import Veo3ActorDefineTool from './pages/tools/Veo3ActorDefineTool';
|
import Veo3ActorDefineTool from './pages/tools/Veo3ActorDefineTool';
|
||||||
|
import Veo3SceneWriterTool from './pages/tools/Veo3SceneWriterTool';
|
||||||
import CommandGenerator from './components/CommandGenerator';
|
import CommandGenerator from './components/CommandGenerator';
|
||||||
import MaterialCenter from './pages/MaterialCenter';
|
import MaterialCenter from './pages/MaterialCenter';
|
||||||
import VideoGeneration from './pages/VideoGeneration';
|
import VideoGeneration from './pages/VideoGeneration';
|
||||||
@@ -185,6 +186,7 @@ function App() {
|
|||||||
<Route path="/tools/ai-model-face-hair-fix" element={<AiModelFaceHairFixTool />} />
|
<Route path="/tools/ai-model-face-hair-fix" element={<AiModelFaceHairFixTool />} />
|
||||||
<Route path="/tools/topaz-video-ai" element={<TopazVideoAITool />} />
|
<Route path="/tools/topaz-video-ai" element={<TopazVideoAITool />} />
|
||||||
<Route path="/tools/veo3-actor-define" element={<Veo3ActorDefineTool />} />
|
<Route path="/tools/veo3-actor-define" element={<Veo3ActorDefineTool />} />
|
||||||
|
<Route path="/tools/veo3-scene-writer" element={<Veo3SceneWriterTool />} />
|
||||||
<Route path="/tools/command-generator" element={<CommandGenerator />} />
|
<Route path="/tools/command-generator" element={<CommandGenerator />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Frame,
|
Frame,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
User,
|
User,
|
||||||
|
Clapperboard,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Tool, ToolCategory, ToolStatus } from '../types/tool';
|
import { Tool, ToolCategory, ToolStatus } from '../types/tool';
|
||||||
|
|
||||||
@@ -35,6 +36,21 @@ export const TOOLS_DATA: Tool[] = [
|
|||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
lastUpdated: '2025-08-15'
|
lastUpdated: '2025-08-15'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'veo3-scene-writer',
|
||||||
|
name: 'VEO3 场景写作工具',
|
||||||
|
description: '专业的影视场景提示词生成助手,支持图片上传和智能对话,创建电影级别的场景描述',
|
||||||
|
longDescription: '基于 VEO3 场景写作系统的专业场景提示词生成工具,集成 Gemini AI 提供智能对话功能。支持上传参考图片进行场景分析,或通过对话生成全新场景。能够创建包含视觉效果、镜头语言、环境氛围等完整场景描述,输出专业的提示词格式。适用于影视制作、广告创意、短视频制作等需要场景设计的场景。',
|
||||||
|
icon: Clapperboard,
|
||||||
|
route: '/tools/veo3-scene-writer',
|
||||||
|
category: ToolCategory.AI_TOOLS,
|
||||||
|
status: ToolStatus.STABLE,
|
||||||
|
tags: ['场景写作', 'VEO3', '影视制作', 'AI对话', '图片分析', '提示词生成'],
|
||||||
|
isNew: true,
|
||||||
|
isPopular: true,
|
||||||
|
version: '1.0.0',
|
||||||
|
lastUpdated: '2025-08-15'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'topaz-video-ai',
|
id: 'topaz-video-ai',
|
||||||
name: 'Topaz Video AI 参数配置器',
|
name: 'Topaz Video AI 参数配置器',
|
||||||
|
|||||||
481
apps/desktop/src/pages/tools/Veo3SceneWriterTool.tsx
Normal file
481
apps/desktop/src/pages/tools/Veo3SceneWriterTool.tsx
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Send,
|
||||||
|
AlertCircle,
|
||||||
|
Sparkles,
|
||||||
|
Upload,
|
||||||
|
FileImage,
|
||||||
|
X,
|
||||||
|
Video,
|
||||||
|
MessageSquare,
|
||||||
|
RefreshCw,
|
||||||
|
Copy,
|
||||||
|
CheckCircle,
|
||||||
|
Clapperboard,
|
||||||
|
FolderOpen
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
|
import {
|
||||||
|
veo3SceneWriterService,
|
||||||
|
Veo3SceneWriterResponse
|
||||||
|
} from '../../services/veo3SceneWriterService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VEO3 场景写作消息接口
|
||||||
|
*/
|
||||||
|
interface SceneWriterMessage {
|
||||||
|
id: string;
|
||||||
|
type: 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
timestamp: Date;
|
||||||
|
status?: 'sending' | 'sent' | 'error';
|
||||||
|
attachments?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VEO3 场景写作工具页面
|
||||||
|
* 集成 ag-ui 实现对话和本地附件选择功能
|
||||||
|
*/
|
||||||
|
export const Veo3SceneWriterTool: React.FC = () => {
|
||||||
|
const [messages, setMessages] = useState<SceneWriterMessage[]>([]);
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
||||||
|
const [serviceAvailable, setServiceAvailable] = useState<boolean | null>(null);
|
||||||
|
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 检查服务可用性
|
||||||
|
useEffect(() => {
|
||||||
|
const checkService = async () => {
|
||||||
|
const available = await veo3SceneWriterService.checkServiceAvailability();
|
||||||
|
setServiceAvailable(available);
|
||||||
|
};
|
||||||
|
checkService();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 自动滚动到底部
|
||||||
|
const scrollToBottom = useCallback(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages, scrollToBottom]);
|
||||||
|
|
||||||
|
// 生成消息ID
|
||||||
|
const generateMessageId = useCallback(() => {
|
||||||
|
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 选择文件
|
||||||
|
const handleSelectFiles = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const selected = await open({
|
||||||
|
multiple: true,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: 'Images',
|
||||||
|
extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Videos',
|
||||||
|
extensions: ['mp4', 'avi', 'mov', 'mkv', 'webm']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'All Files',
|
||||||
|
extensions: ['*']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selected && Array.isArray(selected)) {
|
||||||
|
setSelectedFiles(prev => [...prev, ...selected]);
|
||||||
|
} else if (selected && typeof selected === 'string') {
|
||||||
|
setSelectedFiles(prev => [...prev, selected]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('文件选择失败:', error);
|
||||||
|
setError('文件选择失败');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 移除选中的文件
|
||||||
|
const removeSelectedFile = useCallback((index: number) => {
|
||||||
|
setSelectedFiles(prev => prev.filter((_, i) => i !== index));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 清空选中的文件
|
||||||
|
const clearSelectedFiles = useCallback(() => {
|
||||||
|
setSelectedFiles([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
const handleSendMessage = useCallback(async () => {
|
||||||
|
if ((!input.trim() && selectedFiles.length === 0) || isLoading) return;
|
||||||
|
|
||||||
|
const userMessage: SceneWriterMessage = {
|
||||||
|
id: generateMessageId(),
|
||||||
|
type: 'user',
|
||||||
|
content: input.trim() || '(附件)',
|
||||||
|
timestamp: new Date(),
|
||||||
|
status: 'sent',
|
||||||
|
attachments: selectedFiles.length > 0 ? [...selectedFiles] : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
const assistantMessage: SceneWriterMessage = {
|
||||||
|
id: generateMessageId(),
|
||||||
|
type: 'assistant',
|
||||||
|
content: '',
|
||||||
|
timestamp: new Date(),
|
||||||
|
status: 'sending'
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, userMessage, assistantMessage]);
|
||||||
|
setInput('');
|
||||||
|
setSelectedFiles([]);
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let response: Veo3SceneWriterResponse;
|
||||||
|
|
||||||
|
if (selectedFiles.length > 0) {
|
||||||
|
response = await veo3SceneWriterService.sendMessageWithAttachments(
|
||||||
|
userMessage.content,
|
||||||
|
selectedFiles
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
response = await veo3SceneWriterService.sendMessage(userMessage.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
setMessages(prev => prev.map(msg =>
|
||||||
|
msg.id === assistantMessage.id
|
||||||
|
? {
|
||||||
|
...msg,
|
||||||
|
content: response.content,
|
||||||
|
status: 'sent'
|
||||||
|
}
|
||||||
|
: msg
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || '发送失败');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : '未知错误';
|
||||||
|
setError(errorMessage);
|
||||||
|
|
||||||
|
setMessages(prev => prev.map(msg =>
|
||||||
|
msg.id === assistantMessage.id
|
||||||
|
? {
|
||||||
|
...msg,
|
||||||
|
content: `抱歉,处理时出现错误:${errorMessage}`,
|
||||||
|
status: 'error'
|
||||||
|
}
|
||||||
|
: msg
|
||||||
|
));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [input, selectedFiles, isLoading, generateMessageId]);
|
||||||
|
|
||||||
|
// 处理键盘事件
|
||||||
|
const handleKeyPress = useCallback((e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSendMessage();
|
||||||
|
}
|
||||||
|
}, [handleSendMessage]);
|
||||||
|
|
||||||
|
// 清除会话
|
||||||
|
const handleClearConversation = useCallback(async () => {
|
||||||
|
const success = await veo3SceneWriterService.clearConversation();
|
||||||
|
if (success) {
|
||||||
|
setMessages([]);
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 重置会话
|
||||||
|
const handleResetSession = useCallback(() => {
|
||||||
|
veo3SceneWriterService.resetSession();
|
||||||
|
setMessages([]);
|
||||||
|
setError(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 复制消息内容
|
||||||
|
const handleCopyMessage = useCallback(async (content: string, messageId: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
setCopiedMessageId(messageId);
|
||||||
|
setTimeout(() => setCopiedMessageId(null), 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制失败:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 如果服务不可用,显示错误状态
|
||||||
|
if (serviceAvailable === false) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full p-8 text-center">
|
||||||
|
<AlertCircle className="w-16 h-16 text-red-500 mb-4" />
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800 mb-2">VEO3 场景写作服务不可用</h2>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
请确保后端服务正在运行,并且已正确配置 Gemini API。
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
重新加载
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full bg-gradient-to-br from-blue-50 to-indigo-50">
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="flex-shrink-0 bg-white border-b border-gray-200 p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-500 rounded-lg flex items-center justify-center">
|
||||||
|
<Clapperboard className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-semibold text-gray-800">VEO3 场景写作工具</h1>
|
||||||
|
<p className="text-sm text-gray-600">专业的影视场景提示词生成助手</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleClearConversation}
|
||||||
|
className="px-3 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-4 h-4 mr-1 inline" />
|
||||||
|
清除对话
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleResetSession}
|
||||||
|
className="px-3 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-1 inline" />
|
||||||
|
重置会话
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 聊天消息区域 */}
|
||||||
|
<div className="flex-1 p-4 space-y-6 overflow-y-auto">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||||
|
<div className="w-20 h-20 bg-gradient-to-br from-blue-100 to-indigo-100 rounded-full flex items-center justify-center mb-6">
|
||||||
|
<Sparkles className="w-10 h-10 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600 max-w-lg mb-8 text-lg leading-relaxed">
|
||||||
|
我是 VEO3 场景写作专家,帮助您创建专业的影视场景提示词。您可以上传参考图片或描述场景需求。
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-2xl">
|
||||||
|
{[
|
||||||
|
'我想创建一个场景提示词',
|
||||||
|
'上传场景参考图片',
|
||||||
|
'生成电影级别的场景',
|
||||||
|
'创建特定风格的场景'
|
||||||
|
].map((suggestion, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setInput(suggestion)}
|
||||||
|
className="px-3 py-2 text-sm text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-lg transition-all duration-200 hover:shadow-md hover:scale-105"
|
||||||
|
>
|
||||||
|
{suggestion}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{messages.map((message) => (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`max-w-[80%] rounded-lg p-4 ${
|
||||||
|
message.type === 'user'
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-white border border-gray-200 text-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* 消息内容 */}
|
||||||
|
<div className="whitespace-pre-wrap">{message.content}</div>
|
||||||
|
|
||||||
|
{/* 附件显示 */}
|
||||||
|
{message.attachments && message.attachments.length > 0 && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{message.attachments.map((file, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2 text-sm opacity-80">
|
||||||
|
<FileImage className="w-4 h-4" />
|
||||||
|
<span>{file.split('/').pop() || file}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 消息操作 */}
|
||||||
|
{message.type === 'assistant' && message.status === 'sent' && (
|
||||||
|
<div className="flex items-center gap-2 mt-2 pt-2 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopyMessage(message.content, message.id)}
|
||||||
|
className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
{copiedMessageId === message.id ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-3 h-3" />
|
||||||
|
已复制
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="w-3 h-3" />
|
||||||
|
复制
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 状态指示 */}
|
||||||
|
{message.status === 'sending' && (
|
||||||
|
<div className="flex items-center gap-2 mt-2 text-sm opacity-70">
|
||||||
|
<div className="animate-spin w-4 h-4 border-2 border-current border-t-transparent rounded-full"></div>
|
||||||
|
<span>正在处理...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{message.status === 'error' && (
|
||||||
|
<div className="flex items-center gap-2 mt-2 text-sm text-red-500">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
<span>发送失败</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 错误提示 */}
|
||||||
|
{error && (
|
||||||
|
<div className="mx-4 mb-2 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2">
|
||||||
|
<AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
|
||||||
|
<span className="text-red-700 text-sm">{error}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setError(null)}
|
||||||
|
className="ml-auto text-red-600 hover:text-red-800 text-sm font-medium"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 输入区域 */}
|
||||||
|
<div className="flex-shrink-0 bg-white border-t border-gray-200">
|
||||||
|
{/* 选中的文件显示 */}
|
||||||
|
{selectedFiles.length > 0 && (
|
||||||
|
<div className="border-b border-gray-100 p-3">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm font-medium text-gray-700">
|
||||||
|
已选择 {selectedFiles.length} 个文件
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={clearSelectedFiles}
|
||||||
|
className="text-xs text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedFiles.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center gap-2 bg-gray-100 rounded-lg px-3 py-1 text-sm"
|
||||||
|
>
|
||||||
|
<FileImage className="w-4 h-4 text-gray-500" />
|
||||||
|
<span className="max-w-32 truncate">{file.split('/').pop() || file}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => removeSelectedFile(index)}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 输入框 */}
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex gap-3 items-end">
|
||||||
|
<button
|
||||||
|
onClick={handleSelectFiles}
|
||||||
|
className="px-3 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 transition-colors flex items-center gap-2 flex-shrink-0"
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4" />
|
||||||
|
选择文件
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<textarea
|
||||||
|
ref={inputRef}
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
|
placeholder="描述您想要创建的场景,或上传参考图片..."
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||||
|
rows={1}
|
||||||
|
style={{
|
||||||
|
minHeight: '48px',
|
||||||
|
maxHeight: '120px',
|
||||||
|
height: 'auto'
|
||||||
|
}}
|
||||||
|
onInput={(e) => {
|
||||||
|
const target = e.target as HTMLTextAreaElement;
|
||||||
|
target.style.height = 'auto';
|
||||||
|
target.style.height = `${Math.min(target.scrollHeight, 120)}px`;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
disabled={(!input.trim() && selectedFiles.length === 0) || isLoading}
|
||||||
|
className="px-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:cursor-not-allowed transition-all duration-200 flex items-center justify-center flex-shrink-0"
|
||||||
|
style={{ height: '48px', minHeight: '48px' }}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full"></div>
|
||||||
|
) : (
|
||||||
|
<Send className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 text-center text-xs text-gray-500">
|
||||||
|
<span>按 Enter 发送,Shift + Enter 换行</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Veo3SceneWriterTool;
|
||||||
193
apps/desktop/src/services/veo3SceneWriterService.ts
Normal file
193
apps/desktop/src/services/veo3SceneWriterService.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VEO3 场景写作消息接口
|
||||||
|
*/
|
||||||
|
export interface Veo3SceneWriterMessage {
|
||||||
|
content: string;
|
||||||
|
attachments?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VEO3 场景写作响应接口
|
||||||
|
*/
|
||||||
|
export interface Veo3SceneWriterResponse {
|
||||||
|
success: boolean;
|
||||||
|
content: string;
|
||||||
|
error?: string;
|
||||||
|
conversation_history?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VEO3 场景写作会话状态接口
|
||||||
|
*/
|
||||||
|
export interface Veo3SceneWriterSession {
|
||||||
|
session_id: string;
|
||||||
|
conversation_history: string[];
|
||||||
|
created_at: string;
|
||||||
|
last_updated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VEO3 场景写作服务类
|
||||||
|
* 封装与 Rust 后端 veo3-scene-writer crate 的通信逻辑
|
||||||
|
*/
|
||||||
|
export class Veo3SceneWriterService {
|
||||||
|
private static instance: Veo3SceneWriterService;
|
||||||
|
private sessionId: string;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.sessionId = `veo3-scene-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取服务实例(单例模式)
|
||||||
|
*/
|
||||||
|
public static getInstance(): Veo3SceneWriterService {
|
||||||
|
if (!Veo3SceneWriterService.instance) {
|
||||||
|
Veo3SceneWriterService.instance = new Veo3SceneWriterService();
|
||||||
|
}
|
||||||
|
return Veo3SceneWriterService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送文本消息到 VEO3 场景写作工具
|
||||||
|
*/
|
||||||
|
public async sendMessage(message: string): Promise<Veo3SceneWriterResponse> {
|
||||||
|
try {
|
||||||
|
const response = await invoke<Veo3SceneWriterResponse>('veo3_scene_writer_send_message', {
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
message: message
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('VEO3 场景写作消息发送失败:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
content: '',
|
||||||
|
error: error instanceof Error ? error.message : '未知错误'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送带附件的消息到 VEO3 场景写作工具
|
||||||
|
*/
|
||||||
|
public async sendMessageWithAttachments(
|
||||||
|
message: string,
|
||||||
|
attachmentPaths: string[]
|
||||||
|
): Promise<Veo3SceneWriterResponse> {
|
||||||
|
try {
|
||||||
|
const response = await invoke<Veo3SceneWriterResponse>('veo3_scene_writer_send_message_with_attachments', {
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
message: message,
|
||||||
|
attachmentPaths: attachmentPaths
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('VEO3 场景写作带附件消息发送失败:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
content: '',
|
||||||
|
error: error instanceof Error ? error.message : '未知错误'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话历史记录
|
||||||
|
*/
|
||||||
|
public async getConversationHistory(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const history = await invoke<string[]>('veo3_scene_writer_get_conversation_history', {
|
||||||
|
sessionId: this.sessionId
|
||||||
|
});
|
||||||
|
|
||||||
|
return history;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取 VEO3 场景写作会话历史失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除会话历史记录
|
||||||
|
*/
|
||||||
|
public async clearConversation(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await invoke('veo3_scene_writer_clear_conversation', {
|
||||||
|
sessionId: this.sessionId
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除 VEO3 场景写作会话历史失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前会话ID
|
||||||
|
*/
|
||||||
|
public getSessionId(): string {
|
||||||
|
return this.sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置会话(生成新的会话ID)
|
||||||
|
*/
|
||||||
|
public resetSession(): void {
|
||||||
|
this.sessionId = `veo3-scene-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查服务是否可用
|
||||||
|
*/
|
||||||
|
public async checkServiceAvailability(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await invoke('veo3_scene_writer_check_availability');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('VEO3 场景写作服务不可用:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出服务实例
|
||||||
|
*/
|
||||||
|
export const veo3SceneWriterService = Veo3SceneWriterService.getInstance();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷函数:发送消息
|
||||||
|
*/
|
||||||
|
export const sendVeo3SceneWriterMessage = (message: string): Promise<Veo3SceneWriterResponse> => {
|
||||||
|
return veo3SceneWriterService.sendMessage(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷函数:发送带附件的消息
|
||||||
|
*/
|
||||||
|
export const sendVeo3SceneWriterMessageWithAttachments = (
|
||||||
|
message: string,
|
||||||
|
attachmentPaths: string[]
|
||||||
|
): Promise<Veo3SceneWriterResponse> => {
|
||||||
|
return veo3SceneWriterService.sendMessageWithAttachments(message, attachmentPaths);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷函数:获取会话历史
|
||||||
|
*/
|
||||||
|
export const getVeo3SceneWriterConversationHistory = (): Promise<string[]> => {
|
||||||
|
return veo3SceneWriterService.getConversationHistory();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷函数:清除会话
|
||||||
|
*/
|
||||||
|
export const clearVeo3SceneWriterConversation = (): Promise<boolean> => {
|
||||||
|
return veo3SceneWriterService.clearConversation();
|
||||||
|
};
|
||||||
@@ -198,13 +198,24 @@ impl Veo3SceneWriter {
|
|||||||
message: &str,
|
message: &str,
|
||||||
attachment_paths: Vec<&str>
|
attachment_paths: Vec<&str>
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
// 处理文件内容,分离文本文件和图片文件
|
||||||
|
let (text_content, image_attachments) = self.process_attachments(attachment_paths).await?;
|
||||||
|
|
||||||
|
// 构建完整消息,包含文本文件内容
|
||||||
|
let mut full_message = if text_content.is_empty() {
|
||||||
|
message.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}\n\n附件内容:\n{}", message, text_content)
|
||||||
|
};
|
||||||
|
|
||||||
// 构建包含历史记录的完整消息
|
// 构建包含历史记录的完整消息
|
||||||
let full_message = self.build_message_with_history(message);
|
full_message = self.build_message_with_history(&full_message);
|
||||||
|
|
||||||
let mut request = MessageRequest::with_agent(&full_message, &self.agent_id);
|
let mut request = MessageRequest::with_agent(&full_message, &self.agent_id);
|
||||||
|
|
||||||
for path in attachment_paths {
|
// 只添加图片附件到请求中
|
||||||
request = request.attach(Attachment::new(path));
|
for path in image_attachments {
|
||||||
|
request = request.attach(Attachment::new(&path));
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = self.sdk.send_message(request).await?;
|
let response = self.sdk.send_message(request).await?;
|
||||||
@@ -215,6 +226,45 @@ impl Veo3SceneWriter {
|
|||||||
Ok(response.content)
|
Ok(response.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 处理附件,分离文本文件和图片文件
|
||||||
|
async fn process_attachments(
|
||||||
|
&self,
|
||||||
|
attachment_paths: Vec<&str>
|
||||||
|
) -> Result<(String, Vec<String>), Box<dyn std::error::Error>> {
|
||||||
|
let mut text_content = Vec::new();
|
||||||
|
let mut image_attachments = Vec::new();
|
||||||
|
|
||||||
|
for path in attachment_paths {
|
||||||
|
let path_lower = path.to_lowercase();
|
||||||
|
|
||||||
|
// 检查文件扩展名
|
||||||
|
if path_lower.ends_with(".json") || path_lower.ends_with(".txt") ||
|
||||||
|
path_lower.ends_with(".md") || path_lower.ends_with(".yaml") ||
|
||||||
|
path_lower.ends_with(".yml") || path_lower.ends_with(".toml") {
|
||||||
|
// 读取文本文件内容
|
||||||
|
match std::fs::read_to_string(path) {
|
||||||
|
Ok(content) => {
|
||||||
|
let file_name = std::path::Path::new(path)
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
text_content.push(format!("=== {} ===\n{}", file_name, content));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("读取文件失败 {}: {}", path, e);
|
||||||
|
text_content.push(format!("=== {} ===\n[文件读取失败: {}]", path, e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 图片文件或其他文件,作为附件处理
|
||||||
|
image_attachments.push(path.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let combined_text = text_content.join("\n\n");
|
||||||
|
Ok((combined_text, image_attachments))
|
||||||
|
}
|
||||||
|
|
||||||
/// 发送带单个附件的消息
|
/// 发送带单个附件的消息
|
||||||
pub async fn send_message_with_attachment(
|
pub async fn send_message_with_attachment(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -357,13 +407,24 @@ impl Veo3ActorDefine {
|
|||||||
message: &str,
|
message: &str,
|
||||||
attachment_paths: Vec<&str>
|
attachment_paths: Vec<&str>
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
// 处理文件内容,分离文本文件和图片文件
|
||||||
|
let (text_content, image_attachments) = self.process_attachments(attachment_paths).await?;
|
||||||
|
|
||||||
|
// 构建完整消息,包含文本文件内容
|
||||||
|
let mut full_message = if text_content.is_empty() {
|
||||||
|
message.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}\n\n附件内容:\n{}", message, text_content)
|
||||||
|
};
|
||||||
|
|
||||||
// 构建包含历史记录的完整消息
|
// 构建包含历史记录的完整消息
|
||||||
let full_message = self.build_message_with_history(message);
|
full_message = self.build_message_with_history(&full_message);
|
||||||
|
|
||||||
let mut request = MessageRequest::with_agent(&full_message, &self.agent_id);
|
let mut request = MessageRequest::with_agent(&full_message, &self.agent_id);
|
||||||
|
|
||||||
for path in attachment_paths {
|
// 只添加图片附件到请求中
|
||||||
request = request.attach(Attachment::new(path));
|
for path in image_attachments {
|
||||||
|
request = request.attach(Attachment::new(&path));
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = self.sdk.send_message(request).await?;
|
let response = self.sdk.send_message(request).await?;
|
||||||
@@ -374,6 +435,45 @@ impl Veo3ActorDefine {
|
|||||||
Ok(response.content)
|
Ok(response.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 处理附件,分离文本文件和图片文件
|
||||||
|
async fn process_attachments(
|
||||||
|
&self,
|
||||||
|
attachment_paths: Vec<&str>
|
||||||
|
) -> Result<(String, Vec<String>), Box<dyn std::error::Error>> {
|
||||||
|
let mut text_content = Vec::new();
|
||||||
|
let mut image_attachments = Vec::new();
|
||||||
|
|
||||||
|
for path in attachment_paths {
|
||||||
|
let path_lower = path.to_lowercase();
|
||||||
|
|
||||||
|
// 检查文件扩展名
|
||||||
|
if path_lower.ends_with(".json") || path_lower.ends_with(".txt") ||
|
||||||
|
path_lower.ends_with(".md") || path_lower.ends_with(".yaml") ||
|
||||||
|
path_lower.ends_with(".yml") || path_lower.ends_with(".toml") {
|
||||||
|
// 读取文本文件内容
|
||||||
|
match std::fs::read_to_string(path) {
|
||||||
|
Ok(content) => {
|
||||||
|
let file_name = std::path::Path::new(path)
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
text_content.push(format!("=== {} ===\n{}", file_name, content));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("读取文件失败 {}: {}", path, e);
|
||||||
|
text_content.push(format!("=== {} ===\n[文件读取失败: {}]", path, e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 图片文件或其他文件,作为附件处理
|
||||||
|
image_attachments.push(path.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let combined_text = text_content.join("\n\n");
|
||||||
|
Ok((combined_text, image_attachments))
|
||||||
|
}
|
||||||
|
|
||||||
/// 发送带单个附件的消息
|
/// 发送带单个附件的消息
|
||||||
pub async fn send_message_with_attachment(
|
pub async fn send_message_with_attachment(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|||||||
Reference in New Issue
Block a user