feat: trigger task record (#4689)

This commit is contained in:
shaohuzhang1
2026-01-23 20:09:16 +08:00
committed by GitHub
parent df5487b661
commit 5947dd04a5
12 changed files with 357 additions and 122 deletions

View File

@@ -1,5 +1,6 @@
# Generated by Django 5.2.8 on 2026-01-22 03:09
# Generated by Django 5.2.9 on 2026-01-23 09:47
import common.encoder.encoder
import django.db.models.deletion
import uuid_utils.compat
from django.db import migrations, models
@@ -40,7 +41,7 @@ class Migration(migrations.Migration):
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('source_type', models.CharField(choices=[('APPLICATION', 'Application'), ('TOOL', 'Tool')], default='APPLICATION', max_length=256, verbose_name='触发器任务类型')),
('source_id', models.UUIDField(blank=True, null=True, verbose_name='资源id')),
('source_id', models.UUIDField(verbose_name='资源id')),
('is_active', models.BooleanField(db_index=True, default=True)),
('parameter', models.JSONField(default=list)),
('meta', models.JSONField(default=dict)),
@@ -48,6 +49,26 @@ class Migration(migrations.Migration):
],
options={
'db_table': 'event_trigger_task',
'unique_together': {('trigger', 'source_id', 'source_type')},
},
),
migrations.CreateModel(
name='TaskRecord',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('source_type', models.CharField(choices=[('APPLICATION', 'Application'), ('TOOL', 'Tool')], default='APPLICATION', max_length=256, verbose_name='触发器任务类型')),
('source_id', models.UUIDField(verbose_name='资源id')),
('task_record_id', models.UUIDField(verbose_name='任务记录id')),
('meta', models.JSONField(default=dict, encoder=common.encoder.encoder.SystemEncoder)),
('state', models.CharField(choices=[('PENDING', 'Pending'), ('STARTED', 'Started'), ('SUCCESS', 'Success'), ('FAILURE', 'Failure'), ('REVOKE', 'Revoke'), ('REVOKED', 'Revoked')], default='STARTED', max_length=20, verbose_name='状态')),
('run_time', models.FloatField(default=0, verbose_name='运行时长')),
('trigger', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='trigger.trigger')),
('trigger_task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='trigger.triggertask')),
],
options={
'db_table': 'event_trigger_task_record',
},
),
]

View File

