发布自己的 vscode 大模型问答插件,vue+nodejs 接入文心一言api

随着大模型能力越来越卷,在垂直领域的落地也在加快,对于大模型代码生成能力而言,最简洁高效的方式就是集成为常用IDE的插件,在vscode的插件战场中,比较知名的就有 GitHub Copilot, 智谱清言的codegeex, 讯飞星火的iFlyCode。

那么我们就以开发一个简易的大模型对话插件,来探究一下vscode插件开发到发布的流程,研究一下文心一言大模型api的接入

跟着操作大约30-60分钟,你需要

万字长文 Action!

一:首先明确插件开发方式

  1. 如果你的插件只提供原生vscode能力,没有复杂的UI需求,只需要在vscode插件项目上开发即可,类似插件比如Volar Git History Eslint
  2. 如果提供复杂UI交互,定制化界面,就需要在vscode插件内嵌iframe页面(用iframe展示线上web地址与使用vscode提供的一套UI组件皆可,详见第三节),我这里选择访问线上地址,因此需要开发一个vscode插件项目与一个vue3项目(其他框架亦可),类似的复杂插件比如 CodeGeeX iFlyCode,会将web页面展示在侧边栏中。

本文主要讲解 如何在vscode插件中通过iframe展示web页面,获得更好的拓展性与可维护性

二:新建一个Vscode 插件项目

1. 官网教程地址

开始你的第一个插件项目

2. 一步一步来创建

  • 找到一个比较舒服的文件夹,打开cmd,通过以下命令安装 vscode项目脚手架,取的是 registry.npmjs.org 镜像源,因此可能会有科学问题
cmd 复制代码
  npm install -g yo generator-code
  • 安装完成后,直接用命令创建新的插件项目
cmd 复制代码
 yo code
  • 进入配置页面,默认就选择 NewExtension(TypeScript),后面的按照图中来就可
  • 然后会自动创建好项目,并执行npm i,然后用 vscode 打开项目

3. 分析目录结构以及运行插件

目录结构就很清晰了,我们主要涉及修改 extension.ts 以及 package.json文件

上图中,extension.ts 中 activate() 方法就是插件的入口函数,每次插件启动都会执行此函数,当前代码是注册了一个hello world命令,当你在vscode中通过 ctrl+shift+p 调出输入框并输入hello world,就会执行此注册命令的回调,弹出一个message框,下面我们来试一下

在当前项目中,直接按F5,会启动一个扩展开发宿主,你的插件就运行在这个vscode窗口上啦 下面我们调出命令输入框ctrl+shift+p ,输入 hello world, 会提示命令,选中执行,右下角会发现弹了一个message!!!

什么? 你的没弹出? 那你岂不是和我当时一样倒霉,但你不需要花时间去挖这个奇怪的~bug !

首先看一下你的vscode版本

当前vscode版本不能低于 package.json 中的最低版本要求!

这样写表示最低支持到1.83.0版本!改一下重新reload一下宿主插件,再试试命令就可以弹出啦! 到此我们的插件侧项目就搭建好了,下面我们简单建一个vue项目,嵌入到侧边栏中

三:新建一个Vue3 项目,在侧边栏中展示,实现vscode插件 <=> vue项目 双向消息传递

文章开头我们提到,插件内展示丰富的UI,既可以用iframe展示线上web网页,也可以在插件内部用vsode ui实现。 下面我主要演示用iframe的方式,另一种嵌入方式推荐大家去看一下CodeGeeX 插件源码如何做的,引入了一套vscode风格的UI组件@vscode/webview-ui-toolkit,源码里面的webviewUI文件夹与translationWebviewProvider.ts文件都是相关代码。

1. 新建vue3+vite+ts项目

go 复制代码
找一个舒服的文件夹,打开cmd
```cmd
  npm init vite
 ```
 执行后按需选择自己的框架与开发环境,然后run dev一下子,拿到地址, 比如 `http://localhost:5173/`

