feat: 添加 VEO3 场景写作工具并优化文件处理逻辑

- 创建 VEO3SceneWriterTool 页面组件,集成聊天界面和文件选择功能
- 添加 veo3SceneWriterService 服务层,封装与 Rust 后端的通信逻辑
- 实现 Tauri 命令支持,调用 veo3-scene-writer crate
- 更新工具数据配置,添加 VEO3 场景写作工具
- 优化文件处理逻辑:JSON/TXT 文件读取内容作为消息,图片文件作为附件
- 支持多种文本格式:.json, .txt, .md, .yaml, .yml, .toml
- 提供专业的影视场景提示词生成功能
This commit is contained in:
imeepos
2025-08-18 10:14:01 +08:00
parent 9ba62d9e06
commit e2a1f43e85
7 changed files with 984 additions and 8 deletions

View File

@@ -41,6 +41,7 @@ pub fn run() {
.manage(commands::image_editing_commands::ImageEditingState::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::Veo3SceneWriterSessionManager::new(std::collections::HashMap::new()))
.invoke_handler(tauri::generate_handler![
commands::project_commands::create_project,
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_remove_session,
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| {
// 初始化日志系统

View File

@@ -5,7 +5,7 @@ use tauri::State;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use chrono;
use veo3_scene_writer::Veo3ActorDefine;
use veo3_scene_writer::{Veo3ActorDefine, Veo3SceneWriter};
use crate::infrastructure::tolerant_json_parser::TolerantJsonParser;
/// VEO3 角色定义响应结构
@@ -20,6 +20,9 @@ pub struct Veo3ActorDefineResponse {
/// VEO3 角色定义会话管理器
pub type Veo3ActorDefineSessionManager = Mutex<HashMap<String, Veo3ActorDefine>>;
/// VEO3 场景写作会话管理器
pub type Veo3SceneWriterSessionManager = Mutex<HashMap<String, Veo3SceneWriter>>;
/// 检查 VEO3 角色定义服务可用性
#[tauri::command]
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)]
mod tests {
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]
async fn test_session_management() {
let session_manager = Arc::new(Veo3ActorDefineSessionManager::new(HashMap::new()));

View File

@@ -37,6 +37,7 @@ import { EnrichedAnalysisDemo } from './pages/tools/EnrichedAnalysisDemo';
import AiModelFaceHairFixTool from './pages/tools/AiModelFaceHairFixTool';
import TopazVideoAITool from './pages/tools/TopazVideoAITool';
import Veo3ActorDefineTool from './pages/tools/Veo3ActorDefineTool';
import Veo3SceneWriterTool from './pages/tools/Veo3SceneWriterTool';
import CommandGenerator from './components/CommandGenerator';
import MaterialCenter from './pages/MaterialCenter';
import VideoGeneration from './pages/VideoGeneration';
@@ -185,6 +186,7 @@ function App() {
<Route path="/tools/ai-model-face-hair-fix" element={<AiModelFaceHairFixTool />} />
<Route path="/tools/topaz-video-ai" element={<TopazVideoAITool />} />
<Route path="/tools/veo3-actor-define" element={<Veo3ActorDefineTool />} />
<Route path="/tools/veo3-scene-writer" element={<Veo3SceneWriterTool />} />
<Route path="/tools/command-generator" element={<CommandGenerator />} />
</Routes>
</div>

View File

@@ -12,6 +12,7 @@ import {
Frame,
Sparkles,
User,
Clapperboard,
} from 'lucide-react';
import { Tool, ToolCategory, ToolStatus } from '../types/tool';
@@ -35,6 +36,21 @@ export const TOOLS_DATA: Tool[] = [
version: '1.0.0',
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',
name: 'Topaz Video AI 参数配置器',

View 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;

View 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();
};

View File

@@ -198,13 +198,24 @@ impl Veo3SceneWriter {
message: &str,
attachment_paths: Vec<&str>
) -> 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);
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?;
@@ -215,6 +226,45 @@ impl Veo3SceneWriter {
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(
&mut self,
@@ -357,13 +407,24 @@ impl Veo3ActorDefine {
message: &str,
attachment_paths: Vec<&str>
) -> 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);
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?;
@@ -374,6 +435,45 @@ impl Veo3ActorDefine {
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(
&mut self,