refactor: 端口策略+探针弹框改用shadcn Dialog,无黑色蒙布居中弹出

This commit is contained in:
attacker committed 2026-06-24 17:01:35 +00:00
1 parent ffa73353f1
commit 6e479f6df7
3 files changed
+511 -138

No files matched your search

+364
View File
@@ -0,0 +1,364 @@
# AIOps 后端代码深度审计报告
> 审计目标:`/srv/aiops/backend`
> 模块名:`ezops` | Go 版本:`1.24.0` | Web 框架:Gin | ORM:GORM | 日志:zap
> 审计时间:2026-06-25
> 审计方式:纯静态读取(`find` / `wc` / `grep`),**未修改任何文件**
---
## 一、代码规模统计
### 1.1 全局总览
| 指标 | 数值 |
|---|---|
| Go 源文件总数 | **116** 个 |
| Go 源代码总行数 | **34,485** 行 |
| 顶层包分布 | `internal`(97) / `pkg`(14) / `cmd`(3) / `scripts`(2) |
### 1.2 各目录 `.go` 文件数 + 总行数
| 目录 | 文件数 | 总行数 | 说明 |
|---|---:|---:|---|
| `internal/api/handlers` | **65** | **21,418** | 业务处理器(核心代码集中地) |
| `internal/models` | **26** | **4,269** | 数据模型 |
| `internal/api/routes` | 1 | 853 | 路由注册 |
| `internal/api/middleware` | 2 | 278 | 中间件(JWT / Admin / PipelineAccess) |
| `internal/cloud` | 2 | 286 | 云厂商 SDK 封装 |
| `internal/config` | 1 | 105 | 配置加载 |
| `pkg/kubernetesclient` | 1 | 1,896 | K8s 客户端封装(最大 pkg) |
| `pkg/zabbix` | 1 | 757 | Zabbix API 客户端 |
| `pkg/snmp` | 1 | 621 | SNMP 采集 |
| `pkg/toolinstaller` | 3 | 592 | 工具懒加载(kubectl/maven/node/helm) |
| `pkg/prometheus` | 1 | 531 | Prometheus 客户端 |
| `pkg/remoteexec` | 1 | 420 | 原生 SSH 批量执行 |
| `pkg/redis` | 3 | 290 | Redis/Redka 缓存抽象 |
| `pkg/database` | 1 | 150 | GORM 初始化 |
| `pkg/crypto` | 1 | 66 | AES 加解密(凭据) |
| `pkg/logger` | 1 | 90 | zap 封装 |
| `cmd` | 3 | 1,043 | 程序入口(main / seed / embed) |
| `scripts` | 2 | 820 | 辅助脚本 |
> **规模结论**:`internal/api/handlers` 占全部代码的 **62%**(21,418 / 34,485),是绝对的业务核心;其中 `kubernetes.go` 单文件 **4,204 行**,是全仓最大文件(存在拆分必要,见第五章)。
### 1.3 `handlers/` 各文件行数与函数数(按行数降序)
| 文件 | 行数 | 函数数 | 功能 |
|---|---:|---:|---|
| kubernetes.go | **4,204** | **134** | K8s 集群 + 资源全量管理(⚠️ 超大文件) |
| pipeline_executor.go | 665 | 18 | 流水线执行引擎 V2 |
| agent.go | 624 | 12 | Agent 上报/聚合/健康探针 |
| pipeline.go | 580 | 13 | 流水线 CRUD + 队列 + 成员 |
| agent_install.go | 514 | 10 | Agent 远程安装/卸载/升级 |
| alert_channels.go | 483 | 16 | 告警通知渠道 |
| ansible.go | 462 | 18 | Ansible 自动化 |
| k8s_export.go | 451 | 6 | K8s 资源批量导出 |
| alert_engine.go | 424 | 10 | 告警引擎(定时扫描) |
| server.go | 417 | 8 | 服务器 CRUD |
| automation_cron.go | 411 | 8 | 定时任务 |
| credential.go | 400 | 13 | 凭据管理 V2 |
| ldap_users.go | 397 | 5 | LDAP 用户导入/管理 |
| user.go | 360 | 6 | 用户/登录 |
| user_admin.go | 360 | 8 | 管理员用户管理 |
| audit.go | 360 | 9 | 审计日志(含 1 处 TODO) |
| dashboard.go | 356 | 5 | 仪表盘统计 |
| automation_script.go | 356 | 8 | 脚本执行 |
| prometheus_datasource.go | 352 | 9 | Prometheus 数据源 |
| llm_chat.go | 349 | 6 | LLM 对话代理 |
| alert_rules.go | 336 | 7 | 告警规则 |
| prometheus_proxy.go | 331 | 13 | Prometheus API 代理 |
| webshell.go | 325 | 5 | WebSSH 终端 |
| ldap.go | 319 | 6 | LDAP 配置 |
| remote_exec.go | 309 | 7 | SSH 远程批量执行 |
| health_probe.go | 308 | 7 | 健康探针 CRUD |
| billing.go | 307 | 7 | 云账单 |
| agent_task.go | 296 | 13 | Agent 远程任务/规则/事件 |
| zabbix_proxy.go | 284 | 5 | Zabbix API 代理 |
| cloud_sync.go | 277 | 5 | 云平台区域/实例同步 |
| pipeline_release.go | 261 | 6 | 应用发布 |
| navigation_link.go | 259 | 5 | 导航链接 |
| deploy_environments.go | 234 | 11 | 环境管理 |
| cloud_domain.go | 230 | 6 | 域名管理 |
| tool_config.go | 217 | 8 | 工具配置 |
| approval.go | 209 | 6 | 审批 |
| zabbix_datasource.go | 205 | 8 | Zabbix 数据源 |
| cloud_accounts.go | 202 | 5 | 云账号 |
| item_role.go | 201 | 8 | Item 角色 |
| metrics.go | 188 | 7 | 指标浏览器 |
| approval_process.go | 187 | 5 | 审批流程 |
| navigation.go | 186 | 5 | 导航分组 |
| workspace_files.go | 184 | 5 | 工作区文件浏览 |
| kubectl_shell.go | 183 | 3 | kubectl WebSocket 终端 |
| role.go | 168 | 4 | 角色 CRUD |
| billing_alert.go | 168 | 7 | 账单告警 |
| llm.go | 167 | 6 | LLM 配置 |
| site.go | 166 | 8 | 站点拨测 |
| deploy.go | 164 | 11 | 仓库/镜像仓库 |
| server_batch.go | 161 | 2 | 服务器批量导入/删除 |
| deploy_groups.go | 151 | 8 | 用户组 |
| cloud_region.go | 151 | 6 | 区域 |
| log.go | 150 | 6 | 日志监控 |
| alert_contacts.go | 149 | 8 | 告警联系人/组 |
| terminal_session.go | 147 | 6 | 终端会话审计 |
| snmp.go | 143 | 9 | SNMP 设备 |
| ws_pipeline_log.go | 142 | 3 | 流水线日志 WS 推流 |
| resource_model.go | 134 | 5 | 资源模型 |
| alert.go | 133 | 4 | 告警事件 |
| cloud_platform.go | 132 | 5 | 云平台 |
| ldap_ou.go | 122 | 2 | LDAP OU 管理 |
| metrics_ws.go | 120 | 7 | 指标 WS Hub |
| automation.go | 76 | 3 | 自动化处理器(构造 + 列表代理) |
| credential_assign.go | 62 | 1 | 凭据分配到服务器 |
| llm_history.go | 49 | 2 | 对话历史查询/清空 |
> Handler 文件合计 **65** 个,**21,418** 行,约 **500+** 个 handler 函数。
---
## 二、API 路由清单
**路由定义文件**:`internal/api/routes/routes.go`(853 行,单文件集中注册)
### 2.1 路由总量
| 维度 | 数值 |
|---|---|
| **路由总数** | **411** |
| GET | 171 |
| POST | 144 |
| DELETE | 54 |
| PUT | 41 |
| PATCH | 1 |
### 2.2 路由按模块分组统计(method + 代表性 path)
> 路径前缀统一为 `/api/v1`(除 `/health`、`/ws/*`、`/agent/*` 静态外)。
> 鉴权:标注「公开」无需 JWT;其余均经 `middleware.JWTAuth`;`/admin/*` 额外加 `AdminAuth`;流水线 `/:code` 走 `PipelineAccess`。
| # | 模块 | 路由数 | 代表性路由(method + path) |
|---|---|---:|---|
| 1 | 健康检查 | 1 | `GET /health` |
| 2 | WebSocket 通道 | 5 | `GET /ws/terminal`、`/ws/metrics`、`/ws/pipeline/:runId/log`、`/ws/kubernetes-exec/:id/.../pods/:podName`、`/ws/kubectl` |
| 3 | 认证(公开) | 2 | `POST /auth/login`、`/auth/register` |
| 4 | Webhook 触发(公开) | 2 | `POST /webhook/job/:jobName`、`/webhook/pipeline/:code` |
| 5 | Agent API(上报/查询/任务,公开+JWT 混合) | 30 | `POST /agent/report`、`GET /agent/tasks/poll`、`GET /agent/latest`、`POST /agent/tasks`、`GET/POST/PUT/DELETE /agent/rules`、`POST /agent/uninstall` 等 |
| 6 | 用户中心 | 4 | `GET/PUT /user/profile`、`POST /user/change-password`、`GET /user/roles` |
| 7 | 管理员-用户管理 | 10 | `GET/POST /admin/users`、`GET/PUT/DELETE /admin/users/:id`、`POST /admin/users/batch-import`、`GET /admin/users/import-template` |
| 8 | 管理员-角色权限 | 4 | `GET/POST /admin/roles`、`PUT/DELETE /admin/roles/:id` |
| 9 | 管理员-权限列表 | 1 | `GET /admin/permissions` |
| 10 | Item 角色(Jenkins 式正则匹配) | 4 | `GET/POST /admin/item-roles`、`PUT/DELETE /admin/item-roles/:id` |
| 11 | LDAP 配置 | 10 | `GET/PUT /admin/ldap-config`、`POST .../test`、`/users`、`/import`、`/ou-list`、`/create-ou`、`/create-user`、`/delete-user`、`/update-user` |
| 12 | 仪表盘 | 4 | `GET /dashboard/stats`、`/recent-alerts`、`/recent-releases`、`/metrics` |
| 13 | 服务器 / Agent 安装 | 11 | `GET/POST /servers`、`GET/PUT/DELETE /servers/:id`、`POST /servers/batch`、`/batch-delete`、`POST /servers/:id/install-agent`、`/install-agent`(批量) |
| 14 | 云账号 / 云同步 | 8 | `GET/POST/PUT/DELETE /cloud-accounts`、`POST /cloud-accounts/:id/sync-regions`、`GET .../regions`、`POST .../discover-instances`、`/import-instances` |
| 15 | 域名 | 4 | `GET/POST/PUT/DELETE /domains` |
| 16 | 云平台 | 4 | `GET/POST/PUT/DELETE /platforms` |
| 17 | 区域 | 5 | `GET/POST/PUT /regions`、`DELETE /regions/batch-delete`、`/:id` |
| 18 | 云账单 | 6 | `GET/POST /cloud-billings`、`POST .../batch`、`DELETE .../:id`、`GET .../stats`、`/providers` |
| 19 | 账单告警 | 5 | `GET/POST/PUT/DELETE /billing-alerts`、`POST .../:id/toggle` |
| 20 | 资源模型 | 4 | `GET/POST/PUT/DELETE /resource-models` |
| 21 | **K8s 集群管理(集群域 `/kubernetes-clusters`)** | **38** | 集群 CRUD + 同步、节点/Pod/Deployment/Service/Ingress 列表、拓扑、HPA/CronJob/ReplicaSet、回滚、Pod 文件、exec、scale/restart/pause/resume |
| 22 | **K8s 资源管理(Kuboard 域 `/kubernetes/:clusterCode/...`)** | **66** | namespace 看板、Deployments/Pods/Services/Ingresses/HPA/CronJob/ReplicaSet/StatefulSet/DaemonSet/ConfigMap/Secret/PVC/RBAC 全量操作、YAML 在线编辑、Events、容量分析、多集群对比、批量导出 |
| 23 | 告警(规则/事件/渠道/联系人) | 22 | `GET /alerts`、`GET/POST/PUT/DELETE /alerts/rules`、`POST /:id/ack`、通知渠道 + 测试、联系人/联系组 CRUD、`/quick-setup` |
| 24 | 流水线(CI/CD) | 19 | `GET/POST /pipelines`、`/:code` 增删改查 + trigger + runs + stop + 队列 + 成员 + 组授权 |
| 25 | 用户组 | 7 | `GET/POST/PUT/DELETE /deploy/groups`、成员管理 |
| 26 | 环境管理 | 4 | `GET/POST/PUT/DELETE /deploy/environments` |
| 27 | 应用发布 | 6 | `GET/POST /releases`、`POST /:id/approve`、`/reject`、`/deploy`、`/rollback` |
| 28 | 代码仓库 | 5 | `GET/POST/PUT/DELETE /deploy/repos` |
| 29 | 镜像仓库 | 5 | `GET/POST/PUT/DELETE /deploy/registries` |
| 30 | 工具配置 | 6 | `GET/PUT /deploy/tools/:id`、`POST .../install`、`/check`、`GET .../versions` |
| 31 | 定时任务 | 8 | `GET/POST/PUT/DELETE /cron-jobs`、`/toggle`、`/execute`、`batch-delete` |
| 32 | 脚本管理 | 7 | `GET/POST/PUT/DELETE /scripts`、`/execute`、`batch-delete` |
| 33 | 远程执行(SSH) | 4 | `POST /remote-exec/execute`、`/upload`、`GET /hosts`、`POST /server/:id/exec` |
| 34 | 任务执行记录 | 1 | `GET /task-executions` |
| 35 | 日志监控 | 4 | `GET /logs`、`GET/POST/DELETE /logs/sources` |
| 36 | 审批流程 | 10 | 模板 + 流程 CRUD、`/approve`、`/reject` |
| 37 | 导航 | 9 | groups + links CRUD、`/favorite` |
| 38 | 审计日志 | 7 | `GET /audit/logs`、`/:id`、`/export`、`/statistics`、`/modules`、`/actions`、`POST /logs/clear` |
| 39 | 终端会话审计 | 5 | `GET /terminal-sessions`、`/:id`、`/:id/record`、`DELETE /:id`、`batch-delete` |
| 40 | AI 助手(LLM) | 7 | `GET/PUT /aiops/llm-config`、`/test`、`POST /chat`、`/chat/stream`、`GET/DELETE /chat-history` |
| 41 | 指标浏览器 | 5 | `GET /metrics/targets`、`/list`、`/query`、`/names`、`/top` |
| 42 | SNMP 设备 | 8 | 设备 + 模板 CRUD、`batch-delete`、`/stats` |
| 43 | Zabbix 代理 | 2 | `POST /zabbix/connect`、`GET /zabbix/host/:hostid` |
| 44 | Prometheus 代理 | 4 | `POST /prometheus/connect`、`/query`、`/query_range`、`/dashboard` |
| 45 | Prometheus 数据源 | 7 | CRUD + `/:id/default` + `/test` |
| 46 | Zabbix 数据源 | 7 | CRUD + `/:id/default` + `/test` |
| 47 | 凭据管理 | 7 | `GET/POST/PUT/PATCH/DELETE /credentials`、`GET /tags`、`POST /servers/:id/assign-credential` |
| 48 | 站点拨测 | 7 | tasks CRUD + batch-delete + results + stats |
| | **合计** | **411** | |
> **路由结构特征**:
> - K8s 相关路由共 **104** 条(集群域 38 + Kuboard 域 66),占总路由 **25%**,是系统最重模块;存在**两套并行路由体系**(`/kubernetes-clusters/:id/...` 与 `/kubernetes/:clusterCode/...`),功能高度重叠,长期维护成本高。
> - 告警体系路由 **22** 条,覆盖规则/事件/升级/静默/渠道/联系人全链路。
> - 公开(无 JWT)路由:`/health`、`/auth/*`、`/webhook/*`、`/agent/*`(上报侧)、WebSocket(部分用 `?token=` 鉴权)。
---
## 三、数据模型清单
**目录**:`internal/models/`(26 个文件)
### 3.1 模型总量
| 指标 | 数值 |
|---|---|
| 模型文件数 | **26** |
| `struct` 类型总数 | **218**(含 Request/Response DTO) |
| **GORM 映射数据库表数** | **86** |
### 3.2 各模型文件核心模型与表名
| 文件 | 核心模型 | 数据库表 |
|---|---|---|
| `agent.go` | AgentReport / AgentPortPolicy | `monitor_agent_reports`、`monitor_agent_port_policy` |
| `agent_task.go` | AgentTask / AgentRule / AgentAlert | `monitor_agent_tasks`、`monitor_agent_rules`、`monitor_agent_alerts` |
| `alert.go` | AlertRule / AlertEvent / NotifyChannel / AlertSilence / AlertUpgrade / AlertContact(Group) | `monitor_alert_rules`、`monitor_alert_events`、`monitor_alert_conditions`、`monitor_alert_notify_templates`、`monitor_alert_upgrades`、`monitor_alert_upgrade_records`、`monitor_alert_silences`、`monitor_notify_channels`、`monitor_metric_data`、`monitor_alert_contacts`、`monitor_alert_contact_groups` |
| `approval.go` | ApprovalTemplate / ApprovalProcess / ApprovalStep | `sys_approval_templates`、`approval_processes`、`approval_steps` |
| `audit.go` | AuditLog | `sys_audit_logs` |
| `automation.go` | CronJob / Script / TaskExecution | `ops_cron_jobs`、`ops_scripts`、`ops_task_executions` |
| `billing.go` | CloudBilling / BillingAlertRule | `asset_cloud_billings`、`monitor_billing_alert_rules` |
| `cicd.go` | Pipeline / PipelineRun / AppRelease | `ops_pipelines`、`ops_pipeline_runs`、`deploy_app_releases` |
| `common.go` | Response / ListResponse / PageQuery / DashboardStats(DTO,无表) | — |
| `credential.go` | Credential(多类型) | `iam_credentials` |
| `deploy.go` | PipelineRepo / PipelineRegistry / DeployEnvironment / DeployUserGroup 等 | `deploy_pipeline_repos`、`deploy_pipeline_registries`、`deploy_environments`、`deploy_pipeline_members`、`deploy_user_groups`、`deploy_user_group_members` |
| `deploy_tool.go` | DeployTool | `deploy_tools` |
| `kubernetes.go` | KubernetesCluster / Node / Pod / Deployment / Service / Ingress / TopologyLayout | `kubernetes_clusters`、`kubernetes_nodes`、`kubernetes_pods`、`kubernetes_deployments`、`kubernetes_services`、`kubernetes_ingresses`、`kubernetes_topology_layouts` |
| `llm.go` | LLMConfig / ChatHistory | `ai_llm_configs`、`ai_chat_histories` |
| `metrics_aggregate.go` | MetricsAggregate | `monitor_metrics_aggregate` |
| `monitor.go` | MonitorTarget / MonitorMetric / MonitorDashboard / HostGroup / HealthProbe 等 | `monitor_targets`、`monitor_metrics`、`metric_values`、`monitor_metric_histories`、`monitor_metric_tags`、`monitor_dashboards`、`dashboard_panels`、`asset_host_groups`、`monitor_health_probes` |
| `navigation.go` | NavigationGroup / NavigationLink | `sys_navigation_groups`、`navigation_links` |
| `ops_modules.go` | AnsiblePlaybook / AnsibleJob / LogSource / LogEntry | `ops_ansible_playbooks`、`ops_ansible_jobs`、`ops_ansible_inventories`、`sys_log_sources`、`sys_log_entries` |
| `prometheus_datasource.go` | PrometheusDataSource | `monitor_prometheus_datasources` |
| `resource_model.go` | ResourceModel | `asset_resource_models` |
| `server.go` | Server / Cloud / CloudAccount / Region / Domain | `asset_servers`、`asset_clouds`、`asset_cloud_accounts`、`asset_regions`、`asset_domains` |
| `site.go` | SiteTask / SiteResult / SiteAlert | `ops_site_tasks`、`ops_site_results`、`ops_site_alerts` |
| `snmp.go` | SNMPDevice / SNMPTemplate / SNMPMetric / SNMPInterface | `ops_snmp_devices`、`ops_snmp_templates`、`ops_snmp_template_oids`、`ops_snmp_metrics`、`ops_snmp_interfaces`、`snmp_history` |
| `terminal_session.go` | TerminalSession | `sys_terminal_sessions` |
| `user.go` | User / Role / Permission / ItemRole / LDAPConfig | `iam_users`、`iam_roles`、`iam_permissions`、`iam_item_roles`、`iam_user_item_roles`、`iam_ldap_configs` |
| `zabbix_datasource.go` | ZabbixDataSource | `monitor_zabbix_datasources` |
> **命名规范**:表名采用「业务前缀_模块_实体」前缀分组,一致性良好:
> - `iam_*` 身份认证 | `asset_*` 资产 | `monitor_*` 监控 | `ops_*` 运维 | `deploy_*` 发布 | `sys_*` 系统 | `ai_*` AI
> - 少数表未严格遵循:`approval_processes`、`approval_steps`、`navigation_links`、`dashboard_panels`、`metric_values`、`snmp_history`(缺前缀,轻微规范不一致)。
---
## 四、功能模块清单
> 评定标准:**✅ 完善** = 实现完整、行数充足;**⚠️ 轻量但完整** = 行数小但功能单一已实现;**🚧 仅框架/TODO** = 含 TODO 或未完成。
### 4.1 功能完善度评估(按域归类)
| 业务域 | handler 文件 | 评定 | 说明 |
|---|---|---|---|
| **Kubernetes 管理** | kubernetes.go(4204)、k8s_export.go(451) | ✅ 完善 | 全仓最重模块,两套路由(集群域+Kuboard域),覆盖 Deploy/Pod/Svc/Ingress/HPA/CronJob/RS/STS/DS/CM/Secret/PVC/RBAC/YAML/Events/容量/对比/导出 |
| **流水线 CI/CD** | pipeline.go(580)、pipeline_executor.go(665)、pipeline_release.go(261) | ✅ 完善 | 执行引擎 V2 + Redis 实时日志推送,支持 git/maven/npm/go/docker/kubectl 全链路 |
| **Agent 体系** | agent.go(624)、agent_install.go(514)、agent_task.go(296)、health_probe.go(308) | ✅ 完善 | 上报/聚合/远程任务/告警规则/健康探针/端口策略/安装/卸载/升级 |
| **告警** | alert.go(133)、alert_rules.go(336)、alert_channels.go(483)、alert_contacts.go(149)、alert_engine.go(424) | ✅ 完善 | 规则+引擎(30s轮询)+渠道+联系人+静默+升级 |
| **用户/权限** | user.go(360)、user_admin.go(360)、role.go(168)、item_role.go(201) | ✅ 完善 | 用户/角色/权限/Item角色(Jenkins式) |
| **LDAP** | ldap.go(319)、ldap_users.go(397)、ldap_ou.go(122) | ✅ 完善 | 配置/测试/用户导入/OU 管理/用户 CRUD |
| **服务器资产** | server.go(417)、server_batch.go(161) | ✅ 完善 | CRUD + 批量导入/删除 + Agent 安装 |
| **云资源** | cloud_accounts.go(202)、cloud_sync.go(277)、cloud_platform.go(132)、cloud_region.go(151)、cloud_domain.go(230) | ✅ 完善 | 账号/同步/平台/区域/域名 |
| **凭据** | credential.go(400)、credential_assign.go(62) | ⚠️ 轻量但完整 | 多类型加密凭据 + 服务器分配(小文件但功能单一已实现) |
| **自动化运维** | automation.go(76)、automation_cron.go(411)、automation_script.go(356)、ansible.go(462) | ✅ 完善 | 定时任务/脚本/Ansible |
| **远程执行** | remote_exec.go(309) | ✅ 完善 | 原生 SSH 批量执行/上传/主机匹配 |
| **审批** | approval.go(209)、approval_process.go(187) | ✅ 完善 | 模板 + 流程 + 步骤 |
| **发布部署** | deploy.go(164)、deploy_environments.go(234)、deploy_groups.go(151)、deploy.go(repo/registry) | ✅ 完善 | 仓库/镜像仓库/环境/用户组 |
| **工具配置** | tool_config.go(217) | ✅ 完善 | kubectl/maven/node/helm 懒加载 |
| **监控数据源** | prometheus_datasource.go(352)、prometheus_proxy.go(331)、zabbix_datasource.go(205)、zabbix_proxy.go(284) | ✅ 完善 | Prom/Zabbix 数据源 CRUD + 代理查询 |
| **指标** | metrics.go(188)、metrics_ws.go(120) | ✅ 完善 | 指标浏览 + WS 实时推送 |
| **网络设备** | snmp.go(143) | ⚠️ 轻量但完整 | SNMP 设备/模板 CRUD |
| **站点拨测** | site.go(166) | ✅ 完善 | HTTP/TCP/DNS/PING 任务+结果+告警 |
| **账单** | billing.go(307)、billing_alert.go(168) | ✅ 完善 | 云账单 + 账单告警规则 |
| **仪表盘** | dashboard.go(356) | ✅ 完善 | 统计/近期告警/近期发布/指标 |
| **审计/合规** | audit.go(360)、terminal_session.go(147) | 🚧 **含 TODO** | 审计日志完整,但 **`ExportAuditLogs` 未实现 CSV/Excel 导出**,仅返回 JSON |
| **导航** | navigation.go(186)、navigation_link.go(259) | ✅ 完善 | 分组 + 链接 + 收藏 |
| **AI 助手** | llm.go(167)、llm_chat.go(349)、llm_history.go(49) | ✅ 完善 | LLM 配置 + OpenAI 兼容对话(含流式)+ 历史 |
| **终端** | webshell.go(325)、kubectl_shell.go(183)、ws_pipeline_log.go(142)、workspace_files.go(184) | ✅ 完善 | WebSSH/kubectl 终端 + WS 日志 + 工作区文件 |
| **日志监控** | log.go(150) | ✅ 完善 | 日志查询 + 数据源 |
### 4.2 仅有框架 / 待完善项
| 文件 | 行数 | 问题 |
|---|---:|---|
| `audit.go` (`ExportAuditLogs`) | 360 | 🚧 **TODO**:`// TODO: 实现CSV/Excel导出功能`,当前仅返回 JSON,未生成实际导出文件 |
| `automation.go` | 76 | ⚠️ 仅含构造函数 + 列表代理(实际逻辑在 cron/script 子文件),属正常拆分,非残缺 |
| `credential_assign.go` | 62 | ⚠️ 仅 1 个函数,但已完整实现,非残缺 |
| `llm_history.go` | 49 | ⚠️ 仅 2 个函数(历史查询+清空),已完整实现,非残缺 |
> **结论**:除 `audit.go` 的导出功能为明确 TODO 外,**无其他空壳/桩代码**。整体功能完成度高。
---
## 五、已知问题 / 技术债
### 5.1 TODO / FIXME / HACK / XXX 注释
| 位置 | 内容 | 严重度 |
|---|---|---|
| `internal/api/handlers/audit.go:223` | `// TODO: 实现CSV/Excel导出功能` — 导出接口返回 JSON 而非 CSV/Excel 文件 | 🟡 中(功能未达预期) |
> **全仓 TODO/FIXME/HACK/XXX 总计:仅 1 处**。代码注释卫生状况良好。
### 5.2 `panic(` 调用
> **全仓 `panic(` 调用数:0**(排除测试文件)。
> ✅ Handler 层无 panic,符合生产规范(均通过 `c.JSON(...)` 返回错误)。
### 5.3 `_ =` 显式忽略错误(共 13 处)
| 文件:行 | 被忽略的表达式 | 风险评估 |
|---|---|---|
| `handlers/dashboard.go:236` | `_ = h.cache.Set(...)` | 🟢 低:缓存写入失败可接受(缓存降级) |
| `handlers/server.go:137` | `_ = h.cache.Set(...)` | 🟢 低:同上 |
| `handlers/agent.go:254` | `_ = h.cache.Expire(...)` | 🟢 低:缓存 TTL 失败可接受 |
| `handlers/agent.go:258` | `_ = h.cache.ZRemRangeByScore(...)` | 🟢 低:清理旧指标,失败影响小 |
| `handlers/kubernetes.go:1292/1334` | `_ = msgType`(WebSocket 消息类型) | 🟢 低:显式丢弃未用变量(编译要求) |
| `handlers/kubernetes.go:1353` | `_ = cluster` | 🟢 低:占位 |
| `handlers/kubernetes.go:3655` | `_ = c.ShouldBindJSON(&req)` | 🟡 **中**:绑定错误被忽略,但注释说明「允许空 body,下面会校验」——依赖后续校验,需确认所有分支确有校验 |
| `handlers/automation_script.go:267` | `_ = c.ShouldBindJSON(&req)` | 🟡 **中**:同上模式,绑定错误被忽略 |
| `handlers/ldap_users.go:164` | `_ = bcrypt.MinCost` | 🟢 低:常量引用占位 |
| `handlers/ldap_users.go:210` | `_ = json.Unmarshal(...)` | 🟡 **中**:LDAP 用户映射 JSON 解析失败被静默忽略,可能导致字段映射丢失而不报警 |
| `pkg/database/database.go:148` | `_ = lastErr` | 🟢 低:重试后记录最终错误 |
| `pkg/remoteexec/executor.go:226` | `_ = encodedLen` | 🟢 低:未用变量占位 |
> **建议**:重点复查 3 处 🟡 中风险——`kubernetes.go:3655`、`automation_script.go:267`、`ldap_users.go:210`,确认忽略错误不会导致空指针或逻辑漏洞。
### 5.4 其他风险信号
| 类别 | 发现 | 数量/位置 | 建议 |
|---|---|---|---|
| **`exec.Command` 命令执行** | pipeline_executor / automation / ansible / tool_config 调用外部命令(git/mvn/npm/go/docker/kubectl/ansible-playbook/bash/sh/python3) | **18 处** | 多数参数为固定/受控;`tool_config.go:148` 的 `sh -c <CheckCommand>` 与 `automation.go:39/41` 的 `bash -c <content>` 若内容来自用户输入需**严格校验命令注入** |
| **SQL 注入** | `.Where(fmt.Sprintf(...))` 拼接 | **0 处** | ✅ 全仓未发现 Sprintf 拼 Where,GORM 参数化查询使用规范 |
| **`defer Close()` 未检错** | 文件/连接关闭 | 22 处 | 🟢 低:Close 错误通常可忽略,符合 Go 惯例 |
| **超大文件** | `kubernetes.go` 单文件 4,204 行 / 134 函数 | 1 个 | 🔴 **高**:建议按资源类型(Deployment/Pod/Service/HPA/CronJob/YAML/导出等)拆分,否则维护困难 |
| **K8s 双路由体系** | `/kubernetes-clusters/:id/...` 与 `/kubernetes/:clusterCode/...` 功能重叠 | 2 套(104 路由) | 🟡 中:长期应统一为一套路由,避免双份逻辑漂移 |
| **routes.go 单文件** | 853 行集中注册 411 条路由 | 1 个 | 🟡 中:建议按模块拆分为多个注册函数 |
### 5.5 告警引擎架构提示
`routes.go:845-852` 中告警引擎以 **goroutine + 30s ticker** 方式常驻启动:
```go
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C { alertEngine.Check() }
}()
```
- 🟡 **无优雅退出机制**(无 context 取消),进程关闭时无法平滑停止;
- 🟡 单实例运行,**多副本部署会重复触发告警**(需结合分布式锁或单实例调度)。
---
## 六、总体评估
| 维度 | 评级 | 说明 |
|---|---|---|
| **功能完整度** | ⭐⭐⭐⭐ | 411 条路由覆盖资产/监控/告警/CI-CD/K8s/自动化/AI 全栈,除审计导出 TODO 外无空壳 |
| **代码规范** | ⭐⭐⭐⭐ | TODO/panic 几乎为零,命名前缀统一,GORM 参数化规范 |
| **可维护性** | ⭐⭐⭐ | kubernetes.go 超大文件 + K8s 双路由体系是主要技术债 |
| **健壮性** | ⭐⭐⭐⭐ | handler 无 panic,但 3 处错误忽略需复查 |
| **安全性** | ⭐⭐⭐⭐ | 无 SQL 注入;exec.Command 命令注入面需复核用户输入边界 |
**核心结论**:这是一个功能完备、规范度较高的中型 AIOps 后端(3.4 万行 / 411 路由 / 86 表)。主要技术债集中在 **K8s 模块的代码体积与双路由体系**,以及**告警引擎的单实例/无优雅退出**设计。建议优先级:① 拆分 `kubernetes.go` → ② 统一 K8s 路由 → ③ 复查 3 处错误忽略 → ④ 补全审计导出 → ⑤ 告警引擎增加分布式协调。
+3 -1
View File
@@ -48,13 +48,15 @@ function DialogContent({
className,
children,
showCloseButton = true,
showOverlay = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
showOverlay?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
{showOverlay && <DialogOverlay />}
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
+144 -137
View File
@@ -8,6 +8,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
} from '@/components/ui/dialog';
import {
Cpu, HardDrive, Network, Activity, Server, Loader2,
Info, Monitor, Disc, Clock, List, HeartPulse, Plus, Trash2, RefreshCw, Edit, CheckCircle, XCircle,
@@ -815,159 +818,163 @@ export function HostMonitor() {
</Tabs>
{/* 添加/编辑探针对话框 */}
{probeDialogOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setProbeDialogOpen(false)}>
<div className="bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg p-5 w-[480px] max-h-[90vh] overflow-y-auto shadow-xl" onClick={e => e.stopPropagation()}>
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)] mb-4">
<Dialog open={probeDialogOpen} onOpenChange={setProbeDialogOpen}>
<DialogContent showOverlay={false} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-lg">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">
{editingProbe ? '编辑探针' : '添加探针'}
</h3>
<div className="space-y-3">
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">探针名称 <span className="text-red-400">*</span></label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.name} onChange={e => setProbeForm({ ...probeForm, name: e.target.value })} placeholder="如:cex-agent 健康检查" />
</div>
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">目标主机</label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.hostname} onChange={e => setProbeForm({ ...probeForm, hostname: e.target.value })} />
</div>
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">探针类型</label>
<select className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.probe_type} onChange={e => setProbeForm({ ...probeForm, probe_type: e.target.value })}>
<option value="http_get">HTTP GET</option>
<option value="tcp_socket">TCP Socket</option>
<option value="exec_command">Exec Command</option>
</select>
</div>
</DialogTitle>
<DialogDescription className="text-[var(--dg-text-secondary)]">
{editingProbe ? '修改健康探针配置' : '创建新的健康探针,Agent 在目标主机本地执行探测'}
</DialogDescription>
</DialogHeader>
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1">
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">探针名称 <span className="text-red-400">*</span></label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.name} onChange={e => setProbeForm({ ...probeForm, name: e.target.value })} placeholder="如:cex-agent 健康检查" />
</div>
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">目标主机</label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.hostname} onChange={e => setProbeForm({ ...probeForm, hostname: e.target.value })} />
</div>
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">探针类型</label>
<select className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.probe_type} onChange={e => setProbeForm({ ...probeForm, probe_type: e.target.value })}>
<option value="http_get">HTTP GET</option>
<option value="tcp_socket">TCP Socket</option>
<option value="exec_command">Exec Command</option>
</select>
</div>
{probeForm.probe_type === 'http_get' && (
<>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">HTTP URL <span className="text-red-400">*</span></label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_url} onChange={e => setProbeForm({ ...probeForm, http_url: e.target.value })} placeholder="http://localhost:8080/health" /></div>
<div className="grid grid-cols-2 gap-2">
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">期望状态码</label>
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_expect_code} onChange={e => setProbeForm({ ...probeForm, http_expect_code: Number(e.target.value) })} /></div>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">期望响应内容(可选)</label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_expect_body} onChange={e => setProbeForm({ ...probeForm, http_expect_body: e.target.value })} /></div>
</div>
</>
)}
{probeForm.probe_type === 'tcp_socket' && (
{probeForm.probe_type === 'http_get' && (
<>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">HTTP URL <span className="text-red-400">*</span></label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_url} onChange={e => setProbeForm({ ...probeForm, http_url: e.target.value })} placeholder="http://localhost:8080/health" /></div>
<div className="grid grid-cols-2 gap-2">
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">TCP 目标地址</label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.tcp_host} onChange={e => setProbeForm({ ...probeForm, tcp_host: e.target.value })} placeholder="localhost" /></div>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">TCP 端口</label>
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.tcp_port} onChange={e => setProbeForm({ ...probeForm, tcp_port: Number(e.target.value) })} /></div>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">期望状态码</label>
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_expect_code} onChange={e => setProbeForm({ ...probeForm, http_expect_code: Number(e.target.value) })} /></div>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">期望响应内容(可选)</label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_expect_body} onChange={e => setProbeForm({ ...probeForm, http_expect_body: e.target.value })} /></div>
</div>
)}
{probeForm.probe_type === 'exec_command' && (
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">执行命令 <span className="text-red-400">*</span></label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs font-mono text-[var(--dg-text-primary)]" value={probeForm.exec_command} onChange={e => setProbeForm({ ...probeForm, exec_command: e.target.value })} placeholder="pgrep -x mysqld" /></div>
)}
</>
)}
{probeForm.probe_type === 'tcp_socket' && (
<div className="grid grid-cols-2 gap-2">
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">超时(秒)</label>
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.timeout} onChange={e => setProbeForm({ ...probeForm, timeout: Number(e.target.value) })} /></div>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">间隔(秒)</label>
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.interval} onChange={e => setProbeForm({ ...probeForm, interval: Number(e.target.value) })} /></div>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">TCP 目标地址</label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.tcp_host} onChange={e => setProbeForm({ ...probeForm, tcp_host: e.target.value })} placeholder="localhost" /></div>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">TCP 端口</label>
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.tcp_port} onChange={e => setProbeForm({ ...probeForm, tcp_port: Number(e.target.value) })} /></div>
</div>
)}
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">描述</label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.description} onChange={e => setProbeForm({ ...probeForm, description: e.target.value })} /></div>
</div>
<div className="flex justify-end gap-2 mt-5">
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => setProbeDialogOpen(false)}>取消</Button>
<Button size="sm" className="h-7 text-[11px] bg-blue-600 hover:bg-blue-500" onClick={async () => {
if (!probeForm.name || !probeForm.hostname) return;
try {
if (editingProbe) {
await agentApi.updateHealthProbe(editingProbe.id, probeForm);
} else {
await agentApi.createHealthProbe(probeForm);
}
setProbeDialogOpen(false);
fetchProbes();
} catch (e) { alert('保存失败'); }
}} disabled={!probeForm.name || !probeForm.hostname}>
{editingProbe ? '保存' : '创建'}
</Button>
{probeForm.probe_type === 'exec_command' && (
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">执行命令 <span className="text-red-400">*</span></label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs font-mono text-[var(--dg-text-primary)]" value={probeForm.exec_command} onChange={e => setProbeForm({ ...probeForm, exec_command: e.target.value })} placeholder="pgrep -x mysqld" /></div>
)}
<div className="grid grid-cols-2 gap-2">
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">超时(秒)</label>
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.timeout} onChange={e => setProbeForm({ ...probeForm, timeout: Number(e.target.value) })} /></div>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">间隔(秒)</label>
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.interval} onChange={e => setProbeForm({ ...probeForm, interval: Number(e.target.value) })} /></div>
</div>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">描述</label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.description} onChange={e => setProbeForm({ ...probeForm, description: e.target.value })} /></div>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => setProbeDialogOpen(false)}>取消</Button>
<Button size="sm" className="h-7 text-[11px] bg-blue-600 hover:bg-blue-500" onClick={async () => {
if (!probeForm.name || !probeForm.hostname) return;
try {
if (editingProbe) {
await agentApi.updateHealthProbe(editingProbe.id, probeForm);
} else {
await agentApi.createHealthProbe(probeForm);
}
setProbeDialogOpen(false);
fetchProbes();
} catch (e) { alert('保存失败'); }
}} disabled={!probeForm.name || !probeForm.hostname}>
{editingProbe ? '保存' : '创建'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 端口策略对话框 */}
{portPolicyDialogOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setPortPolicyDialogOpen(false)}>
<div className="bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg p-5 w-[440px] max-h-[90vh] overflow-y-auto shadow-xl" onClick={e => e.stopPropagation()}>
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)] mb-4">端口监控策略</h3>
<div className="space-y-4">
{/* 模式选择 */}
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">过滤模式</label>
<div className="flex gap-2">
<button
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'whitelist' ? 'bg-blue-500/20 border-blue-500 text-blue-400' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
onClick={() => setPortPolicyMode('whitelist')}
>
白名单 ─ 全量上报
</button>
<button
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'blacklist' ? 'bg-amber-500/20 border-amber-500 text-amber-400' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
onClick={() => setPortPolicyMode('blacklist')}
>
黑名单 ─ 仅上报选中
</button>
</div>
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1.5 leading-relaxed">
{portPolicyMode === 'whitelist'
? '所有发现的端口都上报,任意端口变化都会触发告警 + 恢复通知。'
: '仅上报选中端口的变化,其余端口变化静默忽略(不告警)。系统进程端口(sshd/rpcbind/systemd等)永远忽略。'}
</p>
</div>
{/* 端口白名单 — 仅黑名单模式可编辑 */}
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">
端口列表 <span className="text-[var(--dg-text-muted)]">(逗号分隔,如: 80,443,3306,6379)</span>
{portPolicyMode === 'whitelist' && <span className="text-blue-400 ml-1">— 白名单模式无需配置</span>}
</label>
<textarea
className={`w-full h-20 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 py-1.5 text-xs font-mono resize-none ${
portPolicyMode === 'whitelist' ? 'text-[var(--dg-text-muted)] opacity-50 cursor-not-allowed' : 'text-[var(--dg-text-primary)]'
}`}
value={portWhitelist.join(',')}
onChange={e => {
if (portPolicyMode === 'whitelist') return;
const val = e.target.value;
const nums = val.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n) && n > 0 && n <= 65535);
setPortWhitelist(nums);
}}
placeholder={portPolicyMode === 'whitelist' ? '白名单模式下所有端口自动上报' : '80,443,3306,6379'}
readOnly={portPolicyMode === 'whitelist'}
/>
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1">
已输入 {portWhitelist.length} 个端口
</p>
</div>
{/* 系统排除说明 */}
<div className="bg-amber-500/5 border border-amber-500/20 rounded p-2.5">
<p className="text-[10px] text-amber-300/80 leading-relaxed">
⚠️ 无论选择哪种模式,以下系统进程端口<b>永远不上报</b>:sshd、rpcbind、master、systemd、dbus-daemon、rsyslogd 等。Agent 采集时自动跳过。
</p>
<Dialog open={portPolicyDialogOpen} onOpenChange={setPortPolicyDialogOpen}>
<DialogContent showOverlay={false} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-md">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">端口监控策略</DialogTitle>
<DialogDescription className="text-[var(--dg-text-secondary)]">配置 Agent 端口上报过滤规则</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 模式选择 */}
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">过滤模式</label>
<div className="flex gap-2">
<button
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'whitelist' ? 'bg-blue-500/20 border-blue-500 text-blue-400' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
onClick={() => setPortPolicyMode('whitelist')}
>
白名单 ─ 全量上报
</button>
<button
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'blacklist' ? 'bg-amber-500/20 border-amber-500 text-amber-400' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
onClick={() => setPortPolicyMode('blacklist')}
>
黑名单 ─ 仅上报选中
</button>
</div>
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1.5 leading-relaxed">
{portPolicyMode === 'whitelist'
? '所有发现的端口都上报,任意端口变化都会触发告警 + 恢复通知。'
: '仅上报选中端口的变化,其余端口变化静默忽略(不告警)。系统进程端口(sshd/rpcbind/systemd等)永远忽略。'}
</p>
</div>
<div className="flex justify-end gap-2 mt-5">
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => setPortPolicyDialogOpen(false)}>取消</Button>
<Button size="sm" className="h-7 text-[11px] bg-blue-600 hover:bg-blue-500" onClick={handleSavePortPolicy} disabled={portPolicySaving}>
{portPolicySaving ? '保存中…' : '保存策略'}
</Button>
{/* 端口白名单 — 仅黑名单模式可编辑 */}
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">
端口列表 <span className="text-[var(--dg-text-muted)]">(逗号分隔,如: 80,443,3306,6379)</span>
{portPolicyMode === 'whitelist' && <span className="text-blue-400 ml-1">— 白名单模式无需配置</span>}
</label>
<textarea
className={`w-full h-20 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 py-1.5 text-xs font-mono resize-none ${
portPolicyMode === 'whitelist' ? 'text-[var(--dg-text-muted)] opacity-50 cursor-not-allowed' : 'text-[var(--dg-text-primary)]'
}`}
value={portWhitelist.join(',')}
onChange={e => {
if (portPolicyMode === 'whitelist') return;
const val = e.target.value;
const nums = val.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n) && n > 0 && n <= 65535);
setPortWhitelist(nums);
}}
placeholder={portPolicyMode === 'whitelist' ? '白名单模式下所有端口自动上报' : '80,443,3306,6379'}
readOnly={portPolicyMode === 'whitelist'}
/>
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1">
已输入 {portWhitelist.length} 个端口
</p>
</div>
{/* 系统排除说明 */}
<div className="bg-amber-500/5 border border-amber-500/20 rounded p-2.5">
<p className="text-[10px] text-amber-300/80 leading-relaxed">
⚠️ 无论选择哪种模式,以下系统进程端口<b>永远不上报</b>:sshd、rpcbind、master、systemd、dbus-daemon、rsyslogd 等。Agent 采集时自动跳过。
</p>
</div>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => setPortPolicyDialogOpen(false)}>取消</Button>
<Button size="sm" className="h-7 text-[11px] bg-blue-600 hover:bg-blue-500" onClick={handleSavePortPolicy} disabled={portPolicySaving}>
{portPolicySaving ? '保存中…' : '保存策略'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}