桌面客户端desktop代码初始化提交

This commit is contained in:
aylvn 2025-03-16 12:13:27 +08:00
parent 38e34219d3
commit 0710570811
55 changed files with 12608 additions and 0 deletions

5
desktop/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
build
node_modules
frontend/dist
frontend/package.json.md5
*.log

73
desktop/README.md Normal file
View File

@ -0,0 +1,73 @@
# 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:
cd .\OpenManus-front-end
python app.py
### 4. Package the Project
To build the application:
wails build
The built application will be located in the projects dist directory.

73
desktop/README_zh.md Normal file
View File

@ -0,0 +1,73 @@
# 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
启动服务端:
cd .\OpenManus-front-end
python app.py
### 4. 打包项目
构建应用:
wails build
构建好的应用在项目dist目录下

27
desktop/app.go Normal file
View File

@ -0,0 +1,27 @@
package main
import (
"context"
"fmt"
)
// App struct
type App struct {
ctx context.Context
}
// 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
}
// 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)
}

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)

View File

@ -0,0 +1,73 @@
# 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 .\OpenManus-Desktop\frontend
npm install
### 3. 运行项目
运行项目:
cd .\OpenManus-Desktop
wails dev
启动服务端:
cd .\OpenManus-front-end
python app.py
### 4. 打包项目
构建应用:
wails build
构建好的应用在项目dist目录下

View File

@ -0,0 +1,13 @@
<!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>

3496
desktop/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
{
"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",
"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
desktop/frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
<template>
<!-- 全局配置 -->
<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'
/** 暗黑主题 */
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()
//
const userPrefersDark = ref(null)
onMounted(() => {
// 使 useStorage isDark
useStorage(
'user-prefers-dark',
userPrefersDark,
localStorage,
isDark.value ? 'dark' : 'light'
)
})
// isDark
watch(isDark, (newValue) => {
userPrefersDark.value = newValue ? 'dark' : 'light'
})
/* 全局配置 */
const config = reactive({
// -
autoInsertSpace: true,
// -
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: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

View File

@ -0,0 +1,201 @@
import utils from '@/assets/js/utils'
// 临时缓存文件信息
function cache(fileObj, $event) {
console.log('cache fileObj start:', fileObj, $event.target, $event.dataTransfer)
console.log('typeof fileObj:', Array.isArray(fileObj))
// 如果fileObj是数组,创建一个新的元素,追加到数组
// event.target.files和event.dataTransfer.files是JavaScript中与文件上传和拖放相关的事件属性。
// event.target.files这个属性是在HTML的文件输入元素<input type="file">)上使用时,
// 当用户选择文件并触发change事件时可以通过event.target.files获取到用户选择的文件列表。
// event.dataTransfer.files这个属性是在用户拖放文件到一个元素上时
// 可以通过event.dataTransfer.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("无法识别的事件")
return
}
const file = files[0]
console.log("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(fileType, typeof (fileType))
if (utils.notNull(fileType) && fileType.startsWith("image")) {
fileInfo.imgUrl = fileInfo.fileUrl
}
fileInfo.fileName = file.name
console.log('cache fileObj end:', fileInfo)
if (Array.isArray(fileObj)) {
// 操作成功后追加到数组末尾
fileObj.push(fileInfo)
}
if ($event.type == 'change') {
// 解决选择相同的文件 不触发change事件的问题,放在最后清理
$event.target.value = null
}
}
// 上传文件
async function upload(fileObj) {
console.log("准备开始上传文件!", fileObj, fileObj.file, fileObj.fileId)
// 当前地址
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("开始上传文件!", 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("文件同步上传处理完毕", fileObj)
return fileObj
}
// 更新文件备注
async function updRemark(fileId, remarkUpd) {
const param = {
fileId: fileId,
remark: remarkUpd
}
await utils.awaitPost('/common/file/updRemark', param)
console.log("更新文件备注成功")
}
// 批量上传文件
async function uploads(fileObjs) {
if (utils.isEmpty(fileObjs)) {
return
}
for (let index in fileObjs) {
console.log('fileObjs[index]:', fileObjs, index, fileObjs.length, fileObjs[index])
await upload(fileObjs[index])
console.log("uploads index:", index, "上传文件完毕", fileObjs[index])
}
}
// 上传文件(onChange时)
function upOnChg(fileObj, $event) {
const file = $event.target.files[0] || $event.dataTransfer.files[0]
// 当前地址
let URL = window.URL || window.webkitURL
// 转成 blob地址
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("文件上传结果:", data)
Object.assign(fileObj, data)
fileObj.remarkUpd = data.remark
})
}
function add(fileList) {
const comp = {
index: fileList.length,
file: null,
fileId: null,
fileName: null,
fileUrl: null,
imgUrl: null,
remark: null
}
fileList.push(comp)
}
function del(fileObj, index) {
console.log("fileObj,index:", fileObj, index)
if (Array.isArray(fileObj)) {
fileObj.splice(index, 1)
} else {
utils.clearProps(fileObj)
}
}
function trans(javaFile, jsFile) {
if (jsFile == undefined || jsFile == null) {
return
}
// 如果是数组,先清空数组
if (jsFile instanceof Array) {
jsFile.splice(0, jsFile.length)
} else {
utils.clearProps(jsFile)
}
if (javaFile == undefined || javaFile == null) {
return
}
// 数组类型
if (jsFile instanceof Array) {
for (let java of javaFile) {
const js = {}
java.remarkUpd = java.remark
Object.assign(js, java)
jsFile.push(js)
}
} else {
// 对象类型
console.log("对象类型", jsFile instanceof Array)
javaFile.remarkUpd = javaFile.remark
Object.assign(jsFile, javaFile)
}
}
// 从Comps中收集fileId
function fileIds(fileList) {
return fileList.map(comp => comp.fileId).join(',')
}
export default {
// onChange时缓存
cache,
// 上传文件
upload,
// 上传文件
uploads,
// 上传文件
upOnChg,
// onChange时上传
upOnChg,
// 添加到组件列表
add,
// 从组件列表中删除组件
del,
// 文件Java对象与js对象转换
trans,
// 从Comps中收集fileId
fileIds
}

View File

@ -0,0 +1,22 @@
import { useEventListener } from '@vueuse/core'
/*
* 显示页面遮罩
*/
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))
}
/*
* 隐藏页面遮罩
*/
export const closeShade = function (closeCallBack = () => { }) {
const shadeEl = document.querySelector('.layout-shade')
shadeEl && shadeEl.remove()
closeCallBack()
}

View File

