首页/新闻资讯/正文详情

Electron 打包 SillyTavern:从命令行到桌面应用实战

发布时间:2026/9/26 21:56:24 来源:云帆数科 栏目:资讯中心
Electron 打包 SillyTavern:从命令行到桌面应用实战
1. 为什么要把 SillyTavern 从命令行搬进桌面窗口SillyTavern 这个项目玩过 AI 角色扮演或者本地大模型对话的人应该都不陌生。它本质上是一个跑在本地的前端界面通过 Node.js 启动一个服务然后你在浏览器里打开localhost:8000来使用。功能确实强大角色卡管理、世界书、多模型接入、提示词模板该有的都有。但问题也很明显每次要用的时候你得先打开终端敲一行node server.js或者npm start等它跑起来再手动打开浏览器输入地址。用完之后如果关掉了终端窗口服务就断了。对于天天折腾的开发者来说这不算事但对于只是想安安静静聊个天的普通用户这套流程属实有点劝退。我身边就有朋友每次想用都得问我一遍“那个命令是啥来着”。后来我干脆给他写了个批处理脚本双击就能启动。但批处理脚本有个毛病会弹出一个黑框框关掉黑框服务就停了而且没法做系统托盘、没法做自动更新、没法控制窗口大小。于是我就动了把它打包成真正桌面应用的念头。这个项目的核心目标很明确把 SillyTavern 这个基于 Node.js 的 Web 应用通过 Electron 包装成一个独立的桌面程序。最终交付的成果是一个.exe文件Windows 平台用户双击图标就能启动不需要安装 Node.js不需要打开命令行不需要手动开浏览器。程序启动后自动在内部加载 SillyTavern 的服务窗口直接显示界面关闭窗口时服务也随之停止。整个过程对用户完全透明。适合谁来参考这篇内容如果你手头有一个 Node.js 的 Web 项目想把它变成桌面应用分发或者你本身就是 SillyTavern 的用户想自己定制一个桌面版本再或者你只是想了解一下 Electron 打包 Node 服务的完整流程那这篇东西应该能帮到你。我会把整个打包过程拆开揉碎从环境准备到最终出包包括中间踩过的坑和解决方案尽量做到你照着做就能跑通。2. 整体方案设计与技术选型拆解2.1 为什么选 Electron 而不是 Tauri 或 NW.js把 Web 应用变成桌面应用市面上主流的方案有三个Electron、Tauri、NW.js。我最终选了 Electron原因有几个。首先是生态成熟度。Electron 出来的时间最长社区资源最丰富遇到问题基本都能搜到答案。SillyTavern 本身就是一个 Node.js 项目Electron 的主进程天然就是 Node 环境可以直接在内部调用 SillyTavern 的启动逻辑不需要做额外的适配层。这一点非常关键因为 SillyTavern 的服务端代码依赖了大量的 Node 原生模块和文件系统操作如果用一个纯 WebView 方案这些都得重写。其次是打包工具链。Electron 有 electron-builder 和 electron-forge 两套成熟的打包方案生成 Windows 安装包、便携版、macOS 的 dmg、Linux 的 AppImage 都很方便。Tauri 虽然包体积小、性能好但它用的是 Rust 后端和系统 WebView对于需要内嵌 Node 运行时的场景来说反而更麻烦。NW.js 虽然也是 Node 环境但社区活跃度不如 Electron文档也相对零散。再一个考虑是调试便利性。Electron 可以直接打开 DevTools主进程和渲染进程都能调试对于排查 SillyTavern 前端加载问题很有帮助。而且 Electron 的 IPC 通信机制很清晰主进程负责启动服务、管理窗口渲染进程负责显示界面职责分明。当然 Electron 也有缺点最明显的就是包体积大。一个最简单的 Electron 应用打包出来至少 150MB 起步因为要把整个 Chromium 和 Node 运行时都塞进去。但考虑到 SillyTavern 本身依赖的 node_modules 也不小最终打包出来大概 300-400MB对于桌面应用来说还能接受。如果你对体积特别敏感可以考虑 Tauri但那就需要把 SillyTavern 的服务端逻辑用 Rust 重写或者用 sidecar 方式调用外部二进制复杂度会高很多。2.2 主进程与渲染进程的职责划分Electron 的架构是主进程加渲染进程的模式。主进程负责创建窗口、管理应用生命周期、调用系统 API渲染进程就是一个个网页负责显示界面。在我们的场景里职责划分是这样的主进程要做的事情包括启动 SillyTavern 的 HTTP 服务、创建 BrowserWindow、加载 SillyTavern 的界面地址、处理窗口关闭事件时停止服务、创建系统托盘图标、处理单实例锁防止用户重复双击打开多个窗口。渲染进程要做的事情很简单就是显示 SillyTavern 的界面。因为 SillyTavern 本身就是一个完整的 Web 应用我们不需要在渲染进程里写任何业务逻辑只需要让 BrowserWindow 加载http://localhost:8000就行了。这里有一个关键点SillyTavern 的服务启动是异步的需要等它监听端口成功之后才能加载界面。如果在服务还没起来的时候就加载 URL会显示连接失败。所以主进程里需要有一个等待机制轮询检测端口是否可用或者监听服务启动的回调。2.3 服务启动方式的两种选择把 SillyTavern 的服务跑起来有两种思路。第一种是子进程方式。在主进程里用child_process.spawn启动一个独立的 Node 进程来跑 SillyTavern 的server.js。这种方式的优点是隔离性好服务崩溃了不会影响主进程而且可以直接复用 SillyTavern 原有的启动脚本。缺点是进程管理稍微复杂一点需要在应用退出时确保子进程也被杀掉否则会留下僵尸进程。第二种是同进程方式。直接在主进程里requireSillyTavern 的服务端代码让它在主进程的 Node 环境里运行。这种方式更简单不需要管理子进程但缺点是如果服务端代码有全局状态或者阻塞操作可能会影响主进程的响应性。而且 SillyTavern 的代码可能有一些和 Electron 主进程不兼容的地方需要做适配。我最终选了子进程方式。虽然多了一层管理但稳定性更好而且 SillyTavern 的版本更新时我只需要替换服务端代码目录不需要重新打包整个 Electron 应用。这对于后续维护来说方便很多。3. 环境准备与项目结构搭建3.1 基础环境要求在开始之前你需要确保本机已经安装了以下工具Node.js建议用 18.x 或 20.x 的 LTS 版本。SillyTavern 对 Node 版本有要求太老的版本可能不支持某些语法特性。我实测 18.18.0 和 20.11.0 都没问题。npm随 Node.js 一起安装的就行版本 9 以上。Git用来克隆 SillyTavern 的源码如果你直接下载压缩包也可以不用。Windows 系统这篇内容以 Windows 打包为例macOS 和 Linux 的流程类似但有些细节不同。不需要全局安装 Electron我们会在项目里作为开发依赖安装。也不需要安装 Visual Studio 或者 Python除非你在安装依赖时遇到了需要编译原生模块的情况。SillyTavern 的依赖里有一些可能需要编译的包但大多数情况下都有预编译的二进制文件可用。3.2 创建 Electron 项目骨架先找一个空目录作为我们的 Electron 包装项目。我给它起名叫sillytavern-desktop。mkdir sillytavern-desktop cd sillytavern-desktop npm init -y然后安装 Electron 作为开发依赖npm install electron --save-dev这里注意一下版本选择。Electron 的版本更新很快新版本可能引入一些 breaking change。我建议用比较稳定的版本比如 28.x 或者 29.x。太老的版本可能不支持新的 Node API太新的版本可能和 electron-builder 有兼容性问题。npm install electron29.1.0 --save-dev接下来安装 electron-builder这是用来打包最终可执行文件的工具npm install electron-builder --save-dev3.3 把 SillyTavern 源码放进项目SillyTavern 的源码可以从它的官方仓库获取。你可以用 git clone也可以直接下载 release 包。我建议用 git clone方便后续更新。git clone https://github.com/SillyTavern/SillyTavern.git server这里我把 SillyTavern 的代码放在server目录下和 Electron 的主进程代码分开。这样做的好处是结构清晰而且打包的时候可以单独处理服务端代码。克隆完成之后进入 server 目录安装依赖cd server npm install cd ..这一步可能会花一些时间SillyTavern 的依赖比较多。如果遇到网络问题可以考虑配置 npm 的镜像源。安装完成后你可以先试试能不能正常启动cd server node server.js如果能看到类似SillyTavern is listening on port 8000的输出说明服务端没问题。按 CtrlC 停掉继续下一步。3.4 项目目录结构规划经过上面的步骤我们的项目目录结构应该是这样的sillytavern-desktop/ ├── package.json ├── main.js # Electron 主进程入口 ├── preload.js # 预加载脚本可选 ├── server/ # SillyTavern 服务端代码 │ ├── server.js │ ├── package.json │ ├── node_modules/ │ └── ... ├── build/ # 打包资源图标等 │ ├── icon.ico │ └── icon.png └── dist/ # 打包输出目录自动生成这个结构比较清晰主进程代码在根目录服务端代码在 server 子目录打包资源放在 build 目录。接下来我们逐个文件来写。4. 核心代码实现与关键细节4.1 主进程启动逻辑主进程的入口文件是main.js这是整个桌面应用的核心。它需要完成以下几件事启动 SillyTavern 服务、等待服务就绪、创建窗口、加载界面、处理退出清理。先来看启动服务的部分。我们用child_process.spawn来启动 SillyTavernconst { app, BrowserWindow, Tray, Menu, shell } require(electron); const { spawn } require(child_process); const path require(path); const net require(net); let mainWindow null; let tray null; let serverProcess null; const SERVER_PORT 8000; const SERVER_HOST 127.0.0.1; function startServer() { return new Promise((resolve, reject) { const serverPath path.join(__dirname, server, server.js); const serverDir path.join(__dirname, server); serverProcess spawn(process.execPath, [serverPath], { cwd: serverDir, env: { ...process.env, NODE_ENV: production, ELECTRON_RUN_AS_NODE: 1 }, stdio: [ignore, pipe, pipe] }); serverProcess.stdout.on(data, (data) { console.log([Server] ${data.toString().trim()}); }); serverProcess.stderr.on(data, (data) { console.error([Server Error] ${data.toString().trim()}); }); serverProcess.on(error, (err) { reject(err); }); serverProcess.on(exit, (code) { console.log(Server process exited with code ${code}); }); // 轮询检测端口是否可用 waitForPort(SERVER_HOST, SERVER_PORT, 30000) .then(resolve) .catch(reject); }); }这里有几个关键点需要解释。第一process.execPath在 Electron 环境下指向的是 Electron 的可执行文件而不是系统的 Node。我们通过设置ELECTRON_RUN_AS_NODE1环境变量让 Electron 以纯 Node 模式运行这样就能直接执行server.js了。这个技巧非常实用因为打包后的应用里没有独立的 node 可执行文件只能复用 Electron 自带的 Node 运行时。第二stdio配置为[ignore, pipe, pipe]这样我们可以捕获服务端的输出日志方便调试。如果你不需要日志可以改成ignore。第三waitForPort函数用来轮询检测端口是否已经监听。SillyTavern 启动需要一定时间特别是第一次启动时要初始化各种配置。我们不能在 spawn 之后立刻加载界面必须等端口真正可用。4.2 端口检测与等待机制waitForPort的实现如下function waitForPort(host, port, timeout) { const startTime Date.now(); const interval 500; return new Promise((resolve, reject) { const check () { if (Date.now() - startTime timeout) { reject(new Error(Timeout waiting for ${host}:${port})); return; } const socket new net.Socket(); socket.setTimeout(1000); socket.on(connect, () { socket.destroy(); resolve(); }); socket.on(timeout, () { socket.destroy(); setTimeout(check, interval); }); socket.on(error, () { socket.destroy(); setTimeout(check, interval); }); socket.connect(port, host); }; check(); }); }这个函数的逻辑很简单每隔 500 毫秒尝试连接一次目标端口如果连接成功就 resolve如果超时就 reject。超时时间设为 30 秒一般 SillyTavern 在 10 秒内就能启动完成。如果你的机器比较慢或者依赖比较多可以适当调大这个值。注意这里用net.Socket而不是http.get来检测是因为 TCP 连接检测更底层不受 HTTP 协议层面的影响。有些服务可能还没完全准备好处理 HTTP 请求但端口已经监听了这时候用 TCP 检测更准确。4.3 窗口创建与界面加载服务就绪之后就可以创建窗口了function createWindow() { mainWindow new BrowserWindow({ width: 1280, height: 800, minWidth: 800, minHeight: 600, icon: path.join(__dirname, build, icon.ico), webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, preload.js) }, show: false }); mainWindow.loadURL(http://${SERVER_HOST}:${SERVER_PORT}); mainWindow.once(ready-to-show, () { mainWindow.show(); }); mainWindow.on(closed, () { mainWindow null; }); // 外部链接用系统浏览器打开 mainWindow.webContents.setWindowOpenHandler(({ url }) { shell.openExternal(url); return { action: deny }; }); }窗口配置里有几个值得注意的地方。show: false配合ready-to-show事件可以避免窗口白屏闪烁。Electron 创建窗口后默认会立刻显示但这时候页面还没加载完会看到一片空白。等页面渲染完成后再显示体验会好很多。nodeIntegration: false和contextIsolation: true是安全配置。因为 SillyTavern 的界面代码是运行在浏览器环境里的不应该有访问 Node API 的权限。虽然我们加载的是本地服务但保持这个安全边界是好习惯。setWindowOpenHandler用来处理页面里的target_blank链接。默认情况下 Electron 会弹出一个新窗口但那个新窗口没有地址栏也没有控制按钮体验很差。我们把它改成用系统默认浏览器打开更符合用户预期。4.4 应用生命周期管理Electron 应用的生命周期事件需要仔细处理否则容易出现服务没关干净或者窗口关了但进程还在的情况const gotTheLock app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); } else { app.on(second-instance, () { if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); } }); app.whenReady().then(async () { try { await startServer(); createWindow(); createTray(); } catch (err) { console.error(Failed to start:, err); app.quit(); } }); } app.on(window-all-closed, () { if (process.platform ! darwin) { app.quit(); } }); app.on(before-quit, () { if (serverProcess) { serverProcess.kill(); serverProcess null; } });requestSingleInstanceLock用来防止用户重复双击图标打开多个实例。如果已经有实例在运行第二个实例会直接退出并把焦点还给第一个实例的窗口。before-quit事件里杀掉服务进程确保应用退出时不会留下后台进程。这里用kill()发送的是 SIGTERM 信号SillyTavern 收到后会正常退出。如果某些情况下进程没有响应可能需要用kill(SIGKILL)强制杀掉但那样可能会导致数据没保存所以优先用 SIGTERM。4.5 系统托盘与最小化到托盘系统托盘是个加分项让用户可以把窗口收起来但保持服务运行function createTray() { const iconPath path.join(__dirname, build, icon.ico); tray new Tray(iconPath); const contextMenu Menu.buildFromTemplate([ { label: 显示窗口, click: () { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } } }, { label: 退出, click: () { app.quit(); } } ]); tray.setToolTip(SillyTavern Desktop); tray.setContextMenu(contextMenu); tray.on(double-click, () { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } }); }托盘的图标需要是.ico格式Windows或者.png格式macOS/Linux。图标尺寸建议用 256x256这样在不同 DPI 下都能清晰显示。如果你想让关闭窗口时最小化到托盘而不是退出应用可以修改window-all-closed的处理逻辑改成隐藏窗口而不是退出。但这样用户可能会困惑为什么应用还在运行所以需要在托盘菜单里提供明确的退出选项。5. 打包配置与出包流程5.1 package.json 配置electron-builder 的配置可以放在package.json的build字段里也可以单独用一个electron-builder.yml文件。我习惯放在package.json里方便统一管理{ name: sillytavern-desktop, version: 1.0.0, description: SillyTavern Desktop Application, main: main.js, scripts: { start: electron ., build: electron-builder --win, build:portable: electron-builder --win portable }, build: { appId: com.sillytavern.desktop, productName: SillyTavern, directories: { output: dist, buildResources: build }, files: [ main.js, preload.js, server/**/*, !server/node_modules/.cache/**/*, !server/data/**/* ], extraResources: [ { from: server, to: server, filter: [**/*, !node_modules/.cache/**/*] } ], win: { target: [ { target: nsis, arch: [x64] }, { target: portable, arch: [x64] } ], icon: build/icon.ico }, nsis: { oneClick: false, allowToChangeInstallationDirectory: true, createDesktopShortcut: true, createStartMenuShortcut: true } }, devDependencies: { electron: ^29.1.0, electron-builder: ^24.13.3 } }这里有几个配置项需要重点说明。files字段定义了哪些文件会被打包进 asar 归档。注意我们把server/**/*也包含进去了但排除了node_modules/.cache和data目录。data目录是 SillyTavern 运行时生成的数据不应该打包进去否则每次安装都是旧数据。extraResources用来把文件复制到 resources 目录而不是打进 asar。为什么要把 server 目录单独复制出来因为 asar 归档里的文件是只读的而 SillyTavern 运行时需要写入数据比如角色卡、聊天记录、配置文件。如果把 server 放在 asar 里写入操作会失败。所以我们需要把 server 目录作为额外资源复制到 resources 目录下运行时从那里读取和写入。这也意味着主进程里引用 server 路径的方式需要调整。在开发环境下server 目录在项目根目录在打包后server 目录在process.resourcesPath下。所以路径解析逻辑要兼容两种情况function getServerPath() { if (app.isPackaged) { return path.join(process.resourcesPath, server); } return path.join(__dirname, server); }5.2 图标资源准备图标是打包过程中容易被忽视但很重要的部分。Windows 需要.ico格式macOS 需要.icns格式Linux 用.png就行。.ico文件不是简单地把 png 改后缀名它实际上是一个包含多个尺寸图像的容器格式。你需要准备 16x16、32x32、48x48、256x256 等多个尺寸的图标然后用工具合成一个.ico文件。我常用的是在线工具或者 ImageMagickmagick convert icon-16.png icon-32.png icon-48.png icon-256.png icon.ico如果你没有现成的图标可以先用一个简单的占位图标后面再替换。但注意图标文件不能是空的否则 electron-builder 会报错。5.3 执行打包配置写好之后执行打包命令npm run buildelectron-builder 会自动下载对应平台的 Electron 二进制文件然后按照配置进行打包。第一次打包会花比较长的时间因为要下载 Electron 的预编译包大概 100MB 左右。下载完成后会缓存到本地后续打包就快了。打包完成后dist目录下会生成以下文件SillyTavern Setup 1.0.0.exeNSIS 安装包用户双击安装后会在开始菜单和桌面创建快捷方式。SillyTavern 1.0.0.exe便携版双击直接运行不需要安装。我一般会同时生成这两种安装版适合长期使用便携版适合放在 U 盘里随身携带。5.4 打包后的体积优化前面提到过Electron 应用打包出来体积不小。我实测这个项目打包后大概 350MB 左右其中 Electron 运行时占了 200MB 左右SillyTavern 的 node_modules 占了 100MB 左右。如果想减小体积有几个方向可以尝试。一是清理不必要的依赖。SillyTavern 的 node_modules 里可能有一些只在开发时用到的包可以在打包前用npm prune --production清理掉 devDependencies。但要注意有些包虽然在 devDependencies 里运行时却需要清理前要确认清楚。二是使用 asar 压缩。electron-builder 默认会把文件打包成 asar 归档这本身就有一定的压缩效果。但 server 目录因为需要写入权限不能放进 asar所以这部分没法压缩。三是考虑用 electron-builder 的compression选项。可以设置为maximum会用 7z 做最大压缩但打包时间会变长。compression: maximum不过说实话对于桌面应用来说350MB 和 250MB 的差别用户感知不强除非你有特别的分发需求否则不用太纠结体积。6. 常见问题与排查技巧实录6.1 服务启动失败端口被占用这是最常见的问题。如果 8000 端口已经被其他程序占用了SillyTavern 启动会失败然后我们的等待逻辑会超时最终应用启动失败。排查方法很简单在命令行里执行netstat -ano | findstr :8000如果能看到输出说明端口被占用了。最后一列是进程 ID可以用任务管理器找到对应进程并结束它。更优雅的方案是在代码里动态选择一个可用端口。可以写一个函数从 8000 开始尝试如果被占用就试 8001、8002直到找到可用的function findAvailablePort(startPort) { return new Promise((resolve, reject) { const server net.createServer(); server.unref(); server.on(error, () { resolve(findAvailablePort(startPort 1)); }); server.listen(startPort, () { const port server.address().port; server.close(() resolve(port)); }); }); }然后在启动服务前先调用这个函数获取可用端口再把端口通过环境变量传给 SillyTavern。不过 SillyTavern 是否支持通过环境变量指定端口需要看它的配置。如果不支持可能需要修改它的配置文件或者启动参数。6.2 打包后白屏或加载失败开发环境下运行正常打包后打开是白屏这种情况通常是路径问题。首先检查server目录是否正确复制到了 resources 下。可以打开安装目录进入resources文件夹看看有没有server目录。如果没有说明extraResources配置有问题。其次检查服务是否真的启动了。可以在主进程里加日志输出把服务端的 stdout 和 stderr 都打印出来。如果服务启动失败日志里会有错误信息。还有一个可能的原因是防火墙拦截。Windows 防火墙可能会阻止 Electron 应用监听端口或者发起本地连接。第一次运行时系统会弹出防火墙提示要选择“允许访问”。如果之前误点了“取消”可以在防火墙设置里手动添加例外。6.3 关闭窗口后进程残留有时候关闭了应用窗口但任务管理器里还能看到进程。这通常是因为服务子进程没有被正确杀掉。检查before-quit事件里是否调用了serverProcess.kill()。另外要注意如果用户是通过任务管理器强制结束的before-quit不会触发子进程可能会残留。可以在服务端代码里加一个心跳检测如果父进程消失了就自动退出。但这就涉及到修改 SillyTavern 的源码了比较麻烦。一个折中方案是在应用启动时检查是否有残留的旧进程如果有就先杀掉。可以通过记录进程 ID 到临时文件的方式来实现但增加了复杂度。对于大多数场景确保正常退出时清理干净就够了。6.4 数据丢失或配置不保存前面提到过asar 里的文件是只读的。如果 SillyTavern 尝试往 asar 里的路径写数据会静默失败或者报错。确保server目录是通过extraResources复制到 resources 下的而不是打进 asar。同时检查 SillyTavern 的数据目录配置确保它指向的是可写路径。SillyTavern 默认会把数据存在server/data目录下。在打包后的环境里这个路径应该是resources/server/data。如果用户安装到了C:\Program Files下这个目录可能需要管理员权限才能写入。所以 NSIS 安装包默认会安装到%LOCALAPPDATA%\Programs下避免权限问题。如果你发现数据没有保存可以检查一下resources/server/data目录是否存在以及当前用户是否有写入权限。6.5 常见问题速查表问题现象可能原因排查方法解决方案启动后白屏服务未就绪或路径错误查看主进程日志检查 resources/server 是否存在修正 extraResources 配置增加等待时间端口被占用8000 端口已被其他程序使用netstat -ano | findstr :8000关闭占用程序或动态选择端口打包后体积过大Electron 运行时和依赖较多检查 node_modules 大小清理 devDependencies使用 maximum 压缩关闭后进程残留子进程未正确终止任务管理器查看进程确保 before-quit 中调用 kill数据不保存写入路径只读检查 data 目录权限确保 server 目录在 resources 下而非 asar 内图标不显示图标格式或尺寸不对检查 build/icon.ico用多尺寸合成正确的 ico 文件防火墙拦截系统阻止本地端口监听查看防火墙通知允许应用通过防火墙6.6 几个实操心得第一个心得是关于开发调试的。在开发阶段不要每次都打包测试那样太慢了。可以直接用npm start运行 Electron它会加载本地的 server 目录。只有确认功能没问题了再执行打包。打包测试主要验证路径解析和资源复制是否正确。第二个心得是关于版本管理的。SillyTavern 更新比较频繁如果你想把新版本的服务端代码集成进来只需要替换server目录然后重新打包即可。Electron 主进程的代码一般不需要改动除非 SillyTavern 的启动方式或者端口有变化。所以建议把 Electron 包装层和 SillyTavern 服务端分开管理用 git submodule 或者单独的更新脚本。第三个心得是关于用户数据迁移的。如果你之前用命令行方式运行 SillyTavern数据存在原来的目录里。换成桌面版之后数据目录变了之前的角色卡和聊天记录需要手动复制过去。可以在应用里加一个“导入数据”的功能或者至少在文档里说明数据目录的位置。我在实际打包过程中还遇到过一个比较隐蔽的问题SillyTavern 的某些依赖在打包后会找不到。原因是这些依赖在安装时编译了原生模块而打包后的 Electron 运行时版本和安装依赖时的 Node 版本不一致导致二进制不兼容。解决方法是使用electron-rebuild重新编译原生模块npm install electron-rebuild --save-dev npx electron-rebuild但这一步比较耗时而且不是所有依赖都需要。可以先打包测试如果遇到Module version mismatch之类的错误再针对性地 rebuild。最后再分享一个小技巧如果你想让应用启动更快可以在打包时把 SillyTavern 的 node_modules 用asar打包只读部分而把需要写入的 data 目录单独放在外面。这样既能减少文件数量加快加载又能保证写入正常。不过配置起来稍微复杂一点需要仔细规划哪些目录需要写入权限。

