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 等工具操作文件,自动同步到前端

相关推荐
兵慌码乱9 天前
基于 MediaPipe 与 PySide2 的手势交互音乐控制系统实现:轻量化视觉交互全流程解析
python·opencv·计算机视觉·人机交互·手势识别·mediapipe·pyside2
BSD_HY14 天前
2026年FSR传感器行业报告:市场规模、竞争格局与发展趋势
人机交互·制造·fsr·薄膜开关·深圳工厂
某林21214 天前
从 Isaac Lab API 踩坑到硬件 MVP 的全链路实战破局
python·机器人·人机交互·ros2
洛星核16 天前
CrewAI 安装、使用方法详细全解
人工智能·github·人机交互·ai编程·agi·智能体
洛星核16 天前
Aider 安装、使用方法详细全解
人工智能·github·人机交互·ai编程·agi
Mr..Jackey17 天前
瑞佑 RUI Builder 图形化 UI 设计工具
arm开发·人工智能·单片机·ui·人机交互·ra8889·lcd控制芯片
元岳数字人小元17 天前
AI 数字人开发公司浅谈 虚拟数字人打造景区新服务
人工智能·人机交互·交互
小玮看世界17 天前
【技术成长实录】北京地铁12号线数据分析系统:从一个观察到一个完整项目的演进之路
python·人机交互·学习方法·cicd·项目交付
byte轻骑兵17 天前
【AVRCP】规范精讲[28]:媒体源上电全流程,蓝牙音频控制启动就靠这一套
网络·音视频·人机交互·媒体·avrcp
kaixinshier18 天前
【无标题】
大模型·人机交互·语音识别·tts·s100p