Manus在浏览器内实时人机交互技术

概述

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/... │                     │
│                └─────────────────┘                     │
└─────────────────────────────────────────────────────────┘

数据流

  1. 「用户编辑」 → Monaco Editor → WebSocket → 后端 → 沙箱文件系统

  2. 「Agent 修改」 → 工具调用 → 沙箱文件系统 → WebSocket → Monaco Editor 刷新

  3. 「文件监听」 → 沙箱 inotify → 后端检测 → WebSocket → 前端更新

关键特性

特性 实现方式
语法高亮 Monaco Editor 内置
代码补全 Monaco + LSP
实时同步 WebSocket 双向通信
差异对比 Monaco DiffEditor
文件树 自定义组件 + 沙箱 API
修改高亮 Monaco Decorations

总结

Manus 浏览器编辑器的核心是:

  1. 「Monaco Editor」 - 提供 VS Code 级别的编辑体验

  2. 「WebSocket」 - 实现前后端实时双向同步

  3. 「沙箱文件系统」 - 作为真实存储,Agent 和用户操作同一份文件

  4. 「工具集成」 - Agent 通过 edit_filereplace_in_file 等工具操作文件,自动同步到前端

相关推荐
猫头虎4 小时前
如何把家里 NAS 挂载到公司电脑当“本地盘”用?(Windows & Mac 通过SMB协议挂载NAS硬盘教程,节点小宝异地组网版)
windows·网络协议·计算机网络·macos·缓存·人机交互·信息与通信
Coovally AI模型快速验证1 天前
从“单例模仿”到“多面融合”,视觉上下文学习迈向“团队协作”式提示融合
人工智能·学习·算法·yolo·计算机视觉·人机交互
木斯佳2 天前
HarmonyOS实战(人机交互篇):当ToB系统开始“思考”,我们该设计什么样的界面?
华为·人机交互·harmonyos
Coco恺撒2 天前
【脑机接口】难在哪里,【人工智能】如何破局(1.用户篇)
人工智能·深度学习·开源·生活·人机交互·智能家居
与光同尘 大道至简3 天前
ESP32 小智 AI 机器人入门教程从原理到实现(自己云端部署)
人工智能·python·单片机·机器人·github·人机交互·visual studio
工业HMI实战笔记4 天前
HMI权限分级设计:兼顾安全与操作效率的平衡术
运维·数据库·安全·ui·自动化·人机交互·交互
Coco恺撒5 天前
【脑机接口+人工智能】阔别三载,温暖归来
人工智能·经验分享·神经网络·人机交互·创业创新·学习方法
深圳博达智联5 天前
博达智联供水4G控制器方案:厂家集中管控,终端用户手机远程控,运维成本降一半
物联网·智能手机·人机交互
cy_cy0025 天前
展厅多屏联动推动智慧展示创新
科技·人机交互·交互·软件构建