'use strict';
const BUSINESS_SOURCE = 'LTJT_MOCK_BUSINESS';
const EXTENSION_SOURCE = 'LTJT_ORDER_ASSISTANT_EXTENSION';
const pending = new Map();
let currentTaskId = sessionStorage.getItem('ltjt_mock_current_task_id') || '';
let pollTimer = null;
let eventStream = null;
let bridgeConnected = false;
let erpAutomationEnabled = true;
let extensionCompatible = false;
let remoteSyncInProgress = false;
let syncRequested = false;
let csrfToken = '';
let authUser = null;
let taskStore = [];
const MAX_TASK_LOG_ENTRIES = 300;
const BROWSER_CONNECTION_ID = `administrator-browser:${location.origin}`;
const REQUIRED_EXTENSION_VERSION = '0.3.1';
const TERMINAL_TASK_STATUSES = new Set([
'completed',
'reconciliation_pending',
'blocked',
'failed',
'cancelled',
'dry_run',
'parse_failed'
]);
const SAMPLE_RAW = `下单模式:团队-单个下单
订单性质:测试
产品名称:遇见老挝
出发日期:2026-08-11
人数:
成人:2
小孩占床:1
小孩不占床:1
婴儿:0
领队:0
用房:
SGL:0
TWN:0
TRP:0
DBL:2
HNM:0
TL:0
单价:
成人:520
小孩占床:220
小孩不占床:120
婴儿:0
领队:0
计调OP:测试
销售人:琳琳
备注:笔记本接管重复拦截测试,确认真实保存 ERP。`;
function $(selector) {
return document.querySelector(selector);
}
async function apiRequest(path, options = {}) {
const method = String(options.method || 'GET').toUpperCase();
const headers = new Headers(options.headers || {});
if (options.body !== undefined && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json');
if (method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && csrfToken) {
headers.set('X-CSRF-Token', csrfToken);
}
const response = await fetch(path, {
...options,
method,
headers,
credentials: 'same-origin',
body: options.body !== undefined && typeof options.body !== 'string' ? JSON.stringify(options.body) : options.body
});
let payload = null;
try {
payload = await response.json();
} catch (error) {
payload = null;
}
if (response.status === 401 && path !== '/api/auth/login') {
showLoginPanel('登录已过期,请重新登录。');
}
if (!response.ok) {
throw new Error(payload?.message || payload?.error || `请求失败:HTTP ${response.status}`);
}
return payload || {};
}
function showLoginPanel(message = '') {
authUser = null;
csrfToken = '';
if (eventStream) {
eventStream.close();
eventStream = null;
}
const panel = $('#loginPanel');
const workbench = $('#workbench');
const authState = $('#authState');
const logoutButton = $('#logoutButton');
if (panel) panel.hidden = false;
if (workbench) workbench.hidden = true;
if (authState) authState.hidden = true;
if (logoutButton) logoutButton.hidden = true;
const error = $('#loginError');
if (error) error.textContent = message;
}
function showAuthenticatedApp(user) {
authUser = user;
const panel = $('#loginPanel');
const workbench = $('#workbench');
const authState = $('#authState');
const logoutButton = $('#logoutButton');
if (panel) panel.hidden = true;
if (workbench) workbench.hidden = false;
if (authState) {
authState.hidden = false;
authState.textContent = `${user.username} · 管理员`;
}
if (logoutButton) logoutButton.hidden = false;
}
async function refreshCsrfToken() {
const result = await apiRequest('/api/auth/csrf');
csrfToken = result.csrf_token || '';
if (!csrfToken) throw new Error('服务端未返回 CSRF 会话令牌。');
}
async function syncRemoteTasks() {
if (!authUser) return;
if (remoteSyncInProgress) {
syncRequested = true;
return;
}
remoteSyncInProgress = true;
try {
do {
syncRequested = false;
const result = await apiRequest('/api/tasks');
// Keep cancelled records durable on the server, but hide them from the
// active task-card list even if an older server or an SSE race returns one.
taskStore = (Array.isArray(result.tasks) ? result.tasks : []).filter((task) => (
task?.status !== 'cancelled' && task?.result?.status !== 'cancelled'
));
if (!taskStore.some((task) => task.task_id === currentTaskId)) {
currentTaskId = taskStore[0]?.task_id || '';
}
if (currentTaskId) sessionStorage.setItem('ltjt_mock_current_task_id', currentTaskId);
else sessionStorage.removeItem('ltjt_mock_current_task_id');
renderTaskCards();
} while (syncRequested && authUser);
} finally {
remoteSyncInProgress = false;
if (syncRequested && authUser) {
syncRequested = false;
queueMicrotask(() => syncRemoteTasks().catch((error) => {
setOutput({ status: 'sync_error', message: error.message });
}));
}
}
}
function startRemoteEventStream() {
if (eventStream) eventStream.close();
const lastEventId = taskStore.reduce((highest, task) => (
Math.max(highest, Number(task?.last_event_id || 0))
), 0);
eventStream = new EventSource(`/api/events?since=${encodeURIComponent(lastEventId)}`);
eventStream.addEventListener('task', () => {
syncRemoteTasks().catch((error) => {
setOutput({ status: 'sync_error', message: error.message });
});
});
eventStream.onerror = () => {
// EventSource reconnects automatically; polling remains the fallback authority refresh.
};
}
function normalizeText(text) {
return String(text || '').replace(/\r/g, '').trim();
}
function loadTaskStore() {
return [];
}
function saveTaskStore() {
// The backend is authoritative. The in-memory array is a render cache only.
}
function normalizeLogMessage(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function taskLogLevel(status, message) {
const source = `${status || ''} ${message || ''}`;
if (/failed|blocked|error|uncertain|失败|阻断|错误|不可用/i.test(source)) return 'ERROR';
if (/completed|received|accepted|confirmed|success|完成|收到|接单|确认|成功/i.test(source)) return 'OK';
return 'INFO';
}
function appendTaskLogEntry(task, message, { status = '', level } = {}) {
const normalizedMessage = normalizeLogMessage(message);
if (!task || !normalizedMessage) return;
if (!Array.isArray(task.logs)) task.logs = [];
const entry = {
at: new Date().toISOString(),
level: level || taskLogLevel(status, normalizedMessage),
status: status || '',
message: normalizedMessage
};
const last = task.logs[task.logs.length - 1];
if (last && last.message === entry.message && last.status === entry.status) return;
task.logs.push(entry);
if (task.logs.length > MAX_TASK_LOG_ENTRIES) {
task.logs.splice(0, task.logs.length - MAX_TASK_LOG_ENTRIES);
}
}
function taskSummary(operation) {
const data = operation?.data || {};
const counts = data.passenger_counts || {};
const action = operation?.action || operation?.operation || '';
const labels = {
team_order_create: '团队-单个下单',
team_order_batch_create: '团队-批量下单',
shared_plan_create: '散拼-创建母团计划',
shared_child_order_create: '散拼-录入子单',
order_update: '已有订单更新',
passenger_list_import: '旅客名单导入',
confirmation_export: '确认件导出/恢复',
create_order: 'ERP 创建订单',
update_order: 'ERP 更新订单',
export_confirmation: 'ERP 导出恢复'
};
return {
action,
operation_label: labels[action] || action || '输入解析任务',
route: operation?.route || '',
identifier: operation?.identifier || data.existing_refs?.identifier || data.existing_refs?.order_no || '',
product: data.product?.name || '',
departure_date: data.departure_dates?.[0] || '',
test_marker: data.test_marker || '',
passenger_total: ['adult', 'child_bed', 'child_no_bed', 'infant', 'leader'].reduce((sum, key) => sum + toNumber(counts[key]), 0),
op_user: data.op_user?.name || '',
sales_user: data.sales_user?.name || ''
};
}
function upsertLocalTask(task) {
const index = taskStore.findIndex((item) => item.task_id === task.task_id);
if (index >= 0) {
const existing = taskStore[index];
const previousMessage = existing.message;
const previousStatus = existing.status;
taskStore[index] = { ...existing, ...task, updated_at: new Date().toISOString() };
if (task.message && (task.message !== previousMessage || task.status !== previousStatus)) {
appendTaskLogEntry(taskStore[index], task.message, { status: task.status });
}
} else {
const nextTask = { ...task, updated_at: new Date().toISOString() };
appendTaskLogEntry(nextTask, nextTask.message, { status: nextTask.status });
taskStore.unshift(nextTask);
}
saveTaskStore();
renderTaskCards();
}
function updateLocalTask(taskId, patch) {
const existing = taskStore.find((item) => item.task_id === taskId);
if (!existing) return;
const previousMessage = existing.message;
const previousStatus = existing.status;
Object.assign(existing, patch, { updated_at: new Date().toISOString() });
const messageChanged = typeof patch.message === 'string' && patch.message !== previousMessage;
const statusChanged = patch.status && patch.status !== previousStatus;
if (messageChanged || (statusChanged && !patch.message)) {
appendTaskLogEntry(existing, patch.message || `状态更新:${patch.status}`, { status: patch.status });
}
saveTaskStore();
renderTaskCards();
}
function selectedTask() {
return taskStore.find((item) => item.task_id === currentTaskId) || taskStore[0] || null;
}
function taskStatusText(task) {
if (!task) return '未知';
const status = task.status || task.result?.status || 'created';
if (requiresManualConfirmation(task)) return '待确认';
const stage = task.result?.stage || task.stage || '';
if (status === 'running' && stage === 'parse') return '发送中 / 解析中';
if (status === 'parse_queued') return '排队解析';
if (status === 'parse_running') return '解析中';
if (['agent_parse_blocked', 'parse_failed', 'parse_blocked'].includes(status)) return '外部解析失败';
if (status === 'agent_parse_passed') return '外部解析完成';
if (status === 'parse') return '解析中';
if (status === 'completed') return '已完成';
if (status === 'running') return '执行中';
if (status === 'accepted') return '已接单';
if (status === 'confirmed') return '已确认,提交中';
if (status === 'queued') return '排队中';
if (status === 'paused') return '已暂停';
if (status === 'awaiting_confirmation') return '等待人工确认';
if (status === 'confirmed') return '已确认,待提交';
if (status === 'waiting_extension') return '等待插件连接';
if (status === 'parse_failed') return '拆解失败';
if (status === 'parse_blocked') return '拆解失败';
if (status === 'dry_run') return '已规划,未写入 ERP';
if (status === 'operation_blocked') return '业务未接入';
if (status === 'execution_uncertain') return '保存状态待回查';
if (status === 'reconciliation_pending') return '待回查';
if (status === 'cancelled') return '已取消';
if (status === 'post_save_recovery_required') return '需要恢复导出';
if (status === 'blocked') return '已阻断';
if (status === 'saved_unverified') return '已提交待回查';
return stage || status;
}
function statusClassName(status) {
const normalized = String(status || 'created').replace(/[^a-z0-9_-]/gi, '_');
return isFailureStatus(normalized) ? `${normalized} is-failure` : normalized;
}
function isFailureStatus(status) {
const normalized = String(status || '').toLowerCase();
return /(?:failed|blocked|error|uncertain)/.test(normalized) || normalized === 'saved_unverified';
}
function requiresManualConfirmation(task) {
if (!task?.operation || task.confirmed_at || task.handoff_status === 'accepted') return false;
const status = task.status || task.result?.status || 'created';
return !['parse_failed', 'parse_blocked', 'completed'].includes(status);
}
function taskCanonicalStatus(task) {
return String(task?.status || task?.result?.status || 'created');
}
function isTaskPollable(task) {
const status = taskCanonicalStatus(task);
if (TERMINAL_TASK_STATUSES.has(status)) return false;
return Boolean(
task?.confirmed_at
&& (
['queued', 'accepted', 'running'].includes(status)
|| ['accepted', 'running'].includes(task?.handoff_status)
)
);
}
function isExecutionClaimed(task) {
return Boolean(
task?.confirmed_at
&& (
['queued', 'accepted', 'running'].includes(taskCanonicalStatus(task))
|| ['accepted', 'running', 'reconciliation_pending'].includes(task?.handoff_status)
)
);
}
function canStartConfirmedTask(task) {
return Boolean(
task?.operation
&& task?.confirmed_at
&& task?.status === 'confirmed'
&& task?.handoff_status === 'awaiting_handoff'
);
}
function taskStateClass(status) {
if (['completed', 'parse_response_received', 'task_confirmed'].includes(status)) return 'state-ok';
if (isFailureStatus(status)) return 'state-bad';
return 'state-warn';
}
function taskDisplayText(task, output) {
const status = output?.status;
if (status === 'parse_sending') return '正在发送到外部解析服务';
if (status === 'parse_response_received') return '已收到外部解析结果';
if (status === 'parse_response_blocked') return '外部解析服务已阻断';
if (status === 'agent_parse_blocked') return '外部 Skill 阻断';
if (status === 'agent_parse_passed') return '外部解析完成';
if (status === 'create_error') return '创建失败';
return taskStatusText(task);
}
function selectedOperation(task) {
return task?.operation || task?.result?.operation || null;
}
function formatBusinessDate(value) {
return value ? String(value) : '未填写';
}
function formatDateTime(value) {
if (!value) return '未记录';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return String(value);
return date.toLocaleString('zh-CN', { hour12: false });
}
function formatMoney(value) {
const amount = toNumber(value);
return `¥${amount.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`;
}
const DISPLAY_REDACT_KEY_PATTERN = /(?:api[_-]?key|authorization|cookie|password|secret|csrf(?:[_-]?token)?)/i;
const DISPLAY_MASK_KEY_PATTERN = /(?:session[_-]?id|idempotency[_-]?key)/i;
function redactJsonForDisplay(value, key = '') {
if (DISPLAY_REDACT_KEY_PATTERN.test(key)) return '[已隐藏]';
if (DISPLAY_MASK_KEY_PATTERN.test(key)) return value ? '[已生成]' : '';
if (Array.isArray(value)) return value.map((item) => redactJsonForDisplay(item, key));
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value).map(([entryKey, entryValue]) => [
entryKey,
redactJsonForDisplay(entryValue, entryKey)
])
);
}
return value;
}
function taskResponsePayload(task) {
if (!task) return null;
const parseResponse = task.parse_response || task.parse_result || null;
return {
parse_response: parseResponse,
operation: task.operation || parseResponse?.operation || null,
handoff_response: task.handoff_response || null,
executor_response: task.result || null
};
}
function formatTaskResponse(task) {
const payload = taskResponsePayload(task);
if (!payload || Object.values(payload).every((value) => value === null)) {
return '等待返回 JSON……';
}
return JSON.stringify(redactJsonForDisplay(payload), null, 2);
}
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function appendField(container, label, value) {
const item = el('div', 'detail-field');
item.append(el('span', '', label));
const displayValue = value === undefined || value === null || value === '' ? '未填写' : value;
item.append(el('strong', '', displayValue));
container.append(item);
}
function appendMetric(container, label, value) {
const item = el('div', 'metric');
item.append(el('span', '', label));
item.append(el('strong', '', value || '未填写'));
container.append(item);
}
function createDetailBlock(title) {
const block = el('section', 'detail-block');
block.append(el('h3', '', title));
return block;
}
function passengerRows(counts) {
return [
['成人', counts.adult],
['小孩占床', counts.child_bed],
['小孩不占床', counts.child_no_bed],
['婴儿', counts.infant],
['领队', counts.leader]
].map(([label, value]) => [label, toNumber(value)]);
}
function priceRows(data) {
const counts = data.passenger_counts || {};
const prices = data.prices || {};
return [
['成人', counts.adult, prices.adult],
['小孩占床', counts.child_bed, prices.child_bed],
['小孩不占床', counts.child_no_bed, prices.child_no_bed],
['婴儿', counts.infant, prices.infant],
['领队', counts.leader, prices.leader]
].map(([label, count, price]) => {
const normalizedCount = toNumber(count);
const normalizedPrice = toNumber(price);
return {
label,
count: normalizedCount,
price: normalizedPrice,
amount: normalizedCount * normalizedPrice
};
}).filter((row) => row.count || row.price);
}
function renderMiniTable(rows, columns) {
const table = el('div', 'mini-table');
const head = el('div', 'mini-table-row mini-table-head');
for (const column of columns) head.append(el('span', '', column.label));
table.append(head);
for (const row of rows) {
const line = el('div', 'mini-table-row');
for (const column of columns) line.append(el('span', '', column.render(row)));
table.append(line);
}
return table;
}
function taskStepState(task, step) {
const status = task.status || task.result?.status || 'created';
const handoff = task.handoff_status || '';
const hasOperation = Boolean(selectedOperation(task));
if (step === 'parse') {
if (['parse_failed', 'parse_blocked'].includes(status)) return 'blocked';
return hasOperation ? 'done' : 'active';
}
if (step === 'handoff') {
if (status === 'waiting_extension') return 'active';
if (handoff === 'accepted' || ['accepted', 'running', 'dry_run', 'saved_unverified', 'completed', 'blocked'].includes(status)) return 'done';
return hasOperation ? 'active' : 'wait';
}
if (step === 'erp') {
if (status === 'dry_run') return 'done';
if (status === 'blocked') return 'blocked';
if (['saved_unverified', 'completed'].includes(status)) return 'done';
if (['accepted', 'running', 'queued'].includes(status)) return 'active';
return 'wait';
}
if (step === 'verify') {
if (status === 'completed') return 'done';
if (status === 'saved_unverified') return 'active';
if (status === 'blocked') return 'blocked';
return 'wait';
}
return 'wait';
}
function parseErpReceiptText(value) {
const raw = String(value || '');
const text = raw
.replace(/\r/g, '')
.replace(/
/gi, '\n')
.replace(/<\/(p|div|li|tr)>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/ /gi, ' ')
.replace(/&/gi, '&')
.replace(/</gi, '<')
.replace(/>/gi, '>')
.trim();
if (!text) return null;
const lines = text.split('\n').map((line) => line.trim()).filter(Boolean);
const valueAfterLabel = (label) => {
const pattern = new RegExp(`${label}\\s*[::]\\s*([^\\n]+)`, 'i');
const line = lines.find((item) => pattern.test(item));
const source = line || text;
const match = source.match(pattern);
const value = match ? match[1].trim() : '';
return label === '团号' ? value.replace(/[\s。;;,,].*$/, '').trim() : value;
};
const groupNumber = valueAfterLabel('团号');
const product = valueAfterLabel('产品');
const passengerText = valueAfterLabel('人数');
const success = /下单成功|成功|success|ສໍາເລັດ/i.test(text);
if (!groupNumber && !product && !passengerText) return null;
return {
status: success ? 'submit_success_receipt' : 'submit_receipt',
success,
title: lines[0] || '',
group_number: groupNumber,
product,
passenger_text: passengerText
};
}
function receiptFromTask(task) {
const result = task?.result || {};
const direct = result.erp_receipt || result.report?.erp_receipt;
if (direct) return direct;
const alerts = [
...(Array.isArray(result.alerts) ? result.alerts : []),
...(Array.isArray(result.report?.alerts) ? result.report.alerts : [])
];
const alertReceipt = alerts.map((alert) => alert.erp_receipt || parseErpReceiptText(alert.message)).find(Boolean);
if (alertReceipt) return alertReceipt;
return [
result.message,
result.report?.message,
task?.message,
result.report?.raw_message
].map(parseErpReceiptText).find(Boolean) || null;
}
function taskStageSnapshot(task) {
const emptyStage = (note = '等待任务') => ({
mode: 'pending',
current: null,
currentLabel: '',
note
});
if (!task) {
return {
agent: emptyStage(),
business: emptyStage(),
receipt: emptyStage()
};
}
const result = task.result || {};
const status = String(result.status || task.status || 'created');
const stage = String(result.stage || task.stage || '');
const operation = selectedOperation(task);
const receipt = receiptFromTask(task);
const parseFailed = ['agent_parse_blocked', 'parse_failed', 'parse_blocked'].includes(status)
|| (stage === 'parse' && /failed|blocked/i.test(status));
const agentSucceeded = Boolean(operation)
|| ['agent_parse_passed', 'awaiting_confirmation', 'confirmed', 'queued', 'accepted', 'running', 'paused', 'waiting_extension', 'saved_unverified', 'completed', 'blocked'].includes(status);
const agent = parseFailed
? {
mode: 'failed',
current: 'failed',
currentLabel: 'AI处理失败',
note: '外部解析未通过,未进入业务系统处理。'
}
: agentSucceeded
? {
mode: 'complete',
current: 'success',
currentLabel: 'AI处理成功',
note: '已完成指令拆解,等待或已进入业务系统处理。'
}
: {
mode: 'active',
current: 'submitted',
currentLabel: 'AI处理中',
note: '已提交原始指令,正在等待解析结果。'
};
let business = emptyStage('等待 Agent 处理完成');
if (agentSucceeded) {
if (status === 'completed' || status === 'batch_fallback_completed') {
business = {
mode: 'complete',
current: 'completed',
currentLabel: '业务处理已完成',
note: '业务系统已完成提交和回查流程。'
};
} else if (status === 'reconciliation_pending' || status === 'execution_uncertain' || status === 'saved_unverified') {
business = {
mode: 'active',
current: 'processing',
currentLabel: '业务结果待回查',
note: 'ERP 可能已写入,自动执行已停止且不会重试。'
};
} else if (['blocked', 'cancelled', 'extension_error', 'batch_fallback_incomplete', 'live_submit_blocked', 'preflight_blocked'].includes(status)) {
business = {
mode: 'failed',
current: 'processing',
currentLabel: '业务处理失败',
note: '业务系统或插件执行未完成,请查看下方流式日志。'
};
} else if (status === 'accepted') {
business = {
mode: 'active',
current: 'accepted',
currentLabel: '业务系统已接单',
note: '插件已接单,准备执行 ERP 操作。'
};
} else if (status === 'running' || ['browser_execution', 'open_order_form', 'wait_order_form', 'preflight', 'live_submit', 'verification', 'batch_fallback'].includes(stage)) {
business = {
mode: 'active',
current: 'processing',
currentLabel: '业务系统处理中',
note: '正在执行 ERP 操作或回查。'
};
} else if (['queued', 'confirmed', 'waiting_extension', 'paused'].includes(status)) {
business = {
mode: 'active',
current: null,
currentLabel: status === 'paused' ? '等待恢复执行' : '等待插件接单',
note: status === 'paused' ? '插件 ERP 操作已暂停。' : '已确认,等待插件接单。'
};
} else {
business = {
mode: 'pending',
current: null,
currentLabel: '待提交执行',
note: '等待人工确认后交给业务系统执行。'
};
}
}
let receiptStage;
if (receipt) {
receiptStage = {
mode: 'complete',
current: 'received',
currentLabel: '已获取回执',
note: receipt.group_number ? `回执团号:${receipt.group_number}` : '已收到 ERP 返回的保存回执。'
};
} else if (status === 'reconciliation_pending' || status === 'execution_uncertain' || status === 'saved_unverified') {
receiptStage = {
mode: 'active',
current: 'pending',
currentLabel: '待人工回查',
note: '请按 ERP 团号、订单号或测试标记核对,禁止重新执行。'
};
} else if (business.mode === 'failed') {
receiptStage = {
mode: 'failed',
current: 'pending',
currentLabel: '回执获取失败',
note: '当前没有可用的 ERP 回执。'
};
} else if (business.mode === 'complete') {
receiptStage = {
mode: 'active',
current: 'pending',
currentLabel: '等待回执',
note: '业务流程完成,等待 ERP 回执。'
};
} else {
receiptStage = emptyStage('业务处理完成后获取 ERP 回执');
}
return { agent, business, receipt: receiptStage };
}
function renderTaskStages(task) {
const container = $('#taskStages');
if (!container) return;
container.innerHTML = '';
const snapshot = taskStageSnapshot(task);
const stages = [
{
key: 'agent',
index: '01',
title: 'AI处理',
options: [
{ key: 'submitted', label: '已提交' },
{ key: 'success', label: '已成功' },
{ key: 'failed', label: '已失败' }
]
},
{
key: 'business',
index: '02',
title: '业务系统处理',
options: [
{ key: 'accepted', label: '已接单' },
{ key: 'processing', label: '处理中' },
{ key: 'completed', label: '已完成' }
]
},
{
key: 'receipt',
index: '03',
title: '获取回执',
options: [
{ key: 'pending', label: '待获取' },
{ key: 'received', label: '已获取回执' }
]
}
];
const flow = el('div', 'task-stage-flow');
for (const stage of stages) {
const state = snapshot[stage.key];
const card = el('article', `task-stage-card is-${state.mode}`);
const heading = el('div', 'task-stage-heading');
heading.append(el('span', 'task-stage-index', stage.index));
const titleBlock = el('div', 'task-stage-title-block');
titleBlock.append(el('strong', 'task-stage-title', stage.title));
titleBlock.append(el('span', 'task-stage-current', state.currentLabel || '待开始'));
heading.append(titleBlock);
card.append(heading);
const statuses = el('div', 'task-stage-statuses');
statuses.setAttribute('role', 'list');
const visibleOptions = stage.key === 'agent'
? stage.options.filter((option) => !['success', 'failed'].includes(option.key) || option.key === state.current)
: stage.options;
for (const option of visibleOptions) {
const statusItem = el('span', `task-stage-status${state.current === option.key ? ' is-current' : ''}`, option.label);
statusItem.setAttribute('role', 'listitem');
statuses.append(statusItem);
}
card.append(statuses);
card.append(el('p', 'task-stage-note', state.note));
flow.append(card);
}
container.append(flow);
}
function renderTaskDetail() {
const container = $('#taskDetail');
if (!container) return;
container.innerHTML = '';
const task = selectedTask();
const taskState = $('#taskState');
const confirmButton = $('#confirmTaskButton');
const deleteButton = $('#deleteTaskButton');
const status = task?.status || task?.result?.status || 'created';
renderTaskStages(task);
if (confirmButton) {
const canConfirm = Boolean(task && requiresManualConfirmation(task));
const canStart = Boolean(task && canStartConfirmedTask(task));
confirmButton.hidden = !canConfirm && !canStart;
confirmButton.disabled = !canConfirm && !canStart;
confirmButton.textContent = canStart ? '开始执行' : '确认执行';
}
if (deleteButton) {
deleteButton.disabled = !task
|| isExecutionClaimed(task)
|| TERMINAL_TASK_STATUSES.has(taskCanonicalStatus(task));
}
if (taskState) {
taskState.textContent = task ? taskStatusText(task) : '未创建';
taskState.className = `state ${task ? taskStateClass(status) : 'state-warn'}`;
}
const fallbackEntries = task
? [{
at: task.updated_at || task.created_at || new Date().toISOString(),
level: taskLogLevel(status, task.message),
status,
message: task.message || '任务已创建,等待流程事件。'
}]
: [];
const entries = Array.isArray(task?.logs) && task.logs.length ? task.logs : fallbackEntries;
const logPanel = el('section', 'task-output-panel');
logPanel.append(el('div', 'task-output-label', '流程日志'));
const log = el('pre', 'task-log', entries.length
? entries.map(formatTaskLogEntry).join('\n')
: '暂无任务日志。');
log.id = 'taskLog';
log.setAttribute('role', 'log');
log.setAttribute('aria-live', 'polite');
logPanel.append(log);
const responsePanel = el('section', 'task-output-panel');
responsePanel.append(el('div', 'task-output-label', '返回 JSON'));
const response = el('pre', 'task-json-output', formatTaskResponse(task));
response.id = 'taskResponseJson';
response.setAttribute('aria-live', 'polite');
responsePanel.append(response);
container.append(logPanel, responsePanel);
log.scrollTop = log.scrollHeight;
}
function formatTaskLogEntry(entry) {
const date = new Date(entry.at || Date.now());
const time = Number.isNaN(date.getTime())
? '--:--:--'
: date.toLocaleTimeString('zh-CN', { hour12: false });
const level = String(entry.level || 'INFO').toUpperCase().padEnd(5, ' ');
const status = entry.status ? `[${entry.status}]` : '[event]';
return `${time} ${level} ${status} ${entry.message || ''}`;
}
function renderTaskCards() {
const container = $('#taskCards');
if (!container) return;
$('#taskCount').textContent = `${taskStore.length} 个任务`;
container.innerHTML = '';
if (!taskStore.length) {
const empty = document.createElement('div');
empty.className = 'task-empty';
empty.textContent = '暂无任务。';
container.appendChild(empty);
renderTaskDetail();
return;
}
for (const task of taskStore) {
const card = document.createElement('article');
card.className = `task-card${task.task_id === currentTaskId ? ' is-selected' : ''}`;
card.dataset.taskId = task.task_id;
const summary = task.summary || taskSummary(task.operation);
const status = task.status || task.result?.status || 'created';
card.innerHTML = `