upstream sources - mclucy/lucy GitHub Wiki

上游数据源(Upstream Sources)

本文档系统说明 Lucy 的上游数据源抽象层(upstream abstraction layer),包括:

  • Provider 接口及其能力边界
  • routing 路由机制(显式 Source、自动回退、拓扑感知)
  • 并行执行策略
  • 现有 Provider 实现对比
  • 新增自定义 Source 的完整操作清单

建议先阅读 架构总览 中的 Data Flow 与 Module Matrix,再阅读本页。

Lucy 通过统一的 Provider 接口屏蔽不同上游平台的差异。调用方(如 install、cmd)只依赖抽象接口,不直接引用具体 provider 包。Source 选择策略(平台感知、自动回退、显式指定)集中在 upstream/routing 子包中,与 Provider 实现解耦。

关键要点

核心概念

Provider 接口(能力边界)

Provider 定义于 upstream/upstream_types.go,是 Lucy 与外部数据源之间的能力边界(capability boundary)。所有 provider 都必须实现以下 7 个方法:

type Provider interface {
    Search(query string, options types.SearchOptions) (RawSearchResults, error)
    Fetch(id types.PackageId) (RawPackageRemote, error)
    Information(name types.ProjectName) (RawProjectInformation, error)
    Dependencies(id types.PackageId) (RawPackageDependencies, error)
    Support(name types.ProjectName) (RawProjectSupport, error)
    ParseAmbiguousVersion(id types.PackageId) (types.PackageId, error)
    Source() types.Source
}

参考:upstream/upstream_types.go:21

