From 8ffbdb4d45b6838903652f2ed910b06cc0a104e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=9B=E5=85=AE?= Date: Fri, 12 Jun 2026 00:59:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=E6=88=90=E5=91=98?= =?UTF-8?q?=E6=9D=83=E9=99=90=E4=B8=8E=20Agent=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/views/MembersView.vue: 新增成员、项目权限和密码消息管理 - src/views/SettingsView.vue: 接入 Agent 配置读取、保存和连接测试 - src/services/devopsApi.ts: 补齐成员、消息和 Agent 配置真实接口 - src/views/AgentView.vue: 移除旧独立 Agent 页面,保留顶部右侧抽屉入口 --- README.md | 5 +- RTK.md | 8 +- src/layouts/MainLayout.vue | 12 +- src/router/index.ts | 15 + src/services/devopsApi.ts | 90 ++++++ src/types/devops.ts | 87 ++++++ src/views/AgentView.vue | 243 --------------- src/views/MembersView.vue | 615 +++++++++++++++++++++++++++++++++++++ src/views/SettingsView.vue | 169 +++++++++- 9 files changed, 992 insertions(+), 252 deletions(-) delete mode 100644 src/views/AgentView.vue create mode 100644 src/views/MembersView.vue diff --git a/README.md b/README.md index b0b70ba..1559afe 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ Vue 3 + Vite + TypeScript 实现的 DevOps 运维控制台第一阶段,用于 ## 当前范围 -- 登录、总览、项目配置、发布中心、运行记录、系统配置,以及全局右侧 Agent 抽屉。 +- 登录、总览、项目配置、成员权限、发布中心、运行记录、系统配置,以及全局右侧 Agent 抽屉。 - 所有项目、发布、refs、流程和集成状态均调用 `devops-platform-api`,前端不再提供本地 mock 数据降级。 +- 成员权限页调用后端 `/members` 和 `/messages`,维护普通成员账号、项目权限、密码重置和平台消息。 - 发布中心包含项目、环境、ref/tag、备注、生产二次确认、触发、取消、重试和 Jenkins 状态同步入口。 - 运行记录展示 Jenkins queue/build、Build URL、开始/结束时间、执行提示、失败摘要和日志摘要。 - BPMN Viewer 使用 `bpmn-js` 渲染后端流程 XML,并通过进度统计、节点状态 badge、右侧时间线和详情面板展示 queued/running/success/failed/skipped/canceled 状态。 @@ -41,7 +42,7 @@ pnpm build 接口入口为 `src/services/devopsApi.ts`,只调用后端统一 envelope 响应;后端不可用时页面应暴露真实接口错误,不伪造项目、运行记录或 refs。后续以 `/Users/mac033/Desktop/my-project/devops-platform-docs/devops-platform-implementation-tracker.md` 中的产品约束和后端 OpenAPI 为准继续收敛字段。 -通知平台配置方案见 [docs/notification-platform-config.md](docs/notification-platform-config.md),包含企业微信、飞书和通用 webhook 的后端变量、安全边界与验证步骤。`/settings` 页面支持展示后端返回的变量说明字段,用于 hover 查看每个变量含义。 +通知平台配置方案见 [docs/notification-platform-config.md](docs/notification-platform-config.md),包含企业微信、飞书和通用 webhook 的后端变量、安全边界与验证步骤。`/settings` 页面支持展示后端返回的变量说明字段,用于 hover 查看每个变量含义,并通过 `/agent/config` 维护后端保存的模型 baseURL、model 和 key。 浏览器端不得保存或直连以下密钥和服务: diff --git a/RTK.md b/RTK.md index 8a09ac5..1a4930c 100644 --- a/RTK.md +++ b/RTK.md @@ -15,13 +15,14 @@ ## 页面结构 -- `/login`:本地操作员会话入口,后续接后端认证。 +- `/login`:真实平台账号登录入口,支持 token 恢复和首次改密。 - `/overview`:总览,展示服务状态、最近发布和项目环境。 - `/projects`:项目配置,展示 Gitea 仓库、Jenkins Job、通知通道和环境策略。 +- `/members`:成员权限,仅超级管理员可见,维护普通成员、项目权限和密码重置消息。 - `/releases`:发布中心,提供发布参数、二次确认、触发、取消、重试和 BPMN 流程状态。 - `/runs`:运行记录,展示 run 列表和流程轨迹。 - 顶部 Agent 按钮:打开全局右侧抽屉,只保留 DevOps 场景能力;`/agent` 仅重定向到总览,不作为独立侧边栏入口。 -- `/settings`:系统配置,展示 API、BPMN、Agent、通知边界。 +- `/settings`:系统配置,展示 API、BPMN、Agent、通知边界,并维护后端 Agent 模型配置。 ## 数据约定 @@ -32,8 +33,9 @@ ## 后续接入点 -- `devopsApi` 所有项目、发布、refs、流程和 Agent 调用均走后端接口。 +- `devopsApi` 所有项目、发布、refs、流程、成员、消息和 Agent 调用均走后端接口。 - 发布中心触发按钮接入后端发布单创建接口。 - 运行记录接入后端 run/step 查询接口。 - BPMN XML 由后端流程定义接口返回。 - Agent 面板调用后端 Agent proxy,前端只传 DevOps 场景参数。 +- Agent 配置页调用后端 `/agent/config`,前端不持久化 key、baseURL 或 model。 diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 7a8130b..91cdc85 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -11,6 +11,7 @@ import { SwitchButton, Tickets, Tools, + User, } from '@element-plus/icons-vue' import { computed, onMounted, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' @@ -28,13 +29,18 @@ const agentOpen = ref(false) const activeMenu = computed(() => route.path) -const menus = [ +const menus = computed(() => [ { path: '/overview', label: '总览', icon: DataAnalysis }, { path: '/projects', label: '项目配置', icon: Operation }, + ...(session.role === 'super_admin' + ? [{ path: '/members', label: '成员权限', icon: User }] + : []), { path: '/releases', label: '发布中心', icon: DocumentChecked }, { path: '/runs', label: '运行记录', icon: Tickets }, - { path: '/settings', label: '系统配置', icon: Setting }, -] + ...(session.role === 'super_admin' + ? [{ path: '/settings', label: '系统配置', icon: Setting }] + : []), +]) const jenkinsStatus = computed(() => { return platform.integrations.find((item) => item.key === 'jenkins')?.state || 'offline' diff --git a/src/router/index.ts b/src/router/index.ts index 91d6bfb..5356ade 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -4,6 +4,7 @@ import MainLayout from '../layouts/MainLayout.vue' import LoginView from '../views/LoginView.vue' import OverviewView from '../views/OverviewView.vue' import ProjectsView from '../views/ProjectsView.vue' +import MembersView from '../views/MembersView.vue' import ReleaseCenterView from '../views/ReleaseCenterView.vue' import RunsView from '../views/RunsView.vue' import SettingsView from '../views/SettingsView.vue' @@ -32,6 +33,14 @@ const routes: RouteRecordRaw[] = [ name: 'projects', component: ProjectsView, }, + { + path: 'members', + name: 'members', + component: MembersView, + meta: { + superAdminOnly: true, + }, + }, { path: 'releases', name: 'releases', @@ -73,6 +82,12 @@ router.beforeEach(async (to) => { } } + if (to.meta.superAdminOnly && session.role !== 'super_admin') { + return { + name: 'overview', + } + } + if (to.name === 'login' && authenticated && !session.mustChangePassword) { return { name: 'overview', diff --git a/src/services/devopsApi.ts b/src/services/devopsApi.ts index 6c38b23..d8ad670 100644 --- a/src/services/devopsApi.ts +++ b/src/services/devopsApi.ts @@ -3,12 +3,15 @@ */ import type { AgentCard, + AgentConfigSummary, + AgentConnectionTestResult, AgentInvocation, AuthenticatedUser, AuditLogEntry, ChangePasswordPayload, CreateAgentInvocationPayload, CreateDeployRunPayload, + CreateMemberPayload, DashboardSummary, EnvironmentName, HealthState, @@ -20,13 +23,18 @@ import type { JenkinsSyncSummary, LoginPayload, LoginResult, + MemberSummary, PipelineStep, + PlatformMessageSummary, ProjectConfig, ProjectRefs, ReleaseRun, ReleaseStatus, + SaveAgentConfigPayload, StepStatus, SystemSetting, + UpdateMemberPermissionsPayload, + UpdateMemberStatusPayload, } from '../types/devops' import { http } from './http' @@ -243,6 +251,66 @@ export const devopsApi = { lastDataSource = 'backend' }, + async getMembers(): Promise { + const { data } = await http.get('/members') + lastDataSource = 'backend' + return Array.isArray(data) ? data : [] + }, + + async createMember(payload: CreateMemberPayload): Promise { + const { data } = await http.post('/members', payload) + lastDataSource = 'backend' + return data + }, + + async updateMemberPermissions( + memberId: string, + payload: UpdateMemberPermissionsPayload, + ): Promise { + const { data } = await http.patch( + `/members/${memberId}/permissions`, + payload, + ) + lastDataSource = 'backend' + return data + }, + + async resetMemberPassword(memberId: string): Promise { + const { data } = await http.post( + `/members/${memberId}/reset-password`, + ) + lastDataSource = 'backend' + return data + }, + + async updateMemberStatus( + memberId: string, + payload: UpdateMemberStatusPayload, + ): Promise { + const { data } = await http.patch( + `/members/${memberId}/status`, + payload, + ) + lastDataSource = 'backend' + return data + }, + + async deleteMember(memberId: string): Promise { + await http.delete(`/members/${memberId}`) + lastDataSource = 'backend' + }, + + async getMessages(): Promise { + const { data } = await http.get('/messages') + lastDataSource = 'backend' + return Array.isArray(data) ? data : [] + }, + + async markMessageHandled(messageId: string): Promise { + await http.post(`/messages/${messageId}/handled`) + lastDataSource = 'backend' + }, + async getDashboard(): Promise { const [health, backendProjects, backendRuns, integrationConfig] = await Promise.all([ fetchHealth(), @@ -343,6 +411,28 @@ export const devopsApi = { return fetchIntegrationConfigStatus() }, + async getAgentConfig(): Promise { + const { data } = await http.get('/agent/config') + lastDataSource = 'backend' + return data + }, + + async saveAgentConfig( + payload: SaveAgentConfigPayload, + ): Promise { + const { data } = await http.put('/agent/config', payload) + lastDataSource = 'backend' + return data + }, + + async testAgentConfig(): Promise { + const { data } = await http.post( + '/agent/config/test', + ) + lastDataSource = 'backend' + return data + }, + async getReleaseProcessXml(): Promise { const { data } = await http.get( '/process-definitions/release', diff --git a/src/types/devops.ts b/src/types/devops.ts index 28afbfe..020b02a 100644 --- a/src/types/devops.ts +++ b/src/types/devops.ts @@ -35,6 +35,12 @@ export type AuthRole = 'super_admin' | 'member' export type AuthStatus = 'active' | 'disabled' +export type ProjectPermissionLevel = 'none' | 'read' | 'build' + +export type PlatformMessageState = 'pending' | 'handled' + +export type PlatformMessageKind = 'password_reset_request' + export interface AuthenticatedUser { id: string account: string @@ -61,6 +67,66 @@ export interface ChangePasswordPayload { newPassword: string } +export interface MemberProjectPermission { + projectId: string + projectKey: string + projectName: string + level: ProjectPermissionLevel + levelName: string +} + +export interface MemberSummary { + id: string + account: string + displayName: string + role: AuthRole + roleName: string + status: AuthStatus + statusName: string + mustChangePassword: boolean + hasPendingPasswordResetRequest: boolean + lastLoginAt?: string + createdAt: string + updatedAt: string + projectPermissions: MemberProjectPermission[] +} + +export interface ProjectPermissionPayload { + projectKey: string + level: ProjectPermissionLevel +} + +export interface CreateMemberPayload { + account: string + displayName: string + permissions?: ProjectPermissionPayload[] +} + +export interface UpdateMemberPermissionsPayload { + permissions: ProjectPermissionPayload[] +} + +export interface UpdateMemberStatusPayload { + status: AuthStatus +} + +export interface PlatformMessageSummary { + id: string + type: PlatformMessageKind + status: PlatformMessageState + title: string + content: string + requesterId?: string + requesterAccount: string + targetResourceType?: string + targetResourceId?: string + targetUrl?: string + handledById?: string + handledAt?: string + createdAt: string + updatedAt: string +} + export type IntegrationConfigKey = | 'jenkins' | 'gitea' @@ -273,3 +339,24 @@ export interface CreateAgentInvocationPayload { projectKey?: string promptSummary: string } + +export interface AgentConfigSummary { + baseURL: string + model: string + hasKey: boolean + maskedKey?: string + warning: string + updatedAt?: string +} + +export interface SaveAgentConfigPayload { + key?: string + baseURL: string + model: string +} + +export interface AgentConnectionTestResult { + success: boolean + message: string + checkedAt: string +} diff --git a/src/views/AgentView.vue b/src/views/AgentView.vue deleted file mode 100644 index de370e8..0000000 --- a/src/views/AgentView.vue +++ /dev/null @@ -1,243 +0,0 @@ - - - - - diff --git a/src/views/MembersView.vue b/src/views/MembersView.vue new file mode 100644 index 0000000..47b7637 --- /dev/null +++ b/src/views/MembersView.vue @@ -0,0 +1,615 @@ + + + + + diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index 25ef806..a19a230 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -2,17 +2,29 @@ /** * 系统配置页展示集成变量说明、配置边界和审计日志脱敏 JSON。 */ -import { Refresh } from '@element-plus/icons-vue' +import { Connection, Refresh } from '@element-plus/icons-vue' +import { ElMessage } from 'element-plus' +import { onMounted, reactive, ref } from 'vue' import AuditJsonCell from '../components/AuditJsonCell.vue' import StatusPill from '../components/StatusPill.vue' +import { devopsApi } from '../services/devopsApi' import { usePlatformStore } from '../stores/platform' import type { + AgentConfigSummary, AuditLogEntry, IntegrationConfigItem, IntegrationConfigState, } from '../types/devops' const platform = usePlatformStore() +const agentConfigLoading = ref(false) +const agentConfigTesting = ref(false) +const agentConfig = ref(null) +const agentConfigForm = reactive({ + key: '', + baseURL: '', + model: '', +}) type VariableTagType = 'success' | 'warning' | 'danger' | 'info' type VariableGroup = { key: string @@ -31,6 +43,10 @@ const configStatusMeta: Record< missing: { label: '未配置', type: 'danger' }, } +onMounted(() => { + void loadAgentConfig() +}) + function configStatusLabel(status: IntegrationConfigState): string { return configStatusMeta[status]?.label ?? '未配置' } @@ -150,6 +166,56 @@ function variableDescription( function variableSourceLabel(item: IntegrationConfigItem, name: string): string { return variableHelp(item, name)?.description ? '后端返回' : '等待后端说明' } + +async function loadAgentConfig() { + agentConfigLoading.value = true + + try { + const config = await devopsApi.getAgentConfig() + applyAgentConfig(config) + } finally { + agentConfigLoading.value = false + } +} + +async function saveAgentConfig() { + agentConfigLoading.value = true + + try { + const config = await devopsApi.saveAgentConfig({ + key: agentConfigForm.key.trim() || undefined, + baseURL: agentConfigForm.baseURL.trim(), + model: agentConfigForm.model.trim(), + }) + applyAgentConfig(config) + agentConfigForm.key = '' + ElMessage.success('Agent 配置已保存') + await platform.loadAll() + } catch (error) { + ElMessage.error(error instanceof Error ? error.message : 'Agent 配置保存失败') + } finally { + agentConfigLoading.value = false + } +} + +async function testAgentConfig() { + agentConfigTesting.value = true + + try { + const result = await devopsApi.testAgentConfig() + ElMessage.success(result.message) + } catch (error) { + ElMessage.error(error instanceof Error ? error.message : 'Agent 连接测试失败') + } finally { + agentConfigTesting.value = false + } +} + +function applyAgentConfig(config: AgentConfigSummary) { + agentConfig.value = config + agentConfigForm.baseURL = config.baseURL + agentConfigForm.model = config.model +}