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 @@
+