# 千界万锻 扩展包安装器 · 设计大纲

> 状态：设计草案 v0.4（2026-06-11）
> 目标：为「千界万锻（SDBF）」整合包提供一个跨启动器、可安装/卸载/更新扩展包（DLC）的图形化工具。
> 部署形态：**安装器作为自带 OpenJFX 的 fat-jar 放置于游戏实例目录内**（如 `/.minecraft/DLC_Installer.jar`），以自身所在目录作为目标实例根，由用户机上已存在的 Java（运行 MC 必备）启动。
> 技术栈：**Java 17 + JavaFX（打入 fat-jar）**；CurseForge 内容仅依据 `manifest.json` 的 `projectID`/`fileID`，**全程不使用 API key**。
>
> **决策汇总**：①不附带 `cf_files.json`，仅用 manifest 的 projectID/fileID；②版本号一律按字符串处理；③`/*` 递归包含全部子文件与子文件夹；④多 DLC 同名文件冲突→弹窗警告，旧文件移至 `SDBF_DLC/conflicts/`，新文件占据原位；⑤同②。
> ⑥安装器置于实例目录、以自身位置定位实例；⑦下载链路不使用 key；⑧`base_modpack_version` 取自各扩展包信息文件，仅显示、不做任何判断。
> ⑨依赖用户已有 Java 运行（MC 启动必备），JavaFX 打进 fat-jar；⑩无 key 下载主路径为 `curseforge.com/api/v1/mods/{projectID}/files/{fileID}/download`（302→forgecdn），项目页解析为兜底；⑪**同时支持两种内容形式**——`manifest.json` 声明下载的 mod 与扩展包自带的 `mods/*.jar` 可在同一扩展包内共存。
> **v0.5 落定**：⑫不附带启动脚本；⑬加入**镜像源**（MCIM `mod.mcimirror.top`，镜像 CF API 与文件）作为可配置下载源；⑭`conflicts/` **仅在界面显示、不做还原/清理等处理**。

---

## 0. 调研结论（设计前提）

### 0.1 CurseForge 导入包格式（manifest.json）

CF 导入包是一个 zip，根目录包含 `manifest.json`、`overrides/`（必选），通常还有 `modlist.html`（可忽略）。`manifest.json` 结构如下：

```json
{
  "minecraft": {
    "version": "1.20.1",
    "modLoaders": [
      { "id": "forge-47.2.0", "primary": true }
    ]
  },
  "manifestType": "minecraftModpack",
  "manifestVersion": 1,
  "name": "扩展包名称",
  "version": "1.0.0",
  "author": "作者",
  "files": [
    { "projectID": 238222, "fileID": 4670867, "required": true }
  ],
  "overrides": "overrides"
}
```

要点：
- `files[]` 只给 `projectID`（模组项目 ID）+ `fileID`（具体文件 ID）+ `required`，**不含文件名和下载地址**，需要安装器自行解析为可下载链接。这是本设计最关键的约束（见 §6.3）。
- `overrides/` 内是要直接复制进实例的文件（config、kubejs、SDBF_DLC 等）。本规范的 DLC 信息文件就放在 `overrides/SDBF_DLC/[id].json`。
- `minecraft.version` 与 `modLoaders` 可用于做加载器/MC 版本的兼容性预校验。
- `version` 是**任意字符串**（用户提供的本体样例为 `"b_260610"`，加载器 `forge-47.4.10`，MC `1.20.1`）。安装器对所有版本号一律按字符串处理，不做语义解析。

> 已确认（用户决策①）：扩展包**不附带** `cf_files.json` 之类的文件名缓存，安装器只能依据 `projectID`/`fileID` 在运行时解析文件名与下载地址。

### 0.2 CurseForge 获取文件名/下载地址的途径（关键兼容性约束）

由于决策①不附带 `cf_files.json`，安装器**必须在运行时**把 `(projectID, fileID)` 解析成「文件名 + 可下载 URL」。可用途径：

**A. 网站下载端点**（无需 key，决策⑦/⑩，本设计默认主路径）
- `GET https://www.curseforge.com/api/v1/mods/{projectID}/files/{fileID}/download`
- 行为：返回 `302`，`Location` 指向 forgecdn 真实直链（URL 末段即 `fileName`）。跟随重定向即可同时拿到**文件名**和**文件流**，全程无需 API key。
- **风险**：这是 CF 网站前端使用的端点，**非官方契约**，可能随站点改版或加风控（限速、Cloudflare 校验）。因此保留 B/手动兜底。

