From f50fc95e814fc2a5cb7b93147da123dddb1931f7 Mon Sep 17 00:00:00 2001 From: sanbuphy Date: Fri, 27 Feb 2026 18:46:27 +0800 Subject: [PATCH] docs: add stage-3 cross-platform sections - Add 3.8-pwa-local-app - Add 3.9-browser-ai-extension - Add 3.10-electron-voice-to-text - Add 3.11-nft-minting - Add 3.12-vscode-extension - Add 3.13-qt-industrial-hmi --- .../3.10-electron-voice-to-text/index.md | 501 ++++++++++ .../cross-platform/3.11-nft-minting/index.md | 361 +++++++ .../3.12-vscode-extension/index.md | 893 ++++++++++++++++++ .../3.13-qt-industrial-hmi/index.md | 694 ++++++++++++++ .../cross-platform/3.8-pwa-local-app/index.md | 369 ++++++++ .../3.9-browser-ai-extension/index.md | 478 ++++++++++ 6 files changed, 3296 insertions(+) create mode 100644 docs/zh-cn/stage-3/cross-platform/3.10-electron-voice-to-text/index.md create mode 100644 docs/zh-cn/stage-3/cross-platform/3.11-nft-minting/index.md create mode 100644 docs/zh-cn/stage-3/cross-platform/3.12-vscode-extension/index.md create mode 100644 docs/zh-cn/stage-3/cross-platform/3.13-qt-industrial-hmi/index.md create mode 100644 docs/zh-cn/stage-3/cross-platform/3.8-pwa-local-app/index.md create mode 100644 docs/zh-cn/stage-3/cross-platform/3.9-browser-ai-extension/index.md diff --git a/docs/zh-cn/stage-3/cross-platform/3.10-electron-voice-to-text/index.md b/docs/zh-cn/stage-3/cross-platform/3.10-electron-voice-to-text/index.md new file mode 100644 index 0000000..4a3b686 --- /dev/null +++ b/docs/zh-cn/stage-3/cross-platform/3.10-electron-voice-to-text/index.md @@ -0,0 +1,501 @@ +# 如何开发跨平台 Electron 桌面程序——语音转文字应用 + +# 第 1 章:什么是 Electron 和桌面应用开发 + +在这篇教程中,我们将完整跑通一条闭环:从零开始用 Electron 构建一个语音转文字的桌面应用,支持云端 API 和本地模型两种识别方式,最终打包成可以在 Windows、macOS、Linux 上安装运行的真实桌面程序。 + +本次教程,你至少需要具备: + +- 一台电脑(Windows 或 Mac,推荐 Mac,因为 Apple Silicon 跑本地模型非常快) +- Node.js 环境(18.0 以上版本) +- 你的 AI 编程助手(Cursor / Trae / Claude Code) +- (可选)OpenAI API Key(如果使用云端模式) +- 一个麦克风(笔记本自带的就行) + +## 1.1 什么是 Electron? + +你每天都在用的 **VS Code、Slack、Discord、Notion**,它们有一个共同点:都是用 **Electron** 构建的桌面应用。 + +Electron 是一个开源框架,它让你可以用 **HTML + CSS + JavaScript**(也就是做网页的那套技术)来构建 **Windows、macOS、Linux** 三个平台通用的桌面程序。它的原理很简单——把 Chromium 浏览器和 Node.js 打包在一起,你的网页就变成了一个独立的桌面 App。 + +**一句话理解**:Electron = 一个"隐形的 Chrome 浏览器" + Node.js 的系统能力。 + +![placeholder: 一张示意图,展示 Electron 的架构:Chromium(负责 UI 渲染)+ Node.js(负责系统访问)= 桌面应用](images/image1.png) + +## 1.2 Electron 的核心架构 + +Electron 应用由两种进程组成,理解它们是开发的关键: + +**主进程(Main Process)** + +* 相当于 App 的"总管" +* 负责创建窗口、管理应用生命周期、访问文件系统等原生能力 +* 运行在 Node.js 环境中,可以使用所有 Node.js 模块 +* 整个应用只有一个主进程 + +**渲染进程(Renderer Process)** + +* 相当于 App 的"门面" +* 就是一个 Chromium 网页,负责展示 UI +* 每个窗口对应一个渲染进程 +* 出于安全考虑,渲染进程不能直接访问 Node.js API + +**预加载脚本(Preload Script)** + +* 主进程和渲染进程之间的"桥梁" +* 通过 `contextBridge` 安全地暴露特定的 API 给渲染进程 + +它们之间通过 **IPC(进程间通信)** 来传递消息,就像打电话一样:渲染进程说"我要录音",主进程收到后去调用系统麦克风。 + +![placeholder: 一张 Electron 进程架构图,展示 Main Process、Renderer Process、Preload Script 之间的关系和 IPC 通信](images/image2.png) + +## 1.3 我们要做什么? + +在这篇教程中,我们将构建一个 **语音转文字(Speech-to-Text)** 桌面应用。它的功能很直观: + +1. 点击"开始录音"按钮,App 开始监听麦克风 +2. 说完话后点击"停止",App 将语音发送给 AI 进行识别 +3. 识别结果以文字形式展示在界面上,可以一键复制 + +**两种识别模式可选:** + +| 对比维度 | 云端 API 模式 | 本地模型模式 | +|---------|-------------|------------| +| 代表方案 | OpenAI Whisper API | whisper.cpp | +| 是否需要联网 | 是 | 否 | +| 识别速度 | 取决于网络 | 取决于硬件(Apple Silicon 上极快) | +| 中文识别质量 | 优秀 | 优秀(large-v3 模型) | +| 使用成本 | $0.006/分钟 | 免费 | +| 模型体积 | 无需下载 | tiny 模型 75MB,large 模型 3GB | +| 适合场景 | 快速上手、轻量使用 | 注重隐私、离线使用、长期高频使用 | + +![placeholder: 一张应用效果预览图,展示语音转文字应用的 UI:顶部有录音按钮和波形动画,下方是识别出的文字,右上角有模式切换开关](images/image3.png) + +## 1.4 重要提醒:Web Speech API 在 Electron 中不可用 + +如果你搜索过"Electron 语音识别",可能会看到有人推荐使用浏览器自带的 `Web Speech API`。**请注意:这个方案在 Electron 中行不通。** + +Google 已经关闭了对非 Chrome/Edge 浏览器壳的语音 API 支持。Electron 虽然基于 Chromium,但它不是 Chrome 本身,所以 `window.SpeechRecognition` 会直接报错。 + +这就是为什么我们需要使用 OpenAI Whisper API 或 whisper.cpp 这样的独立方案。 + +## 1.5 本教程的路线图 + +我们将按以下步骤完成整个流程: + +1. **创建 Electron 项目**:用 Electron Forge 搭建项目骨架,理解进程间通信 +2. **实现录音功能**:在渲染进程中捕获麦克风,处理音频数据 +3. **云端识别(方案 A)**:调用 OpenAI Whisper API 进行语音转文字 +4. **本地识别(方案 B)**:使用 whisper.cpp 在本地运行模型,无需联网 +5. **打包与分发**:将应用打包成可安装的桌面程序 + +# 第 2 章:创建 Electron 项目 + +## 2.1 用 AI 初始化项目 + +打开你的 AI 编程助手,在对话框中输入以下 Prompt: + +``` +请帮我使用 Electron Forge 创建一个新的 Electron 项目,使用 Vite 模板。 +项目名叫 voice-to-text。 +请执行:npx create-electron-app voice-to-text --template=vite +创建完成后进入项目目录并安装依赖。 +``` + +Electron Forge 是 Electron 官方推荐的脚手架工具,它帮你处理了项目初始化、打包、分发等繁琐的事情。 + +创建完成后,项目结构大致如下: + +``` +voice-to-text/ +├── src/ +│ ├── main.js # 主进程入口 +│ ├── preload.js # 预加载脚本(桥梁) +│ ├── renderer.js # 渲染进程入口 +│ └── index.html # 应用的 HTML 页面 +├── forge.config.js # Electron Forge 配置 +├── vite.main.config.mjs # 主进程 Vite 配置 +├── vite.preload.config.mjs # 预加载脚本 Vite 配置 +├── vite.renderer.config.mjs # 渲染进程 Vite 配置 +└── package.json +``` + +## 2.2 启动并预览 + +让 AI 帮你启动开发服务器: + +``` +请帮我启动 Electron 开发服务器,执行 npm start +``` + +几秒钟后,一个桌面窗口会弹出来——这就是你的 Electron 应用!虽然现在只有一个默认的欢迎页面,但它已经是一个真正的桌面程序了。 + +![placeholder: Electron 应用首次启动的截图,展示默认的欢迎页面窗口](images/image4.png) + +## 2.3 理解进程间通信(IPC) + +在开始写语音功能之前,我们需要理解 Electron 最核心的概念——**IPC(Inter-Process Communication,进程间通信)**。 + +因为渲染进程(UI 界面)和主进程(系统能力)是隔离的,它们之间需要通过 IPC "打电话"来协作: + +``` +渲染进程(UI) 主进程(系统) + │ │ + │── "我要开始录音" ──────────→ │ + │ │── 调用麦克风 + │ │── 处理音频 + │ ←──── "这是识别结果" ────────│ + │ │ + │── 显示文字到界面 │ +``` + +在代码中,这个通信通过 `preload.js` 来桥接: + +```javascript +// preload.js - 安全地暴露 API 给渲染进程 +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('electronAPI', { + // 渲染进程 → 主进程 + sendAudio: (audioData) => ipcRenderer.invoke('transcribe-audio', audioData), + // 主进程 → 渲染进程 + onResult: (callback) => ipcRenderer.on('transcription-result', callback) +}) +``` + +```javascript +// main.js - 主进程监听消息 +const { ipcMain } = require('electron') + +ipcMain.handle('transcribe-audio', async (event, audioData) => { + // 在这里调用 Whisper API 或 whisper.cpp + const text = await transcribe(audioData) + return text +}) +``` + +![placeholder: 一张 IPC 通信流程图,展示 Renderer → Preload → Main 的消息传递过程](images/image5.png) + +# 第 3 章:实现录音功能 + +## 3.1 在渲染进程中捕获麦克风 + +浏览器(也就是 Electron 的渲染进程)提供了 `navigator.mediaDevices.getUserMedia` API 来访问麦克风。让 AI 帮你实现录音功能: + +``` +请帮我修改 src/index.html 和 src/renderer.js,实现以下功能: + +界面设计: +1. 一个大的圆形 "开始录音" 按钮,点击后变成红色的 "停止录音" +2. 录音时显示一个简单的脉冲动画,表示正在录音 +3. 下方有一个文字显示区域,用于展示识别结果 +4. 底部有 "复制文字" 和 "清空" 两个按钮 +5. 右上角有一个设置图标,点击可以切换识别模式(云端/本地) + +录音逻辑(在 renderer.js 中): +1. 点击按钮后,使用 navigator.mediaDevices.getUserMedia 获取麦克风权限 +2. 使用 MediaRecorder 录制音频,格式为 webm +3. 停止录音后,将音频 Blob 转为 ArrayBuffer +4. 通过 window.electronAPI.sendAudio 发送给主进程 +5. 等待主进程返回识别结果并显示 +``` + +核心录音代码: + +```javascript +// renderer.js +let mediaRecorder = null +let audioChunks = [] + +async function startRecording() { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: { + channelCount: 1, + sampleRate: 16000, + echoCancellation: true, + noiseSuppression: true + } + }) + + mediaRecorder = new MediaRecorder(stream, { + mimeType: 'audio/webm;codecs=opus' + }) + + audioChunks = [] + mediaRecorder.ondataavailable = (e) => audioChunks.push(e.data) + + mediaRecorder.onstop = async () => { + const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }) + const arrayBuffer = await audioBlob.arrayBuffer() + + // 发送给主进程进行识别 + const result = await window.electronAPI.sendAudio(arrayBuffer) + document.getElementById('result').textContent = result + } + + mediaRecorder.start() +} +``` + +![placeholder: 应用录音界面的截图,展示录音按钮(录音中状态,红色脉冲动画)和下方的文字显示区域](images/image6.png) + +## 3.2 处理麦克风权限 + +Electron 默认会拦截权限请求。我们需要在主进程中明确允许麦克风访问: + +``` +请帮我在 main.js 中添加麦克风权限处理: +1. 使用 session.defaultSession.setPermissionRequestHandler 处理权限请求 +2. 当请求类型为 'media' 时,自动允许 +3. 对于 macOS,确保在 package.json 或 entitlements 中声明了麦克风使用说明 +``` + +```javascript +// main.js 中添加 +const { session } = require('electron') + +session.defaultSession.setPermissionRequestHandler( + (webContents, permission, callback) => { + if (permission === 'media') { + callback(true) + } else { + callback(false) + } + } +) +``` + +> **macOS 用户注意**:macOS 会弹出系统级的麦克风权限请求对话框,这是正常的,点击"允许"即可。 + +# 第 4 章:方案 A——云端识别(OpenAI Whisper API) + +这是最简单的方案,只需要一个 API Key 和几行代码。 + +## 4.1 获取 OpenAI API Key + +1. 访问 [OpenAI Platform](https://platform.openai.com/),注册并登录 +2. 进入 API Keys 页面,点击 **"Create new secret key"** +3. 复制生成的 Key(以 `sk-` 开头),妥善保存 + +> **费用参考**:Whisper API 的价格是 **$0.006/分钟**,也就是说识别 1 小时的语音只需要 $0.36(约 2.5 元人民币),非常便宜。 + +## 4.2 在主进程中调用 Whisper API + +让 AI 帮你在主进程中实现语音识别: + +``` +请帮我在 main.js 中实现 OpenAI Whisper API 的调用: +1. 安装 node-fetch(如果需要)或使用 Node.js 内置的 fetch +2. 创建 transcribeWithWhisper 函数,接收音频 ArrayBuffer +3. 将 ArrayBuffer 转为 Blob/File,构建 FormData +4. 调用 https://api.openai.com/v1/audio/transcriptions +5. 模型使用 whisper-1,语言设为 zh(中文) +6. 返回识别出的文字 +7. API Key 从环境变量或配置文件读取 +``` + +核心代码: + +```javascript +// main.js +async function transcribeWithWhisper(audioBuffer, apiKey) { + const blob = new Blob([audioBuffer], { type: 'audio/webm' }) + const formData = new FormData() + formData.append('file', blob, 'audio.webm') + formData.append('model', 'whisper-1') + formData.append('language', 'zh') + + const response = await fetch( + 'https://api.openai.com/v1/audio/transcriptions', + { + method: 'POST', + headers: { Authorization: `Bearer ${apiKey}` }, + body: formData + } + ) + + const data = await response.json() + return data.text +} +``` + +![placeholder: 应用运行截图,展示用户说了一段中文后,Whisper API 返回的识别结果](images/image7.png) + +## 4.3 添加设置界面 + +让 AI 帮你在渲染进程中添加一个简单的设置面板,用于输入 API Key 和切换识别模式: + +``` +请帮我在 index.html 中添加一个设置面板: +1. 右上角有一个齿轮图标,点击展开设置面板 +2. 设置面板包含: + - 识别模式切换(云端 API / 本地模型) + - API Key 输入框(仅云端模式显示) + - 语言选择下拉框(中文/英文/自动检测) +3. 设置保存到 localStorage +4. 面板可以点击外部区域关闭 +``` + +![placeholder: 设置面板展开的截图,展示模式切换开关和 API Key 输入框](images/image8.png) + +# 第 5 章:方案 B——本地识别(whisper.cpp) + +如果你不想依赖云端 API,或者需要离线使用,whisper.cpp 是最佳选择。它是 OpenAI Whisper 模型的 C++ 移植版本,可以完全在本地运行,不需要联网。 + +## 5.1 安装 whisper.cpp 的 Node.js 绑定 + +让 AI 帮你安装和配置: + +``` +请帮我在项目中安装 nodejs-whisper 包: +npm install nodejs-whisper + +安装完成后,请帮我下载 whisper 的 tiny 模型(用于测试,体积小速度快)。 +nodejs-whisper 会自动处理模型下载。 +``` + +> **模型选择指南**: +> * `tiny`(75MB):速度最快,适合测试和轻量使用,准确率一般 +> * `base`(142MB):速度和准确率的平衡点 +> * `small`(466MB):中文识别质量明显提升 +> * `large-v3-turbo`(1.5GB):推荐!速度是 large 的 5-8 倍,准确率仅差 1-2% +> * `large-v3`(3GB):最高准确率,但速度较慢,需要较好的硬件 + +## 5.2 在主进程中集成 whisper.cpp + +让 AI 帮你实现本地识别功能: + +``` +请帮我在 main.js 中添加 whisper.cpp 本地识别功能: +1. 引入 nodejs-whisper +2. 创建 transcribeWithLocal 函数 +3. 接收音频 ArrayBuffer,先保存为临时 WAV 文件(16kHz 单声道) +4. 调用 nodejs-whisper 进行识别 +5. 返回识别文字 +6. 识别完成后删除临时文件 +``` + +核心代码: + +```javascript +// main.js +const { nodewhisper } = require('nodejs-whisper') +const path = require('path') +const fs = require('fs') +const os = require('os') + +async function transcribeWithLocal(audioBuffer) { + // 保存为临时文件 + const tempPath = path.join(os.tmpdir(), `recording-${Date.now()}.wav`) + fs.writeFileSync(tempPath, Buffer.from(audioBuffer)) + + try { + const result = await nodewhisper(tempPath, { + modelName: 'base', + autoDownloadModelName: 'base', + whisperOptions: { + language: 'zh', + word_timestamps: true + } + }) + return result.map(r => r.speech).join('') + } finally { + // 清理临时文件 + fs.unlinkSync(tempPath) + } +} +``` + +![placeholder: 本地模型识别的运行截图,展示离线状态下依然能正常识别中文语音](images/image9.png) + +## 5.3 Apple Silicon 用户的福音 + +如果你使用的是 M1/M2/M3/M4 芯片的 Mac,whisper.cpp 会自动利用 **Metal GPU 加速** 和 **Apple Neural Engine**,识别速度可以达到 **比实时更快**——也就是说,1 分钟的语音可能只需要几秒钟就能识别完。 + +对于 NVIDIA 显卡用户,whisper.cpp 也支持 **CUDA 加速**,同样能获得很好的性能。 + +# 第 6 章:打包与分发 + +开发完成后,我们需要把应用打包成可以分发的安装包。 + +## 6.1 使用 Electron Forge 打包 + +Electron Forge 已经内置在我们的项目中,打包非常简单: + +``` +请帮我执行 Electron Forge 的打包命令: +npx electron-forge make +``` + +这个命令会根据你当前的操作系统自动生成对应的安装包: + +* **macOS**:生成 `.dmg` 安装镜像和 `.zip` 压缩包 +* **Windows**:生成 `.exe` 安装程序(Squirrel 格式) +* **Linux**:生成 `.deb`(Debian/Ubuntu)和 `.rpm`(Fedora)包 + +打包产物在 `out/make/` 目录下。 + +![placeholder: out/make 目录的文件列表截图,展示生成的 .dmg 或 .exe 安装包](images/image10.png) + +## 6.2 应用体积优化 + +Electron 应用的一个"痛点"是体积较大(因为打包了整个 Chromium)。一些优化建议: + +* 确保只有 `dependencies` 中的包会被打包,开发依赖放在 `devDependencies` +* 使用 Vite 的 tree-shaking 减少 JS 体积 +* 如果使用本地模型,考虑让用户首次启动时下载,而不是打包在安装包里 + +| 配置 | 预估体积 | +|------|---------| +| 纯 Electron 应用(无模型) | ~150-200 MB | +| + whisper tiny 模型 | ~250 MB | +| + whisper large-v3-turbo 模型 | ~1.7 GB | + +## 6.3 跨平台注意事项 + +**macOS:** +* 发布到 App Store 或分发给其他用户需要 **代码签名**(Apple Developer ID,$99/年) +* 还需要经过 Apple 的 **公证(Notarization)** 流程 +* 麦克风权限需要在 `Info.plist` 中声明 `NSMicrophoneUsageDescription` +* 建议构建 Universal Binary 以同时支持 Intel 和 Apple Silicon + +**Windows:** +* 建议进行代码签名,否则 Windows SmartScreen 会弹出安全警告 +* 用户仍然可以选择"仍要运行"来使用未签名的应用 + +**Linux:** +* 不需要代码签名 +* 推荐同时提供 `.deb` 和 `.AppImage` 格式 + +> **提示**:对于个人项目或小范围分发,可以暂时跳过代码签名,直接把打包好的文件发给朋友使用。 + +# 第 7 章:写在最后 + +恭喜你!你已经从零构建了一个跨平台的语音转文字桌面应用。回顾一下我们做了什么: + +1. 用 Electron Forge 搭建了跨平台桌面应用骨架 +2. 理解了主进程、渲染进程和 IPC 通信机制 +3. 实现了麦克风录音和音频捕获 +4. 集成了两种语音识别方案:云端 Whisper API 和本地 whisper.cpp +5. 学会了打包和分发 Electron 应用 + +Electron 的强大之处在于——你用做网页的技术栈,就能构建出 VS Code、Slack 这样级别的桌面应用。而 AI 语音识别技术的成熟,让"语音转文字"这个曾经需要专业团队才能做的功能,现在一个人就能搞定。 + +**进阶方向:** + +* **实时字幕**:使用 AudioWorklet 实现流式音频传输,配合支持流式识别的 API,实现边说边出字 +* **会议记录助手**:录制整场会议,自动生成带时间戳的文字记录,再用 AI 总结要点 +* **多语言翻译**:识别语音后,调用翻译 API 实时翻译成其他语言 +* **语音笔记本**:结合本地数据库(如 SQLite),构建一个可搜索的语音笔记应用 + +***用你的声音,让代码替你记录一切。*** + +# 参考文献 + +* [Electron 官方文档](https://www.electronjs.org/docs/latest/) +* [Electron Forge 官方文档](https://www.electronforge.io/) +* [OpenAI Whisper API 文档](https://platform.openai.com/docs/guides/speech-to-text) +* [whisper.cpp GitHub 仓库](https://github.com/ggml-org/whisper.cpp) +* [nodejs-whisper npm 包](https://www.npmjs.com/package/nodejs-whisper) +* [MDN MediaDevices.getUserMedia()](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) + diff --git a/docs/zh-cn/stage-3/cross-platform/3.11-nft-minting/index.md b/docs/zh-cn/stage-3/cross-platform/3.11-nft-minting/index.md new file mode 100644 index 0000000..ff73aec --- /dev/null +++ b/docs/zh-cn/stage-3/cross-platform/3.11-nft-minting/index.md @@ -0,0 +1,361 @@ +# 如何快速开发并铸造 NFT——10 分钟上手版 + +# 第 1 章:什么是 NFT 和智能合约 + +在这篇教程中,我们将完整跑通一条闭环:从零开始编写一个 NFT 智能合约,部署到以太坊测试网,铸造出属于你自己的 NFT,并在 OpenSea 上查看它。全程使用浏览器在线工具,不需要安装任何本地环境,10 分钟即可完成。 + +本次教程,你至少需要具备: + +- Chrome 浏览器(安装 MetaMask 钱包插件) +- 一个 MetaMask 钱包账户 +- 一点点 Sepolia 测试网 ETH(免费领取,下文会教你) + +> **零成本、零配置**:全程使用浏览器在线工具(Remix IDE),不用装 Node.js / Hardhat;代码直接用 OpenZeppelin 官方安全模板;铸造后能在 OpenSea 测试网看到自己的 NFT。 + +## 1.1 什么是 NFT? + +NFT(Non-Fungible Token,非同质化代币)是区块链上的一种数字资产。和比特币、以太币这些"同质化"代币不同,每一个 NFT 都是独一无二的——就像世界上没有两幅完全相同的画。 + +你可以把 NFT 理解为 **"数字世界的收藏证书"**。它可以代表: + +* 一幅数字画作的所有权 +* 一张活动门票 +* 一个游戏道具 +* 一份学习证书 +* 甚至一条推文 + +NFT 的核心价值在于:**它用区块链技术证明了"这个数字物品属于你",而且这个证明是公开透明、不可篡改的。** + +![placeholder: 一张示意图,展示 NFT 的概念:左边是一幅数字画作,右边是区块链上的所有权记录,中间用箭头连接](images/image1.png) + +## 1.2 什么是智能合约? + +智能合约(Smart Contract)是运行在区块链上的一段程序代码。你可以把它理解为 **"自动执行的合同"**——一旦部署到区块链上,它就会按照代码逻辑自动运行,任何人都无法篡改。 + +NFT 就是通过智能合约来创建和管理的。当你"铸造"(Mint)一个 NFT 时,实际上是调用了智能合约中的一个函数,让它在区块链上记录:"编号为 #0 的 NFT 属于你的钱包地址"。 + +我们将使用 **Solidity** 语言编写智能合约。别担心,借助 OpenZeppelin 提供的现成模板,你只需要写不到 15 行代码。 + +## 1.3 我们要铸造什么 NFT? + +我们将铸造一个 **"Vibe Coder 学习证书"** NFT——证明你完成了这篇教程,掌握了区块链开发的基础技能。这个 NFT 将: + +* 拥有独一无二的编号(Token ID) +* 记录在以太坊 Sepolia 测试网上 +* 可以在 OpenSea 测试网上查看和展示 +* (可选)附带一张你自定义的图片 + +当然,你也可以把它改成任何你喜欢的主题——一幅 AI 生成的画作、一张活动纪念卡、一个像素头像……NFT 的内容完全由你决定。 + +## 1.4 为什么用测试网? + +以太坊有"主网"和"测试网"之分: + +| 对比 | 主网(Mainnet) | 测试网(Sepolia) | +|------|----------------|------------------| +| ETH 价值 | 真金白银 | 免费领取,无真实价值 | +| 部署费用 | 需要花真钱(Gas 费) | 完全免费 | +| 适用场景 | 正式发布 | 学习、测试、开发 | +| 功能差异 | 无 | 与主网完全一致 | + +测试网和主网的功能完全一样,唯一的区别是测试网的 ETH 没有真实价值。所以我们可以放心地在测试网上学习和实验,不用担心花钱。 + +## 1.5 本教程的路线图 + +我们将按以下步骤完成整个流程: + +1. **准备钱包和测试币**(2 分钟):安装 MetaMask,领取免费测试 ETH +2. **编写并部署智能合约**(4 分钟):在 Remix IDE 中编写 NFT 合约并部署到 Sepolia +3. **铸造 NFT 并查看成果**(4 分钟):调用合约铸造 NFT,在 OpenSea 和 Etherscan 上验证 +4. **进阶:给 NFT 添加图片**(可选):使用 IPFS 存储图片,让 NFT 更完整 + +# 第 2 章:准备钱包和测试币(2 分钟) + +## 2.1 安装 MetaMask 钱包 + +MetaMask 是最流行的以太坊钱包,它是一个浏览器插件,让你可以和区块链上的应用交互。 + +1. 打开 Chrome 浏览器,访问 [MetaMask 官网](https://metamask.io/) +2. 点击 **"Download"**,安装 Chrome 插件 +3. 安装完成后,点击浏览器右上角的 MetaMask 狐狸图标 +4. 选择 **"创建新钱包"**,设置密码 +5. **重要**:妥善保存你的助记词(12 个英文单词)。测试网钱包丢了无所谓,但养成好习惯很重要 + +![placeholder: MetaMask 安装和创建钱包的截图流程:安装插件 → 创建钱包 → 设置密码 → 备份助记词](images/image2.png) + +## 2.2 切换到 Sepolia 测试网 + +MetaMask 默认连接的是以太坊主网。我们需要切换到 Sepolia 测试网: + +1. 点击 MetaMask 顶部的网络下拉菜单(默认显示"Ethereum Mainnet") +2. 点击 **"Show test networks"**(显示测试网络) +3. 选择 **"Sepolia test network"** + +如果没有看到 Sepolia 选项,点击 **"Add network"**,手动添加: + +| 配置项 | 值 | +|-------|-----| +| Network Name | Sepolia test network | +| RPC URL | `https://rpc.sepolia.org` | +| Chain ID | 11155111 | +| Currency Symbol | SepoliaETH | +| Block Explorer | `https://sepolia.etherscan.io` | + +![placeholder: MetaMask 切换到 Sepolia 测试网的截图,展示网络下拉菜单和 Sepolia 选项](images/image3.png) + +## 2.3 领取免费测试 ETH + +部署合约和铸造 NFT 都需要支付 Gas 费(交易手续费)。在测试网上,Gas 费用测试 ETH 支付,完全免费。 + +访问以下任一水龙头(Faucet)网站,输入你的钱包地址,即可领取免费的 Sepolia ETH: + +| 水龙头 | 地址 | 每次领取量 | 是否需要登录 | +|--------|------|-----------|------------| +| QuickNode | `https://faucet.quicknode.com/ethereum/sepolia` | 0.1 ETH | 需要 | +| Alchemy | `https://www.alchemy.com/faucets/ethereum-sepolia` | 0.1 ETH | 需要 | +| Google Cloud | `https://cloud.google.com/application/web3/faucet/ethereum/sepolia` | 0.05 ETH | 需要 Google 账号 | + +> **提示**:0.1 个测试 ETH 足够你部署合约 + 铸造几十个 NFT 了。如果一个水龙头领不到,换一个试试。 + +领取成功后,回到 MetaMask,你会看到余额从 0 变成了 0.1 ETH(可能需要等待几秒钟)。 + +![placeholder: 水龙头网站截图,展示输入钱包地址并领取测试 ETH 的过程](images/image4.png) + +# 第 3 章:编写并部署 NFT 智能合约(4 分钟) + +## 3.1 打开 Remix IDE + +Remix 是以太坊官方推荐的在线智能合约开发环境,完全在浏览器中运行,不需要安装任何东西。 + +打开浏览器,访问:**https://remix.ethereum.org/** + +你会看到一个类似 VS Code 的界面,左侧是文件管理器,中间是代码编辑器,右侧是编译和部署面板。 + +![placeholder: Remix IDE 首页截图,展示文件管理器、代码编辑器和右侧面板](images/image5.png) + +## 3.2 创建合约文件 + +1. 在左侧文件管理器中,点击 **"contracts"** 文件夹 +2. 点击上方的 **"+"** 按钮,新建文件 +3. 命名为 **`MySimpleNFT.sol`** +4. 粘贴以下代码: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +// 引入 OpenZeppelin 官方的 ERC721 安全模板 +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +// 最简 NFT 合约:只有名称、符号、铸造功能 +contract MySimpleNFT is ERC721 { + uint256 private _tokenId; + + // 初始化 NFT 集合的名称和符号 + constructor() ERC721("VibeCoder", "VIBE") {} + + // 铸造 NFT:调用就给当前地址发一个 + function mint() public { + _safeMint(msg.sender, _tokenId); + _tokenId++; + } +} +``` + +**代码解读(不到 15 行,每行都能看懂):** + +| 代码 | 含义 | +|------|------| +| `pragma solidity ^0.8.20` | 指定 Solidity 编译器版本 | +| `import "@openzeppelin/..."` | 引入 OpenZeppelin 的 ERC721 标准实现(经过安全审计的模板) | +| `contract MySimpleNFT is ERC721` | 创建一个继承 ERC721 标准的合约 | +| `ERC721("VibeCoder", "VIBE")` | NFT 集合名称为 "VibeCoder",符号为 "VIBE" | +| `_safeMint(msg.sender, _tokenId)` | 给调用者铸造一个新 NFT | +| `_tokenId++` | 每铸造一个,编号自动 +1 | + +> **ERC721 是什么?** 它是以太坊上 NFT 的标准协议,定义了 NFT 应该具备的基本功能(转账、查询所有者等)。OpenZeppelin 提供了经过安全审计的实现,我们直接继承就行,不用自己从零写。 + +![placeholder: Remix IDE 中粘贴合约代码的截图](images/image6.png) + +## 3.3 编译合约 + +1. 点击左侧面板的 **"Solidity Compiler"**(锤子图标) +2. 编译器版本选择 **0.8.20**(或更高的 0.8.x 版本) +3. 点击 **"Compile MySimpleNFT.sol"** +4. 看到绿色对勾 ✅ 表示编译成功 + +> 如果报错,检查 Solidity 版本是否匹配,以及 OpenZeppelin 的 import 路径是否正确。Remix 会自动从 npm 下载 OpenZeppelin 依赖。 + +![placeholder: Remix 编译成功的截图,展示绿色对勾和编译器版本选择](images/image7.png) + +## 3.4 部署合约到 Sepolia 测试网 + +1. 点击左侧面板的 **"Deploy & Run Transactions"**(以太坊图标) +2. **Environment** 选择 **"Injected Provider - MetaMask"** + - 这会自动连接你的 MetaMask 钱包 + - MetaMask 会弹出连接请求,点击 **"连接"** +3. 确认网络显示为 **Sepolia (11155111)** +4. Contract 下拉框选择 **MySimpleNFT** +5. 点击 **"Deploy"** 按钮 +6. MetaMask 弹出交易确认,点击 **"确认"**(Gas 费极低,测试网免费) + +等待几秒钟,部署成功后,下方 **"Deployed Contracts"** 区域会显示你的合约地址。**复制并保存这个地址**,后面查看 NFT 时需要用到。 + +![placeholder: Remix 部署合约的截图,展示 Environment 选择、MetaMask 连接确认、Deploy 按钮和部署成功后的合约地址](images/image8.png) + +# 第 4 章:铸造 NFT 并查看成果(4 分钟) + +## 4.1 铸造你的第一个 NFT + +部署成功后,在 Remix 下方的 **"Deployed Contracts"** 区域,你会看到合约的交互面板。 + +1. 展开合约面板,找到 **"mint"** 按钮(橙色) +2. 直接点击 **"mint"**(不需要输入任何参数) +3. MetaMask 弹出交易确认,点击 **"确认"** +4. 等待几秒钟,交易完成 + +恭喜!你刚刚铸造了编号为 #0 的 NFT,它现在属于你的钱包地址。 + +你可以继续点击 "mint" 铸造更多——每次铸造的 NFT 编号会自动递增(#1、#2、#3……)。 + +![placeholder: Remix 中点击 mint 按钮并在 MetaMask 中确认交易的截图](images/image9.png) + +## 4.2 验证铸造结果 + +**方式 1:在 Remix 中验证** + +在合约面板中,找到 **"balanceOf"** 函数(蓝色按钮),输入你的钱包地址,点击调用。如果返回 `1`(或你铸造的数量),说明铸造成功。 + +你也可以调用 **"ownerOf"** 函数,输入 `0`(Token ID),它会返回你的钱包地址——证明编号 #0 的 NFT 属于你。 + +**方式 2:在 Etherscan 上验证(推荐)** + +1. 打开 [Sepolia Etherscan](https://sepolia.etherscan.io/) +2. 在搜索框中粘贴你的**合约地址** +3. 你会看到合约的详情页面,包括所有交易记录 +4. 点击 **"Token Tracker"** 链接,可以看到你铸造的所有 NFT + +在 Etherscan 上,每一笔铸造交易都有完整的记录:谁铸造的、什么时候铸造的、Token ID 是多少——这就是区块链"公开透明、不可篡改"的魅力。 + +![placeholder: Sepolia Etherscan 上查看合约和 NFT 铸造记录的截图,展示交易列表和 Token Tracker](images/image10.png) + +# 第 5 章:进阶——给 NFT 添加图片(可选) + +目前我们铸造的 NFT 只有编号,没有图片和描述。要让 NFT 更完整,我们需要用到 **IPFS**(星际文件系统)来存储图片和元数据。 + +## 5.1 什么是 IPFS? + +IPFS 是一个去中心化的文件存储网络。和普通的云存储不同,IPFS 上的文件不依赖某一台服务器,而是分布在全球的节点上。这意味着: + +* 文件不会因为某台服务器宕机而丢失 +* 文件内容由哈希值唯一标识,无法被篡改 +* 非常适合存储 NFT 的图片和元数据 + +## 5.2 上传图片到 Pinata + +[Pinata](https://pinata.cloud/) 是最流行的 IPFS 存储服务,免费版提供 1GB 存储空间,足够我们使用。 + +1. 访问 https://pinata.cloud/,注册一个免费账号 +2. 登录后,点击 **"Upload"** → **"File"** +3. 选择你想作为 NFT 图片的文件(可以用 AI 生成一张,或者随便找一张图片) +4. 上传成功后,复制文件的 **CID**(类似 `QmXyz...` 的一串字符) + +你的图片地址就是:`ipfs://你的CID` + +![placeholder: Pinata 上传图片的截图,展示上传按钮和上传成功后的 CID](images/image11.png) + +## 5.3 创建元数据 JSON + +NFT 的元数据(Metadata)是一个 JSON 文件,描述了 NFT 的名称、描述和图片地址。创建一个 `metadata.json` 文件: + +```json +{ + "name": "Vibe Coder Certificate #0", + "description": "This NFT certifies that the holder has completed the NFT minting tutorial and entered the world of Web3.", + "image": "ipfs://你的图片CID", + "attributes": [ + { "trait_type": "Course", "value": "Easy Vibe" }, + { "trait_type": "Skill", "value": "Smart Contract" }, + { "trait_type": "Level", "value": "Beginner" } + ] +} +``` + +将 `metadata.json` 也上传到 Pinata,获得元数据的 CID。 + +## 5.4 升级合约以支持图片 + +要让 NFT 带上图片,我们需要稍微升级一下合约,加入 `tokenURI` 功能。回到 Remix,创建一个新文件 `MyNFTWithImage.sol`: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; + +contract MyNFTWithImage is ERC721, ERC721URIStorage { + uint256 private _tokenId; + + constructor() ERC721("VibeCoder", "VIBE") {} + + // 铸造时传入元数据地址 + function mint(string memory uri) public { + _safeMint(msg.sender, _tokenId); + _setTokenURI(_tokenId, uri); + _tokenId++; + } + + // 以下是 Solidity 要求的重写 + function tokenURI(uint256 tokenId) + public view override(ERC721, ERC721URIStorage) + returns (string memory) + { + return super.tokenURI(tokenId); + } + + function supportsInterface(bytes4 interfaceId) + public view override(ERC721, ERC721URIStorage) + returns (bool) + { + return super.supportsInterface(interfaceId); + } +} +``` + +部署后,调用 `mint` 时传入你的元数据地址(如 `ipfs://QmAbc.../metadata.json`),铸造出的 NFT 就会带上图片和描述了。 + +![placeholder: 在 Etherscan 上查看带图片的 NFT 详情截图](images/image12.png) + +# 第 6 章:写在最后 + +恭喜你!你已经从零完成了一次完整的 NFT 开发闭环。回顾一下我们做了什么: + +1. 理解了 NFT 和智能合约的基本概念 +2. 安装了 MetaMask 钱包并切换到 Sepolia 测试网 +3. 在 Remix IDE 中编写了不到 15 行的 NFT 智能合约 +4. 将合约部署到以太坊测试网 +5. 铸造了属于自己的 NFT,并在 Etherscan 上验证 +6. (可选)学会了用 IPFS 给 NFT 添加图片和元数据 + +整个过程没有安装任何本地环境,没有花一分钱,全程在浏览器中完成。这就是区块链开发的魅力——门槛比你想象的低得多。 + +**进阶方向:** + +* **使用 Hardhat / Foundry 本地开发**:当你的合约逻辑变复杂时,Remix 就不够用了。Hardhat 和 Foundry 是专业的本地开发框架,支持自动化测试、脚本部署、Gas 优化等 +* **添加白名单和铸造限制**:限制谁可以铸造、每人最多铸造几个、设置铸造价格等 +* **构建 Mint 前端页面**:用 React + ethers.js / viem 构建一个漂亮的铸造页面,让用户通过网页一键铸造 +* **探索 ERC1155 多版本 NFT**:ERC1155 允许同一个 Token ID 有多个副本,适合游戏道具、门票等场景 +* **部署到主网**:当你准备好了,把合约部署到以太坊主网(或 Polygon、Base 等 L2 链,Gas 费更低) + +***你的第一个 NFT 已经在链上了,区块链世界的大门已经打开。*** + +# 参考文献 + +* [OpenZeppelin ERC721 文档](https://docs.openzeppelin.com/contracts/5.x/erc721) +* [Remix IDE 官方文档](https://remix-ide.readthedocs.io/) +* [MetaMask 官方文档](https://docs.metamask.io/) +* [Solidity 官方文档](https://docs.soliditylang.org/) +* [Sepolia Etherscan](https://sepolia.etherscan.io/) +* [Pinata IPFS 存储服务](https://pinata.cloud/) +* [ERC721 标准规范(EIP-721)](https://eips.ethereum.org/EIPS/eip-721) diff --git a/docs/zh-cn/stage-3/cross-platform/3.12-vscode-extension/index.md b/docs/zh-cn/stage-3/cross-platform/3.12-vscode-extension/index.md new file mode 100644 index 0000000..7acb544 --- /dev/null +++ b/docs/zh-cn/stage-3/cross-platform/3.12-vscode-extension/index.md @@ -0,0 +1,893 @@ +# 如何开发 VS Code 插件——打造你的 AI 项目助手 + +# 第 1 章:什么是 VS Code 插件开发 + +在这篇教程中,我们将完整跑通一条闭环:从零开始开发一个 VS Code 插件,它能作为你的 AI 项目助手——内置项目模板一键生成、支持选中文件或代码段与 AI 对话、多文件问答梳理,还有自定义快捷键。你会亲手完成插件的开发、调试,并学会如何发布到 VS Code 插件市场。 + +本次教程,你至少需要具备: + +- Node.js 环境(18.0 以上版本) +- VS Code 编辑器(1.90 以上版本) +- 你的 AI 编程助手(Cursor / Trae / Claude Code) +- (可选)GitHub Copilot 订阅(用于调用 Language Model API) + +> **全程 Vibe Coding**:我们会用 AI 编程助手帮你生成大部分代码,你只需要理解核心概念和架构,然后用自然语言描述需求即可。 + +## 1.1 VS Code 插件能做什么? + +你每天都在用 VS Code 插件——Prettier 帮你格式化代码、GitLens 帮你看 Git 历史、GitHub Copilot 帮你写代码。这些插件本质上都是用 TypeScript/JavaScript 编写的程序,通过 VS Code 提供的 API 来扩展编辑器的功能。 + +VS Code 插件可以做的事情远比你想象的多: + +* **添加新的 UI 元素**:侧边栏面板、状态栏信息、Webview 自定义页面 +* **处理文件和代码**:读取、修改、创建文件,分析代码结构 +* **集成外部服务**:调用 API、连接数据库、对接 CI/CD +* **扩展编辑器能力**:自定义语言支持、代码补全、诊断提示 +* **接入 AI 能力**:通过 Chat Participant API 创建 AI 对话助手,通过 Language Model API 调用大模型 + +![placeholder: VS Code 插件生态示意图,展示插件可以扩展的各个区域:侧边栏、编辑器、状态栏、命令面板、Chat 面板](images/image1.png) + +## 1.2 VS Code 插件的核心架构 + +VS Code 插件运行在一个独立的 **Extension Host(插件宿主)** 进程中,和编辑器主进程隔离,这样即使插件崩溃也不会影响编辑器本身。 + +一个插件由以下几个核心部分组成: + +* **package.json(插件清单)**:插件的"身份证",声明插件的名称、入口文件、贡献点(commands、menus、keybindings 等) +* **extension.ts(入口文件)**:插件的"大脑",导出 `activate()` 和 `deactivate()` 两个函数 +* **Contribution Points(贡献点)**:在 package.json 中声明插件要"贡献"给 VS Code 的东西——命令、菜单项、快捷键、侧边栏视图等 +* **VS Code API**:VS Code 提供的一整套 TypeScript API,让你可以操作编辑器的方方面面 + +``` +VS Code 编辑器 + │ + ├── Extension Host(插件宿主进程) + │ ├── 你的插件 + │ │ ├── package.json → 声明"我能做什么" + │ │ ├── extension.ts → 实现"怎么做" + │ │ └── 其他模块 → 具体功能代码 + │ ├── 其他插件 A + │ └── 其他插件 B + │ + └── 编辑器主进程(UI 渲染) +``` + +![placeholder: VS Code 插件架构图,展示 Extension Host 进程与编辑器主进程的关系](images/image2.png) + +## 1.3 我们要做什么插件? + +我们将开发一个名为 **"AI Project Bot"** 的 VS Code 插件,它是你的 AI 项目助手,具备以下功能: + +| 功能 | 说明 | +|------|------| +| 项目模板 | 侧边栏展示项目模板列表,一键生成新项目骨架 | +| AI 对话 | 在 VS Code Chat 面板中创建 `@project-bot` 参与者,支持项目相关问答 | +| 文件/段落 Chat | 右键选中代码或文件,直接发送给 AI 分析、解释、重构 | +| 多文件问答 | 在资源管理器中多选文件,一键让 AI 梳理文件关系和逻辑 | +| 快捷键 | 自定义快捷键快速触发常用操作 | + +![placeholder: AI Project Bot 插件效果预览图,展示侧边栏模板列表、Chat 面板中的 @project-bot 对话、右键菜单](images/image3.png) + +## 1.4 本教程的路线图 + +我们将按以下步骤完成整个流程: + +1. **创建插件项目**(3 分钟):用脚手架生成项目骨架,理解核心文件 +2. **实现项目模板功能**(5 分钟):用 TreeView 在侧边栏展示模板,一键生成项目 +3. **实现 AI Chat 参与者**(5 分钟):用 Chat Participant API 创建 `@project-bot` +4. **实现文件/段落 Chat 和多文件问答**(5 分钟):右键菜单 + 多选文件 + AI 分析 +5. **添加快捷键和 UX 优化**(3 分钟):自定义快捷键、状态栏提示 +6. **发布到插件市场**(可选):打包并提交审核 + +# 第 2 章:创建插件项目(3 分钟) + +## 2.1 用脚手架生成项目 + +VS Code 官方提供了 Yeoman 脚手架工具来快速创建插件项目。让 AI 帮你执行: + +``` +请帮我安装 VS Code 插件开发脚手架并创建项目: +1. 安装 Yeoman 和 VS Code 插件生成器:npm install -g yo generator-code +2. 运行 yo code 生成项目,选择以下选项: + - 类型:New Extension (TypeScript) + - 名称:ai-project-bot + - 标识符:ai-project-bot + - 描述:AI 项目助手——模板生成、智能对话、多文件问答 + - 包管理器:npm +3. 进入项目目录并安装依赖 +``` + +生成后的项目结构: + +``` +ai-project-bot/ +├── .vscode/ +│ ├── launch.json # 调试配置(F5 启动调试) +│ └── tasks.json # 编译任务 +├── src/ +│ └── extension.ts # 插件入口文件 +├── package.json # 插件清单(最重要的文件) +├── tsconfig.json # TypeScript 配置 +└── vsc-extension-quickstart.md # 快速入门指南(可删除) +``` + +## 2.2 理解 package.json——插件的"身份证" + +`package.json` 是 VS Code 插件最核心的文件。除了常规的 npm 包信息外,它还有一个 `contributes` 字段,用来声明插件要"贡献"给 VS Code 的所有东西: + +```json +{ + "name": "ai-project-bot", + "displayName": "AI Project Bot", + "description": "AI 项目助手——模板生成、智能对话、多文件问答", + "version": "0.0.1", + "engines": { "vscode": "^1.90.0" }, + "activationEvents": [], + "main": "./out/extension.js", + "contributes": { + "commands": [], + "menus": {}, + "keybindings": [], + "viewsContainers": {}, + "views": {}, + "chatParticipants": [] + } +} +``` + +**关键字段解读:** + +| 字段 | 作用 | +|------|------| +| `engines.vscode` | 插件支持的最低 VS Code 版本 | +| `activationEvents` | 什么时候激活插件(留空表示按需激活) | +| `main` | 编译后的入口文件路径 | +| `contributes` | 插件贡献的所有功能(命令、菜单、快捷键、视图等) | + +![placeholder: package.json 文件在编辑器中的截图,高亮 contributes 字段](images/image4.png) + +## 2.3 理解 extension.ts——插件的"大脑" + +打开 `src/extension.ts`,你会看到两个核心函数: + +```typescript +import * as vscode from 'vscode' + +// 插件被激活时调用(第一次执行命令、打开特定文件等) +export function activate(context: vscode.ExtensionContext) { + console.log('AI Project Bot 已激活!') + + // 在这里注册命令、视图、Chat 参与者等 + const disposable = vscode.commands.registerCommand( + 'ai-project-bot.helloWorld', + () => { + vscode.window.showInformationMessage('Hello from AI Project Bot!') + } + ) + + context.subscriptions.push(disposable) +} + +// 插件被停用时调用(VS Code 关闭时) +export function deactivate() {} +``` + +**核心概念:** + +* `activate(context)`:插件的初始化函数,所有功能都在这里注册 +* `context.subscriptions`:一个"垃圾回收"数组,把注册的东西放进去,VS Code 会在插件停用时自动清理 +* `vscode.commands.registerCommand`:注册一个命令,用户可以通过命令面板(Ctrl+Shift+P)调用 + +## 2.4 启动调试 + +按 **F5** 键,VS Code 会打开一个新的 **Extension Development Host** 窗口——这是一个加载了你插件的全新 VS Code 实例。 + +在新窗口中按 **Ctrl+Shift+P**,输入 "Hello World",你会看到右下角弹出一条消息。这说明你的插件已经在运行了。 + +![placeholder: VS Code 调试插件的截图,展示 Extension Development Host 窗口和 Hello World 消息](images/image5.png) + +> **调试技巧**:修改代码后,在 Extension Development Host 窗口中按 **Ctrl+Shift+P** → **"Developer: Reload Window"** 即可重新加载插件,不需要重启调试。 + +# 第 3 章:实现项目模板功能(5 分钟) + +## 3.1 设计模板系统 + +我们要在 VS Code 侧边栏中添加一个"项目模板"面板,用户可以浏览模板列表,点击后一键生成项目骨架。这需要用到 VS Code 的 **TreeView API**。 + +让 AI 帮你实现: + +``` +请帮我在 ai-project-bot 插件中实现项目模板功能: + +1. 在 package.json 中添加以下贡献点: + - 一个新的 viewsContainers.activitybar 项,id 为 "project-bot",标题 "AI Project Bot" + - 在该容器下添加一个 view,id 为 "projectTemplates",名称 "项目模板" + - 添加命令 "ai-project-bot.createFromTemplate",标题 "从模板创建项目" + +2. 创建 src/templates/templateProvider.ts: + - 实现 TreeDataProvider,提供以下模板分类和模板: + - 前端:React + TypeScript、Vue 3 + TypeScript、Next.js App + - 后端:Express API、FastAPI Python + - 全栈:T3 Stack(Next.js + tRPC + Prisma) + - 每个模板项显示名称、描述和图标 + +3. 创建 src/templates/scaffolder.ts: + - 实现 createProjectFromTemplate 函数 + - 让用户选择目标文件夹 + - 根据模板类型生成对应的项目文件结构 +``` + +## 3.2 在 package.json 中声明视图 + +首先在 `package.json` 的 `contributes` 中添加侧边栏视图: + +```json +{ + "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "project-bot", + "title": "AI Project Bot", + "icon": "resources/bot-icon.svg" + } + ] + }, + "views": { + "project-bot": [ + { + "id": "projectTemplates", + "name": "项目模板" + } + ] + }, + "commands": [ + { + "command": "ai-project-bot.createFromTemplate", + "title": "从模板创建项目", + "icon": "$(add)" + } + ], + "menus": { + "view/title": [ + { + "command": "ai-project-bot.createFromTemplate", + "when": "view == projectTemplates", + "group": "navigation" + } + ] + } + } +} +``` + +这段配置做了三件事: + +1. 在左侧活动栏添加了一个 "AI Project Bot" 图标入口 +2. 在该入口下创建了一个 "项目模板" 视图 +3. 在视图标题栏添加了一个 "+" 按钮用于创建项目 + +![placeholder: VS Code 侧边栏中显示 AI Project Bot 图标和项目模板列表的截图](images/image6.png) + +## 3.3 实现 TreeDataProvider + +TreeDataProvider 是 VS Code 用来填充树形视图数据的接口。我们需要实现两个方法:`getTreeItem`(返回单个节点的显示信息)和 `getChildren`(返回子节点列表)。 + +核心代码: + +```typescript +// src/templates/templateProvider.ts +import * as vscode from 'vscode' + +interface Template { + name: string + description: string + category: string + command: string // 用于生成项目的命令,如 "npx create-react-app" +} + +const TEMPLATES: Template[] = [ + { name: 'React + TypeScript', description: '使用 Vite 构建的 React 项目', category: '前端', command: 'npm create vite@latest {{name}} -- --template react-ts' }, + { name: 'Vue 3 + TypeScript', description: '使用 Vite 构建的 Vue 3 项目', category: '前端', command: 'npm create vite@latest {{name}} -- --template vue-ts' }, + { name: 'Next.js App', description: 'Next.js App Router 全栈项目', category: '前端', command: 'npx create-next-app@latest {{name}} --typescript --app' }, + { name: 'Express API', description: 'Express + TypeScript REST API', category: '后端', command: 'npx create-express-api {{name}}' }, + { name: 'FastAPI Python', description: 'Python FastAPI 后端项目', category: '后端', command: 'pip install fastapi uvicorn' }, +] + +// 树节点:分类或模板 +class TemplateItem extends vscode.TreeItem { + constructor( + public readonly label: string, + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + public readonly template?: Template + ) { + super(label, collapsibleState) + if (template) { + this.description = template.description + this.tooltip = `${template.name}\n${template.description}\n命令: ${template.command}` + this.contextValue = 'template' + this.command = { + command: 'ai-project-bot.createFromTemplate', + title: '创建项目', + arguments: [template] + } + } + } +} + +export class TemplateProvider implements vscode.TreeDataProvider { + getTreeItem(element: TemplateItem): vscode.TreeItem { + return element + } + + getChildren(element?: TemplateItem): TemplateItem[] { + if (!element) { + // 根节点:返回分类列表 + const categories = [...new Set(TEMPLATES.map(t => t.category))] + return categories.map( + cat => new TemplateItem(cat, vscode.TreeItemCollapsibleState.Expanded) + ) + } + // 子节点:返回该分类下的模板 + return TEMPLATES + .filter(t => t.category === element.label) + .map(t => new TemplateItem(t.name, vscode.TreeItemCollapsibleState.None, t)) + } +} +``` + +## 3.4 注册视图和创建命令 + +在 `extension.ts` 中注册 TreeView 和创建项目的命令: + +```typescript +// src/extension.ts +import { TemplateProvider } from './templates/templateProvider' + +export function activate(context: vscode.ExtensionContext) { + // 注册模板视图 + const templateProvider = new TemplateProvider() + vscode.window.registerTreeDataProvider('projectTemplates', templateProvider) + + // 注册创建项目命令 + const createCmd = vscode.commands.registerCommand( + 'ai-project-bot.createFromTemplate', + async (template) => { + if (!template) { + // 如果没有传入模板(从命令面板调用),让用户选择 + const pick = await vscode.window.showQuickPick( + TEMPLATES.map(t => ({ label: t.name, description: t.description, template: t })), + { placeHolder: '选择一个项目模板' } + ) + if (!pick) return + template = pick.template + } + + // 让用户输入项目名称 + const name = await vscode.window.showInputBox({ + prompt: '输入项目名称', + placeHolder: 'my-awesome-project' + }) + if (!name) return + + // 让用户选择目标文件夹 + const folder = await vscode.window.showOpenDialog({ + canSelectFolders: true, + openLabel: '选择项目存放位置' + }) + if (!folder) return + + // 执行创建命令 + const terminal = vscode.window.createTerminal('AI Project Bot') + terminal.show() + const cmd = template.command.replace('{{name}}', name) + terminal.sendText(`cd "${folder[0].fsPath}" && ${cmd}`) + + vscode.window.showInformationMessage(`正在创建 ${template.name} 项目: ${name}`) + } + ) + + context.subscriptions.push(createCmd) +} +``` + +现在按 F5 调试,你会在左侧活动栏看到 AI Project Bot 图标,点击后展开模板列表,点击任意模板即可创建项目。 + +![placeholder: 点击模板后弹出项目名称输入框和文件夹选择对话框的截图](images/image7.png) + +# 第 4 章:实现 AI Chat 参与者(5 分钟) + +## 4.1 什么是 Chat Participant API? + +从 VS Code 1.90 开始,插件可以通过 **Chat Participant API** 在 VS Code 的 Chat 面板中创建自己的 AI 助手。用户在聊天框中输入 `@project-bot 帮我分析这个项目的架构`,你的插件就会收到这条消息并返回 AI 生成的回复。 + +Chat Participant API 的核心概念: + +* **Participant(参与者)**:你的 AI 助手在 Chat 面板中的身份,用 `@名称` 来调用 +* **Slash Commands(斜杠命令)**:参与者支持的快捷指令,如 `/explain`、`/refactor` +* **Language Model API**:调用 VS Code 内置的大模型(如 Copilot 的 GPT-4o)来生成回复 +* **Stream(流式响应)**:通过 `stream.markdown()` 逐步输出回复内容 + +## 4.2 在 package.json 中声明 Chat 参与者 + +在 `contributes` 中添加: + +```json +{ + "contributes": { + "chatParticipants": [ + { + "id": "ai-project-bot.projectBot", + "name": "project-bot", + "fullName": "AI Project Bot", + "description": "你的 AI 项目助手,帮你分析代码、解释架构、生成方案", + "isSticky": true + } + ] + } +} +``` + +`isSticky: true` 表示用户选择这个参与者后,后续消息会默认发给它,不需要每次都输入 `@project-bot`。 + +## 4.3 实现 Chat 参与者处理函数 + +让 AI 帮你编写核心逻辑: + +``` +请帮我创建 src/chat/chatParticipant.ts,实现 Chat Participant: +1. 注册 "ai-project-bot.projectBot" 参与者 +2. 支持三个斜杠命令: + - /explain:解释选中的代码或当前文件 + - /refactor:给出重构建议 + - /template:推荐适合当前项目的技术栈模板 +3. 使用 Language Model API 调用 VS Code 内置模型生成回复 +4. 回复使用流式输出(stream.markdown) +``` + +核心代码: + +```typescript +// src/chat/chatParticipant.ts +import * as vscode from 'vscode' + +export function registerChatParticipant(context: vscode.ExtensionContext) { + const participant = vscode.chat.createChatParticipant( + 'ai-project-bot.projectBot', + async (request, chatContext, stream, token) => { + // 获取可用的语言模型 + const models = await vscode.lm.selectChatModels({ family: 'gpt-4o' }) + const model = models[0] + + if (!model) { + stream.markdown('未找到可用的语言模型,请确保已安装 GitHub Copilot。') + return + } + + // 根据斜杠命令构建不同的系统提示 + let systemPrompt = '你是一个专业的项目开发助手。' + + if (request.command === 'explain') { + systemPrompt = '你是一个代码解释专家。请用简洁易懂的中文解释用户提供的代码,包括功能、逻辑流程和关键设计决策。' + } else if (request.command === 'refactor') { + systemPrompt = '你是一个代码重构专家。请分析用户提供的代码,给出具体的重构建议和改进后的代码示例。' + } else if (request.command === 'template') { + systemPrompt = '你是一个技术选型专家。根据用户描述的项目需求,推荐最合适的技术栈和项目模板。' + } + + // 构建消息 + const messages = [ + vscode.LanguageModelChatMessage.User(systemPrompt), + vscode.LanguageModelChatMessage.User(request.prompt) + ] + + // 流式输出回复 + const response = await model.sendRequest(messages, {}, token) + for await (const chunk of response.stream) { + stream.markdown(chunk) + } + + return { metadata: { command: request.command || '' } } + } + ) + + // 注册斜杠命令 + participant.slashCommandProvider = { + provideSlashCommands: () => [ + { name: 'explain', description: '解释代码的功能和逻辑' }, + { name: 'refactor', description: '给出重构建议和改进方案' }, + { name: 'template', description: '推荐适合的项目模板和技术栈' } + ] + } + + // 注册跟进建议 + participant.followupProvider = { + provideFollowups: (result) => { + if (result.metadata?.command === 'explain') { + return [ + { prompt: '能画一个流程图吗?', label: '生成流程图' }, + { prompt: '有什么潜在的 bug 吗?', label: '检查潜在问题' } + ] + } + return [] + } + } + + context.subscriptions.push(participant) +} +``` + +在 `extension.ts` 中调用注册函数: + +```typescript +import { registerChatParticipant } from './chat/chatParticipant' + +export function activate(context: vscode.ExtensionContext) { + // ... 之前的模板注册代码 ... + registerChatParticipant(context) +} +``` + +现在在 Chat 面板中输入 `@project-bot /explain 这段代码是做什么的?`,你的插件就会调用大模型生成解释。 + +![placeholder: VS Code Chat 面板中 @project-bot 对话的截图,展示 /explain 命令的使用和流式回复](images/image8.png) + +# 第 5 章:文件/段落 Chat 与多文件问答(5 分钟) + +## 5.1 右键菜单:选中代码发送给 AI + +我们希望用户在编辑器中选中一段代码,右键就能把它发送给 AI 分析。这需要用到 VS Code 的 **Context Menu(右键菜单)** 贡献点。 + +在 `package.json` 中添加: + +```json +{ + "contributes": { + "commands": [ + { + "command": "ai-project-bot.explainSelection", + "title": "AI: 解释选中代码" + }, + { + "command": "ai-project-bot.refactorSelection", + "title": "AI: 重构选中代码" + } + ], + "menus": { + "editor/context": [ + { + "command": "ai-project-bot.explainSelection", + "when": "editorHasSelection", + "group": "ai-project-bot@1" + }, + { + "command": "ai-project-bot.refactorSelection", + "when": "editorHasSelection", + "group": "ai-project-bot@2" + } + ] + } + } +} +``` + +**关键配置解读:** + +* `when: "editorHasSelection"`:只有选中了代码时才显示这些菜单项 +* `group: "ai-project-bot@1"`:菜单项分组,`@1` 和 `@2` 控制排序 + +## 5.2 实现选中代码分析 + +```typescript +// src/commands/selectionCommands.ts +import * as vscode from 'vscode' + +export function registerSelectionCommands(context: vscode.ExtensionContext) { + // 解释选中代码 + const explainCmd = vscode.commands.registerCommand( + 'ai-project-bot.explainSelection', + async () => { + const editor = vscode.window.activeTextEditor + if (!editor) return + + const selection = editor.selection + const selectedText = editor.document.getText(selection) + const fileName = editor.document.fileName.split('/').pop() + const startLine = selection.start.line + 1 + const endLine = selection.end.line + 1 + + // 构建带上下文的提示 + const prompt = [ + `请解释以下代码(来自 ${fileName},第 ${startLine}-${endLine} 行):`, + '```', + selectedText, + '```', + '请说明:1. 这段代码的功能 2. 核心逻辑 3. 可能的改进点' + ].join('\n') + + // 调用 Language Model API + const models = await vscode.lm.selectChatModels({ family: 'gpt-4o' }) + if (!models.length) { + vscode.window.showErrorMessage('未找到可用的语言模型') + return + } + + // 在输出面板中显示结果 + const outputChannel = vscode.window.createOutputChannel('AI Project Bot') + outputChannel.show() + outputChannel.appendLine(`\n--- 代码解释 (${fileName}:${startLine}-${endLine}) ---\n`) + + const messages = [ + vscode.LanguageModelChatMessage.User(prompt) + ] + const response = await models[0].sendRequest(messages, {}) + for await (const chunk of response.stream) { + outputChannel.append(chunk) + } + } + ) + + context.subscriptions.push(explainCmd) +} +``` + +![placeholder: 编辑器中右键选中代码后显示 AI 菜单项的截图](images/image9.png) + +## 5.3 多文件问答:批量分析文件关系 + +这是我们插件最强大的功能之一——在资源管理器中多选文件,一键让 AI 梳理它们之间的关系和逻辑。 + +在 `package.json` 中添加资源管理器右键菜单: + +```json +{ + "contributes": { + "commands": [ + { + "command": "ai-project-bot.analyzeFiles", + "title": "AI: 分析选中文件的关系" + } + ], + "menus": { + "explorer/context": [ + { + "command": "ai-project-bot.analyzeFiles", + "when": "explorerResourceIsFile", + "group": "ai-project-bot" + } + ] + } + } +} +``` + +实现多文件分析命令: + +```typescript +// src/commands/multiFileAnalysis.ts +import * as vscode from 'vscode' + +export function registerMultiFileCommands(context: vscode.ExtensionContext) { + const analyzeCmd = vscode.commands.registerCommand( + 'ai-project-bot.analyzeFiles', + async (clickedFile: vscode.Uri, selectedFiles: vscode.Uri[]) => { + // selectedFiles 包含所有被选中的文件 + const files = selectedFiles || [clickedFile] + + if (files.length < 2) { + vscode.window.showWarningMessage('请至少选择 2 个文件进行分析') + return + } + + // 读取所有选中文件的内容 + const fileContents: string[] = [] + for (const file of files) { + const content = await vscode.workspace.fs.readFile(file) + const fileName = vscode.workspace.asRelativePath(file) + fileContents.push( + `--- ${fileName} ---\n${Buffer.from(content).toString('utf8')}` + ) + } + + const prompt = [ + `请分析以下 ${files.length} 个文件之间的关系:`, + '', + ...fileContents, + '', + '请说明:', + '1. 这些文件各自的职责', + '2. 它们之间的依赖和调用关系', + '3. 数据流向(如果有的话)', + '4. 架构上的建议或潜在问题' + ].join('\n') + + // 调用模型并在 Webview 中展示结果 + const models = await vscode.lm.selectChatModels({ family: 'gpt-4o' }) + if (!models.length) { + vscode.window.showErrorMessage('未找到可用的语言模型') + return + } + + const outputChannel = vscode.window.createOutputChannel('AI Project Bot') + outputChannel.show() + outputChannel.appendLine( + `\n--- 多文件分析 (${files.length} 个文件) ---\n` + ) + + const messages = [ + vscode.LanguageModelChatMessage.User(prompt) + ] + const response = await models[0].sendRequest(messages, {}) + for await (const chunk of response.stream) { + outputChannel.append(chunk) + } + } + ) + + context.subscriptions.push(analyzeCmd) +} +``` + +使用方式:在资源管理器中按住 Ctrl(Mac 上是 Cmd)多选文件,右键选择 "AI: 分析选中文件的关系",AI 就会读取所有文件内容并给出分析报告。 + +![placeholder: 资源管理器中多选文件后右键菜单显示 AI 分析选项的截图](images/image10.png) + +# 第 6 章:快捷键与 UX 优化(3 分钟) + +## 6.1 自定义快捷键 + +快捷键是提升效率的关键。在 `package.json` 中添加: + +```json +{ + "contributes": { + "keybindings": [ + { + "command": "ai-project-bot.explainSelection", + "key": "ctrl+shift+e", + "mac": "cmd+shift+e", + "when": "editorTextFocus && editorHasSelection" + }, + { + "command": "ai-project-bot.refactorSelection", + "key": "ctrl+shift+r", + "mac": "cmd+shift+r", + "when": "editorTextFocus && editorHasSelection" + }, + { + "command": "ai-project-bot.createFromTemplate", + "key": "ctrl+shift+n", + "mac": "cmd+shift+n", + "when": "" + } + ] + } +} +``` + +**when 条件解读:** + +| 条件 | 含义 | +|------|------| +| `editorTextFocus` | 光标在编辑器中 | +| `editorHasSelection` | 有选中的文本 | +| `explorerViewletVisible` | 资源管理器面板可见 | +| `!editorReadonly` | 文件不是只读的 | + +多个条件用 `&&` 连接表示"同时满足"。 + +## 6.2 状态栏提示 + +在状态栏添加一个快捷入口,让用户随时知道插件在运行: + +```typescript +// src/statusBar.ts +import * as vscode from 'vscode' + +export function createStatusBarItem(context: vscode.ExtensionContext) { + const statusBar = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 100 + ) + statusBar.text = '$(hubot) AI Bot' + statusBar.tooltip = '点击打开 AI Project Bot' + statusBar.command = 'ai-project-bot.createFromTemplate' + statusBar.show() + + context.subscriptions.push(statusBar) +} +``` + +`$(hubot)` 是 VS Code 内置的图标语法,你可以在 [Codicon 图标库](https://microsoft.github.io/vscode-codicons/dist/codicon.html) 中找到所有可用图标。 + +![placeholder: VS Code 状态栏中显示 AI Bot 图标的截图](images/image11.png) + +# 第 7 章:发布到插件市场(可选) + +## 7.1 发布准备 + +VS Code 插件通过 **vsce**(Visual Studio Code Extensions)工具打包和发布。 + +``` +请帮我安装 vsce 工具:npm install -g @vscode/vsce +``` + +发布前需要准备: + +1. **Azure DevOps 账号**:访问 [dev.azure.com](https://dev.azure.com/),注册并创建一个组织 +2. **Personal Access Token(PAT)**:在 Azure DevOps 中创建一个 PAT,权限选择 **Marketplace → Manage** +3. **Publisher ID**:在 [VS Code Marketplace](https://marketplace.visualstudio.com/manage) 中创建一个发布者身份 + +## 7.2 完善 package.json + +发布前需要补充一些元信息: + +```json +{ + "publisher": "your-publisher-id", + "repository": { + "type": "git", + "url": "https://github.com/yourname/ai-project-bot" + }, + "categories": ["AI", "Other"], + "keywords": ["ai", "project", "template", "chat"], + "icon": "resources/icon.png", + "galleryBanner": { + "color": "#1e1e2e", + "theme": "dark" + } +} +``` + +还需要创建一个 `README.md` 作为插件在市场中的介绍页面,以及一个 `CHANGELOG.md` 记录版本变更。 + +## 7.3 打包与发布 + +```bash +# 打包为 .vsix 文件(可以手动安装) +vsce package + +# 发布到市场 +vsce publish +``` + +打包后会生成一个 `ai-project-bot-0.0.1.vsix` 文件。你可以把这个文件发给朋友,他们通过 VS Code 的 "Install from VSIX" 就能安装。 + +如果要正式发布到市场,执行 `vsce publish` 后,插件会在几分钟内出现在 VS Code 插件市场中。 + +![placeholder: VS Code 插件市场中 AI Project Bot 插件页面的截图](images/image12.png) + +> **提示**:首次发布可能需要等待审核。确保你的 README 描述清晰、截图完整,审核通过会更快。 + +# 第 8 章:写在最后 + +恭喜你!你已经从零构建了一个功能完整的 VS Code 插件。回顾一下我们做了什么: + +1. 用 Yeoman 脚手架创建了插件项目,理解了 package.json 和 extension.ts 的核心作用 +2. 用 TreeView API 实现了侧边栏项目模板列表,一键生成新项目 +3. 用 Chat Participant API 创建了 `@project-bot` AI 助手,支持斜杠命令和流式回复 +4. 用右键菜单实现了选中代码发送给 AI 分析的功能 +5. 用多文件选择实现了批量文件关系梳理 +6. 添加了自定义快捷键和状态栏提示 + +VS Code 插件开发的想象空间非常大——你每天使用的那些好用的插件,背后的技术和你刚刚学到的完全一样。 + +**进阶方向:** + +* **Webview 自定义面板**:用 HTML/CSS/JS 构建完全自定义的 UI 面板,比如可视化的项目架构图、交互式的代码审查界面 +* **Language Model Tools**:注册自定义工具让 AI 能调用你的函数,比如查询数据库、执行 API 请求 +* **代码诊断和 CodeLens**:在代码中内联显示 AI 建议、性能提示、安全警告 +* **自定义语言支持**:为特定的 DSL 或配置文件提供语法高亮、自动补全、错误检查 +* **远程开发集成**:让插件在 SSH 远程环境、容器、WSL 中也能正常工作 + +***你的编辑器,你做主。*** + +# 参考文献 + +* [VS Code Extension API 官方文档](https://code.visualstudio.com/api) +* [Chat Participant API 指南](https://code.visualstudio.com/api/extension-guides/chat) +* [Language Model API 指南](https://code.visualstudio.com/api/extension-guides/language-model) +* [TreeView API 指南](https://code.visualstudio.com/api/extension-guides/tree-view) +* [Webview API 指南](https://code.visualstudio.com/api/extension-guides/webview) +* [VS Code 插件发布指南](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) +* [Codicon 图标库](https://microsoft.github.io/vscode-codicons/dist/codicon.html) diff --git a/docs/zh-cn/stage-3/cross-platform/3.13-qt-industrial-hmi/index.md b/docs/zh-cn/stage-3/cross-platform/3.13-qt-industrial-hmi/index.md new file mode 100644 index 0000000..cf1cefc --- /dev/null +++ b/docs/zh-cn/stage-3/cross-platform/3.13-qt-industrial-hmi/index.md @@ -0,0 +1,694 @@ +# 如何开发工业级 Qt 桌面应用——水泵监控 HMI 系统 + +# 第 1 章:什么是工业 HMI 和 Qt 开发 + +在这篇教程中,我们将完整跑通一条闭环:从零开始用 Qt 构建一个工业级的水泵监控 HMI(人机界面)系统,能实时读取传感器数据、绘制压力趋势图、超阈值自动报警、记录故障日志。全程使用 PC 上的免费模拟软件代替真实工控设备,不需要买任何硬件。 + +本次教程,你至少需要具备: + +- 一台电脑(Windows 或 Mac 均可,推荐 Windows,工控软件兼容性更好) +- Qt 6.5 开发环境(Qt Creator + Qt Serial Bus + Qt Charts 模块) +- Modbus Slave 模拟软件(免费下载,充当"虚拟水泵") +- 你的 AI 编程助手(Cursor / Trae / Claude Code) + +> **零硬件、零成本**:全程用 PC 上的免费模拟软件(Modbus Slave)模拟下位机,不用买任何工控设备;代码直接用 Qt 官方的 QModbusTcpClient + Qt Charts 模块,不用手写协议解析;运行后能看到实时压力趋势图、超阈值弹窗报警、故障日志记录,和真实工厂现场效果一致。 + +## 1.1 什么是上位机和下位机? + +在工业自动化领域,有两个你必须理解的概念:**上位机**和**下位机**。 + +**下位机(Lower Computer)**——现场的"手和脚" + +下位机是直接和物理设备打交道的控制器。在工厂里,它通常是 **PLC(可编程逻辑控制器)** 或 **传感器**,负责: + +* 读取现场数据(温度、压力、流量、液位……) +* 控制设备动作(启动水泵、关闭阀门、调节转速……) +* 按照预设逻辑自动运行(压力超标就停泵) + +你可以把下位机理解为工厂里的"工人"——它不需要思考太多,但必须可靠地执行任务。 + +**上位机(Upper Computer)**——控制室的"眼睛和大脑" + +上位机是运行在 PC 或工控机上的监控软件,也就是我们今天要开发的 **HMI(Human-Machine Interface,人机界面)**。它负责: + +* 实时显示现场数据(数字、图表、动画) +* 记录历史数据和报警日志 +* 让操作员远程控制设备 +* 提供数据分析和报表 + +你可以把上位机理解为工厂的"监控中心"——操作员坐在屏幕前,就能掌握整个工厂的运行状态。 + +**它们之间怎么通信?** + +上位机和下位机之间通过 **工业通信协议** 交换数据。最常用的协议就是 **Modbus**——一个诞生于 1979 年的"老前辈",至今仍是工业领域使用最广泛的协议,因为它简单、可靠、几乎所有工控设备都支持。 + +``` +控制室 工厂现场 +┌──────────┐ Modbus 协议 ┌──────────┐ +│ 上位机 │ ◄──────────────► │ 下位机 │ +│ (Qt HMI) │ "请告诉我压力" │ (PLC/传感器)│ +│ │ "压力是 1.20MPa" │ │ +│ 显示数据 │ │ 读取传感器 │ +│ 记录日志 │ │ 控制水泵 │ +│ 报警提示 │ │ 自动保护 │ +└──────────┘ └──────────┘ +``` + +![placeholder: 上位机和下位机的关系示意图,左边是控制室的 PC 屏幕(上位机),右边是工厂现场的 PLC 和水泵(下位机),中间用 Modbus 协议连接](images/image1.png) + +## 1.2 什么是 Modbus 协议? + +Modbus 是工业通信的"普通话"。它定义了上位机和下位机之间"怎么说话"的规则。 + +**核心概念只有两个:** + +* **寄存器(Register)**:下位机中存储数据的"格子"。每个格子有一个地址(0、1、2……),里面存一个数字。比如地址 0 存压力值,地址 1 存温度值。 +* **读/写操作**:上位机可以"读"寄存器(获取数据)或"写"寄存器(发送控制指令)。 + +**Modbus 有两种常见变体:** + +| 变体 | 传输方式 | 适用场景 | +|------|---------|---------| +| Modbus RTU | 串口(RS-485/RS-232) | 短距离、设备直连 | +| Modbus TCP | 以太网(TCP/IP) | 远距离、网络通信 | + +本教程使用 **Modbus TCP**,因为它基于网络,我们可以在同一台电脑上同时运行上位机和模拟下位机,不需要任何物理连线。 + +## 1.3 为什么选择 Qt? + +Qt 是工业软件开发的首选框架之一,很多你在工厂、医院、交通系统中看到的监控界面都是用 Qt 开发的。原因很简单: + +| 优势 | 说明 | +|------|------| +| 跨平台 | 一套代码编译到 Windows、Linux、嵌入式设备 | +| 内置工业协议 | Qt Serial Bus 模块原生支持 Modbus,不用第三方库 | +| 强大的图表 | Qt Charts 模块提供专业级实时图表 | +| 高性能 | C++ 底层,适合实时数据刷新 | +| 成熟稳定 | 30 年历史,工业领域验证充分 | + +## 1.4 我们要做什么? + +我们将构建一个 **水泵监控 HMI 系统**,模拟真实工厂中的水泵压力监控场景: + +| 功能 | 说明 | +|------|------| +| 实时数据读取 | 每秒从下位机读取压力值并显示 | +| 压力趋势图 | 用折线图展示最近 60 秒的压力变化 | +| 超阈值报警 | 压力超过设定值时弹窗报警,界面变红 | +| 故障日志 | 所有报警事件记录到数据库,可查询历史 | +| 手动控制 | 一键启停水泵(写入下位机寄存器) | + +![placeholder: 水泵监控 HMI 系统效果预览图,展示实时压力数值、趋势图、报警指示灯、启停按钮和日志列表](images/image2.png) + +## 1.5 本教程的路线图 + +我们将按以下步骤完成整个流程: + +1. **准备环境和模拟下位机**(2 分钟):安装 Qt 6.5 和 Modbus Slave 模拟器 +2. **创建 Qt 项目并连接 Modbus**(3 分钟):建立上位机与模拟下位机的通信 +3. **实现实时数据读取和显示**(3 分钟):定时读取压力值并更新界面 +4. **绘制实时压力趋势图**(3 分钟):用 Qt Charts 绘制动态折线图 +5. **实现报警系统和故障日志**(3 分钟):超阈值报警 + SQLite 日志记录 +6. **打包与部署**(可选):将应用打包为独立可执行文件 + +# 第 2 章:准备环境和模拟下位机(2 分钟) + +## 2.1 安装 Qt 6.5 + +Qt 提供了免费的开源版本,足够我们使用。 + +1. 访问 [Qt 官网](https://www.qt.io/download-qt-installer),下载 Qt Online Installer +2. 运行安装器,登录或注册 Qt 账号(免费) +3. 在组件选择页面,勾选以下内容: + - **Qt 6.5.x**(或更高版本) + - **Additional Libraries** 中勾选 **Qt Serial Bus**(Modbus 协议支持) + - **Additional Libraries** 中勾选 **Qt Charts**(图表绘制) + - **Qt Creator**(IDE,通常默认勾选) +4. 点击安装,等待完成 + +> **提示**:如果你已经安装了 Qt 但没有 Serial Bus 或 Charts 模块,可以重新运行 Qt Maintenance Tool,在"添加或移除组件"中补装。 + +![placeholder: Qt 安装器的组件选择页面截图,高亮 Qt Serial Bus 和 Qt Charts 的勾选项](images/image3.png) + +## 2.2 安装 Modbus Slave——你的"虚拟水泵" + +Modbus Slave 是一款免费的 Modbus 从站模拟软件,它可以在你的电脑上模拟一台工业设备(PLC/传感器),让你的上位机程序有东西可以"对话"。 + +1. 访问 [modbustools.com](https://www.modbustools.com/modbus_slave.html),下载 Modbus Slave +2. 安装并打开软件 +3. 配置连接: + - 点击菜单 **Connection → Connect** + - 选择 **Modbus TCP/IP** + - IP 地址填 `127.0.0.1`(本机) + - 端口填 `502`(Modbus TCP 默认端口) + - 点击 **OK** 开始监听 + +4. 设置模拟数据: + - 你会看到一个寄存器表格,每行是一个寄存器地址(0、1、2……) + - 双击地址 **0** 的值,改为 **120**(代表压力 1.20 MPa,程序中会除以 100 换算) + - 双击地址 **1** 的值,改为 **350**(代表温度 35.0°C) + - 双击地址 **2** 的值,改为 **1**(代表水泵运行状态:1=运行,0=停止) + +现在 Modbus Slave 就是你的"24 小时运行的虚拟水泵"——窗口保持开着,它会一直响应上位机的读写请求。 + +![placeholder: Modbus Slave 软件截图,展示 TCP 连接配置和寄存器表格中的模拟数据](images/image4.png) + +> **动态模拟技巧**:Modbus Slave 支持自动递增/随机变化。右键点击寄存器值,选择 "Auto increment" 或 "Random",就能模拟真实传感器的数据波动,让你的趋势图更生动。 + +# 第 3 章:创建 Qt 项目并连接 Modbus(3 分钟) + +## 3.1 新建 Qt 项目 + +打开 Qt Creator,创建新项目: + +1. 点击 **File → New Project** +2. 选择 **Application (Qt) → Qt Widgets Application** +3. 项目名称填 **PumpHMI** +4. 选择你安装的 Qt 6.5 Kit +5. 完成创建 + +打开 `PumpHMI.pro` 文件(如果用 CMake 则是 `CMakeLists.txt`),添加两个关键模块: + +```pro +QT += core gui widgets serialbus charts sql +``` + +| 模块 | 作用 | +|------|------| +| `serialbus` | 提供 QModbusTcpClient,用于 Modbus TCP 通信 | +| `charts` | 提供 QChart、QLineSeries,用于绘制实时趋势图 | +| `sql` | 提供 QSqlDatabase,用于 SQLite 故障日志存储 | + +如果使用 CMake,对应的配置是: + +```cmake +find_package(Qt6 REQUIRED COMPONENTS Widgets SerialBus Charts Sql) +target_link_libraries(PumpHMI PRIVATE + Qt6::Widgets Qt6::SerialBus Qt6::Charts Qt6::Sql) +``` + +## 3.2 声明核心成员 + +让 AI 帮你编写头文件: + +``` +请帮我编写 mainwindow.h,声明水泵监控 HMI 的核心成员: +1. QModbusTcpClient 用于 Modbus TCP 通信 +2. QTimer 用于定时读取数据 +3. QChart + QLineSeries 用于实时趋势图 +4. QSqlDatabase 用于故障日志存储 +5. 界面元素:压力显示标签、状态指示灯、启停按钮、日志表格 +``` + +核心头文件: + +```cpp +// mainwindow.h +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class MainWindow : public QMainWindow { + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private slots: + void connectModbus(); // 连接下位机 + void readPressure(); // 定时读取压力数据 + void onReadReady(); // 读取完成回调 + void triggerAlarm(float v); // 触发报警 + void togglePump(); // 启停水泵 + +private: + // Modbus 通信 + QModbusTcpClient *m_modbusClient = nullptr; + QTimer *m_pollTimer = nullptr; + + // 实时图表 + QChart *m_chart = nullptr; + QLineSeries *m_series = nullptr; + QDateTimeAxis *m_axisX = nullptr; + QValueAxis *m_axisY = nullptr; + + // 数据库 + QSqlDatabase m_db; + + // 界面元素 + QLabel *m_pressureLabel = nullptr; // 压力数值显示 + QLabel *m_statusLight = nullptr; // 状态指示灯 + QPushButton *m_pumpButton = nullptr; // 启停按钮 + QTableWidget *m_logTable = nullptr; // 日志表格 + + // 报警阈值 + float m_alarmThreshold = 1.50f; // 压力超过 1.50 MPa 报警 + bool m_pumpRunning = false; + + void setupUI(); + void setupDatabase(); + void logAlarm(float pressure, const QString &message); +}; + +#endif // MAINWINDOW_H +``` + +![placeholder: Qt Creator 中 mainwindow.h 文件的截图](images/image5.png) + +## 3.3 建立 Modbus TCP 连接 + +在 `mainwindow.cpp` 中实现连接逻辑: + +```cpp +// mainwindow.cpp — 连接部分 +void MainWindow::connectModbus() +{ + m_modbusClient = new QModbusTcpClient(this); + + // 连接到 Modbus Slave 模拟器 + m_modbusClient->setConnectionParameter( + QModbusDevice::NetworkPortParameter, 502); + m_modbusClient->setConnectionParameter( + QModbusDevice::NetworkAddressParameter, "127.0.0.1"); + m_modbusClient->setTimeout(1000); // 超时 1 秒 + m_modbusClient->setNumberOfRetries(3); // 重试 3 次 + + if (!m_modbusClient->connectDevice()) { + statusBar()->showMessage("连接下位机失败!", 3000); + return; + } + + statusBar()->showMessage("已连接到下位机 (127.0.0.1:502)", 3000); + + // 启动定时器,每秒读取一次数据 + m_pollTimer = new QTimer(this); + connect(m_pollTimer, &QTimer::timeout, this, &MainWindow::readPressure); + m_pollTimer->start(1000); // 1000ms = 1秒 +} +``` + +**代码解读:** + +| 代码 | 含义 | +|------|------| +| `QModbusTcpClient` | Qt 内置的 Modbus TCP 客户端,负责和下位机通信 | +| `NetworkPortParameter, 502` | 连接到 502 端口(和 Modbus Slave 中设置的一致) | +| `NetworkAddressParameter, "127.0.0.1"` | 连接本机(因为模拟器就在本机运行) | +| `m_pollTimer->start(1000)` | 每隔 1 秒自动调用 `readPressure()` 读取数据 | + +## 3.4 读取压力数据 + +```cpp +// mainwindow.cpp — 读取部分 +void MainWindow::readPressure() +{ + if (!m_modbusClient || m_modbusClient->state() != QModbusDevice::ConnectedState) + return; + + // 构建读取请求:从地址 0 开始,读取 3 个保持寄存器 + QModbusDataUnit readUnit( + QModbusDataUnit::HoldingRegisters, // 寄存器类型 + 0, // 起始地址 + 3 // 读取数量 + ); + + // 发送读取请求(异步) + if (auto *reply = m_modbusClient->sendReadRequest(readUnit, 1)) { + if (!reply->isFinished()) { + connect(reply, &QModbusReply::finished, + this, &MainWindow::onReadReady); + } else { + delete reply; // 广播请求,直接删除 + } + } +} + +void MainWindow::onReadReady() +{ + auto *reply = qobject_cast(sender()); + if (!reply) return; + + if (reply->error() == QModbusDevice::NoError) { + const QModbusDataUnit unit = reply->result(); + + // 解析数据(寄存器值除以 100 得到实际值) + float pressure = unit.value(0) / 100.0f; // 地址 0:压力 (MPa) + float temperature = unit.value(1) / 10.0f; // 地址 1:温度 (°C) + int pumpStatus = unit.value(2); // 地址 2:水泵状态 + + // 更新界面显示 + m_pressureLabel->setText( + QString("%1 MPa").arg(pressure, 0, 'f', 2)); + + // 检查是否需要报警 + if (pressure > m_alarmThreshold) { + triggerAlarm(pressure); + } + + // 更新趋势图(下一章实现) + // updateChart(pressure); + + } else { + statusBar()->showMessage( + QString("读取失败: %1").arg(reply->errorString()), 2000); + } + + reply->deleteLater(); +} +``` + +**Modbus 读取流程解读:** + +``` +readPressure() 被定时器触发 + → 构建 QModbusDataUnit(告诉下位机"我要读地址 0-2 的数据") + → sendReadRequest() 发送请求(异步,不阻塞界面) + → 下位机返回数据 + → onReadReady() 被触发 + → 解析寄存器值,更新界面 +``` + +![placeholder: 程序运行截图,展示压力数值实时更新,状态栏显示"已连接到下位机"](images/image6.png) + +# 第 4 章:绘制实时压力趋势图(3 分钟) + +## 4.1 初始化图表 + +Qt Charts 提供了专业级的图表组件。让 AI 帮你在构造函数中初始化: + +``` +请帮我在 MainWindow 构造函数中初始化 Qt Charts 实时折线图: +1. 创建 QChart 和 QLineSeries +2. X 轴为时间轴(QDateTimeAxis),显示最近 60 秒 +3. Y 轴为数值轴(QValueAxis),范围 0-3.0 MPa +4. 折线颜色为蓝色,线宽 2px +5. 将图表放入 QChartView 并添加到界面布局中 +``` + +核心代码: + +```cpp +// mainwindow.cpp — 图表初始化 +void MainWindow::setupChart() +{ + m_series = new QLineSeries(); + m_series->setName("压力 (MPa)"); + m_series->setPen(QPen(QColor("#2196F3"), 2)); + + m_chart = new QChart(); + m_chart->addSeries(m_series); + m_chart->setTitle("实时压力趋势"); + m_chart->setAnimationOptions(QChart::NoAnimation); // 实时数据不要动画 + + // X 轴:时间 + m_axisX = new QDateTimeAxis(); + m_axisX->setFormat("HH:mm:ss"); + m_axisX->setTitleText("时间"); + m_chart->addAxis(m_axisX, Qt::AlignBottom); + m_series->attachAxis(m_axisX); + + // Y 轴:压力值 + m_axisY = new QValueAxis(); + m_axisY->setRange(0, 3.0); + m_axisY->setTitleText("压力 (MPa)"); + m_axisY->setLabelFormat("%.1f"); + m_chart->addAxis(m_axisY, Qt::AlignLeft); + m_series->attachAxis(m_axisY); + + // 创建图表视图 + QChartView *chartView = new QChartView(m_chart); + chartView->setRenderHint(QPainter::Antialiasing); + + // 添加到布局(假设已有 centralLayout) + centralLayout->addWidget(chartView); +} +``` + +## 4.2 实时更新图表数据 + +每次读取到新的压力值时,往折线图中追加一个数据点,并保持只显示最近 60 秒的数据: + +```cpp +// mainwindow.cpp — 图表更新 +void MainWindow::updateChart(float pressure) +{ + QDateTime now = QDateTime::currentDateTime(); + + // 追加新数据点 + m_series->append(now.toMSecsSinceEpoch(), pressure); + + // 只保留最近 60 秒的数据(避免内存无限增长) + QDateTime cutoff = now.addSecs(-60); + while (m_series->count() > 0 && + m_series->at(0).x() < cutoff.toMSecsSinceEpoch()) { + m_series->remove(0); + } + + // 更新 X 轴范围:始终显示最近 60 秒 + m_axisX->setRange(cutoff, now); +} +``` + +然后在 `onReadReady()` 中调用它: + +```cpp +// 在 onReadReady() 中,解析完压力值后添加: +updateChart(pressure); +``` + +现在运行程序,你会看到一条蓝色折线在实时滚动——每秒新增一个数据点,始终显示最近 60 秒的压力变化。如果你在 Modbus Slave 中手动修改寄存器值,折线会立刻反映出变化。 + +![placeholder: 实时压力趋势图运行截图,展示蓝色折线在滚动更新,X 轴为时间,Y 轴为压力值](images/image7.png) + +> **性能提示**:`QChart::NoAnimation` 很重要——实时数据每秒刷新,如果开启动画会导致界面卡顿。这是工业 HMI 开发中的常见经验。 + +# 第 5 章:报警系统与故障日志(3 分钟) + +## 5.1 超阈值报警 + +当压力超过设定阈值时,我们需要:界面变红警示 + 弹窗提醒 + 记录日志。 + +```cpp +// mainwindow.cpp — 报警逻辑 +void MainWindow::triggerAlarm(float pressure) +{ + // 界面变红 + m_pressureLabel->setStyleSheet( + "color: white; background-color: #F44336;" + "font-size: 32px; padding: 10px; border-radius: 8px;"); + + // 状态指示灯变红 + m_statusLight->setStyleSheet( + "background-color: #F44336; border-radius: 12px;" + "min-width: 24px; min-height: 24px;"); + + // 弹窗报警(只在首次超阈值时弹出,避免反复弹窗) + static bool alarmActive = false; + if (!alarmActive) { + alarmActive = true; + QMessageBox::warning(this, "压力报警", + QString("当前压力 %1 MPa 超过阈值 %2 MPa!\n请立即检查水泵运行状态。") + .arg(pressure, 0, 'f', 2) + .arg(m_alarmThreshold, 0, 'f', 2)); + } + + // 记录到数据库 + logAlarm(pressure, + QString("压力超阈值: %1 MPa > %2 MPa") + .arg(pressure, 0, 'f', 2) + .arg(m_alarmThreshold, 0, 'f', 2)); + + // 压力恢复正常时重置 + if (pressure <= m_alarmThreshold) { + alarmActive = false; + m_pressureLabel->setStyleSheet( + "color: #2196F3; font-size: 32px; padding: 10px;"); + m_statusLight->setStyleSheet( + "background-color: #4CAF50; border-radius: 12px;" + "min-width: 24px; min-height: 24px;"); + } +} +``` + +![placeholder: 压力超阈值时的报警截图,展示红色背景的压力数值、红色指示灯和报警弹窗](images/image8.png) + +## 5.2 SQLite 故障日志 + +工业系统必须记录所有报警事件,方便事后追溯。我们用 SQLite 数据库来存储: + +```cpp +// mainwindow.cpp — 数据库初始化 +void MainWindow::setupDatabase() +{ + m_db = QSqlDatabase::addDatabase("QSQLITE"); + m_db.setDatabaseName("pump_alarm_log.db"); + + if (!m_db.open()) { + qWarning() << "无法打开数据库:" << m_db.lastError().text(); + return; + } + + // 创建报警日志表 + QSqlQuery query; + query.exec( + "CREATE TABLE IF NOT EXISTS alarm_log (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " timestamp DATETIME DEFAULT CURRENT_TIMESTAMP," + " pressure REAL," + " message TEXT" + ")" + ); +} +``` + +## 5.3 记录和展示日志 + +```cpp +// mainwindow.cpp — 写入日志 +void MainWindow::logAlarm(float pressure, const QString &message) +{ + // 写入数据库 + QSqlQuery query; + query.prepare( + "INSERT INTO alarm_log (pressure, message) VALUES (?, ?)"); + query.addBindValue(pressure); + query.addBindValue(message); + query.exec(); + + // 同时更新界面上的日志表格 + int row = m_logTable->rowCount(); + m_logTable->insertRow(row); + m_logTable->setItem(row, 0, + new QTableWidgetItem( + QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"))); + m_logTable->setItem(row, 1, + new QTableWidgetItem(QString::number(pressure, 'f', 2))); + m_logTable->setItem(row, 2, + new QTableWidgetItem(message)); + + // 自动滚动到最新一条 + m_logTable->scrollToBottom(); +} +``` + +日志表格显示三列:时间、压力值、报警信息。每次报警都会自动追加一行,同时写入 SQLite 数据库持久化存储。 + +![placeholder: 故障日志表格截图,展示多条报警记录,包含时间戳、压力值和报警信息](images/image9.png) + +## 5.4 手动启停水泵 + +除了读取数据,上位机还需要能控制下位机。我们通过"写入寄存器"来实现水泵的启停: + +```cpp +// mainwindow.cpp — 控制水泵 +void MainWindow::togglePump() +{ + if (!m_modbusClient || m_modbusClient->state() != QModbusDevice::ConnectedState) + return; + + m_pumpRunning = !m_pumpRunning; + + // 构建写入请求:向地址 2 写入 1(启动)或 0(停止) + QModbusDataUnit writeUnit( + QModbusDataUnit::HoldingRegisters, 2, 1); + writeUnit.setValue(0, m_pumpRunning ? 1 : 0); + + if (auto *reply = m_modbusClient->sendWriteRequest(writeUnit, 1)) { + connect(reply, &QModbusReply::finished, this, [this, reply]() { + if (reply->error() == QModbusDevice::NoError) { + m_pumpButton->setText(m_pumpRunning ? "停止水泵" : "启动水泵"); + m_pumpButton->setStyleSheet(m_pumpRunning + ? "background-color: #F44336; color: white; padding: 12px;" + : "background-color: #4CAF50; color: white; padding: 12px;"); + statusBar()->showMessage( + m_pumpRunning ? "水泵已启动" : "水泵已停止", 2000); + } + reply->deleteLater(); + }); + } +} +``` + +在 Modbus Slave 中,你会看到地址 2 的值随着你点击按钮在 0 和 1 之间切换——这就是上位机"控制"下位机的过程。 + +![placeholder: 水泵启停按钮的截图,展示绿色"启动水泵"和红色"停止水泵"两种状态](images/image10.png) + +# 第 6 章:打包与部署(可选) + +## 6.1 使用 windeployqt / macdeployqt 打包 + +Qt 提供了官方的部署工具,自动收集应用所需的所有动态库: + +**Windows:** + +```bash +# 先构建 Release 版本,然后在构建目录中执行: +windeployqt PumpHMI.exe +``` + +`windeployqt` 会自动把 Qt 的 DLL、插件、翻译文件等复制到 exe 所在目录,打包后的文件夹可以直接发给别人使用。 + +**macOS:** + +```bash +macdeployqt PumpHMI.app -dmg +``` + +这会生成一个 `.dmg` 安装镜像,双击即可安装。 + +## 6.2 使用 Qt Installer Framework 制作安装包 + +如果你想做一个专业的安装向导(像 Windows 上常见的"下一步、下一步、完成"),可以使用 Qt Installer Framework: + +``` +请帮我用 Qt Installer Framework 为 PumpHMI 创建安装包: +1. 创建 installer 目录结构(config、packages) +2. 配置 config.xml(安装包名称、版本、目标目录) +3. 将 windeployqt 输出的文件放入 packages/com.example.pumphmi/data/ +4. 运行 binarycreator 生成安装包 +``` + +![placeholder: PumpHMI 安装向导截图,展示安装路径选择和安装进度](images/image11.png) + +# 第 7 章:写在最后 + +恭喜你!你已经从零构建了一个工业级的水泵监控 HMI 系统。回顾一下我们做了什么: + +1. 理解了上位机、下位机和 Modbus 协议的核心概念 +2. 用 Modbus Slave 模拟了一台"虚拟水泵",无需任何真实硬件 +3. 用 Qt 的 QModbusTcpClient 建立了上位机与下位机的通信 +4. 用 Qt Charts 绘制了实时滚动的压力趋势图 +5. 实现了超阈值报警弹窗和 SQLite 故障日志记录 +6. 实现了远程启停水泵的控制功能 + +整个过程没有用到任何真实工控设备,但开发出的程序和真实工厂现场使用的 HMI 系统在架构和功能上完全一致。当你把 Modbus Slave 换成真实的 PLC,这个程序就能直接用在生产环境中。 + +**进阶方向:** + +* **多设备监控**:同时连接多台下位机,用选项卡或分屏展示不同设备的数据 +* **历史数据回放**:从 SQLite 中读取历史数据,用时间滑块回放任意时段的趋势图 +* **OPC UA 协议**:Modbus 适合简单场景,更复杂的工业系统通常使用 OPC UA 协议,Qt 同样有官方支持(Qt OPC UA 模块) +* **Web 远程监控**:用 Qt WebSocket 模块把实时数据推送到浏览器端,实现手机远程查看 +* **AI 预测性维护**:把历史压力数据喂给机器学习模型,预测设备何时可能故障,提前维护 + +***用代码守护工业现场的每一台设备。*** + +# 参考文献 + +* [Qt Serial Bus 官方文档](https://doc.qt.io/qt-6/qtserialbus-index.html) +* [Qt Modbus TCP Client 示例](https://doc.qt.io/qt-6/qtserialbus-modbus-client-example.html) +* [Qt Charts 官方文档](https://doc.qt.io/qt-6/qtcharts-index.html) +* [Modbus 协议规范](https://modbus.org/specs.php) +* [Modbus Slave 模拟工具](https://www.modbustools.com/modbus_slave.html) +* [Qt Installer Framework 文档](https://doc.qt.io/qtinstallerframework/) +``` diff --git a/docs/zh-cn/stage-3/cross-platform/3.8-pwa-local-app/index.md b/docs/zh-cn/stage-3/cross-platform/3.8-pwa-local-app/index.md new file mode 100644 index 0000000..ce6f902 --- /dev/null +++ b/docs/zh-cn/stage-3/cross-platform/3.8-pwa-local-app/index.md @@ -0,0 +1,369 @@ +# 如何开发 PWA 本地应用——让网页变成"真正的 App" + +# 第 1 章:什么是 PWA 和 PWA 开发 + +在这篇教程中,我们将完整跑通一条闭环:从一个普通的网页项目,到一个可以安装在电脑桌面和手机主屏幕上、断网也能正常使用的"真正的 App"。你会亲手把一个 React 应用变成 PWA,部署上线后在手机上安装体验。 + +本次教程,你至少需要具备: + +- 一台电脑(Windows 或 Mac 均可) +- Node.js 环境(18.0 以上版本) +- 你的 AI 编程助手(Cursor / Trae / Claude Code 等) +- 一个手机(用于体验移动端安装) + +## 1.1 什么是 PWA? + +你有没有想过:**一个网页,能不能像微信、抖音一样,直接安装在手机桌面上,点开就用,甚至断网也能跑?** + +答案是:可以。这就是 **PWA(Progressive Web App,渐进式 Web 应用)**。 + +PWA 本质上还是一个网页,但它通过几项关键技术,让自己"进化"成了一个接近原生 App 的存在: + +* **可安装**:用户可以把它"安装"到桌面/主屏幕,拥有自己的图标和启动画面,打开后没有浏览器的地址栏,看起来就像一个独立的 App。 +* **可离线**:即使没有网络,App 也能正常打开并展示缓存过的内容。 +* **可推送**:像原生 App 一样发送通知提醒。 + +简单来说,PWA 就是 **"穿上了 App 外衣的网页"**。 + +![placeholder: 一张对比图,左边是普通网页在浏览器中打开的样子(有地址栏),右边是同一个网页以 PWA 模式安装后的样子(独立窗口,无地址栏,像原生 App)](images/image1.png) + +## 1.2 PWA 的三大核心技术 + +要让一个普通网页"进化"成 PWA,需要三样东西: + +**1. HTTPS(安全连接)** + +PWA 必须运行在 HTTPS 协议下。这是浏览器的硬性要求——只有安全的网站才有资格使用 Service Worker 等高级功能。好消息是,现在主流的部署平台(Vercel、Netlify、GitHub Pages)都自动提供免费的 HTTPS。 + +**2. Web App Manifest(应用清单)** + +这是一个 JSON 配置文件,告诉浏览器:"我是一个 App,我叫什么名字、用什么图标、打开后长什么样"。它决定了你的 PWA 安装后的外观和行为。 + +**3. Service Worker(服务工作线程)** + +这是 PWA 的"灵魂"。它是一段运行在浏览器后台的 JavaScript 代码,充当你的 App 和网络之间的"中间人"。它可以拦截网络请求、缓存资源,从而实现离线访问。你可以把它理解为一个 **"住在浏览器里的小管家"**,负责帮你存东西、取东西。 + +![placeholder: 一张示意图,展示 PWA 三大核心技术的关系:HTTPS 是地基,Manifest 是门面,Service Worker 是引擎](images/image2.png) + +## 1.3 为什么选择 PWA? + +在 Vibe Coding 时代,PWA 是性价比最高的"跨平台方案"之一: + +| 对比维度 | 原生 App | PWA | +|---------|---------|-----| +| 开发成本 | 需要分别开发 iOS / Android / 桌面端 | 一套代码,全平台通用 | +| 安装方式 | 需要去应用商店下载 | 浏览器里直接安装,秒装 | +| 更新方式 | 用户需要手动更新 | 自动更新,用户无感 | +| 体积大小 | 动辄几十 MB | 通常只有几百 KB | +| 离线能力 | 天然支持 | 通过 Service Worker 支持 | +| 适用场景 | 需要深度硬件访问(AR/蓝牙等) | 内容展示、工具类、轻量应用 | + +**一句话总结**:如果你的应用不需要调用摄像头的 AR 功能或蓝牙硬件,PWA 几乎是最省心的选择。 + +## 1.4 本教程的路线图 + +我们将使用 **Vite + React + vite-plugin-pwa** 的技术栈,按以下步骤完成整个流程: + +1. **创建项目并配置 PWA**:用 Vite 创建 React 项目,安装并配置 vite-plugin-pwa +2. **本地体验 PWA**:构建生产版本,在电脑上安装并测试离线能力 +3. **部署上线**:部署到 Vercel 获得 HTTPS 地址 +4. **手机安装**:在 Android 和 iPhone 上安装并使用 + +# 第 2 章:创建项目并配置 PWA + +## 2.1 用 AI 初始化项目 + +打开你的 AI 编程助手(Cursor / Trae / Claude Code),在对话框中输入以下 Prompt: + +``` +请帮我创建一个 Vite + React + TypeScript 项目,项目名叫 my-pwa-app。 +要求: +1. 使用 npm create vite@latest 创建项目 +2. 选择 React 框架和 TypeScript 模板 +3. 创建完成后进入项目目录并安装依赖 +4. 额外安装 vite-plugin-pwa 插件:npm install vite-plugin-pwa -D +``` + +等 AI 执行完毕后,你的项目目录结构大致如下: + +``` +my-pwa-app/ +├── public/ # 静态资源(图标放这里) +├── src/ +│ ├── App.tsx # 主组件 +│ ├── main.tsx # 入口文件 +│ └── App.css # 样式 +├── index.html # HTML 入口 +├── vite.config.ts # Vite 配置(PWA 配置写在这里) +├── package.json +└── tsconfig.json +``` + +## 2.2 准备 App 图标 + +PWA 需要图标才能被安装。我们至少需要两个尺寸:**192x192** 和 **512x512** 像素的 PNG 图片。 + +你可以让 AI 帮你生成: + +``` +请帮我用 HTML Canvas 生成两个简单的 PWA 图标(192x192 和 512x512), +背景色用渐变蓝色,中间写一个白色的字母 "P"。 +保存到 public/icon-192.png 和 public/icon-512.png。 +``` + +或者你也可以用任何设计工具(Figma、Canva)做一个你喜欢的图标,放到 `public/` 目录下。 + +![placeholder: 两个 PWA 图标示例,一个 192x192,一个 512x512,蓝色渐变背景加白色字母](images/image3.png) + +## 2.3 配置 vite-plugin-pwa + +这是最关键的一步。打开 `vite.config.ts`,让 AI 帮你配置 PWA 插件: + +``` +请帮我修改 vite.config.ts,添加 vite-plugin-pwa 的配置。要求: +1. 引入 VitePWA 插件 +2. 注册类型设为 autoUpdate(自动更新) +3. 配置 manifest: + - name: "My PWA App" + - short_name: "MyPWA" + - description: "一个示例 PWA 应用" + - theme_color: "#4285f4" + - background_color: "#ffffff" + - display: "standalone" + - 图标使用 public 目录下的 icon-192.png 和 icon-512.png +4. workbox 配置:缓存所有 js、css、html、png、svg 文件 +``` + +AI 会帮你生成类似这样的配置: + +```typescript +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { VitePWA } from 'vite-plugin-pwa' + +export default defineConfig({ + plugins: [ + react(), + VitePWA({ + registerType: 'autoUpdate', + manifest: { + name: 'My PWA App', + short_name: 'MyPWA', + description: '一个示例 PWA 应用', + theme_color: '#4285f4', + background_color: '#ffffff', + display: 'standalone', + icons: [ + { + src: '/icon-192.png', + sizes: '192x192', + type: 'image/png' + }, + { + src: '/icon-512.png', + sizes: '512x512', + type: 'image/png' + } + ] + }, + workbox: { + globPatterns: ['**/*.{js,css,html,ico,png,svg}'] + } + }) + ] +}) +``` + +**关键配置解读:** + +* `registerType: 'autoUpdate'`:当你发布新版本时,用户下次打开 App 会自动更新,无需手动操作。 +* `display: 'standalone'`:安装后以独立窗口运行,没有浏览器地址栏,看起来像原生 App。 +* `workbox.globPatterns`:告诉 Service Worker 要缓存哪些类型的文件,这些文件在离线时也能访问。 + +![placeholder: vite.config.ts 文件的编辑器截图,展示 PWA 配置代码](images/image4.png) + +## 2.4 给 App 添加一些内容 + +一个空白页面没什么意思。让 AI 帮你写一个简单但实用的页面,比如一个 **待办事项(Todo)应用**——这样我们还能体验离线数据持久化: + +``` +请帮我修改 App.tsx,实现一个简单的待办事项应用: +1. 顶部有一个输入框和 "添加" 按钮 +2. 下方展示待办列表,每项有完成/删除按钮 +3. 数据使用 localStorage 持久化存储 +4. 界面风格简洁现代,使用蓝色主题色 #4285f4 +5. 适配移动端(响应式布局) +``` + +这个 Todo 应用非常适合演示 PWA 的能力:即使断网,你依然可以添加和管理待办事项,因为数据存在本地,页面资源也被 Service Worker 缓存了。 + +![placeholder: Todo 应用的界面截图,展示输入框、待办列表和完成/删除按钮](images/image5.png) + +# 第 3 章:本地体验 PWA + +## 3.1 构建并预览 + +PWA 的 Service Worker 只在生产构建中生效(开发模式下不会注册)。所以我们需要先构建,再预览: + +``` +请帮我执行以下命令: +1. npm run build(构建生产版本) +2. npm run preview(启动本地预览服务器) +``` + +构建完成后,Vite 会在 `dist/` 目录下生成所有文件,包括自动生成的 `sw.js`(Service Worker)和 `manifest.webmanifest`。 + +预览服务器启动后,打开浏览器访问提示的地址(通常是 `http://localhost:4173`)。 + +## 3.2 在电脑上安装 PWA + +打开预览地址后,你会注意到浏览器地址栏右侧出现了一个 **安装图标**(一个小小的下载箭头或 "+" 号)。 + +**Chrome / Edge 安装步骤:** + +1. 点击地址栏右侧的安装图标 +2. 在弹出的对话框中点击 **"安装"** +3. PWA 会以独立窗口打开,同时在你的桌面/开始菜单/Dock 中创建快捷方式 + +安装后的 PWA 看起来就像一个原生桌面应用——没有地址栏,没有标签页,有自己的窗口和图标。 + +![placeholder: 两张对比截图:左边是浏览器中的安装提示,右边是安装后的独立窗口效果](images/image6.png) + +**macOS Safari 安装步骤:** + +1. 在 Safari 中打开 PWA 地址 +2. 点击菜单栏的 **文件 → 添加到程序坞** +3. PWA 图标会出现在 Dock 中 + +## 3.3 测试离线能力 + +这是 PWA 最酷的部分。让我们验证一下离线是否真的能用: + +1. 确保 PWA 已经在浏览器中打开过一次(让 Service Worker 缓存资源) +2. **断开网络**(关闭 Wi-Fi 或拔掉网线) +3. 刷新页面——你会发现 **App 依然正常加载!** +4. 添加几个待办事项——数据正常保存在 localStorage 中 + +你也可以打开 Chrome DevTools(F12)→ Application → Service Workers,查看 Service Worker 的运行状态和缓存的资源列表。 + +![placeholder: Chrome DevTools 的 Application 面板截图,展示 Service Worker 状态和 Cache Storage 中缓存的文件列表](images/image7.png) + +# 第 4 章:部署上线 + +PWA 必须运行在 HTTPS 上才能正常工作。好消息是,现在主流的部署平台都自动提供免费的 HTTPS。我们以 **Vercel** 为例(也可以用 Netlify 或 GitHub Pages)。 + +## 4.1 部署到 Vercel + +**第一步:安装 Vercel CLI** + +``` +请帮我全局安装 Vercel CLI:npm install -g vercel +``` + +**第二步:部署** + +在项目根目录下执行: + +``` +请帮我执行 vercel 命令部署项目。 +当提示 "Set up and deploy?" 时选择 Yes。 +当提示 "Which scope?" 时选择你的账号。 +当提示 "Link to existing project?" 时选择 No。 +当提示 "What's your project's name?" 时输入 my-pwa-app。 +当提示 "In which directory is your code located?" 时直接回车(默认当前目录)。 +当提示 "Want to modify these settings?" 时选择 No。 +``` + +等待几十秒,Vercel 会自动构建并部署你的项目。完成后,你会得到一个类似 `https://my-pwa-app.vercel.app` 的 HTTPS 地址。 + +![placeholder: 终端中 Vercel 部署成功的截图,展示部署后的 URL](images/image8.png) + +**第三步:验证 PWA** + +在浏览器中打开部署后的地址,你应该能看到: + +1. 地址栏右侧出现安装图标 +2. 打开 DevTools → Application → Manifest,能看到你配置的 App 信息 +3. Service Workers 标签下显示 Service Worker 已激活 + +## 4.2 使用 GitHub Pages 部署(替代方案) + +如果你更喜欢 GitHub Pages,需要额外配置 `base` 路径: + +``` +请帮我修改 vite.config.ts,添加 base 配置: +base: '/my-pwa-app/'(替换为你的 GitHub 仓库名) +同时更新 manifest 中的 icon 路径,加上 base 前缀。 +``` + +然后将构建产物推送到 GitHub 仓库的 `gh-pages` 分支即可。 + +# 第 5 章:在手机上安装 PWA + +这是最激动人心的部分——让你的网页变成手机上的"App"。 + +## 5.1 Android 手机安装 + +1. 在手机的 **Chrome 浏览器** 中打开你部署好的 PWA 地址 +2. Chrome 可能会自动弹出 **"添加到主屏幕"** 的横幅提示,直接点击即可 +3. 如果没有自动弹出,点击右上角的 **三个点菜单 → "安装应用"** 或 **"添加到主屏幕"** +4. 确认安装后,你的手机桌面上就会出现 App 图标 + +打开它,你会发现它以全屏模式运行,没有浏览器的地址栏和导航按钮,和原生 App 几乎一模一样。 + +![placeholder: Android 手机上的安装流程截图:左边是 Chrome 中的安装提示,中间是确认对话框,右边是安装后桌面上的图标](images/image9.png) + +## 5.2 iPhone 安装 + +iOS 上安装 PWA 只能通过 **Safari** 浏览器(其他浏览器不支持): + +1. 在 **Safari** 中打开你的 PWA 地址 +2. 点击底部的 **分享按钮**(方框加向上箭头的图标) +3. 在弹出的菜单中选择 **"添加到主屏幕"** +4. 给 App 起个名字,点击 **"添加"** + +从 iOS 26 开始,所有添加到主屏幕的网站都会默认以独立 App 模式打开,这是一个重大改进。 + +![placeholder: iPhone 上的安装流程截图:左边是 Safari 的分享菜单,右边是添加到主屏幕的确认页面](images/image10.png) + +> **iOS 的已知限制**: +> * 推送通知需要 iOS 16.4 以上,且必须先将 PWA 添加到主屏幕 +> * 不支持后台同步(Background Sync) +> * 存储空间比 Android 更受限 + +## 5.3 用 Lighthouse 审计你的 PWA + +Google 提供了一个叫 **Lighthouse** 的工具,可以给你的 PWA 打分。打开 Chrome DevTools(F12)→ Lighthouse 标签 → 勾选 "Progressive Web App" → 点击 "Analyze page load"。 + +一个合格的 PWA 应该在 PWA 评分上拿到满分。如果有扣分项,Lighthouse 会告诉你具体原因和修复建议。 + +![placeholder: Lighthouse 审计结果截图,展示 PWA 评分为满分,各项检查都通过](images/image11.png) + +# 第 6 章:写在最后 + +恭喜你!你已经成功构建了一个可以安装在电脑和手机上的 PWA 应用。回顾一下我们做了什么: + +1. 用 Vite + React 创建了一个 Web 应用 +2. 通过 vite-plugin-pwa 添加了 Service Worker 和 Manifest +3. 部署到 Vercel 获得了 HTTPS 地址 +4. 在电脑和手机上都成功安装并体验了离线能力 + +PWA 的魅力在于它的"渐进式"——你不需要一开始就做到完美。先让你的网页能被安装、能离线访问,然后再逐步添加推送通知、后台同步等高级功能。 + +**进阶方向:** + +* **推送通知**:使用 Push API + Notification API,让你的 App 能像微信一样发送消息提醒 +* **后台同步**:使用 Background Sync API,在网络恢复时自动同步离线期间的操作 +* **更智能的缓存策略**:根据不同类型的资源使用不同的 Workbox 缓存策略(CacheFirst、NetworkFirst、StaleWhileRevalidate) +* **发布到应用商店**:使用 [PWA Builder](https://www.pwabuilder.com/) 可以将 PWA 打包成 Android APK 或 Microsoft Store 应用 + +***一套代码,全平台通用——这就是 PWA 的力量。*** + +# 参考文献 + +* [Vite PWA 官方文档](https://vite-pwa-org.netlify.app/guide/) +* [Google PWA 开发指南](https://web.dev/progressive-web-apps/) +* [MDN Web App Manifest 文档](https://developer.mozilla.org/en-US/docs/Web/Manifest) +* [Workbox 缓存策略详解](https://developer.chrome.com/docs/workbox/caching-strategies-overview/) +* [PWA Builder - 将 PWA 发布到应用商店](https://www.pwabuilder.com/) + diff --git a/docs/zh-cn/stage-3/cross-platform/3.9-browser-ai-extension/index.md b/docs/zh-cn/stage-3/cross-platform/3.9-browser-ai-extension/index.md new file mode 100644 index 0000000..753e8fe --- /dev/null +++ b/docs/zh-cn/stage-3/cross-platform/3.9-browser-ai-extension/index.md @@ -0,0 +1,478 @@ +# 如何开发浏览器 AI 助手插件——一键总结任意网页 + +# 第 1 章:什么是浏览器插件和 Chrome 插件开发 + +在这篇教程中,我们将完整跑通一条闭环:从零开始开发一个 AI 驱动的 Chrome 浏览器插件,它能读取你正在浏览的任意网页内容,然后用 AI 帮你一键生成摘要。你会亲手完成插件的开发、调试,并学会如何发布到 Chrome Web Store。 + +本次教程,你至少需要具备: + +- Chrome 浏览器(建议 138 以上版本,如果要用内置 AI) +- 一个代码编辑器(VS Code / Cursor / Trae) +- (可选)OpenAI 或 Claude 的 API Key + +## 1.1 什么是浏览器插件? + +你一定用过浏览器插件(Extension)——广告拦截器、翻译工具、密码管理器……它们就像浏览器的"外挂装备",能在你浏览网页时提供额外的超能力。 + +想象一下:你打开一篇 5000 字的技术博客,点一下插件按钮,几秒钟后,一份精炼的中文摘要就出现在侧边栏里。这就是我们要构建的东西。 + +![placeholder: 一张效果预览图,左边是一个长文章网页,右边是 Chrome 侧边栏中显示的 AI 生成的摘要](images/image1.png) + +## 1.2 Chrome 插件的基本架构 + +Chrome 插件(基于 Manifest V3)由几个核心部分组成,它们各司其职: + +* **Manifest 文件(manifest.json)**:插件的"身份证",声明插件的名称、权限、入口文件等。 +* **Service Worker(后台脚本)**:插件的"大脑",在后台处理事件、调用 API。它不是一直运行的,而是按需启动。 +* **Content Script(内容脚本)**:插件的"眼睛",注入到网页中,能读取页面的 DOM 内容。 +* **Side Panel(侧边栏)**:插件的"脸面",在浏览器右侧展示 UI,用户在这里看到 AI 的总结结果。 +* **Options Page(设置页)**:让用户配置 API Key 等参数。 + +它们之间的协作流程是这样的: + +``` +用户点击插件图标 + → 侧边栏打开 + → 用户点击"总结"按钮 + → 侧边栏通知 Service Worker + → Service Worker 让 Content Script 去读取页面文字 + → Content Script 返回页面内容 + → Service Worker 把内容发给 AI API + → AI 返回摘要 + → Service Worker 把摘要发回侧边栏显示 +``` + +![placeholder: 一张架构流程图,展示 Content Script、Service Worker、Side Panel 之间的消息传递关系](images/image2.png) + +## 1.3 两种 AI 方案:云端 API vs 浏览器内置 AI + +我们的插件有两种获取 AI 能力的方式: + +**方案 A:调用云端 AI API(OpenAI / Claude)** + +* 优点:模型能力强大,支持所有设备 +* 缺点:需要 API Key,需要联网,有使用成本 +* 适合:追求高质量摘要、需要处理复杂内容 + +**方案 B:使用 Chrome 内置 AI(Summarizer API)** + +从 Chrome 138 开始,Google 在浏览器中内置了基于 Gemini Nano 的 AI 能力,其中就包括 **Summarizer API**——完全在本地运行,不需要 API Key,不需要联网,完全免费。 + +* 优点:免费、隐私安全、无需 API Key +* 缺点:需要 Chrome 138+、需要较好的硬件(4GB+ 显存或 16GB+ 内存)、模型能力不如云端 +* 适合:注重隐私、不想花钱、硬件条件允许 + +**本教程将同时实现两种方案**,你可以根据自己的情况选择。 + +## 1.4 本教程的路线图 + +我们将从零构建一个名为 **"AI Page Summarizer"** 的 Chrome 插件,按以下步骤完成: + +1. **搭建插件骨架**:创建 Manifest V3 项目结构,加载到 Chrome 中 +2. **实现核心功能**:Content Script 读取页面 + Service Worker 调用 AI API + 侧边栏展示结果 +3. **接入 Chrome 内置 AI**:使用 Summarizer API 实现免费本地总结 +4. **测试与调试**:掌握 Chrome 插件的调试技巧 +5. **发布到 Chrome Web Store**:打包并提交审核 + +# 第 2 章:搭建插件骨架 + +## 2.1 创建项目结构 + +打开你的 AI 编程助手(Cursor / Trae / Claude Code),新建一个空文件夹 `ai-page-summarizer`,然后在对话框中输入: + +``` +请帮我创建一个 Chrome 浏览器插件项目,使用 Manifest V3。 +项目名叫 ai-page-summarizer,功能是用 AI 总结网页内容。 +请创建以下文件结构: + +ai-page-summarizer/ +├── manifest.json # MV3 清单文件 +├── background.js # Service Worker 后台脚本 +├── content.js # 内容脚本(读取页面文字) +├── sidepanel.html # 侧边栏 HTML +├── sidepanel.js # 侧边栏逻辑 +├── sidepanel.css # 侧边栏样式 +├── options.html # 设置页面 +├── options.js # 设置页面逻辑 +└── icons/ # 图标文件夹 + +manifest.json 的要求: +1. manifest_version: 3 +2. 权限:storage, activeTab, scripting, sidePanel +3. 后台使用 service_worker: "background.js" +4. 配置 side_panel,默认路径为 sidepanel.html +5. action 配置默认图标和标题 +``` + +AI 会帮你生成完整的项目骨架。让我们逐个看看每个文件的作用。 + +## 2.2 manifest.json——插件的"身份证" + +这是 Chrome 插件最重要的文件,它告诉浏览器这个插件是什么、需要什么权限、有哪些组件: + +```json +{ + "manifest_version": 3, + "name": "AI Page Summarizer", + "version": "1.0", + "description": "用 AI 一键总结任意网页内容", + "permissions": ["storage", "activeTab", "scripting", "sidePanel"], + "background": { + "service_worker": "background.js" + }, + "action": { + "default_title": "AI Page Summarizer", + "default_icon": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } + }, + "side_panel": { + "default_path": "sidepanel.html" + }, + "options_page": "options.html", + "icons": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } +} +``` + +**权限解读:** + +* `storage`:允许插件存储数据(比如用户的 API Key) +* `activeTab`:允许插件访问用户当前正在看的标签页(仅在用户点击插件时生效,非常安全) +* `scripting`:允许插件向页面注入脚本来读取内容 +* `sidePanel`:允许使用 Chrome 侧边栏 API + +![placeholder: manifest.json 文件在编辑器中的截图](images/image2b.png) + +## 2.3 准备图标 + +Chrome 插件需要三个尺寸的图标:16x16、48x48、128x128。你可以让 AI 帮你生成: + +``` +请帮我生成三个简单的 Chrome 插件图标(16x16、48x48、128x128), +设计风格:圆角矩形,渐变紫色背景,中间一个白色的 AI 闪电符号。 +保存到 icons/ 目录下,分别命名为 icon-16.png、icon-48.png、icon-128.png。 +``` + +## 2.4 加载插件到 Chrome + +在写代码之前,我们先把这个"空壳"插件加载到 Chrome 里,这样后续每次修改都能实时看到效果: + +1. 打开 Chrome,地址栏输入 `chrome://extensions/` +2. 打开右上角的 **"开发者模式"** 开关 +3. 点击 **"加载已解压的扩展程序"** +4. 选择你的 `ai-page-summarizer` 文件夹 + +你会看到插件出现在列表中,右上角的工具栏也会多出一个图标。 + +![placeholder: Chrome 扩展管理页面的截图,展示如何开启开发者模式并加载插件](images/image3.png) + +> **提示**:每次修改代码后,回到 `chrome://extensions/` 页面,点击插件卡片上的 **刷新按钮(🔄)** 即可更新。 + +# 第 3 章:实现核心功能——读取页面 + AI 总结 + +## 3.1 Content Script:读取页面文字 + +Content Script 是注入到网页中的脚本,它能直接访问页面的 DOM。我们用它来提取页面的文字内容。 + +让 AI 帮你编写 `content.js`: + +``` +请帮我编写 content.js,功能是: +1. 监听来自 Service Worker 的消息 +2. 当收到 "getPageContent" 消息时,提取当前页面的文字内容 +3. 提取逻辑:获取 document.body.innerText,同时获取页面标题和 URL +4. 将提取的内容通过 sendResponse 返回 +``` + +AI 会生成类似这样的代码: + +```javascript +// content.js +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'getPageContent') { + const content = document.body.innerText || document.body.textContent + sendResponse({ + content: content.trim(), + title: document.title, + url: window.location.href + }) + } + return true // 保持消息通道开放 +}) +``` + +## 3.2 Service Worker:调用 AI API + +Service Worker 是插件的"大脑",负责协调各个组件之间的通信,以及调用外部 AI API。 + +让 AI 帮你编写 `background.js`: + +``` +请帮我编写 background.js,功能是: +1. 当用户点击插件图标时,打开侧边栏 +2. 监听来自侧边栏的 "summarize" 消息 +3. 收到消息后,向当前标签页的 content script 发送 "getPageContent" 消息获取页面内容 +4. 拿到页面内容后,从 chrome.storage.local 读取用户配置的 API Key 和模型选择 +5. 根据配置调用对应的 AI API(支持 OpenAI 和 Claude) +6. 将 AI 返回的摘要发送回侧边栏 + +对于 OpenAI,调用 https://api.openai.com/v1/chat/completions,模型用 gpt-4o-mini +对于 Claude,调用 https://api.anthropic.com/v1/messages,模型用 claude-sonnet-4-20250514 +系统提示词:请用中文总结以下网页内容,提取核心要点,控制在 300 字以内。 +``` + +核心代码片段如下: + +```javascript +// background.js + +// 点击图标时打开侧边栏 +chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }) + +// 监听来自侧边栏的消息 +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'summarize') { + handleSummarize(request.tabId).then(sendResponse) + return true // 异步响应 + } +}) + +async function handleSummarize(tabId) { + // 1. 获取页面内容 + const [response] = await chrome.tabs.sendMessage(tabId, { + action: 'getPageContent' + }) + + // 2. 读取用户配置 + const { apiKey, provider } = await chrome.storage.local.get([ + 'apiKey', 'provider' + ]) + + if (!apiKey) { + return { error: '请先在设置页面配置 API Key' } + } + + // 3. 调用 AI API + const summary = provider === 'claude' + ? await callClaude(response.content, apiKey) + : await callOpenAI(response.content, apiKey) + + return { summary, title: response.title } +} +``` + +![placeholder: background.js 代码在编辑器中的截图](images/image4.png) + +## 3.3 侧边栏 UI:展示总结结果 + +侧边栏是用户与插件交互的主界面。让 AI 帮你编写侧边栏的 HTML、CSS 和 JS: + +``` +请帮我编写侧边栏的三个文件: + +sidepanel.html: +- 顶部显示插件名称 "AI Page Summarizer" +- 一个蓝色的 "总结当前页面" 按钮 +- 一个加载动画区域(默认隐藏) +- 一个结果展示区域,显示页面标题和 AI 摘要 +- 底部有一个 "复制摘要" 按钮 + +sidepanel.css: +- 简洁现代的设计风格,类似 Notion 的排版 +- 宽度自适应侧边栏 +- 按钮有 hover 效果 +- 加载动画用 CSS 实现 + +sidepanel.js: +- 点击 "总结" 按钮时,获取当前标签页 ID +- 向 background.js 发送 summarize 消息 +- 显示加载动画 +- 收到结果后隐藏加载动画,展示摘要 +- "复制" 按钮使用 navigator.clipboard.writeText 复制文字 +``` + +![placeholder: 侧边栏 UI 效果截图,展示总结按钮、加载状态和摘要结果三种状态](images/image5.png) + +## 3.4 设置页面:配置 API Key + +用户需要一个地方来输入自己的 API Key。让 AI 帮你编写设置页面: + +``` +请帮我编写 options.html 和 options.js: +- 一个下拉选择框,选择 AI 提供商(OpenAI / Claude) +- 一个密码输入框,输入 API Key(type="password") +- 一个 "保存" 按钮 +- 保存时使用 chrome.storage.local.set 存储配置 +- 页面加载时从 storage 读取已保存的配置并回填 +- 保存成功后显示 "设置已保存" 的提示 +``` + +> **安全提醒**:API Key 存储在 `chrome.storage.local` 中,仅在本地设备上保存。但如果你要发布到 Chrome Web Store 供他人使用,更安全的做法是搭建一个后端代理服务器,避免 API Key 直接暴露在客户端。 + +![placeholder: 设置页面的截图,展示 AI 提供商选择和 API Key 输入框](images/image6.png) + +# 第 4 章:使用 Chrome 内置 AI(无需 API Key) + +从 Chrome 138 开始,Google 在浏览器中内置了基于 **Gemini Nano** 的 AI 能力,其中最适合我们场景的就是 **Summarizer API**——完全在本地运行,不需要 API Key,不需要联网,完全免费。 + +## 4.1 检查浏览器是否支持 + +内置 AI 有硬件要求: + +* 桌面端 Chrome 138+(Windows 10+、macOS 13+、Linux、ChromeOS) +* 22 GB 可用存储空间(需要下载模型) +* GPU 显存 4GB 以上,或 CPU 内存 16GB 以上且 4 核以上 + +在 Chrome 地址栏输入 `chrome://flags`,搜索 `Summarization API`,确保它是 **Enabled** 状态。 + +![placeholder: chrome://flags 页面截图,展示 Summarization API 的开关位置](images/image7.png) + +## 4.2 使用 Summarizer API + +让 AI 帮你在 `background.js` 中添加内置 AI 的支持: + +``` +请帮我在 background.js 中添加 Chrome 内置 Summarizer API 的支持: +1. 添加一个 summarizeWithBuiltinAI 函数 +2. 先检查 Summarizer.availability() 是否返回 'readily-available' +3. 如果可用,创建 summarizer 实例,配置 type 为 'key-points',format 为 'markdown',length 为 'medium' +4. 调用 summarizer.summarize() 进行总结 +5. 在 handleSummarize 函数中,增加一个 provider === 'builtin' 的分支 +``` + +核心代码: + +```javascript +async function summarizeWithBuiltinAI(text) { + // 检查是否可用 + const availability = await Summarizer.availability() + if (availability !== 'readily-available') { + throw new Error('Chrome 内置 AI 不可用,请检查浏览器版本和硬件要求') + } + + // 创建总结器 + const summarizer = await Summarizer.create({ + type: 'key-points', + format: 'markdown', + length: 'medium' + }) + + // 执行总结 + const summary = await summarizer.summarize(text, { + context: '这是一篇网页文章' + }) + + return summary +} +``` + +## 4.3 更新设置页面 + +在 `options.html` 的 AI 提供商下拉框中,增加一个 **"Chrome 内置 AI(免费)"** 选项。当用户选择这个选项时,隐藏 API Key 输入框(因为不需要)。 + +``` +请帮我修改 options.html 和 options.js: +1. 在 AI 提供商下拉框中增加选项 "Chrome 内置 AI(免费,无需 API Key)",value 为 "builtin" +2. 当选择 builtin 时,隐藏 API Key 输入框 +3. 当选择 OpenAI 或 Claude 时,显示 API Key 输入框 +``` + +![placeholder: 更新后的设置页面截图,展示三个 AI 提供商选项,选中 Chrome 内置 AI 时 API Key 输入框隐藏](images/image8.png) + +# 第 5 章:测试与调试 + +## 5.1 本地测试流程 + +开发 Chrome 插件的调试方式和普通网页略有不同: + +**调试 Service Worker:** +1. 打开 `chrome://extensions/` +2. 找到你的插件,点击 **"Service Worker"** 链接 +3. 会打开一个专门的 DevTools 窗口,可以看到 console.log 输出和网络请求 + +**调试侧边栏:** +1. 打开侧边栏后,右键点击侧边栏内容 +2. 选择 **"检查"**(Inspect) +3. 会打开侧边栏的 DevTools + +**调试 Content Script:** +1. 在任意网页上按 F12 打开 DevTools +2. 在 Console 面板中,点击左上角的下拉框,选择你的插件名称 +3. 就能看到 Content Script 的 console 输出 + +![placeholder: Chrome DevTools 调试插件的截图,展示如何选择不同的执行上下文来调试不同组件](images/image9.png) + +## 5.2 常见问题排查 + +| 问题 | 可能原因 | 解决方法 | +|------|---------|---------| +| 点击图标没反应 | Service Worker 报错 | 检查 Service Worker 的 DevTools Console | +| 获取不到页面内容 | Content Script 未注入 | 刷新页面后重试,检查 manifest 中的 matches 配置 | +| API 调用失败 | API Key 错误或过期 | 在设置页面重新输入 API Key | +| 侧边栏空白 | sidepanel.html 路径错误 | 检查 manifest 中的 side_panel.default_path | + +# 第 6 章:发布到 Chrome Web Store(可选) + +如果你想把插件分享给其他人使用,可以发布到 Chrome Web Store。 + +## 6.1 发布准备 + +1. **注册开发者账号**:访问 [Chrome Web Store Developer Dashboard](https://chrome.google.com/webstore/devconsole),支付一次性 $5 美元注册费 +2. **开启两步验证**:Google 账号必须开启两步验证才能发布插件 +3. **准备素材**: + * 插件图标:128x128 PNG + * 至少一张截图:推荐 1280x800 像素 + * 详细的功能描述 + * 隐私政策说明(如果你的插件处理用户数据) + +## 6.2 打包与上传 + +1. 将插件文件夹打包为 `.zip` 文件(不是 `.crx`) +2. 在 Developer Dashboard 中点击 **"New Item"** +3. 上传 `.zip` 文件 +4. 填写商店信息(名称、描述、截图、分类等) +5. 填写隐私实践(声明你的插件收集了哪些数据) +6. 点击 **"Submit for Review"** + +Google 会对提交的插件进行审核,通常需要几个工作日。权限越少、描述越清晰,审核通过越快。 + +![placeholder: Chrome Web Store Developer Dashboard 的截图,展示插件上传和信息填写界面](images/image10.png) + +# 第 7 章:写在最后 + +恭喜你!你已经从零构建了一个 AI 驱动的浏览器插件。回顾一下我们做了什么: + +1. 理解了 Chrome 插件的 Manifest V3 架构 +2. 用 Content Script 读取网页内容 +3. 用 Service Worker 调用 AI API 生成摘要 +4. 用 Side Panel 展示总结结果 +5. 还学会了使用 Chrome 内置 AI(无需 API Key) + +浏览器插件是一个非常有趣的开发领域——它让你能够"增强"互联网上的任何网页。除了总结页面,你还可以用类似的架构做很多事情: + +**进阶方向:** + +* **翻译助手**:一键将外文网页翻译成中文 +* **阅读标注**:在网页上高亮和批注,保存到云端 +* **价格追踪**:监控电商网页的价格变化并提醒 +* **代码解释器**:在 GitHub 上选中代码,AI 自动解释 + +Chrome 内置 AI 的出现更是降低了门槛——你甚至不需要 API Key 就能构建 AI 驱动的插件。随着浏览器 AI 能力的不断增强,这个领域的想象空间会越来越大。 + +***去给你的浏览器装上超能力吧!*** + +# 参考文献 + +* [Chrome Extension 官方文档 - Manifest V3](https://developer.chrome.com/docs/extensions/develop/) +* [Chrome Side Panel API](https://developer.chrome.com/docs/extensions/reference/api/sidePanel) +* [Chrome 内置 AI - Summarizer API](https://developer.chrome.com/docs/ai/summarizer-api) +* [Chrome 内置 AI - Prompt API](https://developer.chrome.com/docs/ai/prompt-api) +* [OpenAI API 文档](https://platform.openai.com/docs/api-reference) +* [Anthropic Claude API 文档](https://docs.anthropic.com/en/docs/) +