@ -0,0 +1,639 @@
import axios from "axios"
import { ElMessage } from 'element-plus'
import { Greet } from '@/../wailsjs/go/main/App.js'
/** axios start */
// 创建 axios 实例
const $axios = axios.create({
baseURL: "api",
timeout: 12000
})
// 请求拦截器
$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)
}
)
// 响应拦截器
$axios.interceptors.response.use(
(response) => {
// console.log("response:", response)
if (response.status == 200) {
return response.data
} else {
pop("请求错误:" + response.status)
}
},
(error) => {
console.log("error:" + JSON.stringify(error))
if (error.response == undefined || error.response == null) {
pop("未知请求错误!")
} else if (error.response.status == 500) {
pop("请求后台服务异常,请稍后重试!")
} else {
pop("请求错误:" + 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 调用 go 接口
*/
function greet(name) {
return Greet(name).then(resp => {
console.log("greet resp:", resp)
return resp
})
}
/**
* 判断对象为空
*/
function isNull(obj) {
return obj == undefined || obj == null
}
/**
* 判断对象非空
*/
function notNull(obj) {
return obj != undefined && obj != null
}
/**
* 判断空字符串
*/
function isBlank(str) {
return str == undefined || str == null || /^s*$/.test(str)
}
/**
* 判断不为空字符串
*/
function notBlank(str) {
return !isBlank(str)
}
/**
* 判断数组为空
*/
function isEmpty(arr) {
return arr == undefined || arr == null || (arr instanceof Array && arr.length == 0)
}
/**
* 判断数组非空
*/
function notEmpty(arr) {
return arr != undefined && arr != null && arr instanceof Array && arr.length > 0
}
/**
* 判断对象为true
*/
function isTrue(obj) {
return obj == true || obj == 'true'
}
/**
* 判断对象为false
*/
function isFalse(obj) {
return !isTrue(obj)
}
/**
* @param {string} str - 要搜索的字符串
* @param {string} char - 要查找的字符
* @returns {number} - 字符在字符串中出现的次数
*/
function getCharCount(str, char) {
// 使用g表示整个字符串都要匹配
var regex = new RegExp(char, 'g')
// match方法可在字符串内检索指定的值或找到一个或多个正则表达式的匹配
var result = str.match(regex)
var count = !result ? 0 : result.length
return count
}
/**
* 日期格式化
* 默认格式为yyyy-MM-dd HH:mm:ss
*/
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)
}
/**
* 遍历对象中的日期,并进行格式化
*/
function fomateDateProperty(obj) {
for (let i in obj) {
//遍历对象中的属性
if (obj[i] == null) {
continue
} else if (obj[i] instanceof Date) {
// 格式化为yyyy-MM-dd HH:mm:ss
obj[i] = dateFormat(obj[i])
} else if (obj[i].constructor === Object) {
//如果发现该属性的值还是一个对象,再判空后进行迭代调用
if (Object.keys(obj[i]).length > 0) {
//判断对象上是否存在属性,如果为空对象则删除
fomateDateProperty(obj[i])
}
} else if (obj[i].constructor === Array) {
//对象值如果是数组,判断是否为空数组后进入数据遍历判空逻辑
if (obj[i].length > 0) {
for (let j = 0; j < obj[i].length; j++) {
//遍历数组
fomateDateProperty(obj[i][j])
}
}
}
}
}
// 遍历删除对象中的空值属性
function delNullProperty(obj) {
for (let i in obj) {
//遍历对象中的属性
if (obj[i] === undefined || obj[i] === null || obj[i] === "") {
//首先除去常规空数据用delete关键字
delete obj[i]
} else if (obj[i].constructor === Object) {
//如果发现该属性的值还是一个对象,再判空后进行迭代调用
if (Object.keys(obj[i]).length === 0) delete obj[i]
//判断对象上是否存在属性,如果为空对象则删除
delNullProperty(obj[i])
} else if (obj[i].constructor === Array) {
//对象值如果是数组,判断是否为空数组后进入数据遍历判空逻辑
if (obj[i].length === 0) {
//如果数组为空则删除
delete obj[i]
} else {
for (let index = 0; index < obj[i].length; index++) {
//遍历数组
if (obj[i][index] === undefined || obj[i][index] === null || obj[i][index] === "" || JSON.stringify(obj[i][index]) === "{}") {
obj[i].splice(index, 1)
//如果数组值为以上空值则修改数组长度,移除空值下标后续值依次提前
index--
//由于数组当前下标内容已经被替换成下一个值,所以计数器需要自减以抵消之后的自增
}
if (obj[i].constructor === Object) {
//如果发现数组值中有对象,则再次进入迭代
delNullProperty(obj[i])
}
}
}
}
}
}
/**
* 弹出消息框
* @param msg 消息内容
* @param type
*/
function pop(msg, type) {
ElMessage({ message: msg, type: type })
}
function popNoData(data) {
if (data == undefined || data == null || (data instanceof Array && data.length == 0)) {
ElMessage("暂无数据!")
}
}
/**
* 当前时间字符串
*/
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
}
/**
* 构建分页
*/
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)
}
/**
* 清空数组
*/
function clearArray(arr) {
if (arr == undefined || arr == null || arr.length == 0) {
return
}
arr.splice(0, arr.length)
}
/**
* 清空属性
*/
function clearProps(obj) {
if (obj == undefined || obj == null) {
return
}
for (let i in obj) {
obj[i] = null
}
}
/**
* 复制对象属性
*/
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)
}
}
/**
* 复制数组
*/
function copyArray(source, target) {
if (target == undefined || target == null) {
return
}
// 先清空数组
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)
}
}
/**
* 发生变更的属性
*/
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
}
/**
* 是否存在不同属性
*/
function hasDfProps(origin, target) {
const df = dfProps(origin, target)
for (let i in df) {
if (df[i] != null) {
return true
}
}
return false
}
/**
* 所有字段为空
*/
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 '新增'
}
if ('UPD' == label) {
return '更新'
}
if ('DEL' == label) {
return '删除'
}
return label
}
/**
* 重试调用
*/
function retry(method) {
const params = []
for (var i = 1; i < arguments.length; i++) {
params.push(arguments[i])
}
setTimeout(() => {
method(params)
}, 500)
}
/**
* 根据opts编码匹配中文
*/
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
}
/** 下划线转首字母小写驼峰 */
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("")
}
/** 防抖函数 */
function debounce(func, delay) {
let timer
return function () {
const context = this
const args = arguments
clearTimeout(timer)
timer = setTimeout(() => {
func.apply(context, args)
}, delay)
}
}
export default {
/**
* http请求 GET请求
*/
get,
/**
* http请求, 异步等待 GET请求
*/
awaitGet,
/**
* http请求 POST请求
*/
post,
/**
* http请求, 异步等待 POST请求
*/
awaitPost,
/**
* http请求 DELETE请求
*/
del,
/**
* http请求, 异步等待 DELETE请求
*/
awaitDel,
/**
* 判断对象为空
*/
isNull,
/**
* 判断对象非空
*/
notNull,
isBlank,
notBlank,
/**
* 判断数组为空
*/
isEmpty,
/**
* 判断数组非空
*/
notEmpty,
isTrue,
isFalse,
getCharCount,
/**
* 弹出消息提示
*/
pop,
/**
* 判定数据是否为空, 如果为空则提示暂无数据
*/
popNoData,
/**
* 遍历删除对象中的空值属性
*/
delNullProperty,
/**
*
* 当前时间字符串
*/
nowDatetimeStr,
/**
* 构建分页
*/
buildPage,
/**
* 清空数组
*/
clearArray,
/**
* 清空属性
*/
clearProps,
/**
* 复制对象属性
*/
copyProps,
/**
* 复制数组
*/
copyArray,
/**
* 日期格式化
* 默认格式为yyyy-MM-dd HH:mm:ss
*/
dateFormat,
/**
* 遍历对象中的日期,并进行格式化
*/
fomateDateProperty,
/**
* 发生变更的属性
*/
dfProps,
hasDfProps,
isAllPropsNull,
colorByLabel,
descByLabel,
/**
* 重试调用
*/
retry,
resolveLabelFromOpts,
underScoreToCamelCase,
debounce,
}

