- 支持 Python 脚本定时执行(类似青龙面板) - 内置 SDK:AI大模型、钉钉/企微通知、数据库查询、HTTP请求、变量存储 - 安全沙箱执行,禁用危险模块 - 前端脚本编辑器,支持测试执行 - SDK 文档查看 - 日志通过 TraceID 与 platform_logs 关联
This commit is contained in:
@@ -16,7 +16,9 @@
|
||||
"element-plus": "^2.5.0",
|
||||
"@element-plus/icons-vue": "^2.3.0",
|
||||
"echarts": "^5.4.0",
|
||||
"dayjs": "^1.11.0"
|
||||
"dayjs": "^1.11.0",
|
||||
"codemirror": "^5.65.0",
|
||||
"vue-codemirror": "^6.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
|
||||
@@ -32,18 +32,27 @@ const form = reactive({
|
||||
schedule_type: 'simple',
|
||||
time_points: [],
|
||||
cron_expression: '',
|
||||
execution_type: 'webhook', // webhook | script
|
||||
webhook_url: '',
|
||||
input_params: '',
|
||||
script_content: '',
|
||||
is_enabled: true
|
||||
})
|
||||
|
||||
// 时间选择器
|
||||
const newTimePoint = ref('')
|
||||
|
||||
// 脚本测试
|
||||
const testLoading = ref(false)
|
||||
const testResult = ref(null)
|
||||
|
||||
// SDK 文档
|
||||
const sdkDocsVisible = ref(false)
|
||||
const sdkDocs = ref(null)
|
||||
|
||||
const rules = {
|
||||
tenant_id: [{ required: true, message: '请选择租户', trigger: 'change' }],
|
||||
task_name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
|
||||
webhook_url: [{ required: true, message: '请输入 Webhook URL', trigger: 'blur' }]
|
||||
task_name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 日志对话框
|
||||
@@ -136,11 +145,14 @@ function handleCreate() {
|
||||
schedule_type: 'simple',
|
||||
time_points: [],
|
||||
cron_expression: '',
|
||||
execution_type: 'webhook',
|
||||
webhook_url: '',
|
||||
input_params: '',
|
||||
script_content: '',
|
||||
is_enabled: true
|
||||
})
|
||||
newTimePoint.value = ''
|
||||
testResult.value = null
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -154,11 +166,14 @@ function handleEdit(row) {
|
||||
schedule_type: row.schedule_type || 'simple',
|
||||
time_points: row.time_points || [],
|
||||
cron_expression: row.cron_expression || '',
|
||||
webhook_url: row.webhook_url,
|
||||
execution_type: row.execution_type || 'webhook',
|
||||
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
|
||||
})
|
||||
newTimePoint.value = ''
|
||||
testResult.value = null
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -190,6 +205,16 @@ async function handleSubmit() {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证执行配置
|
||||
if (form.execution_type === 'webhook' && !form.webhook_url) {
|
||||
ElMessage.error('请输入 Webhook URL')
|
||||
return
|
||||
}
|
||||
if (form.execution_type === 'script' && !form.script_content) {
|
||||
ElMessage.error('请输入脚本内容')
|
||||
return
|
||||
}
|
||||
|
||||
// 解析输入参数
|
||||
let inputParams = null
|
||||
if (form.input_params) {
|
||||
@@ -208,7 +233,9 @@ async function handleSubmit() {
|
||||
schedule_type: form.schedule_type,
|
||||
time_points: form.schedule_type === 'simple' ? form.time_points : null,
|
||||
cron_expression: form.schedule_type === 'cron' ? form.cron_expression : null,
|
||||
webhook_url: form.webhook_url,
|
||||
execution_type: form.execution_type,
|
||||
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
|
||||
}
|
||||
@@ -294,6 +321,54 @@ function handleLogsPageChange(page) {
|
||||
fetchLogs()
|
||||
}
|
||||
|
||||
// 测试脚本执行
|
||||
async function handleTestScript() {
|
||||
if (!form.script_content) {
|
||||
ElMessage.warning('请先输入脚本内容')
|
||||
return
|
||||
}
|
||||
if (!form.tenant_id) {
|
||||
ElMessage.warning('请先选择租户')
|
||||
return
|
||||
}
|
||||
|
||||
testLoading.value = true
|
||||
testResult.value = null
|
||||
|
||||
try {
|
||||
const res = await api.post('/api/scheduled-tasks/test-script', {
|
||||
tenant_id: form.tenant_id,
|
||||
script_content: form.script_content
|
||||
})
|
||||
testResult.value = res.data
|
||||
if (res.data.success) {
|
||||
ElMessage.success('脚本执行成功')
|
||||
} else {
|
||||
ElMessage.error('脚本执行失败')
|
||||
}
|
||||
} catch (e) {
|
||||
testResult.value = { success: false, error: e.message || '请求失败' }
|
||||
} finally {
|
||||
testLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看 SDK 文档
|
||||
async function handleShowSdkDocs() {
|
||||
try {
|
||||
const res = await api.get('/api/scheduled-tasks/sdk-docs')
|
||||
sdkDocs.value = res.data
|
||||
sdkDocsVisible.value = true
|
||||
} catch (e) {
|
||||
ElMessage.error('获取文档失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取执行类型描述
|
||||
function getExecutionTypeDesc(row) {
|
||||
return row.execution_type === 'script' ? '脚本' : 'Webhook'
|
||||
}
|
||||
|
||||
// 快速选择租户
|
||||
function selectTenant(code) {
|
||||
if (query.tenant_id === code) {
|
||||
@@ -369,6 +444,13 @@ onMounted(() => {
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="执行方式" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.execution_type === 'script' ? 'success' : 'primary'" size="small">
|
||||
{{ row.execution_type === 'script' ? '脚本' : 'Webhook' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_enabled ? 'success' : 'info'" size="small">
|
||||
@@ -502,18 +584,86 @@ onMounted(() => {
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Webhook URL" prop="webhook_url">
|
||||
<el-input v-model="form.webhook_url" placeholder="如:https://n8n.ireborn.com.cn/webhook/xxx" />
|
||||
<el-divider content-position="left">执行配置</el-divider>
|
||||
|
||||
<el-form-item label="执行方式">
|
||||
<el-radio-group v-model="form.execution_type">
|
||||
<el-radio value="webhook">Webhook(调用 n8n 等)</el-radio>
|
||||
<el-radio value="script">Python 脚本</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="输入参数">
|
||||
<el-input
|
||||
v-model="form.input_params"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder='可选,JSON 格式,如:{"key": "value"}'
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- Webhook 模式 -->
|
||||
<template v-if="form.execution_type === 'webhook'">
|
||||
<el-form-item label="Webhook URL">
|
||||
<el-input v-model="form.webhook_url" placeholder="如:https://n8n.ireborn.com.cn/webhook/xxx" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="输入参数">
|
||||
<el-input
|
||||
v-model="form.input_params"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder='可选,JSON 格式,如:{"key": "value"}'
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 脚本模式 -->
|
||||
<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>
|
||||
<el-button
|
||||
type="success"
|
||||
size="small"
|
||||
:loading="testLoading"
|
||||
@click="handleTestScript"
|
||||
>
|
||||
测试执行
|
||||
</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="form.script_content"
|
||||
type="textarea"
|
||||
:rows="15"
|
||||
placeholder="# Python 脚本
|
||||
# 可用方法:ai(), dingtalk(), wecom(), db(), http_get(), http_post()
|
||||
# get_var(), set_var(), log()
|
||||
|
||||
content = ai('生成一段营销文案')
|
||||
dingtalk('你的webhook', content)
|
||||
log('执行完成')"
|
||||
class="script-textarea"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 测试结果 -->
|
||||
<el-form-item v-if="testResult" label="测试结果">
|
||||
<el-alert
|
||||
:type="testResult.success ? 'success' : 'error'"
|
||||
:closable="false"
|
||||
>
|
||||
<template #title>
|
||||
{{ testResult.success ? '执行成功' : '执行失败' }}
|
||||
<span v-if="testResult.execution_time_ms" style="margin-left: 8px; color: #909399">
|
||||
({{ testResult.execution_time_ms }}ms)
|
||||
</span>
|
||||
</template>
|
||||
<div v-if="testResult.error" class="test-error">{{ testResult.error }}</div>
|
||||
<div v-if="testResult.output" class="test-output">
|
||||
<strong>输出:</strong>
|
||||
<pre>{{ testResult.output }}</pre>
|
||||
</div>
|
||||
<div v-if="testResult.logs && testResult.logs.length" class="test-logs">
|
||||
<strong>日志:</strong>
|
||||
<pre>{{ testResult.logs.join('\n') }}</pre>
|
||||
</div>
|
||||
</el-alert>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="form.is_enabled" />
|
||||
@@ -526,6 +676,26 @@ onMounted(() => {
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- SDK 文档对话框 -->
|
||||
<el-dialog v-model="sdkDocsVisible" title="SDK 文档" width="800px">
|
||||
<div v-if="sdkDocs" class="sdk-docs">
|
||||
<p>{{ sdkDocs.description }}</p>
|
||||
|
||||
<h4>可用方法</h4>
|
||||
<div v-for="method in sdkDocs.methods" :key="method.name" class="sdk-method">
|
||||
<code>{{ method.name }}</code>
|
||||
<p>{{ method.description }}</p>
|
||||
<pre class="sdk-example">{{ method.example }}</pre>
|
||||
</div>
|
||||
|
||||
<h4>示例脚本</h4>
|
||||
<pre class="sdk-example-script">{{ sdkDocs.example_script }}</pre>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="sdkDocsVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 日志对话框 -->
|
||||
<el-dialog
|
||||
v-model="logsDialogVisible"
|
||||
@@ -642,4 +812,89 @@ onMounted(() => {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 脚本编辑器 */
|
||||
.script-editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.script-textarea :deep(.el-textarea__inner) {
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 测试结果 */
|
||||
.test-error {
|
||||
color: #f56c6c;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.test-output,
|
||||
.test-logs {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.test-output pre,
|
||||
.test-logs pre {
|
||||
background: #f5f7fa;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* SDK 文档 */
|
||||
.sdk-docs {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sdk-docs h4 {
|
||||
margin: 16px 0 8px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.sdk-method {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sdk-method code {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #409eff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sdk-method p {
|
||||
margin: 0 0 8px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.sdk-example {
|
||||
background: #fff;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.sdk-example-script {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user