@@ -181,3 +181,6 @@ workspace/
|
||||
|
||||
# Private Config
|
||||
config/config.toml
|
||||
|
||||
# Desktop runtime
|
||||
desktop/frontend/wailsjs/runtime/
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"OpenManus/src/utils"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
@@ -26,6 +28,44 @@ func NewApp() *App {
|
||||
// so we can call the runtime methods
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
|
||||
// 注册事件监听器
|
||||
runtime.EventsOn(ctx, "events", func(data ...interface{}) {
|
||||
if len(data) > 0 {
|
||||
for i := 0; i < len(data); i++ {
|
||||
fmt.Println("Received events with data:", data[i])
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Received events without data")
|
||||
}
|
||||
})
|
||||
|
||||
// 注册bat批处理事件监听器
|
||||
runtime.EventsOn(ctx, "bat", func(data ...interface{}) {
|
||||
if len(data) == 2 {
|
||||
fmt.Println("Received bat with data, batId: ", data[0])
|
||||
fmt.Println("Received bat with data, batPath: ", data[1])
|
||||
utils.ExecBatFile(a.ctx, data[0].(string), data[1].(string))
|
||||
} else if len(data) > 0 && len(data) < 2 {
|
||||
fmt.Println("Received bat with data, required 2 paramters, found 1: ", data[0])
|
||||
} else {
|
||||
fmt.Println("Received bat without data")
|
||||
}
|
||||
})
|
||||
|
||||
// 注册执行py脚本监听器
|
||||
runtime.EventsOn(ctx, "pyFile", func(data ...interface{}) {
|
||||
if len(data) == 2 {
|
||||
fmt.Println("Received bat with data, batId: ", data[0])
|
||||
fmt.Println("Received bat with data, batPath: ", data[1])
|
||||
utils.ExecPyFile(a.ctx, data[0].(string), data[1].(string))
|
||||
} else if len(data) > 0 && len(data) < 2 {
|
||||
fmt.Println("Received bat with data, required 2 paramters, found 1: ", data[0])
|
||||
} else {
|
||||
fmt.Println("Received bat without data")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
@@ -35,8 +75,33 @@ func (a *App) Greet(name string) string {
|
||||
|
||||
// ReadAll reads file content
|
||||
func (a *App) ReadAll(filePath string) string {
|
||||
utils.Log("ReadAll filePath: ", filePath)
|
||||
// Read the file content, resulting in a JSON string containing file content and callback ID
|
||||
data := string(utils.ReadAll(filePath))
|
||||
utils.Log("ReadAll data: ", data)
|
||||
return data
|
||||
}
|
||||
|
||||
func (a *App) SaveFile(filePath string, data string) {
|
||||
utils.SaveFile(filePath, data)
|
||||
}
|
||||
|
||||
func (a *App) PathExists(path string) bool {
|
||||
exists, _ := utils.PathExists(path)
|
||||
return exists
|
||||
}
|
||||
|
||||
func (a *App) DirSize(path string) int64 {
|
||||
utils.Log("DirSize path: ", path)
|
||||
size, _ := utils.DirSize(path)
|
||||
utils.Log("DirSize size: ", size)
|
||||
return size
|
||||
}
|
||||
|
||||
func (a *App) AppPath() string {
|
||||
return utils.AppPath()
|
||||
}
|
||||
|
||||
func (a *App) CheckPort(port string) bool {
|
||||
return utils.CheckPort(port)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>OpenManus</title>
|
||||
<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>
|
||||
<div id="app"></div>
|
||||
<script src="./src/main.js" type="module"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
Generated
+12
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.8.3",
|
||||
"element-plus": "^2.9.2",
|
||||
"marked": "^15.0.7",
|
||||
"pinia": "^3.0.1",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"qs": "^6.14.0",
|
||||
@@ -2133,6 +2134,17 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/marked/-/marked-15.0.7.tgz",
|
||||
"integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.8.3",
|
||||
"element-plus": "^2.9.2",
|
||||
"marked": "^15.0.7",
|
||||
"pinia": "^3.0.1",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"qs": "^6.14.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
:root {
|
||||
--el-menu-base-level-padding: 10px !important;
|
||||
--el-menu-base-level-padding: 4px !important;
|
||||
/** Indents for submenu items */
|
||||
--el-menu-level-padding: 20px !important;
|
||||
}
|
||||
@@ -186,12 +186,23 @@ img.edit:hover {
|
||||
}
|
||||
|
||||
.main-content {
|
||||
width: 100%;
|
||||
padding: 0px 16px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.main-content-full-height {
|
||||
width: 100%;
|
||||
/** 44 + 16 + 16 */
|
||||
height: calc(100vh - 76px);
|
||||
padding: 0px 16px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/** Element Plus Start */
|
||||
@@ -245,6 +256,10 @@ button.el-button--default {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button.el-button--large {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
button>span.el-button__text--expand {
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -270,7 +285,7 @@ button>span.el-button__text--expand {
|
||||
|
||||
/* If defined in AsideMenu, menu components associated may not read some overridden styles */
|
||||
.el-sub-menu__title {
|
||||
padding: 0px 10px !important;
|
||||
padding: 0px 4px !important;
|
||||
border-radius: 6px;
|
||||
border: none !important;
|
||||
}
|
||||
@@ -349,6 +364,10 @@ div.el-divider {
|
||||
margin-bottom: auto !important;
|
||||
}
|
||||
|
||||
.el-step__description {
|
||||
padding-right: 0px !important;
|
||||
}
|
||||
|
||||
/** Element Plus End */
|
||||
|
||||
.block {
|
||||
@@ -444,6 +463,20 @@ div.el-divider {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fyc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fys {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center !important;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.fxsb {
|
||||
display: flex;
|
||||
justify-content: space-between !important;
|
||||
@@ -502,20 +535,33 @@ div.el-divider {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-logo,
|
||||
.header-title {
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
.header-title h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
h1.el-text {
|
||||
--el-text-font-size: 48px;
|
||||
letter-spacing: -0.5px
|
||||
}
|
||||
|
||||
.header-title h2 {
|
||||
h2.el-text {
|
||||
--el-text-font-size: 32px;
|
||||
letter-spacing: -0.5px
|
||||
}
|
||||
|
||||
h3.el-text {
|
||||
--el-text-font-size: 24px;
|
||||
letter-spacing: -0.5px
|
||||
}
|
||||
|
||||
h4.el-text {
|
||||
--el-text-font-size: 20px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.5px
|
||||
}
|
||||
|
||||
h5.el-text {
|
||||
--el-text-font-size: 16px;
|
||||
letter-spacing: -0.5px
|
||||
}
|
||||
|
||||
h6.el-text {
|
||||
--el-text-font-size: 14px;
|
||||
letter-spacing: -0.5px
|
||||
}
|
||||
|
||||
@@ -934,6 +980,30 @@ div.el-divider {
|
||||
margin-left: 12px !important;
|
||||
}
|
||||
|
||||
.mt--8 {
|
||||
margin-top: -8px !important;
|
||||
}
|
||||
|
||||
.mt--10 {
|
||||
margin-top: -10px !important;
|
||||
}
|
||||
|
||||
.mt--12 {
|
||||
margin-top: -12px !important;
|
||||
}
|
||||
|
||||
.mt--16 {
|
||||
margin-top: -16px !important;
|
||||
}
|
||||
|
||||
.mt--20 {
|
||||
margin-top: -20px !important;
|
||||
}
|
||||
|
||||
.mt--24 {
|
||||
margin-top: -24px !important;
|
||||
}
|
||||
|
||||
.mt-0-5 {
|
||||
margin-top: 0.5px !important;
|
||||
}
|
||||
@@ -970,6 +1040,10 @@ div.el-divider {
|
||||
margin-top: 18px !important;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px !important;
|
||||
}
|
||||
|
||||
.mr-8 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
@@ -1061,6 +1135,11 @@ div.el-divider {
|
||||
margin-bottom: 18px !important;
|
||||
}
|
||||
|
||||
.mtb-20 {
|
||||
margin-top: 20px !important;
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
|
||||
.plr-4 {
|
||||
padding-left: 4px !important;
|
||||
padding-right: 4px !important;
|
||||
@@ -1280,6 +1359,10 @@ div.el-divider {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.min-h32 {
|
||||
min-height: 32px !important;
|
||||
}
|
||||
|
||||
.min-h46 {
|
||||
min-height: 46px !important;
|
||||
}
|
||||
@@ -1296,6 +1379,14 @@ div.el-divider {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.max-w-400 {
|
||||
max-width: 400px !important;
|
||||
}
|
||||
|
||||
.max-w-500 {
|
||||
max-width: 500px !important;
|
||||
}
|
||||
|
||||
.max-w-640 {
|
||||
max-width: 640px !important;
|
||||
}
|
||||
@@ -1603,6 +1694,18 @@ div.el-divider {
|
||||
padding-left: 10px !important;
|
||||
}
|
||||
|
||||
.pl-12 {
|
||||
padding-left: 12px !important;
|
||||
}
|
||||
|
||||
.pl-14 {
|
||||
padding-left: 14px !important;
|
||||
}
|
||||
|
||||
.pl-16 {
|
||||
padding-left: 16px !important;
|
||||
}
|
||||
|
||||
.pl-20 {
|
||||
padding-left: 20px !important;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -1,4 +1,4 @@
|
||||
import { ReadAll } from '@/../wailsjs/go/main/App.js'
|
||||
import { ReadAll, SaveFile, PathExists, DirSize, AppPath } from '@/../wailsjs/go/main/App.js'
|
||||
import utils from '@/assets/js/utils'
|
||||
|
||||
// Temporary cache for file information
|
||||
@@ -186,6 +186,125 @@ function readAll(filePath) {
|
||||
return ReadAll(filePath)
|
||||
}
|
||||
|
||||
// Await Read file contents
|
||||
async function awaitReadAll(filePath) {
|
||||
return await ReadAll(filePath)
|
||||
}
|
||||
|
||||
// Save file
|
||||
function saveFile(filePath, content) {
|
||||
return SaveFile(filePath, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a TOML node info to a json object
|
||||
*/
|
||||
async function readTomlNode(filePath, nodeName) {
|
||||
const fileContent = await readAll(filePath)
|
||||
// console.log("Read Toml file, filePath:", filePath, ", fileContent:", fileContent)
|
||||
if (utils.isBlank(fileContent)) {
|
||||
utils.pop('readTomlFailed')
|
||||
return
|
||||
}
|
||||
const lines = utils.stringToLines(fileContent)
|
||||
|
||||
// Read Node
|
||||
const nodeStart = lines.findIndex((line) => {
|
||||
return line.includes("[" + nodeName + "]")
|
||||
})
|
||||
const node = {}
|
||||
for (let i = nodeStart + 1; i < lines.length; i++) {
|
||||
// console.log("line: ", lines[i])
|
||||
// Determine whether the next configuration module has been reached.
|
||||
if (lines[i].startsWith("[")) {
|
||||
break
|
||||
}
|
||||
// 读取配置
|
||||
const line = lines[i]
|
||||
if (line.startsWith("#")) {
|
||||
continue
|
||||
}
|
||||
const lineArr = line.split("=")
|
||||
if (lineArr.length == 0) {
|
||||
continue
|
||||
}
|
||||
const key = lineArr[0].trim()
|
||||
let value = ""
|
||||
if (lineArr.length == 2) {
|
||||
value = lineArr[1].trim()
|
||||
}
|
||||
node[key] = value
|
||||
}
|
||||
console.log("Read node from toml file, result: ", node)
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a toml node
|
||||
*/
|
||||
async function saveTomlNode(filePath, nodeName, newNodeJson) {
|
||||
const fileContent = await readAll(filePath)
|
||||
// console.log("Read Toml file, filePath:", filePath, ", fileContent:", fileContent)
|
||||
if (utils.isBlank(fileContent)) {
|
||||
utils.pop(t('readTomlFailed'))
|
||||
return
|
||||
}
|
||||
const lines = utils.stringToLines(fileContent)
|
||||
|
||||
// Read Node
|
||||
const nodeStart = lines.findIndex((line) => {
|
||||
return line.includes("[" + nodeName + "]")
|
||||
})
|
||||
|
||||
for (let i = nodeStart + 1; i < lines.length; i++) {
|
||||
// console.log("line: ", lines[i])
|
||||
// Determine whether the next configuration module has been reached.
|
||||
if (lines[i].startsWith("[")) {
|
||||
break
|
||||
}
|
||||
// 读取配置
|
||||
const line = lines[i]
|
||||
if (line.startsWith("#")) {
|
||||
continue
|
||||
}
|
||||
const lineArr = line.split("=")
|
||||
if (lineArr.length == 0) {
|
||||
continue
|
||||
}
|
||||
const key = lineArr[0].trim()
|
||||
let value = newNodeJson[key]
|
||||
if (utils.isNull(value)) {
|
||||
continue
|
||||
}
|
||||
value = value.trim()
|
||||
lines[i] = key + " = " + value
|
||||
}
|
||||
console.log("Save node from toml file, new lines: ", lines)
|
||||
const newContent = lines.join("\n")
|
||||
await saveFile(filePath, newContent)
|
||||
}
|
||||
|
||||
|
||||
function pathExists(path) {
|
||||
return PathExists(path)
|
||||
}
|
||||
|
||||
function dirSize(path) {
|
||||
return DirSize(path)
|
||||
}
|
||||
|
||||
async function awaitDirSize(path) {
|
||||
return await dirSize(path)
|
||||
}
|
||||
|
||||
function appPath(path) {
|
||||
return AppPath(path)
|
||||
}
|
||||
|
||||
async function awaitAppPath(path) {
|
||||
return await appPath(path)
|
||||
}
|
||||
|
||||
export default {
|
||||
// Cache on onChange
|
||||
cache,
|
||||
@@ -206,5 +325,14 @@ export default {
|
||||
// Collect fileId from Comps
|
||||
fileIds,
|
||||
// Read file
|
||||
readAll
|
||||
readAll,
|
||||
// Read toml node
|
||||
readTomlNode,
|
||||
// Save toml node
|
||||
saveTomlNode,
|
||||
pathExists,
|
||||
dirSize,
|
||||
awaitDirSize,
|
||||
appPath,
|
||||
awaitAppPath,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Greet } from '@/../wailsjs/go/main/App.js'
|
||||
import { Greet, CheckPort } from '@/../wailsjs/go/main/App.js'
|
||||
import axios from "axios"
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
@@ -82,10 +82,17 @@ async function awaitDel(url, param) {
|
||||
function greet(name) {
|
||||
return Greet(name).then(resp => {
|
||||
console.log("greet resp:", resp)
|
||||
return resp
|
||||
})
|
||||
}
|
||||
|
||||
function checkPort(port) {
|
||||
return CheckPort(port)
|
||||
}
|
||||
|
||||
async function awaitCheckPort(port) {
|
||||
return await checkPort(port)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if object is null
|
||||
*/
|
||||
@@ -545,6 +552,7 @@ function stringToLines(str) {
|
||||
}
|
||||
return str.split('\n')
|
||||
}
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Synchronous GET HTTP request
|
||||
@@ -687,4 +695,7 @@ export default {
|
||||
|
||||
stringToLines,
|
||||
|
||||
checkPort,
|
||||
|
||||
awaitCheckPort,
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ const intValidator = (rule, value, callback) => {
|
||||
}
|
||||
|
||||
function validator() {
|
||||
console.log("arguments:", arguments)
|
||||
// console.log("arguments:", arguments)
|
||||
if (arguments.length <= 1) {
|
||||
const type = arguments[0]
|
||||
// Default validation logic, no special characters
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
English | [中文](README_zh.md)
|
||||
|
||||
[](https://github.com/mannaandpoem/OpenManus/stargazers)
|
||||
 
|
||||
[](https://opensource.org/licenses/MIT)  
|
||||
[](https://discord.gg/DYn29wFk9z)
|
||||
|
||||
# 👋 OpenManus
|
||||
|
||||
Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫!
|
||||
|
||||
Our team
|
||||
members [@mannaandpoem](https://github.com/mannaandpoem) [@XiangJinyu](https://github.com/XiangJinyu) [@MoshiQAQ](https://github.com/MoshiQAQ) [@didiforgithub](https://github.com/didiforgithub) [@stellaHSR](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building!
|
||||
|
||||
It's a simple implementation, so we welcome any suggestions, contributions, and feedback!
|
||||
|
||||
Enjoy your own agent with OpenManus!
|
||||
|
||||
We're also excited to introduce [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL), an open-source project dedicated to reinforcement learning (RL)- based (such as GRPO) tuning methods for LLM agents, developed collaboratively by researchers from UIUC and OpenManus.
|
||||
|
||||
## Project Demo
|
||||
|
||||
<video src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" data-canonical-src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
|
||||
|
||||
## Installation
|
||||
|
||||
We provide two installation methods. Method 2 (using uv) is recommended for faster installation and better dependency management.
|
||||
|
||||
### Method 1: Using conda
|
||||
|
||||
1. Create a new conda environment:
|
||||
|
||||
```bash
|
||||
conda create -n open_manus python=3.12
|
||||
conda activate open_manus
|
||||
```
|
||||
|
||||
2. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mannaandpoem/OpenManus.git
|
||||
cd OpenManus
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Method 2: Using uv (Recommended)
|
||||
|
||||
1. Install uv (A fast Python package installer and resolver):
|
||||
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
2. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mannaandpoem/OpenManus.git
|
||||
cd OpenManus
|
||||
```
|
||||
|
||||
3. Create a new virtual environment and activate it:
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate # On Unix/macOS
|
||||
# Or on Windows:
|
||||
# .venv\Scripts\activate
|
||||
```
|
||||
|
||||
4. Install dependencies:
|
||||
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
OpenManus requires configuration for the LLM APIs it uses. Follow these steps to set up your configuration:
|
||||
|
||||
1. Create a `config.toml` file in the `config` directory (you can copy from the example):
|
||||
|
||||
```bash
|
||||
cp config/config.example.toml config/config.toml
|
||||
```
|
||||
|
||||
2. Edit `config/config.toml` to add your API keys and customize settings:
|
||||
|
||||
```toml
|
||||
# Global LLM configuration
|
||||
[llm]
|
||||
model = "gpt-4o"
|
||||
base_url = "https://api.openai.com/v1"
|
||||
api_key = "sk-..." # Replace with your actual API key
|
||||
max_tokens = 4096
|
||||
temperature = 0.0
|
||||
|
||||
# Optional configuration for specific LLM models
|
||||
[llm.vision]
|
||||
model = "gpt-4o"
|
||||
base_url = "https://api.openai.com/v1"
|
||||
api_key = "sk-..." # Replace with your actual API key
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
One line for run OpenManus:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
Then input your idea via terminal!
|
||||
|
||||
For unstable version, you also can run:
|
||||
|
||||
```bash
|
||||
python run_flow.py
|
||||
```
|
||||
|
||||
## How to contribute
|
||||
|
||||
We welcome any friendly suggestions and helpful contributions! Just create issues or submit pull requests.
|
||||
|
||||
Or contact @mannaandpoem via 📧email: mannaandpoem@gmail.com
|
||||
|
||||
## Community Group
|
||||
Join our networking group on Feishu and share your experience with other developers!
|
||||
|
||||
<div align="center" style="display: flex; gap: 20px;">
|
||||
<img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
|
||||
</div>
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#mannaandpoem/OpenManus&Date)
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
Thanks to [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
|
||||
and [browser-use](https://github.com/browser-use/browser-use) for providing basic support for this project!
|
||||
|
||||
OpenManus is built by contributors from MetaGPT. Huge thanks to this agent community!
|
||||
|
||||
## Cite
|
||||
```bibtex
|
||||
@misc{openmanus2025,
|
||||
author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong},
|
||||
title = {OpenManus: An open-source framework for building general AI agents},
|
||||
year = {2025},
|
||||
publisher = {GitHub},
|
||||
journal = {GitHub repository},
|
||||
howpublished = {\url{https://github.com/mannaandpoem/OpenManus}},
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,148 @@
|
||||
[English](README.md) | 中文
|
||||
|
||||
[](https://github.com/mannaandpoem/OpenManus/stargazers)
|
||||
 
|
||||
[](https://opensource.org/licenses/MIT)  
|
||||
[](https://discord.gg/DYn29wFk9z)
|
||||
|
||||
# 👋 OpenManus
|
||||
|
||||
Manus 非常棒,但 OpenManus 无需邀请码即可实现任何创意 🛫!
|
||||
|
||||
我们的团队成员 [@mannaandpoem](https://github.com/mannaandpoem) [@XiangJinyu](https://github.com/XiangJinyu) [@MoshiQAQ](https://github.com/MoshiQAQ) [@didiforgithub](https://github.com/didiforgithub) https://github.com/stellaHSR 来自 [@MetaGPT](https://github.com/geekan/MetaGPT) 组织,我们在 3
|
||||
小时内完成了原型开发并持续迭代中!
|
||||
|
||||
这是一个简洁的实现方案,欢迎任何建议、贡献和反馈!
|
||||
|
||||
用 OpenManus 开启你的智能体之旅吧!
|
||||
|
||||
我们也非常高兴地向大家介绍 [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL),这是一个专注于基于强化学习(RL,例如 GRPO)的方法来优化大语言模型(LLM)智能体的开源项目,由来自UIUC 和 OpenManus 的研究人员合作开发。
|
||||
|
||||
## 项目演示
|
||||
|
||||
<video src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" data-canonical-src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
|
||||
|
||||
## 安装指南
|
||||
|
||||
我们提供两种安装方式。推荐使用方式二(uv),因为它能提供更快的安装速度和更好的依赖管理。
|
||||
|
||||
### 方式一:使用 conda
|
||||
|
||||
1. 创建新的 conda 环境:
|
||||
|
||||
```bash
|
||||
conda create -n open_manus python=3.12
|
||||
conda activate open_manus
|
||||
```
|
||||
|
||||
2. 克隆仓库:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mannaandpoem/OpenManus.git
|
||||
cd OpenManus
|
||||
```
|
||||
|
||||
3. 安装依赖:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 方式二:使用 uv(推荐)
|
||||
|
||||
1. 安装 uv(一个快速的 Python 包管理器):
|
||||
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
2. 克隆仓库:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mannaandpoem/OpenManus.git
|
||||
cd OpenManus
|
||||
```
|
||||
|
||||
3. 创建并激活虚拟环境:
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate # Unix/macOS 系统
|
||||
# Windows 系统使用:
|
||||
# .venv\Scripts\activate
|
||||
```
|
||||
|
||||
4. 安装依赖:
|
||||
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
OpenManus 需要配置使用的 LLM API,请按以下步骤设置:
|
||||
|
||||
1. 在 `config` 目录创建 `config.toml` 文件(可从示例复制):
|
||||
|
||||
```bash
|
||||
cp config/config.example.toml config/config.toml
|
||||
```
|
||||
|
||||
2. 编辑 `config/config.toml` 添加 API 密钥和自定义设置:
|
||||
|
||||
```toml
|
||||
# 全局 LLM 配置
|
||||
[llm]
|
||||
model = "gpt-4o"
|
||||
base_url = "https://api.openai.com/v1"
|
||||
api_key = "sk-..." # 替换为真实 API 密钥
|
||||
max_tokens = 4096
|
||||
temperature = 0.0
|
||||
|
||||
# 可选特定 LLM 模型配置
|
||||
[llm.vision]
|
||||
model = "gpt-4o"
|
||||
base_url = "https://api.openai.com/v1"
|
||||
api_key = "sk-..." # 替换为真实 API 密钥
|
||||
```
|
||||
|
||||
## 快速启动
|
||||
|
||||
一行命令运行 OpenManus:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
然后通过终端输入你的创意!
|
||||
|
||||
如需体验开发中版本,可运行:
|
||||
|
||||
```bash
|
||||
python run_flow.py
|
||||
```
|
||||
|
||||
## 贡献指南
|
||||
|
||||
我们欢迎任何友好的建议和有价值的贡献!可以直接创建 issue 或提交 pull request。
|
||||
|
||||
或通过 📧 邮件联系 @mannaandpoem:mannaandpoem@gmail.com
|
||||
|
||||
## 交流群
|
||||
|
||||
加入我们的飞书交流群,与其他开发者分享经验!
|
||||
|
||||
<div align="center" style="display: flex; gap: 20px;">
|
||||
<img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
|
||||
</div>
|
||||
|
||||
## Star 数量
|
||||
|
||||
[](https://star-history.com/#mannaandpoem/OpenManus&Date)
|
||||
|
||||
## 致谢
|
||||
|
||||
特别感谢 [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
|
||||
和 [browser-use](https://github.com/browser-use/browser-use) 为本项目提供的基础支持!
|
||||
|
||||
OpenManus 由 MetaGPT 社区的贡献者共同构建,感谢这个充满活力的智能体开发者社区!
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-menu class="el-menu-custom" :default-active="activeMenu()" :collapse="menuCollapse" @open="handleOpen"
|
||||
@close="handleClose">
|
||||
@close="handleClose" :popper-offset="8">
|
||||
|
||||
<el-menu-item index="M02" @click="routeTo('/task')">
|
||||
<el-icon>
|
||||
@@ -32,8 +32,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ChatDotRound, List, Clock, Setting } from '@element-plus/icons-vue'
|
||||
import { ref, inject, onMounted, reactive, watch } from 'vue'
|
||||
import { List, Clock, Setting } from '@element-plus/icons-vue'
|
||||
import { ref, computed, inject, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { storeToRefs } from 'pinia'
|
||||
@@ -43,7 +43,10 @@ const utils = inject('utils')
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const config = useConfig()
|
||||
const { menuCollapse } = storeToRefs(config)
|
||||
|
||||
const { collapse, resizeCollapse } = storeToRefs(config)
|
||||
|
||||
const menuCollapse = computed(() => collapse.value || resizeCollapse.value)
|
||||
|
||||
const handleOpen = (key, keyPath) => {
|
||||
// console.log(key, keyPath)
|
||||
@@ -79,11 +82,6 @@ const menuList = [
|
||||
menuName: "menu.config.llm",
|
||||
href: "/config/llm"
|
||||
},
|
||||
{
|
||||
index: "M9903",
|
||||
menuName: "menu.config.theme",
|
||||
href: "/config/theme"
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
@@ -115,9 +113,9 @@ watch(() => router.currentRoute.value.path, (newValue, oldValue) => {
|
||||
function activeMenu() {
|
||||
const currRoute = router.currentRoute
|
||||
const path = currRoute.value.path
|
||||
console.log("currRoute path:", path)
|
||||
// console.log("currRoute path:", path)
|
||||
let index = getIndexByPath(path)
|
||||
console.log("index:", index)
|
||||
// console.log("index:", index)
|
||||
if (utils.notNull(index)) {
|
||||
return index
|
||||
}
|
||||
@@ -125,9 +123,9 @@ function activeMenu() {
|
||||
const lastIndex = path.lastIndexOf('/')
|
||||
if (lastIndex != -1) {
|
||||
const newPath = path.substring(0, lastIndex)
|
||||
console.log("newPath from parent path:", newPath)
|
||||
// console.log("newPath from parent path:", newPath)
|
||||
index = getIndexByPath(newPath)
|
||||
console.log("index from parent path:", index)
|
||||
// console.log("index from parent path:", index)
|
||||
if (utils.notNull(index)) {
|
||||
return index
|
||||
}
|
||||
@@ -255,8 +253,6 @@ function getMenuNameByCode(code) {
|
||||
return t(code)
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -278,26 +274,28 @@ li {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/** When the menu is collapsed, redefine the hover menu height */
|
||||
.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;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
margin-left: 6px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/** When the menu is collapsed, redefine the hover menu height */
|
||||
.el-menu-item {
|
||||
min-width: 32px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.el-menu-custom .el-menu--collapse {
|
||||
width: 44px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.el-menu-custom:not(.el-menu--collapse) {
|
||||
width: 200px;
|
||||
width: 188px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
<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 type="primary" @click="refresh" class="pl-14 pr-4">
|
||||
<img v-if="isDark" src="@/assets/img/logo-w-sm.png" class="fxc" height="26px" alt="logo" />
|
||||
<img v-if="!isDark" src="@/assets/img/logo-b-sm.png" class="fxc" height="26px" alt="logo" />
|
||||
</el-link>
|
||||
</div>
|
||||
<el-link class="plr-10 w-56" @click="menuToggle">
|
||||
@@ -60,11 +61,16 @@ import { showShade, closeShade } from '@/assets/js/shade'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useDark } from '@vueuse/core'
|
||||
|
||||
const router = useRouter()
|
||||
const config = useConfig()
|
||||
const isDark = useDark()
|
||||
|
||||
const { shrink, collapse, resizeCollapse } = storeToRefs(config)
|
||||
|
||||
const menuCollapse = computed(() => collapse.value || resizeCollapse.value)
|
||||
|
||||
const { shrink, menuCollapse } = storeToRefs(config)
|
||||
const currentRoute = reactive(router.currentRoute)
|
||||
|
||||
// Default transition effect, slide to the left
|
||||
@@ -83,53 +89,50 @@ const menuAnimationDuration = ref(0)
|
||||
// Function to toggle the menu between expanded and collapsed states
|
||||
function menuToggle() {
|
||||
menuAnimationDuration.value = '300ms'
|
||||
|
||||
if (menuCollapse.value) {
|
||||
// console.log("Extend menu")
|
||||
if (shrink.value) {
|
||||
// Expend the shade if menu is collapsing
|
||||
showShade(() => {
|
||||
// Callback function to close the shade after the menu has collapsed
|
||||
config.setMenuCollapse(true)
|
||||
config.setCollapse(true)
|
||||
})
|
||||
}
|
||||
config.setCollapse(false)
|
||||
} else {
|
||||
// If the menu is in an expanded state, close the shade
|
||||
closeShade()
|
||||
config.setCollapse(true)
|
||||
}
|
||||
// Toggle the menu state
|
||||
config.setMenuCollapse(!menuCollapse.value)
|
||||
resizeCollapse.value = collapse.value
|
||||
console.log("collapse:", collapse.value, ", menuCollapse:", menuCollapse.value)
|
||||
}
|
||||
|
||||
// Adaptive layout
|
||||
function onAdaptiveLayout() {
|
||||
// Get the current window width
|
||||
const clientWidth = document.body.clientWidth
|
||||
// console.log("menuCollapse:", menuCollapse.value, config.getMenuCollapse(), "clientWidth:", clientWidth)
|
||||
// Determine if the aside menu should be shrunk based on the window width
|
||||
if (clientWidth < 800) {
|
||||
config.setShrink(true)
|
||||
if (!menuCollapse.value) {
|
||||
// Collapse the menu if it is not already collapsed
|
||||
menuToggle()
|
||||
}
|
||||
config.setResizeCollapse(true)
|
||||
} else {
|
||||
config.setShrink(false)
|
||||
config.setResizeCollapse(false)
|
||||
}
|
||||
closeShade()
|
||||
}
|
||||
|
||||
watch(() => router.currentRoute.value, (newValue, oldValue) => {
|
||||
// Toggle the menu when the route changes
|
||||
onAdaptiveLayout()
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
onAdaptiveLayout()
|
||||
useEventListener(window, 'resize', onAdaptiveLayout)
|
||||
})
|
||||
|
||||
watch(() => router.currentRoute.value.path, (newValue, oldValue) => {
|
||||
// If the layout is shrunk and the menu is expanded, collapse the menu
|
||||
if (shrink.value && !menuCollapse.value) {
|
||||
menuToggle()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
function refresh() {
|
||||
// Reload the page
|
||||
location.reload()
|
||||
@@ -157,7 +160,7 @@ header {
|
||||
}
|
||||
|
||||
aside {
|
||||
background-color: var(--el-fg-color);
|
||||
z-index: 9999999;
|
||||
}
|
||||
|
||||
aside.shrink {
|
||||
@@ -222,7 +225,6 @@ main {
|
||||
|
||||
.fixed-menu-expand {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
height: 44px;
|
||||
width: 200px;
|
||||
/* Reference to the keyframes */
|
||||
@@ -231,12 +233,13 @@ main {
|
||||
animation-duration: v-bind('menuAnimationDuration');
|
||||
animation-timing-function: ease-in-out;
|
||||
background-color: var(--el-fg-color);
|
||||
/* border-bottom: 1px solid var(--el-bg-color); */
|
||||
z-index: 9999999;
|
||||
}
|
||||
|
||||
.scrollbar-menu-wrapper {
|
||||
top: 44px;
|
||||
height: calc(100vh - 44px);
|
||||
top: 45px;
|
||||
height: calc(100vh - 45px);
|
||||
background-color: var(--el-fg-color);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<main>
|
||||
<div class="container">
|
||||
<!-- 路由展示区 -->
|
||||
<RouterView />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
main {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: safe center;
|
||||
align-items: safe center;
|
||||
}
|
||||
</style>
|
||||
@@ -55,23 +55,27 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { onMounted, ref, reactive, inject } from 'vue'
|
||||
import { ArrowDown, Refresh, Moon, Sunny } from '@element-plus/icons-vue'
|
||||
import { useConfig } from '@/store/config'
|
||||
/** 暗黑主题切换 */
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const utils = inject("utils")
|
||||
const files = inject("files")
|
||||
const config = useConfig()
|
||||
const isDark = useDark()
|
||||
const { t } = useI18n()
|
||||
|
||||
const modelList = ref(config.modelList)
|
||||
|
||||
const selectedModel = ref(config.selectedModel != null ? config.selectedModel : modelList.value[0])
|
||||
const modelList = ref([])
|
||||
|
||||
const selectedModel = ref(config.selectedModel)
|
||||
|
||||
function handleSwitchModel(mod) {
|
||||
// console.log("handleSwitchModel:", model)
|
||||
selectedModel.value = mod
|
||||
config.setSelectedModel(mod)
|
||||
}
|
||||
|
||||
const langList = ref(config.langList)
|
||||
@@ -85,6 +89,56 @@ function handleSwitchLang(lang) {
|
||||
location.reload()
|
||||
}
|
||||
|
||||
const llmConfig = reactive({
|
||||
model: null,
|
||||
base_url: null,
|
||||
api_key: null,
|
||||
max_tokens: null,
|
||||
temperature: null,
|
||||
})
|
||||
|
||||
function loadLlmConfig() {
|
||||
const filePath = appDataPath.value + "\\config\\config.toml"
|
||||
files.readTomlNode(filePath, "llm").then((node) => {
|
||||
console.log("config/config.toml: ", node)
|
||||
if (utils.isBlank(node)) {
|
||||
utils.pop(t('readTomlFailed'))
|
||||
return
|
||||
}
|
||||
utils.copyProps(node, llmConfig)
|
||||
let model = node.model
|
||||
if (utils.isBlank(model)) {
|
||||
model = "No Model"
|
||||
}
|
||||
if (model.startsWith("\"")) {
|
||||
model = model.substring(1)
|
||||
}
|
||||
if (model.endsWith("\"")) {
|
||||
model = model.substring(0, model.length - 1)
|
||||
}
|
||||
utils.clearArray(modelList.value)
|
||||
modelList.value.push(model)
|
||||
if (selectedModel.value == null) {
|
||||
config.setSelectedModel(modelList.value[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const appDataPath = ref()
|
||||
|
||||
onMounted(async () => {
|
||||
await files.awaitAppPath().then((path) => {
|
||||
appDataPath.value = path
|
||||
console.log('appDataPath: ', appDataPath.value)
|
||||
if (appDataPath.value && appDataPath.value.endsWith('\\desktop\\build\\bin')) {
|
||||
appDataPath.value = appDataPath.value.replace('\\desktop\\build\\bin', '')
|
||||
}
|
||||
console.log('appDataPath: ', appDataPath.value)
|
||||
})
|
||||
// 读取配置文件config/config.toml
|
||||
loadLlmConfig()
|
||||
})
|
||||
|
||||
function refresh() {
|
||||
location.reload()
|
||||
}
|
||||
@@ -102,7 +156,7 @@ function refresh() {
|
||||
.el-dropdown-link {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
min-width: 80px;
|
||||
min-width: 40px;
|
||||
color: var(--el-color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -39,15 +39,45 @@ export default {
|
||||
clearCacheSuccess: "Clear cache success",
|
||||
openManusAgiTips: "The above content is generated by OpenManus for reference only",
|
||||
taskStatus: {
|
||||
null: "Execution Status",
|
||||
name: "Task Status",
|
||||
success: "Success",
|
||||
failed: "Failed",
|
||||
running: "Running",
|
||||
terminated: "Terminated",
|
||||
},
|
||||
taskExecFailed: "Task execution failed",
|
||||
newTask: "New Task",
|
||||
readConfigSuccess: "Read config success",
|
||||
readConfigFailed: "Read config failed",
|
||||
readTomlFailed: "Read config failed",
|
||||
baseConfig: "Base Settings",
|
||||
serverConfig: "Server Config",
|
||||
inDevelopment: "In development, not currently supported.",
|
||||
expandAll: "Expand All",
|
||||
collapseAll: "Collapse All",
|
||||
generatedContent: "Generated Content",
|
||||
welcomeUseOpenManus: "Welcome to use OpenManus",
|
||||
welcomeUseOpenManusTips: "OpenManus is a powerful AI assistant that can help you with a wide range of tasks. It can generate code, write articles, and answer questions. You can use it to save time and effort. Let's get started!",
|
||||
getStarted: "Get Started",
|
||||
docs: "Docs",
|
||||
toIndex: "To Index",
|
||||
envConfig: "Env Config",
|
||||
initConfig: "Init Config",
|
||||
step1: "Step 1",
|
||||
step2: "Step 2",
|
||||
step3: "Step 3",
|
||||
envLibDownload: "Dependency Libary Download",
|
||||
checkedPass: "Checked OK",
|
||||
checkedFailed: "Checked Failed",
|
||||
llmConfig: "LLM Config",
|
||||
envLibExists: "Dependency Libary Exists",
|
||||
envLibNotExists: "Dependency Libary Not Exists, Please Download First.",
|
||||
envLibDownloadTips: "The 2GB environmental library is downloading.",
|
||||
envLibDownloadSuccess: "Dependency Libary Download Success",
|
||||
envLibDownloadFailed: "Dependency Libary Download Failed",
|
||||
envLibDownloading: "Dependency Libary Downloading",
|
||||
startTheExperience: "Start the Experience",
|
||||
startOpenManusService: "Start OpenManus Service, Please Wait...",
|
||||
initConfigFirst: "Please init config first",
|
||||
openManusIsStarting: "OpenManus is starting, please wait...",
|
||||
}
|
||||
|
||||
@@ -38,15 +38,45 @@ export default {
|
||||
clearCacheSuccess: "清理缓存成功",
|
||||
openManusAgiTips: "以上内容由OpenManus生成, 仅供参考和借鉴",
|
||||
taskStatus: {
|
||||
null: "任务异常",
|
||||
name: "任务状态",
|
||||
success: "成功",
|
||||
failed: "失败",
|
||||
running: "运行中",
|
||||
terminated: "终止",
|
||||
},
|
||||
taskExecFailed: "任务执行失败",
|
||||
newTask: "新任务",
|
||||
readConfigSuccess: "读取配置成功",
|
||||
readConfigFailed: "读取配置失败",
|
||||
readTomlFailed: "读取配置失败",
|
||||
baseConfig: "基础设置",
|
||||
serverConfig: "服务器配置",
|
||||
inDevelopment: "功能开发中,暂不支持",
|
||||
expandAll: "展开所有",
|
||||
collapseAll: "收起所有",
|
||||
generatedContent: "生成内容",
|
||||
welcomeUseOpenManus: "欢迎使用OpenManus",
|
||||
welcomeUseOpenManusTips: "OpenManus是一个强大的AI助手, 可以帮助您完成各种任务, 包括生成代码, 撰写文章, 回答问题等。您可以使用它来节省时间和精力。让我们开始吧!",
|
||||
getStarted: "开始使用",
|
||||
docs: "使用文档",
|
||||
toIndex: "返回首页",
|
||||
envConfig: "环境配置",
|
||||
initConfig: "初始化配置",
|
||||
step1: "第一步",
|
||||
step2: "第二步",
|
||||
step3: "第三步",
|
||||
envLibDownload: "环境依赖库下载",
|
||||
checkedPass: "检测通过",
|
||||
checkedFailed: "检测失败",
|
||||
llmConfig: "大模型配置",
|
||||
envLibExists: "环境依赖库已存在",
|
||||
envLibNotExists: "环境依赖库不存在, 请先下载。",
|
||||
envLibDownloadTips: "环境依赖库约2GB, 请耐心等待下载完成。",
|
||||
envLibDownloadSuccess: "环境依赖库下载成功",
|
||||
envLibDownloadFailed: "环境依赖库下载失败",
|
||||
envLibDownloading: "环境依赖库下载中",
|
||||
startTheExperience: "开始体验",
|
||||
startOpenManusService: "启动OpenManus服务, 请稍候...",
|
||||
initConfigFirst: "请先完成初始化配置",
|
||||
openManusIsStarting: "OpenManus正在启动中, 请稍候...",
|
||||
}
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
const localConfig = localStorage.getItem('config') ? JSON.parse(localStorage.getItem('config')) : {}
|
||||
const init = localConfig.init? localConfig.init : false
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/components/MainFrame.vue'),
|
||||
component: () => import('@/components/SimpleFrame.vue'),
|
||||
meta: {
|
||||
title: "主页"
|
||||
},
|
||||
// 重定向到默认页面
|
||||
redirect: '/task',
|
||||
redirect: init ? '/task' : '/home',
|
||||
children: [
|
||||
{
|
||||
path: 'task',
|
||||
path: 'home',
|
||||
component: () => import('@/views/Home.vue'),
|
||||
meta: {
|
||||
title: "主页"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/task',
|
||||
component: () => import('@/components/MainFrame.vue'),
|
||||
meta: {
|
||||
title: "任务"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: () => import('@/views/task/TaskIndex.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
@@ -22,7 +41,7 @@ const router = createRouter({
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'task/:id',
|
||||
path: ':id',
|
||||
component: () => import('@/views/task/TaskInfo.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
@@ -41,6 +60,24 @@ const router = createRouter({
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/history',
|
||||
component: () => import('@/components/MainFrame.vue'),
|
||||
meta: {
|
||||
title: "历史记录"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: () => import('@/views/task/HistoryIndex.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
title: "历史记录",
|
||||
index: 0
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/config',
|
||||
component: () => import('@/components/MainFrame.vue'),
|
||||
@@ -67,12 +104,12 @@ const router = createRouter({
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'theme',
|
||||
component: () => import('@/views/config/Theme.vue'),
|
||||
path: 'init',
|
||||
component: () => import('@/views/config/Init.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
title: "主题",
|
||||
index: 1
|
||||
title: "初始化配置",
|
||||
index: 0
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
@@ -4,19 +4,21 @@ export const useConfig = defineStore("config", {
|
||||
state: () => {
|
||||
return {
|
||||
// 全局
|
||||
// aside是否收缩
|
||||
shrink: false,
|
||||
isDark: false,
|
||||
// 侧边栏
|
||||
// aside是否收缩
|
||||
shrink: false,
|
||||
// 菜单是否折叠
|
||||
menuCollapse: false,
|
||||
collapse: false,
|
||||
// Resize Collapse
|
||||
resizeCollapse: false,
|
||||
selectedModel: null,
|
||||
modelList: ['qwen2.5-7b', 'deepseek-r1-7b'],
|
||||
selectedLang: { code: 'en', name: 'English' },
|
||||
langList: [{ code: 'en', name: 'English' }, { code: 'zhCn', name: '简体中文' }],
|
||||
langList: [{ code: 'en', name: 'English' }, { code: 'zhCn', name: '中文' }],
|
||||
taskHistory: [
|
||||
// taskId, prompt, stepList, status, createdDt
|
||||
]
|
||||
],
|
||||
init: false,
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
@@ -32,12 +34,20 @@ export const useConfig = defineStore("config", {
|
||||
return this.isDark
|
||||
},
|
||||
|
||||
getMenuCollapse() {
|
||||
return this.menuCollapse
|
||||
getCollapse() {
|
||||
return this.collapse
|
||||
},
|
||||
|
||||
setMenuCollapse(menuCollapse) {
|
||||
this.menuCollapse = menuCollapse
|
||||
setCollapse(collapse) {
|
||||
this.collapse = collapse
|
||||
},
|
||||
|
||||
getResizeCollapse() {
|
||||
return this.resizeCollapse
|
||||
},
|
||||
|
||||
setResizeCollapse(resizeCollapse) {
|
||||
this.resizeCollapse = resizeCollapse
|
||||
},
|
||||
|
||||
getSelectedModel() {
|
||||
@@ -48,14 +58,6 @@ export const useConfig = defineStore("config", {
|
||||
this.selectedModel = selectedModel
|
||||
},
|
||||
|
||||
getModelList() {
|
||||
return this.modelList
|
||||
},
|
||||
|
||||
setModelList(modelList) {
|
||||
utils.copyArray(modelList, this.modelList)
|
||||
},
|
||||
|
||||
getSelectedLang() {
|
||||
return this.selectedLang
|
||||
},
|
||||
@@ -89,6 +91,13 @@ export const useConfig = defineStore("config", {
|
||||
return this.taskHistory[0]
|
||||
},
|
||||
|
||||
getInit() {
|
||||
return this.init
|
||||
},
|
||||
|
||||
setInit(init) {
|
||||
this.init = init
|
||||
}
|
||||
},
|
||||
persist: {
|
||||
key: "config",
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="main-content">
|
||||
|
||||
<el-card class="index" v-show="view == 'index'">
|
||||
<div class="fxe mr-10">
|
||||
<el-link type="primary" @click="toDocs" size="large"> {{ t('docs') }} </el-link>
|
||||
</div>
|
||||
|
||||
<div class="welcome">
|
||||
<el-text tag="h1">{{ t('welcomeUseOpenManus') }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="fxc">
|
||||
<img src="@/assets/img/logo-w.png" width="400" alt="OpenManus" v-if="isDark">
|
||||
<img src="@/assets/img/logo-b.png" width="400" alt="OpenManus" v-if="!isDark">
|
||||
</div>
|
||||
|
||||
<div class="welcome">
|
||||
<el-text tag="h4" class="text-indent">{{ t('welcomeUseOpenManusTips') }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="fxc mtb-20">
|
||||
<el-button type="primary" @click="getStarted" size="large"> {{ t('getStarted') }} </el-button>
|
||||
</div>
|
||||
|
||||
</el-card>
|
||||
|
||||
<el-card v-show="view == 'docs'">
|
||||
<div class="fxe mr-10">
|
||||
<el-link type="primary" @click="toIndex" size="large"> {{ t('docs') }} </el-link>
|
||||
</div>
|
||||
|
||||
<div class="mt-20" v-html="readme"> </div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { marked } from 'marked'
|
||||
import { useDark } from '@vueuse/core'
|
||||
|
||||
const utils = inject('utils')
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
const isDark = useDark()
|
||||
|
||||
const readme = ref('')
|
||||
|
||||
const langList = ref(config.langList)
|
||||
|
||||
const selectedLang = ref(config.selectedLang != null ? config.selectedLang : langList.value[0])
|
||||
|
||||
const view = ref('index')
|
||||
|
||||
function toDocs() {
|
||||
view.value = 'docs'
|
||||
}
|
||||
|
||||
function toIndex() {
|
||||
view.value = 'index'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const readmeMd = await import('@/assets/md/README.md?raw')
|
||||
const readmeZhMd = await import('@/assets/md/README_zh.md?raw')
|
||||
console.log("readmeZhMd:", readmeZhMd)
|
||||
readme.value = marked(selectedLang.value == 'zhCn' ? readmeZhMd.default : readmeMd.default)
|
||||
})
|
||||
|
||||
function getStarted() {
|
||||
router.push({ path: '/config/init' })
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.index {
|
||||
min-height: 540px;
|
||||
height: calc(100vh - 32px);
|
||||
}
|
||||
|
||||
.welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -115,7 +115,7 @@ const baseEdit = computed(() => {
|
||||
})
|
||||
|
||||
const baseNoData = computed(() => {
|
||||
return baseShow && serverConfig.model == null
|
||||
return baseShow
|
||||
})
|
||||
|
||||
const serverShow = computed(() => {
|
||||
@@ -126,7 +126,7 @@ const serverEdit = computed(() => {
|
||||
return viewModel.server == 'edit'
|
||||
})
|
||||
|
||||
const readConfigSuccess = ref(false)
|
||||
const readConfigSuccess = ref(true)
|
||||
|
||||
const serverNoData = computed(() => {
|
||||
return serverShow && !readConfigSuccess.value
|
||||
@@ -147,43 +147,51 @@ function clearCache() {
|
||||
utils.pop(t('clearCacheSuccess'))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 读取配置文件config/config.toml
|
||||
files.readAll("@/../../config/config.toml").then((fileContent) => {
|
||||
console.log("config/config.toml: ", fileContent)
|
||||
if (utils.notBlank(fileContent)) {
|
||||
readConfigSuccess.value = true
|
||||
} else {
|
||||
utils.pop(t('readConfigFailed'))
|
||||
return
|
||||
}
|
||||
const lines = utils.stringToLines(fileContent)
|
||||
const appDataPath = ref()
|
||||
|
||||
// 读取[server]
|
||||
const serverStart = lines.findIndex((line) => {
|
||||
return line.includes("[server]")
|
||||
})
|
||||
for (let i = serverStart + 1; i < lines.length; i++) {
|
||||
console.log("line: ", lines[i])
|
||||
// 判定是否到了下个配置模块
|
||||
if (lines[i].startsWith("[")) {
|
||||
break
|
||||
}
|
||||
// 读取配置
|
||||
const line = lines[i]
|
||||
const lineArr = line.split("=")
|
||||
if (lineArr.length != 2) {
|
||||
continue
|
||||
}
|
||||
const key = lineArr[0].trim()
|
||||
const value = lineArr[1].trim()
|
||||
serverConfig[key] = value
|
||||
onMounted(async () => {
|
||||
await files.awaitAppPath().then((path) => {
|
||||
appDataPath.value = path
|
||||
console.log('appDataPath: ', appDataPath.value)
|
||||
if (appDataPath.value && appDataPath.value.endsWith('\\desktop\\build\\bin')) {
|
||||
appDataPath.value = appDataPath.value.replace('\\desktop\\build\\bin', '')
|
||||
}
|
||||
console.log("serverConfig read from file: ", serverConfig)
|
||||
utils.copyProps(serverConfig, serverConfigUpd)
|
||||
console.log('appDataPath: ', appDataPath.value)
|
||||
})
|
||||
// 读取配置文件config/config.toml
|
||||
loadServerConfig()
|
||||
})
|
||||
|
||||
function loadServerConfig() {
|
||||
const filePath = appDataPath.value + "\\config\\config.toml"
|
||||
files.readTomlNode(filePath, "server").then((node) => {
|
||||
console.log("config/config.toml: ", node)
|
||||
if (utils.isBlank(node)) {
|
||||
readConfigSuccess.value = false
|
||||
utils.pop(t('readTomlFailed'))
|
||||
return
|
||||
}
|
||||
utils.copyProps(node, serverConfig)
|
||||
utils.copyProps(node, serverConfigUpd)
|
||||
})
|
||||
}
|
||||
|
||||
function saveServerConfig() {
|
||||
const filePath = appDataPath.value + "\\config\\config.toml"
|
||||
files.readAll(filePath)
|
||||
files.saveTomlNode(filePath, "server", serverConfigUpd).then((resp) => {
|
||||
console.log("config/config.toml: ", resp)
|
||||
loadServerConfig()
|
||||
toShow('server')
|
||||
})
|
||||
}
|
||||
|
||||
const ruleFormRef = ref()
|
||||
|
||||
const rules = reactive({
|
||||
host: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
port: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
})
|
||||
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
@@ -194,20 +202,13 @@ const submitForm = async () => {
|
||||
return
|
||||
}
|
||||
ElMessage.success('验证通过,提交表单');
|
||||
// update()
|
||||
saveServerConfig()
|
||||
} catch (error) {
|
||||
console.log('error: ', error);
|
||||
ElMessage.error('参数验证失败');
|
||||
}
|
||||
}
|
||||
|
||||
const rules = reactive({
|
||||
host: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
port: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
api_key: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
max_tokens: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
temperature: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -0,0 +1,525 @@
|
||||
<template>
|
||||
<div class="main-content">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="title fxsb">
|
||||
<div>{{ t('initConfig') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Show Data -->
|
||||
<el-steps direction="vertical" :active="stepActive">
|
||||
<el-step title="Step 1" :status="initCheck.envLib ? 'success' : 'wait'">
|
||||
<template #description>
|
||||
<el-card class="mtb-10">
|
||||
<div class="fxsb min-h32">
|
||||
<div>
|
||||
<el-text class="pr-10">{{ t('envLibDownload') }}:</el-text>
|
||||
<el-text>
|
||||
{{ envLibDownloadLoading ? t('envLibDownloading')
|
||||
: (initCheck.envLib ? t('checkedPass') : t('checkedFailed')) }}
|
||||
</el-text>
|
||||
</div>
|
||||
<el-button type="primary" class="mlr-10" @click="envLibDownload"
|
||||
v-show="!initCheck.envLib && !envLibDownloadLoading">
|
||||
{{ t('envLibDownload') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="wp-100" v-show="envLibDownloadLoading">
|
||||
<el-progress :percentage="envLibDownloadProgress.percentage" :stroke-width="15"
|
||||
:status="envLibDownloadProgress.status" striped
|
||||
:striped-flow="envLibDownloadProgress.status != 'success'" :duration="10" class="mtb-10" />
|
||||
<div class="wp-100">
|
||||
<el-text class="download-progress-tips">{{ t('envLibDownloadTips') }}</el-text>
|
||||
<el-text class="download-progress-tips" truncated>
|
||||
{{ envLibDownloadProgress.text }}
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
</el-step>
|
||||
|
||||
<el-step title="Step 2" :status="initCheck.serverConfig ? 'success' : 'wait'">
|
||||
<template #description>
|
||||
<el-card class="mtb-10">
|
||||
<template #header>
|
||||
<div class="fxsb">
|
||||
<div>
|
||||
<el-text class="pr-10">{{ t('serverConfig') }}:</el-text>
|
||||
<el-text>{{ initCheck.serverConfig ? t('checkedPass') : t('checkedFailed') }}</el-text>
|
||||
</div>
|
||||
<el-link type="primary" class="mlr-10" @click="toEdit('server')">{{ t('edit') }}</el-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Show Data -->
|
||||
<div class="card-row-wrap" v-show="serverShow">
|
||||
<div class="card-row-item">
|
||||
<el-text>host:</el-text>
|
||||
<el-text>{{ serverConfig.host }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>port:</el-text>
|
||||
<el-text tag="p">{{ serverConfig.port }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Data -->
|
||||
<el-form ref="ruleFormRef" :model="serverConfigUpd" status-icon :rules="rules" v-show="serverEdit">
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-item">
|
||||
<el-text>{{ t('step2') }}</el-text>
|
||||
<el-form-item prop="host">
|
||||
<el-input v-model="serverConfigUpd.host" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>port:</el-text>
|
||||
<el-form-item prop="port">
|
||||
<el-input v-model="serverConfigUpd.port" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-aline fxc" v-show="serverEdit">
|
||||
<el-button class="mlr-10" @click="toShow('server')">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" class="mlr-10" @click="submitServerConfig">{{ t('submit') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</template>
|
||||
</el-step>
|
||||
|
||||
<el-step title="Step 3" :status="initCheck.llmConfig ? 'success' : 'wait'">
|
||||
<template #description>
|
||||
<el-card class="mtb-10">
|
||||
<template #header>
|
||||
<div class="fxsb">
|
||||
<div>
|
||||
<el-text class="pr-10">{{ t('llmConfig') }}:</el-text>
|
||||
<el-text>{{ initCheck.llmConfig ? t('checkedPass') : t('checkedFailed') }}</el-text>
|
||||
</div>
|
||||
<div>
|
||||
<el-link type="primary" class="no-select plr-6" @click="toEdit('llm')" v-show="llmShow">
|
||||
{{ t('edit') }}
|
||||
</el-link>
|
||||
<el-link type="primary" class="no-select plr-6" @click="toShow('llm')" v-show="llmEdit">
|
||||
{{ t('cancel') }}
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Show Data -->
|
||||
<div class="card-row-wrap" v-show="llmShow">
|
||||
<div class="card-row-item">
|
||||
<el-text>model:</el-text>
|
||||
<el-text>{{ llmConfig.model }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>base_url:</el-text>
|
||||
<el-text tag="p">{{ llmConfig.base_url }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>api_key:</el-text>
|
||||
<el-text>{{ llmConfig.api_key }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>max_tokens:</el-text>
|
||||
<el-text>{{ llmConfig.max_tokens }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>temperature:</el-text>
|
||||
<el-text>{{ llmConfig.temperature }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Module -->
|
||||
<el-form ref="ruleFormRef" :model="llmConfigUpd" status-icon :rules="rules" v-show="llmEdit">
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-item">
|
||||
<el-text>model:</el-text>
|
||||
<el-form-item prop="model">
|
||||
<el-input v-model="llmConfigUpd.model" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>base_url:</el-text>
|
||||
<el-form-item prop="base_url">
|
||||
<el-input v-model="llmConfigUpd.base_url" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>api_key:</el-text>
|
||||
<el-form-item prop="api_key">
|
||||
<el-input v-model="llmConfigUpd.api_key" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>max_tokens:</el-text>
|
||||
<el-form-item prop="max_tokens">
|
||||
<el-input v-model="llmConfigUpd.max_tokens" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>temperature:</el-text>
|
||||
<el-form-item prop="temperature">
|
||||
<el-input v-model="llmConfigUpd.temperature" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-aline fxc" v-show="llmEdit">
|
||||
<el-button class="mlr-10" @click="toShow('llm')">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" class="mlr-10" @click="submitLlmConfig">{{ t('submit') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</template>
|
||||
</el-step>
|
||||
</el-steps>
|
||||
|
||||
</el-card>
|
||||
|
||||
<el-card v-show="initCheckPass">
|
||||
<div class="fxc mtb-20">
|
||||
<el-button type="primary" @click="startTheExperience" size="large"> {{ t('startTheExperience') }} </el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { EventsEmit, EventsOff, EventsOn } from '../../../wailsjs/runtime/runtime'
|
||||
|
||||
const utils = inject('utils')
|
||||
const verify = inject('verify')
|
||||
const files = inject('files')
|
||||
const router = useRouter()
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 视图模式
|
||||
const viewModel = reactive({
|
||||
env: 'show',
|
||||
server: 'show',
|
||||
llm: 'show',
|
||||
})
|
||||
|
||||
function toShow(model) {
|
||||
console.log("toShow:" + model)
|
||||
viewModel[model] = 'show'
|
||||
}
|
||||
|
||||
function toEdit(model) {
|
||||
console.log("toEdit:" + model)
|
||||
viewModel[model] = 'edit'
|
||||
}
|
||||
|
||||
const envShow = computed(() => {
|
||||
return viewModel.env == 'show'
|
||||
})
|
||||
|
||||
const envEdit = computed(() => {
|
||||
return viewModel.env == 'edit'
|
||||
})
|
||||
|
||||
const serverShow = computed(() => {
|
||||
return viewModel.server == 'show'
|
||||
})
|
||||
|
||||
const serverEdit = computed(() => {
|
||||
return viewModel.server == 'edit'
|
||||
})
|
||||
|
||||
const llmShow = computed(() => {
|
||||
return viewModel.llm == 'show'
|
||||
})
|
||||
|
||||
const llmEdit = computed(() => {
|
||||
return viewModel.llm == 'edit'
|
||||
})
|
||||
|
||||
const readConfigSuccess = ref(true)
|
||||
|
||||
const serverConfig = reactive({
|
||||
host: null,
|
||||
port: null,
|
||||
})
|
||||
|
||||
const serverConfigUpd = reactive({
|
||||
host: null,
|
||||
port: null,
|
||||
})
|
||||
|
||||
|
||||
const llmConfig = reactive({
|
||||
model: null,
|
||||
base_url: null,
|
||||
api_key: null,
|
||||
max_tokens: null,
|
||||
temperature: null,
|
||||
})
|
||||
|
||||
const llmConfigUpd = reactive({
|
||||
model: null,
|
||||
base_url: null,
|
||||
api_key: null,
|
||||
max_tokens: null,
|
||||
temperature: null,
|
||||
})
|
||||
|
||||
const initCheck = reactive({
|
||||
envLib: true,
|
||||
serverConfig: false,
|
||||
llmConfig: false,
|
||||
})
|
||||
|
||||
const envLibDownloadProgress = reactive({
|
||||
status: null,
|
||||
percentage: 0,
|
||||
text: '',
|
||||
})
|
||||
|
||||
function envLibCheck() {
|
||||
const envLibPath = appDataPath.value + "\\venv"
|
||||
files.pathExists(envLibPath).then((exists) => {
|
||||
console.log('envLibPath exists: ', exists)
|
||||
if (exists) {
|
||||
initCheck.envLib = true
|
||||
return
|
||||
} else {
|
||||
initCheck.envLib = false
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const envLibDownloadLoading = ref(false)
|
||||
|
||||
function envLibDownload() {
|
||||
const runBatPath = appDataPath.value + "\\run.bat"
|
||||
if (EventsEmit) {
|
||||
// 触发执行bat命令事件给后台
|
||||
console.log('EventsEmit: ', EventsEmit)
|
||||
EventsEmit('bat', 'ExecRunBat', runBatPath)
|
||||
envLibDownloadLoading.value = true
|
||||
venvFolderSizeCheck()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function venvFolderSizeCheck() {
|
||||
const targetSize = 1024 * 1024 * 1024 * 2 // 2GB
|
||||
// 检测venv文件夹大小, 计算百分比
|
||||
const envLibPath = appDataPath.value + "\\venv"
|
||||
await files.awaitDirSize(envLibPath).then((size) => {
|
||||
console.log('envLibPath size: ', size)
|
||||
const percentage = Math.floor((size / targetSize) * 100)
|
||||
if (envLibDownloadProgress.percentage < percentage) {
|
||||
envLibDownloadProgress.percentage = percentage
|
||||
}
|
||||
console.log('envLibDownloadProgress.percentage: ', envLibDownloadProgress.percentage)
|
||||
if (envLibDownloadProgress.percentage >= 100) {
|
||||
envLibDownloadProgress.percentage = 99
|
||||
}
|
||||
})
|
||||
// 递归检测
|
||||
if (envLibDownloadLoading.value && envLibDownloadProgress.percentage < 100) {
|
||||
setTimeout(() => {
|
||||
venvFolderSizeCheck()
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
const appDataPath = ref()
|
||||
|
||||
onMounted(async () => {
|
||||
await files.awaitAppPath().then((path) => {
|
||||
appDataPath.value = path
|
||||
console.log('appDataPath: ', appDataPath.value)
|
||||
if (appDataPath.value && appDataPath.value.endsWith('\\desktop\\build\\bin')) {
|
||||
appDataPath.value = appDataPath.value.replace('\\desktop\\build\\bin', '')
|
||||
}
|
||||
console.log('appDataPath: ', appDataPath.value)
|
||||
})
|
||||
// 读取配置文件config/config.toml
|
||||
loadConfig()
|
||||
|
||||
// 监听后台执行bat命令事件
|
||||
EventsOn('ExecRunBat', (type, data) => {
|
||||
console.log('bat cmd exc: ', type, data)
|
||||
envLibDownloadLoading.value = true
|
||||
if (type == 'msg') {
|
||||
envLibDownloadProgress.text = data
|
||||
}
|
||||
if (data.startsWith('Press any key to continue')
|
||||
|| (data.endsWith('. . .') && envLibDownloadProgress.percentage >= 99)) {
|
||||
// data.endsWith('. . .') 兼容中文环境下的bat命令输出
|
||||
console.log('envLib download finished')
|
||||
envLibDownloadLoading.value = false
|
||||
envLibDownloadProgress.percentage = 100
|
||||
envLibDownloadProgress.status = 'success'
|
||||
envLibDownloadProgress.text = t('envLibDownloadSuccess')
|
||||
initCheck.envLib = true
|
||||
utils.pop(t('envLibDownloadSuccess'))
|
||||
}
|
||||
})
|
||||
|
||||
// 检查环境
|
||||
envLibCheck()
|
||||
})
|
||||
|
||||
const stepActive = computed(() => {
|
||||
if (!initCheck.envLib) {
|
||||
return 0
|
||||
}
|
||||
if (!initCheck.serverConfig) {
|
||||
return 1
|
||||
}
|
||||
if (!initCheck.llmConfig) {
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const initCheckPass = computed(() => {
|
||||
return initCheck.envLib && initCheck.serverConfig && initCheck.llmConfig
|
||||
})
|
||||
|
||||
function startTheExperience() {
|
||||
config.setInit(true)
|
||||
router.push({ path: '/task' })
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
// 取消监听后台执行bat命令事件
|
||||
EventsOff('ExecRunBat', null)
|
||||
})
|
||||
|
||||
function loadConfig() {
|
||||
const filePath = appDataPath.value + "\\config\\config.toml"
|
||||
files.readTomlNode(filePath, "server").then((node) => {
|
||||
console.log("config/config.toml: ", node)
|
||||
if (utils.isBlank(node)) {
|
||||
readConfigSuccess.value = false
|
||||
initCheck.serverConfig = false
|
||||
utils.pop(t('readTomlFailed'))
|
||||
return
|
||||
}
|
||||
utils.copyProps(node, serverConfig)
|
||||
utils.copyProps(node, serverConfigUpd)
|
||||
initCheck.serverConfig = true
|
||||
})
|
||||
|
||||
files.readTomlNode(filePath, "llm").then((node) => {
|
||||
console.log("config/config.toml: ", node)
|
||||
if (utils.isBlank(node)) {
|
||||
readConfigSuccess.value = false
|
||||
initCheck.llmConfig = false
|
||||
utils.pop(t('readTomlFailed'))
|
||||
return
|
||||
}
|
||||
utils.copyProps(node, llmConfig)
|
||||
utils.copyProps(node, llmConfigUpd)
|
||||
initCheck.llmConfig = true
|
||||
})
|
||||
}
|
||||
|
||||
function saveServerConfig() {
|
||||
const filePath = appDataPath.value + "\\config\\config.toml"
|
||||
files.readAll(filePath)
|
||||
files.saveTomlNode(filePath, "server", serverConfigUpd).then((resp) => {
|
||||
console.log("config/config.toml: ", resp)
|
||||
loadConfig()
|
||||
toShow('server')
|
||||
})
|
||||
}
|
||||
|
||||
function saveLlmConfig() {
|
||||
const filePath = appDataPath.value + "\\config\\config.toml"
|
||||
files.readAll(filePath)
|
||||
files.saveTomlNode(filePath, "llm", llmConfigUpd).then((resp) => {
|
||||
console.log("config/config.toml: ", resp)
|
||||
loadConfig()
|
||||
toShow('llm')
|
||||
})
|
||||
}
|
||||
|
||||
const ruleFormRef = ref()
|
||||
|
||||
const rules = reactive({
|
||||
host: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
port: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
|
||||
model: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
base_url: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
api_key: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
max_tokens: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
temperature: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
})
|
||||
|
||||
const submitServerConfig = async () => {
|
||||
try {
|
||||
await ruleFormRef.value.validate();
|
||||
if (!utils.hasDfProps(serverConfig, serverConfigUpd)) {
|
||||
ElMessage.success('未发生更改!');
|
||||
toShow('server')
|
||||
return
|
||||
}
|
||||
ElMessage.success('验证通过,提交表单');
|
||||
saveServerConfig()
|
||||
} catch (error) {
|
||||
console.log('error: ', error);
|
||||
ElMessage.error('参数验证失败');
|
||||
}
|
||||
}
|
||||
|
||||
const submitLlmConfig = async () => {
|
||||
try {
|
||||
await ruleFormRef.value.validate();
|
||||
if (!utils.hasDfProps(llmConfig, llmConfigUpd)) {
|
||||
ElMessage.success('未发生更改!');
|
||||
toShow('llm')
|
||||
return
|
||||
}
|
||||
ElMessage.success('验证通过,提交表单');
|
||||
saveLlmConfig()
|
||||
toShow('llm')
|
||||
} catch (error) {
|
||||
ElMessage.error('参数验证失败');
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-step.is-vertical .el-step__main) {
|
||||
width: calc(100% - 34px);
|
||||
}
|
||||
|
||||
.download-progress-tips {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -131,7 +131,7 @@ const baseEdit = computed(() => {
|
||||
return viewModel.base == 'edit'
|
||||
})
|
||||
|
||||
const readConfigSuccess = ref(false)
|
||||
const readConfigSuccess = ref(true)
|
||||
|
||||
const baseNoData = computed(() => {
|
||||
return baseShow && !readConfigSuccess.value
|
||||
@@ -158,43 +158,54 @@ function clearCache() {
|
||||
utils.pop(t('clearCacheSuccess'))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 读取配置文件config/config.toml
|
||||
files.readAll("@/../../config/config.toml").then((fileContent) => {
|
||||
console.log("config/config.toml: ", fileContent)
|
||||
if (utils.notBlank(fileContent)) {
|
||||
readConfigSuccess.value = true
|
||||
} else {
|
||||
utils.pop(t('readConfigFailed'))
|
||||
return
|
||||
}
|
||||
const lines = utils.stringToLines(fileContent)
|
||||
const appDataPath = ref()
|
||||
|
||||
// 读取[llm]
|
||||
const llmStart = lines.findIndex((line) => {
|
||||
return line.includes("[llm]")
|
||||
})
|
||||
for (let i = llmStart + 1; i < lines.length; i++) {
|
||||
console.log("line: ", lines[i])
|
||||
// 判定是否到了下个配置模块
|
||||
if (lines[i].startsWith("[")) {
|
||||
break
|
||||
}
|
||||
// 读取配置
|
||||
const line = lines[i]
|
||||
const lineArr = line.split("=")
|
||||
if (lineArr.length != 2) {
|
||||
continue
|
||||
}
|
||||
const key = lineArr[0].trim()
|
||||
const value = lineArr[1].trim()
|
||||
llmConfig[key] = value
|
||||
onMounted(async () => {
|
||||
await files.awaitAppPath().then((path) => {
|
||||
appDataPath.value = path
|
||||
console.log('appDataPath: ', appDataPath.value)
|
||||
if (appDataPath.value && appDataPath.value.endsWith('\\desktop\\build\\bin')) {
|
||||
appDataPath.value = appDataPath.value.replace('\\desktop\\build\\bin', '')
|
||||
}
|
||||
console.log("llmConfig read from file: ", llmConfig)
|
||||
utils.copyProps(llmConfig, llmConfigUpd)
|
||||
console.log('appDataPath: ', appDataPath.value)
|
||||
})
|
||||
// 读取配置文件config/config.toml
|
||||
loadLlmConfig()
|
||||
})
|
||||
|
||||
function loadLlmConfig() {
|
||||
const filePath = appDataPath.value + "\\config\\config.toml"
|
||||
files.readTomlNode(filePath, "llm").then((node) => {
|
||||
console.log("config/config.toml: ", node)
|
||||
if (utils.isBlank(node)) {
|
||||
readConfigSuccess.value = false
|
||||
utils.pop(t('readTomlFailed'))
|
||||
return
|
||||
}
|
||||
utils.copyProps(node, llmConfig)
|
||||
utils.copyProps(node, llmConfigUpd)
|
||||
})
|
||||
}
|
||||
|
||||
function saveLlmConfig() {
|
||||
const filePath = appDataPath.value + "\\config\\config.toml"
|
||||
files.readAll(filePath)
|
||||
files.saveTomlNode(filePath, "llm", llmConfigUpd).then((resp) => {
|
||||
console.log("config/config.toml: ", resp)
|
||||
loadLlmConfig()
|
||||
toShow('llm')
|
||||
})
|
||||
}
|
||||
|
||||
const ruleFormRef = ref()
|
||||
|
||||
const rules = reactive({
|
||||
model: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
base_url: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
api_key: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
max_tokens: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
temperature: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
})
|
||||
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
@@ -205,20 +216,13 @@ const submitForm = async () => {
|
||||
return
|
||||
}
|
||||
ElMessage.success('验证通过,提交表单');
|
||||
// update()
|
||||
saveLlmConfig()
|
||||
toShow('base')
|
||||
} catch (error) {
|
||||
ElMessage.error('参数验证失败');
|
||||
}
|
||||
}
|
||||
|
||||
const rules = reactive({
|
||||
model: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
base_url: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
api_key: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
max_tokens: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
temperature: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<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>
|
||||
@@ -1,407 +0,0 @@
|
||||
<template>
|
||||
<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) {
|
||||
// 添加此step到stepList
|
||||
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())
|
||||
}
|
||||
// 判定添加到stepList中的哪个元素元素的subList中
|
||||
console.log("stepList:", taskInfo.value.stepList, "idx:", idx)
|
||||
let parentStep = null
|
||||
const pStepIndex = taskInfo.value.stepList.findIndex(parentStep => parentStep.idx > idx)
|
||||
console.log("pStepIndex:", pStepIndex)
|
||||
if (pStepIndex != -1) {
|
||||
// 取pStep的上一个元素
|
||||
parentStep = taskInfo.value.stepList[pStepIndex - 1]
|
||||
} else {
|
||||
// 不存在时, 添加到stepList最后一个元素末尾
|
||||
parentStep = taskInfo.value.stepList[taskInfo.value.stepList.length - 1]
|
||||
}
|
||||
console.log("parentStep:", parentStep)
|
||||
const existSubStep = parentStep.subList.find(existSubStep => existSubStep.idx == idx)
|
||||
if (!existSubStep) {
|
||||
// 不存在时, 添加到末尾
|
||||
parentStep.subList.push(subStep)
|
||||
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>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="main-content fc">
|
||||
<el-scrollbar ref="scrollRef">
|
||||
<div class="output-area" v-show="taskInfo.taskId != null">
|
||||
<div class="main-content-full-height">
|
||||
<el-scrollbar ref="scrollRef" class="scroll-wrap">
|
||||
<div class="dialog-area" v-show="taskInfo.taskId != null">
|
||||
|
||||
<div class="dialog-user">
|
||||
<div class="blank"></div>
|
||||
@@ -19,78 +19,95 @@
|
||||
</div>
|
||||
|
||||
<div class="dialog-ai">
|
||||
<el-text class="title"> OpenManus-AI </el-text>
|
||||
|
||||
<div class="fxsb">
|
||||
<el-text class="title"> OpenManus-AI </el-text>
|
||||
<div class="plr-10">
|
||||
<el-link type="primary" class="no-select plr-6" @click="expandAll" v-show="notAllExpanded">
|
||||
{{ t('expandAll') }}
|
||||
</el-link>
|
||||
<el-link type="primary" class="no-select plr-6" @click="collapseAll" v-show="notAllCollapsed">
|
||||
{{ t('collapseAll') }}
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-aline">
|
||||
<el-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>
|
||||
<el-collapse v-model="activeNames" @change="handleChange" class="wp-100">
|
||||
<el-collapse-item v-for="(step, index) in taskInfo.stepList" :key="index" :name="step.stepNo"
|
||||
:icon="ArrowRight">
|
||||
<template #title>
|
||||
<div class="fxsb wp-100">
|
||||
<div>
|
||||
<el-text> {{ subStep.result }} </el-text>
|
||||
<el-text class="collapse-color-label mr-10" :class="utils.colorByLabel('step')">
|
||||
{{ t('step') }}
|
||||
</el-text>
|
||||
<el-text class="pl-10">{{ step.result }}</el-text>
|
||||
</div>
|
||||
<el-divider v-if="subIndex != step.subList.length - 1" />
|
||||
<el-text class="tips-text plr-10">{{ step.createdDt }}</el-text>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</template>
|
||||
<div v-for="(subStep, subIndex) in step.subList">
|
||||
<div class="fxsb mtb-10">
|
||||
<el-text> {{ subStep.type }} </el-text>
|
||||
<el-text class="sub-step-time"> {{ subStep.createdDt }} </el-text>
|
||||
</div>
|
||||
<div>
|
||||
<el-text> {{ subStep.result }} </el-text>
|
||||
</div>
|
||||
<el-divider v-if="subIndex != step.subList.length - 1" />
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<div class="input-area">
|
||||
<div class="ctrl-area">
|
||||
<div class="task-area wp-100" v-show="!newTaskFlag">
|
||||
<div class="generated fxc">
|
||||
<div class="generated-label">{{ t('generatedContent') }}</div>
|
||||
<div class="generated-folder">You Can Check Generated Files Here, This Function Is In Developing.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-tools">
|
||||
<div class="new-task" v-show="!newTaskFlag">
|
||||
<el-button round @click="startNewTask">
|
||||
<el-icon :size="16">
|
||||
<CirclePlus />
|
||||
<div class="input-area">
|
||||
<div class="progress-area w-80">
|
||||
<!-- progress-wrap是flex-column布局,器元素高度不会撑开父元素高度 -->
|
||||
<div class="progress-wrap">
|
||||
<el-progress type="dashboard" class="mt-10" :percentage="taskInfo.percentage"
|
||||
:status="taskInfo.progressStatus" :stroke-width="6" :width="60" v-show="taskInfo.status != null">
|
||||
<template #default="{ percentage }">
|
||||
<span class="percentage-value">{{ percentage }}%</span>
|
||||
</template>
|
||||
</el-progress>
|
||||
<el-text class="progress-text">{{ taskInfo.status }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-box">
|
||||
<el-icon @click="uploadFile" class="add-file-area" :size="24">
|
||||
<FolderAdd />
|
||||
</el-icon>
|
||||
<el-input ref="promptEle" type="textarea" v-model="prompt" class="input-style" style="border: none;"
|
||||
:autosize="{ minRows: 1, maxRows: 4 }" autofocus :placeholder="t('promptInputPlaceHolder')"
|
||||
@keydown.enter="handleInputEnter" />
|
||||
|
||||
<el-link class="send-area">
|
||||
<el-icon @click="sendPrompt" :size="24" v-show="!loading && taskInfo.status != 'running'">
|
||||
<Promotion />
|
||||
</el-icon>
|
||||
<span> {{ t('newTask') }} </span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="task-status" v-show="taskInfo.taskId != null">
|
||||
<el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text>
|
||||
<el-text>{{ taskInfo.status }}</el-text>
|
||||
<el-icon @click="stop" :size="24" v-show="loading || taskInfo.status == 'running'">
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-box">
|
||||
<el-icon @click="uploadFile" class="add-file-area" :size="24">
|
||||
<FolderAdd />
|
||||
</el-icon>
|
||||
<el-input ref="promptEle" type="textarea" v-model="prompt" class="input-style" style="border: none;"
|
||||
:autosize="{ minRows: 1, maxRows: 4 }" autofocus :placeholder="t('promptInputPlaceHolder')"
|
||||
@keydown.enter="handleInputEnter" />
|
||||
|
||||
<el-link class="send-area">
|
||||
<el-icon @click="sendPrompt" :size="24" v-show="!loading && taskInfo.status != 'running'">
|
||||
<Promotion />
|
||||
</el-icon>
|
||||
<el-icon @click="stop" :size="24" v-show="loading || taskInfo.status == 'running'">
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-text class="tips">{{ t('openManusAgiTips') }}</el-text>
|
||||
<div class="tips">
|
||||
<el-text class="tips-text">{{ t('openManusAgiTips') }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,12 +115,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { FolderAdd, Promotion, CirclePlus, CircleClose } from '@element-plus/icons-vue'
|
||||
import { ref, reactive, watch, inject, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { FolderAdd, Promotion, CircleClose, ArrowRight } from '@element-plus/icons-vue'
|
||||
import { EventsEmit, EventsOff, EventsOn } from '../../../wailsjs/runtime/runtime'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const utils = inject('utils')
|
||||
const files = inject('files')
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -113,7 +132,7 @@ const promptEle = ref(null)
|
||||
const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message']
|
||||
const eventSource = ref(null)
|
||||
|
||||
const newTaskFlag = ref(false)
|
||||
const newTaskFlag = ref(true)
|
||||
|
||||
const taskInfo = computed(() => {
|
||||
if (newTaskFlag.value) {
|
||||
@@ -122,67 +141,125 @@ const taskInfo = computed(() => {
|
||||
return config.getCurrTask()
|
||||
})
|
||||
|
||||
const serverConfig = ({
|
||||
host: null,
|
||||
port: null,
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const scrollRef = ref(null)
|
||||
|
||||
const activeNames = ref([])
|
||||
|
||||
const notAllExpanded = computed(() => {
|
||||
return taskInfo.value.stepList != null && activeNames.value.length != taskInfo.value.stepList.length
|
||||
})
|
||||
|
||||
const notAllCollapsed = computed(() => {
|
||||
return activeNames.value.length != 0
|
||||
})
|
||||
|
||||
const handleChange = (val) => {
|
||||
console.log("handleChange:", val)
|
||||
}
|
||||
// Expand task log when first loading and auto collapse after 10s.
|
||||
function autoExpandCollapse(stepNo) {
|
||||
scrollToBottom()
|
||||
if (activeNames.value.includes(stepNo)) {
|
||||
return
|
||||
}
|
||||
// console.log("autoExpandCollapse:", stepNo)
|
||||
const hasSubStepList = taskInfo.value.stepList.some(step => {
|
||||
// console.log("stepNo:", step.stepNo, "subList:", JSON.stringify(step.subList))
|
||||
return step.stepNo == stepNo && step.subList != null && step.subList.length > 0
|
||||
})
|
||||
console.log("stepNo", stepNo, "hasSubStepList:", hasSubStepList)
|
||||
if (!hasSubStepList) {
|
||||
return
|
||||
}
|
||||
activeNames.value.push(stepNo)
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// Expand/Collapse all
|
||||
function expandAll() {
|
||||
taskInfo.value.stepList.filter(item => {
|
||||
if (activeNames.value.includes(item.stepNo)) {
|
||||
return
|
||||
}
|
||||
activeNames.value.push(item.stepNo)
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
watch(activeNames.value, (newVal, oldVal) => {
|
||||
console.log("activeNames changed:", newVal, oldVal)
|
||||
})
|
||||
|
||||
|
||||
function collapseAll() {
|
||||
utils.clearArray(activeNames.value)
|
||||
}
|
||||
|
||||
// 建立EventSource连接
|
||||
const buildEventSource = (taskId) => {
|
||||
loading.value = true
|
||||
eventSource.value = new EventSource('http://localhost:5172/tasks/' + taskId + '/events')
|
||||
eventSource.value = new EventSource(remoteBaseUrl.value + '/tasks/' + taskId + '/events')
|
||||
eventSource.value.onmessage = (event) => {
|
||||
console.log('Received data:', event.data)
|
||||
// 在这里处理接收到的数据 不起作用
|
||||
// 在这里处理接收到的数据 不起作用 可能是服务端接口有问题
|
||||
}
|
||||
|
||||
eventTypes.forEach(type => {
|
||||
eventSource.value.addEventListener(type, (event) => handleEvent(event, type))
|
||||
})
|
||||
|
||||
eventSource.value.onerror = (error) => {
|
||||
console.error('EventSource failed:', error)
|
||||
// 处理错误情况
|
||||
loading.value = false
|
||||
eventSource.value.close()
|
||||
taskInfo.value.status = "failed"
|
||||
utils.pop("任务执行失败", "error")
|
||||
}
|
||||
/* eventSource.value.onerror = (error) => {
|
||||
console.error('EventSource failed:', error)
|
||||
// 处理错误情况 可能是服务端接口有问题
|
||||
// loading.value = false
|
||||
// eventSource.value.close()
|
||||
// taskInfo.value.status = "failed"
|
||||
// taskInfo.value.progressStatus = "exception"
|
||||
// utils.pop(t('taskExecFailed'), "error")
|
||||
} */
|
||||
|
||||
}
|
||||
|
||||
const handleEvent = (event, type) => {
|
||||
console.log('Received event, type:', type, event.data)
|
||||
// clearInterval(heartbeatTimer);
|
||||
// console.log('Received event, type:', type, event.data)
|
||||
// clearInterval(heartbeatTimer)
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("type:", type, "data:", data)
|
||||
const data = JSON.parse(event.data)
|
||||
// console.log("type:", type, "data:", data)
|
||||
if (eventSource.value.readyState === EventSource.CLOSED) {
|
||||
console.log('Connection is closed');
|
||||
// console.log('Connection is closed')
|
||||
}
|
||||
if (type == "complete" || data.status == "completed") {
|
||||
console.log('task completed');
|
||||
// console.log('task completed')
|
||||
loading.value = false
|
||||
eventSource.value.close()
|
||||
taskInfo.value.status = "success"
|
||||
taskInfo.value.progressStatus = "success"
|
||||
utils.pop("任务已完成", "success")
|
||||
return
|
||||
}
|
||||
// autoScroll(stepContainer);
|
||||
// autoScroll(stepContainer)
|
||||
buildOutput(taskInfo.value.taskId)
|
||||
} catch (e) {
|
||||
console.error(`Error handling ${type} event:`, 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)
|
||||
await utils.awaitGet(remoteBaseUrl.value + '/tasks/' + taskId).then(data => {
|
||||
// console.log("task info resp:", data)
|
||||
buildStepList(data.steps)
|
||||
console.log("stepList:", taskInfo.value.stepList)
|
||||
// 滚动到底部
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 100)
|
||||
// console.log("stepList:", taskInfo.value.stepList)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -194,6 +271,7 @@ const buildStepList = (steps) => {
|
||||
if (step.type == "log" && step.result.startsWith("Executing step")) {
|
||||
const stepStr = step.result.replace("Executing step ", "").replace("\n", "")
|
||||
const stepNo = stepStr.split("/")[0]
|
||||
const stepCount = stepStr.split("/")[1]
|
||||
if (taskInfo.value.stepList.length < stepNo) {
|
||||
// 添加此step到stepList
|
||||
const parentStep = {
|
||||
@@ -205,8 +283,10 @@ const buildStepList = (steps) => {
|
||||
createdDt: utils.dateFormat(new Date())
|
||||
}
|
||||
taskInfo.value.stepList.push(parentStep)
|
||||
taskInfo.value.percentage = Math.floor((stepNo / stepCount) * 100)
|
||||
return
|
||||
}
|
||||
scrollToBottom()
|
||||
} else {
|
||||
// 子步骤
|
||||
const subStep = {
|
||||
@@ -216,10 +296,10 @@ const buildStepList = (steps) => {
|
||||
createdDt: utils.dateFormat(new Date())
|
||||
}
|
||||
// 判定添加到stepList中的哪个元素元素的subList中
|
||||
console.log("stepList:", taskInfo.value.stepList, "idx:", idx)
|
||||
// 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)
|
||||
// console.log("pStepIndex:", pStepIndex)
|
||||
if (pStepIndex != -1) {
|
||||
// 取pStep的上一个元素
|
||||
parentStep = taskInfo.value.stepList[pStepIndex - 1]
|
||||
@@ -227,16 +307,66 @@ const buildStepList = (steps) => {
|
||||
// 不存在时, 添加到stepList最后一个元素末尾
|
||||
parentStep = taskInfo.value.stepList[taskInfo.value.stepList.length - 1]
|
||||
}
|
||||
console.log("parentStep:", parentStep)
|
||||
// console.log("parentStep:", parentStep)
|
||||
const existSubStep = parentStep.subList.find(existSubStep => existSubStep.idx == idx)
|
||||
if (!existSubStep) {
|
||||
// 不存在时, 添加到末尾
|
||||
parentStep.subList.push(subStep)
|
||||
return
|
||||
}
|
||||
autoExpandCollapse(taskInfo.value.stepList[taskInfo.value.stepList.length - 1].stepNo)
|
||||
}
|
||||
scrollToBottom()
|
||||
})
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
const appDataPath = ref()
|
||||
|
||||
onMounted(async () => {
|
||||
await files.awaitAppPath().then((path) => {
|
||||
appDataPath.value = path
|
||||
console.log('appDataPath: ', appDataPath.value)
|
||||
if (appDataPath.value && appDataPath.value.endsWith('\\desktop\\build\\bin')) {
|
||||
appDataPath.value = appDataPath.value.replace('\\desktop\\build\\bin', '')
|
||||
}
|
||||
console.log('appDataPath: ', appDataPath.value)
|
||||
})
|
||||
// 读取配置文件config/config.toml
|
||||
loadServerConfig()
|
||||
if (taskInfo.value.status == "running") {
|
||||
taskInfo.value.status = null
|
||||
}
|
||||
startNewTask()
|
||||
})
|
||||
|
||||
async function checkStartOpenManusService() {
|
||||
// 检查服务是否启动, 未启动则启动
|
||||
// windows查询占用端口的应用 netstat -ano | findstr 5172
|
||||
// tasklist | findstr [pid] 12345
|
||||
// taskkill -f -pid 12345
|
||||
let isServiceRunning = false
|
||||
await utils.awaitCheckPort(serverConfig.port).then((portInUse) => {
|
||||
isServiceRunning = portInUse
|
||||
console.log("isServiceRunning:", isServiceRunning)
|
||||
})
|
||||
if (!isServiceRunning) {
|
||||
utils.pop(t('openManusIsStarting'))
|
||||
EventsEmit('pyFile', 'ExecAppPy', appDataPath.value + '\\app.py')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function loadServerConfig() {
|
||||
const filePath = appDataPath.value + "\\config\\config.toml"
|
||||
files.readTomlNode(filePath, "server").then((node) => {
|
||||
console.log("config/config.toml: ", node)
|
||||
if (utils.isBlank(node)) {
|
||||
utils.pop(t('readTomlFailed'))
|
||||
return
|
||||
}
|
||||
utils.copyProps(node, serverConfig)
|
||||
checkStartOpenManusService()
|
||||
})
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -247,18 +377,18 @@ onUnmounted(() => {
|
||||
})
|
||||
|
||||
function handleInputEnter(event) {
|
||||
console.log("handleInputEnter:", event)
|
||||
// console.log("handleInputEnter:", event)
|
||||
event.preventDefault()
|
||||
sendPrompt()
|
||||
}
|
||||
|
||||
function uploadFile() {
|
||||
utils.pop("暂不支持,开发中", "warning")
|
||||
utils.pop(t('inDevelopment'), "warning")
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (scrollRef.value) {
|
||||
console.log("scrollRef:", scrollRef.value, scrollRef.value.wrapRef)
|
||||
// console.log("scrollRef:", scrollRef.value, scrollRef.value.wrapRef)
|
||||
const container = scrollRef.value.wrapRef
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight
|
||||
@@ -284,7 +414,7 @@ function sendPrompt() {
|
||||
eventSource.value.close()
|
||||
}
|
||||
|
||||
utils.post('http://localhost:5172/tasks', { prompt: prompt.value }).then(data => {
|
||||
utils.post(remoteBaseUrl.value + '/tasks', { prompt: prompt.value }).then(data => {
|
||||
if (!data.task_id) {
|
||||
throw new Error('Invalid task ID')
|
||||
}
|
||||
@@ -303,39 +433,68 @@ function sendPrompt() {
|
||||
// 建立新的EventSource连接
|
||||
buildEventSource(data.task_id)
|
||||
|
||||
console.log("new task created:", newTask)
|
||||
// console.log("new task created:", newTask)
|
||||
}).catch(error => {
|
||||
console.error('Failed to create task:', error)
|
||||
})
|
||||
}
|
||||
|
||||
function stop() {
|
||||
console.log("stop")
|
||||
// console.log("stop")
|
||||
loading.value = false
|
||||
console.log("eventSource:", eventSource.value, "taskInfo:", taskInfo.value)
|
||||
// console.log("eventSource:", eventSource.value, "taskInfo:", taskInfo.value)
|
||||
if (eventSource.value != null) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
|
||||
taskInfo.value.status = "terminated"
|
||||
taskInfo.value.progressStatus = "exception"
|
||||
utils.pop("用户终止任务", "error")
|
||||
}
|
||||
|
||||
function startNewTask() {
|
||||
console.log("startNewTask:", taskInfo.value)
|
||||
// console.log("startNewTask:", taskInfo.value)
|
||||
if (taskInfo.value.status == "running") {
|
||||
utils.pop("请先终止当前任务", "error")
|
||||
return
|
||||
}
|
||||
newTaskFlag.value = true
|
||||
prompt.value = ''
|
||||
utils.clearArray(activeNames.value)
|
||||
}
|
||||
|
||||
|
||||
const remoteBaseUrl = computed(() => {
|
||||
let url
|
||||
if (utils.notBlank(serverConfig.host)) {
|
||||
url = serverConfig.host
|
||||
if (url.startsWith("\"")) {
|
||||
url = url.substring(1)
|
||||
}
|
||||
if (url.endsWith("\"")) {
|
||||
url = url.substring(0, url.length - 1)
|
||||
}
|
||||
if (utils.notBlank(serverConfig.port)) {
|
||||
url = url + ":" + serverConfig.port
|
||||
}
|
||||
if (!url.startsWith("http")) {
|
||||
url = "http://" + url
|
||||
}
|
||||
} else {
|
||||
// default
|
||||
url = "http://localhost:5172"
|
||||
}
|
||||
return url
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.output-area {
|
||||
.scroll-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-area {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -393,12 +552,10 @@ function startNewTask() {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
.ctrl-area {
|
||||
flex-grow: 0;
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
padding-left: 80px;
|
||||
padding-right: 80px;
|
||||
padding-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -406,25 +563,78 @@ function startNewTask() {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-area .input-tools {
|
||||
.ctrl-area .task-area {
|
||||
width: 100%;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 10px;
|
||||
padding-right: 7px;
|
||||
margin-bottom: 12px;
|
||||
background-color: var(--el-fg-color);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-area .input-tools .new-task {
|
||||
position: relative;
|
||||
left: -80px;
|
||||
.percentage-value {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.input-area .input-tools .task-status {
|
||||
position: relative;
|
||||
right: -80px;
|
||||
.generated {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
height: 68px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.generated-label {
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.generated-folder {
|
||||
width: 100%;
|
||||
min-height: 54px;
|
||||
margin-left: 16px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
width: 100%;
|
||||
padding-right: 80px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.progress-area {
|
||||
align-self: center;
|
||||
flex-grow: 0;
|
||||
/* margin-left: -6px;
|
||||
margin-right: 10px; */
|
||||
}
|
||||
|
||||
.progress-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 80px;
|
||||
height: 54px;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
margin-top: -10px;
|
||||
width: 70px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.input-box {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
background-color: var(--el-fg-color);
|
||||
@@ -457,13 +667,31 @@ function startNewTask() {
|
||||
}
|
||||
|
||||
.tips {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.tips-text {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.sub-step-time {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.icon-ele {
|
||||
margin: 0 8px 0 auto;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.collapse-color-label {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="main-content fc">
|
||||
<div class="main-content-full-height">
|
||||
<!-- 展示模块-暂无数据 -->
|
||||
<div class="no-data" v-show="baseNoData">{{ t('noData') }}</div>
|
||||
|
||||
|
||||
+10
@@ -1,6 +1,16 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function AppPath():Promise<string>;
|
||||
|
||||
export function CheckPort(arg1:string):Promise<boolean>;
|
||||
|
||||
export function DirSize(arg1:string):Promise<number>;
|
||||
|
||||
export function Greet(arg1:string):Promise<string>;
|
||||
|
||||
export function PathExists(arg1:string):Promise<boolean>;
|
||||
|
||||
export function ReadAll(arg1:string):Promise<string>;
|
||||
|
||||
export function SaveFile(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
@@ -2,10 +2,30 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function AppPath() {
|
||||
return window['go']['main']['App']['AppPath']();
|
||||
}
|
||||
|
||||
export function CheckPort(arg1) {
|
||||
return window['go']['main']['App']['CheckPort'](arg1);
|
||||
}
|
||||
|
||||
export function DirSize(arg1) {
|
||||
return window['go']['main']['App']['DirSize'](arg1);
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function PathExists(arg1) {
|
||||
return window['go']['main']['App']['PathExists'](arg1);
|
||||
}
|
||||
|
||||
export function ReadAll(arg1) {
|
||||
return window['go']['main']['App']['ReadAll'](arg1);
|
||||
}
|
||||
|
||||
export function SaveFile(arg1, arg2) {
|
||||
return window['go']['main']['App']['SaveFile'](arg1, arg2);
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
-249
@@ -1,249 +0,0 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
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
|
||||
@@ -1,238 +0,0 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
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);
|
||||
}
|
||||
+4
-2
@@ -4,7 +4,10 @@ go 1.22.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require github.com/wailsapp/wails/v2 v2.10.1
|
||||
require (
|
||||
github.com/wailsapp/wails/v2 v2.10.1
|
||||
golang.org/x/text v0.22.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
@@ -32,7 +35,6 @@ require (
|
||||
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
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// ExecBat 执行bat文件
|
||||
func ExecBatFile(ctx context.Context, batId string, batPath string) {
|
||||
Log("ExecBatFile batPath: ", batPath)
|
||||
if IsBlank(batId) || IsBlank(batPath) {
|
||||
Log("batId or batPath is nil")
|
||||
runtime.EventsEmit(ctx, batId, "error", "batId or batPath is nil")
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("cmd", "/C", "chcp 65001 > nul &&", batPath)
|
||||
// 设置cmd.SysProcAttr.HideWindow为true以隐藏cmd窗口
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "PYTHONIOENCODING=utf-8")
|
||||
|
||||
// 运行命令并获取输出
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
Log("Error creating StdoutPipe for Cmd:", err)
|
||||
runtime.EventsEmit(ctx, batId, "error", err.Error())
|
||||
return
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
Log("Error creating StderrPipe for Cmd:", err)
|
||||
runtime.EventsEmit(ctx, batId, "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
Log("Error starting cmd:", err)
|
||||
runtime.EventsEmit(ctx, batId, "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 读取标准输出和错误输出
|
||||
outputScanner := bufio.NewScanner(stdout)
|
||||
errorScanner := bufio.NewScanner(stderr)
|
||||
|
||||
go func() {
|
||||
for errorScanner.Scan() {
|
||||
// 处理错误输出
|
||||
Log("Error:", errorScanner.Text())
|
||||
runtime.EventsEmit(ctx, batId, "msg", string(errorScanner.Bytes()))
|
||||
}
|
||||
}()
|
||||
|
||||
for outputScanner.Scan() {
|
||||
// 处理标准输出
|
||||
Log("Output:", outputScanner.Text())
|
||||
// 在这里可以将输出逐行发送给客户端,例如通过HTTP响应写入等。
|
||||
|
||||
runtime.EventsEmit(ctx, batId, "msg", string(outputScanner.Bytes()))
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
Log("Error waiting for cmd:", err)
|
||||
runtime.EventsEmit(ctx, batId, "error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func ExecPyFile(ctx context.Context, pyId string, scriptPath string) {
|
||||
// 指定Python解释器的路径,例如在Windows上可能是"python"或"python.exe",在Linux或Mac上是"python3"
|
||||
pythonCmd := "python"
|
||||
Log("ExecPyFile scriptPath: ", scriptPath)
|
||||
// 创建一个*exec.Cmd实例来运行Python脚本
|
||||
cmd := exec.Command(pythonCmd, scriptPath)
|
||||
// 设置cmd.SysProcAttr.HideWindow为true以隐藏cmd窗口
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
// 运行命令并获取输出
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
Log("Error creating StdoutPipe for Cmd:", err)
|
||||
runtime.EventsEmit(ctx, pyId, "error", err.Error())
|
||||
return
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
Log("Error creating StderrPipe for Cmd:", err)
|
||||
runtime.EventsEmit(ctx, pyId, "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
Log("Error starting cmd:", err)
|
||||
runtime.EventsEmit(ctx, pyId, "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 读取标准输出和错误输出
|
||||
outputScanner := bufio.NewScanner(stdout)
|
||||
errorScanner := bufio.NewScanner(stderr)
|
||||
|
||||
go func() {
|
||||
for errorScanner.Scan() {
|
||||
// 处理错误输出
|
||||
Log("Error:", errorScanner.Text())
|
||||
runtime.EventsEmit(ctx, pyId, "msg", string(errorScanner.Bytes()))
|
||||
}
|
||||
}()
|
||||
|
||||
for outputScanner.Scan() {
|
||||
// 处理标准输出
|
||||
Log("Output:", outputScanner.Text())
|
||||
// 在这里可以将输出逐行发送给客户端,例如通过HTTP响应写入等。
|
||||
|
||||
runtime.EventsEmit(ctx, pyId, "msg", string(outputScanner.Bytes()))
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
Log("Error waiting for cmd:", err)
|
||||
runtime.EventsEmit(ctx, pyId, "error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// CheckPort 检查端口是否被占用
|
||||
func CheckPort(port string) bool {
|
||||
Log("CheckPort port: ", port)
|
||||
cmd := exec.Command("powershell", "-Command", "(Get-Process -Id (Get-NetTCPConnection -LocalPort "+port+" | Select-Object -ExpandProperty OwningProcess)).Name")
|
||||
// 设置cmd.SysProcAttr.HideWindow为true以隐藏cmd窗口
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
stdout, err := cmd.Output()
|
||||
if err != nil {
|
||||
Log("Error executing command:", err)
|
||||
return false
|
||||
}
|
||||
|
||||
output := string(stdout)
|
||||
if strings.TrimSpace(output) != "" {
|
||||
// 确保输出不是空字符串或仅包含空白字符
|
||||
Logf("Port %s is in use by process: %s\n", port, strings.TrimSpace(output))
|
||||
return true
|
||||
} else {
|
||||
Logf("Port %s is not in use.\n", port)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// 打开文件
|
||||
// ReadAll file content
|
||||
func ReadAll(filePath string) []byte {
|
||||
if IsBlank(filePath) {
|
||||
fmt.Println("File path is nil")
|
||||
@@ -27,3 +28,61 @@ func ReadAll(filePath string) []byte {
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// SaveFile
|
||||
func SaveFile(filePath string, data string) {
|
||||
if IsBlank(filePath) {
|
||||
fmt.Println("File path is nil")
|
||||
return
|
||||
}
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("Error create file:", err)
|
||||
return
|
||||
}
|
||||
// 确保文件最后被关闭
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.WriteString(data)
|
||||
if err != nil {
|
||||
fmt.Println("Write file error:", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func PathExists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
// 文件或目录已经存在
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
func DirSize(path string) (int64, error) {
|
||||
var size int64
|
||||
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
size += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return size, err
|
||||
}
|
||||
|
||||
func AppPath() string {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
appDir := filepath.Dir(exePath)
|
||||
fmt.Println("Application Directory:", appDir)
|
||||
return appDir
|
||||
}
|
||||
|
||||
@@ -3,9 +3,13 @@ package utils
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// AnyToStr 任意类型数据转string
|
||||
@@ -62,3 +66,17 @@ func IsBlank(s string) bool {
|
||||
func IsNotBlank(s string) bool {
|
||||
return len(s) > 0 && strings.TrimSpace(s) != ""
|
||||
}
|
||||
|
||||
func GbkToUtf8(s string) string {
|
||||
gbkBytes := []byte(s)
|
||||
// 使用transform.Reader将GBK转换为UTF-8
|
||||
reader := transform.NewReader(strings.NewReader(string(gbkBytes)), simplifiedchinese.GBK.NewDecoder())
|
||||
utf8Bytes, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
fmt.Println("Error converting from GBK: ", err)
|
||||
return s
|
||||
}
|
||||
|
||||
fmt.Println("Converted to UTF-8: ", string(utf8Bytes))
|
||||
return string(utf8Bytes)
|
||||
}
|
||||
|
||||
@@ -31,3 +31,4 @@ wails build
|
||||
npm install axios
|
||||
npm install qs
|
||||
npm i --save-dev @types/qs
|
||||
npm install marked
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
@echo off
|
||||
echo Running pre-commit checks...
|
||||
echo If you have not installed pre-commit, please run:
|
||||
echo pip install pre-commit
|
||||
echo and then run this script again.
|
||||
|
||||
pre-commit run --all-files
|
||||
|
||||
pause
|
||||
Reference in New Issue
Block a user