**B. 经项目页解析下载页**（无需 key，A 失败时的兜底）
- 第一跳：`GET https://www.curseforge.com/projects/{projectID}` → `302` 重定向到项目真实地址，从 `Location` 取「项目路径」（如 `/minecraft/mc-mods/jei`）。
- 第二跳：`GET https://www.curseforge.com/{项目路径}/download/{fileID}`（如 `.../minecraft/mc-mods/jei/download/8108851`）→ 跳转到 forgecdn 真实直链，跟随下载。

**C. 指纹反查**：murmur2（seed=1）可由已存在的 jar 计算指纹，用于识别用户手动放入 `mods/` 的文件是否为目标文件（本地计算，无需联网/ key）。

**D. 镜像源 MCIM**（决策⑬，可配置，国内加速/兜底）
- 项目：MCIM（`mcmod-info-mirror`），镜像 CurseForge 与 Modrinth 的 API 与文件，无需自备 key。
- URL 替换规则：
  - `api.curseforge.com` → `mod.mcimirror.top/curseforge`
  - `edge.forgecdn.net` / `mediafilez.forgecdn.net` → `mod.mcimirror.top`
- 用法：经 `GET mod.mcimirror.top/curseforge/v1/mods/{projectID}/files/{fileID}` 取文件信息（含 `downloadUrl`、`fileName`、`hashes`），再把 forgecdn 直链按上表替换为 `mod.mcimirror.top` 下载。
- **限制与注意**（务必在实现中处理）：
  - 仅缓存 **Mod**（CF `classId=6`），**>20MB 的文件不缓存** → 命中不到时必须回退源站。
  - 可能启用 **UA 白名单**，需为安装器设置固定 `User-Agent` 并按其要求登记。
  - 响应头 `sync_at` 给出缓存时间，可据此判断是否信任缓存。
  - 公益服务、可能不稳定/限流；MCIM 官方建议「源站失败时才用镜像」。
- **风险提示**：MCIM 协议禁止「对接口二次封装」，安装器应作为**直接下载客户端**使用镜像、并明确标注来源，不做接口再分发。

> 官方 Studios API（`https://api.curseforge.com` + `x-api-key`）严格要求 key，本设计**不使用**，仅作背景说明。

### 0.3 forgecdn 直链拼接算法（已知 fileName 时，无需 key）

一旦通过途径 A/B 拿到 `fileName`，即可直接拼 CDN 地址（用于断点续传、多源重试或镜像）：

```
host  = https://edge.forgecdn.net   （备用：https://mediafilez.forgecdn.net）
part1 = fileID / 1000               （整数部分，如 4799214 → 4799）
part2 = fileID % 1000               （余数，去前导零的整数，如 214；若为 007 则写 7）
URL   = {host}/files/{part1}/{part2}/{URL编码后的fileName}
```

> 注意：`part2` 为「去掉前导零的整数」，如 fileID=4799007 → part2=7。文件名需 URL 编码（空格→`%20` 等）。
> 单凭 `fileID` **无法**反推 `fileName`，所以 §6.3 的真正主路径是「途径 B 的重定向端点」，本算法主要用于已知文件名后的加速/镜像/续传。

---

## 1. 设计目标与非目标

### 1.1 目标
1. **一键安装/卸载/更新** 符合本规范的扩展包（两种格式：标准解压包、CF 导入包）。
2. **精准卸载**：依据 DLC 信息文件的 `files[]` 清单 + 本地安装登记表，只删自己注入的文件，绝不误删本体或其他 DLC。
3. **依赖与冲突管理**：解析 `dependencies` / `incompatibilities`，安装前校验。
4. **版本兼容提示**：对比 `base_modpack_version` 与当前本体版本，给出风险提示（提示而非强制阻断）。
5. **最大化兼容性**：跨操作系统（Win/macOS/Linux）、跨启动器（Prism/PCL/MultiMC/官方等）、跨 Java 版本、可离线降级。

