当前位置: 首页 > news >正文

Coze源码分析-资源库-编辑插件-后端源码-详细流程

10. 插件编辑流程图

10.1 UpdatePluginMeta接口完整调用流程

用户登录 Coze 平台点击"资源库" → 在表格中点击要编辑的插件行进行编辑场景的后端处理流程:

用户点击编辑 → 前端发起请求 → API网关路由 → Handler处理 → 业务服务层 → 锁定验证 → 数据更新层 → 索引更新 → 响应返回↓                    ↓           ↓          ↓         ↓          ↓            ↓          ↓
前端编辑表单        HTTP PUT请求    路由匹配     参数验证    权限检查     锁定验证     MySQL更新    ES索引更新   JSON响应↓                    ↓           ↓          ↓         ↓          ↓            ↓          ↓
/edit-plugin       /api/plugin_api/  Handler    请求绑定   用户身份    版本检查     plugin_      事件发布     编辑结果update_plugin_   函数调用   参数校验   Session    锁定续期     resource     异步处理     状态返回meta             UpdatePluginMeta   验证       Redis锁定    表更新       ES索引更新   ↓PluginApplicationService↓身份验证(登录检查)↓权限验证(编辑权限检查)↓锁定验证(锁定有效性检查)↓版本检查(版本一致性验证)↓参数验证(编辑字段验证)↓数据库更新事务↓锁定续期操作↓编辑事件发布↓返回编辑结果

10.2 插件编辑详细流程说明

1. API网关层(路由处理)

文件位置backend/api/handler/coze/plugin_develop_service.go

// @router /api/plugin_api/update_plugin_meta [PUT]
func UpdatePluginMeta(ctx context.Context, c *app.RequestContext) {var err errorvar req plugin_develop.UpdatePluginMetaRequest// 1. 请求参数绑定和验证err = c.BindAndValidate(&req)if err != nil {invalidParamRequestResponse(c, err.Error())return}// 2. 插件编辑参数校验if req.GetPluginID() <= 0 {invalidParamRequestResponse(c, "plugin ID is invalid")return}if req.Version == "" {invalidParamRequestResponse(c, "version is required for edit")return}if req.LockID == "" {invalidParamRequestResponse(c, "lock ID is required for edit")return}// 3. 设置锁定ID到上下文ctx = context.WithValue(ctx, consts.ContextKeyPluginLockID, req.LockID)// 4. 调用插件编辑服务resp, err := plugin.PluginApplicationSVC.UpdatePluginMeta(ctx, &req)if err != nil {handlePluginEditBusinessError(c, err)return}// 5. 返回JSON响应c.JSON(consts.StatusOK, resp)
}

处理步骤

  • 路由匹配PUT /api/plugin_api/update_plugin_meta
  • 参数绑定:将HTTP请求体绑定到 UpdatePluginMetaRequest 结构体
  • 参数验证:验证插件ID、版本号、锁定ID等必要参数
  • 上下文设置:将锁定ID设置到请求上下文中
  • 服务调用:调用插件服务的 UpdatePluginMeta 方法
  • 响应返回:返回JSON格式的编辑结果
2. 业务服务层(PluginApplicationService)

文件位置backend/application/plugin/plugin.go

