我也想在 vscode 中写一个 chatGPT 插件

上一篇 vscode 插件开发入门已经带大家入门了 vscode 插件开发,学习本篇文章如若不懂的地方请翻看上篇文章。

学完这篇文章后,你也能开发一个 chatGPTvscode 插件,做一个 chatGPT 聊天框, 可以直接在 vscode 中安装我的插件 CodeToolBox 体验~

实现的功能

  • 在侧边栏添加插件图标,点击图标后打开一个插件视图,视图中有两个按钮
    • 打开 chatGPT 对话框:可以与 chatGPT 进行问答
    • 设置:可以设置用户的 chatGPT 信息,这里需要你去购买一个 openAi 的转发 apikey ,毕竟调用 chatGPT 接口是需要 💴 的。
  • 选中一段文案后,可以右键找到 CodeToolBox => 解释这段文案,自动唤起 chatGPT 对话框,自动提问

代码仓库地址,可以先下载后再看文章,更易理解,创作不易,觉得不错赏个 star 吧 🙏

运行截图:

那开始学习吧~

在侧边栏添加插件图标

vscode 内置图标库

package.json 添加设置

swift 复制代码
"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 添加设置按钮命令
json 复制代码
"contributes": {
  "commands": [
    {
      "command": "CodeToolBox.openSetting",
      "title": "设置"
    },
  ],
}
  • 新建 /src/commands/createSetting.ts
javascript 复制代码
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 添加命令
javascript 复制代码
import { registerCreateSetting } from "./commands/createSetting";
export function activate(context: vscode.ExtensionContext) {
  registerCreateSetting(context);
}
  • package.json 添加插件的设置项
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 都很清楚了

bash 复制代码
"contributes": {
   {
        "command": "CodeToolBox.chatGPTView",
        "title": "chatGPT对话框"
    },
    {
        "command": "CodeToolBox.openChatGPTView",
        "title": "打开chatGPT对话框"
    },
    {
        "command": "CodeToolBox.hideChatGPTView",
        "title": "关闭chatGPT对话框",
        "icon": "$(close)"
    }
}
  • 设置 chatGPT对话框 出现的时机
  1. CodeToolBox.chatGPTViewfalse 时就是那两个按钮的视图
  2. CodeToolBox.chatGPTViewtrue 时就是 chatGPT对话框 的视图

package.json 添加配置

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

perl 复制代码
 "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.chatGPTViewCodeToolBox.openChatGPTViewCodeToolBox.hideChatGPTView, 现在这里处理这三个命令

typescript 复制代码
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 页面,传入 hostnameapiKeymodelselectedText 参数,其中 selectedText 这个 参数是用户选中的文案,下面会介绍这个功能

    • 打开 chatGPT聊天框 其实就是下面代码,其实就是让 vscode 打开 CodeToolBox 插件后再设置 CodeToolBox.chatGPTViewtrue,前面我们在 package.json 设置的条件就会生效,就能切换到 chatGPT聊天框 了,然后再打开 webview 项目的页面

      javascript 复制代码
      commands.executeCommand("workbench.view.extension.CodeToolBox").then(() => {
      commands
        .executeCommand("setContext", "CodeToolBox.chatGPTView", true).then(()=>{
      
        })
      })
  • src/extension.ts 添加命令

javascript 复制代码
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-itmarkdown-it-code-copymarkdown-it-highlightjs 这三个插件,封装了一个渲染 返回数据的组件,可供大家参考一下

需要安装四个依赖

sql 复制代码
yarn add highlightjs markdown-it markdown-it-code-copy markdown-it-highlightjs

组件代码:CodeDisplay.vue

xml 复制代码
<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

xml 复制代码
<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

typescript 复制代码
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

kotlin 复制代码
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

css 复制代码
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

typescript 复制代码
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 命令
json 复制代码
"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,添加命令代码
dart 复制代码
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 的值就会自动提问。

至此,就做完啦

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax