如何使用Novita AI Sandbox构建Chrome插件

如何使用Novita AI Sandbox构建Chrome插件

想象一下,浏览文档网站或编程教程时,你不再感到孤单。你无需独自应对一切,而是有一个AI助手跟随你从一个页面到另一个页面。它不局限于任何单个网页。它始终陪伴在你身边,随时准备提供帮助。它可以安全地执行你在网上遇到的代码、提供解释,并在你需要的时候立即给出见解。

那么,如何将这个体验变为现实呢?通过构建一个浏览器扩展。这个扩展将包含一个用户可以与之聊天的AI代理,并且该代理将能够访问一个安全的沙箱机器,在其中运行代码并安全地执行其他操作。

为了创建这个系统,我们将构建一个Chrome扩展,使用一个支持工具调用的Novita模型,并集成Novita Sandbox作为代理的安全运行时。在本文中,我们将逐步介绍构建它的完整过程。

代码助手截图

在本教程结束时,你将学会:

  • 如何构建一个与AI代理集成的Chrome扩展
  • 如何使用Novita的Agentic LLMs
  • 如何将Novita Sandbox设置为浏览器代理的安全环境
  • 如何使扩展与代理进行实时通信

沙箱:你唯一需要的工具

我们正在构建的Chrome扩展依赖于AI代理来辅助用户。由于这个扩展旨在作为编程助手,代理需要能够运行代码、创建文件、检查输出,以及执行开发者可能做的所有典型任务。你可能认为这需要一长串工具才能实现,但实际上只需要一个:沙箱。

沙箱为代理提供了一个Linux环境,在其中它可以运行命令、创建和修改文件,以及执行通常在终端中进行的任何操作。对于这个项目,我们将使用Novita Sandbox

首先,安装Novita Sandbox包:

pip install novita-sandbox

然后,将环境变量NOVITA_API_KEY设置为你自己的API密钥。完成后,你可以像这样创建和使用沙箱:

from novita_sandbox.code_interpreter import Sandbox

sandbox = Sandbox.create()

result = sandbox.commands.run('ls -l')
print(result)

sandbox.kill()

这段代码创建了一个沙箱,运行了ls -l命令,打印输出,然后关闭沙箱。这个简单的工作流是浏览器助手利用沙箱帮助用户的基础。

现在,让我们将这个原理应用到完整的扩展中。

浏览器助手代理的架构

该项目的架构遵循客户端-服务器模型。Chrome扩展作为客户端,而专用的后端服务器承载AI代理和沙箱环境。

扩展通过WebSocket连接与服务器通信。这允许实时的双向消息传递,使用户的请求和代理的响应能够瞬间流动,没有任何明显的延迟。服务器则与Novita的API进行通信,这些API包括模型端点和沙箱服务。

扩展和后端共同构成了一个智能的浏览器助手,能够安全地运行代码、快速处理信息,并直接在用户的浏览体验中提供有帮助的解释。

构建扩展

现在我们已经了解了整体架构,可以开始实现扩展本身了。我们将从插件服务器开始。

扩展服务器

扩展服务器是一个简单的WebSocket服务,只有一个端点/ws。该端点接收来自用户的消息,并实时返回LLM的响应。它还处理工具调用,每当代理需要执行代码或执行操作时,就会调用沙箱。

依赖项

服务器依赖于三个核心库:

  • FastAPI:提供WebSocket实现的HTTP框架
  • OpenAI:用于与Novita模型通信的SDK
  • Novita Sandbox:安全执行代码的环境

使用以下命令安装它们:

pip install novita-sandbox "fastapi[standard]" openai

将你的Novita API密钥设置为环境变量:

export NOVITA_API_KEY = sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

编写服务器代码

首先,导入所需的模块:

import os
import json
import uvicorn
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from novita_sandbox.code_interpreter import Sandbox
from openai import OpenAI

初始化LLM客户端

接下来,创建一个指向Novita API的OpenAI客户端。在本示例中,我们使用llama-3.3-70b-instruct模型,但任何支持工具调用的Novita模型都可以。

client = OpenAI(
   base_url="https://api.novita.ai/openai",
   api_key=os.environ["NOVITA_API_KEY"],
)

