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