chore: history page support sharedworker.js
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/engigu/baihu-panel/internal/constant"
|
||||||
"github.com/engigu/baihu-panel/internal/services"
|
"github.com/engigu/baihu-panel/internal/services"
|
||||||
"github.com/engigu/baihu-panel/internal/services/repo"
|
"github.com/engigu/baihu-panel/internal/services/repo"
|
||||||
"github.com/engigu/baihu-panel/internal/utils"
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
@@ -70,10 +71,10 @@ func Run(args []string) {
|
|||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
|
|
||||||
// 处理 $SCRIPTS_DIR$ 代号替换
|
// 处理 $SCRIPTS_DIR$ 代号替换
|
||||||
if strings.Contains(cfg.TargetPath, "$SCRIPTS_DIR$") {
|
if strings.Contains(cfg.TargetPath, constant.ScriptsDirPlaceholder) {
|
||||||
scriptsDir := os.Getenv("BH_SCRIPTS_DIR")
|
scriptsDir := os.Getenv("BH_SCRIPTS_DIR")
|
||||||
if scriptsDir != "" {
|
if scriptsDir != "" {
|
||||||
cfg.TargetPath = filepath.Clean(strings.ReplaceAll(cfg.TargetPath, "$SCRIPTS_DIR$", scriptsDir))
|
cfg.TargetPath = filepath.Clean(strings.ReplaceAll(cfg.TargetPath, constant.ScriptsDirPlaceholder, scriptsDir))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,9 @@ const (
|
|||||||
MaxMessageSize = 1024 * 1024 // 1MB
|
MaxMessageSize = 1024 * 1024 // 1MB
|
||||||
// MaxLogSize 允许的最大日志大小 (保留末尾 10MB)
|
// MaxLogSize 允许的最大日志大小 (保留末尾 10MB)
|
||||||
MaxLogSize = 10 * 1024 * 1024 // 10MB
|
MaxLogSize = 10 * 1024 * 1024 // 10MB
|
||||||
|
|
||||||
|
// ScriptsDirPlaceholder 脚本目录占位符
|
||||||
|
ScriptsDirPlaceholder = "$SCRIPTS_DIR$"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TablePrefix 表前缀,从配置文件读取
|
// TablePrefix 表前缀,从配置文件读取
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func resolveWorkDir(workDir string) string {
|
|||||||
return absPath
|
return absPath
|
||||||
}
|
}
|
||||||
// 如果已经是绝对路径,直接返回
|
// 如果已经是绝对路径,直接返回
|
||||||
if strings.HasPrefix(workDir, "$SCRIPTS_DIR$") {
|
if strings.HasPrefix(workDir, constant.ScriptsDirPlaceholder) {
|
||||||
return workDir
|
return workDir
|
||||||
}
|
}
|
||||||
if filepath.IsAbs(workDir) {
|
if filepath.IsAbs(workDir) {
|
||||||
@@ -381,7 +381,7 @@ func (tc *TaskController) deleteRepoPhysicalFiles(task *models.Task) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetPath == "" || targetPath == "$SCRIPTS_DIR$" {
|
if targetPath == "" || targetPath == constant.ScriptsDirPlaceholder {
|
||||||
logger.Warnf("[Controller] 任务 %s 无法确定有效的物理删除路径,跳过", task.Name)
|
logger.Warnf("[Controller] 任务 %s 无法确定有效的物理删除路径,跳过", task.Name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -389,8 +389,8 @@ func (tc *TaskController) deleteRepoPhysicalFiles(task *models.Task) {
|
|||||||
// 确定绝对路径
|
// 确定绝对路径
|
||||||
scriptsDir, _ := filepath.Abs(constant.ScriptsWorkDir)
|
scriptsDir, _ := filepath.Abs(constant.ScriptsWorkDir)
|
||||||
fullPath := targetPath
|
fullPath := targetPath
|
||||||
if strings.HasPrefix(targetPath, "$SCRIPTS_DIR$") {
|
if strings.HasPrefix(targetPath, constant.ScriptsDirPlaceholder) {
|
||||||
fullPath = filepath.Join(scriptsDir, strings.TrimPrefix(targetPath, "$SCRIPTS_DIR$"))
|
fullPath = filepath.Join(scriptsDir, strings.TrimPrefix(targetPath, constant.ScriptsDirPlaceholder))
|
||||||
} else if !filepath.IsAbs(targetPath) {
|
} else if !filepath.IsAbs(targetPath) {
|
||||||
fullPath = filepath.Join(scriptsDir, targetPath)
|
fullPath = filepath.Join(scriptsDir, targetPath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,9 +134,9 @@ func ParseRepoScriptsAndAddCron(taskID string, logWriter io.Writer, forceComment
|
|||||||
displayPath = filepath.Base(path)
|
displayPath = filepath.Base(path)
|
||||||
relDir, _ := filepath.Rel(absScriptsDir, filepath.Dir(absPath))
|
relDir, _ := filepath.Rel(absScriptsDir, filepath.Dir(absPath))
|
||||||
if relDir == "." {
|
if relDir == "." {
|
||||||
displayWorkDir = "$SCRIPTS_DIR$"
|
displayWorkDir = constant.ScriptsDirPlaceholder
|
||||||
} else {
|
} else {
|
||||||
displayWorkDir = filepath.ToSlash(filepath.Join("$SCRIPTS_DIR$", relDir))
|
displayWorkDir = filepath.ToSlash(filepath.Join(constant.ScriptsDirPlaceholder, relDir))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest,
|
|||||||
eventbus.DefaultBus.Publish(eventbus.Event{
|
eventbus.DefaultBus.Publish(eventbus.Event{
|
||||||
Type: eventType,
|
Type: eventType,
|
||||||
Payload: map[string]interface{}{
|
Payload: map[string]interface{}{
|
||||||
|
"log_id": req.LogID,
|
||||||
"task_id": task.ID,
|
"task_id": task.ID,
|
||||||
"task_name": task.Name,
|
"task_name": task.Name,
|
||||||
"status": result.Status,
|
"status": result.Status,
|
||||||
@@ -372,6 +373,7 @@ func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, er
|
|||||||
eventbus.DefaultBus.Publish(eventbus.Event{
|
eventbus.DefaultBus.Publish(eventbus.Event{
|
||||||
Type: constant.EventTaskFailed,
|
Type: constant.EventTaskFailed,
|
||||||
Payload: map[string]interface{}{
|
Payload: map[string]interface{}{
|
||||||
|
"log_id": req.LogID,
|
||||||
"task_id": taskID,
|
"task_id": taskID,
|
||||||
"task_name": taskName,
|
"task_name": taskName,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
@@ -1099,9 +1101,9 @@ func BuildRepoCommand(task *models.Task) (string, string) {
|
|||||||
displayTargetPath := absTargetPath
|
displayTargetPath := absTargetPath
|
||||||
if rel, err := filepath.Rel(scriptsDir, absTargetPath); err == nil && !strings.HasPrefix(rel, "..") {
|
if rel, err := filepath.Rel(scriptsDir, absTargetPath); err == nil && !strings.HasPrefix(rel, "..") {
|
||||||
if rel == "." {
|
if rel == "." {
|
||||||
displayTargetPath = "$SCRIPTS_DIR$"
|
displayTargetPath = constant.ScriptsDirPlaceholder
|
||||||
} else {
|
} else {
|
||||||
displayTargetPath = "$SCRIPTS_DIR$/" + filepath.ToSlash(rel)
|
displayTargetPath = constant.ScriptsDirPlaceholder + "/" + filepath.ToSlash(rel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1233,7 +1235,7 @@ func (es *ExecutorService) refreshExecutionRequestEnvs(req *executor.ExecutionRe
|
|||||||
|
|
||||||
func (es *ExecutorService) ResolvePath(path string) string {
|
func (es *ExecutorService) ResolvePath(path string) string {
|
||||||
absScriptsDir := resolveAbsScriptsDir()
|
absScriptsDir := resolveAbsScriptsDir()
|
||||||
return strings.ReplaceAll(path, "$SCRIPTS_DIR$", absScriptsDir)
|
return strings.ReplaceAll(path, constant.ScriptsDirPlaceholder, absScriptsDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildRepoCommandEnvPrefix() string {
|
func buildRepoCommandEnvPrefix() string {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export const PATHS = {
|
|||||||
CONFIGS_DIR: '/app/configs',
|
CONFIGS_DIR: '/app/configs',
|
||||||
// 环境目录
|
// 环境目录
|
||||||
ENVS_DIR: '/app/envs',
|
ENVS_DIR: '/app/envs',
|
||||||
|
// 脚本目录占位符
|
||||||
|
SCRIPTS_DIR_PLACEHOLDER: '$SCRIPTS_DIR$',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 文件扩展名对应的运行命令
|
// 文件扩展名对应的运行命令
|
||||||
@@ -61,3 +63,12 @@ export const ENV_TYPE = {
|
|||||||
NORMAL: 'normal',
|
NORMAL: 'normal',
|
||||||
SECRET: 'secret',
|
SECRET: 'secret',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
// 任务事件类型
|
||||||
|
export const TASK_EVENTS = {
|
||||||
|
SUCCESS: 'task_success',
|
||||||
|
FAILED: 'task_failed',
|
||||||
|
TIMEOUT: 'task_timeout',
|
||||||
|
RUNNING: 'task_running',
|
||||||
|
QUEUED: 'task_queued',
|
||||||
|
} as const
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export interface WSMessage {
|
||||||
|
type: string
|
||||||
|
timestamp: number
|
||||||
|
payload: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskStatusPayload {
|
||||||
|
id: string
|
||||||
|
task_id: string
|
||||||
|
task_name: string
|
||||||
|
status: string
|
||||||
|
duration: number
|
||||||
|
start_time: string | null
|
||||||
|
end_time: string | null
|
||||||
|
error?: string | null
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { RepoConfig, Task } from '@/api'
|
import type { RepoConfig, Task } from '@/api'
|
||||||
|
import { PATHS } from '@/constants'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仓库命令解析结果
|
* 仓库命令解析结果
|
||||||
@@ -64,10 +65,10 @@ export function parseBaihuCommand(command: string): ParsedRepoResult | null {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
case '--target-path':
|
case '--target-path':
|
||||||
// 处理 $SCRIPTS_DIR$ 占位符
|
// 处理脚本目录占位符
|
||||||
if (value.startsWith('$SCRIPTS_DIR$/')) {
|
if (value.startsWith(`${PATHS.SCRIPTS_DIR_PLACEHOLDER}/`)) {
|
||||||
repoConfig.target_path = value.replace('$SCRIPTS_DIR$/', '')
|
repoConfig.target_path = value.replace(`${PATHS.SCRIPTS_DIR_PLACEHOLDER}/`, '')
|
||||||
} else if (value === '$SCRIPTS_DIR$') {
|
} else if (value === PATHS.SCRIPTS_DIR_PLACEHOLDER) {
|
||||||
repoConfig.target_path = ''
|
repoConfig.target_path = ''
|
||||||
} else {
|
} else {
|
||||||
repoConfig.target_path = value
|
repoConfig.target_path = value
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed, watch, nextTick } from 'vue'
|
import { ref, onMounted, computed, watch, nextTick } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { TASK_STATUS, TASK_TYPE, TASK_STATUS_TEXT } from '@/constants'
|
import { TASK_STATUS, TASK_TYPE, TASK_STATUS_TEXT, TASK_EVENTS } from '@/constants'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
@@ -17,6 +17,7 @@ import BaihuDialog from '@/components/ui/BaihuDialog.vue'
|
|||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { useSiteSettings } from '@/composables/useSiteSettings'
|
import { useSiteSettings } from '@/composables/useSiteSettings'
|
||||||
import TextOverflow from '@/components/TextOverflow.vue'
|
import TextOverflow from '@/components/TextOverflow.vue'
|
||||||
|
import { useEventBus } from '@/composables/useEventBus'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { pageSize } = useSiteSettings()
|
const { pageSize } = useSiteSettings()
|
||||||
@@ -34,6 +35,39 @@ let durationTimer: ReturnType<typeof setInterval> | null = null
|
|||||||
|
|
||||||
const isRefreshing = ref(false)
|
const isRefreshing = ref(false)
|
||||||
|
|
||||||
|
// 监听实时任务事件
|
||||||
|
useEventBus(Object.values(TASK_EVENTS), (payload, type) => {
|
||||||
|
const { log_id, status, duration, end_time } = payload
|
||||||
|
|
||||||
|
// 查找列表中的记录
|
||||||
|
const logItem = logs.value.find(l => l.id === log_id)
|
||||||
|
|
||||||
|
if (logItem) {
|
||||||
|
// 更新已有记录状态
|
||||||
|
logItem.status = status
|
||||||
|
if (duration !== undefined) logItem.duration = duration
|
||||||
|
if (end_time !== undefined) logItem.end_time = end_time
|
||||||
|
|
||||||
|
// 如果详情页打开的是这个记录,也同步更新
|
||||||
|
if (selectedLog.value && selectedLog.value.id === log_id) {
|
||||||
|
selectedLog.value.status = status
|
||||||
|
if (duration !== undefined) selectedLog.value.duration = duration
|
||||||
|
if (end_time !== undefined) selectedLog.value.end_time = end_time
|
||||||
|
|
||||||
|
// 如果任务完成了,停止详情页的轮询定时器
|
||||||
|
if (status !== TASK_STATUS.RUNNING && durationTimer) {
|
||||||
|
clearInterval(durationTimer)
|
||||||
|
durationTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (type === TASK_EVENTS.RUNNING) {
|
||||||
|
// 新任务开始执行,如果在第一页且没有特定筛选条件,则刷新列表
|
||||||
|
if (currentPage.value === 1 && !filterTaskId.value && !filterKeyword.value && (!filterStatus.value || filterStatus.value === 'all')) {
|
||||||
|
loadLogs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 全屏查看
|
// 全屏查看
|
||||||
const showFullscreen = ref(false)
|
const showFullscreen = ref(false)
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ const concurrency = ref(0)
|
|||||||
const concurrencyEnabled = ref(false)
|
const concurrencyEnabled = ref(false)
|
||||||
const commentToTaskEnabled = ref(false)
|
const commentToTaskEnabled = ref(false)
|
||||||
const allEnvsEnabled = ref(false)
|
const allEnvsEnabled = ref(false)
|
||||||
const SCRIPTS_DIR_PLACEHPLDER = '$SCRIPTS_DIR$'
|
|
||||||
const scriptsDir = ref<string>(PATHS.SCRIPTS_DIR)
|
const scriptsDir = ref<string>(PATHS.SCRIPTS_DIR)
|
||||||
|
|
||||||
const cronDescription = computed(() => {
|
const cronDescription = computed(() => {
|
||||||
@@ -366,9 +365,9 @@ function removeEnv(id: string) {
|
|||||||
|
|
||||||
function normalizeLocalWorkDirForDisplay(workDir?: string | null): string {
|
function normalizeLocalWorkDirForDisplay(workDir?: string | null): string {
|
||||||
if (!workDir) return ''
|
if (!workDir) return ''
|
||||||
if (workDir === SCRIPTS_DIR_PLACEHPLDER) return ''
|
if (workDir === PATHS.SCRIPTS_DIR_PLACEHOLDER) return ''
|
||||||
if (workDir.startsWith(`${SCRIPTS_DIR_PLACEHPLDER}/`)) {
|
if (workDir.startsWith(`${PATHS.SCRIPTS_DIR_PLACEHOLDER}/`)) {
|
||||||
return workDir.slice(SCRIPTS_DIR_PLACEHPLDER.length + 1)
|
return workDir.slice(PATHS.SCRIPTS_DIR_PLACEHOLDER.length + 1)
|
||||||
}
|
}
|
||||||
const base = scriptsDir.value || PATHS.SCRIPTS_DIR
|
const base = scriptsDir.value || PATHS.SCRIPTS_DIR
|
||||||
if (workDir === base) return ''
|
if (workDir === base) return ''
|
||||||
@@ -380,16 +379,16 @@ function normalizeLocalWorkDirForDisplay(workDir?: string | null): string {
|
|||||||
|
|
||||||
function encodeLocalWorkDir(workDir?: string | null): string {
|
function encodeLocalWorkDir(workDir?: string | null): string {
|
||||||
const value = workDir?.trim() || ''
|
const value = workDir?.trim() || ''
|
||||||
if (!value) return SCRIPTS_DIR_PLACEHPLDER
|
if (!value) return PATHS.SCRIPTS_DIR_PLACEHOLDER
|
||||||
if (value === SCRIPTS_DIR_PLACEHPLDER || value.startsWith(`${SCRIPTS_DIR_PLACEHPLDER}/`)) {
|
if (value === PATHS.SCRIPTS_DIR_PLACEHOLDER || value.startsWith(`${PATHS.SCRIPTS_DIR_PLACEHOLDER}/`)) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
const base = scriptsDir.value || PATHS.SCRIPTS_DIR
|
const base = scriptsDir.value || PATHS.SCRIPTS_DIR
|
||||||
if (value === base) return SCRIPTS_DIR_PLACEHPLDER
|
if (value === base) return PATHS.SCRIPTS_DIR_PLACEHOLDER
|
||||||
if (value.startsWith(`${base}/`)) {
|
if (value.startsWith(`${base}/`)) {
|
||||||
return `${SCRIPTS_DIR_PLACEHPLDER}/${value.slice(base.length + 1)}`
|
return `${PATHS.SCRIPTS_DIR_PLACEHOLDER}/${value.slice(base.length + 1)}`
|
||||||
}
|
}
|
||||||
return `${SCRIPTS_DIR_PLACEHPLDER}/${value.replace(/^\/+/, '')}`
|
return `${PATHS.SCRIPTS_DIR_PLACEHOLDER}/${value.replace(/^\/+/, '')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
|||||||
Reference in New Issue
Block a user