相关推荐

Windows应用残留注册表清理指南:精准识别与安全删除
Windows应用残留注册表清理指南:精准识别与安全删除

1. 这不是“卸载失败”,而是Windows的“残留登记制”在工作你点开“设置 → 应用 → 应用和功能”,手指划到列表底部,突然发现一个名字熟悉却图标灰暗、版本号为空、点击后只弹出“此应用无法卸载”的条目——它明明上周就被你用控制面板或软… · 2026/9/26 21:56:24

卷积自编码器与K-means聚类:无监督指纹图像识别实战指南
卷积自编码器与K-means聚类:无监督指纹图像识别实战指南

简介:这份zip压缩包(约9.71MB)汇集了武汉理工大学2020年数学建模暑期培训的论文与代码,主题为基于卷积自编码的指纹编码与k聚类模型。内容面向数学建模竞赛参与者、机器学习初学者及生物识别方向研究者,提供了一套从指… · 2026/9/26 21:56:17

Python大数据反电信诈骗系统:从号码清洗到风险评分的实战全解析
Python大数据反电信诈骗系统:从号码清洗到风险评分的实战全解析

简介:这是一套基于大数据与机器学习技术的反电信诈骗管理系统项目,开发语言以Python为主,面向课程设计、毕业设计、安全竞赛及实际业务研究者,提供从通信数据采集、风险识别到可视化管理的完整工程方案。压缩包大小约46.24MB&… · 2026/9/26 21:56:17

