package app import ( "encoding/json" "errors" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "strings" "time" "git.echol.cn/loser/st/server/global" "git.echol.cn/loser/st/server/model/app" "git.echol.cn/loser/st/server/model/app/request" "git.echol.cn/loser/st/server/model/app/response" "go.uber.org/zap" "gorm.io/datatypes" "gorm.io/gorm" ) type ExtensionService struct{} // CreateExtension 创建/安装扩展 func (es *ExtensionService) CreateExtension(userID uint, req *request.CreateExtensionRequest) (*app.AIExtension, error) { // 检查扩展是否已存在 var existing app.AIExtension err := global.GVA_DB.Where("user_id = ? AND name = ?", userID, req.Name).First(&existing).Error if err == nil { return nil, errors.New("扩展已存在") } if err != gorm.ErrRecordNotFound { return nil, err } // 序列化 JSON 字段 tagsJSON, _ := json.Marshal(req.Tags) dependenciesJSON, _ := json.Marshal(req.Dependencies) conflictsJSON, _ := json.Marshal(req.Conflicts) manifestJSON, _ := json.Marshal(req.ManifestData) assetsJSON, _ := json.Marshal(req.AssetsPaths) settingsJSON, _ := json.Marshal(req.Settings) optionsJSON, _ := json.Marshal(req.Options) metadataJSON, _ := json.Marshal(req.Metadata) extension := &app.AIExtension{ UserID: userID, Name: req.Name, DisplayName: req.DisplayName, Version: req.Version, Author: req.Author, Description: req.Description, Homepage: req.Homepage, Repository: req.Repository, License: req.License, Tags: datatypes.JSON(tagsJSON), ExtensionType: req.ExtensionType, Category: req.Category, Dependencies: datatypes.JSON(dependenciesJSON), Conflicts: datatypes.JSON(conflictsJSON), ManifestData: datatypes.JSON(manifestJSON), ScriptPath: req.ScriptPath, StylePath: req.StylePath, AssetsPaths: datatypes.JSON(assetsJSON), Settings: datatypes.JSON(settingsJSON), Options: datatypes.JSON(optionsJSON), IsEnabled: false, IsInstalled: true, IsSystemExt: false, InstallSource: req.InstallSource, SourceURL: req.SourceURL, Branch: req.Branch, AutoUpdate: req.AutoUpdate, InstallDate: time.Now(), Metadata: datatypes.JSON(metadataJSON), } if err := global.GVA_DB.Create(extension).Error; err != nil { return nil, err } global.GVA_LOG.Info("扩展安装成功", zap.Uint("extensionID", extension.ID), zap.String("name", extension.Name)) return extension, nil } // UpdateExtension 更新扩展 func (es *ExtensionService) UpdateExtension(userID, extensionID uint, req *request.UpdateExtensionRequest) error { var extension app.AIExtension if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil { return errors.New("扩展不存在") } // 系统内置扩展不允许修改 if extension.IsSystemExt { return errors.New("系统内置扩展不允许修改") } updates := map[string]interface{}{} if req.DisplayName != "" { updates["display_name"] = req.DisplayName } if req.Description != "" { updates["description"] = req.Description } if req.Settings != nil { settingsJSON, _ := json.Marshal(req.Settings) updates["settings"] = datatypes.JSON(settingsJSON) } if req.Options != nil { optionsJSON, _ := json.Marshal(req.Options) updates["options"] = datatypes.JSON(optionsJSON) } if req.Metadata != nil { metadataJSON, _ := json.Marshal(req.Metadata) updates["metadata"] = datatypes.JSON(metadataJSON) } if err := global.GVA_DB.Model(&extension).Updates(updates).Error; err != nil { return err } return nil } // DeleteExtension 删除/卸载扩展 func (es *ExtensionService) DeleteExtension(userID, extensionID uint, deleteFiles bool) error { var extension app.AIExtension if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil { return errors.New("扩展不存在") } // 系统内置扩展不允许删除 if extension.IsSystemExt { return errors.New("系统内置扩展不允许删除") } // TODO: 如果 deleteFiles=true,删除扩展文件 // 这需要文件系统支持 // 删除扩展(配置已经在扩展记录的 Settings 字段中,无需单独删除) if err := global.GVA_DB.Delete(&extension).Error; err != nil { return err } global.GVA_LOG.Info("扩展卸载成功", zap.Uint("extensionID", extensionID)) return nil } // GetExtension 获取扩展详情 func (es *ExtensionService) GetExtension(userID, extensionID uint) (*app.AIExtension, error) { var extension app.AIExtension if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil { return nil, errors.New("扩展不存在") } return &extension, nil } // GetExtensionList 获取扩展列表 func (es *ExtensionService) GetExtensionList(userID uint, req *request.ExtensionListRequest) (*response.ExtensionListResponse, error) { var extensions []app.AIExtension var total int64 db := global.GVA_DB.Model(&app.AIExtension{}).Where("user_id = ?", userID) // 过滤条件 if req.Name != "" { db = db.Where("name ILIKE ? OR display_name ILIKE ?", "%"+req.Name+"%", "%"+req.Name+"%") } if req.ExtensionType != "" { db = db.Where("extension_type = ?", req.ExtensionType) } if req.Category != "" { db = db.Where("category = ?", req.Category) } if req.IsEnabled != nil { db = db.Where("is_enabled = ?", *req.IsEnabled) } if req.IsInstalled != nil { db = db.Where("is_installed = ?", *req.IsInstalled) } if req.Tag != "" { db = db.Where("tags @> ?", fmt.Sprintf(`["%s"]`, req.Tag)) } // 统计总数 if err := db.Count(&total).Error; err != nil { return nil, err } // 分页查询 if err := db.Scopes(req.Paginate()).Order("created_at DESC").Find(&extensions).Error; err != nil { return nil, err } // 转换响应 result := make([]response.ExtensionResponse, 0, len(extensions)) for i := range extensions { result = append(result, response.ToExtensionResponse(&extensions[i])) } return &response.ExtensionListResponse{ List: result, Total: total, Page: req.Page, PageSize: req.PageSize, }, nil } // ToggleExtension 启用/禁用扩展 func (es *ExtensionService) ToggleExtension(userID, extensionID uint, isEnabled bool) error { var extension app.AIExtension if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil { return errors.New("扩展不存在") } // 检查依赖 if isEnabled { if err := es.checkDependencies(userID, &extension); err != nil { return err } } // 检查冲突 if isEnabled { if err := es.checkConflicts(userID, &extension); err != nil { return err } } updates := map[string]interface{}{ "is_enabled": isEnabled, } if isEnabled { updates["last_enabled"] = time.Now() } if err := global.GVA_DB.Model(&extension).Updates(updates).Error; err != nil { return err } global.GVA_LOG.Info("扩展状态更新", zap.Uint("extensionID", extensionID), zap.Bool("enabled", isEnabled)) return nil } // UpdateExtensionSettings 更新扩展配置 func (es *ExtensionService) UpdateExtensionSettings(userID, extensionID uint, settings map[string]interface{}) error { var extension app.AIExtension if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil { return errors.New("扩展不存在") } settingsJSON, err := json.Marshal(settings) if err != nil { return errors.New("序列化配置失败") } // 直接更新扩展表的 settings 字段 return global.GVA_DB.Model(&extension).Update("settings", datatypes.JSON(settingsJSON)).Error } // GetExtensionSettings 获取扩展配置 func (es *ExtensionService) GetExtensionSettings(userID, extensionID uint) (map[string]interface{}, error) { // 获取扩展信息 var extension app.AIExtension if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil { return nil, errors.New("扩展不存在") } // 从扩展的 Settings 字段读取用户配置 var settings map[string]interface{} if len(extension.Settings) > 0 { if err := json.Unmarshal([]byte(extension.Settings), &settings); err != nil { return nil, errors.New("解析配置失败: " + err.Error()) } } // 如果 ManifestData 中有默认配置,合并进来 if len(extension.ManifestData) > 0 { var manifest map[string]interface{} if err := json.Unmarshal([]byte(extension.ManifestData), &manifest); err == nil { if manifestSettings, ok := manifest["settings"].(map[string]interface{}); ok && manifestSettings != nil { // 只添加用户未设置的默认值 if settings == nil { settings = make(map[string]interface{}) } for k, v := range manifestSettings { if _, exists := settings[k]; !exists { settings[k] = v } } } } } if settings == nil { settings = make(map[string]interface{}) } return settings, nil } // UpdateExtensionStats 更新扩展统计 func (es *ExtensionService) UpdateExtensionStats(userID, extensionID uint, action string, value int) error { var extension app.AIExtension if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil { return errors.New("扩展不存在") } updates := map[string]interface{}{} switch action { case "usage": updates["usage_count"] = gorm.Expr("usage_count + ?", value) case "error": updates["error_count"] = gorm.Expr("error_count + ?", value) case "load": // 计算平均加载时间 newAvg := (extension.LoadTime*extension.UsageCount + value) / (extension.UsageCount + 1) updates["load_time"] = newAvg default: return errors.New("未知的统计类型") } return global.GVA_DB.Model(&extension).Updates(updates).Error } // GetExtensionManifest 获取扩展 manifest func (es *ExtensionService) GetExtensionManifest(userID, extensionID uint) (*response.ExtensionManifestResponse, error) { var extension app.AIExtension if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil { return nil, errors.New("扩展不存在") } var manifestData map[string]interface{} if extension.ManifestData != nil { _ = json.Unmarshal([]byte(extension.ManifestData), &manifestData) } // 从 manifestData 构建响应 manifest := &response.ExtensionManifestResponse{ Name: extension.Name, DisplayName: extension.DisplayName, Version: extension.Version, Description: extension.Description, Author: extension.Author, Homepage: extension.Homepage, Repository: extension.Repository, License: extension.License, Type: extension.ExtensionType, Category: extension.Category, Entry: extension.ScriptPath, Style: extension.StylePath, } // 解析数组和对象 if extension.Tags != nil { _ = json.Unmarshal([]byte(extension.Tags), &manifest.Tags) } if extension.Dependencies != nil { _ = json.Unmarshal([]byte(extension.Dependencies), &manifest.Dependencies) } if extension.Conflicts != nil { _ = json.Unmarshal([]byte(extension.Conflicts), &manifest.Conflicts) } if extension.AssetsPaths != nil { _ = json.Unmarshal([]byte(extension.AssetsPaths), &manifest.Assets) } if extension.Settings != nil { _ = json.Unmarshal([]byte(extension.Settings), &manifest.Settings) } if extension.Options != nil { _ = json.Unmarshal([]byte(extension.Options), &manifest.Options) } if extension.Metadata != nil { _ = json.Unmarshal([]byte(extension.Metadata), &manifest.Metadata) } return manifest, nil } // ImportExtension 导入扩展(从文件) func (es *ExtensionService) ImportExtension(userID uint, manifestData []byte) (*app.AIExtension, error) { // 解析 manifest.json var manifest app.AIExtensionManifest if err := json.Unmarshal(manifestData, &manifest); err != nil { return nil, errors.New("无效的 manifest.json 格式") } // 验证必填字段 if manifest.Name == "" || manifest.Version == "" { return nil, errors.New("manifest 缺少必填字段") } // 构建创建请求 req := &request.CreateExtensionRequest{ Name: manifest.Name, DisplayName: manifest.DisplayName, Version: manifest.Version, Author: manifest.Author, Description: manifest.Description, Homepage: manifest.Homepage, Repository: manifest.Repository, License: manifest.License, Tags: manifest.Tags, ExtensionType: manifest.Type, Category: manifest.Category, Dependencies: manifest.Dependencies, Conflicts: manifest.Conflicts, ScriptPath: manifest.Entry, StylePath: manifest.Style, AssetsPaths: manifest.Assets, Settings: manifest.Settings, Options: manifest.Options, InstallSource: "file", Metadata: manifest.Metadata, } // 将 manifest 原始数据也保存 var manifestMap map[string]interface{} _ = json.Unmarshal(manifestData, &manifestMap) req.ManifestData = manifestMap return es.CreateExtension(userID, req) } // ExportExtension 导出扩展 func (es *ExtensionService) ExportExtension(userID, extensionID uint) ([]byte, error) { manifest, err := es.GetExtensionManifest(userID, extensionID) if err != nil { return nil, err } return json.MarshalIndent(manifest, "", " ") } // checkDependencies 检查扩展依赖 func (es *ExtensionService) checkDependencies(userID uint, extension *app.AIExtension) error { if extension.Dependencies == nil || len(extension.Dependencies) == 0 { return nil } var dependencies map[string]string _ = json.Unmarshal([]byte(extension.Dependencies), &dependencies) for depName := range dependencies { var depExt app.AIExtension err := global.GVA_DB.Where("user_id = ? AND name = ? AND is_enabled = true", userID, depName).First(&depExt).Error if err != nil { return fmt.Errorf("缺少依赖扩展: %s", depName) } // TODO: 检查版本号是否满足要求 } return nil } // checkConflicts 检查扩展冲突 func (es *ExtensionService) checkConflicts(userID uint, extension *app.AIExtension) error { if extension.Conflicts == nil || len(extension.Conflicts) == 0 { return nil } var conflicts []string _ = json.Unmarshal([]byte(extension.Conflicts), &conflicts) for _, conflictName := range conflicts { var conflictExt app.AIExtension err := global.GVA_DB.Where("user_id = ? AND name = ? AND is_enabled = true", userID, conflictName).First(&conflictExt).Error if err == nil { return fmt.Errorf("扩展 %s 与 %s 冲突", extension.Name, conflictName) } } return nil } // GetEnabledExtensions 获取用户启用的所有扩展(用于前端加载) func (es *ExtensionService) GetEnabledExtensions(userID uint) ([]response.ExtensionResponse, error) { var extensions []app.AIExtension if err := global.GVA_DB.Where("user_id = ? AND is_enabled = true AND is_installed = true", userID). Order("created_at ASC").Find(&extensions).Error; err != nil { return nil, err } result := make([]response.ExtensionResponse, 0, len(extensions)) for i := range extensions { result = append(result, response.ToExtensionResponse(&extensions[i])) } return result, nil } // InstallExtensionFromURL 智能安装扩展(自动识别 Git URL 或 Manifest URL) func (es *ExtensionService) InstallExtensionFromURL(userID uint, url string, branch string) (*app.AIExtension, error) { global.GVA_LOG.Info("开始从 URL 安装扩展", zap.String("url", url), zap.String("branch", branch)) // 智能识别 URL 类型 if isGitURL(url) { global.GVA_LOG.Info("检测到 Git 仓库 URL,使用 Git 安装") if branch == "" { branch = "main" } return es.InstallExtensionFromGit(userID, url, branch) } // 否则作为 manifest.json URL 处理 global.GVA_LOG.Info("作为 Manifest URL 处理") return es.downloadAndInstallFromManifestURL(userID, url) } // isGitURL 判断是否为 Git 仓库 URL func isGitURL(url string) bool { // Git 仓库特征: // 1. 包含 .git 后缀 // 2. 包含常见的 Git 托管平台域名(github.com, gitlab.com, gitee.com 等) // 3. 不以 /manifest.json 或 .json 结尾 url = strings.ToLower(url) // 如果明确以 .json 结尾,不是 Git URL if strings.HasSuffix(url, ".json") { return false } // 如果包含 .git 后缀,是 Git URL if strings.HasSuffix(url, ".git") { return true } // 检查是否包含 Git 托管平台域名 gitHosts := []string{ "github.com", "gitlab.com", "gitee.com", "bitbucket.org", "gitea.io", "codeberg.org", } for _, host := range gitHosts { if strings.Contains(url, host) { // 如果包含 Git 平台且不是 raw 文件 URL,则认为是 Git 仓库 if !strings.Contains(url, "/raw/") && !strings.Contains(url, "/blob/") { return true } } } return false } // downloadAndInstallFromManifestURL 从 Manifest URL 下载并安装 func (es *ExtensionService) downloadAndInstallFromManifestURL(userID uint, manifestURL string) (*app.AIExtension, error) { // 创建 HTTP 客户端 client := &http.Client{ Timeout: 30 * time.Second, } // 下载 manifest.json resp, err := client.Get(manifestURL) if err != nil { return nil, fmt.Errorf("下载 manifest.json 失败: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("下载 manifest.json 失败: HTTP %d", resp.StatusCode) } // 读取响应内容 manifestData, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取 manifest.json 失败: %w", err) } // 解析 manifest var manifest app.AIExtensionManifest if err := json.Unmarshal(manifestData, &manifest); err != nil { return nil, fmt.Errorf("解析 manifest.json 失败: %w", err) } // 验证必填字段 if manifest.Name == "" { return nil, errors.New("manifest.json 缺少 name 字段") } // 检查扩展是否已存在 var existing app.AIExtension err = global.GVA_DB.Where("user_id = ? AND name = ?", userID, manifest.Name).First(&existing).Error if err == nil { return nil, fmt.Errorf("扩展 %s 已安装", manifest.Name) } if err != gorm.ErrRecordNotFound { return nil, err } // 将 manifest 转换为 map[string]interface{} var manifestMap map[string]interface{} if err := json.Unmarshal(manifestData, &manifestMap); err != nil { return nil, fmt.Errorf("转换 manifest 失败: %w", err) } // 构建创建请求 createReq := &request.CreateExtensionRequest{ Name: manifest.Name, DisplayName: manifest.DisplayName, Version: manifest.Version, Author: manifest.Author, Description: manifest.Description, Homepage: manifest.Homepage, Repository: manifest.Repository, // 使用 manifest 中的 repository License: manifest.License, Tags: manifest.Tags, ExtensionType: manifest.Type, Category: manifest.Category, Dependencies: manifest.Dependencies, Conflicts: manifest.Conflicts, ManifestData: manifestMap, ScriptPath: manifest.Entry, StylePath: manifest.Style, AssetsPaths: manifest.Assets, Settings: manifest.Settings, Options: manifest.Options, InstallSource: "url", SourceURL: manifestURL, // 记录原始 URL 用于更新 AutoUpdate: manifest.AutoUpdate, Metadata: nil, } // 确保扩展类型有效 if createReq.ExtensionType == "" { createReq.ExtensionType = "ui" } // 创建扩展 extension, err := es.CreateExtension(userID, createReq) if err != nil { return nil, fmt.Errorf("创建扩展失败: %w", err) } global.GVA_LOG.Info("从 URL 安装扩展成功", zap.Uint("extensionID", extension.ID), zap.String("name", extension.Name), zap.String("url", manifestURL)) return extension, nil } // UpgradeExtension 升级扩展版本(根据安装来源自动选择更新方式) func (es *ExtensionService) UpgradeExtension(userID, extensionID uint, force bool) (*app.AIExtension, error) { // 获取扩展信息 var extension app.AIExtension if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil { return nil, errors.New("扩展不存在") } global.GVA_LOG.Info("开始升级扩展", zap.Uint("extensionID", extensionID), zap.String("name", extension.Name), zap.String("installSource", extension.InstallSource), zap.String("sourceUrl", extension.SourceURL)) // 根据安装来源选择更新方式 switch extension.InstallSource { case "git": return es.updateExtensionFromGit(userID, &extension, force) case "url": return es.updateExtensionFromURL(userID, &extension) default: return nil, fmt.Errorf("不支持的安装来源: %s", extension.InstallSource) } } // updateExtensionFromGit 从 Git 仓库更新扩展 func (es *ExtensionService) updateExtensionFromGit(userID uint, extension *app.AIExtension, force bool) (*app.AIExtension, error) { if extension.SourceURL == "" { return nil, errors.New("缺少 Git 仓库 URL") } global.GVA_LOG.Info("从 Git 更新扩展", zap.String("name", extension.Name), zap.String("sourceUrl", extension.SourceURL), zap.String("branch", extension.Branch)) // 重新克隆(简单方式,避免处理本地修改) return es.InstallExtensionFromGit(userID, extension.SourceURL, extension.Branch) } // updateExtensionFromURL 从 URL 更新扩展(重新下载 manifest.json) func (es *ExtensionService) updateExtensionFromURL(userID uint, extension *app.AIExtension) (*app.AIExtension, error) { if extension.SourceURL == "" { return nil, errors.New("缺少 Manifest URL") } global.GVA_LOG.Info("从 URL 更新扩展", zap.String("name", extension.Name), zap.String("sourceUrl", extension.SourceURL)) // 重新下载并安装 return es.downloadAndInstallFromManifestURL(userID, extension.SourceURL) } // InstallExtensionFromGit 从 Git URL 安装扩展 func (es *ExtensionService) InstallExtensionFromGit(userID uint, gitUrl, branch string) (*app.AIExtension, error) { // 验证 Git URL if !strings.Contains(gitUrl, "://") && !strings.HasSuffix(gitUrl, ".git") { return nil, errors.New("无效的 Git URL") } // 创建临时目录 tempDir, err := os.MkdirTemp("", "extension-*") if err != nil { return nil, fmt.Errorf("创建临时目录失败: %w", err) } defer os.RemoveAll(tempDir) // 确保清理临时目录 global.GVA_LOG.Info("开始从 Git 克隆扩展", zap.String("gitUrl", gitUrl), zap.String("branch", branch), zap.String("tempDir", tempDir)) // 执行 git clone cmd := exec.Command("git", "clone", "--depth=1", "--branch="+branch, gitUrl, tempDir) output, err := cmd.CombinedOutput() if err != nil { global.GVA_LOG.Error("Git clone 失败", zap.String("gitUrl", gitUrl), zap.String("output", string(output)), zap.Error(err)) return nil, fmt.Errorf("Git clone 失败: %s", string(output)) } // 读取 manifest.json manifestPath := filepath.Join(tempDir, "manifest.json") manifestData, err := os.ReadFile(manifestPath) if err != nil { return nil, fmt.Errorf("读取 manifest.json 失败: %w", err) } // 解析 manifest var manifest app.AIExtensionManifest if err := json.Unmarshal(manifestData, &manifest); err != nil { return nil, fmt.Errorf("解析 manifest.json 失败: %w", err) } // 检查扩展是否已存在 var existing app.AIExtension err = global.GVA_DB.Where("user_id = ? AND name = ?", userID, manifest.Name).First(&existing).Error if err == nil { return nil, fmt.Errorf("扩展 %s 已安装", manifest.Name) } if err != gorm.ErrRecordNotFound { return nil, err } // 将 manifest 转换为 map[string]interface{} var manifestMap map[string]interface{} if err := json.Unmarshal(manifestData, &manifestMap); err != nil { return nil, fmt.Errorf("转换 manifest 失败: %w", err) } // 构建创建请求 createReq := &request.CreateExtensionRequest{ Name: manifest.Name, DisplayName: manifest.DisplayName, Version: manifest.Version, Author: manifest.Author, Description: manifest.Description, Homepage: manifest.Homepage, Repository: manifest.Repository, // 使用 manifest 中的 repository License: manifest.License, Tags: manifest.Tags, ExtensionType: manifest.Type, Category: manifest.Category, Dependencies: manifest.Dependencies, Conflicts: manifest.Conflicts, ManifestData: manifestMap, ScriptPath: manifest.Entry, StylePath: manifest.Style, AssetsPaths: manifest.Assets, Settings: manifest.Settings, Options: manifest.Options, InstallSource: "git", SourceURL: gitUrl, // 记录 Git URL 用于更新 Branch: branch, // 记录分支 AutoUpdate: manifest.AutoUpdate, Metadata: manifest.Metadata, } // 创建扩展记录 extension, err := es.CreateExtension(userID, createReq) if err != nil { return nil, fmt.Errorf("创建扩展记录失败: %w", err) } global.GVA_LOG.Info("从 Git 安装扩展成功", zap.Uint("extensionID", extension.ID), zap.String("name", extension.Name), zap.String("version", extension.Version)) return extension, nil }