news 2026/8/2 12:56:45

基于Electron与AI大模型构建智能Markdown桌面编辑器实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Electron与AI大模型构建智能Markdown桌面编辑器实战

大家好,我是长期分享开发实战经验的博主。在日常工作中,无论是写技术文档、整理学习笔记还是撰写项目报告,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 成为你的写作助手,而非一个需要频繁切换界面的独立工具。

核心设计理念包括:

  1. 上下文感知:AI 的操作基于你当前正在编辑的文档、选中的文本或光标位置,提供精准的辅助。
  2. 低摩擦交互:通过快捷键、右键菜单、侧边栏指令等方式,让 AI 功能触手可及,无需打断写作流。
  3. 结果可控:所有 AI 的修改或生成内容,都需经过用户确认(如应用、替换、插入),用户拥有最终控制权。
  4. 离线与隐私:支持连接本地部署的大模型(如通过 Ollama),保障敏感或私有文档的内容安全。

1.3 技术栈选型理由

为了实现一个跨平台、高性能、且易于集成的桌面应用,我们选择了以下技术栈:

  • 前端/界面Electron+React+TypeScript。Electron 允许我们使用 Web 技术(HTML, CSS, JS)构建跨平台(Windows, macOS, Linux)桌面应用。React 提供了高效的 UI 组件化开发体验,TypeScript 则能极大地提升代码的可维护性和开发体验,减少类型错误。
  • 编辑器核心CodeMirror 6Monaco Editor。两者都是优秀的基于 Web 的代码编辑器。CodeMirror 更轻量,定制化程度高;Monaco Editor(VS Code 所用)功能更强大,开箱即用。本项目基于对 Markdown 特定语法高亮、折叠、缩进等功能的深度定制需求,选择了 CodeMirror 6。
  • AI 集成OpenAI API(GPT系列) 或Ollama(本地模型)。通过标准的 HTTP API 调用,我们可以灵活接入云端或本地的 AI 模型服务。为了演示的通用性,本文将主要围绕 OpenAI API 进行,但架构设计上完全支持切换为任何兼容的 API 端点。
  • 状态与数据管理ZustandValtio。对于中小型桌面应用,这些轻量级的状态管理库比 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.2mistral

项目结构预览:在开始前,我们先看一下最终的项目目录结构,以便有个全局认识。

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 功能作为核心,其调用链路设计至关重要。我们采用“渲染进程发起 -> 主进程代理 -> 网络请求”的模式。

  1. 为什么由主进程代理?直接在前端调用 API 会暴露 API Key,非常不安全。主进程作为后端,可以安全地管理密钥(从环境变量或加密配置文件中读取),并处理网络请求。
  2. 流程
    • 用户在渲染进程的编辑器中选中文本,点击“优化语法”按钮。
    • 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.ts