func (p *PluginApplicationService) UpdatePluginMeta(ctx context.Context, req *pluginAPI.UpdatePluginMetaRequest) (resp *pluginAPI.UpdatePluginMetaResponse, err error) {// 1. 用户身份验证userID := ctxutil.GetUIDFromCtx(ctx)if userID == nil {return nil, errorx.New(errno.ErrPluginPermissionCode, errorx.KV(errno.PluginMsgKey, "session is required"))}// 2. 锁定ID和用户ID验证lockID := ctx.Value(consts.ContextKeyPluginLockID)if lockID == nil {return nil, errorx.New(errno.ErrPluginLockRequired, "lock ID is required")}// 3. 验证授权类型(如果编辑了授权信息)if req.AuthType != nil {_authType, ok := model.ToAuthType(req.GetAuthType())if !ok {return nil, fmt.Errorf("invalid auth type '%d'", req.GetAuthType())}}// 4. 验证授权子类型(如果编辑了)if req.SubAuthType != nil {_authSubType, ok := model.ToAuthSubType(req.GetSubAuthType())if !ok {return nil, fmt.Errorf("invalid sub authz type '%d'", req.GetSubAuthType())}}// 5. 构建编辑请求r := &service.UpdatePluginRequest{PluginID:   req.GetPluginID(),Version:    req.Version,SpaceID:    req.GetSpaceID(),EditorID:   *userID,LockID:     lockID.(string),IconURI:    req.Icon.URI,Name:       req.GetName(),Desc:       req.GetDesc(),ServerURL:  req.GetURL(),CommonParams: req.CommonParams,AuthInfo: &service.PluginAuthInfo{AuthzType:    req.AuthType,Location:     req.Location,Key:          req.Key,ServiceToken: req.ServiceToken,OAuthInfo:    req.OauthInfo,AuthzSubType: req.SubAuthType,AuthzPayload: req.AuthPayload,},}// 6. 执行编辑操作updatedPlugin, err := p.DomainSVC.UpdatePlugin(ctx, r)if err != nil {return nil, errorx.Wrapf(err, "UpdatePlugin failed")}// 7. 发布编辑事件err = p.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{OpType: searchEntity.Updated,Resource: &searchEntity.ResourceDocument{ResType:       resCommon.ResType_Plugin,ResSubType:    ptr.Of(int32(updatedPlugin.PluginType)),ResID:         updatedPlugin.ID,Name:          &updatedPlugin.Name,SpaceID:       &updatedPlugin.SpaceID,APPID:         updatedPlugin.ProjectID,OwnerID:       userID,PublishStatus: ptr.Of(resCommon.PublishStatus_UnPublished),UpdateTimeMS:  ptr.Of(time.Now().UnixMilli()),},})if err != nil {return nil, fmt.Errorf("publish resource '%d' failed, err=%v", updatedPlugin.ID, err)}// 8. 返回编辑结果resp = &pluginAPI.UpdatePluginMetaResponse{PluginID:   updatedPlugin.ID,Version:    updatedPlugin.Version,LockStatus: &pluginAPI.LockStatus{LockID:       lockID.(string),ExpireTime:   time.Now().Add(30 * time.Minute).Unix(),LastRenewal:  time.Now().Unix(),},}return resp, nil
}

核心功能

  • 身份验证:从上下文中提取用户ID,验证用户登录状态
  • 锁定验证:确保编辑请求携带有效的锁定ID
  • 参数验证:验证编辑参数的有效性,特别是授权相关字段
  • 权限检查:验证用户是否具有插件编辑权限
  • 数据构建:构建完整的插件编辑请求数据结构
  • 数据更新:调用领域服务更新数据库中的插件记录
  • 事件发布:发布插件编辑事件用于异步索引更新
  • 响应组装:构建标准化的编辑响应数据结构
3. 领域服务层(插件编辑领域服务)

核心功能

  • 锁定验证:验证编辑锁定的有效性和所有权
  • 版本检查:确保版本一致性,防止并发编辑冲突
  • 权限验证:验证用户是否具有插件编辑权限
  • 数据更新:更新数据库中的插件信息
  • 锁定续期:延长编辑锁定的有效期
