想象一下,浏览文档网站或编程教程时,你不再感到孤单。你无需独自应对一切,而是有一个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”。
- 扩展在页面上打开助手对话框。
- 用户点击Connect与扩展服务器建立连接。
- 连接建立后,用户可以直接在消息框中输入内容。
- 输入消息后,他们只需点击Send即可将其发送到服务器。
- 如果他们想向代理提供页面上下文,可以点击Extract来捕获网页上的所有可见内容。
- 他们也可以手动添加额外的上下文,然后再次点击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中。请按照以下步骤操作:
- 将扩展服务器代码保存到Python文件中。
- 安装所有依赖项,并设置服务器所需的环境变量。
- 在你的机器上创建一个名为code-assistant-extension的新文件夹,用于存放扩展客户端。
- 将以下文件添加到该文件夹中:
- manifest.json
- background.js
- content.js
- styles.css
- 打开Chrome,访问:chrome://extensions/
- 在右上角启用开发者模式。
- 点击加载已解压的扩展程序。
- 选择包含扩展文件的文件夹。
- 扩展将出现在工具栏中。
- 在网页上右键点击任何文本选择,即可在上下文菜单中看到Agent Sandbox。
你的扩展现在可以使用了。
仓库 - AI 代码助手浏览器扩展
结论
在本文中,我们构建了一个Chrome扩展,它连接到一个由Novita Sandbox驱动的后端,让AI代理能够安全地执行代码并在用户浏览时提供帮助。这种模式不仅限于编程帮助;它还可以用于互动学习工具、调试助手、文档增强等更多场景。
该架构与浏览器无关,这意味着同样的方法可以以最小的改动适配任何现代浏览器。从这里开始,你可以扩展助手的功能、优化用户界面,或添加新的沙箱工具。这个基础为创建功能强大的智能浏览器伴侣打开了大门。
Novita AI是一个领先的AI云平台,为开发者提供易于使用的API以及经济实惠、可靠的GPU基础设施,用于构建和扩展AI应用程序。