import { 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.ts

import { 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.ts

import { 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: Record<string, 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 lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>AI Markdown Editor</title> </head> <body> <div id="root"></div> <script type="module" src="/src/renderer/main.tsx"></script> </body> </html>

创建 React 应用入口src/renderer/main.tsx

import 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.tsx

import React from 'react'; import MarkdownEditor from './components/MarkdownEditor'; import AIOperationsPanel from './components/AIOperationsPanel'; import './App.css'; function App() { return ( <div className="app-container"> <header className="app-header"> <h1>🤖 AI Markdown Editor</h1> <p>智能写作,触手可及</p> </header> <main className="app-main"> <div className="editor-section"> <MarkdownEditor /> </div> <div className="sidebar"> <AIOperationsPanel /> </div> </main> </div> ); } export default App;

创建编辑器组件src/renderer/components/MarkdownEditor.tsx

import 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 = useRef<HTMLDivElement>(null); const viewRef = useRef<EditorView | 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} className="markdown-editor" />; }; export default MarkdownEditor;

创建状态管理 Storesrc/renderer/stores/editorStore.ts

import { 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 = create<EditorStore>((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.tsx

import 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 className="ai-panel"> <h3>AI 智能助手</h3> <p className="ai-hint">选中编辑器中的文本,然后点击下方功能。</p> {error && <div className="ai-error">{error}</div>} <div className="ai-buttons"> {operations.map((op) => ( <button key={op.id} onClick={() => handleAIClick(op.id)} disabled={isProcessing} className="ai-button" > <span className="button-label">{op.label}</span> <span className="button-desc">{op.desc}</span> {isProcessing && <span className="processing-indicator">处理中...</span>} </button> ))} </div> <div className="ai-tips"> <h4>使用技巧</h4> <ul> <li>选中段落进行润色或总结。</li> <li>选中标题或列表项进行扩展。</li> <li>翻译功能对整段英文效果更好。</li> <li>所有操作结果都需要你确认后才应用。</li> </ul> </div> </div> ); }; export default AIOperationsPanel;

创建自定义 Hooksrc/renderer/hooks/useAIProcessor.ts

import { 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 运行与验证

现在,所有核心部分已完成。让我们启动应用。

  1. 设置环境变量:在项目根目录创建.env文件(确保已加入.gitignore),并填入你的 OpenAI API Key。
    OPENAI_API_KEY=sk-your-actual-api-key-here
  2. 安装依赖并启动
    npm install npm run dev
    这个命令会同时启动 Vite 开发服务器(在http://localhost:5173)和 Electron 应用。
  3. 验证功能
    • Electron 窗口应成功打开,并加载 React 应用界面。
    • 编辑器内应有预设的 Markdown 文本。
    • 在编辑器中选中一段文本。
    • 点击侧边栏的“润色语法”等按钮。
    • 主进程会调用 OpenAI API,返回结果后,会弹出确认对话框。
    • 点击“确定”后,编辑器中的选中文本将被 AI 生成的内容替换。

5. 常见问题与排查思路

在开发和运行过程中,你可能会遇到以下问题:

问题现象常见原因解决思路
应用启动失败,白屏或报错1. 依赖未安装完全。
2. TypeScript 编译错误。
3. 主进程或渲染进程代码有语法错误。
1. 删除node_modulespackage-lock.json,重新npm install
2. 运行npm run build:main检查主进程 TS 错误。
3. 查看终端或 Electron 开发者工具控制台(Ctrl+Shift+I)的具体报错信息。
侧边栏 AI 按钮点击无反应1. 未选中文本。
2.window.api未定义,Preload 脚本注入失败。
3. IPC 通信处理器未在主进程注册。
1. 确保在编辑器中选中了文本。
2. 检查src/main/preload.ts是否正确暴露了invokeAI方法,以及BrowserWindowpreload路径是否正确。
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.jsondev:electron脚本前添加cross-env
AI 请求超时或网络错误1. 网络连接问题。
2. API Key 无效或余额不足。
3. OpenAI API 服务暂时不可用。
1. 检查网络。
2. 登录 OpenAI 平台检查 API Key 状态和余额。
3. 查看 OpenAI 状态页面或稍后重试。错误信息会在侧边栏显示。
编辑器样式异常或无法输入1. CodeMirror 扩展未正确引入。
2. CSS 样式冲突。
1. 检查MarkdownEditor.tsxextensions数组是否包含了必要的扩展(如markdown(),keymap)。
2. 检查浏览器开发者工具的元素样式,看是否有全局 CSS 覆盖。
打包后应用无法运行1. 资源路径错误。
2. 环境变量在打包后未携带。
3. 原生模块兼容性问题。
1. 确保vite.config.tsbase设置为./,并且主进程加载生产环境 HTML 的路径正确。
2. 环境变量需通过extraResources或构建时注入等方式提供给打包后应用,这需要更复杂的配置。
3. 如果使用了原生 Node 模块,需在package.jsonbuild配置中正确设置。

6. 最佳实践与工程建议

将一个小 demo 变成一个健壮、可维护的开源项目,还需要考虑很多工程化细节。

6.1 项目结构与代码组织

  • 清晰的模块边界:如我们所示,严格区分mainrenderershared。共享的类型定义(如 IPC 通信的消息格式)应放在shared目录。
  • 组件化与复用:将 UI 拆分为更小的、可复用的组件(如ButtonModalSettingItem)。
  • 自定义 Hooks:将数据获取、事件监听等逻辑封装成 Hooks(如useAIProcessoruseFileOperations),使组件更纯粹。

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 指令集等,将其打造成一个真正强大的生产力工具。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/2 12:56:30

Unity镜面反射Shader实现:从Phong到PBR与SSR进阶指南

1. 项目概述&#xff1a;从“看起来像”到“感觉对”的质感飞跃在Unity3D里鼓捣过一阵子3D模型的朋友&#xff0c;估计都经历过这么一个阶段&#xff1a;模型导进来了&#xff0c;贴图也贴上了&#xff0c;乍一看有模有样&#xff0c;但总觉得哪里不对劲——它看起来“平”&…

作者头像 李华
网站建设 2026/8/2 12:56:07

YimMenu:GTA5终极安全增强菜单的5个核心优势

YimMenu&#xff1a;GTA5终极安全增强菜单的5个核心优势 【免费下载链接】YimMenu YimMenu, a GTA V menu protecting against a wide ranges of the public crashes and improving the overall experience. 项目地址: https://gitcode.com/GitHub_Trending/yi/YimMenu …

作者头像 李华
网站建设 2026/8/2 12:55:37

IEC 61508功能安全标准解析:从核心概念到工程实践

1. 项目概述&#xff1a;从“通用”二字&#xff0c;拆解IEC 61508的底层逻辑如果你在汽车电子、工业自动化、轨道交通或者医疗器械领域工作&#xff0c;最近几年一定被“功能安全”这个词反复轰炸。各种认证、评审、文档搞得人头大&#xff0c;而这一切的源头&#xff0c;几乎…

作者头像 李华
网站建设 2026/8/2 12:52:54

基于ESP32-C3的RS485智能视觉摄像头:工业AI与Modbus融合实战

1. 项目概述&#xff1a;当视觉AI遇见工业总线最近在捣鼓一个挺有意思的玩意儿&#xff0c;我把它叫做“RS485 Vision AI Camera”。简单说&#xff0c;就是把一个带AI视觉识别能力的摄像头&#xff0c;通过工业领域最常用的RS485总线给接出来。这想法源于一个很实际的需求&…

作者头像 李华
网站建设 2026/8/2 12:50:15

前端转大模型:为什么能跑通Demo,却上不了生产环境?

聊《做过前端的人学大模型&#xff0c;哪些经验可以直接迁移&#xff1f;》之前&#xff0c;先说一句实在的&#xff1a;别急着背概念&#xff0c;先看它在真实项目里到底解决什么问题。摘要摘要&#xff1a;前端做AI应用有天然优势——交互设计、流式展示、用户体验。但Demo能…

作者头像 李华