change store

This commit is contained in:
godo 2024-07-30 16:52:53 +08:00
parent d8839f9138
commit 6f7313b770
20 changed files with 942 additions and 387 deletions

View file

@ -11,7 +11,7 @@ An efficient intranet office platform that includes various tools such as Word,
English | [简体中文](README.md) English | [简体中文](README.md)
[FAQ](./docs/Faq.md) | [API](./docs/Api.md) [FAQ](./docs/Faq.md) | [API](./docs/Api.md) | [AppStore](./docs/Store.md)
### Install ### Install

View file

@ -3,7 +3,7 @@
</p> </p>
<h1 align="center">GodoOS</h1> <h1 align="center">GodoOS</h1>
一款高效的内网办公平台内含word/excel/ppt/pdf/内网聊天/白板/思维导图等多个办公系统工具支持原生文件存储。平台界面精仿windows风格操作简便同时保持低资源消耗和高性能运行。无需注册即可自动连接内网用户实现即时通讯和文件共享。 一款高效的内网办公平台内含word/excel/ppt/pdf/内网聊天/白板/思维导图等多个办公系统工具支持原生文件存储。平台界面精仿windows风格操作简便同时保持低资源消耗和高性能运行。无需注册即可自动连接内网用户实现即时通讯和文件共享。灵活高配置的应用商店,可无限扩展。
<div align="center"> <div align="center">
@ -11,7 +11,7 @@
[English](README.en.md) | 简体中文 [English](README.en.md) | 简体中文
[FAQ](./docs/Faq.md) | [API](./docs/Api.md) [FAQ](./docs/Faq.md) | [API](./docs/Api.md) | [应用开发](./docs/Store.md)
@ -48,6 +48,8 @@
- ***精小打包后仅65M确包含了所有的办公套件*** - ***精小打包后仅65M确包含了所有的办公套件***
- ***可无限扩展,支持自定义应用*** - ***可无限扩展,支持自定义应用***
- ***golang开发后端低资源消耗和高性能*** - ***golang开发后端低资源消耗和高性能***
- ***支持多平台Windows、Linux、MacOS***
- ***只会html和拥有可执行的应用简单学习一下应用商店配置即可开发出复杂的应用***
## 🚧 开发进程 ## 🚧 开发进程
- 2024年7月发布v1.0.0版本,发布后,项目进入第二阶段。 - 2024年7月发布v1.0.0版本,发布后,项目进入第二阶段。

View file

@ -16,6 +16,11 @@
- **Content-Type**: `application/json` - **Content-Type**: `application/json`
- **响应体**: 目录内容列表 - **响应体**: 目录内容列表
#### 示例
```bash
PATH_PARAM="/path/to/directory"
curl -X GET "http://localhost:56780/files/read?path=$PATH_PARAM"
```
--- ---
### 获取文件或目录状态 ### 获取文件或目录状态
@ -33,6 +38,12 @@
- **Content-Type**: `application/json` - **Content-Type**: `application/json`
- **响应体**: 文件或目录的状态信息 - **响应体**: 文件或目录的状态信息
#### 示例
```bash
# 获取文件或目录状态
PATH_PARAM="/path/to/file_or_directory"
curl -X GET "http://localhost:56780/files/stat?path=$PATH_PARAM"
```
--- ---
### 更改文件权限 ### 更改文件权限
@ -51,6 +62,12 @@
- **Content-Type**: `application/json` - **Content-Type**: `application/json`
- **响应体**: 操作结果 - **响应体**: 操作结果
#### 示例
```bash
# 更改文件权限
JSON='{"path":"/path/to/file","mode":"0644"}'
curl -X POST -H "Content-Type: application/json" -d "$JSON" "http://localhost:56780/files/chmod"
```
--- ---
### 检查文件或目录是否存在 ### 检查文件或目录是否存在
@ -67,7 +84,12 @@
#### 响应 #### 响应
- **Content-Type**: `application/json` - **Content-Type**: `application/json`
- **响应体**: 存在性检查结果 - **响应体**: 存在性检查结果
#### 示例
```bash
# 检查文件或目录是否存在
PATH_PARAM="/path/to/file_or_directory"
curl -X GET "http://localhost:56780/files/exists?path=$PATH_PARAM"
```
--- ---
### 读取文件内容 ### 读取文件内容
@ -84,7 +106,12 @@
#### 响应 #### 响应
- **Content-Type**: `application/json` - **Content-Type**: `application/json`
- **响应体**: 文件内容 - **响应体**: 文件内容
#### 示例
```bash
# 读取文件内容
PATH_PARAM="/path/to/file"
curl -X GET "http://localhost:56780/files/readfile?path=$PATH_PARAM"
```
--- ---
### 删除文件 ### 删除文件
@ -101,7 +128,12 @@
#### 响应 #### 响应
- **Content-Type**: `application/json` - **Content-Type**: `application/json`
- **响应体**: 操作结果 - **响应体**: 操作结果
#### 示例
```bash
# 删除文件
PATH_PARAM="/path/to/file"
curl -X GET "http://localhost:56780/files/unlink?path=$PATH_PARAM"
```
--- ---
### 清空文件系统 ### 清空文件系统
@ -115,7 +147,11 @@
#### 响应 #### 响应
- **Content-Type**: `application/json` - **Content-Type**: `application/json`
- **响应体**: 操作结果 - **响应体**: 操作结果
#### 示例
```bash
# 清空文件系统
curl -X GET "http://localhost:56780/files/clear"
```
--- ---
### 重命名文件或目录 ### 重命名文件或目录
@ -132,7 +168,13 @@
#### 响应 #### 响应
- **Content-Type**: `application/json` - **Content-Type**: `application/json`
- **响应体**: 操作结果 - **响应体**: 操作结果
#### 示例
```bash
# 重命名文件或目录
OLD_PATH_PARAM="/path/to/old_file_or_directory"
NEW_PATH_PARAM="/path/to/new_file_or_directory"
curl -X GET "http://localhost:56780/files/rename?oldPath=$OLD_PATH_PARAM&newPath=$NEW_PATH_PARAM"
```
--- ---
### 创建目录 ### 创建目录
@ -149,7 +191,12 @@
#### 响应 #### 响应
- **Content-Type**: `application/json` - **Content-Type**: `application/json`
- **响应体**: 操作结果 - **响应体**: 操作结果
#### 示例
```bash
# 创建目录
DIR_PATH_PARAM="/path/to/new_directory"
curl -X POST "http://localhost:56780/files/mkdir?dirPath=$DIR_PATH_PARAM"
```
--- ---
### 删除目录 ### 删除目录
@ -166,7 +213,12 @@
#### 响应 #### 响应
- **Content-Type**: `application/json` - **Content-Type**: `application/json`
- **响应体**: 操作结果 - **响应体**: 操作结果
#### 示例
```bash
# 删除目录
DIR_PATH_PARAM="/path/to/directory"
curl -X GET "http://localhost:56780/files/rmdir?dirPath=$DIR_PATH_PARAM"
```
--- ---
### 复制文件 ### 复制文件
@ -183,7 +235,13 @@
#### 响应 #### 响应
- **Content-Type**: `application/json` - **Content-Type**: `application/json`
- **响应体**: 操作结果 - **响应体**: 操作结果
#### 示例
```bash
# 复制文件
SRC_PATH_PARAM="/path/to/source_file"
DST_PATH_PARAM="/path/to/destination_file"
curl -X GET "http://localhost:56780/files/copyfile?srcPath=$SRC_PATH_PARAM&dstPath=$DST_PATH_PARAM"
```
--- ---
### 写入文件 ### 写入文件
@ -204,7 +262,13 @@
#### 响应 #### 响应
- **Content-Type**: `application/json` - **Content-Type**: `application/json`
- **响应体**: 操作结果 - **响应体**: 操作结果
#### 示例
```bash
# 写入文件
FILE_PATH_PARAM="/path/to/file"
CONTENT="This is the content to be written."
curl -X POST -F "content=@-$CONTENT" "http://localhost:56780/files/writefile?filePath=$FILE_PATH_PARAM"
```
--- ---
### 追加文件内容 ### 追加文件内容
@ -225,7 +289,13 @@
#### 响应 #### 响应
- **Content-Type**: `application/json` - **Content-Type**: `application/json`
- **响应体**: 操作结果 - **响应体**: 操作结果
#### 示例
```bash
# 追加文件内容
FILE_PATH_PARAM="/path/to/file"
CONTENT="This is the content to be appended."
curl -X POST -F "content=@-$CONTENT" "http://localhost:56780/files/appendfile?filePath=$FILE_PATH_PARAM"
```
--- ---
### 文件系统事件监听 ### 文件系统事件监听
@ -237,3 +307,4 @@
- **path** (监听的文件或目录路径) - **path** (监听的文件或目录路径)
- **callback** (事件回调函数) - **callback** (事件回调函数)
- **errback** (错误回调函数) - **errback** (错误回调函数)

View file