2. 将web页面展示在vscode侧边栏

第一步当然是先建一个iframe把我们的localhost地址填进去呗,开始。

插件侧

markdown 复制代码
  **在extension.ts 同级目录下新建 chatWebview.ts**


 vscode 提供了两种创建iframe的方法,`WebviewViewProvider 和 createWebviewPanel`
 我们介绍一下WebviewViewProvider如何使用,选其一即可
  • WebviewViewProvider 是一个接口,因此建一个自己的类实现它的方法即可 下面我们创建一个实现WebviewViewProvider接口的类ChatWebview

    chatWebview.ts 文件: (可直接运行)

    具体代码作用看注释

ts 复制代码
import { window, Position, WebviewView, WebviewViewProvider } from "vscode";
export class ChatWebview implements WebviewViewProvider {
  // 写一个public变量,方便对象引用创建后的webview实例,但是可能存在还未完全解析完成时,访问值为null
  // 看了vscode api发现,resolveWebView 返回一个 Thenable,可以在解析完成后拿到webview实例
  // 但是这个函数是在webview容器第一次显示时自动执行,不需要手动调用,不知道怎么拿到Thenable
  public webview: WebviewView | null = null;
  resolveWebviewView(webviewView: WebviewView): void | Thenable<void> {
    this.webview = webviewView;
    webviewView.webview.options = {
      enableScripts: true,
    };
    // 监听web端传来的消息
    webviewView.webview.onDidReceiveMessage((message) => {
      switch (message.command) {
        case "WebSendMesToVscode":
          // 实现一个简单的功能,将web端传递过来的消息插入到当前活动编辑器中
          let editor = window.activeTextEditor;
          editor?.edit((edit) => {
            let position = editor?.selection
              ? editor?.selection.start
              : new Position(0, 0);
            edit.insert(position, message.data);
          });
          return;
      }
    }, undefined);
    // webview 展示的内容本身就是嵌套在一个iframe中,因此在此html中再嵌套一个iframe时,需要传递两次postMessage
    webviewView.webview.html = `
    <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <style>
        html,
        body {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            background-color:#000000;
            overflow:hidden;
        }
        .webView_iframe {
            width: 100%;
            height: 100%;
            border: none;
        }
        .outer{
          width: 100%;
          height: 100%;
          overflow: hidden;
        }
      </style>
    </head>
    <body>
      <script>
      
      console.log('Hello from the webview!');
      // 向vscode 传递消息的固定写法, vscode 为我们封装好了postMessage
      const vscode = acquireVsCodeApi();
      // 接收来自web页面的消息
      window.addEventListener('message', event => {
          const message = event.data;
          switch (message.command) {
               // 插件传递消息给web端
              case 'vscodeSendMesToWeb':
                  let iframe = document.getElementById('WebviewIframe')
                  WebviewIframe.contentWindow.postMessage(message, "*")
                  console.log("fromWebViewIframe: "+message.data)
                  break;
              // web端发送消息给插件
              case 'WebSendMesToVscode':
                    vscode.postMessage(message);
                    break;
          }
      });

     </script>
        <div class="outer">
           <iframe id='WebviewIframe' class="webView_iframe" sandbox="allow-scripts allow-same-origin allow-forms allow-pointer-lock allow-downloads" allow="cross-origin-isolated; clipboard-read; clipboard-write;" src="http://localhost:5173/"></iframe>
        </div>
    </body>
    </html>
    `;
  }
}

提供webview视图的类创建好了,然后我们需要在入口函数中实例化一个webview ,然后把这个视图注册到vscode侧边栏中

打开extension.ts文件,修改如下 (代码可直接运行)