### 1.2 非目标（当前阶段）
- 不做整合包本体的安装/更新（只管 DLC）。
- 不做 KubeJS/配方冲突的语义级检测（只做文件级冲突检测）。
- 不充当 CF 启动器，不管理 Forge/MC 本体 jar。

---

## 2. 技术选型

| 项 | 选择 | 理由 |
|---|---|---|
| 语言/运行时 | Java 17（LTS） | 平衡现代语法与广泛兼容；JavaFX 需要 11+ |
| GUI | JavaFX 17（OpenJFX） | 用户指定；现代外观、FXML/CSS 易维护 |
| 打包分发 | **自带 OpenJFX 的 fat-jar**（决策⑨），由用户已有 Java 启动 | MC 运行必备 Java，故不打包运行时；但普通 JRE 不含 JavaFX，所以把 OpenJFX 模块与各平台 native 库一并打入 jar（用 `javafx-maven-plugin` 的 shade 或 Gradle Shadow + 跨平台 classifier） |
| JSON | Jackson 或 Gson | 解析 DLC 信息文件 / manifest |
| HTTP 下载 | JDK 内置 `java.net.http.HttpClient` | 零额外依赖，支持异步与断点（自行实现 Range） |
| 压缩解包 | JDK `java.util.zip` / Commons Compress | 处理 zip 扩展包 |
| 哈希 | JDK MessageDigest（SHA-1）+ 自实现 Murmur2（CF 指纹） | 文件校验 + CF fingerprint 反查 |
| 日志 | SLF4J + logback，或 JUL | 排障 |
| 构建 | Gradle（推荐）或 Maven | 依赖管理 + jpackage 插件 |

> 兼容性补充：JavaFX 不再随 JDK 捆绑，因此**必须**用 jpackage 把 OpenJFX 模块一起打进分发包，杜绝「用户 JDK 不带 FX」导致的启动失败。

---

## 3. 总体架构（分层）

```
┌─────────────────────────────────────────────┐
│  Presentation 层 (JavaFX)                     │
│   FXML 视图 + Controller + ViewModel          │
├─────────────────────────────────────────────┤
│  Application/Service 层                        │
│   安装编排 InstallOrchestrator                │
│   依赖&冲突解析 DependencyResolver            │
│   版本信息展示 VersionDisplay                 │
├─────────────────────────────────────────────┤
│  Domain 层                                     │
│   DlcInfo / Manifest / InstalledRegistry      │
│   FileEntry / WildcardSpec                    │
├─────────────────────────────────────────────┤
│  Infrastructure 层                            │
│   InstanceLocator 实例定位                    │
│   PackageParser 扩展包解析                    │
│   Downloader (CDN/API/本地)                   │
│   FileInjector 事务化文件读写                 │
│   RegistryStore 本地登记表持久化              │
└─────────────────────────────────────────────┘
```

界面与业务彻底分离，所有耗时操作（下载、解压、复制）走后台线程，通过 JavaFX `Task`/`Service` 回传进度，避免阻塞 UI 线程。

---

## 4. 数据模型

### 4.1 DLC 信息文件（沿用规范 2.1）
`DlcInfo`：`id, name, version, baseModpackVersion, author[], description, links{}, dependencies[], incompatibilities[], files[]`。
- `files[]` 元素支持通配符，解析为 `WildcardSpec`。**决策③：`/*` 递归包含该文件夹下的全部子文件与子文件夹**（如 `kubejs/contentpacks/new_dlc_cp/*` 标记 `new_dlc_cp` 整个目录树）。安装时将通配展开为具体文件列表写入 registry；卸载按 registry 的具体清单执行。

### 4.2 CF Manifest
`CfManifest`：`minecraft{version, modLoaders[]}, name, version, author, files[]{projectID, fileID, required}, overrides`。

### 4.3 本地安装登记表（新增，安装器核心持久化）
路径建议：`<instance>/SDBF_DLC/installed.json`（或 `.sdbf/installed.json`）。

```json
{
  "registry_version": 1,
  "modpack_version_detected": "b_260603",
  "installed": {
    "new_dlc": {
      "version": "1.0.1",
      "installed_at": "2026-06-10T12:00:00Z",
      "source_format": "standard|curseforge",
      "files": [
        { "path": "mods/new_mod1.jar", "sha1": "…", "origin": "package|cdn|api" }
      ],
      "downloaded_mods": [
        { "projectID": 238222, "fileID": 4670867, "path": "mods/xxx.jar" }
      ]
    }
  }
}
```

