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>,
}
use std::process::Command;
use tauri_plugin_shell::ShellExt;
#[derive(Debug, Serialize, Deserialize)]
pub struct VideoProcessRequest {
@ -64,8 +65,10 @@ pub struct AudioTrack {
pub volume: f64,
}
// Helper function to execute Python commands
async fn execute_python_command(args: &[String]) -> Result<String, String> {
// Helper function to execute Python commands using Tauri Shell Plugin
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);
// 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);
// Try python3 first, then python (for Windows compatibility)
let python_cmd = if cfg!(target_os = "windows") {
"python"
// Use Tauri Shell Plugin for better process management
let python_cmd = if cfg!(target_os = "windows") { "python" } else { "python3" };
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 {
"python3"
command
};
let mut cmd = Command::new(python_cmd);
cmd.current_dir(&project_root); // Set working directory to project root
let (mut rx, _child) = command
.spawn()
.map_err(|e| format!("Failed to spawn Python process: {}", e))?;
for arg in args {
cmd.arg(arg);
let mut stdout_lines = Vec::new();
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
.output()
.map_err(|e| {
let error_msg = format!("Failed to execute Python script: {} (args: {:?}, cwd: {:?})", e, args, project_root);
println!("Command execution failed: {}", error_msg);
error_msg
})?;
// Check if we have a successful termination event
let mut success = false;
let mut exit_code = None;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("Python script stdout: {}", stdout);
if !stderr.is_empty() {
println!("Python script stderr: {}", stderr);
// Look for termination event in our collected events
// If we received a Terminated event, we already have the exit status
if let Some(last_event) = rx.try_recv().ok() {
if let CommandEvent::Terminated(payload) = last_event {
success = payload.code == Some(0);
exit_code = payload.code;
}
}
println!("Python script exit code: {:?}", output.status.code());
if output.status.success() {
Ok(stdout.to_string())
// If no termination event was captured, assume success if we have output
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 {
let error_msg = format!(
"Python script failed with exit code {:?}. Stderr: {}. Stdout: {}",
output.status.code(),
stderr,
stdout
exit_code,
stderr_lines.join("\n"),
stdout_lines.join("\n")
);
println!("Python script error: {}", error_msg);
Err(error_msg)
@ -255,7 +305,7 @@ pub async fn load_project(project_path: String) -> Result<ProjectInfo, String> {
}
#[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![
"python_core/ai_video/video_generator.py".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());
}
execute_python_command(&args).await
execute_python_command(app, &args).await
}
#[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)
.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());
}
execute_python_command(&args).await
execute_python_command(app, &args).await
}
#[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...");
// Get project root directory

View File

@ -42,14 +42,6 @@ const AIVideoPage: React.FC = () => {
</div>
<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">
<Settings size={16} className="mr-2" />
@ -66,32 +58,6 @@ const AIVideoPage: React.FC = () => {
<div className="h-full flex">
{/* Main Content */}
<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" />
</div>