ts 复制代码
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from "vscode";
import { ChatWebview } from "./chatWebview";
// This method is called when your extension is activated
// vscode 插件入口函数,当插件第一次加载时会执行activate
export function activate(context: vscode.ExtensionContext) {
  console.log('Congratulations, your extension "Chat" is now active!');

  // 实现侧边栏的初始化
  // 实例化一个chatWebview
  const chatWebview = new ChatWebview();
  // 注册webview 到id为 Chat-sidebar 的views中
  context.subscriptions.push(
    vscode.window.registerWebviewViewProvider("Chat-sidebar", chatWebview, {
      webviewOptions: {
        // 这是一个比较有用的配置项,可以确保你的插件在不可见时不会被销毁,建议开启,否侧每次打开都会重新加载一次插件
        retainContextWhenHidden: true,
      },
    })
  );
  // 这里实现了一个简单的功能,在vscode打开的文件中,选中代码时会实时展示在web页面上
  // 监听用户选中文本事件
  vscode.window.onDidChangeTextEditorSelection((event) => {
    const editor = event.textEditor;
    let document = editor.document;
    let selection = editor.selection;
   // 获取当前窗口的文本
    let text = document.getText(selection);
    // 上文提到chatWebview可能为null 因此需要可选链写法,所以这里存在不稳定性,不过测试没问题~
    chatWebview?.webview?.webview.postMessage({
      // 第一次postMessage,下一次在chatWebview文件的iframe中 
      command: "vscodeSendMesToWeb",
      data: text,
    });
  });
}

// This method is called when your extension is deactivated
export function deactivate() {}

至此,我们实例化了ChatWebview,并将其与视图Chat-siderbar绑定
下面我们需要在package.json中将视图注册到侧边栏中,并指定名字,图标等 打开package.json 文件,修改如下 将原本的 contributes 字段替换一下

确保activitybar 中的id,在views中有对应的视图,我们这里id是Chat-sidebar-view,在views就要有有对应名字的视图, 并且该视图 Chat-sidebar-view 的id为我们 ChatWebview 绑定的视图id

json 复制代码
"contributes": {
    "commands": [],
    "viewsContainers": {
      "activitybar": [
        {
          "id": "Chat-sidebar-view",
          "title": "Chat",
          "icon": "images/vite.svg"
        }
      ]
    },
    "views": {
      "Chat-sidebar-view": [
        {
          "type": "webview",
          "id": "Chat-sidebar",
          "name": " Chat",
          "icon": "images/vite.svg",
          "contextualTitle": "Chat"
        }
      ]
    }
  },

到此!我们的视图和双向通讯在插件侧已经完成了,我们试一下! 直接F5运行,打开拓展开发宿主

点击左侧栏图标,会看见我们deweb页面加载出来啦!

再试一下选中文本的事件和postMessage通讯,点击上方help,选择倒数第三个Toggle developer tools或者按ctrl+shift+i 可以打开谷歌开发者工具,调试vscode

随便打开一个项目文件,在窗口中选中文本,会发现控制台一直在输出 fromWebViewIframe: ...... ,我们第一步通讯通了,下面在vue项目中加一下消息接收和发送。

Web侧

打开index.html,增加message的监听,收到消息时插入到container中

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Vue + TS</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
    <script type="module">
      window.addEventListener("message", (event) => {
        const message = event.data;
        switch (message.command) {
          case "vscodeSendMesToWeb":
            const div = document.getElementById("container");
            div.innerHTML = message.data;
            break;
        }
      });
    </script>
  </body>
</html>

我们试一下,在拓展开发宿主中选中文本,会实时展示在页面上!

下面我们发送消息试一下 简单修改一下 HellowWord.vue 组件,增加一个sendMessage 方法

ts 复制代码
<script setup lang="ts">
import { ref } from "vue";

defineProps<{ msg: string }>();

const count = ref(0);

const sendMessage = () => {
  window.parent.postMessage(
    {
      command: "WebSendMesToVscode",
      data: "this message is from vue3",
    },
    "*"
  );
};
</script>

