Electron 构建一个集成MCP

我们来深入探讨如何使用 Electron 构建一个集成"模型上下文协议"(Model Context Protocol - MCP)的聊天室应用,主要目的是学习 MCP 的概念和实现。

核心目标:

  1. 理解什么是"模型上下文协议"(MCP)及其在 LLM 应用中的重要性。
  2. 学习如何在一个 Electron 应用中实现 MCP 来管理与 LLM 的对话。
  3. 掌握 Electron 的基本开发流程(主进程、渲染器进程、IPC 通信)。
  4. 集成一个 LLM API(以 OpenAI API 为例)来驱动聊天机器人的响应。
  5. 构建一个基础但功能完整的聊天界面。

免责声明: "Model Context Protocol (MCP)" 并不是一个广泛公认的、标准化的协议名称(如 HTTP 或 TCP/IP)。在这里,我们将其理解为一套管理和组织发送给大语言模型(LLM)的上下文信息的规则、结构或策略。不同的应用或框架可能有自己的实现方式,但其核心目标是相似的:有效地维护对话历史、系统指令和其他相关信息,以获得更准确、更连贯的 LLM 响应。


目录

  • 第一部分:理解模型上下文协议 (MCP)
    • 1.1 MCP 的核心概念:为什么需要管理上下文?
    • 1.2 MCP 在聊天应用中的作用
    • 1.3 设计一个基础的 MCP 结构
    • 1.4 上下文窗口(Context Window)的挑战与策略
  • 第二部分:Electron 基础与项目搭建
    • 2.1 Electron 简介:是什么,为什么选择它?
    • 2.2 核心概念:主进程 (Main Process) 与渲染器进程 (Renderer Process)
    • 2.3 进程间通信 (IPC):连接两个世界的桥梁
    • 2.4 开发环境准备 (Node.js, npm/yarn)
    • 2.5 创建第一个 Electron 项目 (使用 Electron Forge 或手动配置)
    • 2.6 项目结构概览
  • 第三部分:构建聊天界面 (UI)
    • 3.1 选择前端技术栈 (HTML, CSS, Vanilla JS 或框架)
    • 3.2 HTML 结构设计 (消息列表、输入框、发送按钮)
    • 3.3 CSS 样式实现 (基础布局与美化)
    • 3.4 JavaScript 交互逻辑 (DOM 操作、事件监听)
  • 第四部分:在 Electron 中实现 MCP 逻辑
    • 4.1 数据结构定义 (JavaScript 对象数组)
    • 4.2 在渲染器进程中管理本地消息状态
    • 4.3 通过 IPC 将用户消息和 MCP 请求发送到主进程
    • 4.4 在主进程中维护和更新完整的 MCP 上下文
    • 4.5 实现上下文截断策略 (例如:固定长度、Token 限制)
    • 4.6 将 MCP 数据格式化为 LLM API 请求格式
  • 第五部分:集成大语言模型 (LLM) API
    • 5.1 选择 LLM API (以 OpenAI API 为例)
    • 5.2 获取 API 密钥及安全管理
    • 5.3 在主进程中使用 Node.js 发起 HTTP 请求 (node-fetch, axios)
    • 5.4 处理 API 响应 (成功、失败、错误处理)
    • 5.5 将 LLM 的响应通过 IPC 发送回渲染器进程
    • 5.6 (进阶) 处理流式响应 (Streaming Responses)
  • 第六部分:整合流程与代码示例
    • 6.1 整体数据流图
    • 6.2 main.js (主进程核心代码)
    • 6.3 preload.js (预加载脚本与 IPC 安全暴露)
    • 6.4 renderer.js (渲染器进程逻辑)
    • 6.5 index.html (UI 结构)
    • 6.6 关键函数示例 (MCP 更新、API 调用、IPC 处理)
  • 第七部分:进阶功能与优化
    • 7.1 错误处理与用户友好提示
    • 7.2 对话历史的持久化存储 (如 electron-store)
    • 7.3 优化 MCP 策略 (例如:摘要、RAG 雏形)
    • 7.4 UI 改进 (加载状态、滚动条、代码高亮等)
    • 7.5 安全性考量 (API 密钥、数据传输)
    • 7.6 打包与分发 Electron 应用
  • 第八部分:总结与后续学习
    • 8.1 项目回顾与关键学习点
    • 8.2 常见问题与排错思路
    • 8.3 进一步学习 MCP 和 LLM 应用开发的资源

第一部分:理解模型上下文协议 (MCP)

1.1 MCP 的核心概念:为什么需要管理上下文?

大语言模型(LLM)通常是无状态的。这意味着,如果你连续向 LLM 发送两条消息,它默认不会记得第一条消息的内容。每次 API 调用都是独立的。为了让 LLM 能够理解对话的连贯性、记住之前的讨论内容,甚至遵循特定的指令或扮演某个角色,我们需要在每次请求时,将相关的"上下文"信息一起发送给它。

"上下文"通常包括:

  1. 系统指令 (System Prompt): 定义 LLM 的角色、行为准则、输出格式等。例如:"你是一个乐于助人的 AI 助手。"
  2. 对话历史 (Conversation History): 用户和 AI 之间之前的交互记录。
  3. 当前用户输入 (Current User Input): 用户最新发送的消息。
  4. (可选) 额外信息 (Additional Context): 比如用户信息、当前时间、文档片段(用于 RAG)等。

模型上下文协议 (MCP) 就是我们用来结构化、组织和管理这些上下文信息的一套方法或规则。没有良好的 MCP,LLM 的响应可能会:

  • 前后矛盾: 忘记之前说过的话。
  • 缺乏连贯性: 无法理解当前问题与之前讨论的关系。
  • 偏离主题: 无法保持对话的焦点。
  • 忽略指令: 忘记了系统设定的角色或要求。

1.2 MCP 在聊天应用中的作用

在聊天应用中,MCP 至关重要。用户期望与 AI 的对话是流畅自然的,就像和人聊天一样。MCP 的作用体现在:

  • 维护对话状态: 准确记录用户和 AI 的每一轮交互。
  • 构建有效输入: 将对话历史和当前输入整合成 LLM 能理解的格式。
  • 控制上下文长度: LLM 的输入有长度限制(称为 Context Window),MCP 需要策略来处理过长的对话历史。
  • 支持多轮对话: 让 AI 基于之前的讨论进行有意义的回应。
  • 实现特定功能: 如角色扮演、遵循特定输出格式等。

1.3 设计一个基础的 MCP 结构

一个常见且有效的 MCP 结构是使用一个对象数组来表示对话历史。每个对象代表对话中的一条消息,并包含关键信息,如:

  • role: 消息发送者的角色。常见的角色有:
    • system: 系统指令,通常放在最前面,用于设定 AI 的行为。
    • user: 代表用户的输入。
    • assistant: 代表 AI (LLM) 的回复。
    • (可选) function/tool: 用于函数调用/工具使用的场景。
  • content: 消息的具体文本内容。

示例 MCP 结构 (JavaScript 数组):

javascript 复制代码
[
  { role: 'system', content: '你是一个翻译助手,将用户输入翻译成英文。' },
  { role: 'user', content: '你好,世界!' },
  { role: 'assistant', content: 'Hello, world!' },
  { role: 'user', content: '今天天气怎么样?' }
  // 下一次请求时,会将这个数组连同新的用户信息一起发送给 LLM
]

这个结构清晰地记录了对话的流程和各方发言。在每次需要调用 LLM API 时,我们会构建或更新这个数组,并将其作为输入的一部分发送。

1.4 上下文窗口(Context Window)的挑战与策略

LLM 不是无限记忆的。它们能处理的上下文信息量有一个上限,这叫做"上下文窗口"(Context Window),通常以 Token 数量(大致可以理解为单词或字符块)来衡量。例如,GPT-3.5 Turbo 的某个版本可能有 4k 或 16k 的 Token 限制,GPT-4 则有更大的窗口。

当对话历史变得非常长,超过模型的上下文窗口限制时,就会出现问题。如果不加处理,API 调用可能会失败,或者模型只能看到最近的部分信息,导致"失忆"。

因此,MCP 必须包含上下文管理策略来应对这个问题。常见的策略有:

  1. 截断 (Truncation):
    • 简单截断: 保留最新的 N 条消息,或保留总 Token 数不超过限制的最新消息。这是最简单但也可能丢失重要早期信息的方法。通常会优先丢弃最旧的 user/assistant 对话,但保留 system 指令。
    • 保留首尾: 保留第一条(通常是系统指令)和最后几条消息。
  2. 摘要 (Summarization):
    • 使用另一个 LLM 调用(或者简单的规则)将较早的对话历史进行总结,用一个简短的摘要替换掉冗长的旧消息。这能保留一些长期记忆,但会增加复杂性和潜在的成本。
  3. 基于 Token 的滑动窗口: 精确计算每条消息的 Token 数,从旧到新累加,直到接近窗口上限,只发送窗口内的消息。这需要一个 Tokenizer 库(如 tiktoken for OpenAI)。
  4. 向量数据库 / RAG (Retrieval-Augmented Generation): 对于需要参考大量外部知识或非常长历史的情况,可以将历史信息或文档存储在向量数据库中。当用户提问时,先检索最相关的片段,然后将这些片段连同最近的对话历史一起注入上下文。这是更高级的技术。

对于学习目的,我们将首先实现简单的截断策略。


第二部分:Electron 基础与项目搭建

2.1 Electron 简介:是什么,为什么选择它?