`displaced` 字段记录因冲突被移走的文件（见 §5.3 冲突处理）：

```json
"displaced": [
  { "path": "config/shared.toml", "moved_to": "SDBF_DLC/conflicts/config/shared.toml", "owner_before": "old_dlc" }
]
```

作用：①卸载/更新时按**实际落地的文件**（而非仅 `files[]` 通配）精准删除；②记录每个文件的 SHA-1 用于「是否被本体/其他 DLC 占用」「是否被用户改动」判断；③记录 CF 下载来源便于复用缓存；④记录冲突位移，**仅供显示/追溯**（决策⑭：不据此自动还原）。

---

## 5. 核心模块职责

### 5.1 InstanceLocator（实例定位）
- **决策⑥：以安装器自身所在目录为目标实例根**。安装器作为 `DLC_Installer.jar` 被放进实例目录（如 `/.minecraft/`），启动时通过自身 jar 路径（`CodeSource` / `ProtectionDomain`）反推所在目录并直接作为实例根，无需让用户选启动器。
  - 这与「跨 Prism/PCL/MultiMC」天然兼容——不管哪个启动器，实例的 `.minecraft`（或对应 game 目录）就是 jar 所在处。
- 启动后做一次轻量自检：确认同目录存在 `mods/`、`config/` 或 `SDBF_DLC/` 等特征，给用户一个「当前实例：<路径>」的确认条；若疑似放错位置则提示。
- 兜底：仍提供「手动选择实例根目录」按钮（应对用户把 jar 放在别处运行的情况）。

### 5.2 PackageParser（扩展包解析）
- 输入：用户拖入/选择的 `xxx.zip`。
- 自动识别格式：根目录含 `manifest.json` + `overrides/` → CF 导入格式；否则按标准格式（顶层一个以 ID 命名的文件夹）。
- **决策⑪：两种内容形式可共存**。同一扩展包内既可有 `manifest.json` 声明的待下载 mod，又可在 `overrides/mods/`（CF 格式）或标准格式的 `mods/` 内自带 jar。解析时把"待下载清单"与"自带文件清单"合并为统一的注入计划：
  - 自带文件：直接进文件注入流程（§5.6）。
  - manifest 声明的 mod：进下载流程（§6.3），下载结果落到 `mods/` 后再纳入注入与 registry。
  - 若同一文件名既自带又在 manifest 列出，以**自带文件优先**，跳过该项下载（并记日志）。
- 校验：定位 `SDBF_DLC/[id].json`，做 schema 校验（必填字段、ID 合法性 `^[a-z0-9_]+$`、ID 与文件名一致）。
- 处理嵌套问题（规范 1.2.1 第 2 步提到的「`new_dlc/new_dlc/` 双层嵌套」）：自动探测并拆掉多余层级。
- 输出：规范化的 `StagedPackage`（一个临时解包目录 + 解析好的 `DlcInfo` + 可选 `CfManifest` + 合并后的注入计划）。

### 5.3 DependencyResolver（依赖与冲突）
- 读取目标实例的 `installed.json`，构建已装 DLC 图。
- 校验 `dependencies` 是否全部已装；缺失则提示并（可选）引导用户先装依赖。
- 校验 `incompatibilities` 与已装集合是否相交；相交则**阻断**或要求用户显式确认。
- **文件级冲突检测与处理（决策④）**：把待装 `files[]` 递归展开后的路径集合，与「本体文件」「其他 DLC 登记的文件」求交集。命中冲突时：
  1. **弹窗警告**用户，列出每个冲突文件及其原属 DLC（或本体）。
  2. 用户确认继续后，将**旧文件移动**到 `<instance>/SDBF_DLC/conflicts/<原相对路径>`（保留原目录层级，避免同名再撞）。
  3. **新文件写入原位**，由当前正在安装的 DLC 接管。
  4. 在新 DLC 的 registry 记录该位移（`displaced[]`），**仅供追溯/显示**。
  - **决策⑭：`conflicts/` 仅显示、不做处理**——安装器不会自动还原、合并、去重或清理 conflicts 中的文件；卸载时也不回填（见 §6.2）。被挪进去的旧文件保留在 `SDBF_DLC/conflicts/<原相对路径>` 供用户自行查看/手动处置。
  - 注：与本体文件冲突时同样按此处理（旧的本体文件进 conflicts），但应在弹窗中**额外高亮**「这会覆盖整合包本体文件」，风险更高。

