fix: task message push config fixing
This commit is contained in:
@@ -140,7 +140,7 @@ func (m *CronManager) AddTask(task CronTask) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.entryMap[taskID] = entryID
|
m.entryMap[taskID] = entryID
|
||||||
m.logger.Infof("[CronManager] 任务已调度 #%s %s (%s)", taskID, name, task.GetSchedule())
|
m.logger.Infof("[CronManager] 任务已添加调度 #%s %s (%s)", taskID, name, task.GetSchedule())
|
||||||
|
|
||||||
// 初始触发一次下次运行时间通知
|
// 初始触发一次下次运行时间通知
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -357,17 +357,17 @@ func (s *NotificationService) handleEvent(bindingType string) eventbus.Handler {
|
|||||||
text = fmt.Sprintf("用户 %v 刚刚修改了密码", payload["username"])
|
text = fmt.Sprintf("用户 %v 刚刚修改了密码", payload["username"])
|
||||||
case constant.EventTaskSuccess:
|
case constant.EventTaskSuccess:
|
||||||
title = fmt.Sprintf("任务[%v] 成功", payload["task_name"])
|
title = fmt.Sprintf("任务[%v] 成功", payload["task_name"])
|
||||||
text = fmt.Sprintf("任务 #%v %v\n状态: 成功\n耗时: %vms", payload["task_id"], payload["task_name"], payload["duration"])
|
text = fmt.Sprintf("任务 #%v %v\n状态: 成功\n执行时间: %v\n耗时: %vms", payload["task_id"], payload["task_name"], payload["start_time"], payload["duration"])
|
||||||
case constant.EventTaskFailed:
|
case constant.EventTaskFailed:
|
||||||
title = fmt.Sprintf("任务[%v] 失败", payload["task_name"])
|
title = fmt.Sprintf("任务[%v] 失败", payload["task_name"])
|
||||||
if errStr, ok := payload["error"]; ok {
|
if errStr, ok := payload["error"]; ok {
|
||||||
text = fmt.Sprintf("任务 #%v %v\n执行失败\n错误: %v", payload["task_id"], payload["task_name"], errStr)
|
text = fmt.Sprintf("任务 #%v %v\n执行失败\n执行时间: %v\n错误: %v", payload["task_id"], payload["task_name"], payload["start_time"], errStr)
|
||||||
} else {
|
} else {
|
||||||
text = fmt.Sprintf("任务 #%v %v\n执行失败\n状态: %v\n耗时: %vms", payload["task_id"], payload["task_name"], payload["status"], payload["duration"])
|
text = fmt.Sprintf("任务 #%v %v\n执行失败\n状态: %v\n执行时间: %v\n耗时: %vms", payload["task_id"], payload["task_name"], payload["status"], payload["start_time"], payload["duration"])
|
||||||
}
|
}
|
||||||
case constant.EventTaskTimeout:
|
case constant.EventTaskTimeout:
|
||||||
title = fmt.Sprintf("任务[%v] 超时", payload["task_name"])
|
title = fmt.Sprintf("任务[%v] 超时", payload["task_name"])
|
||||||
text = fmt.Sprintf("任务 #%v %v\n执行超时\n耗时: %vms", payload["task_id"], payload["task_name"], payload["duration"])
|
text = fmt.Sprintf("任务 #%v %v\n执行超时\n执行时间: %v\n耗时: %vms", payload["task_id"], payload["task_name"], payload["start_time"], payload["duration"])
|
||||||
case constant.EventSystemNotice:
|
case constant.EventSystemNotice:
|
||||||
title, _ = payload["title"].(string)
|
title, _ = payload["title"].(string)
|
||||||
text, _ = payload["content"].(string)
|
text, _ = payload["content"].(string)
|
||||||
|
|||||||
@@ -265,12 +265,13 @@ 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{}{
|
||||||
"task_id": task.ID,
|
"task_id": task.ID,
|
||||||
"task_name": task.Name,
|
"task_name": task.Name,
|
||||||
"status": result.Status,
|
"status": result.Status,
|
||||||
"duration": result.Duration,
|
"start_time": result.StartTime.Format("2006-01-02 15:04:05"),
|
||||||
"output": result.Output,
|
"duration": result.Duration,
|
||||||
"error": result.Error,
|
"output": result.Output,
|
||||||
|
"error": result.Error,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -317,8 +317,8 @@ function setLogLimit(binding: NotifyBinding, limit: number) {
|
|||||||
<FileText class="w-2.5 h-2.5" />
|
<FileText class="w-2.5 h-2.5" />
|
||||||
<span class="text-[9px]">发送日志</span>
|
<span class="text-[9px]">发送日志</span>
|
||||||
</div>
|
</div>
|
||||||
<Switch :checked="isLogEnabled(binding)"
|
<Switch :model-value="isLogEnabled(binding)"
|
||||||
@update:checked="(val: boolean) => toggleLog(binding, val)" class="scale-75 origin-right" />
|
@update:model-value="(val: boolean) => toggleLog(binding, val)" class="scale-75 origin-right" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 日志字数限制配置 -->
|
<!-- 日志字数限制配置 -->
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import { api, type Task, type RepoConfig, type Agent, type MiseLanguage } from '
|
|||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { getCronDescription } from '@/utils/cron'
|
import { getCronDescription } from '@/utils/cron'
|
||||||
|
import TaskNotificationConfig from './components/TaskNotificationConfig.vue'
|
||||||
|
|
||||||
|
const notificationConfigRef = ref<InstanceType<typeof TaskNotificationConfig> | null>(null)
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -361,6 +364,8 @@ watch(() => props.open, async (val) => {
|
|||||||
updateAvailableVersions(lang)
|
updateAvailableVersions(lang)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 加载通知配置
|
||||||
|
await notificationConfigRef.value?.loadConfig(props.isEdit ? props.task?.id : undefined)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -400,9 +405,11 @@ async function save() {
|
|||||||
form.value.agent_id = selectedAgentId.value === 'local' ? null : 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)
|
||||||
|
await notificationConfigRef.value?.saveConfig(form.value.id)
|
||||||
toast.success('同步任务已更新')
|
toast.success('同步任务已更新')
|
||||||
} else {
|
} else {
|
||||||
await api.tasks.create(form.value)
|
const task = await api.tasks.create(form.value)
|
||||||
|
await notificationConfigRef.value?.saveConfig(task.id)
|
||||||
toast.success('同步任务已创建')
|
toast.success('同步任务已创建')
|
||||||
}
|
}
|
||||||
emit('update:open', false)
|
emit('update:open', false)
|
||||||
@@ -535,7 +542,7 @@ async function save() {
|
|||||||
<Label class="sm:text-right text-xs text-muted-foreground uppercase tracking-wider">下载模式</Label>
|
<Label class="sm:text-right text-xs text-muted-foreground uppercase tracking-wider">下载模式</Label>
|
||||||
<div class="sm:col-span-3">
|
<div class="sm:col-span-3">
|
||||||
<div class="flex items-center space-x-2 bg-muted/20 px-3 py-1.5 rounded-full border border-muted-foreground/10 w-fit">
|
<div class="flex items-center space-x-2 bg-muted/20 px-3 py-1.5 rounded-full border border-muted-foreground/10 w-fit">
|
||||||
<Checkbox id="single-file-sync" v-model:checked="isSingleFile" class="scale-90" />
|
<Checkbox id="single-file-sync" v-model="isSingleFile" class="scale-90" />
|
||||||
<Label for="single-file-sync" class="text-[11px] font-medium cursor-pointer">作为单文件直接下载</Label>
|
<Label for="single-file-sync" class="text-[11px] font-medium cursor-pointer">作为单文件直接下载</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -821,6 +828,9 @@ async function save() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 通知配置 -->
|
||||||
|
<TaskNotificationConfig ref="notificationConfigRef" :task-id="isEdit ? task?.id : undefined" />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import DirTreeSelect from '@/components/DirTreeSelect.vue'
|
import DirTreeSelect from '@/components/DirTreeSelect.vue'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Plus, ChevronDown, X, Search, Check, ChevronsUpDown, AlertCircle, Terminal, Clock, Zap } from 'lucide-vue-next'
|
||||||
import { Plus, ChevronDown, X, Search, Check, ChevronsUpDown, Loader2, AlertCircle, Terminal, Clock, Zap, Bell } from 'lucide-vue-next'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { api, type Task, type EnvVar, type Agent, type MiseLanguage, type NotifyChannel, type NotifyBinding } from '@/api'
|
import { api, type Task, type EnvVar, type Agent, type MiseLanguage } from '@/api'
|
||||||
import { PATHS, TRIGGER_TYPE } from '@/constants'
|
import { PATHS, TRIGGER_TYPE } from '@/constants'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { getCronDescription } from '@/utils/cron'
|
import { getCronDescription } from '@/utils/cron'
|
||||||
|
import TaskNotificationConfig from './components/TaskNotificationConfig.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -55,7 +55,7 @@ const workDirCache = ref<Record<string, string>>({})
|
|||||||
const concurrency = ref(0)
|
const concurrency = ref(0)
|
||||||
const concurrencyEnabled = ref(false)
|
const concurrencyEnabled = ref(false)
|
||||||
const allEnvsEnabled = ref(false)
|
const allEnvsEnabled = ref(false)
|
||||||
const SCRIPTS_DIR_PLACEHOLDER = '$SCRIPTS_DIR$'
|
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(() => {
|
||||||
@@ -140,129 +140,7 @@ const availablePlugins = ref<string[]>([])
|
|||||||
const pluginSearch = ref('')
|
const pluginSearch = ref('')
|
||||||
const versionSearch = ref('')
|
const versionSearch = ref('')
|
||||||
|
|
||||||
// 通知配置相关
|
const notificationConfigRef = ref<InstanceType<typeof TaskNotificationConfig> | null>(null)
|
||||||
const notifyChannels = ref<NotifyChannel[]>([])
|
|
||||||
const notifyWayId = ref<string>('none')
|
|
||||||
const notifyOnSuccess = ref(false)
|
|
||||||
const notifyOnFailure = ref(false)
|
|
||||||
const notifyOnTimeout = ref(false)
|
|
||||||
const notifyIncludeLog = ref(false)
|
|
||||||
const notifyLogLimit = ref(1000)
|
|
||||||
|
|
||||||
async function loadNotificationConfig() {
|
|
||||||
try {
|
|
||||||
notifyChannels.value = await api.notify.getChannels()
|
|
||||||
if (props.isEdit && props.task?.id) {
|
|
||||||
const allBindings = await api.notify.getBindings()
|
|
||||||
const taskBindings = allBindings.filter(b => b.data_id === props.task!.id && b.type === 'task')
|
|
||||||
|
|
||||||
console.log('DEBUG [loadNotificationConfig] TaskID:', props.task!.id, 'Total:', allBindings.length, 'Matched:', taskBindings.length)
|
|
||||||
|
|
||||||
if (taskBindings.length > 0) {
|
|
||||||
notifyWayId.value = taskBindings[0].way_id
|
|
||||||
|
|
||||||
// 使用 nextTick 确保 UI 响应
|
|
||||||
setTimeout(() => {
|
|
||||||
notifyOnSuccess.value = taskBindings.some(b => b.event === 'task_success')
|
|
||||||
notifyOnFailure.value = taskBindings.some(b => b.event === 'task_failed')
|
|
||||||
notifyOnTimeout.value = taskBindings.some(b => b.event === 'task_timeout')
|
|
||||||
|
|
||||||
console.log('DEBUG [loadNotificationConfig] States Set:', {
|
|
||||||
success: notifyOnSuccess.value,
|
|
||||||
failure: notifyOnFailure.value,
|
|
||||||
timeout: notifyOnTimeout.value
|
|
||||||
})
|
|
||||||
|
|
||||||
// 尝试解析日志配置
|
|
||||||
const extraBinding = taskBindings.find(b => b.extra && b.extra !== '')
|
|
||||||
if (extraBinding) {
|
|
||||||
try {
|
|
||||||
const extra = JSON.parse(extraBinding.extra)
|
|
||||||
notifyIncludeLog.value = !!extra.enable_log
|
|
||||||
notifyLogLimit.value = extra.log_limit || 1000
|
|
||||||
} catch {
|
|
||||||
notifyIncludeLog.value = false
|
|
||||||
notifyLogLimit.value = 1000
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
notifyIncludeLog.value = false
|
|
||||||
notifyLogLimit.value = 1000
|
|
||||||
}
|
|
||||||
}, 50)
|
|
||||||
} else {
|
|
||||||
resetNotificationConfig()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resetNotificationConfig()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Load notifications failed', e)
|
|
||||||
resetNotificationConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetNotificationConfig() {
|
|
||||||
notifyWayId.value = 'none'
|
|
||||||
notifyOnSuccess.value = false
|
|
||||||
notifyOnFailure.value = false
|
|
||||||
notifyOnTimeout.value = false
|
|
||||||
notifyIncludeLog.value = false
|
|
||||||
notifyLogLimit.value = 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveNotifications(taskId: string) {
|
|
||||||
console.log('DEBUG [saveNotifications]:', {
|
|
||||||
taskId,
|
|
||||||
notifyWayId: notifyWayId.value,
|
|
||||||
notifyOnSuccess: notifyOnSuccess.value,
|
|
||||||
notifyOnFailure: notifyOnFailure.value,
|
|
||||||
notifyOnTimeout: notifyOnTimeout.value,
|
|
||||||
notifyIncludeLog: notifyIncludeLog.value,
|
|
||||||
notifyLogLimit: notifyLogLimit.value
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const bindings: Partial<NotifyBinding>[] = []
|
|
||||||
|
|
||||||
if (notifyWayId.value !== 'none') {
|
|
||||||
const events = [
|
|
||||||
{ type: 'task_success', enabled: notifyOnSuccess.value },
|
|
||||||
{ type: 'task_failed', enabled: notifyOnFailure.value },
|
|
||||||
{ type: 'task_timeout', enabled: notifyOnTimeout.value }
|
|
||||||
]
|
|
||||||
|
|
||||||
const extra = JSON.stringify({
|
|
||||||
enable_log: notifyIncludeLog.value,
|
|
||||||
log_limit: notifyLogLimit.value
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const event of events) {
|
|
||||||
if (event.enabled) {
|
|
||||||
bindings.push({
|
|
||||||
event: event.type,
|
|
||||||
way_id: notifyWayId.value,
|
|
||||||
extra: extra
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('DEBUG [saveNotifications] payload bindings:', bindings)
|
|
||||||
|
|
||||||
// 调用批量保存,后端会自动清理该任务旧的绑定
|
|
||||||
await api.notify.saveBindingsBatch({
|
|
||||||
type: 'task',
|
|
||||||
data_id: taskId,
|
|
||||||
bindings: bindings
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Save notifications failed', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(notifyOnSuccess, (val) => console.log('DEBUG [notifyOnSuccess]:', val))
|
|
||||||
watch(notifyOnFailure, (val) => console.log('DEBUG [notifyOnFailure]:', val))
|
|
||||||
watch(notifyOnTimeout, (val) => console.log('DEBUG [notifyOnTimeout]:', val))
|
|
||||||
|
|
||||||
const filteredPlugins = computed(() => {
|
const filteredPlugins = computed(() => {
|
||||||
if (!pluginSearch.value) return availablePlugins.value
|
if (!pluginSearch.value) return availablePlugins.value
|
||||||
@@ -455,7 +333,7 @@ watch(() => props.open, async (val) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 加载通知配置
|
// 加载通知配置
|
||||||
await loadNotificationConfig()
|
await notificationConfigRef.value?.loadConfig(props.isEdit ? props.task?.id : undefined)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -484,9 +362,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_PLACEHOLDER) return ''
|
if (workDir === SCRIPTS_DIR_PLACEHPLDER) return ''
|
||||||
if (workDir.startsWith(`${SCRIPTS_DIR_PLACEHOLDER}/`)) {
|
if (workDir.startsWith(`${SCRIPTS_DIR_PLACEHPLDER}/`)) {
|
||||||
return workDir.slice(SCRIPTS_DIR_PLACEHOLDER.length + 1)
|
return workDir.slice(SCRIPTS_DIR_PLACEHPLDER.length + 1)
|
||||||
}
|
}
|
||||||
const base = scriptsDir.value || PATHS.SCRIPTS_DIR
|
const base = scriptsDir.value || PATHS.SCRIPTS_DIR
|
||||||
if (workDir === base) return ''
|
if (workDir === base) return ''
|
||||||
@@ -498,16 +376,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_PLACEHOLDER
|
if (!value) return SCRIPTS_DIR_PLACEHPLDER
|
||||||
if (value === SCRIPTS_DIR_PLACEHOLDER || value.startsWith(`${SCRIPTS_DIR_PLACEHOLDER}/`)) {
|
if (value === SCRIPTS_DIR_PLACEHPLDER || value.startsWith(`${SCRIPTS_DIR_PLACEHPLDER}/`)) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
const base = scriptsDir.value || PATHS.SCRIPTS_DIR
|
const base = scriptsDir.value || PATHS.SCRIPTS_DIR
|
||||||
if (value === base) return SCRIPTS_DIR_PLACEHOLDER
|
if (value === base) return SCRIPTS_DIR_PLACEHPLDER
|
||||||
if (value.startsWith(`${base}/`)) {
|
if (value.startsWith(`${base}/`)) {
|
||||||
return `${SCRIPTS_DIR_PLACEHOLDER}/${value.slice(base.length + 1)}`
|
return `${SCRIPTS_DIR_PLACEHPLDER}/${value.slice(base.length + 1)}`
|
||||||
}
|
}
|
||||||
return `${SCRIPTS_DIR_PLACEHOLDER}/${value.replace(/^\/+/, '')}`
|
return `${SCRIPTS_DIR_PLACEHPLDER}/${value.replace(/^\/+/, '')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
@@ -554,11 +432,11 @@ async function save() {
|
|||||||
|
|
||||||
if (props.isEdit && form.value.id) {
|
if (props.isEdit && form.value.id) {
|
||||||
const task = await api.tasks.update(form.value.id, form.value)
|
const task = await api.tasks.update(form.value.id, form.value)
|
||||||
await saveNotifications(task.id)
|
await notificationConfigRef.value?.saveConfig(task.id)
|
||||||
toast.success('任务已更新')
|
toast.success('任务已更新')
|
||||||
} else {
|
} else {
|
||||||
const task = await api.tasks.create(form.value)
|
const task = await api.tasks.create(form.value)
|
||||||
await saveNotifications(task.id)
|
await notificationConfigRef.value?.saveConfig(task.id)
|
||||||
toast.success('任务已创建')
|
toast.success('任务已创建')
|
||||||
}
|
}
|
||||||
emit('update:open', false)
|
emit('update:open', false)
|
||||||
@@ -994,79 +872,10 @@ async function save() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<!-- 通知配置 -->
|
||||||
<!-- 通知配置 Section -->
|
<TaskNotificationConfig ref="notificationConfigRef" :task-id="isEdit ? task?.id : undefined" />
|
||||||
<section class="space-y-4">
|
</div>
|
||||||
<div class="flex items-center gap-2 mb-1">
|
</ScrollArea>
|
||||||
<div class="h-4 w-1 bg-primary rounded-full" />
|
|
||||||
<h3 class="text-sm font-semibold text-foreground/80">通知配置</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-4 pl-3 border-l border-muted">
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-3">
|
|
||||||
<Label class="sm:text-right text-xs text-muted-foreground uppercase tracking-wider">通知渠道</Label>
|
|
||||||
<div class="sm:col-span-3">
|
|
||||||
<Select v-model="notifyWayId">
|
|
||||||
<SelectTrigger class="h-9 bg-muted/30 border-muted-foreground/20">
|
|
||||||
<SelectValue placeholder="不启用通知" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">不启用通知</SelectItem>
|
|
||||||
<SelectItem v-for="ch in notifyChannels" :key="ch.id" :value="ch.id">
|
|
||||||
{{ ch.name }}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="notifyWayId !== 'none'">
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-start gap-3">
|
|
||||||
<Label class="sm:text-right text-xs text-muted-foreground uppercase tracking-wider pt-1.5">通知时机</Label>
|
|
||||||
<div class="sm:col-span-3 space-y-3">
|
|
||||||
<div class="flex flex-wrap gap-4 p-3 rounded-lg bg-muted/20 border border-muted-foreground/10 items-center">
|
|
||||||
<div class="flex items-center gap-2 group">
|
|
||||||
<Checkbox :id="`ns-${props.task?.id || 'new'}`" v-model:checked="notifyOnSuccess" />
|
|
||||||
<label :for="`ns-${props.task?.id || 'new'}`" class="text-xs shrink-0 cursor-pointer group-hover:text-primary transition-colors">成功时</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 group">
|
|
||||||
<Checkbox :id="`nf-${props.task?.id || 'new'}`" v-model:checked="notifyOnFailure" />
|
|
||||||
<label :for="`nf-${props.task?.id || 'new'}`" class="text-xs shrink-0 cursor-pointer group-hover:text-primary transition-colors">失败时</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 group">
|
|
||||||
<Checkbox :id="`nt-${props.task?.id || 'new'}`" v-model:checked="notifyOnTimeout" />
|
|
||||||
<label :for="`nt-${props.task?.id || 'new'}`" class="text-xs shrink-0 cursor-pointer group-hover:text-primary transition-colors">超时时</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-3 rounded-xl bg-primary/5 border border-primary/10 space-y-3">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2 text-xs font-semibold">
|
|
||||||
<Bell :class="cn('h-3.5 w-3.5', notifyIncludeLog ? 'text-primary' : 'text-muted-foreground')" />
|
|
||||||
附带执行日志
|
|
||||||
</div>
|
|
||||||
<Switch :model-value="notifyIncludeLog" @update:model-value="(v: boolean) => notifyIncludeLog = v" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="notifyIncludeLog" class="flex items-center gap-2 animate-in fade-in slide-in-from-top-1 duration-200 pl-5">
|
|
||||||
<div class="flex items-center gap-2 px-2.5 py-1 rounded-full bg-background/60 border border-muted-foreground/10 focus-within:border-primary/30 transition-all shadow-sm">
|
|
||||||
<span class="text-[10px] text-muted-foreground opacity-60 whitespace-nowrap">长度限制</span>
|
|
||||||
<div class="h-3 w-[1px] bg-muted-foreground/10" />
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<input type="text" inputmode="numeric" :value="notifyLogLimit" @input="(e: any) => notifyLogLimit = Number(e.target.value.replace(/\D/g, ''))"
|
|
||||||
class="w-14 h-4 text-center text-[11px] font-mono bg-transparent border-none outline-none focus:ring-0 p-0" />
|
|
||||||
<span class="text-[10px] text-muted-foreground opacity-40">字</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between px-6 py-4 bg-muted/20 border-t shrink-0 backdrop-blur-sm">
|
<div class="flex items-center justify-between px-6 py-4 bg-muted/20 border-t shrink-0 backdrop-blur-sm">
|
||||||
<p class="text-[10px] text-muted-foreground/50 italic">最后编辑于: {{ isEdit ? (form.updated_at || '刚才') : '现在' }}</p>
|
<p class="text-[10px] text-muted-foreground/50 italic">最后编辑于: {{ isEdit ? (form.updated_at || '刚才') : '现在' }}</p>
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Bell } from 'lucide-vue-next'
|
||||||
|
import { api, type NotifyChannel, type NotifyBinding } from '@/api'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
taskId?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const notifyChannels = ref<NotifyChannel[]>([])
|
||||||
|
const notifyWayId = ref<string>('none')
|
||||||
|
const notifyOnSuccess = ref(false)
|
||||||
|
const notifyOnFailure = ref(false)
|
||||||
|
const notifyOnTimeout = ref(false)
|
||||||
|
const notifyIncludeLog = ref(false)
|
||||||
|
const notifyLogLimit = ref(1000)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
notifyChannels.value = await api.notify.getChannels()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fetch channels failed', e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function resetConfig() {
|
||||||
|
notifyWayId.value = 'none'
|
||||||
|
notifyOnSuccess.value = false
|
||||||
|
notifyOnFailure.value = false
|
||||||
|
notifyOnTimeout.value = false
|
||||||
|
notifyIncludeLog.value = false
|
||||||
|
notifyLogLimit.value = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadConfig(taskId?: string) {
|
||||||
|
if (!taskId) {
|
||||||
|
resetConfig()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allBindings = await api.notify.getBindings()
|
||||||
|
// 过滤出该任务的所有绑定
|
||||||
|
const taskBindings = allBindings.filter(b => b.data_id === taskId && b.type === 'task')
|
||||||
|
|
||||||
|
if (taskBindings.length > 0 && taskBindings[0]) {
|
||||||
|
notifyWayId.value = taskBindings[0].way_id || 'none'
|
||||||
|
|
||||||
|
// 直接设置,无需 setTimeout,内部已经安全了
|
||||||
|
notifyOnSuccess.value = taskBindings.some(b => b.event === 'task_success')
|
||||||
|
notifyOnFailure.value = taskBindings.some(b => b.event === 'task_failed')
|
||||||
|
notifyOnTimeout.value = taskBindings.some(b => b.event === 'task_timeout')
|
||||||
|
|
||||||
|
const extraBinding = taskBindings.find(b => b.extra && b.extra !== '')
|
||||||
|
if (extraBinding && extraBinding.extra) {
|
||||||
|
try {
|
||||||
|
const extra = JSON.parse(extraBinding.extra)
|
||||||
|
notifyIncludeLog.value = !!extra.enable_log
|
||||||
|
notifyLogLimit.value = extra.log_limit || 1000
|
||||||
|
} catch {
|
||||||
|
notifyIncludeLog.value = false
|
||||||
|
notifyLogLimit.value = 1000
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notifyIncludeLog.value = false
|
||||||
|
notifyLogLimit.value = 1000
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resetConfig()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Load notifications failed', e)
|
||||||
|
resetConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig(taskId: string) {
|
||||||
|
try {
|
||||||
|
const bindings: Partial<NotifyBinding>[] = []
|
||||||
|
|
||||||
|
if (notifyWayId.value !== 'none') {
|
||||||
|
const events = [
|
||||||
|
{ type: 'task_success', enabled: notifyOnSuccess.value },
|
||||||
|
{ type: 'task_failed', enabled: notifyOnFailure.value },
|
||||||
|
{ type: 'task_timeout', enabled: notifyOnTimeout.value }
|
||||||
|
]
|
||||||
|
|
||||||
|
const extra = JSON.stringify({
|
||||||
|
enable_log: notifyIncludeLog.value,
|
||||||
|
log_limit: notifyLogLimit.value
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
if (event.enabled) {
|
||||||
|
bindings.push({
|
||||||
|
event: event.type,
|
||||||
|
way_id: notifyWayId.value,
|
||||||
|
extra: extra
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用批量保存,后端会自动清理该任务旧的绑定
|
||||||
|
await api.notify.saveBindingsBatch({
|
||||||
|
type: 'task',
|
||||||
|
data_id: taskId,
|
||||||
|
bindings: bindings
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Save notifications failed', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
loadConfig,
|
||||||
|
saveConfig
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="space-y-4">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<div class="h-4 w-1 bg-primary rounded-full" />
|
||||||
|
<h3 class="text-sm font-semibold text-foreground/80">通知配置</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 pl-3 border-l border-muted">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-3">
|
||||||
|
<Label class="sm:text-right text-xs text-muted-foreground uppercase tracking-wider">通知渠道</Label>
|
||||||
|
<div class="sm:col-span-3">
|
||||||
|
<Select v-model="notifyWayId">
|
||||||
|
<SelectTrigger class="h-9 bg-muted/30 border-muted-foreground/20">
|
||||||
|
<SelectValue placeholder="不启用通知" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">不启用通知</SelectItem>
|
||||||
|
<SelectItem v-for="ch in notifyChannels" :key="ch.id" :value="ch.id">
|
||||||
|
{{ ch.name }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="notifyWayId !== 'none'">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-4 items-start gap-3">
|
||||||
|
<Label class="sm:text-right text-xs text-muted-foreground uppercase tracking-wider pt-1.5">通知时机</Label>
|
||||||
|
<div class="sm:col-span-3 space-y-3">
|
||||||
|
<div class="flex flex-wrap gap-4 p-3 rounded-lg bg-muted/20 border border-muted-foreground/10 items-center">
|
||||||
|
<div class="flex items-center gap-2 group">
|
||||||
|
<Checkbox :id="`ns-${taskId || 'new'}`" v-model="notifyOnSuccess" />
|
||||||
|
<label :for="`ns-${taskId || 'new'}`" class="text-xs shrink-0 cursor-pointer group-hover:text-primary transition-colors">成功时</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 group">
|
||||||
|
<Checkbox :id="`nf-${taskId || 'new'}`" v-model="notifyOnFailure" />
|
||||||
|
<label :for="`nf-${taskId || 'new'}`" class="text-xs shrink-0 cursor-pointer group-hover:text-primary transition-colors">失败时</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 group">
|
||||||
|
<Checkbox :id="`nt-${taskId || 'new'}`" v-model="notifyOnTimeout" />
|
||||||
|
<label :for="`nt-${taskId || 'new'}`" class="text-xs shrink-0 cursor-pointer group-hover:text-primary transition-colors">超时时</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 rounded-xl bg-primary/5 border border-primary/10 space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2 text-xs font-semibold">
|
||||||
|
<Bell :class="cn('h-3.5 w-3.5', notifyIncludeLog ? 'text-primary' : 'text-muted-foreground')" />
|
||||||
|
附带执行日志
|
||||||
|
</div>
|
||||||
|
<Switch v-model="notifyIncludeLog" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="notifyIncludeLog" class="flex items-center gap-2 animate-in fade-in slide-in-from-top-1 duration-200 pl-5">
|
||||||
|
<div class="flex items-center gap-2 px-2.5 py-1 rounded-full bg-background/60 border border-muted-foreground/10 focus-within:border-primary/30 transition-all shadow-sm">
|
||||||
|
<span class="text-[10px] text-muted-foreground opacity-60 whitespace-nowrap">长度限制</span>
|
||||||
|
<div class="h-3 w-[1px] bg-muted-foreground/10" />
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<input type="text" inputmode="numeric" :value="notifyLogLimit" @input="(e: any) => notifyLogLimit = Number(e.target.value.replace(/\D/g, ''))"
|
||||||
|
class="w-14 h-4 text-center text-[11px] font-mono bg-transparent border-none outline-none focus:ring-0 p-0" />
|
||||||
|
<span class="text-[10px] text-muted-foreground opacity-40">字</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user