feat: 改为真实后端接口模式
- devopsApi: 移除前端 mock 降级和本地发布单/refs 伪造逻辑 - views: 同步发布、运行记录、配置和顶部状态文案为真实接口模式 - docs: 更新前端维护规则,禁止新增 mock 数据降级
This commit is contained in:
@@ -1,2 +1 @@
|
||||
VITE_API_BASE_URL=http://localhost:4300
|
||||
VITE_DEVOPS_USE_MOCK=false
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
- 路由使用 Vue Router。
|
||||
- 请求封装使用 Axios,并集中在 `src/services/**`。
|
||||
- BPMN 展示使用 `bpmn-js`,不手写替代渲染器。
|
||||
- mock 数据集中在 `src/mocks/**`,后续以后端 OpenAPI/Swagger 替换。
|
||||
- 不新增前端 mock 数据或接口失败降级;项目、发布、refs、流程和 Agent 调用统一走后端 API。
|
||||
|
||||
## API 与安全边界
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Vue 3 + Vite + TypeScript 实现的 DevOps 运维控制台第一阶段,用于
|
||||
## 当前范围
|
||||
|
||||
- 登录、总览、项目配置、发布中心、运行记录、系统配置,以及全局右侧 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 状态同步入口。
|
||||
- 运行记录展示 Jenkins queue/build、Build URL、开始/结束时间、执行提示、失败摘要和日志摘要。
|
||||
- BPMN Viewer 使用 `bpmn-js` 渲染后端流程 XML,并按 queued/running/success/failed/skipped/canceled 状态高亮节点。
|
||||
@@ -29,7 +29,7 @@ pnpm install
|
||||
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 契约来源
|
||||
|
||||
接口入口为 `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 为准继续收敛字段。
|
||||
|
||||
浏览器端不得保存或直连以下密钥和服务:
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
- `role-user`
|
||||
- `my-resume`
|
||||
|
||||
第一阶段为本地 mock UI,后续接入 `devops-platform-api`。
|
||||
当前前端只调用 `devops-platform-api`,不再提供本地 mock 数据降级。
|
||||
|
||||
## 页面结构
|
||||
|
||||
- `/login`:mock 登录。
|
||||
- `/login`:本地操作员会话入口,后续接后端认证。
|
||||
- `/overview`:总览,展示服务状态、最近发布和项目环境。
|
||||
- `/projects`:项目配置,展示 Gitea 仓库、Jenkins Job、企微机器人和环境策略。
|
||||
- `/releases`:发布中心,提供发布参数、二次确认、触发、取消、重试和 BPMN 流程状态。
|
||||
@@ -23,7 +23,6 @@
|
||||
|
||||
## 数据约定
|
||||
|
||||
- mock 数据入口:`src/mocks/devops.ts`
|
||||
- API 封装入口:`src/services/devopsApi.ts`
|
||||
- Axios 实例:`src/services/http.ts`
|
||||
- 平台状态:`src/stores/platform.ts`
|
||||
@@ -31,7 +30,7 @@
|
||||
|
||||
## 后续接入点
|
||||
|
||||
- 用后端 OpenAPI 替换 `devopsApi` mock 返回。
|
||||
- `devopsApi` 所有项目、发布、refs、流程和 Agent 调用均走后端接口。
|
||||
- 发布中心触发按钮接入后端发布单创建接口。
|
||||
- 运行记录接入后端 run/step 查询接口。
|
||||
- BPMN XML 由后端流程定义接口返回。
|
||||
|
||||
@@ -41,7 +41,7 @@ const jenkinsStatus = computed(() => {
|
||||
})
|
||||
|
||||
const apiModeLabel = computed(() => {
|
||||
return platform.dataSource === 'backend' ? '后端 API' : 'Mock 降级'
|
||||
return '后端 API'
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
@@ -83,7 +83,7 @@ function logout() {
|
||||
<div class="env-strip">
|
||||
<span>本地开发</span>
|
||||
<StatusPill :state="jenkinsStatus" text="Jenkins" />
|
||||
<ElTag :type="platform.dataSource === 'backend' ? 'success' : 'info'" round>
|
||||
<ElTag type="success" round>
|
||||
{{ apiModeLabel }}
|
||||
</ElTag>
|
||||
</div>
|
||||
@@ -94,7 +94,7 @@ function logout() {
|
||||
</ElButton>
|
||||
<ElTag type="primary" effect="plain" round>
|
||||
<ElIcon><Connection /></ElIcon>
|
||||
{{ platform.dataSource === 'backend' ? '后端已联通' : '后端不可用时自动降级' }}
|
||||
后端已联通
|
||||
</ElTag>
|
||||
<ElDropdown>
|
||||
<ElButton>
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
AgentCard,
|
||||
AgentInvocation,
|
||||
AgentMode,
|
||||
AgentPurpose,
|
||||
CreateAgentInvocationPayload,
|
||||
CreateDeployRunPayload,
|
||||
DashboardSummary,
|
||||
@@ -36,7 +24,7 @@ import type {
|
||||
} from '../types/devops'
|
||||
import { http } from './http'
|
||||
|
||||
type DataSource = 'backend' | 'mock'
|
||||
type DataSource = 'backend'
|
||||
type BackendHealthStatus = 'ok' | 'not_configured' | 'unavailable'
|
||||
type BackendRunStatus = 'pending' | 'queued' | 'running' | 'success' | 'failed' | 'canceled'
|
||||
|
||||
@@ -57,6 +45,8 @@ interface BackendProject {
|
||||
name: EnvironmentName
|
||||
displayName: string
|
||||
jenkinsJobPath: string
|
||||
releasePolicy?: 'branch' | 'tag' | 'master'
|
||||
refPattern?: string
|
||||
}>
|
||||
}
|
||||
|
||||
@@ -108,9 +98,7 @@ interface BackendProcessDefinition {
|
||||
nodeIds: string[]
|
||||
}
|
||||
|
||||
let lastDataSource: DataSource = 'mock'
|
||||
|
||||
const wait = async () => new Promise((resolve) => window.setTimeout(resolve, 180))
|
||||
let lastDataSource: DataSource = 'backend'
|
||||
const integrationConfigKeys: IntegrationConfigKey[] = [
|
||||
'jenkins',
|
||||
'gitea',
|
||||
@@ -123,21 +111,86 @@ const integrationConfigKeys: IntegrationConfigKey[] = [
|
||||
]
|
||||
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 = {
|
||||
getLastDataSource(): DataSource {
|
||||
return lastDataSource
|
||||
},
|
||||
|
||||
async getDashboard(): Promise<DashboardSummary> {
|
||||
return withBackendFallback(
|
||||
async () => {
|
||||
const [health, backendProjects, backendRuns, integrationConfig] = await Promise.all([
|
||||
fetchHealth(),
|
||||
fetchProjects(),
|
||||
fetchRuns(),
|
||||
fetchIntegrationConfigStatusWithFallback(),
|
||||
fetchIntegrationConfigStatus(),
|
||||
])
|
||||
const adaptedProjects = toProjectConfigs(backendProjects)
|
||||
lastDataSource = 'backend'
|
||||
|
||||
return {
|
||||
integrations: toIntegrationStatuses(health),
|
||||
@@ -148,196 +201,105 @@ export const devopsApi = {
|
||||
integrationConfig,
|
||||
}
|
||||
},
|
||||
() => dashboardSummary,
|
||||
)
|
||||
},
|
||||
|
||||
async getIntegrations(): Promise<IntegrationStatus[]> {
|
||||
return withBackendFallback(
|
||||
async () => toIntegrationStatuses(await fetchHealth()),
|
||||
() => integrations,
|
||||
)
|
||||
lastDataSource = 'backend'
|
||||
return toIntegrationStatuses(await fetchHealth())
|
||||
},
|
||||
|
||||
async getProjects(): Promise<ProjectConfig[]> {
|
||||
return withBackendFallback(
|
||||
async () => toProjectConfigs(await fetchProjects()),
|
||||
() => mockProjects,
|
||||
)
|
||||
lastDataSource = 'backend'
|
||||
return toProjectConfigs(await fetchProjects())
|
||||
},
|
||||
|
||||
async getProjectRefs(projectKey: string, projectList = mockProjects): Promise<ProjectRefs> {
|
||||
return withBackendFallback(
|
||||
async () => {
|
||||
async getProjectRefs(projectKey: string): Promise<ProjectRefs> {
|
||||
const { data } = await http.get<ProjectRefs>(`/projects/${projectKey}/refs`)
|
||||
lastDataSource = 'backend'
|
||||
return data
|
||||
},
|
||||
() => createFallbackRefs(projectKey, projectList),
|
||||
)
|
||||
},
|
||||
|
||||
async getRuns(projectList = mockProjects): Promise<ReleaseRun[]> {
|
||||
return withBackendFallback(
|
||||
async () => (await fetchRuns()).map((run) => toReleaseRun(run, projectList)),
|
||||
() => mockRuns,
|
||||
)
|
||||
async getRuns(projectList: ProjectConfig[] = []): Promise<ReleaseRun[]> {
|
||||
lastDataSource = 'backend'
|
||||
return (await fetchRuns()).map((run) => toReleaseRun(run, projectList))
|
||||
},
|
||||
|
||||
async createDeployRun(
|
||||
payload: CreateDeployRunPayload,
|
||||
projectList = mockProjects,
|
||||
projectList: ProjectConfig[] = [],
|
||||
): Promise<ReleaseRun> {
|
||||
return withBackendFallback(
|
||||
async () => {
|
||||
const { data } = await http.post<BackendDeployRun>('/deploy-runs', payload)
|
||||
lastDataSource = 'backend'
|
||||
return toReleaseRun(data, projectList)
|
||||
},
|
||||
() => createFallbackRun(payload, projectList, 'queued'),
|
||||
)
|
||||
},
|
||||
|
||||
async cancelDeployRun(runId: string, source?: ReleaseRun): Promise<ReleaseRun> {
|
||||
return withBackendFallback(
|
||||
async () => {
|
||||
async cancelDeployRun(
|
||||
runId: string,
|
||||
projectList: ProjectConfig[] = [],
|
||||
): Promise<ReleaseRun> {
|
||||
const { data } = await http.post<BackendDeployRun>(`/deploy-runs/${runId}/cancel`)
|
||||
return toReleaseRun(data, mockProjects)
|
||||
},
|
||||
() => ({
|
||||
...(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 : '人工取消,本节点未继续执行。',
|
||||
})),
|
||||
}),
|
||||
)
|
||||
lastDataSource = 'backend'
|
||||
return toReleaseRun(data, projectList)
|
||||
},
|
||||
|
||||
async retryDeployRun(runId: string, source?: ReleaseRun): Promise<ReleaseRun> {
|
||||
return withBackendFallback(
|
||||
async () => {
|
||||
async retryDeployRun(
|
||||
runId: string,
|
||||
projectList: ProjectConfig[] = [],
|
||||
): Promise<ReleaseRun> {
|
||||
const { data } = await http.post<BackendDeployRun>(`/deploy-runs/${runId}/retry`)
|
||||
return toReleaseRun(data, mockProjects)
|
||||
},
|
||||
() =>
|
||||
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',
|
||||
),
|
||||
)
|
||||
lastDataSource = 'backend'
|
||||
return toReleaseRun(data, projectList)
|
||||
},
|
||||
|
||||
async syncJenkinsRun(
|
||||
runId: string,
|
||||
source?: ReleaseRun,
|
||||
projectList = mockProjects,
|
||||
projectList: ProjectConfig[] = [],
|
||||
): Promise<ReleaseRun> {
|
||||
return withBackendFallback(
|
||||
async () => {
|
||||
const { data } = await http.post<BackendDeployRun>(`/deploy-runs/${runId}/sync-jenkins`)
|
||||
lastDataSource = 'backend'
|
||||
return toReleaseRun(data, projectList)
|
||||
},
|
||||
() => source ?? mockRuns.find((run) => run.id === runId) ?? mockRuns[0],
|
||||
)
|
||||
},
|
||||
|
||||
async syncJenkinsRuns(projectList = mockProjects): Promise<JenkinsSyncSummary> {
|
||||
return withBackendFallback(
|
||||
async () => {
|
||||
async syncJenkinsRuns(projectList: ProjectConfig[] = []): Promise<JenkinsSyncSummary> {
|
||||
const { data } = await http.post<BackendJenkinsSyncSummary>(
|
||||
'/deploy-runs/sync-jenkins',
|
||||
)
|
||||
lastDataSource = 'backend'
|
||||
return toJenkinsSyncSummary(data, projectList)
|
||||
},
|
||||
() => ({
|
||||
checkedAt: new Date().toISOString(),
|
||||
totalCandidates: 0,
|
||||
syncedRuns: [],
|
||||
failedRuns: [],
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
||||
async getAgentCards(): Promise<AgentCard[]> {
|
||||
await wait()
|
||||
return agentCards
|
||||
},
|
||||
|
||||
async getIntegrationConfigStatus(): Promise<IntegrationConfigStatus> {
|
||||
return withBackendFallback(
|
||||
async () => fetchIntegrationConfigStatus(),
|
||||
() => mockIntegrationConfigStatus,
|
||||
)
|
||||
lastDataSource = 'backend'
|
||||
return fetchIntegrationConfigStatus()
|
||||
},
|
||||
|
||||
async createAgentInvocation(
|
||||
payload: CreateAgentInvocationPayload,
|
||||
): Promise<AgentInvocation> {
|
||||
return withBackendFallback(
|
||||
async () => {
|
||||
const { data } = await http.post<AgentInvocation>('/agent/invocations', payload)
|
||||
lastDataSource = 'backend'
|
||||
return data
|
||||
},
|
||||
() => createFallbackAgentInvocation(payload),
|
||||
)
|
||||
},
|
||||
|
||||
async getSettings(): Promise<SystemSetting[]> {
|
||||
return withBackendFallback(
|
||||
async () => {
|
||||
const [health, integrationConfig] = await Promise.all([
|
||||
fetchHealth(),
|
||||
fetchIntegrationConfigStatusWithFallback(),
|
||||
fetchIntegrationConfigStatus(),
|
||||
])
|
||||
|
||||
lastDataSource = 'backend'
|
||||
return toSystemSettings(health, integrationConfig)
|
||||
},
|
||||
() => settings,
|
||||
)
|
||||
},
|
||||
|
||||
async getReleaseProcessXml(): Promise<string> {
|
||||
return withBackendFallback(
|
||||
async () => {
|
||||
const { data } = await http.get<BackendProcessDefinition>(
|
||||
'/process-definitions/release',
|
||||
)
|
||||
lastDataSource = 'backend'
|
||||
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> {
|
||||
@@ -360,14 +322,6 @@ async function fetchIntegrationConfigStatus(): Promise<IntegrationConfigStatus>
|
||||
return sanitizeIntegrationConfigStatus(data)
|
||||
}
|
||||
|
||||
async function fetchIntegrationConfigStatusWithFallback(): Promise<IntegrationConfigStatus> {
|
||||
try {
|
||||
return await fetchIntegrationConfigStatus()
|
||||
} catch {
|
||||
return mockIntegrationConfigStatus
|
||||
}
|
||||
}
|
||||
|
||||
function toIntegrationStatuses(health: BackendHealth): IntegrationStatus[] {
|
||||
return [
|
||||
toIntegrationStatus('jenkins', 'Jenkins', health),
|
||||
@@ -464,7 +418,6 @@ function sanitizeIntegrationNote(note?: string): string | undefined {
|
||||
|
||||
function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
|
||||
return backendProjects.map((project) => {
|
||||
const mockProject = mockProjects.find((item) => item.key === project.key)
|
||||
const repo = parseRepository(project.repositoryUrl, project.key)
|
||||
|
||||
return {
|
||||
@@ -473,30 +426,24 @@ function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
|
||||
repository: project.repositoryUrl,
|
||||
owner: repo.owner,
|
||||
repo: repo.repo,
|
||||
serviceType: mockProject?.serviceType ?? '服务',
|
||||
serviceType: serviceTypeLabel(project.key),
|
||||
defaultBranch: project.defaultBranch,
|
||||
tagPolicy: mockProject?.tagPolicy ?? '生产发布优先选择 v* tag',
|
||||
tagPolicy: tagPolicyLabel(project.environments),
|
||||
environments: project.environments.map((environment) => {
|
||||
const mockEnvironment = mockProject?.environments.find(
|
||||
(item) => item.name === environment.name,
|
||||
)
|
||||
|
||||
return {
|
||||
name: environment.name,
|
||||
label: environment.displayName,
|
||||
branchPolicy:
|
||||
mockEnvironment?.branchPolicy ??
|
||||
(environment.name === 'production' ? 'v* tag' : project.defaultBranch),
|
||||
status: mockEnvironment?.status ?? 'idle',
|
||||
version: mockEnvironment?.version ?? '-',
|
||||
lastRunId: mockEnvironment?.lastRunId ?? '-',
|
||||
lastReleasedAt: mockEnvironment?.lastReleasedAt ?? '-',
|
||||
lastActor: mockEnvironment?.lastActor ?? '-',
|
||||
branchPolicy: releasePolicyLabel(environment.releasePolicy, project.defaultBranch),
|
||||
status: 'idle' as const,
|
||||
version: '-',
|
||||
lastRunId: '-',
|
||||
lastReleasedAt: '-',
|
||||
lastActor: '-',
|
||||
jenkinsJob: environment.jenkinsJobPath,
|
||||
wecomRobot: mockEnvironment?.wecomRobot ?? '后端通知配置',
|
||||
wecomRobot: '后端通知配置',
|
||||
}
|
||||
}),
|
||||
integrations: mockProject?.integrations ?? {
|
||||
integrations: {
|
||||
jenkins: 'warning',
|
||||
gitea: '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 {
|
||||
if (status === 'ok') {
|
||||
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 {
|
||||
if (!value) {
|
||||
return '-'
|
||||
@@ -814,7 +742,7 @@ function formatDuration(
|
||||
|
||||
function toSystemSettings(
|
||||
health: BackendHealth,
|
||||
integrationConfig = mockIntegrationConfigStatus,
|
||||
integrationConfig: IntegrationConfigStatus,
|
||||
): SystemSetting[] {
|
||||
const integrationByKey = new Map(
|
||||
integrationConfig.integrations.map((item) => [item.key, item]),
|
||||
@@ -825,7 +753,7 @@ function toSystemSettings(
|
||||
return {
|
||||
...item,
|
||||
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'
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ import type {
|
||||
} from '../types/devops'
|
||||
|
||||
/**
|
||||
* 平台运行态聚合后端和 mock 降级数据,页面只消费统一后的展示模型。
|
||||
* 平台运行态聚合后端真实接口数据,页面只消费统一后的展示模型。
|
||||
*/
|
||||
export const usePlatformStore = defineStore('platform', () => {
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<'backend' | 'mock'>('mock')
|
||||
const dataSource = ref<'backend'>('backend')
|
||||
const projects = ref<ProjectConfig[]>([])
|
||||
const runs = ref<ReleaseRun[]>([])
|
||||
const integrations = ref<IntegrationStatus[]>([])
|
||||
@@ -61,21 +61,21 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
}
|
||||
|
||||
async function cancelDeployRun(run: ReleaseRun) {
|
||||
const canceled = await devopsApi.cancelDeployRun(run.id, run)
|
||||
const canceled = await devopsApi.cancelDeployRun(run.id, projects.value)
|
||||
upsertRun(canceled)
|
||||
dataSource.value = devopsApi.getLastDataSource()
|
||||
return canceled
|
||||
}
|
||||
|
||||
async function retryDeployRun(run: ReleaseRun) {
|
||||
const retried = await devopsApi.retryDeployRun(run.id, run)
|
||||
const retried = await devopsApi.retryDeployRun(run.id, projects.value)
|
||||
upsertRun(retried)
|
||||
dataSource.value = devopsApi.getLastDataSource()
|
||||
return retried
|
||||
}
|
||||
|
||||
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)
|
||||
dataSource.value = devopsApi.getLastDataSource()
|
||||
return synced
|
||||
@@ -89,7 +89,7 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
}
|
||||
|
||||
async function loadProjectRefs(projectKey: string) {
|
||||
const refs = await devopsApi.getProjectRefs(projectKey, projects.value)
|
||||
const refs = await devopsApi.getProjectRefs(projectKey)
|
||||
projectRefs.value = {
|
||||
...projectRefs.value,
|
||||
[projectKey]: refs,
|
||||
|
||||
@@ -4,13 +4,13 @@ import { defineStore } from 'pinia'
|
||||
export const useSessionStore = defineStore('session', () => {
|
||||
const operator = ref('ops-admin')
|
||||
const role = ref<'admin' | 'operator' | 'viewer'>('admin')
|
||||
const token = ref('mock-session')
|
||||
const token = ref('local-session')
|
||||
|
||||
const isAuthenticated = computed(() => Boolean(token.value))
|
||||
|
||||
function login(name: string) {
|
||||
operator.value = name || 'ops-admin'
|
||||
token.value = 'mock-session'
|
||||
token.value = 'local-session'
|
||||
}
|
||||
|
||||
function logout() {
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ export interface ProjectConfig {
|
||||
}
|
||||
|
||||
export interface ProjectRefs {
|
||||
source: 'gitea' | 'fallback' | 'mock'
|
||||
source: 'gitea' | 'fallback'
|
||||
branches: Array<{
|
||||
name: string
|
||||
commitSha?: string
|
||||
|
||||
@@ -10,7 +10,7 @@ const session = useSessionStore()
|
||||
|
||||
const form = reactive({
|
||||
operator: 'ops-admin',
|
||||
password: 'mock-password',
|
||||
password: '',
|
||||
})
|
||||
|
||||
function submit() {
|
||||
@@ -26,7 +26,7 @@ function submit() {
|
||||
<div class="login-copy">
|
||||
<ElIcon><Monitor /></ElIcon>
|
||||
<h1>DevOps 运维平台</h1>
|
||||
<p>第一阶段使用 mock session,仅用于本地控制台联调。真实身份、RBAC 和审计由后端接入后替换。</p>
|
||||
<p>当前使用本地操作员会话进入控制台;真实身份、RBAC 和审计由后端认证模块接入。</p>
|
||||
</div>
|
||||
<ElForm class="login-form" label-position="top" @submit.prevent="submit">
|
||||
<ElFormItem label="操作员">
|
||||
|
||||
@@ -25,7 +25,7 @@ const runningCount = computed(() => platform.runningRuns.length)
|
||||
<div class="status-grid">
|
||||
<MetricTile label="纳管项目" :value="platform.projects.length" caption="含 DevOps 平台自管理项目" :icon="Cpu" />
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ const runningCount = computed(() => platform.runningRuns.length)
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>集成状态</h2>
|
||||
<span>{{ platform.dataSource === 'backend' ? '来自后端 health' : '后端不可用,当前使用 mock' }}</span>
|
||||
<span>来自后端 health</span>
|
||||
</div>
|
||||
<div class="panel-body compact-list">
|
||||
<div v-for="item in platform.integrations" :key="item.key" class="list-row">
|
||||
|
||||
@@ -10,7 +10,7 @@ const platform = usePlatformStore()
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>项目配置</h1>
|
||||
<p>第一版展示项目、环境、Jenkins Job、Gitea 仓库和通知通道绑定关系。</p>
|
||||
<p>展示后端返回的项目、环境、Jenkins Job、Gitea 仓库和通知通道绑定关系。</p>
|
||||
</div>
|
||||
<ElButton type="primary" plain disabled>新增项目</ElButton>
|
||||
</div>
|
||||
@@ -18,7 +18,7 @@ const platform = usePlatformStore()
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>项目清单</h2>
|
||||
<span>{{ platform.dataSource === 'backend' ? '由后端 projects 接口返回' : '当前使用本地 mock 配置' }}</span>
|
||||
<span>由后端 projects 接口返回</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ElTable :data="platform.projects" row-key="key">
|
||||
|
||||
@@ -33,10 +33,7 @@ const environmentOptions = computed(() => {
|
||||
const environments = selectedProject.value?.environments ?? []
|
||||
|
||||
if (!environments.length) {
|
||||
return [
|
||||
{ label: '测试环境', value: 'test' },
|
||||
{ label: '生产环境', value: 'production' },
|
||||
]
|
||||
return []
|
||||
}
|
||||
|
||||
return environments.map((environment) => ({
|
||||
@@ -57,10 +54,10 @@ const refOptions = computed(() => {
|
||||
const refs = currentRefs.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(() => {
|
||||
@@ -74,7 +71,7 @@ const refSourceText = computed(() => {
|
||||
return '后端 fallback refs'
|
||||
}
|
||||
|
||||
return '本地 mock refs'
|
||||
return '等待后端 refs'
|
||||
})
|
||||
|
||||
const canTrigger = computed(() => {
|
||||
@@ -106,7 +103,7 @@ async function triggerRelease() {
|
||||
})
|
||||
activeRun.value = run
|
||||
localStatus.value = run.status
|
||||
ElMessage.success(platform.dataSource === 'backend' ? '发布单已提交到后端' : '后端不可用,已生成本地发布单')
|
||||
ElMessage.success('发布单已提交到后端')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -138,7 +135,7 @@ async function retryRelease() {
|
||||
const run = await platform.retryDeployRun(shownRun.value)
|
||||
activeRun.value = run
|
||||
localStatus.value = run.status
|
||||
ElMessage.success(platform.dataSource === 'backend' ? '已提交重试发布单' : '后端不可用,已生成本地重试单')
|
||||
ElMessage.success('已提交重试发布单')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -154,7 +151,7 @@ async function syncJenkinsRun() {
|
||||
const run = await platform.syncJenkinsRun(shownRun.value)
|
||||
activeRun.value = run
|
||||
localStatus.value = run.status
|
||||
ElMessage.success(platform.dataSource === 'backend' ? '已同步 Jenkins 状态' : '后端不可用,保留本地状态')
|
||||
ElMessage.success('已同步 Jenkins 状态')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -225,7 +222,7 @@ watch(refOptions, (options) => {
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>发布参数</h2>
|
||||
<span>{{ platform.dataSource === 'backend' ? '通过后端创建发布单' : '后端不可用时使用本地降级' }}</span>
|
||||
<span>通过后端创建发布单</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ElForm label-position="top">
|
||||
|
||||
@@ -36,7 +36,7 @@ async function syncSelectedRun() {
|
||||
|
||||
const run = await platform.syncJenkinsRun(selectedRun.value)
|
||||
selectedRunId.value = run.id
|
||||
ElMessage.success(platform.dataSource === 'backend' ? '已同步 Jenkins 状态' : '后端不可用,保留本地状态')
|
||||
ElMessage.success('已同步 Jenkins 状态')
|
||||
}
|
||||
|
||||
async function syncAllJenkinsRuns() {
|
||||
|
||||
@@ -169,7 +169,7 @@ function formatCheckedAt(value: string): string {
|
||||
<strong class="mono">VITE_API_BASE_URL</strong>
|
||||
<small>联调时指向后端网关;未配置时默认 /api。</small>
|
||||
</span>
|
||||
<ElTag type="info" round>{{ platform.dataSource === 'backend' ? '后端已联通' : '/api 或 mock 降级' }}</ElTag>
|
||||
<ElTag type="info" round>后端接口</ElTag>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span>
|
||||
|
||||
Reference in New Issue
Block a user