├── .env.example ├── .github └── workflows │ └── docker-build-and-push.yml ├── .gitignore ├── README.md ├── README_zh.md ├── backend ├── Dockerfile ├── README.md ├── README_zh.md ├── app │ ├── __init__.py │ ├── application │ │ ├── __init__.py │ │ ├── errors │ │ │ └── exceptions.py │ │ └── services │ │ │ ├── __init__.py │ │ │ └── agent_service.py │ ├── domain │ │ ├── __init__.py │ │ ├── events │ │ │ ├── __init__.py │ │ │ └── agent_events.py │ │ ├── external │ │ │ ├── __init__.py │ │ │ ├── browser.py │ │ │ ├── llm.py │ │ │ ├── message_queue.py │ │ │ ├── sandbox.py │ │ │ ├── search.py │ │ │ └── task.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── agent.py │ │ │ ├── memory.py │ │ │ ├── plan.py │ │ │ ├── session.py │ │ │ └── tool_result.py │ │ ├── repositories │ │ │ ├── agent_repository.py │ │ │ └── session_repository.py │ │ ├── services │ │ │ ├── __init__.py │ │ │ ├── agent_domain_service.py │ │ │ ├── agent_task_runner.py │ │ │ ├── agents │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── execution.py │ │ │ │ └── planner.py │ │ │ ├── flows │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ └── plan_act.py │ │ │ ├── prompts │ │ │ │ ├── __init__.py │ │ │ │ ├── execution.py │ │ │ │ └── planner.py │ │ │ └── tools │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── browser.py │ │ │ │ ├── file.py │ │ │ │ ├── message.py │ │ │ │ ├── plan.py │ │ │ │ ├── search.py │ │ │ │ └── shell.py │ │ └── utils │ │ │ └── json_parser.py │ ├── infrastructure │ │ ├── __init__.py │ │ ├── config.py │ │ ├── external │ │ │ ├── __init__.py │ │ │ ├── browser │ │ │ │ └── playwright_browser.py │ │ │ ├── llm │ │ │ │ └── openai_llm.py │ │ │ ├── message_queue │ │ │ │ └── redis_stream_queue.py │ │ │ ├── sandbox │ │ │ │ └── docker_sandbox.py │ │ │ ├── search │ │ │ │ └── google_search.py │ │ │ └── task │ │ │ │ └── redis_task.py │ │ ├── logging.py │ │ ├── models │ │ │ └── documents.py │ │ ├── repositories │ │ │ ├── mongo_agent_repository.py │ │ │ └── mongo_session_repository.py │ │ ├── storage │ │ │ ├── mongodb.py │ │ │ └── redis.py │ │ └── utils │ │ │ └── llm_json_parser.py │ ├── interfaces │ │ ├── api │ │ │ └── routes.py │ │ ├── errors │ │ │ └── exception_handlers.py │ │ └── schemas │ │ │ ├── event.py │ │ │ ├── request.py │ │ │ └── response.py │ └── main.py ├── dev.sh ├── requirements.txt └── run.sh ├── build.sh ├── dev.sh ├── docker-compose-development.yml ├── docker-compose-example.yml ├── docker-compose.yml ├── frontend ├── .dockerignore ├── Dockerfile ├── README.md ├── README_zh.md ├── docker-entrypoint.sh ├── env.d.ts ├── index.html ├── nginx.conf ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── chatting.svg │ └── favicon.ico ├── src │ ├── App.vue │ ├── api │ │ ├── agent.ts │ │ └── client.ts │ ├── assets │ │ ├── global.css │ │ └── theme.css │ ├── components │ │ ├── BrowserToolView.vue │ │ ├── ChatBox.vue │ │ ├── ChatMessage.vue │ │ ├── ContextMenu.vue │ │ ├── CustomDialog.vue │ │ ├── FileToolView.vue │ │ ├── Panel.vue │ │ ├── PlanPanel.vue │ │ ├── SearchToolView.vue │ │ ├── SessionItem.vue │ │ ├── ShellToolView.vue │ │ ├── SimpleBar.vue │ │ ├── Toast.vue │ │ ├── ToolPanel.vue │ │ ├── ToolUse.vue │ │ └── icons │ │ │ ├── AttachmentIcon.vue │ │ │ ├── BrowserIcon.vue │ │ │ ├── EditIcon.vue │ │ │ ├── ErrorIcon.vue │ │ │ ├── InfoIcon.vue │ │ │ ├── ManusIcon.vue │ │ │ ├── ManusLogoTextIcon.vue │ │ │ ├── ManusTextIcon.vue │ │ │ ├── SearchIcon.vue │ │ │ ├── SendIcon.vue │ │ │ ├── ShellIcon.vue │ │ │ ├── SpinnigIcon.vue │ │ │ ├── StepSuccessIcon.vue │ │ │ └── SuccessIcon.vue │ ├── composables │ │ ├── useContextMenu.ts │ │ ├── useDialog.ts │ │ ├── useI18n.ts │ │ ├── usePanelState.ts │ │ ├── useTime.ts │ │ └── useTool.ts │ ├── constants │ │ └── tool.ts │ ├── locales │ │ ├── en.ts │ │ ├── index.ts │ │ └── zh.ts │ ├── main.ts │ ├── pages │ │ ├── ChatPage.vue │ │ └── HomePage.vue │ ├── types │ │ ├── event.ts │ │ ├── message.ts │ │ ├── panel.ts │ │ └── response.ts │ └── utils │ │ ├── time.ts │ │ └── toast.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── mockserver ├── Dockerfile ├── dev.sh ├── main.py ├── mock_datas │ ├── browser_tools.yaml │ ├── default.yaml │ ├── file_tools.yaml │ ├── message_tools.yaml │ ├── search_tools.yaml │ └── shell_tools.yaml └── requirements.txt ├── run.sh └── sandbox ├── Dockerfile ├── README.md ├── README_zh.md ├── app ├── __init__.py ├── api │ ├── __init__.py │ ├── router.py │ └── v1 │ │ ├── __init__.py │ │ ├── file.py │ │ ├── shell.py │ │ └── supervisor.py ├── core │ ├── __init__.py │ ├── config.py │ ├── exceptions.py │ └── middleware.py ├── main.py ├── models │ ├── __init__.py │ ├── file.py │ ├── shell.py │ └── supervisor.py ├── schemas │ ├── __init__.py │ ├── file.py │ ├── response.py │ └── shell.py └── services │ ├── __init__.py │ ├── file.py │ ├── shell.py │ └── supervisor.py ├── requirements.txt └── supervisord.conf /.env.example: -------------------------------------------------------------------------------- 1 | # Model provider configuration 2 | API_KEY= 3 | API_BASE=http://mockserver:8090/v1 4 | 5 | # Model configuration 6 | MODEL_NAME=deepseek-chat 7 | TEMPERATURE=0.7 8 | MAX_TOKENS=2000 9 | 10 | # MongoDB configuration 11 | #MONGODB_URI=mongodb://mongodb:27017 12 | #MONGODB_DATABASE=manus 13 | #MONGODB_USERNAME= 14 | #MONGODB_PASSWORD= 15 | 16 | # Redis configuration 17 | #REDIS_HOST=redis 18 | #REDIS_PORT=6379 19 | #REDIS_DB=0 20 | #REDIS_PASSWORD= 21 | 22 | # Sandbox configuration 23 | #SANDBOX_ADDRESS= 24 | SANDBOX_IMAGE=simpleyyt/manus-sandbox 25 | SANDBOX_NAME_PREFIX=sandbox 26 | SANDBOX_TTL_MINUTES=30 27 | SANDBOX_NETWORK=manus-network 28 | #SANDBOX_CHROME_ARGS= 29 | #SANDBOX_HTTPS_PROXY= 30 | #SANDBOX_HTTP_PROXY= 31 | #SANDBOX_NO_PROXY= 32 | 33 | # Optional: Google search configuration 34 | #GOOGLE_SEARCH_API_KEY= 35 | #GOOGLE_SEARCH_ENGINE_ID= 36 | 37 | # Log configuration 38 | LOG_LEVEL=INFO -------------------------------------------------------------------------------- /.github/workflows/docker-build-and-push.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker Images to Docker Hub 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | tags: 9 | - 'v*' 10 | pull_request: 11 | branches: 12 | - main 13 | - develop 14 | 15 | env: 16 | REGISTRY: docker.io 17 | IMAGE_REGISTRY: simpleyyt 18 | 19 | jobs: 20 | build-and-push: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | component: [frontend, backend, sandbox] 25 | 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v4 29 | 30 | - name: Set up Docker Buildx 31 | uses: docker/setup-buildx-action@v3 32 | 33 | - name: Log in to Docker Hub 34 | if: github.event_name != 'pull_request' 35 | uses: docker/login-action@v3 36 | with: 37 | registry: ${{ env.REGISTRY }} 38 | username: ${{ secrets.DOCKERHUB_USERNAME }} 39 | password: ${{ secrets.DOCKERHUB_TOKEN }} 40 | 41 | - name: Extract metadata 42 | id: meta 43 | uses: docker/metadata-action@v5 44 | with: 45 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_REGISTRY }}/manus-${{ matrix.component }} 46 | tags: | 47 | type=ref,event=branch 48 | type=ref,event=pr 49 | type=semver,pattern={{version}} 50 | type=semver,pattern={{major}}.{{minor}} 51 | type=semver,pattern={{major}} 52 | type=raw,value=latest,enable={{is_default_branch}} 53 | 54 | - name: Build and push Docker image 55 | uses: docker/build-push-action@v5 56 | with: 57 | context: ./${{ matrix.component }} 58 | file: ./${{ matrix.component }}/Dockerfile 59 | platforms: linux/amd64,linux/arm64 60 | push: ${{ github.event_name != 'pull_request' }} 61 | tags: ${{ steps.meta.outputs.tags }} 62 | labels: ${{ steps.meta.outputs.labels }} 63 | cache-from: type=gha 64 | cache-to: type=gha,mode=max 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual Environment 24 | venv/ 25 | env/ 26 | ENV/ 27 | .env 28 | .venv 29 | env.bak/ 30 | venv.bak/ 31 | 32 | # IDE 33 | .idea/ 34 | .cursor/ 35 | .vscode/ 36 | *.swp 37 | *.swo 38 | .DS_Store 39 | 40 | # Jupyter Notebook 41 | .ipynb_checkpoints 42 | 43 | # Testing 44 | .coverage 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Logs 58 | *.log 59 | logs/ 60 | 61 | # Local development settings 62 | .env.local 63 | .env.development.local 64 | .env.test.local 65 | .env.production.local 66 | 67 | # Distribution / packaging 68 | .Python 69 | build/ 70 | develop-eggs/ 71 | dist/ 72 | downloads/ 73 | eggs/ 74 | .eggs/ 75 | lib/ 76 | lib64/ 77 | parts/ 78 | sdist/ 79 | var/ 80 | wheels/ 81 | *.egg-info/ 82 | .installed.cfg 83 | *.egg 84 | 85 | # Unit test / coverage reports 86 | htmlcov/ 87 | .tox/ 88 | .coverage 89 | .coverage.* 90 | .cache 91 | nosetests.xml 92 | coverage.xml 93 | *.cover 94 | .hypothesis/ 95 | 96 | # Node.js 97 | node_modules/ 98 | npm-debug.log* 99 | yarn-debug.log* 100 | yarn-error.log* 101 | .pnpm-debug.log* 102 | .npm 103 | .yarn 104 | .yarnrc 105 | .pnp.* 106 | .next/ 107 | out/ 108 | .nuxt/ 109 | dist/ 110 | .cache/ 111 | .parcel-cache/ 112 | .vercel/ 113 | .turbo/ 114 | storybook-static/ 115 | coverage/ 116 | .nyc_output/ 117 | .env*.local 118 | .vite/ 119 | docker-compose-test.yml 120 | *.code-workspace 121 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim 2 | 3 | WORKDIR /app 4 | 5 | # Install dependencies 6 | COPY requirements.txt . 7 | RUN pip install --no-cache-dir -r requirements.txt 8 | 9 | # Copy project files 10 | COPY . . 11 | 12 | # Set script execution permissions 13 | RUN chmod +x run.sh 14 | 15 | # Set environment variables 16 | ENV PYTHONPATH=/app 17 | 18 | # Expose port 19 | EXPOSE 8000 20 | 21 | # Start command 22 | CMD ["./run.sh"] -------------------------------------------------------------------------------- /backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Manus AI Agent Backend 3 | """ 4 | -------------------------------------------------------------------------------- /backend/app/application/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/application/__init__.py -------------------------------------------------------------------------------- /backend/app/application/errors/exceptions.py: -------------------------------------------------------------------------------- 1 | class AppException(RuntimeError): 2 | def __init__( 3 | self, 4 | code: int, 5 | msg: str, 6 | status_code: int = 400, 7 | ): 8 | super().__init__(msg) 9 | self.code = code 10 | self.msg = msg 11 | self.status_code = status_code 12 | 13 | 14 | class NotFoundError(AppException): 15 | def __init__(self, msg: str = "Resource not found"): 16 | super().__init__(code=404, msg=msg, status_code=404) 17 | 18 | 19 | class BadRequestError(AppException): 20 | def __init__(self, msg: str = "Bad request parameters"): 21 | super().__init__(code=400, msg=msg, status_code=400) 22 | 23 | 24 | class ServerError(AppException): 25 | def __init__(self, msg: str = "Internal server error"): 26 | super().__init__(code=500, msg=msg, status_code=500) 27 | 28 | 29 | class UnauthorizedError(AppException): 30 | def __init__(self, msg: str = "Unauthorized"): 31 | super().__init__(code=401, msg=msg, status_code=401) -------------------------------------------------------------------------------- /backend/app/application/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/application/services/__init__.py -------------------------------------------------------------------------------- /backend/app/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/domain/__init__.py -------------------------------------------------------------------------------- /backend/app/domain/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/domain/events/__init__.py -------------------------------------------------------------------------------- /backend/app/domain/events/agent_events.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from typing import Dict, Any, Literal, Optional, Union 3 | from datetime import datetime 4 | import time 5 | import uuid 6 | from enum import Enum 7 | from app.domain.models.plan import Plan, Step 8 | 9 | 10 | class PlanStatus(str, Enum): 11 | """Plan status enum""" 12 | CREATED = "created" 13 | UPDATED = "updated" 14 | COMPLETED = "completed" 15 | 16 | 17 | class StepStatus(str, Enum): 18 | """Step status enum""" 19 | STARTED = "started" 20 | FAILED = "failed" 21 | COMPLETED = "completed" 22 | 23 | 24 | class ToolStatus(str, Enum): 25 | """Tool status enum""" 26 | CALLING = "calling" 27 | CALLED = "called" 28 | 29 | 30 | class BaseEvent(BaseModel): 31 | """Base class for agent events""" 32 | type: str 33 | id: str = Field(default_factory=lambda: str(uuid.uuid4())) 34 | timestamp: datetime = Field(default_factory=lambda: datetime.now()) 35 | 36 | class ErrorEvent(BaseEvent): 37 | """Error event""" 38 | type: Literal["error"] = "error" 39 | error: str 40 | 41 | class PlanEvent(BaseEvent): 42 | """Plan related events""" 43 | type: Literal["plan"] = "plan" 44 | plan: Plan 45 | status: PlanStatus 46 | step: Optional[Step] = None 47 | 48 | class ToolEvent(BaseEvent): 49 | """Tool related events""" 50 | type: Literal["tool"] = "tool" 51 | tool_name: str 52 | function_name: str 53 | function_args: Dict[str, Any] 54 | status: ToolStatus 55 | function_result: Optional[Any] = None 56 | 57 | class TitleEvent(BaseEvent): 58 | """Title event""" 59 | type: Literal["title"] = "title" 60 | title: str 61 | 62 | class StepEvent(BaseEvent): 63 | """Step related events""" 64 | type: Literal["step"] = "step" 65 | step: Step 66 | status: StepStatus 67 | 68 | class MessageEvent(BaseEvent): 69 | """Message event""" 70 | type: Literal["message"] = "message" 71 | role: Literal["user", "assistant"] = "assistant" 72 | message: str 73 | 74 | class DoneEvent(BaseEvent): 75 | """Done event""" 76 | type: Literal["done"] = "done" 77 | 78 | AgentEvent = Union[ 79 | BaseEvent, 80 | ErrorEvent, 81 | PlanEvent, 82 | ToolEvent, 83 | StepEvent, 84 | MessageEvent, 85 | DoneEvent, 86 | TitleEvent 87 | ] 88 | 89 | 90 | class AgentEventFactory: 91 | """Factory class for JSON conversion and AgentEvent manipulation""" 92 | 93 | @staticmethod 94 | def from_json(event_str: str) -> AgentEvent: 95 | """Create an AgentEvent from JSON string""" 96 | event = BaseEvent.model_validate_json(event_str) 97 | 98 | if (event.type == "plan"): 99 | return PlanEvent.model_validate_json(event_str) 100 | elif (event.type == "step"): 101 | return StepEvent.model_validate_json(event_str) 102 | elif (event.type == "tool"): 103 | return ToolEvent.model_validate_json(event_str) 104 | elif (event.type == "message"): 105 | return MessageEvent.model_validate_json(event_str) 106 | elif (event.type == "error"): 107 | return ErrorEvent.model_validate_json(event_str) 108 | elif (event.type == "done"): 109 | return DoneEvent.model_validate_json(event_str) 110 | elif (event.type == "title"): 111 | return TitleEvent.model_validate_json(event_str) 112 | else: 113 | return event 114 | 115 | @staticmethod 116 | def to_json(event: AgentEvent) -> str: 117 | """Convert an AgentEvent to JSON string""" 118 | return event.model_dump_json() 119 | -------------------------------------------------------------------------------- /backend/app/domain/external/__init__.py: -------------------------------------------------------------------------------- 1 | from app.domain.external.llm import LLM 2 | from app.domain.external.sandbox import Sandbox 3 | from app.domain.external.browser import Browser 4 | from app.domain.external.search import SearchEngine 5 | 6 | __all__ = ['LLM', 'Sandbox', 'Browser', 'SearchEngine'] -------------------------------------------------------------------------------- /backend/app/domain/external/browser.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Protocol 2 | from app.domain.models.tool_result import ToolResult 3 | 4 | class Browser(Protocol): 5 | """Browser service gateway interface""" 6 | 7 | async def view_page(self) -> ToolResult: 8 | """View current page content""" 9 | ... 10 | 11 | async def navigate(self, url: str) -> ToolResult: 12 | """Navigate to specified URL""" 13 | ... 14 | 15 | async def restart(self, url: str) -> ToolResult: 16 | """Restart browser and navigate to specified URL""" 17 | ... 18 | 19 | async def click( 20 | self, 21 | index: Optional[int] = None, 22 | coordinate_x: Optional[float] = None, 23 | coordinate_y: Optional[float] = None 24 | ) -> ToolResult: 25 | """Click element""" 26 | ... 27 | 28 | async def input( 29 | self, 30 | text: str, 31 | press_enter: bool, 32 | index: Optional[int] = None, 33 | coordinate_x: Optional[float] = None, 34 | coordinate_y: Optional[float] = None 35 | ) -> ToolResult: 36 | """Input text""" 37 | ... 38 | 39 | async def move_mouse( 40 | self, 41 | coordinate_x: float, 42 | coordinate_y: float 43 | ) -> ToolResult: 44 | """Move mouse""" 45 | ... 46 | 47 | async def press_key(self, key: str) -> ToolResult: 48 | """Simulate key press""" 49 | ... 50 | 51 | async def select_option( 52 | self, 53 | index: int, 54 | option: int 55 | ) -> ToolResult: 56 | """Select dropdown option""" 57 | ... 58 | 59 | async def scroll_up( 60 | self, 61 | to_top: Optional[bool] = None 62 | ) -> ToolResult: 63 | """Scroll up""" 64 | ... 65 | 66 | async def scroll_down( 67 | self, 68 | to_bottom: Optional[bool] = None 69 | ) -> ToolResult: 70 | """Scroll down""" 71 | ... 72 | 73 | async def console_exec(self, javascript: str) -> ToolResult: 74 | """Execute JavaScript code""" 75 | ... 76 | 77 | async def console_view(self, max_lines: Optional[int] = None) -> ToolResult: 78 | """View console output""" 79 | ... 80 | -------------------------------------------------------------------------------- /backend/app/domain/external/llm.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any, Optional, Protocol 2 | 3 | class LLM(Protocol): 4 | """AI service gateway interface for interacting with AI services""" 5 | 6 | async def ask( 7 | self, 8 | messages: List[Dict[str, str]], 9 | tools: Optional[List[Dict[str, Any]]] = None, 10 | response_format: Optional[Dict[str, Any]] = None 11 | ) -> Dict[str, Any]: 12 | """Send chat request to AI service 13 | 14 | Args: 15 | messages: List of messages, including conversation history 16 | tools: Optional list of tools for function calling 17 | response_format: Optional response format configuration 18 | 19 | Returns: 20 | Response message from AI service 21 | """ 22 | ... 23 | 24 | @property 25 | def model_name(self) -> str: 26 | """Get the model name""" 27 | ... 28 | 29 | @property 30 | def temperature(self) -> float: 31 | """Get the temperature""" 32 | ... 33 | 34 | @property 35 | def max_tokens(self) -> int: 36 | """Get the max tokens""" 37 | ... -------------------------------------------------------------------------------- /backend/app/domain/external/message_queue.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Protocol, Tuple, Optional 2 | 3 | class MessageQueue(Protocol): 4 | """Message queue interface for agent communication""" 5 | 6 | async def put(self, message: Any) -> str: 7 | """Put a message into the queue 8 | 9 | Returns: 10 | str: Message ID 11 | """ 12 | ... 13 | 14 | async def get(self, start_id: Optional[str] = None, block_ms: Optional[int] = None) -> Tuple[str, Any]: 15 | """Get a message from the queue 16 | 17 | Args: 18 | start_id: Message ID to start reading from, defaults to "0" meaning from the earliest message 19 | block_ms: Block time in milliseconds, defaults to None meaning no blocking 20 | 21 | Returns: 22 | Tuple[str, Any]: (Message ID, Message content), returns (None, None) if no message 23 | """ 24 | ... 25 | 26 | async def pop(self) -> Tuple[str, Any]: 27 | """Get and remove the first message from the queue 28 | 29 | Returns: 30 | Tuple[str, Any]: (Message ID, Message content), returns (None, None) if queue is empty 31 | """ 32 | ... 33 | 34 | async def clear(self) -> None: 35 | """Clear all messages from the queue""" 36 | ... 37 | 38 | async def is_empty(self) -> bool: 39 | """Check if the queue is empty""" 40 | ... 41 | 42 | async def size(self) -> int: 43 | """Get the current size of the queue""" 44 | ... 45 | 46 | async def delete_message(self, message_id: str) -> bool: 47 | """Delete a specific message from the queue 48 | 49 | Args: 50 | message_id: ID of the message to delete 51 | 52 | Returns: 53 | bool: True if message was deleted successfully, False otherwise 54 | """ 55 | ... 56 | -------------------------------------------------------------------------------- /backend/app/domain/external/search.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Protocol 2 | from app.domain.models.tool_result import ToolResult 3 | 4 | class SearchEngine(Protocol): 5 | """Search engine service gateway interface""" 6 | 7 | async def search( 8 | self, 9 | query: str, 10 | date_range: Optional[str] = None 11 | ) -> ToolResult: 12 | """Search webpages using search engine 13 | 14 | Args: 15 | query: Search query, Google search style, using 3-5 keywords 16 | date_range: (Optional) Time range filter for search results 17 | 18 | Returns: 19 | Search results 20 | """ 21 | ... -------------------------------------------------------------------------------- /backend/app/domain/external/task.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol, Any, Awaitable, Optional, Callable 2 | from abc import ABC, abstractmethod 3 | from app.domain.external.message_queue import MessageQueue 4 | 5 | 6 | class TaskRunner(ABC): 7 | """Abstract base class defining the interface for task runners. 8 | 9 | This interface defines two essential lifecycle methods: 10 | - run: Main task execution logic 11 | - on_stop: Called when task execution needs to stop 12 | """ 13 | 14 | @abstractmethod 15 | async def run(self, task: "Task") -> None: 16 | """Main task execution logic. 17 | 18 | This method contains the core functionality of the task. 19 | Implementations should handle setup, execution, and cleanup. 20 | """ 21 | ... 22 | 23 | @abstractmethod 24 | async def destroy(self) -> None: 25 | """Destroy the task and release resources. 26 | 27 | Called when the task needs to be destroyed. 28 | This method is responsible for cleaning up and releasing all resources used by the task, 29 | including but not limited to: 30 | - Closing network connections 31 | - Freeing memory 32 | - Cleaning up temporary files 33 | - Stopping background processes etc. 34 | """ 35 | ... 36 | 37 | @abstractmethod 38 | async def on_done(self, task: "Task") -> None: 39 | """Called when task execution is done. 40 | 41 | Use this method to handle graceful shutdown and cleanup. 42 | This method should ensure all resources are properly released. 43 | """ 44 | ... 45 | 46 | class Task(Protocol): 47 | """Protocol defining the interface for task management operations.""" 48 | 49 | async def run(self) -> None: 50 | """Run a task.""" 51 | ... 52 | 53 | def cancel(self) -> bool: 54 | """Cancel a task. 55 | 56 | Returns: 57 | bool: True if the task is cancelled, False otherwise 58 | """ 59 | ... 60 | 61 | @property 62 | def input_stream(self) -> MessageQueue: 63 | """Input stream.""" 64 | ... 65 | 66 | @property 67 | def output_stream(self) -> MessageQueue: 68 | """Output stream.""" 69 | ... 70 | 71 | @property 72 | def id(self) -> str: 73 | """Task ID.""" 74 | ... 75 | 76 | @property 77 | def done(self) -> bool: 78 | """Check if the task is done. 79 | 80 | Returns: 81 | bool: True if the task is done, False otherwise 82 | """ 83 | ... 84 | 85 | @classmethod 86 | def get(cls, task_id: str) -> Optional["Task"]: 87 | """Get a task by its ID. 88 | 89 | Returns: 90 | Optional[Task]: Task instance if found, None otherwise 91 | """ 92 | ... 93 | 94 | @classmethod 95 | def create(cls, runner: TaskRunner) -> "Task": 96 | """Create a new task instance with the specified task runner. 97 | 98 | Args: 99 | runner (TaskRunner): The task runner that will execute this task 100 | 101 | Returns: 102 | Task: New task instance 103 | """ 104 | ... 105 | 106 | @classmethod 107 | async def destroy(cls) -> None: 108 | """Destroy all task instances. 109 | 110 | Cleans up all running tasks and releases associated resources. 111 | """ 112 | ... 113 | -------------------------------------------------------------------------------- /backend/app/domain/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/domain/models/__init__.py -------------------------------------------------------------------------------- /backend/app/domain/models/agent.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict 2 | from datetime import datetime, UTC 3 | from pydantic import BaseModel, Field, field_validator 4 | from app.domain.models.memory import Memory 5 | import uuid 6 | 7 | class Agent(BaseModel): 8 | """ 9 | Agent aggregate root that manages the lifecycle and state of an AI agent 10 | Including its execution context, memory, and current plan 11 | """ 12 | id: str = Field(default_factory=lambda: uuid.uuid4().hex[:16]) 13 | memories: Dict[str, Memory] = Field(default_factory=dict) 14 | model_name: str = Field(default="") 15 | temperature: float = Field(default=0.7) 16 | max_tokens: int = Field(default=2000) 17 | 18 | # Context related fields 19 | created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) # Creation timestamp 20 | updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) # Last update timestamp 21 | 22 | @field_validator("temperature") 23 | def validate_temperature(cls, v: float) -> float: 24 | """Validate temperature is between 0 and 1""" 25 | if not 0 <= v <= 1: 26 | raise ValueError("Temperature must be between 0 and 1") 27 | return v 28 | 29 | @field_validator("max_tokens") 30 | def validate_max_tokens(cls, v: Optional[int]) -> Optional[int]: 31 | """Validate max_tokens is positive if provided""" 32 | if v is not None and v <= 0: 33 | raise ValueError("Max tokens must be positive") 34 | return v 35 | 36 | class Config: 37 | arbitrary_types_allowed = True 38 | -------------------------------------------------------------------------------- /backend/app/domain/models/memory.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import List, Dict, Any, Optional 3 | 4 | class Memory(BaseModel): 5 | """ 6 | Memory class, defining the basic behavior of memory 7 | """ 8 | messages: List[Dict[str, Any]] = [] 9 | 10 | def get_message_role(self, message: Dict[str, Any]) -> str: 11 | """Get the role of the message""" 12 | return message.get("role") 13 | 14 | def add_message(self, message: Dict[str, Any]) -> None: 15 | """Add message to memory""" 16 | self.messages.append(message) 17 | 18 | def add_messages(self, messages: List[Dict[str, Any]]) -> None: 19 | """Add messages to memory""" 20 | self.messages.extend(messages) 21 | 22 | def get_messages(self) -> List[Dict[str, Any]]: 23 | """Get all message history""" 24 | return self.messages 25 | 26 | def get_latest_system_message(self) -> Dict[str, Any]: 27 | """Get the latest system message""" 28 | for message in reversed(self.messages): 29 | if self.get_message_role(message) == "system": 30 | return message 31 | return {} 32 | 33 | def get_non_system_messages(self) -> List[Dict[str, Any]]: 34 | """Get all non-system messages""" 35 | return [message for message in self.messages if self.get_message_role(message) != "system"] 36 | 37 | def get_messages_with_latest_system(self) -> List[Dict[str, Any]]: 38 | """Get all non-system messages plus the latest system message""" 39 | latest_system = self.get_latest_system_message() 40 | non_system_messages = self.get_non_system_messages() 41 | if latest_system: 42 | return [latest_system] + non_system_messages 43 | return non_system_messages 44 | 45 | def clear_messages(self) -> None: 46 | """Clear memory""" 47 | self.messages = [] 48 | 49 | def get_filtered_messages(self) -> List[Dict[str, Any]]: 50 | """Get all non-system and non-tool response messages, plus the latest system message""" 51 | latest_system = self.get_latest_system_message() 52 | messages = [message for message in self.messages 53 | if self.get_message_role(message) != "system"] 54 | #and self.get_message_role(message) != "tool"] 55 | if latest_system: 56 | return [latest_system] + messages 57 | return messages 58 | 59 | def roll_back(self) -> None: 60 | """Roll back memory""" 61 | if len(self.messages) > 1 and \ 62 | self.get_message_role(self.messages[-1]) == "tool" and \ 63 | self.get_message_role(self.messages[-2]) != "tool": 64 | self.messages.pop() 65 | elif len(self.messages) > 0 and self.get_message_role(self.messages[-1]) == "user": 66 | self.messages.pop() 67 | 68 | @property 69 | def empty(self) -> bool: 70 | """Check if memory is empty""" 71 | return len(self.messages) == 0 72 | -------------------------------------------------------------------------------- /backend/app/domain/models/plan.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import List, Dict, Any, Optional 3 | from enum import Enum 4 | 5 | class ExecutionStatus(str, Enum): 6 | PENDING = "pending" 7 | RUNNING = "running" 8 | COMPLETED = "completed" 9 | FAILED = "failed" 10 | 11 | class Step(BaseModel): 12 | id: str 13 | description: str 14 | status: ExecutionStatus = ExecutionStatus.PENDING 15 | result: Optional[str] = None 16 | error: Optional[str] = None 17 | 18 | def is_done(self) -> bool: 19 | return self.status == ExecutionStatus.COMPLETED or self.status == ExecutionStatus.FAILED 20 | 21 | class Plan(BaseModel): 22 | id: str 23 | title: str 24 | goal: str 25 | steps: List[Step] 26 | message: Optional[str] = None 27 | status: ExecutionStatus = ExecutionStatus.PENDING 28 | result: Optional[Dict[str, Any]] = None 29 | error: Optional[str] = None 30 | 31 | def is_done(self) -> bool: 32 | return self.status == ExecutionStatus.COMPLETED or self.status == ExecutionStatus.FAILED 33 | 34 | def get_next_step(self) -> Optional[Step]: 35 | for step in self.steps: 36 | if not step.is_done(): 37 | return step 38 | return None 39 | -------------------------------------------------------------------------------- /backend/app/domain/models/session.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime, UTC 3 | from typing import List, Optional 4 | from enum import Enum 5 | import uuid 6 | from app.domain.events.agent_events import BaseEvent 7 | 8 | 9 | class SessionStatus(str, Enum): 10 | """Session status enum""" 11 | ACTIVE = "active" 12 | COMPLETED = "completed" 13 | 14 | 15 | class Session(BaseModel): 16 | """Session model""" 17 | id: str = Field(default_factory=lambda: uuid.uuid4().hex[:16]) 18 | sandbox_id: Optional[str] = Field(default=None) # Identifier for the sandbox environment 19 | agent_id: str 20 | task_id: Optional[str] = None 21 | title: Optional[str] = None 22 | unread_message_count: int = 0 23 | latest_message: Optional[str] = None 24 | latest_message_at: Optional[datetime] = Field(default_factory=lambda: datetime.now(UTC)) 25 | created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) 26 | updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) 27 | events: List[BaseEvent] = [] 28 | status: SessionStatus = SessionStatus.ACTIVE -------------------------------------------------------------------------------- /backend/app/domain/models/tool_result.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import Any, Optional 3 | 4 | class ToolResult(BaseModel): 5 | success: bool 6 | message: Optional[str] = None 7 | data: Optional[Any] = None 8 | -------------------------------------------------------------------------------- /backend/app/domain/repositories/agent_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List, Protocol 2 | from app.domain.models.agent import Agent 3 | from app.domain.models.plan import Plan 4 | from app.domain.models.memory import Memory 5 | 6 | class AgentRepository(Protocol): 7 | """Repository interface for Agent aggregate""" 8 | 9 | async def save(self, agent: Agent) -> None: 10 | """Save or update an agent""" 11 | ... 12 | 13 | async def find_by_id(self, agent_id: str) -> Optional[Agent]: 14 | """Find an agent by its ID""" 15 | ... 16 | 17 | async def add_memory(self, agent_id: str, 18 | name: str, 19 | memory: Memory) -> None: 20 | """Add or update a memory for an agent""" 21 | ... 22 | 23 | async def get_memory(self, agent_id: str, name: str) -> Memory: 24 | """Get memory by name from agent, create if not exists""" 25 | ... 26 | 27 | async def save_memory(self, agent_id: str, name: str, memory: Memory) -> None: 28 | """Update the messages of a memory""" 29 | ... -------------------------------------------------------------------------------- /backend/app/domain/repositories/session_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Protocol, List 2 | from datetime import datetime 3 | from app.domain.models.session import Session, SessionStatus 4 | from app.domain.events.agent_events import BaseEvent 5 | 6 | class SessionRepository(Protocol): 7 | """Repository interface for Session aggregate""" 8 | 9 | async def save(self, session: Session) -> None: 10 | """Save or update a session""" 11 | ... 12 | 13 | async def find_by_id(self, session_id: str) -> Optional[Session]: 14 | """Find a session by its ID""" 15 | ... 16 | 17 | async def update_title(self, session_id: str, title: str) -> None: 18 | """Update the title of a session""" 19 | ... 20 | 21 | async def update_latest_message(self, session_id: str, message: str, timestamp: datetime) -> None: 22 | """Update the latest message of a session""" 23 | ... 24 | 25 | async def add_event(self, session_id: str, event: BaseEvent) -> None: 26 | """Add an event to a session""" 27 | ... 28 | 29 | async def update_status(self, session_id: str, status: SessionStatus) -> None: 30 | """Update the status of a session""" 31 | ... 32 | 33 | async def update_unread_message_count(self, session_id: str, count: int) -> None: 34 | """Update the unread message count of a session""" 35 | ... 36 | 37 | async def increment_unread_message_count(self, session_id: str) -> None: 38 | """Increment the unread message count of a session""" 39 | ... 40 | 41 | async def decrement_unread_message_count(self, session_id: str) -> None: 42 | """Decrement the unread message count of a session""" 43 | ... 44 | 45 | async def delete(self, session_id: str) -> None: 46 | """Delete a session""" 47 | ... 48 | 49 | async def get_all(self) -> List[Session]: 50 | """Get all sessions""" 51 | ... -------------------------------------------------------------------------------- /backend/app/domain/services/__init__.py: -------------------------------------------------------------------------------- 1 | from .agent_domain_service import AgentDomainService 2 | 3 | __all__ = [ 4 | 'AgentDomainService' 5 | ] 6 | -------------------------------------------------------------------------------- /backend/app/domain/services/agents/__init__.py: -------------------------------------------------------------------------------- 1 | from ...models.agent import Agent 2 | 3 | __all__ = [ 4 | 'Agent' 5 | ] 6 | -------------------------------------------------------------------------------- /backend/app/domain/services/agents/execution.py: -------------------------------------------------------------------------------- 1 | from typing import AsyncGenerator, Optional 2 | from app.domain.models.plan import Plan, Step, ExecutionStatus 3 | from app.domain.services.agents.base import BaseAgent 4 | from app.domain.external.llm import LLM 5 | from app.domain.external.sandbox import Sandbox 6 | from app.domain.external.browser import Browser 7 | from app.domain.external.search import SearchEngine 8 | from app.domain.repositories.agent_repository import AgentRepository 9 | from app.domain.services.prompts.execution import EXECUTION_SYSTEM_PROMPT, EXECUTION_PROMPT 10 | from app.domain.events.agent_events import ( 11 | BaseEvent, 12 | StepEvent, 13 | StepStatus, 14 | ErrorEvent, 15 | MessageEvent, 16 | DoneEvent, 17 | ) 18 | from app.domain.services.tools.shell import ShellTool 19 | from app.domain.services.tools.browser import BrowserTool 20 | from app.domain.services.tools.search import SearchTool 21 | from app.domain.services.tools.file import FileTool 22 | from app.domain.services.tools.message import MessageTool 23 | from app.domain.utils.json_parser import JsonParser 24 | 25 | 26 | class ExecutionAgent(BaseAgent): 27 | """ 28 | Execution agent class, defining the basic behavior of execution 29 | """ 30 | 31 | name: str = "execution" 32 | system_prompt: str = EXECUTION_SYSTEM_PROMPT 33 | 34 | def __init__( 35 | self, 36 | agent_id: str, 37 | agent_repository: AgentRepository, 38 | llm: LLM, 39 | sandbox: Sandbox, 40 | browser: Browser, 41 | json_parser: JsonParser, 42 | search_engine: Optional[SearchEngine] = None, 43 | ): 44 | super().__init__( 45 | agent_id=agent_id, 46 | agent_repository=agent_repository, 47 | llm=llm, 48 | json_parser=json_parser, 49 | tools=[ 50 | ShellTool(sandbox), 51 | BrowserTool(browser), 52 | FileTool(sandbox), 53 | MessageTool() 54 | ] 55 | ) 56 | 57 | # Only add search tool when search_engine is not None 58 | if search_engine: 59 | self.tools.append(SearchTool(search_engine)) 60 | 61 | async def execute_step(self, plan: Plan, step: Step) -> AsyncGenerator[BaseEvent, None]: 62 | message = EXECUTION_PROMPT.format(goal=plan.goal, step=step.description) 63 | step.status = ExecutionStatus.RUNNING 64 | yield StepEvent(status=StepStatus.STARTED, step=step) 65 | async for event in self.execute(message): 66 | if isinstance(event, ErrorEvent): 67 | step.status = ExecutionStatus.FAILED 68 | step.error = event.error 69 | yield StepEvent(status=StepStatus.FAILED, step=step) 70 | 71 | if isinstance(event, MessageEvent): 72 | step.status = ExecutionStatus.COMPLETED 73 | step.result = event.message 74 | yield StepEvent(status=StepStatus.COMPLETED, step=step) 75 | yield event 76 | step.status = ExecutionStatus.COMPLETED 77 | 78 | -------------------------------------------------------------------------------- /backend/app/domain/services/agents/planner.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List, AsyncGenerator, Optional 2 | import json 3 | import logging 4 | from app.domain.models.plan import Plan, Step 5 | from app.domain.services.agents.base import BaseAgent 6 | from app.domain.models.memory import Memory 7 | from app.domain.external.llm import LLM 8 | from app.domain.services.prompts.planner import ( 9 | PLANNER_SYSTEM_PROMPT, 10 | CREATE_PLAN_PROMPT, 11 | UPDATE_PLAN_PROMPT 12 | ) 13 | from app.domain.events.agent_events import ( 14 | BaseEvent, 15 | PlanEvent, 16 | PlanStatus, 17 | ErrorEvent, 18 | MessageEvent, 19 | DoneEvent, 20 | ) 21 | from app.domain.external.sandbox import Sandbox 22 | from app.domain.services.tools.file import FileTool 23 | from app.domain.services.tools.shell import ShellTool 24 | from app.domain.repositories.agent_repository import AgentRepository 25 | from app.domain.utils.json_parser import JsonParser 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | class PlannerAgent(BaseAgent): 30 | """ 31 | Planner agent class, defining the basic behavior of planning 32 | """ 33 | 34 | name: str = "planner" 35 | system_prompt: str = PLANNER_SYSTEM_PROMPT 36 | format: Optional[str] = "json_object" 37 | 38 | def __init__( 39 | self, 40 | agent_id: str, 41 | agent_repository: AgentRepository, 42 | llm: LLM, 43 | json_parser: JsonParser, 44 | ): 45 | super().__init__( 46 | agent_id=agent_id, 47 | agent_repository=agent_repository, 48 | llm=llm, 49 | json_parser=json_parser, 50 | ) 51 | 52 | 53 | async def create_plan(self, message: Optional[str] = None) -> AsyncGenerator[BaseEvent, None]: 54 | message = CREATE_PLAN_PROMPT.format(user_message=message) if message else None 55 | async for event in self.execute(message): 56 | if isinstance(event, MessageEvent): 57 | logger.info(event.message) 58 | parsed_response = await self.json_parser.parse(event.message) 59 | steps = [Step(id=step["id"], description=step["description"]) for step in parsed_response["steps"]] 60 | plan = Plan(id=f"plan_{len(steps)}", goal=parsed_response["goal"], title=parsed_response["title"], steps=steps, message=parsed_response["message"], todo=parsed_response.get("todo", "")) 61 | yield PlanEvent(status=PlanStatus.CREATED, plan=plan) 62 | else: 63 | yield event 64 | 65 | async def update_plan(self, plan: Plan) -> AsyncGenerator[BaseEvent, None]: 66 | message = UPDATE_PLAN_PROMPT.format(plan=plan.model_dump_json(include={"steps"}), goal=plan.goal) 67 | async for event in self.execute(message): 68 | if isinstance(event, MessageEvent): 69 | parsed_response = await self.json_parser.parse(event.message) 70 | new_steps = [Step(id=step["id"], description=step["description"]) for step in parsed_response["steps"]] 71 | 72 | # Find the index of the first pending step 73 | first_pending_index = None 74 | for i, step in enumerate(plan.steps): 75 | if not step.is_done(): 76 | first_pending_index = i 77 | break 78 | 79 | # If there are pending steps, replace all pending steps 80 | if first_pending_index is not None: 81 | # Keep completed steps 82 | updated_steps = plan.steps[:first_pending_index] 83 | # Add new steps 84 | updated_steps.extend(new_steps) 85 | # Update steps in plan 86 | plan.steps = updated_steps 87 | 88 | yield PlanEvent(status=PlanStatus.UPDATED, plan=plan) 89 | else: 90 | yield event -------------------------------------------------------------------------------- /backend/app/domain/services/flows/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/domain/services/flows/__init__.py -------------------------------------------------------------------------------- /backend/app/domain/services/flows/base.py: -------------------------------------------------------------------------------- 1 | from app.domain.events.agent_events import BaseEvent 2 | from app.domain.models.agent import Agent 3 | from typing import AsyncGenerator 4 | from abc import ABC, abstractmethod 5 | from app.domain.repositories.agent_repository import AgentRepository 6 | 7 | class BaseFlow(ABC): 8 | 9 | @abstractmethod 10 | def run(self) -> AsyncGenerator[BaseEvent, None]: 11 | pass 12 | 13 | @abstractmethod 14 | def is_done(self) -> bool: 15 | pass 16 | -------------------------------------------------------------------------------- /backend/app/domain/services/prompts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/domain/services/prompts/__init__.py -------------------------------------------------------------------------------- /backend/app/domain/services/tools/__init__.py: -------------------------------------------------------------------------------- 1 | from app.domain.services.tools.base import BaseTool 2 | from app.domain.services.tools.browser import BrowserTool 3 | from app.domain.services.tools.shell import ShellTool 4 | from app.domain.services.tools.search import SearchTool 5 | from app.domain.services.tools.message import MessageTool 6 | from app.domain.services.tools.file import FileTool 7 | 8 | __all__ = [ 9 | 'BaseTool', 10 | 'BrowserTool', 11 | 'ShellTool', 12 | 'SearchTool', 13 | 'MessageTool', 14 | 'FileTool', 15 | ] 16 | -------------------------------------------------------------------------------- /backend/app/domain/services/tools/base.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List, Callable 2 | import inspect 3 | from app.domain.models.tool_result import ToolResult 4 | 5 | def tool( 6 | name: str, 7 | description: str, 8 | parameters: Dict[str, Dict[str, Any]], 9 | required: List[str] 10 | ) -> Callable: 11 | """Tool registration decorator 12 | 13 | Args: 14 | name: Tool name 15 | description: Tool description 16 | parameters: Tool parameter definitions 17 | required: List of required parameters 18 | 19 | Returns: 20 | Decorator function 21 | """ 22 | def decorator(func): 23 | # Create tool schema directly using provided parameters, without automatic extraction 24 | schema = { 25 | "type": "function", 26 | "function": { 27 | "name": name, 28 | "description": description, 29 | "parameters": { 30 | "type": "object", 31 | "properties": parameters, 32 | "required": required 33 | } 34 | } 35 | } 36 | 37 | # Store tool information 38 | func._function_name = name 39 | func._tool_description = description 40 | func._tool_schema = schema 41 | 42 | return func 43 | 44 | return decorator 45 | 46 | class BaseTool: 47 | """Base tool class, providing common tool calling methods""" 48 | 49 | name: str = "" 50 | 51 | def __init__(self): 52 | """Initialize base tool class""" 53 | self._tools_cache = None 54 | 55 | def get_tools(self) -> List[Dict[str, Any]]: 56 | """Get all registered tools 57 | 58 | Returns: 59 | List of tools 60 | """ 61 | if self._tools_cache is not None: 62 | return self._tools_cache 63 | 64 | tools = [] 65 | for _, method in inspect.getmembers(self, inspect.ismethod): 66 | if hasattr(method, '_tool_schema'): 67 | tools.append(method._tool_schema) 68 | 69 | self._tools_cache = tools 70 | return tools 71 | 72 | def has_function(self, function_name: str) -> bool: 73 | """Check if specified function exists 74 | 75 | Args: 76 | function_name: Function name 77 | 78 | Returns: 79 | Whether the tool exists 80 | """ 81 | for _, method in inspect.getmembers(self, inspect.ismethod): 82 | if hasattr(method, '_function_name') and method._function_name == function_name: 83 | return True 84 | return False 85 | 86 | async def invoke_function(self, function_name: str, **kwargs) -> ToolResult: 87 | """Invoke specified tool 88 | 89 | Args: 90 | function_name: Function name 91 | **kwargs: Parameters 92 | 93 | Returns: 94 | Invocation result 95 | 96 | Raises: 97 | ValueError: Raised when tool doesn't exist 98 | """ 99 | for _, method in inspect.getmembers(self, inspect.ismethod): 100 | if hasattr(method, '_function_name') and method._function_name == function_name: 101 | return await method(**kwargs) 102 | 103 | raise ValueError(f"Tool '{function_name}' not found") -------------------------------------------------------------------------------- /backend/app/domain/services/tools/message.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Union 2 | from app.domain.services.tools.base import tool, BaseTool 3 | from app.domain.models.tool_result import ToolResult 4 | 5 | class MessageTool(BaseTool): 6 | """Message tool class, providing message sending functions for user interaction""" 7 | 8 | name: str = "message" 9 | 10 | def __init__(self): 11 | """Initialize message tool class""" 12 | super().__init__() 13 | 14 | @tool( 15 | name="message_notify_user", 16 | description="Send a message to user without requiring a response. Use for acknowledging receipt of messages, providing progress updates, reporting task completion, or explaining changes in approach.", 17 | parameters={ 18 | "text": { 19 | "type": "string", 20 | "description": "Message text to display to user" 21 | } 22 | }, 23 | required=["text"] 24 | ) 25 | async def message_notify_user( 26 | self, 27 | text: str 28 | ) -> ToolResult: 29 | """Send notification message to user, no response needed 30 | 31 | Args: 32 | text: Message text to display to user 33 | 34 | Returns: 35 | Message sending result 36 | """ 37 | 38 | # Return success result, actual UI display logic implemented by caller 39 | return ToolResult( 40 | success=True, 41 | data=text 42 | ) 43 | -------------------------------------------------------------------------------- /backend/app/domain/services/tools/plan.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/domain/services/tools/plan.py -------------------------------------------------------------------------------- /backend/app/domain/services/tools/search.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from app.domain.external.search import SearchEngine 3 | from app.domain.services.tools.base import tool, BaseTool 4 | from app.domain.models.tool_result import ToolResult 5 | 6 | class SearchTool(BaseTool): 7 | """Search tool class, providing search engine interaction functions""" 8 | 9 | name: str = "search" 10 | 11 | def __init__(self, search_engine: SearchEngine): 12 | """Initialize search tool class 13 | 14 | Args: 15 | search_engine: Search engine service 16 | """ 17 | super().__init__() 18 | self.search_engine = search_engine 19 | 20 | @tool( 21 | name="info_search_web", 22 | description="Search web pages using search engine. Use for obtaining latest information or finding references.", 23 | parameters={ 24 | "query": { 25 | "type": "string", 26 | "description": "Search query in Google search style, using 3-5 keywords." 27 | }, 28 | "date_range": { 29 | "type": "string", 30 | "enum": ["all", "past_hour", "past_day", "past_week", "past_month", "past_year"], 31 | "description": "(Optional) Time range filter for search results." 32 | } 33 | }, 34 | required=["query"] 35 | ) 36 | async def info_search_web( 37 | self, 38 | query: str, 39 | date_range: Optional[str] = None 40 | ) -> ToolResult: 41 | """Search webpages using search engine 42 | 43 | Args: 44 | query: Search query, Google search style, using 3-5 keywords 45 | date_range: (Optional) Time range filter for search results 46 | 47 | Returns: 48 | Search results 49 | """ 50 | return await self.search_engine.search(query, date_range) -------------------------------------------------------------------------------- /backend/app/domain/utils/json_parser.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol, Optional, Any, Union, Dict, List 2 | 3 | class JsonParser(Protocol): 4 | """Json parser interface""" 5 | 6 | async def parse(self, text: str, default_value: Optional[Any] = None) -> Union[Dict, List, Any]: 7 | """ 8 | Parse LLM output string to JSON using multiple strategies. 9 | Falls back to LLM parsing if local strategies fail. 10 | 11 | Args: 12 | text: The raw string output from LLM 13 | default_value: Default value to return if parsing fails 14 | 15 | Returns: 16 | Parsed JSON object (dict, list, or other JSON-serializable type) 17 | 18 | Raises: 19 | ValueError: If all parsing strategies fail and no default value provided 20 | """ 21 | -------------------------------------------------------------------------------- /backend/app/infrastructure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/infrastructure/__init__.py -------------------------------------------------------------------------------- /backend/app/infrastructure/config.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings 2 | from functools import lru_cache 3 | 4 | 5 | class Settings(BaseSettings): 6 | 7 | # Model provider configuration 8 | api_key: str | None = None 9 | api_base: str = "https://api.deepseek.com/v1" 10 | 11 | # Model configuration 12 | model_name: str = "deepseek-chat" 13 | temperature: float = 0.7 14 | max_tokens: int = 2000 15 | 16 | # MongoDB configuration 17 | mongodb_uri: str = "mongodb://mongodb:27017" 18 | mongodb_database: str = "manus" 19 | mongodb_username: str | None = None 20 | mongodb_password: str | None = None 21 | 22 | # Redis configuration 23 | redis_host: str = "redis" 24 | redis_port: int = 6379 25 | redis_db: int = 0 26 | redis_password: str | None = None 27 | 28 | # Sandbox configuration 29 | sandbox_address: str | None = None 30 | sandbox_image: str | None = None 31 | sandbox_name_prefix: str | None = None 32 | sandbox_ttl_minutes: int | None = 30 33 | sandbox_network: str | None = None # Docker network bridge name 34 | sandbox_chrome_args: str | None = "" 35 | sandbox_https_proxy: str | None = None 36 | sandbox_http_proxy: str | None = None 37 | sandbox_no_proxy: str | None = None 38 | 39 | # Search engine configuration 40 | google_search_api_key: str | None = None 41 | google_search_engine_id: str | None = None 42 | 43 | # Logging configuration 44 | log_level: str = "INFO" 45 | 46 | class Config: 47 | env_file = ".env" 48 | env_file_encoding = "utf-8" 49 | 50 | def validate(self): 51 | if not self.api_key: 52 | raise ValueError("API key is required") 53 | 54 | @lru_cache() 55 | def get_settings() -> Settings: 56 | settings = Settings() 57 | settings.validate() 58 | return settings 59 | -------------------------------------------------------------------------------- /backend/app/infrastructure/external/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /backend/app/infrastructure/external/llm/openai_llm.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any, Optional 2 | from openai import AsyncOpenAI 3 | from app.domain.external.llm import LLM 4 | from app.infrastructure.config import get_settings 5 | import logging 6 | 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | class OpenAILLM(LLM): 11 | def __init__(self): 12 | settings = get_settings() 13 | self.client = AsyncOpenAI( 14 | api_key=settings.api_key, 15 | base_url=settings.api_base 16 | ) 17 | 18 | self._model_name = settings.model_name 19 | self._temperature = settings.temperature 20 | self._max_tokens = settings.max_tokens 21 | logger.info(f"Initialized OpenAI LLM with model: {self._model_name}") 22 | 23 | @property 24 | def model_name(self) -> str: 25 | return self._model_name 26 | 27 | @property 28 | def temperature(self) -> float: 29 | return self._temperature 30 | 31 | @property 32 | def max_tokens(self) -> int: 33 | return self._max_tokens 34 | 35 | async def ask(self, messages: List[Dict[str, str]], 36 | tools: Optional[List[Dict[str, Any]]] = None, 37 | response_format: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 38 | """Send chat request to OpenAI API""" 39 | response = None 40 | try: 41 | if tools: 42 | logger.debug(f"Sending request to OpenAI with tools, model: {self._model_name}") 43 | response = await self.client.chat.completions.create( 44 | model=self._model_name, 45 | temperature=self._temperature, 46 | max_tokens=self._max_tokens, 47 | messages=messages, 48 | tools=tools, 49 | response_format=response_format, 50 | ) 51 | else: 52 | logger.debug(f"Sending request to OpenAI without tools, model: {self._model_name}") 53 | response = await self.client.chat.completions.create( 54 | model=self._model_name, 55 | temperature=self._temperature, 56 | max_tokens=self._max_tokens, 57 | messages=messages, 58 | response_format=response_format 59 | ) 60 | return response.choices[0].message.model_dump() 61 | except Exception as e: 62 | logger.error(f"Error calling OpenAI API: {str(e)}") 63 | raise -------------------------------------------------------------------------------- /backend/app/infrastructure/external/search/google_search.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import logging 3 | import httpx 4 | from app.domain.models.tool_result import ToolResult 5 | from app.domain.external.search import SearchEngine 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | class GoogleSearchEngine(SearchEngine): 10 | """Google API based search engine implementation""" 11 | 12 | def __init__(self, api_key: str, cx: str): 13 | """Initialize Google search engine 14 | 15 | Args: 16 | api_key: Google Custom Search API key 17 | cx: Google Search Engine ID 18 | """ 19 | self.api_key = api_key 20 | self.cx = cx 21 | self.base_url = "https://www.googleapis.com/customsearch/v1" 22 | 23 | async def search( 24 | self, 25 | query: str, 26 | date_range: Optional[str] = None 27 | ) -> ToolResult: 28 | """Search web pages using Google API 29 | 30 | Args: 31 | query: Search query, Google search style, use 3-5 keywords 32 | date_range: (Optional) Time range filter for search results 33 | 34 | Returns: 35 | Search results 36 | """ 37 | params = { 38 | "key": self.api_key, 39 | "cx": self.cx, 40 | "q": query 41 | } 42 | 43 | # Add time range filter 44 | if date_range and date_range != "all": 45 | # Convert date_range to time range parameters supported by Google API 46 | # For example: via dateRestrict parameter (d[number]: day, w[number]: week, m[number]: month, y[number]: year) 47 | date_mapping = { 48 | "past_hour": "d1", 49 | "past_day": "d1", 50 | "past_week": "w1", 51 | "past_month": "m1", 52 | "past_year": "y1" 53 | } 54 | if date_range in date_mapping: 55 | params["dateRestrict"] = date_mapping[date_range] 56 | 57 | try: 58 | async with httpx.AsyncClient() as client: 59 | response = await client.get(self.base_url, params=params) 60 | response.raise_for_status() 61 | data = response.json() 62 | 63 | # Process search results 64 | search_results = [] 65 | if "items" in data: 66 | for item in data["items"]: 67 | search_results.append({ 68 | "title": item.get("title", ""), 69 | "link": item.get("link", ""), 70 | "snippet": item.get("snippet", ""), 71 | }) 72 | 73 | # Build return result 74 | results = { 75 | "query": query, 76 | "date_range": date_range, 77 | "search_info": data.get("searchInformation", {}), 78 | "results": search_results, 79 | "total_results": data.get("searchInformation", {}).get("totalResults", "0") 80 | } 81 | 82 | return ToolResult(success=True, data=results) 83 | 84 | except Exception as e: 85 | logger.error(f"Google Search API call failed: {e}") 86 | return ToolResult( 87 | success=False, 88 | message=f"Google Search API call failed: {e}", 89 | data={ 90 | "query": query, 91 | "date_range": date_range, 92 | "results": [] 93 | } 94 | ) 95 | -------------------------------------------------------------------------------- /backend/app/infrastructure/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from logging.handlers import RotatingFileHandler 4 | import os 5 | from .config import get_settings 6 | 7 | def setup_logging(): 8 | """ 9 | Configure the application logging system 10 | 11 | Sets up log levels, formatters, and handlers for both console and file output. 12 | Ensures proper log rotation to prevent log files from growing too large. 13 | """ 14 | # Get configuration 15 | settings = get_settings() 16 | 17 | # Get root logger 18 | root_logger = logging.getLogger() 19 | 20 | # Set root log level 21 | log_level = getattr(logging, settings.log_level) 22 | root_logger.setLevel(log_level) 23 | 24 | # Create formatter 25 | formatter = logging.Formatter( 26 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s', 27 | datefmt='%Y-%m-%d %H:%M:%S' 28 | ) 29 | 30 | # Create console handler 31 | console_handler = logging.StreamHandler(sys.stdout) 32 | console_handler.setFormatter(formatter) 33 | console_handler.setLevel(log_level) 34 | 35 | # Add handlers to root logger 36 | root_logger.addHandler(console_handler) 37 | 38 | # Disable verbose logging for pymongo 39 | logging.getLogger("pymongo").setLevel(logging.WARNING) 40 | logging.getLogger("websockets").setLevel(logging.WARNING) 41 | 42 | # Log initialization complete 43 | root_logger.info("Logging system initialized - Console and file logging active") -------------------------------------------------------------------------------- /backend/app/infrastructure/models/documents.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional, List 2 | from datetime import datetime, timezone 3 | from beanie import Document 4 | from app.domain.models.memory import Memory 5 | from app.domain.events.agent_events import AgentEvent 6 | from app.domain.models.session import SessionStatus 7 | 8 | class AgentDocument(Document): 9 | """MongoDB document for Agent""" 10 | agent_id: str 11 | model_name: str 12 | temperature: float 13 | max_tokens: int 14 | memories: Dict[str, Memory] = {} 15 | created_at: datetime = datetime.now(timezone.utc) 16 | updated_at: datetime = datetime.now(timezone.utc) 17 | 18 | class Settings: 19 | name = "agents" 20 | indexes = [ 21 | "agent_id", 22 | ] 23 | 24 | 25 | class SessionDocument(Document): 26 | """MongoDB model for Session""" 27 | session_id: str 28 | sandbox_id: Optional[str] = None 29 | agent_id: str 30 | task_id: Optional[str] = None 31 | title: Optional[str] = None 32 | unread_message_count: int = 0 33 | latest_message: Optional[str] = None 34 | latest_message_at: Optional[datetime] = None 35 | created_at: datetime = datetime.now(timezone.utc) 36 | updated_at: datetime = datetime.now(timezone.utc) 37 | events: List[AgentEvent] 38 | status: SessionStatus 39 | 40 | class Settings: 41 | name = "sessions" 42 | indexes = [ 43 | "session_id", 44 | ] -------------------------------------------------------------------------------- /backend/app/infrastructure/repositories/mongo_agent_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from datetime import datetime, UTC 3 | from app.domain.models.agent import Agent 4 | from app.domain.models.memory import Memory 5 | from app.domain.repositories.agent_repository import AgentRepository 6 | from app.infrastructure.models.documents import AgentDocument 7 | import logging 8 | 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | class MongoAgentRepository(AgentRepository): 13 | """MongoDB implementation of AgentRepository""" 14 | 15 | async def save(self, agent: Agent) -> None: 16 | """Save or update an agent""" 17 | mongo_agent = await AgentDocument.find_one( 18 | AgentDocument.agent_id == agent.id 19 | ) 20 | 21 | if not mongo_agent: 22 | mongo_agent = self._to_mongo_agent(agent) 23 | await mongo_agent.save() 24 | return 25 | 26 | # Use generic update method from base class 27 | mongo_agent.model_name=agent.model_name 28 | mongo_agent.temperature=agent.temperature 29 | mongo_agent.max_tokens=agent.max_tokens 30 | mongo_agent.memories=agent.memories 31 | mongo_agent.updated_at=datetime.now(UTC) 32 | await mongo_agent.save() 33 | 34 | async def find_by_id(self, agent_id: str) -> Optional[Agent]: 35 | """Find an agent by its ID""" 36 | mongo_agent = await AgentDocument.find_one( 37 | AgentDocument.agent_id == agent_id 38 | ) 39 | return self._to_domain_agent(mongo_agent) if mongo_agent else None 40 | 41 | async def add_memory(self, agent_id: str, 42 | name: str, 43 | memory: Memory) -> None: 44 | """Add or update a memory for an agent""" 45 | result = await AgentDocument.find_one( 46 | AgentDocument.agent_id == agent_id 47 | ).update( 48 | {"$set": {f"memories.{name}": memory, "updated_at": datetime.now(UTC)}} 49 | ) 50 | if not result: 51 | raise ValueError(f"Agent {agent_id} not found") 52 | 53 | async def get_memory(self, agent_id: str, name: str) -> Memory: 54 | """Get memory by name from agent, create if not exists""" 55 | mongo_agent = await AgentDocument.find_one( 56 | AgentDocument.agent_id == agent_id 57 | ) 58 | if not mongo_agent: 59 | raise ValueError(f"Agent {agent_id} not found") 60 | return mongo_agent.memories.get(name, Memory(messages=[])) 61 | 62 | async def save_memory(self, agent_id: str, name: str, memory: Memory) -> None: 63 | """Update the messages of a memory""" 64 | result = await AgentDocument.find_one( 65 | AgentDocument.agent_id == agent_id 66 | ).update( 67 | {"$set": {f"memories.{name}": memory, "updated_at": datetime.now(UTC)}} 68 | ) 69 | if not result: 70 | raise ValueError(f"Agent {agent_id} not found") 71 | 72 | def _to_domain_agent(self, mongo_agent: AgentDocument) -> Agent: 73 | """Convert MongoDB document to domain model""" 74 | 75 | return Agent( 76 | id=mongo_agent.agent_id, 77 | model_name=mongo_agent.model_name, 78 | temperature=mongo_agent.temperature, 79 | max_tokens=mongo_agent.max_tokens, 80 | created_at=mongo_agent.created_at, 81 | updated_at=mongo_agent.updated_at, 82 | memories=mongo_agent.memories 83 | ) 84 | 85 | 86 | def _to_mongo_agent(self, agent: Agent) -> AgentDocument: 87 | """Create a new MongoDB agent from domain agent""" 88 | return AgentDocument( 89 | agent_id=agent.id, 90 | model_name=agent.model_name, 91 | temperature=agent.temperature, 92 | max_tokens=agent.max_tokens, 93 | created_at=agent.created_at, 94 | updated_at=agent.updated_at, 95 | memories=agent.memories 96 | ) -------------------------------------------------------------------------------- /backend/app/infrastructure/storage/mongodb.py: -------------------------------------------------------------------------------- 1 | from motor.motor_asyncio import AsyncIOMotorClient 2 | from pymongo.errors import ConnectionFailure 3 | from typing import Optional 4 | import logging 5 | from functools import lru_cache 6 | from app.infrastructure.config import get_settings 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | class MongoDB: 11 | def __init__(self): 12 | self._client: Optional[AsyncIOMotorClient] = None 13 | self._settings = get_settings() 14 | 15 | async def initialize(self) -> None: 16 | """Initialize MongoDB connection and Beanie ODM.""" 17 | if self._client is not None: 18 | return 19 | 20 | try: 21 | # Connect to MongoDB 22 | if self._settings.mongodb_username and self._settings.mongodb_password: 23 | # Use authenticated connection if username and password are configured 24 | self._client = AsyncIOMotorClient( 25 | self._settings.mongodb_uri, 26 | username=self._settings.mongodb_username, 27 | password=self._settings.mongodb_password, 28 | ) 29 | else: 30 | # Use unauthenticated connection if no credentials are provided 31 | self._client = AsyncIOMotorClient( 32 | self._settings.mongodb_uri, 33 | ) 34 | # Verify the connection 35 | await self._client.admin.command('ping') 36 | logger.info("Successfully connected to MongoDB") 37 | except ConnectionFailure as e: 38 | logger.error(f"Failed to connect to MongoDB: {str(e)}") 39 | raise 40 | except Exception as e: 41 | logger.error(f"Failed to initialize Beanie: {str(e)}") 42 | raise 43 | 44 | async def shutdown(self) -> None: 45 | """Shutdown MongoDB connection.""" 46 | if self._client is not None: 47 | self._client.close() 48 | self._client = None 49 | logger.info("Disconnected from MongoDB") 50 | get_mongodb.cache_clear() 51 | 52 | @property 53 | def client(self) -> AsyncIOMotorClient: 54 | """Get the MongoDB client.""" 55 | return self._client 56 | 57 | 58 | @lru_cache 59 | def get_mongodb() -> MongoDB: 60 | """Get the MongoDB instance.""" 61 | return MongoDB() 62 | 63 | -------------------------------------------------------------------------------- /backend/app/infrastructure/storage/redis.py: -------------------------------------------------------------------------------- 1 | from redis.asyncio import Redis 2 | from functools import lru_cache 3 | import logging 4 | from app.infrastructure.config import get_settings 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | class RedisClient: 9 | def __init__(self): 10 | self._client: Redis | None = None 11 | self._settings = get_settings() 12 | 13 | async def initialize(self) -> None: 14 | """Initialize Redis connection.""" 15 | if self._client is not None: 16 | return 17 | 18 | try: 19 | # Connect to Redis 20 | self._client = Redis( 21 | host=self._settings.redis_host, 22 | port=self._settings.redis_port, 23 | db=self._settings.redis_db, 24 | password=self._settings.redis_password, 25 | decode_responses=True 26 | ) 27 | # Verify the connection 28 | await self._client.ping() 29 | logger.info("Successfully connected to Redis") 30 | except Exception as e: 31 | logger.error(f"Failed to connect to Redis: {str(e)}") 32 | raise 33 | 34 | async def shutdown(self) -> None: 35 | """Shutdown Redis connection.""" 36 | if self._client is not None: 37 | await self._client.close() 38 | self._client = None 39 | logger.info("Disconnected from Redis") 40 | get_redis.cache_clear() 41 | 42 | @property 43 | def client(self) -> Redis: 44 | """Get Redis client instance.""" 45 | if self._client is None: 46 | raise RuntimeError("Redis client not initialized") 47 | return self._client 48 | 49 | @lru_cache 50 | def get_redis() -> RedisClient: 51 | """Get the Redis client instance.""" 52 | return RedisClient() -------------------------------------------------------------------------------- /backend/app/interfaces/errors/exception_handlers.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, FastAPI 2 | from fastapi.responses import JSONResponse 3 | import logging 4 | from starlette.exceptions import HTTPException as StarletteHTTPException 5 | 6 | from app.application.errors.exceptions import AppException 7 | from app.interfaces.schemas.response import APIResponse 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def register_exception_handlers(app: FastAPI) -> None: 13 | """Register all exception handlers""" 14 | 15 | @app.exception_handler(AppException) 16 | async def api_exception_handler(request: Request, exc: AppException) -> JSONResponse: 17 | """Handle custom API exceptions""" 18 | logger.warning(f"APIException: {exc.msg}") 19 | return JSONResponse( 20 | status_code=exc.status_code, 21 | content=APIResponse( 22 | code=exc.code, 23 | msg=exc.msg, 24 | data=None 25 | ).model_dump(), 26 | ) 27 | 28 | @app.exception_handler(StarletteHTTPException) 29 | async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse: 30 | """Handle HTTP exceptions""" 31 | logger.warning(f"HTTPException: {exc.detail}") 32 | return JSONResponse( 33 | status_code=exc.status_code, 34 | content=APIResponse( 35 | code=exc.status_code, 36 | msg=exc.detail, 37 | data=None 38 | ).model_dump(), 39 | ) 40 | 41 | @app.exception_handler(Exception) 42 | async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse: 43 | """Handle all uncaught exceptions""" 44 | logger.exception(f"Unhandled exception: {str(exc)}") 45 | return JSONResponse( 46 | status_code=500, 47 | content=APIResponse( 48 | code=500, 49 | msg="Internal server error", 50 | data=None 51 | ).model_dump(), 52 | ) -------------------------------------------------------------------------------- /backend/app/interfaces/schemas/request.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import Optional 3 | 4 | class ChatRequest(BaseModel): 5 | timestamp: Optional[int] = None 6 | message: Optional[str] = None 7 | event_id: Optional[str] = None 8 | 9 | class FileViewRequest(BaseModel): 10 | file: str 11 | 12 | class ShellViewRequest(BaseModel): 13 | session_id: str -------------------------------------------------------------------------------- /backend/app/interfaces/schemas/response.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Generic, Optional, TypeVar, List 2 | from datetime import datetime 3 | from pydantic import BaseModel 4 | from app.interfaces.schemas.event import AgentSSEEvent 5 | from app.domain.models.session import SessionStatus 6 | 7 | T = TypeVar('T') 8 | 9 | 10 | class APIResponse(BaseModel, Generic[T]): 11 | code: int = 0 12 | msg: str = "success" 13 | data: Optional[T] = None 14 | 15 | @staticmethod 16 | def success(data: Optional[T] = None) -> "APIResponse[T]": 17 | return APIResponse(code=0, msg="success", data=data) 18 | 19 | @staticmethod 20 | def error(code: int, msg: str) -> "APIResponse[T]": 21 | return APIResponse(code=code, msg=msg, data=None) 22 | 23 | 24 | class CreateSessionResponse(BaseModel): 25 | session_id: str 26 | 27 | class GetSessionResponse(BaseModel): 28 | session_id: str 29 | title: Optional[str] = None 30 | events: List[AgentSSEEvent] = [] 31 | 32 | class ListSessionItem(BaseModel): 33 | session_id: str 34 | title: Optional[str] = None 35 | latest_message: Optional[str] = None 36 | latest_message_at: Optional[int] = None 37 | status: SessionStatus 38 | unread_message_count: int 39 | 40 | class ListSessionResponse(BaseModel): 41 | sessions: List[ListSessionItem] 42 | 43 | class ConsoleRecord(BaseModel): 44 | ps1: str 45 | command: str 46 | output: str 47 | 48 | class ShellViewResponse(BaseModel): 49 | output: str 50 | session_id: str 51 | console: Optional[List[ConsoleRecord]] = None 52 | 53 | class FileViewResponse(BaseModel): 54 | content: str 55 | file: str 56 | -------------------------------------------------------------------------------- /backend/dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | openai 4 | pydantic 5 | pydantic-settings 6 | python-dotenv 7 | sse-starlette 8 | httpx 9 | rich 10 | playwright>=1.42.0 11 | markdownify 12 | docker 13 | websockets 14 | motor>=3.3.2 15 | pymongo>=4.6.1 16 | beanie>=1.25.0 17 | async-lru>=2.0.0 18 | redis>=5.0.1 -------------------------------------------------------------------------------- /backend/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec uvicorn app.main:app --host 0.0.0.0 --port 8000 -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export BUILDX_NO_DEFAULT_ATTESTATIONS=1 4 | docker buildx bake "$@" 5 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Determine which Docker Compose command to use 4 | if command -v docker &> /dev/null && docker compose version &> /dev/null; then 5 | COMPOSE="docker compose" 6 | elif command -v docker-compose &> /dev/null; then 7 | COMPOSE="docker-compose" 8 | else 9 | echo "Error: Neither docker compose nor docker-compose command found" >&2 10 | exit 1 11 | fi 12 | 13 | 14 | # Execute Docker Compose command 15 | $COMPOSE -f docker-compose-development.yml "$@" 16 | -------------------------------------------------------------------------------- /docker-compose-development.yml: -------------------------------------------------------------------------------- 1 | services: 2 | frontend-dev: 3 | build: 4 | context: ./frontend 5 | dockerfile: Dockerfile 6 | target: build-stage # Use build stage image instead of production image 7 | command: ["npm", "run", "dev", "--", "--host", "0.0.0.0"] # Start in development mode and listen on all network interfaces 8 | volumes: 9 | - ./frontend:/app # Mount source code directory 10 | - /app/node_modules # Avoid overwriting container's node_modules 11 | ports: 12 | - "5173:5173" # Vite default development port 13 | environment: 14 | - NODE_ENV=development 15 | - VITE_API_URL=http://127.0.0.1:8000 16 | depends_on: 17 | - backend 18 | restart: unless-stopped 19 | networks: 20 | - manus-network 21 | 22 | backend: 23 | build: 24 | context: ./backend 25 | dockerfile: Dockerfile 26 | command: ["./dev.sh"] # Start in reload mode 27 | volumes: 28 | - ./backend:/app # Mount source code directory 29 | - /var/run/docker.sock:/var/run/docker.sock:ro 30 | - /app/__pycache__ # Avoid overwriting cache files 31 | - /app/.venv # Avoid overwriting virtual environment 32 | ports: 33 | - "8000:8000" 34 | depends_on: 35 | sandbox: 36 | condition: service_started 37 | required: false 38 | mongodb: 39 | condition: service_started 40 | required: true 41 | restart: unless-stopped 42 | networks: 43 | - manus-network 44 | env_file: 45 | - .env 46 | environment: 47 | - SANDBOX_ADDRESS=sandbox # Use single container as sandbox 48 | 49 | sandbox: 50 | build: 51 | context: ./sandbox 52 | dockerfile: Dockerfile 53 | hostname: sandbox 54 | volumes: 55 | - ./sandbox:/app # Mount source code directory 56 | - ./sandbox/supervisord.conf:/etc/supervisor/conf.d/app.conf 57 | - /app/__pycache__ # Avoid overwriting cache files 58 | - /app/.venv # Avoid overwriting virtual environment 59 | ports: 60 | #- "9222:9222" 61 | - "5902:5900" 62 | #- "5901:5901" 63 | - "8080:8080" 64 | environment: 65 | - UVI_ARGS="--reload" 66 | - LOG_LEVEL=${LOG_LEVEL:-DEBUG} 67 | restart: unless-stopped 68 | networks: 69 | - manus-network 70 | 71 | mockserver: 72 | build: 73 | context: ./mockserver 74 | dockerfile: Dockerfile 75 | volumes: 76 | - ./mockserver:/app # Mount source code directory 77 | - /app/__pycache__ # Avoid overwriting cache files 78 | - /app/.venv # Avoid overwriting virtual environment 79 | restart: unless-stopped 80 | environment: 81 | - MOCK_DATA_FILE=default.yaml 82 | - MOCK_DELAY=1 83 | networks: 84 | - manus-network 85 | 86 | mongodb: 87 | image: mongo:7.0 88 | volumes: 89 | - mongodb_data:/data/db 90 | restart: unless-stopped 91 | #ports: 92 | # - "27017:27017" 93 | networks: 94 | - manus-network 95 | 96 | redis: 97 | image: redis:7.0 98 | restart: unless-stopped 99 | networks: 100 | - manus-network 101 | 102 | volumes: 103 | mongodb_data: 104 | name: manus-mongodb-data 105 | 106 | networks: 107 | manus-network: 108 | name: manus-network 109 | driver: bridge 110 | -------------------------------------------------------------------------------- /docker-compose-example.yml: -------------------------------------------------------------------------------- 1 | services: 2 | frontend: 3 | image: simpleyyt/manus-frontend 4 | ports: 5 | - "5173:80" 6 | depends_on: 7 | - backend 8 | restart: unless-stopped 9 | networks: 10 | - manus-network 11 | environment: 12 | - BACKEND_URL=http://backend:8000 13 | 14 | backend: 15 | image: simpleyyt/manus-backend 16 | depends_on: 17 | - sandbox 18 | restart: unless-stopped 19 | volumes: 20 | - /var/run/docker.sock:/var/run/docker.sock:ro 21 | networks: 22 | - manus-network 23 | environment: 24 | # OpenAI API base URL 25 | - API_BASE=https://api.openai.com/v1 26 | # OpenAI API key, replace with your own 27 | - API_KEY=sk-xxxx 28 | # LLM model name 29 | - MODEL_NAME=gpt-4o 30 | # LLM temperature parameter, controls randomness 31 | - TEMPERATURE=0.7 32 | # Maximum tokens for LLM response 33 | - MAX_TOKENS=2000 34 | 35 | # MongoDB connection URI (optional) 36 | #- MONGODB_URI=mongodb://mongodb:27017 37 | # MongoDB database name (optional) 38 | #- MONGODB_DATABASE=manus 39 | # MongoDB username (optional) 40 | #- MONGODB_USERNAME= 41 | # MongoDB password (optional) 42 | #- MONGODB_PASSWORD= 43 | 44 | # Redis server hostname (optional) 45 | #- REDIS_HOST=redis 46 | # Redis server port (optional) 47 | #- REDIS_PORT=6379 48 | # Redis database number (optional) 49 | #- REDIS_DB=0 50 | # Redis password (optional) 51 | #- REDIS_PASSWORD= 52 | 53 | # Sandbox server address (optional) 54 | #- SANDBOX_ADDRESS= 55 | # Docker image used for the sandbox 56 | - SANDBOX_IMAGE=simpleyyt/manus-sandbox 57 | # Prefix for sandbox container names 58 | - SANDBOX_NAME_PREFIX=sandbox 59 | # Time-to-live for sandbox containers in minutes 60 | - SANDBOX_TTL_MINUTES=30 61 | # Docker network for sandbox containers 62 | - SANDBOX_NETWORK=manus-network 63 | # Chrome browser arguments for sandbox (optional) 64 | #- SANDBOX_CHROME_ARGS= 65 | # HTTPS proxy for sandbox (optional) 66 | #- SANDBOX_HTTPS_PROXY= 67 | # HTTP proxy for sandbox (optional) 68 | #- SANDBOX_HTTP_PROXY= 69 | # No proxy hosts for sandbox (optional) 70 | #- SANDBOX_NO_PROXY= 71 | 72 | # Google Search API key for web search capability (optional) 73 | #- GOOGLE_SEARCH_API_KEY= 74 | # Google Custom Search Engine ID (optional) 75 | #- GOOGLE_SEARCH_ENGINE_ID= 76 | 77 | # Application log level 78 | - LOG_LEVEL=INFO 79 | 80 | sandbox: 81 | image: simpleyyt/manus-sandbox 82 | command: /bin/sh -c "exit 0" # prevent sandbox from starting, ensure image is pulled 83 | restart: "no" 84 | networks: 85 | - manus-network 86 | 87 | mongodb: 88 | image: mongo:7.0 89 | volumes: 90 | - mongodb_data:/data/db 91 | restart: unless-stopped 92 | #ports: 93 | # - "27017:27017" 94 | networks: 95 | - manus-network 96 | 97 | redis: 98 | image: redis:7.0 99 | restart: unless-stopped 100 | networks: 101 | - manus-network 102 | 103 | volumes: 104 | mongodb_data: 105 | name: manus-mongodb-data 106 | 107 | networks: 108 | manus-network: 109 | name: manus-network 110 | driver: bridge -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | frontend: 3 | image: ${IMAGE_REGISTRY:-simpleyyt}/manus-frontend:${IMAGE_TAG:-latest} 4 | build: 5 | context: ./frontend 6 | dockerfile: Dockerfile 7 | x-bake: 8 | platforms: 9 | - linux/amd64 10 | - linux/arm64 11 | ports: 12 | - "5173:80" 13 | depends_on: 14 | - backend 15 | restart: unless-stopped 16 | networks: 17 | - manus-network 18 | environment: 19 | - BACKEND_URL=http://backend:8000 20 | 21 | backend: 22 | image: ${IMAGE_REGISTRY:-simpleyyt}/manus-backend:${IMAGE_TAG:-latest} 23 | build: 24 | context: ./backend 25 | dockerfile: Dockerfile 26 | x-bake: 27 | platforms: 28 | - linux/amd64 29 | - linux/arm64 30 | depends_on: 31 | - sandbox 32 | restart: unless-stopped 33 | volumes: 34 | - /var/run/docker.sock:/var/run/docker.sock:ro 35 | networks: 36 | - manus-network 37 | env_file: 38 | - .env 39 | 40 | sandbox: 41 | image: ${IMAGE_REGISTRY:-simpleyyt}/manus-sandbox:${IMAGE_TAG:-latest} 42 | build: 43 | context: ./sandbox 44 | dockerfile: Dockerfile 45 | x-bake: 46 | platforms: 47 | - linux/amd64 48 | - linux/arm64 49 | command: /bin/sh -c "exit 0" # prevent sandbox from starting, ensure image is pulled 50 | restart: "no" 51 | networks: 52 | - manus-network 53 | 54 | mongodb: 55 | image: mongo:7.0 56 | volumes: 57 | - mongodb_data:/data/db 58 | restart: unless-stopped 59 | #ports: 60 | # - "27017:27017" 61 | networks: 62 | - manus-network 63 | 64 | redis: 65 | image: redis:7.0 66 | restart: unless-stopped 67 | networks: 68 | - manus-network 69 | 70 | volumes: 71 | mongodb_data: 72 | name: manus-mongodb-data 73 | 74 | networks: 75 | manus-network: 76 | name: manus-network 77 | driver: bridge -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | # 依赖目录 2 | node_modules 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | 7 | # 编辑器和IDE文件 8 | .idea 9 | .vscode 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | *.sw? 15 | 16 | # 构建输出 17 | dist 18 | dist-ssr 19 | coverage 20 | 21 | # 本地环境文件 22 | .env.local 23 | .env.*.local 24 | 25 | # 日志 26 | logs 27 | *.log 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | pnpm-debug.log* 32 | lerna-debug.log* 33 | 34 | # 其他 35 | .DS_Store 36 | .git 37 | .gitignore 38 | README.md 39 | **/TEST* 40 | **/.env -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node.js as base image 2 | FROM node:18-alpine as build-stage 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy package.json and package-lock.json files 8 | COPY package*.json ./ 9 | 10 | # Install project dependencies 11 | RUN npm ci 12 | 13 | # Copy project files 14 | COPY . . 15 | 16 | # Build application - skip type checking 17 | RUN npm run build 18 | 19 | # Use nginx as production environment image 20 | FROM nginx:stable-alpine as production-stage 21 | 22 | # Copy built files to nginx 23 | COPY --from=build-stage /app/dist /usr/share/nginx/html 24 | 25 | # Copy nginx configuration 26 | COPY nginx.conf /etc/nginx/nginx.conf.template 27 | 28 | # Copy entrypoint script 29 | COPY docker-entrypoint.sh /docker-entrypoint.sh 30 | RUN chmod +x /docker-entrypoint.sh 31 | 32 | # Expose port 80 33 | EXPOSE 80 34 | 35 | # Start nginx with environment variable substitution 36 | CMD ["/docker-entrypoint.sh"] -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # AI Manus Frontend 2 | 3 | English | [中文](README_zh.md) 4 | 5 | This is an AI chatbot application built with Vue 3 + TypeScript + Vite. This project is ported from the React version, maintaining the same functionality and interface design. 6 | 7 | ## Features 8 | 9 | - Chat interface 10 | - Tool panels (Search, Files, Terminal, Browser) 11 | 12 | ## Installation 13 | 14 | Create a `.env.development` file with the following configuration: 15 | 16 | ``` 17 | # Backend address 18 | VITE_API_URL=http://127.0.0.1:8000 19 | ``` 20 | 21 | ```bash 22 | # Install dependencies 23 | npm install 24 | 25 | # Run in development mode 26 | npm run dev 27 | 28 | # Build production version 29 | npm run build 30 | ``` 31 | 32 | ## Docker Deployment 33 | 34 | This project supports containerized deployment using Docker: 35 | 36 | ```bash 37 | # Build Docker image 38 | docker build -t ai-chatbot-vue . 39 | 40 | # Run container (map container port 80 to host port 8080) 41 | docker run -d -p 8080:80 ai-chatbot-vue 42 | 43 | # Access the application 44 | # Open browser and visit http://localhost:8080 45 | ``` 46 | 47 | ## Project Structure 48 | 49 | ``` 50 | src/ 51 | ├── assets/ # Static resources and CSS files 52 | ├── components/ # Reusable components 53 | │ ├── ChatInput.vue # Chat input component 54 | │ ├── ChatMessage.vue # Chat message component 55 | │ ├── Sidebar.vue # Sidebar component 56 | │ ├── ToolPanel.vue # Tool panel component 57 | │ └── ui/ # UI components 58 | ├── pages/ # Page components 59 | │ ├── ChatPage.vue # Chat page 60 | │ └── HomePage.vue # Home page 61 | ├── App.vue # Root component 62 | ├── main.ts # Entry file 63 | └── index.css # Global styles 64 | ``` -------------------------------------------------------------------------------- /frontend/README_zh.md: -------------------------------------------------------------------------------- 1 | # AI Manus 前端 2 | 3 | [English](README.md) | 中文 4 | 5 | 这是一个使用 Vue 3 + TypeScript + Vite 构建的 AI 聊天机器人应用。该项目是从 React 版本移植过来的,保持了同样的功能和界面设计。 6 | 7 | ## 特性 8 | 9 | - 聊天界面 10 | - 工具面板(搜索、文件、终端、浏览器) 11 | 12 | ## 安装 13 | 14 | 创建`.env.development`文件,并创建以下配置: 15 | 16 | ``` 17 | # 后端地址 18 | VITE_API_URL=http://127.0.0.1:8000 19 | ``` 20 | 21 | ```bash 22 | # 安装依赖 23 | npm install 24 | 25 | # 开发模式运行 26 | npm run dev 27 | 28 | # 构建生产版本 29 | npm run build 30 | ``` 31 | 32 | ## Docker 部署 33 | 34 | 本项目支持使用 Docker 进行容器化部署: 35 | 36 | ```bash 37 | # 构建 Docker 镜像 38 | docker build -t ai-chatbot-vue . 39 | 40 | # 运行容器(将容器的80端口映射到主机的8080端口) 41 | docker run -d -p 8080:80 ai-chatbot-vue 42 | 43 | # 访问应用 44 | # 打开浏览器访问 http://localhost:8080 45 | ``` 46 | 47 | ## 项目结构 48 | 49 | ``` 50 | src/ 51 | ├── assets/ # 静态资源和CSS文件 52 | ├── components/ # 可复用组件 53 | │ ├── ChatInput.vue # 聊天输入组件 54 | │ ├── ChatMessage.vue # 聊天消息组件 55 | │ ├── Sidebar.vue # 侧边栏组件 56 | │ ├── ToolPanel.vue # 工具面板组件 57 | │ └── ui/ # UI组件 58 | ├── pages/ # 页面组件 59 | │ ├── ChatPage.vue # 聊天页面 60 | │ └── HomePage.vue # 首页 61 | ├── App.vue # 根组件 62 | ├── main.ts # 入口文件 63 | └── index.css # 全局样式 64 | ``` 65 | -------------------------------------------------------------------------------- /frontend/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Replace environment variables in nginx config 4 | envsubst '${BACKEND_URL}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf 5 | 6 | # Start nginx 7 | nginx -g "daemon off;" -------------------------------------------------------------------------------- /frontend/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Manus 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log notice; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 16 | '$status $body_bytes_sent "$http_referer" ' 17 | '"$http_user_agent" "$http_x_forwarded_for"'; 18 | 19 | access_log /var/log/nginx/access.log main; 20 | 21 | sendfile on; 22 | keepalive_timeout 65; 23 | 24 | map $http_upgrade $connection_upgrade { 25 | default upgrade; 26 | '' close; 27 | } 28 | 29 | server { 30 | listen 80; 31 | server_name localhost; 32 | 33 | location / { 34 | root /usr/share/nginx/html; 35 | index index.html index.htm; 36 | try_files $uri $uri/ /index.html; 37 | } 38 | 39 | location /api/ { 40 | resolver 127.0.0.11 valid=10s; # Docker DNS 41 | set $backend_url "${BACKEND_URL}"; 42 | proxy_pass $backend_url; 43 | proxy_http_version 1.1; 44 | proxy_set_header Host $host; 45 | proxy_set_header X-Real-IP $remote_addr; 46 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 47 | proxy_set_header X-Forwarded-Proto $scheme; 48 | 49 | # WebSocket support 50 | proxy_set_header Upgrade $http_upgrade; 51 | proxy_set_header Connection $connection_upgrade; 52 | proxy_read_timeout 300s; 53 | proxy_send_timeout 300s; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-vue-typescript-starter", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "type-check": "vue-tsc", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@microsoft/fetch-event-source": "^2.0.1", 14 | "@novnc/novnc": "^1.5.0", 15 | "@types/dompurify": "^3.2.0", 16 | "@vueuse/core": "^10.1.2", 17 | "axios": "^1.8.4", 18 | "clsx": "^2.0.0", 19 | "dompurify": "^3.2.5", 20 | "framer-motion": "^10.12.16", 21 | "lucide-vue-next": "^0.511.0", 22 | "marked": "^15.0.8", 23 | "monaco-editor": "^0.52.2", 24 | "tailwind-merge": "^1.13.1", 25 | "vue": "^3.3.4", 26 | "vue-i18n": "^9.14.4", 27 | "vue-router": "^4.2.2" 28 | }, 29 | "devDependencies": { 30 | "@tailwindcss/typography": "^0.5.16", 31 | "@vitejs/plugin-vue": "^4.2.3", 32 | "autoprefixer": "^10.4.14", 33 | "postcss": "^8.4.24", 34 | "tailwindcss": "^3.3.2", 35 | "typescript": "^5.1.3", 36 | "vite": "^4.3.9", 37 | "vite-plugin-monaco-editor": "^1.1.0", 38 | "vue-tsc": "^1.6.5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /frontend/public/chatting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | -------------------------------------------------------------------------------- /frontend/src/api/client.ts: -------------------------------------------------------------------------------- 1 | // Backend API client configuration 2 | import axios, { AxiosError } from 'axios'; 3 | 4 | // API configuration 5 | export const API_CONFIG = { 6 | host: import.meta.env.VITE_API_URL || '', 7 | version: 'v1', 8 | timeout: 30000, // Request timeout in milliseconds 9 | }; 10 | 11 | // Complete API base URL 12 | export const BASE_URL = API_CONFIG.host 13 | ? `${API_CONFIG.host}/api/${API_CONFIG.version}` 14 | : `/api/${API_CONFIG.version}`; 15 | 16 | // Unified response format 17 | export interface ApiResponse { 18 | code: number; 19 | msg: string; 20 | data: T; 21 | } 22 | 23 | // Error format 24 | export interface ApiError { 25 | code: number; 26 | message: string; 27 | details?: unknown; 28 | } 29 | 30 | // Create axios instance 31 | export const apiClient = axios.create({ 32 | baseURL: BASE_URL, 33 | timeout: API_CONFIG.timeout, 34 | headers: { 35 | 'Content-Type': 'application/json', 36 | }, 37 | }); 38 | 39 | // Request interceptor, can add authentication token etc. 40 | apiClient.interceptors.request.use( 41 | (config) => { 42 | // Authentication token can be added here 43 | // const token = localStorage.getItem('token'); 44 | // if (token) { 45 | // config.headers.Authorization = `Bearer ${token}`; 46 | // } 47 | return config; 48 | }, 49 | (error) => Promise.reject(error) 50 | ); 51 | 52 | // Response interceptor, unified error handling 53 | apiClient.interceptors.response.use( 54 | (response) => { 55 | // Check backend response format 56 | if (response.data && typeof response.data.code === 'number') { 57 | // If it's a business logic error (code not 0), convert to error handling 58 | if (response.data.code !== 0) { 59 | const apiError: ApiError = { 60 | code: response.data.code, 61 | message: response.data.msg || 'Unknown error', 62 | details: response.data 63 | }; 64 | return Promise.reject(apiError); 65 | } 66 | } 67 | return response; 68 | }, 69 | (error: AxiosError) => { 70 | const apiError: ApiError = { 71 | code: 500, 72 | message: 'Request failed', 73 | }; 74 | 75 | if (error.response) { 76 | const status = error.response.status; 77 | apiError.code = status; 78 | 79 | // Try to extract detailed error information from response content 80 | if (error.response.data && typeof error.response.data === 'object') { 81 | const data = error.response.data as any; 82 | if (data.code && data.msg) { 83 | apiError.code = data.code; 84 | apiError.message = data.msg; 85 | } else { 86 | apiError.message = data.message || error.response.statusText || 'Request failed'; 87 | } 88 | apiError.details = data; 89 | } else { 90 | apiError.message = error.response.statusText || 'Request failed'; 91 | } 92 | } else if (error.request) { 93 | apiError.code = 503; 94 | apiError.message = 'Network error, please check your connection'; 95 | } 96 | 97 | console.error('API Error:', apiError); 98 | return Promise.reject(apiError); 99 | } 100 | ); -------------------------------------------------------------------------------- /frontend/src/assets/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | #app { 7 | display: contents; 8 | } 9 | 10 | body,html { 11 | width: 100%; 12 | height: 100%; 13 | margin: 0; 14 | padding: 0; 15 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI Variable Display,Segoe UI,Helvetica,Apple Color Emoji,Arial,sans-serif,Segoe UI Emoji,Segoe UI Symbol; 16 | background-color: var(--background-white-main); 17 | color: var(--text-primary) 18 | } 19 | 20 | .clickable { 21 | cursor: pointer; 22 | -webkit-user-select: none; 23 | -moz-user-select: none; 24 | user-select: none; 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/components/BrowserToolView.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 79 | -------------------------------------------------------------------------------- /frontend/src/components/CustomDialog.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | -------------------------------------------------------------------------------- /frontend/src/components/SearchToolView.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/ShellToolView.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 92 | -------------------------------------------------------------------------------- /frontend/src/components/ToolUse.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 54 | -------------------------------------------------------------------------------- /frontend/src/components/icons/AttachmentIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/icons/BrowserIcon.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | -------------------------------------------------------------------------------- /frontend/src/components/icons/EditIcon.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 53 | -------------------------------------------------------------------------------- /frontend/src/components/icons/ErrorIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/icons/InfoIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/icons/SearchIcon.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | -------------------------------------------------------------------------------- /frontend/src/components/icons/SendIcon.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/icons/ShellIcon.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | -------------------------------------------------------------------------------- /frontend/src/components/icons/SpinnigIcon.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /frontend/src/components/icons/StepSuccessIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/icons/SuccessIcon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/composables/useContextMenu.ts: -------------------------------------------------------------------------------- 1 | import { ref, markRaw } from 'vue' 2 | 3 | export interface MenuItem { 4 | key: string; 5 | label: string; 6 | icon?: any; // Vue component or SVG 7 | variant?: 'default' | 'danger'; 8 | checked?: boolean; 9 | disabled?: boolean; 10 | action?: (itemId: string) => void; 11 | } 12 | 13 | // Global state for context menu 14 | const contextMenuVisible = ref(false) 15 | const selectedItemId = ref() 16 | const menuPosition = ref({ x: 0, y: 0 }) 17 | const menuItems = ref([]) 18 | const menuItemClickHandler = ref<((itemKey: string, itemId: string) => void) | null>(null) 19 | const onCloseHandler = ref<((itemId: string) => void) | null>(null) 20 | const targetElement = ref(null) 21 | 22 | export function useContextMenu() { 23 | // Show context menu with specific menu items 24 | const showContextMenu = ( 25 | itemId: string, 26 | element: HTMLElement, 27 | items: MenuItem[], 28 | onMenuItemClick?: (itemKey: string, itemId: string) => void, 29 | onClose?: (itemId: string) => void 30 | ) => { 31 | hideContextMenu() 32 | selectedItemId.value = itemId 33 | targetElement.value = element 34 | menuItems.value = items 35 | menuItemClickHandler.value = onMenuItemClick || null 36 | onCloseHandler.value = onClose || null 37 | contextMenuVisible.value = true 38 | } 39 | 40 | // Hide context menu 41 | const hideContextMenu = () => { 42 | const currentItemId = selectedItemId.value 43 | const currentOnCloseHandler = onCloseHandler.value 44 | 45 | contextMenuVisible.value = false 46 | selectedItemId.value = undefined 47 | menuItems.value = [] 48 | menuItemClickHandler.value = null 49 | onCloseHandler.value = null 50 | targetElement.value = null 51 | 52 | // Call onClose callback if provided 53 | if (currentOnCloseHandler && currentItemId) { 54 | currentOnCloseHandler(currentItemId) 55 | } 56 | } 57 | 58 | // Handle menu item click (called from ContextMenu component) 59 | const handleMenuItemClick = (item: MenuItem) => { 60 | if (item.disabled) return; 61 | 62 | if (selectedItemId.value) { 63 | // Call the provided handler 64 | if (menuItemClickHandler.value) { 65 | menuItemClickHandler.value(item.key, selectedItemId.value); 66 | } 67 | 68 | // Execute action if provided 69 | if (item.action) { 70 | item.action(selectedItemId.value); 71 | } 72 | } 73 | 74 | hideContextMenu(); 75 | } 76 | 77 | return { 78 | // Reactive state 79 | contextMenuVisible, 80 | selectedItemId, 81 | menuPosition, 82 | menuItems, 83 | targetElement, 84 | 85 | // Actions 86 | showContextMenu, 87 | hideContextMenu, 88 | handleMenuItemClick 89 | } 90 | } 91 | 92 | // Utility functions for creating common menu items 93 | export const createMenuItem = ( 94 | key: string, 95 | label: string, 96 | options: Partial> = {} 97 | ): MenuItem => ({ 98 | key, 99 | label, 100 | variant: 'default', 101 | ...options, 102 | icon: options.icon ? markRaw(options.icon) : options.icon 103 | }) 104 | 105 | export const createDangerMenuItem = ( 106 | key: string, 107 | label: string, 108 | options: Partial> = {} 109 | ): MenuItem => ({ 110 | key, 111 | label, 112 | variant: 'danger', 113 | ...options, 114 | icon: options.icon ? markRaw(options.icon) : options.icon 115 | }) 116 | 117 | export const createSeparator = (): MenuItem => ({ 118 | key: 'separator', 119 | label: '', 120 | disabled: true, 121 | }) -------------------------------------------------------------------------------- /frontend/src/composables/useDialog.ts: -------------------------------------------------------------------------------- 1 | import { ref, reactive, readonly } from 'vue' 2 | import { useI18n } from 'vue-i18n' 3 | 4 | // Dialog state 5 | interface DialogState { 6 | title: string 7 | content: string 8 | confirmText: string 9 | cancelText: string 10 | confirmType: 'primary' | 'danger' 11 | onConfirm?: () => void | Promise 12 | onCancel?: () => void 13 | } 14 | 15 | // Global state 16 | const dialogVisible = ref(false) 17 | const dialogConfig = reactive({ 18 | title: '', 19 | content: '', 20 | confirmText: '', 21 | cancelText: '', 22 | confirmType: 'primary', 23 | onConfirm: undefined, 24 | onCancel: undefined 25 | }) 26 | 27 | export function useDialog() { 28 | const { t } = useI18n() 29 | 30 | // Handle confirm 31 | const handleConfirm = async () => { 32 | if (dialogConfig.onConfirm) { 33 | await dialogConfig.onConfirm() 34 | } 35 | dialogVisible.value = false 36 | } 37 | 38 | // Handle cancel 39 | const handleCancel = () => { 40 | if (dialogConfig.onCancel) { 41 | dialogConfig.onCancel() 42 | } 43 | dialogVisible.value = false 44 | } 45 | 46 | // Show general confirm dialog 47 | const showConfirmDialog = (options: { 48 | title: string 49 | content: string 50 | confirmText?: string 51 | cancelText?: string 52 | confirmType?: 'primary' | 'danger' 53 | onConfirm?: () => void | Promise 54 | onCancel?: () => void 55 | }) => { 56 | Object.assign(dialogConfig, { 57 | title: options.title, 58 | content: options.content, 59 | confirmText: options.confirmText || t('Confirm'), 60 | cancelText: options.cancelText || t('Cancel'), 61 | confirmType: options.confirmType || 'primary', 62 | onConfirm: options.onConfirm, 63 | onCancel: options.onCancel 64 | }) 65 | dialogVisible.value = true 66 | } 67 | 68 | // Show delete session dialog 69 | const showDeleteSessionDialog = (onConfirm?: () => void | Promise) => { 70 | showConfirmDialog({ 71 | title: t('Are you sure you want to delete this session?'), 72 | content: t('The chat history of this session cannot be recovered after deletion.'), 73 | confirmText: t('Delete'), 74 | cancelText: t('Cancel'), 75 | confirmType: 'danger', 76 | onConfirm 77 | }) 78 | } 79 | 80 | return { 81 | dialogVisible: readonly(dialogVisible), 82 | dialogConfig: readonly(dialogConfig), 83 | handleConfirm, 84 | handleCancel, 85 | showConfirmDialog, 86 | showDeleteSessionDialog 87 | } 88 | } -------------------------------------------------------------------------------- /frontend/src/composables/useI18n.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import { ref, watch } from 'vue' 3 | import messages from '../locales' 4 | import type { Locale } from '../locales' 5 | 6 | const STORAGE_KEY = 'manus-locale' 7 | 8 | // Get browser language and map to supported locale 9 | const getBrowserLocale = (): Locale => { 10 | const browserLang = navigator.language || navigator.languages?.[0] 11 | // Check if browser language starts with any supported locale 12 | if (browserLang?.startsWith('zh')) { 13 | return 'zh' 14 | } 15 | if (browserLang?.startsWith('en')) { 16 | return 'en' 17 | } 18 | // Default to Chinese if no match 19 | return 'en' 20 | } 21 | 22 | // Get current language from localStorage, default to browser language 23 | const getStoredLocale = (): Locale => { 24 | const storedLocale = localStorage.getItem(STORAGE_KEY) 25 | return (storedLocale as Locale) || getBrowserLocale() 26 | } 27 | 28 | // Create i18n instance 29 | export const i18n = createI18n({ 30 | legacy: false, // Use Composition API mode 31 | locale: getStoredLocale(), 32 | fallbackLocale: 'en', 33 | messages 34 | }) 35 | 36 | // Create a composable to use in components 37 | export function useLocale() { 38 | const currentLocale = ref(getStoredLocale()) 39 | 40 | // Switch language 41 | const setLocale = (locale: Locale) => { 42 | i18n.global.locale.value = locale 43 | currentLocale.value = locale 44 | localStorage.setItem(STORAGE_KEY, locale) 45 | document.querySelector('html')?.setAttribute('lang', locale) 46 | } 47 | 48 | // Watch language change 49 | watch(currentLocale, (val) => { 50 | setLocale(val) 51 | }) 52 | 53 | return { 54 | currentLocale, 55 | setLocale 56 | } 57 | } 58 | 59 | export default i18n -------------------------------------------------------------------------------- /frontend/src/composables/usePanelState.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from 'vue' 2 | import type { PanelState } from '../types/panel' 3 | 4 | // Local storage key for panel state 5 | const PANEL_STATE_KEY = 'manus-panel-state' 6 | 7 | // Read initial state from localStorage 8 | const getInitialPanelState = (): boolean => { 9 | try { 10 | const saved = localStorage.getItem(PANEL_STATE_KEY) 11 | return saved ? JSON.parse(saved) : false 12 | } catch (error) { 13 | console.error('Failed to read panel state from localStorage:', error) 14 | return false 15 | } 16 | } 17 | 18 | // Global panel state management 19 | const isPanelShow = ref(getInitialPanelState()) 20 | 21 | // Save panel state to localStorage 22 | const savePanelState = (state: boolean) => { 23 | try { 24 | localStorage.setItem(PANEL_STATE_KEY, JSON.stringify(state)) 25 | } catch (error) { 26 | console.error('Failed to save panel state to localStorage:', error) 27 | } 28 | } 29 | 30 | // Watch for panel state changes and save to localStorage 31 | watch(isPanelShow, (newValue) => { 32 | savePanelState(newValue) 33 | }, { immediate: false }) 34 | 35 | export function usePanelState(): PanelState { 36 | // Toggle panel visibility 37 | const togglePanel = () => { 38 | isPanelShow.value = !isPanelShow.value 39 | } 40 | 41 | // Set panel visibility 42 | const setPanel = (visible: boolean) => { 43 | isPanelShow.value = visible 44 | } 45 | 46 | // Show panel 47 | const showPanel = () => { 48 | isPanelShow.value = true 49 | } 50 | 51 | // Hide panel 52 | const hidePanel = () => { 53 | isPanelShow.value = false 54 | } 55 | 56 | return { 57 | isPanelShow, 58 | togglePanel, 59 | setPanel, 60 | showPanel, 61 | hidePanel 62 | } 63 | } -------------------------------------------------------------------------------- /frontend/src/composables/useTime.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed, onMounted, onUnmounted } from 'vue'; 2 | import { formatRelativeTime, formatCustomTime } from '../utils/time'; 3 | import { useI18n } from 'vue-i18n'; 4 | 5 | export function useRelativeTime() { 6 | // Create a reactive current time variable to trigger re-rendering 7 | const currentTime = ref(Date.now()); 8 | 9 | // Set a timer to update the time every minute 10 | let timer: number | null = null; 11 | 12 | onMounted(() => { 13 | timer = window.setInterval(() => { 14 | currentTime.value = Date.now(); 15 | }, 60000); // Update every minute 16 | }); 17 | 18 | onUnmounted(() => { 19 | if (timer !== null) { 20 | clearInterval(timer); 21 | timer = null; 22 | } 23 | }); 24 | 25 | // Calculate relative time, depends on currentTime for automatic updates 26 | const relativeTime = computed(() => { 27 | currentTime.value; // Depends on currentTime, recalculate when currentTime updates 28 | return (timestamp: number) => formatRelativeTime(timestamp); 29 | }); 30 | 31 | return { 32 | relativeTime 33 | }; 34 | } 35 | 36 | export function useCustomTime() { 37 | const { t, locale } = useI18n(); 38 | 39 | // Create a reactive current time variable to trigger re-rendering 40 | const currentTime = ref(Date.now()); 41 | 42 | // Set a timer to update the time every minute 43 | let timer: number | null = null; 44 | 45 | onMounted(() => { 46 | timer = window.setInterval(() => { 47 | currentTime.value = Date.now(); 48 | }, 60000); // Update every minute 49 | }); 50 | 51 | onUnmounted(() => { 52 | if (timer !== null) { 53 | clearInterval(timer); 54 | timer = null; 55 | } 56 | }); 57 | 58 | // Calculate custom formatted time, depends on currentTime for automatic updates 59 | const customTime = computed(() => { 60 | currentTime.value; // Depends on currentTime, recalculate when currentTime updates 61 | return (timestamp: number) => formatCustomTime(timestamp, t, locale.value); 62 | }); 63 | 64 | return { 65 | customTime 66 | }; 67 | } -------------------------------------------------------------------------------- /frontend/src/composables/useTool.ts: -------------------------------------------------------------------------------- 1 | import { computed, Ref } from 'vue'; 2 | import { ToolContent } from '../types/message'; 3 | import { useI18n } from 'vue-i18n'; 4 | import { TOOL_ICON_MAP, TOOL_NAME_MAP, TOOL_FUNCTION_MAP, TOOL_FUNCTION_ARG_MAP, TOOL_COMPONENT_MAP } from '../constants/tool'; 5 | 6 | export function useToolInfo(tool?: Ref) { 7 | const { t } = useI18n(); 8 | 9 | const toolInfo = computed(() => { 10 | if (!tool || !tool.value) return null; 11 | let functionArg = tool.value.args[TOOL_FUNCTION_ARG_MAP[tool.value.function]] || ''; 12 | if (TOOL_FUNCTION_ARG_MAP[tool.value.function] === 'file') { 13 | functionArg = functionArg.replace(/^\/home\/ubuntu\//, ''); 14 | } 15 | return { 16 | icon: TOOL_ICON_MAP[tool.value.name] || null, 17 | name: t(TOOL_NAME_MAP[tool.value.name] || ''), 18 | function: t(TOOL_FUNCTION_MAP[tool.value.function] || ''), 19 | functionArg: functionArg, 20 | view: TOOL_COMPONENT_MAP[tool.value.name] || null 21 | }; 22 | }); 23 | 24 | return { 25 | toolInfo 26 | }; 27 | } -------------------------------------------------------------------------------- /frontend/src/constants/tool.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tool function mapping 3 | */ 4 | export const TOOL_FUNCTION_MAP: {[key: string]: string} = { 5 | // Shell tools 6 | "shell_exec": "Executing command", 7 | "shell_view": "Viewing command output", 8 | "shell_wait": "Waiting for command completion", 9 | "shell_write_to_process": "Writing data to process", 10 | "shell_kill_process": "Terminating process", 11 | 12 | // File tools 13 | "file_read": "Reading file", 14 | "file_write": "Writing file", 15 | "file_str_replace": "Replacing file content", 16 | "file_find_in_content": "Searching file content", 17 | "file_find_by_name": "Finding file", 18 | 19 | // Browser tools 20 | "browser_view": "Viewing webpage", 21 | "browser_navigate": "Navigating to webpage", 22 | "browser_restart": "Restarting browser", 23 | "browser_click": "Clicking element", 24 | "browser_input": "Entering text", 25 | "browser_move_mouse": "Moving mouse", 26 | "browser_press_key": "Pressing key", 27 | "browser_select_option": "Selecting option", 28 | "browser_scroll_up": "Scrolling up", 29 | "browser_scroll_down": "Scrolling down", 30 | "browser_console_exec": "Executing JS code", 31 | "browser_console_view": "Viewing console output", 32 | 33 | // Search tools 34 | "info_search_web": "Searching web", 35 | 36 | // Message tools 37 | "message_notify_user": "Sending notification", 38 | "message_ask_user": "Asking question" 39 | }; 40 | 41 | /** 42 | * Display name mapping for tool function parameters 43 | */ 44 | export const TOOL_FUNCTION_ARG_MAP: {[key: string]: string} = { 45 | "shell_exec": "command", 46 | "shell_view": "shell", 47 | "shell_wait": "shell", 48 | "shell_write_to_process": "input", 49 | "shell_kill_process": "shell", 50 | "file_read": "file", 51 | "file_write": "file", 52 | "file_str_replace": "file", 53 | "file_find_in_content": "file", 54 | "file_find_by_name": "path", 55 | "browser_view": "page", 56 | "browser_navigate": "url", 57 | "browser_restart": "url", 58 | "browser_click": "element", 59 | "browser_input": "text", 60 | "browser_move_mouse": "position", 61 | "browser_press_key": "key", 62 | "browser_select_option": "option", 63 | "browser_scroll_up": "page", 64 | "browser_scroll_down": "page", 65 | "browser_console_exec": "code", 66 | "browser_console_view": "console", 67 | "info_search_web": "query", 68 | "message_notify_user": "message", 69 | "message_ask_user": "question" 70 | }; 71 | 72 | /** 73 | * Tool name mapping 74 | */ 75 | export const TOOL_NAME_MAP: {[key: string]: string} = { 76 | "shell": "Terminal", 77 | "file": "File", 78 | "browser": "Browser", 79 | "info": "Information", 80 | "message": "Message" 81 | }; 82 | 83 | import SearchIcon from '../components/icons/SearchIcon.vue'; 84 | import EditIcon from '../components/icons/EditIcon.vue'; 85 | import BrowserIcon from '../components/icons/BrowserIcon.vue'; 86 | import ShellIcon from '../components/icons/ShellIcon.vue'; 87 | 88 | /** 89 | * Tool icon mapping 90 | */ 91 | export const TOOL_ICON_MAP: {[key: string]: any} = { 92 | "shell": ShellIcon, 93 | "file": EditIcon, 94 | "browser": BrowserIcon, 95 | "search": SearchIcon, 96 | "message": "" 97 | }; 98 | 99 | import ShellToolView from '../components/ShellToolView.vue'; 100 | import FileToolView from '../components/FileToolView.vue'; 101 | import SearchToolView from '../components/SearchToolView.vue'; 102 | import BrowserToolView from '../components/BrowserToolView.vue'; 103 | 104 | /** 105 | * Mapping from tool names to components 106 | */ 107 | export const TOOL_COMPONENT_MAP: {[key: string]: any} = { 108 | "shell": ShellToolView, 109 | "file": FileToolView, 110 | "search": SearchToolView, 111 | "browser": BrowserToolView 112 | }; 113 | -------------------------------------------------------------------------------- /frontend/src/locales/en.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Hello': 'Hello', 3 | 'What can I do for you?': 'What can I do for you?', 4 | 'Failed to create agent, please try again later': 'Failed to create agent, please try again later', 5 | 'New Chat': 'New Chat', 6 | 'New Task': 'New Task', 7 | 'Thinking': 'Thinking', 8 | 'Task Progress': 'Task Progress', 9 | 'Task Completed': 'Task Completed', 10 | 'Create a task to get started': 'Create a task to get started', 11 | 'Delete': 'Delete', 12 | 'Just now': 'Just now', 13 | 'minutes ago': 'minutes ago', 14 | 'hours ago': 'hours ago', 15 | 'days ago': 'days ago', 16 | 'months ago': 'months ago', 17 | 'years ago': 'years ago', 18 | // Weekdays 19 | 'Sunday': 'Sun', 20 | 'Monday': 'Mon', 21 | 'Tuesday': 'Tue', 22 | 'Wednesday': 'Wed', 23 | 'Thursday': 'Thu', 24 | 'Friday': 'Fri', 25 | 'Saturday': 'Sat', 26 | 'Manus Computer': 'Manus Computer', 27 | 'Manus is using': 'Manus is using', 28 | 'Jump to live': 'Jump to live', 29 | 'Failed to load file content': 'Failed to load file content', 30 | 'Give Manus a task to work on...': 'Give Manus a task to work on...', 31 | // Shell tools 32 | 'Executing command': 'Executing command', 33 | 'Viewing command output': 'Viewing command output', 34 | 'Waiting for command completion': 'Waiting for command completion', 35 | 'Writing data to process': 'Writing data to process', 36 | 'Terminating process': 'Terminating process', 37 | // File tools 38 | 'Reading file': 'Reading file', 39 | 'Writing file': 'Writing file', 40 | 'Replacing file content': 'Replacing file content', 41 | 'Searching file content': 'Searching file content', 42 | 'Finding file': 'Finding file', 43 | // Browser tools 44 | 'Viewing webpage': 'Viewing webpage', 45 | 'Navigating to webpage': 'Navigating to webpage', 46 | 'Restarting browser': 'Restarting browser', 47 | 'Clicking element': 'Clicking element', 48 | 'Entering text': 'Entering text', 49 | 'Moving mouse': 'Moving mouse', 50 | 'Pressing key': 'Pressing key', 51 | 'Selecting option': 'Selecting option', 52 | 'Scrolling up': 'Scrolling up', 53 | 'Scrolling down': 'Scrolling down', 54 | 'Executing JS code': 'Executing JS code', 55 | 'Viewing console output': 'Viewing console output', 56 | // Search tools 57 | 'Searching web': 'Searching web', 58 | // Message tools 59 | 'Sending notification': 'Sending notification', 60 | 'Asking question': 'Asking question', 61 | // Tool names 62 | 'Terminal': 'Terminal', 63 | 'File': 'File', 64 | 'Browser': 'Browser', 65 | 'Information': 'Information', 66 | 'Message': 'Message', 67 | // Dialog 68 | 'Confirm': 'Confirm', 69 | 'Cancel': 'Cancel', 70 | 'Close Dialog': 'Close Dialog', 71 | 'Are you sure you want to delete this session?': 'Are you sure you want to delete this session?', 72 | 'The chat history of this session cannot be recovered after deletion.': 'The chat history of this session cannot be recovered after deletion.', 73 | 'Deleted successfully': 'Deleted successfully', 74 | 'Failed to delete session': 'Failed to delete session' 75 | } -------------------------------------------------------------------------------- /frontend/src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import en from './en' 2 | import zh from './zh' 3 | 4 | export default { 5 | en, 6 | zh 7 | } 8 | 9 | export type Locale = 'en' | 'zh' 10 | 11 | export const availableLocales: { label: string; value: Locale }[] = [ 12 | { label: 'English', value: 'en' }, 13 | { label: '中文', value: 'zh' } 14 | ] -------------------------------------------------------------------------------- /frontend/src/locales/zh.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Hello': '你好', 3 | 'What can I do for you?': '我能为你做什么?', 4 | 'Failed to create agent, please try again later': '创建Agent失败,请稍后重试', 5 | 'New Chat': '新对话', 6 | 'New Task': '新建任务', 7 | 'Thinking': '思考中', 8 | 'Task Progress': '任务进度', 9 | 'Task Completed': '任务已完成', 10 | 'Create a task to get started': '新建一个任务以开始', 11 | 'Delete': '删除', 12 | 'Just now': '刚刚', 13 | 'minutes ago': '分钟前', 14 | 'hours ago': '小时前', 15 | 'days ago': '天前', 16 | 'months ago': '个月前', 17 | 'years ago': '年前', 18 | // Weekdays 19 | 'Sunday': '周日', 20 | 'Monday': '周一', 21 | 'Tuesday': '周二', 22 | 'Wednesday': '周三', 23 | 'Thursday': '周四', 24 | 'Friday': '周五', 25 | 'Saturday': '周六', 26 | 'Manus Computer': 'Manus 的电脑', 27 | 'Manus is using': 'Manus 正在使用', 28 | 'Jump to live': '跳到实时', 29 | 'Failed to load file content': '加载文件内容失败', 30 | 'Give Manus a task to work on...': '给 Manus 一个任务...', 31 | // Shell tools 32 | 'Executing command': '正在执行命令', 33 | 'Viewing command output': '正在查看命令输出', 34 | 'Waiting for command completion': '正在等待命令完成', 35 | 'Writing data to process': '正在向进程写入数据', 36 | 'Terminating process': '正在终止进程', 37 | // File tools 38 | 'Reading file': '正在读取文件', 39 | 'Writing file': '正在写入文件', 40 | 'Replacing file content': '正在替换文件内容', 41 | 'Searching file content': '正在搜索文件内容', 42 | 'Finding file': '正在查找文件', 43 | // Browser tools 44 | 'Viewing webpage': '正在查看网页', 45 | 'Navigating to webpage': '正在导航到网页', 46 | 'Restarting browser': '正在重启浏览器', 47 | 'Clicking element': '正在点击元素', 48 | 'Entering text': '正在输入文本', 49 | 'Moving mouse': '正在移动鼠标', 50 | 'Pressing key': '正在按键', 51 | 'Selecting option': '正在选择选项', 52 | 'Scrolling up': '正在向上滚动', 53 | 'Scrolling down': '正在向下滚动', 54 | 'Executing JS code': '正在执行JS代码', 55 | 'Viewing console output': '正在查看控制台输出', 56 | // Search tools 57 | 'Searching web': '正在搜索网络', 58 | // Message tools 59 | 'Sending notification': '正在发送通知', 60 | 'Asking question': '正在提问', 61 | // Tool names 62 | 'Terminal': '终端', 63 | 'File': '文件', 64 | 'Browser': '浏览器', 65 | 'Information': '信息', 66 | 'Message': '消息', 67 | // Dialog 68 | 'Confirm': '确认', 69 | 'Cancel': '取消', 70 | 'Close Dialog': '关闭对话框', 71 | 'Are you sure you want to delete this session?': '确定要删除该会话吗?', 72 | 'The chat history of this session cannot be recovered after deletion.': '删除后将无法恢复该会话的聊天记录。', 73 | 'Deleted successfully': '删除成功', 74 | 'Failed to delete session': '删除会话失败' 75 | } -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createRouter, createWebHistory } from 'vue-router' 3 | import App from './App.vue' 4 | import './assets/global.css' 5 | import './assets/theme.css' 6 | import './utils/toast' 7 | import i18n from './composables/useI18n' 8 | 9 | // Import page components 10 | import HomePage from './pages/HomePage.vue' 11 | import ChatPage from './pages/ChatPage.vue' 12 | 13 | // Create router 14 | const router = createRouter({ 15 | history: createWebHistory(), 16 | routes: [ 17 | { path: '/', component: HomePage }, 18 | { path: '/chat', component: ChatPage }, 19 | { path: '/chat/:sessionId', component: ChatPage } 20 | ] 21 | }) 22 | 23 | const app = createApp(App) 24 | 25 | app.use(router) 26 | app.use(i18n) 27 | app.mount('#app') -------------------------------------------------------------------------------- /frontend/src/pages/HomePage.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 74 | -------------------------------------------------------------------------------- /frontend/src/types/event.ts: -------------------------------------------------------------------------------- 1 | export type AgentSSEEvent = { 2 | event: 'tool' | 'step' | 'message' | 'error' | 'done' | 'title'; 3 | data: ToolEventData | StepEventData | MessageEventData | ErrorEventData | DoneEventData | TitleEventData; 4 | } 5 | 6 | export interface BaseEventData { 7 | event_id: string; 8 | timestamp: number; 9 | } 10 | 11 | export interface ToolEventData extends BaseEventData { 12 | name: string; 13 | function: string; 14 | args: {[key: string]: any}; 15 | } 16 | 17 | export interface StepEventData extends BaseEventData { 18 | status: "pending" | "running" | "completed" | "failed" 19 | id: string 20 | description: string 21 | } 22 | 23 | export interface MessageEventData extends BaseEventData { 24 | content: string; 25 | role: "user" | "assistant"; 26 | } 27 | 28 | export interface ErrorEventData extends BaseEventData { 29 | error: string; 30 | } 31 | 32 | export interface DoneEventData extends BaseEventData { 33 | } 34 | 35 | export interface TitleEventData extends BaseEventData { 36 | title: string; 37 | } 38 | 39 | export interface PlanEventData extends BaseEventData { 40 | steps: StepEventData[]; 41 | } -------------------------------------------------------------------------------- /frontend/src/types/message.ts: -------------------------------------------------------------------------------- 1 | export type MessageType = "user" | "assistant" | "tool" | "step"; 2 | 3 | export interface Message { 4 | type: MessageType; 5 | content: BaseContent; 6 | } 7 | 8 | export interface BaseContent { 9 | timestamp: number; 10 | } 11 | 12 | export interface MessageContent extends BaseContent { 13 | content: string; 14 | } 15 | 16 | export interface ToolContent extends BaseContent { 17 | name: string; 18 | function: string; 19 | args: any; 20 | result?: any; 21 | } 22 | 23 | export interface StepContent extends BaseContent { 24 | id: string; 25 | description: string; 26 | status: 'pending' | 'running' | 'completed' | 'failed'; 27 | tools: ToolContent[]; 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/types/panel.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue' 2 | 3 | export interface PanelState { 4 | isPanelShow: Ref 5 | togglePanel: () => void 6 | setPanel: (visible: boolean) => void 7 | showPanel: () => void 8 | hidePanel: () => void 9 | } -------------------------------------------------------------------------------- /frontend/src/types/response.ts: -------------------------------------------------------------------------------- 1 | import { AgentSSEEvent } from "./event"; 2 | 3 | export enum SessionStatus { 4 | ACTIVE = "active", 5 | COMPLETED = "completed" 6 | } 7 | 8 | export interface CreateSessionResponse { 9 | session_id: string; 10 | } 11 | 12 | export interface GetSessionResponse { 13 | session_id: string; 14 | title: string | null; 15 | events: AgentSSEEvent[]; 16 | } 17 | 18 | export interface ListSessionItem { 19 | session_id: string; 20 | title: string | null; 21 | latest_message: string | null; 22 | latest_message_at: number | null; 23 | status: SessionStatus; 24 | unread_message_count: number; 25 | } 26 | 27 | export interface ListSessionResponse { 28 | sessions: ListSessionItem[]; 29 | } 30 | 31 | export interface ConsoleRecord { 32 | ps1: string; 33 | command: string; 34 | output: string; 35 | } 36 | 37 | export interface ShellViewResponse { 38 | output: string; 39 | session_id: string; 40 | console: ConsoleRecord[]; 41 | } 42 | 43 | export interface FileViewResponse { 44 | content: string; 45 | file: string; 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/utils/time.ts: -------------------------------------------------------------------------------- 1 | import { useI18n } from 'vue-i18n'; 2 | 3 | /** 4 | * Time related utility functions 5 | */ 6 | 7 | /** 8 | * Convert timestamp to relative time (e.g., minutes ago, hours ago, days ago) 9 | * @param timestamp Timestamp (seconds) 10 | * @returns Formatted relative time string 11 | */ 12 | export const formatRelativeTime = (timestamp: number): string => { 13 | const { t } = useI18n(); 14 | const now = Math.floor(Date.now() / 1000); 15 | const diffSec = now - timestamp; 16 | const diffMin = Math.floor(diffSec / 60); 17 | const diffHour = Math.floor(diffMin / 60); 18 | const diffDay = Math.floor(diffHour / 24); 19 | const diffMonth = Math.floor(diffDay / 30); 20 | const diffYear = Math.floor(diffMonth / 12); 21 | 22 | if (diffSec < 60) { 23 | return t('Just now'); 24 | } else if (diffMin < 60) { 25 | return `${diffMin} ${t('minutes ago')}`; 26 | } else if (diffHour < 24) { 27 | return `${diffHour} ${t('hours ago')}`; 28 | } else if (diffDay < 30) { 29 | return `${diffDay} ${t('days ago')}`; 30 | } else if (diffMonth < 12) { 31 | return `${diffMonth} ${t('months ago')}`; 32 | } else { 33 | return `${diffYear} ${t('years ago')}`; 34 | } 35 | }; 36 | 37 | /** 38 | * Format timestamp according to custom requirements: 39 | * - Today: show time (HH:MM) 40 | * - This week: show day of week (e.g., 周一) 41 | * - This year: show date (MM/DD) 42 | * - Other years: show year/month (YYYY/MM) 43 | * @param timestamp Timestamp (seconds) 44 | * @param t Translation function from i18n 45 | * @param locale Current locale (for date formatting) 46 | * @returns Formatted time string 47 | */ 48 | export const formatCustomTime = (timestamp: number, t?: (key: string) => string, locale?: string): string => { 49 | const date = new Date(timestamp * 1000); 50 | const now = new Date(); 51 | 52 | // Check if it's today 53 | const isToday = date.toDateString() === now.toDateString(); 54 | if (isToday) { 55 | // Use locale-appropriate time format 56 | const timeFormat = locale?.startsWith('zh') ? 'zh-CN' : 'en-US'; 57 | return date.toLocaleTimeString(timeFormat, { 58 | hour: '2-digit', 59 | minute: '2-digit', 60 | hour12: false 61 | }); 62 | } 63 | 64 | // Check if it's this week 65 | const startOfWeek = new Date(now); 66 | startOfWeek.setDate(now.getDate() - now.getDay() + 1); // Monday as start of week 67 | startOfWeek.setHours(0, 0, 0, 0); 68 | 69 | const endOfWeek = new Date(startOfWeek); 70 | endOfWeek.setDate(startOfWeek.getDate() + 6); 71 | endOfWeek.setHours(23, 59, 59, 999); 72 | 73 | if (date >= startOfWeek && date <= endOfWeek) { 74 | const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; 75 | if (t) { 76 | return t(weekdays[date.getDay()]); 77 | } else { 78 | return weekdays[date.getDay()]; 79 | } 80 | } 81 | 82 | // Check if it's this year 83 | const isThisYear = date.getFullYear() === now.getFullYear(); 84 | if (isThisYear) { 85 | // Use locale-appropriate date format 86 | if (locale?.startsWith('zh')) { 87 | return `${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`; 88 | } else { 89 | // For English and other locales, use MM/DD format 90 | return `${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`; 91 | } 92 | } 93 | 94 | // Other years: show year/month 95 | return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}`; 96 | }; -------------------------------------------------------------------------------- /frontend/src/utils/toast.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Toast service 3 | * Provide global Toast message notification functionality 4 | */ 5 | 6 | type ToastType = 'error' | 'info' | 'success'; 7 | 8 | interface ToastOptions { 9 | message: string; 10 | type?: ToastType; 11 | duration?: number; 12 | } 13 | 14 | /** 15 | * Show Toast message 16 | * @param options - Toast configuration or message string 17 | */ 18 | export function showToast(options: ToastOptions | string): void { 19 | let config: ToastOptions; 20 | 21 | if (typeof options === 'string') { 22 | config = { message: options }; 23 | } else { 24 | config = options; 25 | } 26 | 27 | // Default configuration 28 | const detail = { 29 | message: config.message, 30 | type: config.type || 'info', 31 | duration: config.duration === undefined ? 3000 : config.duration 32 | }; 33 | 34 | // Create custom event 35 | const event = new CustomEvent('toast', { detail }); 36 | 37 | // Trigger event 38 | window.dispatchEvent(event); 39 | } 40 | 41 | // Convenient methods 42 | export function showErrorToast(message: string, duration?: number): void { 43 | showToast({ message, type: 'error', duration }); 44 | } 45 | 46 | export function showInfoToast(message: string, duration?: number): void { 47 | showToast({ message, type: 'info', duration }); 48 | } 49 | 50 | export function showSuccessToast(message: string, duration?: number): void { 51 | showToast({ message, type: 'success', duration }); 52 | } 53 | 54 | // To support non-Vue page calls, add to global window object 55 | declare global { 56 | interface Window { 57 | toast: { 58 | show: typeof showToast; 59 | error: typeof showErrorToast; 60 | info: typeof showInfoToast; 61 | success: typeof showSuccessToast; 62 | }; 63 | } 64 | } 65 | 66 | // Mount to window object 67 | if (typeof window !== 'undefined') { 68 | window.toast = { 69 | show: showToast, 70 | error: showErrorToast, 71 | info: showInfoToast, 72 | success: showSuccessToast 73 | }; 74 | } -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | darkMode: 'class', 11 | plugins: [ 12 | require('@tailwindcss/typography'), 13 | ], 14 | } -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "env.d.ts"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json" 3 | } -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import monacoEditorPlugin from 'vite-plugin-monaco-editor'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | vue(), 9 | (monacoEditorPlugin as any).default({}) 10 | ], 11 | optimizeDeps: { 12 | exclude: ['lucide-vue-next'], 13 | }, 14 | }); -------------------------------------------------------------------------------- /mockserver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt . 6 | RUN pip install --no-cache-dir -r requirements.txt 7 | 8 | COPY . . 9 | 10 | EXPOSE 8090 11 | 12 | CMD ["./dev.sh"] -------------------------------------------------------------------------------- /mockserver/dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Start uvicorn server with hot reload 4 | exec uvicorn main:app --host 0.0.0.0 --port 8090 --reload -------------------------------------------------------------------------------- /mockserver/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from pydantic import BaseModel 4 | import json 5 | import yaml 6 | from typing import List, Optional, Dict, Any 7 | import os 8 | from pathlib import Path 9 | import asyncio 10 | import logging 11 | import sys 12 | 13 | # Configure logging 14 | logger = logging.getLogger() 15 | logger.setLevel(logging.INFO) 16 | formatter = logging.Formatter( 17 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s', 18 | datefmt='%Y-%m-%d %H:%M:%S' 19 | ) 20 | console_handler = logging.StreamHandler(sys.stdout) 21 | console_handler.setFormatter(formatter) 22 | logger.addHandler(console_handler) 23 | 24 | app = FastAPI() 25 | 26 | # Add CORS middleware 27 | app.add_middleware( 28 | CORSMiddleware, 29 | allow_origins=["*"], 30 | allow_credentials=True, 31 | allow_methods=["*"], 32 | allow_headers=["*"], 33 | ) 34 | 35 | class Message(BaseModel): 36 | role: str 37 | content: Optional[str] = None 38 | tool_calls: Optional[List[Dict[str, Any]]] = None 39 | 40 | class ChatCompletionRequest(BaseModel): 41 | model: str 42 | messages: List[Message] 43 | temperature: Optional[float] = 0.7 44 | max_tokens: Optional[int] = None 45 | stream: Optional[bool] = False 46 | 47 | class ChatCompletionResponse(BaseModel): 48 | #id: str 49 | #object: str 50 | #created: int 51 | #model: str 52 | choices: List[Dict[str, Any]] 53 | 54 | def load_mock_data(): 55 | # Get mock data filename from environment variable, default to default.yaml 56 | mock_file = os.getenv("MOCK_DATA_FILE", "default.yaml") 57 | mock_file_path = Path(__file__).parent / "mock_datas" / mock_file 58 | 59 | with open(mock_file_path, 'r', encoding='utf-8') as f: 60 | logger.info(f"Loading mock data from {mock_file}") 61 | if mock_file.endswith('.json'): 62 | return json.load(f) 63 | else: 64 | return yaml.safe_load(f) 65 | 66 | current_index = 0 67 | 68 | @app.post("/v1/chat/completions", response_model=ChatCompletionResponse) 69 | async def chat_completions(request: ChatCompletionRequest): 70 | global current_index 71 | mock_data = load_mock_data() 72 | if not mock_data: 73 | current_index = 0 74 | logger.error("No mock data available") 75 | raise HTTPException(status_code=500, detail="No mock data available") 76 | 77 | # 从环境变量读取时延值,默认为0秒 78 | delay = float(os.getenv("MOCK_DELAY", "1")) 79 | if delay > 0: 80 | logger.debug(f"Applying mock delay of {delay} seconds") 81 | await asyncio.sleep(delay) 82 | 83 | response = mock_data[current_index] 84 | current_index = (current_index + 1) % len(mock_data) 85 | logger.info(f"Returning mock response {current_index}/{len(mock_data)}") 86 | return response 87 | -------------------------------------------------------------------------------- /mockserver/mock_datas/default.yaml: -------------------------------------------------------------------------------- 1 | - choices: 2 | - index: 0 3 | message: 4 | role: assistant 5 | content: | 6 | { 7 | "message": "This is a test", 8 | "goal": "test goal", 9 | "title": "test title", 10 | "steps": [ 11 | { 12 | "id": "1", 13 | "description": "test step" 14 | } 15 | ] 16 | } 17 | 18 | - choices: 19 | - index: 0 20 | message: 21 | role: assistant 22 | content: null 23 | tool_calls: 24 | - type: function 25 | function: 26 | name: message_notify_user 27 | arguments: | 28 | { 29 | "text": "test message tools." 30 | } 31 | 32 | - choices: 33 | - index: 0 34 | message: 35 | role: assistant 36 | content: null 37 | tool_calls: 38 | - type: function 39 | function: 40 | name: file_write 41 | arguments: | 42 | { 43 | "file": "/home/ubuntu/example.txt", 44 | "content": "New content\nSecond line", 45 | "append": false, 46 | "leading_newline": true, 47 | "trailing_newline": true 48 | } 49 | 50 | - choices: 51 | - index: 0 52 | message: 53 | role: assistant 54 | content: null 55 | tool_calls: 56 | - type: function 57 | function: 58 | name: shell_exec 59 | arguments: | 60 | { 61 | "id": "shell-1", 62 | "exec_dir": "/home/ubuntu", 63 | "command": "ls -la" 64 | } 65 | 66 | - choices: 67 | - index: 0 68 | message: 69 | role: assistant 70 | content: null 71 | tool_calls: 72 | - type: function 73 | function: 74 | name: browser_navigate 75 | arguments: | 76 | { 77 | "url": "https://www.example.com" 78 | } 79 | 80 | - choices: 81 | - index: 0 82 | message: 83 | role: assistant 84 | content: | 85 | test end 86 | 87 | - id: chatcmpl 88 | object: chat.completion 89 | model: gpt-4o 90 | choices: 91 | - index: 0 92 | message: 93 | role: assistant 94 | content: | 95 | { 96 | "steps": [] 97 | } 98 | finish_reason: stop 99 | -------------------------------------------------------------------------------- /mockserver/mock_datas/file_tools.yaml: -------------------------------------------------------------------------------- 1 | - choices: 2 | - index: 0 3 | message: 4 | role: assistant 5 | content: | 6 | { 7 | "message": "This is a test", 8 | "goal": "test goal", 9 | "title": "test title", 10 | "steps": [ 11 | { 12 | "id": "1", 13 | "description": "test file tools" 14 | } 15 | ] 16 | } 17 | 18 | - choices: 19 | - index: 0 20 | message: 21 | role: assistant 22 | content: null 23 | tool_calls: 24 | - type: function 25 | function: 26 | name: file_write 27 | arguments: | 28 | { 29 | "file": "/home/ubuntu/example.txt", 30 | "content": "New content\nSecond line", 31 | "append": false, 32 | "leading_newline": true, 33 | "trailing_newline": true 34 | } 35 | 36 | 37 | - choices: 38 | - index: 0 39 | message: 40 | role: assistant 41 | content: null 42 | tool_calls: 43 | - type: function 44 | function: 45 | name: file_read 46 | arguments: | 47 | { 48 | "file": "/home/ubuntu/example.txt", 49 | "start_line": 0, 50 | "end_line": 10 51 | } 52 | 53 | - choices: 54 | - index: 0 55 | message: 56 | role: assistant 57 | content: null 58 | tool_calls: 59 | - type: function 60 | function: 61 | name: file_str_replace 62 | arguments: | 63 | { 64 | "file": "/home/ubuntu/example.txt", 65 | "old_str": "old text", 66 | "new_str": "new text" 67 | } 68 | 69 | - choices: 70 | - index: 0 71 | message: 72 | role: assistant 73 | content: null 74 | tool_calls: 75 | - type: function 76 | function: 77 | name: file_find_in_content 78 | arguments: | 79 | { 80 | "file": "/home/ubuntu/example.txt", 81 | "regex": "pattern.*" 82 | } 83 | 84 | - choices: 85 | - index: 0 86 | message: 87 | role: assistant 88 | content: null 89 | tool_calls: 90 | - type: function 91 | function: 92 | name: file_find_by_name 93 | arguments: | 94 | { 95 | "path": "/home/ubuntu", 96 | "glob": "*.txt" 97 | } 98 | 99 | - choices: 100 | - index: 0 101 | message: 102 | role: assistant 103 | content: | 104 | test file end 105 | 106 | - choices: 107 | - index: 0 108 | message: 109 | role: assistant 110 | content: | 111 | { 112 | "steps": [] 113 | } 114 | -------------------------------------------------------------------------------- /mockserver/mock_datas/message_tools.yaml: -------------------------------------------------------------------------------- 1 | - choices: 2 | - index: 0 3 | message: 4 | role: assistant 5 | content: | 6 | { 7 | "message": "This is a test", 8 | "goal": "test goal", 9 | "title": "test title", 10 | "steps": [ 11 | { 12 | "id": "1", 13 | "description": "test message tools" 14 | } 15 | ] 16 | } 17 | 18 | - choices: 19 | - index: 0 20 | message: 21 | role: assistant 22 | content: null 23 | tool_calls: 24 | - type: function 25 | function: 26 | name: message_notify_user 27 | arguments: | 28 | { 29 | "text": "Task is in progress, please wait..." 30 | } 31 | 32 | - choices: 33 | - index: 0 34 | message: 35 | role: assistant 36 | content: | 37 | test message end 38 | 39 | - choices: 40 | - index: 0 41 | message: 42 | role: assistant 43 | content: | 44 | { 45 | "steps": [] 46 | } 47 | -------------------------------------------------------------------------------- /mockserver/mock_datas/search_tools.yaml: -------------------------------------------------------------------------------- 1 | - choices: 2 | - index: 0 3 | message: 4 | role: assistant 5 | content: | 6 | { 7 | "message": "This is a test", 8 | "goal": "test goal", 9 | "title": "test title", 10 | "steps": [ 11 | { 12 | "id": "1", 13 | "description": "test search tools" 14 | } 15 | ] 16 | } 17 | 18 | - choices: 19 | - index: 0 20 | message: 21 | role: assistant 22 | content: null 23 | tool_calls: 24 | - type: function 25 | function: 26 | name: info_search_web 27 | arguments: | 28 | { 29 | "query": "latest AI developments 2024", 30 | "date_range": "past_month" 31 | } 32 | finish_reason: stop 33 | 34 | - choices: 35 | - index: 0 36 | message: 37 | role: assistant 38 | content: | 39 | test message end 40 | 41 | - choices: 42 | - index: 0 43 | message: 44 | role: assistant 45 | content: | 46 | { 47 | "steps": [] 48 | } -------------------------------------------------------------------------------- /mockserver/mock_datas/shell_tools.yaml: -------------------------------------------------------------------------------- 1 | - choices: 2 | - index: 0 3 | message: 4 | role: assistant 5 | content: | 6 | { 7 | "message": "This is a test", 8 | "goal": "test goal", 9 | "title": "test title", 10 | "steps": [ 11 | { 12 | "id": "1", 13 | "description": "test shell tools" 14 | } 15 | ] 16 | } 17 | 18 | - choices: 19 | - index: 0 20 | message: 21 | role: assistant 22 | content: null 23 | tool_calls: 24 | - type: function 25 | function: 26 | name: shell_exec 27 | arguments: | 28 | { 29 | "id": "shell-1", 30 | "exec_dir": "/home/ubuntu", 31 | "command": "ls -la" 32 | } 33 | 34 | - choices: 35 | - index: 0 36 | message: 37 | role: assistant 38 | content: null 39 | tool_calls: 40 | - type: function 41 | function: 42 | name: shell_view 43 | arguments: | 44 | { 45 | "id": "shell-1" 46 | } 47 | 48 | - choices: 49 | - index: 0 50 | message: 51 | role: assistant 52 | content: null 53 | tool_calls: 54 | - type: function 55 | function: 56 | name: shell_wait 57 | arguments: | 58 | { 59 | "id": "shell-1", 60 | "seconds": 10 61 | } 62 | 63 | - choices: 64 | - index: 0 65 | message: 66 | role: assistant 67 | content: null 68 | tool_calls: 69 | - type: function 70 | function: 71 | name: shell_write_to_process 72 | arguments: | 73 | { 74 | "id": "shell-1", 75 | "input": "y", 76 | "press_enter": true 77 | } 78 | 79 | - choices: 80 | - index: 0 81 | message: 82 | role: assistant 83 | content: null 84 | tool_calls: 85 | - type: function 86 | function: 87 | name: shell_kill_process 88 | arguments: | 89 | { 90 | "id": "shell-1" 91 | } 92 | - choices: 93 | - index: 0 94 | message: 95 | role: assistant 96 | content: | 97 | test file end 98 | 99 | - choices: 100 | - index: 0 101 | message: 102 | role: assistant 103 | content: | 104 | { 105 | "steps": [] 106 | } -------------------------------------------------------------------------------- /mockserver/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.104.1 2 | uvicorn==0.24.0 3 | pydantic==2.4.2 4 | PyYAML==6.0.1 -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Determine which Docker Compose command to use 4 | if command -v docker &> /dev/null && docker compose version &> /dev/null; then 5 | COMPOSE="docker compose" 6 | elif command -v docker-compose &> /dev/null; then 7 | COMPOSE="docker-compose" 8 | else 9 | echo "Error: Neither docker compose nor docker-compose command found" >&2 10 | exit 1 11 | fi 12 | 13 | 14 | # Execute Docker Compose command 15 | $COMPOSE -f docker-compose.yml "$@" 16 | -------------------------------------------------------------------------------- /sandbox/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | # Avoid interactive prompts during installation 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | 6 | # Set hostname to sandbox 7 | ENV HOSTNAME=sandbox 8 | 9 | # Update and install basic tools 10 | RUN apt-get update && apt-get install -y \ 11 | sudo \ 12 | bc \ 13 | curl \ 14 | wget \ 15 | gnupg \ 16 | software-properties-common \ 17 | xvfb \ 18 | x11vnc \ 19 | xterm \ 20 | socat \ 21 | supervisor \ 22 | websockify \ 23 | && apt-get clean \ 24 | && rm -rf /var/lib/apt/lists/* 25 | 26 | # Create user ubuntu and grant sudo privileges 27 | RUN useradd -m -d /home/ubuntu -s /bin/bash ubuntu && \ 28 | echo "ubuntu ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ubuntu 29 | 30 | # Install Python 3.10.12 31 | RUN add-apt-repository ppa:deadsnakes/ppa && \ 32 | apt-get update && \ 33 | apt-get install -y python3.10 python3.10-venv python3.10-dev python3-pip && \ 34 | update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1 && \ 35 | apt-get clean && \ 36 | rm -rf /var/lib/apt/lists/* 37 | 38 | # Install Node.js 20.18.0 39 | RUN mkdir -p /etc/apt/keyrings && \ 40 | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ 41 | echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \ 42 | apt-get update && \ 43 | apt-get install -y nodejs && \ 44 | apt-get clean && \ 45 | rm -rf /var/lib/apt/lists/* 46 | 47 | # Install Google Chrome 48 | RUN add-apt-repository ppa:xtradeb/apps -y && \ 49 | apt-get update && \ 50 | apt-get install -y chromium --no-install-recommends && \ 51 | apt-get clean && \ 52 | rm -rf /var/lib/apt/lists/* 53 | 54 | # Install Chinese fonts and language support 55 | RUN apt-get update && apt-get install -y \ 56 | fonts-noto-cjk \ 57 | fonts-noto-color-emoji \ 58 | language-pack-zh-hans \ 59 | locales \ 60 | && locale-gen zh_CN.UTF-8 \ 61 | && apt-get clean \ 62 | && rm -rf /var/lib/apt/lists/* 63 | 64 | # Set default locale 65 | ENV LANG=zh_CN.UTF-8 \ 66 | LANGUAGE=zh_CN:zh \ 67 | LC_ALL=zh_CN.UTF-8 68 | 69 | # Set working directory 70 | WORKDIR /app 71 | 72 | # Copy project files (copy dependency files first to leverage cache) 73 | COPY requirements.txt . 74 | 75 | # Install Python dependencies 76 | RUN pip3 install --no-cache-dir -r requirements.txt 77 | 78 | # Copy remaining project files 79 | COPY . . 80 | 81 | # Configure supervisor 82 | COPY supervisord.conf /etc/supervisor/conf.d/app.conf 83 | 84 | # Expose ports 85 | EXPOSE 8080 9222 5900 5901 86 | 87 | ENV UVI_ARGS="" 88 | ENV CHROME_ARGS="" 89 | 90 | # Use supervisor to start all services 91 | CMD ["supervisord", "-n", "-c", "/app/supervisord.conf"] 92 | -------------------------------------------------------------------------------- /sandbox/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/sandbox/app/__init__.py -------------------------------------------------------------------------------- /sandbox/app/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/sandbox/app/api/__init__.py -------------------------------------------------------------------------------- /sandbox/app/api/router.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from app.api.v1 import shell, supervisor, file 4 | 5 | api_router = APIRouter() 6 | api_router.include_router(shell.router, prefix="/shell", tags=["shell"]) 7 | api_router.include_router(supervisor.router, prefix="/supervisor", tags=["supervisor"]) 8 | api_router.include_router(file.router, prefix="/file", tags=["file"]) 9 | -------------------------------------------------------------------------------- /sandbox/app/api/v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/sandbox/app/api/v1/__init__.py -------------------------------------------------------------------------------- /sandbox/app/api/v1/file.py: -------------------------------------------------------------------------------- 1 | """ 2 | File operation API interfaces 3 | """ 4 | from fastapi import APIRouter 5 | from app.schemas.file import ( 6 | FileReadRequest, FileWriteRequest, FileReplaceRequest, 7 | FileSearchRequest, FileFindRequest 8 | ) 9 | from app.schemas.response import Response 10 | from app.services.file import file_service 11 | 12 | router = APIRouter() 13 | 14 | @router.post("/read", response_model=Response) 15 | async def read_file(request: FileReadRequest): 16 | """ 17 | Read file content 18 | """ 19 | result = await file_service.read_file( 20 | file=request.file, 21 | start_line=request.start_line, 22 | end_line=request.end_line, 23 | sudo=request.sudo 24 | ) 25 | 26 | # Construct response 27 | return Response( 28 | success=True, 29 | message="File read successfully", 30 | data=result.model_dump() 31 | ) 32 | 33 | @router.post("/write", response_model=Response) 34 | async def write_file(request: FileWriteRequest): 35 | """ 36 | Write file content 37 | """ 38 | result = await file_service.write_file( 39 | file=request.file, 40 | content=request.content, 41 | append=request.append, 42 | leading_newline=request.leading_newline, 43 | trailing_newline=request.trailing_newline, 44 | sudo=request.sudo 45 | ) 46 | 47 | # Construct response 48 | return Response( 49 | success=True, 50 | message="File written successfully", 51 | data=result.model_dump() 52 | ) 53 | 54 | @router.post("/replace", response_model=Response) 55 | async def replace_in_file(request: FileReplaceRequest): 56 | """ 57 | Replace string in file 58 | """ 59 | result = await file_service.str_replace( 60 | file=request.file, 61 | old_str=request.old_str, 62 | new_str=request.new_str, 63 | sudo=request.sudo 64 | ) 65 | 66 | # Construct response 67 | return Response( 68 | success=True, 69 | message=f"Replacement completed, replaced {result.replaced_count} occurrences", 70 | data=result.model_dump() 71 | ) 72 | 73 | @router.post("/search", response_model=Response) 74 | async def search_in_file(request: FileSearchRequest): 75 | """ 76 | Search in file content 77 | """ 78 | result = await file_service.find_in_content( 79 | file=request.file, 80 | regex=request.regex, 81 | sudo=request.sudo 82 | ) 83 | 84 | # Construct response 85 | return Response( 86 | success=True, 87 | message=f"Search completed, found {len(result.matches)} matches", 88 | data=result.model_dump() 89 | ) 90 | 91 | @router.post("/find", response_model=Response) 92 | async def find_files(request: FileFindRequest): 93 | """ 94 | Find files by name pattern 95 | """ 96 | result = await file_service.find_by_name( 97 | path=request.path, 98 | glob_pattern=request.glob 99 | ) 100 | 101 | # Construct response 102 | return Response( 103 | success=True, 104 | message=f"Search completed, found {len(result.files)} files", 105 | data=result.model_dump() 106 | ) 107 | -------------------------------------------------------------------------------- /sandbox/app/api/v1/shell.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from app.schemas.shell import ( 3 | ShellExecRequest, ShellViewRequest, ShellWaitRequest, 4 | ShellWriteToProcessRequest, ShellKillProcessRequest, 5 | ) 6 | from app.schemas.response import Response 7 | from app.services.shell import shell_service 8 | from app.core.exceptions import BadRequestException 9 | 10 | router = APIRouter() 11 | 12 | @router.post("/exec", response_model=Response) 13 | async def exec_command(request: ShellExecRequest): 14 | """ 15 | Execute command in the specified shell session 16 | """ 17 | # If no session ID is provided, automatically create one 18 | if not request.id or request.id == "": 19 | request.id = shell_service.create_session_id() 20 | 21 | result = await shell_service.exec_command( 22 | session_id=request.id, 23 | exec_dir=request.exec_dir, 24 | command=request.command 25 | ) 26 | 27 | # Construct response 28 | return Response( 29 | success=True, 30 | message="Command executed", 31 | data=result.model_dump() 32 | ) 33 | 34 | @router.post("/view", response_model=Response) 35 | async def view_shell(request: ShellViewRequest): 36 | """ 37 | View output of the specified shell session 38 | """ 39 | if not request.id or request.id == "": 40 | raise BadRequestException("Session ID not provided") 41 | 42 | result = await shell_service.view_shell(session_id=request.id) 43 | 44 | # Construct response 45 | return Response( 46 | success=True, 47 | message="Session content retrieved successfully", 48 | data=result.model_dump() 49 | ) 50 | 51 | @router.post("/wait", response_model=Response) 52 | async def wait_for_process(request: ShellWaitRequest): 53 | """ 54 | Wait for the process in the specified shell session to return 55 | """ 56 | result = await shell_service.wait_for_process( 57 | session_id=request.id, 58 | seconds=request.seconds 59 | ) 60 | 61 | # Construct response 62 | return Response( 63 | success=True, 64 | message=f"Process completed, return code: {result.returncode}", 65 | data=result.model_dump() 66 | ) 67 | 68 | @router.post("/write", response_model=Response) 69 | async def write_to_process(request: ShellWriteToProcessRequest): 70 | """ 71 | Write input to the process in the specified shell session 72 | """ 73 | if not request.id or request.id == "": 74 | raise BadRequestException("Session ID not provided") 75 | 76 | result = await shell_service.write_to_process( 77 | session_id=request.id, 78 | input_text=request.input, 79 | press_enter=request.press_enter 80 | ) 81 | 82 | # Construct response 83 | return Response( 84 | success=True, 85 | message="Input written", 86 | data=result.model_dump() 87 | ) 88 | 89 | @router.post("/kill", response_model=Response) 90 | async def kill_process(request: ShellKillProcessRequest): 91 | """ 92 | Terminate the process in the specified shell session 93 | """ 94 | result = await shell_service.kill_process(session_id=request.id) 95 | 96 | # Construct response 97 | message = "Process terminated" if result.status == "terminated" else "Process ended" 98 | return Response( 99 | success=True, 100 | message=message, 101 | data=result.model_dump() 102 | ) -------------------------------------------------------------------------------- /sandbox/app/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/sandbox/app/core/__init__.py -------------------------------------------------------------------------------- /sandbox/app/core/config.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Union 2 | from pydantic import field_validator 3 | from pydantic_settings import BaseSettings 4 | 5 | 6 | class Settings(BaseSettings): 7 | ORIGINS: List[str] = ["*"] 8 | 9 | # Service timeout settings (minutes) 10 | SERVICE_TIMEOUT_MINUTES: Optional[int] = None 11 | 12 | # Log configuration 13 | LOG_LEVEL: str = "INFO" 14 | 15 | @field_validator("ORIGINS", mode="before") 16 | def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: 17 | if isinstance(v, str) and not v.startswith("["): 18 | return [i.strip() for i in v.split(",")] 19 | elif isinstance(v, (list, str)): 20 | return v 21 | raise ValueError(v) 22 | 23 | class Config: 24 | case_sensitive = True 25 | env_file = ".env" 26 | 27 | 28 | settings = Settings() -------------------------------------------------------------------------------- /sandbox/app/core/exceptions.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, status 2 | from fastapi.responses import JSONResponse 3 | from fastapi.exceptions import RequestValidationError 4 | from starlette.exceptions import HTTPException as StarletteHTTPException 5 | from typing import Any 6 | from app.schemas.response import Response 7 | import logging 8 | 9 | # Get logger 10 | logger = logging.getLogger(__name__) 11 | 12 | # Custom exception classes 13 | class AppException(Exception): 14 | """Base application exception class""" 15 | def __init__( 16 | self, 17 | message: str = "An error occurred", 18 | status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR, 19 | data: Any = None 20 | ): 21 | self.message = message 22 | self.status_code = status_code 23 | self.data = data 24 | logger.error("AppException: %s (code: %d)", message, status_code) 25 | super().__init__(self.message) 26 | 27 | class ResourceNotFoundException(AppException): 28 | """Resource not found exception""" 29 | def __init__(self, message: str = "Resource not found"): 30 | super().__init__(message=message, status_code=status.HTTP_404_NOT_FOUND) 31 | 32 | class BadRequestException(AppException): 33 | """Bad request exception""" 34 | def __init__(self, message: str = "Bad request"): 35 | super().__init__(message=message, status_code=status.HTTP_400_BAD_REQUEST) 36 | 37 | class UnauthorizedException(AppException): 38 | """Unauthorized exception""" 39 | def __init__(self, message: str = "Unauthorized"): 40 | super().__init__(message=message, status_code=status.HTTP_401_UNAUTHORIZED) 41 | 42 | # Exception handlers 43 | async def app_exception_handler(request: Request, exc: AppException): 44 | """Handle application custom exceptions""" 45 | logger.error("Processing application exception: %s", exc.message) 46 | response = Response.error( 47 | message=exc.message, 48 | data=exc.data 49 | ) 50 | return JSONResponse( 51 | status_code=exc.status_code, 52 | content=response.model_dump() 53 | ) 54 | 55 | async def http_exception_handler(request: Request, exc: StarletteHTTPException): 56 | """Handle HTTP exceptions""" 57 | logger.error("Processing HTTP exception: %s (code: %d)", exc.detail, exc.status_code) 58 | response = Response.error( 59 | message=str(exc.detail) 60 | ) 61 | return JSONResponse( 62 | status_code=exc.status_code, 63 | content=response.model_dump() 64 | ) 65 | 66 | async def validation_exception_handler(request: Request, exc: RequestValidationError): 67 | """Handle validation exceptions""" 68 | errors = exc.errors() 69 | error_messages = [] 70 | for error in errors: 71 | error_messages.append({ 72 | "loc": error.get("loc", []), 73 | "msg": error.get("msg", ""), 74 | "type": error.get("type", "") 75 | }) 76 | 77 | logger.error("Validation error: %s", error_messages) 78 | response = Response.error( 79 | message="Request data validation failed", 80 | data=error_messages 81 | ) 82 | return JSONResponse( 83 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, 84 | content=response.model_dump() 85 | ) 86 | 87 | async def general_exception_handler(request: Request, exc: Exception): 88 | """Handle all other exceptions""" 89 | error_message = f"Internal server error: {str(exc)}" 90 | logger.error("Unhandled exception: %s", error_message, exc_info=True) 91 | response = Response.error( 92 | message=error_message 93 | ) 94 | return JSONResponse( 95 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 96 | content=response.model_dump() 97 | ) -------------------------------------------------------------------------------- /sandbox/app/core/middleware.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from fastapi import Request 3 | from starlette.middleware.base import BaseHTTPMiddleware 4 | from starlette.responses import Response 5 | 6 | from app.core.config import settings 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | async def auto_extend_timeout_middleware(request: Request, call_next): 12 | """ 13 | Middleware to automatically extend timeout on every API request 14 | Only auto-extends when auto-expand is enabled (disabled when user explicitly manages timeout) 15 | """ 16 | from app.services.supervisor import supervisor_service 17 | 18 | # Only extend timeout if timeout is currently active, it's an API request, 19 | # and not a timeout management API call, and auto-expand is enabled 20 | if (settings.SERVICE_TIMEOUT_MINUTES is not None and 21 | supervisor_service.timeout_active and 22 | request.url.path.startswith("/api/") and 23 | not request.url.path.startswith("/api/v1/supervisor/timeout/") and 24 | supervisor_service.auto_expand_enabled): 25 | try: 26 | await supervisor_service.extend_timeout() 27 | logger.debug("Timeout automatically extended due to API request: %s", request.url.path) 28 | except Exception as e: 29 | logger.warning("Failed to auto-extend timeout: %s", str(e)) 30 | 31 | response = await call_next(request) 32 | return response -------------------------------------------------------------------------------- /sandbox/app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from fastapi.exceptions import RequestValidationError 4 | from starlette.exceptions import HTTPException as StarletteHTTPException 5 | import logging 6 | import sys 7 | 8 | from app.core.config import settings 9 | from app.api.router import api_router 10 | from app.core.exceptions import ( 11 | AppException, 12 | app_exception_handler, 13 | http_exception_handler, 14 | validation_exception_handler, 15 | general_exception_handler 16 | ) 17 | from app.core.middleware import auto_extend_timeout_middleware 18 | 19 | # Configure logging 20 | def setup_logging(): 21 | """ 22 | Set up the application logging system 23 | 24 | Configures log level, format, and handlers based on application settings. 25 | Outputs logs to stdout for container compatibility. 26 | """ 27 | log_level = getattr(logging, settings.LOG_LEVEL) 28 | log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 29 | 30 | logging.basicConfig( 31 | level=log_level, 32 | format=log_format, 33 | handlers=[logging.StreamHandler(sys.stdout)] 34 | ) 35 | # Get root logger 36 | root_logger = logging.getLogger() 37 | 38 | # Set root log level 39 | log_level = getattr(logging, settings.LOG_LEVEL) 40 | root_logger.setLevel(log_level) 41 | 42 | # Log setup completion 43 | logging.info("Sandbox logging system initialized with level: %s", settings.LOG_LEVEL) 44 | 45 | # Initialize logging 46 | setup_logging() 47 | logger = logging.getLogger(__name__) 48 | 49 | app = FastAPI( 50 | version="1.0.0", 51 | ) 52 | 53 | # Set up CORS 54 | app.add_middleware( 55 | CORSMiddleware, 56 | allow_origins=settings.ORIGINS, 57 | allow_credentials=True, 58 | allow_methods=["*"], 59 | allow_headers=["*"], 60 | ) 61 | 62 | logger.info("Sandbox API server starting") 63 | 64 | # Register middleware 65 | app.middleware("http")(auto_extend_timeout_middleware) 66 | 67 | # Register exception handlers 68 | app.add_exception_handler(AppException, app_exception_handler) 69 | app.add_exception_handler(StarletteHTTPException, http_exception_handler) 70 | app.add_exception_handler(RequestValidationError, validation_exception_handler) 71 | app.add_exception_handler(Exception, general_exception_handler) 72 | 73 | # Register routes 74 | app.include_router(api_router, prefix="/api/v1") 75 | 76 | logger.info("Sandbox API routes registered and server ready") -------------------------------------------------------------------------------- /sandbox/app/models/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 业务模型定义 3 | """ 4 | from app.models.shell import ShellCommandResult, ShellViewResult, ShellWaitResult, ShellWriteResult, ShellKillResult 5 | from app.models.supervisor import ProcessInfo, SupervisorActionResult, SupervisorTimeout 6 | from app.models.file import FileReadResult, FileWriteResult, FileReplaceResult, FileSearchResult, FileFindResult 7 | 8 | __all__ = [ 9 | 'ShellCommandResult', 'ShellViewResult', 'ShellWaitResult', 'ShellWriteResult', 'ShellKillResult', 10 | 'ProcessInfo', 'SupervisorActionResult', 'SupervisorTimeout', 11 | 'FileReadResult', 'FileWriteResult', 'FileReplaceResult', 'FileSearchResult', 'FileFindResult' 12 | ] 13 | -------------------------------------------------------------------------------- /sandbox/app/models/file.py: -------------------------------------------------------------------------------- 1 | """ 2 | File operation related models 3 | """ 4 | from pydantic import BaseModel, Field 5 | from typing import List, Optional 6 | 7 | 8 | class FileReadResult(BaseModel): 9 | """File read result""" 10 | content: str = Field(..., description="File content") 11 | file: str = Field(..., description="Path of the read file") 12 | 13 | 14 | class FileWriteResult(BaseModel): 15 | """File write result""" 16 | file: str = Field(..., description="Path of the written file") 17 | bytes_written: Optional[int] = Field(None, description="Number of bytes written") 18 | 19 | 20 | class FileReplaceResult(BaseModel): 21 | """File content replacement result""" 22 | file: str = Field(..., description="Path of the operated file") 23 | replaced_count: int = Field(0, description="Number of replacements") 24 | 25 | 26 | class FileSearchResult(BaseModel): 27 | """File content search result""" 28 | file: str = Field(..., description="Path of the searched file") 29 | matches: List[str] = Field([], description="List of matched content") 30 | line_numbers: List[int] = Field([], description="List of matched line numbers") 31 | 32 | 33 | class FileFindResult(BaseModel): 34 | """File find result""" 35 | path: str = Field(..., description="Path of the search directory") 36 | files: List[str] = Field([], description="List of found files") 37 | -------------------------------------------------------------------------------- /sandbox/app/models/shell.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shell business model definitions 3 | """ 4 | from typing import Optional, List 5 | from pydantic import BaseModel, Field 6 | 7 | 8 | class ConsoleRecord(BaseModel): 9 | """Shell command console record model""" 10 | ps1: str = Field(..., description="Command prompt") 11 | command: str = Field(..., description="Executed command") 12 | output: str = Field(default="", description="Command output") 13 | 14 | 15 | class ShellTask(BaseModel): 16 | """Shell task model""" 17 | id: str = Field(..., description="Task unique identifier") 18 | command: str = Field(..., description="Executed command") 19 | status: str = Field(..., description="Task status") 20 | created_at: str = Field(..., description="Task creation time") 21 | output: Optional[str] = Field(None, description="Task output") 22 | 23 | 24 | class ShellCommandResult(BaseModel): 25 | """Shell command execution result model""" 26 | session_id: str = Field(..., description="Shell session ID") 27 | command: str = Field(..., description="Executed command") 28 | status: str = Field(..., description="Command execution status") 29 | returncode: Optional[int] = Field(None, description="Process return code, only has value when status is completed") 30 | output: Optional[str] = Field(None, description="Command execution output, only has value when status is completed") 31 | console: Optional[List[ConsoleRecord]] = Field(None, description="Console command records") 32 | 33 | 34 | class ShellViewResult(BaseModel): 35 | """Shell session content view result model""" 36 | output: str = Field(..., description="Shell session output content") 37 | session_id: str = Field(..., description="Shell session ID") 38 | console: Optional[List[ConsoleRecord]] = Field(None, description="Console command records") 39 | 40 | 41 | class ShellWaitResult(BaseModel): 42 | """Process wait result model""" 43 | returncode: int = Field(..., description="Process return code") 44 | 45 | 46 | class ShellWriteResult(BaseModel): 47 | """Process input write result model""" 48 | status: str = Field(..., description="Write status") 49 | 50 | 51 | class ShellKillResult(BaseModel): 52 | """Process termination result model""" 53 | status: str = Field(..., description="Process status") 54 | returncode: int = Field(..., description="Process return code") -------------------------------------------------------------------------------- /sandbox/app/models/supervisor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Supervisor business model definitions 3 | """ 4 | from typing import Optional, List 5 | from pydantic import BaseModel, Field 6 | 7 | 8 | class ProcessInfo(BaseModel): 9 | """Process information model""" 10 | name: str = Field(..., description="Process name") 11 | group: str = Field(..., description="Process group") 12 | description: str = Field(..., description="Process description") 13 | start: int = Field(..., description="Start timestamp") 14 | stop: int = Field(..., description="Stop timestamp") 15 | now: int = Field(..., description="Current timestamp") 16 | state: int = Field(..., description="State code") 17 | statename: str = Field(..., description="State name") 18 | spawnerr: str = Field(..., description="Spawn error") 19 | exitstatus: int = Field(..., description="Exit status code") 20 | logfile: str = Field(..., description="Log file") 21 | stdout_logfile: str = Field(..., description="Standard output log file") 22 | stderr_logfile: str = Field(..., description="Standard error log file") 23 | pid: int = Field(..., description="Process ID") 24 | 25 | 26 | class SupervisorActionResult(BaseModel): 27 | """Supervisor operation result model""" 28 | status: str = Field(..., description="Operation status") 29 | result: Optional[List[str]] = Field(None, description="Operation result") 30 | stop_result: Optional[List[str]] = Field(None, description="Stop result") 31 | start_result: Optional[List[str]] = Field(None, description="Start result") 32 | shutdown_result: Optional[List[str]] = Field(None, description="Shutdown result") 33 | 34 | 35 | class SupervisorTimeout(BaseModel): 36 | """Supervisor timeout model""" 37 | status: Optional[str] = Field(None, description="Timeout setting status") 38 | active: bool = Field(False, description="Whether timeout is active") 39 | shutdown_time: Optional[str] = Field(None, description="Shutdown time") 40 | timeout_minutes: Optional[float] = Field(None, description="Timeout duration (minutes)") 41 | remaining_seconds: Optional[float] = Field(None, description="Remaining seconds") -------------------------------------------------------------------------------- /sandbox/app/schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/sandbox/app/schemas/__init__.py -------------------------------------------------------------------------------- /sandbox/app/schemas/file.py: -------------------------------------------------------------------------------- 1 | """ 2 | File operation request models 3 | """ 4 | from pydantic import BaseModel, Field 5 | from typing import Optional 6 | 7 | 8 | class FileReadRequest(BaseModel): 9 | """File read request""" 10 | file: str = Field(..., description="Absolute file path") 11 | start_line: Optional[int] = Field(None, description="Start line (0-based)") 12 | end_line: Optional[int] = Field(None, description="End line (not inclusive)") 13 | sudo: Optional[bool] = Field(False, description="Whether to use sudo privileges") 14 | 15 | 16 | class FileWriteRequest(BaseModel): 17 | """File write request""" 18 | file: str = Field(..., description="Absolute file path") 19 | content: str = Field(..., description="Content to write") 20 | append: Optional[bool] = Field(False, description="Whether to use append mode") 21 | leading_newline: Optional[bool] = Field(False, description="Whether to add leading newline") 22 | trailing_newline: Optional[bool] = Field(False, description="Whether to add trailing newline") 23 | sudo: Optional[bool] = Field(False, description="Whether to use sudo privileges") 24 | 25 | 26 | class FileReplaceRequest(BaseModel): 27 | """File content replacement request""" 28 | file: str = Field(..., description="Absolute file path") 29 | old_str: str = Field(..., description="Original string to replace") 30 | new_str: str = Field(..., description="New string to replace with") 31 | sudo: Optional[bool] = Field(False, description="Whether to use sudo privileges") 32 | 33 | 34 | class FileSearchRequest(BaseModel): 35 | """File content search request""" 36 | file: str = Field(..., description="Absolute file path") 37 | regex: str = Field(..., description="Regular expression pattern") 38 | sudo: Optional[bool] = Field(False, description="Whether to use sudo privileges") 39 | 40 | 41 | class FileFindRequest(BaseModel): 42 | """File find request""" 43 | path: str = Field(..., description="Directory path to search") 44 | glob: str = Field(..., description="Filename pattern (glob syntax)") 45 | -------------------------------------------------------------------------------- /sandbox/app/schemas/response.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any 2 | from pydantic import BaseModel, Field 3 | 4 | # Unified response model 5 | class Response(BaseModel): 6 | """Generic response model for API interface return results""" 7 | success: bool = Field(True, description="Whether the operation was successful") 8 | message: Optional[str] = Field("Operation successful", description="Operation result message") 9 | data: Optional[Any] = Field(None, description="Data returned from the operation") 10 | 11 | # Shortcut method to create error response 12 | @classmethod 13 | def error(cls, message: str, data: Any = None) -> "Response": 14 | """Create an error response instance""" 15 | return cls(success=False, message=message, data=data) -------------------------------------------------------------------------------- /sandbox/app/schemas/shell.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from typing import Optional 3 | 4 | class ShellExecRequest(BaseModel): 5 | """Shell command execution request model""" 6 | id: Optional[str] = Field(None, description="Unique identifier of the target shell session, if not provided, one will be automatically created") 7 | exec_dir: Optional[str] = Field(None, description="Working directory for command execution (must use absolute path)") 8 | command: str = Field(..., description="Shell command to execute") 9 | 10 | 11 | class ShellViewRequest(BaseModel): 12 | """Shell session content view request model""" 13 | id: str = Field(..., description="Unique identifier of the target shell session") 14 | 15 | 16 | class ShellWaitRequest(BaseModel): 17 | """Shell process wait request model""" 18 | id: str = Field(..., description="Unique identifier of the target shell session") 19 | seconds: Optional[int] = Field(None, description="Wait time (seconds)") 20 | 21 | 22 | class ShellWriteToProcessRequest(BaseModel): 23 | """Request model for writing input to a running process""" 24 | id: str = Field(..., description="Unique identifier of the target shell session") 25 | input: str = Field(..., description="Input content to write to the process") 26 | press_enter: bool = Field(..., description="Whether to press enter key after input") 27 | 28 | 29 | class ShellKillProcessRequest(BaseModel): 30 | """Request model for terminating a running process""" 31 | id: str = Field(..., description="Unique identifier of the target shell session") 32 | -------------------------------------------------------------------------------- /sandbox/app/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/sandbox/app/services/__init__.py -------------------------------------------------------------------------------- /sandbox/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | pydantic 4 | email-validator 5 | python-multipart 6 | pydantic-settings --------------------------------------------------------------------------------