实现插件配置持久化与任务队列持久化

插件配置持久化:
- 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:
祀梦
2026-04-02 12:35:06 +08:00
parent b77641f059
commit 66943df864
13 changed files with 472 additions and 73 deletions

View File

@@ -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')
}

View File

@@ -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)
},

View File

@@ -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>