开启左侧

Manus复刻!带高仿 WebUI 和沙盒,可本地部署

[复制链接]
在线会员 OZQ 发表于 3 小时前 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题 |快速收录

作家:摇一摇(知乎)
https://zhuanlan.zhihu.com/p/1903248937877477029
>>参加 青稞AI手艺交换群,取年青AI钻研员/开辟者交换最新AI手艺





近来正在进修 LLM Agent,但是末觉“纸上教去末觉浅,尽知此事要躬止”,以是念写个小名目尝尝脚。正在那小我私家均写一个 Manus 的时期,正在那个半启卷的情况下,何况 Manus 的提词汇已经透露的情况下,是否是尔也能够写一个,尔收拾整顿了一下自己的需要以下:
    • 可单主机云上布置,佳布置正在尔野里的 Server 上。• 散成浏览器、Shell、Python、Node 等东西及 Ubuntu 沙盒情况,每个任务分派一个沙盒。• Web UI 取提醒词汇间接借鉴民间 Manus 的。

分离 Cursor 该当能够快速天写出一个 Manus 的示例,传闻 OpenManus 4 小时便写进去了。

名目地点:
https://github.com/Simpleyyt/ai-manus
Demo 示范

Code USE


Prompt:写一个庞大的 python 示例
Browser Use


Prompt: 任务:llm 最新论文

结果能够道是相称的拼集,可是今朝是进修目标,提醒词汇取 Agent 过程可以皆需要劣化一下,懒患上再调了,便接给广阔网友了。

部分设想

Manus复刻!戴下仿 WebUI 战沙盒,可当地布置w2.jpg

部分体系由三个模块构成:Web、Server 取 Sandbox,用户使用过程以下:
当用户倡议对于话时:

    • Web 背 Server 收收创立 Agent 恳求,Server 颠末/var/run/docker.sock创立出 Sandbox,并前去会话 ID。• Sandbox 是一个 Ubuntu Docker 情况,里面会启用 chrome 浏览器及 File/Shell 等东西的 API效劳 。• Web 朝会话 ID 中收收用户消息,Server 支到用户消息后,将消息收收给 PlanAct Agent处置 。• PlanAct Agent处置 过程当中会挪用相干东西完毕任务。• Agent处置 过程当中发生的统统工作颠末 SSE 发还 Web。
当用户浏览东西时:

浏览器:
    • Sandbox 的无头浏览器颠末 xvfb 取 x11vnc 启用了 vnc效劳 ,而且颠末 websockify 将 vnc 转移成 websocket。• Web 的 NoVNC 组件颠末 Server 的 Websocket Forward 转收到 Sandbox,完毕浏览器检察。

别的东西:别的东西道理也是好未几。

AI Agent 设想情势

AI Agent 是甚么?相信各人皆听烂了,尔正在那里道的必然没有如他人道的佳,简朴来讲即是:AI Agent = LLM + Planning + Memory + Tools。

Manus复刻!戴下仿 WebUI 战沙盒,可当地布置w3.jpg

FunctionCall or ReAct or LangChain?

先来讲一下 Tool Use局部 ,今朝能够使用的方法是:
    • 1)下阶模子自己的 FunctionCall才干 ;• 2)ReAct Prompt 框架;• 3)LangChain Agent 框架,实在它是前二者的下度启拆。

最简朴是使用 LangChain 如许下度启拆的框架了,可是关于进修目标的名目来讲,尔不竭剧烈念明白它面前 干了甚么,以是那个先 Pass 失落了。

使用 FunctionCall 去使用东西请求比力下阶的模子,但是 ReAct 的设想又太烦琐,思去念来,仍是先使用 FunctionCall,先用佳的模子开辟,前面再去钻研 ReAct 框架。对于 ReAct ,感兴致能够瞅瞅:ReAct 框架 | Prompt Engineering Guide[1]。

Manus复刻!戴下仿 WebUI 战沙盒,可当地布置w4.jpg

因而,该名目对于 LLM 的请求以下:
    • 兼容 OpenAI 交心• 撑持 FunctionCall• 撑持 Json 格局输出(因为抛弃了 LangChain 又念费事)

Plan-and-Act Agent 设想情势

部分使用 Plan-and-Act 的 Agent 设想情势,相干论文:Plan-and-Act : Improving Planning of Agents for Long-Horizon Tasks[2],它的相干过程以下:

Manus复刻!戴下仿 WebUI 战沙盒,可当地布置w5.jpg

行将体系分红 Planner 战 Executor,Planner 将任务截至计划装分,Executor 担当任务分步施行,将施行成果前去给 Planner 从头计划。

名目中的形状流转图以下:

