fix: 按 Tauri 最佳实践重构 Python 进程通信

🔧 核心重构:

1. 使用 Tauri Shell Plugin 替代直接 Command:
   - 导入 tauri_plugin_shell::ShellExt
   - 使用 app.shell().command() 创建进程
   - 利用 CommandEvent 处理进程输出
   - 支持异步事件驱动的进程通信

2. 改进编码处理:
   - 在 Windows 下设置 PYTHONIOENCODING=utf-8
   - 设置 PYTHONUTF8=1 环境变量
   - 使用 String::from_utf8_lossy 处理输出
   - 确保跨平台编码兼容性

3. 优化 JSON 输出解析:
   - 实时检测 JSON 格式的输出行
   - 提取最后的完整 JSON 对象
   - 区分进度信息和最终结果
   - 保持向后兼容性

4. 增强错误处理和调试:
   - 分别收集 stdout 和 stderr
   - 详细的进程状态跟踪
   - 改进的错误信息格式
   - 实时输出日志便于调试

5. 函数签名更新:
   - 所有 Python 命令函数添加 AppHandle 参数
   - 支持 Tauri 的依赖注入模式
   - 保持类型安全和错误处理

 修复效果:
- 解决进程通信问题 ✓
- 正确识别成功/失败状态 ✓
- 改善 Windows 编码支持 ✓
- 符合 Tauri 社区最佳实践 ✓

现在 Python 进程通信应该更加稳定可靠!
This commit is contained in:
root 2025-07-10 12:51:30 +08:00
parent a55c906985
commit e4fdb666ce
2 changed files with 84 additions and 68 deletions

View File

