diff --git a/1-AgentSkills/coding-go-gin-gorm/SKILL.md b/1-AgentSkills/coding-go-gin-gorm/SKILL.md new file mode 100644 index 0000000..aaff56d --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/SKILL.md @@ -0,0 +1,425 @@ +--- +name: developing-go-gin-gorm +description: Generates and reviews Go backend code using GIN and GORM frameworks. Enforces layered architecture (handler→service→dao), unified API response format, POST+RequestBody API design, DTO naming conventions, Chinese comments, Asia/Shanghai timezone, structured logging, and framework best practices. Trigger on Go API development, GIN handler creation, GORM repository implementation, or code review requests. +argument-hint: " " e.g., "create user-handler", "review service/order.go", "scaffold api/v1/product" +allowed-tools: + - Read + - Write + - Edit + - Glob + - Grep + - Bash +--- + +# Go GIN/GORM 开发规范 Skill + +## 触发条件 +- 用户请求创建/修改 Go 后端代码(GIN handler、GORM dao、service) +- 用户请求代码审查 +- 用户提及 API 开发、数据库操作、统一响应、日志、时间处理 +- 用户请求设计 API 接口、DTO 结构 + +## 上下文收集 + +执行前先收集项目信息: + +!`ls -la go.mod go.sum 2>/dev/null || echo "No go.mod found"` + +!`head -20 go.mod 2>/dev/null || echo ""` + +## $ARGUMENTS 解析 + +期望格式:` ` + +| action | 说明 | +|--------|------| +| `create` | 创建新文件(handler/service/dao/dto) | +| `review` | 审查现有代码 | +| `scaffold` | 生成完整模块骨架 | +| `fix` | 修复不符合规范的代码 | + +--- + +## Plan 阶段 + +### 产物清单(按 action 确定) + +| action | 产物 | +|--------|------| +| `create handler` | `/api/xxx_handler.go` 或 `/internal/handler/xxx.go` | +| `create service` | `/internal/service/xxx_service.go` | +| `create dao` | `/internal/dao/xxx_dao.go` | +| `create dto` | `/internal/model/dto/xxx_dto.go` | +| `scaffold` | 上述全部 + entity | + +### 决策点 + +1. **目录风格**:检查项目是用 `/api` 还是 `/internal/handler` +2. **模块命名**:从 $ARGUMENTS 提取资源名(如 `user`、`order`) +3. **是否已存在**:先 Glob 检查目标文件 + +--- + +## Execute 阶段 + +### Handler 层编写规则 + +``` +1. 仅做:参数解析 → 调用 service → 返回响应 +2. 禁止:编写业务逻辑、直接操作数据库 +3. 必须:使用 common.ResponseSuccess / common.ResponseError +4. 错误处理:gorm.ErrRecordNotFound → CodeNotFound +``` + +### Service 层编写规则 + +``` +1. 编排 dao 层完成业务 +2. 记录关键业务日志(Info 级别) +3. 错误包装:fmt.Errorf("xxx: %w", err) +4. 业务异常记录 Warning 级别日志 +``` + +### DAO 层编写规则 + +``` +1. 封装所有 GORM 操作 +2. 禁止在 service 层写 SQL +3. 复杂查询用 Raw/Exec +4. 善用链式调用,但复杂场景优先原生 SQL +``` + +### 统一响应格式(强制) + +```go +// 成功 +common.ResponseSuccess(c, data) +common.ResponseSuccessWithMessage(c, data, "创建成功") + +// 失败 +common.ResponseError(c, common.CodeParamError, "参数错误") +common.ResponseErrorWithDetail(c, common.CodeServerError, "系统错误", err) +``` + +错误码定义 → 读取 `reference/error-codes.go` + +### 注释规范(强制中文) + +```go +// GetUserByID 根据用户ID获取用户信息 +// @param ctx context.Context - 请求上下文 +// @param userID int64 - 用户唯一ID +// @return *model.User - 用户信息,未找到返回nil +// @return error - 查询错误 +func (s *UserService) GetUserByID(ctx context.Context, userID int64) (*model.User, error) +``` + +--- + +## API 设计规范(强制) + +### 核心原则:POST + RequestBody + +``` +所有 API 优先使用 POST 方法,参数通过 RequestBody 传递 +避免使用 PathVariables 和 RequestParams +``` + +### 禁止与推荐 + +| 禁止 | 推荐 | +|------|------| +| `GET /api/projects/{project_id}` | `POST /api/projects/detail` + RequestBody | +| `GET /api/users?role=admin&page=1` | `POST /api/users/list` + RequestBody | +| URL 中传递敏感信息 | RequestBody 传递所有参数 | + +### API 路径命名规范 + +| 操作 | 后缀 | 示例 | +|------|------|------| +| 列表查询 | `/list` | `POST /api/projects/list` | +| 详情查询 | `/detail` | `POST /api/projects/detail` | +| 创建 | `/create` | `POST /api/projects/create` | +| 更新 | `/update` | `POST /api/projects/update` | +| 删除 | `/delete` | `POST /api/projects/delete` | +| 同步 | `/sync` | `POST /api/jenkins/organizations/sync` | +| 触发 | `/trigger` | `POST /api/builds/trigger` | + +### DTO 命名规范 + +| 类型 | 命名格式 | 示例 | +|------|----------|------| +| 列表请求 | `List{资源}Request` | `ListBuildsRequest` | +| 详情请求 | `Get{资源}Request` | `GetBuildRequest` | +| 创建请求 | `Create{资源}Request` | `CreateProjectRequest` | +| 更新请求 | `Update{资源}Request` | `UpdateProjectRequest` | +| 删除请求 | `Delete{资源}Request` | `DeleteProjectRequest` | +| 列表响应 | `List{资源}Response` | `ListBuildsResponse` | +| 详情响应 | `{资源}DetailResponse` | `BuildDetailResponse` | + +### 通用分页结构 + +```go +// 请求 +type PageRequest struct { + Page int `json:"page" binding:"required,min=1"` + PageSize int `json:"page_size" binding:"required,min=1,max=100"` +} + +// 响应 +type ListResponse struct { + List []interface{} `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} +``` + +### 模块错误码范围 + +| 范围 | 模块 | +|------|------| +| 0 | 成功 | +| 1000-1999 | 通用错误 | +| 2000-2999 | 用户/权限 | +| 3000-3999 | Jenkins | +| 4000-4999 | 项目管理 | +| 5000-5999 | Exchange-Hub | + +详细规范 → 读取 `reference/api-design-spec.md` + +--- + +## 日志规范(强制) + +### 指定框架 +项目统一使用 `rmdc-common/wdd_log/log_utils.go` + +### 日志级别使用场景 + +| 级别 | 使用场景 | 示例 | +|------|----------|------| +| `Debug` | 开发调试,详细流程、变量值 | `log.Debug(ctx, "查询参数", map[string]interface{}{"userID": id})` | +| `Info` | 关键业务节点 | `log.Info(ctx, "用户登录成功", ...)` / `log.Info(ctx, "订单创建成功", ...)` | +| `Warning` | 可预期非致命异常,程序可继续 | `log.Warning(ctx, "外部API超时,启用备用方案", ...)` | +| `Error` | 严重错误,业务流程中断 | `log.Error(ctx, "数据库连接失败", ...)` 必须记录堆栈 | + +### 日志内容要求 +``` +1. 简练、关键 +2. 必须包含 TraceID、UserID 等追溯信息 +3. Error 级别必须记录完整错误堆栈 +``` + +### 日志记录位置 + +| 层级 | 记录内容 | +|------|----------| +| Handler | 使用 `ResponseErrorWithDetail` 自动记录 Error 日志 | +| Service | 关键业务操作记录 Info;业务异常记录 Warning | +| DAO | 一般不记录日志,错误向上抛出 | + +--- + +## 时间处理(强制东八区) + +### 核心规则 +``` +时区:Asia/Shanghai (UTC+8) +格式:RFC3339 +``` + +### 禁止与必须 + +| 禁止 | 必须使用 | +|------|----------| +| `time.Now()` | `TimeUtils.Now()` | +| `time.Parse()` | `TimeUtils.Parse()` | +| 直接格式化 | `TimeUtils.Format()` | + +### 工具库位置 +- 后端:`rmdc-common/utils/TimeUtils.go` +- 前端:`TonyMask/src/utils/timeUtils.ts` + +### 使用示例 +```go +// ✅ 正确 +now := TimeUtils.Now() +timestamp := TimeUtils.Now().Format(time.RFC3339) + +// ❌ 错误 +now := time.Now() // 禁止直接使用 +``` + +--- + +## 框架使用规范 + +### GIN 框架 + +#### 路由组织(强制分组) +```go +// ✅ 正确:使用路由分组 +v1 := r.Group("/api/v1") +{ + users := v1.Group("/users") + { + users.GET("/:id", userHandler.GetByID) + users.POST("/", userHandler.Create) + } +} + +// ❌ 错误:扁平路由 +r.GET("/api/v1/users/:id", ...) +``` + +#### 中间件使用 +```go +// 全局中间件 +r.Use(middleware.Recovery()) // 恢复 +r.Use(middleware.Logger()) // 日志 +r.Use(middleware.CORS()) // 跨域 + +// 路由组中间件 +authGroup := r.Group("/admin") +authGroup.Use(middleware.Auth()) +``` + +#### 响应规范 +``` +所有 API 响应必须通过 pkg/common 统一响应函数 +禁止直接使用 c.JSON()、c.String() 等 +``` + +### GORM 框架 + +#### 操作位置 +``` +所有 GORM 操作必须在 dao 层 +严禁在 service 层拼接查询 +``` + +#### 链式调用 vs 原生 SQL + +| 场景 | 推荐方式 | +|------|----------| +| 简单 CRUD | 链式调用 `db.Where().First()` | +| 复杂查询(多表 JOIN、子查询) | `Raw()` / `Exec()` 原生 SQL | +| 批量操作 | `Raw()` / `Exec()` 保证性能 | + +```go +// 简单查询 - 链式调用 +db.Where("status = ?", 1).Find(&users) + +// 复杂查询 - 原生 SQL +db.Raw(` + SELECT u.*, COUNT(o.id) as order_count + FROM users u + LEFT JOIN orders o ON u.id = o.user_id + WHERE u.status = ? + GROUP BY u.id +`, 1).Scan(&results) +``` + +#### 错误处理 +```go +// 必须处理 ErrRecordNotFound +if errors.Is(err, gorm.ErrRecordNotFound) { + common.ResponseError(c, common.CodeNotFound, "资源不存在") + return +} +``` + +--- + +## Verify 阶段 Checklist + +### 结构检查 +- [ ] 依赖方向正确:handler → service → dao(无反向引用) +- [ ] handler 层无业务逻辑 +- [ ] dao 层无 service 引用 +- [ ] 使用 internal 包保护私有代码 + +### 响应检查 +- [ ] 所有 API 使用 `common.ResponseSuccess/Error` +- [ ] 错误码来自 `common.Code*` 常量 +- [ ] 时间戳格式为 RFC3339 +- [ ] 无直接 `c.JSON()` 调用 + +### 代码检查 +- [ ] 公开函数/结构体有中文注释 +- [ ] 注释格式:`// 函数名 功能描述` +- [ ] 无直接 `time.Now()` 调用 +- [ ] 无丢弃的 error(`_ = err` 禁止) +- [ ] 包名小写无下划线 + +### 日志检查 +- [ ] 使用项目统一日志库 +- [ ] Error 日志包含完整堆栈 +- [ ] 关键业务操作有 Info 日志 +- [ ] 日志包含 TraceID 等追溯信息 + +### GORM 检查 +- [ ] `gorm.ErrRecordNotFound` 已处理 +- [ ] 复杂查询在 dao 层使用 Raw/Exec +- [ ] 无 service 层直接 DB 操作 + +### GIN 检查 +- [ ] 使用路由分组组织 API +- [ ] 通用逻辑使用中间件处理 +- [ ] 响应通过统一函数返回 + +### API 设计检查 +- [ ] 使用 POST + RequestBody(非 GET + PathVariables) +- [ ] API 路径使用正确后缀(/list, /detail, /create 等) +- [ ] DTO 命名符合规范(List/Get/Create/Update/Delete + 资源 + Request/Response) +- [ ] 分页请求嵌入 PageRequest +- [ ] 分页响应包含 list/total/page/page_size +- [ ] 敏感信息不在 URL 中 +- [ ] 请求体必须验证(ShouldBindJSON) + +--- + +## 常见陷阱 + +| 陷阱 | 正确做法 | +|------|----------| +| handler 写业务逻辑 | 移到 service 层 | +| 直接 `c.JSON()` | 用 `common.ResponseSuccess()` | +| 忽略 `ErrRecordNotFound` | 转为 `CodeNotFound` 返回 | +| `time.Now()` | `TimeUtils.Now()` | +| 英文注释 | 改为中文 | +| dao 引用 service | 违反依赖原则,重构 | +| service 写 SQL | 移到 dao 层 | +| 扁平路由 | 使用 Router Group | +| 日志缺少上下文 | 添加 TraceID、UserID | +| Error 日志无堆栈 | 记录完整错误信息 | +| `GET /api/users/{id}` | `POST /api/users/detail` + RequestBody | +| URL 传参数 `?page=1` | RequestBody 传递 | +| DTO 命名不规范 | 使用 `List/Get/Create/Update/Delete` + 资源名 | +| 敏感信息在 URL | 移到 RequestBody | + +--- + +## Reference 文件索引 + +| 场景 | 读取文件 | +|------|----------| +| 需要完整目录结构说明 | `reference/project-structure.md` | +| 需要响应结构体定义 | `reference/api-response-spec.md` | +| 需要错误码完整列表 | `reference/error-codes.go` | +| 需要编码规范细节 | `reference/coding-standards.md` | +| 需要日志使用详细说明 | `reference/logging-standards.md` | +| 需要时间处理详细说明 | `reference/time-handling.md` | +| 需要框架使用详细说明 | `reference/framework-usage.md` | +| 需要 API 设计详细说明 | `reference/api-design-spec.md` | +| 需要代码示例 | `examples/*.go` | + +--- + +## 快速命令 + +验证项目结构: +```bash +./scripts/validate-structure.sh +``` diff --git a/1-AgentSkills/coding-go-gin-gorm/examples/dao-example.go b/1-AgentSkills/coding-go-gin-gorm/examples/dao-example.go new file mode 100644 index 0000000..6736817 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/examples/dao-example.go @@ -0,0 +1,55 @@ +package dao + +import ( + "context" + + "gorm.io/gorm" + + "my-project/internal/model/entity" +) + +// UserDAO 用户数据访问对象 +type UserDAO struct { + db *gorm.DB +} + +// NewUserDAO 创建用户DAO实例 +// @param db *gorm.DB - 数据库连接 +// @return *UserDAO - DAO实例 +func NewUserDAO(db *gorm.DB) *UserDAO { + return &UserDAO{db: db} +} + +// FindByID 根据ID查询用户 +// @param ctx context.Context - 请求上下文 +// @param id int64 - 用户ID +// @return *entity.User - 用户实体 +// @return error - 查询错误,未找到返回gorm.ErrRecordNotFound +func (d *UserDAO) FindByID(ctx context.Context, id int64) (*entity.User, error) { + var user entity.User + if err := d.db.WithContext(ctx).First(&user, id).Error; err != nil { + return nil, err + } + return &user, nil +} + +// Create 创建用户 +// @param ctx context.Context - 请求上下文 +// @param user *entity.User - 用户实体 +// @return error - 创建错误 +func (d *UserDAO) Create(ctx context.Context, user *entity.User) error { + return d.db.WithContext(ctx).Create(user).Error +} + +// FindByEmail 根据邮箱查询用户 +// @param ctx context.Context - 请求上下文 +// @param email string - 用户邮箱 +// @return *entity.User - 用户实体 +// @return error - 查询错误 +func (d *UserDAO) FindByEmail(ctx context.Context, email string) (*entity.User, error) { + var user entity.User + if err := d.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} diff --git a/1-AgentSkills/coding-go-gin-gorm/examples/handler-example.go b/1-AgentSkills/coding-go-gin-gorm/examples/handler-example.go new file mode 100644 index 0000000..2fe2dba --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/examples/handler-example.go @@ -0,0 +1,70 @@ +package handler + +import ( + "errors" + "strconv" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "my-project/internal/model/dto" + "my-project/internal/service" + "my-project/pkg/common" +) + +// UserHandler 用户相关API处理器 +type UserHandler struct { + userService *service.UserService +} + +// NewUserHandler 创建用户Handler实例 +// @param userService *service.UserService - 用户服务 +// @return *UserHandler - Handler实例 +func NewUserHandler(userService *service.UserService) *UserHandler { + return &UserHandler{userService: userService} +} + +// GetUserByID 根据ID获取用户信息 +// @param c *gin.Context - GIN上下文 +func (h *UserHandler) GetUserByID(c *gin.Context) { + // 1. 参数解析 + idStr := c.Param("id") + userID, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + common.ResponseError(c, common.CodeParamError, "用户ID格式错误") + return + } + + // 2. 调用Service + user, err := h.userService.GetUserByID(c.Request.Context(), userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + common.ResponseError(c, common.CodeNotFound, "用户不存在") + return + } + common.ResponseErrorWithDetail(c, common.CodeServerError, "获取用户失败", err) + return + } + + // 3. 成功响应 + common.ResponseSuccess(c, user) +} + +// CreateUser 创建用户 +// @param c *gin.Context - GIN上下文 +func (h *UserHandler) CreateUser(c *gin.Context) { + var req dto.CreateUserRequest + + if err := c.ShouldBindJSON(&req); err != nil { + common.ResponseErrorWithDetail(c, common.CodeValidationFail, "参数验证失败", err) + return + } + + user, err := h.userService.CreateUser(c.Request.Context(), &req) + if err != nil { + common.ResponseErrorWithDetail(c, common.CodeBusiness, "创建用户失败", err) + return + } + + common.ResponseSuccessWithMessage(c, user, "用户创建成功") +} diff --git a/1-AgentSkills/coding-go-gin-gorm/examples/service-example.go b/1-AgentSkills/coding-go-gin-gorm/examples/service-example.go new file mode 100644 index 0000000..0fb9d7b --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/examples/service-example.go @@ -0,0 +1,60 @@ +package service + +import ( + "context" + "fmt" + + "my-project/internal/dao" + "my-project/internal/model/dto" + "my-project/internal/model/entity" + "my-project/pkg/log" +) + +// UserService 用户业务服务 +type UserService struct { + userDAO *dao.UserDAO +} + +// NewUserService 创建用户服务实例 +// @param userDAO *dao.UserDAO - 用户数据访问对象 +// @return *UserService - 服务实例 +func NewUserService(userDAO *dao.UserDAO) *UserService { + return &UserService{userDAO: userDAO} +} + +// GetUserByID 根据用户ID获取用户信息 +// @param ctx context.Context - 请求上下文 +// @param userID int64 - 用户唯一ID +// @return *entity.User - 用户实体 +// @return error - 查询错误 +func (s *UserService) GetUserByID(ctx context.Context, userID int64) (*entity.User, error) { + user, err := s.userDAO.FindByID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("查询用户失败: %w", err) + } + return user, nil +} + +// CreateUser 创建新用户 +// @param ctx context.Context - 请求上下文 +// @param req *dto.CreateUserRequest - 创建请求 +// @return *entity.User - 创建的用户实体 +// @return error - 创建错误 +func (s *UserService) CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error) { + user := &entity.User{ + Username: req.Username, + Email: req.Email, + } + + if err := s.userDAO.Create(ctx, user); err != nil { + return nil, fmt.Errorf("创建用户失败: %w", err) + } + + // 记录关键业务日志 + log.Info(ctx, "用户创建成功", map[string]interface{}{ + "userID": user.ID, + "username": user.Username, + }) + + return user, nil +} diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/api-design-spec.md b/1-AgentSkills/coding-go-gin-gorm/reference/api-design-spec.md new file mode 100644 index 0000000..4ee91a2 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/api-design-spec.md @@ -0,0 +1,330 @@ +# API 设计规范 + +## 核心原则 + +### 1. 使用 POST + RequestBody + +> **核心规范**: 所有 API 优先使用 POST 方法,参数通过 RequestBody 传递 + +```go +// ✅ 推荐方式 +POST /api/jenkins/builds/list +{ + "organization_folder": "Backend", + "repository_name": "cmii-fly-center", + "branch_name": "master", + "page": 1, + "page_size": 10 +} + +// ❌ 避免使用 +GET /api/jenkins/organizations/{org}/repositories/{repo}/branches/{branch}/builds?page=1&page_size=10 +``` + +### 2. 避免 PathVariables + +```go +// ❌ 不推荐 +GET /api/projects/{project_id} +GET /api/builds/{build_id}/console + +// ✅ 推荐 +POST /api/projects/detail +{ + "project_id": "namespace_abc12345" +} + +POST /api/builds/console +{ + "organization_folder": "Backend", + "repository_name": "cmii-fly-center", + "branch_name": "master", + "build_number": 123 +} +``` + +### 3. 避免 RequestParams + +```go +// ❌ 不推荐 +GET /api/users/list?role=admin&status=active&page=1 + +// ✅ 推荐 +POST /api/users/list +{ + "role": "admin", + "status": "active", + "page": 1, + "page_size": 20 +} +``` + +--- + +## 统一响应格式 + +### 成功响应 + +```json +{ + "code": 0, + "message": "success", + "data": { + // 业务数据 + } +} +``` + +### 分页响应 + +```json +{ + "code": 0, + "message": "success", + "data": { + "list": [...], + "total": 100, + "page": 1, + "page_size": 20 + } +} +``` + +### 错误响应 + +```json +{ + "code": 1001, + "message": "参数错误: organization_folder不能为空", + "data": null +} +``` + +--- + +## 请求结构规范 + +### 通用分页请求 + +```go +type PageRequest struct { + Page int `json:"page" binding:"required,min=1"` + PageSize int `json:"page_size" binding:"required,min=1,max=100"` +} +``` + +### 通用筛选请求 + +```go +type ListRequest struct { + PageRequest + Keyword string `json:"keyword,omitempty"` // 搜索关键词 + Status string `json:"status,omitempty"` // 状态筛选 + SortBy string `json:"sort_by,omitempty"` // 排序字段 + SortOrder string `json:"sort_order,omitempty"` // asc/desc +} +``` + +--- + +## API 命名规范 + +### 操作类型后缀 + +| 操作 | 后缀 | 示例 | +|------|------|------| +| 列表查询 | `/list` | `/api/projects/list` | +| 详情查询 | `/detail` | `/api/projects/detail` | +| 创建 | `/create` | `/api/projects/create` | +| 更新 | `/update` | `/api/projects/update` | +| 删除 | `/delete` | `/api/projects/delete` | +| 同步 | `/sync` | `/api/jenkins/organizations/sync` | +| 触发 | `/trigger` | `/api/builds/trigger` | +| 导出 | `/export` | `/api/projects/export` | + +### 模块前缀 + +| 模块 | 前缀 | +|------|------| +| Jenkins | `/api/jenkins/` | +| 项目管理 | `/api/projects/` | +| 用户 | `/api/users/` | +| 权限 | `/api/permissions/` | +| 审计 | `/api/audit/` | +| Exchange-Hub | `/api/exchange-hub/` | +| DCU | `/api/dcu/` | + +--- + +## Handler 实现模板 + +```go +// ListBuilds 获取构建列表 +// @Summary 获取构建列表 +// @Tags 构建管理 +// @Accept json +// @Produce json +// @Param request body dto.ListBuildsRequest true "请求参数" +// @Success 200 {object} response.Response{data=dto.ListBuildsResponse} +// @Router /api/jenkins/builds/list [post] +func (h *BuildHandler) ListBuilds(c *gin.Context) { + var req dto.ListBuildsRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.ParamError(c, err) + return + } + + resp, err := h.buildService.ListBuilds(c.Request.Context(), &req) + if err != nil { + response.Error(c, err) + return + } + + response.Success(c, resp) +} +``` + +--- + +## DTO 设计规范 + +### 请求 DTO 命名 + +```go +// 列表请求: List{资源}Request +type ListBuildsRequest struct { + PageRequest + OrganizationFolder string `json:"organization_folder" binding:"required"` + RepositoryName string `json:"repository_name" binding:"required"` + BranchName string `json:"branch_name,omitempty"` +} + +// 详情请求: Get{资源}Request 或 {资源}DetailRequest +type GetBuildRequest struct { + OrganizationFolder string `json:"organization_folder" binding:"required"` + RepositoryName string `json:"repository_name" binding:"required"` + BranchName string `json:"branch_name" binding:"required"` + BuildNumber int `json:"build_number" binding:"required"` +} + +// 创建请求: Create{资源}Request +type CreateProjectRequest struct { + Name string `json:"name" binding:"required"` + Namespace string `json:"namespace" binding:"required"` + Province string `json:"province" binding:"required"` + City string `json:"city" binding:"required"` +} + +// 更新请求: Update{资源}Request +type UpdateProjectRequest struct { + ProjectID string `json:"project_id" binding:"required"` + Name string `json:"name,omitempty"` + Province string `json:"province,omitempty"` + City string `json:"city,omitempty"` +} + +// 删除请求: Delete{资源}Request +type DeleteProjectRequest struct { + ProjectID string `json:"project_id" binding:"required"` +} +``` + +### 响应 DTO 命名 + +```go +// 列表响应: List{资源}Response +type ListBuildsResponse struct { + List []*BuildDTO `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +// 详情响应: {资源}DetailResponse 或直接使用 {资源}DTO +type BuildDetailResponse struct { + *BuildDTO + ConsoleOutput string `json:"console_output,omitempty"` +} +``` + +--- + +## 错误码规范 + +### 错误码范围 + +| 范围 | 模块 | +|------|------| +| 1000-1999 | 通用错误 | +| 2000-2999 | 用户/权限 | +| 3000-3999 | Jenkins模块 | +| 4000-4999 | 项目管理 | +| 5000-5999 | Exchange-Hub | +| 6000-6999 | Watchdog | + +### 通用错误码 + +| 错误码 | 说明 | +|--------|------| +| 0 | 成功 | +| 1001 | 参数错误 | +| 1002 | 未授权 | +| 1003 | 禁止访问 | +| 1004 | 资源不存在 | +| 1005 | 内部错误 | + +--- + +## 前端调用示例 + +```typescript +// api/modules/jenkins.ts +export const jenkinsApi = { + // 获取构建列表 + listBuilds: (data: ListBuildsRequest) => + request.post('/api/jenkins/builds/list', data), + + // 触发构建 + triggerBuild: (data: TriggerBuildRequest) => + request.post('/api/jenkins/builds/trigger', data), + + // 获取构建详情 + getBuildDetail: (data: GetBuildRequest) => + request.post('/api/jenkins/builds/detail', data), +}; +``` + +--- + +## 安全规范 + +### 1. 敏感字段不出现在 URL + +```go +// ❌ 敏感信息泄露到URL +GET /api/auth/login?username=admin&password=123456 + +// ✅ 使用RequestBody +POST /api/auth/login +{ + "username": "admin", + "password": "123456" +} +``` + +### 2. 必须验证请求体 + +```go +func (h *Handler) CreateProject(c *gin.Context) { + var req dto.CreateProjectRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.ParamError(c, err) + return + } + // 后续处理... +} +``` + +### 3. 审计敏感操作 + +所有写操作需通过审计中间件记录。 diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/api-response-spec.md b/1-AgentSkills/coding-go-gin-gorm/reference/api-response-spec.md new file mode 100644 index 0000000..b111a2e --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/api-response-spec.md @@ -0,0 +1,35 @@ +# API 响应规范 + +## 统一响应结构 + +```go +type Response struct { + Code int `json:"code"` // 业务状态码,0=成功 + Status int `json:"status"` // HTTP 状态码 + Timestamp string `json:"timestamp"` // RFC3339 东八区 + Data interface{} `json:"data"` // 业务数据 + Message string `json:"message,omitempty"` // 消息 + Error string `json:"error,omitempty"` // 错误详情 +} +``` + +## 使用函数 + +| 场景 | 函数 | +|------|------| +| 查询成功 | `ResponseSuccess(c, data)` | +| 操作成功 | `ResponseSuccessWithMessage(c, data, "msg")` | +| 普通错误 | `ResponseError(c, code, "msg")` | +| 详细错误 | `ResponseErrorWithDetail(c, code, "msg", err)` | + +## HTTP 状态码映射 + +| 业务码 | HTTP 状态码 | +|--------|-------------| +| CodeSuccess | 200 | +| CodeParamError, CodeValidationFail | 400 | +| CodeUnauthorized | 401 | +| CodeForbidden | 403 | +| CodeNotFound | 404 | +| CodeTimeout | 408 | +| 其他 | 500 | diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/coding-standards.md b/1-AgentSkills/coding-go-gin-gorm/reference/coding-standards.md new file mode 100644 index 0000000..8fc48b7 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/coding-standards.md @@ -0,0 +1,44 @@ +# 编码规范 + +## 命名规范 + +| 类型 | 规则 | 示例 | +|------|------|------| +| 包名 | 小写单词,无下划线 | `service`, `utils` | +| 变量/函数 | 驼峰命名 | `getUserByID` | +| 公开标识 | 首字母大写 | `GetUserByID` | +| 接口 | 单方法以 `er` 结尾 | `Reader`, `Writer` | + +## 注释规范(中文,必须) + +```go +// GetUserByID 根据用户ID获取用户信息 +// @param ctx context.Context - 请求上下文 +// @param userID int64 - 用户唯一ID +// @return *model.User - 用户信息 +// @return error - 查询错误 +func (s *UserService) GetUserByID(ctx context.Context, userID int64) (*model.User, error) +``` + +## 错误处理 + +1. 必须 `if err != nil` 处理 +2. 用 `fmt.Errorf("xxx: %w", err)` 包装 +3. 禁止 `_ = err` 丢弃错误 +4. Handler 层必须通过统一响应返回 + +## 日志级别 + +| 级别 | 用途 | +|------|------| +| Debug | 开发调试,详细流程 | +| Info | 关键业务节点 | +| Warning | 可预期非致命异常 | +| Error | 严重错误,必须记录堆栈 | + +## 时间处理 + +- 时区:Asia/Shanghai (UTC+8) +- 格式:RFC3339 +- 禁止:`time.Now()` +- 使用:`TimeUtils.Now()` diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/error-codes.go b/1-AgentSkills/coding-go-gin-gorm/reference/error-codes.go new file mode 100644 index 0000000..a61ebe3 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/error-codes.go @@ -0,0 +1,35 @@ +package common + +// 业务状态码常量 +const ( + CodeSuccess = 0 // 成功 + CodeServerError = 10001 // 服务器内部错误 + CodeParamError = 10002 // 参数错误 + CodeUnauthorized = 10003 // 未授权 + CodeForbidden = 10004 // 禁止访问 + CodeNotFound = 10005 // 资源不存在 + CodeTimeout = 10006 // 请求超时 + CodeValidationFail = 10007 // 验证失败 + CodeBusiness = 20001 // 业务逻辑错误 (20001-29999) +) + +// CodeMessage 错误码消息映射 +var CodeMessage = map[int]string{ + CodeSuccess: "success", + CodeServerError: "服务器内部错误", + CodeParamError: "参数错误", + CodeUnauthorized: "未授权,请先登录", + CodeForbidden: "权限不足,禁止访问", + CodeNotFound: "请求的资源不存在", + CodeTimeout: "请求超时", + CodeValidationFail: "数据验证失败", + CodeBusiness: "业务处理失败", +} + +// GetMessage 根据错误码获取默认消息 +func GetMessage(code int) string { + if msg, ok := CodeMessage[code]; ok { + return msg + } + return "未知错误" +} diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/framework-usage.md b/1-AgentSkills/coding-go-gin-gorm/reference/framework-usage.md new file mode 100644 index 0000000..66af57a --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/framework-usage.md @@ -0,0 +1,264 @@ +# 框架使用规范 + +## GIN 框架 + +### 路由组织 + +#### 强制使用路由分组 (Router Group) + +```go +func SetupRouter(r *gin.Engine) { + // API 版本分组 + v1 := r.Group("/api/v1") + { + // 用户模块 + users := v1.Group("/users") + { + users.GET("/", userHandler.List) + users.GET("/:id", userHandler.GetByID) + users.POST("/", userHandler.Create) + users.PUT("/:id", userHandler.Update) + users.DELETE("/:id", userHandler.Delete) + } + + // 订单模块 + orders := v1.Group("/orders") + { + orders.GET("/", orderHandler.List) + orders.GET("/:id", orderHandler.GetByID) + orders.POST("/", orderHandler.Create) + } + } +} +``` + +#### 禁止扁平路由 + +```go +// ❌ 错误:扁平路由,难以维护 +r.GET("/api/v1/users", ...) +r.GET("/api/v1/users/:id", ...) +r.POST("/api/v1/users", ...) +r.GET("/api/v1/orders", ...) +``` + +### 中间件使用 + +#### 全局中间件 + +```go +func SetupMiddleware(r *gin.Engine) { + // Recovery - 恢复 panic,防止程序崩溃 + r.Use(middleware.Recovery()) + + // Logger - 请求日志记录 + r.Use(middleware.Logger()) + + // CORS - 跨域处理 + r.Use(middleware.CORS()) + + // TraceID - 请求追踪 + r.Use(middleware.TraceID()) +} +``` + +#### 路由组中间件 + +```go +// 需要认证的路由组 +authGroup := r.Group("/api/v1/admin") +authGroup.Use(middleware.Auth()) +{ + authGroup.GET("/dashboard", adminHandler.Dashboard) + authGroup.GET("/users", adminHandler.ListUsers) +} + +// 需要特定权限的路由组 +superAdmin := authGroup.Group("/super") +superAdmin.Use(middleware.RequireRole("super_admin")) +{ + superAdmin.DELETE("/users/:id", adminHandler.DeleteUser) +} +``` + +#### 常用中间件职责 + +| 中间件 | 职责 | +|--------|------| +| Recovery | 捕获 panic,返回 500 错误 | +| Logger | 记录请求日志(方法、路径、耗时等) | +| CORS | 处理跨域请求 | +| Auth | 验证用户身份(JWT/Session) | +| TraceID | 生成/传递请求追踪 ID | +| RateLimit | 请求频率限制 | + +### 响应规范 + +#### 强制使用统一响应 + +```go +// ✅ 正确:使用统一响应函数 +common.ResponseSuccess(c, data) +common.ResponseError(c, common.CodeParamError, "参数错误") + +// ❌ 错误:直接使用 GIN 原生方法 +c.JSON(200, data) +c.String(200, "success") +c.AbortWithStatusJSON(400, gin.H{"error": "bad request"}) +``` + +--- + +## GORM 框架 + +### 操作位置规范 + +``` +所有 GORM 操作必须在 dao 层实现 +严禁在 service 层直接操作数据库 +``` + +### 查询方式选择 + +#### 简单 CRUD - 链式调用 + +```go +// 单条查询 +var user entity.User +db.Where("id = ?", userID).First(&user) + +// 列表查询 +var users []entity.User +db.Where("status = ?", 1). + Order("created_at DESC"). + Limit(10). + Offset(0). + Find(&users) + +// 创建 +db.Create(&user) + +// 更新 +db.Model(&user).Updates(map[string]interface{}{ + "name": "new name", + "status": 1, +}) + +// 删除 +db.Delete(&user, userID) +``` + +#### 复杂查询 - Raw/Exec + +**推荐场景**: +- 多表 JOIN +- 子查询 +- 复杂聚合 +- 批量操作 +- 性能敏感场景 + +```go +// 多表 JOIN 查询 +type UserWithOrderCount struct { + entity.User + OrderCount int64 `json:"order_count"` +} + +var results []UserWithOrderCount +db.Raw(` + SELECT u.*, COUNT(o.id) as order_count + FROM users u + LEFT JOIN orders o ON u.id = o.user_id + WHERE u.status = ? + GROUP BY u.id + ORDER BY order_count DESC + LIMIT ? +`, 1, 10).Scan(&results) + +// 批量更新 +db.Exec(` + UPDATE orders + SET status = ? + WHERE user_id = ? AND status = ? +`, "completed", userID, "pending") + +// 复杂子查询 +db.Raw(` + SELECT * FROM users + WHERE id IN ( + SELECT user_id FROM orders + WHERE amount > ? + GROUP BY user_id + HAVING COUNT(*) > ? + ) +`, 1000, 5).Scan(&users) +``` + +### 错误处理 + +#### 必须处理 ErrRecordNotFound + +```go +// DAO 层 +func (d *UserDAO) FindByID(ctx context.Context, id int64) (*entity.User, error) { + var user entity.User + if err := d.db.WithContext(ctx).First(&user, id).Error; err != nil { + return nil, err // 包含 ErrRecordNotFound + } + return &user, nil +} + +// Handler 层 +user, err := h.userService.GetUserByID(ctx, userID) +if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + common.ResponseError(c, common.CodeNotFound, "用户不存在") + return + } + common.ResponseErrorWithDetail(c, common.CodeServerError, "查询失败", err) + return +} +``` + +### 事务处理 + +```go +// Service 层事务 +func (s *OrderService) CreateOrder(ctx context.Context, req *dto.CreateOrderRequest) error { + return s.db.Transaction(func(tx *gorm.DB) error { + // 1. 创建订单 + order := &entity.Order{...} + if err := tx.Create(order).Error; err != nil { + return fmt.Errorf("创建订单失败: %w", err) + } + + // 2. 扣减库存 + if err := tx.Model(&entity.Product{}). + Where("id = ? AND stock >= ?", req.ProductID, req.Quantity). + Update("stock", gorm.Expr("stock - ?", req.Quantity)).Error; err != nil { + return fmt.Errorf("扣减库存失败: %w", err) + } + + // 3. 创建支付记录 + payment := &entity.Payment{...} + if err := tx.Create(payment).Error; err != nil { + return fmt.Errorf("创建支付记录失败: %w", err) + } + + return nil + }) +} +``` + +### Context 传递 + +```go +// 必须使用 WithContext 传递上下文 +db.WithContext(ctx).First(&user, id) +db.WithContext(ctx).Create(&order) + +// 支持超时控制和取消 +ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) +defer cancel() +db.WithContext(ctx).Find(&users) +``` diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/logging-standards.md b/1-AgentSkills/coding-go-gin-gorm/reference/logging-standards.md new file mode 100644 index 0000000..51f06a6 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/logging-standards.md @@ -0,0 +1,100 @@ +# 日志规范 + +## 指定框架 + +项目统一使用内部日志库:`rmdc-common/wdd_log/log_utils.go` + +## 日志级别定义 + +### Debug +- **用途**:开发调试,记录程序执行流程、变量值等详细信息 +- **场景**:默认开发日志级别 +- **示例**: +```go +log.Debug(ctx, "开始处理用户请求", map[string]interface{}{ + "userID": userID, + "requestID": requestID, +}) +``` + +### Info +- **用途**:记录关键业务操作节点 +- **场景**:用户登录、订单创建、支付成功等关键业务 +- **示例**: +```go +log.Info(ctx, "用户登录成功", map[string]interface{}{ + "userID": user.ID, + "username": user.Username, + "ip": c.ClientIP(), +}) + +log.Info(ctx, "订单创建成功", map[string]interface{}{ + "orderID": order.ID, + "amount": order.Amount, + "userID": order.UserID, +}) +``` + +### Warning +- **用途**:记录可预期的、非致命的异常情况,程序仍可继续运行 +- **场景**:外部 API 超时启用备用方案、配置缺失使用默认值等 +- **示例**: +```go +log.Warning(ctx, "外部API调用超时,已启用备用方案", map[string]interface{}{ + "api": "payment-gateway", + "timeout": "5s", + "fallback": "local-cache", +}) +``` + +### Error +- **用途**:记录严重错误,导致当前业务流程无法继续 +- **场景**:数据库连接失败、关键参数校验失败等 +- **要求**:必须详细记录错误信息和堆栈 +- **示例**: +```go +log.Error(ctx, "数据库连接失败", map[string]interface{}{ + "host": dbConfig.Host, + "port": dbConfig.Port, + "error": err.Error(), + "stack": debug.Stack(), +}) +``` + +## 日志内容规范 + +### 必须包含 +1. **TraceID** - 请求追踪 ID +2. **UserID** - 用户标识(如适用) +3. **操作描述** - 简练的中文描述 +4. **关键参数** - 与操作相关的关键数据 + +### 格式要求 +```go +log.Info(ctx, "操作描述", map[string]interface{}{ + "key1": value1, + "key2": value2, +}) +``` + +## 各层日志职责 + +### Handler 层 +- 使用 `ResponseErrorWithDetail` 自动记录 Error 日志 +- 一般不主动记录日志 + +### Service 层 +- **Info**:关键业务操作成功(创建订单、支付、用户注册等) +- **Warning**:业务逻辑异常但可处理 +- **Error**:通过 ResponseErrorWithDetail 在 Handler 层统一记录 + +### DAO 层 +- 一般不记录日志 +- 错误向上抛出,由 Handler 层统一处理 + +## 禁止事项 + +1. 禁止在日志中记录敏感信息(密码、Token、完整银行卡号等) +2. 禁止使用 `fmt.Println` 或 `log.Println` +3. 禁止在循环中大量记录日志 +4. Error 日志禁止缺少堆栈信息 diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/project-structure.md b/1-AgentSkills/coding-go-gin-gorm/reference/project-structure.md new file mode 100644 index 0000000..37df808 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/project-structure.md @@ -0,0 +1,39 @@ +# 项目目录结构规范 + +## 核心目录 + +| 目录 | 职责 | 禁止事项 | +|------|------|----------| +| `/api` 或 `/internal/handler` | GIN Handler 层,解析请求、调用 service、返回响应 | 禁止写业务逻辑 | +| `/internal/service` | 业务逻辑核心,编排 dao 完成功能 | - | +| `/internal/dao` 或 `/internal/repository` | 数据访问层,封装 GORM 操作 | 禁止引用 service | +| `/internal/model/entity` | 数据库表结构对应的持久化对象 | - | +| `/internal/model/dto` | API 数据传输对象(请求/响应) | - | +| `/pkg/common` | 统一响应、错误码、公共工具 | - | +| `/configs` | 配置文件 | - | +| `/cmd` | main.go 入口 | - | + +## 依赖规则 + +``` +handler → service → dao + ↓ ↓ ↓ + pkg/common (任意层可引用) +``` + +**严禁反向或跨层依赖** + +## go.mod 内部模块引用 + +```go +module my-project + +go 1.24 + +require ( + wdd.io/TonyCommon v1.0.0 +) + +// 本地开发使用 replace +replace wdd.io/TonyCommon => ../TonyCommon +``` diff --git a/1-AgentSkills/coding-go-gin-gorm/reference/time-handling.md b/1-AgentSkills/coding-go-gin-gorm/reference/time-handling.md new file mode 100644 index 0000000..6f6cbb7 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/reference/time-handling.md @@ -0,0 +1,120 @@ +# 时间处理规范 + +## 核心原则 + +所有在前端和后端之间传输、以及在数据库中存储的时间,**必须统一为东八区时间 (Asia/Shanghai, UTC+8)**。 + +## 指定工具库 + +| 端 | 工具库路径 | +|----|-----------| +| 后端 | `rmdc-common/utils/TimeUtils.go` | +| 前端 | `TonyMask/src/utils/timeUtils.ts` | + +## 时间格式 + +- API 响应中的 `timestamp` 字段统一使用 **RFC3339** 格式 +- 示例:`2024-01-15T14:30:00+08:00` + +## 禁止与必须 + +### 禁止直接使用 + +```go +// ❌ 禁止 +time.Now() +time.Parse(layout, value) +t.Format(layout) +``` + +### 必须使用工具库 + +```go +// ✅ 正确 +TimeUtils.Now() +TimeUtils.Parse(layout, value) +TimeUtils.Format(t, layout) +``` + +## 常用场景示例 + +### 获取当前时间 + +```go +// ❌ 错误 +now := time.Now() + +// ✅ 正确 +now := TimeUtils.Now() +``` + +### 格式化时间戳 + +```go +// ❌ 错误 +timestamp := time.Now().Format(time.RFC3339) + +// ✅ 正确 +timestamp := TimeUtils.Now().Format(time.RFC3339) +``` + +### 解析时间字符串 + +```go +// ❌ 错误 +t, err := time.Parse(time.RFC3339, timeStr) + +// ✅ 正确 +t, err := TimeUtils.Parse(time.RFC3339, timeStr) +``` + +### 数据库时间字段 + +```go +type Order struct { + ID int64 `gorm:"primaryKey"` + CreatedAt time.Time `gorm:"autoCreateTime"` // GORM 自动处理 + UpdatedAt time.Time `gorm:"autoUpdateTime"` // GORM 自动处理 + ExpireAt time.Time // 业务时间使用 TimeUtils +} + +// 设置业务时间 +order.ExpireAt = TimeUtils.Now().Add(24 * time.Hour) +``` + +### API 响应时间 + +```go +type Response struct { + Code int `json:"code"` + Status int `json:"status"` + Timestamp string `json:"timestamp"` // RFC3339 格式 + Data interface{} `json:"data"` +} + +// 构建响应 +resp := Response{ + Timestamp: TimeUtils.Now().Format(time.RFC3339), + // ... +} +``` + +## TimeUtils 常用方法 + +| 方法 | 说明 | +|------|------| +| `Now()` | 获取当前东八区时间 | +| `Parse(layout, value)` | 解析时间字符串(东八区) | +| `Format(t, layout)` | 格式化时间 | +| `StartOfDay(t)` | 获取当天零点 | +| `EndOfDay(t)` | 获取当天 23:59:59 | +| `AddDays(t, days)` | 增加天数 | + +## 时区配置 + +确保服务器和数据库时区配置正确: + +```go +// 数据库连接配置 +dsn := "user:pass@tcp(host:3306)/db?charset=utf8mb4&parseTime=True&loc=Asia%2FShanghai" +``` diff --git a/1-AgentSkills/coding-go-gin-gorm/scripts/validate-structure.sh b/1-AgentSkills/coding-go-gin-gorm/scripts/validate-structure.sh new file mode 100644 index 0000000..50aa584 --- /dev/null +++ b/1-AgentSkills/coding-go-gin-gorm/scripts/validate-structure.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# 验证 Go GIN/GORM 项目结构 + +set -e + +echo "=== Go 项目结构验证 ===" + +# 检查 go.mod +if [ ! -f "go.mod" ]; then + echo "❌ 缺少 go.mod" + exit 1 +fi +echo "✅ go.mod 存在" + +# 检查核心目录 +DIRS=("internal/service" "internal/dao" "internal/model" "pkg/common") +for dir in "${DIRS[@]}"; do + if [ -d "$dir" ]; then + echo "✅ $dir 存在" + else + echo "⚠️ $dir 不存在" + fi +done + +# 检查 handler 目录(两种风格) +if [ -d "api" ] || [ -d "internal/handler" ]; then + echo "✅ handler 目录存在" +else + echo "⚠️ 缺少 api/ 或 internal/handler/" +fi + +# 检查反向依赖(dao 不应引用 service) +echo "" +echo "=== 检查依赖方向 ===" +if grep -r "internal/service" internal/dao/ 2>/dev/null; then + echo "❌ dao 层存在对 service 的反向依赖" + exit 1 +fi +echo "✅ 无反向依赖" + +# 检查 time.Now() 使用 +echo "" +echo "=== 检查 time.Now() 使用 ===" +if grep -rn "time\.Now()" --include="*.go" internal/ api/ 2>/dev/null | grep -v "_test.go"; then + echo "⚠️ 发现直接使用 time.Now(),应使用 TimeUtils.Now()" +else + echo "✅ 无直接 time.Now() 调用" +fi + +echo "" +echo "=== 验证完成 ===" diff --git a/1-AgentSkills/coding-vue3-vuetify/SKILL.md b/1-AgentSkills/coding-vue3-vuetify/SKILL.md new file mode 100644 index 0000000..55a62c0 --- /dev/null +++ b/1-AgentSkills/coding-vue3-vuetify/SKILL.md @@ -0,0 +1,141 @@ +--- +name: coding-vue3-vuetify +description: Build production-grade Vue 3 + TypeScript + Vuetify 3 interfaces with architectural rigor. Use when creating Vue components, pages, layouts, Pinia stores, or API modules. Enforces strict typing, Composition API patterns, Material Design 3 aesthetics, and bulletproof data handling. +--- + +This skill crafts Vue 3 + Vuetify 3 code that is architecturally sound, type-safe to the bone, and visually polished. Every component should feel like it belongs in a production codebase that senior engineers would be proud to maintain. + +The user provides: $ARGUMENTS (component specs, page requirements, feature requests, or architectural questions). + +## Architectural Thinking + +Before writing a single line, establish clarity: + +- **Component Identity**: Is this a Page, Layout, Reusable Component, Composable, Store, or API Module? Each has distinct patterns. +- **Data Gravity**: Where does state live? Props flow down, events bubble up. Pinia for cross-component state. `provide/inject` for deep hierarchies. +- **Scroll Strategy**: Which container owns the scroll? Never the body. Always explicit. Always controlled. +- **Failure Modes**: What happens when data is `null`? Empty array? Network timeout? Design for the unhappy path first. + +**CRITICAL**: Production code anticipates chaos. Type everything. Guard everything. Gracefully degrade everything. + +## Core Dogma + +### TypeScript Absolutism +- ` + + diff --git a/1-AgentSkills/coding-vue3-vuetify/examples/page-layout.vue b/1-AgentSkills/coding-vue3-vuetify/examples/page-layout.vue new file mode 100644 index 0000000..1f89ec2 --- /dev/null +++ b/1-AgentSkills/coding-vue3-vuetify/examples/page-layout.vue @@ -0,0 +1,155 @@ + + + diff --git a/1-AgentSkills/coding-vue3-vuetify/reference/api-patterns.md b/1-AgentSkills/coding-vue3-vuetify/reference/api-patterns.md new file mode 100644 index 0000000..5e94551 --- /dev/null +++ b/1-AgentSkills/coding-vue3-vuetify/reference/api-patterns.md @@ -0,0 +1,238 @@ +# API 客户端模式 + +## 标准响应类型 + +```typescript +// @/types/api.d.ts +export interface ApiResponse { + code: number + status: number + timestamp: string + data: T + message?: string + error?: string +} + +export interface PageParams { + page: number + pageSize: number + sort?: string + order?: 'asc' | 'desc' +} + +export interface PageResult { + list: T[] + total: number + page: number + pageSize: number +} + +export enum ApiErrorCode { + Success = 0, + ServerError = 10001, + ParamError = 10002, + Unauthorized = 10003, + Forbidden = 10004, + NotFound = 10005, + Timeout = 10006, + ValidationFail = 10007, + BusinessError = 20001, +} +``` + +## Axios 实例 + +```typescript +// @/api/index.ts +import axios from 'axios' +import { setupInterceptors } from './interceptors' + +const request = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL, + timeout: 15000, + headers: { + 'Content-Type': 'application/json', + }, +}) + +setupInterceptors(request) + +export default request +``` + +## 响应拦截器 + +```typescript +// @/api/interceptors.ts +import type { AxiosInstance, AxiosResponse } from 'axios' +import { useSnackbar } from '@/composables/useSnackbar' +import { useAuthStore } from '@/store/auth' +import router from '@/router' +import { ApiErrorCode, type ApiResponse } from '@/types/api' + +export function setupInterceptors(instance: AxiosInstance) { + // 请求拦截器 + instance.interceptors.request.use( + (config) => { + const authStore = useAuthStore() + if (authStore.token) { + config.headers.Authorization = `Bearer ${authStore.token}` + } + return config + }, + (error) => Promise.reject(error) + ) + + // 响应拦截器 + instance.interceptors.response.use( + (response: AxiosResponse) => { + const { code, data, message } = response.data + + // 业务成功 + if (code === ApiErrorCode.Success) { + return data + } + + // 业务失败 + const snackbar = useSnackbar() + snackbar.error(message || '操作失败') + + return Promise.reject(new Error(message)) + }, + (error) => { + const snackbar = useSnackbar() + const status = error.response?.status + + switch (status) { + case 401: + useAuthStore().logout() + router.push('/login') + snackbar.error('登录已过期,请重新登录') + break + case 403: + snackbar.error('无权访问') + break + case 404: + snackbar.error('请求的资源不存在') + break + case 500: + snackbar.error('服务器错误,请稍后重试') + break + default: + if (error.code === 'ECONNABORTED') { + snackbar.error('请求超时,请检查网络') + } else if (!error.response) { + snackbar.error('网络连接失败') + } + } + + return Promise.reject(error) + } + ) +} +``` + +## API 模块模板 + +```typescript +// @/api/modules/[domain].ts +import request from '@/api' +import type { PageParams, PageResult } from '@/types/api' +import type { Entity, CreateDto, UpdateDto } from '@/types/[domain]' + +export const entityApi = { + // 分页列表 + getPage: (params: PageParams) => + request.get>('/entities', { params }), + + // 详情 + getById: (id: string) => + request.get(`/entities/${id}`), + + // 新增 + create: (data: CreateDto) => + request.post('/entities', data), + + // 更新 + update: (id: string, data: UpdateDto) => + request.put(`/entities/${id}`, data), + + // 删除 + remove: (id: string) => + request.delete(`/entities/${id}`), +} +``` + +## 请求取消处理 + +```typescript +// composables/useCancelableRequest.ts +import { onUnmounted } from 'vue' +import type { AxiosRequestConfig } from 'axios' + +export function useCancelableRequest() { + const controller = new AbortController() + + onUnmounted(() => { + controller.abort() + }) + + function withCancel( + requestFn: (config?: AxiosRequestConfig) => Promise + ): Promise { + return requestFn({ signal: controller.signal }) + } + + return { withCancel, abort: () => controller.abort() } +} +``` + +## 请求重试 + +```typescript +// utils/retryRequest.ts +export async function retryRequest( + fn: () => Promise, + options: { retries?: number; delay?: number } = {} +): Promise { + const { retries = 3, delay = 1000 } = options + + for (let attempt = 0; attempt < retries; attempt++) { + try { + return await fn() + } catch (error) { + if (attempt === retries - 1) throw error + await new Promise((resolve) => setTimeout(resolve, delay * (attempt + 1))) + } + } + + throw new Error('Max retries exceeded') +} +``` + +## 并发请求处理 + +```typescript +// 使用 Promise.all 并发请求 +async function fetchDashboardData() { + const [users, orders, stats] = await Promise.all([ + userApi.getList(), + orderApi.getRecent(), + statsApi.getSummary(), + ]) + + return { users, orders, stats } +} + +// 使用 Promise.allSettled 处理部分失败 +async function fetchWithFallback() { + const results = await Promise.allSettled([ + userApi.getList(), + orderApi.getList(), + ]) + + return results.map((result) => + result.status === 'fulfilled' ? result.value : [] + ) +} +``` diff --git a/1-AgentSkills/coding-vue3-vuetify/reference/layout-patterns.md b/1-AgentSkills/coding-vue3-vuetify/reference/layout-patterns.md new file mode 100644 index 0000000..fae9677 --- /dev/null +++ b/1-AgentSkills/coding-vue3-vuetify/reference/layout-patterns.md @@ -0,0 +1,182 @@ +# 布局模式参考 + +## 标准页面骨架 + +```vue + +``` + +## Flexbox 滚动陷阱解决方案 + +```css +/* 问题:子元素撑破父容器 */ +.parent { + display: flex; + flex-direction: column; + height: 100%; +} + +.content { + flex-grow: 1; + /* 必须添加以下任一属性 */ + min-height: 0; /* 推荐 */ + /* 或 */ + overflow: hidden; +} +``` + +## 粘性筛选栏 + +```vue + + + +``` + +## 分栏布局(侧边栏 + 主内容) + +```vue + +``` + +## 双栏详情布局 + +```vue + +``` + +## Tab 切换布局 + +```vue + +``` + +## 响应式断点处理 + +```vue + + + +``` diff --git a/1-AgentSkills/coding-vue3-vuetify/reference/typescript-rules.md b/1-AgentSkills/coding-vue3-vuetify/reference/typescript-rules.md new file mode 100644 index 0000000..9a970b9 --- /dev/null +++ b/1-AgentSkills/coding-vue3-vuetify/reference/typescript-rules.md @@ -0,0 +1,142 @@ +# TypeScript 严格规范 + +## 类型定义位置 + +``` +src/types/ +├── api.d.ts # ApiResponse, PageParams, etc. +├── user.d.ts # User domain types +├── order.d.ts # Order domain types +└── common.d.ts # Shared utilities +``` + +## 禁止 `any` 的替代方案 + +| 场景 | 错误 | 正确 | +|------|------|------| +| 未知对象 | `any` | `Record` | +| 动态数组 | `any[]` | `unknown[]` + type guard | +| 回调函数 | `(x: any) => any` | 泛型 `(x: T) => T` | +| 第三方库缺失类型 | `any` | 创建 `.d.ts` 声明文件 | + +## Props 与 Emits 类型化 + +```typescript +// Props +interface Props { + user: User + mode?: 'view' | 'edit' + onSave?: (data: User) => void +} + +const props = withDefaults(defineProps(), { + mode: 'view', +}) + +// Emits +interface Emits { + (e: 'update', value: User): void + (e: 'cancel'): void +} + +const emit = defineEmits() +``` + +## 泛型组合式函数 + +```typescript +// composables/useFetch.ts +export function useFetch( + fetcher: () => Promise, + options?: { immediate?: boolean } +) { + const data = ref(null) as Ref + const loading = ref(false) + const error = ref(null) + + async function execute() { + loading.value = true + error.value = null + try { + data.value = await fetcher() + } catch (e) { + error.value = e instanceof Error ? e : new Error(String(e)) + } finally { + loading.value = false + } + } + + if (options?.immediate !== false) { + onMounted(execute) + } + + return { data, loading, error, execute } +} +``` + +## 类型守卫 + +```typescript +function isUser(obj: unknown): obj is User { + return ( + typeof obj === 'object' && + obj !== null && + 'id' in obj && + 'name' in obj + ) +} + +// 使用 +const data: unknown = await fetchData() +if (isUser(data)) { + console.log(data.name) // TypeScript 知道这是 User +} +``` + +## 联合类型与字面量类型 + +```typescript +// 状态枚举替代方案 +type UserStatus = 'active' | 'disabled' | 'pending' + +// 带类型的事件处理 +type TableAction = + | { type: 'edit'; payload: User } + | { type: 'delete'; payload: string } + | { type: 'view'; payload: User } + +function handleAction(action: TableAction) { + switch (action.type) { + case 'edit': + openEditDialog(action.payload) // payload 是 User + break + case 'delete': + confirmDelete(action.payload) // payload 是 string (id) + break + } +} +``` + +## 工具类型使用 + +```typescript +// Partial - 所有属性可选 +type UpdateUserDto = Partial + +// Pick - 选择部分属性 +type UserPreview = Pick + +// Omit - 排除部分属性 +type CreateUserDto = Omit + +// Required - 所有属性必填 +type CompleteUser = Required + +// Record - 键值映射 +type UserMap = Record + +// 自定义工具类型 +type Nullable = T | null +type AsyncReturnType Promise> = + T extends (...args: any) => Promise ? R : never +``` diff --git a/1-AgentSkills/coding-vue3-vuetify/reference/ui-interaction.md b/1-AgentSkills/coding-vue3-vuetify/reference/ui-interaction.md new file mode 100644 index 0000000..2e5119a --- /dev/null +++ b/1-AgentSkills/coding-vue3-vuetify/reference/ui-interaction.md @@ -0,0 +1,345 @@ +# UI 交互规范 + +## 数据表格 + +```vue + +``` + +## 虚拟滚动列表 + +```vue + +``` + +## 骨架屏加载 + +```vue + +``` + +## 空状态组件 + +```vue + + + +``` + +## 多行文本截断 + +```vue + + + +``` + +## 确认对话框 + +```vue + + + +``` + +## 表单验证 + +```vue + + + +``` + +## Snackbar 全局通知 + +```typescript +// composables/useSnackbar.ts +import { ref } from 'vue' + +interface SnackbarOptions { + text: string + color?: string + timeout?: number +} + +const snackbar = ref({ + show: false, + text: '', + color: 'success', + timeout: 3000, +}) + +export function useSnackbar() { + function show(options: SnackbarOptions) { + snackbar.value = { ...snackbar.value, ...options, show: true } + } + + function success(text: string) { + show({ text, color: 'success' }) + } + + function error(text: string) { + show({ text, color: 'error', timeout: 5000 }) + } + + function warning(text: string) { + show({ text, color: 'warning' }) + } + + function info(text: string) { + show({ text, color: 'info' }) + } + + return { snackbar, show, success, error, warning, info } +} +``` + +```vue + + + + +``` diff --git a/1-AgentSkills/developing-project-management/SKILL.md b/1-AgentSkills/developing-project-management/SKILL.md new file mode 100644 index 0000000..1ba8754 --- /dev/null +++ b/1-AgentSkills/developing-project-management/SKILL.md @@ -0,0 +1,184 @@ +--- +name: developing-project-management +description: Guides development of rmdc-project-management module including project lifecycle management, version control (Git-like), ACL permissions, TOTP authorization, and workflow integration. Triggered when modifying project CRUD, draft/version APIs, permission grants, or authorization features. Keywords: project lifecycle, version snapshot, ACL, TOTP, workflow callback, SuperAdmin. +argument-hint: " [target]" where change-type is one of: api|entity|service|migration|frontend|auth. Example: "api draft-submit" or "migration add-field" +allowed-tools: + - Read + - Glob + - Grep + - Bash + - Edit + - Write +--- + +# Developing Project Management Module + +本 Skill 指导 `rmdc-project-management` 模块的开发,该模块是 RMDC 系统的核心业务模块,负责以 K8s Namespace 为粒度的项目全生命周期管理。 + +## 模块定位 + +- **核心职责**: 项目 CRUD、版本控制(Git-like)、细粒度 ACL 权限、一级 TOTP 授权 +- **技术栈**: Go + Gin + GORM + PostgreSQL (JSONB) +- **架构**: 模块化单体,通过接口注入与 `rmdc-work-procedure` 工单模块协作 + +## 动态上下文注入 + +使用前先获取当前仓库状态: + +```bash +# 查看项目管理模块目录结构 +!`find . -path "*/rmdc-project-management/*" -name "*.go" | head -20` + +# 查找生命周期状态相关代码 +!`grep -rn "lifecycle_status\|LifecycleStatus" --include="*.go" | head -15` +``` + +--- + +## Plan(计划阶段) + +### 产物清单 + +根据 `$ARGUMENTS` 确定变更范围: + +| 变更类型 | 产物文件 | 影响模块 | +|:---|:---|:---| +| `api` | `handler/*.go`, `router.go` | rmdc-core 路由注册 | +| `entity` | `entity/*.go` | 数据库迁移、DTO 映射 | +| `service` | `service/*.go` | 业务逻辑、版本快照 | +| `migration` | `migrations/*.sql` | 数据库 Schema | +| `frontend` | `pages/*.vue`, `components/*.vue` | 前端联调 | +| `auth` | `service/auth_*.go` | TOTP 授权、Exchange-Hub 交互 | + +### 决策点 + +1. **是否涉及生命周期状态变更?** + - 若涉及,必须同步更新状态机转换逻辑 + - 检查 `reference/lifecycle-state-machine.md` + +2. **是否修改版本快照结构?** + - 若涉及,需评估历史版本兼容性 + - 更新 `VersionSnapshot` 结构体 + +3. **是否变更 ACL 权限模型?** + - 若涉及,需同步 `rmdc-user-auth` 模块 + - 检查 `reference/acl-permission-model.md` + +4. **是否影响工单模块回调?** + - 若涉及,需更新 `ProjectLifecycleUpdater` 接口实现 + - 检查 `reference/workflow-state-mapping.md` + +--- + +## Verify(验证阶段) + +### Checklist + +- [ ] **生命周期状态机完整性**: 所有状态转换有明确的触发条件和权限控制 +- [ ] **版本快照一致性**: `projects` 表与 `project_versions` 表数据同步 +- [ ] **乐观锁检查**: 并发修改时 `base_version == current_version` 校验存在 +- [ ] **ACL 权限验证**: 接口权限注解与业务逻辑一致 +- [ ] **工单回调幂等**: 状态更新操作具备幂等性 +- [ ] **敏感字段加密**: 密码字段使用 AES-256 加密存储 +- [ ] **审计日志**: 所有写操作记录到 `rmdc-audit-log` +- [ ] **TOTP 授权安全**: 一级密钥仅 SuperAdmin 可访问 + +### 验证命令 + +```bash +# 检查实体字段与数据库 Schema 一致性 +!`grep -rn "gorm:\"" entity/project.go | head -20` + +# 检查 API 路由权限注解 +!`grep -rn "RequireRole\|RequirePermission" handler/*.go` + +# 运行模块单元测试 +go test ./internal/project/... -v -cover +``` + +--- + +## Execute(执行阶段) + +### API 开发流程 + +1. **定义请求/响应结构体** → `dto/project_dto.go` +2. **实现 Service 方法** → `service/project_service.go` +3. **实现 Handler 方法** → `handler/project_handler.go` +4. **注册路由** → `router.go` (注意权限中间件) +5. **编写单元测试** → `*_test.go` + +### 版本快照变更流程 + +1. 更新 `VersionSnapshot` 结构体定义 +2. 确保 `CompareVersions` Diff 算法兼容新字段 +3. 添加字段到 Diff 结果的字段名映射表 +4. 测试历史版本查看功能不受影响 + +### 生命周期状态变更流程 + +1. 更新 `reference/lifecycle-state-machine.md` 状态图 +2. 修改 `service/lifecycle_service.go` 状态转换逻辑 +3. 同步更新 `ProjectLifecycleUpdater` 接口实现 +4. 验证与工单模块的状态映射表一致 + +### 授权功能变更流程 + +1. 检查 `project_auth_configs` 表结构 +2. 更新 `AuthorizationInfo` 结构体 +3. 确保 TOTP 密钥生成/验证逻辑正确 +4. 测试与 Exchange-Hub 的授权指令下发 + +--- + +## Pitfalls(常见问题) + +1. **超管直改未生成版本**: SuperAdmin 直接修改 `projects` 表时,必须同时插入 `project_versions` 记录,否则版本链断裂 + +2. **草稿基准版本过期**: 用户 A 基于 v3 创建草稿,超管修改产生 v4,用户 A 提交时需检测冲突并提示 Rebase + +3. **工单回调重复处理**: 工单模块可能重试回调,`ProjectLifecycleUpdater` 实现必须幂等 + +4. **ACL 权限遗漏授权模块**: `authorization_info` 模块仅 SuperAdmin 可见,其他角色查询时需过滤 + +5. **密码字段明文泄露**: `AdminPassword`、`SSHPwd` 等字段响应时必须脱敏或不返回 + +6. **省市级联校验缺失**: 前端省市级联选择后,后端需校验省市对应关系有效性 + +7. **Namespace 唯一性**: 创建项目时必须校验 `namespace` 全局唯一且符合 RFC 1123 DNS 标签规范 + +8. **JSONB 字段空值处理**: `basic_info`、`deploy_business` 等 JSONB 字段为空时,需返回空对象 `{}` 而非 `null` + +--- + +## 模块依赖关系 + +``` +rmdc-project-management +├── → rmdc-user-auth (用户鉴权、ACL 权限查询) +├── → rmdc-work-procedure (工单创建、状态转换) +├── → rmdc-audit-log (操作审计记录) +├── → rmdc-exchange-hub (授权指令下发) +└── ← rmdc-core (路由注册、依赖注入) +``` + +## 关键接口 + +| 类别 | 路径 | 权限 | +|:---|:---|:---| +| 项目列表 | `POST /api/project/list` | Login | +| 项目详情 | `POST /api/project/detail` | View ACL | +| 创建项目 | `POST /api/project/create` | SuperAdmin | +| 直接更新 | `POST /api/project/update` | SuperAdmin | +| 保存草稿 | `POST /api/project/draft/save` | View ACL | +| 提交审核 | `POST /api/project/draft/submit` | View ACL | +| 版本历史 | `POST /api/project/version/list` | View ACL | +| 权限分配 | `POST /api/project/permission/grant` | SuperAdmin | + +## 相关文档 + +- 生命周期状态机: `reference/lifecycle-state-machine.md` +- API 端点清单: `reference/api-endpoints.md` +- 数据库 Schema: `reference/database-schema.md` +- ACL 权限模型: `reference/acl-permission-model.md` +- 工单状态映射: `reference/workflow-state-mapping.md` diff --git a/1-AgentSkills/developing-project-management/examples/lifecycle-callback.go b/1-AgentSkills/developing-project-management/examples/lifecycle-callback.go new file mode 100644 index 0000000..f346a23 --- /dev/null +++ b/1-AgentSkills/developing-project-management/examples/lifecycle-callback.go @@ -0,0 +1,280 @@ +package service + +import ( + "context" + "fmt" + "time" +) + +// ProjectLifecycleUpdater 项目生命周期状态更新接口 +// 由 rmdc-core 注入,工单模块状态变更时调用 +type ProjectLifecycleUpdater interface { + UpdateLifecycleStatus(projectID, lifecycleStatus string) error + SetLifecycleToDrafting(projectID string) error + SetLifecycleToReviewing(projectID string) error + SetLifecycleToReleased(projectID string) error + SetLifecycleToModifying(projectID string) error +} + +// ProjectLifecycleService 项目生命周期服务实现 +type ProjectLifecycleService struct { + repo ProjectRepository + versionSvc *VersionService + auditSvc AuditService + idempotency IdempotencyChecker +} + +// NewProjectLifecycleService 创建项目生命周期服务 +func NewProjectLifecycleService( + repo ProjectRepository, + versionSvc *VersionService, + auditSvc AuditService, +) *ProjectLifecycleService { + return &ProjectLifecycleService{ + repo: repo, + versionSvc: versionSvc, + auditSvc: auditSvc, + } +} + +// UpdateLifecycleStatus 更新项目生命周期状态 +func (s *ProjectLifecycleService) UpdateLifecycleStatus(projectID, lifecycleStatus string) error { + ctx := context.Background() + + // 幂等性检查 + idempotencyKey := fmt.Sprintf("lifecycle:%s:%s:%d", projectID, lifecycleStatus, time.Now().Unix()/60) + if s.idempotency != nil && s.idempotency.Exists(idempotencyKey) { + return nil // 已处理,直接返回 + } + + // 更新生命周期状态 + err := s.repo.UpdateLifecycleStatus(ctx, projectID, lifecycleStatus) + if err != nil { + return fmt.Errorf("update lifecycle status failed: %w", err) + } + + // 记录审计日志 + s.auditSvc.Log(ctx, AuditLog{ + Resource: "project", + Action: "lifecycle_change", + ResourceID: projectID, + Details: map[string]interface{}{ + "new_status": lifecycleStatus, + }, + }) + + // 标记已处理 + if s.idempotency != nil { + s.idempotency.Set(idempotencyKey, 24*time.Hour) + } + + return nil +} + +// SetLifecycleToReleased 设置为已发布状态(审批通过时) +func (s *ProjectLifecycleService) SetLifecycleToReleased(projectID string) error { + ctx := context.Background() + + // 1. 获取项目信息 + project, err := s.repo.GetByProjectID(ctx, projectID) + if err != nil { + return fmt.Errorf("get project failed: %w", err) + } + + // 2. 获取草稿快照 + draftVersion, err := s.repo.GetActiveDraft(ctx, projectID) + if err != nil { + return fmt.Errorf("get draft failed: %w", err) + } + + // 3. 开启事务 + return s.repo.Transaction(ctx, func(txCtx context.Context) error { + // 3.1 更新生命周期状态 + if err := s.repo.UpdateLifecycleStatus(txCtx, projectID, LifecycleReleased); err != nil { + return err + } + + // 3.2 更新认证状态为正式 + if err := s.repo.UpdateCertificationStatus(txCtx, projectID, CertificationOfficial); err != nil { + return err + } + + // 3.3 将草稿内容合并到主表 + if err := s.repo.MergeDraftToMaster(txCtx, projectID, draftVersion.SnapshotData); err != nil { + return err + } + + // 3.4 创建正式版本快照 + snapshot := &VersionSnapshot{} + if err := parseSnapshot(draftVersion.SnapshotData, snapshot); err != nil { + return err + } + + if err := s.versionSvc.CreateOfficialVersion( + txCtx, + projectID, + snapshot, + draftVersion.CommitterID, + draftVersion.CommitterName, + "审批通过,发布正式版本", + ); err != nil { + return err + } + + // 3.5 删除草稿 + if err := s.repo.DeleteDraft(txCtx, projectID, draftVersion.UserID); err != nil { + return err + } + + return nil + }) +} + +// SetLifecycleToDrafting 设置为填写中状态(工单被打回后) +func (s *ProjectLifecycleService) SetLifecycleToDrafting(projectID string) error { + ctx := context.Background() + + err := s.repo.UpdateLifecycleStatus(ctx, projectID, LifecycleDrafting) + if err != nil { + return fmt.Errorf("set lifecycle to drafting failed: %w", err) + } + + s.auditSvc.Log(ctx, AuditLog{ + Resource: "project", + Action: "lifecycle_change", + ResourceID: projectID, + Details: map[string]interface{}{ + "new_status": LifecycleDrafting, + "reason": "workflow_returned", + }, + }) + + return nil +} + +// SetLifecycleToReviewing 设置为审核中状态(提交审核时) +func (s *ProjectLifecycleService) SetLifecycleToReviewing(projectID string) error { + ctx := context.Background() + + // 更新生命周期状态 + err := s.repo.UpdateLifecycleStatus(ctx, projectID, LifecycleReviewing) + if err != nil { + return fmt.Errorf("set lifecycle to reviewing failed: %w", err) + } + + // 更新认证状态为待审核 + err = s.repo.UpdateCertificationStatus(ctx, projectID, CertificationPending) + if err != nil { + return fmt.Errorf("set certification to pending failed: %w", err) + } + + s.auditSvc.Log(ctx, AuditLog{ + Resource: "project", + Action: "lifecycle_change", + ResourceID: projectID, + Details: map[string]interface{}{ + "new_status": LifecycleReviewing, + }, + }) + + return nil +} + +// SetLifecycleToModifying 设置为变更中状态(发起修改工单时) +func (s *ProjectLifecycleService) SetLifecycleToModifying(projectID string) error { + ctx := context.Background() + + err := s.repo.UpdateLifecycleStatus(ctx, projectID, LifecycleModifying) + if err != nil { + return fmt.Errorf("set lifecycle to modifying failed: %w", err) + } + + s.auditSvc.Log(ctx, AuditLog{ + Resource: "project", + Action: "lifecycle_change", + ResourceID: projectID, + Details: map[string]interface{}{ + "new_status": LifecycleModifying, + }, + }) + + return nil +} + +// CheckVersionConflict 检查版本冲突(乐观锁) +func (s *ProjectLifecycleService) CheckVersionConflict( + ctx context.Context, + projectID string, + baseVersion int, +) error { + currentVersion, err := s.repo.GetCurrentVersion(ctx, projectID) + if err != nil { + return fmt.Errorf("get current version failed: %w", err) + } + + if currentVersion != baseVersion { + return &VersionConflictError{ + ProjectID: projectID, + BaseVersion: baseVersion, + CurrentVersion: currentVersion, + } + } + + return nil +} + +// VersionConflictError 版本冲突错误 +type VersionConflictError struct { + ProjectID string + BaseVersion int + CurrentVersion int +} + +func (e *VersionConflictError) Error() string { + return fmt.Sprintf( + "version conflict: project %s base version %d, current version %d", + e.ProjectID, e.BaseVersion, e.CurrentVersion, + ) +} + +// 辅助类型和函数 + +type ProjectRepository interface { + GetByProjectID(ctx context.Context, projectID string) (*Project, error) + UpdateLifecycleStatus(ctx context.Context, projectID, status string) error + UpdateCertificationStatus(ctx context.Context, projectID, status string) error + GetActiveDraft(ctx context.Context, projectID string) (*ProjectVersion, error) + MergeDraftToMaster(ctx context.Context, projectID string, data []byte) error + DeleteDraft(ctx context.Context, projectID string, userID int64) error + GetCurrentVersion(ctx context.Context, projectID string) (int, error) + Transaction(ctx context.Context, fn func(ctx context.Context) error) error +} + +type AuditService interface { + Log(ctx context.Context, log AuditLog) +} + +type AuditLog struct { + Resource string + Action string + ResourceID string + Details map[string]interface{} +} + +type IdempotencyChecker interface { + Exists(key string) bool + Set(key string, ttl time.Duration) +} + +type Project struct { + ID int64 + ProjectID string + LifecycleStatus string + CertificationStatus string + CurrentVersion int +} + +func parseSnapshot(data []byte, snapshot *VersionSnapshot) error { + // JSON 解析实现 + return nil +} diff --git a/1-AgentSkills/developing-project-management/examples/project-entity.go b/1-AgentSkills/developing-project-management/examples/project-entity.go new file mode 100644 index 0000000..aed33f7 --- /dev/null +++ b/1-AgentSkills/developing-project-management/examples/project-entity.go @@ -0,0 +1,132 @@ +package entity + +import ( + "encoding/json" + "time" +) + +// Project 项目主表实体 +type Project struct { + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` + ProjectID string `gorm:"type:varchar(64);uniqueIndex;not null" json:"project_id"` + Name string `gorm:"type:varchar(128);not null" json:"name"` + Namespace string `gorm:"type:varchar(64);uniqueIndex;not null" json:"namespace"` + + // 生命周期状态: INIT/DRAFTING/REVIEWING/RELEASED/MODIFYING/ARCHIVED + LifecycleStatus string `gorm:"type:varchar(32);default:'INIT'" json:"lifecycle_status"` + // 认证状态: draft/pending/official + CertificationStatus string `gorm:"type:varchar(32);default:'draft'" json:"certification_status"` + + // 当前正式版本号 + CurrentVersion int `gorm:"default:0" json:"current_version"` + + // JSONB 存储 + BasicInfo json.RawMessage `gorm:"type:jsonb" json:"basic_info"` + DeployBusiness json.RawMessage `gorm:"type:jsonb" json:"deploy_business"` + DeployEnv json.RawMessage `gorm:"type:jsonb" json:"deploy_env"` + DeployMiddleware json.RawMessage `gorm:"type:jsonb" json:"deploy_middleware"` + + // 填写人 + DetailFillerID int64 `json:"detail_filler_id"` + DetailFillerName string `gorm:"type:varchar(64)" json:"detail_filler_name"` + + // 审计字段 + CreatedBy int64 `json:"created_by"` + CreatedByName string `gorm:"type:varchar(64)" json:"created_by_name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"` +} + +// TableName 指定表名 +func (Project) TableName() string { + return "projects" +} + +// 生命周期状态常量 +const ( + LifecycleInit = "INIT" + LifecycleDrafting = "DRAFTING" + LifecycleReviewing = "REVIEWING" + LifecycleReleased = "RELEASED" + LifecycleModifying = "MODIFYING" + LifecycleArchived = "ARCHIVED" +) + +// 认证状态常量 +const ( + CertificationDraft = "draft" + CertificationPending = "pending" + CertificationOfficial = "official" +) + +// ProjectVersion 项目版本表 (含草稿) +type ProjectVersion struct { + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` + ProjectID string `gorm:"type:varchar(64);index;not null" json:"project_id"` + + // 版本号 (正式版本递增, 草稿为0) + Version int `gorm:"not null;default:0" json:"version"` + + // 版本类型: official/fill_draft/modify_draft + VersionType string `gorm:"type:varchar(32);not null" json:"version_type"` + + // 基准版本号(草稿基于哪个正式版本创建) + BaseVersion int `gorm:"default:0" json:"base_version"` + + // 草稿所属用户ID (仅草稿类型有值) + UserID int64 `gorm:"index" json:"user_id"` + UserName string `gorm:"type:varchar(64)" json:"user_name"` + + // 关联工单ID (1:1关系, 仅草稿类型有值) + WorkflowID string `gorm:"type:varchar(64);index" json:"workflow_id"` + + // 完整快照数据 + SnapshotData json.RawMessage `gorm:"type:jsonb" json:"snapshot_data"` + + // 变更信息 + CommitMessage string `gorm:"type:varchar(255)" json:"commit_message"` + CommitterID int64 `json:"committer_id"` + CommitterName string `gorm:"type:varchar(64)" json:"committer_name"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// TableName 指定表名 +func (ProjectVersion) TableName() string { + return "project_versions" +} + +// 版本类型常量 +const ( + VersionTypeOfficial = "official" + VersionTypeFillDraft = "fill_draft" + VersionTypeModifyDraft = "modify_draft" +) + +// ProjectAuthConfig 项目授权配置 +type ProjectAuthConfig struct { + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` + ProjectID string `gorm:"type:varchar(64);uniqueIndex;not null" json:"project_id"` + + TierOneSecret string `gorm:"type:varchar(128)" json:"tier_one_secret"` // 一级TOTP密钥 + TimeOffset int `gorm:"default:30" json:"time_offset"` // 允许时间偏移(秒) + TOTPEnabled bool `gorm:"default:false" json:"totp_enabled"` + + TierTwoSecret string `gorm:"type:varchar(128)" json:"tier_two_secret"` // 二级TOTP密钥 + + AuthType string `gorm:"type:varchar(32)" json:"auth_type"` // permanent/time_limited + AuthDays int `json:"auth_days"` // 授权有效期(天) + AuthorizedAt *time.Time `json:"authorized_at"` + RevokedAt *time.Time `json:"revoked_at"` + IsOffline bool `gorm:"default:false" json:"is_offline"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// TableName 指定表名 +func (ProjectAuthConfig) TableName() string { + return "project_auth_configs" +} diff --git a/1-AgentSkills/developing-project-management/examples/version-service.go b/1-AgentSkills/developing-project-management/examples/version-service.go new file mode 100644 index 0000000..a0fd463 --- /dev/null +++ b/1-AgentSkills/developing-project-management/examples/version-service.go @@ -0,0 +1,315 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" +) + +// VersionSnapshot 版本快照结构 +type VersionSnapshot struct { + BasicInfo *BasicInfo `json:"basic_info"` + DeployBusiness *DeployBusiness `json:"deploy_business"` + DeployEnv *DeployEnv `json:"deploy_env"` + DeployMiddleware *DeployMiddleware `json:"deploy_middleware"` +} + +// BasicInfo 基本信息 +type BasicInfo struct { + Province string `json:"province"` + City string `json:"city"` + IndustryContact string `json:"industry_contact"` + IndustryPhone string `json:"industry_phone"` + ProjectNature string `json:"project_nature"` +} + +// DeployBusiness 部署业务信息 +type DeployBusiness struct { + DeployerName string `json:"deployer_name"` + DeployerPhone string `json:"deployer_phone"` + DeployStartTime string `json:"deploy_start_time"` + DeployEndTime string `json:"deploy_end_time"` + SystemVersion string `json:"system_version"` + SystemType string `json:"system_type"` + MainEntrance string `json:"main_entrance"` + AdminUsername string `json:"admin_username"` + AdminPassword string `json:"admin_password"` // 加密存储 +} + +// DeployEnv 部署环境信息 +type DeployEnv struct { + Hosts []HostInfo `json:"hosts"` + NetworkType string `json:"network_type"` + MainPublicIP string `json:"main_public_ip"` + DomainURL string `json:"domain_url"` + SSLEnabled bool `json:"ssl_enabled"` + ManagementType string `json:"management_type"` + ManagementURL string `json:"management_url"` + ManagementUser string `json:"management_user"` + ManagementPwd string `json:"management_pwd"` // 加密存储 + HostCount int `json:"host_count"` + TotalCPU int `json:"total_cpu"` + CPUModel string `json:"cpu_model"` + TotalMemory int `json:"total_memory"` + TotalStorage int `json:"total_storage"` +} + +// HostInfo 主机信息 +type HostInfo struct { + Hostname string `json:"hostname"` + InternalIP string `json:"internal_ip"` + PublicIP string `json:"public_ip"` + CanAccessPublic bool `json:"can_access_public"` + SSHPort int `json:"ssh_port"` + SSHUser string `json:"ssh_user"` + SSHPwd string `json:"ssh_pwd"` // 加密存储 + Role string `json:"role"` // master/worker/storage +} + +// DeployMiddleware 部署中间件信息 +type DeployMiddleware struct { + MySQL MiddlewareInfo `json:"mysql"` + Redis MiddlewareInfo `json:"redis"` + EMQX MiddlewareInfo `json:"emqx"` + MinIO MiddlewareInfo `json:"minio"` + InfluxDB MiddlewareInfo `json:"influxdb"` + Nacos MiddlewareInfo `json:"nacos"` + K8SDashboard MiddlewareInfo `json:"k8s_dashboard"` +} + +// MiddlewareInfo 中间件信息 +type MiddlewareInfo struct { + PublicIP string `json:"public_ip"` + PublicPort int `json:"public_port"` + InternalIP string `json:"internal_ip"` + InternalPort int `json:"internal_port"` + K8SAddress string `json:"k8s_address"` + K8SPort int `json:"k8s_port"` + AdminUser string `json:"admin_user"` + AdminPwd string `json:"admin_pwd"` // 加密存储 + Version string `json:"version"` +} + +// DiffResult 差异结果 +type DiffResult struct { + Module string `json:"module"` + FieldDiffs []FieldDiff `json:"field_diffs"` +} + +// FieldDiff 字段差异 +type FieldDiff struct { + FieldPath string `json:"field_path"` + FieldName string `json:"field_name"` + OldValue interface{} `json:"old_value"` + NewValue interface{} `json:"new_value"` + ChangeType string `json:"change_type"` // add/modify/delete +} + +// VersionService 版本服务 +type VersionService struct { + repo VersionRepository + fieldMap map[string]string // 字段路径到中文名的映射 +} + +// NewVersionService 创建版本服务 +func NewVersionService(repo VersionRepository) *VersionService { + return &VersionService{ + repo: repo, + fieldMap: initFieldNameMap(), + } +} + +// initFieldNameMap 初始化字段名映射 +func initFieldNameMap() map[string]string { + return map[string]string{ + "basic_info.province": "省份", + "basic_info.city": "城市", + "basic_info.industry_contact": "行业组人员", + "basic_info.industry_phone": "行业组电话", + "basic_info.project_nature": "项目性质", + "deploy_business.deployer_name": "部署人姓名", + "deploy_business.system_version": "系统版本", + "deploy_env.host_count": "主机台数", + "deploy_env.main_public_ip": "主公网IP", + "deploy_env.domain_url": "域名URL", + // ... 更多字段映射 + } +} + +// CompareVersions 比较两个版本的差异 +func (s *VersionService) CompareVersions( + ctx context.Context, + baseVersion, targetVersion *VersionSnapshot, +) ([]DiffResult, error) { + var results []DiffResult + + modules := []struct { + Name string + Base interface{} + Target interface{} + }{ + {"基本信息", baseVersion.BasicInfo, targetVersion.BasicInfo}, + {"部署业务", baseVersion.DeployBusiness, targetVersion.DeployBusiness}, + {"部署环境", baseVersion.DeployEnv, targetVersion.DeployEnv}, + {"部署中间件", baseVersion.DeployMiddleware, targetVersion.DeployMiddleware}, + } + + for _, m := range modules { + diffs := s.diffJSON(m.Name, m.Base, m.Target) + if len(diffs) > 0 { + results = append(results, DiffResult{ + Module: m.Name, + FieldDiffs: diffs, + }) + } + } + return results, nil +} + +// diffJSON 对比两个结构体的差异 +func (s *VersionService) diffJSON(moduleName string, base, target interface{}) []FieldDiff { + var diffs []FieldDiff + + baseBytes, _ := json.Marshal(base) + targetBytes, _ := json.Marshal(target) + + var baseMap, targetMap map[string]interface{} + json.Unmarshal(baseBytes, &baseMap) + json.Unmarshal(targetBytes, &targetMap) + + // 遍历目标版本的字段 + for key, newVal := range targetMap { + oldVal, exists := baseMap[key] + fieldPath := fmt.Sprintf("%s.%s", moduleName, key) + + if !exists { + // 新增字段 + diffs = append(diffs, FieldDiff{ + FieldPath: fieldPath, + FieldName: s.getFieldName(fieldPath), + OldValue: nil, + NewValue: newVal, + ChangeType: "add", + }) + } else if !equalValues(oldVal, newVal) { + // 修改字段 + diffs = append(diffs, FieldDiff{ + FieldPath: fieldPath, + FieldName: s.getFieldName(fieldPath), + OldValue: oldVal, + NewValue: newVal, + ChangeType: "modify", + }) + } + } + + // 检查删除的字段 + for key, oldVal := range baseMap { + if _, exists := targetMap[key]; !exists { + fieldPath := fmt.Sprintf("%s.%s", moduleName, key) + diffs = append(diffs, FieldDiff{ + FieldPath: fieldPath, + FieldName: s.getFieldName(fieldPath), + OldValue: oldVal, + NewValue: nil, + ChangeType: "delete", + }) + } + } + + return diffs +} + +// getFieldName 获取字段中文名 +func (s *VersionService) getFieldName(fieldPath string) string { + if name, ok := s.fieldMap[fieldPath]; ok { + return name + } + return fieldPath +} + +// equalValues 比较两个值是否相等 +func equalValues(a, b interface{}) bool { + aBytes, _ := json.Marshal(a) + bBytes, _ := json.Marshal(b) + return string(aBytes) == string(bBytes) +} + +// CreateOfficialVersion 创建正式版本(审批通过时调用) +func (s *VersionService) CreateOfficialVersion( + ctx context.Context, + projectID string, + snapshot *VersionSnapshot, + committerID int64, + committerName string, + commitMessage string, +) error { + // 1. 获取当前版本号 + currentVersion, err := s.repo.GetCurrentVersion(ctx, projectID) + if err != nil { + return fmt.Errorf("get current version failed: %w", err) + } + + // 2. 序列化快照 + snapshotData, err := json.Marshal(snapshot) + if err != nil { + return fmt.Errorf("marshal snapshot failed: %w", err) + } + + // 3. 创建新版本记录 + newVersion := &ProjectVersion{ + ProjectID: projectID, + Version: currentVersion + 1, + VersionType: VersionTypeOfficial, + SnapshotData: snapshotData, + CommitMessage: commitMessage, + CommitterID: committerID, + CommitterName: committerName, + } + + // 4. 事务中插入版本并更新项目当前版本号 + return s.repo.CreateVersionAndUpdateProject(ctx, newVersion) +} + +// GetVersionHistory 获取版本历史 +func (s *VersionService) GetVersionHistory( + ctx context.Context, + projectID string, + page, pageSize int, +) ([]*VersionHistory, int64, error) { + return s.repo.GetVersionHistory(ctx, projectID, page, pageSize) +} + +// VersionHistory 版本历史记录 +type VersionHistory struct { + Version int `json:"version"` + VersionType string `json:"version_type"` + CommitMessage string `json:"commit_message"` + CommitterID int64 `json:"committer_id"` + CommitterName string `json:"committer_name"` + WorkflowID string `json:"workflow_id"` + CreatedAt string `json:"created_at"` + ChangeSummary string `json:"change_summary"` +} + +// VersionRepository 版本仓储接口 +type VersionRepository interface { + GetCurrentVersion(ctx context.Context, projectID string) (int, error) + CreateVersionAndUpdateProject(ctx context.Context, version *ProjectVersion) error + GetVersionHistory(ctx context.Context, projectID string, page, pageSize int) ([]*VersionHistory, int64, error) + GetVersionByNumber(ctx context.Context, projectID string, version int) (*ProjectVersion, error) +} + +// ProjectVersion 项目版本实体(简化版) +type ProjectVersion struct { + ProjectID string + Version int + VersionType string + BaseVersion int + UserID int64 + WorkflowID string + SnapshotData []byte + CommitMessage string + CommitterID int64 + CommitterName string +} diff --git a/1-AgentSkills/developing-project-management/reference/acl-permission-model.md b/1-AgentSkills/developing-project-management/reference/acl-permission-model.md new file mode 100644 index 0000000..6f5640e --- /dev/null +++ b/1-AgentSkills/developing-project-management/reference/acl-permission-model.md @@ -0,0 +1,158 @@ +# ACL 权限模型 + +## 功能权限 (RBAC) + +| 权限代码 | 说明 | 角色 | +|:---|:---|:---| +| `project:create` | 创建项目 | SuperAdmin | +| `project:delete` | 删除/归档项目 | SuperAdmin | +| `project:edit` | 直接编辑项目 | SuperAdmin | +| `project:edit_workflow` | 通过工单编辑项目 | User (有ACL权限) | +| `project:auth_manage` | 一级/二级授权管理 | SuperAdmin | +| `project:permission_manage` | 项目权限分配 | SuperAdmin | + +## 数据权限 (ACL) - 模块级别 + +### 模块定义 + +| 模块代码 | 模块名称 | 说明 | +|:---|:---|:---| +| `basic_info` | 基本信息模块 | 项目名称、命名空间、省份城市等 | +| `business_info` | 部署业务模块 | 部署人、部署时间、系统版本等 | +| `environment_info` | 部署环境模块 | 主机信息、网络环境、域名等 | +| `middleware_info` | 部署中间件模块 | MySQL、Redis、EMQX等配置 | +| `authorization_info` | 项目授权模块 | TOTP授权信息(仅SuperAdmin) | + +### 权限类型 + +| 权限类型 | 说明 | +|:---|:---| +| `view` | 查看权限(可查看项目信息,可发起修改工单) | +| `export` | 导出权限(可导出项目信息) | + +> **说明**:编辑权限通过工单系统实现,拥有 `view` 权限的用户可以发起修改工单,由 SuperAdmin 审批后生效。 + +## 权限规则 + +1. **SuperAdmin**: 拥有所有项目的所有模块的全部权限,可直接修改 +2. **Admin**: 可以访问自己被授权的项目模块,可以向普通用户转授权限 +3. **Normal User**: 只能访问被授权的项目模块,修改需通过工单 +4. **项目填写人**: 自动获得该项目的查看权限 +5. **授权模块**: 仅 SuperAdmin 可见 + +## ACL 表结构(位于 rmdc-user-auth) + +```go +// ProjectACL 项目权限表 (模块级别) +type ProjectACL struct { + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` + ProjectID string `gorm:"type:varchar(64);index;not null" json:"project_id"` + UserID int64 `gorm:"index;not null" json:"user_id"` + + // 模块代码: basic_info/business_info/environment_info/middleware_info/authorization_info + ModuleCode string `gorm:"type:varchar(32);not null" json:"module_code"` + + // 权限类型 + CanView bool `gorm:"default:false" json:"can_view"` + CanExport bool `gorm:"default:false" json:"can_export"` + + // 授权信息 + GrantedBy int64 `json:"granted_by"` + GrantedAt time.Time `json:"granted_at"` + + UpdatedAt time.Time `json:"updated_at"` +} +``` + +## 权限检查流程 + +```go +// CheckProjectModulePermission 检查用户对项目模块的权限 +func (s *ACLService) CheckProjectModulePermission( + ctx context.Context, + userID int64, + projectID string, + moduleCode string, + permType string, // "view" or "export" +) (bool, error) { + // 1. 检查是否为 SuperAdmin + if s.IsSuperAdmin(ctx, userID) { + return true, nil + } + + // 2. 授权模块仅 SuperAdmin 可访问 + if moduleCode == "authorization_info" { + return false, nil + } + + // 3. 查询 ACL 表 + var acl ProjectACL + err := s.db.Where("project_id = ? AND user_id = ? AND module_code = ?", + projectID, userID, moduleCode).First(&acl).Error + if err != nil { + return false, nil + } + + // 4. 检查权限类型 + switch permType { + case "view": + return acl.CanView, nil + case "export": + return acl.CanExport, nil + default: + return false, nil + } +} +``` + +## 权限分配接口 + +### 授予权限 + +```json +// POST /api/project/permission/grant +{ + "project_id": "proj_001", + "user_id": 123, + "modules": [ + { + "module_code": "basic_info", + "can_view": true, + "can_export": false + }, + { + "module_code": "business_info", + "can_view": true, + "can_export": true + } + ] +} +``` + +### 批量设置权限 + +```json +// POST /api/project/permission/batch +{ + "project_id": "proj_001", + "permissions": [ + { + "user_id": 123, + "modules": ["basic_info", "business_info"] + }, + { + "user_id": 456, + "modules": ["basic_info"] + } + ] +} +``` + +## 权限继承规则 + +| 场景 | 规则 | +|:---|:---| +| 项目创建 | 填写人自动获得所有模块的 view 权限 | +| 权限转授 | Admin 只能转授自己拥有的权限 | +| 权限撤销 | 不影响已创建的草稿/工单 | +| 项目归档 | 保留权限记录,但无法访问 | diff --git a/1-AgentSkills/developing-project-management/reference/api-endpoints.md b/1-AgentSkills/developing-project-management/reference/api-endpoints.md new file mode 100644 index 0000000..b4fc02e --- /dev/null +++ b/1-AgentSkills/developing-project-management/reference/api-endpoints.md @@ -0,0 +1,151 @@ +# API 端点清单 + +## 项目管理 + +| 方法 | 路径 | 描述 | 权限 | +|:---|:---|:---|:---| +| POST | `/api/project/list` | 获取项目列表 (自动过滤ACL) | Login | +| POST | `/api/project/detail` | 获取项目详情 (Master版本) | View ACL | +| POST | `/api/project/create` | 创建项目 | SuperAdmin | +| POST | `/api/project/update` | 直接更新项目 | SuperAdmin | +| POST | `/api/project/delete` | 删除项目 (软删除) | SuperAdmin | +| POST | `/api/project/export` | 导出项目信息 | Export ACL | + +## 版本管理 + +| 方法 | 路径 | 描述 | 权限 | +|:---|:---|:---|:---| +| POST | `/api/project/version/list` | 获取版本历史列表 | View ACL | +| POST | `/api/project/version/detail` | 获取指定版本详情 | View ACL | +| POST | `/api/project/version/diff` | 获取版本差异 | View ACL | +| POST | `/api/project/version/diff-with-current` | 对比指定版本与当前版本差异 | View ACL | + +## 草稿管理 + +| 方法 | 路径 | 描述 | 权限 | +|:---|:---|:---|:---| +| POST | `/api/project/draft/get` | 获取当前用户的草稿 | View ACL | +| POST | `/api/project/draft/save` | 保存草稿 | View ACL | +| POST | `/api/project/draft/submit` | 提交审核 | View ACL | +| POST | `/api/project/draft/discard` | 放弃草稿 | View ACL | + +## 权限管理 (SuperAdmin) + +| 方法 | 路径 | 描述 | 权限 | +|:---|:---|:---|:---| +| POST | `/api/project/permission/list` | 获取项目权限列表 | SuperAdmin | +| POST | `/api/project/permission/grant` | 授予权限 | SuperAdmin | +| POST | `/api/project/permission/revoke` | 撤销权限 | SuperAdmin | +| POST | `/api/project/permission/batch` | 批量设置权限 | SuperAdmin | + +## 授权管理 (SuperAdmin) + +| 方法 | 路径 | 描述 | 权限 | +|:---|:---|:---|:---| +| POST | `/api/project/auth/config` | 获取授权配置 | SuperAdmin | +| POST | `/api/project/auth/update` | 更新授权配置 | SuperAdmin | +| POST | `/api/project/auth/grant` | 下发授权 | SuperAdmin | +| POST | `/api/project/auth/revoke` | 撤销授权 | SuperAdmin | + +## 请求/响应示例 + +### 项目列表 + +**Request:** +```json +{ + "page": 1, + "page_size": 20, + "keyword": "", + "lifecycle_status": "", + "province": "" +} +``` + +**Response:** +```json +{ + "code": 0, + "message": "success", + "data": { + "total": 100, + "list": [ + { + "project_id": "proj_001", + "name": "示例项目", + "namespace": "example-ns", + "lifecycle_status": "RELEASED", + "certification_status": "official", + "province": "北京市", + "city": "北京市" + } + ] + } +} +``` + +### 保存草稿 + +**Request:** +```json +{ + "project_id": "proj_001", + "basic_info": { + "province": "北京市", + "city": "北京市", + "industry_contact": "张三", + "industry_phone": "13800138000" + }, + "deploy_business": { + "deployer_name": "李四", + "system_version": "v2.0.0" + } +} +``` + +**Response:** +```json +{ + "code": 0, + "message": "草稿保存成功", + "data": { + "draft_id": 123, + "updated_at": "2026-01-21T10:00:00Z" + } +} +``` + +### 版本对比 + +**Request:** +```json +{ + "project_id": "proj_001", + "base_version": 2, + "target_version": 3 +} +``` + +**Response:** +```json +{ + "code": 0, + "message": "success", + "data": { + "diffs": [ + { + "module": "部署环境", + "field_diffs": [ + { + "field_path": "deploy_env.host_count", + "field_name": "主机台数", + "old_value": 3, + "new_value": 5, + "change_type": "modify" + } + ] + } + ] + } +} +``` diff --git a/1-AgentSkills/developing-project-management/reference/database-schema.md b/1-AgentSkills/developing-project-management/reference/database-schema.md new file mode 100644 index 0000000..7ec2a53 --- /dev/null +++ b/1-AgentSkills/developing-project-management/reference/database-schema.md @@ -0,0 +1,239 @@ +# 数据库 Schema + +## projects 表(项目主表) + +```sql +CREATE TABLE projects ( + id BIGSERIAL PRIMARY KEY, + project_id VARCHAR(64) UNIQUE NOT NULL, + name VARCHAR(128) NOT NULL, + namespace VARCHAR(64) UNIQUE NOT NULL, + + -- 生命周期状态: INIT/DRAFTING/REVIEWING/RELEASED/MODIFYING/ARCHIVED + lifecycle_status VARCHAR(32) DEFAULT 'INIT', + -- 认证状态: draft/pending/official + certification_status VARCHAR(32) DEFAULT 'draft', + + -- 当前正式版本号 + current_version INT DEFAULT 0, + + -- JSONB 存储 + basic_info JSONB, + deploy_business JSONB, + deploy_env JSONB, + deploy_middleware JSONB, + + -- 填写人 + detail_filler_id BIGINT, + detail_filler_name VARCHAR(64), + + -- 审计字段 + created_by BIGINT, + created_by_name VARCHAR(64), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_projects_namespace ON projects(namespace); +CREATE INDEX idx_projects_lifecycle ON projects(lifecycle_status); +CREATE INDEX idx_projects_deleted ON projects(deleted_at); +``` + +## project_versions 表(版本历史) + +```sql +CREATE TABLE project_versions ( + id BIGSERIAL PRIMARY KEY, + project_id VARCHAR(64) NOT NULL, + + -- 版本号 (正式版本递增, 草稿为0) + version INT NOT NULL DEFAULT 0, + -- 版本类型: official/fill_draft/modify_draft + version_type VARCHAR(32) NOT NULL, + + -- 基准版本号(草稿基于哪个正式版本创建) + base_version INT DEFAULT 0, + + -- 草稿所属用户 + user_id BIGINT, + user_name VARCHAR(64), + + -- 关联工单 + workflow_id VARCHAR(64), + + -- 完整快照 + snapshot_data JSONB, + + -- 变更信息 + commit_message VARCHAR(255), + committer_id BIGINT, + committer_name VARCHAR(64), + + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_versions_project ON project_versions(project_id); +CREATE INDEX idx_versions_workflow ON project_versions(workflow_id); +CREATE INDEX idx_versions_user ON project_versions(user_id); +CREATE UNIQUE INDEX idx_versions_project_version ON project_versions(project_id, version) WHERE version > 0; +``` + +## project_workflows 表(项目工单关联) + +```sql +CREATE TABLE project_workflows ( + id BIGSERIAL PRIMARY KEY, + project_id VARCHAR(64) NOT NULL, + workflow_id VARCHAR(64) UNIQUE NOT NULL, + + -- 工单类型: fill(填写)/modify(修改) + workflow_type VARCHAR(32) NOT NULL, + + -- 工单状态 (冗余存储,便于查询) + status VARCHAR(32), + + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_pw_project ON project_workflows(project_id); +CREATE INDEX idx_pw_status ON project_workflows(status); +``` + +## project_auth_configs 表(授权配置) + +```sql +CREATE TABLE project_auth_configs ( + id BIGSERIAL PRIMARY KEY, + project_id VARCHAR(64) UNIQUE NOT NULL, + + tier_one_secret VARCHAR(128), -- 一级TOTP密钥 (加密存储) + time_offset INT DEFAULT 30, -- 允许时间偏移(秒) + totp_enabled BOOLEAN DEFAULT FALSE, + + tier_two_secret VARCHAR(128), -- 二级TOTP密钥 (来自Watchdog) + + auth_type VARCHAR(32), -- permanent/time_limited + auth_days INT, -- 授权有效期(天) + authorized_at TIMESTAMPTZ, + revoked_at TIMESTAMPTZ, + is_offline BOOLEAN DEFAULT FALSE, + + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_auth_project ON project_auth_configs(project_id); +``` + +## JSONB 结构定义 + +### basic_info + +```json +{ + "province": "北京市", + "city": "北京市", + "industry_contact": "张三", + "industry_phone": "13800138000", + "project_nature": "market" +} +``` + +### deploy_business + +```json +{ + "deployer_name": "李四", + "deployer_phone": "13900139000", + "deploy_start_time": "2026-01-01", + "deploy_end_time": "2026-01-15", + "system_version": "v2.0.0", + "system_type": "fly-control", + "main_entrance": "https://example.com", + "admin_username": "admin", + "admin_password": "" +} +``` + +### deploy_env + +```json +{ + "hosts": [ + { + "hostname": "master-01", + "internal_ip": "192.168.1.10", + "public_ip": "", + "can_access_public": false, + "ssh_port": 22, + "ssh_user": "root", + "ssh_pwd": "", + "role": "master" + } + ], + "network_type": "internal", + "main_public_ip": "", + "domain_url": "", + "ssl_enabled": false, + "management_type": "bastion", + "management_url": "", + "management_user": "", + "management_pwd": "", + "host_count": 3, + "total_cpu": 24, + "cpu_model": "Intel Xeon", + "total_memory": 64, + "total_storage": 1000 +} +``` + +### deploy_middleware + +```json +{ + "mysql": { + "public_ip": "", + "public_port": 0, + "internal_ip": "192.168.1.10", + "internal_port": 3306, + "k8s_address": "mysql-svc", + "k8s_port": 3306, + "admin_user": "root", + "admin_pwd": "", + "version": "8.0" + }, + "redis": { ... }, + "emqx": { ... }, + "minio": { ... }, + "influxdb": { ... }, + "nacos": { ... }, + "k8s_dashboard": { ... } +} +``` + +## 敏感字段加密说明 + +以下字段必须使用 AES-256 加密存储: + +| 表 | 字段路径 | 说明 | +|:---|:---|:---| +| projects | `deploy_business.admin_password` | 系统超管密码 | +| projects | `deploy_env.hosts[*].ssh_pwd` | SSH密码 | +| projects | `deploy_env.management_pwd` | 管理后台密码 | +| projects | `deploy_middleware.*.admin_pwd` | 中间件超管密码 | +| project_auth_configs | `tier_one_secret` | 一级TOTP密钥 | +| project_auth_configs | `tier_two_secret` | 二级TOTP密钥 | + +## 索引优化建议 + +```sql +-- 常用查询优化 +CREATE INDEX idx_projects_lifecycle_cert ON projects(lifecycle_status, certification_status); +CREATE INDEX idx_projects_province ON projects((basic_info->>'province')); + +-- JSONB GIN 索引(按需添加) +CREATE INDEX idx_projects_basic_gin ON projects USING GIN (basic_info); +``` diff --git a/1-AgentSkills/developing-project-management/reference/lifecycle-state-machine.md b/1-AgentSkills/developing-project-management/reference/lifecycle-state-machine.md new file mode 100644 index 0000000..fbf3ff8 --- /dev/null +++ b/1-AgentSkills/developing-project-management/reference/lifecycle-state-machine.md @@ -0,0 +1,90 @@ +# 项目生命周期状态机 + +## 状态定义 + +| 状态 | 说明 | 触发动作 | 权限 | +|:---|:---|:---|:---| +| **INIT** | 项目元数据已创建,等待详细信息录入 | 超级管理员创建项目 | SuperAdmin | +| **DRAFTING** | 正在进行初始信息填写(关联填写工单) | 指定填写人保存/编辑 | 填写人/SuperAdmin | +| **REVIEWING** | 初始信息或变更信息提交审核 | 提交审核 | SuperAdmin | +| **RELEASED** | 审核通过,正常运行中 | 审核通过 | All (View) | +| **MODIFYING** | 存在活跃的变更工单(不影响主线运行) | 发起修改工单 | Owner/SuperAdmin | +| **ARCHIVED** | 软删除状态,不可见但保留数据 | 删除项目 | SuperAdmin | + +## 状态转换图 + +``` +[*] ──创建项目──> INIT + │ + ├──分配填写人──> DRAFTING ──保存草稿──> DRAFTING + │ │ + │ └──提交审核──> REVIEWING + │ │ + │ ┌──审核打回──<────┤ + │ │ │ + │ v └──审核通过──> RELEASED + │ DRAFTING │ + │ │ + │ ┌──发起修改工单──<───────────────┤ + │ │ │ + │ v └──归档删除──> ARCHIVED ──> [*] + │ MODIFYING ──保存草稿──> MODIFYING + │ │ + │ ├──提交变更审核──> REVIEWING + │ │ + │ └──撤销变更──> RELEASED +``` + +## 状态转换条件 + +| From | To | 事件 | 条件 | +|:---|:---|:---|:---| +| INIT | DRAFTING | 分配填写人 | 填写人 ID 有效 | +| DRAFTING | DRAFTING | 保存草稿 | 表单数据有效 | +| DRAFTING | REVIEWING | 提交审核 | 必填字段完整 | +| REVIEWING | DRAFTING | 审核打回 | SuperAdmin 操作 | +| REVIEWING | RELEASED | 审核通过 | SuperAdmin 操作 | +| RELEASED | MODIFYING | 发起修改工单 | 有 View ACL 权限 | +| RELEASED | ARCHIVED | 归档删除 | SuperAdmin 操作 | +| MODIFYING | MODIFYING | 保存草稿 | 表单数据有效 | +| MODIFYING | REVIEWING | 提交变更审核 | 必填字段完整 | +| MODIFYING | RELEASED | 撤销变更/审核通过 | 用户操作/SuperAdmin | + +## Mermaid 状态图 + +```mermaid +stateDiagram-v2 + [*] --> INIT: 创建项目 + + INIT --> DRAFTING: 分配填写人 + + DRAFTING --> DRAFTING: 保存草稿 + DRAFTING --> REVIEWING: 提交审核 + + REVIEWING --> DRAFTING: 审核打回 + REVIEWING --> RELEASED: 审核通过 + + RELEASED --> MODIFYING: 发起修改工单 + RELEASED --> ARCHIVED: 归档删除 + + MODIFYING --> MODIFYING: 保存草稿 + MODIFYING --> REVIEWING: 提交变更审核 + MODIFYING --> RELEASED: 撤销变更/审核通过 + + ARCHIVED --> [*] + + note right of RELEASED: 项目认证状态=official + note right of DRAFTING: 支持多次保存草稿 + note right of MODIFYING: 可同时存在多个变更工单 +``` + +## 状态与认证状态映射 + +| 生命周期状态 | 认证状态 (certification_status) | +|:---|:---| +| INIT | draft | +| DRAFTING | draft | +| REVIEWING | pending | +| RELEASED | official | +| MODIFYING | official (主线不变) | +| ARCHIVED | official (保留) | diff --git a/1-AgentSkills/developing-project-management/reference/workflow-state-mapping.md b/1-AgentSkills/developing-project-management/reference/workflow-state-mapping.md new file mode 100644 index 0000000..2cd808c --- /dev/null +++ b/1-AgentSkills/developing-project-management/reference/workflow-state-mapping.md @@ -0,0 +1,146 @@ +# 工单状态映射表 + +## 设计原则 + +项目模块(`rmdc-project-management`)与工单模块(`rmdc-work-procedure`)之间需要双向协作: +1. **项目 → 工单**:项目模块调用工单模块创建/转换工单 +2. **工单 → 项目**:工单状态变更后同步更新项目生命周期状态 + +当前系统采用 **"模块化单体"架构**,使用 **接口注入(依赖注入)** 方式实现模块间回调。 + +## 填写工单 (project_detail) + +| 工单事件 | 工单From状态 | 工单To状态 | 项目生命周期状态 | 说明 | +|:---|:---|:---|:---|:---| +| `create` | - | `created` | `INIT→DRAFTING` | 创建填写工单 | +| `accept` | `assigned` | `in_progress` | `DRAFTING` (保持) | 填写人开始填写 | +| `draft_save` | `in_progress` | `in_progress` | `DRAFTING` (保持) | 保存草稿 | +| `submit/complete` | `in_progress` | `pending_review` | `REVIEWING` | 提交审核 | +| `return` | `pending_review` | `returned` | `DRAFTING` | 审核人打回 | +| `resubmit` | `returned` | `pending_review` | `REVIEWING` | 重新提交 | +| `approve` | `pending_review` | `approved` | `RELEASED` | 审核通过 | +| `revoke` | any | `revoked` | `INIT` | 撤销工单 | + +## 修改工单 (project_modify) + +| 工单事件 | 工单From状态 | 工单To状态 | 项目生命周期状态 | 说明 | +|:---|:---|:---|:---|:---| +| `create` | - | `created` | `RELEASED→MODIFYING` | 发起修改工单 | +| `accept` | `assigned` | `in_progress` | `MODIFYING` (保持) | 开始修改 | +| `draft_save` | `in_progress` | `in_progress` | `MODIFYING` (保持) | 保存草稿 | +| `submit/complete` | `in_progress` | `pending_review` | `REVIEWING` | 提交审核 | +| `return` | `pending_review` | `returned` | `MODIFYING` | 审核人打回 | +| `resubmit` | `returned` | `pending_review` | `REVIEWING` | 重新提交 | +| `approve` | `pending_review` | `approved` | `RELEASED` | 审核通过 | +| `revoke` | any | `revoked` | `RELEASED` | 撤销工单 | + +## 回调接口定义 + +### 项目模块提供的回调接口 + +```go +// ProjectLifecycleUpdater 项目生命周期状态更新接口 +// 由 rmdc-core 在初始化时注入,工单模块状态变更时调用 +type ProjectLifecycleUpdater interface { + // UpdateLifecycleStatus 更新项目生命周期状态 + UpdateLifecycleStatus(projectID, lifecycleStatus string) error + + // SetLifecycleToDrafting 设置为填写中状态(工单被打回后) + SetLifecycleToDrafting(projectID string) error + + // SetLifecycleToReviewing 设置为审核中状态(提交审核时) + SetLifecycleToReviewing(projectID string) error + + // SetLifecycleToReleased 设置为已发布状态(审批通过时) + SetLifecycleToReleased(projectID string) error + + // SetLifecycleToModifying 设置为变更中状态(发起修改工单时) + SetLifecycleToModifying(projectID string) error +} +``` + +### 项目模块调用工单模块的接口 + +```go +// WorkflowTransitioner 工单状态转换接口 +// 由 rmdc-core 在初始化时注入,项目模块通过此接口调用工单模块 +type WorkflowTransitioner interface { + // TransitionWorkflow 触发工单状态转换 + TransitionWorkflow(workflowID, event string, operatorID uint64, + operatorName string, remark string) (newStatus string, err error) +} + +// WorkflowCreator 工单创建接口 +type WorkflowCreator interface { + // CreateProjectWorkflow 创建项目相关工单 + CreateProjectWorkflow(req CreateWorkflowRequest) (workflowID string, err error) +} +``` + +## 依赖注入流程 + +在 `rmdc-core/cmd/main.go` 中完成模块间的依赖注入: + +```go +// 1. 初始化项目和工单服务 +projectSvc := projectHandler.RegisterRoutes(r, dbs.Project, authMiddleware) +workflowSvc := workflowHandler.RegisterRoutes(r, dbs.Workflow, authMiddleware) + +// 2. 注入工单→项目的回调(状态同步) +projectCallbackSvc := initProjectWorkflowCallbacks(dbs.Project) +workflowSvc.SetProjectLifecycleUpdater(projectCallbackSvc) + +// 3. 注入项目→工单的调用(创建/转换工单) +workflowCreator := initProjectWorkflowCreator(workflowSvc) +projectSvc.SetWorkflowCreator(workflowCreator) + +workflowTransitioner := initProjectWorkflowTransitioner(workflowSvc) +projectSvc.SetWorkflowTransitioner(workflowTransitioner) +``` + +## 回调处理实现 + +工单模块状态转换后,自动调用已注入的 `ProjectLifecycleUpdater` 接口更新项目状态: + +```go +// 工单模块 - 状态转换后触发项目状态更新 +func (s *WorkflowService) handleProjectLifecycleCallback(workflow *entity.Workflow, event string) { + // 从业务载荷中获取项目ID + projectID, ok := workflow.BusinessPayload["project_id"].(string) + if !ok || projectID == "" { + return + } + + // 根据事件类型更新项目生命周期状态 + if s.projectLifecycleUpdater != nil { + switch event { + case entity.EventApprove: + s.projectLifecycleUpdater.SetLifecycleToReleased(projectID) + case entity.EventReturn: + s.projectLifecycleUpdater.SetLifecycleToDrafting(projectID) + case entity.EventComplete, entity.EventResubmit: + s.projectLifecycleUpdater.SetLifecycleToReviewing(projectID) + } + } +} +``` + +## 回调处理要点 + +1. **幂等性**: 使用 `projectID + event + timestamp` 作为幂等键,防止重复处理 +2. **事务边界**: 状态更新与版本快照生成应在同一事务 +3. **审计日志**: 记录状态变更操作到 `rmdc-audit-log` +4. **错误处理**: 回调失败不应阻塞工单状态转换,记录日志后继续 + +## 与 HTTP 回调的对比 + +| 特性 | 接口注入(当前实现) | HTTP 回调 | +|:---|:---|:---| +| **模块解耦** | ⚠️ 接口级解耦 | ✅ 完全解耦 | +| **分布式支持** | ❌ 不支持 | ✅ 支持 | +| **性能** | ✅ 进程内调用 | ⚠️ 网络开销 | +| **复杂度** | ✅ 简单直接 | ⚠️ 需要重试/幂等处理 | +| **事务一致性** | ✅ 强一致性 | ⚠️ 最终一致性 | +| **适用场景** | 模块化单体架构 | 微服务架构 | + +> 如果未来系统需要微服务化,可参考备选文档迁移到 HTTP 回调方案。 diff --git a/1-AgentSkills/developing-project-management/scripts/verify-project-module.sh b/1-AgentSkills/developing-project-management/scripts/verify-project-module.sh new file mode 100644 index 0000000..2195667 --- /dev/null +++ b/1-AgentSkills/developing-project-management/scripts/verify-project-module.sh @@ -0,0 +1,231 @@ +#!/bin/bash +# verify-project-module.sh +# 验证 rmdc-project-management 模块的完整性 +# 依赖: go, grep, find +# 用法: ./verify-project-module.sh [project-root-path] + +set -e + +PROJECT_ROOT="${1:-.}" + +echo "==========================================" +echo "RMDC Project Management Module Verification" +echo "==========================================" +echo "Project Root: $PROJECT_ROOT" +echo "" + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +PASS_COUNT=0 +FAIL_COUNT=0 +WARN_COUNT=0 + +check_pass() { + echo -e "${GREEN}[PASS]${NC} $1" + ((PASS_COUNT++)) +} + +check_fail() { + echo -e "${RED}[FAIL]${NC} $1" + ((FAIL_COUNT++)) +} + +check_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" + ((WARN_COUNT++)) +} + +# 1. 检查实体定义 +echo "1. Checking Entity Definitions..." +if grep -rq "LifecycleStatus" "$PROJECT_ROOT"/internal/project/entity/*.go 2>/dev/null; then + check_pass "LifecycleStatus field exists in Project entity" +else + check_fail "LifecycleStatus field missing in Project entity" +fi + +if grep -rq "CurrentVersion" "$PROJECT_ROOT"/internal/project/entity/*.go 2>/dev/null; then + check_pass "CurrentVersion field exists for version tracking" +else + check_fail "CurrentVersion field missing for version tracking" +fi + +if grep -rq "CertificationStatus" "$PROJECT_ROOT"/internal/project/entity/*.go 2>/dev/null; then + check_pass "CertificationStatus field exists" +else + check_fail "CertificationStatus field missing" +fi + +# 2. 检查生命周期状态常量 +echo "" +echo "2. Checking Lifecycle Status Constants..." +REQUIRED_STATES=("INIT" "DRAFTING" "REVIEWING" "RELEASED" "MODIFYING" "ARCHIVED") +for state in "${REQUIRED_STATES[@]}"; do + if grep -rq "\"$state\"" "$PROJECT_ROOT"/internal/project/ 2>/dev/null; then + check_pass "Lifecycle state '$state' defined" + else + check_fail "Lifecycle state '$state' not found" + fi +done + +# 3. 检查 API 路由 +echo "" +echo "3. Checking API Routes..." +REQUIRED_ROUTES=( + "/api/project/list" + "/api/project/detail" + "/api/project/create" + "/api/project/update" + "/api/project/draft/save" + "/api/project/draft/submit" + "/api/project/version/list" + "/api/project/permission/grant" +) +for route in "${REQUIRED_ROUTES[@]}"; do + if grep -rq "$route" "$PROJECT_ROOT"/internal/project/ 2>/dev/null; then + check_pass "Route '$route' registered" + else + check_fail "Route '$route' not found" + fi +done + +# 4. 检查权限中间件 +echo "" +echo "4. Checking Permission Middleware..." +if grep -rq "SuperAdmin\|RequireRole\|RequirePermission" "$PROJECT_ROOT"/internal/project/handler/*.go 2>/dev/null; then + check_pass "Permission middleware applied" +else + check_warn "Permission middleware not detected - verify manually" +fi + +# 5. 检查版本服务 +echo "" +echo "5. Checking Version Service..." +if grep -rq "CompareVersions" "$PROJECT_ROOT"/internal/project/service/*.go 2>/dev/null; then + check_pass "Version comparison service implemented" +else + check_fail "Version comparison service not found" +fi + +if grep -rq "CreateOfficialVersion" "$PROJECT_ROOT"/internal/project/service/*.go 2>/dev/null; then + check_pass "CreateOfficialVersion method exists" +else + check_fail "CreateOfficialVersion method not found" +fi + +# 6. 检查回调接口 +echo "" +echo "6. Checking Lifecycle Callback Interface..." +if grep -rq "ProjectLifecycleUpdater" "$PROJECT_ROOT"/internal/project/*.go 2>/dev/null; then + check_pass "ProjectLifecycleUpdater interface defined" +else + check_fail "ProjectLifecycleUpdater interface not found" +fi + +CALLBACK_METHODS=("SetLifecycleToDrafting" "SetLifecycleToReviewing" "SetLifecycleToReleased" "SetLifecycleToModifying") +for method in "${CALLBACK_METHODS[@]}"; do + if grep -rq "$method" "$PROJECT_ROOT"/internal/project/ 2>/dev/null; then + check_pass "Callback method '$method' implemented" + else + check_fail "Callback method '$method' not found" + fi +done + +# 7. 检查敏感字段加密 +echo "" +echo "7. Checking Sensitive Field Encryption..." +if grep -rq "Encrypt\|Decrypt\|AES\|crypto" "$PROJECT_ROOT"/internal/project/service/*.go 2>/dev/null; then + check_pass "Encryption functions detected" +else + check_warn "Encryption functions not detected - verify password field handling" +fi + +# 8. 检查审计日志集成 +echo "" +echo "8. Checking Audit Log Integration..." +if grep -rq "audit\|AuditService\|auditSvc" "$PROJECT_ROOT"/internal/project/service/*.go 2>/dev/null; then + check_pass "Audit log integration detected" +else + check_warn "Audit log integration not detected - verify manually" +fi + +# 9. 检查乐观锁实现 +echo "" +echo "9. Checking Optimistic Lock..." +if grep -rq "base_version\|BaseVersion\|VersionConflict" "$PROJECT_ROOT"/internal/project/ 2>/dev/null; then + check_pass "Optimistic lock (version conflict check) implemented" +else + check_warn "Optimistic lock not detected - concurrent modification may cause issues" +fi + +# 10. 运行单元测试(如果可用) +echo "" +echo "10. Running Unit Tests..." +if [ -d "$PROJECT_ROOT/internal/project" ]; then + cd "$PROJECT_ROOT" + if go test ./internal/project/... -v -short -count=1 2>&1 | head -20; then + check_pass "Unit tests executed" + else + check_warn "Unit tests may have issues - check output above" + fi +else + check_warn "Project directory not found, skipping tests" +fi + +# 11. 检查编译 +echo "" +echo "11. Checking Build..." +if [ -d "$PROJECT_ROOT/internal/project" ]; then + cd "$PROJECT_ROOT" + if go build ./internal/project/... 2>/dev/null; then + check_pass "Module compiles successfully" + else + check_fail "Module compilation failed" + fi +else + check_warn "Project directory not found, skipping build check" +fi + +# 12. 检查 JSONB 字段定义 +echo "" +echo "12. Checking JSONB Field Definitions..." +JSONB_FIELDS=("basic_info" "deploy_business" "deploy_env" "deploy_middleware") +for field in "${JSONB_FIELDS[@]}"; do + if grep -rq "\"$field\"" "$PROJECT_ROOT"/internal/project/entity/*.go 2>/dev/null; then + check_pass "JSONB field '$field' defined" + else + check_fail "JSONB field '$field' not found" + fi +done + +# 总结 +echo "" +echo "==========================================" +echo "Verification Summary" +echo "==========================================" +echo -e "Passed: ${GREEN}$PASS_COUNT${NC}" +echo -e "Failed: ${RED}$FAIL_COUNT${NC}" +echo -e "Warnings: ${YELLOW}$WARN_COUNT${NC}" + +if [ $FAIL_COUNT -gt 0 ]; then + echo "" + echo -e "${RED}Some checks failed. Please review and fix the issues.${NC}" + echo "" + echo "Common fixes:" + echo " - Missing lifecycle states: Add constants in entity/constants.go" + echo " - Missing routes: Register in handler/router.go" + echo " - Missing callback interface: Implement ProjectLifecycleUpdater" + echo " - Missing version service: Implement version comparison logic" + exit 1 +elif [ $WARN_COUNT -gt 0 ]; then + echo "" + echo -e "${YELLOW}Some warnings detected. Please verify manually.${NC}" + exit 0 +else + echo "" + echo -e "${GREEN}All checks passed!${NC}" + exit 0 +fi diff --git a/1-AgentSkills/developing-rmdc-system/SKILL.md b/1-AgentSkills/developing-rmdc-system/SKILL.md new file mode 100644 index 0000000..f4c3f0e --- /dev/null +++ b/1-AgentSkills/developing-rmdc-system/SKILL.md @@ -0,0 +1,229 @@ +--- +name: developing-rmdc-system +description: Guides development and architecture decisions for the RMDC (Runtime Management & DevOps Center) platform. Use when creating new modules, understanding module dependencies, implementing cross-module features, or reviewing system-level changes. Keywords: RMDC, architecture, module, dependency, API gateway, MQTT, watchdog, exchange-hub, authorization. +argument-hint: " | | " +allowed-tools: + - Read + - Glob + - Grep + - Bash +--- + +# RMDC System Development Guide + +## System Overview + +RMDC (Runtime Management & DevOps Center) 是以项目(K8s Namespace)为核心维度的统一运维与交付平台。通过"边缘代理(Watchdog) + 消息总线(Exchange-Hub)"架构打通内外网边界。 + +### Architecture Layers + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Presentation │ Vue3 + Vuetify3 + TypeScript │ +├─────────────────────────────────────────────────────────────┤ +│ Gateway │ rmdc-core (API Gateway + Auth + Routing) │ +├─────────────────────────────────────────────────────────────┤ +│ Business │ jenkins-dac | project-mgmt | user-auth │ +│ │ audit-log | notice-center | monitor │ +├─────────────────────────────────────────────────────────────┤ +│ Communication │ rmdc-exchange-hub (MQTT Gateway) │ +├─────────────────────────────────────────────────────────────┤ +│ Message Broker │ MQTT Broker (EMQX/Mosquitto) │ +├─────────────────────────────────────────────────────────────┤ +│ Edge │ rmdc-watchdog → watchdog-node/agent │ +├─────────────────────────────────────────────────────────────┤ +│ Data │ PostgreSQL 13+ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Module Registry + +| Module | Responsibility | Tech Stack | Depends On | +|--------|---------------|------------|------------| +| **rmdc-core** | API Gateway, Auth, Routing | Go + Gin | rmdc-common | +| **rmdc-jenkins-branch-dac** | Jenkins DAC, Build Mgmt | Jenkins API, MinIO | rmdc-common, rmdc-audit-log | +| **rmdc-exchange-hub** | MQTT Gateway, Command Lifecycle | MQTT, PostgreSQL | rmdc-common, rmdc-project-mgmt | +| **rmdc-watchdog** | Edge Proxy, K8S Ops, L2 Auth | K8S API, TOTP | rmdc-common | +| **rmdc-project-management** | Project CRUD, L1 Auth Center | PostgreSQL | rmdc-common, rmdc-audit-log | +| **rmdc-audit-log** | Audit Logging | PostgreSQL | rmdc-common | +| **rmdc-user-auth** | User Auth, RBAC | JWT, PostgreSQL | rmdc-common | + +> 详细依赖矩阵见 `reference/module-dependencies.md` + +--- + +## Plan Phase + +当开始 RMDC 相关开发任务时,首先执行以下检查: + +### 1. Identify Affected Modules + +```bash +# 动态注入:查看当前模块结构 +!`ls -la 8-CMII-RMDC/` + +# 动态注入:搜索涉及的模块设计文档 +!`grep -rnE "module|模块|service|接口" 8-CMII-RMDC/1-rmdc-system/ | head -30` +``` + +### 2. Produce Checklist + +- [ ] 确定变更涉及的模块列表 +- [ ] 确认是否涉及跨模块通信(MQTT/HTTP) +- [ ] 确认是否涉及契约变更(API/Event/Schema) +- [ ] 确认是否涉及授权层级变更(L1/L2) +- [ ] 确认是否需要数据库迁移 + +### 3. Decision Points + +| Decision | Options | Impact | +|----------|---------|--------| +| New module vs extend existing | 新增模块需注册到rmdc-core | 路由、鉴权、审计 | +| Sync vs Async communication | HTTP同步 / MQTT异步 | 延迟、可靠性 | +| L1 vs L2 authorization | project-mgmt(L1) / watchdog(L2) | 安全边界 | + +--- + +## Verify Phase + +### Cross-Module Compatibility Checklist + +- [ ] **API Gateway**: rmdc-core 路由配置已更新 +- [ ] **Authentication**: JWT claims 字段兼容 +- [ ] **RBAC**: 权限点已在 rmdc-user-auth 注册 +- [ ] **Audit**: 审计日志已按模块分表配置 +- [ ] **MQTT Topics**: 新增 topic 已在 exchange-hub 注册 +- [ ] **Authorization**: L1/L2 授权流程已验证 + +### Dependency Verification + +```bash +# 动态注入:检查模块间 import 关系 +!`grep -rn "import.*rmdc-" --include="*.go" . | grep -v vendor | head -20` + +# 动态注入:验证 go.mod 依赖 +!`cat go.mod | grep -E "rmdc-|wdd.io"` +``` + +### Integration Points + +| From | To | Protocol | Verify | +|------|----|----------|--------| +| rmdc-core | Business modules | HTTP/Internal | 路由注册 | +| Business modules | exchange-hub | HTTP | 指令下发 | +| exchange-hub | MQTT Broker | MQTT Pub/Sub | Topic 配置 | +| MQTT Broker | watchdog | MQTT | 公网连通性 | +| watchdog | watchdog-node/agent | HTTP/gRPC | 内网通信 | + +--- + +## Execute Phase + +### Adding New Business Module + +1. Create module directory following structure: + ``` + rmdc-{module-name}/ + ├── cmd/main.go + ├── configs/ + ├── internal/ + │ ├── config/ + │ ├── dao/ + │ ├── handler/ + │ ├── model/{dto,entity}/ + │ └── service/ + └── pkg/ + ``` + +2. Register routes in `rmdc-core`: + ```go + // rmdc-core/internal/router/router.go + moduleGroup := r.Group("/api/{module}") + moduleGroup.Use(middleware.AuthMiddleware()) + ``` + +3. Configure audit logging: + ```go + // Add module to determineModule() in audit_service.go + case strings.Contains(path, "/{module}/"): + return "{module}" + ``` + +4. Update RBAC permissions in `rmdc-user-auth` + +### Cross-Module Communication + +**HTTP (Sync)**: 模块间直接调用 +```go +resp, err := http.Post("http://rmdc-exchange-hub:8080/api/commands/send", ...) +``` + +**MQTT (Async)**: 通过 exchange-hub 下发 +```go +exhub.SendCommand(ctx, &Command{ + ProjectID: projectID, + CommandType: "k8s_exec", + Payload: payload, +}) +``` + +--- + +## Pitfalls + +1. **循环依赖**: 业务模块间禁止直接 import,必须通过 rmdc-common 定义接口 +2. **JWT Claims 不一致**: 修改 JWT 结构需同步更新所有解析方验证逻辑 +3. **MQTT Topic 命名冲突**: 新增 topic 前必须检查 `reference/mqtt-topics.md` +4. **L1/L2 授权边界模糊**: 平台侧操作走 L1(project-mgmt),边缘侧操作走 L2(watchdog) +5. **审计日志遗漏**: 新模块必须配置独立审计表并注册到 DAOManager +6. **数据库连接池耗尽**: 每个模块独立配置连接池,注意总数不超过 PostgreSQL max_connections +7. **MQTT QoS 选择错误**: 指令类消息必须使用 QoS=1,状态类可用 QoS=0 + +--- + +## Related Skills + +- `developing-rmdc-core` - API Gateway 开发 +- `developing-rmdc-jenkins-dac` - Jenkins DAC 模块开发 +- `developing-rmdc-exchange-hub` - MQTT 网关开发 +- `developing-rmdc-watchdog` - 边缘代理开发 +- `developing-rmdc-project-mgmt` - 项目管理模块开发 +- `developing-rmdc-audit-log` - 审计日志模块开发 +- `developing-rmdc-user-auth` - 用户认证模块开发 +- `designing-rmdc-contracts` - API/事件契约设计 +- `managing-rmdc-migrations` - 数据库迁移管理 +- `implementing-rmdc-observability` - 可观测性实现 + +--- + +## Quick Reference + +### Tech Stack + +| Layer | Technology | +|-------|------------| +| Frontend | Vue3, TypeScript, Vuetify3 | +| Backend | Go 1.21+, Gin, GORM | +| Database | PostgreSQL 13+ | +| Message | MQTT (EMQX/Mosquitto) | +| Storage | MinIO | +| Container | Docker, Kubernetes | + +### API Response Format + +```json +{ + "code": 0, + "message": "success", + "data": {...} +} +``` + +### Authorization Layers + +| Layer | Scope | Validity | Algorithm | +|-------|-------|----------|-----------| +| L1 (一级) | project-mgmt ↔ watchdog | 30 min | SHA256, 8-digit | +| L2 (二级) | watchdog ↔ agent/node | 30 sec | SHA1, 6-digit (TOTP) | diff --git a/1-AgentSkills/developing-rmdc-system/examples/module-structure.go b/1-AgentSkills/developing-rmdc-system/examples/module-structure.go new file mode 100644 index 0000000..8e208b9 --- /dev/null +++ b/1-AgentSkills/developing-rmdc-system/examples/module-structure.go @@ -0,0 +1,177 @@ +// RMDC Module Standard Structure Example +// This file demonstrates the recommended project structure for RMDC business modules + +package main + +/* +Directory Structure: + +rmdc-{module-name}/ +├── cmd/ +│ └── main.go # Entry point +├── configs/ +│ └── config.yaml # Configuration file +├── internal/ +│ ├── config/ +│ │ └── config.go # Config struct and loader +│ ├── dao/ +│ │ └── {entity}_dao.go # Data access layer +│ ├── handler/ +│ │ └── {feature}_handler.go # HTTP handlers +│ ├── model/ +│ │ ├── dto/ +│ │ │ ├── request.go # Request DTOs +│ │ │ └── response.go # Response DTOs +│ │ └── entity/ +│ │ └── {table}.go # Database entities +│ └── service/ +│ └── {feature}_service.go # Business logic +├── pkg/ +│ └── {shared}/ # Shared packages +├── go.mod +└── go.sum +*/ + +import ( + "context" + "net/http" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// ============== Entity Layer ============== + +// Project represents the database entity +type Project struct { + ID int64 `gorm:"primaryKey" json:"id"` + ProjectID string `gorm:"uniqueIndex;size:64" json:"project_id"` + Name string `gorm:"size:128" json:"name"` + Namespace string `gorm:"uniqueIndex;size:64" json:"namespace"` + Status string `gorm:"size:32" json:"status"` +} + +func (Project) TableName() string { + return "projects" +} + +// ============== DTO Layer ============== + +// CreateProjectRequest is the request DTO +type CreateProjectRequest struct { + Name string `json:"name" binding:"required"` + Namespace string `json:"namespace" binding:"required"` +} + +// ProjectResponse is the response DTO +type ProjectResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +// ============== DAO Layer ============== + +// ProjectDAO handles database operations +type ProjectDAO struct { + db *gorm.DB +} + +func NewProjectDAO(db *gorm.DB) *ProjectDAO { + return &ProjectDAO{db: db} +} + +func (d *ProjectDAO) Create(ctx context.Context, project *Project) error { + return d.db.WithContext(ctx).Create(project).Error +} + +func (d *ProjectDAO) GetByID(ctx context.Context, id int64) (*Project, error) { + var project Project + err := d.db.WithContext(ctx).First(&project, id).Error + return &project, err +} + +// ============== Service Layer ============== + +// ProjectService contains business logic +type ProjectService struct { + dao *ProjectDAO +} + +func NewProjectService(dao *ProjectDAO) *ProjectService { + return &ProjectService{dao: dao} +} + +func (s *ProjectService) CreateProject(ctx context.Context, req *CreateProjectRequest) (*Project, error) { + project := &Project{ + Name: req.Name, + Namespace: req.Namespace, + Status: "PENDING", + } + // Business logic: generate project_id + project.ProjectID = "proj_" + req.Namespace + + if err := s.dao.Create(ctx, project); err != nil { + return nil, err + } + return project, nil +} + +// ============== Handler Layer ============== + +// ProjectHandler handles HTTP requests +type ProjectHandler struct { + service *ProjectService +} + +func NewProjectHandler(service *ProjectService) *ProjectHandler { + return &ProjectHandler{service: service} +} + +// CreateProject godoc +// @Summary Create a new project +// @Tags projects +// @Accept json +// @Produce json +// @Param request body CreateProjectRequest true "Create project request" +// @Success 200 {object} ProjectResponse +// @Router /api/projects/create [post] +func (h *ProjectHandler) CreateProject(c *gin.Context) { + var req CreateProjectRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, ProjectResponse{ + Code: 10004, + Message: "Invalid parameters: " + err.Error(), + }) + return + } + + project, err := h.service.CreateProject(c.Request.Context(), &req) + if err != nil { + c.JSON(http.StatusInternalServerError, ProjectResponse{ + Code: 10005, + Message: "Failed to create project: " + err.Error(), + }) + return + } + + c.JSON(http.StatusOK, ProjectResponse{ + Code: 0, + Message: "success", + Data: project, + }) +} + +// ============== Router Registration ============== + +// RegisterRoutes registers module routes to the router group +func RegisterRoutes(r *gin.RouterGroup, handler *ProjectHandler) { + projects := r.Group("/projects") + { + projects.POST("/create", handler.CreateProject) + // projects.POST("/list", handler.ListProjects) + // projects.POST("/detail", handler.GetProject) + // projects.POST("/update", handler.UpdateProject) + // projects.POST("/delete", handler.DeleteProject) + } +} diff --git a/1-AgentSkills/developing-rmdc-system/reference/api-design-rules.md b/1-AgentSkills/developing-rmdc-system/reference/api-design-rules.md new file mode 100644 index 0000000..8ccf489 --- /dev/null +++ b/1-AgentSkills/developing-rmdc-system/reference/api-design-rules.md @@ -0,0 +1,131 @@ +# RMDC API Design Rules + +## Core Principles + +1. **POST + RequestBody 优先**: 所有 API 优先使用 POST 方法 +2. **避免 PathVariables**: 资源标识放入 RequestBody +3. **避免 RequestParams**: 查询参数放入 RequestBody +4. **统一响应格式**: 所有响应遵循标准结构 + +## URL Naming Convention + +| Operation | Suffix | Example | +|-----------|--------|---------| +| List | `/list` | `POST /api/projects/list` | +| Detail | `/detail` | `POST /api/projects/detail` | +| Create | `/create` | `POST /api/projects/create` | +| Update | `/update` | `POST /api/projects/update` | +| Delete | `/delete` | `POST /api/projects/delete` | + +## Request Format + +```json +{ + "project_id": "namespace_xxx", + "page": 1, + "page_size": 20, + "filters": { + "status": "ONLINE", + "name": "keyword" + } +} +``` + +## Response Format + +### Success Response + +```json +{ + "code": 0, + "message": "success", + "data": { + // response payload + } +} +``` + +### Paginated Response + +```json +{ + "code": 0, + "message": "success", + "data": { + "list": [...], + "total": 100, + "page": 1, + "page_size": 20 + } +} +``` + +### Error Response + +```json +{ + "code": 40001, + "message": "Invalid parameter: project_id is required", + "data": null +} +``` + +## Error Code Ranges + +| Range | Module | Description | +|-------|--------|-------------| +| 0 | - | Success | +| 10000-19999 | rmdc-core | Gateway errors | +| 20000-29999 | rmdc-jenkins-dac | Jenkins errors | +| 30000-39999 | rmdc-project-mgmt | Project errors | +| 40000-49999 | rmdc-exchange-hub | Exchange errors | +| 50000-59999 | rmdc-watchdog | Watchdog errors | +| 60000-69999 | rmdc-user-auth | Auth errors | +| 70000-79999 | rmdc-audit-log | Audit errors | + +## Common Error Codes + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| 10001 | 401 | Unauthorized | +| 10002 | 403 | Forbidden | +| 10003 | 404 | Resource not found | +| 10004 | 400 | Invalid parameters | +| 10005 | 500 | Internal server error | +| 10006 | 429 | Rate limit exceeded | + +## HTTP Headers + +### Request Headers + +| Header | Required | Description | +|--------|----------|-------------| +| `Authorization` | Yes | `Bearer {jwt_token}` | +| `Content-Type` | Yes | `application/json` | +| `X-Request-ID` | No | 请求追踪 ID | + +### Response Headers + +| Header | Description | +|--------|-------------| +| `X-Request-ID` | 请求追踪 ID(回显或生成) | +| `X-Response-Time` | 响应耗时(毫秒) | + +## JWT Claims + +```json +{ + "user_id": 1, + "username": "admin", + "role": "SuperAdmin", + "permissions": ["project:read", "project:write"], + "exp": 1704501234, + "iat": 1704414834 +} +``` + +## Versioning Strategy + +- 当前版本: 无版本前缀 (`/api/...`) +- 破坏性变更: 使用版本前缀 (`/api/v2/...`) +- 废弃策略: 旧版本保留 6 个月,响应头标记 `Deprecation: true` diff --git a/1-AgentSkills/developing-rmdc-system/reference/authorization-layers.md b/1-AgentSkills/developing-rmdc-system/reference/authorization-layers.md new file mode 100644 index 0000000..846cdf3 --- /dev/null +++ b/1-AgentSkills/developing-rmdc-system/reference/authorization-layers.md @@ -0,0 +1,105 @@ +# RMDC Authorization Layers + +## Overview + +RMDC 采用双层授权架构,确保跨网络操作的安全性。 + +``` +┌────────────────────────────────────────────────────────────┐ +│ RMDC Platform (内网) │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ project-mgmt │────▶│ exchange-hub │ │ +│ │ (L1 Auth Center) │ │ (MQTT Gateway) │ │ +│ └──────────────────┘ └────────┬─────────┘ │ +└────────────────────────────────────┼───────────────────────┘ + │ MQTT (公网) +┌────────────────────────────────────┼───────────────────────┐ +│ Project Env (边缘/外网) │ +│ ┌───────▼─────────┐ │ +│ │ rmdc-watchdog │ │ +│ │ (L2 Auth Center)│ │ +│ └───────┬─────────┘ │ +│ ┌───────────────┼───────────────┐ │ +│ ┌─────▼─────┐ ┌─────▼─────┐ │ +│ │ watchdog │ │ watchdog │ │ +│ │ -node │ │ -agent │ │ +│ └───────────┘ └───────────┘ │ +└────────────────────────────────────────────────────────────┘ +``` + +## L1 Authorization (一级授权) + +| Attribute | Value | +|-----------|-------| +| Scope | project-management ↔ watchdog | +| Algorithm | SHA256 | +| Code Length | 8 digits | +| Validity | 30 minutes | +| Transport | MQTT message payload | + +### L1 Flow + +1. `project-management` 创建项目时生成 `auth_secret` +2. 部署 watchdog 时将 `auth_secret` 写入配置文件 +3. watchdog 启动后生成 TOTP 验证码发送注册请求 +4. `exchange-hub` 转发给 `project-management` 验证 +5. 验证通过后项目状态变为 `ONLINE` + +### L1 Code Generation + +```go +func GenerateL1Code(secret string, timestamp int64) string { + // 30-minute window + counter := timestamp / (30 * 60) + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(fmt.Sprintf("%d", counter))) + hash := h.Sum(nil) + offset := hash[len(hash)-1] & 0x0F + code := binary.BigEndian.Uint32(hash[offset:offset+4]) & 0x7FFFFFFF + return fmt.Sprintf("%08d", code%100000000) +} +``` + +## L2 Authorization (二级授权) + +| Attribute | Value | +|-----------|-------| +| Scope | watchdog ↔ watchdog-agent/node | +| Algorithm | SHA1 (RFC 6238 TOTP) | +| Code Length | 6 digits | +| Validity | 30 seconds | +| Transport | HTTP Header / gRPC Metadata | + +### L2 Flow + +1. watchdog 作为二级授权中心持有本项目 `agent_secret` +2. agent/node 启动时从配置获取 `agent_secret` +3. 每次请求携带当前 TOTP 验证码 +4. watchdog 验证后执行操作 + +### L2 Code Generation + +```go +// Standard TOTP (RFC 6238) +func GenerateL2Code(secret string) string { + key, _ := base32.StdEncoding.DecodeString(secret) + return totp.GenerateCode(key, time.Now()) +} +``` + +## Security Considerations + +1. **Secret Storage**: 所有 secret 必须加密存储(AES-256-GCM) +2. **Transport Security**: MQTT 必须启用 TLS +3. **Clock Sync**: 所有节点时间偏差 < 30 秒 +4. **Replay Protection**: 每个 message_id 只能使用一次 +5. **Rate Limiting**: 验证失败超过 5 次锁定 15 分钟 + +## Error Codes + +| Code | Description | Action | +|------|-------------|--------| +| AUTH_L1_INVALID | L1 验证码无效 | 检查 secret 配置 | +| AUTH_L1_EXPIRED | L1 验证码过期 | 重新生成验证码 | +| AUTH_L2_INVALID | L2 TOTP 无效 | 检查时间同步 | +| AUTH_L2_LOCKED | L2 验证已锁定 | 等待 15 分钟 | diff --git a/1-AgentSkills/developing-rmdc-system/reference/module-dependencies.md b/1-AgentSkills/developing-rmdc-system/reference/module-dependencies.md new file mode 100644 index 0000000..db6524b --- /dev/null +++ b/1-AgentSkills/developing-rmdc-system/reference/module-dependencies.md @@ -0,0 +1,61 @@ +# RMDC Module Dependencies + +## Dependency Matrix + +``` + rmdc-common rmdc-core audit-log user-auth project-mgmt exchange-hub watchdog jenkins-dac +rmdc-core ✓ - ✓ ✓ ✓ ✓ - ✓ +rmdc-jenkins-dac ✓ - ✓ - - - - - +rmdc-exchange-hub ✓ - ✓ - ✓ - - - +rmdc-watchdog ✓ - - - - - - - +rmdc-project-mgmt ✓ - ✓ - - ✓ - - +rmdc-audit-log ✓ - - - - - - - +rmdc-user-auth ✓ - ✓ - - - - - +``` + +## Dependency Rules + +### Allowed Dependencies + +| Module | Can Import | +|--------|------------| +| rmdc-core | All business modules (as router aggregator) | +| Business modules | rmdc-common, rmdc-audit-log (for logging) | +| rmdc-exchange-hub | rmdc-project-mgmt (for project validation) | + +### Prohibited Dependencies + +- **Business → Business**: 业务模块间禁止直接依赖 +- **Circular**: 任何形成环的依赖关系 +- **Edge → Platform**: watchdog 不可依赖平台侧模块(仅通过 MQTT 通信) + +## External Service Dependencies + +| Module | External Service | Protocol | Purpose | +|--------|-----------------|----------|---------| +| rmdc-jenkins-dac | Jenkins | REST API | Build management | +| rmdc-jenkins-dac | MinIO | S3 API | Artifact storage | +| rmdc-exchange-hub | MQTT Broker | MQTT | Message relay | +| rmdc-watchdog | K8S API Server | HTTPS | Container orchestration | +| All modules | PostgreSQL | TCP/5432 | Data persistence | + +## Data Flow Direction + +``` +Platform Side Edge Side +┌─────────────┐ ┌─────────────┐ +│ rmdc-core │ │ watchdog │ +│ ↓ │ │ ↓ │ +│ Business │ ──MQTT via ExHub──→ │ node/agent │ +│ Modules │ ←─MQTT via ExHub─── │ │ +└─────────────┘ └─────────────┘ +``` + +## Version Compatibility + +| Component | Min Version | Recommended | +|-----------|-------------|-------------| +| Go | 1.21 | 1.24+ | +| PostgreSQL | 13 | 15+ | +| MQTT Broker | 3.1.1 | 5.0 | +| Kubernetes | 1.24 | 1.28+ | diff --git a/1-AgentSkills/developing-rmdc-system/reference/mqtt-topics.md b/1-AgentSkills/developing-rmdc-system/reference/mqtt-topics.md new file mode 100644 index 0000000..d195dbc --- /dev/null +++ b/1-AgentSkills/developing-rmdc-system/reference/mqtt-topics.md @@ -0,0 +1,75 @@ +# RMDC MQTT Topic Registry + +## Topic Naming Convention + +``` +wdd/RDMC/{type}/{direction}[/{project_id}] +``` + +| Segment | Values | Description | +|---------|--------|-------------| +| type | `command`, `message` | 指令类 / 数据类 | +| direction | `up`, `down` | 上行(边缘→平台) / 下行(平台→边缘) | +| project_id | namespace_xxx | 项目标识(下行必需) | + +## Registered Topics + +### Uplink (Edge → Platform) + +| Topic | Publisher | Subscriber | Purpose | +|-------|-----------|------------|---------| +| `wdd/RDMC/command/up` | watchdog | exchange-hub | 边缘发送指令响应 | +| `wdd/RDMC/message/up` | watchdog | exchange-hub | 边缘发送数据/状态 | + +### Downlink (Platform → Edge) + +| Topic Pattern | Publisher | Subscriber | Purpose | +|---------------|-----------|------------|---------| +| `wdd/RDMC/command/down/{project_id}` | exchange-hub | watchdog | 下发操作指令 | +| `wdd/RDMC/message/down/{project_id}` | exchange-hub | watchdog | 下发配置/数据 | + +## Message Payload Schema + +```json +{ + "message_id": "uuid-v4", + "type": "command|message", + "project_id": "namespace_xxx", + "command_type": "k8s_exec|host_exec|register|heartbeat|...", + "timestamp": 1704501234567, + "version": "1.0", + "signature": "hmac-sha256-hex", + "payload": { + // command-specific data + } +} +``` + +## Command Types + +| command_type | Direction | Description | +|--------------|-----------|-------------| +| `register` | up | Watchdog 注册请求 | +| `heartbeat` | up/down | 心跳保活 | +| `k8s_exec` | down | K8S 操作指令 | +| `host_exec` | down | 主机操作指令 | +| `result` | up | 指令执行结果 | +| `log_stream` | up | 日志流数据 | +| `metrics` | up | 监控指标数据 | + +## QoS Requirements + +| Message Type | QoS Level | Reason | +|--------------|-----------|--------| +| Command (指令) | 1 | 至少一次,确保送达 | +| Result (结果) | 1 | 至少一次,确保响应 | +| Heartbeat | 0 | 最多一次,允许丢失 | +| Metrics | 0 | 最多一次,允许丢失 | +| Log stream | 0 | 最多一次,高频数据 | + +## Adding New Topic + +1. Update this file with new topic definition +2. Register in `exchange-hub/internal/mqtt/subscriber.go` +3. Add handler in `exchange-hub/internal/handler/mqtt_handler.go` +4. Update watchdog subscription if needed diff --git a/1-AgentSkills/developing-rmdc-system/scripts/verify-module-deps.sh b/1-AgentSkills/developing-rmdc-system/scripts/verify-module-deps.sh new file mode 100644 index 0000000..a7a98f4 --- /dev/null +++ b/1-AgentSkills/developing-rmdc-system/scripts/verify-module-deps.sh @@ -0,0 +1,172 @@ +#!/bin/bash +# verify-module-deps.sh +# Verifies RMDC module dependency rules +# +# Dependencies: +# - bash 4.0+ +# - grep +# - find +# +# Usage: +# ./verify-module-deps.sh [module-path] +# ./verify-module-deps.sh /path/to/rmdc-project-management + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +MODULE_PATH="${1:-.}" +ERRORS=0 +WARNINGS=0 + +echo "==========================================" +echo "RMDC Module Dependency Verification" +echo "==========================================" +echo "Checking: $MODULE_PATH" +echo "" + +# Define prohibited dependencies +declare -A PROHIBITED_DEPS +PROHIBITED_DEPS["rmdc-jenkins-branch-dac"]="rmdc-project-management rmdc-exchange-hub rmdc-watchdog rmdc-user-auth" +PROHIBITED_DEPS["rmdc-project-management"]="rmdc-jenkins-branch-dac rmdc-exchange-hub rmdc-watchdog rmdc-user-auth" +PROHIBITED_DEPS["rmdc-exchange-hub"]="rmdc-jenkins-branch-dac rmdc-watchdog rmdc-user-auth" +PROHIBITED_DEPS["rmdc-watchdog"]="rmdc-jenkins-branch-dac rmdc-project-management rmdc-exchange-hub rmdc-user-auth rmdc-audit-log" +PROHIBITED_DEPS["rmdc-user-auth"]="rmdc-jenkins-branch-dac rmdc-project-management rmdc-exchange-hub rmdc-watchdog" +PROHIBITED_DEPS["rmdc-audit-log"]="rmdc-jenkins-branch-dac rmdc-project-management rmdc-exchange-hub rmdc-watchdog rmdc-user-auth" + +# Detect current module +detect_module() { + if [ -f "$MODULE_PATH/go.mod" ]; then + grep "^module" "$MODULE_PATH/go.mod" | awk '{print $2}' | xargs basename + else + basename "$MODULE_PATH" + fi +} + +CURRENT_MODULE=$(detect_module) +echo "Detected module: $CURRENT_MODULE" +echo "" + +# Check 1: Verify go.mod exists +echo "[Check 1] go.mod existence..." +if [ -f "$MODULE_PATH/go.mod" ]; then + echo -e "${GREEN}PASS${NC}: go.mod found" +else + echo -e "${RED}FAIL${NC}: go.mod not found" + ((ERRORS++)) +fi +echo "" + +# Check 2: Verify prohibited imports in go.mod +echo "[Check 2] Prohibited dependencies in go.mod..." +if [ -f "$MODULE_PATH/go.mod" ]; then + PROHIBITED="${PROHIBITED_DEPS[$CURRENT_MODULE]:-}" + if [ -n "$PROHIBITED" ]; then + for dep in $PROHIBITED; do + if grep -q "$dep" "$MODULE_PATH/go.mod" 2>/dev/null; then + echo -e "${RED}FAIL${NC}: Prohibited dependency found: $dep" + ((ERRORS++)) + fi + done + if [ $ERRORS -eq 0 ]; then + echo -e "${GREEN}PASS${NC}: No prohibited dependencies in go.mod" + fi + else + echo -e "${YELLOW}SKIP${NC}: No prohibition rules for $CURRENT_MODULE" + fi +else + echo -e "${YELLOW}SKIP${NC}: go.mod not found" +fi +echo "" + +# Check 3: Verify import statements in Go files +echo "[Check 3] Prohibited imports in source code..." +if [ -d "$MODULE_PATH" ]; then + PROHIBITED="${PROHIBITED_DEPS[$CURRENT_MODULE]:-}" + if [ -n "$PROHIBITED" ]; then + for dep in $PROHIBITED; do + FOUND=$(find "$MODULE_PATH" -name "*.go" -exec grep -l "\".*$dep" {} \; 2>/dev/null | head -5) + if [ -n "$FOUND" ]; then + echo -e "${RED}FAIL${NC}: Found import of $dep in:" + echo "$FOUND" | while read -r f; do echo " - $f"; done + ((ERRORS++)) + fi + done + if [ $ERRORS -eq 0 ]; then + echo -e "${GREEN}PASS${NC}: No prohibited imports in source files" + fi + else + echo -e "${YELLOW}SKIP${NC}: No prohibition rules for $CURRENT_MODULE" + fi +fi +echo "" + +# Check 4: Verify standard directory structure +echo "[Check 4] Standard directory structure..." +REQUIRED_DIRS=("cmd" "internal" "configs") +OPTIONAL_DIRS=("pkg" "scripts" "docs") + +for dir in "${REQUIRED_DIRS[@]}"; do + if [ -d "$MODULE_PATH/$dir" ]; then + echo -e "${GREEN}PASS${NC}: Required directory exists: $dir/" + else + echo -e "${RED}FAIL${NC}: Missing required directory: $dir/" + ((ERRORS++)) + fi +done + +for dir in "${OPTIONAL_DIRS[@]}"; do + if [ -d "$MODULE_PATH/$dir" ]; then + echo -e "${GREEN}INFO${NC}: Optional directory exists: $dir/" + fi +done +echo "" + +# Check 5: Verify internal structure +echo "[Check 5] Internal package structure..." +INTERNAL_DIRS=("config" "dao" "handler" "model" "service") +if [ -d "$MODULE_PATH/internal" ]; then + for dir in "${INTERNAL_DIRS[@]}"; do + if [ -d "$MODULE_PATH/internal/$dir" ]; then + echo -e "${GREEN}PASS${NC}: internal/$dir/ exists" + else + echo -e "${YELLOW}WARN${NC}: internal/$dir/ not found" + ((WARNINGS++)) + fi + done +else + echo -e "${YELLOW}SKIP${NC}: internal/ directory not found" +fi +echo "" + +# Check 6: Verify rmdc-common dependency +echo "[Check 6] rmdc-common dependency..." +if [ -f "$MODULE_PATH/go.mod" ]; then + if grep -q "rmdc-common" "$MODULE_PATH/go.mod"; then + echo -e "${GREEN}PASS${NC}: rmdc-common dependency found" + else + echo -e "${YELLOW}WARN${NC}: rmdc-common dependency not found (may be acceptable for some modules)" + ((WARNINGS++)) + fi +fi +echo "" + +# Summary +echo "==========================================" +echo "Verification Summary" +echo "==========================================" +echo -e "Errors: ${RED}$ERRORS${NC}" +echo -e "Warnings: ${YELLOW}$WARNINGS${NC}" +echo "" + +if [ $ERRORS -gt 0 ]; then + echo -e "${RED}VERIFICATION FAILED${NC}" + exit 1 +else + echo -e "${GREEN}VERIFICATION PASSED${NC}" + exit 0 +fi diff --git a/1-AgentSkills/developing-watchdog/SKILL.md b/1-AgentSkills/developing-watchdog/SKILL.md new file mode 100644 index 0000000..12c7d6c --- /dev/null +++ b/1-AgentSkills/developing-watchdog/SKILL.md @@ -0,0 +1,101 @@ +--- +name: developing-watchdog +description: Guides development of rmdc-watchdog edge agent module including K8S operations, MQTT messaging, authorization management, and node/agent coordination. Use when implementing watchdog features, adding K8S actions, modifying heartbeat logic, or debugging authorization flows. Keywords: watchdog, edge-agent, k8s-operator, mqtt, authorization, heartbeat, node, agent. +argument-hint: ": k8s-action | heartbeat | mqtt-handler | node-comm | auth-flow" +allowed-tools: + - Read + - Glob + - Grep + - Bash + - Edit + - Write +--- + +# Developing rmdc-watchdog + +rmdc-watchdog 是部署在项目环境的边缘代理,职责包括:二级授权中心、K8S操作代理、指令接收执行、监控数据上报。 + +## 动态上下文注入 + +```bash +# 查看项目结构 +!`ls -la rmdc-watchdog/internal/` + +# 查找现有Handler实现 +!`grep -rn "func.*Handler" rmdc-watchdog/internal/handler/` + +# 查找MQTT消息路由 +!`grep -n "case\|switch" rmdc-watchdog/internal/service/message_router.go` +``` + +## Plan + +根据 `$ARGUMENTS` 确定开发类型: + +| 类型 | 产物 | 影响模块 | +|------|------|----------| +| k8s-action | `pkg/k8s/client.go`, `service/k8s_service.go` | exchange-hub指令定义 | +| heartbeat | `handler/heartbeat_handler.go`, `service/auth_service.go` | watchdog-agent同步修改 | +| mqtt-handler | `service/mqtt_service.go`, `service/message_router.go` | exchange-hub Topic契约 | +| node-comm | `service/node_service.go` | watchdog-node API同步 | +| auth-flow | `service/auth_service.go`, `dao/auth_dao.go` | project-management授权契约 | + +**决策点**: +1. 是否新增MQTT消息类型?→ 需同步 exchange-hub +2. 是否修改心跳结构?→ 需同步 watchdog-agent +3. 是否修改K8S指令参数?→ 需同步 octopus-operator + +## Verify + +- [ ] TOTP验证逻辑:一级(8位/30分钟/SHA256) vs 二级(6位/30秒/SHA1) +- [ ] K8S操作边界:仅允许审计过的操作(logs/exec/scale/restart/delete/get/apply) +- [ ] MQTT Topic格式:`wdd/RDMC/{command|message}/{up|down}/{project_id}` +- [ ] 时间戳校验:|now - timestamp| < 5分钟 +- [ ] Node通信:HTTP + Tier-Two TOTP认证 +- [ ] 执行结果上报:包含 command_id, status, exit_code, output, duration + +```bash +# 验证编译 +!`cd rmdc-watchdog && go build ./...` + +# 验证单元测试 +!`cd rmdc-watchdog && go test ./internal/... -v` +``` + +## Execute + +### 添加新K8S操作 + +1. 在 `pkg/k8s/client.go` 添加K8S API方法 +2. 在 `internal/service/k8s_service.go` 的 switch 添加 case +3. 更新 `K8sExecCommand` 结构(如需新参数) +4. 同步更新 exchange-hub 指令下发定义 + +### 添加新指令类型 + +1. 在 `message_router.go` 添加路由分支 +2. 创建对应 Handler 和 Service +3. 同步更新 exchange-hub 指令下发 + +### 修改心跳逻辑 + +1. 修改 `auth_service.go` 的 `VerifyHeartbeat` +2. 同步修改 watchdog-agent 心跳发送 +3. 更新 DTO 结构 + +## Pitfalls + +1. **TOTP层级混淆**:一级授权(project-management↔watchdog)与二级授权(watchdog↔agent/node)使用不同参数 +2. **时间偏移未处理**:授权文件需计算 `timeOffset = now - firstAuthTime` +3. **Node离线未检测**:转发主机指令前需 `CheckHostOnline(host_id)` +4. **日志截断遗漏**:业务故障日志仅回传最近300行 +5. **密钥公网传输**:tier_one_secret/tier_two_secret 必须通过配置文件离线部署,禁止MQTT传输 +6. **响应TOTP缺失**:双向验证要求服务端返回TOTP供客户端校验 +7. **心跳间隔不一致**:watchdog→exchange-hub 5秒;agent/node→watchdog 10秒(默认) + +## Reference + +- [状态机](reference/state-machine.md) +- [MQTT Topics](reference/mqtt-topics.md) +- [API端点](reference/api-endpoints.md) +- [安全机制](reference/security-mechanisms.md) diff --git a/1-AgentSkills/developing-watchdog/reference/api-endpoints.md b/1-AgentSkills/developing-watchdog/reference/api-endpoints.md new file mode 100644 index 0000000..285f74b --- /dev/null +++ b/1-AgentSkills/developing-watchdog/reference/api-endpoints.md @@ -0,0 +1,56 @@ +# Watchdog API 端点 + +## Watchdog HTTP API (Port: 8990) + +| 路径 | 方法 | 说明 | 认证 | +|------|------|------|------| +| `/api/heartbeat` | POST | Agent心跳接口 | Tier-Two TOTP | +| `/api/heartbeat/hosts` | GET | 获取所有心跳主机 | 内部调用 | +| `/api/node/info` | POST | Node信息上报接口 | Tier-Two TOTP | +| `/api/node/list` | GET | 获取所有Node列表 | 内部调用 | +| `/api/node/metrics/:node_id` | GET | 获取指定Node运行指标 | 内部调用 | +| `/api/authorization/generate` | GET | 生成授权文件 | 内部调用 | +| `/api/authorization/auth` | POST | 接收授权码 | Tier-One TOTP | +| `/api/authorization/hosts` | GET | 获取所有已授权主机 | 内部调用 | + +## Node HTTP API (Port: 8081) + +| 路径 | 方法 | 说明 | 认证 | +|------|------|------|------| +| `/api/exec` | POST | 执行命令 | Tier-Two TOTP | +| `/api/info` | GET | 获取主机信息 | Tier-Two TOTP | +| `/api/metrics` | GET | 获取运行指标 | Tier-Two TOTP | +| `/api/dltu` | POST | 镜像操作(Download-Load-Tag-Upload) | Tier-Two TOTP | + +## 请求/响应结构 + +### HeartbeatRequest +```go +type HeartbeatRequest struct { + HostInfo HostInfo `json:"host_info"` + EnvInfo EnvInfo `json:"env_info"` + Timestamp int64 `json:"timestamp"` + TOTPCode string `json:"totp_code"` +} +``` + +### HeartbeatResponse +```go +type HeartbeatResponse struct { + Authorized bool `json:"authorized"` + TOTPCode string `json:"totp_code"` + Timestamp int64 `json:"timestamp"` + SecondTOTPSecret string `json:"second_totp_secret,omitempty"` +} +``` + +### NodeInfoRequest +```go +type NodeInfoRequest struct { + NodeID string `json:"node_id"` + HostInfo HostInfo `json:"host_info"` + Metrics NodeRuntimeMetrics `json:"metrics"` + Timestamp int64 `json:"timestamp"` + TOTPCode string `json:"totp_code"` +} +``` diff --git a/1-AgentSkills/developing-watchdog/reference/mqtt-topics.md b/1-AgentSkills/developing-watchdog/reference/mqtt-topics.md new file mode 100644 index 0000000..8840396 --- /dev/null +++ b/1-AgentSkills/developing-watchdog/reference/mqtt-topics.md @@ -0,0 +1,50 @@ +# MQTT Topic 定义 + +## 上行(Watchdog → Exchange-Hub) + +| Topic | 消息类型 | 说明 | +|-------|----------|------| +| `wdd/RDMC/command/up` | register | 项目注册 | +| `wdd/RDMC/command/up` | auth_request | 授权申请 | +| `wdd/RDMC/message/up` | register_complete | 注册完成确认 | +| `wdd/RDMC/message/up` | heartbeat | 心跳数据 | +| `wdd/RDMC/message/up` | monitor | 监控数据上报 | +| `wdd/RDMC/message/up` | exec_result | 指令执行结果 | +| `wdd/RDMC/message/up` | log_result | 日志查询结果 | +| `wdd/RDMC/message/up` | alert | 告警信息 | + +## 下行(Exchange-Hub → Watchdog) + +| Topic | 消息类型 | 说明 | +|-------|----------|------| +| `wdd/RDMC/command/down/{project_id}` | auth_response | 授权响应 | +| `wdd/RDMC/command/down/{project_id}` | auth_revoke | 授权撤销 | +| `wdd/RDMC/command/down/{project_id}` | log_query | 日志查询指令 | +| `wdd/RDMC/command/down/{project_id}` | host_exec | 主机执行指令 | +| `wdd/RDMC/command/down/{project_id}` | k8s_exec | K8S执行指令 | +| `wdd/RDMC/command/down/{project_id}` | update | 业务更新指令 | +| `wdd/RDMC/message/down/{project_id}` | register_ack | 注册确认消息 | + +## Topic命名规范 + +- 前缀:`wdd/RDMC/` +- 类型:`command`(指令)或 `message`(消息) +- 方向:`up`(上行)或 `down`(下行) +- 项目ID:下行Topic需包含 `{project_id}` 用于路由 + +## 消息结构 + +```go +type BaseMessage struct { + MessageID string `json:"message_id"` + Type string `json:"type"` // command | message + ProjectID string `json:"project_id"` + Timestamp int64 `json:"timestamp"` +} + +type DataMessage struct { + BaseMessage + DataType string `json:"data_type"` // 具体消息类型 + Payload interface{} `json:"payload"` +} +``` diff --git a/1-AgentSkills/developing-watchdog/reference/security-mechanisms.md b/1-AgentSkills/developing-watchdog/reference/security-mechanisms.md new file mode 100644 index 0000000..611cafb --- /dev/null +++ b/1-AgentSkills/developing-watchdog/reference/security-mechanisms.md @@ -0,0 +1,43 @@ +# 安全机制汇总 + +## 通信安全 + +| 场景 | 安全机制 | 参数 | +|------|----------|------| +| Center ↔ Watchdog | Tier-One TOTP + AES-GCM | 8位码, 30分钟有效期, SHA256 | +| Watchdog ↔ Agent | Tier-Two TOTP | 6位码, 30秒有效期, SHA1 | +| Watchdog ↔ Node | Tier-Two TOTP复用 | 内网HTTP + TOTP认证 | +| HTTP备用接口 | 复用Tier-Two TOTP密钥 | 需要TOTP认证 | +| 消息传输 | TLS加密 | MQTT over TLS | +| 敏感数据 | AES-256-GCM加密 | 授权码、密钥等 | + +## 身份认证 + +| 机制 | 说明 | +|------|------| +| 主机信息 | 硬件指纹绑定: MachineID+CPU+Memory+Serial | +| 双向TOTP验证 | 请求方发送TOTP,响应方返回新TOTP | +| 挑战应答 | 32位随机挑战码确保通信双方身份 | + +## 授权保护 + +| 机制 | 说明 | +|------|------| +| 死手系统 | 心跳失败自毁,连续12次失败触发SIGTERM | +| 授权时间校验 | 检测时间篡改,timeOffset异常触发降级 | +| 授权撤销 | 支持远程撤销项目授权 | + +## 密钥传输原则 + +- tier_one_secret 和 tier_two_secret 在 project-management 创建项目时生成 +- 密钥通过项目配置文件离线部署到 Watchdog +- **禁止通过公网MQTT传输密钥** + +## 操作审计 + +| 操作类型 | 审计要求 | +|----------|----------| +| K8S操作 | 记录command_id, action, 执行结果 | +| 主机命令 | 记录script, args, exit_code | +| 授权变更 | 记录授权/撤销时间、操作人 | +| 数据导出 | 需签名+TOTP校验,写审计日志 | diff --git a/1-AgentSkills/developing-watchdog/reference/state-machine.md b/1-AgentSkills/developing-watchdog/reference/state-machine.md new file mode 100644 index 0000000..3ea67e2 --- /dev/null +++ b/1-AgentSkills/developing-watchdog/reference/state-machine.md @@ -0,0 +1,45 @@ +# Watchdog 状态机 + +## 连接状态机 + +``` +状态流转:offline -> connecting -> verifying -> online -> disconnecting -> offline +``` + +| 状态 | 触发条件 | 下一状态 | +|------|----------|----------| +| offline | 初始/心跳超时30秒 | connecting | +| connecting | 尝试MQTT连接 | verifying | +| verifying | TOTP双向验证 | online/offline | +| online | 验证成功 | disconnecting | +| disconnecting | 主动断开/网络异常 | offline | + +## 授权状态机 + +``` +未初始化 -> 收集主机信息 -> 等待授权 -> 已授权 + ↓ + 授权过期/撤销 -> 未授权 -> 等待授权(重新申请) +``` + +## 状态转换详情 + +### 未初始化 → 收集主机信息 +- 触发:Node/Agent首次连接 +- 动作:AddHostInfo() + +### 收集主机信息 → 等待授权 +- 触发:GenerateAuthorizationFile() +- 动作:发布授权申请Command到MQTT + +### 等待授权 → 已授权 +- 触发:收到有效授权码 +- 动作:解密并持久化授权信息 + +### 已授权 → 授权过期 +- 触发:时间篡改检测 +- 动作:设置initialized=false + +### 已授权 → 未授权 +- 触发:收到auth_revoke指令 +- 动作:清除本地授权存储 diff --git a/1-AgentSkills/developing-watchdog/scripts/verify-watchdog.sh b/1-AgentSkills/developing-watchdog/scripts/verify-watchdog.sh new file mode 100644 index 0000000..7673448 --- /dev/null +++ b/1-AgentSkills/developing-watchdog/scripts/verify-watchdog.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# verify-watchdog.sh - Watchdog模块验证脚本 +# 依赖: go 1.21+, golangci-lint (可选) +# 用法: ./verify-watchdog.sh [watchdog_dir] + +set -e + +WATCHDOG_DIR="${1:-./rmdc-watchdog}" + +echo "=== Watchdog 模块验证 ===" +echo "目标目录: $WATCHDOG_DIR" +echo "" + +# 检查目录存在 +if [ ! -d "$WATCHDOG_DIR" ]; then + echo "错误: 目录不存在 $WATCHDOG_DIR" + exit 1 +fi + +# 1. 编译检查 +echo "[1/5] 编译检查..." +cd "$WATCHDOG_DIR" +if go build ./... 2>&1; then + echo "✓ 编译通过" +else + echo "✗ 编译失败" + exit 1 +fi + +# 2. 单元测试 +echo "" +echo "[2/5] 单元测试..." +if go test ./internal/... -v -cover 2>&1; then + echo "✓ 单元测试通过" +else + echo "⚠ 部分测试失败,请检查" +fi + +# 3. Lint检查 +echo "" +echo "[3/5] Lint检查..." +if command -v golangci-lint &> /dev/null; then + if golangci-lint run ./... 2>&1; then + echo "✓ Lint检查通过" + else + echo "⚠ Lint检查有警告" + fi +else + echo "⚠ golangci-lint未安装,跳过" +fi + +# 4. TOTP参数验证 +echo "" +echo "[4/5] TOTP参数验证..." + +# Tier-One: 8位/30分钟 +if grep -rq "Digits.*8" pkg/totp/ 2>/dev/null || grep -rq "8.*Digits" pkg/totp/ 2>/dev/null; then + echo "✓ Tier-One TOTP位数配置存在" +else + echo "⚠ 未找到Tier-One TOTP位数定义 (应为8位)" +fi + +# Tier-Two: 6位/30秒 +if grep -rq "Digits.*6" pkg/totp/ 2>/dev/null || grep -rq "6.*Digits" pkg/totp/ 2>/dev/null; then + echo "✓ Tier-Two TOTP位数配置存在" +else + echo "⚠ 未找到Tier-Two TOTP位数定义 (应为6位)" +fi + +# 5. K8S操作白名单验证 +echo "" +echo "[5/5] K8S操作白名单验证..." +ALLOWED_ACTIONS="logs exec scale restart delete get apply" +K8S_SERVICE="internal/service/k8s_service.go" + +if [ -f "$K8S_SERVICE" ]; then + for action in $ALLOWED_ACTIONS; do + if grep -q "case \"$action\"" "$K8S_SERVICE" 2>/dev/null; then + echo "✓ K8S操作 '$action' 已实现" + else + echo "⚠ K8S操作 '$action' 未找到" + fi + done +else + echo "⚠ K8S服务文件不存在: $K8S_SERVICE" +fi + +echo "" +echo "=== 验证完成 ===" diff --git a/1-AgentSkills/developing-work-procedure/SKILL.md b/1-AgentSkills/developing-work-procedure/SKILL.md new file mode 100644 index 0000000..eef8335 --- /dev/null +++ b/1-AgentSkills/developing-work-procedure/SKILL.md @@ -0,0 +1,145 @@ +--- +name: developing-work-procedure +description: Guides development of rmdc-work-procedure workflow module including state machine implementation, workflow CRUD operations, status transitions, optimistic locking, WebSocket events, and Mermaid diagram generation. Triggers when modifying workflow tables, adding workflow types, changing state transitions, or implementing workflow APIs. Keywords: workflow, work-procedure, state-machine, optimistic-lock, workflow-type, transition, mermaid. +argument-hint: " - specify the workflow type (user_registration|user_management|project_detail|microservice_update), API path, database table, or state to work on" +allowed-tools: + - Read + - Edit + - Write + - Glob + - Grep + - Bash +--- + +# Developing Work Procedure Module + +本 Skill 指导 `rmdc-work-procedure` 工单流程模块的开发,包括状态机实现、工单 CRUD、状态转换、并发控制、事件通知与流程图生成。 + +## Quick Context + +```bash +# 动态注入:查看模块结构 +!`find . -name "*.go" -path "*/rmdc-work-procedure/*" | head -20` + +# 动态注入:查看现有状态定义 +!`grep -rn "status.*=" --include="*.go" ./rmdc-work-procedure/` +``` + +## Plan + +### 产物清单 +- [ ] 状态机配置/代码(状态定义、转换规则、权限矩阵) +- [ ] 数据库 Migration(workflows主表、steps表、history表、扩展表) +- [ ] API Handler + Service 实现 +- [ ] WebSocket 事件推送逻辑 +- [ ] Mermaid 流程图生成器 +- [ ] 单元测试 + 集成测试 + +### 决策点 +1. **工单类型**:新增还是修改现有类型?(user_registration / user_management / project_detail / microservice_update) +2. **状态扩展**:是否需要扩展状态?(如 microservice_update 的 executing/monitoring/rollbacked) +3. **并发策略**:乐观锁版本号校验是否充分? +4. **回调契约**:业务模块回调接口是否需要变更? + +## Verify + +### 状态机完整性 +- [ ] 所有状态均有明确的入口和出口转换 +- [ ] 终态(approved/rejected/revoked/closed)不可再转换 +- [ ] 撤销(revoke)仅对非终态生效 +- [ ] 状态转换权限矩阵与角色匹配(SuperAdmin/Creator/Assignee/System) + +### 数据库一致性 +- [ ] `workflows` 主表包含 `version` 字段(乐观锁) +- [ ] 扩展表 FK 正确引用 `workflows.id` +- [ ] `workflow_track_history` 记录所有状态变更 +- [ ] Migration 可回滚(提供 DOWN 脚本) + +### API 契约兼容 +- [ ] POST 统一风格(非 RESTful GET/PUT/DELETE) +- [ ] 响应结构 `{code, message, data}` 一致 +- [ ] 错误码对齐全局规范(409 版本冲突、403 权限不足、404 工单不存在) +- [ ] `version` 字段在更新请求中必传 + +### 事件一致性 +- [ ] WebSocket 事件类型枚举完整(见 reference/websocket-events.md) +- [ ] 事件 payload 包含 workflow_id、from_status、to_status +- [ ] 通知中心模块接口调用正确 + +### 流程图生成 +- [ ] Mermaid 源码语法正确(stateDiagram-v2) +- [ ] 当前节点高亮(classDef current) +- [ ] 前端异步加载 mermaid 库 + +## Execute + +### 1. 状态机实现 +```bash +# 检查现有状态定义 +!`grep -rn "const.*Status" ./rmdc-work-procedure/` + +# 创建/更新状态枚举 +# 参考 reference/state-machine.md +``` + +### 2. 数据库变更 +```bash +# 生成 migration 文件 +go run cmd/migrate/main.go create add_workflow_tables + +# 验证 schema +./scripts/verify-schema.sh +``` + +### 3. API 实现 +```bash +# 查看现有 handler +!`ls -la ./rmdc-work-procedure/internal/handler/` + +# 实现核心接口(参考 reference/api-contracts.md) +# - /api/workflow/create +# - /api/workflow/transition +# - /api/workflow/callback +``` + +### 4. 并发控制 +```go +// 乐观锁更新模板(见 examples/state-transition.go) +result := db.Model(&Workflow{}). + Where("id = ? AND version = ?", id, version). + Updates(map[string]interface{}{ + "status": newStatus, + "version": gorm.Expr("version + 1"), + }) +if result.RowsAffected == 0 { + return ErrVersionConflict // 409 +} +``` + +### 5. 测试验证 +```bash +# 运行状态机测试 +go test ./rmdc-work-procedure/... -run TestStateMachine -v + +# 验证 API 契约 +./scripts/verify-api-contracts.sh +``` + +## Pitfalls + +1. **版本号遗漏**:更新工单时忘记传递 `version` 字段,导致乐观锁失效;客户端需缓存并回传 version +2. **终态误转换**:对 approved/rejected/revoked/closed 状态尝试非法转换,需在服务层硬校验 +3. **扩展表不同步**:创建工单时忘记同步写入对应 `*_ext` 扩展表,导致业务数据丢失 +4. **事件推送遗漏**:状态变更后忘记调用通知中心,处理人无法收到实时通知 +5. **撤销硬删除**:user_registration 类型撤销需硬删除用户,其他类型仅标记状态 +6. **回调幂等**:业务模块回调 `/api/workflow/callback` 需做幂等处理,避免重复状态变更 +7. **草稿状态混淆**:project_detail 的 `draft_saved` 是扩展状态,不属于基础状态机 +8. **Mermaid 安全**:前端初始化 mermaid 需设置 `securityLevel: 'strict'`,避免 XSS + +## Related References + +- [状态机定义](reference/state-machine.md) +- [数据库 Schema](reference/database-schema.md) +- [API 契约](reference/api-contracts.md) +- [工单类型详情](reference/workflow-types.md) +- [WebSocket 事件](reference/websocket-events.md) diff --git a/1-AgentSkills/developing-work-procedure/examples/mermaid-generator.go b/1-AgentSkills/developing-work-procedure/examples/mermaid-generator.go new file mode 100644 index 0000000..0e5f9a4 --- /dev/null +++ b/1-AgentSkills/developing-work-procedure/examples/mermaid-generator.go @@ -0,0 +1,227 @@ +package mermaid + +import ( + "fmt" + "strings" +) + +// DiagramResponse 流程图响应 +type DiagramResponse struct { + MermaidCode string `json:"mermaid_code"` + CurrentNode string `json:"current_node"` + Nodes []DiagramNode `json:"nodes"` +} + +// DiagramNode 节点信息 +type DiagramNode struct { + ID string `json:"id"` + Label string `json:"label"` + Status string `json:"status"` // completed / current / pending / error + Assignee string `json:"assignee"` + CompletedAt string `json:"completed_at"` +} + +// DiagramGenerator 流程图生成器 +type DiagramGenerator struct{} + +// NewDiagramGenerator 创建生成器 +func NewDiagramGenerator() *DiagramGenerator { + return &DiagramGenerator{} +} + +// Generate 根据工单类型生成流程图 +func (g *DiagramGenerator) Generate(workflowType, currentStatus string, nodes []DiagramNode) *DiagramResponse { + switch workflowType { + case "user_registration": + return g.generateUserRegistration(currentStatus, nodes) + case "user_management": + return g.generateUserManagement(currentStatus, nodes) + case "project_detail": + return g.generateProjectDetail(currentStatus, nodes) + case "microservice_update": + return g.generateMicroserviceUpdate(currentStatus, nodes) + default: + return g.generateDefault(currentStatus, nodes) + } +} + +// generateUserRegistration 生成用户注册流程图 +func (g *DiagramGenerator) generateUserRegistration(currentStatus string, nodes []DiagramNode) *DiagramResponse { + var sb strings.Builder + + sb.WriteString("stateDiagram-v2\n") + sb.WriteString(" direction LR\n\n") + sb.WriteString(" [*] --> created: 用户注册\n\n") + sb.WriteString(" created --> pending_review: 自动提交审核\n") + sb.WriteString(" created --> revoked: 用户撤销\n\n") + sb.WriteString(" pending_review --> approved: 超管审批通过\n") + sb.WriteString(" pending_review --> rejected: 超管审批拒绝\n") + sb.WriteString(" pending_review --> revoked: 用户撤销\n\n") + sb.WriteString(" rejected --> pending_review: 用户修改后重新提交\n") + sb.WriteString(" rejected --> revoked: 用户撤销\n\n") + sb.WriteString(" approved --> closed: 关闭工单\n") + sb.WriteString(" rejected --> closed: 关闭工单\n") + sb.WriteString(" revoked --> closed: 关闭工单\n\n") + sb.WriteString(" closed --> [*]\n\n") + + g.appendStyles(&sb, currentStatus) + + return &DiagramResponse{ + MermaidCode: sb.String(), + CurrentNode: currentStatus, + Nodes: nodes, + } +} + +// generateUserManagement 生成用户管理流程图 +func (g *DiagramGenerator) generateUserManagement(currentStatus string, nodes []DiagramNode) *DiagramResponse { + var sb strings.Builder + + sb.WriteString("stateDiagram-v2\n") + sb.WriteString(" direction LR\n\n") + sb.WriteString(" [*] --> created: 发起管理操作\n\n") + sb.WriteString(" created --> pending_review: 提交审核\n") + sb.WriteString(" created --> revoked: 用户撤销\n") + sb.WriteString(" created --> closed: 用户关闭\n\n") + sb.WriteString(" pending_review --> approved: 超管审批通过\n") + sb.WriteString(" pending_review --> rejected: 超管审批拒绝\n") + sb.WriteString(" pending_review --> returned: 超管打回\n") + sb.WriteString(" pending_review --> revoked: 用户撤销\n\n") + sb.WriteString(" returned --> pending_review: 重新提交\n") + sb.WriteString(" returned --> revoked: 用户撤销\n") + sb.WriteString(" returned --> closed: 用户关闭\n\n") + sb.WriteString(" rejected --> closed: 关闭工单\n") + sb.WriteString(" approved --> closed: 关闭工单\n") + sb.WriteString(" revoked --> closed: 关闭工单\n\n") + sb.WriteString(" closed --> [*]\n\n") + + g.appendStyles(&sb, currentStatus) + + return &DiagramResponse{ + MermaidCode: sb.String(), + CurrentNode: currentStatus, + Nodes: nodes, + } +} + +// generateProjectDetail 生成项目详情工单流程图 +func (g *DiagramGenerator) generateProjectDetail(currentStatus string, nodes []DiagramNode) *DiagramResponse { + var sb strings.Builder + + sb.WriteString("stateDiagram-v2\n") + sb.WriteString(" direction LR\n\n") + sb.WriteString(" [*] --> created: 创建项目\n") + sb.WriteString(" created --> assigned: 分配填写人\n\n") + sb.WriteString(" assigned --> in_progress: 用户开始填写\n") + sb.WriteString(" assigned --> assigned: 重新分配\n\n") + sb.WriteString(" in_progress --> draft_saved: 保存草稿\n") + sb.WriteString(" draft_saved --> in_progress: 继续填写\n") + sb.WriteString(" in_progress --> pending_review: 提交审核\n") + sb.WriteString(" in_progress --> assigned: 重新分配\n\n") + sb.WriteString(" pending_review --> approved: 审批通过\n") + sb.WriteString(" pending_review --> returned: 审批打回\n\n") + sb.WriteString(" returned --> in_progress: 用户重新填写\n") + sb.WriteString(" returned --> pending_review: 重新提交\n\n") + sb.WriteString(" approved --> closed: 项目认证完成\n\n") + sb.WriteString(" closed --> [*]\n\n") + + g.appendStyles(&sb, currentStatus) + + return &DiagramResponse{ + MermaidCode: sb.String(), + CurrentNode: currentStatus, + Nodes: nodes, + } +} + +// generateMicroserviceUpdate 生成微服务更新流程图 +func (g *DiagramGenerator) generateMicroserviceUpdate(currentStatus string, nodes []DiagramNode) *DiagramResponse { + var sb strings.Builder + + sb.WriteString("stateDiagram-v2\n") + sb.WriteString(" direction LR\n\n") + sb.WriteString(" [*] --> created: 发起更新请求\n\n") + sb.WriteString(" created --> pending_review: 提交审核\n") + sb.WriteString(" created --> revoked: 用户撤销\n\n") + sb.WriteString(" pending_review --> approved: 审批通过\n") + sb.WriteString(" pending_review --> returned: 审批打回\n") + sb.WriteString(" pending_review --> rejected: 审批拒绝\n") + sb.WriteString(" pending_review --> revoked: 用户撤销\n\n") + sb.WriteString(" returned --> pending_review: 修改后重新提交\n") + sb.WriteString(" returned --> revoked: 用户撤销\n\n") + sb.WriteString(" approved --> executing: 开始执行\n\n") + sb.WriteString(" executing --> monitoring: 更新成功\n") + sb.WriteString(" executing --> rollbacked: 更新失败回滚\n\n") + sb.WriteString(" monitoring --> closed: 运行正常\n") + sb.WriteString(" monitoring --> rollbacked: 运行异常回滚\n\n") + sb.WriteString(" rollbacked --> closed: 记录失败信息\n") + sb.WriteString(" rejected --> closed: 关闭工单\n") + sb.WriteString(" revoked --> closed: 关闭工单\n\n") + sb.WriteString(" closed --> [*]\n\n") + + g.appendStyles(&sb, currentStatus) + + return &DiagramResponse{ + MermaidCode: sb.String(), + CurrentNode: currentStatus, + Nodes: nodes, + } +} + +// generateDefault 生成默认流程图 +func (g *DiagramGenerator) generateDefault(currentStatus string, nodes []DiagramNode) *DiagramResponse { + var sb strings.Builder + + sb.WriteString("stateDiagram-v2\n") + sb.WriteString(" direction LR\n\n") + sb.WriteString(" [*] --> created: 工单创建\n\n") + sb.WriteString(" created --> pending: 提交\n") + sb.WriteString(" created --> assigned: 自动分配\n\n") + sb.WriteString(" pending --> assigned: 分配\n\n") + sb.WriteString(" assigned --> in_progress: 接单\n\n") + sb.WriteString(" in_progress --> pending_review: 完成\n\n") + sb.WriteString(" pending_review --> approved: 通过\n") + sb.WriteString(" pending_review --> rejected: 拒绝\n") + sb.WriteString(" pending_review --> returned: 打回\n\n") + sb.WriteString(" returned --> in_progress: 重新提交\n\n") + sb.WriteString(" approved --> closed: 关闭\n") + sb.WriteString(" rejected --> closed: 关闭\n\n") + sb.WriteString(" closed --> [*]\n\n") + + g.appendStyles(&sb, currentStatus) + + return &DiagramResponse{ + MermaidCode: sb.String(), + CurrentNode: currentStatus, + Nodes: nodes, + } +} + +// appendStyles 添加样式定义 +func (g *DiagramGenerator) appendStyles(sb *strings.Builder, currentStatus string) { + sb.WriteString(" classDef completed fill:#4caf50,color:#fff\n") + sb.WriteString(" classDef current fill:#ff9800,color:#fff,stroke-width:3px\n") + sb.WriteString(" classDef pending fill:#e0e0e0,color:#666\n") + sb.WriteString(" classDef error fill:#f44336,color:#fff\n\n") + sb.WriteString(fmt.Sprintf(" class %s current\n", currentStatus)) +} + +// InjectNodeStyles 根据节点状态注入样式 +func (g *DiagramGenerator) InjectNodeStyles(mermaidCode string, nodes []DiagramNode) string { + var sb strings.Builder + sb.WriteString(mermaidCode) + sb.WriteString("\n") + + for _, node := range nodes { + switch node.Status { + case "completed": + sb.WriteString(fmt.Sprintf(" class %s completed\n", node.ID)) + case "current": + sb.WriteString(fmt.Sprintf(" class %s current\n", node.ID)) + case "error": + sb.WriteString(fmt.Sprintf(" class %s error\n", node.ID)) + } + } + + return sb.String() +} diff --git a/1-AgentSkills/developing-work-procedure/examples/state-transition.go b/1-AgentSkills/developing-work-procedure/examples/state-transition.go new file mode 100644 index 0000000..509df24 --- /dev/null +++ b/1-AgentSkills/developing-work-procedure/examples/state-transition.go @@ -0,0 +1,198 @@ +package statemachine + +// State 工单状态 +type State string + +const ( + StatusCreated State = "created" + StatusPending State = "pending" + StatusAssigned State = "assigned" + StatusInProgress State = "in_progress" + StatusPendingReview State = "pending_review" + StatusReturned State = "returned" + StatusApproved State = "approved" + StatusRejected State = "rejected" + StatusRevoked State = "revoked" + StatusClosed State = "closed" + // 扩展状态 - microservice_update + StatusExecuting State = "executing" + StatusMonitoring State = "monitoring" + StatusRollbacked State = "rollbacked" + // 扩展状态 - project_detail + StatusDraftSaved State = "draft_saved" +) + +// Event 转换事件 +type Event string + +const ( + EventSubmit Event = "submit" + EventAutoAssign Event = "auto_assign" + EventAssign Event = "assign" + EventAccept Event = "accept" + EventReassign Event = "reassign" + EventComplete Event = "complete" + EventApprove Event = "approve" + EventReject Event = "reject" + EventReturn Event = "return" + EventResubmit Event = "resubmit" + EventRevoke Event = "revoke" + EventClose Event = "close" + // 扩展事件 - microservice_update + EventExecute Event = "execute" + EventExecuteOK Event = "execute_ok" + EventExecuteFail Event = "execute_fail" + EventMonitorOK Event = "monitor_ok" + EventMonitorFail Event = "monitor_fail" + EventRollbackDone Event = "rollback_done" + // 扩展事件 - project_detail + EventSaveDraft Event = "save_draft" + EventContinueEdit Event = "continue_edit" +) + +// Transition 状态转换规则 +type Transition struct { + From State + Event Event + To State +} + +// BaseTransitions 基础状态转换规则 +var BaseTransitions = []Transition{ + // 创建后流转 + {StatusCreated, EventSubmit, StatusPending}, + {StatusCreated, EventAutoAssign, StatusAssigned}, + {StatusCreated, EventRevoke, StatusRevoked}, + + // 待分配流转 + {StatusPending, EventAssign, StatusAssigned}, + {StatusPending, EventRevoke, StatusRevoked}, + + // 已分配流转 + {StatusAssigned, EventAccept, StatusInProgress}, + {StatusAssigned, EventReassign, StatusAssigned}, + {StatusAssigned, EventRevoke, StatusRevoked}, + + // 处理中流转 + {StatusInProgress, EventComplete, StatusPendingReview}, + {StatusInProgress, EventReturn, StatusReturned}, + {StatusInProgress, EventReassign, StatusAssigned}, + {StatusInProgress, EventRevoke, StatusRevoked}, + + // 待审核流转 + {StatusPendingReview, EventApprove, StatusApproved}, + {StatusPendingReview, EventReject, StatusRejected}, + {StatusPendingReview, EventReturn, StatusReturned}, + {StatusPendingReview, EventRevoke, StatusRevoked}, + + // 已打回流转 + {StatusReturned, EventResubmit, StatusInProgress}, + {StatusReturned, EventRevoke, StatusRevoked}, + + // 终态关闭 + {StatusApproved, EventClose, StatusClosed}, + {StatusRejected, EventClose, StatusClosed}, + {StatusRevoked, EventClose, StatusClosed}, +} + +// MicroserviceUpdateTransitions 微服务更新扩展转换规则 +var MicroserviceUpdateTransitions = []Transition{ + {StatusApproved, EventExecute, StatusExecuting}, + {StatusExecuting, EventExecuteOK, StatusMonitoring}, + {StatusExecuting, EventExecuteFail, StatusRollbacked}, + {StatusMonitoring, EventMonitorOK, StatusClosed}, + {StatusMonitoring, EventMonitorFail, StatusRollbacked}, + {StatusRollbacked, EventRollbackDone, StatusClosed}, +} + +// ProjectDetailTransitions 项目详情扩展转换规则 +var ProjectDetailTransitions = []Transition{ + {StatusInProgress, EventSaveDraft, StatusDraftSaved}, + {StatusDraftSaved, EventContinueEdit, StatusInProgress}, +} + +// TerminalStates 终态集合 +var TerminalStates = map[State]bool{ + StatusApproved: true, + StatusRejected: true, + StatusRevoked: true, + StatusClosed: true, +} + +// AllTransitions 获取所有转换规则(包含扩展) +func AllTransitions(workflowType string) []Transition { + transitions := make([]Transition, len(BaseTransitions)) + copy(transitions, BaseTransitions) + + switch workflowType { + case "microservice_update": + transitions = append(transitions, MicroserviceUpdateTransitions...) + case "project_detail": + transitions = append(transitions, ProjectDetailTransitions...) + } + + return transitions +} + +// CanTransition 检查是否可以转换 +func CanTransition(from State, event Event) (State, bool) { + for _, t := range BaseTransitions { + if t.From == from && t.Event == event { + return t.To, true + } + } + return "", false +} + +// CanTransitionWithType 检查是否可以转换(带工单类型) +func CanTransitionWithType(from State, event Event, workflowType string) (State, bool) { + transitions := AllTransitions(workflowType) + for _, t := range transitions { + if t.From == from && t.Event == event { + return t.To, true + } + } + return "", false +} + +// IsTerminal 检查是否为终态 +func IsTerminal(s State) bool { + return TerminalStates[s] +} + +// CanRevoke 检查是否可撤销 +func CanRevoke(s State) bool { + return !IsTerminal(s) +} + +// GetAvailableEvents 获取当前状态可用的事件 +func GetAvailableEvents(from State, workflowType string) []Event { + transitions := AllTransitions(workflowType) + events := make([]Event, 0) + seen := make(map[Event]bool) + + for _, t := range transitions { + if t.From == from && !seen[t.Event] { + events = append(events, t.Event) + seen[t.Event] = true + } + } + + return events +} + +// ValidateTransitionChain 验证转换链是否合法 +func ValidateTransitionChain(workflowType string, states []State, events []Event) bool { + if len(states) != len(events)+1 { + return false + } + + for i, event := range events { + toState, valid := CanTransitionWithType(states[i], event, workflowType) + if !valid || toState != states[i+1] { + return false + } + } + + return true +} diff --git a/1-AgentSkills/developing-work-procedure/examples/workflow-service.go b/1-AgentSkills/developing-work-procedure/examples/workflow-service.go new file mode 100644 index 0000000..8dbe3ee --- /dev/null +++ b/1-AgentSkills/developing-work-procedure/examples/workflow-service.go @@ -0,0 +1,252 @@ +package service + +import ( + "context" + "fmt" + "time" + + "gorm.io/gorm" +) + +// Workflow 工单实体 +type Workflow struct { + ID string `gorm:"primaryKey;size:64"` + Version int `gorm:"not null;default:1"` + ModuleCode string `gorm:"size:32;not null"` + WorkflowType string `gorm:"size:32;not null"` + Priority int `gorm:"default:3"` + Status string `gorm:"size:32;not null"` + CurrentStep int `gorm:"default:1"` + TotalSteps int `gorm:"default:1"` + CreatorID int64 `gorm:"not null"` + CreatorName string `gorm:"size:64"` + AssigneeID *int64 + AssigneeName string `gorm:"size:64"` + DelegatedBy *int64 + Deadline *time.Time + StartedAt *time.Time + CompletedAt *time.Time + Title string `gorm:"size:256"` + Description string `gorm:"type:text"` + BusinessPayload string `gorm:"type:json"` + ResultPayload string `gorm:"type:json"` + CreatedAt time.Time + UpdatedAt time.Time +} + +// WorkflowTrackHistory 工单追踪历史 +type WorkflowTrackHistory struct { + ID int64 `gorm:"primaryKey;autoIncrement"` + WorkflowID string `gorm:"size:64;not null"` + FromStatus string `gorm:"size:32"` + ToStatus string `gorm:"size:32"` + Event string `gorm:"size:32"` + OperatorID int64 + OperatorType string `gorm:"size:16"` // user / system / timer + OperatorIP string `gorm:"size:45"` + ChangeDetails string `gorm:"type:json"` + Remark string `gorm:"type:text"` + CreatedAt time.Time +} + +// CreateWorkflowRequest 创建工单请求 +type CreateWorkflowRequest struct { + ModuleCode string + ModulePrefix string + WorkflowType string + Title string + Description string + Priority int + CreatorID int64 + CreatorName string + CreatorUsername string + AssigneeID *int64 + Deadline *time.Time + BusinessPayload string +} + +// TransitionRequest 状态转换请求 +type TransitionRequest struct { + WorkflowID string + Version int + FromStatus string + ToStatus string + Event string + OperatorID int64 + OperatorType string + OperatorIP string + Remark string +} + +// 错误定义 +var ( + ErrVersionConflict = fmt.Errorf("version conflict") + ErrWorkflowNotFound = fmt.Errorf("workflow not found") + ErrInvalidTransition = fmt.Errorf("invalid state transition") +) + +// WorkflowService 工单服务 +type WorkflowService struct { + db *gorm.DB +} + +// NewWorkflowService 创建工单服务 +func NewWorkflowService(db *gorm.DB) *WorkflowService { + return &WorkflowService{db: db} +} + +// CreateWorkflow 创建工单 +func (s *WorkflowService) CreateWorkflow(ctx context.Context, req *CreateWorkflowRequest) (*Workflow, error) { + // 生成工单ID: {module_prefix}-{timestamp}-{creator} + workflowID := fmt.Sprintf("%s-%s-%s", + req.ModulePrefix, + time.Now().Format("20060102150405"), + req.CreatorUsername, + ) + + workflow := &Workflow{ + ID: workflowID, + Version: 1, + ModuleCode: req.ModuleCode, + WorkflowType: req.WorkflowType, + Status: StatusCreated, + Priority: req.Priority, + CreatorID: req.CreatorID, + CreatorName: req.CreatorName, + AssigneeID: req.AssigneeID, + Title: req.Title, + Description: req.Description, + Deadline: req.Deadline, + BusinessPayload: req.BusinessPayload, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // 事务:同时创建主表和扩展表 + err := s.db.Transaction(func(tx *gorm.DB) error { + if err := tx.Create(workflow).Error; err != nil { + return err + } + // 根据工单类型创建扩展表记录 + return s.createExtension(tx, workflow, req) + }) + + if err != nil { + return nil, err + } + + return workflow, nil +} + +// createExtension 根据工单类型创建扩展表记录 +func (s *WorkflowService) createExtension(tx *gorm.DB, workflow *Workflow, req *CreateWorkflowRequest) error { + switch workflow.WorkflowType { + case WorkflowTypeUserRegistration: + // 创建用户注册扩展记录 + return tx.Exec(` + INSERT INTO user_registration_ext (workflow_id, target_user_id, original_status) + VALUES (?, ?, ?) + `, workflow.ID, 0, "disabled").Error + case WorkflowTypeUserManagement: + // 创建用户管理扩展记录 + return tx.Exec(` + INSERT INTO user_management_ext (workflow_id, target_user_id, action_type) + VALUES (?, ?, ?) + `, workflow.ID, 0, "modify").Error + case WorkflowTypeProjectDetail: + // 创建项目详情扩展记录 + return tx.Exec(` + INSERT INTO project_detail_ext (workflow_id, project_id) + VALUES (?, ?) + `, workflow.ID, "").Error + case WorkflowTypeMicroserviceUpdate: + // 创建微服务更新扩展记录 + return tx.Exec(` + INSERT INTO microservice_update_ext (workflow_id, project_id, namespace, service_name) + VALUES (?, ?, ?, ?) + `, workflow.ID, "", "", "").Error + } + return nil +} + +// TransitionWorkflow 状态转换(带乐观锁) +func (s *WorkflowService) TransitionWorkflow(ctx context.Context, req *TransitionRequest) error { + // 验证状态转换是否合法 + toStatus, valid := CanTransition(State(req.FromStatus), Event(req.Event)) + if !valid { + return ErrInvalidTransition + } + req.ToStatus = string(toStatus) + + return s.db.Transaction(func(tx *gorm.DB) error { + // 1. 乐观锁更新 + result := tx.Model(&Workflow{}). + Where("id = ? AND version = ?", req.WorkflowID, req.Version). + Updates(map[string]interface{}{ + "status": req.ToStatus, + "version": gorm.Expr("version + 1"), + "updated_at": time.Now(), + }) + + if result.RowsAffected == 0 { + return ErrVersionConflict // 409 + } + + // 2. 记录状态变更历史 + history := &WorkflowTrackHistory{ + WorkflowID: req.WorkflowID, + FromStatus: req.FromStatus, + ToStatus: req.ToStatus, + Event: req.Event, + OperatorID: req.OperatorID, + OperatorType: req.OperatorType, + OperatorIP: req.OperatorIP, + Remark: req.Remark, + CreatedAt: time.Now(), + } + return tx.Create(history).Error + }) +} + +// GetWorkflow 获取工单详情 +func (s *WorkflowService) GetWorkflow(ctx context.Context, workflowID string) (*Workflow, error) { + var workflow Workflow + if err := s.db.Where("id = ?", workflowID).First(&workflow).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrWorkflowNotFound + } + return nil, err + } + return &workflow, nil +} + +// GetWorkflowHistory 获取工单历史 +func (s *WorkflowService) GetWorkflowHistory(ctx context.Context, workflowID string) ([]WorkflowTrackHistory, error) { + var history []WorkflowTrackHistory + err := s.db.Where("workflow_id = ?", workflowID). + Order("created_at ASC"). + Find(&history).Error + return history, err +} + +// 工单类型常量 +const ( + WorkflowTypeUserRegistration = "user_registration" + WorkflowTypeUserManagement = "user_management" + WorkflowTypeProjectDetail = "project_detail" + WorkflowTypeMicroserviceUpdate = "microservice_update" +) + +// 状态常量 +const ( + StatusCreated = "created" + StatusPending = "pending" + StatusAssigned = "assigned" + StatusInProgress = "in_progress" + StatusPendingReview = "pending_review" + StatusReturned = "returned" + StatusApproved = "approved" + StatusRejected = "rejected" + StatusRevoked = "revoked" + StatusClosed = "closed" +) diff --git a/1-AgentSkills/developing-work-procedure/reference/api-contracts.md b/1-AgentSkills/developing-work-procedure/reference/api-contracts.md new file mode 100644 index 0000000..cab6966 --- /dev/null +++ b/1-AgentSkills/developing-work-procedure/reference/api-contracts.md @@ -0,0 +1,357 @@ +# API 契约 + +## 通用响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": { ... } +} +``` + +## 错误码 + +| 错误码 | HTTP Status | 说明 | +|-------|-------------|------| +| 0 | 200 | 成功 | +| 40001 | 400 | 参数校验失败 | +| 40301 | 403 | 权限不足 | +| 40401 | 404 | 工单不存在 | +| 40901 | 409 | 版本冲突(乐观锁) | +| 50001 | 500 | 服务器内部错误 | + +## 工单核心接口 + +### POST /api/workflow/create + +创建工单 + +**Request:** +```json +{ + "module_code": "rmdc-project-management", + "workflow_type": "project_detail", + "title": "项目信息填写-ProjectA", + "description": "请填写项目详细信息", + "priority": 3, + "assignee_id": 123, + "deadline": "2026-01-15T18:00:00Z", + "business_payload": { + "project_id": "proj-001" + } +} +``` + +**Response:** +```json +{ + "code": 0, + "message": "success", + "data": { + "workflow_id": "project-20260108100000-admin", + "status": "created", + "version": 1, + "created_at": "2026-01-08T10:00:00Z" + } +} +``` + +### POST /api/workflow/detail + +获取工单详情 + +**Request:** +```json +{ + "workflow_id": "project-20260108100000-admin" +} +``` + +**Response:** +```json +{ + "code": 0, + "message": "success", + "data": { + "id": "project-20260108100000-admin", + "version": 2, + "module_code": "rmdc-project-management", + "workflow_type": "project_detail", + "status": "in_progress", + "title": "项目信息填写-ProjectA", + "creator_id": 1, + "creator_name": "超级管理员", + "assignee_id": 123, + "assignee_name": "张三", + "current_step": 2, + "total_steps": 3, + "steps": [...], + "history": [...], + "business_payload": {...}, + "created_at": "2026-01-08T10:00:00Z", + "updated_at": "2026-01-08T11:30:00Z" + } +} +``` + +### POST /api/workflow/status + +获取工单状态(轻量) + +**Request:** +```json +{ + "workflow_id": "project-20260108100000-admin" +} +``` + +**Response:** +```json +{ + "code": 0, + "data": { + "workflow_id": "project-20260108100000-admin", + "status": "in_progress", + "version": 2, + "updated_at": "2026-01-08T11:30:00Z" + } +} +``` + +### POST /api/workflow/transition + +状态转换 + +**Request:** +```json +{ + "workflow_id": "project-20260108100000-admin", + "version": 2, + "event": "complete", + "remark": "项目信息填写完成,提交审核" +} +``` + +**Response:** +```json +{ + "code": 0, + "message": "success", + "data": { + "workflow_id": "project-20260108100000-admin", + "from_status": "in_progress", + "to_status": "pending_review", + "version": 3 + } +} +``` + +**版本冲突响应 (409):** +```json +{ + "code": 40901, + "message": "版本冲突,工单已被其他人修改,请刷新后重试" +} +``` + +### POST /api/workflow/assign + +分配处理人 + +**Request:** +```json +{ + "workflow_id": "project-20260108100000-admin", + "version": 1, + "assignee_id": 123, + "remark": "分配给张三填写" +} +``` + +### POST /api/workflow/delegate + +委派给他人 + +**Request:** +```json +{ + "workflow_id": "microservice-20260108150000-user", + "version": 3, + "delegate_to_id": 456, + "remark": "委派给李四执行更新" +} +``` + +### POST /api/workflow/revoke + +撤销工单 + +**Request:** +```json +{ + "workflow_id": "user-20260108090000-zhangsan", + "version": 2, + "confirm": true, + "remark": "用户取消注册" +} +``` + +### POST /api/workflow/callback + +业务模块回调 + +**Request:** +```json +{ + "workflow_id": "microservice-20260108150000-user", + "callback_type": "execute_complete", + "result": "success", + "result_payload": { + "execute_time": "2026-01-08T15:35:00Z", + "new_pod_count": 3 + } +} +``` + +**callback_type 枚举:** +- `execute_start` - 开始执行 +- `execute_complete` - 执行完成 +- `execute_failed` - 执行失败 +- `monitor_healthy` - 监控健康 +- `monitor_unhealthy` - 监控异常 +- `rollback_complete` - 回滚完成 + +## 工单列表与查询 + +### POST /api/workflow/list + +工单列表(分页) + +**Request:** +```json +{ + "page": 1, + "page_size": 20, + "workflow_type": "project_detail", + "status": ["in_progress", "pending_review"], + "creator_id": 1, + "assignee_id": 123, + "start_time": "2026-01-01T00:00:00Z", + "end_time": "2026-01-31T23:59:59Z" +} +``` + +**Response:** +```json +{ + "code": 0, + "data": { + "total": 50, + "page": 1, + "page_size": 20, + "list": [...] + } +} +``` + +### POST /api/workflow/my/created + +我发起的工单 + +### POST /api/workflow/my/assigned + +分配给我的工单 + +### POST /api/workflow/my/pending + +待我处理的工单 + +### POST /api/workflow/history + +工单历史记录 + +**Request:** +```json +{ + "workflow_id": "project-20260108100000-admin" +} +``` + +**Response:** +```json +{ + "code": 0, + "data": { + "history": [ + { + "id": 1, + "from_status": "created", + "to_status": "assigned", + "event": "assign", + "operator_name": "超级管理员", + "remark": "分配给张三填写", + "created_at": "2026-01-08T10:05:00Z" + } + ] + } +} +``` + +### POST /api/workflow/diagram + +获取工单流程图 + +**Request:** +```json +{ + "workflow_id": "project-20260108100000-admin" +} +``` + +**Response:** +```json +{ + "code": 0, + "data": { + "mermaid_code": "stateDiagram-v2\n direction LR\n [*] --> created: 创建项目\n ...", + "current_node": "pending_review", + "nodes": [ + { + "id": "created", + "label": "创建项目", + "status": "completed", + "assignee": "超级管理员", + "completed_at": "2026-01-08T10:00:00Z" + }, + { + "id": "in_progress", + "label": "填写信息", + "status": "completed", + "assignee": "张三", + "completed_at": "2026-01-08T14:00:00Z" + }, + { + "id": "pending_review", + "label": "待审核", + "status": "current", + "assignee": "超级管理员" + } + ] + } +} +``` + +## 权限管理接口(仅 SuperAdmin) + +### POST /api/workflow/permission/grant + +授予权限 + +### POST /api/workflow/permission/revoke + +撤销权限 + +### POST /api/workflow/permission/list + +权限列表 diff --git a/1-AgentSkills/developing-work-procedure/reference/database-schema.md b/1-AgentSkills/developing-work-procedure/reference/database-schema.md new file mode 100644 index 0000000..9758207 --- /dev/null +++ b/1-AgentSkills/developing-work-procedure/reference/database-schema.md @@ -0,0 +1,236 @@ +# 数据库 Schema + +## 主表 workflows + +```sql +CREATE TABLE workflows ( + id VARCHAR(64) PRIMARY KEY, -- 格式: {module}-{timestamp}-{creator} + version INT NOT NULL DEFAULT 1, -- 乐观锁版本号 + + -- 工单类型与来源 + module_code VARCHAR(32) NOT NULL, -- rmdc-user-auth / rmdc-project-management / deliver-update + workflow_type VARCHAR(32) NOT NULL, -- user_registration / user_management / project_detail / microservice_update + priority INT DEFAULT 3, -- 1-5 优先级 + + -- 状态与流程 + status VARCHAR(32) NOT NULL, + current_step INT DEFAULT 1, + total_steps INT DEFAULT 1, + + -- 人员关联 + creator_id BIGINT NOT NULL, + creator_name VARCHAR(64), + assignee_id BIGINT, + assignee_name VARCHAR(64), + delegated_by BIGINT, -- 委派来源 + + -- 时间管理 + deadline DATETIME, + started_at DATETIME, + completed_at DATETIME, + + -- 业务数据 + title VARCHAR(256), + description TEXT, + business_payload JSON, -- 通用业务数据 + result_payload JSON, -- 处理结果数据 + + -- 审计 + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + + INDEX idx_module_code (module_code), + INDEX idx_workflow_type (workflow_type), + INDEX idx_status (status), + INDEX idx_creator (creator_id), + INDEX idx_assignee (assignee_id) +); +``` + +## 步骤表 workflow_steps + +```sql +CREATE TABLE workflow_steps ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + workflow_id VARCHAR(64) NOT NULL, + step_order INT NOT NULL, + step_name VARCHAR(64), + step_type VARCHAR(32), -- submit / approve / execute / monitor + + assignee_id BIGINT, + assignee_name VARCHAR(64), + status VARCHAR(32), + result VARCHAR(32), + remark TEXT, + input_data JSON, + output_data JSON, + + started_at DATETIME, + completed_at DATETIME, + created_at DATETIME NOT NULL, + + INDEX idx_workflow (workflow_id), + FOREIGN KEY (workflow_id) REFERENCES workflows(id) +); +``` + +## 追踪历史表 workflow_track_history + +```sql +CREATE TABLE workflow_track_history ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + workflow_id VARCHAR(64) NOT NULL, + + from_status VARCHAR(32), + to_status VARCHAR(32), + event VARCHAR(32), -- submit / approve / reject / return / revoke / close + + operator_id BIGINT, + operator_type VARCHAR(16), -- user / system / timer + operator_ip VARCHAR(45), + + change_details JSON, + remark TEXT, + + created_at DATETIME NOT NULL, + + INDEX idx_workflow (workflow_id), + INDEX idx_operator (operator_id) +); +``` + +## 扩展表 + +### user_registration_ext + +用户注册工单扩展表 + +```sql +CREATE TABLE user_registration_ext ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + workflow_id VARCHAR(64) NOT NULL UNIQUE, + target_user_id BIGINT NOT NULL, -- 被注册用户ID + original_status VARCHAR(32), -- 原用户状态(备份) + FOREIGN KEY (workflow_id) REFERENCES workflows(id) +); +``` + +### user_management_ext + +用户管理工单扩展表 + +```sql +CREATE TABLE user_management_ext ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + workflow_id VARCHAR(64) NOT NULL UNIQUE, + target_user_id BIGINT NOT NULL, -- 被管理用户ID + action_type VARCHAR(32) NOT NULL, -- modify / delete / enable / disable + original_data JSON, -- 变更前数据快照 + modified_data JSON, -- 期望变更数据 + FOREIGN KEY (workflow_id) REFERENCES workflows(id) +); +``` + +**original_data / modified_data 示例:** + +```json +{ + "username": "zhangsan", + "chinese_name": "张三", + "email": "zhangsan@example.com", + "phone": "13800138000", + "group_name": "产品与解决方案组", + "dev_role": "backend", + "rmdc_role": "normal" +} +``` + +### project_detail_ext + +项目详情工单扩展表 + +```sql +CREATE TABLE project_detail_ext ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + workflow_id VARCHAR(64) NOT NULL UNIQUE, + project_id VARCHAR(64) NOT NULL, -- 关联项目ID + detail_filler_id BIGINT, -- 填写人ID + detail_filler_name VARCHAR(64), -- 填写人姓名 + draft_data JSON, -- 草稿数据 + FOREIGN KEY (workflow_id) REFERENCES workflows(id) +); +``` + +**draft_data 示例:** + +```json +{ + "deploy_business": { + "deployer_name": "张三", + "deployer_phone": "13800138000", + "deploy_start_time": "2026-01-01", + "system_version": "v3.2.0" + }, + "deploy_env": { + "network_type": "internal", + "main_public_ip": "10.0.0.1", + "host_count": 3 + }, + "deploy_middleware": { + "mysql": { + "internal_ip": "10.0.0.10", + "internal_port": 3306 + } + }, + "last_saved_at": "2026-01-08T10:30:00Z" +} +``` + +### microservice_update_ext + +微服务更新工单扩展表 + +```sql +CREATE TABLE microservice_update_ext ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + workflow_id VARCHAR(64) NOT NULL UNIQUE, + project_id VARCHAR(64) NOT NULL, -- 目标项目ID + namespace VARCHAR(64) NOT NULL, -- K8S命名空间 + service_name VARCHAR(128) NOT NULL, -- 微服务名称 + build_id VARCHAR(128), -- 构建物ID + current_version VARCHAR(64), -- 当前版本 + target_version VARCHAR(64), -- 目标版本 + scheduled_at DATETIME, -- 计划执行时间 + execute_result VARCHAR(32), -- success / failed / rollbacked + rollback_info JSON, -- 回滚信息 + FOREIGN KEY (workflow_id) REFERENCES workflows(id) +); +``` + +**rollback_info 示例:** + +```json +{ + "rollback_reason": "Pod健康检查失败", + "rollback_time": "2026-01-08T15:30:00Z", + "rollback_version": "v3.1.5", + "error_details": { + "pod_name": "service-abc-7d8f9c6b5d-xyz", + "error_message": "Readiness probe failed: connection refused", + "failed_checks": 3 + } +} +``` + +## Migration 回滚脚本 + +```sql +-- DOWN migration +DROP TABLE IF EXISTS microservice_update_ext; +DROP TABLE IF EXISTS project_detail_ext; +DROP TABLE IF EXISTS user_management_ext; +DROP TABLE IF EXISTS user_registration_ext; +DROP TABLE IF EXISTS workflow_track_history; +DROP TABLE IF EXISTS workflow_steps; +DROP TABLE IF EXISTS workflows; +``` diff --git a/1-AgentSkills/developing-work-procedure/reference/state-machine.md b/1-AgentSkills/developing-work-procedure/reference/state-machine.md new file mode 100644 index 0000000..49c2d4a --- /dev/null +++ b/1-AgentSkills/developing-work-procedure/reference/state-machine.md @@ -0,0 +1,107 @@ +# 工单状态机定义 + +## 基础状态(所有工单类型共享) + +| 状态代码 | 状态名称 | 说明 | 是否终态 | +|---------|---------|------|---------| +| `created` | 已创建 | 工单刚创建,等待分配或自动分配 | 否 | +| `pending` | 待分配 | 等待管理员手动分配处理人 | 否 | +| `assigned` | 已分配 | 已分配处理人,等待接单 | 否 | +| `in_progress` | 处理中 | 处理人正在业务模块中处理 | 否 | +| `pending_review` | 待审核 | 处理完成,等待审核 | 否 | +| `returned` | 已打回 | 审核未通过,需重新处理 | 否 | +| `approved` | 已通过 | 审核通过 | **是** | +| `rejected` | 已拒绝 | 审核拒绝,流程终止 | **是** | +| `revoked` | 已撤销 | 发起人撤销 | **是** | +| `closed` | 已关闭 | 流程完结 | **是** | + +## 扩展状态(按工单类型) + +| 工单类型 | 扩展状态 | 说明 | +|---------|---------|------| +| `microservice_update` | `executing` | 微服务更新执行中 | +| `microservice_update` | `monitoring` | 微服务运行状态监控中 | +| `microservice_update` | `rollbacked` | 更新失败已回滚 | +| `project_detail` | `draft_saved` | 草稿已保存 | + +## 状态转换权限矩阵 + +| 转换事件 | 触发角色 | 前置条件 | +|---------|---------|---------| +| `submit` | 创建人 | 状态为 `created` | +| `auto_assign` | 系统 | 配置了自动分配规则 | +| `assign` | SuperAdmin | 状态为 `pending` | +| `accept` | 处理人 | 状态为 `assigned` | +| `reassign` | SuperAdmin | 状态为 `assigned`/`in_progress` | +| `complete` | 处理人 | 状态为 `in_progress` | +| `approve` | SuperAdmin | 状态为 `pending_review` | +| `reject` | SuperAdmin | 状态为 `pending_review` | +| `return` | SuperAdmin | 状态为 `pending_review`/`in_progress` | +| `resubmit` | 创建人/处理人 | 状态为 `returned` | +| `revoke` | 创建人 | 状态非终态 | +| `close` | 系统/SuperAdmin | 状态为终态或 `revoked` | + +## 通用状态转换图 + +```mermaid +stateDiagram-v2 + [*] --> created: 工单创建 + + created --> pending: submit + created --> assigned: auto_assign + + pending --> assigned: assign + + assigned --> in_progress: accept + assigned --> assigned: reassign + + in_progress --> pending_review: complete + in_progress --> returned: return + in_progress --> assigned: reassign + + pending_review --> approved: approve + pending_review --> rejected: reject + pending_review --> returned: return + + returned --> in_progress: resubmit + + approved --> closed: close + rejected --> closed: close + + created --> revoked: revoke + pending --> revoked: revoke + assigned --> revoked: revoke + in_progress --> revoked: revoke(需确认) + pending_review --> revoked: revoke(需确认) + returned --> revoked: revoke + + revoked --> closed: close + + closed --> [*] +``` + +## 微服务更新扩展状态转换 + +```mermaid +stateDiagram-v2 + approved --> executing: 开始执行 + approved --> approved: 等待定时执行 + + executing --> monitoring: 更新成功 + executing --> executing: 执行重试 + executing --> rollbacked: 更新失败回滚 + + monitoring --> closed: 运行正常 + monitoring --> rollbacked: 运行异常回滚 + + rollbacked --> closed: 记录失败信息 +``` + +## 项目详情扩展状态转换 + +```mermaid +stateDiagram-v2 + in_progress --> draft_saved: 保存草稿 + draft_saved --> in_progress: 继续填写 + in_progress --> pending_review: 提交审核 +``` diff --git a/1-AgentSkills/developing-work-procedure/reference/websocket-events.md b/1-AgentSkills/developing-work-procedure/reference/websocket-events.md new file mode 100644 index 0000000..fd39778 --- /dev/null +++ b/1-AgentSkills/developing-work-procedure/reference/websocket-events.md @@ -0,0 +1,237 @@ +# WebSocket 事件定义 + +## 事件类型 + +| 事件类型 | 触发时机 | 接收方 | +|---------|---------|-------| +| `WORKFLOW_CREATED` | 工单创建 | 处理人 | +| `WORKFLOW_ASSIGNED` | 工单分配 | 处理人 | +| `WORKFLOW_REASSIGNED` | 工单重新委派 | 原处理人、新处理人 | +| `WORKFLOW_STATUS_CHANGED` | 状态变更 | 发起人、处理人 | +| `WORKFLOW_REVOKED` | 工单撤销 | 处理人 | +| `WORKFLOW_MODIFIED` | 工单内容修改 | 处理人 | +| `WORKFLOW_TIMEOUT` | 工单超时 | 发起人、处理人 | +| `WORKFLOW_COMPLETED` | 工单完成 | 发起人 | + +## 事件 Payload 结构 + +```json +{ + "event_type": "WORKFLOW_STATUS_CHANGED", + "workflow_id": "project-20260108100000-admin", + "workflow_type": "project_detail", + "from_status": "pending_review", + "to_status": "approved", + "operator_id": 1, + "operator_name": "超级管理员", + "timestamp": "2026-01-08T15:30:00Z", + "extra": { + "remark": "审批通过" + } +} +``` + +## 各事件详细说明 + +### WORKFLOW_CREATED + +工单创建事件 + +```json +{ + "event_type": "WORKFLOW_CREATED", + "workflow_id": "project-20260108100000-admin", + "workflow_type": "project_detail", + "title": "项目信息填写-ProjectA", + "creator_id": 1, + "creator_name": "超级管理员", + "assignee_id": 123, + "assignee_name": "张三", + "priority": 3, + "deadline": "2026-01-15T18:00:00Z", + "timestamp": "2026-01-08T10:00:00Z" +} +``` + +### WORKFLOW_ASSIGNED + +工单分配事件 + +```json +{ + "event_type": "WORKFLOW_ASSIGNED", + "workflow_id": "project-20260108100000-admin", + "assignee_id": 123, + "assignee_name": "张三", + "assigned_by_id": 1, + "assigned_by_name": "超级管理员", + "remark": "请在3天内完成项目信息填写", + "timestamp": "2026-01-08T10:05:00Z" +} +``` + +### WORKFLOW_REASSIGNED + +工单重新委派事件 + +```json +{ + "event_type": "WORKFLOW_REASSIGNED", + "workflow_id": "project-20260108100000-admin", + "from_assignee_id": 123, + "from_assignee_name": "张三", + "to_assignee_id": 456, + "to_assignee_name": "李四", + "reassigned_by_id": 1, + "reassigned_by_name": "超级管理员", + "remark": "张三请假,转交给李四处理", + "timestamp": "2026-01-09T09:00:00Z" +} +``` + +### WORKFLOW_STATUS_CHANGED + +状态变更事件 + +```json +{ + "event_type": "WORKFLOW_STATUS_CHANGED", + "workflow_id": "project-20260108100000-admin", + "workflow_type": "project_detail", + "from_status": "in_progress", + "to_status": "pending_review", + "event": "complete", + "operator_id": 123, + "operator_name": "张三", + "timestamp": "2026-01-08T14:00:00Z", + "extra": { + "remark": "项目信息填写完成" + } +} +``` + +### WORKFLOW_REVOKED + +工单撤销事件 + +```json +{ + "event_type": "WORKFLOW_REVOKED", + "workflow_id": "user-20260108090000-zhangsan", + "workflow_type": "user_registration", + "revoked_by_id": 100, + "revoked_by_name": "张三", + "remark": "用户取消注册", + "timestamp": "2026-01-08T11:00:00Z", + "extra": { + "target_user_deleted": true + } +} +``` + +### WORKFLOW_MODIFIED + +工单内容修改事件 + +```json +{ + "event_type": "WORKFLOW_MODIFIED", + "workflow_id": "project-20260108100000-admin", + "modified_by_id": 1, + "modified_by_name": "超级管理员", + "modified_fields": ["deadline", "priority"], + "timestamp": "2026-01-08T16:00:00Z", + "extra": { + "old_deadline": "2026-01-15T18:00:00Z", + "new_deadline": "2026-01-20T18:00:00Z", + "old_priority": 3, + "new_priority": 1 + } +} +``` + +### WORKFLOW_TIMEOUT + +工单超时事件 + +```json +{ + "event_type": "WORKFLOW_TIMEOUT", + "workflow_id": "project-20260108100000-admin", + "workflow_type": "project_detail", + "deadline": "2026-01-15T18:00:00Z", + "current_status": "in_progress", + "assignee_id": 123, + "assignee_name": "张三", + "timestamp": "2026-01-15T18:00:01Z" +} +``` + +### WORKFLOW_COMPLETED + +工单完成事件 + +```json +{ + "event_type": "WORKFLOW_COMPLETED", + "workflow_id": "project-20260108100000-admin", + "workflow_type": "project_detail", + "final_status": "approved", + "result": "success", + "creator_id": 1, + "creator_name": "超级管理员", + "timestamp": "2026-01-10T10:00:00Z", + "extra": { + "total_duration_hours": 48, + "steps_completed": 3 + } +} +``` + +## 前端处理策略 + +| 事件 | 前端行为 | +|-----|---------| +| `WORKFLOW_CREATED` | 待办列表增加新工单提醒 | +| `WORKFLOW_ASSIGNED` | 显示分配通知,可跳转工单详情 | +| `WORKFLOW_REASSIGNED` | 原处理人:显示提示弹窗,切换只读模式;新处理人:显示接收通知 | +| `WORKFLOW_STATUS_CHANGED` | 更新工单状态显示,刷新流程图高亮 | +| `WORKFLOW_REVOKED` | 自动关闭处理页面,引导至工单列表 | +| `WORKFLOW_MODIFIED` | 提示"工单已被修改",自动刷新或提示手动刷新 | +| `WORKFLOW_TIMEOUT` | 显示超时警告,标红显示 | +| `WORKFLOW_COMPLETED` | 显示完成通知,可查看结果 | + +## 并发冲突处理 + +### 乐观锁冲突场景 + +当用户正在处理工单时,收到 `WORKFLOW_REASSIGNED` 或 `WORKFLOW_MODIFIED` 事件: + +1. **WORKFLOW_REASSIGNED 处理流程**: + - 显示弹窗:「此工单已被重新委派给XXX,您的处理权限已转移」 + - 页面切换为只读模式 + - 提供"返回列表"按钮 + - 若用户仍尝试提交,后端返回 409 Conflict + +2. **WORKFLOW_MODIFIED 处理流程**: + - 显示提示:「工单内容已被修改,请刷新页面」 + - 若用户有未保存的本地修改,提示冲突 + - 提供"刷新"和"查看变更"选项 + +### 版本号同步 + +前端需在每次获取工单详情时缓存 `version` 字段,提交操作时携带该版本号: + +```javascript +// 获取工单详情 +const { data } = await api.getWorkflowDetail(workflowId); +this.workflowVersion = data.version; + +// 提交状态转换 +await api.transitionWorkflow({ + workflow_id: workflowId, + version: this.workflowVersion, // 必传 + event: 'complete', + remark: '处理完成' +}); +``` diff --git a/1-AgentSkills/developing-work-procedure/reference/workflow-types.md b/1-AgentSkills/developing-work-procedure/reference/workflow-types.md new file mode 100644 index 0000000..fd2d911 --- /dev/null +++ b/1-AgentSkills/developing-work-procedure/reference/workflow-types.md @@ -0,0 +1,175 @@ +# 工单类型定义 + +## 类型列表 + +| 工单类型代码 | 类型名称 | 来源模块 | 可撤销 | 特殊处理 | +|-------------|---------|---------|-------|---------| +| `user_registration` | 用户注册工单 | rmdc-user-auth | 是 | 撤销时硬删除用户 | +| `user_management` | 用户管理工单 | rmdc-user-auth | 是 | 保存变更前后快照 | +| `project_detail` | 项目信息填写工单 | rmdc-project-management | 否(仅超管) | 支持草稿保存 | +| `microservice_update` | 微服务更新工单 | deliver-update | 是 | 执行/监控/回滚扩展状态 | + +## 工单 ID 生成规则 + +**格式**:`{module_prefix}-{timestamp}-{creator_username}` + +**示例**: +- `project-20260106110303-zeaslity` +- `user-20260108093000-admin` +- `microservice-20260108150000-zhangsan` + +## 各类型详细说明 + +### user_registration(用户注册工单) + +**业务场景**:非超级管理员用户注册新用户后,需要经过超级管理员审批才能激活用户账号。 + +**业务规则**: +| 规则项 | 说明 | +|-------|------| +| 发起人 | 管理员、普通用户 | +| 默认处理人 | 超级管理员 | +| 被注册用户初始状态 | `disabled` | +| 审批通过后用户状态 | `active` | +| 可撤销 | 是(创建人可撤销) | +| 撤销后处理 | **硬删除**被注册用户 | + +**步骤定义**: +| 步骤序号 | 步骤名称 | 步骤类型 | 处理人 | +|---------|---------|---------|-------| +| 1 | 提交注册 | `submit` | 系统自动 | +| 2 | 审核注册 | `approve` | 超级管理员 | + +**扩展字段**(user_registration_ext): +- `target_user_id` - 被注册用户ID +- `original_status` - 原用户状态(备份) + +--- + +### user_management(用户管理工单) + +**业务场景**:非超级管理员用户对已注册用户进行管理操作(修改、删除、启用、禁用)时,需要经过超级管理员审批。 + +**业务规则**: +| 规则项 | 说明 | +|-------|------| +| 发起人 | 管理员、普通用户(只能管理自己注册的用户) | +| 默认处理人 | 超级管理员 | +| 操作类型 | `modify`/`delete`/`enable`/`disable` | +| 可撤销 | 是 | +| 可关闭 | 是(创建人可主动关闭) | + +**权限矩阵**: +| 操作人角色 | 可管理用户范围 | +|-----------|--------------| +| 超级管理员 | 所有用户(无需工单) | +| 管理员 | 普通用户、三方用户(自己注册的) | +| 普通用户 | 三方用户(自己注册的) | +| 三方用户 | 无 | + +**步骤定义**: +| 步骤序号 | 步骤名称 | 步骤类型 | 处理人 | +|---------|---------|---------|-------| +| 1 | 提交管理操作 | `submit` | 创建人 | +| 2 | 审核管理操作 | `approve` | 超级管理员 | + +**扩展字段**(user_management_ext): +- `target_user_id` - 被管理用户ID +- `action_type` - 操作类型 +- `original_data` - 变更前数据快照 +- `modified_data` - 期望变更数据 + +--- + +### project_detail(项目信息填写工单) + +**业务场景**:超级管理员创建项目元数据后,分配给普通用户填写项目详情信息,填写完成后提交审核。 + +**业务规则**: +| 规则项 | 说明 | +|-------|------| +| 发起人 | 超级管理员 | +| 填写人 | 被分配的普通用户 | +| 审批人 | 超级管理员 | +| 支持草稿 | 是(可多次保存草稿) | +| 可撤销 | 否(仅超级管理员可取消) | +| 项目状态关联 | 草稿状态绑定唯一工单 | + +**项目状态对应关系**: +| 工单状态 | 项目认证状态 | +|---------|------------| +| `assigned` / `in_progress` | `draft` (草稿) | +| `pending_review` | `pending` (待审核) | +| `approved` / `closed` | `official` (正式) | +| `returned` | `draft` (草稿) | + +**步骤定义**: +| 步骤序号 | 步骤名称 | 步骤类型 | 处理人 | +|---------|---------|---------|-------| +| 1 | 分配填写人 | `assign` | 超级管理员 | +| 2 | 填写项目信息 | `execute` | 被分配用户 | +| 3 | 审核项目信息 | `approve` | 超级管理员 | + +**扩展字段**(project_detail_ext): +- `project_id` - 关联项目ID +- `detail_filler_id` - 填写人ID +- `detail_filler_name` - 填写人姓名 +- `draft_data` - 草稿数据 + +--- + +### microservice_update(微服务更新工单) + +**业务场景**:普通用户在构建详情页面发起微服务更新请求,经过超级管理员审批后执行更新操作,并监控微服务运行状态。 + +**业务规则**: +| 规则项 | 说明 | +|-------|------| +| 发起人 | 普通用户 | +| 审批人 | 超级管理员 | +| 执行人 | 超级管理员或委派人 | +| 支持定时执行 | 是 | +| 支持委派 | 是 | +| 自动回滚 | 是(更新失败时自动回滚) | + +**相关模块**: +| 模块 | 职责 | +|-----|------| +| rmdc-jenkins-branch-dac | 提供构建物信息 | +| rmdc-exchange-hub | MQTT消息中继 | +| rmdc-watchdog | K8S操作执行、状态监控 | + +**步骤定义**: +| 步骤序号 | 步骤名称 | 步骤类型 | 处理人 | +|---------|---------|---------|-------| +| 1 | 提交更新请求 | `submit` | 普通用户 | +| 2 | 审核更新请求 | `approve` | 超级管理员 | +| 3 | 执行更新 | `execute` | 超级管理员/委派人 | +| 4 | 监控运行状态 | `monitor` | 系统自动 | + +**扩展字段**(microservice_update_ext): +- `project_id` - 目标项目ID +- `namespace` - K8S命名空间 +- `service_name` - 微服务名称 +- `build_id` - 构建物ID +- `current_version` - 当前版本 +- `target_version` - 目标版本 +- `scheduled_at` - 计划执行时间 +- `execute_result` - 执行结果 +- `rollback_info` - 回滚信息 + +**更新页面布局**: +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 微服务更新 │ +├─────────────────┬─────────────────────────┬────────────────────┤ +│ 构建物信息 │ 更新表单 │ 操作区 │ +│ │ │ │ +│ 组织: acme │ 目标项目: [下拉选择] │ │ +│ 仓库: service │ 目标微服务: [下拉选择] │ [发起更新] │ +│ 分支: main │ 更新时间: [日期选择] │ │ +│ 构建号: #123 │ ○ 立即执行 │ [取消] │ +│ 版本: v3.2.0 │ ○ 定时执行 │ │ +│ │ 备注: [文本框] │ │ +└─────────────────┴─────────────────────────┴────────────────────┘ +``` diff --git a/1-AgentSkills/developing-work-procedure/scripts/verify-api-contracts.sh b/1-AgentSkills/developing-work-procedure/scripts/verify-api-contracts.sh new file mode 100644 index 0000000..5c01c09 --- /dev/null +++ b/1-AgentSkills/developing-work-procedure/scripts/verify-api-contracts.sh @@ -0,0 +1,143 @@ +#!/bin/bash +# 验证 API 契约一致性 +# 依赖: curl, jq +# 用法: ./verify-api-contracts.sh [API_BASE_URL] + +set -e + +API_BASE="${1:-http://localhost:8080}" + +echo "=== 验证 API 契约 ===" +echo "API 地址: $API_BASE" +echo "" + +# 检查依赖 +command -v curl >/dev/null 2>&1 || { echo "[ERROR] 需要安装 curl"; exit 1; } +command -v jq >/dev/null 2>&1 || { echo "[ERROR] 需要安装 jq"; exit 1; } + +FAILED=0 +PASSED=0 + +# 必需的接口列表 +ENDPOINTS=( + "/api/workflow/create" + "/api/workflow/detail" + "/api/workflow/status" + "/api/workflow/transition" + "/api/workflow/assign" + "/api/workflow/delegate" + "/api/workflow/revoke" + "/api/workflow/callback" + "/api/workflow/list" + "/api/workflow/my/created" + "/api/workflow/my/assigned" + "/api/workflow/my/pending" + "/api/workflow/history" + "/api/workflow/diagram" +) + +echo "--- 检查接口可访问性 ---" +for endpoint in "${ENDPOINTS[@]}"; do + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$API_BASE$endpoint" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer test-token" \ + -d '{}' 2>/dev/null || echo "000") + + if [ "$HTTP_CODE" == "000" ]; then + echo "[FAIL] 接口 $endpoint 无法连接" + ((FAILED++)) + elif [ "$HTTP_CODE" == "404" ]; then + echo "[FAIL] 接口 $endpoint 不存在 (404)" + ((FAILED++)) + elif [ "$HTTP_CODE" == "401" ] || [ "$HTTP_CODE" == "403" ]; then + echo "[PASS] 接口 $endpoint 存在 (需要认证: $HTTP_CODE)" + ((PASSED++)) + else + echo "[PASS] 接口 $endpoint 可访问 (HTTP $HTTP_CODE)" + ((PASSED++)) + fi +done + +echo "" +echo "--- 检查响应格式 ---" + +# 测试 /api/workflow/list 接口响应格式 +RESP=$(curl -s -X POST "$API_BASE/api/workflow/list" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer test-token" \ + -d '{"page": 1, "page_size": 1}' 2>/dev/null || echo '{}') + +# 检查 code 字段 +if echo "$RESP" | jq -e '.code' > /dev/null 2>&1; then + CODE=$(echo "$RESP" | jq '.code') + echo "[PASS] 响应包含 code 字段 (值: $CODE)" + ((PASSED++)) +else + echo "[FAIL] 响应缺少 code 字段" + ((FAILED++)) +fi + +# 检查 message 字段 +if echo "$RESP" | jq -e '.message' > /dev/null 2>&1; then + echo "[PASS] 响应包含 message 字段" + ((PASSED++)) +else + echo "[FAIL] 响应缺少 message 字段" + ((FAILED++)) +fi + +# 检查 data 字段 +if echo "$RESP" | jq -e '.data' > /dev/null 2>&1; then + echo "[PASS] 响应包含 data 字段" + ((PASSED++)) +else + echo "[WARN] 响应可能缺少 data 字段(空响应或错误)" +fi + +echo "" +echo "--- 检查错误码规范 ---" + +# 测试 404 错误码 +RESP_404=$(curl -s -X POST "$API_BASE/api/workflow/detail" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer test-token" \ + -d '{"workflow_id": "non-existent-id"}' 2>/dev/null || echo '{}') + +if echo "$RESP_404" | jq -e '.code' > /dev/null 2>&1; then + ERROR_CODE=$(echo "$RESP_404" | jq '.code') + if [ "$ERROR_CODE" == "40401" ] || [ "$ERROR_CODE" == "404" ]; then + echo "[PASS] 404 错误码符合规范 (code: $ERROR_CODE)" + ((PASSED++)) + else + echo "[INFO] 404 错误码: $ERROR_CODE" + fi +fi + +echo "" +echo "--- 检查版本冲突处理 ---" + +# 测试 409 版本冲突 +RESP_409=$(curl -s -X POST "$API_BASE/api/workflow/transition" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer test-token" \ + -d '{"workflow_id": "test", "version": -1, "event": "approve"}' 2>/dev/null || echo '{}') + +if echo "$RESP_409" | jq -e '.code' > /dev/null 2>&1; then + ERROR_CODE=$(echo "$RESP_409" | jq '.code') + echo "[INFO] 版本冲突响应码: $ERROR_CODE" +fi + +echo "" +echo "=== 验证结果 ===" +echo "通过: $PASSED" +echo "失败: $FAILED" + +if [ $FAILED -gt 0 ]; then + echo "" + echo "部分 API 契约验证失败,请检查接口实现" + exit 1 +else + echo "" + echo "API 契约验证通过!" + exit 0 +fi diff --git a/1-AgentSkills/developing-work-procedure/scripts/verify-schema.sh b/1-AgentSkills/developing-work-procedure/scripts/verify-schema.sh new file mode 100644 index 0000000..6e071d0 --- /dev/null +++ b/1-AgentSkills/developing-work-procedure/scripts/verify-schema.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# 验证数据库 Schema 完整性 +# 依赖: psql (PostgreSQL) 或 mysql client +# 用法: ./verify-schema.sh + +set -e + +DB_TYPE="${DB_TYPE:-postgres}" +DB_HOST="${DB_HOST:-localhost}" +DB_PORT="${DB_PORT:-5432}" +DB_NAME="${DB_NAME:-rmdc}" +DB_USER="${DB_USER:-rmdc}" +DB_PASSWORD="${DB_PASSWORD:-}" + +echo "=== 验证工单模块 Schema ===" +echo "数据库: $DB_TYPE://$DB_HOST:$DB_PORT/$DB_NAME" +echo "" + +# 必需的表 +REQUIRED_TABLES=( + "workflows" + "workflow_steps" + "workflow_track_history" + "user_registration_ext" + "user_management_ext" + "project_detail_ext" + "microservice_update_ext" +) + +# workflows 表必需的列 +WORKFLOWS_COLUMNS=( + "id" + "version" + "module_code" + "workflow_type" + "status" + "creator_id" + "assignee_id" + "created_at" + "updated_at" +) + +FAILED=0 +PASSED=0 + +# 检查表是否存在 +check_table_exists() { + local table=$1 + if [ "$DB_TYPE" == "postgres" ]; then + result=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c \ + "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = '$table')" 2>/dev/null | tr -d ' ') + [ "$result" == "t" ] && return 0 || return 1 + else + result=$(mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" -D "$DB_NAME" -N -e \ + "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = '$table'" 2>/dev/null) + [ "$result" -gt 0 ] && return 0 || return 1 + fi +} + +# 检查列是否存在 +check_column_exists() { + local table=$1 + local column=$2 + if [ "$DB_TYPE" == "postgres" ]; then + result=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c \ + "SELECT EXISTS (SELECT FROM information_schema.columns WHERE table_name = '$table' AND column_name = '$column')" 2>/dev/null | tr -d ' ') + [ "$result" == "t" ] && return 0 || return 1 + else + result=$(mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" -D "$DB_NAME" -N -e \ + "SELECT COUNT(*) FROM information_schema.columns WHERE table_name = '$table' AND column_name = '$column'" 2>/dev/null) + [ "$result" -gt 0 ] && return 0 || return 1 + fi +} + +echo "--- 检查表是否存在 ---" +for table in "${REQUIRED_TABLES[@]}"; do + if check_table_exists "$table"; then + echo "[PASS] 表 $table 存在" + ((PASSED++)) + else + echo "[FAIL] 表 $table 不存在" + ((FAILED++)) + fi +done + +echo "" +echo "--- 检查 workflows 表核心字段 ---" +for column in "${WORKFLOWS_COLUMNS[@]}"; do + if check_column_exists "workflows" "$column"; then + echo "[PASS] workflows.$column 字段存在" + ((PASSED++)) + else + echo "[FAIL] workflows.$column 字段缺失" + ((FAILED++)) + fi +done + +echo "" +echo "--- 检查乐观锁字段 ---" +if check_column_exists "workflows" "version"; then + echo "[PASS] workflows.version 字段存在(乐观锁)" + ((PASSED++)) +else + echo "[FAIL] workflows.version 字段缺失(乐观锁必需)" + ((FAILED++)) +fi + +echo "" +echo "--- 检查外键约束 ---" +EXT_TABLES=("user_registration_ext" "user_management_ext" "project_detail_ext" "microservice_update_ext") +for table in "${EXT_TABLES[@]}"; do + if check_column_exists "$table" "workflow_id"; then + echo "[PASS] $table.workflow_id 字段存在" + ((PASSED++)) + else + echo "[FAIL] $table.workflow_id 字段缺失" + ((FAILED++)) + fi +done + +echo "" +echo "=== 验证结果 ===" +echo "通过: $PASSED" +echo "失败: $FAILED" + +if [ $FAILED -gt 0 ]; then + echo "" + echo "请运行数据库迁移修复缺失的表/字段" + exit 1 +else + echo "" + echo "所有 Schema 检查通过!" + exit 0 +fi diff --git a/1-AgentSkills/developing-work-procedure/scripts/verify-state-machine.sh b/1-AgentSkills/developing-work-procedure/scripts/verify-state-machine.sh new file mode 100644 index 0000000..cb5b2b3 --- /dev/null +++ b/1-AgentSkills/developing-work-procedure/scripts/verify-state-machine.sh @@ -0,0 +1,156 @@ +#!/bin/bash +# 验证状态机定义完整性 +# 依赖: go (用于解析 Go 源码) +# 用法: ./verify-state-machine.sh [项目根目录] + +set -e + +PROJECT_ROOT="${1:-.}" + +echo "=== 验证状态机定义 ===" +echo "项目目录: $PROJECT_ROOT" +echo "" + +# 基础状态列表 +BASE_STATES=( + "created" + "pending" + "assigned" + "in_progress" + "pending_review" + "returned" + "approved" + "rejected" + "revoked" + "closed" +) + +# 扩展状态列表 +EXTENDED_STATES=( + "executing" + "monitoring" + "rollbacked" + "draft_saved" +) + +# 终态列表 +TERMINAL_STATES=( + "approved" + "rejected" + "revoked" + "closed" +) + +# 事件列表 +EVENTS=( + "submit" + "auto_assign" + "assign" + "accept" + "reassign" + "complete" + "approve" + "reject" + "return" + "resubmit" + "revoke" + "close" +) + +FAILED=0 +PASSED=0 +WARNINGS=0 + +echo "--- 检查基础状态定义 ---" +for state in "${BASE_STATES[@]}"; do + if grep -rq "\"$state\"" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then + echo "[PASS] 状态 '$state' 已定义" + ((PASSED++)) + else + echo "[FAIL] 状态 '$state' 未定义" + ((FAILED++)) + fi +done + +echo "" +echo "--- 检查扩展状态定义 ---" +for state in "${EXTENDED_STATES[@]}"; do + if grep -rq "\"$state\"" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then + echo "[PASS] 扩展状态 '$state' 已定义" + ((PASSED++)) + else + echo "[WARN] 扩展状态 '$state' 未定义(可选)" + ((WARNINGS++)) + fi +done + +echo "" +echo "--- 检查事件定义 ---" +for event in "${EVENTS[@]}"; do + if grep -rq "\"$event\"" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then + echo "[PASS] 事件 '$event' 已定义" + ((PASSED++)) + else + echo "[FAIL] 事件 '$event' 未定义" + ((FAILED++)) + fi +done + +echo "" +echo "--- 检查终态保护 ---" +for state in "${TERMINAL_STATES[@]}"; do + # 检查是否有 TerminalStates 或 IsTerminal 相关定义 + if grep -rq "Terminal.*$state\|IsTerminal\|终态" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then + echo "[PASS] 终态 '$state' 有保护机制" + ((PASSED++)) + else + echo "[WARN] 终态 '$state' 可能缺少保护机制" + ((WARNINGS++)) + fi +done + +echo "" +echo "--- 检查撤销权限 ---" +if grep -rq "CanRevoke\|revoke.*Terminal\|IsTerminal" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then + echo "[PASS] 撤销权限检查已实现" + ((PASSED++)) +else + echo "[WARN] 未找到撤销权限检查逻辑" + ((WARNINGS++)) +fi + +echo "" +echo "--- 检查乐观锁实现 ---" +if grep -rq "version.*=.*version.*\+.*1\|version.*Expr\|RowsAffected.*==.*0" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then + echo "[PASS] 乐观锁机制已实现" + ((PASSED++)) +else + echo "[FAIL] 未找到乐观锁实现" + ((FAILED++)) +fi + +echo "" +echo "--- 检查状态历史记录 ---" +if grep -rq "WorkflowTrackHistory\|workflow_track_history\|TrackHistory" --include="*.go" "$PROJECT_ROOT" 2>/dev/null; then + echo "[PASS] 状态历史记录已实现" + ((PASSED++)) +else + echo "[FAIL] 未找到状态历史记录实现" + ((FAILED++)) +fi + +echo "" +echo "=== 验证结果 ===" +echo "通过: $PASSED" +echo "警告: $WARNINGS" +echo "失败: $FAILED" + +if [ $FAILED -gt 0 ]; then + echo "" + echo "请检查并修复失败项" + exit 1 +else + echo "" + echo "状态机验证通过!" + exit 0 +fi diff --git a/1-AgentSkills/implementing-deadman-switch/SKILL.md b/1-AgentSkills/implementing-deadman-switch/SKILL.md new file mode 100644 index 0000000..d901223 --- /dev/null +++ b/1-AgentSkills/implementing-deadman-switch/SKILL.md @@ -0,0 +1,146 @@ +--- +name: implementing-deadman-switch +description: Guides implementation of deadman switch (dead hand system) and heartbeat mechanism in watchdog-agent for authorization enforcement. Use when modifying heartbeat intervals, failure thresholds, or business process termination logic. Keywords: deadman, heartbeat, agent, authorization, sigterm, fail-count, self-destruct. +argument-hint: ": agent-heartbeat | fail-threshold | kill-logic | interval-config" +allowed-tools: + - Read + - Glob + - Grep + - Bash + - Edit + - Write +--- + +# Implementing Deadman Switch + +watchdog-agent 内置死手系统,当连续授权失败达到阈值时终止业务进程。 + +## 动态上下文注入 + +```bash +# 查找Agent心跳实现 +!`grep -rn "heartbeat\|Heartbeat" rmdc-watchdog-agent/` + +# 查找kill逻辑 +!`grep -n "SIGTERM\|Kill\|Signal" rmdc-watchdog-agent/` +``` + +## Plan + +根据 `$ARGUMENTS` 确定修改范围: + +| Component | 涉及文件 | 关键参数 | +|-----------|----------|----------| +| agent-heartbeat | agent心跳模块 | HeartbeatRequest/Response | +| fail-threshold | 失败计数逻辑 | maxRetryCount=12 | +| kill-logic | 进程终止逻辑 | SIGTERM信号 | +| interval-config | 心跳间隔配置 | 成功2h/失败1h | + +**产物清单**: +- Agent心跳循环实现 +- 失败计数与阈值判断 +- 业务进程终止逻辑 + +## Verify + +- [ ] 失败阈值:maxRetryCount = 12 +- [ ] 心跳间隔:成功后2小时,失败后1小时 +- [ ] TOTP验证:首次连接获取密钥,后续请求双向验证 +- [ ] 终止信号:使用SIGTERM(优雅终止),非SIGKILL +- [ ] 计数重置:授权成功后 failCount = 1(非0) +- [ ] 时间戳校验:|now - timestamp| < 5分钟 + +```bash +# 验证Agent编译 +!`cd rmdc-watchdog-agent && go build ./...` + +# 验证心跳逻辑 +!`cd rmdc-watchdog-agent && go test ./... -v -run TestHeartbeat` +``` + +## Execute + +### 心跳循环实现 + +```go +func (a *Agent) heartbeatLoop() { + failCount := 0 + + for { + resp, err := a.sendHeartbeat() + + if err != nil || !resp.Authorized { + failCount++ + + if failCount >= 12 { + a.killBusiness() + return + } + + time.Sleep(1 * time.Hour) // 失败后等待1小时 + } else { + failCount = 1 // 成功后重置为1 + time.Sleep(2 * time.Hour) // 成功后等待2小时 + } + } +} +``` + +### 业务终止实现 + +```go +func (a *Agent) killBusiness() { + log.Warn("deadman switch triggered, terminating business process") + a.businessProcess.Signal(syscall.SIGTERM) +} +``` + +### 首次连接处理 + +```go +func (a *Agent) sendHeartbeat() (*HeartbeatResponse, error) { + req := &HeartbeatRequest{ + HostInfo: a.hostInfo, + EnvInfo: a.envInfo, + Timestamp: time.Now().UnixMilli(), + TOTPCode: "", // 首次为空 + } + + // 非首次连接,生成TOTP + if a.tierTwoSecret != "" { + req.TOTPCode = totp.GenerateTierTwo(a.tierTwoSecret) + } + + resp, err := a.httpClient.Post(a.watchdogURL+"/api/heartbeat", req) + if err != nil { + return nil, err + } + + // 首次连接,保存密钥 + if resp.SecondTOTPSecret != "" { + a.tierTwoSecret = resp.SecondTOTPSecret + } + + // 验证服务端TOTP(双向验证) + if req.TOTPCode != "" && !totp.VerifyTierTwo(resp.TOTPCode, a.tierTwoSecret) { + return nil, errors.New("invalid server totp") + } + + return resp, nil +} +``` + +## Pitfalls + +1. **failCount初始值**:成功后设为1而非0,避免边界条件错误 +2. **SIGKILL误用**:应使用SIGTERM允许业务优雅退出 +3. **心跳阻塞**:sendHeartbeat需设置超时,避免网络问题导致卡死 +4. **双向验证遗漏**:必须验证服务端返回的TOTP +5. **首次连接特殊处理**:TOTPCode为空时获取密钥,不计入失败 +6. **间隔配置硬编码**:应支持配置化,便于不同项目调整 +7. **日志泄露**:禁止在日志中打印TOTP密钥 + +## Reference + +- [心跳参数配置](reference/heartbeat-params.md) +- [Agent生命周期](reference/agent-lifecycle.md) diff --git a/1-AgentSkills/implementing-deadman-switch/reference/agent-lifecycle.md b/1-AgentSkills/implementing-deadman-switch/reference/agent-lifecycle.md new file mode 100644 index 0000000..7482a81 --- /dev/null +++ b/1-AgentSkills/implementing-deadman-switch/reference/agent-lifecycle.md @@ -0,0 +1,92 @@ +# Agent 生命周期 + +## 启动流程 + +``` +1. Agent作为Sidecar随业务Pod启动 +2. 收集主机信息(MachineID, CPU, Memory, Serial, IP) +3. 收集环境信息(K8S_NAMESPACE, APPLICATION_NAME) +4. 首次心跳请求(TOTPCode="") +5. 获取并保存tier_two_secret +6. 进入心跳循环 +``` + +## 心跳状态机 + +``` +初始化 -> 首次心跳 -> 获取密钥 -> 心跳循环 + ↓ + 成功: failCount=1, 等待2h + 失败: failCount++, 等待1h + ↓ + failCount >= 12 + ↓ + 触发死手系统 -> 终止业务 +``` + +## 心跳循环详情 + +### 成功路径 +1. 生成Tier-Two TOTP (6位/30秒) +2. 发送心跳请求 +3. Watchdog验证TOTP +4. Watchdog检查授权状态 +5. 返回 {Authorized: true, TOTPCode: xxx} +6. Agent验证响应TOTP +7. failCount = 1 +8. 等待2小时 + +### 失败路径 +1. 发送心跳请求 +2. 返回错误或 {Authorized: false} +3. failCount++ +4. 检查 failCount >= 12 +5. 未达阈值: 等待1小时,继续循环 +6. 达到阈值: 触发死手系统 + +## 死手系统触发 + +```go +func (a *Agent) killBusiness() { + log.Warn("deadman switch triggered after %d failures", failCount) + + // 发送SIGTERM,允许优雅退出 + if err := a.businessProcess.Signal(syscall.SIGTERM); err != nil { + log.Error("failed to send SIGTERM: %v", err) + // 最后手段:SIGKILL + a.businessProcess.Signal(syscall.SIGKILL) + } +} +``` + +## 与Watchdog交互 + +### 请求结构 +```go +type HeartbeatRequest struct { + HostInfo HostInfo `json:"host_info"` + EnvInfo EnvInfo `json:"env_info"` + Timestamp int64 `json:"timestamp"` + TOTPCode string `json:"totp_code"` // 首次为空 +} +``` + +### 响应结构 +```go +type HeartbeatResponse struct { + Authorized bool `json:"authorized"` + TOTPCode string `json:"totp_code"` // 双向验证 + Timestamp int64 `json:"timestamp"` + SecondTOTPSecret string `json:"second_totp_secret"` // 首次返回 +} +``` + +## 异常处理 + +| 场景 | 处理方式 | +|------|----------| +| 网络超时 | 计入失败,继续循环 | +| Watchdog不可达 | 计入失败,继续循环 | +| TOTP验证失败 | 计入失败,继续循环 | +| 响应TOTP无效 | 计入失败,可能中间人攻击 | +| 授权状态false | 计入失败,等待授权恢复 | diff --git a/1-AgentSkills/implementing-deadman-switch/reference/heartbeat-params.md b/1-AgentSkills/implementing-deadman-switch/reference/heartbeat-params.md new file mode 100644 index 0000000..1f5c204 --- /dev/null +++ b/1-AgentSkills/implementing-deadman-switch/reference/heartbeat-params.md @@ -0,0 +1,54 @@ +# 心跳参数配置 + +## 核心参数 + +| 参数 | 值 | 说明 | +|------|------|------| +| maxRetryCount | 12 | 最大连续失败次数 | +| defaultHeartbeatInterval | 2小时 | 成功后心跳间隔 | +| failWaitInterval | 1小时 | 失败后等待间隔 | +| timestampTolerance | 5分钟 | 时间戳校验容忍度 | + +## 触发条件 + +死手系统触发条件:**连续12次心跳失败** + +### 时间计算 +- 最短触发时间:12次 × 1小时 = 12小时 +- 实际触发时间取决于网络和服务状态 + +### 失败场景 +1. 网络不可达 +2. Watchdog服务异常 +3. TOTP验证失败 +4. 授权状态为false +5. 响应TOTP校验失败 + +## 终止方式 + +| 属性 | 值 | +|------|-----| +| 信号 | SIGTERM (15) | +| 目的 | 允许业务进程优雅退出 | +| 后续 | 由K8S重启策略决定是否重启 | + +## 配置示例 + +```yaml +# agent配置 +heartbeat: + watchdog_url: "http://rmdc-watchdog:8990" + max_retry_count: 12 + success_interval: "2h" + fail_interval: "1h" + timestamp_tolerance: "5m" +``` + +## 环境变量 + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| WATCHDOG_URL | Watchdog地址 | http://rmdc-watchdog:8990 | +| MAX_RETRY_COUNT | 最大重试次数 | 12 | +| SUCCESS_INTERVAL | 成功后间隔 | 2h | +| FAIL_INTERVAL | 失败后间隔 | 1h | diff --git a/1-AgentSkills/implementing-deadman-switch/scripts/verify-deadman.sh b/1-AgentSkills/implementing-deadman-switch/scripts/verify-deadman.sh new file mode 100644 index 0000000..1697f21 --- /dev/null +++ b/1-AgentSkills/implementing-deadman-switch/scripts/verify-deadman.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# verify-deadman.sh - 死手系统实现验证脚本 +# 依赖: go 1.21+ +# 用法: ./verify-deadman.sh [agent_dir] + +set -e + +AGENT_DIR="${1:-./rmdc-watchdog-agent}" + +echo "=== 死手系统实现验证 ===" +echo "目标目录: $AGENT_DIR" +echo "" + +# 检查目录 +if [ ! -d "$AGENT_DIR" ]; then + echo "⚠ 目录不存在: $AGENT_DIR" + echo "尝试查找watchdog-agent目录..." + AGENT_DIR=$(find . -type d -name "*watchdog*agent*" 2>/dev/null | head -1) + if [ -z "$AGENT_DIR" ]; then + echo "✗ 未找到agent目录" + exit 1 + fi + echo "找到: $AGENT_DIR" +fi + +# 1. 验证心跳实现 +echo "" +echo "[1/5] 验证心跳实现..." +if grep -rq "heartbeat\|Heartbeat" "$AGENT_DIR" --include="*.go" 2>/dev/null; then + echo "✓ 找到心跳相关代码" + HEARTBEAT_FILES=$(grep -rl "heartbeat\|Heartbeat" "$AGENT_DIR" --include="*.go" 2>/dev/null | head -3) + echo " 文件: $HEARTBEAT_FILES" +else + echo "⚠ 未找到心跳相关代码" +fi + +# 2. 验证失败阈值 +echo "" +echo "[2/5] 验证失败阈值 (maxRetryCount=12)..." +if grep -rq "12" "$AGENT_DIR" --include="*.go" 2>/dev/null | grep -qi "retry\|count\|max\|fail"; then + echo "✓ 找到阈值配置" +else + echo "⚠ 未明确找到阈值12的配置" +fi + +# 3. 验证SIGTERM信号 +echo "" +echo "[3/5] 验证终止信号 (SIGTERM)..." +if grep -rq "SIGTERM\|syscall.Signal" "$AGENT_DIR" --include="*.go" 2>/dev/null; then + echo "✓ 找到SIGTERM信号使用" +else + echo "⚠ 未找到SIGTERM信号" +fi + +if grep -rq "SIGKILL" "$AGENT_DIR" --include="*.go" 2>/dev/null; then + echo "⚠ 发现SIGKILL使用,请确认仅作为最后手段" +fi + +# 4. 验证心跳间隔 +echo "" +echo "[4/5] 验证心跳间隔..." +if grep -rq "2.*[Hh]our\|time.Hour.*2\|7200" "$AGENT_DIR" --include="*.go" 2>/dev/null; then + echo "✓ 找到2小时间隔配置" +else + echo "⚠ 未找到2小时成功间隔" +fi + +if grep -rq "1.*[Hh]our\|time.Hour\|3600" "$AGENT_DIR" --include="*.go" 2>/dev/null; then + echo "✓ 找到1小时间隔配置" +else + echo "⚠ 未找到1小时失败间隔" +fi + +# 5. 验证TOTP双向验证 +echo "" +echo "[5/5] 验证TOTP双向验证..." +if grep -rq "SecondTOTPSecret\|second_totp_secret" "$AGENT_DIR" --include="*.go" 2>/dev/null; then + echo "✓ 找到密钥获取逻辑" +else + echo "⚠ 未找到密钥获取逻辑" +fi + +if grep -rq "response.*[Tt]otp\|verify.*[Tt]otp" "$AGENT_DIR" --include="*.go" 2>/dev/null; then + echo "✓ 找到响应TOTP验证" +else + echo "⚠ 未找到响应TOTP验证,请确认双向验证" +fi + +# 编译检查 +echo "" +echo "[额外] 编译检查..." +cd "$AGENT_DIR" +if go build ./... 2>&1; then + echo "✓ 编译通过" +else + echo "⚠ 编译有问题" +fi + +echo "" +echo "=== 验证完成 ===" diff --git a/1-AgentSkills/implementing-k8s-ops/SKILL.md b/1-AgentSkills/implementing-k8s-ops/SKILL.md new file mode 100644 index 0000000..2b58a5c --- /dev/null +++ b/1-AgentSkills/implementing-k8s-ops/SKILL.md @@ -0,0 +1,112 @@ +--- +name: implementing-k8s-ops +description: Guides implementation of K8S operation proxy in rmdc-watchdog for executing Kubernetes API calls including logs, exec, scale, restart, delete, get, and apply actions. Use when adding new K8S actions or modifying execution logic. Keywords: kubernetes, k8s, operator, logs, exec, scale, restart, deployment, pod. +argument-hint: ": logs | exec | scale | restart | delete | get | apply | new-action" +allowed-tools: + - Read + - Glob + - Grep + - Bash + - Edit + - Write +--- + +# Implementing K8S Operations + +rmdc-watchdog 作为K8S操作代理,执行来自 exchange-hub 下发的K8S指令。 + +## 动态上下文注入 + +```bash +# 查看K8S客户端实现 +!`cat rmdc-watchdog/pkg/k8s/client.go` + +# 查找现有action处理 +!`grep -n "case \"" rmdc-watchdog/internal/service/k8s_service.go` +``` + +## Plan + +根据 `$ARGUMENTS` 确定操作类型: + +| Action | 目标资源 | 关键参数 | +|--------|----------|----------| +| logs | Pod | container, tail_lines, follow | +| exec | Pod | container, command[], timeout | +| scale | Deployment/StatefulSet | scale_count | +| restart | Deployment/StatefulSet | - | +| delete | 任意资源 | - | +| get | 任意资源 | output_format | +| apply | 任意资源 | yaml_content | + +**产物清单**: +- `pkg/k8s/client.go` - K8S API调用封装 +- `internal/service/k8s_service.go` - K8S服务逻辑 +- `internal/handler/k8s_handler.go` - K8S请求处理 + +**决策点**: +1. 新action是否需要额外参数?→ 更新 K8sExecCommand 结构 +2. 是否涉及敏感操作?→ 需添加审计日志 +3. 是否需要超时控制?→ 使用 context.WithTimeout + +## Verify + +- [ ] 操作白名单:仅允许 logs/exec/scale/restart/delete/get/apply +- [ ] 超时处理:所有K8S API调用必须设置timeout +- [ ] 结果格式:ExecResult包含command_id, status, exit_code, output, error, duration +- [ ] 日志截断:tail_lines限制,避免大日志阻塞 +- [ ] 权限边界:仅操作项目namespace内资源 +- [ ] 执行上报:结果通过MQTT `wdd/RDMC/message/up` 上报 + +```bash +# 验证K8S客户端 +!`cd rmdc-watchdog && go test ./pkg/k8s/... -v` + +# 验证K8S服务 +!`cd rmdc-watchdog && go test ./internal/service/... -v -run TestK8s` +``` + +## Execute + +### 添加新K8S操作 + +1. **扩展K8S Client** +```go +// pkg/k8s/client.go +func (c *Client) NewAction(namespace, name string, params Params) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(params.Timeout)*time.Second) + defer cancel() + // K8S API调用 +} +``` + +2. **添加Service分支** +```go +// internal/service/k8s_service.go +case "new-action": + output, err = s.k8sClient.NewAction(cmd.Namespace, cmd.Name, params) +``` + +3. **更新指令结构(如需)** +```go +type K8sExecCommand struct { + // 新增字段 + NewParam string `json:"new_param,omitempty"` +} +``` + +4. **同步exchange-hub指令定义** + +## Pitfalls + +1. **Namespace逃逸**:必须校验操作仅限项目namespace +2. **超时未设置**:K8S API调用卡住会阻塞整个handler +3. **大日志OOM**:logs操作未设置tail_lines导致内存溢出 +4. **exec命令注入**:command[]需过滤危险命令 +5. **follow日志未清理**:流式日志需session管理,用户停止时清理 +6. **结果丢失**:执行完成必须通过MQTT上报,失败重试 + +## Reference + +- [K8S操作类型](reference/k8s-actions.md) +- [指令结构定义](reference/command-structure.md) diff --git a/1-AgentSkills/implementing-k8s-ops/reference/command-structure.md b/1-AgentSkills/implementing-k8s-ops/reference/command-structure.md new file mode 100644 index 0000000..23a0217 --- /dev/null +++ b/1-AgentSkills/implementing-k8s-ops/reference/command-structure.md @@ -0,0 +1,74 @@ +# K8S 指令结构定义 + +## K8sExecCommand - 执行指令 + +```go +type K8sExecCommand struct { + CommandID string `json:"command_id"` // 唯一指令ID + Namespace string `json:"namespace"` // 目标namespace + Resource string `json:"resource"` // 资源类型: deployment, pod, statefulset... + Name string `json:"name"` // 资源名称 + Action string `json:"action"` // 操作: logs, exec, scale, restart, delete, get, apply + Container string `json:"container,omitempty"` // 容器名(logs/exec用) + Command []string `json:"command,omitempty"` // 命令(exec用) + Timeout int `json:"timeout"` // 超时秒数 + TailLines int `json:"tail_lines,omitempty"` // 日志行数(logs用) + FollowLogs bool `json:"follow_logs,omitempty"`// 实时日志(logs用) + Scale int `json:"scale,omitempty"` // 副本数(scale用) + YamlContent string `json:"yaml_content,omitempty"`// YAML内容(apply用) +} +``` + +## ExecResult - 执行结果 + +```go +type ExecResult struct { + CommandID string `json:"command_id"` // 对应指令ID + Status string `json:"status"` // success, failure, timeout + ExitCode int `json:"exit_code"` // 退出码 + Output string `json:"output"` // 标准输出 + Error string `json:"error"` // 错误信息 + StartTime int64 `json:"start_time"` // 开始时间戳(ms) + EndTime int64 `json:"end_time"` // 结束时间戳(ms) + Duration int64 `json:"duration"` // 执行时长(ms) +} +``` + +## MQTT消息封装 + +### 下发指令 (Exchange-Hub → Watchdog) +```json +{ + "message_id": "uuid", + "type": "command", + "project_id": "project_12345678", + "timestamp": 1700000000000, + "data_type": "k8s_exec", + "payload": { + "command_id": "cmd_uuid", + "namespace": "my-namespace", + "resource": "pod", + "name": "my-pod", + "action": "logs", + "tail_lines": 100 + } +} +``` + +### 上报结果 (Watchdog → Exchange-Hub) +```json +{ + "message_id": "uuid", + "type": "message", + "project_id": "project_12345678", + "timestamp": 1700000001000, + "data_type": "exec_result", + "payload": { + "command_id": "cmd_uuid", + "status": "success", + "exit_code": 0, + "output": "log content...", + "duration": 1500 + } +} +``` diff --git a/1-AgentSkills/implementing-k8s-ops/reference/k8s-actions.md b/1-AgentSkills/implementing-k8s-ops/reference/k8s-actions.md new file mode 100644 index 0000000..25cbfe4 --- /dev/null +++ b/1-AgentSkills/implementing-k8s-ops/reference/k8s-actions.md @@ -0,0 +1,69 @@ +# K8S 操作类型定义 + +## 支持的操作 + +| Action | 说明 | 目标资源 | 参数 | +|--------|------|----------|------| +| `logs` | 获取日志 | Pod | container, tail_lines, follow | +| `exec` | 执行命令 | Pod | container, command[], timeout | +| `scale` | 扩缩容 | Deployment/StatefulSet | scale_count | +| `restart` | 滚动重启 | Deployment/StatefulSet | - | +| `delete` | 删除资源 | Pod/Deployment/Service等 | - | +| `get` | 获取信息 | 任意资源 | output_format | +| `apply` | 应用配置 | 任意资源 | yaml_content | + +## 操作详情 + +### logs - 获取日志 +```go +type LogsParams struct { + Container string `json:"container"` + TailLines int `json:"tail_lines"` // 默认100,最大1000 + Follow bool `json:"follow"` // 是否实时跟踪 +} +``` + +### exec - 执行命令 +```go +type ExecParams struct { + Container string `json:"container"` + Command []string `json:"command"` + Timeout int `json:"timeout"` // 秒 +} +``` +**安全约束**:禁止执行 rm -rf、shutdown、reboot 等危险命令 + +### scale - 扩缩容 +```go +type ScaleParams struct { + Replicas int `json:"replicas"` // 目标副本数 +} +``` + +### restart - 滚动重启 +无额外参数,使用 kubectl rollout restart 等效操作 + +### delete - 删除资源 +无额外参数,需确认资源类型和名称 + +### get - 获取信息 +```go +type GetParams struct { + OutputFormat string `json:"output_format"` // json, yaml, wide +} +``` + +### apply - 应用配置 +```go +type ApplyParams struct { + YamlContent string `json:"yaml_content"` // YAML内容 +} +``` + +## 安全约束 + +1. **Namespace限制**:仅操作项目namespace内资源 +2. **操作白名单**:仅允许上述7种操作 +3. **命令过滤**:exec操作过滤危险命令 +4. **日志限制**:tail_lines最大1000行 +5. **超时控制**:所有操作必须设置timeout diff --git a/1-AgentSkills/implementing-k8s-ops/scripts/verify-k8s-ops.sh b/1-AgentSkills/implementing-k8s-ops/scripts/verify-k8s-ops.sh new file mode 100644 index 0000000..81cb86e --- /dev/null +++ b/1-AgentSkills/implementing-k8s-ops/scripts/verify-k8s-ops.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# verify-k8s-ops.sh - K8S操作实现验证脚本 +# 依赖: go 1.21+ +# 用法: ./verify-k8s-ops.sh [watchdog_dir] + +set -e + +WATCHDOG_DIR="${1:-./rmdc-watchdog}" + +echo "=== K8S 操作实现验证 ===" +echo "目标目录: $WATCHDOG_DIR" +echo "" + +# 1. 检查K8S客户端文件 +echo "[1/4] 检查K8S客户端..." +K8S_CLIENT="$WATCHDOG_DIR/pkg/k8s/client.go" +if [ -f "$K8S_CLIENT" ]; then + echo "✓ K8S客户端文件存在: $K8S_CLIENT" +else + echo "✗ K8S客户端文件不存在" +fi + +# 2. 验证操作白名单 +echo "" +echo "[2/4] 验证K8S操作白名单..." +K8S_SERVICE="$WATCHDOG_DIR/internal/service/k8s_service.go" +ALLOWED_ACTIONS="logs exec scale restart delete get apply" + +if [ -f "$K8S_SERVICE" ]; then + echo "检查 $K8S_SERVICE:" + for action in $ALLOWED_ACTIONS; do + if grep -q "case \"$action\"" "$K8S_SERVICE" 2>/dev/null; then + echo " ✓ '$action' 已实现" + else + echo " ⚠ '$action' 未找到" + fi + done +else + echo "⚠ K8S服务文件不存在: $K8S_SERVICE" +fi + +# 3. 验证超时处理 +echo "" +echo "[3/4] 验证超时处理..." +if grep -rq "context.WithTimeout\|WithDeadline" "$WATCHDOG_DIR/pkg/k8s/" 2>/dev/null; then + echo "✓ 找到超时控制实现" +else + echo "⚠ 未找到超时控制,请确认K8S调用设置了timeout" +fi + +# 4. 验证结果结构 +echo "" +echo "[4/4] 验证ExecResult结构..." +if grep -rq "ExecResult" "$WATCHDOG_DIR" --include="*.go" 2>/dev/null; then + echo "✓ 找到ExecResult定义" + + # 检查必要字段 + REQUIRED_FIELDS="CommandID Status ExitCode Output Duration" + for field in $REQUIRED_FIELDS; do + if grep -rq "$field" "$WATCHDOG_DIR" --include="*.go" 2>/dev/null | grep -q "ExecResult\|exec_result"; then + echo " ✓ 字段 '$field' 存在" + else + echo " ⚠ 字段 '$field' 未找到" + fi + done +else + echo "⚠ 未找到ExecResult定义" +fi + +# 5. 运行测试 +echo "" +echo "[5/5] 运行K8S相关测试..." +cd "$WATCHDOG_DIR" +if go test ./pkg/k8s/... -v 2>&1 | head -20; then + echo "✓ K8S测试执行完成" +else + echo "⚠ K8S测试执行有问题" +fi + +echo "" +echo "=== 验证完成 ===" diff --git a/1-AgentSkills/implementing-totp-auth/SKILL.md b/1-AgentSkills/implementing-totp-auth/SKILL.md new file mode 100644 index 0000000..1ba47ae --- /dev/null +++ b/1-AgentSkills/implementing-totp-auth/SKILL.md @@ -0,0 +1,108 @@ +--- +name: implementing-totp-auth +description: Guides implementation of TOTP-based two-tier authorization mechanism for RMDC system. Use when implementing authorization flows, modifying TOTP parameters, or debugging auth failures between project-management, watchdog, agent, and node components. Keywords: totp, authorization, tier-one, tier-two, security, authentication, hmac. +argument-hint: ": tier-one | tier-two | dual-verify | auth-file" +allowed-tools: + - Read + - Glob + - Grep + - Bash + - Edit + - Write +--- + +# Implementing TOTP Authorization + +RMDC采用双层TOTP授权机制保护项目环境安全。 + +## 动态上下文注入 + +```bash +# 查找TOTP实现 +!`grep -rn "TOTP\|totp" rmdc-watchdog/pkg/totp/` + +# 查找授权验证逻辑 +!`grep -n "Verify.*TOTP\|Generate.*TOTP" rmdc-watchdog/internal/service/auth_service.go` +``` + +## Plan + +根据 `$ARGUMENTS` 确定实现范围: + +| Scope | 涉及组件 | 关键参数 | +|-------|----------|----------| +| tier-one | project-management ↔ watchdog | 8位/30分钟/SHA256 | +| tier-two | watchdog ↔ agent/node | 6位/30秒/SHA1 | +| dual-verify | 所有通信 | 请求TOTP + 响应TOTP | +| auth-file | watchdog授权申请/解析 | 加密主机信息 + TOTP + namespace | + +**产物清单**: +- `pkg/totp/totp.go` - TOTP生成与验证 +- `internal/service/auth_service.go` - 授权服务 +- `internal/model/entity/auth_info.go` - 授权信息实体 + +## Verify + +- [ ] Tier-One参数:8位码、30分钟有效、SHA256、AES-GCM加密 +- [ ] Tier-Two参数:6位码、30秒有效、SHA1 +- [ ] 双向验证:服务端响应必须包含TOTP供客户端校验 +- [ ] 时间戳校验:|now - timestamp| < 5分钟(可配置) +- [ ] 密钥存储:tier_one_secret/tier_two_secret不通过公网传输 +- [ ] 授权文件包含:EncryptedHostMap, TOTPCode, EncryptedNamespace + +```bash +# 验证TOTP生成 +!`cd rmdc-watchdog && go test ./pkg/totp/... -v -run TestGenerate` + +# 验证授权流程 +!`cd rmdc-watchdog && go test ./internal/service/... -v -run TestAuth` +``` + +## Execute + +### 实现Tier-One TOTP + +```go +// 8位,30分钟有效,SHA256 +func GenerateTierOneTOTP(secret string) string { + return totp.Generate(secret, totp.Config{ + Digits: 8, + Period: 1800, // 30分钟 + Algorithm: crypto.SHA256, + }) +} +``` + +### 实现Tier-Two TOTP + +```go +// 6位,30秒有效,SHA1 +func GenerateTierTwoTOTP(secret string) string { + return totp.Generate(secret, totp.Config{ + Digits: 6, + Period: 30, + Algorithm: crypto.SHA1, + }) +} +``` + +### 实现双向验证 + +1. 客户端生成TOTP并发送请求 +2. 服务端验证客户端TOTP +3. 服务端生成新TOTP并返回 +4. 客户端验证服务端TOTP + +## Pitfalls + +1. **算法混淆**:Tier-One用SHA256,Tier-Two用SHA1,不可互换 +2. **有效期错配**:30分钟 vs 30秒,单位差异大 +3. **时间同步问题**:需配置 `time_offset_allowed` 容忍时钟偏差 +4. **单向验证漏洞**:缺少响应TOTP会导致中间人攻击风险 +5. **密钥泄露**:禁止在日志/错误信息中打印secret +6. **首次连接处理**:TOTPCode为空时需返回密钥,后续请求必须验证 + +## Reference + +- [TOTP层级说明](reference/totp-tiers.md) +- [授权流程图](reference/auth-flow.md) diff --git a/1-AgentSkills/implementing-totp-auth/reference/auth-flow.md b/1-AgentSkills/implementing-totp-auth/reference/auth-flow.md new file mode 100644 index 0000000..693f290 --- /dev/null +++ b/1-AgentSkills/implementing-totp-auth/reference/auth-flow.md @@ -0,0 +1,72 @@ +# 授权流程图 + +## 项目注册流程 + +``` +1. 管理员在RMDC Portal创建项目 +2. project-management生成: + - project_id = namespace_<8位随机数> + - tier_one_secret + - tier_two_secret + - time_offset_allowed +3. 返回项目配置文件给管理员 +4. 管理员部署watchdog时使用配置文件(密钥不经公网) +``` + +## Watchdog注册流程 + +``` +1. Watchdog连接MQTT Broker +2. 订阅: wdd/RDMC/command/down/ +3. 订阅: wdd/RDMC/message/down/ +4. 生成Tier-One TOTP验证码(8位,30分钟) +5. 发布注册Command到 wdd/RDMC/command/up +6. Exchange-Hub验证TOTP +7. 生成32位随机挑战码 +8. 返回register_ack + 服务端TOTP +9. Watchdog验证服务端TOTP +10. 回复挑战码确认 +11. 注册完成,状态变为Online +``` + +## 授权申请流程 + +``` +1. Node上报主机信息 → Watchdog +2. Agent首次心跳 → Watchdog +3. Watchdog收集完成后生成授权文件: + - EncryptedHostMap + - TOTPCode(8位,30分钟) + - EncryptedNamespace +4. 发布auth_request到MQTT +5. Center验证TOTP和主机信息 +6. 返回授权码(加密) +7. Watchdog解密并持久化 +``` + +## Agent心跳流程 + +``` +首次连接: +1. Agent收集主机信息 +2. 发送心跳(TOTPCode="") +3. Watchdog返回SecondTOTPSecret +4. Agent保存密钥 + +后续心跳: +1. Agent生成Tier-Two TOTP(6位,30秒) +2. 发送心跳 +3. Watchdog验证TOTP +4. 检查主机授权状态 +5. 返回{Authorized, ResponseTOTP} +6. Agent验证ResponseTOTP +7. 根据结果更新failCount +``` + +## 死手系统触发 + +``` +failCount >= 12 时: +1. Agent发送SIGTERM信号 +2. 业务进程优雅终止 +``` diff --git a/1-AgentSkills/implementing-totp-auth/reference/totp-tiers.md b/1-AgentSkills/implementing-totp-auth/reference/totp-tiers.md new file mode 100644 index 0000000..a705d19 --- /dev/null +++ b/1-AgentSkills/implementing-totp-auth/reference/totp-tiers.md @@ -0,0 +1,53 @@ +# TOTP 层级定义 + +## Tier-One(一级授权) + +| 属性 | 值 | +|------|-----| +| 通信方 | project-management ↔ watchdog | +| 位数 | 8位 | +| 有效期 | 30分钟 (1800秒) | +| 算法 | SHA256 | +| 加密 | AES-256-GCM | +| 用途 | 项目注册、授权申请/下发 | + +## Tier-Two(二级授权) + +| 属性 | 值 | +|------|-----| +| 通信方 | watchdog ↔ agent/node | +| 位数 | 6位 | +| 有效期 | 30秒 | +| 算法 | SHA1 | +| 加密 | 无(内网通信) | +| 用途 | 心跳验证、指令执行 | + +## 双向验证流程 + +``` +1. 客户端生成TOTP → 发送请求 +2. 服务端验证客户端TOTP → 生成新TOTP返回 +3. 客户端验证服务端TOTP → 确认双方身份 +``` + +## 密钥管理 + +### 生成时机 +- tier_one_secret: project-management创建项目时生成 +- tier_two_secret: project-management创建项目时生成 + +### 传输方式 +- 通过项目配置文件离线部署到Watchdog +- **禁止通过MQTT公网传输** + +### 存储位置 +- project-management: 数据库加密存储 +- watchdog: 配置文件/环境变量 +- agent/node: 首次连接时从watchdog获取tier_two_secret + +## 时间容忍度 + +| 配置项 | 默认值 | 说明 | +|--------|--------|------| +| time_offset_allowed | 300秒 | 时间戳校验容忍度 | +| totp_window | 1 | TOTP验证窗口(前后各1个周期) | diff --git a/1-AgentSkills/implementing-totp-auth/scripts/verify-totp-impl.sh b/1-AgentSkills/implementing-totp-auth/scripts/verify-totp-impl.sh new file mode 100644 index 0000000..d218e73 --- /dev/null +++ b/1-AgentSkills/implementing-totp-auth/scripts/verify-totp-impl.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# verify-totp-impl.sh - TOTP实现验证脚本 +# 依赖: go 1.21+ +# 用法: ./verify-totp-impl.sh [project_dir] + +set -e + +PROJECT_DIR="${1:-.}" + +echo "=== TOTP 实现验证 ===" +echo "项目目录: $PROJECT_DIR" +echo "" + +# 1. 查找TOTP实现文件 +echo "[1/4] 查找TOTP实现..." +TOTP_FILES=$(find "$PROJECT_DIR" -name "*.go" -exec grep -l "totp\|TOTP" {} \; 2>/dev/null || true) +if [ -n "$TOTP_FILES" ]; then + echo "找到TOTP相关文件:" + echo "$TOTP_FILES" | head -10 + echo "" +else + echo "⚠ 未找到TOTP相关文件" +fi + +# 2. 验证Tier-One参数 +echo "[2/4] 验证Tier-One参数 (8位/30分钟/SHA256)..." +TIER_ONE_OK=true + +if grep -rq "8" "$PROJECT_DIR" --include="*.go" 2>/dev/null | grep -qi "digit\|Digit"; then + echo "✓ 找到8位配置" +else + echo "⚠ 未明确找到8位配置" + TIER_ONE_OK=false +fi + +if grep -rq "1800\|30.*[Mm]in" "$PROJECT_DIR" --include="*.go" 2>/dev/null; then + echo "✓ 找到30分钟有效期配置" +else + echo "⚠ 未找到30分钟有效期配置" + TIER_ONE_OK=false +fi + +if grep -rq "SHA256\|sha256" "$PROJECT_DIR" --include="*.go" 2>/dev/null; then + echo "✓ 找到SHA256算法" +else + echo "⚠ 未找到SHA256算法" + TIER_ONE_OK=false +fi + +# 3. 验证Tier-Two参数 +echo "" +echo "[3/4] 验证Tier-Two参数 (6位/30秒/SHA1)..." +TIER_TWO_OK=true + +if grep -rq "6" "$PROJECT_DIR" --include="*.go" 2>/dev/null | grep -qi "digit\|Digit"; then + echo "✓ 找到6位配置" +else + echo "⚠ 未明确找到6位配置" + TIER_TWO_OK=false +fi + +if grep -rq "30" "$PROJECT_DIR" --include="*.go" 2>/dev/null | grep -qi "period\|second\|Period\|Second"; then + echo "✓ 找到30秒有效期配置" +else + echo "⚠ 未找到30秒有效期配置" + TIER_TWO_OK=false +fi + +if grep -rq "SHA1\|sha1" "$PROJECT_DIR" --include="*.go" 2>/dev/null; then + echo "✓ 找到SHA1算法" +else + echo "⚠ 未找到SHA1算法" + TIER_TWO_OK=false +fi + +# 4. 验证双向验证 +echo "" +echo "[4/4] 验证双向验证实现..." +if grep -rq "response.*[Tt]otp\|[Tt]otp.*response" "$PROJECT_DIR" --include="*.go" 2>/dev/null; then + echo "✓ 找到响应TOTP相关代码" +else + echo "⚠ 未找到响应TOTP实现,请确认双向验证" +fi + +# 总结 +echo "" +echo "=== 验证总结 ===" +if [ "$TIER_ONE_OK" = true ] && [ "$TIER_TWO_OK" = true ]; then + echo "✓ TOTP参数配置基本正确" +else + echo "⚠ 部分TOTP参数需要确认" +fi diff --git a/1-Golang项目/3-api-development-prompt.md b/1-Golang项目/3-api-development-prompt.md new file mode 100644 index 0000000..c2bba03 --- /dev/null +++ b/1-Golang项目/3-api-development-prompt.md @@ -0,0 +1,332 @@ +# RMDC API开发规范提示词 + +--- + +## 设计原则 + +### 1. 使用POST + RequestBody + +> **核心规范**: 所有API优先使用POST方法,参数通过RequestBody传递 + +```go +// ✅ 推荐方式 +POST /api/jenkins/builds/list +{ + "organization_folder": "Backend", + "repository_name": "cmii-fly-center", + "branch_name": "master", + "page": 1, + "page_size": 10 +} + +// ❌ 避免使用 +GET /api/jenkins/organizations/{org}/repositories/{repo}/branches/{branch}/builds?page=1&page_size=10 +``` + +### 2. 避免PathVariables + +```go +// ❌ 不推荐 +GET /api/projects/{project_id} +GET /api/builds/{build_id}/console + +// ✅ 推荐 +POST /api/projects/detail +{ + "project_id": "namespace_abc12345" +} + +POST /api/builds/console +{ + "organization_folder": "Backend", + "repository_name": "cmii-fly-center", + "branch_name": "master", + "build_number": 123 +} +``` + +### 3. 避免RequestParams + +```go +// ❌ 不推荐 +GET /api/users/list?role=admin&status=active&page=1 + +// ✅ 推荐 +POST /api/users/list +{ + "role": "admin", + "status": "active", + "page": 1, + "page_size": 20 +} +``` + +--- + +## 统一响应格式 + +### 成功响应 + +```json +{ + "code": 0, + "message": "success", + "data": { + // 业务数据 + } +} +``` + +### 分页响应 + +```json +{ + "code": 0, + "message": "success", + "data": { + "list": [...], + "total": 100, + "page": 1, + "page_size": 20 + } +} +``` + +### 错误响应 + +```json +{ + "code": 1001, + "message": "参数错误: organization_folder不能为空", + "data": null +} +``` + +--- + +## 请求结构规范 + +### 通用分页请求 + +```go +type PageRequest struct { + Page int `json:"page" binding:"required,min=1"` + PageSize int `json:"page_size" binding:"required,min=1,max=100"` +} +``` + +### 通用筛选请求 + +```go +type ListRequest struct { + PageRequest + Keyword string `json:"keyword,omitempty"` // 搜索关键词 + Status string `json:"status,omitempty"` // 状态筛选 + SortBy string `json:"sort_by,omitempty"` // 排序字段 + SortOrder string `json:"sort_order,omitempty"` // asc/desc +} +``` + +--- + +## API命名规范 + +### 操作类型后缀 + +| 操作 | 后缀 | 示例 | +|------|------|------| +| 列表查询 | `/list` | `/api/projects/list` | +| 详情查询 | `/detail` | `/api/projects/detail` | +| 创建 | `/create` | `/api/projects/create` | +| 更新 | `/update` | `/api/projects/update` | +| 删除 | `/delete` | `/api/projects/delete` | +| 同步 | `/sync` | `/api/jenkins/organizations/sync` | +| 触发 | `/trigger` | `/api/builds/trigger` | +| 导出 | `/export` | `/api/projects/export` | + +### 模块前缀 + +| 模块 | 前缀 | +|------|------| +| Jenkins | `/api/jenkins/` | +| 项目管理 | `/api/projects/` | +| 用户 | `/api/users/` | +| 权限 | `/api/permissions/` | +| 审计 | `/api/audit/` | +| Exchange-Hub | `/api/exchange-hub/` | +| DCU | `/api/dcu/` | + +--- + +## Handler实现模板 + +```go +// ListBuilds 获取构建列表 +// @Summary 获取构建列表 +// @Tags 构建管理 +// @Accept json +// @Produce json +// @Param request body dto.ListBuildsRequest true "请求参数" +// @Success 200 {object} response.Response{data=dto.ListBuildsResponse} +// @Router /api/jenkins/builds/list [post] +func (h *BuildHandler) ListBuilds(c *gin.Context) { + var req dto.ListBuildsRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.ParamError(c, err) + return + } + + resp, err := h.buildService.ListBuilds(c.Request.Context(), &req) + if err != nil { + response.Error(c, err) + return + } + + response.Success(c, resp) +} +``` + +--- + +## DTO设计规范 + +### 请求DTO命名 + +```go +// 列表请求: List{资源}Request +type ListBuildsRequest struct { + PageRequest + OrganizationFolder string `json:"organization_folder" binding:"required"` + RepositoryName string `json:"repository_name" binding:"required"` + BranchName string `json:"branch_name,omitempty"` +} + +// 详情请求: Get{资源}Request 或 {资源}DetailRequest +type GetBuildRequest struct { + OrganizationFolder string `json:"organization_folder" binding:"required"` + RepositoryName string `json:"repository_name" binding:"required"` + BranchName string `json:"branch_name" binding:"required"` + BuildNumber int `json:"build_number" binding:"required"` +} + +// 创建请求: Create{资源}Request +type CreateProjectRequest struct { + Name string `json:"name" binding:"required"` + Namespace string `json:"namespace" binding:"required"` + Province string `json:"province" binding:"required"` + City string `json:"city" binding:"required"` +} + +// 更新请求: Update{资源}Request +type UpdateProjectRequest struct { + ProjectID string `json:"project_id" binding:"required"` + Name string `json:"name,omitempty"` + Province string `json:"province,omitempty"` + City string `json:"city,omitempty"` +} + +// 删除请求: Delete{资源}Request +type DeleteProjectRequest struct { + ProjectID string `json:"project_id" binding:"required"` +} +``` + +### 响应DTO命名 + +```go +// 列表响应: List{资源}Response +type ListBuildsResponse struct { + List []*BuildDTO `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +// 详情响应: {资源}DetailResponse 或直接使用 {资源}DTO +type BuildDetailResponse struct { + *BuildDTO + ConsoleOutput string `json:"console_output,omitempty"` +} +``` + +--- + +## 错误码规范 + +### 错误码范围 + +| 范围 | 模块 | +|------|------| +| 1000-1999 | 通用错误 | +| 2000-2999 | 用户/权限 | +| 3000-3999 | Jenkins模块 | +| 4000-4999 | 项目管理 | +| 5000-5999 | Exchange-Hub | +| 6000-6999 | Watchdog | + +### 通用错误码 + +| 错误码 | 说明 | +|--------|------| +| 0 | 成功 | +| 1001 | 参数错误 | +| 1002 | 未授权 | +| 1003 | 禁止访问 | +| 1004 | 资源不存在 | +| 1005 | 内部错误 | + +--- + +## 前端调用示例 + +```typescript +// api/modules/jenkins.ts +export const jenkinsApi = { + // 获取构建列表 + listBuilds: (data: ListBuildsRequest) => + request.post('/api/jenkins/builds/list', data), + + // 触发构建 + triggerBuild: (data: TriggerBuildRequest) => + request.post('/api/jenkins/builds/trigger', data), + + // 获取构建详情 + getBuildDetail: (data: GetBuildRequest) => + request.post('/api/jenkins/builds/detail', data), +}; +``` + +--- + +## 安全规范 + +### 1. 敏感字段不出现在URL + +```go +// ❌ 敏感信息泄露到URL +GET /api/auth/login?username=admin&password=123456 + +// ✅ 使用RequestBody +POST /api/auth/login +{ + "username": "admin", + "password": "123456" +} +``` + +### 2. 必须验证请求体 + +```go +func (h *Handler) CreateProject(c *gin.Context) { + var req dto.CreateProjectRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.ParamError(c, err) + return + } + // 后续处理... +} +``` + +### 3. 审计敏感操作 + +所有写操作需通过审计中间件记录。 diff --git a/1-Golang项目/go-gin-gorm-style.md b/1-Golang项目/go-gin-gorm-style.md index 87cc44a..f8a6b22 100644 --- a/1-Golang项目/go-gin-gorm-style.md +++ b/1-Golang项目/go-gin-gorm-style.md @@ -1,43 +1,43 @@ -`` **1. 核心角色与指令 (Core Persona & Directive)** -你将扮演一个拥有10年以上经验的Golang后端架构师。你不仅精通Golang语言特性(特别是并发模型和标准库),更是GIN、GORM等主流框架的资深用户。你的代码风格严谨、可读性强,并且极度重视项目结构、模块化、日志规范和文档注释。 +你将扮演一个拥有10年以上经验的Golang后端架构师。你不仅精通Golang语言特性(特别是并发模型和标准库),更是GIN、GORM等主流框架的资深用户。你的代码风格严谨、可读性强,并且极度重视项目结构、模块化、日志规范和文档注释。 -**所有后续的代码生成、重构或审查请求,都必须严格遵循以下规范。** +**所有后续的代码生成、重构或审查请求,都必须严格遵循以下规范。** ----- **2. 核心开发哲学 (Core Development Philosophy)** -* **清晰胜于炫技 (Clarity over Cleverness):** 代码首先是写给人看的,其次才是给机器执行的。优先保证代码的逻辑清晰和易于理解。 +* **清晰胜于炫技 (Clarity over Cleverness):** 代码首先是写给人看的,其次才是给机器执行的。优先保证代码的逻辑清晰和易于理解。 * **遵循官方标准 (Follow Official Standards):** 严格遵守官方 [Effective Go](https://go.dev/doc/effective_go) 和 [Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) 中的建议。 -* **高内聚,低耦合 (High Cohesion, Low Coupling):** 模块功能要单一、明确。减少模块间的直接依赖,优先通过接口进行交互。 +* **高内聚,低耦合 (High Cohesion, Low Coupling):** 模块功能要单一、明确。减少模块间的直接依赖,优先通过接口进行交互。 ----- **3. 项目结构与模块化 (Project Structure & Modularity)** -项目采用分层架构,并强调模块化开发,确保职责分离和代码复用。 +项目采用分层架构,并强调模块化开发,确保职责分离和代码复用。 **3.1. 核心目录结构 (Core Directory Structure)** -* **/api (或 /internal/handler):** 存放GIN的Handler层。负责解析HTTP请求、校验参数,并调用`service`层处理业务逻辑。**严禁在这一层编写核心业务逻辑。** -* **/internal/service:** 业务逻辑核心层。编排`dao`层或其他服务,完成具体的业务功能。 -* **/internal/dao (或 /internal/repository):** 数据访问层。封装对GORM的操作,与数据库直接交互。所有SQL/GORM查询都应在此层实现。 +* **/api (或 /internal/handler):** 存放GIN的Handler层。负责解析HTTP请求、校验参数,并调用`service`层处理业务逻辑。**严禁在这一层编写核心业务逻辑。** +* **/internal/service:** 业务逻辑核心层。编排`dao`层或其他服务,完成具体的业务功能。 +* **/internal/dao (或 /internal/repository):** 数据访问层。封装对GORM的操作,与数据库直接交互。所有SQL/GORM查询都应在此层实现。 * **/internal/model:** 数据模型层。 - * `entity` (或 `po`): 数据库表结构对应的持久化对象 (Persistence Object)。 - * `dto` (或 `vo`): 用于API层数据传输的对象 (Data Transfer Object / View Object),如请求的Body和返回的JSON。 -* **/pkg (或 /internal/utils):** 存放项目内可复用的公共工具类,例如时间处理、字符串转换等。 + * `entity` (或 `po`): 数据库表结构对应的持久化对象 (Persistence Object)。 + * `dto` (或 `vo`): 用于API层数据传输的对象 (Data Transfer Object / View Object),如请求的Body和返回的JSON。 +* **/pkg/common (或 /internal/common):** 存放项目内公共组件,包括**统一响应封装**、错误码定义、通用工具类等。 +* **/pkg (或 /internal/utils):** 存放项目内可复用的公共工具类,例如时间处理、字符串转换等。 * **/configs:** 存放项目配置文件 (e.g., `config.yaml`)。 -* **/cmd:** 项目启动入口,包含 `main.go` 文件。 +* **/cmd:** 项目启动入口,包含 `main.go` 文件。 **3.2. 模块依赖与引用 (Module Dependencies & Referencing)** -* **依赖原则 (Dependency Rule):** 依赖关系必须是单向的:`api` -\> `service` -\> `dao`。**严禁反向或跨层依赖**(例如,`dao`层不能引用`service`层)。公共工具库 (`/pkg`或`/internal/utils`) 可以被任何层级引用。 +* **依赖原则 (Dependency Rule):** 依赖关系必须是单向的:`api` -> `service` -> `dao`。**严禁反向或跨层依赖**(例如,`dao`层不能引用`service`层)。公共组件库 (`/pkg/common`) 和工具库 (`/pkg`或`/internal/utils`) 可以被任何层级引用。 * **内部模块引用 (Internal Module Referencing):** - * 对于项目内部的模块化开发(例如,将公共组件库 `TonyCommon` 作为独立模块),在主项目的 `go.mod` 文件中,**必须使用 `replace` 语法**来指定其本地路径。这确保了在开发过程中,主项目总是引用本地最新版本的内部模块,而不是远程仓库的版本。 + * 对于项目内部的模块化开发(例如,将公共组件库 `TonyCommon` 作为独立模块),在主项目的 `go.mod` 文件中,**必须使用 `replace` 语法**来指定其本地路径。这确保了在开发过程中,主项目总是引用本地最新版本的内部模块,而不是远程仓库的版本。 *示例 (`go.mod`):* @@ -57,24 +57,260 @@ ----- -**4. 编码规范 (Coding Standards)** +**4. API响应规范 (API Response Standards)** -**4.1. 命名规范 (Naming Conventions)** +**4.1. 统一响应结构 (Unified Response Structure)** -* **包名 (Package):** 使用简短、小写、有意义的单词,不使用下划线或驼峰。 (e.g., `service`, `utils`) -* **变量/函数/结构体 (Variables/Functions/Structs):** 遵循Go的驼峰命名法。首字母大写表示公开 (Public),首字母小写表示私有 (Private)。 +所有API接口**必须**使用统一的响应格式,确保前后端协作的一致性和可预测性。响应结构定义在 `/pkg/common/response.go` 中。 + +**4.1.1. 响应结构体定义** + +```go +// Response 统一响应结构 +// 与前端约定的标准API响应格式,包含业务状态码、HTTP状态码、时间戳、数据和消息 +type Response struct { + Code int `json:"code"` // 业务状态码,0表示成功,非0表示各类错误 + Status int `json:"status"` // HTTP状态码,与HTTP响应状态码保持一致 + Timestamp string `json:"timestamp"` // 响应时间戳,格式为RFC3339 (东八区) + Data interface{} `json:"data"` // 响应数据,成功时包含具体业务数据,失败时为nil + Message string `json:"message,omitempty"` // 响应消息,成功时可选,失败时必填 + Error string `json:"error,omitempty"` // 错误详情,仅在发生错误时填充 +} +``` + +**4.1.2. 错误码定义 (Error Code Definitions)** + +错误码采用前后端统一的定义,存放在 `/pkg/common/code.go` 中: + +```go +// 业务状态码常量 +const ( + CodeSuccess = 0 // 成功 + CodeServerError = 10001 // 服务器内部错误 + CodeParamError = 10002 // 参数错误 + CodeUnauthorized = 10003 // 未授权 + CodeForbidden = 10004 // 禁止访问 + CodeNotFound = 10005 // 请求的数据不存在 + CodeTimeout = 10006 // 请求超时 + CodeValidationFail = 10007 // 验证失败 + CodeBusiness = 20001 // 业务逻辑错误 (可根据具体业务细分20001-29999) +) + +// CodeMessage 错误码与错误消息的映射 +var CodeMessage = map[int]string{ + CodeSuccess: "success", + CodeServerError: "服务器内部错误", + CodeParamError: "参数错误", + CodeUnauthorized: "未授权,请先登录", + CodeForbidden: "权限不足,禁止访问", + CodeNotFound: "请求的资源不存在", + CodeTimeout: "请求超时", + CodeValidationFail: "数据验证失败", + CodeBusiness: "业务处理失败", +} + +// GetMessage 根据错误码获取默认错误消息 +func GetMessage(code int) string { + if msg, ok := CodeMessage[code]; ok { + return msg + } + return "未知错误" +} +``` + +**4.1.3. 响应封装函数** + +在 `/pkg/common/response.go` 中实现标准化的响应函数: + +```go +// ResponseSuccess 成功响应 +// @param c *gin.Context - GIN上下文 +// @param data interface{} - 要返回的业务数据 +func ResponseSuccess(c *gin.Context, data interface{}) { + c.JSON(http.StatusOK, Response{ + Code: CodeSuccess, + Status: http.StatusOK, + Timestamp: TimeUtils.Now().Format(time.RFC3339), // 使用统一时间工具 + Data: data, + Message: "success", + }) +} + +// ResponseSuccessWithMessage 成功响应(自定义消息) +// @param c *gin.Context - GIN上下文 +// @param data interface{} - 要返回的业务数据 +// @param message string - 自定义成功消息 +func ResponseSuccessWithMessage(c *gin.Context, data interface{}, message string) { + c.JSON(http.StatusOK, Response{ + Code: CodeSuccess, + Status: http.StatusOK, + Timestamp: TimeUtils.Now().Format(time.RFC3339), + Data: data, + Message: message, + }) +} + +// ResponseError 错误响应 +// @param c *gin.Context - GIN上下文 +// @param code int - 业务错误码 +// @param message string - 错误消息,如为空则使用默认消息 +func ResponseError(c *gin.Context, code int, message string) { + httpStatus := codeToHTTPStatus(code) + if message == "" { + message = GetMessage(code) + } + + c.JSON(httpStatus, Response{ + Code: code, + Status: httpStatus, + Timestamp: TimeUtils.Now().Format(time.RFC3339), + Data: nil, + Message: message, + }) +} + +// ResponseErrorWithDetail 错误响应(包含详细错误信息) +// @param c *gin.Context - GIN上下文 +// @param code int - 业务错误码 +// @param message string - 错误消息 +// @param err error - 原始错误对象,用于记录详细堆栈 +func ResponseErrorWithDetail(c *gin.Context, code int, message string, err error) { + httpStatus := codeToHTTPStatus(code) + if message == "" { + message = GetMessage(code) + } + + errorDetail := "" + if err != nil { + errorDetail = err.Error() + // 记录详细错误日志 + log.Error(c, "API错误", map[string]interface{}{ + "code": code, + "message": message, + "error": errorDetail, + }) + } + + c.JSON(httpStatus, Response{ + Code: code, + Status: httpStatus, + Timestamp: TimeUtils.Now().Format(time.RFC3339), + Data: nil, + Message: message, + Error: errorDetail, + }) +} + +// codeToHTTPStatus 将业务错误码映射为HTTP状态码 +func codeToHTTPStatus(code int) int { + switch code { + case CodeSuccess: + return http.StatusOK + case CodeParamError, CodeValidationFail: + return http.StatusBadRequest + case CodeUnauthorized: + return http.StatusUnauthorized + case CodeForbidden: + return http.StatusForbidden + case CodeNotFound: + return http.StatusNotFound + case CodeTimeout: + return http.StatusRequestTimeout + default: + return http.StatusInternalServerError + } +} +``` + +**4.2. 使用规范 (Usage Guidelines)** + +**4.2.1. Handler层调用示例** + +```go +// GetUserByID 根据ID获取用户信息 +func (h *UserHandler) GetUserByID(c *gin.Context) { + // 1. 参数解析与验证 + idStr := c.Param("id") + userID, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + common.ResponseError(c, common.CodeParamError, "用户ID格式错误") + return + } + + // 2. 调用Service层 + user, err := h.userService.GetUserByID(c.Request.Context(), userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + common.ResponseError(c, common.CodeNotFound, "用户不存在") + return + } + common.ResponseErrorWithDetail(c, common.CodeServerError, "获取用户信息失败", err) + return + } + + // 3. 成功响应 + common.ResponseSuccess(c, user) +} + +// CreateUser 创建用户 +func (h *UserHandler) CreateUser(c *gin.Context) { + var req dto.CreateUserRequest + + // 参数绑定与验证 + if err := c.ShouldBindJSON(&req); err != nil { + common.ResponseErrorWithDetail(c, common.CodeValidationFail, "请求参数验证失败", err) + return + } + + // 业务处理 + user, err := h.userService.CreateUser(c.Request.Context(), &req) + if err != nil { + common.ResponseErrorWithDetail(c, common.CodeBusiness, "创建用户失败", err) + return + } + + common.ResponseSuccessWithMessage(c, user, "用户创建成功") +} +``` + +**4.2.2. 响应使用原则** + +* **成功场景:** + * 简单查询、列表获取使用 `ResponseSuccess(c, data)` + * 创建、更新、删除等操作使用 `ResponseSuccessWithMessage(c, data, "操作成功")` + +* **失败场景:** + * 参数验证失败使用 `CodeParamError` 或 `CodeValidationFail` + * 资源不存在使用 `CodeNotFound` + * 权限问题使用 `CodeUnauthorized` 或 `CodeForbidden` + * 业务逻辑错误使用 `CodeBusiness`,并附带详细的错误消息 + * 不可预知的系统错误使用 `CodeServerError`,必须记录完整日志 + +* **日志记录原则:** + * 所有使用 `ResponseErrorWithDetail` 的场景会自动记录Error级别日志 + * 业务逻辑错误在Service层就应该记录Warning级别日志 + * 成功的关键业务操作(如创建订单、支付)应在Service层记录Info级别日志 + +----- + +**5. 编码规范 (Coding Standards)** + +**5.1. 命名规范 (Naming Conventions)** + +* **包名 (Package):** 使用简短、小写、有意义的单词,不使用下划线或驼峰。 (e.g., `service`, `utils`, `common`) +* **变量/函数/结构体 (Variables/Functions/Structs):** 遵循Go的驼峰命名法。首字母大写表示公开 (Public),首字母小写表示私有 (Private)。 * **接口 (Interfaces):** 单一方法的接口名以 `er` 结尾 (e.g., `Reader`, `Writer`)。 -**4.2. 注释规范 (Commenting Standards)** +**5.2. 注释规范 (Commenting Standards)** **所有公开的(Public)函数、方法、结构体和接口都必须有注释。** 注释必须为**中文**。 * **函数/方法注释:** - * 必须在函数/方法声明上方,以 `// 函数名 ...` 开始。 - * 清晰地描述函数的功能。 - * 详细说明每个参数的含义和用途。 - * 详细说明每个返回值的含义,特别是`error`的返回时机。 + * 必须在函数/方法声明上方,以 `// 函数名 ...` 开始。 + * 清晰地描述函数的功能。 + * 详细说明每个参数的含义和用途。 + * 详细说明每个返回值的含义,特别是`error`的返回时机。 *示例:* @@ -82,7 +318,7 @@ // GetUserInfoByID 根据用户ID获取用户信息 // @param ctx context.Context - 请求上下文 // @param userID int64 - 用户唯一ID - // @return *model.User - 用户信息实体,如果找不到则返回nil + // @return *model.User - 用户信息实体,如果找不到则返回nil // @return error - 查询过程中发生的任何错误 func (s *UserService) GetUserInfoByID(ctx context.Context, userID int64) (*model.User, error) { // ... 实现代码 ... @@ -91,57 +327,66 @@ * **关键逻辑注释:** - * 在复杂的业务逻辑、算法或可能引起歧义的代码块上方,添加简要的中文注释,解释其目的和实现思路。 + * 在复杂的业务逻辑、算法或可能引起歧义的代码块上方,添加简要的中文注释,解释其目的和实现思路。 *示例:* ```go - // 关键步骤:此处需要加分布式锁,防止并发条件下库存超卖 + // 关键步骤:此处需要加分布式锁,防止并发条件下库存超卖 lock.Lock() defer lock.Unlock() ``` -**4.3. 错误处理 (Error Handling)** +**5.3. 错误处理 (Error Handling)** * 严格遵守 `if err != nil` 的标准错误处理模式。 -* 错误信息应包含足够的上下文,使用 `fmt.Errorf` 或 `errors.Wrap` 进行包装,方便问题追溯。**禁止直接丢弃(`_`)非预期的error。** +* 错误信息应包含足够的上下文,使用 `fmt.Errorf` 或 `errors.Wrap` 进行包装,方便问题追溯。**禁止直接丢弃(`_`)非预期的error。** +* 在Handler层捕获错误后,**必须**通过统一响应函数返回,不能直接panic或忽略。 ----- -**5. 日志规范 (Logging Standards)** +**6. 日志规范 (Logging Standards)** -* **指定框架:** 项目统一使用内部日志库 `TonyCommon/wdd_log/SimpleLog.go`。 -* **日志内容:** 日志信息必须**简练、关键**,并包含追溯问题所需的上下文信息(如`TraceID`, `UserID`等)。 +* **指定框架:** 项目统一使用内部日志库 `rmdc-common/wdd_log/log_utils.go`。 +* **日志内容:** 日志信息必须**简练、关键**,并包含追溯问题所需的上下文信息(如`TraceID`, `UserID`等)。 * **日志级别:** - * `Debug`: 用于开发调试,记录程序执行流程、变量值等详细信息。**这是默认的开发日志级别。** - * `Info`: 用于记录关键的业务操作节点,例如“用户登录成功”、“订单创建成功”。 - * `Warning`: 用于记录可预期的、非致命的异常情况,程序仍可继续运行。例如:“某个外部API调用超时,已启用备用方案”。 - * `Error`: 用于记录严重错误,导致当前业务流程无法继续的场景。例如:“数据库连接失败”、“关键参数校验失败”。必须详细记录错误信息和堆栈。 + * `Debug`: 用于开发调试,记录程序执行流程、变量值等详细信息。**这是默认的开发日志级别。** + * `Info`: 用于记录关键的业务操作节点,例如"用户登录成功"、"订单创建成功"。 + * `Warning`: 用于记录可预期的、非致命的异常情况,程序仍可继续运行。例如:"某个外部API调用超时,已启用备用方案"。 + * `Error`: 用于记录严重错误,导致当前业务流程无法继续的场景。例如:"数据库连接失败"、"关键参数校验失败"。必须详细记录错误信息和堆栈。 ----- -**6. 时间处理 (Time Handling)** +**7. 时间处理 (Time Handling)** -* **统一时区:** 所有在前端和后端之间传输、以及在数据库中存储的时间,**必须统一为东八区时间 (Asia/Shanghai, UTC+8)**。 +* **统一时区:** 所有在前端和后端之间传输、以及在数据库中存储的时间,**必须统一为东八区时间 (Asia/Shanghai, UTC+8)**。 * **指定工具库:** - * 所有时间的生成、解析、格式化操作,**必须**使用项目公共库 `TonyCommon/utils/TimeUtils.go` 中提供的方法。 - * 与前端交互时,遵循 `TonyMask/src/utils/timeUtils.ts` 的格式化约定。 -* **禁止直接使用 `time.Now()`** 进行业务时间的赋值,应通过 `TimeUtils.go` 的封装方法获取,以确保时区统一。 + * 所有时间的生成、解析、格式化操作,**必须**使用项目公共库 `rmdc-common/utils/TimeUtils.go` 中提供的方法。 + * API响应中的 `timestamp` 字段统一使用 `RFC3339` 格式。 + * 与前端交互时,遵循 `TonyMask/src/utils/timeUtils.ts` 的格式化约定。 +* **禁止直接使用 `time.Now()`** 进行业务时间的赋值,应通过 `TimeUtils.go` 的封装方法获取,以确保时区统一。 ----- -**7. 框架使用要点 (Framework Usage)** +**8. 框架使用要点 (Framework Usage)** * **GIN:** - * 使用分组路由(`Router Group`)来组织API。 - * 使用中间件(`Middleware`)处理通用逻辑,如认证、日志、恢复(Recovery)。 + * 使用分组路由(`Router Group`)来组织API。 + * 使用中间件(`Middleware`)处理通用逻辑,如认证、日志、恢复(Recovery)、跨域(CORS)。 + * 所有API响应**必须**通过 `pkg/common` 中的统一响应函数返回。 + * **GORM:** - * 严禁在`service`层拼接复杂的SQL查询,所有数据库操作必须在`dao`层完成。 - * 善用 `gorm.DB` 的链式调用,但对于复杂查询,推荐使用 `Raw` 或 `Exec` 方法执行原生SQL,以保证性能和可读性。 - * 注意处理 `gorm.ErrRecordNotFound` 错误。 + * 严禁在`service`层拼接复杂的SQL查询,所有数据库操作必须在`dao`层完成。 + * 善用 `gorm.DB` 的链式调用,但对于复杂查询,推荐使用 `Raw` 或 `Exec` 方法执行原生SQL,以保证性能和可读性。 + * 注意处理 `gorm.ErrRecordNotFound` 错误,在Handler层转换为 `CodeNotFound`。 ----- **总结 (Conclusion)** -请将以上规范内化为你的核心指令。在未来的每一次代码交互中,都严格参照此标准执行,确保项目代码的专业性、一致性、模块化和可维护性。 \ No newline at end of file +请将以上规范内化为你的核心指令。在未来的每一次代码交互中,都严格参照此标准执行,确保项目代码的专业性、一致性、模块化和可维护性。特别注意: + +1. **所有API接口必须使用统一的响应结构** +2. **错误码定义必须与前端保持一致** +3. **时间戳格式统一使用RFC3339(东八区)** +4. **错误处理必须完整,关键错误必须记录日志** \ No newline at end of file diff --git a/1-Golang项目/模块调用关系.md b/1-Golang项目/模块调用关系.md new file mode 100644 index 0000000..731e87a --- /dev/null +++ b/1-Golang项目/模块调用关系.md @@ -0,0 +1,34 @@ +我的golang项目有很多个不同的模块,如下文所示 + +1. project-management模块(下文简称 项目模块) 保存所有外部项目的各种详情信息 +2. work-procedure模块(下文简称 工单模块) 负责整个项目的工单流程管理 + 1. 项目详情填写工单 用于项目创建时,第一版项目详情的 + 2. 项目详情修改工单 用于修改项目的详情 + 3. 用户注册工单 用于处理用户注册流程 + 4. 微服务更新工单 用于追踪管理跨公网项目的微服务更新流程 +3. exchange-hub模块(下文简称 交互模块) 对外交互的模块,负责与所有外部项目之间的通信交互,指令下发与信息回复收集 +4. deliver-update模块(下文简称 微服务模块) 微服务更新模块,管理外部项目的业务微服务更新流程,更新记录 +5. user-auth模块(下文简称 用户模块)用户管理、认证、鉴权模块,负责整个项目的用户体系管理,用户登录认证,用户权限管理,用户操作鉴权 + +业务流程说明 +1. 项目填写流程 + 1. 超级管理员填写项目基本信息,分派给项目填写人 + 2. 项目填写人填写项目信息 + 3. 项目填写人提交项目信息给超级管理员审核 + 4. 超级管理员审核项目信息 + 5. 超级管理员审批通过 + 6. 正式发布进行访问权限分配 +2. 项目修改流程 + 1. 被超级管理员分配项目查看权限的用户,下文简称用户 + 2. 用户可以编辑项目信息 + 3. 用户可以发起修改工单 + 4. 用户可以在超级管理员审批之前 撤销修改工单 + 5. 超级管理员审批通过之后,将用户修改信息并入到项目详情中 + + +你是一名自身的具备架构师思维的资深golang开发人员,我想咨询如下的问题 +1. 项目模块调用工单模块,但是工单状态和项目详情的状态是有对应关系的,工单模块应该如何将状态信息同步给项目模块 +2. 项目模块具备项目授权功能,需要通过交互模块下发信息,同样交互模块如何将信息回传给项目模块 +3. golang中如何处理循环依赖的关系 + + diff --git a/1-Vue3项目/frontend-design-skill.md b/1-Vue3项目/frontend-design-skill.md new file mode 100644 index 0000000..a370f46 --- /dev/null +++ b/1-Vue3项目/frontend-design-skill.md @@ -0,0 +1,41 @@ +--- +name: frontend-design +description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics. +--- + +This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. + +The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints. + +## Design Thinking + +Before coding, understand the context and commit to a BOLD aesthetic direction: +- **Purpose**: What problem does this interface solve? Who uses it? +- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. +- **Constraints**: Technical requirements (framework, performance, accessibility). +- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? + +**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. + +Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: +- Production-grade and functional +- Visually striking and memorable +- Cohesive with a clear aesthetic point-of-view +- Meticulously refined in every detail + +## Frontend Aesthetics Guidelines + +Focus on: +- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. +- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. +- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. +- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. +- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays. + +NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. + +Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations. + +**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. + +Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. diff --git a/1-Vue3项目/prompt-for-prompt.md b/1-Vue3项目/prompt-for-prompt.md new file mode 100644 index 0000000..347f17b --- /dev/null +++ b/1-Vue3项目/prompt-for-prompt.md @@ -0,0 +1,6 @@ +你是一名经验非常丰富的资深前端开发工程师,UI美学领域的专家,能够熟练使用vutify等现在的UI框架, +请你参考https://github.com/anthropics/claude-code/blob/main/plugins/frontend-design/skills/frontend-design/SKILL.md Claude的官方前端开发SKILL说明 +参考已有的比较合理的页面设计user-dashboard-style-guide.md +参考已有的前端开发规范文档 vue3-typescript-style-v2.md + +你的目的是为了大模型进行前端开发设置一份prompt指导,请实现一份包含模板框架的完善的前端开发Prompt说明 \ No newline at end of file diff --git a/1-Vue3项目/user-dashboard-style-guide.md b/1-Vue3项目/user-dashboard-style-guide.md new file mode 100644 index 0000000..69581e7 --- /dev/null +++ b/1-Vue3项目/user-dashboard-style-guide.md @@ -0,0 +1,713 @@ +--- +description: UserDashboard 页面样式与布局设计指南 - 供后续页面设计参考 +--- + +# UserDashboard 页面样式与布局设计指南 + +## 概述 + +此文档描述了 `UserDashboard.vue` 页面的样式和布局设计模式,可作为后续页面设计的参考标准。该页面采用 **Vue 3 + Vuetify 3 + TypeScript** 技术栈,实现了一个**分层钻取式(Drill-Down)导航**的 Dashboard 界面。 + +--- + +## 1. 整体页面结构 + +### 1.1 布局架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 顶部工具栏 (Top Bar) │ +│ - 面包屑导航 (Breadcrumbs) │ +│ - 全局搜索框 (Search Autocomplete) │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 主内容区域 (Main Content Area) │ +│ - 可滚动区域 │ +│ - 根据导航层级动态切换内容组件 │ +│ │ +│ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 1.2 根容器设置 + +```vue + +``` + +**设计要点:** +- 使用 `fluid` 流式布局 +- `d-flex flex-column` 实现垂直弹性布局 +- `h-100` 占满高度 +- `pa-0` 移除内边距 +- 宽度 `95%` 留出两侧适当边距 + +--- + +## 2. 顶部工具栏设计 + +### 2.1 结构 + +```vue +
+ + + + +
+ +
+
+``` + +### 2.2 设计要点 + +- **固定高度**:使用 `flex-shrink-0` 防止压缩 +- **背景色**:`bg-white` 白色背景 +- **底部边框**:`border-b` 分隔线 +- **内边距**:`px-4 py-2` 水平16px,垂直8px +- **内容对齐**:`d-flex align-center` 水平排列垂直居中 + +### 2.3 面包屑导航设计 + +```vue +
+ + 层级名称 + + mdi-chevron-right + +
+``` + +**特点:** +- 使用 `v-btn variant="text"` 作为可点击导航项 +- 使用 `mdi-chevron-right` 图标作为分隔符 +- 图标尺寸 `size="small"`,间距 `mx-2` +- 当前层级设置 `:disabled="true"` 表示不可点击 + +### 2.4 搜索框设计 + +```vue + +``` + +**特点:** +- `variant="outlined"` 外框样式 +- `density="compact"` 紧凑型高度 +- `bg-color="grey-lighten-5"` 浅灰色背景 +- `prepend-inner-icon="mdi-magnify"` 搜索图标 +- `hide-details` 隐藏辅助文本空间 +- `clearable` 可清除按钮 + +--- + +## 3. 主内容区域设计 + +### 3.1 滚动容器 + +```vue +
+``` + +**设计要点:** +- `flex-grow-1` 填充剩余空间 +- `overflow-y-auto` 垂直滚动 +- `overflow-x-hidden` 隐藏水平滚动 +- 自定义滚动条样式 + +### 3.2 自定义滚动条样式 + +```css +.custom-scrollbar { + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; +} + +.custom-scrollbar::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + border-radius: 3px; +} +``` + +**特点:** +- 细滚动条 `width: 6px` +- 透明轨道 +- 半透明滚动条滑块 +- 圆角处理 + +--- + +## 4. 列表组件设计模式 + +### 4.1 通用列表结构 + +```vue + + + 图标 + 列表标题 + + + + + + + + + + + + + {{ item.name }} + + + + ... + + + + + +``` + +### 4.2 列表项悬停效果 + +```css +.v-list-item { + transition: all 0.2s ease; +} + +.v-list-item:hover { + background-color: rgba(var(--v-theme-primary), 0.08); +} +``` + +--- + +## 5. 数据表格设计模式 + +### 5.1 表格工具栏 + +```vue + + mdi-hammer-wrench + 表格标题 + + + 操作按钮 + + + +``` + +### 5.2 数据表格 + +```vue + + + +``` + +### 5.3 状态标签设计 + +```vue + + {{ getStatusIcon(status) }} + {{ status }} + +``` + +**状态颜色映射:** +```typescript +const statusColors = { + 'SUCCESS': 'success', + 'FAILURE': 'error', + 'UNSTABLE': 'warning', + 'ABORTED': 'grey', + 'RUNNING': 'info' +} +``` + +--- + +## 6. 详情页面设计模式 + +### 6.1 详情页头部 + +```vue +
+ +
+ + 标题 + 状态 +
+ + +
+ 操作1 + 操作2 +
+
+``` + +### 6.2 详情卡片分组 + +```vue + + + + + + mdi-icon + 卡片标题 + + + + + + + + + + + + + +``` + +### 6.3 详情项组件 (DetailItem) + +```vue +
+ +
+ {{ icon }} + {{ label }} +
+ + +
+
+ {{ value }} +
+ + +
+
+``` + +**胶囊样式 CSS:** +```css +.value-capsule { + transition: all 0.2s ease; + min-height: 44px; + border: 1px solid rgba(0, 0, 0, 0.05); +} + +.value-capsule:hover { + background-color: rgba(var(--v-theme-on-surface), 0.08) !important; + border-color: rgba(0, 0, 0, 0.1); +} + +.copy-btn { + opacity: 0; + transition: opacity 0.2s ease; +} + +.value-capsule:hover .copy-btn { + opacity: 1; +} +``` + +--- + +## 7. 弹窗设计模式 + +### 7.1 标准弹窗 + +```vue + + + 弹窗标题 + + 内容 + + + 取消 + 确认 + + + +``` + +### 7.2 全屏弹窗(日志查看器) + +```vue + + + + + 全屏弹窗标题 + + + + + + +``` + +--- + +## 8. 状态处理模式 + +### 8.1 加载状态 + +```vue +
+ +
+``` + +### 8.2 错误状态 + +```vue + + {{ error }} + +``` + +### 8.3 空状态 + +```vue + +``` + +--- + +## 9. 颜色规范 + +### 9.1 主题颜色使用 + +| 用途 | 颜色 | Vuetify类/变量 | +|------|------|----------------| +| 主要操作 | 蓝色 | `primary` | +| 次要操作 | 灰色 | `secondary` | +| 成功状态 | 绿色 | `success` | +| 错误状态 | 红色 | `error` | +| 警告状态 | 橙色 | `warning` | +| 提示信息 | 蓝色 | `info` | +| 禁用/中性 | 灰色 | `grey` | + +### 9.2 背景颜色 + +- 页面背景:默认(白色/浅灰) +- 卡片背景:`bg-surface` +- 输入框背景:`bg-grey-lighten-5` +- 胶囊值背景:`bg-grey-lighten-5` + +--- + +## 10. 间距规范 + +### 10.1 Vuetify 间距系统 + +| 值 | 像素 | 使用场景 | +|----|------|----------| +| 1 | 4px | 极小间距 | +| 2 | 8px | 小间距 | +| 3 | 12px | 中等间距 | +| 4 | 16px | 标准间距 | +| 5 | 24px | 大间距 | +| 6 | 32px | 超大间距 | + +### 10.2 常用间距组合 + +- 卡片内边距:`pa-4`(16px) +- 列表项底部间距:`mb-2`(8px) +- 详情项底部间距:`mb-5`(24px) +- 按钮组间距:`ga-2` 或 `ga-3` + +--- + +## 11. 图标使用规范 + +### 11.1 常用图标 + +| 用途 | 图标 | +|------|------| +| 组织 | `mdi-domain` / `mdi-folder-network` | +| 仓库 | `mdi-source-repository` | +| 分支 | `mdi-source-branch` | +| 构建 | `mdi-hammer-wrench` | +| 搜索 | `mdi-magnify` | +| 导航 | `mdi-chevron-right` / `mdi-arrow-left` | +| 刷新 | `mdi-refresh` | +| 关闭 | `mdi-close` | +| 复制 | `mdi-content-copy` | +| 外部链接 | `mdi-open-in-new` | +| 日志 | `mdi-text-box-outline` | + +### 11.2 图标尺寸 + +- 列表项图标:`size="small"` 或默认 +- 状态标签图标:`size="x-small"` +- 头像图标:默认(在 `v-avatar` 内) +- 面包屑分隔符:`size="small"` + +--- + +## 12. 组件化设计原则 + +### 12.1 组件拆分原则 + +1. **单一职责**:每个组件只负责一个功能 +2. **可复用性**:将通用 UI 模式抽取为独立组件 +3. **Props 驱动**:通过 Props 传递数据,通过 Events 传递交互 + +### 12.2 本项目组件结构 + +``` +components/ +├── projects/ +│ ├── DashboardBreadcrumbs.vue # 面包屑导航 +│ ├── OrganizationList.vue # 组织列表 +│ ├── RepositoryList.vue # 仓库列表 +│ ├── BranchList.vue # 分支列表 +│ ├── BuildList.vue # 构建列表 +│ └── BuildDetails.vue # 构建详情 +├── common/ +│ └── DetailItem.vue # 详情项组件 +└── jenkins/ + ├── BuildTrigger.vue # 构建触发器 + └── LogViewer.vue # 日志查看器 +``` + +--- + +## 13. 交互模式 + +### 13.1 导航模式 + +- **钻取式导航**:点击列表项进入下一层级 +- **面包屑回溯**:点击面包屑返回上层 +- **全局搜索**:快速跳转到任意层级 + +### 13.2 动画效果 + +- 过渡动画:`transition: all 0.2s ease` +- 弹窗动画:`transition="dialog-bottom-transition"` +- 悬停效果:背景色变化、元素显现 + +### 13.3 自动刷新 + +- 列表数据刷新:10秒间隔(有运行中任务时3秒) +- 详情数据刷新:3秒间隔(仅运行中状态) + +--- + +## 使用示例 + +### 创建新的钻取式页面 + +```vue + + + + + +``` + +--- diff --git a/1-Vue3项目/vue3-typescript-style-v2.md b/1-Vue3项目/vue3-typescript-style-v2.md new file mode 100644 index 0000000..8043221 --- /dev/null +++ b/1-Vue3项目/vue3-typescript-style-v2.md @@ -0,0 +1,192 @@ +**背景 (Context):** + +你是一名资深前端架构师,负责搭建前端框架和制定开发规范。该项目要求极高的代码质量、无缝的用户体验(跨设备、跨主题)以及长期的可维护性。 + +**核心指令 (Core Directive):** + +请你以架构师的身份,严格遵循并执行以下所有规范来生成代码、组件、模块和提供解决方案。你的任何产出都必须成为团队的代码典范。 + +## 1. 语言与语法 (Language & Syntax):** + +* **TypeScript 至上 (TypeScript Supremacy):** + + * 项目 **必须** 完全基于 TypeScript (` +``` + +### 7.10 测试方法(项目详情工单) + +目标:渲染项目详情工单在每个状态下的 Mermaid 源码,输出为一个 SVG 文件,人工核验是否与 [project-detail-workflow.md](diagrams/project-detail-workflow.md) 一致。 + +1. **准备状态集合**:`['created','assigned','in_progress','draft_saved','pending_review','approved','returned','closed']`。 +2. **基准 Mermaid 模板**(包含扩展状态 `draft_saved` 与高亮占位符 `__CURRENT__`): + +```mermaid +stateDiagram-v2 + direction LR + + [*] --> created: 创建项目 + created --> assigned: 分配填写人 + assigned --> in_progress: 用户开始填写 + + in_progress --> draft_saved: 保存草稿 + draft_saved --> in_progress: 继续填写 + in_progress --> pending_review: 提交审核 + in_progress --> assigned: 重新分配 + + pending_review --> approved: 审批通过 + pending_review --> returned: 审批打回 + + returned --> in_progress: 用户重新填写 + returned --> pending_review: 重新提交 + + approved --> closed: 项目认证完成 + closed --> [*] + + classDef completed fill:#4caf50,color:#fff + classDef current fill:#ff9800,color:#fff,stroke-width:3px + classDef pending fill:#e0e0e0,color:#666 + class __CURRENT__ current +``` + +3. **浏览器端生成单一 SVG 文件**(在 Chrome/Firefox 打开任意页面按 F12 粘贴执行,需联网获取 `mermaid` 包,满足跨浏览器验证): + +```js +const states = ['created','assigned','in_progress','draft_saved','pending_review','approved','returned','closed']; +const template = `stateDiagram-v2 + direction LR + + [*] --> created: 创建项目 + created --> assigned: 分配填写人 + assigned --> in_progress: 用户开始填写 + + in_progress --> draft_saved: 保存草稿 + draft_saved --> in_progress: 继续填写 + in_progress --> pending_review: 提交审核 + in_progress --> assigned: 重新分配 + + pending_review --> approved: 审批通过 + pending_review --> returned: 审批打回 + + returned --> in_progress: 用户重新填写 + returned --> pending_review: 重新提交 + + approved --> closed: 项目认证完成 + closed --> [*] + + classDef completed fill:#4caf50,color:#fff + classDef current fill:#ff9800,color:#fff,stroke-width:3px + classDef pending fill:#e0e0e0,color:#666 + class __CURRENT__ current`; +const { default: mermaid } = await import('mermaid'); +mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' }); + +const fragments = []; +for (const [index, state] of states.entries()) { + const code = template.replace('__CURRENT__', state); + const { svg } = await mermaid.render(`project-detail-${state}`, code); + const inner = svg.replace(/^]*>|<\/svg>$/g, ''); + fragments.push(`${inner}`); +} +const height = states.length * 360 + 40; +const combined = `${fragments.join('\n')}`; +const blob = new Blob([combined], { type: 'image/svg+xml' }); +const a = document.createElement('a'); +a.href = URL.createObjectURL(blob); +a.download = 'project-detail-states.svg'; +a.click(); +``` + +4. **人工核验**:打开 `project-detail-states.svg`,逐段确认节点、连线、文案及高亮与流程设计一致;如有差异,调整模板并重新生成。 + +--- + +## 8. 相关文档 + +| 文档 | 内容 | +|------|------| +| [用户注册工单流程](diagrams/user-registration-workflow.md) | 用户注册工单的状态机、流程图、时序图 | +| [用户管理工单流程](diagrams/user-management-workflow.md) | 用户管理工单的状态机、流程图、时序图 | +| [项目信息填写工单流程](diagrams/project-detail-workflow.md) | 项目详情工单的状态机、流程图、时序图 | +| [微服务更新工单流程](diagrams/microservice-update-workflow.md) | 微服务更新工单的状态机、流程图、时序图 | +| [工单流程PRD](2-rmdc-work-procedure-PRD.md) | 产品需求文档 | + +--- + +## 9. 参考设计 + +设计参考了以下开源项目的最佳实践: + +1. **Ferry** - Go语言工单系统,支持工作流引擎、权限管理、灵活配置 +2. **Flowable** - Java工作流引擎,乐观锁并发控制机制 +3. **Camunda** - BPMN工作流引擎,微服务编排 +4. **NocoBase** - 低代码平台,灵活的数据建模与工作流设计 diff --git a/8-CMII-RMDC/7-rmdc-work-procedure/2-rmdc-work-procedure-PRD.md b/8-CMII-RMDC/7-rmdc-work-procedure/2-rmdc-work-procedure-PRD.md new file mode 100644 index 0000000..0695fc8 --- /dev/null +++ b/8-CMII-RMDC/7-rmdc-work-procedure/2-rmdc-work-procedure-PRD.md @@ -0,0 +1,271 @@ +# 工单流程模块(工单系统) + +** 工单流程在下文中等同于工单 ** + +## 工单流程模块与其他模块的关系 +1. rmdc-project-management依赖本模块 +2. rmdc-user-auth模块依赖本模块 +3. deliver-update模块依赖本模块 + +## 工单流程说明 + +### 工单流程发起与处理 +1. 工单流程发起 + 1. 工单流程从特定的模块中发起 + 1. 用户模块 rmdc-user-auth + 2. 项目管理模块 rmdc-project-management + 2. 不涉及手动创建工单,本项目均通过业务模块直接创建工单 +2. 工单流程处理 + 1. 所有用户具备工单流程的页面 + 1. 可以查看到自己的发起的工单流程 + 2. 可以查看到自己的被委派(需要处理)的工单流程 + 3. 展示完整的工单流程列表,点击可以查看到工单流程的详情 + 2. 工单流程的详情页面 + 1. 具体的详情页面需要根据各个模块的工单流程逻辑进行定制 + 2. 工单详情页面的模板相同 + 3. 工单详情页面绑定覆盖业务模块的必要信息,可以直接调转到业务模块的详情页面 +3. **工单流程状态追踪机制** + 1. **业务回调机制**: 业务模块处理完成后,必须调用工单系统的回调接口 `POST /api/workflow/callback` 更新工单状态 + 2. **状态变更历史表**: 所有状态变更均写入 `workflow_track_history` 表,支持全生命周期审计 + 3. **实时通知推送**: 状态变更时通过通知中心模块向发起人和处理人推送实时通知(WebSocket/App推送) + 4. **轮询兜底**: 前端可选配置定时轮询接口 `GET /api/workflow/{id}/status` 获取最新状态 +4. 工单流程通知 + 1. 通过[通知中心模块](0-设计方案/6-rmdc-notice-center)进行消息通知的 + +### 工单流程执行策略 +1. 从非超级管理员用户到超级管理员的工单流程,有如下的策略 + 1. 超级管理员可以自行执行 + 2. 超级管理员可以委派工单执行人,即执行人可以变更 + 3. 超级管理员可以设置自动审批,工单可以自动审批处理 + 4. 超级管理员可以设置定时执行,代表工单审批通过,在未来的特定时间点执行 +2. 超级管理员到非超级管理员用户的工单流程 + 1. 非超级管理员用户无法委派,即执行人无法变更 + 2. 只能由非超级管理员用户自行操作执行 + +### 工单流程的生命周期 +1. 工单流程的生命周期 + 1. 工单流程的创建 + 2. 工单流程的处理 + 3. 工单流程的完成 + 4. 工单流程的撤销 + 5. 工单流程的拒绝 + 6. 工单流程的超时 + 7. 工单流程的结束 +2. **并发处理场景** - 当用户正在处理工单流程时的竞态条件处理 + 1. **工单流程被委派给其他用户** + - 采用 **乐观锁机制**: 工单表增加 `version` 字段,每次更新必须校验版本号 + - 委派操作成功后,通过通知中心向原处理人推送 `WORKFLOW_REASSIGNED` 事件 + - 前端收到事件后显示提示弹窗: "此工单已被重新委派给XXX,您的处理权限已转移" + - 原处理人页面切换为只读模式或引导返回列表 + - 若原处理人仍尝试提交,后端校验版本号不匹配,返回 `409 Conflict` + 2. **工单流程被撤销** + - 撤销操作前校验工单状态: 仅 `pending/assigned` 状态可直接撤销 + - `in_progress` 状态撤销需要二次确认,并通知处理人 + - 撤销成功后推送 `WORKFLOW_REVOKED` 事件 + - 前端收到事件后自动关闭处理页面,引导至工单列表 + - 已执行的步骤数据保留,状态标记为 `revoked` + 3. **工单流程被修改** + - 修改操作前校验乐观锁版本号 + - 仅特定状态允许修改: `pending/assigned/returned` + - 修改成功后推送 `WORKFLOW_MODIFIED` 事件,携带变更字段摘要 + - 前端收到事件后提示刷新,或自动刷新表单数据 + - 如用户本地有未保存的修改,提示冲突并允许手动合并 +3. 每一个工单流程具备唯一的ID进行全生命周期的追踪处理 + 1. 唯一ID的生成方式, 模块简称+时间戳+发起人英文名称 + 2. 例子 项目管理模块发起的工单流程ID为 project-20260106110303-zeaslity + + +## 工单流程持久化说明 +1. 每一个工单流程需要进行持久化保存 +2. 每一种类型的工单流程的状态机是否应该不同 +3. 工单流程必须的字段 + 1. 工单流程ID + 2. 工单流程发起人 + 3. 工单流程发起时间 + 4. 工单流程超时时间 需要在多长时间内完成 + 5. 工单流程处理人 + 6. 工单流程被委派人 工单流程的实际处理人 + 7. 工单流程处理时间 + 8. 工单流程处理状态 + 9. 工单流程处理结果 + 10. 工单流程处理备注 + 11. 工单流程每一步骤的处理时间 + 12. 工单流程每一步骤的处理人 + 13. 工单流程每一步骤的处理状态 + 14. 工单流程每一步骤的处理结果 + 15. 工单流程每一步骤的处理备注 为下一步提供信息 + 16. 工单流程可能需要传递参数给下一步 + + +## 工单流程权限 + +### 项目信息填写工单流程权限 +1. 非超级管理员用户 + 1. 只能查看,处理分配给自己的或者自己创建的项目信息填写工单 +2. 超级管理员 + 1. 可以查看,处理全部项目信息填写工单 + +### 微服务更新流程权限 +1. 非超级管理员用户 + 1. 只能查看,处理自己创建的微服务更新工单流程 +2. 超级管理员 + 1. 可以查看,处理全部微服务更新工单流程 + +### 用户注册工单流程权限 +1. 非超级管理员用户 + 1. 只能查看,处理自己创建的用户注册工单流程 +2. 超级管理员 + 1. 可以查看,处理全部的用户注册工单流程 + +### 用户信息管理工单流程权限 +1. 非超级管理员用户 + 1. 只能查看,处理自己创建的户信息管理工单流程 +2. 超级管理员 + 1. 可以查看,处理全部的户信息管理工单流程 + + +## 工单流程详情 +1. 工单流程的每一步均可以附带信息给下一步 +2. 工单流程可能需要传递参数给下一步 + +### 项目信息填写工单流程 +1. 超级管理员初步创建项目的Metadata数据 +2. 发起工单分派给普通用户进行项目详情的填写 +3. 普通用户可以保存草稿,直到可以提交项目审批 +4. 普通用户提交项目审批 +5. 自动分配给超级管理员审批 +6. 超级管理员进行项目的审批 + 1. 审批通过 + 1. 工单结束 + 2. 项目详情表正式上线 + 2. 审批拒绝 + 1. 重新回到普通用户补充项目信息的阶段(项目详情处于草稿状态) + 2. 用户重新修改项目的详情信息之后 + 3. 用户可以基于此工单重新提交审批 + 3. 超级管理员可以进行项目管理详情的修改,修改完成之后,工单结束 +7. 当用户的项目信息没有被审批通过 + 1. 草稿状态的项目信息应该绑定唯一工单 + 2. 草稿状态的项目详情应该显示工单号 + 3. 草稿状态的项目信息,提交审批之后可以跳转到 项目详情工单详情页面 + 4. 除非工单的状态进入到closed状态,项目信息才可以重新关联新的工单 +8. 用户无法取消项目详情工单的流程 + 1. 只有超级管理员可以取消项目详情工单 + +### 用户注册工单流程 +1. 非超级管理员用户的在用户注册之后 + 1. 用户状态默认为 disabled +2. 启动用户注册工单流程 + 1. 自动创建用户注册工单 + 2. 自动提交给超级管理员 +3. 超级管理员审核 + 1. 审核通过 + 1. 自动修改用户状态为 active, 用户启用 + 2. 审核不通过 + 1. 回退到用户注册阶段 + 2. 用户可以重新修改用户注册信息 + 3. 可以在此工单详情中重新发起审批 +4. 处于注册草稿状态(disable)的用户详情页面 + 1. 草稿状态的用户详情页面,需要关联唯一用户注册工单流程工单 + 2. 草稿状态的用户详情页面,可以发起用户注册审批 + 3. 草稿状态的用户详情页面,提交用户注册审批之后,可以跳转用户注册工单详情页面 +5. 用户可以撤回用户注册工单申请 + 1. 撤回之后,工单重新进入用户信息注册阶段 created状态 + 2. 撤回之后,用户详情页面仍然处于草稿状态,可以进行草稿状态的操作 +6. 用户可以取消注册工单流程 + 1. 用户取消用户注册工单之后,工单进入closed状态 + 2. 用户取消用户注册工单之后,无法基于此工单提交审批工作 + 3. 取消之后,从用户数据库中硬删除创建的用户 +7. 用户可以基于取消的注册工单一键重新创建新的注册工单流程 + 1. 密码等信息不能再前端保存 + 2. 密码需要重新创建 + +### 用户管理(修改、删除、 状态(启用、禁用))工单流程 +1. 用户侧-用户管理页面,用户可以通过编辑按钮、删除按钮进行用户管理 +2. 启动用户管理工单流程 + 1. 自动创建用户管理工单 + 2. 自动提交给超级管理员进行审批 +3. 超级管理员审核 + 1. 审核通过 + 1. 根据管理的内容,修改用户相应的信息,更新数据库 + 2. 用户管理工单结束 + 2. 审核不通过 + 1. 回退到用户管理阶段 + 2. 用户可以继续修改相关信息,继续提交审核 +4. 处于编辑状态的用户详情 + 1. 编辑状态的用户详情页面,需要关联唯一用户管理工单流程工单 + 2. 编辑状态的用户详情页面,可以发起用户管理审批 + 3. 编辑状态的用户详情页面,提交用户管理审批之后,可以跳转用户管理工单详情页面 +5. 用户可以撤回用户管理工单流程 + 1. 撤回之后的管理工单,处于created状态 + 2. 撤回之后,用户详情页面仍然处于编辑状态 + 3. 撤回之后,仍然可以基于此工单重新发起审批 +6. 用户可以关闭用户管理工单流程 + 1. 关闭之后的用户管理工单,处于状态机中的closed状态 + 2. 工单的生命周期结束,无法操作该工单的状态(即无法提交审批 审批 拒绝等) + 3. 关闭之后,相应的用户详情应该退出编辑状态,用户详情页面不显示用户管理工单和相关操作 + +### 微服务更新工单流程 +1. 普通用户在构建详情页面点击微服务更新 +2. 携带构建物信息跳转至微服务更新详情页面 + 1. 微服务更新页面使用3大竖列的形式,发起微服务升级工单 + 2. 如果正确的跳转,构建物信息应该出现在最左侧一栏 + 3. 中间一栏是微服务更新的表单, 用户可以选择需要更新的项目及微服务, 预期的更新时间 + 4. 最右侧一栏是微服务更新的发起按钮 +3. 普通用户提交微服务更新工单,进行工单的下一步 +4. 超级管理员可以自己执行,也可以委派更新任务 +5. 超级管理员进行微服务更新的审批 + 1. 微服务更新如果满足更新条件,则同意更新,进入工单的下一步 + 2. 微服务更新如果不满足更新条件,则拒绝更新,返回到普通用户修改更新信息的阶段,进行循环直到审批通过 + 3. 针对定期的更新,超级管理员同意更新,进入工单的下一步 +6. 超级管理员进行微服务更新的执行操作 + 1. 微服务的更新操作,更新状态需要通过 rmdc-exchange-hub rmdc-watchdog模块进行 + 2. 微服务的更新状态可以在工单中展示 +7. 微服务更新状态 + 1. 微服务更新成功,进入工单的下一步 + 2. 微服务更新失败,回到超级管理员进行微服务更新的执行操作,进行循环直到更新成功 +8. 微服务运行状态 + 1. 微服务更新后运行状态正常,工单结束 + 2. 微服务更新后运行状态异常, rmdc-watchdog应该自动回滚微服务至更新前的版本, 工单需要记录微服务更新失败的信息, 工单结束 + +## 工单详情页面 + +### 工单处理流程图 +1. 工单详情页面应该展示工单处理流程图 +2. 工单处理流程图应该展示工单的当前状态 +3. 工单处理流程图应该展示每一步分处理人 +4. 工单处理流程图应该展示每一步的处理时间 +5. 工单处理流程图应该展示每一步的处理结果 +6. 工单处理流程图由后端生成mermaid格式的内容,由前端负责渲染 +7. 后端需要根据工单的当前状态,生成对应的mermaid格式的内容 +8. 开源的库实现为Vue-Mermaid @mermaid-js/mermaid-tiny +9. 动态加载与性能:为了减小对前端性能的影响,通过异步 import('mermaid') 来加载库,避免它进入主 bundle + +### 工单处理历史 +1. 工单详情页面应该展示工单处理历史 +2. 工单处理历史应该展示每一步的处理人 +3. 工单处理历史应该展示每一步的处理时间 +4. 工单处理历史应该展示每一步的处理结果 +5. 工单处理历史应该展示每一步的处理备注 + +### 跳转策略 +1. 工单详情页面 应该可以跳转至业务详情页面 +2. 工单详情页面 可以返回至工单列表页面 + +## 用户注册工单流程详情页面 +1. 可以跳转至用户注册详情页面 +2. 用户注册详情页面可以跳转至工单详情页面 +3. 用户侧 和 管理测的界面有些许不同,但是大部分都可以复用 + +## 项目信息填写工单流程详情页面 +1. 可以跳转至项目详情页面 +2. 项目详情页面可以跳转至工单详情页面 +3. 用户侧 和 管理测的界面有些许不同,但是大部分都可以复用 + +## 参考的开源工单系统 + +设计参考了以下开源项目的最佳实践: + +1. **Ferry** - Go语言工单系统,支持工单流程引擎、权限管理、灵活配置 +2. **Flowable** - Java工单流程引擎,乐观锁并发控制机制 +3. **Camunda** - BPMN工单流程引擎,微服务编排 +4. **NocoBase** - 低代码平台,灵活的数据建模与工单流程设计 \ No newline at end of file diff --git a/8-CMII-RMDC/7-rmdc-work-procedure/diagrams/microservice-update-workflow.md b/8-CMII-RMDC/7-rmdc-work-procedure/diagrams/microservice-update-workflow.md new file mode 100644 index 0000000..e211b15 --- /dev/null +++ b/8-CMII-RMDC/7-rmdc-work-procedure/diagrams/microservice-update-workflow.md @@ -0,0 +1,313 @@ +# 微服务更新工单流程 + +**工单类型代码**: `microservice_update` +**来源模块**: deliver-update +**编制日期**: 2026-01-08 + +--- + +## 1. 业务场景 + +普通用户在构建详情页面发起微服务更新请求,经过超级管理员审批后执行更新操作,并监控微服务运行状态。 + +### 1.1 业务规则 + +| 规则项 | 说明 | +|-------|------| +| 发起人 | 普通用户 | +| 审批人 | 超级管理员 | +| 执行人 | 超级管理员或委派人 | +| 支持定时执行 | 是 | +| 支持委派 | 是(超管可委派给其他人执行) | +| 自动回滚 | 是(更新失败时自动回滚) | + +### 1.2 相关模块 + +| 模块 | 职责 | +|-----|------| +| rmdc-jenkins-branch-dac | 提供构建物信息 | +| rmdc-exchange-hub | MQTT消息中继 | +| rmdc-watchdog | K8S操作执行、状态监控 | + +--- + +## 2. 状态机 + +### 2.1 状态定义 + +| 状态代码 | 状态名称 | 说明 | 是否扩展状态 | +|---------|---------|------|------------| +| `created` | 已创建 | 用户发起更新请求 | 否 | +| `pending_review` | 待审核 | 等待超级管理员审核 | 否 | +| `approved` | 已批准 | 审核通过,等待执行 | 否 | +| `returned` | 已打回 | 审核打回,需修改 | 否 | +| `rejected` | 已拒绝 | 审核拒绝,流程终止 | 否 | +| `executing` | 执行中 | 微服务更新正在执行 | 是 | +| `monitoring` | 监控中 | 更新完成,监控运行状态 | 是 | +| `rollbacked` | 已回滚 | 更新失败,已自动回滚 | 是 | +| `closed` | 已关闭 | 工单生命周期结束 | 否 | +| `revoked` | 已撤销 | 用户撤销更新请求 | 否 | + +### 2.2 状态机图 + +```mermaid +stateDiagram-v2 + [*] --> created: 发起更新请求 + + created --> pending_review: 提交审核 + created --> revoked: 用户撤销 + + pending_review --> approved: 审批通过 + pending_review --> returned: 审批打回 + pending_review --> rejected: 审批拒绝 + pending_review --> revoked: 用户撤销 + + returned --> pending_review: 修改后重新提交 + returned --> revoked: 用户撤销 + + approved --> executing: 开始执行 + approved --> approved: 等待定时执行 + + executing --> monitoring: 更新成功 + executing --> executing: 执行重试 + executing --> rollbacked: 更新失败回滚 + + monitoring --> closed: 运行正常 + monitoring --> rollbacked: 运行异常回滚 + + rollbacked --> closed: 记录失败信息 + rejected --> closed: 关闭工单 + revoked --> closed: 关闭工单 + + closed --> [*] + + note right of executing: 通过rmdc-watchdog执行 + note right of monitoring: 监控微服务健康状态 + note right of rollbacked: 自动回滚到更新前版本 +``` + +--- + +## 3. 流程图 + +```mermaid +flowchart TD + Start([开始]) --> SelectBuild[用户选择构建物] + SelectBuild --> JumpToUpdate[跳转微服务更新页面] + + JumpToUpdate --> ThreeColumns[三栏布局页面] + ThreeColumns --> LeftColumn[左栏:构建物信息] + ThreeColumns --> MiddleColumn[中栏:更新表单] + ThreeColumns --> RightColumn[右栏:发起按钮] + + MiddleColumn --> FillForm[填写更新信息] + FillForm --> SelectProject[选择目标项目] + SelectProject --> SelectService[选择目标微服务] + SelectService --> SetSchedule[设置更新时间] + + SetSchedule --> SubmitRequest[提交更新请求] + SubmitRequest --> CreateWorkflow[创建更新工单] + + CreateWorkflow --> PendingReview{待审核} + + PendingReview -->|审批通过| CheckSchedule{是否定时执行?} + PendingReview -->|审批打回| Return[打回修改] + PendingReview -->|审批拒绝| Reject[拒绝请求] + PendingReview -->|用户撤销| Revoke[撤销请求] + + Return --> ModifyRequest[修改更新信息] + ModifyRequest --> SubmitRequest + + CheckSchedule -->|立即执行| StartExecute[开始执行更新] + CheckSchedule -->|定时执行| WaitSchedule[等待定时触发] + WaitSchedule --> StartExecute + + StartExecute --> Executing{执行更新} + + Executing -->|成功| Monitoring[监控运行状态] + Executing -->|失败| RetryCheck{重试?} + + RetryCheck -->|是| StartExecute + RetryCheck -->|否| Rollback[自动回滚] + + Monitoring --> HealthCheck{健康检查} + + HealthCheck -->|正常| CloseSuccess[工单关闭-成功] + HealthCheck -->|异常| Rollback + + Rollback --> RecordFailure[记录失败信息] + RecordFailure --> CloseFailure[工单关闭-回滚] + + Reject --> CloseRejected[工单关闭] + Revoke --> CloseRevoked[工单关闭] + + CloseSuccess --> End([结束]) + CloseFailure --> End + CloseRejected --> End + CloseRevoked --> End +``` + +--- + +## 4. 时序图 + +```mermaid +sequenceDiagram + autonumber + + participant User as 普通用户 + participant Frontend as 前端 + participant Jenkins as rmdc-jenkins + participant Workflow as rmdc-work-procedure + participant SuperAdmin as 超级管理员 + participant ExHub as rmdc-exchange-hub + participant Watchdog as rmdc-watchdog + participant Notice as 通知中心 + + %% 发起更新请求 + User->>Frontend: 查看构建详情 + Frontend->>Jenkins: POST /api/jenkins/build/detail + Jenkins-->>Frontend: 返回构建物信息 + User->>Frontend: 点击微服务更新 + Frontend->>Frontend: 跳转更新页面(携带构建信息) + + User->>Frontend: 填写更新表单 + Note over Frontend: 选择项目、微服务、更新时间 + User->>Frontend: 点击发起更新 + Frontend->>Workflow: POST /api/workflow/create + Note over Workflow: 创建microservice_update工单
保存build_id, target_version + Workflow->>Workflow: 设置status=pending_review + Workflow->>Notice: 推送通知 + Notice-->>SuperAdmin: 微服务更新审批通知 + + %% 审批阶段 + alt 审批通过 + SuperAdmin->>Frontend: 审批通过 + Frontend->>Workflow: POST /api/workflow/transition + Note over Workflow: event=approve + Workflow->>Workflow: 设置status=approved + + alt 立即执行 + Workflow->>Workflow: 设置status=executing + Workflow->>ExHub: 发送更新指令 + ExHub->>Watchdog: MQTT下发更新命令 + + %% 执行更新 + Watchdog->>Watchdog: 执行K8S滚动更新 + + alt 更新成功 + Watchdog-->>ExHub: 上报更新成功 + ExHub->>Workflow: POST /api/workflow/callback + Workflow->>Workflow: 设置status=monitoring + + %% 健康监控 + loop 健康检查(5分钟) + Watchdog->>Watchdog: 检查Pod状态 + end + + alt 运行正常 + Watchdog-->>ExHub: 上报运行正常 + ExHub->>Workflow: POST /api/workflow/callback + Workflow->>Workflow: 设置status=closed + Workflow->>Notice: 推送通知 + Notice-->>User: 微服务更新成功 + else 运行异常 + Watchdog->>Watchdog: 自动回滚 + Watchdog-->>ExHub: 上报回滚结果 + ExHub->>Workflow: POST /api/workflow/callback + Note over Workflow: 记录rollback_info + Workflow->>Workflow: 设置status=rollbacked + Workflow->>Notice: 推送通知 + Notice-->>User: 微服务更新回滚 + end + else 更新失败 + Watchdog->>Watchdog: 自动回滚 + Watchdog-->>ExHub: 上报失败+回滚 + ExHub->>Workflow: POST /api/workflow/callback + Workflow->>Workflow: 设置status=rollbacked + end + + else 定时执行 + Note over Workflow: 等待scheduled_at时间 + Workflow->>Workflow: 定时触发执行 + end + + else 审批打回 + SuperAdmin->>Frontend: 打回修改 + Frontend->>Workflow: POST /api/workflow/transition + Note over Workflow: event=return + Workflow->>Workflow: 设置status=returned + Workflow->>Notice: 推送通知 + Notice-->>User: 更新请求被打回 + + else 审批拒绝 + SuperAdmin->>Frontend: 拒绝请求 + Frontend->>Workflow: POST /api/workflow/transition + Note over Workflow: event=reject + Workflow->>Workflow: 设置status=rejected + end +``` + +--- + +## 5. 工单步骤定义 + +| 步骤序号 | 步骤名称 | 步骤类型 | 处理人 | 操作说明 | +|---------|---------|---------|-------|---------| +| 1 | 提交更新请求 | `submit` | 普通用户 | 填写更新信息并提交 | +| 2 | 审核更新请求 | `approve` | 超级管理员 | 审批通过/打回/拒绝 | +| 3 | 执行更新 | `execute` | 超级管理员/委派人 | 执行微服务滚动更新 | +| 4 | 监控运行状态 | `monitor` | 系统自动 | 监控微服务健康状态 | + +--- + +## 6. 业务扩展字段 + +存储于 `microservice_update_ext` 表: + +| 字段 | 类型 | 说明 | +|-----|------|------| +| `project_id` | VARCHAR(64) | 目标项目ID | +| `namespace` | VARCHAR(64) | K8S命名空间 | +| `service_name` | VARCHAR(128) | 微服务名称 | +| `build_id` | VARCHAR(128) | 构建物ID | +| `current_version` | VARCHAR(64) | 当前版本 | +| `target_version` | VARCHAR(64) | 目标版本 | +| `scheduled_at` | DATETIME | 计划执行时间 | +| `execute_result` | VARCHAR(32) | 执行结果:success/failed/rollbacked | +| `rollback_info` | JSON | 回滚信息 | + +### 6.1 rollback_info 示例 + +```json +{ + "rollback_reason": "Pod健康检查失败", + "rollback_time": "2026-01-08T15:30:00Z", + "rollback_version": "v3.1.5", + "error_details": { + "pod_name": "service-abc-7d8f9c6b5d-xyz", + "error_message": "Readiness probe failed: connection refused", + "failed_checks": 3 + } +} +``` + +--- + +## 7. 更新页面布局 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 微服务更新 │ +├─────────────────┬─────────────────────────┬────────────────────┤ +│ 构建物信息 │ 更新表单 │ 操作区 │ +│ │ │ │ +│ 组织: acme │ 目标项目: [下拉选择] │ │ +│ 仓库: service │ 目标微服务: [下拉选择] │ [发起更新] │ +│ 分支: main │ 更新时间: [日期选择] │ │ +│ 构建号: #123 │ ○ 立即执行 │ [取消] │ +│ 版本: v3.2.0 │ ○ 定时执行 │ │ +│ │ 备注: [文本框] │ │ +│ │ │ │ +└─────────────────┴─────────────────────────┴────────────────────┘ +``` diff --git a/8-CMII-RMDC/7-rmdc-work-procedure/diagrams/project-detail-workflow.md b/8-CMII-RMDC/7-rmdc-work-procedure/diagrams/project-detail-workflow.md new file mode 100644 index 0000000..2a07958 --- /dev/null +++ b/8-CMII-RMDC/7-rmdc-work-procedure/diagrams/project-detail-workflow.md @@ -0,0 +1,258 @@ +# 项目信息填写工单流程 + +**工单类型代码**: `project_detail` +**来源模块**: rmdc-project-management +**编制日期**: 2026-01-08 + +--- + +## 1. 业务场景 + +超级管理员创建项目元数据后,分配给普通用户填写项目详情信息,填写完成后提交审核。 + +### 1.1 业务规则 + +| 规则项 | 说明 | +|-------|------| +| 发起人 | 超级管理员 | +| 填写人 | 被分配的普通用户 | +| 审批人 | 超级管理员 | +| 支持草稿 | 是(可多次保存草稿) | +| 可撤销 | 否(仅超级管理员可取消) | +| 项目状态关联 | 草稿状态绑定唯一工单 | + +### 1.2 项目状态对应关系 + +| 工单状态 | 项目认证状态 | +|---------|------------| +| `assigned` / `in_progress` | `draft` (草稿) | +| `pending_review` | `pending` (待审核) | +| `approved` / `closed` | `official` (正式) | +| `returned` | `draft` (草稿) | + +--- + +## 2. 状态机 + +### 2.1 状态定义 + +| 状态代码 | 状态名称 | 说明 | +|---------|---------|------| +| `created` | 已创建 | 项目创建,工单生成 | +| `assigned` | 已分配 | 已分配填写人 | +| `in_progress` | 填写中 | 用户正在填写项目信息 | +| `draft_saved` | 草稿已保存 | 扩展状态:草稿保存成功 | +| `pending_review` | 待审核 | 填写完成,等待审核 | +| `approved` | 已通过 | 审核通过,项目正式上线 | +| `returned` | 已打回 | 审核打回,需重新填写 | +| `closed` | 已关闭 | 工单生命周期结束 | + +### 2.2 状态机图 + +```mermaid +stateDiagram-v2 + [*] --> created: 创建项目 + + created --> assigned: 分配填写人 + + assigned --> in_progress: 用户开始填写 + assigned --> assigned: 重新分配 + + in_progress --> in_progress: 保存草稿 + in_progress --> pending_review: 提交审核 + in_progress --> assigned: 重新分配 + + pending_review --> approved: 审批通过 + pending_review --> returned: 审批打回 + + returned --> in_progress: 用户重新填写 + returned --> pending_review: 重新提交 + + approved --> closed: 关闭工单 + + closed --> [*] + + note right of approved: 项目状态变为official + note right of in_progress: 支持多次保存草稿 +``` + +--- + +## 3. 流程图 + +```mermaid +flowchart TD + Start([开始]) --> CreateProject[超管创建项目元数据] + CreateProject --> CreateWorkflow[自动创建项目详情工单] + CreateWorkflow --> SelectFiller[选择项目信息填写人] + SelectFiller --> AssignWorkflow[工单分配给填写人] + + AssignWorkflow --> NotifyFiller[通知填写人] + NotifyFiller --> UserAccept[填写人接受任务] + + UserAccept --> FillDetail[填写项目详情信息] + FillDetail --> SaveDraft{保存操作} + + SaveDraft -->|保存草稿| DraftSaved[草稿保存成功] + DraftSaved --> FillDetail + + SaveDraft -->|提交审核| ValidateRequired{必填项校验} + ValidateRequired -->|校验失败| FillDetail + ValidateRequired -->|校验通过| SubmitReview[提交审核] + + SubmitReview --> PendingReview{待审核} + + PendingReview -->|审批通过| Approve[审批通过] + PendingReview -->|审批打回| Return[打回修改] + PendingReview -->|超管直接修改| AdminModify[超管修改信息] + + Approve --> SetOfficial[项目状态设为official] + SetOfficial --> CloseWorkflow[关闭工单] + CloseWorkflow --> End([结束]) + + Return --> BackToFill[回到填写阶段] + BackToFill --> FillDetail + + AdminModify --> SetOfficial + + %% 重新分配分支 + AssignWorkflow --> ReassignCheck{需要重新分配?} + ReassignCheck -->|是| SelectFiller + ReassignCheck -->|否| NotifyFiller +``` + +--- + +## 4. 时序图 + +```mermaid +sequenceDiagram + autonumber + + participant Admin as 超级管理员 + participant Frontend as 前端 + participant Project as rmdc-project-management + participant Workflow as rmdc-work-procedure + participant User as 填写人 + participant Notice as 通知中心 + + %% 项目创建阶段 + Admin->>Frontend: 创建项目 + Frontend->>Project: POST /api/project/create + Note over Project: 创建项目元数据
status=draft + Project->>Workflow: POST /api/workflow/create + Note over Workflow: 创建project_detail工单
保存detail_filler_id + Workflow-->>Project: 返回工单ID + Project->>Project: 关联工单ID + Project-->>Frontend: 项目创建成功 + + %% 分配填写人 + Admin->>Frontend: 选择填写人 + Frontend->>Workflow: POST /api/workflow/assign + Workflow->>Workflow: 设置status=assigned + Workflow->>Notice: 推送通知 + Notice-->>User: 项目信息填写通知 + + %% 用户填写阶段 + User->>Frontend: 进入项目详情页 + Frontend->>Project: POST /api/project/detail + Project-->>Frontend: 返回项目信息+草稿数据 + + loop 填写并保存草稿 + User->>Frontend: 填写项目信息 + Frontend->>Project: POST /api/project/draft/save + Project->>Workflow: 更新draft_data + Project-->>Frontend: 草稿保存成功 + end + + %% 提交审核 + User->>Frontend: 点击提交审核 + Frontend->>Project: POST /api/project/submit + Note over Project: 校验必填项 + Project->>Workflow: POST /api/workflow/transition + Note over Workflow: event=complete + Workflow->>Workflow: 设置status=pending_review + Workflow->>Notice: 推送通知 + Notice-->>Admin: 项目详情审核通知 + + %% 审批场景 + alt 审批通过 + Admin->>Frontend: 点击通过 + Frontend->>Workflow: POST /api/workflow/transition + Note over Workflow: event=approve + Workflow->>Project: 回调更新项目状态 + Project->>Project: 设置status=official + Workflow->>Workflow: 设置status=approved + Workflow->>Notice: 推送通知 + Notice-->>User: 项目审核通过 + + else 审批打回 + Admin->>Frontend: 点击打回 + Frontend->>Workflow: POST /api/workflow/transition + Note over Workflow: event=return
附带修改意见 + Workflow->>Workflow: 设置status=returned + Workflow->>Notice: 推送通知 + Notice-->>User: 项目被打回 + + User->>Frontend: 根据意见修改 + Frontend->>Project: POST /api/project/draft/save + Frontend->>Workflow: POST /api/workflow/transition + Note over Workflow: event=resubmit + Workflow->>Workflow: 设置status=pending_review + + else 超管直接修改 + Admin->>Frontend: 直接修改项目信息 + Frontend->>Project: POST /api/project/update + Project->>Workflow: POST /api/workflow/transition + Note over Workflow: event=approve
超管直接完成 + Workflow->>Workflow: 设置status=approved + end +``` + +--- + +## 5. 工单步骤定义 + +| 步骤序号 | 步骤名称 | 步骤类型 | 处理人 | 操作说明 | +|---------|---------|---------|-------|---------| +| 1 | 分配填写人 | `assign` | 超级管理员 | 选择项目信息填写人 | +| 2 | 填写项目信息 | `execute` | 被分配用户 | 填写项目详情,可保存草稿 | +| 3 | 审核项目信息 | `approve` | 超级管理员 | 审批通过/打回 | + +--- + +## 6. 业务扩展字段 + +存储于 `project_detail_ext` 表: + +| 字段 | 类型 | 说明 | +|-----|------|------| +| `project_id` | VARCHAR(64) | 关联项目ID | +| `detail_filler_id` | BIGINT | 填写人ID | +| `detail_filler_name` | VARCHAR(64) | 填写人姓名 | +| `draft_data` | JSON | 草稿数据 | + +### 6.1 draft_data 示例 + +```json +{ + "deploy_business": { + "deployer_name": "张三", + "deployer_phone": "13800138000", + "deploy_start_time": "2026-01-01", + "system_version": "v3.2.0" + }, + "deploy_env": { + "network_type": "internal", + "main_public_ip": "10.0.0.1", + "host_count": 3 + }, + "deploy_middleware": { + "mysql": { + "internal_ip": "10.0.0.10", + "internal_port": 3306 + } + }, + "last_saved_at": "2026-01-08T10:30:00Z" +} +``` diff --git a/8-CMII-RMDC/7-rmdc-work-procedure/diagrams/user-management-workflow.md b/8-CMII-RMDC/7-rmdc-work-procedure/diagrams/user-management-workflow.md new file mode 100644 index 0000000..7ceb6a1 --- /dev/null +++ b/8-CMII-RMDC/7-rmdc-work-procedure/diagrams/user-management-workflow.md @@ -0,0 +1,234 @@ +# 用户管理工单流程 + +**工单类型代码**: `user_management` +**来源模块**: rmdc-user-auth +**编制日期**: 2026-01-08 + +--- + +## 1. 业务场景 + +非超级管理员用户对已注册用户进行管理操作(修改、删除、启用、禁用)时,需要经过超级管理员审批。 + +### 1.1 业务规则 + +| 规则项 | 说明 | +|-------|------| +| 发起人 | 管理员、普通用户(只能管理自己注册的用户) | +| 默认处理人 | 超级管理员 | +| 操作类型 | `modify`(修改)、`delete`(删除)、`enable`(启用)、`disable`(禁用) | +| 可撤销 | 是(创建人可撤销) | +| 可关闭 | 是(创建人可主动关闭) | + +### 1.2 权限矩阵 + +| 操作人角色 | 可管理用户范围 | +|-----------|--------------| +| 超级管理员 | 所有用户(无需工单) | +| 管理员 | 普通用户、三方用户(自己注册的) | +| 普通用户 | 三方用户(自己注册的) | +| 三方用户 | 无 | + +--- + +## 2. 状态机 + +### 2.1 状态定义 + +| 状态代码 | 状态名称 | 说明 | +|---------|---------|------| +| `created` | 已创建 | 发起用户管理操作,工单创建 | +| `pending_review` | 待审核 | 等待超级管理员审核 | +| `approved` | 已通过 | 审核通过,管理操作已执行 | +| `rejected` | 已拒绝 | 审核拒绝 | +| `returned` | 已打回 | 打回修改 | +| `revoked` | 已撤销 | 用户撤销管理操作 | +| `closed` | 已关闭 | 工单生命周期结束 | + +### 2.2 状态机图 + +```mermaid +stateDiagram-v2 + [*] --> created: 发起管理操作 + + created --> pending_review: 提交审核 + created --> revoked: 用户撤销 + created --> closed: 用户关闭 + + pending_review --> approved: 超管审批通过 + pending_review --> rejected: 超管审批拒绝 + pending_review --> returned: 超管打回 + pending_review --> revoked: 用户撤销(需确认) + + returned --> pending_review: 重新提交 + returned --> revoked: 用户撤销 + returned --> closed: 用户关闭 + + rejected --> closed: 关闭工单 + approved --> closed: 关闭工单 + revoked --> closed: 关闭工单 + + closed --> [*] + + note right of approved: 执行管理操作 + note right of closed: 退出用户编辑状态 +``` + +--- + +## 3. 流程图 + +```mermaid +flowchart TD + Start([开始]) --> InitiateManage[发起用户管理操作] + InitiateManage --> SelectAction{选择操作类型} + + SelectAction -->|修改| ModifyAction[修改用户信息] + SelectAction -->|删除| DeleteAction[删除用户] + SelectAction -->|启用| EnableAction[启用用户] + SelectAction -->|禁用| DisableAction[禁用用户] + + ModifyAction --> CreateWorkflow[创建管理工单] + DeleteAction --> CreateWorkflow + EnableAction --> CreateWorkflow + DisableAction --> CreateWorkflow + + CreateWorkflow --> EnterEditMode[用户进入编辑状态] + EnterEditMode --> SubmitReview[提交审核] + + SubmitReview --> PendingReview{待审核} + + PendingReview -->|审批通过| Approve[审批通过] + PendingReview -->|审批拒绝| Reject[直接拒绝] + PendingReview -->|打回| Return[打回修改] + PendingReview -->|用户撤销| Revoke[撤销工单] + + Approve --> ExecuteAction[执行管理操作] + ExecuteAction --> UpdateDB[更新数据库] + UpdateDB --> ExitEditMode1[退出编辑状态] + ExitEditMode1 --> CloseApproved[关闭工单] + CloseApproved --> End([结束]) + + Return --> UserModify{用户修改操作} + UserModify -->|重新提交| SubmitReview + UserModify -->|放弃操作| CloseFromReturn[关闭工单] + + Reject --> ExitEditMode2[退出编辑状态] + ExitEditMode2 --> ClosedRejected[关闭工单] + ClosedRejected --> End + + Revoke --> ExitEditMode3[退出编辑状态] + ExitEditMode3 --> ClosedRevoked[关闭工单] + ClosedRevoked --> End + + CloseFromReturn --> ExitEditMode4[退出编辑状态] + ExitEditMode4 --> End +``` + +--- + +## 4. 时序图 + +```mermaid +sequenceDiagram + autonumber + + participant User as 管理人 + participant Frontend as 前端 + participant UserAuth as rmdc-user-auth + participant Workflow as rmdc-work-procedure + participant SuperAdmin as 超级管理员 + participant Notice as 通知中心 + + %% 发起管理操作 + User->>Frontend: 点击编辑/删除/启用/禁用 + Frontend->>UserAuth: 校验管理权限 + UserAuth-->>Frontend: 权限校验通过 + Frontend->>Workflow: POST /api/workflow/create + Note over Workflow: 创建user_management工单
保存original_data快照 + Workflow->>UserAuth: 标记用户进入编辑状态 + Workflow-->>Frontend: 工单创建成功 + Frontend-->>User: 进入编辑模式 + + %% 用户修改并提交 + User->>Frontend: 修改信息/确认操作 + Frontend->>Workflow: POST /api/workflow/transition + Note over Workflow: event=submit
保存modified_data + Workflow->>Workflow: 设置status=pending_review + Workflow->>Notice: 推送通知 + Notice-->>SuperAdmin: 用户管理审批通知 + + %% 审批通过场景 + alt 审批通过 + SuperAdmin->>Frontend: 查看工单详情 + Frontend->>Workflow: POST /api/workflow/detail + Workflow-->>Frontend: 返回工单信息+变更对比 + SuperAdmin->>Frontend: 点击通过 + Frontend->>Workflow: POST /api/workflow/transition + Note over Workflow: event=approve + Workflow->>UserAuth: 回调执行管理操作 + UserAuth->>UserAuth: 根据action_type执行操作 + Workflow->>UserAuth: 清除用户编辑状态 + Workflow->>Workflow: 设置status=approved + Workflow->>Notice: 推送通知 + Notice-->>User: 管理操作已通过 + + %% 打回场景 + else 打回修改 + SuperAdmin->>Frontend: 点击打回 + Frontend->>Workflow: POST /api/workflow/transition + Note over Workflow: event=return
附带修改意见 + Workflow->>Workflow: 设置status=returned + Workflow->>Notice: 推送通知 + Notice-->>User: 管理操作被打回 + + User->>Frontend: 根据意见修改 + Frontend->>Workflow: POST /api/workflow/transition + Note over Workflow: event=resubmit + Workflow->>Workflow: 设置status=pending_review + + %% 用户关闭场景 + else 用户主动关闭 + User->>Frontend: 点击关闭工单 + Frontend->>Workflow: POST /api/workflow/close + Workflow->>UserAuth: 清除用户编辑状态 + Workflow->>Workflow: 设置status=closed + Note over Workflow: 放弃所有修改 + end +``` + +--- + +## 5. 工单步骤定义 + +| 步骤序号 | 步骤名称 | 步骤类型 | 处理人 | 操作说明 | +|---------|---------|---------|-------|---------| +| 1 | 提交管理操作 | `submit` | 创建人 | 提交管理操作审核 | +| 2 | 审核管理操作 | `approve` | 超级管理员 | 审批通过/拒绝/打回 | + +--- + +## 6. 业务扩展字段 + +存储于 `user_management_ext` 表: + +| 字段 | 类型 | 说明 | +|-----|------|------| +| `target_user_id` | BIGINT | 被管理用户ID | +| `action_type` | VARCHAR(32) | 操作类型:modify/delete/enable/disable | +| `original_data` | JSON | 变更前数据快照 | +| `modified_data` | JSON | 期望变更数据 | + +### 6.1 original_data / modified_data 示例 + +```json +{ + "username": "zhangsan", + "chinese_name": "张三", + "email": "zhangsan@example.com", + "phone": "13800138000", + "group_name": "产品与解决方案组", + "dev_role": "backend", + "rmdc_role": "normal" +} +``` diff --git a/8-CMII-RMDC/7-rmdc-work-procedure/diagrams/user-registration-workflow.md b/8-CMII-RMDC/7-rmdc-work-procedure/diagrams/user-registration-workflow.md new file mode 100644 index 0000000..e298182 --- /dev/null +++ b/8-CMII-RMDC/7-rmdc-work-procedure/diagrams/user-registration-workflow.md @@ -0,0 +1,190 @@ +# 用户注册工单流程 + +**工单类型代码**: `user_registration` +**来源模块**: rmdc-user-auth +**编制日期**: 2026-01-08 + +--- + +## 1. 业务场景 + +非超级管理员用户注册新用户后,需要经过超级管理员审批才能激活用户账号。 + +### 1.1 业务规则 + +| 规则项 | 说明 | +|-------|------| +| 发起人 | 管理员、普通用户(根据权限可注册不同角色账号) | +| 默认处理人 | 超级管理员 | +| 被注册用户初始状态 | `disabled` | +| 审批通过后用户状态 | `active` | +| 可撤销 | 是(创建人可撤销) | +| 撤销后处理 | 硬删除被注册用户 | + +--- + +## 2. 状态机 + +### 2.1 状态定义 + +| 状态代码 | 状态名称 | 说明 | +|---------|---------|------| +| `created` | 已创建 | 用户注册完成,工单自动创建 | +| `pending_review` | 待审核 | 等待超级管理员审核 | +| `approved` | 已通过 | 审核通过,用户已激活 | +| `rejected` | 已拒绝 | 审核拒绝,需重新修改 | +| `revoked` | 已撤销 | 用户取消注册 | +| `closed` | 已关闭 | 工单生命周期结束 | + +### 2.2 状态机图 + +```mermaid +stateDiagram-v2 + [*] --> created: 用户注册 + + created --> pending_review: 自动提交审核 + created --> revoked: 用户撤销 + + pending_review --> approved: 超管审批通过 + pending_review --> rejected: 超管审批拒绝 + pending_review --> revoked: 用户撤销(需确认) + + rejected --> pending_review: 用户修改后重新提交 + rejected --> revoked: 用户撤销 + + approved --> closed: 关闭工单 + rejected --> closed: 关闭工单 + revoked --> closed: 关闭工单 + + closed --> [*] + + note right of approved: 自动激活用户 + note right of revoked: 硬删除用户 +``` + +--- + +## 3. 流程图 + +```mermaid +flowchart TD + Start([开始]) --> Register[用户注册新账号] + Register --> CreateWorkflow[自动创建注册工单] + CreateWorkflow --> SetDisabled[设置用户状态为disabled] + SetDisabled --> AutoSubmit[自动提交给超级管理员] + + AutoSubmit --> PendingReview{待审核} + + PendingReview -->|审批通过| Approve[审批通过] + PendingReview -->|审批拒绝| Reject[打回修改] + PendingReview -->|用户撤销| RevokeConfirm{确认撤销?} + + Approve --> ActivateUser[激活用户账号] + ActivateUser --> CloseApproved[关闭工单] + CloseApproved --> End([结束]) + + Reject --> UserModify{用户修改信息} + UserModify -->|重新提交| AutoSubmit + UserModify -->|放弃注册| RevokeFromReject[撤销工单] + + RevokeConfirm -->|确认| Revoke[撤销工单] + RevokeConfirm -->|取消| PendingReview + + Revoke --> DeleteUser[硬删除用户] + RevokeFromReject --> DeleteUser + DeleteUser --> ClosedRevoked[关闭工单] + ClosedRevoked --> RestartOption{一键重新创建?} + + RestartOption -->|是| Register + RestartOption -->|否| End +``` + +--- + +## 4. 时序图 + +```mermaid +sequenceDiagram + autonumber + + participant User as 注册人 + participant Frontend as 前端 + participant UserAuth as rmdc-user-auth + participant Workflow as rmdc-work-procedure + participant SuperAdmin as 超级管理员 + participant Notice as 通知中心 + + %% 用户注册阶段 + User->>Frontend: 填写注册表单 + Frontend->>UserAuth: POST /api/user/register + UserAuth->>UserAuth: 创建用户(status=disabled) + UserAuth->>Workflow: POST /api/workflow/create + Note over Workflow: 创建user_registration工单 + Workflow->>Workflow: 设置status=pending_review + Workflow->>Notice: 推送通知 + Notice-->>SuperAdmin: 新用户注册审批通知 + UserAuth-->>Frontend: 注册成功,待审批 + Frontend-->>User: 显示等待审批状态 + + %% 审批通过场景 + alt 审批通过 + SuperAdmin->>Frontend: 查看工单详情 + Frontend->>Workflow: POST /api/workflow/detail + Workflow-->>Frontend: 返回工单信息 + SuperAdmin->>Frontend: 点击通过 + Frontend->>Workflow: POST /api/workflow/transition + Note over Workflow: event=approve + Workflow->>UserAuth: 回调更新用户状态 + UserAuth->>UserAuth: 更新status=active + Workflow->>Workflow: 设置status=approved + Workflow->>Notice: 推送通知 + Notice-->>User: 注册已通过通知 + + %% 审批拒绝场景 + else 审批拒绝 + SuperAdmin->>Frontend: 点击拒绝 + Frontend->>Workflow: POST /api/workflow/transition + Note over Workflow: event=reject + Workflow->>Workflow: 设置status=rejected + Workflow->>Notice: 推送通知 + Notice-->>User: 注册被拒绝通知 + + %% 用户重新提交 + User->>Frontend: 修改注册信息 + Frontend->>UserAuth: POST /api/user/update + Frontend->>Workflow: POST /api/workflow/transition + Note over Workflow: event=resubmit + Workflow->>Workflow: 设置status=pending_review + Workflow->>Notice: 推送通知 + Notice-->>SuperAdmin: 重新提交审批通知 + + %% 用户撤销场景 + else 用户撤销 + User->>Frontend: 点击撤销注册 + Frontend->>Workflow: POST /api/workflow/revoke + Workflow->>Workflow: 设置status=revoked + Workflow->>UserAuth: 回调删除用户 + UserAuth->>UserAuth: 硬删除用户记录 + Workflow->>Workflow: 设置status=closed + end +``` + +--- + +## 5. 工单步骤定义 + +| 步骤序号 | 步骤名称 | 步骤类型 | 处理人 | 操作说明 | +|---------|---------|---------|-------|---------| +| 1 | 提交注册 | `submit` | 系统自动 | 用户注册后自动提交审核 | +| 2 | 审核注册 | `approve` | 超级管理员 | 审批通过/拒绝/打回 | + +--- + +## 6. 业务扩展字段 + +存储于 `user_registration_ext` 表: + +| 字段 | 类型 | 说明 | +|-----|------|------| +| `target_user_id` | BIGINT | 被注册用户ID | +| `original_status` | VARCHAR(32) | 原用户状态(备份) | diff --git a/8-CMII-RMDC/8-rmdc-monitor-center/9-user-contact-list.md b/8-CMII-RMDC/8-rmdc-monitor-center/9-user-contact-list.md new file mode 100644 index 0000000..028d14a --- /dev/null +++ b/8-CMII-RMDC/8-rmdc-monitor-center/9-user-contact-list.md @@ -0,0 +1,121 @@ +| 序号 | 姓名 | 工号 | 手机 | 短号 | 邮箱 | 所属小组 | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| 1 | 周剑 | E0003260015 | 13980000043 | 63802 | zhoujian@cmii.chinamobile.com | 低空经济技术研发运营中心 | +| 2 | 李天扬 | E1000119593 | 18200367441 | | litianyang@cmii.chinamobile.com | 低空经济技术研发运营中心 | +| 3 | 陈灵美 | E1000119592 | 18884838475 | | chenlingmei@cmii.chinamobile.com | 低空经济技术研发运营中心 | +| 4 | 杨子仪 | E1000119468 | 17336343307 | | yangziyi@cmii.chinamobile.com | 低空经济技术研发运营中心 | +| 5 | 马建均 | E0003260135 | 18802881983 | 65125 | majianjun@cmii.chinamobile.com | 低空经济技术研发运营中心 | +| 6 | 彭国宇 | E1000120189 | 13648063032 | | pengguoyu@cmii.chinamobile.com | 低空经济技术研发运营中心 | +| 7 | 韩雨亭 | E1000039626 | 18802880189 | 63810 | hanyuting@cmii.chinamobile.com | 产品与解决方案组 | +| 8 | 张震 | E0003260280 | 18802880227 | 63811 | zhangzhen@cmii.chinamobile.com | 产品与解决方案组 | +| 9 | 黄佳伟 | E1000039533 | 18802880662 | 63824 | huangjiawei@cmii.chinamobile.com | 产品与解决方案组 | +| 10 | 杨瑶 | E1000047533 | 18428381971 | 63822 | yangyao01@cmii.chinamobile.com | 产品与解决方案组 | +| 11 | 刘子扬 | E0003260069 | 18802880817 | 63887 | liuziyang@cmii.chinamobile.com | 产品与解决方案组 | +| 12 | 张淼 | E1000000251 | 18782209849 | 63860 | zhangmiao@cmii.chinamobile.com | 产品与解决方案组 | +| 13 | 徐源林 | E1000041972 | 18382354080 | 63888 | xuyuanlin@cmii.chinamobile.com | 产品与解决方案组 | +| 14 | 何博 | E1000094217 | 13550064201 | 63955 | hebo@cmii.chinamoble.com | 产品与解决方案组 | +| 15 | 严朝阳 | E1000041760 | 18802881969 | 65691 | yanchaoyang@cmii.chinamobile.com | 产品与解决方案组 | +| 16 | 童钰辉 | E1000088894 | 18802880389 | 63914 | tongyuhui@cmii.chinamobile.com | 产品与解决方案组 | +| 17 | 刘庭璇 | E1000039627 | 18428359971 | 63932 | liutingxuan@cmii.chinamobile.com | 产品与解决方案组 | +| 18 | 郭忠勇 | E1000042092 | 18224071422 | 63895 | guozhongyong@cmii.chinamobile.com | 产品与解决方案组 | +| 19 | 黄知齐 | E1000111665 | 18380104927 | 63929 | huangzhiqi@cmii.chinamobile.com | 产品与解决方案组 | +| 20 | 傅军 | E1000029753 | 15208325190 | 63842 | fujun@cmii.chinamobile.com | 产品与解决方案组 | +| 21 | 牛锐 | E0003811308 | 13910388292 | | niurui@cmii.chinamobile.com | 战略组 | +| 22 | 程倩倩 | E1000047233 | 18802880930 | 63865 | chengqianqian@cmii.chinamobile.com | 战略组 | +| 23 | 林汲 | E1000094622 | 18884817362 | 63920 | linji@cmii.chinamobile.com | 战略组 | +| 24 | 兰盾 | E0003260413 | 13808205185 | 63805 | landun@cmii.chinamobile.com | 监管军团 | +| 25 | 唐兵 | E0003260554 | 18202806037 | 63876 | tangbing@cmii.chinamobile.com | 监管军团 | +| 26 | 邓玖根 | E1000047135 | 15208489298 | 63877 | dengjiugen@cmii.chinamobile.com | 监管军团 | +| 27 | 刘家琛 | E1000093002 | 18215599792 | 63917 | liujiachen@cmii.chinamobile.com | 监管军团 | +| 28 | 陈宽 | E1000112978 | 19982088679 | 63931 | chenkuan@cmii.chinamobile.com | 监管军团 | +| 29 | 徐晓东 | E0038015091 | 13708024468 | 63892 | xuxiaodong@cmii.chinamobile.com | 监管军团 | +| 30 | 李航宇 | E0038009499 | 15881102865 | 63834 | lihangyu@cmii.chinamobile.com | 监管军团 | +| 31 | 黄益 | E0003260227 | 18244215251 | 63894 | huangyi01@cmii.chinamobile.com | 监管军团 | +| 32 | 张愉菲 | E1000092739 | 18280136310 | 63915 | zhangyufei@cmii.chinamobile.com | 监管军团 | +| 33 | 郑文龙 | E1000047230 | 13890436063 | 63878 | zhengwenlong@cmii.chinamobile.com | 监管军团 | +| 34 | 何孝游 | E1000041675 | 18380417399 | 63836 | hexiaoyou@cmii.chinamobile.com | 监管军团 | +| 35 | 王历 | E1000050130 | 18802881952 | 63875 | wangli02@cmii.chinamobile.com | 监管军团 | +| 36 | 彭璐 | E1000030112 | 13688161465 | 63806 | penglu@cmii.chinamobile.com | 创新研究与应用组(PMO) | +| 37 | 成均保 | E0003260372 | 18802881118 | 63815 | chengjunbao@cmii.chinamobile.com | 创新研究与应用组(PMO) | +| 38 | 管泽鑫 | E1000048417 | 18802881168 | 63882 | guanzexin@cmii.chinamobile.com | 创新研究与应用组(PMO) | +| 39 | 左莹莹 | E0003260324 | 18802881112 | 63812 | zuoyingying@cmii.chinamobile.com | 创新研究与应用组(PMO) | +| 40 | 王艺霖 | E1000047239 | 18408218668 | 63813 | wangyilin@cmii.chinamobile.com | 创新研究与应用组(PMO) | +| 41 | 孔祥 | E1000046205 | 18628265601 | 63884 | kongxiang@cmii.chinamobile.com | 创新研究与应用组(PMO) | +| 42 | 李垚 | E1000044408 | 18200532206 | 63883 | liyao@cmii.chinamobile.com | 创新研究与应用组(PMO) | +| 43 | 向昱佼 | E0003260048 | 18802880204 | 63885 | xiangyujiao@cmii.chinamobile.com | 创新研究与应用组(PMO) | +| 44 | 邱裕鹤 | E0003260241 | 18802880201 | 63803 | qiuyuhe@cmii.chinamobile.com | 云平台系统组 | +| 45 | 任一珂 | E1000000224 | 13558770317 | 63833 | renyike@cmii.chinamobile.com | 监视控制组 | +| 46 | 吴云江 | E1000029758 | 13679007674 | 63880 | wuyunjiang@cmii.chinamobile.com | 监视控制组 | +| 47 | 李煜寒 | E1000041015 | 18802880525 | 63839 | liyuhan@cmii.chinamobile.com | 监视控制组 | +| 48 | 王少飞 | E1000042202 | 13438837100 | 63840 | wangshaofei@cmii.chinamobile.com | 监视控制组 | +| 49 | 王义元 | E1000042204 | 18802881214 | 63879 | wangyiyuan@cmii.chinamobile.com | 监视控制组 | +| 50 | 袁雪波 | E1000030093 | 15281059786 | 63862 | yuanxuebo@cmii.chinamobile.com | 监视控制组 | +| 51 | 张聪 | E0003260142 | 18802881839 | 63818 | zhangcong@cmii.chinamobile.com | 监视控制组 | +| 52 | 王子文 | E1000047232 | 18802881074 | 63864 | wangziwen@cmii.chinamobile.com | 监视控制组 | +| 53 | 罗瑞 | E0003260343 | 18802881217 | 63816 | luorui@cmii.chinamobile.com | 监视控制组 | +| 54 | 李松 | E1000000485 | 15882223415 | 63912 | lisong@cmii.chinamobile.com | 监视控制组 | +| 55 | 王清 | E1000000225 | 18215575866 | 63858 | wangqing@cmii.chinamobile.com | 智能云网组 | +| 56 | 刘喜 | E0003260164 | 15828477091 | 63866 | liuxi@cmii.chinamobile.com | 智能云网组 | +| 57 | 何虎 | E1000049890 | 13308059662 | 63841 | hehu@cmii.chinamobile.com | 智能云网组 | +| 58 | 罗文 | E1000039988 | 18780009301 | 63846 | luowen@cmii.chinamobile.com | 智能云网组 | +| 59 | 唐志梁 | E1000049663 | 18802880517 | 63847 | tangzhiliang@cmii.chinamobile.com | 智能云网组 | +| 60 | 钟林林 | E1000029771 | 15208192232 | 63844 | zhonglinlin@cmii.chinamobile.com | 智能云网组 | +| 61 | 胡宝顺 | E1000040553 | 15928589640 | 63835 | hubaoshun@cmii.chinamobile.com | 智能云网组 | +| 62 | 郝昊 | E1000109766 | 14708066381 | 63927 | haohao@cmii.chinamobile.com | 智能云网组 | +| 63 | 张雅洁 | E0003260313 | 18802880806 | 63820 | zhangyajie@cmii.chinamobile.com | 智能云网组 | +| 64 | 龙卫 | E1000036080 | 18280096573 | 63838 | longwei@cmii.chinamobile.com | 智能云网组 | +| 65 | 冉靖 | E1000103347 | 13540664346 | 63924 | ranjing@cmii.chinamobile.com | 智能云网组 | +| 66 | 张睿婧 | E1000103348 | 13689066043 | 63923 | zhangruijing@cmii.chinamobile.com | 智能云网组 | +| 67 | 郑岩 | E1000108860 | 18328603976 | 63928 | zhengyan@cmii.chinamobile.com | 智能云网组 | +| 68 | 刘芸志 | E1000030098 | 18802881811 | 63825 | liuyunzhi@cmii.chinamobile.com | 视觉交互组 | +| 69 | 谭雪敏 | E1000050960 | 13488938986 | 63826 | tanxuemin@cmii.chinamobile.com | 视觉交互组 | +| 70 | 王雄飞 | E1000036084 | 15108281409 | 63827 | wangxiongfei@cmii.chinamobile.com | 视觉交互组 | +| 71 | 罗婷婷 | E1000030087 | 18802880413 | 63828 | luotingting@cmii.chinamobile.com | 视觉交互组 | +| 72 | 李佩哲 | E1000036085 | 18328088043 | 63829 | lipeizhe@cmii.chinamobile.com | 视觉交互组 | +| 73 | 杨淳婷 | E0003260183 | 18802880215 | 63830 | yangchunting@cmii.chinamobile.com | 视觉交互组 | +| 74 | 李贞 | E0003260187 | 18384166196 | 63831 | lizhen@cmii.chinamobile.com | 视觉交互组 | +| 75 | 朱君妍 | E1000049635 | 13980501129 | 63821 | zhujunyan@cmii.chinamobile.com | 视觉交互组 | +| 76 | 梁运珠 | E1000049209 | 18200259225 | 63859 | liangyunzhu@cmii.chinamobile.com | 视觉交互组 | +| 77 | 汪珂丽 | E1000088899 | 18810909875 | | wangkeli@cmii.chinamobile.com | 空域计算组 | +| 78 | 秦正 | E1000029111 | 18802880226 | 63849 | qinzheng@cmii.chinamobile.com | 空域计算组 | +| 79 | 周金福 | E0003260323 | 18224495673 | 63855 | zhoujinfu@cmii.chinamobile.com | 空域计算组 | +| 80 | 张艾 | E1000029770 | 15208206929 | 63843 | zhangai@cmii.chinamobile.com | 空域计算组 | +| 81 | 李志杨 | E1000036309 | 18628380221 | 63845 | lizhiyang@cmii.chinamobile.com | 空域计算组 | +| 82 | 杨云猇 | E1000041171 | 13550397849 | 63848 | yangyunxiao@cmii.chinamobile.com | 空域计算组 | +| 83 | 赵子瑶 | E0003260332 | 18802880615 | 63817 | zhaoziyao@cmii.chinamobile.com | 空域计算组 | +| 84 | 刘晨虹 | E1000103357 | 19827801957 | 63922 | liuchenhong@cmii.chinamobile.com | 空域计算组 | +| 85 | 黄滟茹 | E1000103362 | 13693498572 | 63925 | huangyanru@cmii.chinamobile.com | 空域计算组 | +| 86 | 杨文彬 | E1000084157 | 15216616583 | 63916 | yangwenbin@cmii.chinamobile.com | 空域计算组 | +| 87 | 吴庆洲 | E1000048906 | 13880168124 | 63850 | wuqingzhou@cmii.chinamobile.com | Al+能力组 | +| 88 | 崔诚煜 | E1000047244 | 17358568202 | 63851 | cuichengyu@cmii.chinamobile.com | Al+能力组 | +| 89 | 段亚康 | E1000041886 | 18802881205 | 63853 | duanyakang@cmii.chinamobile.com | Al+能力组 | +| 90 | 米俊桦 | E1000047236 | 18780275860 | 63856 | mijunhua@cmii.chinamobile.com | Al+能力组 | +| 91 | 王玲玉 | E1000048430 | 18802880318 | 63857 | wanglingyu@cmii.chinamobile.com | Al+能力组 | +| 92 | 王水介 | E1000041170 | 18215623760 | 63837 | wangshuijie@cmii.chinamobile.com | Al+能力组 | +| 93 | 陈盛伟 | E0003260259 | 13730884296 | 63804 | chenshengwei@cmii.chinamobile.com | 应急军团 | +| 94 | 张雷 | E1000045186 | 13568880613 | 63867 | zhanglei@cmii.chinamobile.com | 应急军团 | +| 95 | 阳煦平 | E1000026734 | 18802880109 | 63869 | yangxuping@cmii.chinamobile.com | 应急军团 | +| 96 | 谭倩 | E1000040200 | 15198157192 | 63873 | tanqian@cmii.chinamobile.com | 应急军团 | +| 97 | 王文靖 | E0003260163 | 18802881201 | 63870 | wangwenjing@cmii.chinamobile.com | 应急军团 | +| 98 | 李帆 | E1000047536 | 15008406387 | 63872 | lifan01@cmii.chinamobile.com | 应急军团 | +| 99 | 曾辉 | E1000049874 | 18382023722 | 63871 | zenghui@cmii.chinamobile.com | 应急军团 | +| 100 | 刘东 | E0003260050 | 13981766521 | 63808 | liudong@cmii.chinamobile.com | 应急军团 | +| 101 | 李爽 | E1000029110 | 18683782070 | 63874 | lishuang@cmii.chinamobile.com | 应急军团 | +| 102 | 杨光平 | E1000000252 | 13881748089 | 63868 | yangguangping@cmii.chinamobile.com | 应急军团 | +| 103 | 袁嵩 | E1000093977 | 18802880885 | 63919 | yuansong@cmii.chinamobile.com | 应急军团 | +| 104 | 蔡为 | E1000026733 | 18802881450 | 63898 | caiwei@cmii.chinamobile.com | 应急军团 | +| 105 | 陶科翔 | E0038037028 | 13909008628 | 63890 | taokexiang@cmii.chinamobile.com | 行业组南区 | +| 106 | 刘桓良 | E0038016245 | 13540246242 | 63893 | liuhuanliang@cmii.chinamobile.com | 行业组南区 | +| 107 | 刘绍坤 | E0003901279 | 15828665201 | 63823 | liushaokun@cmii.chinamobile.com | 行业组南区 | +| 108 | 张杨 | E0003260053 | 13880606478 | 63809 | zhangyang@cmii.chinamobile.com | 行业组南区 | +| 109 | 卢禹杉 | E0003260398 | 18802881163 | 63889 | luyushan@cmii.chinamobile.com | 行业组南区 | +| 110 | 邹安洋 | E0003260149 | 15802893337 | 63891 | zouanyang@cmii.chinamobile.com | 行业组南区 | +| 111 | 闫少普 | E0003260162 | 18802881206 | 63886 | yanshaopu@cmii.chinamobile.com | 行业组南区 | +| 112 | 王佳章 | E1000047237 | 18802880882 | 63881 | wangjiazhang@cmii.chinamobile.com | 行业组南区 | +| 113 | 刘长杰 | E0003260049 | 13608177983 | 63807 | liuchangjie@cmii.chinamobile.com | 行业组北区 | +| 114 | 何源 | E0003260064 | 13666181777 | 63911 | heyuan@cmii.chinamobile.com | 行业组北区 | +| 115 | 覃小龙 | E0003260166 | 18802880145 | 63897 | qinxiaolong@cmii.chinamobile.com | 行业组北区 | +| 116 | 陈一溶 | E1000064296 | 15008491662 | 63930 | chenyirong@cmii.chinamobile.com | 行业组北区 | +| 117 | 唐飞 | E0003260158 | 18802880112 | 63899 | tangfei@cmii.chinamobile.com | 行业组北区 | +| 118 | 杨尧 | E0003260193 | 18802881998 | 63814 | yangyao@cmii.chinamobile.com | 行业组北区 | +| 119 | 贾金岩 | E0003260263 | 18802880610 | 63863 | jiajinyan@cmii.chinamobile.com | 行业组北区 | \ No newline at end of file diff --git a/8-CMII-RMDC/8-rmdc-monitor-center/user-access-control-verification.md b/8-CMII-RMDC/8-rmdc-monitor-center/user-access-control-verification.md new file mode 100644 index 0000000..1a5b0a7 --- /dev/null +++ b/8-CMII-RMDC/8-rmdc-monitor-center/user-access-control-verification.md @@ -0,0 +1,76 @@ +# 用户权限控制逻辑与验证步骤 + +## 权限控制逻辑 + +```mermaid +flowchart TD + A[用户发起注册] --> B{检查操作人角色} + B -->|超级管理员| C[直接创建用户 - 状态 active] + B -->|管理员| D{目标角色} + B -->|普通用户| E{目标角色} + B -->|三方用户| F[拒绝 - 无权限] + + D -->|normal/third| G[创建用户 - 状态 disabled
创建工单等待审批] + D -->|其他| F + + E -->|third| G + E -->|其他| F + + G --> H[超级管理员审批] + H -->|通过| I[用户状态改为 active] + H -->|拒绝| J[用户保持 disabled] +``` + +## 验证步骤 + +### 1. 用户注册工单测试 + +1. 使用 **非超级管理员** (如 admin 或 normal 用户) 登录。 +2. 对于 **Normal 用户**: + - 侧边栏点击 **用户管理**。 + - 点击 **新建用户**,应跳转到 `/user/register-user`。 + - 提交注册后,应跳转到 `/user/workflows` (我的工单)。 +3. 对于 **Admin 用户**: + - 进入 **系统管理 -> 用户管理**。 + - 点击 **新建用户**,应跳转到 `/admin/users/register`。 + - 提交注册后,应跳转到 `/admin/workflows` (工单管理)。 +4. 填写注册信息,点击提交。 + - 预期:提示“注册申请已提交”,不直接创建可用用户。 +5. 切换为 **超级管理员** (superadmin) 登录。 +6. 进入 **我的工单** 或工单管理页面,查看待审批的注册工单。 +7. 点击工单详情,查看注册信息是否正确显示。 +8. 审批通过工单。 + - 预期:工单状态变为 Approved,用户状态变为 active。 +9. 在用户管理列表中验证新用户已存在且为激活状态。 + +### 2. 用户广场与管理功能测试 + +1. **用户广场 (User Square)** + - 登录普通用户。 + - 侧边栏点击 **用户广场** (替代原通讯录)。 + - 验证页面展示是否类似项目列表(支持卡片/表格切换)。 + - 验证搜索和筛选功能是否正常。 + - 验证点击用户仅弹出详情,**无**编辑/删除按钮。 + - **数据验证**:应能看到所有 active 用户(包括其他管理员创建的),但看不到 active 以外的用户。 + +2. **用户管理 (User Management)** + - 侧边栏点击 **用户管理**。 + - **普通用户**:验证只能看到 **自己创建的** 用户(包括待审核、已禁用等状态)。不能看到别人创建的用户。 + - **管理员**:验证能看到所有用户。 + - 验证 **编辑/删除** 按钮是否出现,并能正常触发工单流程。 + +3. **用户注册 (User Registration)** + - 侧边栏点击 **用户管理 -> 新建用户**(或通过路由 `/user/register-user`)。 + - 验证是否进入注册页面。 + +### 3. 用户管理工单测试 + +1. 使用 **管理员** (admin) 登录。 +2. 在 **用户管理** 列表中,对一个 **普通用户** (normal) 或 **三方用户** (third) 进行编辑。 +3. 修改信息并填写 **变更理由**,点击更新。 + - 预期:提示“修改申请已提交”。 +4. 对同一用户点击 **删除** 按钮,填写 **删除理由**,点击确认。 + - 预期:提示“删除申请已提交”。 +5. 切换为 **超级管理员** (superadmin) 登录。 +6. 查看相应的管理工单,审批通过。 +7. 验证用户信息已更新或用户已被删除。 diff --git a/8-CMII-RMDC/9-rmdc-user-auth/1-user-auth-DDS.md b/8-CMII-RMDC/9-rmdc-user-auth/1-user-auth-DDS.md new file mode 100644 index 0000000..8aa355b --- /dev/null +++ b/8-CMII-RMDC/9-rmdc-user-auth/1-user-auth-DDS.md @@ -0,0 +1,112 @@ +# 用户认证模块 + +## 用户体系 +### 项目初始化数据 +1. [公司通信录](0-设计方案\9-rmdc-user-auth\9-user-contact-list.md)应该作为项目数据库初始化的数据导入到数据库中 + + +### 用户结构体 +1. 用户ID +2. 用户中文名 必须要真实姓名 + 1. 默认为[公司通信录]中的姓名 +3. 用户英文名 也是昵称 可以为任意 + 1. 默认为[公司通信录]中邮箱去除掉@之后的部分 +4. 用户头像ID avatar_id +5. 用户头像框ID avatar_frame_id +6. 性别 gender +7. 电子邮箱 +8. 手机号 +9. 短号 +10. 工号 work_id +11. 所属小组 group_name 产品与解决方案组 +12. 状态 user_status active, locked, disabled 默认为 locked 删除为 disabled +13. 注册人ID registered_by_id 被谁注册的 +14. 密码 需要保存的是加密后的密码 默认值是 supercyy.1加密后的hash值 +15. 密码过期时间 password_expires_at +16. 开发角色 dev_role 后端开发 backend, 前端开发 frontend, 测试工程师 test, 全干工程师 ops, 行业解决方案专家 seller, 未知 unknown 默认为 unknown +17. RMDC系统角色 rmdc_role 超级管理员,管理员,普通用户,三方用户 superadmin, admin, normal, third 默认为normal +18. 注册时间 +19. 更新时间 +20. 删除时间(设置为disabled的时间) + +## 页面功能 +### 用户管理 +1. 只有超级管理员具有此页面的权限 +1. 管理用户的信息 + 1. 新增用户 + 2. 修改用户信息 + 3. 删除用户 + 4. 启用注册用户 +2. 开启注册权限 + 1. 开放自己注册的功能 + 2. 用户自行注册之后的状态默认是 locked + +### 通信录页面 +1. normal用户及以上具有通信录页面的权限 +2. 展示部分用户的信息,方便联系其他的用户 +3. 默认使用列表的形式展示,点击用户之后可以查看用户详细信息 +4. 前端支持搜索,支持通过用户中文名,英文名 手机号 短号 快速搜索到用户 + +## 认证体系 +1. 用户使用默认密码登录之后, 强制要求修改密码 +2. 用户的密码有效期为3个月,过期需要强制修改密码 +3. 前端和后端之间密码的传输不能是明文的,需要进行RSA加密传输 +4. RMDC后端项目启动之后,需要创建一个RSA密钥对,并保存到数据库中 + 1. RSA密钥对的有效期应该设置为30天,启动之后应该解析密钥对,并保存到内存中 + 2. 如果密钥对过期,需要重新创建密钥对,并保存到数据库中 +5. 前端登录需要调用后端获取RSA公钥,然后使用公钥加密密码,再将加密后的密码发送给后端 +6. 后端接收到加密后的密码之后,使用自己的私钥解密密码,然后进行密码验证 + +## 授权体系 +1. 各个模块的授权均应该放置在此模块中进行 + +## 用户注册、管理功能 +2026年1月8日 结合工单系统,开发非超级管理员账号的注册功能 + +### 用户注册、管理权限 +1. RMDC角色为管理员的用户 + 1. 可以注册、管理(修改信息、启用、禁用、删除)普通用户权限的账户 + 2. 可以注册、管理(修改信息、启用、禁用、删除)三方用户权限的账户 +2. RMDC角色为普通用户的用户 + 1. 只能注册三方用户权限的账户 + 2. 不能注册普通用户权限的账户 + 3. 只能管理(修改信息、启用、禁用、删除)自己注册的三方用户权限的账户 +3. RMDC角色为三方用户的用户 + 1. 不能注册任何权限的账户 + 2. 不能管理(修改信息、启用、禁用、删除)任何用户权限的账户 +4. RMDC角色为超级管理员的用户 + 1. 可以注册任何权限的账户 + 2. 可以管理(修改信息、启用、禁用、删除)任何用户权限的账户 +5. 用户管理的原则为:谁注册谁管理 + 1. 用户管理页面的功能 + 2. 注册用户的注册人负责管理被注册用户的生命周期 + 3. 超级管理员负责审核所有用户注册的工单 + +### 用户注册工单流程 +1. 非超级管理员用户的在用户注册之后 + 1. 用户状态默认为disabled +2. 启动用户注册工单流程 + 1. 自动创建用户注册工单 + 2. 自动提交给超级管理员 +3. 超级管理员审核 + 1. 审核通过 + 1. 自动修改用户状态为 active, 用户启用 + 2. 审核不通过 + 1. 回退到用户注册阶段 + +### 用户管理工单流程 +1. 非超级管理员用户的在用户管理之后 +2. 启动用户管理工单流程 + 1. 自动创建用户管理工单 + 2. 自动提交给超级管理员 +3. 超级管理员审核 + 1. 审核通过 + 1. 根据管理的内容,修改用户相应的信息 + 2. 审核不通过 + 1. 回退到用户管理阶段 + +### 用户注册页面 +1. 请基于管理用户注册页面进行开发 +2. 设计的更加好看 + + \ No newline at end of file diff --git a/9-HomeGameCenter/1-原始需求/0-产品经理-prompt.md b/9-HomeGameCenter/1-原始需求/0-产品经理-prompt.md new file mode 100644 index 0000000..0e1ea36 --- /dev/null +++ b/9-HomeGameCenter/1-原始需求/0-产品经理-prompt.md @@ -0,0 +1,17 @@ +你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。 + +---- + +你是一名出色的产品经理,能够根据用户的初始需求,理解用户需求的真实需求意图,改善客户不够完善的需求,形成专业、简练的需求文档。并且能够在基础需求上优化产品的额设计和功能 + +请根据要求,研究jenkins的API,进行深度的思考,优化[1-初始需求稿.md],直接给出优化后的PRD + +---- + +你是一名出色的产品经理,请你根据[1-初始需求稿.md]客户的原始需求,审查[2-优化产品需求文档PRD.md],检查PRD文档是否完全满足原始需求;如果有更加优秀的设计方案,请给出修改建议 + +---- + +你是一名出色的产品经理,能够根据用户的初始需求,理解用户需求的真实需求意图,改善客户不够完善的需求,形成专业、简练的需求文档。并且能够在基础需求上优化产品的额设计和功能 + +你之前根据[1-初始需求稿.md]输出了[2-优化产品需求文档PRD.md], 目前初始需求有一些更新,见[1.1-初始需求稿.md],请你详细对比[1.1-初始需求稿.md]和[1-初始需求稿.md]之间的差异, 修改[2-优化产品需求文档PRD.md],得到新的需求文档 \ No newline at end of file diff --git a/9-HomeGameCenter/1-原始需求/1-初始需求稿.md b/9-HomeGameCenter/1-原始需求/1-初始需求稿.md new file mode 100644 index 0000000..e69de29 diff --git a/9-HomeGameCenter/1-原始需求/2-优化产品需求文档PRD.md b/9-HomeGameCenter/1-原始需求/2-优化产品需求文档PRD.md new file mode 100644 index 0000000..e69de29 diff --git a/9-HomeGameCenter/2-概要详细设计/0-概要设计prompt.md b/9-HomeGameCenter/2-概要详细设计/0-概要设计prompt.md new file mode 100644 index 0000000..463dc49 --- /dev/null +++ b/9-HomeGameCenter/2-概要详细设计/0-概要设计prompt.md @@ -0,0 +1,26 @@ +你是一名资深的软件系统架构师,具备以下核心职责与能力,绘图请使用mermaid语言: + +## 需求分析与理解 + +- 深度解读产品需求文档(PRD),识别业务目标、功能需求与非功能性需求 +- 分析需求间的依赖关系与优先级,识别潜在的技术风险与挑战 + +## 架构设计与规划 + +- 设计高可用、可扩展、高性能的系统架构,确保系统健壮性与安全性 +- 制定技术选型方案,包括开发语言、框架、中间件、数据库等关键技术栈 +- 绘制多层次架构图(系统架构图、数据流图、组件交互图) +- 定义模块划分、接口规范、数据模型与核心算法策略 +- 规划系统分层结构(展现层、业务层、数据层),明确各层职责边界 + +## 方案设计与输出 + +- 针对核心需求点提供详细的技术解决方案,包含实现路径与备选方案 +- 设计关键业务流程的时序图与状态机,确保逻辑清晰完整 +- 输出规范化的《系统详细设计说明书》,包含架构设计、接口定义、数据库设计等完整文档 + +请根据[2-优化产品需求文档PRD.md],按照上述的要求,输出系统详细设计说明书 + + + +你之前根据[2-优化产品需求文档PRD.md]输出了[3-详细设计说明书.md], 目前PRD需求有一些更新,见[2.1-优化产品需求文档PRD.md],请你详细对比[2.1-优化产品需求文档PRD.md]和[2-优化产品需求文档PRD.md]之间的差异, 修改[3-详细设计说明书.md],得到新的需求文档.要求尽量不改动[3-详细设计说明书.md]的初始设计,只改动差异化部分的设计 \ No newline at end of file diff --git a/9-HomeGameCenter/2-概要详细设计/3-详细设计说明书.md b/9-HomeGameCenter/2-概要详细设计/3-详细设计说明书.md new file mode 100644 index 0000000..e69de29 diff --git a/9-HomeGameCenter/3-实现详细稿/1-实现功能-prompt.md b/9-HomeGameCenter/3-实现详细稿/1-实现功能-prompt.md new file mode 100644 index 0000000..e69de29 diff --git a/98-AgentSkills/1-AgentSkills综述.md b/98-AgentSkills/1-AgentSkills综述.md new file mode 100644 index 0000000..c9bb194 --- /dev/null +++ b/98-AgentSkills/1-AgentSkills综述.md @@ -0,0 +1,257 @@ +# Claude Code Skills 模块构建与AI编程辅助指南 + +## Claude Code Skills 提示词模块化构建方法 + +**什么是Claude Code Skills?** Claude Code Skills(智能体技能)是由Anthropic推出的一种"**技能包**"机制,用于封装专业知识和工作流程,使大模型(如Claude)能够按需加载相应模块,完成特定领域的任务[\[1\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=%E4%BB%80%E4%B9%88%E6%98%AF%20Claude%20Skills%EF%BC%9F)[\[2\]](https://www.bilibili.com/opus/1132124852115734530#:~:text=Claude%20Skills%20%E6%9C%AC%E8%B4%A8%E4%B8%8A%E6%98%AF%E4%B8%80%E7%A7%8D,)。每个Skill本质上是一个**自包含的功能模块**,包含了**元数据、指令和资源**等要素。与传统一次性的大段 Prompt 不同,Skills采用**渐进式披露**架构,将上下文按需逐级加载,提高了上下文利用效率[\[3\]](https://claudecn.com/#:~:text=Agent%20Skills%20%E5%8F%AF%E5%A4%8D%E7%94%A8%E7%9A%84%E6%8A%80%E8%83%BD%E7%B3%BB%E7%BB%9F%EF%BC%8C%E8%AE%A9%20Claude%20%E6%8E%8C%E6%8F%A1%E7%89%B9%E5%AE%9A%E9%A2%86%E5%9F%9F%E4%B8%93%E4%B8%9A%E7%9F%A5%E8%AF%86%E3%80%82%E5%8C%85%E6%8B%AC,%E8%BF%9E%E6%8E%A5%E5%A4%96%E9%83%A8%E5%B7%A5%E5%85%B7%E5%92%8C%E6%95%B0%E6%8D%AE%E6%BA%90%EF%BC%8C%E6%89%A9%E5%B1%95%20Claude%20%E7%9A%84%E8%83%BD%E5%8A%9B%E8%BE%B9%E7%95%8C%EF%BC%8C%E6%9E%84%E5%BB%BA%E5%BC%BA%E5%A4%A7%E7%9A%84%20AI%20%E5%BA%94%E7%94%A8%E7%94%9F%E6%80%81%E3%80%82)[\[4\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=,3.2.2%20Markdown%20%E6%8C%87%E4%BB%A4%E6%AD%A3%E6%96%87)。换言之,我们可以把Claude Skills理解为AI工具箱中的插件,将反复使用的提示语和脚本封装成可复用模块,以便在需要时自动调用,从而避免每次手动重复提示[\[5\]](https://cloud.tencent.com/developer/article/2616585#:~:text=Claude%20Skills%E6%98%AFAnthropic%E6%8E%A8%E5%87%BA%E7%9A%84AI%E5%8A%9F%E8%83%BD%E9%9D%A9%E5%91%BD%EF%BC%8C%E5%8F%AF%E5%B0%86%E7%94%A8%E6%88%B7%E4%BD%BF%E7%94%A8AI%E7%9A%84%E4%B9%A0%E6%83%AF%E6%96%87%E4%BB%B6%E5%8C%96%E7%AE%A1%E7%90%86%E3%80%82%E5%AE%83%E8%83%BD%E8%A7%A3%E5%86%B3Claude%E5%81%A5%E5%BF%98%E3%80%81%E9%9C%80%E9%87%8D%E5%A4%8D%E6%8F%90%E7%A4%BA%E8%AF%8D%E7%9A%84%E9%97%AE%E9%A2%98%EF%BC%8C%E5%B0%86%E4%BB%BB%E5%8A%A1%E8%AF%B4%E6%98%8E%E3%80%81%E5%B7%A5%E5%85%B7%E4%BB%A3%E7%A0%81%E7%AD%89%E6%89%93%E5%8C%85%E6%88%90%20)[\[6\]](https://www.facebook.com/iamvista/photos/%E6%9F%90%E5%A4%A9%E6%B7%B1%E5%A4%9C%E6%88%91%E6%AD%A3%E5%9C%A8%E8%B6%95%E4%B8%80%E4%BB%BD%E6%96%87%E4%BB%B6%E5%A4%A9%E5%95%8A%E5%90%8C%E6%A8%A3%E7%9A%84%E6%9E%B6%E6%A7%8B%E5%90%8C%E6%A8%A3%E7%9A%84%E8%AA%9E%E6%B0%A3%E5%90%8C%E6%A8%A3%E7%9A%84%E6%A0%BC%E5%BC%8F%E8%A6%81%E6%B1%82%E4%BD%86%E6%88%91%E5%8F%88%E5%BE%97%E9%87%8D%E6%96%B0%E6%89%93%E4%B8%80%E6%AC%A1%E8%AB%8B%E7%94%A8%E9%80%99%E5%80%8B%E6%A0%BC%E5%BC%8F%E8%AB%8B%E5%85%88%E5%95%8F%E4%B8%89%E5%80%8B%E6%BE%84%E6%B8%85%E5%95%8F%E9%A1%8C%E8%AB%8B%E6%8A%8A%E8%BC%B8%E5%87%BA%E5%88%86%E6%88%90%E5%9B%9B%E6%AE%B5%E8%AB%8B%E9%99%84%E4%B8%8A%E5%8F%AF%E7%9B%B4%E6%8E%A5%E8%B2%BC%E5%88%B0-notion-%E7%9A%84/10162293093624053/#:~:text=,%E6%88%91%E6%80%8E%E9%BA%BC%E5%81%9A%E4%B8%80%E5%80%8B%E5%A5%BDskill%EF%BC%8C%E8%AE%8A%E6%88%90skill)。 + +**技能文件结构:** 每个Skill通常以独立文件夹形式存在,并包含一个核心定义文件(例如SKILL.md)以及可选的示例、模板和脚本子目录[\[7\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=%E6%8A%80%E8%83%BD%E7%BB%93%E6%9E%84%EF%BC%9A)。其基本结构如下(以my-skill为例)[\[8\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=my,%E8%BE%85%E5%8A%A9%E8%84%9A%E6%9C%AC%EF%BC%88%E5%8F%AF%E9%80%89%EF%BC%89): + +my-skill/ +├── SKILL.md # 技能定义(必需,含YAML元数据和Markdown内容) +├── examples/ # 示例文件(可选) +├── templates/ # 模板文件(可选) +└── scripts/ # 辅助脚本(可选) + +在SKILL.md中,开头使用YAML格式的**前置元数据(Skill Manifest)**描述技能信息,接着是Markdown格式的技能内容[\[9\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=%E6%8A%80%E8%83%BD%E5%AE%9A%E4%B9%89%E6%A0%BC%E5%BC%8F)。例如: + +\--- +name: my-skill-name +description: 用一句话清晰描述此技能的功能和适用场景 +version: 1.0.0 +author: 张三 +tags: \[类别1, 类别2\] +\--- +\# 我的技能名称 +
\## 目的 +这里解释该技能要解决的问题、使用场景。 +
\## 指令 +这里编写赋予AI的详细指令,指导其执行本技能涉及的任务步骤。 +
\## 示例 +\### 示例 1:基本用法 +输入:... +输出:... +
\### 示例 2:进阶用法 +输入:... +输出:... +
\## 指南 +\- 提示使用者的一些注意事项或最佳实践要点。 +
\## 限制 +\- 列出本技能的已知局限,如不会处理某些特殊情况等。 + +上述示例展示了技能定义模板的关键部分[\[10\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=,%E7%B1%BB%E5%88%AB2)[\[11\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=):YAML区块定义技能名称、描述、版本、作者和标签等元数据;随后Markdown正文按章节提供了技能的**用途说明**、AI执行的**详细指令**、使用**示例**以及指导和限制。元数据使技能模块**可版本化**和**可审计**,而指令部分则充当AI执行该技能的"剧本"。此外,开发者可以在scripts/目录中加入辅助脚本(如Python脚本)或在templates/加入模板文件,供AI在执行技能时调用[\[12\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=Anthropic%20Claude%20Skills%20%E6%98%AF%E5%8C%85%E5%90%AB%E6%8C%87%E4%BB%A4%E3%80%81%E8%84%9A%E6%9C%AC%E5%92%8C%E8%B5%84%E6%BA%90%E7%9A%84%E6%96%87%E4%BB%B6%E5%A4%B9%EF%BC%8CClaude%20%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E4%BB%A5%E6%8F%90%E9%AB%98%E4%B8%93%E4%B8%9A%E4%BB%BB%E5%8A%A1%E7%9A%84%E6%80%A7%E8%83%BD%E3%80%82)[\[13\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=)。这种结构使得每个Skill成为**可组合、可共享**的最小单元,方便团队协作和知识复用[\[14\]](https://github.com/wensia/xiaohongshu-poster-generator#:~:text=wensia%2Fxiaohongshu,Code%20%2B%20MCP%20%E7%9A%84%E5%B0%8F%E7%BA%A2%E4%B9%A6%E6%98%9F%E5%BA%A7%E5%86%85%E5%AE%B9%E8%87%AA%E5%8A%A8%E5%8C%96%E5%B7%A5%E4%BD%9C%E6%B5%81%EF%BC%8C%E6%94%AF%E6%8C%81%E6%B5%B7%E6%8A%A5%E7%94%9F%E6%88%90%E3%80%81%E7%88%86%E6%96%87%E5%88%86%E6%9E%90%E3%80%81%E5%86%85%E5%AE%B9%E5%8F%91%E5%B8%83%E7%AD%89%E5%8A%9F%E8%83%BD%E3%80%82%20%E5%8A%9F%E8%83%BD%E7%89%B9%E6%80%A7)。 + +**构建Skills的步骤(从入门到精通):** 对于初学者,推荐循序渐进掌握Claude Skills的制作与应用,可参考以下三步策略[\[15\]](https://blog.csdn.net/yangshangwei/article/details/156836796#:~:text=%E4%B8%80%E4%B8%AA%E5%8F%AF%E8%90%BD%E5%9C%B0%E7%9A%84%E4%B8%89%E6%AD%A5%E6%B3%95,3%20%E7%AC%AC%E4%B8%89%E6%AD%A5%EF%BC%9A%E6%8A%8AClaude%20%E5%BD%93%E2%80%9C%E6%90%AD%E5%AD%90%E2%80%9D%EF%BC%8C%E8%80%8C%E4%B8%8D%E6%98%AF%E6%90%9C%E7%B4%A2%E6%A1%86): + +- **直接复用现有技能:** **"拿来即用"**是上手Claude Skills最快的方法[\[15\]](https://blog.csdn.net/yangshangwei/article/details/156836796#:~:text=%E4%B8%80%E4%B8%AA%E5%8F%AF%E8%90%BD%E5%9C%B0%E7%9A%84%E4%B8%89%E6%AD%A5%E6%B3%95,3%20%E7%AC%AC%E4%B8%89%E6%AD%A5%EF%BC%9A%E6%8A%8AClaude%20%E5%BD%93%E2%80%9C%E6%90%AD%E5%AD%90%E2%80%9D%EF%BC%8C%E8%80%8C%E4%B8%8D%E6%98%AF%E6%90%9C%E7%B4%A2%E6%A1%86)。Anthropic官方提供了丰富的内置技能库(涵盖文档处理、代码开发、创意生成等),社区也涌现出大量共享技能包。初始阶段,先尝试调用这些**官方和社区现成Skills**来完成任务。例如使用/skill.docx快速生成Word报告,或/skill.xlsx分析Excel数据等[\[16\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=,%E9%A2%84%E7%AE%97%E6%98%8E%E7%BB%86%20%E5%90%AF%E7%94%A8%E4%BF%AE%E8%AE%A2%E8%B7%9F%E8%B8%AA%E4%BB%A5%E4%BE%9B%E5%AE%A1%E6%9F%A5)[\[17\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=)。通过观察这些技能的效果和实现方式,来体会技能的结构和用途。 +- **将重复任务技能化:** 当发现某类任务需要频繁向AI重复说明时,就可以考虑将其封装为自定义Skill[\[15\]](https://blog.csdn.net/yangshangwei/article/details/156836796#:~:text=%E4%B8%80%E4%B8%AA%E5%8F%AF%E8%90%BD%E5%9C%B0%E7%9A%84%E4%B8%89%E6%AD%A5%E6%B3%95,3%20%E7%AC%AC%E4%B8%89%E6%AD%A5%EF%BC%9A%E6%8A%8AClaude%20%E5%BD%93%E2%80%9C%E6%90%AD%E5%AD%90%E2%80%9D%EF%BC%8C%E8%80%8C%E4%B8%8D%E6%98%AF%E6%90%9C%E7%B4%A2%E6%A1%86)。经验法则是:**凡是你在对话中重复3次以上的提示**,都值得沉淀为技能模块。首先,整理该任务的明确步骤和专业知识要点,把你的**方法论**写下来[\[6\]](https://www.facebook.com/iamvista/photos/%E6%9F%90%E5%A4%A9%E6%B7%B1%E5%A4%9C%E6%88%91%E6%AD%A3%E5%9C%A8%E8%B6%95%E4%B8%80%E4%BB%BD%E6%96%87%E4%BB%B6%E5%A4%A9%E5%95%8A%E5%90%8C%E6%A8%A3%E7%9A%84%E6%9E%B6%E6%A7%8B%E5%90%8C%E6%A8%A3%E7%9A%84%E8%AA%9E%E6%B0%A3%E5%90%8C%E6%A8%A3%E7%9A%84%E6%A0%BC%E5%BC%8F%E8%A6%81%E6%B1%82%E4%BD%86%E6%88%91%E5%8F%88%E5%BE%97%E9%87%8D%E6%96%B0%E6%89%93%E4%B8%80%E6%AC%A1%E8%AB%8B%E7%94%A8%E9%80%99%E5%80%8B%E6%A0%BC%E5%BC%8F%E8%AB%8B%E5%85%88%E5%95%8F%E4%B8%89%E5%80%8B%E6%BE%84%E6%B8%85%E5%95%8F%E9%A1%8C%E8%AB%8B%E6%8A%8A%E8%BC%B8%E5%87%BA%E5%88%86%E6%88%90%E5%9B%9B%E6%AE%B5%E8%AB%8B%E9%99%84%E4%B8%8A%E5%8F%AF%E7%9B%B4%E6%8E%A5%E8%B2%BC%E5%88%B0-notion-%E7%9A%84/10162293093624053/#:~:text=,%E6%88%91%E6%80%8E%E9%BA%BC%E5%81%9A%E4%B8%80%E5%80%8B%E5%A5%BDskill%EF%BC%8C%E8%AE%8A%E6%88%90skill)。然后,利用Claude Code内置的**Skill Creator**功能,让AI协助你生成技能骨架[\[18\]](https://hbwdj.gov.cn/appbdetail-imqqsmrp9897358.d#:~:text=%E9%AA%97%E4%BD%A0%E7%9A%84%EF%BC%8C%E5%85%B6%E5%AE%9EAI%E6%A0%B9%E6%9C%AC%E4%B8%8D%E9%9C%80%E8%A6%81%E9%82%A3%E4%B9%88%E5%A4%9A%E6%8F%90%E7%A4%BA%E8%AF%8D%E3%80%82%20%E4%BD%A0%E5%8F%AA%E9%9C%80%E8%A6%81%E8%B0%83%E7%94%A8AI%20%E6%9C%AC%E8%BA%AB%E7%9A%84%E2%80%9CSkill%20Creator%E2%80%9D%E6%8A%80%E8%83%BD%EF%BC%8C%E7%94%A8%E4%BD%A0%E7%9A%84%E8%AF%AD%E8%A8%80%E6%8F%8F%E8%BF%B0%E8%87%AA%E5%B7%B1%E7%9A%84%E9%9C%80%E6%B1%82%EF%BC%8C%E8%AE%A9AI%E8%87%AA%E5%8A%A8%E5%B8%AE%E4%BD%A0%E7%94%9F%E6%88%90%E4%B8%80%E9%97%A8%E6%8A%80%E8%83%BD%EF%BC%8C%E4%BD%BF%E7%94%A8%E8%B5%B7%E6%9D%A5%E9%9D%9E%E5%B8%B8%E5%8F%8B%E5%A5%BD%EF%BC%8CAI%E4%BC%9A%E4%B8%80%E6%AD%A5%E6%AD%A5%E5%BC%95%E5%AF%BC%E4%BD%A0%E8%AF%B4%E5%87%BA%E4%BD%A0%E7%9A%84%E9%9C%80%E6%B1%82%EF%BC%8C%E4%BD%A0%E5%8F%AA%20)。例如,你可以对Claude说:"帮我创建一个Skill,用于根据文章内容自动选择并插入配图"。Claude会逐步引导你提供细节,自动产出包含正确YAML元数据和指令内容的Skill文件[\[18\]](https://hbwdj.gov.cn/appbdetail-imqqsmrp9897358.d#:~:text=%E9%AA%97%E4%BD%A0%E7%9A%84%EF%BC%8C%E5%85%B6%E5%AE%9EAI%E6%A0%B9%E6%9C%AC%E4%B8%8D%E9%9C%80%E8%A6%81%E9%82%A3%E4%B9%88%E5%A4%9A%E6%8F%90%E7%A4%BA%E8%AF%8D%E3%80%82%20%E4%BD%A0%E5%8F%AA%E9%9C%80%E8%A6%81%E8%B0%83%E7%94%A8AI%20%E6%9C%AC%E8%BA%AB%E7%9A%84%E2%80%9CSkill%20Creator%E2%80%9D%E6%8A%80%E8%83%BD%EF%BC%8C%E7%94%A8%E4%BD%A0%E7%9A%84%E8%AF%AD%E8%A8%80%E6%8F%8F%E8%BF%B0%E8%87%AA%E5%B7%B1%E7%9A%84%E9%9C%80%E6%B1%82%EF%BC%8C%E8%AE%A9AI%E8%87%AA%E5%8A%A8%E5%B8%AE%E4%BD%A0%E7%94%9F%E6%88%90%E4%B8%80%E9%97%A8%E6%8A%80%E8%83%BD%EF%BC%8C%E4%BD%BF%E7%94%A8%E8%B5%B7%E6%9D%A5%E9%9D%9E%E5%B8%B8%E5%8F%8B%E5%A5%BD%EF%BC%8CAI%E4%BC%9A%E4%B8%80%E6%AD%A5%E6%AD%A5%E5%BC%95%E5%AF%BC%E4%BD%A0%E8%AF%B4%E5%87%BA%E4%BD%A0%E7%9A%84%E9%9C%80%E6%B1%82%EF%BC%8C%E4%BD%A0%E5%8F%AA%20)。接着你可以在此基础上完善调整,加入脚本或模板,实现更复杂的逻辑。通过这种**人机协作**,即使不熟悉YAML语法的新手也能快速构建出可用的技能雏形。 +- **测试迭代与高级优化:** 创建Skill后,在实际项目中多次调用测试,验证其稳定性和效果。根据AI每次执行技能的表现,不断**迭代优化**指令和脚本:完善边缘情况处理,增加更多示例和指南,让AI对技能意图理解更准确。Anthropic提供了**渐进式构建**技能的最佳实践,例如"MASTER六步法"等框架,用于从手工Prompt打磨到自动技能的完整流程[\[19\]](https://lilys.ai/zh/notes/claude-code-20251031/claude-code-skills-automate-everything#:~:text=%E6%83%B3%20%E8%87%AA%E5%8A%A8%E5%8C%96%E4%BD%A0%E6%89%80%E5%81%9A%E7%9A%84%E4%B8%80%E5%88%87%20%E5%90%97%EF%BC%9F%E5%AD%A6%E4%B9%A0%E5%A6%82%E4%BD%95%E5%88%A9%E7%94%A8%20%E5%85%8B%E5%8A%B3%E5%BE%B7%C2%B7%E7%A7%91%E5%BE%B7%E6%8A%80%E8%83%BD%20%E5%88%9B%E5%BB%BA%E8%87%AA%E5%AE%9A%E4%B9%89%E5%B7%A5%E4%BD%9C%E6%B5%81%E3%80%82%E6%9C%AC%E5%86%85%E5%AE%B9%E6%8F%AD%E7%A4%BA%E4%BA%86,%E5%85%AD%E6%AD%A5MASTER%E6%A1%86%E6%9E%B6%EF%BC%8C%E6%95%99%E4%BD%A0%E5%A6%82%E4%BD%95%E9%80%9A%E8%BF%87%20%E8%BF%AD%E4%BB%A3%E5%8F%8D%E9%A6%88%20%E8%AE%AD%E7%BB%83AI%E3%80%82%E6%8E%8C%E6%8F%A1%E6%AD%A4%E6%96%B9%E6%B3%95%EF%BC%8C%E4%BD%A0%E5%B0%B1%E8%83%BD%E5%B0%86%E9%87%8D%E5%A4%8D%E4%BB%BB%E5%8A%A1%E8%BD%AC%E5%8C%96%E4%B8%BA%E9%AB%98%E6%95%88%E7%9A%84%20%E7%B3%BB%E7%BB%9F%E5%8C%96%E6%8A%80%E8%83%BD%EF%BC%8C%E5%AE%9E%E7%8E%B0%E5%B7%A5%E4%BD%9C%E6%95%88%E7%8E%87%E7%9A%84%E6%8C%87%E6%95%B0%E7%BA%A7%E6%8F%90%E5%8D%87%E3%80%82)[\[20\]](https://lilys.ai/zh/notes/claude-code-20251031/claude-code-skills-automate-everything#:~:text=4.%20MASTER%E6%A1%86%E6%9E%B6%EF%BC%9A%E7%AC%AC%E4%BA%8C%E9%98%B6%E6%AE%B5%E2%80%94%E2%80%94%E7%B3%BB%E7%BB%9F%E5%8C%96%E4%B8%BA%E6%8A%80%E8%83%BD%20)。核心思想是在对话中**演示和纠正**AI完成任务的全过程,然后一句话让Claude将过程固化为新技能[\[20\]](https://lilys.ai/zh/notes/claude-code-20251031/claude-code-skills-automate-everything#:~:text=4.%20MASTER%E6%A1%86%E6%9E%B6%EF%BC%9A%E7%AC%AC%E4%BA%8C%E9%98%B6%E6%AE%B5%E2%80%94%E2%80%94%E7%B3%BB%E7%BB%9F%E5%8C%96%E4%B8%BA%E6%8A%80%E8%83%BD%20)[\[21\]](https://lilys.ai/zh/notes/claude-code-20251031/claude-code-skills-automate-everything#:~:text=1.%20%E7%B3%BB%E7%BB%9F%E5%8C%96%E7%9B%AE%E6%A0%87%EF%BC%9A%E5%B0%86%E5%AD%A6%E5%88%B0%E7%9A%84%E6%89%80%E6%9C%89%E5%85%B3%E9%94%AE%E7%BB%8F%E9%AA%8C%E6%95%99%E8%AE%AD%E8%BD%AC%E5%8C%96%E4%B8%BA%20%E7%B3%BB%E7%BB%9F%E5%8C%96%E6%8A%80%E8%83%BD%E3%80%82,48)。之后再让Claude应用该技能执行类似任务,观察输出并调整技能定义[\[22\]](https://lilys.ai/zh/notes/claude-code-20251031/claude-code-skills-automate-everything#:~:text=match%20at%20L3327%205,57)。这种循环可以逐步提升技能质量,直到达到"专家级"水平。在进阶阶段,你还可以尝试**组合多个Skills**协同工作,以及利用Claude Code提供的Hooks和子代理机制扩展技能的功能边界,实现更复杂的自动化开发流程。 + +通过以上步骤,从模仿官方示例到定制自己的技能,再到深入技能架构的优化,你将完成从入门到精通Claude Code Skills的蜕变。在这一过程中,要牢记技能开发的目的:**减少重复劳动、固化专业知识,并让AI行为更可控**。下一节我们将进一步探讨这一新范式如何将开发流程从传统Prompt工程升级为模块化的自动化开发。 + +## 从Prompt工程到可复用模块:范式转移 + +Claude Skills的出现标志着AI应用开发从"Prompt Engineering"(提示词工程)向"**Context Engineering**"(上下文工程)的范式转移[\[23\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=1)。传统的Prompt工程往往依赖人工反复调试长提示语,把所有指导都硬编码在一个巨大prompt里,不仅上下文窗口占用大,而且难以复用和维护。相反,Skills将复杂提示拆解为**可版本化、可审计、可组合**的**运行时模块**[\[24\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=%E4%B8%BA%E4%BA%86%E9%81%BF%E5%85%8D%E2%80%9C%E4%BB%8B%E7%BB%8D%E5%8A%9F%E8%83%BD%E4%BD%86%E7%BC%BA%E5%B0%91%E5%8F%AF%E6%A3%80%E9%AA%8C%E7%BB%93%E8%AE%BA%E2%80%9D%EF%BC%8C%E6%9C%AC%E6%96%87%E5%85%88%E6%8A%8A%20Skills%20%E7%9A%84%E6%9E%B6%E6%9E%84%E4%BB%B7%E5%80%BC%E6%94%B6%E6%95%9B%E6%88%90%E4%B8%80%E5%8F%A5%E5%8F%AF%E9%AA%8C%E8%AF%81%E7%9A%84%E5%91%BD%E9%A2%98%EF%BC%9A)。这一转变带来了三大核心收益[\[25\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=Skills%20%E6%8A%8A%20LLM%20%E7%B3%BB%E7%BB%9F%E4%BB%8E%E2%80%9C%E6%96%87%E6%9C%AC%E5%A0%86%E5%8F%A0%E7%9A%84%E5%8D%95%E4%BD%93%E6%8F%90%E7%A4%BA%E8%AF%8D%E2%80%9D%EF%BC%8C%E9%87%8D%E6%9E%84%E4%B8%BA%E2%80%9C%E5%8F%AF%E7%89%88%E6%9C%AC%E5%8C%96%E3%80%81%E5%8F%AF%E5%AE%A1%E8%AE%A1%E3%80%81%E5%8F%AF%E7%BB%84%E5%90%88%E7%9A%84%E8%BF%90%E8%A1%8C%E6%97%B6%E6%A8%A1%E5%9D%97%E2%80%9D%EF%BC%9B%E6%A0%B8%E5%BF%83%E6%94%B6%E7%9B%8A%E6%9D%A5%E8%87%AA%E4%B8%89%E4%BB%B6%E4%BA%8B%EF%BC%9A%E4%B8%8A%E4%B8%8B%E6%96%87%E9%A2%84%E7%AE%97%E5%8F%AF%E6%8E%A7%E3%80%81%E6%89%A7%E8%A1%8C%E8%B7%AF%E5%BE%84%E5%8F%AF%E6%8E%A7%E3%80%81%E6%9D%83%E9%99%90%E8%BE%B9%E7%95%8C%E5%8F%AF%E6%8E%A7%E3%80%82): + +- **上下文预算可控:** Skills采用**分层渐进披露**机制,将提示内容分为元数据、指令、资源三层,按需加载[\[26\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=,3.%20%E6%8A%80%E6%9C%AF%E8%A7%A3%E6%9E%84%EF%BC%9ASkill%20%E7%9A%84%E7%89%A9%E7%90%86%E5%BD%A2%E6%80%81%E4%B8%8E%E8%A7%84%E8%8C%83)。模型初始只载入轻量的元信息(如技能名称和描述),在确认相关任务时再注入具体指令,必要时最后才加载代码模板或数据资源[\[3\]](https://claudecn.com/#:~:text=Agent%20Skills%20%E5%8F%AF%E5%A4%8D%E7%94%A8%E7%9A%84%E6%8A%80%E8%83%BD%E7%B3%BB%E7%BB%9F%EF%BC%8C%E8%AE%A9%20Claude%20%E6%8E%8C%E6%8F%A1%E7%89%B9%E5%AE%9A%E9%A2%86%E5%9F%9F%E4%B8%93%E4%B8%9A%E7%9F%A5%E8%AF%86%E3%80%82%E5%8C%85%E6%8B%AC,%E8%BF%9E%E6%8E%A5%E5%A4%96%E9%83%A8%E5%B7%A5%E5%85%B7%E5%92%8C%E6%95%B0%E6%8D%AE%E6%BA%90%EF%BC%8C%E6%89%A9%E5%B1%95%20Claude%20%E7%9A%84%E8%83%BD%E5%8A%9B%E8%BE%B9%E7%95%8C%EF%BC%8C%E6%9E%84%E5%BB%BA%E5%BC%BA%E5%A4%A7%E7%9A%84%20AI%20%E5%BA%94%E7%94%A8%E7%94%9F%E6%80%81%E3%80%82)。这样避免了"一次性把所有提示堆入上下文"的低效做法,防止上下文窗口的"公地悲剧"[\[27\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=,2.3%20%E6%8A%8A%E2%80%9C%E5%88%86%E5%B1%82%E6%8A%AB%E9%9C%B2%E2%80%9D%E6%8F%90%E5%8D%87%E4%B8%BA%E8%BF%90%E8%A1%8C%E6%97%B6%E7%8A%B6%E6%80%81%E6%9C%BA)。通过精细控制常驻、激活、执行三类上下文开销,开发者可以**显著降低Token占用**,将有限的上下文预算用在刀刃上。 +- **执行路径可控:** 在Skills架构下,模型不再承担所有推理细节,而更像一个**编排者**。我们可以把复杂决策逻辑迁移到Skill的脚本中,由可测试的代码或明确的规则去执行,Claude则根据指令**调度脚本和资源**[\[28\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=,%E6%9D%83%E9%99%90%E8%BE%B9%E7%95%8C%E5%8F%AF%E6%8E%A7%EF%BC%9A%E7%94%A8%E6%B2%99%E7%AE%B1%E3%80%81%E7%BD%91%E7%BB%9C%E4%BB%A3%E7%90%86%E4%B8%8E%E6%9D%83%E9%99%90%E6%8F%90%E7%A4%BA%EF%BC%8C%E6%8A%8A%E5%B7%A5%E5%85%B7%E6%89%A7%E8%A1%8C%E9%9D%A2%E6%94%B6%E6%95%9B%E5%88%B0%E5%8F%AF%E5%AE%A1%E8%AE%A1%E3%80%81%E5%8F%AF%E6%B2%BB%E7%90%86%E7%9A%84%E8%BE%B9%E7%95%8C%E5%86%85%E3%80%82)。通过这种软硬结合,AI生成过程更具确定性,可重复性更高。例如一个技能可以包含预定义的正则脚本来处理文本格式,模型只需调用而非每次重新"即兴发挥"。这让AI行为如同运行程序模块一样**可预测、可验证**。 +- **权限边界可控:** Skills配合Claude Code提供的沙箱和权限机制,可将AI的操作限制在安全边界内[\[28\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=,%E6%9D%83%E9%99%90%E8%BE%B9%E7%95%8C%E5%8F%AF%E6%8E%A7%EF%BC%9A%E7%94%A8%E6%B2%99%E7%AE%B1%E3%80%81%E7%BD%91%E7%BB%9C%E4%BB%A3%E7%90%86%E4%B8%8E%E6%9D%83%E9%99%90%E6%8F%90%E7%A4%BA%EF%BC%8C%E6%8A%8A%E5%B7%A5%E5%85%B7%E6%89%A7%E8%A1%8C%E9%9D%A2%E6%94%B6%E6%95%9B%E5%88%B0%E5%8F%AF%E5%AE%A1%E8%AE%A1%E3%80%81%E5%8F%AF%E6%B2%BB%E7%90%86%E7%9A%84%E8%BE%B9%E7%95%8C%E5%86%85%E3%80%82)。例如,在技能元数据或Claude配置中声明此技能允许的文件读写范围、外部命令白名单等,Claude执行技能时就不会越权[\[28\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=,%E6%9D%83%E9%99%90%E8%BE%B9%E7%95%8C%E5%8F%AF%E6%8E%A7%EF%BC%9A%E7%94%A8%E6%B2%99%E7%AE%B1%E3%80%81%E7%BD%91%E7%BB%9C%E4%BB%A3%E7%90%86%E4%B8%8E%E6%9D%83%E9%99%90%E6%8F%90%E7%A4%BA%EF%BC%8C%E6%8A%8A%E5%B7%A5%E5%85%B7%E6%89%A7%E8%A1%8C%E9%9D%A2%E6%94%B6%E6%95%9B%E5%88%B0%E5%8F%AF%E5%AE%A1%E8%AE%A1%E3%80%81%E5%8F%AF%E6%B2%BB%E7%90%86%E7%9A%84%E8%BE%B9%E7%95%8C%E5%86%85%E3%80%82)。通过引入**工具使用权限提示**以及隔离执行环境(如MCP服务器、虚拟机沙箱),开发者可以防范AI执行系统命令或访问敏感数据时可能带来的风险[\[29\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=%2A%204.1%20%E7%9B%91%E7%9D%A3%E8%80%85,6.2%20%E5%8D%8F%E5%90%8C%E6%9E%B6%E6%9E%84%EF%BC%9ASkill%20%E4%BD%9C%E4%B8%BA%E7%BC%96%E6%8E%92%E8%80%85)。相比任由AI自由解释模糊指令,这种方式下每个技能的**作用域和副作用都是受控**的,大幅提高了AI应用的安全性和可靠性。 + +综上所述,模块化的Skills架构将LLM从以前的"提示词拼凑的巨石"解耦为若干**可管理的小单元**[\[24\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=%E4%B8%BA%E4%BA%86%E9%81%BF%E5%85%8D%E2%80%9C%E4%BB%8B%E7%BB%8D%E5%8A%9F%E8%83%BD%E4%BD%86%E7%BC%BA%E5%B0%91%E5%8F%AF%E6%A3%80%E9%AA%8C%E7%BB%93%E8%AE%BA%E2%80%9D%EF%BC%8C%E6%9C%AC%E6%96%87%E5%85%88%E6%8A%8A%20Skills%20%E7%9A%84%E6%9E%B6%E6%9E%84%E4%BB%B7%E5%80%BC%E6%94%B6%E6%95%9B%E6%88%90%E4%B8%80%E5%8F%A5%E5%8F%AF%E9%AA%8C%E8%AF%81%E7%9A%84%E5%91%BD%E9%A2%98%EF%BC%9A)。开发者能够像搭建乐高一样组装技能,复用成熟模块,版本迭代升级技能库,并针对不同任务**按需加载**合适的技能组合[\[30\]](https://www.bilibili.com/opus/1132124852115734530#:~:text=Claude%20Skills%20%E5%AE%9E%E6%88%98%E6%8C%87%E5%8D%97%EF%BC%9A3%20%E5%88%86%E9%92%9F%E6%90%9E%E5%AE%9APPT%E3%80%81%E6%B5%B7%E6%8A%A5%E4%B8%8ELogo%20,%E5%AE%83%E5%B0%86%E5%B8%B8%E8%A7%81%E7%9A%84%E5%8A%9E%E5%85%AC%E4%BB%BB%E5%8A%A1%28%E5%A6%82Excel%20%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90%E3%80%81PPT%20%E6%BC%94%E7%A4%BA%E7%94%9F%E6%88%90%E3%80%81%E6%96%87%E6%A1%A3%E5%A4%84%E7%90%86%E3%80%81%E5%93%81%E7%89%8C%E8%AE%BE%E8%AE%A1%E7%AD%89%29%E5%B0%81%E8%A3%85%E6%88%90%E5%8F%AF%E5%A4%8D%E7%94%A8%E7%9A%84%E6%8A%80%E8%83%BD%E6%A8%A1%E5%9D%97%E3%80%82%E8%BF%99%E7%A7%8D%E8%AE%BE%E8%AE%A1%E7%90%86%E5%BF%B5%E7%9A%84%E6%A0%B8%E5%BF%83%E4%BC%98%E5%8A%BF)[\[31\]](https://lilys.ai/zh/notes/claude-skills-20251022/no-code-ai-workflow-claude-skills#:~:text=Skill%20%E5%9F%BA%E7%A1%80%E7%BB%93%E6%9E%84%EF%BC%9A%E6%9C%80%E7%AE%80%E5%8D%95%E6%83%85%E5%86%B5%E4%B8%8B%EF%BC%8CSkill%20%E6%98%AF%E4%B8%80%E4%B8%AA%E5%8C%85%E5%90%ABSkill%20Markdown%20%E6%96%87%E4%BB%B6%E7%9A%84%E7%9B%AE%E5%BD%95,36%5D)。这不仅提高了开发效率,也让AI行为更加透明可控--我们可以审查每个Skill的定义,像审计代码一样检查AI决策依据。这种以**技能库**为中心的上下文工程理念,正在帮助AI从通用助手进化为各行业的专家[\[32\]](https://github.com/0xfnzero/AI-Code-Tutorials#:~:text=%E4%BB%8E%E9%9B%B6%E5%9F%BA%E7%A1%80%E5%88%B0%E9%AB%98%E7%BA%A7%E5%BA%94%E7%94%A8%EF%BC%8C%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0Claude%20Code%EF%BC%8C%E6%8E%8C%E6%8F%A1AI%20%E8%BE%85%E5%8A%A9%E7%BC%96%E7%A8%8B%E6%8A%80%E8%83%BD%EF%BC%8C%E6%8F%90%E5%8D%87%E5%BC%80%E5%8F%91%E6%95%88%E7%8E%8710%20%E5%80%8D%20,md%E3%80%81%E5%B7%A5%E5%85%B7%E6%9D%83%E9%99%90%E3%80%81gh%20CLI%EF%BC%89%3B%20%E7%BB%99Claude%20%E6%9B%B4%E5%A4%9A%E5%B7%A5%E5%85%B7%EF%BC%88bash%E3%80%81MCP)。许多团队已开始构建自己的私有Skills库,将领域知识固化为技能手册,与团队共享使用[\[31\]](https://lilys.ai/zh/notes/claude-skills-20251022/no-code-ai-workflow-claude-skills#:~:text=Skill%20%E5%9F%BA%E7%A1%80%E7%BB%93%E6%9E%84%EF%BC%9A%E6%9C%80%E7%AE%80%E5%8D%95%E6%83%85%E5%86%B5%E4%B8%8B%EF%BC%8CSkill%20%E6%98%AF%E4%B8%80%E4%B8%AA%E5%8C%85%E5%90%ABSkill%20Markdown%20%E6%96%87%E4%BB%B6%E7%9A%84%E7%9B%AE%E5%BD%95,36%5D)。Prompt工程不再是黑箱中的玄学调参,而变成了**软件工程化**的过程:定义规范→编码技能→测试迭代→部署上线。可以预见,随着技能生态的完善和技能市场的兴起[\[33\]](https://news.qq.com/rain/a/20260107A02N2N00#:~:text=Skills%20%E5%B0%B1%E6%98%AF%E7%BB%99Claude%20%E7%9A%84),未来"会写Prompt"将不再是关键竞争力,取而代之的是"**会设计AI技能模块**",让AI真正成为持续进化的数字员工。 + +## Claude/GPT-4 助力 Golang + Vue3 全栈项目开发 + +大型语言模型(LLM)如Claude 2和GPT-4已成为开发者强有力的**编程助手**。下面我们以Golang后端 + Vue3 (TypeScript + Vuetify UI) 前端的全栈项目为例,探讨如何利用Claude/GPT-4提高各环节开发效率,并重点说明**系统构建方法、提示词模板设计**及**工作流程集成**。 + +### 系统构建与架构规划 + +在项目初期,AI可以充当"**架构顾问**"的角色,帮助选择技术栈、搭建骨架。开发者可以用自然语言向Claude/GPT-4描述项目需求和约束,让其建议合适的框架和方案。例如:"我想用Go开发后端REST API,用哪种Web框架比较合适?"Claude基于知识建议使用Gin框架,并解释其上手简单、配置少的优点[\[34\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L93%20,side%20rendering%20and)。同样,对于前端,它可能推荐Vue3 + TypeScript配合Vuetify组件库,以快速构建响应式UI[\[35\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L96%20,No%20magic%2C%20no)[\[36\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=,No%20magic%2C%20no)。在实际案例中,开发者Korbinian Schleifer分享道:他询问Claude选择Go框架,Claude推荐了Gin,使他很快建立起第一个路由;选择前端则采用Vue3 + TS,Claude也支持这一选择,认为Vue的模板语法更贴近HTML,学习曲线相对平缓[\[34\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L93%20,side%20rendering%20and)[\[37\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=,No%20magic%2C%20no)。借助AI的建议,可以较快确定技术栈并初始化项目结构(例如生成基础的Go项目模块和Vue前端脚手架)。 + +**分层实现:** 拆解全栈系统时,建议先让AI协助规划前后端接口契约和模块划分。例如,可以让Claude梳理"后台需要提供哪些REST API,以及前端各页面对应哪些组件和状态管理"。通过逐步追问,Claude能够产出一个简单的**架构清单或示意图**。有趣的是,Claude擅长**文字描述架构**,甚至可以生成架构图的代码(如Excalidraw的JSON)供开发者直接渲染出图表[\[38\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=Claude%20can%20describe%20architecture%20well%2C,com%20which%20I%20then)。这有助于开发者和AI达成对系统设计的一致理解。 + +### 提示词设计与编程对话技巧 + +在与AI pair programming(结对编程)的过程中,**提示词的设计**直接影响AI产出的质量和可控性。以下是实战中总结的Prompt技巧: + +- **提供充分的上下文:** _"Context is king."_ 与LLM协作编程时,不要吝啬提供背景信息[\[39\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L316%20Give%20Claude,context%2C%20the%20better%20the%20suggestions)。明确告知AI当前项目的**技术栈、代码结构、已有代码片段**、使用的库版本,以及你已经尝试过的方法等[\[39\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L316%20Give%20Claude,context%2C%20the%20better%20the%20suggestions)。上下文越完整,AI越能理解需求,给出针对性的建议。例如,在请求AI编写某个Vue组件时,先说明项目使用Vuetify版本、已有的全局样式等,AI就能生成更符合项目风格的代码。 +- **过程式分解任务:** 避免一次让AI生成庞大复杂的代码,不妨将需求拆成**多步对话**。你可以先让AI**列出实现思路**或步骤清单,然后确认方案后再逐步让其实现每一步。这类似于指导一个新手程序员:先讨论方案,再Coding。Claude等模型擅长这种渐进式对话,会根据前文步骤逐一输出相应代码。在这个过程中,可灵活插入自己的想法,例如:"步骤2很棒,但请在实现时使用Vuetify的Grid系统。" 通过逐步细化指令,模型生成的代码将更符合预期。 +- **及时反馈和明确决策:** 当AI给出多个方案或不确定措辞时,务必**明确告诉它你选择了哪一种**[\[40\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L322%20When%20Claude,things%20you%E2%80%99ve%20already%20decided%20against)。例如Claude可能提出两种API设计思路,选定其中一个后应回复:"我决定采用方案B,接下来按照这个方案继续。" 这样AI在后续对话中会聚焦于你选定的方向,避免反复纠结已否定的路线[\[40\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L322%20When%20Claude,things%20you%E2%80%99ve%20already%20decided%20against)。同样,如果AI生成的代码不符合预期,要迅速指出问题所在(bug、风格偏差或逻辑错误)并要求修改。通过这种**紧反馈回路**,AI相当于接受了快速code review,能更快调整输出方向。 +- **保持风格一致性:** 对于多人协作或涉及特定编码规范的项目,可以在提示中强调代码风格和约定。例如:"请使用Go语言的标准错误处理模式,前端TS代码请遵循项目已有的ESLint规则,组件命名采用PascalCase。"Claude/GPT-4会据此自适应输出格式。实践表明,让AI扮演特定角色有助于风格统一,比如提示它"你是资深Go工程师"或"充当代码审核者",使其回答更严格遵循专业规范[\[41\]](https://www.cnblogs.com/treasury-manager/p/19217990#:~:text=OpenCode%20%E4%B8%80%E4%B8%AA%E7%A5%9E%E5%A5%87%E7%9A%84CLI%20%EF%BC%88%E5%8F%AF%E4%BB%A5%E8%9E%8D%E5%90%88Claude%20Code%2C%20iFLow,code%29%20%E2%86%90%20%E8%87%AA%E5%8A%A8%E4%BD%BF%E7%94%A8oracle%20%E7%9A%84%E6%A8%A1%E5%9E%8B%E2%86%93%20%E8%B0%83%E7%94%A8%40explore)。此外,一些高级用户会维护一个**项目说明文档**(如CLAUDE.md或README),里面列出项目编码准则,然后在每次对话开始时提供给AI参考。这相当于建立上下文的**长期记忆**。 +- **控制对话长度:** 在长时间对话或大型项目中,注意LLM的上下文窗口限制。如果对话持续很多轮且内容庞杂,Claude可能出现响应变慢甚至遗忘前文细节的情况[\[42\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=After%20long%20chats%2C%20Claude%20noticeably,you%20have%20done%20so%20far)。此时有两个对策:一是**总结当前进度**,开启一个新会话,将概要和关键代码片段提供给新对话作为背景,让AI轻装上阵接着干[\[42\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=After%20long%20chats%2C%20Claude%20noticeably,you%20have%20done%20so%20far);二是利用像OpenCode这样的工具,它可以自动维护项目知识(通过索引代码库等),减轻每次都传输大量历史的负担[\[43\]](https://opencode.ai/docs#:~:text=%2Finit)[\[44\]](https://opencode.ai/docs#:~:text=You%20can%20ask%20OpenCode%20to,explain%20the%20codebase%20to%20you)。总之,要避免一口气把所有内容都塞给模型,多用**小步快跑、阶段重启**的方式确保AI始终聚焦有效信息。 + +通过上述Prompt工程技巧,开发者在Golang+Vue项目中与AI协作时,可以达到接近"**实时对话编程**"的体验。例如,有开发者分享他在几天内用Claude辅助完成了一个Go后端+Vue前端的CRUD应用,从中**学习了Go的新特性**(如 if err := ... 用法,Claude解释了其作用[\[45\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=if%20err%20%3A%3D%20godotenv,))也让Claude帮助生成了一些前端组件代码[\[46\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L236%20This%20is,JavaScript%20pretending%20to%20be%20HTML)。期间他总结的心得是:"让Claude就像团队新人一样工作:给足资料,明确任务,迅速反馈,必要时重开小灶(新会话)。"[\[47\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=3,getting%20lost%20in%20tutorial%20hell)。结果显示,Claude在提供代码解释、框架用法指导和产生样板代码等方面极大加速了开发,但在某些细节上仍需要人工审查和调试。这提醒我们,AI虽然强大但不是万能,"Human in the loop"(人类介入)依然重要--**AI负责快,人工负责对**。利用好AI的长处(速度、记忆、生成能力)并辅以人类的判断力,才能在全栈开发中如虎添翼。 + +### 工作流程集成与工具链结合 + +要充分发挥Claude/GPT-4在编码中的作用,建议将其集成进日常开发流程和工具链中,实现"AI协同开发"的闭环。以下是几种可行的集成方式: + +- **终端集成:** 使用AI驱动的命令行助手,如Anthropic官方的Claude Code CLI或者开源的OpenCode工具(详见下文),在终端中与AI互动编程[\[48\]](https://zhuanlan.zhihu.com/p/1991170184573122515#:~:text=OpenCode%20%E6%98%AF%E4%B8%80%E4%B8%AAAI%20%E7%BC%96%E7%A8%8B%E5%8A%A9%E6%89%8B%EF%BC%8C%E8%B7%91%E5%9C%A8%E4%BD%A0%E7%9A%84%E7%BB%88%E7%AB%AF%E9%87%8C%EF%BC%88%E5%B0%B1%E6%98%AF%E9%82%A3%E4%B8%AA%E9%BB%91%E8%89%B2%E7%AA%97%E5%8F%A3%EF%BC%89%E3%80%82%20%E4%BD%A0%E8%B7%9F%E5%AE%83%E8%AF%B4%E8%AF%9D%EF%BC%8C%E5%AE%83%E5%B0%B1%E5%B8%AE%E4%BD%A0%E5%86%99%E4%BB%A3%E7%A0%81%E3%80%82%20,%E2%86%92%20%E5%AE%83%E6%94%B9)。这类工具允许AI直接读取项目文件、执行Git命令、运行测试等。例如,你在终端对AI说"帮我实现登录功能",它就能新建代码文件、写入实现并运行测试验证功能[\[48\]](https://zhuanlan.zhihu.com/p/1991170184573122515#:~:text=OpenCode%20%E6%98%AF%E4%B8%80%E4%B8%AAAI%20%E7%BC%96%E7%A8%8B%E5%8A%A9%E6%89%8B%EF%BC%8C%E8%B7%91%E5%9C%A8%E4%BD%A0%E7%9A%84%E7%BB%88%E7%AB%AF%E9%87%8C%EF%BC%88%E5%B0%B1%E6%98%AF%E9%82%A3%E4%B8%AA%E9%BB%91%E8%89%B2%E7%AA%97%E5%8F%A3%EF%BC%89%E3%80%82%20%E4%BD%A0%E8%B7%9F%E5%AE%83%E8%AF%B4%E8%AF%9D%EF%BC%8C%E5%AE%83%E5%B0%B1%E5%B8%AE%E4%BD%A0%E5%86%99%E4%BB%A3%E7%A0%81%E3%80%82%20,%E2%86%92%20%E5%AE%83%E6%94%B9)。这种方式将AI无缝嵌入开发者熟悉的环境,大大减少在浏览器对话与IDE编辑器之间来回切换的摩擦。 +- **IDE插件:** 如果偏好图形界面,可以使用支持GPT的IDE插件或扩展(如VS Code的Claude插件、ChatGPT Copilot等)。这些插件通常提供代码补全、文档查询、错误诊断等功能。例如在Vue组件文件中,选中一段报错代码,让AI解释错误原因并给出修改建议。集成在IDE中的AI还能结合LSP(语言服务器协议)获取更精准的代码上下文[\[49\]](https://github.com/anomalyco/opencode#:~:text=close%20and%20pricing%20will%20drop,what%27s%20possible%20in%20the%20terminal),从而提升回答专业度。许多现代编辑器插件已经支持将当前文件、甚至整个项目知识作为提示上下文传递给LLM,提高交互质量。 +- **CI/CD 流程:** 在持续集成阶段,可以借助AI做代码审查和自动修复。比如Push代码后,触发一个CI Job调用Claude来分析最近的变更是否存在代码风格违背或潜在bug,然后自动提出修改建议(甚至直接开Pull Request修复简单问题)。Anthropic的Claude Code支持类似的**代码审查子代理**,可配置ESLint、单元测试覆盖率检查等,在每次提交时由AI代理提示改进之处[\[50\]](https://cloud.tencent.com/developer/article/2574516#:~:text=Claude%20Code%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83%E5%AE%88%E6%8A%A4%E8%80%85%E5%AD%90%E4%BB%A3%E7%90%86%E5%AE%9E%E6%88%98%E6%8C%87%E5%8D%97%20,Code%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83%E5%AD%90%E4%BB%A3%E7%90%86%E6%98%AFAI%E9%A9%B1%E5%8A%A8%E7%9A%84%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E7%AE%A1%E5%AE%B6%EF%BC%8C%E8%83%BD%E8%87%AA%E5%8A%A8%E7%BB%9F%E4%B8%80%E5%9B%A2%E9%98%9F%E4%BB%A3%E7%A0%81%E9%A3%8E%E6%A0%BC%E3%80%81%E6%89%A7%E8%A1%8C%E5%91%BD%E5%90%8D%E8%A7%84%E8%8C%83%E3%80%81%E6%A3%80%E6%9F%A5%E6%B5%8B%E8%AF%95%E8%A6%86%E7%9B%96%E7%8E%87%E3%80%82%E9%80%9A%E8%BF%87ESLint%E3%80%81Prettier%E7%AD%89%E5%B7%A5%E5%85%B7%E9%85%8D%E7%BD%AE%EF%BC%8C)。这种AI辅助的代码守护者可以减轻Reviewer负担,保证代码质量的一致性。 +- **文档和测试生成:** 将AI融入开发的"后勤"工作中也是一种高效做法。例如利用GPT-4根据代码自动生成文档注释、根据函数签名生成单元测试样例等。许多开源工具(如OpenAI's Codex、或OpenCode中的特定命令)可以一键生成注释和测试代码。开发者可以让AI先生成,再自行检查润色,从而快速补齐文档和测试,提高代码可维护性。 + +总的来说,无论通过何种途径集成AI助手,都应当**明确AI担当的角色**:是需求的分析者、编码的执行者还是结果的评审者。合理分配这些角色,让AI参与从需求->设计->编码->测试的各个环节,并建立**反馈闭环**(例如AI写完代码立即运行测试验证,失败再由AI修复),就能够形成流水线式的智能开发流程。在实践中,随着使用AI的成熟度提高,你甚至会把一些日常繁琐任务全权交给AI处理,例如批量重构代价不高的代码、格式标准化、依赖升级兼容性修改等,而自己专注于核心业务逻辑和架构决策。这正是AI辅助开发所追求的目标:**让人类做更高价值的创造,让AI承担重复冗杂的劳动**。 + +## 开源AI编程工具集成:OpenCode 与 Oh My OpenCode + +为了将大模型更好地融入实际开发,一些开源项目提供了强大的工具和插件。如前所述,OpenCode是当前炙手可热的开源AI编程助手,而"Oh My OpenCode (OMO)"则是其上一款明星插件,能将单Agent升级为多Agent协作。下面我们详细介绍两者的特点、使用方法和最佳实践。 + +### OpenCode:终端AI编程助手 + +![](data:image/png;base64,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/uu/0O5vfvObHB6kNWhssskmX/3qVydPnmxlctVVV1166aWoRiEzSCXzFCsO1/BIgO+0007veMc7/uM//uOGG26oddE8lXQV0Ef/8i//YsnxwQ9+sC/P69pkGFWK7o1vfOP666//gQ98oFahg3S+A/CmlXpH0xFN/aIqWPduu+22n//857/yla+449qr5QUwwVWtGm8pN1Y/9rGP/e///u95553XLLwXQPNCNtEvr3/96zfeeGMDuO1hWy0ZT5LNNtvsfe97n7usiaItk3IQCAJBIAiMGAQWwRaCEYPFfAViskfK5Y932GGH5z//+QcddBAmsemmmzZKTP/PfvazDz/88KIF6jGA73//+//2b/+Gyq+zzjoNj0Q7TjjhBJz+61//+uqrr44NN00abf0VSPYn3LW+V3ygPHdfgY4azGCLLbZYe+21q95nf76169HBN73pTWi6xQys3v72t0vYN6x9nkrmKYCyrLbaap/73Oc22mgjyJdprbzH2GabbQrSrkpUdq1vO69MZr311qv+7W3RJeqB9fS92p+eMtfhQFX2VVJiA9T3d6mtn0w7uval/sqaOAxma9eOdy818t/73vfqFDJ9NXStLLEBLnXomTp1qrXfbbfd1pWq0jMYVYalsYGg77rrrk0UGrq13c6S7r1quoTAmcHob/vcn/wA9S6tu+66SHlbT4fptkxXVSq71peeAS51GM1pEAgCQSAIDAUEQtwXsBdkxG+66Sb5sLe85S3Szz/96U9f9apXffzjH2/IKL0yyo5masQwrrjiCjk8hKMhl8Q0ufnmm88666wLL7zwwQcf7EpE+nqJFRXVoF+hvRXEae3SUSBWbUusnHG1qdeQMw6Fqm8cICyfp77dlrZGXr2rjaq+TlZNKd98881PP/30e+65RytNfLqqORPKTFc4Kp2WCWIKFUt/ykuejO1Gz33uc21Goker0l+aGyXEqr5aVXSNDwOYcKlxpgBpe0V/OV+mS087CsJaEWsuMV0gVPhVr8ZRTvpsy9NcnqsnU/I+S6zCVM9K24RTR9tE07CjoG2hRL4KjZ7GDTWNiZJ/ylOesv322xNwqCGgOVe33nrr/fffv3yr+tJW+PC2kK9KTUob+Qqk6vv7JGOIXnfddf/6r/96yimntDPi9JQDPh286k9J1RNw9x1yyCEW0j0x9N4FLjV4qnHa1Nclzqvp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whc4ucwyyzznOc/h2Py60R9Q/dUP3lterbTSSu4vvik/4QlP2GabbebXvcGbi2QQCAJBIAgstQiEuC+Cri+WM09FZvS11lrr8MMPwzmmTp1qan/uc59bM722lDR62uVS27emMde0amr6FshgYxgYeiHdj59JrBJTL3OpsrhLV1Uq+6vva6hqusqXub6XerT3Hn21qe5bOYAJ8qJrt1qc0fXnqn5vuyRhDPB6LdCu76+5ehqsRg499NBVV10VHWxaKdB20EEHbbHFFh2BNyg1wh361fe9xBA9jaTT9dZbD1kv4s6WYbPxxhurb2T6KmlMK3S92rWyUdi3wFy7ibEKwFpdtOv7NmxqaFhllVWe//zn1wK1aaVgHfXsZz97/fXX7w/ARklHQVtHVSpYHLq/qls322wzC7M2Sh1tcxoEgkAQCAJBYMEQCHFfMNwe0wqhecx5Pydmdzm5e+6554c//OExxxzzs5/9bPXVV990001rgqfEQQYzQ01QpVLjKkrhVGWbtLnqksOljvqu9knOmDGDKlZm9h4lxmKvmh5uhMQoKzSXyrQmCo3aasIfwo1kFXiisvQ0V13SxCV6tGqrqkr02iXlxoS2jr56CGjeuEqmadK1UK4Sa5oolKTCooquq2mVTAhtv/3222ijjfpGx6vqu3lGQRU9lEyaNEkZVo1FZdwake2o7y86Yi5xRkc45mmaQNtz5aZJY6IUNi45JVPKuee0uTRffVeuCtkbKu+FGrvqlesY5ODXBKUGILqv3AaQn1g7Qt9R346uPWKJVbxM08MNNQ7IuL98utq+v+pqPoNAEAgCQSAILBIExi0SLUunEnO2eRrz3n777Y8//vj7778f0WxzgjYspnPkAP847bTTpk+fjodJuqPvDzzwgCY0YBUUnnPOOdLhm2+++U033XTqqaeqWXHFFaU8UXwarrnmmgsvvFAl/vHEJz7xhhtukLwnf/vtt5911llUYSFtox1lDR0qfdLmKAEF+nfbbffVV1/t5ptvPuOMMzAPld4G7LLLLnabIC5XXnnlJZdcQr4qH374YZnXiy66qNw+8cQTqwnP5RpFKrpzzz331ltv1WTDDTek5I477thuu+0Efumll/7jH/8oN6SKt912W+EgPX//+98FWGRIfnT33XeXYL7xxhtvu+02e2DOPPPMcpu8jCY37rvvPlHffffdA0ct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uo1/aNdWgzDvxmgEU2qS7hAQdApASafU+M54h+A22G3FKNOMMMofsAhkJYxllMPj8wcRGC8/4VPjsA7S7bNyNAOZQ6HziCdDH01ocZGKgUXgk/epbpjzVHddE5WeFoFFPAdMG9t8C5hd7n3ve7OyZMmSn/3sZz/60Y/0hx/84Afj7Wr366yzzvOf//zddtttrbXWestb3vLf//3fs+tC0xraaKONtt9++wsuuODHP/6xwWu8/Ery1GC69tprP+1pT7vnPe9pYXPllVeee+65046tolMLP/nJT8xwUDUF/vCHP6RK4u53v3vSAqRT5pyjse6661qAaQMcZr2ZG4SUdf4cd9xx97rXvQh/7GMf+8AHPtCLbp999qHtgx/84Pgq05A22GCDnXba6aKLLtLqutPVoN1Z52iZqgNi2vbkSjJfdunX5GVnLQmBY4455v73vz9gP/e5z51xxhmzUwXY+9znPnvssceHPvShb33rWysOrErHzKAxvkK73g4FcK+99rrpppu0mV6D6RZc+PTk0QHWgHnooYdqUeoIvBdeeGFrJPSI6wlPeMJ3v/vdj370o4EdDtttt91973tfjZzAtNGRX3/99Q855BD6Nd3zzz//Ix/5SDMxbfEFEADCxhtv/MhHPtKwbLzSJOQ86lGPEuN5553H/14jETX/999//6997Wuf/vSnx1c9Va6VKt7B5sHDBzzgATvssMPcTkyg07p8BlV2rd/ud7/7GZy///3vf+c739EeuthyQ8497nEPE7f0AlT9SmICRFtvvfWGG24I//GBD607BXfcccd5nXRWEqBWZjem3x5emb2/A30zKGyyySYnnHDCiSeeiFm+973vPemkk1796ldP2xP4/Kd/+qcHHHAA+X/7t3+76qqrxo/Fs46Rh1tttdWrXvUqU4K+Oms9C1wQgNj2Ntts89CHPvR3f/d3TeRIz3gfRIdpWf9su+22KO+BBx74D//wDwYdTPqxj33sf/zHf2RAN2r/yZ/8ye/93u/JH69w8qesGP3VI1LF9O67784c7j6qGZBv0f3O7/zOE5/4xG50SlnUvfSlL33e855n9iU8xhMFH/awh/3FX/wFpjJP9asJbbHFFv/7v/9rBdj1c4xXHnHbxPyIRzxivP/jlcziqfrl7eabb27Vd9RRR8WTWegBphURYKmaPOpRhlLj2KeanRAQYg96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/9nPfkbyrne96y9+8Ys11lhjvPysn1J+2223sTLeq67/0k24l54wuhl5O8r0KCW33nor9+5yl7uMElhp87uR/vznP8+tQNROz+fJY6SE8LT129M/5lZroXPQpTFFVvwRo7FL1Zprrtma33jNiR2Ad77z9LsPUNIXFKFcd5vQxFAHxtfd0CJzmzl585hbu6O0BdVRTxd1fsbPORmfx9da2v8k/S7Nfg67/KKuoNXK+cGqH9M+tbdbbrnFcEpmwkFytQJzsQfbJw2LPZ6F9D8M0ufd7353/cSMzroheLwPup+O9NSnPnX33XdfZ5113vWud5122mmTDNnj1Q4+ZWjJkiUPfvCDP/OZz9x8881jyAp/CLsSUQuBV6E7lKNHnrodNLQiOUwrTm1MN3NDdfJwiy22APUXv/jFMeEMlo3+wSIJZxLmN6hzRjk8Z8XFeYPpnnvueY973ENCIB//+MflN20kt9xyS23puuuuG3S4iUmQvPe9773ppptec801P/3pT8cLdwuOSlO42Wab3e1ud/vsZz87SmbO8zU2dcquxmamufjii3/4wx92ARlad1yF3g477ACl733ve135QQ+Z2H777R/4wAdiYD/5yU+YGN8dBjUkJ0Yf+9jH6rZQYvrKK68cb3qUqmnzx7TMRz7ykSD68pe/PE+mp/WtK6DVcSPedvPHp8dEN77gQj7l5AYbbLD++ut/+tOf1m1XpH9pOVtttZVWp80M1hpDm2yyyT3veU8D9fgA6VlrrbX0F3puvPHGFXFpvKFV/inMoTc5gJB3DdbdggHFumHnYQ972LXXXvujH/2I50LQOB/wgAdcddVVbcqOPwbSrbfe+ogjjth4442/8Y1v/NM//dO0g+SCBVKG5gSB2xnDnKhbfZQYyvWKN09dF1544fve977/+7//O/7443Wn8SDoY7/7u7/7J3/yJ/oSypURZHyR2T1F/vCVf/3Xf73f/e7X69hdhRzg9hOe8ATzCsqok6+77rrKbrTRRso+5CEPEamB4OCDD/7zP/9zw0e37AqmWXnBC15wzDHHxNw///M/P+hBD2JulFqPXvSiFz33uc/lzyiZofkQWHvttQedv8/UNZg/VMmsM+lH00WH84lU+ulPf/pxxx33j//4j0cddVQ3FnVhdP63f/u3V73qVYjseMcyOr/uda/bcMMNx4A2odts4bWvfvWr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6ezs3Hzzza2433///Yp1dHRUHhlXTHVnGZQoLlRXhncVSdg111xz0qRJ7QO2vV6xHXbYwa4VNqVL+bXWWmu11VaTdtjzY728BMCXKt7usssua6yxRrU7Xz7vVeu8dairpI8UBLzOOutol0NafldXlyCVb26diiT4RhtttP322ysmrVGqrLrqqkLydsMNN3zKU54CkZJOlfK8AMugOAuHClcSIRACIRACIRACITBWCIwbK4GOwjiJQtqRip01axZFSExTkHUN3RwzZWlX+qWXXvrLX/7y3//93wnKW265RUUy94gjjiC1Z8+ezeBFF110+OGHP/jgg5UFcnPy5Mnf/OY3adlPfvKTlO5//dd/Ec32k/z4xz9W2H53hRX7xCc+8cgjj4wbN36nnXYkhX/0ox994xvfcEqVr33ta9ttt90999xz3nnn7bjjjqydeeaZRHnlpZ4QquX8D3zgAw8//PBVV13FpsZ++MMf5kjkLHz7299W/uUvf/nrX//6t73tbQ888ICZgFaQyOqS4N///vd/+MMfoqR1X/ziF80ZHnroIa275JJLPve5z2ndYYcd9pznPEer11tvPYGxfPLJJ3/1q19V5j//8z+nTp1Kvv/f//3fZz/72dI6pqjzL3/5y1S7WYG3Gv73v/9dq9/85jfze+211+6557M23XSTf/zjHx/60Ifmz58vpE9/+tPPeMYzhCeM22+//eMf/zgC2lJvbNIhEAIhEAIhEAIhMPoJZMV9mH1Eo1vWJUBPOumkz3zmM7Tj+uuvTxmT1061NEpoTps2ba+99jr33HPPOOMMZfbYYw8VadAXvehFNs+8973vtaX7Yx/72E477fTUpz5VeWVYI0C9fv7znyf0v/71r8uneknqt7/97faT0KOKlUMxy9h2vZ966im07LHHHmu3Ccm+YMGC973vfWx6Vcv0YJ999iHZBwrVkrz16S984QvHH3/8m970Jvrb5wPrrrtuKT9jxgxOpR2UtEVucw+tIOuf9axn/b//9/9e+9rXfulLX+LL0jvXvmNqX1Bp3Uc/+lEr4jvvvLOA//d///fd7373DTfccOqpp76n//jFL34hKvr+gx/8oMmAvUD11omKUifxldW6888//zvf+Q7vgFD5phD33Xffu9512BFHfAlPDLmm5l/1qlfR7pi8613vAgd/zB8Dln9DIARCIARCIARC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AiEQAiGw0hOIcF/pL4EACIEQCIEQCIEQCIEQGAsEItzHQi8lxhAIgRAIgRAIgRAIgZWeQIT7Sn8JBEAIhEAIhEAIhEAIhMBYIBDhPhZ6KTGGQAiEQAiEQAiEQAis9AQi3Ff6SyAAQiAEQiAEQiAEQiAExgKBCPex0EuJMQRCIARCIARCIARCYKUnMG6lJdC9aH5HZ1dn1/hCoLe3d6VFkYaHQAiEQAiEQAiEwJgj0NHRMeZiXsqAV1Lh3rN44Tb7v/vhmdfffcUpneMmUO1Tpkzp6upaSpqpHgIhEAIhEAIhEAIhMAIEenp65s6dOwKORpWLlVW49yzecJeX3nP1mXdednLnKhN0ycSJEydM6FPwo6p7EkwIhEAIhEAIhEAIhEAzge7u7gj3ZiwrbE73ogU93Yuq5pHs5ahykgiBEAiBEAiBEAiBEBidBMi20RnYco0qX05drnhjPARCIARCIARCIARCIASWDYEI92XDMVZCIARCIARCIARCIARCYLkSiHBfrnhjPARCIARCIARCIARCIASWDYEI92XDMVZCIARCIARCIARCIARCYLkSiHBfrnhjPARCIARCIARCIARCIASWDYEI92XDMVZCIARCIARCIARCIARCYLkSiHBfrnhjPARCIARCIARCIARCIASWDYEI92XDMVZCIARCIARCIARCIARCYLkSiHBfrnhjPARCIARCIARCIARCIASWDYEI92XDMVZCIARCIARCIARCIARCYLkSiHBfrnhjPARCIARCIARCIARCIASWDYEI92XDMVZCIARCIARCIARCIARCYLkSGLdcra/Axnt7excvXtzcwI6OjnHjQrUZTHJCIARCIARCIARCIASWikAk5nDwUe1TpkyZMWNGT09PvT7VTs3fcccdCtTzkw6BEAiBEAiBEAiBEAiBpSQQ4T4cgAsXLtxrr72OOuqo+fPn1+t3dXXdfffdBx544IIFC4j4+qmkQyAEQiAEQiAEQiAEQmBpCGSP+/DpRZoPn11qhkAIhEAIhEAIhEAIDJFAhPsQgdWKZz9MDUaSIRACIRACIRACIRACy5dAhPvy5RvrIRACIRACIRACIRACIbBMCES4LxOMMRICIRACIRACIRACIRACy5dAhPvy5RvrIRACIRACIRACIRACIbBMCES4LxOMMRICIRACIRACIRACIRACy5dAhPvy5Rvr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QiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEQiAEliOB/w9Ihlp7Vomg6wAAAABJRU5ErkJggg==) +_OpenCode终端界面示例:开发者请求修改主页按钮颜色,AI在代码库中搜索相关文件并提出修改方案。界面底部显示当前模式为"Build",使用的模型是Claude Opus 4.5(Claude Code的API型号),以及OpenCode Zen推荐的模型配置。开发者可通过快捷键与AI交互(如Tab切换Agent模式)。该界面展示了OpenCode如何理解指令并进行代码操作的过程。_[_\[51\]_](https://github.com/anomalyco/opencode#:~:text=OpenCode%20includes%20two%20built,key)[_\[52\]_](https://opencode.ai/docs#:~:text=1) + +OpenCode是一个**完全开源的AI编码Agent**,支持在终端、桌面应用或IDE插件中运行[\[53\]](https://opencode.ai/docs#:~:text=OpenCode%20is%20an%20open%20source,desktop%20app%2C%20or%20IDE%20extension)。它的设计初衷是提供类似Anthropic Claude Code的体验,但**不绑定特定模型**且更加可定制[\[54\]](https://github.com/anomalyco/opencode#:~:text=%2A%20100,what%27s%20possible%20in%20the%20terminal)。OpenCode可以配置使用Anthropic Claude、OpenAI GPT-4、Google PaLM乃至本地开源模型,共支持超过75种LLM型号[\[55\]](https://github.com/anomalyco/opencode#:~:text=%2A%20100,push%20the%20limits%20of%20what%27s)。这种**模型无关性**意味着开发者可根据任务需要和成本自行选择AI引擎,而不局限于Claude,实现弹性升级[\[54\]](https://github.com/anomalyco/opencode#:~:text=%2A%20100,what%27s%20possible%20in%20the%20terminal)。另外,OpenCode内置对LSP(语言服务器协议)的支持,能自动根据项目语言加载相应语言服务器,增强代码理解能力[\[49\]](https://github.com/anomalyco/opencode#:~:text=close%20and%20pricing%20will%20drop,what%27s%20possible%20in%20the%20terminal)。 + +**安装与初始化:** 安装OpenCode非常简单,可通过npm、Homebrew等包管理器一键安装[\[56\]](https://github.com/anomalyco/opencode#:~:text=Installation)[\[57\]](https://github.com/anomalyco/opencode#:~:text=npm%20i%20,Any%20OS)。安装后,启动前需要提供LLM的API密钥并进行简单配置[\[58\]](https://opencode.ai/docs#:~:text=Configure)[\[59\]](https://opencode.ai/docs#:~:text=If%20you%20are%20new%20to,verified%20by%20the%20OpenCode%20team)。进入项目目录运行opencode,首次使用可执行命令/init让OpenCode扫描项目,生成AGENTS.md索引文件[\[60\]](https://opencode.ai/docs#:~:text=Next%2C%20initialize%20OpenCode%20for%20the,by%20running%20the%20following%20command)。这个文件记录了项目的结构和代码模式,有点像项目的"知识图谱",建议加入版本管理[\[61\]](https://opencode.ai/docs#:~:text=This%20will%20get%20OpenCode%20to,file%20in%20the%20project%20root)。有了它,OpenCode在对话中就能更好地引用项目文件、理解代码上下文。 + +**工作模式与命令:** OpenCode提供了**TUI交互界面**,你可以像与人聊天一样对它下指令。例如:"请在settings.ts中添加与notes.ts类似的身份验证代码"[\[62\]](https://opencode.ai/docs#:~:text=Make%20changes)。OpenCode会理解你的自然语言,自动完成在指定文件中插入代码的操作。其强大之处在于内置了**两种Agent模式**,可通过<kbd>TAB</kbd>键切换:[\[51\]](https://github.com/anomalyco/opencode#:~:text=OpenCode%20includes%20two%20built,key) + +- **Plan模式(规划模式)** - _"顾问"_:该模式下AI具有只读分析权限,不直接修改代码[\[51\]](https://github.com/anomalyco/opencode#:~:text=OpenCode%20includes%20two%20built,key)。当你提出新需求,OpenCode建议先按<kbd>TAB</kbd>切换到Plan模式,它会给出实现计划[\[63\]](https://opencode.ai/docs#:~:text=1)。例如你说"添加软删除功能",Plan模式下AI可能回复:"1)在数据库加deleted字段,2)后端新增恢复接口,3)前端加回收站页面…"[\[64\]](https://opencode.ai/docs#:~:text=Now%20let%E2%80%99s%20describe%20what%20we,want%20it%20to%20do)。规划模式会**禁用自动执行**,确保AI的提议经过你审阅。它甚至会在执行外部命令前征求同意(例如运行bash或安装依赖,需要你确认)[\[65\]](https://github.com/anomalyco/opencode#:~:text=%2A%20build%20,unfamiliar%20codebases%20or%20planning%20changes)。**最佳实践**是先让AI在Plan模式列出步骤和方案,开发者可以补充或更正细节[\[66\]](https://opencode.ai/docs#:~:text=2)。如觉得方案不完善,可继续对话调整直到满意为止[\[67\]](https://opencode.ai/docs#:~:text=2)。 +- **Build模式(构建模式)** - _"执行者"_:在你接受规划后,按<kbd>TAB</kbd>回到Build模式,命令AI执行更改[\[68\]](https://opencode.ai/docs#:~:text=3)。Build模式下AI有**完全访问权限**,可以编辑文件、创建新文件、运行测试或Git命令等[\[51\]](https://github.com/anomalyco/opencode#:~:text=OpenCode%20includes%20two%20built,key)。延续上例,当你说"计划看起来不错,请动手实现",OpenCode将依次修改代码并可能运行项目验证[\[69\]](https://opencode.ai/docs#:~:text=Once%20you%20feel%20comfortable%20with,hitting%20the%20Tab%20key%20again)。它会将所有操作结果(如diff或者终端输出)显示在对话中,方便你跟踪进度。如果修改不符合预期,还可以及时喊停。**小贴士:**对于非常简单的修改,也可直接在Build模式下让AI改,无需先plan[\[62\]](https://opencode.ai/docs#:~:text=Make%20changes)--但前提是你对改动很有把握并已提供足够细节,否则直接构建可能出现偏差。 + +OpenCode在交互上还有几个贴心功能:你可以在对话中通过@文件名快速引用项目文件,AI会自动加载其内容作为上下文[\[70\]](https://opencode.ai/docs#:~:text=Tip);如果AI修改了文件,你可以用/undo命令**撤销更改**,一步步回滚到之前状态[\[71\]](https://opencode.ai/docs#:~:text=Let%E2%80%99s%20say%20you%20ask%20OpenCode,to%20make%20some%20changes);完成某个任务后,用/share可以生成对话的分享链接,方便团队其他成员查看这次AI改动的经过[\[72\]](https://opencode.ai/docs#:~:text=Share)。这些特性能鼓励开发者大胆尝试AI修改,因为**所有操作都可追溯和撤销**,相当于给AI上了一道"安全网"。 + +**最佳实践提示:** 使用OpenCode时,养成一些习惯可以大大提升体验: + +- **将AI当作新人**:对OpenCode的指令要清晰具体,必要时分解成子任务。它官方文档建议开发者"像对团队新人讲解任务那样与AI对话"[\[73\]](https://opencode.ai/docs#:~:text=You%20want%20to%20give%20OpenCode,junior%20developer%20on%20your%20team)。这意味着描述需求时要交代背景、"Done的定义"等,使AI少走弯路。 +- **多给示例和引用**:如果要AI写类似已有代码的功能,引用相关文件路径并指出相似之处[\[74\]](https://opencode.ai/docs#:~:text=We%20need%20to%20add%20authentication,look%20at%20how%20this%20is)。例如:"参考notes.ts里的做法,在settings.ts实现类似逻辑"[\[75\]](https://opencode.ai/docs#:~:text=without%20having%20to%20review%20the,plan%20first)。OpenCode会据此调取对应文件,提高修改正确率。 +- **审阅Plan并反馈**:Plan模式输出方案后务必仔细检查,每条步骤是否符合预期。如有遗漏或不妥,直接在对话中反馈修改。**不要吝惜反馈**--AI需要你的指引来校准方向[\[40\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L322%20When%20Claude,things%20you%E2%80%99ve%20already%20decided%20against)。充分的前期沟通会减少后期反复。 +- **小步提交**:AI每完成一部分改动,可先在本地运行测试或预览效果,再决定让其继续下一步。分阶段验收能及时发现问题,便于快速/undo回滚并调整Prompt。 + +OpenCode的出现,使个人开发者也能拥有"AI对话编程"的高效体验。有博文称其是"终端里的AI老司机",让写代码"爽到飞起"[\[76\]](https://blog.csdn.net/u012094427/article/details/148866474#:~:text=%E4%BB%8A%E5%A4%A9%E6%88%91%E4%BB%AC%E8%A6%81%E8%81%8A%E7%9A%84%E7%A1%AC%E6%A0%B8%E8%AF%9D%E9%A2%98%EF%BC%8C%E6%98%AF%E4%B8%AA%E8%AE%A9%E6%9E%81%E5%AE%A2%E4%BB%AC%E9%A2%A4%E6%8A%96%E3%80%81%E8%AE%A9%E7%A8%8B%E5%BA%8F%E5%91%98%E4%BB%AC%E5%B0%96%E5%8F%AB%EF%BC%8C%E8%AE%A9%E5%86%99%E4%BB%A3%E7%A0%81%E7%88%BD%E5%88%B0%E9%A3%9E%E8%B5%B7%E7%9A%84%E5%AD%98%E5%9C%A8%E2%80%94%E2%80%94OpenCode%EF%BC%8C%E5%BC%80%E6%BA%90AI%E7%BB%88%E7%AB%AF%E7%BC%96%E7%A0%81%E5%8A%A9%E6%89%8B%E3%80%82%20%E5%9C%A8AI%E5%85%A8%E8%83%BD%E5%86%99%E4%BB%A3%E7%A0%81%E3%80%81Copilot%E5%AE%B6%E5%A4%A7%E4%B8%9A%E5%A4%A7%E3%80%81%20)。而当OpenCode与更强大的插件Oh My OpenCode结合后,威力更是倍增--我们接下来就介绍OMO插件如何将AI辅助开发推向新的高度。 + +### Oh My OpenCode:Sisyphus多代理增强插件 + +Oh My OpenCode(简称OMO)是OpenCode的明星社区插件,由开发者Yeongyu Kim开源。它被称为"**最强Agent挂架**(Agent Harness)",内部代号"Sisyphus",寓意让AI像西西弗斯般不懈耕耘,为你完成复杂开发任务[\[77\]](https://x.com/Nateemerson/status/2002043382953288046/photo/1#:~:text=YeonGyu,and%20practices%20for%20agentic%20coding)[\[78\]](https://github.com/code-yeongyu/oh-my-opencode#:~:text=The%20Best%20Agent%20Harness,Agent%20that%20codes%20like%20you)。简单来说,OMO通过引入**多智能体协作**和**优化的提示策略**,将单一的OpenCode助手升级为一个"**AI开发团队**"。其主要特性和最佳用法包括: + +- **多代理编排系统:** Sisyphus采用前沿的**多代理协同架构**,将开发任务拆解后分配给不同专长的子代理协作完成[\[79\]](https://post.smzdm.com/p/aog8xq7m#:~:text=%E4%B8%80%E4%BA%BA%E6%8A%B5%E4%B8%80%E4%B8%AA%E5%BC%80%E5%8F%91%E5%9B%A2%E9%98%9F%E7%9A%84AI%E7%BC%96%E7%A8%8B%E7%A5%9E%E5%99%A8%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%20,%E5%9C%A8%E6%88%90%E6%9C%AC%E6%8E%A7%E5%88%B6%E6%96%B9%E9%9D%A2%EF%BC%8C%E8%AF%A5%E7%B3%BB%E7%BB%9F%E6%94%AF%E6%8C%81%E5%A4%9A%E6%A8%A1%E5%9E%8B%E6%B7%B7%E5%90%88%E4%BD%BF%E7%94%A8%E7%AD%96%E7%95%A5%E3%80%82%E9%80%9A%E8%BF%87%E9%85%8D%E7%BD%AE%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%B0%86)[\[41\]](https://www.cnblogs.com/treasury-manager/p/19217990#:~:text=OpenCode%20%E4%B8%80%E4%B8%AA%E7%A5%9E%E5%A5%87%E7%9A%84CLI%20%EF%BC%88%E5%8F%AF%E4%BB%A5%E8%9E%8D%E5%90%88Claude%20Code%2C%20iFLow,code%29%20%E2%86%90%20%E8%87%AA%E5%8A%A8%E4%BD%BF%E7%94%A8oracle%20%E7%9A%84%E6%A8%A1%E5%9E%8B%E2%86%93%20%E8%B0%83%E7%94%A8%40explore)。在OMO中,OpenCode不再只有一个AI在工作,而是一个主代理统筹,底下有如"Oracle预言家"、"Explorer探索者"、"Coder编码员"等多个子Agent各司其职[\[80\]](https://www.cnblogs.com/treasury-manager/p/19217990#:~:text=OpenCode%20%E4%B8%80%E4%B8%AA%E7%A5%9E%E5%A5%87%E7%9A%84CLI%20%EF%BC%88%E5%8F%AF%E4%BB%A5%E8%9E%8D%E5%90%88Claude%20Code%2C%20iFLow,code%29%20%E2%86%90%20%E8%87%AA%E5%8A%A8%E4%BD%BF%E7%94%A8oracle%20%E7%9A%84%E6%A8%A1%E5%9E%8B%E2%86%93%20%E8%B0%83%E7%94%A8%40explore)。例如,当你抛给系统一个大型需求,主代理会调用Oracle子代理来分析需求/查询知识,再调用Explorer代理搜索代码库甚至外部资料,最后由Coder代理生成代码方案,主代理综合各方反馈给出最终实现[\[80\]](https://www.cnblogs.com/treasury-manager/p/19217990#:~:text=OpenCode%20%E4%B8%80%E4%B8%AA%E7%A5%9E%E5%A5%87%E7%9A%84CLI%20%EF%BC%88%E5%8F%AF%E4%BB%A5%E8%9E%8D%E5%90%88Claude%20Code%2C%20iFLow,code%29%20%E2%86%90%20%E8%87%AA%E5%8A%A8%E4%BD%BF%E7%94%A8oracle%20%E7%9A%84%E6%A8%A1%E5%9E%8B%E2%86%93%20%E8%B0%83%E7%94%A8%40explore)。这种**软并行**的流程让复杂任务的处理效率和质量显著提高--相当于同时拥有多位AI专家在协同开发。 +- **多模型混合策略:** OMO支持配置不同的LLM模型给不同子代理,以充分利用各模型所长并控制成本[\[79\]](https://post.smzdm.com/p/aog8xq7m#:~:text=%E4%B8%80%E4%BA%BA%E6%8A%B5%E4%B8%80%E4%B8%AA%E5%BC%80%E5%8F%91%E5%9B%A2%E9%98%9F%E7%9A%84AI%E7%BC%96%E7%A8%8B%E7%A5%9E%E5%99%A8%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%20,%E5%9C%A8%E6%88%90%E6%9C%AC%E6%8E%A7%E5%88%B6%E6%96%B9%E9%9D%A2%EF%BC%8C%E8%AF%A5%E7%B3%BB%E7%BB%9F%E6%94%AF%E6%8C%81%E5%A4%9A%E6%A8%A1%E5%9E%8B%E6%B7%B7%E5%90%88%E4%BD%BF%E7%94%A8%E7%AD%96%E7%95%A5%E3%80%82%E9%80%9A%E8%BF%87%E9%85%8D%E7%BD%AE%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%B0%86)。例如主代理和编码代理用Claude的高性能模型,而探索/搜索代理用开源模型或较廉价的API,以降低开销[\[79\]](https://post.smzdm.com/p/aog8xq7m#:~:text=%E4%B8%80%E4%BA%BA%E6%8A%B5%E4%B8%80%E4%B8%AA%E5%BC%80%E5%8F%91%E5%9B%A2%E9%98%9F%E7%9A%84AI%E7%BC%96%E7%A8%8B%E7%A5%9E%E5%99%A8%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%20,%E5%9C%A8%E6%88%90%E6%9C%AC%E6%8E%A7%E5%88%B6%E6%96%B9%E9%9D%A2%EF%BC%8C%E8%AF%A5%E7%B3%BB%E7%BB%9F%E6%94%AF%E6%8C%81%E5%A4%9A%E6%A8%A1%E5%9E%8B%E6%B7%B7%E5%90%88%E4%BD%BF%E7%94%A8%E7%AD%96%E7%95%A5%E3%80%82%E9%80%9A%E8%BF%87%E9%85%8D%E7%BD%AE%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%B0%86)。有文章指出OMO可以通过配置,实现"大模型做决策,小模型跑腿"的组合,在保证效果的同时节约预算[\[79\]](https://post.smzdm.com/p/aog8xq7m#:~:text=%E4%B8%80%E4%BA%BA%E6%8A%B5%E4%B8%80%E4%B8%AA%E5%BC%80%E5%8F%91%E5%9B%A2%E9%98%9F%E7%9A%84AI%E7%BC%96%E7%A8%8B%E7%A5%9E%E5%99%A8%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%20,%E5%9C%A8%E6%88%90%E6%9C%AC%E6%8E%A7%E5%88%B6%E6%96%B9%E9%9D%A2%EF%BC%8C%E8%AF%A5%E7%B3%BB%E7%BB%9F%E6%94%AF%E6%8C%81%E5%A4%9A%E6%A8%A1%E5%9E%8B%E6%B7%B7%E5%90%88%E4%BD%BF%E7%94%A8%E7%AD%96%E7%95%A5%E3%80%82%E9%80%9A%E8%BF%87%E9%85%8D%E7%BD%AE%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%B0%86)。这对需要长时间运行的自动化Agent特别重要。实际使用中,很多人将GPT-4、Claude、甚至本地模型共同挂接到OMO,让它们各尽其用,整套系统智能且经济。 +- **全流程自动执行(Ultrawork模式):** **"不用读文档,上来就干!"** 这是不少OMO用户的直观感受[\[81\]](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/README.zh-cn.md#:~:text=oh,%E8%80%81%E5%AE%9E%E8%AF%B4%EF%BC%8C%E7%94%9A%E8%87%B3%E4%B8%8D%E7%94%A8%E8%B4%B9%E5%BF%83%E8%AF%BB%E6%96%87%E6%A1%A3%E3%80%82%E5%8F%AA%E9%9C%80%E5%86%99%E4%BD%A0%E7%9A%84%E6%8F%90%E7%A4%BA%E3%80%82%E5%8C%85%E5%90%AB%27ultrawork%27%20%E5%85%B3%E9%94%AE%E8%AF%8D%E3%80%82Sisyphus%20%E4%BC%9A%E5%88%86%E6%9E%90%E7%BB%93%E6%9E%84%EF%BC%8C%E6%94%B6%E9%9B%86%E4%B8%8A%E4%B8%8B%E6%96%87%EF%BC%8C%E6%8C%96%E6%8E%98%E5%A4%96%E9%83%A8%E6%BA%90%E4%BB%A3%E7%A0%81%EF%BC%8C%E7%84%B6%E5%90%8E%E6%8C%81%E7%BB%AD%E6%8E%A8%E8%BF%9B)。OMO内置了一种特殊的提示触发机制,例如当在对话中包含关键词"ultrawork"时,Sisyphus会**自动接管任务**,从分析、检索到编码一步步执行下去,几乎无需人介入[\[81\]](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/README.zh-cn.md#:~:text=oh,%E8%80%81%E5%AE%9E%E8%AF%B4%EF%BC%8C%E7%94%9A%E8%87%B3%E4%B8%8D%E7%94%A8%E8%B4%B9%E5%BF%83%E8%AF%BB%E6%96%87%E6%A1%A3%E3%80%82%E5%8F%AA%E9%9C%80%E5%86%99%E4%BD%A0%E7%9A%84%E6%8F%90%E7%A4%BA%E3%80%82%E5%8C%85%E5%90%AB%27ultrawork%27%20%E5%85%B3%E9%94%AE%E8%AF%8D%E3%80%82Sisyphus%20%E4%BC%9A%E5%88%86%E6%9E%90%E7%BB%93%E6%9E%84%EF%BC%8C%E6%94%B6%E9%9B%86%E4%B8%8A%E4%B8%8B%E6%96%87%EF%BC%8C%E6%8C%96%E6%8E%98%E5%A4%96%E9%83%A8%E6%BA%90%E4%BB%A3%E7%A0%81%EF%BC%8C%E7%84%B6%E5%90%8E%E6%8C%81%E7%BB%AD%E6%8E%A8%E8%BF%9B)。开发者只要提出高层次目标,如"ultrawork: 实现一个博客网站的前后端,包括用户注册、发帖、评论"等,接下来Sisyphus就会自主规划子任务、调用子代理查资料、写代码、测试,迭代直到完成[\[81\]](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/README.zh-cn.md#:~:text=oh,%E8%80%81%E5%AE%9E%E8%AF%B4%EF%BC%8C%E7%94%9A%E8%87%B3%E4%B8%8D%E7%94%A8%E8%B4%B9%E5%BF%83%E8%AF%BB%E6%96%87%E6%A1%A3%E3%80%82%E5%8F%AA%E9%9C%80%E5%86%99%E4%BD%A0%E7%9A%84%E6%8F%90%E7%A4%BA%E3%80%82%E5%8C%85%E5%90%AB%27ultrawork%27%20%E5%85%B3%E9%94%AE%E8%AF%8D%E3%80%82Sisyphus%20%E4%BC%9A%E5%88%86%E6%9E%90%E7%BB%93%E6%9E%84%EF%BC%8C%E6%94%B6%E9%9B%86%E4%B8%8A%E4%B8%8B%E6%96%87%EF%BC%8C%E6%8C%96%E6%8E%98%E5%A4%96%E9%83%A8%E6%BA%90%E4%BB%A3%E7%A0%81%EF%BC%8C%E7%84%B6%E5%90%8E%E6%8C%81%E7%BB%AD%E6%8E%A8%E8%BF%9B)。在B站等平台的演示视频中,可以看到OpenCode + OMO真的实现了**全程零干预**地产出项目雏形[\[82\]](https://www.youtube.com/watch?v=twFjLiy2Pmc#:~:text=%E8%A7%86%E9%A2%91%E7%AE%80%E4%BB%8B%EF%BC%9A%20%E6%9C%AC%E6%9C%9F%E8%A7%86%E9%A2%91%E8%AF%A6%E7%BB%86%E6%BC%94%E7%A4%BA%E4%BA%86%E5%A6%82%E4%BD%95%E5%9C%A8Opencode%E4%B8%AD%E4%BD%BF%E7%94%A8%E6%9C%80%E5%BC%BA%E5%BC%80%E6%BA%90%E6%8F%92%E4%BB%B6Oh%20My%20Opencode%EF%BC%88OMO%EF%BC%89%EF%BC%8C%E5%B0%86%E5%8D%95%E4%B8%80AI%E7%BC%96%E7%A8%8B%E5%8A%A9%E6%89%8B%E5%8D%87%E7%BA%A7%E4%B8%BA%E5%A4%9A%E6%A8%A1%E5%9E%8B%E5%8D%8F%E4%BD%9C%E7%9A%84AI%E5%BC%80%E5%8F%91%E5%9B%A2%E9%98%9F%EF%BC%81)[\[83\]](https://x.com/AISuperDomain/status/2009823408301994209#:~:text=OpenCode%E7%9A%84%E6%9C%80%E5%BC%BA%E5%BC%80%E6%BA%90%E6%8F%92%E4%BB%B6oh%20my%20opencode%E7%A1%AE%E5%AE%9E%E9%9D%9E%E5%B8%B8%E5%A5%BD%E7%94%A8%20%E8%A7%86%E9%A2%91%E7%AE%80%E4%BB%8B%EF%BC%9A%20%E6%9C%AC%E6%9C%9F%E8%A7%86%E9%A2%91%E8%AF%A6%E7%BB%86%E6%BC%94%E7%A4%BA%E4%BA%86%E5%A6%82%E4%BD%95%E5%9C%A8Opencode%E4%B8%AD%E4%BD%BF%E7%94%A8%E6%9C%80%E5%BC%BA%E5%BC%80%E6%BA%90%E6%8F%92%E4%BB%B6Oh,My%20Opencode%EF%BC%88OMO%EF%BC%89%EF%BC%8C%E5%B0%86%E5%8D%95%E4%B8%80AI%E7%BC%96%E7%A8%8B%E5%8A%A9%E6%89%8B%E5%8D%87%E7%BA%A7%E4%B8%BA%E5%A4%9A%E6%A8%A1%E5%9E%8B%E5%8D%8F%E4%BD%9C%E7%9A%84AI%E5%BC%80%E5%8F%91%E5%9B%A2%E9%98%9F%EF%BC%81%20%E6%A0%B8%E5%BF%83%E5%86%85%E5%AE%B9%EF%BC%9AOMO%E6%8F%92%E4%BB%B6)。当然如此长链路的执行可能偶有偏差,但这展示了Agentic AI开发的巨大潜力。对于使用OMO的我们,建议在较明确且可分解的项目上尝试"Ultrawork"模式,静待AI团队跑完全流程,再对结果集中验收修改。 +- **可定制 Hooks 与插件:** OMO不仅提供默认的智能体团队,还允许高级用户扩展自定义**Hooks**和插件逻辑[\[84\]](https://blog.csdn.net/gitblog_00895/article/details/144862506#:~:text=oh,)。其Hooks系统支持在AI执行过程的关键节点插入自定义代码,以实现特殊监控或操作[\[84\]](https://blog.csdn.net/gitblog_00895/article/details/144862506#:~:text=oh,)。比如可以在每次子代理调用外部API前触发Hook检查额度,或在生成代码后通过Hook自动运行格式化工具[\[84\]](https://blog.csdn.net/gitblog_00895/article/details/144862506#:~:text=oh,)。这赋予OMO极高的可扩展性,能够适应各种个性化需求。因此最佳实践是:在熟练掌握默认OMO能力后,再根据自己团队的流程编写Hooks或定制代理,打造**专属的AI流水线**。目前社区也涌现了许多OMO的附加插件和配置心得,可借鉴来强化你的AI团队。 + +作为OpenCode的"超充电"版,Oh My OpenCode显著提升了AI编程助手的自主性和专业度。有用户戏称:"用了OMO,仿佛一个人带着一支AI舰队写代码"--一人顶一个团队的效率令人震撼[\[79\]](https://post.smzdm.com/p/aog8xq7m#:~:text=%E4%B8%80%E4%BA%BA%E6%8A%B5%E4%B8%80%E4%B8%AA%E5%BC%80%E5%8F%91%E5%9B%A2%E9%98%9F%E7%9A%84AI%E7%BC%96%E7%A8%8B%E7%A5%9E%E5%99%A8%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%20,%E5%9C%A8%E6%88%90%E6%9C%AC%E6%8E%A7%E5%88%B6%E6%96%B9%E9%9D%A2%EF%BC%8C%E8%AF%A5%E7%B3%BB%E7%BB%9F%E6%94%AF%E6%8C%81%E5%A4%9A%E6%A8%A1%E5%9E%8B%E6%B7%B7%E5%90%88%E4%BD%BF%E7%94%A8%E7%AD%96%E7%95%A5%E3%80%82%E9%80%9A%E8%BF%87%E9%85%8D%E7%BD%AE%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%B0%86)[\[85\]](https://post.smzdm.com/p/aog8xq7m#:~:text=%E5%9C%A8%E6%88%90%E6%9C%AC%E6%8E%A7%E5%88%B6%E6%96%B9%E9%9D%A2%EF%BC%8C%E8%AF%A5%E7%B3%BB%E7%BB%9F%E6%94%AF%E6%8C%81%E5%A4%9A%E6%A8%A1%E5%9E%8B%E6%B7%B7%E5%90%88%E4%BD%BF%E7%94%A8%E7%AD%96%E7%95%A5%E3%80%82%E9%80%9A%E8%BF%87%E9%85%8D%E7%BD%AE%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%B0%86%20)。不过也需注意,OMO毕竟处于新兴阶段,全自动模式下如果任务不清晰,AI可能产生偏差 output。因此,**明确的任务描述**依然是成功的前提。在对OMO下达指令时,尽量描述清楚最终期望和约束条件,并监控关键输出节点。如果结果不理想,可以通过交互引导主代理调整方案或重试特定子任务。相信随着社区不断优化Sisyphus的提示技巧和反馈机制,OMO会变得越来越"稳"。对于敢于尝新的开发者,它无疑是值得深入研究的利器:熟练掌握后,你将拥有前所未有的**开发自动化能力**,显著缩短从想法到产品的路径。 + +## 总结 + +从Claude Code Skills到OpenCode再到Oh My OpenCode,我们见证了AI辅助开发从**手工提示**走向**模块化复用**、从**单点对话**走向**多智能体协作**的演进。Claude Code Skills教会我们如何将**提示词工程升级为可复用的技能模块**--通过YAML结构化知识,渐进披露上下文,让AI随时调用专业技能包,大幅减少重复劳动[\[5\]](https://cloud.tencent.com/developer/article/2616585#:~:text=Claude%20Skills%E6%98%AFAnthropic%E6%8E%A8%E5%87%BA%E7%9A%84AI%E5%8A%9F%E8%83%BD%E9%9D%A9%E5%91%BD%EF%BC%8C%E5%8F%AF%E5%B0%86%E7%94%A8%E6%88%B7%E4%BD%BF%E7%94%A8AI%E7%9A%84%E4%B9%A0%E6%83%AF%E6%96%87%E4%BB%B6%E5%8C%96%E7%AE%A1%E7%90%86%E3%80%82%E5%AE%83%E8%83%BD%E8%A7%A3%E5%86%B3Claude%E5%81%A5%E5%BF%98%E3%80%81%E9%9C%80%E9%87%8D%E5%A4%8D%E6%8F%90%E7%A4%BA%E8%AF%8D%E7%9A%84%E9%97%AE%E9%A2%98%EF%BC%8C%E5%B0%86%E4%BB%BB%E5%8A%A1%E8%AF%B4%E6%98%8E%E3%80%81%E5%B7%A5%E5%85%B7%E4%BB%A3%E7%A0%81%E7%AD%89%E6%89%93%E5%8C%85%E6%88%90%20)[\[6\]](https://www.facebook.com/iamvista/photos/%E6%9F%90%E5%A4%A9%E6%B7%B1%E5%A4%9C%E6%88%91%E6%AD%A3%E5%9C%A8%E8%B6%95%E4%B8%80%E4%BB%BD%E6%96%87%E4%BB%B6%E5%A4%A9%E5%95%8A%E5%90%8C%E6%A8%A3%E7%9A%84%E6%9E%B6%E6%A7%8B%E5%90%8C%E6%A8%A3%E7%9A%84%E8%AA%9E%E6%B0%A3%E5%90%8C%E6%A8%A3%E7%9A%84%E6%A0%BC%E5%BC%8F%E8%A6%81%E6%B1%82%E4%BD%86%E6%88%91%E5%8F%88%E5%BE%97%E9%87%8D%E6%96%B0%E6%89%93%E4%B8%80%E6%AC%A1%E8%AB%8B%E7%94%A8%E9%80%99%E5%80%8B%E6%A0%BC%E5%BC%8F%E8%AB%8B%E5%85%88%E5%95%8F%E4%B8%89%E5%80%8B%E6%BE%84%E6%B8%85%E5%95%8F%E9%A1%8C%E8%AB%8B%E6%8A%8A%E8%BC%B8%E5%87%BA%E5%88%86%E6%88%90%E5%9B%9B%E6%AE%B5%E8%AB%8B%E9%99%84%E4%B8%8A%E5%8F%AF%E7%9B%B4%E6%8E%A5%E8%B2%BC%E5%88%B0-notion-%E7%9A%84/10162293093624053/#:~:text=,%E6%88%91%E6%80%8E%E9%BA%BC%E5%81%9A%E4%B8%80%E5%80%8B%E5%A5%BDskill%EF%BC%8C%E8%AE%8A%E6%88%90skill)。OpenCode则以开源之力,将AI编程助手引入开发者日常工具链,在终端或IDE中实现了与AI无缝结对编程[\[48\]](https://zhuanlan.zhihu.com/p/1991170184573122515#:~:text=OpenCode%20%E6%98%AF%E4%B8%80%E4%B8%AAAI%20%E7%BC%96%E7%A8%8B%E5%8A%A9%E6%89%8B%EF%BC%8C%E8%B7%91%E5%9C%A8%E4%BD%A0%E7%9A%84%E7%BB%88%E7%AB%AF%E9%87%8C%EF%BC%88%E5%B0%B1%E6%98%AF%E9%82%A3%E4%B8%AA%E9%BB%91%E8%89%B2%E7%AA%97%E5%8F%A3%EF%BC%89%E3%80%82%20%E4%BD%A0%E8%B7%9F%E5%AE%83%E8%AF%B4%E8%AF%9D%EF%BC%8C%E5%AE%83%E5%B0%B1%E5%B8%AE%E4%BD%A0%E5%86%99%E4%BB%A3%E7%A0%81%E3%80%82%20,%E2%86%92%20%E5%AE%83%E6%94%B9)。其Plan/Build双模式和Undo/Redo等机制,被证明是安全高效地让AI参与编码的最佳实践[\[63\]](https://opencode.ai/docs#:~:text=1)[\[71\]](https://opencode.ai/docs#:~:text=Let%E2%80%99s%20say%20you%20ask%20OpenCode,to%20make%20some%20changes)。而Oh My OpenCode更进一步,引入多Agent架构和自动化工作流,展示了"AI团队开发"的雏形[\[79\]](https://post.smzdm.com/p/aog8xq7m#:~:text=%E4%B8%80%E4%BA%BA%E6%8A%B5%E4%B8%80%E4%B8%AA%E5%BC%80%E5%8F%91%E5%9B%A2%E9%98%9F%E7%9A%84AI%E7%BC%96%E7%A8%8B%E7%A5%9E%E5%99%A8%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%20,%E5%9C%A8%E6%88%90%E6%9C%AC%E6%8E%A7%E5%88%B6%E6%96%B9%E9%9D%A2%EF%BC%8C%E8%AF%A5%E7%B3%BB%E7%BB%9F%E6%94%AF%E6%8C%81%E5%A4%9A%E6%A8%A1%E5%9E%8B%E6%B7%B7%E5%90%88%E4%BD%BF%E7%94%A8%E7%AD%96%E7%95%A5%E3%80%82%E9%80%9A%E8%BF%87%E9%85%8D%E7%BD%AE%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%B0%86)[\[85\]](https://post.smzdm.com/p/aog8xq7m#:~:text=%E5%9C%A8%E6%88%90%E6%9C%AC%E6%8E%A7%E5%88%B6%E6%96%B9%E9%9D%A2%EF%BC%8C%E8%AF%A5%E7%B3%BB%E7%BB%9F%E6%94%AF%E6%8C%81%E5%A4%9A%E6%A8%A1%E5%9E%8B%E6%B7%B7%E5%90%88%E4%BD%BF%E7%94%A8%E7%AD%96%E7%95%A5%E3%80%82%E9%80%9A%E8%BF%87%E9%85%8D%E7%BD%AE%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%B0%86%20)。利用OMO,复杂项目也许只需提出愿景,AI代理即可协同完成大部分实现,开发者转为高层监督和收尾调整的角色。 + +当然,再强大的AI工具也需要人来驾驭。**明确的问题定义、合理的分工接口、及时的反馈干预**,依然是成功应用AI的关键。对于Golang+Vue3这类前后端结合的项目,我们强调了提供充足上下文、分步指导AI、结合测试验证等提示词设计要点,以充分发挥Claude/GPT-4的长处[\[39\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L316%20Give%20Claude,context%2C%20the%20better%20the%20suggestions)[\[40\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L322%20When%20Claude,things%20you%E2%80%99ve%20already%20decided%20against)。当这些经验与像OpenCode这样的平台结合,AI辅助编程就真正融入了软件开发生命周期。从代码生成、重构到调试、测试,各环节皆有AI参与,其价值不再停留在"生成几段代码"上,而是成为一种**开发范式的升级**。 + +正如Anthropic在文档中所言:未来的开发者将不仅仅会写代码,更要善于**打造和利用AI技能**[\[23\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=1)。通过模块化的Skills,我们可以将个人经验沉淀,让AI持续进化成为领域专家;通过工具与插件,我们可以让AI深度融入团队协作,实现人机共创。当下的这些探索,正是迈向"自我进化软件"的垫脚石。希望本指南的调研和总结,能帮助你在实践中少走弯路,加速拥抱AI赋能的全新开发模式。在不远的将来,写代码或许就像与聪明的同事对话一样自然,而我们将有更多时间专注于创造真正有价值的东西。让我们拭目以待,也积极参与这一场AI与编程的革命吧! + +**参考文献:** + +- Anthropic Claude Skills 官方文档及教程[\[9\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=%E6%8A%80%E8%83%BD%E5%AE%9A%E4%B9%89%E6%A0%BC%E5%BC%8F)[\[86\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=%E4%B8%BA%E4%BA%86%E9%81%BF%E5%85%8D%E2%80%9C%E4%BB%8B%E7%BB%8D%E5%8A%9F%E8%83%BD%E4%BD%86%E7%BC%BA%E5%B0%91%E5%8F%AF%E6%A3%80%E9%AA%8C%E7%BB%93%E8%AE%BA%E2%80%9D%EF%BC%8C%E6%9C%AC%E6%96%87%E5%85%88%E6%8A%8A%20Skills%20%E7%9A%84%E6%9E%B6%E6%9E%84%E4%BB%B7%E5%80%BC%E6%94%B6%E6%95%9B%E6%88%90%E4%B8%80%E5%8F%A5%E5%8F%AF%E9%AA%8C%E8%AF%81%E7%9A%84%E5%91%BD%E9%A2%98%EF%BC%9A) +- 社区博客与实践案例(CSDN、知乎等)对Claude Code/Skills的解读[\[15\]](https://blog.csdn.net/yangshangwei/article/details/156836796#:~:text=%E4%B8%80%E4%B8%AA%E5%8F%AF%E8%90%BD%E5%9C%B0%E7%9A%84%E4%B8%89%E6%AD%A5%E6%B3%95,3%20%E7%AC%AC%E4%B8%89%E6%AD%A5%EF%BC%9A%E6%8A%8AClaude%20%E5%BD%93%E2%80%9C%E6%90%AD%E5%AD%90%E2%80%9D%EF%BC%8C%E8%80%8C%E4%B8%8D%E6%98%AF%E6%90%9C%E7%B4%A2%E6%A1%86)[\[18\]](https://hbwdj.gov.cn/appbdetail-imqqsmrp9897358.d#:~:text=%E9%AA%97%E4%BD%A0%E7%9A%84%EF%BC%8C%E5%85%B6%E5%AE%9EAI%E6%A0%B9%E6%9C%AC%E4%B8%8D%E9%9C%80%E8%A6%81%E9%82%A3%E4%B9%88%E5%A4%9A%E6%8F%90%E7%A4%BA%E8%AF%8D%E3%80%82%20%E4%BD%A0%E5%8F%AA%E9%9C%80%E8%A6%81%E8%B0%83%E7%94%A8AI%20%E6%9C%AC%E8%BA%AB%E7%9A%84%E2%80%9CSkill%20Creator%E2%80%9D%E6%8A%80%E8%83%BD%EF%BC%8C%E7%94%A8%E4%BD%A0%E7%9A%84%E8%AF%AD%E8%A8%80%E6%8F%8F%E8%BF%B0%E8%87%AA%E5%B7%B1%E7%9A%84%E9%9C%80%E6%B1%82%EF%BC%8C%E8%AE%A9AI%E8%87%AA%E5%8A%A8%E5%B8%AE%E4%BD%A0%E7%94%9F%E6%88%90%E4%B8%80%E9%97%A8%E6%8A%80%E8%83%BD%EF%BC%8C%E4%BD%BF%E7%94%A8%E8%B5%B7%E6%9D%A5%E9%9D%9E%E5%B8%B8%E5%8F%8B%E5%A5%BD%EF%BC%8CAI%E4%BC%9A%E4%B8%80%E6%AD%A5%E6%AD%A5%E5%BC%95%E5%AF%BC%E4%BD%A0%E8%AF%B4%E5%87%BA%E4%BD%A0%E7%9A%84%E9%9C%80%E6%B1%82%EF%BC%8C%E4%BD%A0%E5%8F%AA%20) +- OpenCode项目仓库README及官方文档[\[51\]](https://github.com/anomalyco/opencode#:~:text=OpenCode%20includes%20two%20built,key)[\[52\]](https://opencode.ai/docs#:~:text=1) +- Oh My OpenCode项目介绍、社区经验分享[\[80\]](https://www.cnblogs.com/treasury-manager/p/19217990#:~:text=OpenCode%20%E4%B8%80%E4%B8%AA%E7%A5%9E%E5%A5%87%E7%9A%84CLI%20%EF%BC%88%E5%8F%AF%E4%BB%A5%E8%9E%8D%E5%90%88Claude%20Code%2C%20iFLow,code%29%20%E2%86%90%20%E8%87%AA%E5%8A%A8%E4%BD%BF%E7%94%A8oracle%20%E7%9A%84%E6%A8%A1%E5%9E%8B%E2%86%93%20%E8%B0%83%E7%94%A8%40explore)[\[81\]](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/README.zh-cn.md#:~:text=oh,%E8%80%81%E5%AE%9E%E8%AF%B4%EF%BC%8C%E7%94%9A%E8%87%B3%E4%B8%8D%E7%94%A8%E8%B4%B9%E5%BF%83%E8%AF%BB%E6%96%87%E6%A1%A3%E3%80%82%E5%8F%AA%E9%9C%80%E5%86%99%E4%BD%A0%E7%9A%84%E6%8F%90%E7%A4%BA%E3%80%82%E5%8C%85%E5%90%AB%27ultrawork%27%20%E5%85%B3%E9%94%AE%E8%AF%8D%E3%80%82Sisyphus%20%E4%BC%9A%E5%88%86%E6%9E%90%E7%BB%93%E6%9E%84%EF%BC%8C%E6%94%B6%E9%9B%86%E4%B8%8A%E4%B8%8B%E6%96%87%EF%BC%8C%E6%8C%96%E6%8E%98%E5%A4%96%E9%83%A8%E6%BA%90%E4%BB%A3%E7%A0%81%EF%BC%8C%E7%84%B6%E5%90%8E%E6%8C%81%E7%BB%AD%E6%8E%A8%E8%BF%9B) +- Korbinian Schleifer: _Learning Go and Vue with Claude AI as my Pair Programmer_[\[39\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L316%20Give%20Claude,context%2C%20the%20better%20the%20suggestions)[\[40\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L322%20When%20Claude,things%20you%E2%80%99ve%20already%20decided%20against) +- 更多详见上述来源引用[\[8\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=my,%E8%BE%85%E5%8A%A9%E8%84%9A%E6%9C%AC%EF%BC%88%E5%8F%AF%E9%80%89%EF%BC%89)[\[68\]](https://opencode.ai/docs#:~:text=3)等。 + +[\[1\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=%E4%BB%80%E4%B9%88%E6%98%AF%20Claude%20Skills%EF%BC%9F) [\[7\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=%E6%8A%80%E8%83%BD%E7%BB%93%E6%9E%84%EF%BC%9A) [\[8\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=my,%E8%BE%85%E5%8A%A9%E8%84%9A%E6%9C%AC%EF%BC%88%E5%8F%AF%E9%80%89%EF%BC%89) [\[9\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=%E6%8A%80%E8%83%BD%E5%AE%9A%E4%B9%89%E6%A0%BC%E5%BC%8F) [\[10\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=,%E7%B1%BB%E5%88%AB2) [\[11\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=) [\[12\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=Anthropic%20Claude%20Skills%20%E6%98%AF%E5%8C%85%E5%90%AB%E6%8C%87%E4%BB%A4%E3%80%81%E8%84%9A%E6%9C%AC%E5%92%8C%E8%B5%84%E6%BA%90%E7%9A%84%E6%96%87%E4%BB%B6%E5%A4%B9%EF%BC%8CClaude%20%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E4%BB%A5%E6%8F%90%E9%AB%98%E4%B8%93%E4%B8%9A%E4%BB%BB%E5%8A%A1%E7%9A%84%E6%80%A7%E8%83%BD%E3%80%82) [\[13\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=) [\[16\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=,%E9%A2%84%E7%AE%97%E6%98%8E%E7%BB%86%20%E5%90%AF%E7%94%A8%E4%BF%AE%E8%AE%A2%E8%B7%9F%E8%B8%AA%E4%BB%A5%E4%BE%9B%E5%AE%A1%E6%9F%A5) [\[17\]](https://ide.unitmesh.cc/spec/claude-skill#:~:text=) Claude Skill | AutoDev - Tailor Your AI Coding Experience + + + +[\[2\]](https://www.bilibili.com/opus/1132124852115734530#:~:text=Claude%20Skills%20%E6%9C%AC%E8%B4%A8%E4%B8%8A%E6%98%AF%E4%B8%80%E7%A7%8D,) [\[30\]](https://www.bilibili.com/opus/1132124852115734530#:~:text=Claude%20Skills%20%E5%AE%9E%E6%88%98%E6%8C%87%E5%8D%97%EF%BC%9A3%20%E5%88%86%E9%92%9F%E6%90%9E%E5%AE%9APPT%E3%80%81%E6%B5%B7%E6%8A%A5%E4%B8%8ELogo%20,%E5%AE%83%E5%B0%86%E5%B8%B8%E8%A7%81%E7%9A%84%E5%8A%9E%E5%85%AC%E4%BB%BB%E5%8A%A1%28%E5%A6%82Excel%20%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90%E3%80%81PPT%20%E6%BC%94%E7%A4%BA%E7%94%9F%E6%88%90%E3%80%81%E6%96%87%E6%A1%A3%E5%A4%84%E7%90%86%E3%80%81%E5%93%81%E7%89%8C%E8%AE%BE%E8%AE%A1%E7%AD%89%29%E5%B0%81%E8%A3%85%E6%88%90%E5%8F%AF%E5%A4%8D%E7%94%A8%E7%9A%84%E6%8A%80%E8%83%BD%E6%A8%A1%E5%9D%97%E3%80%82%E8%BF%99%E7%A7%8D%E8%AE%BE%E8%AE%A1%E7%90%86%E5%BF%B5%E7%9A%84%E6%A0%B8%E5%BF%83%E4%BC%98%E5%8A%BF) Claude Skills 实战指南:3 分钟搞定PPT、海报与Logo - Bilibili + + + +[\[3\]](https://claudecn.com/#:~:text=Agent%20Skills%20%E5%8F%AF%E5%A4%8D%E7%94%A8%E7%9A%84%E6%8A%80%E8%83%BD%E7%B3%BB%E7%BB%9F%EF%BC%8C%E8%AE%A9%20Claude%20%E6%8E%8C%E6%8F%A1%E7%89%B9%E5%AE%9A%E9%A2%86%E5%9F%9F%E4%B8%93%E4%B8%9A%E7%9F%A5%E8%AF%86%E3%80%82%E5%8C%85%E6%8B%AC,%E8%BF%9E%E6%8E%A5%E5%A4%96%E9%83%A8%E5%B7%A5%E5%85%B7%E5%92%8C%E6%95%B0%E6%8D%AE%E6%BA%90%EF%BC%8C%E6%89%A9%E5%B1%95%20Claude%20%E7%9A%84%E8%83%BD%E5%8A%9B%E8%BE%B9%E7%95%8C%EF%BC%8C%E6%9E%84%E5%BB%BA%E5%BC%BA%E5%A4%A7%E7%9A%84%20AI%20%E5%BA%94%E7%94%A8%E7%94%9F%E6%80%81%E3%80%82) Claude 中文 - Claude AI 开发技术社区 + + + +[\[4\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=,3.2.2%20Markdown%20%E6%8C%87%E4%BB%A4%E6%AD%A3%E6%96%87) [\[23\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=1) [\[24\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=%E4%B8%BA%E4%BA%86%E9%81%BF%E5%85%8D%E2%80%9C%E4%BB%8B%E7%BB%8D%E5%8A%9F%E8%83%BD%E4%BD%86%E7%BC%BA%E5%B0%91%E5%8F%AF%E6%A3%80%E9%AA%8C%E7%BB%93%E8%AE%BA%E2%80%9D%EF%BC%8C%E6%9C%AC%E6%96%87%E5%85%88%E6%8A%8A%20Skills%20%E7%9A%84%E6%9E%B6%E6%9E%84%E4%BB%B7%E5%80%BC%E6%94%B6%E6%95%9B%E6%88%90%E4%B8%80%E5%8F%A5%E5%8F%AF%E9%AA%8C%E8%AF%81%E7%9A%84%E5%91%BD%E9%A2%98%EF%BC%9A) [\[25\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=Skills%20%E6%8A%8A%20LLM%20%E7%B3%BB%E7%BB%9F%E4%BB%8E%E2%80%9C%E6%96%87%E6%9C%AC%E5%A0%86%E5%8F%A0%E7%9A%84%E5%8D%95%E4%BD%93%E6%8F%90%E7%A4%BA%E8%AF%8D%E2%80%9D%EF%BC%8C%E9%87%8D%E6%9E%84%E4%B8%BA%E2%80%9C%E5%8F%AF%E7%89%88%E6%9C%AC%E5%8C%96%E3%80%81%E5%8F%AF%E5%AE%A1%E8%AE%A1%E3%80%81%E5%8F%AF%E7%BB%84%E5%90%88%E7%9A%84%E8%BF%90%E8%A1%8C%E6%97%B6%E6%A8%A1%E5%9D%97%E2%80%9D%EF%BC%9B%E6%A0%B8%E5%BF%83%E6%94%B6%E7%9B%8A%E6%9D%A5%E8%87%AA%E4%B8%89%E4%BB%B6%E4%BA%8B%EF%BC%9A%E4%B8%8A%E4%B8%8B%E6%96%87%E9%A2%84%E7%AE%97%E5%8F%AF%E6%8E%A7%E3%80%81%E6%89%A7%E8%A1%8C%E8%B7%AF%E5%BE%84%E5%8F%AF%E6%8E%A7%E3%80%81%E6%9D%83%E9%99%90%E8%BE%B9%E7%95%8C%E5%8F%AF%E6%8E%A7%E3%80%82) [\[26\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=,3.%20%E6%8A%80%E6%9C%AF%E8%A7%A3%E6%9E%84%EF%BC%9ASkill%20%E7%9A%84%E7%89%A9%E7%90%86%E5%BD%A2%E6%80%81%E4%B8%8E%E8%A7%84%E8%8C%83) [\[27\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=,2.3%20%E6%8A%8A%E2%80%9C%E5%88%86%E5%B1%82%E6%8A%AB%E9%9C%B2%E2%80%9D%E6%8F%90%E5%8D%87%E4%B8%BA%E8%BF%90%E8%A1%8C%E6%97%B6%E7%8A%B6%E6%80%81%E6%9C%BA) [\[28\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=,%E6%9D%83%E9%99%90%E8%BE%B9%E7%95%8C%E5%8F%AF%E6%8E%A7%EF%BC%9A%E7%94%A8%E6%B2%99%E7%AE%B1%E3%80%81%E7%BD%91%E7%BB%9C%E4%BB%A3%E7%90%86%E4%B8%8E%E6%9D%83%E9%99%90%E6%8F%90%E7%A4%BA%EF%BC%8C%E6%8A%8A%E5%B7%A5%E5%85%B7%E6%89%A7%E8%A1%8C%E9%9D%A2%E6%94%B6%E6%95%9B%E5%88%B0%E5%8F%AF%E5%AE%A1%E8%AE%A1%E3%80%81%E5%8F%AF%E6%B2%BB%E7%90%86%E7%9A%84%E8%BE%B9%E7%95%8C%E5%86%85%E3%80%82) [\[29\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=%2A%204.1%20%E7%9B%91%E7%9D%A3%E8%80%85,6.2%20%E5%8D%8F%E5%90%8C%E6%9E%B6%E6%9E%84%EF%BC%9ASkill%20%E4%BD%9C%E4%B8%BA%E7%BC%96%E6%8E%92%E8%80%85) [\[86\]](https://claudecn.com/blog/claude-skills-architecture/#:~:text=%E4%B8%BA%E4%BA%86%E9%81%BF%E5%85%8D%E2%80%9C%E4%BB%8B%E7%BB%8D%E5%8A%9F%E8%83%BD%E4%BD%86%E7%BC%BA%E5%B0%91%E5%8F%AF%E6%A3%80%E9%AA%8C%E7%BB%93%E8%AE%BA%E2%80%9D%EF%BC%8C%E6%9C%AC%E6%96%87%E5%85%88%E6%8A%8A%20Skills%20%E7%9A%84%E6%9E%B6%E6%9E%84%E4%BB%B7%E5%80%BC%E6%94%B6%E6%95%9B%E6%88%90%E4%B8%80%E5%8F%A5%E5%8F%AF%E9%AA%8C%E8%AF%81%E7%9A%84%E5%91%BD%E9%A2%98%EF%BC%9A) Claude Skills 架构拆解:渐进披露、运行时与安全沙箱 - Claude 中文 - Claude AI 开发技术社区 + + + +[\[5\]](https://cloud.tencent.com/developer/article/2616585#:~:text=Claude%20Skills%E6%98%AFAnthropic%E6%8E%A8%E5%87%BA%E7%9A%84AI%E5%8A%9F%E8%83%BD%E9%9D%A9%E5%91%BD%EF%BC%8C%E5%8F%AF%E5%B0%86%E7%94%A8%E6%88%B7%E4%BD%BF%E7%94%A8AI%E7%9A%84%E4%B9%A0%E6%83%AF%E6%96%87%E4%BB%B6%E5%8C%96%E7%AE%A1%E7%90%86%E3%80%82%E5%AE%83%E8%83%BD%E8%A7%A3%E5%86%B3Claude%E5%81%A5%E5%BF%98%E3%80%81%E9%9C%80%E9%87%8D%E5%A4%8D%E6%8F%90%E7%A4%BA%E8%AF%8D%E7%9A%84%E9%97%AE%E9%A2%98%EF%BC%8C%E5%B0%86%E4%BB%BB%E5%8A%A1%E8%AF%B4%E6%98%8E%E3%80%81%E5%B7%A5%E5%85%B7%E4%BB%A3%E7%A0%81%E7%AD%89%E6%89%93%E5%8C%85%E6%88%90%20) 最近很火爆的Claude Skills到底是个啥?解决什么问题?怎么用! + + + +[\[6\]](https://www.facebook.com/iamvista/photos/%E6%9F%90%E5%A4%A9%E6%B7%B1%E5%A4%9C%E6%88%91%E6%AD%A3%E5%9C%A8%E8%B6%95%E4%B8%80%E4%BB%BD%E6%96%87%E4%BB%B6%E5%A4%A9%E5%95%8A%E5%90%8C%E6%A8%A3%E7%9A%84%E6%9E%B6%E6%A7%8B%E5%90%8C%E6%A8%A3%E7%9A%84%E8%AA%9E%E6%B0%A3%E5%90%8C%E6%A8%A3%E7%9A%84%E6%A0%BC%E5%BC%8F%E8%A6%81%E6%B1%82%E4%BD%86%E6%88%91%E5%8F%88%E5%BE%97%E9%87%8D%E6%96%B0%E6%89%93%E4%B8%80%E6%AC%A1%E8%AB%8B%E7%94%A8%E9%80%99%E5%80%8B%E6%A0%BC%E5%BC%8F%E8%AB%8B%E5%85%88%E5%95%8F%E4%B8%89%E5%80%8B%E6%BE%84%E6%B8%85%E5%95%8F%E9%A1%8C%E8%AB%8B%E6%8A%8A%E8%BC%B8%E5%87%BA%E5%88%86%E6%88%90%E5%9B%9B%E6%AE%B5%E8%AB%8B%E9%99%84%E4%B8%8A%E5%8F%AF%E7%9B%B4%E6%8E%A5%E8%B2%BC%E5%88%B0-notion-%E7%9A%84/10162293093624053/#:~:text=,%E6%88%91%E6%80%8E%E9%BA%BC%E5%81%9A%E4%B8%80%E5%80%8B%E5%A5%BDskill%EF%BC%8C%E8%AE%8A%E6%88%90skill) 請先問三個澄清問題、請把輸出分成四段、請附上可直接貼到Notion ... + + + +[\[14\]](https://github.com/wensia/xiaohongshu-poster-generator#:~:text=wensia%2Fxiaohongshu,Code%20%2B%20MCP%20%E7%9A%84%E5%B0%8F%E7%BA%A2%E4%B9%A6%E6%98%9F%E5%BA%A7%E5%86%85%E5%AE%B9%E8%87%AA%E5%8A%A8%E5%8C%96%E5%B7%A5%E4%BD%9C%E6%B5%81%EF%BC%8C%E6%94%AF%E6%8C%81%E6%B5%B7%E6%8A%A5%E7%94%9F%E6%88%90%E3%80%81%E7%88%86%E6%96%87%E5%88%86%E6%9E%90%E3%80%81%E5%86%85%E5%AE%B9%E5%8F%91%E5%B8%83%E7%AD%89%E5%8A%9F%E8%83%BD%E3%80%82%20%E5%8A%9F%E8%83%BD%E7%89%B9%E6%80%A7) wensia/xiaohongshu-poster-generator: 小红书星座海报生成器 - GitHub + + + +[\[15\]](https://blog.csdn.net/yangshangwei/article/details/156836796#:~:text=%E4%B8%80%E4%B8%AA%E5%8F%AF%E8%90%BD%E5%9C%B0%E7%9A%84%E4%B8%89%E6%AD%A5%E6%B3%95,3%20%E7%AC%AC%E4%B8%89%E6%AD%A5%EF%BC%9A%E6%8A%8AClaude%20%E5%BD%93%E2%80%9C%E6%90%AD%E5%AD%90%E2%80%9D%EF%BC%8C%E8%80%8C%E4%B8%8D%E6%98%AF%E6%90%9C%E7%B4%A2%E6%A1%86) LLM - 从Prompt 到Skills_skills 市场 - CSDN博客 + + + +[\[18\]](https://hbwdj.gov.cn/appbdetail-imqqsmrp9897358.d#:~:text=%E9%AA%97%E4%BD%A0%E7%9A%84%EF%BC%8C%E5%85%B6%E5%AE%9EAI%E6%A0%B9%E6%9C%AC%E4%B8%8D%E9%9C%80%E8%A6%81%E9%82%A3%E4%B9%88%E5%A4%9A%E6%8F%90%E7%A4%BA%E8%AF%8D%E3%80%82%20%E4%BD%A0%E5%8F%AA%E9%9C%80%E8%A6%81%E8%B0%83%E7%94%A8AI%20%E6%9C%AC%E8%BA%AB%E7%9A%84%E2%80%9CSkill%20Creator%E2%80%9D%E6%8A%80%E8%83%BD%EF%BC%8C%E7%94%A8%E4%BD%A0%E7%9A%84%E8%AF%AD%E8%A8%80%E6%8F%8F%E8%BF%B0%E8%87%AA%E5%B7%B1%E7%9A%84%E9%9C%80%E6%B1%82%EF%BC%8C%E8%AE%A9AI%E8%87%AA%E5%8A%A8%E5%B8%AE%E4%BD%A0%E7%94%9F%E6%88%90%E4%B8%80%E9%97%A8%E6%8A%80%E8%83%BD%EF%BC%8C%E4%BD%BF%E7%94%A8%E8%B5%B7%E6%9D%A5%E9%9D%9E%E5%B8%B8%E5%8F%8B%E5%A5%BD%EF%BC%8CAI%E4%BC%9A%E4%B8%80%E6%AD%A5%E6%AD%A5%E5%BC%95%E5%AF%BC%E4%BD%A0%E8%AF%B4%E5%87%BA%E4%BD%A0%E7%9A%84%E9%9C%80%E6%B1%82%EF%BC%8C%E4%BD%A0%E5%8F%AA%20) 骗你的,其实AI根本不需要那么多提示词。 + + + +[\[19\]](https://lilys.ai/zh/notes/claude-code-20251031/claude-code-skills-automate-everything#:~:text=%E6%83%B3%20%E8%87%AA%E5%8A%A8%E5%8C%96%E4%BD%A0%E6%89%80%E5%81%9A%E7%9A%84%E4%B8%80%E5%88%87%20%E5%90%97%EF%BC%9F%E5%AD%A6%E4%B9%A0%E5%A6%82%E4%BD%95%E5%88%A9%E7%94%A8%20%E5%85%8B%E5%8A%B3%E5%BE%B7%C2%B7%E7%A7%91%E5%BE%B7%E6%8A%80%E8%83%BD%20%E5%88%9B%E5%BB%BA%E8%87%AA%E5%AE%9A%E4%B9%89%E5%B7%A5%E4%BD%9C%E6%B5%81%E3%80%82%E6%9C%AC%E5%86%85%E5%AE%B9%E6%8F%AD%E7%A4%BA%E4%BA%86,%E5%85%AD%E6%AD%A5MASTER%E6%A1%86%E6%9E%B6%EF%BC%8C%E6%95%99%E4%BD%A0%E5%A6%82%E4%BD%95%E9%80%9A%E8%BF%87%20%E8%BF%AD%E4%BB%A3%E5%8F%8D%E9%A6%88%20%E8%AE%AD%E7%BB%83AI%E3%80%82%E6%8E%8C%E6%8F%A1%E6%AD%A4%E6%96%B9%E6%B3%95%EF%BC%8C%E4%BD%A0%E5%B0%B1%E8%83%BD%E5%B0%86%E9%87%8D%E5%A4%8D%E4%BB%BB%E5%8A%A1%E8%BD%AC%E5%8C%96%E4%B8%BA%E9%AB%98%E6%95%88%E7%9A%84%20%E7%B3%BB%E7%BB%9F%E5%8C%96%E6%8A%80%E8%83%BD%EF%BC%8C%E5%AE%9E%E7%8E%B0%E5%B7%A5%E4%BD%9C%E6%95%88%E7%8E%87%E7%9A%84%E6%8C%87%E6%95%B0%E7%BA%A7%E6%8F%90%E5%8D%87%E3%80%82) [\[20\]](https://lilys.ai/zh/notes/claude-code-20251031/claude-code-skills-automate-everything#:~:text=4.%20MASTER%E6%A1%86%E6%9E%B6%EF%BC%9A%E7%AC%AC%E4%BA%8C%E9%98%B6%E6%AE%B5%E2%80%94%E2%80%94%E7%B3%BB%E7%BB%9F%E5%8C%96%E4%B8%BA%E6%8A%80%E8%83%BD%20) [\[21\]](https://lilys.ai/zh/notes/claude-code-20251031/claude-code-skills-automate-everything#:~:text=1.%20%E7%B3%BB%E7%BB%9F%E5%8C%96%E7%9B%AE%E6%A0%87%EF%BC%9A%E5%B0%86%E5%AD%A6%E5%88%B0%E7%9A%84%E6%89%80%E6%9C%89%E5%85%B3%E9%94%AE%E7%BB%8F%E9%AA%8C%E6%95%99%E8%AE%AD%E8%BD%AC%E5%8C%96%E4%B8%BA%20%E7%B3%BB%E7%BB%9F%E5%8C%96%E6%8A%80%E8%83%BD%E3%80%82,48) [\[22\]](https://lilys.ai/zh/notes/claude-code-20251031/claude-code-skills-automate-everything#:~:text=match%20at%20L3327%205,57) 克劳德代码技能:自动化你的所有操作 + + + +[\[31\]](https://lilys.ai/zh/notes/claude-skills-20251022/no-code-ai-workflow-claude-skills#:~:text=Skill%20%E5%9F%BA%E7%A1%80%E7%BB%93%E6%9E%84%EF%BC%9A%E6%9C%80%E7%AE%80%E5%8D%95%E6%83%85%E5%86%B5%E4%B8%8B%EF%BC%8CSkill%20%E6%98%AF%E4%B8%80%E4%B8%AA%E5%8C%85%E5%90%ABSkill%20Markdown%20%E6%96%87%E4%BB%B6%E7%9A%84%E7%9B%AE%E5%BD%95,36%5D) 无需编写代码也能定制AI 工作流?Claude Skills 让你的AI 更懂你 + + + +[\[32\]](https://github.com/0xfnzero/AI-Code-Tutorials#:~:text=%E4%BB%8E%E9%9B%B6%E5%9F%BA%E7%A1%80%E5%88%B0%E9%AB%98%E7%BA%A7%E5%BA%94%E7%94%A8%EF%BC%8C%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0Claude%20Code%EF%BC%8C%E6%8E%8C%E6%8F%A1AI%20%E8%BE%85%E5%8A%A9%E7%BC%96%E7%A8%8B%E6%8A%80%E8%83%BD%EF%BC%8C%E6%8F%90%E5%8D%87%E5%BC%80%E5%8F%91%E6%95%88%E7%8E%8710%20%E5%80%8D%20,md%E3%80%81%E5%B7%A5%E5%85%B7%E6%9D%83%E9%99%90%E3%80%81gh%20CLI%EF%BC%89%3B%20%E7%BB%99Claude%20%E6%9B%B4%E5%A4%9A%E5%B7%A5%E5%85%B7%EF%BC%88bash%E3%80%81MCP) 0xfnzero/AI-Code-Tutorials: 从零基础到高级应用,系统学习Claude ... + + + +[\[33\]](https://news.qq.com/rain/a/20260107A02N2N00#:~:text=Skills%20%E5%B0%B1%E6%98%AF%E7%BB%99Claude%20%E7%9A%84) 给AI装个技能包:Skills是什么-腾讯新闻 - QQ + + + +[\[34\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L93%20,side%20rendering%20and) [\[35\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L96%20,No%20magic%2C%20no) [\[36\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=,No%20magic%2C%20no) [\[37\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=,No%20magic%2C%20no) [\[38\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=Claude%20can%20describe%20architecture%20well%2C,com%20which%20I%20then) [\[39\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L316%20Give%20Claude,context%2C%20the%20better%20the%20suggestions) [\[40\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L322%20When%20Claude,things%20you%E2%80%99ve%20already%20decided%20against) [\[42\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=After%20long%20chats%2C%20Claude%20noticeably,you%20have%20done%20so%20far) [\[45\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=if%20err%20%3A%3D%20godotenv,) [\[46\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=match%20at%20L236%20This%20is,JavaScript%20pretending%20to%20be%20HTML) [\[47\]](https://medium.com/comsystoreply/learning-go-and-vue-with-claude-ai-as-my-pair-programmer-b2d634e291eb#:~:text=3,getting%20lost%20in%20tutorial%20hell) Learning Go and Vue with Claude AI as my Pair Programmer | comsystoreply + + + +[\[41\]](https://www.cnblogs.com/treasury-manager/p/19217990#:~:text=OpenCode%20%E4%B8%80%E4%B8%AA%E7%A5%9E%E5%A5%87%E7%9A%84CLI%20%EF%BC%88%E5%8F%AF%E4%BB%A5%E8%9E%8D%E5%90%88Claude%20Code%2C%20iFLow,code%29%20%E2%86%90%20%E8%87%AA%E5%8A%A8%E4%BD%BF%E7%94%A8oracle%20%E7%9A%84%E6%A8%A1%E5%9E%8B%E2%86%93%20%E8%B0%83%E7%94%A8%40explore) [\[80\]](https://www.cnblogs.com/treasury-manager/p/19217990#:~:text=OpenCode%20%E4%B8%80%E4%B8%AA%E7%A5%9E%E5%A5%87%E7%9A%84CLI%20%EF%BC%88%E5%8F%AF%E4%BB%A5%E8%9E%8D%E5%90%88Claude%20Code%2C%20iFLow,code%29%20%E2%86%90%20%E8%87%AA%E5%8A%A8%E4%BD%BF%E7%94%A8oracle%20%E7%9A%84%E6%A8%A1%E5%9E%8B%E2%86%93%20%E8%B0%83%E7%94%A8%40explore) OpenCode 一个神奇的CLI (可以融合Claude Code, iFLow ... - 博客园 + + + +[\[43\]](https://opencode.ai/docs#:~:text=%2Finit) [\[44\]](https://opencode.ai/docs#:~:text=You%20can%20ask%20OpenCode%20to,explain%20the%20codebase%20to%20you) [\[52\]](https://opencode.ai/docs#:~:text=1) [\[53\]](https://opencode.ai/docs#:~:text=OpenCode%20is%20an%20open%20source,desktop%20app%2C%20or%20IDE%20extension) [\[58\]](https://opencode.ai/docs#:~:text=Configure) [\[59\]](https://opencode.ai/docs#:~:text=If%20you%20are%20new%20to,verified%20by%20the%20OpenCode%20team) [\[60\]](https://opencode.ai/docs#:~:text=Next%2C%20initialize%20OpenCode%20for%20the,by%20running%20the%20following%20command) [\[61\]](https://opencode.ai/docs#:~:text=This%20will%20get%20OpenCode%20to,file%20in%20the%20project%20root) [\[62\]](https://opencode.ai/docs#:~:text=Make%20changes) [\[63\]](https://opencode.ai/docs#:~:text=1) [\[64\]](https://opencode.ai/docs#:~:text=Now%20let%E2%80%99s%20describe%20what%20we,want%20it%20to%20do) [\[66\]](https://opencode.ai/docs#:~:text=2) [\[67\]](https://opencode.ai/docs#:~:text=2) [\[68\]](https://opencode.ai/docs#:~:text=3) [\[69\]](https://opencode.ai/docs#:~:text=Once%20you%20feel%20comfortable%20with,hitting%20the%20Tab%20key%20again) [\[70\]](https://opencode.ai/docs#:~:text=Tip) [\[71\]](https://opencode.ai/docs#:~:text=Let%E2%80%99s%20say%20you%20ask%20OpenCode,to%20make%20some%20changes) [\[72\]](https://opencode.ai/docs#:~:text=Share) [\[73\]](https://opencode.ai/docs#:~:text=You%20want%20to%20give%20OpenCode,junior%20developer%20on%20your%20team) [\[74\]](https://opencode.ai/docs#:~:text=We%20need%20to%20add%20authentication,look%20at%20how%20this%20is) [\[75\]](https://opencode.ai/docs#:~:text=without%20having%20to%20review%20the,plan%20first) Intro | OpenCode + + + +[\[48\]](https://zhuanlan.zhihu.com/p/1991170184573122515#:~:text=OpenCode%20%E6%98%AF%E4%B8%80%E4%B8%AAAI%20%E7%BC%96%E7%A8%8B%E5%8A%A9%E6%89%8B%EF%BC%8C%E8%B7%91%E5%9C%A8%E4%BD%A0%E7%9A%84%E7%BB%88%E7%AB%AF%E9%87%8C%EF%BC%88%E5%B0%B1%E6%98%AF%E9%82%A3%E4%B8%AA%E9%BB%91%E8%89%B2%E7%AA%97%E5%8F%A3%EF%BC%89%E3%80%82%20%E4%BD%A0%E8%B7%9F%E5%AE%83%E8%AF%B4%E8%AF%9D%EF%BC%8C%E5%AE%83%E5%B0%B1%E5%B8%AE%E4%BD%A0%E5%86%99%E4%BB%A3%E7%A0%81%E3%80%82%20,%E2%86%92%20%E5%AE%83%E6%94%B9) OpenCode + Oh My OpenCode 一份老奶奶都能看懂的AI 编程指南 + + + +[\[49\]](https://github.com/anomalyco/opencode#:~:text=close%20and%20pricing%20will%20drop,what%27s%20possible%20in%20the%20terminal) [\[51\]](https://github.com/anomalyco/opencode#:~:text=OpenCode%20includes%20two%20built,key) [\[54\]](https://github.com/anomalyco/opencode#:~:text=%2A%20100,what%27s%20possible%20in%20the%20terminal) [\[55\]](https://github.com/anomalyco/opencode#:~:text=%2A%20100,push%20the%20limits%20of%20what%27s) [\[56\]](https://github.com/anomalyco/opencode#:~:text=Installation) [\[57\]](https://github.com/anomalyco/opencode#:~:text=npm%20i%20,Any%20OS) [\[65\]](https://github.com/anomalyco/opencode#:~:text=%2A%20build%20,unfamiliar%20codebases%20or%20planning%20changes) GitHub - anomalyco/opencode: The open source coding agent. + + + +[\[50\]](https://cloud.tencent.com/developer/article/2574516#:~:text=Claude%20Code%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83%E5%AE%88%E6%8A%A4%E8%80%85%E5%AD%90%E4%BB%A3%E7%90%86%E5%AE%9E%E6%88%98%E6%8C%87%E5%8D%97%20,Code%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83%E5%AD%90%E4%BB%A3%E7%90%86%E6%98%AFAI%E9%A9%B1%E5%8A%A8%E7%9A%84%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E7%AE%A1%E5%AE%B6%EF%BC%8C%E8%83%BD%E8%87%AA%E5%8A%A8%E7%BB%9F%E4%B8%80%E5%9B%A2%E9%98%9F%E4%BB%A3%E7%A0%81%E9%A3%8E%E6%A0%BC%E3%80%81%E6%89%A7%E8%A1%8C%E5%91%BD%E5%90%8D%E8%A7%84%E8%8C%83%E3%80%81%E6%A3%80%E6%9F%A5%E6%B5%8B%E8%AF%95%E8%A6%86%E7%9B%96%E7%8E%87%E3%80%82%E9%80%9A%E8%BF%87ESLint%E3%80%81Prettier%E7%AD%89%E5%B7%A5%E5%85%B7%E9%85%8D%E7%BD%AE%EF%BC%8C) Claude Code代码规范守护者子代理实战指南 - 腾讯云 + + + +[\[76\]](https://blog.csdn.net/u012094427/article/details/148866474#:~:text=%E4%BB%8A%E5%A4%A9%E6%88%91%E4%BB%AC%E8%A6%81%E8%81%8A%E7%9A%84%E7%A1%AC%E6%A0%B8%E8%AF%9D%E9%A2%98%EF%BC%8C%E6%98%AF%E4%B8%AA%E8%AE%A9%E6%9E%81%E5%AE%A2%E4%BB%AC%E9%A2%A4%E6%8A%96%E3%80%81%E8%AE%A9%E7%A8%8B%E5%BA%8F%E5%91%98%E4%BB%AC%E5%B0%96%E5%8F%AB%EF%BC%8C%E8%AE%A9%E5%86%99%E4%BB%A3%E7%A0%81%E7%88%BD%E5%88%B0%E9%A3%9E%E8%B5%B7%E7%9A%84%E5%AD%98%E5%9C%A8%E2%80%94%E2%80%94OpenCode%EF%BC%8C%E5%BC%80%E6%BA%90AI%E7%BB%88%E7%AB%AF%E7%BC%96%E7%A0%81%E5%8A%A9%E6%89%8B%E3%80%82%20%E5%9C%A8AI%E5%85%A8%E8%83%BD%E5%86%99%E4%BB%A3%E7%A0%81%E3%80%81Copilot%E5%AE%B6%E5%A4%A7%E4%B8%9A%E5%A4%A7%E3%80%81%20) 【爆款长文】终端里的AI编程老司机--全面解读OpenCode! 原创 + + + +[\[77\]](https://x.com/Nateemerson/status/2002043382953288046/photo/1#:~:text=YeonGyu,and%20practices%20for%20agentic%20coding) YeonGyu-Kim built Oh My Opencode (OmO), an OC plugin ... + + + +[\[78\]](https://github.com/code-yeongyu/oh-my-opencode#:~:text=The%20Best%20Agent%20Harness,Agent%20that%20codes%20like%20you) GitHub - code-yeongyu/oh-my-opencode: The Best Agent Harness. Meet Sisyphus: The Batteries-Included Agent that codes like you. + + + +[\[79\]](https://post.smzdm.com/p/aog8xq7m#:~:text=%E4%B8%80%E4%BA%BA%E6%8A%B5%E4%B8%80%E4%B8%AA%E5%BC%80%E5%8F%91%E5%9B%A2%E9%98%9F%E7%9A%84AI%E7%BC%96%E7%A8%8B%E7%A5%9E%E5%99%A8%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%20,%E5%9C%A8%E6%88%90%E6%9C%AC%E6%8E%A7%E5%88%B6%E6%96%B9%E9%9D%A2%EF%BC%8C%E8%AF%A5%E7%B3%BB%E7%BB%9F%E6%94%AF%E6%8C%81%E5%A4%9A%E6%A8%A1%E5%9E%8B%E6%B7%B7%E5%90%88%E4%BD%BF%E7%94%A8%E7%AD%96%E7%95%A5%E3%80%82%E9%80%9A%E8%BF%87%E9%85%8D%E7%BD%AE%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%B0%86) [\[85\]](https://post.smzdm.com/p/aog8xq7m#:~:text=%E5%9C%A8%E6%88%90%E6%9C%AC%E6%8E%A7%E5%88%B6%E6%96%B9%E9%9D%A2%EF%BC%8C%E8%AF%A5%E7%B3%BB%E7%BB%9F%E6%94%AF%E6%8C%81%E5%A4%9A%E6%A8%A1%E5%9E%8B%E6%B7%B7%E5%90%88%E4%BD%BF%E7%94%A8%E7%AD%96%E7%95%A5%E3%80%82%E9%80%9A%E8%BF%87%E9%85%8D%E7%BD%AE%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%B0%86%20) 一人抵一个开发团队的AI编程神器完全指南 - 什么值得买 + + + +[\[81\]](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/README.zh-cn.md#:~:text=oh,%E8%80%81%E5%AE%9E%E8%AF%B4%EF%BC%8C%E7%94%9A%E8%87%B3%E4%B8%8D%E7%94%A8%E8%B4%B9%E5%BF%83%E8%AF%BB%E6%96%87%E6%A1%A3%E3%80%82%E5%8F%AA%E9%9C%80%E5%86%99%E4%BD%A0%E7%9A%84%E6%8F%90%E7%A4%BA%E3%80%82%E5%8C%85%E5%90%AB%27ultrawork%27%20%E5%85%B3%E9%94%AE%E8%AF%8D%E3%80%82Sisyphus%20%E4%BC%9A%E5%88%86%E6%9E%90%E7%BB%93%E6%9E%84%EF%BC%8C%E6%94%B6%E9%9B%86%E4%B8%8A%E4%B8%8B%E6%96%87%EF%BC%8C%E6%8C%96%E6%8E%98%E5%A4%96%E9%83%A8%E6%BA%90%E4%BB%A3%E7%A0%81%EF%BC%8C%E7%84%B6%E5%90%8E%E6%8C%81%E7%BB%AD%E6%8E%A8%E8%BF%9B) oh-my-opencode/README.zh-cn.md at dev - GitHub + + + +[\[82\]](https://www.youtube.com/watch?v=twFjLiy2Pmc#:~:text=%E8%A7%86%E9%A2%91%E7%AE%80%E4%BB%8B%EF%BC%9A%20%E6%9C%AC%E6%9C%9F%E8%A7%86%E9%A2%91%E8%AF%A6%E7%BB%86%E6%BC%94%E7%A4%BA%E4%BA%86%E5%A6%82%E4%BD%95%E5%9C%A8Opencode%E4%B8%AD%E4%BD%BF%E7%94%A8%E6%9C%80%E5%BC%BA%E5%BC%80%E6%BA%90%E6%8F%92%E4%BB%B6Oh%20My%20Opencode%EF%BC%88OMO%EF%BC%89%EF%BC%8C%E5%B0%86%E5%8D%95%E4%B8%80AI%E7%BC%96%E7%A8%8B%E5%8A%A9%E6%89%8B%E5%8D%87%E7%BA%A7%E4%B8%BA%E5%A4%9A%E6%A8%A1%E5%9E%8B%E5%8D%8F%E4%BD%9C%E7%9A%84AI%E5%BC%80%E5%8F%91%E5%9B%A2%E9%98%9F%EF%BC%81) Sisyphus智能体指挥多AI协作,复杂项目开发效率倍增!全程零干预 ... + + + +[\[83\]](https://x.com/AISuperDomain/status/2009823408301994209#:~:text=OpenCode%E7%9A%84%E6%9C%80%E5%BC%BA%E5%BC%80%E6%BA%90%E6%8F%92%E4%BB%B6oh%20my%20opencode%E7%A1%AE%E5%AE%9E%E9%9D%9E%E5%B8%B8%E5%A5%BD%E7%94%A8%20%E8%A7%86%E9%A2%91%E7%AE%80%E4%BB%8B%EF%BC%9A%20%E6%9C%AC%E6%9C%9F%E8%A7%86%E9%A2%91%E8%AF%A6%E7%BB%86%E6%BC%94%E7%A4%BA%E4%BA%86%E5%A6%82%E4%BD%95%E5%9C%A8Opencode%E4%B8%AD%E4%BD%BF%E7%94%A8%E6%9C%80%E5%BC%BA%E5%BC%80%E6%BA%90%E6%8F%92%E4%BB%B6Oh,My%20Opencode%EF%BC%88OMO%EF%BC%89%EF%BC%8C%E5%B0%86%E5%8D%95%E4%B8%80AI%E7%BC%96%E7%A8%8B%E5%8A%A9%E6%89%8B%E5%8D%87%E7%BA%A7%E4%B8%BA%E5%A4%9A%E6%A8%A1%E5%9E%8B%E5%8D%8F%E4%BD%9C%E7%9A%84AI%E5%BC%80%E5%8F%91%E5%9B%A2%E9%98%9F%EF%BC%81%20%E6%A0%B8%E5%BF%83%E5%86%85%E5%AE%B9%EF%BC%9AOMO%E6%8F%92%E4%BB%B6) OpenCode的最强开源插件oh my opencode确实非常好用 + + + +[\[84\]](https://blog.csdn.net/gitblog_00895/article/details/144862506#:~:text=oh,) oh-my-opencode高级用法:自定义钩子和插件系统详解 - CSDN博客 + + \ No newline at end of file diff --git a/98-AgentSkills/2-prompt-for-prompt.md b/98-AgentSkills/2-prompt-for-prompt.md new file mode 100644 index 0000000..8150eda --- /dev/null +++ b/98-AgentSkills/2-prompt-for-prompt.md @@ -0,0 +1,4 @@ +你是一名精通prompt engineering和context engineering的大师,非常精通利用LLM大模型 +进行代码开发工作。 +对于ClaudeCode Agent Skills有着深刻的理解和认识,熟练掌握官方的SKILL开发指导说明,熟练掌握开源的SKILL用法。 +请你为一名具备LLM开发prompt经验的人员,写一份详细的SKILL使用及开发说明,对比Prompt与SKILL的差异,突出SKILL的特点 \ No newline at end of file diff --git a/98-AgentSkills/3-Agent Skills 使用与开发实战指南-p.md b/98-AgentSkills/3-Agent Skills 使用与开发实战指南-p.md new file mode 100644 index 0000000..1d201f5 --- /dev/null +++ b/98-AgentSkills/3-Agent Skills 使用与开发实战指南-p.md @@ -0,0 +1,601 @@ +# Claude Code Agent Skills 使用与开发实战指南 + +(面向有 Prompt 经验的 LLM 开发者) + +*** + +## 0. 面向谁 \& 要解决什么问题 + +本指南面向已经熟悉「写系统 Prompt / 模板 Prompt 来驱动大模型」的工程师,重点回答三个问题: + +1. **从 Prompt 到 Skill 的本质差异是什么?** +2. **在 Claude Code / Agent SDK 里,Skill 实际是怎么被发现、触发和执行的?** +3. **如何把现有的 Prompt 工作流系统性迁移为 Skill,并做好工程化维护?** + +全文会大量对比 Prompt vs Skill,突出 Skill 在「可复用、可维护、可组合」上的优势,并给出面向工程实践的设计建议。 + +*** + +## 1. 核心概念:什么是 Agent Skill? + +从官方定义出发,**Skill = 指令包(Markdown)+ 元数据(YAML)+ 可选脚本/资源文件**,用于给 Claude 注入特定领域的专业能力,并在需要时**按需加载(progressive disclosure)**。 + +可以粗略理解为: +> 从「一次性 Prompt」升级为「可发现、可触发、可共享的领域专家插件」。 + +### 1.1 Skill 的组成 + +典型 Skill 目录结构(Claude Code / Agent SDK 默认形式)如下: + +```text +my-skill/ +├── SKILL.md # 必须,有 YAML frontmatter + 主指令 +├── reference.md # 可选,详细文档/规范,按需加载 +├── examples.md # 可选,用法示例 +└── scripts/ # 可选,可执行脚本(Python/JS/bash 等) + └── helper.py +``` + +其中: + +- **SKILL.md** + - 顶部 `---` 之间为 YAML frontmatter: + - `name`: Skill 名(唯一标识) + - `description`: Skill 做什么 \& 何时使用(触发判断关键信号) + - 下方为 Markdown 正文:指令、步骤、示例、链接到其它文件等 +- **其他 Markdown 文件** + - 延伸文档、规范、API 说明、更多示例 + - Skill 激活后才按需加载,避免一开始就占满上下文 +- **scripts/** + - 放可执行脚本,由 Claude 通过 code execution / bash 调用,结果再由模型解释 + + +### 1.2 Skill 在系统里的位置 + +在 Agent SDK / Claude Code 中,Skill 被作为一种特殊的「工具」存在(工具名通常为 `Skill`),出现在工具列表中,与 `Read` / `Write` / `Bash` 等并列。 + +但它和传统工具最大的区别是: + +- 传统工具:**执行动作 → 返回结果** +- Skill:**注入指令/上下文 → 修改后续推理环境** + +换句话说,Skill 更像是**上下文工程 (context engineering) 的模块化单元**,通过: + +- 在对话历史中插入系统/用户级指令 +- 动态调整可用工具、模型、思考 token 限制等 + +*** + +## 2. Prompt vs Skill:从 Prompt 工程师视角的系统性对比 + +为了方便有 Prompt 经验的读者,先用表格给出高层对比。 + +### 2.1 Prompt / Slash Command / Skill / MCP / Subagent 对比 + +| 维度 | 普通 Prompt | Slash Command (`/xxx`) | Skill (Agent Skill) | MCP 工具 | Subagent | +| :-- | :-- | :-- | :-- | :-- | :-- | +| 触发方式 | 用户临时输入 | 用户显式输入命令 | 模型根据描述自动触发(可带确认) | 模型显式选择调用 | 用户或模型显式切换 | +| 存储形态 | 文本片段 / 模板 | 单一 `.md` 文件 | 目录 + `SKILL.md` + 资源文件/脚本 | 外部服务 / 进程 | 独立 agent 配置文件 | +| 作用范围 | 当前对话 | 当前项目或用户 | 全局(用户级)+ 项目级 | 任意集成的应用 | 子上下文 | +| 核心能力 | 影响一次或一段对话行为 | 复用一段 Prompt | 封装**可重用工作流 + 资源 + 可执行逻辑** | 访问外部数据/服务 | 长期专职 agent,独立上下文 | +| 上下文成本 | 每次要重新输入或粘贴 | 命令内容一次性进入上下文 | 先只加载 metadata,触发后再加载全文(progressive disclosure) | 按调用次数计入提示 | 独立 context window | +| 发现机制 | 人脑记忆/文档 | 人主动记命令名 | 模型基于 `description` 自动匹配任务 | 调用逻辑通常在 app 代码里 | 由 orchestrator 决定何时委派 | +| 脚本支持 | 需要外部 pipeline | 支持有限(多数为文本) | 可绑定脚本并在 code execution 环境中运行 | 自己就是代码 | 可使用一组工具 | +| 复用 \& 版本管理 | 很难系统管理 | 可以通过 Git 管理单文件 | 自然地通过 Git / 目录版本化、共享 | 依赖微服务/Git | 类似 micro-service 配置 | + +从 Prompt 工程师角度,可以总结为: + +> **Prompt 是「指令文本」,Skill 是「指令系统」** +> Prompt 强在「灵活,快速试验」,Skill 强在「标准化,可复制,可演化」。 + +### 2.2 Skill 相比纯 Prompt 的关键优势 + +1. **发现 \& 自动触发** + - 不需要用户记住「要贴哪段 Prompt」,模型通过 Skill 的 `description` 判断当前请求是否适配,然后请求使用该 Skill。 + - 对于团队内部知识,极大降低使用门槛(只要「说人话描述任务」即可)。 +2. **进阶的上下文工程** + - Progressive disclosure:启动时只加载 metadata;触发 Skill 时再读 `SKILL.md` 和相关文件,既省 token 又避免污染通用对话。 + - 能把长文档、规范、脚本拆分到多个文件,按需加载,而不是一次性塞进系统 Prompt。 +3. **与可执行代码融合** + - 可在 `scripts/` 里放好经过验证的 Python/JS/Bash 脚本,Skill 中只写「何时、如何调用脚本、如何解释输出」。 + - 把「确定性处理」(解析文件、复杂计算、格式转换)迁到脚本,「不确定性决策」留给模型。 +4. **工程化与团队协作** + - Skill 目录可以直接纳入 Git 仓库,与代码、文档同一生命周期进行评审、测试、发布。 + - 可以在个人级 (`~/.claude/skills/`) 和项目级 (`.claude/skills/`) 间共享和组合。 +5. **跨界面统一复用** + - 同一个 Skill 可同时在 Claude App / Claude Code / API / 支持 Skills 的 IDE 插件中使用。 + - 你在本地为团队开发的 Skill,理论上可以作为「统一的工作流组件」在多个入口使用。 + +*** + +## 3. Skill 的运行机制:发现 → 触发 → 执行 + +理解运行机制,有助于更好地做「技能触发设计」和「上下文工程」。 + +### 3.1 Discovery:启动时的轻量加载 + +当 Agent 启动(Claude Code、SDK 程序、支持 Skill 的 app)时,会做: + +1. 扫描 `~/.claude/skills/` 和当前工程 `.claude/skills/` 中的子目录。 +2. 对每个目录,只读取 `SKILL.md` 的 YAML frontmatter(`name` + `description` 等)。 +3. 把这些 metadata 以文本形式加入 Skill 工具的描述中,出现在模型能看到的 system 提示中。 + +**此时模型只知道:** +> 「有哪些技能、叫啥名字、干什么、什么时候用」, +> 但还没有看到真正的长指令和资源内容。 + +### 3.2 Activation:根据自然语言请求触发 + +当用户输入任务(例如「帮我生成一份符合公司品牌规范的 PPT」),流程大致是: + +1. 模型在看到当前用户消息 + Skill 列表描述后,**根据语义匹配**,判断有哪些 Skill 适用。 +2. 如果判断某个 Skill 合适,会通过 `Skill` 工具发起一次调用,内部携带 `command`/`name` 等信息。 +3. 前端/SDK 通常会给用户一个「是否使用某某 Skill」的确认提示(尤其在 Claude Code/Claude App 中)。 + +匹配质量高度依赖: + +- `description` 里是否写清楚**任务类型、输入形式、关键关键词**; +- 是否给出类似「当用户说 X/Y/Z 时,使用本 Skill」的语句。 + + +### 3.3 Execution:加载指令、脚本与资源 + +Skill 激活后,系统会做: + +1. 通过 `Bash` / 文件访问工具读取 `SKILL.md` 正文内容,插入到对话上下文中(通常以用户可见 + 隐藏两条消息注入)。 +2. 如果 `SKILL.md` 里引用了其他 Markdown 文件(如 `reference.md` / `examples.md`),则在需要时再读取对应内容,进一步注入上下文。 +3. 若指令中包含像「运行 `scripts/analyzer.py` 并解析输出」的步骤,Claude 会: + - 使用 code execution / bash 工具执行脚本; + - 读取输出文件(比如 JSON/CSV),再进行解释和总结。 +4. 在整个 Skill 激活期内,可以调整: + - 可用工具的白名单/黑名单(例如只读文件、不允许 Bash 修改文件); + - 使用的模型(例如对某 Skill 固定使用 Sonnet / Opus); + - 思考 token 等参数,使复杂任务能更稳定完成。 + +Skill 完成任务后,可以自然退出,其指令虽然已在对话里,但后续模型可根据上下文判断是否继续遵循,或在需要时再次触发其它 Skill。 + +*** + +## 4. Skill 目录结构与 `SKILL.md` 设计详解 + +### 4.1 `SKILL.md` 最小可用示例 + +以一个「解释代码」的 Skill 为例(官方文档类似示例): + +```markdown +--- +name: explaining-code +description: Explain source code in clear, structured language; use when users ask you to explain or understand code. +--- + +# Explaining Code + +## Goal +When this skill is active, you explain the given code in a way that is: +- Clear and concise +- Structured with headings and bullet points where helpful +- Focused on behavior, design decisions, and potential issues + +## Instructions + +1. First, identify the language, framework, and main purpose of the code. +2. Then, explain: + - What the code does step by step + - How key functions, classes, and modules interact + - Any assumptions or implicit behaviors +3. Highlight: + - Potential bugs or edge cases + - Performance concerns + - Security considerations +4. Provide suggestions for improvement if relevant. + +## Examples + +### Example 1: Small function + +_User request_: "Explain what this JavaScript function does and if there are any issues." + +... +``` + +要点: + +- **YAML frontmatter 必须有 `name` \& `description`**;模型就是靠这两个字段判断何时使用该 Skill。 +- 正文建议包含: + - 明确的目标(Goal) + - 详细的步骤(Instructions) + - 若干典型示例(Examples) +- Skill 是给「另一实例的 Claude」看的文档,写法应面向模型:步骤清晰、可执行、少模糊语义。 + + +### 4.2 推荐的正文结构模板 + +对于多数工程类 Skill,可以采用固定骨架: + +```markdown +# + +## 1. 适用场景(When to use this Skill) +- 当用户... +- 当输入包含... +- 不要在以下情况使用... + +## 2. 输入与输出约定 +- 输入可以是: + - ... +- 输出必须包括: + - ... +- 对输出格式的要求(如 JSON schema / Markdown 结构) + +## 3. 工作流步骤 +1. ... +2. ... +3. ... + +## 4. 参考规范 / 约束 +- 引用 `style-guide.md` / `api-reference.md` 等 +- 明确必须遵守的约束(安全/隐私/合规) + +## 5. 示例 +### 示例 1 +_用户..._ +_期望输出..._ + +## 6. 常见错误与修正 +- 如果出现 X,则... +``` + +这样设计有几个好处: + +- 方便未来的 Claude 实例按「When to use」部分进行自我检查; +- 输入输出约定可以和脚本/下游系统严格对齐; +- 工作流步骤使 Skill 更像一份 SOP(标准作业流程),可复用性强。 + + +### 4.3 多文件 Skill 的组织方式 + +多文件 Skill 适合复杂领域,例如: + +- 公司级代码规范/安全规范; +- 复杂文档模板(产品需求文档、设计文档、测试计划); +- 大型 API / 数据仓库查询规范。 + +典型做法是: + +```text +code-review/ +├── SKILL.md # Overview + 导航 +├── SECURITY.md # 安全审计检查表 +├── PERFORMANCE.md # 性能审查要点 +├── STYLE.md # 代码风格规范 +└── scripts/ + └── run-linters.sh +``` + +在 `SKILL.md` 中: + +- 先给出总体工作流; +- 再指示 Claude 在特定步骤「按需打开 SECURITY.md / PERFORMANCE.md」等文件。 + +这天然支持 progressive disclosure:仅在安全相关检查时才加载 `SECURITY.md`,避免所有规范一次性进入上下文。 + +*** + +## 5. 在 Claude Code / Agent SDK / App 中使用 Skill + +### 5.1 Claude Code 中的使用路径 + +在 Claude Code(VS Code / 其他 IDE 集成)中,Skill 的使用大致包括: + +1. **安装/放置位置** + - 全局:`~/.claude/skills//SKILL.md` + - 项目级:`/.claude/skills//SKILL.md` + 项目级具备更高优先级,更适合项目特定约束。 +2. **启用 Skill 工具** + - Agent SDK 或 Claude Code 的配置中,需要将 `Skill` 工具加入 `allowedTools` / `allowed_tools` 才能使用 Skill。 +3. **交互方式** + - **自然语言触发**:直接按业务语言提需求,只要和 `description` 匹配,模型就会尝试触发 Skill。 + - **显式指定技能名**:如「使用 `explaining-code` 这个技能帮我…」,可提高触发概率。 + - **列出可用 Skill**(在 IDE 中):例如通过命令面板或直接问 Claude 「列出当前可用的 Skills」,方便调试加载是否成功。 +4. **结合 Subagent 使用** + - 在 Subagent 的配置里可以指定 `skills: skill1, skill2`,让某个 Subagent 启动时自动加载对应技能。 + - 典型用法:创建 `code-reviewer` Subagent,并为其加载 `code-review` Skill,包括安全/性能/风格规范。 + +### 5.2 在 Claude App / Web / 桌面端中的使用 + +技能同样在 web/桌面端可用: + +- 在设置中的「Skills」部分可以: + - 启用内置官方技能(Excel/Word/PPT/PDF 等); + - 上传自定义 Skill(打包成 zip 或按要求目录); +- 对话中,当 Claude 自动使用 Skill 时,会有明显标记(例如「已使用某某 Skill」),便于观察和调试; +- 「Skill Creator」技能可以辅助用自然语言生成新的 Skill 初稿(对 prompt 工程师尤其友好)。 + + +### 5.3 在 API / Agent SDK 中使用 + +在 Agent SDK 或直接使用 Claude Messages API 时: + +- 在 agent 配置中声明开启 Skills,并允许 `Skill` 工具。 +- 上下文中会自动注入已安装技能的 metadata,后续与 Claude Code 的逻辑一致。 +- 某些环境还支持在 API 调用中显式指定要使用的 Skill 名称,以更强约束模型行为。 + +*** + +## 6. 从 Prompt 到 Skill:一个系统化迁移方法论 + +假设你已经有一批在日常工作中频繁使用的 Prompt(代码审查模板、文档模板、业务流程模板等),可以按如下步骤逐步迁移为 Skill。 + +### 6.1 Step 1:挑选适合作为 Skill 的场景 + +优先选择具备以下特征的 Prompt: + +- **任务稳定、重复频率高** + 例如「生成符合公司模板的周报」、「按照固定格式写 PRD」、「执行特定框架的代码审查」。 +- **指令较长,粘贴成本高** + 每次要粘贴 100+ 行 Prompt 才能用的流程。 +- **有明确可复用工作流** + 如「先收集需求 → 再分析约束 → 再生成文档 → 再生成测试用例」。 +- **需要结合外部资源 / 规范** + 如品牌规范 PDF、API 文档、现有脚本。 + +这类场景用 Skill 迁移后收益最大。 + +### 6.2 Step 2:抽象为「领域工作流」 + +把原 Prompt 拆成几个维度: + +1. **适用场景 / 不适用场景**(写入 `description` 和正文「When to use」部分) +2. **输入输出契约**(类型、格式、字段) +3. **步骤化工作流**(尽量写成 numbered list) +4. **引用的外部知识/规范** +5. **可交给脚本完成的确定性操作** + +再决定: + +- 哪些应该留在 `SKILL.md` 主体中; +- 哪些应拆出到 `reference.md` / `examples.md`; +- 哪些迁移到 `scripts/` 中由代码执行。 + + +### 6.3 Step 3:创建目录与 `SKILL.md`,写好 frontmatter + +在本地或项目中创建目录: + +```bash +mkdir -p ~/.claude/skills/my-weekly-report +``` + +然后新建 `SKILL.md`,至少包含: + +```yaml +--- +name: weekly-report +description: Create structured weekly status reports following our team's format, tone, and content guidelines. Use when users want to draft, revise, or polish weekly reports. +--- +``` + +描述里建议同时包含: + +- **任务类型**(create / review / analyze…) +- **对象类型**(weekly report / PRD / code / log…) +- **关键触发词**(weekly report, 周报, 状态更新 等) + +有实践表明,过于抽象的描述会导致触发不稳定,描述应「贴近用户会说的话」。 + +### 6.4 Step 4:迁移 Prompt 内容为结构化指令 + +把原来的大段 Prompt 迁移到 `SKILL.md` 正文里: + +- 按前文推荐的「适用场景 / 输入输出 / 工作流 / 规范 / 示例」骨架重构; +- 适当拆分为多个 Markdown 文件,通过链接组织; +- 将那些「给人看的摆设性说明」删减,把关键的决策规则和格式要求留下。 + +此时建议使用官方的 `skill-creator` Skill 做一次「反向审阅」:让 Claude 作为「Skill 审查专家」检查你的 `SKILL.md` 是否清晰、可执行。 + +### 6.5 Step 5:引入脚本(可选) + +对于涉及复杂文件处理、数据转换的 Skill: + +- 在 `scripts/` 目录中放置脚本(如 `scripts/generate_charts.py`); +- 在 `SKILL.md` 中写明: + - 何时触发该脚本; + - 调用方式(命令行参数、输入输出文件); + - 如何基于脚本输出继续生成响应。 + +示例片段: + +```markdown +### Step 3: Generate metrics + +1. Run `python {baseDir}/scripts/metrics.py --input "{INPUT_PATH}" --output "metrics.json"`. +2. Read `metrics.json` and: + - Summarize key metrics + - Highlight anomalies + - Propose follow-up actions +``` + +这样可以让 Skill 在执行时自动落地可靠、可测试的脚本逻辑。 + +### 6.6 Step 6:测试、迭代与版本管理 + +- 在 Claude Code 中打开工程,触发 Skill 相关任务,观察: + - Skill 是否被正确检测与加载; + - 输出是否稳定、可预期; + - 是否存在错误触发或遗漏触发的情况。 +- 把 Skill 目录纳入 Git: + - 对 `SKILL.md` 和脚本做 code review; + - 用单元测试或 golden test 验证脚本输出与模型输出模式; + - 给 Skill 加上版本号和 changelog,避免团队混乱。 + +*** + +## 7. 与其它能力的关系:Prompt / Slash Commands / MCP / Subagents + +### 7.1 与 Slash Commands 的关系 + +官方文档已经明确给出对比: + +- **Slash Commands 更适合:** + - 简单、单文件的快捷 Prompt 片段; + - 用户显式调用的「动作」(如 `/review`、`/optimize`)。 +- **Skills 更适合:** + - 多步骤、复杂工作流; + - 需要脚本/多个文档支持的能力; + - 希望由模型自动发现与触发的能力。 + +一个现实做法是: + +- 将「一两句话就能说清楚的常用命令」做成 Slash Command; +- 将「背后有 SOP/规范/脚本组合」的能力做成 Skill。 + + +### 7.2 与 MCP(Model Context Protocol)工具的关系 + +MCP 工具解决的是「对接外部服务和数据源」的问题,而 Skill 解决的是「**如何使用这些工具与数据完成任务**」的问题。 + +常见组合方式: + +- MCP 提供: + - 数据库查询; + - 外部 API 调用; + - 文件存储与检索。 +- Skill 提供: + - 如何根据业务规则选择查询; + - 如何解释结果并组装报告; + - 如何处理失败/重试逻辑(在指令层面)。 + +可以理解为: + +> MCP = 手里的工具箱 +> Skill = 用这些工具完成某项工程的施工手册 + +### 7.3 与 Subagents 的关系 + +Subagent 是具备独立系统 Prompt / 上下文 / 工具集合的「子代理」: + +- 适合: + - 长期运行的专职角色(如「安全审核专家」「数据科学家」); + - 需要独立上下文,避免主对话污染。 +- 而 Skill 本身不创建新上下文,只是在当前 agent 环境里注入额外知识与流程。 + +两者可以协同: + +- Subagent 的系统 Prompt 里定义角色; +- 为该 Subagent 配置一组 Skills(通过 `skills:` 字段自动加载),让其在子上下文中运用这些技能处理任务。 + +*** + +## 8. Skill 设计与实现的最佳实践(结合 Prompt 工程经验) + +从 Prompt 工程到 Skill 工程,有几条实践尤为关键: + +### 8.1 面向「触发」写 description,而不是面向人写标题 + +- 不要只写「Brand style enforcement skill」; +- 应写「Enforce our company's brand style when generating documents, slides, and emails; use when users request brand-compliant content or mention brand guidelines.」 + +要点: + +- 包含**动词 + 对象 + 触发语句**; +- 尽量覆盖用户实际会说的关键词(包括中英文同义词); +- 明确排除场景(可写在正文里)。 + + +### 8.2 把 Skill 当「SOP + Playground」而不是「单次提示」 + +- SOP:用 numbered steps 描述必须遵守的流程; +- Playground:在示例部分给出多个「现实场景 -> 标准输出」的例子,帮助模型对齐风格。 + +对比传统 Prompt,把「元层说明」(你是谁、你要怎么做)抽象到稳定 SOPS;把一次性的上下文保留在对话中。 + +### 8.3 严格区分「模型擅长」和「脚本擅长」的部分 + +- 让脚本做: + - 复杂解析(PDF/Excel/日志); + - 一致性要求高的格式转换; + - 算法类计算、指标统计。 +- 让模型做: + - 决策、总结、解释; + - 文本生成、风格迁移; + - 多步推理。 + +Skill 文本中应明确告诉 Claude 什么时候「先运行脚本再思考」,减轻 hallucination 风险。 + +### 8.4 使用 progressive disclosure 优化上下文 + +- 把大型规范/文档拆分为多个文件; +- 在 `SKILL.md` 中通过「当执行到某步时,再读取某文件」的方式引用; +- 避免在 Skill 中「一上来就复制粘贴整本手册」。 + +这样不仅省 token,还减少上下文「噪音」,提高任务对齐度。 + +### 8.5 将测试和迭代流程制度化 + +- 为关键 Skill 设计一组「golden prompts + expected patterns」; +- 每次改动 `SKILL.md` 或脚本后,跑一遍回归验证; +- 把「Skill 使用问题」回溯到: + - description 不清; + - 指令歧义; + - 示例不足; + - 或脚本行为与说明不一致。 + +社区和官方仓库里的 Skill(如 `skill-creator`)是很好的参考和基准,可以从中抽取通用结构和写法。 + +*** + +## 9. 常见坑与排错思路 + +结合官方文档与社区经验,总结几个典型问题及解决方向: + +1. **Skill 明明写好了,但从未触发** + - 检查: + - 目录名是否正确,`SKILL.md` 是否位于根目录; + - frontmatter 是否合法 YAML,`name`/`description` 是否存在; + - 是否在 agent 配置中启用了 `Skill` 工具; + - `description` 是否过于抽象、不含实际触发关键词。 +2. **Skill 误触发或过度触发** + - 现象:稍有相关就被加载,导致上下文被早早塞满。 + - 解决: + - 在 description 中更明确限定任务范围; + - 在正文「When NOT to use」部分增加自检指令,让 Claude 自我约束; + - 将多个任务拆成多个更细粒度的 Skill。 +3. **Skill 行为不一致,时好时坏** + - 可能原因: + - 工作流步骤过于模糊,缺少关键约束; + - 示例不足,导致风格漂移; + - 依赖的脚本无单测或输出不稳定。 + - 建议: + - 增加「负例」示例,说明不应该产生的输出; + - 对关键决策处增加明确的「思考检查清单」。 +4. **多 Skill 并用时出现冲突** + - 例如:多个文档生成 Skill 都认为自己适用。 + - 治理方式: + - 在 description 中加入更具体的触发条件(如特定文件类型、特定领域关键词); + - 在 Skill 正文中加入互斥说明:如「如果 X 更匹配另一个 Skill,则不要使用本 Skill」。 + +*** + +## 10. 总结:从 Prompt 工程到 Skill 工程 + +从体系化视角看: + +- **Prompt 工程**解决的是: + 「如何构造一次性上下文,让模型在当前会话表现良好」。 +- **Skill 工程**解决的是: + 「如何以模块化方式封装知识与流程,让模型在不同场景、长期、多人协作下都表现一致」。 + +对于已经有成熟 Prompt 经验的开发者,下一步的升级路径可以概括为: + +1. 把高频、高价值 Prompt 抽象成可重用工作流(SOP); +2. 迁移为 Skill:写好 `SKILL.md` 的 frontmatter 与结构化正文; +3. 将领域知识与脚本一起封装,利用 progressive disclosure 和 code execution 提升稳定性与效率; +4. 通过 Git + 测试 + 版本管理,把 Skill 纳入工程资产; +5. 结合 MCP 和 Subagent,把 Skill 作为「领域 Playbook」,构建真正可运营的 AI agent 体系。 + +在 Claude Code / Agent SDK 生态下,Skill 已经成为官方推荐的「中间层抽象」: +与 MCP 工具、Subagent、Projects 等能力一起,构成从 Prompt 工程到 Agent 工程的关键桥梁。 + +如果后续希望,可以进一步围绕你的具体场景(例如代码库结构、文档模板、数据分析流程)给出一份「定制 Skill 设计蓝图」,直接从现有 Prompt 资产出发设计目录和 `SKILL.md` 模板。 + + diff --git a/98-AgentSkills/4-Claude Code Agent Skills 架构深度解析与实战实施指南:从提示工程到智能体编排的范式转移.md b/98-AgentSkills/4-Claude Code Agent Skills 架构深度解析与实战实施指南:从提示工程到智能体编排的范式转移.md new file mode 100644 index 0000000..ac8d762 --- /dev/null +++ b/98-AgentSkills/4-Claude Code Agent Skills 架构深度解析与实战实施指南:从提示工程到智能体编排的范式转移.md @@ -0,0 +1,429 @@ +## 1. 执行摘要:智能体时代的上下文工程新前沿 + +在生成式人工智能(Generative AI)的演进历程中,开发者与大语言模型(LLM)的交互模式正经历着一场深刻的范式转移。长期以来,\*\*提示工程(Prompt Engineering)\*\*一直是驾驭模型能力的核心手段,通过精心构造的静态文本来引导模型的随机性输出。然而,随着软件工程复杂度的指数级上升,单纯依赖无状态(Stateless)的提示词已难以满足构建大规模、多步骤、具备长期记忆的自动化系统的需求。 + +本报告旨在为具备深厚提示工程背景的专家提供一份关于 **Claude Code Agent Skills** 的权威技术指南。Claude Code Skills 不仅仅是提示词的延伸,它们代表了一种全新的\*\*智能体编排(Agentic Orchestration)\*\*架构。在这种架构中,静态的文本指令被转化为动态的、可执行的、且具备文件系统感知能力的“技能”模块。这种转变使得 LLM 从一个被动的文本生成器,进化为一个能够主动感知环境、执行确定性代码、并根据任务需求动态加载上下文的智能实体 1。 + +通过对 **模型上下文协议(Model Context Protocol, MCP)** 、文件系统级记忆架构以及混合执行模式(Hybrid Execution Pattern)的深度剖析,本报告将揭示 Skill 如何解决传统提示工程中面临的“上下文窗口过载”、“工具幻觉”以及“复杂逻辑推理不稳定”等核心痛点 3。对于致力于构建自动化代码开发工作流的资深工程师而言,掌握 Skill 的开发与部署,是迈向下一代 AI 辅助开发(AI-Assisted Development)的关键一步。 + +--- + +## 2. 架构基础:从无状态对话到有状态智能体 + +要理解 Agent Skills 的本质,首先必须解构 Claude Code 的运行时环境。与传统的 ChatGPT 或 Claude.ai 网页端对话不同,Claude Code 是一个运行在开发者本地终端(CLI)中的**智能体循环(Agentic Loop)** 。这种环境差异决定了其认知架构的根本不同。 + +### 2.1 文件系统即认知中枢 + +在传统的 LLM 应用开发中,所谓的“记忆”通常是通过向量数据库(Vector Database)实现的检索增强生成(RAG),或者是硬编码在系统提示词(System Prompt)中的规则。然而,Claude Code 引入了一种更为原生且符合开发者直觉的架构:**文件系统即长期记忆与能力注册表** 1。 + +Agent Skills 并非存储在云端数据库或复杂的配置中心,而是以扁平文件的形式直接驻留在项目的 `.claude/skills/` 目录或用户的主目录 `~/.claude/skills/` 中。这种设计带来了几个关键的架构优势: + +1. **上下文局部性(Contextual Locality):** Claude Code 具备递归发现机制。当智能体在 `packages/frontend/` 目录下工作时,它不仅会加载全局技能,还会自动发现并挂载该子目录下的 `.claude/skills/`。这意味着,开发者可以为前端项目定义一套特定的构建和测试技能,而为后端项目定义另一套数据库迁移技能,模型会根据当前的工作路径自动切换“思维模式”和“工具箱” 6。 +2. **版本控制即治理(GitOps for AI):** 由于 Skill 本质上是 Markdown 和脚本文件,它们完全纳入 Git 版本控制体系。这意味着团队对智能体行为的每一次修改、优化或回滚,都像代码审查(Code Review)一样可追溯、可管理。这解决了传统提示工程中 Prompt 版本混乱、难以协作的难题 7。 +3. **环境感知(Environment Awareness):** Skill 能够直接访问本地环境。通过 `{baseDir}` 等变量插值,Skill 可以精确调用与其关联的 Python 或 Bash 脚本,实现从“文本建议”到“代码执行”的跨越 。 + +### 2.2 元工具(Meta-Tool)范式与动态加载 + +从技术实现的角度来看,Skill 是对 LLM **工具使用(Tool Use / Function Calling)** 能力的高级抽象。当开发者创建一个 `SKILL.md` 文件时,Claude Code 的运行时环境(Runtime)并不会立即将文件中的数千字说明全部塞入模型的上下文窗口。这样做会导致上下文窗口迅速耗尽,且过多的无关信息会干扰模型的注意力机制。 + +相反,系统采用了一种\*\*元工具(Meta-Tool)\*\*机制 。在启动阶段,Claude Code 仅扫描所有 Skill 的 YAML 前置元数据(Frontmatter),提取 `name`(名称)和 `description`(描述)。这些轻量级的信息被注册为模型可见的工具定义。 + +当用户发出指令(例如“分析这个 Git 仓库的提交历史”)时,模型首先基于 `description` 进行语义匹配,判断是否需要调用某个 Skill。只有当模型决定激活特定 Skill 时,运行时环境才会读取 `SKILL.md` 的完整内容,并将其作为“临时系统指令”注入到当前的上下文窗口中。这种\*\*按需加载(Just-in-Time Context Loading)\*\*策略,是 Skill 能够支持数百个工具而不会导致模型“痴呆”或成本爆炸的核心原因 6。 + +### 2.3 执行生命周期深度解析 + +一个典型的 Skill 执行过程包含以下几个严密的微步骤,理解这一流程对于调试和优化至关重要: + +1. **意图识别(Intent Recognition):** 用户输入自然语言。模型在后台评估所有可用 Skill 的描述字段。 +2. **路由决策(Routing Decision):** 模型输出一个工具调用请求,例如 `UseSkill(name="git-analyzer")`。 +3. **上下文注入(Context Injection):** 系统拦截该调用,读取 `.claude/skills/git-analyzer/SKILL.md`,解析其中的 `{baseDir}` 等变量,将处理后的 Markdown 文本插入对话历史。 +4. **混合推理(Hybrid Reasoning):** 模型阅读 Skill 中的详细步骤(Prompt),结合用户输入的参数(Arguments),规划执行路径。 +5. **代码执行(Code Execution):** 如果 Skill 指示运行脚本,模型会生成 `Bash` 或 `CodeExecution` 工具调用,系统在本地沙箱中执行脚本并返回 stdout/stderr 结果。 +6. **结果合成(Result Synthesis):** 模型根据脚本的输出,结合 Skill 中定义的输出格式要求,生成最终回复 2。 + +--- + +## 3. 比较分析:提示词、MCP 与 Skills 的三角关系 + +对于精通提示工程的开发者来说,最大的认知障碍往往在于混淆 **Prompt(提示词)** 、**MCP(模型上下文协议)** 和 **Skill(技能)** 的边界。它们虽然都旨在增强模型能力,但在架构栈中处于完全不同的层级。 + +### 3.1 提示工程 vs. 技能工程 + +从提示工程向技能工程的演进,本质上是从**无状态指令**向**有状态能力**的跨越。 + +| **维度** | **提示工程 (Prompt Engineering)** | **技能工程 (Skill Engineering)** | **核心差异分析** | | | | | +| -- | ---------------------------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------- | -- | -- | -- | -- | +| **持久性** | 临时性;仅存在于当前会话窗口或需手动粘贴。 | 持久性;作为文件驻留于项目结构中 (`.claude/skills`)。 | Skill 是项目的永久资产,而 Prompt 是临时消耗品。 | | | | | +| **上下文成本** | 高;复杂的 System Prompt 会持续占用 Token 配额。 | 低;采用“懒加载”机制,仅在激活时消耗 Token。 | Skill 架构允许定义成百上千种能力,而不影响基础对话成本6。 | | | | | +| **执行确定性** | 低;依赖模型生成代码并由用户复制粘贴。 | 高;通过捆绑 Python/Bash 脚本实现确定性逻辑。 | Prompt 告诉模型“如何写代码”,Skill 赋予模型“运行代码的手”4。 | | | | | +| **作用域** | 全局(System Prompt)或 局部(User Message)。 | 层级化;支持组织级、用户级、项目级和目录级覆盖。 | Skill 具备继承和覆盖特性,更适合大型工程团队的协作管理6。 | | | | | +| **逻辑封装** | 困难;超长 Prompt 容易导致“中间迷失”(Lost-in-the-Middle)。 | 模块化;复杂逻辑被封装在独立文件和脚本中。 | Skill 鼓励“关注点分离”,将业务逻辑从 LLM 的推理中剥离出来11。 | | | | | + +**深度洞察:** 传统的提示工程实际上是在试图用自然语言“模拟”计算机程序的逻辑。例如,通过 Prompt 要求模型“像 Python 解释器一样行事”。而 Skill 工程则是**回归本源**,让模型调用真正的 Python 解释器来处理计算任务,只保留模型作为“推理引擎”和“自然语言接口”的角色。这种“脑(LLM)+ 手(Script)”的分离是构建可靠智能体的基石 12。 + +### 3.2 技能 vs. 模型上下文协议 (MCP) + +**模型上下文协议 (MCP)** 是 Anthropic 推出的一项开放标准,旨在解决 AI 模型与外部数据源连接的“碎片化”问题 3。许多开发者困惑于何时使用 MCP,何时使用 Skill。 + +MCP(连接层 / "USB-C 接口"): + +MCP 是底层的数据管道和工具协议。它定义了 Client(如 Claude Code)如何与 Server(如 Postgres 数据库、Google Drive、GitHub)进行通信。MCP Server 暴露的是原子化的工具(Atomic Tools)和资源(Resources)。 + +- *例子:* `postgres.query_table`、`github.get_pull_request`、`filesystem.read_file`。 +- *特点:* 通用、标准化、无业务逻辑。 + +Skills(编排层 / "驱动程序"): + +Skill 是上层的业务逻辑编排器。它定义了如何使用这些 MCP 工具来完成一个复杂的任务。Skill 是一份 SOP(标准作业程序),指导模型按顺序调用 MCP 工具,处理中间结果,并做出决策。 + +- *例子:* 一个名为“自动化代码审查”的 Skill,可能会先调用 MCP 的 `git.diff` 获取变更,再调用 `filesystem.read` 读取代码规范,最后综合生成审查意见。 + +**对比表格:** + +| **特性** | **Model Context Protocol (MCP)** | **Claude Code Agent Skills** | | | | +| -- | --------------------------------------------------------- | ------------------------------------------ | -- | -- | -- | +| **抽象层级** | 低层级;原子化 API 接口。 | 高层级;端到端工作流(Workflow)。 | | | | +| **实现方式** | 编写代码(TypeScript/Python SDK)。 | 编写 Markdown (`SKILL.md`) + 脚本。 | | | | +| **主要角色** | 提供数据访问和基础动作(躯干)。 | 提供操作指南和策略推理(大脑)。 | | | | +| **可移植性** | 通用协议;适用于所有支持 MCP 的客户端(IDE、Desktop)。 | 目前主要针对 Claude Code CLI 优化。 | | | | +| **核心价值** | 连接孤岛数据(Data Connectivity)。 | 固化专家经验(Expertise Distillation)。 | | | | + +架构协同效应: + +最强大的模式是将 Skill 作为 MCP 工具的“包装器”。直接将原始的 SQL 查询工具(通过 MCP 暴露)交给 LLM 往往是危险的,因为它可能执行 DROP TABLE。但是,通过编写一个 SKILL.md,可以限制模型只能执行特定的查询模式,或者强制模型在执行破坏性操作前进行二次确认。Skill 在这里充当了 MCP 工具的安全护栏和上下文增强器 2。 + +--- + +## 4. 深度解析:Skill 开发规范与 `SKILL.md` 详解 + +作为 Context Engineering 的大师,理解 `SKILL.md` 的微观结构是至关重要的。这不仅仅是写文档,而是在编写**可执行的自然语言代码**。 + +### 4.1 Skill 的解剖学结构 + +一个标准的 Skill 是一个包含 `SKILL.md` 的文件夹。该文件由两部分组成:**YAML Frontmatter**(元数据)和 **Markdown Body**(指令体)1。 + +#### 4.1.1 YAML Frontmatter:元数据配置 + +这是 Skill 的“注册表信息”,决定了 Skill 何时以及如何被系统调度。 + +YAML + +``` +--- +name: api-schema-validator +description: Validates API response JSON against the OpenAPI specification. Use when user asks to check API consistency or debug backend responses. +allowed-tools: Bash, Read, Write(test_reports/*) +model: claude-3-opus-20240229 +user-invocable: true +--- +``` + +**关键字段深度分析:** + +- **`name`**:必须是 kebab-case(短横线连接)。这是模型内部引用该 Skill 的唯一标识符。 +- **`description`**:这是**上下文工程**中最关键的部分。它实际上是语义路由(Semantic Router)的匹配键。一个模糊的描述(如 "Helper tool")会导致 Skill 永远不被触发。一个优秀的描述应该包含用户的潜在意图(Intent)和触发场景(Trigger Context),例如 "Use when user asks to..." 6。 +- **`allowed-tools`**:这是一个**安全沙箱**机制。通过显式列出允许使用的工具,开发者可以防止 Skill 执行未授权的操作。 + + - *高级用法:* 支持 Glob 模式匹配。例如 `Bash(python scripts/*.py:*)` 表示仅允许执行 `scripts` 目录下的 Python 脚本,严禁执行 `rm -rf /` 等危险命令。这是企业级部署中不可或缺的安全特性 2。 +- **`model`**:可选字段。允许强制指定 Skill 运行的模型版本。对于需要极其复杂推理(如架构设计)的 Skill,可以强制指定 `claude-3-opus`;而对于简单的格式化任务,可以使用 `claude-3-haiku` 以降低延迟和成本 17。 +- **`user-invocable`**:如果设置为 `false`,则该 Skill 不会出现在 `/` 斜杠命令菜单中,只能由模型根据上下文隐式触发。这适用于那些作为“后台进程”存在的辅助 Skill 6。 + +#### 4.1.2 Markdown Body:指令工程 + +`SKILL.md` 的正文是 Skill 的灵魂。它采用了“思维链(Chain-of-Thought)”的最佳实践,将模糊的任务转化为确定的步骤。 + +**结构化最佳实践:** + +1. **目标声明(Objective Statement):** 用一句话清晰定义 Skill 的产出。 +2. **参数解析(Argument Handling):** 明确指示模型如何处理 `$ARGUMENTS` 变量。 +3. **分步逻辑(Step-by-Step Logic):** 使用编号列表强制模型按顺序执行。这是防止模型“跳步”或“幻觉”的最有效手段。 +4. **输出规范(Output Schema):** 严格定义输出的格式(如 XML 标签、JSON 结构或 Markdown 表格),以便于后续工具或人类阅读。 + +**示例片段:** + +# API Schema Validator + +## Context + +You are a QA Automation Engineer. Your goal is to verify that the API response matches the rigorous OpenAPI v3.0 standard. + +## Execution Plan + +1. **Parse Arguments:** The user input is stored in `$ARGUMENTS`. Treat this as the target API endpoint URL. +2. **Fetch Schema:** Read the `openapi.yaml` file from the project root. +3. **Execute Request:** Use `curl` to fetch the response from `$ARGUMENTS`. +4. **Compare:** + + - Validate field types (e.g., string vs int). + - Check for required fields. +5. **Report:** Output findings in a Markdown table listing any violations. + +### 4.2 复杂参数处理与变量插值 + +在 Prompt Engineering 中,我们习惯于处理自然语言。但在 Skill Engineering 中,我们需要处理结构化数据和变量。这是一个常见的陷阱区域。 + +\$ARGUMENTS 变量的微妙之处: + +当 Skill 被触发时,Claude Code 会捕捉用户的输入并将其放入 \$ARGUMENTS 变量中。 + +- *场景:* 用户输入 `/validate-api https://api.example.com/users`。 +- *变量:* `$ARGUMENTS` 的值为 `"https://api.example.com/users"`。 + +引用与空格问题: + +根据研究片段 18,在处理包含空格的参数时存在一定的不确定性。传统的 Shell 脚本习惯使用位置参数 \$1, \$2。虽然 Claude Code 的 Slash Command 支持这种位置参数解析,但在纯 Skill 模式下,最佳实践是将 \$ARGUMENTS 视为一个原始字符串(Raw String),并在 Skill 内部指示模型或脚本去解析它。 + +- *错误模式:* 直接假设 `$1` 可用,或者未加引号直接传入 Shell:`python script.py $ARGUMENTS`(如果参数含空格,会导致脚本接收到错误的参数个数)。 +- *正确模式:* 始终使用双引号包裹:`python script.py "$ARGUMENTS"`。并在脚本内部使用 `argparse` 处理复杂的参数逻辑 20。 + +JSON 注入模式: + +对于需要传入复杂配置(如多个标志位、嵌套结构)的 Skill,最稳健的工程实践是要求(或由前置 Agent 生成)JSON 格式的参数。 + +- *指令设计:* "Analyze the input `$ARGUMENTS`. If it is not valid JSON, stop and ask the user for JSON format." +- *优势:* LLM 解析 JSON 的能力远强于解析复杂的 Shell 命令行参数,这规避了转义字符和参数错位的风险 21。 + +--- + +## 5. 高级实现:确定性代码集成与混合智能 + +Claude Code Skills 的真正威力在于它能够将**确定性代码(Deterministic Code)与概率性推理(Probabilistic Reasoning)** 结合。这是解决 LLM “数学不好”、“逻辑跳跃”或“无法精确操作文件”等弱点的终极方案。 + +### 5.1 “大脑 + 计算器”混合架构模式 + +在这个模式中,`SKILL.md` 充当“大脑”,负责理解意图、规划任务和解释结果;而伴随的 Python/Node.js 脚本充当“计算器”,负责执行具体的计算、文件分析或 API 调用。 + +目录结构示例: + +.claude/skills/ + +└── git-churn-analyzer/ + +├── SKILL.md + +└── scripts/ + +└── calculate\_churn.py + +实现细节: + +LLM 非常不擅长直接统计 git 提交记录(因为上下文窗口有限,且计数容易出错)。因此,我们在 SKILL.md 中禁止模型手动统计,而是强制其运行脚本。 + +**SKILL.md 核心指令:** + +## Execution Strategy + +DO NOT attempt to count commits by reading git logs manually. + +1. Execute the analysis script located in this skill's directory: + python {baseDir}/scripts/calculate\_churn.py --target "\$ARGUMENTS" +2. The script will generate a file named `churn_report.json`. Read this file. +3. Based on the JSON data, explain to the user which modules require refactoring due to high churn. + +**关键技术点:** + +- **`{baseDir}`** **插值:** 这是一个极其重要的系统变量。它会被运行时环境动态替换为 Skill 所在的绝对路径。这确保了无论用户在项目的哪个子目录下调用 Skill,Claude 都能准确找到配套的脚本文件,解决了相对路径引用的噩梦 。 + +### 5.2 规避 Shell 解析限制与安全沙箱 + +在 Agent 开发中,直接让 LLM 生成复杂的 Shell 命令(包含管道符 `|`、重定向 `>`、通配符 `*`)是极具风险的。 + +1. **语法错误风险:** 模型生成的 Shell 命令可能在不同 OS(MacOS vs Linux vs Windows)上表现不一致。 +2. **权限拒绝风险:** `allowed-tools` 的解析器可能无法正确理解复杂的管道命令,导致权限校验失败 11。 + +解决方案:封装脚本化。 + +将复杂的逻辑(如 find. -name "\*.ts" | xargs grep "pattern" \> result.txt)封装进 scripts/search.sh。Skill 只需要申请运行 bash scripts/search.sh 的权限。这不仅绕过了 Shell 解析的歧义,还极大地提升了安全性——因为审核一个固定的脚本比审核模型生成的随机命令要容易得多。 + +### 5.3 钩子(Hooks)与“守门人”模式 + +Skill 还可以与 Claude Code 的 **Hooks** 系统结合,充当工作流的“守门人”。 + +- **场景:** `pre-commit` 检查。 +- **机制:** 配置一个 Hook,在用户尝试提交代码时自动触发 `security-audit` Skill。 +- **流程:** 该 Skill 运行 `trufflehog` 或自定义 Python 脚本扫描敏感信息。如果发现密钥泄露,Skill 会指示 Claude 终止操作并向用户发出警告。 +- **洞察:** 这种模式将 AI 从“被动助手”升级为“主动合规官”,在代码进入仓库前就拦截潜在风险 22。 + +--- + +## 6. MCP 集成策略:构建连接万物的神经网络 + +虽然 Skill 在本地环境中表现出色,但要构建真正的企业级 Agent,必须连接到 Jira、Salesforce、GitHub 或生产数据库。这正是 **MCP (Model Context Protocol)** 发挥作用的地方。 + +### 6.1 Skill 作为 MCP 的编排者 + +专家级开发者应将 Skill 视为**逻辑层**,将 MCP 视为**数据层**。Skill 的作用是“教”模型如何正确地使用 MCP 工具。 + +实战案例:Jira 故障修复工作流 + +如果没有 Skill,用户需要手动提示 Claude:“先查 Jira,再查代码,然后修 Bug”。 + +有了 fix-jira-issue Skill,流程如下: + +1. **触发:** 用户输入 `/fix ENG-4521`。 +2. **Skill 激活:** `fix-jira-issue` 被加载。 +3. **MCP 调用 1:** Skill 指令:“使用 `jira-mcp` 服务器的 `get_issue` 工具获取 ENG-4521 的详情。” +4. **逻辑推理:** Claude 阅读 Jira 返回的 JSON,理解这是一个空指针异常。 +5. **MCP 调用 2:** Skill 指令:“使用 `github-mcp` 工具基于 `main` 分支创建新分支 `fix/ENG-4521`。” +6. **本地执行:** Skill 指令:“读取堆栈跟踪中的文件,定位问题并修复。” + +**洞察:** Skill 实际上固化了使用 MCP 工具的“最佳实践”。它屏蔽了底层 API 的复杂性,即使用户不知道如何操作 Jira API,也能通过 Skill 完成任务 23。 + +### 6.2 解决“上下文过载”问题 + +直接连接 MCP 服务器的一个常见副作用是**元数据爆炸**。例如,连接一个企业级 Postgres 数据库的 MCP Server 可能会立即向上下文注入 500 个表的 Schema 定义,瞬间挤占所有的推理空间 3。 + +基于 Skill 的渐进式披露(Progressive Disclosure): + +我们可以编写一个 database-explorer Skill 来缓解这个问题: + +1. **第一步:** 仅查询数据库的表名列表(Table Names),而非完整 Schema。 +2. **第二步:** 让模型根据用户问题(如“查询用户信息”),推断出可能相关的表(如 `users`, `profiles`)。 +3. 第三步: 仅针对这几个相关表,调用 MCP 工具获取详细 Schema。 + 通过这种应用层面的渐进式加载,Skill 充当了一个智能过滤器,确保上下文窗口始终只包含当前任务所需的最少信息量。这是大规模系统集成的核心优化手段。 + +--- + +## 7. 战略工程:最佳实践与性能优化 + +编写一个能跑的 Skill 很容易,但编写一个健壮、高效且安全的 Skill 需要遵循严格的工程纪律。 + +### 7.1 渐进式披露与 Token 经济学 + +“上下文窗口是一种公共资源(Public Good)” 9。每一个被 Skill 定义消耗的 Token,都在挤占对话历史和代码文件的空间。 + +- **策略:** 保持 `SKILL.md` 极其精简(建议 500 行以内)。 +- **技术:** 如果 Skill 需要参考一份 50 页的 API 文档,**绝对不要**将其直接粘贴在 `SKILL.md` 中。 +- **实现:** 将文档放入 `docs/api_spec.md`。在 `SKILL.md` 中写入:“如果且仅当你需要查询特定 API 参数时,读取 `docs/api_spec.md`。” +- **效果:** 这种\*\*按需读取(Read-on-Demand)\*\*机制确保了在 Skill 激活的初始阶段只消耗极少的 Token,只有在深层任务执行时才动态加载重型知识库 6。 + +### 7.2 确定性解析 vs. 概率性猜测 + +不要让 LLM 去做正则引擎该做的事。 + +- *弱模式:* 提示模型“查看输入字符串 `user:admin role:editor` 并提取用户名”。LLM 可能会在边缘情况(如用户名包含冒号)下出错。 +- 强模式: 将字符串传递给 scripts/parser.py,脚本使用严格的正则表达式提取数据并返回 JSON。 + 原则: 使用 LLM 处理意图(Intent)和非结构化数据,使用脚本处理语法(Syntax)和结构化数据。 + +### 7.3 安全性:最小权限原则 + +在企业环境中,Skill 本质上是拥有 Shell 访问权的自动化脚本,风险极大。 + +- **权限管控:** 必须在 YAML Frontmatter 中严格限制 `allowed-tools`。如果一个 Skill 只需要读取日志,决不能给它 `Write` 或 `Bash` 的权限 。 +- **代码审查:** 将 `.claude/skills` 目录纳入最严格的 Code Review 流程。任何对 `SKILL.md` 或关联脚本的修改都应被视为对生产代码的修改。 + +### 7.4 调试 Agent Skills + +调试一个“黑盒”模型的思维过程极具挑战性。 + +- **会话隔离:** 利用 `${CLAUDE_SESSION_ID}` 变量。在脚本中将日志输出到 `/tmp/logs/${CLAUDE_SESSION_ID}.log`。这样可以将特定会话的脚本执行轨迹与并行运行的其他会话区分开来 6。 +- **显式回显(Verbose Echoing):** 脚本不应默默失败。如果脚本遇到错误(如文件未找到),应打印明确的错误信息到 `stdout`。Claude 能够读取这些输出并进行**自我修正(Self-Correction)** 。例如,看到“FileNotFoundError”,模型通常会尝试查找正确的文件路径并重试。 + +--- + +## 8. 详细实现案例 + +### 8.1 案例一:“智能重构” Skill(AST 语法树集成) + +此 Skill 展示了如何利用 Python 脚本进行抽象语法树(AST)分析,确保模型生成的重构代码在语法上是合法的,从而避免“代码写完跑不通”的尴尬。 + +**文件路径:** `.claude/skills/refactor/SKILL.md` + +--- + +## name: smart-refactor description: Safely refactors Python functions using AST validation. Use when the user wants to rename functions or extract methods. allowed-tools: Bash, Read, Write + +# Smart Refactor Skill + +## Context + +You are a Python Refactoring Specialist. You prioritize code stability and correctness. + +## Workflow + +1. **Analyze Request:** Identify the target file and function from `$ARGUMENTS`. +2. **Pre-Computation Validation:** + + - Execute the AST validator script: + python {baseDir}/scripts/validate\_ast.py check "\$ARGUMENTS" + - If the script returns an error, **STOP** immediately and report the syntax error to the user. +3. **Plan & Edit:** Propose the refactoring plan and use the `Edit` tool to apply changes. +4. **Post-Computation Verification:** + + - Run the validator script again on the modified file. + - If the script reports syntax errors, **Revert** the changes using `git checkout` and inform the user of the failure. + +**配套脚本:** `.claude/skills/refactor/scripts/validate_ast.py` + +Python + +``` +import ast +import sys + +def check_syntax(file_path): + try: + with open(file_path, 'r') as f: + source = f.read() + ast.parse(source) + print(f"SUCCESS: {file_path} contains valid Python syntax.") + sys.exit(0) + except SyntaxError as e: + print(f"ERROR: Syntax error in {file_path}: {e}") + sys.exit(1) + except Exception as e: + print(f"ERROR: Could not read file: {e}") + sys.exit(1) + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("Usage: validate_ast.py check ") + sys.exit(1) + check_syntax(sys.argv) +``` + +**分析:** 这个 Skill 注入了一个**确定性护栏**。它不再盲目信任模型的输出,而是利用 AST 解析器作为客观的裁判。这体现了 Agent Engineering 的核心思想:**Trust, but Verify(信任,但要验证)** 。 + +### 8.2 案例二:强制 TDD 开发循环 + +此 Skill 并不执行复杂代码,而是通过 Prompt 强制执行一种行为约束,即“测试驱动开发(TDD)”。 + +**文件路径:** `.claude/skills/tdd-cycle/SKILL.md` + +--- + +## name: tdd-cycle description: Implements features using strict Test-Driven Development protocol. Use when asked to "implement feature X" or "add functionality". model: claude-3-opus-20240229 + +# TDD Enforcement Protocol + +## Core Directive + +You are FORBIDDEN from writing any implementation code until you have witnessed a specific test failure. + +## Execution Steps + +1. **Phase 1: RED (Write Test)** + + - Analyze the requirement. + - Create a new test file in `tests/` asserting the desired behavior. + - Run the test command: `npm test` + - **Checkpoint:** Verify the output shows a specific assertion error matching the new feature. If the test passes or fails for the wrong reason, refine the test. +2. **Phase 2: GREEN (Make it Pass)** + + - Write the *minimum* implementation code required to satisfy the test. + - Do NOT optimize or add extra features yet. + - Run `npm test` again to confirm all tests pass. +3. **Phase 3: REFACTOR (Clean Up)** + + - Review the code for duplication or clarity issues. + - Refactor if necessary, ensuring tests remain green. + +**分析:** 这个 Skill 充当了**行为约束器(Behavioral Constraint)** 。它为智能体植入了一个“超我(Super-ego)”,抑制了 LLM 倾向于直接给出最终代码(往往带有 Bug)的冲动。通过强制执行 Red-Green-Refactor 循环,Skill 显著提高了生成代码的质量和可靠性。 + +--- + +## 9. 结论:Agentic Workflows 的未来展望 + +Claude Code Agent Skills 的出现,标志着我们正在从**指令式交互(Instructional Interfacing)迈向环境工程(Environmental Engineering)** 。作为专家级的开发者,我们的工作重心正从“如何写出完美的提示词”转移到“如何构建完美的智能体运行环境”。 + +掌握 `SKILL.md` 的架构、策略性地使用 `{baseDir}` 脚本封装复杂逻辑、以及精细编排 MCP 数据流,是构建下一代半自主工程师(Semi-autonomous Engineers)的关键。未来,软件工程团队的核心资产将不仅仅是业务代码,还包括随代码库一同演进的、高度专业化的 Agent Skills 库。这些 Skill 将不仅包含代码的知识,更包含团队的工程文化、架构规范和最佳实践,真正实现“代码即知识,环境即智能”。 \ No newline at end of file diff --git a/99-项目模板/1-原始需求/0-产品经理-prompt.md b/99-项目模板/1-原始需求/0-产品经理-prompt.md new file mode 100644 index 0000000..0e1ea36 --- /dev/null +++ b/99-项目模板/1-原始需求/0-产品经理-prompt.md @@ -0,0 +1,17 @@ +你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。 + +---- + +你是一名出色的产品经理,能够根据用户的初始需求,理解用户需求的真实需求意图,改善客户不够完善的需求,形成专业、简练的需求文档。并且能够在基础需求上优化产品的额设计和功能 + +请根据要求,研究jenkins的API,进行深度的思考,优化[1-初始需求稿.md],直接给出优化后的PRD + +---- + +你是一名出色的产品经理,请你根据[1-初始需求稿.md]客户的原始需求,审查[2-优化产品需求文档PRD.md],检查PRD文档是否完全满足原始需求;如果有更加优秀的设计方案,请给出修改建议 + +---- + +你是一名出色的产品经理,能够根据用户的初始需求,理解用户需求的真实需求意图,改善客户不够完善的需求,形成专业、简练的需求文档。并且能够在基础需求上优化产品的额设计和功能 + +你之前根据[1-初始需求稿.md]输出了[2-优化产品需求文档PRD.md], 目前初始需求有一些更新,见[1.1-初始需求稿.md],请你详细对比[1.1-初始需求稿.md]和[1-初始需求稿.md]之间的差异, 修改[2-优化产品需求文档PRD.md],得到新的需求文档 \ No newline at end of file diff --git a/99-项目模板/1-原始需求/1-初始需求稿.md b/99-项目模板/1-原始需求/1-初始需求稿.md new file mode 100644 index 0000000..262f17f --- /dev/null +++ b/99-项目模板/1-原始需求/1-初始需求稿.md @@ -0,0 +1,33 @@ +# 家庭游戏中心 + +为父母设计的游戏中心,具有极度的简易操作性 + +## 操控逻辑 +1. SteamOS的方式,完全屏蔽底层的操作系统,最好无法退出的形式 + 1. 请分析基于LINUX还是WINDOWS实现 + 2. 如果为WINDOWS需要对系统进行深度的简化裁剪,去除系统更新等内容 +2. 开机自动进入,需要系统层面的深度优化 +3. 进入之后很难推出 +4. 无广告 + +## 设计逻辑 +1. 字体足够大,尽量减少文字的出现 +2. 鼠标控制器足够大 +3. 类似于大富翁,梦想城镇的美术风格 + +## 游戏目录 + +### 扑克类 +1. 斗地主 +2. 拖拉机 + +### 麻将类 +1. 卡五星 + +### 赛车类 + + +## 技术栈 +1. 熟练掌握Golang Python +2. 能够看懂Vue等前端代码,可以修改 +3. 能够看懂TypeScript \ No newline at end of file diff --git a/99-项目模板/1-原始需求/2-优化产品需求文档PRD.md b/99-项目模板/1-原始需求/2-优化产品需求文档PRD.md new file mode 100644 index 0000000..e69de29 diff --git a/99-项目模板/2-概要详细设计/0-概要设计prompt.md b/99-项目模板/2-概要详细设计/0-概要设计prompt.md new file mode 100644 index 0000000..463dc49 --- /dev/null +++ b/99-项目模板/2-概要详细设计/0-概要设计prompt.md @@ -0,0 +1,26 @@ +你是一名资深的软件系统架构师,具备以下核心职责与能力,绘图请使用mermaid语言: + +## 需求分析与理解 + +- 深度解读产品需求文档(PRD),识别业务目标、功能需求与非功能性需求 +- 分析需求间的依赖关系与优先级,识别潜在的技术风险与挑战 + +## 架构设计与规划 + +- 设计高可用、可扩展、高性能的系统架构,确保系统健壮性与安全性 +- 制定技术选型方案,包括开发语言、框架、中间件、数据库等关键技术栈 +- 绘制多层次架构图(系统架构图、数据流图、组件交互图) +- 定义模块划分、接口规范、数据模型与核心算法策略 +- 规划系统分层结构(展现层、业务层、数据层),明确各层职责边界 + +## 方案设计与输出 + +- 针对核心需求点提供详细的技术解决方案,包含实现路径与备选方案 +- 设计关键业务流程的时序图与状态机,确保逻辑清晰完整 +- 输出规范化的《系统详细设计说明书》,包含架构设计、接口定义、数据库设计等完整文档 + +请根据[2-优化产品需求文档PRD.md],按照上述的要求,输出系统详细设计说明书 + + + +你之前根据[2-优化产品需求文档PRD.md]输出了[3-详细设计说明书.md], 目前PRD需求有一些更新,见[2.1-优化产品需求文档PRD.md],请你详细对比[2.1-优化产品需求文档PRD.md]和[2-优化产品需求文档PRD.md]之间的差异, 修改[3-详细设计说明书.md],得到新的需求文档.要求尽量不改动[3-详细设计说明书.md]的初始设计,只改动差异化部分的设计 \ No newline at end of file diff --git a/99-项目模板/2-概要详细设计/3-详细设计说明书.md b/99-项目模板/2-概要详细设计/3-详细设计说明书.md new file mode 100644 index 0000000..2f4d816 --- /dev/null +++ b/99-项目模板/2-概要详细设计/3-详细设计说明书.md @@ -0,0 +1,1785 @@ +# 家庭游戏中心 (Family Game Center) - 产品需求文档 (PRD) + +**文档版本:** 1.0 +**最后更新:** 2024-12-29 +**状态:** 已审批,进入开发阶段 +**目标平台:** Linux (Ubuntu 22.04 LTS) / 开发平台: Windows 11 + +--- + +## 📋 文档目录 + +1. [项目概述](#项目概述) +2. [功能需求](#功能需求) +3. [技术方案](#技术方案) +4. [系统架构](#系统架构) +5. [开发规范](#开发规范) +6. [API 设计](#api-设计) +7. [数据库设计](#数据库设计) +8. [UI/UX 设计规范](#uiux-设计规范) +9. [安全需求](#安全需求) +10. [测试计划](#测试计划) +11. [部署方案](#部署方案) +12. [验收标准](#验收标准) + +--- + +## 项目概述 + +### 1.1 项目背景 + +为父母(年龄 50+ 岁)设计的易用家庭游戏平台。该平台旨在提供一个完全锁定的游戏环境,使非技术用户无法误操作进入系统,只能使用游戏功能。 + +### 1.2 项目目标 + +| 目标 | 说明 | +|------|------| +| **易用性** | 父母无计算机知识也能独立使用 | +| **安全性** | 系统级锁定,用户无法退出或破坏系统 | +| **稳定性** | 无中断运行 48+ 小时 | +| **游戏性** | AI 对手难度可调,赢率 40-60% | +| **维护性** | 单人可维护和更新 | + +### 1.3 产品范围 + +**包含:** +- 4 款主要游戏 (斗地主、拖拉机、卡五星、赛车) +- Linux Kiosk 启动器 +- 响应式菜单 UI +- 本地 AI 对手 +- 游戏存档管理 +- 基础统计功能 + +**不包含:** +- 网络在线多人对战 +- 云同步和备份 +- 用户账户系统 +- 第三方集成 +- 语音/视频功能 + +### 1.4 项目成功标准 + +✓ MVP 版本 (斗地主 + 菜单): 6-8 周 +✓ 完整版 (全部游戏): 12-18 周 +✓ 总成本: 硬件 5-15K,软件 0 元 +✓ 团队规模: 1-3 人 +✓ 风险等级: 低-中等 + +--- + +## 功能需求 + +### 2.1 用户故事 + +#### 故事 1: 启动和菜单 +``` +作为 父亲 +我想 开机后直接进入游戏菜单 +这样 我无需学习系统操作,只需选择想玩的游戏 +``` + +**验收标准:** +- 系统启动后 5 秒内进入菜单 +- 菜单显示 4 个游戏卡片 +- 卡片字体 32px+,易于阅读 +- 按钮 80px+,易于点击 +- 菜单背景颜色高对比度 (WCAG AAA) + +#### 故事 2: 启动游戏 +``` +作为 母亲 +我想 点击游戏卡片启动游戏 +这样 我可以开始游玩 +``` + +**验收标准:** +- 点击卡片 3 秒内启动游戏 +- 游戏加载时显示进度提示 +- 游戏结束自动返回菜单 +- 支持按 ESC 返回菜单 + +#### 故事 3: 无法退出 +``` +作为 家人 +我想 用户无法通过快捷键或菜单退出游戏 +这样 游戏不会被误关闭或系统被破坏 +``` + +**验收标准:** +- Alt+F4 无效 +- Alt+Tab 无效 +- Windows 键无效 +- 右键菜单禁用 +- 任务栏隐藏 +- TTY 无法切换 (Ctrl+Alt+Fx) + +#### 故事 4: AI 对手 +``` +作为 玩家 +我想 和智能电脑对手对战 +这样 我可以随时游戏,不需要其他玩家 +``` + +**验收标准:** +- 所有游戏都有 2-4 个 AI 对手 +- AI 难度可调 (简单/中等/困难) +- AI 出牌合法且有策略性 +- AI 响应时间 1-2 秒 + +#### 故事 5: 保存进度 +``` +作为 玩家 +我想 每局游戏的成绩和统计被自动保存 +这样 我可以看到我的进度和胜率 +``` + +**验收标准:** +- 游戏结束后自动保存成绩 +- 显示历史成绩和统计数据 +- 统计包括: 总局数、胜率、平均用时 +- 数据持久存储在本地数据库 + +### 2.2 功能清单 + +#### 2.2.1 菜单系统 (Priority: P0 - 必须) + +| 功能 | 说明 | 优先级 | +|------|----------------------------------------|--------| +| **游戏列表展示** | 网格布局显示 4 个游戏卡片 | P0 | +| **游戏卡片** | 包含海报、名称、简描 | P0 | +| **启动游戏** | 点击卡片启动对应游戏 | P0 | +| **时间显示** | 菜单顶部显示当前时间 | P1 | +| **返回菜单** | 游戏结束自动返回,支持 ESC 返回 | P0 | +| **响应式布局** | 适配 1080p 到 4K 分辨率, 以2K分辨率 150%缩放作为基准开发 | P0 | +| **高对比度** | WCAG AAA 标准配色 | P0 | +| **父母友好** | 大字体、大按钮 | P0 | + +#### 2.2.2 斗地主游戏 (Priority: P0 - 必须) + +| 功能 | 说明 | 优先级 | +|------|------|--------| +| **基础规则** | 单张、对、顺、炸、王炸判定 | P0 | +| **手牌显示** | 玩家手牌按大小排序显示 | P0 | +| **出牌交互** | 鼠标/触屏选牌并出牌 | P0 | +| **地主判定** | 系统自动判定地主 | P0 | +| **AI 对手** | 2 个 AI 农民对手 | P0 | +| **游戏流程** | 发牌→抢地主→轮流出牌→结束 | P0 | +| **声音效果** | 出牌音效、胜负音效 | P1 | +| **游戏统计** | 保存胜负和用时 | P1 | + +**牌型规则:** +- 单张: 任何一张牌 +- 对牌: 同点数的两张牌 +- 三张: 同点数的三张牌 +- 顺子: 5 张及以上连续的牌 (2 不能在顺子中) +- 炸弹: 同点数的四张牌 (可以压任何牌) +- 王炸: 红黑王 (最大) +- 其他: 简化版,不实现飞机等复杂牌型 + +#### 2.2.3 拖拉机游戏 (Priority: P1 - 高) + +| 功能 | 说明 | 优先级 | +|------|------|--------| +| **基础规则** | 四人跑牌,拖拉机机制 | P1 | +| **主牌概念** | 主花色、主点数 | P1 | +| **AI 对手** | 3 个 AI 对手 | P1 | +| **游戏流程** | 发牌→选主→轮流出牌→计分→结束 | P1 | + +#### 2.2.4 卡五星麻将 (Priority: P2 - 可选) + +| 功能 | 说明 | 优先级 | +|------|------|--------| +| **基础规则** | 五人麻将变种 | P2 | +| **AI 对手** | 4 个 AI 对手 | P2 | +| **胡牌类型** | 简化版,5-10 种常见胡牌 | P2 | + +#### 2.2.5 赛车游戏 (Priority: P1 - 高) + +| 功能 | 说明 | 优先级 | +|------|------|--------| +| **基础机制** | 加速、减速、转向、碰撞 | P1 | +| **AI 对手** | 1 个 AI 赛手 | P1 | +| **赛道** | 简单环形赛道,3 圈 | P1 | +| **计时** | 完成赛道的用时和排名 | P1 | + +#### 2.2.6 系统功能 (Priority: P0 - 必须) + +| 功能 | 说明 | 优先级 | +|------|------|--------| +| **自动启动** | 系统启动后自动进入菜单 | P0 | +| **快捷键禁用** | 完全禁用系统快捷键 | P0 | +| **进程监控** | 游戏异常退出时自动返回菜单 | P0 | +| **数据持久化** | 游戏数据本地保存 | P0 | +| **日志记录** | 系统和游戏日志记录 | P1 | +| **故障恢复** | 自动修复数据损坏和异常状态 | P1 | + +### 2.3 非功能需求 + +| 类别 | 需求 | 验收标准 | +|------|------|---------| +| **性能** | 启动时间 | < 5 秒进入菜单 | +| **性能** | 菜单响应 | < 100ms UI 反馈 | +| **性能** | 内存占用** | < 500MB (菜单+游戏) | +| **性能** | 帧率 | 60fps 稳定 | +| **可靠性** | 运行时长 | 无中断 48+ 小时 | +| **可靠性** | 故障恢复 | 自动重启恢复 | +| **安全性** | 系统锁定 | 三层防护机制 | +| **兼容性** | 分辨率 | 1080p, 1440p, 2560p, 4K | +| **兼容性** | 输入 | 鼠标、触屏、手柄 | +| **可维护性** | 代码** | 注释完整,易扩展 | +| **可维护性** | 文档** | API、部署、故障排查 | + +--- + +## 技术方案 + +### 3.1 技术栈选择 + +#### 3.1.1 开发平台 (Windows 11) + +``` +IDE/编辑器: VS Code / JetBrains IDE +Golang: 1.20+ (Golang for Windows) +Node.js: 18+ LTS (Windows版) +Godot Engine: 4.1+ (Windows版) +Git: Git Bash or GitHub Desktop +包管理器: npm (Node.js), go mod (Golang) +``` + +**开发优势:** +- 所有开发工具都有完整的 Windows 支持 +- Golang 和 Node.js 可在 Windows 上原生编译 +- Godot 有完整的 Windows 编辑器 +- 跨平台编译: 在 Windows 编译 Linux 二进制 + +#### 3.1.2 部署平台 (Ubuntu 22.04 LTS) + +``` +操作系统: Ubuntu 22.04 LTS (Server 版本推荐) +系统配置: Kiosk 模式 + systemd + Openbox +启动器: Golang 二进制 + zserge/webview +菜单UI: Vue 3 构建输出 +游戏: Godot 导出的 Linux 64-bit 二进制 +运行时: 无需额外依赖 (所有编译为静态二进制) +``` + +**部署优势:** +- Fedora Silverblue 不可变文件系统,或 Ubuntu Server 最小化 +- Kiosk 配置完全锁定系统 +- 无强制系统更新 +- 极低资源占用 + +### 3.2 架构设计 + +``` +┌─────────────────────────────────────────┐ +│ 用户交互层 (User Layer) │ +│ 鼠标/触屏/键盘 │ +└──────────────────┬──────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ 应用层 (Application Layer) │ +│ │ +│ ┌────────────────┐ ┌──────────────┐ │ +│ │ Golang Launcher│ │ Process Mgr │ │ +│ │ - Webview │ │ - Game spawn │ │ +│ │ - 全屏模式 │ │ - 监控 │ │ +│ └────────┬───────┘ └──────┬───────┘ │ +│ │ │ │ +│ ┌────────┴──────────────────┴────┐ │ +│ │ HTTP Server (localhost:8888) │ │ +│ └────────────────┬───────────────┘ │ +│ │ │ +│ ┌────────────────┴──────────────┐ │ +│ │ Vue 3 前端菜单 │ │ +│ │ - 游戏网格 │ │ +│ │ - 响应式布局 │ │ +│ └────────────────┬──────────────┘ │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ 游戏层 (Game Layer) │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌────────┐ │ +│ │Godot Game│ │Godot Game│ │Pixi.js │ │ +│ │ 斗地主 │ │ 拖拉机 │ │ 赛车 │ │ +│ └──────────┘ └──────────┘ └────────┘ │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ 系统层 (System Layer - Linux) │ +│ │ +│ ┌────────────────────────────────────┐ │ +│ │ systemd 系统管理 │ │ +│ │ - 自动登录 kiosk 用户 │ │ +│ │ - 启动 game-launcher 服务 │ │ +│ │ - 禁用睡眠、更新等 │ │ +│ └────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────┐ │ +│ │ X11/Wayland Display Server │ │ +│ │ - Openbox 极简窗口管理 │ │ +│ │ - 禁用快捷键 (X11 事件) │ │ +│ │ - 禁用系统菜单 │ │ +│ └────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────┐ │ +│ │ Linux Kernel │ │ +│ │ - SELinux / AppArmor │ │ +│ │ - kiosk 用户权限管理 │ │ +│ └────────────────────────────────────┘ │ +└─────────────────────────────────────────┘ +``` + +### 3.3 技术决策表 + +| 组件 | 选型 | 原因 | +|------|------|------| +| **启动器** | Golang | 内存 20MB, 启动 <100ms, 单二进制 | +| **菜单 UI** | Vue 3 + TypeScript | 响应式, 易修改, 开发快 | +| **卡牌游戏** | Godot 4 + GDScript | UI 系统优秀, 易学, 体积小 | +| **赛车游戏** | Pixi.js (Web 版) | 2D 性能最优, 易于参数调整 | +| **AI 算法** | 决策树 + 蒙特卡洛 | 快速开发, 难度可调, 可解释 | +| **数据库** | SQLite | 轻量级, 无需服务器, 易于备份 | +| **配置** | JSON | 易读易写, 无需解析库 | + +--- + +## 系统架构 + +### 4.1 模块划分 + +``` +project-root/ +│ +├─ launcher/ # Golang 启动器 (Module-1) +│ ├─ cmd/main.go # 入口点 +│ ├─ internal/ +│ │ ├─ launcher/launcher.go # 核心启动器逻辑 +│ │ ├─ server/http.go # HTTP 服务器 +│ │ ├─ process/manager.go # 进程管理 +│ │ ├─ config/config.go # 配置加载 +│ │ └─ logger/logger.go # 日志系统 +│ ├─ go.mod +│ ├─ go.sum +│ └─ Makefile # 编译脚本 +│ +├─ frontend/ # Vue 3 菜单 (Module-2) +│ ├─ src/ +│ │ ├─ App.vue # 根组件 +│ │ ├─ main.ts # 入口 +│ │ ├─ components/ +│ │ │ ├─ GameGrid.vue # 游戏网格 +│ │ │ ├─ GameCard.vue # 游戏卡片 +│ │ │ └─ LoadingSpinner.vue # 加载提示 +│ │ ├─ views/ +│ │ │ ├─ MainMenu.vue # 菜单视图 +│ │ │ ├─ StatisticsView.vue # 统计视图 (可选) +│ │ │ └─ SettingsView.vue # 设置视图 (可选) +│ │ ├─ services/ +│ │ │ └─ api.ts # API 通信 +│ │ ├─ types/ +│ │ │ └─ game.ts # 类型定义 +│ │ └─ styles/ +│ │ └─ main.css # 全局样式 +│ ├─ public/ +│ │ ├─ games.json # 游戏配置 +│ │ ├─ images/ # 游戏海报 +│ │ └─ fonts/ # 中文字体 +│ ├─ package.json +│ ├─ tsconfig.json +│ ├─ vite.config.ts +│ └─ Makefile +│ +├─ games/ # 游戏项目 +│ │ +│ ├─ doudizhu/ # 斗地主 (Module-3) +│ │ ├─ project.godot # Godot 项目配置 +│ │ ├─ scenes/ +│ │ │ ├─ Game.tscn # 主场景 +│ │ │ ├─ Card.tscn # 卡牌节点 +│ │ │ ├─ Player.tscn # 玩家区域 +│ │ │ └─ UI/ +│ │ │ ├─ HandUI.tscn +│ │ │ ├─ TableUI.tscn +│ │ │ └─ InfoPanel.tscn +│ │ ├─ scripts/ +│ │ │ ├─ Game.gd # 主逻辑 (~800 行) +│ │ │ ├─ CardLogic.gd # 规则判定 (~300 行) +│ │ │ ├─ AIPlayer.gd # AI 对手 (~500 行) +│ │ │ └─ UIController.gd # UI 控制 (~200 行) +│ │ ├─ assets/ +│ │ │ ├─ images/ +│ │ │ │ ├─ cards/ # 108 张牌纹理 +│ │ │ │ └─ ui/ +│ │ │ ├─ sounds/ +│ │ │ │ ├─ card_play.wav +│ │ │ │ ├─ win.wav +│ │ │ │ └─ lose.wav +│ │ │ └─ fonts/ +│ │ └─ export_presets.cfg # 导出配置 +│ │ +│ ├─ tractor/ # 拖拉机 (Module-4) +│ │ ├─ project.godot +│ │ ├─ scenes/ +│ │ ├─ scripts/ +│ │ │ ├─ Game.gd +│ │ │ ├─ CardLogic.gd # 拖拉机规则 +│ │ │ └─ AIPlayer.gd +│ │ └─ assets/ +│ │ +│ ├─ mahjong/ # 卡五星 (Module-5, 可选) +│ │ ├─ project.godot +│ │ ├─ scenes/ +│ │ ├─ scripts/ +│ │ │ ├─ Game.gd +│ │ │ ├─ MahjongLogic.gd # 麻将规则 +│ │ │ └─ AIPlayer.gd +│ │ └─ assets/ +│ │ +│ └─ racing/ # 赛车 (Module-6, Pixi.js) +│ ├─ src/ +│ │ ├─ main.ts +│ │ ├─ Game.ts # 游戏主类 +│ │ ├─ Car.ts # 车辆物理 +│ │ ├─ Track.ts # 赛道管理 +│ │ ├─ AI.ts # AI 赛手 +│ │ └─ UI.ts # UI 管理 +│ ├─ assets/ +│ │ ├─ images/ +│ │ ├─ sounds/ +│ │ └─ fonts/ +│ ├─ package.json +│ └─ tsconfig.json +│ +├─ shared/ # 共享资源 +│ ├─ fonts/ +│ │ ├─ SourceHanSansSC-Regular.otf # 中文字体 +│ │ └─ SourceHanSansSC-Bold.otf +│ ├─ images/ +│ │ ├─ doudizhu.jpg # 游戏海报 +│ │ ├─ tractor.jpg +│ │ ├─ mahjong.jpg +│ │ └─ racing.jpg +│ └─ sounds/ +│ ├─ ui_click.wav +│ ├─ ui_hover.wav +│ └─ bgm.ogg +│ +├─ config/ # 配置文件 +│ ├─ kiosk/ +│ │ ├─ gdm-custom.conf # GDM 自动登录 +│ │ ├─ systemd-service.ini # systemd 服务 +│ │ ├─ openbox-rc.xml # Openbox 配置 +│ │ └─ xset-config.sh # X11 配置脚本 +│ └─ app/ +│ ├─ launcher.json # 启动器配置 +│ └─ games.json # 游戏清单 +│ +├─ scripts/ # 构建和部署脚本 +│ ├─ build-windows.bat # Windows 编译脚本 +│ ├─ build-linux.sh # Linux 编译脚本 +│ ├─ build-all.sh # 全量编译脚本 +│ ├─ cross-compile.sh # 交叉编译脚本 +│ ├─ deploy.sh # 部署脚本 +│ ├─ setup-kiosk.sh # Kiosk 配置脚本 +│ └─ verify.sh # 验证脚本 +│ +├─ docs/ # 文档 +│ ├─ PRD.md # 本文档 +│ ├─ ARCHITECTURE.md # 架构详解 +│ ├─ DEVELOPMENT.md # 开发指南 +│ ├─ API.md # API 文档 +│ ├─ DEPLOYMENT.md # 部署指南 +│ ├─ TESTING.md # 测试指南 +│ └─ TROUBLESHOOTING.md # 故障排查 +│ +├─ tests/ # 测试用例 +│ ├─ launcher/ +│ ├─ frontend/ +│ ├─ games/ +│ └─ integration/ +│ +├─ README.md # 项目概述 +├─ Makefile # 顶层编译脚本 +├─ docker-compose.yml # 可选: Docker 支持 +└─ .github/ + └─ workflows/ # CI/CD 配置 + +``` + +### 4.2 依赖关系 + +``` +启动器 (Golang) + │ + ├─ 依赖: zserge/webview (嵌入式浏览器) + │ stdlib (net/http, os/exec, etc.) + │ + └─→ 启动 HTTP 服务器 (localhost:8888) + │ + ├─ 提供静态文件: 菜单 UI (Vue) + │ + ├─ 提供 API 端点: /api/launch + │ │ + │ └─→ 调用 os/exec 启动游戏进程 + │ + └─ 提供 Webview + │ + └─→ 显示菜单 UI + │ + └─→ 用户交互 + │ + └─→ 调用 API 启动游戏 + +游戏进程 (Godot / Pixi.js) + │ + ├─ 通过 IPC/stdio 接收启动参数 + │ + └─ 独立运行,不需与启动器通信 + (游戏结束时自己主动退出) + +菜单 UI (Vue 3) + │ + ├─ 依赖: Vue 3, TypeScript, Vite + │ + └─ 运行于 Webview 中 + │ + ├─ 加载 games.json + │ + └─ 通过 fetch API 调用启动器 HTTP 接口 +``` + +--- + +## 开发规范 + +### 5.1 代码规范 + +#### 5.1.1 Golang 代码规范 + +```go +// 文件头注释 +// Package main 描述此包的用途 +package main + +import ( + "fmt" + "os" + // 标准库分组 + + "github.com/zserge/webview" + // 第三方库分组 +) + +const ( + // 常量大写 + 下划线 + DEFAULT_WEB_PORT = 8888 + LAUNCHER_VERSION = "1.0.0" +) + +type GameLauncher struct { + // 字段注释 + currentGame *exec.Cmd // 当前运行的游戏进程 + webPort int // HTTP 服务器端口 + config Config // 配置对象 +} + +// NewGameLauncher 创建启动器实例 +// 返回: *GameLauncher - 启动器指针 +func NewGameLauncher() *GameLauncher { + return &GameLauncher{ + webPort: DEFAULT_WEB_PORT, + } +} + +// Start 启动游戏启动器 +// 返回: error - 错误信息 +func (gl *GameLauncher) Start() error { + // 函数实现 + return nil +} +``` + +**规则:** +- 变量名用驼峰命名 (camelCase) +- 常量用大写 (CONSTANT_NAME) +- 每个公共函数必须有注释 +- 每个包必须有注释说明其用途 +- 错误处理不能忽略: `if err != nil { return err }` +- 使用 `fmt.Sprintf` 构建字符串,不要用 `+` + +#### 5.1.2 Vue/TypeScript 代码规范 + +```typescript +// 文件头注释 +/** + * GameLauncher.vue - 游戏启动器主组件 + * + * 功能: + * - 显示游戏菜单网格 + * - 处理游戏启动逻辑 + * - 显示加载状态 + */ + + + + + + +``` + +**规则:** +- 使用 `const` 优于 `let`,尽量避免 `var` +- 使用 TypeScript,不要用 JavaScript +- 组件名使用 PascalCase (GameCard.vue) +- 变量名使用 camelCase +- 使用 BEM 命名法管理 CSS 类名 +- 必须包含 ARIA 无障碍属性 +- 类型定义在每个文件头部 + +#### 5.1.3 GDScript 代码规范 + +```gdscript +# 文件头注释 +## CardLogic.gd - 卡牌逻辑和规则判定 +## +## 实现斗地主的卡牌判定算法 +## 包括牌型检查、胜负判定等 + +class_name CardLogic + +# 常量定义 +const CARD_RANKS = ["9", "10", "J", "Q", "K", "A", "2"] +const CARD_SUITS = ["♠", "♥", "♦", "♣", "王"] + +# 静态函数 +static func is_valid_play(cards: Array) -> bool: + """检查出牌是否合法""" + if cards.is_empty(): + return false + + # 单张 + if cards.size() == 1: + return true + + # 对牌 + if cards.size() == 2: + return cards[0].rank == cards[1].rank + + return false + +# 实例函数 +func get_best_play(hand: Array, table_cards: Array) -> Array: + """根据手牌和桌面牌,给出最佳出牌""" + # 实现逻辑 + return [] +``` + +**规则:** +- 类名使用 PascalCase (CardLogic) +- 函数名使用 snake_case (is_valid_play) +- 常量使用 SCREAMING_SNAKE_CASE +- 每个公共函数使用 `##` 文档注释 +- 避免全局变量,使用类成员变量 + +### 5.2 分支管理 + +``` +main (生产分支) + │ + ├─ develop (开发分支) + │ │ + │ ├─ feature/launcher (启动器功能) + │ ├─ feature/menu-ui (菜单 UI) + │ ├─ feature/doudizhu (斗地主) + │ ├─ feature/ai-algorithm (AI 算法) + │ └─ bugfix/xxx (bug 修复) + │ + └─ release/v1.0 (发版分支) +``` + +**规则:** +- 功能开发: `feature/功能名` +- bug 修复: `bugfix/bug名` +- 发版准备: `release/版本号` +- 每个 PR 必须有描述和关联 issue +- 至少一人 code review 后才能合并 + +### 5.3 提交规范 + +```bash +# 格式: (): +# +#