@@ -1,32 +0,0 @@
# Generated by Django 5.2.9 on 2026-01-22 09:32
import common.encoder.encoder
import uuid_utils.compat
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('trigger', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='TaskRecord',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('source_type', models.CharField(choices=[('APPLICATION', 'Application'), ('TOOL', 'Tool')], default='APPLICATION', max_length=256, verbose_name='触发器任务类型')),
('source_id', models.UUIDField(blank=True, null=True, verbose_name='资源id')),
('task_record_id', models.UUIDField(blank=True, verbose_name='任务记录id')),
('meta', models.JSONField(default=dict, encoder=common.encoder.encoder.SystemEncoder)),
('state', models.CharField(choices=[('PENDING', 'Pending'), ('STARTED', 'Started'), ('SUCCESS', 'Success'), ('FAILURE', 'Failure'), ('REVOKE', 'Revoke'), ('REVOKED', 'Revoked')], default='STARTED', max_length=20, verbose_name='状态')),
('run_time', models.FloatField(default=0, verbose_name='运行时长')),
],
options={
'db_table': 'event_trigger_task_record',
},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.2.9 on 2026-01-22 11:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('trigger', '0002_taskrecord'),
]
operations = [
migrations.AlterField(
model_name='taskrecord',
name='task_record_id',
field=models.UUIDField(blank=True, null=True, verbose_name='任务记录id'),
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 5.2.9 on 2026-01-23 07:49
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('trigger', '0003_alter_taskrecord_task_record_id'),
]
operations = [
migrations.AlterUniqueTogether(
name='triggertask',
unique_together={('trigger', 'source_id', 'source_type')},
),
]

View File

@@ -48,7 +48,7 @@ class TriggerTask(AppModelMixin):
source_type = models.CharField(verbose_name="触发器任务类型", choices=TriggerTaskTypeChoices.choices,
default=TriggerTaskTypeChoices.APPLICATION, max_length=256
)
source_id = models.UUIDField(verbose_name="资源id", blank=True, null=True)
source_id = models.UUIDField(verbose_name="资源id")
is_active = models.BooleanField(default=True, db_index=True)
parameter = models.JSONField(default=list)
meta = models.JSONField(default=dict)
@@ -60,10 +60,15 @@ class TriggerTask(AppModelMixin):
class TaskRecord(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
trigger = models.ForeignKey(Trigger, on_delete=models.CASCADE)
trigger_task = models.ForeignKey(TriggerTask, on_delete=models.CASCADE)
source_type = models.CharField(verbose_name="触发器任务类型", choices=TriggerTaskTypeChoices.choices,
default=TriggerTaskTypeChoices.APPLICATION, max_length=256)
source_id = models.UUIDField(verbose_name="资源id", blank=True, null=True)
task_record_id = models.UUIDField(verbose_name="任务记录id", blank=True, null=True)
source_id = models.UUIDField(verbose_name="资源id")
task_record_id = models.UUIDField(verbose_name="任务记录id")
meta = models.JSONField(default=dict, encoder=SystemEncoder)
state = models.CharField(verbose_name='状态', max_length=20,
choices=State.choices,

View File

@@ -6,11 +6,17 @@
@date2026/1/14 16:34
@desc:
"""
import os
from django.db import models
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from trigger.models import TriggerTask
from common.db.search import native_page_search, get_dynamics_model
from common.utils.common import get_file_content
from maxkb.conf import PROJECT_DIR
from trigger.models import TriggerTask, TaskRecord
class TriggerTaskResponse(serializers.ModelSerializer):
@@ -32,3 +38,38 @@ class TriggerTaskQuerySerializer(serializers.Serializer):
if with_valid:
self.is_valid(raise_exception=True)
return [TriggerTaskResponse(row).data for row in self.get_query_set()]
class TriggerTaskRecordQuerySerializer(serializers.Serializer):
trigger_id = serializers.CharField(required=True, label=_("Trigger ID"))
workspace_id = serializers.CharField(required=True, label=_('workspace id'))
state = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_('Trigger task'))
name = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_('Trigger task'))
def get_query_set(self):
trigger_query_set = QuerySet(
model=get_dynamics_model({
'ett.state': models.CharField(),
'sdc.name': models.CharField(),
'ett.workspace_id': models.CharField(),
'ett.trigger_id': models.UUIDField(),
}))
trigger_query_set = trigger_query_set.filter(
**{'ett.trigger_id': self.data.get("trigger_id")})
if self.data.get('state'):
trigger_query_set = trigger_query_set.filter(**{'ett.state': self.data.get('state')})
if self.data.get("name"):
trigger_query_set = trigger_query_set.filter(**{'sdc.name__contains': self.data.get('name')})
return trigger_query_set
def list(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
return [TriggerTaskResponse(row).data for row in self.get_query_set()]
def page(self, current_page, page_size, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
return native_page_search(current_page, page_size, self.get_query_set(), get_file_content(
os.path.join(PROJECT_DIR, "apps", "trigger", "sql", 'get_trigger_task_record_page_list.sql')
))

View File

@@ -0,0 +1,27 @@
WITH source_data_cte AS (SELECT 'APPLICATION' as source_type,
id,
"name",
"desc",
"user_id",
"workspace_id",
"icon",
"type",
"folder_id"
FROM application
UNION ALL
SELECT 'TOOL' as source_type,
id,
"name",
"desc",
"user_id",
"workspace_id",
"icon",
"tool_type"::text as "type",
"folder_id"
FROM tool)
select ett.*,
sdc.name as source_name,
sdc.icon as source_icon
from event_trigger_task_record ett
left join source_data_cte sdc
on ett.source_id = sdc.id and ett.source_type = sdc.source_type

View File

@@ -19,12 +19,17 @@ urlpatterns = [
name='delete batch'),
path('workspace/<str:workspace_id>/trigger/batch_activate', views.TriggerView.BatchActivate.as_view(),
name='activate batch'),
path('workspace/<str:workspace_id>/trigger/<str:trigger_id>', views.TriggerView.Operate.as_view(), name='trigger operate'),
path('workspace/<str:workspace_id>/trigger/<str:trigger_id>', views.TriggerView.Operate.as_view(),
name='trigger operate'),
path('workspace/<str:workspace_id>/trigger/<int:current_page>/<int:page_size>', views.TriggerView.Page.as_view(),
name='trigger_page'),
path('workspace/<str:workspace_id>/<str:source_type>/<str:source_id>/trigger/<str:trigger_id>',
views.TaskSourceTriggerView.Operate.as_view(), name='task source trigger operate'),
path('workspace/<str:workspace_id>/<str:source_type>/<str:source_id>/trigger', views.TaskSourceTriggerView.as_view(), name='task source trigger'),
path('workspace/<str:workspace_id>/<str:source_type>/<str:source_id>/trigger',
views.TaskSourceTriggerView.as_view(), name='task source trigger'),
path(
'workspace/<str:workspace_id>/trigger/<str:trigger_id>/task_record/<int:current_page>/<int:page_size>',
views.TriggerTaskRecordPageView.as_view(), name='trigger_task_record'),
path('workspace/<str:workspace_id>/task', views.TriggerTaskView.as_view(), name='task'),
path('trigger/v1/webhook/<str:trigger_id>', EventTriggerView.as_view(), name='trigger_webhook')
]

View File

@@ -13,7 +13,7 @@ from rest_framework.views import APIView
from application.api.application_api import ApplicationCreateAPI
from common import result
from trigger.serializers.trigger_task import TriggerTaskQuerySerializer
from trigger.serializers.trigger_task import TriggerTaskQuerySerializer, TriggerTaskRecordQuerySerializer
class TriggerTaskView(APIView):
@@ -30,3 +30,27 @@ class TriggerTaskView(APIView):
def get(self, request: Request, workspace_id: str, trigger_id: str):
return result.success(
TriggerTaskQuerySerializer(data={'workspace_id': workspace_id, 'trigger_id': trigger_id}).list())
class TriggerTaskRecordView(APIView):
pass
class TriggerTaskRecordPageView(APIView):
@extend_schema(
methods=['GET'],
description=_('Get a paginated list of execution records for trigger tasks.'),
summary=_('Get a paginated list of execution records for trigger tasks.'),
operation_id=_('Get a paginated list of execution records for trigger tasks.'), # type: ignore
parameters=ApplicationCreateAPI.get_parameters(),
request=ApplicationCreateAPI.get_request(),
responses=ApplicationCreateAPI.get_response(),
tags=[_('Trigger')] # type: ignore
)
def get(self, request: Request, workspace_id: str, trigger_id: str, current_page: int, page_size: int):
return result.success(
TriggerTaskRecordQuerySerializer(
data={'workspace_id': workspace_id, 'trigger_id': trigger_id,
'state': request.query_params.get('state'),
'name': request.query_params.get('name')})
.page(current_page, page_size))

View File

@@ -15,95 +15,102 @@ Object.defineProperty(prefix, 'value', {
/**
* 触发器列表
* @param data
* @param loading
* @returns
* @param data
* @param loading
* @returns
*/
const getTriggerList: (
data?: any,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (data,loading) => {
const getTriggerList: (data?: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
data,
loading,
) => {
return get(`${prefix.value}`, data, loading)
}
/**
* 触发器详情
* @param trigger_id
* @param loading
* @returns
* @param trigger_id
* @param loading
* @returns
*/
const getTriggerDetail: (
trigger_id: string,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (trigger_id,loading) => {
const getTriggerDetail: (trigger_id: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
trigger_id,
loading,
) => {
return get(`${prefix.value}/${trigger_id}`, {}, loading)
}
/**
* 创建触发器
* @param data
* @param loading
* @returns
* @param data
* @param loading
* @returns
*/
const postTrigger: (data: TriggerData, loading?: Ref<boolean>) => Promise<Result<any>> = (
data, loading
data,
loading,
) => {
return post(`${prefix.value}`, data, undefined,loading)
return post(`${prefix.value}`, data, undefined, loading)
}
/**
* 修改触发器
* @param trigger_id
* @param data
* @param loading
* @returns
* @param trigger_id
* @param data
* @param loading
* @returns
*/
const putTrigger: (trigger_id: string, data: TriggerData, loading?: Ref<boolean>) => Promise<Result<any>> = (
trigger_id,data,loading
) => {
return put(`${prefix.value}/${trigger_id}`, data, undefined,loading)
const putTrigger: (
trigger_id: string,
data: TriggerData,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (trigger_id, data, loading) => {
return put(`${prefix.value}/${trigger_id}`, data, undefined, loading)
}
/**
* 删除触发器
* @param trigger_id
* @param loading
* @returns
* @param trigger_id
* @param loading
* @returns
*/
const deleteTrigger: (trigger_id: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
trigger_id,loading
trigger_id,
loading,
) => {
return del(`${prefix.value}/${trigger_id}`, undefined, {},loading)
return del(`${prefix.value}/${trigger_id}`, undefined, {}, loading)
}
/**
* 批量删除触发器
* @param data
* @param loading
* @returns
* @param loading
* @returns
*/
const delMulTrigger: (data: any, loading?: Ref<boolean>) => Promise<Result<boolean>> = (
data: any,
loading
loading,
) => {
return put(`${prefix.value}/batch_delete`,{id_list: data}, undefined,loading)
return put(`${prefix.value}/batch_delete`, { id_list: data }, undefined, loading)
}
/**
* 批量激活/禁用触发器
* @param data
* @param loading
* @returns
* @param data
* @param loading
* @returns
*/
const activateMulTrigger: (data: any, loading?: Ref<boolean>) => Promise<Result<boolean>> = (
data: any,
loading
loading,
) => {
return put(`${prefix.value}/batch_activate`,{id_list: data.id_list, is_active: data.is_active}, undefined,loading)
return put(
`${prefix.value}/batch_activate`,
{ id_list: data.id_list, is_active: data.is_active },
undefined,
loading,
)
}
/**
* 分页查询触发器
* @param page 分页参数
@@ -114,6 +121,26 @@ const activateMulTrigger: (data: any, loading?: Ref<boolean>) => Promise<Result<
const pageTrigger = (page: pageRequest, param: any, loading?: Ref<boolean>) => {
return get(`${prefix.value}/${page.current_page}/${page.page_size}`, param, loading)
}
/**
* 分页查询触发器执行任务
* @param trigger_id 触发器id
* @param page 分页参数
* @param param 查询参数
* @param loading 记载器
* @returns
*/
const pageTriggerTaskRecord = (
trigger_id: string,
page: pageRequest,
param: any,
loading?: Ref<boolean>,
) => {
return get(
`${prefix.value}/${trigger_id}/task_record/${page.current_page}/${page.page_size}`,
param,
loading,
)
}
export default {
pageTrigger,
getTriggerList,
@@ -122,5 +149,6 @@ export default {
putTrigger,
deleteTrigger,
delMulTrigger,
activateMulTrigger
activateMulTrigger,
pageTriggerTaskRecord,
}

View File

@@ -0,0 +1,145 @@
<template>
<el-drawer v-model="drawer" title="执行记录" direction="rtl" :before-close="close">
<div class="lighter mb-12">
{{ $t('views.system.resourceMapping.sub_title') }}
</div>
<div class="flex-between mb-16">
<div class="flex-between complex-search">
<el-select class="complex-search__left" v-model="searchType" style="width: 100px">
<el-option :label="$t('common.name')" value="name" />
<el-option :label="$t('common.status.label')" value="state" />
</el-select>
<el-input
v-if="searchType === 'name'"
v-model="query.resource_name"
:placeholder="$t('common.search')"
style="width: 220px"
clearable
@keyup.enter="page()"
/>
<el-select
v-else-if="searchType === 'state'"
v-model="query.source_type"
@change="page()"
filterable
clearable
:reserve-keyword="false"
collapse-tags
collapse-tags-tooltip
style="width: 220px"
:placeholder="$t('common.search')"
>
<el-option :label="$t('common.status.success')" value="SUCCESS" />
<el-option :label="$t('common.status.STARTED')" value="STARTED" />
<el-option :label="$t('common.status.fail')" value="FAILURE" />
</el-select>
</div>
</div>
<app-table
ref="multipleTableRef"
class="mt-16"
:data="tableData"
:pagination-config="paginationConfig"
@sizeChange="handleSizeChange"
@changePage="page"
:maxTableHeight="200"
:row-key="(row: any) => row.id"
v-loading="loading"
:tooltip-options="{
popperClass: 'max-w-350',
}"
>
<el-table-column prop="name" :label="$t('common.name')" min-width="130" show-overflow-tooltip>
<template #default="{ row }">
<el-button link>
<div class="flex align-center">
<KnowledgeIcon class="mr-8" :size="22" :type="row.icon" />
<el-avatar shape="square" :size="22" style="background: none" class="mr-8">
<img
v-if="row.source_type === 'TOOL'"
:src="resetUrl(row?.icon, resetUrl('./favicon.ico'))"
alt=""
/>
<img
v-if="row.source_type === 'APPLICATION'"
:src="resetUrl(row?.icon, resetUrl('./favicon.ico'))"
alt=""
/>
</el-avatar>
<span>{{ row.name }}</span>
</div>
</el-button>
</template>
</el-table-column>
<el-table-column
prop="desc"
min-width="120"
show-overflow-tooltip
:label="$t('common.desc')"
/>
<el-table-column
prop="source_type"
min-width="120"
show-overflow-tooltip
:label="$t('common.type')"
>
<template #default="{ row }">
{{
row.source_type === 'APPLICATION'
? $t('views.application.title')
: $t('views.knowledge.title')
}}
</template>
</el-table-column>
<el-table-column
prop="username"
min-width="120"
show-overflow-tooltip
:label="$t('common.creator')"
/>
</app-table>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { isAppIcon, resetUrl } from '@/utils/common'
import triggerAPI from '@/api/trigger/trigger'
const searchType = ref<string>('name')
const drawer = ref<boolean>(false)
const paginationConfig = reactive({
current_page: 1,
page_size: 20,
total: 0,
})
const tableData = ref<Array<any>>([])
const page = () => {
if (current_trigger_id.value) {
triggerAPI
.pageTriggerTaskRecord(current_trigger_id.value, paginationConfig, { ...query.value })
.then((ok) => {
tableData.value = ok.data.records
paginationConfig.total = ok.data.total
})
}
}
const query = ref<any>({
state: '',
name: '',
})
const loading = ref<boolean>(false)
const current_trigger_id = ref<string>()
const open = (trigger_id: string) => {
current_trigger_id.value = trigger_id
drawer.value = true
page()
}
const handleSizeChange = () => {}
const close = () => {
drawer.value = false
}
defineExpose({ open, close })
</script>
<style lang="scss" scoped></style>

View File

@@ -259,6 +259,7 @@
</el-button>
</span>
</el-tooltip>
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
<span class="mr-4">
<el-button type="primary" text @click="deleteTrigger(row)">
@@ -273,10 +274,11 @@
</div>
</el-card>
<TriggerDrawer @refresh="getList()" ref="triggerDrawerRef"></TriggerDrawer>
<TriggerTaskRecordDrawer ref="triggerTaskRecordDrawerRef"></TriggerTaskRecordDrawer>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import type { ElTable } from 'element-plus'
import { MsgSuccess, MsgConfirm, MsgError } from '@/utils/message'
@@ -284,6 +286,7 @@ import useStore from '@/stores'
import triggerAPI from '@/api/trigger/trigger'
import { TriggerType } from '@/enums/trigger'
import { t } from '@/locales'
import TriggerTaskRecordDrawer from './component/TriggerTaskRecordDrawer.vue'
import permissionMap from '@/permission'
import { datetimeFormat } from '@/utils/time'
import WorkspaceApi from '@/api/workspace/workspace'
@@ -293,6 +296,9 @@ import type { TriggerData } from '@/api/type/trigger'
import TriggerDrawer from '@/views/trigger/component/TriggerDrawer.vue'
const { user } = useStore()
const triggerTaskRecordDrawerRef = ref<InstanceType<typeof TriggerTaskRecordDrawer>>()
const triggerDrawerRef = ref<InstanceType<typeof TriggerDrawer>>()
const openCreateTriggerDrawer = () => {
triggerDrawerRef.value?.open()
@@ -302,7 +308,7 @@ const openEditTriggerDrawer = (trigger: any) => {
}
const openExecutionRecordDrawer = (trigger: any) => {
triggerTaskRecordDrawerRef.value?.open(trigger.id)
}
const loading = ref(false)