概述
Manus 在浏览器中嵌入代码编辑器,让用户可以实时查看和编辑 Agent 操作的文件。核心方案是 「Monaco Editor + WebSocket + 沙箱文件系统」。
核心技术方案
1. Monaco Editor 集成
Monaco Editor 是 VS Code 的核心编辑器,提供完整的代码编辑能力。
go
import * as monaco from'monaco-editor';
// 创建编辑器实例
const editor = monaco.editor.create(document.getElementById('container')!, {
value: '',
language: 'python',
theme: 'vs-dark',
automaticLayout: true,
minimap: { enabled: true },
fontSize: 14,
lineNumbers: 'on',
scrollBeyondLastLine: false,
});
// 监听内容变化
editor.onDidChangeModelContent((e) => {
const content = editor.getValue();
// 同步到后端文件系统
syncToBackend(content);
});
// 设置语言
function setLanguage(filePath: string) {
const ext = filePath.split('.').pop();
const langMap: Record<string, string> = {
'py': 'python',
'js': 'javascript',
'ts': 'typescript',
'go': 'go',
'rs': 'rust',
'md': 'markdown',
};
monaco.editor.setModelLanguage(
editor.getModel()!,
langMap[ext || ''] || 'plaintext'
);
}
2. WebSocket 实时同步
go
// 前端 WebSocket 连接
class EditorSync {
private ws: WebSocket;
private editor: monaco.editor.IStandaloneCodeEditor;
private currentFile: string = '';
constructor(editor: monaco.editor.IStandaloneCodeEditor) {
this.editor = editor;
this.ws = new WebSocket('wss://api.manus.ai/editor');
this.setupListeners();
}
private setupListeners() {
this.ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case'file_content':
// 打开文件
this.editor.setValue(msg.content);
this.currentFile = msg.path;
break;
case'file_changed':
// Agent 修改了文件
if (msg.path === this.currentFile) {
this.showDiff(msg.content);
}
break;
case'file_created':
// Agent 创建了新文件
this.openFile(msg.path);
break;
case'highlight_range':
// 高亮显示修改区域
this.highlightRange(msg.range);
break;
}
};
}
openFile(path: string) {
this.ws.send(JSON.stringify({
action: 'open_file',
path: path
}));
}
saveFile() {
this.ws.send(JSON.stringify({
action: 'save_file',
path: this.currentFile,
content: this.editor.getValue()
}));
}
private showDiff(newContent: string) {
const oldContent = this.editor.getValue();
// 显示差异或直接更新
this.editor.setValue(newContent);
}
private highlightRange(range: monaco.IRange) {
this.editor.deltaDecorations([], [{
range: new monaco.Range(
range.startLineNumber,
range.startColumn,
range.endLineNumber,
range.endColumn
),
options: {
className: 'highlight-change',
isWholeLine: false
}
}]);
}
}
3. 后端文件同步服务
go
import asyncio
from pathlib import Path
from dataclasses import dataclass
from typing import Optional
@dataclass
class FileChange:
path: str
content: str
source: str # 'user' or 'agent'
class EditorSyncHandler:
"""编辑器同步处理器"""
def __init__(self, sandbox, websocket):
self.sandbox = sandbox
self.websocket = websocket
self.watch_task: Optional[asyncio.Task] = None
asyncdef handle_message(self, message: dict):
"""处理前端消息"""
action = message.get('action')
if action == 'open_file':
await self.handle_open(message['path'])
elif action == 'save_file':
await self.handle_save(message['path'], message['content'])
elif action == 'list_files':
await self.handle_list(message.get('path', '/'))
asyncdef handle_open(self, file_path: str):
"""打开文件"""
try:
content = await self.sandbox.read_file(file_path)
await self.websocket.send_json({
'type': 'file_content',
'path': file_path,
'content': content
})
except FileNotFoundError:
await self.websocket.send_json({
'type': 'error',
'message': f'File not found: {file_path}'
})
asyncdef handle_save(self, file_path: str, content: str):
"""保存文件"""
await self.sandbox.write_file(file_path, content)
await self.websocket.send_json({
'type': 'file_saved',
'path': file_path
})
asyncdef handle_list(self, dir_path: str):
"""列出目录"""
files = await self.sandbox.list_files(dir_path)
await self.websocket.send_json({
'type': 'file_tree',
'path': dir_path,
'files': files
})
asyncdef notify_file_change(self, file_path: str, content: str):
"""通知前端文件变化(Agent 修改时调用)"""
await self.websocket.send_json({
'type': 'file_changed',
'path': file_path,
'content': content
})
asyncdef start_watching(self, dir_path: str = '/workspace'):
"""监听文件变化"""
self.watch_task = asyncio.create_task(
self._watch_files(dir_path)
)
asyncdef _watch_files(self, dir_path: str):
"""文件监听循环"""
# 使用 inotify 或轮询检测文件变化
last_state = await self._get_file_state(dir_path)
whileTrue:
await asyncio.sleep(0.5)
current_state = await self._get_file_state(dir_path)
# 检测变化
for path, mtime in current_state.items():
if path notin last_state or last_state[path] != mtime:
content = await self.sandbox.read_file(path)
await self.notify_file_change(path, content)
last_state = current_state
asyncdef _get_file_state(self, dir_path: str) -> dict[str, float]:
"""获取目录下所有文件的修改时间"""
result = {}
files = await self.sandbox.list_files(dir_path, recursive=True)
for f in files:
if f['type'] == 'file':
result[f['path']] = f['mtime']
return result
4. Agent 文件操作工具
go
class FileEditTool:
"""文件编辑工具"""
name = "edit_file"
description = "Create or overwrite a file in the sandbox"
def __init__(self, sandbox, editor_sync: EditorSyncHandler):
self.sandbox = sandbox
self.editor_sync = editor_sync
asyncdef execute(self, file_path: str, content: str) -> str:
# 1. 写入沙箱文件系统
await self.sandbox.write_file(file_path, content)
# 2. 通知前端编辑器刷新
await self.editor_sync.notify_file_change(file_path, content)
returnf"File {file_path} created/updated"
class CodeReplaceTool:
"""代码替换工具 - 精准编辑"""
name = "replace_in_file"
description = "Replace specific text in a file"
def __init__(self, sandbox, editor_sync: EditorSyncHandler):
self.sandbox = sandbox
self.editor_sync = editor_sync
asyncdef execute(
self,
file_path: str,
old_str: str,
new_str: str
) -> str:
# 读取文件
content = await self.sandbox.read_file(file_path)
# 检查目标字符串是否存在
if old_str notin content:
returnf"Error: Target string not found in {file_path}"
# 计算替换位置(用于高亮)
start_index = content.index(old_str)
lines_before = content[:start_index].count('\n')
# 执行替换
new_content = content.replace(old_str, new_str, 1)
await self.sandbox.write_file(file_path, new_content)
# 通知前端并高亮
await self.editor_sync.websocket.send_json({
'type': 'file_changed',
'path': file_path,
'content': new_content,
'highlight': {
'startLine': lines_before + 1,
'endLine': lines_before + new_str.count('\n') + 1
}
})
returnf"Replaced in {file_path}"
class FileReadTool:
"""文件读取工具"""
name = "read_file"
description = "Read file content from sandbox"
def __init__(self, sandbox):
self.sandbox = sandbox
asyncdef execute(
self,
file_path: str,
offset: int = 0,
limit: int = None
) -> str:
content = await self.sandbox.read_file(file_path)
lines = content.split('\n')
if limit:
lines = lines[offset:offset + limit]
# 返回带行号的内容
numbered = [f"{i + offset + 1}|{line}"for i, line in enumerate(lines)]
return'\n'.join(numbered)
5. 差异对比编辑器
go
// 显示 Agent 修改的差异
class DiffViewer {
private diffEditor: monaco.editor.IStandaloneDiffEditor | null = null;
show(container: HTMLElement, oldContent: string, newContent: string) {
this.diffEditor = monaco.editor.createDiffEditor(container, {
originalEditable: false,
renderSideBySide: true,
automaticLayout: true,
});
this.diffEditor.setModel({
original: monaco.editor.createModel(oldContent, 'python'),
modified: monaco.editor.createModel(newContent, 'python'),
});
}
accept() {
// 用户接受修改
const modified = this.diffEditor?.getModifiedEditor().getValue();
this.dispose();
return modified;
}
reject() {
// 用户拒绝修改
const original = this.diffEditor?.getOriginalEditor().getValue();
this.dispose();
return original;
}
dispose() {
this.diffEditor?.dispose();
this.diffEditor = null;
}
}
6. 文件树组件
go
interface FileNode {
name: string;
path: string;
type: 'file' | 'directory';
children?: FileNode[];
}
class FileExplorer {
private tree: FileNode[] = [];
private onFileSelect: (path: string) =>void;
constructor(container: HTMLElement, onSelect: (path: string) => void) {
this.onFileSelect = onSelect;
this.render(container);
}
async refresh(ws: WebSocket) {
ws.send(JSON.stringify({ action: 'list_files', path: '/' }));
}
updateTree(files: FileNode[]) {
this.tree = files;
this.renderTree();
}
private renderTree() {
// 渲染文件树 UI
// 点击文件时调用 this.onFileSelect(path)
}
}
整体架构
go
┌─────────────────────────────────────────────────────────┐
│ 浏览器前端 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ File Explorer│ │ Monaco Editor│ │ Terminal │ │
│ │ 文件树 │ │ 代码编辑器 │ │ xterm.js │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
│ WebSocket 连接 │
└───────────────────────────┼─────────────────────────────┘
│
┌───────────────────────────┼─────────────────────────────┐
│ │ 后端服务 │
│ ┌────────▼────────┐ │
│ │ WebSocket Server│ │
│ └────────┬────────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │EditorSync │ │ Agent │ │ Terminal │ │
│ │ Handler │ │ Engine │ │ PTY │ │
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Sandbox │ │
│ │ File System │ │
│ │ /workspace/... │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
数据流
-
「用户编辑」 → Monaco Editor → WebSocket → 后端 → 沙箱文件系统
-
「Agent 修改」 → 工具调用 → 沙箱文件系统 → WebSocket → Monaco Editor 刷新
-
「文件监听」 → 沙箱 inotify → 后端检测 → WebSocket → 前端更新
关键特性
| 特性 | 实现方式 |
|---|---|
| 语法高亮 | Monaco Editor 内置 |
| 代码补全 | Monaco + LSP |
| 实时同步 | WebSocket 双向通信 |
| 差异对比 | Monaco DiffEditor |
| 文件树 | 自定义组件 + 沙箱 API |
| 修改高亮 | Monaco Decorations |
总结
Manus 浏览器编辑器的核心是:
-
「Monaco Editor」 - 提供 VS Code 级别的编辑体验
-
「WebSocket」 - 实现前后端实时双向同步
-
「沙箱文件系统」 - 作为真实存储,Agent 和用户操作同一份文件
-
「工具集成」 - Agent 通过
edit_file、replace_in_file等工具操作文件,自动同步到前端