基于 WEB-UI-Development-Guide 标准的整体升级,并更名 LokiDeploy → opsx-loki。 前端(Gin+React+shadcn/ui): - 按 02/07 章重构:页面域化拆分(无超 400 行文件)、PageContainer/ListQuery、 反馈分级(成功静默/失败 toast/危险确认)、401 单飞刷新+队列重放 - 暗色模式(next-themes)+ 语义 Token(--primary 品牌色/success/warning/info) - 部署向导(状态推导四步:环境→存储→Loki→采集,内嵌表单闭环) - 日志查询页:LogQL 标签构建器/拖拽直方图/日志上下文/Live Tail/查询历史 - 按用户+标签策略的查询授权(服务端强制合并策略) 后端(Go+Gin+JSON DB): - 简单用户管理(仅 admin,无 RBAC)+ TokenVersion 会话吊销 + 登录 IP 限流 - LDAP 接入:配置(AES 加密存储/hasPassword 脱敏)/测试连接/批量导入/登录 - 存储配置测试连接(SigV4 零依赖)+ 桶不存在自动创建 - Loki 三模式部署修复:SSH 密钥认证、3.x 配置模板、ruler 对象存储嵌套块、 凭证解密、孤儿 deploying 状态恢复、Grafana 部署(可选默认关)+面板导入 - DaemonSet 采集纯 YAML(去 helm 依赖)+ DNS 自愈回退 ClusterIP + 集群名标签 - 日志查询代理:cluster-svc 经 kube-apiserver proxy/url 直连 + cursor 翻页 - 统一 log 命名空间;部署就绪探活;Grafana 访问地址自动记录 修复:run.sh 增量编译检测、Sanitized 脱敏补全、LokiDeployment Update 白名单、 健康检查 CreatedAt 零值等 10+ 存量 bug。 数据兼容:DB 沿用旧 lokideploy.json(新名优先);localStorage 一次性迁移; HKDF 加密前缀保留(已加密凭证不受影响)。
10 KiB
AGENTS.md
本文件为各类 AI 编码代理(Cursor、Aider、Claude Code、Copilot 等)在本仓库中工作时的通用指引。
内容与 CLAUDE.md 保持同步;两份文件描述的是同一套约定,修改其一请同步另一份。
项目概览
opsx-loki 是一个基于 Gin (Go) + React/TypeScript + JSON 文件存储 的平台,用于把 Grafana
Loki 日志聚合系统部署到 Kubernetes 集群(通过 kubectl/helm)或裸金属 Linux 主机(通过 SSH)。
Go 二进制同时提供 REST API 服务和已构建的前端(SPA)静态资源。
默认登录账号: admin / admin123(首次启动时由 backend/seed/seed.go 写入数据库)。
常用命令
启动后端前的必备环境变量
后端在缺少以下变量时会 拒绝启动(由 backend/config/config.go 通过 log.Fatal 强制)。
./run.sh 会自动注入开发用默认值;直接 go run 或 Docker 则不会。
export JWT_SECRET=$(openssl rand -hex 32) # >= 32 字符
export ENCRYPTION_KEY=$(openssl rand -hex 32) # >= 32 字节(不是 16,详见「安全」一节)
开发模式(两个终端,热重载)
# 终端 1 — 后端(端口 8080)
cd backend && go run .
# 终端 2 — 前端(端口 3000,会把 /api 代理到 :8080,见 vite.config.ts)
cd frontend && npm install && npm run dev
也可以用 ./run.sh,它会同时拉起前后端并注入开发用默认密钥。
构建单体二进制(前端 + 后端)
bash build.sh # 产物:backend/bin/lokideploy,并把 frontend/dist 复制到 backend/dist
cd backend && go build -o bin/lokideploy . # 仅构建后端
cd frontend && npm run build # 仅构建前端(tsc -b && vite build)
测试与 lint
cd backend && go test ./... # 所有 Go 测试(utils、repository)
cd backend && go test ./utils/ -run TestName -v # 运行单个测试
cd frontend && npm run lint # ESLint
注意:models/ 目录目前 没有 测试,测试集中在 utils/ 和 repository/。
Docker
export JWT_SECRET=... ENCRYPTION_KEY=...
docker compose up -d # 开发用 compose(docker-compose.yml)
make build start stop logs status clean # Makefile 快捷命令
docker compose -f docker-compose.prod.yml up -d # 生产环境(读取 ./secrets/*.txt;先运行 `make secrets`)
架构
后端分层(backend/,模块路径 opsxloki,Go 1.24)
请求流转:router → middleware(JWT)→ controller → service → repository(JsonDB)。
config/—GetConfig()单例。读取环境变量;密钥缺失或长度不足时直接log.Fatal。routers/router.go— 所有路由注册在/api/v1下;注册 SPA 静态资源服务,并用NoRoute回退到index.html以支持前端路由。findDistPath()负责相对于可执行文件定位dist/(兼容go run的临时目录和打包后的二进制两种情况)。controllers/— 薄薄的 Gin handler,每个资源一个文件。职责:绑定/校验入参、调用 service、 用models.SanitizedJSON(...)包装返回结果。services/— 业务逻辑层。部署流水线就在这里:DeploymentService.Deploy()→LokiDeployer.Deploy(),按部署模式 (k8s-helm/k8s-yaml/host-binary)分发,实际通过 shell 调用kubectl/helm或 SSH。 其它 service:K8sClient、ssh(+ssh_errors)、storage、cluster_info, 以及较新的 collector(日志采集器) 模块(collector*.go— 部署客户端日志采集器)。middleware/jwt.go—JWTAuth()(解析后对照 JsonDB 校验TokenVersion,并以数据库最新 role 为准)、AdminAuth();ratelimit.go— 登录接口 IP 滑动窗口限流(20 次/分钟)。- 用户管理(简单两级,无 RBAC):
GET/POST /users、DELETE /users/:id、POST /users/:id/reset-password,全部仅 admin。改密/重置密码会递增用户TokenVersion, 旧 access+refresh token 立即失效(会话吊销)。 utils/—response.go(响应封装助手)、crypto.go(Argon2 + AES-GCM)、jwt.go。seed/— 数据库为空时写入默认 admin 用户。
数据层 — JSON 文件数据库(唯一数据源)
持久化是 单个 JSON 文件,位于 $DATA_DIR/opsxloki.json(兼容加载旧 lokideploy.json)(默认 ./data/)。
没有 SQL 数据库。repository.JsonDB 是一个与文件对应的内存结构:
- 并发模型(重要):写操作由
Lock()/RLock()保护,随后RequestSave()会通过一个 单一的、串行化的saveWorkergoroutine 调度一次 防抖(500ms)原子写 (写临时文件 →fsync→ rename)。worker 可能仍在运行时 绝对不要 直接调用Save(); 常规写入请用RequestSave()。 - 优雅关停的顺序很关键(
main.go+repository.Shutdown):停防抖定时器 → 关闭 worker 并 等待退出 → 最后一次doSave。顺序颠倒会导致更新丢失。 - 这意味着应用 只能单实例运行 — 不可以水平扩展。
NextID(table)为每个集合分配单调递增的整数 ID。
存在两份
JsonDB实现。 真正在用的是repository/db.go,被main.go、seed以及所有 service 通过repository.GetDB()调用。models/db.go是一份 正在迁移、尚未清理的旧副本 — 它仍然会被编译(属于package models),但已经没有任何代码调用models.GetDB/InitDB/Shutdown。新增数据库代码请写到repository,不要写到models。 在旧副本被删除前,修改其中一份时要保证另一份也能通过编译。
响应封装与脱敏
- 所有 API 响应都是
{ code, message, data }。code == 0表示成功;前端 (frontend/src/api/http.ts)在code !== 0时抛错。utils.Success/Error/...负责构造。 - 永远不要直接返回原始 model。 必须用
models.SanitizedJSON(...)包装。各 model 实现Sanitized()方法(models/sanitizer.go),用于剔除敏感字段(密码、kubeconfig、私钥、token)。 新增一个 model = 新增一个Sanitized()方法 + 在SanitizedJSON中补一个 case(单对象 + 切片各一个)。
凭证安全(utils/crypto.go)
- 密码用 Argon2 哈希。
- 凭证(kubeconfig、SSH 密码/密钥、存储密钥)用 AES-GCM 加密;AES 密钥由
ENCRYPTION_KEY经 HKDF 派生 得到(原始密钥永远不会被直接使用)。 ENCRYPTION_KEY必须 ≥ 32 字节。(较早的文档/脚本写的是 16 — 现在这样会导致启动失败。)
前端(frontend/,React 19 + Vite 7 + TypeScript)
前端开发标准:
docs/web-ui-guide/(9 章,含分章 AI 检查清单)。编码、Review、重构 时以该标准为基准;与标准冲突的既有代码即为待改进项。
- 技术栈:Tailwind、shadcn/ui(Radix 基础组件在
src/components/ui/,只保留实际 使用的,需要新组件npx shadcn add <name>)、react-router 7、react-hook-form + zod、 sonner(通知)、next-themes(暗色)。 - 页面按域组织(
src/pages/<domain>/):index.tsx编排(≤400 行)+components/(表单弹窗/卡片/详情)+hooks/(私有数据逻辑)。跨页复用第三次才晋升 到src/components/。 src/api/— API 客户端,一域一模块。http.ts提供:结构化ApiError(status/code)、 30s 超时、silent请求选项、401 单飞刷新 + 队列重放(refreshToken 走裸 fetch, 登录/轮询接口静默)、buildQuery编码 query。组件禁止绕过 api 层直接 fetch。- 凭证存取统一走
src/lib/auth-storage.ts,key 带命名空间(lokideploy.auth.*); redirect 回跳校验在src/lib/redirect.ts(防开放重定向)。 - 路由单源:
src/router/routes.tsx(meta: title/icon/order)驱动菜单、顶栏标题与document.title;守卫RequireAuth带来源路径回跳;404 走pages/not-found。 - 部署向导(
/wizard):状态推导式步骤地图(preflight 端点聚合各资源状态),四步:环境与 目标 → 存储 → Loki(+Grafana)→ 采集;k8s 资源统一默认log命名空间(可改)。 Grafana 为可选(默认关闭):k8s 用 NodePort(自动分配)+数据源 ConfigMap,主机用 tarball+systemd;平台已内置日志查询,通常无需 Grafana。 - LDAP 接入(参照 opsx-web-lite):设置页配置(SystemSetting 表 JSON,Bind 密码 AES 加密、
对外只回
hasPassword);启用后 ①登录支持 LDAP 用户验证并自动建号(authSource=ldap)②用户页 出现「从 LDAP 导入」(已导入置灰;本地同名转 LDAP);/settings/ldap/{test,users}端点。 - 日志查询(
/logs):数据源LogSources(cluster-svc=经 kubectl --raw service proxy 查集群内 Loki,不依赖 ClusterIP/DNS;url=直连;部署成功自动注册);/logs/{query,labels,label/*/values,histogram}代理端点(cursor 翻页);前端 LogQL 高亮补全 + 直方图 + 标签点击过滤 + Live Tail(3s 轮询)。 - 用户管理页(
/users,metaadminOnly: true— 菜单按角色过滤,后端 AdminAuth 兜底); 部署表单的 Loki 版本下拉来自GET /deployments/versions(失败回退兜底列表)。 - 列表数据一律
useListQuery(AbortController 取消 + 版本号防错序 + 分页);反馈分级: 成功静默(禁 toast.success)、失败 toast.error、危险操作 ConfirmDialog、加载失败渲染 ErrorState + 重试(与空态严格区分)。 - 主题:语义 Token 单源(
src/index.css,品牌色 =--primary,状态色 = success/warning/destructive/info);暗色.darkclass 由 next-themes 驱动 (keylokideploy.theme.mode),禁止硬编码调色板类(如bg-emerald-600)。 - 开发服务器跑在
:3000(strictPort),把/api代理到:8080;生产环境下由 Go 二进制直接提供dist/静态服务。构建指纹(__APP_VERSION__/__COMMIT_HASH__) 在 Settings「关于」展示。
Loki 部署相关细节
- 合法的部署模式:
k8s-helm、k8s-yaml、host-binary。K8s 模式需要一个 Cluster(kubeconfig);host-binary需要一个 SSH Host,会安装 Loki + systemd。 - Loki 版本会对照
services.LokiVersionList校验(最低支持主版本:2) — 见services/deployment.go中的ValidateLokiVersion。 kubectl/helm在 Docker 镜像中安装时做了 SHA256 校验(见Dockerfile)。