# 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 /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、构建脚本统一编排”的原则,就可以复用当前项目的打包方法。