Electron 是一个使用 Web 技术(HTML, CSS, JavaScript)构建跨平台桌面应用程序的框架。它结合了 Chromium(Google Chrome 的开源内核,用于渲染界面)和 Node.js(用于访问底层系统功能和后端逻辑)。

为什么选择 Electron 来学习 MCP?

  • 熟悉的技术栈: 如果你熟悉 Web 开发,上手 Electron 会相对容易。
  • 强大的 Node.js 后端: Electron 应用内建了一个完整的 Node.js 环境,可以直接在其中运行服务器端逻辑、访问文件系统、调用原生模块、发起网络请求(如调用 LLM API),而无需单独部署后端服务。这对于集成 LLM API 非常方便。
  • 跨平台: 一套代码可以打包成 Windows, macOS, Linux 应用。
  • 本地运行: 适合构建需要离线功能或希望将数据(如 API 密钥、聊天记录)保留在本地的应用。
  • 学习隔离: 将 UI(渲染器进程)和后端逻辑(主进程,包括 MCP 管理和 API 调用)清晰地分开,有助于理解软件架构。

2.2 核心概念:主进程 (Main Process) 与渲染器进程 (Renderer Process)

这是 Electron 最核心的概念:

  • 主进程 (Main Process):
    • 每个 Electron 应用只有一个主进程。
    • 它是应用的入口点,通常运行在 main.js 或类似的文件中。
    • 负责创建和管理应用的窗口(BrowserWindow 实例)。
    • 拥有完整的 Node.js API 访问权限,可以执行任何 Node.js 能做的事情(文件系统、网络、操作系统交互等)。
    • 我们的 MCP 核心逻辑和 LLM API 调用将主要放在主进程中,因为这里可以安全地管理 API 密钥并执行网络请求。
  • 渲染器进程 (Renderer Process):
    • 每个 BrowserWindow 实例都运行在一个独立的渲染器进程中。
    • 负责渲染 Web 页面(HTML, CSS, JS),也就是用户看到的界面。
    • 本质上是一个嵌入了 Node.js 能力的 Chromium 浏览器环境。
    • 出于安全原因,默认情况下渲染器进程不能直接访问 Node.js 的全部 API 或文件系统。 对系统资源的访问需要通过主进程进行。
    • 我们的聊天界面 UI 逻辑(显示消息、处理用户输入)将运行在渲染器进程中。

2.3 进程间通信 (IPC):连接两个世界的桥梁

由于主进程和渲染器进程是独立的,它们需要一种机制来相互通信。这就是 IPC (Inter-Process Communication)。Electron 提供了几个模块来实现 IPC:

  • ipcMain (在主进程中使用): 监听来自渲染器进程的消息,并可以向特定的渲染器进程发送消息。
  • ipcRenderer (在渲染器进程中使用): 向主进程发送消息,并监听来自主进程的消息。

常见的 IPC 模式:

  1. 渲染器 -> 主进程 (单向): 渲染器发送消息,主进程处理。例如,用户点击发送按钮后,渲染器通过 ipcRenderer.send() 将消息内容发送给主进程。
  2. 渲染器 -> 主进程 -> 渲染器 (请求/响应): 渲染器发送请求,主进程处理后将结果发回给该渲染器。例如,渲染器请求主进程调用 LLM API,主进程完成后通过 event.reply()webContents.send() 将 AI 的响应发回。这需要使用 ipcRenderer.invoke()ipcMain.handle() (推荐,基于 Promise) 或 ipcRenderer.on() 配合 event.reply()

为了安全,通常不直接在渲染器进程中暴露 ipcRenderer。而是通过 preload.js 脚本选择性地将安全的 IPC 功能暴露给渲染器。

2.4 开发环境准备

你需要安装:

  1. Node.js:Node.js 官网 下载并安装 LTS 版本。这将同时安装 npm (Node Package Manager)。
  2. (可选) Yarn: 另一个流行的包管理器,可以替代 npm。 (npm install -g yarn)
  3. 代码编辑器: 如 VS Code,它对 JavaScript 和 Electron 有良好的支持。

2.5 创建第一个 Electron 项目

推荐使用官方的 create-electron-app 工具或 Electron Forge:

使用 Electron Forge (推荐):

bash 复制代码
# 创建项目 (会提示选择模板,可以选择如 `webpack` 或 `vite` 模板)
npm init electron-app@latest my-mcp-chat-app --template=webpack # 或 --template=vite
# 或使用 yarn
# yarn create electron-app my-mcp-chat-app --template=webpack

# 进入项目目录
cd my-mcp-chat-app

# 安装依赖
npm install # 或 yarn install

# 启动开发环境
npm run start # 或 yarn start

Electron Forge 会生成一个包含基本结构、构建脚本和热重载功能的项目模板,极大地简化了开发流程。

手动配置 (更底层,有助于理解):

  1. 创建项目目录 my-mcp-chat-app 并进入。

  2. 初始化 npm 项目: npm init -y

  3. 安装 Electron: npm install --save-dev electron

  4. 创建核心文件:

    • main.js: 主进程入口。
    • index.html: 渲染器进程的 UI 页面。
    • renderer.js: 渲染器进程的 JavaScript。
    • (可选) preload.js: 预加载脚本。
  5. 修改 package.json,添加入口点和启动脚本:

    json 复制代码
    {
      "name": "my-mcp-chat-app",
      "version": "1.0.0",
      "description": "",
      "main": "main.js", // 指定主进程文件
      "scripts": {
        "start": "electron ." // 添加启动脚本
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "electron": "^28.0.0" // 版本号可能不同
      }
    }
  6. 编写 main.js, index.html, renderer.js 的基本内容 (后续章节会详细介绍)。

2.6 项目结构概览 (以 Electron Forge Webpack 模板为例)

csharp 复制代码
my-mcp-chat-app/
├── node_modules/       # 项目依赖
├── out/                # 打包输出目录
├── src/                # 源代码目录
│   ├── main.js         # 主进程入口 (或 index.js)
│   ├── preload.js      # 预加载脚本 (或 index.js)
│   ├── renderer.js     # 渲染器进程入口 (或 index.js)
│   └── index.html      # HTML 页面
├── forge.config.js     # Electron Forge 配置文件
├── package.json        # 项目元数据和依赖
└── webpack.main.config.js # Webpack 主进程配置
└── webpack.renderer.config.js # Webpack 渲染器进程配置
└── webpack.rules.js    # Webpack 规则
└── ... (其他配置文件)

不同模板结构可能略有差异,但核心文件(main.js, preload.js, renderer.js, index.html)的概念是通用的。


第三部分:构建聊天界面 (UI)

我们将使用简单的 HTML, CSS 和 Vanilla JavaScript 来构建界面,专注于核心功能,避免引入前端框架的复杂性,以便更清晰地展示 Electron 和 MCP 的集成。

3.1 选择前端技术栈

  • HTML: 定义页面结构。
  • CSS: 设置样式和布局。
  • Vanilla JavaScript: 处理用户交互、DOM 操作以及与主进程的通信。

你当然也可以使用 React, Vue, Angular 等框架,Electron 对它们都有良好的支持。但为了简化学习,我们这里用原生技术。

3.2 HTML 结构设计 (src/index.html)

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">
  <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">
  <title>MCP 学习聊天室</title>
  <link rel="stylesheet" href="styles.css"> <!-- 引入 CSS 文件 -->
</head>
<body>
  <div class="chat-container">
    <h1>MCP 学习聊天室</h1>

    <div id="message-list" class="message-list">
      <!-- 消息将通过 JavaScript 动态添加到这里 -->
      <!-- 示例消息结构:
      <div class="message user-message">
        <span class="message-sender">你:</span>
        <p class="message-content">你好!</p>
      </div>
      <div class="message assistant-message">
        <span class="message-sender">AI:</span>
        <p class="message-content">你好!有什么可以帮你的吗?</p>
      </div>
       -->
    </div>

    <div class="input-area">
      <textarea id="message-input" placeholder="输入消息..."></textarea>
      <button id="send-button">发送</button>
    </div>
  </div>

  <!-- 引入渲染器脚本 -->
  <!-- 注意:如果使用 Electron Forge + Webpack/Vite,入口点可能是由打包工具处理的 -->
  <!-- 查看你的模板配置,可能不需要手动引入 renderer.js -->
  <!-- <script src="./renderer.js"></script> -->
</body>
</html>
  • Content-Security-Policy (CSP): 这是 Electron 推荐的安全设置,限制了资源加载的来源,防止 XSS 攻击。
  • chat-container: 包裹整个聊天界面的容器。
  • message-list: 用于显示消息的区域。我们会用 JS 动态添加消息元素。
  • message: 代表单条消息的容器。
  • user-message, assistant-message: 用于区分用户和 AI 消息的 CSS 类,方便设置不同样式。
  • message-sender, message-content: 显示发送者和内容。
  • input-area: 包含输入框和发送按钮。
  • message-input: textarea 用于多行输入。
  • send-button: 发送按钮。

3.3 CSS 样式实现 (src/styles.css 或放在 <style> 标签内)

创建一个 src/styles.css 文件 (或根据你的模板调整路径) 并添加基础样式:

css 复制代码
body {
  font-family: sans-serif;
  margin: 0;
  background-color: #f4f4f4;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
}

.chat-container {
  width: 90%;
  max-width: 600px;
  height: 80vh;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: column;
  overflow: hidden; /* 防止内容溢出 */
}

h1 {
  text-align: center;
  padding: 15px;
  margin: 0;
  background-color: #444;
  color: white;
  font-size: 1.2em;
}

