(() => { const state = { token: '', queue: '', base: location.origin, timer: null, connected: false, loading: false, selectedTask: null, actionMode: '', selectedSchedule: null, selectedWorkflow: null, selectedSubscription: null }; const $ = (id) => document.getElementById(id); const text = (id, value) => { const el = $(id); if (el) el.textContent = value; }; const pageNames = { overview: '总览', tasks: '任务执行', schedules: '定时计划', workflows: '流程实例', subscriptions: '结果订阅' }; const sessionQueue = sessionStorage.getItem('orbit.console.queue') || ''; $('api-url').value = location.origin; $('queue-id').value = sessionQueue; function showToast(message) { const toast = $('toast'); toast.textContent = message; toast.classList.add('show'); clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('show'), 2800); } function formatTime(value) { if (!value) return '—'; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); return new Intl.DateTimeFormat('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(date); } function shortID(id) { return id && id.length > 14 ? `${id.slice(0, 8)}…${id.slice(-4)}` : (id || '—'); } function stateClass(value) { return String(value || '').toLowerCase(); } function extractItems(payload) { return payload?.data?.items || payload?.items || []; } function extractData(payload) { return payload?.data ?? payload ?? {}; } function currentView() { const raw = location.hash.replace(/^#\/?/, '').split('/')[0]; return pageNames[raw] ? raw : 'overview'; } function setView(view, updateHash = true) { const next = pageNames[view] ? view : 'overview'; document.querySelectorAll('.page-view').forEach(page => page.classList.toggle('active', page.dataset.page === next)); document.querySelectorAll('.nav-item').forEach(item => item.classList.toggle('active', item.dataset.view === next)); text('page-title', pageNames[next]); if (updateHash && location.hash !== `#/${next}`) location.hash = `/${next}`; window.scrollTo({ top: 0, behavior: 'smooth' }); } async function request(path, options = {}) { const headers = { Accept: 'application/json', ...(options.headers || {}) }; if (state.token) headers.Authorization = `Bearer ${state.token}`; const response = await fetch(`${state.base.replace(/\/$/, '')}${path}`, { ...options, headers }); const raw = await response.text(); let body = {}; try { body = raw ? JSON.parse(raw) : {}; } catch { body = { message: raw }; } if (!response.ok) throw new Error(body?.error?.message || body?.message || `HTTP ${response.status}`); return body; } function setConnection(connected, detail = '') { state.connected = connected; const authenticated = connected && Boolean(state.token && state.queue); $('connection-state').textContent = authenticated ? '已连接' : (connected ? '服务在线' : '未连接'); $('connection-state').classList.toggle('ready', connected); $('side-status').textContent = authenticated ? '环境已连接' : (connected ? '服务在线' : '等待连接'); $('side-endpoint').textContent = connected ? state.base : 'API 未连接'; $('side-progress').style.width = connected ? '100%' : '0%'; document.querySelector('.status-dot').classList.toggle('ready', connected); document.querySelector('.live-chip').classList.toggle('ready', connected); $('live-label').textContent = connected ? '环境在线' : '本地环境'; if (detail) showToast(detail); } async function loadHealth() { try { const [live, version] = await Promise.all([request('/health/live'), request('/version')]); text('metric-health', live?.status === 'alive' ? '在线' : '异常'); text('metric-health-sub', `Go ${version?.go_version || '—'} · API v${version?.http_protocol_version || '—'}`); text('side-status', '环境已连接'); return version; } catch (error) { text('metric-health', '离线'); text('metric-health-sub', error.message); setConnection(false); throw error; } } function renderTasks(items) { text('task-count', `${items.length} 项`); text('metric-tasks', String(items.length)); const counts = items.reduce((acc, item) => { const key = stateClass(item.state); acc[key] = (acc[key] || 0) + 1; return acc; }, {}); const total = Math.max(items.length, 1); const width = (name) => `${(counts[name] || 0) / total * 100}%`; $('task-bar-succeeded').style.width = width('succeeded'); $('task-bar-running').style.width = width('running'); $('task-bar-waiting').style.width = `${((counts.waiting || 0) + (counts.pending || 0)) / total * 100}%`; $('task-bar-other').style.width = `${((counts.failed || 0) + (counts.cancelled || 0) + (counts.needs_review || 0)) / total * 100}%`; text('metric-tasks-sub', `${counts.succeeded || 0} 成功 · ${counts.running || 0} 执行中`); const body = $('task-table'); if (!items.length) { body.innerHTML = '◇当前队列没有任务提交任务后,这里会自动显示最新状态'; return; } body.innerHTML = items.slice(0, 8).map(item => `
${escapeHTML(shortID(item.id))}${escapeHTML(item.business_key || '无业务键')}
${escapeHTML(item.handler || '—')}
${escapeHTML(item.handler_version || '')}${escapeHTML(item.state || 'UNKNOWN')}${escapeHTML(item.revision || '—')}${escapeHTML(formatTime(item.created_at))}`).join(''); body.querySelectorAll('.task-row').forEach(row => { row.addEventListener('click', () => openTaskDetail(row.dataset.taskId)); row.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openTaskDetail(row.dataset.taskId); } }); }); } function renderSchedules(items) { text('metric-schedules', String(items.length)); const el = $('schedule-list'); if (!items.length) { el.innerHTML = '
◷
当前没有定时计划需要 schedules:read 权限
'; return; } el.innerHTML = items.map(item => `
◷
${escapeHTML(item.name || shortID(item.id))}${escapeHTML(item.queue_id ? shortID(item.queue_id) : '计划')}
${escapeHTML(item.paused ? '已暂停' : '运行中')}
`).join(''); el.querySelectorAll('.schedule-row').forEach(row => { row.addEventListener('click', () => openScheduleDetail(row.dataset.scheduleId)); row.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openScheduleDetail(row.dataset.scheduleId); } }); }); } function renderWorkflows(items) { text('metric-workflows', String(items.length)); text('metric-workflows-sub', items.length ? '最近实例' : '当前没有实例'); const el = $('workflow-list'); if (!items.length) { el.innerHTML = '
⌘
当前没有流程实例需要 workflows:read 权限
'; return; } el.innerHTML = items.map(item => `
⌘
${escapeHTML(item.business_key || shortID(item.id))}${escapeHTML(item.state || '—')} · ${escapeHTML(item.version || '—')}
${escapeHTML(formatTime(item.created_at))}
`).join(''); el.querySelectorAll('.workflow-row').forEach(row => { row.addEventListener('click', () => openWorkflowDetail(row.dataset.workflowId)); row.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openWorkflowDetail(row.dataset.workflowId); } }); }); } function renderSubscriptions(items) { text('metric-subscriptions', String(items.length)); const active = items.filter(item => item.enabled !== false).length; $('subscription-meter-fill').style.width = `${items.length ? active / items.length * 100 : 0}%`; text('metric-subscriptions-sub', items.length ? `${active} 个已启用 · 交付自动恢复` : '当前没有结果订阅'); const el = $('subscription-list'); if (!items.length) { el.innerHTML = '
⌁
当前没有结果订阅需要 events:read 权限
'; return; } el.innerHTML = items.map(item => `
⌁
${escapeHTML(item.name || shortID(item.id))}待处理 ${escapeHTML(String(item.pending_count ?? 0))} · 上限 ${escapeHTML(String(item.max_pending ?? '—'))}
${item.enabled === false ? '已暂停' : '已启用'}
`).join(''); el.querySelectorAll('.subscription-row').forEach(row => { row.addEventListener('click', () => openSubscriptionDetail(row.dataset.subscriptionId)); row.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openSubscriptionDetail(row.dataset.subscriptionId); } }); }); } function escapeHTML(value) { return String(value ?? '').replace(/[&<>'"]/g, char => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char])); } function formatJSON(value) { if (value === undefined || value === null || value === '') return ''; try { return JSON.stringify(typeof value === 'string' ? JSON.parse(value) : value, null, 2); } catch { return String(value); } } function operationKey(prefix) { const id = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`; return `console-${prefix}-${id}`.slice(0, 128); } function terminalState(value) { return ['SUCCEEDED', 'FAILED', 'CANCELLED'].includes(value); } function showTaskDrawer() { $('task-drawer-backdrop').hidden = false; document.body.classList.add('drawer-open'); } function closeTaskDrawer() { $('task-drawer-backdrop').hidden = true; document.body.classList.remove('drawer-open'); closeActionForm(); } function renderDetailList(id, items, emptyText, renderItem) { const el = $(id); if (!items.length) { el.innerHTML = `
${escapeHTML(emptyText)}
`; return; } el.innerHTML = items.map(renderItem).join(''); } function renderTaskDetail(task) { state.selectedTask = task; text('detail-title', task.handler || '任务详情'); text('detail-id', task.id || '—'); text('detail-state', task.state || 'UNKNOWN'); $('detail-state').className = `state-pill ${stateClass(task.state)}`; text('detail-updated', `最后更新 ${formatTime(task.updated_at)}`); text('detail-handler', `${task.handler || '—'} · ${task.handler_version || '—'}`); text('detail-revision', task.revision || '—'); text('detail-attempt-count', task.attempt_count ?? '0'); text('detail-effect', task.effect_uncertain ? '待核查' : '已确认'); text('detail-created', formatTime(task.created_at)); text('detail-deadline', formatTime(task.deadline_at)); const resultBlock = $('detail-result-block'); resultBlock.hidden = !task.result; if (task.result) $('detail-result').textContent = formatJSON(task.result); const canCancel = !terminalState(task.state) && !task.cancel_requested_at; const canRetry = (task.state === 'FAILED' || task.state === 'CANCELLED') && !task.effect_uncertain; $('detail-actions').hidden = !(canCancel || canRetry); $('task-cancel-button').hidden = !canCancel; $('task-retry-button').hidden = !canRetry; text('detail-action-note', task.state === 'NEEDS_REVIEW' ? '此任务需要先完成带证据的人工核查,确认旧执行已经结清后再决定最终状态。' : '写操作需要对应的管理权限;服务端会再次校验版本、状态、幂等键和队列范围。'); } function renderAttempts(items, error) { text('detail-attempts-count', error ? '权限不足' : `${items.length} 次`); renderDetailList('detail-attempts', items, error ? '当前凭据没有 tasks:read 历史权限。' : '还没有执行尝试。', item => `
第 ${escapeHTML(item.epoch)} 次 · ${escapeHTML(item.state || 'UNKNOWN')}${escapeHTML(item.error_code || '无错误')} · ${escapeHTML(formatTime(item.created_at))}
${item.retryable ? '可重试' : '不可重试'}
`); } function renderOperations(items, error) { text('detail-operations-count', error ? '权限不足' : `${items.length} 条`); renderDetailList('detail-operations', items, error ? '当前凭据没有 tasks:audit:read 权限。' : '还没有操作审计。', item => `
${escapeHTML(item.action || '操作')}${escapeHTML(item.actor_id || '—')} · ${escapeHTML(formatTime(item.created_at))}
${escapeHTML(shortID(item.id))}
`); } async function openTaskDetail(id) { if (!id) return; if (!state.token) { showToast('请先连接具备 tasks:read 权限的凭据'); return; } showTaskDrawer(); text('detail-title', '正在读取任务…'); text('detail-id', shortID(id)); text('detail-state', '读取中'); $('detail-actions').hidden = true; $('detail-attempts').innerHTML = '
正在读取尝试历史…
'; $('detail-operations').innerHTML = '
正在读取操作审计…
'; const encoded = encodeURIComponent(id); const [taskResult, attemptsResult, operationsResult] = await Promise.allSettled([request(`/v1/tasks/${encoded}`), request(`/v1/tasks/${encoded}/attempts?limit=20`), request(`/v1/tasks/${encoded}/operations?limit=20`)]); if (taskResult.status !== 'fulfilled') { text('detail-title', '任务读取失败'); text('detail-action-note', taskResult.reason?.message || '无法读取任务详情'); return; } const task = extractData(taskResult.value); renderTaskDetail(task); renderAttempts(attemptsResult.status === 'fulfilled' ? extractItems(attemptsResult.value) : [], attemptsResult.status === 'rejected'); renderOperations(operationsResult.status === 'fulfilled' ? extractItems(operationsResult.value) : [], operationsResult.status === 'rejected'); } function closeActionForm() { state.actionMode = ''; $('task-action-form').hidden = true; $('action-error').textContent = ''; } function openActionForm(mode) { if (!state.selectedTask) return; state.actionMode = mode; $('task-action-form').hidden = false; $('action-error').textContent = ''; $('action-reason').value = ''; $('action-evidence').value = `evidence:console-${Date.now()}`; $('action-safe').checked = false; const retry = mode === 'retry'; text('action-kicker', retry ? 'TASK RECOVERY' : 'TASK CONTROL'); text('action-title', retry ? '安全重试任务' : '取消任务'); text('action-description', retry ? '只有确认旧执行不会继续产生外部效果时,才能创建新的关联任务。' : '取消会记录持久化取消意图;运行中的处理器仍需上报结果或等待恢复。'); $('action-evidence-wrap').hidden = !retry; $('action-timeout-wrap').hidden = !retry; $('action-safe-wrap').hidden = !retry; $('action-submit').textContent = retry ? '创建重试任务 →' : '提交取消 →'; $('action-reason').focus(); } async function submitAction(event) { event.preventDefault(); if (!state.selectedTask || !state.actionMode) return; const reason = $('action-reason').value.trim(); if (!reason) { $('action-error').textContent = '请填写操作原因。'; return; } const task = state.selectedTask; const retry = state.actionMode === 'retry'; const body = { expected_revision: String(task.revision), reason }; const path = `/v1/tasks/${encodeURIComponent(task.id)}:${retry ? 'retry' : 'cancel'}`; if (retry) { const timeout = Number($('action-timeout').value); if (!$('action-safe').checked) { $('action-error').textContent = '请先确认安全重试声明。'; return; } if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 2592000000) { $('action-error').textContent = '任务总时限必须是 1 到 2592000000 毫秒。'; return; } const evidence = $('action-evidence').value.trim(); if (!evidence) { $('action-error').textContent = '请填写证据引用。'; return; } Object.assign(body, { evidence_ref: evidence, safe_to_retry: true, task_timeout_ms: timeout }); } const button = $('action-submit'); button.disabled = true; $('action-error').textContent = ''; try { const response = await request(path, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': operationKey(retry ? 'retry' : 'cancel') }, body: JSON.stringify(body) }); showToast(retry ? '已创建关联重试任务' : '取消意图已提交'); closeActionForm(); await refresh(); await openTaskDetail(task.id); if (response?.data?.task?.id && retry) showToast(`重试任务:${shortID(response.data.task.id)}`); } catch (error) { $('action-error').textContent = error.message || '操作失败'; } finally { button.disabled = false; } } function showScheduleDrawer() { $('schedule-drawer-backdrop').hidden = false; document.body.classList.add('drawer-open'); } function closeScheduleDrawer() { $('schedule-drawer-backdrop').hidden = true; document.body.classList.remove('drawer-open'); closeScheduleAction(); } function closeScheduleAction() { $('schedule-action-form').hidden = true; $('schedule-action-error').textContent = ''; } function scheduleRuleLabel(rule) { if (!rule) return '—'; return rule.kind === 'CRON' ? `Cron · ${rule.cron || '—'}` : `固定间隔 · ${rule.interval_ms || '—'} ms`; } function renderScheduleOccurrences(items, error) { text('schedule-occurrences-count', error ? '权限不足' : `${items.length} 条`); renderDetailList('schedule-occurrences', items, error ? '当前凭据没有 schedules:read 触发历史权限。' : '还没有触发记录。', item => `
${escapeHTML(item.decision || 'UNKNOWN')} · ${escapeHTML(formatTime(item.scheduled_at))}${escapeHTML(item.reason || '—')} · 版本 ${escapeHTML(item.schedule_version || '—')}
${item.task_id ? escapeHTML(shortID(item.task_id)) : '无任务'}
`); } function renderScheduleDetail(schedule) { state.selectedSchedule = schedule; text('schedule-detail-title', schedule.name || '定时计划'); text('schedule-detail-id', schedule.id || '—'); text('schedule-detail-state', schedule.paused ? '已暂停' : '运行中'); $('schedule-detail-state').className = `state-pill ${schedule.paused ? 'waiting' : 'succeeded'}`; text('schedule-detail-updated', `最后更新 ${formatTime(schedule.updated_at)}`); text('schedule-detail-name', schedule.name || '—'); text('schedule-detail-queue', shortID(schedule.queue_id)); text('schedule-detail-revision', schedule.revision || '—'); text('schedule-detail-version', schedule.current_version || '—'); text('schedule-detail-next', formatTime(schedule.next_fire_at)); const definition = schedule.current?.definition || {}; const rule = definition.rule || {}; text('schedule-detail-timezone', rule.timezone || '—'); text('schedule-detail-rule-kind', rule.kind || '—'); $('schedule-detail-rule').innerHTML = `${escapeHTML(scheduleRuleLabel(rule))}漏跑 ${escapeHTML(definition.policy?.misfire || '—')} · 重叠 ${escapeHTML(definition.policy?.overlap || '—')} · 补偿上限 ${escapeHTML(definition.policy?.catchup_limit || '—')}`; $('schedule-actions').hidden = false; text('schedule-control-button', schedule.paused ? '恢复计划' : '暂停计划'); text('schedule-action-note', schedule.paused ? '恢复会按冻结的漏跑策略继续处理暂停期间的触发点。' : '暂停只停止未来触发,已经创建的任务继续执行。'); } async function openScheduleDetail(id) { if (!id) return; if (!state.token) { showToast('请先连接具备 schedules:read 权限的凭据'); return; } showScheduleDrawer(); text('schedule-detail-title', '正在读取计划…'); text('schedule-detail-id', shortID(id)); $('schedule-actions').hidden = true; $('schedule-occurrences').innerHTML = '
正在读取触发记录…
'; const encoded = encodeURIComponent(id); const [scheduleResult, occurrencesResult] = await Promise.allSettled([request(`/v1/schedules/${encoded}`), request(`/v1/schedules/${encoded}/occurrences?limit=20`)]); if (scheduleResult.status !== 'fulfilled') { text('schedule-detail-title', '计划读取失败'); text('schedule-action-note', scheduleResult.reason?.message || '无法读取计划详情'); return; } renderScheduleDetail(extractData(scheduleResult.value)); renderScheduleOccurrences(occurrencesResult.status === 'fulfilled' ? extractItems(occurrencesResult.value) : [], occurrencesResult.status === 'rejected'); } function openScheduleAction() { if (!state.selectedSchedule) return; const paused = state.selectedSchedule.paused; $('schedule-action-form').hidden = false; $('schedule-action-error').textContent = ''; $('schedule-action-reason').value = ''; text('schedule-action-title', paused ? '恢复计划' : '暂停计划'); text('schedule-action-description', paused ? '恢复后会按计划冻结的漏跑和重叠策略继续处理。' : '暂停只停止未来触发,不取消已经创建的任务。'); $('schedule-action-reason').focus(); } async function submitScheduleAction(event) { event.preventDefault(); if (!state.selectedSchedule) return; const reason = $('schedule-action-reason').value.trim(); if (!reason) { $('schedule-action-error').textContent = '请填写操作原因。'; return; } const paused = state.selectedSchedule.paused; const button = $('schedule-action-submit'); button.disabled = true; $('schedule-action-error').textContent = ''; try { await request(`/v1/schedules/${encodeURIComponent(state.selectedSchedule.id)}:${paused ? 'resume' : 'pause'}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': operationKey(paused ? 'schedule-resume' : 'schedule-pause') }, body: JSON.stringify({ expected_revision: String(state.selectedSchedule.revision), reason }) }); showToast(paused ? '计划已恢复' : '计划已暂停'); closeScheduleAction(); await refresh(); await openScheduleDetail(state.selectedSchedule.id); } catch (error) { $('schedule-action-error').textContent = error.message || '计划控制失败'; } finally { button.disabled = false; } } function showWorkflowDrawer() { $('workflow-drawer-backdrop').hidden = false; document.body.classList.add('drawer-open'); } function closeWorkflowDrawer() { $('workflow-drawer-backdrop').hidden = true; document.body.classList.remove('drawer-open'); } function showSubscriptionDrawer() { $('subscription-drawer-backdrop').hidden = false; document.body.classList.add('drawer-open'); } function closeSubscriptionDrawer() { $('subscription-drawer-backdrop').hidden = true; document.body.classList.remove('drawer-open'); } function renderWorkflowDetail(workflow) { state.selectedWorkflow = workflow; text('workflow-detail-title', workflow.business_key || '流程实例'); text('workflow-detail-id', workflow.id || '—'); text('workflow-detail-state', workflow.state || 'UNKNOWN'); $('workflow-detail-state').className = `state-pill ${stateClass(workflow.state)}`; text('workflow-detail-updated', `最后更新 ${formatTime(workflow.updated_at)}`); text('workflow-detail-definition', workflow.definition_id || '—'); text('workflow-detail-version', workflow.version || '—'); text('workflow-detail-revision', workflow.revision || '—'); text('workflow-detail-control-revision', workflow.control_revision || '—'); text('workflow-detail-business-key', workflow.business_key || '—'); text('workflow-detail-deadline', formatTime(workflow.deadline_at)); const outputBlock = $('workflow-output-block'); const hasOutput = Object.prototype.hasOwnProperty.call(workflow, 'output'); outputBlock.hidden = !hasOutput; if (hasOutput) $('workflow-output').textContent = formatJSON(workflow.output) || 'null'; } function renderWorkflowSteps(payload, error) { const items = Array.isArray(payload?.steps) ? payload.steps : []; text('workflow-steps-count', error ? '权限不足' : `${items.length} 个`); renderDetailList('workflow-steps', items, error ? '当前凭据没有 workflows:read 步骤权限。' : '该实例还没有可展示的步骤。', item => { const link = item.task_id ? `任务 ${shortID(item.task_id)}` : (item.activation_id ? `激活 ${shortID(item.activation_id)}` : '未激活'); return `
${escapeHTML(item.key || item.id || '未命名步骤')} · ${escapeHTML(item.kind || 'STEP')}${escapeHTML(item.state || 'UNKNOWN')} · ${escapeHTML(link)} · 修订 ${escapeHTML(item.revision || '—')}
${item.activation_sequence ? `第 ${escapeHTML(item.activation_sequence)} 代` : '—'}
`; }); } function renderWorkflowEvents(payload, error) { const items = Array.isArray(payload?.items) ? payload.items : []; text('workflow-events-count', error ? '权限不足' : `${items.length} 条`); renderDetailList('workflow-events', items, error ? '当前凭据没有 workflows:audit:read 事件审计权限。' : '还没有保存的状态事件。', item => `
${escapeHTML(item.type || '状态事件')} · ${escapeHTML(item.state || '—')}${escapeHTML(item.state_scope || item.aggregate_kind || 'WORKFLOW')} ${item.step_key ? `· ${escapeHTML(item.step_key)}` : ''} · ${escapeHTML(formatTime(item.occurred_at))}
修订 ${escapeHTML(item.aggregate_revision || '—')}
`); } async function openWorkflowDetail(id) { if (!id) return; if (!state.token) { showToast('请先连接具备 workflows:read 权限的凭据'); return; } showWorkflowDrawer(); text('workflow-detail-title', '正在读取流程…'); text('workflow-detail-id', shortID(id)); text('workflow-detail-state', '读取中'); $('workflow-steps').innerHTML = '
正在读取步骤…
'; $('workflow-events').innerHTML = '
正在读取事件…
'; const encoded = encodeURIComponent(id); const [workflowResult, stepsResult, eventsResult] = await Promise.allSettled([request(`/v1/workflows/${encoded}`), request(`/v1/workflows/${encoded}/steps`), request(`/v1/workflows/${encoded}/events?limit=20`)]); if (workflowResult.status !== 'fulfilled') { text('workflow-detail-title', '流程读取失败'); text('workflow-detail-note', workflowResult.reason?.message || '无法读取流程详情'); renderWorkflowSteps({}, true); renderWorkflowEvents({}, true); return; } renderWorkflowDetail(extractData(workflowResult.value)); renderWorkflowSteps(stepsResult.status === 'fulfilled' ? extractData(stepsResult.value) : {}, stepsResult.status === 'rejected'); renderWorkflowEvents(eventsResult.status === 'fulfilled' ? extractData(eventsResult.value) : {}, eventsResult.status === 'rejected'); } function renderSubscriptionDetail(subscription) { state.selectedSubscription = subscription; text('subscription-detail-title', subscription.name || '结果订阅'); text('subscription-detail-id', subscription.id || '—'); text('subscription-detail-state', subscription.enabled === false ? '已暂停' : '已启用'); $('subscription-detail-state').className = `state-pill ${subscription.enabled === false ? 'waiting' : 'succeeded'}`; text('subscription-detail-updated', `最后更新 ${formatTime(subscription.updated_at)}`); text('subscription-detail-name', subscription.name || '—'); text('subscription-detail-queue', shortID(subscription.queue_id)); text('subscription-detail-revision', subscription.revision || '—'); text('subscription-detail-pending', subscription.pending_count ?? '0'); text('subscription-detail-max-pending', subscription.max_pending ?? '—'); text('subscription-detail-max-attempts', subscription.max_attempts ?? '—'); text('subscription-detail-cursor', subscription.cursor_sequence ?? '0'); text('subscription-detail-types', Array.isArray(subscription.types) && subscription.types.length ? subscription.types.join(', ') : '全部结果类型'); } function renderSubscriptionDeliveries(payload, error) { const items = Array.isArray(payload?.items) ? payload.items : []; text('subscription-deliveries-count', error ? '权限不足' : `${items.length} 条`); renderDetailList('subscription-deliveries', items, error ? '当前凭据没有 events:read 交付历史权限。' : '还没有交付记录。', item => `
${escapeHTML(item.state || 'UNKNOWN')} · 序号 ${escapeHTML(item.sequence || '—')}${escapeHTML(formatTime(item.created_at))} · 尝试 ${escapeHTML(item.attempts ?? 0)}/${escapeHTML(item.attempt_budget ?? '—')} · 可用 ${escapeHTML(formatTime(item.available_at))}
${escapeHTML(shortID(item.event_id))}
`); } async function openSubscriptionDetail(id) { if (!id) return; if (!state.token) { showToast('请先连接具备 events:read 权限的凭据'); return; } showSubscriptionDrawer(); text('subscription-detail-title', '正在读取订阅…'); text('subscription-detail-id', shortID(id)); $('subscription-deliveries').innerHTML = '
正在读取交付历史…
'; const encoded = encodeURIComponent(id); const [subscriptionResult, deliveriesResult] = await Promise.allSettled([request(`/v1/subscriptions/${encoded}`), request(`/v1/subscriptions/${encoded}/deliveries?limit=20`)]); if (subscriptionResult.status !== 'fulfilled') { text('subscription-detail-title', '订阅读取失败'); text('subscription-detail-note', subscriptionResult.reason?.message || '无法读取订阅详情'); renderSubscriptionDeliveries({}, true); return; } renderSubscriptionDetail(extractData(subscriptionResult.value)); renderSubscriptionDeliveries(deliveriesResult.status === 'fulfilled' ? extractData(deliveriesResult.value) : {}, deliveriesResult.status === 'rejected'); } async function refresh() { if (state.loading) return; state.loading = true; $('refresh-button').classList.add('spinning'); try { await loadHealth(); if (!state.token || !state.queue) { setConnection(true); text('last-updated', `服务在线 · ${formatTime(new Date())}`); return; } const tasks = await request(`/v1/tasks?queue_id=${encodeURIComponent(state.queue)}&limit=20`); renderTasks(extractItems(tasks)); const optional = await Promise.allSettled([ request(`/v1/schedules?queue_id=${encodeURIComponent(state.queue)}&limit=20`), request('/v1/workflows?limit=20'), request(`/v1/subscriptions?queue_id=${encodeURIComponent(state.queue)}&limit=20`), ]); if (optional[0].status === 'fulfilled') renderSchedules(extractItems(optional[0].value)); if (optional[1].status === 'fulfilled') renderWorkflows(extractItems(optional[1].value)); if (optional[2].status === 'fulfilled') renderSubscriptions(extractItems(optional[2].value)); setConnection(true); text('last-updated', `刚刚更新 · ${formatTime(new Date())}`); text('metric-health-sub', '数据库与 Schema 正常'); } catch (error) { setConnection(false); showToast(`连接失败:${error.message}`); text('last-updated', '刷新失败'); } finally { state.loading = false; $('refresh-button').classList.remove('spinning'); } } function connect() { state.base = $('api-url').value.trim().replace(/\/$/, '') || location.origin; state.token = $('token').value.trim(); state.queue = $('queue-id').value.trim(); sessionStorage.setItem('orbit.console.queue', state.queue); refresh(); } $('connect-button').addEventListener('click', connect); $('connect-hero').addEventListener('click', () => $('connection-panel').scrollIntoView({ behavior: 'smooth', block: 'center' })); $('refresh-button').addEventListener('click', refresh); $('close-task-drawer').addEventListener('click', closeTaskDrawer); $('task-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'task-drawer-backdrop') closeTaskDrawer(); }); $('action-cancel').addEventListener('click', closeActionForm); $('task-action-form').addEventListener('submit', submitAction); $('task-cancel-button').addEventListener('click', () => openActionForm('cancel')); $('task-retry-button').addEventListener('click', () => openActionForm('retry')); $('close-schedule-drawer').addEventListener('click', closeScheduleDrawer); $('schedule-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'schedule-drawer-backdrop') closeScheduleDrawer(); }); $('schedule-control-button').addEventListener('click', openScheduleAction); $('schedule-action-cancel').addEventListener('click', closeScheduleAction); $('schedule-action-form').addEventListener('submit', submitScheduleAction); $('close-workflow-drawer').addEventListener('click', closeWorkflowDrawer); $('workflow-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'workflow-drawer-backdrop') closeWorkflowDrawer(); }); $('close-subscription-drawer').addEventListener('click', closeSubscriptionDrawer); $('subscription-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'subscription-drawer-backdrop') closeSubscriptionDrawer(); }); document.addEventListener('keydown', event => { if (event.key === 'Escape') { if (!$('task-action-form').hidden) closeActionForm(); else if (!$('schedule-action-form').hidden) closeScheduleAction(); else if (!$('task-drawer-backdrop').hidden) closeTaskDrawer(); else if (!$('schedule-drawer-backdrop').hidden) closeScheduleDrawer(); else if (!$('workflow-drawer-backdrop').hidden) closeWorkflowDrawer(); else if (!$('subscription-drawer-backdrop').hidden) closeSubscriptionDrawer(); } }); $('reveal-token').addEventListener('click', () => { const field = $('token'); field.type = field.type === 'password' ? 'text' : 'password'; $('reveal-token').textContent = field.type === 'password' ? '显示' : '隐藏'; }); $('credential-file').addEventListener('change', event => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const credential = JSON.parse(reader.result); $('token').value = credential.token || ''; $('queue-id').value = credential.queue_id || ''; showToast('凭据已读取,只保留在当前页面内'); } catch { showToast('凭据 JSON 无法解析'); } }; reader.readAsText(file); }); document.querySelectorAll('[data-view]').forEach(button => button.addEventListener('click', event => { event.preventDefault(); setView(button.dataset.view); })); window.addEventListener('hashchange', () => setView(currentView(), false)); setView(currentView(), false); refresh(); state.timer = setInterval(refresh, 10000); })();