Files
param-auto-manager/static/js/search.js
T
hz4th_coder 6534c3315b feat: 新增后台任务系统,支持抓取任务在后台持续运行
- 新增后台任务API (/api/tasks)
- 抓取任务在后台独立运行,不受页面刷新影响
- 支持任务状态查询和手动停止
- 新增后台任务管理界面
- 数据库新增 background_tasks 表
- 前端使用轮询方式更新任务进度
2026-07-14 10:59:18 +08:00

864 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// API基础地址
const API_BASE = '';
// 搜索结果存储
let searchResults = [];
let currentResultIndex = -1;
// 后台任务相关
let currentTaskId = null;
let taskPollInterval = null;
// 页面加载初始化
document.addEventListener('DOMContentLoaded', () => {
// 回车搜索
document.getElementById('search-keyword').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
doSearch();
}
});
// 加载失败URL和后台任务
loadFailedUrls();
loadBackgroundTasks();
});
// 执行搜索
async function doSearch() {
const keyword = document.getElementById('search-keyword').value.trim();
const maxResults = parseInt(document.getElementById('search-count').value) || 10;
const engine = document.getElementById('search-engine').value;
const useCache = document.getElementById('use-cache').checked;
const autoFetch = document.getElementById('auto-fetch').checked;
const autoSave = document.getElementById('auto-save').checked;
if (!keyword) {
showToast('请输入搜索关键词', 'error');
return;
}
// 更新状态
const engineNames = {
'bing_cn': 'Bing 中国',
'bing_global': 'Bing 国际',
'google': 'Google',
'baidu': '百度'
};
updateStatus('searching', `正在通过 ${engineNames[engine]} 搜索...`);
document.getElementById('search-btn').disabled = true;
// 显示进度和停止按钮
shouldStop = false;
const progress = document.getElementById('search-progress');
progress.style.display = 'block';
document.getElementById('stop-btn').style.display = 'inline-flex';
document.getElementById('progress-fill').style.width = '0%';
document.getElementById('progress-text').textContent = '正在搜索...';
try {
const response = await fetch(`${API_BASE}/api/articles/internet-search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ keyword, max_results: maxResults, engine, use_cache: useCache })
});
const data = await response.json();
if (data.success) {
searchResults = data.results.map(r => ({
...r,
fetched: false,
saved: false,
content: ''
}));
displayResults();
// 检查是否被停止
if (shouldStop) {
document.getElementById('stop-btn').style.display = 'none';
document.getElementById('progress-text').textContent = '已停止';
updateStatus('warning', '搜索已停止');
return;
}
// 显示缓存状态
if (data.cached) {
updateStatus('success', `找到 ${searchResults.length} 条结果(使用缓存)`);
showToast('使用缓存结果', 'success');
} else {
updateStatus('success', `找到 ${searchResults.length} 条结果`);
}
document.getElementById('results-count').textContent = `${searchResults.length} 条结果`;
document.getElementById('save-all-btn').disabled = searchResults.length === 0;
document.getElementById('progress-fill').style.width = '100%';
document.getElementById('progress-text').textContent = '搜索完成!';
document.getElementById('stop-btn').style.display = 'none'; // 隐藏停止按钮
// 自动抓取和保存(后台任务方式)
if (autoFetch && searchResults.length > 0) {
setTimeout(() => fetchAllResults(autoSave), 500);
} else {
setTimeout(() => {
progress.style.display = 'none';
}, 1000);
}
} else {
document.getElementById('stop-btn').style.display = 'none';
updateStatus('error', '搜索失败');
showToast('搜索失败: ' + data.error, 'error');
}
} catch (error) {
document.getElementById('stop-btn').style.display = 'none';
updateStatus('error', '搜索出错');
showToast('搜索出错', 'error');
console.error(error);
}
document.getElementById('search-btn').disabled = false;
}
// 显示搜索结果
function displayResults() {
const container = document.getElementById('search-results');
if (searchResults.length === 0) {
container.innerHTML = '<div class="empty-text">未找到相关结果</div>';
return;
}
container.innerHTML = searchResults.map((r, i) => `
<div class="result-item ${r.saved ? 'saved' : ''} ${r.fetched ? 'fetched' : ''}" id="result-${i}">
<div class="result-number">${i + 1}</div>
<div class="result-main">
<div class="result-title" onclick="showResultDetail(${i})">
${r.saved ? '<i class="ri-check-line saved-icon"></i>' : ''}
${escapeHtml(r.title)}
</div>
${r.pageTitle && r.pageTitle !== r.title ? `
<div class="result-page-title">
<i class="ri-article-line"></i> 网页标题: ${escapeHtml(r.pageTitle)}
</div>
` : ''}
<div class="result-url">
<a href="${escapeHtml(r.url)}" target="_blank">
<i class="ri-external-link-line"></i>
${escapeHtml(r.url.substring(0, 70))}${r.url.length > 70 ? '...' : ''}
</a>
</div>
${r.content ? `<div class="result-content-preview">${escapeHtml(r.content.substring(0, 100))}...</div>` : ''}
</div>
<div class="result-actions">
<button onclick="fetchResult(${i})" class="btn btn-sm btn-secondary" id="fetch-btn-${i}" ${r.fetched ? 'disabled' : ''}>
<i class="ri-download-line"></i> ${r.fetched ? '已抓取' : '抓取'}
</button>
<button onclick="saveResult(${i})" class="btn btn-sm btn-success" id="save-btn-${i}" ${r.saved ? 'disabled' : ''}>
<i class="ri-save-line"></i> ${r.saved ? '已保存' : '保存'}
</button>
</div>
</div>
`).join('');
}
// 检查URL是否已在内容库
async function checkUrlExists(url) {
try {
const response = await fetch(`${API_BASE}/api/articles/search?q=${encodeURIComponent(url)}`);
const data = await response.json();
if (data.success && data.articles && data.articles.length > 0) {
// 检查是否有完全匹配的 URL
return data.articles.some(a => a.url === url);
}
} catch (error) {
console.error('检查URL失败:', error);
}
return false;
}
// 抓取单个结果
async function fetchResult(index) {
const result = searchResults[index];
if (!result || result.fetched) return;
const btn = document.getElementById(`fetch-btn-${index}`);
btn.disabled = true;
btn.innerHTML = '<i class="ri-loader-4-line"></i> 检查中...';
// 先检查内容库是否已存在
const exists = await checkUrlExists(result.url);
if (exists) {
searchResults[index].fetched = true;
searchResults[index].saved = true;
searchResults[index].content = '[内容库中已存在此链接]';
btn.innerHTML = '<i class="ri-check-line"></i> 已存在';
btn.className = 'btn btn-sm btn-warning';
const saveBtn = document.getElementById(`save-btn-${index}`);
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="ri-check-line"></i> 已存在';
const item = document.getElementById(`result-${index}`);
item.classList.add('saved');
showToast('该链接已在内容库中', 'warning');
return;
}
btn.innerHTML = '<i class="ri-loader-4-line"></i> 抓取中...';
try {
const response = await fetch(`${API_BASE}/api/articles/fetch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: result.url,
product_names: [result.title],
search_title: result.title // 搜索结果的标题
})
});
const data = await response.json();
if (data.success) {
searchResults[index].fetched = true;
searchResults[index].content = data.data.content;
searchResults[index].pageTitle = data.data.title || result.title; // 抓取到的标题
btn.innerHTML = '<i class="ri-check-line"></i> 已抓取';
// 更新结果显示
const item = document.getElementById(`result-${index}`);
item.classList.add('fetched');
showToast('抓取成功', 'success');
displayResults();
} else {
// 记录失败URL
await recordFailedUrl(result.url, result.title, data.error);
btn.disabled = false;
btn.innerHTML = '<i class="ri-download-line"></i> 抓取';
btn.className = 'btn btn-sm btn-danger';
showToast('抓取失败: ' + data.error, 'error');
}
} catch (error) {
// 记录失败URL
await recordFailedUrl(result.url, result.title, '抓取出错');
btn.disabled = false;
btn.innerHTML = '<i class="ri-download-line"></i> 抓取';
btn.className = 'btn btn-sm btn-danger';
showToast('抓取出错', 'error');
}
}
// 记录失败的URL
async function recordFailedUrl(url, title, errorMessage) {
try {
await fetch(`${API_BASE}/api/articles/failed-urls`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, title, error_message: errorMessage })
});
} catch (error) {
console.error('记录失败URL出错:', error);
}
}
// 保存单个结果
async function saveResult(index) {
const result = searchResults[index];
if (!result) return;
// 如果已经保存(可能是内容库中已存在),直接返回
if (result.saved) {
return;
}
// 如果未抓取,先抓取
if (!result.fetched) {
await fetchResult(index);
// 抓取后如果已标记为 saved(内容库已存在),不继续保存
if (searchResults[index].saved) return;
}
const btn = document.getElementById(`save-btn-${index}`);
btn.disabled = true;
btn.innerHTML = '<i class="ri-loader-4-line"></i> 保存中...';
try {
const response = await fetch(`${API_BASE}/api/articles`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
product_names: [searchResults[index].title],
category: '',
keywords: [],
summary: searchResults[index].content.substring(0, 200),
content: searchResults[index].content,
source: searchResults[index].url,
url: searchResults[index].url
})
});
const data = await response.json();
if (data.success) {
searchResults[index].saved = true;
btn.innerHTML = '<i class="ri-check-line"></i> 已保存';
const item = document.getElementById(`result-${index}`);
item.classList.add('saved');
showToast('保存成功', 'success');
displayResults();
} else {
btn.disabled = false;
btn.innerHTML = '<i class="ri-save-line"></i> 保存';
showToast('保存失败: ' + data.error, 'error');
}
} catch (error) {
btn.disabled = false;
btn.innerHTML = '<i class="ri-save-line"></i> 保存';
showToast('保存出错', 'error');
}
}
// 一键保存全部
async function saveAllResults() {
const unsaved = searchResults.filter(r => !r.saved);
if (unsaved.length === 0) {
showToast('没有需要保存的结果', 'error');
return;
}
const progress = document.getElementById('save-progress');
progress.style.display = 'block';
let saved = 0;
const total = unsaved.length;
for (let i = 0; i < searchResults.length; i++) {
if (searchResults[i].saved) continue;
document.getElementById('save-progress-text').textContent =
`正在保存 ${saved + 1}/${total}...`;
document.getElementById('save-fill').style.width =
`${(saved / total) * 100}%`;
await saveResult(i);
saved++;
}
document.getElementById('save-fill').style.width = '100%';
document.getElementById('save-progress-text').textContent = '保存完成!';
setTimeout(() => {
progress.style.display = 'none';
}, 1500);
showToast(`成功保存 ${saved} 条结果`, 'success');
}
// 抓取所有结果(后台任务方式)
async function fetchAllResults(autoSave) {
const progress = document.getElementById('search-progress');
progress.style.display = 'block';
document.getElementById('stop-btn').style.display = 'inline-flex';
document.getElementById('progress-fill').style.width = '0%';
document.getElementById('progress-text').textContent = '正在启动后台任务...';
// 准备结果数据
const resultsToSend = searchResults.filter(r => !r.fetched).map(r => ({
title: r.title,
url: r.url
}));
if (resultsToSend.length === 0) {
document.getElementById('progress-text').textContent = '所有结果已抓取';
document.getElementById('stop-btn').style.display = 'none';
setTimeout(() => progress.style.display = 'none', 1000);
return;
}
try {
// 启动后台任务
const response = await fetch(`${API_BASE}/api/tasks/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
results: resultsToSend,
auto_save: autoSave,
category: '',
keywords: []
})
});
const data = await response.json();
if (data.success) {
currentTaskId = data.task_id;
showToast('后台任务已启动', 'success');
// 开始轮询任务状态
startTaskPolling(autoSave);
} else {
document.getElementById('stop-btn').style.display = 'none';
showToast('启动后台任务失败: ' + data.error, 'error');
progress.style.display = 'none';
}
} catch (error) {
document.getElementById('stop-btn').style.display = 'none';
showToast('启动任务出错', 'error');
progress.style.display = 'none';
console.error(error);
}
}
// 开始轮询任务状态
function startTaskPolling(autoSave) {
if (taskPollInterval) {
clearInterval(taskPollInterval);
}
taskPollInterval = setInterval(async () => {
try {
const response = await fetch(`${API_BASE}/api/tasks/${currentTaskId}/status`);
const data = await response.json();
if (data.success) {
updateTaskProgress(data.task);
// 任务完成
if (data.task.status === 'completed') {
clearInterval(taskPollInterval);
taskPollInterval = null;
onTaskCompleted(data.task);
} else if (data.task.status === 'failed') {
clearInterval(taskPollInterval);
taskPollInterval = null;
showToast('任务失败: ' + data.task.error_message, 'error');
document.getElementById('stop-btn').style.display = 'none';
} else if (data.task.status === 'stopped') {
clearInterval(taskPollInterval);
taskPollInterval = null;
showToast('任务已停止', 'warning');
onTaskStopped(data.task);
}
}
} catch (error) {
console.error('轮询任务状态失败:', error);
}
}, 1000); // 每秒轮询一次
}
// 更新任务进度显示
function updateTaskProgress(task) {
const progress = document.getElementById('search-progress');
const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text');
const total = task.total || 0;
const current = task.progress || 0;
const currentItem = task.current_item || '';
progressFill.style.width = total > 0 ? `${(current / total) * 100}%` : '0%';
progressText.textContent = `正在抓取 ${current}/${total}... ${currentItem ? currentItem.substring(0, 30) : ''}`;
}
// 任务完成处理
function onTaskCompleted(task) {
const progress = document.getElementById('search-progress');
document.getElementById('progress-fill').style.width = '100%';
const result = task.result || {};
document.getElementById('progress-text').textContent =
`抓取完成!成功 ${result.success || 0},失败 ${result.failed || 0},已保存 ${result.saved || 0}`;
document.getElementById('stop-btn').style.display = 'none';
setTimeout(() => {
progress.style.display = 'none';
}, 2000);
showToast(`抓取完成:成功 ${result.success || 0},失败 ${result.failed || 0}`, 'success');
// 刷新失败URL列表
loadFailedUrls();
// 标记所有结果为已抓取
searchResults.forEach(r => r.fetched = true);
displayResults();
}
// 任务停止处理
function onTaskStopped(task) {
const progress = document.getElementById('search-progress');
document.getElementById('progress-fill').style.width =
task.total > 0 ? `${(task.progress / task.total) * 100}%` : '0%';
document.getElementById('progress-text').textContent =
`已停止(已处理 ${task.progress || 0}/${task.total || 0}`;
document.getElementById('stop-btn').style.display = 'none';
setTimeout(() => {
progress.style.display = 'none';
}, 2000);
loadFailedUrls();
}
// 停止自动处理(后台任务版本)
async function stopAutoProcess() {
if (!currentTaskId) return;
try {
const response = await fetch(`${API_BASE}/api/tasks/${currentTaskId}/stop`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast('正在停止任务...', 'warning');
} else {
showToast('停止任务失败', 'error');
}
} catch (error) {
showToast('停止任务出错', 'error');
}
}
// 显示结果详情
function showResultDetail(index) {
currentResultIndex = index;
const result = searchResults[index];
const body = document.getElementById('result-detail-body');
body.innerHTML = `
<div class="detail-meta">
<div class="detail-meta-item">
<label>标题</label>
<span>${escapeHtml(result.title)}</span>
</div>
<div class="detail-meta-item">
<label>URL</label>
<a href="${escapeHtml(result.url)}" target="_blank">${escapeHtml(result.url)}</a>
</div>
<div class="detail-meta-item">
<label>状态</label>
<span>${result.fetched ? '已抓取' : '未抓取'} / ${result.saved ? '已保存' : '未保存'}</span>
</div>
</div>
${result.content ? `
<div class="detail-section">
<h4><i class="ri-file-text-line"></i> 页面内容</h4>
<pre style="white-space: pre-wrap; max-height: 400px; overflow-y: auto; background: #f8f9fa; padding: 15px; border-radius: 8px;">${escapeHtml(result.content.substring(0, 3000))}${result.content.length > 3000 ? '\n...(内容过长,已截断)' : ''}</pre>
</div>
` : '<div class="detail-section"><p>尚未抓取内容,点击"保存到内容库"将自动抓取并保存</p></div>'}
`;
document.getElementById('result-detail-modal').classList.add('active');
}
// 保存当前结果
async function saveCurrentResult() {
if (currentResultIndex >= 0) {
await saveResult(currentResultIndex);
closeModal('result-detail-modal');
}
}
// 清空结果
function clearResults() {
searchResults = [];
displayResults();
document.getElementById('results-count').textContent = '0 条结果';
document.getElementById('save-all-btn').disabled = true;
updateStatus('ready', '就绪');
}
// 更新状态
function updateStatus(status, text) {
const dot = document.getElementById('search-status');
const statusText = document.getElementById('status-text');
dot.className = 'status-dot';
if (status === 'searching') {
dot.style.background = '#f59e0b';
} else if (status === 'success') {
dot.style.background = '#10b981';
} else if (status === 'error') {
dot.style.background = '#ef4444';
} else {
dot.style.background = '#10b981';
}
statusText.textContent = text;
}
// 关闭模态框
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
// 显示提示
function showToast(message, type = '') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast active ${type}`;
setTimeout(() => {
toast.classList.remove('active');
}, 3000);
}
// HTML转义
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ========== 失败URL管理 ==========
// 加载失败URL列表
async function loadFailedUrls() {
try {
const response = await fetch(`${API_BASE}/api/articles/failed-urls`);
const data = await response.json();
if (data.success) {
document.getElementById('failed-count').textContent = `${data.count} 条`;
const container = document.getElementById('failed-urls-list');
if (data.urls.length === 0) {
container.innerHTML = '<div class="empty-text">暂无失败记录</div>';
} else {
container.innerHTML = data.urls.map(url => `
<div class="failed-url-item" id="failed-${url.id}">
<div class="failed-url-info">
<div class="failed-url-title">${escapeHtml(url.title || url.url.substring(0, 50))}</div>
<div class="failed-url-detail">
<a href="${escapeHtml(url.url)}" target="_blank">
<i class="ri-external-link-line"></i> ${escapeHtml(url.url.substring(0, 60))}${url.url.length > 60 ? '...' : ''}
</a>
<span class="failed-error">${escapeHtml(url.error_message || '未知错误')}</span>
</div>
<div class="failed-url-meta">
<span>重试: ${url.retry_count || 0} 次</span>
<span>${url.created_at || ''}</span>
</div>
</div>
<div class="failed-url-actions">
<button onclick="retryFailedUrl(${url.id}, '${escapeHtml(url.url)}')" class="btn btn-sm btn-warning">
<i class="ri-restart-line"></i> 重试
</button>
<button onclick="deleteFailedUrl(${url.id})" class="btn btn-sm btn-danger">
<i class="ri-delete-bin-line"></i>
</button>
</div>
</div>
`).join('');
}
}
} catch (error) {
console.error('加载失败URL出错:', error);
}
}
// 重试单个失败URL
async function retryFailedUrl(urlId, url) {
const item = document.getElementById(`failed-${urlId}`);
item.classList.add('loading');
try {
const response = await fetch(`${API_BASE}/api/articles/failed-urls/retry`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const data = await response.json();
if (data.success) {
showToast('重试成功', 'success');
loadFailedUrls();
} else {
showToast('重试失败: ' + data.error, 'error');
item.classList.remove('loading');
}
} catch (error) {
showToast('重试出错', 'error');
item.classList.remove('loading');
}
}
// 全部重试
async function retryAllFailed() {
showToast('正在重试所有失败URL...', '');
const response = await fetch(`${API_BASE}/api/articles/failed-urls`);
const data = await response.json();
if (data.success && data.urls.length > 0) {
for (const url of data.urls) {
await retryFailedUrl(url.id, url.url);
await new Promise(r => setTimeout(r, 500)); // 避免太快
}
}
}
// 删除失败URL记录
async function deleteFailedUrl(urlId) {
try {
await fetch(`${API_BASE}/api/articles/failed-urls/${urlId}`, {
method: 'DELETE'
});
loadFailedUrls();
} catch (error) {
showToast('删除失败', 'error');
}
}
// 清空所有失败URL
async function clearFailedUrls() {
if (!confirm('确定要清空所有失败记录吗?')) return;
try {
await fetch(`${API_BASE}/api/articles/failed-urls/clear`, {
method: 'POST'
});
showToast('已清空', 'success');
loadFailedUrls();
} catch (error) {
showToast('清空失败', 'error');
}
}
// ========== 后台任务管理 ==========
// 加载后台任务列表
async function loadBackgroundTasks() {
try {
const response = await fetch(`${API_BASE}/api/tasks/recent?limit=10`);
const data = await response.json();
if (data.success) {
const container = document.getElementById('background-tasks-list');
if (data.tasks.length === 0) {
container.innerHTML = '<div class="empty-text">暂无后台任务</div>';
} else {
container.innerHTML = data.tasks.map(task => {
const statusClass = {
'pending': 'status-pending',
'running': 'status-running',
'completed': 'status-success',
'failed': 'status-error',
'stopped': 'status-warning'
}[task.status] || '';
const statusText = {
'pending': '等待中',
'running': '运行中',
'completed': '已完成',
'failed': '失败',
'stopped': '已停止'
}[task.status] || task.status;
const progressPercent = task.total > 0 ?
Math.round((task.progress / task.total) * 100) : 0;
const result = task.result ? JSON.parse(task.result) : {};
return `
<div class="background-task-item ${statusClass}">
<div class="task-info">
<div class="task-id">${escapeHtml(task.task_id)}</div>
<div class="task-status">
<span class="status-badge ${statusClass}">${statusText}</span>
${task.status === 'running' ?
`<span class="task-progress">${task.progress || 0}/${task.total || 0}</span>` : ''}
${task.status === 'completed' && result.success ?
`<span>成功 ${result.success},失败 ${result.failed},已保存 ${result.saved}</span>` : ''}
</div>
<div class="task-time">${task.created_at || ''}
${task.finished_at ? ' → ' + task.finished_at : ''}
</div>
</div>
<div class="task-actions">
${task.status === 'running' ?
`<button onclick="stopBackgroundTask('${task.task_id}')" class="btn btn-sm btn-danger">
<i class="ri-stop-line"></i> 停止
</button>` : ''}
${task.status !== 'running' && task.status !== 'pending' ?
`<button onclick="deleteBackgroundTask('${task.task_id}')" class="btn btn-sm btn-secondary">
<i class="ri-delete-bin-line"></i>
</button>` : ''}
</div>
</div>
`;
}).join('');
}
}
} catch (error) {
console.error('加载后台任务出错:', error);
}
}
// 停止后台任务
async function stopBackgroundTask(taskId) {
try {
const response = await fetch(`${API_BASE}/api/tasks/${taskId}/stop`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast('任务已停止', 'success');
loadBackgroundTasks();
} else {
showToast('停止失败: ' + data.error, 'error');
}
} catch (error) {
showToast('停止任务出错', 'error');
}
}
// 删除后台任务记录
async function deleteBackgroundTask(taskId) {
try {
const response = await fetch(`${API_BASE}/api/tasks/${taskId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showToast('任务已删除', 'success');
loadBackgroundTasks();
} else {
showToast('删除失败: ' + data.error, 'error');
}
} catch (error) {
showToast('删除任务出错', 'error');
}
}
// 清理已完成的任务
async function clearCompletedTasks() {
try {
const response = await fetch(`${API_BASE}/api/tasks/clear`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
loadBackgroundTasks();
} else {
showToast('清理失败', 'error');
}
} catch (error) {
showToast('清理任务出错', 'error');
}
}