// 验证插件编辑权限
func (s *PluginDomainService) validateEditPermission(ctx context.Context, userID int64, pluginID int64, spaceID int64) error {// 1. 检查插件是否存在plugin, err := s.pluginRepo.GetByID(ctx, pluginID)if err != nil {return fmt.Errorf("获取插件信息失败: %w", err)}if plugin == nil {return errorx.New(errno.ErrPluginRecordNotFound, errorx.KV("msg", "插件不存在"),errorx.KV("plugin_id", pluginID))}// 2. 检查用户是否有空间访问权限hasAccess, err := s.spaceRepo.CheckUserAccess(ctx, userID, spaceID)if err != nil {return fmt.Errorf("检查空间访问权限失败: %w", err)}if !hasAccess {return errorx.New(errno.ErrPluginPermissionCode, errorx.KV("msg", "用户无权限访问此空间"),errorx.KV("user_id", userID),errorx.KV("space_id", spaceID))}// 3. 检查用户角色权限userRole, err := s.userRepo.GetUserRole(ctx, userID, spaceID)if err != nil {return fmt.Errorf("获取用户角色失败: %w", err)}// 4. 检查是否为插件所有者或管理员if userID == plugin.DeveloperID || userRole >= model.RoleAdmin {return nil}return errorx.New(errno.ErrPluginPermissionCode,errorx.KV("msg", "用户无权限编辑此插件"),errorx.KV("user_id", userID),errorx.KV("plugin_id", pluginID),errorx.KV("required_role", model.RoleDeveloper))
}// 验证插件锁定状态
func (s *PluginDomainService) validatePluginLock(ctx context.Context, pluginID int64, lockID string, userID int64) error {// 1. 从Redis获取锁定信息lockInfo, err := s.lockService.GetLock(ctx, pluginID)if err != nil {return fmt.Errorf("获取锁定信息失败: %w", err)}// 2. 检查锁定是否存在if lockInfo == nil {return errorx.New(errno.ErrPluginLockNotExist,errorx.KV("msg", "插件未被锁定,无法编辑"),errorx.KV("plugin_id", pluginID))}// 3. 验证锁定IDif lockInfo.LockID != lockID {return errorx.New(errno.ErrPluginLockMismatch,errorx.KV("msg", "锁定ID不匹配"),errorx.KV("plugin_id", pluginID))}// 4. 验证锁定所有者if lockInfo.UserID != userID {return errorx.New(errno.ErrPluginLockOwnerMismatch,errorx.KV("msg", "不是锁定所有者"),errorx.KV("plugin_id", pluginID),errorx.KV("user_id", userID))}// 5. 检查锁定是否过期if time.Now().Unix() > lockInfo.ExpireTime {return errorx.New(errno.ErrPluginLockExpired,errorx.KV("msg", "锁定已过期"),errorx.KV("plugin_id", pluginID))}return nil
}// 验证插件版本一致性
func (s *PluginDomainService) validatePluginVersion(ctx context.Context, pluginID int64, requestedVersion string) error {// 1. 获取当前插件版本plugin, err := s.pluginRepo.GetByID(ctx, pluginID)if err != nil {return fmt.Errorf("获取插件信息失败: %w", err)}// 2. 验证版本一致性if plugin.Version != requestedVersion {return errorx.New(errno.ErrPluginVersionConflict,errorx.KV("msg", "版本冲突,请刷新后重试"),errorx.KV("plugin_id", pluginID),errorx.KV("client_version", requestedVersion),errorx.KV("server_version", plugin.Version))}return nil
}// 验证插件编辑参数
func (s *PluginDomainService) validateEditParams(ctx context.Context, req *UpdatePluginRequest) error {// 1. 验证插件IDif req.PluginID <= 0 {return errorx.New(errno.ErrPluginInvalidParamCode,errorx.KV("msg", "插件ID无效"),errorx.KV("field", "plugin_id"))}// 2. 验证版本号if req.Version == "" {return errorx.New(errno.ErrPluginInvalidParamCode,errorx.KV("msg", "版本号不能为空"),errorx.KV("field", "version"))}// 3. 验证锁定IDif req.LockID == "" {return errorx.New(errno.ErrPluginInvalidParamCode,errorx.KV("msg", "锁定ID不能为空"),errorx.KV("field", "lock_id"))}// 4. 验证编辑字段(如果提供了)if req.Name != "" && len(req.Name) > 100 {return errorx.New(errno.ErrPluginInvalidParamCode,errorx.KV("msg", "插件名称长度不能超过100个字符"),errorx.KV("field", "name"),errorx.KV("max_length", 100))}if req.Desc != "" && len(req.Desc) > 1000 {return errorx.New(errno.ErrPluginInvalidParamCode,errorx.KV("msg", "插件描述长度不能超过1000个字符"),errorx.KV("field", "desc"),errorx.KV("max_length", 1000))}// 5. 验证服务器URL(如果编辑了)if req.ServerURL != "" {if !isValidURL(req.ServerURL) {return errorx.New(errno.ErrPluginInvalidParamCode,errorx.KV("msg", "服务器URL格式无效"),errorx.KV("field", "server_url"),errorx.KV("value", req.ServerURL))}}return nil
}
4. 数据持久化层

MySQL数据库操作

  • 表名plugin_resourceplugin_auth
  • 操作类型:UPDATE操作
  • 事务处理:确保数据一致性
  • 乐观锁机制:使用版本号控制并发编辑
  • 锁定续期:编辑成功后延长锁定时间
