From 679d6947e7e58985290e0af17744c47aa14c5878 Mon Sep 17 00:00:00 2001 From: ZhuJW Date: Fri, 13 Mar 2026 21:11:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4task?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 3 +- src/Layout.tsx | 14 + src/config.ts | 3 + src/pages/TaskExecutor.tsx | 740 +++++++++++++++++++++++++++++++++++++ template/task.html | 685 ++++++++++++++++++++++++++++++++++ vite.config.ts | 5 + 6 files changed, 1449 insertions(+), 1 deletion(-) create mode 100644 src/pages/TaskExecutor.tsx create mode 100644 template/task.html diff --git a/src/App.tsx b/src/App.tsx index 4ea3430..841d471 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,7 @@ import Layout from "./Layout"; import PlanningAgent from "./pages/PlanningAgent"; import DevOpsAgent from "./pages/DevOpsAgent"; import QualityGate from "./pages/QualityGate"; - +import TaskExecutor from "./pages/TaskExecutor"; import Home from "./pages/Home"; export default function App() { @@ -18,6 +18,7 @@ export default function App() { } /> } /> } /> + } /> } /> diff --git a/src/Layout.tsx b/src/Layout.tsx index ad24c11..09fd67e 100644 --- a/src/Layout.tsx +++ b/src/Layout.tsx @@ -9,6 +9,7 @@ const PAGE_TITLES: Record = { "/quality/dashboard": "质量门控 (Quality Gate Dashboard)", "/quality/pr-list": "合并请求审查 (PR List)", "/quality/settings": "质量设置", + "/task": "任务执行器 (Task Executor)", }; export default function Layout() { @@ -82,6 +83,19 @@ export default function Layout() { 质量门控 + + `flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-semibold transition-all duration-150 ${ + isActive + ? "bg-orange-500/10 text-orange-500" + : "text-txt hover:bg-surface-muted hover:text-orange-500" + }` + } + > + + 任务执行器 +
设置与支持
diff --git a/src/config.ts b/src/config.ts index 6a1d925..99282e1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,4 +12,7 @@ export const API = { /** 质量门禁 Agent — PR scanning & code review */ quality: "/quality-api", + + /** SDLC Task Executor — multi-agent software delivery */ + sdlc: "/api/v1/sdlc", } as const; diff --git a/src/pages/TaskExecutor.tsx b/src/pages/TaskExecutor.tsx new file mode 100644 index 0000000..df28aa0 --- /dev/null +++ b/src/pages/TaskExecutor.tsx @@ -0,0 +1,740 @@ +import { useCallback, useEffect, useReducer, useRef } from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import rehypeRaw from "rehype-raw"; + +/* ─── Types ─── */ +type Stage = { + id: string; + name: string; + agent: string; + status: "pending" | "processing" | "completed"; +}; + +type LogEntry = { + timestamp: string; + event: string; + source: string; + message: string; +}; + +type Result = { + title: string; + content: string; + timestamp: string; + expanded: boolean; +}; + +type PollResponse = { + events: Array<{ + event: string; + data: any; + }>; + has_more: boolean; + status: "processing" | "completed" | "failed"; +}; + +type State = { + requirement: string; + taskId: string | null; + isProcessing: boolean; + connectionStatus: "connecting" | "connected" | "disconnected"; + stages: Stage[]; + logs: LogEntry[]; + results: Result[]; + showCopyToast: boolean; +}; + +type Action = + | { type: "set-requirement"; value: string } + | { type: "set-task-id"; value: string } + | { type: "set-processing"; value: boolean } + | { type: "set-connection"; status: "connecting" | "connected" | "disconnected" } + | { type: "init-stages" } + | { type: "update-stage"; stageId: string; status: "pending" | "processing" | "completed" } + | { type: "add-log"; log: LogEntry } + | { type: "add-result"; result: Result } + | { type: "toggle-result"; index: number } + | { type: "clear-logs" } + | { type: "show-copy-toast"; show: boolean } + | { type: "reset" }; + +/* ─── Helpers ─── */ +function formatTime(ts: number) { + return new Date(ts).toLocaleTimeString("zh-CN"); +} + +function formatDate(timestamp: string | number) { + if (!timestamp) return ""; + try { + return new Date(timestamp).toLocaleString("zh-CN"); + } catch { + return String(timestamp); + } +} + +function reducer(state: State, action: Action): State { + switch (action.type) { + case "set-requirement": + return { ...state, requirement: action.value }; + case "set-task-id": + return { ...state, taskId: action.value }; + case "set-processing": + return { ...state, isProcessing: action.value }; + case "set-connection": + return { ...state, connectionStatus: action.status }; + case "init-stages": + return { + ...state, + stages: [ + { id: "pm", name: "需求分析", agent: "PM Agent", status: "pending" }, + { id: "qa", name: "测试设计", agent: "QA Agent", status: "pending" }, + { id: "dev", name: "代码实现", agent: "Dev Agent", status: "pending" }, + { id: "final", name: "交付完成", agent: "Orchestrator", status: "pending" }, + ], + }; + case "update-stage": + return { + ...state, + stages: state.stages.map((s) => + s.id === action.stageId ? { ...s, status: action.status } : s + ), + }; + case "add-log": + return { + ...state, + logs: [...state.logs, action.log].slice(-100), + }; + case "add-result": + return { + ...state, + results: [...state.results, action.result], + }; + case "toggle-result": + return { + ...state, + results: state.results.map((r, i) => + i === action.index ? { ...r, expanded: !r.expanded } : r + ), + }; + case "clear-logs": + return { ...state, logs: [] }; + case "show-copy-toast": + return { ...state, showCopyToast: action.show }; + case "reset": + return { + ...state, + taskId: null, + isProcessing: false, + connectionStatus: "disconnected", + stages: [], + logs: [], + results: [], + showCopyToast: false, + }; + default: + return state; + } +} + +const initialState: State = { + requirement: "", + taskId: null, + isProcessing: false, + connectionStatus: "disconnected", + stages: [], + logs: [], + results: [], + showCopyToast: false, +}; + +/* ─── API Functions ─── */ +async function startSDLC(requirement: string) { + const res = await fetch("/task-api/v1/sdlc/start", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ requirement }), + }); + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + return (await res.json()) as { task_id: string }; +} + +async function pollEvents(taskId: string, lastIndex: number): Promise { + const res = await fetch(`/task-api/v1/sdlc/poll/${taskId}?last_index=${lastIndex}`); + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + return res.json(); +} + +/* ─── Copy to Clipboard ─── */ +async function copyToClipboard(text: string) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + return false; + } +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━ Component ━━━━━━━━━━━━━━━━━━━━━━━ */ +export default function TaskExecutor() { + const [state, dispatch] = useReducer(reducer, initialState); + const pollTimeoutRef = useRef(null); + const pollCountRef = useRef(0); + const lastIndexRef = useRef(0); + const maxPolls = 600; // 最多轮询 600 次 (10 分钟) + + const canStart = state.requirement.trim().length >= 10 && !state.isProcessing; + + const connectionStatusClass = { + connecting: "bg-yellow-500 animate-pulse", + connected: "bg-green-500", + disconnected: "bg-red-500", + }[state.connectionStatus]; + + const connectionStatusText = { + connecting: "连接中...", + connected: "已连接", + disconnected: "未连接", + }[state.connectionStatus]; + + const getStageIconClass = (status: Stage["status"]) => { + const map = { + pending: "bg-gray-400", + processing: "bg-blue-500", + completed: "bg-green-500", + }; + return map[status] || map.pending; + }; + + const getStageBadgeClass = (status: Stage["status"]) => { + const map = { + pending: "bg-gray-100 text-gray-800", + processing: "bg-blue-100 text-blue-800", + completed: "bg-green-100 text-green-800", + }; + return map[status] || map.pending; + }; + + const getStageStatusText = (status: Stage["status"]) => { + const map = { + pending: "等待中", + processing: "进行中", + completed: "已完成", + }; + return map[status] || status; + }; + + const getLogLevelClass = (event: string) => { + const map: Record = { + pm_start: "text-blue-400", + pm_complete: "text-green-400", + qa_start: "text-blue-400", + qa_complete: "text-green-400", + dev_start: "text-blue-400", + dev_complete: "text-green-400", + final_result: "text-purple-400", + error: "text-red-400", + system: "text-yellow-400", + task_started: "text-white", + }; + return map[event] || "text-gray-400"; + }; + + /** + * 处理单个事件 + */ + const handleEvent = useCallback( + (eventType: string, data: any) => { + switch (eventType) { + case "task_started": + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "task_started", + source: "System", + message: data.message || "任务已启动", + }, + }); + break; + + case "pm_start": + dispatch({ type: "update-stage", stageId: "pm", status: "processing" }); + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "pm_start", + source: "PM Agent", + message: "开始需求分析...", + }, + }); + break; + + case "pm_complete": + dispatch({ type: "update-stage", stageId: "pm", status: "completed" }); + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "pm_complete", + source: "PM Agent", + message: "需求分析完成", + }, + }); + if (data.content && data.timestamp) { + dispatch({ + type: "add-result", + result: { + title: "📋 软件需求规格说明书 (SRS)", + content: data.content, + timestamp: data.timestamp, + expanded: true, + }, + }); + } + break; + + case "qa_start": + dispatch({ type: "update-stage", stageId: "qa", status: "processing" }); + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "qa_start", + source: "QA Agent", + message: "开始测试用例设计...", + }, + }); + break; + + case "qa_complete": + dispatch({ type: "update-stage", stageId: "qa", status: "completed" }); + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "qa_complete", + source: "QA Agent", + message: "测试用例设计完成", + }, + }); + if (data.content && data.timestamp) { + dispatch({ + type: "add-result", + result: { + title: "🧪 测试方案与用例", + content: data.content, + timestamp: data.timestamp, + expanded: true, + }, + }); + } + break; + + case "dev_start": + dispatch({ type: "update-stage", stageId: "dev", status: "processing" }); + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "dev_start", + source: "Dev Agent", + message: "开始代码实现...", + }, + }); + break; + + case "dev_complete": + dispatch({ type: "update-stage", stageId: "dev", status: "completed" }); + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "dev_complete", + source: "Dev Agent", + message: "代码实现完成", + }, + }); + if (data.content && data.timestamp) { + dispatch({ + type: "add-result", + result: { + title: "💻 代码实现", + content: data.content, + timestamp: data.timestamp, + expanded: true, + }, + }); + } + break; + + case "final_result": + dispatch({ type: "update-stage", stageId: "final", status: "completed" }); + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "final_result", + source: "System", + message: "SDLC 流程完成", + }, + }); + break; + + case "error": + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "error", + source: "Error", + message: data.error || "未知错误", + }, + }); + alert(`执行错误:${data.error}`); + break; + } + }, + [], + ); + + /** + * 轮询函数 + */ + const poll = useCallback( + (taskId: string) => { + if (pollCountRef.current >= maxPolls) { + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "system", + source: "POLL", + message: "轮询超时", + }, + }); + dispatch({ type: "set-processing", value: false }); + dispatch({ type: "set-connection", status: "disconnected" }); + return; + } + + pollEvents(taskId, lastIndexRef.current) + .then(({ events, has_more, status }) => { + // 处理新事件 + events.forEach((event) => { + lastIndexRef.current++; + handleEvent(event.event, event.data); + }); + + // 检查是否继续轮询 + if (status === "completed" || status === "failed") { + dispatch({ type: "set-processing", value: false }); + dispatch({ type: "set-connection", status: "disconnected" }); + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "system", + source: "POLL", + message: `任务完成,状态:${status}`, + }, + }); + return; + } + + if (has_more || events.length > 0) { + pollCountRef.current++; + pollTimeoutRef.current = setTimeout(() => poll(taskId), 500); + } else if (status === "processing") { + pollCountRef.current++; + pollTimeoutRef.current = setTimeout(() => poll(taskId), 1000); + } + }) + .catch((err) => { + console.error("轮询失败:", err); + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "error", + source: "POLL", + message: err.message, + }, + }); + pollCountRef.current++; + pollTimeoutRef.current = setTimeout(() => poll(taskId), 2000); + }); + }, + [handleEvent], + ); + + const handleStart = useCallback(async () => { + if (!canStart) return; + + dispatch({ type: "reset" }); + dispatch({ type: "set-processing", value: true }); + + try { + const data = await startSDLC(state.requirement); + dispatch({ type: "set-task-id", value: data.task_id }); + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "system", + source: "System", + message: `Task ID: ${data.task_id}`, + }, + }); + + dispatch({ type: "init-stages" }); + + // 开始轮询 + dispatch({ type: "set-connection", status: "connecting" }); + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "system", + source: "POLL", + message: `开始轮询任务:${data.task_id}`, + }, + }); + + lastIndexRef.current = 0; + pollCountRef.current = 0; + setTimeout(() => poll(data.task_id), 500); + } catch (err) { + dispatch({ + type: "add-log", + log: { + timestamp: formatTime(Date.now()), + event: "error", + source: "Error", + message: (err as Error).message, + }, + }); + dispatch({ type: "set-processing", value: false }); + alert(`启动失败:${(err as Error).message}`); + } + }, [canStart, state.requirement, poll]); + + const handleCopy = useCallback(async (content: string) => { + const success = await copyToClipboard(content); + if (success) { + dispatch({ type: "show-copy-toast", show: true }); + setTimeout(() => { + dispatch({ type: "show-copy-toast", show: false }); + }, 2000); + } else { + alert("复制失败,请手动复制"); + } + }, []); + + // 清理轮询定时器 + useEffect(() => { + return () => { + if (pollTimeoutRef.current) { + clearTimeout(pollTimeoutRef.current); + } + }; + }, []); + + return ( +
+ {/* Header */} +
+
+
+
+
+ + + +
+
+

SDLC Agent Demo

+

多智能体端到端软件交付协同系统

+
+
+
+
+ + {connectionStatusText} +
+
+
+
+
+ + {/* Main Content */} +
+ {/* Requirement Input */} +
+

1. 输入软件需求

+
+ +
+

+ 当前任务 ID: {{ taskId || '无' }} +

+ +
+
+
+ + +
+

2. 执行进度

+
+
+
+
+ {{ index + 1 }} +
+
+

{{ stage.name }}

+

{{ stage.agent }}

+
+
+
+ + {{ getStageStatusText(stage.status) }} + +
+
+
+
+ + +
+
+

3. 实时日志

+ +
+
+
+ {{ log.timestamp }} + [{{ log.event }}] + {{ log.message }} +
+
+
+ + +
+
+

4. 输出结果

+ +
+ +
+
+
+
+ + + +

{{ result.title }}

+
+
+ {{ formatDate(result.timestamp) }} + +
+
+
+ +
+
+
+
+
+
+ + +
+
+

+ 基于 CrewAI + Qwen3.5-flash + FastAPI(SSE) 构建 | Bosch Demo +

+
+
+ + +
+ ✓ 已复制到剪贴板 +
+
+ + + + diff --git a/vite.config.ts b/vite.config.ts index d9fde78..bd6940f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -23,6 +23,11 @@ export default defineConfig(({ mode }) => { changeOrigin: true, rewrite: (path) => path.replace(/^\/quality-api/, "/api"), }, + "/task-api": { + target: env.VITE_QUALITY_API_BASE || "http://localhost:8080", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/task-api/, "/api"), + }, }, }, };