upstream sources - mclucy/lucy GitHub Wiki
本文档系统说明 Lucy 的上游数据源抽象层(upstream abstraction layer),包括:
-
Provider接口及其能力边界 - routing 路由机制(显式 Source、自动回退、拓扑感知)
- 并行执行策略
- 现有 Provider 实现对比
- 新增自定义 Source 的完整操作清单
建议先阅读 架构总览 中的 Data Flow 与 Module Matrix,再阅读本页。
Lucy 通过统一的 Provider 接口屏蔽不同上游平台的差异。调用方(如 install、cmd)只依赖抽象接口,不直接引用具体 provider 包。Source 选择策略(平台感知、自动回退、显式指定)集中在 upstream/routing 子包中,与 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 |
将抽象版本标识(latest、compatible、any)解析为具体版本字符串 |
Source |
返回该 provider 对应的 types.Source 语义标识符 |
Provider 方法返回的是内部 Raw* 类型(如 RawSearchResults、RawPackageRemote),而非通用 types.* 类型。每个 Raw* 类型都需要实现对应的 To*(),由 upstream 包统一执行规范化。
// upstream.Fetch() 的规范化流程
raw, err := provider.Fetch(id) // provider 返回 Raw 类型
packageRemote = raw.ToPackageRemote() // 统一转换为 types.PackageRemote参考:upstream/upstream.go:30、upstream/upstream_types.go:52。
这是理解上游层设计的关键:
-
types.Source:稳定的语义标识符,面向用户与存储(例如 CLI 参数--source modrinth)。 -
Provider:运行时执行器,由 routing 根据 Source 选择,负责实际 API 调用。
两者是有意分离的:Source 可以是策略标记(如 SourceAuto、SourceUnknown);一个 Source 可以对应一个、多个或零个 Provider。
参考:upstream/upstream_types.go:1(接口注释)。
路由逻辑位于 upstream/routing/routing.go,负责将 (Platform, Source) 解析为有序 []Provider。
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。
当 source 未显式指定(SourceAuto)且 platform 为 PlatformAny 时,按以下顺序尝试:
var autoProviders = []upstream.Provider{
modrinth.Provider,
mcdr.Provider,
}参考:upstream/routing/routing.go:35。
ResolveProviders(platform, src) 的路由行为如下:
SourceUnknown -> 返回错误
SourceAuto + PlatformAny -> 返回 autoProviders
SourceAuto + PlatformForge/Fabric/Neoforge -> 返回 [modrinth.Provider]
SourceAuto + PlatformMCDR -> 返回 [mcdr.Provider]
显式 Source -> 从 providerBySource 查找
参考:upstream/routing/routing.go:68。
当运行时拓扑(RuntimeTopology)可用时,routing 根据节点 Capability 动态生成 Provider 列表,以支持混合服务器(如 Fabric + MCDR)的精准路由。
参考:upstream/routing/routing.go:92。
upstream/routing/routing_execution.go 提供三种并行模式:
并发对所有 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。
并发对所有 Provider 执行 Fetch,收集全部成功结果:
func FetchMany(providers []upstream.Provider, id types.PackageId) ([]types.PackageRemote, []ProviderError)参考:upstream/routing/routing_execution.go:94。
并发执行 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 | Source 标识 | 包路径 | 需要认证 | 完成度 | 备注 |
|---|---|---|---|---|---|
| Modrinth | SourceModrinth |
upstream/modrinth |
否(有频率限制) | 较完整 |
Search、Fetch、Information、Support 已实现;Dependencies panic |
| CurseForge | SourceCurseForge |
upstream/curseforge |
是(构建时注入) | 部分 |
Search、Fetch、Information 已实现;Dependencies、Support panic |
| MCDR | SourceMCDR |
upstream/mcdr |
否 | 部分 |
Search、Fetch、Information 已实现;Dependencies、Support 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 API 的 key 需要在 构建时 通过 ldflags 注入:
go build -ldflags "-X github.com/mclucy/lucy/upstream/curseforge.ApiKey=<YOUR_KEY>"实现要点:
- key 存储在包级变量
curseforge.ApiKey - 每次 HTTP 请求通过
x-api-keyheader 发送 - 若构建时未注入,运行时返回
ErrNoApiKey
参考:upstream/curseforge/curseforge_http.go:16。
Warning
根据上游文档 CurseForge REST API,API key 由开发者账户申请。Lucy 的构建脚本应在 CI/CD 中经由环境变量传入,而非硬编码到仓库。
以下以 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:46、upstream/routing/routing_execution.go:38、upstream/modrinth/modrinth.go:38。
新增一个上游数据源时,请按以下步骤执行。
在 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 包中定义
}实现全部 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 { /* 规范化 */ }在 upstream/routing/routing.go 的 providerBySource 中新增映射:
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:35、upstream/routing/routing.go:46。
types.Source 是用户可见的语义标识符,需要在 types 包中定义:
const SourceMyProvider Source = "myprovider"Warning
以下内容基于非完整代码推断,可能与实际预期行为存在偏差。
types.Source 常量的具体文件位置基于现有 provider 的 Source() 返回值推断(如 types.SourceModrinth、types.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 常量)