Files
CyberStrikeAI/web/static/js/tasks.js
T
2026-04-13 23:33:02 +08:00

1959 lines
78 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.
// 任务管理页面功能
function _t(key, opts) {
return typeof window.t === 'function' ? window.t(key, opts) : key;
}
/** 插值不转 HTML 实体(避免日期里的 / 变成 / 再被 escapeHtml 成乱码) */
function _tPlain(key, opts) {
if (typeof window.t !== 'function') return key;
const base = opts && typeof opts === 'object' ? opts : {};
const interp = base.interpolation && typeof base.interpolation === 'object' ? base.interpolation : {};
return window.t(key, {
...base,
interpolation: { escapeValue: false, ...interp }
});
}
/** Cron 队列在「本轮 completed」等状态下的展示文案(底层 status 不变,仅 UI 强调循环调度) */
function getBatchQueueStatusPresentation(queue) {
const map = {
pending: { text: _t('tasks.statusPending'), class: 'batch-queue-status-pending' },
running: { text: _t('tasks.statusRunning'), class: 'batch-queue-status-running' },
paused: { text: _t('tasks.statusPaused'), class: 'batch-queue-status-paused' },
completed: { text: _t('tasks.statusCompleted'), class: 'batch-queue-status-completed' },
cancelled: { text: _t('tasks.statusCancelled'), class: 'batch-queue-status-cancelled' }
};
const base = map[queue.status] || { text: queue.status, class: 'batch-queue-status-unknown' };
const cronOn = queue.scheduleMode === 'cron' && queue.scheduleEnabled !== false;
const nextStr = queue.nextRunAt ? new Date(queue.nextRunAt).toLocaleString() : '';
const empty = { sublabel: null, progressNote: null, callout: null };
if (cronOn && queue.status === 'completed') {
return {
text: _t('tasks.statusCronCycleIdle'),
class: 'batch-queue-status-cron-cycle',
sublabel: nextStr ? _tPlain('tasks.cronNextRunLine', { time: nextStr }) : null,
progressNote: _t('tasks.cronRoundDoneProgressHint'),
callout: _t('tasks.cronRecurringCallout')
};
}
if (cronOn && queue.status === 'running') {
return {
text: _t('tasks.statusCronRunning'),
class: 'batch-queue-status-running batch-queue-cron-active',
sublabel: nextStr ? _tPlain('tasks.cronNextRunLine', { time: nextStr }) : null,
progressNote: _t('tasks.cronRunningProgressHint'),
callout: null
};
}
if (cronOn && queue.status === 'pending' && nextStr) {
return {
...base,
...empty,
sublabel: _tPlain('tasks.cronPendingScheduled', { time: nextStr }),
progressNote: _t('tasks.cronPendingProgressNote')
};
}
return { ...base, ...empty };
}
// HTML转义函数(如果未定义)
if (typeof escapeHtml === 'undefined') {
function escapeHtml(text) {
if (text == null) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// 任务管理状态
const tasksState = {
allTasks: [],
filteredTasks: [],
selectedTasks: new Set(),
autoRefresh: true,
refreshInterval: null,
durationUpdateInterval: null,
completedTasksHistory: [], // 保存最近完成的任务历史
showHistory: true // 是否显示历史记录
};
// 从localStorage加载已完成任务历史
function loadCompletedTasksHistory() {
try {
const saved = localStorage.getItem('tasks-completed-history');
if (saved) {
const history = JSON.parse(saved);
// 只保留最近24小时内完成的任务
const now = Date.now();
const oneDayAgo = now - 24 * 60 * 60 * 1000;
tasksState.completedTasksHistory = history.filter(task => {
const completedTime = task.completedAt || task.startedAt;
return completedTime && new Date(completedTime).getTime() > oneDayAgo;
});
// 保存清理后的历史
saveCompletedTasksHistory();
}
} catch (error) {
console.error('加载已完成任务历史失败:', error);
tasksState.completedTasksHistory = [];
}
}
// 保存已完成任务历史到localStorage
function saveCompletedTasksHistory() {
try {
localStorage.setItem('tasks-completed-history', JSON.stringify(tasksState.completedTasksHistory));
} catch (error) {
console.error('保存已完成任务历史失败:', error);
}
}
// 更新已完成任务历史
function updateCompletedTasksHistory(currentTasks) {
// 保存当前所有任务作为快照(用于下次比较)
const currentTaskIds = new Set(currentTasks.map(t => t.conversationId));
// 如果是首次加载,只需要保存当前任务快照
if (tasksState.allTasks.length === 0) {
return;
}
const previousTaskIds = new Set(tasksState.allTasks.map(t => t.conversationId));
// 找出刚完成的任务(之前存在但现在不存在的)
// 只要任务从列表中消失了,就认为它已完成
const justCompleted = tasksState.allTasks.filter(task => {
return previousTaskIds.has(task.conversationId) && !currentTaskIds.has(task.conversationId);
});
// 将刚完成的任务添加到历史中
justCompleted.forEach(task => {
// 检查是否已存在(避免重复添加)
const exists = tasksState.completedTasksHistory.some(t => t.conversationId === task.conversationId);
if (!exists) {
// 如果任务状态不是最终状态,标记为completed
const finalStatus = ['completed', 'failed', 'timeout', 'cancelled'].includes(task.status)
? task.status
: 'completed';
tasksState.completedTasksHistory.push({
conversationId: task.conversationId,
message: task.message || '未命名任务',
startedAt: task.startedAt,
status: finalStatus,
completedAt: new Date().toISOString()
});
}
});
// 限制历史记录数量(最多保留50条)
if (tasksState.completedTasksHistory.length > 50) {
tasksState.completedTasksHistory = tasksState.completedTasksHistory
.sort((a, b) => new Date(b.completedAt || b.startedAt) - new Date(a.completedAt || a.startedAt))
.slice(0, 50);
}
saveCompletedTasksHistory();
}
// 加载任务列表
async function loadTasks() {
const listContainer = document.getElementById('tasks-list');
if (!listContainer) return;
listContainer.innerHTML = '<div class="loading-spinner">' + _t('tasks.loadingTasks') + '</div>';
try {
// 并行加载运行中的任务和已完成的任务历史
const [activeResponse, completedResponse] = await Promise.allSettled([
apiFetch('/api/agent-loop/tasks'),
apiFetch('/api/agent-loop/tasks/completed').catch(() => null) // 如果API不存在,返回null
]);
// 处理运行中的任务
if (activeResponse.status === 'rejected' || !activeResponse.value || !activeResponse.value.ok) {
throw new Error(_t('tasks.loadTaskListFailed'));
}
const activeResult = await activeResponse.value.json();
const activeTasks = activeResult.tasks || [];
// 加载已完成任务历史(如果API可用)
let completedTasks = [];
if (completedResponse.status === 'fulfilled' && completedResponse.value && completedResponse.value.ok) {
try {
const completedResult = await completedResponse.value.json();
completedTasks = completedResult.tasks || [];
} catch (e) {
console.warn('解析已完成任务历史失败:', e);
}
}
// 保存所有任务
tasksState.allTasks = activeTasks;
// 更新已完成任务历史(从后端API获取)
if (completedTasks.length > 0) {
// 合并后端历史记录和本地历史记录(去重)
const backendTaskIds = new Set(completedTasks.map(t => t.conversationId));
const localHistory = tasksState.completedTasksHistory.filter(t =>
!backendTaskIds.has(t.conversationId)
);
// 后端的历史记录优先,然后添加本地独有的
tasksState.completedTasksHistory = [
...completedTasks.map(t => ({
conversationId: t.conversationId,
message: t.message || '未命名任务',
startedAt: t.startedAt,
status: t.status || 'completed',
completedAt: t.completedAt || new Date().toISOString()
})),
...localHistory
];
// 限制历史记录数量
if (tasksState.completedTasksHistory.length > 50) {
tasksState.completedTasksHistory = tasksState.completedTasksHistory
.sort((a, b) => new Date(b.completedAt || b.startedAt) - new Date(a.completedAt || a.startedAt))
.slice(0, 50);
}
saveCompletedTasksHistory();
} else {
// 如果后端API不可用,仍然使用前端逻辑更新历史
updateCompletedTasksHistory(activeTasks);
}
updateTaskStats(activeTasks);
filterAndSortTasks();
startDurationUpdates();
} catch (error) {
console.error('加载任务失败:', error);
listContainer.innerHTML = `
<div class="tasks-empty">
<p>${_t('tasks.loadFailedRetry')}: ${escapeHtml(error.message)}</p>
<button class="btn-secondary" onclick="loadTasks()">${_t('tasks.retry')}</button>
</div>
`;
}
}
// 更新任务统计
function updateTaskStats(tasks) {
const stats = {
running: 0,
cancelling: 0,
completed: 0,
failed: 0,
timeout: 0,
cancelled: 0,
total: tasks.length
};
tasks.forEach(task => {
if (task.status === 'running') {
stats.running++;
} else if (task.status === 'cancelling') {
stats.cancelling++;
} else if (task.status === 'completed') {
stats.completed++;
} else if (task.status === 'failed') {
stats.failed++;
} else if (task.status === 'timeout') {
stats.timeout++;
} else if (task.status === 'cancelled') {
stats.cancelled++;
}
});
const statRunning = document.getElementById('stat-running');
const statCancelling = document.getElementById('stat-cancelling');
const statCompleted = document.getElementById('stat-completed');
const statTotal = document.getElementById('stat-total');
if (statRunning) statRunning.textContent = stats.running;
if (statCancelling) statCancelling.textContent = stats.cancelling;
if (statCompleted) statCompleted.textContent = stats.completed;
if (statTotal) statTotal.textContent = stats.total;
}
// 筛选任务
function filterTasks() {
filterAndSortTasks();
}
// 排序任务
function sortTasks() {
filterAndSortTasks();
}
// 筛选和排序任务
function filterAndSortTasks() {
const statusFilter = document.getElementById('tasks-status-filter')?.value || 'all';
const sortBy = document.getElementById('tasks-sort-by')?.value || 'time-desc';
// 合并当前任务和历史任务
let allTasks = [...tasksState.allTasks];
// 如果显示历史记录,添加历史任务
if (tasksState.showHistory) {
const historyTasks = tasksState.completedTasksHistory
.filter(ht => !tasksState.allTasks.some(t => t.conversationId === ht.conversationId))
.map(ht => ({ ...ht, isHistory: true }));
allTasks = [...allTasks, ...historyTasks];
}
// 筛选
let filtered = allTasks;
if (statusFilter === 'active') {
// 仅运行中的任务(不包括历史)
filtered = tasksState.allTasks.filter(task =>
task.status === 'running' || task.status === 'cancelling'
);
} else if (statusFilter === 'history') {
// 仅历史记录
filtered = allTasks.filter(task => task.isHistory);
} else if (statusFilter !== 'all') {
filtered = allTasks.filter(task => task.status === statusFilter);
}
// 排序
filtered.sort((a, b) => {
const aTime = new Date(a.completedAt || a.startedAt);
const bTime = new Date(b.completedAt || b.startedAt);
switch (sortBy) {
case 'time-asc':
return aTime - bTime;
case 'time-desc':
return bTime - aTime;
case 'status':
return (a.status || '').localeCompare(b.status || '');
case 'message':
return (a.message || '').localeCompare(b.message || '');
default:
return 0;
}
});
tasksState.filteredTasks = filtered;
renderTasks(filtered);
updateBatchActions();
}
// 切换显示历史记录
function toggleShowHistory(show) {
tasksState.showHistory = show;
localStorage.setItem('tasks-show-history', show ? 'true' : 'false');
filterAndSortTasks();
}
// 计算执行时长
function calculateDuration(startedAt) {
if (!startedAt) return _t('tasks.unknown');
const start = new Date(startedAt);
const now = new Date();
const diff = Math.floor((now - start) / 1000);
if (diff < 60) {
return diff + _t('tasks.durationSeconds');
} else if (diff < 3600) {
const minutes = Math.floor(diff / 60);
const seconds = diff % 60;
return minutes + _t('tasks.durationMinutes') + ' ' + seconds + _t('tasks.durationSeconds');
} else {
const hours = Math.floor(diff / 3600);
const minutes = Math.floor((diff % 3600) / 60);
return hours + _t('tasks.durationHours') + ' ' + minutes + _t('tasks.durationMinutes');
}
}
// 开始时长更新
function startDurationUpdates() {
// 清除旧的定时器
if (tasksState.durationUpdateInterval) {
clearInterval(tasksState.durationUpdateInterval);
}
// 每秒更新一次执行时长
tasksState.durationUpdateInterval = setInterval(() => {
updateTaskDurations();
}, 1000);
}
// 更新任务执行时长显示
function updateTaskDurations() {
const taskItems = document.querySelectorAll('.task-item[data-task-id]');
taskItems.forEach(item => {
const startedAt = item.dataset.startedAt;
const status = item.dataset.status;
const durationEl = item.querySelector('.task-duration');
if (durationEl && startedAt && (status === 'running' || status === 'cancelling')) {
durationEl.textContent = calculateDuration(startedAt);
}
});
}
// 渲染任务列表
function renderTasks(tasks) {
const listContainer = document.getElementById('tasks-list');
if (!listContainer) return;
if (tasks.length === 0) {
listContainer.innerHTML = `
<div class="tasks-empty">
<p>${_t('tasks.noMatchingTasks')}</p>
${tasksState.allTasks.length === 0 && tasksState.completedTasksHistory.length > 0 ?
'<p style="margin-top: 8px; color: var(--text-muted); font-size: 0.875rem;">' + _t('tasks.historyHint') + '</p>' : ''}
</div>
`;
return;
}
// 状态映射
const statusMap = {
'running': { text: _t('tasks.statusRunning'), class: 'task-status-running' },
'cancelling': { text: _t('tasks.statusCancelling'), class: 'task-status-cancelling' },
'failed': { text: _t('tasks.statusFailed'), class: 'task-status-failed' },
'timeout': { text: _t('tasks.statusTimeout'), class: 'task-status-timeout' },
'cancelled': { text: _t('tasks.statusCancelled'), class: 'task-status-cancelled' },
'completed': { text: _t('tasks.statusCompleted'), class: 'task-status-completed' }
};
// 分离当前任务和历史任务
const activeTasks = tasks.filter(t => !t.isHistory);
const historyTasks = tasks.filter(t => t.isHistory);
let html = '';
// 渲染当前任务
if (activeTasks.length > 0) {
html += activeTasks.map(task => renderTaskItem(task, statusMap)).join('');
}
// 渲染历史任务
if (historyTasks.length > 0) {
html += `<div class="tasks-history-section">
<div class="tasks-history-header">
<span class="tasks-history-title">📜 ` + _t('tasks.recentCompletedTasks') + `</span>
<button class="btn-secondary btn-small" onclick="clearTasksHistory()">` + _t('tasks.clearHistory') + `</button>
</div>
${historyTasks.map(task => renderTaskItem(task, statusMap, true)).join('')}
</div>`;
}
listContainer.innerHTML = html;
}
// 渲染单个任务项
function renderTaskItem(task, statusMap, isHistory = false) {
const startedTime = task.startedAt ? new Date(task.startedAt) : null;
const completedTime = task.completedAt ? new Date(task.completedAt) : null;
const timeText = startedTime && !isNaN(startedTime.getTime())
? startedTime.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
: _t('tasks.unknownTime');
const completedText = completedTime && !isNaN(completedTime.getTime())
? completedTime.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
: '';
const status = statusMap[task.status] || { text: task.status, class: 'task-status-unknown' };
const isFinalStatus = ['failed', 'timeout', 'cancelled', 'completed'].includes(task.status);
const canCancel = !isFinalStatus && task.status !== 'cancelling' && !isHistory;
const isSelected = tasksState.selectedTasks.has(task.conversationId);
const duration = (task.status === 'running' || task.status === 'cancelling')
? calculateDuration(task.startedAt)
: '';
return `
<div class="task-item ${isHistory ? 'task-item-history' : ''}" data-task-id="${task.conversationId}" data-started-at="${task.startedAt}" data-status="${task.status}">
<div class="task-header">
<div class="task-info">
${canCancel ? `
<label class="task-checkbox">
<input type="checkbox" ${isSelected ? 'checked' : ''}
onchange="toggleTaskSelection('${task.conversationId}', this.checked)">
</label>
` : '<div class="task-checkbox-placeholder"></div>'}
<span class="task-status ${status.class}">${status.text}</span>
${isHistory ? '<span class="task-history-badge" title="' + _t('tasks.historyBadge') + '">📜</span>' : ''}
<span class="task-message" title="${escapeHtml(task.message || _t('tasks.unnamedTask'))}">${escapeHtml(task.message || _t('tasks.unnamedTask'))}</span>
</div>
<div class="task-actions">
${duration ? `<span class="task-duration" title="${_t('tasks.duration')}">⏱ ${duration}</span>` : ''}
<span class="task-time" title="${isHistory && completedText ? _t('tasks.completedAt') : _t('tasks.startedAt')}">
${isHistory && completedText ? completedText : timeText}
</span>
${canCancel ? `<button class="btn-secondary btn-small" onclick="cancelTask('${task.conversationId}', this)">` + _t('tasks.cancelTask') + `</button>` : ''}
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewConversation('${task.conversationId}')">` + _t('tasks.viewConversation') + `</button>` : ''}
</div>
</div>
${task.conversationId ? `
<div class="task-details">
<span class="task-id-label">` + _t('tasks.conversationIdLabel') + `:</span>
<span class="task-id-value" title="` + _t('tasks.clickToCopy') + `" onclick="copyTaskId('${task.conversationId}')">${escapeHtml(task.conversationId)}</span>
</div>
` : ''}
</div>
`;
}
// 清空任务历史
function clearTasksHistory() {
if (!confirm(_t('tasks.clearHistoryConfirm'))) {
return;
}
tasksState.completedTasksHistory = [];
saveCompletedTasksHistory();
filterAndSortTasks();
}
// 切换任务选择
function toggleTaskSelection(conversationId, selected) {
if (selected) {
tasksState.selectedTasks.add(conversationId);
} else {
tasksState.selectedTasks.delete(conversationId);
}
updateBatchActions();
}
// 更新批量操作UI
function updateBatchActions() {
const batchActions = document.getElementById('tasks-batch-actions');
const selectedCount = document.getElementById('tasks-selected-count');
if (!batchActions || !selectedCount) return;
const count = tasksState.selectedTasks.size;
if (count > 0) {
batchActions.style.display = 'flex';
selectedCount.textContent = typeof window.t === 'function' ? window.t('mcp.selectedCount', { count: count }) : `已选择 ${count}`;
} else {
batchActions.style.display = 'none';
}
}
// 清除任务选择
function clearTaskSelection() {
tasksState.selectedTasks.clear();
updateBatchActions();
// 重新渲染以更新复选框状态
filterAndSortTasks();
}
// 批量取消任务
async function batchCancelTasks() {
const selected = Array.from(tasksState.selectedTasks);
if (selected.length === 0) return;
if (!confirm(_t('tasks.confirmCancelTasks', { n: selected.length }))) {
return;
}
let successCount = 0;
let failCount = 0;
for (const conversationId of selected) {
try {
const response = await apiFetch('/api/agent-loop/cancel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ conversationId }),
});
if (response.ok) {
successCount++;
} else {
failCount++;
}
} catch (error) {
console.error('取消任务失败:', conversationId, error);
failCount++;
}
}
// 清除选择
clearTaskSelection();
// 刷新任务列表
await loadTasks();
// 显示结果
if (failCount > 0) {
alert(_t('tasks.batchCancelResultPartial', { success: successCount, fail: failCount }));
} else {
alert(_t('tasks.batchCancelResultSuccess', { n: successCount }));
}
}
// 复制任务ID
function copyTaskId(conversationId) {
navigator.clipboard.writeText(conversationId).then(() => {
// 显示复制成功提示
const tooltip = document.createElement('div');
tooltip.textContent = _t('tasks.copiedToast');
tooltip.style.cssText = 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.8); color: white; padding: 8px 16px; border-radius: 4px; z-index: 10000;';
document.body.appendChild(tooltip);
setTimeout(() => tooltip.remove(), 1000);
}).catch(err => {
console.error('复制失败:', err);
});
}
// 取消任务
async function cancelTask(conversationId, button) {
if (!conversationId) return;
const originalText = button.textContent;
button.disabled = true;
button.textContent = _t('tasks.cancelling');
try {
const response = await apiFetch('/api/agent-loop/cancel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ conversationId }),
});
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || _t('tasks.cancelTaskFailed'));
}
// 从选择中移除
tasksState.selectedTasks.delete(conversationId);
updateBatchActions();
// 重新加载任务列表
await loadTasks();
} catch (error) {
console.error('取消任务失败:', error);
alert(_t('tasks.cancelTaskFailed') + ': ' + error.message);
button.disabled = false;
button.textContent = originalText;
}
}
// 查看对话
function viewConversation(conversationId) {
if (!conversationId) return;
// 切换到对话页面
if (typeof switchPage === 'function') {
switchPage('chat');
// 加载并选中该对话 - 使用全局函数
setTimeout(() => {
// 尝试多种方式加载对话
if (typeof loadConversation === 'function') {
loadConversation(conversationId);
} else if (typeof window.loadConversation === 'function') {
window.loadConversation(conversationId);
} else {
// 如果函数不存在,尝试通过URL跳转
window.location.hash = `chat?conversation=${conversationId}`;
console.log('切换到对话页面,对话ID:', conversationId);
}
}, 500);
}
}
// 刷新任务列表
async function refreshTasks() {
await loadTasks();
}
// 切换自动刷新
function toggleTasksAutoRefresh(enabled) {
tasksState.autoRefresh = enabled;
// 保存到localStorage
localStorage.setItem('tasks-auto-refresh', enabled ? 'true' : 'false');
if (enabled) {
// 启动自动刷新
if (!tasksState.refreshInterval) {
tasksState.refreshInterval = setInterval(() => {
loadBatchQueues();
}, 5000);
}
} else {
// 停止自动刷新
if (tasksState.refreshInterval) {
clearInterval(tasksState.refreshInterval);
tasksState.refreshInterval = null;
}
}
}
// 初始化任务管理页面
function initTasksPage() {
// 恢复自动刷新设置
const autoRefreshCheckbox = document.getElementById('tasks-auto-refresh');
if (autoRefreshCheckbox) {
const saved = localStorage.getItem('tasks-auto-refresh');
const enabled = saved !== null ? saved === 'true' : true;
autoRefreshCheckbox.checked = enabled;
toggleTasksAutoRefresh(enabled);
} else {
toggleTasksAutoRefresh(true);
}
// 只加载批量任务队列
loadBatchQueues();
}
// 清理定时器(页面切换时调用)
function cleanupTasksPage() {
if (tasksState.refreshInterval) {
clearInterval(tasksState.refreshInterval);
tasksState.refreshInterval = null;
}
if (tasksState.durationUpdateInterval) {
clearInterval(tasksState.durationUpdateInterval);
tasksState.durationUpdateInterval = null;
}
tasksState.selectedTasks.clear();
stopBatchQueueRefresh();
}
// 导出函数供全局使用
window.loadTasks = loadTasks;
window.cancelTask = cancelTask;
window.viewConversation = viewConversation;
window.refreshTasks = refreshTasks;
window.initTasksPage = initTasksPage;
window.cleanupTasksPage = cleanupTasksPage;
window.filterTasks = filterTasks;
window.sortTasks = sortTasks;
window.toggleTaskSelection = toggleTaskSelection;
window.clearTaskSelection = clearTaskSelection;
window.batchCancelTasks = batchCancelTasks;
window.copyTaskId = copyTaskId;
window.toggleTasksAutoRefresh = toggleTasksAutoRefresh;
window.toggleShowHistory = toggleShowHistory;
window.clearTasksHistory = clearTasksHistory;
// ==================== 批量任务功能 ====================
// 批量任务状态
const batchQueuesState = {
queues: [],
currentQueueId: null,
refreshInterval: null,
// 筛选和分页状态
filterStatus: 'all', // 'all', 'pending', 'running', 'paused', 'completed', 'cancelled'
searchKeyword: '',
currentPage: 1,
pageSize: 10,
total: 0,
totalPages: 1
};
// 显示新建任务模态框
async function showBatchImportModal() {
const modal = document.getElementById('batch-import-modal');
const input = document.getElementById('batch-tasks-input');
const titleInput = document.getElementById('batch-queue-title');
const roleSelect = document.getElementById('batch-queue-role');
const agentModeSelect = document.getElementById('batch-queue-agent-mode');
const scheduleModeSelect = document.getElementById('batch-queue-schedule-mode');
const cronExprInput = document.getElementById('batch-queue-cron-expr');
if (modal && input) {
input.value = '';
if (titleInput) {
titleInput.value = '';
}
// 重置角色选择为默认
if (roleSelect) {
roleSelect.value = '';
}
if (agentModeSelect) {
agentModeSelect.value = 'single';
}
if (scheduleModeSelect) {
scheduleModeSelect.value = 'manual';
}
if (cronExprInput) {
cronExprInput.value = '';
}
handleBatchScheduleModeChange();
updateBatchImportStats('');
// 加载并填充角色列表
if (roleSelect && typeof loadRoles === 'function') {
try {
const loadedRoles = await loadRoles();
// 清空现有选项(除了默认选项)
roleSelect.innerHTML = '<option value="">' + _t('batchImportModal.defaultRole') + '</option>';
// 添加已启用的角色
const sortedRoles = loadedRoles.sort((a, b) => {
if (a.name === '默认') return -1;
if (b.name === '默认') return 1;
return (a.name || '').localeCompare(b.name || '', 'zh-CN');
});
sortedRoles.forEach(role => {
if (role.name !== '默认' && role.enabled !== false) {
const option = document.createElement('option');
option.value = role.name;
option.textContent = role.name;
roleSelect.appendChild(option);
}
});
} catch (error) {
console.error('加载角色列表失败:', error);
}
}
modal.style.display = 'block';
input.focus();
}
}
// 关闭新建任务模态框
function closeBatchImportModal() {
const modal = document.getElementById('batch-import-modal');
if (modal) {
modal.style.display = 'none';
}
}
function handleBatchScheduleModeChange() {
const scheduleModeSelect = document.getElementById('batch-queue-schedule-mode');
const cronGroup = document.getElementById('batch-queue-cron-group');
const cronExprInput = document.getElementById('batch-queue-cron-expr');
const isCron = scheduleModeSelect && scheduleModeSelect.value === 'cron';
if (cronGroup) {
cronGroup.style.display = isCron ? 'block' : 'none';
}
if (cronExprInput) {
if (isCron) {
cronExprInput.setAttribute('required', 'required');
} else {
cronExprInput.removeAttribute('required');
cronExprInput.value = '';
}
}
}
// 更新新建任务统计
function updateBatchImportStats(text) {
const statsEl = document.getElementById('batch-import-stats');
if (!statsEl) return;
const lines = text.split('\n').filter(line => line.trim() !== '');
const count = lines.length;
if (count > 0) {
statsEl.innerHTML = '<div class="batch-import-stat">' + _t('tasks.taskCount', { count: count }) + '</div>';
statsEl.style.display = 'block';
} else {
statsEl.style.display = 'none';
}
}
// 监听批量任务输入
document.addEventListener('DOMContentLoaded', function() {
const input = document.getElementById('batch-tasks-input');
if (input) {
input.addEventListener('input', function() {
updateBatchImportStats(this.value);
});
}
});
// 创建批量任务队列
async function createBatchQueue() {
const input = document.getElementById('batch-tasks-input');
const titleInput = document.getElementById('batch-queue-title');
const roleSelect = document.getElementById('batch-queue-role');
const agentModeSelect = document.getElementById('batch-queue-agent-mode');
const scheduleModeSelect = document.getElementById('batch-queue-schedule-mode');
const cronExprInput = document.getElementById('batch-queue-cron-expr');
if (!input) return;
const text = input.value.trim();
if (!text) {
alert(_t('tasks.enterTaskPrompt'));
return;
}
// 按行分割任务
const tasks = text.split('\n').map(line => line.trim()).filter(line => line !== '');
if (tasks.length === 0) {
alert(_t('tasks.noValidTask'));
return;
}
// 获取标题(可选)
const title = titleInput ? titleInput.value.trim() : '';
// 获取角色(可选,空字符串表示默认角色)
const role = roleSelect ? roleSelect.value || '' : '';
const agentMode = agentModeSelect ? (agentModeSelect.value === 'multi' ? 'multi' : 'single') : 'single';
const scheduleMode = scheduleModeSelect ? (scheduleModeSelect.value === 'cron' ? 'cron' : 'manual') : 'manual';
const cronExpr = cronExprInput ? cronExprInput.value.trim() : '';
if (scheduleMode === 'cron' && !cronExpr) {
alert(_t('batchImportModal.cronExprRequired'));
return;
}
try {
const response = await apiFetch('/api/batch-tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title, tasks, role, agentMode, scheduleMode, cronExpr }),
});
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || _t('tasks.createBatchQueueFailed'));
}
const result = await response.json();
closeBatchImportModal();
// 显示队列详情
showBatchQueueDetail(result.queueId);
// 刷新批量队列列表
refreshBatchQueues();
} catch (error) {
console.error('创建批量任务队列失败:', error);
alert(_t('tasks.createBatchQueueFailed') + ': ' + error.message);
}
}
// 获取角色图标(辅助函数)
function getRoleIconForDisplay(roleName, rolesList) {
if (!roleName || roleName === '') {
return '🔵'; // 默认角色图标
}
if (Array.isArray(rolesList) && rolesList.length > 0) {
const role = rolesList.find(r => r.name === roleName);
if (role && role.icon) {
let icon = role.icon;
// 检查是否是 Unicode 转义格式(可能包含引号)
const unicodeMatch = icon.match(/^"?\\U([0-9A-F]{8})"?$/i);
if (unicodeMatch) {
try {
const codePoint = parseInt(unicodeMatch[1], 16);
icon = String.fromCodePoint(codePoint);
} catch (e) {
// 转换失败,使用默认图标
console.warn('转换 icon Unicode 转义失败:', icon, e);
return '👤';
}
}
return icon;
}
}
return '👤'; // 默认图标
}
// 加载批量任务队列列表
async function loadBatchQueues(page) {
const section = document.getElementById('batch-queues-section');
if (!section) return;
// 如果指定了page,使用它;否则使用当前页
if (page !== undefined) {
batchQueuesState.currentPage = page;
}
// 加载角色列表(用于显示正确的角色图标)
let loadedRoles = [];
if (typeof loadRoles === 'function') {
try {
loadedRoles = await loadRoles();
} catch (error) {
console.warn('加载角色列表失败,将使用默认图标:', error);
}
}
batchQueuesState.loadedRoles = loadedRoles; // 保存到状态中供渲染使用
// 构建查询参数
const params = new URLSearchParams();
params.append('page', batchQueuesState.currentPage.toString());
params.append('limit', batchQueuesState.pageSize.toString());
if (batchQueuesState.filterStatus && batchQueuesState.filterStatus !== 'all') {
params.append('status', batchQueuesState.filterStatus);
}
if (batchQueuesState.searchKeyword) {
params.append('keyword', batchQueuesState.searchKeyword);
}
try {
const response = await apiFetch(`/api/batch-tasks?${params.toString()}`);
if (!response.ok) {
throw new Error(_t('tasks.loadFailedRetry'));
}
const result = await response.json();
batchQueuesState.queues = result.queues || [];
batchQueuesState.total = result.total || 0;
batchQueuesState.totalPages = result.total_pages || 1;
renderBatchQueues();
} catch (error) {
console.error('加载批量任务队列失败:', error);
section.style.display = 'block';
const list = document.getElementById('batch-queues-list');
if (list) {
list.innerHTML = '<div class="tasks-empty"><p>' + _t('tasks.loadFailedRetry') + ': ' + escapeHtml(error.message) + '</p><button class="btn-secondary" onclick="refreshBatchQueues()">' + _t('tasks.retry') + '</button></div>';
}
}
}
// 筛选批量任务队列
function filterBatchQueues() {
const statusFilter = document.getElementById('batch-queues-status-filter');
const searchInput = document.getElementById('batch-queues-search');
if (statusFilter) {
batchQueuesState.filterStatus = statusFilter.value;
}
if (searchInput) {
batchQueuesState.searchKeyword = searchInput.value.trim();
}
// 重置到第一页并重新加载
batchQueuesState.currentPage = 1;
loadBatchQueues(1);
}
// 渲染批量任务队列列表
function renderBatchQueues() {
const section = document.getElementById('batch-queues-section');
const list = document.getElementById('batch-queues-list');
const pagination = document.getElementById('batch-queues-pagination');
if (!section || !list) return;
section.style.display = 'block';
const queues = batchQueuesState.queues;
if (queues.length === 0) {
list.innerHTML = '<div class="tasks-empty"><p>' + _t('tasks.noBatchQueues') + '</p></div>';
if (pagination) pagination.style.display = 'none';
return;
}
// 确保分页控件可见(重置之前可能设置的 display: none
if (pagination) {
pagination.style.display = '';
}
list.innerHTML = queues.map(queue => {
const pres = getBatchQueueStatusPresentation(queue);
// 统计任务状态
const stats = {
total: queue.tasks.length,
pending: 0,
running: 0,
completed: 0,
failed: 0,
cancelled: 0
};
queue.tasks.forEach(task => {
if (task.status === 'pending') stats.pending++;
else if (task.status === 'running') stats.running++;
else if (task.status === 'completed') stats.completed++;
else if (task.status === 'failed') stats.failed++;
else if (task.status === 'cancelled') stats.cancelled++;
});
const progress = stats.total > 0 ? Math.round((stats.completed + stats.failed + stats.cancelled) / stats.total * 100) : 0;
// 允许删除待执行、已完成或已取消状态的队列
const canDelete = queue.status === 'pending' || queue.status === 'completed' || queue.status === 'cancelled';
const loadedRoles = batchQueuesState.loadedRoles || [];
const roleIcon = getRoleIconForDisplay(queue.role, loadedRoles);
const roleName = queue.role && queue.role !== '' ? queue.role : _t('batchQueueDetailModal.defaultRole');
const isCronCycleIdle = queue.scheduleMode === 'cron' && queue.scheduleEnabled !== false && queue.status === 'completed';
const cardMod = isCronCycleIdle ? ' batch-queue-item--cron-wait' : '';
const progressFillMod = isCronCycleIdle ? ' batch-queue-progress-fill--cron-wait' : '';
const agentLabel = queue.agentMode === 'multi' ? _t('batchImportModal.agentModeMulti') : _t('batchImportModal.agentModeSingle');
let scheduleLabel = queue.scheduleMode === 'cron' ? _t('batchImportModal.scheduleModeCron') : _t('batchImportModal.scheduleModeManual');
if (queue.scheduleMode === 'cron' && queue.cronExpr) {
scheduleLabel += ` (${queue.cronExpr})`;
}
const configLine = [roleName, agentLabel, scheduleLabel].map(s => escapeHtml(s)).join(' · ');
const cronPausedNote = queue.scheduleMode === 'cron' && queue.scheduleEnabled === false
? ` <span class="batch-queue-inline-warn" title="${escapeHtml(_t('batchQueueDetailModal.scheduleCronAutoHint'))}">(${escapeHtml(_t('batchQueueDetailModal.cronSchedulePausedBadge'))})</span>`
: '';
const shortId = queue.id.length > 14 ? escapeHtml(queue.id.slice(0, 12)) + '\u2026' : escapeHtml(queue.id);
const titleBlock = queue.title
? `<h4 class="batch-queue-card-title">${escapeHtml(queue.title)}</h4>`
: `<h4 class="batch-queue-card-title batch-queue-card-title--muted">${escapeHtml(_t('tasks.batchQueueUntitled'))}</h4>`;
const doneCount = stats.completed + stats.failed + stats.cancelled;
const statsCompact = `<span class="batch-queue-statsline__item">${escapeHtml(_t('tasks.totalLabel'))}\u00a0${stats.total}</span><span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item">${escapeHtml(_t('tasks.pendingLabel'))}\u00a0${stats.pending}</span><span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item">${escapeHtml(_t('tasks.runningLabel'))}\u00a0${stats.running}</span><span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item batch-queue-statsline__item--ok">${escapeHtml(_t('tasks.completedLabel'))}\u00a0${stats.completed}</span><span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item batch-queue-statsline__item--err">${escapeHtml(_t('tasks.failedLabel'))}\u00a0${stats.failed}</span>${stats.cancelled > 0 ? `<span class="batch-queue-statsline__sep">\u00b7</span><span class="batch-queue-statsline__item">${escapeHtml(_t('tasks.cancelledLabel'))}\u00a0${stats.cancelled}</span>` : ''}`;
return `
<div class="batch-queue-item batch-queue-item--compact${cardMod}" data-queue-id="${queue.id}" onclick="showBatchQueueDetail('${queue.id}')">
<div class="batch-queue-item__inner">
<div class="batch-queue-item__top">
<div class="batch-queue-item__title-col">
${titleBlock}
<p class="batch-queue-item__config">${configLine}${cronPausedNote}</p>
<p class="batch-queue-item__idline"><code title="${escapeHtml(queue.id)}">${shortId}</code><span class="batch-queue-item__idsep">\u00b7</span><span>${escapeHtml(_t('tasks.createdTimeLabel'))}\u00a0${escapeHtml(new Date(queue.createdAt).toLocaleString())}</span></p>
</div>
<div class="batch-queue-item__top-actions" onclick="event.stopPropagation();">
${canDelete ? `<button type="button" class="batch-queue-icon-btn" onclick="deleteBatchQueueFromList('${queue.id}')" title="${escapeHtml(_t('tasks.deleteQueue'))}" aria-label="${escapeHtml(_t('tasks.deleteQueue'))}"><svg class="batch-queue-icon-btn__svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>` : ''}
</div>
</div>
<div class="batch-queue-item__mid">
<div class="batch-queue-item__mid-left">
<span class="batch-queue-status ${pres.class}">${escapeHtml(pres.text)}</span>
${pres.sublabel ? `<span class="batch-queue-item__sublabel">${escapeHtml(pres.sublabel)}</span>` : ''}
</div>
<div class="batch-queue-item__mid-right">
<div class="batch-queue-progress-bar batch-queue-progress-bar--card batch-queue-progress-bar--list">
<div class="batch-queue-progress-fill${progressFillMod}" style="width: ${progress}%"></div>
</div>
<span class="batch-queue-item__pct">${progress}%\u00a0<span class="batch-queue-item__pct-frac">(${doneCount}/${stats.total})</span></span>
</div>
</div>
<div class="batch-queue-statsline" aria-label="${escapeHtml(_t('tasks.batchQueueTitle'))}">${statsCompact}</div>
</div>
</div>
`;
}).join('');
// 渲染分页控件
renderBatchQueuesPagination();
}
// 渲染批量任务队列分页控件(结构与样式对齐 MCP 监控 .monitor-pagination
function renderBatchQueuesPagination() {
const paginationContainer = document.getElementById('batch-queues-pagination');
if (!paginationContainer) return;
const { currentPage, pageSize, total, totalPages } = batchQueuesState;
// 即使只有一页也显示分页信息(与 MCP 监控一致)
if (total === 0) {
paginationContainer.innerHTML = '';
return;
}
// 计算显示范围
const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1;
const end = total === 0 ? 0 : Math.min(currentPage * pageSize, total);
let paginationHTML = '<div class="monitor-pagination">';
// 左侧:显示范围信息和每页数量选择器(参考Skills样式)
paginationHTML += `
<div class="pagination-info">
<span>` + _t('tasks.paginationShow', { start: start, end: end, total: total }) + `</span>
<label class="pagination-page-size">
` + _t('tasks.paginationPerPage') + `
<select id="batch-queues-page-size-pagination" onchange="changeBatchQueuesPageSize()">
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
<option value="50" ${pageSize === 50 ? 'selected' : ''}>50</option>
<option value="100" ${pageSize === 100 ? 'selected' : ''}>100</option>
</select>
</label>
</div>
`;
// 右侧:分页按钮(参考Skills样式:首页、上一页、第X/Y页、下一页、末页)
paginationHTML += `
<div class="pagination-controls">
<button class="btn-secondary" onclick="goBatchQueuesPage(1)" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>` + _t('tasks.paginationFirst') + `</button>
<button class="btn-secondary" onclick="goBatchQueuesPage(${currentPage - 1})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>` + _t('tasks.paginationPrev') + `</button>
<span class="pagination-page">` + _t('tasks.paginationPage', { current: currentPage, total: totalPages || 1 }) + `</span>
<button class="btn-secondary" onclick="goBatchQueuesPage(${currentPage + 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>` + _t('tasks.paginationNext') + `</button>
<button class="btn-secondary" onclick="goBatchQueuesPage(${totalPages || 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>` + _t('tasks.paginationLast') + `</button>
</div>
`;
paginationHTML += '</div>';
paginationContainer.innerHTML = paginationHTML;
}
// 跳转到指定页面
function goBatchQueuesPage(page) {
const { totalPages } = batchQueuesState;
if (page < 1 || page > totalPages) return;
loadBatchQueues(page);
// 滚动到列表顶部
const list = document.getElementById('batch-queues-list');
if (list) {
list.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
// 改变每页显示数量
function changeBatchQueuesPageSize() {
const pageSizeSelect = document.getElementById('batch-queues-page-size-pagination');
if (!pageSizeSelect) return;
const newPageSize = parseInt(pageSizeSelect.value, 10);
if (newPageSize && newPageSize > 0) {
batchQueuesState.pageSize = newPageSize;
batchQueuesState.currentPage = 1; // 重置到第一页
loadBatchQueues(1);
}
}
// 显示批量任务队列详情
async function showBatchQueueDetail(queueId) {
const modal = document.getElementById('batch-queue-detail-modal');
const title = document.getElementById('batch-queue-detail-title');
const content = document.getElementById('batch-queue-detail-content');
const startBtn = document.getElementById('batch-queue-start-btn');
const cancelBtn = document.getElementById('batch-queue-cancel-btn');
const deleteBtn = document.getElementById('batch-queue-delete-btn');
const addTaskBtn = document.getElementById('batch-queue-add-task-btn');
if (!modal || !content) return;
try {
// 加载角色列表(如果还未加载)
let loadedRoles = [];
if (typeof loadRoles === 'function') {
try {
loadedRoles = await loadRoles();
} catch (error) {
console.warn('加载角色列表失败,将使用默认图标:', error);
}
}
const response = await apiFetch(`/api/batch-tasks/${queueId}`);
if (!response.ok) {
throw new Error(_t('tasks.getQueueDetailFailed'));
}
const result = await response.json();
const queue = result.queue;
batchQueuesState.currentQueueId = queueId;
const pres = getBatchQueueStatusPresentation(queue);
if (title) {
// textContent 本身会做转义;这里不要再 escapeHtml,否则会把 && 显示成 &amp;...(看起来像“变形/乱码”)
title.textContent = queue.title ? _t('tasks.batchQueueTitle') + ' - ' + String(queue.title) : _t('tasks.batchQueueTitle');
}
// 更新按钮显示
const pauseBtn = document.getElementById('batch-queue-pause-btn');
if (addTaskBtn) {
addTaskBtn.style.display = queue.status === 'pending' ? 'inline-block' : 'none';
}
if (startBtn) {
// pending状态显示"开始执行"paused状态显示"继续执行"
startBtn.style.display = (queue.status === 'pending' || queue.status === 'paused') ? 'inline-block' : 'none';
if (startBtn && queue.status === 'paused') {
startBtn.textContent = _t('tasks.resumeExecute');
} else if (startBtn && queue.status === 'pending') {
startBtn.textContent = _t('batchQueueDetailModal.startExecute');
}
}
if (pauseBtn) {
// running状态显示"暂停队列"
pauseBtn.style.display = queue.status === 'running' ? 'inline-block' : 'none';
}
if (deleteBtn) {
// 允许删除待执行、已完成或已取消状态的队列
deleteBtn.style.display = (queue.status === 'pending' || queue.status === 'completed' || queue.status === 'cancelled' || queue.status === 'paused') ? 'inline-block' : 'none';
}
// 任务状态映射
const taskStatusMap = {
'pending': { text: _t('tasks.statusPending'), class: 'batch-task-status-pending' },
'running': { text: _t('tasks.statusRunning'), class: 'batch-task-status-running' },
'completed': { text: _t('tasks.statusCompleted'), class: 'batch-task-status-completed' },
'failed': { text: _t('tasks.failedLabel'), class: 'batch-task-status-failed' },
'cancelled': { text: _t('tasks.statusCancelled'), class: 'batch-task-status-cancelled' }
};
let roleLineVal = '';
if (queue.role && queue.role !== '') {
let roleName = queue.role;
let roleIcon = '\uD83D\uDC64';
if (Array.isArray(loadedRoles) && loadedRoles.length > 0) {
const role = loadedRoles.find(r => r.name === roleName);
if (role && role.icon) {
let icon = role.icon;
const unicodeMatch = icon.match(/^"?\\U([0-9A-F]{8})"?$/i);
if (unicodeMatch) {
try {
const codePoint = parseInt(unicodeMatch[1], 16);
icon = String.fromCodePoint(codePoint);
} catch (e) {
// ignore
}
}
roleIcon = icon;
}
}
roleLineVal = roleIcon + ' ' + escapeHtml(roleName);
} else {
roleLineVal = '\uD83D\uDD35 ' + escapeHtml(_t('batchQueueDetailModal.defaultRole'));
}
const agentModeText = queue.agentMode === 'multi' ? _t('batchImportModal.agentModeMulti') : _t('batchImportModal.agentModeSingle');
const scheduleModeText = queue.scheduleMode === 'cron' ? _t('batchImportModal.scheduleModeCron') : _t('batchImportModal.scheduleModeManual');
const scheduleDetail = escapeHtml(scheduleModeText) + (queue.scheduleMode === 'cron' && queue.cronExpr ? `${escapeHtml(queue.cronExpr)}` : '');
const showProgressNoteInModal = !!(pres.progressNote && !pres.callout);
// 保存滚动位置,防止刷新时滚动条弹回顶部
const modalBody = content.closest('.modal-body');
const tasksList = content.querySelector('.batch-queue-tasks-list');
const savedModalBodyScrollTop = modalBody ? modalBody.scrollTop : 0;
const savedTasksListScrollTop = tasksList ? tasksList.scrollTop : 0;
content.innerHTML = `
<div class="batch-queue-detail-layout">
<section class="batch-queue-detail-hero">
<span class="batch-queue-status ${pres.class}">${escapeHtml(pres.text)}</span>
${pres.sublabel ? `<p class="batch-queue-detail-hero__sub">${escapeHtml(pres.sublabel)}</p>` : ''}
${showProgressNoteInModal ? `<p class="batch-queue-detail-hero__note">${escapeHtml(pres.progressNote)}</p>` : ''}
</section>
<section class="batch-queue-detail-kv">
${queue.title ? `<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.queueTitle'))}</span><span class="bq-kv__v">${escapeHtml(queue.title)}</span></div>` : ''}
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.role'))}</span><span class="bq-kv__v">${roleLineVal}</span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchImportModal.agentMode'))}</span><span class="bq-kv__v">${escapeHtml(agentModeText)}</span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchImportModal.scheduleMode'))}</span><span class="bq-kv__v">${scheduleDetail}</span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.taskTotal'))}</span><span class="bq-kv__v">${queue.tasks.length}</span></div>
${queue.scheduleMode === 'cron' ? `<div class="bq-kv bq-kv--block"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.scheduleCronAuto'))}</span><span class="bq-kv__v bq-kv__v--control"><label class="bq-cron-toggle"><input type="checkbox" ${queue.scheduleEnabled !== false ? 'checked' : ''} onchange="updateBatchQueueScheduleEnabled(this.checked)" /><span class="bq-cron-toggle__hint">${escapeHtml(_t('batchQueueDetailModal.scheduleCronAutoHint'))}</span></label></span></div>` : ''}
</section>
${queue.lastScheduleError ? `<div class="bq-alert bq-alert--err"><strong>${escapeHtml(_t('batchQueueDetailModal.lastScheduleError'))}</strong><p>${escapeHtml(queue.lastScheduleError)}</p></div>` : ''}
${queue.lastRunError ? `<div class="bq-alert bq-alert--err"><strong>${escapeHtml(_t('batchQueueDetailModal.lastRunError'))}</strong><p>${escapeHtml(queue.lastRunError)}</p></div>` : ''}
${pres.callout ? `<div class="batch-queue-cron-callout batch-queue-cron-callout--compact"><span class="batch-queue-cron-callout-icon" aria-hidden="true">\u21BB</span><p>${escapeHtml(pres.callout)}</p></div>` : ''}
<details class="batch-queue-detail-tech">
<summary class="batch-queue-detail-tech__sum">${escapeHtml(_t('batchQueueDetailModal.technicalDetails'))}</summary>
<div class="batch-queue-detail-tech__body">
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.queueId'))}</span><span class="bq-kv__v"><code>${escapeHtml(queue.id)}</code></span></div>
<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.createdAt'))}</span><span class="bq-kv__v">${escapeHtml(new Date(queue.createdAt).toLocaleString())}</span></div>
${queue.startedAt ? `<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.startedAt'))}</span><span class="bq-kv__v">${escapeHtml(new Date(queue.startedAt).toLocaleString())}</span></div>` : ''}
${queue.completedAt ? `<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.completedAt'))}</span><span class="bq-kv__v">${escapeHtml(new Date(queue.completedAt).toLocaleString())}</span></div>` : ''}
${queue.scheduleMode === 'cron' && queue.nextRunAt && !pres.sublabel ? `<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.nextRunAt'))}</span><span class="bq-kv__v">${escapeHtml(new Date(queue.nextRunAt).toLocaleString())}</span></div>` : ''}
${queue.lastScheduleTriggerAt ? `<div class="bq-kv"><span class="bq-kv__k">${escapeHtml(_t('batchQueueDetailModal.lastScheduleTriggerAt'))}</span><span class="bq-kv__v">${escapeHtml(new Date(queue.lastScheduleTriggerAt).toLocaleString())}</span></div>` : ''}
</div>
</details>
</div>
<div class="batch-queue-tasks-list">
<h4>` + _t('batchQueueDetailModal.taskList') + `</h4>
${queue.tasks.map((task, index) => {
const taskStatus = taskStatusMap[task.status] || { text: task.status, class: 'batch-task-status-unknown' };
const canEdit = queue.status === 'pending' && task.status === 'pending';
const taskMessageEscaped = escapeHtml(task.message).replace(/'/g, "&#39;").replace(/"/g, "&quot;").replace(/\n/g, "\\n");
return `
<div class="batch-task-item ${task.status === 'running' ? 'batch-task-item-active' : ''}" data-queue-id="${queue.id}" data-task-id="${task.id}" data-task-message="${taskMessageEscaped}">
<div class="batch-task-header">
<span class="batch-task-index">#${index + 1}</span>
<span class="batch-task-status ${taskStatus.class}">${taskStatus.text}</span>
<span class="batch-task-message" title="${escapeHtml(task.message)}">${escapeHtml(task.message)}</span>
${canEdit ? `<button class="btn-secondary btn-small batch-task-edit-btn" onclick="editBatchTaskFromElement(this); event.stopPropagation();">` + _t('common.edit') + `</button>` : ''}
${canEdit ? `<button class="btn-secondary btn-small btn-danger batch-task-delete-btn" onclick="deleteBatchTaskFromElement(this); event.stopPropagation();">` + _t('common.delete') + `</button>` : ''}
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewBatchTaskConversation('${task.conversationId}'); event.stopPropagation();">` + _t('tasks.viewConversation') + `</button>` : ''}
</div>
${task.startedAt ? `<div class="batch-task-time">` + _t('batchQueueDetailModal.startLabel') + `: ${new Date(task.startedAt).toLocaleString()}</div>` : ''}
${task.completedAt ? `<div class="batch-task-time">` + _t('batchQueueDetailModal.completeLabel') + `: ${new Date(task.completedAt).toLocaleString()}</div>` : ''}
${task.error ? `<div class="batch-task-error">` + _t('batchQueueDetailModal.errorLabel') + `: ${escapeHtml(task.error)}</div>` : ''}
${task.result ? `<div class="batch-task-result">` + _t('batchQueueDetailModal.resultLabel') + `: ${escapeHtml(task.result.substring(0, 200))}${task.result.length > 200 ? '...' : ''}</div>` : ''}
</div>
`;
}).join('')}
</div>
`;
// 恢复滚动位置
if (savedModalBodyScrollTop > 0 && modalBody) {
modalBody.scrollTop = savedModalBodyScrollTop;
}
const newTasksList = content.querySelector('.batch-queue-tasks-list');
if (savedTasksListScrollTop > 0 && newTasksList) {
newTasksList.scrollTop = savedTasksListScrollTop;
}
modal.style.display = 'block';
// 如果队列正在运行,自动刷新
if (queue.status === 'running') {
startBatchQueueRefresh(queueId);
}
} catch (error) {
console.error('获取队列详情失败:', error);
alert(_t('tasks.getQueueDetailFailed') + ': ' + error.message);
}
}
// 开始批量任务队列
async function startBatchQueue() {
const queueId = batchQueuesState.currentQueueId;
if (!queueId) return;
try {
const response = await apiFetch(`/api/batch-tasks/${queueId}/start`, {
method: 'POST',
});
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || _t('tasks.startBatchQueueFailed'));
}
// 刷新详情
showBatchQueueDetail(queueId);
refreshBatchQueues();
} catch (error) {
console.error('启动批量任务失败:', error);
alert(_t('tasks.startBatchQueueFailed') + ': ' + error.message);
}
}
// 暂停批量任务队列
async function pauseBatchQueue() {
const queueId = batchQueuesState.currentQueueId;
if (!queueId) return;
if (!confirm(_t('tasks.pauseQueueConfirm'))) {
return;
}
try {
const response = await apiFetch(`/api/batch-tasks/${queueId}/pause`, {
method: 'POST',
});
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || _t('tasks.pauseQueueFailed'));
}
// 刷新详情
showBatchQueueDetail(queueId);
refreshBatchQueues();
} catch (error) {
console.error('暂停批量任务失败:', error);
alert(_t('tasks.pauseQueueFailed') + ': ' + error.message);
}
}
// 删除批量任务队列(从详情模态框)
async function deleteBatchQueue() {
const queueId = batchQueuesState.currentQueueId;
if (!queueId) return;
if (!confirm(_t('tasks.deleteQueueConfirm'))) {
return;
}
try {
const response = await apiFetch(`/api/batch-tasks/${queueId}`, {
method: 'DELETE',
});
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || _t('tasks.deleteQueueFailed'));
}
closeBatchQueueDetailModal();
refreshBatchQueues();
} catch (error) {
console.error('删除批量任务队列失败:', error);
alert(_t('tasks.deleteQueueFailed') + ': ' + error.message);
}
}
// 从列表删除批量任务队列
async function deleteBatchQueueFromList(queueId) {
if (!queueId) return;
if (!confirm(_t('tasks.deleteQueueConfirm'))) {
return;
}
try {
const response = await apiFetch(`/api/batch-tasks/${queueId}`, {
method: 'DELETE',
});
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || _t('tasks.deleteQueueFailed'));
}
// 如果当前正在查看这个队列的详情,关闭详情模态框
if (batchQueuesState.currentQueueId === queueId) {
closeBatchQueueDetailModal();
}
// 刷新队列列表
refreshBatchQueues();
} catch (error) {
console.error('删除批量任务队列失败:', error);
alert(_t('tasks.deleteQueueFailed') + ': ' + error.message);
}
}
// 关闭批量任务队列详情模态框
function closeBatchQueueDetailModal() {
const modal = document.getElementById('batch-queue-detail-modal');
if (modal) {
modal.style.display = 'none';
}
batchQueuesState.currentQueueId = null;
stopBatchQueueRefresh();
}
// 开始批量队列刷新
function startBatchQueueRefresh(queueId) {
if (batchQueuesState.refreshInterval) {
clearInterval(batchQueuesState.refreshInterval);
}
batchQueuesState.refreshInterval = setInterval(() => {
if (batchQueuesState.currentQueueId === queueId) {
showBatchQueueDetail(queueId);
refreshBatchQueues();
} else {
stopBatchQueueRefresh();
}
}, 3000); // 每3秒刷新一次
}
// 停止批量队列刷新
function stopBatchQueueRefresh() {
if (batchQueuesState.refreshInterval) {
clearInterval(batchQueuesState.refreshInterval);
batchQueuesState.refreshInterval = null;
}
}
// 刷新批量任务队列列表
async function refreshBatchQueues() {
await loadBatchQueues(batchQueuesState.currentPage);
}
// 查看批量任务的对话
function viewBatchTaskConversation(conversationId) {
if (!conversationId) return;
// 关闭批量任务详情模态框
closeBatchQueueDetailModal();
// 直接使用URL hash跳转,让router处理页面切换和对话加载
// 这样更可靠,因为router会确保页面切换完成后再加载对话
window.location.hash = `chat?conversation=${conversationId}`;
}
// 编辑批量任务的状态
const editBatchTaskState = {
queueId: null,
taskId: null
};
// 从元素获取任务信息并打开编辑模态框
function editBatchTaskFromElement(button) {
const taskItem = button.closest('.batch-task-item');
if (!taskItem) {
console.error('无法找到任务项元素');
return;
}
const queueId = taskItem.getAttribute('data-queue-id');
const taskId = taskItem.getAttribute('data-task-id');
const taskMessage = taskItem.getAttribute('data-task-message');
if (!queueId || !taskId) {
console.error('任务信息不完整');
return;
}
// 解码HTML实体
const decodedMessage = taskMessage
.replace(/&#39;/g, "'")
.replace(/&quot;/g, '"')
.replace(/\\n/g, '\n');
editBatchTask(queueId, taskId, decodedMessage);
}
// 打开编辑批量任务模态框
function editBatchTask(queueId, taskId, currentMessage) {
editBatchTaskState.queueId = queueId;
editBatchTaskState.taskId = taskId;
const modal = document.getElementById('edit-batch-task-modal');
const messageInput = document.getElementById('edit-task-message');
if (!modal || !messageInput) {
console.error('编辑任务模态框元素不存在');
return;
}
messageInput.value = currentMessage;
modal.style.display = 'block';
// 聚焦到输入框
setTimeout(() => {
messageInput.focus();
messageInput.select();
}, 100);
// 添加ESC键监听
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
closeEditBatchTaskModal();
document.removeEventListener('keydown', handleKeyDown);
}
};
document.addEventListener('keydown', handleKeyDown);
// 添加Enter+Ctrl/Cmd保存功能
const handleKeyPress = (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
saveBatchTask();
document.removeEventListener('keydown', handleKeyPress);
}
};
messageInput.addEventListener('keydown', handleKeyPress);
}
// 关闭编辑批量任务模态框
function closeEditBatchTaskModal() {
const modal = document.getElementById('edit-batch-task-modal');
if (modal) {
modal.style.display = 'none';
}
editBatchTaskState.queueId = null;
editBatchTaskState.taskId = null;
}
// 保存批量任务
async function saveBatchTask() {
const queueId = editBatchTaskState.queueId;
const taskId = editBatchTaskState.taskId;
const messageInput = document.getElementById('edit-task-message');
if (!queueId || !taskId) {
alert(_t('tasks.taskIncomplete'));
return;
}
if (!messageInput) {
alert(_t('tasks.cannotGetTaskMessageInput'));
return;
}
const message = messageInput.value.trim();
if (!message) {
alert(_t('tasks.taskMessageRequired'));
return;
}
try {
const response = await apiFetch(`/api/batch-tasks/${queueId}/tasks/${taskId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: message }),
});
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || _t('tasks.updateTaskFailed'));
}
// 关闭编辑模态框
closeEditBatchTaskModal();
// 刷新队列详情
if (batchQueuesState.currentQueueId === queueId) {
showBatchQueueDetail(queueId);
}
// 刷新队列列表
refreshBatchQueues();
} catch (error) {
console.error('保存任务失败:', error);
alert(_t('tasks.saveTaskFailed') + ': ' + error.message);
}
}
// 显示添加批量任务模态框
function showAddBatchTaskModal() {
const queueId = batchQueuesState.currentQueueId;
if (!queueId) {
alert(_t('tasks.queueInfoMissing'));
return;
}
const modal = document.getElementById('add-batch-task-modal');
const messageInput = document.getElementById('add-task-message');
if (!modal || !messageInput) {
console.error('添加任务模态框元素不存在');
return;
}
messageInput.value = '';
modal.style.display = 'block';
// 聚焦到输入框
setTimeout(() => {
messageInput.focus();
}, 100);
// 添加ESC键监听
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
closeAddBatchTaskModal();
document.removeEventListener('keydown', handleKeyDown);
}
};
document.addEventListener('keydown', handleKeyDown);
// 添加Enter+Ctrl/Cmd保存功能
const handleKeyPress = (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
saveAddBatchTask();
messageInput.removeEventListener('keydown', handleKeyPress);
}
};
messageInput.addEventListener('keydown', handleKeyPress);
}
// 关闭添加批量任务模态框
function closeAddBatchTaskModal() {
const modal = document.getElementById('add-batch-task-modal');
const messageInput = document.getElementById('add-task-message');
if (modal) {
modal.style.display = 'none';
}
if (messageInput) {
messageInput.value = '';
}
}
// 保存添加的批量任务
async function saveAddBatchTask() {
const queueId = batchQueuesState.currentQueueId;
const messageInput = document.getElementById('add-task-message');
if (!queueId) {
alert(_t('tasks.queueInfoMissing'));
return;
}
if (!messageInput) {
alert(_t('tasks.cannotGetTaskMessageInput'));
return;
}
const message = messageInput.value.trim();
if (!message) {
alert(_t('tasks.taskMessageRequired'));
return;
}
try {
const response = await apiFetch(`/api/batch-tasks/${queueId}/tasks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: message }),
});
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || _t('tasks.addTaskFailed'));
}
// 关闭添加任务模态框
closeAddBatchTaskModal();
// 刷新队列详情
if (batchQueuesState.currentQueueId === queueId) {
showBatchQueueDetail(queueId);
}
// 刷新队列列表
refreshBatchQueues();
} catch (error) {
console.error('添加任务失败:', error);
alert(_t('tasks.addTaskFailed') + ': ' + error.message);
}
}
// 从元素获取任务信息并删除任务
function deleteBatchTaskFromElement(button) {
const taskItem = button.closest('.batch-task-item');
if (!taskItem) {
console.error('无法找到任务项元素');
return;
}
const queueId = taskItem.getAttribute('data-queue-id');
const taskId = taskItem.getAttribute('data-task-id');
const taskMessage = taskItem.getAttribute('data-task-message');
if (!queueId || !taskId) {
console.error('任务信息不完整');
return;
}
// 解码HTML实体以显示消息
const decodedMessage = taskMessage
.replace(/&#39;/g, "'")
.replace(/&quot;/g, '"')
.replace(/\\n/g, '\n');
// 截断长消息用于确认对话框
const displayMessage = decodedMessage.length > 50
? decodedMessage.substring(0, 50) + '...'
: decodedMessage;
if (!confirm(_t('tasks.confirmDeleteTask', { message: displayMessage }))) {
return;
}
deleteBatchTask(queueId, taskId);
}
// 删除批量任务
async function deleteBatchTask(queueId, taskId) {
if (!queueId || !taskId) {
alert(_t('tasks.taskIncomplete'));
return;
}
try {
const response = await apiFetch(`/api/batch-tasks/${queueId}/tasks/${taskId}`, {
method: 'DELETE',
});
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || _t('tasks.deleteTaskFailed'));
}
// 刷新队列详情
if (batchQueuesState.currentQueueId === queueId) {
showBatchQueueDetail(queueId);
}
// 刷新队列列表
refreshBatchQueues();
} catch (error) {
console.error('删除任务失败:', error);
alert(_t('tasks.deleteTaskFailed') + ': ' + error.message);
}
}
async function updateBatchQueueScheduleEnabled(enabled) {
const queueId = batchQueuesState.currentQueueId;
if (!queueId) return;
try {
const response = await apiFetch(`/api/batch-tasks/${queueId}/schedule-enabled`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scheduleEnabled: enabled }),
});
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || _t('batchQueueDetailModal.scheduleToggleFailed'));
}
showBatchQueueDetail(queueId);
refreshBatchQueues();
} catch (e) {
console.error(e);
alert(_t('batchQueueDetailModal.scheduleToggleFailed') + ': ' + e.message);
showBatchQueueDetail(queueId);
}
}
// 导出函数
window.showBatchImportModal = showBatchImportModal;
window.closeBatchImportModal = closeBatchImportModal;
window.createBatchQueue = createBatchQueue;
window.showBatchQueueDetail = showBatchQueueDetail;
window.startBatchQueue = startBatchQueue;
window.pauseBatchQueue = pauseBatchQueue;
window.deleteBatchQueue = deleteBatchQueue;
window.closeBatchQueueDetailModal = closeBatchQueueDetailModal;
window.refreshBatchQueues = refreshBatchQueues;
window.viewBatchTaskConversation = viewBatchTaskConversation;
window.editBatchTask = editBatchTask;
window.editBatchTaskFromElement = editBatchTaskFromElement;
window.closeEditBatchTaskModal = closeEditBatchTaskModal;
window.saveBatchTask = saveBatchTask;
window.filterBatchQueues = filterBatchQueues;
window.goBatchQueuesPage = goBatchQueuesPage;
window.changeBatchQueuesPageSize = changeBatchQueuesPageSize;
window.showAddBatchTaskModal = showAddBatchTaskModal;
window.closeAddBatchTaskModal = closeAddBatchTaskModal;
window.saveAddBatchTask = saveAddBatchTask;
window.deleteBatchTaskFromElement = deleteBatchTaskFromElement;
window.deleteBatchQueueFromList = deleteBatchQueueFromList;
window.handleBatchScheduleModeChange = handleBatchScheduleModeChange;
window.updateBatchQueueScheduleEnabled = updateBatchQueueScheduleEnabled;
// 语言切换后,列表/分页/详情弹窗由 JS 渲染的文案需用当前语言重绘(applyTranslations 不会处理 innerHTML 内容)
document.addEventListener('languagechange', function () {
try {
const tasksPage = document.getElementById('page-tasks');
if (!tasksPage || !tasksPage.classList.contains('active')) {
return;
}
if (document.getElementById('batch-queues-list')) {
renderBatchQueues();
}
const detailModal = document.getElementById('batch-queue-detail-modal');
if (
detailModal &&
detailModal.style.display === 'block' &&
batchQueuesState.currentQueueId
) {
showBatchQueueDetail(batchQueuesState.currentQueueId);
}
} catch (e) {
console.warn('languagechange tasks refresh failed', e);
}
});