.message-list {
  flex-grow: 1; /* 占据剩余空间 */
  overflow-y: auto; /* 允许内容滚动 */
  padding: 15px;
  border-bottom: 1px solid #eee;
  display: flex; /* 使消息可以从底部开始排列 */
  flex-direction: column; /* 消息垂直排列 */
}

.message {
  margin-bottom: 15px;
  max-width: 80%; /* 消息不会占满整行 */
  padding: 10px 15px;
  border-radius: 18px;
  line-height: 1.4;
}

.user-message {
  background-color: #dcf8c6;
  align-self: flex-end; /* 用户消息靠右 */
  border-bottom-right-radius: 5px;
}

.assistant-message {
  background-color: #eee;
  align-self: flex-start; /* AI 消息靠左 */
  border-bottom-left-radius: 5px;
}

.message-sender {
  font-weight: bold;
  font-size: 0.8em;
  color: #555;
  display: block; /* 让发送者单独一行(可选) */
  margin-bottom: 3px;
}

.message-content {
    margin: 0;
    word-wrap: break-word; /* 长单词换行 */
}


.input-area {
  display: flex;
  padding: 15px;
  border-top: 1px solid #eee;
  background-color: #f9f9f9;
}

#message-input {
  flex-grow: 1;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 20px;
  resize: none; /* 禁止拖拽调整大小 */
  margin-right: 10px;
  font-size: 1em;
  height: 40px; /* 初始高度 */
  box-sizing: border-box; /* 让 padding 不增加总高度 */
}

#send-button {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 20px;
  cursor: pointer;
  font-size: 1em;
  transition: background-color 0.2s ease;
}

#send-button:hover {
  background-color: #0056b3;
}

#send-button:disabled {
    background-color: #ccc;
    cursor: not-allowed;
}

这些样式提供了一个基础的、类似常见聊天应用的布局。

3.4 JavaScript 交互逻辑 (src/renderer.js)

这个文件负责处理用户界面上的交互:

  1. 获取 DOM 元素的引用。
  2. 监听发送按钮的点击事件和输入框的回车事件。
  3. 获取用户输入的消息。
  4. 将用户消息显示在界面上。
  5. 通过 IPC 将用户消息发送给主进程处理 (这是与 MCP 和 LLM 集成的关键)
  6. 监听来自主进程的 AI 响应消息,并将其显示在界面上。
  7. (可选) 在等待 AI 响应时显示加载状态。
javascript 复制代码
// src/renderer.js

// 获取 DOM 元素
const messageList = document.getElementById('message-list');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');

// --- 消息显示函数 ---
function displayMessage(sender, content, type) {
  const messageElement = document.createElement('div');
  messageElement.classList.add('message', `${type}-message`); // 'user-message' or 'assistant-message'

  const senderElement = document.createElement('span');
  senderElement.classList.add('message-sender');
  senderElement.textContent = sender === 'user' ? '你:' : 'AI:';

  const contentElement = document.createElement('p');
  contentElement.classList.add('message-content');
  contentElement.textContent = content; // 简单处理文本,实际应用可能需要处理 Markdown 或 HTML

  // messageElement.appendChild(senderElement); // 可以选择是否显示发送者标签
  messageElement.appendChild(contentElement);
  messageList.appendChild(messageElement);

  // 滚动到底部
  messageList.scrollTop = messageList.scrollHeight;
}

// --- 发送消息处理 ---
function sendMessage() {
    const messageText = messageInput.value.trim();
    if (messageText === '') {
        return; // 不发送空消息
    }

    // 1. 在界面上显示用户自己的消息
    displayMessage('user', messageText, 'user');

    // 2. 清空输入框
    messageInput.value = '';

    // 3. 禁用发送按钮和输入框,防止重复发送 (可选,但推荐)
    setSendingState(true);

    // 4. 通过 IPC 将消息发送给主进程处理
    // 我们将使用 preload.js 暴露的 'sendMessageToMain' 函数
    if (window.electronAPI && window.electronAPI.sendMessageToMain) {
        console.log('Renderer: Sending message to main:', messageText);
        window.electronAPI.sendMessageToMain(messageText);
    } else {
        console.error("electronAPI or sendMessageToMain not found! Check preload.js");
        // 可以显示错误给用户
        displayMessage('system', '错误:无法连接到后台服务。', 'assistant'); // 借用 assistant 样式
        setSendingState(false); // 发生错误,恢复输入
    }
}

// --- 设置发送状态 (禁用/启用输入) ---
function setSendingState(isSending) {
    messageInput.disabled = isSending;
    sendButton.disabled = isSending;
    if (isSending) {
        sendButton.textContent = '发送中...';
    } else {
        sendButton.textContent = '发送';
        messageInput.focus(); // AI响应后,输入框重新获得焦点
    }
}


// --- 监听发送按钮点击 ---
sendButton.addEventListener('click', sendMessage);

// --- 监听输入框回车 (Ctrl+Enter 或 Shift+Enter 发送,普通 Enter 换行 - 可选) ---
messageInput.addEventListener('keydown', (event) => {
  // 按下 Enter 发送 (如果需要 Enter 换行,Ctrl+Enter 发送,则逻辑不同)
  if (event.key === 'Enter' && !event.shiftKey) {
    event.preventDefault(); // 阻止默认的换行行为
    sendMessage();
  }
});

// --- 监听来自主进程的 AI 响应 ---
// 我们将使用 preload.js 暴露的 'handleAIMessage' 函数来注册回调
if (window.electronAPI && window.electronAPI.handleAIMessage) {
    window.electronAPI.handleAIMessage((event, aiMessage) => {
        console.log('Renderer: Received AI message from main:', aiMessage);
        // 在界面上显示 AI 的消息
        displayMessage('assistant', aiMessage, 'assistant');
        // AI 响应回来后,恢复输入状态
        setSendingState(false);
    });

    // (可选) 监听来自主进程的错误消息
    window.electronAPI.handleError((event, errorMessage) => {
        console.error('Renderer: Received error from main:', errorMessage);
        displayMessage('system', `错误: ${errorMessage}`, 'assistant'); // 借用 assistant 样式显示错误
        setSendingState(false); // 出错后也要恢复输入
    });

} else {
    console.error("electronAPI or handleAIMessage/handleError not found! Check preload.js");
    displayMessage('system', '错误:无法接收后台消息。', 'assistant');
}


// --- 初始化 ---
// 可以在这里发送一条欢迎消息,或者从主进程请求历史记录(如果做了持久化)
// displayMessage('assistant', '你好!我是 MCP 学习助手,请开始提问吧。', 'assistant');
setSendingState(false); // 初始状态设为可输入
console.log('Renderer process loaded.');

关键点:

  • displayMessage 函数: 封装了将消息添加到 UI 的逻辑,包括创建 DOM 元素、添加 CSS 类和滚动到底部。
  • sendMessage 函数: 获取输入、显示用户消息、清空输入框、调用 window.electronAPI.sendMessageToMain(messageText) 将消息发送到主进程
  • setSendingState 函数: 控制输入框和按钮的禁用状态,提供用户反馈。
  • window.electronAPI.handleAIMessage(...): 注册一个回调函数,当主进程通过 IPC 发送 AI 响应回来时,这个回调会被触发 ,然后调用 displayMessage 显示 AI 消息并恢复输入状态。
  • window.electronAPI.handleError(...): 注册错误处理回调。
  • window.electronAPI: 这是我们将在 preload.js 中定义的对象,用于安全地暴露 IPC 功能给渲染器进程。

现在,我们有了 UI 的骨架和基础交互。下一步是在 Electron 中实现 MCP 逻辑和连接主/渲染器进程。


第四部分:在 Electron 中实现 MCP 逻辑

这部分是核心,我们将连接主进程和渲染器进程,并在主进程中实现 MCP 上下文管理。

4.1 数据结构定义 (回顾)

我们将在主进程中维护一个数组来存储 MCP 上下文,结构如下:

javascript 复制代码
// 示例 (在 main.js 中)
let conversationHistory = [
  // 可以预设一个 system prompt
  { role: 'system', content: '你是一个简洁明了的 AI 助手。' }
];

4.2 在渲染器进程中管理本地消息状态 (已在 renderer.js 中实现)

renderer.js 已经实现了将用户输入和 AI 响应显示在界面上的逻辑。它本身维护了 UI 上的消息列表状态。但它维护用于发送给 LLM 的完整 MCP 上下文。

4.3 通过 IPC 将用户消息和 MCP 请求发送到主进程

我们需要设置 IPC 通道。这涉及三个文件:main.js, preload.js, renderer.js

1. preload.js (安全桥梁)

preload.js 脚本在渲染器进程加载 Web 页面之前运行,并且可以访问 Node.js API 和 DOM API 。它的主要作用是选择性地、安全地将主进程的功能(通过 IPC)暴露给渲染器进程,而不是直接暴露 ipcRenderer 或其他 Node.js 模块。

javascript 复制代码
// src/preload.js
const { contextBridge, ipcRenderer } = require('electron');

console.log('Preload script loaded.');

