Merge pull request #439 from K-tang-mkv/tx_dev
[mcp] Add openmanus server based on MCP
This commit is contained in:
commit
a8fc3e9709
@ -9,8 +9,7 @@ English | [中文](README_zh.md)
|
||||
|
||||
Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫!
|
||||
|
||||
Our team
|
||||
members [@mannaandpoem](https://github.com/mannaandpoem) [@XiangJinyu](https://github.com/XiangJinyu) [@MoshiQAQ](https://github.com/MoshiQAQ) [@didiforgithub](https://github.com/didiforgithub) [@stellaHSR](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building!
|
||||
Our team members [@Xinbin Liang](https://github.com/mannaandpoem) and [@Jinyu Xiang](https://github.com/XiangJinyu) (core authors), along with [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), and [@Sirui Hong](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building!
|
||||
|
||||
It's a simple implementation, so we welcome any suggestions, contributions, and feedback!
|
||||
|
||||
|
17
app.py
17
app.py
@ -1,7 +1,5 @@
|
||||
import asyncio
|
||||
import threading
|
||||
import uuid
|
||||
import webbrowser
|
||||
from datetime import datetime
|
||||
from json import dumps
|
||||
|
||||
@ -175,7 +173,8 @@ async def task_events(task_id: str):
|
||||
|
||||
task = task_manager.tasks.get(task_id)
|
||||
if task:
|
||||
yield f"event: status\ndata: {dumps({'type': 'status', 'status': task.status, 'steps': task.steps})}\n\n"
|
||||
status_data = {"type": "status", "status": task.status, "steps": task.steps}
|
||||
yield f"event: status\ndata: {dumps(status_data)}\n\n"
|
||||
|
||||
while True:
|
||||
try:
|
||||
@ -193,7 +192,12 @@ async def task_events(task_id: str):
|
||||
elif event["type"] == "step":
|
||||
task = task_manager.tasks.get(task_id)
|
||||
if task:
|
||||
yield f"event: status\ndata: {dumps({'type': 'status', 'status': task.status, 'steps': task.steps})}\n\n"
|
||||
status_data = {
|
||||
"type": "status",
|
||||
"status": task.status,
|
||||
"steps": task.steps,
|
||||
}
|
||||
yield f"event: status\ndata: {dumps(status_data)}\n\n"
|
||||
yield f"event: {event['type']}\ndata: {formatted_event}\n\n"
|
||||
elif event["type"] in ["think", "tool", "act", "run"]:
|
||||
yield f"event: {event['type']}\ndata: {formatted_event}\n\n"
|
||||
@ -244,12 +248,7 @@ async def generic_exception_handler(request: Request, exc: Exception):
|
||||
)
|
||||
|
||||
|
||||
def open_local_browser():
|
||||
webbrowser.open_new_tab("http://localhost:5172")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
threading.Timer(3, open_local_browser).start()
|
||||
uvicorn.run(app, host="localhost", port=5172)
|
||||
|
130
openmanus_server/README.md
Normal file
130
openmanus_server/README.md
Normal file
@ -0,0 +1,130 @@
|
||||
# OpenManus-server 🤖
|
||||
|
||||
This project provides a server based on [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) that exposes **OpenManus** tool functionalities as standardized APIs.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
This MCP server provides access to the following OpenManus tools:
|
||||
|
||||
1. **Browser Automation** 🌐 - Navigate webpages, click elements, input text, and more
|
||||
2. **Google Search** 🔍 - Execute searches and retrieve result links
|
||||
3. **Python Code Execution** 🐍 - Run Python code in a secure environment
|
||||
4. **File Saving** 💾 - Save content to local files
|
||||
5. **Termination Control** 🛑 - Control program execution flow
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- OpenManus project dependencies
|
||||
|
||||
### Installation Steps
|
||||
|
||||
1. First, install the OpenManus project:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mannaandpoem/OpenManus.git
|
||||
cd OpenManus
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
# Using uv (recommended)
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
uv venv
|
||||
source .venv/bin/activate # Unix/macOS
|
||||
# or .venv\Scripts\activate # Windows
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Install MCP dependencies:
|
||||
|
||||
```bash
|
||||
uv pip install -r openmanus_server/mcp_requirements.txt
|
||||
```
|
||||
|
||||
## Demo display
|
||||
<video src="./assets/demo.mp4" data-canonical-src="./assets/demo.mp4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
|
||||
|
||||
|
||||
|
||||
## 📖 Usage
|
||||
|
||||
### 1. Testing your server with Claude for Desktop 🖥️
|
||||
|
||||
> ⚠️ **Note**: Claude for Desktop is not yet available on Linux. Linux users can build an MCP client that connects to the server we just built.
|
||||
|
||||
#### Step 1: Installation Check ✅
|
||||
First, make sure you have Claude for Desktop installed. [You can install the latest version here](https://claude.ai/download). If you already have Claude for Desktop, **make sure it's updated to the latest version**.
|
||||
|
||||
#### Step 2: Configuration Setup ⚙️
|
||||
We'll need to configure Claude for Desktop for this server you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
|
||||
|
||||
```bash
|
||||
vim ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
||||
```
|
||||
|
||||
#### Step 3: Server Configuration 🔧
|
||||
You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
|
||||
|
||||
In this case, we'll add our single Openmanus server like so:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"openmanus": {
|
||||
"command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/uv",
|
||||
"args": [
|
||||
"--directory",
|
||||
"/ABSOLUTE/PATH/TO/OpenManus/openmanus_server",
|
||||
"run",
|
||||
"openmanus_server.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 **Tip**: You may need to put the full path to the uv executable in the command field. You can get this by running:
|
||||
> - MacOS/Linux: `which uv`
|
||||
> - Windows: `where uv`
|
||||
|
||||
#### Step 4: Understanding the Configuration 📝
|
||||
This tells Claude for Desktop:
|
||||
1. There's an MCP server named "openmanus" 🔌
|
||||
2. To launch it by running `uv --directory /ABSOLUTE/PATH/TO/OpenManus/openmanus_server run openmanus_server.py` 🚀
|
||||
|
||||
#### Step 5: Activation 🔄
|
||||
Save the file, and restart Claude for Desktop.
|
||||
|
||||
#### Step 6: Verification ✨
|
||||
Let's make sure Claude for Desktop is picking up the six tools we've exposed in our `openmanus` server. You can do this by looking for the hammer icon 
|
||||

|
||||
|
||||
After clicking on the hammer icon, you should see tools listed:
|
||||

|
||||
|
||||
#### Ready to Test! 🎉
|
||||
**Now, you can test the openmanus server in Claude for Desktop**:
|
||||
* 🔍 Try to find the recent news about Manus AI agent, and write a post for me!
|
||||
|
||||
|
||||
|
||||
### 💻 2. Testing with simple Client Example
|
||||
|
||||
Check out `openmanus_client_example.py` to test the openmanus server using the MCP client.
|
||||
|
||||
```
|
||||
uv run openmanus_server/openmanus_client_example.py openmanus_server/openmanus_server.py
|
||||
```
|
||||
|
||||
|
||||
## 🔒 Security Considerations
|
||||
|
||||
- When using in production, ensure proper authentication and authorization mechanisms are in place
|
||||
- The Python execution tool has timeout limits to prevent long-running code
|
||||
|
||||
## 📄 License
|
||||
|
||||
Same license as the OpenManus project
|
BIN
openmanus_server/assets/1.jpg
Normal file
BIN
openmanus_server/assets/1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 97 KiB |
BIN
openmanus_server/assets/2.png
Normal file
BIN
openmanus_server/assets/2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 274 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31.4175 14L22.985 5.51002C20.7329 3.26243 17.6811 2.00012 14.4993 2.00012C11.3176 2.00012 8.26581 3.26243 6.01372 5.51002L6.00247 5.52127L4.28122 7.30002C4.10292 7.49163 4.00685 7.74552 4.01364 8.00717C4.02043 8.26883 4.12954 8.51739 4.31754 8.6995C4.50554 8.88161 4.75745 8.98276 5.01919 8.98122C5.28092 8.97968 5.53163 8.87558 5.71747 8.69127L7.43372 6.91877C8.12421 6.22842 8.91217 5.64303 9.77247 5.18127L15.585 11L3.58497 23C3.39921 23.1857 3.25185 23.4062 3.15131 23.6489C3.05077 23.8916 2.99902 24.1517 2.99902 24.4144C2.99902 24.6771 3.05077 24.9372 3.15131 25.1799C3.25185 25.4225 3.39921 25.643 3.58497 25.8288L6.17122 28.415C6.35694 28.6008 6.57744 28.7481 6.82012 28.8487C7.06281 28.9492 7.32291 29.001 7.5856 29.001C7.84828 29.001 8.10839 28.9492 8.35107 28.8487C8.59375 28.7481 8.81425 28.6008 8.99997 28.415L21 16.415L22.7925 18.2075L25 20.4125C25.1857 20.5983 25.4062 20.7456 25.6489 20.8462C25.8916 20.9467 26.1517 20.9985 26.4143 20.9985C26.677 20.9985 26.9371 20.9467 27.1798 20.8462C27.4225 20.7456 27.643 20.5983 27.8287 20.4125L31.415 16.8263C31.7897 16.4516 32.0005 15.9436 32.0009 15.4137C32.0014 14.8838 31.7915 14.3753 31.4175 14ZM7.58497 27L4.99997 24.4138L13.5 15.9138L16.085 18.5L7.58497 27ZM20.2925 14.29L17.5 17.0838L14.9137 14.5L17.7075 11.7063C17.8004 11.6134 17.8742 11.5031 17.9245 11.3817C17.9749 11.2603 18.0008 11.1302 18.0008 10.9988C18.0008 10.8673 17.9749 10.7372 17.9245 10.6158C17.8742 10.4944 17.8004 10.3841 17.7075 10.2913L11.79 4.37502C13.4996 3.89351 15.3067 3.87606 17.0253 4.32445C18.744 4.77284 20.3122 5.67089 21.5687 6.92627L27.0962 12.49L23.5 16.0825L21.7075 14.29C21.6146 14.197 21.5043 14.1233 21.3829 14.073C21.2615 14.0226 21.1314 13.9967 21 13.9967C20.8686 13.9967 20.7384 14.0226 20.617 14.073C20.4956 14.1233 20.3853 14.197 20.2925 14.29ZM26.4175 18.9975L24.9175 17.4975L28.5 13.9063L30 15.4063L26.4175 18.9975Z" fill="#343330"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
BIN
openmanus_server/assets/demo.mp4
Normal file
BIN
openmanus_server/assets/demo.mp4
Normal file
Binary file not shown.
3
openmanus_server/mcp_requirements.txt
Normal file
3
openmanus_server/mcp_requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# Core dependencies
|
||||
mcp
|
||||
httpx>=0.27.0
|
181
openmanus_server/openmanus_client_example.py
Normal file
181
openmanus_server/openmanus_client_example.py
Normal file
@ -0,0 +1,181 @@
|
||||
import asyncio
|
||||
import os
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import Optional
|
||||
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
|
||||
class OpenManusClient:
|
||||
def __init__(self):
|
||||
# Initialize session and client objects
|
||||
self.session: Optional[ClientSession] = None
|
||||
self.exit_stack = AsyncExitStack()
|
||||
self.stdio = None
|
||||
self.write = None
|
||||
|
||||
async def connect_to_server(self, server_script_path: str):
|
||||
"""Connect to an MCP server via stdio
|
||||
|
||||
Args:
|
||||
server_script_path: Path to the server script
|
||||
"""
|
||||
if not server_script_path.endswith(".py"):
|
||||
raise ValueError("Server script must be a .py file")
|
||||
|
||||
# Get the current directory to add to PYTHONPATH
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(current_dir) # Get parent directory
|
||||
|
||||
# Prepare environment variables
|
||||
env = os.environ.copy() # Copy current environment
|
||||
|
||||
# Add current directory and project root to PYTHONPATH
|
||||
path_separator = (
|
||||
";" if os.name == "nt" else ":"
|
||||
) # Use ; for Windows, : for Unix
|
||||
if "PYTHONPATH" in env:
|
||||
env[
|
||||
"PYTHONPATH"
|
||||
] = f"{current_dir}{path_separator}{project_root}{path_separator}{env['PYTHONPATH']}"
|
||||
else:
|
||||
env["PYTHONPATH"] = f"{current_dir}{path_separator}{project_root}"
|
||||
|
||||
server_params = StdioServerParameters(
|
||||
command="python", args=[server_script_path], env=env
|
||||
)
|
||||
|
||||
stdio_transport = await self.exit_stack.enter_async_context(
|
||||
stdio_client(server_params)
|
||||
)
|
||||
self.stdio, self.write = stdio_transport
|
||||
self.session = await self.exit_stack.enter_async_context(
|
||||
ClientSession(self.stdio, self.write)
|
||||
)
|
||||
await self.session.initialize()
|
||||
|
||||
# List available tools
|
||||
response = await self.session.list_tools()
|
||||
tools = response.tools
|
||||
print("\nConnected to server with tools:", [tool.name for tool in tools])
|
||||
return tools
|
||||
|
||||
async def run_examples(self):
|
||||
"""Run example tool calls to demonstrate functionality"""
|
||||
try:
|
||||
print("\nExample 1: Google Search")
|
||||
search_result = await self.session.call_tool(
|
||||
"google_search", {"query": "Model Context Protocol", "num_results": 5}
|
||||
)
|
||||
print(f"Search results: {search_result.content}")
|
||||
|
||||
print("\nExample 2: Python Code Execution")
|
||||
code = """
|
||||
import math
|
||||
result = 0
|
||||
for i in range(1, 10):
|
||||
result += math.sqrt(i)
|
||||
print(f"Calculation result: {result}")
|
||||
"""
|
||||
python_result = await self.session.call_tool(
|
||||
"python_execute", {"code": code, "timeout": 3}
|
||||
)
|
||||
print(f"Python execution result: {python_result.content}")
|
||||
|
||||
print("\nExample 3: File Saving")
|
||||
file_result = await self.session.call_tool(
|
||||
"file_saver",
|
||||
{
|
||||
"content": "This is a test file content saved through MCP",
|
||||
"file_path": "mcp_test_file.txt",
|
||||
},
|
||||
)
|
||||
print(f"File save result: {file_result.content}")
|
||||
|
||||
print("\nExample 4: Browser Usage")
|
||||
# Navigate to webpage
|
||||
browser_result = await self.session.call_tool(
|
||||
"browser_use", {"action": "navigate", "url": "https://www.example.com"}
|
||||
)
|
||||
print(f"Browser navigation result: {browser_result.content}")
|
||||
|
||||
# Get browser state
|
||||
state_result = await self.session.call_tool("get_browser_state", {})
|
||||
print(f"Browser state: {state_result.content}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError during example execution: {str(e)}")
|
||||
|
||||
async def chat_loop(self):
|
||||
"""Run an interactive chat loop for testing tools"""
|
||||
print("\nOpenManus MCP Client Started!")
|
||||
print("Type your commands or 'quit' to exit.")
|
||||
print(
|
||||
"Available commands: google_search, python_execute, file_saver, browser_use, get_browser_state"
|
||||
)
|
||||
|
||||
while True:
|
||||
try:
|
||||
command = input("\nCommand: ").strip()
|
||||
|
||||
if command.lower() == "quit":
|
||||
break
|
||||
|
||||
# Parse command and parameters
|
||||
parts = command.split(maxsplit=1)
|
||||
if len(parts) == 0:
|
||||
continue
|
||||
|
||||
tool_name = parts[0]
|
||||
tool_args = {}
|
||||
if len(parts) > 1:
|
||||
try:
|
||||
tool_args = eval(parts[1]) # Convert string to dict
|
||||
except:
|
||||
print(
|
||||
"Invalid arguments format. Please provide a valid Python dictionary."
|
||||
)
|
||||
continue
|
||||
|
||||
result = await self.session.call_tool(tool_name, tool_args)
|
||||
print("\nResult:", result.content)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError: {str(e)}")
|
||||
|
||||
async def cleanup(self):
|
||||
"""Clean up resources"""
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
await self.exit_stack.aclose()
|
||||
print("\nClosed MCP client connection")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point"""
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python openmanus_client_example.py <path_to_server_script>")
|
||||
print("Example: python openmanus_client_example.py ../mcp_server.py")
|
||||
sys.exit(1)
|
||||
|
||||
client = OpenManusClient()
|
||||
try:
|
||||
await client.connect_to_server(server_script_path=sys.argv[1])
|
||||
|
||||
# Run examples first
|
||||
await client.run_examples()
|
||||
|
||||
# Then start interactive chat loop
|
||||
await client.chat_loop()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}")
|
||||
finally:
|
||||
await client.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
196
openmanus_server/openmanus_server.py
Normal file
196
openmanus_server/openmanus_server.py
Normal file
@ -0,0 +1,196 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
|
||||
# Add current directory to Python path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(current_dir)
|
||||
sys.path.insert(0, parent_dir)
|
||||
sys.path.insert(0, current_dir)
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("mcp-server")
|
||||
|
||||
# Import OpenManus tools
|
||||
from app.tool.browser_use_tool import BrowserUseTool
|
||||
from app.tool.file_saver import FileSaver
|
||||
from app.tool.google_search import GoogleSearch
|
||||
from app.tool.python_execute import PythonExecute
|
||||
from app.tool.terminate import Terminate
|
||||
|
||||
|
||||
# Initialize FastMCP server
|
||||
openmanus = FastMCP("openmanus")
|
||||
|
||||
# Initialize tool instances
|
||||
browser_tool = BrowserUseTool()
|
||||
google_search_tool = GoogleSearch()
|
||||
python_execute_tool = PythonExecute()
|
||||
file_saver_tool = FileSaver()
|
||||
terminate_tool = Terminate()
|
||||
|
||||
|
||||
# Browser tool
|
||||
@openmanus.tool()
|
||||
async def browser_use(
|
||||
action: str,
|
||||
url: Optional[str] = None,
|
||||
index: Optional[int] = None,
|
||||
text: Optional[str] = None,
|
||||
script: Optional[str] = None,
|
||||
scroll_amount: Optional[int] = None,
|
||||
tab_id: Optional[int] = None,
|
||||
) -> str:
|
||||
"""Execute various browser operations.
|
||||
|
||||
Args:
|
||||
action: The browser operation to execute, possible values include:
|
||||
- navigate: Navigate to specified URL
|
||||
- click: Click on an element on the page
|
||||
- input_text: Input text into a text field
|
||||
- screenshot: Take a screenshot of the current page
|
||||
- get_html: Get HTML of the current page
|
||||
- get_text: Get text content of the current page
|
||||
- execute_js: Execute JavaScript code
|
||||
- scroll: Scroll the page
|
||||
- switch_tab: Switch to specified tab
|
||||
- new_tab: Open new tab
|
||||
- close_tab: Close current tab
|
||||
- refresh: Refresh current page
|
||||
url: URL for 'navigate' or 'new_tab' operations
|
||||
index: Element index for 'click' or 'input_text' operations
|
||||
text: Text for 'input_text' operation
|
||||
script: JavaScript code for 'execute_js' operation
|
||||
scroll_amount: Scroll pixels for 'scroll' operation (positive for down, negative for up)
|
||||
tab_id: Tab ID for 'switch_tab' operation
|
||||
"""
|
||||
logger.info(f"Executing browser operation: {action}")
|
||||
result = await browser_tool.execute(
|
||||
action=action,
|
||||
url=url,
|
||||
index=index,
|
||||
text=text,
|
||||
script=script,
|
||||
scroll_amount=scroll_amount,
|
||||
tab_id=tab_id,
|
||||
)
|
||||
return json.dumps(result.model_dump())
|
||||
|
||||
|
||||
@openmanus.tool()
|
||||
async def get_browser_state() -> str:
|
||||
"""Get current browser state, including URL, title, tabs and interactive elements."""
|
||||
logger.info("Getting browser state")
|
||||
result = await browser_tool.get_current_state()
|
||||
return json.dumps(result.model_dump())
|
||||
|
||||
|
||||
# Google search tool
|
||||
@openmanus.tool()
|
||||
async def google_search(query: str, num_results: int = 10) -> str:
|
||||
"""Execute Google search and return list of relevant links.
|
||||
|
||||
Args:
|
||||
query: Search query
|
||||
num_results: Number of results to return (default is 10)
|
||||
"""
|
||||
logger.info(f"Executing Google search: {query}")
|
||||
results = await google_search_tool.execute(query=query, num_results=num_results)
|
||||
return json.dumps(results)
|
||||
|
||||
|
||||
# Python execution tool
|
||||
@openmanus.tool()
|
||||
async def python_execute(code: str, timeout: int = 5) -> str:
|
||||
"""Execute Python code and return results.
|
||||
|
||||
Args:
|
||||
code: Python code to execute
|
||||
timeout: Execution timeout in seconds
|
||||
"""
|
||||
logger.info("Executing Python code")
|
||||
result = await python_execute_tool.execute(code=code, timeout=timeout)
|
||||
return json.dumps(result)
|
||||
|
||||
|
||||
# File saver tool
|
||||
@openmanus.tool()
|
||||
async def file_saver(content: str, file_path: str, mode: str = "w") -> str:
|
||||
"""Save content to local file.
|
||||
|
||||
Args:
|
||||
content: Content to save
|
||||
file_path: File path
|
||||
mode: File open mode (default is 'w')
|
||||
"""
|
||||
logger.info(f"Saving file: {file_path}")
|
||||
result = await file_saver_tool.execute(
|
||||
content=content, file_path=file_path, mode=mode
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
# Terminate tool
|
||||
@openmanus.tool()
|
||||
async def terminate(status: str) -> str:
|
||||
"""Terminate program execution.
|
||||
|
||||
Args:
|
||||
status: Termination status, can be 'success' or 'failure'
|
||||
"""
|
||||
logger.info(f"Terminating execution: {status}")
|
||||
result = await terminate_tool.execute(status=status)
|
||||
return result
|
||||
|
||||
|
||||
# Clean up resources
|
||||
async def cleanup():
|
||||
"""Clean up all tool resources"""
|
||||
logger.info("Cleaning up resources")
|
||||
await browser_tool.cleanup()
|
||||
|
||||
|
||||
# Register cleanup function
|
||||
import atexit
|
||||
|
||||
|
||||
atexit.register(lambda: asyncio.run(cleanup()))
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse command line arguments"""
|
||||
parser = argparse.ArgumentParser(description="OpenManus MCP Server")
|
||||
parser.add_argument(
|
||||
"--transport",
|
||||
choices=["stdio", "http"],
|
||||
default="stdio",
|
||||
help="Communication method: stdio or http (default: stdio)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host", default="127.0.0.1", help="HTTP server host (default: 127.0.0.1)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port", type=int, default=8000, help="HTTP server port (default: 8000)"
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_args()
|
||||
|
||||
if args.transport == "stdio":
|
||||
logger.info("Starting OpenManus server (stdio mode)")
|
||||
openmanus.run(transport="stdio")
|
||||
else:
|
||||
logger.info(f"Starting OpenManus server (HTTP mode) at {args.host}:{args.port}")
|
||||
openmanus.run(transport="http", host=args.host, port=args.port)
|
14
pytest.ini
14
pytest.ini
@ -1,14 +0,0 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
|
||||
# Log settings
|
||||
log_cli = true
|
||||
log_cli_level = INFO
|
||||
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
|
||||
log_cli_date_format = %Y-%m-%d %H:%M:%S
|
||||
|
||||
# Make sure asyncio works properly
|
||||
asyncio_mode = auto
|
307
static/main.js
307
static/main.js
@ -1,307 +0,0 @@
|
||||
let currentEventSource = null;
|
||||
|
||||
function createTask() {
|
||||
const promptInput = document.getElementById('prompt-input');
|
||||
const prompt = promptInput.value.trim();
|
||||
|
||||
if (!prompt) {
|
||||
alert("Please enter a valid prompt");
|
||||
promptInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentEventSource) {
|
||||
currentEventSource.close();
|
||||
currentEventSource = null;
|
||||
}
|
||||
|
||||
const container = document.getElementById('task-container');
|
||||
container.innerHTML = '<div class="loading">Initializing task...</div>';
|
||||
document.getElementById('input-container').classList.add('bottom');
|
||||
|
||||
fetch('/tasks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ prompt })
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => { throw new Error(err.detail || 'Request failed') });
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data.task_id) {
|
||||
throw new Error('Invalid task ID');
|
||||
}
|
||||
setupSSE(data.task_id);
|
||||
loadHistory();
|
||||
})
|
||||
.catch(error => {
|
||||
container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
||||
console.error('Failed to create task:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function setupSSE(taskId) {
|
||||
let retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 2000;
|
||||
|
||||
const container = document.getElementById('task-container');
|
||||
|
||||
function connect() {
|
||||
const eventSource = new EventSource(`/tasks/${taskId}/events`);
|
||||
currentEventSource = eventSource;
|
||||
|
||||
let heartbeatTimer = setInterval(() => {
|
||||
container.innerHTML += '<div class="ping">·</div>';
|
||||
}, 5000);
|
||||
|
||||
const pollInterval = setInterval(() => {
|
||||
fetch(`/tasks/${taskId}`)
|
||||
.then(response => response.json())
|
||||
.then(task => {
|
||||
updateTaskStatus(task);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Polling failed:', error);
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
const handleEvent = (event, type) => {
|
||||
clearInterval(heartbeatTimer);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
container.querySelector('.loading')?.remove();
|
||||
container.classList.add('active');
|
||||
|
||||
const stepContainer = ensureStepContainer(container);
|
||||
const { formattedContent, timestamp } = formatStepContent(data, type);
|
||||
const step = createStepElement(type, formattedContent, timestamp);
|
||||
|
||||
stepContainer.appendChild(step);
|
||||
autoScroll(stepContainer);
|
||||
|
||||
fetch(`/tasks/${taskId}`)
|
||||
.then(response => response.json())
|
||||
.then(task => {
|
||||
updateTaskStatus(task);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Status update failed:', error);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Error handling ${type} event:`, e);
|
||||
}
|
||||
};
|
||||
|
||||
const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message'];
|
||||
eventTypes.forEach(type => {
|
||||
eventSource.addEventListener(type, (event) => handleEvent(event, type));
|
||||
});
|
||||
|
||||
eventSource.addEventListener('complete', (event) => {
|
||||
clearInterval(heartbeatTimer);
|
||||
clearInterval(pollInterval);
|
||||
container.innerHTML += `
|
||||
<div class="complete">
|
||||
<div>✅ Task completed</div>
|
||||
<pre>${lastResultContent}</pre>
|
||||
</div>
|
||||
`;
|
||||
eventSource.close();
|
||||
currentEventSource = null;
|
||||
});
|
||||
|
||||
eventSource.addEventListener('error', (event) => {
|
||||
clearInterval(heartbeatTimer);
|
||||
clearInterval(pollInterval);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
container.innerHTML += `
|
||||
<div class="error">
|
||||
❌ Error: ${data.message}
|
||||
</div>
|
||||
`;
|
||||
eventSource.close();
|
||||
currentEventSource = null;
|
||||
} catch (e) {
|
||||
console.error('Error handling failed:', e);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
if (eventSource.readyState === EventSource.CLOSED) return;
|
||||
|
||||
console.error('SSE connection error:', err);
|
||||
clearInterval(heartbeatTimer);
|
||||
clearInterval(pollInterval);
|
||||
eventSource.close();
|
||||
|
||||
if (retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
container.innerHTML += `
|
||||
<div class="warning">
|
||||
⚠ Connection lost, retrying in ${retryDelay/1000} seconds (${retryCount}/${maxRetries})...
|
||||
</div>
|
||||
`;
|
||||
setTimeout(connect, retryDelay);
|
||||
} else {
|
||||
container.innerHTML += `
|
||||
<div class="error">
|
||||
⚠ Connection lost, please try refreshing the page
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
connect();
|
||||
}
|
||||
|
||||
function loadHistory() {
|
||||
fetch('/tasks')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error(`请求失败: ${response.status} - ${text.substring(0, 100)}`);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(tasks => {
|
||||
const listContainer = document.getElementById('task-list');
|
||||
listContainer.innerHTML = tasks.map(task => `
|
||||
<div class="task-card" data-task-id="${task.id}">
|
||||
<div>${task.prompt}</div>
|
||||
<div class="task-meta">
|
||||
${new Date(task.created_at).toLocaleString()} -
|
||||
<span class="status status-${task.status ? task.status.toLowerCase() : 'unknown'}">
|
||||
${task.status || '未知状态'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载历史记录失败:', error);
|
||||
const listContainer = document.getElementById('task-list');
|
||||
listContainer.innerHTML = `<div class="error">加载失败: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function ensureStepContainer(container) {
|
||||
let stepContainer = container.querySelector('.step-container');
|
||||
if (!stepContainer) {
|
||||
container.innerHTML = '<div class="step-container"></div>';
|
||||
stepContainer = container.querySelector('.step-container');
|
||||
}
|
||||
return stepContainer;
|
||||
}
|
||||
|
||||
function formatStepContent(data, eventType) {
|
||||
return {
|
||||
formattedContent: data.result,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
};
|
||||
}
|
||||
|
||||
function createStepElement(type, content, timestamp) {
|
||||
const step = document.createElement('div');
|
||||
step.className = `step-item ${type}`;
|
||||
step.innerHTML = `
|
||||
<div class="log-line">
|
||||
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
|
||||
<pre>${content}</pre>
|
||||
</div>
|
||||
`;
|
||||
return step;
|
||||
}
|
||||
|
||||
function autoScroll(element) {
|
||||
requestAnimationFrame(() => {
|
||||
element.scrollTo({
|
||||
top: element.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
});
|
||||
setTimeout(() => {
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
|
||||
function getEventIcon(eventType) {
|
||||
const icons = {
|
||||
'think': '🤔',
|
||||
'tool': '🛠️',
|
||||
'act': '🚀',
|
||||
'result': '🏁',
|
||||
'error': '❌',
|
||||
'complete': '✅',
|
||||
'log': '📝',
|
||||
'run': '⚙️'
|
||||
};
|
||||
return icons[eventType] || 'ℹ️';
|
||||
}
|
||||
|
||||
function getEventLabel(eventType) {
|
||||
const labels = {
|
||||
'think': 'Thinking',
|
||||
'tool': 'Using Tool',
|
||||
'act': 'Action',
|
||||
'result': 'Result',
|
||||
'error': 'Error',
|
||||
'complete': 'Complete',
|
||||
'log': 'Log',
|
||||
'run': 'Running'
|
||||
};
|
||||
return labels[eventType] || 'Info';
|
||||
}
|
||||
|
||||
function updateTaskStatus(task) {
|
||||
const statusBar = document.getElementById('status-bar');
|
||||
if (!statusBar) return;
|
||||
|
||||
if (task.status === 'completed') {
|
||||
statusBar.innerHTML = `<span class="status-complete">✅ Task completed</span>`;
|
||||
} else if (task.status === 'failed') {
|
||||
statusBar.innerHTML = `<span class="status-error">❌ Task failed: ${task.error || 'Unknown error'}</span>`;
|
||||
} else {
|
||||
statusBar.innerHTML = `<span class="status-running">⚙️ Task running: ${task.status}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadHistory();
|
||||
|
||||
document.getElementById('prompt-input').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
createTask();
|
||||
}
|
||||
});
|
||||
|
||||
const historyToggle = document.getElementById('history-toggle');
|
||||
if (historyToggle) {
|
||||
historyToggle.addEventListener('click', () => {
|
||||
const historyPanel = document.getElementById('history-panel');
|
||||
if (historyPanel) {
|
||||
historyPanel.classList.toggle('open');
|
||||
historyToggle.classList.toggle('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const clearButton = document.getElementById('clear-btn');
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener('click', () => {
|
||||
document.getElementById('prompt-input').value = '';
|
||||
document.getElementById('prompt-input').focus();
|
||||
});
|
||||
}
|
||||
});
|
353
static/style.css
353
static/style.css
@ -1,353 +0,0 @@
|
||||
:root {
|
||||
--primary-color: #007bff;
|
||||
--primary-hover: #0056b3;
|
||||
--success-color: #28a745;
|
||||
--error-color: #dc3545;
|
||||
--warning-color: #ff9800;
|
||||
--info-color: #2196f3;
|
||||
--text-color: #333;
|
||||
--text-light: #666;
|
||||
--bg-color: #f8f9fa;
|
||||
--border-color: #ddd;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
min-width: 0;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.history-panel {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
margin-top: 10px;
|
||||
max-height: calc(100vh - 160px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.task-container {
|
||||
@extend .card;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.welcome-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: var(--text-light);
|
||||
background: white;
|
||||
z-index: 1;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.welcome-message h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.input-container {
|
||||
@extend .card;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#prompt-input {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.task-item {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.task-item:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.task-item.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
#input-container.bottom {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
background: #fff;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.task-card:hover {
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.status-running {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.step-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
margin-bottom: 10px;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.step-item .log-line:not(.result) {
|
||||
opacity: 0.7;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.step-item .log-line.result {
|
||||
opacity: 1;
|
||||
color: #333;
|
||||
font-size: 1em;
|
||||
background: #e8f5e9;
|
||||
border-left: 4px solid #4caf50;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.step-item.show {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.log-line.think,
|
||||
.step-item pre.think {
|
||||
background: var(--info-color-light);
|
||||
border-left: 4px solid var(--info-color);
|
||||
}
|
||||
|
||||
.log-line.tool,
|
||||
.step-item pre.tool {
|
||||
background: var(--warning-color-light);
|
||||
border-left: 4px solid var(--warning-color);
|
||||
}
|
||||
|
||||
.log-line.result,
|
||||
.step-item pre.result {
|
||||
background: var(--success-color-light);
|
||||
border-left: 4px solid var(--success-color);
|
||||
}
|
||||
|
||||
.log-line.error,
|
||||
.step-item pre.error {
|
||||
background: var(--error-color-light);
|
||||
border-left: 4px solid var(--error-color);
|
||||
}
|
||||
|
||||
.log-line.info,
|
||||
.step-item pre.info {
|
||||
background: var(--bg-color);
|
||||
border-left: 4px solid var(--text-light);
|
||||
}
|
||||
|
||||
.log-prefix {
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 5px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.step-item pre {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
overflow-x: hidden;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
color: var(--text-color);
|
||||
background: var(--bg-color);
|
||||
|
||||
&.log {
|
||||
background: var(--bg-color);
|
||||
border-left: 4px solid var(--text-light);
|
||||
}
|
||||
&.think {
|
||||
background: var(--info-color-light);
|
||||
border-left: 4px solid var(--info-color);
|
||||
}
|
||||
&.tool {
|
||||
background: var(--warning-color-light);
|
||||
border-left: 4px solid var(--warning-color);
|
||||
}
|
||||
&.result {
|
||||
background: var(--success-color-light);
|
||||
border-left: 4px solid var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.step-item strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #007bff;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.step-item div {
|
||||
color: #444;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 15px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ping {
|
||||
color: #ccc;
|
||||
text-align: center;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
padding: 10px;
|
||||
background: #ffe6e6;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.complete {
|
||||
color: #28a745;
|
||||
padding: 10px;
|
||||
background: #e6ffe6;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.complete pre {
|
||||
max-width: 100%;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OpenManus Local Version</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="history-panel">
|
||||
<h2>History Tasks</h2>
|
||||
<div id="task-list" class="task-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="main-panel">
|
||||
<div id="task-container" class="task-container">
|
||||
<div class="welcome-message">
|
||||
<h1>Welcome to OpenManus Local Version</h1>
|
||||
<p>Please enter a task prompt to start a new task</p>
|
||||
</div>
|
||||
<div id="log-container" class="step-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="input-container" class="input-container">
|
||||
<input
|
||||
type="text"
|
||||
id="prompt-input"
|
||||
placeholder="Enter task prompt..."
|
||||
onkeypress="if(event.keyCode === 13) createTask()"
|
||||
>
|
||||
<button onclick="createTask()">Create Task</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/main.js"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user