feat: 任务断点续跑/暂停中断/LLM 过滤优化与调度容错

调度与状态机:
- 修复 PAUSED 任务被拾起后复活执行(点击暂停反而开始任务):next_queued_run
  只取 QUEUED,execute_run 以 PAUSED 进入直接返回,暂停必须显式 resume
- 重启恢复:启动时 recover_interrupted_runs 把遗留 RUNNING 置 QUEUED(保留产物)
- 暂停信号 paused.flag:暂停接口写、继续/重试清除,OCR 逐帧检查秒级中断,
  节点内被暂停保持 PAUSED 不误报 FAILED
- 调度轮询容错:_loop 异常不杀死线程(曾致任务永久停留 QUEUED)

subtitle-ocr 节点级断点:
- ocr_partial.jsonl 逐帧存档,重启/暂停后只处理未处理帧,产物与一次跑完一致
- 进度日志携带窗口平均耗时与线程数;取消后抑制进度日志井喷

llm-filter 过滤质量与限流自适应:
- 上下文净化:喂给 LLM 的是过滤后的字幕(规则层垃圾从上下文剔除)
- 正则确定性过滤:裸网址域名、HTML/水印模式直接删除
- 429/5xx 指数退避重试;worker 限流错误 report_failure 内存临时降最大线程数
  并缩容(无错误窗口回升),失败条目降并发后重试一轮
- 保留长文本保护(noise 不删 ≥min_keep_len 文本,LLM 判定不稳的必要兜底)

前端:
- 工作流编排页支持选择工作流编辑(加载最新/历史版本)、版本历史面板、
  新建/编辑双模式;管理后台编辑跳转 workflow.html?edit=<id>

工作流:ocr-subtitle v7(filter pool_max_workers=20、pool_fast_threshold=1)
This commit is contained in:
2026-08-19 01:27:41 +08:00
parent 4ebbfc5198
commit b16e0e9f3c
19 changed files with 1237 additions and 111 deletions
+40 -3
View File
@@ -238,7 +238,12 @@ def test_db_migration_adds_param_overrides(tmp_path) -> None:
def test_pause_resume_run(tmp_path) -> None:
"""验证 pause_run/resume_run 的状态流转与 PAUSED 任务被调度器取到。"""
"""验证 pause_run/resume_run 的状态流转与 PAUSED 任务被调度器自动拾起。
修复回归:PAUSED 任务若被 next_queued_run 取到,execute_run 会把它复活为
RUNNING 继续执行——"点击暂停反而开始任务"。暂停必须由用户显式 resume
PAUSED → QUEUED)后调度器才重新执行。
"""
db = Database(tmp_path / "wov.db")
db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1})
now = "2026-01-01T00:00:00+00:00"
@@ -255,13 +260,45 @@ def test_pause_resume_run(tmp_path) -> None:
)
db.pause_run("run_p", now)
assert db.get_run("run_p")["status"] == "PAUSED"
# PAUSED 任务会被 next_queued_run 取到(等待续跑)。
assert db.next_queued_run()["id"] == "run_p"
# 已暂停的任务会被调度器拾起(等待用户显式 resume)。
assert db.next_queued_run() is None
db.resume_run("run_p", now)
assert db.get_run("run_p")["status"] == "QUEUED"
assert db.next_queued_run()["id"] == "run_p"
def test_recover_interrupted_runs(tmp_path) -> None:
"""重启恢复:遗留 RUNNING 任务恢复为 QUEUED(保留产物供断点续跑)。
进程被杀/重启时 RUNNING 任务不会自动收尾,若保持 RUNNING 将永久孤儿
next_queued_run 不拾起、暂停后又被 execute_run 复活)。恢复为 QUEUED
后调度器会从产物表断点续跑;用户主动暂停的 PAUSED 任务保持不变。
"""
db = Database(tmp_path / "wov.db")
db.upsert_workflow({"id": "demo", "name": "Demo", "published": 1, "latest_version": 1})
now = "2026-01-01T00:00:00+00:00"
for run_id, status in (("run_orphan", "RUNNING"), ("run_paused", "PAUSED"),
("run_done", "COMPLETED")):
db.create_run(
{
"id": run_id,
"workflow_id": "demo",
"workflow_version": 1,
"status": status,
"progress": 0.5,
"created_at": now,
"updated_at": now,
}
)
recovered = db.recover_interrupted_runs("2026-01-02T00:00:00+00:00")
assert recovered == 1 # 只有 RUNNING 被恢复。
assert db.get_run("run_orphan")["status"] == "QUEUED"
assert db.get_run("run_orphan")["updated_at"] == "2026-01-02T00:00:00+00:00"
assert db.get_run("run_paused")["status"] == "PAUSED"
assert db.get_run("run_done")["status"] == "COMPLETED"
# 恢复后调度器可拾起并断点续跑。
assert db.next_queued_run()["id"] == "run_orphan"
def test_restore_run_outputs(tmp_path) -> None:
"""验证从产物重建节点输出(断点续跑的依据)。"""
db = Database(tmp_path / "wov.db")