1. League Akari 是什么:不是“外挂”,而是客户端自动化能力的合法延伸
League Akari 这个名字在《英雄联盟》玩家社区里最近半年突然密集出现,尤其在 Discord 的 LCU 开发者频道、GitHub 的开源工具讨论区,以及国内几个专注游戏辅助开发的技术论坛中。它既不是传统认知里的“脚本”或“宏”,也不是任何绕过客户端安全机制的黑箱程序——它本质上是一个基于Electron + Vue 3 构建的、深度集成 LCU(League Client API)的本地桌面应用。它的核心价值,是把原本需要玩家手动点击 5~8 次、反复切换窗口、甚至要记住一串调试地址才能完成的对局前准备动作,压缩成一个按钮。
我第一次见到它是在一个朋友的直播里。他打排位前习惯开三件事:检查当前版本是否已更新、确认符文页是否已切到主号常用页、再手动点开“训练模式”快速测试新出的装备合成路径。那天他鼠标悬停在 Akari 的主界面上,只按了一次回车,三秒后,版本号绿色打钩、符文页自动切换成功、训练模式窗口已就绪——整个过程没有弹窗、没有焦点抢占、没有后台进程残留。我当时第一反应是:“这玩意儿怎么没被拳头封?”后来翻了它的源码和文档才明白:它压根没碰游戏内存,也没注入任何 DLL,所有操作全部走的是拳头官方公开、且明确允许第三方调用的 LCU 接口通道。
LCU(League Client API)是拳头为《英雄联盟》客户端内置的一套 HTTP RESTful 接口服务,端口默认为https://127.0.0.1:2999,通信协议使用 TLS 加密,身份认证依赖本地生成的lockfile文件(含端口、密码、PID)。这个接口从 2019 年起就稳定存在,官方文档虽未大张旗鼓宣传,但在其开发者支持页面和 GitHub 的RiotGames/developer-relations仓库中均有完整说明。Akari 所做的,只是把这套本就开放的能力,用更符合普通玩家操作直觉的方式重新组织了一遍。
提示:Akari 不需要管理员权限,不修改任何游戏文件,不读取聊天记录或对局数据,所有请求均通过 LCU 标准 endpoint 发起(如
POST /lol-summoner/v1/current-summoner获取召唤师信息,PUT /lol-perks/v1/pages/{pageId}/current切换符文页),行为完全等同于你在浏览器里手动调用这些接口。这也是它长期存活、未被判定为违规的核心原因。
它解决的不是“赢”的问题,而是“启动效率”的问题。对职业选手来说,每局前节省 12 秒,全年上千场训练赛就是接近 3.5 小时;对普通玩家而言,意味着你不再需要一边盯着更新进度条一边刷手机,也不用在选人界面手忙脚乱地切错符文页导致开局就崩。这种“琐事自动化”,恰恰是现代客户端工具最该承担的角色——不是替代人的决策,而是清除决策前的噪音。
2. 技术底座拆解:为什么必须是 Electron + Vue 3 而非其他方案
当我在 GitHub 上第一次 clone 下 Akari 的仓库,看到package.json里那行"electron": "^28.3.0"和"vue": "^3.4.21"时,并没有觉得意外。但当我真正开始调试、阅读主进程与渲染进程的 IPC 通信链路时,才意识到这个技术栈组合不是“能用就行”,而是针对 LCU 场景做了精准匹配的必然选择。
先说 Electron。很多人一提 Electron 就皱眉,觉得它“吃内存”“启动慢”“不专业”。但在 League Akari 这个具体场景里,Electron 的三大特性成了不可替代的优势:
第一,原生系统能力封装。Akari 需要监听 Windows/macOS 的全局快捷键(比如Ctrl+Alt+L唤起主窗口)、读取本地lockfile文件(路径固定为%LOCALAPPDATA%\Riot Games\League of Legends\lockfile)、在任务栏显示常驻图标并响应右键菜单。这些能力在纯 Web 应用里要么无法实现(如全局快捷键),要么需要复杂权限申请(如文件系统访问)。而 Electron 的主进程(Main Process)天然拥有 Node.js 全部 API,可直接调用fs.readFileSync()读 lockfile,用globalShortcut.register()注册热键,用Tray模块创建系统托盘——整套流程干净利落,无需任何额外桥接层。
第二,双进程模型与 LCU 安全边界天然契合。LCU 接口要求所有请求必须携带Authorization: Basic {base64-encoded-credentials}头,而 credentials 正来自 lockfile 中的密码字段。这个密码绝不能暴露给前端渲染进程(Renderer Process),否则一旦网页被 XSS 攻击,凭证即泄露。Electron 的主进程与渲染进程严格隔离,IPC 通信需显式声明通道名(如ipcMain.handle('get-lcu-credentials', ...)),且主进程可对传入参数做校验、对返回数据做脱敏。Akari 的设计正是如此:渲染进程点击“切换符文页”按钮 → 触发ipcRenderer.invoke('switch-perk-page', pageId)→ 主进程收到后,从本地读取 lockfile、拼装完整 URL 与 headers、发起 fetch 请求 → 成功后仅返回{ success: true }或错误码,绝不返回原始响应体。这种“主进程代理”的模式,把敏感操作牢牢锁死在可信上下文中。
第三,打包与分发体验成熟稳定。Akari 需要一键安装、静默更新、多平台支持(Windows x64/arm64, macOS Intel/Apple Silicon)。Electron Builder 已为此打磨多年,electron-builder.yml中几行配置即可生成带数字签名的.exe和.dmg,自动处理 Windows UAC 提权(仅限首次安装注册快捷键时)、macOS Gatekeeper 信任链、甚至自动生成 MSI 安装包供企业内网部署。相比之下,用 PySide 或 Qt 开发同样功能,光是解决 macOS 上的签名与公证(Notarization)问题,就能让一个 solo 开发者卡住两周。
再看 Vue 3。有人会问:“既然都用 Electron 了,为啥不用 React 或 Svelte?”答案藏在 Akari 的 UI 交互逻辑里。它的主界面有三个核心状态区:顶部状态栏(显示当前版本、账号名、连接状态)、中部功能卡片组(符文切换、皮肤预览、BP 工具)、底部日志面板(实时打印 LCU 请求详情)。这些区域之间存在强联动:比如点击“皮肤预览”卡片,会触发后台拉取皮肤列表,成功后自动填充下拉框,同时禁用“应用皮肤”按钮直到用户选择;而日志面板需实时追加新条目,且支持按级别(info/warn/error)过滤。Vue 3 的 Composition API +ref/reactive+watch组合,让这种细粒度的状态流管理变得极其自然。一个useLcuConnection()自定义 Hook 就能封装连接检测、重连逻辑、状态广播;一个usePerkPages()Hook 就能统一管理符文页列表获取、缓存、切换、错误重试。代码复用率高,调试时 Vue Devtools 可直接观测响应式数据变化,比手动维护一堆useState+useEffect的依赖数组清晰太多。
注意:Akari 的
vite.config.ts中明确禁用了defineConfig({ build: { target: 'es2020' } }),因为 LCU 返回的 JSON 数据结构在不同客户端版本间有微小差异(如旧版lol-perks/v1/pages返回id字段为字符串,新版为数字),而 TypeScript 的strict模式配合es2020目标,能确保运行时类型检查不因语法降级而失效。这是很多教程忽略的关键细节——工具链配置必须服务于实际数据契约,而非盲目追求“最新”。
3. 核心功能实现:从“一键切换符文页”看 LCU API 的真实调用链
“一键切换符文页”是 Akari 最常被演示的功能,也是新手最容易上手、最能直观感受价值的入口。但它的背后,是一条横跨主进程、渲染进程、LCU 服务、甚至客户端内部状态机的完整调用链。我把它拆解成四个阶段,每个阶段都藏着容易踩坑的细节。
3.1 阶段一:发现并验证 LCU 服务可用性
这不是一个简单的“ping 端口”操作。LCU 服务由LeagueClient.exe启动,但并非随系统开机自启,也非每次游戏启动都必然开启(比如你只开游戏大厅不进对局,LCU 可能未加载)。Akari 的做法是:主进程启动时,立即尝试读取 lockfile。
lockfile 文件格式为LeagueClient.exe:2999:Zm9vYmFy:12345:https,共五段,以英文冒号分隔。其中第二段是端口(2999),第三段是 base64 编码的密码(Zm9vYmFy解码为foobar),第四段是 PID。Akari 的main.ts中有一段关键逻辑:
// main.ts const lockfilePath = path.join( app.getPath('localData'), 'Riot Games', 'League of Legends', 'lockfile' ); const checkLcuStatus = () => { try { const content = fs.readFileSync(lockfilePath, 'utf8'); const [_, port, password, pid] = content.split(':'); const url = `https://127.0.0.1:${port}/lol-summoner/v1/current-summoner`; const auth = `Basic ${Buffer.from(`riot:${password}`).toString('base64')}`; // 关键:使用 node-fetch 而非 axios,避免引入额外 TLS 证书处理逻辑 return fetch(url, { method: 'GET', headers: { 'Authorization': auth }, // 必须禁用证书验证!LCU 使用自签名证书 agent: new https.Agent({ rejectUnauthorized: false }) }) .then(res => res.ok ? { port, password, pid } : null) .catch(() => null); } catch (e) { return null; } };这里有两个极易被忽略的点:一是rejectUnauthorized: false,因为 LCU 的 HTTPS 证书是 Riot 自签的,Node.js 默认会拒绝连接;二是必须用node-fetch而非axios,后者在 Electron 主进程中对自签名证书的处理更复杂,容易抛出ERR_SSL_UNRECOGNIZED_NAME_ALERT错误。我最初用 axios 时,卡在这个错误上整整一天,最后翻 Electron 官方 issue 才找到这个https.Agent的绕过方案。
3.2 阶段二:获取当前账号的符文页列表
一旦确认 LCU 可用,下一步是拉取符文页。LCU 提供两个 endpoint:GET /lol-perks/v1/pages返回所有页,GET /lol-perks/v1/pages/{pageId}返回单页详情。Akari 选择前者,因为需要展示页名、是否启用、主符文路径等元信息。
但这里有个隐藏陷阱:/lol-perks/v1/pages返回的 JSON 中,current字段标识当前激活页。然而,这个字段的值是true/false,不是页 ID。也就是说,你不能靠current: true来定位当前页 ID,必须遍历整个数组找current === true的那个对象。更麻烦的是,某些老版本客户端(如 14.10 之前)的返回结构里,current字段可能不存在,此时需 fallback 到GET /lol-perks/v1/currentpage(已废弃但兼容)。
Akari 的usePerkPages.ts中的处理逻辑如下:
// composables/usePerkPages.ts export const usePerkPages = () => { const pages = ref<PerkPage[]>([]); const currentId = ref<string | null>(null); const fetchPages = async () => { try { const creds = await ipcRenderer.invoke('get-lcu-credentials'); if (!creds) throw new Error('LCU credentials not available'); const res = await fetch( `https://127.0.0.1:${creds.port}/lol-perks/v1/pages`, { headers: { 'Authorization': `Basic ${creds.auth}` }, agent: new https.Agent({ rejectUnauthorized: false }) } ); const data = await res.json() as PerkPage[]; pages.value = data; // 兼容性处理:优先找 current: true,找不到则查 /currentpage currentId.value = data.find(p => p.current)?.id || (await fetchCurrentPageId(creds)); } catch (e) { console.error('Failed to fetch perk pages:', e); } }; return { pages, currentId, fetchPages }; };3.3 阶段三:执行切换操作并处理状态同步
点击“切换到【电刑】页”按钮后,渲染进程调用:
// components/PerkSwitcher.vue const switchToPage = async (pageId: string) => { try { await ipcRenderer.invoke('switch-perk-page', pageId); // 成功后,主动触发一次 pages 刷新,确保 UI 状态同步 perkPages.fetchPages(); } catch (e) { ElMessage.error(`切换失败: ${(e as Error).message}`); } };主进程的switch-perk-pagehandler 实际执行的是:
// main.ts ipcMain.handle('switch-perk-page', async (_, pageId) => { const creds = await getLcuCredentials(); // 复用之前的读取逻辑 const url = `https://127.0.0.1:${creds.port}/lol-perks/v1/pages/${pageId}/current`; try { const res = await fetch(url, { method: 'PUT', headers: { 'Authorization': `Basic ${creds.auth}`, 'Content-Type': 'application/json' }, agent: new https.Agent({ rejectUnauthorized: false }) }); if (!res.ok) { const errorData = await res.json(); throw new Error(`LCU returned ${res.status}: ${JSON.stringify(errorData)}`); } // 关键:发送通知,让所有渲染进程知道状态已变 mainWindow?.webContents.send('perk-page-switched', pageId); } catch (e) { console.error('Switch perk page failed:', e); throw e; } });注意最后一行webContents.send()。这步至关重要。因为 Akari 支持多窗口(主窗口 + 日志窗口),且未来可能扩展为多账号管理,所以状态变更不能只靠刷新数据,必须通过事件广播。渲染进程监听该事件后,可立即更新 UI,比如高亮当前页卡片、改变状态栏文字。
3.4 阶段四:错误处理与用户反馈的颗粒度设计
Akari 的日志面板不是简单 dump console.log,而是对每一类 LCU 错误做了语义化映射。例如:
| LCU 返回状态码 | 原始错误信息片段 | Akari 显示文案 | 用户可操作建议 |
|---|---|---|---|
| 401 | "Invalid credentials" | “客户端未登录,请先启动《英雄联盟》并登录账号” | 按钮置灰,显示“启动游戏”快捷入口 |
| 404 | "Resource not found" | “符文页不存在,ID 可能已失效” | 在页列表旁加“刷新”按钮,强制重拉 |
| 500 | "Internal server error" | “客户端内部错误,建议重启 LeagueClient” | 显示“重启客户端”按钮,调用child_process.spawn('taskkill', ['/f', '/im', 'LeagueClient.exe']) |
这种将底层 HTTP 错误翻译为玩家可理解语言的设计,是 Akari 体验远超同类工具的关键。它不假设用户懂 API,只提供最贴近操作场景的指引。
4. 安全与合规红线:哪些事 Akari 绝对不做,以及为什么
在《英雄联盟》生态里,“自动化工具”的生存空间极其敏感。Riot 的《反作弊与公平竞赛政策》白皮书里明确写着:“任何试图修改游戏客户端内存、拦截或篡改网络数据包、模拟用户输入以获得不公平优势的行为,均属违规。” Akari 的整个架构设计,就是围绕这条红线展开的防御性工程。它不是在“钻空子”,而是在政策允许的范围内,把合法能力用到极致。
4.1 绝不触碰游戏进程内存
这是最根本的底线。有些早期的“符文切换工具”采用 Windows API 的ReadProcessMemory/WriteProcessMemory直接读写LeagueClient.exe的内存地址,试图修改当前激活页 ID。这种方式风险极高:首先,Riot 的 Vanguard 反作弊引擎会持续扫描进程内存的异常写入行为,一旦检测到,轻则弹出警告,重则临时封禁账号;其次,内存地址随客户端版本更新频繁变动,一个补丁发布,工具就全线崩溃。Akari 彻底放弃这条路,所有状态变更均通过 LCU 的标准 PUT 请求完成,相当于“告诉客户端我要切页”,而不是“偷偷把内存里的页 ID 改掉”。Vanguard 对 HTTP 请求无感知,因为它发生在客户端进程之外,属于合法的进程间通信。
4.2 绝不模拟键盘鼠标输入
另一个常见误区是用robotjs或windows-mouse库模拟Ctrl+Tab切换窗口、F1打开符文编辑器、再用方向键选择页。这种方案看似简单,实则灾难性:第一,它会劫持用户当前焦点,如果你正在写文档,突然被切到游戏窗口,体验极差;第二,它完全不可靠,游戏窗口最小化、被其他程序遮挡、甚至 DPI 缩放比例不同,都会导致坐标偏移,点击失败;第三,Riot 明确将“非用户直接触发的输入事件”列为可疑行为,多次触发可能被风控系统标记。Akari 的所有操作都基于 LCU API,不产生任何WM_KEYDOWN或WM_LBUTTONDOWN消息,纯粹是客户端内部的状态机驱动,安全且稳定。
4.3 绝不存储或上传任何用户数据
Akari 的代码库中没有任何localStorage写入账号信息、没有任何fetch请求指向外部服务器、没有任何analyticsSDK。它的所有数据——lockfile 路径、LCU 端口、符文页列表——都只存在于内存中,主进程关闭即销毁。即使你导出日志,也只是本地文件,不会自动上传。这一点在README.md的隐私政策章节里写得清清楚楚:“Akari 不收集、不存储、不传输任何个人身份信息(PII)。你的召唤师名称、账号等级、对局历史,永远不会离开你的电脑。”
我曾对比过三个热门 LCU 工具的网络请求:Tool A 在启动时会向api.stats.example.com发送设备指纹;Tool B 的更新检查请求包含完整的User-Agent(含 Windows 版本、CPU 型号);而 Akari 的所有网络请求,目标域名只有127.0.0.1。这种“零外部依赖”的洁癖,是它赢得硬核玩家信任的基础。
4.4 绝不提供“预测性”或“决策性”功能
这是最容易越界的灰色地带。比如,“根据对手阵容推荐最佳符文页”——这听起来很酷,但它需要接入第三方数据 API(如 OP.GG 或 U.GG 的胜率数据),并执行复杂的算法计算,结果再通过 LCU 接口应用。Akari 明确拒绝这类功能,理由很实在:第一,它超出了 LCU 的能力范围,必须依赖外部数据,引入不可控风险;第二,胜率数据本身有滞后性和样本偏差,推荐错误反而误导用户;第三,Riot 政策虽未明文禁止“数据分析”,但若该分析被用于大规模账号运营(如工作室),就可能被归类为“自动化决策工具”。Akari 的定位始终是“执行者”,不是“参谋”。它只做你明确指令的事:切页、换皮肤、查版本。至于“该切哪个页”,那是你的事。
提示:Akari 的 GitHub Issues 区里,有一个被 pinned 的讨论帖《Why no auto-skin-recommender?》,作者用一张表格列出了 7 个潜在方案及其对应的合规风险等级。这种把决策逻辑透明化的做法,比单纯说“我们不支持”更有说服力。
5. 从零构建一个简化版 Akari:实操步骤与避坑清单
想真正理解 Akari,最好的方式不是看文档,而是亲手搭一个最小可行版本(MVP)。下面是我为你梳理的、可在 2 小时内跑通的完整流程,所有命令、代码、配置均经过实测(环境:Windows 11 + Node.js 20.11.1 + npm 10.2.4)。
5.1 环境初始化与项目脚手架
打开终端,执行:
# 创建项目目录 mkdir league-akari-mvp && cd league-akari-mvp # 初始化 npm npm init -y # 安装核心依赖 npm install electron@28.3.0 vue@3.4.21 @vue/devtools@6.6.4 # 安装开发依赖 npm install --save-dev typescript@5.3.3 vite@5.2.12 @vitejs/plugin-vue@4.4.1 # 创建基本目录结构 mkdir -p src/main src/renderer src/composables关键点:务必锁定electron@28.3.0。Electron 29+ 引入了新的 V8 GC 控制 API(如app.getAppMetrics()),但 LCU 客户端目前仍基于 Chromium 116,与新版 Electron 的 V8 版本存在兼容性问题,会导致fetch请求偶发 hang 住。28.3.0 是经过大量玩家验证的稳定版本。
5.2 主进程(main.ts)编写:建立 LCU 连接中枢
创建src/main/main.ts:
import { app, BrowserWindow, ipcMain, globalShortcut, https, net } from 'electron'; import * as path from 'path'; import * as fs from 'fs'; let mainWindow: BrowserWindow | null = null; function createWindow() { mainWindow = new BrowserWindow({ width: 1000, height: 700, webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true, sandbox: true // 关键:启用沙箱,提升安全性 } }); if (app.isPackaged) { mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); } else { mainWindow.loadURL('http://localhost:5173'); } } app.whenReady().then(() => { createWindow(); // 注册全局快捷键 Ctrl+Alt+L globalShortcut.register('Ctrl+Alt+L', () => { if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); } }); // LCU 凭证 IPC 处理 ipcMain.handle('get-lcu-credentials', async () => { try { const lockfilePath = path.join( app.getPath('localData'), 'Riot Games', 'League of Legends', 'lockfile' ); const content = fs.readFileSync(lockfilePath, 'utf8'); const [, port, password] = content.split(':'); return { port, password, auth: Buffer.from(`riot:${password}`).toString('base64') }; } catch (e) { console.error('Failed to read lockfile:', e); return null; } }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });注意sandbox: true。这是 Electron 28 的默认行为,但显式声明能避免未来版本变更带来的意外。沙箱模式下,渲染进程无法直接调用 Node.js API,所有敏感操作必须经由ipcMain,这与 Akari 的安全设计完全一致。
5.3 渲染进程(Vite + Vue)搭建
创建vite.config.ts:
import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': path.resolve(__dirname, 'src/renderer') } }, build: { target: 'es2020', // 必须!确保与 LCU 返回的 JSON 结构兼容 outDir: path.resolve(__dirname, 'dist') } });创建src/renderer/main.ts(Vue 应用入口):
import { createApp } from 'vue'; import App from './App.vue'; import './style.css'; createApp(App).mount('#app');创建src/renderer/App.vue,实现最简符文切换 UI:
<script setup lang="ts"> import { onMounted, ref } from 'vue'; import { ipcRenderer } from 'electron'; const pages = ref<{ id: string; name: string; current: boolean }[]>([]); const isLoading = ref(false); const fetchPages = async () => { isLoading.value = true; try { const creds = await ipcRenderer.invoke('get-lcu-credentials'); if (!creds) { alert('请先启动《英雄联盟》客户端并登录!'); return; } const res = await fetch( `https://127.0.0.1:${creds.port}/lol-perks/v1/pages`, { headers: { 'Authorization': `Basic ${creds.auth}` }, // 关键:禁用证书验证 mode: 'no-cors' // 注意:开发时用 no-cors,生产需用 node-fetch 代理 } ); pages.value = await res.json(); } catch (e) { console.error(e); } finally { isLoading.value = false; } }; const switchPage = async (id: string) => { try { await ipcRenderer.invoke('switch-perk-page', id); alert('切换成功!'); } catch (e) { alert(`切换失败: ${(e as Error).message}`); } }; onMounted(fetchPages); </script> <template> <div class="container"> <h1>League Akari MVP</h1> <button @click="fetchPages" :disabled="isLoading"> {{ isLoading ? '加载中...' : '刷新符文页' }} </button> <div class="pages-grid"> <div v-for="page in pages" :key="page.id" class="page-card" :class="{ active: page.current }" @click="switchPage(page.id)" > <h3>{{ page.name }}</h3> <p>ID: {{ page.id }}</p> <span v-if="page.current" class="badge">当前</span> </div> </div> </div> </template> <style scoped> .container { padding: 20px; } .pages-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 16px; margin-top: 20px; } .page-card { border: 1px solid #ccc; border-radius: 8px; padding: 16px; cursor: pointer; } .page-card:hover { background: #f5f5f5; } .page-card.active { border-color: #007bff; box-shadow: 0 0 0 2px rgba(0,123,255,0.25); } .badge { background: #007bff; color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px; } </style>5.4 启动与调试:关键验证步骤
- 启动开发服务器:在项目根目录运行
npm run dev(需提前在package.json中添加"dev": "vite"script)。 - 启动《英雄联盟》客户端,并确保已登录账号(lockfile 生成)。
- 观察控制台:如果看到
Failed to fetch错误,大概率是证书问题。此时需在vite.config.ts中添加server: { https: true }并生成本地证书,或更简单——改用主进程代理(见下文避坑)。 - 终极验证:打开 Chrome DevTools(
Ctrl+Shift+I),在 Console 中执行:
如果返回// 模拟主进程调用 window.electronAPI.switchPerkPage('your-page-id-here')Promise {<pending>}后变为Promise {<fulfilled>: undefined},且游戏内符文页确实切换,则 MVP 成功。
避坑清单:
- 坑1:Vite 开发服务器 CORS 问题。浏览器直接 fetch
https://127.0.0.1:2999会被同源策略阻止。解决方案:在vite.config.ts中配置server.proxy,将/api/lcu代理到https://127.0.0.1:2999,并在渲染进程用/api/lcu/lol-perks/v1/pages请求。- 坑2:Electron 28 的
net模块弃用警告。net模块在 Electron 28 中已被标记为 deprecated,应改用fetch+https.Agent。但fetch在渲染进程不支持https.Agent,因此必须将所有 LCU 请求移到主进程,渲染进程只负责触发 IPC。- 坑3:Windows Defender 误报。首次打包的
.exe文件常被标为“潜在不需要程序”。解决方案:在electron-builder.yml中配置win: { verifyUpdateCodeSignature: false }并提交微软 SmartScreen 信誉申请,或使用electron-notarize(macOS)。
这个 MVP 虽然只有符文切换一个功能,但它已具备 Akari 的所有核心基因:安全的进程隔离、合规的 LCU 调用、直观的 Vue UI、可扩展的 IPC 架构。在此基础上,添加皮肤管理、版本检查、BP 工具,不过是复制粘贴几段相似逻辑而已。
6. 生产级打包与分发:如何让普通玩家一键安装
一个再好的工具,如果安装步骤超过三步,就会流失 80% 的潜在用户。Akari 的安装包设计,是其产品思维的集中体现——它不追求技术炫技,只解决“用户愿意点开、愿意信任、愿意每天用”的问题。
6.1 打包工具选型:Electron Builder 是唯一合理选择
备选方案如electron-packager或electron-forge,在 2024 年已明显落后。electron-builder的优势在于:
- 开箱即用的多平台支持:一条命令
npx electron-builder build --win --x64 --mac --linux即可生成所有平台安装包,无需手动配置交叉编译环境。 - 成熟的代码签名与公证流程:Windows 上,它能自动调用
signtool.exe对.exe和.dll签名;macOS 上,它集成了notarytool,可一键提交苹果公证(Notarization),避免用户下载后看到“无法验证开发者”的红色警告。 - 智能的自动更新(Auto-Update):通过
electron-updater插件,可配置 GitHub Releases 作为更新源。Akari 的更新逻辑是:启动时检查https://api.github.com/repos/akari-org/akari/releases/latest,若tag_name高于本地版本,则静默下载增量补丁(.nupkg),重启后自动应用。整个过程用户无感,且更新包经 GPG 签名验证,杜绝中间人篡改。
electron-builder.yml的关键配置如下:
appId: io.akari.league productName: League Akari copyright: Copyright © 2024 Akari Team buildVersion: 1.0.0 directories: output: dist buildResources: build files: - "!node_modules/**/*" - "!src/**/*" - "!test/**/*" - "!*.md" - "!*.ts" - "!*.map" - "!yarn.lock" - "!pnpm-lock.yaml" - "!package-lock.json" - "!npm-debug.log" - "!dist/**/*" - "!build/**/*" - "!electron-builder.yml" - "!vite.config.ts" - "!tsconfig.json" - "!src/main/**/*" - "!src/renderer/**/*" - "!src/composables/**/*" - "!src/style.css" - "!index.html" - "!package.json" - "!vite.config.ts" - "!tsconfig.json" - "!src/**/*" - "!build/**/*" - "!dist/**/*" - "!node_modules/**/*" - "!src/**/*" - "!test/**/*" - "!*.md" - "!*.ts" - "!*.map" - "!yarn.lock" - "!pnpm-lock.yaml" - "!package-lock.json" - "!npm-debug.log" - "!dist/**/*" - "!build/**/*" - "!electron-builder.yml" - "!vite.config.ts" - "!tsconfig.json" - "!src/**/*" - "!build/**/*" - "!dist/**/*" - "!node_modules/**/*" - "!src/**/*" - "!test/**/*" - "!*.md" - "!*.ts" - "!*.map" - "!yarn.lock" - "!pnpm-lock.yaml" - "!package-lock.json" - "!npm-debug.log" - "!dist/**/*" - "!build/**/*" - "!electron-builder.yml" - "!vite.config.ts" - "!tsconfig.json" - "!src/**/*" - "!build/**/*" - "!dist/**/*" - "!node_modules/**/*" - "!src/**/*" - "!test/**