// 使用 contextBridge 暴露安全的 API 给渲染器进程
contextBridge.exposeInMainWorld('electronAPI', {
  // --- 从渲染器发送到主进程 ---

  // 发送用户消息
  sendMessageToMain: (message) => {
    console.log('Preload: Sending "send-message" IPC event with:', message);
    ipcRenderer.send('send-message', message); // 'send-message' 是我们约定的通道名称
  },

  // --- 从主进程接收消息 ---

  // 注册处理 AI 回复的回调函数
  handleAIMessage: (callback) => {
    console.log('Preload: Registering handler for "ai-reply" IPC event.');
    // 使用 ipcRenderer.on 来持续监听来自主进程的 'ai-reply' 通道
    // 注意:为了避免内存泄漏,在窗口关闭或重新加载时应该移除监听器
    // 但在这个简单示例中,我们暂时省略清理逻辑
    ipcRenderer.on('ai-reply', (event, ...args) => callback(event, ...args));
  },

  // 注册处理错误信息的回调函数
  handleError: (callback) => {
    console.log('Preload: Registering handler for "app-error" IPC event.');
    ipcRenderer.on('app-error', (event, ...args) => callback(event, ...args));
  }

  // 如果需要请求/响应模式 (例如,请求历史记录)
  // invokeSomething: async (data) => {
  //   return await ipcRenderer.invoke('some-channel', data);
  // }
});
  • contextBridge.exposeInMainWorld('electronAPI', ...): 这是关键。它创建了一个 window.electronAPI 对象,渲染器进程的 JavaScript (renderer.js) 可以安全地访问这个对象及其定义的方法。
  • ipcRenderer.send('channel-name', data): 向主进程发送消息。
  • ipcRenderer.on('channel-name', callback): 监听来自主进程的指定通道的消息。回调函数会接收到 event 对象和主进程发送的数据。

2. main.js (主进程 - 监听 IPC)

主进程需要监听 preload.js 中使用的 IPC 通道 ('send-message')。

javascript 复制代码
// src/main.js (部分代码 - IPC 监听和 MCP 管理)
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');

// --- MCP 上下文管理 ---
let conversationHistory = [
  { role: 'system', content: '你是一个使用 Electron 构建的聊天机器人,请简洁回答。' }
];
const MAX_CONTEXT_MESSAGES = 10; // 示例:最多保留最近 10 条 user/assistant 消息 + system

// --- LLM API 调用函数 (将在下一部分实现) ---
async function callLLMApi(context) {
  console.log("Main: Calling LLM API with context:", JSON.stringify(context, null, 2));
  // 模拟 API 调用延迟
  await new Promise(resolve => setTimeout(resolve, 1500));

  // TODO: 在第五部分替换为真实的 API 调用
  // 模拟 AI 回复
  const lastUserMessage = context[context.length - 1]?.content || "空消息";
  const aiResponse = `我是 AI,收到了你的消息:"${lastUserMessage}"。MCP 学习进展如何?`;

  console.log("Main: Mock LLM API response:", aiResponse);
  return aiResponse; // 返回模拟响应
  // 真实场景下,这里会处理 API 的成功或失败
}

// --- 窗口创建函数 ---
function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      // __dirname 指向当前文件所在目录 (打包后可能会变化,要小心)
      // path.join 确保路径在不同操作系统下都正确
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true, // 推荐开启,增强安全性
      nodeIntegration: false, // 推荐关闭,渲染器不直接访问 Node API
    }
  });

  // 加载 index.html
  // 如果使用 Electron Forge + Webpack/Vite,入口点可能不同
  // 请参考你的模板配置
  if (process.env.NODE_ENV === 'development') {
    // 开发环境通常加载本地服务器 URL,支持热重载
    // 这个 URL 取决于你的模板 (Webpack dev server, Vite dev server)
    // mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY); // Webpack 模板示例
    // mainWindow.loadURL('http://localhost:5173');   // Vite 模板示例 (端口可能不同)
     mainWindow.loadFile(path.join(__dirname, '../src/index.html')); // 简单加载文件
  } else {
    // 生产环境加载打包后的文件
    mainWindow.loadFile(path.join(__dirname, '../src/index.html'));
  }


  // 打开开发者工具 (可选)
  mainWindow.webContents.openDevTools();

  // --- IPC 监听器 ---
  ipcMain.on('send-message', async (event, userMessage) => {
    console.log('Main: Received "send-message" IPC event with:', userMessage);

    // 1. 更新 MCP 上下文
    conversationHistory.push({ role: 'user', content: userMessage });

    // 2. (可选) 应用上下文截断策略
    applyContextTruncation(); // 实现见下文

    try {
      // 3. 调用 LLM API (目前是模拟调用)
      // 将当前的对话历史作为上下文传递
      const aiReply = await callLLMApi(conversationHistory);

      // 4. 将 AI 的回复也加入 MCP 上下文
      conversationHistory.push({ role: 'assistant', content: aiReply });
      // 再次截断,确保添加 AI 回复后也不超长 (虽然可能有点冗余,但保险)
      applyContextTruncation();

      // 5. 通过 IPC 将 AI 回复发送回渲染器进程
      console.log('Main: Sending "ai-reply" IPC event back to renderer with:', aiReply);
      // event.sender 指向发送消息的那个渲染器窗口
      event.sender.send('ai-reply', aiReply);

    } catch (error) {
      console.error('Main: Error processing message or calling LLM API:', error);
      // 向渲染器发送错误消息
      event.sender.send('app-error', `处理消息时出错: ${error.message}`);
      // 可以考虑是否从 history 中移除最后一条 user message
      // conversationHistory.pop();
    }
  });

  // (如果需要请求/响应模式)
  // ipcMain.handle('some-channel', async (event, data) => {
  //   const result = await doSomething(data);
  //   return result;
  // });

} // end createWindow

// --- 应用生命周期事件 ---
app.whenReady().then(() => {
  createWindow();

  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit();
});


// --- MCP 上下文截断函数 ---
function applyContextTruncation() {
  // 保留 system prompt (如果有)
  const systemPrompt = conversationHistory.find(msg => msg.role === 'system');
  const chatMessages = conversationHistory.filter(msg => msg.role !== 'system');

  if (chatMessages.length > MAX_CONTEXT_MESSAGES) {
    const messagesToKeep = chatMessages.slice(-MAX_CONTEXT_MESSAGES); // 取最后 N 条
    conversationHistory = systemPrompt ? [systemPrompt, ...messagesToKeep] : messagesToKeep;
    console.log(`Main: Context truncated. Kept ${conversationHistory.length} messages.`);
  }
}

关键更改和添加:

  • conversationHistory: 在主进程中定义,用于存储完整的 MCP 上下文。
  • MAX_CONTEXT_MESSAGES: 定义上下文窗口大小(以消息数量计,简单策略)。
  • callLLMApi 函数: 目前是模拟函数,接收上下文,等待一下,然后返回一个固定的 AI 回复。我们将在下一部分替换它。
  • ipcMain.on('send-message', ...): 监听来自渲染器的 'send-message' 事件。
    • 接收到用户消息 (userMessage)。
    • 将其添加到 conversationHistory
    • 调用 applyContextTruncation 来管理上下文长度。
    • 调用 (模拟的) callLLMApi,传入当前 conversationHistory
    • 将 AI 返回的 aiReply 也添加到 conversationHistory
    • 再次调用截断函数 (可选但安全)。
    • 使用 event.sender.send('ai-reply', aiReply) 将 AI 回复通过 'ai-reply' 通道发回给发送消息的那个渲染器窗口
    • 添加了 try...catch 来处理潜在错误,并通过 'app-error' 通道发送错误信息。
  • applyContextTruncation 函数: 实现了简单的截断逻辑:保留 system prompt (如果存在),然后只保留最新的 MAX_CONTEXT_MESSAGES 条用户/助手消息。

3. renderer.js (调用暴露的 API)

我们之前写的 renderer.js 已经在使用 window.electronAPI 了,现在它应该可以正常工作了:

  • window.electronAPI.sendMessageToMain(messageText) 会触发 preload.js 中的 ipcRenderer.send('send-message', ...)
  • window.electronAPI.handleAIMessage((event, aiMessage) => { ... }) 会注册一个回调,当 main.js 中的 event.sender.send('ai-reply', ...) 执行时,这个回调会被 preload.js 中的 ipcRenderer.on('ai-reply', ...) 触发。

至此,我们已经打通了渲染器 -> 主进程 -> (模拟 LLM) -> 主进程 -> 渲染器的完整流程,并且在主进程中初步实现了 MCP 上下文的存储和简单截断管理。

4.4 在主进程中维护和更新完整的 MCP 上下文 (已在 main.js 中实现)

main.js 中的 conversationHistory 数组现在就是我们维护的 MCP 上下文。每次用户发送消息和 AI 回复后,都会更新这个数组。

4.5 实现上下文截断策略 (已在 main.js 中实现)

applyContextTruncation 函数实现了基于消息数量的简单截断。

更复杂的截断 (基于 Token):

如果需要基于 Token 的精确截断,你需要:

  1. 安装一个 Tokenizer 库,如 tiktoken (适用于 OpenAI 模型): npm install tiktokenyarn add tiktoken
  2. main.js 中引入并使用它来计算每条消息的 Token 数。
  3. 修改 applyContextTruncation 逻辑,累加 Token 数,确保总数不超过模型的限制。
javascript 复制代码
// 示例:使用 tiktoken 进行截断 (需要安装 tiktoken)
// const { getEncoding } = require("tiktoken");
// const encoding = getEncoding("cl100k_base"); // 适用于 gpt-3.5-turbo 和 gpt-4

// const MAX_CONTEXT_TOKENS = 4000; // 示例 Token 限制

// function applyTokenBasedTruncation() {
//   let currentTokens = 0;
//   const messagesToSend = [];
//   const systemPrompt = conversationHistory.find(msg => msg.role === 'system');