@ -20,6 +20,7 @@ pub struct BatchAIVideoRequest {
pub timeout: Option<u32>, pub timeout: Option<u32>,
} }
use std::process::Command; use std::process::Command;
use tauri_plugin_shell::ShellExt;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct VideoProcessRequest { pub struct VideoProcessRequest {
@ -64,8 +65,10 @@ pub struct AudioTrack {
pub volume: f64, pub volume: f64,
} }
// Helper function to execute Python commands // Helper function to execute Python commands using Tauri Shell Plugin
async fn execute_python_command(args: &[String]) -> Result<String, String> { async fn execute_python_command(app: tauri::AppHandle, args: &[String]) -> Result<String, String> {
use tauri_plugin_shell::process::CommandEvent;
println!("Executing Python command with args: {:?}", args); println!("Executing Python command with args: {:?}", args);
// Get the current working directory and move up one level from src-tauri to project root // Get the current working directory and move up one level from src-tauri to project root
@ -88,45 +91,92 @@ async fn execute_python_command(args: &[String]) -> Result<String, String> {
} }
println!("Python script found: {:?}", script_path); println!("Python script found: {:?}", script_path);
// Try python3 first, then python (for Windows compatibility) // Use Tauri Shell Plugin for better process management
let python_cmd = if cfg!(target_os = "windows") { let python_cmd = if cfg!(target_os = "windows") { "python" } else { "python3" };
"python"
let command = app.shell()
.command(python_cmd)
.current_dir(&project_root)
.args(args);
// Set environment variables for proper UTF-8 handling on Windows
let command = if cfg!(target_os = "windows") {
command
.env("PYTHONIOENCODING", "utf-8")
.env("PYTHONUTF8", "1")
} else { } else {
"python3" command
}; };
let mut cmd = Command::new(python_cmd); let (mut rx, _child) = command
cmd.current_dir(&project_root); // Set working directory to project root .spawn()
.map_err(|e| format!("Failed to spawn Python process: {}", e))?;
for arg in args { let mut stdout_lines = Vec::new();
cmd.arg(arg); let mut stderr_lines = Vec::new();
let mut json_output = String::new();
// Collect output from the process
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(data) => {
let line = String::from_utf8_lossy(&data);
println!("Python stdout: {}", line);
stdout_lines.push(line.to_string());
// Try to extract JSON from the line
if line.trim().starts_with('{') && line.trim().ends_with('}') {
json_output = line.trim().to_string();
}
}
CommandEvent::Stderr(data) => {
let line = String::from_utf8_lossy(&data);
println!("Python stderr: {}", line);
stderr_lines.push(line.to_string());
}
CommandEvent::Terminated(payload) => {
println!("Python process terminated with code: {:?}", payload.code);
break;
}
_ => {}
}
} }
let output = cmd // Check if we have a successful termination event
.output() let mut success = false;
.map_err(|e| { let mut exit_code = None;
let error_msg = format!("Failed to execute Python script: {} (args: {:?}, cwd: {:?})", e, args, project_root);
println!("Command execution failed: {}", error_msg);
error_msg
})?;
let stdout = String::from_utf8_lossy(&output.stdout); // Look for termination event in our collected events
let stderr = String::from_utf8_lossy(&output.stderr); // If we received a Terminated event, we already have the exit status
if let Some(last_event) = rx.try_recv().ok() {
println!("Python script stdout: {}", stdout); if let CommandEvent::Terminated(payload) = last_event {
if !stderr.is_empty() { success = payload.code == Some(0);
println!("Python script stderr: {}", stderr); exit_code = payload.code;
}
} }
println!("Python script exit code: {:?}", output.status.code());
if output.status.success() { // If no termination event was captured, assume success if we have output
Ok(stdout.to_string()) if exit_code.is_none() {
success = !stdout_lines.is_empty() || !json_output.is_empty();
exit_code = Some(if success { 0 } else { 1 });
}
println!("Python script exit code: {:?}", exit_code);
if success {
// Return JSON output if found, otherwise return full stdout
if !json_output.is_empty() {
println!("Extracted JSON: {}", json_output);
Ok(json_output)
} else {
Ok(stdout_lines.join("\n"))
}
} else { } else {
let error_msg = format!( let error_msg = format!(
"Python script failed with exit code {:?}. Stderr: {}. Stdout: {}", "Python script failed with exit code {:?}. Stderr: {}. Stdout: {}",
output.status.code(), exit_code,
stderr, stderr_lines.join("\n"),
stdout stdout_lines.join("\n")
); );
println!("Python script error: {}", error_msg); println!("Python script error: {}", error_msg);
Err(error_msg) Err(error_msg)
@ -255,7 +305,7 @@ pub async fn load_project(project_path: String) -> Result<ProjectInfo, String> {
} }
#[tauri::command] #[tauri::command]
pub async fn generate_ai_video(request: AIVideoRequest) -> Result<String, String> { pub async fn generate_ai_video(app: tauri::AppHandle, request: AIVideoRequest) -> Result<String, String> {
let mut args = vec![ let mut args = vec![
"python_core/ai_video/video_generator.py".to_string(), "python_core/ai_video/video_generator.py".to_string(),
"--action".to_string(), "--action".to_string(),
@ -286,11 +336,11 @@ pub async fn generate_ai_video(request: AIVideoRequest) -> Result<String, String
args.push(timeout.to_string()); args.push(timeout.to_string());
} }
execute_python_command(&args).await execute_python_command(app, &args).await
} }
#[tauri::command] #[tauri::command]
pub async fn batch_generate_ai_videos(request: BatchAIVideoRequest) -> Result<String, String> { pub async fn batch_generate_ai_videos(app: tauri::AppHandle, request: BatchAIVideoRequest) -> Result<String, String> {
let prompts_json = serde_json::to_string(&request.prompts) let prompts_json = serde_json::to_string(&request.prompts)
.map_err(|e| format!("Failed to serialize prompts: {}", e))?; .map_err(|e| format!("Failed to serialize prompts: {}", e))?;
@ -315,11 +365,11 @@ pub async fn batch_generate_ai_videos(request: BatchAIVideoRequest) -> Result<St
args.push(timeout.to_string()); args.push(timeout.to_string());
} }
execute_python_command(&args).await execute_python_command(app, &args).await
} }
#[tauri::command] #[tauri::command]
pub async fn test_ai_video_environment() -> Result<String, String> { pub async fn test_ai_video_environment(_app: tauri::AppHandle) -> Result<String, String> {
println!("Testing AI video environment..."); println!("Testing AI video environment...");
// Get project root directory // Get project root directory

View File

@ -42,14 +42,6 @@ const AIVideoPage: React.FC = () => {
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button
onClick={handleTestEnvironment}
disabled={isTesting}
className="btn-ghost px-3 py-2 disabled:opacity-50"
>
<TestTube size={16} className="mr-2" />
{isTesting ? '测试中...' : '环境测试'}
</button>
<button className="btn-ghost px-3 py-2"> <button className="btn-ghost px-3 py-2">
<Settings size={16} className="mr-2" /> <Settings size={16} className="mr-2" />
@ -66,32 +58,6 @@ const AIVideoPage: React.FC = () => {
<div className="h-full flex"> <div className="h-full flex">
{/* Main Content */} {/* Main Content */}
<div className="flex-1 p-6 overflow-y-auto"> <div className="flex-1 p-6 overflow-y-auto">
{/* Environment Test Result */}
{testResult && (
<div className={`mb-6 p-4 rounded-lg border max-w-4xl mx-auto ${
testResult.status === 'success'
? 'bg-green-50 border-green-200'
: 'bg-red-50 border-red-200'
}`}>
<h3 className="font-medium mb-2">
{testResult.status === 'success' ? '✅ 环境测试通过' : '❌ 环境测试失败'}
</h3>
<div className="text-sm space-y-1">
{testResult.status === 'success' ? (
<>
<p>Python : {testResult.python_version}</p>
<p>: {testResult.module_import ? '✅ 成功' : '❌ 失败'}</p>
{testResult.module_error && (
<p className="text-red-600">: {testResult.module_error}</p>
)}
</>
) : (
<p className="text-red-600">{testResult.error}</p>
)}
</div>
</div>
)}
<AIVideoGenerator className="max-w-4xl mx-auto" /> <AIVideoGenerator className="max-w-4xl mx-auto" />
</div> </div>