From 80d664936aa705a7ccf788ff72225d6ad41d616e Mon Sep 17 00:00:00 2001 From: CPTProgrammer <46586216+CPTProgrammer@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:28:42 +0800 Subject: [PATCH] Add validation for Modrinth and CurseForge version fields --- src/components/ConfigModal.tsx | 96 +++++++++++++++++++++++++++++++--- src/services/project.ts | 42 +++++++++++++++ src/types/curseforge/files.ts | 1 + src/types/modrinth/version.ts | 1 + 4 files changed, 134 insertions(+), 6 deletions(-) diff --git a/src/components/ConfigModal.tsx b/src/components/ConfigModal.tsx index 38da390..fed91ae 100644 --- a/src/components/ConfigModal.tsx +++ b/src/components/ConfigModal.tsx @@ -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[] = [ : { 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[] = [ @@ -565,23 +649,23 @@ function ConfigModalForm({ - - + - - + - - + | 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 | 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 文件列表失败" }; + } +} diff --git a/src/types/curseforge/files.ts b/src/types/curseforge/files.ts index 49eb0cb..d0e89e9 100644 --- a/src/types/curseforge/files.ts +++ b/src/types/curseforge/files.ts @@ -172,6 +172,7 @@ export const CurseForgeFileSchema = z.object({ /** 文件模块列表 */ modules: z.array(FileModuleSchema), }).pick({ + displayName: true, fileName: true, }); export type CurseForgeFile = z.infer; diff --git a/src/types/modrinth/version.ts b/src/types/modrinth/version.ts index e471e99..839ed6a 100644 --- a/src/types/modrinth/version.ts +++ b/src/types/modrinth/version.ts @@ -114,6 +114,7 @@ export const VersionSchema = BaseVersionSchema.extend({ /** 该版本的所有文件列表 */ files: z.array(VersionFileSchema), }).pick({ + name: true, version_number: true, }); export type Version = z.infer;