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:
@@ -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 的系统能力。
|
||||
|
||||

|
||||
|
||||
## 1.2 Electron 的核心架构
|
||||
|
||||
Electron 应用由两种进程组成,理解它们是开发的关键:
|
||||
|
||||
**主进程(Main Process)**
|
||||
|
||||
* 相当于 App 的"总管"
|
||||
* 负责创建窗口、管理应用生命周期、访问文件系统等原生能力
|
||||
* 运行在 Node.js 环境中,可以使用所有 Node.js 模块
|
||||
* 整个应用只有一个主进程
|
||||
|
||||
**渲染进程(Renderer Process)**
|
||||
|
||||
* 相当于 App 的"门面"
|
||||
* 就是一个 Chromium 网页,负责展示 UI
|
||||
* 每个窗口对应一个渲染进程
|
||||
* 出于安全考虑,渲染进程不能直接访问 Node.js API
|
||||
|
||||
**预加载脚本(Preload Script)**
|
||||
|
||||
* 主进程和渲染进程之间的"桥梁"
|
||||
* 通过 `contextBridge` 安全地暴露特定的 API 给渲染进程
|
||||
|
||||
它们之间通过 **IPC(进程间通信)** 来传递消息,就像打电话一样:渲染进程说"我要录音",主进程收到后去调用系统麦克风。
|
||||
|
||||

|
||||
|
||||
## 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 |
|
||||
| 适合场景 | 快速上手、轻量使用 | 注重隐私、离线使用、长期高频使用 |
|
||||
|
||||

|
||||
|
||||
## 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 应用!虽然现在只有一个默认的欢迎页面,但它已经是一个真正的桌面程序了。
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
})
|
||||
```
|
||||
|
||||

|
||||
|
||||
# 第 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()
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 4.3 添加设置界面
|
||||
|
||||
让 AI 帮你在渲染进程中添加一个简单的设置面板,用于输入 API Key 和切换识别模式:
|
||||
|
||||
```
|
||||
请帮我在 index.html 中添加一个设置面板:
|
||||
1. 右上角有一个齿轮图标,点击展开设置面板
|
||||
2. 设置面板包含:
|
||||
- 识别模式切换(云端 API / 本地模型)
|
||||
- API Key 输入框(仅云端模式显示)
|
||||
- 语言选择下拉框(中文/英文/自动检测)
|
||||
3. 设置保存到 localStorage
|
||||
4. 面板可以点击外部区域关闭
|
||||
```
|
||||
|
||||

|
||||
|
||||
# 第 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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/` 目录下。
|
||||
|
||||

|
||||
|
||||
## 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?
|
||||
|
||||
NFT(Non-Fungible Token,非同质化代币)是区块链上的一种数字资产。和比特币、以太币这些"同质化"代币不同,每一个 NFT 都是独一无二的——就像世界上没有两幅完全相同的画。
|
||||
|
||||
你可以把 NFT 理解为 **"数字世界的收藏证书"**。它可以代表:
|
||||
|
||||
* 一幅数字画作的所有权
|
||||
* 一张活动门票
|
||||
* 一个游戏道具
|
||||
* 一份学习证书
|
||||
* 甚至一条推文
|
||||
|
||||
NFT 的核心价值在于:**它用区块链技术证明了"这个数字物品属于你",而且这个证明是公开透明、不可篡改的。**
|
||||
|
||||

|
||||
|
||||
## 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 个英文单词)。测试网钱包丢了无所谓,但养成好习惯很重要
|
||||
|
||||

|
||||
|
||||
## 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` |
|
||||
|
||||

|
||||
|
||||
## 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(可能需要等待几秒钟)。
|
||||
|
||||

