1 Commits

10 changed files with 606 additions and 114 deletions

116
API_Gateway_Rules.md Normal file
View File

@@ -0,0 +1,116 @@
# Safe OS UI - API Gateway 路由转发规则文档
本系统包含多个独立的后端AI服务规划系统、开发平台、质量门禁等。为了使前端统一指向单一入口并简化跨域和请求分发配置建议使用 API Gateway 进行统一路由和负载均衡。
以下是当前前端所有与后端服务交互的接口清单以及转发规则。Gateway 服务需要解析相应的路径前缀Ingress Prefix将请求 Rewrite 后转发至对应的内部后端服务。
---
## 整体架构与入口规则
- **统一入口 URL**`http://<api-gateway-host>:<port>` (前端将所有 API 请求指向此地址)
- **核心路由匹配策略**:基于 URL前缀 进行正则匹配或前缀匹配。
| 业务模块 | Ingress Prefix (外部路径) | Forward Target (内部服务地址) | Rewrite Rule (路径重写规则) |
| --- | --- | --- | --- |
| **Planning System** | `/planning-api/*` | `http://localhost:8090` | `/planning-api/(.*)``/api/$1` |
| **DevOps System** | `/devops-api/*` | `http://localhost:8000` | `/devops-api/(.*)``/$1` |
| **Quality Gate System** | `/quality-api/*` | `http://localhost:5000` | `/quality-api/(.*)``/api/$1` |
---
## 1. 规划子系统接口 (Planning System)
**底层服务**: `http://localhost:8090`
前缀重写规则:请求至统一入口 `.../planning-api/X` 会被转发至内部的 `.../api/X`
| 方法 | 外部暴露路径 (Gateway URL) | 内部转发路径 (Target URL) | 接口描述 |
| --- | --- | --- | --- |
| `POST` | `/planning-api/chat/stream` | `/api/chat/stream` | 铁三角 Agent发送大系统规划/史诗业务需求内容SSE流式数据返回 |
| `POST` | `/planning-api/upload` | `/api/upload` | 文档上传:支持用户上传附件作为系统知识或输入来源 |
---
## 2. 研发自动引擎接口 (DevOps System)
**底层服务**: `http://localhost:8000`
前缀重写规则:请求至统一入口 `.../devops-api/X` 会被去掉前缀,转为后端的 `.../X`。该系统重度依赖**SSE (Server-Sent Events) 流式连接**请在Gateway配置中确保不会缓冲或阻断流数据。
| 方法 | 外部暴露路径 (Gateway URL) | 内部转发路径 (Target URL) | 接口描述 |
| --- | --- | --- | --- |
| `POST` | `/devops-api/session/start` | `/session/start` | 新建研发自动化任务会话 (Session) |
| `POST` | `/devops-api/session/{sessionId}/clarify` | `/session/{sessionId}/clarify` | 基于需求说明进行需求澄清和补充 |
| `GET` | `/devops-api/session/{sessionId}/pm/stream` | `/session/{sessionId}/pm/stream` | PM角色执行需求细化分析流程 (SSE流) |
| `GET` | `/devops-api/session/{sessionId}/pm/refine/stream` | `/session/{sessionId}/pm/refine/stream` | 追加PM反馈继续流式提炼需求 (SSE流) |
| `GET` | `/devops-api/session/{sessionId}/qa/stream` | `/session/{sessionId}/qa/stream` | QA角色基于需求生成测试用例并执行评估 (SSE流) |
| `GET` | `/devops-api/session/{sessionId}/qa/refine/stream` | `/session/{sessionId}/qa/refine/stream` | 追加QA用例反馈并更新测试集 (SSE流) |
| `GET` | `/devops-api/session/{sessionId}/dev/stream` | `/session/{sessionId}/dev/stream` | 核心编码过程:依据需求、架构与测试生成最终业务/Java等工程代码 (SSE流) |
| `POST` | `/devops-api/session/{sessionId}/test/run` | `/session/{sessionId}/test/run` | 运行集成编译与单元/端到端测试并获取结果 |
| `GET` | `/devops-api/session/{sessionId}/test/fix/stream` | `/session/{sessionId}/test/fix/stream` | 针对测试错误进行的AI代码自动修复 (SSE流) |
*注意: `{sessionId}` 属于路径层级中的动态参数,具体网关转发时使用泛类型或通配符放行。*
---
## 3. 代码质量门禁接口 (Quality Gate)
**底层服务**: `http://localhost:5000`
前缀重写规则:请求至统一入口 `.../quality-api/X` 会被转发至后端内部的 `.../api/X`
| 方法 | 外部暴露路径 (Gateway URL) | 内部转发路径 (Target URL) | 接口描述 |
| --- | --- | --- | --- |
| `GET` | `/quality-api/prs` | `/api/prs` | 获取 PR 扫描工单列表(支持多种查询参数) |
| `GET` | `/quality-api/prs/history` | `/api/prs/history` | 获取 PR 处理历史,用于趋势看板 (e.g., `?limit=15`) |
| `GET` | `/quality-api/prs/{prId}` | `/api/prs/{prId}` | 获取某一条指定 PR 扫描记录的详细执行状态及摘要 |
| `GET` | `/quality-api/prs/{prId}/files` | `/api/prs/{prId}/files` | 拉取这条 PR 的被影响/改动的文件目录结构 |
| `GET` | `/quality-api/prs/{prId}/file` | `/api/prs/{prId}/file` | 获取PR里指定文件的 Diff 对象(包含行内审查反馈),附带 `?path=xxx` 查询参数 |
| `POST`| `/quality-api/prs/{prId}/merge` | `/api/prs/{prId}/merge` | 在门禁界面确认问题修缮无误后,合并该次 PR |
| `POST`| `/quality-api/prs/{prId}/close` | `/api/prs/{prId}/close` | 门禁审查不通过,拒绝/关闭此条扫描和合并申请 |
---
## Nginx Gateway 配置示例 (参考)
如果网关选用 Nginx可快速参考如下配置来完成上述 Rewrite 与 Proxy
```nginx
server {
listen 80;
server_name api-gateway.safe-os.local;
# 1. Planning API 转发
location /planning-api/ {
# 截取 /planning-api/ 后的内容,拼接到 /api/
rewrite ^/planning-api/(.*)$ /api/$1 break;
proxy_pass http://localhost:8090;
# 启用流连接所需的头信息
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
}
# 2. DevOps API 转发
location /devops-api/ {
# 截取 /devops-api/ 后的内容,直接拼接
rewrite ^/devops-api/(.*)$ /$1 break;
proxy_pass http://localhost:8000;
# 支撑 SSE (Server-Sent Events) 的流配置
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s; # 考虑大模型生成代码时间较长
}
# 3. Quality Gate API 转发
location /quality-api/ {
# 截取 /quality-api/ 后的内容,拼接到 /api/
rewrite ^/quality-api/(.*)$ /api/$1 break;
proxy_pass http://localhost:5000;
}
}
```
以上文档即针对前端当前服务现状所整理的 API Gateway 路由规划规范,您可以直接根据此规范开发 API Gateway 或调整 Nginx 等负载均衡器的配置。