//   // 计算 System Prompt 的 Token (如果存在)
//   if (systemPrompt) {
//     currentTokens += encoding.encode(systemPrompt.content).length;
//   }

//   // 从最新消息开始向前遍历
//   for (let i = conversationHistory.length - 1; i >= 0; i--) {
//     const message = conversationHistory[i];
//     if (message.role === 'system') continue; // System prompt 已单独处理

//     const messageTokens = encoding.encode(message.content).length;

//     // 加上这条消息的 token 是否会超出限制?
//     // (需要为模型响应预留一些 token,比如预留 500 token)
//     const PRESERVE_FOR_RESPONSE = 500;
//     if (currentTokens + messageTokens < MAX_CONTEXT_TOKENS - PRESERVE_FOR_RESPONSE) {
//       messagesToSend.unshift(message); // 加到结果数组的开头,保持顺序
//       currentTokens += messageTokens;
//     } else {
//       // 超出限制,停止添加更早的消息
//       console.log(`Main: Token limit reached. Stopped adding older messages.`);
//       break;
//     }
//   }

//   // 组合最终的上下文
//   conversationHistory = systemPrompt ? [systemPrompt, ...messagesToSend] : messagesToSend;
//   console.log(`Main: Context truncated based on tokens. Kept ${conversationHistory.length} messages, ~${currentTokens} tokens.`);
// }

// 在 ipcMain.on('send-message', ...) 中调用 applyTokenBasedTruncation() 替换之前的调用

4.6 将 MCP 数据格式化为 LLM API 请求格式

我们当前的 conversationHistory 数组 ([{ role: 'user', content: '...' }, ...]) 已经非常接近许多 LLM API(如 OpenAI)要求的格式了。在 callLLMApi 函数内部,当准备发送请求时,这个数组可以直接用作 API 请求体中 messages 字段的值。


第五部分:集成大语言模型 (LLM) API

现在,我们将用真实的 LLM API 调用替换掉 main.js 中的模拟函数。这里以 OpenAI API (GPT 系列模型) 为例。

5.1 选择 LLM API

  • OpenAI API: 常用,性能强大,提供多种模型 (GPT-4, GPT-3.5-Turbo)。需要注册账号并获取 API Key,按使用量付费。
  • Anthropic Claude API: 另一个强大的竞争者,有不同的特点和定价。
  • Google Gemini API: Google 提供的模型 API。
  • 本地模型 (通过 Ollama, LM Studio 等): 如果你希望在本地运行模型(无需 API Key,数据不离开本地),可以使用 Ollama 或 LM Studio 等工具启动一个本地服务,它们通常提供与 OpenAI 兼容的 API 接口。这样,你的 Electron 应用只需将请求发送到本地地址(如 http://localhost:11434/v1/chat/completions)。

我们将使用 OpenAI API 作为示例。

5.2 获取 API 密钥及安全管理

  1. 注册 OpenAI 账户: 访问 OpenAI 官网 并创建一个账户。
  2. 获取 API Key: 登录后,在 API Keys 页面创建一个新的 Secret Key。这个 Key 非常重要,绝不能直接硬编码在代码中!
  3. 安全存储 API Key:
    • 环境变量: 这是推荐的方式。在启动 Electron 应用前设置一个环境变量,例如 OPENAI_API_KEY
      • 在终端 (Linux/macOS): export OPENAI_API_KEY='your_key_here'
      • 在终端 (Windows CMD): set OPENAI_API_KEY=your_key_here
      • 在终端 (Windows PowerShell): $env:OPENAI_API_KEY='your_key_here'
      • 然后从这个终端启动你的 Electron 应用 (npm run start)。在 main.js 中可以通过 process.env.OPENAI_API_KEY 读取。
    • .env 文件: 使用 dotenv 包 (npm install dotenv)。在项目根目录创建 .env 文件,写入 OPENAI_API_KEY=your_key_here。在 main.js 顶部加载它:require('dotenv').config();确保将 .env 文件添加到 .gitignore 中,防止意外提交!
    • Electron Store (不推荐用于敏感密钥): 可以用 electron-store 存储配置,但对于 API Key 这种敏感信息,环境变量或专门的密钥管理方案更好。

我们将使用环境变量的方式。

5.3 在主进程中使用 Node.js 发起 HTTP 请求

我们需要一个 HTTP 客户端库来向 OpenAI API 发送 POST 请求。node-fetch (v2 支持 CommonJS) 或 axios 都是不错的选择。

安装 node-fetch (v2): npm install node-fetch@2 (v3+ 是 ESM only,在默认的 CommonJS Electron 项目中使用 v2 更方便)

修改 main.js 中的 callLLMApi 函数:

javascript 复制代码
// src/main.js (修改 callLLMApi 部分)
const fetch = require('node-fetch'); // 引入 node-fetch

// --- 从环境变量读取 API Key ---
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
if (!OPENAI_API_KEY) {
  console.error("错误:未设置 OPENAI_API_KEY 环境变量!");
  // 在应用启动时就应该提示用户或退出
  // 这里我们暂时允许应用继续运行,但 API 调用会失败
}

const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
const MODEL_NAME = 'gpt-3.5-turbo'; // 或者 'gpt-4', 'gpt-4-turbo-preview' 等

// --- 替换原来的模拟函数 ---
async function callLLMApi(context) {
  console.log(`Main: Calling OpenAI API (${MODEL_NAME}) with context size: ${context.length}`);

  if (!OPENAI_API_KEY) {
    throw new Error("OpenAI API Key 未配置。");
  }

  try {
    const response = await fetch(OPENAI_API_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${OPENAI_API_KEY}`
      },
      body: JSON.stringify({
        model: MODEL_NAME,
        messages: context, // 直接使用我们的 MCP 上下文数组
        // temperature: 0.7, // 控制创造性,可选
        // max_tokens: 150, // 限制回复长度,可选
        // stream: false, // 设置为 true 以启用流式响应 (见 5.6)
      })
    });

    if (!response.ok) {
      // 处理 API 返回的错误
      const errorBody = await response.json();
      console.error('Main: OpenAI API Error:', response.status, response.statusText, errorBody);
      throw new Error(`OpenAI API 请求失败: ${response.status} ${response.statusText} - ${errorBody?.error?.message || '未知错误'}`);
    }

    const data = await response.json();

    // 提取 AI 的回复内容
    // 检查响应结构是否符合预期
    if (data.choices && data.choices.length > 0 && data.choices[0].message && data.choices[0].message.content) {
       const aiResponse = data.choices[0].message.content.trim();
       console.log("Main: OpenAI API Response received:", aiResponse);
       return aiResponse;
    } else {
       console.error("Main: OpenAI API 响应格式不符合预期:", data);
       throw new Error("无法从 API 响应中提取有效的回复内容。");
    }

  } catch (error) {
    console.error('Main: Error calling OpenAI API:', error);
    // 将错误向上抛出,让调用者 (IPC handler) 处理
    throw error; // 重新抛出错误,以便在 IPC 处理程序中捕获并发送给渲染器
  }
}

关键更改:

  • 引入 node-fetch
  • process.env.OPENAI_API_KEY 读取密钥,并添加检查。
  • 定义 API URL 和模型名称。
  • 使用 fetch 发送 POST 请求:
    • 设置 Authorization header。
    • 将我们的 context (即 conversationHistory) 数组直接放在请求体的 messages 字段中。
  • 检查 response.ok 来判断请求是否成功 (HTTP 状态码 2xx)。
  • 如果失败,尝试解析错误体并抛出更详细的错误。
  • 如果成功,解析 JSON 响应体 (response.json())。
  • 从响应的 data.choices[0].message.content 中提取 AI 的回复文本。
  • 添加了更健壮的错误处理和日志记录。

5.4 处理 API 响应 (已在 callLLMApi 和 IPC handler 中实现)

  • 成功: callLLMApi 函数解析响应并返回 AI 的文本内容。ipcMain.on('send-message', ...) 的回调接收到这个内容,将其添加到 conversationHistory,并通过 event.sender.send('ai-reply', aiReply) 发送给渲染器。
  • 失败/错误:
    • fetch 本身可能因网络问题失败 (进入 catch 块)。
    • API 可能返回非 2xx 状态码 (如 401 未授权, 400 错误请求, 429 请求过多, 500 服务器错误) (被 !response.ok 捕获)。
    • API 响应格式可能不符合预期 (进入 elsecatch 块)。
    • 在这些情况下,callLLMApi 会抛出错误。这个错误会在 ipcMain.onasync 回调中被 try...catch 块捕获,然后通过 event.sender.send('app-error', ...) 将错误消息发送给渲染器。renderer.js 中的 window.electronAPI.handleError 回调会接收并显示这个错误。

5.5 将 LLM 的响应通过 IPC 发送回渲染器进程 (已实现)

event.sender.send('ai-reply', aiReply)event.sender.send('app-error', errorMessage) 负责将成功结果或错误信息发送回渲染器。

5.6 (进阶) 处理流式响应 (Streaming Responses)

对于聊天应用,用户体验通常可以通过流式响应得到改善:AI 生成回复时,逐字或逐句地显示出来,而不是等待整个回复生成完毕。

这需要:

  1. 修改 API 调用:callLLMApifetch 请求体中设置 stream: true
  2. 处理流式数据: API 不会一次性返回 JSON,而是返回一个 Server-Sent Events (SSE) 流。你需要读取这个流,解析每一块数据(通常是 JSON 片段),提取其中的 delta.content(增量内容)。
  3. 修改 IPC: 不能只在最后发送一次 ai-reply。需要定义新的 IPC 通道,例如 ai-reply-stream-chunk,每收到一小块文本就通过这个通道发送给渲染器。还需要一个 ai-reply-stream-end 通道来告知渲染器流结束了。
  4. 修改渲染器: renderer.js 需要监听新的流式通道。收到 chunk 时,将文本追加到当前 AI 消息的末尾。收到 end 时,标记消息完成,并恢复输入状态。

实现流式响应会显著增加复杂度,但能极大提升交互感。 这里提供一个概念性的 main.js 修改思路:

javascript 复制代码
// src/main.js (流式处理概念)
// ... (需要修改 callLLMApi 和 IPC handler)

async function handleStreamedLLMResponse(context, eventSender) {
  if (!OPENAI_API_KEY) throw new Error("OpenAI API Key 未配置。");

  const response = await fetch(OPENAI_API_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${OPENAI_API_KEY}`
    },
    body: JSON.stringify({
      model: MODEL_NAME,
      messages: context,
      stream: true, // 开启流式
    })
  });

  if (!response.ok) {
     const errorBody = await response.json();
     throw new Error(`OpenAI API 请求失败: ${response.status} ${response.statusText} - ${errorBody?.error?.message || '未知错误'}`);
  }

  let fullResponse = ""; // 用于累积完整回复以存入 history
  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      const chunk = decoder.decode(value, { stream: true });
      // SSE 数据通常以 "data: " 开头,可能有多行,以 "\n\n" 分隔
      const lines = chunk.split('\n\n');
      for (const line of lines) {
        if (line.startsWith('data: ')) {
          const dataStr = line.substring(6).trim();
          if (dataStr === '[DONE]') {
             // 流结束的标志
             eventSender.send('ai-reply-stream-end'); // 通知渲染器结束
             console.log("Main: Stream ended.");
             return fullResponse; // 返回累积的完整回复
          }
          try {
            const jsonData = JSON.parse(dataStr);
            const deltaContent = jsonData.choices?.[0]?.delta?.content;
            if (deltaContent) {
              // 发送增量内容到渲染器
              eventSender.send('ai-reply-stream-chunk', deltaContent);
              fullResponse += deltaContent; // 累积完整回复
            }
          } catch (parseError) {
            console.warn('Main: Failed to parse stream chunk JSON:', dataStr, parseError);
          }
        }
      }
    }
  } catch (streamError) {
      console.error("Main: Error reading stream:", streamError);
      eventSender.send('app-error', `读取 AI 响应流时出错: ${streamError.message}`);
      throw streamError; // 重新抛出,可能需要在上层处理
  } finally {
      reader.releaseLock(); // 释放读取器锁
  }
   // 如果循环正常结束但没有收到 [DONE] (异常情况)
   eventSender.send('ai-reply-stream-end'); // 确保发送结束信号
   return fullResponse;
}

