Compare commits
3 Commits
eeb5e728ff
...
zhujw
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
679d6947e7 | ||
|
|
250192f699 | ||
|
|
cef1ae0414 |
@@ -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() {
|
||||
<Route path="/quality/dashboard" element={<QualityGate view="dashboard" />} />
|
||||
<Route path="/quality/pr-list" element={<QualityGate view="pr-list" />} />
|
||||
<Route path="/quality/settings" element={<QualityGate view="settings" />} />
|
||||
<Route path="/task" element={<TaskExecutor />} />
|
||||
<Route path="*" element={<Navigate to="/planning" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -9,6 +9,7 @@ const PAGE_TITLES: Record<string, string> = {
|
||||
"/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() {
|
||||
<i className="fa-solid fa-shield-halved w-4 text-center shrink-0"></i>
|
||||
<span>质量门控</span>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/task"
|
||||
className={({ isActive }) =>
|
||||
`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"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<i className="fa-solid fa-list-check w-4 text-center shrink-0"></i>
|
||||
<span>任务执行器</span>
|
||||
</NavLink>
|
||||
</nav>
|
||||
|
||||
<div className="text-[10px] font-bold text-txt-muted uppercase tracking-widest mt-8 mb-3 px-1">设置与支持</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background:
|
||||
radial-gradient(circle at 20% -10%, #eef2ff 0%, transparent 35%),
|
||||
radial-gradient(circle at 20% -10%, #fff1f2 0%, transparent 35%),
|
||||
radial-gradient(circle at 90% 0%, #f5f5f5 0%, transparent 28%),
|
||||
#fff;
|
||||
color: #1a1a1a;
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function Home() {
|
||||
<div className="p-[60px] w-full min-h-[calc(100vh-3.5rem)] flex flex-col">
|
||||
|
||||
{/* 欢迎横幅 */}
|
||||
<div className="mb-8 flex items-center justify-between overflow-hidden relative rounded-2xl border border-magenta/15 bg-gradient-to-br from-magenta/[0.06] via-white to-white shadow-[0_4px_16px_rgba(99,102,241,0.06)] p-6">
|
||||
<div className="mb-8 flex items-center justify-between overflow-hidden relative rounded-2xl border border-magenta/15 bg-gradient-to-br from-magenta/[0.06] via-white to-white shadow-[0_4px_16px_rgba(244,63,94,0.06)] p-6">
|
||||
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-magenta via-magenta/60 to-transparent"></div>
|
||||
<div className="pt-1">
|
||||
<h2 className="text-2xl font-extrabold text-txt mb-1.5">欢迎回来,SAFe OS 团队 👋</h2>
|
||||
@@ -163,7 +163,7 @@ export default function Home() {
|
||||
|
||||
{/* 悬浮 AI 问答按钮 */}
|
||||
<button
|
||||
className="fixed bottom-8 right-8 w-14 h-14 bg-magenta text-white rounded-2xl shadow-[0_8px_24px_rgba(99,102,241,0.35)] hover:shadow-[0_12px_32px_rgba(99,102,241,0.45)] hover:-translate-y-1 active:translate-y-0 transition-all duration-200 flex items-center justify-center text-xl z-50"
|
||||
className="fixed bottom-8 right-8 w-14 h-14 bg-magenta text-white rounded-2xl shadow-[0_8px_24px_rgba(244,63,94,0.35)] hover:shadow-[0_12px_32px_rgba(244,63,94,0.45)] hover:-translate-y-1 active:translate-y-0 transition-all duration-200 flex items-center justify-center text-xl z-50"
|
||||
title="AI 助手"
|
||||
>
|
||||
<i className="fa-solid fa-robot"></i>
|
||||
|
||||
740
src/pages/TaskExecutor.tsx
Normal file
740
src/pages/TaskExecutor.tsx
Normal file
@@ -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<PollResponse> {
|
||||
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<NodeJS.Timeout | null>(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<string, string> = {
|
||||
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 (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 py-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-8 w-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h1 className="text-xl font-bold text-gray-900">SDLC Agent Demo</h1>
|
||||
<p className="text-sm text-gray-500">多智能体端到端软件交付协同系统</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center text-sm">
|
||||
<span className={`inline-block w-2 h-2 rounded-full mr-2 ${connectionStatusClass}`}></span>
|
||||
<span className="text-gray-600">{connectionStatusText}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="max-w-7xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
|
||||
{/* Requirement Input */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">1. 输入软件需求</h2>
|
||||
<div className="space-y-4">
|
||||
<textarea
|
||||
value={state.requirement}
|
||||
onChange={(e) => dispatch({ type: "set-requirement", value: e.target.value })}
|
||||
rows={5}
|
||||
placeholder={`请输入您的软件需求描述,例如:\n开发一个用户管理系统,支持用户的增删改查功能,需要包含以下特性:\n- 用户注册和登录\n- 用户信息管理\n- 角色权限控制\n- 操作日志记录`}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||
disabled={state.isProcessing}
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-500">
|
||||
当前任务 ID: {state.taskId || "无"}
|
||||
</p>
|
||||
<button
|
||||
onClick={handleStart}
|
||||
disabled={!canStart}
|
||||
className={`px-6 py-2.5 rounded-lg font-medium text-white transition-all duration-200 ${
|
||||
canStart
|
||||
? "bg-blue-600 hover:bg-blue-700 shadow-md hover:shadow-lg"
|
||||
: "bg-gray-400 cursor-not-allowed"
|
||||
}`}
|
||||
>
|
||||
{state.isProcessing ? "执行中..." : "开始执行"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Stages */}
|
||||
{state.stages.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">2. 执行进度</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{state.stages.map((stage, index) => (
|
||||
<div
|
||||
key={stage.id}
|
||||
className={`rounded-lg border-2 p-4 bg-white transition-all duration-300 ${
|
||||
stage.status === "processing"
|
||||
? "border-blue-500 transform scale-[1.02] shadow-[0_10px_25px_rgba(59,130,246,0.3)] animate-pulse"
|
||||
: stage.status === "completed"
|
||||
? "border-green-500 bg-gradient-to-br from-green-50 to-white"
|
||||
: "border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center mb-2">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center mr-3 ${getStageIconClass(stage.status)}`}
|
||||
>
|
||||
<span className="text-white font-bold text-sm">{index + 1}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">{stage.name}</h3>
|
||||
<p className="text-xs text-gray-500">{stage.agent}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStageBadgeClass(stage.status)}`}
|
||||
>
|
||||
{getStageStatusText(stage.status)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Real-time Logs */}
|
||||
{state.logs.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900">3. 实时日志</h2>
|
||||
<button
|
||||
onClick={() => dispatch({ type: "clear-logs" })}
|
||||
className="text-sm text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
清空日志
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded-lg p-4 h-64 overflow-y-auto font-mono text-sm">
|
||||
{state.logs.map((log, index) => (
|
||||
<div key={index} className="mb-1">
|
||||
<span className="text-gray-500">{log.timestamp}</span>
|
||||
<span className={`ml-2 ${getLogLevelClass(log.event)}`}>[{log.event}]</span>
|
||||
<span className="text-gray-300 ml-2">{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results */}
|
||||
{state.results.length > 0 && (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">4. 输出结果</h2>
|
||||
|
||||
{state.results.map((result, index) => (
|
||||
<div key={index} className="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div
|
||||
onClick={() => dispatch({ type: "toggle-result", index })}
|
||||
className="px-6 py-4 bg-gray-50 border-b border-gray-200 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className={`w-5 h-5 mr-2 text-gray-500 transform transition-transform ${
|
||||
result.expanded ? "rotate-90" : ""
|
||||
}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<h3 className="font-semibold text-gray-900">{result.title}</h3>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-gray-500">{formatDate(result.timestamp)}</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopy(result.content);
|
||||
}}
|
||||
className="text-sm text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{result.expanded && (
|
||||
<div className="p-6">
|
||||
<div className="markdown-body prose prose-sm max-w-none">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
|
||||
{result.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-white border-t border-gray-200 mt-12">
|
||||
<div className="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
基于 CrewAI + Qwen3.5-flash + FastAPI(Polling) 构建 | Bosch Demo
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* Copy Toast */}
|
||||
{state.showCopyToast && (
|
||||
<div className="fixed bottom-4 right-4 bg-green-600 text-white px-6 py-3 rounded-lg shadow-lg transition-opacity duration-300">
|
||||
✓ 已复制到剪贴板
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,11 +5,11 @@ export default {
|
||||
extend: {
|
||||
colors: {
|
||||
magenta: {
|
||||
DEFAULT: "#6366F1",
|
||||
50: "#EEF2FF",
|
||||
100: "#E0E7FF",
|
||||
600: "#6366F1",
|
||||
700: "#4F46E5",
|
||||
DEFAULT: "#F43F5E",
|
||||
50: "#FFF1F2",
|
||||
100: "#FFE4E6",
|
||||
600: "#F43F5E",
|
||||
700: "#BE123C",
|
||||
},
|
||||
surface: {
|
||||
DEFAULT: "#FFFFFF",
|
||||
|
||||
685
template/task.html
Normal file
685
template/task.html
Normal file
@@ -0,0 +1,685 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SDLC Agent Demo - 多智能体软件交付协同系统</title>
|
||||
|
||||
<!-- TailwindCSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Vue 3 -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
|
||||
<!-- Highlight.js 代码高亮 - 使用浅色主题 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
|
||||
<!-- Marked Markdown 解析 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
|
||||
<style>
|
||||
/* 自定义样式 */
|
||||
.stage-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stage-card.active {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.stage-card.completed {
|
||||
border-color: #10B981;
|
||||
background: linear-gradient(135deg, #ECFDF5 0%, #FFFFFF 100%);
|
||||
}
|
||||
|
||||
.stage-card.processing {
|
||||
border-color: #3B82F6;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); }
|
||||
50% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); }
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
background: #f6f8fa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #e1e4e8;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body ul, .markdown-body ol {
|
||||
padding-left: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* SSE 连接状态指示器 */
|
||||
.connection-status {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.connection-connected {
|
||||
background-color: #10B981;
|
||||
}
|
||||
|
||||
.connection-disconnected {
|
||||
background-color: #EF4444;
|
||||
}
|
||||
|
||||
.connection-connecting {
|
||||
background-color: #F59E0B;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<div id="app" class="min-h-screen">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="bg-white shadow-sm border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 py-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-8 w-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h1 class="text-xl font-bold text-gray-900">SDLC Agent Demo</h1>
|
||||
<p class="text-sm text-gray-500">多智能体端到端软件交付协同系统</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center text-sm">
|
||||
<span :class="['connection-status', connectionStatusClass]"></span>
|
||||
<span class="text-gray-600">{{ connectionStatusText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="max-w-7xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
|
||||
<!-- 需求输入区 -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">1. 输入软件需求</h2>
|
||||
<div class="space-y-4">
|
||||
<textarea
|
||||
v-model="requirement"
|
||||
rows="5"
|
||||
placeholder="请输入您的软件需求描述,例如: 开发一个用户管理系统,支持用户的增删改查功能,需要包含以下特性: - 用户注册和登录 - 用户信息管理 - 角色权限控制 - 操作日志记录"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||
:disabled="isProcessing"
|
||||
></textarea>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-gray-500">
|
||||
当前任务 ID: {{ taskId || '无' }}
|
||||
</p>
|
||||
<button
|
||||
@click="startSDLCProcess"
|
||||
:disabled="!canStart || isProcessing"
|
||||
:class="[
|
||||
'px-6 py-2.5 rounded-lg font-medium text-white transition-all duration-200',
|
||||
canStart && !isProcessing
|
||||
? 'bg-blue-600 hover:bg-blue-700 shadow-md hover:shadow-lg'
|
||||
: 'bg-gray-400 cursor-not-allowed'
|
||||
]"
|
||||
>
|
||||
{{ isProcessing ? '执行中...' : '开始执行' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度展示区 -->
|
||||
<div class="mb-6" v-show="stages.length > 0">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">2. 执行进度</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div
|
||||
v-for="(stage, index) in stages"
|
||||
:key="stage.id"
|
||||
:class="[
|
||||
'stage-card rounded-lg border-2 p-4 bg-white',
|
||||
{ 'active': stage.status === 'processing' },
|
||||
{ 'completed': stage.status === 'completed' },
|
||||
{ 'processing': stage.status === 'processing' }
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center mb-2">
|
||||
<div
|
||||
:class="[
|
||||
'w-8 h-8 rounded-full flex items-center justify-center mr-3',
|
||||
getStageIconClass(stage.status)
|
||||
]"
|
||||
>
|
||||
<span class="text-white font-bold text-sm">{{ index + 1 }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-gray-900">{{ stage.name }}</h3>
|
||||
<p class="text-xs text-gray-500">{{ stage.agent }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<span :class="[
|
||||
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
|
||||
getStageBadgeClass(stage.status)
|
||||
]">
|
||||
{{ getStageStatusText(stage.status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 实时日志区 -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6" v-show="logs.length > 0">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900">3. 实时日志</h2>
|
||||
<button
|
||||
@click="clearLogs"
|
||||
class="text-sm text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
清空日志
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-gray-900 rounded-lg p-4 h-64 overflow-y-auto font-mono text-sm">
|
||||
<div v-for="(log, index) in logs" :key="index" class="mb-1">
|
||||
<span class="text-gray-500">{{ log.timestamp }}</span>
|
||||
<span :class="getLogLevelClass(log.event)" class="ml-2">[{{ log.event }}]</span>
|
||||
<span class="text-gray-300 ml-2">{{ log.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果展示区 -->
|
||||
<div class="space-y-6" v-show="results.length > 0">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900">4. 输出结果</h2>
|
||||
<button
|
||||
@click="downloadResults"
|
||||
:disabled="!isCompleted"
|
||||
:class="[
|
||||
'px-4 py-2 rounded-lg font-medium text-white transition-all duration-200 flex items-center',
|
||||
isCompleted
|
||||
? 'bg-green-600 hover:bg-green-700 shadow-md'
|
||||
: 'bg-gray-400 cursor-not-allowed'
|
||||
]"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
||||
</svg>
|
||||
{{ isCompleted ? '打包下载结果' : '执行完成后下载' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-for="(result, index) in results" :key="index" class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div
|
||||
@click="result.expanded = !result.expanded"
|
||||
class="px-6 py-4 bg-gray-50 border-b border-gray-200 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
:class="['w-5 h-5 mr-2 text-gray-500 transform transition-transform', result.expanded ? 'rotate-90' : '']"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<h3 class="font-semibold text-gray-900">{{ result.title }}</h3>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs text-gray-500">{{ formatDate(result.timestamp) }}</span>
|
||||
<button
|
||||
@click.stop="copyToClipboard(result.content)"
|
||||
class="text-sm text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="result.expanded" class="p-6">
|
||||
<div class="markdown-body" v-html="renderMarkdown(result.content)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="bg-white border-t border-gray-200 mt-12">
|
||||
<div class="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
|
||||
<p class="text-center text-sm text-gray-500">
|
||||
基于 CrewAI + Qwen3.5-flash + FastAPI(SSE) 构建 | Bosch Demo
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- 复制成功提示 -->
|
||||
<div v-if="showCopyToast" class="fixed bottom-4 right-4 bg-green-600 text-white px-6 py-3 rounded-lg shadow-lg transition-opacity duration-300">
|
||||
✓ 已复制到剪贴板
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp } = Vue;
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
// 需求输入
|
||||
requirement: '',
|
||||
|
||||
// 任务管理
|
||||
taskId: null,
|
||||
isProcessing: false,
|
||||
|
||||
// SSE 连接
|
||||
eventSource: null,
|
||||
connectionStatus: 'disconnected', // 'connecting', 'connected', 'disconnected'
|
||||
|
||||
// 阶段定义
|
||||
stages: [],
|
||||
|
||||
// 实时日志
|
||||
logs: [],
|
||||
|
||||
// 结果数据
|
||||
results: [],
|
||||
|
||||
// UI 状态
|
||||
showCopyToast: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
canStart() {
|
||||
return this.requirement.trim().length >= 10 && !this.isProcessing;
|
||||
},
|
||||
|
||||
connectionStatusClass() {
|
||||
const statusMap = {
|
||||
'connecting': 'connection-connecting',
|
||||
'connected': 'connection-connected',
|
||||
'disconnected': 'connection-disconnected'
|
||||
};
|
||||
return statusMap[this.connectionStatus];
|
||||
},
|
||||
|
||||
connectionStatusText() {
|
||||
const textMap = {
|
||||
'connecting': '连接中...',
|
||||
'connected': '已连接',
|
||||
'disconnected': '未连接'
|
||||
};
|
||||
return textMap[this.connectionStatus];
|
||||
},
|
||||
|
||||
isCompleted() {
|
||||
return this.stages.length > 0 &&
|
||||
this.stages.every(s => s.status === 'completed');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* 下载打包结果
|
||||
*/
|
||||
downloadResults() {
|
||||
if (!this.taskId || !this.isCompleted) return;
|
||||
|
||||
const url = `/api/v1/sdlc/download/${this.taskId}`;
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
|
||||
/**
|
||||
* 启动 SDLC 流程
|
||||
*/
|
||||
async startSDLCProcess() {
|
||||
if (!this.canStart) return;
|
||||
|
||||
try {
|
||||
this.isProcessing = true;
|
||||
this.stages = [];
|
||||
this.logs = [];
|
||||
this.results = [];
|
||||
|
||||
// 调用 API 启动任务
|
||||
const response = await fetch('/api/v1/sdlc/start', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
requirement: this.requirement
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.taskId = data.task_id;
|
||||
this.addLog('system', '任务已启动', `Task ID: ${data.task_id}`);
|
||||
|
||||
// 初始化阶段
|
||||
this.initStages();
|
||||
|
||||
// 开始轮询任务事件
|
||||
this.connectPolling(data.task_id);
|
||||
|
||||
} catch (error) {
|
||||
console.error('启动失败:', error);
|
||||
this.addLog('error', '启动失败', error.message);
|
||||
this.isProcessing = false;
|
||||
alert(`启动失败:${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化阶段
|
||||
*/
|
||||
initStages() {
|
||||
this.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' }
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* 轮询任务事件(替代 SSE)
|
||||
*/
|
||||
connectPolling(taskId) {
|
||||
this.connectionStatus = 'connecting';
|
||||
this.addLog('system', 'POLL', `开始轮询任务:${taskId}`);
|
||||
|
||||
let lastIndex = 0;
|
||||
let pollCount = 0;
|
||||
const maxPolls = 600; // 最多轮询 600 次 (10 分钟)
|
||||
|
||||
const poll = () => {
|
||||
if (pollCount >= maxPolls) {
|
||||
this.addLog('system', 'POLL', '轮询超时');
|
||||
this.isProcessing = false;
|
||||
this.connectionStatus = 'disconnected';
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/v1/sdlc/poll/${taskId}?last_index=${lastIndex}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const { events, has_more, status } = data;
|
||||
|
||||
// 处理新事件
|
||||
events.forEach(event => {
|
||||
lastIndex++;
|
||||
this.handleEvent(event);
|
||||
});
|
||||
|
||||
// 检查是否继续轮询
|
||||
if (status === 'completed' || status === 'failed') {
|
||||
this.isProcessing = false;
|
||||
this.connectionStatus = 'disconnected';
|
||||
this.addLog('system', 'POLL', `任务完成,状态:${status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (has_more || events.length > 0) {
|
||||
pollCount++;
|
||||
setTimeout(poll, 500); // 每 500ms 轮询一次
|
||||
} else if (status === 'processing') {
|
||||
pollCount++;
|
||||
setTimeout(poll, 1000); // 无新事件时 1 秒后再试
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('轮询失败:', err);
|
||||
this.addLog('error', 'POLL', err.message);
|
||||
pollCount++;
|
||||
setTimeout(poll, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
// 开始轮询
|
||||
setTimeout(poll, 500);
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理单个事件
|
||||
*/
|
||||
handleEvent(event) {
|
||||
const eventType = event.event;
|
||||
const data = event.data;
|
||||
|
||||
switch(eventType) {
|
||||
case 'task_started':
|
||||
this.addLog('task_started', 'System', data.message || '任务已启动');
|
||||
break;
|
||||
|
||||
case 'pm_start':
|
||||
this.updateStageStatus('pm', 'processing');
|
||||
this.addLog('pm_start', 'PM Agent', '开始需求分析...');
|
||||
break;
|
||||
|
||||
case 'pm_complete':
|
||||
this.updateStageStatus('pm', 'completed');
|
||||
this.addLog('pm_complete', 'PM Agent', '需求分析完成');
|
||||
this.addResult('📋 软件需求规格说明书 (SRS)', data.content, data.timestamp);
|
||||
break;
|
||||
|
||||
case 'qa_start':
|
||||
this.updateStageStatus('qa', 'processing');
|
||||
this.addLog('qa_start', 'QA Agent', '开始测试用例设计...');
|
||||
break;
|
||||
|
||||
case 'qa_complete':
|
||||
this.updateStageStatus('qa', 'completed');
|
||||
this.addLog('qa_complete', 'QA Agent', '测试用例设计完成');
|
||||
this.addResult('🧪 测试方案与用例', data.content, data.timestamp);
|
||||
break;
|
||||
|
||||
case 'dev_start':
|
||||
this.updateStageStatus('dev', 'processing');
|
||||
this.addLog('dev_start', 'Dev Agent', '开始代码实现...');
|
||||
break;
|
||||
|
||||
case 'dev_complete':
|
||||
this.updateStageStatus('dev', 'completed');
|
||||
this.addLog('dev_complete', 'Dev Agent', '代码实现完成');
|
||||
this.addResult('💻 代码实现', data.content, data.timestamp);
|
||||
break;
|
||||
|
||||
case 'final_result':
|
||||
this.updateStageStatus('final', 'completed');
|
||||
this.addLog('final_result', 'System', 'SDLC 流程完成');
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
this.addLog('error', 'Error', data.error || '未知错误');
|
||||
alert(`执行错误:${data.error}`);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新阶段状态
|
||||
*/
|
||||
updateStageStatus(stageId, status) {
|
||||
const stage = this.stages.find(s => s.id === stageId);
|
||||
if (stage) {
|
||||
stage.status = status;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加日志
|
||||
*/
|
||||
addLog(event, source, message) {
|
||||
this.logs.push({
|
||||
timestamp: new Date().toLocaleTimeString('zh-CN'),
|
||||
event,
|
||||
source,
|
||||
message
|
||||
});
|
||||
// 保持最新 100 条日志
|
||||
if (this.logs.length > 100) {
|
||||
this.logs.shift();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加结果
|
||||
*/
|
||||
addResult(title, content, timestamp) {
|
||||
this.results.push({
|
||||
title,
|
||||
content,
|
||||
timestamp,
|
||||
expanded: true
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空日志
|
||||
*/
|
||||
clearLogs() {
|
||||
this.logs = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取阶段图标样式
|
||||
*/
|
||||
getStageIconClass(status) {
|
||||
const classMap = {
|
||||
'pending': 'bg-gray-400',
|
||||
'processing': 'bg-blue-500',
|
||||
'completed': 'bg-green-500'
|
||||
};
|
||||
return classMap[status] || classMap['pending'];
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取阶段徽章样式
|
||||
*/
|
||||
getStageBadgeClass(status) {
|
||||
const classMap = {
|
||||
'pending': 'bg-gray-100 text-gray-800',
|
||||
'processing': 'bg-blue-100 text-blue-800',
|
||||
'completed': 'bg-green-100 text-green-800'
|
||||
};
|
||||
return classMap[status] || classMap['pending'];
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取阶段状态文本
|
||||
*/
|
||||
getStageStatusText(status) {
|
||||
const textMap = {
|
||||
'pending': '等待中',
|
||||
'processing': '进行中',
|
||||
'completed': '已完成'
|
||||
};
|
||||
return textMap[status] || status;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取日志级别样式
|
||||
*/
|
||||
getLogLevelClass(event) {
|
||||
const classMap = {
|
||||
'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'
|
||||
};
|
||||
return classMap[event] || 'text-gray-400';
|
||||
},
|
||||
|
||||
/**
|
||||
* 渲染 Markdown
|
||||
*/
|
||||
renderMarkdown(content) {
|
||||
if (!content) return '';
|
||||
return marked.parse(content);
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
formatDate(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
try {
|
||||
return new Date(timestamp).toLocaleString('zh-CN');
|
||||
} catch {
|
||||
return timestamp;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 复制到剪贴板
|
||||
*/
|
||||
async copyToClipboard(content) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
this.showCopyToast = true;
|
||||
setTimeout(() => {
|
||||
this.showCopyToast = false;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err);
|
||||
alert('复制失败,请手动复制');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
// 清理 SSE 连接
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
}
|
||||
}
|
||||
}).mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user