ZeosDBO 6.0.12源码编译与安装:老项目数据库访问层迁移的稳定选择
ZeosDBO 6.0.12源码编译与安装:老项目数据库访问层迁移的稳定选择

简介:zeosdbo-6.0.12 最新稳定版源码是一套面向 Delphi、C Builder、Kylix 等 Borland 编译器的原生数据库组件库。其核心效用在于通过统一的本地数据集与数据库组件,将 MySQL、PostgreSQL、InterBase、Firebird、MS SQL、Sybase 等多种数据库的差异封装… · 2026/9/26 22:24:54

深入自己.skill源码:6个Python工具如何解析聊天记录与照片,构建数字分身
深入自己.skill源码:6个Python工具如何解析聊天记录与照片,构建数字分身

深入自己.skill源码:6个Python工具如何解析聊天记录与照片,构建数字分身 【免费下载链接】yourself-skill 与其蒸馏别人,不如蒸馏自己。欢迎加入数字永生!Inspired by colleague-skill(同事skill)。 项目… · 2026/9/26 22:24:54

Win10右键新建菜单丢失文本文档?注册表ShellNew修复实战
Win10右键新建菜单丢失文本文档?注册表ShellNew修复实战

说个真实情况:前阵子给一台win10办公电脑装驱动,装完顺手右键一看,新建菜单里的“文本文档”不见了。按我以前的脾气,肯定先重装系统,但一台电脑装完系统再补软件,半天就没了。后来静下心查了一遍&#xff… · 2026/9/26 22:24:47

