Files
koring-launcher/DEV.md
T
dream_pep 3653e26a09 feat: 发布 v1.2.0 版本,新增多项核心功能
- 引入 electron-updater 实现自动更新功能
- 新增 Java 环境扫描与校验的 IPC 处理逻辑
- 实现离线账号登录功能
- 新增配置变更跨进程广播机制
- 重构游戏启动逻辑,使用主进程内存配置作为唯一权威来源
- 新增界面显示与语言设置的配置页面
- 添加 Windows 平台自动发布 CI 流水线
- 迁移旧版配置/认证文件到用户数据目录
- 修复崩溃日志路径、表单控件等多项 bug
- 重构设置页组件系统统一界面样式
2026-08-28 02:08:12 +08:00

25 KiB
Raw Permalink Blame History

Koring Launcher — 开发文档

技术栈

技术
Frontend React 19 + Vite 7 + TypeScript
UI Tailwind CSS v4 + shadcn/ui (base-ui)
状态管理 Zustand
Main Process Electron + Node.js / TypeScript / @xmcl/*
配置存储 YAML (js-yaml)
认证存储 JSON 文件 (koring-auth.json)
包管理 pnpm
目标平台 Windows (x64)

快速命令

pnpm install                        # 安装依赖
pnpm dev                            # 完整开发环境 (renderer + main)
pnpm dev:renderer                   # 仅前端 (Vite, port 1420)
pnpm dev:main                       # 仅主进程 (tsc + electron)
pnpm build                          # 生产构建 (vite + tsc)
pnpm dist:dev                       # dev 图标 + Windows 安装包
pnpm dist:beta                      # beta 图标 + Windows 安装包
pnpm dist:run                       # production 图标 + Windows 安装包

构建流程

开发调试

pnpm dev
  → pnpm build:main                 # 先编译主进程 TS→JS
  → concurrently:
      pnpm dev:renderer             # Vite dev server (localhost:1420, HMR)
      electron .                    # 加载 localhost:1420
  • 前端热更新(HMR
  • 主进程修改后需重启 pnpm dev
  • 调试工具:Ctrl+Shift+I 打开 DevTools

生产打包

pnpm dist:beta
  → pnpm build                      # 1. 编译 renderer + main
  → pnpm icon:beta                  # 2. 复制 public/icons/beta/ → build/
  → electron-builder --win          # 3. 读取 build/icon.ico 打包
  → 输出 dist-electron/koring-launcher-1.0.0-setup.exe

构建模式

模式 VITE_BUILD_MODE 图标目录 Badge 说明
dev "dev" public/icons/dev/ 🟢 DEV 开发预览版
beta "beta" public/icons/beta/ 🟡 BETA 测试版
run "run" public/icons/run/ 正式版

模式由 src/lib/mode.ts 导出 BUILD_MODE, isDev, isBeta, isRun, DEFAULT_BG, LOGO_SVG, APP_ICON

环境变量 (.env.*)

VITE_BUILD_MODE=dev|beta|run
VITE_APP_ICON=dev.png|beta.png|run.png
VITE_START_POP=true|false              # 启动弹窗开关
VITE_START_POP_TITLE="..."             # 弹窗标题
VITE_START_POP_INFO="..."              # 弹窗内容
VITE_START_POP_BOUTTON="..."           # 弹窗按钮文字

项目结构

koring-launcher/
├── src/                        # 前端源码
│   ├── App.tsx                 # 路由入口 + configStore 初始化
│   ├── index.css               # 全局样式 + CSS 变量 + 动画
│   ├── layouts/
│   │   └── RootLayout.tsx      # 三层布局: BackgroundLayer + ContentLayer + SystemLayer
│   ├── components/
│   │   ├── background/
│   │   │   └── BackgroundLayer.tsx    # 全屏背景层 + 视差 + 强内容遮罩
│   │   ├── system/
│   │   │   ├── TitleBar.tsx           # 自定义标题栏 (WebkitAppRegion: drag)
│   │   │   └── WindowControls.tsx     # 窗口按钮 (WebkitAppRegion: no-drag)
│   │   ├── splash/
│   │   │   └── Splash.tsx             # 启动动画 (React 组件)
│   │   ├── silk/
│   │   │   └── Silk.tsx               # WebGL 丝绸着色器 (Three.js)
│   │   ├── task/
│   │   │   ├── TaskButton.tsx         # 标题栏任务指示器
│   │   │   └── TaskCard.tsx           # 单个任务卡片
│   │   ├── ui/                        # shadcn/ui 组件
│   │   ├── setting/                   # 设置页原语(HeroUI 统一)
│   │   │   ├── SettingSurface.tsx     # 卡片唯一原语(HeroUI Surface + 磨砂 + 圆角)
│   │   │   ├── SettingCard.tsx        # 设置卡片(SettingSurface + 标准 padding
│   │   │   ├── SettingRow.tsx         # 设置行(label/desc + 控件)
│   │   │   ├── SettingBadge.tsx       # 统一徽章(neutral/primary/success/warning/info/error/violet
│   │   │   ├── SettingListItem.tsx    # 列表项行(版本行/扫描行,支持选中态)
│   │   │   ├── SectionTitle.tsx       # PageHeader/SectionTitleHeroUI Typography
│   │   │   ├── controls.tsx           # 设置控件(Select/NumberField/Switch/Radio/TextArea/FilePicker + fieldCls
│   │   │   └── Surface.tsx            # Surface 兼容别名(旧 API,样式与卡片统一)
│   │   ├── VersionCard.tsx            # 版本/更新卡片
│   │   ├── UnderConstruction.tsx      # "装修中" 占位组件
│   │   └── StartupPopup.tsx           # 启动弹窗
│   ├── stores/
│   │   ├── configStore.ts     # 统一配置 store (→ Koring.yml)
│   │   ├── themeStore.ts      # 主题 (委托 configStore)
│   │   ├── a11yStore.ts       # 无障碍 (委托 configStore)
│   │   ├── backgroundStore.ts # 背景 (委托 configStore)
│   │   ├── authStore.ts       # 认证 (→ koring-auth.json)
│   │   ├── routeStore.ts      # 路由 (历史栈)
│   │   ├── taskStore.ts       # 任务队列 (localStorage)
│   │   ├── instanceStore.ts   # 实例管理
│   │   ├── installStore.ts    # Minecraft 安装
│   │   ├── launchStore.ts     # 游戏启动
│   │   ├── modsStore.ts       # Mod 搜索
│   │   ├── updateStore.ts     # 应用更新
│   │   └── devStore.ts        # 开发者调试
│   ├── api/
│   │   ├── ipc.ts             # 核心 IPC 工具 (invoke, onIpcEvent)
│   │   ├── config.ts          # AppConfig 读写 + updateConfig(section, patch)
│   │   ├── auth.ts            # 登录 API
│   │   ├── background.ts      # 背景控制
│   │   ├── install.ts         # Minecraft 安装
│   │   ├── launch.ts          # 统一游戏启动 (launchGame / onGameEvent)
│   │   ├── java.ts            # Java 检测 (scanJava / resolveJava)
│   │   ├── mods.ts            # Mod 搜索
│   │   ├── instance.ts        # 实例 API
│   │   └── update.ts          # 应用更新
│   ├── hooks/
│   │   └── useTheme.ts         # 同步 darkMode → .dark class
│   ├── lib/
│   │   ├── mode.ts             # BUILD_MODE, DEFAULT_BG, LOGO_SVG, APP_ICON
│   │   └── utils.ts            # cn() 工具函数
│   ├── types/
│   │   └── task.ts             # Task 类型定义
│   └── pages/                  # 页面组件
├── electron/                    # Electron 主进程
│   ├── main.ts                  # 主入口, 窗口管理, splash→main 过渡
│   ├── preload.ts               # Context bridge (window.electronAPI)
│   ├── config.ts                # YAML 配置管理 (稀疏保存)
│   ├── auth.ts                  # 认证数据持久化 (JSON 文件)
│   ├── core/                    # @xmcl/* 集成
│   │   ├── auth.ts              # Microsoft OAuth, Xbox Live, MC auth
│   │   ├── installer.ts         # @xmcl/installer
│   │   ├── launcher.ts          # @xmcl/core 游戏启动
│   │   ├── launch-options.ts    # 配置→LaunchOption 映射 (parseArgs/buildLaunchOptions/resolveJavaPath)
│   │   ├── paths.ts             # 相对 gameDir 归一化 (resolveGamePath)
│   │   ├── modrinth.ts          # Modrinth/CurseForge API
│   │   └── instance.ts          # 实例管理(含 importExistingInstance sourceGamePath
│   ├── handlers/                # IPC 处理器
│   │   ├── config.ts            # 配置读写 + config:update/config:changed
│   │   ├── auth.ts              # 认证操作
│   │   ├── install.ts           # 安装操作
│   │   ├── launch.ts            # 统一游戏启动 + afterLaunch
│   │   ├── java.ts              # Java 检测 (java:scan / java:resolve)
│   │   ├── mods.ts              # Mod 操作
│   │   ├── instance.ts          # 实例操作(gamePath 统一 resolveGamePath
│   │   ├── background.ts        # 背景操作
│   │   ├── task.ts              # 任务系统
│   │   ├── system.ts            # 系统信息
│   │   └── window.ts            # 窗口控制 + splash 管理
│   └── types/
│       └── electron.d.ts        # TypeScript 声明
├── public/                      # 静态资源
│   ├── icons/
│   │   ├── dev/icon.ico, icon.png
│   │   ├── beta/icon.ico, icon.png
│   │   └── run/icon.ico, icon.png
│   ├── background.png           # 默认背景图
│   ├── koring-licon.svg         # Logo
│   └── ...
├── build/                       # 构建资源 (gitignored, 由 switch-icon.js 生成)
│   ├── icon.ico
│   └── icon.png
├── scripts/
│   └── switch-icon.js           # 图标切换脚本
├── splash.html                  # 启动动画 HTML 入口
├── electron-builder.yml         # 打包配置
├── vite.config.ts               # Vite 配置
├── tsconfig.electron.json       # 主进程 TS 编译配置
└── package.json

配置存储架构

概览

数据类型 存储位置 格式 说明
用户设置 打包:app.getPath('userData')/Koring.yml;开发:项目根 Koring.yml YAML 所有可配置项
认证数据 打包:userData/koring-auth.json;开发:项目根 JSON token/xboxProfile
崩溃日志 打包:userData/koring-crash.log;开发:项目根 JSONL 崩溃记录,max 1000 行
任务历史 localStorage koring-task-history JSON 临时,max 50

打包模式统一使用系统用户数据目录,避免安装到 Program Files 等只读目录时写入失败。 旧版「exe 旁」文件在启动时由 migrateLegacyFiles()electron/main.ts)自动复制迁移,复制不删除。

主进程权威写入模型(single source of truth

渲染进程 setX(patch) ──config:update {section, patch}──▶ 主进程 updateConfig()
                                                          ├─ 深度合并到内存缓存(getConfig()
                                                          ├─ 300ms debounce 稀疏写盘(saveConfig
                                                          └─ 广播 config:changed(完整配置)
渲染进程 onConfigChanged ──▶ configStore.applyChanged() 覆盖本地镜像
  • 主进程内存缓存(electron/config.tscurrent)是唯一权威;渲染进程不直接写盘
  • 启动游戏(launch:launch)直接读内存 getConfig(),保证永远用最新配置,无磁盘竞争。
  • 退出时 flushConfig()window-all-closed)强制写盘。
  • 认证(Koring 账户)同步进配置时同样走 updateConfig({ koringUser }) / deleteConfigKey('koringUser')

Koring.yml 结构

version: 1
oobe: true                # 首次启动引导完成标记

app:
  language: zh-CN         # zh-CN | en-US(语言包开发中,仅保存偏好 + <html lang>

theme:
  darkMode: auto          # auto | light | dark
  parallax: true

a11y:
  reduceMotion: false
  reduceTransparency: false
  highContrast: false
  contentBlurOpacity: 50  # 0-100

background:
  bgType: image           # image | color
  image: /background.png
  blur: 0                 # 0-20
  opacity: 100            # 0-100

game:
  gameDir: .minecraft
  resourceDir: ""
  savesDir: ""
  instancesDir: .minecraft/instances
  gameDirs: []            # 已添加的游戏目录列表

java:
  javaPath: ""
  memMode: auto           # auto | custom
  memGB: 4                # 1-16
  gc: auto                # auto | zgc | g1
  jvmArgs: ""

advanced:
  afterLaunch: close      # close | minimize | keep
  winMode: default        # default | fullscreen | custom
  customWidth: 854
  customHeight: 480
  gameArgs: ""
  preLaunchCmd: ""
  debugMode: false
  server:                 # 快速进入服务器(ip 空则不自动加入)
    ip: ""
    port: 25565

download:
  fileSource: mirror      # mirror | official | official-only
  versionSource: mirror
  threads: 16             # 1-64
  speedLimit: 0           # KB/s, 0=不限速

network:
  securityId:
    enabled: false
    authUrl: ""

ui:
  showInstanceTitle: true # 首页实例标题
  showTaskButton: true    # 标题栏任务队列按钮

向上兼容策略

  1. 版本号version 字段,每次结构变更递增
  2. 默认值填充 — 加载时缺失字段自动补全,不丢数据(loadConfigDEFAULTS 深度合并)
  3. 迁移函数 — 结构变更时在 migrate() 中按版本链执行
  4. 未知字段保留 — YAML 解析器保留不认识的字段
  5. Debounce 写入 — 主进程 300ms debounce 稀疏写盘,避免频繁 IO
  6. 稀疏保存diffValue(config, DEFAULTS) 只写非默认值;默认值改回后从文件移除

Electron 主进程

窗口管理

窗口 尺寸 特性
splash 480×320 无边框, 透明, 不可缩放, 居中
main 1000×700 (min 800×600) 无边框, 透明, 初始隐藏

启动流程

app.whenReady()
  → registerAllHandlers()       # 注册所有 IPC 处理器
  → createSplashWindow()        # 立即显示 splash
  → createMainWindow()          # 后台创建 main (show: false)
  → ready-to-show + 1.5s min   # 两个条件都满足后:
      → mainWindow.show()       # 显示主窗口
      → splashWindow.close()    # 关闭 splash

Mutable Win Ref

electron/main.ts 使用可变的 win 对象,所有处理器在运行时读取 win.mainWindow(而非注册时捕获):

const win: { mainWindow: BrowserWindow | null; splashWindow: BrowserWindow | null } = {
  mainWindow: null,
  splashWindow: null,
};

// 处理器中:
registerInstallHandlers(win);   // 传入 ref
// handler 内部:
win.mainWindow?.webContents.send('install:progress', data);

IPC 处理器

频道 说明
config:get / config:update / config:save 配置读写;config:update 提交 {section, patch},主进程合并 + debounce 写盘 + 广播 config:changed
config:changed(事件) 主进程广播完整配置,渲染端 configStore.applyChanged 覆盖镜像
config:preload(事件) 启动时推送初始配置 + isFirstLaunch
auth:offline-login / auth:get / auth:save / auth:delete 认证操作
install:minecraft / install:mod-loader / install:version-list 安装操作
launch:launch / launch:diagnose 统一游戏启动:{instanceName, gamePath, profile, server?};主进程读权威配置 → buildLaunchOptions@xmcl/core launch;事件经 launch:event 推送(stdout/stderr/window-ready/exit
java:scan / java:resolve Java 环境检测(JAVA_HOME/PATH/常见目录)/ 路径校验
mods:search / mods:install Mod 操作
instance:create / instance:list / instance:delete / instance:install / instance:import 实例操作(启动统一走 launch:launch
background:set-image / background:set-color / background:reset 背景操作
task:progress / task:completed 任务进度
system:info / system:open-path 系统信息 / 打开路径
window:minimize / window:maximize / window:close 窗口控制
window:openSplash / window:closeSplash Splash 管理
dialog:openFile / dialog:openFolder 文件/文件夹选择器

游戏启动链路(配置驱动)

launchStore.launch(instanceName, gamePath)
  → launchGame({instanceName, gamePath, profile, server?})   # src/api/launch.ts
  → ipc launch:launch                                        # electron/handlers/launch.ts
      → getConfig()               # 主进程内存权威配置
      → getInstanceInfo()         # 实例 runtime / path / 健康检查
      → resolveJavaPath()         # config.javaPath → 系统扫描 → PATH
      → buildLaunchOptions()      # electron/core/launch-options.ts(配置→LaunchOption 映射)
      → @xmcl/core launch() + createMinecraftProcessWatcher
      → 事件 launch:eventstdout/stderr/window-ready/exit+ playtime 累计
      → window-ready 时按 advanced.afterLaunch 处理启动器窗口(close/minimize/keep

配置 → 启动参数映射buildLaunchOptions):

配置字段 映射
java.memMode=auto 实例 minMemory/maxMemory(未设则 1024/4096
java.memMode=custom min=min(2,memGB)Gmax=memGB G
java.gc=zgc/g1 -XX:+UseZGC / -XX:+UseG1GC
java.jvmArgs 引号感知 parseArgs 并入 extraJVMArgs
advanced.gameArgs parseArgs 并入 extraMCArgs
advanced.winMode resolutionfullscreen / custom 宽高)
advanced.preLaunchCmd prependCommandWindows 批处理需 cmd /c 前缀)
advanced.debugMode -Dkoring.debugMode=true + 日志流
profile / server gameProfile+accessToken / server

Zustand Stores

configStore (主进程权威模型的渲染端镜像)

配置的渲染端镜像。所有 setter 只向主进程提交 {section, patch}config:update),不直接写盘;主进程合并后广播 config:changedapplyChanged 以广播为准覆盖本地。

config: AppConfig       // 完整配置(镜像)
loaded: boolean         // 是否已加载

init()                  // 兜底:从主进程加载配置(config:get)
applyPreloaded()        // 启动预载(config:preload
applyChanged()          // 主进程广播覆盖(config:changed
setTheme(patch)         // 乐观更新本地 + submit("theme", patch)
setA11y(patch)
setBackground(patch)
setGame(patch)
setJava(patch)
setAdvanced(patch)
setDownload(patch)
setNetwork(patch)
setInstances(list)      // 数组整体替换
setOobe(value)

themeStore (委托 configStore)

darkMode: "auto" | "light" | "dark"
parallax: boolean
setDarkMode(mode)       // 更新 DOM + configStore
setParallax(v)          // configStore
syncThemeFromConfig()   // 启动时从 config 同步

backgroundStore (委托 configStore)

type: "image" | "color", image, blur, opacity
setImage / setColor / setBlur / setOpacity / reset
syncBackgroundFromConfig()

authStore (委托 JSON 文件)

user: AuthResult | null
initFromFile()          // 从 koring-auth.json 加载
loginOffline(username)  // 通过主进程 + 保存到文件
logout()                // 清除文件

其他 Store

  • routeStore — 历史栈导航
  • taskStore — 任务队列 (localStorage)
  • devStore — 开发者调试 (内存)
  • installStore — Minecraft 安装 (内存)
  • launchStore — 游戏启动 (内存)
  • modsStore — Mod 搜索 (内存)
  • updateStore — 应用更新 (内存)
  • instanceStore — 实例管理

页面路由

顶层路由 (标题栏可见)

Key Label 组件 说明
home 首页 pages/home/index.tsx StartCard 启动组件
store 资源 pages/store/index.tsx 🚧 装修中
today 资讯 pages/today/index.tsx 🚧 装修中
play-link 联机 pages/play-link/index.tsx 🚧 装修中
setting 设置 pages/setting/index.tsx 侧边栏 + 内容区

隐藏路由

Key Label 组件
task-queue 任务队列 pages/task-queue.tsx
debug 调试 pages/debug/index.tsx
debug-splash 启动动画调试 pages/debug/splash-debug.tsx
debug-display 显示效果调试 pages/debug/display-debug.tsx
debug-version-card 版本卡片调试 pages/debug/version-card-debug.tsx
debug-task 任务队列调试 pages/debug/task-debug.tsx

核心组件

三层布局 (RootLayout)

z-0   BackgroundLayer     全屏背景图 + 视差 + 模糊 + 强内容遮罩
z-1   ContentLayer        页面内容区 (top: 40px, overflow-auto)
z-100 SystemLayer         自定义标题栏 (TitleBar)
z-200 StartupPopup        启动弹窗 (环境变量控制)

TitleBar 标题栏

三种模式:

  • default: 左侧品牌文字 + 中间胶囊菜单 (可拖拽切换) + 右侧窗口控制
  • sub: 左侧返回按钮 + 品牌文字 + 右侧窗口控制 (隐藏 TaskButton)
  • window: 仅窗口控制

WindowControls 窗口控制

使用 <button> 元素,CSS WebkitAppRegion: "no-drag" 实现按钮可点击。

BackgroundLayer 背景层

  • 支持 image (CSS background-image) 和 color (CSS background-color) 两种类型
  • 视差效果: 鼠标移动时背景偏移 ±20px, scale(1.05)
  • 强内容遮罩: 非 home 页面自动显示 (可通过 contentBlurOpacity 控制)
  • 深色模式叠加层: bg-black/35

TaskQueue 任务系统

  • 执行器模式: addTask(type, title, desc, async (ctx) => {...})
  • 支持并行执行 (多个任务同时运行)
  • AbortController 取消机制
  • localStorage 持久化历史 (max 50)
  • 任务类型: install / download / update / launch / auth / sync / custom

设置原语与控件规范(HeroUI 3

所有设置子页面共用 src/components/setting/ 原语,一套样式组合

原语 说明
SettingSurface 卡片唯一原语(HeroUI Surface + 磨砂 blur + rounded-xl + 统一边框/背景)
SettingCard 设置卡片(= SettingSurface + px-5 py-4
SettingRow 设置行(label/desc 用 HeroUI Typography + 右侧控件)
SectionTitle / PageHeader HeroUI Typography.Heading / Paragraph
SettingBadge 统一徽章:neutral / primary / success / warning / info / error / violet
SettingListItem 列表项行(版本行/扫描行,支持 selected 高亮)
SettingSelect / SettingNumberField / SettingSwitch / SettingRadioGroup / SettingTextArea / SettingFilePicker HeroUI 控件封装
fieldCls 输入框统一样式类(见下方注意)

HeroUI 3 控件要点(易踩坑)

  1. 事件用 onChange 而非 onValueChange——HeroUI 3 基于 react-ariaonValueChange 不存在且静默失效(全项目已统一修复)。
  2. Radio 必须显式渲染圆点<Radio.Control><Radio.Indicator /></Radio.Control>,否则无选中指示器。
  3. field 默认无边框HeroUI 默认主题 --field-border-width: 0pxInput/TextArea/NumberField/Radio 需叠加 fieldCls(或等价边框类)保证视觉完整;Select.Trigger 已内置边框类。
  4. Select 单选selectedKey + onSelectionChange(回调可能是 Key | nullSet<Key>,控件内已做兼容)。
  5. NumberFieldGroup 内需显式渲染 Increment/Decrement 按钮。

游戏目录版本识别(设置页 → 导入)

  • 扫描任意目录(主目录 / 已添加目录)→ instance:scan-dirscanGameDirectories(读 versions/ 子目录,检测 JSON/JAR 健康度与 Forge/Fabric/Quilt/OptiFine 加载器)。
  • 导入源 = 当前扫描目录handleImport/handleImportAllscanTarget 作为 sourceGamePath,实例仍创建在主库 gameDir/instances/
  • 主目录变更自动重扫useEffect([gameDir])),旧结果先清空。
  • 批量导入幂等:先取 listInstances 名集合,已存在实例跳过并计入「跳过」。
  • 相对 gameDir 归一化electron/core/paths.tsresolveGamePath 在 IPC 边界(instance/launch/task)统一应用,相对路径按 exe 目录(打包)/ 项目根(开发)解析,与 .minecraft 创建位置一致。

设置子页面状态(参考 PCL2 组织)

分组 页面 状态 对接
通用 主页 / Koring 账户 / 关于 / 版权 保留
游戏 游戏账户&档案 实装 auth:offline-login(离线账号);微软按钮占位
游戏 Java虚拟机与内存 实装 java:scan / java:resolve / java.*
游戏 游戏目录 实装 instance:scan-dir / instance:importsourceGamePath
游戏 高级设置 实装 advanced.*(含快速进入服务器 advanced.server
个性化 主题与背景 实装 theme.* / background.*
个性化 主界面 实装 ui.showInstanceTitle / ui.showTaskButton(应用到 InstanceTitle / WindowControls
个性化 语言 实装 app.language(保存 + <html lang>;语言包开发中)
个性化 辅助功能 实装 a11y.*
网络 下载 实装 download.*
网络 安全识别服务 实装 network.securityId
网络 以太联机 / 陶瓦联机 占位 无后端接口
其他 服务与反馈 / 赞助我们 / 开发者选项 保留