Files
get_wechat/桌面应用打包技术方案.md

1482 lines
39 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 安装包,输出目录为:
```text
release/
```
当前目录下可见的安装包包括:
```text
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 架构:
1. React 前端继续用 Vite 构建成静态资源。
2. FastAPI 后端继续提供 HTTP API并在生产环境托管前端静态文件。
3. `chatlog.exe` 作为本地外部二进制程序,由 Electron 主进程启动和管理。
4. Electron 只负责桌面窗口、启动控制、子进程生命周期、资源路径适配和安装包能力。
5. PyInstaller 把 Python 后端打成独立的 Windows 可执行目录。
6. electron-builder 把 Electron 主程序、前端构建产物、后端可执行目录、`chatlog.exe`、DLL 和许可文件一起封装为安装包。
可以把它理解成:
```text
用户双击桌面应用
|
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 开发服务器运行:
```text
chatlab-web/frontend/
```
构建时执行:
```powershell
npm run build
```
产物位于:
```text
chatlab-web/frontend/dist/
```
打包脚本会把这个目录复制到:
```text
electron-launcher/build-resources/frontend/
```
Electron 安装包内的资源结构最终类似:
```text
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
后端入口是:
```text
chatlog_fastAPI/run_backend.py
```
这个入口读取环境变量 `CHATLAB_BACKEND_PORT`,然后启动 Uvicorn
```python
uvicorn.run(app, host="127.0.0.1", port=port, reload=False, log_level="info")
```
PyInstaller 配置文件是:
```text
chatlog_fastAPI/ChatLabBackend.spec
```
当前配置重点包括:
- 入口脚本:`run_backend.py`
- 输出名称:`ChatLabBackend`
- 模式:`COLLECT`,即 onedir 目录形式
- 控制台:`console=True`
- 收集 `jieba` 数据文件
- 显式收集 `uvicorn``fastapi``pydantic_settings``aiosqlite``apscheduler` 等 hidden imports
构建命令为:
```powershell
py -3.12 -m PyInstaller ChatLabBackend.spec --noconfirm --clean
```
构建后生成:
```text
chatlog_fastAPI/dist/ChatLabBackend/
ChatLabBackend.exe
_internal/
...
```
打包脚本会把该目录复制到:
```text
electron-launcher/build-resources/backend/
```
Electron 打包后,运行时后端路径就是:
```text
resources/backend/ChatLabBackend.exe
```
#### 2.2.4 外部程序和 DLL 作为 extraResources 随安装包发布
当前项目必须依赖:
```text
chatlog.exe
lib/windows_x64/wx_key.dll
```
这些不是 Electron 的 JS 文件,也不应该被打进 asar 包里。因此当前项目使用 `electron-builder``extraResources`,把它们作为普通文件复制到安装目录的 `resources/` 下。
打包前的临时资源目录为:
```text
electron-launcher/build-resources/
```
脚本会写入:
```text
electron-launcher/build-resources/
chatlog.exe
lib/
windows_x64/
wx_key.dll
frontend/
backend/
DISCLAIMER.md
LICENSE
```
`electron-builder.config.cjs` 中通过 `extraResources` 把整个 `build-resources` 复制到安装包资源目录:
```js
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 启动后端时设置:
```text
CHATLAB_DATA_DIR=%APPDATA%/ChatLab
```
FastAPI 后端通过 `config.py` 读取该目录,数据库默认放在类似路径:
```text
%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
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1
```
该脚本默认执行完整构建:
1. 生成或刷新 Electron 图标。
2. 构建 React 前端。
3. 用 PyInstaller 构建 Python 后端。
4. 重置 `electron-launcher/build-resources`
5. 复制 `chatlog.exe``lib`、前端 `dist`、后端 `dist/ChatLabBackend`、许可文件。
6. 扫描敏感文件,阻止 `.env``knowledge*.db`、证书、私钥、缓存等进入发布资源。
7. 生成 `release/manifest.txt`
8. 调用 `electron-builder` 生成 Windows NSIS 安装包。
9. 将安装包和 blockmap 复制到 `release/`
10. 如启用签名,校验安装包签名状态。
### 3.2 可选参数
脚本支持跳过部分步骤,便于调试:
```powershell
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
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1 `
-Sign `
-CertificateFile "D:\certs\ChatLab-CodeSigning.pfx" `
-CertificatePassword "证书密码" `
-PublisherName "证书中的发布者名称" `
-ForceSign
```
环境变量方式:
```powershell
$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 开发源码目录
```text
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 安装包内的运行时结构
安装后大致结构如下:
```text
安装目录/
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 标准目录模板
建议新项目从一开始就按下面结构组织:
```text
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不放安装目录 |
对当前项目这类本地服务型应用,推荐运行方式为:
```text
Electron -> 启动本地后端 -> 后端托管前端 -> Electron 加载后端 URL
```
#### 第二步:前端适配生产环境
前端要满足:
- 能通过 `npm run build` 生成静态资源。
- 页面路由支持 SPA fallback。
- API 请求尽量使用相对路径,例如 `/api/xxx`,不要硬编码 `http://127.0.0.1:5173`
- 开发环境可以通过 Vite proxy 转发 API。
- 生产环境由本地后端托管静态文件并处理 API。
示例 `vite.config.js`
```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 },
},
},
})
```
生产构建命令:
```powershell
cd frontend
npm install
npm run build
```
#### 第三步:后端适配桌面运行
后端要满足:
- 能从环境变量读取端口。
- 只监听 `127.0.0.1`,避免暴露到局域网。
- 提供 `/health` 健康检查。
- 能从环境变量读取数据目录。
- 能从环境变量读取静态资源目录。
- 生产环境托管前端 `dist`
- 不依赖当前工作目录查找文件,尽量使用绝对路径或环境变量路径。
示例 `run_backend.py`
```python
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()
```
示例静态资源托管:
```python
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`,最小示例:
```python
# -*- 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
```python
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")
```
构建命令:
```powershell
cd backend
py -3.12 -m PyInstaller Backend.spec --noconfirm --clean
```
建议优先使用 onedir而不是 onefile
| 模式 | 优点 | 缺点 | 建议 |
|---|---|---|---|
| onedir | 启动快,依赖文件清晰,问题好排查 | 文件多 | 桌面应用内置后端优先使用 |
| onefile | 单个 exe 好看 | 启动慢,会解压临时文件,杀软误报概率更高 | 只适合很小的工具 |
#### 第五步:创建 Electron 壳
`desktop/package.json` 示例:
```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` 需要包含这些核心能力:
- 资源路径适配。
- 创建窗口。
- 启动后端。
- 等待健康检查。
- 加载业务页面。
- 关闭时清理子进程。
路径适配示例:
```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')
}
```
启动后端示例:
```js
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() })
}
```
窗口加载策略:
```js
async function openAppWindow() {
await startBackend()
await waitForHealth()
mainWindow.loadURL(backendUrl)
}
```
#### 第六步:配置 preload 和 IPC
如果启动页需要按钮控制后端、显示日志、触发刷新等,不要在渲染进程直接开启 Node 能力。推荐:
- `nodeIntegration: false`
- `contextIsolation: true`
- 通过 `preload.js` 暴露有限 API
示例:
```js
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)),
})
```
主进程:
```js
const { ipcMain } = require('electron')
ipcMain.handle('start-all', async () => {
await openAppWindow()
return { ok: true, backendUrl }
})
```
#### 第七步:配置 electron-builder
示例 `desktop/electron-builder.config.cjs`
```js
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 脚本串联所有步骤。伪代码如下:
```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 构建链路
```text
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 运行链路
```text
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 主进程中写死源码路径。所有资源路径都通过类似函数统一生成:
```js
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 数据目录规范
桌面应用不要把用户数据写入安装目录。建议:
```text
%APPDATA%/应用名/
data/
logs/
cache/
config/
```
后端通过环境变量读取:
```text
APP_DATA_DIR
```
Electron 负责传入:
```js
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 构建前检查
执行:
```powershell
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 本地开发运行检查
开发模式建议分别检查:
```powershell
cd chatlab-web/frontend
npm run build
```
```powershell
cd chatlog_fastAPI
python run_backend.py
```
```powershell
cd electron-launcher
npm run start
```
重点看:
- Electron 启动页能打开。
- FastAPI 能启动。
- `/health` 返回正常。
- 前端静态页面能被后端托管。
- chatlog 服务能启动并返回群聊或会话数据。
### 9.3 打包检查
执行:
```powershell
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 机器或虚拟机上安装:
1. 双击安装包。
2. 确认桌面快捷方式和开始菜单快捷方式生成。
3. 启动应用。
4. 确认启动页显示正常。
5. 点击启动或进入系统。
6. 确认 FastAPI 启动成功。
7. 确认 chatlog 启动成功。
8. 确认主界面加载成功。
9. 确认关闭窗口后无残留 `ChatLabBackend.exe``chatlog.exe`
10. 确认用户数据写入 `%APPDATA%/ChatLab`,不是安装目录。
### 9.5 发布前安全检查
检查 `release/manifest.txt`,确认没有:
```text
.env
knowledge*.db
__pycache__
*.pfx
*.p12
*.pvk
*.cer
*.crt
*.key
certs/
```
签名包额外执行:
```powershell
Get-AuthenticodeSignature .\release\ChatLab-Setup-*.exe
```
期望:
```text
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 首次准备环境
```powershell
# 前端依赖
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 本地测试构建
```powershell
cd 项目根目录
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1
```
### 12.3 只准备资源,不生成安装包
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1 -SkipInstaller
```
### 12.4 前端没改,只重打后端和安装包
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1 -SkipFrontend
```
### 12.5 后端没改,只重打前端和安装包
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\build-desktop.ps1 -SkipBackend
```
### 12.6 正式签名发布
```powershell
$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、构建脚本统一编排”的原则就可以复用当前项目的打包方法。