View File

@@ -0,0 +1,140 @@
# Vibe Coding Design Docs: Workspace/Artifact Split-Screen Pattern
## 1. Context & Objective
The goal is to implement a UI/UX pattern similar to **Claude Artifacts** or **Gemini Deep Research**. When a specific complex task is triggered (e.g., "Project Planning Skill"), the single-column chat interface should smoothly transition into a split-screen layout:
- **Left Panel (35%)**: Conversational context, CoT (Chain of Thought) traces, tool calls, and user input.
- **Right Panel (65%)**: A dedicated "Workspace" or "Artifact" rendering area to display long-form content (Markdown, code, diagrams) generated by the Agent's skills.
Crucially, this system must support **Reflexion/Iterative generation**. The user can comment on the generated artifact in the left panel, and the agent should update the artifact in the right panel based on the feedback.
---
## 2. Frontend Implementation Guide (React + Vite + Tailwind)
### 2.1 State Management (State & Types)
Extend the existing frontend state to track the workspace status and content.
```typescript
// 1. Extend the StreamEvent type to support UI control and artifact streaming
type StreamEvent = {
type:
| "thought"
| "tool_call"
| "tool_result"
| "message" // Standard chat message
| "error"
| "workspace_start" // Trigger right panel open
| "workspace_delta" // Streaming text for the right panel
| "workspace_end"; // Streaming completed
content: string;
step?: number;
tool_name?: string;
workspace_title?: string; // Optional title for the artifact
};
// 2. Add Workspace State (Can be added to useReducer or a separate useState)
type WorkspaceState = {
isOpen: boolean;
title: string;
content: string;
isGenerating: boolean;
};
```
### 2.2 SSE Parsing Logic
Modify the `onEvent` handler inside `streamChat` to intercept `workspace_*` events.
- When `workspace_start` arrives: Set `workspace.isOpen = true`, clear previous content, set `isGenerating = true`.
- When `workspace_delta` arrives: Append text to `workspace.content`. Do **not** append this text to the left-panel chat history to avoid redundancy.
- When `workspace_end` arrives: Set `isGenerating = false`.
### 2.3 Layout & UI Re-architecture
Refactor the root `<div>` of `PlanningAgent.tsx` to handle dynamic flex layouts. Use Tailwind's transition utilities for smooth scaling.
```tsx
<div className="flex h-full w-full overflow-hidden bg-surface">
{/* Left Panel: Chat & Controls */}
<div
className={`flex flex-col h-full transition-all duration-300 ease-in-out ${
workspace.isOpen ? 'w-[35%] border-r border-border min-w-[350px]' : 'w-full max-w-5xl mx-auto'
}`}
>
{/* Existing Message List & Input Area */}
</div>
{/* Right Panel: Workspace / Deep Research Output */}
{workspace.isOpen && (
<div className="w-[65%] h-full flex flex-col bg-surface-muted transition-opacity duration-300 animate-fade-in">
{/* Header */}
<div className="h-14 border-b border-border flex items-center px-6 justify-between bg-white">
<h3 className="font-semibold text-txt flex items-center gap-2">
<IconDocument /> {workspace.title || 'Project Planning Document'}
</h3>
{workspace.isGenerating && (
<span className="text-sm text-magenta animate-pulse flex items-center gap-1">
<Spinner /> Generating...
</span>
)}
</div>
{/* Markdown Content Area */}
<div className="flex-1 overflow-y-auto p-8 prose prose-slate max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
{workspace.content}
</ReactMarkdown>
</div>
</div>
)}
</div>
```
---
## 3. Backend Implementation Guide (Agent / ReAct Loop)
The backend agent requires structural changes to understand the "Artifact" concept, emit correct SSE events, and maintain the artifact in its memory for iterative edits.
### 3.1 Tool / Skill Definition
When defining the `Project Planning Skill` for the LLM, clearly state its output behavior so the LLM knows *when* to use it.
- **Tool Description**: `use_planning_workspace`: "Invoke this tool to generate, structure, or update a major project planning document. The output will be rendered in a dedicated UI workspace."
### 3.2 Context Injection (Memory for Reflexion)
To allow the user to say *"extend the testing phase to 2 weeks"*, the LLM **must know what is currently in the right panel**.
- **Before sending the prompt to the LLM**, query the database/session for the current Artifact state.
- **Prompt Assembly**:
```text
[System Prompt / ReAct Instructions]
...
[Current Workspace Artifact (if exists)]
<workspace>
# Project Plan
1. Dev Phase: 1 week
2. Testing Phase: 1 week
</workspace>
[Chat History]
User: extend the testing phase to 2 weeks.
```
### 3.3 Streaming Control (Hijacking the Stream)
Within the ReAct execution loop, when the Agent decides to execute the `Project Planning Skill`:
1. The Backend normally streams `thought` or `tool_call` events.
2. Upon entering the specific Skill execution, the backend emits `{"type": "workspace_start", "workspace_title": "Update: Project Plan"}`.
3. As the LLM (or a sub-agent) generates the Markdown schema, the backend maps these tokens to `workspace_delta` events and flushes them to the frontend.
4. (CRITICAL) Do **not** send these tokens as `message` or `final` chat events. The chat bubble should only say something like: *"I have updated the project plan in the workspace area."*
5. Save the final generated Markdown text into the session memory as the `Current Artifact` for future context injection.
---
## 4. Work Flow Summary (For LLM context generation)
1. `User` sends prompt: "Plan the new feature".
2. `Agent` thinks (`type: thought`), decides to use `Project Planning Skill` (`type: tool_call`).
3. `Agent` emits `{"type": "workspace_start"}`.
4. `Frontend` expands right panel (65% width).
5. `Agent` streams `{"type": "workspace_delta", "content": "..."}`.
6. `Frontend` live-renders Markdown in the right panel.
7. `Agent` finishes, saves artifact to backend session.
8. `User` reads right panel, types in left panel: "Change point 2".
9. `Agent` receives Left Panel history + Right Panel Artifact Content.
10. `Agent` updates document, streaming new `workspace_delta`. Frontend live-updates the right panel.