ZeosDBO 6.0.12源码编译与TZConnection数据库连接实战
ZeosDBO 6.0.12源码编译与TZConnection数据库连接实战

简介:ZeosDBO 6.0.12最新稳定版源码包,面向使用Delphi、C Builder、Kylix等Borland系开发工具的数据库应用开发者,旨在提供跨数据库统一访问的原生数据集与组件,覆盖MySQL、MSSQL、InterBase、Firebird、Sybase、PostgreSQL&#… · 2026/9/26 22:24:47

3步搞定网站活动模板:不会代码也能做出最佳实践
3步搞定网站活动模板:不会代码也能做出最佳实践

3步搞定网站活动模板:不会代码也能做出最佳实践 自己不会代码,却想快速上线一个高转化的活动页?这大概是很多运营和甲方最头疼的事。找外包太贵且慢,自己写代码又劝退,这时候 网站活动模板… · 2026/9/26 22:24:40

Win10右键“新建文本文档”消失?注册表ShellNew修复指南
Win10右键“新建文本文档”消失?注册表ShellNew修复指南

1. 问题还原与根源剖析:右键“新建”菜单是怎么把文本文档弄丢的 先说结论:Win10 右键“新建”菜单里的“文本文档”选项,本质上不是系统自己维护的一个固定项,而是靠注册表里的一个 Shell 扩展项动态生成的。我遇到过很多次这种情… · 2026/9/26 22:24:40

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码