Add CurseForge API client with file upload and game endpoints

This commit is contained in:
CPTProgrammer 2026-07-06 22:16:32 +08:00
parent 376bba5b75
commit a52dd3915e
No known key found for this signature in database
4 changed files with 201 additions and 0 deletions

View 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 APIapi.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 APIminecraft.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 APIminecraft.curseforge.combody 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>;
}
}

View 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);
}

View 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 LoaderEnvironmentJava
* 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);
}

View File

@ -0,0 +1,3 @@
export { CurseForgeClient } from "./client";
export { getVersionTypes, getGameVersions } from "./game";
export { getFiles, uploadFile, updateFile } from "./file";