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
This commit is contained in:
sanbuphy
2026-02-27 18:46:27 +08:00
parent 6334a29a63
commit f50fc95e81
6 changed files with 3296 additions and 0 deletions
@@ -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 模型 75MBlarge 模型 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 最核心的概念——**IPCInter-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 芯片的 Macwhisper.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)
@@ -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
NFTNon-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)
@@ -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"
- 在该容器下添加一个 viewid 为 "projectTemplates",名称 "项目模板"
- 添加命令 "ai-project-bot.createFromTemplate",标题 "从模板创建项目"
2. 创建 src/templates/templateProvider.ts
- 实现 TreeDataProvider,提供以下模板分类和模板:
- 前端:React + TypeScript、Vue 3 + TypeScript、Next.js App
- 后端:Express API、FastAPI Python
- 全栈:T3 StackNext.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<TemplateItem> {
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 TokenPAT**:在 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)
@@ -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 或工控机上的监控软件,也就是我们今天要开发的 **HMIHuman-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 <QMainWindow>
#include <QModbusTcpClient>
#include <QModbusDataUnit>
#include <QTimer>
#include <QtCharts>
#include <QSqlDatabase>
#include <QLabel>
#include <QPushButton>
#include <QTableWidget>
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<QModbusReply *>(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/)
```
@@ -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
你有没有想过:**一个网页,能不能像微信、抖音一样,直接安装在手机桌面上,点开就用,甚至断网也能跑?**
答案是:可以。这就是 **PWAProgressive 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 DevToolsF12)→ 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 CLInpm 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 DevToolsF12)→ 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/)
@@ -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 APIOpenAI / Claude**
* 优点:模型能力强大,支持所有设备
* 缺点:需要 API Key,需要联网,有使用成本
* 适合:追求高质量摘要、需要处理复杂内容
**方案 B:使用 Chrome 内置 AISummarizer 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 Keytype="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/)