- .gitignore 增加 internal/webui/dist/,构建产物不再纳入版本控制 (与 web/dist 一致;go:embed 仍依赖磁盘产物,不依赖 git 跟踪) - 从索引移除已跟踪的 internal/webui/dist(历史提交仍保留旧产物) - 新增 AGENTS.md:项目目的、架构、容器构建流程(前端 node:20-alpine 与 Go golang:1.22-alpine 均走 podman)、环境注意事项与编码约定
8.5 KiB
AGENTS.md — ws_usernode 项目指南
本文件供开发/编码 agent 快速上手:项目目的、架构、构建流程、环境注意事项与编码约定。
详细设计见 docs/PLAN.md,接口见 docs/API.md,部署见 docs/DEPLOY.md。
项目目的
服务器用户管理节点:对服务器上的外部用户(非本机管理员)做集中化管理,交付形态为单个 Go 二进制(前端产物 go:embed 嵌入,单文件分发)。核心能力:
- 系统账号生命周期:创建、禁用、删除、过期与回收;统一
ext_前缀、external组、口令锁定(仅 SSH 密钥登录) - SSH 公钥自助管理:用户上传/重命名/吊销,实时同步
authorized_keys - 新账号申请 + 管理员审批流(通过后自动建号并邮件通知)
- SMTP 邮件通知:OTP 登录验证码、审批结果、到期提醒、密码重置
- 管理操作审计(append-only,保留 30 天 + 手动/每日定时归档)
部署形态:单机节点 + RESTful API v1;系统账号操作经 sudoers 白名单命令,为后期"控制面 + 多节点 agent"预留。
技术栈与目录
| 层 | 技术 |
|---|---|
| 后端 | Go ≥ 1.22 + Gin + GORM(SQLite 开发 / MySQL 生产)+ log/slog |
| 前端 | Vue 3 + Vite + TypeScript + Element Plus + Pinia + Vue Router + Axios |
| 构建 | podman 容器构建前端(node:20-alpine),Go 二进制 go:embed 嵌入 |
| 配置 | TOML(config.example.toml),环境变量覆盖 USERNODE_<SECTION>_<FIELD> |
cmd/usernode/ CLI:serve / migrate / admin / user / help
internal/api/ HTTP handler + 审计 helper
internal/service/ 业务逻辑(user/approval/audit/key/auth/settings/lifecycle)
internal/model/ GORM 模型(User/Approval/AuditLog/MailLog/Session/Setting)
internal/pkg/ 校验等公共工具(validate.go:用户名/邮箱规则)
internal/auth/ OTP、验证码、会话、密码重置
internal/system/ 系统账号操作层(useradd/usermod/...,sudoers 白名单)
internal/webui/ go:embed 前端产物(dist/ 被 git 跟踪)
internal/router/ 路由装配
web/ 前端源码(Vue3)
docs/ PLAN.md / API.md / DEPLOY.md
deploy/ Containerfile(多阶段镜像)、sudoers、systemd 服务
开发构建流程
一切构建操作必须在 podman 容器内进行,不要尝试在宿主机 install / build。
# 一次性:构建前端 dev 镜像 + 初始化 node_modules volume
make web-install
# 前端热开发(容器挂载源码,5173 端口,API 代理到 Go 后端 8080)
make web-dev # 依赖宿主机 Go 后端已启动(make run)
# 标准构建(每次改前端后走这套):
make web-build # 容器内 npm run build → web/dist → 复制到 internal/webui/dist
make build-embed # 重新编译含前端产物的 Go 二进制 → bin/usernode
# 后端
make build # 仅编译 Go(不含前端)
make run # 本地运行(默认 config.toml)
make test # go test ./...
make vet
make migrate # 数据库迁移
make image # 多阶段容器镜像(deploy/Containerfile,内含 golang:1.22-alpine 构建阶段)
make clean # 清 bin/ 和 web/dist
关键细节
-
Go 容器构建方式:项目标准做法是
deploy/Containerfile多阶段构建(前端 node:20-alpine → Go golang:1.22-alpine → 运行镜像),make image一次产出含前端与后端的镜像。本地开发若要单独产 Go 二进制,应用与容器一致的 golang:1.22-alpine 环境(podman run --rm -v "$PWD":/src -w /src golang:1.22-alpine sh -c "CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/usernode ./cmd/usernode"),避免宿主机 go 1.26.7 版本漂移。 -
make build-embed直接用宿主机 go 编译(当前 1.26.7),与项目基线(go.modgo 1.22)不一致;仅在确认宿主机 go 版本与目标一致时使用,否则改走容器构建。 -
Go 测试/静态检查可用宿主机 go(
make test/make vet,仅本地开发验证用;正式产物以容器构建为准)。 -
前端构建链:
web/dist(gitignore)→internal/webui/dist(git 跟踪,必须随改动提交,否则 index.html 引用的新 hash 文件不入库)。 -
重建前清空
internal/webui/dist:make web-build内是cp -r合并复制,不清旧 hash 文件会导致go:embed把新旧产物一起打进二进制。标准做法:rm -rf internal/webui/dist && mkdir -p internal/webui/dist && make web-build。 -
新增 npm 依赖(在容器内安装,同步宿主机 package.json / package-lock.json):
podman run --rm --network=host \ -v "$PWD/web:/app" -v ws_usernode_node_modules:/app/node_modules \ ws-usernode-web-dev sh -c "npm install <pkg> --save" -
前端类型检查(容器内):
podman run --rm --network=host -v "$PWD/web:/app" -v ws_usernode_node_modules:/app/node_modules ws-usernode-web-dev sh -c "npx vue-tsc --noEmit"。 -
代理环境:容器构建无法继承宿主机 proxychains。Makefile 经
--build-arg传HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY。若宿主机 LD_PRELOAD 了 proxychains,podman 拉镜像会失败,需:env -u LD_PRELOAD -u PROXYCHAINS_CONF_FILE make web-build。 -
git 跟踪约定:
bin/、web/dist/、web/node_modules/、data/、config.toml均 gitignore;internal/webui/dist/例外(跟踪)。
环境注意事项(本开发机)
- 环境可能存在代理(proxychains node 等),本地回环可能会被拒绝,尝试绕过。
- 不要在本机装 Node 工具链,一律走 podman。
- 正式 Go 产物请走
golang:1.22-alpine容器(见"Go 容器构建方式"),测试/静态检查可用宿主机 go。 - podman 有镜像
localhost/ws-usernode-web-dev与 volumews_usernode_node_modules(缓存依赖,勿删)。 - 无法访问本地浏览器后端(browser-use 不可用),视觉验证交给用户。
编码约定与易错点
- 外部用户名校验(
internal/pkg/validate.go):申请/创建时传不含前缀的用户名,须匹配^[a-z][a-z0-9]{1,31}$(小写开头、2~32 位);最终系统账号 =user_prefix+ 用户名,总长 ≤ 32。 - 申请页(ApplyView)已改为中文姓名:前端校验 2~10 个汉字,用
pinyin-pro({ toneType:'none', type:'array', surname:'head' },surname 处理姓氏多音字如 曾→zeng、单→shan)转拼音作为用户名提交;alert 实时预览ext_<username>。改这里时保持"姓名→拼音→提交"链路与surname:'head'选项。 - 同名处理:提交申请时后端
usernameAvailable拦截"用户表已存在 + 同名待审批",返回ErrUsernameTaken→ HTTP 409;审批通过时复查用户表(防审批期间被手动创建)。前端 axios 拦截器统一弹出后端 error 文案,失败也进审计。 - 审计 append-only:业务代码只允许
AuditService.Record写入(禁 Update/Delete);Result∈ success/failed。AuditFilter.Result支持按结果筛选(管理员"错误日志"= 审计页选"失败"),导出 CSV 仅带时间范围(有意为之,勿随意改)。 - 审计清理:
audit.archive_dir留空 = 不归档也不清理(防丢审计);生产配置目录后超期审计先归档 CSV 再删除。 - 系统账号操作:
system.dry_run=true时只打印命令;生产sudo=true走 sudoers 白名单(deploy/sudoers),节点以专有用户运行。 - 邮件:SMTP 留空则禁用邮件;OTP 双通道(邮件 +
usernode user otp --username ext_xxx)同验证码、同 10 分钟有效期、同 60s 冷却。 - 前端主题:主题定制集中在
web/src/styles/theme.css(CSS 变量覆盖 Element Plus)+web/src/stores/theme.ts(暗色切换,localStorage keyws-usernode-theme);index.html有防暗色闪烁内联脚本。改主题优先改变量而非逐页样式。 - 测试:后端
go test ./...;前端无测试框架,靠 vue-tsc + 构建。
其他补充
- 版本号在 Makefile
VERSION(当前0.3.0-m5),编译期-X main.version注入。 - 新模型必须注册进
internal/model的模型列表以触发 AutoMigrate。 - git 暂存时仅暂存已知改动,不要直接全部暂存。提交前必须和用户确认,即使在完全访问(YOLO)的模式下也需要先询问用户再提交。
- 数据库迁移:开发可直接
make migrate;生产 MySQL 切换参考deploy/migrate-sqlite2mysql.sh。