Compare commits

..

19 Commits

Author SHA1 Message Date
liangxinbing 13f98a0fde update README and format code
Pre-commit checks / pre-commit-check (push) Failing after 0s
2025-03-12 00:26:25 +08:00
liangxinbing 487b44fda8 update README and format code 2025-03-11 23:07:31 +08:00
mannaandpoem 737abe4f90 Merge pull request #480 from mczhuge/main
feat: refactor the LLM (using LiteLLM & add cost calculation)
2025-03-11 22:44:29 +08:00
Mingchen Zhuge cbef113736 Update config.py
recover the original `config.py`
2025-03-11 05:44:16 -07:00
Mingchen Zhuge dc8ed530f0 add litellm in the requirements.txt 2025-03-11 15:26:11 +03:00
Mingchen Zhuge 62cfcb182e using LiteLLM to support flexible LLM providing & adding cost calculations 2025-03-11 15:23:08 +03:00
mannaandpoem 111a2bc6b1 Merge pull request #469 from cyzus/fix-infinite-loop-for-simple-query
fix infinite loop for simple query
2025-03-11 19:20:15 +08:00
Yizhou Chi 61d705f6e6 fix prompt order 2025-03-11 17:55:40 +08:00
Yizhou Chi cafd4a18da fix infinite loop 2025-03-11 17:39:28 +08:00
xiangjinyu b67d1b90e0 Adjust feishu group 2025-03-11 16:45:25 +08:00
xiangjinyu 6a89ae8256 Adjust feishu group 2025-03-11 16:42:51 +08:00
xiangjinyu 4d36a4c3e2 Adjust feishu group 2025-03-11 16:41:40 +08:00
xiangjinyu fed6fe1421 Adjust feishu group 2025-03-11 16:40:27 +08:00
xiangjinyu 3a958944a0 Adjust code structure 2025-03-11 16:32:13 +08:00
liangxinbing 8d18c05350 replace max_steps to 20 for Manus 2025-03-11 13:14:43 +08:00
liangxinbing eeefddf6bf Bug Fixing based on basy.py
Repair from Bob Feng8 Xu
2025-03-11 13:13:24 +08:00
liangxinbing 35de1f9e15 remove front-end code 2025-03-11 13:11:43 +08:00
xiangjinyu 2c0b2d1fb3 Merge remote-tracking branch 'origin/main' 2025-03-11 11:47:15 +08:00
xiangjinyu 211d0694e3 update team members in README.md 2025-03-11 11:46:57 +08:00
85 changed files with 604 additions and 15931 deletions
-6
View File
@@ -178,9 +178,3 @@ data/
# Workspace
workspace/
# Private Config
config/config.toml
# Desktop runtime
desktop/frontend/wailsjs/runtime/
+3 -2
View File
@@ -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!
@@ -144,6 +143,8 @@ Join our networking group on Feishu and share your experience with other develop
Thanks to [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
and [browser-use](https://github.com/browser-use/browser-use) for providing basic support for this project!
Additionally, we are grateful to [AAAJ](https://github.com/metauto-ai/agent-as-a-judge), [MetaGPT](https://github.com/geekan/MetaGPT) and [OpenHands](https://github.com/All-Hands-AI/OpenHands).
OpenManus is built by contributors from MetaGPT. Huge thanks to this agent community!
## Cite
+2
View File
@@ -145,4 +145,6 @@ python run_flow.py
特别感谢 [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
和 [browser-use](https://github.com/browser-use/browser-use) 为本项目提供的基础支持!
此外,我们感谢 [AAAJ](https://github.com/metauto-ai/agent-as-a-judge)[MetaGPT](https://github.com/geekan/MetaGPT) 和 [OpenHands](https://github.com/All-Hands-AI/OpenHands).
OpenManus 由 MetaGPT 社区的贡献者共同构建,感谢这个充满活力的智能体开发者社区!
-291
View File
@@ -1,291 +0,0 @@
import asyncio
import os
import threading
import tomllib
import uuid
import webbrowser
from datetime import datetime
from functools import partial
from json import dumps
from pathlib import Path
from fastapi import Body, FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import (
FileResponse,
HTMLResponse,
JSONResponse,
StreamingResponse,
)
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class Task(BaseModel):
id: str
prompt: str
created_at: datetime
status: str
steps: list = []
def model_dump(self, *args, **kwargs):
data = super().model_dump(*args, **kwargs)
data["created_at"] = self.created_at.isoformat()
return data
class TaskManager:
def __init__(self):
self.tasks = {}
self.queues = {}
def create_task(self, prompt: str) -> Task:
task_id = str(uuid.uuid4())
task = Task(
id=task_id, prompt=prompt, created_at=datetime.now(), status="pending"
)
self.tasks[task_id] = task
self.queues[task_id] = asyncio.Queue()
return task
async def update_task_step(
self, task_id: str, step: int, result: str, step_type: str = "step"
):
if task_id in self.tasks:
task = self.tasks[task_id]
task.steps.append({"step": step, "result": result, "type": step_type})
await self.queues[task_id].put(
{"type": step_type, "step": step, "result": result}
)
await self.queues[task_id].put(
{"type": "status", "status": task.status, "steps": task.steps}
)
async def complete_task(self, task_id: str):
if task_id in self.tasks:
task = self.tasks[task_id]
task.status = "completed"
await self.queues[task_id].put(
{"type": "status", "status": task.status, "steps": task.steps}
)
await self.queues[task_id].put({"type": "complete"})
async def fail_task(self, task_id: str, error: str):
if task_id in self.tasks:
self.tasks[task_id].status = f"failed: {error}"
await self.queues[task_id].put({"type": "error", "message": error})
task_manager = TaskManager()
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.get("/download")
async def download_file(file_path: str):
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(file_path, filename=os.path.basename(file_path))
@app.post("/tasks")
async def create_task(prompt: str = Body(..., embed=True)):
task = task_manager.create_task(prompt)
asyncio.create_task(run_task(task.id, prompt))
return {"task_id": task.id}
from app.agent.manus import Manus
async def run_task(task_id: str, prompt: str):
try:
task_manager.tasks[task_id].status = "running"
agent = Manus(
name="Manus",
description="A versatile agent that can solve various tasks using multiple tools",
)
async def on_think(thought):
await task_manager.update_task_step(task_id, 0, thought, "think")
async def on_tool_execute(tool, input):
await task_manager.update_task_step(
task_id, 0, f"Executing tool: {tool}\nInput: {input}", "tool"
)
async def on_action(action):
await task_manager.update_task_step(
task_id, 0, f"Executing action: {action}", "act"
)
async def on_run(step, result):
await task_manager.update_task_step(task_id, step, result, "run")
from app.logger import logger
class SSELogHandler:
def __init__(self, task_id):
self.task_id = task_id
async def __call__(self, message):
import re
# Extract - Subsequent Content
cleaned_message = re.sub(r"^.*? - ", "", message)
event_type = "log"
if "✨ Manus's thoughts:" in cleaned_message:
event_type = "think"
elif "🛠️ Manus selected" in cleaned_message:
event_type = "tool"
elif "🎯 Tool" in cleaned_message:
event_type = "act"
elif "📝 Oops!" in cleaned_message:
event_type = "error"
elif "🏁 Special tool" in cleaned_message:
event_type = "complete"
await task_manager.update_task_step(
self.task_id, 0, cleaned_message, event_type
)
sse_handler = SSELogHandler(task_id)
logger.add(sse_handler)
result = await agent.run(prompt)
await task_manager.update_task_step(task_id, 1, result, "result")
await task_manager.complete_task(task_id)
except Exception as e:
await task_manager.fail_task(task_id, str(e))
@app.get("/tasks/{task_id}/events")
async def task_events(task_id: str):
async def event_generator():
if task_id not in task_manager.queues:
yield f"event: error\ndata: {dumps({'message': 'Task not found'})}\n\n"
return
queue = task_manager.queues[task_id]
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"
while True:
try:
event = await queue.get()
formatted_event = dumps(event)
yield ": heartbeat\n\n"
if event["type"] == "complete":
yield f"event: complete\ndata: {formatted_event}\n\n"
break
elif event["type"] == "error":
yield f"event: error\ndata: {formatted_event}\n\n"
break
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"
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"
else:
yield f"event: {event['type']}\ndata: {formatted_event}\n\n"
except asyncio.CancelledError:
print(f"Client disconnected for task {task_id}")
break
except Exception as e:
print(f"Error in event stream: {str(e)}")
yield f"event: error\ndata: {dumps({'message': str(e)})}\n\n"
break
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
@app.get("/tasks")
async def get_tasks():
sorted_tasks = sorted(
task_manager.tasks.values(), key=lambda task: task.created_at, reverse=True
)
return JSONResponse(
content=[task.model_dump() for task in sorted_tasks],
headers={"Content-Type": "application/json"},
)
@app.get("/tasks/{task_id}")
async def get_task(task_id: str):
if task_id not in task_manager.tasks:
raise HTTPException(status_code=404, detail="Task not found")
return task_manager.tasks[task_id]
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
return JSONResponse(
status_code=500, content={"message": f"Server error: {str(exc)}"}
)
def open_local_browser(config):
webbrowser.open_new_tab(f"http://{config['host']}:{config['port']}")
def load_config():
try:
config_path = Path(__file__).parent / "config" / "config.toml"
with open(config_path, "rb") as f:
config = tomllib.load(f)
return {"host": config["server"]["host"], "port": config["server"]["port"]}
except FileNotFoundError:
raise RuntimeError(
"Configuration file not found, please check if config/fig.toml exists"
)
except KeyError as e:
raise RuntimeError(
f"The configuration file is missing necessary fields: {str(e)}"
)
if __name__ == "__main__":
import uvicorn
config = load_config()
open_with_config = partial(open_local_browser, config)
threading.Timer(3, open_with_config).start()
uvicorn.run(app, host=config["host"], port=config["port"])
+3 -1
View File
@@ -4,7 +4,7 @@ from typing import List, Literal, Optional
from pydantic import BaseModel, Field, model_validator
from app.llm import LLM
from app.llm.inference import LLM
from app.logger import logger
from app.schema import AgentState, Memory, Message
@@ -144,6 +144,8 @@ class BaseAgent(BaseModel, ABC):
results.append(f"Step {self.current_step}: {step_result}")
if self.current_step >= self.max_steps:
self.current_step = 0 # setting back to 0 when reached max steps
self.state = AgentState.IDLE # setting the status
results.append(f"Terminated: Reached max steps ({self.max_steps})")
return "\n".join(results) if results else "No steps executed"
+2 -3
View File
@@ -26,12 +26,11 @@ class Manus(ToolCallAgent):
system_prompt: str = SYSTEM_PROMPT
next_step_prompt: str = NEXT_STEP_PROMPT
max_observe: int = 2000
max_steps: int = 20
# Add general-purpose tools to the tool collection
available_tools: ToolCollection = Field(
default_factory=lambda: ToolCollection(
PythonExecute(), GoogleSearch(), BrowserUseTool(), FileSaver(), Terminate()
)
)
max_steps: int = 20
+1 -1
View File
@@ -4,7 +4,7 @@ from typing import Optional
from pydantic import Field
from app.agent.base import BaseAgent
from app.llm import LLM
from app.llm.inference import LLM
from app.schema import AgentState, Memory
+1 -5
View File
@@ -1,5 +1,5 @@
import json
from typing import Any, List, Literal, Optional, Union
from typing import Any, List, Literal
from pydantic import Field
@@ -31,7 +31,6 @@ class ToolCallAgent(ReActAgent):
tool_calls: List[ToolCall] = Field(default_factory=list)
max_steps: int = 30
max_observe: Optional[Union[int, bool]] = None
async def think(self) -> bool:
"""Process current state and decide next actions using tools"""
@@ -115,9 +114,6 @@ class ToolCallAgent(ReActAgent):
f"🎯 Tool '{command.function.name}' completed its mission! Result: {result}"
)
if self.max_observe:
result = result[: self.max_observe]
# Add tool response to memory
tool_msg = Message.tool_message(
content=result, tool_call_id=command.id, name=command.function.name
+1 -1
View File
@@ -6,7 +6,7 @@ from pydantic import Field
from app.agent.base import BaseAgent
from app.flow.base import BaseFlow, PlanStepStatus
from app.llm import LLM
from app.llm.inference import LLM
from app.logger import logger
from app.schema import AgentState, Message
from app.tool import PlanningTool
-264
View File
@@ -1,264 +0,0 @@
from typing import Dict, List, Literal, Optional, Union
from openai import (
APIError,
AsyncAzureOpenAI,
AsyncOpenAI,
AuthenticationError,
OpenAIError,
RateLimitError,
)
from tenacity import retry, stop_after_attempt, wait_random_exponential
from app.config import LLMSettings, config
from app.logger import logger # Assuming a logger is set up in your app
from app.schema import Message
class LLM:
_instances: Dict[str, "LLM"] = {}
def __new__(
cls, config_name: str = "default", llm_config: Optional[LLMSettings] = None
):
if config_name not in cls._instances:
instance = super().__new__(cls)
instance.__init__(config_name, llm_config)
cls._instances[config_name] = instance
return cls._instances[config_name]
def __init__(
self, config_name: str = "default", llm_config: Optional[LLMSettings] = None
):
if not hasattr(self, "client"): # Only initialize if not already initialized
llm_config = llm_config or config.llm
llm_config = llm_config.get(config_name, llm_config["default"])
self.model = llm_config.model
self.max_tokens = llm_config.max_tokens
self.temperature = llm_config.temperature
self.api_type = llm_config.api_type
self.api_key = llm_config.api_key
self.api_version = llm_config.api_version
self.base_url = llm_config.base_url
if self.api_type == "azure":
self.client = AsyncAzureOpenAI(
base_url=self.base_url,
api_key=self.api_key,
api_version=self.api_version,
)
else:
self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
@staticmethod
def format_messages(messages: List[Union[dict, Message]]) -> List[dict]:
"""
Format messages for LLM by converting them to OpenAI message format.
Args:
messages: List of messages that can be either dict or Message objects
Returns:
List[dict]: List of formatted messages in OpenAI format
Raises:
ValueError: If messages are invalid or missing required fields
TypeError: If unsupported message types are provided
Examples:
>>> msgs = [
... Message.system_message("You are a helpful assistant"),
... {"role": "user", "content": "Hello"},
... Message.user_message("How are you?")
... ]
>>> formatted = LLM.format_messages(msgs)
"""
formatted_messages = []
for message in messages:
if isinstance(message, dict):
# If message is already a dict, ensure it has required fields
if "role" not in message:
raise ValueError("Message dict must contain 'role' field")
formatted_messages.append(message)
elif isinstance(message, Message):
# If message is a Message object, convert it to dict
formatted_messages.append(message.to_dict())
else:
raise TypeError(f"Unsupported message type: {type(message)}")
# Validate all messages have required fields
for msg in formatted_messages:
if msg["role"] not in ["system", "user", "assistant", "tool"]:
raise ValueError(f"Invalid role: {msg['role']}")
if "content" not in msg and "tool_calls" not in msg:
raise ValueError(
"Message must contain either 'content' or 'tool_calls'"
)
return formatted_messages
@retry(
wait=wait_random_exponential(min=1, max=60),
stop=stop_after_attempt(6),
)
async def ask(
self,
messages: List[Union[dict, Message]],
system_msgs: Optional[List[Union[dict, Message]]] = None,
stream: bool = True,
temperature: Optional[float] = None,
) -> str:
"""
Send a prompt to the LLM and get the response.
Args:
messages: List of conversation messages
system_msgs: Optional system messages to prepend
stream (bool): Whether to stream the response
temperature (float): Sampling temperature for the response
Returns:
str: The generated response
Raises:
ValueError: If messages are invalid or response is empty
OpenAIError: If API call fails after retries
Exception: For unexpected errors
"""
try:
# Format system and user messages
if system_msgs:
system_msgs = self.format_messages(system_msgs)
messages = system_msgs + self.format_messages(messages)
else:
messages = self.format_messages(messages)
if not stream:
# Non-streaming request
response = await self.client.chat.completions.create(
model=self.model,
messages=messages,
max_tokens=self.max_tokens,
temperature=temperature or self.temperature,
stream=False,
)
if not response.choices or not response.choices[0].message.content:
raise ValueError("Empty or invalid response from LLM")
return response.choices[0].message.content
# Streaming request
response = await self.client.chat.completions.create(
model=self.model,
messages=messages,
max_tokens=self.max_tokens,
temperature=temperature or self.temperature,
stream=True,
)
collected_messages = []
async for chunk in response:
chunk_message = chunk.choices[0].delta.content or ""
collected_messages.append(chunk_message)
print(chunk_message, end="", flush=True)
print() # Newline after streaming
full_response = "".join(collected_messages).strip()
if not full_response:
raise ValueError("Empty response from streaming LLM")
return full_response
except ValueError as ve:
logger.error(f"Validation error: {ve}")
raise
except OpenAIError as oe:
logger.error(f"OpenAI API error: {oe}")
raise
except Exception as e:
logger.error(f"Unexpected error in ask: {e}")
raise
@retry(
wait=wait_random_exponential(min=1, max=60),
stop=stop_after_attempt(6),
)
async def ask_tool(
self,
messages: List[Union[dict, Message]],
system_msgs: Optional[List[Union[dict, Message]]] = None,
timeout: int = 60,
tools: Optional[List[dict]] = None,
tool_choice: Literal["none", "auto", "required"] = "auto",
temperature: Optional[float] = None,
**kwargs,
):
"""
Ask LLM using functions/tools and return the response.
Args:
messages: List of conversation messages
system_msgs: Optional system messages to prepend
timeout: Request timeout in seconds
tools: List of tools to use
tool_choice: Tool choice strategy
temperature: Sampling temperature for the response
**kwargs: Additional completion arguments
Returns:
ChatCompletionMessage: The model's response
Raises:
ValueError: If tools, tool_choice, or messages are invalid
OpenAIError: If API call fails after retries
Exception: For unexpected errors
"""
try:
# Validate tool_choice
if tool_choice not in ["none", "auto", "required"]:
raise ValueError(f"Invalid tool_choice: {tool_choice}")
# Format messages
if system_msgs:
system_msgs = self.format_messages(system_msgs)
messages = system_msgs + self.format_messages(messages)
else:
messages = self.format_messages(messages)
# Validate tools if provided
if tools:
for tool in tools:
if not isinstance(tool, dict) or "type" not in tool:
raise ValueError("Each tool must be a dict with 'type' field")
# Set up the completion request
response = await self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=temperature or self.temperature,
max_tokens=self.max_tokens,
tools=tools,
tool_choice=tool_choice,
timeout=timeout,
**kwargs,
)
# Check if response is valid
if not response.choices or not response.choices[0].message:
print(response)
raise ValueError("Invalid or empty response from LLM")
return response.choices[0].message
except ValueError as ve:
logger.error(f"Validation error in ask_tool: {ve}")
raise
except OpenAIError as oe:
if isinstance(oe, AuthenticationError):
logger.error("Authentication failed. Check API key.")
elif isinstance(oe, RateLimitError):
logger.error("Rate limit exceeded. Consider increasing retry attempts.")
elif isinstance(oe, APIError):
logger.error(f"API error: {oe}")
raise
except Exception as e:
logger.error(f"Unexpected error in ask_tool: {e}")
raise
View File
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
class Cost:
"""
Cost class can record various costs during running and evaluation.
Currently we define the following costs:
accumulated_cost: the total cost (USD $) of the current LLM.
"""
def __init__(self) -> None:
self._accumulated_cost: float = 0.0
self._costs: list[float] = []
@property
def accumulated_cost(self) -> float:
return self._accumulated_cost
@accumulated_cost.setter
def accumulated_cost(self, value: float) -> None:
if value < 0:
raise ValueError("Total cost cannot be negative.")
self._accumulated_cost = value
@property
def costs(self) -> list:
return self._costs
def add_cost(self, value: float) -> None:
if value < 0:
raise ValueError("Added cost cannot be negative.")
self._accumulated_cost += value
self._costs.append(value)
def get(self):
"""
Return the costs in a dictionary.
"""
return {"accumulated_cost": self._accumulated_cost, "costs": self._costs}
def log(self):
"""
Log the costs.
"""
cost = self.get()
logs = ""
for key, value in cost.items():
logs += f"{key}: {value}\n"
return logs
+525
View File
@@ -0,0 +1,525 @@
import base64
import os
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
import litellm
from litellm import completion, completion_cost
from litellm.exceptions import (
APIConnectionError,
RateLimitError,
ServiceUnavailableError,
)
from tenacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_random_exponential,
)
from app.config import LLMSettings, config
from app.llm.cost import Cost
from app.logger import logger
from app.schema import Message
class LLM:
_instances: Dict[str, "LLM"] = {}
def __new__(
cls, config_name: str = "default", llm_config: Optional[LLMSettings] = None
):
if config_name not in cls._instances:
instance = super().__new__(cls)
instance.__init__(config_name, llm_config)
cls._instances[config_name] = instance
return cls._instances[config_name]
def __init__(
self, config_name: str = "default", llm_config: Optional[LLMSettings] = None
):
if not hasattr(
self, "initialized"
): # Only initialize if not already initialized
llm_config = llm_config or config.llm
llm_config = llm_config.get(config_name, llm_config["default"])
self.model = getattr(llm_config, "model", "gpt-3.5-turbo")
self.max_tokens = getattr(llm_config, "max_tokens", 4096)
self.temperature = getattr(llm_config, "temperature", 0.7)
self.top_p = getattr(llm_config, "top_p", 0.9)
self.api_type = getattr(llm_config, "api_type", "openai")
self.api_key = getattr(
llm_config, "api_key", os.environ.get("OPENAI_API_KEY", "")
)
self.api_version = getattr(llm_config, "api_version", "")
self.base_url = getattr(llm_config, "base_url", "https://api.openai.com/v1")
self.timeout = getattr(llm_config, "timeout", 60)
self.num_retries = getattr(llm_config, "num_retries", 3)
self.retry_min_wait = getattr(llm_config, "retry_min_wait", 1)
self.retry_max_wait = getattr(llm_config, "retry_max_wait", 10)
self.custom_llm_provider = getattr(llm_config, "custom_llm_provider", None)
# Get model info if available
self.model_info = None
try:
self.model_info = litellm.get_model_info(self.model)
except Exception as e:
logger.warning(f"Could not get model info for {self.model}: {e}")
# Configure litellm
if self.api_type == "azure":
litellm.api_base = self.base_url
litellm.api_key = self.api_key
litellm.api_version = self.api_version
else:
litellm.api_key = self.api_key
if self.base_url:
litellm.api_base = self.base_url
# Initialize cost tracker
self.cost_tracker = Cost()
self.initialized = True
# Initialize completion function
self._initialize_completion_function()
def _initialize_completion_function(self):
"""Initialize the completion function with retry logic"""
def attempt_on_error(retry_state):
logger.error(
f"{retry_state.outcome.exception()}. Attempt #{retry_state.attempt_number}"
)
return True
@retry(
reraise=True,
stop=stop_after_attempt(self.num_retries),
wait=wait_random_exponential(
min=self.retry_min_wait, max=self.retry_max_wait
),
retry=retry_if_exception_type(
(RateLimitError, APIConnectionError, ServiceUnavailableError)
),
after=attempt_on_error,
)
def wrapper(*args, **kwargs):
model_name = self.model
if self.api_type == "azure":
model_name = f"azure/{self.model}"
# Set default parameters if not provided
if "max_tokens" not in kwargs:
kwargs["max_tokens"] = self.max_tokens
if "temperature" not in kwargs:
kwargs["temperature"] = self.temperature
if "top_p" not in kwargs:
kwargs["top_p"] = self.top_p
if "timeout" not in kwargs:
kwargs["timeout"] = self.timeout
kwargs["model"] = model_name
# Add API credentials if not in kwargs
if "api_key" not in kwargs:
kwargs["api_key"] = self.api_key
if "base_url" not in kwargs and self.base_url:
kwargs["base_url"] = self.base_url
if "api_version" not in kwargs and self.api_version:
kwargs["api_version"] = self.api_version
if "custom_llm_provider" not in kwargs and self.custom_llm_provider:
kwargs["custom_llm_provider"] = self.custom_llm_provider
resp = completion(**kwargs)
return resp
self._completion = wrapper
@staticmethod
def format_messages(messages: List[Union[dict, Message]]) -> List[dict]:
"""
Format messages for LLM by converting them to OpenAI message format.
Args:
messages: List of messages that can be either dict or Message objects
Returns:
List[dict]: List of formatted messages in OpenAI format
Raises:
ValueError: If messages are invalid or missing required fields
TypeError: If unsupported message types are provided
"""
formatted_messages = []
for message in messages:
if isinstance(message, dict):
# If message is already a dict, ensure it has required fields
if "role" not in message:
raise ValueError("Message dict must contain 'role' field")
formatted_messages.append(message)
elif isinstance(message, Message):
# If message is a Message object, convert it to dict
formatted_messages.append(message.to_dict())
else:
raise TypeError(f"Unsupported message type: {type(message)}")
# Validate all messages have required fields
for msg in formatted_messages:
if msg["role"] not in ["system", "user", "assistant", "tool"]:
raise ValueError(f"Invalid role: {msg['role']}")
if "content" not in msg and "tool_calls" not in msg:
raise ValueError(
"Message must contain either 'content' or 'tool_calls'"
)
return formatted_messages
def _calculate_and_track_cost(self, response) -> float:
"""
Calculate and track the cost of an LLM API call.
Args:
response: The response from litellm
Returns:
float: The calculated cost
"""
try:
# Use litellm's completion_cost function
cost = completion_cost(completion_response=response)
# Add the cost to our tracker
if cost > 0:
self.cost_tracker.add_cost(cost)
logger.info(
f"Added cost: ${cost:.6f}, Total: ${self.cost_tracker.accumulated_cost:.6f}"
)
return cost
except Exception as e:
logger.warning(f"Cost calculation failed: {e}")
return 0.0
def is_local(self) -> bool:
"""
Check if the model is running locally.
Returns:
bool: True if the model is running locally, False otherwise
"""
if self.base_url:
return any(
substring in self.base_url
for substring in ["localhost", "127.0.0.1", "0.0.0.0"]
)
if self.model and (
self.model.startswith("ollama") or "local" in self.model.lower()
):
return True
return False
def do_completion(self, *args, **kwargs) -> Tuple[Any, float, float]:
"""
Perform a completion request and track cost.
Returns:
Tuple[Any, float, float]: (response, current_cost, accumulated_cost)
"""
response = self._completion(*args, **kwargs)
# Calculate and track cost
current_cost = self._calculate_and_track_cost(response)
return response, current_cost, self.cost_tracker.accumulated_cost
@staticmethod
def encode_image(image_path: str) -> str:
"""
Encode an image to base64.
Args:
image_path: Path to the image file
Returns:
str: Base64-encoded image
"""
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode("utf-8")
def prepare_messages(
self, text: str, image_path: Optional[str] = None
) -> List[dict]:
"""
Prepare messages for completion, including multimodal content if needed.
Args:
text: Text content
image_path: Optional path to an image file
Returns:
List[dict]: Formatted messages
"""
messages = [{"role": "user", "content": text}]
if image_path:
base64_image = self.encode_image(image_path)
messages[0]["content"] = [
{"type": "text", "text": text},
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{base64_image}"},
},
]
return messages
def do_multimodal_completion(
self, text: str, image_path: str
) -> Tuple[Any, float, float]:
"""
Perform a multimodal completion with text and image.
Args:
text: Text prompt
image_path: Path to the image file
Returns:
Tuple[Any, float, float]: (response, current_cost, accumulated_cost)
"""
messages = self.prepare_messages(text, image_path=image_path)
return self.do_completion(messages=messages)
@retry(
wait=wait_random_exponential(min=1, max=60),
stop=stop_after_attempt(6),
)
async def ask(
self,
messages: List[Union[dict, Message]],
system_msgs: Optional[List[Union[dict, Message]]] = None,
stream: bool = True,
temperature: Optional[float] = None,
) -> str:
"""
Send a prompt to the LLM and get the response.
Args:
messages: List of conversation messages
system_msgs: Optional system messages to prepend
stream (bool): Whether to stream the response
temperature (float): Sampling temperature for the response
Returns:
str: The generated response
Raises:
ValueError: If messages are invalid or response is empty
Exception: For unexpected errors
"""
try:
# Format system and user messages
if system_msgs:
system_msgs = self.format_messages(system_msgs)
messages = system_msgs + self.format_messages(messages)
else:
messages = self.format_messages(messages)
model_name = self.model
if self.api_type == "azure":
# For Azure, litellm expects model name in format: azure/<deployment_name>
model_name = f"azure/{self.model}"
if not stream:
# Non-streaming request
response = await litellm.acompletion(
model=model_name,
messages=messages,
max_tokens=self.max_tokens,
temperature=temperature or self.temperature,
stream=False,
)
# Calculate and track cost
self._calculate_and_track_cost(response)
if not response.choices or not response.choices[0].message.content:
raise ValueError("Empty or invalid response from LLM")
return response.choices[0].message.content
# Streaming request
collected_messages = []
async for chunk in await litellm.acompletion(
model=model_name,
messages=messages,
max_tokens=self.max_tokens,
temperature=temperature or self.temperature,
stream=True,
):
chunk_message = chunk.choices[0].delta.content or ""
collected_messages.append(chunk_message)
print(chunk_message, end="", flush=True)
# For streaming responses, cost is calculated on the last chunk
if hasattr(chunk, "usage") and chunk.usage:
self._calculate_and_track_cost(chunk)
print() # Newline after streaming
full_response = "".join(collected_messages).strip()
if not full_response:
raise ValueError("Empty response from streaming LLM")
return full_response
except ValueError as ve:
logger.error(f"Validation error: {ve}")
raise
except Exception as e:
logger.error(f"Unexpected error in ask: {e}")
raise
@retry(
wait=wait_random_exponential(min=1, max=60),
stop=stop_after_attempt(6),
)
async def ask_tool(
self,
messages: List[Union[dict, Message]],
system_msgs: Optional[List[Union[dict, Message]]] = None,
timeout: int = 60,
tools: Optional[List[dict]] = None,
tool_choice: Literal["none", "auto", "required"] = "auto",
temperature: Optional[float] = None,
**kwargs,
):
"""
Ask LLM using functions/tools and return the response.
Args:
messages: List of conversation messages
system_msgs: Optional system messages to prepend
timeout: Request timeout in seconds
tools: List of tools to use
tool_choice: Tool choice strategy
temperature: Sampling temperature for the response
**kwargs: Additional completion arguments
Returns:
The model's response
Raises:
ValueError: If tools, tool_choice, or messages are invalid
Exception: For unexpected errors
"""
try:
# Validate tool_choice
if tool_choice not in ["none", "auto", "required"]:
raise ValueError(f"Invalid tool_choice: {tool_choice}")
# Format messages
if system_msgs:
system_msgs = self.format_messages(system_msgs)
messages = system_msgs + self.format_messages(messages)
else:
messages = self.format_messages(messages)
# Validate tools if provided
if tools:
for tool in tools:
if not isinstance(tool, dict) or "type" not in tool:
raise ValueError("Each tool must be a dict with 'type' field")
model_name = self.model
if self.api_type == "azure":
# For Azure, litellm expects model name in format: azure/<deployment_name>
model_name = f"azure/{self.model}"
# Set up the completion request
response = await litellm.acompletion(
model=model_name,
messages=messages,
temperature=temperature or self.temperature,
max_tokens=self.max_tokens,
tools=tools,
tool_choice=tool_choice,
timeout=timeout,
**kwargs,
)
# Calculate and track cost
self._calculate_and_track_cost(response)
# Check if response is valid
if not response.choices or not response.choices[0].message:
print(response)
raise ValueError("Invalid or empty response from LLM")
return response.choices[0].message
except ValueError as ve:
logger.error(f"Validation error: {ve}")
raise
except Exception as e:
logger.error(f"Unexpected error in ask_tool: {e}")
raise
def get_cost(self):
"""
Get the current cost information.
Returns:
dict: Dictionary containing accumulated cost and individual costs
"""
return self.cost_tracker.get()
def log_cost(self):
"""
Log the current cost information.
Returns:
str: Formatted string of cost information
"""
return self.cost_tracker.log()
def get_token_count(self, messages):
"""
Get the token count for a list of messages.
Args:
messages: List of messages
Returns:
int: Token count
"""
return litellm.token_counter(model=self.model, messages=messages)
def __str__(self):
return f"LLM(model={self.model}, base_url={self.base_url})"
def __repr__(self):
return str(self)
# Example usage
if __name__ == "__main__":
# Load environment variables if needed
from dotenv import load_dotenv
load_dotenv()
# Create LLM instance
llm = LLM()
# Test text completion
messages = llm.prepare_messages("Hello, how are you?")
response, cost, total_cost = llm.do_completion(messages=messages)
print(f"Response: {response['choices'][0]['message']['content']}")
print(f"Cost: ${cost:.6f}, Total cost: ${total_cost:.6f}")
# Test multimodal if image path is available
image_path = os.getenv("TEST_IMAGE_PATH")
if image_path and os.path.exists(image_path):
multimodal_response, mm_cost, mm_total_cost = llm.do_multimodal_completion(
"What's in this image?", image_path
)
print(
f"Multimodal response: {multimodal_response['choices'][0]['message']['content']}"
)
print(f"Cost: ${mm_cost:.6f}, Total cost: ${mm_total_cost:.6f}")
+4
View File
@@ -10,5 +10,9 @@ BrowserUseTool: Open, browse, and use web browsers.If you open a local HTML file
GoogleSearch: Perform web information retrieval
Terminate: End the current interaction when the task is complete or when you need additional information from the user. Use this tool to signal that you've finished addressing the user's request or need clarification before proceeding further.
Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, you can break down the problem and use different tools step by step to solve it. After using each tool, clearly explain the execution results and suggest the next steps.
Always maintain a helpful, informative tone throughout the interaction. If you encounter any limitations or need more details, clearly communicate this to the user before terminating.
"""
+6 -7
View File
@@ -12,6 +12,8 @@ from pydantic_core.core_schema import ValidationInfo
from app.tool.base import BaseTool, ToolResult
MAX_LENGTH = 2000
_BROWSER_DESCRIPTION = """
Interact with a web browser to perform various actions such as navigation, element interaction,
content extraction, and tab management. Supported actions include:
@@ -103,12 +105,7 @@ class BrowserUseTool(BaseTool):
async def _ensure_browser_initialized(self) -> BrowserContext:
"""Ensure browser and context are initialized."""
if self.browser is None:
# 使用Chrome命令行参数设置窗口大小和位置
browser_config = BrowserConfig(
headless=False,
disable_security=True,
)
self.browser = BrowserUseBrowser(browser_config)
self.browser = BrowserUseBrowser(BrowserConfig(headless=False))
if self.context is None:
self.context = await self.browser.new_context()
self.dom_service = DomService(await self.context.get_current_page())
@@ -185,7 +182,9 @@ class BrowserUseTool(BaseTool):
elif action == "get_html":
html = await context.get_page_html()
truncated = html[:2000] + "..." if len(html) > 2000 else html
truncated = (
html[:MAX_LENGTH] + "..." if len(html) > MAX_LENGTH else html
)
return ToolResult(output=truncated)
elif action == "get_text":
+1 -2
View File
@@ -1,8 +1,7 @@
from app.tool.base import BaseTool
_TERMINATE_DESCRIPTION = """Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task.
When you have finished all the tasks, call this tool to end the work."""
_TERMINATE_DESCRIPTION = """Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task."""
class Terminate(BaseTool):
Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 166 KiB

+4 -9
View File
@@ -1,7 +1,7 @@
# Global LLM configuration
[llm]
model = "claude-3-5-sonnet"
base_url = "https://api.openai.com/v1"
model = "gpt-4o" #"claude-3-5-sonnet"
base_url = "https://api.openai.com/v1" # "https://api.anthropic.com"
api_key = "sk-..."
max_tokens = 4096
temperature = 0.0
@@ -17,11 +17,6 @@ temperature = 0.0
# Optional configuration for specific LLM models
[llm.vision]
model = "claude-3-5-sonnet"
base_url = "https://api.openai.com/v1"
model = "gpt-4o" # "claude-3-5-sonnet"
base_url = "https://api.openai.com/v1" # "https://api.anthropic.com"
api_key = "sk-..."
# Server configuration
[server]
host = "localhost"
port = 5172
-5
View File
@@ -1,5 +0,0 @@
build
node_modules
frontend/dist
frontend/package.json.md5
*.log
-71
View File
@@ -1,71 +0,0 @@
# OpenManus-Desktop Project
## Project Overview
OpenManus-Desktop is a desktop application built on the Wails framework, combining Go backend and Vue3 frontend technologies. The project utilizes Vite as the frontend build tool, offering an efficient development experience.
## Technology Stack
- Backend: Go
- Frontend: Vue3 + Vite
- UI Framework: Element Plus
- State Management: Pinia
- Routing: Vue Router
- Build Tool: Wails
## Development Environment Requirements
- Go 1.18+
- Node.js 20+
- Wails CLI v2+
## Getting Started
### 1. Install Development Environment
#### 1.1. Install Golang Environment
Golang environment : https://go.dev/dl/
#### 1.2. Install Wails Client
wails: https://wails.io/
// For users in mainland China, use a proxy
go env -w GOPROXY=https://goproxy.cn
go install github.com/wailsapp/wails/v2/cmd/wails@latest
Run the following command to check if the Wails client is installed successfully:
wails doctor
#### 1.3. Install Node.js Environment
nodejs: https://nodejs.org/en
### 2. Install Project Dependencies
cd .\desktop\frontend
npm install
### 3. Run the Project
To run the project:
cd .\desktop
wails dev
To start the backend service:
After configuring the config/config.toml file, execute the following command to start the server:
cd .. (Project root directory)
python app.py
### 4. Package the Project
To build the application:
wails build
The built application will be located in the projects dist directory.
-71
View File
@@ -1,71 +0,0 @@
# OpenManus-Desktop 项目
## 项目简介
OpenManus-Desktop 是一个基于Wails框架构建的桌面应用程序,结合了Go后端和Vue3前端技术栈。项目采用Vite作为前端构建工具,提供了高效的开发体验。
## 技术栈
- 后端: Go
- 前端: Vue3 + Vite
- UI 框架: Element Plus
- 状态管理: Pinia
- 路由: Vue Router
- 构建工具: Wails
## 开发环境要求
- Go 1.18+
- Node.js 20+
- Wails CLI v2+
## 快速开始
### 1. 安装开发环境
#### 1.1. 安装Go语言环境
Go环境下载: https://go.dev/dl/
#### 1.2. 安装wails客户端
wails官网: https://wails.io/
// 中国大陆使用代理
go env -w GOPROXY=https://goproxy.cn
go install github.com/wailsapp/wails/v2/cmd/wails@latest
执行以下命名令检查wails客户端安装是否成功:
wails doctor
#### 1.3. 安装Node.js环境
nodejs官网安装: https://nodejs.org/en
### 2. 安装项目依赖
cd .\desktop\frontend
npm install
### 3. 运行项目
运行项目:
cd .\desktop
wails dev
启动服务端:
配置好config/config.toml文件后, 执行以下命令启动服务端:
cd .. (项目根目录)
python app.py
### 4. 打包项目
构建应用:
wails build
构建好的应用在项目dist目录下
-107
View File
@@ -1,107 +0,0 @@
package main
import (
"OpenManus/src/utils"
"context"
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
}
type File struct {
Result string `json:"result"`
Error string `json:"error"`
Callbackid string `json:"callbackid"`
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
// 注册事件监听器
runtime.EventsOn(ctx, "events", func(data ...interface{}) {
if len(data) > 0 {
for i := 0; i < len(data); i++ {
fmt.Println("Received events with data:", data[i])
}
} else {
fmt.Println("Received events without data")
}
})
// 注册bat批处理事件监听器
runtime.EventsOn(ctx, "bat", func(data ...interface{}) {
if len(data) == 2 {
fmt.Println("Received bat with data, batId: ", data[0])
fmt.Println("Received bat with data, batPath: ", data[1])
utils.ExecBatFile(a.ctx, data[0].(string), data[1].(string))
} else if len(data) > 0 && len(data) < 2 {
fmt.Println("Received bat with data, required 2 paramters, found 1: ", data[0])
} else {
fmt.Println("Received bat without data")
}
})
// 注册执行py脚本监听器
runtime.EventsOn(ctx, "pyFile", func(data ...interface{}) {
if len(data) == 2 {
fmt.Println("Received bat with data, batId: ", data[0])
fmt.Println("Received bat with data, batPath: ", data[1])
utils.ExecPyFile(a.ctx, data[0].(string), data[1].(string))
} else if len(data) > 0 && len(data) < 2 {
fmt.Println("Received bat with data, required 2 paramters, found 1: ", data[0])
} else {
fmt.Println("Received bat without data")
}
})
}
// Greet returns a greeting for the given name
func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s, It's show time!", name)
}
// ReadAll reads file content
func (a *App) ReadAll(filePath string) string {
utils.Log("ReadAll filePath: ", filePath)
// Read the file content, resulting in a JSON string containing file content and callback ID
data := string(utils.ReadAll(filePath))
utils.Log("ReadAll data: ", data)
return data
}
func (a *App) SaveFile(filePath string, data string) {
utils.SaveFile(filePath, data)
}
func (a *App) PathExists(path string) bool {
exists, _ := utils.PathExists(path)
return exists
}
func (a *App) DirSize(path string) int64 {
utils.Log("DirSize path: ", path)
size, _ := utils.DirSize(path)
utils.Log("DirSize size: ", size)
return size
}
func (a *App) AppPath() string {
return utils.AppPath()
}
func (a *App) CheckPort(port string) bool {
return utils.CheckPort(port)
}
-8
View File
@@ -1,8 +0,0 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs,
check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
-15
View File
@@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>OpenManus</title>
</head>
<body>
<div id="app"></div>
<script src="./src/main.js" type="module"></script>
</body>
</html>
-3508
View File
File diff suppressed because it is too large Load Diff
-32
View File
@@ -1,32 +0,0 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.8.3",
"element-plus": "^2.9.2",
"marked": "^15.0.7",
"pinia": "^3.0.1",
"pinia-plugin-persistedstate": "^4.2.0",
"qs": "^6.14.0",
"sql-formatter": "^15.4.9",
"vue": "^3.2.37",
"vue-i18n": "^11.0.0-rc.1",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@types/qs": "^6.9.18",
"@vitejs/plugin-vue": "^3.0.3",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.83.1",
"unplugin-auto-import": "^0.19.0",
"unplugin-vue-components": "^0.28.0",
"vite": "^3.0.7"
}
}
-2069
View File
File diff suppressed because it is too large Load Diff
-53
View File
@@ -1,53 +0,0 @@
<template>
<!-- Global Configuration -->
<el-config-provider :size="size" :z-index="zIndex" :locale="locale" :button="config" :message="config"
:value-on-clear="null" :empty-values="[undefined, null]">
<RouterView />
</el-config-provider>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import en from 'element-plus/es/locale/lang/en'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
/** Dark Theme */
import { useDark, useStorage } from '@vueuse/core'
const size = 'default'
const zIndex = 2000
const localConfig = localStorage.getItem('config') ? JSON.parse(localStorage.getItem('config')) : {}
const localeStr = localConfig.selectedLang ? localConfig.selectedLang.code : 'en'
const locale = localeStr == 'en' ? en : zhCn
const isDark = useDark()
// Store user preferences
const userPrefersDark = ref(null)
onMounted(() => {
// Use useStorage hook to sync isDark and local storage
useStorage(
'user-prefers-dark',
userPrefersDark,
localStorage,
isDark.value ? 'dark' : 'light'
)
})
// Watch isDark changes and update local storage
watch(isDark, (newValue) => {
userPrefersDark.value = newValue ? 'dark' : 'light'
})
/* Global Configuration */
const config = reactive({
// Button - Automatically insert space between Chinese characters
autoInsertSpace: true,
// Message - Maximum number of messages that can be displayed simultaneously
max: 3,
})
</script>
<style scoped></style>
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 989 B

