feat: 脚本执行平台增强功能
Some checks failed
continuous-integration/drone/push Build is failing

- 新增重试和失败告警功能(支持自动重试N次,失败后钉钉/企微通知)
- 新增密钥管理(安全存储API Key等敏感信息)
- 新增脚本模板库(预置常用脚本模板)
- 新增脚本版本管理(自动保存历史版本,支持回滚)
- 新增执行统计(成功率、平均耗时、7日趋势)
- SDK 新增多租户遍历能力(get_tenants/get_tenant_config/get_all_tenant_configs)
- SDK 新增密钥读取方法(get_secret)
This commit is contained in:
2026-01-28 11:59:50 +08:00
parent 644255891e
commit 9b72e6127f
4 changed files with 1142 additions and 28 deletions

View File

@@ -36,9 +36,30 @@ const form = reactive({
webhook_url: '',
input_params: '',
script_content: '',
is_enabled: true
is_enabled: true,
// 重试和告警
retry_count: 0,
retry_interval: 60,
alert_on_failure: false,
alert_webhook: ''
})
// 模板
const templateList = ref([])
const templateDialogVisible = ref(false)
// 版本
const versionsDialogVisible = ref(false)
const versionsLoading = ref(false)
const versionsList = ref([])
const currentVersionTaskId = ref(null)
// 统计
const statsDialogVisible = ref(false)
const statsLoading = ref(false)
const statsData = ref(null)
const currentStatsTaskId = ref(null)
// 时间选择器
const newTimePoint = ref('')
@@ -149,7 +170,11 @@ function handleCreate() {
webhook_url: '',
input_params: '',
script_content: '',
is_enabled: true
is_enabled: true,
retry_count: 0,
retry_interval: 60,
alert_on_failure: false,
alert_webhook: ''
})
newTimePoint.value = ''
testResult.value = null
@@ -170,7 +195,11 @@ function handleEdit(row) {
webhook_url: row.webhook_url || '',
input_params: row.input_params ? JSON.stringify(row.input_params, null, 2) : '',
script_content: row.script_content || '',
is_enabled: row.is_enabled
is_enabled: row.is_enabled,
retry_count: row.retry_count || 0,
retry_interval: row.retry_interval || 60,
alert_on_failure: !!row.alert_on_failure,
alert_webhook: row.alert_webhook || ''
})
newTimePoint.value = ''
testResult.value = null
@@ -237,7 +266,11 @@ async function handleSubmit() {
webhook_url: form.execution_type === 'webhook' ? form.webhook_url : null,
script_content: form.execution_type === 'script' ? form.script_content : null,
input_params: inputParams,
is_enabled: form.is_enabled
is_enabled: form.is_enabled,
retry_count: form.retry_count,
retry_interval: form.retry_interval,
alert_on_failure: form.alert_on_failure,
alert_webhook: form.alert_webhook || null
}
try {
@@ -379,6 +412,86 @@ function selectTenant(code) {
handleSearch()
}
// ============ 模板功能 ============
async function fetchTemplates() {
try {
const res = await api.get('/api/scheduled-tasks/templates')
templateList.value = res.data.items || []
} catch (e) {
console.error('获取模板列表失败:', e)
}
}
function handleSelectTemplate() {
fetchTemplates()
templateDialogVisible.value = true
}
function applyTemplate(template) {
form.script_content = template.script_content
templateDialogVisible.value = false
ElMessage.success(`已应用模板:${template.name}`)
}
// ============ 版本功能 ============
async function handleViewVersions(row) {
currentVersionTaskId.value = row.id
versionsLoading.value = true
versionsDialogVisible.value = true
try {
const res = await api.get(`/api/scheduled-tasks/${row.id}/versions`)
versionsList.value = res.data.items || []
} catch (e) {
console.error(e)
} finally {
versionsLoading.value = false
}
}
async function handleRollback(version) {
await ElMessageBox.confirm(`确定回滚到版本 ${version} 吗?`, '版本回滚', {
type: 'warning'
})
try {
const res = await api.post(`/api/scheduled-tasks/${currentVersionTaskId.value}/versions/${version}/rollback`)
ElMessage.success(res.data.message)
versionsDialogVisible.value = false
fetchList()
} catch (e) {
// 错误已在拦截器处理
}
}
async function handleViewVersionContent(version) {
try {
const res = await api.get(`/api/scheduled-tasks/${currentVersionTaskId.value}/versions/${version}`)
ElMessageBox.alert(res.data.script_content, `版本 ${version} 脚本内容`, {
customClass: 'version-content-dialog',
dangerouslyUseHTMLString: false
})
} catch (e) {
// 错误已在拦截器处理
}
}
// ============ 统计功能 ============
async function handleViewStats(row) {
currentStatsTaskId.value = row.id
statsLoading.value = true
statsDialogVisible.value = true
try {
const res = await api.get(`/api/scheduled-tasks/${row.id}/stats`)
statsData.value = res.data
} catch (e) {
console.error(e)
} finally {
statsLoading.value = false
}
}
onMounted(() => {
fetchTenants()
fetchList()
@@ -485,9 +598,20 @@ onMounted(() => {
{{ row.is_enabled ? '禁用' : '启用' }}
</el-button>
<el-button type="info" link size="small" @click="handleViewLogs(row)">
<el-icon><Document /></el-icon>
日志
</el-button>
<el-button type="primary" link size="small" @click="handleViewStats(row)">
统计
</el-button>
<el-button
v-if="row.execution_type === 'script'"
type="warning"
link
size="small"
@click="handleViewVersions(row)"
>
版本
</el-button>
<el-button v-if="authStore.isOperator" type="danger" link size="small" @click="handleDelete(row)">
删除
</el-button>
@@ -613,9 +737,14 @@ onMounted(() => {
<template v-if="form.execution_type === 'script'">
<el-form-item label="脚本内容">
<div class="script-editor-header">
<el-button type="primary" link size="small" @click="handleShowSdkDocs">
查看 SDK 文档
</el-button>
<div>
<el-button type="primary" link size="small" @click="handleShowSdkDocs">
查看 SDK 文档
</el-button>
<el-button type="warning" link size="small" @click="handleSelectTemplate">
选择模板
</el-button>
</div>
<el-button
type="success"
size="small"
@@ -665,6 +794,35 @@ log('执行完成')"
</el-form-item>
</template>
<el-divider content-position="left">高级配置</el-divider>
<el-form-item label="失败重试">
<div class="retry-config">
<span>失败后重试</span>
<el-input-number v-model="form.retry_count" :min="0" :max="10" size="small" style="width: 100px" />
<span>间隔</span>
<el-input-number v-model="form.retry_interval" :min="10" :max="3600" :step="10" size="small" style="width: 100px" />
<span></span>
</div>
</el-form-item>
<el-form-item label="失败告警">
<el-switch v-model="form.alert_on_failure" />
<span v-if="form.alert_on_failure" style="margin-left: 16px; color: #909399; font-size: 12px">
执行失败时发送通知
</span>
</el-form-item>
<el-form-item v-if="form.alert_on_failure" label="告警地址">
<el-input
v-model="form.alert_webhook"
placeholder="钉钉或企微机器人 Webhook URL"
/>
<div class="form-tip">
填写钉钉或企微机器人 Webhook 地址任务执行失败时会自动发送告警通知
</div>
</el-form-item>
<el-form-item label="启用状态">
<el-switch v-model="form.is_enabled" />
</el-form-item>
@@ -748,6 +906,109 @@ log('执行完成')"
<el-button @click="logsDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 模板选择对话框 -->
<el-dialog v-model="templateDialogVisible" title="选择脚本模板" width="600px">
<div v-if="templateList.length === 0" class="empty-tip">
暂无可用模板
</div>
<div v-else class="template-list">
<div
v-for="template in templateList"
:key="template.id"
class="template-item"
@click="applyTemplate(template)"
>
<div class="template-name">{{ template.name }}</div>
<div class="template-desc">{{ template.description || '暂无描述' }}</div>
<el-tag v-if="template.category" size="small" type="info">{{ template.category }}</el-tag>
</div>
</div>
<template #footer>
<el-button @click="templateDialogVisible = false">取消</el-button>
</template>
</el-dialog>
<!-- 版本管理对话框 -->
<el-dialog v-model="versionsDialogVisible" title="脚本版本历史" width="700px">
<el-table v-loading="versionsLoading" :data="versionsList" style="width: 100%">
<el-table-column prop="version" label="版本" width="80" />
<el-table-column prop="change_note" label="变更说明">
<template #default="{ row }">
{{ row.change_note || '-' }}
</template>
</el-table-column>
<el-table-column prop="created_by" label="创建人" width="100">
<template #default="{ row }">
{{ row.created_by || '-' }}
</template>
</el-table-column>
<el-table-column label="创建时间" width="170">
<template #default="{ row }">
{{ formatTime(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleViewVersionContent(row.version)">
查看
</el-button>
<el-button type="warning" link size="small" @click="handleRollback(row.version)">
回滚
</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="versionsDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 统计对话框 -->
<el-dialog v-model="statsDialogVisible" title="执行统计" width="600px">
<div v-loading="statsLoading">
<template v-if="statsData">
<div class="stats-grid">
<div class="stats-item">
<div class="stats-value">{{ statsData.total }}</div>
<div class="stats-label">总执行次数</div>
</div>
<div class="stats-item success">
<div class="stats-value">{{ statsData.success }}</div>
<div class="stats-label">成功</div>
</div>
<div class="stats-item danger">
<div class="stats-value">{{ statsData.failed }}</div>
<div class="stats-label">失败</div>
</div>
<div class="stats-item">
<div class="stats-value">{{ statsData.success_rate }}%</div>
<div class="stats-label">成功率</div>
</div>
<div class="stats-item">
<div class="stats-value">{{ statsData.avg_duration }}s</div>
<div class="stats-label">平均耗时</div>
</div>
</div>
<div v-if="statsData.trend && statsData.trend.length" class="stats-trend">
<h4>最近7天趋势</h4>
<div class="trend-chart">
<div v-for="item in statsData.trend" :key="item.date" class="trend-day">
<div class="trend-bars">
<div class="trend-bar success" :style="{ height: (item.success / (item.success + item.failed) * 100) + '%' }"></div>
<div class="trend-bar danger" :style="{ height: (item.failed / (item.success + item.failed) * 100) + '%' }"></div>
</div>
<div class="trend-date">{{ item.date?.slice(5) }}</div>
</div>
</div>
</div>
</template>
</div>
<template #footer>
<el-button @click="statsDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -897,4 +1158,142 @@ log('执行完成')"
line-height: 1.5;
overflow-x: auto;
}
/* 重试配置 */
.retry-config {
display: flex;
align-items: center;
gap: 8px;
color: #606266;
font-size: 14px;
}
/* 模板列表 */
.template-list {
max-height: 400px;
overflow-y: auto;
}
.template-item {
padding: 12px 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
}
.template-item:hover {
border-color: #409eff;
background: #f5f7fa;
}
.template-name {
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.template-desc {
font-size: 12px;
color: #909399;
margin-bottom: 8px;
}
.empty-tip {
text-align: center;
color: #909399;
padding: 40px 0;
}
/* 统计 */
.stats-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stats-item {
text-align: center;
padding: 16px;
background: #f5f7fa;
border-radius: 8px;
}
.stats-item.success {
background: #f0f9eb;
}
.stats-item.danger {
background: #fef0f0;
}
.stats-value {
font-size: 24px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.stats-item.success .stats-value {
color: #67c23a;
}
.stats-item.danger .stats-value {
color: #f56c6c;
}
.stats-label {
font-size: 12px;
color: #909399;
}
.stats-trend h4 {
margin: 0 0 12px;
color: #303133;
}
.trend-chart {
display: flex;
gap: 8px;
align-items: flex-end;
height: 100px;
}
.trend-day {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.trend-bars {
display: flex;
flex-direction: column;
width: 100%;
height: 80px;
background: #f5f7fa;
border-radius: 4px;
overflow: hidden;
}
.trend-bar {
width: 100%;
transition: height 0.3s;
}
.trend-bar.success {
background: #67c23a;
}
.trend-bar.danger {
background: #f56c6c;
}
.trend-date {
font-size: 10px;
color: #909399;
margin-top: 4px;
}
</style>