Manus复刻!戴下仿 WebUI 战沙盒,可当地布置w6.jpg

体系撑持被挨断,统统挨断的消息城市流背 Planner Agent,Planner Agent 为按照用户的挨断消息从头计划。
Sandbox 设想

为了完毕每一个任务使用零丁的 Docker 沙盒,咱们 Server 颠末/var/run/docker.sock截至机械上 Docker 沙盒的创立取烧毁。
Sandbox 历程取性命周期办理

全部 Sandbox 颠末 supervisord 历程办理,而且颠末 supervisord 完毕 Sandbox 的 TTL 办理。因为 Agent 今朝不主动烧毁体制,以是需要 Sandbox 主动过时自烧毁,并完毕绝时等交心。
File & Shell 东西

文献操纵取 Shell 号令施行不甚么易度,Cursor 最善于那些,尔把 Manus 的东西描绘拾给 Cursor 后,很快便用 FastAPI 助尔天生了一整套代码,不能不道很颠簸,根本不如何改正。
Browser 东西

今朝各处的 Manus 皆正在使用 browser-use[3]那个库,为了进修战钻研的目标,尔仍是决定使用 Playwright + Chrome自己 弄一个。因为今朝尚未才气使用望觉模子,以是仍是以笔墨模子为根底去操纵浏览器。

为了让 Sandbox 越发地道,Sandbox 只启用 Chrome,而且表露 CDP 战 VNC 让 Server 去操纵。
启用 Browser

坑面一:启用参数

Chrome browser 的启用有许多参数,碰到成就再找参数有面费时吃力,间接站正在伟人的肩膀上,参照 browser-use 的启用参数:
https://github.com/browser-use/browser-use/blob/main/browser_use/browser/chrome.py
坑面两:CDP 监听地点没有撑持0.0.0.0

新版原 Chrome 仿佛已经没有撑持--remote-debugging-address参数(参照:https://issues.chromium.org/issues/327558594),处置计划能够颠末端心转收:
#假定 CDP 监听正在 127.0.0.1:8222
socat TCP-LISTEN:9222,bind=0.0.0.0,fork,reuseaddr TCP:127.0.0.1:8222VNC拜访

因为 Docker 镜像内乱不 X Server 等图形情况,统统颠末假造 X11 显现效劳器Xvfb去给 Chrome 画造窗心,并颠末x11vnc供给 VNC Server:
# 启用 Xvfb 正在 Display :1
Xvfb :1 -screen 0 1280x1029x24

# Chrome 浏览器指定 Display
谷歌-chrome \
    --display=:1 \
    ...

# 启用 VNC效劳
x11vnc -display :1 -nopw -listen 0.0.0.0 -xkb -forever -rfbport 5900
因为 VNC 的四层端心对于反背代办署理转收没有友好,以是那里借使用websockify将 VNC 转成七层Websocket:
# 将表露 5901 端心 Websocket效劳
websockify 0.0.0.0:5901 localhost:5900
以就于前面NoVNC跟尾。
AI 网页元艳操纵取疑息提炼

一开端灵活天把全部 html 拾给年夜模子,发明是止欠亨的,一拾已往便爆 Token 了。小调研了一下,发明现在支流的作法是:
    • 1)可接互元艳提炼;• 2)网页疑息提炼。

起首是提炼看来的、可接互的元艳,以就让年夜模子识别哪些能够输出、面打等。一般将元艳提炼成index <tag>text</tag>,比方:
1 <input>脚机号</input>
2 <input>暗码</input>
3 <button>确认</button>
...
并正在本标签中把 ID 号标注下来,那里是 Cursor 给尔天生的代码,尔也不细瞅,但是它能 work:
const interactiveElements = [];
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;

// Get all potentially relevant interactive elements
const elements = document.querySelectorAll('button, a, input, textarea, select, [role="button"], [tabindex]:not([tabindex="-1"])');

let validElementIndex = 0; // For generating consecutive indices

