Compare commits

...

45 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
62 changed files with 14188 additions and 0 deletions
+6
View File
@@ -178,3 +178,9 @@ data/
# Workspace
workspace/
# Private Config
config/config.toml
# Desktop runtime
desktop/frontend/wailsjs/runtime/
+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