fix: 保持产物收尾幂等并保护批量成品完整性
This commit is contained in:
+35
-22
@@ -18,6 +18,7 @@ from wov_app import registry
|
||||
from wov_app.config import SCHEDULER_INTERVAL_SECONDS
|
||||
from wov_app.db import Database
|
||||
from wov_app.logging import get_logger
|
||||
from wov_app.storage import atomic_copy
|
||||
|
||||
# 调度器运行日志:节点进度、暂停/续跑等状态变化。
|
||||
logger = get_logger("scheduler")
|
||||
@@ -237,19 +238,21 @@ class WorkflowScheduler:
|
||||
# 处理 final_outputs,为用户端提供简洁的下载别名。
|
||||
for alias, ref in definition.final_outputs.items():
|
||||
resolved = self._resolve_ref(ref, run.get("input_uri"), outputs_by_node)
|
||||
if resolved is not None:
|
||||
# 最终产物按 上传文件名.标识.时间戳 重命名,区分语言与版本。
|
||||
resolved = self._final_artifact_uri(resolved, run, definition, alias, ref)
|
||||
self.db.create_artifact(
|
||||
{
|
||||
"run_id": run_id,
|
||||
"node_id": ref.partition(".")[0],
|
||||
"name": alias,
|
||||
"uri": resolved,
|
||||
"mime_type": self._mime_type(resolved),
|
||||
"size": self._file_size(resolved),
|
||||
}
|
||||
)
|
||||
if resolved is None:
|
||||
raise ValueError(f"missing final output: {alias} ({ref})")
|
||||
# 成品按 上传文件名.标识.时间戳 命名,保留节点原始文件与 URI。
|
||||
# 同一个 run 重复收尾使用稳定路径,不再因暂停/重启反复改名。
|
||||
resolved = self._final_artifact_uri(resolved, run, definition, alias, ref)
|
||||
self.db.create_artifact(
|
||||
{
|
||||
"run_id": run_id,
|
||||
"node_id": ref.partition(".")[0],
|
||||
"name": alias,
|
||||
"uri": resolved,
|
||||
"mime_type": self._mime_type(resolved),
|
||||
"size": self._file_size(resolved),
|
||||
}
|
||||
)
|
||||
|
||||
# 全部节点成功后标记完成;期间被暂停则保持 PAUSED,等待续跑补做收尾。
|
||||
if self.db.get_run(run_id)["status"] == "PAUSED":
|
||||
@@ -285,25 +288,35 @@ class WorkflowScheduler:
|
||||
alias: str,
|
||||
ref: str,
|
||||
) -> str:
|
||||
"""把最终产物重命名为 上传文件名.标识.时间戳 并返回新 URI。
|
||||
"""生成命名成品副本并返回稳定 URI,节点原始产物始终保留。
|
||||
|
||||
标识优先取产出节点的 target_language 参数(如 zh-CN),否则回退为
|
||||
产物别名;时间戳取当前时刻,用于区分同一上传文件的多次运行版本。
|
||||
重命名在原地进行(同目录),不复制文件。
|
||||
标识优先取产出节点的 target_language,否则回退别名;时间戳固定为
|
||||
run 创建时间。每个别名单独目录,防止相同语言/扩展名的输出互相覆盖。
|
||||
复制使用原子替换;收尾重复执行覆盖相同目标,不生成新的时间戳副本。
|
||||
"""
|
||||
source = Path(resolved)
|
||||
# 续跑等场景下源文件可能已被上次收尾重命名过:不再重命名,原样返回。
|
||||
if not source.is_file():
|
||||
return resolved
|
||||
# 兼容旧版本:原文件已改名,但最终别名仍记录有效路径时直接复用。
|
||||
existing = self.db.get_artifact(run["id"], alias)
|
||||
if existing is not None and Path(existing["uri"]).is_file():
|
||||
return existing["uri"]
|
||||
raise ValueError(f"missing final output file: {alias} ({resolved})")
|
||||
# 基础名来自上传文件名;无上传文件时退回通用名称 subtitle。
|
||||
base = Path(run["input_uri"]).stem if run.get("input_uri") else "subtitle"
|
||||
# 通过最终输出引用定位产出节点,取其语言参数作为标识。
|
||||
node_id = ref.partition(".")[0]
|
||||
node = next((item for item in definition.nodes if item.id == node_id), None)
|
||||
tag = (node.params.get("target_language") if node else None) or alias
|
||||
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
|
||||
new_path = source.with_name(f"{base}.{tag}.{timestamp}{source.suffix}")
|
||||
source.rename(new_path)
|
||||
# 编码目录别名避免路径分隔符;保留常规 cn_srt/ass 名称便于排查。
|
||||
from urllib.parse import quote
|
||||
|
||||
timestamp = datetime.fromisoformat(run["created_at"]).strftime("%Y%m%d%H%M%S")
|
||||
final_dir = self.storage_dir / "runs" / run["id"] / "finals" / ("output-" + quote(alias, safe=""))
|
||||
filename = f"{base}.{tag}.{timestamp}{source.suffix}"
|
||||
if Path(filename).name != filename or "\\" in filename:
|
||||
raise ValueError(f"invalid final output filename: {alias}")
|
||||
new_path = final_dir / filename
|
||||
atomic_copy(source, new_path)
|
||||
return str(new_path)
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user