### 5.4 VersionDisplay（版本信息展示，原 CompatibilityChecker）
- **决策⑧：`base_modpack_version` 取自每个扩展包自己的信息文件，仅作展示，不做任何判断/比较/阻断。**
- 安装器不再尝试探测当前本体版本，也不做相等性校验——只在扩展包详情/安装确认界面把该字符串原样显示给用户（如「此扩展包基于本体版本：`b_260603`」），由玩家自行判断。
- 版本号一律按字符串处理（决策②/⑤）。
- 若为 CF 格式，可顺带展示 manifest 的 `version` / `minecraft.version` / `modLoaders` 字符串供参考，同样仅展示。

### 5.5 Downloader（CF 内容获取，三级降级）
见 §6.3。统一接口 `resolve(projectID, fileID) → DownloadPlan`，内部按优先级尝试多个来源，带 SHA-1/长度校验、断点续传、并发下载、本地缓存（`<appdata>/sdbf-installer/cache/`）。

### 5.6 FileInjector（事务化注入）
- 所有写操作先落到临时区，再原子性地移动到实例目录；全程记录已写文件清单。
- **两类"挪开旧文件"要区分**：
  - `SDBF_DLC/conflicts/`：决策④的**永久**冲突位移——被别的 DLC 接管的旧文件存这里，卸载时可选还原。
  - `SDBF_DLC/.backup/<dlc_id>/`：**临时**事务备份——仅用于单次安装失败时的回滚，成功后即可清理。
- 失败时**回滚**：删除本次已写入的文件，从 `.backup` 还原本次被改动的文件，并把**本次事务**刚挪进 `conflicts/` 的文件移回原位。
  - 注：这是单次安装失败的事务完整性恢复，仅针对"正在进行且失败"的这次操作；与决策⑭"不处理已有 conflicts"不矛盾——安装**成功后**的 conflicts 文件不再被任何流程触碰。

### 5.7 RegistryStore（登记表持久化）
- 读写 `installed.json`，提供原子写（临时文件 + rename）。
- 版本迁移（`registry_version`）。

### 5.8 GUI（JavaFX）
见 §7。

---

## 6. 关键流程

### 6.1 安装流程
```
选实例 → 选扩展包 zip → 解析&校验(PackageParser)
  → 依赖/冲突/版本检查(Resolver+Checker) → 向用户展示摘要&风险
  → [CF格式] 解析 manifest.files → Downloader 下载所有 mod 到暂存
  → FileInjector 事务化注入 overrides/标准内容 + 下载的 mod
  → 写 installed.json → 完成报告(成功/跳过/警告)
```

### 6.2 卸载流程
```
从 installed.json 读取该 DLC 实际落地文件清单
  → 逐个校验 SHA-1：与记录一致(未被用户改动) → 删除
       （被用户改动过的文件 → 弹窗让用户选择「仍删除/保留」）
  → 不处理 conflicts/（决策⑭）：被本 DLC 顶替的旧文件保留在 conflicts/ 不回填
  → 清理空目录 → 更新 installed.json
```
> 因决策④下"同一路径同一时刻只归属一个 DLC"（冲突时旧文件已被挪到 conflicts），卸载无需引用计数；又因决策⑭不回填 conflicts，卸载逻辑进一步简化为"按 registry 清单删除"。

### 6.3 CF Mod 下载（仅凭 projectID/fileID，无 cf_files.json）

输入只有 `(projectID, fileID)`，目标是「拿到 fileName + 文件流」。**决策⑦：全程不使用 API key**。下载源顺序受设置项「下载源策略」影响（源站优先 / 镜像优先 / 仅镜像 / 仅源站，默认**源站优先、失败回退镜像**，符合 MCIM 建议）。

