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
+193 -281
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 {
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,221 +111,195 @@ 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(),
])
const adaptedProjects = toProjectConfigs(backendProjects)
const [health, backendProjects, backendRuns, integrationConfig] = await Promise.all([
fetchHealth(),
fetchProjects(),
fetchRuns(),
fetchIntegrationConfigStatus(),
])
const adaptedProjects = toProjectConfigs(backendProjects)
lastDataSource = 'backend'
return {
integrations: toIntegrationStatuses(health),
projects: adaptedProjects,
runs: backendRuns.map((run) => toReleaseRun(run, adaptedProjects)),
agentCards,
settings: toSystemSettings(health, integrationConfig),
integrationConfig,
}
},
() => dashboardSummary,
)
return {
integrations: toIntegrationStatuses(health),
projects: adaptedProjects,
runs: backendRuns.map((run) => toReleaseRun(run, adaptedProjects)),
agentCards,
settings: toSystemSettings(health, integrationConfig),
integrationConfig,
}
},
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 () => {
const { data } = await http.get<ProjectRefs>(`/projects/${projectKey}/refs`)
return data
},
() => createFallbackRefs(projectKey, projectList),
)
async getProjectRefs(projectKey: string): Promise<ProjectRefs> {
const { data } = await http.get<ProjectRefs>(`/projects/${projectKey}/refs`)
lastDataSource = 'backend'
return data
},
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)
return toReleaseRun(data, projectList)
},
() => createFallbackRun(payload, projectList, 'queued'),
)
const { data } = await http.post<BackendDeployRun>('/deploy-runs', payload)
lastDataSource = 'backend'
return toReleaseRun(data, projectList)
},
async cancelDeployRun(runId: string, source?: ReleaseRun): Promise<ReleaseRun> {
return withBackendFallback(
async () => {
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 : '人工取消,本节点未继续执行。',
})),
}),
)
async cancelDeployRun(
runId: string,
projectList: ProjectConfig[] = [],
): Promise<ReleaseRun> {
const { data } = await http.post<BackendDeployRun>(`/deploy-runs/${runId}/cancel`)
lastDataSource = 'backend'
return toReleaseRun(data, projectList)
},
async retryDeployRun(runId: string, source?: ReleaseRun): Promise<ReleaseRun> {
return withBackendFallback(
async () => {
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',
),
)
async retryDeployRun(
runId: string,
projectList: ProjectConfig[] = [],
): Promise<ReleaseRun> {
const { data } = await http.post<BackendDeployRun>(`/deploy-runs/${runId}/retry`)
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`)
return toReleaseRun(data, projectList)
},
() => source ?? mockRuns.find((run) => run.id === runId) ?? mockRuns[0],
)
const { data } = await http.post<BackendDeployRun>(`/deploy-runs/${runId}/sync-jenkins`)
lastDataSource = 'backend'
return toReleaseRun(data, projectList)
},
async syncJenkinsRuns(projectList = mockProjects): Promise<JenkinsSyncSummary> {
return withBackendFallback(
async () => {
const { data } = await http.post<BackendJenkinsSyncSummary>(
'/deploy-runs/sync-jenkins',
)
return toJenkinsSyncSummary(data, projectList)
},
() => ({
checkedAt: new Date().toISOString(),
totalCandidates: 0,
syncedRuns: [],
failedRuns: [],
}),
async syncJenkinsRuns(projectList: ProjectConfig[] = []): Promise<JenkinsSyncSummary> {
const { data } = await http.post<BackendJenkinsSyncSummary>(
'/deploy-runs/sync-jenkins',
)
lastDataSource = 'backend'
return toJenkinsSyncSummary(data, projectList)
},
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)
return data
},
() => createFallbackAgentInvocation(payload),
)
const { data } = await http.post<AgentInvocation>('/agent/invocations', payload)
lastDataSource = 'backend'
return data
},
async getSettings(): Promise<SystemSetting[]> {
return withBackendFallback(
async () => {
const [health, integrationConfig] = await Promise.all([
fetchHealth(),
fetchIntegrationConfigStatusWithFallback(),
])
return toSystemSettings(health, integrationConfig)
},
() => settings,
)
const [health, integrationConfig] = await Promise.all([
fetchHealth(),
fetchIntegrationConfigStatus(),
])
lastDataSource = 'backend'
return toSystemSettings(health, integrationConfig)
},
async getReleaseProcessXml(): Promise<string> {
return withBackendFallback(
async () => {
const { data } = await http.get<BackendProcessDefinition>(
'/process-definitions/release',
)
return data.xml
},
() => releaseProcessXml,
const { data } = await http.get<BackendProcessDefinition>(
'/process-definitions/release',
)
},
}
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()
}
return data.xml
},
}
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')
}