Compare commits

...

55 Commits

Author SHA1 Message Date
mannaandpoem 0b7d0675ec Merge pull request #1034 from aylvn/desktop
Pre-commit checks / pre-commit-check (push) Failing after 1s
Desktop: Improve home page and task page
2025-04-06 21:12:48 +08:00
aylvn beb52b4f36 Desktop: Improve home page 2025-04-06 17:09:51 +08:00
aylvn 68372c46a9 Merge branch 'mannaandpoem:desktop' into desktop 2025-04-06 14:01:04 +08:00
aylvn 4f34aae6bd Desktop: init page improve 2025-04-04 18:48:21 +08:00
aylvn 41d6c81903 Desktop: init page improve 2025-03-31 22:46:06 +08:00
mannaandpoem 4e3738cf78 Merge pull request #917 from aylvn/desktop
Desktop: Improve task page
2025-03-31 22:40:57 +08:00
aylvn e7bdfe123d Desktop: init page improve 2025-03-31 22:23:24 +08:00
aylvn d365b5453b Desktop: Adjust setup layout 2025-03-31 00:24:50 +08:00
aylvn 5b810e4320 Desktop: Adjust setup layout 2025-03-30 23:47:27 +08:00
aylvn 15697b9956 Desktop: hide cmd window when exec bash 2025-03-30 20:24:05 +08:00
aylvn 201215b20c Desktop: Improve task page 2025-03-30 20:14:06 +08:00
aylvn cd0206b744 Desktop: handle prd env file path 2025-03-30 18:06:50 +08:00
aylvn cd7b16761a Desktop: Improve setup feature 2025-03-30 17:25:37 +08:00
aylvn 8bba57609e Desktop: Init setting development 2025-03-30 10:54:34 +08:00
aylvn d0ec46043d f 2025-03-29 15:33:38 +08:00
aylvn dab2ec5d97 Desktop: Remove theme config menu 2025-03-29 15:02:16 +08:00
aylvn 4ea3524ac1 Desktop: Improve task page 2025-03-29 14:58:34 +08:00
aylvn 8789cf1972 Desktop: Improve task page 2025-03-23 20:17:57 +08:00
aylvn 8b28a520a2 Desktop: Improve task page 2025-03-23 13:21:46 +08:00
aylvn 412eaa14dc delete test files 2025-03-23 13:09:03 +08:00
aylvn b001e3f1cc Desktop: Improve the task page. 2025-03-23 13:07:35 +08:00
aylvn ad9845b543 Desktop: Improve task page. 2025-03-22 23:28:20 +08:00
aylvn f0bcc3d65a Desktop: Improve the menu action 2025-03-22 18:55:56 +08:00
aylvn ccd20c3d77 Merge branch 'desktop' of https://github.com/aylvn/desktop into desktop 2025-03-22 14:35:16 +08:00
aylvn d8640a2270 Desktop: Improve the task page 2025-03-22 14:35:11 +08:00
aylvn eab5ce42bf Merge branch 'mannaandpoem:desktop' into desktop 2025-03-22 10:02:44 +08:00
aylvn 80f822b209 delete test file 2025-03-22 01:17:47 +08:00
aylvn 414ac3e096 Desktop: Improve the saving functions of general settings and large model settings. 2025-03-22 01:05:10 +08:00
aylvn 9a205fd884 desktop avoid trace wails runtime files 2025-03-21 20:50:56 +08:00
aylvn 0ece058823 desktop avoid trace wails runtime files 2025-03-21 20:49:11 +08:00
Isaac 7e18c97a2f Merge pull request #821 from aylvn/desktop
Desktop has implemented the large model configuration page and settings page functionality
2025-03-19 15:44:17 +08:00
Sheng Fan e190e1484e merge: manually check translations 2025-03-19 12:54:13 +08:00
Sheng Fan e3202ade12 refactor: rewrite comments in English 2025-03-19 12:44:27 +08:00
aylvn ec35d42936 resolved some pre-commit check problems 2025-03-18 22:33:32 +08:00
aylvn da607e31b2 Desktop version has added a llm configuration page and a general configuration page 2025-03-18 22:20:37 +08:00
aylvn 4a0c3c6231 Merge branch 'mannaandpoem:desktop' into desktop 2025-03-17 23:19:12 +08:00
aylvn 2d507c0bcf Desktop client reads llm configuration from the config/config.toml file 2025-03-17 23:15:14 +08:00
aylvn 7e42e4ccd2 优化任务页面布局和交互控制 2025-03-16 18:57:30 +08:00
aylvn bcada6ead5 增加深色/浅色主题切换功能 2025-03-16 16:22:22 +08:00
aylvn d08db8a76a 新增历史任务详情页 2025-03-16 16:02:03 +08:00
aylvn 94cf2be101 任务状态展示优化 2025-03-16 15:04:04 +08:00
aylvn 00bf070dbd 历史任务页面开发 2025-03-16 14:58:07 +08:00
mannaandpoem 77b42aaf47 Merge pull request #679 from aylvn/desktop
Desktop
2025-03-16 12:43:49 +08:00
aylvn ae053d6e27 更新desktop README.md 2025-03-16 12:26:07 +08:00
aylvn 0710570811 桌面客户端desktop代码初始化提交 2025-03-16 12:13:27 +08:00
Isaac 38e34219d3 Merge pull request #565 from Feige-cn/front-end
fix download bug
2025-03-13 16:34:57 +08:00
Feige-cn 693f6a90ad fix download bug 2025-03-13 11:06:49 +08:00
xiangjinyu d847b550d6 add max_steps 2025-03-12 19:22:32 +08:00
xiangjinyu 5bba31db3e update terminate prompt 2025-03-12 19:21:13 +08:00
xiangjinyu 548c402316 add max_observe in toolcall and manus 2025-03-12 17:23:08 +08:00
xiangjinyu c96c016feb update browser-use window size 2025-03-12 17:21:06 +08:00
Isaac 728fdaffd6 Merge pull request #520 from Feige-cn/front-end
Optimize front-end display
2025-03-12 15:53:22 +08:00
Feige-cn fa2b05b658 Optimize front-end display 2025-03-12 15:45:02 +08:00
Isaac 7b96e12858 Merge pull request #456 from Feige-cn/front-end
Synchronize built-in environment
2025-03-11 13:58:00 +08:00
Feige-cn 62f937c1ca Synchronize built-in environment 2025-03-11 13:48:00 +08:00
71 changed files with 14743 additions and 60 deletions
+6
View File
@@ -178,3 +178,9 @@ data/
# Workspace
workspace/
# Private Config
config/config.toml
# Desktop runtime
desktop/frontend/wailsjs/runtime/
+43 -7
View File
@@ -1,13 +1,22 @@
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 HTMLResponse, JSONResponse, StreamingResponse
from fastapi.responses import (
FileResponse,
HTMLResponse,
JSONResponse,
StreamingResponse,
)
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
@@ -90,6 +99,14 @@ 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)
@@ -107,7 +124,6 @@ async def run_task(task_id: str, prompt: str):
agent = Manus(
name="Manus",
description="A versatile agent that can solve various tasks using multiple tools",
max_steps=30,
)
async def on_think(thought):
@@ -135,7 +151,7 @@ async def run_task(task_id: str, prompt: str):
async def __call__(self, message):
import re
# 提取 - 后面的内容
# Extract - Subsequent Content
cleaned_message = re.sub(r"^.*? - ", "", message)
event_type = "log"
@@ -244,12 +260,32 @@ async def generic_exception_handler(request: Request, exc: Exception):
)
def open_local_browser():
webbrowser.open_new_tab("http://localhost:5172")
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
threading.Timer(3, open_local_browser).start()
uvicorn.run(app, host="localhost", port=5172)
config = load_config()
open_with_config = partial(open_local_browser, config)
threading.Timer(3, open_with_config).start()
uvicorn.run(app, host=config["host"], port=config["port"])
+3
View File
@@ -26,6 +26,9 @@ 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(
+5 -1
View File
@@ -1,5 +1,5 @@
import json
from typing import Any, List, Literal
from typing import Any, List, Literal, Optional, Union
from pydantic import Field
@@ -31,6 +31,7 @@ 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"""
@@ -114,6 +115,9 @@ 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 -1
View File
@@ -103,7 +103,12 @@ class BrowserUseTool(BaseTool):
async def _ensure_browser_initialized(self) -> BrowserContext:
"""Ensure browser and context are initialized."""
if self.browser is None:
self.browser = BrowserUseBrowser(BrowserConfig(headless=False))
# 使用Chrome命令行参数设置窗口大小和位置
browser_config = BrowserConfig(
headless=False,
disable_security=True,
)
self.browser = BrowserUseBrowser(browser_config)
if self.context is None:
self.context = await self.browser.new_context()
self.dom_service = DomService(await self.context.get_current_page())
+2 -1
View File
@@ -1,7 +1,8 @@
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."""
_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."""
class Terminate(BaseTool):
+5
View File
@@ -20,3 +20,8 @@ temperature = 0.0
model = "claude-3-5-sonnet"
base_url = "https://api.openai.com/v1"
api_key = "sk-..."
# Server configuration
[server]
host = "localhost"
port = 5172
+5
View File
@@ -0,0 +1,5 @@
build
node_modules
frontend/dist
frontend/package.json.md5
*.log
+71
View File
@@ -0,0 +1,71 @@
# OpenManus-Desktop Project
## Project Overview
OpenManus-Desktop is a desktop application built on the Wails framework, combining Go backend and Vue3 frontend technologies. The project utilizes Vite as the frontend build tool, offering an efficient development experience.
## Technology Stack
- Backend: Go
- Frontend: Vue3 + Vite
- UI Framework: Element Plus
- State Management: Pinia
- Routing: Vue Router
- Build Tool: Wails
## Development Environment Requirements
- Go 1.18+
- Node.js 20+
- Wails CLI v2+
## Getting Started
### 1. Install Development Environment
#### 1.1. Install Golang Environment
Golang environment : https://go.dev/dl/
#### 1.2. Install Wails Client
wails: https://wails.io/
// For users in mainland China, use a proxy
go env -w GOPROXY=https://goproxy.cn
go install github.com/wailsapp/wails/v2/cmd/wails@latest
Run the following command to check if the Wails client is installed successfully:
wails doctor
#### 1.3. Install Node.js Environment
nodejs: https://nodejs.org/en
### 2. Install Project Dependencies
cd .\desktop\frontend
npm install
### 3. Run the Project
To run the project:
cd .\desktop
wails dev
To start the backend service:
After configuring the config/config.toml file, execute the following command to start the server:
cd .. (Project root directory)
python app.py
### 4. Package the Project
To build the application:
wails build
The built application will be located in the projects dist directory.
+71
View File
@@ -0,0 +1,71 @@
# OpenManus-Desktop 项目
## 项目简介
OpenManus-Desktop 是一个基于Wails框架构建的桌面应用程序,结合了Go后端和Vue3前端技术栈。项目采用Vite作为前端构建工具,提供了高效的开发体验。
## 技术栈
- 后端: Go
- 前端: Vue3 + Vite
- UI 框架: Element Plus
- 状态管理: Pinia
- 路由: Vue Router
- 构建工具: Wails
## 开发环境要求
- Go 1.18+
- Node.js 20+
- Wails CLI v2+
## 快速开始
### 1. 安装开发环境
#### 1.1. 安装Go语言环境
Go环境下载: https://go.dev/dl/
#### 1.2. 安装wails客户端
wails官网: https://wails.io/
// 中国大陆使用代理
go env -w GOPROXY=https://goproxy.cn
go install github.com/wailsapp/wails/v2/cmd/wails@latest
执行以下命名令检查wails客户端安装是否成功:
wails doctor
#### 1.3. 安装Node.js环境
nodejs官网安装: https://nodejs.org/en
### 2. 安装项目依赖
cd .\desktop\frontend
npm install
### 3. 运行项目
运行项目:
cd .\desktop
wails dev
启动服务端:
配置好config/config.toml文件后, 执行以下命令启动服务端:
cd .. (项目根目录)
python app.py
### 4. 打包项目
构建应用:
wails build
构建好的应用在项目dist目录下
+107
View File
@@ -0,0 +1,107 @@
package main
import (
"OpenManus/src/utils"
"context"
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
type App struct {
ctx context.Context
}
type File struct {
Result string `json:"result"`
Error string `json:"error"`
Callbackid string `json:"callbackid"`
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
// 注册事件监听器
runtime.EventsOn(ctx, "events", func(data ...interface{}) {
if len(data) > 0 {
for i := 0; i < len(data); i++ {
fmt.Println("Received events with data:", data[i])
}
} else {
fmt.Println("Received events without data")
}
})
// 注册bat批处理事件监听器
runtime.EventsOn(ctx, "bat", func(data ...interface{}) {
if len(data) == 2 {
fmt.Println("Received bat with data, batId: ", data[0])
fmt.Println("Received bat with data, batPath: ", data[1])
utils.ExecBatFile(a.ctx, data[0].(string), data[1].(string))
} else if len(data) > 0 && len(data) < 2 {
fmt.Println("Received bat with data, required 2 paramters, found 1: ", data[0])
} else {
fmt.Println("Received bat without data")
}
})
// 注册执行py脚本监听器
runtime.EventsOn(ctx, "pyFile", func(data ...interface{}) {
if len(data) == 2 {
fmt.Println("Received bat with data, batId: ", data[0])
fmt.Println("Received bat with data, batPath: ", data[1])
utils.ExecPyFile(a.ctx, data[0].(string), data[1].(string))
} else if len(data) > 0 && len(data) < 2 {
fmt.Println("Received bat with data, required 2 paramters, found 1: ", data[0])
} else {
fmt.Println("Received bat without data")
}
})
}
// Greet returns a greeting for the given name
func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s, It's show time!", name)
}
// ReadAll reads file content
func (a *App) ReadAll(filePath string) string {
utils.Log("ReadAll filePath: ", filePath)
// Read the file content, resulting in a JSON string containing file content and callback ID
data := string(utils.ReadAll(filePath))
utils.Log("ReadAll data: ", data)
return data
}
func (a *App) SaveFile(filePath string, data string) {
utils.SaveFile(filePath, data)
}
func (a *App) PathExists(path string) bool {
exists, _ := utils.PathExists(path)
return exists
}
func (a *App) DirSize(path string) int64 {
utils.Log("DirSize path: ", path)
size, _ := utils.DirSize(path)
utils.Log("DirSize size: ", size)
return size
}
func (a *App) AppPath() string {
return utils.AppPath()
}
func (a *App) CheckPort(port string) bool {
return utils.CheckPort(port)
}
+8
View File
@@ -0,0 +1,8 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs,
check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>OpenManus</title>
</head>
<body>
<div id="app"></div>
<script src="./src/main.js" type="module"></script>
</body>
</html>
+3508
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.8.3",
"element-plus": "^2.9.2",
"marked": "^15.0.7",
"pinia": "^3.0.1",
"pinia-plugin-persistedstate": "^4.2.0",
"qs": "^6.14.0",
"sql-formatter": "^15.4.9",
"vue": "^3.2.37",
"vue-i18n": "^11.0.0-rc.1",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@types/qs": "^6.9.18",
"@vitejs/plugin-vue": "^3.0.3",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.83.1",
"unplugin-auto-import": "^0.19.0",
"unplugin-vue-components": "^0.28.0",
"vite": "^3.0.7"
}
}
+2069
View File
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
<template>
<!-- Global Configuration -->
<el-config-provider :size="size" :z-index="zIndex" :locale="locale" :button="config" :message="config"
:value-on-clear="null" :empty-values="[undefined, null]">
<RouterView />
</el-config-provider>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import en from 'element-plus/es/locale/lang/en'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
/** Dark Theme */
import { useDark, useStorage } from '@vueuse/core'
const size = 'default'
const zIndex = 2000
const localConfig = localStorage.getItem('config') ? JSON.parse(localStorage.getItem('config')) : {}
const localeStr = localConfig.selectedLang ? localConfig.selectedLang.code : 'en'
const locale = localeStr == 'en' ? en : zhCn
const isDark = useDark()
// Store user preferences
const userPrefersDark = ref(null)
onMounted(() => {
// Use useStorage hook to sync isDark and local storage
useStorage(
'user-prefers-dark',
userPrefersDark,
localStorage,
isDark.value ? 'dark' : 'light'
)
})
// Watch isDark changes and update local storage
watch(isDark, (newValue) => {
userPrefersDark.value = newValue ? 'dark' : 'light'
})
/* Global Configuration */
const config = reactive({
// Button - Automatically insert space between Chinese characters
autoInsertSpace: true,
// Message - Maximum number of messages that can be displayed simultaneously
max: 3,
})
</script>
<style scoped></style>
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

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