feat: refact id column define
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
> 出于安全及环境隔离考虑,推荐使用 Docker/Compose 部署方式。[镜像地址](https://github.com/engigu/baihu-panel/pkgs/container/baihu)
|
> 出于安全及环境隔离考虑,推荐使用 Docker/Compose 部署方式。[镜像地址](https://github.com/engigu/baihu-panel/pkgs/container/baihu)
|
||||||
|
|
||||||
|
## ⚠️ 重大升级与迁移说明 (v3 数据结构)
|
||||||
|
本次更新包含底层数据结构的重大突破,将所有数据的 ID 类型从数字编号平滑迁移为20位的字符式全局唯一标识符(`xid`)。系统在启动时会**自动进行数据的清洗、映射、拷贝与外键修补**,以确保旧数据被妥善对接。
|
||||||
|
- **备份位置**:执行迁移前,即使有程序自动转换逻辑(data\migration_v3_backup_backup_xxx.zip),为了数据安全,仍然建议您**提前手动进行备份**。系统的默认 SQLite 数据库文件通常位于配置的 `data/` 目录中。
|
||||||
|
- **如果遇到失败**:由于不同用户原本的数据和环境复杂度存在差异,如果遇到未预期的迁移失败或数据显示丢失,**请使用原本备份的数据库** 并 **降级至 `v1.0.10` 及以下旧版本** 进行恢复与使用。
|
||||||
|
- **全新启用**:对部分希望拥抱新数据结构的用户而言,也可以根据情况选择在新版本中直接重新建立配置。
|
||||||
|
- **致谢与展望**:这次数据结构级的大幅重构,主要是为了后续项目功能扩展(含分布式管理、多租户隔离、数据同步等)的底层根基准备,再不改以后改不动了。给大家带来的使用不便敬请见谅,感谢支持!
|
||||||
|
|
||||||
## 快速部署
|
## 快速部署
|
||||||
|
|
||||||
### 🐳 方式一:Docker 部署(推荐)
|
### 🐳 方式一:Docker 部署(推荐)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/engigu/baihu-panel/cmd/reposync"
|
"github.com/engigu/baihu-panel/cmd/reposync"
|
||||||
"github.com/engigu/baihu-panel/cmd/resetpwd"
|
"github.com/engigu/baihu-panel/cmd/resetpwd"
|
||||||
"github.com/engigu/baihu-panel/cmd/restore"
|
"github.com/engigu/baihu-panel/cmd/restore"
|
||||||
|
// "github.com/engigu/baihu-panel/cmd/migrate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommandHandler 定义命令执行函数
|
// CommandHandler 定义命令执行函数
|
||||||
@@ -14,4 +15,5 @@ var Handlers = map[string]CommandHandler{
|
|||||||
"reposync": reposync.Run,
|
"reposync": reposync.Run,
|
||||||
"resetpwd": resetpwd.Run,
|
"resetpwd": resetpwd.Run,
|
||||||
"restore": restore.Run,
|
"restore": restore.Run,
|
||||||
|
// "migrate": migrate.Run,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package migrate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/engigu/baihu-panel/internal/bootstrap"
|
||||||
|
"github.com/engigu/baihu-panel/internal/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run(args []string) {
|
||||||
|
fmt.Println("Starting Migration V3...")
|
||||||
|
// 初始化基础环境(配置和数据库,但不运行常规 Migrate,因为我们想手动控)
|
||||||
|
// 不过 bootstrap.New() 会调用 Migrate().
|
||||||
|
// 我们可以调用 InitBasic()
|
||||||
|
app := bootstrap.InitBasic()
|
||||||
|
if app == nil {
|
||||||
|
fmt.Println("Failed to initialize app")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 此时数据库已经连接,Migrate() 已经运行过了(因为 bootstrap.InitBasic 调用了 app.initDatabase)
|
||||||
|
// 由于我们在 Migrate() 中集成了 RunMigrationV3(),所以其实已经跑过了。
|
||||||
|
// 如果用户想重复跑,或者单独跑:
|
||||||
|
err := services.RunMigrationV3()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Migration failed: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Migration V3 completed successfully.")
|
||||||
|
}
|
||||||
@@ -64,6 +64,7 @@ require (
|
|||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||||
github.com/spf13/afero v1.14.0 // indirect
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
github.com/spf13/cast v1.9.2 // indirect
|
github.com/spf13/cast v1.9.2 // indirect
|
||||||
|
|||||||
@@ -244,6 +244,8 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
|||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||||
|
|||||||
@@ -84,6 +84,11 @@ func (a *App) initDatabase() {
|
|||||||
logger.Fatalf("Failed to init database: %v", err)
|
logger.Fatalf("Failed to init database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 执行 V3 迁移(ID 变更迁移)
|
||||||
|
if err := services.RunMigrationV3(); err != nil {
|
||||||
|
logger.Fatalf("Failed to run V3 migration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := database.Migrate(); err != nil {
|
if err := database.Migrate(); err != nil {
|
||||||
logger.Fatalf("Failed to migrate database: %v", err)
|
logger.Fatalf("Failed to migrate database: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ func (c *AgentController) List(ctx *gin.Context) {
|
|||||||
|
|
||||||
// Update 更新 Agent
|
// Update 更新 Agent
|
||||||
func (c *AgentController) Update(ctx *gin.Context) {
|
func (c *AgentController) Update(ctx *gin.Context) {
|
||||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
id := ctx.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(ctx, "无效的 ID")
|
utils.BadRequest(ctx, "无效的 ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -67,14 +67,14 @@ func (c *AgentController) Update(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取旧状态
|
// 获取旧状态
|
||||||
oldAgent := c.agentService.GetByID(uint(id))
|
oldAgent := c.agentService.GetByID(id)
|
||||||
if oldAgent == nil {
|
if oldAgent == nil {
|
||||||
utils.NotFound(ctx, "Agent 不存在")
|
utils.NotFound(ctx, "Agent 不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
wasEnabled := oldAgent.Enabled
|
wasEnabled := oldAgent.Enabled
|
||||||
|
|
||||||
if err := c.agentService.Update(uint(id), req.Name, req.Description, req.Enabled); err != nil {
|
if err := c.agentService.Update(id, req.Name, req.Description, req.Enabled); err != nil {
|
||||||
utils.ServerError(ctx, err.Error())
|
utils.ServerError(ctx, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -83,14 +83,14 @@ func (c *AgentController) Update(ctx *gin.Context) {
|
|||||||
if wasEnabled != req.Enabled {
|
if wasEnabled != req.Enabled {
|
||||||
if req.Enabled {
|
if req.Enabled {
|
||||||
// 启用:发送任务列表
|
// 启用:发送任务列表
|
||||||
c.wsManager.SendToAgent(uint(id), services.WSTypeEnabled, map[string]interface{}{
|
c.wsManager.SendToAgent(id, services.WSTypeEnabled, map[string]interface{}{
|
||||||
"message": "Agent 已启用",
|
"message": "Agent 已启用",
|
||||||
})
|
})
|
||||||
// 发送任务列表
|
// 发送任务列表
|
||||||
c.wsManager.BroadcastTasks(uint(id))
|
c.wsManager.BroadcastTasks(id)
|
||||||
} else {
|
} else {
|
||||||
// 禁用:发送禁用消息,Agent 收到后清空任务
|
// 禁用:发送禁用消息,Agent 收到后清空任务
|
||||||
c.wsManager.SendToAgent(uint(id), services.WSTypeDisabled, map[string]interface{}{
|
c.wsManager.SendToAgent(id, services.WSTypeDisabled, map[string]interface{}{
|
||||||
"message": "Agent 已禁用",
|
"message": "Agent 已禁用",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -101,13 +101,13 @@ func (c *AgentController) Update(ctx *gin.Context) {
|
|||||||
|
|
||||||
// Delete 删除 Agent
|
// Delete 删除 Agent
|
||||||
func (c *AgentController) Delete(ctx *gin.Context) {
|
func (c *AgentController) Delete(ctx *gin.Context) {
|
||||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
id := ctx.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(ctx, "无效的 ID")
|
utils.BadRequest(ctx, "无效的 ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.agentService.Delete(uint(id)); err != nil {
|
if err := c.agentService.Delete(id); err != nil {
|
||||||
utils.BadRequest(ctx, err.Error())
|
utils.BadRequest(ctx, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -117,13 +117,13 @@ func (c *AgentController) Delete(ctx *gin.Context) {
|
|||||||
|
|
||||||
// RegenerateToken 重新生成 Token
|
// RegenerateToken 重新生成 Token
|
||||||
func (c *AgentController) RegenerateToken(ctx *gin.Context) {
|
func (c *AgentController) RegenerateToken(ctx *gin.Context) {
|
||||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
id := ctx.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(ctx, "无效的 ID")
|
utils.BadRequest(ctx, "无效的 ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := c.agentService.RegenerateToken(uint(id))
|
token, err := c.agentService.RegenerateToken(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ServerError(ctx, err.Error())
|
utils.ServerError(ctx, err.Error())
|
||||||
return
|
return
|
||||||
@@ -324,13 +324,13 @@ func (c *AgentController) GetVersion(ctx *gin.Context) {
|
|||||||
|
|
||||||
// ForceUpdate 强制更新指定 Agent
|
// ForceUpdate 强制更新指定 Agent
|
||||||
func (c *AgentController) ForceUpdate(ctx *gin.Context) {
|
func (c *AgentController) ForceUpdate(ctx *gin.Context) {
|
||||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
id := ctx.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(ctx, "无效的 ID")
|
utils.BadRequest(ctx, "无效的 ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.agentService.SetForceUpdate(uint(id)); err != nil {
|
if err := c.agentService.SetForceUpdate(id); err != nil {
|
||||||
utils.ServerError(ctx, err.Error())
|
utils.ServerError(ctx, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -390,20 +390,20 @@ func (c *AgentController) WSConnect(ctx *gin.Context) {
|
|||||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
ctx.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Infof("[AgentWS] 注册成功: Agent #%d, isNew=%v", agent.ID, isNewAgent)
|
logger.Infof("[AgentWS] 注册成功: Agent #%s, isNew=%v", agent.ID, isNewAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !agent.Enabled {
|
if !agent.Enabled {
|
||||||
c.wsManager.RecordConnectFail(ip)
|
c.wsManager.RecordConnectFail(ip)
|
||||||
logger.Warnf("[AgentWS] Agent #%d 已禁用, IP=%s", agent.ID, ip)
|
logger.Warnf("[AgentWS] Agent #%s 已禁用, IP=%s", agent.ID, ip)
|
||||||
ctx.JSON(http.StatusForbidden, gin.H{"error": "Agent 已禁用"})
|
ctx.JSON(http.StatusForbidden, gin.H{"error": "Agent 已禁用"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("[AgentWS] 准备升级连接: Agent #%d, IP=%s", agent.ID, ip)
|
logger.Infof("[AgentWS] 准备升级连接: Agent #%s, IP=%s", agent.ID, ip)
|
||||||
conn, err := agentUpgrader.Upgrade(ctx.Writer, ctx.Request, nil)
|
conn, err := agentUpgrader.Upgrade(ctx.Writer, ctx.Request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("[AgentWS] 升级连接失败: %v, Agent #%d, IP=%s", err, agent.ID, ip)
|
logger.Errorf("[AgentWS] 升级连接失败: %v, Agent #%s, IP=%s", err, agent.ID, ip)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +434,7 @@ func (c *AgentController) WSConnect(ctx *gin.Context) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.Infof("[AgentWS] Agent #%d 连接成功 (配置: workers=%d, queue=%d, rate=%d)",
|
logger.Infof("[AgentWS] Agent #%s 连接成功 (配置: workers=%d, queue=%d, rate=%d)",
|
||||||
agent.ID, workerCount, queueSize, rateInterval)
|
agent.ID, workerCount, queueSize, rateInterval)
|
||||||
|
|
||||||
// 启动读写协程
|
// 启动读写协程
|
||||||
@@ -449,9 +449,9 @@ func (c *AgentController) WSConnect(ctx *gin.Context) {
|
|||||||
func (c *AgentController) wsReadPump(ac *services.AgentConnection, agent *models.Agent) {
|
func (c *AgentController) wsReadPump(ac *services.AgentConnection, agent *models.Agent) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
logger.Errorf("[AgentWS] Agent #%d wsReadPump panic: %v", agent.ID, r)
|
logger.Errorf("[AgentWS] Agent #%s wsReadPump panic: %v", agent.ID, r)
|
||||||
}
|
}
|
||||||
logger.Infof("[AgentWS] Agent #%d wsReadPump 退出", agent.ID)
|
logger.Infof("[AgentWS] Agent #%s wsReadPump 退出", agent.ID)
|
||||||
c.wsManager.Unregister(agent.ID, ac)
|
c.wsManager.Unregister(agent.ID, ac)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -471,7 +471,7 @@ func (c *AgentController) wsReadPump(ac *services.AgentConnection, agent *models
|
|||||||
for {
|
for {
|
||||||
_, message, err := ac.ReadMessage()
|
_, message, err := ac.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warnf("[AgentWS] Agent #%d 读取错误: %v", agent.ID, err)
|
logger.Warnf("[AgentWS] Agent #%s 读取错误: %v", agent.ID, err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,9 +488,9 @@ func (c *AgentController) wsReadPump(ac *services.AgentConnection, agent *models
|
|||||||
func (c *AgentController) wsWritePump(ac *services.AgentConnection) {
|
func (c *AgentController) wsWritePump(ac *services.AgentConnection) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
logger.Errorf("[AgentWS] Agent #%d wsWritePump panic: %v", ac.AgentID, r)
|
logger.Errorf("[AgentWS] Agent #%s wsWritePump panic: %v", ac.AgentID, r)
|
||||||
}
|
}
|
||||||
logger.Infof("[AgentWS] Agent #%d wsWritePump 退出", ac.AgentID)
|
logger.Infof("[AgentWS] Agent #%s wsWritePump 退出", ac.AgentID)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
@@ -500,15 +500,15 @@ func (c *AgentController) wsWritePump(ac *services.AgentConnection) {
|
|||||||
select {
|
select {
|
||||||
case message, ok := <-ac.Send:
|
case message, ok := <-ac.Send:
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Warnf("[AgentWS] Agent #%d Send channel 已关闭", ac.AgentID)
|
logger.Warnf("[AgentWS] Agent #%s Send channel 已关闭", ac.AgentID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ac.IsClosed() {
|
if ac.IsClosed() {
|
||||||
logger.Warnf("[AgentWS] Agent #%d 连接已关闭(write)", ac.AgentID)
|
logger.Warnf("[AgentWS] Agent #%s 连接已关闭(write)", ac.AgentID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := ac.WriteMessage(message); err != nil {
|
if err := ac.WriteMessage(message); err != nil {
|
||||||
logger.Warnf("[AgentWS] Agent #%d 写入消息失败: %v", ac.AgentID, err)
|
logger.Warnf("[AgentWS] Agent #%s 写入消息失败: %v", ac.AgentID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
@@ -516,7 +516,7 @@ func (c *AgentController) wsWritePump(ac *services.AgentConnection) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := ac.WritePing(); err != nil {
|
if err := ac.WritePing(); err != nil {
|
||||||
logger.Warnf("[AgentWS] Agent #%d 发送 Ping 失败: %v", ac.AgentID, err)
|
logger.Warnf("[AgentWS] Agent #%s 发送 Ping 失败: %v", ac.AgentID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -546,15 +546,15 @@ func (c *AgentController) handleWSMessage(ac *services.AgentConnection, agent *m
|
|||||||
// handleTaskHeartbeat 处理任务心跳
|
// handleTaskHeartbeat 处理任务心跳
|
||||||
func (c *AgentController) handleTaskHeartbeat(agent *models.Agent, data json.RawMessage) {
|
func (c *AgentController) handleTaskHeartbeat(agent *models.Agent, data json.RawMessage) {
|
||||||
var req struct {
|
var req struct {
|
||||||
LogID uint `json:"log_id"`
|
LogID string `json:"log_id"`
|
||||||
Duration int64 `json:"duration"`
|
Duration int64 `json:"duration"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(data, &req); err != nil {
|
if err := json.Unmarshal(data, &req); err != nil {
|
||||||
logger.Errorf("[AgentWS] 解析心跳消息失败: %v", err)
|
logger.Errorf("[AgentWS] 解析心跳消息失败: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.LogID > 0 {
|
if req.LogID != "" {
|
||||||
logger.Infof("[AgentWS] 收到任务心跳: LogID=%d, Duration=%dms", req.LogID, req.Duration)
|
logger.Infof("[AgentWS] 收到任务心跳: LogID=%s, Duration=%dms", req.LogID, req.Duration)
|
||||||
c.agentService.UpdateTaskDuration(req.LogID, req.Duration)
|
c.agentService.UpdateTaskDuration(req.LogID, req.Duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -565,7 +565,7 @@ func (c *AgentController) handleFetchTasks(agent *models.Agent) {
|
|||||||
c.wsManager.SendToAgent(agent.ID, services.WSTypeTasks, map[string]interface{}{
|
c.wsManager.SendToAgent(agent.ID, services.WSTypeTasks, map[string]interface{}{
|
||||||
"tasks": tasks,
|
"tasks": tasks,
|
||||||
})
|
})
|
||||||
logger.Infof("[AgentWS] Agent #%d 请求任务列表,返回 %d 个任务", agent.ID, len(tasks))
|
logger.Infof("[AgentWS] Agent #%s 请求任务列表,返回 %d 个任务", agent.ID, len(tasks))
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleHeartbeat 处理心跳
|
// handleHeartbeat 处理心跳
|
||||||
@@ -619,7 +619,7 @@ func (c *AgentController) handleTaskResult(agent *models.Agent, data json.RawMes
|
|||||||
// handleTaskLog 处理 Agent 发送的实时日志
|
// handleTaskLog 处理 Agent 发送的实时日志
|
||||||
func (c *AgentController) handleTaskLog(agent *models.Agent, data json.RawMessage) {
|
func (c *AgentController) handleTaskLog(agent *models.Agent, data json.RawMessage) {
|
||||||
var logMsg struct {
|
var logMsg struct {
|
||||||
LogID uint `json:"log_id"`
|
LogID string `json:"log_id"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(data, &logMsg); err != nil {
|
if err := json.Unmarshal(data, &logMsg); err != nil {
|
||||||
@@ -631,12 +631,12 @@ func (c *AgentController) handleTaskLog(agent *models.Agent, data json.RawMessag
|
|||||||
if tl != nil {
|
if tl != nil {
|
||||||
tl.Write([]byte(logMsg.Content))
|
tl.Write([]byte(logMsg.Content))
|
||||||
} else {
|
} else {
|
||||||
logger.Warnf("[AgentWS] 收到任务日志但未找到活跃 TinyLog: LogID=%d, ContentSize=%d", logMsg.LogID, len(logMsg.Content))
|
logger.Warnf("[AgentWS] 收到任务日志 but could not find active TinyLog: LogID=%s, ContentSize=%d", logMsg.LogID, len(logMsg.Content))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyTaskUpdate 通知 Agent 任务更新
|
// NotifyTaskUpdate 通知 Agent 任务更新
|
||||||
func (c *AgentController) NotifyTaskUpdate(agentID uint) {
|
func (c *AgentController) NotifyTaskUpdate(agentID string) {
|
||||||
c.wsManager.BroadcastTasks(agentID)
|
c.wsManager.BroadcastTasks(agentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,13 +682,13 @@ func (c *AgentController) CreateToken(ctx *gin.Context) {
|
|||||||
|
|
||||||
// DeleteToken 删除令牌
|
// DeleteToken 删除令牌
|
||||||
func (c *AgentController) DeleteToken(ctx *gin.Context) {
|
func (c *AgentController) DeleteToken(ctx *gin.Context) {
|
||||||
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
|
id := ctx.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(ctx, "无效的 ID")
|
utils.BadRequest(ctx, "无效的 ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.agentService.DeleteToken(uint(id)); err != nil {
|
if err := c.agentService.DeleteToken(id); err != nil {
|
||||||
utils.ServerError(ctx, err.Error())
|
utils.ServerError(ctx, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ func (dc *DashboardController) GetSendStats(c *gin.Context) {
|
|||||||
|
|
||||||
// TaskStats 任务执行统计
|
// TaskStats 任务执行统计
|
||||||
type TaskStats struct {
|
type TaskStats struct {
|
||||||
TaskID uint `json:"task_id"`
|
TaskID string `json:"task_id"`
|
||||||
TaskName string `json:"task_name"`
|
TaskName string `json:"task_name"`
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ func (dc *DashboardController) GetTaskStats(c *gin.Context) {
|
|||||||
|
|
||||||
// 按 task_id 聚合统计
|
// 按 task_id 聚合统计
|
||||||
var results []struct {
|
var results []struct {
|
||||||
TaskID uint
|
TaskID string
|
||||||
Total int
|
Total int
|
||||||
}
|
}
|
||||||
database.DB.Model(&models.SendStats{}).
|
database.DB.Model(&models.SendStats{}).
|
||||||
@@ -170,7 +170,7 @@ func (dc *DashboardController) GetTaskStats(c *gin.Context) {
|
|||||||
Find(&results)
|
Find(&results)
|
||||||
|
|
||||||
// 获取任务名称
|
// 获取任务名称
|
||||||
taskIDs := make([]uint, 0, len(results))
|
taskIDs := make([]string, 0, len(results))
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
taskIDs = append(taskIDs, r.TaskID)
|
taskIDs = append(taskIDs, r.TaskID)
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ func (dc *DashboardController) GetTaskStats(c *gin.Context) {
|
|||||||
if len(taskIDs) > 0 {
|
if len(taskIDs) > 0 {
|
||||||
database.DB.Where("id IN ?", taskIDs).Find(&tasks)
|
database.DB.Where("id IN ?", taskIDs).Find(&tasks)
|
||||||
}
|
}
|
||||||
taskNameMap := make(map[uint]string)
|
taskNameMap := make(map[string]string)
|
||||||
for _, t := range tasks {
|
for _, t := range tasks {
|
||||||
taskNameMap[t.ID] = t.Name
|
taskNameMap[t.ID] = t.Name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/engigu/baihu-panel/internal/models"
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
@@ -71,8 +70,8 @@ func (c *DependencyController) Create(ctx *gin.Context) {
|
|||||||
|
|
||||||
// Delete 删除依赖
|
// Delete 删除依赖
|
||||||
func (c *DependencyController) Delete(ctx *gin.Context) {
|
func (c *DependencyController) Delete(ctx *gin.Context) {
|
||||||
id, err := strconv.Atoi(ctx.Param("id"))
|
id := ctx.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(ctx, "无效的 ID")
|
utils.BadRequest(ctx, "无效的 ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -186,8 +185,8 @@ func (c *DependencyController) GetReinstallAllCommand(ctx *gin.Context) {
|
|||||||
|
|
||||||
// Uninstall 卸载依赖
|
// Uninstall 卸载依赖
|
||||||
func (c *DependencyController) Uninstall(ctx *gin.Context) {
|
func (c *DependencyController) Uninstall(ctx *gin.Context) {
|
||||||
id, err := strconv.Atoi(ctx.Param("id"))
|
id := ctx.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(ctx, "无效的 ID")
|
utils.BadRequest(ctx, "无效的 ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -220,8 +219,8 @@ func (c *DependencyController) Uninstall(ctx *gin.Context) {
|
|||||||
|
|
||||||
// Reinstall 重新安装依赖
|
// Reinstall 重新安装依赖
|
||||||
func (c *DependencyController) Reinstall(ctx *gin.Context) {
|
func (c *DependencyController) Reinstall(ctx *gin.Context) {
|
||||||
id, err := strconv.Atoi(ctx.Param("id"))
|
id := ctx.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(ctx, "无效的 ID")
|
utils.BadRequest(ctx, "无效的 ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/engigu/baihu-panel/internal/models/vo"
|
"github.com/engigu/baihu-panel/internal/models/vo"
|
||||||
"github.com/engigu/baihu-panel/internal/services"
|
"github.com/engigu/baihu-panel/internal/services"
|
||||||
@@ -19,7 +18,7 @@ func NewEnvController(envService *services.EnvService) *EnvController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ec *EnvController) CreateEnvVar(c *gin.Context) {
|
func (ec *EnvController) CreateEnvVar(c *gin.Context) {
|
||||||
userID := 1
|
userID := c.GetString("userID")
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
@@ -43,7 +42,7 @@ func (ec *EnvController) CreateEnvVar(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ec *EnvController) GetEnvVars(c *gin.Context) {
|
func (ec *EnvController) GetEnvVars(c *gin.Context) {
|
||||||
userID := 1
|
userID := c.GetString("userID")
|
||||||
p := utils.ParsePagination(c)
|
p := utils.ParsePagination(c)
|
||||||
name := c.DefaultQuery("name", "")
|
name := c.DefaultQuery("name", "")
|
||||||
envVars, total := ec.envService.GetEnvVarsWithPagination(userID, name, p.Page, p.PageSize)
|
envVars, total := ec.envService.GetEnvVarsWithPagination(userID, name, p.Page, p.PageSize)
|
||||||
@@ -51,14 +50,14 @@ func (ec *EnvController) GetEnvVars(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ec *EnvController) GetAllEnvVars(c *gin.Context) {
|
func (ec *EnvController) GetAllEnvVars(c *gin.Context) {
|
||||||
userID := 1
|
userID := c.GetString("userID")
|
||||||
envVars := ec.envService.GetEnvVarsByUserID(userID)
|
envVars := ec.envService.GetEnvVarsByUserID(userID)
|
||||||
utils.Success(c, vo.ToEnvVOListFromModels(envVars))
|
utils.Success(c, vo.ToEnvVOListFromModels(envVars))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *EnvController) GetEnvVar(c *gin.Context) {
|
func (ec *EnvController) GetEnvVar(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id := c.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(c, "无效的环境变量ID")
|
utils.BadRequest(c, "无效的环境变量ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -73,8 +72,8 @@ func (ec *EnvController) GetEnvVar(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ec *EnvController) UpdateEnvVar(c *gin.Context) {
|
func (ec *EnvController) UpdateEnvVar(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id := c.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(c, "无效的环境变量ID")
|
utils.BadRequest(c, "无效的环境变量ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -112,8 +111,8 @@ func (ec *EnvController) UpdateEnvVar(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ec *EnvController) DeleteEnvVar(c *gin.Context) {
|
func (ec *EnvController) DeleteEnvVar(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id := c.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(c, "无效的环境变量ID")
|
utils.BadRequest(c, "无效的环境变量ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ func NewExecutorController(executorService *tasks.ExecutorService) *ExecutorCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ec *ExecutorController) ExecuteTask(c *gin.Context) {
|
func (ec *ExecutorController) ExecuteTask(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id := c.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(c, "无效的任务ID")
|
utils.BadRequest(c, "无效的任务ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/engigu/baihu-panel/internal/database"
|
"github.com/engigu/baihu-panel/internal/database"
|
||||||
"github.com/engigu/baihu-panel/internal/models"
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
@@ -19,7 +18,7 @@ func NewLogController() *LogController {
|
|||||||
|
|
||||||
func (lc *LogController) GetLogs(c *gin.Context) {
|
func (lc *LogController) GetLogs(c *gin.Context) {
|
||||||
p := utils.ParsePagination(c)
|
p := utils.ParsePagination(c)
|
||||||
taskID, _ := strconv.Atoi(c.DefaultQuery("task_id", "0"))
|
taskID := c.DefaultQuery("task_id", "")
|
||||||
taskName := c.DefaultQuery("task_name", "")
|
taskName := c.DefaultQuery("task_name", "")
|
||||||
status := c.DefaultQuery("status", "")
|
status := c.DefaultQuery("status", "")
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ func (lc *LogController) GetLogs(c *gin.Context) {
|
|||||||
var total int64
|
var total int64
|
||||||
|
|
||||||
query := database.DB.Model(&models.TaskLog{})
|
query := database.DB.Model(&models.TaskLog{})
|
||||||
if taskID > 0 {
|
if taskID != "" {
|
||||||
query = query.Where("task_id = ?", taskID)
|
query = query.Where("task_id = ?", taskID)
|
||||||
}
|
}
|
||||||
if status != "" {
|
if status != "" {
|
||||||
@@ -36,7 +35,7 @@ func (lc *LogController) GetLogs(c *gin.Context) {
|
|||||||
|
|
||||||
// 按任务名称过滤
|
// 按任务名称过滤
|
||||||
if taskName != "" {
|
if taskName != "" {
|
||||||
var taskIDs []uint
|
var taskIDs []string
|
||||||
database.DB.Model(&models.Task{}).Where("name LIKE ?", "%"+taskName+"%").Pluck("id", &taskIDs)
|
database.DB.Model(&models.Task{}).Where("name LIKE ?", "%"+taskName+"%").Pluck("id", &taskIDs)
|
||||||
if len(taskIDs) > 0 {
|
if len(taskIDs) > 0 {
|
||||||
query = query.Where("task_id IN ?", taskIDs)
|
query = query.Where("task_id IN ?", taskIDs)
|
||||||
@@ -49,14 +48,14 @@ func (lc *LogController) GetLogs(c *gin.Context) {
|
|||||||
query.Count(&total)
|
query.Count(&total)
|
||||||
query.Order("id DESC").Offset(p.Offset()).Limit(p.PageSize).Find(&logs)
|
query.Order("id DESC").Offset(p.Offset()).Limit(p.PageSize).Find(&logs)
|
||||||
|
|
||||||
taskIDList := make([]uint, 0)
|
taskIDList := make([]string, 0)
|
||||||
for _, log := range logs {
|
for _, log := range logs {
|
||||||
taskIDList = append(taskIDList, log.TaskID)
|
taskIDList = append(taskIDList, log.TaskID)
|
||||||
}
|
}
|
||||||
|
|
||||||
var tasks []models.Task
|
var tasks []models.Task
|
||||||
database.DB.Where("id IN ?", taskIDList).Find(&tasks)
|
database.DB.Where("id IN ?", taskIDList).Find(&tasks)
|
||||||
taskMap := make(map[uint]models.Task)
|
taskMap := make(map[string]models.Task)
|
||||||
for _, t := range tasks {
|
for _, t := range tasks {
|
||||||
taskMap[t.ID] = t
|
taskMap[t.ID] = t
|
||||||
}
|
}
|
||||||
@@ -87,14 +86,14 @@ func (lc *LogController) GetLogs(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (lc *LogController) GetLogDetail(c *gin.Context) {
|
func (lc *LogController) GetLogDetail(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id := c.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(c, "无效的日志ID")
|
utils.BadRequest(c, "无效的日志ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var log models.TaskLog
|
var log models.TaskLog
|
||||||
if err := database.DB.First(&log, id).Error; err != nil {
|
if err := database.DB.Where("id = ?", id).First(&log).Error; err != nil {
|
||||||
utils.NotFound(c, "日志不存在")
|
utils.NotFound(c, "日志不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -104,7 +103,7 @@ func (lc *LogController) GetLogDetail(c *gin.Context) {
|
|||||||
|
|
||||||
func (lc *LogController) ClearLogs(c *gin.Context) {
|
func (lc *LogController) ClearLogs(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
TaskID *int `json:"task_id"`
|
TaskID *string `json:"task_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -113,7 +112,7 @@ func (lc *LogController) ClearLogs(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query := database.DB.Model(&models.TaskLog{})
|
query := database.DB.Model(&models.TaskLog{})
|
||||||
if req.TaskID != nil && *req.TaskID > 0 {
|
if req.TaskID != nil && *req.TaskID != "" {
|
||||||
query = query.Where("task_id = ?", *req.TaskID)
|
query = query.Where("task_id = ?", *req.TaskID)
|
||||||
} else {
|
} else {
|
||||||
query = query.Where("1 = 1") // Allow delete all without GORM safety block
|
query = query.Where("1 = 1") // Allow delete all without GORM safety block
|
||||||
@@ -128,13 +127,13 @@ func (lc *LogController) ClearLogs(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (lc *LogController) DeleteLog(c *gin.Context) {
|
func (lc *LogController) DeleteLog(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id := c.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(c, "无效的日志ID")
|
utils.BadRequest(c, "无效的日志ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := database.DB.Delete(&models.TaskLog{}, id).Error; err != nil {
|
if err := database.DB.Where("id = ?", id).Delete(&models.TaskLog{}).Error; err != nil {
|
||||||
utils.ServerError(c, "删除日志失败")
|
utils.ServerError(c, "删除日志失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/engigu/baihu-panel/internal/database"
|
"github.com/engigu/baihu-panel/internal/database"
|
||||||
"github.com/engigu/baihu-panel/internal/models"
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
@@ -25,10 +24,7 @@ func (lc *LogWSController) StreamLog(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logID, err := strconv.ParseUint(logIDStr, 10, 32)
|
logID := logIDStr
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,7 +34,7 @@ func (lc *LogWSController) StreamLog(c *gin.Context) {
|
|||||||
|
|
||||||
// 1. 检查数据库中是否已结束
|
// 1. 检查数据库中是否已结束
|
||||||
var taskLog models.TaskLog
|
var taskLog models.TaskLog
|
||||||
if err := database.DB.First(&taskLog, uint(logID)).Error; err == nil {
|
if err := database.DB.Where("id = ?", logID).First(&taskLog).Error; err == nil {
|
||||||
if taskLog.Status != "running" {
|
if taskLog.Status != "running" {
|
||||||
// 已结束,读取库内日志
|
// 已结束,读取库内日志
|
||||||
content, err := utils.DecompressFromBase64(taskLog.Output)
|
content, err := utils.DecompressFromBase64(taskLog.Output)
|
||||||
@@ -52,14 +48,14 @@ func (lc *LogWSController) StreamLog(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 未结束或未找到记录,尝试从 TinyLogManager 获取
|
// 2. 未结束或未找到记录,尝试从 TinyLogManager 获取
|
||||||
tl := tasks.GetActiveLog(uint(logID))
|
tl := tasks.GetActiveLog(logID)
|
||||||
if tl == nil {
|
if tl == nil {
|
||||||
conn.WriteMessage(websocket.TextMessage, []byte("未找到正在运行的任务日志"))
|
conn.WriteMessage(websocket.TextMessage, []byte("未找到正在运行的任务日志"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送系统提示
|
// 发送系统提示
|
||||||
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("[System] 连接成功,正在监听日志... (LogID: %d)\n", logID)))
|
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("[System] 连接成功,正在监听日志... (LogID: %s)\n", logID)))
|
||||||
|
|
||||||
// 发送最后 100 行
|
// 发送最后 100 行
|
||||||
lastLines, err := tl.ReadLastLines(100)
|
lastLines, err := tl.ReadLastLines(100)
|
||||||
@@ -78,7 +74,7 @@ func (lc *LogWSController) StreamLog(c *gin.Context) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
// 任务结束,尝试刷新最后一次库内完整内容
|
// 任务结束,尝试刷新最后一次库内完整内容
|
||||||
var finalLog models.TaskLog
|
var finalLog models.TaskLog
|
||||||
if err := database.DB.First(&finalLog, uint(logID)).Error; err == nil {
|
if err := database.DB.Where("id = ?", logID).First(&finalLog).Error; err == nil {
|
||||||
content, _ := utils.DecompressFromBase64(finalLog.Output)
|
content, _ := utils.DecompressFromBase64(finalLog.Output)
|
||||||
if content != "" {
|
if content != "" {
|
||||||
conn.WriteMessage(websocket.TextMessage, []byte("\n--- 任务已结束 ---\n"))
|
conn.WriteMessage(websocket.TextMessage, []byte("\n--- 任务已结束 ---\n"))
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/engigu/baihu-panel/internal/models/vo"
|
"github.com/engigu/baihu-panel/internal/models/vo"
|
||||||
"github.com/engigu/baihu-panel/internal/services"
|
"github.com/engigu/baihu-panel/internal/services"
|
||||||
@@ -19,7 +18,7 @@ func NewScriptController(scriptService *services.ScriptService) *ScriptControlle
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sc *ScriptController) CreateScript(c *gin.Context) {
|
func (sc *ScriptController) CreateScript(c *gin.Context) {
|
||||||
userID := 1
|
userID := c.GetString("userID")
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
@@ -36,7 +35,7 @@ func (sc *ScriptController) CreateScript(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sc *ScriptController) GetScripts(c *gin.Context) {
|
func (sc *ScriptController) GetScripts(c *gin.Context) {
|
||||||
userID := 1
|
userID := c.GetString("userID")
|
||||||
scripts := sc.scriptService.GetScriptsByUserID(userID)
|
scripts := sc.scriptService.GetScriptsByUserID(userID)
|
||||||
vos := vo.ToScriptVOListFromModels(scripts)
|
vos := vo.ToScriptVOListFromModels(scripts)
|
||||||
for i := range vos {
|
for i := range vos {
|
||||||
@@ -46,8 +45,8 @@ func (sc *ScriptController) GetScripts(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sc *ScriptController) GetScript(c *gin.Context) {
|
func (sc *ScriptController) GetScript(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id := c.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(c, "无效的脚本ID")
|
utils.BadRequest(c, "无效的脚本ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -62,8 +61,8 @@ func (sc *ScriptController) GetScript(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sc *ScriptController) UpdateScript(c *gin.Context) {
|
func (sc *ScriptController) UpdateScript(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id := c.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(c, "无效的脚本ID")
|
utils.BadRequest(c, "无效的脚本ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -88,8 +87,8 @@ func (sc *ScriptController) UpdateScript(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sc *ScriptController) DeleteScript(c *gin.Context) {
|
func (sc *ScriptController) DeleteScript(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id := c.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(c, "无效的脚本ID")
|
utils.BadRequest(c, "无效的脚本ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ func (sc *SettingsController) ChangePassword(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 暂时使用固定用户名 admin
|
userID := c.GetString("userID")
|
||||||
user := sc.userService.GetUserByUsername("admin")
|
var user *models.User
|
||||||
if user == nil {
|
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||||
utils.NotFound(c, "用户不存在")
|
utils.NotFound(c, "用户不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/engigu/baihu-panel/internal/constant"
|
"github.com/engigu/baihu-panel/internal/constant"
|
||||||
"github.com/engigu/baihu-panel/internal/models/vo"
|
"github.com/engigu/baihu-panel/internal/models/vo"
|
||||||
@@ -63,7 +62,7 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
|
|||||||
CleanConfig string `json:"clean_config"`
|
CleanConfig string `json:"clean_config"`
|
||||||
Envs string `json:"envs"`
|
Envs string `json:"envs"`
|
||||||
Languages []map[string]string `json:"languages"`
|
Languages []map[string]string `json:"languages"`
|
||||||
AgentID *uint `json:"agent_id"`
|
AgentID *string `json:"agent_id"`
|
||||||
TriggerType string `json:"trigger_type"`
|
TriggerType string `json:"trigger_type"`
|
||||||
RetryCount int `json:"retry_count"`
|
RetryCount int `json:"retry_count"`
|
||||||
RetryInterval int `json:"retry_interval"`
|
RetryInterval int `json:"retry_interval"`
|
||||||
@@ -90,14 +89,14 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
|
|||||||
|
|
||||||
// 转换为绝对路径(Agent 任务保持原样)
|
// 转换为绝对路径(Agent 任务保持原样)
|
||||||
workDir := req.WorkDir
|
workDir := req.WorkDir
|
||||||
if req.AgentID == nil || *req.AgentID == 0 {
|
if req.AgentID == nil || *req.AgentID == "" {
|
||||||
workDir = resolveWorkDir(req.WorkDir)
|
workDir = resolveWorkDir(req.WorkDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
task := tc.taskService.CreateTask(req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange)
|
task := tc.taskService.CreateTask(req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange)
|
||||||
|
|
||||||
// 如果是 Agent 任务,通知 Agent;否则添加到本地 cron
|
// 如果是 Agent 任务,通知 Agent;否则添加到本地 cron
|
||||||
if task.AgentID != nil && *task.AgentID > 0 {
|
if task.AgentID != nil && *task.AgentID != "" {
|
||||||
tc.agentWSManager.BroadcastTasks(*task.AgentID)
|
tc.agentWSManager.BroadcastTasks(*task.AgentID)
|
||||||
} else {
|
} else {
|
||||||
tc.executorService.AddCronTask(task)
|
tc.executorService.AddCronTask(task)
|
||||||
@@ -114,12 +113,9 @@ func (tc *TaskController) GetTasks(c *gin.Context) {
|
|||||||
tags := c.DefaultQuery("tags", "")
|
tags := c.DefaultQuery("tags", "")
|
||||||
taskType := c.DefaultQuery("type", "")
|
taskType := c.DefaultQuery("type", "")
|
||||||
|
|
||||||
var agentID *uint
|
var agentID *string
|
||||||
if agentIDStr != "" {
|
if agentIDStr != "" {
|
||||||
if id, err := strconv.ParseUint(agentIDStr, 10, 32); err == nil {
|
agentID = &agentIDStr
|
||||||
uid := uint(id)
|
|
||||||
agentID = &uid
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks, total := tc.taskService.GetTasksWithPagination(p.Page, p.PageSize, name, agentID, tags, taskType)
|
tasks, total := tc.taskService.GetTasksWithPagination(p.Page, p.PageSize, name, agentID, tags, taskType)
|
||||||
@@ -127,8 +123,8 @@ func (tc *TaskController) GetTasks(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TaskController) GetTask(c *gin.Context) {
|
func (tc *TaskController) GetTask(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id := c.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(c, "无效的任务ID")
|
utils.BadRequest(c, "无效的任务ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -143,15 +139,15 @@ func (tc *TaskController) GetTask(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TaskController) UpdateTask(c *gin.Context) {
|
func (tc *TaskController) UpdateTask(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id := c.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(c, "无效的任务ID")
|
utils.BadRequest(c, "无效的任务ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取旧任务信息(用于判断 agent 变更)
|
// 获取旧任务信息(用于判断 agent 变更)
|
||||||
oldTask := tc.taskService.GetTaskByID(id)
|
oldTask := tc.taskService.GetTaskByID(id)
|
||||||
var oldAgentID *uint
|
var oldAgentID *string
|
||||||
if oldTask != nil {
|
if oldTask != nil {
|
||||||
oldAgentID = oldTask.AgentID
|
oldAgentID = oldTask.AgentID
|
||||||
}
|
}
|
||||||
@@ -169,7 +165,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
|
|||||||
Envs string `json:"envs"`
|
Envs string `json:"envs"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
Languages []map[string]string `json:"languages"`
|
Languages []map[string]string `json:"languages"`
|
||||||
AgentID *uint `json:"agent_id"`
|
AgentID *string `json:"agent_id"`
|
||||||
TriggerType string `json:"trigger_type"`
|
TriggerType string `json:"trigger_type"`
|
||||||
RetryCount int `json:"retry_count"`
|
RetryCount int `json:"retry_count"`
|
||||||
RetryInterval int `json:"retry_interval"`
|
RetryInterval int `json:"retry_interval"`
|
||||||
@@ -190,7 +186,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
|
|||||||
|
|
||||||
// 转换为绝对路径(Agent 任务保持原样)
|
// 转换为绝对路径(Agent 任务保持原样)
|
||||||
workDir := req.WorkDir
|
workDir := req.WorkDir
|
||||||
if req.AgentID == nil || *req.AgentID == 0 {
|
if req.AgentID == nil || *req.AgentID == "" {
|
||||||
workDir = resolveWorkDir(req.WorkDir)
|
workDir = resolveWorkDir(req.WorkDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,12 +197,12 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理任务调度
|
// 处理任务调度
|
||||||
if task.AgentID != nil && *task.AgentID > 0 {
|
if task.AgentID != nil && *task.AgentID != "" {
|
||||||
// Agent 任务:从本地 cron 移除,通知 Agent
|
// Agent 任务:从本地 cron 移除,通知 Agent
|
||||||
tc.executorService.RemoveCronTask(task.ID)
|
tc.executorService.RemoveCronTask(task.ID)
|
||||||
tc.agentWSManager.BroadcastTasks(*task.AgentID)
|
tc.agentWSManager.BroadcastTasks(*task.AgentID)
|
||||||
// 如果 agent 变更了,也通知旧 agent
|
// 如果 agent 变更了,也通知旧 agent
|
||||||
if oldAgentID != nil && *oldAgentID > 0 && *oldAgentID != *task.AgentID {
|
if oldAgentID != nil && *oldAgentID != "" && *oldAgentID != *task.AgentID {
|
||||||
tc.agentWSManager.BroadcastTasks(*oldAgentID)
|
tc.agentWSManager.BroadcastTasks(*oldAgentID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -217,7 +213,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
|
|||||||
tc.executorService.RemoveCronTask(task.ID)
|
tc.executorService.RemoveCronTask(task.ID)
|
||||||
}
|
}
|
||||||
// 如果之前是 agent 任务,通知旧 agent 移除
|
// 如果之前是 agent 任务,通知旧 agent 移除
|
||||||
if oldAgentID != nil && *oldAgentID > 0 {
|
if oldAgentID != nil && *oldAgentID != "" {
|
||||||
tc.agentWSManager.BroadcastTasks(*oldAgentID)
|
tc.agentWSManager.BroadcastTasks(*oldAgentID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,20 +222,20 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TaskController) DeleteTask(c *gin.Context) {
|
func (tc *TaskController) DeleteTask(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id := c.Param("id")
|
||||||
if err != nil {
|
if id == "" {
|
||||||
utils.BadRequest(c, "无效的任务ID")
|
utils.BadRequest(c, "无效的任务ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取任务信息(用于通知 agent)
|
// 获取任务信息(用于通知 agent)
|
||||||
task := tc.taskService.GetTaskByID(id)
|
task := tc.taskService.GetTaskByID(id)
|
||||||
var agentID *uint
|
var agentID *string
|
||||||
if task != nil {
|
if task != nil {
|
||||||
agentID = task.AgentID
|
agentID = task.AgentID
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.executorService.RemoveCronTask(uint(id))
|
tc.executorService.RemoveCronTask(id)
|
||||||
|
|
||||||
success := tc.taskService.DeleteTask(id)
|
success := tc.taskService.DeleteTask(id)
|
||||||
if !success {
|
if !success {
|
||||||
@@ -248,7 +244,7 @@ func (tc *TaskController) DeleteTask(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果是 agent 任务,通知 agent
|
// 如果是 agent 任务,通知 agent
|
||||||
if agentID != nil && *agentID > 0 {
|
if agentID != nil && *agentID != "" {
|
||||||
tc.agentWSManager.BroadcastTasks(*agentID)
|
tc.agentWSManager.BroadcastTasks(*agentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,13 +252,13 @@ func (tc *TaskController) DeleteTask(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TaskController) StopTask(c *gin.Context) {
|
func (tc *TaskController) StopTask(c *gin.Context) {
|
||||||
logID, err := strconv.ParseUint(c.Param("logID"), 10, 32)
|
logID := c.Param("logID")
|
||||||
if err != nil {
|
if logID == "" {
|
||||||
utils.BadRequest(c, "无效的日志ID")
|
utils.BadRequest(c, "无效的日志ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tc.executorService.StopTaskExecution(uint(logID))
|
err := tc.executorService.StopTaskExecution(logID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.BadRequest(c, err.Error())
|
utils.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -85,11 +85,9 @@ func (tc *TerminalController) HandleWebSocket(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Windows 使用 pipe 模式,Unix 使用 PTY 模式
|
// Windows 使用 pipe 模式,Unix 使用 PTY 模式
|
||||||
userID := 1
|
userID := c.GetString("userID")
|
||||||
if v, exists := c.Get("userID"); exists {
|
if userID == "" {
|
||||||
if id, ok := v.(uint); ok {
|
userID = "1" // 兜底
|
||||||
userID = int(id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
@@ -100,7 +98,7 @@ func (tc *TerminalController) HandleWebSocket(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handlePtyMode 使用 PTY 处理终端(Unix/macOS)
|
// handlePtyMode 使用 PTY 处理终端(Unix/macOS)
|
||||||
func (tc *TerminalController) handlePtyMode(conn *websocket.Conn, userID int) {
|
func (tc *TerminalController) handlePtyMode(conn *websocket.Conn, userID string) {
|
||||||
// 发送 PTY 模式标识
|
// 发送 PTY 模式标识
|
||||||
conn.WriteMessage(websocket.TextMessage, []byte("__PTY_MODE__"))
|
conn.WriteMessage(websocket.TextMessage, []byte("__PTY_MODE__"))
|
||||||
|
|
||||||
@@ -174,7 +172,7 @@ func (tc *TerminalController) handlePtyMode(conn *websocket.Conn, userID int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handlePipeMode 使用 pipe 处理终端(Windows)
|
// handlePipeMode 使用 pipe 处理终端(Windows)
|
||||||
func (tc *TerminalController) handlePipeMode(conn *websocket.Conn, userID int) {
|
func (tc *TerminalController) handlePipeMode(conn *websocket.Conn, userID string) {
|
||||||
// 发送 pipe 模式标识
|
// 发送 pipe 模式标识
|
||||||
conn.WriteMessage(websocket.TextMessage, []byte("__PIPE_MODE__"))
|
conn.WriteMessage(websocket.TextMessage, []byte("__PIPE_MODE__"))
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Migrate() error {
|
func Migrate() error {
|
||||||
// 先执行自定义迁移
|
// 执行自定义迁移
|
||||||
if err := customMigrations(); err != nil {
|
if err := customMigrations(); err != nil {
|
||||||
logger.Warnf("[Database] 自定义迁移警告: %v", err)
|
logger.Warnf("[Database] 自定义迁移警告: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,13 +59,13 @@ type Result struct {
|
|||||||
// Hooks 执行钩子接口
|
// Hooks 执行钩子接口
|
||||||
type Hooks interface {
|
type Hooks interface {
|
||||||
// PreExecute 执行前钩子,返回日志ID和错误
|
// PreExecute 执行前钩子,返回日志ID和错误
|
||||||
PreExecute(ctx context.Context, req Request) (logID uint, err error)
|
PreExecute(ctx context.Context, req Request) (logID string, err error)
|
||||||
|
|
||||||
// PostExecute 执行后钩子,处理日志压缩和记录更新
|
// PostExecute 执行后钩子,处理日志压缩和记录更新
|
||||||
PostExecute(ctx context.Context, logID uint, result *Result) error
|
PostExecute(ctx context.Context, logID string, result *Result) error
|
||||||
|
|
||||||
// OnHeartbeat 执行中心跳钩子,用于更新实时状态
|
// OnHeartbeat 执行中心跳钩子,用于更新实时状态
|
||||||
OnHeartbeat(ctx context.Context, logID uint, duration int64) error
|
OnHeartbeat(ctx context.Context, logID string, duration int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute 执行命令(基础版本,不带钩子)
|
// Execute 执行命令(基础版本,不带钩子)
|
||||||
@@ -85,7 +85,7 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 仍然触发 PreExecute 以便流程完整
|
// 仍然触发 PreExecute 以便流程完整
|
||||||
var logID uint
|
var logID string
|
||||||
if hooks != nil {
|
if hooks != nil {
|
||||||
logID, _ = hooks.PreExecute(ctx, req)
|
logID, _ = hooks.PreExecute(ctx, req)
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. 执行前钩子
|
// 1. 执行前钩子
|
||||||
var logID uint
|
var logID string
|
||||||
if hooks != nil {
|
if hooks != nil {
|
||||||
id, err := hooks.PreExecute(ctx, req)
|
id, err := hooks.PreExecute(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -171,7 +171,7 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer
|
|||||||
)
|
)
|
||||||
f, ptyErr := pty.Start(cmd)
|
f, ptyErr := pty.Start(cmd)
|
||||||
if ptyErr == nil {
|
if ptyErr == nil {
|
||||||
logger.Infof("[Executor] 任务 #%d 启动于 PTY 模式", logID)
|
logger.Infof("[Executor] 任务 #%s 启动于 PTY 模式", logID)
|
||||||
ptyFile = f
|
ptyFile = f
|
||||||
started = true
|
started = true
|
||||||
copyDone = make(chan struct{})
|
copyDone = make(chan struct{})
|
||||||
@@ -182,7 +182,7 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer
|
|||||||
f.Close()
|
f.Close()
|
||||||
}()
|
}()
|
||||||
} else {
|
} else {
|
||||||
logger.Errorf("[Executor] 任务 #%d PTY 启动失败: %v", logID, ptyErr)
|
logger.Errorf("[Executor] 任务 #%s PTY 启动失败: %v", logID, ptyErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer
|
|||||||
if stdout != stderr && stdout != io.Discard {
|
if stdout != stderr && stdout != io.Discard {
|
||||||
logger.Debugf("[Executor] 任务 #%d stdout (%p) 和 stderr (%p) 不同,回退到 Pipe 模式。", logID, stdout, stderr)
|
logger.Debugf("[Executor] 任务 #%d stdout (%p) 和 stderr (%p) 不同,回退到 Pipe 模式。", logID, stdout, stderr)
|
||||||
}
|
}
|
||||||
logger.Infof("[Executor] 任务 #%d 启动于 Pipe 模式", logID)
|
logger.Infof("[Executor] 任务 #%s 启动于 Pipe 模式", logID)
|
||||||
if stdout != nil && stdout == stderr {
|
if stdout != nil && stdout == stderr {
|
||||||
pr, pw, err := os.Pipe()
|
pr, pw, err := os.Pipe()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const (
|
|||||||
// ExecutionRequest 执行请求(标准接口)
|
// ExecutionRequest 执行请求(标准接口)
|
||||||
type ExecutionRequest struct {
|
type ExecutionRequest struct {
|
||||||
TaskID string // 任务 ID
|
TaskID string // 任务 ID
|
||||||
LogID uint // 日志 ID
|
LogID string // 日志 ID
|
||||||
Name string // 任务名称
|
Name string // 任务名称
|
||||||
Type TaskType // 任务类型
|
Type TaskType // 任务类型
|
||||||
Command string // 命令
|
Command string // 命令
|
||||||
@@ -84,7 +84,7 @@ type ExecutionMetadata struct {
|
|||||||
// ExecutionResult 执行结果(标准接口)
|
// ExecutionResult 执行结果(标准接口)
|
||||||
type ExecutionResult struct {
|
type ExecutionResult struct {
|
||||||
TaskID string // 任务 ID
|
TaskID string // 任务 ID
|
||||||
LogID uint // 日志 ID
|
LogID string // 日志 ID
|
||||||
Success bool // 是否成功
|
Success bool // 是否成功
|
||||||
Output string // 输出内容
|
Output string // 输出内容
|
||||||
Error string // 错误信息
|
Error string // 错误信息
|
||||||
@@ -151,15 +151,15 @@ type schedulerHooksAdapter struct {
|
|||||||
req *ExecutionRequest
|
req *ExecutionRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *schedulerHooksAdapter) PreExecute(ctx context.Context, req Request) (uint, error) {
|
func (h *schedulerHooksAdapter) PreExecute(ctx context.Context, req Request) (string, error) {
|
||||||
return h.req.LogID, nil
|
return h.req.LogID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *schedulerHooksAdapter) PostExecute(ctx context.Context, logID uint, result *Result) error {
|
func (h *schedulerHooksAdapter) PostExecute(ctx context.Context, logID string, result *Result) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *schedulerHooksAdapter) OnHeartbeat(ctx context.Context, logID uint, duration int64) error {
|
func (h *schedulerHooksAdapter) OnHeartbeat(ctx context.Context, logID string, duration int64) error {
|
||||||
if h.handler != nil {
|
if h.handler != nil {
|
||||||
h.handler.OnTaskHeartbeat(h.req, duration)
|
h.handler.OnTaskHeartbeat(h.req, duration)
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ type Scheduler struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
logger SchedulerLogger
|
logger SchedulerLogger
|
||||||
runningTasks map[string]context.CancelFunc // 记录运行中的任务,用于停止 (TaskID -> CancelFunc)
|
runningTasks map[string]context.CancelFunc // 记录运行中的任务,用于停止 (TaskID -> CancelFunc)
|
||||||
runningExecs map[uint]context.CancelFunc // 记录运行中的执行,用于停止 (LogID -> CancelFunc)
|
runningExecs map[string]context.CancelFunc // 记录运行中的执行,用于停止 (LogID -> CancelFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScheduler 创建调度器
|
// NewScheduler 创建调度器
|
||||||
@@ -216,7 +216,7 @@ func NewScheduler(config SchedulerConfig, handler SchedulerEventHandler) *Schedu
|
|||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
logger: &DefaultLogger{},
|
logger: &DefaultLogger{},
|
||||||
runningTasks: make(map[string]context.CancelFunc),
|
runningTasks: make(map[string]context.CancelFunc),
|
||||||
runningExecs: make(map[uint]context.CancelFunc),
|
runningExecs: make(map[string]context.CancelFunc),
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
@@ -443,7 +443,7 @@ func (s *Scheduler) executeTask(req *ExecutionRequest) (*ExecutionResult, error)
|
|||||||
// 注册到运行中任务
|
// 注册到运行中任务
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.runningTasks[req.TaskID] = cancel
|
s.runningTasks[req.TaskID] = cancel
|
||||||
if req.LogID > 0 {
|
if req.LogID != "" {
|
||||||
s.runningExecs[req.LogID] = cancel
|
s.runningExecs[req.LogID] = cancel
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
@@ -451,7 +451,7 @@ func (s *Scheduler) executeTask(req *ExecutionRequest) (*ExecutionResult, error)
|
|||||||
defer func() {
|
defer func() {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
delete(s.runningTasks, req.TaskID)
|
delete(s.runningTasks, req.TaskID)
|
||||||
if req.LogID > 0 {
|
if req.LogID != "" {
|
||||||
delete(s.runningExecs, req.LogID)
|
delete(s.runningExecs, req.LogID)
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
@@ -527,14 +527,14 @@ func (s *Scheduler) StopTask(taskID string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StopLog 停止正在运行的任务(通过 LogID,精确停止单个执行副本)
|
// StopLog 停止正在运行的任务(通过 LogID,精确停止单个执行副本)
|
||||||
func (s *Scheduler) StopLog(logID uint) bool {
|
func (s *Scheduler) StopLog(logID string) bool {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
cancel, exists := s.runningExecs[logID]
|
cancel, exists := s.runningExecs[logID]
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
|
|
||||||
if exists && cancel != nil {
|
if exists && cancel != nil {
|
||||||
cancel()
|
cancel()
|
||||||
s.logger.Infof("[Scheduler] 已尝试停止任务执行 #%d", logID)
|
s.logger.Infof("[Scheduler] 已尝试停止任务执行 #%s", logID)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/engigu/baihu-panel/internal/constant"
|
"github.com/engigu/baihu-panel/internal/constant"
|
||||||
|
"github.com/engigu/baihu-panel/internal/database"
|
||||||
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
"github.com/engigu/baihu-panel/internal/services"
|
"github.com/engigu/baihu-panel/internal/services"
|
||||||
"github.com/engigu/baihu-panel/internal/utils"
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
|
|
||||||
@@ -35,9 +37,19 @@ func AuthRequired() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将用户信息存入上下文
|
// 安全增强:校验数据库中该用户的 ID 是否与 Token 一致
|
||||||
c.Set("userID", userID)
|
// 防止迁移后旧 Token 中的数字 ID 污染新数据
|
||||||
c.Set("username", username)
|
var user models.User
|
||||||
|
if err := database.DB.Where("username = ?", username).First(&user).Error; err != nil || user.ID != userID {
|
||||||
|
utils.Unauthorized(c, "会话失效,请重新登录")
|
||||||
|
ClearAuthCookie(c)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将用户信息存入上下文 (必须使用数据库中的最新 ID)
|
||||||
|
c.Set("userID", user.ID)
|
||||||
|
c.Set("username", user.Username)
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,8 +91,16 @@ func checkApiToken(c *gin.Context, settingsSvc *services.SettingsService) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Set("userID", uint(1)) // 模拟 Admin 角色
|
// 模拟 Admin 角色,必须通过实际存在的 admin 用户 ID 来关联
|
||||||
c.Set("username", "api_token_user")
|
var adminUser models.User
|
||||||
|
if err := database.DB.Where("role = ?", "admin").First(&adminUser).Error; err != nil {
|
||||||
|
utils.Unauthorized(c, "未找到管理员账户,API Token 校验失败")
|
||||||
|
c.Abort()
|
||||||
|
return true // 返回 true 表示中间件已处理并截断了请求
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("userID", adminUser.ID)
|
||||||
|
c.Set("username", adminUser.Username)
|
||||||
c.Next()
|
c.Next()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/engigu/baihu-panel/internal/constant"
|
"github.com/engigu/baihu-panel/internal/constant"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -10,7 +8,7 @@ import (
|
|||||||
|
|
||||||
// Agent 远程执行代理
|
// Agent 远程执行代理
|
||||||
type Agent struct {
|
type Agent struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
Name string `json:"name" gorm:"size:100;not null"` // Agent 名称
|
Name string `json:"name" gorm:"size:100;not null"` // Agent 名称
|
||||||
Token string `json:"token" gorm:"size:64;index"` // 认证 Token(可重复使用)
|
Token string `json:"token" gorm:"size:64;index"` // 认证 Token(可重复使用)
|
||||||
MachineID string `json:"machine_id" gorm:"size:64;uniqueIndex"` // 机器识别码(唯一)
|
MachineID string `json:"machine_id" gorm:"size:64;uniqueIndex"` // 机器识别码(唯一)
|
||||||
@@ -36,7 +34,7 @@ func (Agent) TableName() string {
|
|||||||
|
|
||||||
// AgentToken Agent 令牌
|
// AgentToken Agent 令牌
|
||||||
type AgentToken struct {
|
type AgentToken struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
Token string `json:"token" gorm:"size:64;uniqueIndex;not null"` // 令牌
|
Token string `json:"token" gorm:"size:64;uniqueIndex;not null"` // 令牌
|
||||||
Remark string `json:"remark" gorm:"size:255"` // 备注
|
Remark string `json:"remark" gorm:"size:255"` // 备注
|
||||||
MaxUses int `json:"max_uses" gorm:"default:0"` // 最大使用次数,0 表示无限制
|
MaxUses int `json:"max_uses" gorm:"default:0"` // 最大使用次数,0 表示无限制
|
||||||
@@ -54,7 +52,7 @@ func (AgentToken) TableName() string {
|
|||||||
|
|
||||||
// AgentTask Agent 任务配置(用于下发给 Agent)
|
// AgentTask Agent 任务配置(用于下发给 Agent)
|
||||||
type AgentTask struct {
|
type AgentTask struct {
|
||||||
ID uint `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
Schedule string `json:"schedule"`
|
Schedule string `json:"schedule"`
|
||||||
@@ -67,7 +65,7 @@ type AgentTask struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t AgentTask) GetID() string {
|
func (t AgentTask) GetID() string {
|
||||||
return strconv.FormatUint(uint64(t.ID), 10)
|
return t.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t AgentTask) GetName() string {
|
func (t AgentTask) GetName() string {
|
||||||
@@ -88,9 +86,9 @@ func (t AgentTask) GetRandomRange() int {
|
|||||||
|
|
||||||
// AgentTaskResult Agent 上报的任务执行结果
|
// AgentTaskResult Agent 上报的任务执行结果
|
||||||
type AgentTaskResult struct {
|
type AgentTaskResult struct {
|
||||||
TaskID uint `json:"task_id"`
|
TaskID string `json:"task_id"`
|
||||||
LogID uint `json:"log_id"`
|
LogID string `json:"log_id"`
|
||||||
AgentID uint `json:"agent_id"`
|
AgentID string `json:"agent_id"`
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
Output string `json:"output"`
|
Output string `json:"output"`
|
||||||
Error string `json:"error"` // 额外的系统错误信息
|
Error string `json:"error"` // 额外的系统错误信息
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
// Dependency 依赖包模型
|
// Dependency 依赖包模型
|
||||||
type Dependency struct {
|
type Dependency struct {
|
||||||
ID int `json:"id" gorm:"primaryKey"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
Name string `json:"name" gorm:"size:100;not null"`
|
Name string `json:"name" gorm:"size:100;not null"`
|
||||||
Version string `json:"version" gorm:"size:50"`
|
Version string `json:"version" gorm:"size:50"`
|
||||||
Language string `json:"language" gorm:"size:100;index"` // 关联语言 (node, python...)
|
Language string `json:"language" gorm:"size:100;index"` // 关联语言 (node, python...)
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import (
|
|||||||
|
|
||||||
// EnvironmentVariable represents an environment variable
|
// EnvironmentVariable represents an environment variable
|
||||||
type EnvironmentVariable struct {
|
type EnvironmentVariable struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
Name string `json:"name" gorm:"size:255;not null"`
|
Name string `json:"name" gorm:"size:255;not null"`
|
||||||
Value string `json:"value" gorm:"type:text"`
|
Value string `json:"value" gorm:"type:text"`
|
||||||
Remark string `json:"remark" gorm:"size:500"`
|
Remark string `json:"remark" gorm:"size:500"`
|
||||||
Hidden bool `json:"hidden" gorm:"default:true"`
|
Hidden bool `json:"hidden" gorm:"default:true"`
|
||||||
UserID uint `json:"user_id" gorm:"index"`
|
UserID string `json:"user_id" gorm:"size:20;index"`
|
||||||
CreatedAt LocalTime `json:"created_at"`
|
CreatedAt LocalTime `json:"created_at"`
|
||||||
UpdatedAt LocalTime `json:"updated_at"`
|
UpdatedAt LocalTime `json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
@@ -25,10 +25,10 @@ func (EnvironmentVariable) TableName() string {
|
|||||||
|
|
||||||
// Script represents a script file
|
// Script represents a script file
|
||||||
type Script struct {
|
type Script struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
Name string `json:"name" gorm:"size:255;not null"`
|
Name string `json:"name" gorm:"size:255;not null"`
|
||||||
Content string `json:"content" gorm:"type:text"`
|
Content string `json:"content" gorm:"type:text"`
|
||||||
UserID uint `json:"user_id" gorm:"index"`
|
UserID string `json:"user_id" gorm:"size:20;index"`
|
||||||
CreatedAt LocalTime `json:"created_at"`
|
CreatedAt LocalTime `json:"created_at"`
|
||||||
UpdatedAt LocalTime `json:"updated_at"`
|
UpdatedAt LocalTime `json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Language struct {
|
type Language struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
Plugin string `json:"plugin" gorm:"size:100;not null;index"`
|
Plugin string `json:"plugin" gorm:"size:100;not null;index"`
|
||||||
Version string `json:"version" gorm:"size:100;not null;index"`
|
Version string `json:"version" gorm:"size:100;not null;index"`
|
||||||
InstallPath string `json:"install_path" gorm:"size:255"`
|
InstallPath string `json:"install_path" gorm:"size:255"`
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
// LoginLog 登录日志
|
// LoginLog 登录日志
|
||||||
type LoginLog struct {
|
type LoginLog struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
Username string `json:"username" gorm:"size:100;index;not null"`
|
Username string `json:"username" gorm:"size:100;index;not null"`
|
||||||
IP string `json:"ip" gorm:"size:50"`
|
IP string `json:"ip" gorm:"size:50"`
|
||||||
UserAgent string `json:"user_agent" gorm:"size:500"`
|
UserAgent string `json:"user_agent" gorm:"size:500"`
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
// SendStats 任务执行统计
|
// SendStats 任务执行统计
|
||||||
type SendStats struct {
|
type SendStats struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
TaskID uint `json:"task_id" gorm:"uniqueIndex:idx_task_day_status"`
|
TaskID string `json:"task_id" gorm:"size:20;uniqueIndex:idx_task_day_status"`
|
||||||
Day string `json:"day" gorm:"size:10;uniqueIndex:idx_task_day_status"` // 格式: 2006-01-02
|
Day string `json:"day" gorm:"size:10;uniqueIndex:idx_task_day_status"` // 格式: 2006-01-02
|
||||||
Status string `json:"status" gorm:"size:20;uniqueIndex:idx_task_day_status"`
|
Status string `json:"status" gorm:"size:20;uniqueIndex:idx_task_day_status"`
|
||||||
Num int `json:"num" gorm:"default:0"`
|
Num int `json:"num" gorm:"default:0"`
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
// Setting 系统设置
|
// Setting 系统设置
|
||||||
type Setting struct {
|
type Setting struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
Section string `json:"section" gorm:"size:50;not null;index:idx_section_key"`
|
Section string `json:"section" gorm:"size:50;not null;index:idx_section_key"`
|
||||||
Key string `json:"key" gorm:"size:100;not null;index:idx_section_key"`
|
Key string `json:"key" gorm:"size:100;not null;index:idx_section_key"`
|
||||||
Value string `json:"value" gorm:"type:text"`
|
Value string `json:"value" gorm:"type:text"`
|
||||||
|
|||||||
+8
-10
@@ -1,8 +1,6 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/engigu/baihu-panel/internal/constant"
|
"github.com/engigu/baihu-panel/internal/constant"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -34,7 +32,7 @@ type TaskConfig struct {
|
|||||||
|
|
||||||
// Task 代表一个计划任务
|
// Task 代表一个计划任务
|
||||||
type Task struct {
|
type Task struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
Name string `json:"name" gorm:"size:255;not null"`
|
Name string `json:"name" gorm:"size:255;not null"`
|
||||||
Command string `json:"command" gorm:"type:text"` // 普通任务的命令
|
Command string `json:"command" gorm:"type:text"` // 普通任务的命令
|
||||||
Tags string `json:"tags" gorm:"size:255;default:''"` // 标签,逗号分隔
|
Tags string `json:"tags" gorm:"size:255;default:''"` // 标签,逗号分隔
|
||||||
@@ -45,9 +43,9 @@ type Task struct {
|
|||||||
Timeout int `json:"timeout" gorm:"default:30"` // 超时时间(分钟),默认30分钟
|
Timeout int `json:"timeout" gorm:"default:30"` // 超时时间(分钟),默认30分钟
|
||||||
WorkDir string `json:"work_dir" gorm:"size:255;default:''"` // 工作目录,为空则使用 scripts 目录
|
WorkDir string `json:"work_dir" gorm:"size:255;default:''"` // 工作目录,为空则使用 scripts 目录
|
||||||
CleanConfig string `json:"clean_config" gorm:"size:255;default:''"` // 清理配置 JSON
|
CleanConfig string `json:"clean_config" gorm:"size:255;default:''"` // 清理配置 JSON
|
||||||
Envs string `json:"envs" gorm:"size:255;default:''"` // 环境变量ID列表,逗号分隔
|
Envs string `json:"envs" gorm:"type:text"` // 环境变量ID列表,逗号分隔
|
||||||
Languages []map[string]string `json:"languages" gorm:"serializer:json;type:text"` // 针对本地任务的语言配置列表
|
Languages []map[string]string `json:"languages" gorm:"serializer:json;type:text"` // 针对本地任务的语言配置列表
|
||||||
AgentID *uint `json:"agent_id" gorm:"index"` // Agent ID,为空表示本地执行
|
AgentID *string `json:"agent_id" gorm:"size:20;index"` // Agent ID,为空表示本地执行
|
||||||
RetryCount int `json:"retry_count" gorm:"default:0"` // 失败重试次数
|
RetryCount int `json:"retry_count" gorm:"default:0"` // 失败重试次数
|
||||||
RetryInterval int `json:"retry_interval" gorm:"default:0"` // 失败重试间隔(秒)
|
RetryInterval int `json:"retry_interval" gorm:"default:0"` // 失败重试间隔(秒)
|
||||||
RandomRange int `json:"random_range" gorm:"default:0"` // 随机延迟范围(秒)
|
RandomRange int `json:"random_range" gorm:"default:0"` // 随机延迟范围(秒)
|
||||||
@@ -65,7 +63,7 @@ func (Task) TableName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) GetID() string {
|
func (t *Task) GetID() string {
|
||||||
return fmt.Sprintf("%d", t.ID)
|
return t.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) GetName() string {
|
func (t *Task) GetName() string {
|
||||||
@@ -93,7 +91,7 @@ func (t *Task) GetLanguages() []map[string]string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) GetUseMise() bool {
|
func (t *Task) GetUseMise() bool {
|
||||||
return t.AgentID == nil || *t.AgentID == 0
|
return t.AgentID == nil || *t.AgentID == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) UseMise() bool {
|
func (t *Task) UseMise() bool {
|
||||||
@@ -110,9 +108,9 @@ func (t *Task) GetRandomRange() int {
|
|||||||
|
|
||||||
// TaskLog 代表任务执行的日志记录
|
// TaskLog 代表任务执行的日志记录
|
||||||
type TaskLog struct {
|
type TaskLog struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
TaskID uint `json:"task_id" gorm:"index"`
|
TaskID string `json:"task_id" gorm:"size:20;index"`
|
||||||
AgentID *uint `json:"agent_id" gorm:"index"` // Agent ID,为空表示本地执行
|
AgentID *string `json:"agent_id" gorm:"size:20;index"` // Agent ID,为空表示本地执行
|
||||||
Command string `json:"command" gorm:"type:text"`
|
Command string `json:"command" gorm:"type:text"`
|
||||||
Output string `json:"-" gorm:"type:longtext"` // gzip+base64 压缩后的日志
|
Output string `json:"-" gorm:"type:longtext"` // gzip+base64 压缩后的日志
|
||||||
Error string `json:"error" gorm:"type:text"` // 额外的系统错误信息
|
Error string `json:"error" gorm:"type:text"` // 额外的系统错误信息
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
// User represents a system user
|
// User represents a system user
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
Username string `json:"username" gorm:"size:100;uniqueIndex;not null"`
|
Username string `json:"username" gorm:"size:100;uniqueIndex;not null"`
|
||||||
Password string `json:"-" gorm:"size:255;not null"`
|
Password string `json:"-" gorm:"size:255;not null"`
|
||||||
Email string `json:"email" gorm:"size:255"`
|
Email string `json:"email" gorm:"size:255"`
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
// AgentVO 代理视图对象
|
// AgentVO 代理视图对象
|
||||||
type AgentVO struct {
|
type AgentVO struct {
|
||||||
ID uint `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
@@ -71,7 +71,7 @@ func ToAgentVOListFromModels(agents []models.Agent) []*AgentVO {
|
|||||||
|
|
||||||
// AgentTokenVO 代理令牌视图对象
|
// AgentTokenVO 代理令牌视图对象
|
||||||
type AgentTokenVO struct {
|
type AgentTokenVO struct {
|
||||||
ID uint `json:"id"`
|
ID string `json:"id"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
MaxUses int `json:"max_uses"`
|
MaxUses int `json:"max_uses"`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
// DependencyVO 依赖包视图对象
|
// DependencyVO 依赖包视图对象
|
||||||
type DependencyVO struct {
|
type DependencyVO struct {
|
||||||
ID int `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
// ScriptVO 脚本视图对象
|
// ScriptVO 脚本视图对象
|
||||||
type ScriptVO struct {
|
type ScriptVO struct {
|
||||||
ID uint `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Content string `json:"content,omitempty"` // 仅在拉取详情时返回
|
Content string `json:"content,omitempty"` // 仅在拉取详情时返回
|
||||||
CreatedAt models.LocalTime `json:"created_at"`
|
CreatedAt models.LocalTime `json:"created_at"`
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
// UserVO 用户视图对象
|
// UserVO 用户视图对象
|
||||||
type UserVO struct {
|
type UserVO struct {
|
||||||
ID uint `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
@@ -31,7 +31,7 @@ func ToUserVO(user *models.User) *UserVO {
|
|||||||
|
|
||||||
// EnvVO 环境变量视图对象
|
// EnvVO 环境变量视图对象
|
||||||
type EnvVO struct {
|
type EnvVO struct {
|
||||||
ID uint `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
@@ -79,7 +79,7 @@ func ToEnvVOListFromModels(envs []models.EnvironmentVariable) []*EnvVO {
|
|||||||
|
|
||||||
// LoginLogVO 登录日志视图对象
|
// LoginLogVO 登录日志视图对象
|
||||||
type LoginLogVO struct {
|
type LoginLogVO struct {
|
||||||
ID uint `json:"id"`
|
ID string `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
UserAgent string `json:"user_agent"`
|
UserAgent string `json:"user_agent"`
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
// TaskVO 任务视图对象
|
// TaskVO 任务视图对象
|
||||||
type TaskVO struct {
|
type TaskVO struct {
|
||||||
ID uint `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
Tags string `json:"tags"`
|
Tags string `json:"tags"`
|
||||||
@@ -20,7 +20,7 @@ type TaskVO struct {
|
|||||||
CleanConfig string `json:"clean_config"`
|
CleanConfig string `json:"clean_config"`
|
||||||
Envs string `json:"envs"`
|
Envs string `json:"envs"`
|
||||||
Languages []map[string]string `json:"languages"`
|
Languages []map[string]string `json:"languages"`
|
||||||
AgentID *uint `json:"agent_id"`
|
AgentID *string `json:"agent_id"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
RetryCount int `json:"retry_count"`
|
RetryCount int `json:"retry_count"`
|
||||||
RetryInterval int `json:"retry_interval"`
|
RetryInterval int `json:"retry_interval"`
|
||||||
@@ -85,11 +85,11 @@ func ToTaskVOListFromModels(tasks []models.Task) []*TaskVO {
|
|||||||
|
|
||||||
// TaskLogVO 任务历史视图对象
|
// TaskLogVO 任务历史视图对象
|
||||||
type TaskLogVO struct {
|
type TaskLogVO struct {
|
||||||
ID uint `json:"id"`
|
ID string `json:"id"`
|
||||||
TaskID uint `json:"task_id"`
|
TaskID string `json:"task_id"`
|
||||||
TaskName string `json:"task_name"`
|
TaskName string `json:"task_name"`
|
||||||
TaskType string `json:"task_type"`
|
TaskType string `json:"task_type"`
|
||||||
AgentID *uint `json:"agent_id"`
|
AgentID *string `json:"agent_id"`
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
@@ -148,7 +148,7 @@ func ToTaskLogVOListFromModels(logs []models.TaskLog) []*TaskLogVO {
|
|||||||
// ExecutionResultVO 任务执行结果视图对象
|
// ExecutionResultVO 任务执行结果视图对象
|
||||||
type ExecutionResultVO struct {
|
type ExecutionResultVO struct {
|
||||||
TaskID string `json:"task_id"`
|
TaskID string `json:"task_id"`
|
||||||
LogID uint `json:"log_id,omitempty"`
|
LogID string `json:"log_id,omitempty"`
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Output string `json:"output,omitempty"`
|
Output string `json:"output,omitempty"`
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/engigu/baihu-panel/internal/logger"
|
"github.com/engigu/baihu-panel/internal/logger"
|
||||||
"github.com/engigu/baihu-panel/internal/models"
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
"github.com/engigu/baihu-panel/internal/services/tasks"
|
"github.com/engigu/baihu-panel/internal/services/tasks"
|
||||||
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -46,6 +47,7 @@ func (s *AgentService) CreateToken(remark string, maxUses int, expiresAt *time.T
|
|||||||
token := generateToken()
|
token := generateToken()
|
||||||
|
|
||||||
agentToken := &models.AgentToken{
|
agentToken := &models.AgentToken{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
Token: token,
|
Token: token,
|
||||||
Remark: remark,
|
Remark: remark,
|
||||||
MaxUses: maxUses,
|
MaxUses: maxUses,
|
||||||
@@ -69,8 +71,8 @@ func (s *AgentService) ListTokens() []models.AgentToken {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteToken 删除令牌
|
// DeleteToken 删除令牌
|
||||||
func (s *AgentService) DeleteToken(id uint) error {
|
func (s *AgentService) DeleteToken(id string) error {
|
||||||
return database.DB.Delete(&models.AgentToken{}, id).Error
|
return database.DB.Where("id = ?", id).Delete(&models.AgentToken{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateToken 验证令牌
|
// ValidateToken 验证令牌
|
||||||
@@ -98,7 +100,7 @@ func (s *AgentService) ValidateToken(token string) (*models.AgentToken, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UseToken 使用令牌(增加使用计数)
|
// UseToken 使用令牌(增加使用计数)
|
||||||
func (s *AgentService) UseToken(id uint) {
|
func (s *AgentService) UseToken(id string) {
|
||||||
database.DB.Model(&models.AgentToken{}).Where("id = ?", id).UpdateColumn("used_count", gorm.Expr("used_count + 1"))
|
database.DB.Model(&models.AgentToken{}).Where("id = ?", id).UpdateColumn("used_count", gorm.Expr("used_count + 1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +128,7 @@ func (s *AgentService) RegisterByToken(token string, machineID string, ip string
|
|||||||
"last_seen": now,
|
"last_seen": now,
|
||||||
})
|
})
|
||||||
s.UseToken(agentToken.ID)
|
s.UseToken(agentToken.ID)
|
||||||
logger.Infof("[Agent] Agent #%d 通过 machine_id 复用 (%s)", existing.ID, machineID[:8]+"...")
|
logger.Infof("[Agent] Agent #%s 通过 machine_id 复用 (%s)", existing.ID, machineID[:8]+"...")
|
||||||
return &existing, false, nil
|
return &existing, false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,6 +136,7 @@ func (s *AgentService) RegisterByToken(token string, machineID string, ip string
|
|||||||
// 创建 Agent,使用令牌作为认证 Token
|
// 创建 Agent,使用令牌作为认证 Token
|
||||||
now := models.LocalTime(time.Now())
|
now := models.LocalTime(time.Now())
|
||||||
agent := &models.Agent{
|
agent := &models.Agent{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
Name: fmt.Sprintf("agent-%d", time.Now().Unix()),
|
Name: fmt.Sprintf("agent-%d", time.Now().Unix()),
|
||||||
Token: token,
|
Token: token,
|
||||||
MachineID: machineID,
|
MachineID: machineID,
|
||||||
@@ -148,7 +151,7 @@ func (s *AgentService) RegisterByToken(token string, machineID string, ip string
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.UseToken(agentToken.ID)
|
s.UseToken(agentToken.ID)
|
||||||
logger.Infof("[Agent] Agent 通过令牌注册: #%d (%s)", agent.ID, ip)
|
logger.Infof("[Agent] Agent 通过令牌注册: #%s (%s)", agent.ID, ip)
|
||||||
return agent, true, nil
|
return agent, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +176,7 @@ func (s *AgentService) Register(req *models.AgentRegisterRequest, ip string) (*m
|
|||||||
// 创建新 Agent,使用令牌作为认证 Token
|
// 创建新 Agent,使用令牌作为认证 Token
|
||||||
now := models.LocalTime(time.Now())
|
now := models.LocalTime(time.Now())
|
||||||
agent := &models.Agent{
|
agent := &models.Agent{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Token: req.Token,
|
Token: req.Token,
|
||||||
Hostname: req.Hostname,
|
Hostname: req.Hostname,
|
||||||
@@ -194,7 +198,7 @@ func (s *AgentService) Register(req *models.AgentRegisterRequest, ip string) (*m
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update 更新 Agent
|
// Update 更新 Agent
|
||||||
func (s *AgentService) Update(id uint, name, description string, enabled bool) error {
|
func (s *AgentService) Update(id string, name, description string, enabled bool) error {
|
||||||
return database.DB.Model(&models.Agent{}).Where("id = ?", id).Updates(map[string]interface{}{
|
return database.DB.Model(&models.Agent{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||||
"name": name,
|
"name": name,
|
||||||
"description": description,
|
"description": description,
|
||||||
@@ -203,7 +207,7 @@ func (s *AgentService) Update(id uint, name, description string, enabled bool) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete 删除 Agent(物理删除)
|
// Delete 删除 Agent(物理删除)
|
||||||
func (s *AgentService) Delete(id uint) error {
|
func (s *AgentService) Delete(id string) error {
|
||||||
// 检查是否有关联任务
|
// 检查是否有关联任务
|
||||||
var count int64
|
var count int64
|
||||||
database.DB.Model(&models.Task{}).Where("agent_id = ?", id).Count(&count)
|
database.DB.Model(&models.Task{}).Where("agent_id = ?", id).Count(&count)
|
||||||
@@ -211,13 +215,13 @@ func (s *AgentService) Delete(id uint) error {
|
|||||||
return &ServiceError{Message: "该 Agent 下还有关联任务,无法删除"}
|
return &ServiceError{Message: "该 Agent 下还有关联任务,无法删除"}
|
||||||
}
|
}
|
||||||
|
|
||||||
return database.DB.Unscoped().Delete(&models.Agent{}, id).Error
|
return database.DB.Unscoped().Where("id = ?", id).Delete(&models.Agent{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByID 根据 ID 获取 Agent
|
// GetByID 根据 ID 获取 Agent
|
||||||
func (s *AgentService) GetByID(id uint) *models.Agent {
|
func (s *AgentService) GetByID(id string) *models.Agent {
|
||||||
var agent models.Agent
|
var agent models.Agent
|
||||||
if err := database.DB.First(&agent, id).Error; err != nil {
|
if err := database.DB.Where("id = ?", id).First(&agent).Error; err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &agent
|
return &agent
|
||||||
@@ -249,7 +253,7 @@ func (s *AgentService) List() []models.Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RegenerateToken 重新生成 Token - 已废弃,保留空实现避免路由错误
|
// RegenerateToken 重新生成 Token - 已废弃,保留空实现避免路由错误
|
||||||
func (s *AgentService) RegenerateToken(id uint) (string, error) {
|
func (s *AgentService) RegenerateToken(id string) (string, error) {
|
||||||
return "", &ServiceError{Message: "此功能已禁用"}
|
return "", &ServiceError{Message: "此功能已禁用"}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +305,7 @@ func (s *AgentService) Heartbeat(token, ip, version, buildTime, hostname, osType
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTasks 获取 Agent 的任务列表
|
// GetTasks 获取 Agent 的任务列表
|
||||||
func (s *AgentService) GetTasks(agentID uint) []models.AgentTask {
|
func (s *AgentService) GetTasks(agentID string) []models.AgentTask {
|
||||||
var tasks []models.Task
|
var tasks []models.Task
|
||||||
database.DB.Where("agent_id = ? AND enabled = ?", agentID, true).Find(&tasks)
|
database.DB.Where("agent_id = ? AND enabled = ?", agentID, true).Find(&tasks)
|
||||||
|
|
||||||
@@ -359,13 +363,13 @@ func (s *AgentService) ReportResult(result *models.AgentTaskResult) error {
|
|||||||
|
|
||||||
// 先尝试通知正在等待的 goroutine
|
// 先尝试通知正在等待的 goroutine
|
||||||
if agentWSManager.NotifyRemoteResult(result) {
|
if agentWSManager.NotifyRemoteResult(result) {
|
||||||
logger.Infof("[Agent] 已通知正在等待任务 #%d 结果的 goroutine", result.TaskID)
|
logger.Infof("[Agent] 已通知正在等待任务 #%s 结果的 goroutine", result.TaskID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有人在等待(例如服务重启后),则由本协程负责处理结果入库
|
// 如果没有人在等待(例如服务重启后),则由本协程负责处理结果入库
|
||||||
// 如果没有人在等待(例如服务重启后),则由本协程负责处理结果入库(记录日志并清理)
|
// 如果没有人在等待(例如服务重启后),则由本协程负责处理结果入库(记录日志并清理)
|
||||||
logger.Infof("[Agent] 没有找到等待任务 #%d 结果的 goroutine,直接处理结果", result.TaskID)
|
logger.Infof("[Agent] 没有找到等待任务 #%s 结果的 goroutine,直接处理结果", result.TaskID)
|
||||||
sendStatsService := NewSendStatsService()
|
sendStatsService := NewSendStatsService()
|
||||||
taskLogService := tasks.NewTaskLogService(sendStatsService)
|
taskLogService := tasks.NewTaskLogService(sendStatsService)
|
||||||
|
|
||||||
@@ -379,7 +383,7 @@ func (s *AgentService) ReportResult(result *models.AgentTaskResult) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTaskDuration 更新任务耗时(心跳)
|
// UpdateTaskDuration 更新任务耗时(心跳)
|
||||||
func (s *AgentService) UpdateTaskDuration(logID uint, duration int64) error {
|
func (s *AgentService) UpdateTaskDuration(logID string, duration int64) error {
|
||||||
taskLogService := tasks.NewTaskLogService(nil)
|
taskLogService := tasks.NewTaskLogService(nil)
|
||||||
return taskLogService.UpdateTaskDuration(logID, duration)
|
return taskLogService.UpdateTaskDuration(logID, duration)
|
||||||
}
|
}
|
||||||
@@ -504,12 +508,12 @@ func (s *AgentService) GetAgentBinary(osType, arch string) ([]byte, string, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetForceUpdate 设置强制更新标志
|
// SetForceUpdate 设置强制更新标志
|
||||||
func (s *AgentService) SetForceUpdate(id uint) error {
|
func (s *AgentService) SetForceUpdate(id string) error {
|
||||||
return database.DB.Model(&models.Agent{}).Where("id = ?", id).Update("force_update", true).Error
|
return database.DB.Model(&models.Agent{}).Where("id = ?", id).Update("force_update", true).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearForceUpdate 清除强制更新标志
|
// ClearForceUpdate 清除强制更新标志
|
||||||
func (s *AgentService) ClearForceUpdate(id uint) error {
|
func (s *AgentService) ClearForceUpdate(id string) error {
|
||||||
return database.DB.Model(&models.Agent{}).Where("id = ?", id).Update("force_update", false).Error
|
return database.DB.Model(&models.Agent{}).Where("id = ?", id).Update("force_update", false).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import (
|
|||||||
|
|
||||||
// AgentWSManager WebSocket 连接管理器
|
// AgentWSManager WebSocket 连接管理器
|
||||||
type AgentWSManager struct {
|
type AgentWSManager struct {
|
||||||
connections map[uint]*AgentConnection // Agent ID -> 连接对象
|
connections map[string]*AgentConnection // Agent ID -> 连接对象
|
||||||
ipConnections map[string]int // IP -> 连接数
|
ipConnections map[string]int // IP -> 连接数
|
||||||
ipLastAttempt map[string]time.Time // IP -> 最后连接尝试时间
|
ipLastAttempt map[string]time.Time // IP -> 最后连接尝试时间
|
||||||
ipFailCount map[string]int // IP -> 连续失败次数
|
ipFailCount map[string]int // IP -> 连续失败次数
|
||||||
remoteWaiters map[uint]chan *models.AgentTaskResult // 日志 ID -> 结果通道
|
remoteWaiters map[string]chan *models.AgentTaskResult // 日志 ID -> 结果通道
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ const (
|
|||||||
|
|
||||||
// AgentConnection Agent WebSocket 连接
|
// AgentConnection Agent WebSocket 连接
|
||||||
type AgentConnection struct {
|
type AgentConnection struct {
|
||||||
AgentID uint
|
AgentID string
|
||||||
IP string
|
IP string
|
||||||
Conn *websocket.Conn
|
Conn *websocket.Conn
|
||||||
Send chan []byte
|
Send chan []byte
|
||||||
@@ -72,11 +72,11 @@ var agentWSOnce sync.Once
|
|||||||
func GetAgentWSManager() *AgentWSManager {
|
func GetAgentWSManager() *AgentWSManager {
|
||||||
agentWSOnce.Do(func() {
|
agentWSOnce.Do(func() {
|
||||||
agentWSManager = &AgentWSManager{
|
agentWSManager = &AgentWSManager{
|
||||||
connections: make(map[uint]*AgentConnection),
|
connections: make(map[string]*AgentConnection),
|
||||||
ipConnections: make(map[string]int),
|
ipConnections: make(map[string]int),
|
||||||
ipLastAttempt: make(map[string]time.Time),
|
ipLastAttempt: make(map[string]time.Time),
|
||||||
ipFailCount: make(map[string]int),
|
ipFailCount: make(map[string]int),
|
||||||
remoteWaiters: make(map[uint]chan *models.AgentTaskResult),
|
remoteWaiters: make(map[string]chan *models.AgentTaskResult),
|
||||||
}
|
}
|
||||||
go agentWSManager.cleanupLoop()
|
go agentWSManager.cleanupLoop()
|
||||||
})
|
})
|
||||||
@@ -137,7 +137,7 @@ func (m *AgentWSManager) RecordConnectSuccess(ip string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register 注册连接
|
// Register 注册连接
|
||||||
func (m *AgentWSManager) Register(agentID uint, conn *websocket.Conn, ip string) *AgentConnection {
|
func (m *AgentWSManager) Register(agentID string, conn *websocket.Conn, ip string) *AgentConnection {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
@@ -164,12 +164,12 @@ func (m *AgentWSManager) Register(agentID uint, conn *websocket.Conn, ip string)
|
|||||||
// 增加 IP 连接计数
|
// 增加 IP 连接计数
|
||||||
m.ipConnections[ip]++
|
m.ipConnections[ip]++
|
||||||
|
|
||||||
logger.Infof("[AgentWS] Agent #%d 已连接 (%s)", agentID, ip)
|
logger.Infof("[AgentWS] Agent #%s 已连接 (%s)", agentID, ip)
|
||||||
return ac
|
return ac
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unregister 注销连接(只注销指定的连接实例)
|
// Unregister 注销连接(只注销指定的连接实例)
|
||||||
func (m *AgentWSManager) Unregister(agentID uint, ac *AgentConnection) {
|
func (m *AgentWSManager) Unregister(agentID string, ac *AgentConnection) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
@@ -183,19 +183,19 @@ func (m *AgentWSManager) Unregister(agentID uint, ac *AgentConnection) {
|
|||||||
}
|
}
|
||||||
conn.Close()
|
conn.Close()
|
||||||
delete(m.connections, agentID)
|
delete(m.connections, agentID)
|
||||||
logger.Infof("[AgentWS] Agent #%d 已断开", agentID)
|
logger.Infof("[AgentWS] Agent #%s 已断开", agentID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConnection 获取连接
|
// GetConnection 获取连接
|
||||||
func (m *AgentWSManager) GetConnection(agentID uint) *AgentConnection {
|
func (m *AgentWSManager) GetConnection(agentID string) *AgentConnection {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
return m.connections[agentID]
|
return m.connections[agentID]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendToAgent 发送消息给指定 Agent
|
// SendToAgent 发送消息给指定 Agent
|
||||||
func (m *AgentWSManager) SendToAgent(agentID uint, msgType string, data interface{}) error {
|
func (m *AgentWSManager) SendToAgent(agentID string, msgType string, data interface{}) error {
|
||||||
conn := m.GetConnection(agentID)
|
conn := m.GetConnection(agentID)
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return nil // Agent 不在线
|
return nil // Agent 不在线
|
||||||
@@ -214,7 +214,7 @@ func (m *AgentWSManager) SendToAgent(agentID uint, msgType string, data interfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BroadcastTasks 广播任务更新给指定 Agent
|
// BroadcastTasks 广播任务更新给指定 Agent
|
||||||
func (m *AgentWSManager) BroadcastTasks(agentID uint) {
|
func (m *AgentWSManager) BroadcastTasks(agentID string) {
|
||||||
agentService := NewAgentService()
|
agentService := NewAgentService()
|
||||||
tasks := agentService.GetTasks(agentID)
|
tasks := agentService.GetTasks(agentID)
|
||||||
m.SendToAgent(agentID, WSTypeTasks, map[string]interface{}{
|
m.SendToAgent(agentID, WSTypeTasks, map[string]interface{}{
|
||||||
@@ -223,7 +223,7 @@ func (m *AgentWSManager) BroadcastTasks(agentID uint) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RegisterRemoteWaiter 注册远程任务结果等待者
|
// RegisterRemoteWaiter 注册远程任务结果等待者
|
||||||
func (m *AgentWSManager) RegisterRemoteWaiter(logID uint) chan *models.AgentTaskResult {
|
func (m *AgentWSManager) RegisterRemoteWaiter(logID string) chan *models.AgentTaskResult {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
ch := make(chan *models.AgentTaskResult, 1)
|
ch := make(chan *models.AgentTaskResult, 1)
|
||||||
@@ -232,7 +232,7 @@ func (m *AgentWSManager) RegisterRemoteWaiter(logID uint) chan *models.AgentTask
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UnregisterRemoteWaiter 注销远程任务结果等待者
|
// UnregisterRemoteWaiter 注销远程任务结果等待者
|
||||||
func (m *AgentWSManager) UnregisterRemoteWaiter(logID uint) {
|
func (m *AgentWSManager) UnregisterRemoteWaiter(logID string) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
delete(m.remoteWaiters, logID)
|
delete(m.remoteWaiters, logID)
|
||||||
@@ -293,7 +293,7 @@ func (m *AgentWSManager) cleanupLoop() {
|
|||||||
delete(m.connections, agentID)
|
delete(m.connections, agentID)
|
||||||
// 更新数据库状态
|
// 更新数据库状态
|
||||||
database.DB.Model(&models.Agent{}).Where("id = ?", agentID).Update("status", constant.AgentStatusOffline)
|
database.DB.Model(&models.Agent{}).Where("id = ?", agentID).Update("status", constant.AgentStatusOffline)
|
||||||
logger.Infof("[AgentWS] Agent #%d 心跳超时,已断开", agentID)
|
logger.Infof("[AgentWS] Agent #%s 心跳超时,已断开", agentID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ func (s *BackupService) CreateBackup() (string, error) {
|
|||||||
|
|
||||||
// 写入元数据信息
|
// 写入元数据信息
|
||||||
sysInfo := map[string]interface{}{
|
sysInfo := map[string]interface{}{
|
||||||
"version": "v2",
|
"version": "v3",
|
||||||
"ts": time.Now().Format("2006-01-02 15:04:05"),
|
"ts": time.Now().Format("2006-01-02 15:04:05"),
|
||||||
}
|
}
|
||||||
sysFile, err := zipWriter.Create("__sys__.json")
|
sysFile, err := zipWriter.Create("__sys__.json")
|
||||||
@@ -197,6 +197,24 @@ func (s *BackupService) Restore(zipPath string) error {
|
|||||||
fileMap[f.Name] = f
|
fileMap[f.Name] = f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 校验版本
|
||||||
|
if f, ok := fileMap["__sys__.json"]; ok {
|
||||||
|
rc, err := f.Open()
|
||||||
|
if err == nil {
|
||||||
|
var sysInfo map[string]interface{}
|
||||||
|
json.NewDecoder(rc).Decode(&sysInfo)
|
||||||
|
rc.Close()
|
||||||
|
if v, ok := sysInfo["version"]; ok {
|
||||||
|
vs, _ := v.(string)
|
||||||
|
if vs < "v3" {
|
||||||
|
return fmt.Errorf("只能数据随版本升级上来,当前备份版本为 %s,限制 v3 以下的不能导入", vs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// } else {
|
||||||
|
// return fmt.Errorf("非法备份包:缺失版本标记")
|
||||||
|
}
|
||||||
|
|
||||||
// 开启全局事务
|
// 开启全局事务
|
||||||
return database.DB.Transaction(func(tx *gorm.DB) error {
|
return database.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
// 1. 清空现有数据(物理删除)
|
// 1. 清空现有数据(物理删除)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/engigu/baihu-panel/internal/database"
|
"github.com/engigu/baihu-panel/internal/database"
|
||||||
"github.com/engigu/baihu-panel/internal/models"
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
"github.com/engigu/baihu-panel/internal/services/deps"
|
"github.com/engigu/baihu-panel/internal/services/deps"
|
||||||
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DependencyService struct{}
|
type DependencyService struct{}
|
||||||
@@ -36,12 +37,15 @@ func (s *DependencyService) Create(dep *models.Dependency) error {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return errors.New("依赖已存在")
|
return errors.New("依赖已存在")
|
||||||
}
|
}
|
||||||
|
if dep.ID == "" {
|
||||||
|
dep.ID = utils.GenerateID()
|
||||||
|
}
|
||||||
return database.DB.Create(dep).Error
|
return database.DB.Create(dep).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete 删除依赖记录
|
// Delete 删除依赖记录
|
||||||
func (s *DependencyService) Delete(id int) error {
|
func (s *DependencyService) Delete(id string) error {
|
||||||
return database.DB.Delete(&models.Dependency{}, id).Error
|
return database.DB.Where("id = ?", id).Delete(&models.Dependency{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install 安装依赖
|
// Install 安装依赖
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/engigu/baihu-panel/internal/database"
|
"github.com/engigu/baihu-panel/internal/database"
|
||||||
"github.com/engigu/baihu-panel/internal/models"
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EnvService struct{}
|
type EnvService struct{}
|
||||||
@@ -14,42 +14,26 @@ func NewEnvService() *EnvService {
|
|||||||
return &EnvService{}
|
return &EnvService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (es *EnvService) CreateEnvVar(name, value, remark string, hidden bool, userID int) *models.EnvironmentVariable {
|
func (es *EnvService) CreateEnvVar(name, value, remark string, hidden bool, userID string) *models.EnvironmentVariable {
|
||||||
env := &models.EnvironmentVariable{
|
env := &models.EnvironmentVariable{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: value,
|
Value: value,
|
||||||
Remark: remark,
|
Remark: remark,
|
||||||
Hidden: hidden,
|
Hidden: hidden,
|
||||||
UserID: uint(userID),
|
UserID: userID,
|
||||||
}
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"name": name,
|
|
||||||
"value": value,
|
|
||||||
"remark": remark,
|
|
||||||
"hidden": hidden,
|
|
||||||
"user_id": userID,
|
|
||||||
}
|
|
||||||
database.DB.Model(&models.EnvironmentVariable{}).Create(data)
|
|
||||||
|
|
||||||
// 将自动生成的 ID 赋值回对象以便返回
|
|
||||||
if id, ok := data["id"].(uint); ok {
|
|
||||||
env.ID = id
|
|
||||||
} else if id, ok := data["id"].(int64); ok {
|
|
||||||
env.ID = uint(id)
|
|
||||||
} else {
|
|
||||||
// 如果 ID 没有自动回填到 map,尝试通过刚才的数据查出来
|
|
||||||
database.DB.Where("name = ? AND user_id = ?", name, userID).Order("id DESC").First(env)
|
|
||||||
}
|
}
|
||||||
|
database.DB.Create(env)
|
||||||
return env
|
return env
|
||||||
}
|
}
|
||||||
|
|
||||||
func (es *EnvService) GetEnvVarsByUserID(userID int) []models.EnvironmentVariable {
|
func (es *EnvService) GetEnvVarsByUserID(userID string) []models.EnvironmentVariable {
|
||||||
var envs []models.EnvironmentVariable
|
var envs []models.EnvironmentVariable
|
||||||
database.DB.Where("user_id = ?", userID).Find(&envs)
|
database.DB.Where("user_id = ?", userID).Find(&envs)
|
||||||
return envs
|
return envs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (es *EnvService) GetEnvVarsWithPagination(userID int, name string, page, pageSize int) ([]models.EnvironmentVariable, int64) {
|
func (es *EnvService) GetEnvVarsWithPagination(userID string, name string, page, pageSize int) ([]models.EnvironmentVariable, int64) {
|
||||||
var envs []models.EnvironmentVariable
|
var envs []models.EnvironmentVariable
|
||||||
var total int64
|
var total int64
|
||||||
|
|
||||||
@@ -63,17 +47,17 @@ func (es *EnvService) GetEnvVarsWithPagination(userID int, name string, page, pa
|
|||||||
return envs, total
|
return envs, total
|
||||||
}
|
}
|
||||||
|
|
||||||
func (es *EnvService) GetEnvVarByID(id int) *models.EnvironmentVariable {
|
func (es *EnvService) GetEnvVarByID(id string) *models.EnvironmentVariable {
|
||||||
var env models.EnvironmentVariable
|
var env models.EnvironmentVariable
|
||||||
if err := database.DB.First(&env, id).Error; err != nil {
|
if err := database.DB.Where("id = ?", id).First(&env).Error; err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &env
|
return &env
|
||||||
}
|
}
|
||||||
|
|
||||||
func (es *EnvService) UpdateEnvVar(id int, name, value, remark string, hidden bool) *models.EnvironmentVariable {
|
func (es *EnvService) UpdateEnvVar(id string, name, value, remark string, hidden bool) *models.EnvironmentVariable {
|
||||||
var env models.EnvironmentVariable
|
var env models.EnvironmentVariable
|
||||||
if err := database.DB.First(&env, id).Error; err != nil {
|
if err := database.DB.Where("id = ?", id).First(&env).Error; err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
env.Name = name
|
env.Name = name
|
||||||
@@ -84,8 +68,8 @@ func (es *EnvService) UpdateEnvVar(id int, name, value, remark string, hidden bo
|
|||||||
return &env
|
return &env
|
||||||
}
|
}
|
||||||
|
|
||||||
func (es *EnvService) DeleteEnvVar(id int) bool {
|
func (es *EnvService) DeleteEnvVar(id string) bool {
|
||||||
result := database.DB.Delete(&models.EnvironmentVariable{}, id)
|
result := database.DB.Where("id = ?", id).Delete(&models.EnvironmentVariable{})
|
||||||
return result.RowsAffected > 0
|
return result.RowsAffected > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,12 +91,12 @@ func (es *EnvService) GetEnvVarsByIDs(envIDs string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// splitEnvIDs 解析逗号分隔的ID字符串
|
// splitEnvIDs 解析逗号分隔的ID字符串
|
||||||
func splitEnvIDs(envIDs string) []int {
|
func splitEnvIDs(envIDs string) []string {
|
||||||
var ids []int
|
var ids []string
|
||||||
for _, s := range strings.Split(envIDs, ",") {
|
for _, s := range strings.Split(envIDs, ",") {
|
||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
if id, err := strconv.Atoi(s); err == nil {
|
if s != "" {
|
||||||
ids = append(ids, id)
|
ids = append(ids, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ids
|
return ids
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package services
|
|||||||
import (
|
import (
|
||||||
"github.com/engigu/baihu-panel/internal/database"
|
"github.com/engigu/baihu-panel/internal/database"
|
||||||
"github.com/engigu/baihu-panel/internal/models"
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginLogService struct{}
|
type LoginLogService struct{}
|
||||||
@@ -14,6 +15,7 @@ func NewLoginLogService() *LoginLogService {
|
|||||||
// Create 创建登录日志
|
// Create 创建登录日志
|
||||||
func (s *LoginLogService) Create(username, ip, userAgent, status, message string) error {
|
func (s *LoginLogService) Create(username, ip, userAgent, status, message string) error {
|
||||||
log := &models.LoginLog{
|
log := &models.LoginLog{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
Username: username,
|
Username: username,
|
||||||
IP: ip,
|
IP: ip,
|
||||||
UserAgent: userAgent,
|
UserAgent: userAgent,
|
||||||
|
|||||||
@@ -0,0 +1,326 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/engigu/baihu-panel/internal/database"
|
||||||
|
"github.com/engigu/baihu-panel/internal/logger"
|
||||||
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MigrationTable 定义迁移配置
|
||||||
|
type MigrationTable struct {
|
||||||
|
Model any
|
||||||
|
EntityName string
|
||||||
|
FKs map[string]string // 单个外键映射: 字段名 -> 实体名
|
||||||
|
MultiFKs map[string]string // 复合外键映射 (如逗号分隔): 字段名 -> 实体名
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMigrationTables() []MigrationTable {
|
||||||
|
return []MigrationTable{
|
||||||
|
{&models.User{}, "users", nil, nil},
|
||||||
|
{&models.Agent{}, "agents", nil, nil},
|
||||||
|
{&models.AgentToken{}, "tokens", nil, nil},
|
||||||
|
{&models.EnvironmentVariable{}, "envs", map[string]string{"UserID": "users"}, nil},
|
||||||
|
{&models.Task{}, "tasks", map[string]string{"AgentID": "agents"}, map[string]string{"Envs": "envs"}},
|
||||||
|
{&models.TaskLog{}, "task_logs", map[string]string{"TaskID": "tasks", "AgentID": "agents"}, nil},
|
||||||
|
{&models.Script{}, "scripts", map[string]string{"UserID": "users"}, nil},
|
||||||
|
{&models.Setting{}, "settings", nil, nil},
|
||||||
|
{&models.SendStats{}, "send_stats", map[string]string{"TaskID": "tasks"}, nil},
|
||||||
|
{&models.LoginLog{}, "login_logs", nil, nil},
|
||||||
|
{&models.Language{}, "languages", nil, nil},
|
||||||
|
{&models.Dependency{}, "deps", nil, nil},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTableName(db *gorm.DB, model any) string {
|
||||||
|
stmt := &gorm.Statement{DB: db}
|
||||||
|
if err := stmt.Parse(model); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return stmt.Schema.Table
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTableStringID(db *gorm.DB, model any) bool {
|
||||||
|
if !db.Migrator().HasTable(model) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
columnTypes, err := db.Migrator().ColumnTypes(model)
|
||||||
|
if err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, ct := range columnTypes {
|
||||||
|
if strings.ToLower(ct.Name()) == "id" {
|
||||||
|
typeName := strings.ToLower(ct.DatabaseTypeName())
|
||||||
|
return strings.Contains(typeName, "char") || strings.Contains(typeName, "text") || strings.Contains(typeName, "string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getValFromMap(m map[string]interface{}, key string) (interface{}, bool) {
|
||||||
|
lowerKey := strings.ToLower(key)
|
||||||
|
for k, v := range m {
|
||||||
|
if strings.ToLower(k) == lowerKey {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunMigrationV3() error {
|
||||||
|
db := database.DB
|
||||||
|
if db == nil {
|
||||||
|
return fmt.Errorf("数据库未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0. 检查迁移标记,防止重复迁移逻辑被误判触发
|
||||||
|
if db.Migrator().HasTable(&models.Setting{}) {
|
||||||
|
var migrationFlag models.Setting
|
||||||
|
err := db.Where("section = ? AND `key` = ?", "system", "migration_v3_success").First(&migrationFlag).Error
|
||||||
|
if err == nil && migrationFlag.Value == "true" {
|
||||||
|
// 如果已经是字符串 ID 模式,双重确认
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tables := getMigrationTables()
|
||||||
|
needMigration := false
|
||||||
|
for _, t := range tables {
|
||||||
|
if !isTableStringID(db, t.Model) {
|
||||||
|
needMigration = true
|
||||||
|
logger.Infof("[MigrationV3] 表 [%s] ID 为数字,需迁移", getTableName(db, t.Model))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !needMigration {
|
||||||
|
// 没有数字 ID 表了,但也可能还没打标(比如之前手动修过表),此时补一个标
|
||||||
|
return markMigrationSuccess(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 备份过程
|
||||||
|
backupDir := "./data"
|
||||||
|
os.MkdirAll(backupDir, 0755)
|
||||||
|
// 如果已经有还原过的记录,为了防止反复循环,我们可以检查还原标记
|
||||||
|
backups, _ := filepath.Glob(filepath.Join(backupDir, "migration_v3_backup_*.zip"))
|
||||||
|
if len(backups) == 0 {
|
||||||
|
logger.Infof("[MigrationV3] 执行关键备份...")
|
||||||
|
backupService := NewBackupService()
|
||||||
|
zipPath, err := backupService.CreateBackup()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("自动备份失败,流程终止: %v", err)
|
||||||
|
}
|
||||||
|
newPath := filepath.Join(backupDir, fmt.Sprintf("migration_v3_backup_%s.zip", filepath.Base(zipPath)))
|
||||||
|
os.Rename(zipPath, newPath)
|
||||||
|
logger.Infof("[MigrationV3] 备份成功: %s", newPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
mappings := make(map[string]map[uint]string)
|
||||||
|
err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
return performHardMigration(tx, mappings)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 标记成功
|
||||||
|
return markMigrationSuccess(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func markMigrationSuccess(db *gorm.DB) error {
|
||||||
|
if !db.Migrator().HasTable(&models.Setting{}) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var flag models.Setting
|
||||||
|
err := db.Where("section = ? AND `key` = ?", "system", "migration_v3_success").First(&flag).Error
|
||||||
|
if err != nil {
|
||||||
|
// 创建或更新
|
||||||
|
flag = models.Setting{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
|
Section: "system",
|
||||||
|
Key: "migration_v3_success",
|
||||||
|
Value: "true",
|
||||||
|
}
|
||||||
|
return db.Create(&flag).Error
|
||||||
|
}
|
||||||
|
return db.Model(&flag).Update("value", "true").Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func performHardMigration(tx *gorm.DB, mappings map[string]map[uint]string) error {
|
||||||
|
allTables := getMigrationTables()
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// 第一阶段:全量构建 ID 映射映射表 (Pass 1)
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
for _, t := range allTables {
|
||||||
|
actualName := getTableName(tx, t.Model)
|
||||||
|
if actualName == "" || !tx.Migrator().HasTable(actualName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mappings[t.EntityName] = make(map[uint]string)
|
||||||
|
oldTableName := actualName + "_v2_bak"
|
||||||
|
|
||||||
|
// 如果还没有备份表,说明这是第一次处理该表,先重命名
|
||||||
|
if !tx.Migrator().HasTable(oldTableName) {
|
||||||
|
if isTableStringID(tx, t.Model) {
|
||||||
|
continue // 已经是字符串 ID 且无备份,跳过
|
||||||
|
}
|
||||||
|
if err := tx.Migrator().RenameTable(actualName, oldTableName); err != nil {
|
||||||
|
return fmt.Errorf("重命名表 %s 失败: %v", actualName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预先为该表所有记录生成新的 xid
|
||||||
|
var rows []map[string]interface{}
|
||||||
|
tx.Table(oldTableName).Select("id").Find(&rows)
|
||||||
|
for _, row := range rows {
|
||||||
|
if val, ok := getValFromMap(row, "id"); ok {
|
||||||
|
uid := parseUint(val)
|
||||||
|
if uid > 0 {
|
||||||
|
mappings[t.EntityName][uid] = utils.GenerateID()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Infof("[MigrationV3] Pass 1: 构建关键表 %s 的 ID 映射, 共 %d 条", actualName, len(mappings[t.EntityName]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// 第二、三阶段:正式转换数据并处理关联字段 (Pass 2 & 3)
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
for _, t := range allTables {
|
||||||
|
actualName := getTableName(tx, t.Model)
|
||||||
|
oldTableName := actualName + "_v2_bak"
|
||||||
|
|
||||||
|
if !tx.Migrator().HasTable(oldTableName) {
|
||||||
|
// 虽然可能已经改过格式,但为了安全还是 AutoMigrate 一下
|
||||||
|
tx.AutoMigrate(t.Model)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("[MigrationV3] Pass 2&3: 正在转换数据并修复关联: %s", actualName)
|
||||||
|
tx.AutoMigrate(t.Model)
|
||||||
|
|
||||||
|
// 获取新表的有效列名(小写)
|
||||||
|
columnTypes, _ := tx.Migrator().ColumnTypes(t.Model)
|
||||||
|
validColumns := make(map[string]bool)
|
||||||
|
for _, ct := range columnTypes {
|
||||||
|
validColumns[strings.ToLower(ct.Name())] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldData []map[string]interface{}
|
||||||
|
if err := tx.Table(oldTableName).Find(&oldData).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range oldData {
|
||||||
|
// 1. 处理主键 ID
|
||||||
|
if val, ok := getValFromMap(row, "id"); ok {
|
||||||
|
uid := parseUint(val)
|
||||||
|
if nid, exists := mappings[t.EntityName][uid]; exists {
|
||||||
|
row["id"] = nid
|
||||||
|
} else {
|
||||||
|
row["id"] = utils.GenerateID()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 处理单外键关联 (Phase 3)
|
||||||
|
for field, parentEntity := range t.FKs {
|
||||||
|
columnName := getColumnName(field)
|
||||||
|
if val, ok := getValFromMap(row, columnName); ok && val != nil {
|
||||||
|
ufk := parseUint(val)
|
||||||
|
if ufk > 0 {
|
||||||
|
if nid, exists := mappings[parentEntity][ufk]; exists {
|
||||||
|
row[columnName] = nid
|
||||||
|
} else {
|
||||||
|
row[columnName] = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
row[columnName] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 处理复合多外键字段 (envs)
|
||||||
|
for field, parentEntity := range t.MultiFKs {
|
||||||
|
columnName := getColumnName(field)
|
||||||
|
if val, ok := getValFromMap(row, columnName); ok && val != nil {
|
||||||
|
if strVal, ok := val.(string); ok && strVal != "" {
|
||||||
|
row[columnName] = transformMultiIDs(strVal, parentEntity, mappings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 过滤不存在的列并插入
|
||||||
|
filteredRow := make(map[string]interface{})
|
||||||
|
for k, v := range row {
|
||||||
|
if validColumns[strings.ToLower(k)] {
|
||||||
|
filteredRow[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.Table(actualName).Create(filteredRow).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迁移完成,清理备份表
|
||||||
|
tx.Migrator().DropTable(oldTableName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:解析各种数字 ID
|
||||||
|
func parseUint(val interface{}) uint {
|
||||||
|
if val == nil { return 0 }
|
||||||
|
switch v := val.(type) {
|
||||||
|
case uint: return v
|
||||||
|
case int64: return uint(v)
|
||||||
|
case int: return uint(v)
|
||||||
|
case uint64: return uint(v)
|
||||||
|
case float64: return uint(v)
|
||||||
|
case string:
|
||||||
|
var u uint
|
||||||
|
fmt.Sscanf(v, "%d", &u)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:字段名转列名
|
||||||
|
func getColumnName(field string) string {
|
||||||
|
switch field {
|
||||||
|
case "AgentID": return "agent_id"
|
||||||
|
case "TaskID": return "task_id"
|
||||||
|
case "UserID": return "user_id"
|
||||||
|
case "LogID": return "log_id"
|
||||||
|
case "Envs": return "envs"
|
||||||
|
default: return strings.ToLower(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:处理逗号分隔的 ID 列表
|
||||||
|
func transformMultiIDs(oldStr string, parentEntity string, mappings map[string]map[uint]string) string {
|
||||||
|
parts := strings.Split(oldStr, ",")
|
||||||
|
var result []string
|
||||||
|
for _, p := range parts {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p == "" { continue }
|
||||||
|
if len(p) == 20 && !utils.IsNumeric(p) {
|
||||||
|
result = append(result, p) // 已经是 xid,保留
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uid := parseUint(p)
|
||||||
|
if uid > 0 {
|
||||||
|
if nid, exists := mappings[parentEntity][uid]; exists {
|
||||||
|
result = append(result, nid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(result, ",")
|
||||||
|
}
|
||||||
@@ -283,7 +283,7 @@ func (s *MiseService) syncToDB(languages []MiseLanguage) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentIds []uint
|
var currentIds []string
|
||||||
for _, lang := range languages {
|
for _, lang := range languages {
|
||||||
var model models.Language
|
var model models.Language
|
||||||
// 以 plugin 和 version 作为联合唯一标识(业务逻辑上)
|
// 以 plugin 和 version 作为联合唯一标识(业务逻辑上)
|
||||||
@@ -308,6 +308,7 @@ func (s *MiseService) syncToDB(languages []MiseLanguage) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// 如果不存在,则创建
|
// 如果不存在,则创建
|
||||||
newLang := models.Language{
|
newLang := models.Language{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
Plugin: lang.Plugin,
|
Plugin: lang.Plugin,
|
||||||
Version: lang.Version,
|
Version: lang.Version,
|
||||||
InstallPath: lang.InstallPath,
|
InstallPath: lang.InstallPath,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package services
|
|||||||
import (
|
import (
|
||||||
"github.com/engigu/baihu-panel/internal/database"
|
"github.com/engigu/baihu-panel/internal/database"
|
||||||
"github.com/engigu/baihu-panel/internal/models"
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ScriptService struct{}
|
type ScriptService struct{}
|
||||||
@@ -11,33 +12,34 @@ func NewScriptService() *ScriptService {
|
|||||||
return &ScriptService{}
|
return &ScriptService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ScriptService) CreateScript(name, content string, userID int) *models.Script {
|
func (ss *ScriptService) CreateScript(name, content string, userID string) *models.Script {
|
||||||
script := &models.Script{
|
script := &models.Script{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
Name: name,
|
Name: name,
|
||||||
Content: content,
|
Content: content,
|
||||||
UserID: uint(userID),
|
UserID: userID,
|
||||||
}
|
}
|
||||||
database.DB.Create(script)
|
database.DB.Create(script)
|
||||||
return script
|
return script
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ScriptService) GetScriptsByUserID(userID int) []models.Script {
|
func (ss *ScriptService) GetScriptsByUserID(userID string) []models.Script {
|
||||||
var scripts []models.Script
|
var scripts []models.Script
|
||||||
database.DB.Where("user_id = ?", userID).Find(&scripts)
|
database.DB.Where("user_id = ?", userID).Find(&scripts)
|
||||||
return scripts
|
return scripts
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ScriptService) GetScriptByID(id int) *models.Script {
|
func (ss *ScriptService) GetScriptByID(id string) *models.Script {
|
||||||
var script models.Script
|
var script models.Script
|
||||||
if err := database.DB.First(&script, id).Error; err != nil {
|
if err := database.DB.Where("id = ?", id).First(&script).Error; err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &script
|
return &script
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ScriptService) UpdateScript(id int, name, content string) *models.Script {
|
func (ss *ScriptService) UpdateScript(id string, name, content string) *models.Script {
|
||||||
var script models.Script
|
var script models.Script
|
||||||
if err := database.DB.First(&script, id).Error; err != nil {
|
if err := database.DB.Where("id = ?", id).First(&script).Error; err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
script.Name = name
|
script.Name = name
|
||||||
@@ -46,7 +48,7 @@ func (ss *ScriptService) UpdateScript(id int, name, content string) *models.Scri
|
|||||||
return &script
|
return &script
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ScriptService) DeleteScript(id int) bool {
|
func (ss *ScriptService) DeleteScript(id string) bool {
|
||||||
result := database.DB.Delete(&models.Script{}, id)
|
result := database.DB.Where("id = ?", id).Delete(&models.Script{})
|
||||||
return result.RowsAffected > 0
|
return result.RowsAffected > 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/engigu/baihu-panel/internal/database"
|
"github.com/engigu/baihu-panel/internal/database"
|
||||||
"github.com/engigu/baihu-panel/internal/models"
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
"github.com/engigu/baihu-panel/internal/systime"
|
"github.com/engigu/baihu-panel/internal/systime"
|
||||||
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SendStatsService struct{}
|
type SendStatsService struct{}
|
||||||
@@ -15,7 +16,7 @@ func NewSendStatsService() *SendStatsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IncrementStats 增加任务执行统计
|
// IncrementStats 增加任务执行统计
|
||||||
func (s *SendStatsService) IncrementStats(taskID uint, status string) error {
|
func (s *SendStatsService) IncrementStats(taskID string, status string) error {
|
||||||
day := systime.FormatDate(time.Now())
|
day := systime.FormatDate(time.Now())
|
||||||
|
|
||||||
var stats models.SendStats
|
var stats models.SendStats
|
||||||
@@ -24,6 +25,7 @@ func (s *SendStatsService) IncrementStats(taskID uint, status string) error {
|
|||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
// 不存在则创建
|
// 不存在则创建
|
||||||
stats = models.SendStats{
|
stats = models.SendStats{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
TaskID: taskID,
|
TaskID: taskID,
|
||||||
Day: day,
|
Day: day,
|
||||||
Status: status,
|
Status: status,
|
||||||
@@ -37,7 +39,7 @@ func (s *SendStatsService) IncrementStats(taskID uint, status string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetStatsByTaskID 获取任务的统计数据
|
// GetStatsByTaskID 获取任务的统计数据
|
||||||
func (s *SendStatsService) GetStatsByTaskID(taskID uint) []models.SendStats {
|
func (s *SendStatsService) GetStatsByTaskID(taskID string) []models.SendStats {
|
||||||
var stats []models.SendStats
|
var stats []models.SendStats
|
||||||
database.DB.Where("task_id = ?", taskID).Order("day DESC").Find(&stats)
|
database.DB.Where("task_id = ?", taskID).Order("day DESC").Find(&stats)
|
||||||
return stats
|
return stats
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ func (s *SettingsService) InitSettings() error {
|
|||||||
var count int64
|
var count int64
|
||||||
database.DB.Model(&models.Setting{}).Where("section = ? AND `key` = ?", section, key).Count(&count)
|
database.DB.Model(&models.Setting{}).Where("section = ? AND `key` = ?", section, key).Count(&count)
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
if err := database.DB.Create(&models.Setting{Section: section, Key: key, Value: value}).Error; err != nil {
|
if err := database.DB.Create(&models.Setting{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
|
Section: section,
|
||||||
|
Key: key,
|
||||||
|
Value: value,
|
||||||
|
}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +43,12 @@ func (s *SettingsService) InitSettings() error {
|
|||||||
} else {
|
} else {
|
||||||
secretValue = utils.RandomString(32)
|
secretValue = utils.RandomString(32)
|
||||||
}
|
}
|
||||||
if err := database.DB.Create(&models.Setting{Section: constant.SectionSecurity, Key: constant.KeySecret, Value: secretValue}).Error; err != nil {
|
if err := database.DB.Create(&models.Setting{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
|
Section: constant.SectionSecurity,
|
||||||
|
Key: constant.KeySecret,
|
||||||
|
Value: secretValue,
|
||||||
|
}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -69,7 +79,12 @@ func (s *SettingsService) Get(section, key string) string {
|
|||||||
func (s *SettingsService) Set(section, key, value string) error {
|
func (s *SettingsService) Set(section, key, value string) error {
|
||||||
var setting models.Setting
|
var setting models.Setting
|
||||||
if database.DB.Where("section = ? AND `key` = ?", section, key).First(&setting).Error != nil {
|
if database.DB.Where("section = ? AND `key` = ?", section, key).First(&setting).Error != nil {
|
||||||
return database.DB.Create(&models.Setting{Section: section, Key: key, Value: value}).Error
|
return database.DB.Create(&models.Setting{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
|
Section: section,
|
||||||
|
Key: key,
|
||||||
|
Value: value,
|
||||||
|
}).Error
|
||||||
}
|
}
|
||||||
return database.DB.Model(&setting).Update("value", value).Error
|
return database.DB.Model(&setting).Update("value", value).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ import (
|
|||||||
|
|
||||||
// AgentWSManager 接口定义(避免循环依赖)
|
// AgentWSManager 接口定义(避免循环依赖)
|
||||||
type AgentWSManager interface {
|
type AgentWSManager interface {
|
||||||
RegisterRemoteWaiter(logID uint) chan *models.AgentTaskResult
|
RegisterRemoteWaiter(logID string) chan *models.AgentTaskResult
|
||||||
UnregisterRemoteWaiter(logID uint)
|
UnregisterRemoteWaiter(logID string)
|
||||||
SendToAgent(agentID uint, msgType string, data interface{}) error
|
SendToAgent(agentID string, msgType string, data interface{}) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// SettingsService 接口定义(避免循环依赖)
|
// SettingsService 接口定义(避免循环依赖)
|
||||||
@@ -115,10 +115,9 @@ func (h *ServerSchedulerHandler) OnTaskScheduled(req *executor.ExecutionRequest)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ServerSchedulerHandler) OnTaskExecuting(req *executor.ExecutionRequest) (io.Writer, io.Writer, error) {
|
func (h *ServerSchedulerHandler) OnTaskExecuting(req *executor.ExecutionRequest) (io.Writer, io.Writer, error) {
|
||||||
var taskID uint
|
taskID := req.TaskID
|
||||||
fmt.Sscanf(req.TaskID, "%d", &taskID)
|
|
||||||
|
|
||||||
task := h.es.taskService.GetTaskByID(int(taskID))
|
task := h.es.taskService.GetTaskByID(taskID)
|
||||||
// 系统任务(无 taskID)不记录数据库日志,直接返回空写入器
|
// 系统任务(无 taskID)不记录数据库日志,直接返回空写入器
|
||||||
if task == nil {
|
if task == nil {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
@@ -168,7 +167,7 @@ func (h *ServerSchedulerHandler) OnTaskExecuting(req *executor.ExecutionRequest)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ServerSchedulerHandler) OnTaskHeartbeat(req *executor.ExecutionRequest, duration int64) {
|
func (h *ServerSchedulerHandler) OnTaskHeartbeat(req *executor.ExecutionRequest, duration int64) {
|
||||||
if req.LogID > 0 {
|
if req.LogID != "" {
|
||||||
h.es.taskLogService.UpdateTaskDuration(req.LogID, duration)
|
h.es.taskLogService.UpdateTaskDuration(req.LogID, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,14 +183,13 @@ func (h *ServerSchedulerHandler) OnTaskStarted(req *executor.ExecutionRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest, result *executor.ExecutionResult) {
|
func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest, result *executor.ExecutionResult) {
|
||||||
if req.LogID == 0 {
|
if req.LogID == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var taskID uint
|
taskID := req.TaskID
|
||||||
fmt.Sscanf(req.TaskID, "%d", &taskID)
|
|
||||||
|
|
||||||
task := h.es.taskService.GetTaskByID(int(taskID))
|
task := h.es.taskService.GetTaskByID(taskID)
|
||||||
if task == nil {
|
if task == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -204,7 +202,7 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest,
|
|||||||
var err error
|
var err error
|
||||||
output, err = tl.CompressAndCleanup()
|
output, err = tl.CompressAndCleanup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("[Executor] 压缩任务 #%d 日志失败: %v", task.ID, err)
|
logger.Errorf("[Executor] 压缩任务 #%s 日志失败: %v", task.ID, err)
|
||||||
output = "[System Error] 日志处理失败: " + err.Error()
|
output = "[System Error] 日志处理失败: " + err.Error()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -230,7 +228,7 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果有 AgentID,也记录下来
|
// 如果有 AgentID,也记录下来
|
||||||
if task.AgentID != nil && *task.AgentID > 0 {
|
if task.AgentID != nil && *task.AgentID != "" {
|
||||||
agentID := *task.AgentID
|
agentID := *task.AgentID
|
||||||
taskLog.AgentID = &agentID
|
taskLog.AgentID = &agentID
|
||||||
}
|
}
|
||||||
@@ -251,12 +249,11 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, err error) {
|
func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, err error) {
|
||||||
if req.LogID == 0 {
|
if req.LogID == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var taskID uint
|
taskID := req.TaskID
|
||||||
fmt.Sscanf(req.TaskID, "%d", &taskID)
|
|
||||||
|
|
||||||
// 移除运行记录
|
// 移除运行记录
|
||||||
if req.Metadata.GoID != 0 {
|
if req.Metadata.GoID != 0 {
|
||||||
@@ -288,8 +285,8 @@ func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 补充 AgentID
|
// 补充 AgentID
|
||||||
task := h.es.taskService.GetTaskByID(int(taskID))
|
task := h.es.taskService.GetTaskByID(taskID)
|
||||||
if task != nil && task.AgentID != nil && *task.AgentID > 0 {
|
if task != nil && task.AgentID != nil && *task.AgentID != "" {
|
||||||
agentID := *task.AgentID
|
agentID := *task.AgentID
|
||||||
taskLog.AgentID = &agentID
|
taskLog.AgentID = &agentID
|
||||||
}
|
}
|
||||||
@@ -321,10 +318,10 @@ func (es *ExecutorService) HandleTaskRetry(task *models.Task, req *executor.Exec
|
|||||||
|
|
||||||
if retryIndex < task.RetryCount {
|
if retryIndex < task.RetryCount {
|
||||||
retryIndex++
|
retryIndex++
|
||||||
logger.Infof("[Executor] 任务 #%d 执行失败/出错,将在 %d 秒后进行第 %d/%d 次重试...", task.ID, task.RetryInterval, retryIndex, task.RetryCount)
|
logger.Infof("[Executor] 任务 #%s 执行失败/出错,将在 %d 秒后进行第 %d/%d 次重试...", task.ID, task.RetryInterval, retryIndex, task.RetryCount)
|
||||||
|
|
||||||
es.scheduler.EnqueueDelayed(time.Duration(task.RetryInterval)*time.Second, func() *executor.ExecutionRequest {
|
es.scheduler.EnqueueDelayed(time.Duration(task.RetryInterval)*time.Second, func() *executor.ExecutionRequest {
|
||||||
latestTask := es.taskService.GetTaskByID(int(task.ID))
|
latestTask := es.taskService.GetTaskByID(task.ID)
|
||||||
if latestTask == nil || !latestTask.Enabled {
|
if latestTask == nil || !latestTask.Enabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -350,8 +347,7 @@ func (es *ExecutorService) HandleTaskRetry(task *models.Task, req *executor.Exec
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ServerSchedulerHandler) OnCronNextRun(req *executor.ExecutionRequest, nextRun time.Time) {
|
func (h *ServerSchedulerHandler) OnCronNextRun(req *executor.ExecutionRequest, nextRun time.Time) {
|
||||||
var taskID uint
|
taskID := req.TaskID
|
||||||
fmt.Sscanf(req.TaskID, "%d", &taskID)
|
|
||||||
// 更新数据库中的下次运行时间
|
// 更新数据库中的下次运行时间
|
||||||
database.DB.Model(&models.Task{}).Where("id = ?", taskID).Update("next_run", nextRun)
|
database.DB.Model(&models.Task{}).Where("id = ?", taskID).Update("next_run", nextRun)
|
||||||
}
|
}
|
||||||
@@ -359,19 +355,19 @@ func (h *ServerSchedulerHandler) OnCronNextRun(req *executor.ExecutionRequest, n
|
|||||||
// LocalTaskHooks 本地任务钩子适配器
|
// LocalTaskHooks 本地任务钩子适配器
|
||||||
type LocalTaskHooks struct {
|
type LocalTaskHooks struct {
|
||||||
es *ExecutorService
|
es *ExecutorService
|
||||||
logID uint
|
logID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *LocalTaskHooks) PreExecute(ctx context.Context, req executor.Request) (uint, error) {
|
func (h *LocalTaskHooks) PreExecute(ctx context.Context, req executor.Request) (string, error) {
|
||||||
return h.logID, nil
|
return h.logID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *LocalTaskHooks) PostExecute(ctx context.Context, logID uint, result *executor.Result) error {
|
func (h *LocalTaskHooks) PostExecute(ctx context.Context, logID string, result *executor.Result) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *LocalTaskHooks) OnHeartbeat(ctx context.Context, logID uint, duration int64) error {
|
func (h *LocalTaskHooks) OnHeartbeat(ctx context.Context, logID string, duration int64) error {
|
||||||
if logID > 0 {
|
if logID != "" {
|
||||||
return h.es.taskLogService.UpdateTaskDuration(logID, duration)
|
return h.es.taskLogService.UpdateTaskDuration(logID, duration)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -379,10 +375,9 @@ func (h *LocalTaskHooks) OnHeartbeat(ctx context.Context, logID uint, duration i
|
|||||||
|
|
||||||
// ExecuteDispatcher 实现任务分发逻辑
|
// ExecuteDispatcher 实现任务分发逻辑
|
||||||
func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor.ExecutionRequest, stdout, stderr io.Writer) (*executor.Result, error) {
|
func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor.ExecutionRequest, stdout, stderr io.Writer) (*executor.Result, error) {
|
||||||
var taskID uint
|
taskID := req.TaskID
|
||||||
fmt.Sscanf(req.TaskID, "%d", &taskID)
|
|
||||||
|
|
||||||
task := es.taskService.GetTaskByID(int(taskID))
|
task := es.taskService.GetTaskByID(taskID)
|
||||||
// 系统任务(无 taskID)直接本地执行
|
// 系统任务(无 taskID)直接本地执行
|
||||||
if task == nil {
|
if task == nil {
|
||||||
return executor.Execute(ctx, executor.Request{
|
return executor.Execute(ctx, executor.Request{
|
||||||
@@ -409,7 +404,7 @@ func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 远程任务
|
// 远程任务
|
||||||
if task.AgentID != nil && *task.AgentID > 0 {
|
if task.AgentID != nil && *task.AgentID != "" {
|
||||||
return es.ExecuteRemoteForScheduler(task, req.LogID)
|
return es.ExecuteRemoteForScheduler(task, req.LogID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,8 +462,8 @@ func (es *ExecutorService) AddCronTask(task *models.Task) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemoveCronTask 移除计划任务
|
// RemoveCronTask 移除计划任务
|
||||||
func (es *ExecutorService) RemoveCronTask(taskID uint) {
|
func (es *ExecutorService) RemoveCronTask(taskID string) {
|
||||||
es.cronManager.RemoveTask(fmt.Sprintf("%d", taskID))
|
es.cronManager.RemoveTask(taskID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateCron 验证 Cron 表达式
|
// ValidateCron 验证 Cron 表达式
|
||||||
@@ -494,10 +489,10 @@ func (es *ExecutorService) loadCronTasks() {
|
|||||||
go func(t models.Task) {
|
go func(t models.Task) {
|
||||||
// 延迟一点时间再触发,确保系统完全启动
|
// 延迟一点时间再触发,确保系统完全启动
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(3 * time.Second)
|
||||||
logger.Infof("[Executor] 触发开机服务启动任务 #%d: %s", t.ID, t.Name)
|
logger.Infof("[Executor] 触发开机服务启动任务 #%s: %s", t.ID, t.Name)
|
||||||
es.ExecuteTask(int(t.ID), nil)
|
es.ExecuteTask(t.ID, nil)
|
||||||
}(task)
|
}(task)
|
||||||
} else if task.TriggerType == constant.TriggerTypeCron && task.Schedule != "" && (task.AgentID == nil || *task.AgentID == 0) {
|
} else if task.TriggerType == constant.TriggerTypeCron && task.Schedule != "" && (task.AgentID == nil || *task.AgentID == "") {
|
||||||
// 只调度本地任务(agent_id 为空或 0)的定时任务
|
// 只调度本地任务(agent_id 为空或 0)的定时任务
|
||||||
err := es.cronManager.AddTask(&task)
|
err := es.cronManager.AddTask(&task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -519,11 +514,11 @@ func (es *ExecutorService) Reload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteTask executes a task by ID(同步执行,供 API 调用)
|
// ExecuteTask executes a task by ID(同步执行,供 API 调用)
|
||||||
func (es *ExecutorService) ExecuteTask(taskID int, extraEnvs []string) *executor.ExecutionResult {
|
func (es *ExecutorService) ExecuteTask(taskID string, extraEnvs []string) *executor.ExecutionResult {
|
||||||
task := es.taskService.GetTaskByID(taskID)
|
task := es.taskService.GetTaskByID(taskID)
|
||||||
if task == nil {
|
if task == nil {
|
||||||
return &executor.ExecutionResult{
|
return &executor.ExecutionResult{
|
||||||
TaskID: fmt.Sprintf("%d", taskID),
|
TaskID: taskID,
|
||||||
Success: false,
|
Success: false,
|
||||||
Error: "任务不存在",
|
Error: "任务不存在",
|
||||||
StartTime: time.Now(),
|
StartTime: time.Now(),
|
||||||
@@ -532,9 +527,9 @@ func (es *ExecutorService) ExecuteTask(taskID int, extraEnvs []string) *executor
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. 检查并发
|
// 1. 检查并发
|
||||||
if err := es.CheckConcurrency(uint(taskID)); err != nil {
|
if err := es.CheckConcurrency(taskID); err != nil {
|
||||||
return &executor.ExecutionResult{
|
return &executor.ExecutionResult{
|
||||||
TaskID: fmt.Sprintf("%d", taskID),
|
TaskID: taskID,
|
||||||
Success: false,
|
Success: false,
|
||||||
Error: err.Error(), // 这里会返回 "任务正在运行中,拒绝并行执行"
|
Error: err.Error(), // 这里会返回 "任务正在运行中,拒绝并行执行"
|
||||||
StartTime: time.Now(),
|
StartTime: time.Now(),
|
||||||
@@ -548,7 +543,7 @@ func (es *ExecutorService) ExecuteTask(taskID int, extraEnvs []string) *executor
|
|||||||
}
|
}
|
||||||
|
|
||||||
req := &executor.ExecutionRequest{
|
req := &executor.ExecutionRequest{
|
||||||
TaskID: fmt.Sprintf("%d", task.ID),
|
TaskID: task.ID,
|
||||||
Name: task.Name,
|
Name: task.Name,
|
||||||
Command: task.Command,
|
Command: task.Command,
|
||||||
WorkDir: task.WorkDir,
|
WorkDir: task.WorkDir,
|
||||||
@@ -562,7 +557,7 @@ func (es *ExecutorService) ExecuteTask(taskID int, extraEnvs []string) *executor
|
|||||||
es.scheduler.EnqueueOrExecute(req)
|
es.scheduler.EnqueueOrExecute(req)
|
||||||
|
|
||||||
return &executor.ExecutionResult{
|
return &executor.ExecutionResult{
|
||||||
TaskID: fmt.Sprintf("%d", task.ID),
|
TaskID: task.ID,
|
||||||
Success: true,
|
Success: true,
|
||||||
Status: constant.TaskStatusQueued,
|
Status: constant.TaskStatusQueued,
|
||||||
StartTime: time.Now(),
|
StartTime: time.Now(),
|
||||||
@@ -570,7 +565,7 @@ func (es *ExecutorService) ExecuteTask(taskID int, extraEnvs []string) *executor
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StopTaskExecution stops a running task execution by LogID
|
// StopTaskExecution stops a running task execution by LogID
|
||||||
func (es *ExecutorService) StopTaskExecution(logID uint) error {
|
func (es *ExecutorService) StopTaskExecution(logID string) error {
|
||||||
var taskLog models.TaskLog
|
var taskLog models.TaskLog
|
||||||
if err := database.DB.First(&taskLog, logID).Error; err != nil {
|
if err := database.DB.First(&taskLog, logID).Error; err != nil {
|
||||||
return fmt.Errorf("日志不存在")
|
return fmt.Errorf("日志不存在")
|
||||||
@@ -580,21 +575,21 @@ func (es *ExecutorService) StopTaskExecution(logID uint) error {
|
|||||||
return fmt.Errorf("任务已结束")
|
return fmt.Errorf("任务已结束")
|
||||||
}
|
}
|
||||||
|
|
||||||
task := es.taskService.GetTaskByID(int(taskLog.TaskID))
|
task := es.taskService.GetTaskByID(taskLog.TaskID)
|
||||||
if task == nil {
|
if task == nil {
|
||||||
return fmt.Errorf("任务不存在")
|
return fmt.Errorf("任务不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 远程任务:发送停止指令到 Agent
|
// 远程任务:发送停止指令到 Agent
|
||||||
if task.AgentID != nil && *task.AgentID > 0 {
|
if task.AgentID != nil && *task.AgentID != "" {
|
||||||
logger.Infof("[Executor] 请求停止远程任务 #%d (Agent #%d, LogID: %d)", task.ID, *task.AgentID, logID)
|
logger.Infof("[Executor] 请求停止远程任务 #%s (Agent #%s, LogID: %s)", task.ID, *task.AgentID, logID)
|
||||||
return es.agentWSManager.SendToAgent(*task.AgentID, constant.WSTypeStop, map[string]interface{}{
|
return es.agentWSManager.SendToAgent(*task.AgentID, constant.WSTypeStop, map[string]interface{}{
|
||||||
"log_id": logID,
|
"log_id": logID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本地任务:直接停止调度器中的执行实例
|
// 本地任务:直接停止调度器中的执行实例
|
||||||
logger.Infof("[Executor] 请求停止本地任务 #%d (LogID: %d)", task.ID, logID)
|
logger.Infof("[Executor] 请求停止本地任务 #%s (LogID: %s)", task.ID, logID)
|
||||||
if es.scheduler.StopLog(logID) {
|
if es.scheduler.StopLog(logID) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -658,7 +653,7 @@ func (es *ExecutorService) UpdateResult(res executor.ExecutionResult) {
|
|||||||
|
|
||||||
// 查找是否已存在(通过 LogID)
|
// 查找是否已存在(通过 LogID)
|
||||||
for i := range es.results {
|
for i := range es.results {
|
||||||
if es.results[i].LogID == res.LogID && res.LogID != 0 {
|
if es.results[i].LogID == res.LogID && res.LogID != "" {
|
||||||
es.results[i] = res
|
es.results[i] = res
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -703,9 +698,9 @@ func (es *ExecutorService) CleanupRunningTasks() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckConcurrency 检查任务并发限制(只读检查)
|
// CheckConcurrency 检查任务并发限制(只读检查)
|
||||||
func (es *ExecutorService) CheckConcurrency(taskID uint) error {
|
func (es *ExecutorService) CheckConcurrency(taskID string) error {
|
||||||
var task models.Task
|
var task models.Task
|
||||||
if err := database.DB.Select("config, running_go").First(&task, taskID).Error; err != nil {
|
if err := database.DB.Select("config, running_go").Where("id = ?", taskID).First(&task).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var goids []int64
|
var goids []int64
|
||||||
@@ -725,11 +720,11 @@ func (es *ExecutorService) CheckConcurrency(taskID uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddRunningGo 添加当前 goroutine ID 到任务的 running_go 字段
|
// AddRunningGo 添加当前 goroutine ID 到任务的 running_go 字段
|
||||||
func (es *ExecutorService) AddRunningGo(taskID uint) (int64, error) {
|
func (es *ExecutorService) AddRunningGo(taskID string) (int64, error) {
|
||||||
goid := utils.GetGoroutineID()
|
goid := utils.GetGoroutineID()
|
||||||
err := database.DB.Transaction(func(tx *gorm.DB) error {
|
err := database.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
var task models.Task
|
var task models.Task
|
||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&task, taskID).Error; err != nil {
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", taskID).First(&task).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var goids []int64
|
var goids []int64
|
||||||
@@ -756,10 +751,10 @@ func (es *ExecutorService) AddRunningGo(taskID uint) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemoveRunningGo 从任务的 running_go 字段移除指定 goroutine ID
|
// RemoveRunningGo 从任务的 running_go 字段移除指定 goroutine ID
|
||||||
func (es *ExecutorService) RemoveRunningGo(taskID uint, goid int64) {
|
func (es *ExecutorService) RemoveRunningGo(taskID string, goid int64) {
|
||||||
database.DB.Transaction(func(tx *gorm.DB) error {
|
database.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
var task models.Task
|
var task models.Task
|
||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&task, taskID).Error; err != nil {
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", taskID).First(&task).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var goids []int64
|
var goids []int64
|
||||||
@@ -778,17 +773,17 @@ func (es *ExecutorService) RemoveRunningGo(taskID uint, goid int64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteRemoteForScheduler 供 Scheduler 调用,执行远程任务并等待结果
|
// ExecuteRemoteForScheduler 供 Scheduler 调用,执行远程任务并等待结果
|
||||||
func (es *ExecutorService) ExecuteRemoteForScheduler(task *models.Task, logID uint) (*executor.Result, error) {
|
func (es *ExecutorService) ExecuteRemoteForScheduler(task *models.Task, logID string) (*executor.Result, error) {
|
||||||
agentID := *task.AgentID
|
agentID := *task.AgentID
|
||||||
logger.Infof("[Executor] 远程执行任务 #%d: %s (Agent #%d, LogID: %d)", task.ID, task.Name, agentID, logID)
|
logger.Infof("[Executor] 远程执行任务 #%s: %s (Agent #%s, LogID: %s)", task.ID, task.Name, agentID, logID)
|
||||||
|
|
||||||
// 1. 检查 Agent 状态
|
// 1. 检查 Agent 状态
|
||||||
var agent models.Agent
|
var agent models.Agent
|
||||||
if err := database.DB.First(&agent, agentID).Error; err != nil {
|
if err := database.DB.Where("id = ?", agentID).First(&agent).Error; err != nil {
|
||||||
return nil, fmt.Errorf("Agent #%d 不存在", agentID)
|
return nil, fmt.Errorf("Agent #%s 不存在", agentID)
|
||||||
}
|
}
|
||||||
if !agent.Enabled {
|
if !agent.Enabled {
|
||||||
return nil, fmt.Errorf("Agent #%d 已禁用", agentID)
|
return nil, fmt.Errorf("Agent #%s 已禁用", agentID)
|
||||||
}
|
}
|
||||||
if es.agentWSManager == nil {
|
if es.agentWSManager == nil {
|
||||||
return nil, fmt.Errorf("AgentWSManager 未初始化")
|
return nil, fmt.Errorf("AgentWSManager 未初始化")
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
// SendStatsService 接口定义(避免循环依赖)
|
// SendStatsService 接口定义(避免循环依赖)
|
||||||
type SendStatsService interface {
|
type SendStatsService interface {
|
||||||
IncrementStats(taskID uint, status string) error
|
IncrementStats(taskID string, status string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskLogService 任务日志服务
|
// TaskLogService 任务日志服务
|
||||||
@@ -35,9 +35,10 @@ type CleanConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateEmptyLog 创建一个空的日志记录(任务开始时调用)
|
// CreateEmptyLog 创建一个空的日志记录(任务开始时调用)
|
||||||
func (s *TaskLogService) CreateEmptyLog(taskID uint, command string) (*models.TaskLog, error) {
|
func (s *TaskLogService) CreateEmptyLog(taskID string, command string) (*models.TaskLog, error) {
|
||||||
startTime := models.Now()
|
startTime := models.Now()
|
||||||
taskLog := &models.TaskLog{
|
taskLog := &models.TaskLog{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
TaskID: taskID,
|
TaskID: taskID,
|
||||||
Command: command,
|
Command: command,
|
||||||
Status: "running",
|
Status: "running",
|
||||||
@@ -52,9 +53,10 @@ func (s *TaskLogService) CreateEmptyLog(taskID uint, command string) (*models.Ta
|
|||||||
// SaveTaskLog 保存或更新任务日志
|
// SaveTaskLog 保存或更新任务日志
|
||||||
func (s *TaskLogService) SaveTaskLog(taskLog *models.TaskLog) error {
|
func (s *TaskLogService) SaveTaskLog(taskLog *models.TaskLog) error {
|
||||||
var err error
|
var err error
|
||||||
if taskLog.ID > 0 {
|
if taskLog.ID != "" {
|
||||||
err = database.DB.Model(taskLog).Updates(taskLog).Error
|
err = database.DB.Model(taskLog).Where("id = ?", taskLog.ID).Updates(taskLog).Error
|
||||||
} else {
|
} else {
|
||||||
|
taskLog.ID = utils.GenerateID()
|
||||||
err = database.DB.Create(taskLog).Error
|
err = database.DB.Create(taskLog).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,12 +71,12 @@ func (s *TaskLogService) SaveTaskLog(taskLog *models.TaskLog) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTaskDuration 更新任务耗时(心跳)
|
// UpdateTaskDuration 更新任务耗时(心跳)
|
||||||
func (s *TaskLogService) UpdateTaskDuration(logID uint, duration int64) error {
|
func (s *TaskLogService) UpdateTaskDuration(logID string, duration int64) error {
|
||||||
return database.DB.Model(&models.TaskLog{}).Where("id = ?", logID).Update("duration", duration).Error
|
return database.DB.Model(&models.TaskLog{}).Where("id = ?", logID).Update("duration", duration).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTaskStats 更新任务统计
|
// UpdateTaskStats 更新任务统计
|
||||||
func (s *TaskLogService) UpdateTaskStats(taskID uint, status string) {
|
func (s *TaskLogService) UpdateTaskStats(taskID string, status string) {
|
||||||
if s.sendStatsService == nil {
|
if s.sendStatsService == nil {
|
||||||
logger.Error("[TaskLog] SendStatsService 未初始化")
|
logger.Error("[TaskLog] SendStatsService 未初始化")
|
||||||
return
|
return
|
||||||
@@ -87,9 +89,9 @@ func (s *TaskLogService) UpdateTaskStats(taskID uint, status string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CleanTaskLogs 清理任务日志
|
// CleanTaskLogs 清理任务日志
|
||||||
func (s *TaskLogService) CleanTaskLogs(taskID uint) {
|
func (s *TaskLogService) CleanTaskLogs(taskID string) {
|
||||||
var task models.Task
|
var task models.Task
|
||||||
if err := database.DB.First(&task, taskID).Error; err != nil {
|
if err := database.DB.Where("id = ?", taskID).First(&task).Error; err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +125,7 @@ func (s *TaskLogService) CleanTaskLogs(taskID uint) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if deleted > 0 {
|
if deleted > 0 {
|
||||||
logger.Infof("[TaskLog] 清理任务 #%d 的 %d 条日志", taskID, deleted)
|
logger.Infof("[TaskLog] 清理任务 #%s 的 %d 条日志", taskID, deleted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +155,7 @@ func (s *TaskLogService) CreateTaskLogFromAgentResult(result *models.AgentTaskRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
taskLog := &models.TaskLog{
|
taskLog := &models.TaskLog{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
TaskID: result.TaskID,
|
TaskID: result.TaskID,
|
||||||
AgentID: &result.AgentID,
|
AgentID: &result.AgentID,
|
||||||
Command: result.Command,
|
Command: result.Command,
|
||||||
@@ -177,7 +180,7 @@ func (s *TaskLogService) CreateTaskLogFromAgentResult(result *models.AgentTaskRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateTaskLogFromLocalExecution 从本地执行结果创建任务日志
|
// CreateTaskLogFromLocalExecution 从本地执行结果创建任务日志
|
||||||
func (s *TaskLogService) CreateTaskLogFromLocalExecution(taskID uint, command, output, systemErr, status string, duration int64, exitCode int, start, end time.Time, isCompressed bool) (*models.TaskLog, error) {
|
func (s *TaskLogService) CreateTaskLogFromLocalExecution(taskID string, command, output, systemErr, status string, duration int64, exitCode int, start, end time.Time, isCompressed bool) (*models.TaskLog, error) {
|
||||||
var compressed string
|
var compressed string
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -196,6 +199,7 @@ func (s *TaskLogService) CreateTaskLogFromLocalExecution(taskID uint, command, o
|
|||||||
endTime := models.LocalTime(end)
|
endTime := models.LocalTime(end)
|
||||||
|
|
||||||
taskLog := &models.TaskLog{
|
taskLog := &models.TaskLog{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
TaskID: taskID,
|
TaskID: taskID,
|
||||||
Command: command,
|
Command: command,
|
||||||
Output: compressed,
|
Output: compressed,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/engigu/baihu-panel/internal/constant"
|
"github.com/engigu/baihu-panel/internal/constant"
|
||||||
"github.com/engigu/baihu-panel/internal/database"
|
"github.com/engigu/baihu-panel/internal/database"
|
||||||
"github.com/engigu/baihu-panel/internal/models"
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TaskService struct{}
|
type TaskService struct{}
|
||||||
@@ -12,7 +13,7 @@ func NewTaskService() *TaskService {
|
|||||||
return &TaskService{}
|
return &TaskService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *uint, languages []map[string]string, triggerType string, tags string, retryCount int, retryInterval int, randomRange int) *models.Task {
|
func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *string, languages []map[string]string, triggerType string, tags string, retryCount int, retryInterval int, randomRange int) *models.Task {
|
||||||
if taskType == "" {
|
if taskType == "" {
|
||||||
taskType = "task"
|
taskType = "task"
|
||||||
}
|
}
|
||||||
@@ -20,6 +21,7 @@ func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, w
|
|||||||
triggerType = constant.TriggerTypeCron
|
triggerType = constant.TriggerTypeCron
|
||||||
}
|
}
|
||||||
task := &models.Task{
|
task := &models.Task{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
Name: name,
|
Name: name,
|
||||||
Command: command,
|
Command: command,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
@@ -52,7 +54,7 @@ func (ts *TaskService) GetTasks() []models.Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTasksWithPagination 分页获取任务列表
|
// GetTasksWithPagination 分页获取任务列表
|
||||||
func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string, agentID *uint, tags string, taskType string) ([]models.Task, int64) {
|
func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string, agentID *string, tags string, taskType string) ([]models.Task, int64) {
|
||||||
var tasks []models.Task
|
var tasks []models.Task
|
||||||
var total int64
|
var total int64
|
||||||
|
|
||||||
@@ -76,17 +78,17 @@ func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string, a
|
|||||||
return tasks, total
|
return tasks, total
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TaskService) GetTaskByID(id int) *models.Task {
|
func (ts *TaskService) GetTaskByID(id string) *models.Task {
|
||||||
var task models.Task
|
var task models.Task
|
||||||
if err := database.DB.First(&task, id).Error; err != nil {
|
if err := database.DB.Where("id = ?", id).First(&task).Error; err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &task
|
return &task
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *uint, languages []map[string]string, triggerType string, tags string, retryCount int, retryInterval int, randomRange int) *models.Task {
|
func (ts *TaskService) UpdateTask(id string, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *string, languages []map[string]string, triggerType string, tags string, retryCount int, retryInterval int, randomRange int) *models.Task {
|
||||||
var task models.Task
|
var task models.Task
|
||||||
if err := database.DB.First(&task, id).Error; err != nil {
|
if err := database.DB.Where("id = ?", id).First(&task).Error; err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
task.Name = name
|
task.Name = name
|
||||||
@@ -117,7 +119,7 @@ func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeou
|
|||||||
return &task
|
return &task
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TaskService) DeleteTask(id int) bool {
|
func (ts *TaskService) DeleteTask(id string) bool {
|
||||||
result := database.DB.Delete(&models.Task{}, id)
|
result := database.DB.Where("id = ?", id).Delete(&models.Task{})
|
||||||
return result.RowsAffected > 0
|
return result.RowsAffected > 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ import (
|
|||||||
var (
|
var (
|
||||||
// globalTinyLogManager 跟踪所有活跃的 TinyLog 实例
|
// globalTinyLogManager 跟踪所有活跃的 TinyLog 实例
|
||||||
globalTinyLogManager = &TinyLogManager{
|
globalTinyLogManager = &TinyLogManager{
|
||||||
logs: make(map[uint]*TinyLog),
|
logs: make(map[string]*TinyLog),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type TinyLogManager struct {
|
type TinyLogManager struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
logs map[uint]*TinyLog
|
logs map[string]*TinyLog
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *TinyLogManager) Register(log *TinyLog) {
|
func (m *TinyLogManager) Register(log *TinyLog) {
|
||||||
@@ -31,26 +31,26 @@ func (m *TinyLogManager) Register(log *TinyLog) {
|
|||||||
m.logs[log.LogID] = log
|
m.logs[log.LogID] = log
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *TinyLogManager) Unregister(logID uint) {
|
func (m *TinyLogManager) Unregister(logID string) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
delete(m.logs, logID)
|
delete(m.logs, logID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *TinyLogManager) Get(logID uint) *TinyLog {
|
func (m *TinyLogManager) Get(logID string) *TinyLog {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
return m.logs[logID]
|
return m.logs[logID]
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveLog 通过 ID 获取活跃的 TinyLog 实例
|
// GetActiveLog 通过 ID 获取活跃的 TinyLog 实例
|
||||||
func GetActiveLog(logID uint) *TinyLog {
|
func GetActiveLog(logID string) *TinyLog {
|
||||||
return globalTinyLogManager.Get(logID)
|
return globalTinyLogManager.Get(logID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TinyLog 是一个高性能、低内存占用的日志收集器
|
// TinyLog 是一个高性能、低内存占用的日志收集器
|
||||||
type TinyLog struct {
|
type TinyLog struct {
|
||||||
LogID uint
|
LogID string
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
file *os.File
|
file *os.File
|
||||||
path string
|
path string
|
||||||
@@ -61,7 +61,7 @@ type TinyLog struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewTinyLog 创建一个新的 TinyLog 实例(基于临时文件存储)并注册它
|
// NewTinyLog 创建一个新的 TinyLog 实例(基于临时文件存储)并注册它
|
||||||
func NewTinyLog(logID uint) (*TinyLog, error) {
|
func NewTinyLog(logID string) (*TinyLog, error) {
|
||||||
f, err := os.CreateTemp("", "task_log_*.log")
|
f, err := os.CreateTemp("", "task_log_*.log")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/engigu/baihu-panel/internal/constant"
|
"github.com/engigu/baihu-panel/internal/constant"
|
||||||
"github.com/engigu/baihu-panel/internal/database"
|
"github.com/engigu/baihu-panel/internal/database"
|
||||||
"github.com/engigu/baihu-panel/internal/models"
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserService struct{}
|
type UserService struct{}
|
||||||
@@ -22,6 +23,7 @@ func (us *UserService) hashPassword(password string) string {
|
|||||||
|
|
||||||
func (us *UserService) CreateUser(username, password, email, role string) *models.User {
|
func (us *UserService) CreateUser(username, password, email, role string) *models.User {
|
||||||
user := &models.User{
|
user := &models.User{
|
||||||
|
ID: utils.GenerateID(),
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: us.hashPassword(password),
|
Password: us.hashPassword(password),
|
||||||
Email: email,
|
Email: email,
|
||||||
@@ -59,6 +61,6 @@ func (us *UserService) AuthenticateUser(username, password string) bool {
|
|||||||
return us.ValidatePassword(user, password)
|
return us.ValidatePassword(user, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (us *UserService) UpdatePassword(userID uint, newPassword string) error {
|
func (us *UserService) UpdatePassword(userID string, newPassword string) error {
|
||||||
return database.DB.Model(&models.User{}).Where("id = ?", userID).Update("password", us.hashPassword(newPassword)).Error
|
return database.DB.Model(&models.User{}).Where("id = ?", userID).Update("password", us.hashPassword(newPassword)).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rs/xid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateID 生成一个新的 ID (使用 xid,20位字符)
|
||||||
|
func GenerateID() string {
|
||||||
|
return xid.New().String()
|
||||||
|
}
|
||||||
|
// IsNumeric 检查字符串是否全为数字
|
||||||
|
func IsNumeric(s string) bool {
|
||||||
|
for _, c := range s {
|
||||||
|
if c < '0' || c > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s != ""
|
||||||
|
}
|
||||||
@@ -8,13 +8,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
UserID uint `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateToken 生成 JWT token
|
// GenerateToken 生成 JWT token
|
||||||
func GenerateToken(userID uint, username string, expireDays int, secret string) (string, error) {
|
func GenerateToken(userID string, username string, expireDays int, secret string) (string, error) {
|
||||||
claims := Claims{
|
claims := Claims{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Username: username,
|
Username: username,
|
||||||
@@ -29,18 +29,18 @@ func GenerateToken(userID uint, username string, expireDays int, secret string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParseToken 解析 JWT token
|
// ParseToken 解析 JWT token
|
||||||
func ParseToken(tokenString string, secret string) (uint, string, error) {
|
func ParseToken(tokenString string, secret string) (string, string, error) {
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
|
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
|
||||||
return []byte(secret), nil
|
return []byte(secret), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||||
return claims.UserID, claims.Username, nil
|
return claims.UserID, claims.Username, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, "", errors.New("invalid token")
|
return "", "", errors.New("invalid token")
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-37
@@ -58,27 +58,27 @@ export const api = {
|
|||||||
request('/auth/register', { method: 'POST', body: JSON.stringify(data) })
|
request('/auth/register', { method: 'POST', body: JSON.stringify(data) })
|
||||||
},
|
},
|
||||||
tasks: {
|
tasks: {
|
||||||
list: (params?: { page?: number; page_size?: number; name?: string; agent_id?: number; tags?: string; type?: string }) => {
|
list: (params?: { page?: number; page_size?: number; name?: string; agent_id?: string; tags?: string; type?: string }) => {
|
||||||
const query = new URLSearchParams()
|
const query = new URLSearchParams()
|
||||||
if (params?.page) query.set('page', String(params.page))
|
if (params?.page) query.set('page', String(params.page))
|
||||||
if (params?.page_size) query.set('page_size', String(params.page_size))
|
if (params?.page_size) query.set('page_size', String(params.page_size))
|
||||||
if (params?.name) query.set('name', params.name)
|
if (params?.name) query.set('name', params.name)
|
||||||
if (params?.tags) query.set('tags', params.tags)
|
if (params?.tags) query.set('tags', params.tags)
|
||||||
if (params?.agent_id) query.set('agent_id', String(params.agent_id))
|
if (params?.agent_id) query.set('agent_id', params.agent_id)
|
||||||
if (params?.type) query.set('type', params.type)
|
if (params?.type) query.set('type', params.type)
|
||||||
return request<TaskListResponse>(`/tasks?${query}`)
|
return request<TaskListResponse>(`/tasks?${query}`)
|
||||||
},
|
},
|
||||||
create: (data: Partial<Task>) => request<Task>('/tasks', { method: 'POST', body: JSON.stringify(data) }),
|
create: (data: Partial<Task>) => request<Task>('/tasks', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
update: (id: number, data: Partial<Task>) => request<Task>(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
update: (id: string, data: Partial<Task>) => request<Task>(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||||
delete: (id: number) => request(`/tasks/${id}`, { method: 'DELETE' }),
|
delete: (id: string) => request(`/tasks/${id}`, { method: 'DELETE' }),
|
||||||
execute: (id: number) => request<ExecutionResult>(`/execute/task/${id}`, { method: 'POST' }),
|
execute: (id: string) => request<ExecutionResult>(`/execute/task/${id}`, { method: 'POST' }),
|
||||||
stop: (logID: number) => request(`/tasks/stop/${logID}`, { method: 'POST' })
|
stop: (logID: string) => request(`/tasks/stop/${logID}`, { method: 'POST' })
|
||||||
},
|
},
|
||||||
scripts: {
|
scripts: {
|
||||||
list: () => request<Script[]>('/scripts'),
|
list: () => request<Script[]>('/scripts'),
|
||||||
create: (data: Partial<Script>) => request<Script>('/scripts', { method: 'POST', body: JSON.stringify(data) }),
|
create: (data: Partial<Script>) => request<Script>('/scripts', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
update: (id: number, data: Partial<Script>) => request<Script>(`/scripts/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
update: (id: string, data: Partial<Script>) => request<Script>(`/scripts/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||||
delete: (id: number) => request(`/scripts/${id}`, { method: 'DELETE' })
|
delete: (id: string) => request(`/scripts/${id}`, { method: 'DELETE' })
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
list: (params?: { page?: number; page_size?: number; name?: string }) => {
|
list: (params?: { page?: number; page_size?: number; name?: string }) => {
|
||||||
@@ -90,27 +90,27 @@ export const api = {
|
|||||||
},
|
},
|
||||||
all: () => request<EnvVar[]>('/env/all'),
|
all: () => request<EnvVar[]>('/env/all'),
|
||||||
create: (data: Partial<EnvVar>) => request<EnvVar>('/env', { method: 'POST', body: JSON.stringify(data) }),
|
create: (data: Partial<EnvVar>) => request<EnvVar>('/env', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
update: (id: number, data: Partial<EnvVar>) => request<EnvVar>(`/env/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
update: (id: string, data: Partial<EnvVar>) => request<EnvVar>(`/env/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||||
delete: (id: number) => request(`/env/${id}`, { method: 'DELETE' })
|
delete: (id: string) => request(`/env/${id}`, { method: 'DELETE' })
|
||||||
},
|
},
|
||||||
execute: {
|
execute: {
|
||||||
command: (command: string) => request('/execute/command', { method: 'POST', body: JSON.stringify({ command }) }),
|
command: (command: string) => request('/execute/command', { method: 'POST', body: JSON.stringify({ command }) }),
|
||||||
results: () => request('/execute/results')
|
results: () => request('/execute/results')
|
||||||
},
|
},
|
||||||
logs: {
|
logs: {
|
||||||
list: (params?: { page?: number; page_size?: number; task_id?: number; task_name?: string; status?: string }) => {
|
list: (params?: { page?: number; page_size?: number; task_id?: string; task_name?: string; status?: string }) => {
|
||||||
const query = new URLSearchParams()
|
const query = new URLSearchParams()
|
||||||
if (params?.page) query.set('page', String(params.page))
|
if (params?.page) query.set('page', String(params.page))
|
||||||
if (params?.page_size) query.set('page_size', String(params.page_size))
|
if (params?.page_size) query.set('page_size', String(params.page_size))
|
||||||
if (params?.task_id) query.set('task_id', String(params.task_id))
|
if (params?.task_id) query.set('task_id', params.task_id)
|
||||||
if (params?.task_name) query.set('task_name', params.task_name)
|
if (params?.task_name) query.set('task_name', params.task_name)
|
||||||
if (params?.status) query.set('status', params.status)
|
if (params?.status) query.set('status', params.status)
|
||||||
return request<LogListResponse>(`/logs?${query}`)
|
return request<LogListResponse>(`/logs?${query}`)
|
||||||
},
|
},
|
||||||
get: (id: number) => request<LogDetail>(`/logs/${id}`),
|
get: (id: string) => request<LogDetail>(`/logs/${id}`),
|
||||||
detail: (id: number) => request<LogDetail>(`/logs/${id}`),
|
detail: (id: string) => request<LogDetail>(`/logs/${id}`),
|
||||||
delete: (id: number) => request(`/logs/${id}`, { method: 'DELETE' }),
|
delete: (id: string) => request(`/logs/${id}`, { method: 'DELETE' }),
|
||||||
clear: (taskId?: number) => request('/logs/clear', { method: 'POST', body: JSON.stringify({ task_id: taskId }) })
|
clear: (taskId?: string) => request('/logs/clear', { method: 'POST', body: JSON.stringify({ task_id: taskId }) })
|
||||||
},
|
},
|
||||||
dashboard: {
|
dashboard: {
|
||||||
stats: () => request<Stats>('/stats'),
|
stats: () => request<Stats>('/stats'),
|
||||||
@@ -217,11 +217,11 @@ export const api = {
|
|||||||
},
|
},
|
||||||
create: (data: { name: string; version?: string; language: string; lang_version?: string; remark?: string }) =>
|
create: (data: { name: string; version?: string; language: string; lang_version?: string; remark?: string }) =>
|
||||||
request<Dependency>('/deps', { method: 'POST', body: JSON.stringify(data) }),
|
request<Dependency>('/deps', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
delete: (id: number) => request(`/deps/${id}`, { method: 'DELETE' }),
|
delete: (id: string) => request(`/deps/${id}`, { method: 'DELETE' }),
|
||||||
install: (data: any) => request<any>('/deps/install', { method: 'POST', body: JSON.stringify(data) }),
|
install: (data: any) => request<any>('/deps/install', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
getInstallCmd: (data: any) => request<{ command: string }>('/deps/install-cmd', { method: 'POST', body: JSON.stringify(data) }),
|
getInstallCmd: (data: any) => request<{ command: string }>('/deps/install-cmd', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
uninstall: (id: number) => request<any>(`/deps/uninstall/${id}`, { method: 'POST' }),
|
uninstall: (id: string) => request<any>(`/deps/uninstall/${id}`, { method: 'POST' }),
|
||||||
reinstall: (id: number) => request(`/deps/reinstall/${id}`, { method: 'POST' }),
|
reinstall: (id: string) => request(`/deps/reinstall/${id}`, { method: 'POST' }),
|
||||||
reinstallAll: (language: string, lang_version?: string) => {
|
reinstallAll: (language: string, lang_version?: string) => {
|
||||||
const query = new URLSearchParams({ language })
|
const query = new URLSearchParams({ language })
|
||||||
if (lang_version) query.set('lang_version', lang_version)
|
if (lang_version) query.set('lang_version', lang_version)
|
||||||
@@ -241,16 +241,16 @@ export const api = {
|
|||||||
agents: {
|
agents: {
|
||||||
list: () => request<Agent[]>('/agents'),
|
list: () => request<Agent[]>('/agents'),
|
||||||
getVersion: () => request<{ version: string; platforms: { os: string; arch: string; filename: string }[] }>('/agents/version'),
|
getVersion: () => request<{ version: string; platforms: { os: string; arch: string; filename: string }[] }>('/agents/version'),
|
||||||
update: (id: number, data: { name: string; description?: string; enabled: boolean }) =>
|
update: (id: string, data: { name: string; description?: string; enabled: boolean }) =>
|
||||||
request('/agents/' + id, { method: 'PUT', body: JSON.stringify(data) }),
|
request('/agents/' + id, { method: 'PUT', body: JSON.stringify(data) }),
|
||||||
delete: (id: number) => request('/agents/' + id, { method: 'DELETE' }),
|
delete: (id: string) => request('/agents/' + id, { method: 'DELETE' }),
|
||||||
forceUpdate: (id: number) => request('/agents/' + id + '/update', { method: 'POST' }),
|
forceUpdate: (id: string) => request('/agents/' + id + '/update', { method: 'POST' }),
|
||||||
downloadUrl: (os: string, arch: string) => `${API_BASE_URL}/agent/download?os=${os}&arch=${arch}`,
|
downloadUrl: (os: string, arch: string) => `${API_BASE_URL}/agent/download?os=${os}&arch=${arch}`,
|
||||||
// 令牌管理
|
// 令牌管理
|
||||||
listTokens: () => request<AgentToken[]>('/agents/tokens'),
|
listTokens: () => request<AgentToken[]>('/agents/tokens'),
|
||||||
createToken: (data: { remark?: string; max_uses?: number; expires_at?: string }) =>
|
createToken: (data: { remark?: string; max_uses?: number; expires_at?: string }) =>
|
||||||
request<AgentToken>('/agents/tokens', { method: 'POST', body: JSON.stringify(data) }),
|
request<AgentToken>('/agents/tokens', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
deleteToken: (id: number) => request('/agents/tokens/' + id, { method: 'DELETE' })
|
deleteToken: (id: string) => request('/agents/tokens/' + id, { method: 'DELETE' })
|
||||||
},
|
},
|
||||||
mise: {
|
mise: {
|
||||||
list: () => request<MiseLanguage[]>('/mise/ls'),
|
list: () => request<MiseLanguage[]>('/mise/ls'),
|
||||||
@@ -272,7 +272,7 @@ export interface FileNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Task {
|
export interface Task {
|
||||||
id: number
|
id: string
|
||||||
name: string
|
name: string
|
||||||
command: string
|
command: string
|
||||||
tags: string
|
tags: string
|
||||||
@@ -288,7 +288,7 @@ export interface Task {
|
|||||||
retry_interval: number
|
retry_interval: number
|
||||||
random_range: number
|
random_range: number
|
||||||
languages: { name: string; version: string }[]
|
languages: { name: string; version: string }[]
|
||||||
agent_id: number | null
|
agent_id: string | null
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
last_run: string
|
last_run: string
|
||||||
next_run: string
|
next_run: string
|
||||||
@@ -308,7 +308,7 @@ export interface RepoConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecutionResult {
|
export interface ExecutionResult {
|
||||||
TaskID: number
|
TaskID: string
|
||||||
Success: boolean
|
Success: boolean
|
||||||
Output: string
|
Output: string
|
||||||
Error: string
|
Error: string
|
||||||
@@ -324,13 +324,13 @@ export interface TaskListResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Script {
|
export interface Script {
|
||||||
id: number
|
id: string
|
||||||
name: string
|
name: string
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnvVar {
|
export interface EnvVar {
|
||||||
id: number
|
id: string
|
||||||
name: string
|
name: string
|
||||||
value: string
|
value: string
|
||||||
remark: string
|
remark: string
|
||||||
@@ -355,8 +355,8 @@ export interface Stats {
|
|||||||
|
|
||||||
|
|
||||||
export interface TaskLog {
|
export interface TaskLog {
|
||||||
id: number
|
id: string
|
||||||
task_id: number
|
task_id: string
|
||||||
task_name: string
|
task_name: string
|
||||||
task_type: string
|
task_type: string
|
||||||
command: string
|
command: string
|
||||||
@@ -376,8 +376,8 @@ export interface LogListResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LogDetail {
|
export interface LogDetail {
|
||||||
id: number
|
id: string
|
||||||
task_id: number
|
task_id: string
|
||||||
command: string
|
command: string
|
||||||
output: string
|
output: string
|
||||||
error: string | null
|
error: string | null
|
||||||
@@ -417,7 +417,7 @@ export interface SchedulerSettings {
|
|||||||
|
|
||||||
|
|
||||||
export interface LoginLog {
|
export interface LoginLog {
|
||||||
id: number
|
id: string
|
||||||
username: string
|
username: string
|
||||||
ip: string
|
ip: string
|
||||||
user_agent: string
|
user_agent: string
|
||||||
@@ -441,13 +441,13 @@ export interface DailyStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskStatsItem {
|
export interface TaskStatsItem {
|
||||||
task_id: number
|
task_id: string
|
||||||
task_name: string
|
task_name: string
|
||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Dependency {
|
export interface Dependency {
|
||||||
id: number
|
id: string
|
||||||
name: string
|
name: string
|
||||||
version: string
|
version: string
|
||||||
language: string
|
language: string
|
||||||
@@ -459,7 +459,7 @@ export interface Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Agent {
|
export interface Agent {
|
||||||
id: number
|
id: string
|
||||||
name: string
|
name: string
|
||||||
token: string
|
token: string
|
||||||
machine_id: string
|
machine_id: string
|
||||||
@@ -478,7 +478,7 @@ export interface Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentToken {
|
export interface AgentToken {
|
||||||
id: number
|
id: string
|
||||||
token: string
|
token: string
|
||||||
remark: string
|
remark: string
|
||||||
max_uses: number
|
max_uses: number
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ async function createToken() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteToken(id: number) {
|
async function deleteToken(id: string) {
|
||||||
try {
|
try {
|
||||||
await api.agents.deleteToken(id)
|
await api.agents.deleteToken(id)
|
||||||
await loadAgents()
|
await loadAgents()
|
||||||
@@ -230,7 +230,7 @@ onUnmounted(() => {
|
|||||||
<!-- 大屏表头 -->
|
<!-- 大屏表头 -->
|
||||||
<div
|
<div
|
||||||
class="hidden sm:flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium">
|
class="hidden sm:flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium">
|
||||||
<span class="w-10 sm:w-12 shrink-0">ID</span>
|
<span class="w-10 sm:w-12 shrink-0">序号</span>
|
||||||
<span class="w-6 shrink-0"></span>
|
<span class="w-6 shrink-0"></span>
|
||||||
<span class="w-24 sm:w-32 shrink-0">名称</span>
|
<span class="w-24 sm:w-32 shrink-0">名称</span>
|
||||||
<span class="w-24 sm:w-28 shrink-0">IP</span>
|
<span class="w-24 sm:w-28 shrink-0">IP</span>
|
||||||
@@ -246,11 +246,11 @@ onUnmounted(() => {
|
|||||||
{{ searchQuery ? '无匹配结果' : '暂无 Agent' }}
|
{{ searchQuery ? '无匹配结果' : '暂无 Agent' }}
|
||||||
</div>
|
</div>
|
||||||
<!-- 小屏布局 -->
|
<!-- 小屏布局 -->
|
||||||
<div v-for="agent in filteredAgents" :key="agent.id"
|
<div v-for="(agent, index) in filteredAgents" :key="agent.id"
|
||||||
class="sm:hidden p-3 hover:bg-muted/50 transition-colors">
|
class="sm:hidden p-3 hover:bg-muted/50 transition-colors">
|
||||||
<div class="flex items-start justify-between mb-2">
|
<div class="flex items-start justify-between mb-2">
|
||||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<span class="text-xs text-muted-foreground shrink-0">#{{ agent.id }}</span>
|
<span class="text-xs text-muted-foreground shrink-0">#{{ filteredAgents.length - index }}</span>
|
||||||
<span class="flex items-center shrink-0" :title="isOnline(agent) ? '在线' : '离线'">
|
<span class="flex items-center shrink-0" :title="isOnline(agent) ? '在线' : '离线'">
|
||||||
<div v-if="isOnline(agent)"
|
<div v-if="isOnline(agent)"
|
||||||
class="h-5 w-5 rounded-full bg-green-500/10 flex items-center justify-center">
|
class="h-5 w-5 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||||
@@ -310,9 +310,9 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 大屏布局 -->
|
<!-- 大屏布局 -->
|
||||||
<div v-for="agent in filteredAgents" :key="`desktop-${agent.id}`"
|
<div v-for="(agent, index) in filteredAgents" :key="`desktop-${agent.id}`"
|
||||||
class="hidden sm:flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 hover:bg-muted/50 transition-colors">
|
class="hidden sm:flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 hover:bg-muted/50 transition-colors">
|
||||||
<span class="w-10 sm:w-12 shrink-0 text-muted-foreground text-xs sm:text-sm">#{{ agent.id }}</span>
|
<span class="w-10 sm:w-12 shrink-0 text-muted-foreground text-xs sm:text-sm">#{{ filteredAgents.length - index }}</span>
|
||||||
<span class="w-6 shrink-0 flex justify-center">
|
<span class="w-6 shrink-0 flex justify-center">
|
||||||
<span class="flex justify-center shrink-0" :title="isOnline(agent) ? '在线' : '离线'">
|
<span class="flex justify-center shrink-0" :title="isOnline(agent) ? '在线' : '离线'">
|
||||||
<div v-if="isOnline(agent)"
|
<div v-if="isOnline(agent)"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const activeTab = ref('python')
|
|||||||
const deps = ref<Dependency[]>([])
|
const deps = ref<Dependency[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const installing = ref(false)
|
const installing = ref(false)
|
||||||
const reinstalling = ref<number | null>(null)
|
const reinstalling = ref<string | null>(null)
|
||||||
const reinstallingAll = ref(false)
|
const reinstallingAll = ref(false)
|
||||||
const installedLangs = ref<string[]>([])
|
const installedLangs = ref<string[]>([])
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ const envVars = ref<EnvVar[]>([])
|
|||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
const editingEnv = ref<Partial<EnvVar>>({})
|
const editingEnv = ref<Partial<EnvVar>>({})
|
||||||
const isEdit = ref(false)
|
const isEdit = ref(false)
|
||||||
const showValues = ref<Record<number, boolean>>({})
|
const showValues = ref<Record<string, boolean>>({})
|
||||||
const showDeleteDialog = ref(false)
|
const showDeleteDialog = ref(false)
|
||||||
const deleteEnvId = ref<number | null>(null)
|
const deleteEnvId = ref<string | null>(null)
|
||||||
|
|
||||||
const filterName = ref('')
|
const filterName = ref('')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
@@ -84,7 +84,7 @@ async function saveEnv() {
|
|||||||
} catch { toast.error('保存失败') }
|
} catch { toast.error('保存失败') }
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmDelete(id: number) {
|
function confirmDelete(id: string) {
|
||||||
deleteEnvId.value = id
|
deleteEnvId.value = id
|
||||||
showDeleteDialog.value = true
|
showDeleteDialog.value = true
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ async function deleteEnv() {
|
|||||||
deleteEnvId.value = null
|
deleteEnvId.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleShow(id: number) {
|
function toggleShow(id: string) {
|
||||||
showValues.value[id] = !showValues.value[id]
|
showValues.value[id] = !showValues.value[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const { pageSize } = useSiteSettings()
|
|||||||
const logs = ref<TaskLog[]>([])
|
const logs = ref<TaskLog[]>([])
|
||||||
const selectedLog = ref<TaskLog | null>(null)
|
const selectedLog = ref<TaskLog | null>(null)
|
||||||
const filterKeyword = ref('')
|
const filterKeyword = ref('')
|
||||||
const filterTaskId = ref<number | undefined>(undefined)
|
const filterTaskId = ref<string | undefined>(undefined)
|
||||||
const filterStatus = ref<string | undefined>(undefined)
|
const filterStatus = ref<string | undefined>(undefined)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
@@ -49,7 +49,7 @@ const showClearDialog = ref(false)
|
|||||||
|
|
||||||
// 删除单条日志弹窗
|
// 删除单条日志弹窗
|
||||||
const showDeleteDialog = ref(false)
|
const showDeleteDialog = ref(false)
|
||||||
const deleteLogId = ref<number | null>(null)
|
const deleteLogId = ref<string | null>(null)
|
||||||
|
|
||||||
const wsContent = ref('')
|
const wsContent = ref('')
|
||||||
const isWsLoading = ref(false)
|
const isWsLoading = ref(false)
|
||||||
@@ -62,7 +62,7 @@ const decompressedOutput = computed(() => {
|
|||||||
|
|
||||||
async function loadLogs() {
|
async function loadLogs() {
|
||||||
try {
|
try {
|
||||||
const params: { page: number; page_size: number; task_id?: number; task_name?: string; status?: string } = {
|
const params: { page: number; page_size: number; task_id?: string; task_name?: string; status?: string } = {
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
page_size: pageSize.value
|
page_size: pageSize.value
|
||||||
}
|
}
|
||||||
@@ -241,7 +241,7 @@ async function handleClearLogs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmDeleteLog(id: number) {
|
function confirmDeleteLog(id: string) {
|
||||||
deleteLogId.value = id
|
deleteLogId.value = id
|
||||||
showDeleteDialog.value = true
|
showDeleteDialog.value = true
|
||||||
}
|
}
|
||||||
@@ -291,7 +291,7 @@ onMounted(() => {
|
|||||||
// 从 URL 读取参数
|
// 从 URL 读取参数
|
||||||
const taskIdParam = route.query.task_id
|
const taskIdParam = route.query.task_id
|
||||||
if (taskIdParam) {
|
if (taskIdParam) {
|
||||||
filterTaskId.value = Number(taskIdParam)
|
filterTaskId.value = String(taskIdParam)
|
||||||
}
|
}
|
||||||
const statusParam = route.query.status
|
const statusParam = route.query.status
|
||||||
if (statusParam) {
|
if (statusParam) {
|
||||||
@@ -302,7 +302,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
// 监听路由变化
|
// 监听路由变化
|
||||||
watch(() => route.query, (newQuery) => {
|
watch(() => route.query, (newQuery) => {
|
||||||
filterTaskId.value = newQuery.task_id ? Number(newQuery.task_id) : undefined
|
filterTaskId.value = newQuery.task_id ? String(newQuery.task_id) : undefined
|
||||||
filterStatus.value = newQuery.status ? String(newQuery.status) : undefined
|
filterStatus.value = newQuery.status ? String(newQuery.status) : undefined
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
loadLogs()
|
loadLogs()
|
||||||
@@ -352,7 +352,7 @@ watch(() => route.query, (newQuery) => {
|
|||||||
<!-- 小屏表头 -->
|
<!-- 小屏表头 -->
|
||||||
<div
|
<div
|
||||||
class="flex sm:hidden items-center gap-2 px-3 py-2 border-b bg-muted/20 text-xs text-muted-foreground font-medium">
|
class="flex sm:hidden items-center gap-2 px-3 py-2 border-b bg-muted/20 text-xs text-muted-foreground font-medium">
|
||||||
<span class="w-14 shrink-0">ID</span>
|
<span class="w-14 shrink-0">序号</span>
|
||||||
<span class="w-10 shrink-0 text-center">类型</span>
|
<span class="w-10 shrink-0 text-center">类型</span>
|
||||||
<span class="flex-1 min-w-0">任务名称</span>
|
<span class="flex-1 min-w-0">任务名称</span>
|
||||||
<span class="w-8 shrink-0 text-center">状态</span>
|
<span class="w-8 shrink-0 text-center">状态</span>
|
||||||
@@ -362,7 +362,7 @@ watch(() => route.query, (newQuery) => {
|
|||||||
<!-- 大屏表头 -->
|
<!-- 大屏表头 -->
|
||||||
<div
|
<div
|
||||||
class="hidden sm:flex items-center gap-4 px-4 h-11 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
class="hidden sm:flex items-center gap-4 px-4 h-11 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||||
<span class="w-16 shrink-0">ID</span>
|
<span class="w-16 shrink-0">序号</span>
|
||||||
<span class="w-12 shrink-0 text-center">类型</span>
|
<span class="w-12 shrink-0 text-center">类型</span>
|
||||||
<span class="w-36 shrink-0">任务名称</span>
|
<span class="w-36 shrink-0">任务名称</span>
|
||||||
<span class="flex-1 min-w-0">命令</span>
|
<span class="flex-1 min-w-0">命令</span>
|
||||||
@@ -376,13 +376,13 @@ watch(() => route.query, (newQuery) => {
|
|||||||
<div v-if="logs.length === 0" class="text-sm text-muted-foreground text-center py-8">
|
<div v-if="logs.length === 0" class="text-sm text-muted-foreground text-center py-8">
|
||||||
暂无日志
|
暂无日志
|
||||||
</div>
|
</div>
|
||||||
<div v-for="log in logs" :key="log.id" :class="[
|
<div v-for="(log, index) in logs" :key="log.id" :class="[
|
||||||
'cursor-pointer hover:bg-muted/30 transition-colors group',
|
'cursor-pointer hover:bg-muted/30 transition-colors group',
|
||||||
selectedLog?.id === log.id && 'bg-accent/50'
|
selectedLog?.id === log.id && 'bg-accent/50'
|
||||||
]" @click="selectLog(log)">
|
]" @click="selectLog(log)">
|
||||||
<!-- 小屏行 -->
|
<!-- 小屏行 -->
|
||||||
<div class="flex sm:hidden items-center gap-2 px-3 py-2">
|
<div class="flex sm:hidden items-center gap-2 px-3 py-2">
|
||||||
<span class="w-14 shrink-0 text-muted-foreground text-xs">#{{ log.id }}</span>
|
<span class="w-14 shrink-0 text-muted-foreground text-xs">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||||
<span class="w-6 shrink-0 flex justify-center" :title="getTaskTypeTitle(log.task_type || 'task')">
|
<span class="w-6 shrink-0 flex justify-center" :title="getTaskTypeTitle(log.task_type || 'task')">
|
||||||
<GitBranch v-if="log.task_type === TASK_TYPE.REPO" class="h-3.5 w-3.5 text-primary" />
|
<GitBranch v-if="log.task_type === TASK_TYPE.REPO" class="h-3.5 w-3.5 text-primary" />
|
||||||
<Terminal v-else class="h-3.5 w-3.5 text-primary" />
|
<Terminal v-else class="h-3.5 w-3.5 text-primary" />
|
||||||
@@ -424,7 +424,7 @@ watch(() => route.query, (newQuery) => {
|
|||||||
</div>
|
</div>
|
||||||
<!-- 大屏行 -->
|
<!-- 大屏行 -->
|
||||||
<div class="hidden sm:flex items-center gap-4 px-4 py-2">
|
<div class="hidden sm:flex items-center gap-4 px-4 py-2">
|
||||||
<span class="w-16 shrink-0 text-muted-foreground text-sm">#{{ log.id }}</span>
|
<span class="w-16 shrink-0 text-muted-foreground text-sm">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||||
<span class="w-10 shrink-0 flex justify-center" :title="getTaskTypeTitle(log.task_type || 'task')">
|
<span class="w-10 shrink-0 flex justify-center" :title="getTaskTypeTitle(log.task_type || 'task')">
|
||||||
<GitBranch v-if="log.task_type === TASK_TYPE.REPO" class="h-4 w-4 text-primary" />
|
<GitBranch v-if="log.task_type === TASK_TYPE.REPO" class="h-4 w-4 text-primary" />
|
||||||
<Terminal v-else class="h-4 w-4 text-primary" />
|
<Terminal v-else class="h-4 w-4 text-primary" />
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
const { pageSize } = useSiteSettings()
|
const { pageSize } = useSiteSettings()
|
||||||
|
|
||||||
interface LoginLog {
|
interface LoginLog {
|
||||||
id: number
|
id: string
|
||||||
username: string
|
username: string
|
||||||
ip: string
|
ip: string
|
||||||
user_agent: string
|
user_agent: string
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ async function save() {
|
|||||||
|
|
||||||
form.value.config = JSON.stringify(configToSave)
|
form.value.config = JSON.stringify(configToSave)
|
||||||
form.value.command = `[${repoConfig.value.source_type}] ${repoConfig.value.source_url}`
|
form.value.command = `[${repoConfig.value.source_type}] ${repoConfig.value.source_url}`
|
||||||
form.value.agent_id = selectedAgentId.value === 'local' ? null : Number(selectedAgentId.value)
|
form.value.agent_id = selectedAgentId.value === 'local' ? null : selectedAgentId.value
|
||||||
if (props.isEdit && form.value.id) {
|
if (props.isEdit && form.value.id) {
|
||||||
await api.tasks.update(form.value.id, form.value)
|
await api.tasks.update(form.value.id, form.value)
|
||||||
toast.success('同步任务已更新')
|
toast.success('同步任务已更新')
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const cleanType = ref('none')
|
|||||||
const cleanKeep = ref(30)
|
const cleanKeep = ref(30)
|
||||||
const allEnvVars = ref<EnvVar[]>([])
|
const allEnvVars = ref<EnvVar[]>([])
|
||||||
const allAgents = ref<Agent[]>([])
|
const allAgents = ref<Agent[]>([])
|
||||||
const selectedEnvIds = ref<number[]>([])
|
const selectedEnvIds = ref<string[]>([])
|
||||||
const selectedAgentId = ref<string>('local')
|
const selectedAgentId = ref<string>('local')
|
||||||
const selectedTriggerType = ref<string>('cron')
|
const selectedTriggerType = ref<string>('cron')
|
||||||
const envSearchQuery = ref('')
|
const envSearchQuery = ref('')
|
||||||
@@ -256,7 +256,7 @@ watch(() => props.open, async (val) => {
|
|||||||
}
|
}
|
||||||
// 解析环境变量
|
// 解析环境变量
|
||||||
if (props.task?.envs) {
|
if (props.task?.envs) {
|
||||||
selectedEnvIds.value = props.task.envs.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n))
|
selectedEnvIds.value = props.task.envs.split(',').map(s => s.trim()).filter(Boolean)
|
||||||
} else {
|
} else {
|
||||||
selectedEnvIds.value = []
|
selectedEnvIds.value = []
|
||||||
}
|
}
|
||||||
@@ -303,14 +303,14 @@ async function loadData() {
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
function addEnv(id: number) {
|
function addEnv(id: string) {
|
||||||
if (!selectedEnvIds.value.includes(id)) {
|
if (!selectedEnvIds.value.includes(id)) {
|
||||||
selectedEnvIds.value.push(id)
|
selectedEnvIds.value.push(id)
|
||||||
}
|
}
|
||||||
envSearchQuery.value = ''
|
envSearchQuery.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeEnv(id: number) {
|
function removeEnv(id: string) {
|
||||||
selectedEnvIds.value = selectedEnvIds.value.filter(envId => envId !== id)
|
selectedEnvIds.value = selectedEnvIds.value.filter(envId => envId !== id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +320,7 @@ async function save() {
|
|||||||
form.value.envs = selectedEnvIds.value.join(',')
|
form.value.envs = selectedEnvIds.value.join(',')
|
||||||
form.value.type = 'task'
|
form.value.type = 'task'
|
||||||
form.value.trigger_type = selectedTriggerType.value
|
form.value.trigger_type = selectedTriggerType.value
|
||||||
form.value.agent_id = selectedAgentId.value === 'local' ? null : Number(selectedAgentId.value)
|
form.value.agent_id = selectedAgentId.value === 'local' ? null : selectedAgentId.value
|
||||||
|
|
||||||
// 保存语言环境配置
|
// 保存语言环境配置
|
||||||
form.value.languages = selectedLangs.value.map(l => ({
|
form.value.languages = selectedLangs.value.map(l => ({
|
||||||
|
|||||||
@@ -26,19 +26,19 @@ const showRepoDialog = ref(false)
|
|||||||
const editingTask = ref<Partial<Task>>({})
|
const editingTask = ref<Partial<Task>>({})
|
||||||
const isEdit = ref(false)
|
const isEdit = ref(false)
|
||||||
const showDeleteDialog = ref(false)
|
const showDeleteDialog = ref(false)
|
||||||
const deleteTaskId = ref<number | null>(null)
|
const deleteTaskId = ref<string | null>(null)
|
||||||
|
|
||||||
const filterName = ref('')
|
const filterName = ref('')
|
||||||
const filterTags = ref('')
|
const filterTags = ref('')
|
||||||
const filterType = ref('all')
|
const filterType = ref('all')
|
||||||
const filterAgentId = ref<number | null>(null)
|
const filterAgentId = ref<string | null>(null)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
// 创建 agent 映射表
|
// 创建 agent 映射表
|
||||||
const agentMap = computed(() => {
|
const agentMap = computed(() => {
|
||||||
const map: Record<number, Agent> = {}
|
const map: Record<string, Agent> = {}
|
||||||
agents.value.forEach(a => { map[a.id] = a })
|
agents.value.forEach(a => { map[a.id] = a })
|
||||||
return map
|
return map
|
||||||
})
|
})
|
||||||
@@ -147,7 +147,7 @@ function duplicateTask(task: Task) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmDelete(id: number) {
|
function confirmDelete(id: string) {
|
||||||
deleteTaskId.value = id
|
deleteTaskId.value = id
|
||||||
showDeleteDialog.value = true
|
showDeleteDialog.value = true
|
||||||
}
|
}
|
||||||
@@ -163,9 +163,9 @@ async function deleteTask() {
|
|||||||
deleteTaskId.value = null
|
deleteTaskId.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const executingTaskId = ref<number | null>(null)
|
const executingTaskId = ref<string | null>(null)
|
||||||
|
|
||||||
async function runTask(id: number) {
|
async function runTask(id: string) {
|
||||||
executingTaskId.value = id
|
executingTaskId.value = id
|
||||||
toast.message('正在执行...', { id: 'executing' })
|
toast.message('正在执行...', { id: 'executing' })
|
||||||
try {
|
try {
|
||||||
@@ -189,8 +189,8 @@ async function toggleTask(task: Task, enabled: boolean) {
|
|||||||
} catch { toast.error('操作失败') }
|
} catch { toast.error('操作失败') }
|
||||||
}
|
}
|
||||||
|
|
||||||
function viewLogs(taskId: number) {
|
function viewLogs(taskId: string) {
|
||||||
router.push({ path: '/history', query: { task_id: String(taskId) } })
|
router.push({ path: '/history', query: { task_id: taskId } })
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTaskTypeTitle(type: string) {
|
function getTaskTypeTitle(type: string) {
|
||||||
@@ -204,7 +204,7 @@ onMounted(async () => {
|
|||||||
// 从 URL 参数读取 agent_id
|
// 从 URL 参数读取 agent_id
|
||||||
const agentIdParam = route.query.agent_id
|
const agentIdParam = route.query.agent_id
|
||||||
if (agentIdParam) {
|
if (agentIdParam) {
|
||||||
filterAgentId.value = Number(agentIdParam)
|
filterAgentId.value = String(agentIdParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadTasks()
|
loadTasks()
|
||||||
@@ -212,7 +212,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// 监听路由参数变化
|
// 监听路由参数变化
|
||||||
watch(() => route.query.agent_id, (newVal) => {
|
watch(() => route.query.agent_id, (newVal) => {
|
||||||
filterAgentId.value = newVal ? Number(newVal) : null
|
filterAgentId.value = newVal ? String(newVal) : null
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
loadTasks()
|
loadTasks()
|
||||||
})
|
})
|
||||||
@@ -282,7 +282,7 @@ watch(() => route.query.agent_id, (newVal) => {
|
|||||||
<!-- 表头 -->
|
<!-- 表头 -->
|
||||||
<div
|
<div
|
||||||
class="flex flex-wrap sm:flex-nowrap items-center gap-x-2 gap-y-2 sm:gap-4 px-3 sm:px-4 py-2 sm:py-1.5 border-b bg-muted/20 text-xs sm:text-sm text-muted-foreground font-medium min-w-0 sm:min-w-[1000px]">
|
class="flex flex-wrap sm:flex-nowrap items-center gap-x-2 gap-y-2 sm:gap-4 px-3 sm:px-4 py-2 sm:py-1.5 border-b bg-muted/20 text-xs sm:text-sm text-muted-foreground font-medium min-w-0 sm:min-w-[1000px]">
|
||||||
<span class="w-10 sm:w-12 shrink-0 max-sm:order-1">ID</span>
|
<span class="w-10 sm:w-12 shrink-0 max-sm:order-1">序号</span>
|
||||||
<span class="w-8 shrink-0 text-center max-sm:order-2">类型</span>
|
<span class="w-8 shrink-0 text-center max-sm:order-2">类型</span>
|
||||||
<span class="flex-1 min-w-0 sm:flex-none sm:w-40 md:w-48 lg:w-56 shrink-0 max-sm:order-3">名称</span>
|
<span class="flex-1 min-w-0 sm:flex-none sm:w-40 md:w-48 lg:w-56 shrink-0 max-sm:order-3">名称</span>
|
||||||
<span class="w-24 sm:w-32 shrink-0 hidden md:block">执行位置</span>
|
<span class="w-24 sm:w-32 shrink-0 hidden md:block">执行位置</span>
|
||||||
@@ -300,10 +300,9 @@ watch(() => route.query.agent_id, (newVal) => {
|
|||||||
<div v-if="tasks.length === 0" class="text-sm text-muted-foreground text-center py-8">
|
<div v-if="tasks.length === 0" class="text-sm text-muted-foreground text-center py-8">
|
||||||
暂无任务
|
暂无任务
|
||||||
</div>
|
</div>
|
||||||
<div v-for="task in tasks" :key="task.id"
|
<div v-for="(task, index) in tasks" :key="task.id"
|
||||||
class="flex flex-wrap sm:flex-nowrap items-center gap-x-2 gap-y-2 sm:gap-4 px-3 sm:px-4 py-2.5 sm:py-1.5 hover:bg-muted/30 transition-colors">
|
class="flex flex-wrap sm:flex-nowrap items-center gap-x-2 gap-y-2 sm:gap-4 px-3 sm:px-4 py-2.5 sm:py-1.5 hover:bg-muted/30 transition-colors">
|
||||||
<span class="w-10 sm:w-12 shrink-0 text-muted-foreground text-xs sm:text-sm max-sm:order-1">#{{ task.id
|
<span class="w-10 sm:w-12 shrink-0 text-muted-foreground text-xs sm:text-sm max-sm:order-1">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||||
}}</span>
|
|
||||||
<span class="w-8 shrink-0 flex justify-center max-sm:order-2" :title="getTaskTypeTitle(task.type || 'task')">
|
<span class="w-8 shrink-0 flex justify-center max-sm:order-2" :title="getTaskTypeTitle(task.type || 'task')">
|
||||||
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-primary" />
|
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-primary" />
|
||||||
<Terminal v-else class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-primary" />
|
<Terminal v-else class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-primary" />
|
||||||
|
|||||||
Reference in New Issue
Block a user