View File

@ -0,0 +1,204 @@
import utils from '@/assets/js/utils'
/** 英文编码正则 */
const codeReg = /^[A-Za-z0-9_\-\.]+$/
/** 手机号正则 */
const mobileReg = /^1[3456789]\d{9}$/
/** 大陆身份证正则 */
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])$)$/
/** 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('输入不能为空'))
} else {
callback()
}
}
const nameValidator = (rule, value, callback) => {
if (utils.isBlank(value)) {
callback()
} else if (value.length > 50) {
callback(new Error('字符数不能超过50'))
} else {
callback()
}
}
const mobileValidator = (rule, value, callback) => {
if (utils.isNull(value)) {
callback()
} else if (!mobileReg.test(value)) {
callback(new Error('手机号格式错误'))
} else {
callback()
}
}
const idNoValidator = (rule, value, callback) => {
if (utils.isNull(value)) {
callback()
} else if (!idNoReg.test(value)) {
callback(new Error('手机号格式错误'))
} else {
callback()
}
}
const emailValidator = (rule, value, callback) => {
if (utils.isNull(value)) {
callback()
} else if (!emailReg.test(value)) {
callback(new Error('手机号格式错误'))
} else {
callback()
}
}
const codeValidator = (rule, value, callback) => {
if (utils.isBlank(value)) {
callback()
} else if (!codeReg.test(value)) {
callback(new Error('编码格式错误'))
} else {
callback()
}
}
const intValidator = (rule, value, callback) => {
if (utils.isBlank(value)) {
callback()
} else if (!Number.isInteger(value)) {
callback(new Error('请输入整数'))
} else {
callback()
}
}
function validator() {
console.log("arguments:", arguments)
if (arguments.length <= 1) {
const type = arguments[0]
// 默认校验逻辑, 不含有特殊字符
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
}
}
// 复合校验器
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('输入不能为空'))
break
} else if (typeStr == 'code' && !codeReg.test(value)) {
callback(new Error('编码格式错误'))
break
} else if (typeStr == 'int' && Number.isInteger(value)) {
callback(new Error('请输入整数'))
break
}
}
// 兜底callback()只会触发一次
callback()
}
return complexValidator
}
export default {
username: (username) => {
if (typeof (username) == "undefined" || username == null) {
return "账号不能为空"
}
username = username.trim()
if (username.length < 4) {
return "账号字符不能小于4位"
}
if (username.length > 20) {
return "账号字符不能大于20位"
}
const reg = /^[A-Za-z0-9]+$/
if (!reg.test(username)) {
return "账号为必须为字母和数字"
}
return null
},
password: (password) => {
if (typeof (password) == "undefined" || password == null) {
return "密码不能为空"
}
password = password.trim()
if (password.length < 4) {
return "密码字符不能小于4位"
}
if (password.length > 20) {
return "密码字符不能大于20位"
}
const reg = /^[A-Za-z0-9\.\-\_\+]+$/
if (!reg.test(password)) {
return "密码为必须为字母和数字或.-+_"
}
return null
},
email: (email) => {
if (typeof (email) == "undefined" || email == null) {
return "邮箱不能为空"
}
const reg = /^[A-Za-z0-9._%-]+@([A-Za-z0-9-]+\.)+[A-Za-z]{2,4}$/
if (!reg.test(email)) {
return "邮箱格式不正确"
}
return null
},
validCode: (validCode) => {
if (typeof (validCode) == "undefined" || validCode == null) {
return "验证码不能为空"
}
validCode = validCode.trim()
if (validCode.length != 6) {
return "验证码必须为6位"
}
const reg = /^[A-Za-z0-9]{6}$/
if (!reg.test(validCode)) {
return "验证码格式不正确"
}
return null
},
validator,
}

View File

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

View File

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

View File

@ -0,0 +1,54 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { terser } from 'rollup-plugin-terser'
// 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,
// 分解块,将大块分解成更小的块
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
// 让每个插件都打包成独立的文件
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
},
// 单位b, 合并较小模块
experimentalMinChunkSize: 10 * 1024,
}
},
}
})

View File

@ -0,0 +1,292 @@
<template>
<el-menu class="el-menu-custom" :default-active="activeMenu()" :collapse="menuCollapse" @open="handleOpen"
@close="handleClose">
<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 { ChatDotRound, List, Clock, Setting } from '@element-plus/icons-vue'
import { ref, inject, onMounted, reactive, 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 { menuCollapse } = storeToRefs(config)
const handleOpen = (key, keyPath) => {
// console.log(key, keyPath)
}
const handleClose = (key, keyPath) => {
// console.log(key, keyPath)
}
//
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"
},
{
index: "M9903",
menuName: "menu.config.theme",
href: "/config/theme"
}
]
},
]
onMounted(() => {
// ,
// 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('LeftMenurouter.currentRoute.value.path', newValue, oldValue)
// ,
activeMenu()
})
//
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
}
return "1"
}
// index
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
}
}
// path,to
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("node.to:", node.to)
return thdMenu.index
}
}
}
}
}
// ,to
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
}
}
}
}
// ,to
}
}
// routes
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 {
/* 防止双击选中 */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
span {
/* 字体大小 */
font-size: 16px;
}
li {
/* 字体大小 */
font-size: 15px;
}
/** 菜单折叠时hover菜单项高度这里必须再定义一次 */
.el-menu-item {
min-width: 44px;
height: 36px;
line-height: 36px;
}
.el-menu-custom {
border-right: none;
--el-menu-item-height: 40px;
--el-menu-sub-item-height: 36px;
padding-top: 10px;
padding-bottom: 10px;
}
.el-menu-custom .el-menu--collapse {
width: 44px;
}
.el-menu-custom:not(.el-menu--collapse) {
width: 200px;
}
</style>

