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

39 KiB
Raw Permalink Blame History

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 架构:

  1. React 前端继续用 Vite 构建成静态资源。
  2. FastAPI 后端继续提供 HTTP API并在生产环境托管前端静态文件。
  3. chatlog.exe 作为本地外部二进制程序,由 Electron 主进程启动和管理。
  4. Electron 只负责桌面窗口、启动控制、子进程生命周期、资源路径适配和安装包能力。
  5. PyInstaller 把 Python 后端打成独立的 Windows 可执行目录。
  6. 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_DIRCHATLAB_STATIC_DIRCHATLAB_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 数据文件
  • 显式收集 uvicornfastapipydantic_settingsaiosqliteapscheduler 等 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-builderextraResources,把它们作为普通文件复制到安装目录的 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

该脚本默认执行完整构建:

  1. 生成或刷新 Electron 图标。
  2. 构建 React 前端。
  3. 用 PyInstaller 构建 Python 后端。
  4. 重置 electron-launcher/build-resources
  5. 复制 chatlog.exelib、前端 dist、后端 dist/ChatLabBackend、许可文件。
  6. 扫描敏感文件,阻止 .envknowledge*.db、证书、私钥、缓存等进入发布资源。
  7. 生成 release/manifest.txt
  8. 调用 electron-builder 生成 Windows NSIS 安装包。
  9. 将安装包和 blockmap 复制到 release/
  10. 如启用签名,校验安装包签名状态。

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.crtcerts/ 进入发布资源。

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.exefrontend/chatlog.exelib/ 的绝对路径。

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: false
  • contextIsolation: 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 主进程里已经对 apiKeydata_keyimg_keysk-xxxx 做了日志脱敏。

8.5 安全规范

正式打包前必须检查:

  • .env 不进入安装包。
  • 测试数据库不进入安装包。
  • 用户数据不进入安装包。
  • 证书和私钥不进入安装包。
  • __pycache__ 不进入安装包。
  • 日志文件不进入安装包。
  • API Key 不写死到前端代码。
  • Electron 渲染进程不启用 Node 权限。

当前项目的安全策略包括:

  • build-desktop.ps1 构建前扫描 build-resourcesrelease
  • electron-builder.config.cjsextraResources.filter 中排除敏感文件。
  • preload.js 通过 contextBridge 暴露有限 IPC。
  • mainWindow 设置 nodeIntegration: falsecontextIsolation: 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 机器或虚拟机上安装:

  1. 双击安装包。
  2. 确认桌面快捷方式和开始菜单快捷方式生成。
  3. 启动应用。
  4. 确认启动页显示正常。
  5. 点击启动或进入系统。
  6. 确认 FastAPI 启动成功。
  7. 确认 chatlog 启动成功。
  8. 确认主界面加载成功。
  9. 确认关闭窗口后无残留 ChatLabBackend.exechatlog.exe
  10. 确认用户数据写入 %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.closebefore-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.exelib 新项目实际外部资源
敏感文件规则 .envknowledge*.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、构建脚本统一编排”的原则就可以复用当前项目的打包方法。