Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13f98a0fde | |||
| 487b44fda8 | |||
| 737abe4f90 | |||
| cbef113736 | |||
| dc8ed530f0 | |||
| 62cfcb182e | |||
| 111a2bc6b1 | |||
| 61d705f6e6 | |||
| cafd4a18da | |||
| b67d1b90e0 | |||
| 6a89ae8256 | |||
| 4d36a4c3e2 | |||
| fed6fe1421 | |||
| 3a958944a0 | |||
| 8d18c05350 | |||
| eeefddf6bf | |||
| 35de1f9e15 | |||
| 2c0b2d1fb3 | |||
| 211d0694e3 |
@@ -178,9 +178,3 @@ data/
|
||||
|
||||
# Workspace
|
||||
workspace/
|
||||
|
||||
# Private Config
|
||||
config/config.toml
|
||||
|
||||
# Desktop runtime
|
||||
desktop/frontend/wailsjs/runtime/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 社区的贡献者共同构建,感谢这个充满活力的智能体开发者社区!
|
||||
|
||||
@@ -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"])
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 +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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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,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):
|
||||
|
||||
|
Before Width: | Height: | Size: 217 KiB After Width: | Height: | Size: 166 KiB |
@@ -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
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
build
|
||||
node_modules
|
||||
frontend/dist
|
||||
frontend/package.json.md5
|
||||
*.log
|
||||
@@ -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 project’s dist directory.
|
||||
@@ -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目录下
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 989 B |
@@ -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,
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
English | [中文](README_zh.md)
|
||||
|
||||
[](https://github.com/mannaandpoem/OpenManus/stargazers)
|
||||
 
|
||||
[](https://opensource.org/licenses/MIT)  
|
||||
[](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
|
||||
|
||||
[](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}},
|
||||
}
|
||||
```
|
||||
@@ -1,148 +0,0 @@
|
||||
[English](README.md) | 中文
|
||||
|
||||
[](https://github.com/mannaandpoem/OpenManus/stargazers)
|
||||
 
|
||||
[](https://opensource.org/licenses/MIT)  
|
||||
[](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。
|
||||
|
||||
或通过 📧 邮件联系 @mannaandpoem:mannaandpoem@gmail.com
|
||||
|
||||
## 交流群
|
||||
|
||||
加入我们的飞书交流群,与其他开发者分享经验!
|
||||
|
||||
<div align="center" style="display: flex; gap: 20px;">
|
||||
<img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
|
||||
</div>
|
||||
|
||||
## Star 数量
|
||||
|
||||
[](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>
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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
|
||||
@@ -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: "首页",
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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
|
||||
@@ -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",
|
||||
}
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -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>;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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=
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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">×</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">×</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');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||