diff --git a/.env.example b/.env.example index 646b153..d95cb7e 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1 @@ VITE_API_BASE_URL=http://localhost:4300 -VITE_DEVOPS_USE_MOCK=false diff --git a/AGENTS.md b/AGENTS.md index ebd7d9b..6b6519d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ - 路由使用 Vue Router。 - 请求封装使用 Axios,并集中在 `src/services/**`。 - BPMN 展示使用 `bpmn-js`,不手写替代渲染器。 -- mock 数据集中在 `src/mocks/**`,后续以后端 OpenAPI/Swagger 替换。 +- 不新增前端 mock 数据或接口失败降级;项目、发布、refs、流程和 Agent 调用统一走后端 API。 ## API 与安全边界 diff --git a/README.md b/README.md index 393d9c4..d6511e0 100644 --- a/README.md +++ b/README.md @@ -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 为准继续收敛字段。 浏览器端不得保存或直连以下密钥和服务: diff --git a/RTK.md b/RTK.md index 170839f..870a680 100644 --- a/RTK.md +++ b/RTK.md @@ -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 由后端流程定义接口返回。 diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 38d899f..125660a 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -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() {
本地开发 - + {{ apiModeLabel }}
@@ -94,7 +94,7 @@ function logout() { - {{ platform.dataSource === 'backend' ? '后端已联通' : '后端不可用时自动降级' }} + 后端已联通 diff --git a/src/mocks/devops.ts b/src/mocks/devops.ts deleted file mode 100644 index 9975952..0000000 --- a/src/mocks/devops.ts +++ /dev/null @@ -1,638 +0,0 @@ -/** - * 本地降级数据只模拟 DevOps 运维展示,不包含任何真实 token、webhook 或服务地址。 - */ -import type { - AgentCard, - DashboardSummary, - IntegrationConfigStatus, - IntegrationStatus, - PipelineStep, - ProjectConfig, - ReleaseRun, - SystemSetting, -} from '../types/devops' - -export const releaseProcessXml = ` - - - - Flow_Start_Gitea - - - Flow_Start_Gitea - Flow_Gitea_Jenkins - - - Flow_Gitea_Jenkins - Flow_Jenkins_Health - - - Flow_Jenkins_Health - Flow_Health_Notify - - - Flow_Health_Notify - Flow_Notify_End - - - Flow_Notify_End - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -` - -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, -} diff --git a/src/services/devopsApi.ts b/src/services/devopsApi.ts index bd4ed60..6716a7e 100644 --- a/src/services/devopsApi.ts +++ b/src/services/devopsApi.ts @@ -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 { - 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 { - return withBackendFallback( - async () => toIntegrationStatuses(await fetchHealth()), - () => integrations, - ) + lastDataSource = 'backend' + return toIntegrationStatuses(await fetchHealth()) }, async getProjects(): Promise { - return withBackendFallback( - async () => toProjectConfigs(await fetchProjects()), - () => mockProjects, - ) + lastDataSource = 'backend' + return toProjectConfigs(await fetchProjects()) }, - async getProjectRefs(projectKey: string, projectList = mockProjects): Promise { - return withBackendFallback( - async () => { - const { data } = await http.get(`/projects/${projectKey}/refs`) - return data - }, - () => createFallbackRefs(projectKey, projectList), - ) + async getProjectRefs(projectKey: string): Promise { + const { data } = await http.get(`/projects/${projectKey}/refs`) + lastDataSource = 'backend' + return data }, - async getRuns(projectList = mockProjects): Promise { - return withBackendFallback( - async () => (await fetchRuns()).map((run) => toReleaseRun(run, projectList)), - () => mockRuns, - ) + async getRuns(projectList: ProjectConfig[] = []): Promise { + lastDataSource = 'backend' + return (await fetchRuns()).map((run) => toReleaseRun(run, projectList)) }, async createDeployRun( payload: CreateDeployRunPayload, - projectList = mockProjects, + projectList: ProjectConfig[] = [], ): Promise { - return withBackendFallback( - async () => { - const { data } = await http.post('/deploy-runs', payload) - return toReleaseRun(data, projectList) - }, - () => createFallbackRun(payload, projectList, 'queued'), - ) + const { data } = await http.post('/deploy-runs', payload) + lastDataSource = 'backend' + return toReleaseRun(data, projectList) }, - async cancelDeployRun(runId: string, source?: ReleaseRun): Promise { - return withBackendFallback( - async () => { - const { data } = await http.post(`/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 { + const { data } = await http.post(`/deploy-runs/${runId}/cancel`) + lastDataSource = 'backend' + return toReleaseRun(data, projectList) }, - async retryDeployRun(runId: string, source?: ReleaseRun): Promise { - return withBackendFallback( - async () => { - const { data } = await http.post(`/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 { + const { data } = await http.post(`/deploy-runs/${runId}/retry`) + lastDataSource = 'backend' + return toReleaseRun(data, projectList) }, async syncJenkinsRun( runId: string, - source?: ReleaseRun, - projectList = mockProjects, + projectList: ProjectConfig[] = [], ): Promise { - return withBackendFallback( - async () => { - const { data } = await http.post(`/deploy-runs/${runId}/sync-jenkins`) - return toReleaseRun(data, projectList) - }, - () => source ?? mockRuns.find((run) => run.id === runId) ?? mockRuns[0], - ) + const { data } = await http.post(`/deploy-runs/${runId}/sync-jenkins`) + lastDataSource = 'backend' + return toReleaseRun(data, projectList) }, - async syncJenkinsRuns(projectList = mockProjects): Promise { - return withBackendFallback( - async () => { - const { data } = await http.post( - '/deploy-runs/sync-jenkins', - ) - return toJenkinsSyncSummary(data, projectList) - }, - () => ({ - checkedAt: new Date().toISOString(), - totalCandidates: 0, - syncedRuns: [], - failedRuns: [], - }), + async syncJenkinsRuns(projectList: ProjectConfig[] = []): Promise { + const { data } = await http.post( + '/deploy-runs/sync-jenkins', ) + lastDataSource = 'backend' + return toJenkinsSyncSummary(data, projectList) }, async getAgentCards(): Promise { - await wait() return agentCards }, async getIntegrationConfigStatus(): Promise { - return withBackendFallback( - async () => fetchIntegrationConfigStatus(), - () => mockIntegrationConfigStatus, - ) + lastDataSource = 'backend' + return fetchIntegrationConfigStatus() }, async createAgentInvocation( payload: CreateAgentInvocationPayload, ): Promise { - return withBackendFallback( - async () => { - const { data } = await http.post('/agent/invocations', payload) - return data - }, - () => createFallbackAgentInvocation(payload), - ) + const { data } = await http.post('/agent/invocations', payload) + lastDataSource = 'backend' + return data }, async getSettings(): Promise { - 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 { - return withBackendFallback( - async () => { - const { data } = await http.get( - '/process-definitions/release', - ) - return data.xml - }, - () => releaseProcessXml, + const { data } = await http.get( + '/process-definitions/release', ) - }, -} - -async function withBackendFallback( - request: () => Promise, - fallback: () => T | Promise, -): Promise { - 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 { @@ -360,14 +322,6 @@ async function fetchIntegrationConfigStatus(): Promise return sanitizeIntegrationConfigStatus(data) } -async function fetchIntegrationConfigStatusWithFallback(): Promise { - 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 = { - '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') -} diff --git a/src/stores/platform.ts b/src/stores/platform.ts index a5d1dfc..eecc00d 100644 --- a/src/stores/platform.ts +++ b/src/stores/platform.ts @@ -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([]) const runs = ref([]) const integrations = ref([]) @@ -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, diff --git a/src/stores/session.ts b/src/stores/session.ts index 655dc83..3119168 100644 --- a/src/stores/session.ts +++ b/src/stores/session.ts @@ -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() { diff --git a/src/types/devops.ts b/src/types/devops.ts index 029afa0..04e4e8c 100644 --- a/src/types/devops.ts +++ b/src/types/devops.ts @@ -106,7 +106,7 @@ export interface ProjectConfig { } export interface ProjectRefs { - source: 'gitea' | 'fallback' | 'mock' + source: 'gitea' | 'fallback' branches: Array<{ name: string commitSha?: string diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index 3d93592..7b2917a 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -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() {