-338
View File
@@ -1,338 +0,0 @@
import { ReadAll, SaveFile, PathExists, DirSize, AppPath } from '@/../wailsjs/go/main/App.js'
import utils from '@/assets/js/utils'
// Temporary cache for file information
function cache(fileObj, $event) {
console.log('Caching fileObj start:', fileObj, $event.target, $event.dataTransfer)
console.log('typeof fileObj:', Array.isArray(fileObj))
// If fileObj is an array, create a new element and append to the array
// event.target.files and event.dataTransfer.files are event properties in JavaScript related to file upload and drag-and-drop.
// event.target.files: This property is used with HTML file input elements (<input type="file">),
// When the user selects a file and triggers the change event, event.target.files can be used to get the list of files selected by the user.
// event.dataTransfer.files: This property is used when the user drags and drops files onto an element,
// event.dataTransfer.files can be used to get the list of dropped files.
console.log('$event:', $event, $event.type)
let files
if ($event.type == 'change') {
files = $event.target.files
} else if ($event.type == 'drop') {
files = $event.dataTransfer.files
} else {
console.error("Unrecognized event type")
return
}
const file = files[0]
console.log("Selected file:", file)
const fileInfo = Array.isArray(fileObj) ? new Object() : fileObj
fileInfo.file = file
let URL = window.URL || window.webkitURL
fileInfo.fileUrl = URL.createObjectURL(file)
const fileType = file.type
console.log("File type:", fileType, typeof (fileType))
if (utils.notNull(fileType) && fileType.startsWith("image")) {
fileInfo.imgUrl = fileInfo.fileUrl
}
fileInfo.fileName = file.name
console.log('Caching fileObj completed:', fileInfo)
if (Array.isArray(fileObj)) {
// Append to the end of the array after successful operation
fileObj.push(fileInfo)
}
if ($event.type == 'change') {
// Solve the problem of selecting the same file not triggering the change event, clean up at the end
$event.target.value = null
}
}
// Upload file
async function upload(fileObj) {
console.log("Preparing to upload file...", fileObj, fileObj.file, fileObj.fileId)
// Current location handling
if (utils.isNull(fileObj.file)) {
if (utils.notNull(fileObj.fileId) && fileObj.remark != fileObj.remarkUpd) {
let remark = null
if (utils.notNull(fileObj.remarkUpd)) {
remark = fileObj.remarkUpd
}
await updRemark(fileObj.fileId, remark)
}
return
}
console.log("Starting file upload...", fileObj, fileObj.file, fileObj.fileId)
const url = '/common/file/upload'
const formData = new FormData()
formData.append('file', fileObj.file)
if (utils.notNull(fileObj.remark)) {
formData.append('remark', fileObj.remark)
} else if (utils.notNull(fileObj.remarkUpd)) {
formData.append('remark', fileObj.remarkUpd)
}
const data = await utils.awaitPost(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
Object.assign(fileObj, data)
console.log("File upload processed successfully", fileObj)
return fileObj
}
// Update file remark
async function updRemark(fileId, remarkUpd) {
const param = {
fileId: fileId,
remark: remarkUpd
}
await utils.awaitPost('/common/file/updRemark', param)
console.log("File remark updated successfully")
}
// Batch upload files
async function uploads(fileObjs) {
if (utils.isEmpty(fileObjs)) {
return
}
for (let index in fileObjs) {
console.log('Processing file object:', fileObjs, index, fileObjs.length, fileObjs[index])
await upload(fileObjs[index])
console.log("uploads index:", index, "File upload completed", fileObjs[index])
}
}
// Handle file upload (onChange event)
function upOnChg(fileObj, $event) {
const file = $event.target.files[0] || $event.dataTransfer.files[0]
// Current location
let URL = window.URL || window.webkitURL
// Convert to blob URL
fileObj.fileUrl = URL.createObjectURL(file)
const url = '/common/file/upload'
const formData = new FormData()
formData.append('file', file)
formData.append('remark', fileObj.remark)
utils.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then((data) => {
console.log("File upload result:", data)
Object.assign(fileObj, data)
fileObj.remarkUpd = data.remark
})
}
// Add to component list
function add(fileList) {
const comp = {
index: fileList.length,
file: null,
fileId: null,
fileName: null,
fileUrl: null,
imgUrl: null,
remark: null
}
fileList.push(comp)
}
// Remove component from list
function del(fileObj, index) {
console.log("Deleting file object:", fileObj, index)
if (Array.isArray(fileObj)) {
fileObj.splice(index, 1)
} else {
utils.clearProps(fileObj)
}
}
// Convert between Java and JS file objects
function trans(javaFile, jsFile) {
if (jsFile == undefined || jsFile == null) {
return
}
// Clear array if present
if (jsFile instanceof Array) {
jsFile.splice(0, jsFile.length)
} else {
utils.clearProps(jsFile)
}
if (javaFile == undefined || javaFile == null) {
return
}
// Handle array type
if (jsFile instanceof Array) {
for (let java of javaFile) {
const js = {}
java.remarkUpd = java.remark
Object.assign(js, java)
jsFile.push(js)
}
} else {
// Handle object type
console.log("Object type conversion", jsFile instanceof Array)
javaFile.remarkUpd = javaFile.remark
Object.assign(jsFile, javaFile)
}
}
// Collect file IDs from components
function fileIds(fileList) {
return fileList.map(comp => comp.fileId).join(',')
}
// Read file contents
function readAll(filePath) {
return ReadAll(filePath)
}
// Await Read file contents
async function awaitReadAll(filePath) {
return await ReadAll(filePath)
}
// Save file
function saveFile(filePath, content) {
return SaveFile(filePath, content)
}
/**
* Reads a TOML node info to a json object
*/
async function readTomlNode(filePath, nodeName) {
const fileContent = await readAll(filePath)
// console.log("Read Toml file, filePath:", filePath, ", fileContent:", fileContent)
if (utils.isBlank(fileContent)) {
utils.pop('readTomlFailed')
return
}
const lines = utils.stringToLines(fileContent)
// Read Node
const nodeStart = lines.findIndex((line) => {
return line.includes("[" + nodeName + "]")
})
const node = {}
for (let i = nodeStart + 1; i < lines.length; i++) {
// console.log("line: ", lines[i])
// Determine whether the next configuration module has been reached.
if (lines[i].startsWith("[")) {
break
}
// 读取配置
const line = lines[i]
if (line.startsWith("#")) {
continue
}
const lineArr = line.split("=")
if (lineArr.length == 0) {
continue
}
const key = lineArr[0].trim()
let value = ""
if (lineArr.length == 2) {
value = lineArr[1].trim()
}
node[key] = value
}
console.log("Read node from toml file, result: ", node)
return node
}
/**
* Save a toml node
*/
async function saveTomlNode(filePath, nodeName, newNodeJson) {
const fileContent = await readAll(filePath)
// console.log("Read Toml file, filePath:", filePath, ", fileContent:", fileContent)
if (utils.isBlank(fileContent)) {
utils.pop(t('readTomlFailed'))
return
}
const lines = utils.stringToLines(fileContent)
// Read Node
const nodeStart = lines.findIndex((line) => {
return line.includes("[" + nodeName + "]")
})
for (let i = nodeStart + 1; i < lines.length; i++) {
// console.log("line: ", lines[i])
// Determine whether the next configuration module has been reached.
if (lines[i].startsWith("[")) {
break
}
// 读取配置
const line = lines[i]
if (line.startsWith("#")) {
continue
}
const lineArr = line.split("=")
if (lineArr.length == 0) {
continue
}
const key = lineArr[0].trim()
let value = newNodeJson[key]
if (utils.isNull(value)) {
continue
}
value = value.trim()
lines[i] = key + " = " + value
}
console.log("Save node from toml file, new lines: ", lines)
const newContent = lines.join("\n")
await saveFile(filePath, newContent)
}
function pathExists(path) {
return PathExists(path)
}
function dirSize(path) {
return DirSize(path)
}
async function awaitDirSize(path) {
return await dirSize(path)
}
function appPath(path) {
return AppPath(path)
}
async function awaitAppPath(path) {
return await appPath(path)
}
export default {
// Cache on onChange
cache,
// Upload file
upload,
// Upload files
uploads,
// Upload file
upOnChg,
// Upload on onChange
upOnChg,
// Add to component list
add,
// Delete component from component list
del,
// Convert between Java object and js object
trans,
// Collect fileId from Comps
fileIds,
// Read file
readAll,
// Read toml node
readTomlNode,
// Save toml node
saveTomlNode,
pathExists,
dirSize,
awaitDirSize,
appPath,
awaitAppPath,
}
-22
View File
@@ -1,22 +0,0 @@
import { useEventListener } from '@vueuse/core'
/*
* Show page shade
*/
export const showShade = function (closeCallBack) {
const className = 'shade'
const containerEl = document.querySelector('.layout-container')
const shadeDiv = document.createElement('div')
shadeDiv.setAttribute('class', 'layout-shade ' + className)
containerEl.appendChild(shadeDiv)
useEventListener(shadeDiv, 'click', () => closeShade(closeCallBack))
}
/*
* Hide page shade
*/
export const closeShade = function (closeCallBack = () => { }) {
const shadeEl = document.querySelector('.layout-shade')
shadeEl && shadeEl.remove()
closeCallBack()
}
-701
View File
@@ -1,701 +0,0 @@
import { Greet, CheckPort } from '@/../wailsjs/go/main/App.js'
import axios from "axios"
import { ElMessage } from 'element-plus'
/** axios start */
// Create a new axios instance
const $axios = axios.create({
baseURL: "api",
timeout: 12000
})
// Request interceptors
$axios.interceptors.request.use(
(config) => {
config.headers["token"] = ''
if (config.method == "post" || config.method == "put") {
delNullProperty(config.data)
fomateDateProperty(config.data)
} else if (config.method == "get" || config.method == "delete") {
delNullProperty(config.params)
fomateDateProperty(config.params)
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// Response interceptors
$axios.interceptors.response.use(
(response) => {
// console.log("response:", response)
if (response.status == 200) {
return response.data
} else {
pop("Exception occurred in response:" + response.status)
}
},
(error) => {
console.log("error:" + JSON.stringify(error))
if (error.response == undefined || error.response == null) {
pop("Unknown request error!")
pop("Unknown request error!")
} else if (error.response.status == 500) {
pop("Unable to communicate with backend, please retry later!")
} else {
pop("Request error:" + error)
pop("Request error:" + error)
}
return Promise.reject(error)
}
)
function get(url, param) {
return $axios.get(url, { params: param })
}
async function awaitGet(url, param) {
return await $axios.get(url, { params: param })
}
function post(url, param) {
return $axios.post(url, param)
}
async function awaitPost(url, param) {
return await $axios.post(url, param)
}
function del(url, param) {
return $axios.delete(url, { params: param })
}
async function awaitDel(url, param) {
return await $axios.delete(url, { params: param })
}
/**
* demo call Go interfaces
*/
function greet(name) {
return Greet(name).then(resp => {
console.log("greet resp:", resp)
})
}
function checkPort(port) {
return CheckPort(port)
}
async function awaitCheckPort(port) {
return await checkPort(port)
}
/**
* Check if object is null
*/
function isNull(obj) {
return obj == undefined || obj == null
}
/**
* Check if object is not null
*/
function notNull(obj) {
return obj != undefined && obj != null
}
/**
* Check if string is blank
*/
function isBlank(str) {
return str == undefined || str == null || /^s*$/.test(str)
}
/**
* Identify a non-empty string
*/
function notBlank(str) {
return !isBlank(str)
}
/**
* Check if array is empty
*/
function isEmpty(arr) {
return arr == undefined || arr == null || (arr instanceof Array && arr.length == 0)
}
/**
* Check if array is not empty
*/
function notEmpty(arr) {
return arr != undefined && arr != null && arr instanceof Array && arr.length > 0
}
/**
* Check if object is true
*/
function isTrue(obj) {
return obj == true || obj == 'true'
}
/**
* Check if object is false
*/
function isFalse(obj) {
return !isTrue(obj)
}
/**
* Get count of a specific character in a string
* @param {string} str - String to search
* @param {string} char - Character to find
* @returns {number} - Occurrence count
*/
function getCharCount(str, char) {
// g=match globally
var regex = new RegExp(char, 'g')
// Search for all occurrences of the character in the string
var result = str.match(regex)
var count = !result ? 0 : result.length
return count
}
/**
* Format date with specified pattern
* @param {Date|string} date - Date object or date string
* @param {string} format - Target format pattern; by default, `yyyy-MM-dd HH:mm:ss`
* @returns {string} - Formatted date string
*/
function dateFormat(date, format) {
if (date == undefined || date == null || date == '') {
return date
}
if (format == undefined || format == null
|| format == '' || format == 0
|| format == "datetime" || format == 'date_time'
|| format == 'DATE_TIME' || format == 'DATETIME') {
format = "yyyy-MM-dd HH:mm:ss"
} else if (format == 'date' || format == 'DATE' || format == 1) {
format = "yyyy-MM-dd"
}
date = new Date(date)
const Y = date.getFullYear() + '',
M = date.getMonth() + 1,
D = date.getDate(),
H = date.getHours(),
m = date.getMinutes(),
s = date.getSeconds()
return format.replace(/YYYY|yyyy/g, Y)
.replace(/YY|yy/g, Y.substring(2, 2))
.replace(/MM/g, (M < 10 ? '0' : '') + M)
.replace(/dd/g, (D < 10 ? '0' : '') + D)
.replace(/HH|hh/g, (H < 10 ? '0' : '') + H)
.replace(/mm/g, (m < 10 ? '0' : '') + m)
.replace(/ss/g, (s < 10 ? '0' : '') + s)
}
/**
* Recursively format Date properties in objects/arrays
* @param {Object} obj - Target object to process
*/
function fomateDateProperty(obj) {
for (let i in obj) {
// Iterate through all properties of the object
if (obj[i] == null) {
continue
} else if (obj[i] instanceof Date) {
// Format as `yyyy-MM-dd HH:mm:ss`
obj[i] = dateFormat(obj[i])
} else if (obj[i].constructor === Object) {
// Recursively format nested objects
if (Object.keys(obj[i]).length > 0) {
// Delete empty properties
fomateDateProperty(obj[i])
}
} else if (obj[i].constructor === Array) {
// Recursively clean nested arrays
if (obj[i].length > 0) {
for (let j = 0; j < obj[i].length; j++) {
// Iterate through all array items
fomateDateProperty(obj[i][j])
}
}
}
}
}
/**
* Remove null/empty properties recursively
* @param {Object} obj - Target object to clean
*/
function delNullProperty(obj) {
for (let i in obj) {
// Iterate through all properties of the object
if (obj[i] === undefined || obj[i] === null || obj[i] === "") {
// Delete general null/empty properties
delete obj[i]
} else if (obj[i].constructor === Object) {
// Recursively clean nested objects
if (Object.keys(obj[i]).length === 0) delete obj[i]
// Delete empty properties
delNullProperty(obj[i])
} else if (obj[i].constructor === Array) {
// Recursively clean arrays
if (obj[i].length === 0) {
// Delete empty arrays
delete obj[i]
} else {
for (let index = 0; index < obj[i].length; index++) {
// Iterate through all array items
if (obj[i][index] === undefined || obj[i][index] === null || obj[i][index] === "" || JSON.stringify(obj[i][index]) === "{}") {
obj[i].splice(index, 1)
// Delete null/empty array items
index--
// Do decrement to avoid skipping next item (index is now pointing to the next item)
}
if (obj[i].constructor === Object) {
// Recursively clean nested objects in array items
delNullProperty(obj[i])
}
}
}
}
}
}
/**
* Display message notification
* @param {string} msg - Message content
* @param {string} type - Message type (success/warning/error/etc)
*/
function pop(msg, type) {
ElMessage({ message: msg, type: type })
}
/**
* Show default message when no data available
* @param {*} data - Data to check
*/
function popNoData(data) {
if (data == undefined || data == null || (data instanceof Array && data.length == 0)) {
ElMessage("No data available!")
}
}
/**
* Get current datetime as formatted string
* @returns {string} Current datetime in yyyy-MM-dd HH:mm format
*/
function nowDatetimeStr() {
const date = new Date()
const datetimeStr = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
return datetimeStr
}
/**
* Pagination structure builder
* @param {Object} source - Source pagination data
* @param {Object} target - Target pagination object
*/
function buildPage(source, target) {
target.pageNum = source.pageNum
target.pageSize = source.pageSize
target.total = source.total
target.pages = source.pages
copyArray(source.list, target.list)
}
/**
* Clear array contents
* @param {Array} arr - Array to clear
*/
function clearArray(arr) {
if (arr == undefined || arr == null || arr.length == 0) {
return
}
arr.splice(0, arr.length)
}
/**
* Reset object properties to null
* @param {Object} obj - Target object
*/
function clearProps(obj) {
if (obj == undefined || obj == null) {
return
}
for (let i in obj) {
obj[i] = null
}
}
/**
* Copy properties between objects
* @param {Object} source - Source object
* @param {Object} target - Target object
*/
function copyProps(source, target = {}) {
if (target == undefined || target == null) {
target = {}
}
if (source == undefined || source == null) {
source = new Object()
}
for (let i in target) {
target[i] = (source[i] != undefined ? source[i] : null)
}
}
/**
* Clone array contents
* @param {Array} source - Source array
* @param {Array} target - Target array
*/
function copyArray(source, target) {
if (target == undefined || target == null) {
return
}
// Clear the array first
if (target.length > 0) {
target.splice(0, target.length)
/* while (target.length > 0) {
target.pop()
} */
}
if (source == undefined || source == null) {
return
}
for (let i of source) {
target.push(i)
}
}
/**
* Find changed properties between objects
* @param {Object} origin - Original object
* @param {Object} target - Modified object
* @returns {Object} Changed properties
*/
function dfProps(origin, target) {
if (origin == undefined || origin == null || target == undefined || target == null) {
return target
}
var dfObj = {}
for (let i in target) {
if (target[i] != null && target[i] != origin[i]) {
dfObj[i] = target[i]
}
}
return dfObj
}
/**
* Check for property differences
* @param {Object} origin - Original object
* @param {Object} target - Modified object
* @returns {boolean} True if differences exist
*/
function hasDfProps(origin, target) {
const df = dfProps(origin, target)
for (let i in df) {
if (df[i] != null) {
return true
}
}
return false
}
/**
* Check if all object properties are null
* @param {Object} target - Object to check
* @returns {boolean} True if all properties are null
*/
function isAllPropsNull(target) {
if (target == undefined || target == null) {
return true
}
for (let i in target) {
if (target[i] != null) {
return false
}
}
return true
}
function colorByLabel(label) {
if ('ADD' == label) {
return 'bg-success'
}
if ('UPD' == label) {
return 'bg-primary'
}
if ('DEL' == label) {
return 'bg-danger'
}
if ('step' == label) {
return 'bg-primary'
}
if ('log' == label) {
return 'bg-success'
}
if ('tool' == label) {
return 'bg-primary'
}
if ('think' == label) {
return 'bg-danger'
}
if ('run' == label) {
return 'bg-success'
}
if ('message' == label) {
return 'bg-success'
}
if ('act' == label) {
return 'bg-danger'
}
}
function descByLabel(label) {
if ('ADD' == label) {
return 'Add'
}
if ('UPD' == label) {
return 'Update'
}
if ('DEL' == label) {
return 'Delete'
}
return label
}
/**
* Retry calls
* @param {Function} method - Method to call
* @param {any} params - Method parameters that are passed to the method
*/
function retry(method) {
const params = []
for (var i = 1; i < arguments.length; i++) {
params.push(arguments[i])
}
setTimeout(() => {
method(params)
}, 500)
}
/**
* Resolve label from options
* @param {string|number} keyOrVal - Key or value to resolve
* @param {Array} opts - Options array
* @returns {string} Resolved label if found, or original keyOrVal if not found
*/
function resolveLabelFromOpts(keyOrVal, opts) {
if (isEmpty(opts)) {
return keyOrVal
}
for (let opt of opts) {
if (opt.key == keyOrVal || opt.value == keyOrVal) {
return opt.label
}
}
return keyOrVal
}
/**
* Underscored string to camel case string
* @param {String} underscore Underscored string
* @returns Camel case string
*/
function underScoreToCamelCase(underscore) {
if (isNull(underscore) || !underscore.includes('_')) {
return underscore
}
const words = underscore.split('_')
for (let i = 1; i < words.length; i++) {
if (words[i] == "") {
words[i] = ""
continue
}
words[i] = words[i].substring(0, 1).toUpperCase() + words[i].substring(1, words[i].length)
}
return words.join("")
}
/**
* Debounce a function call
* @param {Function} func Function to debounce
* @param {Number} delay Delay in milliseconds
* @returns Debounced function
*/
function debounce(func, delay) {
let timer
return function () {
const context = this
const args = arguments
clearTimeout(timer)
timer = setTimeout(() => {
func.apply(context, args)
}, delay)
}
}
/**
* Convert string to lines
*/
function stringToLines(str) {
if (str == undefined || str == null) {
return []
}
return str.split('\n')
}
export default {
/**
* Synchronous GET HTTP request
*/
get,
/**
* Asynchronous GET HTTP request (async/await)
*/
awaitGet,
/**
* Synchronous POST HTTP request
*/
post,
/**
* Asynchronous POST HTTP request (async/await)
*/
awaitPost,
/**
* Synchronous DELETE HTTP request
*/
del,
/**
* Asynchronous DELETE HTTP request (async/await)
*/
awaitDel,
/**
* Checks if a value is null/undefined
*/
isNull,
/**
* Verifies a value is not null/undefined
*/
notNull,
isBlank,
notBlank,
/**
* Checks if an array is empty
*/
isEmpty,
/**
* Verifies an array contains elements
*/
notEmpty,
isTrue,
isFalse,
getCharCount,
/**
* Displays a toast notification
*/
pop,
/**
* Shows "No data" notification for empty datasets
*/
popNoData,
/**
* Removes null/undefined properties from an object
*/
delNullProperty,
/**
* Gets current datetime as formatted string (YYYY-MM-DD HH:mm:ss)
*/
nowDatetimeStr,
/**
* Constructs pagination parameters
*/
buildPage,
/**
* Clears all elements from an array
*/
clearArray,
/**
* Resets object properties to null/undefined
*/
clearProps,
/**
* Copies properties between objects
*/
copyProps,
/**
* Creates a shallow array copy
*/
copyArray,
/**
* Formats Date object to string (customizable format)
*/
dateFormat,
/**
* Formats Date properties in objects to strings
*/
fomateDateProperty,
/**
* Tracks changed properties between object states
*/
dfProps,
hasDfProps,
isAllPropsNull,
colorByLabel,
descByLabel,
/**
* Retries failed operations with attempts
*/
retry,
resolveLabelFromOpts,
underScoreToCamelCase,
debounce,
stringToLines,
checkPort,
awaitCheckPort,
}
-205
View File
@@ -1,205 +0,0 @@
import utils from '@/assets/js/utils'
/** Regex for English letters, numbers, and underscores */
const codeReg = /^[A-Za-z0-9_\-\.]+$/
/** Regex for mobile phone number in China (Mainland) */
const mobileReg = /^1[3456789]\d{9}$/
/** Regex for ID card number in China (Mainland) */
const idNoReg = /^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/
/** Regex for email */
const emailReg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
const commonValidator = (rule, value, callback) => {
if (utils.isNull(value)) {
callback()
} else {
callback()
}
}
const notBlankValidator = (rule, value, callback) => {
if (utils.isBlank(value)) {
callback(new Error('Input cannot be blank'))
} else {
callback()
}
}
const nameValidator = (rule, value, callback) => {
if (utils.isBlank(value)) {
callback()
} else if (value.length > 50) {
callback(new Error('Name too long (max 50 characters)'))
} else {
callback()
}
}
const mobileValidator = (rule, value, callback) => {
if (utils.isNull(value)) {
callback()
} else if (!mobileReg.test(value)) {
callback(new Error('Invalid mobile number'))
} else {
callback()
}
}
const idNoValidator = (rule, value, callback) => {
if (utils.isNull(value)) {
callback()
} else if (!idNoReg.test(value)) {
callback(new Error('Invalid ID card number'))
} else {
callback()
}
}
const emailValidator = (rule, value, callback) => {
if (utils.isNull(value)) {
callback()
} else if (!emailReg.test(value)) {
callback(new Error('Invalid email address'))
} else {
callback()
}
}
const codeValidator = (rule, value, callback) => {
if (utils.isBlank(value)) {
callback()
} else if (!codeReg.test(value)) {
callback(new Error('Invalid code format'))
callback(new Error('Invalid code format'))
} else {
callback()
}
}
const intValidator = (rule, value, callback) => {
if (utils.isBlank(value)) {
callback()
} else if (!Number.isInteger(value)) {
callback(new Error('Input must be an integer'))
} else {
callback()
}
}
function validator() {
// console.log("arguments:", arguments)
if (arguments.length <= 1) {
const type = arguments[0]
// Default validation logic, no special characters
if (utils.isBlank(type)) {
return commonValidator
} else if (type == 'notBlank') {
return notBlankValidator
} else if (type == 'name') {
return nameValidator
} else if (type == 'mobile') {
return mobileValidator
} else if (type == 'idNo') {
return idNoValidator
} else if (type == 'email') {
return emailValidator
} else if (type == 'code') {
return codeValidator
} else if (type == 'int') {
return intValidator
} else {
return commonValidator
}
}
// Complex validators
const complexValidator = (rule, value, callback) => {
for (let i = 0; i < arguments.length; i++) {
const typeStr = arguments[i]
if (typeStr == 'notBlank' && utils.isBlank(value)) {
callback(new Error('Input cannot be blank'))
break
} else if (typeStr == 'code' && !codeReg.test(value)) {
callback(new Error('Invalid code format'))
break
} else if (typeStr == 'int' && Number.isInteger(value)) {
callback(new Error('Please enter an integer'))
break
}
}
// Ensure callback is called at least once
callback()
}
return complexValidator
}
export default {
username: (username) => {
if (typeof (username) == "undefined" || username == null) {
return "Username cannot be blank"
}
username = username.trim()
if (username.length < 4) {
return "Username must be at least 4 characters long"
}
if (username.length > 20) {
return "Username must be at most 20 characters long"
}
const reg = /^[A-Za-z0-9]+$/
if (!reg.test(username)) {
return "Username must be letters and numbers only"
}
return null
},
password: (password) => {
if (typeof (password) == "undefined" || password == null) {
return "Password cannot be blank"
}
password = password.trim()
if (password.length < 4) {
return "Password must be at least 4 characters long"
}
if (password.length > 20) {
return "Password must be at most 20 characters long"
}
const reg = /^[A-Za-z0-9\.\-\_\+]+$/
if (!reg.test(password)) {
return "Password must be letters, numbers, and special characters (.-_+) only"
}
return null
},
email: (email) => {
if (typeof (email) == "undefined" || email == null) {
return "Email cannot be blank"
}
const reg = /^[A-Za-z0-9._%-]+@([A-Za-z0-9-]+\.)+[A-Za-z]{2,4}$/
if (!reg.test(email)) {
return "Invalid email address"
}
return null
},
validCode: (validCode) => {
if (typeof (validCode) == "undefined" || validCode == null) {
return "Verification code cannot be blank"
}
validCode = validCode.trim()
if (validCode.length != 6) {
return "Verification code must be 6 characters long"
}
const reg = /^[A-Za-z0-9]{6}$/
if (!reg.test(validCode)) {
return "Invalid verification code format"
}
return null
},
validator,
}
@@ -1,8 +0,0 @@
html.dark {
color-scheme: dark;
--el-fg-color: #1d1e1f;
--el-bg-color: #141414;
--el-vd-bg-color: rgb(20, 20, 20, 0.8);
--el-vd-border: var(--el-border-color);
--bg-color-overlay: #1d1e1f;
}
@@ -1,9 +0,0 @@
:root {
color-scheme: light;
--el-fg-color:#ffffff;
--el-bg-color: #f5f5f5;
--el-vd-bg-color: rgb(255, 255, 255, 0.9);
--el-vd-border: #cccccc;
--bg-color-overlay: #f5f5f5;
}
-159
View File
@@ -1,159 +0,0 @@
English | [中文](README_zh.md)
[![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus/stargazers)
&ensp;
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) &ensp;
[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z)
# 👋 OpenManus
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!
It's a simple implementation, so we welcome any suggestions, contributions, and feedback!
Enjoy your own agent with OpenManus!
We're also excited to introduce [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL), an open-source project dedicated to reinforcement learning (RL)- based (such as GRPO) tuning methods for LLM agents, developed collaboratively by researchers from UIUC and OpenManus.
## Project Demo
<video src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" data-canonical-src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
## Installation
We provide two installation methods. Method 2 (using uv) is recommended for faster installation and better dependency management.
### Method 1: Using conda
1. Create a new conda environment:
```bash
conda create -n open_manus python=3.12
conda activate open_manus
```
2. Clone the repository:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
cd OpenManus
```
3. Install dependencies:
```bash
pip install -r requirements.txt
```
### Method 2: Using uv (Recommended)
1. Install uv (A fast Python package installer and resolver):
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
2. Clone the repository:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
cd OpenManus
```
3. Create a new virtual environment and activate it:
```bash
uv venv
source .venv/bin/activate # On Unix/macOS
# Or on Windows:
# .venv\Scripts\activate
```
4. Install dependencies:
```bash
uv pip install -r requirements.txt
```
## Configuration
OpenManus requires configuration for the LLM APIs it uses. Follow these steps to set up your configuration:
1. Create a `config.toml` file in the `config` directory (you can copy from the example):
```bash
cp config/config.example.toml config/config.toml
```
2. Edit `config/config.toml` to add your API keys and customize settings:
```toml
# Global LLM configuration
[llm]
model = "gpt-4o"
base_url = "https://api.openai.com/v1"
api_key = "sk-..." # Replace with your actual API key
max_tokens = 4096
temperature = 0.0
# Optional configuration for specific LLM models
[llm.vision]
model = "gpt-4o"
base_url = "https://api.openai.com/v1"
api_key = "sk-..." # Replace with your actual API key
```
## Quick Start
One line for run OpenManus:
```bash
python main.py
```
Then input your idea via terminal!
For unstable version, you also can run:
```bash
python run_flow.py
```
## How to contribute
We welcome any friendly suggestions and helpful contributions! Just create issues or submit pull requests.
Or contact @mannaandpoem via 📧email: mannaandpoem@gmail.com
## Community Group
Join our networking group on Feishu and share your experience with other developers!
<div align="center" style="display: flex; gap: 20px;">
<img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
</div>
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=mannaandpoem/OpenManus&type=Date)](https://star-history.com/#mannaandpoem/OpenManus&Date)
## Acknowledgement
Thanks to [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
and [browser-use](https://github.com/browser-use/browser-use) for providing basic support for this project!
OpenManus is built by contributors from MetaGPT. Huge thanks to this agent community!
## Cite
```bibtex
@misc{openmanus2025,
author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong},
title = {OpenManus: An open-source framework for building general AI agents},
year = {2025},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/mannaandpoem/OpenManus}},
}
```
-148
View File
@@ -1,148 +0,0 @@
[English](README.md) | 中文
[![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus/stargazers)
&ensp;
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) &ensp;
[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z)
# 👋 OpenManus
Manus 非常棒,但 OpenManus 无需邀请码即可实现任何创意 🛫!
我们的团队成员 [@mannaandpoem](https://github.com/mannaandpoem) [@XiangJinyu](https://github.com/XiangJinyu) [@MoshiQAQ](https://github.com/MoshiQAQ) [@didiforgithub](https://github.com/didiforgithub) https://github.com/stellaHSR 来自 [@MetaGPT](https://github.com/geekan/MetaGPT) 组织,我们在 3
小时内完成了原型开发并持续迭代中!
这是一个简洁的实现方案,欢迎任何建议、贡献和反馈!
用 OpenManus 开启你的智能体之旅吧!
我们也非常高兴地向大家介绍 [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL),这是一个专注于基于强化学习(RL,例如 GRPO)的方法来优化大语言模型(LLM)智能体的开源项目,由来自UIUC 和 OpenManus 的研究人员合作开发。
## 项目演示
<video src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" data-canonical-src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
## 安装指南
我们提供两种安装方式。推荐使用方式二(uv),因为它能提供更快的安装速度和更好的依赖管理。
### 方式一:使用 conda
1. 创建新的 conda 环境:
```bash
conda create -n open_manus python=3.12
conda activate open_manus
```
2. 克隆仓库:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
cd OpenManus
```
3. 安装依赖:
```bash
pip install -r requirements.txt
```
### 方式二:使用 uv(推荐)
1. 安装 uv(一个快速的 Python 包管理器):
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
2. 克隆仓库:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
cd OpenManus
```
3. 创建并激活虚拟环境:
```bash
uv venv
source .venv/bin/activate # Unix/macOS 系统
# Windows 系统使用:
# .venv\Scripts\activate
```
4. 安装依赖:
```bash
uv pip install -r requirements.txt
```
## 配置说明
OpenManus 需要配置使用的 LLM API,请按以下步骤设置:
1. 在 `config` 目录创建 `config.toml` 文件(可从示例复制):
```bash
cp config/config.example.toml config/config.toml
```
2. 编辑 `config/config.toml` 添加 API 密钥和自定义设置:
```toml
# 全局 LLM 配置
[llm]
model = "gpt-4o"
base_url = "https://api.openai.com/v1"
api_key = "sk-..." # 替换为真实 API 密钥
max_tokens = 4096
temperature = 0.0
# 可选特定 LLM 模型配置
[llm.vision]
model = "gpt-4o"
base_url = "https://api.openai.com/v1"
api_key = "sk-..." # 替换为真实 API 密钥
```
## 快速启动
一行命令运行 OpenManus
```bash
python main.py
```
然后通过终端输入你的创意!
如需体验开发中版本,可运行:
```bash
python run_flow.py
```
## 贡献指南
我们欢迎任何友好的建议和有价值的贡献!可以直接创建 issue 或提交 pull request。
或通过 📧 邮件联系 @mannaandpoemmannaandpoem@gmail.com
## 交流群
加入我们的飞书交流群,与其他开发者分享经验!
<div align="center" style="display: flex; gap: 20px;">
<img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
</div>
## Star 数量
[![Star History Chart](https://api.star-history.com/svg?repos=mannaandpoem/OpenManus&type=Date)](https://star-history.com/#mannaandpoem/OpenManus&Date)
## 致谢
特别感谢 [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
和 [browser-use](https://github.com/browser-use/browser-use) 为本项目提供的基础支持!
OpenManus 由 MetaGPT 社区的贡献者共同构建,感谢这个充满活力的智能体开发者社区!
@@ -1,54 +0,0 @@
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import { terser } from 'rollup-plugin-terser'
import AutoImport from 'unplugin-auto-import/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
terser()
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://127.0.0.1:8020',
changeOrigin: true,
rewrite: (path) => path.replace(/\/api/, ''),
}
}
},
build: {
chunkSizeWarningLimit: 1500,
// Split chunks, break large chunks into smaller ones
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
// Let each plugin be packaged into an independent file
return id.toString().split('node_modules/')[1].split('/')[0].toString()
}
// Unit b, merge smaller modules
experimentalMinChunkSize: 10 * 1024
}
}
}
},
})
@@ -1,301 +0,0 @@
<template>
<el-menu class="el-menu-custom" :default-active="activeMenu()" :collapse="menuCollapse" @open="handleOpen"
@close="handleClose" :popper-offset="8">
<el-menu-item index="M02" @click="routeTo('/task')">
<el-icon>
<List />
</el-icon>
<span>{{ getMenuNameByCode('M02') }}</span>
</el-menu-item>
<el-menu-item index="M03" @click="routeTo('/history')">
<el-icon>
<Clock />
</el-icon>
<span>{{ getMenuNameByCode('M03') }}</span>
</el-menu-item>
<el-sub-menu index="M99" v-if="hasMenuPerm('M99')">
<template #title>
<el-icon>
<setting />
</el-icon>
<span>{{ getMenuNameByCode('M99') }}</span>
</template>
<el-menu-item v-if="listSubMenu('M99') != null" v-for="secMenu in listSubMenu('M99')" :index="secMenu.index"
@click="routeTo(secMenu.href)">
{{ getMenuNameByCode(secMenu.index) }}
</el-menu-item>
</el-sub-menu>
</el-menu>
</template>
<script setup>
import { List, Clock, Setting } from '@element-plus/icons-vue'
import { ref, computed, inject, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useConfig } from '@/store/config'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
const utils = inject('utils')
const { t } = useI18n()
const router = useRouter()
const config = useConfig()
const { collapse, resizeCollapse } = storeToRefs(config)
const menuCollapse = computed(() => collapse.value || resizeCollapse.value)
const handleOpen = (key, keyPath) => {
// console.log(key, keyPath)
}
const handleClose = (key, keyPath) => {
// console.log(key, keyPath)
}
// Menu List
const menuList = [
{
index: "M02",
menuName: "menu.task",
href: "/task"
},
{
index: "M03",
menuName: "menu.history",
href: "/history"
},
{
index: "M99",
menuName: "menu.config.settings",
href: null,
subMenuList: [
{
index: "M9901",
menuName: "menu.config.general",
href: "/config/general"
},
{
index: "M9902",
menuName: "menu.config.llm",
href: "/config/llm"
},
]
},
]
onMounted(() => {
// Check menu position after refresh
// activeMenu()
})
function hasMenuPerm(menuCode) {
return menuList.some(menuLv1 => menuLv1.index == menuCode)
}
function listSubMenu(menuCode) {
const matchedMenu = menuList.find(menuLv1 => menuLv1.index == menuCode)
if (matchedMenu != null) {
return matchedMenu.subMenuList
}
return null
}
watch(() => router.currentRoute.value.path, (newValue, oldValue) => {
// console.log('LeftMenu侦听到router.currentRoute.value.path发生更新', newValue, oldValue)
// Check menu position after route change
activeMenu()
})
// Check activated menu position
function activeMenu() {
const currRoute = router.currentRoute
const path = currRoute.value.path
// console.log("currRoute path:", path)
let index = getIndexByPath(path)
// console.log("index:", index)
if (utils.notNull(index)) {
return index
}
// No match, try to find menu for parent path
const lastIndex = path.lastIndexOf('/')
if (lastIndex != -1) {
const newPath = path.substring(0, lastIndex)
// console.log("newPath from parent path:", newPath)
index = getIndexByPath(newPath)
// console.log("index from parent path:", index)
if (utils.notNull(index)) {
return index
}
}
return "1"
}
// Query menu index by path
function getIndexByPath(path) {
for (let fstMenu of menuList) {
// console.log(fstMenu.index, fstMenu.href == path)
if (fstMenu.href == path) {
// console.log("完整1级菜单匹配上")
return fstMenu.index
}
const secMenuList = fstMenu.subMenuList
if (utils.notEmpty(secMenuList)) {
for (let secMenu of secMenuList) {
// console.log(secMenu.index, secMenu.href == path)
if (secMenu.href == path) {
return secMenu.index
}
const thdMenuList = secMenu.subMenuList
if (utils.notEmpty(thdMenuList)) {
for (let thdMenu of thdMenuList) {
if (thdMenu.href == path) {
return thdMenu.index
}
}
// If no third-level menu path matches, use the 'to' from the route configuration to find a match
for (let thdMenu of thdMenuList) {
const nodeList = routeMap.get(path)
if (utils.isEmpty(nodeList)) {
continue
}
for (let node of nodeList) {
if (node.to == thdMenu.href) {
// console.log("A match was found for node.to:", node.to)
return thdMenu.index
}
}
}
}
}
// Iterate through each secondary menu item in the secMenuList
for (let secMenu of secMenuList) {
// console.log(secMenu.index, secMenu.href == path)
const nodeList = routeMap.get(path)
if (utils.isEmpty(nodeList)) {
continue
}
for (let node of nodeList) {
if (node.to == secMenu.href) {
// console.log("匹配上node.to:", node.to)
return secMenu.index
}
}
}
}
// None of the menu items match the path, try to find a match for the 'to' from the route configuration
}
}
// get routes configuration
const routes = router.options.routes
// console.log("routes:", routes)
const routeMap = new Map()
routes.forEach(lv1 => {
// console.log("lv1:", lv1)
buildRoutePer(lv1)
})
// console.log(routeMap)
function buildRoutePer(lv1) {
const lv2List = lv1.children
if (utils.isEmpty(lv2List)) {
const node1 = {
title: lv1.meta.title
}
const nodeList = [node1]
routeMap.set(lv1.path, nodeList)
return
}
lv2List.forEach(lv2 => {
// console.log("lv2:", lv2)
const node1 = {
title: lv1.meta.title
}
const node2 = {
title: lv2.meta.title
}
const nodeList = [node1, node2]
if (utils.notNull(lv2.meta.subTitle)) {
const node3 = {
title: lv2.meta.subTitle,
to: null
}
nodeList.push(node3)
}
routeMap.set(lv1.path + '/' + lv2.path, nodeList)
})
}
function routeTo(href) {
if (href == undefined || href == null || href == '') {
return
}
router.push(href)
}
function getMenuNameByCode(code) {
for (let menu of menuList) {
if (menu.index == code) {
return t(menu.menuName)
}
if (menu.subMenuList != null) {
for (let subMenu of menu.subMenuList) {
if (subMenu.index == code) {
return t(subMenu.menuName)
}
}
}
}
return t(code)
}
</script>
<style scoped>
span {
/* Prevent text selection from double-clicking */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
span {
/* Font size */
font-size: 16px;
}
li {
/* Font size */
font-size: 15px;
}
.el-menu-custom {
border-right: none;
--el-menu-item-height: 40px;
--el-menu-sub-item-height: 36px;
padding-top: 6px;
padding-bottom: 6px;
margin-left: 6px;
margin-right: 6px;
}
/** When the menu is collapsed, redefine the hover menu height */
.el-menu-item {
min-width: 32px;
height: 36px;
line-height: 36px;
}
.el-menu-custom .el-menu--collapse {
width: 32px;
}
.el-menu-custom:not(.el-menu--collapse) {
width: 188px;
}
</style>
@@ -1,251 +0,0 @@
<template>
<el-container class="layout-container">
<el-aside width="collapse" class="layout-aside" :class="shrink ? 'shrink' : ''">
<div :class="menuCollapse ? 'fixed-menu-collapse fxc' : 'fixed-menu-expand fxsb'">
<div v-show="!menuCollapse" class="menu-logo">
<el-link type="primary" @click="refresh" class="pl-14 pr-4">
<img v-if="isDark" src="@/assets/img/logo-w-sm.png" class="fxc" height="26px" alt="logo" />
<img v-if="!isDark" src="@/assets/img/logo-b-sm.png" class="fxc" height="26px" alt="logo" />
</el-link>
</div>
<el-link class="plr-10 w-56" @click="menuToggle">
<el-icon :size="20">
<Fold v-show="!menuCollapse" />
<Expand v-show="menuCollapse" />
</el-icon>
</el-link>
</div>
<el-scrollbar class="scrollbar-menu-wrapper" :class="shrink ? 'shrink' : ''">
<AsideMenu />
</el-scrollbar>
</el-aside>
<el-container>
<el-header>
<TopHeader />
</el-header>
<el-main>
<el-scrollbar style="width: 100%;">
<!-- Router View Container -->
<!-- { Component } = currently matched route component -->
<RouterView v-slot="{ Component }">
<!-- Cached Route Transition: Only keeps alive components with keepAlive meta flag
Transition animation requires single root element in component Key ensures proper re-rendering on route path changes -->
<transition :name="transitionName">
<KeepAlive>
<Component :is="Component" v-if="keepAlive" :key="$route.path" />
</KeepAlive>
</transition>
<!-- Non-cached Route Transition: Fresh instance for other components
Separate transition to prevent animation conflicts -->
<transition :name="transitionName">
<Component :is="Component" v-if="!keepAlive" :key="$route.path" />
</transition>
</RouterView>
</el-scrollbar>
</el-main>
</el-container>
<div class="aside-menu-shade">
</div>
</el-container>
</template>
<script setup>
import TopHeader from '@/components/TopHeader.vue'
import AsideMenu from '@/components/AsideMenu.vue'
import { ref, reactive, computed, watch, onBeforeMount } from 'vue'
import { useRouter, RouterView } from 'vue-router'
import { Expand, Fold } from '@element-plus/icons-vue'
import { showShade, closeShade } from '@/assets/js/shade'
import { useConfig } from '@/store/config'
import { useEventListener } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { useDark } from '@vueuse/core'
const router = useRouter()
const config = useConfig()
const isDark = useDark()
const { shrink, collapse, resizeCollapse } = storeToRefs(config)
const menuCollapse = computed(() => collapse.value || resizeCollapse.value)
const currentRoute = reactive(router.currentRoute)
// Default transition effect, slide to the left
let transitionName = 'slide-left'
const keepAlive = computed(() => {
return currentRoute.value.meta.keepAlive
})
/**
* Set the menu animation duration to 0ms on page refresh to prevent the menu from expanding or collapsing
* with an animation. This ensures that the menu state remains consistent after a page reload.
*/
const menuAnimationDuration = ref(0)
// Function to toggle the menu between expanded and collapsed states
function menuToggle() {
menuAnimationDuration.value = '300ms'
if (menuCollapse.value) {
// console.log("Extend menu")
if (shrink.value) {
// Expend the shade if menu is collapsing
showShade(() => {
// Callback function to close the shade after the menu has collapsed
config.setCollapse(true)
})
}
config.setCollapse(false)
} else {
// If the menu is in an expanded state, close the shade
closeShade()
config.setCollapse(true)
}
resizeCollapse.value = collapse.value
console.log("collapse:", collapse.value, ", menuCollapse:", menuCollapse.value)
}
// Adaptive layout
function onAdaptiveLayout() {
// Get the current window width
const clientWidth = document.body.clientWidth
// Determine if the aside menu should be shrunk based on the window width
if (clientWidth < 800) {
config.setShrink(true)
config.setResizeCollapse(true)
} else {
config.setShrink(false)
config.setResizeCollapse(false)
}
closeShade()
}
watch(() => router.currentRoute.value, (newValue, oldValue) => {
// Toggle the menu when the route changes
onAdaptiveLayout()
})
onBeforeMount(() => {
onAdaptiveLayout()
useEventListener(window, 'resize', onAdaptiveLayout)
})
function refresh() {
// Reload the page
location.reload()
}
</script>
<style scoped>
.layout-container {
height: 100vh;
}
header {
width: 100%;
height: 44px;
padding: 0px;
/* width: calc(100% -32px);
margin-left: 16px;
margin-right: 16px;
border-radius: 6px; */
background-color: var(--el-fg-color);
display: flex;
justify-content: center;
align-items: center;
}
aside {
z-index: 9999999;
}
aside.shrink {
width: 44px;
}
.layout-aside {
margin: 0;
height: 100vh;
overflow: hidden;
transition: width .3s ease;
}
main {
height: calc(100vh - 44px);
width: 100%;
padding: 0px;
overflow: hidden;
background-color: var(--el-bg-color);
}
.menu-logo {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Keyframes for the menu collapse animation */
@keyframes menuCollapse {
0% {
width: 200px;
}
100% {
width: 44px;
}
}
/* Keyframes for the menu expand animation */
@keyframes menuExpand {
0% {
width: 44px;
}
100% {
width: 200px;
}
}
.fixed-menu-collapse {
position: fixed;
z-index: 9999;
height: 44px;
width: 44px;
/* Reference to the keyframes */
animation-name: menuCollapse;
/* Duration of the animation */
animation-duration: v-bind('menuAnimationDuration');
animation-timing-function: ease-in-out;
background-color: var(--el-fg-color);
}
.fixed-menu-expand {
position: fixed;
height: 44px;
width: 200px;
/* Reference to the keyframes */
animation-name: menuExpand;
/* Duration of the animation */
animation-duration: v-bind('menuAnimationDuration');
animation-timing-function: ease-in-out;
background-color: var(--el-fg-color);
/* border-bottom: 1px solid var(--el-bg-color); */
z-index: 9999999;
}
.scrollbar-menu-wrapper {
top: 45px;
height: calc(100vh - 45px);
background-color: var(--el-fg-color);
}
.scrollbar-menu-wrapper.shrink {
position: fixed;
left: 0;
z-index: 9999999;
}
</style>
@@ -1,24 +0,0 @@
<template>
<main>
<div class="container">
<!-- 路由展示区 -->
<RouterView />
</div>
</main>
</template>
<script setup>
import { RouterView } from 'vue-router'
</script>
<style scoped>
main {
width: 100%;
height: 100vh;
display: flex;
justify-content: safe center;
align-items: safe center;
}
</style>
@@ -1,112 +0,0 @@
<template>
<div class="fxsb table-tools">
<div v-show="!advSearch">
<el-button type="default" @click="baseSearch">
<el-icon :size="20">
<Refresh />
</el-icon>
</el-button>
<el-button type="danger" class="ml-10" @click="delSelected" :disabled="selectedRows.length == 0">
<el-icon :size="20" class="pr-4">
<Delete />
</el-icon>
{{ t('delete') }}
</el-button>
</div>
<div v-show="advSearch">
<el-button @click="resetSearch"> {{ t('reset') }}</el-button>
<el-button type="primary" @click="search"> {{ t('search') }}</el-button>
</div>
<div>
<el-input v-model="searchForm.kw" @input="baseSearch" clearable v-show="!advSearch" class="mr-8" />
<el-button-group>
<el-button type="default" @click="advSearchSwitch">
<el-icon :size="20">
<Search />
</el-icon>
</el-button>
<el-button type="default">
<el-dropdown :hide-on-click="false">
<el-icon :size="20">
<Grid />
</el-icon>
<template #dropdown>
<el-dropdown-menu class="dropdown-max">
<el-dropdown-item :command=item.prop v-for="(item, index) in tableColumns" :key="index">
<el-checkbox :label="item.label" :value="item.isShow" :checked="item.isShow"
@change="checkTableColumn($event, item.prop)" />
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-button>
</el-button-group>
</div>
</div>
</template>
<script setup>
import { Refresh, Search, Grid, Plus, Delete } from '@element-plus/icons-vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
advSearch: {
default: false
},
searchForm: {
default: () => ({
kw: null
})
},
tableColumns: {
default: () => []
},
selectedRows: {
default: []
}
})
const emits = defineEmits([
'search',
'baseSearch',
'advSearchSwitch',
'checkTableColumn',
'delSelected',
'resetSearch',
])
const baseSearch = () => {
console.log('baseSearch')
emits('baseSearch')
}
const search = () => {
emits('search')
}
const delSelected = () => {
emits('delSelected')
}
const advSearchSwitch = () => {
emits('advSearchSwitch')
}
const checkTableColumn = (isCheck, prop) => {
console.log('checkTableColumn:', isCheck, prop)
emits('checkTableColumn', isCheck, prop)
}
const resetSearch = () => {
emits('resetSearch')
}
</script>
<style scoped>
.table-tools {
width: 100%;
}
</style>
@@ -1,197 +0,0 @@
<template>
<!-- 导航栏 -->
<div class="nav-bar">
<div class="fxc">
<!-- 左侧固定下拉 -->
<el-dropdown trigger="click" @command="handleSwitchModel" class="fxc plr-16">
<span class="el-dropdown-link">
{{ selectedModel }}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="mod in modelList" :key="mod" :command="mod">
{{ mod }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 刷新 -->
<el-link @click="refresh">
<el-icon :size="20">
<Refresh />
</el-icon>
</el-link>
</div>
<!-- 右侧 -->
<div class="fxc">
<div class="mlr-8">
<el-switch v-model="isDark" :active-action-icon="Moon" :inactive-action-icon="Sunny" width="32"
style="--el-switch-on-color: #4c4d4f; --el-switch-off-color: #f2f2f2;" />
</div>
<!-- 右侧固定下拉 -->
<el-dropdown trigger="click" @command="handleSwitchLang" class="fxc plr-16">
<span class="el-dropdown-link">
{{ selectedLang.name }}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="lang in langList" :key="lang" :command="lang">
{{ lang.name }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, reactive, inject } from 'vue'
import { ArrowDown, Refresh, Moon, Sunny } from '@element-plus/icons-vue'
import { useConfig } from '@/store/config'
/** 暗黑主题切换 */
import { useDark } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
const utils = inject("utils")
const files = inject("files")
const config = useConfig()
const isDark = useDark()
const { t } = useI18n()
const modelList = ref([])
const selectedModel = ref(config.selectedModel)
function handleSwitchModel(mod) {
// console.log("handleSwitchModel:", model)
selectedModel.value = mod
config.setSelectedModel(mod)
}
const langList = ref(config.langList)
const selectedLang = ref(config.selectedLang != null ? config.selectedLang : langList.value[0])
function handleSwitchLang(lang) {
selectedLang.value = lang
config.setSelectedLang(lang)
// i18n.locale = lang.code
location.reload()
}
const llmConfig = reactive({
model: null,
base_url: null,
api_key: null,
max_tokens: null,
temperature: null,
})
function loadLlmConfig() {
const filePath = appDataPath.value + "\\config\\config.toml"
files.readTomlNode(filePath, "llm").then((node) => {
console.log("config/config.toml: ", node)
if (utils.isBlank(node)) {
utils.pop(t('readTomlFailed'))
return
}
utils.copyProps(node, llmConfig)
let model = node.model
if (utils.isBlank(model)) {
model = "No Model"
}
if (model.startsWith("\"")) {
model = model.substring(1)
}
if (model.endsWith("\"")) {
model = model.substring(0, model.length - 1)
}
utils.clearArray(modelList.value)
modelList.value.push(model)
if (selectedModel.value == null) {
config.setSelectedModel(modelList.value[0])
}
})
}
const appDataPath = ref()
onMounted(async () => {
await files.awaitAppPath().then((path) => {
appDataPath.value = path
console.log('appDataPath: ', appDataPath.value)
if (appDataPath.value && appDataPath.value.endsWith('\\desktop\\build\\bin')) {
appDataPath.value = appDataPath.value.replace('\\desktop\\build\\bin', '')
}
console.log('appDataPath: ', appDataPath.value)
})
// 读取配置文件config/config.toml
loadLlmConfig()
})
function refresh() {
location.reload()
}
</script>
<style scoped>
.nav-bar {
display: flex;
height: 44px;
width: 100%;
justify-content: space-between;
}
.el-dropdown-link {
text-align: center;
cursor: pointer;
min-width: 40px;
color: var(--el-color-primary);
display: flex;
align-items: center;
/* 禁止双击选中文字 */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.nav-menu {
height: 100%;
text-align: left;
vertical-align: middle;
display: flex;
justify-content: start;
align-items: center;
}
.nav-menu .item {
width: 50px;
}
.nav-menu .profile {
width: 100px;
}
.nav-menu .profile img {
width: 40px;
height: 30px;
padding: 0 5px;
}
:deep(.el-switch span.el-switch__core) {
width: 32px !important;
min-width: 32px!important;
}
</style>
-279
View File
@@ -1,279 +0,0 @@
<template>
<!-- 预览图片列表 -->
<div class="wp-100" v-if="isImgType" v-for="(file, index) in fileList">
<div class="center img-container" v-show="file.imgUrl != null">
<img :src="file.imgUrl" alt="" class="imgSize max-wp-100" />
<div class="edit" v-show="file.imgUrl != null">
<el-text tag="label">
<el-icon :size="32">
<Edit />
</el-icon>
<input type="file" :accept="accept" @change="files.cache(file, $event)" />
</el-text>
<el-text tag="label">
<el-icon :size="32">
<Delete @click="files.del(fileList, index)" />
</el-icon>
</el-text>
</div>
</div>
<div class="wp-100 mlr-auto mt-0-5" v-if="desc">
<el-input type="textarea" v-model="file.remarkUpd" placeholder="请添加描述" />
</div>
<el-divider v-if="index < fileList.length - 1" />
</div>
<!-- 预览单个图片 -->
<div class="wp-100" v-if="isImgType && file != null && file.imgUrl != null">
<div class="center img-container">
<img :src="file.imgUrl" alt="" class="imgSize max-wp-100" />
<div class="edit">
<el-text tag="label">
<el-icon :size="32">
<Edit />
</el-icon>
<input type="file" :accept="accept" @change="files.cache(file, $event)" />
</el-text>
<el-text tag="label" style="text-align: right;">
<el-icon :size="32">
<Delete @click="files.del(file)" />
</el-icon>
</el-text>
</div>
</div>
<div class="wp-100 mlr-auto mt-0-5" v-if="desc">
<el-input type="textarea" v-model="file.remarkUpd" placeholder="请添加描述" />
</div>
</div>
<!-- 预览文件列表 -->
<div class="file-preview" v-if="isFileType" v-for="(file, index) in fileList">
<el-text type="primary" class="min-w-360" style="text-align: left;">{{ file.fileName }}</el-text>
<el-text tag="label" style="text-align: right;">
<el-icon c:size="16">
<Delete @click="files.del(fileList, index)" />
</el-icon>
</el-text>
</div>
<!-- 预览文件 -->
<div class="file-preview" v-if="isFileType && file != null && file.fileUrl != null">
<el-text type="primary" class="min-w-360" style="text-align: left;">{{ file.fileName }}</el-text>
<el-text tag="label">
<el-icon c:size="16">
<Delete @click="files.del(file)" />
</el-icon>
</el-text>
</div>
<!-- 添加文件列表 -->
<div class="add" v-if="fileList != undefined && fileList != null" :class="isDragover ? 'is-dragover' : ''"
@dragover.prevent="handleDragOver" @dragleave="handleDragLeave($event)"
@drop.prevent="handleDrop(fileList, $event)">
<el-text>点击按钮上传</el-text>
<el-text tag="label">
<el-icon :size="32">
<UploadFilled />
</el-icon>
<input type="file" :accept="accept" @change="files.cache(fileList, $event)" />
</el-text>
<el-text>或将文件拖动到此处</el-text>
</div>
<!-- 添加单个文件 -->
<div class="add" v-if="file != undefined && file != null && file.fileUrl == null && file.imgUrl == null"
:class="addCss, isDragover ? 'is-dragover' : ''" @dragover.prevent="handleDragOver"
@dragleave="handleDragLeave($event)" @drop.prevent="handleDrop(file, $event)">
<el-text>点击按钮上传</el-text>
<el-text tag="label">
<el-icon :size="32">
<UploadFilled />
</el-icon>
<input type="file" :accept="accept" @change="files.cache(file, $event)" />
</el-text>
<el-text>或将文件拖动到此处</el-text>
</div>
</template>
<script setup>
import { ref, computed, onMounted, inject } from 'vue'
import { Edit, Delete, UploadFilled } from '@element-plus/icons-vue'
const utils = inject('utils')
const files = inject('files')
const props = defineProps(['title', 'file', 'fileList', 'accept', 'type', 'desc', 'w', 'h', 'addCss'])
const title = computed(() => {
return utils.notNull(props.title) ? props.title : "选择文件"
})
const accept = computed(() => {
return utils.notNull(props.accept) ? props.accept : "*"
})
const type = computed(() => {
return utils.notNull(props.type) ? props.type : 'file'
})
const desc = computed(() => {
return utils.notNull(props.desc) ? props.desc : false
})
const w = computed(() => {
if (utils.notNull(props.w)) {
return props.w + 'px'
}
return null
})
const h = computed(() => {
if (utils.notNull(props.h)) {
return props.h + 'px'
}
return null
})
const addCss = computed(() => {
return utils.notNull(props.addCss) ? props.addCss : ""
})
const isImgType = computed(() => {
return props.type == 'img'
})
const isFileType = computed(() => {
return props.type == undefined || props.type == null || props.type == 'file'
})
const isDragover = ref(false)
// 改变样式表示可以放置
// event.target.style.backgroundColor = 'lightblue';
function handleDragOver() {
console.log("handleDragOver")
isDragover.value = true
}
function handleDragLeave($event) {
console.log("handleDragLeave")
// 防抖处理
if ($event.currentTarget.contains($event.relatedTarget)) {
isDragover.value = true
} else {
isDragover.value = false
}
}
function handleDrop(fileObj, $event) {
console.log("handleDrop:", fileObj)
isDragover.value = false
files.cache(fileObj, $event)
}
onMounted(() => {
console.log("file,fileList:", props.file, props.fileList)
})
</script>
<style scoped>
input[type=file] {
display: none;
}
.el-text {
margin-left: 10px;
margin-right: 10px;
color: var(--el-text-color);
background-color: none;
text-align: center;
vertical-align: middle;
}
.el-text:hover {
color: var(--el-color-primary);
}
/* 图片hover按钮-start */
.img-container {
position: relative;
display: inline-block;
min-height: 60px;
width: 100%;
/*防止撑开父元素*/
min-width: 0;
border-radius: 6px;
}
.img-container i {
animation: blink 2s infinite;
}
.img-container img {
margin: 0 auto;
display: block;
opacity: 0.7;
transition: 0.5s ease;
}
.img-container div {
min-height: 60px;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
}
div.add {
width: 100%;
min-height: 60px;
margin: 9px 14px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
display: flex;
justify-content: center;
align-items: center;
}
div:hover.add {
border: 1px dashed var(--el-color-primary);
}
.is-dragover {
padding: calc(var(--el-upload-dragger-padding-horizontal) - 1px) calc(var(--el-upload-dragger-padding-vertical) - 1px);
background-color: var(--el-color-primary-light-9);
border: 2px dashed var(--el-color-primary)
}
div.file-preview {
display: flex;
justify-content: space-between !important;
align-items: center;
width: 100%;
border-radius: 6px;
padding: 0px;
margin-top: 9px;
margin-bottom: 9px;
}
div:hover.file-preview {
background-color: var(--el-color-primary-light-9);
}
i {
cursor: pointer;
}
textarea {
height: 30px;
}
.imgSize {
width: v-bind(w);
height: v-bind(h);
}
</style>
-84
View File
@@ -1,84 +0,0 @@
export default {
add: "Add",
edit: "Edit",
delete: "Delete",
search: "Search",
reset: "Reset",
confirm: "Confirm",
cancel: "Cancel",
save: "Save",
submit: "Submit",
export: "Export",
import: "Import",
copy: "Copy",
paste: "Paste",
cut: "Cut",
baseInfo: "Base Info",
createdDt: "Created Date",
updatedDt: "Updated Date",
noData: "No Data",
menu: {
task: "Task",
history: "History",
config: {
settings: "Settings",
general: "General Config",
llm: "LLM Config",
theme: "Theme Config"
}
},
user: "User",
switchModel: "Switch Model",
step: "Step",
promptInputPlaceHolder: "Please Input Task Prompt",
promptInput: "Prompt Input",
promptInputKw: "Prompt Input",
clearCache: "Clear Cache",
clearCacheSuccess: "Clear cache success",
openManusAgiTips: "The above content is generated by OpenManus for reference only",
taskStatus: {
null: "Execution Status",
name: "Task Status",
success: "Success",
failed: "Failed",
running: "Running",
terminated: "Terminated",
},
taskExecFailed: "Task execution failed",
newTask: "New Task",
readConfigSuccess: "Read config success",
readTomlFailed: "Read config failed",
baseConfig: "Base Settings",
serverConfig: "Server Config",
inDevelopment: "In development, not currently supported.",
expandAll: "Expand All",
collapseAll: "Collapse All",
generatedContent: "Generated Content",
welcomeUseOpenManus: "Welcome to use OpenManus",
welcomeUseOpenManusTips: "OpenManus is a powerful AI assistant that can help you with a wide range of tasks. It can generate code, write articles, and answer questions. You can use it to save time and effort. Let's get started!",
getStarted: "Get Started",
docs: "Docs",
toIndex: "To Index",
envConfig: "Env Config",
initConfig: "Init Config",
step1: "Step 1",
step2: "Step 2",
step3: "Step 3",
envLibDownload: "Dependency Libary Download",
checkedPass: "Checked OK",
checkedFailed: "Checked Failed",
llmConfig: "LLM Config",
envLibExists: "Dependency Libary Exists",
envLibNotExists: "Dependency Libary Not Exists, Please Download First.",
envLibDownloadTips: "The 2GB environmental library is downloading.",
envLibDownloadSuccess: "Dependency Libary Download Success",
envLibDownloadFailed: "Dependency Libary Download Failed",
envLibDownloading: "Dependency Libary Downloading",
startTheExperience: "Start the Experience",
startOpenManusService: "Start OpenManus Service, Please Wait...",
initConfigFirst: "Please init config first",
openManusIsStarting: "OpenManus is starting, please wait...",
homePage: "HomePage",
}
-22
View File
@@ -1,22 +0,0 @@
// i18n配置
import { createI18n } from "vue-i18n"
import zhCn from "./zh-cn"
import en from "./en"
const config = localStorage.getItem('config') ? JSON.parse(localStorage.getItem('config')) : {}
// 创建i18n
const i18n = createI18n({
// 语言标识
locale: config.selectedLang ? config.selectedLang.code : 'zhCn',
// 全局注入,可以直接使用$t
globalInjection: true,
// 处理报错: Uncaught (in promise) SyntaxError: Not available in legacy mode (at message-compiler.esm-bundler.js:54:19)
legacy: false,
messages: {
zhCn,
en
}
})
export default i18n
-83
View File
@@ -1,83 +0,0 @@
export default {
add: "新增",
edit: "编辑",
delete: "删除",
search: "搜索",
reset: "重置",
confirm: "确认",
cancel: "取消",
save: "保存",
submit: "提交",
export: "导出",
import: "导入",
copy: "复制",
paste: "粘贴",
cut: "剪切",
baseInfo: "基本信息",
createdDt: "创建时间",
updatedDt: "更新时间",
noData: "暂无数据",
menu: {
task: "任务",
history: "历史记录",
config: {
settings: "设置",
general: "通用设置",
llm: "大模型设置",
theme: "主题设置"
}
},
user: '用户',
step: "步骤",
promptInputPlaceHolder: "请输入任务提示词",
promptInput: "提示词输入",
promptInputKw: "提示词关键字",
clearCache: "清理缓存",
clearCacheSuccess: "清理缓存成功",
openManusAgiTips: "以上内容由OpenManus生成, 仅供参考和借鉴",
taskStatus: {
null: "任务异常",
name: "任务状态",
success: "成功",
failed: "失败",
running: "运行中",
terminated: "终止",
},
taskExecFailed: "任务执行失败",
newTask: "新任务",
readConfigSuccess: "读取配置成功",
readTomlFailed: "读取配置失败",
baseConfig: "基础设置",
serverConfig: "服务器配置",
inDevelopment: "功能开发中,暂不支持",
expandAll: "展开所有",
collapseAll: "收起所有",
generatedContent: "生成内容",
welcomeUseOpenManus: "欢迎使用OpenManus",
welcomeUseOpenManusTips: "OpenManus是一个强大的AI助手, 可以帮助您完成各种任务, 包括生成代码, 撰写文章, 回答问题等。您可以使用它来节省时间和精力。让我们开始吧!",
getStarted: "开始使用",
docs: "使用文档",
toIndex: "返回首页",
envConfig: "环境配置",
initConfig: "初始化配置",
step1: "第一步",
step2: "第二步",
step3: "第三步",
envLibDownload: "环境依赖库下载",
checkedPass: "检测通过",
checkedFailed: "检测失败",
llmConfig: "大模型配置",
envLibExists: "环境依赖库已存在",
envLibNotExists: "环境依赖库不存在, 请先下载。",
envLibDownloadTips: "环境依赖库约2GB, 请耐心等待下载完成。",
envLibDownloadSuccess: "环境依赖库下载成功",
envLibDownloadFailed: "环境依赖库下载失败",
envLibDownloading: "环境依赖库下载中",
startTheExperience: "开始体验",
startOpenManusService: "启动OpenManus服务, 请稍候...",
initConfigFirst: "请先完成初始化配置",
openManusIsStarting: "OpenManus正在启动中, 请稍候...",
homePage: "首页",
}
-54
View File
@@ -1,54 +0,0 @@
import './assets/css/main.css'
import files from '@/assets/js/files'
import utils from '@/assets/js/utils'
import verify from '@/assets/js/verify'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { createApp } from 'vue'
import App from './App.vue'
import i18n from './locales/i18n'
import router from './router'
// import ElementPlus from 'element-plus'
import { ElMessage } from 'element-plus'
import 'element-plus/dist/index.css'
/* Dark theme configuration */
import '@/assets/less/dark.css'
import '@/assets/less/light.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
// Configure Vue production flags
window.__VUE_PROD_DEVTOOLS__ = false
window.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
const app = createApp(App)
app.use(pinia)
// Globally reference router
app.use(router)
app.use(i18n)
// Register Element Plus Message component globally (required for use in utils)
app.use(ElMessage)
// Global configuration of ElementPlus
// ElSelect.props.placeholder.default = '请选择'
// When ElementPlus is imported, a global configuration object can be passed in which contains size and zIndex properties
// size is used to set the default size of form components, and zIndex is used to set the layer level of pop-up components.
// app.use(ElementPlus, { locale, size: 'default', zIndex: 2000 })
// Configure global providers for shared utilities
app.provide('utils', utils)
app.provide('files', files)
app.provide('verify', verify)
/* app.provide('uuid', uuidv4) */
app.mount('#app')
-120
View File
@@ -1,120 +0,0 @@
import { createRouter, createWebHashHistory } from 'vue-router'
const localConfig = localStorage.getItem('config') ? JSON.parse(localStorage.getItem('config')) : {}
const init = localConfig.init? localConfig.init : false
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
component: () => import('@/components/SimpleFrame.vue'),
meta: {
title: "主页"
},
// 重定向到默认页面
redirect: init ? '/task' : '/home',
children: [
{
path: 'home',
component: () => import('@/views/Home.vue'),
meta: {
title: "主页"
}
}
]
},
{
path: '/task',
component: () => import('@/components/MainFrame.vue'),
meta: {
title: "任务"
},
children: [
{
path: '',
component: () => import('@/views/task/TaskIndex.vue'),
meta: {
keepAlive: true,
title: "任务列表",
index: 0
}
},
{
path: ':id',
component: () => import('@/views/task/TaskInfo.vue'),
meta: {
keepAlive: true,
title: "任务信息",
index: 0
}
},
{
path: 'history',
component: () => import('@/views/task/HistoryIndex.vue'),
meta: {
keepAlive: true,
title: "历史记录",
index: 0
}
},
]
},
{
path: '/history',
component: () => import('@/components/MainFrame.vue'),
meta: {
title: "历史记录"
},
children: [
{
path: '',
component: () => import('@/views/task/HistoryIndex.vue'),
meta: {
keepAlive: true,
title: "历史记录",
index: 0
}
},
]
},
{
path: '/config',
component: () => import('@/components/MainFrame.vue'),
meta: {
title: "设置"
},
children: [
{
path: 'general',
component: () => import('@/views/config/General.vue'),
meta: {
keepAlive: false,
title: "常规设置",
index: 0
}
},
{
path: 'llm',
component: () => import('@/views/config/Llm.vue'),
meta: {
keepAlive: false,
title: "大模型配置",
index: 0
}
},
{
path: 'init',
component: () => import('@/views/config/Init.vue'),
meta: {
keepAlive: false,
title: "初始化配置",
index: 0
}
},
]
},
]
})
export default router
-105
View File
@@ -1,105 +0,0 @@
import { defineStore } from "pinia"
export const useConfig = defineStore("config", {
state: () => {
return {
// 全局
isDark: false,
// 侧边栏
// aside是否收缩
shrink: false,
// 菜单是否折叠
collapse: false,
// Resize Collapse
resizeCollapse: false,
selectedModel: null,
selectedLang: { code: 'en', name: 'English' },
langList: [{ code: 'en', name: 'English' }, { code: 'zhCn', name: '中文' }],
taskHistory: [
// taskId, prompt, stepList, status, createdDt
],
init: false,
}
},
actions: {
getShrink() {
return this.shrink
},
setShrink(shrink) {
this.shrink = shrink
},
getIsDark() {
return this.isDark
},
getCollapse() {
return this.collapse
},
setCollapse(collapse) {
this.collapse = collapse
},
getResizeCollapse() {
return this.resizeCollapse
},
setResizeCollapse(resizeCollapse) {
this.resizeCollapse = resizeCollapse
},
getSelectedModel() {
return this.selectedModel
},
setSelectedModel(selectedModel) {
this.selectedModel = selectedModel
},
getSelectedLang() {
return this.selectedLang
},
setSelectedLang(selectedLang) {
this.selectedLang = selectedLang
},
getLangList() {
return this.langList
},
getTaskHistory() {
return this.taskHistory
},
setTaskHistory(taskHistory) {
utils.copyArray(taskHistory, this.taskHistory)
},
addTaskHistory(task) {
// 添加到数组开头
this.taskHistory.unshift(task)
},
// 获取当前, 任务列表中第一个
getCurrTask() {
if (this.taskHistory.length == 0) {
return {}
}
return this.taskHistory[0]
},
getInit() {
return this.init
},
setInit(init) {
this.init = init
}
},
persist: {
key: "config",
}
})
-106
View File
@@ -1,106 +0,0 @@
<template>
<div>
<el-card class="index" body-style="height: 100%;" v-show="view == 'index'">
<div class="fc hp-100">
<div class="fxe mr-10 wp-100">
<el-link type="primary" @click="toDocs" size="large"> {{ t('docs') }} </el-link>
</div>
<div class="body-content">
<div class="welcome">
<el-text tag="h1">{{ t('welcomeUseOpenManus') }}</el-text>
</div>
<div class="fxc">
<img src="@/assets/img/logo-w.png" width="400" alt="OpenManus" v-if="isDark">
<img src="@/assets/img/logo-b.png" width="400" alt="OpenManus" v-if="!isDark">
</div>
<div class="welcome">
<el-text tag="h4" class="text-indent">{{ t('welcomeUseOpenManusTips') }}</el-text>
</div>
<div class="fxc mtb-20">
<el-button type="primary" @click="getStarted" size="large"> {{ t('getStarted') }} </el-button>
</div>
</div>
</div>
</el-card>
<el-card class="mb-0" v-show="view == 'docs'">
<el-scrollbar>
<div class="fxe mr-10">
<el-link type="primary" @click="toIndex" size="large"> {{ t('homePage') }} </el-link>
</div>
<div class="mt-20" v-html="readme"> </div>
</el-scrollbar>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from "vue-router"
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
import { marked } from 'marked'
import { useDark } from '@vueuse/core'
const router = useRouter()
const config = useConfig()
const { t } = useI18n()
const isDark = useDark()
const readme = ref('')
const langList = ref(config.langList)
const selectedLang = ref(config.selectedLang != null ? config.selectedLang : langList.value[0])
const view = ref('index')
function toDocs() {
view.value = 'docs'
}
function toIndex() {
view.value = 'index'
}
onMounted(async () => {
const readmeMd = await import('@/assets/md/README.md?raw')
const readmeZhMd = await import('@/assets/md/README_zh.md?raw')
console.log("readmeZhMd:", readmeZhMd)
readme.value = marked(selectedLang.value == 'zhCn' ? readmeZhMd.default : readmeMd.default)
})
function getStarted() {
router.push({ path: '/config/init' })
}
</script>
<style scoped>
.index {
min-height: 540px;
height: 100vh;
margin-bottom: 0px;
}
.body-content {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.welcome {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>
@@ -1,214 +0,0 @@
<template>
<div class="main-content">
<el-card>
<template #header>
<div class="title fxsb">
<div>{{ t('baseConfig') }}</div>
</div>
</template>
<!-- Show Data -->
<div class="card-row-wrap" v-show="baseShow">
<div class="card-row-aline fxsb">
<el-text>{{ t('clearCache') }}:</el-text>
<el-button type="danger" class="mlr-10" @click="clearCache">{{ t('clearCache') }}</el-button>
</div>
</div>
</el-card>
<el-card>
<template #header>
<div class="title fxsb">
<div> {{ t('serverConfig') }}</div>
<div>
<el-link type="primary" class="no-select plr-6" @click="toEdit('server')" v-show="serverShow">
{{ t('edit') }}
</el-link>
<el-link type="primary" class="no-select plr-6" @click="toShow('server')" v-show="serverEdit">
{{ t('cancel') }}
</el-link>
</div>
</div>
</template>
<!-- No Data -->
<div class="no-data" v-show="serverNoData">{{ t('noData') }}</div>
<!-- Show Data -->
<div class="card-row-wrap" v-show="serverShow">
<div class="card-row-item">
<el-text>host:</el-text>
<el-text>{{ serverConfig.host }}</el-text>
</div>
<div class="card-row-item">
<el-text>port:</el-text>
<el-text tag="p">{{ serverConfig.port }}</el-text>
</div>
</div>
<!-- Edit Module -->
<el-form ref="ruleFormRef" :model="serverConfigUpd" status-icon :rules="rules" v-show="serverEdit">
<div class="card-row-wrap">
<div class="card-row-item">
<el-text>host:</el-text>
<el-form-item prop="host">
<el-input v-model="serverConfigUpd.host" />
</el-form-item>
</div>
<div class="card-row-item">
<el-text>port:</el-text>
<el-form-item prop="port">
<el-input v-model="serverConfigUpd.port" />
</el-form-item>
</div>
<div class="card-row-aline fxc" v-show="serverEdit">
<el-button class="mlr-10" @click="toShow('server')">{{ t('cancel') }}</el-button>
<el-button type="primary" class="mlr-10" @click="submitForm">{{ t('submit') }}</el-button>
</div>
</div>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, inject, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
const utils = inject('utils')
const files = inject('files')
const verify = inject('verify')
const router = useRouter()
const config = useConfig()
const { t } = useI18n()
// 视图模式
const viewModel = reactive({
base: 'show',
server: 'show',
})
function toShow(model) {
console.log("toShow:" + model)
viewModel[model] = 'show'
}
function toEdit(model) {
console.log("toEdit:" + model)
viewModel[model] = 'edit'
}
const baseShow = computed(() => {
return viewModel.base == 'show'
})
const baseEdit = computed(() => {
return viewModel.base == 'edit'
})
const baseNoData = computed(() => {
return baseShow
})
const serverShow = computed(() => {
return viewModel.server == 'show'
})
const serverEdit = computed(() => {
return viewModel.server == 'edit'
})
const readConfigSuccess = ref(true)
const serverNoData = computed(() => {
return serverShow && !readConfigSuccess.value
})
const serverConfig = reactive({
host: null,
port: null,
})
const serverConfigUpd = reactive({
host: null,
port: null,
})
function clearCache() {
config.$reset()
utils.pop(t('clearCacheSuccess'))
}
const appDataPath = ref()
onMounted(async () => {
await files.awaitAppPath().then((path) => {
appDataPath.value = path
console.log('appDataPath: ', appDataPath.value)
if (appDataPath.value && appDataPath.value.endsWith('\\desktop\\build\\bin')) {
appDataPath.value = appDataPath.value.replace('\\desktop\\build\\bin', '')
}
console.log('appDataPath: ', appDataPath.value)
})
// 读取配置文件config/config.toml
loadServerConfig()
})
function loadServerConfig() {
const filePath = appDataPath.value + "\\config\\config.toml"
files.readTomlNode(filePath, "server").then((node) => {
console.log("config/config.toml: ", node)
if (utils.isBlank(node)) {
readConfigSuccess.value = false
utils.pop(t('readTomlFailed'))
return
}
utils.copyProps(node, serverConfig)
utils.copyProps(node, serverConfigUpd)
})
}
function saveServerConfig() {
const filePath = appDataPath.value + "\\config\\config.toml"
files.readAll(filePath)
files.saveTomlNode(filePath, "server", serverConfigUpd).then((resp) => {
console.log("config/config.toml: ", resp)
loadServerConfig()
toShow('server')
})
}
const ruleFormRef = ref()
const rules = reactive({
host: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
port: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
})
const submitForm = async () => {
try {
await ruleFormRef.value.validate();
if (!utils.hasDfProps(serverConfig, serverConfigUpd)) {
ElMessage.success('未发生更改!');
toShow('server')
return
}
ElMessage.success('验证通过,提交表单');
saveServerConfig()
} catch (error) {
console.log('error: ', error);
ElMessage.error('参数验证失败');
}
}
</script>
<style scoped></style>
-526
View File
@@ -1,526 +0,0 @@
<template>
<div class="main-content">
<el-card>
<template #header>
<div class="title fxsb">
<div>{{ t('initConfig') }}</div>
</div>
</template>
<!-- Show Data -->
<el-steps direction="vertical" :active="stepActive">
<el-step title="Step 1" :status="initCheck.envLib ? 'success' : 'wait'">
<template #description>
<el-card class="mtb-10">
<div class="fxsb min-h32">
<div>
<el-text class="pr-10">{{ t('envLibDownload') }}:</el-text>
<el-text>
{{ envLibDownloadLoading ? t('envLibDownloading')
: (initCheck.envLib ? t('checkedPass') : t('checkedFailed')) }}
</el-text>
</div>
<el-button type="primary" class="mlr-10" @click="envLibDownload"
v-show="!initCheck.envLib && !envLibDownloadLoading">
{{ t('envLibDownload') }}
</el-button>
</div>
<div class="wp-100" v-show="envLibDownloadLoading">
<el-progress :percentage="envLibDownloadProgress.percentage" :stroke-width="15"
:status="envLibDownloadProgress.status" striped
:striped-flow="envLibDownloadProgress.status != 'success'" :duration="10" class="mtb-10" />
<div class="wp-100">
<el-text class="download-progress-tips">{{ t('envLibDownloadTips') }}</el-text>
<br />
<el-text class="download-progress-tips" truncated>
{{ envLibDownloadProgress.text }}
</el-text>
</div>
</div>
</el-card>
</template>
</el-step>
<el-step title="Step 2" :status="initCheck.serverConfig ? 'success' : 'wait'">
<template #description>
<el-card class="mtb-10">
<template #header>
<div class="fxsb">
<div>
<el-text class="pr-10">{{ t('serverConfig') }}:</el-text>
<el-text>{{ initCheck.serverConfig ? t('checkedPass') : t('checkedFailed') }}</el-text>
</div>
<el-link type="primary" class="mlr-10" @click="toEdit('server')">{{ t('edit') }}</el-link>
</div>
</template>
<!-- Show Data -->
<div class="card-row-wrap" v-show="serverShow">
<div class="card-row-item">
<el-text>host:</el-text>
<el-text>{{ serverConfig.host }}</el-text>
</div>
<div class="card-row-item">
<el-text>port:</el-text>
<el-text tag="p">{{ serverConfig.port }}</el-text>
</div>
</div>
<!-- Edit Data -->
<el-form ref="ruleFormRef" :model="serverConfigUpd" status-icon :rules="rules" v-show="serverEdit">
<div class="card-row-wrap">
<div class="card-row-item">
<el-text>{{ t('step2') }}</el-text>
<el-form-item prop="host">
<el-input v-model="serverConfigUpd.host" />
</el-form-item>
</div>
<div class="card-row-item">
<el-text>port:</el-text>
<el-form-item prop="port">
<el-input v-model="serverConfigUpd.port" />
</el-form-item>
</div>
<div class="card-row-aline fxc" v-show="serverEdit">
<el-button class="mlr-10" @click="toShow('server')">{{ t('cancel') }}</el-button>
<el-button type="primary" class="mlr-10" @click="submitServerConfig">{{ t('submit') }}</el-button>
</div>
</div>
</el-form>
</el-card>
</template>
</el-step>
<el-step title="Step 3" :status="initCheck.llmConfig ? 'success' : 'wait'">
<template #description>
<el-card class="mtb-10">
<template #header>
<div class="fxsb">
<div>
<el-text class="pr-10">{{ t('llmConfig') }}:</el-text>
<el-text>{{ initCheck.llmConfig ? t('checkedPass') : t('checkedFailed') }}</el-text>
</div>
<div>
<el-link type="primary" class="no-select plr-6" @click="toEdit('llm')" v-show="llmShow">
{{ t('edit') }}
</el-link>
<el-link type="primary" class="no-select plr-6" @click="toShow('llm')" v-show="llmEdit">
{{ t('cancel') }}
</el-link>
</div>
</div>
</template>
<!-- Show Data -->
<div class="card-row-wrap" v-show="llmShow">
<div class="card-row-item">
<el-text>model:</el-text>
<el-text>{{ llmConfig.model }}</el-text>
</div>
<div class="card-row-item">
<el-text>base_url:</el-text>
<el-text tag="p">{{ llmConfig.base_url }}</el-text>
</div>
<div class="card-row-item">
<el-text>api_key:</el-text>
<el-text>{{ llmConfig.api_key }}</el-text>
</div>
<div class="card-row-item">
<el-text>max_tokens:</el-text>
<el-text>{{ llmConfig.max_tokens }}</el-text>
</div>
<div class="card-row-item">
<el-text>temperature:</el-text>
<el-text>{{ llmConfig.temperature }}</el-text>
</div>
</div>
<!-- Edit Module -->
<el-form ref="ruleFormRef" :model="llmConfigUpd" status-icon :rules="rules" v-show="llmEdit">
<div class="card-row-wrap">
<div class="card-row-item">
<el-text>model:</el-text>
<el-form-item prop="model">
<el-input v-model="llmConfigUpd.model" />
</el-form-item>
</div>
<div class="card-row-item">
<el-text>base_url:</el-text>
<el-form-item prop="base_url">
<el-input v-model="llmConfigUpd.base_url" />
</el-form-item>
</div>
<div class="card-row-item">
<el-text>api_key:</el-text>
<el-form-item prop="api_key">
<el-input v-model="llmConfigUpd.api_key" />
</el-form-item>
</div>
<div class="card-row-item">
<el-text>max_tokens:</el-text>
<el-form-item prop="max_tokens">
<el-input v-model="llmConfigUpd.max_tokens" />
</el-form-item>
</div>
<div class="card-row-item">
<el-text>temperature:</el-text>
<el-form-item prop="temperature">
<el-input v-model="llmConfigUpd.temperature" />
</el-form-item>
</div>
<div class="card-row-aline fxc" v-show="llmEdit">
<el-button class="mlr-10" @click="toShow('llm')">{{ t('cancel') }}</el-button>
<el-button type="primary" class="mlr-10" @click="submitLlmConfig">{{ t('submit') }}</el-button>
</div>
</div>
</el-form>
</el-card>
</template>
</el-step>
</el-steps>
</el-card>
<el-card v-show="initCheckPass">
<div class="fxc mtb-20">
<el-button type="primary" @click="startTheExperience" size="large"> {{ t('startTheExperience') }} </el-button>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
import { EventsEmit, EventsOff, EventsOn } from '../../../wailsjs/runtime/runtime'
const utils = inject('utils')
const verify = inject('verify')
const files = inject('files')
const router = useRouter()
const config = useConfig()
const { t } = useI18n()
// 视图模式
const viewModel = reactive({
env: 'show',
server: 'show',
llm: 'show',
})
function toShow(model) {
console.log("toShow:" + model)
viewModel[model] = 'show'
}
function toEdit(model) {
console.log("toEdit:" + model)
viewModel[model] = 'edit'
}
const envShow = computed(() => {
return viewModel.env == 'show'
})
const envEdit = computed(() => {
return viewModel.env == 'edit'
})
const serverShow = computed(() => {
return viewModel.server == 'show'
})
const serverEdit = computed(() => {
return viewModel.server == 'edit'
})
const llmShow = computed(() => {
return viewModel.llm == 'show'
})
const llmEdit = computed(() => {
return viewModel.llm == 'edit'
})
const readConfigSuccess = ref(true)
const serverConfig = reactive({
host: null,
port: null,
})
const serverConfigUpd = reactive({
host: null,
port: null,
})
const llmConfig = reactive({
model: null,
base_url: null,
api_key: null,
max_tokens: null,
temperature: null,
})
const llmConfigUpd = reactive({
model: null,
base_url: null,
api_key: null,
max_tokens: null,
temperature: null,
})
const initCheck = reactive({
envLib: true,
serverConfig: false,
llmConfig: false,
})
const envLibDownloadProgress = reactive({
status: null,
percentage: 0,
text: '',
})
function envLibCheck() {
const envLibPath = appDataPath.value + "\\venv"
files.pathExists(envLibPath).then((exists) => {
console.log('envLibPath exists: ', exists)
if (exists) {
initCheck.envLib = true
return
} else {
initCheck.envLib = false
return
}
})
}
const envLibDownloadLoading = ref(false)
function envLibDownload() {
const runBatPath = appDataPath.value + "\\run.bat"
if (EventsEmit) {
// 触发执行bat命令事件给后台
console.log('EventsEmit: ', EventsEmit)
EventsEmit('bat', 'ExecRunBat', runBatPath)
envLibDownloadLoading.value = true
venvFolderSizeCheck()
}
}
async function venvFolderSizeCheck() {
const targetSize = 1024 * 1024 * 1024 * 2 // 2GB
// 检测venv文件夹大小, 计算百分比
const envLibPath = appDataPath.value + "\\venv"
await files.awaitDirSize(envLibPath).then((size) => {
console.log('envLibPath size: ', size)
const percentage = Math.floor((size / targetSize) * 100)
if (envLibDownloadProgress.percentage < percentage) {
envLibDownloadProgress.percentage = percentage
}
console.log('envLibDownloadProgress.percentage: ', envLibDownloadProgress.percentage)
if (envLibDownloadProgress.percentage >= 100) {
envLibDownloadProgress.percentage = 99
}
})
// 递归检测
if (envLibDownloadLoading.value && envLibDownloadProgress.percentage < 100) {
setTimeout(() => {
venvFolderSizeCheck()
}, 300)
}
}
const appDataPath = ref()
onMounted(async () => {
await files.awaitAppPath().then((path) => {
appDataPath.value = path
console.log('appDataPath: ', appDataPath.value)
if (appDataPath.value && appDataPath.value.endsWith('\\desktop\\build\\bin')) {
appDataPath.value = appDataPath.value.replace('\\desktop\\build\\bin', '')
}
console.log('appDataPath: ', appDataPath.value)
})
// 读取配置文件config/config.toml
loadConfig()
// 监听后台执行bat命令事件
EventsOn('ExecRunBat', (type, data) => {
console.log('bat cmd exc: ', type, data)
envLibDownloadLoading.value = true
if (type == 'msg') {
envLibDownloadProgress.text = data
}
if (data.startsWith('Press any key to continue')
|| (data.endsWith('. . .') && envLibDownloadProgress.percentage >= 99)) {
// data.endsWith('. . .') 兼容中文环境下的bat命令输出
console.log('envLib download finished')
envLibDownloadLoading.value = false
envLibDownloadProgress.percentage = 100
envLibDownloadProgress.status = 'success'
envLibDownloadProgress.text = t('envLibDownloadSuccess')
initCheck.envLib = true
utils.pop(t('envLibDownloadSuccess'))
}
})
// 检查环境
envLibCheck()
})
const stepActive = computed(() => {
if (!initCheck.envLib) {
return 0
}
if (!initCheck.serverConfig) {
return 1
}
if (!initCheck.llmConfig) {
return 2
}
return 0
})
const initCheckPass = computed(() => {
return initCheck.envLib && initCheck.serverConfig && initCheck.llmConfig
})
function startTheExperience() {
config.setInit(true)
router.push({ path: '/task' })
}
onUnmounted(() => {
// 取消监听后台执行bat命令事件
EventsOff('ExecRunBat', null)
})
function loadConfig() {
const filePath = appDataPath.value + "\\config\\config.toml"
files.readTomlNode(filePath, "server").then((node) => {
console.log("config/config.toml: ", node)
if (utils.isBlank(node)) {
readConfigSuccess.value = false
initCheck.serverConfig = false
utils.pop(t('readTomlFailed'))
return
}
utils.copyProps(node, serverConfig)
utils.copyProps(node, serverConfigUpd)
initCheck.serverConfig = true
})
files.readTomlNode(filePath, "llm").then((node) => {
console.log("config/config.toml: ", node)
if (utils.isBlank(node)) {
readConfigSuccess.value = false
initCheck.llmConfig = false
utils.pop(t('readTomlFailed'))
return
}
utils.copyProps(node, llmConfig)
utils.copyProps(node, llmConfigUpd)
initCheck.llmConfig = true
})
}
function saveServerConfig() {
const filePath = appDataPath.value + "\\config\\config.toml"
files.readAll(filePath)
files.saveTomlNode(filePath, "server", serverConfigUpd).then((resp) => {
console.log("config/config.toml: ", resp)
loadConfig()
toShow('server')
})
}
function saveLlmConfig() {
const filePath = appDataPath.value + "\\config\\config.toml"
files.readAll(filePath)
files.saveTomlNode(filePath, "llm", llmConfigUpd).then((resp) => {
console.log("config/config.toml: ", resp)
loadConfig()
toShow('llm')
})
}
const ruleFormRef = ref()
const rules = reactive({
host: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
port: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
model: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
base_url: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
api_key: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
max_tokens: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
temperature: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
})
const submitServerConfig = async () => {
try {
await ruleFormRef.value.validate();
if (!utils.hasDfProps(serverConfig, serverConfigUpd)) {
ElMessage.success('未发生更改!');
toShow('server')
return
}
ElMessage.success('验证通过,提交表单');
saveServerConfig()
} catch (error) {
console.log('error: ', error);
ElMessage.error('参数验证失败');
}
}
const submitLlmConfig = async () => {
try {
await ruleFormRef.value.validate();
if (!utils.hasDfProps(llmConfig, llmConfigUpd)) {
ElMessage.success('未发生更改!');
toShow('llm')
return
}
ElMessage.success('验证通过,提交表单');
saveLlmConfig()
toShow('llm')
} catch (error) {
ElMessage.error('参数验证失败');
}
}
</script>
<style scoped>
:deep(.el-step.is-vertical .el-step__main) {
width: calc(100% - 34px);
}
.download-progress-tips {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
-228
View File
@@ -1,228 +0,0 @@
<template>
<div class="main-content">
<el-card>
<template #header>
<div class="title fxsb">
<div>LLM Config</div>
<div>
<el-link type="primary" class="no-select plr-6" @click="toEdit('base')" v-show="baseShow">
{{ t('edit') }}
</el-link>
<el-link type="primary" class="no-select plr-6" @click="toShow('base')" v-show="baseEdit">
{{ t('cancel') }}
</el-link>
</div>
</div>
</template>
<!-- No Data -->
<div class="no-data" v-show="baseNoData">{{ t('noData') }}</div>
<!-- Show Data -->
<div class="card-row-wrap" v-show="baseShow">
<div class="card-row-item">
<el-text>model:</el-text>
<el-text>{{ llmConfig.model }}</el-text>
</div>
<div class="card-row-item">
<el-text>base_url:</el-text>
<el-text tag="p">{{ llmConfig.base_url }}</el-text>
</div>
<div class="card-row-item">
<el-text>api_key:</el-text>
<el-text>{{ llmConfig.api_key }}</el-text>
</div>
<div class="card-row-item">
<el-text>max_tokens:</el-text>
<el-text>{{ llmConfig.max_tokens }}</el-text>
</div>
<div class="card-row-item">
<el-text>temperature:</el-text>
<el-text>{{ llmConfig.temperature }}</el-text>
</div>
</div>
<!-- Edit Module -->
<el-form ref="ruleFormRef" :model="llmConfigUpd" status-icon :rules="rules" v-show="baseEdit">
<div class="card-row-wrap">
<div class="card-row-item">
<el-text>model:</el-text>
<el-form-item prop="model">
<el-input v-model="llmConfigUpd.model" />
</el-form-item>
</div>
<div class="card-row-item">
<el-text>base_url:</el-text>
<el-form-item prop="base_url">
<el-input v-model="llmConfigUpd.base_url" />
</el-form-item>
</div>
<div class="card-row-item">
<el-text>api_key:</el-text>
<el-form-item prop="api_key">
<el-input v-model="llmConfigUpd.api_key" />
</el-form-item>
</div>
<div class="card-row-item">
<el-text>max_tokens:</el-text>
<el-form-item prop="max_tokens">
<el-input v-model="llmConfigUpd.max_tokens" />
</el-form-item>
</div>
<div class="card-row-item">
<el-text>temperature:</el-text>
<el-form-item prop="temperature">
<el-input v-model="llmConfigUpd.temperature" />
</el-form-item>
</div>
<div class="card-row-aline fxc" v-show="baseEdit">
<el-button class="mlr-10" @click="toShow('base')">{{ t('cancel') }}</el-button>
<el-button type="primary" class="mlr-10" @click="submitForm">{{ t('submit') }}</el-button>
</div>
</div>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, inject, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
const utils = inject('utils')
const files = inject('files')
const verify = inject('verify')
const router = useRouter()
const config = useConfig()
const { t } = useI18n()
// 视图模式
const viewModel = reactive({
base: 'show',
})
function toShow(model) {
console.log("toShow:" + model)
viewModel[model] = 'show'
}
function toEdit(model) {
console.log("toEdit:" + model)
viewModel[model] = 'edit'
}
const baseShow = computed(() => {
return viewModel.base == 'show'
})
const baseEdit = computed(() => {
return viewModel.base == 'edit'
})
const readConfigSuccess = ref(true)
const baseNoData = computed(() => {
return baseShow && !readConfigSuccess.value
})
const llmConfig = reactive({
model: null,
base_url: null,
api_key: null,
max_tokens: null,
temperature: null,
})
const llmConfigUpd = reactive({
model: null,
base_url: null,
api_key: null,
max_tokens: null,
temperature: null,
})
function clearCache() {
config.$reset()
utils.pop(t('clearCacheSuccess'))
}
const appDataPath = ref()
onMounted(async () => {
await files.awaitAppPath().then((path) => {
appDataPath.value = path
console.log('appDataPath: ', appDataPath.value)
if (appDataPath.value && appDataPath.value.endsWith('\\desktop\\build\\bin')) {
appDataPath.value = appDataPath.value.replace('\\desktop\\build\\bin', '')
}
console.log('appDataPath: ', appDataPath.value)
})
// 读取配置文件config/config.toml
loadLlmConfig()
})
function loadLlmConfig() {
const filePath = appDataPath.value + "\\config\\config.toml"
files.readTomlNode(filePath, "llm").then((node) => {
console.log("config/config.toml: ", node)
if (utils.isBlank(node)) {
readConfigSuccess.value = false
utils.pop(t('readTomlFailed'))
return
}
utils.copyProps(node, llmConfig)
utils.copyProps(node, llmConfigUpd)
})
}
function saveLlmConfig() {
const filePath = appDataPath.value + "\\config\\config.toml"
files.readAll(filePath)
files.saveTomlNode(filePath, "llm", llmConfigUpd).then((resp) => {
console.log("config/config.toml: ", resp)
loadLlmConfig()
toShow('llm')
})
}
const ruleFormRef = ref()
const rules = reactive({
model: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
base_url: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
api_key: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
max_tokens: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
temperature: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
})
const submitForm = async () => {
try {
await ruleFormRef.value.validate();
if (!utils.hasDfProps(llmConfig, llmConfigUpd)) {
ElMessage.success('未发生更改!');
toShow('base')
return
}
ElMessage.success('验证通过,提交表单');
saveLlmConfig()
toShow('base')
} catch (error) {
ElMessage.error('参数验证失败');
}
}
</script>
<style scoped></style>
@@ -1,237 +0,0 @@
<template>
<div class="main-content">
<el-card>
<template #header>
<div class="adv-search" :class="advSearch ? 'expand' : ''">
<div class="card-row-wrap">
<div class="card-row-item">
<el-text tag="label">taskId:</el-text>
<el-input v-model="searchForm.taskId" placeholder="taskId" maxlength="50" show-word-limit />
</div>
<div class="card-row-item">
<el-text>{{ t('promptInputKw') }}:</el-text>
<el-input v-model="searchForm.promptInput" :placeholder="t('promptInputKw')" />
</div>
<div class="card-row-item">
<el-text>{{ t('taskStatus.name') }}:</el-text>
<el-select clearable v-model="searchForm.taskStatus">
<el-option v-for="opt in taskStatusOpts" :key="opt.key" :value="opt.value" :label="t(opt.label)" />
</el-select>
</div>
</div>
</div>
<TableTools :advSearch="advSearch" :searchForm="searchForm" :tableColumns="tableColumns"
:selectedRows="selectedRows" @baseSearch="baseSearch" @search="search" @advSearchSwitch="advSearchSwitch"
@delSelected="delSelected" @resetSearch="resetSearch" @checkTableColumn="checkTableColumn" />
</template>
<el-table ref="tableRef" @selection-change="handleSelectionChange" :data="pageInfo.list" stripe border
style="width: 100%" highlight-current-row max-height="760" :cell-style="{ textAlign: 'center' }"
:header-cell-style="{ textAlign: 'center' }">
<el-table-column type="selection" width="55" />
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="taskId" label="TaskId" width="300">
<template #default="scope">
<el-link @click="toTaskInfo(scope.row.taskId)" type="primary" class="h-20">
{{ scope.row.taskId }}
</el-link>
</template>
</el-table-column>
<el-table-column v-for="col in showTableColumns" :prop=col.prop :label="col.label" :width="col.width"
:minWidth="col.minWidth" :showOverflowTooltip="col.showOverflowTooltip" />
</el-table>
<el-pagination v-model:current-page="pageInfo.pageNum" v-model:page-size="pageInfo.pageSize"
:total="pageInfo.total" layout="total, prev, pager, next" />
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, inject, computed, onMounted, onBeforeUnmount, onUnmounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
const router = useRouter()
const utils = inject('utils')
const config = useConfig()
const { t } = useI18n()
const tableRef = ref()
const selectedRows = ref([])
// 高级搜索
const advSearch = ref(false)
function advSearchSwitch() {
advSearch.value = !advSearch.value
}
const pageInfo = reactive({
pageNum: 1,
pageSize: 10,
total: 0,
pages: 1,
list: []
})
watch(() => pageInfo.pageNum, () => {
// 触发列表查询
search()
})
watch(() => pageInfo.pageSize, () => {
// 触发列表查询
search()
})
const searchForm = reactive({
kw: null,
taskId: null,
promptInput: null,
taskStatus: null
})
const taskStatusOpts = reactive([
{ key: "success", value: "success", label: "taskStatus.success" },
{ key: "failed", value: "failed", label: "taskStatus.failed" },
{ key: "running", value: "running", label: "taskStatus.running" },
{ key: "terminated", value: "terminated", label: "taskStatus.terminated" }
])
const tableColumns = ref([
{ prop: "prompt", label: t('promptInput'), isShow: true, showOverflowTooltip: true, minWidth: 300, sortable: true },
{ prop: "statusDesc", label: t('taskStatus.name'), isShow: true, width: 160 },
{ prop: "createdDt", label: t('createdDt'), isShow: true, width: 160 }
])
const showTableColumns = computed(() => {
return tableColumns.value.filter(item => item.isShow)
})
// 任务历史
const taskHistory = computed(() => {
return config.taskHistory
})
// 基本搜索
const baseSearch = utils.debounce(() => {
const kw = searchForm.kw
utils.clearProps(searchForm)
searchForm.kw = kw
search()
}, 500)
// 搜索
// 修改search方法
function search() {
searchForm.pageNum = pageInfo.pageNum
searchForm.pageSize = pageInfo.pageSize
console.log("search searchForm:", searchForm, pageInfo)
const filteredTaskList = taskHistory.value.filter(taskInfo => {
if (utils.notBlank(searchForm.kw)) {
if (!taskInfo.prompt.includes(searchForm.kw) && !taskInfo.taskId.includes(searchForm.kw)) {
return false
}
return true
}
if (utils.notBlank(searchForm.taskId) && taskInfo.taskId != searchForm.taskId) {
return false
}
if (utils.notBlank(searchForm.promptInput) && !taskInfo.prompt.includes(searchForm.promptInput)) {
return false
}
if (utils.notBlank(searchForm.taskStatus) && taskInfo.status != searchForm.taskStatus) {
return false
}
return true
})
// 计算总条数
pageInfo.total = filteredTaskList.length
// 分页处理
const startIndex = (pageInfo.pageNum - 1) * pageInfo.pageSize
const endIndex = startIndex + pageInfo.pageSize
pageInfo.list = filteredTaskList.slice(startIndex, endIndex)
// 任务状态处理
pageInfo.list.forEach(item => {
item.statusDesc = t('taskStatus.' + item.status)
})
}
const handleSelectionChange = (val) => {
selectedRows.value = val
}
// 删除选中的数据
function delSelected() {
if (selectedRows.value.length == 0) {
utils.pop("请选择要删除的数据!")
return
}
selectedRows.value.forEach(item => {
for (let i = 0; i < taskHistory.value.length; i++) {
if (taskHistory.value[i].taskId == item.taskId) {
taskHistory.value.splice(i, 1)
i--
}
}
})
baseSearch()
}
// 定义变量存储事件监听的引用(不能是常量)
let listener = null
// 在组件挂载时添加事件监听
onMounted(() => {
listener = (event) => {
if (event.key === 'Enter') {
search()
}
}
window.addEventListener('keyup', listener)
console.log("onMounted pageInfo:", pageInfo)
search()
})
// 在组件卸载前移除事件监听
onBeforeUnmount(() => {
window.removeEventListener('keyup', listener)
})
function checkTableColumn(isCheck, prop) {
console.log("checkTableColumn:", isCheck, prop)
tableColumns.value.forEach(item => {
if (item.prop == prop) {
item.isShow = isCheck
}
})
}
function resetSearch() {
utils.clearProps(searchForm)
searchForm.openStatus = "OPEN"
}
function toTaskInfo(taskId) {
console.log("toTaskInfo:", taskId)
router.push("/task/"+taskId)
}
</script>
<style scoped></style>
@@ -1,697 +0,0 @@
<template>
<div class="main-content-full-height">
<el-scrollbar ref="scrollRef" class="scroll-wrap">
<div class="dialog-area" v-show="taskInfo.taskId != null">
<div class="dialog-user">
<div class="blank"></div>
<div class="content">
<div class="title fxc">
<img src="@/assets/img/user.png" class="user-img" />
<el-text>
{{ t('user') }}
</el-text>
</div>
<el-text class="prompt">
{{ taskInfo.prompt }}
</el-text>
</div>
</div>
<div class="dialog-ai">
<div class="fxsb">
<el-text class="title"> OpenManus-AI </el-text>
<div class="plr-10">
<el-link type="primary" class="no-select plr-6" @click="expandAll" v-show="notAllExpanded">
{{ t('expandAll') }}
</el-link>
<el-link type="primary" class="no-select plr-6" @click="collapseAll" v-show="notAllCollapsed">
{{ t('collapseAll') }}
</el-link>
</div>
</div>
<div class="card-row-wrap">
<div class="card-row-aline">
<el-collapse v-model="activeNames" @change="handleChange" class="wp-100">
<el-collapse-item v-for="(step, index) in taskInfo.stepList" :key="index" :name="step.stepNo"
:icon="ArrowRight">
<template #title>
<div class="fxsb wp-100">
<div>
<el-text class="collapse-color-label mr-10" :class="utils.colorByLabel('step')">
{{ t('step') }}
</el-text>
<el-text class="pl-10">{{ step.result }}</el-text>
</div>
<el-text class="tips-text plr-10">{{ step.createdDt }}</el-text>
</div>
</template>
<div v-for="(subStep, subIndex) in step.subList">
<div class="fxsb mtb-10">
<el-text> {{ subStep.type }} </el-text>
<el-text class="sub-step-time"> {{ subStep.createdDt }} </el-text>
</div>
<div>
<el-text> {{ subStep.result }} </el-text>
</div>
<el-divider v-if="subIndex != step.subList.length - 1" />
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</div>
</div>
</el-scrollbar>
<div class="ctrl-area">
<!-- 暂时隐藏该区域 v-show="!newTaskFlag" -->
<div class="task-area wp-100" v-show="false">
<div class="generated fxc">
<div class="generated-label">{{ t('generatedContent') }}</div>
<div class="generated-folder">You Can Check Generated Files Here, This Function Is In Developing.</div>
</div>
</div>
<div class="input-area">
<div class="progress-area w-80">
<div class="progress-wrap">
<el-progress type="dashboard" class="mt-10" :percentage="taskInfo.percentage"
:status="taskInfo.progressStatus" :stroke-width="6" :width="60" v-show="taskInfo.status != null">
<template #default="{ percentage }">
<span class="percentage-value">{{ percentage }}%</span>
</template>
</el-progress>
<el-text class="progress-text">{{ taskInfo.status }}</el-text>
</div>
</div>
<div class="input-box">
<el-icon @click="uploadFile" class="add-file-area" :size="24">
<FolderAdd />
</el-icon>
<el-input ref="promptEle" type="textarea" v-model="prompt" class="input-style" style="border: none;"
:autosize="{ minRows: 1, maxRows: 4 }" autofocus :placeholder="t('promptInputPlaceHolder')"
@keydown.enter="handleInputEnter" />
<el-link class="send-area">
<el-icon @click="sendPrompt" :size="24" v-show="!loading && taskInfo.status != 'running'">
<Promotion />
</el-icon>
<el-icon @click="stop" :size="24" v-show="loading || taskInfo.status == 'running'">
<CircleClose />
</el-icon>
</el-link>
</div>
</div>
<div class="tips">
<el-text class="tips-text">{{ t('openManusAgiTips') }}</el-text>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, watch, inject, computed, onMounted, onUnmounted } from 'vue'
import { FolderAdd, Promotion, CircleClose, ArrowRight } from '@element-plus/icons-vue'
import { EventsEmit, EventsOff, EventsOn } from '../../../wailsjs/runtime/runtime'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
const utils = inject('utils')
const files = inject('files')
const config = useConfig()
const { t } = useI18n()
const prompt = ref('')
const promptEle = ref(null)
const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message']
const eventSource = ref(null)
const newTaskFlag = ref(true)
const taskInfo = computed(() => {
if (newTaskFlag.value) {
return {}
}
return config.getCurrTask()
})
const serverConfig = ({
host: null,
port: null,
})
const loading = ref(false)
const scrollRef = ref(null)
const activeNames = ref([])
const notAllExpanded = computed(() => {
return taskInfo.value.stepList != null && activeNames.value.length != taskInfo.value.stepList.length
})
const notAllCollapsed = computed(() => {
return activeNames.value.length != 0
})
const handleChange = (val) => {
console.log("handleChange:", val)
}
// Expand task log when first loading and auto collapse after 10s.
function autoExpandCollapse(stepNo) {
scrollToBottom()
if (activeNames.value.includes(stepNo)) {
return
}
// console.log("autoExpandCollapse:", stepNo)
const hasSubStepList = taskInfo.value.stepList.some(step => {
// console.log("stepNo:", step.stepNo, "subList:", JSON.stringify(step.subList))
return step.stepNo == stepNo && step.subList != null && step.subList.length > 0
})
console.log("stepNo", stepNo, "hasSubStepList:", hasSubStepList)
if (!hasSubStepList) {
return
}
activeNames.value.push(stepNo)
setTimeout(() => {
scrollToBottom()
}, 500)
}
// Expand/Collapse all
function expandAll() {
taskInfo.value.stepList.filter(item => {
if (activeNames.value.includes(item.stepNo)) {
return
}
activeNames.value.push(item.stepNo)
setTimeout(() => {
scrollToBottom()
}, 500)
})
}
watch(activeNames.value, (newVal, oldVal) => {
console.log("activeNames changed:", newVal, oldVal)
})
function collapseAll() {
utils.clearArray(activeNames.value)
}
// 建立EventSource连接
const buildEventSource = (taskId) => {
loading.value = true
eventSource.value = new EventSource(remoteBaseUrl.value + '/tasks/' + taskId + '/events')
eventSource.value.onmessage = (event) => {
console.log('Received data:', event.data)
// 在这里处理接收到的数据 不起作用 可能是服务端接口有问题
}
eventTypes.forEach(type => {
eventSource.value.addEventListener(type, (event) => handleEvent(event, type))
})
/* eventSource.value.onerror = (error) => {
console.error('EventSource failed:', error)
// 处理错误情况 可能是服务端接口有问题
// loading.value = false
// eventSource.value.close()
// taskInfo.value.status = "failed"
// taskInfo.value.progressStatus = "exception"
// utils.pop(t('taskExecFailed'), "error")
} */
}
const handleEvent = (event, type) => {
// console.log('Received event, type:', type, event.data)
// clearInterval(heartbeatTimer)
try {
const data = JSON.parse(event.data)
// console.log("type:", type, "data:", data)
if (eventSource.value.readyState === EventSource.CLOSED) {
// console.log('Connection is closed')
}
if (type == "complete" || data.status == "completed") {
// console.log('task completed')
loading.value = false
taskInfo.value.status = "success"
taskInfo.value.progressStatus = "success"
utils.pop("任务已完成", "success")
return
}
// autoScroll(stepContainer)
buildOutput(taskInfo.value.taskId)
} catch (e) {
console.error(`Error handling ${type} event:`, e)
}
}
async function buildOutput(taskId) {
// 同步执行,确保数据顺序
await utils.awaitGet(remoteBaseUrl.value + '/tasks/' + taskId).then(data => {
// console.log("task info resp:", data)
buildStepList(data.steps)
// console.log("stepList:", taskInfo.value.stepList)
})
}
// 封装stepList
const buildStepList = (steps) => {
// stepList
steps.forEach((step, idx) => {
// 步骤
if (step.type == "log" && step.result.startsWith("Executing step")) {
const stepStr = step.result.replace("Executing step ", "").replace("\n", "")
const stepNo = stepStr.split("/")[0]
const stepCount = stepStr.split("/")[1]
if (taskInfo.value.stepList.length < stepNo) {
// 添加此step到stepList
const parentStep = {
type: "log",
idx: idx,
stepNo: stepNo,
result: stepStr,
subList: [],
createdDt: utils.dateFormat(new Date())
}
taskInfo.value.stepList.push(parentStep)
taskInfo.value.percentage = Math.floor((stepNo / stepCount) * 100)
return
}
scrollToBottom()
} else {
// 子步骤
const subStep = {
type: step.type,
idx: idx,
result: step.result,
createdDt: utils.dateFormat(new Date())
}
// 判定添加到stepList中的哪个元素元素的subList中
// console.log("stepList:", taskInfo.value.stepList, "idx:", idx)
let parentStep = null
const pStepIndex = taskInfo.value.stepList.findIndex(parentStep => parentStep.idx > idx)
// console.log("pStepIndex:", pStepIndex)
if (pStepIndex != -1) {
// 取pStep的上一个元素
parentStep = taskInfo.value.stepList[pStepIndex - 1]
} else {
// 不存在时, 添加到stepList最后一个元素末尾
parentStep = taskInfo.value.stepList[taskInfo.value.stepList.length - 1]
}
// console.log("parentStep:", parentStep)
const existSubStep = parentStep.subList.find(existSubStep => existSubStep.idx == idx)
if (!existSubStep) {
// 不存在时, 添加到末尾
parentStep.subList.push(subStep)
}
autoExpandCollapse(taskInfo.value.stepList[taskInfo.value.stepList.length - 1].stepNo)
}
scrollToBottom()
})
scrollToBottom()
}
const appDataPath = ref()
onMounted(async () => {
await files.awaitAppPath().then((path) => {
appDataPath.value = path
console.log('appDataPath: ', appDataPath.value)
if (appDataPath.value && appDataPath.value.endsWith('\\desktop\\build\\bin')) {
appDataPath.value = appDataPath.value.replace('\\desktop\\build\\bin', '')
}
console.log('appDataPath: ', appDataPath.value)
})
// 读取配置文件config/config.toml
loadServerConfig()
if (taskInfo.value.status == "running") {
taskInfo.value.status = null
}
startNewTask()
})
async function checkStartOpenManusService() {
// 检查服务是否启动, 未启动则启动
// windows查询占用端口的应用 netstat -ano | findstr 5172
// tasklist | findstr [pid] 12345
// taskkill -f -pid 12345
let isServiceRunning = false
await utils.awaitCheckPort(serverConfig.port).then((portInUse) => {
isServiceRunning = portInUse
console.log("isServiceRunning:", isServiceRunning)
})
if (!isServiceRunning) {
utils.pop(t('openManusIsStarting'))
EventsEmit('pyFile', 'ExecAppPy', appDataPath.value + '\\app.py')
}
}
function loadServerConfig() {
const filePath = appDataPath.value + "\\config\\config.toml"
files.readTomlNode(filePath, "server").then((node) => {
console.log("config/config.toml: ", node)
if (utils.isBlank(node)) {
utils.pop(t('readTomlFailed'))
return
}
utils.copyProps(node, serverConfig)
checkStartOpenManusService()
})
}
onUnmounted(() => {
// 组件卸载时关闭EventSource连接
if (eventSource.value) {
eventSource.value.close()
}
})
function handleInputEnter(event) {
// console.log("handleInputEnter:", event)
event.preventDefault()
sendPrompt()
}
function uploadFile() {
utils.pop(t('inDevelopment'), "warning")
}
const scrollToBottom = () => {
if (scrollRef.value) {
// console.log("scrollRef:", scrollRef.value, scrollRef.value.wrapRef)
const container = scrollRef.value.wrapRef
if (container) {
container.scrollTop = container.scrollHeight
}
}
}
// 发送提示词
function sendPrompt() {
if (utils.isBlank(prompt.value)) {
utils.pop("Please enter a valid prompt", "error")
promptEle.value.focus()
return
}
if (taskInfo.value.status == "running") {
utils.pop("请先终止当前任务", "error")
return
}
// 关闭之前的连接
if (eventSource.value != null) {
eventSource.value.close()
}
utils.post(remoteBaseUrl.value + '/tasks', { prompt: prompt.value }).then(data => {
if (!data.task_id) {
throw new Error('Invalid task ID')
}
const newTask = {
taskId: data.task_id,
prompt: prompt.value,
status: "running",
createdDt: utils.dateFormat(new Date()),
stepList: []
}
// 保存历史记录
config.addTaskHistory(newTask)
newTaskFlag.value = false
// 发送完成后清空输入框
prompt.value = ''
// 建立新的EventSource连接
buildEventSource(data.task_id)
// console.log("new task created:", newTask)
}).catch(error => {
console.error('Failed to create task:', error)
})
}
function stop() {
// console.log("stop")
loading.value = false
// console.log("eventSource:", eventSource.value, "taskInfo:", taskInfo.value)
if (eventSource.value != null) {
eventSource.value.close()
}
taskInfo.value.status = "terminated"
taskInfo.value.progressStatus = "exception"
utils.pop("用户终止任务", "error")
}
function startNewTask() {
// console.log("startNewTask:", taskInfo.value)
if (taskInfo.value.status == "running") {
utils.pop("请先终止当前任务", "error")
return
}
newTaskFlag.value = true
prompt.value = ''
utils.clearArray(activeNames.value)
}
const remoteBaseUrl = computed(() => {
let url
if (utils.notBlank(serverConfig.host)) {
url = serverConfig.host
if (url.startsWith("\"")) {
url = url.substring(1)
}
if (url.endsWith("\"")) {
url = url.substring(0, url.length - 1)
}
if (utils.notBlank(serverConfig.port)) {
url = url + ":" + serverConfig.port
}
if (!url.startsWith("http")) {
url = "http://" + url
}
} else {
// default
url = "http://localhost:5172"
}
return url
})
</script>
<style scoped>
.scroll-wrap {
width: 100%;
}
.dialog-area {
flex-grow: 1;
}
.dialog-user {
display: flex;
justify-content: center;
align-items: space-between;
margin-bottom: 16px;
}
.dialog-user .blank {
flex-grow: 1;
}
.dialog-user .content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: end;
border-radius: 12px;
background-color: var(--el-fg-color);
}
.dialog-user .title {
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
align-self: flex-end;
margin: 6px 16px;
font-size: 15px;
}
.dialog-user .prompt {
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
align-self: flex-end;
margin: 0px 16px 6px 16px;
}
.dialog-user .user-img {
width: 20px;
height: 20px;
margin-right: 2px;
margin-bottom: 4px;
}
.dialog {
width: 100%;
}
.dialog-ai {
background-color: var(--el-fg-color);
border-radius: 12px;
}
.dialog-ai .title {
margin: 6px 12px;
font-size: 15px;
}
.ctrl-area {
flex-grow: 0;
width: 100%;
max-height: 200px;
padding-top: 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.ctrl-area .task-area {
width: 100%;
padding-left: 10px;
padding-right: 7px;
margin-bottom: 12px;
background-color: var(--el-fg-color);
border-radius: 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.percentage-value {
display: block;
font-size: 16px;
}
.generated {
flex-grow: 1;
width: 100%;
height: 68px;
padding-left: 16px;
}
.generated-label {
width: 80px;
text-align: right;
}
.generated-folder {
width: 100%;
min-height: 54px;
margin-left: 16px;
background-color: var(--el-bg-color);
border-radius: 6px;
}
.input-area {
width: 100%;
padding-right: 80px;
border-radius: 16px;
display: flex;
justify-content: center;
align-items: end;
}
.progress-area {
align-self: center;
flex-grow: 0;
/* margin-left: -6px;
margin-right: 10px; */
}
.progress-wrap {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 80px;
height: 54px;
margin-left: -10px;
}
.progress-text {
margin-top: -10px;
width: 70px;
text-align: center;
}
.input-box {
flex-grow: 1;
width: 100%;
border-radius: 16px;
background-color: var(--el-fg-color);
display: flex;
justify-content: center;
align-items: center;
}
.input-style {
width: 100%;
padding-top: 12px;
padding-bottom: 12px;
}
.input-style :deep(.el-textarea__inner) {
outline: none;
border: none;
resize: none;
box-shadow: none;
}
.add-file-area {
margin-left: 16px;
margin-right: 8px;
}
.send-area {
margin-left: 8px;
margin-right: 16px;
}
.tips {
padding-top: 8px;
}
.tips-text {
color: var(--el-text-color-secondary);
font-size: 12px;
}
.sub-step-time {
color: var(--el-text-color-secondary);
font-size: 12px;
}
.icon-ele {
margin: 0 8px 0 auto;
color: #409eff;
}
.collapse-color-label {
margin-top: 10px;
margin-bottom: 10px;
padding-left: 10px;
padding-right: 10px;
height: 24px;
line-height: 24px;
border-radius: 6px;
}
</style>
@@ -1,167 +0,0 @@
<template>
<div class="main-content-full-height">
<!-- 展示模块-暂无数据 -->
<div class="no-data" v-show="baseNoData">{{ t('noData') }}</div>
<!-- 展示模块 -->
<div class="output-area" v-show="baseShow">
<div class="dialog-user">
<div class="blank"></div>
<div class="content">
<div class="title fxc">
<img src="@/assets/img/user.png" class="user-img" />
<el-text>
{{ t('user') }}
</el-text>
</div>
<el-text class="prompt">
{{ taskInfo.prompt }}
</el-text>
</div>
</div>
<div class="dialog-ai">
<el-text class="title"> OpenManus-AI </el-text>
<div class="card-row-wrap">
<div class="card-row-aline">
<el-timeline class="wp-100">
<el-timeline-item v-for="(step, index) in taskInfo.stepList" :key="index" :timestamp="step.createdDt"
placement="top">
<el-card>
<div>
<h4 class="color-label mr-10" :class="utils.colorByLabel('step')">
{{ t('step') }}
</h4>
<el-text>{{ step.result }}</el-text>
</div>
<el-divider />
<div v-for="(subStep, subIndex) in step.subList">
<div class="fxsb mtb-10">
<el-text> {{ subStep.type }} </el-text>
<el-text class="sub-step-time"> {{ subStep.createdDt }} </el-text>
</div>
<div>
<el-text> {{ subStep.result }} </el-text>
</div>
<el-divider v-if="subIndex != step.subList.length - 1" />
</div>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</div>
</div>
<div class="task-status" v-show="taskInfo != null">
<el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text>
<el-text>{{ taskInfo.status }}</el-text>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
import { User } from '@element-plus/icons-vue'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
const utils = inject('utils')
const config = useConfig()
const { t } = useI18n()
// 视图模式
const viewModel = reactive({
base: 'show'
})
const baseShow = computed(() => {
return viewModel.base == 'show'
})
const baseNoData = computed(() => {
return baseShow && taskInfo.value == null
})
const taskInfo = computed(() => {
return config.getCurrTask()
})
</script>
<style scoped>
.output-area {
flex-grow: 1;
}
.dialog-user {
display: flex;
justify-content: center;
align-items: space-between;
margin-bottom: 16px;
}
.dialog-user .blank {
flex-grow: 1;
}
.dialog-user .content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: end;
border-radius: 12px;
background-color: var(--el-fg-color);
}
.dialog-user .title {
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
align-self: flex-end;
margin: 6px 16px;
font-size: 15px;
}
.dialog-user .prompt {
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
align-self: flex-end;
margin: 0px 16px 6px 16px;
}
.dialog-user .user-img {
width: 20px;
height: 20px;
margin-right: 2px;
margin-bottom: 4px;
}
.dialog {
width: 100%;
}
.dialog-ai {
background-color: var(--el-fg-color);
border-radius: 12px;
}
.dialog-ai .title {
margin: 6px 12px;
font-size: 15px;
}
.task-status {
align-self: self-start;
padding-top: 12px;
padding-bottom: 16px;
}
.sub-step-time {
color: var(--el-text-color-secondary);
font-size: 12px;
}
</style>
-54
View File
@@ -1,54 +0,0 @@
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import { terser } from 'rollup-plugin-terser'
import AutoImport from 'unplugin-auto-import/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
terser()
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://127.0.0.1:8020',
changeOrigin: true,
rewrite: (path) => path.replace(/\/api/, ''),
}
}
},
build: {
chunkSizeWarningLimit: 1500,
// Fine-tune bundling strategy
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
// Extract package name from module path to create separate chunks
return id.toString().split('node_modules/')[1].split('/')[0].toString()
}
},
// Attempt to merge chunks smaller than 10KB (in bytes)
experimentalMinChunkSize: 10 * 1024,
}
},
}
})
-16
View File
@@ -1,16 +0,0 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function AppPath():Promise<string>;
export function CheckPort(arg1:string):Promise<boolean>;
export function DirSize(arg1:string):Promise<number>;
export function Greet(arg1:string):Promise<string>;
export function PathExists(arg1:string):Promise<boolean>;
export function ReadAll(arg1:string):Promise<string>;
export function SaveFile(arg1:string,arg2:string):Promise<void>;
-31
View File
@@ -1,31 +0,0 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function AppPath() {
return window['go']['main']['App']['AppPath']();
}
export function CheckPort(arg1) {
return window['go']['main']['App']['CheckPort'](arg1);
}
export function DirSize(arg1) {
return window['go']['main']['App']['DirSize'](arg1);
}
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
export function PathExists(arg1) {
return window['go']['main']['App']['PathExists'](arg1);
}
export function ReadAll(arg1) {
return window['go']['main']['App']['ReadAll'](arg1);
}
export function SaveFile(arg1, arg2) {
return window['go']['main']['App']['SaveFile'](arg1, arg2);
}
-40
View File
@@ -1,40 +0,0 @@
module OpenManus
go 1.22.0
toolchain go1.24.1
require (
github.com/wailsapp/wails/v2 v2.10.1
golang.org/x/text v0.22.0
)
require (
github.com/bep/debounce v1.2.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/gosod v1.0.4 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
)
// replace github.com/wailsapp/wails/v2 v2.9.2 => C:\Users\aylvn\go\pkg\mod
-79
View File
@@ -1,79 +0,0 @@
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-45
View File
@@ -1,45 +0,0 @@
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
// Create an instance of the app structure
app := NewApp()
/* AppMenu := menu.NewMenu()
FileMenu := AppMenu.AddSubmenu("File")
FileMenu.AddText("&Open", keys.CmdOrCtrl("o"), nil)
FileMenu.AddSeparator()
FileMenu.AddText("Quit", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
runtime.Quit(app.ctx)
}) */
// Create application with options
err := wails.Run(&options.App{
Title: "OpenManus",
Width: 1024,
Height: 768,
// Menu: AppMenu,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
Bind: []interface{}{
app,
},
})
if err != nil {
println("Error:", err.Error())
}
}
-150
View File
@@ -1,150 +0,0 @@
package utils
import (
"bufio"
"context"
"os"
"os/exec"
"strings"
"syscall"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// ExecBat 执行bat文件
func ExecBatFile(ctx context.Context, batId string, batPath string) {
Log("ExecBatFile batPath: ", batPath)
if IsBlank(batId) || IsBlank(batPath) {
Log("batId or batPath is nil")
runtime.EventsEmit(ctx, batId, "error", "batId or batPath is nil")
return
}
cmd := exec.Command("cmd", "/C", "chcp 65001 > nul &&", batPath)
// 设置cmd.SysProcAttr.HideWindow为true以隐藏cmd窗口
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "PYTHONIOENCODING=utf-8")
// 运行命令并获取输出
stdout, err := cmd.StdoutPipe()
if err != nil {
Log("Error creating StdoutPipe for Cmd:", err)
runtime.EventsEmit(ctx, batId, "error", err.Error())
return
}
stderr, err := cmd.StderrPipe()
if err != nil {
Log("Error creating StderrPipe for Cmd:", err)
runtime.EventsEmit(ctx, batId, "error", err.Error())
return
}
if err := cmd.Start(); err != nil {
Log("Error starting cmd:", err)
runtime.EventsEmit(ctx, batId, "error", err.Error())
return
}
// 读取标准输出和错误输出
outputScanner := bufio.NewScanner(stdout)
errorScanner := bufio.NewScanner(stderr)
go func() {
for errorScanner.Scan() {
// 处理错误输出
Log("Error:", errorScanner.Text())
runtime.EventsEmit(ctx, batId, "msg", string(errorScanner.Bytes()))
}
}()
for outputScanner.Scan() {
// 处理标准输出
Log("Output:", outputScanner.Text())
// 在这里可以将输出逐行发送给客户端,例如通过HTTP响应写入等。
runtime.EventsEmit(ctx, batId, "msg", string(outputScanner.Bytes()))
}
if err := cmd.Wait(); err != nil {
Log("Error waiting for cmd:", err)
runtime.EventsEmit(ctx, batId, "error", err.Error())
}
}
func ExecPyFile(ctx context.Context, pyId string, scriptPath string) {
// 指定Python解释器的路径,例如在Windows上可能是"python"或"python.exe",在Linux或Mac上是"python3"
pythonCmd := "python"
Log("ExecPyFile scriptPath: ", scriptPath)
// 创建一个*exec.Cmd实例来运行Python脚本
cmd := exec.Command(pythonCmd, scriptPath)
// 设置cmd.SysProcAttr.HideWindow为true以隐藏cmd窗口
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
// 运行命令并获取输出
stdout, err := cmd.StdoutPipe()
if err != nil {
Log("Error creating StdoutPipe for Cmd:", err)
runtime.EventsEmit(ctx, pyId, "error", err.Error())
return
}
stderr, err := cmd.StderrPipe()
if err != nil {
Log("Error creating StderrPipe for Cmd:", err)
runtime.EventsEmit(ctx, pyId, "error", err.Error())
return
}
if err := cmd.Start(); err != nil {
Log("Error starting cmd:", err)
runtime.EventsEmit(ctx, pyId, "error", err.Error())
return
}
// 读取标准输出和错误输出
outputScanner := bufio.NewScanner(stdout)
errorScanner := bufio.NewScanner(stderr)
go func() {
for errorScanner.Scan() {
// 处理错误输出
Log("Error:", errorScanner.Text())
runtime.EventsEmit(ctx, pyId, "msg", string(errorScanner.Bytes()))
}
}()
for outputScanner.Scan() {
// 处理标准输出
Log("Output:", outputScanner.Text())
// 在这里可以将输出逐行发送给客户端,例如通过HTTP响应写入等。
runtime.EventsEmit(ctx, pyId, "msg", string(outputScanner.Bytes()))
}
if err := cmd.Wait(); err != nil {
Log("Error waiting for cmd:", err)
runtime.EventsEmit(ctx, pyId, "error", err.Error())
}
}
// CheckPort 检查端口是否被占用
func CheckPort(port string) bool {
Log("CheckPort port: ", port)
cmd := exec.Command("powershell", "-Command", "(Get-Process -Id (Get-NetTCPConnection -LocalPort "+port+" | Select-Object -ExpandProperty OwningProcess)).Name")
// 设置cmd.SysProcAttr.HideWindow为true以隐藏cmd窗口
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
stdout, err := cmd.Output()
if err != nil {
Log("Error executing command:", err)
return false
}
output := string(stdout)
if strings.TrimSpace(output) != "" {
// 确保输出不是空字符串或仅包含空白字符
Logf("Port %s is in use by process: %s\n", port, strings.TrimSpace(output))
return true
} else {
Logf("Port %s is not in use.\n", port)
return false
}
}
-88
View File
@@ -1,88 +0,0 @@
package utils
import (
"fmt"
"io"
"os"
"path/filepath"
)
// ReadAll file content
func ReadAll(filePath string) []byte {
if IsBlank(filePath) {
fmt.Println("File path is nil")
return nil
}
file, err := os.Open(filePath)
if err != nil {
fmt.Println("Error opening file:", err)
return nil
}
// 确保文件最后被关闭
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
fmt.Println("Read file error:", err)
return nil
}
return data
}
// SaveFile
func SaveFile(filePath string, data string) {
if IsBlank(filePath) {
fmt.Println("File path is nil")
return
}
file, err := os.Create(filePath)
if err != nil {
fmt.Println("Error create file:", err)
return
}
// 确保文件最后被关闭
defer file.Close()
_, err = file.WriteString(data)
if err != nil {
fmt.Println("Write file error:", err)
return
}
return
}
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
// 文件或目录已经存在
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
func DirSize(path string) (int64, error) {
var size int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
size += info.Size()
}
return nil
})
return size, err
}
func AppPath() string {
exePath, err := os.Executable()
if err != nil {
panic(err)
}
appDir := filepath.Dir(exePath)
fmt.Println("Application Directory:", appDir)
return appDir
}
-232
View File
@@ -1,232 +0,0 @@
package utils
import (
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
)
var host = "http://localhost:8020"
type Body struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
// 结构体中的变量命名首字母大写,不然不能被外部访问到
type Resp struct {
Code int `json:"code"`
Data interface{} `json:"data"`
}
func buildResp(resp *http.Response, err error) Resp {
// 确保在函数退出时关闭resp的主体
defer resp.Body.Close()
// 打印请求结果
Logf("Http Resp: %v, err: %v\n", resp, err)
if err != nil {
// 网络请求处理错误
Log("Network Error: ", err)
return Resp{resp.StatusCode, err.Error()}
}
if resp.StatusCode != 200 {
// 网络请求状态码异常
Log("Http Resp Status Code Error: ", resp)
return Resp{resp.StatusCode, resp.Status}
}
// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
Log("Error Reading Response Body: ", err)
return Resp{202, err.Error()}
}
// 打印响应内容
Log("Http Response Body: ", string(body))
// 判断返回内容类型
// Log(resp)
Logf("Content-Type=%v", resp.Header.Get("Content-Type"))
contentType := resp.Header.Get("Content-Type")
// 处理返回响应状态和内容
var bodySt Body
if !strings.HasPrefix(contentType, "application/json") {
// 非json,直接返回
return Resp{resp.StatusCode, string(body)}
}
// 序列化后返回
err = json.Unmarshal(body, &bodySt)
if err != nil {
Log("Parse Body Json Faild: ", err)
return Resp{202, err.Error()}
}
if bodySt.Code != 200 {
return Resp{bodySt.Code, bodySt.Msg}
}
return Resp{bodySt.Code, bodySt.Data}
}
func buildReqUrl(uri string) string {
if strings.HasPrefix(uri, "http") {
return uri
}
return host + uri
}
// Go http请求 https://www.cnblogs.com/Xinenhui/p/17496684.html
// Get
func Get(uri string, param map[string]interface{}, header map[string]string) Resp {
Logf("Get Uri: %s, Param: %s, Header: %s\n", uri, param, header)
apiUrl := buildReqUrl(uri)
//新建一个GET请求
req, err := http.NewRequest("GET", apiUrl, nil)
if err != nil {
return Resp{202, err.Error()}
}
// 请求头部信息
// Set时候,如果原来这一项已存在,后面的就修改已有的
// Add时候,如果原本不存在,则添加,如果已存在,就不做任何修改
for k, v := range header {
req.Header.Set(k, v)
}
// url参数处理
q := req.URL.Query()
for k, v := range param {
strOfV, err := AnyToStr(v)
if err != nil {
return Resp{202, err.Error()}
}
q.Set(k, strOfV)
}
req.URL.RawQuery = q.Encode()
// 发送请求给服务端,实例化一个客户端
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return Resp{202, err.Error()}
}
return buildResp(resp, err)
}
// Post Json
func Post(uri string, param map[string]interface{}, header map[string]string) Resp {
Logf("Post Json Uri: %s, Param: %s, Header: %s\n", uri, param, header)
apiUrl := buildReqUrl(uri)
// Json参数处理
jsonStr, err := json.Marshal(param)
if err != nil {
Log("Error Marshalling Map To JSON: ", err)
return Resp{202, err.Error()}
}
Logf("Post Json Body Payload: %s\n", string(jsonStr))
// 新建一个POST请求
req, err := http.NewRequest("POST", apiUrl, strings.NewReader(string(jsonStr)))
if err != nil {
return Resp{202, err.Error()}
}
// 请求头部信息
// Set时候,如果原来这一项已存在,后面的就修改已有的
// Add时候,如果原本不存在,则添加,如果已存在,就不做任何修改
for k, v := range header {
req.Header.Set(k, v)
}
// Post Json表单请求头
req.Header.Add("Content-Type", "application/json")
//发送请求给服务端,实例化一个客户端
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return Resp{202, err.Error()}
}
return buildResp(resp, err)
}
// Post Form
func PostForm(uri string, param map[string]interface{}, header map[string]string) Resp {
Logf("Post Form Uri: %s, Param: %s\n", uri, param)
apiUrl := buildReqUrl(uri)
// PostForm参数处理
urlMap := url.Values{}
for k, v := range param {
strOfV, err := AnyToStr(v)
if err != nil {
return Resp{202, err.Error()}
}
urlMap.Set(k, strOfV)
}
Logf("Post Form Body Payload: %s\n", urlMap.Encode())
// 新建一个POST请求
req, err := http.NewRequest("POST", apiUrl, strings.NewReader(urlMap.Encode()))
if err != nil {
return Resp{202, err.Error()}
}
// 请求头部信息
// Set时候,如果原来这一项已存在,后面的就修改已有的
// Add时候,如果原本不存在,则添加,如果已存在,就不做任何修改
for k, v := range header {
req.Header.Set(k, v)
}
// Post FormData表单请求头
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
//发送请求给服务端,实例化一个客户端
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return Resp{202, err.Error()}
}
return buildResp(resp, err)
}
// Del
func Del(uri string, param map[string]interface{}, header map[string]string) Resp {
Logf("Del Uri: %s, Param: %s\n", uri, param)
apiUrl := buildReqUrl(uri)
//新建一个Del请求
req, err := http.NewRequest("DELETE", apiUrl, nil)
if err != nil {
return Resp{202, err.Error()}
}
// 请求头部信息
// Set时候,如果原来这一项已存在,后面的就修改已有的
// Add时候,如果原本不存在,则添加,如果已存在,就不做任何修改
for k, v := range header {
req.Header.Set(k, v)
}
// url参数处理
q := req.URL.Query()
for k, v := range param {
strOfV, err := AnyToStr(v)
if err != nil {
return Resp{202, err.Error()}
}
q.Set(k, strOfV)
}
req.URL.RawQuery = q.Encode()
// 发送请求给服务端,实例化一个客户端
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return Resp{202, err.Error()}
}
return buildResp(resp, err)
}
-40
View File
@@ -1,40 +0,0 @@
package utils
import (
"log"
"os"
"strings"
)
// 保存日志到文件
func Log(v ...any) {
// 打开文件
file, err := os.OpenFile("wails.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return
}
defer file.Close()
// 设置logger
logToFile := log.New(file, "WailsLog: ", log.LstdFlags)
log.Println(v...)
// 写入日志
logToFile.Println(v...)
}
// 保存日志到文件
func Logf(format string, v ...any) {
if !strings.HasSuffix(format, "\n") {
format = format + "\n"
}
// 打开文件
file, err := os.OpenFile("wails.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return
}
defer file.Close()
// 设置logger
logToFile := log.New(file, "WailsLog: ", log.LstdFlags)
log.Printf(format, v...)
// 写入日志
logToFile.Printf(format, v...)
}
-82
View File
@@ -1,82 +0,0 @@
package utils
import (
"encoding/json"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
)
// AnyToStr 任意类型数据转string
func AnyToStr(i interface{}) (string, error) {
if i == nil {
return "", nil
}
v := reflect.ValueOf(i)
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return "", nil
}
v = v.Elem()
}
switch v.Kind() {
case reflect.String:
return v.String(), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(v.Int(), 10), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return strconv.FormatUint(v.Uint(), 10), nil
case reflect.Float32:
return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil
case reflect.Float64:
return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil
case reflect.Complex64:
return fmt.Sprintf("(%g+%gi)", real(v.Complex()), imag(v.Complex())), nil
case reflect.Complex128:
return fmt.Sprintf("(%g+%gi)", real(v.Complex()), imag(v.Complex())), nil
case reflect.Bool:
return strconv.FormatBool(v.Bool()), nil
case reflect.Slice, reflect.Map, reflect.Struct, reflect.Array:
str, _ := json.Marshal(i)
return string(str), nil
default:
return "", fmt.Errorf("unable to cast %#v of type %T to string", i, i)
}
}
func IsEmpty(s string) bool {
return len(s) == 0
}
func IsNotEmpty(s string) bool {
return len(s) > 0
}
func IsBlank(s string) bool {
return len(s) == 0 || strings.TrimSpace(s) == ""
}
func IsNotBlank(s string) bool {
return len(s) > 0 && strings.TrimSpace(s) != ""
}
func GbkToUtf8(s string) string {
gbkBytes := []byte(s)
// 使用transform.Reader将GBK转换为UTF-8
reader := transform.NewReader(strings.NewReader(string(gbkBytes)), simplifiedchinese.GBK.NewDecoder())
utf8Bytes, err := io.ReadAll(reader)
if err != nil {
fmt.Println("Error converting from GBK: ", err)
return s
}
fmt.Println("Converted to UTF-8: ", string(utf8Bytes))
return string(utf8Bytes)
}
-34
View File
@@ -1,34 +0,0 @@
wails官网: https://wails.io/
go环境下载: https://go.dev/dl/
https://blog.csdn.net/jankin6/article/details/140087959
go env -w GOPROXY=https://goproxy.cn
go install github.com/wailsapp/wails/v2/cmd/wails@latest
wails doctor
D:
mkdir VsCodeProjects
cd VsCodeProjects
// 初始化创建项目
wails init -n OpenManus -t vue
// 进入项目并运行
cd .\OpenManus
wails dev
// 构建应用
wails build
// 安装工具库 frontend包下
cd D:\VsCodeProjects\OpenManus\frontend
npm install vue-router@latest
npm i pinia
pnpm i pinia-plugin-persistedstate
npm install axios
npm install qs
npm i --save-dev @types/qs
npm install marked
-13
View File
@@ -1,13 +0,0 @@
{
"$schema": "https://wails.io/schemas/config.v2.json",
"name": "OpenManus",
"outputfilename": "OpenManus",
"frontend:install": "npm install",
"frontend:build": "npm run build",
"frontend:dev:watcher": "npm run dev",
"frontend:dev:serverUrl": "auto",
"author": {
"name": "aylvn",
"email": "aylvn@sina.com"
}
}
-14
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
+1
View File
@@ -20,3 +20,4 @@ aiofiles~=24.1.0
pydantic_core~=2.27.2
colorama~=0.4.6
playwright~=1.49.1
litellm~=1.63.6
-9
View File
@@ -1,9 +0,0 @@
@echo off
echo Running pre-commit checks...
echo If you have not installed pre-commit, please run:
echo pip install pre-commit
echo and then run this script again.
pre-commit run --all-files
pause
-42
View File
@@ -1,42 +0,0 @@
@echo off
setlocal
cd /d %~dp0
set "VENV_DIR=%~dp0venv"
set "PYTHON_PATH=%VENV_DIR%\python.exe"
where git >nul 2>&1
if %errorlevel% == 0 (
echo Trying to sync with GitHub repository...
git pull origin front-end 2>&1 || echo Failed to sync with GitHub, skipping update...
) else (
echo Git not detected, skipping code synchronization
)
if not exist "%VENV_DIR%\" (
echo Virtual environment not found, initializing installation...
python -m venv "%VENV_DIR%" || (
echo Failed to create virtual environment, please install Python 3.12 first
pause
exit /b 1
)
call "%VENV_DIR%\Scripts\activate.bat"
pip install -r requirements.txt || (
echo Dependency installation failed, please check requirements. txt
pause
exit /b 1
)
)
echo Starting Python application...
if not exist "%PYTHON_PATH%" (
echo Error: Python executable file does not exist in %PYTHON_PATH%
echo Please try deleting the venv folder and running the script again
pause
exit /b 1
)
"%PYTHON_PATH%" "%~dp0app.py"
pause
endlocal
-540
View File
@@ -1,540 +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();
promptInput.value = '';
})
.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;
let lastResultContent = '';
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);
// Initial polling
fetch(`/tasks/${taskId}`)
.then(response => response.json())
.then(task => {
updateTaskStatus(task);
})
.catch(error => {
console.error('Initial status fetch failed:', error);
});
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);
try {
const data = JSON.parse(event.data);
lastResultContent = data.result || '';
container.innerHTML += `
<div class="complete">
<div>✅ Task completed</div>
<pre>${lastResultContent}</pre>
</div>
`;
fetch(`/tasks/${taskId}`)
.then(response => response.json())
.then(task => {
updateTaskStatus(task);
})
.catch(error => {
console.error('Final status update failed:', error);
});
eventSource.close();
currentEventSource = null;
} catch (e) {
console.error('Error handling complete event:', e);
}
});
eventSource.addEventListener('error', (event) => {
clearInterval(heartbeatTimer);
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);
eventSource.close();
fetch(`/tasks/${taskId}`)
.then(response => response.json())
.then(task => {
if (task.status === 'completed' || task.status === 'failed') {
updateTaskStatus(task);
if (task.status === 'completed') {
container.innerHTML += `
<div class="complete">
<div>✅ Task completed</div>
</div>
`;
} else {
container.innerHTML += `
<div class="error">
❌ Error: ${task.error || 'Task failed'}
</div>
`;
}
} else 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>
`;
}
})
.catch(error => {
console.error('Task status check failed:', error);
if (retryCount < maxRetries) {
retryCount++;
setTimeout(connect, retryDelay);
}
});
};
}
connect();
}
function loadHistory() {
fetch('/tasks')
.then(response => {
if (!response.ok) {
return response.text().then(text => {
throw new Error(`request failure: ${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 || 'Unknown state'}
</span>
</div>
</div>
`).join('');
})
.catch(error => {
console.error('Failed to load history records:', error);
const listContainer = document.getElementById('task-list');
listContainer.innerHTML = `<div class="error">Load Fail: ${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');
// Executing step
const stepRegex = /Executing step (\d+)\/(\d+)/;
if (type === 'log' && stepRegex.test(content)) {
const match = content.match(stepRegex);
const currentStep = parseInt(match[1]);
const totalSteps = parseInt(match[2]);
step.className = 'step-divider';
step.innerHTML = `
<div class="step-circle">${currentStep}</div>
<div class="step-line"></div>
<div class="step-info">${currentStep}/${totalSteps}</div>
`;
} else if (type === 'act') {
// Check if it contains information about file saving
const saveRegex = /Content successfully saved to (.+)/;
const match = content.match(saveRegex);
step.className = `step-item ${type}`;
if (match && match[1]) {
const filePath = match[1].trim();
const fileName = filePath.split('/').pop();
const fileExtension = fileName.split('.').pop().toLowerCase();
// Handling different types of files
let fileInteractionHtml = '';
if (['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'].includes(fileExtension)) {
fileInteractionHtml = `
<div class="file-interaction image-preview">
<img src="${filePath}" alt="${fileName}" class="preview-image" onclick="showFullImage('${filePath}')">
<a href="/download?file_path=${filePath}" download="${fileName}" class="download-link">⬇️ 下载图片</a>
</div>
`;
} else if (['mp3', 'wav', 'ogg'].includes(fileExtension)) {
fileInteractionHtml = `
<div class="file-interaction audio-player">
<audio controls src="${filePath}"></audio>
<a href="/download?file_path=${filePath}" download="${fileName}" class="download-link">⬇️ 下载音频</a>
</div>
`;
} else if (['html', 'js', 'py'].includes(fileExtension)) {
fileInteractionHtml = `
<div class="file-interaction code-file">
<button onclick="simulateRunPython('${filePath}')" class="run-button">▶️ 模拟运行</button>
<a href="/download?file_path=${filePath}" download="${fileName}" class="download-link">⬇️ 下载文件</a>
</div>
`;
} else {
fileInteractionHtml = `
<div class="file-interaction">
<a href="/download?file_path=${filePath}" download="${fileName}" class="download-link">⬇️ 下载文件: ${fileName}</a>
</div>
`;
}
step.innerHTML = `
<div class="log-line">
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
<pre>${content}</pre>
${fileInteractionHtml}
</div>
`;
} else {
step.innerHTML = `
<div class="log-line">
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
<pre>${content}</pre>
</div>
`;
}
} else {
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>`;
if (currentEventSource) {
currentEventSource.close();
currentEventSource = null;
}
} else if (task.status === 'failed') {
statusBar.innerHTML = `<span class="status-error">❌ Task failed: ${task.error || 'Unknown error'}</span>`;
if (currentEventSource) {
currentEventSource.close();
currentEventSource = null;
}
} else {
statusBar.innerHTML = `<span class="status-running">⚙️ Task running: ${task.status}</span>`;
}
}
// Display full screen image
function showFullImage(imageSrc) {
const modal = document.getElementById('image-modal');
if (!modal) {
const modalDiv = document.createElement('div');
modalDiv.id = 'image-modal';
modalDiv.className = 'image-modal';
modalDiv.innerHTML = `
<span class="close-modal">&times;</span>
<img src="${imageSrc}" class="modal-content" id="full-image">
`;
document.body.appendChild(modalDiv);
const closeBtn = modalDiv.querySelector('.close-modal');
closeBtn.addEventListener('click', () => {
modalDiv.classList.remove('active');
});
modalDiv.addEventListener('click', (e) => {
if (e.target === modalDiv) {
modalDiv.classList.remove('active');
}
});
setTimeout(() => modalDiv.classList.add('active'), 10);
} else {
document.getElementById('full-image').src = imageSrc;
modal.classList.add('active');
}
}
// Simulate running Python files
function simulateRunPython(filePath) {
let modal = document.getElementById('python-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'python-modal';
modal.className = 'python-modal';
modal.innerHTML = `
<div class="python-console">
<div class="close-modal">&times;</div>
<div class="python-output">Loading Python file contents...</div>
</div>
`;
document.body.appendChild(modal);
const closeBtn = modal.querySelector('.close-modal');
closeBtn.addEventListener('click', () => {
modal.classList.remove('active');
});
}
modal.classList.add('active');
// Load Python file content
fetch(filePath)
.then(response => response.text())
.then(code => {
const outputDiv = modal.querySelector('.python-output');
outputDiv.innerHTML = '';
const codeElement = document.createElement('pre');
codeElement.textContent = code;
codeElement.style.marginBottom = '20px';
codeElement.style.padding = '10px';
codeElement.style.borderBottom = '1px solid #444';
outputDiv.appendChild(codeElement);
// Add simulation run results
const resultElement = document.createElement('div');
resultElement.innerHTML = `
<div style="color: #4CAF50; margin-top: 10px; margin-bottom: 10px;">
> Simulated operation output:</div>
<pre style="color: #f8f8f8;">
#This is the result of Python code simulation run
#The actual operational results may vary
# Running ${filePath.split('/').pop()}...
print("Hello from Python Simulated environment!")
# Code execution completed
</pre>
`;
outputDiv.appendChild(resultElement);
})
.catch(error => {
console.error('Error loading Python file:', error);
const outputDiv = modal.querySelector('.python-output');
outputDiv.innerHTML = `Error loading file: ${error.message}`;
});
}
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();
});
}
// Add keyboard event listener to close modal boxes
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const imageModal = document.getElementById('image-modal');
if (imageModal && imageModal.classList.contains('active')) {
imageModal.classList.remove('active');
}
const pythonModal = document.getElementById('python-modal');
if (pythonModal && pythonModal.classList.contains('active')) {
pythonModal.classList.remove('active');
}
}
});
});
-522
View File
@@ -1,522 +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;
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-left: 20px;
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 division line style */
.step-divider {
display: flex;
align-items: center;
width: 100%;
margin: 15px 0;
position: relative;
}
.step-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: var(--primary-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.2em;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
z-index: 2;
flex-shrink: 0;
}
/* File interaction style */
.file-interaction {
margin-top: 15px;
padding: 10px;
border-radius: 6px;
background-color: #f5f7fa;
border: 1px solid var(--border-color);
}
.download-link {
display: inline-block;
padding: 8px 16px;
background-color: var(--primary-color);
color: white;
text-decoration: none;
border-radius: 4px;
font-size: 0.9em;
transition: background-color 0.2s;
}
.download-link:hover {
background-color: var(--primary-hover);
}
.preview-image {
max-width: 100%;
max-height: 200px;
margin-bottom: 10px;
border-radius: 4px;
cursor: pointer;
transition: transform 0.2s;
}
.preview-image:hover {
transform: scale(1.02);
}
.audio-player {
display: flex;
flex-direction: column;
gap: 10px;
}
.audio-player audio {
width: 100%;
margin-bottom: 10px;
}
.run-button {
display: inline-block;
padding: 8px 16px;
background-color: var(--success-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
margin-right: 10px;
transition: background-color 0.2s;
}
.run-button:hover {
background-color: #218838;
}
/* Full screen image modal box */
.image-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
z-index: 1000;
justify-content: center;
align-items: center;
}
.image-modal.active {
display: flex;
}
.modal-content {
max-width: 90%;
max-height: 90%;
}
.close-modal {
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
}
/* Python runs simulation modal boxes */
.python-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
z-index: 1000;
justify-content: center;
align-items: center;
}
.python-modal.active {
display: flex;
}
.python-console {
width: 80%;
height: 80%;
background-color: #1e1e1e;
color: #f8f8f8;
border-radius: 8px;
padding: 15px;
font-family: 'Courier New', monospace;
overflow: auto;
}
.python-output {
white-space: pre-wrap;
line-height: 1.5;
}
.step-line {
flex-grow: 1;
height: 2px;
background-color: var(--border-color);
margin-left: 10px;
}
.step-info {
margin-left: 15px;
font-weight: bold;
color: var(--text-light);
font-size: 0.9em;
}
.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;
}
-39
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>