2026-07-06 04:56:50 +08:00
|
|
|
|
# 整体框架
|
|
|
|
|
|
|
|
|
|
|
|
## 数据文件
|
|
|
|
|
|
|
|
|
|
|
|
### `secrets.json`
|
|
|
|
|
|
|
|
|
|
|
|
存放 API 密钥,位于项目根目录。
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"curseforge": "<CurseForge API Token>",
|
|
|
|
|
|
"modrinth": "<Modrinth Personal Access Token>"
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### `configs/{config_name}.json`
|
|
|
|
|
|
|
|
|
|
|
|
存放单个 mod 项目的配置,位于 `configs/` 目录下。文件名即为配置名,用户通过 WebUI 下拉选择。
|
|
|
|
|
|
|
|
|
|
|
|
```jsonc
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "模组名称",
|
|
|
|
|
|
"modrinth": {
|
|
|
|
|
|
"project_id": "AABBCCDD",
|
|
|
|
|
|
"version_name": "ModName v${version} for Minecraft ${mc_version_range}",
|
|
|
|
|
|
"version": "${version}-mc${mc_version}",
|
|
|
|
|
|
"loaders": ["fabric"],
|
|
|
|
|
|
"environment": "client_and_server",
|
|
|
|
|
|
"dependencies": [
|
|
|
|
|
|
{ "dependency_type": "required", "project_id": "P7dR8mSH" }
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
"curseforge": {
|
|
|
|
|
|
"project_id": 123456,
|
2026-07-08 04:55:53 +08:00
|
|
|
|
"version_name": "#{filename_format}",
|
2026-07-06 04:56:50 +08:00
|
|
|
|
"environment": ["Client", "Server"],
|
|
|
|
|
|
"loaders": ["fabric"],
|
|
|
|
|
|
"relations": {
|
|
|
|
|
|
"projects": []
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
"project_dir": "/path/to/project",
|
|
|
|
|
|
"minecraft_properties_dir": "./properties", // 目录内为 ${mc_version}.properties 文件,文件名即对应游戏版本
|
|
|
|
|
|
"project_properties_path": "./gradle.properties", // 项目版本配置文件路径
|
|
|
|
|
|
"mod_version_field": "mod_version", // 版本号在 project_properties_path 文件中的键名
|
|
|
|
|
|
"filename_format": "modname-${version}-mc${mc_version}.jar",
|
|
|
|
|
|
"source_filename_format": "modname-${version}-mc${mc_version}-sources.jar"
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
| 字段 | 类型 | 说明 |
|
|
|
|
|
|
|------|------|------|
|
|
|
|
|
|
| `name` | string | 配置文件显示名称(模组名称) |
|
|
|
|
|
|
| `modrinth` | object | Modrinth 配置 |
|
|
|
|
|
|
| `modrinth.project_id` | string | Modrinth 项目 ID(8 位 base62) |
|
|
|
|
|
|
| `modrinth.version_name` | string | 版本名称模板,`${mc_version_range}` 为 `"1.19"` 或 `"1.20-1.20.2"` |
|
|
|
|
|
|
| `modrinth.version` | string | 版本号模板 |
|
|
|
|
|
|
| `modrinth.loaders` | string[] | 加载器列表 |
|
|
|
|
|
|
| `modrinth.environment` | string | 运行环境,必填。可选值:`client_and_server`、`client_only`、`server_only` 等 |
|
|
|
|
|
|
| `modrinth.dependencies` | object[] | 依赖列表,无依赖填 `[]` |
|
|
|
|
|
|
| `curseforge` | object | CurseForge 配置 |
|
|
|
|
|
|
| `curseforge.project_id` | number | CurseForge 项目 ID |
|
2026-07-08 04:55:53 +08:00
|
|
|
|
| `curseforge.version_name` | string | 版本名称模板,如 `"#{filename_format}"` |
|
2026-07-06 04:56:50 +08:00
|
|
|
|
| `curseforge.environment` | string[] | 运行环境 |
|
|
|
|
|
|
| `curseforge.loaders` | string[] | 加载器列表 |
|
|
|
|
|
|
| `curseforge.relations` | object | 关联项目,格式 `{ projects: [...] }` |
|
|
|
|
|
|
| `project_dir` | string | 项目目录绝对路径 |
|
|
|
|
|
|
| `minecraft_properties_dir` | string | 相对项目目录的 MC 版本配置目录,默认 `./properties`。目录内为 `${mc_version}.properties` 文件,文件名即为可用游戏版本 |
|
|
|
|
|
|
| `project_properties_path` | string | 相对项目目录的项目版本配置文件路径,默认 `./gradle.properties` |
|
|
|
|
|
|
| `mod_version_field` | string | 版本号在 `project_properties_path` 文件中的键名,默认 `"mod_version"` |
|
|
|
|
|
|
| `filename_format` | string | 构建产物文件名模板,用于反向解析 `${version}` 和 `${mc_version}` |
|
|
|
|
|
|
| `source_filename_format` | string | 源码 jar 文件名模板,用于正向生成文件名 |
|
|
|
|
|
|
|
2026-07-08 04:55:53 +08:00
|
|
|
|
> **递归解析规则**:所有含占位符的模板字符串,正向 format 或反向解析后,若结果仍包含占位符,则需继续解析,直至结果中不再有占位符为止。
|
|
|
|
|
|
>
|
|
|
|
|
|
> - `#{...}` — 引用占位符,如 `#{filename_format}` 表示此处将引用解析后的值
|
|
|
|
|
|
> - `${...}` — 模板变量,由运行时填入实际值,如 `${version}`、`${mc_version}`
|
2026-07-06 04:56:50 +08:00
|
|
|
|
|
|
|
|
|
|
## 运行时流程
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 启动
|
|
|
|
|
|
|
|
|
|
|
|
- 启动 Next.js 开发服务器
|
|
|
|
|
|
- 后端读取 `configs/` 目录,列出所有可用配置文件
|
|
|
|
|
|
- 后端读取 `secrets.json`,加载两个平台的 API 密钥到内存
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 选择配置
|
|
|
|
|
|
|
|
|
|
|
|
- 前端 ConfigSelector 下拉框展示所有配置(按 `name` 显示)
|
|
|
|
|
|
- 用户选择一个配置后,前端请求后端解析项目信息
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 解析项目
|
|
|
|
|
|
|
|
|
|
|
|
后端收到选中配置后:
|
|
|
|
|
|
|
|
|
|
|
|
1. 读取 `{project_dir}/{project_properties_path}`,提取 `mod_version_field` 对应的值 → `version`
|
|
|
|
|
|
2. 扫描 `{project_dir}/{minecraft_properties_dir}/` 目录,获取所有 `${mc_version}.properties` 文件名 → `mc_version[]`
|
2026-07-10 08:37:23 +08:00
|
|
|
|
3. 对 `mc_version[]` 排序后计算每个版本的兼容范围:
|
|
|
|
|
|
- 非最后一个版本:范围为 `[当前版本, 下一个版本)`,如 `"1.19.1"` → `"1.19.1-1.19.2"`
|
|
|
|
|
|
- 最后一个版本:`< 26` 的版本用 `~` 匹配次版本号(如 `1.21.x`);`≥ 26` 的版本用 `^` 匹配主版本号(如 `26.x`),截止版本由用户在下拉框中选择
|
|
|
|
|
|
4. 遍历每个 `mc_version`:
|
2026-07-06 04:56:50 +08:00
|
|
|
|
- 用 `version` 和 `mc_version` 填入 `filename_format` 模板,生成构建产物文件名
|
|
|
|
|
|
- 用 `version` 和 `mc_version` 填入 `source_filename_format` 模板,生成源码文件名
|
|
|
|
|
|
- 检查 `{project_dir}/build/libs/` 下是否存在对应文件
|
2026-07-10 08:37:23 +08:00
|
|
|
|
5. 将所有匹配结果返回前端,由 VersionTable 展示(MC 版本 | 兼容范围 | 构建产物 | 源码)
|
2026-07-06 04:56:50 +08:00
|
|
|
|
|
|
|
|
|
|
### 4. 版本确认与发布
|
|
|
|
|
|
|
|
|
|
|
|
#### 第一页:版本选择
|
|
|
|
|
|
|
|
|
|
|
|
1. 用户点击 `[一键发布]`,弹出 PublishModal
|
|
|
|
|
|
2. 前端请求两个平台的已有版本列表:
|
|
|
|
|
|
- Modrinth:`GET /project/{id}/version`
|
|
|
|
|
|
- CurseForge:通过对应 API 获取
|
|
|
|
|
|
3. 后端比对已有版本与待发布版本(按 `version` 模板生成的值匹配)
|
|
|
|
|
|
4. 前端左右两栏展示(待发布版本与已有版本合并后,按 `mod_version` 降序、`mc_version` 降序排列):
|
|
|
|
|
|
- 顶部绿色行 = 待发布版本,复选框默认勾选
|
|
|
|
|
|
- 下方灰色行 = 已有版本,复选框禁用
|
|
|
|
|
|
- 已存在的待发布版本显示为已有版本颜色,复选框禁用
|
|
|
|
|
|
5. 用户调整勾选后点击"下一步"
|
|
|
|
|
|
|
|
|
|
|
|
#### 第二页:发布设置
|
|
|
|
|
|
|
|
|
|
|
|
- 用户填写 changelog(Markdown,Code / Preview 切换)
|
|
|
|
|
|
- 选择 version_type(默认 `release`)
|
|
|
|
|
|
- 点击"确认发布";可点击"上一步"返回修改
|
|
|
|
|
|
|
|
|
|
|
|
### 5. 执行发布
|
|
|
|
|
|
|
2026-07-11 05:39:08 +08:00
|
|
|
|
1. 前端调用 `trpc.publish.useMutation()` 发起发布
|
|
|
|
|
|
2. 前端通过 `trpc.progress.useSubscription()` 监听进度,实时更新进度条
|
2026-07-08 06:13:19 +08:00
|
|
|
|
3. 进度条格式:`{平台图标} 平台名: [======== ] 0 / N`,仅显示有勾选任务的平台
|
|
|
|
|
|
4. 单个平台内按 MC 版本顺序逐个提交,**不可并行**;两个平台之间可以并行执行
|
|
|
|
|
|
5. 每个版本的发布流程:
|
2026-07-06 04:56:50 +08:00
|
|
|
|
- 解析模板:用 `version` 和 `mc_version` 填入各平台 `version_name`、`version` 模板(含递归解析)
|
|
|
|
|
|
- Modrinth:构造 multipart/form-data 请求,含 `data` JSON + 构建产物文件 + 源码文件
|
|
|
|
|
|
- CurseForge:构造 multipart/form-data 请求,含 `metadata` JSON + 文件
|
2026-07-08 06:13:19 +08:00
|
|
|
|
- 通过 tRPC subscription 推送进度更新
|
|
|
|
|
|
6. **任一请求失败则取消当前平台的后续所有任务**,不影响另一个平台
|
2026-07-18 10:04:16 +08:00
|
|
|
|
7. 全部完成或失败后,在第 3 步进度页内嵌显示最终状态(成功数 / 失败数 + 各平台失败原因列表)
|
2026-07-17 19:15:41 +08:00
|
|
|
|
|
|
|
|
|
|
### 错误处理与可见性
|
|
|
|
|
|
|
|
|
|
|
|
- 单个版本上传失败:记入该平台 `errors`,发出 `failed` 进度事件(进度条变红),该平台后续版本取消;
|
|
|
|
|
|
- 平台准备阶段失败(元数据拉取、版本范围计算):记为该平台**全部失败**,同样发出 `failed` 事件,并在服务端终端输出 `console.error`;
|
|
|
|
|
|
- mutation 层兜底:任何未被捕获的平台异常记入 `errors` 并输出服务端日志;
|
2026-07-18 10:04:16 +08:00
|
|
|
|
- 前端进度页的结果区按平台分行列出所有失败原因。
|
2026-07-17 17:00:05 +08:00
|
|
|
|
|
|
|
|
|
|
## Dry-run(模拟发布)
|
|
|
|
|
|
|
|
|
|
|
|
在 `.env.local` 中设置 `PUBLISH_DRY_RUN=1` 后重启开发服务器,发布流程进入模拟模式:
|
|
|
|
|
|
|
|
|
|
|
|
- 配置解析、版本范围计算、文件读取、multipart 请求组装全部照常执行;
|
|
|
|
|
|
- 两个平台的上传请求在 `ModrinthClient.post` / `CurseForgeClient.uploadPost` 内被拦截,**不会发生任何实际上传**;
|
|
|
|
|
|
- 每次拦截会把完整请求摘要(metadata JSON、各文件字段名/大小)打印到终端,并追加写入日志文件供人工核对,路径见终端输出;可用环境变量 `PUBLISH_DRY_RUN_LOG_DIR` 指定日志目录,默认为系统临时目录;
|
|
|
|
|
|
- 平台返回伪造响应以维持调用链类型校验(Modrinth 为 `{ name: "(dry-run)", ... }`,CurseForge 为 `{ id: 0 }`),并带 1.5 秒模拟延迟,让前端进度条逐个推进。
|
|
|
|
|
|
|
|
|
|
|
|
实现位置:`src/lib/utils/dryRun.ts`。
|