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