赞
踩
目录
三:新建一个Vue3 项目,在侧边栏中展示,实现vscode插件 <=> vue项目 双向消息传递
1. 行内提示功能的设计与实现(InlineCompletionItemProvider[9])
2. SSH 远程打开文件能力(使用 remote-ssh 插件提供的命令)
随着大模型能力越来越卷,在垂直领域的落地也在加快,对于大模型代码生成能力而言,最简洁高效的方式就是集成为常用IDE的插件,在vscode的插件战场中,比较知名的就有 GitHub Copilot, 智谱清言的codegeex, 讯飞星火的iFlyCode。
那么我们就以开发一个简易的大模型对话插件,来探究一下vscode插件开发到发布的流程,研究一下文心一言大模型api的接入
跟着操作大约30-60分钟,你需要
安装vscode,npm/yarn等,node版本 >12.0
最好有时间提前看一看 vscode 官网api开发文档[1]
万字长文 Action!
如果你的插件只提供原生vscode能力,没有复杂的UI需求,只需要在vscode插件项目
上开发即可,类似插件比如Volar
Git History
Eslint
如果提供复杂UI交互,定制化界面,就需要在vscode插件内嵌iframe页面(用iframe展示线上web地址与使用vscode提供的一套UI组件皆可,详见第三节),我这里选择访问线上地址,因此需要开发一个vscode插件项目与一个vue3项目
(其他框架亦可),类似的复杂插件比如 CodeGeeX
iFlyCode
,会将web页面展示在侧边栏中。
image.png
本文主要讲解 如何在vscode插件中通过iframe展示web页面,获得更好的拓展性与可维护性
开始你的第一个插件项目[2]
找到一个比较舒服的文件夹,打开cmd,通过以下命令安装 vscode项目脚手架,取的是 registry.npmjs.org[3] 镜像源,因此可能会有科学问题
npm install -g yo generator-code
安装完成后,直接用命令创建新的插件项目
yo code
进入配置页面,默认就选择 NewExtension(TypeScript),后面的按照图中来就可
然后会自动创建好项目,并执行npm i,然后用 vscode 打开项目
目录结构就很清晰了,我们主要涉及修改 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项目,嵌入到侧边栏中
文章开头我们提到,插件内展示丰富的UI,既可以用iframe展示线上web网页,也可以在插件内部用vsode ui实现。下面我主要演示用iframe的方式,另一种嵌入方式推荐大家去看一下 CodeGeeX 插件[4]源码如何做的,引入了一套vscode风格的UI组件@vscode/webview-ui-toolkit
,源码里面的webviewUI文件夹与translationWebviewProvider.ts
文件都是相关代码。
找一个舒服的文件夹,打开cmd
npm init vite
执行后按需选择自己的框架与开发环境,然后run dev一下子,拿到地址, 比如 http://localhost:5173/
第一步当然是先建一个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项目中加一下消息接收和发送。
打开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,并接入百度文心一言大模型,做一个自己的插件小助手,如果他能记住我们之前问过的代码,并帮我们举一反三,并提醒我们查漏补缺就好了。
我分别注册了智谱清言(chatGLM)与文心一言(ERNIE-Bot),发现两者都有基础的免费额度,前者相对于后者代码能力貌似更强一些,我们这里做一个简单类似于代码错题本的对话助手,就接入文心一言吧
首先我们要去官网[5],注册一下开发者账号,并且实名认证 整个过程很简单,然后我们看一下api 文档[6]
下面我把主要步骤说一下 首先我们要创建一个自己的应用,获取到Secret Key
和API Key
进入下面页面,点击创建应用,输入应用名称和应用描述直接确定即可,然后会有一个应用生成,里面就有我们的Secret Key
和API 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
首先找一个舒服的文件夹,新建一个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
言归正传,我们来增加接口调用吧,顺便画一个看得过去的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
啊?文心一言还挺强,紧跟时事哦
所以你的接口通了吗?通了的话点个赞吧,好人一生平安~
没通的话原因有点多,代码是没问题的,其他的可以评论区讨论下
至此我们的聊天小插件算是开发完成了,我们学习了如何创建一个vscode插件,随后搭建了一个vue3项目展示在了侧边栏里,然后我们用nodejs接入了文心一言api,前端调用接口简单实现了对话功能,希望你看完这篇文章有所收获,有所感想!
插件开发手册[8]
根据教程注册账号,拿到自己的Token,通过vsce publish 1.0.x 来更新版本
插件商店的图标是通过读取package.json中的icon来展示的,该字段与publisher同级
"icon": "images/icon.png",
因为插件实时访问的前端服务,当我们更新前端资源时,当然希望插件能同步更新,此时要注意前端资源的缓存策略,最好是配置为 cache-control:no-store no-cache
先说一个思路,就是用InlineCompletionItemProvider实现行内提示
先说一个思路,就是通过在控制台执行 remote-ssh 的命令 :code --reuse-window vscode-remote://ssh-remote+${hostname}${path}
来实现打开远程ssh地址的文件,需要安装remote-ssh插件
先说一个思路,就是监听用户打开文件夹时的事件,然后再窗口中打开一个新的webview,试用了一些jsonToHtml的包不如自己手动格式化,将bool格式化为checkbox等.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。