View File

@ -0,0 +1,240 @@
<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-20 pr-4">
<img src="@/assets/img/logo-sm.png" class="fxc" height="34px" 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%;">
<!-- 路由展示区 -->
<!-- { Component }指当前路由所对应的组件 -->
<RouterView v-slot="{ Component }">
<!-- 添加过渡动画 需要确保插入的component元素只有一个根节点, 否则报错. component中的根元素的transition会覆盖transitionName的样式
而且需要保证component中根元素的宽度相同所以最好是统一给component添加一个根元素 -->
<transition :name="transitionName">
<KeepAlive>
<Component :is="Component" v-if="keepAlive" :key="$route.path" />
</KeepAlive>
</transition>
<!-- 添加过渡动画 需要确保插入的component元素只有一个根节点 -->
<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'
const router = useRouter()
const config = useConfig()
const { shrink, menuCollapse } = storeToRefs(config)
const currentRoute = reactive(router.currentRoute)
// ,
let transitionName = 'slide-left'
const keepAlive = computed(() => {
return currentRoute.value.meta.keepAlive
})
/** 固定菜单头展开折叠动画时间 刷新页面时菜单不会展开或折叠, 设置持续时间为0, 不产生动画 */
const menuAnimationDuration = ref(0)
//
function menuToggle() {
menuAnimationDuration.value = '300ms'
if (menuCollapse.value) {
// console.log(", ")
if (shrink.value) {
// ,
showShade(() => {
// console.log(", , ")
config.setMenuCollapse(true)
})
}
} else {
// console.log(", , ")
closeShade()
}
//
config.setMenuCollapse(!menuCollapse.value)
}
function onAdaptiveLayout() {
//
const clientWidth = document.body.clientWidth
// console.log("menuCollapse:", menuCollapse.value, config.getMenuCollapse(), "clientWidth:", clientWidth)
// aside
if (clientWidth < 800) {
config.setShrink(true)
if (!menuCollapse.value) {
// ,
menuToggle()
}
} else {
config.setShrink(false)
}
}
onBeforeMount(() => {
onAdaptiveLayout()
useEventListener(window, 'resize', onAdaptiveLayout)
})
watch(() => router.currentRoute.value.path, (newValue, oldValue) => {
// console.log(",,:", newValue, oldValue)
if (shrink.value && !menuCollapse.value) {
// console.log(", , ")
menuToggle()
}
})
function refresh() {
// console.log("")
location.reload()
}
</script>
<style scoped>
.layout-container {
height: 100vh;
}
header {
width: 100%;
height: 44px;
padding: 0px;
background-color: var(--el-bg-color);
display: flex;
justify-content: center;
align-items: center;
}
aside {
background-color: var(--el-fg-color);
}
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;
}
.menu-logo {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/** 菜单折叠 */
@keyframes menuCollapse {
0% {
width: 200px;
}
100% {
width: 44px;
}
}
/** 菜单展开 */
@keyframes menuExpand {
0% {
width: 44px;
}
100% {
width: 200px;
}
}
.fixed-menu-collapse {
position: fixed;
z-index: 9999;
height: 44px;
width: 44px;
/* 引用上面定义的@keyframes名称 */
animation-name: menuCollapse;
/* 动画持续时间 */
animation-duration: v-bind('menuAnimationDuration');
animation-timing-function: ease-in-out;
background-color: var(--el-fg-color);
}
.fixed-menu-expand {
position: fixed;
z-index: 9999;
height: 44px;
width: 200px;
/* 引用上面定义的@keyframes名称 */
animation-name: menuExpand;
/* 动画持续时间 */
animation-duration: v-bind('menuAnimationDuration');
animation-timing-function: ease-in-out;
background-color: var(--el-fg-color);
z-index: 9999999
}
.scrollbar-menu-wrapper {
top: 44px;
height: calc(100vh - 44px);
background-color: var(--el-fg-color);
}
.scrollbar-menu-wrapper.shrink {
position: fixed;
left: 0;
z-index: 9999999
}
</style>

View File

@ -0,0 +1,123 @@
<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="primary" @click="toAddPage" style="vertical-align: middle;">
<el-icon :size="20" class="pr-4">
<Plus />
</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>
删除
</el-button>
</div>
<div v-show="advSearch">
<el-button @click="resetSearch">重置</el-button>
<el-button type="primary" @click="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'
const props = defineProps({
advSearch: {
default: false
},
searchForm: {
default: () => ({
kw: null
})
},
tableColumns: {
default: () => []
},
selectedRows: {
default: []
},
addable: {
default: false
}
})
const emits = defineEmits([
'search',
'baseSearch',
'advSearchSwitch',
'checkTableColumn',
'delSelected',
'resetSearch',
'toAddPage',
])
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')
}
const toAddPage = () => {
emits('toAddPage')
}
</script>
<style scoped>
.table-tools {
width: 100%;
}
</style>

View File

@ -0,0 +1,127 @@
<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>
<!-- 右侧固定下拉 -->
<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>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { ArrowDown, Refresh } from '@element-plus/icons-vue'
import { useConfig } from '@/store/config'
const config = useConfig()
const modelList = ref(config.modelList)
const selectedModel = ref(config.selectedModel != null ? config.selectedModel : modelList.value[0])
function handleSwitchModel(mod) {
// console.log("handleSwitchModel:", model)
selectedModel.value = 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()
}
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: 80px;
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;
}
</style>

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>

View File

@ -0,0 +1,25 @@
export default {
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",
clearCacheSuccess: "Clear cache success",
openManusAgiTips: "The above content is generated by OpenManus for reference only",
taskStatus: {
name: "Task Status",
success: "Success",
failed: "Failed",
running: "Running",
terminated: "Terminated",
}
}

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

View File