model = "meta-llama/llama-3.3-70b-instruct"

定义工具模式

代理将使用四个工具,每个工具都与沙箱交互:

  • read_file:读取文件内容
  • write_file:创建并写入单个文件
  • write_files:创建并写入多个文件
  • run_commands:在沙箱内执行Shell命令

以下是完整的工具模式:

tools = [
   {
       "type": "function",
       "function": {
           "name": "read_file",
           "description": "Read contents of a file inside the sandbox",
           "parameters": {
               "type": "object",
               "properties": {"path": {"type": "string"}},
               "required": ["path"],
           },
       },
   },
   {
       "type": "function",
       "function": {
           "name": "write_file",
           "description": "Write a single file inside the sandbox",
           "parameters": {
               "type": "object",
               "properties": {
                   "path": {"type": "string"},
                   "data": {"type": "string"},
               },
               "required": ["path", "data"],
           },
       },
   },
   {
       "type": "function",
       "function": {
           "name": "write_files",
           "description": "Write multiple files inside the sandbox",
           "parameters": {
               "type": "object",
               "properties": {
                   "files": {
                       "type": "array",
                       "items": {
                           "type": "object",
                           "properties": {
                               "path": {"type": "string"},
                               "data": {"type": "string"},
                           },
                           "required": ["path", "data"],
                       },
                   }
               },
               "required": ["files"],
           },
       },
   },
   {
       "type": "function",
       "function": {
           "name": "run_commands",
           "description": "Run a shell command inside the sandbox working directory",
           "parameters": {
               "type": "object",
               "properties": {
                   "command": {"type": "string"},
               },
               "required": ["command"],
           },
       },
   },
]

配置HTTP服务器

设置FastAPI并启用CORS,以便Chrome扩展可以向服务器发送请求。

app = FastAPI()

app.add_middleware(
   CORSMiddleware,
   allow_origins=["*"],
   allow_methods=["*"],
   allow_headers=["*"],
)

实现工具

定义一个处理函数,实现每个工具,并将代理的工具调用路由到沙箱:

def make_tool_handlers(sandbox):
   def read_file(path: str):
       print(f"[LOG] read_file called with path: {path}")
       try:
           content = sandbox.files.read(path)
           print(f"[LOG] read_file result: {content}")
           return content
       except Exception as e:
           return f"Error reading file: {e}"

   def write_file(path: str, data: str):
       print(f"[LOG] write_file called with path: {path}")
       try:
           sandbox.files.write(path, data)
           return f"File created successfully at {path}"
       except Exception as e:
           return f"Error writing file: {e}"

   def write_files(files: list):
       print(f"[LOG] write_files called with {len(files)} files")
       try:
           sandbox.files.write_files(files)
           return f"{len(files)} file(s) created successfully"
       except Exception as e:
           return f"Error writing multiple files: {e}"

   def run_commands(command: str):
       print(f"[LOG] run_commands called with command: {command}")
       try:
           result = sandbox.commands.run(command)
           return result.stdout
       except Exception as e:
           return f"Error running command: {e}"

   return {
       "read_file": read_file,
       "write_file": write_file,
       "write_files": write_files,
       "run_commands": run_commands,
   }

创建WebSocket端点

现在实现WebSocket端点。当用户连接到端点时,会创建一个新的沙箱实例。

用户与代理之间的所有通信都通过这个连接进行。如果用户要求代理使用某个工具,代理会通过处理函数选择并执行相应的工具。

当连接关闭时,沙箱会被终止。