<template>
  <h1>{{ msg }}</h1>

  <div class="card">
    <button type="button" @click="sendMessage">click</button>
    <p>
      Edit
      <code>components/HelloWorld.vue</code> to test HMR
    </p>
  </div>

  <p>
    Check out
    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
      >create-vue</a
    >, the official Vue + Vite starter
  </p>
  <p>
    Install
    <a href="https://github.com/vuejs/language-tools" target="_blank">Volar</a>
    in your IDE for a better DX
  </p>
  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>

<style scoped>
.read-the-docs {
  color: #888;
}
</style>

我们点击一下按钮,会发现在当前文件光标处插入了一条信息。 this message is from vue3!

到此我们的小插件展示出来了,也实现了数据互通。 下面我们实现一个简单对话UI,并接入百度文心一言大模型,做一个自己的插件小助手,如果他能记住我们之前问过的代码,并帮我们举一反三,并提醒我们查漏补缺就好了。

四:接入大模型对话能力,实现ChatUI

1. 大模型接入准备

我分别注册了智谱清言(chatGLM)与文心一言(ERNIE-Bot),发现两者都有基础的免费额度,前者相对于后者代码能力貌似更强一些,我们这里做一个简单类似于代码错题本的对话助手,就接入文心一言吧

首先我们要去官网,注册一下开发者账号,并且实名认证 整个过程很简单,然后我们看一下api 文档

下面我把主要步骤说一下 首先我们要创建一个自己的应用,获取到Secret KeyAPI Key

进入下面页面,点击创建应用 ,输入应用名称和应用描述直接确定即可,然后会有一个应用生成,里面就有我们的Secret KeyAPI Key

我们要拿这两个key,去获取 access_tokenrefresh_token, 用于JWT鉴权,有两种方式,其一我们可以在网页中访问一下拿到一次性30天的access_token用于临时测试,其二最好在项目http请求前自动用refresh_token去获取access_token

下面我们访问一下这个地址,当然你要把双key换成你自己的应用的~! https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=你的key&client_secret=你的key

直接用浏览器访问一下呢,然后就在请求里拿到了access_token (不能有小可爱找不到吧~) 拿到token了我们来这里测试一下 测试地址

填入标出的这两项,第二项示例如下

json 复制代码
[
  {
    "role": "user",
    "content": "介绍一下自己"
  },
]

可以看到接口调用结果

2. nodejs调用api

首先找一个舒服的文件夹,新建一个node项目,我们这里选用express框架,可以参考我这里的命令行

npm init 后会生成一个package.json,然后我们安装一下常用包
npm install express sequelize mysql2 axios body-parser cors --save 之后就可以在vscode中打开我们的项目了,我们先新建一个server.js ,作为我们的入口文件,再建一个chat.js 作为我们的大模型调用文件

两个文件代码如下,具体解析见注释,可以直接复制过去,然后在控制台执行node server.js 直接启动服务~

js 复制代码
//server.js
const Conversation = require("./chat.js");
const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");

const app = express();
// 暂时允许所有跨域请求
let corsOptions = {
  origin: "*",
};
app.use(cors(corsOptions));
// content-type:application/json
app.use(bodyParser.json());
// content-type:application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: true }));
// 对话类
const conversation = new Conversation();
// 定义/chat路由处理POST请求
app.post("/chat", async (req, res) => {
  const { messages = "" } = req.body || {};
  if (typeof messages !== "string") {
    return res.status(400).send({ error: "Invalid messages type" });
  }
  try {
    // 调用ask方法获取大模型结果
    const response = await conversation.ask(messages);
    return res.status(200).send({ message: response });
  } catch (error) {
    return res.status(500).send({ error: error.messages });
  }
});
// 设置监听端口
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(`服务器运行端口: ${PORT}.`);
});
js 复制代码
chat.js
// 访问模型服务
const axios = require("axios");
// 这里就是你的accessToken,我改了两个数,所以你得替换成自己的喽~
const accessToken =
  "24.88635a1444105db00bb6684c0598a9a3.2542000.1741590285.281335-42231960";
