39 KiB
ChatLab 桌面应用打包技术方案
本文档总结当前项目的桌面化打包方式,并抽象成一套后续新项目可以复用的技术方案。当前项目采用的是“Web 应用 + 本地后端服务 + Electron 外壳 + 安装包生成器”的路线:业务仍按前后端分离开发,发布时把前端静态资源、Python 后端可执行文件、第三方命令行工具和运行时 DLL 一起放入 Electron 安装包中,最终交付一个 Windows 桌面应用安装程序。
1. 当前项目概览
1.1 项目定位
当前项目是一个本地化运行的 ChatLab 售后智能助手,主要能力是读取本机 PC 微信聊天记录,结合 AI 对售后群消息进行检索、话题分析、知识文档生成和知识库管理。
系统运行在客户本机,核心数据不需要上传到外部服务器。用户看到的是一个桌面应用窗口,底层实际由多个本地服务协同完成:
| 模块 | 路径 | 技术栈 | 作用 |
|---|---|---|---|
| 桌面壳 | electron-launcher/ |
Electron | 提供桌面窗口、启动页、进程管理、IPC 通信、安装包配置 |
| 前端页面 | chatlab-web/frontend/ |
React + Vite | 提供聊天记录、AI 分析、知识库、设置等界面 |
| 业务后端 | chatlog_fastAPI/ |
Python + FastAPI | 统一业务接口、AI 调用、数据库、任务调度、静态资源托管 |
| 微信数据服务 | chatlog.exe |
Go 可执行文件 | 读取本机微信数据,提供 127.0.0.1:5030 的 chatlog API |
| 本地 DLL | lib/windows_x64/wx_key.dll |
Windows DLL | 配合微信数据密钥识别 |
| 构建脚本 | scripts/build-desktop.ps1 |
PowerShell | 串联图标、前端、后端、资源复制、安装包构建和签名校验 |
1.2 当前桌面化结果
当前项目已经生成 Windows 安装包,输出目录为:
release/
当前目录下可见的安装包包括:
release/ChatLab-Setup-1.0.0.exe
release/ChatLab-Setup-1.0.1-202605210454.exe
安装包由 electron-builder 生成,Windows 安装器类型为 NSIS。安装后用户可以通过桌面快捷方式或开始菜单启动 ChatLab售后智能助手。
2. 当前项目使用的打包方法
2.1 总体思路
当前项目没有把 Web 应用重写成原生桌面应用,而是保留原来的 Web 架构:
- React 前端继续用 Vite 构建成静态资源。
- FastAPI 后端继续提供 HTTP API,并在生产环境托管前端静态文件。
chatlog.exe作为本地外部二进制程序,由 Electron 主进程启动和管理。- Electron 只负责桌面窗口、启动控制、子进程生命周期、资源路径适配和安装包能力。
- PyInstaller 把 Python 后端打成独立的 Windows 可执行目录。
- electron-builder 把 Electron 主程序、前端构建产物、后端可执行目录、
chatlog.exe、DLL 和许可文件一起封装为安装包。
可以把它理解成:
用户双击桌面应用
|
v
Electron 启动
|
+--> 启动 FastAPI 后端,随机选择本地端口
|
+--> 启动 chatlog.exe,固定提供 127.0.0.1:5030
|
+--> 等待 /health 和 chatlog API 就绪
|
v
Electron 窗口加载 FastAPI 地址
|
v
FastAPI 返回 React 静态页面,页面再调用本地 API
2.2 关键设计点
2.2.1 Electron 不是业务后端,只是桌面运行容器
electron-launcher/main.js 中的主进程负责:
- 创建桌面窗口。
- 根据
app.isPackaged区分开发环境和打包环境。 - 在开发环境从源码目录启动 Python 后端。
- 在打包环境从
resources/backend/ChatLabBackend.exe启动后端。 - 启动
chatlog.exe并传入微信账号、密钥、工作目录等参数。 - 给 FastAPI 注入
CHATLAB_DATA_DIR、CHATLAB_STATIC_DIR、CHATLAB_BACKEND_PORT等环境变量。 - 等待后端
/health和 chatlog 服务就绪。 - 加载后端地址作为最终业务界面。
- 应用关闭时清理 FastAPI 和 chatlog 子进程。
这种方式的好处是:Electron 不承载复杂业务逻辑,业务逻辑仍然留在原来的 Python 后端和 React 前端里,后续维护成本低。
2.2.2 前端打成静态文件,由 FastAPI 托管
开发时,前端通过 Vite 开发服务器运行:
chatlab-web/frontend/
构建时执行:
npm run build
产物位于:
chatlab-web/frontend/dist/
打包脚本会把这个目录复制到:
electron-launcher/build-resources/frontend/
Electron 安装包内的资源结构最终类似:
resources/
frontend/
index.html
assets/
index-xxxx.js
index-xxxx.css
FastAPI 通过 CHATLAB_STATIC_DIR 知道前端静态资源路径,并在 main.py 中挂载:
/assets/favicon.svg/icons.svg//{full_path:path}SPA fallback
所以生产环境下不再单独启动 Vite,也没有 5173 前端开发端口。用户访问到的是 FastAPI 托管出来的 React 页面。
2.2.3 Python 后端用 PyInstaller 打成 onedir
后端入口是:
chatlog_fastAPI/run_backend.py
这个入口读取环境变量 CHATLAB_BACKEND_PORT,然后启动 Uvicorn:
uvicorn.run(app, host="127.0.0.1", port=port, reload=False, log_level="info")
PyInstaller 配置文件是:
chatlog_fastAPI/ChatLabBackend.spec
当前配置重点包括:
- 入口脚本:
run_backend.py - 输出名称:
ChatLabBackend - 模式:
COLLECT,即 onedir 目录形式 - 控制台:
console=True - 收集
jieba数据文件 - 显式收集
uvicorn、fastapi、pydantic_settings、aiosqlite、apscheduler等 hidden imports
构建命令为:
py -3.12 -m PyInstaller ChatLabBackend.spec --noconfirm --clean
构建后生成:
chatlog_fastAPI/dist/ChatLabBackend/
ChatLabBackend.exe
_internal/
...
打包脚本会把该目录复制到:
electron-launcher/build-resources/backend/
Electron 打包后,运行时后端路径就是:
resources/backend/ChatLabBackend.exe
2.2.4 外部程序和 DLL 作为 extraResources 随安装包发布
当前项目必须依赖:
chatlog.exe
lib/windows_x64/wx_key.dll
这些不是 Electron 的 JS 文件,也不应该被打进 asar 包里。因此当前项目使用 electron-builder 的 extraResources,把它们作为普通文件复制到安装目录的 resources/ 下。
打包前的临时资源目录为:
electron-launcher/build-resources/
脚本会写入:
electron-launcher/build-resources/
chatlog.exe
lib/
windows_x64/
wx_key.dll
frontend/
backend/
DISCLAIMER.md
LICENSE
electron-builder.config.cjs 中通过 extraResources 把整个 build-resources 复制到安装包资源目录:
extraResources: [
{
from: path.join(__dirname, 'build-resources'),
to: '.',
filter: [
'**/*',
'!**/.env',
'!**/knowledge*.db',
'!**/__pycache__/**',
'!**/*.pfx',
'!**/*.p12',
'!**/*.pvk',
'!**/*.cer',
'!**/*.crt',
'!**/*.key',
'!**/certs/**',
],
},
]
这里的原则是:运行期必须作为真实文件存在的内容,都走 extraResources,不要放进 Electron 的 files 或 asar 内。
2.2.5 数据目录放到用户 AppData,不写入安装目录
Electron 启动后端时设置:
CHATLAB_DATA_DIR=%APPDATA%/ChatLab
FastAPI 后端通过 config.py 读取该目录,数据库默认放在类似路径:
%APPDATA%/ChatLab/data/knowledge.db
这样做有几个优点:
- 安装目录通常没有写权限,避免运行时报权限错误。
- 用户数据与程序文件分离,方便升级安装包。
- 卸载、迁移、备份时路径清晰。
- 不会把客户数据误打进安装包。
后续新项目也应该遵循这个原则:安装目录只放程序文件,用户数据放 %APPDATA%/应用名、%LOCALAPPDATA%/应用名 或用户显式选择的数据目录。
2.2.6 Electron 主进程负责端口和进程生命周期
当前项目中:
- FastAPI 端口由 Electron 动态寻找空闲端口。
chatlog.exe固定使用127.0.0.1:5030。- Electron 先启动服务,再轮询健康检查。
- 窗口关闭时通过
taskkill /pid /f /t清理 Windows 子进程树。
这个设计解决了桌面应用常见的几个问题:
- 用户不需要自己打开命令行。
- 后端端口冲突概率降低。
- 后端启动失败时可以在启动页显示日志。
- 关闭桌面应用时不会残留后台进程。
3. 当前项目的构建入口
3.1 一键构建命令
当前桌面版构建入口为:
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1
该脚本默认执行完整构建:
- 生成或刷新 Electron 图标。
- 构建 React 前端。
- 用 PyInstaller 构建 Python 后端。
- 重置
electron-launcher/build-resources。 - 复制
chatlog.exe、lib、前端dist、后端dist/ChatLabBackend、许可文件。 - 扫描敏感文件,阻止
.env、knowledge*.db、证书、私钥、缓存等进入发布资源。 - 生成
release/manifest.txt。 - 调用
electron-builder生成 Windows NSIS 安装包。 - 将安装包和 blockmap 复制到
release/。 - 如启用签名,校验安装包签名状态。
3.2 可选参数
脚本支持跳过部分步骤,便于调试:
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1 -SkipIcon
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1 -SkipFrontend
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1 -SkipBackend
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1 -SkipInstaller
常见用途:
| 参数 | 用途 |
|---|---|
-SkipIcon |
图标没有变化时跳过图标生成 |
-SkipFrontend |
前端没有变化时跳过 Vite 构建 |
-SkipBackend |
后端没有变化时跳过 PyInstaller |
-SkipInstaller |
只准备 build-resources,不生成安装包 |
3.3 代码签名构建
未签名安装包可以用于本地测试,但客户电脑上容易触发 Windows SmartScreen 或杀毒软件提示。正式交付建议使用 Windows 代码签名证书。
命令行方式:
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1 `
-Sign `
-CertificateFile "D:\certs\ChatLab-CodeSigning.pfx" `
-CertificatePassword "证书密码" `
-PublisherName "证书中的发布者名称" `
-ForceSign
环境变量方式:
$env:CHATLAB_PFX_FILE = "D:\certs\ChatLab-CodeSigning.pfx"
$env:CHATLAB_PFX_PASSWORD = "证书密码"
$env:CHATLAB_CERT_PUBLISHER_NAME = "证书中的发布者名称"
$env:CHATLAB_FORCE_SIGN = "1"
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1
签名相关变量:
| 变量 | 说明 |
|---|---|
CHATLAB_PFX_FILE |
PFX/P12 证书完整路径 |
CHATLAB_PFX_PASSWORD |
证书密码 |
CHATLAB_CERT_PUBLISHER_NAME |
可选,发布者名称 |
CHATLAB_TIMESTAMP_SERVER |
可选,默认 http://timestamp.digicert.com |
CHATLAB_FORCE_SIGN |
设置为 1 后签名失败会中断构建 |
证书安全要求:
- 证书不要放进项目目录。
- 证书不要放进
build-resources。 - 证书密码不要写进代码仓库。
- 构建脚本已经阻止
.pfx、.p12、.pvk、.key、.cer、.crt、certs/进入发布资源。
4. 目录与产物说明
4.1 开发源码目录
get_wechat_me/
chatlab-web/
frontend/
src/
public/
dist/
package.json
vite.config.js
chatlog_fastAPI/
main.py
run_backend.py
config.py
requirements.txt
ChatLabBackend.spec
routers/
services/
dist/
electron-launcher/
main.js
preload.js
index.html
package.json
electron-builder.config.cjs
build/
build-resources/
dist/
lib/
windows_x64/
wx_key.dll
scripts/
build-desktop.ps1
make-icon.cjs
chatlog.exe
release/
4.2 构建过程中产生的关键目录
| 目录 | 生成者 | 作用 |
|---|---|---|
chatlab-web/frontend/dist |
Vite | 前端静态资源 |
chatlog_fastAPI/build |
PyInstaller | Python 打包中间产物 |
chatlog_fastAPI/dist/ChatLabBackend |
PyInstaller | Python 后端可执行目录 |
electron-launcher/build |
图标脚本 | Electron 图标 |
electron-launcher/build-resources |
构建脚本 | 准备给 electron-builder 的额外资源 |
electron-launcher/dist |
electron-builder | win-unpacked 和安装包 |
release |
构建脚本 | 最终交付产物目录 |
4.3 安装包内的运行时结构
安装后大致结构如下:
安装目录/
ChatLab售后智能助手.exe
resources/
app.asar 或 app/
main.js
preload.js
index.html
package.json
backend/
ChatLabBackend.exe
_internal/
frontend/
index.html
assets/
chatlog.exe
lib/
windows_x64/
wx_key.dll
DISCLAIMER.md
LICENSE
Electron 主进程通过 process.resourcesPath 找到 resources/,再拼出 backend/ChatLabBackend.exe、frontend/、chatlog.exe 和 lib/ 的绝对路径。
5. 当前方案为什么适合这个项目
5.1 适合多技术栈项目
当前项目同时包含:
- React 前端
- Python FastAPI 后端
- Go 编译好的
chatlog.exe - Windows DLL
- 本地 SQLite 数据
- AI API 配置
如果强行改成单一技术栈,成本很高。Electron 的好处是可以把这些组件“原样收纳”进桌面应用,同时保留已有前后端开发方式。
5.2 适合本地化交付
客户只需要安装一个桌面程序,不需要手动安装 Python、Node.js、前端依赖、后端依赖,也不需要知道命令行怎么启动。
打包后:
- Python 运行时随 PyInstaller 产物携带。
- 前端 JS/CSS 已经静态化。
- Electron 内置 Chromium,不依赖客户电脑浏览器。
- chatlog 和 DLL 随资源文件一起分发。
- 用户数据写入 AppData,不污染安装目录。
5.3 适合需要启动多个本地服务的应用
这个项目不是单页面离线工具,而是必须启动本地后端和微信数据服务。Electron 主进程天然适合做“进程管家”:
- 启动服务。
- 显示日志。
- 检查健康状态。
- 控制按钮状态。
- 关闭时清理进程。
- 在异常时给用户可理解的提示。
6. 新项目复用方案
后续如果要把新的 Web + 后端项目打包成桌面应用,可以按下面方案实施。
6.1 推荐技术选型
| 层级 | 推荐方案 | 说明 |
|---|---|---|
| 桌面壳 | Electron | 最适合承载已有 Web 应用和本地子进程 |
| 安装包 | electron-builder | 支持 NSIS、图标、快捷方式、签名、extraResources |
| 前端 | Vite / React / Vue 等 | 构建成静态资源即可 |
| Python 后端 | PyInstaller onedir | 对 FastAPI、依赖库、本地数据文件支持较好 |
| Node 后端 | 直接作为 Electron 子进程或打包为 pkg/nexe | 视项目复杂度选择 |
| Go/Rust 后端 | 直接编译 exe 后作为 extraResources | 最简单稳定 |
| 本地数据 | AppData / LocalAppData | 不写安装目录 |
| 进程通信 | HTTP + IPC | 业务走 HTTP,桌面控制走 Electron IPC |
6.2 标准目录模板
建议新项目从一开始就按下面结构组织:
new-project/
frontend/
package.json
vite.config.js
src/
dist/
backend/
main.py
run_backend.py
requirements.txt
Backend.spec
dist/
desktop/
main.js
preload.js
index.html
package.json
electron-builder.config.cjs
build/
build-resources/
dist/
native-tools/
your-tool.exe
dlls/
scripts/
build-desktop.ps1
release/
如果项目没有 Python 后端,可以删除 backend/ 和 PyInstaller 步骤。如果项目没有外部二进制工具,可以删除 native-tools/。
6.3 新项目落地步骤
第一步:明确桌面应用运行方式
先回答下面几个问题:
| 问题 | 建议 |
|---|---|
| 前端是否需要联网? | 如果只是本地业务,优先由本地后端托管静态资源 |
| 后端是否必须存在? | 有数据库、AI、文件、系统调用时建议保留本地后端 |
| 是否需要外部 exe/DLL? | 需要真实文件的工具统一放入 extraResources |
| 是否需要安装包? | 正式交付建议使用 NSIS 安装包 |
| 是否需要代码签名? | 客户交付建议签名 |
| 用户数据放在哪里? | 放 AppData,不放安装目录 |
对当前项目这类本地服务型应用,推荐运行方式为:
Electron -> 启动本地后端 -> 后端托管前端 -> Electron 加载后端 URL
第二步:前端适配生产环境
前端要满足:
- 能通过
npm run build生成静态资源。 - 页面路由支持 SPA fallback。
- API 请求尽量使用相对路径,例如
/api/xxx,不要硬编码http://127.0.0.1:5173。 - 开发环境可以通过 Vite proxy 转发 API。
- 生产环境由本地后端托管静态文件并处理 API。
示例 vite.config.js:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
strictPort: true,
host: '127.0.0.1',
proxy: {
'/api': { target: 'http://127.0.0.1:8000', changeOrigin: true },
},
},
})
生产构建命令:
cd frontend
npm install
npm run build
第三步:后端适配桌面运行
后端要满足:
- 能从环境变量读取端口。
- 只监听
127.0.0.1,避免暴露到局域网。 - 提供
/health健康检查。 - 能从环境变量读取数据目录。
- 能从环境变量读取静态资源目录。
- 生产环境托管前端
dist。 - 不依赖当前工作目录查找文件,尽量使用绝对路径或环境变量路径。
示例 run_backend.py:
import os
import uvicorn
from main import app
def main():
port = int(os.environ.get("APP_BACKEND_PORT", "8000"))
uvicorn.run(app, host="127.0.0.1", port=port, reload=False, log_level="info")
if __name__ == "__main__":
main()
示例静态资源托管:
import os
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
app = FastAPI()
static_dir = Path(os.environ.get("APP_STATIC_DIR", "frontend/dist"))
if static_dir.exists():
assets_dir = static_dir / "assets"
if assets_dir.exists():
app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets")
@app.get("/", include_in_schema=False)
async def index():
return FileResponse(static_dir / "index.html")
@app.get("/{full_path:path}", include_in_schema=False)
async def spa_fallback(full_path: str):
target = static_dir / full_path
if target.exists() and target.is_file():
return FileResponse(target)
return FileResponse(static_dir / "index.html")
第四步:配置 PyInstaller
创建 Backend.spec,最小示例:
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
["run_backend.py"],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name="Backend",
console=True,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
name="Backend",
)
如果后端使用 FastAPI、Uvicorn、APScheduler、jieba、Pydantic Settings 等动态导入库,需要显式收集 hidden imports 或 data files:
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
datas = []
datas += collect_data_files("jieba")
hiddenimports = []
hiddenimports += collect_submodules("uvicorn")
hiddenimports += collect_submodules("fastapi")
hiddenimports += collect_submodules("pydantic_settings")
hiddenimports += collect_submodules("apscheduler")
构建命令:
cd backend
py -3.12 -m PyInstaller Backend.spec --noconfirm --clean
建议优先使用 onedir,而不是 onefile:
| 模式 | 优点 | 缺点 | 建议 |
|---|---|---|---|
| onedir | 启动快,依赖文件清晰,问题好排查 | 文件多 | 桌面应用内置后端优先使用 |
| onefile | 单个 exe 好看 | 启动慢,会解压临时文件,杀软误报概率更高 | 只适合很小的工具 |
第五步:创建 Electron 壳
desktop/package.json 示例:
{
"name": "your-app-desktop",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder --win --config electron-builder.config.cjs"
},
"devDependencies": {
"electron": "^42.0.0",
"electron-builder": "^26.8.1"
}
}
desktop/main.js 需要包含这些核心能力:
- 资源路径适配。
- 创建窗口。
- 启动后端。
- 等待健康检查。
- 加载业务页面。
- 关闭时清理子进程。
路径适配示例:
const { app, BrowserWindow } = require('electron')
const path = require('path')
function isPackaged() {
return app.isPackaged
}
function projectRoot() {
return isPackaged() ? process.resourcesPath : path.resolve(__dirname, '..')
}
function resourcePath(...parts) {
return path.join(projectRoot(), ...parts)
}
function backendExePath() {
return resourcePath('backend', 'Backend.exe')
}
function frontendDistDir() {
return isPackaged()
? resourcePath('frontend')
: resourcePath('frontend', 'dist')
}
启动后端示例:
const { spawn } = require('child_process')
const net = require('net')
const http = require('http')
let backendPort = null
let backendUrl = null
let backendProcess = null
function getFreePort() {
return new Promise((resolve, reject) => {
const server = net.createServer()
server.listen(0, '127.0.0.1', () => {
const port = server.address().port
server.close(() => resolve(port))
})
server.on('error', reject)
})
}
async function startBackend() {
if (backendProcess) return
backendPort = backendPort || await getFreePort()
backendUrl = `http://127.0.0.1:${backendPort}`
const env = {
...process.env,
APP_BACKEND_PORT: String(backendPort),
APP_STATIC_DIR: frontendDistDir(),
APP_DATA_DIR: path.join(app.getPath('appData'), 'YourApp'),
}
const command = isPackaged() ? backendExePath() : 'python'
const args = isPackaged() ? [] : ['run_backend.py']
const cwd = isPackaged() ? path.dirname(command) : resourcePath('backend')
backendProcess = spawn(command, args, { cwd, env, windowsHide: true, shell: !isPackaged() })
}
窗口加载策略:
async function openAppWindow() {
await startBackend()
await waitForHealth()
mainWindow.loadURL(backendUrl)
}
第六步:配置 preload 和 IPC
如果启动页需要按钮控制后端、显示日志、触发刷新等,不要在渲染进程直接开启 Node 能力。推荐:
nodeIntegration: falsecontextIsolation: true- 通过
preload.js暴露有限 API
示例:
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('desktopAPI', {
startAll: () => ipcRenderer.invoke('start-all'),
getStatus: () => ipcRenderer.invoke('get-status'),
onLog: (callback) => ipcRenderer.on('log', (_event, value) => callback(value)),
})
主进程:
const { ipcMain } = require('electron')
ipcMain.handle('start-all', async () => {
await openAppWindow()
return { ok: true, backendUrl }
})
第七步:配置 electron-builder
示例 desktop/electron-builder.config.cjs:
const path = require('path')
module.exports = {
appId: 'com.company.yourapp',
productName: 'YourApp',
icon: 'build/icon.ico',
directories: {
output: 'dist',
},
files: [
'main.js',
'preload.js',
'index.html',
'build/icon.ico',
'build/icon.png',
'package.json',
],
extraResources: [
{
from: path.join(__dirname, 'build-resources'),
to: '.',
filter: [
'**/*',
'!**/.env',
'!**/*.db',
'!**/__pycache__/**',
'!**/*.pfx',
'!**/*.p12',
'!**/*.key',
'!**/certs/**',
],
},
],
win: {
target: 'nsis',
icon: 'build/icon.ico',
artifactName: 'YourApp-Setup-${version}.${ext}',
},
nsis: {
oneClick: false,
allowToChangeInstallationDirectory: true,
perMachine: false,
createDesktopShortcut: true,
createStartMenuShortcut: true,
shortcutName: 'YourApp',
},
}
要点:
files只放 Electron 自己运行所需的 JS/HTML/图标/package。extraResources放后端 exe、前端 dist、外部工具、DLL、模板文件等真实资源。- 敏感文件必须通过 filter 排除。
- 正式应用应设置稳定的
appId。 productName会影响安装目录、快捷方式和应用显示名。
第八步:编写统一构建脚本
建议新项目也使用一个 PowerShell 脚本串联所有步骤。伪代码如下:
$ErrorActionPreference = "Stop"
$Root = Resolve-Path (Join-Path $PSScriptRoot "..")
$Frontend = Join-Path $Root "frontend"
$Backend = Join-Path $Root "backend"
$Desktop = Join-Path $Root "desktop"
$Resources = Join-Path $Desktop "build-resources"
$Release = Join-Path $Root "release"
# 1. 构建前端
Push-Location $Frontend
npm.cmd run build
Pop-Location
# 2. 构建后端
Push-Location $Backend
py -3.12 -m PyInstaller Backend.spec --noconfirm --clean
Pop-Location
# 3. 重置资源目录
if (Test-Path $Resources) {
Remove-Item -LiteralPath (Resolve-Path $Resources).Path -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $Resources | Out-Null
# 4. 复制资源
Copy-Item -LiteralPath (Join-Path $Frontend "dist") -Destination (Join-Path $Resources "frontend") -Recurse -Force
Copy-Item -LiteralPath (Join-Path $Backend "dist\Backend") -Destination (Join-Path $Resources "backend") -Recurse -Force
Copy-Item -LiteralPath (Join-Path $Root "native-tools\your-tool.exe") -Destination (Join-Path $Resources "your-tool.exe") -Force
# 5. 生成安装包
Push-Location $Desktop
npm.cmd run build
Pop-Location
# 6. 复制到 release
New-Item -ItemType Directory -Force -Path $Release | Out-Null
Copy-Item -Path (Join-Path $Desktop "dist\*.exe") -Destination $Release -Force
当前项目的 scripts/build-desktop.ps1 已经是一份更完整的版本,包含:
- Python 版本回退逻辑。
- 证书路径校验。
- 环境变量签名参数。
- 安全删除目录。
- 敏感文件扫描。
- release manifest。
- 签名校验。
新项目建议直接以它为蓝本改名、改路径、改资源即可。
7. 当前项目构建链路详解
7.1 构建链路
scripts/build-desktop.ps1
|
+--> scripts/make-icon.cjs
| |
| +--> electron-launcher/build/icon.ico
| +--> electron-launcher/build/icon.png
|
+--> chatlab-web/frontend
| |
| +--> npm run build
| +--> chatlab-web/frontend/dist
|
+--> chatlog_fastAPI
| |
| +--> py -3.12 -m PyInstaller ChatLabBackend.spec
| +--> chatlog_fastAPI/dist/ChatLabBackend
|
+--> electron-launcher/build-resources
| |
| +--> chatlog.exe
| +--> lib/
| +--> frontend/
| +--> backend/
| +--> LICENSE / DISCLAIMER.md
|
+--> electron-launcher
| |
| +--> npm run build
| +--> electron-builder --win --config electron-builder.config.cjs
|
+--> release/
|
+--> ChatLab-Setup-版本号-构建标识.exe
+--> ChatLab-Setup-版本号-构建标识.exe.blockmap
+--> manifest.txt
7.2 运行链路
ChatLab售后智能助手.exe
|
v
Electron main.js
|
+--> createWindow()
| |
| +--> loadFile("index.html")
| +--> 显示启动控制页
|
+--> 用户点击启动 / start-all
|
+--> startBackend()
| |
| +--> 查找空闲端口
| +--> 设置 CHATLAB_DATA_DIR
| +--> 设置 CHATLAB_STATIC_DIR
| +--> 设置 CHATLAB_BACKEND_PORT
| +--> 启动 resources/backend/ChatLabBackend.exe
| +--> 等待 /health
|
+--> startChatlog()
| |
| +--> 检查 PC 微信进程
| +--> 执行 chatlog.exe key --force
| +--> 读取 ~/.chatlog/chatlog.json
| +--> 启动 chatlog.exe server --auto-decrypt ...
| +--> 等待 127.0.0.1:5030 API 就绪
|
+--> mainWindow.loadURL(backendUrl)
|
+--> FastAPI 返回 React 页面
8. 新项目可复用的技术规范
8.1 资源路径规范
必须区分开发环境和打包环境:
| 场景 | 根目录 |
|---|---|
| 开发环境 | 项目源码根目录 |
| 打包环境 | process.resourcesPath |
不要在 Electron 主进程中写死源码路径。所有资源路径都通过类似函数统一生成:
function projectRoot() {
return app.isPackaged ? process.resourcesPath : path.resolve(__dirname, '..')
}
function resourcePath(...parts) {
return path.join(projectRoot(), ...parts)
}
8.2 端口规范
推荐:
- 后端端口由 Electron 动态分配。
- 对外只监听
127.0.0.1。 - 前端生产环境使用相对路径调用 API。
- 必须提供
/health。 - 启动页等待
/health成功后再进入系统。
如果有必须固定端口的外部工具,要在启动前检查端口占用,并给出明确错误提示。
8.3 数据目录规范
桌面应用不要把用户数据写入安装目录。建议:
%APPDATA%/应用名/
data/
logs/
cache/
config/
后端通过环境变量读取:
APP_DATA_DIR
Electron 负责传入:
APP_DATA_DIR: path.join(app.getPath('appData'), 'YourApp')
8.4 日志规范
建议至少保留两类日志:
| 日志 | 位置 | 作用 |
|---|---|---|
| 启动页实时日志 | Electron IPC | 给用户和售后定位启动问题 |
| 文件日志 | AppData logs | 给开发定位线上问题 |
敏感信息必须脱敏,例如:
- API Key
- token
- 数据库密码
- 用户密钥
- 证书路径和密码
当前项目在 Electron 主进程里已经对 apiKey、data_key、img_key、sk-xxxx 做了日志脱敏。
8.5 安全规范
正式打包前必须检查:
.env不进入安装包。- 测试数据库不进入安装包。
- 用户数据不进入安装包。
- 证书和私钥不进入安装包。
__pycache__不进入安装包。- 日志文件不进入安装包。
- API Key 不写死到前端代码。
- Electron 渲染进程不启用 Node 权限。
当前项目的安全策略包括:
build-desktop.ps1构建前扫描build-resources和release。electron-builder.config.cjs在extraResources.filter中排除敏感文件。preload.js通过contextBridge暴露有限 IPC。mainWindow设置nodeIntegration: false、contextIsolation: true。
8.6 签名规范
内部测试可以使用未签名包,但正式交付建议签名:
- 证书放项目外。
- 密码通过环境变量传入。
- 启用时间戳服务器。
- 启用
forceCodeSigning或自定义强校验。 - 构建结束后用
Get-AuthenticodeSignature校验。
Windows 签名不能完全消除 SmartScreen 提示,但可以显著降低拦截概率,并建立发布者信誉。
9. 验收测试方案
9.1 构建前检查
执行:
node -v
npm -v
py -3.12 -V
检查:
- Node.js 可用。
- Python 3.12 可用。
- 前端依赖已安装。
- Electron 依赖已安装。
- Python 后端依赖已安装。
chatlog.exe存在。lib/windows_x64/wx_key.dll存在。- 图标文件可生成或已存在。
9.2 本地开发运行检查
开发模式建议分别检查:
cd chatlab-web/frontend
npm run build
cd chatlog_fastAPI
python run_backend.py
cd electron-launcher
npm run start
重点看:
- Electron 启动页能打开。
- FastAPI 能启动。
/health返回正常。- 前端静态页面能被后端托管。
- chatlog 服务能启动并返回群聊或会话数据。
9.3 打包检查
执行:
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1
检查输出:
chatlab-web/frontend/dist已更新。chatlog_fastAPI/dist/ChatLabBackend/ChatLabBackend.exe存在。electron-launcher/build-resources/frontend存在。electron-launcher/build-resources/backend/ChatLabBackend.exe存在。electron-launcher/build-resources/chatlog.exe存在。electron-launcher/build-resources/lib/windows_x64/wx_key.dll存在。release/manifest.txt已生成。release/ChatLab-Setup-*.exe已生成。
9.4 安装后功能检查
在干净 Windows 机器或虚拟机上安装:
- 双击安装包。
- 确认桌面快捷方式和开始菜单快捷方式生成。
- 启动应用。
- 确认启动页显示正常。
- 点击启动或进入系统。
- 确认 FastAPI 启动成功。
- 确认 chatlog 启动成功。
- 确认主界面加载成功。
- 确认关闭窗口后无残留
ChatLabBackend.exe和chatlog.exe。 - 确认用户数据写入
%APPDATA%/ChatLab,不是安装目录。
9.5 发布前安全检查
检查 release/manifest.txt,确认没有:
.env
knowledge*.db
__pycache__
*.pfx
*.p12
*.pvk
*.cer
*.crt
*.key
certs/
签名包额外执行:
Get-AuthenticodeSignature .\release\ChatLab-Setup-*.exe
期望:
Status: Valid
10. 常见问题与处理方案
10.1 打包后前端空白
可能原因:
- 前端构建产物没有复制到
build-resources/frontend。 - FastAPI 没有正确读取
CHATLAB_STATIC_DIR。 - React 路由没有 SPA fallback。
- 生产环境 API 地址仍写死为开发端口。
处理:
- 检查安装目录
resources/frontend/index.html是否存在。 - 检查
resources/frontend/assets是否存在。 - 打开后端
/health查看static_dir或日志。 - 前端 API 统一改为相对路径。
10.2 打包后后端启动失败
可能原因:
- PyInstaller hidden imports 不完整。
- 某些数据文件没有被
collect_data_files收集。 - 后端依赖当前工作目录。
- 安装目录没有写权限。
处理:
- 先运行
chatlog_fastAPI/dist/ChatLabBackend/ChatLabBackend.exe看报错。 - 检查
warn-ChatLabBackend.txt。 - 在
.spec中增加 hidden imports 或 datas。 - 数据写入 AppData,不写程序目录。
10.3 Electron 找不到 exe 或 DLL
可能原因:
- 资源没有复制到
build-resources。 - 路径仍按源码目录查找。
- 资源被打入 asar,导致外部程序无法直接运行。
处理:
- 外部 exe/DLL 使用
extraResources。 - 使用
process.resourcesPath拼运行时路径。 - 不要让外部可执行文件依赖 asar 内路径。
10.4 关闭应用后仍有后台进程
可能原因:
- 只 kill 了父进程,没有 kill 子进程树。
- Electron 退出流程没有等待清理。
处理:
- Windows 使用
taskkill /pid <pid> /f /t。 window.close和before-quit中都调用清理逻辑。- 清理时维护进程句柄,不要只靠进程名杀。
10.5 客户电脑提示风险或拦截
可能原因:
- 安装包未签名。
- 新证书发布者信誉不足。
- PyInstaller 或 Electron 包体较大,被杀软谨慎处理。
- onefile 自解压行为更容易被误报。
处理:
- 使用代码签名证书。
- 使用 onedir 后端。
- 避免把测试工具、调试脚本、临时文件放进安装包。
- 给客户提供发布者、用途、安装路径说明。
10.6 中文应用名乱码
可能原因:
- 文件本身编码不是 UTF-8。
- PowerShell 控制台编码导致显示乱码。
- 构建环境 locale 不一致。
处理:
- 源码文件统一保存为 UTF-8。
- 构建脚本和配置文件避免混用编码。
- 如控制台显示乱码,但安装包内应用名正常,可优先检查实际安装结果。
11. 新项目复制当前方案时需要修改的清单
| 修改项 | 当前项目值 | 新项目需要改成 |
|---|---|---|
| 应用名 | ChatLab售后智能助手 |
新项目产品名 |
| appId | com.chatlab.desktop |
com.公司名.应用名 |
| Electron 目录 | electron-launcher |
新项目 desktop 目录 |
| 前端目录 | chatlab-web/frontend |
新项目前端目录 |
| 后端目录 | chatlog_fastAPI |
新项目后端目录 |
| 后端 exe 名 | ChatLabBackend.exe |
新项目后端 exe 名 |
| 数据目录 | %APPDATA%/ChatLab |
%APPDATA%/新应用名 |
| 图标 | electron-launcher/build/icon.ico |
新项目图标 |
| 安装包名 | ChatLab-Setup-${version}-${buildLabel}.exe |
新项目安装包命名 |
| 额外资源 | chatlog.exe、lib |
新项目实际外部资源 |
| 敏感文件规则 | .env、knowledge*.db、证书等 |
按新项目补充 |
| 健康检查 | /health |
新项目健康检查接口 |
| 固定端口 | chatlog 5030 |
新项目实际需要 |
| 环境变量前缀 | CHATLAB_ |
新项目独立前缀 |
12. 建议的最终构建命令手册
12.1 首次准备环境
# 前端依赖
cd chatlab-web/frontend
npm install
# Electron 依赖
cd ../../electron-launcher
npm install
# Python 依赖
cd ../chatlog_fastAPI
py -3.12 -m pip install -r requirements.txt
py -3.12 -m pip install pyinstaller
12.2 本地测试构建
cd 项目根目录
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1
12.3 只准备资源,不生成安装包
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1 -SkipInstaller
12.4 前端没改,只重打后端和安装包
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1 -SkipFrontend
12.5 后端没改,只重打前端和安装包
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1 -SkipBackend
12.6 正式签名发布
$env:CHATLAB_PFX_FILE = "D:\certs\ChatLab-CodeSigning.pfx"
$env:CHATLAB_PFX_PASSWORD = "证书密码"
$env:CHATLAB_CERT_PUBLISHER_NAME = "证书中的发布者名称"
$env:CHATLAB_FORCE_SIGN = "1"
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1
13. 结论
当前项目使用的是一套比较稳妥的桌面化工程方案:Electron 负责桌面体验和进程管理,React 负责界面,FastAPI 负责业务接口和静态资源托管,PyInstaller 负责 Python 后端二进制化,electron-builder 负责 Windows 安装包生成,extraResources 负责携带外部 exe、DLL 和构建产物。
这套方案的核心价值在于:不破坏原来的 Web 项目结构,又能把多进程、本地服务、外部工具和前端页面统一交付成一个用户可安装、可双击启动的桌面应用。后续新项目只要按照“前端静态化、后端可执行化、Electron 管理进程、资源走 extraResources、用户数据进 AppData、构建脚本统一编排”的原则,就可以复用当前项目的打包方法。