# 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_
_` | ``` 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。 ```bash # 一次性:构建前端 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.mod `go 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): ```bash 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 --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` 与 volume `ws_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_`。改这里时保持"姓名→拼音→提交"链路与 `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 key `ws-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`。