const ERNIEB4 =
  "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro";
const ERNIEB =
  "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions";

class Conversation {
  constructor() {
    // 上下文数据存在这里,文心的调用是需要把所有的历史对话数据全部传过去,所以上下文窗口大小得注意
    this.messages = [];
  }

  async ask(prompt) {
    // 问句push进去
    this.messages.push({ role: "user", content: prompt });
    console.log("message" + this.messages[0]);
    try {
      const res = await axios.post(
        ERNIEB,
        { messages: this.messages },
        { params: { access_token: accessToken } }
      );
      const { data } = res;
      console.log(data);
      // 答案也放进去
      this.messages.push({ role: "assistant", content: data.result });
      return data.result;
    } catch (error) {
      console.log("调用模型失败" + error);
    }
  }
}
// 导出函数
module.exports = Conversation;

所以我们的服务起了吗?网页试一下8080呗,通了就可以

下面我们在前端代码中加一下接口调用,就大功告成啦!

想必看到这里你也累了,我们去调戏一下 Sydney

看来一时半会我们还是不可替代的 o_O

3. 前端接口调试

言归正传,我们来增加接口调用吧,顺便画一个看得过去的UI界面

先装一下 npm install @ant-design/icons-vue

然后把App.vue删一下

js 复制代码
// app.vue
<script setup lang="ts">
import ChatUI from "./components/chatUI.vue";
</script>

<template>
  <div class="container">
    <ChatUI />
  </div>
</template>

<style scoped>
.container {
  width: 100%;
  height: 100%;
}
</style>

无需多言,chatUI.vue代码奉上(主要界面gpt画的,我加了接口调用)\

js 复制代码
// chatUI.vue
<template>
  <div class="chat-container">
    <div class="messages">
      <div
        v-for="(item, index) in chatList"
        :key="index"
        :class="['message', item.type]"
      >
        <div class="bubble">{{ item.content }}</div>
        <div class="avatar">
          <component
            :is="item.type === 'question' ? UserOutlined : RobotOutlined"
          />
        </div>
      </div>
    </div>
    <div class="input-area">
      <a-input
        v-model:value="inputValue"
        placeholder="Type a message..."
        @pressEnter="handleSend"
      />
      <a-button type="primary" @click="handleSend">send</a-button>
    </div>
  </div>
</template>

<script setup lang="ts">
import axios from "axios";
import { ref } from "vue";
import { UserOutlined, RobotOutlined } from "@ant-design/icons-vue";
const inputValue = ref("");
let chatList = ref<any[]>([]);

const handleSend = () => {
  const question = inputValue.value.trim();
  if (question) {
    getAnswer(question);
    chatList.value.push({ type: "question", content: question });
    inputValue.value = ""; // 清空输入框
  }
};
function getAnswer(question: string) {
  const URL = "http://localhost:8080/chat";
  const payload = {
    messages: question,
  };
  sendPost(
    URL,
    payload,
    {},
    (res: any) => {
      console.log(res.data.message);
      chatList.value.push({ type: "answer", content: res.data.message });
    },
    (err: any) => {
      console.log(err);
    }
  );
}

//post方法
function sendPost(
  url: string,
  data: any,
  headers = {},
  funcSuccess: any,
  funcError: any
) {
  const headerTem = {
    "content-Type": "application/json;charset=UTF-8",
  };
  if (JSON.stringify(headers) != "{}") {
    Object.assign(headerTem, headers);
  }
  axios
    .post(url, data, {
      headers: headerTem,
    })
    .then(function (res) {
      console.log("sendPost res info :", res);
      funcSuccess(res);
    })
    .catch((err) => {
      console.log("sendPost err info :" + err);
      if (funcError) {
        funcError(err);
      }
    });
}
</script>

