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
+149 -9
View File
@@ -124,6 +124,8 @@ async function loadWorkflows() {
<td>${escapeHtml(workflow.latest_version)}</td>
<td>${workflow.published ? badge("ok") : badge("draft")}</td>
<td>
<button data-edit-workflow="${escapeHtml(workflow.id)}">编辑</button>
<button data-versions-workflow="${escapeHtml(workflow.id)}">版本</button>
<button class="danger" data-delete-workflow="${escapeHtml(workflow.id)}">删除</button>
${workflow.published ? "" : `<button data-publish-workflow="${escapeHtml(workflow.id)}">发布</button>`}
</td>
@@ -480,7 +482,109 @@ async function deleteRun(runId) {
}
}
// 创建或更新工作流:解析 DAG JSON 后提交,随后刷新列表
// 当前编辑模式:null=新建;否则为正在编辑的工作流 ID(保存追加新版本)
let editingWorkflowId = null;
// 切换编辑器模式:id 为空进入新建,否则锁定 ID 并提示保存将追加新版本。
function setEditMode(id) {
editingWorkflowId = id;
const idInput = document.getElementById("workflowId");
const mode = document.getElementById("editMode");
const save = document.getElementById("saveWorkflow");
if (!idInput || !mode || !save) return;
if (id) {
idInput.value = id;
idInput.disabled = true; // 编辑模式下锁定 ID,防止误建新工作流。
mode.textContent = `编辑中:${id} —— 保存将追加为新版本。`;
save.textContent = "保存为新版本";
} else {
idInput.disabled = false;
mode.textContent = "新建工作流:填写 ID 与 DAG 后保存。";
save.textContent = "创建工作流";
}
}
// 新建模式:清空表单并预填演示 DAG 模板。
function resetWorkflowForm() {
document.getElementById("workflowId").value = "";
document.getElementById("workflowName").value = "";
document.getElementById("workflowDescription").value = "";
document.getElementById("workflowDefinition").value = JSON.stringify(DEMO_WORKFLOW, null, 2);
setEditMode(null);
}
// 加载指定工作流的最新定义到编辑器(列表"编辑"按钮)。
// 当前页面没有编辑器表单(如管理后台列表)时跳转到编排页并自动加载。
async function loadWorkflowForEdit(workflowId) {
if (!document.getElementById("workflowName")) {
window.location.href = `/workflow.html?edit=${encodeURIComponent(workflowId)}`;
return;
}
try {
const workflow = await api(`/api/admin/workflows/${workflowId}`);
document.getElementById("workflowName").value = workflow.name || "";
document.getElementById("workflowDescription").value = workflow.description || "";
const latest = workflow.latest_version_data;
document.getElementById("workflowDefinition").value = latest
? JSON.stringify(latest.definition, null, 2)
: JSON.stringify({ name: "", version: 1, nodes: [], edges: [], entry_inputs: {}, final_outputs: {} }, null, 2);
setEditMode(workflowId);
} catch (error) {
alert(`加载失败:${error.message}`);
}
}
// 展示指定工作流的版本历史面板。
async function loadWorkflowVersions(workflowId) {
try {
const versions = await api(`/api/admin/workflows/${workflowId}/versions`);
const tbody = document.getElementById("versionList");
document.getElementById("versionPanelTitle").textContent = `版本历史:${workflowId}`;
tbody.innerHTML = versions.length
? versions
.map(
(version) => `
<tr>
<td>v${escapeHtml(version.version)}</td>
<td>${escapeHtml(formatTime(version.created_at))}</td>
<td><button class="ghost" data-load-version="${escapeHtml(workflowId)}:${escapeHtml(version.version)}">加载到编辑器</button></td>
</tr>`,
)
.join("")
: '<tr><td colspan="3">暂无版本</td></tr>';
document.getElementById("versionPanel").hidden = false;
} catch (error) {
alert(`版本加载失败:${error.message}`);
}
}
// 关闭版本历史面板。
function closeVersionPanel() {
const panel = document.getElementById("versionPanel");
if (panel) panel.hidden = true;
}
// 把指定工作流的某历史版本定义加载回编辑器(可对比、回滚后保存为新版本)。
async function loadVersionIntoEditor(workflowId, version) {
try {
const versions = await api(`/api/admin/workflows/${workflowId}/versions`);
const target = versions.find((item) => item.version === Number(version));
if (!target) {
alert(`版本 v${version} 不存在`);
return;
}
const workflow = await api(`/api/admin/workflows/${workflowId}`);
document.getElementById("workflowName").value = workflow.name || "";
document.getElementById("workflowDescription").value = workflow.description || "";
document.getElementById("workflowDefinition").value = JSON.stringify(target.definition, null, 2);
setEditMode(workflowId);
closeVersionPanel();
} catch (error) {
alert(`版本加载失败:${error.message}`);
}
}
// 创建或保存工作流:新建时创建首个版本;编辑时(ID 已锁定)追加新版本。
async function createWorkflow() {
const workflowId = document.getElementById("workflowId").value.trim();
const name = document.getElementById("workflowName").value.trim();
@@ -493,7 +597,7 @@ async function createWorkflow() {
return;
}
try {
await api("/api/admin/workflows", {
const result = await api("/api/admin/workflows", {
method: "POST",
body: JSON.stringify({
id: workflowId || undefined,
@@ -502,8 +606,14 @@ async function createWorkflow() {
definition,
}),
});
alert("工作流已保存");
alert(`工作流已保存${result.id} v${result.latest_version}`);
await loadWorkflows();
// 编辑模式保持编辑状态并刷新最新版本;新建模式回到空表单。
if (editingWorkflowId) {
await loadWorkflowForEdit(editingWorkflowId);
} else {
resetWorkflowForm();
}
} catch (error) {
alert(`保存失败:${error.message}`);
}
@@ -564,6 +674,22 @@ async function uploadVideo() {
// 全局点击委托:按按钮上的 data-* 属性分发到对应操作。
document.addEventListener("click", (event) => {
const editButton = event.target.closest("[data-edit-workflow]");
if (editButton) {
loadWorkflowForEdit(editButton.dataset.editWorkflow);
return;
}
const versionsButton = event.target.closest("[data-versions-workflow]");
if (versionsButton) {
loadWorkflowVersions(versionsButton.dataset.versionsWorkflow);
return;
}
const loadVersionButton = event.target.closest("[data-load-version]");
if (loadVersionButton) {
const [workflowId, version] = loadVersionButton.dataset.loadVersion.split(":");
loadVersionIntoEditor(workflowId, version);
return;
}
const deleteWorkflowButton = event.target.closest("[data-delete-workflow]");
if (deleteWorkflowButton) {
deleteWorkflow(deleteWorkflowButton.dataset.deleteWorkflow);
@@ -597,13 +723,27 @@ document.addEventListener("click", (event) => {
// 页面初始化:预填 DAG、绑定按钮事件并加载对应页面数据。
document.addEventListener("DOMContentLoaded", async () => {
const workflowDefinition = document.getElementById("workflowDefinition");
if (workflowDefinition) {
workflowDefinition.value = JSON.stringify(DEMO_WORKFLOW, null, 2);
// 工作流编辑器:默认进入新建模式并预填演示模板;
// 支持 URL 参数 ?edit=<id> 直接加载指定工作流进行编辑。
if (document.getElementById("workflowDefinition")) {
resetWorkflowForm();
const params = new URLSearchParams(window.location.search);
const editId = params.get("edit");
if (editId) {
await loadWorkflowForEdit(editId);
}
}
const createWorkflowButton = document.getElementById("createWorkflow");
if (createWorkflowButton) {
createWorkflowButton.addEventListener("click", createWorkflow);
const saveWorkflowButton = document.getElementById("saveWorkflow");
if (saveWorkflowButton) {
saveWorkflowButton.addEventListener("click", createWorkflow);
}
const resetWorkflowButton = document.getElementById("resetWorkflow");
if (resetWorkflowButton) {
resetWorkflowButton.addEventListener("click", resetWorkflowForm);
}
const closeVersionsButton = document.getElementById("closeVersions");
if (closeVersionsButton) {
closeVersionsButton.addEventListener("click", closeVersionPanel);
}
const publishWorkflowButton = document.getElementById("publishWorkflow");
if (publishWorkflowButton) {