```
① 本地缓存命中（按 fileID 索引，命中后校验长度/SHA-1）   → 直接用，离线可用

② 网站下载端点（无 key，源站主路径，决策⑩）
     GET https://www.curseforge.com/api/v1/mods/{projectID}/files/{fileID}/download
     → 跟随 302 到 forgecdn，URL 末段即 fileName，直接下载文件流
     → edge.forgecdn.net 失败时按 §0.3 切 mediafilez.forgecdn.net 重试

③ 经项目页解析下载页（② 失败时兜底）
     a) GET https://www.curseforge.com/projects/{projectID}
          → 302 重定向到项目真实地址，从 Location 取"项目路径"（如 /minecraft/mc-mods/jei）
     b) GET https://www.curseforge.com/{项目路径}/download/{fileID}
          → 跳转到 forgecdn 真实直链（含 fileName），跟随下载

④ 镜像源 MCIM（决策⑬，可配置；"镜像优先/仅镜像"时提到 ② 之前）
     GET https://mod.mcimirror.top/curseforge/v1/mods/{projectID}/files/{fileID}
       → 取 fileName + downloadUrl(+hashes)，把 forgecdn 域名替换为 mod.mcimirror.top 下载
     注意：>20MB 或非 Mod 文件镜像可能未缓存 → 回退源站 ②③；需带固定 UA

⑤ 手动下载兜底（以上都失败：作者禁止分发 / 风控 / 页面改版 / 镜像未缓存）
     → 标记该 mod「需手动下载」，直接给出 ③b 的下载页链接
     → 引导用户手动放入 mods/，安装器监听目录并用文件指纹(murmur2)校验匹配
```

工程要点：
- `manifest.files` 可达上千项（本体样例 1500+），下载需**并发池 + 限速 + 退避重试**，避免触发 CF/Cloudflare 风控；失败项进单独重试队列。
- 全部走本地缓存（`<instance>/SDBF_DLC/.cache/` 或安装器同级 `cache/`，按 fileID 命名），多个 DLC / 重装可复用。
- 跟随重定向时校验最终 host 属于 `*.forgecdn.net`，防止被劫持到恶意地址。
- 路径 ③ 依赖 CF 网页结构（重定向 + 下载页跳转），属**非官方契约**，可能随站点改版失效，故必须保留 ④ 手动兜底。
- 解析 `项目路径` 时只取 `Location` 头，不解析 HTML，降低对页面结构的依赖。

### 6.4 更新流程
```
比较新包 version 与 installed 记录
  → 计算文件差异：新增/修改/删除
  → 删除「旧有但新版不再包含」的文件
  → 注入新增/变更文件（同安装的事务&备份机制）
  → 更新 installed.json
```

---

## 7. GUI 设计（JavaFX）

主要界面：
1. **实例选择栏**（顶部）：下拉显示自动探测到的实例 + 「手动选择目录」按钮 + 当前本体版本徽标。
2. **已装扩展包列表**（左/主区）：卡片展示图标（`DLC_icons/[id].png`）、名称、版本、作者、休谟指数（若有）；状态徽标（正常/版本不匹配/缺依赖/有冲突）。
3. **操作区**：拖拽或选择 zip 安装；对已装项提供「更新 / 卸载 / 查看详情」。
4. **安装确认对话框**：展示解析摘要、依赖树、冲突/版本警告、待下载 mod 列表，需用户确认。
5. **进度面板**：解压 / 下载（多文件并发进度）/ 注入 三阶段进度 + 实时日志。
6. **设置页**：**下载源策略**（源站优先[默认] / 镜像优先 / 仅镜像 / 仅源站）、镜像地址（默认 MCIM `mod.mcimirror.top`，可改）、缓存目录、并发数、网络代理、语言。**不含 API key 项**（决策⑦）。
7. **conflicts 查看页**（决策⑭）：只读列出 `SDBF_DLC/conflicts/` 中的文件及其原相对路径/来源 DLC，**仅显示**，不提供还原/删除/清理操作；可附「在文件管理器中打开」让用户自行处置。

交互原则：所有耗时操作非阻塞（`javafx.concurrent.Task`），可取消；失败可一键回滚；全程中文界面，预留 i18n（`ResourceBundle`）。

---

## 8. 兼容性策略（贯穿设计）

