Appearance
续上一篇vscode 插件开发入门,这里 默认大家已经入门了,如果没入门的赶紧学习我的上一篇文章哦。
我们应该都在 vscode
中使用过有关 chatGPT
的插件吧,比如说打开一个 chatGPT
的对话框,选中文案后让 chatGPT
解释这段文案。学完这篇文章后,你也可以 做一个这样的插件!!!
本次要实现的功能
- 在侧边栏添加插件图标,点击图标后打开一个插件视图,视图中有两个按钮
- 打开
chatGPT 对话框
:可以与chatGPT
进行问答 - 设置:可以设置用户的
chatGPT
信息,这里需要你去购买一个openAi
的转发apikey
,毕竟调用chatGPT
是需要付费的。推荐一 个网站购买,我也是在这里买的,注:本人无任何收益。
- 打开
- 选中一段文案后,可以右键找到
CodeToolBox => 解释这段文案
,自动唤起chatGPT 对话框
,自动提问
直接上视频看效果吧~,视频地址,ps: 掘金说 我视频连接格式不对,没法上传。。。
代码仓库地址,创作不易,觉得可 以赏个 star
吧 🙏
在侧边栏添加插件图标
package.json
添加设置
"contributes": {
// 左侧侧边栏的容器设置,唯一标识 id 需要下方设置对应的 views,这里设置其名称、图标
"viewsContainers": {
"activitybar": [
{
"id": "CodeToolBox",
"title": "CodeToolBox",
"icon": "images/tool.png" // 自定义图标,请手动添加图片
}
]
},
// 对应上方设置的唯一 id,设置这个标签点击打开后的视图,name是视图上方的名称
"views": {
"CodeToolBox": [
{
"id": "CodeToolBox.welcome",
"name": "welcome",
}
]
},
// 设置这个视图里面的内容,
// 目前添加两个按钮(打开chatGPT对话框、设置)
"viewsWelcome": [
{
"view": "CodeToolBox.welcome",
"contents": "[打开chatGPT对话框](command:CodeToolBox.chatGPTView)\n[设置](command:CodeToolBox.openSetting)"
}
],
}
下面把图示位置称为插件视图
添加插件设置
package.json
添加设置按钮命令
"contributes": {
"commands": [
{
"command": "CodeToolBox.openSetting",
"title": "设置"
},
],
}
- 新建
/src/commands/createSetting.ts
import { commands, ExtensionContext } from "vscode";
export const registerCreateSetting = (context: ExtensionContext) => {
context.subscriptions.push(
commands.registerCommand("CodeToolBox.openSetting", () => {
// 打开插件设置
commands.executeCommand("workbench.action.openSettings", "CodeToolBox");
}),
);
};
- 在
src/extension.ts
添加命令
import { registerCreateSetting } from "./commands/createSetting";
export function activate(context: vscode.ExtensionContext) {
registerCreateSetting(context);
}
package.json
添加插件的设置项
"contributes": {
"configuration": {
"type": "object",
"title": "CodeToolBox",
"properties": {
"CodeToolBox.hostname": {
"type": "string",
"default": "api.openai.com",
"description": "第三方代理地址"
},
"CodeToolBox.apiKey": {
"type": "string",
"default": "api.openai.com",
"description": "第三方代理提供的apiKey"
},
"CodeToolBox.model": {
"type": "string",
"default": "gpt-3.5-turbo",
"description": "chatGPT模型(默认:gpt-3.5-turbo)"
}
}
}
}
这样当点击设置时,插件的设置就会自动打开,这里必须设置两个值,一个是你购买的 apiKey
,还有一个 houtname
,如果你也是在我上面那个地址购买的应该是 api.chatanywhere.com.cn
,这些设置后面需要获取然后传给 webview
去调 openAI
的接口
添加 chatGPT 对话框
需要实现:
- 点击
打开chatGPT对话框
按钮后在当前插件视图中切换到chatGPT对话框
- 打开后当然需要关闭吧,所以我们要在视图上方添加设置按钮以及关闭按钮
- 后面再去编写这个
chatGPT对话框
的页面
实现切换 chatGPT对话框
- 在
package.json
添加配置
新增命令,我们需要下面三个命令,对应的 title
都很清楚了
"contributes": {
{
"command": "CodeToolBox.chatGPTView",
"title": "chatGPT对话框"
},
{
"command": "CodeToolBox.openChatGPTView",
"title": "打开chatGPT对话框"
},
{
"command": "CodeToolBox.hideChatGPTView",
"title": "关闭chatGPT对话框",
"icon": "$(close)"
}
}
- 设置
chatGPT对话框
出现的时机
- 当
CodeToolBox.chatGPTView
为false
时就是那两个按钮的视图 - 当
CodeToolBox.chatGPTView
为true
时就是chatGPT对话框
的视图
在 package.json
添加配置
"views": {
"CodeToolBox": [
{
"id": "CodeToolBox.welcome",
"name": "welcome",
"when": "!CodeToolBox.chatGPTView"
},
{
"type": "webview",
"id": "CodeToolBox.chatGPTView",
"name": "chatGPT",
"when": "CodeToolBox.chatGPTView"
}
]
},
- 当插件视图为
chatGPT对话框
时,我们在其顶部添加两个按钮,设置与关闭
在 package.json
添加配置, 配置插件视图顶部,即 title
"menus": {
"view/title": [
{
"command": "CodeToolBox.hideChatGPTView",
"when": "view == CodeToolBox.chatGPTView", // 当插件视图为 `chatGPT对话框` 时才出现
"group": "navigation@4" // 分组是为了不让他两在同一个 `...` 出现
},
{
"command": "CodeToolBox.openSetting",
"when": "view == CodeToolBox.chatGPTView",
"group": "navigation@3"
}
]
}
编写命令代码
- 配置完了,我们来编写命令的代码了,新建
/src/commands/createChatGPTView.ts
CodeToolBox.chatGPTView
、CodeToolBox.openChatGPTView
、CodeToolBox.hideChatGPTView
, 现在这里处理这三个命令
import {
commands,
ExtensionContext,
WebviewView,
WebviewViewProvider,
window,
workspace,
} from "vscode";
import { getHtmlForWebview } from "../utils/webviewUtils";
// 创建一个 webview 视图
let webviewViewProvider: MyWebviewViewProvider | undefined;
// 实现 Webview 视图提供者接口,以下内容都是 chatGPT 提供
class MyWebviewViewProvider implements WebviewViewProvider {
public webview?: WebviewView["webview"];
constructor(private context: ExtensionContext) {
this.context = context;
}
resolveWebviewView(webviewView: WebviewView): void {
this.webview = webviewView.webview;
// 设置 enableScripts 选项为 true
webviewView.webview.options = {
enableScripts: true,
};
// 设置 Webview 的内容
webviewView.webview.html = getHtmlForWebview(
this.context,
webviewView.webview,
);
webviewView.webview.onDidReceiveMessage(
(message: {
cmd: string;
cbid: string;
data: any;
skipError?: boolean;
}) => {
// 监听webview反馈回来加载完成,初始化主动推送消息
if (message.cmd === "webviewLoaded") {
console.log("反馈消息:", message);
}
},
);
}
// 销毁
removeWebView() {
this.webview = undefined;
}
}
const openChatGPTView = (selectedText?: string) => {
// 唤醒 chatGPT 视图
commands.executeCommand("workbench.view.extension.CodeToolBox").then(() => {
commands
.executeCommand("setContext", "CodeToolBox.chatGPTView", true)
.then(() => {
const config = workspace.getConfiguration("CodeToolBox");
const hostname = config.get("hostname");
const apiKey = config.get("apiKey");
const model = config.get("model");
setTimeout(() => {
// 发送任务,并传递参数
if (!webviewViewProvider || !webviewViewProvider?.webview) {
return;
}
webviewViewProvider.webview.postMessage({
cmd: "vscodePushTask",
task: "route",
data: {
path: "/chat-gpt-view",
query: {
hostname,
apiKey,
selectedText,
model,
},
},
});
}, 500);
});
});
};
export const registerCreateChatGPTView = (context: ExtensionContext) => {
// 注册 webview 视图
webviewViewProvider = new MyWebviewViewProvider(context);
context.subscriptions.push(
window.registerWebviewViewProvider(
"CodeToolBox.chatGPTView",
webviewViewProvider,
{
webviewOptions: {
retainContextWhenHidden: true,
},
},
),
);
context.subscriptions.push(
// 添加打开视图
commands.registerCommand("CodeToolBox.openChatGPTView", () => {
openChatGPTView();
}),
// 添加关闭视图
commands.registerCommand("CodeToolBox.hideChatGPTView", () => {
commands
.executeCommand("setContext", "CodeToolBox.chatGPTView", false)
.then(() => {
webviewViewProvider?.removeWebView();
});
}),
);
};
解释代码
我们定一个
MyWebviewViewProvider
类,这个是webview
视图的类型,初始化一 个webviewViewProvider
的实例,在resolveWebviewView
这个方法中去设置 webview 里面的内容,有一些封装的方法在上一篇文章有,如果实在看不懂就下载我的 代码下来研究吧。并且给
webview
发送消息,让它打开chat-gpt-view
页面,传入hostname
、apiKey
、model
、selectedText
参数,其中selectedText
这个 参数是用户选中的文案,下面会介绍这个功能打开
chatGPT聊天框
其实就是下面代码,其实就是让vscode
打开CodeToolBox
插件后再设置CodeToolBox.chatGPTView
为true
,前面我们在package.json
设置的条件就会生效,就能切换到chatGPT聊天框
了,然后再打开webview
项目的页面commands.executeCommand("workbench.view.extension.CodeToolBox").then(() => { commands .executeCommand("setContext", "CodeToolBox.chatGPTView", true).then(()=>{ }) })
在
src/extension.ts
添加命令
import { registerCreateChatGPTView } from "./commands/createChatGPTView";
export function activate(context: vscode.ExtensionContext) {
registerCreateChatGPTView(context);
}
编写 chatGPT对话框
页面
这里就是自己写一个 chatGPT对话框
的页面,我上一篇文章有提到 webview
项目的创 建,这里我使用的 vue2+vite
,打包的时候必须要要打包成一个 js
才能在 vscode
中使用,所以这里建议大家跟我使用一样的,可以直接拉代码看我的项目吧,避免踩坑。
- 一个聊天对话框的页面相信大家都会写,这里有个关键点就是
openAI
返回的数据其实 一段字符串,我们需要去解析它,并让它以markdown
的格式输出,并且要逐字输出 - 因为
openAI
自带的流式输出我不知道如何监听获取,所以我这里是直接获取整个答案 文本,使用requestAnimationFrame
逐字输出 - 这里我使用的
markdown-it
、markdown-it-code-copy
、markdown-it-highlightjs
这三个插件,封装了一个渲染 返回数据的组件,可供大家参考一下
需要安装四个依赖
yarn add highlightjs markdown-it markdown-it-code-copy markdown-it-highlightjs
组件代码:CodeDisplay.vue
<template>
<div class="code-container">
<div v-html="markdown.render(answer)"></div>
</div>
</template>
<script lang="ts" setup>
import MarkdownIt from "markdown-it";
import markdownItCodeCopy from "markdown-it-code-copy";
import markdownItHighlightjs from "markdown-it-highlightjs";
const markdown = new MarkdownIt()
.use(markdownItHighlightjs)
.use(markdownItCodeCopy);
defineProps({
answer: {
type: String,
required: true,
},
});
</script>
<style>
@import url("highlightjs/styles/default.css");
</style>
- 贴一下自己的页面代码、关键方法以及样式吧
页面模板文件:index.vue
<template>
<div class="chat-container">
<div class="messages-container" ref="scrollContainer">
<div class="empty-item"></div>
<div
:class="['message-item', item.role]"
v-for="item in model.messageList.value"
:key="item.content"
>
<CodeDisplay :answer="item.content" />
<span class="time">{{ item.time }}</span>
</div>
<div class="loading-container" v-if="model.loading.value">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</div>
<div class="input-container">
<a-input
v-model:value="model.userInput.value"
class="user-input"
placeholder="请输入您的问题"
@keyup.enter="presenter.sendMessageEnter"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, ref, watch } from "vue";
import CodeDisplay from "../components/CodeDisplay.vue";
import { usePresenter } from "./presenter";
const presenter = usePresenter();
const { model } = presenter;
const scrollContainer = ref();
watch(
() => [model.messageList.value, model.loading.value],
() => {
nextTick(() => {
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight;
});
},
{
deep: true,
},
);
</script>
<style scoped lang="scss">
@import url("./index.scss");
</style>
<style>
.dot {
width: 12px;
height: 12px;
margin: 0 5px;
opacity: 0;
background-color: #fff;
border-radius: 50%;
animation: fadeIn 1.6s forwards infinite;
}
@import url("./index.scss");
</style>
页面数据:model.ts
import { ref } from "vue";
import { useRoute } from "vue-router";
import type { Message } from "./api";
export const useModel = () => {
// 当前调用的域名
const hostname = (useRoute().query.hostname as string) || "";
const apiKey = (useRoute().query.apiKey as string) || "";
const model = (useRoute().query.model as string) || "";
// 消息列表
const messageList = ref<Message[]>([]);
// 用户输入
const userInput = ref("");
// 是否在加载
const loading = ref(false);
// 是否能重新提交,在加载已经流式输出时不能重新提交
const canSubmit = ref(true);
return {
messageList,
userInput,
hostname,
apiKey,
loading,
canSubmit,
model,
};
};
export type Model = ReturnType<typeof useModel>;
方法文件:service.ts
import { fetchChatGPTQuestion } from "./api";
import { Model } from "./model";
export default class Service {
private model: Model;
constructor(model: Model) {
this.model = model;
}
async askQuestion() {
try {
this.model.loading.value = true;
this.model.canSubmit.value = false;
const res = await fetchChatGPTQuestion({
houseName: this.model.hostname,
apiKey: this.model.apiKey,
messages: this.model.messageList.value,
model: this.model.model,
});
if (res?.choices && res?.choices.length) {
this.model.messageList.value.push({
content: "",
role: "system",
time: new Date().toLocaleString(),
});
this.showText(res.choices[0].message.content);
}
} catch (error) {
this.model.messageList.value.push({
content: "",
role: "system",
time: new Date().toLocaleString(),
});
this.showText("sorry,未搜索到答案");
this.model.canSubmit.value = true;
} finally {
this.model.loading.value = false;
}
}
showText(orginText: string) {
let currentIndex = 0;
const animate = () => {
this.model.messageList.value[
this.model.messageList.value.length - 1
].content += orginText[currentIndex];
currentIndex++;
if (currentIndex < orginText.length) {
const timeout = setTimeout(() => {
requestAnimationFrame(animate);
// requestAnimationFrame 感觉太快了,延迟一下
if (currentIndex === orginText.length - 1) {
this.model.canSubmit.value = true;
}
clearTimeout(timeout);
}, 30);
}
};
animate();
}
}
接口文件:api.ts
import { request } from "@/utils/request";
interface IFetchChatGPTQuestionResult {
choices: {
finish_reason: string;
index: number;
message: {
content: string;
role: string;
};
}[];
}
interface IFetchChatGPTQuestionParams {
houseName: string;
apiKey: string;
model: string;
messages: Message[];
}
export interface Message {
content: string;
role: "user" | "system";
time: string;
}
// POST 请求示例
export function fetchChatGPTQuestion(data: IFetchChatGPTQuestionParams) {
return request<IFetchChatGPTQuestionResult>({
url: `https://${data.houseName}/v1/chat/completions`,
method: "POST",
data: {
model: data.model,
messages: data.messages,
},
headers: {
Authorization: `Bearer ${data.apiKey}`,
},
});
}
方法驱动文件:presenter.tsx
import { watch } from "vue";
import { useRoute } from "vue-router";
import { useModel } from "./model";
import Service from "./service";
export const usePresenter = () => {
const model = useModel();
const service = new Service(model);
const route = useRoute();
// 发送消息
const sendMessage = (content: string) => {
model.messageList.value.push({
content,
role: "user",
time: new Date().toLocaleString(),
});
service.askQuestion();
model.userInput.value = "";
};
// 回车发送
const sendMessageEnter = () => {
if (model.userInput.value && model.canSubmit.value) {
sendMessage(model.userInput.value);
}
};
watch(
() => route.query?.selectedText,
() => {
if (route.query?.selectedText && model.canSubmit.value) {
sendMessage(`${route.query.selectedText}, 请帮我解释这段文案`);
}
},
{
immediate: true,
},
);
return {
model,
service,
sendMessageEnter,
sendMessage,
};
};
ps:总感觉这样贴代码很罗嗦,但也怕大家看不懂,下面看看效果图:
至此,chatGPT 对话框
的动能就算完成了~
实现选中文案自动打开 chatGPT 对话框
这里要实现的功能:用户选中编辑器的一段文案后,右键找到 CodeToolBox => 解释这段文案
,自动唤起 chatGPT 对话框
,并自动提问
- 在
package.json
添加命令explainByChatGPT
命令
"contributes": {
"commands": [
{
"command": "CodeToolBox.explainByChatGPT",
"title": "解释这段文案"
}
],
// 添加右键菜单
"editor/context": [
{
"submenu": "CodeToolBox/editor/context" // 设置编辑视图中的右键菜单
}
],
"submenus": [
{
"id": "CodeToolBox/editor/context", // 定义id便于今后添加更多的右键菜单
"label": "CodeToolBox",
"icon": "$(octoface)"
}
],
"CodeToolBox/editor/context": [
{
"command": "CodeToolBox.explainByChatGPT" // 添加解释这段文案右键菜单
}
]
}
- 编辑
/src/commands/createChatGPTView.ts
,添加命令代码
export const registerCreateChatGPTView = (context: ExtensionContext) => {
context.subscriptions.push(
// 添加解释这段文案
commands.registerCommand("CodeToolBox.explainByChatGPT", () => {
// 获取当前活动的文本编辑器
const editor = window.activeTextEditor;
if (editor) {
// 获取用户选中的文本
const selectedText = editor.document.getText(editor.selection);
if (!selectedText) {
window.showInformationMessage("没有选中的文本");
return;
}
// 获取本插件的设置
const config = workspace.getConfiguration("CodeToolBox");
const hostname = config.get("hostname");
const apiKey = config.get("apiKey");
if (!hostname) {
window.showInformationMessage(
"请先设置插件 CodeToolBox 的 hostname,点击左侧标签栏 CodeToolBox 的图标进行设置",
);
return;
}
if (!apiKey) {
window.showInformationMessage(
"请先设置插件 CodeToolBox 的 apiKey,点击左侧标签栏 CodeToolBox 的图标进行设置",
);
return;
}
// 打开左侧的 chatGPT 对话框,并传入问题
openChatGPTView(selectedText);
} else {
window.showInformationMessage("没有活动的文本编辑器");
}
}),
)
};
这里会获取用户选中的文本,若没有选中文本则会提示,调用 openChatGPTView
方法,传 递 hostname、apiKey、model、selectedText
参数给 webview 进行处理,接收到 selectedText
的值就会自动提问。
至此,功能就算做完拉