Electron+FastAPI构建可控流式AI对话系统

发布时间:2026/9/26 23:42:16
Electron+FastAPI构建可控流式AI对话系统 1. 这不是“套壳网页”而是一套真正能跑起来的本地AI对话系统Electron 与 FastAPI 如何完成流式 Agent 对话——这句话背后藏着一个被很多人低估的现实市面上大量所谓“本地大模型桌面应用”其实只是把 Web 页面用 Electron 包了一层壳主进程几乎不参与业务逻辑所有 AI 推理、状态管理、流式控制全压在前端 Vue/React 里。一旦模型响应慢、网络抖动、用户频繁中断整个对话就卡死、错乱、上下文丢失。我去年帮三个团队重构过类似项目最典型的问题是用户点“停止”按钮后前端 JavaScript 已经 abort 了 fetch但 FastAPI 后端还在往 SSE 流里拼命写 token导致后续请求被阻塞或者 Electron 渲染进程崩溃重启Vue 实例一销毁整个对话历史、正在 streaming 的 chunk、甚至还没 flush 的 buffer 全没了。这根本不是“流式”而是“伪流式”。真正的流式 Agent 对话必须让 Electron 主进程成为对话状态的“守门人”FastAPI 成为流式数据的“精密泵浦”而渲染进程只负责“忠实呈现”。三者分工明确主进程管生命周期启动/终止/重连/上下文快照、管 IPC 路由把用户输入精准转发给 FastAPI把 SSE 数据安全拆包传给 Vue、管资源兜底内存监控、进程保活、异常熔断FastAPI 不只是个 API 服务器它要承担 Agent 的执行引擎角色——解析用户指令、调用工具、维护对话状态树、做 token 级别流控、支持 abort 信号穿透渲染进程则彻底轻量化只做 UI 渲染和事件捕获所有状态都从主进程同步绝不自己维护 history 数组。这个架构下“流式”才有了物理意义每个 token 都能被独立追踪、可中断、可回溯、可审计。你看到的不是“一行行蹦出来的文字”而是从 LLM 内核出发经 FastAPI 编排、IPC 封装、Vue 渲染的完整数据链路。它解决的不是“怎么让字出来”而是“怎么让每一次 token 输出都可控、可溯、可组合”。适合两类人一是想落地真实 Agent 应用的开发者需要稳定、可维护、能进生产环境的方案二是技术选型阶段的架构师需要看清 Electron FastAPI 组合在 AI 桌面场景中的真实能力边界与关键设计陷阱。下面我就从零开始把这套系统怎么搭、为什么这么搭、踩过哪些坑全盘托出。2. 整体架构设计为什么必须是“主进程调度 FastAPI 执行 渲染进程呈现”三权分立2.1 传统误区把 Electron 当成“高级浏览器壳”后果有多严重很多教程教你怎么用create-react-appelectron-forge快速打包一个 Chat UI然后在useEffect里发 fetch 请求到本地 FastAPI。这看似简单实则埋下三大隐患状态割裂Vue 的ref(history)和 FastAPI 的session_state完全无关。用户清空聊天记录只是删了前端数组但 FastAPI 里可能还存着上一轮的conversation_id和tool_calls历史。下次发请求Agent 可能基于错误上下文执行。流控失效前端用AbortController中断 fetch只能终止 HTTP 连接但 FastAPI 的StreamingResponse生成器还在运行。Python 的async for循环不会自动感知前端断开除非你手动检查request.is_disconnected()并break。而 Electron 渲染进程崩溃时这个连接根本不会被通知FastAPI 进程会持续占用 CPU 和内存直到超时。Agent 能力阉割真正的 Agent 需要记忆、工具调用、多步规划。这些逻辑如果全放在前端意味着你要用 JavaScript 实现 LLM 的 prompt engineering、function calling 解析、state machine 管理——这既不可靠JS 浮点精度、异步竞态也不安全密钥、工具参数暴露在客户端。我见过最离谱的案例某团队把 RAG 检索逻辑写在 Vue 的computed里每次用户输入就触发一次本地向量库搜索。结果用户打字快一点连续触发 5 次搜索Electron 渲染进程直接 OOM 崩溃而 FastAPI 还在后台默默生成 embedding……整个应用变成“输入即崩溃”。2.2 正确解法主进程作为“中央调度室”接管三大核心职责我们把 Electron 主进程从“窗口管理者”升级为“对话操作系统内核”。它不碰模型不写 prompt只干三件事对话生命周期管理创建唯一conversation_idUUID4全程透传给 FastAPI监听用户“新建对话”、“继续对话”、“删除对话”操作主动 kill 对应 FastAPI worker 进程通过child_process.kill()在 App 退出前自动序列化当前对话 state 到app.getPath(userData)/conversations/下的 JSON 文件含 timestamp、last_message、tool_historyIPC 通信路由与流式封装渲染进程发ipcRenderer.invoke(send-message, { cid, content })主进程收到后不直接调 FastAPI而是a) 校验cid是否合法查本地缓存或文件b) 构造标准 HTTP POST 请求体含X-Conversation-ID,Content-Type: application/jsonc) 用axios或node-fetch调用 FastAPI/chat/streamd) 接收 SSE 响应流逐 chunk 解析data: {...}过滤掉event:和id:行e) 将纯文本 token 或结构化对象如{ type: tool_call, name: web_search, args: {...} }通过ipcMain.handle返回给渲染进程资源与异常兜底用app.getAppMetrics()每 5 秒检测主进程内存占用超过 800MB 自动触发 GCglobal.gc()需开启--expose-gc参数FastAPI 进程异常退出时exit事件自动重启并恢复最近一次 conversation state渲染进程崩溃webContents.crashed事件立即弹窗提示“对话已保存点击恢复”而非白屏提示主进程绝不处理任何 LLM 推理逻辑。它的代码里不应该出现llm.generate()、tokenizer.encode()这类调用。它的价值在于“确定性”——确保每一次用户操作都有且仅有一个确定的执行路径和状态归宿。2.3 FastAPI 作为“Agent 执行引擎”必须突破 REST API 思维FastAPI 默认是 RESTful 的但 Agent 需要的是状态化、可中断、可组合的执行流。我们做了三处关键改造状态管理放弃 session拥抱 conversation_id不用request.sessionFlask 风格也不用全局 dict线程不安全。每个请求带X-Conversation-IDheaderFastAPI 用Depends(get_conversation_state)依赖注入从 Redis 或本地 SQLite 加载该会话的完整 state含 messages、tool_results、pending_tool_calls。这样即使 FastAPI 多进程部署状态也能一致。流式输出SSE token 级别控制/chat/stream接口返回StreamingResponse但关键在 yield 的内容async def stream_response(): # 1. 发送初始消息告诉前端“开始思考” yield event: start\ndata: {\status\:\thinking\}\n\n # 2. Agent 执行循环可中断 for token in agent_stream: if await request.is_disconnected(): logger.info(fClient disconnected for {cid}) break yield fdata: {{\type\:\token\,\content\:\{token}\}}\n\n # 3. 发送结束消息含最终 state final_state await get_conversation_state(cid) yield fevent: end\ndata: {json.dumps(final_state)}\n\n这样前端就能监听event: start/end做 loading 状态切换data: token做实时渲染event: end做状态持久化。Abort 信号穿透从浏览器到 LLM 内核用户点“停止”渲染进程发ipcRenderer.invoke(abort-stream, cid)→ 主进程发 HTTP DELETE 到/chat/abort/{cid}→ FastAPI 的abort_endpoint设置全局 flagABORT_FLAGS[cid] True→ Agent 执行循环中每 yield 一个 token 后检查if ABORT_FLAGS.get(cid): break。比单纯关 HTTP 连接更可靠。2.4 渲染进程做最薄的“UI 皮肤”拒绝任何状态自治Vue 3 Composition API 下我们只保留两个 reactive 对象// store/chat.ts export const chatStore reactive({ conversations: [] as Conversation[], // 从主进程同步非本地创建 currentConversation: null as Conversation | null, isStreaming: false, pendingMessage: }) // 绑定 IPC 事件 ipcRenderer.on(conversation-updated, (e, conv) { const idx chatStore.conversations.findIndex(c c.id conv.id) if (idx ! -1) chatStore.conversations[idx] conv if (conv.id chatStore.currentConversation?.id) { chatStore.currentConversation conv } })所有messages数组、isStreaming开关、pendingMessage输入框值全部来自主进程推送。Vue 不做任何push()、splice()、filter()操作。用户点击“发送”只触发ipcRenderer.invoke(send-message, ...)然后静等conversation-updated事件。这种“单向数据流”看似笨重却换来绝对的状态一致性——哪怕你同时开 5 个窗口只要它们订阅同一个conversation_id看到的永远是同一份实时状态。3. 核心细节实现从 IPC 协议设计到 SSE 流解析的完整链路3.1 IPC 协议设计为什么不用send而坚持用invokeElectron 的 IPC 有sendfire-and-forget、invokepromise-based、postMessage跨 context三种模式。对于 Agent 对话必须用invoke原因有三流式响应需要 Promise 链用户点击发送后UI 要立刻禁用输入框、显示 loading等第一个 token 到达才激活。如果用send你得自己维护一个 Mapcid, resolveFn还要处理超时、重复调用复杂度飙升。invoke天然返回 Promise.then()里直接更新 UI 状态。错误必须可捕获FastAPI 可能返回 422参数错误、503LLM 服务不可用、401认证失败。invoke的 Promise 会 reject 这些 HTTP 错误你可以在.catch()里统一弹窗提示send则需额外监听error事件分散处理逻辑。类型安全可保障TypeScript 下invoke可定义精确的参数和返回类型// main.ts ipcMain.handle(send-message, async (e, payload: SendMessagePayload) { return await callFastAPI(payload) // 返回 PromiseStreamResponse }) // renderer.ts const response await ipcRenderer.invoke(send-message, { cid: abc-123, content: 帮我查一下今天北京天气 })我们定义了 7 个核心 IPC 方法全部采用invoke方法名触发方用途返回值get-conversations渲染进程启动时获取所有历史对话列表Conversation[]create-conversation用户点“新建”创建新对话返回cid{ id: string }send-message用户点“发送”发送消息启动流式响应StreamResponse含 initial stateabort-stream用户点“停止”中断当前流式请求voiddelete-conversation用户右键删除删除对话及本地文件voidexport-conversation用户点“导出”导出 JSON 文件{ path: string }get-app-metrics设置页点击获取内存/CPU 使用率{ memory: number, cpu: number }注意所有 IPC 方法名必须小写加短横线kebab-case这是 Electron 官方推荐规范避免大小写混淆导致调试困难。3.2 FastAPI 项目目录结构如何组织才能支撑 Agent 复杂逻辑一个健康的 FastAPI Agent 项目绝不能是main.py一把梭。我们采用分层架构目录如下fastapi-agent/ ├── main.py # ASGI 入口只含 app FastAPI() 和 uvicorn.run() ├── api/ │ ├── __init__.py │ ├── v1/ # 版本化 API │ │ ├── __init__.py │ │ ├── chat.py # /chat/stream, /chat/abort 核心接口 │ │ └── health.py # /health 检查 LLM 服务状态 ├── core/ │ ├── __init__.py │ ├── agent/ # Agent 执行引擎 │ │ ├── __init__.py │ │ ├── base.py # BaseAgent 抽象类定义 execute, stream 方法 │ │ ├── openai.py # OpenAIAgent 实现调用 openai.ChatCompletion.create │ │ └── local.py # LocalAgent 实现调用 llama.cpp 或 ollama │ ├── state/ # 对话状态管理 │ │ ├── __init__.py │ │ ├── manager.py # ConversationStateManagerRedis/SQLite 适配器 │ │ └── models.py # Pydantic 模型Conversation, Message, ToolCall │ └── streaming/ # 流式工具 │ ├── __init__.py │ ├── sse.py # SSE 响应包装器自动添加 event/data/id │ └── tokenizer.py # Tokenizer 工具用于估算流式进度 ├── utils/ │ ├── __init__.py │ ├── logging.py # 结构化日志按 cid 打 tag │ └── security.py # API Key 验证从环境变量或 config.toml 读取 ├── config/ │ ├── __init__.py │ ├── settings.py # Settings 类继承 pydantic.BaseSettings │ └── config.toml # 生产配置model_name, api_key, redis_url └── tests/ # 单元测试重点测 Agent.execute 流程关键设计点Agent 分层抽象base.py定义async def stream(self, messages: List[Message]) - AsyncIterator[str]所有具体 AgentOpenAI、Local、Anthropic必须实现它。这样主进程调用时完全不关心底层模型只需agent get_agent(config.model_type); async for token in agent.stream(messages): ...。State Manager 可插拔manager.py提供get_conversation_state()和update_conversation_state()两个方法底层可切换 Redis分布式、SQLite单机轻量、甚至内存 Dict开发调试。切换只需改一行settings.STATE_BACKEND redis。SSE 工具独立封装sse.py提供SSEEventStream类自动处理data:前缀、\n\n分隔、event:类型标记。避免每个接口都手写字符串拼接降低出错概率。3.3 SSE 流解析渲染进程如何安全、高效地消费流式数据前端消费 SSE最大的坑是EventSourceAPI 不支持AbortController无法优雅中断。所以我们弃用EventSource改用fetchReadableStream代码如下// renderer/utils/sse-parser.ts export async function consumeSSE( url: string, onToken: (token: string) void, onEvent: (event: string, data: any) void, signal?: AbortSignal ) { const response await fetch(url, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ cid, content }), signal // 关键支持 abort }) if (!response.body) throw new Error(No response body) const reader response.body.getReader() const decoder new TextDecoder() let buffer try { while (true) { const { done, value } await reader.read() if (done) break buffer decoder.decode(value, { stream: true }) const lines buffer.split(\n) buffer lines.pop() || // 保留未完成的行 for (const line of lines) { if (!line.trim()) continue if (line.startsWith(data:)) { const data line.slice(5).trim() try { const parsed JSON.parse(data) if (parsed.type token) { onToken(parsed.content) } else { onEvent(data, parsed) } } catch (e) { console.warn(Invalid SSE data:, line) } } else if (line.startsWith(event:)) { const event line.slice(6).trim() onEvent(event, null) } } } } finally { reader.releaseLock() } }这个解析器解决了三个痛点中断可靠signal传入fetchreader.read()会自动 reject无需手动清理。内存安全stream: true让TextDecoder不缓存未完成的 UTF-8 字节避免中文乱码buffer变量只存未分行的碎片不累积整条流。错误容忍遇到非法 JSON只 warn 不 crash保证 UI 不卡死。在 Vue 组件中调用script setup const { pendingMessage, isStreaming } chatStore const sendMessage async () { if (!pendingMessage.trim()) return isStreaming true try { await consumeSSE( /api/v1/chat/stream, (token) { // 追加到当前消息的 content 字段 const lastMsg chatStore.currentConversation?.messages.at(-1) if (lastMsg lastMsg.role assistant) { lastMsg.content token } }, (event, data) { if (event start) { // 插入新的 assistant 消息占位符 chatStore.currentConversation?.messages.push({ role: assistant, content: , timestamp: Date.now() }) } else if (event end) { // 更新最终 state触发持久化 chatStore.currentConversation data } }, abortController.signal // 来自组件的 AbortController ) } catch (e) { if (e.name AbortError) { console.log(Stream aborted by user) } else { ElMessage.error(请求失败: ${e.message}) } } finally { isStreaming false } } /script3.4 对话状态管理短期记忆如何在 Electron FastAPI 架构中落地“人的短期记忆怎么实现”——这不是心理学问题而是工程问题。我们的方案是三层记忆叠加。L1渲染进程内存毫秒级Vue 的ref(messages)存最新 5 条消息用于 UI 快速响应。但它不参与任何逻辑只是currentConversation.messages的只读投影。用户滚动查看历史触发ipcRenderer.invoke(get-conversation, cid)从主进程拉全量数据。L2主进程内存分钟级主进程用Mapstring, Conversation缓存最近 10 个活跃对话。Key 是cidValue 是完整Conversation对象含 messages、tools、metadata。当渲染进程请求get-conversation优先查此 Map未命中再从磁盘或 FastAPI 加载。缓存 TTL 设为 5 分钟超时自动delete。L3FastAPI 持久化永久级FastAPI 的ConversationStateManager每次update_conversation_state()都写入 SQLite开发或 Redis生产。SQLite 表结构CREATE TABLE conversations ( id TEXT PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, messages TEXT NOT NULL, -- JSON array of messages tools TEXT, -- JSON array of tool calls metadata TEXT -- JSON object for custom fields );关键点messages字段存完整 message list不是增量 diff。这样恢复时SELECT messages FROM conversations WHERE id?一条 SQL 就拿到全部上下文避免 N1 查询。实操心得不要试图在 SQLite 里做“消息 diff 存储”。我试过只存 delta新增的 message结果发现恢复时要按时间戳排序、合并、去重代码复杂度翻倍且极易出错。直接存全量 JSON用 SQLite 的json_extract()函数查特定字段性能足够好1000 条消息 5ms。4. 实操全流程从初始化到流式渲染的 12 个关键步骤4.1 步骤 1-3环境准备与项目初始化Step 1安装 Node.js 与 Python 环境必须使用 Node.js 18支持fetch和AbortSignalPython 3.9FastAPI 依赖。验证命令node -v # v18.17.0 python -c import sys; print(sys.version) # 3.9.0注意Windows 用户务必关闭 Windows Defender 实时保护否则electron-builder打包时会被误杀。Mac 用户需在electron-builder.yml中配置hardenedRuntime: true否则上架 Mac App Store 会被拒。Step 2创建 Electron 主进程骨架mkdir electron-fastapi-agent cd electron-fastapi-agent npm init -y npm install electron24.0.0 --save-dev npm install electron/remote --save-prod # 用于渲染进程调用主进程 API可选main.js最小化启动const { app, BrowserWindow, ipcMain } require(electron) const path require(path) const { spawn } require(child_process) function createWindow() { const win new BrowserWindow({ width: 1200, height: 800, webPreferences: { nodeIntegration: true, contextIsolation: false, // 开发期方便生产需设为 true preload.js enableRemoteModule: true } }) win.loadFile(index.html) } app.whenReady().then(() { createWindow() app.on(activate, () BrowserWindow.getAllWindows().length 0 createWindow()) }) app.on(window-all-closed, () process.platform ! darwin app.quit())Step 3初始化 FastAPI 后端mkdir fastapi-backend cd fastapi-backend python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install fastapi uvicorn python-dotenv pydantic-settingsmain.pyfrom fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app FastAPI(titleAgent API, version0.1.0) app.add_middleware( CORSMiddleware, allow_origins[http://localhost:3000], # Electron 开发时的 origin allow_credentialsTrue, allow_methods[*], allow_headers[*], ) app.get(/health) def health_check(): return {status: ok}启动命令uvicorn main:app --host 127.0.0.1 --port 8000 --reload4.2 步骤 4-6IPC 通信与 FastAPI Agent 接口打通Step 4主进程启动 FastAPI 子进程在main.js中添加let fastapiProcess null function startFastAPI() { if (fastapiProcess !fastapiProcess.killed) return const pythonPath process.env.PYTHON_PATH || python const backendPath path.join(__dirname, fastapi-backend) fastapiProcess spawn(pythonPath, [ -m, uvicorn, main:app, --host, 127.0.0.1, --port, 8000, --reload, --reload-dir, backendPath ], { cwd: backendPath, stdio: [ignore, pipe, pipe] }) fastapiProcess.stdout.on(data, (data) { console.log([FastAPI], data.toString()) }) fastapiProcess.stderr.on(data, (data) { console.error([FastAPI ERROR], data.toString()) }) fastapiProcess.on(close, (code) { console.log(FastAPI exited with code ${code}) if (code ! 0) { // 自动重启 setTimeout(startFastAPI, 2000) } }) } app.whenReady().then(startFastAPI)Step 5实现send-messageIPC 方法在main.js中追加const axios require(axios) ipcMain.handle(send-message, async (e, { cid, content }) { try { const response await axios.post(http://127.0.0.1:8000/api/v1/chat/stream, { conversation_id: cid, content }, { headers: { X-Conversation-ID: cid }, timeout: 300000 // 5分钟超时 }) // 返回初始 stateFastAPI 在 stream 开头就发 return response.data } catch (error) { if (error.response) { throw new Error(API Error: ${error.response.status} ${error.response.data.detail}) } else if (error.request) { throw new Error(Network Error: FastAPI not responding) } else { throw new Error(Request Error: ${error.message}) } } })Step 6FastAPI 实现/chat/stream接口fastapi-backend/api/v1/chat.pyfrom fastapi import APIRouter, Request, Depends, Header from starlette.responses import StreamingResponse from typing import AsyncGenerator from core.agent import get_agent from core.state.manager import get_conversation_state, update_conversation_state router APIRouter() router.post(/stream) async def chat_stream( request: Request, conversation_id: str Header(..., aliasX-Conversation-ID), content: str ): # 1. 加载当前 state state await get_conversation_state(conversation_id) # 2. 构建 messages用户输入 历史 messages state.messages [{role: user, content: content}] # 3. 获取 Agent 实例 agent get_agent() # 4. 流式响应生成器 async def event_generator(): # 发送 start 事件 yield event: start\ndata: {\status\:\thinking\}\n\n # Agent 执行 async for token in agent.stream(messages): if await request.is_disconnected(): break yield fdata: {{\type\:\token\,\content\:\{token}\}}\n\n # 更新 state 并发送 end 事件 new_state await update_conversation_state(conversation_id, messages [{role: assistant, content: }]) yield fevent: end\ndata: {new_state.json()}\n\n return StreamingResponse( event_generator(), media_typetext/event-stream, headers{Cache-Control: no-cache, Connection: keep-alive} )4.3 步骤 7-9渲染进程集成与流式 UI 实现Step 7Vue 3 项目初始化cd .. npm install -g create-vue create-vuelatest # 选择 TypeScript, ESLint, Prettier cd client npm install npm install axios vueuse/coresrc/main.ts中启用vueuse/coreimport { createApp } from vue import { createPinia } from pinia import App from ./App.vue import ./style.css // Electron IPC 适配 if (window.require) { (window as any).electron window.require(electron) } createApp(App).use(createPinia()).mount(#app)Step 8创建 Chat 组件与 SSE 消费逻辑src/components/Chat.vuetemplate div classchat-container div classmessages refmessagesRef div v-formsg in messages :keymsg.id classmessage :classmsg.role div classcontent{{ msg.content }}/div /div div v-ifisStreaming classmessage assistant div classcontentspan classtyping▌/span/div /div /div div classinput-area textarea v-modelpendingMessage keydown.enter.preventsendMessage placeholder输入消息... / button clicksendMessage :disabledisStreaming发送/button button clickabortStream v-ifisStreaming停止/button /div /div /template script setup langts import { ref, onMounted, watch } from vue import { consumeSSE } from /utils/sse-parser import { ipcRenderer } from electron const props defineProps{ cid: string }() const messages refany[]([]) const pendingMessage ref() const isStreaming ref(false) const messagesRef refHTMLElement | null(null) const abortController refAbortController | null(null) // 加载历史消息 onMounted(async () { const conv await ipcRenderer.invoke(get-conversation, props.cid) messages.value conv?.messages || [] }) // 自动滚动到底部 watch(messages, () { if (messagesRef.value) { messagesRef.value.scrollTop messagesRef.value.scrollHeight } }) const sendMessage async () { if (!pendingMessage.value.trim()) return isStreaming.value true abortController.value new AbortController() try { await consumeSSE( http://127.0.0.1:8000/api/v1/chat/stream, (token) { const lastMsg messages.value.at(-1) if (lastMsg?.role assistant) { lastMsg.content token } }, (event, data) { if (event start) { messages.value.push({ role: assistant, content: , id: Date.now() }) } else if (event end) { // 更新本地 messages触发 UI 更新 messages.value data.messages } }, abortController.value.signal ) } catch (e) { console.error(e) } finally { isStreaming.value false abortController.value null } } const abortStream () { if (abortController.value) { abortController.value.abort() } } /scriptStep 9Electron 加载 Vue 开发服务器修改main.js中的win.loadFile(index.html)为// 开发模式加载 Vue Vite 服务器 if (process.env.NODE_ENV development) { win.loadURL(http://localhost:3000) } else { win.loadFile(path.join(__dirname, ../client/dist/index.html)) }并在package.json中添加脚本scripts: { dev:electron: concurrently \npm run dev:vue\ \npm run dev:main\, dev:vue: vite --host, dev:main: electron . }4.4 步骤 10-12打包、优化与上线前检查Step 10Electron 打包配置electron-builder.ymlappId: com.example.agent productName: AgentDesktop copyright: Copyright © 2024 directories: output: dist files: - !node_modules/**/* - !src/**/* - !fastapi-backend/**/* - !client/**/* - !**/*.ts - !**/*.map - !**/test/**/* - !**/tests/**/* - !**/__tests__/**/* - !**/coverage/**/* - !**/dist-electron/**/* - !**/dist/**/* - !**/build/**/* - !**/out/**/* - !**/node_modules/electron/**/* - !**/node_modules/electron/**/* - !**/node_modules/electron-builder/**/* - !**/node_modules/electron-packager/**/* - !**/node_modules/electron-rebuild/**/* - !**/node_modules/electron-installer-common/**/* - !**/node_modules/electron-installer-dmg/**/* - !**/node

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询