- **操作系统**：仅用 JDK 跨平台 API + `Path`，不硬编码路径分隔符；单个 fat-jar 跨三平台，内含各平台 OpenJFX native（win/mac/linux classifier）。
- **启动器**：以自身 jar 所在目录定位实例（决策⑥）+ 手动选目录兜底，不假设特定启动器。
- **Java/JavaFX**：复用用户已有 Java（MC 必备，决策⑨）启动，JavaFX 打入 jar；不自带运行时、不附带启动脚本（决策⑫）。
- **离线/弱网/国内**：本地缓存优先；源站多 host 重试 + 断点续传；可切 MCIM 镜像加速（决策⑬）；最终失败可手动放 mod。
- **CF 限制**：全程无 key（决策⑦）；遇禁止分发 / 镜像未缓存的 mod 走「手动下载引导」。
- **编码/路径**：zip 解包统一按 UTF-8；处理 Windows 路径长度限制与非法字符。
- **向后兼容**：`registry_version` / `dlc_format_version` 预留，便于规范演进后老安装器优雅降级。

---

## 9. 错误处理与日志
- 分级：可恢复（提示+重试/跳过）vs 致命（中止+回滚）。
- 每次操作生成可导出的安装报告（成功/跳过/警告/失败逐项）。
- 日志写入 `<appdata>/sdbf-installer/logs/`，便于用户反馈 issue。

## 10. 安全
- 解压防 Zip Slip（校验解出路径不逃逸目标目录）。
- 下载文件强制 SHA-1/长度校验，校验失败丢弃。
- 不内嵌密钥；用户填入的 key 本地加密存储或仅留内存。
- 卸载/覆盖前备份，杜绝误删。

---

## 11. 建议的依赖清单
- JavaFX 17（controls, fxml）——含 win/mac/linux 三平台 native classifier，打入 fat-jar
- Jackson Databind（或 Gson）
- SLF4J + Logback
- （可选）Apache Commons Compress
- Gradle + Shadow 插件（`com.github.johnrengelman.shadow`）打 fat-jar；jar 主类做 JavaFX 启动引导（避免「无 module-path 启动 FX」问题，如用 `Application.launch` 包装器）

---

## 12. 已定决策（v0.2 已落实）
1. ✅ **不附带 `cf_files.json`**：仅用 `projectID`/`fileID`，运行时经 CF 网站重定向端点解析文件名与下载（§6.3）。代价：依赖非官方端点，需 ③④ 兜底。
2. ✅ **版本号按字符串处理**：仅做字符串相等比较，不一致时提示不阻断（§5.4）。
3. ✅ **`/*` 递归**包含全部子文件与子文件夹（§4.1、§5.2）。
4. ✅ **同名文件冲突**：弹窗警告 → 旧文件移入 `SDBF_DLC/conflicts/` → 新文件占原位 → registry 记 `displaced`（§5.3、§6.2）。

## 13. 已定决策（v0.5 落实）
- ✅ **运行时/打包**：fat-jar 内置 OpenJFX，由用户已有 Java 启动（决策⑨）；**不附带启动脚本**（决策⑫）。
- ✅ **无 key 下载主路径**：`curseforge.com/api/v1/mods/{id}/files/{id}/download`（决策⑩）。
- ✅ **混合内容形式**：manifest 下载的 mod 与自带 `mods/*.jar` 共存，自带优先（决策⑪）。
- ✅ **镜像源**：MCIM `mod.mcimirror.top`，可配置下载源策略，默认源站优先、失败回退镜像（决策⑬）。
- ✅ **conflicts**：仅显示、不做处理（决策⑭）。

## 14. 仍待明确的开放问题（较小，不阻塞开发）
1. **CF/镜像 风控强度**：源站路径在上千 mod 批量请求时是否被限速/拦截、MCIM 的 UA 白名单与稳定性，均需实测，据此定并发数与退避策略。
2. **镜像 UA 登记**：MCIM 可能要求登记安装器的固定 `User-Agent`，需在上线前向 MCIM 提交。

---

## 15. 开发里程碑（建议）
- **M1 核心无 GUI**：解析两种格式 + 标准安装/卸载 + registry（CLI 可测）。
- **M2 CF 下载**：网站重定向端点 + forgecdn 续传 + 缓存 + 手动下载兜底（+ 可选 API key）。
- **M3 JavaFX GUI**：实例探测、列表、安装向导、进度。
- **M4 依赖/冲突/版本检查 + 更新流程**。
- **M5 jpackage 三平台分发 + i18n + 打磨**。
