
大家好我是长期分享开发实战经验的博主。在日常工作中无论是写技术文档、整理学习笔记还是撰写项目报告Markdown 都是我的首选工具。然而传统的 Markdown 编辑器在智能化辅助方面往往有所欠缺比如语法检查、内容润色、格式优化等都需要手动完成效率上不去。最近我结合 AI 大模型的能力开发并开源了一款桌面应用旨在将 AI 的智能写作与编辑能力深度集成到 Markdown 工作流中。本文将详细介绍这款应用的设计思路、技术实现、核心功能以及如何从零开始搭建和运行它。无论你是想了解 AI 与桌面应用结合的实践还是希望获得一个强大的个人写作工具这篇文章都能为你提供完整的指南。1. 背景与核心概念在深入代码之前我们有必要厘清几个核心概念并理解这个项目要解决的根本问题。1.1 Markdown 与 AI 结合的痛点Markdown 是一种轻量级标记语言以其简洁的语法和强大的可读性深受开发者、写作者和技术博主的喜爱。我们用它来写博客、记笔记、编写 API 文档。然而随着内容创作的深入一些痛点逐渐浮现格式纠错繁琐忘记关闭列表、标题层级混乱、链接格式错误等需要肉眼检查。内容优化依赖人工想让一段描述更精炼、更专业或者检查错别字和语病往往需要反复斟酌或借助其他工具。结构化生成能力弱从零开始撰写一篇结构清晰的技术文章大纲或者将杂乱的想法整理成有条理的列表比较耗时。而近年来AI 大模型特别是大型语言模型LLM在自然语言处理上展现出惊人能力能够很好地理解、生成和优化文本。将 AI 能力引入 Markdown 编辑器理论上可以自动化解决上述大部分问题。1.2 项目定位AI-Native Markdown 编辑器本项目并非一个简单的“编辑器聊天框”拼接。它的核心定位是AI-Native即 AI 能力不是外挂功能而是深度融入编辑器的每一个核心交互环节。目标是打造一个“懂写作”的桌面应用让 AI 成为你的写作助手而非一个需要频繁切换界面的独立工具。核心设计理念包括上下文感知AI 的操作基于你当前正在编辑的文档、选中的文本或光标位置提供精准的辅助。低摩擦交互通过快捷键、右键菜单、侧边栏指令等方式让 AI 功能触手可及无需打断写作流。结果可控所有 AI 的修改或生成内容都需经过用户确认如应用、替换、插入用户拥有最终控制权。离线与隐私支持连接本地部署的大模型如通过 Ollama保障敏感或私有文档的内容安全。1.3 技术栈选型理由为了实现一个跨平台、高性能、且易于集成的桌面应用我们选择了以下技术栈前端/界面ElectronReactTypeScript。Electron 允许我们使用 Web 技术HTML, CSS, JS构建跨平台Windows, macOS, Linux桌面应用。React 提供了高效的 UI 组件化开发体验TypeScript 则能极大地提升代码的可维护性和开发体验减少类型错误。编辑器核心CodeMirror 6或Monaco Editor。两者都是优秀的基于 Web 的代码编辑器。CodeMirror 更轻量定制化程度高Monaco EditorVS Code 所用功能更强大开箱即用。本项目基于对 Markdown 特定语法高亮、折叠、缩进等功能的深度定制需求选择了 CodeMirror 6。AI 集成OpenAI API(GPT系列) 或Ollama(本地模型)。通过标准的 HTTP API 调用我们可以灵活接入云端或本地的 AI 模型服务。为了演示的通用性本文将主要围绕 OpenAI API 进行但架构设计上完全支持切换为任何兼容的 API 端点。状态与数据管理Zustand或Valtio。对于中小型桌面应用这些轻量级的状态管理库比 Redux 更简洁高效。构建工具Vite。提供极速的启动和热更新提升开发效率。2. 环境准备与版本说明在开始编码前请确保你的开发环境已就绪。以下版本是本文撰写时的稳定版本你可以根据实际情况调整。操作系统Windows 10/11, macOS 10.15, 或主流的 Linux 发行版如 Ubuntu 20.04。本教程在 macOS 和 Windows 上均测试通过。Node.jsv18.0.0或更高版本推荐 LTS 版本如 v20.x。这是 Electron 和前端工具链的运行时基础。包管理器npm(随 Node.js 安装) 或yarn/pnpm。本文示例使用npm。代码编辑器Visual Studio Code。强烈推荐因其对 TypeScript、React 和 Electron 生态有极佳的支持。AI 服务准备云端方案OpenAI API Key你需要一个有效的 OpenAI 账号并获取 API Key。请注意保管不要将其硬编码在客户端代码中。本地方案Ollama如果你希望本地运行模型需要安装 Ollama 并拉取一个合适的模型如llama3.2或mistral。项目结构预览在开始前我们先看一下最终的项目目录结构以便有个全局认识。ai-markdown-desktop/ ├── src/ │ ├── main/ # Electron 主进程代码 │ │ ├── index.ts │ │ ├── preload.ts │ │ └── ... │ ├── renderer/ # React 渲染进程代码 │ │ ├── App.tsx │ │ ├── components/ # React 组件 │ │ ├── stores/ # 状态管理 │ │ ├── hooks/ # 自定义 Hooks │ │ └── ... │ └── shared/ # 主进程和渲染进程共享的类型/工具 ├── public/ # 静态资源 ├── package.json ├── tsconfig.json ├── vite.config.ts └── ...3. 核心原理与架构拆解一个 Electron 应用通常包含两个进程主进程和渲染进程。理解它们的分工是开发的关键。3.1 主进程与渲染进程通信主进程src/main/index.ts。这是一个 Node.js 环境负责管理应用生命周期创建窗口、菜单、托盘、处理系统原生事件文件读写、系统对话框以及一些需要更高权限或 Node API 的操作。它不能直接操作 DOM。渲染进程src/renderer/。每个窗口都是一个独立的渲染进程是 Chromium 浏览器环境运行我们的 React 应用负责 UI 展示和用户交互。出于安全考虑它默认不能直接访问 Node.js API。通信桥梁Preload 脚本和IPC进程间通信。Preload 脚本(src/main/preload.ts)在主进程的上下文中运行但在渲染进程加载页面之前注入到页面中。它的核心作用是将一些安全的、受控的 Node.js API 或自定义功能通过contextBridge暴露给渲染进程的window对象。IPC渲染进程通过window.api(由 preload 暴露) 发送消息 (ipcRenderer.send) 到主进程主进程通过ipcMain.handle监听并处理这些消息然后将结果返回给渲染进程。这种架构确保了安全性渲染进程受限和功能性通过主进程访问系统资源的平衡。3.2 AI 能力集成的设计AI 功能作为核心其调用链路设计至关重要。我们采用“渲染进程发起 - 主进程代理 - 网络请求”的模式。为什么由主进程代理直接在前端调用 API 会暴露 API Key非常不安全。主进程作为后端可以安全地管理密钥从环境变量或加密配置文件中读取并处理网络请求。流程用户在渲染进程的编辑器中选中文本点击“优化语法”按钮。React 组件调用一个自定义 Hook如useAIProcessor。Hook 通过window.api.invokeAI发送 IPC 请求包含指令如polish和选中文本。主进程的 IPC 处理器接收到请求从安全位置读取 API Key构造请求体调用 OpenAI API。主进程收到 AI 响应后通过 IPC 将结果返回给渲染进程。渲染进程收到结果更新编辑器状态将 AI 生成的内容插入或替换原文本。3.3 编辑器状态管理编辑器内容、光标位置、AI 处理状态等都需要集中管理。我们使用 Zustand 创建一个 Store。文档状态存储当前的 Markdown 原始文本。编辑器实例引用存储 CodeMirror 编辑器的实例以便在非 React 事件如 IPC 回调中操作编辑器。AI 处理状态存储当前是否正在处理 AI 请求、错误信息等用于显示加载状态或错误提示。4. 完整实战从零构建应用接下来我们一步步实现这个应用。请跟随操作所有代码均可复制运行。4.1 初始化项目与基础配置首先创建项目目录并初始化。mkdir ai-markdown-desktop cd ai-markdown-desktop npm init -y安装主要的开发依赖npm install electron react react-dom typescript types/node types/react types/react-dom npm install vite vitejs/plugin-react --save-dev npm install electron-builder --save-dev # 用于打包创建基本的配置文件tsconfig.json{ compilerOptions: { target: ES2020, useDefineForClassFields: true, lib: [ES2020, DOM, DOM.Iterable], module: ESNext, skipLibCheck: true, moduleResolution: bundler, allowImportingTsExtensions: true, resolveJsonModule: true, isolatedModules: true, noEmit: true, jsx: react-jsx, strict: true, noUnusedLocals: true, noUnusedParameters: true, noFallthroughCasesInSwitch: true, baseUrl: ., paths: { /*: [src/*], main/*: [src/main/*], renderer/*: [src/renderer/*], shared/*: [src/shared/*] } }, include: [src] }创建 Vite 配置文件vite.config.tsimport { defineConfig } from vite; import react from vitejs/plugin-react; import path from path; export default defineConfig({ plugins: [react()], base: ./, // 确保资源使用相对路径 resolve: { alias: { : path.resolve(__dirname, ./src), main: path.resolve(__dirname, ./src/main), renderer: path.resolve(__dirname, ./src/renderer), shared: path.resolve(__dirname, ./src/shared), }, }, build: { outDir: dist/renderer, // 渲染进程构建输出目录 emptyOutDir: true, }, });更新package.json添加必要的脚本和 Electron 入口{ name: ai-markdown-desktop, version: 1.0.0, private: true, main: dist/main/index.js, scripts: { dev: concurrently -k \npm run dev:vite\ \npm run dev:electron\, dev:vite: vite, dev:electron: wait-on tcp:5173 electron ., build: npm run build:renderer npm run build:main, build:renderer: vite build, build:main: tsc -p tsconfig.main.json, postinstall: electron-builder install-app-deps, pack: npm run build electron-builder --dir, dist: npm run build electron-builder }, dependencies: { codemirror/state: ^6.4.0, codemirror/view: ^6.26.0, codemirror/lang-markdown: ^6.2.2, codemirror/commands: ^6.3.3, zustand: ^4.5.0, axios: ^1.6.0 }, devDependencies: { types/electron: ^1.6.10, types/react: ^18.2.0, types/react-dom: ^18.2.0, concurrently: ^8.2.0, electron: ^28.0.0, electron-builder: ^24.0.0, typescript: ^5.0.0, vite: ^5.0.0, vitejs/plugin-react: ^4.0.0, wait-on: ^7.0.0 } }我们需要为 Electron 主进程单独创建一个 TypeScript 配置文件tsconfig.main.json{ extends: ./tsconfig.json, compilerOptions: { module: CommonJS, outDir: dist/main, noEmit: false }, include: [src/main/**/*] }4.2 实现 Electron 主进程创建主进程入口文件src/main/index.tsimport { app, BrowserWindow, ipcMain, dialog } from electron; import path from path; import { fileURLToPath } from url; import { invokeAIHandler } from ./ai-handler; // 稍后实现 const __dirname path.dirname(fileURLToPath(import.meta.url)); let mainWindow: BrowserWindow | null null; const createWindow () { mainWindow new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, preload.js), contextIsolation: true, // 必须开启安全关键 nodeIntegration: false, // 必须关闭安全关键 }, }); // 开发环境下加载 Vite 开发服务器地址 if (process.env.NODE_ENV development) { mainWindow.loadURL(http://localhost:5173); mainWindow.webContents.openDevTools(); } else { // 生产环境加载构建后的文件 mainWindow.loadFile(path.join(__dirname, ../renderer/index.html)); } }; app.whenReady().then(() { // 注册 AI 处理的 IPC 处理器 ipcMain.handle(invoke-ai, invokeAIHandler); createWindow(); app.on(activate, () { if (BrowserWindow.getAllWindows().length 0) createWindow(); }); }); app.on(window-all-closed, () { if (process.platform ! darwin) app.quit(); });创建 Preload 脚本src/main/preload.ts定义安全的 API 接口import { contextBridge, ipcRenderer } from electron; // 暴露给渲染进程的 API contextBridge.exposeInMainWorld(api, { // 调用 AI 功能 invokeAI: (payload: { instruction: string; text: string }) ipcRenderer.invoke(invoke-ai, payload), // 可以在此添加其他安全的方法如文件操作 // openFile: () ipcRenderer.invoke(dialog:openFile), });4.3 实现 AI 处理模块这是核心的后端逻辑。创建src/main/ai-handler.tsimport { ipcMainInvokeEvent } from electron/main; import axios from axios; // 从环境变量获取 API Key生产环境请使用更安全的方式管理密钥 const OPENAI_API_KEY process.env.OPENAI_API_KEY; const OPENAI_API_URL https://api.openai.com/v1/chat/completions; // 指令到系统 Prompt 的映射 const instructionToSystemPrompt: Recordstring, string { polish: 你是一位专业的文本编辑助手。请润色用户提供的 Markdown 文本修正语法错误优化表达使其更流畅、专业但保持其原意和 Markdown 格式。直接返回润色后的文本不要添加解释。, summarize: 你是一位专业的总结助手。请用简洁的语言总结用户提供的 Markdown 文本的核心内容。直接返回总结文本。, expand: 你是一位写作助手。请根据用户提供的 Markdown 文本片段或主题进行合理的扩展和阐述使其内容更丰富、完整。直接返回扩展后的文本。, translateToChinese: 你是一位翻译助手。将用户提供的英文 Markdown 文本准确、流畅地翻译成中文并保留原有的 Markdown 格式。直接返回翻译后的文本。, // 可以继续添加更多指令... }; export const invokeAIHandler async ( _event: ipcMainInvokeEvent, payload: { instruction: string; text: string } ): Promise{ success: boolean; data?: string; error?: string } { const { instruction, text } payload; if (!OPENAI_API_KEY) { return { success: false, error: OpenAI API Key 未配置。请设置 OPENAI_API_KEY 环境变量。 }; } const systemPrompt instructionToSystemPrompt[instruction]; if (!systemPrompt) { return { success: false, error: 不支持的指令: ${instruction} }; } if (!text || text.trim().length 0) { return { success: false, error: 输入文本不能为空。 }; } try { const response await axios.post( OPENAI_API_URL, { model: gpt-3.5-turbo, // 可根据需要更换模型如 gpt-4 messages: [ { role: system, content: systemPrompt }, { role: user, content: text }, ], temperature: 0.7, max_tokens: 2000, }, { headers: { Authorization: Bearer ${OPENAI_API_KEY}, Content-Type: application/json, }, } ); const aiResponse response.data.choices[0]?.message?.content?.trim(); if (!aiResponse) { throw new Error(AI 返回内容为空); } return { success: true, data: aiResponse }; } catch (error: any) { console.error(AI 调用失败:, error); const errorMsg error.response?.data?.error?.message || error.message || 未知错误; return { success: false, error: AI 处理失败: ${errorMsg} }; } };重要安全提示在实际项目中绝对不要将 API Key 硬编码在客户端或提交到代码仓库。上述示例从环境变量读取。更安全的生产环境做法是开发一个简单的后端服务如使用 Express.js将 API Key 保存在服务器端桌面应用通过该服务代理请求。本文为简化演示采用了主进程环境变量方案请务必妥善保管你的.env文件需自行创建并加入.gitignore。4.4 构建 React 渲染进程应用首先创建 HTML 入口index.html于项目根目录!DOCTYPE html html langen head meta charsetUTF-8 / link relicon typeimage/svgxml href/vite.svg / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleAI Markdown Editor/title /head body div idroot/div script typemodule src/src/renderer/main.tsx/script /body /html创建 React 应用入口src/renderer/main.tsximport React from react; import ReactDOM from react-dom/client; import App from ./App; import ./index.css; ReactDOM.createRoot(document.getElementById(root)!).render( React.StrictMode App / /React.StrictMode );创建主应用组件src/renderer/App.tsximport React from react; import MarkdownEditor from ./components/MarkdownEditor; import AIOperationsPanel from ./components/AIOperationsPanel; import ./App.css; function App() { return ( div classNameapp-container header classNameapp-header h1 AI Markdown Editor/h1 p智能写作触手可及/p /header main classNameapp-main div classNameeditor-section MarkdownEditor / /div div classNamesidebar AIOperationsPanel / /div /main /div ); } export default App;创建编辑器组件src/renderer/components/MarkdownEditor.tsximport React, { useEffect, useRef } from react; import { EditorState } from codemirror/state; import { EditorView, keymap } from codemirror/view; import { defaultKeymap } from codemirror/commands; import { markdown } from codemirror/lang-markdown; import { useEditorStore } from ../stores/editorStore; import ./MarkdownEditor.css; const MarkdownEditor: React.FC () { const editorRef useRefHTMLDivElement(null); const viewRef useRefEditorView | null(null); const { setEditorView, content, setContent } useEditorStore(); useEffect(() { if (!editorRef.current) return; // 初始化编辑器状态 const startState EditorState.create({ doc: content, extensions: [ markdown(), keymap.of(defaultKeymap), EditorView.updateListener.of((update) { if (update.docChanged) { const newContent update.state.doc.toString(); setContent(newContent); } }), EditorView.theme({ : { height: 100%, fontSize: 16px }, .cm-scroller: { overflow: auto }, .cm-content: { fontFamily: Menlo, Monaco, Consolas, monospace }, }), ], }); // 创建编辑器视图 const view new EditorView({ state: startState, parent: editorRef.current, }); viewRef.current view; setEditorView(view); // 组件卸载时销毁编辑器 return () { view.destroy(); viewRef.current null; }; }, []); // 只在挂载时初始化 // 当外部 content 变化时如 AI 替换后更新编辑器 useEffect(() { const view viewRef.current; if (view content ! view.state.doc.toString()) { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: content, }, }); } }, [content]); return div ref{editorRef} classNamemarkdown-editor /; }; export default MarkdownEditor;创建状态管理 Storesrc/renderer/stores/editorStore.tsimport { create } from zustand; import { EditorView } from codemirror/view; interface EditorStore { content: string; editorView: EditorView | null; isAIProcessing: boolean; aiError: string | null; setContent: (content: string) void; setEditorView: (view: EditorView) void; setAIProcessing: (processing: boolean) void; setAIError: (error: string | null) void; // 一个工具函数获取当前选中的文本 getSelectedText: () string; // 一个工具函数用新文本替换选中部分 replaceSelection: (newText: string) void; } export const useEditorStore createEditorStore((set, get) ({ content: # 欢迎使用 AI Markdown 编辑器\n\n在这里开始你的写作...\n\n- AI 可以帮助你**润色**、**总结**、**扩展**内容。\n- 试试选中一些文本然后点击侧边栏的按钮。, editorView: null, isAIProcessing: false, aiError: null, setContent: (content) set({ content }), setEditorView: (editorView) set({ editorView }), setAIProcessing: (isAIProcessing) set({ isAIProcessing }), setAIError: (aiError) set({ aiError }), getSelectedText: () { const view get().editorView; if (!view) return ; const selection view.state.selection; if (selection.main.empty) return ; // 没有选中文本 return view.state.sliceDoc(selection.main.from, selection.main.to); }, replaceSelection: (newText) { const view get().editorView; if (!view) return; const selection view.state.selection; view.dispatch({ changes: { from: selection.main.from, to: selection.main.to, insert: newText, }, // 将光标移动到插入文本的末尾 selection: { anchor: selection.main.from newText.length }, }); }, }));创建 AI 操作面板组件src/renderer/components/AIOperationsPanel.tsximport React from react; import { useAIProcessor } from ../hooks/useAIProcessor; import ./AIOperationsPanel.css; const AIOperationsPanel: React.FC () { const { processWithAI, isProcessing, error } useAIProcessor(); const handleAIClick async (instruction: string) { await processWithAI(instruction); }; const operations [ { id: polish, label: ✨ 润色语法, desc: 优化表达修正错误 }, { id: summarize, label: 总结内容, desc: 提取核心要点 }, { id: expand, label: 扩展阐述, desc: 丰富内容细节 }, { id: translateToChinese, label: 翻译成中文, desc: 英译中 }, ]; return ( div classNameai-panel h3AI 智能助手/h3 p classNameai-hint选中编辑器中的文本然后点击下方功能。/p {error div classNameai-error{error}/div} div classNameai-buttons {operations.map((op) ( button key{op.id} onClick{() handleAIClick(op.id)} disabled{isProcessing} classNameai-button span classNamebutton-label{op.label}/span span classNamebutton-desc{op.desc}/span {isProcessing span classNameprocessing-indicator处理中.../span} /button ))} /div div classNameai-tips h4使用技巧/h4 ul li选中段落进行润色或总结。/li li选中标题或列表项进行扩展。/li li翻译功能对整段英文效果更好。/li li所有操作结果都需要你确认后才应用。/li /ul /div /div ); }; export default AIOperationsPanel;创建自定义 Hooksrc/renderer/hooks/useAIProcessor.tsimport { useCallback } from react; import { useEditorStore } from ../stores/editorStore; // 扩展 Window 接口以包含我们通过 preload 暴露的 api declare global { interface Window { api: { invokeAI: (payload: { instruction: string; text: string }) Promise{ success: boolean; data?: string; error?: string; }; }; } } export const useAIProcessor () { const { getSelectedText, replaceSelection, setAIProcessing, setAIError, isAIProcessing, aiError, } useEditorStore(); const processWithAI useCallback( async (instruction: string) { const selectedText getSelectedText(); if (!selectedText) { setAIError(请先在编辑器中选中一些文本。); return; } setAIProcessing(true); setAIError(null); try { // 通过预加载脚本暴露的 API 调用主进程 const result await window.api.invokeAI({ instruction, text: selectedText, }); if (result.success result.data) { // 在实际应用中这里可以弹出一个预览对话框让用户确认 // 本例中我们直接替换但强烈建议添加用户确认环节 if (confirm(AI 建议如下\n\n${result.data}\n\n是否替换选中文本)) { replaceSelection(result.data); } } else { setAIError(result.error || AI 处理失败未知错误。); } } catch (err: any) { setAIError(请求失败: ${err.message}); } finally { setAIProcessing(false); } }, [getSelectedText, replaceSelection, setAIProcessing, setAIError] ); return { processWithAI, isProcessing: isAIProcessing, error: aiError, }; };4.5 运行与验证现在所有核心部分已完成。让我们启动应用。设置环境变量在项目根目录创建.env文件确保已加入.gitignore并填入你的 OpenAI API Key。OPENAI_API_KEYsk-your-actual-api-key-here安装依赖并启动npm install npm run dev这个命令会同时启动 Vite 开发服务器在http://localhost:5173和 Electron 应用。验证功能Electron 窗口应成功打开并加载 React 应用界面。编辑器内应有预设的 Markdown 文本。在编辑器中选中一段文本。点击侧边栏的“润色语法”等按钮。主进程会调用 OpenAI API返回结果后会弹出确认对话框。点击“确定”后编辑器中的选中文本将被 AI 生成的内容替换。5. 常见问题与排查思路在开发和运行过程中你可能会遇到以下问题问题现象常见原因解决思路应用启动失败白屏或报错1. 依赖未安装完全。2. TypeScript 编译错误。3. 主进程或渲染进程代码有语法错误。1. 删除node_modules和package-lock.json重新npm install。2. 运行npm run build:main检查主进程 TS 错误。3. 查看终端或 Electron 开发者工具控制台CtrlShiftI的具体报错信息。侧边栏 AI 按钮点击无反应1. 未选中文本。2.window.api未定义Preload 脚本注入失败。3. IPC 通信处理器未在主进程注册。1. 确保在编辑器中选中了文本。2. 检查src/main/preload.ts是否正确暴露了invokeAI方法以及BrowserWindow的preload路径是否正确。3. 检查src/main/index.ts中是否调用了ipcMain.handle(invoke-ai, ...)。调用 AI 时提示 “API Key 未配置”1..env文件不存在或位置不对。2..env文件中的变量名错误。3. 主进程未正确加载环境变量。1. 确保.env文件在项目根目录。2. 确保变量名为OPENAI_API_KEY。3. 在启动 Electron 时环境变量需被加载。使用cross-env包或在启动脚本中设置。可以尝试在package.json的dev:electron脚本前添加cross-env。AI 请求超时或网络错误1. 网络连接问题。2. API Key 无效或余额不足。3. OpenAI API 服务暂时不可用。1. 检查网络。2. 登录 OpenAI 平台检查 API Key 状态和余额。3. 查看 OpenAI 状态页面或稍后重试。错误信息会在侧边栏显示。编辑器样式异常或无法输入1. CodeMirror 扩展未正确引入。2. CSS 样式冲突。1. 检查MarkdownEditor.tsx中extensions数组是否包含了必要的扩展如markdown(),keymap。2. 检查浏览器开发者工具的元素样式看是否有全局 CSS 覆盖。打包后应用无法运行1. 资源路径错误。2. 环境变量在打包后未携带。3. 原生模块兼容性问题。1. 确保vite.config.ts中base设置为./并且主进程加载生产环境 HTML 的路径正确。2. 环境变量需通过extraResources或构建时注入等方式提供给打包后应用这需要更复杂的配置。3. 如果使用了原生 Node 模块需在package.json的build配置中正确设置。6. 最佳实践与工程建议将一个小 demo 变成一个健壮、可维护的开源项目还需要考虑很多工程化细节。6.1 项目结构与代码组织清晰的模块边界如我们所示严格区分main、renderer、shared。共享的类型定义如 IPC 通信的消息格式应放在shared目录。组件化与复用将 UI 拆分为更小的、可复用的组件如Button、Modal、SettingItem。自定义 Hooks将数据获取、事件监听等逻辑封装成 Hooks如useAIProcessor、useFileOperations使组件更纯粹。6.2 配置与安全管理配置文件使用config目录存放不同环境开发、生产的配置文件。对于 API Key 等敏感信息永远不要提交到代码仓库。使用.env.local并加入.gitignore。安全的密钥管理对于生产级应用考虑实现一个轻量级后端服务如用 Express 或 Next.js API Routes桌面应用只与该服务通信由服务端持有并调用 AI API。这是最安全的做法。支持多模型后端抽象 AI 调用层使其易于切换不同的提供商OpenAI, Anthropic, 本地 Ollama 等。可以设计一个AIClient接口和多个实现。6.3 用户体验与交互优化撤销/重做集成 CodeMirror 的历史扩展确保 AI 操作可以被撤销。AI 操作预览不要直接替换文本。弹出一个模态框Modal展示 AI 建议并提供“应用”、“插入”、“取消”等选项。自定义指令允许用户自定义一些常用的 Prompt 模板并保存为快捷指令。流式响应对于较长的 AI 生成可以尝试接入支持流式响应的 API实现打字机效果提升体验。离线模式与本地模型将 Ollama 集成作为一等公民支持。检测网络状况允许用户选择使用云端模型还是本地模型。6.4 性能与可维护性防抖与节流对频繁触发的事件如编辑器内容变化自动保存进行防抖处理。错误边界在 React 中使用 Error Boundary 捕获并优雅地处理组件渲染错误。日志记录在主进程中集成日志库如winston记录应用运行日志和错误信息便于排查问题。自动化测试为关键的业务逻辑如 AI 指令映射、文本处理编写单元测试为组件编写集成测试。6.5 开源与社区建设完善的 README项目根目录的README.md应包含项目简介、功能特性、截图、安装指南、开发指南、贡献指南等。清晰的许可证选择合适的开源许可证如 MIT、GPL-3.0并在LICENSE文件中明确。Issue 与 PR 模板在.github/目录下创建模板规范社区反馈和贡献流程。持续集成使用 GitHub Actions 或 Travis CI 自动化运行测试、构建和发布流程。通过以上步骤我们不仅实现了一个功能可用的 AI Markdown 桌面应用更搭建了一个具备良好工程实践基础的项目骨架。你可以在此基础上继续深化功能例如添加文件管理、主题切换、导出 PDF、多标签页、更丰富的 AI 指令集等将其打造成一个真正强大的生产力工具。