方法职责
方法 职责
Search 按关键词查询项目列表,支持平台过滤(SearchOptions
Fetch PackageId(项目名 + 版本)拉取发布包下载元数据
Information 按项目名获取项目级元数据(描述、链接、平台支持等)
Dependencies 获取指定包的依赖树(大多数 provider 尚未实现)
Support 查询项目对各平台的支持状态
ParseAmbiguousVersion 将抽象版本标识(latestcompatibleany)解析为具体版本字符串
Source 返回该 provider 对应的 types.Source 语义标识符

Raw 类型与规范化(Normalization)

Provider 方法返回的是内部 Raw* 类型(如 RawSearchResultsRawPackageRemote),而非通用 types.* 类型。每个 Raw* 类型都需要实现对应的 To*(),由 upstream 包统一执行规范化。

// upstream.Fetch() 的规范化流程
raw, err := provider.Fetch(id)         // provider 返回 Raw 类型
packageRemote = raw.ToPackageRemote()  // 统一转换为 types.PackageRemote

参考:upstream/upstream.go:30upstream/upstream_types.go:52

types.SourceProvider 的区别

这是理解上游层设计的关键:

  • types.Source:稳定的语义标识符,面向用户与存储(例如 CLI 参数 --source modrinth)。
  • Provider:运行时执行器,由 routing 根据 Source 选择,负责实际 API 调用。

两者是有意分离的:Source 可以是策略标记(如 SourceAutoSourceUnknown);一个 Source 可以对应一个、多个或零个 Provider。

参考:upstream/upstream_types.go:1(接口注释)。

Routing 路由机制

路由逻辑位于 upstream/routing/routing.go,负责将 (Platform, Source) 解析为有序 []Provider

providerBySource:显式 Source 映射

var providerBySource = map[types.Source]upstream.Provider{
    types.SourceCurseForge: curseforge.Provider,
    types.SourceModrinth:   modrinth.Provider,
    types.SourceGitHub:     githubsource.Provider,
    types.SourceMCDR:       mcdr.Provider,
}

新增 provider 时必须在此注册。参考:upstream/routing/routing.go:46

autoProvidersSourceAuto 回退列表

当 source 未显式指定(SourceAuto)且 platform 为 PlatformAny 时,按以下顺序尝试:

var autoProviders = []upstream.Provider{
    modrinth.Provider,
    mcdr.Provider,
}

参考:upstream/routing/routing.go:35

ResolveProviders(platform, src):决策规则

ResolveProviders(platform, src) 的路由行为如下:

SourceUnknown                           -> 返回错误
SourceAuto + PlatformAny               -> 返回 autoProviders
SourceAuto + PlatformForge/Fabric/Neoforge -> 返回 [modrinth.Provider]
SourceAuto + PlatformMCDR              -> 返回 [mcdr.Provider]
显式 Source                             -> 从 providerBySource 查找

参考:upstream/routing/routing.go:68

ResolveProvidersFromTopology():拓扑感知路由

当运行时拓扑(RuntimeTopology)可用时,routing 根据节点 Capability 动态生成 Provider 列表,以支持混合服务器(如 Fabric + MCDR)的精准路由。

参考:upstream/routing/routing.go:92

并行执行策略

upstream/routing/routing_execution.go 提供三种并行模式:

SearchMany:全量并行搜索

并发对所有 Provider 执行 Search,返回各 Provider 独立结果(不合并):

func SearchMany(providers []upstream.Provider, query types.ProjectName, options types.SearchOptions) ([]types.SearchResults, []ProviderError)

实现采用 sync.WaitGroup + slot 数组,返回顺序由 slot 下标保证。

参考:upstream/routing/routing_execution.go:38

FetchMany:全量并行拉取

并发对所有 Provider 执行 Fetch,收集全部成功结果:

func FetchMany(providers []upstream.Provider, id types.PackageId) ([]types.PackageRemote, []ProviderError)

参考:upstream/routing/routing_execution.go:94

FirstInfo:优先返回(channel-based)

并发执行 Information + Fetch,返回最先成功结果:

func FirstInfo(providers []upstream.Provider, id types.PackageId) (InfoResult, []ProviderError, error)

参考:upstream/routing/routing_execution.go:160

Warning

此功能尚未完整/正确实现 FirstFetch 的优先返回能力尚未实现,当前调用会直接触发 panic("not implemented")。参考:upstream/routing/routing_execution.go:155

现有 Provider 对比

Provider Source 标识 包路径 需要认证 完成度 备注
Modrinth SourceModrinth upstream/modrinth 否(有频率限制) 较完整 SearchFetchInformationSupport 已实现;Dependencies panic
CurseForge SourceCurseForge upstream/curseforge 是(构建时注入) 部分 SearchFetchInformation 已实现;DependenciesSupport panic
MCDR SourceMCDR upstream/mcdr 部分 SearchFetchInformation 已实现;DependenciesSupport panic
GitHub SourceGitHub upstream/githubsource 可选 骨架 所有方法均为 panic(TODO 注释)
Mojang —(内部) upstream/mojang 极早期 仅包含版本清单 URL 常量,无 Provider 实现

Warning

以下内容基于非完整代码推断,可能与实际预期行为存在偏差。

Mojang 当前未实现 Provider 接口,且未注册到 providerBySource,推测仅用于 vanilla server 版本元数据的内部查询,不经过标准 routing。参考:upstream/mojang/mojang.go:1

CurseForge 认证机制

CurseForge API 的 key 需要在 构建时 通过 ldflags 注入:

go build -ldflags "-X github.com/mclucy/lucy/upstream/curseforge.ApiKey=<YOUR_KEY>"

实现要点:

  • key 存储在包级变量 curseforge.ApiKey
  • 每次 HTTP 请求通过 x-api-key header 发送
  • 若构建时未注入,运行时返回 ErrNoApiKey

参考:upstream/curseforge/curseforge_http.go:16

Warning

根据上游文档 CurseForge REST API,API key 由开发者账户申请。Lucy 的构建脚本应在 CI/CD 中经由环境变量传入,而非硬编码到仓库。

端到端示例:register -> route -> call

以下以 Modrinth Search 为例说明完整调用链:

1) 用户执行: lucy search sodium --source modrinth
   |
2) cmd/cmd_search.go 解析 Source = types.SourceModrinth
   |
3) routing.ResolveProviders(platform, SourceModrinth)
   |  从 providerBySource 取出 modrinth.Provider
   |  返回 []Provider{modrinth.Provider}
   |
4) routing.SearchMany([]Provider{modrinth.Provider}, "sodium", options)
   |  goroutine: upstream.Search(modrinth.Provider, "sodium", options)
   |    -> modrinth.Provider.Search("sodium", options)
   |      -> HTTP GET https://api.modrinth.com/v2/search?query=sodium&...
   |      -> 解析响应为 *searchResultResponse(实现 RawSearchResults)
   |    -> raw.ToSearchResults()(规范化为 types.SearchResults)
   |
5) 结果返回 cmd 层并格式化输出

参考:upstream/routing/routing.go:46upstream/routing/routing_execution.go:38upstream/modrinth/modrinth.go:38

新增 Source 操作清单

新增一个上游数据源时,请按以下步骤执行。

