From 9a61622568d0a1fdc7902f1f081e5ef0037f8454 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 23 Apr 2026 12:14:36 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(jvm):=20=E5=A2=9E=E5=8A=A0=20J?= =?UTF-8?q?VM=20=E5=86=99=E5=85=A5=E9=A2=84=E8=A7=88=E4=B8=8E=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 打通 JVM 变更预览、执行确认与审计记录链路 - 增加 Guard 校验、模式约束与审计写入失败回传 - 补齐审计页签、预览弹窗和 Task 5 回归覆盖 --- .../需求进度追踪-JVM缓存可视化编辑-20260422.md | 26 +- frontend/src/components/JVMAuditViewer.tsx | 204 ++++++++++ .../src/components/JVMResourceBrowser.tsx | 383 +++++++++++++++--- frontend/src/components/TabManager.tsx | 7 +- .../components/jvm/JVMChangePreviewModal.tsx | 127 ++++++ frontend/src/types.ts | 25 ++ internal/app/methods_jvm.go | 83 +++- internal/app/methods_jvm_test.go | 207 +++++++++- internal/jvm/audit_store.go | 85 ++++ internal/jvm/audit_store_test.go | 30 ++ internal/jvm/config.go | 18 + internal/jvm/config_test.go | 14 + internal/jvm/guard.go | 144 +++++++ internal/jvm/guard_test.go | 200 +++++++++ 14 files changed, 1475 insertions(+), 78 deletions(-) create mode 100644 frontend/src/components/JVMAuditViewer.tsx create mode 100644 frontend/src/components/jvm/JVMChangePreviewModal.tsx create mode 100644 internal/jvm/audit_store.go create mode 100644 internal/jvm/audit_store_test.go create mode 100644 internal/jvm/guard.go create mode 100644 internal/jvm/guard_test.go diff --git a/docs/需求追踪/需求进度追踪-JVM缓存可视化编辑-20260422.md b/docs/需求追踪/需求进度追踪-JVM缓存可视化编辑-20260422.md index 0a1ac711..89178c06 100644 --- a/docs/需求追踪/需求进度追踪-JVM缓存可视化编辑-20260422.md +++ b/docs/需求追踪/需求进度追踪-JVM缓存可视化编辑-20260422.md @@ -26,7 +26,7 @@ - [x] 阶段 2(影响分析):完成 - [x] 阶段 3(方案设计):完成(已形成正式设计文档) - [x] 阶段 4(实施计划):完成(已形成正式实施计划) -- [ ] 阶段 5(实现与自检):进行中(Task 1、Task 2、Task 3、Task 4 已完成并通过回归) +- [ ] 阶段 5(实现与自检):进行中(Task 1 至 Task 5 已完成并通过回归,Task 6/7 待继续) - [ ] 阶段 6(评审与交付): - [ ] 阶段 7(发布与观察): @@ -43,11 +43,12 @@ - 已完成 Task 1:JVM 共享契约与配置归一化 - 已完成 Task 2:Provider 注册、连接测试与能力探测 API - 已完成 Task 3:JVM 连接表单、图标与展示文案接入 -- 已完成 Task 4:只读资源浏览与 JVM Tab + - 已完成 Task 4:只读资源浏览与 JVM Tab + - 已完成 Task 5:写入预览、Guard 和审计记录 - 进行中: - - Task 5:Guard/Audit、AI 结构化计划等后续任务准备 + - Task 6:AI 结构化变更计划 - 待处理: - - Task 5+:Guard/Audit、AI 结构化计划等后续任务实现 + - Task 7:全量回归、文档回填与交付检查 ## 5. 风险与阻塞 - 风险: @@ -57,6 +58,7 @@ - 若目标 JVM 不允许预埋或动态注入 Agent,则“通用型”能力边界会明显收缩 - 多接入模式会带来能力不一致问题,UI 与权限模型必须显式展示“当前模式支持什么/不支持什么” - 当前 JMX / Endpoint provider 的资源浏览与值读取仍是骨架实现,Task 4 已打通接口与 UI 链路,但真实资源展开会返回 `not implemented` + - Task 5 已打通 Guard/Preview/Audit 主链路,但真实 provider 的 `PreviewChange` / `ApplyChange` 能力仍依赖后续 provider 细化实现 - 阻塞: - 目标应用技术栈、缓存框架与接入约束尚未明确 - 缓解措施: @@ -90,6 +92,9 @@ - Task 3 已完成规格审查与代码质量审查;过程中修复了 JVM 标题文案偏差、模式选项暴露范围、编辑态模式静默降级和 endpoint timeout 失真问题 - 已完成 JVM 只读资源浏览链路:后端新增 `JVMListResources` / `JVMGetValue`,前端新增 `jvm-overview` / `jvm-resource` tab 与侧边栏 JVM 模式/资源节点 - Task 4 已完成规格复审;代码质量复审确认真实 provider 浏览能力仍为后续任务范围,另外已修正 JVM 资源 tab 同名问题 + - 已完成 Task 5:后端新增 `JVMPreviewChange` / `JVMApplyChange` / `JVMListAuditRecords`,补齐 Guard、审计 JSONL 落盘与审计读取能力 + - Task 5 已补齐只读拦截、`prod` 环境确认、provider preview 错误透出、审计写入失败显式回传、连接 `allowedModes` 约束和局部快照合并保底 + - 前端已完成 JVM 变更草稿区、预览弹窗、执行确认、审计记录页签与按 provider mode 的审计过滤 - 证据(日志/截图/链接): - `cmd/optional-driver-agent/main.go` - `internal/db/database.go` @@ -144,7 +149,18 @@ - `go test ./internal/app -run 'TestJVMProbeCapabilities|TestTestJVMConnection' -count=1` - `cd frontend && npm test -- src/utils/jvmRuntimePresentation.test.ts` - `cd frontend && npm run build` + - `internal/jvm/guard.go` + - `internal/jvm/guard_test.go` + - `internal/jvm/audit_store.go` + - `internal/jvm/audit_store_test.go` + - `internal/app/methods_jvm.go` + - `internal/app/methods_jvm_test.go` + - `frontend/src/components/JVMAuditViewer.tsx` + - `frontend/src/components/jvm/JVMChangePreviewModal.tsx` + - `go test ./internal/jvm ./internal/app -run 'TestPreviewChangeBlocksReadOnlyConnection|TestPreviewChangeReturnsProviderPreviewErrorWhenWriteAllowed|TestPreviewChangeMarksProdWritesAsConfirmationRequired|TestPreviewChangeMergesProviderSnapshotsWithoutDroppingDefaults|TestJVMApplyChangeReturnsProviderPayload|TestJVMPreviewChangeRejectsModeOutsideAllowedModes|TestJVMListAuditRecordsReturnsLatestRecords|TestJVMApplyChangeSurfacesAuditWriteFailure' -count=1` + - `go test ./internal/jvm ./internal/app -count=1` + - `cd frontend && npm run build` ## 8. 下一步 -- 下一步行动:进入 Task 5,补齐 Guard/Audit 和 AI 结构化修改计划能力,并收敛真实 provider 的资源浏览实现边界 +- 下一步行动:进入 Task 6,接入 AI 结构化修改计划能力,并在 Task 7 做全量交付检查 - 负责人:Codex diff --git a/frontend/src/components/JVMAuditViewer.tsx b/frontend/src/components/JVMAuditViewer.tsx new file mode 100644 index 00000000..776f7599 --- /dev/null +++ b/frontend/src/components/JVMAuditViewer.tsx @@ -0,0 +1,204 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Alert, Button, Card, Empty, Select, Space, Table, Tag, Typography } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { ReloadOutlined } from '@ant-design/icons'; + +import { useStore } from '../store'; +import type { JVMAuditRecord, TabData } from '../types'; +import JVMModeBadge from './jvm/JVMModeBadge'; + +const { Text } = Typography; + +type JVMAuditViewerProps = { + tab: TabData; +}; + +const LIMIT_OPTIONS = [20, 50, 100, 200]; + +const normalizeAuditRecords = (value: any): JVMAuditRecord[] => { + if (Array.isArray(value)) { + return value as JVMAuditRecord[]; + } + if (Array.isArray(value?.data)) { + return value.data as JVMAuditRecord[]; + } + return []; +}; + +const filterAuditRecordsByMode = (records: JVMAuditRecord[], providerMode?: string): JVMAuditRecord[] => { + const normalizedMode = String(providerMode || '').trim().toLowerCase(); + if (!normalizedMode) { + return records; + } + return records.filter((record) => String(record.providerMode || '').trim().toLowerCase() === normalizedMode); +}; + +const formatTimestamp = (timestamp: number): string => { + if (!timestamp) { + return '-'; + } + const normalized = timestamp > 1e12 ? timestamp : timestamp * 1000; + const date = new Date(normalized); + if (Number.isNaN(date.getTime())) { + return String(timestamp); + } + return date.toLocaleString('zh-CN', { hour12: false }); +}; + +const resultColor = (result: string): string => { + const normalized = String(result || '').trim().toLowerCase(); + if (normalized.includes('success') || normalized.includes('ok') || normalized.includes('done')) { + return 'green'; + } + if (normalized.includes('warn')) { + return 'gold'; + } + if (normalized.includes('fail') || normalized.includes('error')) { + return 'red'; + } + return 'default'; +}; + +const JVMAuditViewer: React.FC = ({ tab }) => { + const connection = useStore((state) => state.connections.find((item) => item.id === tab.connectionId)); + const [limit, setLimit] = useState(50); + const [loading, setLoading] = useState(true); + const [records, setRecords] = useState([]); + const [error, setError] = useState(''); + + const columns = useMemo>( + () => [ + { + title: '时间', + dataIndex: 'timestamp', + key: 'timestamp', + width: 180, + render: (value: number) => formatTimestamp(value), + }, + { + title: '模式', + dataIndex: 'providerMode', + key: 'providerMode', + width: 120, + render: (value: string) => , + }, + { + title: '动作', + dataIndex: 'action', + key: 'action', + width: 120, + render: (value: string) => value || '-', + }, + { + title: '资源', + dataIndex: 'resourceId', + key: 'resourceId', + ellipsis: true, + render: (value: string) => value || '-', + }, + { + title: '原因', + dataIndex: 'reason', + key: 'reason', + ellipsis: true, + render: (value: string) => value || '-', + }, + { + title: '结果', + dataIndex: 'result', + key: 'result', + width: 140, + render: (value: string) => {value || 'unknown'}, + }, + ], + [tab.providerMode], + ); + + const loadRecords = async () => { + if (!connection) { + setLoading(false); + setRecords([]); + setError('连接不存在或已被删除'); + return; + } + + const backendApp = (window as any).go?.app?.App; + if (typeof backendApp?.JVMListAuditRecords !== 'function') { + setLoading(false); + setRecords([]); + setError('JVMListAuditRecords 后端方法不可用'); + return; + } + + setLoading(true); + setError(''); + try { + const result = await backendApp.JVMListAuditRecords(connection.id, limit); + if (result?.success === false) { + setRecords([]); + setError(String(result?.message || '读取 JVM 审计记录失败')); + return; + } + setRecords(filterAuditRecordsByMode(normalizeAuditRecords(result), tab.providerMode)); + } catch (err: any) { + setRecords([]); + setError(err?.message || '读取 JVM 审计记录失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + void loadRecords(); + }, [connection, limit, tab.connectionId]); + + if (!connection) { + return ; + } + + return ( +
+ + + + + + setAction(event.target.value)} + placeholder="例如 update" + maxLength={64} + /> + + + 变更原因 + setReason(event.target.value)} + placeholder="填写本次 JVM 资源变更原因" + maxLength={200} + /> + + + Payload(JSON) + 需要输入 JSON 对象,预览和执行都会直接使用这份 payload。 +