View File

@@ -0,0 +1,197 @@
# WebUI `/api/chat/stream` 前端对接说明
本文档用于指导前端项目接入 LaodingBot 的流式聊天接口,并可直接作为提示词输入给 LLM批量改造其他前端代码。
## 1. 接口总览
- 方法: `POST`
- 路径: `/api/chat/stream`
- 请求头: `Content-Type: application/json`
- 响应类型: `text/event-stream`
- 协议: SSE (Server-Sent Events)
说明: 该接口为单次请求、多次推送。后端会持续推送 `data: <json>\n\n` 格式的事件。
## 2. 请求体
```json
{
"text": "请帮我分析当前目录",
"session_id": "sess_abc",
"user_id": "user_001"
}
```
字段说明:
- `text` (string, required): 用户输入文本,去除空白后不能为空。
- `session_id` (string, optional): 会话 ID不传时后端自动生成。
- `user_id` (string, optional): 用户 ID不传时后端自动生成。
兼容字段:
- `sessionId` 等价于 `session_id`
- `userId` 等价于 `user_id`
## 3. SSE 事件格式
每条 SSE 消息只包含 `data` 字段,内容是 JSON:
```text
data: {"type":"thought","content":"我先判断是否需要调用工具","step":1}
data: {"type":"tool_call","content":"{\"input\":\"pwd\"}","step":1,"tool_name":"shell"}
data: {"type":"tool_result","content":"C:/Project/MyProject","step":1,"tool_name":"shell"}
data: {"type":"final","content":"当前目录是 C:/Project/MyProject","step":2}
```
事件字段:
- `type` (string): 事件类型
- `content` (string): 事件文本内容
- `step` (number, optional): ReAct 步骤编号
- `tool_name` (string, optional): 工具名
事件类型:
- `thought`: LLM 思考片段(可选透传)
- `tool_call`: 工具调用请求(可选透传)
- `tool_result`: 工具执行结果(可选透传)
- `final`: 最终回答
- `error`: 错误信息
说明:
- 默认情况下(`WEBUI_EXPOSE_REASONING=false`WebUI 只向前端返回 `final``error`
- 当设置 `WEBUI_EXPOSE_REASONING=true`WebUI 会额外透传 `thought``tool_call``tool_result` 事件。
- 无论是否透传推理事件,`final` 都会分段累计推送,以便前端实现打字机效果。
## 4. 连接生命周期
- 正常结束: 收到 `type=final` 后结束渲染,连接可由浏览器自然关闭。
- 异常结束: 收到 `type=error`,前端应显示错误并结束当前轮次。
- 网络中断: 前端应允许用户重试,并保留已收到的事件记录。
## 5. 前端渲染建议
推荐前端仅处理两类结果:
- 答案区: 显示最后一个 `final`
- 错误区: 显示 `error`
如果开启了 `WEBUI_EXPOSE_REASONING=true`,建议额外提供“思考面板”:
- 思考区: 渲染 `thought`
- 工具区: 渲染 `tool_call` / `tool_result`
建议状态机:
- `idle`: 初始状态
- `streaming`: 请求中且持续接收事件
- `done`: 收到 `final`
- `failed`: 收到 `error` 或请求异常
## 6. TypeScript 对接示例 (fetch + ReadableStream)
```ts
type StreamEventType = 'thought' | 'tool_call' | 'tool_result' | 'final' | 'error';
interface StreamEvent {
type: StreamEventType;
content: string;
step?: number;
tool_name?: string;
}
export async function streamChat(
payload: { text: string; session_id?: string; user_id?: string },
onEvent: (event: StreamEvent) => void,
signal?: AbortSignal,
): Promise<void> {
const resp = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal,
});
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}`);
}
if (!resp.body) {
throw new Error('ReadableStream is not available');
}
const reader = resp.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// SSE message delimiter: blank line
let idx = buffer.indexOf('\n\n');
while (idx >= 0) {
const chunk = buffer.slice(0, idx).trim();
buffer = buffer.slice(idx + 2);
for (const line of chunk.split('\n')) {
const text = line.trim();
if (!text.startsWith('data:')) continue;
const raw = text.slice(5).trim();
if (!raw) continue;
const event = JSON.parse(raw) as StreamEvent;
onEvent(event);
}
idx = buffer.indexOf('\n\n');
}
}
}
```
## 7. 给 LLM 的改造任务提示词模板
将下面模板发给 LLM可用于自动改造其他前端项目:
```text
你要改造一个前端项目的聊天页面,把非流式接口 `/api/chat` 改为流式接口 `/api/chat/stream`。
后端协议约束:
1) 请求方法 POSTContent-Type=application/json
2) 请求体: { text, session_id?, user_id? }
3) 响应是 SSE 文本流,事件格式为 `data: <json>\n\n`
4) JSON 结构:
- type: thought | tool_call | tool_result | final | error
- content: string
- step?: number
- tool_name?: string
5) 收到 final 视为本轮完成;收到 error 视为失败
6) 默认不要假设前端一定会收到 thought/tool_call/tool_result仅当 `WEBUI_EXPOSE_REASONING=true` 才会透传
你的改造要求:
1) 保留现有 UI 风格和组件结构,不做无关重构
2) 新增流式读取逻辑,支持中途取消 (AbortController)
3) 将事件按 step 渲染到消息区
4) 兼容旧会话字段命名 (session_id / sessionId, user_id / userId)
5) 增加错误态与重试按钮
6) 不破坏原有上传、历史消息和输入框行为
7) 输出改动文件列表 + 每个文件的关键变更说明
请直接给出可运行代码补丁。
```
## 8. 调试清单
- 检查响应头是否为 `text/event-stream`
- 检查每条事件是否以 `data:` 开头并以空行结尾
- 确认 `final``error` 都能正确结束当前轮次
- 验证弱网下不会丢失已收到的事件
- 验证用户快速连续提问时,旧流可被取消

View File

@@ -1,6 +1,26 @@
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
import { API } from "../config";
/* ─── Steps & Labels ─── */
const STEPS = [
{ title: "需求", icon: "💬" },
{ title: "产品分析", icon: "📋" },
{ title: "测试用例", icon: "🧪" },
{ title: "开发代码", icon: "💻" },
{ title: "测试执行", icon: "▶" },
];
const STATUS_LABEL: Record<string, string> = {
clarifying: "澄清中",
pm_ready: "就绪",
pm_done: "产品分析完成",
qa_ready: "产品分析完成",
qa_done: "QA 完成",
dev_ready: "QA 完成",
dev_done: "代码就绪",
test_done: "测试完成",
};
/* ─── Types ─── */
type Step = 0 | 1 | 2 | 3 | 4;
type ClarifyMsg = { role: "user" | "assistant"; content: string };
@@ -200,13 +220,6 @@ function parseDevStream(text: string): DevStreamResult {
}
/* ── 步骤标签 ── */
const STEPS = [
{ title: "Requirements", icon: "💬" },
{ title: "PM Analysis", icon: "📋" },
{ title: "QA Cases", icon: "🧪" },
{ title: "Dev Code", icon: "💻" },
{ title: "Test Run", icon: "▶" },
];
const STATUS_STEP: Record<string, number> = {
clarifying: 0, pm_ready: 0,
@@ -214,13 +227,7 @@ const STATUS_STEP: Record<string, number> = {
qa_done: 2, dev_ready: 2,
dev_done: 3, test_done: 4,
};
const STATUS_LABEL: Record<string, string> = {
clarifying: "Clarifying", pm_ready: "Ready",
pm_done: "PM Done", qa_ready: "PM Done",
qa_done: "QA Done", dev_ready: "QA Done",
dev_done: "Code Ready", test_done: "Tests Done",
};
const TYPE_EMOJI: Record<string, string> = {
"Functional": "🧩", "Performance": "⚡", "Security": "🔒",
@@ -228,9 +235,12 @@ const TYPE_EMOJI: Record<string, string> = {
};
const TYPE_LABEL: Record<string, string> = {
"功能测试": "Functional", "能测试": "Performance",
"安全测试": "Security", "边界测试": "Boundary",
"异常测试": "Exception", "集成测试": "Integration",
"Functional": "能测试", "Performance": "性能测试",
"Security": "安全测试", "Boundary": "边界测试",
"Exception": "异常测试", "Integration": "集成测试",
"功能测试": "功能测试", "性能测试": "性能测试",
"安全测试": "安全测试", "边界测试": "边界测试",
"异常测试": "异常测试", "集成测试": "集成测试",
};
/* ━━━━━━━━━━━━━━━━━━━━━ Component ━━━━━━━━━━━━━━━━━━━━━ */
@@ -548,22 +558,22 @@ export default function DevOpsAgent() {
<div className="flex items-start gap-4 mb-6">
<div className="flex items-center justify-center w-10 h-10 text-xl rounded-2xl bg-magenta/10 shrink-0">📋</div>
<div>
<h2 className="text-base font-bold text-txt mb-0.5">Enter Requirements</h2>
<p className="text-sm text-txt-muted">Describe your product requirements. AI will complete analysis test cases code generation automatically.</p>
<h2 className="text-base font-bold text-txt mb-0.5"></h2>
<p className="text-sm text-txt-muted">AI </p>
</div>
</div>
<textarea
className="input-field min-h-[160px] resize-y mb-5"
value={requirement}
onChange={(e) => setRequirement(e.target.value)}
placeholder="e.g. Implement a user login feature supporting username/password, log all login events, target 1000 QPS concurrency…"
placeholder="例如:实现用户登录,支持用户名/密码登录并记录登录事件,目标并发 1000 QPS…"
/>
<div className="flex items-center justify-between">
<span className="text-xs text-txt-muted">{requirement.length > 0 ? `${requirement.length} chars` : ""}</span>
<button className="btn-magenta" onClick={handleStart} disabled={loading || !requirement.trim()}>
{loading ? (
<span className="flex items-center gap-2"><span className="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin inline-block"></span>Analyzing</span>
) : "Start AI Analysis →"}
<span className="flex items-center gap-2"><span className="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin inline-block"></span></span>
) : "开始 AI 分析 →"}
</button>
</div>
</div>
@@ -578,7 +588,7 @@ export default function DevOpsAgent() {
</div>
<div className="p-4 mb-5 text-sm bg-[#f9f9f9] border border-border rounded-xl">
<span className="block mb-1.5 text-[11px] font-bold text-txt-muted uppercase tracking-wide">Original Requirement</span>
<span className="block mb-1.5 text-[11px] font-bold text-txt-muted uppercase tracking-wide"></span>
<p className="leading-relaxed text-txt">{rawRequirement}</p>
</div>
@@ -604,17 +614,17 @@ export default function DevOpsAgent() {
{status === "clarifying" && (
<div className="pt-4 mt-2 border-t border-border">
<p className="mb-2 text-xs font-semibold tracking-wide uppercase text-txt-muted">Reply to AI</p>
<p className="mb-2 text-xs font-semibold tracking-wide uppercase text-txt-muted"> AI</p>
<textarea
className="input-field min-h-[80px] resize-y mb-3"
value={clarifyInput}
onChange={(e) => setClarifyInput(e.target.value)}
placeholder="Enter your additional details…"
placeholder="输入补充说明…"
disabled={loading}
/>
<div className="flex justify-end">
<button className="btn-magenta" onClick={handleClarify} disabled={loading}>
{loading ? <span className="flex items-center gap-2"><span className="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin inline-block"></span>Sending</span> : "Send"}
{loading ? <span className="flex items-center gap-2"><span className="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin inline-block"></span></span> : "发送"}
</button>
</div>
</div>
@@ -623,11 +633,11 @@ export default function DevOpsAgent() {
{status !== "clarifying" && status !== "" && (
<div className="pt-4 mt-2 border-t border-border">
<div className="flex items-center gap-2 px-4 py-3 mb-4 text-sm text-green-700 border border-green-200 bg-green-50 rounded-xl">
<span></span><span>Requirements confirmed. Ready to start PM analysis.</span>
<span></span><span></span>
</div>
<div className="flex justify-end">
<button className="btn-magenta" onClick={handlePmRun} disabled={loading || streaming}>
{loading ? "Analyzing…" : "Start PM Analysis →"}
{loading ? "分析中…" : "开始产品分析 →"}
</button>
</div>
</div>
@@ -641,12 +651,12 @@ export default function DevOpsAgent() {
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-8 h-8 text-base rounded-xl bg-orange-50">📋</div>
<h2 className="text-base font-bold">PM Analysis</h2>
<h2 className="text-base font-bold"></h2>
</div>
{streaming && (
<span className="flex items-center gap-1.5 text-xs font-semibold text-magenta bg-magenta/5 px-3 py-1 rounded-full">
<span className="w-1.5 h-1.5 bg-magenta rounded-full animate-pulse inline-block"></span>
Analyzing {streamText.length} chars
{streamText.length}
</span>
)}
</div>
@@ -655,10 +665,10 @@ export default function DevOpsAgent() {
{streaming && pmStreamParsed && (
<div ref={pmStreamRef} className="rounded-xl border border-border bg-[#fafafa] p-4 mb-4 space-y-3 max-h-[55vh] overflow-y-auto">
{([
["functional_requirements", "🔧", "Functional Requirements", "bg-blue-50 text-blue-700"],
["non_functional_requirements", "⚙️", "Non-Functional Requirements", "bg-purple-50 text-purple-700"],
["acceptance_criteria", "✅", "Acceptance Criteria", "bg-green-50 text-green-700"],
["edge_cases", "🚧", "Edge Cases", "bg-amber-50 text-amber-700"],
["functional_requirements", "🔧", "功能需求", "bg-blue-50 text-blue-700"],
["non_functional_requirements", "⚙️", "非功能需求", "bg-purple-50 text-purple-700"],
["acceptance_criteria", "✅", "验收准则", "bg-green-50 text-green-700"],
["edge_cases", "🚧", "边界情况", "bg-amber-50 text-amber-700"],
] as const).map(([key, icon, label, color]) => {
const sec = pmStreamParsed[key];
if (!sec.items.length && pmStreamParsed.currentSection !== key) return null;
@@ -677,8 +687,8 @@ export default function DevOpsAgent() {
);
})}
{(pmStreamParsed.summary.value || pmStreamParsed.currentSection === "summary") && (
<div className={`transition-opacity ${ pmStreamParsed.currentSection === "summary" ? "opacity-100" : "opacity-50" }`}>
<div className="inline-flex items-center gap-1 text-[11px] font-bold px-2 py-0.5 rounded-md mb-2 bg-gray-100 text-gray-600">📌 Summary</div>
<div className={`transition-opacity ${ pmStreamParsed.currentSection === "summary" ? "opacity-100" : "opacity-50" }`}>
<div className="inline-flex items-center gap-1 text-[11px] font-bold px-2 py-0.5 rounded-md mb-2 bg-gray-100 text-gray-600">📌 </div>
<p className="text-sm leading-relaxed">{pmStreamParsed.summary.value}{pmStreamParsed.summary.active && <span className="text-magenta animate-pulse">|</span>}</p>
</div>
)}

View File

@@ -24,6 +24,7 @@ type State = {
sessionId: string;
userId: string;
messages: ChatMessage[];
reasoning: Record<string, string>; // Map message ID to its reasoning/traces
chatting: boolean;
error?: string;
};
@@ -31,6 +32,7 @@ type State = {
type Action =
| { type: "add-msg"; msg: ChatMessage }
| { type: "update-msg"; id: string; patch: Partial<ChatMessage> }
| { type: "update-reasoning"; id: string; content: string }
| { type: "set-chatting"; v: boolean }
| { type: "set-error"; v?: string }
| { type: "reset"; sessionId: string; userId: string };
@@ -71,6 +73,11 @@ function reducer(state: State, action: Action): State {
m.id === action.id ? { ...m, ...action.patch } : m,
),
};
case "update-reasoning":
return {
...state,
reasoning: { ...state.reasoning, [action.id]: action.content },
};
case "set-chatting":
return { ...state, chatting: action.v };
case "set-error":
@@ -84,6 +91,7 @@ function reducer(state: State, action: Action): State {
sessionId: action.sessionId,
userId: action.userId,
messages: [],
reasoning: {},
chatting: false,
error: undefined,
};
@@ -101,7 +109,7 @@ async function streamChat(
) {
const res = await fetch(`${API.planning}/chat/stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: { "Content-Type": "application/json", "Accept": "text/event-stream" },
body: JSON.stringify(payload),
signal,
});
@@ -117,11 +125,14 @@ async function streamChat(
while (idx >= 0) {
const pkt = buf.slice(0, idx);
buf = buf.slice(idx + 2);
for (const line of pkt.split("\n")) {
// Split by literal newline, keeping in mind \r\n vs \n
const lines = pkt.split(/\r?\n/);
for (const line of lines) {
const t = line.trim();
if (!t.startsWith("data:")) continue;
try {
onEvent(JSON.parse(t.slice(5).trim()));
const data = JSON.parse(t.slice(5).trim());
onEvent(data);
} catch {
/* skip */
}
@@ -162,6 +173,7 @@ export default function PlanningAgent() {
sessionId: sess.current.sessionId,
userId: sess.current.userId,
messages: [],
reasoning: {},
chatting: false,
});
@@ -209,7 +221,12 @@ export default function PlanningAgent() {
},
(evt) => {
if (evt.type === "final") {
finalText = evt.content;
finalText += evt.content;
dispatch({
type: "update-msg",
id: assistMsg.id,
patch: { content: finalText },
});
} else if (evt.type === "error") {
traces.push(`[error] ${evt.content}`);
} else {
@@ -218,15 +235,13 @@ export default function PlanningAgent() {
traces.push(`${prefix}${evt.type}${tool}: ${evt.content}`);
}
const parts: string[] = [];
if (traces.length) parts.push(traces.join("\n"));
if (finalText) parts.push(finalText);
dispatch({
type: "update-msg",
id: assistMsg.id,
patch: { content: parts.join("\n\n") || "思考中..." },
});
if (traces.length) {
dispatch({
type: "update-reasoning",
id: assistMsg.id,
content: traces.join("\n"),
});
}
},
ctrl.signal,
);
@@ -278,9 +293,9 @@ export default function PlanningAgent() {
<div className="flex flex-col gap-6 w-full">
{state.messages.length === 0 && (
<div className="text-center py-20 card">
<h2 className="text-2xl font-extrabold mb-2">Planning Council Agent</h2>
<h2 className="text-2xl font-extrabold mb-2"></h2>
<p className="text-txt-muted text-sm">
Share a high-level Epic and the agent will plan through PM, Architect, and RTE perspectives.
Epic RTE
</p>
</div>
)}
@@ -288,29 +303,43 @@ export default function PlanningAgent() {
{state.messages.map((msg) => (
<div
key={msg.id}
className={`max-w-[78%] px-6 py-5 text-[0.95rem] leading-relaxed whitespace-pre-wrap rounded-2xl shadow-[0_2px_14px_rgba(0,0,0,0.04)] ${
className={`max-w-[78%] px-6 py-5 text-[0.95rem] leading-relaxed rounded-2xl shadow-[0_2px_14px_rgba(0,0,0,0.04)] ${
msg.role === "user"
? "self-end mr-1 bg-surface-muted text-txt border border-border"
? "self-end mr-1 bg-surface-muted text-txt border border-border whitespace-pre-wrap"
: "self-start bg-white border border-border border-l-4 border-l-magenta"
}`}
>
{msg.role === "assistant" && (
<span className="badge mb-3 block w-fit">SYSTEM / RE-ACT LOOP</span>
<span className="badge mb-3 block w-fit"> / </span>
)}
{msg.role === "assistant" && state.reasoning[msg.id] && (
<details
className="mb-4 bg-surface-muted/50 rounded-lg border border-border overflow-hidden"
open={!msg.content}
>
<summary className="px-4 py-2 text-xs font-medium text-txt-muted cursor-pointer hover:bg-border/20 transition-colors list-none flex items-center gap-2">
<span className={`w-1.5 h-1.5 rounded-full bg-magenta ${!msg.content ? 'animate-pulse' : ''}`}></span>
</summary>
<div className="px-4 py-3 bg-white/50 text-[0.8rem] font-mono whitespace-pre-wrap border-t border-border text-txt-muted">
{state.reasoning[msg.id]}
</div>
</details>
)}
{msg.role === "assistant" ? (
<div className="prose prose-sm max-w-none prose-p:my-1 prose-pre:bg-gray-800 prose-pre:text-gray-100 prose-code:text-magenta prose-code:bg-magenta/10 prose-code:px-1 prose-code:rounded prose-li:my-0">
<div className="prose prose-sm max-w-none prose-p:my-0.5 prose-pre:bg-gray-800 prose-pre:text-gray-100 prose-code:text-magenta prose-code:bg-magenta/10 prose-code:px-1 prose-code:rounded prose-li:my-0">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
>
{msg.content || "Thinking..."}
{msg.content || (state.chatting ? "思考中..." : "")}
</ReactMarkdown>
</div>
) : (
msg.content || "Thinking..."
)}
{msg.status === "failed" && (
<span className="text-red-500 text-xs block mt-2">Failed to send</span>
<span className="text-red-500 text-xs block mt-2"></span>
)}
</div>
))}
@@ -336,7 +365,7 @@ export default function PlanningAgent() {
{/* File upload row */}
<div className="flex items-center gap-3 mb-3 text-sm">
<label className="btn-outline text-xs px-3 py-1.5 cursor-pointer">
{uploading ? "Uploading..." : "Attach File"}
{uploading ? "上传中..." : "附加文件"}
<input type="file" className="hidden" onChange={handleUpload} disabled={uploading} />
</label>
{files.map((f) => (
@@ -346,7 +375,7 @@ export default function PlanningAgent() {
className="ml-auto text-xs text-txt-muted hover:text-magenta"
onClick={handleReset}
>
Reset Session
</button>
</div>
@@ -361,11 +390,11 @@ export default function PlanningAgent() {
send();
}
}}
placeholder="Describe your Epic or provide guidance..."
placeholder="描述你的 Epic 或提供指引..."
disabled={state.chatting}
/>
<button className="btn-magenta" onClick={send} disabled={state.chatting}>
{state.chatting ? "Planning..." : "Send"}
{state.chatting ? "规划中..." : "发送"}
</button>
</div>
</div>

View File

@@ -381,14 +381,14 @@ export default function QualityGate({ view }: QualityGateProps) {
{view === "dashboard" && (
<>
<h2 className="text-xl font-extrabold mb-6">Overview</h2>
<h2 className="text-xl font-extrabold mb-6"></h2>
<div className="grid grid-cols-4 gap-5 mb-8">
{[
{ label: "Open PRs", value: stats.pending, color: "text-yellow-600", bg: "bg-yellow-50" },
{ label: "Merged", value: stats.passed, color: "text-green-600", bg: "bg-green-50" },
{ label: "Rejected", value: stats.rejected, color: "text-red-600", bg: "bg-red-50" },
{ label: "Total Issues", value: stats.totalIssues, color: "text-magenta", bg: "bg-magenta-50" },
{ label: "打开 PR", value: stats.pending, color: "text-yellow-600", bg: "bg-yellow-50" },
{ label: "已合并", value: stats.passed, color: "text-green-600", bg: "bg-green-50" },
{ label: "已拒绝", value: stats.rejected, color: "text-red-600", bg: "bg-red-50" },
{ label: "问题总数", value: stats.totalIssues, color: "text-magenta", bg: "bg-magenta-50" },
].map((s) => (
<div key={s.label} className={`${s.bg} p-6 border border-border rounded-2xl shadow-[0_4px_16px_rgba(0,0,0,0.04)]`}>
<div className={`text-3xl font-extrabold ${s.color}`}>{s.value}</div>
@@ -401,7 +401,7 @@ export default function QualityGate({ view }: QualityGateProps) {
<ProblemTrendChart history={trendHistory} loading={trendLoading} />
</div>
<h3 className="text-lg font-extrabold mb-4">Recent Pull Requests</h3>
<h3 className="text-lg font-extrabold mb-4"> Pull Request</h3>
<PRTable prs={prs.slice(0, 8)} loading={loading} onView={(pr) => openPRDetail(pr.id)} />
</>
)}
@@ -409,7 +409,7 @@ export default function QualityGate({ view }: QualityGateProps) {
{view === "pr-list" && (
<>
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-extrabold">Pull Request List</h2>
<h2 className="text-xl font-extrabold">Pull Request </h2>
<div className="flex gap-2">
{(["all", "open", "merged", "closed"] as const).map((f) => (
<button
@@ -421,11 +421,11 @@ export default function QualityGate({ view }: QualityGateProps) {
: "border-border text-txt-muted hover:border-magenta"
}`}
>
{f === "all" ? "All" : f === "open" ? "Open" : f === "merged" ? "Merged" : "Closed"}
{f === "all" ? "全部" : f === "open" ? "打开" : f === "merged" ? "已合并" : "已关闭"}
</button>
))}
<button className="btn-outline text-xs px-3 py-1.5" onClick={fetchPRs}>
Refresh
</button>
</div>
</div>
@@ -435,21 +435,21 @@ export default function QualityGate({ view }: QualityGateProps) {
{view === "settings" && (
<div className="max-w-xl">
<h2 className="text-xl font-extrabold mb-6">Settings</h2>
<h2 className="text-xl font-extrabold mb-6"></h2>
<div className="card mb-4">
<h3 className="font-bold mb-2">Webhook Configuration</h3>
<p className="text-sm text-txt-muted mb-3">Add this URL to your Gitea repository webhook settings:</p>
<h3 className="font-bold mb-2">Webhook </h3>
<p className="text-sm text-txt-muted mb-3"> URL Gitea webhook </p>
<code className="block bg-surface-muted p-3 text-sm font-mono break-all rounded-lg">
POST {window.location.origin}/quality-api/webhook/gitea
</code>
</div>
<div className="card">
<h3 className="font-bold mb-2">Quick Notes</h3>
<h3 className="font-bold mb-2"></h3>
<ul className="text-sm text-txt-muted list-disc list-inside space-y-1">
<li>Supports Gitea Push and Pull Request events</li>
<li>Runs Pylint, Flake8, ESLint, and Bandit automatically</li>
<li>Optional AI review using DeepSeek-V3</li>
<li>Scan summary can be pushed to Feishu channels</li>
<li> Gitea Push Pull Request </li>
<li> PylintFlake8ESLint Bandit</li>
<li> AI DeepSeek-V3</li>
<li></li>
</ul>
</div>
</div>
@@ -477,7 +477,7 @@ export default function QualityGate({ view }: QualityGateProps) {
<div className="flex flex-1 min-h-0">
<div className="w-[260px] shrink-0 border-r border-border overflow-y-auto p-4 bg-surface-muted/35">
<h3 className="text-xs font-bold text-txt-muted uppercase mb-3">Changed Files</h3>
<h3 className="text-xs font-bold text-txt-muted uppercase mb-3"></h3>
{changedFiles.map((f) => (
<button
key={f.filename}
@@ -502,7 +502,7 @@ export default function QualityGate({ view }: QualityGateProps) {
{/* 代码区:完整代码 + 每行右侧缺陷标注,一行一个滚动条(参考 index copy.html */}
<div className="flex-1 min-h-0 min-w-0 flex flex-col overflow-hidden">
<div className="flex-1 min-h-0 overflow-auto bg-[#1e1e1e] font-mono text-[13px] leading-[1.5]">
{fileContent ? (
{fileContent ? (
<div className="min-w-min">
{(fileContent.content ?? "").split("\n").map((line, i) => {
const lineNum = i + 1;
@@ -573,7 +573,7 @@ export default function QualityGate({ view }: QualityGateProps) {
</div>
) : (
<div className="flex items-center justify-center h-full min-h-[200px] text-[#6c757d] text-sm">
{selectedFile ? "Loading file..." : "Select a file to inspect"}
{selectedFile ? "加载文件..." : "请选择要检查的文件"}
</div>
)}
</div>
@@ -582,21 +582,21 @@ export default function QualityGate({ view }: QualityGateProps) {
<div className="flex justify-end gap-3 px-6 py-4 border-t border-border shrink-0">
<button className="btn-outline" onClick={() => setModalOpen(false)}>
Close
</button>
<button
className="bg-red-600 text-white font-bold text-sm px-6 py-2.5 border-none cursor-pointer hover:opacity-90 disabled:opacity-50 rounded-xl"
onClick={handleClose}
disabled={loading || selectedPR.state !== "open"}
>
Reject
</button>
<button
className="btn-magenta"
onClick={handleMerge}
disabled={loading || selectedPR.state !== "open"}
>
Approve & Merge
</button>
</div>
</div>
@@ -616,27 +616,27 @@ function PRTable({
onView: (pr: PRScan) => void;
}) {
if (loading && prs.length === 0) {
return <div className="text-center py-8 text-txt-muted text-sm">Loading...</div>;
return <div className="text-center py-8 text-txt-muted text-sm">...</div>;
}
if (prs.length === 0) {
return <div className="text-center py-8 text-txt-muted text-sm">No PR records found</div>;
return <div className="text-center py-8 text-txt-muted text-sm"> PR </div>;
}
return (
<div className="overflow-x-auto rounded-2xl border border-border bg-white shadow-[0_6px_22px_rgba(0,0,0,0.04)]">
<table className="w-full text-sm">
<thead>
<tr className="bg-surface-muted text-left">
<tr className="bg-surface-muted text-left">
<th className="px-4 py-3 border-b border-border font-bold">PR#</th>
<th className="px-4 py-3 border-b border-border font-bold">Title</th>
<th className="px-4 py-3 border-b border-border font-bold">Repository</th>
<th className="px-4 py-3 border-b border-border font-bold">Author</th>
<th className="px-4 py-3 border-b border-border font-bold">Branch</th>
<th className="px-4 py-3 border-b border-border font-bold">State</th>
<th className="px-4 py-3 border-b border-border font-bold">Issues</th>
<th className="px-4 py-3 border-b border-border font-bold">Created</th>
<th className="px-4 py-3 border-b border-border font-bold">Action</th>
<th className="px-4 py-3 border-b border-border font-bold"></th>
<th className="px-4 py-3 border-b border-border font-bold"></th>
<th className="px-4 py-3 border-b border-border font-bold"></th>
<th className="px-4 py-3 border-b border-border font-bold"></th>
<th className="px-4 py-3 border-b border-border font-bold"></th>
<th className="px-4 py-3 border-b border-border font-bold"></th>
<th className="px-4 py-3 border-b border-border font-bold"></th>
<th className="px-4 py-3 border-b border-border font-bold"></th>
</tr>
</thead>
<tbody>
@@ -657,7 +657,7 @@ function PRTable({
? "bg-green-100 text-green-700"
: "bg-red-100 text-red-700"
}`}>
{pr.state === "open" ? "Open" : pr.state === "merged" ? "Merged" : "Closed"}
{pr.state === "open" ? "打开" : pr.state === "merged" ? "已合并" : "已关闭"}
</span>
</td>
<td className="px-4 py-3">
@@ -666,14 +666,14 @@ function PRTable({
</span>
</td>
<td className="px-4 py-3 text-xs text-txt-muted">
{new Date(pr.created_at).toLocaleString("en-US")}
{new Date(pr.created_at).toLocaleString("zh-CN")}
</td>
<td className="px-4 py-3">
<button
className="text-magenta font-bold text-xs hover:underline"
onClick={() => onView(pr)}
>
Inspect
</button>
</td>
</tr>

View File

@@ -36,13 +36,13 @@
<i class="fa-solid fa-house w-6"></i> 总览控制台
</a>
<a href="planning.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-planning rounded-lg font-medium transition-colors">
<i class="fa-solid fa-chess-knight w-6"></i> 战略规划 (Planning)
<i class="fa-solid fa-chess-knight w-6"></i> 战略规划
</a>
<a href="devops.html" class="flex items-center px-4 py-3 bg-green-50 text-devops rounded-lg font-medium">
<i class="fa-solid fa-code-branch w-6"></i> 开发运维 (DevOps)
<i class="fa-solid fa-code-branch w-6"></i> 开发运维
</a>
<a href="quality.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-quality rounded-lg font-medium transition-colors">
<i class="fa-solid fa-shield-halved w-6"></i> 质量门控 (Quality Gate)
<i class="fa-solid fa-shield-halved w-6"></i> 质量门控
</a>
</nav>
</div>
@@ -86,7 +86,7 @@
<div class="w-1/3 border-r border-gray-200 bg-gray-50 flex flex-col">
<div class="flex-1 p-4 overflow-y-auto space-y-4 text-sm">
<div class="bg-white p-3 rounded-lg border shadow-sm">
<p class="text-gray-800 font-semibold mb-1"><i class="fa-solid fa-robot text-devops mr-1"></i> Developer Agent</p>
<p class="text-gray-800 font-semibold mb-1"><i class="fa-solid fa-robot text-devops mr-1"></i> 开发代理</p>
<p class="text-gray-600">我已经根据您的 "登录功能" 需求生成了用户控制器的代码框架和对应的单元测试Jest</p>
<p class="text-gray-600 mt-2">测试用例涵盖了 5 种边界情况,您可以查看右侧代码。</p>
</div>

View File

@@ -57,13 +57,13 @@
<i class="fa-solid fa-house w-6"></i> 总览控制台
</a>
<a href="planning.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-planning rounded-lg font-medium transition-colors">
<i class="fa-solid fa-chess-knight w-6"></i> 战略规划 (Planning)
<i class="fa-solid fa-chess-knight w-6"></i> 战略规划
</a>
<a href="devops.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-devops rounded-lg font-medium transition-colors">
<i class="fa-solid fa-code-branch w-6"></i> 开发运维 (DevOps)
<i class="fa-solid fa-code-branch w-6"></i> 开发运维
</a>
<a href="quality.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-quality rounded-lg font-medium transition-colors">
<i class="fa-solid fa-shield-halved w-6"></i> 质量门控 (Quality Gate)
<i class="fa-solid fa-shield-halved w-6"></i> 质量门控
</a>
</nav>

View File

@@ -36,13 +36,13 @@
<i class="fa-solid fa-house w-6"></i> 总览控制台
</a>
<a href="planning.html" class="flex items-center px-4 py-3 bg-blue-50 text-planning rounded-lg font-medium">
<i class="fa-solid fa-chess-knight w-6"></i> 战略规划 (Planning)
<i class="fa-solid fa-chess-knight w-6"></i> 战略规划
</a>
<a href="devops.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-devops rounded-lg font-medium transition-colors">
<i class="fa-solid fa-code-branch w-6"></i> 开发运维 (DevOps)
<i class="fa-solid fa-code-branch w-6"></i> 开发运维
</a>
<a href="quality.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-quality rounded-lg font-medium transition-colors">
<i class="fa-solid fa-shield-halved w-6"></i> 质量门控 (Quality Gate)
<i class="fa-solid fa-shield-halved w-6"></i> 质量门控
</a>
</nav>
</div>

View File

@@ -36,13 +36,13 @@
<i class="fa-solid fa-house w-6"></i> 总览控制台
</a>
<a href="planning.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-planning rounded-lg font-medium transition-colors">
<i class="fa-solid fa-chess-knight w-6"></i> 战略规划 (Planning)
<i class="fa-solid fa-chess-knight w-6"></i> 战略规划
</a>
<a href="devops.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-devops rounded-lg font-medium transition-colors">
<i class="fa-solid fa-code-branch w-6"></i> 开发运维 (DevOps)
<i class="fa-solid fa-code-branch w-6"></i> 开发运维
</a>
<a href="quality.html" class="flex items-center px-4 py-3 bg-purple-50 text-quality rounded-lg font-medium">
<i class="fa-solid fa-shield-halved w-6"></i> 质量门控 (Quality Gate)
<i class="fa-solid fa-shield-halved w-6"></i> 质量门控
</a>
</nav>
</div>
@@ -50,7 +50,7 @@
<main class="flex-1 flex flex-col overflow-y-auto">
<header class="h-16 bg-white shadow-sm flex items-center px-8 border-b-4 border-quality sticky top-0 z-10">
<h1 class="text-xl font-semibold text-gray-800">质量门控 (Quality Gate)Dashboard</h1>
<h1 class="text-xl font-semibold text-gray-800">质量门控 大盘</h1>
</header>
<div class="p-8 max-w-7xl mx-auto w-full">
@@ -59,7 +59,7 @@
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div class="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<div class="text-gray-500 mb-1 text-sm font-semibold">质量状态</div>
<div class="text-2xl font-bold text-green-500">通过 (Passed)</div>
<div class="text-2xl font-bold text-green-500">通过</div>
</div>
<div class="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<div class="text-gray-500 mb-1 text-sm font-semibold">代码覆盖率</div>
@@ -78,7 +78,7 @@
<!-- PR 列表 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 mb-8">
<div class="p-4 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
<h2 class="font-bold text-gray-800">合并请求审查 (PR List)</h2>
<h2 class="font-bold text-gray-800">合并请求审查</h2>
<span class="text-sm text-gray-500">共 3 个待处理</span>
</div>
<div class="divide-y divide-gray-200">