Add CurseForge API client with file upload and game endpoints
This commit is contained in:
parent
376bba5b75
commit
a52dd3915e
78
src/lib/curseforge/client.ts
Normal file
78
src/lib/curseforge/client.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/** CurseForge API 客户端,封装两套 API 的鉴权与 User-Agent。 */
|
||||
export class CurseForgeClient {
|
||||
/** GET /v1/mods/{modId}/files 等只读接口 */
|
||||
private apiBase = "https://api.curseforge.com";
|
||||
|
||||
/** 上传 / 更新文件、Game Version Types / Versions 等接口 */
|
||||
private uploadBase = "https://minecraft.curseforge.com";
|
||||
|
||||
private token: string;
|
||||
private userAgent: string;
|
||||
|
||||
constructor(
|
||||
token: string,
|
||||
userAgent?: string,
|
||||
) {
|
||||
this.token = token;
|
||||
this.userAgent =
|
||||
userAgent ??
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
|
||||
}
|
||||
|
||||
/** V1 API 请求头(x-api-key)。 */
|
||||
private apiHeaders(): Record<string, string> {
|
||||
return { "x-api-key": this.token };
|
||||
}
|
||||
|
||||
/** Upload API 请求头(X-Api-Token + browser User-Agent)。 */
|
||||
private uploadHeaders(): Record<string, string> {
|
||||
return {
|
||||
"X-Api-Token": this.token,
|
||||
"User-Agent": this.userAgent,
|
||||
};
|
||||
}
|
||||
|
||||
/** 发送 GET 请求到 V1 API(api.curseforge.com)并解析 JSON 响应体。 */
|
||||
async get<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${this.apiBase}${path}`, {
|
||||
headers: this.apiHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`CurseForge API GET ${path} failed: ${res.status} ${res.statusText}`,
|
||||
);
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
/** 发送 GET 请求到 Upload API(minecraft.curseforge.com)并解析 JSON 响应体。 */
|
||||
async uploadGet<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${this.uploadBase}${path}`, {
|
||||
headers: this.uploadHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`CurseForge Upload GET ${path} failed: ${res.status} ${res.statusText}`,
|
||||
);
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 POST 请求到 Upload API(minecraft.curseforge.com),body 为 FormData。
|
||||
* 不手动设置 Content-Type,让 fetch 自动生成含 boundary 的头。
|
||||
*/
|
||||
async uploadPost<T>(path: string, body: FormData): Promise<T> {
|
||||
const res = await fetch(`${this.uploadBase}${path}`, {
|
||||
method: "POST",
|
||||
headers: this.uploadHeaders(),
|
||||
body,
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`CurseForge Upload POST ${path} failed: ${res.status} ${res.statusText}`,
|
||||
);
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
}
|
||||
85
src/lib/curseforge/file.ts
Normal file
85
src/lib/curseforge/file.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { CurseForgeClient } from "./client";
|
||||
import {
|
||||
GetModFilesResponseSchema,
|
||||
UploadResponseSchema,
|
||||
type GetModFilesParams,
|
||||
type GetModFilesResponse,
|
||||
type UploadMetadata,
|
||||
type UpdateMetadata,
|
||||
type UploadResponse,
|
||||
} from "@/types";
|
||||
|
||||
/**
|
||||
* 获取指定项目的文件列表(分页)。
|
||||
* GET /v1/mods/{modId}/files
|
||||
*/
|
||||
export async function getFiles(
|
||||
client: CurseForgeClient,
|
||||
params: GetModFilesParams,
|
||||
): Promise<GetModFilesResponse> {
|
||||
const { modId, ...query } = params;
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value !== undefined) {
|
||||
searchParams.set(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
const qs = searchParams.toString();
|
||||
const path = `/v1/mods/${encodeURIComponent(modId)}/files${qs ? `?${qs}` : ""}`;
|
||||
const data = await client.get<unknown>(path);
|
||||
return GetModFilesResponseSchema.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到项目。
|
||||
* POST /api/projects/{projectId}/upload-file
|
||||
*
|
||||
* @param client CurseForge 客户端实例
|
||||
* @param projectId 项目 ID(数字)
|
||||
* @param metadata 已通过 Zod 校验的文件元数据
|
||||
* @param file 要上传的文件 Blob
|
||||
*/
|
||||
export async function uploadFile(
|
||||
client: CurseForgeClient,
|
||||
projectId: number,
|
||||
metadata: UploadMetadata,
|
||||
file: Blob,
|
||||
): Promise<UploadResponse> {
|
||||
const form = new FormData();
|
||||
form.append("metadata", JSON.stringify(metadata));
|
||||
|
||||
const filename =
|
||||
file instanceof File ? file.name : "upload.jar";
|
||||
form.append("file", file, filename);
|
||||
|
||||
const result = await client.uploadPost<unknown>(
|
||||
`/api/projects/${encodeURIComponent(projectId)}/upload-file`,
|
||||
form,
|
||||
);
|
||||
return UploadResponseSchema.parse(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新已上传文件的信息(不重新上传文件本身)。
|
||||
* POST /api/projects/{projectId}/update-file
|
||||
*
|
||||
* @param client CurseForge 客户端实例
|
||||
* @param projectId 项目 ID(数字)
|
||||
* @param metadata 已通过 Zod 校验的更新元数据
|
||||
*/
|
||||
export async function updateFile(
|
||||
client: CurseForgeClient,
|
||||
projectId: number,
|
||||
metadata: UpdateMetadata,
|
||||
): Promise<UploadResponse> {
|
||||
const form = new FormData();
|
||||
form.append("metadata", JSON.stringify(metadata));
|
||||
|
||||
const result = await client.uploadPost<unknown>(
|
||||
`/api/projects/${encodeURIComponent(projectId)}/update-file`,
|
||||
form,
|
||||
);
|
||||
return UploadResponseSchema.parse(result);
|
||||
}
|
||||
35
src/lib/curseforge/game.ts
Normal file
35
src/lib/curseforge/game.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { CurseForgeClient } from "./client";
|
||||
import {
|
||||
GameVersionTypeSchema,
|
||||
GameVersionSchema,
|
||||
type GameVersionType,
|
||||
type GameVersion,
|
||||
} from "@/types";
|
||||
|
||||
/**
|
||||
* 获取 CurseForge 支持的 Game Version Type 列表。
|
||||
* GET /api/game/version-types
|
||||
*/
|
||||
export async function getVersionTypes(
|
||||
client: CurseForgeClient,
|
||||
): Promise<GameVersionType[]> {
|
||||
const data = await client.uploadGet<unknown[]>(
|
||||
"/api/game/version-types",
|
||||
);
|
||||
return GameVersionTypeSchema.array().parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 CurseForge 支持的 Game Version 列表。
|
||||
* 包含 Minecraft 版本、Mod Loader、Environment、Java 等。
|
||||
* 通过 gameVersionTypeID 区分类型。
|
||||
* GET /api/game/versions
|
||||
*/
|
||||
export async function getGameVersions(
|
||||
client: CurseForgeClient,
|
||||
): Promise<GameVersion[]> {
|
||||
const data = await client.uploadGet<unknown[]>(
|
||||
"/api/game/versions",
|
||||
);
|
||||
return GameVersionSchema.array().parse(data);
|
||||
}
|
||||
3
src/lib/curseforge/index.ts
Normal file
3
src/lib/curseforge/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { CurseForgeClient } from "./client";
|
||||
export { getVersionTypes, getGameVersions } from "./game";
|
||||
export { getFiles, uploadFile, updateFile } from "./file";
|
||||
Loading…
Reference in New Issue
Block a user