|
||||
|
||||
# 第 3 章:编写并部署 NFT 智能合约(4 分钟)
|
||||
|
||||
## 3.1 打开 Remix IDE
|
||||
|
||||
Remix 是以太坊官方推荐的在线智能合约开发环境,完全在浏览器中运行,不需要安装任何东西。
|
||||
|
||||
打开浏览器,访问:**https://remix.ethereum.org/**
|
||||
|
||||
你会看到一个类似 VS Code 的界面,左侧是文件管理器,中间是代码编辑器,右侧是编译和部署面板。
|
||||
|
||||

|
||||
|
||||
## 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 提供了经过安全审计的实现,我们直接继承就行,不用自己从零写。
|
||||
|
||||

|
||||
|
||||
## 3.3 编译合约
|
||||
|
||||
1. 点击左侧面板的 **"Solidity Compiler"**(锤子图标)
|
||||
2. 编译器版本选择 **0.8.20**(或更高的 0.8.x 版本)
|
||||
3. 点击 **"Compile MySimpleNFT.sol"**
|
||||
4. 看到绿色对勾 ✅ 表示编译成功
|
||||
|
||||
> 如果报错,检查 Solidity 版本是否匹配,以及 OpenZeppelin 的 import 路径是否正确。Remix 会自动从 npm 下载 OpenZeppelin 依赖。
|
||||
|
||||

|
||||
|
||||
## 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 时需要用到。
|
||||
|
||||