@ -0,0 +1,24 @@
export default {
menu: {
task: "任务",
history: "历史记录",
config: {
settings: "设置",
general: "通用设置",
llm: "大模型设置",
theme: "主题设置"
}
},
user: '用户',
step: "步骤",
promptInputPlaceHolder: "请输入任务提示词",
clearCacheSuccess: "清理缓存成功",
openManusAgiTips: "以上内容由OpenManus生成, 仅供参考和借鉴",
taskStatus: {
name: "任务状态",
success: "成功",
failed: "失败",
running: "运行中",
terminated: "终止",
}
}

View File

@ -0,0 +1,55 @@
import './assets/css/main.css'
import utils from '@/assets/js/utils'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import files from '@/assets/js/files'
import verify from '@/assets/js/verify'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import i18n from './locales/i18n'
// import ElementPlus from 'element-plus'
import { ElMessage } from 'element-plus'
import 'element-plus/dist/index.css'
/* 暗黑主题模式 */
import 'element-plus/theme-chalk/dark/css-vars.css'
import '@/assets/less/light.css'
import '@/assets/less/dark.css'
// 定义特性标志
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)
// 全局引用router
app.use(router)
app.use(i18n)
// ElMessage需要在utils中使用,这里单独引入
app.use(ElMessage)
// 全局使用
// ElSelect.props.placeholder.default = '请选择'
// 在引入 ElementPlus 时,可以传入一个包含 size 和 zIndex 属性的全局配置对象。
// size 用于设置表单组件的默认尺寸zIndex 用于设置弹出组件的层级zIndex 的默认值为 2000。
// app.use(ElementPlus, { locale, size: 'default', zIndex: 2000 })
// 使用vue3 provide注册
app.provide('utils', utils)
app.provide('files', files)
app.provide('verify', verify)
/* app.provide('uuid', uuidv4) */
app.mount('#app')

View File

@ -0,0 +1,74 @@
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
component: () => import('@/components/MainFrame.vue'),
meta: {
title: "主页"
},
// 重定向到默认页面
redirect: '/task',
children: [
{
path: 'task',
component: () => import('@/views/main/Task.vue'),
meta: {
keepAlive: false,
title: "任务",
index: 0
}
},
{
path: 'history',
component: () => import('@/views/main/Home.vue'),
meta: {
keepAlive: false,
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: 'theme',
component: () => import('@/views/config/Theme.vue'),
meta: {
keepAlive: false,
title: "主题",
index: 1
}
},
]
},
]
})
export default router

View File

@ -0,0 +1,96 @@
import { defineStore } from "pinia"
export const useConfig = defineStore("config", {
state: () => {
return {
// 全局
// aside是否收缩
shrink: false,
isDark: false,
// 侧边栏
// 菜单是否折叠
menuCollapse: false,
selectedModel: null,
modelList: ['qwen2.5-7b', 'deepseek-r1-7b'],
selectedLang: { code: 'en', name: 'English' },
langList: [{ code: 'en', name: 'English' }, { code: 'zhCn', name: '简体中文' }],
taskHistory: [
// taskId, prompt, stepList, status, createdDt
]
}
},
actions: {
getShrink() {
return this.shrink
},
setShrink(shrink) {
this.shrink = shrink
},
getIsDark() {
return this.isDark
},
getMenuCollapse() {
return this.menuCollapse
},
setMenuCollapse(menuCollapse) {
this.menuCollapse = menuCollapse
},
getSelectedModel() {
return this.selectedModel
},
setSelectedModel(selectedModel) {
this.selectedModel = selectedModel
},
getModelList() {
return this.modelList
},
setModelList(modelList) {
utils.copyArray(modelList, this.modelList)
},
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]
},
},
persist: {
key: "config",
}
})

View File

@ -0,0 +1,35 @@
<template>
<div class="main-content">
<el-card>
<template #header>
<div class="title fxsb">
<div>基本信息</div>
<div>
<el-link type="primary" class="no-select plr-6" @click="clearCache()">清理缓存</el-link>
</div>
</div>
</template>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, inject, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
const utils = inject('utils')
const router = useRouter()
const config = useConfig()
const { t } = useI18n()
function clearCache() {
config.$reset()
utils.pop(t('clearCacheSuccess'))
}
</script>
<style scoped></style>

View File

@ -0,0 +1,32 @@
<template>
<div class="main-content">
<el-card>
<template #header>
<div class="title fxsb">
<div>基本信息</div>
<div>
<el-link type="primary" class="no-select plr-6" @click="clearCache()">清理缓存</el-link>
</div>
</div>
</template>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, inject, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import {useConfig} from '@/store/config'
const utils = inject('utils')
const router = useRouter()
const config = useConfig()
function clearCache() {
config.$reset()
}
</script>
<style scoped></style>

View File

@ -0,0 +1,32 @@
<template>
<div class="main-content">
<el-card>
<template #header>
<div class="title fxsb">
<div>基本信息</div>
<div>
<el-link type="primary" class="no-select plr-6" @click="clearCache()">清理缓存</el-link>
</div>
</div>
</template>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, inject, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import {useConfig} from '@/store/config'
const utils = inject('utils')
const router = useRouter()
const config = useConfig()
function clearCache() {
config.$reset()
}
</script>
<style scoped></style>

View File

