Add validation for Modrinth and CurseForge version fields

This commit is contained in:
CPTProgrammer 2026-07-15 13:28:42 +08:00
parent f1447a3b91
commit 80d664936a
No known key found for this signature in database
4 changed files with 134 additions and 6 deletions

View File

@ -26,6 +26,8 @@ import {
checkSourceFiles,
lookupModrinthProject,
lookupCurseforgeProject,
fetchModrinthVersionNames,
fetchCurseforgeFileNames,
} from "@/services/project";
import {
useFieldValidations,
@ -37,6 +39,32 @@ import {
type ListValidationDef,
} from "@/hooks/useFieldValidations";
import type { Config, DependencyType, UploadRelationType } from "@/types";
import { Template } from "@/lib/utils/template";
// ---------------------------------------------------------------------------
// 工具
// ---------------------------------------------------------------------------
function parseAndCount(source: string, items: string[]): string[] {
const matched: string[] = [];
try {
const tmpl = new Template(source);
for (const item of items) {
try { tmpl.parse(item); matched.push(item); } catch { /* skip */ }
}
} catch {
return [];
}
return matched;
}
function parseAndCountWithTmpl(tmpl: Template, items: string[]): string[] {
const matched: string[] = [];
for (const item of items) {
try { tmpl.parse(item); matched.push(item); } catch { /* skip */ }
}
return matched;
}
// ---------------------------------------------------------------------------
// 表单路径类型(从 Config 推导)
@ -169,6 +197,62 @@ const STATIC_DEFS: StaticValidationDef<Config>[] = [
: { status: "error", help: r.error };
},
},
// 7. Modrinth 版本名称模板
{
target: ["modrinth", "version_name"],
deps: [["modrinth", "project_id"], ["modrinth", "version_name"]],
async validate(getVal) {
const pid = getVal(["modrinth", "project_id"]) as string | undefined;
const source = getVal(["modrinth", "version_name"]) as string | undefined;
if (!pid || !source) return null;
const r = await fetchModrinthVersionNames(pid);
if (!r.ok) return { status: "error", help: r.error };
const matched = parseAndCount(source, r.names);
if (matched.length > 0) {
return { status: "success", help: `匹配到 ${matched.length} 个版本` };
}
const examples = r.names.slice(0, 3).join("");
return { status: "error", help: `未匹配到任何版本,示例:${examples}` };
},
},
// 8. Modrinth 版本号模板
{
target: ["modrinth", "version"],
deps: [["modrinth", "project_id"], ["modrinth", "version"]],
async validate(getVal) {
const pid = getVal(["modrinth", "project_id"]) as string | undefined;
const source = getVal(["modrinth", "version"]) as string | undefined;
if (!pid || !source) return null;
const r = await fetchModrinthVersionNames(pid);
if (!r.ok) return { status: "error", help: r.error };
const matched = parseAndCount(source, r.numbers);
if (matched.length > 0) {
return { status: "success", help: `匹配到 ${matched.length} 个版本` };
}
const examples = r.numbers.slice(0, 3).join("");
return { status: "error", help: `未匹配到任何版本,示例:${examples}` };
},
},
// 9. CurseForge 版本名称模板
{
target: ["curseforge", "version_name"],
deps: [["curseforge", "project_id"], "filename_format", ["curseforge", "version_name"]],
async validate(getVal) {
const pid = getVal(["curseforge", "project_id"]) as number | undefined;
const fmt = getVal("filename_format") as string | undefined;
const source = getVal(["curseforge", "version_name"]) as string | undefined;
if (pid == null || pid === 0 || !fmt || !source) return null;
const r = await fetchCurseforgeFileNames(pid);
if (!r.ok) return { status: "error", help: r.error };
const tmpl = new Template(source, { filename_format: fmt });
const matched = parseAndCountWithTmpl(tmpl, r.names);
if (matched.length > 0) {
return { status: "success", help: `匹配到 ${matched.length} 个版本` };
}
const examples = r.names.slice(0, 3).join("");
return { status: "error", help: `未匹配到任何版本,示例:${examples}` };
},
},
];
const LIST_DEFS: ListValidationDef<Config>[] = [
@ -565,23 +649,23 @@ function ConfigModalForm({
<Input placeholder="8 位 base62" />
</ValidatedFormItem>
<Form.Item
<ValidatedFormItem
name={["modrinth", "version_name"]}
label="版本名称模板"
rules={[{ required: true }]}
extra="支持 ${version}、${mc_version_range} 等占位符"
>
<Input placeholder="ModName v${version} for Minecraft ${mc_version_range}" />
</Form.Item>
</ValidatedFormItem>
<Form.Item
<ValidatedFormItem
name={["modrinth", "version"]}
label="版本号模板"
rules={[{ required: true }]}
extra="支持 ${version}、${mc_version} 等占位符"
>
<Input placeholder="${version}-mc${mc_version}" />
</Form.Item>
</ValidatedFormItem>
<Form.Item
name={["modrinth", "loaders"]}
@ -696,14 +780,14 @@ function ConfigModalForm({
/>
</ValidatedFormItem>
<Form.Item
<ValidatedFormItem
name={["curseforge", "version_name"]}
label="版本名称模板"
rules={[{ required: true }]}
extra="支持 #{filename_format} 等占位符"
>
<Input placeholder="#{filename_format}" />
</Form.Item>
</ValidatedFormItem>
<Form.Item
name={["curseforge", "environment"]}

View File

@ -257,6 +257,8 @@ export async function checkSourceFiles(
import { getProject } from "@/lib/modrinth/project";
import { getMod } from "@/lib/curseforge/mod";
import { listVersions } from "@/lib/modrinth";
import { getFiles } from "@/lib/curseforge";
import { modrinthClient, curseforgeClient } from "./clients";
export async function lookupModrinthProject(
@ -280,3 +282,43 @@ export async function lookupCurseforgeProject(
return { ok: false, error: err instanceof Error ? err.message : "查询 CurseForge 项目失败" };
}
}
/**
* Modrinth
*/
export async function fetchModrinthVersionNames(
projectId: string,
): Promise<OkResult<{ names: string[]; numbers: string[] }> | ErrResult> {
try {
const versions = await listVersions(modrinthClient, projectId, { include_changelog: false });
return {
ok: true,
names: versions.map((v) => v.name),
numbers: versions.map((v) => v.version_number),
};
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : "获取 Modrinth 版本列表失败" };
}
}
/**
* CurseForge displayName
*/
export async function fetchCurseforgeFileNames(
modId: number,
): Promise<OkResult<{ names: string[] }> | ErrResult> {
try {
const all: string[] = [];
let index = 0;
const pageSize = 50;
while (true) {
const resp = await getFiles(curseforgeClient, { modId, index, pageSize });
for (const f of resp.data) all.push(f.displayName);
if (all.length >= resp.pagination.totalCount) break;
index += pageSize;
}
return { ok: true, names: all };
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : "获取 CurseForge 文件列表失败" };
}
}

View File

@ -172,6 +172,7 @@ export const CurseForgeFileSchema = z.object({
/** 文件模块列表 */
modules: z.array(FileModuleSchema),
}).pick({
displayName: true,
fileName: true,
});
export type CurseForgeFile = z.infer<typeof CurseForgeFileSchema>;

View File

@ -114,6 +114,7 @@ export const VersionSchema = BaseVersionSchema.extend({
/** 该版本的所有文件列表 */
files: z.array(VersionFileSchema),
}).pick({
name: true,
version_number: true,
});
export type Version = z.infer<typeof VersionSchema>;