for (let i = 0; i < elements.length; i++) {
    const element = elements;
    // Check if the element is in the viewport and visible
    const rect = element.getBoundingClientRect();

    // Element must have some dimensions
    if (rect.width === 0 || rect.height === 0) continue;

    // Element must be within the viewport
    if (
        rect.bottom < 0 ||
        rect.top > viewportHeight ||
        rect.right < 0 ||
        rect.left > viewportWidth
    ) continue;

    // Check if the element is visible (not hidden by CSS)
    const style = window.getComputedStyle(element);
    if (
        style.display === 'none' ||
        style.visibility === 'hidden' ||
        style.opacity === '0'
    ) continue;

    // Get element type and text
    let tagName = element.tagName.toLowerCase();
    let text = '';

    if (element.value && ['input', 'textarea', 'select'].includes(tagName)) {
        text = element.value;

        // Add label and placeholder information for input elements
        if (tagName === 'input') {
            // Get associated label text
            let labelText = '';
            if (element.id) {
                const label = document.querySelector(`label[for="${element.id}"]`);
                if (label) {
                    labelText = label.innerText.trim();
                }
            }

            // Look for parent or sibling label
            if (!labelText) {
                const parentLabel = element.closest('label');
                if (parentLabel) {
                    labelText = parentLabel.innerText.trim().replace(element.value, '').trim();
                }
            }

            // Add label information
            if (labelText) {
                text = `[Label: ${labelText}] ${text}`;
            }

            // Add placeholder information
            if (element.placeholder) {
                text = `${text} [Placeholder: ${element.placeholder}]`;
            }
        }
    } else if (element.innerText) {
        text = element.innerText.trim().replace(/\\s+/g, ' ');
    } else if (element.alt) { // For image buttons
        text = element.alt;
    } else if (element.title) { // For elements with title
        text = element.title;
    } else if (element.placeholder) { // For placeholder text
        text = `[Placeholder: ${element.placeholder}]`;
    } else if (element.type) { // For input type
        text = `[${element.type}]`;

        // Add label and placeholder information for text-less input elements
        if (tagName === 'input') {
            // Get associated label text
            let labelText = '';
            if (element.id) {
                const label = document.querySelector(`label[for="${element.id}"]`);
                if (label) {
                    labelText = label.innerText.trim();
                }
            }

            // Look for parent or sibling label
            if (!labelText) {
                const parentLabel = element.closest('label');
                if (parentLabel) {
                    labelText = parentLabel.innerText.trim();
                }
            }

            // Add label information
            if (labelText) {
                text = `[Label: ${labelText}] ${text}`;
            }

            // Add placeholder information
            if (element.placeholder) {
                text = `${text} [Placeholder: ${element.placeholder}]`;
            }
        }
    } else {
        text = '[No text]';
    }

    // Maximum limit on text length to keep it clear
    if (text.length > 100) {
        text = text.substring(0, 97) + '...';
    }

    // Only add data-manus-id attribute to elements that meet the conditions
    element.setAttribute('data-manus-id', `manus-element-${validElementIndex}`);

    // Build selector - using only data-manus-id
    const selector = `[data-manus-id="manus-element-${validElementIndex}"]`;

    // Add element information to the array
    interactiveElements.push({
        index: validElementIndex,  // Use consecutive index
        tag: tagName,
        text: text,
        selector: selector
    });

    validElementIndex++; // Increment valid element counter
}

return interactiveElements;
如许年夜模子就能够按照 ID 号去操纵元艳了。

借要截至网页疑息提炼,今朝支流作法是先来失落不成睹元艳后,先转成 markdown,再给年夜模子截至提炼,以节流 Token,以下:
# Convert to Markdown
markdown_content = markdownify(visible_content)

max_content_length = min(50000, len(markdown_content))
response = await self.llm.ask([{
    "role": "system",
    "content": "You are a professional web page information extraction assistant. Please extract all information from the current page content and convert it to Markdown format."
},
{
    "role": "user",
    "content": markdown_content[:max_content_length]
}
])
至此,年夜模子就能够取网页接互取浏览网页疑息实质了。

Web UI 设想

Web UI 编辑固然属于尔的硬肋,但是属于 Cursor 的刚强,分离对于邪版 Manus 的借鉴也能够弄患上七七八八,页里比力简朴。
怎样布置?

情况请求

原名目主要依靠 Docker中止 开辟取布置,需要装置较新版原的 Docker:
    • Docker 20.10+• Docker Compose

模子才气也是请求比力下:
    • 兼容 OpenAI 交心• 撑持 FunctionCall• 撑持 Json Format输出

举荐 Deepseek 取 ChatGPT。

布置