// --- 修改 IPC Handler ---
ipcMain.on('send-message', async (event, userMessage) => {
    console.log('Main: Received "send-message" IPC event with:', userMessage);
    conversationHistory.push({ role: 'user', content: userMessage });
    applyContextTruncation(); // Or token based truncation

    try {
      // 调用流式处理函数,传入 event.sender
      const fullAIReply = await handleStreamedLLMResponse(conversationHistory, event.sender);

      // 流结束后,将完整回复添加到历史记录
      if (fullAIReply) {
         conversationHistory.push({ role: 'assistant', content: fullAIReply });
         applyContextTruncation(); // Apply truncation again
      } else {
          console.warn("Main: Stream completed but no content accumulated.");
          // 可能需要发送一个空回复或错误给渲染器?
      }

    } catch (error) {
      console.error('Main: Error processing message or calling LLM API (stream):', error);
      // 错误已在 handleStreamedLLMResponse 内部或这里发送给渲染器
      if (!event.sender.isDestroyed()) { // 检查窗口是否还存在
           event.sender.send('app-error', `处理消息时出错: ${error.message}`);
           // 如果流已经开始,可能需要额外发送一个 stream-end 信号
           event.sender.send('ai-reply-stream-end');
      }
    }
});

// --- preload.js 和 renderer.js 也需要相应修改 ---
// preload.js: 添加 handleStreamChunk 和 handleStreamEnd
// renderer.js:
//   - 在 sendMessage 后,创建一个空的 assistant 消息 div
//   - 监听 chunk,将内容追加到该 div
//   - 监听 end,完成追加,恢复输入状态

注意: 流式处理实现细节较多,需要仔细处理各种边界情况和错误。对于初学,可以先实现非流式版本。


第六部分:整合流程与代码示例

现在我们将前面各部分的代码整合起来,展示关键文件的核心内容。

6.1 整体数据流图

sequenceDiagram participant R as Renderer (UI - renderer.js) participant P as Preload (preload.js) participant M as Main (main.js) participant LLM as LLM API (e.g., OpenAI) R->>P: User types message & clicks Send P->>M: ipcRenderer.send('send-message', userMsg) Note over M: Add userMsg to conversationHistory Note over M: Apply context truncation (MCP) M->>LLM: fetch(API_URL, { messages: conversationHistory, ... }) alt Successful Response LLM-->>M: API Response (JSON with AI reply) Note over M: Add aiReply to conversationHistory Note over M: Apply context truncation (MCP) M->>P: event.sender.send('ai-reply', aiReply) P->>R: ipcRenderer.on('ai-reply') callback fired Note over R: Display AI reply in UI Note over R: Enable input else API Error or Network Error LLM-->>M: Error (e.g., 4xx, 5xx) or Fetch fails Note over M: Catch error M->>P: event.sender.send('app-error', errorMsg) P->>R: ipcRenderer.on('app-error') callback fired Note over R: Display error message in UI Note over R: Enable input end

流式响应数据流 (简化版):

sequenceDiagram participant R as Renderer (UI - renderer.js) participant P as Preload (preload.js) participant M as Main (main.js) participant LLM as LLM API (e.g., OpenAI) R->>P: User types message & clicks Send P->>M: ipcRenderer.send('send-message', userMsg) Note over M: Add userMsg to conversationHistory Note over M: Apply context truncation (MCP) M->>LLM: fetch(API_URL, { messages: conversationHistory, stream: true }) LLM-->>M: SSE Stream starts (multiple data chunks) loop Receive Chunks M->>P: event.sender.send('ai-reply-stream-chunk', deltaContent) P->>R: ipcRenderer.on('ai-reply-stream-chunk') callback fired Note over R: Append deltaContent to current AI message div end M->>P: Stream ends (e.g., receives [DONE] or error) Note over M: Accumulate full reply in main process Note over M: Add full reply to conversationHistory Note over M: Apply context truncation (MCP) M->>P: event.sender.send('ai-reply-stream-end') P->>R: ipcRenderer.on('ai-reply-stream-end') callback fired Note over R: Finalize AI message display Note over R: Enable input opt Error during stream M->>P: event.sender.send('app-error', errorMsg) P->>R: ipcRenderer.on('app-error') callback fired Note over R: Display error Note over R: Enable input (possibly after stream-end signal) end

6.2 main.js (主进程核心代码 - 非流式版本)

javascript 复制代码
// src/main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const fetch = require('node-fetch');
// require('dotenv').config(); // 如果使用 .env 文件

// --- 配置 ---
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
const MODEL_NAME = 'gpt-3.5-turbo';
const MAX_CONTEXT_MESSAGES = 10; // 基于消息数量的截断

// --- MCP 上下文 ---
let conversationHistory = [
  { role: 'system', content: '你是一个基于 Electron 和 MCP 概念构建的 AI 聊天助手。' }
];

// --- 检查 API Key ---
if (!OPENAI_API_KEY) {
  console.error("错误:启动前必须设置 OPENAI_API_KEY 环境变量!");
  // 实际应用中可能需要更友好的提示或阻止应用启动
  // 为了演示,我们继续,但 API 调用会失败
}

