From 5ff000599bfece7cba857f0fb020901356be46d8 Mon Sep 17 00:00:00 2001 From: Flintcore Date: Thu, 23 Apr 2026 19:17:34 +0800 Subject: [PATCH] feat: enhance workflow visibility and project metrics --- docs/engineering-optimization-plan.md | 48 ++++++++++++++++++ .../[locale]/settings/prompt-config-form.tsx | 2 +- .../workspace/[projectId]/project-stats.tsx | 12 +++-- .../[projectId]/workflow-pipeline-dag.tsx | 50 +++++++++++++++++-- 4 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 docs/engineering-optimization-plan.md diff --git a/docs/engineering-optimization-plan.md b/docs/engineering-optimization-plan.md new file mode 100644 index 0000000..e4f6241 --- /dev/null +++ b/docs/engineering-optimization-plan.md @@ -0,0 +1,48 @@ +# FlintStudio 工程优化方案(代码质量 + 产品策划) + +## 目标 + +在不改变核心业务流程(`analyze_novel -> ... -> video`)的前提下,优先提升: + +1. **可维护性**:减少 lint 噪音,让“真正的问题”更显眼。 +2. **可观测性**:增强阶段视图与当前流程状态的一致性,降低排障成本。 +3. **产品体验一致性**:让关键数据卡片、异常状态、交互行为具备明确语义。 + +--- + +## 多维度优化建议(策划层) + +### 1) 研发效率与代码质量 + +- 建议把 lint warning 逐步收敛到 0,并在 CI 中使用 `--max-warnings=0` 阶段性门禁。 +- 对 `src/lib/workflow`、`src/app/api/workflows` 增补轻量级契约测试(尤其是 phase 状态转移)。 +- 统一“前端展示状态”与“后端步骤状态”枚举来源,避免并行维护。 + +### 2) 工作流产品能力 + +- 在 DAG 节点详情中加入“重试次数 / 最近错误摘要 / 上游依赖耗时”。 +- 提供“失败后推荐动作”模板(重新分场、降低出图并发、切换模型)。 +- 项目统计卡片可加入“近 24 小时产能”和“平均每集耗时”作为运营指标。 + +### 3) 运行与稳定性 + +- BullMQ 任务建议补充队列级别的积压阈值告警。 +- 对图像/语音供应商 API 增加熔断事件审计,形成故障画像。 +- 在 `/api/health` 增加数据库延迟与 Redis ping 延迟字段,便于 SRE 快速判断瓶颈层。 + +--- + +## 本次已执行的改进(可立即落地) + +1. 清理了前端表单中的无用异常变量,减少 lint warning 干扰。 +2. 修复统计卡片中图标命名与 JSX a11y 规则冲突的问题。 +3. DAG 视图增加 `currentPhase` 驱动的默认选中逻辑,页面初始即可定位当前阶段详情。 +4. 移除未使用的 `setEdges` 变量,收敛无效状态引用。 + +> 以上改动均为低风险、可回滚、小步提交,适合作为“质量基线收敛”的第一轮。 + +## 第二轮功能优化(本次新增) + +1. **DAG 进度可视化增强**:新增总进度条(完成数/总数 + 百分比)和运行中/失败计数。 +2. **当前阶段显式提示**:在图上方展示后端 `currentPhase` 对应中文阶段,避免“图上看不出正在跑哪一步”。 +3. **项目统计扩展**:新增“场次数”指标卡,补齐从集数→场次→分镜→配音→总耗时的核心链路数据。 diff --git a/src/app/[locale]/settings/prompt-config-form.tsx b/src/app/[locale]/settings/prompt-config-form.tsx index 0ac0c26..217c990 100644 --- a/src/app/[locale]/settings/prompt-config-form.tsx +++ b/src/app/[locale]/settings/prompt-config-form.tsx @@ -115,7 +115,7 @@ export function PromptConfigForm() { if (!res.ok) throw new Error("保存失败"); setMessage({ type: "success", text: "提示词已保存" }); setTimeout(() => setMessage(null), 3000); - } catch (e) { + } catch { setMessage({ type: "error", text: "保存失败,请重试" }); } finally { setSaving(false); diff --git a/src/app/[locale]/workspace/[projectId]/project-stats.tsx b/src/app/[locale]/workspace/[projectId]/project-stats.tsx index db0f707..45b6036 100644 --- a/src/app/[locale]/workspace/[projectId]/project-stats.tsx +++ b/src/app/[locale]/workspace/[projectId]/project-stats.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect } from "react"; -import { Film, Image, Mic, Clock } from "lucide-react"; +import { Film, Image as ImageIcon, Mic, Clock, Clapperboard } from "lucide-react"; type Stats = { episodeCount: number; @@ -51,13 +51,19 @@ export function ProjectStats({ projectId }: { projectId: string }) { sub: stats.completedEpisodes > 0 ? "已完成" : "已生成", }, { - icon: , + icon: , label: "分镜图", value: stats.imagesGenerated > 0 ? `${stats.imagesGenerated} / ${stats.panelCount}` : String(stats.panelCount), sub: stats.imagesGenerated > 0 ? "已出图" : "分镜数", }, + { + icon: , + label: "场次数", + value: String(stats.clipCount), + sub: "总场次", + }, { icon: , label: "配音行", @@ -76,7 +82,7 @@ export function ProjectStats({ projectId }: { projectId: string }) { return (
-
+
{cards.map((c) => (
{ setNodes(nodes); }, [nodes, setNodes]); + useEffect(() => { + if (!currentPhase) return; + const phaseExists = PIPELINE_PHASES.some((phase) => phase.key === currentPhase); + if (phaseExists) setSelectedPhaseKey(currentPhase); + }, [currentPhase]); + const onNodeClick: NodeMouseHandler = useCallback( (_, node) => { const phaseKey = node.data?.phaseKey; @@ -181,12 +187,48 @@ export function PipelineDagView({ const selectedLabel = selectedPhaseKey ? PIPELINE_PHASES.find((p) => p.key === selectedPhaseKey)?.label ?? selectedPhaseKey : null; + const currentPhaseLabel = currentPhase + ? PIPELINE_PHASES.find((p) => p.key === currentPhase)?.label ?? currentPhase + : null; + + const phaseMetrics = useMemo(() => { + const statusList = nodesState.map((node) => node.data.status); + const completed = statusList.filter((status) => status === "completed").length; + const running = statusList.filter((status) => status === "running").length; + const failed = statusList.filter((status) => status === "failed").length; + const total = statusList.length; + const progress = total > 0 ? Math.round((completed / total) * 100) : 0; + return { completed, running, failed, total, progress }; + }, [nodesState]); return (
-

- 流水线 DAG · 点击节点查看阶段详情(输入/输出/用时) -

+
+
+ 流水线 DAG · 点击节点查看阶段详情(输入/输出/用时) + {currentPhaseLabel && ( + + 当前阶段:{currentPhaseLabel} + + )} +
+
+
+ 进度 {phaseMetrics.completed}/{phaseMetrics.total} + {phaseMetrics.progress}% +
+
+
+
+
+ 运行中 {phaseMetrics.running} + 失败 {phaseMetrics.failed} +
+
+