创建 provider 包

upstream/ 下创建子目录(例如 upstream/myprovider/),定义 provider 类型并导出包级 Provider 变量:

package myprovider

import "github.com/mclucy/lucy/types"
import "github.com/mclucy/lucy/upstream"

type provider struct{}

var Provider provider

func (provider) Source() types.Source {
    return types.SourceMyProvider // 需先在 types 包中定义
}

实现 upstream.Provider 接口

实现全部 7 个方法。若某方法暂不支持,可保留 panic("TODO: implement ...");但 Source()Search()Fetch()Information() 建议优先实现。

func (provider) Search(query string, options types.SearchOptions) (upstream.RawSearchResults, error) {
    // 调用第三方 API,返回实现 RawSearchResults 的内部类型
}

func (p provider) Fetch(id types.PackageId) (upstream.RawPackageRemote, error) {
    id, err = p.ParseAmbiguousVersion(id)
    // ...
}

// Information, Dependencies, Support, ParseAmbiguousVersion...

每个返回类型都应是内部结构体,并实现对应 To*() 方法,例如:

type mySearchResult struct { /* ... */ }

func (r mySearchResult) ToSearchResults() types.SearchResults { /* 规范化 */ }

routing.go 注册

upstream/routing/routing.goproviderBySource 中新增映射:

import "github.com/mclucy/lucy/upstream/myprovider"

var providerBySource = map[types.Source]upstream.Provider{
    // ... existing entries ...
    types.SourceMyProvider: myprovider.Provider,
}

若需加入 SourceAuto 默认候选列表,还应追加至 autoProviders

var autoProviders = []upstream.Provider{
    modrinth.Provider,
    mcdr.Provider,
    myprovider.Provider, // 按优先级排序
}

参考:upstream/routing/routing.go:35upstream/routing/routing.go:46

types 包新增 Source 常量

types.Source 是用户可见的语义标识符,需要在 types 包中定义:

const SourceMyProvider Source = "myprovider"

Warning

以下内容基于非完整代码推断,可能与实际预期行为存在偏差。

types.Source 常量的具体文件位置基于现有 provider 的 Source() 返回值推断(如 types.SourceModrinthtypes.SourceCurseForge)。请以 types/ 目录中的实际实现为准。

处理认证(如需要)

若上游 API 需要认证,可参考 CurseForge 模式,通过 ldflags 注入:

// myprovider/http.go
var ApiKey string // 由构建时 ldflags 注入

// build command:
// go build -ldflags "-X github.com/mclucy/lucy/upstream/myprovider.ApiKey=<KEY>"

应在 CI/CD 中通过 secrets 注入,不要将真实 key 提交到仓库。

参考:upstream/curseforge/curseforge_http.go:16

相关页面

参考文件

  • upstream/upstream_types.go:21 - Provider 接口定义
  • upstream/upstream_types.go:52 - Raw* 接口定义(规范化契约)
  • upstream/upstream.go:30 - upstream.Fetch() 规范化封装
  • upstream/upstream.go:70 - upstream.Search() 规范化封装
  • upstream/routing/routing.go:35 - autoProviders 默认列表
  • upstream/routing/routing.go:46 - providerBySource 显式映射
  • upstream/routing/routing.go:61 - GetProvider() 公开查询函数
  • upstream/routing/routing.go:68 - ResolveProviders() 路由决策
  • upstream/routing/routing.go:92 - ResolveProvidersFromTopology() 拓扑感知路由
  • upstream/routing/routing_execution.go:38 - SearchMany() 并行搜索
  • upstream/routing/routing_execution.go:94 - FetchMany() 并行拉取
  • upstream/routing/routing_execution.go:149 - FirstFetch() 未实现(panic)
  • upstream/routing/routing_execution.go:160 - FirstInfo() channel-based 优先返回
  • upstream/modrinth/modrinth.go:26 - Modrinth provider 实现示例(最完整)
  • upstream/curseforge/curseforge.go:16 - CurseForge provider 实现
  • upstream/curseforge/curseforge_http.go:16 - CurseForge API key ldflags 注入
  • upstream/mcdr/mcdr.go:14 - MCDR provider 实现
  • upstream/githubsource/githubsource.go:8 - GitHub provider(骨架,均为 TODO)
  • upstream/mojang/mojang.go:1 - Mojang(仅含版本清单 URL 常量)
⚠️ **GitHub.com Fallback** ⚠️