@ -0,0 +1,408 @@
<template :lang="i18n.locale">
<div class="main-content fc">
<el-scrollbar ref="scrollRef">
<div class="output-area" v-show="taskInfo.taskId != null">
<div class="dialog-user">
<div class="blank"></div>
<div class="content">
<el-text class="title">
{{ t('user') }}
</el-text>
<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>
<el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text>
<el-text>{{ t(taskInfo.status) }}</el-text>
</div>
</div>
</el-scrollbar>
<div class="input-area">
<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">
<Promotion />
</el-icon>
<el-icon @click="stop" :size="24" v-show="loading">
<CircleClose />
</el-icon>
</el-link>
</div>
<div>
<el-text class="tips">{{ t('openManusAgiTips') }}</el-text>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
import { FolderAdd, Promotion, Eleme, CircleClose } from '@element-plus/icons-vue'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
import i18n from '@/locales/i18n'
const utils = inject('utils')
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 taskInfo = computed(() => {
return config.getCurrTask()
})
const loading = ref(false)
const scrollRef = ref(null)
// EventSource
const buildEventSource = (taskId) => {
loading.value = true
eventSource.value = new EventSource('http://localhost:5172/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"
utils.pop("任务执行失败", "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
eventSource.value.close()
taskInfo.value.status = "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('http://localhost:5172/tasks/' + taskId).then(data => {
console.log("task info resp:", data)
buildStepList(data.steps)
console.log("stepList:", taskInfo.value.stepList)
//
setTimeout(() => {
scrollToBottom()
}, 100)
})
}
// 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]
if (taskInfo.value.stepList.length < stepNo) {
// stepstepList
const parentStep = {
type: "log",
idx: idx,
stepNo: stepNo,
result: stepStr,
subList: [],
createdDt: utils.dateFormat(new Date())
}
taskInfo.value.stepList.push(parentStep)
return
}
} else {
//
const subStep = {
type: step.type,
idx: idx,
result: step.result,
createdDt: utils.dateFormat(new Date())
}
// stepListsubList
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)
return
}
}
})
}
onUnmounted(() => {
// EventSource
if (eventSource.value) {
eventSource.value.close()
}
})
function handleInputEnter(event) {
console.log("handleInputEnter:", event)
event.preventDefault()
sendPrompt()
}
function uploadFile() {
utils.pop("暂不支持,开发中", "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 (eventSource.value != null) {
eventSource.value.close()
}
if (utils.isBlank(prompt.value)) {
utils.pop("Please enter a valid prompt", "error")
promptEle.value.focus()
return
}
utils.post('http://localhost:5172/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)
//
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
eventSource.value.close()
taskInfo.value.status = "terminated"
utils.pop("用户终止任务", "error")
}
</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 {
width: 100%;
}
.dialog-ai {
margin-bottom: 16px;
background-color: var(--el-fg-color);
border-radius: 12px;
}
.dialog-ai .title {
margin: 6px 12px;
font-size: 15px;
}
.input-area {
flex-grow: 0;
width: 100%;
max-height: 180px;
padding-left: 80px;
padding-right: 80px;
padding-top: 12px;
padding-bottom: 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.input-box {
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 {
color: var(--el-text-color-secondary);
font-size: 12px;
padding-top: 10px;
}
.sub-step-time {
color: var(--el-text-color-secondary);
font-size: 12px;
}
</style>

View File

@ -0,0 +1,407 @@
<template :lang="i18n.locale">
<div class="main-content fc">
<el-scrollbar ref="scrollRef" style="width: 100%;">
<div class="output-area" v-show="taskInfo.taskId != null">
<div class="dialog-user">
<div class="blank"></div>
<div class="content">
<el-text class="title">
{{ t('user') }}
</el-text>
<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')">
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>
<el-text class="pr-10">任务状态:</el-text>
<el-text>{{ taskInfo.status }}</el-text>
</div>
</div>
</el-scrollbar>
<div class="input-area">
<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="请输入指令" @keydown.enter="handleInputEnter" />
<el-link class="send-area">
<el-icon @click="sendPrompt" :size="24" v-show="!loading">
<Promotion />
</el-icon>
<el-icon @click="stop" :size="24" v-show="loading">
<CircleClose />
</el-icon>
</el-link>
</div>
<div>
<el-text class="tips">以上内容由OpenManus生成, 仅供参考和借鉴</el-text>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
import { FolderAdd, Promotion, Eleme, CircleClose } from '@element-plus/icons-vue'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
import i18n from '@/locales/i18n'
const utils = inject('utils')
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 taskInfo = computed(() => {
return config.getCurrTask()
})
const loading = ref(false)
const scrollRef = ref(null)
// EventSource
const buildEventSource = (taskId) => {
loading.value = true
eventSource.value = new EventSource('http://localhost:5172/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"
utils.pop("任务执行失败", "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
eventSource.value.close()
taskInfo.value.status = "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('http://localhost:5172/tasks/' + taskId).then(data => {
console.log("task info resp:", data)
buildStepList(data.steps)
console.log("stepList:", taskInfo.value.stepList)
//
setTimeout(() => {
scrollToBottom()
}, 100)
})
}
// 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]
if (taskInfo.value.stepList.length < stepNo) {
// stepstepList
const parentStep = {
type: "log",
idx: idx,
stepNo: stepNo,
result: stepStr,
subList: [],
createdDt: utils.dateFormat(new Date())
}
taskInfo.value.stepList.push(parentStep)
return
}
} else {
//
const subStep = {
type: step.type,
idx: idx,
result: step.result,
createdDt: utils.dateFormat(new Date())
}
// stepListsubList
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)
return
}
}
})
}
onUnmounted(() => {
// EventSource
if (eventSource.value) {
eventSource.value.close()
}
})
function handleInputEnter(event) {
console.log("handleInputEnter:", event)
event.preventDefault()
sendPrompt()
}
function uploadFile() {
utils.pop("暂不支持,开发中", "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 (eventSource.value != null) {
eventSource.value.close()
}
if (utils.isBlank(prompt.value)) {
utils.pop("Please enter a valid prompt", "error")
promptEle.value.focus()
return
}
utils.post('http://localhost:5172/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)
//
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
eventSource.value.close()
taskInfo.value.status = "terminated"
utils.pop("用户终止任务", "error")
}
</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 {
width: 100%;
}
.dialog-ai {
margin-bottom: 16px;
background-color: var(--el-fg-color);
border-radius: 12px;
}
.dialog-ai .title {
margin: 6px 12px;
font-size: 15px;
}
.input-area {
flex-grow: 0;
width: 100%;
max-height: 180px;
padding-left: 80px;
padding-right: 80px;
padding-top: 12px;
padding-bottom: 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.input-box {
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 {
color: var(--el-text-color-secondary);
font-size: 12px;
padding-top: 10px;
}
.sub-step-time {
color: var(--el-text-color-secondary);
font-size: 12px;
}
</style>

View File

@ -0,0 +1,408 @@
<template :lang="i18n.locale">
<div class="main-content fc">
<el-scrollbar ref="scrollRef">
<div class="output-area" v-show="taskInfo.taskId != null">
<div class="dialog-user">
<div class="blank"></div>
<div class="content">
<el-text class="title">
{{ t('user') }}
</el-text>
<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>
<el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text>
<el-text>{{ taskInfo.status }}</el-text>
</div>
</div>
</el-scrollbar>
<div class="input-area">
<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">
<Promotion />
</el-icon>
<el-icon @click="stop" :size="24" v-show="loading">
<CircleClose />
</el-icon>
</el-link>
</div>
<div>
<el-text class="tips">{{ t('openManusAgiTips') }}</el-text>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
import { FolderAdd, Promotion, Eleme, CircleClose } from '@element-plus/icons-vue'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
import i18n from '@/locales/i18n'
const utils = inject('utils')
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 taskInfo = computed(() => {
return config.getCurrTask()
})
const loading = ref(false)
const scrollRef = ref(null)
// EventSource
const buildEventSource = (taskId) => {
loading.value = true
eventSource.value = new EventSource('http://localhost:5172/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 = "taskStatus.failed"
utils.pop("任务执行失败", "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
eventSource.value.close()
taskInfo.value.status = "taskStatus.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('http://localhost:5172/tasks/' + taskId).then(data => {
console.log("task info resp:", data)
buildStepList(data.steps)
console.log("stepList:", taskInfo.value.stepList)
//
setTimeout(() => {
scrollToBottom()
}, 100)
})
}
// 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]
if (taskInfo.value.stepList.length < stepNo) {
// stepstepList
const parentStep = {
type: "log",
idx: idx,
stepNo: stepNo,
result: stepStr,
subList: [],
createdDt: utils.dateFormat(new Date())
}
taskInfo.value.stepList.push(parentStep)
return
}
} else {
//
const subStep = {
type: step.type,
idx: idx,
result: step.result,
createdDt: utils.dateFormat(new Date())
}
// stepListsubList
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)
return
}
}
})
}
onUnmounted(() => {
// EventSource
if (eventSource.value) {
eventSource.value.close()
}
})
function handleInputEnter(event) {
console.log("handleInputEnter:", event)
event.preventDefault()
sendPrompt()
}
function uploadFile() {
utils.pop("暂不支持,开发中", "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 (eventSource.value != null) {
eventSource.value.close()
}
if (utils.isBlank(prompt.value)) {
utils.pop("Please enter a valid prompt", "error")
promptEle.value.focus()
return
}
utils.post('http://localhost:5172/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)
//
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
eventSource.value.close()
taskInfo.value.status = "taskStatus.terminated"
utils.pop("用户终止任务", "error")
}
</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 {
width: 100%;
}
.dialog-ai {
margin-bottom: 16px;
background-color: var(--el-fg-color);
border-radius: 12px;
}
.dialog-ai .title {
margin: 6px 12px;
font-size: 15px;
}
.input-area {
flex-grow: 0;
width: 100%;
max-height: 180px;
padding-left: 80px;
padding-right: 80px;
padding-top: 12px;
padding-bottom: 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.input-box {
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 {
color: var(--el-text-color-secondary);
font-size: 12px;
padding-top: 10px;
}
.sub-step-time {
color: var(--el-text-color-secondary);
font-size: 12px;
}
</style>

View File

@ -0,0 +1,54 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { terser } from 'rollup-plugin-terser'
// 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,
// 分解块,将大块分解成更小的块
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
// 让每个插件都打包成独立的文件
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
},
// 单位b, 合并较小模块
experimentalMinChunkSize: 10 * 1024,
}
},
}
})

