Files
meijiaka-zy/docs/frontend-compatibility-audit.md
T
小鱼开发 915339d42a release: bump version to 1.6.1
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
2026-05-25 22:35:35 +08:00

23 KiB
Raw Blame History

前端系统兼容性审查报告

审查范围:tauri-app/src 全部源码 + tauri-app/src-tauri/src Rust 层命令 审查维度:跨平台(macOS/Windows)、Tauri API、媒体/音频、CSS、网络、文件系统 审查日期:2026-05-21


一、综述

本次审查共发现 28 项兼容性问题,其中:

级别 数量 说明
🔴 严重 6 可能导致功能失效、安全漏洞或数据损坏
🟡 中等 11 潜在风险,特定场景下会触发问题
🟢 低风险 11 建议优化,影响较小或仅存在于边缘场景

关键结论

  1. Windows 路径处理是最大隐患:多处 Rust 代码对 Windows 路径的反斜杠、大小写、UNC 前缀处理不完善,可能导致 FFmpeg 调用失败或安全检查被绕过。
  2. 前端内存泄漏已确认 1 处CoverDesign.tsxURL.createObjectURL 未释放。
  3. Asset Protocol 过度授权tauri.conf.json"scope": "/**" 允许 WebView 读取整个文件系统。
  4. CSS/Web API 兼容性良好:项目运行在 Tauri 封装的 WebViewEdge/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/passwdasset://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,345
  • tauri-app/src-tauri/src/commands/project.rs:124,140
  • tauri-app/src-tauri/src/ffmpeg_cmd.rs:46,792

问题:在 Windows 上,std::fs::canonicalize() 返回 UNC 路径格式 \\?\C:\Users\...。这种路径格式:

  1. FFmpeg 某些版本不支持,可能导致命令执行失败
  2. starts_with 比较时行为异常,如果比较路径不是 UNC 格式
  3. 序列化到 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()
}

需要添加 dune crate 依赖,这是 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 约 30pxWindows 约 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-367
  • tauri-app/src/pages/VideoGeneration/hooks/useVideoGeneration.ts:371-378
  • tauri-app/src/pages/ContentManagement/VoiceMaterialLibrary.tsx:163-178
audio.onloadedmetadata = () => {
  clearTimeout(timeoutId);
  resolve(audio.duration);
};

问题:如果音频文件损坏、格式不支持或元数据缺失,audio.duration 可能返回 NaNInfinity。直接 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:93
  • tauri-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() 返回 Noneunwrap()直接 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 WebViewEdge/WebKit)中支持良好。Firefox 不支持,但项目不面向 Firefox。无需修复


19. ::-webkit-scrollbar 在 Firefox 中无效

位置:多处(global.cssCoverDesign.css 等)

评估:项目运行在 Tauri WebView 中(基于系统浏览器引擎),不是 Firefox。在 Windows 上基于 WebView2Edge),macOS 上基于 WKWebViewSafari),均支持 WebKit 滚动条样式。无需修复


20. aspect-ratio 在旧版 Safari 中可能不支持

位置:多处使用 aspect-ratio: 9 / 16

评估macOS 12+ 的 Safari 支持 aspect-ratio。如果目标用户可能使用较旧的 macOS 版本,可能需要 padding-top: 177.77% 回退。但鉴于这是 Tauri 桌面应用,可以控制最低系统版本。建议确认 tauri.conf.jsonmacOS.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 WebView2Edge)和 WKWebViewSafari)均支持 requestIdleCallback。回退逻辑也已实现。无需修复


22. navigator.userAgent 已被冻结

位置

  • tauri-app/src/store/authStore.ts:254
  • tauri-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)中不存在。但由于项目运行在 TauriChromium/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.cssCoverDesign.cssappearance 重复声明

位置

  • tauri-app/src/components/Slider/Slider.css:32-33
  • tauri-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

六、修复优先级建议

立即修复(影响功能/安全)

  1. #3 Asset Protocol 过度授权 — 安全漏洞,一行配置修改
  2. #2 Windows 敏感路径大小写 — 安全检查被绕过
  3. #1 URL.createObjectURL 泄漏 — 内存泄漏,用户可见
  4. #4 escape_ffmpeg_path Windows 支持 — Windows 功能失效
  5. #5 canonicalize() UNC 路径 — Windows 文件操作异常

本轮迭代修复(影响体验)

  1. #6 atob() base64url 兼容性 — Token 解析潜在失败
  2. #7 crossOrigin 图片污染提示 — 用户友好性
  3. #8 RAF 后台节流 — 字幕同步
  4. #10 audio.duration NaN 处理 — 音频处理健壮性
  5. #9 控制条高度硬编码 — 预览准确性
  6. #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 或开发文档中明确列出浏览器模式的功能限制,避免开发者困惑。