@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
   await ws.accept()
   print("\n[WS] Client connected")

   # Create sandbox per connection
   sandbox = Sandbox.create(timeout=1200)
   print("[WS] Sandbox created")

   tools_exec = make_tool_handlers(sandbox)
   messages = []  # persistent inside this websocket

   try:
       while True:
           data = await ws.receive_text()
           print(f"[WS] Received message: {data}")

           # Add user message
           messages.append({"role": "user", "content": data})

           # LLM call
           response = client.chat.completions.create(
               model=model,
               messages=messages,
               tools=tools,
           )

           assistant_msg = response.choices[0].message
           messages.append(assistant_msg)

           # If LLM wants to call tools
           if assistant_msg.tool_calls:
               print(f"[WS] Assistant requested {len(assistant_msg.tool_calls)} tool call(s).")

               results = []

               for tool_call in assistant_msg.tool_calls:
                   fn_name = tool_call.function.name
                   fn_args = json.loads(tool_call.function.arguments)

                   print(f"[WS] Tool call: {fn_name} args={fn_args}")

                   if fn_name in tools_exec:
                       result = tools_exec[fn_name](**fn_args)
                   else:
                       result = f"Error: Unknown tool {fn_name}"

                   results.append(result)

                   messages.append({
                       "tool_call_id": tool_call.id,
                       "role": "tool",
                       "content": str(result),
                   })

               # Follow-up model call
               follow_up = client.chat.completions.create(
                   model=model,
                   messages=messages,
               )
               final_answer = follow_up.choices[0].message
               messages.append(final_answer)

               await ws.send_json({
                   "reply": final_answer.content,
                   "tool_output": results,
               })

           else:
               # Simple model text output
               await ws.send_json({"reply": assistant_msg.content})

   except WebSocketDisconnect:
       print("[WS] Client disconnected")

   finally:
       sandbox.kill()
       print("[WS] Sandbox terminated")

运行服务器

最后,使用Uvicorn启动服务:

if __name__ == "__main__":
   uvicorn.run(app, host="0.0.0.0", port=8000)

至此,服务器组件完成。

扩展程序

扩展是用户与之交互的界面。它由少量文件组成,这些文件协同工作,可以在任何网页上运行。激活后,用户可以直接在他们浏览的页面上与扩展服务器进行实时通信。

扩展包含以下文件:

  • manifest.json:定义扩展的配置和权限
  • background.js:包含服务工作线程逻辑,并处理上下文菜单操作
  • content.js:管理页面内交互并显示助手对话框
  • styles.css:提供页内助手窗口的样式

每个文件都有明确的责任,它们共同构成了一个完整、功能性的扩展。

扩展的工作原理

在实现这些文件之前,让我们快速了解一下扩展从用户角度的运作方式:

Agent Sandbox 插件截图

  1. 用户在任意网页上右键点击,弹出上下文菜单。
  2. 他们从菜单中选择“Agent Sandbox”。
  3. 扩展在页面上打开助手对话框。
  4. 用户点击Connect与扩展服务器建立连接。
  5. 连接建立后,用户可以直接在消息框中输入内容。
  6. 输入消息后,他们只需点击Send即可将其发送到服务器。
  7. 如果他们想向代理提供页面上下文,可以点击Extract来捕获网页上的所有可见内容。
  8. 他们也可以手动添加额外的上下文,然后再次点击Send

代码助手截图

现在我们已经了解了工作流程,让我们开始实现扩展文件。

manifest.json

我们创建的第一个文件是manifest.json,它配置了扩展的权限、后台逻辑和内容脚本。

{
 "manifest_version": 3,
 "name": "Agent Sandbox",
 "version": "1.0",
 "description": "Chat with your Novita AI sandbox agent over WebSocket.",
 "permissions": [
   "contextMenus",
   "activeTab",
   "scripting"
 ],
 "host_permissions": [
   "ws://localhost:8000/*",
   "http://localhost:8000/*",
   "https://localhost:8000/*"
 ],
 "background": {
   "service_worker": "background.js"
 },
 "action": {
   "default_icon": "icon.png"
 },
 "content_scripts": [
   {
     "matches": ["<all_urls>"],
     "js": ["content.js"],
     "css": ["styles.css"]
   }
 ]
}

该文件告诉Chrome要加载哪些脚本、需要哪些权限、以及哪个文件作为服务工作线程。

background.js

后台脚本是在幕后运行的服务工作线程。它负责将我们的扩展添加到Chrome上下文菜单,并监听用户交互。当用户选择我们的菜单选项时,后台脚本会向内容脚本发送一条消息,然后内容脚本激活扩展对话框。

chrome.runtime.onInstalled.addListener(() => {
 chrome.contextMenus.create({
   id: "ask-assistant",
   title: "Agent Sandbox",
   contexts: ["all"]
 });
});