<style scoped>
.chat-container {
  min-width: 300px;
  height: 100%;
  display: flex;
  flex-direction: column;
  /* background-color: #1e1e1e; */
  border: 1px solid #999;
  border-radius: 8px;
}

.messages {
  height: 650px;
  overflow-y: auto;
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.bubble {
  color: #333;
  text-align: right;
  margin-right: 8px;
}
.input-area {
  display: flex;
}
.message {
  display: flex;
  align-items: center;
}

.question {
  justify-content: flex-end;
}

.answer {
  justify-content: flex-start;
}
</style>

这个就是简单的调用接口,我就不注释了,我们试一下接口

我们在vscode中看一下当前效果

还可以吧,也不能要求AI太高,哈哈,我们问几个问题试试

啊? 文心一言还挺强,紧跟时事哦

所以你的接口通了吗?通了的话点个赞吧,好人一生平安~

没通的话原因有点多,代码是没问题的,其他的可以评论区讨论下

至此我们的聊天小插件算是开发完成了,我们学习了如何创建一个vscode插件,随后搭建了一个vue3项目展示在了侧边栏里,然后我们用nodejs接入了文心一言api,前端调用接口简单实现了对话功能,希望你看完这篇文章有所收获,有所感想!

五:注册开发者账号并发布插件

1. 推荐教程

插件开发手册

根据教程注册账号,拿到自己的Token,通过vsce publish 1.0.x 来更新版本

2. 增加插件商店图标

插件商店的图标是通过读取package.json中的icon来展示的,该字段与publisher同级

"icon": "images/icon.png",

3. 前端资源的缓存策略会影响插件web页面的实时更新

因为插件实时访问的前端服务,当我们更新前端资源时,当然希望插件能同步更新,此时要注意前端资源的缓存策略,最好是配置为 cache-control:no-store no-cache

六:实战能力探讨(会持续更新,欢迎探讨)

1. 行内提示功能的设计与实现(InlineCompletionItemProvider

先说一个思路,就是用InlineCompletionItemProvider实现行内提示

2. SSH 远程打开文件能力(使用 remote-ssh 插件提供的命令)

先说一个思路,就是通过在控制台执行 remote-ssh 的命令 :code --reuse-window vscode-remote://ssh-remote+${hostname}${path} 来实现打开远程ssh地址的文件,需要安装remote-ssh插件

3. Json 文件可视化编辑(JsonToHtml)

先说一个思路,就是监听用户打开文件夹时的事件,然后再窗口中打开一个新的webview,试用了一些jsonToHtml的包不如自己手动格式化,将bool格式化为checkbox等

相关推荐
DS随心转小程序4 小时前
DeepSeek井号解决方法
人工智能·aigc·deepseek·ds随心转
SmartBrain12 小时前
Agent 技术在医疗场景的应用研究
人工智能·语言模型·aigc
羊仔AI探索12 小时前
AI心理学导师测评,智能体商单案例
ide·人工智能·ai·aigc
小程故事多_8013 小时前
AI Agent架构革命,Skills模式为何能颠覆传统Workflow?
人工智能·架构·aigc
DS随心转小程序14 小时前
ai转pdf
人工智能·pdf·aigc·deepseek·ds随心转
DS随心转小程序14 小时前
豆包公式不乱码
人工智能·aigc·deepseek·ds随心转
得一录1 天前
大模型中的多模态知识
人工智能·aigc
搞科研的小刘选手1 天前
【数字经济专题会议】第三届粤港澳大湾区数字经济与人工智能国际学术会议(DEAI 2026)
人工智能·aigc·软件工程·电子商务·数字经济·经济学·学术会议
luoqice1 天前
彻底清除vscode注册表解决重装后无法使用问题
visual studio code
DS随心转小程序1 天前
ChatGPT和Gemini公式
人工智能·chatgpt·aigc·word·豆包·deepseek·ds随心转