View File

@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Greet(arg1:string):Promise<string>;

View File

@ -0,0 +1,7 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}

View File

@ -0,0 +1,24 @@
{
"name": "@wailsapp/runtime",
"version": "2.0.0",
"description": "Wails Javascript runtime library",
"main": "runtime.js",
"types": "runtime.d.ts",
"scripts": {
},
"repository": {
"type": "git",
"url": "git+https://github.com/wailsapp/wails.git"
},
"keywords": [
"Wails",
"Javascript",
"Go"
],
"author": "Lea Anthony <lea.anthony@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/wailsapp/wails/issues"
},
"homepage": "https://github.com/wailsapp/wails#readme"
}

View File

@ -0,0 +1,249 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export interface Position {
x: number;
y: number;
}
export interface Size {
w: number;
h: number;
}
export interface Screen {
isCurrent: boolean;
isPrimary: boolean;
width : number
height : number
}
// Environment information such as platform, buildtype, ...
export interface EnvironmentInfo {
buildType: string;
platform: string;
arch: string;
}
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
// emits the given event. Optional data may be passed with the event.
// This will trigger any event listeners.
export function EventsEmit(eventName: string, ...data: any): void;
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
// sets up a listener for the given event name, but will only trigger a given number times.
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
// sets up a listener for the given event name, but will only trigger once.
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
// unregisters the listener for the given event name.
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message
export function LogPrint(message: string): void;
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
// logs the given message at the `trace` log level.
export function LogTrace(message: string): void;
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
// logs the given message at the `debug` log level.
export function LogDebug(message: string): void;
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
// logs the given message at the `error` log level.
export function LogError(message: string): void;
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
// logs the given message at the `fatal` log level.
// The application will quit after calling this method.
export function LogFatal(message: string): void;
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
// logs the given message at the `info` log level.
export function LogInfo(message: string): void;
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
// logs the given message at the `warning` log level.
export function LogWarning(message: string): void;
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
// Forces a reload by the main application as well as connected browsers.
export function WindowReload(): void;
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
// Reloads the application frontend.
export function WindowReloadApp(): void;
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
// Sets the window AlwaysOnTop or not on top.
export function WindowSetAlwaysOnTop(b: boolean): void;
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
// *Windows only*
// Sets window theme to system default (dark/light).
export function WindowSetSystemDefaultTheme(): void;
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
// *Windows only*
// Sets window to light theme.
export function WindowSetLightTheme(): void;
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
// *Windows only*
// Sets window to dark theme.
export function WindowSetDarkTheme(): void;
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
// Centers the window on the monitor the window is currently on.
export function WindowCenter(): void;
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
// Sets the text in the window title bar.
export function WindowSetTitle(title: string): void;
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
// Makes the window full screen.
export function WindowFullscreen(): void;
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
// Restores the previous window dimensions and position prior to full screen.
export function WindowUnfullscreen(): void;
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
export function WindowIsFullscreen(): Promise<boolean>;
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
// Sets the width and height of the window.
export function WindowSetSize(width: number, height: number): void;
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
// Gets the width and height of the window.
export function WindowGetSize(): Promise<Size>;
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMaxSize(width: number, height: number): void;
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMinSize(width: number, height: number): void;
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
// Sets the window position relative to the monitor the window is currently on.
export function WindowSetPosition(x: number, y: number): void;
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
// Gets the window position relative to the monitor the window is currently on.
export function WindowGetPosition(): Promise<Position>;
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
// Hides the window.
export function WindowHide(): void;
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
// Shows the window, if it is currently hidden.
export function WindowShow(): void;
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
// Maximises the window to fill the screen.
export function WindowMaximise(): void;
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
// Toggles between Maximised and UnMaximised.
export function WindowToggleMaximise(): void;
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
// Restores the window to the dimensions and position prior to maximising.
export function WindowUnmaximise(): void;
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
// Returns the state of the window, i.e. whether the window is maximised or not.
export function WindowIsMaximised(): Promise<boolean>;
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
// Minimises the window.
export function WindowMinimise(): void;
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
// Restores the window to the dimensions and position prior to minimising.
export function WindowUnminimise(): void;
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
// Returns the state of the window, i.e. whether the window is minimised or not.
export function WindowIsMinimised(): Promise<boolean>;
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
// Returns the state of the window, i.e. whether the window is normal or not.
export function WindowIsNormal(): Promise<boolean>;
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
export function ScreenGetAll(): Promise<Screen[]>;
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
// Opens the given URL in the system browser.
export function BrowserOpenURL(url: string): void;
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
// Returns information about the environment
export function Environment(): Promise<EnvironmentInfo>;
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
// Quits the application.
export function Quit(): void;
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
// Hides the application.
export function Hide(): void;
// [Show](https://wails.io/docs/reference/runtime/intro#show)
// Shows the application.
export function Show(): void;
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
// Returns the current text stored on clipboard
export function ClipboardGetText(): Promise<string>;
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
// Sets a text on the clipboard
export function ClipboardSetText(text: string): Promise<boolean>;
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
// OnFileDropOff removes the drag and drop listeners and handlers.
export function OnFileDropOff() :void
// Check if the file path resolver is available
export function CanResolveFilePaths(): boolean;
// Resolves file paths for an array of files
export function ResolveFilePaths(files: File[]): void

