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
26 KiB
前端系统兼容性审查报告 v2(业务场景驱动)
审查范围:
tauri-app/src全部源码 +tauri-app/src-tauri/srcRust 层命令 审查方法:按用户真实使用路径和数据流转分析,非通用技术罗列 审查日期:2026-05-21 当前版本:v1.6.0
一、综述
本次审查以用户真实操作流程为主线,从数据持久化、媒体处理、第三方服务、版本升级、跨设备迁移五个业务维度展开,共发现 14 项与业务直接相关的兼容性问题。
核心结论:
- BGM 云端化改造存在链路缺口:前端存储了 URL,但混音时直接传给 FFmpeg,未做本地缓存,网络波动或 URL 过期会导致合成失败或产生"无声成片"。
- 积分消费存在 TOCTOU 竞态:预检通过→合成完成→扣费失败之间有时间窗口,可能导致用户白嫖或重复扣费。
- 项目数据完全不可迁移:所有本地路径为绝对路径,无导出/导入功能,换设备后项目报废。
- 磁盘满等大文件场景缺乏保护:合成成果可能直接丢失,大视频上传/下载全量读内存。
- 多处"静默失败":保存项目、分段配音、BGM 混音等关键环节出错时不提示用户,导致用户以为成功实际数据残缺。
二、🔴 严重问题(影响功能、数据或资金)
1. BGM 云端化后混音链路断裂——"无声成片"与合成失败
业务场景:
- 用户在 BGM 弹窗中选择一首云端 BGM(七牛云 URL)
- 保存项目(
bgmMusicPath写入meta.json,值为https://media.liche.cn/.../xxx.mp3) - 几天后点击「合成视频」,FFmpeg 混音时直接拉取该 URL
- 网络波动或 URL 签名过期 → FFmpeg HTTP 输入超时 → 混音失败
实际代码路径:
// VideoCompose.tsx:265-276
if (bgmMusicPath) {
const mixRes = await invoke('mix_bgm_to_video', {
args: {
videoPath: result,
bgmPath: bgmFullPath, // <-- 这里是七牛云 URL,不是本地路径
outputPath: result,
bgmVolume: (bgmVolume ?? 0.25),
},
});
if (mixRes.code !== 200) {
console.warn('BGM 混合失败,使用无 BGM 版本:', mixRes.message);
}
}
// ffmpeg_cmd.rs:509-546
pub async fn mix_bgm_to_video(...) {
let safe_video = validate_safe_path(video_path)?; // 只校验视频路径
let safe_bgm = bgm_path.to_string(); // <-- 直接透传 URL,无校验
run_ffmpeg(app, vec!["-i", safe_video, "-i", safe_bgm, ...])
}
用户实际看到:
- 界面提示「压制成片完成」✅
- 播放视频发现没有背景音乐 ❌
- 用户以为是自己操作问题,反复合成浪费积分和时间
影响评估:
- 功能完整性受损:选了 BGM 却出无声视频
- 积分浪费:每次合成消耗积分,但产出不符合预期
- 用户信任度下降:无法解释为什么有时有 BGM 有时没有
修复建议:
在 VideoCompose.tsx handleStart 中,混音前确保 BGM 为本地文件:
let finalBgmPath = bgmMusicPath;
if (bgmMusicPath?.startsWith('http')) {
// 下载到本地缓存目录
const cacheDir = await invoke<string>('get_bgm_cache_dir');
const cachedPath = `${cacheDir}/bgm_${bgmMusicId}.mp3`;
const exists = await invoke<boolean>('file_exists', { path: cachedPath });
if (!exists) {
useProgressStore.getState().update('正在下载背景音乐...');
await videoComposeApi.downloadFile({ url: bgmMusicPath, outputPath: cachedPath });
}
finalBgmPath = cachedPath;
}
// 然后传给 mix_bgm_to_video
Rust 侧 mix_bgm_to_video 应恢复 validate_safe_path 校验,拒绝 URL:
let safe_bgm = validate_safe_path(bgm_path)?; // 强制要求本地路径
2. 积分消费 TOCTOU 竞态——合成完成扣费失败导致"白嫖"或需重来
业务场景:
- 用户点击「合成视频」,预检余额充足(如 50 积分,需扣 5 分)
- 视频合成耗时 3-5 分钟
- 期间用户在手机端或其他场景消费了积分,余额降至 3 分
- 合成完成后调用
consumePoints,返回 402 "积分不足" - 前端回滚
finalVideoPath状态,但不删除已生成的视频文件
实际代码:
// VideoCompose.tsx:287-309
const composePoints = usePointStore.getState().getRule('compose')?.points || 5;
try {
await pointsApi.consumePoints({
points: composePoints,
sourceType: 'compose',
sourceId: `compose_${useAuthStore.getState().user?.id || 'unknown'}_${Date.now()}`,
description: '压制成片',
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
setResultPath('');
setFinalVideoPath(undefined); // 回滚前端状态
setExportedAt(undefined);
// ❌ 没有删除 products/ 目录下已生成的视频文件
if (msg.includes('402') || msg.includes('积分不足')) {
setShowPointsModal(true);
return;
}
}
用户实际看到:
- 提示「积分不足」弹出充值弹窗
- 但
~/Library/Application Support/cn.meijiaka.ai-zy/projects/xxx/products/下已经有一份完整的.mp4 - 用户可以通过 Finder 直接找到并播放该文件,实际已白嫖成功
- 或者用户充值后再次点击合成,重复消耗时间
影响评估:
- 资金损失风险:用户可在不扣积分的情况下拿到成品
- 用户体验差:明明看到"合成完成"的进度条走到 100%,最后说积分不够
- 运营数据失真:成品文件存在但系统无消费记录
修复建议(方案二选一):
方案 A:积分预占/冻结机制(推荐) 后端新增 "预占积分" API,合成前预占 5 积分,合成完成后确认扣费,失败则释放。消除时间窗口。
方案 B:扣费前置 + 失败清理 若无法改后端,至少做到失败时清理文件:
} catch (err) {
// 回滚状态
setResultPath('');
setFinalVideoPath(undefined);
// 清理已生成的文件
if (outputPath) {
await invoke('delete_project_file', { projectId, filePath: outputPath })
.catch(() => {}); // 清理失败不阻断错误提示
}
// ... 原有错误处理
}
3. 项目数据绝对路径依赖——换设备后项目完全报废
业务场景:
- 用户 Mac A 上创建项目,生成视频,一切正常
- 用户将
~/Library/Application Support/cn.meijiaka.ai-zy/projects/文件夹复制到 Mac B(或 Time Machine 恢复) - 在 Mac B 上打开应用,项目列表显示正常
- 点击项目进入编辑,视频预览空白、配音无法播放、封面无法加载
根本原因:
meta.json 和 segments.json 中所有本地文件路径均为绝对路径:
{
"avatarMaterialPath": "/Users/alice/Library/Application Support/cn.meijiaka.ai-zy/projects/proj_xxx/assets/voice.mp3",
"burnedVideoPath": "/Users/alice/Library/Application Support/.../burned_xxx.mp4",
"coverPath": "/Users/alice/Library/Application Support/.../cover.png"
}
Mac B 上用户名为 bob,上述路径全部指向不存在的目录。
当前代码无修复机制:
loadMeta直接返回磁盘数据,无路径校验或重映射getLocalFileUrl调用 Rustvalidate_media_path,校验通过后会返回asset://URL,但文件不存在时直接抛错useLocalVideo抛错后显示空白,无降级提示
用户实际看到:
- 项目标题、主题等文字信息正常
- 视频预览区域空白或转圈
- 配音试听按钮点击无反应
- 用户以为数据损坏,恐慌
影响评估:
- 用户换机/重装系统后所有本地项目无法继续编辑
- 与「桌面端本地持久化」的核心卖点相矛盾
- Time Machine 备份恢复后项目数据"假死"
修复建议:
短期(最小改动):加载项目时检测路径有效性,无效时给出明确提示:
// initProjectStore 中
const validatedMeta = await validateProjectPaths(meta);
if (validatedMeta.brokenPaths.length > 0) {
toast.warn(`项目 ${validatedMeta.brokenPaths.join(', ')} 关联的文件已丢失,可能因迁移设备或清理磁盘导致`);
}
长期:
- 持久化时存储相对路径(相对于项目目录)
- 加载时解析为绝对路径:
function resolveProjectPath(projectId: string, relativePath: string): string {
return `${APP_LOCAL_DATA_DIR}/projects/${projectId}/${relativePath}`;
}
- 新增「项目包导出/导入」功能:将
meta.json+segments.json+assets/+videos/打包为.zip
4. 磁盘空间不足时合成成果直接丢失
业务场景:
- 用户 Mac 剩余空间 2GB
- 合成一个 1.5GB 的视频,临时文件 + 输出文件刚好占满磁盘
- FFmpeg 合成成功,但
std::fs::copy移动最终文件时因磁盘满失败 - 临时文件被清理,用户一无所获
实际代码:
// video_processing.rs:93
std::fs::rename(&final_output, output_path)
.or_else(|_| {
std::fs::copy(&final_output, output_path)
.and_then(|_| std::fs::remove_file(&final_output))
})
rename 跨卷时失败,copy 在磁盘满时失败,临时文件在 Drop 或后续清理中被删除。
用户实际看到:
- 进度条走到 100%,显示「正在保存...」
- 突然报错「移动最终视频失败」
- 数分钟的等待 + 积分消耗,结果什么都没有
影响评估:
- 极端挫败感:用户最高预期时刻("马上完成了")直接失败
- 积分和时间双重浪费
修复建议:
- 合成前检查磁盘空间:
// 在 handleStart 调用前
let required_space = estimate_output_size(video_paths) * 2; // 输出 + 临时文件
let available = fs2::available_space(&output_parent)?;
if available < required_space {
return Err("磁盘空间不足,需要至少 {} GB 可用空间".into());
}
copy失败时保留临时文件,给用户手动恢复的机会:
if let Err(e) = std::fs::copy(&final_output, output_path) {
return Err(format!("保存失败(磁盘可能已满)。临时文件保留在: {},错误: {}",
final_output.display(), e));
}
5. 大文件上传/下载全量读内存——低配机器 OOM
业务场景:
- 用户生成了一段 10 分钟 1080p 视频,文件大小 500MB
- 点击「上传」或系统自动上传到七牛云/后端
- Rust 侧
std::fs::read(local_path)将 500MB 全量读入内存 - 再复制到 reqwest multipart body,峰值内存占用 >1GB
- 8GB 内存的 MacBook Air 可能触发系统 OOM,应用被杀死
实际代码:
// Rust 侧 upload_video_file / upload_audio_file
let file_bytes = match std::fs::read(local_path) { ... };
let form = reqwest::multipart::Form::new()
.part("file", reqwest::multipart::Part::bytes(file_bytes) ...);
// Rust 侧 download_file
let client = reqwest::Client::new(); // 默认无超时
let bytes = match response.bytes().await { ... }; // 全量入内存
std::fs::write(&safe_output, &bytes);
用户实际看到:
- 上传/下载大文件时应用突然消失(被系统杀死)
- 或进度条卡住很久,没有任何反馈
- 重启后需要重新开始整个流程
影响评估:
- 长视频用户(核心目标用户群)完全无法使用
- 应用稳定性差,低配置设备体验极差
修复建议:
上传改用流式:
use tokio::fs::File;
use tokio::io::AsyncReadExt;
let file = File::open(local_path).await?;
let stream = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new());
let body = reqwest::Body::wrap_stream(stream);
let part = reqwest::multipart::Part::stream(body)
.file_name(filename)
.mime_str("video/mp4")?;
下载改用边下边写 + 超时:
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(300))
.connect_timeout(std::time::Duration::from_secs(30))
.build()?;
let mut response = client.get(url).send().await?;
let mut file = tokio::fs::File::create(&safe_output).await?;
while let Some(chunk) = response.chunk().await? {
tokio::io::copy(&mut chunk.as_ref(), &mut file).await?;
}
三、🟡 中等问题(影响体验或存在数据风险)
6. 保存项目失败用户完全不知情——数据丢失风险
业务场景:
- 用户在 CoverDesign 页面调整标题,触发自动保存
- 磁盘已满(或文件被其他程序锁定)
saveMetaToLocalFile抛出 IO 错误- 错误被
.catch捕获后只console.error,没有任何 UI 提示 - 用户继续编辑,关闭应用
- 重新打开后发现之前的修改全部丢失
实际代码:
// localStorage.ts 中的 safeInvoke 错误处理
try {
const result = await invoke<T>(cmd, args);
return result;
} catch (error) {
console.error(`Tauri IPC 调用失败 [${cmd}]:`, error);
throw error; // 抛给上层
}
// saveMetaToLocalFile 调用链
metaSavePromise = metaSavePromise.then(task).catch(err => {
console.error('保存项目元数据失败:', err);
throw err; // 继续抛出,但无人处理
});
用户实际看到:
- 没有任何错误提示
- 下次打开项目时数据回到旧状态
- 用户以为是应用 bug,不信任自动保存功能
修复建议:
在 saveMetaToLocalFile 的 catch 中增加用户可见提示:
metaSavePromise = metaSavePromise.then(task).catch(err => {
console.error('保存项目元数据失败:', err);
const message = err instanceof Error ? err.message : String(err);
if (message.includes('磁盘') || message.includes('space') || message.includes('No space')) {
toast.error('项目保存失败:磁盘空间不足,请清理后重试');
} else {
toast.error('项目保存失败,请检查文件权限或重启应用');
}
throw err;
});
7. 配音分段失败静默继续——"部分缺失"的配音
业务场景:
- 用户生成 10 段配音,每段对应一个分镜
- 第 3 段
extractAudioSegment或uploadAudioFile失败(网络抖动、文件被占用) - 错误被 catch 后只
console.error,循环继续 - 最终提示「配音合成完成」
- 用户导出视频后发现第 3 分镜没有声音
实际代码:
// VoiceSynthesis.tsx:243-245(近似逻辑)
for (const segment of segments) {
try {
await extractAudioSegment(...);
await uploadAudioFile(...);
} catch (err) {
console.error('分段处理失败:', err); // ❌ 静默吞掉
// 循环继续...
}
}
toast.success('配音合成完成');
用户实际看到:
- 提示「配音合成完成」✅
- 导出视频后发现部分片段无声 ❌
- 无法定位是哪一段出了问题
修复建议:
const failedSegments: string[] = [];
for (const segment of segments) {
try {
await extractAudioSegment(...);
await uploadAudioFile(...);
} catch (err) {
console.error('分段处理失败:', err);
failedSegments.push(segment.id);
// 继续处理其他段,但记录失败
}
}
if (failedSegments.length > 0) {
toast.warn(`配音合成部分完成,第 ${failedSegments.join(', ')} 段处理失败,请检查网络后重试`);
} else {
toast.success('配音合成完成');
}
8. 轮询任务状态遇到网络闪断直接失败——长任务前功尽弃
业务场景:
- 用户提交 Vidu 视频生成任务,进入轮询等待
- 轮询 3 分钟后,用户 WiFi 短暂断开 5 秒
getTaskStatus抛出网络错误while循环无内部 try-catch,整个函数抛出异常- 前端提示「视频生成失败」
- 实际上后端任务仍在执行,用户需重新提交并再次等待
实际代码:
// useVideoGeneration.ts / ScriptCreation.tsx 等处的轮询逻辑
while (status === 'pending' || status === 'running') {
await new Promise(resolve => setTimeout(resolve, pollInterval));
const resp = await taskApi.getTaskStatus(taskId); // ❌ 无 try-catch
status = resp.status;
}
用户实际看到:
- 等待数分钟后突然报错「失败」
- 重新提交后又需等待同样长的时间
- 后端实际上可能已经完成了任务,但前端放弃了
修复建议:
let consecutiveErrors = 0;
const MAX_CONSECUTIVE_ERRORS = 3;
while (status === 'pending' || status === 'running') {
await new Promise(resolve => setTimeout(resolve, pollInterval));
try {
const resp = await taskApi.getTaskStatus(taskId);
status = resp.status;
consecutiveErrors = 0;
} catch (err) {
consecutiveErrors++;
console.warn(`轮询失败 (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}):`, err);
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
throw new Error('网络异常,视频状态获取失败,请稍后重试');
}
// 继续轮询,给用户一个恢复的机会
}
}
9. 余额获取失败 = 误判为余额不足——有积分却被阻止
业务场景:
- 用户打开应用,网络较差
fetchBalance调用失败(console.error后静默)balance保持默认值0- 用户点击「合成视频」
- 预检:
currentBalance < requiredPoints→0 < 5→ 阻止 - 用户明明有积分,却无法使用
实际代码:
// pointStore.ts
fetchBalance: async () => {
try {
const data = await pointsApi.getBalance();
set({ balance: data.balance, rules: data.rules });
} catch (e) {
console.error('获取积分余额失败:', e); // ❌ 静默失败
// balance 保持旧值或 0
}
},
用户实际看到:
- 点击合成按钮后弹出「积分不足」充值弹窗
- 用户去「我的」页面查看,发现余额显示为 0 或旧值
- 刷新页面后余额恢复正常
修复建议:
fetchBalance: async () => {
try {
const data = await pointsApi.getBalance();
set({ balance: data.balance, rules: data.rules, balanceError: null });
} catch (e) {
console.error('获取积分余额失败:', e);
set({ balanceError: '获取余额失败,请检查网络' });
// balance 保持旧值,不要变成 0
}
},
// 预检时
if (balanceError) {
// 无法确认余额,允许操作但提示风险
toast.warn('余额获取失败,将尝试扣费,若余额不足会提示充值');
return true;
}
10. 多应用实例并发修改导致数据覆盖
业务场景:
- 用户双击应用图标,意外打开两个窗口(或命令行启动第二个实例)
- 实例 A 在 CoverDesign 修改标题为「现代简约风」
- 实例 B 在 ScriptCreation 修改主题为「奶油风」
- 两个实例同时点击保存
- 实例 A 的保存覆盖了实例 B 的修改(或反之)
根因分析:
saveMetaToLocalFile 使用 Read-Modify-Write 模式:
- 读
meta.json - 内存合并
- 写
meta.json(带文件锁,保证单写)
但文件锁只保护"写"操作,两个实例可以同时读取同一个文件,然后各自基于旧版本修改并写入,导致后写入的覆盖前者。
用户实际看到:
- 在一个窗口里明明保存了修改
- 切到另一个窗口再切回来,发现修改消失了
- 用户以为是应用不稳定
修复建议:
- 应用层单实例锁:启动时检查是否已有实例在运行
// main.rs
let single = single_instance::SingleInstance::new("cn.meijiaka.ai-zy").unwrap();
if !single.is_single() {
// 已有实例,唤起旧实例窗口并退出
return;
}
- 或文件锁扩展为读写锁:读取时也加共享锁,防止并发读-改-写
11. BGM 预览硬编码开发者路径——正式包无法预览系统 BGM
业务场景:
- 用户安装正式版应用
- 进入 BGM 选择弹窗
- 点击任意系统 BGM 的试听按钮
- 无声音,或报错
实际代码:
// VideoCompose.tsx:113
const audioSrc = item.url || (item.filePath ? `/Users/0fun/work/meijiaka-zy/mixkit_bgm/${item.filePath}` : '');
当 item.url 为空且 item.filePath 存在时,构造的路径是开发者本机绝对路径 /Users/0fun/...,正式包用户机器上不存在此目录。
影响评估:
- 虽然云端化后
item.url应始终有值,但如果 API 返回异常或旧数据残留,会回退到硬编码路径 - 开发环境测试时「正常」的功能,正式包上直接失效
修复建议:
直接移除硬编码回退,若 item.url 为空则禁用试听:
const audioSrc = item.url;
if (!audioSrc) {
toast.warn('该音乐暂无可用的试听链接');
return;
}
12. 封面 Fabric.js 跨域加载失败无用户提示
业务场景:
- 用户选择一张网络图片作为封面背景
- 该图片服务器未配置 CORS 头
image.crossOrigin = 'anonymous'加载失败useCoverFabric.ts中 catch 静默吞掉错误- Canvas 上背景为空白,用户不知道为什么
实际代码:
// useCoverFabric.ts:192-196
const image = new Image();
image.crossOrigin = 'anonymous';
image.onload = () => resolve(image);
image.onerror = (e) => reject(e);
image.src = imagePath;
// ...
} catch {
// no-op: 背景图加载失败已在内部处理
}
用户实际看到:
- 选了背景图,但 Canvas 预览为纯色背景
- 不知道是因为图片跨域、链接失效还是其他原因
修复建议: 在 catch 中区分错误类型并提示:
} catch (err) {
console.error('封面背景加载失败:', err);
if (imagePath.startsWith('http')) {
toast.error('封面图片加载失败,可能是跨域限制或链接失效,请尝试本地上传');
} else {
toast.error('封面图片加载失败,文件可能已被移动或删除');
}
}
四、🟢 低风险/建议(5 项)
13. 自动更新后无数据迁移逻辑
业务场景:
- v1.6.0 用户自动更新到 v1.7.0
- v1.7.0 新增了某个必填字段(如
videoCodec) - 旧项目加载后该字段为
undefined - 如果新功能直接读取此字段不做防御,可能崩溃
现状:
migrateMeta只处理了v0 → v1(添加version字段)- 注释预留了
v1 → v2的扩展点,但无实际实现 - Tauri updater 安装后只是重启应用,不触发任何数据迁移
建议:
在应用启动时(bootstrap 或 App.tsx useEffect)增加一次性的全局迁移检查:
async function runGlobalMigrations() {
const appVersion = await getVersion();
const lastMigratedVersion = localStorage.getItem('last_migrated_version');
if (lastMigratedVersion === appVersion) return;
// 遍历所有本地项目,执行迁移
const projects = await localProjectApi.listProjects();
for (const project of projects) {
const meta = await localProjectApi.loadMeta(project.id);
if (meta) {
const migrated = migrateMeta(meta); // 扩展此函数
await localProjectApi.saveMeta(project.id, migrated);
}
}
localStorage.setItem('last_migrated_version', appVersion);
}
14. 旧字段删除无运行时降级处理
现状: Git 历史中有多个字段被删除/重命名:
subtitlePreset→captionPresetdubbingVoiceId→selectedVoiceIdselectedHumanId/selectedElementId被移除caption→mainTitle(CoverDesign 中有 fallback)
当前行为:
旧项目加载后,旧字段保留在 meta.json 中但被忽略,对应功能降级为默认状态。对用户来说,打开旧项目后发现某些设置"复位"了,但不明白为什么。
建议:
在 migrateMeta 中增加字段映射:
function migrateMeta(raw: Record<string, unknown>): Partial<ProjectMeta> {
// v0 → v1
if ((raw.version as number) < 1) {
raw.version = 1;
}
// 字段重命名映射
if (raw.subtitlePreset && !raw.captionPreset) {
raw.captionPreset = raw.subtitlePreset;
delete raw.subtitlePreset;
}
if (raw.dubbingVoiceId && !raw.selectedVoiceId) {
raw.selectedVoiceId = raw.dubbingVoiceId;
delete raw.dubbingVoiceId;
}
return raw as Partial<ProjectMeta>;
}
五、按业务维度汇总
| 业务维度 | 问题编号 | 核心风险 | 用户感知 |
|---|---|---|---|
| BGM/音频 | 1, 11 | 合成无声、预览失效 | "为什么选了音乐却没有声音" |
| 积分/资金 | 2, 9 | 白嫖可能、误判余额不足 | "明明有积分却说不让用" |
| 数据持久化 | 3, 6, 10, 13, 14 | 换机报废、保存失败无感知、多实例覆盖 | "修改保存后怎么没了" |
| 视频合成 | 4, 5, 7, 8 | 磁盘满丢失、OOM、分段缺失、长任务闪断 | "等了5分钟结果什么都没有" |
| 封面/视觉 | 12 | 跨域图片加载失败无提示 | "选了图片但封面是空的" |
六、修复优先级(按业务影响排序)
P0(立即修复,影响核心功能或资金)
- #1 BGM 混音链路缺口:混音前下载 URL 到本地缓存
- #2 积分 TOCTOU:扣费失败时清理已生成文件,或推动后端预占机制
- #5 大文件 OOM:上传/下载改用流式传输
P1(本轮迭代修复,影响体验)
- #4 磁盘满保护:合成前检查空间,
copy失败保留临时文件 - #6 保存失败无提示:
saveMetaToLocalFile错误 toast 提示 - #7 分段配音失败静默:记录失败段并提示用户
- #8 轮询闪断:增加网络错误容忍和重试
- #9 余额误判:余额获取失败时不阻断用户
P2(后续排期,架构改进)
- #3 项目跨设备迁移:路径相对化 + 导出/导入功能
- #10 多实例并发:应用层单实例锁
- #11 BGM 预览硬编码:移除开发者路径
- #12 封面跨域提示:增加错误提示
- #13 自动更新迁移:全局迁移框架
- #14 旧字段映射:
migrateMeta扩展