Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/apps/desktop/src/api/browser_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ pub async fn browser_webview_create(
let window = app
.get_window("main")
.ok_or_else(|| "main window not found".to_string())?;
#[allow(unused_mut)]
let mut builder =
tauri::webview::WebviewBuilder::new(request.label, tauri::WebviewUrl::External(url))
.initialization_script(video_decoder_compatibility_script())
Expand Down
1 change: 1 addition & 0 deletions src/apps/desktop/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod diff_api;
pub mod dto;
pub mod editor_ai_api;
pub mod external_sources_api;
pub mod video_api;
pub mod git_agent_api;
pub mod git_api;
pub mod i18n_api;
Expand Down
150 changes: 150 additions & 0 deletions src/apps/desktop/src/api/video_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//! Video processing API (gateway to external multimedia tools).
//!
//! All commands are gated behind `#[cfg(feature = "video")]`.
//! When the feature is disabled, no-op stubs return an error so the
//! invoke_handler macro always sees valid symbols.
//!
//! ## 第三方依赖与许可
//!
//! - **FFmpeg** (LGPL 2.1+) — 通过外部命令调用,未捆绑分发。https://ffmpeg.org
//! - **ffmpeg-sidecar** (MIT) — Rust 封装。https://github.com/nathanbabcock/ffmpeg-sidecar (crates.io)

#[cfg(feature = "video")]
use std::path::Path;

/// Validate that a path is safe for ffmpeg to access:
/// - Must resolve to an absolute path within an allowed directory tree
/// - Must exist (for input paths)
/// On Windows, strips the UNC `\\?\` prefix added by canonicalize() for compatibility.
#[cfg(feature = "video")]
fn validate_input_path(path_str: &str) -> Result<std::path::PathBuf, String> {
let path = Path::new(path_str);
let canonical = path
.canonicalize()
.map_err(|e| format!("Invalid or inaccessible path '{}': {e}", path_str))?;
// Strip UNC prefix on Windows for ffmpeg compatibility
let canonical = dunce::simplified(&canonical).to_path_buf();
// Reject paths that resolve to system directories
let canonical_str = canonical.to_string_lossy();
if canonical_str.starts_with("/etc/")
|| canonical_str.starts_with("/proc/")
|| canonical_str.starts_with("/sys/")
|| canonical_str.starts_with("/dev/")
|| canonical_str.starts_with("C:\\Windows")
|| canonical_str.starts_with("C:\\windows")
{
return Err(format!(
"Path '{}' is in a protected system directory",
canonical_str
));
}
Ok(canonical)
}

// ---------------------------------------------------------------------------
// Real implementations (compiled only when "video" feature is enabled)
// ---------------------------------------------------------------------------

#[tauri::command]
#[cfg(feature = "video")]
pub async fn video_execute(args: Vec<String>) -> Result<String, String> {
use ffmpeg_sidecar::command::FfmpegCommand;

let mut cmd = FfmpegCommand::new();
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg == "-i" && i + 1 < args.len() {
let safe_path = validate_input_path(&args[i + 1])?;
cmd.arg("-i");
cmd.arg(safe_path.to_string_lossy().as_ref());
i += 2;
} else {
cmd.arg(arg);
i += 1;
}
}
// Restrict to safe protocols only (prevent concat:/subfile:/crypto: SSRF)
cmd.arg("-protocol_whitelist");
cmd.arg("file,http,https,tcp,tls");
let output: Vec<String> = cmd
.spawn()
.map_err(|e| format!("ffmpeg spawn failed: {e}"))?
.iter()
.map_err(|e| format!("ffmpeg iteration failed: {e}"))?
.into_ffmpeg_stderr()
.collect();
Ok(output.join("\n"))
}

#[tauri::command]
#[cfg(feature = "video")]
pub async fn video_get_metadata(input_path: String) -> Result<serde_json::Value, String> {
use ffmpeg_sidecar::{command::FfmpegCommand, event::FfmpegEvent};

// Validate and canonicalize input path
let safe_path = validate_input_path(&input_path)?;

let iter = FfmpegCommand::new()
.input(safe_path.to_string_lossy().as_ref())
.hide_banner()
.spawn()
.map_err(|e| format!("ffmpeg metadata spawn failed: {e}"))?
.iter()
.map_err(|e| format!("ffmpeg metadata iteration failed: {e}"))?;

let mut duration: Option<f64> = None;
let mut format: Option<String> = None;
let mut width: Option<u32> = None;
let mut height: Option<u32> = None;
let mut fps: Option<f32> = None;
let mut stream_count: u32 = 0;

for event in iter {
match event {
FfmpegEvent::ParsedDuration(d) => {
duration = Some(d.duration);
}
FfmpegEvent::ParsedOutputStream(stream) => {
let stream_format = stream.format.clone();
if let Some(v) = stream.video_data() {
stream_count += 1;
// Keep the first video stream's metadata
if stream_count == 1 {
format = Some(stream_format);
width = Some(v.width);
height = Some(v.height);
fps = Some(v.fps);
}
}
}
FfmpegEvent::Done => break,
_ => {}
}
}

Ok(serde_json::json!({
"duration_secs": duration,
"format": format,
"width": width,
"height": height,
"fps": fps,
"stream_count": stream_count,
}))
}

// ---------------------------------------------------------------------------
// No-op stubs (compiled when "video" feature is disabled)
// ---------------------------------------------------------------------------

#[tauri::command]
#[cfg(not(feature = "video"))]
pub async fn video_execute(_args: Vec<String>) -> Result<String, String> {
Err("video feature not enabled".to_string())
}

#[tauri::command]
#[cfg(not(feature = "video"))]
pub async fn video_get_metadata(_input_path: String) -> Result<serde_json::Value, String> {
Err("video feature not enabled".to_string())
}
6 changes: 3 additions & 3 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1764,7 +1764,7 @@ fn setup_panic_hook() {

// Known wry bug: WKWebView.URL() returns nil after navigating to an
// invalid address, causing url_from_webview to panic on unwrap().
// This is non-fatal the webview is still alive so we log and
// This is non-fatal �?the webview is still alive �?so we log and
// continue instead of killing the process.
// See: https://github.com/tauri-apps/wry/pull/1554
if location.contains("wry") && location.contains("wkwebview") {
Expand All @@ -1781,13 +1781,13 @@ fn setup_panic_hook() {
}

// ── Recovery strategy ──────────────────────────────────────────
// Main-thread panics are unrecoverable the event loop is gone.
// Main-thread panics are unrecoverable �?the event loop is gone.
// Spawned-thread panics only kill that thread; the rest of the
// application can continue. We log a clear message and skip the
// hard exit so the user isn't forced to restart.
if !is_main_thread {
log::warn!(
"Non-main thread panicked application will continue. \
"Non-main thread panicked �?application will continue. \
The affected feature may be degraded until the next restart."
);
return;
Expand Down
1 change: 1 addition & 0 deletions src/crates/assembly/core/src/agentic/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
mod definitions;
mod prompt_builder;
mod registry;
pub mod team_presets;

use crate::agentic::session::{SystemPromptCacheIdentity, UserContextCacheIdentity};
use crate::agentic::tools::framework::ToolExposure;
Expand Down
Loading