// 插件编辑数据库操作
func (r *PluginRepository) Update(ctx context.Context, plugin *model.Plugin) error {// 更新插件主记录,使用乐观锁query := `UPDATE plugin_resource SET name = ?, description = ?, server_url = ?, common_params = ?, icon_uri = ?, version = ?, last_editor_id = ?, edit_count = edit_count + 1,updated_at = ?WHERE id = ? AND version = ?`now := time.Now()result, err := r.db.ExecContext(ctx, query, plugin.Name, plugin.Desc, plugin.ServerURL, plugin.CommonParams,plugin.IconURI, plugin.Version, plugin.LastEditorID,now, plugin.ID, plugin.OldVersion)if err != nil {return fmt.Errorf("更新插件失败: %w", err)}rowsAffected, err := result.RowsAffected()if err != nil {return fmt.Errorf("获取影响行数失败: %w", err)}if rowsAffected == 0 {return errorx.New(errno.ErrPluginVersionConflict, "版本冲突,插件已被其他用户修改")}plugin.UpdatedAt = nowreturn nil
}// 更新插件认证信息
func (r *PluginRepository) UpdateAuth(ctx context.Context, auth *model.PluginAuth) error {query := `UPDATE plugin_auth SET authz_type = ?, location = ?, auth_key = ?, service_token = ?,authz_sub_type = ?, authz_payload = ?, updated_at = ?WHERE plugin_id = ?`now := time.Now()_, err := r.db.ExecContext(ctx, query,auth.AuthzType, auth.Location, auth.Key, auth.ServiceToken,auth.AuthzSubType, auth.AuthzPayload, now, auth.PluginID)if err != nil {return fmt.Errorf("更新插件认证信息失败: %w", err)}auth.UpdatedAt = nowreturn nil
}
5. 事件发布层(EventPublisher)

编辑事件发布流程

// 插件编辑事件发布
func (p *EventPublisher) PublishPluginUpdatedEvent(ctx context.Context, event *entity.PluginUpdatedEvent) error {// 1. 构建编辑事件消息eventMsg := &message.PluginUpdatedMessage{PluginID:      event.PluginID,SpaceID:       event.SpaceID,EditorID:      event.EditorID,Name:          event.Name,PluginType:    event.PluginType,UpdatedAt:     event.UpdatedAt.Unix(),EventType:     "plugin_updated",ServerURL:     event.ServerURL,Description:   event.Description,Version:       event.Version,EditCount:     event.EditCount,}// 2. 序列化事件数据data, err := json.Marshal(eventMsg)if err != nil {return fmt.Errorf("序列化编辑事件失败: %w", err)}// 3. 发布到消息队列err = p.messageQueue.Publish(ctx, "plugin_update_events", data)if err != nil {return fmt.Errorf("发布编辑事件失败: %w", err)}logs.CtxInfof(ctx, "Published plugin updated event, pluginID=%d, editor=%d", event.PluginID, event.EditorID)return nil
}// 异步编辑事件处理器
func (h *PluginEventHandler) HandlePluginUpdatedEvent(ctx context.Context, event *entity.PluginUpdatedEvent) error {// 1. 更新ElasticSearch索引err := h.updateESIndex(ctx, event)if err != nil {logs.CtxErrorf(ctx, "Failed to update ES index: %v", err)return err}// 2. 更新缓存数据err = h.updateCache(ctx, event)if err != nil {logs.CtxWarnf(ctx, "Failed to update cache: %v", err)}// 3. 清除相关缓存err = h.invalidateRelatedCache(ctx, event.PluginID)if err != nil {logs.CtxWarnf(ctx, "Failed to invalidate cache: %v", err)}// 4. 记录编辑审计日志err = h.recordEditAudit(ctx, event)if err != nil {logs.CtxWarnf(ctx, "Failed to record audit log: %v", err)}// 5. 更新统计数据err = h.updateEditStatistics(ctx, event)if err != nil {logs.CtxWarnf(ctx, "Failed to update edit statistics: %v", err)}return nil
}// ElasticSearch索引更新
func (h *PluginEventHandler) updateESIndex(ctx context.Context, event *entity.PluginUpdatedEvent) error {// 构建ES文档更新doc := map[string]interface{}{"name":           event.Name,"description":    event.Description,"server_url":     event.ServerURL,"updated_at":     event.UpdatedAt,"last_editor_id": event.EditorID,"version":        event.Version,"edit_count":     event.EditCount,}// 更新ES索引err := h.esClient.Update(ctx, "coze_resource", fmt.Sprintf("%d", event.PluginID), doc)if err != nil {return fmt.Errorf("更新ES索引失败: %w", err)}logs.CtxInfof(ctx, "Updated plugin in ES index, pluginID=%d", event.PluginID)return nil
}

编辑事件处理内容

  • 索引更新:在ElasticSearch中更新插件文档索引
  • 缓存更新:更新和清除相关的缓存数据
  • 审计记录:记录插件编辑的审计日志
  • 统计更新:更新插件编辑相关的统计数据
6. 响应数据结构

UpdatePluginMetaResponse

