Merge pull request #917 from aylvn/desktop

Desktop: Improve task page
This commit is contained in:
mannaandpoem
2025-03-31 22:40:57 +08:00
committed by GitHub
43 changed files with 2247 additions and 1257 deletions
+3
View File
@@ -181,3 +181,6 @@ workspace/
# Private Config
config/config.toml
# Desktop runtime
desktop/frontend/wailsjs/runtime/
+65
View File
@@ -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)
}
+8 -5
View File
@@ -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>
+12
View File
@@ -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",
+1
View File
@@ -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",
+115 -12
View File
@@ -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

+130 -2
View File
@@ -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,
}
+13 -2
View File
@@ -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,
}
+1 -1
View File
@@ -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
+159
View File
@@ -0,0 +1,159 @@
English | [中文](README_zh.md)
[![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus/stargazers)
&ensp;
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) &ensp;
[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z)
# 👋 OpenManus
Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫!
Our team
members [@mannaandpoem](https://github.com/mannaandpoem) [@XiangJinyu](https://github.com/XiangJinyu) [@MoshiQAQ](https://github.com/MoshiQAQ) [@didiforgithub](https://github.com/didiforgithub) [@stellaHSR](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building!
It's a simple implementation, so we welcome any suggestions, contributions, and feedback!
Enjoy your own agent with OpenManus!
We're also excited to introduce [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL), an open-source project dedicated to reinforcement learning (RL)- based (such as GRPO) tuning methods for LLM agents, developed collaboratively by researchers from UIUC and OpenManus.
## Project Demo
<video src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" data-canonical-src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
## Installation
We provide two installation methods. Method 2 (using uv) is recommended for faster installation and better dependency management.
### Method 1: Using conda
1. Create a new conda environment:
```bash
conda create -n open_manus python=3.12
conda activate open_manus
```
2. Clone the repository:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
cd OpenManus
```
3. Install dependencies:
```bash
pip install -r requirements.txt
```
### Method 2: Using uv (Recommended)
1. Install uv (A fast Python package installer and resolver):
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
2. Clone the repository:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
cd OpenManus
```
3. Create a new virtual environment and activate it:
```bash
uv venv
source .venv/bin/activate # On Unix/macOS
# Or on Windows:
# .venv\Scripts\activate
```
4. Install dependencies:
```bash
uv pip install -r requirements.txt
```
## Configuration
OpenManus requires configuration for the LLM APIs it uses. Follow these steps to set up your configuration:
1. Create a `config.toml` file in the `config` directory (you can copy from the example):
```bash
cp config/config.example.toml config/config.toml
```
2. Edit `config/config.toml` to add your API keys and customize settings:
```toml
# Global LLM configuration
[llm]
model = "gpt-4o"
base_url = "https://api.openai.com/v1"
api_key = "sk-..." # Replace with your actual API key
max_tokens = 4096
temperature = 0.0
# Optional configuration for specific LLM models
[llm.vision]
model = "gpt-4o"
base_url = "https://api.openai.com/v1"
api_key = "sk-..." # Replace with your actual API key
```
## Quick Start
One line for run OpenManus:
```bash
python main.py
```
Then input your idea via terminal!
For unstable version, you also can run:
```bash
python run_flow.py
```
## How to contribute
We welcome any friendly suggestions and helpful contributions! Just create issues or submit pull requests.
Or contact @mannaandpoem via 📧email: mannaandpoem@gmail.com
## Community Group
Join our networking group on Feishu and share your experience with other developers!
<div align="center" style="display: flex; gap: 20px;">
<img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
</div>
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=mannaandpoem/OpenManus&type=Date)](https://star-history.com/#mannaandpoem/OpenManus&Date)
## Acknowledgement
Thanks to [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
and [browser-use](https://github.com/browser-use/browser-use) for providing basic support for this project!
OpenManus is built by contributors from MetaGPT. Huge thanks to this agent community!
## Cite
```bibtex
@misc{openmanus2025,
author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong},
title = {OpenManus: An open-source framework for building general AI agents},
year = {2025},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/mannaandpoem/OpenManus}},
}
```
+148
View File
@@ -0,0 +1,148 @@
[English](README.md) | 中文
[![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus/stargazers)
&ensp;
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) &ensp;
[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z)
# 👋 OpenManus
Manus 非常棒,但 OpenManus 无需邀请码即可实现任何创意 🛫!
我们的团队成员 [@mannaandpoem](https://github.com/mannaandpoem) [@XiangJinyu](https://github.com/XiangJinyu) [@MoshiQAQ](https://github.com/MoshiQAQ) [@didiforgithub](https://github.com/didiforgithub) https://github.com/stellaHSR 来自 [@MetaGPT](https://github.com/geekan/MetaGPT) 组织,我们在 3
小时内完成了原型开发并持续迭代中!
这是一个简洁的实现方案,欢迎任何建议、贡献和反馈!
用 OpenManus 开启你的智能体之旅吧!
我们也非常高兴地向大家介绍 [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL),这是一个专注于基于强化学习(RL,例如 GRPO)的方法来优化大语言模型(LLM)智能体的开源项目,由来自UIUC 和 OpenManus 的研究人员合作开发。
## 项目演示
<video src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" data-canonical-src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
## 安装指南
我们提供两种安装方式。推荐使用方式二(uv),因为它能提供更快的安装速度和更好的依赖管理。
### 方式一:使用 conda
1. 创建新的 conda 环境:
```bash
conda create -n open_manus python=3.12
conda activate open_manus
```
2. 克隆仓库:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
cd OpenManus
```
3. 安装依赖:
```bash
pip install -r requirements.txt
```
### 方式二:使用 uv(推荐)
1. 安装 uv(一个快速的 Python 包管理器):
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
2. 克隆仓库:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
cd OpenManus
```
3. 创建并激活虚拟环境:
```bash
uv venv
source .venv/bin/activate # Unix/macOS 系统
# Windows 系统使用:
# .venv\Scripts\activate
```
4. 安装依赖:
```bash
uv pip install -r requirements.txt
```
## 配置说明
OpenManus 需要配置使用的 LLM API,请按以下步骤设置:
1. 在 `config` 目录创建 `config.toml` 文件(可从示例复制):
```bash
cp config/config.example.toml config/config.toml
```
2. 编辑 `config/config.toml` 添加 API 密钥和自定义设置:
```toml
# 全局 LLM 配置
[llm]
model = "gpt-4o"
base_url = "https://api.openai.com/v1"
api_key = "sk-..." # 替换为真实 API 密钥
max_tokens = 4096
temperature = 0.0
# 可选特定 LLM 模型配置
[llm.vision]
model = "gpt-4o"
base_url = "https://api.openai.com/v1"
api_key = "sk-..." # 替换为真实 API 密钥
```
## 快速启动
一行命令运行 OpenManus
```bash
python main.py
```
然后通过终端输入你的创意!
如需体验开发中版本,可运行:
```bash
python run_flow.py
```
## 贡献指南
我们欢迎任何友好的建议和有价值的贡献!可以直接创建 issue 或提交 pull request。
或通过 📧 邮件联系 @mannaandpoemmannaandpoem@gmail.com
## 交流群
加入我们的飞书交流群,与其他开发者分享经验!
<div align="center" style="display: flex; gap: 20px;">
<img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
</div>
## Star 数量
[![Star History Chart](https://api.star-history.com/svg?repos=mannaandpoem/OpenManus&type=Date)](https://star-history.com/#mannaandpoem/OpenManus&Date)
## 致谢
特别感谢 [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
和 [browser-use](https://github.com/browser-use/browser-use) 为本项目提供的基础支持!
OpenManus 由 MetaGPT 社区的贡献者共同构建,感谢这个充满活力的智能体开发者社区!
+24 -26
View File
@@ -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>
+27 -24
View File
@@ -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>
+59 -5
View File
@@ -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;
+31 -1
View File
@@ -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...",
}
+31 -1
View File
@@ -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正在启动中, 请稍候...",
}
+45 -8
View File
@@ -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
}
},
]
+27 -18
View File
@@ -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",
+95
View File
@@ -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>
+44 -43
View File
@@ -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>
+525
View File
@@ -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>
+46 -42
View File
@@ -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>
-407
View File
@@ -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>
+341 -113
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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>;
+20
View File
@@ -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
View File
@@ -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
-238
View File
@@ -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
View File
@@ -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
+150
View File
@@ -0,0 +1,150 @@
package utils
import (
"bufio"
"context"
"os"
"os/exec"
"strings"
"syscall"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// ExecBat 执行bat文件
func ExecBatFile(ctx context.Context, batId string, batPath string) {
Log("ExecBatFile batPath: ", batPath)
if IsBlank(batId) || IsBlank(batPath) {
Log("batId or batPath is nil")
runtime.EventsEmit(ctx, batId, "error", "batId or batPath is nil")
return
}
cmd := exec.Command("cmd", "/C", "chcp 65001 > nul &&", batPath)
// 设置cmd.SysProcAttr.HideWindow为true以隐藏cmd窗口
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "PYTHONIOENCODING=utf-8")
// 运行命令并获取输出
stdout, err := cmd.StdoutPipe()
if err != nil {
Log("Error creating StdoutPipe for Cmd:", err)
runtime.EventsEmit(ctx, batId, "error", err.Error())
return
}
stderr, err := cmd.StderrPipe()
if err != nil {
Log("Error creating StderrPipe for Cmd:", err)
runtime.EventsEmit(ctx, batId, "error", err.Error())
return
}
if err := cmd.Start(); err != nil {
Log("Error starting cmd:", err)
runtime.EventsEmit(ctx, batId, "error", err.Error())
return
}
// 读取标准输出和错误输出
outputScanner := bufio.NewScanner(stdout)
errorScanner := bufio.NewScanner(stderr)
go func() {
for errorScanner.Scan() {
// 处理错误输出
Log("Error:", errorScanner.Text())
runtime.EventsEmit(ctx, batId, "msg", string(errorScanner.Bytes()))
}
}()
for outputScanner.Scan() {
// 处理标准输出
Log("Output:", outputScanner.Text())
// 在这里可以将输出逐行发送给客户端,例如通过HTTP响应写入等。
runtime.EventsEmit(ctx, batId, "msg", string(outputScanner.Bytes()))
}
if err := cmd.Wait(); err != nil {
Log("Error waiting for cmd:", err)
runtime.EventsEmit(ctx, batId, "error", err.Error())
}
}
func ExecPyFile(ctx context.Context, pyId string, scriptPath string) {
// 指定Python解释器的路径,例如在Windows上可能是"python"或"python.exe",在Linux或Mac上是"python3"
pythonCmd := "python"
Log("ExecPyFile scriptPath: ", scriptPath)
// 创建一个*exec.Cmd实例来运行Python脚本
cmd := exec.Command(pythonCmd, scriptPath)
// 设置cmd.SysProcAttr.HideWindow为true以隐藏cmd窗口
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
// 运行命令并获取输出
stdout, err := cmd.StdoutPipe()
if err != nil {
Log("Error creating StdoutPipe for Cmd:", err)
runtime.EventsEmit(ctx, pyId, "error", err.Error())
return
}
stderr, err := cmd.StderrPipe()
if err != nil {
Log("Error creating StderrPipe for Cmd:", err)
runtime.EventsEmit(ctx, pyId, "error", err.Error())
return
}
if err := cmd.Start(); err != nil {
Log("Error starting cmd:", err)
runtime.EventsEmit(ctx, pyId, "error", err.Error())
return
}
// 读取标准输出和错误输出
outputScanner := bufio.NewScanner(stdout)
errorScanner := bufio.NewScanner(stderr)
go func() {
for errorScanner.Scan() {
// 处理错误输出
Log("Error:", errorScanner.Text())
runtime.EventsEmit(ctx, pyId, "msg", string(errorScanner.Bytes()))
}
}()
for outputScanner.Scan() {
// 处理标准输出
Log("Output:", outputScanner.Text())
// 在这里可以将输出逐行发送给客户端,例如通过HTTP响应写入等。
runtime.EventsEmit(ctx, pyId, "msg", string(outputScanner.Bytes()))
}
if err := cmd.Wait(); err != nil {
Log("Error waiting for cmd:", err)
runtime.EventsEmit(ctx, pyId, "error", err.Error())
}
}
// CheckPort 检查端口是否被占用
func CheckPort(port string) bool {
Log("CheckPort port: ", port)
cmd := exec.Command("powershell", "-Command", "(Get-Process -Id (Get-NetTCPConnection -LocalPort "+port+" | Select-Object -ExpandProperty OwningProcess)).Name")
// 设置cmd.SysProcAttr.HideWindow为true以隐藏cmd窗口
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
stdout, err := cmd.Output()
if err != nil {
Log("Error executing command:", err)
return false
}
output := string(stdout)
if strings.TrimSpace(output) != "" {
// 确保输出不是空字符串或仅包含空白字符
Logf("Port %s is in use by process: %s\n", port, strings.TrimSpace(output))
return true
} else {
Logf("Port %s is not in use.\n", port)
return false
}
}
+60 -1
View File
@@ -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
}
+18
View File
@@ -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)
}
+1
View File
@@ -31,3 +31,4 @@ wails build
npm install axios
npm install qs
npm i --save-dev @types/qs
npm install marked
+9
View File
@@ -0,0 +1,9 @@
@echo off
echo Running pre-commit checks...
echo If you have not installed pre-commit, please run:
echo pip install pre-commit
echo and then run this script again.
pre-commit run --all-files
pause