实现插件配置持久化与任务队列持久化
插件配置持久化:
- plugin_settings 表新增 config_json 字段,支持存储每个插件的自定义配置
- BaseCrawlerPlugin 新增 default_config 属性和 update_config 方法
- PluginSettingsRepository 新增 get_config / set_config 方法
- PluginService 新增 get_plugin_config 和 update_plugin_config
- api/routes/plugins.py 新增 GET /{id}/config 和 POST /{id}/config 接口
- 前端 Plugins.vue 增加配置编辑对话框,支持动态渲染数字/布尔/字符串类型配置
- ip3366 插件示例化:增加 max_pages 配置项,验证配置生效后会动态更新爬取 URL
任务队列持久化:
- 新建 validation_tasks 表:id, ip, port, protocol, status, result, response_time_ms, created_at, updated_at
- 新建 ValidationTaskRepository,提供 insert_batch / acquire_pending / complete_task / reset_processing 等方法
- ValidationQueue 重构:
- submit() 时把任务写入数据库并唤醒 Worker
- Worker 通过 acquire_pending 原子取任务并验证
- 验证完成后更新任务状态并入库有效代理
- 启动时自动恢复之前中断的 processing 任务为 pending
- 支持 drain() 等待所有 pending 完成
- 调度器验证流程同样自动持久化到任务表
其他适配:
- 更新 api/deps.py 和 api/lifespan.py,移除对已删除 settings_service 的残留引用
- 更新前端 pluginService.js 和 api/index.js 增加配置相关 API
This commit is contained in:
@@ -79,6 +79,8 @@ export const proxiesAPI = {
|
||||
export const pluginsAPI = {
|
||||
getPlugins: () => api.get('/api/plugins'),
|
||||
togglePlugin: (pluginId, enabled) => api.put(`/api/plugins/${pluginId}/toggle`, { enabled }),
|
||||
getPluginConfig: (pluginId) => api.get(`/api/plugins/${pluginId}/config`),
|
||||
updatePluginConfig: (pluginId, config) => api.post(`/api/plugins/${pluginId}/config`, { config }),
|
||||
crawlPlugin: (pluginId) => api.post(`/api/plugins/${pluginId}/crawl`),
|
||||
crawlAll: () => api.post('/api/plugins/crawl-all')
|
||||
}
|
||||
|
||||
@@ -9,6 +9,14 @@ export const pluginService = {
|
||||
return pluginsAPI.togglePlugin(pluginId, enabled)
|
||||
},
|
||||
|
||||
async getPluginConfig(pluginId) {
|
||||
return pluginsAPI.getPluginConfig(pluginId)
|
||||
},
|
||||
|
||||
async updatePluginConfig(pluginId, config) {
|
||||
return pluginsAPI.updatePluginConfig(pluginId, config)
|
||||
},
|
||||
|
||||
async crawlPlugin(pluginId) {
|
||||
return pluginsAPI.crawlPlugin(pluginId)
|
||||
},
|
||||
|
||||
@@ -74,11 +74,19 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleOpenConfig(row)"
|
||||
>
|
||||
<el-icon class="btn-icon"><Setting /></el-icon>
|
||||
配置
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleCrawl(row.id)"
|
||||
:loading="crawlingPlugin === row.id"
|
||||
:disabled="!row.enabled"
|
||||
@@ -123,11 +131,50 @@
|
||||
</template>
|
||||
</el-alert>
|
||||
</el-card>
|
||||
|
||||
<!-- 配置编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="configDialogVisible"
|
||||
title="插件配置"
|
||||
width="400px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div v-if="currentPlugin">
|
||||
<div class="config-plugin-name">{{ currentPlugin.name }}</div>
|
||||
<el-form label-width="120px">
|
||||
<el-form-item
|
||||
v-for="(value, key) in configForm"
|
||||
:key="key"
|
||||
:label="String(key)"
|
||||
>
|
||||
<el-input-number
|
||||
v-if="typeof value === 'number'"
|
||||
v-model="configForm[key]"
|
||||
:min="0"
|
||||
style="width: 180px"
|
||||
/>
|
||||
<el-switch
|
||||
v-else-if="typeof value === 'boolean'"
|
||||
v-model="configForm[key]"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="configForm[key]"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="configDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveConfig" :loading="savingConfig">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Connection,
|
||||
@@ -135,7 +182,8 @@ import {
|
||||
Promotion,
|
||||
CircleCheck,
|
||||
CircleClose,
|
||||
Box
|
||||
Box,
|
||||
Setting
|
||||
} from '@element-plus/icons-vue'
|
||||
import { usePluginsStore } from '../stores/plugins'
|
||||
import { pluginService } from '../services/pluginService'
|
||||
@@ -147,6 +195,12 @@ const crawlingPlugin = ref(null)
|
||||
const crawlingAll = ref(false)
|
||||
const lastCrawlResult = ref(null)
|
||||
|
||||
// 配置对话框
|
||||
const configDialogVisible = ref(false)
|
||||
const currentPlugin = ref(null)
|
||||
const configForm = reactive({})
|
||||
const savingConfig = ref(false)
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
async function handleRefresh() {
|
||||
await pluginsStore.fetchPlugins()
|
||||
@@ -158,11 +212,40 @@ async function handleToggle(pluginId, enabled) {
|
||||
if (success) {
|
||||
ElMessage.success(enabled ? '插件已启用' : '插件已禁用')
|
||||
} else {
|
||||
// 失败时刷新列表恢复状态
|
||||
await pluginsStore.fetchPlugins()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOpenConfig(row) {
|
||||
currentPlugin.value = row
|
||||
const response = await pluginService.getPluginConfig(row.id)
|
||||
if (response.code === 200) {
|
||||
Object.keys(configForm).forEach(key => delete configForm[key])
|
||||
Object.assign(configForm, response.data.config || {})
|
||||
configDialogVisible.value = true
|
||||
} else {
|
||||
ElMessage.error('获取插件配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveConfig() {
|
||||
if (!currentPlugin.value) return
|
||||
savingConfig.value = true
|
||||
try {
|
||||
const response = await pluginService.updatePluginConfig(currentPlugin.value.id, { ...configForm })
|
||||
if (response.code === 200) {
|
||||
ElMessage.success('配置保存成功')
|
||||
configDialogVisible.value = false
|
||||
} else {
|
||||
ElMessage.error(response.message || '保存失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('保存配置出错')
|
||||
} finally {
|
||||
savingConfig.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCrawl(pluginId) {
|
||||
try {
|
||||
crawlingPlugin.value = pluginId
|
||||
@@ -176,7 +259,6 @@ async function handleCrawl(pluginId) {
|
||||
message: response.message,
|
||||
data: response.data
|
||||
}
|
||||
// 刷新插件统计
|
||||
await pluginsStore.fetchPlugins()
|
||||
} else {
|
||||
lastCrawlResult.value = {
|
||||
@@ -196,7 +278,6 @@ async function handleCrawl(pluginId) {
|
||||
|
||||
async function handleCrawlAll() {
|
||||
try {
|
||||
// 确认是否爬取所有插件
|
||||
const enabledPlugins = pluginsStore.plugins.filter(p => p.enabled)
|
||||
if (enabledPlugins.length === 0) {
|
||||
ElMessage.warning('没有启用的插件')
|
||||
@@ -225,7 +306,6 @@ async function handleCrawlAll() {
|
||||
data: response.data
|
||||
}
|
||||
ElMessage.success('批量爬取完成')
|
||||
// 刷新插件统计
|
||||
await pluginsStore.fetchPlugins()
|
||||
} else {
|
||||
lastCrawlResult.value = {
|
||||
@@ -373,4 +453,11 @@ onMounted(async () => {
|
||||
.invalid-count {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.config-plugin-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user