feat: 改为真实后端接口模式

- devopsApi: 移除前端 mock 降级和本地发布单/refs 伪造逻辑
- views: 同步发布、运行记录、配置和顶部状态文案为真实接口模式
- docs: 更新前端维护规则,禁止新增 mock 数据降级
This commit is contained in:
湛兮
2026-06-11 21:27:30 +08:00
parent f74454f00a
commit af0649a302
16 changed files with 228 additions and 959 deletions
-1
View File
@@ -1,2 +1 @@
VITE_API_BASE_URL=http://localhost:4300 VITE_API_BASE_URL=http://localhost:4300
VITE_DEVOPS_USE_MOCK=false
+1 -1
View File
@@ -17,7 +17,7 @@
- 路由使用 Vue Router。 - 路由使用 Vue Router。
- 请求封装使用 Axios,并集中在 `src/services/**` - 请求封装使用 Axios,并集中在 `src/services/**`
- BPMN 展示使用 `bpmn-js`,不手写替代渲染器。 - BPMN 展示使用 `bpmn-js`,不手写替代渲染器。
- mock 数据集中在 `src/mocks/**`,后续以后端 OpenAPI/Swagger 替换 - 不新增前端 mock 数据或接口失败降级;项目、发布、refs、流程和 Agent 调用统一走后端 API
## API 与安全边界 ## API 与安全边界
+3 -3
View File
@@ -5,7 +5,7 @@ Vue 3 + Vite + TypeScript 实现的 DevOps 运维控制台第一阶段,用于
## 当前范围 ## 当前范围
- 登录、总览、项目配置、发布中心、运行记录、系统配置,以及全局右侧 Agent 抽屉。 - 登录、总览、项目配置、发布中心、运行记录、系统配置,以及全局右侧 Agent 抽屉。
- 优先调用 `devops-platform-api`端不可用时降级到 mock 数据展示 `access-manage``role-admin``role-user``my-resume``devops-platform-api``devops-platform-web` - 所有项目、发布、refs、流程和集成状态均调用 `devops-platform-api`端不再提供本地 mock 数据降级
- 发布中心包含项目、环境、ref/tag、备注、生产二次确认、触发、取消、重试和 Jenkins 状态同步入口。 - 发布中心包含项目、环境、ref/tag、备注、生产二次确认、触发、取消、重试和 Jenkins 状态同步入口。
- 运行记录展示 Jenkins queue/build、Build URL、开始/结束时间、执行提示、失败摘要和日志摘要。 - 运行记录展示 Jenkins queue/build、Build URL、开始/结束时间、执行提示、失败摘要和日志摘要。
- BPMN Viewer 使用 `bpmn-js` 渲染后端流程 XML,并按 queued/running/success/failed/skipped/canceled 状态高亮节点。 - BPMN Viewer 使用 `bpmn-js` 渲染后端流程 XML,并按 queued/running/success/failed/skipped/canceled 状态高亮节点。
@@ -29,7 +29,7 @@ pnpm install
pnpm dev pnpm dev
``` ```
The local web app listens on `http://localhost:4301` by default. Use `VITE_API_BASE_URL=http://localhost:4300` when switching from mock services to the local API. The local web app listens on `http://localhost:4301` by default. Set `VITE_API_BASE_URL=http://localhost:4300` to point the web app at the local API.
## 构建检查 ## 构建检查
@@ -39,7 +39,7 @@ pnpm build
## API 契约来源 ## API 契约来源
接口入口为 `src/services/devopsApi.ts`默认优先调用后端统一 envelope 响应;后端不可用`VITE_DEVOPS_USE_MOCK=true` 时降级到 `src/mocks/devops.ts`。后续以 `/Users/mac033/Desktop/my-project/devops-platform-docs/devops-platform-implementation-tracker.md` 中的产品约束和后端 OpenAPI 为准继续收敛字段。 接口入口为 `src/services/devopsApi.ts`调用后端统一 envelope 响应;后端不可用时页面应暴露真实接口错误,不伪造项目、运行记录或 refs。后续以 `/Users/mac033/Desktop/my-project/devops-platform-docs/devops-platform-implementation-tracker.md` 中的产品约束和后端 OpenAPI 为准继续收敛字段。
浏览器端不得保存或直连以下密钥和服务: 浏览器端不得保存或直连以下密钥和服务:
+3 -4
View File
@@ -9,11 +9,11 @@
- `role-user` - `role-user`
- `my-resume` - `my-resume`
第一阶段为本地 mock UI,后续接入 `devops-platform-api` 当前前端只调用 `devops-platform-api`,不再提供本地 mock 数据降级
## 页面结构 ## 页面结构
- `/login`mock 登录 - `/login`本地操作员会话入口,后续接后端认证
- `/overview`:总览,展示服务状态、最近发布和项目环境。 - `/overview`:总览,展示服务状态、最近发布和项目环境。
- `/projects`:项目配置,展示 Gitea 仓库、Jenkins Job、企微机器人和环境策略。 - `/projects`:项目配置,展示 Gitea 仓库、Jenkins Job、企微机器人和环境策略。
- `/releases`:发布中心,提供发布参数、二次确认、触发、取消、重试和 BPMN 流程状态。 - `/releases`:发布中心,提供发布参数、二次确认、触发、取消、重试和 BPMN 流程状态。
@@ -23,7 +23,6 @@
## 数据约定 ## 数据约定
- mock 数据入口:`src/mocks/devops.ts`
- API 封装入口:`src/services/devopsApi.ts` - API 封装入口:`src/services/devopsApi.ts`
- Axios 实例:`src/services/http.ts` - Axios 实例:`src/services/http.ts`
- 平台状态:`src/stores/platform.ts` - 平台状态:`src/stores/platform.ts`
@@ -31,7 +30,7 @@
## 后续接入点 ## 后续接入点
- 用后端 OpenAPI 替换 `devopsApi` mock 返回 - `devopsApi` 所有项目、发布、refs、流程和 Agent 调用均走后端接口
- 发布中心触发按钮接入后端发布单创建接口。 - 发布中心触发按钮接入后端发布单创建接口。
- 运行记录接入后端 run/step 查询接口。 - 运行记录接入后端 run/step 查询接口。
- BPMN XML 由后端流程定义接口返回。 - BPMN XML 由后端流程定义接口返回。
+3 -3
View File
@@ -41,7 +41,7 @@ const jenkinsStatus = computed(() => {
}) })
const apiModeLabel = computed(() => { const apiModeLabel = computed(() => {
return platform.dataSource === 'backend' ? '后端 API' : 'Mock 降级' return '后端 API'
}) })
onMounted(() => { onMounted(() => {
@@ -83,7 +83,7 @@ function logout() {
<div class="env-strip"> <div class="env-strip">
<span>本地开发</span> <span>本地开发</span>
<StatusPill :state="jenkinsStatus" text="Jenkins" /> <StatusPill :state="jenkinsStatus" text="Jenkins" />
<ElTag :type="platform.dataSource === 'backend' ? 'success' : 'info'" round> <ElTag type="success" round>
{{ apiModeLabel }} {{ apiModeLabel }}
</ElTag> </ElTag>
</div> </div>
@@ -94,7 +94,7 @@ function logout() {
</ElButton> </ElButton>
<ElTag type="primary" effect="plain" round> <ElTag type="primary" effect="plain" round>
<ElIcon><Connection /></ElIcon> <ElIcon><Connection /></ElIcon>
{{ platform.dataSource === 'backend' ? '后端已联通' : '后端不可用时自动降级' }} 后端已联通
</ElTag> </ElTag>
<ElDropdown> <ElDropdown>
<ElButton> <ElButton>
-638
View File
@@ -1,638 +0,0 @@
/**
* 本地降级数据只模拟 DevOps 运维展示,不包含任何真实 token、webhook 或服务地址。
*/
import type {
AgentCard,
DashboardSummary,
IntegrationConfigStatus,
IntegrationStatus,
PipelineStep,
ProjectConfig,
ReleaseRun,
SystemSetting,
} from '../types/devops'
export const releaseProcessXml = `<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_DevOpsRelease" targetNamespace="http://devops-platform.local/bpmn">
<bpmn:process id="Process_Release" name="DevOps release pipeline" isExecutable="false">
<bpmn:startEvent id="start-release" name="提交发布">
<bpmn:outgoing>Flow_Start_Gitea</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:serviceTask id="gitea-check" name="Gitea ref 校验">
<bpmn:incoming>Flow_Start_Gitea</bpmn:incoming>
<bpmn:outgoing>Flow_Gitea_Jenkins</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:serviceTask id="jenkins-build" name="Jenkins 构建">
<bpmn:incoming>Flow_Gitea_Jenkins</bpmn:incoming>
<bpmn:outgoing>Flow_Jenkins_Health</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:serviceTask id="health-check" name="健康检查">
<bpmn:incoming>Flow_Jenkins_Health</bpmn:incoming>
<bpmn:outgoing>Flow_Health_Notify</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:serviceTask id="notify-wecom" name="通知平台">
<bpmn:incoming>Flow_Health_Notify</bpmn:incoming>
<bpmn:outgoing>Flow_Notify_End</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:endEvent id="finish-release" name="归档审计">
<bpmn:incoming>Flow_Notify_End</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_Start_Gitea" sourceRef="start-release" targetRef="gitea-check" />
<bpmn:sequenceFlow id="Flow_Gitea_Jenkins" sourceRef="gitea-check" targetRef="jenkins-build" />
<bpmn:sequenceFlow id="Flow_Jenkins_Health" sourceRef="jenkins-build" targetRef="health-check" />
<bpmn:sequenceFlow id="Flow_Health_Notify" sourceRef="health-check" targetRef="notify-wecom" />
<bpmn:sequenceFlow id="Flow_Notify_End" sourceRef="notify-wecom" targetRef="finish-release" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_Release">
<bpmndi:BPMNPlane id="BPMNPlane_Release" bpmnElement="Process_Release">
<bpmndi:BPMNShape id="Shape_Start" bpmnElement="start-release">
<dc:Bounds x="120" y="145" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Shape_Gitea" bpmnElement="gitea-check">
<dc:Bounds x="210" y="123" width="118" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Shape_Jenkins" bpmnElement="jenkins-build">
<dc:Bounds x="380" y="123" width="118" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Shape_Health" bpmnElement="health-check">
<dc:Bounds x="550" y="123" width="118" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Shape_Notify" bpmnElement="notify-wecom">
<dc:Bounds x="720" y="123" width="118" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Shape_End" bpmnElement="finish-release">
<dc:Bounds x="900" y="145" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Edge_Start_Gitea" bpmnElement="Flow_Start_Gitea">
<di:waypoint x="156" y="163" />
<di:waypoint x="210" y="163" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Edge_Gitea_Jenkins" bpmnElement="Flow_Gitea_Jenkins">
<di:waypoint x="328" y="163" />
<di:waypoint x="380" y="163" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Edge_Jenkins_Health" bpmnElement="Flow_Jenkins_Health">
<di:waypoint x="498" y="163" />
<di:waypoint x="550" y="163" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Edge_Health_Notify" bpmnElement="Flow_Health_Notify">
<di:waypoint x="668" y="163" />
<di:waypoint x="720" y="163" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Edge_Notify_End" bpmnElement="Flow_Notify_End">
<di:waypoint x="838" y="163" />
<di:waypoint x="900" y="163" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>`
export const integrations: IntegrationStatus[] = [
{
key: 'jenkins',
name: 'Jenkins',
state: 'healthy',
summary: 'folder jobs 已按 my-project 规划,等待后端真实接入',
checkedAt: '2026-06-11 17:40',
latencyMs: 86,
},
{
key: 'gitea',
name: 'Gitea',
state: 'healthy',
summary: '纳管仓库 mock 同步完成,生产发布按项目策略选择 ref/tag',
checkedAt: '2026-06-11 17:40',
latencyMs: 42,
},
{
key: 'notification',
name: '通知平台',
state: 'warning',
summary: '机器人未配置,当前保留企微/飞书/通用 webhook 方案',
checkedAt: '2026-06-11 17:38',
},
{
key: 'llm',
name: 'LLM Agent',
state: 'warning',
summary: 'baseURL/key/model 待后端提供,前端不保存密钥',
checkedAt: '2026-06-11 17:38',
},
]
export const integrationConfigStatus: IntegrationConfigStatus = {
checkedAt: '2026-06-11T17:40:00+08:00',
integrations: [
{
key: 'jenkins',
name: 'Jenkins',
status: 'partial',
required: ['JENKINS_BASE_URL', 'JENKINS_USERNAME', 'JENKINS_API_TOKEN'],
configured: ['JENKINS_BASE_URL', 'JENKINS_USERNAME'],
missing: ['JENKINS_API_TOKEN'],
optional: [],
note: '前端只展示变量名,Jenkins 凭据由后端读取。',
},
{
key: 'gitea',
name: 'Gitea',
status: 'configured',
required: ['GITEA_BASE_URL', 'GITEA_TOKEN'],
configured: ['GITEA_BASE_URL', 'GITEA_TOKEN'],
missing: [],
optional: ['GITEA_WEBHOOK_SECRET'],
note: '仓库访问统一走后端 API。',
},
{
key: 'notification',
name: '通知平台',
status: 'missing',
required: ['NOTIFICATION_PROVIDER'],
configured: [],
missing: ['NOTIFICATION_PROVIDER'],
optional: ['WECOM_WEBHOOK_URL', 'FEISHU_WEBHOOK_URL', 'NOTIFICATION_WEBHOOK_URL'],
note: '浏览器不保存 webhook;后端按 provider 投递。',
},
{
key: 'llm',
name: 'LLM Agent',
status: 'partial',
required: ['LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL'],
configured: ['LLM_MODEL'],
missing: ['LLM_BASE_URL', 'LLM_API_KEY'],
optional: [],
note: '模型供应商只由后端代理调用。',
},
{
key: 'database',
name: 'Database',
status: 'configured',
required: ['DATABASE_URL'],
configured: ['DATABASE_URL'],
missing: [],
optional: ['USE_DATABASE_READS', 'PRISMA_CONNECT_ON_BOOT'],
note: '数据库连接仅服务端可见。',
},
{
key: 'redis',
name: 'Redis',
status: 'configured',
required: ['REDIS_URL'],
configured: ['REDIS_URL'],
missing: [],
optional: [],
note: 'Redis 连接仅服务端可见。',
},
{
key: 'secrets',
name: 'Secrets',
status: 'missing',
required: ['SECRET_ENCRYPTION_KEY'],
configured: [],
missing: ['SECRET_ENCRYPTION_KEY'],
optional: [],
note: '应用密钥只在后端环境中读取。',
},
],
}
export const projects: ProjectConfig[] = [
{
key: 'access-manage',
name: 'access-manage',
repository: 'https://gitea.ops.mrzhan.top/my-project/access-manage.git',
owner: 'my-project',
repo: 'access-manage',
serviceType: '后端服务',
defaultBranch: 'develop',
tagPolicy: '生产发布必须选择 v* tag',
environments: [
{
name: 'test',
label: '测试环境',
branchPolicy: 'develop / feature/*',
status: 'success',
version: 'develop@8f31a2c',
lastRunId: 'run-1048',
lastReleasedAt: '2026-06-11 15:10',
lastActor: 'ops-admin',
jenkinsJob: 'my-project/access-manage-test',
wecomRobot: 'devops-test',
},
{
name: 'production',
label: '生产环境',
branchPolicy: 'v* tag',
status: 'running',
version: 'v2026.06.11-api.1',
lastRunId: 'run-1052',
lastReleasedAt: '2026-06-11 17:12',
lastActor: 'ops-admin',
jenkinsJob: 'my-project/access-manage-prod',
wecomRobot: 'devops-prod',
},
],
integrations: {
jenkins: 'healthy',
gitea: 'healthy',
wecom: 'warning',
llm: 'warning',
},
},
{
key: 'role-admin',
name: 'role-admin',
repository: 'https://gitea.ops.mrzhan.top/my-project/role-admin.git',
owner: 'my-project',
repo: 'role-admin',
serviceType: '管理后台',
defaultBranch: 'develop',
tagPolicy: '生产发布必须由 develop 合并后 tag',
environments: [
{
name: 'test',
label: '测试环境',
branchPolicy: 'develop',
status: 'success',
version: 'develop@b42e18d',
lastRunId: 'run-1043',
lastReleasedAt: '2026-06-11 14:35',
lastActor: 'frontend-worker',
jenkinsJob: 'my-project/role-admin-test',
wecomRobot: 'devops-test',
},
{
name: 'production',
label: '生产环境',
branchPolicy: 'v* tag',
status: 'idle',
version: 'v2026.06.05-envsplit.7',
lastRunId: 'run-1026',
lastReleasedAt: '2026-06-05 16:02',
lastActor: 'ops-admin',
jenkinsJob: 'my-project/role-admin-prod',
wecomRobot: 'devops-prod',
},
],
integrations: {
jenkins: 'healthy',
gitea: 'healthy',
wecom: 'healthy',
llm: 'warning',
},
},
{
key: 'role-user',
name: 'role-user',
repository: 'https://gitea.ops.mrzhan.top/my-project/role-user.git',
owner: 'my-project',
repo: 'role-user',
serviceType: 'C 端员工应用',
defaultBranch: 'develop',
tagPolicy: '生产发布仅允许 release tag',
environments: [
{
name: 'test',
label: '测试环境',
branchPolicy: 'develop / feature/*',
status: 'failed',
version: 'develop@4a09fd2',
lastRunId: 'run-1050',
lastReleasedAt: '2026-06-11 16:24',
lastActor: 'frontend-worker',
jenkinsJob: 'my-project/role-user-test',
wecomRobot: 'devops-test',
},
{
name: 'production',
label: '生产环境',
branchPolicy: 'v* tag',
status: 'idle',
version: 'v2026.06.05-envsplit.7',
lastRunId: 'run-1028',
lastReleasedAt: '2026-06-05 16:08',
lastActor: 'ops-admin',
jenkinsJob: 'my-project/role-user-prod',
wecomRobot: 'devops-prod',
},
],
integrations: {
jenkins: 'error',
gitea: 'healthy',
wecom: 'healthy',
llm: 'warning',
},
},
{
key: 'my-resume',
name: 'my-resume',
repository: 'https://gitea.ops.mrzhan.top/my-project/my-resume.git',
owner: 'my-project',
repo: 'my-resume',
serviceType: '个人站点',
defaultBranch: 'master',
tagPolicy: '生产发布允许 master 或 v* tag',
environments: [
{
name: 'test',
label: '测试环境',
branchPolicy: 'master / feature/*',
status: 'success',
version: 'master@6c92e81',
lastRunId: 'run-1039',
lastReleasedAt: '2026-06-11 12:18',
lastActor: 'frontend-worker',
jenkinsJob: 'my-project/my-resume-test',
wecomRobot: 'devops-test',
},
{
name: 'production',
label: '生产环境',
branchPolicy: 'master 或 v* tag',
status: 'success',
version: 'v2026.06.10-resume.2',
lastRunId: 'run-1033',
lastReleasedAt: '2026-06-10 21:42',
lastActor: 'ops-admin',
jenkinsJob: 'my-project/my-resume-prod',
wecomRobot: 'devops-prod',
},
],
integrations: {
jenkins: 'healthy',
gitea: 'healthy',
wecom: 'healthy',
llm: 'warning',
},
},
{
key: 'devops-platform-api',
name: 'devops-platform-api',
repository: 'https://gitea.ops.mrzhan.top/devops-platform/devops-platform-api.git',
owner: 'devops-platform',
repo: 'devops-platform-api',
serviceType: 'DevOps 后端',
defaultBranch: 'master',
tagPolicy: '合并到 master 即发布 devops.mrzhan.top API',
environments: [
{
name: 'production',
label: '生产环境',
branchPolicy: 'master',
status: 'idle',
version: 'master@local',
lastRunId: '-',
lastReleasedAt: '-',
lastActor: '-',
jenkinsJob: 'devops-platform/prod-devops-platform-api-master',
wecomRobot: '通知平台待配置',
},
],
integrations: {
jenkins: 'warning',
gitea: 'warning',
wecom: 'warning',
llm: 'warning',
},
},
{
key: 'devops-platform-web',
name: 'devops-platform-web',
repository: 'https://gitea.ops.mrzhan.top/devops-platform/devops-platform-web.git',
owner: 'devops-platform',
repo: 'devops-platform-web',
serviceType: 'DevOps 前端',
defaultBranch: 'master',
tagPolicy: '合并到 master 即发布 devops.mrzhan.top Web',
environments: [
{
name: 'production',
label: '生产环境',
branchPolicy: 'master',
status: 'idle',
version: 'master@local',
lastRunId: '-',
lastReleasedAt: '-',
lastActor: '-',
jenkinsJob: 'devops-platform/prod-devops-platform-web-master',
wecomRobot: '通知平台待配置',
},
],
integrations: {
jenkins: 'warning',
gitea: 'warning',
wecom: 'warning',
llm: 'warning',
},
},
]
const baseSteps: PipelineStep[] = [
{
id: 'step-submit',
bpmnNodeId: 'start-release',
name: '提交发布单',
owner: 'Platform',
status: 'success',
duration: '2s',
summary: '记录操作者、项目、环境、ref 与备注。',
},
{
id: 'step-gitea',
bpmnNodeId: 'gitea-check',
name: '校验 Gitea ref',
owner: 'Gitea',
status: 'success',
duration: '5s',
summary: '验证 ref 存在并读取最近 commit 摘要。',
},
{
id: 'step-jenkins',
bpmnNodeId: 'jenkins-build',
name: '触发 Jenkins 构建',
owner: 'Jenkins',
status: 'running',
duration: '2m 18s',
summary: '进入 queue 后解析 build number,等待构建完成。',
},
{
id: 'step-health',
bpmnNodeId: 'health-check',
name: '环境健康检查',
owner: 'HealthCheck',
status: 'pending',
duration: '-',
summary: '构建成功后检查服务可达性和关键接口。',
},
{
id: 'step-wecom',
bpmnNodeId: 'notify-wecom',
name: '发送通知',
owner: 'Notification',
status: 'pending',
duration: '-',
summary: '按开始、成功、失败、取消模板投递到已配置通知平台。',
},
{
id: 'step-archive',
bpmnNodeId: 'finish-release',
name: '归档审计',
owner: 'Platform',
status: 'pending',
duration: '-',
summary: '保存 run、step、审计日志和通知投递结果。',
},
]
const successSteps = baseSteps.map((step) => ({
...step,
status: 'success' as const,
duration: step.duration === '-' ? '3s' : step.duration,
}))
const failedSteps: PipelineStep[] = baseSteps.map((step) => {
if (step.bpmnNodeId === 'jenkins-build') {
return {
...step,
status: 'failed',
duration: '1m 44s',
summary: 'pnpm build 失败,mock 日志提示类型检查未通过。',
}
}
if (['health-check', 'notify-wecom', 'finish-release'].includes(step.bpmnNodeId)) {
return {
...step,
status: 'skipped',
summary: '上游构建失败,当前节点跳过。',
}
}
return {
...step,
status: 'success',
}
})
export const runs: ReleaseRun[] = [
{
id: 'run-1052',
projectKey: 'access-manage',
projectName: 'access-manage',
environment: 'production',
ref: 'v2026.06.11-api.1',
status: 'running',
actor: 'ops-admin',
startedAt: '2026-06-11 17:12',
duration: '2m 25s',
note: '生产 API 修复发布,等待健康检查。',
jenkinsBuildUrl: 'https://jenkins.ops.mrzhan.top/job/my-project/job/access-manage-prod/1052',
commitSummary: 'fix: stabilize access policy migration and health output',
steps: baseSteps,
},
{
id: 'run-1050',
projectKey: 'role-user',
projectName: 'role-user',
environment: 'test',
ref: 'develop@4a09fd2',
status: 'failed',
actor: 'frontend-worker',
startedAt: '2026-06-11 16:24',
duration: '1m 56s',
note: '测试环境自动部署失败,等待诊断。',
jenkinsBuildUrl: 'https://jenkins.ops.mrzhan.top/job/my-project/job/role-user-test/1050',
commitSummary: 'feat: add employee workbench filters',
steps: failedSteps,
},
{
id: 'run-1048',
projectKey: 'access-manage',
projectName: 'access-manage',
environment: 'test',
ref: 'develop@8f31a2c',
status: 'success',
actor: 'ops-admin',
startedAt: '2026-06-11 15:10',
duration: '4m 03s',
note: '测试环境部署成功。',
jenkinsBuildUrl: 'https://jenkins.ops.mrzhan.top/job/my-project/job/access-manage-test/1048',
commitSummary: 'chore: update deployment env schema',
steps: successSteps,
},
]
export const agentCards: AgentCard[] = [
{
mode: 'risk',
title: '发布风险摘要',
description: '基于 ref diff、项目配置、历史失败记录生成检查项。',
inputScope: ['项目', '环境', 'ref/tag', '最近失败 run'],
outputPreview: ['配置变更影响范围', '构建和迁移风险', '发布前人工确认项'],
},
{
mode: 'diagnosis',
title: '失败诊断',
description: '只读取 Jenkins 日志片段、run steps 和已知 runbook。',
inputScope: ['失败 run', '失败节点', '脱敏日志片段'],
outputPreview: ['失败原因摘要', '下一步排查命令', '建议责任边界'],
},
{
mode: 'runbook',
title: 'Runbook 问答',
description: '围绕平台文档、项目 README、历史发布记录回答运维问题。',
inputScope: ['白名单文档', '项目', '环境'],
outputPreview: ['操作步骤', '风险提醒', '需要升级给后端的事项'],
},
{
mode: 'releaseNotes',
title: '发布说明草稿',
description: '根据 commit、PR 和 tag 生成生产发布说明草稿。',
inputScope: ['commit 列表', 'PR 信息', 'tag'],
outputPreview: ['变更摘要', '验证范围', '回滚提示'],
},
]
export const settings: SystemSetting[] = [
{
key: 'api-contract',
name: 'API 契约来源',
status: 'warning',
owner: '后端 OpenAPI',
contract: '本阶段使用 mock/devops.ts,联调后切换到后端 Swagger/OpenAPI。',
boundary: '前端不写死真实 token,不直接访问 Jenkins/Gitea/LLM。',
},
{
key: 'bpmn',
name: 'BPMN 流程定义',
status: 'healthy',
owner: '平台流程配置',
contract: 'step.bpmnNodeId 必须和 BPMN XML 节点 id 一致。',
boundary: '前端只展示流程和状态,不推进状态机。',
},
{
key: 'agent',
name: 'Agent 边界',
status: 'warning',
owner: '后端 Agent proxy',
contract: '只支持发布风险、失败诊断、Runbook、发布说明四类用途。',
boundary: '无泛聊天入口;模型 key/baseURL/model 仅后端保存。',
},
{
key: 'notification',
name: '通知平台',
status: 'warning',
owner: '后端 notification adapter',
contract: '通知模板由后端返回,后续可接企微、飞书或通用 webhook。',
boundary: 'webhook 不进入前端,不在浏览器保存。',
},
]
export const dashboardSummary: DashboardSummary = {
integrations,
projects,
runs,
agentCards,
settings,
integrationConfig: integrationConfigStatus,
}
+159 -247
View File
@@ -1,21 +1,9 @@
/** /**
* 前端唯一 DevOps API 入口;优先联调后端,失败时降级到本地 mock 保持页面可用 * 前端唯一 DevOps API 入口;所有运维数据都来自后端真实接口
*/ */
import {
agentCards,
dashboardSummary,
integrationConfigStatus as mockIntegrationConfigStatus,
integrations,
projects as mockProjects,
releaseProcessXml,
runs as mockRuns,
settings,
} from '../mocks/devops'
import type { import type {
AgentCard, AgentCard,
AgentInvocation, AgentInvocation,
AgentMode,
AgentPurpose,
CreateAgentInvocationPayload, CreateAgentInvocationPayload,
CreateDeployRunPayload, CreateDeployRunPayload,
DashboardSummary, DashboardSummary,
@@ -36,7 +24,7 @@ import type {
} from '../types/devops' } from '../types/devops'
import { http } from './http' import { http } from './http'
type DataSource = 'backend' | 'mock' type DataSource = 'backend'
type BackendHealthStatus = 'ok' | 'not_configured' | 'unavailable' type BackendHealthStatus = 'ok' | 'not_configured' | 'unavailable'
type BackendRunStatus = 'pending' | 'queued' | 'running' | 'success' | 'failed' | 'canceled' type BackendRunStatus = 'pending' | 'queued' | 'running' | 'success' | 'failed' | 'canceled'
@@ -57,6 +45,8 @@ interface BackendProject {
name: EnvironmentName name: EnvironmentName
displayName: string displayName: string
jenkinsJobPath: string jenkinsJobPath: string
releasePolicy?: 'branch' | 'tag' | 'master'
refPattern?: string
}> }>
} }
@@ -108,9 +98,7 @@ interface BackendProcessDefinition {
nodeIds: string[] nodeIds: string[]
} }
let lastDataSource: DataSource = 'mock' let lastDataSource: DataSource = 'backend'
const wait = async () => new Promise((resolve) => window.setTimeout(resolve, 180))
const integrationConfigKeys: IntegrationConfigKey[] = [ const integrationConfigKeys: IntegrationConfigKey[] = [
'jenkins', 'jenkins',
'gitea', 'gitea',
@@ -123,21 +111,86 @@ const integrationConfigKeys: IntegrationConfigKey[] = [
] ]
const redactedVariableName = '非变量名内容已隐藏' const redactedVariableName = '非变量名内容已隐藏'
const agentCards: AgentCard[] = [
{
mode: 'risk',
title: '发布风险摘要',
description: '基于 ref diff、项目配置、历史失败记录生成检查项。',
inputScope: ['项目', '环境', 'ref/tag', '最近失败 run'],
outputPreview: ['配置变更影响范围', '构建和迁移风险', '发布前人工确认项'],
},
{
mode: 'diagnosis',
title: '失败诊断',
description: '只读取 Jenkins 日志片段、run steps 和已知 runbook。',
inputScope: ['失败 run', '失败节点', '脱敏日志片段'],
outputPreview: ['失败原因摘要', '下一步排查命令', '建议责任边界'],
},
{
mode: 'runbook',
title: 'Runbook 问答',
description: '围绕平台文档、项目 README、历史发布记录回答运维问题。',
inputScope: ['白名单文档', '项目', '环境'],
outputPreview: ['操作步骤', '风险提醒', '需要升级给后端的事项'],
},
{
mode: 'releaseNotes',
title: '发布说明草稿',
description: '根据 commit、PR 和 tag 生成生产发布说明草稿。',
inputScope: ['commit 列表', 'PR 信息', 'tag'],
outputPreview: ['变更摘要', '验证范围', '回滚提示'],
},
]
const settings: SystemSetting[] = [
{
key: 'api-contract',
name: 'API 契约来源',
status: 'warning',
owner: '后端 OpenAPI',
contract: '所有项目、发布、refs、流程和集成状态均通过 devops-platform-api 获取。',
boundary: '前端不写死真实 token,不直接访问 Jenkins/Gitea/LLM。',
},
{
key: 'bpmn',
name: 'BPMN 流程定义',
status: 'healthy',
owner: '平台流程配置',
contract: 'step.bpmnNodeId 必须和 BPMN XML 节点 id 一致。',
boundary: '前端只展示流程和状态,不推进状态机。',
},
{
key: 'agent',
name: 'Agent 边界',
status: 'warning',
owner: '后端 Agent proxy',
contract: '只支持发布风险、失败诊断、Runbook、发布说明四类用途。',
boundary: '无泛聊天入口;模型 key/baseURL/model 仅后端保存。',
},
{
key: 'notification',
name: '通知平台',
status: 'warning',
owner: '后端 notification adapter',
contract: '通知模板由后端返回,后续可接企微、飞书或通用 webhook。',
boundary: 'webhook 不进入前端,不在浏览器保存。',
},
]
export const devopsApi = { export const devopsApi = {
getLastDataSource(): DataSource { getLastDataSource(): DataSource {
return lastDataSource return lastDataSource
}, },
async getDashboard(): Promise<DashboardSummary> { async getDashboard(): Promise<DashboardSummary> {
return withBackendFallback(
async () => {
const [health, backendProjects, backendRuns, integrationConfig] = await Promise.all([ const [health, backendProjects, backendRuns, integrationConfig] = await Promise.all([
fetchHealth(), fetchHealth(),
fetchProjects(), fetchProjects(),
fetchRuns(), fetchRuns(),
fetchIntegrationConfigStatusWithFallback(), fetchIntegrationConfigStatus(),
]) ])
const adaptedProjects = toProjectConfigs(backendProjects) const adaptedProjects = toProjectConfigs(backendProjects)
lastDataSource = 'backend'
return { return {
integrations: toIntegrationStatuses(health), integrations: toIntegrationStatuses(health),
@@ -148,196 +201,105 @@ export const devopsApi = {
integrationConfig, integrationConfig,
} }
}, },
() => dashboardSummary,
)
},
async getIntegrations(): Promise<IntegrationStatus[]> { async getIntegrations(): Promise<IntegrationStatus[]> {
return withBackendFallback( lastDataSource = 'backend'
async () => toIntegrationStatuses(await fetchHealth()), return toIntegrationStatuses(await fetchHealth())
() => integrations,
)
}, },
async getProjects(): Promise<ProjectConfig[]> { async getProjects(): Promise<ProjectConfig[]> {
return withBackendFallback( lastDataSource = 'backend'
async () => toProjectConfigs(await fetchProjects()), return toProjectConfigs(await fetchProjects())
() => mockProjects,
)
}, },
async getProjectRefs(projectKey: string, projectList = mockProjects): Promise<ProjectRefs> { async getProjectRefs(projectKey: string): Promise<ProjectRefs> {
return withBackendFallback(
async () => {
const { data } = await http.get<ProjectRefs>(`/projects/${projectKey}/refs`) const { data } = await http.get<ProjectRefs>(`/projects/${projectKey}/refs`)
lastDataSource = 'backend'
return data return data
}, },
() => createFallbackRefs(projectKey, projectList),
)
},
async getRuns(projectList = mockProjects): Promise<ReleaseRun[]> { async getRuns(projectList: ProjectConfig[] = []): Promise<ReleaseRun[]> {
return withBackendFallback( lastDataSource = 'backend'
async () => (await fetchRuns()).map((run) => toReleaseRun(run, projectList)), return (await fetchRuns()).map((run) => toReleaseRun(run, projectList))
() => mockRuns,
)
}, },
async createDeployRun( async createDeployRun(
payload: CreateDeployRunPayload, payload: CreateDeployRunPayload,
projectList = mockProjects, projectList: ProjectConfig[] = [],
): Promise<ReleaseRun> { ): Promise<ReleaseRun> {
return withBackendFallback(
async () => {
const { data } = await http.post<BackendDeployRun>('/deploy-runs', payload) const { data } = await http.post<BackendDeployRun>('/deploy-runs', payload)
lastDataSource = 'backend'
return toReleaseRun(data, projectList) return toReleaseRun(data, projectList)
}, },
() => createFallbackRun(payload, projectList, 'queued'),
)
},
async cancelDeployRun(runId: string, source?: ReleaseRun): Promise<ReleaseRun> { async cancelDeployRun(
return withBackendFallback( runId: string,
async () => { projectList: ProjectConfig[] = [],
): Promise<ReleaseRun> {
const { data } = await http.post<BackendDeployRun>(`/deploy-runs/${runId}/cancel`) const { data } = await http.post<BackendDeployRun>(`/deploy-runs/${runId}/cancel`)
return toReleaseRun(data, mockProjects) lastDataSource = 'backend'
}, return toReleaseRun(data, projectList)
() => ({
...(source ?? mockRuns[0]),
id: source?.id ?? runId,
status: 'canceled',
duration: source?.duration ?? '-',
steps: (source?.steps ?? mockRuns[0].steps).map((step) => ({
...step,
status: step.status === 'success' ? 'success' : 'canceled',
summary: step.status === 'success' ? step.summary : '人工取消,本节点未继续执行。',
})),
}),
)
}, },
async retryDeployRun(runId: string, source?: ReleaseRun): Promise<ReleaseRun> { async retryDeployRun(
return withBackendFallback( runId: string,
async () => { projectList: ProjectConfig[] = [],
): Promise<ReleaseRun> {
const { data } = await http.post<BackendDeployRun>(`/deploy-runs/${runId}/retry`) const { data } = await http.post<BackendDeployRun>(`/deploy-runs/${runId}/retry`)
return toReleaseRun(data, mockProjects) lastDataSource = 'backend'
}, return toReleaseRun(data, projectList)
() =>
createFallbackRun(
{
projectKey: source?.projectKey ?? 'access-manage',
environment: source?.environment ?? 'test',
ref: source?.ref ?? 'develop',
operator: source?.actor ?? 'ops-admin',
remark: source ? `重试 ${source.id}` : `重试 ${runId}`,
},
mockProjects,
'queued',
),
)
}, },
async syncJenkinsRun( async syncJenkinsRun(
runId: string, runId: string,
source?: ReleaseRun, projectList: ProjectConfig[] = [],
projectList = mockProjects,
): Promise<ReleaseRun> { ): Promise<ReleaseRun> {
return withBackendFallback(
async () => {
const { data } = await http.post<BackendDeployRun>(`/deploy-runs/${runId}/sync-jenkins`) const { data } = await http.post<BackendDeployRun>(`/deploy-runs/${runId}/sync-jenkins`)
lastDataSource = 'backend'
return toReleaseRun(data, projectList) return toReleaseRun(data, projectList)
}, },
() => source ?? mockRuns.find((run) => run.id === runId) ?? mockRuns[0],
)
},
async syncJenkinsRuns(projectList = mockProjects): Promise<JenkinsSyncSummary> { async syncJenkinsRuns(projectList: ProjectConfig[] = []): Promise<JenkinsSyncSummary> {
return withBackendFallback(
async () => {
const { data } = await http.post<BackendJenkinsSyncSummary>( const { data } = await http.post<BackendJenkinsSyncSummary>(
'/deploy-runs/sync-jenkins', '/deploy-runs/sync-jenkins',
) )
lastDataSource = 'backend'
return toJenkinsSyncSummary(data, projectList) return toJenkinsSyncSummary(data, projectList)
}, },
() => ({
checkedAt: new Date().toISOString(),
totalCandidates: 0,
syncedRuns: [],
failedRuns: [],
}),
)
},
async getAgentCards(): Promise<AgentCard[]> { async getAgentCards(): Promise<AgentCard[]> {
await wait()
return agentCards return agentCards
}, },
async getIntegrationConfigStatus(): Promise<IntegrationConfigStatus> { async getIntegrationConfigStatus(): Promise<IntegrationConfigStatus> {
return withBackendFallback( lastDataSource = 'backend'
async () => fetchIntegrationConfigStatus(), return fetchIntegrationConfigStatus()
() => mockIntegrationConfigStatus,
)
}, },
async createAgentInvocation( async createAgentInvocation(
payload: CreateAgentInvocationPayload, payload: CreateAgentInvocationPayload,
): Promise<AgentInvocation> { ): Promise<AgentInvocation> {
return withBackendFallback(
async () => {
const { data } = await http.post<AgentInvocation>('/agent/invocations', payload) const { data } = await http.post<AgentInvocation>('/agent/invocations', payload)
lastDataSource = 'backend'
return data return data
}, },
() => createFallbackAgentInvocation(payload),
)
},
async getSettings(): Promise<SystemSetting[]> { async getSettings(): Promise<SystemSetting[]> {
return withBackendFallback(
async () => {
const [health, integrationConfig] = await Promise.all([ const [health, integrationConfig] = await Promise.all([
fetchHealth(), fetchHealth(),
fetchIntegrationConfigStatusWithFallback(), fetchIntegrationConfigStatus(),
]) ])
lastDataSource = 'backend'
return toSystemSettings(health, integrationConfig) return toSystemSettings(health, integrationConfig)
}, },
() => settings,
)
},
async getReleaseProcessXml(): Promise<string> { async getReleaseProcessXml(): Promise<string> {
return withBackendFallback(
async () => {
const { data } = await http.get<BackendProcessDefinition>( const { data } = await http.get<BackendProcessDefinition>(
'/process-definitions/release', '/process-definitions/release',
) )
lastDataSource = 'backend'
return data.xml return data.xml
}, },
() => releaseProcessXml,
)
},
}
async function withBackendFallback<T>(
request: () => Promise<T>,
fallback: () => T | Promise<T>,
): Promise<T> {
if (import.meta.env.VITE_DEVOPS_USE_MOCK === 'true') {
lastDataSource = 'mock'
await wait()
return fallback()
}
try {
const result = await request()
lastDataSource = 'backend'
return result
} catch {
lastDataSource = 'mock'
await wait()
return fallback()
}
} }
async function fetchHealth(): Promise<BackendHealth> { async function fetchHealth(): Promise<BackendHealth> {
@@ -360,14 +322,6 @@ async function fetchIntegrationConfigStatus(): Promise<IntegrationConfigStatus>
return sanitizeIntegrationConfigStatus(data) return sanitizeIntegrationConfigStatus(data)
} }
async function fetchIntegrationConfigStatusWithFallback(): Promise<IntegrationConfigStatus> {
try {
return await fetchIntegrationConfigStatus()
} catch {
return mockIntegrationConfigStatus
}
}
function toIntegrationStatuses(health: BackendHealth): IntegrationStatus[] { function toIntegrationStatuses(health: BackendHealth): IntegrationStatus[] {
return [ return [
toIntegrationStatus('jenkins', 'Jenkins', health), toIntegrationStatus('jenkins', 'Jenkins', health),
@@ -464,7 +418,6 @@ function sanitizeIntegrationNote(note?: string): string | undefined {
function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] { function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
return backendProjects.map((project) => { return backendProjects.map((project) => {
const mockProject = mockProjects.find((item) => item.key === project.key)
const repo = parseRepository(project.repositoryUrl, project.key) const repo = parseRepository(project.repositoryUrl, project.key)
return { return {
@@ -473,30 +426,24 @@ function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
repository: project.repositoryUrl, repository: project.repositoryUrl,
owner: repo.owner, owner: repo.owner,
repo: repo.repo, repo: repo.repo,
serviceType: mockProject?.serviceType ?? '服务', serviceType: serviceTypeLabel(project.key),
defaultBranch: project.defaultBranch, defaultBranch: project.defaultBranch,
tagPolicy: mockProject?.tagPolicy ?? '生产发布优先选择 v* tag', tagPolicy: tagPolicyLabel(project.environments),
environments: project.environments.map((environment) => { environments: project.environments.map((environment) => {
const mockEnvironment = mockProject?.environments.find(
(item) => item.name === environment.name,
)
return { return {
name: environment.name, name: environment.name,
label: environment.displayName, label: environment.displayName,
branchPolicy: branchPolicy: releasePolicyLabel(environment.releasePolicy, project.defaultBranch),
mockEnvironment?.branchPolicy ?? status: 'idle' as const,
(environment.name === 'production' ? 'v* tag' : project.defaultBranch), version: '-',
status: mockEnvironment?.status ?? 'idle', lastRunId: '-',
version: mockEnvironment?.version ?? '-', lastReleasedAt: '-',
lastRunId: mockEnvironment?.lastRunId ?? '-', lastActor: '-',
lastReleasedAt: mockEnvironment?.lastReleasedAt ?? '-',
lastActor: mockEnvironment?.lastActor ?? '-',
jenkinsJob: environment.jenkinsJobPath, jenkinsJob: environment.jenkinsJobPath,
wecomRobot: mockEnvironment?.wecomRobot ?? '后端通知配置', wecomRobot: '后端通知配置',
} }
}), }),
integrations: mockProject?.integrations ?? { integrations: {
jenkins: 'warning', jenkins: 'warning',
gitea: 'warning', gitea: 'warning',
wecom: 'warning', wecom: 'warning',
@@ -571,66 +518,6 @@ function toPipelineStep(step: BackendDeployRunStep): PipelineStep {
} }
} }
function createFallbackRun(
payload: CreateDeployRunPayload,
projectList: ProjectConfig[],
status: ReleaseStatus,
): ReleaseRun {
const project = projectList.find((item) => item.key === payload.projectKey)
const now = new Date().toISOString()
return {
id: payload.idempotencyKey || `local-${Date.now()}`,
projectKey: payload.projectKey,
projectName: project?.name ?? payload.projectKey,
environment: payload.environment,
ref: payload.ref,
status,
actor: payload.operator,
startedAt: formatDateTime(now),
duration: status === 'queued' ? '等待 Jenkins queue' : '-',
note: payload.remark || '本地降级发布单',
jenkinsSummary: '等待 Jenkins 返回',
jenkinsBuildUrl: '',
commitSummary: '本地降级:后端不可用时保留操作上下文',
steps: mockRuns[0].steps.map((step) => ({
...step,
status: step.bpmnNodeId === 'start-release' ? 'success' : 'pending',
})),
}
}
function createFallbackAgentInvocation(
payload: CreateAgentInvocationPayload,
): AgentInvocation {
return {
id: `local-agent-${Date.now()}`,
runId: payload.runId,
projectKey: payload.projectKey,
type: payload.type,
status: 'success',
promptSummary: payload.promptSummary,
resultMarkdown: fallbackAgentResult(payload.type),
createdAt: new Date().toISOString(),
}
}
function createFallbackRefs(projectKey: string, projectList: ProjectConfig[]): ProjectRefs {
const project = projectList.find((item) => item.key === projectKey)
const defaultBranch = project?.defaultBranch ?? 'develop'
return {
source: 'mock',
branches: Array.from(new Set([defaultBranch, 'develop', 'master']))
.filter(Boolean)
.map((name) => ({ name })),
tags: [
{ name: 'v2026.06.05-envsplit.7' },
{ name: 'v2026.06.11-api.1' },
],
}
}
function toHealthState(status: BackendHealthStatus): HealthState { function toHealthState(status: BackendHealthStatus): HealthState {
if (status === 'ok') { if (status === 'ok') {
return 'healthy' return 'healthy'
@@ -760,6 +647,47 @@ function parseRepository(repositoryUrl: string, fallbackRepo: string) {
} }
} }
function serviceTypeLabel(projectKey: string): string {
if (projectKey.endsWith('-web') || projectKey === 'role-admin' || projectKey === 'role-user') {
return '前端应用'
}
if (projectKey === 'my-resume') {
return '静态站点'
}
return '后端服务'
}
function tagPolicyLabel(environments: BackendProject['environments']): string {
const production = environments.find((environment) => environment.name === 'production')
if (production?.releasePolicy === 'master') {
return '生产发布固定 master'
}
if (production?.releasePolicy === 'tag') {
return '生产发布优先选择 v* tag'
}
return '按环境发布策略执行'
}
function releasePolicyLabel(
policy: BackendProject['environments'][number]['releasePolicy'],
defaultBranch: string,
): string {
if (policy === 'tag') {
return 'v* tag'
}
if (policy === 'master') {
return 'master'
}
return defaultBranch
}
function formatDateTime(value?: string): string { function formatDateTime(value?: string): string {
if (!value) { if (!value) {
return '-' return '-'
@@ -814,7 +742,7 @@ function formatDuration(
function toSystemSettings( function toSystemSettings(
health: BackendHealth, health: BackendHealth,
integrationConfig = mockIntegrationConfigStatus, integrationConfig: IntegrationConfigStatus,
): SystemSetting[] { ): SystemSetting[] {
const integrationByKey = new Map( const integrationByKey = new Map(
integrationConfig.integrations.map((item) => [item.key, item]), integrationConfig.integrations.map((item) => [item.key, item]),
@@ -825,7 +753,7 @@ function toSystemSettings(
return { return {
...item, ...item,
status: toHealthState(health.status), status: toHealthState(health.status),
contract: '已优先读取 devops-platform-api 的统一 envelope;后端不可用时降级到本地 mock。', contract: '已通过 devops-platform-api 的统一 envelope 读取真实接口数据。',
} }
} }
@@ -860,19 +788,3 @@ function toHealthStateFromConfig(state?: IntegrationConfigState): HealthState {
return 'error' return 'error'
} }
function fallbackAgentResult(type: AgentPurpose): string {
const mode: Record<AgentPurpose, AgentMode> = {
'release-risk': 'risk',
'failure-diagnosis': 'diagnosis',
'runbook-qa': 'runbook',
'release-note': 'releaseNotes',
'incident-review': 'diagnosis',
}
const card = agentCards.find((item) => item.mode === mode[type])
return [
`${card?.title ?? 'Agent'} 本地降级结果`,
'后端或 LLM 未配置时,前端只保留 DevOps 场景输入,不保存任何模型密钥。',
].join('\n\n')
}
+6 -6
View File
@@ -14,11 +14,11 @@ import type {
} from '../types/devops' } from '../types/devops'
/** /**
* 平台运行态聚合后端和 mock 降级数据,页面只消费统一后的展示模型。 * 平台运行态聚合后端真实接口数据,页面只消费统一后的展示模型。
*/ */
export const usePlatformStore = defineStore('platform', () => { export const usePlatformStore = defineStore('platform', () => {
const loading = ref(false) const loading = ref(false)
const dataSource = ref<'backend' | 'mock'>('mock') const dataSource = ref<'backend'>('backend')
const projects = ref<ProjectConfig[]>([]) const projects = ref<ProjectConfig[]>([])
const runs = ref<ReleaseRun[]>([]) const runs = ref<ReleaseRun[]>([])
const integrations = ref<IntegrationStatus[]>([]) const integrations = ref<IntegrationStatus[]>([])
@@ -61,21 +61,21 @@ export const usePlatformStore = defineStore('platform', () => {
} }
async function cancelDeployRun(run: ReleaseRun) { async function cancelDeployRun(run: ReleaseRun) {
const canceled = await devopsApi.cancelDeployRun(run.id, run) const canceled = await devopsApi.cancelDeployRun(run.id, projects.value)
upsertRun(canceled) upsertRun(canceled)
dataSource.value = devopsApi.getLastDataSource() dataSource.value = devopsApi.getLastDataSource()
return canceled return canceled
} }
async function retryDeployRun(run: ReleaseRun) { async function retryDeployRun(run: ReleaseRun) {
const retried = await devopsApi.retryDeployRun(run.id, run) const retried = await devopsApi.retryDeployRun(run.id, projects.value)
upsertRun(retried) upsertRun(retried)
dataSource.value = devopsApi.getLastDataSource() dataSource.value = devopsApi.getLastDataSource()
return retried return retried
} }
async function syncJenkinsRun(run: ReleaseRun) { async function syncJenkinsRun(run: ReleaseRun) {
const synced = await devopsApi.syncJenkinsRun(run.id, run, projects.value) const synced = await devopsApi.syncJenkinsRun(run.id, projects.value)
upsertRun(synced) upsertRun(synced)
dataSource.value = devopsApi.getLastDataSource() dataSource.value = devopsApi.getLastDataSource()
return synced return synced
@@ -89,7 +89,7 @@ export const usePlatformStore = defineStore('platform', () => {
} }
async function loadProjectRefs(projectKey: string) { async function loadProjectRefs(projectKey: string) {
const refs = await devopsApi.getProjectRefs(projectKey, projects.value) const refs = await devopsApi.getProjectRefs(projectKey)
projectRefs.value = { projectRefs.value = {
...projectRefs.value, ...projectRefs.value,
[projectKey]: refs, [projectKey]: refs,
+2 -2
View File
@@ -4,13 +4,13 @@ import { defineStore } from 'pinia'
export const useSessionStore = defineStore('session', () => { export const useSessionStore = defineStore('session', () => {
const operator = ref('ops-admin') const operator = ref('ops-admin')
const role = ref<'admin' | 'operator' | 'viewer'>('admin') const role = ref<'admin' | 'operator' | 'viewer'>('admin')
const token = ref('mock-session') const token = ref('local-session')
const isAuthenticated = computed(() => Boolean(token.value)) const isAuthenticated = computed(() => Boolean(token.value))
function login(name: string) { function login(name: string) {
operator.value = name || 'ops-admin' operator.value = name || 'ops-admin'
token.value = 'mock-session' token.value = 'local-session'
} }
function logout() { function logout() {
+1 -1
View File
@@ -106,7 +106,7 @@ export interface ProjectConfig {
} }
export interface ProjectRefs { export interface ProjectRefs {
source: 'gitea' | 'fallback' | 'mock' source: 'gitea' | 'fallback'
branches: Array<{ branches: Array<{
name: string name: string
commitSha?: string commitSha?: string
+2 -2
View File
@@ -10,7 +10,7 @@ const session = useSessionStore()
const form = reactive({ const form = reactive({
operator: 'ops-admin', operator: 'ops-admin',
password: 'mock-password', password: '',
}) })
function submit() { function submit() {
@@ -26,7 +26,7 @@ function submit() {
<div class="login-copy"> <div class="login-copy">
<ElIcon><Monitor /></ElIcon> <ElIcon><Monitor /></ElIcon>
<h1>DevOps 运维平台</h1> <h1>DevOps 运维平台</h1>
<p>第一阶段使用 mock session仅用于本地控制台联调真实身份RBAC 和审计由后端接入后替换</p> <p>当前使用本地操作员会话进入控制台真实身份RBAC 和审计由后端认证模块接入</p>
</div> </div>
<ElForm class="login-form" label-position="top" @submit.prevent="submit"> <ElForm class="login-form" label-position="top" @submit.prevent="submit">
<ElFormItem label="操作员"> <ElFormItem label="操作员">
+2 -2
View File
@@ -25,7 +25,7 @@ const runningCount = computed(() => platform.runningRuns.length)
<div class="status-grid"> <div class="status-grid">
<MetricTile label="纳管项目" :value="platform.projects.length" caption="含 DevOps 平台自管理项目" :icon="Cpu" /> <MetricTile label="纳管项目" :value="platform.projects.length" caption="含 DevOps 平台自管理项目" :icon="Cpu" />
<MetricTile label="运行中发布" :value="runningCount" caption="当前 Jenkins 或健康检查处理中" :icon="Finished" tone="warning" /> <MetricTile label="运行中发布" :value="runningCount" caption="当前 Jenkins 或健康检查处理中" :icon="Finished" tone="warning" />
<MetricTile label="成功记录" :value="successCount" caption="最近 mock run 成功数" :icon="CircleCheck" tone="success" /> <MetricTile label="成功记录" :value="successCount" caption="最近发布成功数" :icon="CircleCheck" tone="success" />
<MetricTile label="失败提醒" :value="failedCount" caption="需要诊断或重试的 run" :icon="Failed" tone="danger" /> <MetricTile label="失败提醒" :value="failedCount" caption="需要诊断或重试的 run" :icon="Failed" tone="danger" />
</div> </div>
@@ -33,7 +33,7 @@ const runningCount = computed(() => platform.runningRuns.length)
<section class="panel"> <section class="panel">
<div class="panel-header"> <div class="panel-header">
<h2>集成状态</h2> <h2>集成状态</h2>
<span>{{ platform.dataSource === 'backend' ? '来自后端 health' : '后端不可用,当前使用 mock' }}</span> <span>来自后端 health</span>
</div> </div>
<div class="panel-body compact-list"> <div class="panel-body compact-list">
<div v-for="item in platform.integrations" :key="item.key" class="list-row"> <div v-for="item in platform.integrations" :key="item.key" class="list-row">
+2 -2
View File
@@ -10,7 +10,7 @@ const platform = usePlatformStore()
<div class="page-header"> <div class="page-header">
<div> <div>
<h1>项目配置</h1> <h1>项目配置</h1>
<p>第一版展示项目环境Jenkins JobGitea 仓库和通知通道绑定关系</p> <p>展示后端返回的项目环境Jenkins JobGitea 仓库和通知通道绑定关系</p>
</div> </div>
<ElButton type="primary" plain disabled>新增项目</ElButton> <ElButton type="primary" plain disabled>新增项目</ElButton>
</div> </div>
@@ -18,7 +18,7 @@ const platform = usePlatformStore()
<section class="panel"> <section class="panel">
<div class="panel-header"> <div class="panel-header">
<h2>项目清单</h2> <h2>项目清单</h2>
<span>{{ platform.dataSource === 'backend' ? '由后端 projects 接口返回' : '当前使用本地 mock 配置' }}</span> <span>由后端 projects 接口返回</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<ElTable :data="platform.projects" row-key="key"> <ElTable :data="platform.projects" row-key="key">
+8 -11
View File
@@ -33,10 +33,7 @@ const environmentOptions = computed(() => {
const environments = selectedProject.value?.environments ?? [] const environments = selectedProject.value?.environments ?? []
if (!environments.length) { if (!environments.length) {
return [ return []
{ label: '测试环境', value: 'test' },
{ label: '生产环境', value: 'production' },
]
} }
return environments.map((environment) => ({ return environments.map((environment) => ({
@@ -57,10 +54,10 @@ const refOptions = computed(() => {
const refs = currentRefs.value const refs = currentRefs.value
if (form.environment === 'production' && !useBranchRefs.value) { if (form.environment === 'production' && !useBranchRefs.value) {
return refs?.tags.map((tag) => tag.name) ?? ['v2026.06.05-envsplit.7'] return refs?.tags.map((tag) => tag.name) ?? []
} }
return refs?.branches.map((branch) => branch.name) ?? [selectedProject.value?.defaultBranch ?? 'develop'] return refs?.branches.map((branch) => branch.name) ?? []
}) })
const refSourceText = computed(() => { const refSourceText = computed(() => {
@@ -74,7 +71,7 @@ const refSourceText = computed(() => {
return '后端 fallback refs' return '后端 fallback refs'
} }
return '本地 mock refs' return '等待后端 refs'
}) })
const canTrigger = computed(() => { const canTrigger = computed(() => {
@@ -106,7 +103,7 @@ async function triggerRelease() {
}) })
activeRun.value = run activeRun.value = run
localStatus.value = run.status localStatus.value = run.status
ElMessage.success(platform.dataSource === 'backend' ? '发布单已提交到后端' : '后端不可用,已生成本地发布单') ElMessage.success('发布单已提交到后端')
} finally { } finally {
submitting.value = false submitting.value = false
} }
@@ -138,7 +135,7 @@ async function retryRelease() {
const run = await platform.retryDeployRun(shownRun.value) const run = await platform.retryDeployRun(shownRun.value)
activeRun.value = run activeRun.value = run
localStatus.value = run.status localStatus.value = run.status
ElMessage.success(platform.dataSource === 'backend' ? '已提交重试发布单' : '后端不可用,已生成本地重试单') ElMessage.success('已提交重试发布单')
} finally { } finally {
submitting.value = false submitting.value = false
} }
@@ -154,7 +151,7 @@ async function syncJenkinsRun() {
const run = await platform.syncJenkinsRun(shownRun.value) const run = await platform.syncJenkinsRun(shownRun.value)
activeRun.value = run activeRun.value = run
localStatus.value = run.status localStatus.value = run.status
ElMessage.success(platform.dataSource === 'backend' ? '已同步 Jenkins 状态' : '后端不可用,保留本地状态') ElMessage.success('已同步 Jenkins 状态')
} finally { } finally {
submitting.value = false submitting.value = false
} }
@@ -225,7 +222,7 @@ watch(refOptions, (options) => {
<section class="panel"> <section class="panel">
<div class="panel-header"> <div class="panel-header">
<h2>发布参数</h2> <h2>发布参数</h2>
<span>{{ platform.dataSource === 'backend' ? '通过后端创建发布单' : '后端不可用时使用本地降级' }}</span> <span>通过后端创建发布单</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<ElForm label-position="top"> <ElForm label-position="top">
+1 -1
View File
@@ -36,7 +36,7 @@ async function syncSelectedRun() {
const run = await platform.syncJenkinsRun(selectedRun.value) const run = await platform.syncJenkinsRun(selectedRun.value)
selectedRunId.value = run.id selectedRunId.value = run.id
ElMessage.success(platform.dataSource === 'backend' ? '已同步 Jenkins 状态' : '后端不可用,保留本地状态') ElMessage.success('已同步 Jenkins 状态')
} }
async function syncAllJenkinsRuns() { async function syncAllJenkinsRuns() {
+1 -1
View File
@@ -169,7 +169,7 @@ function formatCheckedAt(value: string): string {
<strong class="mono">VITE_API_BASE_URL</strong> <strong class="mono">VITE_API_BASE_URL</strong>
<small>联调时指向后端网关未配置时默认 /api</small> <small>联调时指向后端网关未配置时默认 /api</small>
</span> </span>
<ElTag type="info" round>{{ platform.dataSource === 'backend' ? '后端已联通' : '/api 或 mock 降级' }}</ElTag> <ElTag type="info" round>后端接口</ElTag>
</div> </div>
<div class="list-row"> <div class="list-row">
<span> <span>