当前位置: 代码网 > it编程>编程语言>Javascript > Vue3+NodeJS 接入文心一言, 发布一个 VSCode 大模型问答插件

Vue3+NodeJS 接入文心一言, 发布一个 VSCode 大模型问答插件

2024年07月28日 Javascript 我要评论
Vue3+NodeJS 接入文心一言, 发布一个 VSCode 大模型问答插件

目录

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

二:新建一个vscode 插件项目

1. 官网教程地址

2. 一步一步来创建

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

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

1. 新建vue3+vite+ts项目

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

(1) 插件项目修改,把视图注册到侧边栏,完成消息传递

(2) web项目修改,增加事件监听

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

1. 大模型接入准备

2. nodejs调用api

3. 前端接口调试

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

1. 推荐教程

2. 增加插件商店图标

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

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

1. 行内提示功能的设计与实现(inlinecompletionitemprovider[9])

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

3. json 文件可视化编辑(jsontohtml)


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

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

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

  • 安装vscode,npm/yarn等,node版本 >12.0

  • 最好有时间提前看一看 vscode 官网api开发文档[1]

万字长文 action!

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

  1. 如果你的插件只提供原生vscode能力,没有复杂的ui需求,只需要在vscode插件项目上开发即可,类似插件比如volar git history eslint

  2. 如果提供复杂ui交互,定制化界面,就需要在vscode插件内嵌iframe页面(用iframe展示线上web地址与使用vscode提供的一套ui组件皆可,详见第三节),我这里选择访问线上地址,因此需要开发一个vscode插件项目与一个vue3项目(其他框架亦可),类似的复杂插件比如 codegeex iflycode,会将web页面展示在侧边栏中。

图片

image.png

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

二:新建一个vscode 插件项目

1. 官网教程地址

开始你的第一个插件项目[2]

2. 一步一步来创建
  • 找到一个比较舒服的文件夹,打开cmd,通过以下命令安装 vscode项目脚手架,取的是 registry.npmjs.org[3] 镜像源,因此可能会有科学问题

  npm install -g yo generator-code
  • 安装完成后,直接用命令创建新的插件项目

 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版本

图片

image.png

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

图片

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

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

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

1. 新建vue3+vite+ts项目

找一个舒服的文件夹,打开cmd

    npm init vite

执行后按需选择自己的框架与开发环境,然后run dev一下子,拿到地址, 比如 http://localhost:5173/

2. 将web页面展示在vscode侧边栏
(1) 插件项目修改,把视图注册到侧边栏,完成消息传递

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

vscode 提供了两种创建iframe的方法,webviewviewprovider 和 createwebviewpanel,选其一即可,这里我们介绍一下webviewviewprovider如何使用

首先在extension.ts 同级目录下新建 chatwebview.ts

  • webviewviewprovider 是一个接口,因此建一个自己的类实现它的方法即可

    图片

    下面我们创建一个实现webviewviewprovider接口的类chatwebview

    chatwebview.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文件,修改如下 (代码可直接运行)

// 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中,这个id为 chat-sidebar 的视图我们稍后会在
  // package.json 中声明,先理解为我们要把iframe渲染在那个地方(侧边栏还是标签页)需要在
  // packagea.json 中控制
  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

"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运行,打开拓展开发宿主

图片

image.png

点击左侧栏图标,会看见我们deweb页面加载出来啦!
再试一下选中文本的事件和postmessage通讯,点击上方help,选择倒数第三个toggle developer tools或者按ctrl+shift+i 可以打开谷歌开发者工具,调试vscode

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

(2) web项目修改,增加事件监听

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

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

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

图片

image.png

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

<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. 大模型接入准备

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

图片

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

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

图片

我们要拿这两个key,去获取 access_token 和 refresh_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了我们来这里测试一下 测试地址[7]

图片

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

[
  {
    "role": "user",
    "content": "介绍一下自己"
  },
]

可以看到接口调用结果

图片

image.png

2. nodejs调用api

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

图片

image.png

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

两个文件代码如下,具体解析见注释,可以直接复制过去,然后在控制台执行node server.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}.`);
});


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呗,通了就可以

图片

image.png

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

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

图片

image.png

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

3. 前端接口调试

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

先装一下 npm install @ant-design/icons-vue
然后把app.vue删一下

// 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画的,我加了接口调用)\

// 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中看一下当前效果

图片

image.png

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

图片

image.png

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

所以你的接口通了吗?通了的话点个赞吧,好人一生平安~
没通的话原因有点多,代码是没问题的,其他的可以评论区讨论下

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

1. 推荐教程

插件开发手册[8]

根据教程注册账号,拿到自己的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[9])

先说一个思路,就是用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等.

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com