type UpdatePluginMetaResponse struct {Code       int64        `json:"code"`        // 响应码Msg        string       `json:"msg"`         // 响应消息Success    bool         `json:"success"`     // 编辑是否成功PluginID   int64        `json:"plugin_id"`   // 插件IDVersion    string       `json:"version"`     // 更新后的版本号LockStatus *LockStatus  `json:"lock_status"` // 锁定状态信息BaseResp   *base.BaseResp `json:"base_resp"`  // 基础响应信息
}

UpdatePluginMetaRequest请求结构

type UpdatePluginMetaRequest struct {PluginID    int64       `json:"plugin_id" binding:"required"`   // 插件IDSpaceID     int64       `json:"space_id" binding:"required"`    // 工作空间IDName        string      `json:"name,omitempty"`                  // 插件名称Desc        string      `json:"desc,omitempty"`                  // 插件描述URL         string      `json:"url,omitempty"`                   // 服务器URLAuthType    *int32      `json:"auth_type,omitempty"`             // 认证类型SubAuthType *int32      `json:"sub_auth_type,omitempty"`         // 认证子类型Location    string      `json:"location,omitempty"`              // 参数位置Key         string      `json:"key,omitempty"`                   // 认证密钥ServiceToken string     `json:"service_token,omitempty"`         // 服务令牌Icon        *Icon       `json:"icon,omitempty"`                  // 插件图标CommonParams string     `json:"common_params,omitempty"`         // 通用参数OauthInfo   *OAuthInfo  `json:"oauth_info,omitempty"`            // OAuth信息AuthPayload string      `json:"auth_payload,omitempty"`          // 认证载荷Version     string      `json:"version" binding:"required"`     // 版本号LockID      string      `json:"lock_id" binding:"required"`     // 锁定ID
}

PluginUpdatedEvent事件结构

type PluginUpdatedEvent struct {PluginID      int64     `json:"plugin_id"`       // 插件IDSpaceID       int64     `json:"space_id"`        // 工作空间IDEditorID      int64     `json:"editor_id"`       // 编辑者IDName          string    `json:"name"`            // 插件名称Description   string    `json:"description"`     // 插件描述PluginType    int32     `json:"plugin_type"`     // 插件类型ServerURL     string    `json:"server_url"`      // 服务器URLUpdatedAt     time.Time `json:"updated_at"`      // 更新时间Version       string    `json:"version"`         // 版本号EditCount     int64     `json:"edit_count"`      // 编辑次数EventType     string    `json:"event_type"`      // 事件类型
}

响应内容说明

  • 成功响应:返回编辑成功状态、插件ID、更新后的版本号和锁定状态
  • 错误响应:返回具体的错误码和错误信息(如权限不足、锁定无效、版本冲突等)
  • 锁定状态:返回当前锁定的ID、过期时间和最后续期时间
  • 版本信息:返回更新后的版本号,用于后续编辑操作
http://www.dtcms.com/a/428110.html

相关文章:

  • Coze源码分析-资源库-编辑插件-后端源码-核心技术与总结
  • 如何安装TraeCN(字节跳动的IDE)
  • 泉州网站的建设医疗器械网
  • 中国数学外国人做视频网站重庆高端设计公司
  • JAVAweb案例之后端的增删改查
  • 建设主管部门网站南宁网站建设报价
  • Union 和 Optional 区别
  • 太原网站建设鸣蝉公司中建官网
  • Redis List 类型全解析
  • 服务器做jsp网站教程视频城市介绍网站模板
  • 做网站一定需要虚拟主机吗自建网站定位
  • CompletableFuture原理与实践----商品信息查询接口优化---信息组装
  • 深圳求职网站哪个好网站对接微信接口
  • Cause: java.sql.SQLException: 无效的列类型: 1111
  • IMU传感器价格与高精度组合惯导市场现状分析
  • (28)ASP.NET Core8.0 SOLID原则
  • API 接口开发与实时实时采集构建京东商品数据通道方案
  • 做网销的网站移动网站 模板
  • 某短视频 sig3 逆向纯算分析
  • CMSIS设计的理解
  • 串扰06-近端串扰的大小
  • 哪些网站是做食品网站前台设计方案
  • 达州市网站建设wordpress文章与页面
  • Powershell维护系统管理任务(五)
  • 美团滑块-[behavior] 加密分析
  • 泉州专业建站单位网站建设实施方案
  • 动作捕捉技术与服务 | 推动人形机器人实现规模化高质量数据采集与训练
  • 网站开发软件有哪设计师网络叫法
  • 服务类网站开发中山家居企业网站建设
  • 【Android】Kotlin.flow在主线程collect为什么不阻塞主线程?