Add Modrinth API client with version and tag endpoints
This commit is contained in:
parent
cb05223145
commit
376bba5b75
50
src/lib/modrinth/client.ts
Normal file
50
src/lib/modrinth/client.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/** Modrinth API v2 的基础请求客户端,封装鉴权与 User-Agent。 */
|
||||
export class ModrinthClient {
|
||||
private base = "https://api.modrinth.com/v2";
|
||||
private token: string;
|
||||
private userAgent: string;
|
||||
|
||||
constructor(token: string, userAgent?: string) {
|
||||
this.token = token;
|
||||
this.userAgent = userAgent ?? "";
|
||||
}
|
||||
|
||||
/** 构造通用请求头(Authorization + User-Agent)。 */
|
||||
private headers(): Record<string, string> {
|
||||
return {
|
||||
Authorization: this.token,
|
||||
"User-Agent": this.userAgent,
|
||||
};
|
||||
}
|
||||
|
||||
/** 发送 GET 请求并解析 JSON 响应体。 */
|
||||
async get<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${this.base}${path}`, {
|
||||
headers: this.headers(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Modrinth API GET ${path} failed: ${res.status} ${res.statusText}`,
|
||||
);
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 POST 请求,body 为 FormData(multipart/form-data)。
|
||||
* 不手动设置 Content-Type,让 fetch 自动生成含 boundary 的头。
|
||||
*/
|
||||
async post<T>(path: string, body: FormData): Promise<T> {
|
||||
const res = await fetch(`${this.base}${path}`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body,
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Modrinth API POST ${path} failed: ${res.status} ${res.statusText}`,
|
||||
);
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
}
|
||||
9
src/lib/modrinth/index.ts
Normal file
9
src/lib/modrinth/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export { ModrinthClient } from "./client";
|
||||
export {
|
||||
listVersions,
|
||||
getVersion,
|
||||
getVersionByNumber,
|
||||
getVersions,
|
||||
createVersion,
|
||||
} from "./version";
|
||||
export { getLoaders, getGameVersions } from "./tag";
|
||||
29
src/lib/modrinth/tag.ts
Normal file
29
src/lib/modrinth/tag.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { ModrinthClient } from "./client";
|
||||
import {
|
||||
LoaderTagSchema,
|
||||
GameVersionTagSchema,
|
||||
type LoaderTag,
|
||||
type GameVersionTag,
|
||||
} from "@/types";
|
||||
|
||||
/**
|
||||
* 获取 Modrinth 支持的全部模组加载器列表。
|
||||
* GET /tag/loader
|
||||
*/
|
||||
export async function getLoaders(
|
||||
client: ModrinthClient,
|
||||
): Promise<LoaderTag[]> {
|
||||
const data = await client.get<unknown[]>("/tag/loader");
|
||||
return LoaderTagSchema.array().parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Modrinth 支持的全部 Minecraft 版本列表。
|
||||
* GET /tag/game_version
|
||||
*/
|
||||
export async function getGameVersions(
|
||||
client: ModrinthClient,
|
||||
): Promise<GameVersionTag[]> {
|
||||
const data = await client.get<unknown[]>("/tag/game_version");
|
||||
return GameVersionTagSchema.array().parse(data);
|
||||
}
|
||||
121
src/lib/modrinth/version.ts
Normal file
121
src/lib/modrinth/version.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { ModrinthClient } from "./client";
|
||||
import {
|
||||
VersionSchema,
|
||||
type Version,
|
||||
type CreatableVersion,
|
||||
} from "@/types";
|
||||
|
||||
/**
|
||||
* 列出指定项目的全部版本。
|
||||
* GET /project/{id|slug}/version
|
||||
*/
|
||||
export async function listVersions(
|
||||
client: ModrinthClient,
|
||||
projectId: string,
|
||||
options?: {
|
||||
loaders?: string[];
|
||||
game_versions?: string[];
|
||||
featured?: boolean;
|
||||
include_changelog?: boolean;
|
||||
},
|
||||
): Promise<Version[]> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (options?.loaders) {
|
||||
params.set("loaders", JSON.stringify(options.loaders));
|
||||
}
|
||||
if (options?.game_versions) {
|
||||
params.set("game_versions", JSON.stringify(options.game_versions));
|
||||
}
|
||||
if (options?.featured !== undefined) {
|
||||
params.set("featured", String(options.featured));
|
||||
}
|
||||
// 强烈建议不带 changelog 以减小响应体
|
||||
params.set(
|
||||
"include_changelog",
|
||||
String(options?.include_changelog ?? false),
|
||||
);
|
||||
|
||||
const qs = params.toString();
|
||||
const path = `/project/${encodeURIComponent(projectId)}/version${qs ? `?${qs}` : ""}`;
|
||||
const data = await client.get<unknown[]>(path);
|
||||
return VersionSchema.array().parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个版本的完整信息。
|
||||
* GET /version/{id}
|
||||
*/
|
||||
export async function getVersion(
|
||||
client: ModrinthClient,
|
||||
versionId: string,
|
||||
): Promise<Version> {
|
||||
const path = `/version/${encodeURIComponent(versionId)}`;
|
||||
const data = await client.get<unknown>(path);
|
||||
return VersionSchema.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过项目 ID + 版本号获取版本。
|
||||
* GET /project/{id|slug}/version/{id|number}
|
||||
*/
|
||||
export async function getVersionByNumber(
|
||||
client: ModrinthClient,
|
||||
projectId: string,
|
||||
versionNumber: string,
|
||||
): Promise<Version> {
|
||||
const path = `/project/${encodeURIComponent(projectId)}/version/${encodeURIComponent(versionNumber)}`;
|
||||
const data = await client.get<unknown>(path);
|
||||
return VersionSchema.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取多个版本信息。
|
||||
* GET /versions?ids=["...","..."]
|
||||
*/
|
||||
export async function getVersions(
|
||||
client: ModrinthClient,
|
||||
versionIds: string[],
|
||||
): Promise<Version[]> {
|
||||
const params = new URLSearchParams();
|
||||
params.set("ids", JSON.stringify(versionIds));
|
||||
const path = `/versions?${params.toString()}`;
|
||||
const data = await client.get<unknown[]>(path);
|
||||
return VersionSchema.array().parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个新版本(含文件上传)。
|
||||
* POST /version
|
||||
*
|
||||
* @param client Modrinth 客户端实例
|
||||
* @param data 已通过 Zod 校验的版本元数据(CreatableVersion)
|
||||
* @param files 文件映射:key 为 multipart 字段名(需与 data.file_parts 一致),value 为文件 Blob
|
||||
*/
|
||||
export async function createVersion(
|
||||
client: ModrinthClient,
|
||||
data: CreatableVersion,
|
||||
files: Record<string, Blob>,
|
||||
): Promise<Version> {
|
||||
const form = new FormData();
|
||||
|
||||
// 元数据 JSON 字符串
|
||||
form.append("data", JSON.stringify(data));
|
||||
|
||||
// 逐一添加文件
|
||||
for (const fieldName of data.file_parts) {
|
||||
const blob = files[fieldName];
|
||||
if (!blob) {
|
||||
throw new Error(
|
||||
`createVersion: file field "${fieldName}" declared in file_parts but not provided in files`,
|
||||
);
|
||||
}
|
||||
// 从 Blob 推断文件名;若无则使用字段名作为回退
|
||||
const filename =
|
||||
blob instanceof File ? blob.name : `${fieldName}.jar`;
|
||||
form.append(fieldName, blob, filename);
|
||||
}
|
||||
|
||||
const result = await client.post<unknown>("/version", form);
|
||||
return VersionSchema.parse(result);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user