diff --git a/backup.go b/backup.go index c56e7a5..ee427f4 100644 --- a/backup.go +++ b/backup.go @@ -16,13 +16,23 @@ type BackupTask struct { } func NewBackupTask(config config.Config) *BackupTask { + var ticker *time.Ticker + if config.BackupInterval > 0 { + ticker = time.NewTicker(time.Duration(config.BackupInterval) * time.Second) + } + return &BackupTask{ Config: config, - Ticker: time.NewTicker(time.Duration(config.BackupInterval) * time.Second), + Ticker: ticker, } } func (task *BackupTask) Schedule() { + if task.Ticker == nil { + // 如果 Ticker 为 nil,不需要进行定时备份 + return + } + for range task.Ticker.C { task.RunBackup() } @@ -60,6 +70,41 @@ func (task *BackupTask) RunBackup() { } else { log.Printf("Backup completed successfully: %s", destinationPath) } + + // 删除旧备份(如果设置了天数) + if task.Config.SaveDeleteDays > 0 { + task.deleteOldBackups() + } + +} + +func (task *BackupTask) deleteOldBackups() { + // 读取备份目录 + files, err := os.ReadDir(task.Config.BackupPath) + if err != nil { + log.Printf("Failed to list backup directory: %v", err) + return + } + + // 删除超过SaveDeleteDays天数的备份 + for _, f := range files { + if f.IsDir() { + backupTime, err := time.Parse("2006-01-02-15-04-05", f.Name()) + if err != nil { + log.Printf("Failed to parse backup directory name: %s, error: %v", f.Name(), err) + continue + } + + if time.Since(backupTime).Hours() > float64(task.Config.SaveDeleteDays*24) { + err := os.RemoveAll(filepath.Join(task.Config.BackupPath, f.Name())) + if err != nil { + log.Printf("Failed to delete old backup: %s, error: %v", f.Name(), err) + } else { + log.Printf("Old backup deleted successfully: %s", f.Name()) + } + } + } + } } // copyDir 递归复制目录及其内容 diff --git a/bot/bot.go b/bot/bot.go index 0cb5d6a..0e1f62f 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -207,7 +207,7 @@ func GensokyoHandlerClosure(c *gin.Context, config config.Config) { func getBotHandler(msg string, message OnebotGroupMessage, config config.Config) { // 检查消息是否至少以一个 "getbot" 开头 if !strings.HasPrefix(msg, "getbot") { - sendGroupMessage(message.GroupID, "错误,指令需要以getbot开头", config) + sendGroupMessage(message.GroupID, message.UserID, "错误,指令需要以getbot开头", config) return } @@ -225,14 +225,14 @@ func getBotHandler(msg string, message OnebotGroupMessage, config config.Config) // 确保在第一个非 "getbot" 部分之后还有足够的参数 if len(parts) < startIndex+3 { - sendGroupMessage(message.GroupID, "指令错误,请在palworld-go项目的机器人管理面板生成指令", config) + sendGroupMessage(message.GroupID, message.UserID, "指令错误,请在palworld-go项目的机器人管理面板生成指令", config) return } // 解析number number, err := strconv.ParseInt(parts[startIndex], 10, 64) if err != nil { - sendGroupMessage(message.GroupID, "错误,请在palworld-go项目的机器人管理面板生成指令,number参数错误", config) + sendGroupMessage(message.GroupID, message.UserID, "错误,请在palworld-go项目的机器人管理面板生成指令,number参数错误", config) return } @@ -251,10 +251,10 @@ func getBotHandler(msg string, message OnebotGroupMessage, config config.Config) if err != nil { fmt.Printf("储存pal-go面板端user配置出错 userid: %v 地址:%v\n", message.UserID, ipWithPort) } else { - sendGroupMessage(message.GroupID, "绑定成功,现在你可以在帕鲁帕鲁机器人管理你的palworld-go面板", config) + sendGroupMessage(message.GroupID, message.UserID, "绑定成功,现在你可以在帕鲁帕鲁机器人管理你的palworld-go面板", config) } } else { - sendGroupMessage(message.GroupID, "指令无效,请重新生成,为了面板安全,palworld-go指令不可重复使用,如需多人使用,可多次生成.", config) + sendGroupMessage(message.GroupID, message.UserID, "指令无效,请重新生成,为了面板安全,palworld-go指令不可重复使用,如需多人使用,可多次生成.", config) } } @@ -263,7 +263,7 @@ func getplayerHandler(msg string, message OnebotGroupMessage, config config.Conf userIPData, err := RetrieveIPByUserID(message.UserID) if err != nil { // 发送错误消息 - sendGroupMessage(message.GroupID, "没有初始化,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给机器人", config) + sendGroupMessage(message.GroupID, message.UserID, "没有初始化,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给机器人", config) return } @@ -302,7 +302,7 @@ func getplayerHandler(msg string, message OnebotGroupMessage, config config.Conf return } defer resp.Body.Close() - sendGroupMessage(message.GroupID, "正在刷新玩家,可能需要3-5秒返回,请勿重复操作", config) + sendGroupMessage(message.GroupID, message.UserID, "正在刷新玩家,可能需要3-5秒返回,请勿重复操作", config) // 读取响应 body, err := io.ReadAll(resp.Body) if err != nil { @@ -332,7 +332,7 @@ func getplayerHandler(msg string, message OnebotGroupMessage, config config.Conf uniqueID, _ := StorePlayerInfo(player.PlayerUID, player.SteamID, player.Name) responseMessage += fmt.Sprintf("[%d] %s 上次在线:%s 在线:%t\n", uniqueID, player.Name, formattedLastOnline, player.Online) } - sendGroupMessage(message.GroupID, responseMessage, config) + sendGroupMessage(message.GroupID, message.UserID, responseMessage, config) } } func formatTimeDifference(t time.Time) string { @@ -352,7 +352,7 @@ func formatTimeDifference(t time.Time) string { return fmt.Sprintf("%d分钟前", minutes) } -func sendGroupMessage(groupID int64, message string, config config.Config) error { +func sendGroupMessage(groupID int64, userID int64, message string, config config.Config) error { // 获取基础URL baseURL := config.Onebotv11HttpApiPath @@ -363,6 +363,7 @@ func sendGroupMessage(groupID int64, message string, config config.Config) error requestBody, err := json.Marshal(map[string]interface{}{ "group_id": groupID, "message": message, + "user_id": userID, }) if err != nil { return fmt.Errorf("failed to marshal request body: %w", err) @@ -389,7 +390,7 @@ func kickorbanHandler(msg string, message OnebotGroupMessage, config config.Conf // 检查是否至少有两部分(例如:"踢人 123") if len(parts) < 2 { - sendGroupMessage(message.GroupID, "指令格式错误 应为 踢人 1 封禁 1 kick 1 ban 1", config) + sendGroupMessage(message.GroupID, message.UserID, "指令格式错误 应为 踢人 1 封禁 1 kick 1 ban 1", config) return } @@ -397,27 +398,27 @@ func kickorbanHandler(msg string, message OnebotGroupMessage, config config.Conf var uniqueID int64 _, err := fmt.Sscanf(parts[1], "%d", &uniqueID) if err != nil { - sendGroupMessage(message.GroupID, "指令格式错误 后方应为数字 空格为分割", config) + sendGroupMessage(message.GroupID, message.UserID, "指令格式错误 后方应为数字 空格为分割", config) return } //测试提审核代码 不要删除 if uniqueID == 666 { - sendGroupMessage(message.GroupID, operation+"测试玩家 成功", config) + sendGroupMessage(message.GroupID, message.UserID, operation+"测试玩家 成功", config) return } // 通过uniqueID获取玩家信息 playerInfo, err := RetrievePlayerInfoByID(uniqueID) if err != nil { - sendGroupMessage(message.GroupID, "获取玩家信息失败: "+err.Error(), config) + sendGroupMessage(message.GroupID, message.UserID, "获取玩家信息失败: "+err.Error(), config) return } // 检查SteamID是否有效 _, err = strconv.ParseInt(playerInfo.SteamID, 10, 64) if err != nil { - sendGroupMessage(message.GroupID, playerInfo.Name+"无效的SteamID,帕鲁服务端通病,玩家增加后再次使用 玩家列表 获取可解决", config) + sendGroupMessage(message.GroupID, message.UserID, playerInfo.Name+"无效的SteamID,帕鲁服务端通病,玩家增加后再次使用 玩家列表 获取可解决", config) return } @@ -428,7 +429,7 @@ func kickorbanHandler(msg string, message OnebotGroupMessage, config config.Conf Type: operation, }) if err != nil { - sendGroupMessage(message.GroupID, "构建请求失败: "+err.Error(), config) + sendGroupMessage(message.GroupID, message.UserID, "构建请求失败: "+err.Error(), config) return } @@ -436,7 +437,7 @@ func kickorbanHandler(msg string, message OnebotGroupMessage, config config.Conf userIPData, err := RetrieveIPByUserID(message.UserID) if err != nil { // 发送错误消息 - sendGroupMessage(message.GroupID, "没有正确设置,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给我", config) + sendGroupMessage(message.GroupID, message.UserID, "没有正确设置,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给我", config) return } @@ -453,7 +454,7 @@ func kickorbanHandler(msg string, message OnebotGroupMessage, config config.Conf apiURL := baseURL + "/api/kickorban" req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(reqBody)) if err != nil { - sendGroupMessage(message.GroupID, "创建请求失败: "+err.Error(), config) + sendGroupMessage(message.GroupID, message.UserID, "创建请求失败: "+err.Error(), config) return } req.Header.Set("Content-Type", "application/json") @@ -461,22 +462,22 @@ func kickorbanHandler(msg string, message OnebotGroupMessage, config config.Conf resp, err := client.Do(req) if err != nil { - sendGroupMessage(message.GroupID, "发送请求失败: "+err.Error(), config) + sendGroupMessage(message.GroupID, message.UserID, "发送请求失败: "+err.Error(), config) return } defer resp.Body.Close() // 检查响应状态 if resp.StatusCode != http.StatusOK { - sendGroupMessage(message.GroupID, fmt.Sprintf("%s %s 失败", operation, playerInfo.Name), config) + sendGroupMessage(message.GroupID, message.UserID, fmt.Sprintf("%s %s 失败", operation, playerInfo.Name), config) return } // 发送成功消息 - sendGroupMessage(message.GroupID, fmt.Sprintf("%s %s 成功", operation, playerInfo.Name), config) + sendGroupMessage(message.GroupID, message.UserID, fmt.Sprintf("%s %s 成功", operation, playerInfo.Name), config) } else { // 发送错误消息 - sendGroupMessage(message.GroupID, "没有获取到面板信息,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给我", config) + sendGroupMessage(message.GroupID, message.UserID, "没有获取到面板信息,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给我", config) return } } @@ -485,7 +486,7 @@ func broadcastMessageHandler(msg string, message OnebotGroupMessage, config conf // 从msg中提取广播内容 parts := strings.SplitN(msg, " ", 2) if len(parts) != 2 { - sendGroupMessage(message.GroupID, "广播指令格式错误", config) + sendGroupMessage(message.GroupID, message.UserID, "广播指令格式错误", config) return } @@ -495,7 +496,7 @@ func broadcastMessageHandler(msg string, message OnebotGroupMessage, config conf } reqBody, err := json.Marshal(broadcastReq) if err != nil { - sendGroupMessage(message.GroupID, "创建广播请求失败", config) + sendGroupMessage(message.GroupID, message.UserID, "创建广播请求失败", config) return } @@ -503,7 +504,7 @@ func broadcastMessageHandler(msg string, message OnebotGroupMessage, config conf userIPData, err := RetrieveIPByUserID(message.UserID) if err != nil { // 发送错误消息 - sendGroupMessage(message.GroupID, "没有正确设置,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给我", config) + sendGroupMessage(message.GroupID, message.UserID, "没有正确设置,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给我", config) return } @@ -519,7 +520,7 @@ func broadcastMessageHandler(msg string, message OnebotGroupMessage, config conf apiURL := baseURL + "/api/broadcast" req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(reqBody)) if err != nil { - sendGroupMessage(message.GroupID, "创建请求失败", config) + sendGroupMessage(message.GroupID, message.UserID, "创建请求失败", config) return } req.Header.Set("Content-Type", "application/json") @@ -529,21 +530,21 @@ func broadcastMessageHandler(msg string, message OnebotGroupMessage, config conf client := &http.Client{} resp, err := client.Do(req) if err != nil { - sendGroupMessage(message.GroupID, "发送广播请求失败", config) + sendGroupMessage(message.GroupID, message.UserID, "发送广播请求失败", config) return } defer resp.Body.Close() // 检查响应状态 if resp.StatusCode != http.StatusOK { - sendGroupMessage(message.GroupID, fmt.Sprintf("广播失败,响应状态码: %d", resp.StatusCode), config) + sendGroupMessage(message.GroupID, message.UserID, fmt.Sprintf("广播失败,响应状态码: %d", resp.StatusCode), config) return } - sendGroupMessage(message.GroupID, "广播消息已成功发送", config) + sendGroupMessage(message.GroupID, message.UserID, "广播消息已成功发送", config) } else { // 发送错误消息 - sendGroupMessage(message.GroupID, "没有获取到面板信息,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给我", config) + sendGroupMessage(message.GroupID, message.UserID, "没有获取到面板信息,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给我", config) return } } @@ -553,14 +554,14 @@ func restartHandler(msg string, message OnebotGroupMessage, config config.Config // 从msg中提取参数 parts := strings.Fields(msg) if len(parts) < 3 { - sendGroupMessage(message.GroupID, "重启指令格式错误,应为 重启服务器 多少秒数后重启(整数) 重启公告内容", config) + sendGroupMessage(message.GroupID, message.UserID, "重启指令格式错误,应为 重启服务器 多少秒数后重启(整数) 重启公告内容", config) return } // 检查时间参数是否为数字 seconds, err := strconv.Atoi(parts[1]) if err != nil { - sendGroupMessage(message.GroupID, "重启时间应为数字", config) + sendGroupMessage(message.GroupID, message.UserID, "重启时间应为数字", config) return } @@ -570,7 +571,7 @@ func restartHandler(msg string, message OnebotGroupMessage, config config.Config userIPData, err := RetrieveIPByUserID(message.UserID) if err != nil { // 发送错误消息 - sendGroupMessage(message.GroupID, "没有正确设置,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给我", config) + sendGroupMessage(message.GroupID, message.UserID, "没有正确设置,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给我", config) return } @@ -593,13 +594,13 @@ func restartHandler(msg string, message OnebotGroupMessage, config config.Config reqBody, err := json.Marshal(restartReq) if err != nil { - sendGroupMessage(message.GroupID, "创建重启请求失败", config) + sendGroupMessage(message.GroupID, message.UserID, "创建重启请求失败", config) return } req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(reqBody)) if err != nil { - sendGroupMessage(message.GroupID, "创建请求失败", config) + sendGroupMessage(message.GroupID, message.UserID, "创建请求失败", config) return } @@ -610,22 +611,22 @@ func restartHandler(msg string, message OnebotGroupMessage, config config.Config client := &http.Client{} resp, err := client.Do(req) if err != nil { - sendGroupMessage(message.GroupID, "发送服务器延时重启请求失败", config) + sendGroupMessage(message.GroupID, message.UserID, "发送服务器延时重启请求失败", config) return } defer resp.Body.Close() // 检查响应状态 if resp.StatusCode != http.StatusOK { - sendGroupMessage(message.GroupID, fmt.Sprintf("服务器重启失败,响应状态码: %d", resp.StatusCode), config) + sendGroupMessage(message.GroupID, message.UserID, fmt.Sprintf("服务器重启失败,响应状态码: %d", resp.StatusCode), config) return } // 发送成功消息 - sendGroupMessage(message.GroupID, "服务器将在"+strconv.Itoa(seconds)+"秒后重启,维护公告: "+announcement, config) + sendGroupMessage(message.GroupID, message.UserID, "服务器将在"+strconv.Itoa(seconds)+"秒后重启,维护公告: "+announcement, config) } else { // 发送错误消息 - sendGroupMessage(message.GroupID, "没有获取到面板信息,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给我", config) + sendGroupMessage(message.GroupID, message.UserID, "没有获取到面板信息,请使用palworld-go面板,在机器人管理或服务器主人处获取指令,然后发给我", config) return } @@ -653,5 +654,5 @@ func listCommandsHandler(message OnebotGroupMessage, config config.Config) { commandsStr := strings.Join(commands, "\n") // 发送指令列表 - sendGroupMessage(message.GroupID, "可用指令列表:\n"+commandsStr, config) + sendGroupMessage(message.GroupID, message.UserID, "可用指令列表:\n"+commandsStr, config) } diff --git a/config/config.go b/config/config.go index f59a26b..1b232e3 100644 --- a/config/config.go +++ b/config/config.go @@ -52,8 +52,10 @@ type Config struct { MaintenanceWarningMessage string `json:"maintenanceWarningMessage"` // 维护警告消息 WorldSettings *GameWorldSettings `json:"worldSettings"` // 帕鲁设定 Engine *Engine `json:"engine"` // 服务端引擎设置 - Players []*PlayerW `json:"players"` //白名单玩家数组 - WhiteCheckTime int `json:"whiteCheckTime"` //白名单检测时间 + Players []*PlayerW `json:"players"` // 白名单玩家数组 + WhiteCheckTime int `json:"whiteCheckTime"` // 白名单检测时间 + SaveDeleteDays int `json:"saveDeleteDays"` // 存档删除时间 + SteamCmdPath string `json:"steamCmdPath"` // 自定义steamcmd路径 } // 默认配置 @@ -70,6 +72,7 @@ var defaultConfig = Config{ UseDll: false, Cert: "", Key: "", + SteamCmdPath: "C:\\Program Files\\PalServer\\steam", ServerOptions: []string{"-useperfthreads", "-NoAsyncLoadingThread", "-UseMultithreadForDS"}, CheckInterval: 30, // 30 秒 WebuiPort: "52000", // Webui 端口号 @@ -81,6 +84,7 @@ var defaultConfig = Config{ MemoryCleanupInterval: 1800, // 内存清理时间间隔,设为半小时(1800秒)0代表不清理 RestartInterval: 0, // 自动重启间隔 WhiteCheckTime: 0, // 白名单检查周期 + SaveDeleteDays: 0, // 存档删除时间 RegularMessages: []string{"", ""}, // 默认的定期推送消息数组,初始可为空 MessageBroadcastInterval: 3600, // 默认消息广播周期,假设为1小时(3600秒) MaintenanceWarningMessage: "server is going to rebot,please relogin at 1minute later.", // 默认的维护警告消息 diff --git a/front/palworld-front/src/components/PlayerManage.vue b/front/palworld-front/src/components/PlayerManage.vue index 8b3d6c4..4912508 100644 --- a/front/palworld-front/src/components/PlayerManage.vue +++ b/front/palworld-front/src/components/PlayerManage.vue @@ -3,6 +3,9 @@ 刷新 + 应用白名单
加载中...
@@ -56,6 +59,13 @@ @click.stop="copyToClipboard(player.steamid)" >复制SteamID + 加入白名单 @@ -93,6 +103,21 @@ const loadPlayers = async () => { } }; +const restartSelf = async () => { + loading.value = true; + try { + const response = await axios.get('/api/restartself', { + withCredentials: true, // 确保携带 cookie + }); + players.value = response.data; + } catch (error) { + console.error('API 重启请求发过去了', error); + $q.notify({ type: 'positive', message: '应用白名单成功' }); + } finally { + loading.value = false; + } +}; + onMounted(loadPlayers); const sortedPlayers = computed(() => { @@ -123,6 +148,22 @@ const kickOrBan = async (player: Player, type: 'kick' | 'ban') => { } }; +const addWhite = async (player: Player) => { + try { + await axios.post('/api/addwhite', { + playeruid: player.playeruid, + steamid: player.steamid, + name: player.name, + }); + $q.notify({ + type: 'positive', + message: `加白成功`, + }); + } catch (error) { + $q.notify({ type: 'negative', message: '操作失败' }); + } +}; + const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text).then(() => { $q.notify({ type: 'positive', message: '复制成功' }); diff --git a/front/palworld-front/src/components/RunningProcessStatus.vue b/front/palworld-front/src/components/RunningProcessStatus.vue index 38f58ea..bc6cfed 100644 --- a/front/palworld-front/src/components/RunningProcessStatus.vue +++ b/front/palworld-front/src/components/RunningProcessStatus.vue @@ -39,6 +39,7 @@ +
@@ -60,4 +61,5 @@ const sendRequest = async (url: string) => { const restartServer = () => sendRequest('/api/restart'); const startServer = () => sendRequest('/api/start'); const stopServer = () => sendRequest('/api/stop'); +const updateServer = () => sendRequest('/api/update'); diff --git a/front/palworld-front/src/pages/IndexView.vue b/front/palworld-front/src/pages/IndexView.vue index c86d0ef..d05198f 100644 --- a/front/palworld-front/src/pages/IndexView.vue +++ b/front/palworld-front/src/pages/IndexView.vue @@ -82,6 +82,12 @@ label="Steam安装路径(启动社区服务器用)" class="q-my-md" /> + + + { + try { + const response = await axios.get('/api/getjson'); + config.value = response.data; + } catch (error) { + console.error('Error fetching configuration:', error); + } +}; + // 设置定时器来定期更新状态 const updateTimer = window.setInterval(() => { updateStatus(); diff --git a/front/palworld-front/src/pages/LoginView.vue b/front/palworld-front/src/pages/LoginView.vue index 1b04df0..f39a6c8 100644 --- a/front/palworld-front/src/pages/LoginView.vue +++ b/front/palworld-front/src/pages/LoginView.vue @@ -83,7 +83,8 @@ async function login() { isLoggedIn.value = true; void $router.push('/index'); } else { - loginError.value = '登录失败,请检查用户名和密码。'; + loginError.value = + '登录失败,请检查用户名和密码。请查看程序窗口输出的用户名密码,或在同目录下config.json中搜索:serverName、adminPassword 对应用户名,密码。'; // 显示通知 $q.notify({ color: 'negative', @@ -93,7 +94,8 @@ async function login() { }); } } catch (err) { - loginError.value = '登录失败,请检查用户名和密码。'; + loginError.value = + '登录失败,请检查用户名和密码。请查看程序窗口输出的用户名密码,或在同目录下config.json中搜索:serverName、adminPassword 对应用户名,密码。'; $q.notify({ color: 'negative', position: 'top', diff --git a/memorycheck.go b/memorycheck.go index c8c209c..aece984 100644 --- a/memorycheck.go +++ b/memorycheck.go @@ -20,14 +20,24 @@ type MemoryCheckTask struct { } func NewMemoryCheckTask(config config.Config, BackupTask *BackupTask) *MemoryCheckTask { + var ticker *time.Ticker + if config.MemoryCheckInterval > 0 { + ticker = time.NewTicker(time.Duration(config.MemoryCheckInterval) * time.Second) + } + return &MemoryCheckTask{ Config: config, - Ticker: time.NewTicker(time.Duration(config.MemoryCheckInterval) * time.Second), + Ticker: ticker, BackupTask: BackupTask, } } func (task *MemoryCheckTask) Schedule() { + if task.Ticker == nil { + // 如果 Ticker 为 nil,不需要进行定时检查 + return + } + for range task.Ticker.C { task.checkMemory() } diff --git a/mod/embeds/pal-plugin-loader.dll b/mod/embeds/pal-plugin-loader.dll index 17c4ebd..2b549ce 100644 Binary files a/mod/embeds/pal-plugin-loader.dll and b/mod/embeds/pal-plugin-loader.dll differ diff --git a/tool/executor.go b/tool/executor.go index b1404e1..1324674 100644 --- a/tool/executor.go +++ b/tool/executor.go @@ -3,10 +3,16 @@ package tool import ( "errors" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" "strings" "time" "github.com/gorcon/rcon" + "github.com/hoshinonyaruko/palworld-go/config" ) var ( @@ -64,3 +70,25 @@ func (e *Executor) Close() error { } return nil } + +// UpdateServer 使用SteamCMD更新服务端 +func CreateAndRunPSScript(config config.Config) error { + scriptContent := fmt.Sprintf("$SteamCmdPath = \"%s\\steamcmd.exe\"\n& $SteamCmdPath +login anonymous +app_update 2394010 validate +quit", config.SteamCmdPath) + scriptPath := filepath.Join(os.TempDir(), "update-server.ps1") + + // 创建.ps1文件 + err := os.WriteFile(scriptPath, []byte(scriptContent), 0644) + if err != nil { + return fmt.Errorf("failed to create PowerShell script: %w", err) + } + + // 使用cmd /C start powershell来在新窗口中执行.ps1脚本 + cmd := exec.Command("cmd", "/C", "start", "powershell", "-ExecutionPolicy", "Bypass", "-File", scriptPath) + err = cmd.Start() + if err != nil { + return fmt.Errorf("failed to execute PowerShell script: %w", err) + } + + log.Printf("PowerShell script started in a new window: %s", scriptPath) + return nil +} diff --git a/tool/rcon.go b/tool/rcon.go index e3f0f85..7688eb6 100644 --- a/tool/rcon.go +++ b/tool/rcon.go @@ -216,7 +216,7 @@ func CheckAndKickPlayers(config config.Config) { } for _, player := range players { - if player.Online && !isPlayerInWhitelist(player, config.Players) { + if player.Online && !IsPlayerInWhitelist(player, config.Players) { // 玩家在线但不在白名单,执行踢出操作 if err := KickPlayer(config, player.SteamID); err != nil { log.Printf("踢出玩家失败: %v", err) @@ -227,7 +227,7 @@ func CheckAndKickPlayers(config config.Config) { } } -func isPlayerInWhitelist(player PlayerW, whitelist []*config.PlayerW) bool { +func IsPlayerInWhitelist(player PlayerW, whitelist []*config.PlayerW) bool { for _, wp := range whitelist { if (wp.Name == "" || wp.Name == player.Name) && (wp.SteamID == "" || wp.SteamID == player.SteamID) && diff --git a/webui/api.go b/webui/api.go index 4c9f51b..1e8c4cc 100644 --- a/webui/api.go +++ b/webui/api.go @@ -59,6 +59,12 @@ type KickOrBanRequest struct { Type string `json:"type"` } +type AddWhiteRequest struct { + PlayerUID string `json:"playeruid"` + SteamID string `json:"steamid"` + Name string `json:"name"` +} + // ChangeSaveRequest 用于解析请求体 type ChangeSaveRequest struct { Path string `json:"path"` @@ -196,6 +202,21 @@ func CombinedMiddleware(config config.Config, db *bbolt.DB) gin.HandlerFunc { handleRestartLater(c, config) return } + // 处理 /update 的POST请求 更新服务端 + if c.Request.URL.Path == "/api/update" && c.Request.Method == http.MethodPost { + handleUpdate(c, config) + return + } + // 处理 /addwhite 的POST请求 + if c.Request.URL.Path == "/api/addwhite" && c.Request.Method == http.MethodPost { + handleAddWhite(c, &config) + return + } + // 处理 /api/restartself 的POST请求 + if c.Request.URL.Path == "/api/restartself" && c.Request.Method == http.MethodGet { + HandleRestartSelf(c, config) + return + } } else if strings.HasPrefix(c.Request.URL.Path, "/bot") { bot.GensokyoHandlerClosure(c, config) @@ -390,6 +411,27 @@ func HandleSaveJSON(c *gin.Context, cfg config.Config) { } +func HandleRestartSelf(c *gin.Context, cfg config.Config) { + // 从请求中获取cookie + cookieValue, err := c.Cookie("login_cookie") + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized: Cookie not provided"}) + return + } + + // 使用ValidateCookie函数验证cookie + isValid, err := ValidateCookie(cookieValue) + if err != nil || !isValid { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized: Invalid cookie"}) + return + } + + // Cookie验证通过后,执行重启操作 + c.JSON(http.StatusOK, gin.H{"message": "Restart initiated"}) + //重启自身 很快 唰的一下 + sys.RestartApplication() +} + func HandleRestart(c *gin.Context, cfg config.Config) { // 从请求中获取cookie cookieValue, err := c.Cookie("login_cookie") @@ -1163,3 +1205,151 @@ func handleRestartLater(c *gin.Context, config config.Config) { c.JSON(http.StatusOK, gin.H{"message": "Restart scheduled successfully"}) } + +// handleUpdate 处理 /api/update 的POST请求 +func handleUpdate(c *gin.Context, config config.Config) { + // 首先检查是否为Windows系统 + if runtime.GOOS != "windows" { + handleUpdateLinux(c, config) + return + } + + // 从请求中获取cookie + cookieValue, err := c.Cookie("login_cookie") + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized: Cookie not provided"}) + return + } + + // 使用ValidateCookie函数验证cookie + isValid, err := ValidateCookie(cookieValue) + if err != nil || !isValid { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized: Invalid cookie"}) + return + } + + // 终止当前服务器进程 + if err := sys.KillProcess(); err != nil { + log.Printf("Failed to stop the server for update: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to stop the server for update"}) + } + + // 在PowerShell中执行更新脚本 + err = tool.CreateAndRunPSScript(config) + if err != nil { + log.Printf("Failed to execute update script: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to execute update script"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Update initiated successfully"}) +} + +// handleUpdateLinux 处理Linux系统上的/api/update的POST请求 +func handleUpdateLinux(c *gin.Context, cfg config.Config) { + // 检查是否为Linux系统 + if runtime.GOOS != "linux" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Update only supported on Linux"}) + return + } + + // 从请求中获取cookie + cookieValue, err := c.Cookie("login_cookie") + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized: Cookie not provided"}) + return + } + + // 使用ValidateCookie函数验证cookie + isValid, err := ValidateCookie(cookieValue) + if err != nil || !isValid { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized: Invalid cookie"}) + return + } + + // 终止游戏服务 + stopCmd := exec.Command("sudo", "systemctl", "stop", "pal-server") + if err := stopCmd.Run(); err != nil { + log.Printf("Failed to stop the server for update: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to stop the server for update"}) + } + + // 更新游戏,假设已经切换至root用户或有足够权限执行 + updateCmd := exec.Command("sh", "-c", "wget -O - https://pal.pet/update_ubuntu.sh | bash") + if err := updateCmd.Run(); err != nil { + log.Printf("Failed to execute update script: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to execute update script"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Update initiated successfully"}) +} + +func handleAddWhite(c *gin.Context, cfg *config.Config) { + var req AddWhiteRequest + + // 绑定JSON请求体到req + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + // 处理特殊值 "" + if req.Name == "" { + req.Name = "" + } + if req.SteamID == "" { + req.SteamID = "" + } + if req.PlayerUID == "" { + req.PlayerUID = "" + } + + // 创建PlayerW对象 + player := &config.PlayerW{ + Name: req.Name, + SteamID: req.SteamID, + PlayerUID: req.PlayerUID, + } + + // 检查玩家是否已在白名单中 + found := false + for i, wp := range cfg.Players { + if IsPlayerInWhitelist(player, cfg.Players) { + // 如果找到相同玩家,检查信息是否完全相同 + if wp.Name != player.Name || wp.SteamID != player.SteamID || wp.PlayerUID != player.PlayerUID { + // 更新玩家信息 + cfg.Players[i] = player + c.JSON(http.StatusOK, gin.H{"message": "Player information updated successfully"}) + } else { + // 玩家信息完全相同,不需要更新 + c.JSON(http.StatusOK, gin.H{"message": "Player already in whitelist with same information"}) + } + found = true + break + } + } + + // 如果玩家不在白名单中,添加玩家 + if !found { + cfg.Players = append(cfg.Players, player) + c.JSON(http.StatusOK, gin.H{"message": "Player added to whitelist successfully"}) + } + + // 调用saveFunc来保存config + writeConfigToFile(*cfg) + + // 重启自身 很快 唰的一下 + // sys.RestartApplication() +} + +func IsPlayerInWhitelist(player *config.PlayerW, whitelist []*config.PlayerW) bool { + for _, wp := range whitelist { + if (wp.Name == "" || wp.Name == player.Name) && + (wp.SteamID == "" || wp.SteamID == player.SteamID) && + (wp.PlayerUID == "" || wp.PlayerUID == player.PlayerUID) { + return true + } + } + return false +}