@ -1,7 +1,3 @@
---
title: Markdown编辑器
icon: circle-info
---
## 概览 ## 概览
Markdown 编辑器允许您使用丰富的文本编辑体验创建和编辑 Markdown 文件。它包括实时保存、文件导入/导出以及访问最近的 Markdown 文件等功能。 Markdown 编辑器允许您使用丰富的文本编辑体验创建和编辑 Markdown 文件。它包括实时保存、文件导入/导出以及访问最近的 Markdown 文件等功能。

159
docs/Store.md Normal file
View file

@ -0,0 +1,159 @@
## 应用管理
### 如何添加一个应用
1. 在用户目录.godoos/run下建立一个应用文件夹。
2. 一个应用需要包含两个配置文件,在应用根目录下创建`install.json`和`store.json`两个文件。 配置文件格式如下:
- install.json
```json
{
"name": "", // string, 应用程序名称。
"url": "", // string, 应用程序下载地址。
"webUrl":"", // string, 如何设置,应用程序将显示到桌面。
"isDev": true, // boolean, 是否为开发环境。
"needDownload": true, // boolean, 是否需要下载。
"needInstall": true, // boolean, 是否需要安装。
"version": "1.0.0", // string, 应用程序版本。
"icon": "", // string, 应用程序图标,为可访问的网络地址。
"checkProgress": true, // boolean, 表示是否显示启动和停止。
"hasRestart": true, // boolean, 是否需要重启。
"setting": true // boolean, 是否需要设置。
}
```
如果不需要后端进程的web应用store.json可以不用配置。
install.json的struct为
```json
type InstallInfo struct {
Name string `json:"name"` // 应用程序名称。重要,必须和应用的目录名称一致。
URL string `json:"url"` // 应用程序下载地址。
WebUrl string `json:"webUrl"` // 应用程序的网页地址。
IsDev bool `json:"isDev"` // 标志位,表示是否为开发者版本。
NeedDownload bool `json:"needDownload"` // 标志位,表示是否需要下载。
NeedInstall bool `json:"needInstall"` // 标志位,表示是否需要安装。
Version string `json:"version"` // 应用程序的版本号。
Desc string `json:"desc"` // 应用程序的描述信息。
Icon string `json:"icon"` // 应用程序的图标路径。
CheckProgress bool `json:"checkProgress"` // 标志位,表示是否显示启动和停止。
HasRestart bool `json:"hasRestart"` // 标志位,表示安装后是否需要重启。
Setting bool `json:"setting"` // 标志位,表示是否需要配置。
}
```
- store.json
```json
{
"name": "mysql5.7", // string, 应用程序名称。可不设置会继承自install.json
"icon": "mysql.png", // string, 应用程序图标一般放在应用程序static目录下。
"setting": {
"binPath": "{exePath}/bin/mysqld.exe", // string, 重要,必须设置。为启动程序路径。
"confPath": "{exePath}/my.ini",// string, 重要,必须设置。为配置文件路径。
"progressName": "mysqld.exe",// string, 进程名称。如果为单线程可不设置。
"isOn": true // boolean, 是否启动守护进程
},
"config": { // object, 配置文件。里面的配置可以任意填写和cmds配合使用。
},
"cmds": {},// object, 命令集。
"install": { // object, 安装配置。
"envs": [],// object[], 环境变量。
"cmds": []// object[], 启动命令。
},
"start": {// object, 启动配置。
"envs": [],// object[], 环境变量。
"cmds": []// object[], 启动命令。
}
}
```
store.json的struct为
```json
type StoreInfo struct {
Name string `json:"name"` // 应用程序的名称。
Icon string `json:"icon"` // 应用程序的图标路径。
Setting Setting `json:"setting"` // 应用程序的配置信息。
Config map[string]any `json:"config"` // 应用程序的配置信息映射。
Cmds map[string][]Cmd `json:"cmds"` // 应用程序的命令集合。
Install Install `json:"install"` // 安装应用程序的信息。
Start Install `json:"start"` // 启动应用程序的信息。
}
```
Setting的结构体为
```json
// 包含应用程序的二进制文件路径、配置文件路径等关键设置信息。
type Setting struct {
BinPath string `json:"binPath"` // 应用程序二进制文件的路径。
ConfPath string `json:"confPath"` // 应用程序配置文件的路径。
ProgressName string `json:"progressName"` // 进程的名称。
IsOn bool `json:"isOn"` //是否守护进程运行。
}
```
Cmd的结构体为
```json
type Cmd struct {
Name string `json:"name"` // 命令的名称。
FilePath string `json:"filePath,omitempty"` // 命令文件的路径。
Content string `json:"content,omitempty"` // 命令的内容。
BinPath string `json:"binPath,omitempty"` // 执行命令的二进制文件路径。
TplPath string `json:"tplPath,omitempty"` // 命令的模板路径。
Cmds []string `json:"cmds,omitempty"` // 要执行的子命令列表。
Waiting int `json:"waiting"` // 等待的时间。
Kill bool `json:"kill"` // 标志位表示是否需要终止之前的命令。如果setting中设置了progressName会优先杀死整个进程
Envs []Item `json:"envs"` // 命令执行时的环境变量。
}
```
Install的struct为
```json
// Install 描述了安装过程中的环境变量和命令列表。
type Install struct {
Envs []Item `json:"envs"` // 安装过程中需要的环境变量。
Cmds []string `json:"cmds"` // 安装过程中需要执行的命令列表。
}
// Item 是一个通用的键值对结构体,用于表示配置项或环境变量等。
type Item struct {
Name string `json:"name"` // 配置项的名称。
Value any `json:"value"` // 配置项的值。
}
```
3. 在应用商店添加应用,选择本地添加,输入应用的目录名字(不需要填写整个目录)。
### 配置文件`store.json`说明
1. `install`和`start`配置可以调用`cmds`里的命令。
2. `cmds`里的命令也可以调用自身的命令。
3. 所有的命令都可以串联使用。
### 如何设置配置
1. 在应用目录下建立`static`目录,并创建`index.html`文件。`install.json`中`setting`设置为`true`。
前端配置样列
```js
const postData = {
dataDir: dataDir,// 此处对应store.json中的config配置项
logDir: logDir,// 此处对应store.json中的config配置项
port: port, // 此处对应store.json中的config配置项
name: "mysql5.7",// 应用名称
cmdKey: "setting"// 命令键cmds的name
}
const comp = await fetch('http://localhost:56780/store/setting', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(postData)
});
```
关键要设置好`name`和`cmdKey``cmdKey`对应`store.json`中的`cmds`数组配置项中的`name`一个数组中又可以配置一系列的命令可以参考Cmd的结构体
### 内置应用说明
- 系列封装了一些用于处理进程控制和文件操作的功能函数,以下是各函数的详细描述:
1. `start` 启动应用
2. `stop` 停止应用
3. `restart` 重启应用
4. `exec` 执行命令 必须设置binPath和cmds
5. `writeFile` 写入文件 必须设置filePath和content以config为依据对content进行替换
6. `changeFile` 修改文件 必须设置filePath和tplPath以config为依据在模板文件中进行替换
7. `deleteFile` 删除文件
8. `unzip` 解压文件 必须设置filePath和contentcontent为解压目录
9. `zip` 压缩文件 必须设置filePath和contentfilePath为将要压缩的文件夹content为压缩后的文件名
10. `mkdir` 创建文件夹 必须设置FilePathFilePath为创建的文件夹路径

View file