chrome.contextMenus.onClicked.addListener((info, tab) => {
 chrome.scripting.executeScript(
   {
     target: { tabId: tab.id },
     files: ["content.js"]
   },
   () => {
     chrome.tabs.sendMessage(tab.id, {
       type: "OPEN_PANEL"
     });
   }
 );
});

content.js

内容脚本负责在网页内显示扩展对话框。当它收到后台脚本的消息时,会打开对话框。这个脚本是纯JavaScript。它通过标准DOM操作管理UI,并使用WebSocket API与扩展服务器通信。

let socket = null;

chrome.runtime.onMessage.addListener((msg) => {
 if (msg.type === "OPEN_PANEL") {
   openPanel();
 }
});

// -------------------------------------------------
// WEBPAGE TEXT EXTRACTOR
// -------------------------------------------------
function extractWebpageContent() {
 const cloned = document.cloneNode(true);
 cloned.querySelectorAll("script, style, iframe, noscript").forEach(e => e.remove());

 let main =
   cloned.querySelector("article") ||
   cloned.querySelector("main") ||
   cloned.querySelector("#content") ||
   cloned.body;

 const text = Array.from(main.querySelectorAll("h1, h2, h3, p"))
   .map(el => el.innerText.trim())
   .filter(Boolean)
   .join("\n\n");

 return text;
}

// -------------------------------------------------
// PANEL UI
// -------------------------------------------------
function openPanel() {
 const old = document.getElementById("assistant-box");
 if (old) old.remove();

 const box = document.createElement("div");
 box.id = "assistant-box";

 box.innerHTML = `
   <div id="assistant-container">
     <div id="assistant-header">
       <h3>Code Assistant</h3>
       <button id="assistant-close">×</button>
     </div>

     <textarea id="assistant-input" placeholder="Message Agent..."></textarea>

     <div class="btn-row">
       <button id="connect-btn">Connect</button>
       <button id="disconnect-btn">Disconnect</button>
       <button id="send-btn">Send</button>
       <button id="extract-btn">Extract</button>
     </div>

     <div id="assistant-result">Not connected.</div>
   </div>
 `;

 document.body.appendChild(box);

 document.getElementById("assistant-close").onclick = () => box.remove();

 const resultBox = document.getElementById("assistant-result");
 const inputBox = document.getElementById("assistant-input");

 // -------------------------------------------------
 // TEXT SELECTION LISTENER
 // -------------------------------------------------
 document.addEventListener("mouseup", () => {
   const selection = window.getSelection().toString().trim();
   if (!selection) return;

   // Append selected text to message box
   inputBox.value += (inputBox.value ? "\n\n" : "") + selection;

   // Scroll text area to bottom
   inputBox.scrollTop = inputBox.scrollHeight;
 });

 // -------------------------------------------------
 // CONNECT
 // -------------------------------------------------
 document.getElementById("connect-btn").onclick = () => {
   if (socket && socket.readyState === WebSocket.OPEN) {
     resultBox.innerText = "Already connected.";
     return;
   }

   socket = new WebSocket("ws://localhost:8000/ws");

   socket.onopen = () => {
     resultBox.innerText = "Connected to WebSocket.";
   };

   socket.onmessage = (event) => {
     const data = JSON.parse(event.data);
     resultBox.innerText += "\n\nAssistant:\n" + data.reply;
     resultBox.scrollTop = resultBox.scrollHeight;
   };

   socket.onerror = () => {
     resultBox.innerText = "WebSocket error.";
   };

   socket.onclose = () => {
     resultBox.innerText = "Disconnected.";
   };
 };

 // -------------------------------------------------
 // DISCONNECT
 // -------------------------------------------------
 document.getElementById("disconnect-btn").onclick = () => {
   if (socket) socket.close();
 };

 // -------------------------------------------------
 // SEND MESSAGE
 // -------------------------------------------------
 document.getElementById("send-btn").onclick = () => {
   const context = inputBox.value;

   if (!socket || socket.readyState !== WebSocket.OPEN) {
     resultBox.innerText = "Not connected.";
     return;
   }

   socket.send(JSON.stringify({ message: context }));

   inputBox.value = "";

   resultBox.innerText += "\n\nYou:\n" + context;
 };

 // -------------------------------------------------
 // EXTRACT PAGE → ADD TO MESSAGE BOX
 // -------------------------------------------------
 document.getElementById("extract-btn").onclick = () => {
   const extracted = extractWebpageContent();

   if (!extracted || extracted.length < 10) {
     resultBox.innerText = "Could not extract useful content.";
     return;
   }

   // Add extracted text to input box (not sent automatically)
   inputBox.value += (inputBox.value ? "\n\n" : "") + extracted;

   // Scroll text area
   inputBox.scrollTop = inputBox.scrollHeight;

   resultBox.innerText = "📄 Extracted content added to message box.";
 };
}

