Add Modrinth edit version API support
This commit is contained in:
parent
619abfb0c5
commit
778ac54bfa
@ -92,6 +92,7 @@ src/types/
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `upload.md` | 新建版本 API(`POST /version`) |
|
||||
| `edit.md` | 修改版本 API(`PATCH /version/{id}`) |
|
||||
| `get.md` | 获取版本信息 API(列出项目版本、获取单个版本等) |
|
||||
| `get-meta.md` | 元数据查询 API(加载器列表、游戏版本列表等) |
|
||||
|
||||
|
||||
181
docs/modrinth/edit.md
Normal file
181
docs/modrinth/edit.md
Normal file
@ -0,0 +1,181 @@
|
||||
# Modrinth 修改版本 API 使用文档
|
||||
|
||||
本文档描述如何通过 Modrinth API 修改一个**已有版本**的元数据,例如更新兼容的 Minecraft 版本范围(`game_versions`)、changelog、加载器列表等,无需重新上传文件。
|
||||
|
||||
---
|
||||
|
||||
## 基本信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| **端点** | `PATCH /version/{id}` |
|
||||
| **生产环境** | `https://api.modrinth.com/v2/version/{id}` |
|
||||
| **测试环境** | `https://staging-api.modrinth.com/v2/version/{id}` |
|
||||
| **Content-Type** | `application/json` |
|
||||
| **认证** | 必需 — Personal Access Token (PAT) |
|
||||
| **所需 Scope** | `VERSION_WRITE` |
|
||||
| **成功响应码** | `204 No Content`(无响应体) |
|
||||
|
||||
> 创建版本用 `POST /version`(见 [upload.md](./upload.md));修改版本不需要重新上传文件,只要传 JSON 即可。
|
||||
|
||||
---
|
||||
|
||||
## 路径参数
|
||||
|
||||
| 参数 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `id` | 版本 ID(8 位 base62 字符串) | `IIJJKKLL` |
|
||||
|
||||
> 版本 ID 可通过 `GET /project/{id|slug}/version` 列表获取(见 [get.md](./get.md))。
|
||||
|
||||
---
|
||||
|
||||
## 请求体(EditableVersion)
|
||||
|
||||
请求体为 JSON,**所有字段均为可选,只需传入要修改的字段**,未传入的字段保持不变。
|
||||
|
||||
### 继承自 BaseVersion 的字段
|
||||
|
||||
| 字段 | 类型 | 说明 | 示例 |
|
||||
|------|------|------|------|
|
||||
| `name` | `string` | 版本名称 | `"Version 1.0.0"` |
|
||||
| `version_number` | `string` | 版本号 | `"1.0.0"` |
|
||||
| `changelog` | `string \| null` | 更新日志(Markdown/纯文本) | `"修复了若干 Bug"` |
|
||||
| `dependencies` | `object[]` | 依赖列表(整体替换),结构同创建版本 | 见 [upload.md](./upload.md#依赖结构) |
|
||||
| `game_versions` | `string[]` | **支持的 Minecraft 版本列表(整体替换)** | `["1.21", "1.21.1"]` |
|
||||
| `version_type` | `string` | 发布渠道:`release`、`beta`、`alpha` | `"release"` |
|
||||
| `loaders` | `string[]` | 支持的加载器列表(整体替换) | `["fabric"]` |
|
||||
| `featured` | `boolean` | 是否为精选版本 | `false` |
|
||||
| `status` | `string` | 版本状态:`listed`、`archived`、`draft`、`unlisted`、`scheduled`、`unknown` | `"listed"` |
|
||||
| `requested_status` | `string \| null` | 请求的状态(用于审核流程):`listed`、`archived`、`draft`、`unlisted` | `"listed"` |
|
||||
|
||||
> **注意:** `game_versions`、`loaders`、`dependencies` 都是**整体替换**语义,传入的值会完全覆盖原值,而不是增量添加。修改兼容范围时应先读取版本当前的 `game_versions`,在此基础上增删后再整体提交。
|
||||
|
||||
### EditableVersion 独有字段
|
||||
|
||||
| 字段 | 类型 | 说明 | 示例 |
|
||||
|------|------|------|------|
|
||||
| `primary_file` | `[string, string]` | 新的主文件,二元组 `[哈希算法, 哈希值]` | `["sha1", "aaaabbbb..."]` |
|
||||
| `file_types` | `object[]` | 要修改文件类型的文件列表(见下方) | 见下方 |
|
||||
|
||||
#### `file_types` 数组项(EditableFileType)
|
||||
|
||||
通过文件哈希定位版本中的某个文件并修改其类型标记:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 | 示例 |
|
||||
|------|------|------|------|------|
|
||||
| `algorithm` | `string` | ✅ | 哈希算法(如 `sha1`、`sha512`) | `"sha1"` |
|
||||
| `hash` | `string` | ✅ | 要修改的文件的哈希值 | `"aaaabbbb..."` |
|
||||
| `file_type` | `string \| null` | ✅ | 新的文件类型;`null` 表示清除类型标记。枚举值见 [upload.md](./upload.md) 的 `FileTypeEnum` 表 | `"sources-jar"` |
|
||||
|
||||
> 文件的 `sha1` / `sha512` 哈希可从版本详情的 `files[].hashes` 字段获取(见 [get.md](./get.md))。
|
||||
|
||||
---
|
||||
|
||||
## 请求示例
|
||||
|
||||
### 修改兼容的 MC 版本范围
|
||||
|
||||
```bash
|
||||
curl -X PATCH "https://api.modrinth.com/v2/version/IIJJKKLL" \
|
||||
-H "Authorization: YOUR_PAT_TOKEN" \
|
||||
-H "User-Agent: your_username/your_project/1.0.0" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"game_versions": ["1.21", "1.21.1", "1.21.4"]
|
||||
}'
|
||||
```
|
||||
|
||||
### Node.js (fetch) 示例
|
||||
|
||||
```js
|
||||
const TOKEN = "YOUR_PAT_TOKEN";
|
||||
const VERSION_ID = "IIJJKKLL";
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.modrinth.com/v2/version/${VERSION_ID}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: TOKEN,
|
||||
"User-Agent": "your_username/your_project/1.0.0",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
game_versions: ["1.21", "1.21.1", "1.21.4"],
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
// 成功时 response.status === 204,无响应体
|
||||
console.log(response.status); // 204
|
||||
```
|
||||
|
||||
### 同时修改 changelog 和发布渠道
|
||||
|
||||
```js
|
||||
await fetch(`https://api.modrinth.com/v2/version/${VERSION_ID}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: TOKEN,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
changelog: "## 更新内容\n\n- 修复了某某 Bug",
|
||||
version_type: "beta",
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 响应
|
||||
|
||||
成功时返回 **`204 No Content`,无响应体**。修改后的完整版本信息可通过 `GET /version/{id}` 再次获取确认。
|
||||
|
||||
---
|
||||
|
||||
## 错误码
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| `204` | 成功 |
|
||||
| `401` | Token 无效、未提供 Token,或 Token 不包含 `VERSION_WRITE` 权限范围 |
|
||||
| `404` | 版本不存在,或无权访问该版本 |
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 增量修改语义
|
||||
|
||||
PATCH 只更新传入的字段,未传入的字段保持服务器上的原值不变。这与 `POST /version`(全量创建)不同。
|
||||
|
||||
### 2. 数组字段是整体替换
|
||||
|
||||
`game_versions`、`loaders`、`dependencies` 传入后会**完全覆盖**原值。若只想"添加一个 MC 版本",正确做法是:
|
||||
|
||||
1. `GET /version/{id}` 读取当前 `game_versions`;
|
||||
2. 在数组中追加新版本号;
|
||||
3. PATCH 提交完整的新数组。
|
||||
|
||||
### 3. 不要传递未修改的 status
|
||||
|
||||
客户端序列化请求体时,未显式设置的字段不应出现在 JSON 中。尤其注意不要让 `status` 被默认值(如 `"listed"`)填充,否则会意外改变版本状态(例如把 `draft` 变成 `listed`)。
|
||||
|
||||
### 4. 与"追加文件"的区别
|
||||
|
||||
本接口只修改元数据,不能上传新文件。向已有版本追加文件应使用 `POST /version/{id}/file`(见 [upload.md](./upload.md) 注意事项第 8 条,当前项目尚未封装该接口)。
|
||||
|
||||
### 5. Rate Limit
|
||||
|
||||
与 Modrinth 其他接口相同:每 IP 每分钟最多 **300** 个请求。
|
||||
|
||||
---
|
||||
|
||||
## 参考
|
||||
|
||||
- [Modrinth API 文档](https://docs.modrinth.com)
|
||||
- [OpenAPI 规范文件](openapi.yml)(`modifyVersion` 操作 / `EditableVersion` schema)
|
||||
- [新建版本 API 文档](upload.md)
|
||||
- [获取版本信息 API 文档](get.md)
|
||||
@ -60,4 +60,26 @@ export class ModrinthClient {
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 PATCH 请求,body 为 JSON。
|
||||
* 成功响应为 204 No Content,无响应体,故返回 void。
|
||||
*/
|
||||
async patch(path: string, body: unknown): Promise<void> {
|
||||
const res = await fetch(`${this.base}${path}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
...this.headers(),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const reqBody = JSON.stringify(body);
|
||||
const resBody = await readErrorBody(res);
|
||||
throw new Error(
|
||||
`Modrinth API PATCH ${path} failed: ${res.status} ${res.statusText}\n\n--Request--:\n${reqBody}\n\n--Response--:\n${resBody}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ export {
|
||||
getVersionByNumber,
|
||||
getVersions,
|
||||
createVersion,
|
||||
updateVersion,
|
||||
} from "./version";
|
||||
export { getLoaders, getGameVersions } from "./tag";
|
||||
export { getProject, getProjects } from "./project";
|
||||
|
||||
@ -3,8 +3,12 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ModrinthClient } from "./client";
|
||||
import { createVersion } from "./version";
|
||||
import type { CreatableVersion } from "@/types";
|
||||
import { createVersion, updateVersion } from "./version";
|
||||
import {
|
||||
EditableVersionSchema,
|
||||
type CreatableVersion,
|
||||
type EditableVersion,
|
||||
} from "@/types";
|
||||
|
||||
describe("createVersion (dry-run)", () => {
|
||||
const OLD_ENV = process.env;
|
||||
@ -71,3 +75,59 @@ describe("createVersion (dry-run)", () => {
|
||||
).rejects.toThrow('file field "sources" declared in file_parts');
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateVersion", () => {
|
||||
const client = new ModrinthClient("fake-token", "test-agent");
|
||||
let fetchMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("发送 PATCH 请求到 /v2/version/{id},body 为 JSON", async () => {
|
||||
const fields: EditableVersion = {
|
||||
game_versions: ["1.21", "1.21.1"],
|
||||
changelog: "updated changelog",
|
||||
};
|
||||
|
||||
await updateVersion(client, "IIJJKKLL", fields);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(url).toBe("https://api.modrinth.com/v2/version/IIJJKKLL");
|
||||
expect(init.method).toBe("PATCH");
|
||||
expect(init.headers).toMatchObject({
|
||||
Authorization: "fake-token",
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
expect(JSON.parse(init.body as string)).toEqual(fields);
|
||||
});
|
||||
|
||||
it("EditableVersionSchema 不注入 status 默认值", () => {
|
||||
// Zod v4 中 .partial() 会触发 default,此处验证 schema 已规避该问题
|
||||
expect(EditableVersionSchema.parse({})).toEqual({});
|
||||
expect(EditableVersionSchema.parse({ game_versions: ["1.21"] })).toEqual({
|
||||
game_versions: ["1.21"],
|
||||
});
|
||||
});
|
||||
|
||||
it("204 响应无 body,正常返回 void", async () => {
|
||||
await expect(
|
||||
updateVersion(client, "IIJJKKLL", { game_versions: ["1.21"] }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("非 2xx 响应抛出含状态码的错误", async () => {
|
||||
fetchMock.mockResolvedValue(
|
||||
new Response(JSON.stringify({ error: "not_found" }), { status: 404 }),
|
||||
);
|
||||
await expect(
|
||||
updateVersion(client, "IIJJKKLL", { game_versions: ["1.21"] }),
|
||||
).rejects.toThrow("404");
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
VersionSchema,
|
||||
type Version,
|
||||
type CreatableVersion,
|
||||
type EditableVersion,
|
||||
} from "@/types";
|
||||
|
||||
/**
|
||||
@ -116,3 +117,23 @@ export async function createVersion(
|
||||
const result = await client.post<unknown>("/version", form);
|
||||
return VersionSchema.parse(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改已有版本的元数据(如兼容的 MC 版本范围、changelog 等)。
|
||||
* PATCH /version/{id}
|
||||
*
|
||||
* 仅传入需要修改的字段即可,未传入的字段保持不变。
|
||||
* 成功响应为 204 No Content,无返回值。
|
||||
*
|
||||
* @param client Modrinth 客户端实例
|
||||
* @param versionId 版本 ID(8 位 base62)
|
||||
* @param fields 已通过 Zod 校验的待修改字段(EditableVersion)
|
||||
*/
|
||||
export async function updateVersion(
|
||||
client: ModrinthClient,
|
||||
versionId: string,
|
||||
fields: EditableVersion,
|
||||
): Promise<void> {
|
||||
const path = `/version/${encodeURIComponent(versionId)}`;
|
||||
await client.patch(path, fields);
|
||||
}
|
||||
|
||||
35
src/types/modrinth/edit-version.ts
Normal file
35
src/types/modrinth/edit-version.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { z } from "zod";
|
||||
import { BaseVersionSchema, FileTypeEnumSchema } from "./version";
|
||||
|
||||
// region EditableFileType — PATCH 时修改单个文件的类型
|
||||
// OpenAPI EditableFileType.required: algorithm, hash, file_type
|
||||
export const EditableFileTypeSchema = z.object({
|
||||
/** 哈希算法(如 sha1、sha512) */
|
||||
algorithm: z.string(),
|
||||
/** 要修改的文件的哈希值 */
|
||||
hash: z.string(),
|
||||
/** 新的文件类型;null 表示清除类型标记 */
|
||||
file_type: FileTypeEnumSchema.nullable(),
|
||||
});
|
||||
export type EditableFileType = z.infer<typeof EditableFileTypeSchema>;
|
||||
// endregion
|
||||
|
||||
// region EditableVersion — PATCH /version/{id} 请求体
|
||||
// OpenAPI EditableVersion = BaseVersion(全字段可选)+ primary_file / file_types
|
||||
//
|
||||
// 注意:不能直接 BaseVersionSchema.partial() —— Zod v4 中 .partial() 仍会
|
||||
// 触发 status 字段的 .default("listed"),导致 PATCH 时误改版本状态。
|
||||
// 因此显式将 status 覆盖为无 default 的可选字段。
|
||||
export const EditableVersionSchema = BaseVersionSchema.partial()
|
||||
.extend({
|
||||
/** 版本状态;不传则不修改 */
|
||||
status: z
|
||||
.enum(["listed", "archived", "draft", "unlisted", "scheduled", "unknown"])
|
||||
.optional(),
|
||||
/** 新的主文件,格式为 [哈希算法, 哈希值],如 ["sha1", "aaaa..."] */
|
||||
primary_file: z.tuple([z.string(), z.string()]).optional(),
|
||||
/** 要修改文件类型的文件列表 */
|
||||
file_types: z.array(EditableFileTypeSchema).optional(),
|
||||
});
|
||||
export type EditableVersion = z.infer<typeof EditableVersionSchema>;
|
||||
// endregion
|
||||
@ -34,6 +34,13 @@ export {
|
||||
type CreateVersionBody,
|
||||
} from "./create-version";
|
||||
|
||||
export {
|
||||
EditableFileTypeSchema,
|
||||
EditableVersionSchema,
|
||||
type EditableFileType,
|
||||
type EditableVersion,
|
||||
} from "./edit-version";
|
||||
|
||||
export {
|
||||
ProjectLicenseSchema,
|
||||
ProjectDonationURLSchema,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user