Merge pull request #439 from K-tang-mkv/tx_dev

[mcp] Add openmanus server based on MCP
This commit is contained in:
mannaandpoem 2025-03-11 13:22:45 +08:00 committed by GitHub
commit a8fc3e9709
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 522 additions and 727 deletions

View File

@ -9,8 +9,7 @@ English | [中文](README_zh.md)
Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫! Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫!
Our team 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!
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!
It's a simple implementation, so we welcome any suggestions, contributions, and feedback! It's a simple implementation, so we welcome any suggestions, contributions, and feedback!

17
app.py
View File

@ -1,7 +1,5 @@
import asyncio import asyncio
import threading
import uuid import uuid
import webbrowser
from datetime import datetime from datetime import datetime
from json import dumps from json import dumps
@ -175,7 +173,8 @@ async def task_events(task_id: str):
task = task_manager.tasks.get(task_id) task = task_manager.tasks.get(task_id)
if task: 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: while True:
try: try:
@ -193,7 +192,12 @@ async def task_events(task_id: str):
elif event["type"] == "step": elif event["type"] == "step":
task = task_manager.tasks.get(task_id) task = task_manager.tasks.get(task_id)
if task: 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" yield f"event: {event['type']}\ndata: {formatted_event}\n\n"
elif event["type"] in ["think", "tool", "act", "run"]: elif event["type"] in ["think", "tool", "act", "run"]:
yield f"event: {event['type']}\ndata: {formatted_event}\n\n" 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__": if __name__ == "__main__":
import uvicorn import uvicorn
threading.Timer(3, open_local_browser).start()
uvicorn.run(app, host="localhost", port=5172) uvicorn.run(app, host="localhost", port=5172)

130
openmanus_server/README.md Normal file
View 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 ![hammer icon](./assets/claude-desktop-mcp-hammer-icon.svg)
![tools_in_claude](./assets/1.jpg)
After clicking on the hammer icon, you should see tools listed:
![alvaliable_tools_list](./assets/2.png)
#### 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

View File

@ -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

Binary file not shown.

View File

@ -0,0 +1,3 @@
# Core dependencies
mcp
httpx>=0.27.0

View 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())

View 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)

View File

@ -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

View File

@ -1,3 +0,0 @@
@echo off
venv\Scripts\python.exe app.py
pause

View File

@ -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();
});
}
});

View File

@ -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;
}

View File

@ -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>