diff --git a/AGENTS.md b/AGENTS.md index 0a47916..df696df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,3 +13,9 @@ - 页面不直接暴露 API 调用细节给普通用户。 - 前端只通过 `/api/...` 调用后端,不内联业务逻辑。 - 后续迁移到 React 时,页面行为应与当前静态页面保持一致。 + +## 代码注释规范 + +- 本仓库所有源码(HTML、JavaScript、CSS 等支持注释的文件)必须配有详细中文注释,说明页面职责、函数作用与关键交互逻辑,确保后续维护人员可以快速理解代码工作原理。 +- 新增或修改代码时,必须同步补充或更新对应注释;不得删除已有注释。 +- 每个 HTML 页面需要在文件头说明页面用途;公共脚本需要说明各函数的职责。 diff --git a/admin.html b/admin.html index cd2bc20..4f6ff52 100644 --- a/admin.html +++ b/admin.html @@ -1,4 +1,5 @@ + @@ -20,6 +21,7 @@

节点管理

+

注册节点

@@ -30,6 +32,7 @@
+

已注册节点

@@ -48,6 +51,7 @@
+

调用节点

@@ -68,6 +72,7 @@
等待调用...
+

节点实例

diff --git a/assets/app.js b/assets/app.js index c86e776..a924d87 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1,3 +1,6 @@ +// WOV 静态前端公共脚本:所有页面共用的 API 封装、渲染函数与事件绑定。 + +// Echo 演示节点的预置 manifest,便于管理后台一键填入。 const ECHO_MANIFEST = { id: "echo", name: "Echo Node", @@ -14,6 +17,7 @@ const ECHO_MANIFEST = { keep_warm: false, }; +// 演示“视频字幕生成”工作流的 DAG 定义,预填在工作流编排页。 const DEMO_WORKFLOW = { name: "视频字幕生成", version: 1, @@ -55,6 +59,7 @@ const DEMO_WORKFLOW = { }, }; +// 统一封装 fetch:自动携带 JSON 头、解析响应并在失败时抛出可读错误。 async function api(path, options = {}) { const response = await fetch(path, { headers: { "Content-Type": "application/json", ...(options.headers || {}) }, @@ -62,12 +67,14 @@ async function api(path, options = {}) { }); const data = await response.json().catch(() => null); if (!response.ok) { + // FastAPI 的校验错误 detail 可能是数组,统一序列化为字符串展示。 const detail = data && data.detail ? JSON.stringify(data.detail) : response.statusText; throw new Error(`${response.status} ${detail}`); } return data; } +// 转义用户可控文本,防止 XSS 注入到表格或状态 HTML 中。 function escapeHtml(value) { return String(value) .replaceAll("&", "&") @@ -77,7 +84,9 @@ function escapeHtml(value) { .replaceAll("'", "'"); } +// 根据状态生成带语义颜色的徽章 HTML。 function badge(status) { + // 统一转小写比较,兼容后端返回的不同大小写。 const value = String(status).toLowerCase(); const className = value === "ready" || value === "completed" || value === "ok" @@ -88,12 +97,14 @@ function badge(status) { return `${escapeHtml(status)}`; } +// 把 ISO 时间格式化为本地时间;非法值原样返回。 function formatTime(value) { if (!value) return ""; const date = new Date(value); return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); } +// 计算距给定时间的流逝时长,用于任务页展示已运行时间。 function formatElapsed(value) { if (!value) return ""; const seconds = Math.floor((Date.now() - new Date(value).getTime()) / 1000); @@ -102,6 +113,7 @@ function formatElapsed(value) { return `${Math.floor(seconds / 60)} 分钟`; } +// 健康检查失败时在页面顶部展示后端不可用提示。 async function loadHealth() { try { await api("/health"); @@ -113,6 +125,7 @@ async function loadHealth() { } } +// 加载已注册节点并渲染节点表格与调用下拉框。 async function loadNodes() { const nodes = await api("/api/admin/nodes"); const tbody = document.getElementById("nodeList"); @@ -136,6 +149,7 @@ async function loadNodes() { .join(""); } +// 加载节点实例并渲染实例表格。 async function loadInstances() { const instances = await api("/api/admin/node-instances"); const tbody = document.getElementById("instanceList"); @@ -156,10 +170,12 @@ async function loadInstances() { : '暂无实例'; } +// 管理后台页面同时刷新节点与实例两个列表。 async function refresh() { await Promise.all([loadNodes(), loadInstances()]); } +// 注册节点:解析文本域中的 manifest JSON 并提交到后端。 async function registerNode() { const raw = document.getElementById("manifestJson").value.trim(); try { @@ -172,6 +188,7 @@ async function registerNode() { } } +// 手动调用节点:读取表单参数并展示返回结果。 async function invokeNode() { const nodeId = document.getElementById("invokeNodeId").value; const runId = document.getElementById("invokeRunId").value.trim() || "run_web"; @@ -190,6 +207,7 @@ async function invokeNode() { } } +// 删除节点前不做二次确认,由点击按钮触发。 async function deleteNode(nodeId) { try { await api(`/api/admin/nodes/${nodeId}`, { method: "DELETE" }); @@ -199,6 +217,7 @@ async function deleteNode(nodeId) { } } +// 请求后端停止指定节点实例。 async function stopInstance(instanceId) { try { await api(`/api/admin/node-instances/${instanceId}/stop`, { method: "POST" }); @@ -208,6 +227,7 @@ async function stopInstance(instanceId) { } } +// 应用中心的 Echo 演示:直接调用 echo 节点并展示结果。 async function runEcho() { const text = document.getElementById("echoText").value; const resultBox = document.getElementById("echoResult"); @@ -227,6 +247,7 @@ async function runEcho() { } } +// 加载工作流列表并渲染发布状态与操作按钮。 async function loadWorkflows() { const workflows = await api("/api/admin/workflows"); const tbody = document.getElementById("workflowList"); @@ -250,6 +271,7 @@ async function loadWorkflows() { : '暂无工作流'; } +// 加载最近任务并渲染任务表格,失败任务提供重试按钮。 async function loadRuns() { const tbody = document.getElementById("runList"); if (!tbody) return; @@ -259,6 +281,7 @@ async function loadRuns() { .map((run) => { const percent = Math.round((run.progress || 0) * 100); const error = run.error || ""; + // RUNNING/QUEUED 附加耗时或排队提示,其余状态只显示徽章。 const statusHtml = run.status === "RUNNING" ? `${badge(run.status)} 已运行 ${formatElapsed(run.updated_at)}` @@ -285,6 +308,7 @@ async function loadRuns() { : '暂无任务'; } +// 请求后端重试失败任务,成功后刷新列表。 async function retryRun(runId) { try { const result = await api(`/api/runs/${runId}/retry`, { method: "POST" }); @@ -295,6 +319,7 @@ async function retryRun(runId) { } } +// 创建或更新工作流:解析 DAG JSON 后提交,随后刷新列表。 async function createWorkflow() { const workflowId = document.getElementById("workflowId").value.trim(); const name = document.getElementById("workflowName").value.trim(); @@ -323,6 +348,7 @@ async function createWorkflow() { } } +// 发布指定工作流,使其出现在用户应用中心。 async function publishWorkflow(workflowId) { try { await api(`/api/admin/workflows/${workflowId}/publish`, { method: "POST" }); @@ -332,6 +358,7 @@ async function publishWorkflow(workflowId) { } } +// 删除指定工作流。 async function deleteWorkflow(workflowId) { try { await api(`/api/admin/workflows/${workflowId}`, { method: "DELETE" }); @@ -341,6 +368,7 @@ async function deleteWorkflow(workflowId) { } } +// 上传视频并轮询任务进度,完成后渲染下载链接。 async function uploadVideo() { const fileInput = document.getElementById("videoFile"); const progress = document.getElementById("runProgress"); @@ -365,6 +393,7 @@ async function uploadVideo() { } } +// 每秒轮询一次任务状态,最多 3600 次(约 1 小时),超时后提示查看接口。 async function pollRun(runId, progress, downloads) { for (let i = 0; i < 3600; i += 1) { const run = await api(`/api/runs/${runId}`); @@ -383,6 +412,7 @@ async function pollRun(runId, progress, downloads) { progress.textContent = "轮询超时,请到 /api/runs 查看任务状态"; } +// 只展示用户关心的最终产物(中文 SRT 与 ASS)下载入口。 function renderArtifacts(runId, artifacts, container) { container.innerHTML = artifacts .filter((item) => ["cn_srt", "ass"].includes(item.name)) @@ -395,6 +425,7 @@ function renderArtifacts(runId, artifacts, container) { .join(""); } +// 全局点击委托:按按钮上的 data-* 属性分发到对应操作。 document.addEventListener("click", (event) => { const deleteNodeButton = event.target.closest("[data-delete-node]"); if (deleteNodeButton) { @@ -422,6 +453,7 @@ document.addEventListener("click", (event) => { } }); +// 页面初始化:预填 manifest/DAG、绑定按钮事件并加载对应页面数据。 document.addEventListener("DOMContentLoaded", async () => { const manifestBox = document.getElementById("manifestJson"); if (manifestBox) { @@ -471,6 +503,7 @@ document.addEventListener("DOMContentLoaded", async () => { if (document.getElementById("workflowList")) { await loadWorkflows(); } + // 任务管理页每 5 秒自动刷新一次状态。 if (document.getElementById("runList")) { await loadRuns(); setInterval(loadRuns, 5000); diff --git a/assets/styles.css b/assets/styles.css index 0ff304f..47d88cc 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -1,3 +1,6 @@ +/* WOV 静态前端全局样式:定义色彩变量、布局与通用组件样式。 */ + +/* 设计令牌:集中管理配色,后续换肤只需修改变量。 */ :root { --bg: #f5f7fa; --surface: #ffffff; @@ -10,10 +13,12 @@ --radius: 8px; } +/* 全局盒模型与基础排版重置。 */ * { box-sizing: border-box; } +/* 页面主体:浅灰背景与默认文字颜色。 */ body { margin: 0; font-family: "Segoe UI", "Microsoft YaHei", sans-serif; @@ -21,6 +26,7 @@ body { background: var(--bg); } +/* 顶部导航栏:品牌标题与页面导航。 */ .topbar { display: flex; align-items: center; @@ -30,6 +36,7 @@ body { border-bottom: 1px solid var(--border); } +/* 品牌标题样式。 */ .brand { font-size: 20px; font-weight: 700; @@ -37,11 +44,13 @@ body { text-decoration: none; } +/* 导航链接横向排列。 */ nav { display: flex; gap: 18px; } +/* 导航链接默认使用弱化色,悬停时高亮。 */ nav a { color: var(--muted); text-decoration: none; @@ -52,12 +61,14 @@ nav a:hover { color: var(--primary); } +/* 内容容器:限制最大宽度并居中。 */ .container { max-width: 1080px; margin: 0 auto; padding: 28px 20px 60px; } +/* 一级标题与二级标题的字号控制。 */ h1 { margin: 0 0 8px; font-size: 28px; @@ -68,10 +79,12 @@ h2 { font-size: 18px; } +/* 弱化文字:用于说明、时间等次要信息。 */ .muted { color: var(--muted); } +/* 应用中心卡片网格:自适应列数的最小宽度布局。 */ .app-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); @@ -79,6 +92,7 @@ h2 { margin-top: 24px; } +/* 卡片与面板共用白底、边框和圆角外观。 */ .card, .panel { background: var(--surface); @@ -87,14 +101,17 @@ h2 { padding: 20px; } +/* 面板之间保持纵向间距。 */ .panel { margin-top: 22px; } +/* 禁用态样式:降低透明度表达不可用。 */ .disabled { opacity: 0.6; } +/* 表单标签:块级显示并加粗。 */ label { display: block; margin: 10px 0 6px; @@ -103,6 +120,7 @@ label { color: var(--muted); } +/* 输入控件统一样式,宽度撑满容器。 */ input, select, textarea { @@ -114,11 +132,13 @@ textarea { background: #fff; } +/* 文本域使用等宽字体并允许纵向拉伸。 */ textarea { font-family: Consolas, monospace; resize: vertical; } +/* 按钮基础样式。 */ button { margin-top: 12px; padding: 9px 16px; @@ -131,21 +151,25 @@ button { cursor: pointer; } +/* 按钮悬停强调边框与文字颜色。 */ button:hover { border-color: var(--primary); color: var(--primary); } +/* 主按钮:填充品牌色。 */ button.primary { background: var(--primary); border-color: var(--primary); color: #fff; } +/* 幽灵按钮:透明背景,用于次要操作。 */ button.ghost { background: transparent; } +/* 操作区与表单网格:弹性换行排列。 */ .actions, .form-grid { display: flex; @@ -153,25 +177,30 @@ button.ghost { flex-wrap: wrap; } +/* 表单网格:默认两列,宽字段占整行。 */ .form-grid { display: grid; grid-template-columns: repeat(2, 1fr); } +/* 宽字段跨满整行。 */ .form-grid .wide { grid-column: 1 / -1; } +/* 表格容器:窄屏时允许横向滚动。 */ .table-wrap { overflow-x: auto; } +/* 表格基础样式。 */ table { width: 100%; border-collapse: collapse; font-size: 14px; } +/* 表头与单元格的间距、对齐与分隔线。 */ th, td { padding: 10px; @@ -180,12 +209,14 @@ td { white-space: nowrap; } +/* 表头使用弱化色与小字号。 */ th { color: var(--muted); font-size: 12px; text-transform: uppercase; } +/* 结果块:深色等宽字体,适合展示 JSON 或日志。 */ .result { margin: 16px 0 0; padding: 14px; @@ -199,6 +230,7 @@ th { overflow-wrap: anywhere; } +/* 下载区:横向排列的下载链接。 */ .downloads { display: flex; gap: 12px; @@ -206,6 +238,7 @@ th { margin-top: 14px; } +/* 下载链接:描边按钮式链接。 */ .download-link { display: inline-block; padding: 8px 14px; @@ -216,10 +249,12 @@ th { text-decoration: none; } +/* 下载链接悬停时轻微填充背景。 */ .download-link:hover { background: #e8f1f9; } +/* 状态徽章基础样式。 */ .badge { display: inline-block; padding: 3px 8px; @@ -228,21 +263,25 @@ th { font-weight: 700; } +/* 成功徽章:绿色背景。 */ .badge.ok { background: #e2f4ea; color: var(--ok); } +/* 警告徽章:黄色背景。 */ .badge.warn { background: #fff2d9; color: #8a5a00; } +/* 错误徽章:红色背景。 */ .badge.error { background: #fde8e6; color: var(--danger); } +/* 危险操作按钮文字颜色。 */ .danger { color: var(--danger); } diff --git a/index.html b/index.html index 20d90f6..a4fbadf 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,5 @@ + @@ -18,10 +19,12 @@
+

WOV 应用中心

选择一个已发布的应用,上传或输入内容后由后台自动执行。

+

Echo 演示

调用已注册的 echo 节点,验证前端到后端再到节点进程的完整链路。

@@ -31,6 +34,7 @@
等待运行...
+

视频字幕生成

上传视频后自动执行提音、转写、翻译和 VR 双眼 ASS 生成。

diff --git a/tasks.html b/tasks.html index fc7a549..14af2a1 100644 --- a/tasks.html +++ b/tasks.html @@ -1,4 +1,5 @@ + @@ -21,6 +22,7 @@

任务管理

查看所有任务的执行状态;失败的任务可以重新排队执行。

+
diff --git a/workflow.html b/workflow.html index 3992a05..014cdcd 100644 --- a/workflow.html +++ b/workflow.html @@ -1,4 +1,5 @@ + @@ -20,6 +21,7 @@

工作流编排

+

工作流定义

@@ -36,6 +38,7 @@
+

已发布工作流