From c3b700b1bedd04bff7d5394279cfd1c598a6a1c0 Mon Sep 17 00:00:00 2001
From: xiwangly2
Date: Fri, 2 Oct 2026 18:30:54 +0800
Subject: [PATCH] fix: allow sponsor source without local key
---
client/src/pages/EnvironmentManagerPage.tsx | 4 +-
...76\346\216\245\350\257\264\346\230\216.md" | 57 +++++++++++++++----
.../__tests__/java-download-artifacts.test.ts | 38 +++++++++++--
server/src/utils/javaDownloadArtifacts.ts | 56 ++++++++++--------
4 files changed, 113 insertions(+), 42 deletions(-)
diff --git a/client/src/pages/EnvironmentManagerPage.tsx b/client/src/pages/EnvironmentManagerPage.tsx
index e92f981..c2320dd 100644
--- a/client/src/pages/EnvironmentManagerPage.tsx
+++ b/client/src/pages/EnvironmentManagerPage.tsx
@@ -1473,7 +1473,7 @@ const EnvironmentManagerPage: React.FC = () => {
'已记录本地赞助者密钥,赞助高速源会在下载时尝试启用专用通道'
) : (
<>
- Java环境安装现已支持赞助者专享国内高速服务器下载,您当前还不是赞助者,欢迎前往
+ 赞助高速源可直接使用;配置本地赞助者密钥后会在下载时尝试启用专用通道,也可前往
{
>
爱发电
- 赞助
+ 支持项目
>
)}
diff --git "a/docs/Java\350\265\236\345\212\251\347\211\210\344\270\213\350\275\275\351\223\276\346\216\245\350\257\264\346\230\216.md" "b/docs/Java\350\265\236\345\212\251\347\211\210\344\270\213\350\275\275\351\223\276\346\216\245\350\257\264\346\230\216.md"
index d912be1..92dc0cc 100644
--- "a/docs/Java\350\265\236\345\212\251\347\211\210\344\270\213\350\275\275\351\223\276\346\216\245\350\257\264\346\230\216.md"
+++ "b/docs/Java\350\265\236\345\212\251\347\211\210\344\270\213\350\275\275\351\223\276\346\216\245\350\257\264\346\230\216.md"
@@ -1,12 +1,25 @@
-# Java赞助版下载链接说明
+# Java 环境下载链接与赞助者下载通道说明
-环境管理页面的 Java 8、11、17、21、25 下载地址已切换为国内高速短链,Windows、Linux x64 和 Linux ARM64 会根据服务端系统信息自动选择对应链接。
+环境管理页面的 Java 下载入口由服务端目录统一驱动,配置位于 `server/src/utils/javaDownloadArtifacts.ts`。前端只展示服务端返回的版本、提供商、通道和平台可用性,不再维护独立下载链接表。
-服务端会按照 Java 版本、操作系统和架构保留压缩包文件名。短链末尾不是压缩包扩展名,安装流程会使用对应的 `.zip` 或 `.tar.gz` 文件名进行解压。
+## 下载源
-下载式 Java 安装只适用于已有明确压缩包产物的架构。RISC-V64、macOS 和未列出的 Linux 架构不会回退到 Linux x64 包,前端会禁用安装按钮,服务端也会拒绝安装请求并提示改用系统 OpenJDK。
+当前目录包含以下提供商:
-RISC-V64 等特殊架构推荐使用发行版包管理器安装 Java,例如:
+| 提供商 | 说明 |
+| --- | --- |
+| 赞助高速源 | 项目维护的 `download.xiaozhuhouses.asia` 直链。未配置密钥时也可走普通通道下载;本地记录赞助者密钥后,会在安装时尝试建立专用下载会话。 |
+| Eclipse Temurin | 通过 Adoptium latest JDK API 解析 GA / EA 包。 |
+| Azul Zulu | 通过 Azul Metadata API 在安装时解析最新匹配的 Zulu JDK 包。 |
+| 系统包管理器 | Linux 上的 OpenJDK headless 包预设,适合 riscv64 等特殊架构。 |
+
+赞助高速源、Temurin 和 Azul 都由服务端按当前系统平台与架构判断是否可用。没有明确产物的组合不会回退到其它架构的包。
+
+## 版本与通道
+
+内置稳定版本包括 Java 8、11、17、21、25、27。自定义 Java 版本支持手动选择主版本、GA / EA 通道和下载提供商;默认使用 Java 25 GA,EA 预览版只在高级自定义入口中主动选择。
+
+RISC-V64 等特殊架构推荐优先使用发行版包管理器安装 Java,例如:
```bash
sudo apt-get update
@@ -16,11 +29,33 @@ sudo apt-get install -y default-jre-headless
实例未指定面板内 Java 版本时,会使用系统 `PATH` 中的 `java`。因此系统 OpenJDK 安装完成后,可以在实例中保持 Java 版本为空,或让启动命令直接使用 `java -jar ...`。
-本次更新的链接配置位于:
+## 赞助者下载通道
+
+赞助者通道与普通通道使用同一个文件直链,区别只在于是否携带由本地赞助者密钥换取的会话 Cookie。
+
+安装流程如下:
+
+1. 服务端解析 Java 版本、提供商、通道、操作系统和架构,得到最终下载地址与压缩包文件名。
+2. 如果下载地址属于 `download.xiaozhuhouses.asia`,且本地记录了赞助者密钥,则调用 `createSponsorDownloadSession()` 建立会话。
+3. 会话建立成功时,下载请求携带 Cookie,下载服务会调度到赞助者专用节点。
+4. 未记录密钥、密钥无效、会话服务不可用或会话建立失败时,安装会自动回退到同一直链的普通下载通道,不会中断安装。
+
+会话 Cookie 只会在 `isSponsorDownloadUrl()` 确认目标域名为 `download.xiaozhuhouses.asia` 时携带,避免泄露到第三方下载源。
+
+## 代码入口
+
+| 位置 | 说明 |
+| --- | --- |
+| `server/src/utils/javaDownloadArtifacts.ts` | Java 版本目录、提供商、平台架构可用性、赞助源链接和文件名映射。 |
+| `server/src/utils/sponsorDownload.ts` | 赞助者会话建立、下载域名校验。 |
+| `server/src/utils/sponsorStatus.ts` | 读取本地记录的赞助者密钥。 |
+| `server/src/routes/environment.ts` | `/java/catalog` 返回下载目录;`/java/install` 解析 provider/version/channel 并组装下载选项。 |
+| `server/src/modules/environment/javaManager.ts` | 下载、保存、解压 Java 安装包,并支持透传会话 Cookie。 |
+| `client/src/pages/EnvironmentManagerPage.tsx` | 展示服务端目录、预设卡片、自定义版本入口和安装进度。 |
-- `client/src/pages/EnvironmentManagerPage.tsx`:页面 Java 环境下载配置。
-- `server/src/routes/environment.ts`:赞助者专用下载地址和压缩包文件名映射。
-- `server/src/utils/javaDownloadArtifacts.ts`:下载式 Java 支持架构、赞助链接和压缩包文件名映射。
-- `server/src/modules/environment/javaManager.ts`:使用映射后的文件名保存并解压下载文件。
+## 验证重点
-Node.js ARM64 下载链接不属于 Java 环境管理页面,本次未纳入 Java 下载配置。
+- 无赞助者密钥时,赞助高速源在支持的平台上仍应可安装。
+- 有赞助者密钥时,同一直链会尝试携带会话 Cookie;失败时应自动回退普通通道。
+- riscv64 不应误用 Linux x64 包;没有赞助源产物时,应落到 Temurin 或系统包管理器等可用选项。
+- EA 预览版不应作为新手默认路径,只能通过高级自定义入口主动选择。
diff --git a/server/src/__tests__/java-download-artifacts.test.ts b/server/src/__tests__/java-download-artifacts.test.ts
index 293663b..70cb4fd 100644
--- a/server/src/__tests__/java-download-artifacts.test.ts
+++ b/server/src/__tests__/java-download-artifacts.test.ts
@@ -64,15 +64,39 @@ describe('java download artifacts', () => {
)
})
- it('keeps sponsor downloads behind sponsor availability', async () => {
- await expect(resolveJavaDownloadOption('java17', 'sponsor', 'linux', 'x64')).rejects.toThrow(/赞助高速源/)
+ it('resolves sponsor downloads without requiring sponsor credentials', async () => {
+ const resolution = await resolveJavaDownloadOption('java17', 'sponsor', 'linux', 'x64')
- const resolution = await resolveJavaDownloadOption('java17', 'sponsor', 'linux', 'x64', {
+ expect(resolution.downloadUrl).toBe(getSponsorDownloadUrl('java17', 'linux', 'x64'))
+ expect(resolution.archiveFileName).toBe(getJavaArchiveFileName('java17', 'linux', 'x64'))
+ })
+
+ it('keeps the first catalog option installable on supported platforms without sponsor credentials', () => {
+ const catalog = getJavaDownloadCatalog('linux', 'x64')
+ const java17Options = catalog.options.filter(option => option.version === 'java17')
+ const sponsorOption = java17Options.find(option => option.provider === 'sponsor')
+
+ expect(java17Options[0]).toEqual(expect.objectContaining({
+ provider: 'sponsor',
+ available: true,
+ recommended: true
+ }))
+ expect(sponsorOption).toEqual(expect.objectContaining({
+ available: true
+ }))
+ })
+
+ it('keeps sponsor first when sponsor credentials are available', () => {
+ const catalog = getJavaDownloadCatalog('linux', 'x64', {
sponsorAvailable: true
})
+ const java17Options = catalog.options.filter(option => option.version === 'java17')
- expect(resolution.downloadUrl).toBe(getSponsorDownloadUrl('java17', 'linux', 'x64'))
- expect(resolution.archiveFileName).toBe(getJavaArchiveFileName('java17', 'linux', 'x64'))
+ expect(java17Options[0]).toEqual(expect.objectContaining({
+ provider: 'sponsor',
+ available: true,
+ recommended: true
+ }))
})
it('keeps sponsor unavailable on riscv64 while offering Temurin and system packages where valid', () => {
@@ -97,6 +121,10 @@ describe('java download artifacts', () => {
expect(java17Options.find(option => option.provider === 'sponsor')).toEqual(expect.objectContaining({
available: false
}))
+ expect(java17Options[0]).toEqual(expect.objectContaining({
+ provider: 'adoptium',
+ available: true
+ }))
expect(java17Options.find(option => option.provider === 'adoptium')).toEqual(expect.objectContaining({
available: true,
downloadUrl: 'https://api.adoptium.net/v3/binary/latest/17/ga/linux/riscv64/jdk/hotspot/normal/eclipse'
diff --git a/server/src/utils/javaDownloadArtifacts.ts b/server/src/utils/javaDownloadArtifacts.ts
index e299724..08e0f70 100644
--- a/server/src/utils/javaDownloadArtifacts.ts
+++ b/server/src/utils/javaDownloadArtifacts.ts
@@ -234,9 +234,8 @@ const JAVA_PROVIDERS: JavaCatalogProvider[] = [
{
id: 'sponsor',
label: '赞助高速源',
- description: '项目提供的国内高速下载源,需要本地记录赞助者密钥',
+ description: '项目提供的国内高速下载源;本地赞助者密钥可启用专用下载会话',
source: 'download',
- sponsorOnly: true,
supportedChannels: ['ga']
},
{
@@ -605,7 +604,6 @@ function buildDownloadOption(
provider: JavaCatalogProvider,
platform: string,
arch: string | undefined,
- sponsorAvailable: boolean,
channel: JavaReleaseChannel,
recommended: boolean
): JavaCatalogOption {
@@ -613,18 +611,6 @@ function buildDownloadOption(
return buildUnavailableDownloadOption(version, provider, platform, arch, channel, recommended, `${provider.label} 不支持 ${getReleaseChannelLabel(channel)} 通道`)
}
- if (provider.id === 'sponsor' && !sponsorAvailable) {
- return buildUnavailableDownloadOption(
- version,
- provider,
- platform,
- arch,
- channel,
- recommended,
- '赞助高速源需要先配置本地赞助者密钥'
- )
- }
-
if (provider.id === 'azul') {
try {
getAzulPlatform(platform, arch)
@@ -719,6 +705,30 @@ function buildSystemPackageOption(
}
}
+function getOptionPriority(option: JavaCatalogOption): number {
+ if (option.available && option.source === 'download' && option.recommended) return 0
+ if (option.available && option.source === 'download') return 1
+ if (option.available && option.source === 'package-manager' && option.recommended) return 2
+ if (option.available) return 3
+ return 4
+}
+
+const PROVIDER_SORT_ORDER: Record = {
+ sponsor: 0,
+ adoptium: 1,
+ azul: 2,
+ system: 3
+}
+
+function sortJavaOptions(options: JavaCatalogOption[]): JavaCatalogOption[] {
+ return options.sort((left, right) => {
+ const priorityDiff = getOptionPriority(left) - getOptionPriority(right)
+ if (priorityDiff !== 0) return priorityDiff
+
+ return PROVIDER_SORT_ORDER[left.provider] - PROVIDER_SORT_ORDER[right.provider]
+ })
+}
+
export function getJavaDownloadCatalog(
platform: string,
arch?: string,
@@ -740,16 +750,19 @@ export function getJavaDownloadCatalog(
for (const provider of JAVA_PROVIDERS.filter(item => item.source === 'download')) {
for (const channel of version.channels) {
const recommended = (
- (provider.id === 'sponsor' && sponsorAvailable && channel === version.defaultChannel) ||
- (provider.id === 'adoptium' && !sponsorAvailable && channel === version.defaultChannel)
+ provider.id === 'sponsor' && channel === version.defaultChannel
)
- catalogOptions.push(buildDownloadOption(version, provider, platform, arch, sponsorAvailable, channel, recommended))
+ catalogOptions.push(buildDownloadOption(version, provider, platform, arch, channel, recommended))
}
}
catalogOptions.push(buildSystemPackageOption(version, platform, arch, isRiscv64))
}
+ const sortedOptions = JAVA_VERSIONS.flatMap(version => (
+ sortJavaOptions(catalogOptions.filter(option => option.version === version.id))
+ ))
+
return {
platform,
arch: arch || 'unknown',
@@ -758,7 +771,7 @@ export function getJavaDownloadCatalog(
providers: JAVA_PROVIDERS,
presets: JAVA_PRESETS,
versions: JAVA_VERSIONS,
- options: catalogOptions,
+ options: sortedOptions,
custom: {
defaultMajor: 25,
defaultChannel: 'ga',
@@ -789,17 +802,12 @@ export async function resolveJavaDownloadOption(
throw new UnsupportedJavaDownloadError('赞助高速源仅支持项目维护的预设版本')
}
- if (!options.sponsorAvailable) {
- throw new UnsupportedJavaDownloadError('赞助高速源需要先配置本地赞助者密钥')
- }
-
return {
version: versionRequest.id,
major: versionRequest.major,
releaseChannel: versionRequest.defaultChannel,
provider,
providerLabel: providerDefinition.label,
- sponsorOnly: true,
downloadUrl: getSponsorDownloadUrl(versionRequest.id, platform, arch),
archiveFileName: getJavaArchiveFileName(versionRequest.id, platform, arch)
}