@ -44,6 +44,7 @@ declare module 'vue' {
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElEmpty: typeof import('element-plus/es')['ElEmpty'] ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm'] ElForm: typeof import('element-plus/es')['ElForm']

View file

@ -4,19 +4,12 @@
"url": "https://gitee.com/ruitao_admin/godoos-mysql5.7/releases/download/mysql5.7/mysql5.7.zip", "url": "https://gitee.com/ruitao_admin/godoos-mysql5.7/releases/download/mysql5.7/mysql5.7.zip",
"needDownload": true, "needDownload": true,
"needInstall": true, "needInstall": true,
"isDev":true,
"version": "5.7.37", "version": "5.7.37",
"icon": "https://img0.baidu.com/it/u=1087008535,3349487719&fm=253&fmt=auto&app=138&f=PNG?w=969&h=500", "icon": "https://img0.baidu.com/it/u=1087008535,3349487719&fm=253&fmt=auto&app=138&f=PNG?w=969&h=500",
"checkProgress": true, "checkProgress": true,
"hasRestart": true, "hasRestart": true,
"setting": [ "setting": true
{
"path": "setting.html",
"lang":{
"cn-zh": "设置",
"en": "Setting"
}
}
]
}, },
{ {
"name": "nginx", "name": "nginx",

View file

@ -221,6 +221,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
padding: 10px; padding: 10px;

View file

@ -1,11 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { OpenDirDialog } from "@/util/goutil"; import { OpenDirDialog } from "@/util/goutil";
import { getSystemKey, parseJson } from "@/system/config";
const apiUrl = getSystemKey("apiUrl");
const installed = getSystemKey("intstalledPlugins");
import { notifySuccess, notifyError } from "@/util/msg";
import { t } from "@/i18n";
const formData = ref({ const formData = ref({
importType: 'download', importType: 'download',
url: '', url: '',
devPath:'' devPath: ''
}) })
const progress = ref(0)
const addType = [ const addType = [
{ {
'name': '远程下载', 'name': '远程下载',
@ -21,12 +27,46 @@ const addType = [
} }
] ]
function selectFile() { function selectFile() {
OpenDirDialog().then((res: string) => { OpenDirDialog().then((res: string) => {
formData.value.devPath = res; formData.value.devPath = res;
}); });
} }
async function addAppByDownload() { async function addAppByDownload() {
//console.log(item)
if (formData.value.url == "") {
notifyError(t("store.urlEmpty"))
return;
}
const completion = await fetch(apiUrl + '/store/download?url=' + formData.value.url)
if (!completion.ok) {
notifyError(t("store.downloadError"))
}
//console.log(completion)
const reader: any = completion.body?.getReader();
if (!reader) {
notifyError(t("store.cantStream"));
}
while (true) {
const { done, value } = await reader?.read();
if (done) {
break;
}
// console.log(value)
const json = await new TextDecoder().decode(value);
//console.log(json)
const res = parseJson(json)
//console.log(res)
if (res) {
if (res.progress) {
progress.value = res.progress
}
if (res.done) {
notifySuccess(t("store.downloadSuccess"))
progress.value = 0
break;
}
}
}
} }
async function addAppByImport() { async function addAppByImport() {
@ -46,6 +86,9 @@ async function addAppByDev() {
<el-form-item label="下载地址"> <el-form-item label="下载地址">
<el-input v-model="formData.url" style="width: 280px;" /> <el-input v-model="formData.url" style="width: 280px;" />
</el-form-item> </el-form-item>
<el-form-item label="下载进度" v-if="progress > 0">
<el-progress :text-inside="true" :stroke-width="20" :percentage="progress" />
</el-form-item>
<el-button type="primary" @click="addAppByDownload()">添加</el-button> <el-button type="primary" @click="addAppByDownload()">添加</el-button>
</template> </template>
<template v-if="formData.importType == 'local'"> <template v-if="formData.importType == 'local'">
@ -53,7 +96,8 @@ async function addAppByDev() {
</template> </template>
<template v-if="formData.importType == 'dev'"> <template v-if="formData.importType == 'dev'">
<el-form-item label="本地路径"> <el-form-item label="本地路径">
<el-input v-model="formData.devPath" style="width: 280px;" @click="selectFile()" /> <el-input v-model="formData.devPath" style="width: 280px;" placeholder="用户目录下.godoos/run只填目录名称"
@click="selectFile()" />
</el-form-item> </el-form-item>
<el-button type="primary" @click="addAppByDev()">添加</el-button> <el-button type="primary" @click="addAppByDev()">添加</el-button>
</template> </template>
@ -61,6 +105,6 @@ async function addAppByDev() {
</template> </template>
<style scoped> <style scoped>
.el-button { .el-button {
margin:20px 100px margin: 20px 100px
} }
</style> </style>

View file

@ -4,8 +4,9 @@
<div class="app-content"> <div class="app-content">
<div class="app-title">{{ item?.name }}</div> <div class="app-title">{{ item?.name }}</div>
<div class="app-desc"> <div class="app-desc">
<span>{{ item?.desc ?? item?.name }}</span>
<el-progress :text-inside="true" style="margin-top: 3px;" v-if="item?.progress" :stroke-width="20" <el-progress :text-inside="true" style="margin-top: 3px;" v-if="item?.progress" :stroke-width="20"
:percentage="item?.progress" /> :percentage="item?.progress" />
</div> </div>
<div class="app-button"> <div class="app-button">
@ -17,17 +18,20 @@
<div v-if="item!.hasRestart && item?.isRuning"> <div v-if="item!.hasRestart && item?.isRuning">
<button @click="restart?.(item)">重启</button> <button @click="restart?.(item)">重启</button>
</div> </div>
<button @click="setting?.(item)" v-if="!item?.isRuning && item?.setting">配置</button>
<button @click="uninstall?.(item)" v-if="!item?.isRuning">卸载</button> <button @click="uninstall?.(item)" v-if="!item?.isRuning">卸载</button>
</template> </template>
<template v-else> <template v-else>
<button v-if="item?.url" @click="install?.(item)" :disabled="item?.progress">安装</button> <button @click="install?.(item)" :disabled="item?.progress">安装</button>
</template> </template>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { BrowserWindow } from "@/system";
defineProps({ defineProps({
item: Object, item: Object,
installedList: Array, installedList: Array,
@ -37,11 +41,26 @@ defineProps({
start: Function, start: Function,
restart: Function restart: Function
}); });
function setting(item: any) {
const win = new BrowserWindow({
title: "配置",
url: `http://localhost:56780/static/${item.name}/index.html`,
icon: "gallery",
width: 500,
height: 500,
center: true,
minimizable: false,
resizable: true,
});
win.show()
}
</script> </script>
<style scoped> <style scoped>
.app-item { .app-item {
width: 350px; width: calc(100% - 20px);
height: 100px; min-height: 100px;
min-width:200px;
margin: 10px; margin: 10px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -49,15 +68,23 @@ defineProps({
align-items: center; align-items: center;
padding: 10px; padding: 10px;
border-radius: 6px; border-radius: 6px;
border: 1px solid #cccccc79; border: 1px solid rgba(204, 204, 204, 0.79);
transition: all 0.2s; transition: all 0.2s;
background-color: white;
}
@media (max-width: 768px) {
.app-item {
flex-direction: column;
width: calc(100% - 20px);
}
} }
.app-img { .app-img {
padding: 10px;
width: 32px; width: 32px;
height: 32px;
border-radius: 4px; border-radius: 4px;
border: 1px solid #cccccc7b; border: 1px solid rgba(204, 204, 204, 0.79);
user-select: none; user-select: none;
} }
@ -67,8 +94,7 @@ defineProps({
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
height: 80px; height: 100%;
width: 100%;
} }
.app-title { .app-title {
@ -76,31 +102,47 @@ defineProps({
font-weight: bold; font-weight: bold;
padding-bottom: 6px; padding-bottom: 6px;
user-select: none; user-select: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.app-desc { .app-desc {
font-size: 12px; font-size: 12px;
color: #666; color: #666;
user-select: none; user-select: none;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
padding: 5px; /* 添加内边距 */
/* 可选:添加边框 */
/* border: 1px solid #ddd; */
/* 可选:添加背景色 */
/* background-color: #f9f9f9; */
}
@media (min-width: 769px) {
.app-desc {
font-size: 14px; /* 在大屏幕上使用更大的字体大小 */
}
} }
.app-item:hover { .app-item:hover {
box-shadow: 0 0 20px 2px #dddddd5b; box-shadow: 0 0 20px 2px rgba(221, 221, 221, 0.5);
transform: translateY(-2px); transform: translateY(-2px);
} }
.app-item:active { .app-item:active {
box-shadow: 0 0 0px #ccc; box-shadow: none;
transform: translateY(0px); transform: translateY(0);
} }
.app-button { .app-button {
width: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
margin-top: 20px;
} }
.app-button button { .app-button button {
@ -111,15 +153,14 @@ defineProps({
background-color: #fff; background-color: #fff;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.app-button button:hover { .app-button button:hover {
box-shadow: 0 0 10px #ccc; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
/* transform: translateY(-2px); */
} }
.app-button button:active { .app-button button:active {
box-shadow: 0 0 0px #ccc; box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15);
transform: translateY(0px);
} }
</style> </style>

View file

@ -24,15 +24,17 @@ onMounted(async () => {
async function getList() { async function getList() {
if (currentCate.value == 'add') return; if (currentCate.value == 'add') return;
storeList.value = storeInitList storeList.value = storeInitList
// const apiUrl = getSystemKey("apiUrl"); // const apiUrl = getSystemKey("apiUrl");
// const storeUrl = apiUrl + '/store/storelist?cate=' + currentCate.value // const storeUrl = apiUrl + '/store/storelist?cate=' + currentCate.value
// fetch(storeUrl).then(res => { // const res = await fetch(storeUrl)
// res.json().then(data => { // if (!res.ok) {
// storeList.value = data // notifyError(t('store.getStoreListError'))
// }) // return
// }).catch(() => { // }
// notifyError(t("store.errorList")) // const data = await res.json()
// }) // //console.log(data)
// storeList.value = data
await checkProgress() await checkProgress()
isready.value = true; isready.value = true;
} }
@ -41,8 +43,8 @@ async function checkProgress() {
if (!completion.ok) { if (!completion.ok) {
return return
} }
let res:any = await completion.json() let res: any = await completion.json()
if(!res || res.length < 1){ if (!res || res.length < 1) {
res = [] res = []
} }
storeList.value.forEach((item: any, index: number) => { storeList.value.forEach((item: any, index: number) => {
@ -50,7 +52,7 @@ async function checkProgress() {
console.log(pitem) console.log(pitem)
if (pitem) { if (pitem) {
storeList.value[index].isRuning = pitem.running storeList.value[index].isRuning = pitem.running
}else{ } else {
storeList.value[index].isRuning = false storeList.value[index].isRuning = false
} }
}) })
@ -69,7 +71,7 @@ function setCache() {
} }
async function install(item: any) { async function install(item: any) {
if (item.needDownload) { if (item.needDownload && !item.isDev) {
await download(item) await download(item)
} }
if (item.needInstall) { if (item.needInstall) {
@ -84,8 +86,11 @@ async function install(item: any) {
notifyError(res.message) notifyError(res.message)
return return
} }
if(res.data){
item.icon = res.data
}
} }
if(item.isOut){ if (item.isOut) {
item.progress = 0 item.progress = 0
const completion = await fetch(apiUrl + '/store/installOut?url=' + item.url) const completion = await fetch(apiUrl + '/store/installOut?url=' + item.url)
if (!completion.ok) { if (!completion.ok) {
@ -98,10 +103,10 @@ async function install(item: any) {
return return
} }
} }
if (item.isWeb) { if (item.webUrl) {
sys.fs.writeFile( sys.fs.writeFile(
`${sys._options.userLocation}Desktop/${item.name}.url`, `${sys._options.userLocation}Desktop/${item.name}.url`,
`link::url::${item.url}::${item.icon}` `link::url::${item.webUrl}::${item.icon}`
); );
} }
@ -165,26 +170,26 @@ async function download(item: any) {
} }
} }
async function pauseApp(item: any) { async function pauseApp(item: any) {
const res:any = await fetch(apiUrl + '/store/stop/' + item.name) const res: any = await fetch(apiUrl + '/store/stop/' + item.name)
if(!res.ok){ if (!res.ok) {
const msg = await res.text() const msg = await res.text()
notifyError(msg) notifyError(msg)
return return
} }
setTimeout(async() => { setTimeout(async () => {
await checkProgress() await checkProgress()
}, 1000) }, 1000)
} }
async function restartApp(item: any) { async function restartApp(item: any) {
await fetch(apiUrl + '/store/restart/' + item.name) await fetch(apiUrl + '/store/restart/' + item.name)
setTimeout(async() => { setTimeout(async () => {
await checkProgress() await checkProgress()
}, 1000) }, 1000)
} }
async function startApp(item: any) { async function startApp(item: any) {
await fetch(apiUrl + '/store/start/' + item.name) await fetch(apiUrl + '/store/start/' + item.name)
setTimeout(async() => { setTimeout(async () => {
await checkProgress() await checkProgress()
}, 1000) }, 1000)
} }
@ -204,25 +209,10 @@ async function startApp(item: any) {
</div> </div>
<div class="store"> <div class="store">
<div v-if="isready" class="store-top"> <div v-if="isready" class="store-top">
<!-- <div class="left-bar"></div> -->
<div class="right-main"> <div class="right-main">
<div class="main-title"> <div class="main-title">
<span class="sub-title">{{ currentTitle }} </span> <span class="sub-title">{{ currentTitle }} </span>
</div> </div>
<!-- <div class="swiper">
<div class="swiper-txt">主页</div>
<div class="swiper-inner">
<div class="swiper-tab">
<img src="/image/store/banner1.jpg" />
</div>
<div class="swiper-tab">
<img src="/image/store/banner2.jpg" />
</div>
<div class="swiper-tab">
<img src="/image/store/banner3.jpg" />
</div>
</div>
</div> -->
<div class="main-app"> <div class="main-app">
<div v-for="item in storeList" v-if="currentCate != 'add'" class="store-item" :key="item.name"> <div v-for="item in storeList" v-if="currentCate != 'add'" class="store-item" :key="item.name">
<AppItem :item="item" :installed-list="installedList" :install="install" :uninstall="uninstall" <AppItem :item="item" :installed-list="installedList" :install="install" :uninstall="uninstall"

View file

@ -41,11 +41,12 @@ func OsStart() {
progressRouter.HandleFunc("/listporgress", store.Status).Methods(http.MethodGet) progressRouter.HandleFunc("/listporgress", store.Status).Methods(http.MethodGet)
progressRouter.HandleFunc("/listport", store.ListPortsHandler).Methods(http.MethodGet) progressRouter.HandleFunc("/listport", store.ListPortsHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/killport", store.KillPortHandler).Methods(http.MethodGet) progressRouter.HandleFunc("/killport", store.KillPortHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/storelist", store.GetStoreInfoHandler).Methods(http.MethodGet) progressRouter.HandleFunc("/storelist", store.GetStoreListHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/download", store.DownloadHandler).Methods(http.MethodGet) progressRouter.HandleFunc("/download", store.DownloadHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/install", store.InstallHandler).Methods(http.MethodGet) progressRouter.HandleFunc("/install", store.InstallHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/installOut", store.RunOutHandler).Methods(http.MethodGet) progressRouter.HandleFunc("/installOut", store.RunOutHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/uninstall", store.UnInstallHandler).Methods(http.MethodGet) progressRouter.HandleFunc("/uninstall", store.UnInstallHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/setting", store.StoreSettingHandler).Methods(http.MethodPost)
router.HandleFunc("/system/updateInfo", sys.GetUpdateUrlHandler).Methods(http.MethodGet) router.HandleFunc("/system/updateInfo", sys.GetUpdateUrlHandler).Methods(http.MethodGet)
router.HandleFunc("/system/update", sys.UpdateAppHandler).Methods(http.MethodGet) router.HandleFunc("/system/update", sys.UpdateAppHandler).Methods(http.MethodGet)

View file

@ -9,12 +9,11 @@ import (
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
) )
func CheckActive(ctx context.Context) { func CheckActive(ctx context.Context) {
ticker := time.NewTicker(1 * time.Minute) ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop() defer ticker.Stop()
activeCheckWg := sync.WaitGroup{} activeCheckWg := sync.WaitGroup{}
@ -38,12 +37,15 @@ func checkInactiveProcesses() {
defer processesMu.RUnlock() defer processesMu.RUnlock()
for name, p := range processes { for name, p := range processes {
pid := p.Cmd.Process.Pid pid := p.Pid
_, err := os.FindProcess(pid) _, err := os.FindProcess(pid)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
p.Running = false p.Running = false
log.Printf("Process %s (PID: %d) not found.", name, pid) log.Printf("Process %s (PID: %d) not found.", name, pid)
if p.IsOn {
go ExecuteScript(name)
}
} else { } else {
log.Printf("Error finding process %s (PID: %d): %v", name, pid, err) log.Printf("Error finding process %s (PID: %d): %v", name, pid, err)
} }
@ -70,11 +72,14 @@ func IsProcessRunning(appName string) (bool, error) {
var output []byte var output []byte
var err error var err error
if cmd.SysProcAttr == nil { // if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{} // cmd.SysProcAttr = &syscall.SysProcAttr{}
} // }
//cmd.SysProcAttr.HideWindow = true // For Windows to hide the console window //cmd.SysProcAttr.HideWindow = true // For Windows to hide the console window
if runtime.GOOS == "windows" {
// 在Windows上通过设置CreationFlags来隐藏窗口
cmd = SetHideConsoleCursor(cmd)
}
if output, err = cmd.CombinedOutput(); err != nil { if output, err = cmd.CombinedOutput(); err != nil {
return false, fmt.Errorf("error checking process: %w", err) return false, fmt.Errorf("error checking process: %w", err)
} }

149
godo/store/cmds.go Normal file
View file

@ -0,0 +1,149 @@
package store
import (
"fmt"
"godo/files"
"godo/libs"
"log"
"os"
"os/exec"
"runtime"
"strings"
"time"
)
func runStop(storeInfo StoreInfo) error {
return StopCmd(storeInfo.Name)
}
func runStart(storeInfo StoreInfo) error {
err := SetEnvs(storeInfo.Start.Envs)
if err != nil {
return fmt.Errorf("failed to set start environment variable %s: %w", storeInfo.Name, err)
}
if !libs.PathExists(storeInfo.Setting.BinPath) {
return fmt.Errorf("script file %s does not exist", storeInfo.Setting.BinPath)
}
var cmd *exec.Cmd
if len(storeInfo.Start.Cmds) > 0 {
cmd = exec.Command(storeInfo.Setting.BinPath, storeInfo.Start.Cmds...)
} else {
cmd = exec.Command(storeInfo.Setting.BinPath)
}
if runtime.GOOS == "windows" {
// 在Windows上通过设置CreationFlags来隐藏窗口
cmd = SetHideConsoleCursor(cmd)
}
if err := cmd.Start(); err != nil {
//log.Println("Error starting script:", err)
return fmt.Errorf("error starting script: %v", err)
}
// 启动脚本命令并返回可能的错误
go func(cmd *exec.Cmd) {
RegisterProcess(storeInfo.Name, storeInfo.Setting.ProgressName, storeInfo.Setting.IsOn, cmd)
if err := cmd.Wait(); err != nil {
log.Println("Error starting script:", err)
return
}
}(cmd)
return nil
}
func runRestart(storeInfo StoreInfo) error {
err := runStop(storeInfo)
if err != nil {
return fmt.Errorf("failed to stop process %s: %w", storeInfo.Name, err)
}
return runStart(storeInfo)
}
func runExec(storeInfo StoreInfo, cmdParam Cmd) error {
err := SetEnvs(cmdParam.Envs)
if err != nil {
return fmt.Errorf("failed to set start environment variable %s: %w", storeInfo.Name, err)
}
log.Printf("bin path:%v", cmdParam.BinPath)
log.Printf("cmds:%v", cmdParam.Cmds)
cmd := exec.Command(cmdParam.BinPath, cmdParam.Cmds...)
if runtime.GOOS == "windows" {
// 在Windows上通过设置CreationFlags来隐藏窗口
cmd = SetHideConsoleCursor(cmd)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to run exec process %s: %w", storeInfo.Name, err)
}
go func() {
if cmdParam.Waiting > 0 {
if err = cmd.Wait(); err != nil {
log.Printf("failed to wait exec process %s: %s", storeInfo.Name, err.Error())
return
}
time.Sleep(time.Second * time.Duration(cmdParam.Waiting))
}
if cmdParam.Kill {
if storeInfo.Setting.ProgressName != "" {
if err := KillProcessByName(storeInfo.Setting.ProgressName); err != nil {
log.Printf("failed to kill process %s: %s", storeInfo.Setting.ProgressName, err.Error())
}
} else {
if err := cmd.Process.Kill(); err != nil {
log.Printf("failed to kill process %s: %s", storeInfo.Name, err.Error())
return
}
}
}
//log.Printf("run exec process %s, name is %s", storeInfo.Name, cmdParam.Name)
}()
return nil
}
func WriteFile(cmd Cmd) error {
if cmd.FilePath != "" {
content := cmd.Content
if content != "" {
err := os.WriteFile(cmd.FilePath, []byte(content), 0644)
if err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
}
}
return nil
}
func ChangeFile(storeInfo StoreInfo, cmd Cmd) error {
if cmd.FilePath != "" && cmd.TplPath != "" && libs.PathExists(cmd.TplPath) && libs.PathExists(cmd.FilePath) {
content, err := os.ReadFile(cmd.TplPath)
if err != nil {
return fmt.Errorf("failed to read tpl file: %w", err)
}
exePath := GetExePath(storeInfo.Name)
contentstr := strings.ReplaceAll(string(content), "{exePath}", exePath)
contentstr = ChangeConfig(storeInfo, contentstr)
if err := os.WriteFile(cmd.FilePath, []byte(contentstr), 0644); err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
}
return nil
}
func DeleteFile(cmd Cmd) error {
if cmd.FilePath != "" {
err := os.Remove(cmd.FilePath)
if err != nil {
return fmt.Errorf("failed to delete file: %w", err)
}
}
return nil
}
func MkDir(cmd Cmd) error {
err := os.MkdirAll(cmd.FilePath, os.ModePerm)
if err != nil {
return fmt.Errorf("failed to make dir: %w", err)
}
return nil
}
func Unzip(cmd Cmd) error {
return files.Decompress(cmd.FilePath, cmd.Content)
}
func Zip(cmd Cmd) error {
return files.Encompress(cmd.FilePath, cmd.Content)
}

View file

@ -3,79 +3,49 @@ package store
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"godo/files"
"godo/libs" "godo/libs"
"log" "log"
"net/http" "net/http"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime"
"strings" "strings"
"time"
) )
type StoreInfo struct {
Name string `json:"name"`
URL string `json:"url"`
NeedDownload bool `json:"needDownload"`
Icon string `json:"icon"`
Setting Setting `json:"setting"`
Config map[string]any `json:"config"`
Cmds map[string][]Cmd `json:"cmds"`
Install Install `json:"install"`
Start Install `json:"start"`
}
type Setting struct {
BinPath string `json:"binPath"`
ConfPath string `json:"confPath"`
DataDir string `json:"dataDir"`
LogDir string `json:"logDir"`
}
type Item struct {
Name string `json:"name"`
Value any `json:"value"`
}
type Cmd struct {
Name string `json:"name"`
FilePath string `json:"filePath,omitempty"`
Content string `json:"content,omitempty"`
BinPath string `json:"binPath,omitempty"`
Cmds []string `json:"cmds,omitempty"`
Waiting int `json:"waiting"`
Kill bool `json:"kill"`
Envs []Item `json:"envs"`
}
type Install struct {
Envs []Item `json:"envs"`
Cmds []string `json:"cmds"`
}
func InstallHandler(w http.ResponseWriter, r *http.Request) { func InstallHandler(w http.ResponseWriter, r *http.Request) {
pluginName := r.URL.Query().Get("name") pluginName := r.URL.Query().Get("name")
if pluginName == "" { if pluginName == "" {
libs.ErrorMsg(w, "the app name is empty!") libs.ErrorMsg(w, "the app name is empty!")
return return
} }
exeDir := libs.GetRunDir() exePath := GetExePath(pluginName)
exePath := filepath.Join(exeDir, pluginName) //log.Printf("the app path is %s", exePath)
log.Printf("the app path is %s", exePath)
if !libs.PathExists(exePath) { if !libs.PathExists(exePath) {
libs.ErrorMsg(w, "the app path is not exists!") libs.ErrorMsg(w, "the app path is not exists!")
return return
} }
installFile := filepath.Join(exePath, "install.json") installInfo, err := GetInstallInfo(pluginName)
if !libs.PathExists(installFile) { if err != nil {
libs.ErrorMsg(w, "the install.json is not exists!") libs.ErrorMsg(w, "the install.json is error:"+err.Error())
return
}
if pluginName != installInfo.Name {
libs.ErrorMsg(w, "the app name must equal the install.json!")
return
}
if !installInfo.NeedInstall {
libs.SuccessMsg(w, "success", "the app is installed!")
return
}
storeFile := filepath.Join(exePath, "store.json")
if !libs.PathExists(storeFile) {
libs.ErrorMsg(w, "the store.json is not exists!")
return return
} }
var storeInfo StoreInfo var storeInfo StoreInfo
content, err := os.ReadFile(installFile) content, err := os.ReadFile(storeFile)
if err != nil { if err != nil {
libs.ErrorMsg(w, "cant read the install.json!") libs.ErrorMsg(w, "cant read the store.json!")
return return
} }
//设置 info.json //设置 info.json
@ -85,17 +55,16 @@ func InstallHandler(w http.ResponseWriter, r *http.Request) {
err = json.Unmarshal(contentBytes, &storeInfo) err = json.Unmarshal(contentBytes, &storeInfo)
if err != nil { if err != nil {
log.Printf("Error during unmarshal: %v", err) log.Printf("Error during unmarshal: %v", err)
libs.ErrorMsg(w, "the install.json is error: "+err.Error()) libs.ErrorMsg(w, "the store.json is error: "+err.Error())
return return
} }
replacePlaceholdersInCmds(&storeInfo) replacePlaceholdersInCmds(&storeInfo)
infoFile := filepath.Join(exePath, "info.json") storeInfo.Name = installInfo.Name
err = SaveStoreInfo(storeInfo, infoFile) err = SaveInfoFile(storeInfo)
if err != nil { if err != nil {
libs.ErrorMsg(w, "save the info.json is error!") libs.ErrorMsg(w, "the store info.json is error: "+err.Error())
return return
} }
//设置config //设置config
if libs.PathExists(storeInfo.Setting.ConfPath + ".tpl") { if libs.PathExists(storeInfo.Setting.ConfPath + ".tpl") {
err = SaveStoreConfig(storeInfo, exePath) err = SaveStoreConfig(storeInfo, exePath)
@ -109,20 +78,27 @@ func InstallHandler(w http.ResponseWriter, r *http.Request) {
libs.ErrorMsg(w, "install the app is error!") libs.ErrorMsg(w, "install the app is error!")
return return
} }
var res string
//复制static目录 //复制static目录
staticPath := filepath.Join(exePath, "static") staticPath := filepath.Join(exePath, "static")
if libs.PathExists(staticPath) { if libs.PathExists(staticPath) {
staticDir := libs.GetStaticDir() staticDir := libs.GetStaticDir()
targetPath := filepath.Join(staticDir, pluginName) targetPath := filepath.Join(staticDir, pluginName)
err = os.Rename(staticPath, targetPath) if !libs.PathExists(targetPath) {
if err != nil { err = os.Rename(staticPath, targetPath)
// libs.ErrorMsg(w, "copy the static is error!") if err != nil {
// return log.Printf("copy the static is error! %v", err)
log.Printf("copy the static is error! %v", err) }
iconPath := filepath.Join(targetPath, storeInfo.Icon)
if libs.PathExists(iconPath) {
res = "http://localhost:56780/static/" + pluginName + "/" + storeInfo.Icon
}
} }
} }
libs.SuccessMsg(w, "success", "install the app success!") libs.SuccessMsg(w, res, "install the app success!")
} }
func UnInstallHandler(w http.ResponseWriter, r *http.Request) { func UnInstallHandler(w http.ResponseWriter, r *http.Request) {
pluginName := r.URL.Query().Get("name") pluginName := r.URL.Query().Get("name")
if pluginName == "" { if pluginName == "" {
@ -136,8 +112,16 @@ func UnInstallHandler(w http.ResponseWriter, r *http.Request) {
// return // return
log.Printf("stop the app is error! %s", err) log.Printf("stop the app is error! %s", err)
} }
exeDir := libs.GetRunDir() installInfo, err := GetInstallInfo(pluginName)
exePath := filepath.Join(exeDir, pluginName) if err != nil {
libs.ErrorMsg(w, "the install.json is error:"+err.Error())
return
}
if installInfo.IsDev {
libs.SuccessMsg(w, "success", "uninstall the app success!")
return
}
exePath := GetExePath(pluginName)
//log.Printf("the app path is %s", exePath) //log.Printf("the app path is %s", exePath)
if libs.PathExists(exePath) { if libs.PathExists(exePath) {
err := os.RemoveAll(exePath) err := os.RemoveAll(exePath)
@ -158,6 +142,28 @@ func UnInstallHandler(w http.ResponseWriter, r *http.Request) {
libs.SuccessMsg(w, "success", "uninstall the app success!") libs.SuccessMsg(w, "success", "uninstall the app success!")
} }
func GetInstallInfo(pluginName string) (InstallInfo, error) {
var installInfo InstallInfo
exePath := GetExePath(pluginName)
installFile := filepath.Join(exePath, "install.json")
if !libs.PathExists(installFile) {
return installInfo, fmt.Errorf("install.json is not exist:%s", installFile)
}
content, err := os.ReadFile(installFile)
if err != nil {
return installInfo, err
}
err = json.Unmarshal(content, &installInfo)
if err != nil {
return installInfo, err
}
return installInfo, nil
}
func GetExePath(pluginName string) string {
exeDir := libs.GetRunDir()
return filepath.Join(exeDir, pluginName)
}
func convertValueToString(value any) string { func convertValueToString(value any) string {
var strValue string var strValue string
switch v := value.(type) { switch v := value.(type) {
@ -187,157 +193,69 @@ func InstallStore(storeInfo StoreInfo) error {
} }
return nil return nil
} }
func RunCmds(storeInfo StoreInfo, cmdKey string) error { func RunCmd(storeInfo StoreInfo, cmd Cmd) error {
cmds := storeInfo.Cmds[cmdKey] if cmd.Name == "stop" {
for _, cmd := range cmds { runStop(storeInfo)
}
if cmd.Name == "stop" { if cmd.Name == "start" {
runStop(storeInfo) runStart(storeInfo)
} }
if cmd.Name == "start" { if cmd.Name == "restart" {
runStart(storeInfo) runRestart(storeInfo)
} }
if cmd.Name == "restart" { if cmd.Name == "exec" {
runRestart(storeInfo) runExec(storeInfo, cmd)
} }
if cmd.Name == "exec" { if cmd.Name == "writeFile" {
runExec(storeInfo, cmd) err := WriteFile(cmd)
} if err != nil {
if cmd.Name == "writeFile" { return fmt.Errorf("failed to write to file: %w", err)
err := WriteFile(cmd)
if err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
}
if cmd.Name == "deleteFile" {
err := DeleteFile(cmd)
if err != nil {
return fmt.Errorf("failed to delete file: %w", err)
}
}
if cmd.Name == "unzip" {
err := Unzip(cmd)
if err != nil {
return fmt.Errorf("failed to unzip file: %w", err)
}
}
if cmd.Name == "zip" {
err := Zip(cmd)
if err != nil {
return fmt.Errorf("failed to unzip file: %w", err)
}
}
if cmd.Name == "mkdir" {
err := MkDir(cmd)
if err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
} }
} }
return nil if cmd.Name == "changeFile" {
} err := ChangeFile(storeInfo, cmd)
func runStop(storeInfo StoreInfo) error { if err != nil {
return StopCmd(storeInfo.Name) return fmt.Errorf("failed to change to file: %w", err)
}
func runStart(storeInfo StoreInfo) error {
err := SetEnvs(storeInfo.Start.Envs)
if err != nil {
return fmt.Errorf("failed to set start environment variable %s: %w", storeInfo.Name, err)
}
if !libs.PathExists(storeInfo.Setting.BinPath) {
return fmt.Errorf("script file %s does not exist", storeInfo.Setting.BinPath)
}
var cmd *exec.Cmd
if len(storeInfo.Start.Cmds) > 0 {
cmd = exec.Command(storeInfo.Setting.BinPath, storeInfo.Start.Cmds...)
} else {
cmd = exec.Command(storeInfo.Setting.BinPath)
}
if runtime.GOOS == "windows" {
// 在Windows上通过设置CreationFlags来隐藏窗口
cmd = SetHideConsoleCursor(cmd)
}
// 启动脚本命令并返回可能的错误
go func(cmd *exec.Cmd) {
if err := cmd.Start(); err != nil {
log.Println("Error starting script:", err)
return
}
RegisterProcess(storeInfo.Name, cmd)
}(cmd)
return nil
}
func runRestart(storeInfo StoreInfo) error {
err := runStop(storeInfo)
if err != nil {
return fmt.Errorf("failed to stop process %s: %w", storeInfo.Name, err)
}
return runStart(storeInfo)
}
func runExec(storeInfo StoreInfo, cmdParam Cmd) error {
err := SetEnvs(cmdParam.Envs)
if err != nil {
return fmt.Errorf("failed to set start environment variable %s: %w", storeInfo.Name, err)
}
log.Printf("bin path:%v", cmdParam.BinPath)
log.Printf("cmds:%v", cmdParam.Cmds)
cmd := exec.Command(cmdParam.BinPath, cmdParam.Cmds...)
if runtime.GOOS == "windows" {
// 在Windows上通过设置CreationFlags来隐藏窗口
cmd = SetHideConsoleCursor(cmd)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to run exec process %s: %w", storeInfo.Name, err)
}
if cmdParam.Waiting > 0 {
if err = cmd.Wait(); err != nil {
return fmt.Errorf("failed to wait for exec process %s: %w", storeInfo.Name, err)
}
time.Sleep(time.Second * time.Duration(cmdParam.Waiting))
}
if cmdParam.Kill {
if err := cmd.Process.Kill(); err != nil {
return fmt.Errorf("failed to kill process: %v", err)
} }
} }
log.Printf("run exec process %s, name is %s", storeInfo.Name, cmdParam.Name) if cmd.Name == "deleteFile" {
return nil err := DeleteFile(cmd)
}
func WriteFile(cmd Cmd) error {
if cmd.FilePath != "" {
content := cmd.Content
if content != "" {
err := os.WriteFile(cmd.FilePath, []byte(content), 0644)
if err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
}
}
return nil
}
func DeleteFile(cmd Cmd) error {
if cmd.FilePath != "" {
err := os.Remove(cmd.FilePath)
if err != nil { if err != nil {
return fmt.Errorf("failed to delete file: %w", err) return fmt.Errorf("failed to delete file: %w", err)
} }
} }
return nil if cmd.Name == "unzip" {
} err := Unzip(cmd)
func MkDir(cmd Cmd) error { if err != nil {
err := os.MkdirAll(cmd.FilePath, os.ModePerm) return fmt.Errorf("failed to unzip file: %w", err)
if err != nil { }
return fmt.Errorf("failed to make dir: %w", err) }
if cmd.Name == "zip" {
err := Zip(cmd)
if err != nil {
return fmt.Errorf("failed to unzip file: %w", err)
}
}
if cmd.Name == "mkdir" {
err := MkDir(cmd)
if err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
} }
return nil return nil
} }
func Unzip(cmd Cmd) error { func RunCmds(storeInfo StoreInfo, cmdKey string) error {
return files.Decompress(cmd.FilePath, cmd.Content) cmds := storeInfo.Cmds[cmdKey]
} for _, cmd := range cmds {
func Zip(cmd Cmd) error { if _, ok := storeInfo.Cmds[cmd.Name]; ok {
return files.Encompress(cmd.FilePath, cmd.Content) RunCmds(storeInfo, cmd.Name)
} else {
RunCmd(storeInfo, cmd)
}
}
return nil
} }
func SetEnvs(envs []Item) error { func SetEnvs(envs []Item) error {
if len(envs) > 0 { if len(envs) > 0 {
for _, item := range envs { for _, item := range envs {
@ -352,6 +270,11 @@ func SetEnvs(envs []Item) error {
} }
return nil return nil
} }
func SaveInfoFile(storeInfo StoreInfo) error {
exePath := GetExePath(storeInfo.Name)
infoFile := filepath.Join(exePath, "info.json")
return SaveStoreInfo(storeInfo, infoFile)
}
func SaveStoreInfo(storeInfo StoreInfo, infoFile string) error { func SaveStoreInfo(storeInfo StoreInfo, infoFile string) error {
// 使用 json.MarshalIndent 直接获取内容的字节切片 // 使用 json.MarshalIndent 直接获取内容的字节切片
content, err := json.MarshalIndent(storeInfo, "", " ") content, err := json.MarshalIndent(storeInfo, "", " ")

View file

@ -10,12 +10,15 @@ import (
) )
type Process struct { type Process struct {
Name string `json:"name"` Name string `json:"name"`
Running bool `json:"running"` Running bool `json:"running"`
ExitCode int `json:"exitCode"` ExitCode int `json:"exitCode"`
Cmd *exec.Cmd `json:"cmd"` Cmd *exec.Cmd `json:"cmd"`
Waiting bool `json:"waiting"` Pid int `json:"pid"`
LastPing time.Time `json:"lastPing"` ProgressName string `json:"progressName"`
Waiting bool `json:"waiting"`
IsOn bool `json:"isOn"`
LastPing time.Time `json:"lastPing"`
} }
var ( var (
@ -26,13 +29,16 @@ var (
func respondWithError(w http.ResponseWriter, code int, message string) { func respondWithError(w http.ResponseWriter, code int, message string) {
http.Error(w, message, code) http.Error(w, message, code)
} }
func RegisterProcess(name string, cmdstr *exec.Cmd) { func RegisterProcess(name string, progressName string, isOn bool, cmdstr *exec.Cmd) {
processesMu.Lock() processesMu.Lock()
defer processesMu.Unlock() defer processesMu.Unlock()
processes[name] = &Process{ processes[name] = &Process{
Name: name, Name: name,
Running: true, Running: true,
Cmd: cmdstr, Cmd: cmdstr,
Pid: cmdstr.Process.Pid,
IsOn: isOn,
ProgressName: progressName,
} }
} }
func GetCmd(name string) *Process { func GetCmd(name string) *Process {
@ -42,20 +48,9 @@ func GetCmd(name string) *Process {
return processes[name] return processes[name]
} }
func Status(w http.ResponseWriter, r *http.Request) { func Status(w http.ResponseWriter, r *http.Request) {
// processesMu.RLock()
// defer processesMu.RUnlock()
var ps []Process var ps []Process
for name, cmd := range processes { for name, cmd := range processes {
// if cmd.Cmd.ProcessState != nil && cmd.Cmd.ProcessState.Exited() { ps = append(ps, Process{Name: name, Running: cmd.Running, Waiting: cmd.Waiting, Pid: cmd.Pid, LastPing: cmd.LastPing, ExitCode: cmd.Cmd.ProcessState.ExitCode()})
// cmd.Running = false
// // 进程已经退出
// ps = append(ps, Process{Name: name, Running: false, Waiting: cmd.Waiting, LastPing: cmd.LastPing, ExitCode: cmd.Cmd.ProcessState.ExitCode()})
// } else {
// // 进程仍在运行
// ps = append(ps, Process{Name: name, Running: true, Waiting: cmd.Waiting, LastPing: cmd.LastPing})
// }
ps = append(ps, Process{Name: name, Running: cmd.Running, Waiting: cmd.Waiting, LastPing: cmd.LastPing, ExitCode: cmd.Cmd.ProcessState.ExitCode()})
} }
jsonBytes, err := json.MarshalIndent(ps, "", " ") jsonBytes, err := json.MarshalIndent(ps, "", " ")

View file

@ -1,12 +1,9 @@
package store package store
import ( import (
"encoding/json"
"fmt" "fmt"
"godo/libs" "godo/libs"
"log"
"net/http" "net/http"
"os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -49,27 +46,6 @@ func StartAll(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "All processes started.") fmt.Fprintf(w, "All processes started.")
} }
func GetStoreInfo(name string) (StoreInfo, error) {
var storeInfo StoreInfo
exeDir := libs.GetRunDir()
infoPath := filepath.Join(exeDir, name, "info.json")
if !libs.PathExists(infoPath) {
return storeInfo, fmt.Errorf("process information for '%s' not found", name)
}
content, err := os.ReadFile(infoPath)
if err != nil {
return storeInfo, fmt.Errorf("failed to read info.json: %v", err)
}
if err := json.Unmarshal(content, &storeInfo); err != nil {
return storeInfo, fmt.Errorf("failed to unmarshal info.json: %v", err)
}
scriptPath := storeInfo.Setting.BinPath
if !libs.PathExists(scriptPath) {
return storeInfo, fmt.Errorf("script file '%s' not found", scriptPath)
}
return storeInfo, nil
}
// ExecuteScript 执行指定名称的脚本。 // ExecuteScript 执行指定名称的脚本。
// 参数: // 参数:
@ -77,42 +53,14 @@ func GetStoreInfo(name string) (StoreInfo, error) {
// 返回值: // 返回值:
// 返回可能遇到的错误如果执行成功则返回nil。 // 返回可能遇到的错误如果执行成功则返回nil。
func ExecuteScript(name string) error { func ExecuteScript(name string) error {
cmd, exists := processes[name] storeInfo, err := GetStoreInfo(name)
if !exists { if err != nil {
log.Printf("process information for '%s' not found", name) return err
//return fmt.Errorf("process information for '%s' not found", name)
storeInfo, err := GetStoreInfo(name)
if err != nil {
return err
}
err = runStart(storeInfo)
if err != nil {
return fmt.Errorf("failed to run script: %v", err)
}
return nil
} }
if cmd.Running { err = runStart(storeInfo)
return nil if err != nil {
return fmt.Errorf("failed to run script: %v", err)
} }
// if err := cmd.Cmd.Start(); err != nil {
// log.Printf("failed to start process %s: %v", name, err)
// return nil
// }
processes[name].Running = true
go func() {
if err := cmd.Cmd.Start(); err != nil {
log.Printf("failed to start process %s: %v", name, err)
return
}
// processesMu.Lock()
// defer processesMu.Unlock()
processes[name].Cmd = cmd.Cmd
}()
return nil return nil
} }
func RunOutHandler(w http.ResponseWriter, r *http.Request) { func RunOutHandler(w http.ResponseWriter, r *http.Request) {

View file

@ -4,6 +4,10 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"os/exec"
"runtime"
"strconv"
"strings"
"time" "time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -11,6 +15,7 @@ import (
) )
func KillByPid(pid int) error { func KillByPid(pid int) error {
log.Println("Killing process with PID:", pid)
p, err := process.NewProcess(int32(pid)) p, err := process.NewProcess(int32(pid))
if err != nil { if err != nil {
return fmt.Errorf("failed to create process object: %w", err) return fmt.Errorf("failed to create process object: %w", err)
@ -23,15 +28,25 @@ func KillByPid(pid int) error {
return nil return nil
} }
func StopCmd(name string) error { func StopCmd(name string) error {
processesMu.Lock()
defer processesMu.Unlock()
cmd, ok := processes[name] cmd, ok := processes[name]
if !ok { if !ok {
return fmt.Errorf("Process not found") return fmt.Errorf("Process not found")
} }
processes[name].Running = false //processes[name].Running = false
err := KillByPid(cmd.Cmd.Process.Pid) if cmd.ProgressName != "" {
if err != nil { err := KillProcessByName(cmd.ProgressName)
return fmt.Errorf("failed to kill process: %w", err) if err != nil {
return fmt.Errorf("failed to kill process: %w", err)
}
} else {
err := KillByPid(cmd.Pid)
if err != nil {
return fmt.Errorf("failed to kill process: %w", err)
}
} }
delete(processes, name)
return nil return nil
} }
func StopProcess(w http.ResponseWriter, r *http.Request) { func StopProcess(w http.ResponseWriter, r *http.Request) {
@ -54,7 +69,8 @@ func StopAllHandler() error {
if err := KillByPid(cmd.Cmd.Process.Pid); err != nil { if err := KillByPid(cmd.Cmd.Process.Pid); err != nil {
return fmt.Errorf("failed to stop process %s: %v", name, err) return fmt.Errorf("failed to stop process %s: %v", name, err)
} }
processes[name].Running = false //processes[name].Running = false
delete(processes, name)
} }
return nil return nil
} }
@ -75,15 +91,15 @@ func ReStartProcess(w http.ResponseWriter, r *http.Request) {
name := vars["name"] name := vars["name"]
// Stop the process first // Stop the process first
if cmd, ok := processes[name]; ok { if _, ok := processes[name]; ok {
if err := KillByPid(cmd.Cmd.Process.Pid); err != nil { if err := StopCmd(name); err != nil {
// respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to stop process %s before restart: %v", name, err)) // respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to stop process %s before restart: %v", name, err))
// return // return
log.Printf("Failed to stop process %s before restart: %v", name, err) log.Printf("Failed to stop process %s before restart: %v", name, err)
} }
// processesMu.Lock() // processesMu.Lock()
// defer processesMu.Unlock() // defer processesMu.Unlock()
processes[name].Running = false //processes[name].Running = false
} else { } else {
respondWithError(w, http.StatusNotFound, fmt.Sprintf("Process %s not found to restart", name)) respondWithError(w, http.StatusNotFound, fmt.Sprintf("Process %s not found to restart", name))
return return
@ -98,3 +114,82 @@ func ReStartProcess(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Process %s restarted.", name) fmt.Fprintf(w, "Process %s restarted.", name)
} }
func killByPids(pids []int) error {
for _, pid := range pids {
if err := KillByPid(pid); err != nil {
return fmt.Errorf("failed to kill process with PID %d: %w", pid, err)
}
fmt.Printf("Killed process with PID %d\n", pid)
}
return nil
}
// KillProcessByName 终止所有与给定名称匹配的进程
func KillProcessByName(processName string) error {
switch runtime.GOOS {
case "windows":
// Windows 系统下的实现
pids, err := findPidsWindows(processName)
if err != nil {
return err
}
err = killByPids(pids)
if err != nil {
return err
}
default:
// Unix/Linux 系统下的实现
pids, err := findPidsUnix(processName)
if err != nil {
return err
}
err = killByPids(pids)
if err != nil {
return err
}
}
return nil
}
// findPidsUnix 在 Unix/Linux 系统下查找具有指定名称的进程的 PID
func findPidsUnix(name string) ([]int, error) {
cmd := exec.Command("pgrep", "-f", name)
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to execute pgrep: %w", err)
}
pids := strings.Fields(string(output))
result := make([]int, len(pids))
for i, pidStr := range pids {
pid, err := strconv.Atoi(pidStr)
if err != nil {
return nil, fmt.Errorf("failed to convert PID to integer: %w", err)
}
result[i] = pid
}
return result, nil
}
// findPidsWindows 在 Windows 系统下查找具有指定名称的进程的 PID
func findPidsWindows(name string) ([]int, error) {
cmd := exec.Command("tasklist")
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to execute tasklist: %w", err)
}
lines := strings.Split(string(output), "\r\n")
var pids []int
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) >= 3 {
if strings.Contains(strings.ToLower(fields[0]), strings.ToLower(name)) {
pid, err := strconv.Atoi(fields[1])
if err != nil {
return nil, fmt.Errorf("failed to convert PID to integer: %w", err)
}
pids = append(pids, pid)
}
}
}
return pids, nil
}

View file

@ -6,10 +6,13 @@ import (
"godo/libs" "godo/libs"
"io" "io"
"net/http" "net/http"
"os"
"path/filepath"
"runtime" "runtime"
"strings"
) )
func GetStoreInfoHandler(w http.ResponseWriter, r *http.Request) { func GetStoreListHandler(w http.ResponseWriter, r *http.Request) {
cate := r.URL.Query().Get("cate") cate := r.URL.Query().Get("cate")
os := runtime.GOOS os := runtime.GOOS
arch := runtime.GOARCH arch := runtime.GOARCH
@ -39,3 +42,76 @@ func GetStoreInfoHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
func StoreSettingHandler(w http.ResponseWriter, r *http.Request) {
var req map[string]any
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
libs.ErrorMsg(w, err.Error())
return
}
if req["name"] == nil {
libs.ErrorMsg(w, "name is required")
return
}
if req["cmdKey"] == nil {
libs.ErrorMsg(w, "cmdKey is required")
return
}
name := req["name"].(string)
cmdKey := req["cmdKey"].(string)
// 替换字符串类型的值中的反斜杠
for key, value := range req {
if strValue, ok := value.(string); ok {
req[key] = strings.ReplaceAll(strValue, "\\", "/")
}
}
storeInfo, err := GetStoreInfo(name)
if err != nil {
libs.ErrorMsg(w, err.Error())
return
}
// 更新storeInfo.Config
for k, v := range req {
if k != "name" && k != "cmdKey" {
storeInfo.Config[k] = v // 如果k存在则更新如果不存在则新增
}
}
err = SaveInfoFile(storeInfo)
if err != nil {
libs.ErrorMsg(w, "the store info.json is error: "+err.Error())
return
}
_, ok := storeInfo.Cmds[cmdKey]
if !ok {
libs.ErrorMsg(w, "cmdKey is not found")
return
}
err = RunCmds(storeInfo, cmdKey)
if err != nil {
libs.ErrorMsg(w, err.Error())
return
}
libs.SuccessMsg(w, "success", "success")
}
func GetStoreInfo(name string) (StoreInfo, error) {
var storeInfo StoreInfo
exeDir := libs.GetRunDir()
infoPath := filepath.Join(exeDir, name, "info.json")
if !libs.PathExists(infoPath) {
return storeInfo, fmt.Errorf("process information for '%s' not found", name)
}
content, err := os.ReadFile(infoPath)
if err != nil {
return storeInfo, fmt.Errorf("failed to read info.json: %v", err)
}
if err := json.Unmarshal(content, &storeInfo); err != nil {
return storeInfo, fmt.Errorf("failed to unmarshal info.json: %v", err)
}
scriptPath := storeInfo.Setting.BinPath
if !libs.PathExists(scriptPath) {
return storeInfo, fmt.Errorf("script file '%s' not found", scriptPath)
}
return storeInfo, nil
}

65
godo/store/types.go Normal file
View file

@ -0,0 +1,65 @@
package store
// InstallInfo 描述了应用程序的安装信息。
// 包含应用程序的名称、下载地址、版本号等关键安装信息。
type InstallInfo struct {
Name string `json:"name"` // 应用程序名称。
URL string `json:"url"` // 应用程序下载地址。
WebUrl string `json:"webUrl"` // 应用程序的网页地址。
IsDev bool `json:"isDev"` // 标志位,表示是否为开发者版本。
NeedDownload bool `json:"needDownload"` // 标志位,表示是否需要下载。
NeedInstall bool `json:"needInstall"` // 标志位,表示是否需要安装。
Version string `json:"version"` // 应用程序的版本号。
Desc string `json:"desc"` // 应用程序的描述信息。
Icon string `json:"icon"` // 应用程序的图标路径。
CheckProgress bool `json:"checkProgress"` // 标志位,表示是否显示启动和停止。
HasRestart bool `json:"hasRestart"` // 标志位,表示安装后是否需要重启。
Setting bool `json:"setting"` // 标志位,表示是否需要配置。
}
// StoreInfo 维护了应用程序商店的信息。
// 包含应用程序的名称、图标、配置信息等。
type StoreInfo struct {
Name string `json:"name"` // 应用程序商店的名称。
Icon string `json:"icon"` // 应用程序商店的图标路径。
Setting Setting `json:"setting"` // 应用程序商店的配置信息。
Config map[string]any `json:"config"` // 应用程序的配置信息映射。
Cmds map[string][]Cmd `json:"cmds"` // 应用程序的命令集合。
Install Install `json:"install"` // 安装应用程序的信息。
Start Install `json:"start"` // 启动应用程序的信息。
}
// Setting 描述了应用程序的设置信息。
// 包含应用程序的二进制文件路径、配置文件路径等关键设置信息。
type Setting struct {
BinPath string `json:"binPath"` // 应用程序二进制文件的路径。
ConfPath string `json:"confPath"` // 应用程序配置文件的路径。
ProgressName string `json:"progressName"` // 进程的名称。
IsOn bool `json:"isOn"` //是否守护进程运行。
}
// Item 是一个通用的键值对结构体,用于表示配置项或环境变量等。
type Item struct {
Name string `json:"name"` // 配置项的名称。
Value any `json:"value"` // 配置项的值。
}
// Cmd 描述了一个命令的详细信息。
// 包含命令的名称、执行路径、环境变量等。
type Cmd struct {
Name string `json:"name"` // 命令的名称。
FilePath string `json:"filePath,omitempty"` // 命令文件的路径。
Content string `json:"content,omitempty"` // 命令的内容。
BinPath string `json:"binPath,omitempty"` // 执行命令的二进制文件路径。
TplPath string `json:"tplPath,omitempty"` // 命令的模板路径。
Cmds []string `json:"cmds,omitempty"` // 要执行的子命令列表。
Waiting int `json:"waiting"` // 等待的时间。
Kill bool `json:"kill"` // 标志位,表示是否需要终止之前的命令。
Envs []Item `json:"envs"` // 命令执行时的环境变量。
}
// Install 描述了安装过程中的环境变量和命令列表。
type Install struct {
Envs []Item `json:"envs"` // 安装过程中需要的环境变量。
Cmds []string `json:"cmds"` // 安装过程中需要执行的命令列表。
}