mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
feat: 计划任务支持访问URL类型
This commit is contained in:
+45
-9
@@ -63,10 +63,17 @@ func (r *cronRepo) Get(id uint) (*biz.Cron, error) {
|
||||
|
||||
func (r *cronRepo) Create(ctx context.Context, req *request.CronCreate) error {
|
||||
config := types.CronConfig{
|
||||
Type: req.SubType,
|
||||
Targets: req.Targets,
|
||||
Storage: req.Storage,
|
||||
Keep: req.Keep,
|
||||
Type: req.SubType,
|
||||
Targets: req.Targets,
|
||||
Storage: req.Storage,
|
||||
Keep: req.Keep,
|
||||
URL: req.URL,
|
||||
Method: req.Method,
|
||||
Headers: req.Headers,
|
||||
Body: req.Body,
|
||||
Timeout: req.Timeout,
|
||||
Insecure: req.Insecure,
|
||||
Retries: req.Retries,
|
||||
}
|
||||
script := r.generateScript(req.Type, config, req.Script)
|
||||
|
||||
@@ -111,12 +118,19 @@ func (r *cronRepo) Update(ctx context.Context, req *request.CronUpdate) error {
|
||||
cron.Name = req.Name
|
||||
|
||||
// 根据类型重新生成脚本
|
||||
if req.Type == "backup" || req.Type == "cutoff" {
|
||||
if req.Type != "shell" {
|
||||
config := types.CronConfig{
|
||||
Type: req.SubType,
|
||||
Targets: req.Targets,
|
||||
Storage: req.Storage,
|
||||
Keep: req.Keep,
|
||||
Type: req.SubType,
|
||||
Targets: req.Targets,
|
||||
Storage: req.Storage,
|
||||
Keep: req.Keep,
|
||||
URL: req.URL,
|
||||
Method: req.Method,
|
||||
Headers: req.Headers,
|
||||
Body: req.Body,
|
||||
Timeout: req.Timeout,
|
||||
Insecure: req.Insecure,
|
||||
Retries: req.Retries,
|
||||
}
|
||||
cron.Config = config
|
||||
script := r.generateScript(req.Type, config, "")
|
||||
@@ -270,6 +284,28 @@ func (r *cronRepo) generateScript(typ string, config types.CronConfig, rawScript
|
||||
sb.WriteString(fmt.Sprintf("acepanel cutoff clear -t container -n '%s' -k '%d' -s '%d'\n", target, config.Keep, config.Storage))
|
||||
}
|
||||
}
|
||||
case "url":
|
||||
method := config.Method
|
||||
if method == "" {
|
||||
method = "GET"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("curl -sSL -X %s", method))
|
||||
if config.Timeout > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" --connect-timeout %d", config.Timeout))
|
||||
}
|
||||
if config.Insecure {
|
||||
sb.WriteString(" -k")
|
||||
}
|
||||
if config.Retries > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" --retry %d", config.Retries))
|
||||
}
|
||||
for key, value := range config.Headers {
|
||||
sb.WriteString(fmt.Sprintf(" -H '%s: %s'", strings.ReplaceAll(key, "'", "'\"'\"'"), strings.ReplaceAll(value, "'", "'\"'\"'")))
|
||||
}
|
||||
if config.Body != "" {
|
||||
sb.WriteString(fmt.Sprintf(" -d '%s'", strings.ReplaceAll(config.Body, "'", "'\"'\"'")))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" '%s'\n", strings.ReplaceAll(config.URL, "'", "'\"'\"'")))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
|
||||
@@ -1,26 +1,40 @@
|
||||
package request
|
||||
|
||||
type CronCreate struct {
|
||||
Name string `form:"name" json:"name" validate:"required|notExists:crons,name"`
|
||||
Type string `form:"type" json:"type" validate:"required"`
|
||||
Time string `form:"time" json:"time" validate:"required|cron"`
|
||||
Script string `form:"script" json:"script"`
|
||||
SubType string `form:"sub_type" json:"sub_type" validate:"requiredIf:Type,backup,cutoff"`
|
||||
Storage uint `form:"storage" json:"storage"`
|
||||
Targets []string `form:"targets" json:"targets" validate:"requiredIf:Type,backup,cutoff"`
|
||||
Keep uint `form:"keep" json:"keep" validate:"required"`
|
||||
Name string `form:"name" json:"name" validate:"required|notExists:crons,name"`
|
||||
Type string `form:"type" json:"type" validate:"required"`
|
||||
Time string `form:"time" json:"time" validate:"required|cron"`
|
||||
Script string `form:"script" json:"script"`
|
||||
SubType string `form:"sub_type" json:"sub_type" validate:"requiredIf:Type,backup,cutoff"`
|
||||
Storage uint `form:"storage" json:"storage"`
|
||||
Targets []string `form:"targets" json:"targets" validate:"requiredIf:Type,backup,cutoff"`
|
||||
Keep uint `form:"keep" json:"keep" validate:"required"`
|
||||
URL string `form:"url" json:"url"`
|
||||
Method string `form:"method" json:"method"`
|
||||
Headers map[string]string `form:"headers" json:"headers"`
|
||||
Body string `form:"body" json:"body"`
|
||||
Timeout uint `form:"timeout" json:"timeout"`
|
||||
Insecure bool `form:"insecure" json:"insecure"`
|
||||
Retries uint `form:"retries" json:"retries"`
|
||||
}
|
||||
|
||||
type CronUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required|exists:crons,id"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Type string `form:"type" json:"type" validate:"required"`
|
||||
Time string `form:"time" json:"time" validate:"required|cron"`
|
||||
Script string `form:"script" json:"script"`
|
||||
SubType string `form:"sub_type" json:"sub_type"`
|
||||
Storage uint `form:"storage" json:"storage"`
|
||||
Targets []string `form:"targets" json:"targets"`
|
||||
Keep uint `form:"keep" json:"keep"`
|
||||
ID uint `form:"id" json:"id" validate:"required|exists:crons,id"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Type string `form:"type" json:"type" validate:"required"`
|
||||
Time string `form:"time" json:"time" validate:"required|cron"`
|
||||
Script string `form:"script" json:"script"`
|
||||
SubType string `form:"sub_type" json:"sub_type"`
|
||||
Storage uint `form:"storage" json:"storage"`
|
||||
Targets []string `form:"targets" json:"targets"`
|
||||
Keep uint `form:"keep" json:"keep"`
|
||||
URL string `form:"url" json:"url"`
|
||||
Method string `form:"method" json:"method"`
|
||||
Headers map[string]string `form:"headers" json:"headers"`
|
||||
Body string `form:"body" json:"body"`
|
||||
Timeout uint `form:"timeout" json:"timeout"`
|
||||
Insecure bool `form:"insecure" json:"insecure"`
|
||||
Retries uint `form:"retries" json:"retries"`
|
||||
}
|
||||
|
||||
type CronStatus struct {
|
||||
|
||||
@@ -6,4 +6,12 @@ type CronConfig struct {
|
||||
Targets []string `json:"targets"` // 目标列表
|
||||
Storage uint `json:"storage"` // 存储 ID(0=本地)
|
||||
Keep uint `json:"keep"` // 保留份数
|
||||
// URL 任务专用
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"` // GET/POST/PUT/DELETE/PATCH/HEAD
|
||||
Headers map[string]string `json:"headers"` // 自定义请求头
|
||||
Body string `json:"body"` // 请求体
|
||||
Timeout uint `json:"timeout"` // 超时时间(秒)
|
||||
Insecure bool `json:"insecure"` // 忽略证书校验
|
||||
Retries uint `json:"retries"` // 失败重试次数
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import app from '@/api/panel/app'
|
||||
import container from '@/api/panel/container'
|
||||
import database from '@/api/panel/database'
|
||||
import storage from '@/api/panel/backup-storage'
|
||||
import container from '@/api/panel/container'
|
||||
import cron from '@/api/panel/cron'
|
||||
import database from '@/api/panel/database'
|
||||
import home from '@/api/panel/home'
|
||||
import website from '@/api/panel/website'
|
||||
import CronSelector from '@/components/common/CronSelector.vue'
|
||||
@@ -32,9 +32,18 @@ const defaultModel = () => ({
|
||||
`#!/bin/bash\nexport PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH\n\n` +
|
||||
$gettext('# Enter your script content here') +
|
||||
`\n`,
|
||||
time: '*/30 * * * *'
|
||||
time: '*/30 * * * *',
|
||||
url: '',
|
||||
method: 'GET',
|
||||
headers: {} as Record<string, string>,
|
||||
body: '',
|
||||
timeout: 10,
|
||||
insecure: false,
|
||||
retries: 0
|
||||
})
|
||||
|
||||
const headerList = ref<{ key: string; value: string }[]>([])
|
||||
|
||||
const formModel = ref(defaultModel())
|
||||
|
||||
const websites = ref<any[]>([])
|
||||
@@ -65,15 +74,24 @@ const containerInstalled = ref(false)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
// 提交前将 headerList 同步到 headers map
|
||||
const headersMap: Record<string, string> = {}
|
||||
for (const item of headerList.value) {
|
||||
if (item.key) {
|
||||
headersMap[item.key] = item.value
|
||||
}
|
||||
}
|
||||
formModel.value.headers = headersMap
|
||||
const data = { ...formModel.value }
|
||||
const request =
|
||||
props.mode === 'create' ? cron.create(data) : cron.update(data.id, data)
|
||||
const request = props.mode === 'create' ? cron.create(data) : cron.update(data.id, data)
|
||||
|
||||
useRequest(request)
|
||||
.onSuccess(() => {
|
||||
show.value = false
|
||||
window.$message.success(
|
||||
props.mode === 'create' ? $gettext('Created successfully') : $gettext('Modified successfully')
|
||||
props.mode === 'create'
|
||||
? $gettext('Created successfully')
|
||||
: $gettext('Modified successfully')
|
||||
)
|
||||
emit('saved')
|
||||
window.$bus.emit('task:refresh-cron')
|
||||
@@ -102,12 +120,22 @@ const generateTaskName = () => {
|
||||
}
|
||||
const prefix = cutoffTypeMap[formModel.value.sub_type] || $gettext('Log Rotation')
|
||||
formModel.value.name = targets.length ? `${prefix} - ${targets.join(', ')}` : prefix
|
||||
} else if (type === 'url') {
|
||||
formModel.value.name = formModel.value.url
|
||||
? `${formModel.value.method} - ${formModel.value.url}`
|
||||
: $gettext('Access URL')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听类型变更自动生成名称
|
||||
watch(
|
||||
() => [formModel.value.type, formModel.value.sub_type, formModel.value.targets],
|
||||
() => [
|
||||
formModel.value.type,
|
||||
formModel.value.sub_type,
|
||||
formModel.value.targets,
|
||||
formModel.value.url,
|
||||
formModel.value.method
|
||||
],
|
||||
() => {
|
||||
if (props.mode === 'create') {
|
||||
generateTaskName()
|
||||
@@ -169,20 +197,35 @@ watch(show, (val) => {
|
||||
keep: config.keep || 1,
|
||||
sub_type: config.type || '',
|
||||
storage: config.storage || 0,
|
||||
script: ''
|
||||
script: '',
|
||||
url: config.url || '',
|
||||
method: config.method || 'GET',
|
||||
headers: config.headers || {},
|
||||
body: config.body || '',
|
||||
timeout: config.timeout ?? 10,
|
||||
insecure: config.insecure ?? false,
|
||||
retries: config.retries ?? 0
|
||||
}
|
||||
nextTick(() => {
|
||||
skipClearTargets.value = false
|
||||
})
|
||||
// 回填 headerList
|
||||
headerList.value = Object.entries(config.headers || {}).map(
|
||||
([key, value]: [string, any]) => ({ key, value })
|
||||
)
|
||||
// 加载关联数据
|
||||
if (props.editData.type === 'cutoff' && config.type === 'container') {
|
||||
loadContainers()
|
||||
}
|
||||
if (props.editData.type === 'backup' && (config.type === 'mysql' || config.type === 'postgres')) {
|
||||
if (
|
||||
props.editData.type === 'backup' &&
|
||||
(config.type === 'mysql' || config.type === 'postgres')
|
||||
) {
|
||||
loadDatabases(config.type)
|
||||
}
|
||||
} else {
|
||||
formModel.value = defaultModel()
|
||||
headerList.value = []
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -245,20 +288,24 @@ onMounted(() => {
|
||||
:options="[
|
||||
{ label: $gettext('Run Script'), value: 'shell' },
|
||||
{ label: $gettext('Backup Data'), value: 'backup' },
|
||||
{ label: $gettext('Log Rotation'), value: 'cutoff' }
|
||||
{ label: $gettext('Log Rotation'), value: 'cutoff' },
|
||||
{ label: $gettext('Access URL'), value: 'url' }
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Task Name')">
|
||||
<n-input v-model:value="formModel.name" :placeholder="$gettext('Task Name')" />
|
||||
</n-form-item>
|
||||
<n-grid :cols="formModel.type !== 'shell' ? 2 : 1" :x-gap="16">
|
||||
<n-grid
|
||||
:cols="formModel.type === 'backup' || formModel.type === 'cutoff' ? 2 : 1"
|
||||
:x-gap="16"
|
||||
>
|
||||
<n-gi>
|
||||
<n-form-item :label="$gettext('Task Schedule')">
|
||||
<cron-selector v-model:value="formModel.time" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi v-if="formModel.type !== 'shell'">
|
||||
<n-gi v-if="formModel.type === 'backup' || formModel.type === 'cutoff'">
|
||||
<n-form-item :label="$gettext('Retention Count')">
|
||||
<n-input-number v-model:value="formModel.keep" :min="1" w-full />
|
||||
</n-form-item>
|
||||
@@ -268,6 +315,75 @@ onMounted(() => {
|
||||
<n-text>{{ $gettext('Script Content') }}</n-text>
|
||||
<common-editor v-model:value="formModel.script" lang="shell" height="40vh" />
|
||||
</div>
|
||||
<!-- URL 类型 -->
|
||||
<template v-if="formModel.type === 'url'">
|
||||
<n-form-item :label="$gettext('Request Method')">
|
||||
<n-select
|
||||
v-model:value="formModel.method"
|
||||
:options="[
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'DELETE', value: 'DELETE' },
|
||||
{ label: 'PATCH', value: 'PATCH' },
|
||||
{ label: 'HEAD', value: 'HEAD' }
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="URL">
|
||||
<n-input v-model:value="formModel.url" placeholder="https://example.com" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Request Headers')">
|
||||
<n-dynamic-input v-model:value="headerList" :on-create="() => ({ key: '', value: '' })">
|
||||
<template #default="{ value }">
|
||||
<div style="display: flex; align-items: center; width: 100%; gap: 8px">
|
||||
<n-input
|
||||
v-model:value="value.key"
|
||||
:placeholder="$gettext('Header Name')"
|
||||
style="flex: 1"
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="value.value"
|
||||
:placeholder="$gettext('Header Value')"
|
||||
style="flex: 2"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</n-dynamic-input>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
v-if="
|
||||
formModel.method === 'POST' ||
|
||||
formModel.method === 'PUT' ||
|
||||
formModel.method === 'PATCH'
|
||||
"
|
||||
:label="$gettext('Request Body')"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="formModel.body"
|
||||
type="textarea"
|
||||
:placeholder="$gettext('Request Body')"
|
||||
:autosize="{ minRows: 3, maxRows: 10 }"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-grid :cols="3" :x-gap="16">
|
||||
<n-gi>
|
||||
<n-form-item :label="$gettext('Timeout (seconds)')">
|
||||
<n-input-number v-model:value="formModel.timeout" :min="0" w-full />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item :label="$gettext('Retry Count')">
|
||||
<n-input-number v-model:value="formModel.retries" :min="0" w-full />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-form-item :label="$gettext('Ignore Certificate')">
|
||||
<n-switch v-model:value="formModel.insecure" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
<!-- 备份类型 -->
|
||||
<n-form-item v-if="formModel.type === 'backup'" :label="$gettext('Backup Type')">
|
||||
<n-radio-group v-model:value="formModel.sub_type">
|
||||
@@ -284,7 +400,9 @@ onMounted(() => {
|
||||
<n-form-item v-if="formModel.type === 'cutoff'" :label="$gettext('Rotation Type')">
|
||||
<n-radio-group v-model:value="formModel.sub_type">
|
||||
<n-radio value="website">{{ $gettext('Website') }}</n-radio>
|
||||
<n-radio value="container" :disabled="!containerInstalled">{{ $gettext('Container') }}</n-radio>
|
||||
<n-radio value="container" :disabled="!containerInstalled">{{
|
||||
$gettext('Container')
|
||||
}}</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<!-- 网站多选 -->
|
||||
@@ -304,7 +422,10 @@ onMounted(() => {
|
||||
</n-form-item>
|
||||
<!-- 数据库多选 -->
|
||||
<n-form-item
|
||||
v-if="formModel.type === 'backup' && (formModel.sub_type === 'mysql' || formModel.sub_type === 'postgres')"
|
||||
v-if="
|
||||
formModel.type === 'backup' &&
|
||||
(formModel.sub_type === 'mysql' || formModel.sub_type === 'postgres')
|
||||
"
|
||||
:label="$gettext('Select Database')"
|
||||
>
|
||||
<n-select
|
||||
|
||||
@@ -50,7 +50,7 @@ const columns: any = [
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: row.type === 'shell' ? 'warning' : row.type === 'backup' ? 'success' : 'info'
|
||||
type: row.type === 'shell' ? 'warning' : row.type === 'backup' ? 'success' : row.type === 'url' ? 'default' : 'info'
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
@@ -58,7 +58,9 @@ const columns: any = [
|
||||
? $gettext('Run Script')
|
||||
: row.type === 'backup'
|
||||
? $gettext('Backup Data')
|
||||
: $gettext('Log Rotation')
|
||||
: row.type === 'url'
|
||||
? $gettext('Access URL')
|
||||
: $gettext('Log Rotation')
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -209,7 +211,7 @@ const handleRun = (row: any) => {
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
if (row.type === 'backup' || row.type === 'cutoff') {
|
||||
if (row.type === 'backup' || row.type === 'cutoff' || row.type === 'url') {
|
||||
// 可视化编辑
|
||||
useRequest(cron.get(row.id)).onSuccess(({ data }) => {
|
||||
visualEditData.value = data
|
||||
|
||||
Reference in New Issue
Block a user