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:
+149
-9
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user