// --- LLM API 调用函数 ---
async function callLLMApi(context) {
  console.log(`Main: Calling OpenAI API (${MODEL_NAME}) with context size: ${context.length}`);
  if (!OPENAI_API_KEY) throw new Error("OpenAI API Key 未配置。");

  try {
    const response = await fetch(OPENAI_API_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${OPENAI_API_KEY}`
      },
      body: JSON.stringify({
        model: MODEL_NAME,
        messages: context,
        // stream: false, // 非流式
      })
    });

    if (!response.ok) {
      const errorBody = await response.json().catch(() => ({})); // 尝试解析,失败则返回空对象
      console.error('Main: OpenAI API Error:', response.status, response.statusText, errorBody);
      throw new Error(`OpenAI API 请求失败: ${response.status} ${response.statusText} - ${errorBody?.error?.message || '无法解析错误详情'}`);
    }

    const data = await response.json();
    if (data.choices?.[0]?.message?.content) {
       const aiResponse = data.choices[0].message.content.trim();
       console.log("Main: OpenAI API Response received.");
       return aiResponse;
    } else {
       console.error("Main: OpenAI API 响应格式不符合预期:", data);
       throw new Error("无法从 API 响应中提取有效的回复内容。");
    }
  } catch (error) {
    console.error('Main: Error calling OpenAI API:', error);
    throw error;
  }
}

// --- MCP 上下文截断函数 ---
function applyContextTruncation() {
  const systemPrompt = conversationHistory.find(msg => msg.role === 'system');
  const chatMessages = conversationHistory.filter(msg => msg.role !== 'system');

  if (chatMessages.length > MAX_CONTEXT_MESSAGES) {
    const messagesToKeep = chatMessages.slice(-MAX_CONTEXT_MESSAGES);
    conversationHistory = systemPrompt ? [systemPrompt, ...messagesToKeep] : messagesToKeep;
    console.log(`Main: Context truncated. Kept ${conversationHistory.length} messages.`);
  }
}

// --- 创建窗口 ---
function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 650, // 稍微调大一点高度
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    }
  });

   // --- 加载 HTML ---
   // !! 重要: 根据你的 Electron 项目模板调整这里的加载方式 !!
   // Electron Forge with Webpack/Vite 通常有特定的入口变量
   // Example for Vite template (check your main process template file):
   // if (process.env.VITE_DEV_SERVER_URL) {
   //     mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
   // } else {
   //     mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
   // }
   // Fallback to simple loadFile if template specifics are unknown:
   mainWindow.loadFile(path.join(__dirname, '../src/index.html')) // 假设 HTML 在 src 目录
     .catch(err => console.error('Failed to load HTML:', err));


  // 打开开发者工具 (便于调试)
  if (process.env.NODE_ENV !== 'production') {
       mainWindow.webContents.openDevTools();
  }


  // --- IPC 监听器 ---
  ipcMain.on('send-message', async (event, userMessage) => {
    console.log('Main: Received "send-message":', userMessage);
    conversationHistory.push({ role: 'user', content: userMessage });
    applyContextTruncation();

    try {
      const aiReply = await callLLMApi(conversationHistory);
      conversationHistory.push({ role: 'assistant', content: aiReply });
      applyContextTruncation(); // 再次截断

      if (!event.sender.isDestroyed()) { // 检查窗口是否还存在
          console.log('Main: Sending "ai-reply" back to renderer.');
          event.sender.send('ai-reply', aiReply);
      }
    } catch (error) {
      console.error('Main: Error processing message:', error);
      if (!event.sender.isDestroyed()) {
          event.sender.send('app-error', `处理消息时出错: ${error.message}`);
      }
      // 考虑是否移除失败的用户消息
      // conversationHistory.pop();
    }
  });

}

// --- App 生命周期 ---
app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

6.3 preload.js (预加载脚本与 IPC 安全暴露)

javascript 复制代码
// src/preload.js
const { contextBridge, ipcRenderer } = require('electron');

console.log('Preload script loaded.');

contextBridge.exposeInMainWorld('electronAPI', {
  // Renderer -> Main
  sendMessageToMain: (message) => {
    ipcRenderer.send('send-message', message);
  },

  // Main -> Renderer (Callbacks)
  handleAIMessage: (callback) => {
    // 移除旧监听器,避免重复注册 (虽然简单场景下影响不大,但好习惯)
    ipcRenderer.removeAllListeners('ai-reply');
    ipcRenderer.on('ai-reply', (event, ...args) => callback(event, ...args));
  },
  handleError: (callback) => {
    ipcRenderer.removeAllListeners('app-error');
    ipcRenderer.on('app-error', (event, ...args) => callback(event, ...args));
  }

  // --- 如果实现流式响应,需要添加 ---
  // handleStreamChunk: (callback) => {
  //   ipcRenderer.removeAllListeners('ai-reply-stream-chunk');
  //   ipcRenderer.on('ai-reply-stream-chunk', (event, ...args) => callback(event, ...args));
  // },
  // handleStreamEnd: (callback) => {
  //   ipcRenderer.removeAllListeners('ai-reply-stream-end');
  //   ipcRenderer.on('ai-reply-stream-end', (event, ...args) => callback(event, ...args));
  // }
});

6.4 renderer.js (渲染器进程逻辑)

(与第三部分 3.4 中的代码基本一致,确保 window.electronAPI 的调用和回调注册正确无误。)

javascript 复制代码
// src/renderer.js

const messageList = document.getElementById('message-list');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
let currentAssistantMessageElement = null; // 用于流式响应

function displayMessage(sender, content, type, isComplete = true) {
  const messageElement = document.createElement('div');
  messageElement.classList.add('message', `${type}-message`);

  const contentElement = document.createElement('p');
  contentElement.classList.add('message-content');
  contentElement.textContent = content; // 注意:实际应用可能需要处理 Markdown 或 XSS

  messageElement.appendChild(contentElement);
  messageList.appendChild(messageElement);
  messageList.scrollTop = messageList.scrollHeight;

  if (type === 'assistant' && !isComplete) {
      currentAssistantMessageElement = contentElement; // 保存对当前 AI 消息内容的引用
  } else {
      currentAssistantMessageElement = null;
  }
}

function appendToCurrentAssistantMessage(textChunk) {
    if (currentAssistantMessageElement) {
        currentAssistantMessageElement.textContent += textChunk;
        messageList.scrollTop = messageList.scrollHeight; // 持续滚动
    } else {
        console.warn("Tried to append chunk but no active assistant message element.");
        // 可以考虑创建一个新的消息元素作为回退
    }
}

function sendMessage() {
    const messageText = messageInput.value.trim();
    if (messageText === '' || sendButton.disabled) return; // 避免发送空消息或重复发送

    displayMessage('user', messageText, 'user');
    messageInput.value = '';
    setSendingState(true);

    if (window.electronAPI?.sendMessageToMain) {
        console.log('Renderer: Sending message to main:', messageText);
        window.electronAPI.sendMessageToMain(messageText);
        // 如果是流式,可以在这里立即创建一个空的 assistant 消息占位符
        // displayMessage('assistant', '', 'assistant', false); // 创建一个不完整的消息
    } else {
        console.error("electronAPI.sendMessageToMain not found!");
        displayMessage('system', '错误:无法发送消息。', 'assistant');
        setSendingState(false);
    }
}

function setSendingState(isSending) {
    messageInput.disabled = isSending;
    sendButton.disabled = isSending;
    sendButton.textContent = isSending ? '...' : '发送';
    if (!isSending) messageInput.focus();
}

// --- 事件监听 ---
sendButton.addEventListener('click', sendMessage);
messageInput.addEventListener('keydown', (event) => {
  if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) { // !event.isComposing 防止输入法未完成时触发
    event.preventDefault();
    sendMessage();
  }
});

// --- IPC 监听 ---
if (window.electronAPI) {
    // 监听非流式回复
    window.electronAPI.handleAIMessage((event, aiMessage) => {
        console.log('Renderer: Received AI message:', aiMessage);
        displayMessage('assistant', aiMessage, 'assistant');
        setSendingState(false);
    });

    // 监听错误
    window.electronAPI.handleError((event, errorMessage) => {
        console.error('Renderer: Received error:', errorMessage);
        displayMessage('system', `错误: ${errorMessage}`, 'assistant'); // 用 assistant 样式显示错误
        setSendingState(false);
        currentAssistantMessageElement = null; // 出错时也重置流状态
    });

    // --- 如果实现流式响应,需要添加/修改监听器 ---
    /*
    let streamingOccurred = false;
    window.electronAPI.handleStreamChunk((event, chunk) => {
        console.log('Renderer: Received stream chunk:', chunk);
        if (!streamingOccurred) {
            // 第一次收到 chunk 时,确保有个空的 assistant 消息元素
            if (!currentAssistantMessageElement) {
                 displayMessage('assistant', '', 'assistant', false);
            }
            streamingOccurred = true;
        }
        appendToCurrentAssistantMessage(chunk);
    });

    window.electronAPI.handleStreamEnd((event) => {
        console.log('Renderer: Received stream end signal.');
        currentAssistantMessageElement = null; // 流结束,不再追加
        setSendingState(false); // 流结束后恢复输入
        streamingOccurred = false; // 重置流状态
    });

    // 修改 sendMessage,在发送后立即调用 displayMessage('assistant', '', 'assistant', false);
    // 取消或注释掉 handleAIMessage 的监听 (因为它不会再被非流式调用触发)
    */

} else {
    console.error("window.electronAPI is not defined! Check preload script and contextIsolation settings.");
    displayMessage('system', '错误:应用初始化失败。', 'assistant');
}

// --- 初始化 ---
setSendingState(false);
console.log('Renderer process initialized.');
// displayMessage('assistant', '你好!输入消息开始聊天吧。', 'assistant'); // 初始欢迎语

6.5 index.html (UI 结构)

(与第三部分 3.2 中的代码一致)

6.6 关键函数示例 (已分散在上述文件中)

  • MCP 更新: main.js 中的 conversationHistory.push(...)
  • MCP 截断: main.js 中的 applyContextTruncation()
  • API 调用: main.js 中的 callLLMApi() (非流式或流式版本)
  • IPC 发送 (Renderer->Main): renderer.js 调用 window.electronAPI.sendMessageToMain() -> preload.js 执行 ipcRenderer.send()
  • IPC 监听 (Main): main.js 中的 ipcMain.on('send-message', ...)
  • IPC 发送 (Main->Renderer): main.js 中的 event.sender.send('ai-reply', ...)event.sender.send('app-error', ...) (或流式 chunk/end)
  • IPC 监听 (Renderer): renderer.js 调用 window.electronAPI.handleAIMessage(...)handleError(...) (或流式) -> preload.js 执行 ipcRenderer.on(...) 注册回调

第七部分:进阶功能与优化

这个基础聊天室已经可以运行并让你学习 MCP 了,但还可以添加许多改进:

7.1 错误处理与用户友好提示

  • 更具体的错误:main.jscatch 块中,可以根据不同的错误类型(网络错误、API 认证错误、API 限流错误、解析错误等)向渲染器发送更具体的错误代码或消息。
  • UI 提示: renderer.js 收到错误时,除了在消息列表显示,还可以在 UI 顶部或底部显示一个短暂的错误提示条 (Toast Notification)。
  • 重试机制: 对于临时的网络错误或 API 限流 (如 429 Too Many Requests),可以在 main.js 中实现简单的自动重试逻辑(带延迟和次数限制)。

7.2 对话历史的持久化存储

目前 conversationHistory 只存在于内存中,应用关闭后就丢失了。

  • electron-store: 一个简单易用的库,用于在本地 JSON 文件中持久化存储数据。

    • 安装: npm install electron-store

    • 使用:

      javascript 复制代码
      // main.js
      const Store = require('electron-store');
      const store = new Store();
      
      // 读取历史记录 (应用启动时)
      let conversationHistory = store.get('chatHistory', [
        { role: 'system', content: '...' } // 默认值
      ]);
      
      // 保存历史记录 (每次更新后)
      function saveHistory() {
          store.set('chatHistory', conversationHistory);
      }
      
      // 在 ipcMain.on('send-message', ...) 成功处理完一次对话后调用 saveHistory()
      // 在 applyContextTruncation 后也可能需要保存,取决于策略
  • 数据库: 对于更复杂的场景或大量数据,可以使用 SQLite (sqlite3 npm 包) 或其他嵌入式数据库。

7.3 优化 MCP 策略

  • 基于 Token 的截断: 如前所述,使用 tiktoken 实现更精确的控制。

  • 摘要: 当历史变得很长时,定期调用 LLM 对旧的部分进行摘要,用摘要替换旧消息。这需要额外的 API 调用和逻辑。

    javascript 复制代码
    // 概念:在 applyContextTruncation 触发时
    if (historyTooLong) {
        const oldMessages = conversationHistory.slice(1, -KEEP_RECENT_COUNT); // 保留 system 和最近的
        const summaryPrompt = `请将以下对话摘要成一段话,保留关键信息:\n${JSON.stringify(oldMessages)}`;
        const summary = await callLLMApi([{ role: 'user', content: summaryPrompt }]); // 用专门的 prompt 请求摘要
        conversationHistory = [
            conversationHistory[0], // system prompt
            { role: 'system', content: `之前的对话摘要: ${summary}` }, // 插入摘要
            ...conversationHistory.slice(-KEEP_RECENT_COUNT) // 保留最近的
        ];
    }
  • RAG (Retrieval-Augmented Generation) 雏形: 如果聊天需要引用特定文档或知识库,可以将文档内容分块、向量化(需要 embedding 模型 API 调用,如 OpenAI Embeddings API)并存入向量数据库(如 ChromaDB、FAISS 的本地实现,或专门的 JS 库)。用户提问时,先将其问题向量化,在数据库中搜索最相似的文本块,然后将这些文本块作为上下文的一部分,连同对话历史一起发送给 LLM。这是一个相当高级的主题。

7.4 UI 改进

  • 加载状态:renderer.jssetSendingState 中,除了禁用按钮,可以在 AI 消息区域显示一个加载动画或占位符。
  • 滚动条样式: 美化 message-list 的滚动条。
  • 代码高亮: 如果 AI 的回复包含代码块,使用 highlight.js 或类似库进行语法高亮。需要在 displayMessage 中检测代码块并应用高亮。
  • Markdown 支持: 使用 marked 或类似库将 AI 回复中的 Markdown 格式渲染为 HTML。注意要进行 XSS 清理(如使用 DOMPurify)。
  • 输入框自动调整高度:textarea 根据内容自动增高。
  • 复制按钮: 为 AI 的消息添加一个"复制"按钮。

7.5 安全性考量

  • API 密钥: 强调绝不硬编码,使用环境变量或安全的密钥管理方式。
  • 输入清理: 虽然 LLM API 通常能处理各种输入,但在显示用户输入或 AI 回复(特别是渲染 HTML 时)要警惕 XSS 攻击。使用 textContent 而不是 innerHTML 来显示纯文本内容可以避免大部分问题。如果需要渲染 HTML(如 Markdown),必须使用 DOMPurify 等库进行清理。
  • IPC 安全: 坚持使用 contextBridgepreload.js,避免在渲染器中开启 nodeIntegration 或禁用 contextIsolation。只暴露必要且安全的函数给渲染器。
  • 依赖项安全: 定期更新 npm 依赖 (npm audit fix),关注安全漏洞。

7.6 打包与分发 Electron 应用

当你准备好分享你的应用时,需要将其打包成可执行文件。

  • Electron Forge: 如果你使用了 Forge,打包非常简单:
    • npm run makeyarn make
    • 这会根据 forge.config.js 的配置,在 out 目录下生成适用于当前操作系统的安装包或可执行文件(如 .exe, .dmg, .deb, .rpm)。你可以配置为特定平台打包。
  • Electron Builder: 另一个流行的打包工具,提供更多配置选项。
    • 安装: npm install --save-dev electron-builder
    • 配置 package.json 中的 build 字段。
    • 运行: npx electron-builder

打包过程会处理代码压缩、依赖捆绑、图标设置、签名(需要证书)等。


第八部分:总结与后续学习

8.1 项目回顾与关键学习点

通过构建这个 Electron 聊天室,你应该学习和实践了:

  • MCP 核心思想: 理解了为什么需要管理 LLM 上下文,以及 MCP 的基本组成(System Prompt, History, User Input)。
  • MCP 实现: 学会了使用 JavaScript 数组来组织上下文,并实现了简单的截断策略来处理上下文窗口限制。
  • Electron 基础: 掌握了主进程、渲染器进程的概念,以及如何使用 IPC(通过 preload.jscontextBridge)进行安全通信。
  • LLM API 集成: 了解了如何从 Electron 主进程安全地调用外部 API(如 OpenAI),处理认证、请求和响应(包括错误处理)。
  • 前后端分离思想: 即使在桌面应用中,也将 UI 逻辑(Renderer)和核心业务逻辑(Main,包含 MCP 和 API 调用)分离。

8.2 常见问题与排错思路

  • API Key 错误: 检查环境变量是否正确设置、是否在正确的终端启动应用、Key 本身是否有效、是否还有额度。
  • IPC 不工作:
    • 确认 preload.jsBrowserWindowwebPreferences 中正确配置。
    • 确认 contextIsolation: true (推荐) 并且使用了 contextBridge.exposeInMainWorld
    • 检查 preload.jsrenderer.js 中使用的通道名称 ('send-message', 'ai-reply') 是否与 main.jsipcMain.on/handleevent.sender.send 使用的名称完全一致。
    • preload.jsrenderer.js 的开头添加 console.log 确认脚本是否被加载。
    • main.jsipcMain.on 回调开头添加 console.log 确认主进程是否收到消息。
    • 打开开发者工具 (Main: mainWindow.webContents.openDevTools()) 查看渲染器进程的 Console 输出和网络请求。
  • HTML/CSS/JS 不加载或样式错误: 检查 main.jsmainWindow.loadFilemainWindow.loadURL 的路径是否正确。检查 HTML 中 CSS 和 JS 文件的引用路径。使用开发者工具的 Elements 和 Console 面板调试。
  • 上下文似乎丢失 (AI "失忆"):
    • main.js 中打印每次调用 callLLMApi 前的 conversationHistory,确认历史是否按预期累积。
    • 检查 applyContextTruncation 的逻辑是否符合预期,是否过早地丢弃了重要信息。
    • 确认 API 调用时 messages 字段确实包含了正确的上下文数组。
  • 应用无法启动: 检查 package.json 中的 main 字段是否指向正确的入口文件 (main.js)。查看终端的错误输出。

8.3 进一步学习 MCP 和 LLM 应用开发的资源

  • LLM 提供商文档:
  • Electron 官方文档: www.electronjs.org/docs/latest... (非常全面)
  • Tokenizer 库: tiktoken (Python/JS)
  • 上下文管理策略深入: 搜索 "LLM context management strategies", "LLM long context handling", "Retrieval-Augmented Generation (RAG)"。
  • LangChain / LlamaIndex: 这些是流行的 LLM 应用开发框架,它们封装了许多 MCP、RAG、Agent 等高级概念的实现,可以极大地简化开发。虽然它们抽象了很多细节,但在理解了底层原理(如此项目)后学习它们会更容易。它们也有 JavaScript/TypeScript 版本。
相关推荐
冴羽14 分钟前
SvelteKit 最新中文文档教程(17)—— 仅服务端模块和快照
前端·javascript·svelte
uhakadotcom16 分钟前
Langflow:打造AI应用的强大工具
前端·面试·github
前端小张同学25 分钟前
AI编程-cursor无限使用, 还有谁不会🎁🎁🎁??
前端·cursor
yanxy51229 分钟前
【TS学习】(15)分布式条件特性
前端·学习·typescript
小杨40440 分钟前
springboot框架项目实践应用十四(扩展sentinel错误提示)
spring boot·后端·spring cloud
陈大爷(有低保)1 小时前
Spring中都用到了哪些设计模式
java·后端·spring
程序员 小柴1 小时前
SpringCloud概述
后端·spring·spring cloud
uhakadotcom1 小时前
Caddy Web服务器初体验:简洁高效的现代选择
前端·面试·github
前端菜鸟来报道1 小时前
前端react 实现分段进度条
前端·javascript·react.js·进度条