Frontend fixes: - fix(VideoCompose): clear step dirty flag after compose success - refactor(MyWorks): play product videos directly without transcode cache - feat(CoverDesign): swap main/subtitle positions in cover preview - fix(SubtitleBurning): charge points after burn success instead of before - fix(VoiceSynthesis/VideoGeneration/SubtitleBurning/CoverDesign): mark downstream steps dirty on re-generation - fix(MyWorks): bind video event listeners after async videoUrl load - fix(CoverDesign): revoke Blob URLs on upload/unmount to prevent memory leak
23 KiB
前端系统兼容性审查报告
审查范围:
tauri-app/src全部源码 +tauri-app/src-tauri/srcRust 层命令 审查维度:跨平台(macOS/Windows)、Tauri API、媒体/音频、CSS、网络、文件系统 审查日期:2026-05-21
一、综述
本次审查共发现 28 项兼容性问题,其中:
| 级别 | 数量 | 说明 |
|---|---|---|
| 🔴 严重 | 6 | 可能导致功能失效、安全漏洞或数据损坏 |
| 🟡 中等 | 11 | 潜在风险,特定场景下会触发问题 |
| 🟢 低风险 | 11 | 建议优化,影响较小或仅存在于边缘场景 |
关键结论:
- Windows 路径处理是最大隐患:多处 Rust 代码对 Windows 路径的反斜杠、大小写、UNC 前缀处理不完善,可能导致 FFmpeg 调用失败或安全检查被绕过。
- 前端内存泄漏已确认 1 处:
CoverDesign.tsx的URL.createObjectURL未释放。 - Asset Protocol 过度授权:
tauri.conf.json中"scope": "/**"允许 WebView 读取整个文件系统。 - CSS/Web API 兼容性良好:项目运行在 Tauri 封装的 WebView(Edge/WebKit)中,现代 CSS 特性和 Web API 支持度较高,未发现严重兼容性问题。
二、🔴 严重问题(6 项)
1. URL.createObjectURL 内存泄漏 — 背景图上传
位置:tauri-app/src/pages/VideoCreation/CoverDesign.tsx:181
const url = URL.createObjectURL(file);
setConfig(prev => ({ ...prev, backgroundImage: url }));
问题:本地上传背景图时创建 Blob URL,但从未调用 URL.revokeObjectURL(url)。用户多次上传不同背景图时,旧的 Blob URL 会一直占用内存,直到页面刷新。
影响:内存泄漏,长时间使用后可能导致应用卡顿或崩溃。
修复建议:
const prevUrl = config.backgroundImage;
const url = URL.createObjectURL(file);
setConfig(prev => ({ ...prev, backgroundImage: url }));
// 释放旧的 Blob URL
if (prevUrl?.startsWith('blob:')) {
URL.revokeObjectURL(prevUrl);
}
// 组件卸载时也要清理
useEffect(() => {
return () => {
if (config.backgroundImage?.startsWith('blob:')) {
URL.revokeObjectURL(config.backgroundImage);
}
};
}, []);
2. Windows 敏感路径检查大小写不敏感问题
位置:tauri-app/src-tauri/src/commands/file.rs:47
let windows_denied = vec![
r"c:\windows\",
r"c:\program files\",
r"c:\program files (x86)\",
r"c:\users\all users\",
];
问题:Windows 文件系统(NTFS)是大小写保留但大小写不敏感的。用户传入 C:\Windows\ 或 C:\WINDOWS\ 会完全绕过上述安全检查。
影响:攻击者可通过大小写变体访问系统敏感目录。
修复建议:
let path_lower = path.to_lowercase();
let windows_denied = vec![
r"c:\windows\",
r"c:\program files\",
r"c:\program files (x86)\",
r"c:\users\all users\",
];
for denied in &windows_denied {
if path_lower.starts_with(denied) {
return Err(...);
}
}
3. Asset Protocol 范围过度授权
位置:tauri-app/src-tauri/tauri.conf.json
"assetProtocol": {
"enable": true,
"scope": ["$APPLOCALDATA/**", "$APPDATA/**", "$APPCONFIG/**", "/**"]
}
问题:/** 允许 WebView 通过 asset:// 协议读取整个文件系统的任何文件。这意味着前端 JavaScript 可以构造 URL 访问用户的任何本地文件(如 asset:///etc/passwd 或 asset://C:/Users/xxx/Documents/)。
影响:严重安全漏洞。即使需要配合路径遍历,也极大扩大了攻击面。
修复建议:移除 /**,仅保留应用数据目录:
"assetProtocol": {
"enable": true,
"scope": ["$APPLOCALDATA/**", "$APPDATA/**", "$APPCONFIG/**"]
}
注:如果确有需要访问用户选择的文件,应通过 Tauri Dialog API 让用户主动选择,而非开放全局文件系统。
4. escape_ffmpeg_path 不支持 Windows 路径格式
位置:tauri-app/src-tauri/src/ffmpeg_cmd.rs:23
fn escape_ffmpeg_path(path: &str) -> String {
path.replace("'", "'\\''")
}
问题:该函数仅转义单引号,但不处理 Windows 反斜杠 \ 和盘符冒号 :。在 FFmpeg 的 ass='{}':fontsdir='{}' filter 语法或 concat demuxer 的 file 'path' 格式中,Windows 路径如 C:\Users\name\video.mp4 中的反斜杠可能被 FFmpeg 解析为转义序列。
影响:Windows 用户在字幕压制、字体加载、视频合成时,FFmpeg 可能因路径解析错误而失败。
修复建议:
fn escape_ffmpeg_path(path: &str) -> String {
// 1. 统一使用正斜杠(FFmpeg 支持跨平台路径分隔符)
let normalized = path.replace('\\', "/");
// 2. 转义单引号(用于 FFmpeg filter 语法中的引号包裹)
normalized.replace("'", "'\\''")
}
注意:Windows 上
C:/Users/...这种正斜杠路径 FFmpeg 完全支持,这是最简单的跨平台方案。
5. canonicalize() 在 Windows 上返回 UNC 路径导致下游问题
位置:多处使用 std::fs::canonicalize
tauri-app/src-tauri/src/commands/product.rs:198,258,345tauri-app/src-tauri/src/commands/project.rs:124,140tauri-app/src-tauri/src/ffmpeg_cmd.rs:46,792
问题:在 Windows 上,std::fs::canonicalize() 返回 UNC 路径格式 \\?\C:\Users\...。这种路径格式:
- FFmpeg 某些版本不支持,可能导致命令执行失败
- 与
starts_with比较时行为异常,如果比较路径不是 UNC 格式 - 序列化到 JSON 传给前端时,前端可能无法正确理解这种路径
影响:Windows 上的文件校验、路径比较、FFmpeg 调用可能全部受影响。
修复建议:封装一个跨平台的 normalize_path 函数,替代 canonicalize:
use std::path::{Path, PathBuf};
fn normalize_path(path: &Path) -> PathBuf {
// 使用 dunce::simplified() 消除 UNC 前缀,同时保持路径有效性
dunce::simplified(path).to_path_buf()
}
需要添加
dunecrate 依赖,这是 Rust 社区处理 UNC 路径的标准方案。
6. atob() 解析 JWT 存在 base64url 兼容性问题
位置:tauri-app/src/api/client.ts:116
const payload = JSON.parse(atob(token.split('.')[1]));
问题:JWT 使用 base64url 编码(将 + → -,/ → _,去掉 padding =),而 atob() 是标准 base64 解码器。如果 JWT payload 中包含 -、_ 或需要 padding 的字符,atob() 会抛出 DOMException。
当前影响有限:因为 exp 字段通常是纯数字时间戳,但理论上如果用户 ID 或其他 claim 包含这些字符就会失败。
修复建议:
function base64UrlDecode(str: string): string {
// base64url → base64
let padding = '';
const padLen = 4 - (str.length % 4);
if (padLen !== 4) {
padding = '='.repeat(padLen);
}
const base64 = str.replace(/-/g, '+').replace(/_/g, '/') + padding;
return atob(base64);
}
// 使用
const payload = JSON.parse(base64UrlDecode(token.split('.')[1]));
三、🟡 中等问题(11 项)
7. crossOrigin = 'anonymous' 跨域图片污染 Canvas
位置:
tauri-app/src/hooks/useCoverFabric.ts:192,235
image.crossOrigin = 'anonymous';
image.src = imagePath;
问题:当加载远程 HTTP(S) 图片时,如果服务器未配置 Access-Control-Allow-Origin 响应头,Canvas 会被污染(tainted)。被污染的 Canvas 调用 toDataURL() 会抛出 SecurityError: The canvas has been tainted by cross-origin data。
当前代码用 try-catch 静默吞掉了错误,用户会看到空白封面,但不知道原因。
修复建议:捕获错误并向用户提示:
try {
// ...加载图片...
} catch (err) {
console.error('封面图片加载失败:', err);
toast.error('封面图片加载失败,可能是跨域限制或图片链接失效');
}
8. requestAnimationFrame + Video 字幕同步在后台标签页节流
位置:tauri-app/src/hooks/useCanvasSubtitleRenderer.ts:158-168
const onFrame = () => {
drawFrame();
if (!video.paused) {
rafRef.current = requestAnimationFrame(onFrame);
}
};
问题:当应用窗口不在前台或标签页在后台时,浏览器会节流 requestAnimationFrame(通常降到 1fps 或完全暂停)。这会导致 Canvas 字幕与视频画面不同步。
影响:用户切出应用再切回时,字幕可能短暂错位。
修复建议:使用 video.requestVideoFrameCallback()(如果支持)作为更精确的同步机制,或在 visibilitychange 事件触发时强制重绘:
useEffect(() => {
const onVisibilityChange = () => {
if (!document.hidden) drawFrame();
};
document.addEventListener('visibilitychange', onVisibilityChange);
return () => document.removeEventListener('visibilitychange', onVisibilityChange);
}, [drawFrame]);
9. video 控制条高度硬编码导致字幕定位偏移
位置:tauri-app/src/hooks/useCanvasSubtitleRenderer.ts:86
const VIDEO_CONTROLS_HEIGHT = 40;
问题:<video controls> 的控制条高度在不同浏览器/OS 上不同(macOS Safari 约 30px,Windows 约 40-50px,全屏模式约 0px)。硬编码 40px 会导致字幕在预览时的垂直位置与压制输出不完全一致。
修复建议:在视频元数据加载后动态计算:
const video = videoRef.current;
if (video) {
const rect = video.getBoundingClientRect();
const videoRect = video.videoWidth / video.videoHeight;
// 实际视频画面高度 = 容器宽度 / 宽高比
const actualVideoHeight = rect.width / videoRect;
const controlsHeight = rect.height - actualVideoHeight;
}
10. audio.duration 可能返回 NaN/Infinity 未处理
位置:
tauri-app/src/pages/VideoCreation/VoiceSynthesis.tsx:360-367tauri-app/src/pages/VideoGeneration/hooks/useVideoGeneration.ts:371-378tauri-app/src/pages/ContentManagement/VoiceMaterialLibrary.tsx:163-178
audio.onloadedmetadata = () => {
clearTimeout(timeoutId);
resolve(audio.duration);
};
问题:如果音频文件损坏、格式不支持或元数据缺失,audio.duration 可能返回 NaN 或 Infinity。直接 resolve 这个值会导致下游计算错误。
修复建议:
audio.onloadedmetadata = () => {
clearTimeout(timeoutId);
if (!isFinite(audio.duration) || audio.duration <= 0) {
reject(new Error('音频时长无效,文件可能损坏或格式不支持'));
} else {
resolve(audio.duration);
}
};
11. rename 在 Windows 上目标已存在时失败
位置:
tauri-app/src-tauri/src/video_processing.rs:93tauri-app/src-tauri/src/commands/video_compose.rs:222
问题:std::fs::rename() 在 Windows 上如果目标文件已存在会直接失败(Unix 是原子替换)。代码虽然有 copy 回退,但逻辑可能留下残留文件。
影响:Windows 上如果输出路径已存在(如用户重复合成),操作可能失败或留下临时文件。
修复建议:在 rename 前先删除目标文件(如果存在):
if output_path.exists() {
std::fs::remove_file(&output_path)?;
}
std::fs::rename(&temp_output, &output_path)?;
12. to_str().unwrap() 在非 UTF-8 路径上会 panic
位置:tauri-app/src-tauri/src/commands/video_compose.rs:80
concat_videos_copy(&app, list_path.to_str().unwrap(), ...)
问题:Windows 允许非 UTF-8 编码的文件路径(历史 OEM code page 文件)。to_str() 返回 None,unwrap() 会直接 panic。
影响:极少数 Windows 用户(使用中文 Windows 95/XP 时代遗留文件系统编码)可能导致应用崩溃。
修复建议:使用 to_string_lossy() 或 as_os_str() 传递路径:
// 如果需要传给 FFmpeg,使用 to_string_lossy()
let path_str = list_path.to_string_lossy();
13. get_fonts_dir 开发模式路径探测在 Windows 上可能失效
位置:tauri-app/src-tauri/src/ffmpeg_cmd.rs:298-328
cwd.join("fonts"),
parent.join("src-tauri/fonts"),
grandparent.join("tauri-app/src-tauri/fonts"),
问题:开发模式下的字体目录探测使用 / 路径拼接。虽然 PathBuf::join 会处理分隔符,但如果开发时的当前工作目录与预期不同(如从 IDE 以不同路径启动),探测会失败。
影响:开发环境下 Windows 开发者可能遇到字体加载失败。
修复建议:添加环境变量覆盖或更健壮的探测逻辑:
// 优先从环境变量读取
if let Ok(font_dir) = std::env::var("MEIJIAKA_FONTS_DIR") {
let p = PathBuf::from(font_dir);
if p.exists() { return Some(p); }
}
14. concat demuxer 列表中的 Windows 反斜杠问题
位置:tauri-app/src-tauri/src/commands/video_compose.rs:65
format!("file '{}'\n", ffmpeg_cmd::escape_ffmpeg_path(path))
问题:FFmpeg concat demuxer 的列表文件格式中,file 'path' 语法在 Windows 上如果路径包含反斜杠,反斜杠可能被 FFmpeg 解释为转义字符。
影响:与问题 #4 类似,Windows 路径导致 FFmpeg 解析错误。
修复建议:在写入 concat 列表前统一将路径中的 \ 替换为 /:
let normalized = path.replace('\\', "/");
format!("file '{}'\n", normalized)
15. load_app_config 失败时无降级配置
位置:tauri-app/src/main.tsx:10-16
try {
const config = await loadAppConfig();
appEnvironment = config.environment;
} catch {
// 加载失败时默认为生产模式
}
问题:如果 load_app_config(Tauri IPC 调用)失败,应用降级为生产模式。这是合理的,但生产模式会禁用右键菜单和 F12 DevTools。开发者在调试时如果 IPC 调用失败,会突然失去所有调试能力,且不知道原因。
修复建议:在降级时输出警告日志:
} catch (e) {
console.warn('[bootstrap] 加载应用配置失败,降级为生产模式:', e);
appEnvironment = 'production';
}
16. window.location.reload() 在 Tauri 中行为不确定
位置:tauri-app/src/pages/Settings/Settings.tsx:178
setTimeout(() => { window.location.reload(); }, 500);
问题:Tauri 应用中的 window.location.reload() 行为与浏览器不同。在某些 Tauri 版本中可能导致:
- 白屏而非正常刷新
- WebView 进程崩溃
- 状态丢失但窗口不重新加载
修复建议:使用 Tauri 的 relaunch() 命令重启整个应用,或重新挂载 React 根组件:
import { relaunch } from '@tauri-apps/plugin-process';
// 重启应用
await relaunch();
17. Math.random() 用于缓存清除参数(安全性)
位置:tauri-app/src/api/client.ts:334
const cacheBuster = `_t=${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
问题:Math.random() 不是加密安全的随机数生成器。虽然这里只是用于缓存清除,但如果未来用于其他安全相关场景会有风险。
修复建议:使用 crypto.randomUUID() 或 crypto.getRandomValues():
const cacheBuster = `_t=${Date.now()}_${crypto.randomUUID().slice(0, 8)}`;
四、🟢 低风险/建议(11 项)
18. backdrop-filter 无标准前缀回退
位置:tauri-app/src/pages/VideoCreation/CoverDesign.css:444-445
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
评估:当前代码同时有标准和 WebKit 前缀版本,在 Tauri WebView(Edge/WebKit)中支持良好。Firefox 不支持,但项目不面向 Firefox。无需修复。
19. ::-webkit-scrollbar 在 Firefox 中无效
位置:多处(global.css、CoverDesign.css 等)
评估:项目运行在 Tauri WebView 中(基于系统浏览器引擎),不是 Firefox。在 Windows 上基于 WebView2(Edge),macOS 上基于 WKWebView(Safari),均支持 WebKit 滚动条样式。无需修复。
20. aspect-ratio 在旧版 Safari 中可能不支持
位置:多处使用 aspect-ratio: 9 / 16
评估:macOS 12+ 的 Safari 支持 aspect-ratio。如果目标用户可能使用较旧的 macOS 版本,可能需要 padding-top: 177.77% 回退。但鉴于这是 Tauri 桌面应用,可以控制最低系统版本。建议确认 tauri.conf.json 中 macOS.minimumSystemVersion 是否要求 12.0+。
21. requestIdleCallback 缺失回退不完整
位置:tauri-app/src/main.tsx:75-79
if ('requestIdleCallback' in window) {
requestIdleCallback(showWindow, { timeout: 500 });
} else {
setTimeout(showWindow, 100);
}
评估:Tauri WebView2(Edge)和 WKWebView(Safari)均支持 requestIdleCallback。回退逻辑也已实现。无需修复。
22. navigator.userAgent 已被冻结
位置:
tauri-app/src/store/authStore.ts:254tauri-app/src/api/client.ts:259
评估:虽然现代浏览器正在限制 navigator.userAgent,但 Tauri WebView 不受此限制。且当前用法仅为日志和登录信息上报,不影响功能。无需修复。
23. document.fonts.check() 参数格式兼容性
位置:tauri-app/src/utils/canvasSubtitleDrawer.ts:209
if (document.fonts.check(`bold 16px ${fontName}`)) {
评估:document.fonts.check() 的参数格式在不同浏览器中实现有细微差异,但 Tauri WebView2/WKWebView 均支持此用法。风险极低。
24. Date.now() 连续调用可能冲突
位置:多处使用 Date.now() 生成文件名
评估:仅在极快速连续调用时(<1ms)可能冲突。当前场景下不太可能。风险极低。
25. autoPlay 视频可能被浏览器阻止
位置:多处 <video autoPlay>
评估:桌面应用中的 WebView 通常不受浏览器自动播放策略限制。但如果用户操作系统设置了辅助功能限制,仍可能被阻止。建议添加 muted 属性作为后备(如果需要自动播放且带声音)。
26. file.path 是非标准 Chromium 属性
位置:tauri-app/src/pages/VideoCreation/CoverDesign.tsx:178
const path = (file as any).path || (file as any).webkitRelativePath || '';
评估:File.path 是 Chromium 的私有属性,在标准浏览器(Firefox)中不存在。但由于项目运行在 Tauri(Chromium/WebView2)中,这当前是可行的。但如果未来需要支持 Web 端部署,需要改用 Tauri Dialog API 获取路径。建议添加注释说明此依赖。
27. storage/engine.rs 无 Windows 文件权限设置
位置:tauri-app/src-tauri/src/storage/engine.rs:161
#[cfg(unix)]
fn set_restrictive_permissions(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path)?.permissions();
perms.set_mode(0o600);
std::fs::set_permissions(path, perms)?;
Ok(())
}
评估:Unix 上设置了 0o600 权限,Windows 上跳过。Windows 上文件默认对用户可读可写,其他用户也可读(取决于 ACL)。虽然这不是严重安全问题(应用数据存储在用户目录),但建议在 Windows 上设置等效的 ACL 限制。
修复建议:
#[cfg(windows)]
fn set_restrictive_permissions(path: &Path) -> Result<()> {
// 使用 windows crate 或 fs_extra 设置 ACL
// 简化为仅当前用户可读写
// 这是可选优化,优先级低
Ok(())
}
28. Slider.css 和 CoverDesign.css 中 appearance 重复声明
位置:
tauri-app/src/components/Slider/Slider.css:32-33tauri-app/src/pages/VideoCreation/CoverDesign.css:79-80
appearance: none;
appearance: none;
评估:纯代码质量问题,不影响兼容性。建议移除重复行。
五、按维度汇总表
| 维度 | 严重 | 中等 | 低风险 | 主要文件 |
|---|---|---|---|---|
| 内存/资源管理 | 1 (#1) | 1 (#10) | 1 (#24) | CoverDesign.tsx, VoiceSynthesis.tsx |
| Windows 路径 | 2 (#4, #5) | 3 (#11, #12, #14) | 1 (#27) | ffmpeg_cmd.rs, file.rs, product.rs |
| 安全检查 | 2 (#2, #3) | 0 | 0 | file.rs, tauri.conf.json |
| Canvas/媒体 | 0 | 3 (#7, #8, #9) | 2 (#20, #25) | useCoverFabric.ts, useCanvasSubtitleRenderer.ts |
| 网络/API | 1 (#6) | 1 (#17) | 2 (#22, #23) | client.ts |
| Tauri 原生 | 0 | 1 (#16) | 1 (#26) | Settings.tsx, CoverDesign.tsx |
| CSS | 0 | 0 | 3 (#18, #19, #28) | CoverDesign.css, global.css |
| 启动/配置 | 0 | 1 (#15) | 1 (#21) | main.tsx |
| 字体加载 | 0 | 1 (#13) | 0 | ffmpeg_cmd.rs |
六、修复优先级建议
立即修复(影响功能/安全)
- #3 Asset Protocol 过度授权 — 安全漏洞,一行配置修改
- #2 Windows 敏感路径大小写 — 安全检查被绕过
- #1 URL.createObjectURL 泄漏 — 内存泄漏,用户可见
- #4 escape_ffmpeg_path Windows 支持 — Windows 功能失效
- #5 canonicalize() UNC 路径 — Windows 文件操作异常
本轮迭代修复(影响体验)
- #6 atob() base64url 兼容性 — Token 解析潜在失败
- #7 crossOrigin 图片污染提示 — 用户友好性
- #8 RAF 后台节流 — 字幕同步
- #10 audio.duration NaN 处理 — 音频处理健壮性
- #9 控制条高度硬编码 — 预览准确性
- #11 Windows rename 已存在 — 文件操作健壮性
后续排期(优化/边缘场景)
12-28. 其余低风险项
七、特别说明:Tauri 环境 vs 浏览器环境的兼容性差异
本项目同时支持两种运行模式:
| 特性 | Tauri 桌面模式 | 浏览器模式(开发调试用) |
|---|---|---|
invoke() |
✅ Tauri IPC | ❌ 会 catch 失败 |
convertFileSrc() |
✅ asset:// |
❌ 会 catch 失败 |
localStorage |
✅ 可用 | ✅ 可用 |
| File 系统 API | ✅ Tauri 插件 | ❌ 不可用 |
__TAURI_INTERNALS__ |
✅ 存在 | ❌ 不存在 |
当前代码对浏览器模式有降级处理(isTauri() 检查 + catch 错误),这是好的实践。但以下功能在浏览器模式下完全不可用,需要评估是否影响开发调试:
- 本地视频预览(依赖
asset://+ FFmpeg 转码) - 文件保存/导出(依赖 Tauri Dialog)
- 项目本地持久化(依赖 Tauri IPC)
- 自动更新(依赖 Tauri Updater)
建议:在 README 或开发文档中明确列出浏览器模式的功能限制,避免开发者困惑。