# ddm-gui **Repository Path**: zhReimu/ddm-gui ## Basic Information - **Project Name**: ddm-gui - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-04 - **Last Updated**: 2026-07-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Steam Depot Downloader 基于 [go-steam](https://gitea.com/bslfyi89/go-steam) 的图形化下载工具,支持自动下载 Manifest 和 DepotKeys。 使用 Go 1.26.2 + [Wails v2.12.0](https://github.com/wailsapp/wails/releases/tag/v2.12.0) + Vue 3 技术栈构建,采用 DDD(领域驱动设计)分层架构,使用 [go-task](https://taskfile.dev/) 管理构建流程。 ## 功能 - **下载参数配置**:支持 App ID、Depot ID、Manifest ID 输入 - **目录选择**:支持下载目录和游戏目录(可选),指定游戏目录后将直接使用该目录 - **执行下载**:调用内置 go-steam 库在进程内执行下载操作(无需外部 DepotDownloaderMod 二进制) - **下载预估弹窗**:根据用户意图展示不同的弹窗按钮: - **执行下载**按钮:先解析 manifest 估算总文件数和字节数,弹窗提示「即将下载 N 个文件 / X MB」,按钮为「直接下载 / 取消」(用户意图就是下载,不提供「先校验」选项) - **校验完整性**按钮:先对本地文件做一轮校验(支持深度校验),统计实际需要下载的文件数和大小,弹窗根据校验结果展示不同按钮: - 有缺失/损坏文件:显示「开始下载 / 取消」,确认后增量下载(仅下载缺失/损坏文件) - 所有文件完整:显示「完成」按钮,提示用户文件全部完整无需下载 - 估算阶段不连接 Steam 网络,不下载任何 chunk - **校验完整性**:以校验模式运行,验证已下载文件的完整性。**入口始终强制启用深度校验**(chunk 级 Adler32),与原版 deoptDownloader 行为对齐,避免「大小正确但内容损坏」的文件被误判为完整导致游戏加载报错 - **深度校验**:chunk 级 Adler32 校验,对每个本地文件按 chunk offset 切片(先按 offset 升序排序,再 ReadAt 随机读,兼容乱序 manifest)计算 **SteamKit2 风格 Adler32** 与 manifest 中的 Crc 对比,损坏文件自动重新下载 - **下载完整性保障**:每个 chunk 下载成功后**强制校验解压后大小(`CbOriginal`)和 Adler32 CRC(`Crc`)**,校验失败计入 failedChunks 触发 per-chunk 故障转移,避免坏数据被静默写入磁盘 - **SteamKit2 风格 Adler32**:Steam depot chunk 的 CRC 字段由 SteamKit2 `Adler32.Calculate` 计算,初始 seed 为 0(即 s1=0, s2=0),**不是** Go 标准库 `hash/adler32.Checksum` 使用的 RFC 1950 标准(s1=1, s2=0)。本项目在 `internal/infrastructure/adler32steam.go` 中实现了与 SteamKit2 完全一致的 Adler32,校验阶段和下载阶段均使用此实现。若误用标准库,所有 chunk 都会因固定差值 `(Δs1=1, Δs2=N)` 误判为损坏 - **多CDN 故障转移**:单文件下载失败时自动切换到下一台 CDN 服务器重试,可在前端配置重试次数(默认 3) - **进度可视化**:实时进度条、下载速度、剩余时间(ETA)、已下载/总字节数、当前文件名(长路径自动截断显示父目录+basename),全部通过结构化进度事件推送前端。速度按短时窗口内累计有效下载字节增量与真实墙钟时间计算,不受并发 chunk 完成顺序影响 - **状态栏防撑爆**:长文件路径通过 CSS `text-overflow:ellipsis` + 前端 `shortenFilePath` 双重截断,保留右侧进度条可视区域 - **检测更新**:检查远端 DepotKeys 并与本地进行条目级合并 - **自动依赖管理**:自动下载 DepotKeys、Manifest - **实时日志输出**:下载过程中的实时日志显示,第三方接口错误时打印请求 URL 和原始响应 - **GitHub hosts 加速**:内置 GitHub hosts 映射,启动时自动从 [hosts.gitcdn.top](https://hosts.gitcdn.top/hosts.json) 获取最新映射,解决国内网络环境下 GitHub 域名解析失败的问题 ## 架构变更说明 本工具早期通过外部 [DepotDownloaderMod](https://gitee.com/zhReimu/DepotDownloaderMod) 二进制执行 depot 内容下载。 自当前版本起,已重构为纯 Go 实现: - **依赖**:[go-steam](https://gitea.com/bslfyi89/go-steam) 库提供 Steam 协议、CDN 下载、manifest 解析等能力 - **去除**:不再打包 `DepotDownloaderMod.7z`,不再需要 7z 解压依赖 - **去除**:不再需要调用外部进程,不再需要 GBK 解码(Windows 控制台输出场景已不存在) - **新增**:`internal/infrastructure/steamdownloader.go` 负责进程内 Steam 下载 - **新增**:`domain.SteamDownloader` 接口与 `application.SteamDownloaderFactory` 工厂模式,保证 application 层不反向依赖 infrastructure ## DepotKeys 合并机制 点击「检测更新」时,远端 DepotKeys 与本地进行**条目级合并**,而非全量覆盖: | 场景 | 处理方式 | |------|---------| | 两边都存在且一致 | 不变动 | | 远端存在、本地不存在 | 新增到本地 | | 本地存在、远端不存在 | 保留本地不变动 | | 两边都存在但不一致 | 以远端为准覆盖本地 | | 本地文件不存在 | 直接全量写入远端数据 | 合并完成后日志会输出统计信息:新增条目数、更新条目数、未变条目数、仅本地条目数、合并后本地总条目数和远端总条目数。 ## 技术栈 | 组件 | 版本 | |------|------| | Go | 1.26.2 | | Wails | v2.12.0 | | Vue | 3.5 | | Vite | 8.x | | go-task | v3 | | go-steam | bslfyi89 fork | | klauspost/compress | v1.18.6(提供 zstd 解压器) | ## 项目架构 采用 DDD(领域驱动设计)四层架构: ``` SteamDepotDownloader/ ├── main.go # 应用入口,Wails 配置,Version 变量 ├── internal/ │ ├── domain/ # 领域层:业务模型和接口 │ │ └── depot.go # LogLevel/LogEntry、HTTPDoer、SteamDownloader/Downloader 接口 │ ├── application/ # 应用层:业务用例编排 │ │ └── service.go # Service、SteamDownloaderFactory、ExecuteDownload、OpenInBrowser │ ├── infrastructure/ # 基础设施层:外部系统实现 │ │ ├── hosts.go # GitHub hosts 映射与远程更新(hosts.gitcdn.top) │ │ ├── httpclient.go # HTTP 客户端(含 hosts 加速、错误日志打印 URL 和原始响应) │ │ ├── repository.go # DownloadRepository(Downloader 接口实现、合并逻辑、本地 depot key 读取) │ │ ├── steamdownloader.go # SteamDownloader(基于 go-steam,进程内 depot 下载) │ │ ├── steamdownloader_test.go # SteamDownloader 单元测试 + Steam CM 端到端集成测试 │ │ └── archive.go # ZIP 解压工具(Manifest 文件下载后解压) │ └── adapter/wails/ # 适配器层:Wails 框架对接 │ └── app.go # App(Wails 绑定、自定义确认对话框、SteamDownloaderFactory 注入) ├── frontend/ # 前端 │ ├── src/ │ │ ├── App.vue # 主界面组件 │ │ ├── style.css # 全局样式 │ │ └── main.js # Vue 入口 │ ├── wailsjs/ # Wails 自动生成的 JS 绑定 │ └── index.html # HTML 入口 ├── build/ │ ├── appicon.png # 应用图标(512×512,透明背景) │ ├── darwin/ # macOS 构建资源 │ └── windows/ # Windows 构建资源(含 icon.ico) ├── Taskfile.yml # go-task 构建配置 ├── wails.json # Wails 项目配置 └── go.mod # Go 模块定义 ``` ### 分层职责 | 层 | 目录 | 职责 | 依赖方向 | |---|---|---|---| | 领域层 | `internal/domain/` | 定义业务模型、SteamDownloader/Downloader 接口契约 | 仅依赖 Go 标准库 | | 应用层 | `internal/application/` | 编排业务用例,通过接口调用 SteamDownloader | 依赖 domain 接口 | | 基础设施层 | `internal/infrastructure/` | 实现接口、HTTP 请求(含 GitHub hosts 加速)、文件操作、go-steam 集成 | 依赖 domain(实现接口)+ go-steam | | 适配器层 | `internal/adapter/wails/` | 对接 Wails 框架,转换事件,注入 SteamDownloaderFactory | 依赖 application + infrastructure | ### 下载流程 1. 读取本地 `depotkeys.txt`,按 depotID 查找对应解密密钥 2. 读取本地 `depot_.manifest` 文件,调用 `depot.Deserialize` 解析 3. 用 depot key 调用 `depot.DecryptFilenames` 解密 manifest 中的文件名 4. 通过 `steam.Client` 匿名登录 Steam CM 网络 5. 调用 `steamcontent.GetServersForSteamPipe` 获取 CDN 服务器列表 6. 调用 `steamcontent.GetCDNAuthToken` 获取每个 host 的鉴权 token(缓存) **2-phase 架构**(校验模式): 7. **Phase 1 - 并行校验**:用 worker 池并行校验所有本地文件 - 快速校验:仅检查文件存在性和大小(用于「直接下载」模式的下载前估算) - 深度校验(「校验完整性」入口强制启用):先按 chunk offset 升序排序,再用 `ReadAt` 按 offset 随机读,对每个切片计算 **SteamKit2 风格 Adler32**(`steamAdler32`,初始 s1=0)与 manifest 中的 Crc 对比(兼容乱序 manifest) - worker 数:`min(CPU 核数, 8, 文件总数)`,磁盘 I/O 密集型,超过 8 反而降速 - 校验失败的文件索引被收集,按升序排序 8. **Phase 2 - 下载**:仅下载缺失/损坏的文件 **单文件下载**(per-chunk 故障转移 + 并发 + 完整性校验): 9. 初始化 `pendingChunks = 该文件的所有 chunk` 10. 对每一台 CDN 服务器(最多 `MaxCDNFallback` 台): - 获取该 host 的 CDN auth token(按 host 缓存) - **并发下载** pendingChunks(默认 4 并发,Steam CDN 安全水位,避免触发限流) - 每个 chunk 下载成功后**校验 `len(data) == CbOriginal` 和 `steamAdler32(data) == Crc`**(SteamKit2 风格 Adler32),校验失败计入 failedChunks - 失败的 chunk 收集为新的 pendingChunks,切换下一台服务器继续 - 所有 chunk 成功即返回完整文件字节 11. per-chunk 故障转移避免"单 chunk 失败导致整文件重下"的浪费 12. **完整性校验**避免坏数据被静默写入磁盘,是「校验后游戏不报错」的关键防线 **进度反馈**: 13. 每个 chunk 完成时通过 `progress` 事件推送结构化进度 14. 展示速度按短时滚动窗口内的**累计有效下载字节增量 / 真实墙钟时间**计算,同一时刻完成的并发 chunk 不会放大统计结果;重试和校验失败的 chunk 不重复计数 15. ETA 基于实际下载阶段的会话平均速度(`totalBytes / downloadElapsed`)计算,连接与完整性校验耗时不计入 16. 校验阶段进度条用紫色,下载阶段用蓝色,前端可视觉区分 17. 状态栏长文件名通过 `shortenFilePath` 截断(保留父目录+basename)+ CSS `text-overflow:ellipsis` 双重防护,避免撑爆布局 ### VZstd chunk 解压实现 现代 Steam depot 默认采用 VZstd 压缩格式(基于 zstd)。一个 VZstd chunk 解密后的字节流并非纯 zstd 流,而是 Steam 私有容器: ``` [4 字节 "VSZa" 头 magic] [4 字节 header CRC32] [N 字节 zstd 压缩载荷] [4 字节 footer CRC32] [4 字节 decompressed size] [4 字节未用区] [3 字节 footer magic "zsv"] ``` 实现要点(与 SteamKit2 `VZstdUtil.Decompress` 行为一致): - `cdn.DecompressChunk` 检测到 `VSZa` magic 后,把整个容器数据传给注册的 `ZstdDecompressor` - `internal/infrastructure/steamdownloader.go` 中的 `zstdDecompressor` 负责剥离**头 8 字节 + 尾 15 字节**(共 23 字节),再把中间的纯 zstd 流交给 `klauspost/compress/zstd` 解码器 - 校验首 4 字节 == `VSZa`、末 3 字节 == `zsv`、总长度 >= 23;不校验 header/footer CRC32(与 SteamKit2 depot chunk 流程 `verifyChecksum=false` 一致) - 若剥离字节数错误(如仅剥离头 4 + 尾 4),zstd 解码器看到的首字节仍是 `'V'` (0x56) 而非 zstd magic `0x28B52FFD`,会报 `invalid input: magic number mismatch`,导致所有 VZstd chunk 在所有 CDN 服务器上全部失败 ## 开发环境搭建 ### 前置要求 - Go 1.26.2+ - Node.js 20.19+(Vite 8 要求 `^20.19.0 || >=22.12.0`) - Wails CLI v2.12.0 - [go-task](https://taskfile.dev/installation/) v3+ ### 安装 Wails CLI ```bash go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0 ``` ### 安装 go-task ```bash go install github.com/go-task/task/v3/cmd/task@latest ``` ## 构建命令 项目使用 go-task 管理构建流程,所有可用任务如下: ```bash # 构建当前平台(默认任务) task # 开发模式(热重载) task dev # 构建所有平台 task build-all # 安装依赖 task deps # 运行所有代码检查 task lint # 格式化代码 task fmt # 静态分析 task vet # 现代化写法检查 task modernize # 运行测试 task test # 运行集成测试(真实连接 Steam CM、匿名登录、获取 CDN 服务器列表) # 需要可访问 Steam CM TCP 27017 端口;沙箱/防火墙环境会自动 t.Skip go test -v -run TestSteamConnectionE2E -timeout 90s ./internal/infrastructure/ # 清理构建产物 task clean ``` ### 版本号 版本号通过 `-ldflags` 在构建时注入,默认为 `dev`。使用 git tag 标记版本后,构建会自动读取: ```bash git tag v1.0.0 task build # 构建的二进制将包含 Version = "v1.0.0" ``` ## 使用说明 1. 输入 App ID(必填) 2. 输入 Depot ID 和 Manifest ID(必填) 3. 选择下载目录或游戏目录 4. 点击「查询ID」可在浏览器中查询对应信息 5. 点击「执行下载」开始下载 6. 点击「校验完整性」验证已下载文件 7. 点击「检测更新」将远端 DepotKeys 与本地进行条目级合并(非全量覆盖)