Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions client/src/pages/EnvironmentManagerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1473,7 +1473,7 @@ const EnvironmentManagerPage: React.FC = () => {
'已记录本地赞助者密钥,赞助高速源会在下载时尝试启用专用通道'
) : (
<>
Java环境安装现已支持赞助者专享国内高速服务器下载,您当前还不是赞助者,欢迎前往
赞助高速源可直接使用;配置本地赞助者密钥后会在下载时尝试启用专用通道,也可前往
<a
href="https://ifdian.net/a/xiaozhuhouses"
target="_blank"
Expand All @@ -1482,7 +1482,7 @@ const EnvironmentManagerPage: React.FC = () => {
>
爱发电
</a>
赞助
支持项目
</>
)}
</p>
Expand Down
57 changes: 46 additions & 11 deletions docs/Java赞助版下载链接说明.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 预览版不应作为新手默认路径,只能通过高级自定义入口主动选择。
38 changes: 33 additions & 5 deletions server/src/__tests__/java-download-artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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'
Expand Down
56 changes: 32 additions & 24 deletions server/src/utils/javaDownloadArtifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,8 @@ const JAVA_PROVIDERS: JavaCatalogProvider[] = [
{
id: 'sponsor',
label: '赞助高速源',
description: '项目提供的国内高速下载源,需要本地记录赞助者密钥',
description: '项目提供的国内高速下载源;本地赞助者密钥可启用专用下载会话',
source: 'download',
sponsorOnly: true,
supportedChannels: ['ga']
},
{
Expand Down Expand Up @@ -605,26 +604,13 @@ function buildDownloadOption(
provider: JavaCatalogProvider,
platform: string,
arch: string | undefined,
sponsorAvailable: boolean,
channel: JavaReleaseChannel,
recommended: boolean
): JavaCatalogOption {
if (!provider.supportedChannels?.includes(channel)) {
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)
Expand Down Expand Up @@ -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<JavaCatalogProviderId, number> = {
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,
Expand All @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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)
}
Expand Down