styles.css

然后我们使用styles.css文件来设置界面样式,并控制扩展在页面上的显示方式。

#assistant-box {

 position: fixed;

 top: 10%;

 right: 10%;

 width: 350px;

 background: #000;

 border: 1px solid #00ff7f;

 border-radius: 8px;

 box-shadow: 0 0 10px rgba(0,255,127,0.5);

 z-index: 999999;

 font-family: monospace;

 color: #00ff7f;

}

#assistant-container {

 padding: 12px;

}

#assistant-header {

 display: flex;

 justify-content: space-between;

 align-items: center;

}

#assistant-close {

 background: transparent;

 border: none;

 font-size: 20px;

 cursor: pointer;

 padding: 0 5px;

 font-weight: bold;

 color: #00ff7f;

}

#assistant-container textarea {

 width: 100%;

 height: 60px;

 margin-top: 8px;

 background: #0d0d0d;

 color: #00ff7f;

 border: 1px solid #00ff7f;

 border-radius: 4px;

 padding: 6px;

 resize: vertical;

 outline: none;

}

#send-btn {

 width: 100%;

 margin-top: 10px;

 background: #00ff7f;

 border: none;

 padding: 10px;

 color: #000;

 font-weight: bold;

 cursor: pointer;

 border-radius: 4px;

 transition: 0.2s;

}

#send-btn:hover {

 background: #00e66a;

}

#assistant-box pre {

 background: #0a0a0a;

 padding: 8px;

 border-radius: 4px;

 max-height: 120px;

 overflow: auto;

 white-space: pre-wrap;

 word-break: break-word;

 margin-top: 5px;

}

#assistant-result {

 margin-top: 12px;

 background: #0d0d0d;

 padding: 8px;

 border-radius: 5px;

 white-space: pre-wrap;

 max-height: 150px;

 overflow: auto;

}

如何设置Chrome扩展

现在所有必需的文件都已就绪,下一步是将扩展加载到Chrome中。请按照以下步骤操作:

  1. 将扩展服务器代码保存到Python文件中。
  2. 安装所有依赖项,并设置服务器所需的环境变量。
  3. 在你的机器上创建一个名为code-assistant-extension的新文件夹,用于存放扩展客户端。
  4. 将以下文件添加到该文件夹中:
    • manifest.json
    • background.js
    • content.js
    • styles.css
  5. 打开Chrome,访问:chrome://extensions/
  6. 在右上角启用开发者模式
  7. 点击加载已解压的扩展程序
  8. 选择包含扩展文件的文件夹。
  9. 扩展将出现在工具栏中。
  10. 在网页上右键点击任何文本选择,即可在上下文菜单中看到Agent Sandbox

你的扩展现在可以使用了。

仓库 - AI 代码助手浏览器扩展

结论

在本文中,我们构建了一个Chrome扩展,它连接到一个由Novita Sandbox驱动的后端,让AI代理能够安全地执行代码并在用户浏览时提供帮助。这种模式不仅限于编程帮助;它还可以用于互动学习工具、调试助手、文档增强等更多场景。

该架构与浏览器无关,这意味着同样的方法可以以最小的改动适配任何现代浏览器。从这里开始,你可以扩展助手的功能、优化用户界面,或添加新的沙箱工具。这个基础为创建功能强大的智能浏览器伴侣打开了大门。

Novita AI是一个领先的AI云平台,为开发者提供易于使用的API以及经济实惠、可靠的GPU基础设施,用于构建和扩展AI应用程序。