View File

@ -0,0 +1,238 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export function LogPrint(message) {
window.runtime.LogPrint(message);
}
export function LogTrace(message) {
window.runtime.LogTrace(message);
}
export function LogDebug(message) {
window.runtime.LogDebug(message);
}
export function LogInfo(message) {
window.runtime.LogInfo(message);
}
export function LogWarning(message) {
window.runtime.LogWarning(message);
}
export function LogError(message) {
window.runtime.LogError(message);
}
export function LogFatal(message) {
window.runtime.LogFatal(message);
}
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
}
export function EventsOn(eventName, callback) {
return EventsOnMultiple(eventName, callback, -1);
}
export function EventsOff(eventName, ...additionalEventNames) {
return window.runtime.EventsOff(eventName, ...additionalEventNames);
}
export function EventsOnce(eventName, callback) {
return EventsOnMultiple(eventName, callback, 1);
}
export function EventsEmit(eventName) {
let args = [eventName].slice.call(arguments);
return window.runtime.EventsEmit.apply(null, args);
}
export function WindowReload() {
window.runtime.WindowReload();
}
export function WindowReloadApp() {
window.runtime.WindowReloadApp();
}
export function WindowSetAlwaysOnTop(b) {
window.runtime.WindowSetAlwaysOnTop(b);
}
export function WindowSetSystemDefaultTheme() {
window.runtime.WindowSetSystemDefaultTheme();
}
export function WindowSetLightTheme() {
window.runtime.WindowSetLightTheme();
}
export function WindowSetDarkTheme() {
window.runtime.WindowSetDarkTheme();
}
export function WindowCenter() {
window.runtime.WindowCenter();
}
export function WindowSetTitle(title) {
window.runtime.WindowSetTitle(title);
}
export function WindowFullscreen() {
window.runtime.WindowFullscreen();
}
export function WindowUnfullscreen() {
window.runtime.WindowUnfullscreen();
}
export function WindowIsFullscreen() {
return window.runtime.WindowIsFullscreen();
}
export function WindowGetSize() {
return window.runtime.WindowGetSize();
}
export function WindowSetSize(width, height) {
window.runtime.WindowSetSize(width, height);
}
export function WindowSetMaxSize(width, height) {
window.runtime.WindowSetMaxSize(width, height);
}
export function WindowSetMinSize(width, height) {
window.runtime.WindowSetMinSize(width, height);
}
export function WindowSetPosition(x, y) {
window.runtime.WindowSetPosition(x, y);
}
export function WindowGetPosition() {
return window.runtime.WindowGetPosition();
}
export function WindowHide() {
window.runtime.WindowHide();
}
export function WindowShow() {
window.runtime.WindowShow();
}
export function WindowMaximise() {
window.runtime.WindowMaximise();
}
export function WindowToggleMaximise() {
window.runtime.WindowToggleMaximise();
}
export function WindowUnmaximise() {
window.runtime.WindowUnmaximise();
}
export function WindowIsMaximised() {
return window.runtime.WindowIsMaximised();
}
export function WindowMinimise() {
window.runtime.WindowMinimise();
}
export function WindowUnminimise() {
window.runtime.WindowUnminimise();
}
export function WindowSetBackgroundColour(R, G, B, A) {
window.runtime.WindowSetBackgroundColour(R, G, B, A);
}
export function ScreenGetAll() {
return window.runtime.ScreenGetAll();
}
export function WindowIsMinimised() {
return window.runtime.WindowIsMinimised();
}
export function WindowIsNormal() {
return window.runtime.WindowIsNormal();
}
export function BrowserOpenURL(url) {
window.runtime.BrowserOpenURL(url);
}
export function Environment() {
return window.runtime.Environment();
}
export function Quit() {
window.runtime.Quit();
}
export function Hide() {
window.runtime.Hide();
}
export function Show() {
window.runtime.Show();
}
export function ClipboardGetText() {
return window.runtime.ClipboardGetText();
}
export function ClipboardSetText(text) {
return window.runtime.ClipboardSetText(text);
}
/**
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
*
* @export
* @callback OnFileDropCallback
* @param {number} x - x coordinate of the drop
* @param {number} y - y coordinate of the drop
* @param {string[]} paths - A list of file paths.
*/
/**
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
*
* @export
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
*/
export function OnFileDrop(callback, useDropTarget) {
return window.runtime.OnFileDrop(callback, useDropTarget);
}
/**
* OnFileDropOff removes the drag and drop listeners and handlers.
*/
export function OnFileDropOff() {
return window.runtime.OnFileDropOff();
}
export function CanResolveFilePaths() {
return window.runtime.CanResolveFilePaths();
}
export function ResolveFilePaths(files) {
return window.runtime.ResolveFilePaths(files);
}

38
desktop/go.mod Normal file
View File

@ -0,0 +1,38 @@
module OpenManus
go 1.22.0
toolchain go1.24.1
require github.com/wailsapp/wails/v2 v2.10.1
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
golang.org/x/text v0.22.0 // indirect
)
// replace github.com/wailsapp/wails/v2 v2.9.2 => C:\Users\aylvn\go\pkg\mod

79
desktop/go.sum Normal file
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
desktop/main.go Normal file
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())
}
}

232
desktop/src/utils/http.go Normal file
View File

@ -0,0 +1,232 @@
package main
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
desktop/src/utils/log.go Normal file
View File

@ -0,0 +1,40 @@
package main
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...)
}

47
desktop/src/utils/str.go Normal file
View File

@ -0,0 +1,47 @@
package main
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
)
// 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)
}
}

34
desktop/wails-build.txt Normal file
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

13
desktop/wails.json Normal file
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"
}
}