举荐使用 Docker Compose中止 布置:
services:
  frontend:
    image: simpleyyt/manus-frontend
    ports:
      - "5173:80"
    depends_on:
      - backend
    restart: unless-stopped
    networks:
      - manus-network
    environment:
      - BACKEND_URL=http://backend:8000

  backend:
    image: simpleyyt/manus-backend
    depends_on:
      - sandbox
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - manus-network
    environment:
      # OpenAI API base URL
      - API_BASE=https://api.openai.com/v1
      # OpenAI API key, replace with your own
      - API_KEY=sk-xxxx
      # LLM model name
      - MODEL_NAME=gpt-4o
      # LLM temperature parameter, controls randomness
      - TEMPERATURE=0.7
      # Maximum tokens for LLM response
      - MAX_TOKENS=2000
      # Google Search API key for web search capability
      #- GOOGLE_SEARCH_API_KEY=
      # Google Custom Search Engine ID
      #- GOOGLE_SEARCH_ENGINE_ID=
      # Application log level
      - LOG_LEVEL=INFO
      # Docker image used for the sandbox
      - SANDBOX_IMAGE=simpleyyt/manus-sandbox
      # Prefix for sandbox container names
      - SANDBOX_NAME_PREFIX=sandbox
      # Time-to-live for sandbox containers in minutes
      - SANDBOX_TTL_MINUTES=30
      # Docker network for sandbox containers
      - SANDBOX_NETWORK=manus-network

  sandbox:
    image: simpleyyt/manus-sandbox
    co妹妹and: /bin/sh -c "exit 0"  # prevent sandbox from starting, ensure image is pulled
    restart: "no"
    networks:
      - manus-network

networks:
  manus-network:
    name: manus-network
    driver: bridge
保留成 docker-compose.yml 文献,并运行:
docker compose up -d
留神:假设提醒 sandbox-1 exited with code 0,那是一般的,那是为了让 sandbox 镜像胜利推与到当地。

翻开浏览器会见 http://localhost:5173 便可会见 Manus。

怎样开辟?

情况准备

情况请求正在布置章节已经干了分析。

下载名目:
git clone https://github.com/Simpleyyt/ai-manus.git
cd ai-manus
复造设置文献:
cp .env.example .env
改正设置文献:
# Model provider configuration
API_KEY=
API_BASE=https://api.openai.com/v1

# Model configuration
MODEL_NAME=gpt-4o
TEMPERATURE=0.7
MAX_TOKENS=2000

# Optional: Google search configuration
#GOOGLE_SEARCH_API_KEY=
#GOOGLE_SEARCH_ENGINE_ID=

# Sandbox configuration
SANDBOX_IMAGE=simpleyyt/sandbox
SANDBOX_NAME_PREFIX=sandbox
SANDBOX_TTL_MINUTES=30
SANDBOX_NETWORK=manus-network

# Log configuration
LOG_LEVEL=INFO开辟

开辟情势下只会全部启用一个沙盒。

运行调试:
# 相称于 docker compose -f docker-compose-development.yaml up
./dev.sh up
Web、Sandbox、Server 城市以 reload方式 运行,即代码窜改会主动 reload。表露的端心以下:
    • 5173: Web 前端端心。• 8000: Server API效劳 端心。• 8080: Sandbox API效劳 端心。• 5900: Sandbox VNC 端心。• 9222: Sandbox Chrome 浏览器 CDP 端心。

当依靠变革时,即requirements.txt大概package.json变革时,能够清理偏重 新建立一下:
# 清理失落统统相干资本
./dev.sh down -v

# 从头建立镜像
./dev.sh build

# 调试运行
./dev.sh up公布

export IMAGE_REGISTRY=your-registry-url
export IMAGE_TAG=latest

# 建立镜像
./run build

# 拉收到响应的镜像堆栈
./run push
写正在最初

原名目主要用于进修取钻研目标,配合窗习战进步,也是代码工程师未来跃变提词汇工程师干面准备。
引用链交

[1] ReAct 框架 | Prompt Engineering Guide: https://www.promptingguide.ai/zh/techniques/react
[2] Plan-and-Act : Improving Planning of Agents for Long-Horizon Tasks: https://arxiv.org/html/2503.09572v1
[3] browser-use: https://github.com/browser-use/browser-use

朝期举荐

Manus复刻!戴下仿 WebUI 战沙盒,可当地布置w7.jpg

Lilian Weng 最新万字少文:从 CoT 到 Aha Moment,掀秘年夜模子「思考」的玄妙!



从字节、百川、Bespoke Labs 3个年夜模子名目,瞅RL启动下的Agent手艺趋势



Agentic 是个谎话,素质仍是典范RL



比照Manus、OpenManus取OWL手艺架构



Manus爆水后的思考:可否能够颠末RL微调LLM去得到决议计划年夜模子



25年甚么样的 Agent 会崭露头角:简朴胜于庞大



对于Agent的思考-2025年



皆瞅到那了,面个存眷再走吧🧐~
您需要登录后才可以回帖 登录 | 立即注册 qq_login

本版积分规则

avatar

关注0

粉丝0

帖子104

发布主题
阅读排行更多+
用专业创造成效
400-778-7781
周一至周五 9:00-18:00
意见反馈:server@mailiao.group
紧急联系:181-67184787
ftqrcode

扫一扫关注我们

Powered by 职贝云数A新零售门户 X3.5© 2004-2025 职贝云数 Inc.( 蜀ICP备2024104722号 )