|
||||
|
||||
# 第 4 章:铸造 NFT 并查看成果(4 分钟)
|
||||
|
||||
## 4.1 铸造你的第一个 NFT
|
||||
|
||||
部署成功后,在 Remix 下方的 **"Deployed Contracts"** 区域,你会看到合约的交互面板。
|
||||
|
||||
1. 展开合约面板,找到 **"mint"** 按钮(橙色)
|
||||
2. 直接点击 **"mint"**(不需要输入任何参数)
|
||||
3. MetaMask 弹出交易确认,点击 **"确认"**
|
||||
4. 等待几秒钟,交易完成
|
||||
|
||||
恭喜!你刚刚铸造了编号为 #0 的 NFT,它现在属于你的钱包地址。
|
||||
|
||||
你可以继续点击 "mint" 铸造更多——每次铸造的 NFT 编号会自动递增(#1、#2、#3……)。
|
||||
|
||||

|
||||
|
||||
## 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 是多少——这就是区块链"公开透明、不可篡改"的魅力。
|
||||
|
||||

|
||||
|
||||
# 第 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`
|
||||
|
||||

|
||||
|
||||
## 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 就会带上图片和描述了。
|
||||
|
||||

|
||||
|
||||
# 第 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 调用大模型
|
||||
|
||||

|
||||
|
||||
## 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 渲染)
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 1.3 我们要做什么插件?
|
||||
|
||||
我们将开发一个名为 **"AI Project Bot"** 的 VS Code 插件,它是你的 AI 项目助手,具备以下功能:
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 项目模板 | 侧边栏展示项目模板列表,一键生成新项目骨架 |
|
||||
| AI 对话 | 在 VS Code Chat 面板中创建 `@project-bot` 参与者,支持项目相关问答 |
|
||||
| 文件/段落 Chat | 右键选中代码或文件,直接发送给 AI 分析、解释、重构 |
|
||||
| 多文件问答 | 在资源管理器中多选文件,一键让 AI 梳理文件关系和逻辑 |
|
||||
| 快捷键 | 自定义快捷键快速触发常用操作 |
|
||||
|
||||

|
||||
|
||||
## 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` | 插件贡献的所有功能(命令、菜单、快捷键、视图等) |
|
||||
|
||||

|
||||
|
||||
## 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",你会看到右下角弹出一条消息。这说明你的插件已经在运行了。
|
||||
|
||||

|
||||
|
||||
> **调试技巧**:修改代码后,在 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. 在视图标题栏添加了一个 "+" 按钮用于创建项目
|
||||
|
||||

|
||||
|
||||
## 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 图标,点击后展开模板列表,点击任意模板即可创建项目。
|
||||
|
||||

|
||||
|
||||
# 第 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 这段代码是做什么的?`,你的插件就会调用大模型生成解释。
|
||||
|
||||

|
||||
|
||||
# 第 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)
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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 就会读取所有文件内容并给出分析报告。
|
||||
|
||||

|
||||
|
||||
# 第 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) 中找到所有可用图标。
|
||||
|
||||

|
||||
|
||||
# 第 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 插件市场中。
|
||||
|
||||

|
||||
|
||||
> **提示**:首次发布可能需要等待审核。确保你的 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 或工控机上的监控软件,也就是我们今天要开发的 **HMI(Human-Machine Interface,人机界面)**。它负责:
|
||||
|
||||
* 实时显示现场数据(数字、图表、动画)
|
||||
* 记录历史数据和报警日志
|
||||
* 让操作员远程控制设备
|
||||
* 提供数据分析和报表
|
||||
|
||||
你可以把上位机理解为工厂的"监控中心"——操作员坐在屏幕前,就能掌握整个工厂的运行状态。
|
||||
|
||||
**它们之间怎么通信?**
|
||||
|
||||
上位机和下位机之间通过 **工业通信协议** 交换数据。最常用的协议就是 **Modbus**——一个诞生于 1979 年的"老前辈",至今仍是工业领域使用最广泛的协议,因为它简单、可靠、几乎所有工控设备都支持。
|
||||
|
||||
```
|
||||
控制室 工厂现场
|
||||
┌──────────┐ Modbus 协议 ┌──────────┐
|
||||
│ 上位机 │ ◄──────────────► │ 下位机 │
|
||||
│ (Qt HMI) │ "请告诉我压力" │ (PLC/传感器)│
|
||||
│ │ "压力是 1.20MPa" │ │
|
||||
│ 显示数据 │ │ 读取传感器 │
|
||||
│ 记录日志 │ │ 控制水泵 │
|
||||
│ 报警提示 │ │ 自动保护 │
|
||||
└──────────┘ └──────────┘
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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 秒的压力变化 |
|
||||
| 超阈值报警 | 压力超过设定值时弹窗报警,界面变红 |
|
||||
| 故障日志 | 所有报警事件记录到数据库,可查询历史 |
|
||||
| 手动控制 | 一键启停水泵(写入下位机寄存器) |
|
||||
|
||||

|
||||
|
||||
## 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,在"添加或移除组件"中补装。
|
||||
|
||||

|
||||
|
||||
## 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 小时运行的虚拟水泵"——窗口保持开着,它会一直响应上位机的读写请求。
|
||||
|
||||

|
||||
|
||||
> **动态模拟技巧**: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
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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() 被触发
|
||||
→ 解析寄存器值,更新界面
|
||||
```
|
||||
|
||||

|
||||
|
||||
# 第 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 中手动修改寄存器值,折线会立刻反映出变化。
|
||||
|
||||

|
||||
|
||||
> **性能提示**:`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;");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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 数据库持久化存储。
|
||||
|
||||

|
||||
|
||||
## 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 之间切换——这就是上位机"控制"下位机的过程。
|
||||
|
||||

|
||||
|
||||
# 第 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 生成安装包
|
||||
```
|
||||
|
||||

|
||||
|
||||
# 第 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?
|
||||
|
||||
你有没有想过:**一个网页,能不能像微信、抖音一样,直接安装在手机桌面上,点开就用,甚至断网也能跑?**
|
||||
|
||||
答案是:可以。这就是 **PWA(Progressive Web App,渐进式 Web 应用)**。
|
||||
|
||||
PWA 本质上还是一个网页,但它通过几项关键技术,让自己"进化"成了一个接近原生 App 的存在:
|
||||
|
||||
* **可安装**:用户可以把它"安装"到桌面/主屏幕,拥有自己的图标和启动画面,打开后没有浏览器的地址栏,看起来就像一个独立的 App。
|
||||
* **可离线**:即使没有网络,App 也能正常打开并展示缓存过的内容。
|
||||
* **可推送**:像原生 App 一样发送通知提醒。
|
||||
|
||||
简单来说,PWA 就是 **"穿上了 App 外衣的网页"**。
|
||||
|
||||

|
||||
|
||||
## 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 和网络之间的"中间人"。它可以拦截网络请求、缓存资源,从而实现离线访问。你可以把它理解为一个 **"住在浏览器里的小管家"**,负责帮你存东西、取东西。
|
||||
|
||||

|
||||
|
||||
## 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/` 目录下。
|
||||
|
||||

|
||||
|
||||
## 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 要缓存哪些类型的文件,这些文件在离线时也能访问。
|
||||
|
||||

|
||||
|
||||
## 2.4 给 App 添加一些内容
|
||||
|
||||
一个空白页面没什么意思。让 AI 帮你写一个简单但实用的页面,比如一个 **待办事项(Todo)应用**——这样我们还能体验离线数据持久化:
|
||||
|
||||
```
|
||||
请帮我修改 App.tsx,实现一个简单的待办事项应用:
|
||||
1. 顶部有一个输入框和 "添加" 按钮
|
||||
2. 下方展示待办列表,每项有完成/删除按钮
|
||||
3. 数据使用 localStorage 持久化存储
|
||||
4. 界面风格简洁现代,使用蓝色主题色 #4285f4
|
||||
5. 适配移动端(响应式布局)
|
||||
```
|
||||
|
||||
这个 Todo 应用非常适合演示 PWA 的能力:即使断网,你依然可以添加和管理待办事项,因为数据存在本地,页面资源也被 Service Worker 缓存了。
|
||||
|
||||

|
||||
|
||||
# 第 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 看起来就像一个原生桌面应用——没有地址栏,没有标签页,有自己的窗口和图标。
|
||||
|
||||

|
||||
|
||||
**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 的运行状态和缓存的资源列表。
|
||||
|
||||

|
||||
|
||||
# 第 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 地址。
|
||||
|
||||

|
||||
|
||||
**第三步:验证 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 几乎一模一样。
|
||||
|
||||

|
||||
|
||||
## 5.2 iPhone 安装
|
||||
|
||||
iOS 上安装 PWA 只能通过 **Safari** 浏览器(其他浏览器不支持):
|
||||
|
||||
1. 在 **Safari** 中打开你的 PWA 地址
|
||||
2. 点击底部的 **分享按钮**(方框加向上箭头的图标)
|
||||
3. 在弹出的菜单中选择 **"添加到主屏幕"**
|
||||
4. 给 App 起个名字,点击 **"添加"**
|
||||
|
||||
从 iOS 26 开始,所有添加到主屏幕的网站都会默认以独立 App 模式打开,这是一个重大改进。
|
||||
|
||||

|
||||
|
||||
> **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 会告诉你具体原因和修复建议。
|
||||
|
||||

|
||||
|
||||
# 第 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 字的技术博客,点一下插件按钮,几秒钟后,一份精炼的中文摘要就出现在侧边栏里。这就是我们要构建的东西。
|
||||
|
||||

|
||||
|
||||
## 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 把摘要发回侧边栏显示
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
## 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` 文件夹
|
||||
|
||||
你会看到插件出现在列表中,右上角的工具栏也会多出一个图标。
|
||||
|
||||

|
||||
|
||||
> **提示**:每次修改代码后,回到 `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 }
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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 复制文字
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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 直接暴露在客户端。
|
||||
|
||||

|
||||
|
||||
# 第 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** 状态。
|
||||
|
||||

|
||||
|
||||
## 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 输入框
|
||||
```
|
||||
|
||||

|
||||
|
||||
# 第 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 输出
|
||||
|
||||

|
||||
|
||||
## 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 会对提交的插件进行审核,通常需要几个工作日。权限越少、描述越清晰,审核通过越快。
|
||||
|
||||

|
||||
|
||||
# 第 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/)
|
||||
|
||||
Reference in New Issue
Block a user