'use strict';
const BUSINESS_SOURCE = 'LIANSYN_PLATFORM';
const EXTENSION_SOURCE = 'LTJT_ORDER_ASSISTANT_EXTENSION';
const CURRENT_PAGE = window.location.pathname.replace(/\/+$/, '') || '/';
const IS_HISTORY_PAGE = CURRENT_PAGE === '/history';
const IS_CHANNELS_PAGE = CURRENT_PAGE === '/channels';
const HOME_TASK_LIMIT = 10;
const HISTORY_TASK_PAGE_SIZE = 24;
const DEFAULT_API_TIMEOUT_MS = 15_000;
const AUTH_API_TIMEOUT_MS = 10_000;
const RESULT_API_TIMEOUT_MS = 20_000;
const LOCAL_TASK_OVERLAY_TTL_MS = 10 * 60_000;
const pending = new Map();
let currentTaskId = sessionStorage.getItem('liansyn_platform_current_task_id') || '';
let pollTimer = null;
let eventStream = null;
let bridgeConnected = false;
let erpAutomationEnabled = true;
let erpHostAccessReady = true;
let erpSessionReady = true;
let extensionCompatible = false;
let latestAiStatus = null;
let latestBridgeStatus = null;
let activeStatusDetails = '';
let remoteSyncInProgress = false;
let syncRequested = false;
let csrfToken = '';
let authUser = null;
let organizationAutomationEnabled = false;
let automationSettingsLoaded = false;
let automationSettingsBusy = false;
let automationSettingsError = '';
let channelList = [];
let channelSettingsBusy = false;
let autoHandoffInProgress = false;
let taskStore = [];
const runtimeTaskStore = new Map();
const remoteTaskStore = new Map();
const taskDetailStore = new Map();
const taskDetailRequests = new Map();
const localTaskOverlayStore = new Map();
let pollInProgress = false;
let backgroundRefreshInProgress = false;
let taskListMeta = {
total: 0,
offset: 0,
limit: HOME_TASK_LIMIT,
has_more: false
};
let historySearch = '';
let historyStatus = '';
let historyOffset = 0;
const lifecycleScrollPositions = new Map();
const taskDeleteStates = new Map();
const taskReconciliationStates = new Map();
const taskReplyStates = new Map();
const taskReplyDrafts = new Map();
const autoHandoffRetryAt = new Map();
const AUTO_HANDOFF_RETRY_MS = 30_000;
const BROWSER_CONNECTION_ID = `administrator-browser:${location.origin}`;
const REQUIRED_EXTENSION_VERSION = '0.5.140';
const MANUAL_HANDOFF_LABEL = '确认并提交到 ERP 插件';
const RETRY_HANDOFF_LABEL = '继续提交到 ERP 插件';
const RECONCILE_LABEL = '只读回查 ERP 现有结果';
const TERMINAL_TASK_STATUSES = new Set([
'completed',
'blocked',
'failed',
'cancelled',
'dry_run',
'parse_failed'
]);
const MANUAL_RECONCILIATION_STATUSES = new Set([
'reconciliation_pending',
'saved_unverified',
'execution_uncertain',
'uncertain'
]);
function requestTimeoutForPath(path) {
if (/^\/api\/auth\//.test(path)) return AUTH_API_TIMEOUT_MS;
if (/\/result$/.test(path)) return RESULT_API_TIMEOUT_MS;
return DEFAULT_API_TIMEOUT_MS;
}
async function fetchWithTimeout(resource, options = {}, timeoutMs = DEFAULT_API_TIMEOUT_MS) {
const controller = new AbortController();
const externalSignal = options.signal;
let timeoutId = null;
let onExternalAbort = null;
if (externalSignal) {
onExternalAbort = () => controller.abort(externalSignal.reason);
if (externalSignal.aborted) onExternalAbort();
else externalSignal.addEventListener('abort', onExternalAbort, { once: true });
}
timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(resource, { ...options, signal: controller.signal });
} catch (error) {
if (controller.signal.aborted && !externalSignal?.aborted) {
const timeoutError = new Error(`请求超时(${Math.round(timeoutMs / 1000)} 秒):${String(resource)}`);
timeoutError.code = 'request_timeout';
throw timeoutError;
}
throw error;
} finally {
if (timeoutId !== null) window.clearTimeout(timeoutId);
if (externalSignal && onExternalAbort) externalSignal.removeEventListener('abort', onExternalAbort);
}
}
function $(selector) {
return document.querySelector(selector);
}
async function apiRequest(path, options = {}) {
const method = String(options.method || 'GET').toUpperCase();
const headers = new Headers(options.headers || {});
const timeoutMs = Number(options.timeoutMs) || requestTimeoutForPath(path);
const requestOptions = { ...options };
delete requestOptions.timeoutMs;
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 fetchWithTimeout(path, {
...requestOptions,
method,
headers,
cache: options.cache || 'no-store',
credentials: 'same-origin',
body: options.body !== undefined && typeof options.body !== 'string' ? JSON.stringify(options.body) : options.body
}, timeoutMs);
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 = '';
stopPolling();
automationSettingsLoaded = false;
automationSettingsError = '';
organizationAutomationEnabled = false;
if (eventStream) {
eventStream.close();
eventStream = null;
}
const panel = $('#loginPanel');
const workbench = $('#workbench');
const channelsPage = $('#channelsPage');
const authState = $('#authState');
const automationButton = $('#automationToggleButton');
const logoutButton = $('#logoutButton');
if (panel) panel.hidden = false;
if (workbench) workbench.hidden = true;
if (channelsPage) channelsPage.hidden = true;
if (authState) authState.hidden = true;
if (automationButton) automationButton.hidden = true;
if (logoutButton) logoutButton.hidden = true;
const submitButton = $('#loginForm button[type="submit"]');
if (submitButton) submitButton.disabled = false;
const error = $('#loginError');
if (error) error.textContent = message;
}
function showAuthChecking() {
authUser = null;
csrfToken = '';
automationSettingsLoaded = false;
automationSettingsError = '';
organizationAutomationEnabled = false;
if (eventStream) {
eventStream.close();
eventStream = null;
}
const panel = $('#loginPanel');
const workbench = $('#workbench');
const channelsPage = $('#channelsPage');
const authState = $('#authState');
const automationButton = $('#automationToggleButton');
const logoutButton = $('#logoutButton');
if (panel) panel.hidden = false;
if (workbench) workbench.hidden = true;
if (channelsPage) channelsPage.hidden = true;
if (authState) authState.hidden = true;
if (automationButton) automationButton.hidden = true;
if (logoutButton) logoutButton.hidden = true;
const submitButton = $('#loginForm button[type="submit"]');
if (submitButton) submitButton.disabled = true;
const error = $('#loginError');
if (error) error.textContent = '正在恢复登录状态(最多等待 10 秒)…';
}
function showAuthenticatedApp(user) {
authUser = user;
const panel = $('#loginPanel');
const workbench = $('#workbench');
const channelsPage = $('#channelsPage');
const authState = $('#authState');
const automationButton = $('#automationToggleButton');
const logoutButton = $('#logoutButton');
if (panel) panel.hidden = true;
if (workbench) workbench.hidden = IS_CHANNELS_PAGE;
if (channelsPage) channelsPage.hidden = !IS_CHANNELS_PAGE;
if (authState) {
authState.hidden = false;
authState.textContent = `${user.username} · 管理员`;
}
if (logoutButton) logoutButton.hidden = false;
if (automationButton) {
automationButton.hidden = IS_CHANNELS_PAGE;
renderAutomationToggle();
}
const submitButton = $('#loginForm button[type="submit"]');
if (submitButton) submitButton.disabled = false;
}
function renderAutomationToggle() {
const button = $('#automationToggleButton');
if (!button) return;
button.hidden = !authUser || IS_CHANNELS_PAGE;
button.disabled = !authUser || automationSettingsBusy || !automationSettingsLoaded || Boolean(automationSettingsError);
button.setAttribute('aria-pressed', organizationAutomationEnabled ? 'true' : 'false');
button.classList.toggle('is-enabled', organizationAutomationEnabled);
button.classList.toggle('is-error', Boolean(automationSettingsError));
button.textContent = automationSettingsError
? '全自动化:读取失败'
: !automationSettingsLoaded
? '全自动化:读取中…'
: organizationAutomationEnabled
? '全自动化:已开启'
: '全自动化:已关闭';
button.title = automationSettingsError
|| (organizationAutomationEnabled
? '已开启:AgentBus 新任务解析通过后将自动进入 ERP'
: '已关闭:AgentBus 新任务需要人工确认后才进入 ERP');
}
async function syncAutomationSettings() {
if (!authUser) return null;
automationSettingsBusy = true;
automationSettingsError = '';
renderAutomationToggle();
try {
const result = await apiRequest('/api/settings/automation');
organizationAutomationEnabled = result.settings?.enabled === true;
automationSettingsLoaded = true;
return result.settings || { enabled: organizationAutomationEnabled };
} catch (error) {
automationSettingsLoaded = false;
automationSettingsError = error.message || String(error);
throw error;
} finally {
automationSettingsBusy = false;
renderAutomationToggle();
}
}
async function toggleAutomationSetting() {
if (!authUser || !automationSettingsLoaded || automationSettingsBusy) return;
const nextEnabled = !organizationAutomationEnabled;
automationSettingsBusy = true;
automationSettingsError = '';
renderAutomationToggle();
try {
const result = await apiRequest('/api/settings/automation', {
method: 'PUT',
body: { enabled: nextEnabled }
});
organizationAutomationEnabled = result.settings?.enabled === true;
automationSettingsLoaded = true;
renderAutomationToggle();
await autoDispatchReadyTasks();
} catch (error) {
automationSettingsError = error.message || String(error);
renderAutomationToggle();
throw error;
} finally {
automationSettingsBusy = false;
renderAutomationToggle();
}
}
function channelStatusLabel(status) {
return {
connected: '已连接',
connecting: '连接中',
error: '连接错误',
disabled: '已停用'
}[status] || '未知状态';
}
function renderChannelList() {
const container = $('#channelList');
const message = $('#channelMessage');
if (!container) return;
container.innerHTML = '';
if (!channelList.length) {
container.append(el('p', 'muted channel-empty', '还没有用户渠道。添加后,每个渠道会使用独立的 AgentBus key。'));
return;
}
for (const channel of channelList) {
const row = el('article', 'channel-row');
const main = el('div', 'channel-row-main');
const heading = el('div', 'channel-row-heading');
heading.append(el('strong', '', channel.display_name || '未命名渠道'));
heading.append(el('span', `state ${channel.status === 'connected' ? 'state-ok' : channel.status === 'error' ? 'state-bad' : 'state-warn'}`, channelStatusLabel(channel.status)));
main.append(heading);
main.append(el('p', 'muted', channel.external_user_ref ? `外部用户:${channel.external_user_ref}` : '未设置外部用户标识'));
main.append(el('p', 'channel-key-state', 'AgentBus key:已配置(平台不会回显)'));
if (channel.last_error) main.append(el('p', 'channel-error', channel.last_error));
row.append(main);
const actions = el('div', 'channel-row-actions');
const toggle = el('button', 'secondary-button', channel.enabled ? '停用' : '启用');
toggle.type = 'button';
toggle.dataset.channelAction = 'toggle';
toggle.dataset.channelId = channel.id;
actions.append(toggle);
const rotate = el('button', 'secondary-button', '轮换 key');
rotate.type = 'button';
rotate.dataset.channelAction = 'rotate';
rotate.dataset.channelId = channel.id;
actions.append(rotate);
const rename = el('button', 'secondary-button', '维护名称');
rename.type = 'button';
rename.dataset.channelAction = 'rename';
rename.dataset.channelId = channel.id;
actions.append(rename);
row.append(actions);
container.append(row);
}
if (message && !channelSettingsBusy) message.textContent = '';
}
async function syncChannels() {
if (!authUser || IS_HISTORY_PAGE) return null;
const result = await apiRequest('/api/channels');
channelList = Array.isArray(result.channels) ? result.channels : [];
renderChannelList();
return channelList;
}
async function createChannelFromForm() {
if (!authUser || channelSettingsBusy) return;
const message = $('#channelMessage');
channelSettingsBusy = true;
if (message) message.textContent = '正在保存渠道…';
try {
const result = await apiRequest('/api/channels', {
method: 'POST',
body: {
display_name: normalizeText($('#channelDisplayName')?.value).slice(0, 120),
external_user_ref: normalizeText($('#channelExternalUserRef')?.value).slice(0, 200),
agentbus_key: String($('#channelAgentbusKey')?.value || '').trim(),
bot_address: normalizeText($('#channelBotAddress')?.value).slice(0, 200),
enabled: true
}
});
if (result.channel) channelList = [...channelList.filter((item) => item.id !== result.channel.id), result.channel];
$('#channelAgentbusKey').value = '';
$('#channelDisplayName').value = '';
$('#channelExternalUserRef').value = '';
$('#channelBotAddress').value = '';
renderChannelList();
if (message) message.textContent = '渠道已保存,连接状态会在服务端异步更新。';
} finally {
channelSettingsBusy = false;
}
}
async function updateChannelEnabled(channelId, enabled) {
if (!authUser || channelSettingsBusy) return;
channelSettingsBusy = true;
try {
await apiRequest(`/api/channels/${encodeURIComponent(channelId)}`, {
method: 'PATCH',
body: { enabled }
});
await syncChannels();
} finally {
channelSettingsBusy = false;
}
}
async function renameChannel(channelId) {
if (!authUser || channelSettingsBusy) return;
const channel = channelList.find((item) => item.id === channelId);
const currentName = channel?.display_name || '';
const nextName = window.prompt('请输入新的渠道名称:', currentName);
if (nextName === null || !nextName.trim() || nextName.trim() === currentName) return;
channelSettingsBusy = true;
const message = $('#channelMessage');
if (message) message.textContent = '正在更新渠道名称…';
try {
await apiRequest(`/api/channels/${encodeURIComponent(channelId)}`, {
method: 'PATCH',
body: { display_name: nextName.trim().slice(0, 120) }
});
await syncChannels();
if (message) message.textContent = '渠道名称已更新。';
} finally {
channelSettingsBusy = false;
}
}
async function rotateChannelKey(channelId) {
if (!authUser || channelSettingsBusy) return;
const key = window.prompt('请输入新的 AgentBus key。保存后旧 key 将立即失效:', '');
if (!key?.trim()) return;
channelSettingsBusy = true;
try {
await apiRequest(`/api/channels/${encodeURIComponent(channelId)}/rotate-key`, {
method: 'POST',
body: { agentbus_key: key.trim() }
});
await syncChannels();
} finally {
channelSettingsBusy = 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 params = new URLSearchParams({
limit: String(IS_HISTORY_PAGE ? HISTORY_TASK_PAGE_SIZE : HOME_TASK_LIMIT),
offset: String(IS_HISTORY_PAGE ? historyOffset : 0)
});
if (IS_HISTORY_PAGE && historySearch) params.set('search', historySearch);
if (IS_HISTORY_PAGE && historyStatus) params.set('status', historyStatus);
const result = await apiRequest(`/api/tasks?${params.toString()}`);
// Keep the server snapshot authoritative, but reject older event/timestamp
// versions and preserve a short-lived local overlay until the result is
// durably visible on the server.
taskStore = (Array.isArray(result.tasks) ? result.tasks : [])
.map(mergeRemoteTask)
.filter((task) => task?.status !== 'cancelled' && task?.result?.status !== 'cancelled');
const pagination = result.pagination || {};
taskListMeta = {
total: Number.isFinite(Number(pagination.total)) ? Number(pagination.total) : taskStore.length,
offset: Number.isFinite(Number(pagination.offset)) ? Number(pagination.offset) : (IS_HISTORY_PAGE ? historyOffset : 0),
limit: Number.isFinite(Number(pagination.limit)) ? Number(pagination.limit) : (IS_HISTORY_PAGE ? HISTORY_TASK_PAGE_SIZE : HOME_TASK_LIMIT),
has_more: pagination.has_more === true
};
if (IS_HISTORY_PAGE && !taskStore.length && historyOffset > 0) {
historyOffset = Math.max(0, historyOffset - HISTORY_TASK_PAGE_SIZE);
syncRequested = true;
continue;
}
for (const task of taskStore) cacheRuntimeTask(task);
if (!taskStore.some((task) => task.task_id === currentTaskId)
&& !(currentTaskId && runtimeTaskStore.has(currentTaskId) && !IS_HISTORY_PAGE)) {
currentTaskId = taskStore[0]?.task_id || '';
}
if (currentTaskId) sessionStorage.setItem('liansyn_platform_current_task_id', currentTaskId);
else sessionStorage.removeItem('liansyn_platform_current_task_id');
renderTaskCards();
} while (syncRequested && authUser);
await syncRuntimeTasks();
await autoDispatchReadyTasks();
} 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 = runtimeTasks().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 cacheRuntimeTask(task) {
if (!task?.task_id) return;
if (isTaskPollable(task) || (isAutomaticTask(task) && canStartConfirmedTask(task))) {
runtimeTaskStore.set(task.task_id, task);
} else {
runtimeTaskStore.delete(task.task_id);
}
}
function runtimeTasks() {
const merged = new Map();
for (const task of taskStore) {
if (task?.task_id) merged.set(task.task_id, task);
}
for (const task of runtimeTaskStore.values()) {
if (!task?.task_id) continue;
const existing = merged.get(task.task_id);
if (!existing || compareTaskVersion(task, existing) >= 0) merged.set(task.task_id, task);
}
for (const [taskId, overlay] of localTaskOverlayStore.entries()) {
if (Date.now() - overlay.at > LOCAL_TASK_OVERLAY_TTL_MS) {
localTaskOverlayStore.delete(taskId);
continue;
}
const existing = merged.get(taskId);
merged.set(taskId, existing
? { ...existing, ...overlay.task, __server_status: existing.__server_status || taskServerStatus(existing) }
: overlay.task);
}
return [...merged.values()];
}
function hasPollableRuntimeTasks() {
return runtimeTasks().some(isTaskPollable);
}
async function syncRuntimeTasks() {
if (!authUser) return;
const result = await apiRequest('/api/tasks?status=active&limit=200&include_total=false');
const activeTasks = (Array.isArray(result.tasks) ? result.tasks : []).map(mergeRemoteTask);
const activeIds = new Set(activeTasks.map((task) => task?.task_id).filter(Boolean));
for (const [taskId, task] of runtimeTaskStore.entries()) {
if (isTaskPollable(task) && !activeIds.has(taskId)) runtimeTaskStore.delete(taskId);
}
for (const task of activeTasks) cacheRuntimeTask(task);
}
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 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: '',
identifier: data.existing_refs?.identifier || data.existing_refs?.order_no || '',
customer: data.customer?.name || '',
product: data.product?.name || '',
leader: data.leader?.name || '',
departure_date: data.departure_dates?.[0] || '',
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 taskServerStatus(task) {
return String(task?.__server_status || task?.status || task?.result?.status || 'created');
}
function compareTaskVersion(left, right) {
const leftEvent = Number(left?.last_event_id || 0);
const rightEvent = Number(right?.last_event_id || 0);
if (leftEvent !== rightEvent) return leftEvent - rightEvent;
const leftTime = Date.parse(left?.updated_at || '') || 0;
const rightTime = Date.parse(right?.updated_at || '') || 0;
return leftTime - rightTime;
}
function isDetailedTask(task) {
return Boolean(task && Array.isArray(task.events) && Object.prototype.hasOwnProperty.call(task, 'operation'));
}
function findTaskById(taskId) {
return taskStore.find((item) => item.task_id === taskId)
|| localTaskOverlayStore.get(taskId)?.task
|| runtimeTaskStore.get(taskId)
|| null;
}
function rememberRemoteTask(task) {
if (!task?.task_id) return task;
const previous = remoteTaskStore.get(task.task_id);
if (previous && compareTaskVersion(previous, task) > 0) return previous;
const normalized = { ...task, __server_status: task.status };
remoteTaskStore.set(task.task_id, normalized);
return normalized;
}
function mergeRemoteTask(task) {
if (!task?.task_id) return task;
const remoteTask = rememberRemoteTask(task);
const detail = taskDetailStore.get(task.task_id);
let merged = remoteTask;
if (detail && compareTaskVersion(detail.task, remoteTask) === 0) {
merged = { ...remoteTask, ...detail.task, __server_status: remoteTask.status };
} else if (detail && compareTaskVersion(detail.task, remoteTask) < 0) {
taskDetailStore.delete(task.task_id);
}
const overlay = localTaskOverlayStore.get(task.task_id);
if (!overlay || Date.now() - overlay.at > LOCAL_TASK_OVERLAY_TTL_MS) {
if (overlay) localTaskOverlayStore.delete(task.task_id);
return merged;
}
if (TERMINAL_TASK_STATUSES.has(String(remoteTask.status || ''))) {
localTaskOverlayStore.delete(task.task_id);
return merged;
}
return { ...merged, ...overlay.task, __server_status: remoteTask.status };
}
function cacheTaskDetails(task) {
if (!isDetailedTask(task)) return;
taskDetailStore.set(task.task_id, { task });
}
async function ensureTaskDetails(taskId, { force = false } = {}) {
const current = findTaskById(taskId);
const cached = taskDetailStore.get(taskId);
if (!force && cached && (!current || compareTaskVersion(cached.task, current) >= 0)) return cached.task;
const existingRequest = taskDetailRequests.get(taskId);
if (existingRequest) return existingRequest;
const request = apiRequest(`/api/tasks/${encodeURIComponent(taskId)}`, { timeoutMs: RESULT_API_TIMEOUT_MS })
.then((result) => {
if (!result.task) throw new Error('服务端未返回任务详情。');
const task = { ...result.task, __server_status: result.task.status };
cacheTaskDetails(task);
upsertLocalTask(task, { forceVisible: false, source: 'server' });
return task;
})
.finally(() => taskDetailRequests.delete(taskId));
taskDetailRequests.set(taskId, request);
return request;
}
function loadSelectedTaskDetails(task) {
if (!authUser || !task || isDetailedTask(task)) return;
const lifecycle = $('#taskLifecycle');
if (lifecycle) lifecycle.textContent = '正在加载任务详情…';
void ensureTaskDetails(task.task_id).catch((error) => {
if (lifecycle?.isConnected) lifecycle.textContent = `任务详情加载失败:${error.message || String(error)}`;
});
}
function upsertLocalTask(task, { forceVisible = true, source = 'server' } = {}) {
if (!task?.task_id) return;
const normalizedTask = source === 'server'
? rememberRemoteTask(task)
: task;
if (source === 'server') {
cacheTaskDetails(normalizedTask);
if (TERMINAL_TASK_STATUSES.has(String(normalizedTask.status || ''))) {
localTaskOverlayStore.delete(normalizedTask.task_id);
}
} else {
localTaskOverlayStore.set(normalizedTask.task_id, {
task: { ...(localTaskOverlayStore.get(normalizedTask.task_id)?.task || {}), ...normalizedTask },
at: Date.now()
});
}
const index = taskStore.findIndex((item) => item.task_id === normalizedTask.task_id);
let nextTask;
if (index >= 0) {
const existing = taskStore[index];
nextTask = { ...existing, ...normalizedTask };
taskStore[index] = nextTask;
} else if (forceVisible && !runtimeTaskStore.has(normalizedTask.task_id)) {
nextTask = { ...normalizedTask };
taskStore.unshift(nextTask);
} else {
nextTask = { ...(runtimeTaskStore.get(normalizedTask.task_id) || {}), ...normalizedTask };
}
cacheRuntimeTask(nextTask);
saveTaskStore();
renderTaskCards();
}
function updateLocalTask(taskId, patch) {
const visibleIndex = taskStore.findIndex((item) => item.task_id === taskId);
const existing = visibleIndex >= 0 ? taskStore[visibleIndex] : findTaskById(taskId);
if (!existing) return;
const updated = { ...existing, ...patch };
localTaskOverlayStore.set(taskId, { task: updated, at: Date.now() });
if (visibleIndex >= 0) taskStore[visibleIndex] = updated;
cacheRuntimeTask(updated);
saveTaskStore();
renderTaskCards();
}
function selectedTask() {
return findTaskById(currentTaskId)
|| (!IS_HISTORY_PAGE && runtimeTaskStore.get(currentTaskId))
|| taskStore[0] || null;
}
function taskFailureInfo(task) {
if (task?.failure && typeof task.failure === 'object') return task.failure;
const parseResponse = task?.parse_response;
if (parseResponse?.failure && typeof parseResponse.failure === 'object') return parseResponse.failure;
if (parseResponse?.failure_stage || parseResponse?.failure_message || parseResponse?.failure_source) {
return parseResponse;
}
return null;
}
function failureStageLabel(stage) {
const labels = {
configuration: '服务配置',
external_session_creation: 'Agent 会话创建',
external_parse_request: 'Agent 请求',
external_session_recovery: 'Agent 会话恢复',
agent_response_parsing: 'Agent 返回解析',
agent_result_validation: 'Agent 结果校验',
operation_contract_validation: 'operation 契约校验',
parse_worker_lease: '解析 Worker 租约',
plugin_handoff: '插件交接',
handoff: '插件交接',
preflight: 'ERP预检',
live_submit: 'ERP保存',
browser_execution: '插件浏览器执行',
erp_execution: 'ERP 写入',
receipt_reconciliation: 'ERP 回执回查',
erp_resolution: 'ERP 对象检索'
};
return labels[String(stage || '')] || String(stage || '任务处理');
}
function taskFailureText(task) {
const failure = taskFailureInfo(task);
if (!failure) return '';
const stage = failureStageLabel(failure.failure_stage || failure.stage);
const message = failure.failure_message || failure.message || failure.error_code || '任务失败。';
return `失败:${stage} · ${message}`;
}
function taskStatusText(task) {
if (!task) return '未知';
const status = task.status || task.result?.status || 'created';
if (requiresManualConfirmation(task)) return 'Agent已返回,待确认并提交 ERP 插件';
const failureText = taskFailureText(task);
if (failureText && isFailureStatus(status)) return failureText;
const stage = task.result?.stage || task.stage || '';
if (status === 'running' && stage === 'parse') return '发送中 / 解析中';
if (status === 'parse_queued') return '排队解析';
if (status === 'parse_running') return '解析中';
if (status === 'awaiting_user_input' || status === 'agent_parse_needs_input') 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' && task.confirmation_mode === 'automatic') return '全自动化已接管,等待插件执行';
if (status === 'confirmed') return '已确认,待提交到 ERP 插件';
if (status === 'queued') return '排队中';
if (status === 'paused') return '已暂停';
if (status === 'awaiting_confirmation') return '等待确认并提交 ERP 插件';
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 taskStatusLabel(task) {
if (!task) return '未知';
const status = taskCanonicalStatus(task);
const stage = task.result?.stage || task.stage || '';
if (requiresManualConfirmation(task)) return '待确认';
if (['agent_parse_blocked', 'parse_failed', 'parse_blocked'].includes(status)) return '失败';
if (isFailureStatus(status)) return '失败';
if (status === 'running' && stage === 'parse') return '解析中';
if (status === 'confirmed' && task.confirmation_mode === 'automatic') return '自动执行中';
const labels = {
created: '待开始',
parse_queued: '排队解析',
parse_running: '解析中',
awaiting_user_input: '等待补充',
agent_parse_needs_input: '等待补充',
agent_parse_passed: '解析完成',
parse: '解析中',
completed: '已完成',
running: '执行中',
accepted: '已接单',
confirmed: '已确认',
queued: '排队中',
paused: '已暂停',
awaiting_confirmation: '待确认',
waiting_extension: '等待插件',
dry_run: '已规划',
operation_blocked: '已阻断',
execution_uncertain: '待回查',
reconciliation_pending: '待回查',
saved_unverified: '待回查',
cancelled: '已取消',
post_save_recovery_required: '待恢复'
};
return labels[status] || 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?.task_id || task.confirmed_at || task.handoff_status === 'accepted') return false;
const status = task.status || task.result?.status || 'created';
return ['awaiting_confirmation', 'agent_parse_passed'].includes(status);
}
function taskCanonicalStatus(task) {
return String(task?.status || task?.result?.status || 'created');
}
function isAwaitingUserInput(task) {
return task?.status === 'awaiting_user_input';
}
async function sendTaskReply(task, textarea, button, statusNode) {
const taskId = task?.task_id;
if (!taskId || !isAwaitingUserInput(task) || taskReplyStates.get(taskId) === 'sending') return;
const message = normalizeText(textarea?.value);
if (!message) {
if (statusNode) statusNode.textContent = '请先填写补充信息。';
textarea?.focus();
return;
}
taskReplyStates.set(taskId, 'sending');
if (button) button.disabled = true;
if (statusNode) statusNode.textContent = '正在提交补充信息…';
try {
const result = await apiRequest('/api/messages', {
method: 'POST',
body: {
message,
task_id: taskId,
conversation_id: task.conversation_id || undefined,
idempotency_key: `manual:${taskId}:${makeRequestId()}`
}
});
taskReplyStates.delete(taskId);
taskReplyDrafts.delete(taskId);
if (result.task) {
currentTaskId = result.task.task_id;
sessionStorage.setItem('liansyn_platform_current_task_id', currentTaskId);
upsertLocalTask(result.task);
} else {
await syncRemoteTasks();
}
} catch (error) {
taskReplyStates.delete(taskId);
if (button?.isConnected) button.disabled = false;
if (statusNode?.isConnected) statusNode.textContent = error.message || String(error);
}
}
function taskImportantMessage(task) {
if (task?.important_message) return task.important_message;
const status = String(task?.status || task?.result?.status || '').toLowerCase();
if (status === 'awaiting_user_input') {
const reply = String(task?.parse_response?.reply || task?.result?.reply || '').trim();
return reply ? {
kind: 'awaiting_user_input',
text: reply,
recorded_at: task?.updated_at || '',
input_request: task?.input_request || undefined
} : null;
}
if (task?.error_summary && isFailureStatus(status)) {
return {
kind: 'error',
text: task.error_summary.message || '任务处理失败。',
recorded_at: task.error_summary.recorded_at || task.updated_at || '',
error_summary: task.error_summary
};
}
if (status === 'completed' && task?.success_receipt) {
const receipt = task.success_receipt;
return {
kind: 'success',
text: task.message || '任务已完成。',
recorded_at: receipt.recorded_at || task.updated_at || '',
success_receipt: receipt
};
}
return null;
}
function renderTaskReplyForm(task) {
const form = el('form', 'task-input-request-form');
form.noValidate = true;
const textarea = el('textarea', 'task-reply-input');
textarea.id = 'taskReplyInput';
textarea.name = 'message';
textarea.rows = 4;
textarea.maxLength = 200_000;
textarea.placeholder = '请输入补充信息,提交后将沿用当前任务的 Agent 会话。';
textarea.value = taskReplyDrafts.get(task.task_id) || '';
textarea.setAttribute('aria-label', '补充当前任务的信息');
textarea.addEventListener('input', () => {
taskReplyDrafts.set(task.task_id, textarea.value);
});
form.append(textarea);
const actions = el('div', 'task-input-request-actions');
const button = el('button', '', taskReplyStates.get(task.task_id) === 'sending' ? '提交中…' : '提交补充信息');
button.id = 'sendTaskReplyButton';
button.type = 'submit';
button.disabled = taskReplyStates.get(task.task_id) === 'sending';
const statusNode = el('span', 'task-input-request-status');
statusNode.id = 'taskReplyStatus';
statusNode.setAttribute('role', 'status');
actions.append(button, statusNode);
form.append(actions);
form.addEventListener('submit', (event) => {
event.preventDefault();
void sendTaskReply(task, textarea, button, statusNode);
});
return form;
}
function renderTaskImportantMessage(task) {
const important = taskImportantMessage(task);
if (!important) return null;
const panel = el('section', 'task-output-panel task-important-message-panel');
panel.id = 'importantMessagePanel';
panel.append(el('div', 'task-output-label', '需返回/交互用户的重要消息'));
const kind = important.kind;
const kindMeta = kind === 'awaiting_user_input'
? { label: '等待补充', className: 'state-warn' }
: kind === 'error'
? { label: '错误摘要', className: 'state-bad' }
: { label: '成功回执', className: 'state-ok' };
const card = el('article', `task-important-message-card ${kindMeta.className}`);
const heading = el('div', 'task-important-message-heading');
heading.append(el('strong', 'task-outcome-title', kindMeta.label));
heading.append(el('time', 'task-outcome-time', `记录时间:${formatDateTime(important.recorded_at)}`));
card.append(heading);
card.append(el('p', 'task-outcome-message', important.text));
if (kind === 'success') {
const receipt = important.success_receipt?.receipt || {};
const groups = displayValues(receipt.group_numbers || receipt.group_number);
const orders = displayValues(receipt.order_numbers || receipt.order_number);
if (groups.length) card.append(el('p', 'task-outcome-line', `团号:${groups.join('、')}`));
if (orders.length) card.append(el('p', 'task-outcome-line', `订单号:${orders.join('、')}`));
}
panel.append(card);
if (kind === 'awaiting_user_input') {
const formPanel = el('section', 'task-input-request');
formPanel.id = 'inputRequestPanel';
const header = el('div', 'task-input-request-head');
header.append(el('div', 'task-output-label', '补充信息'));
header.append(el(
'span',
'task-input-request-state',
taskReplyStates.get(task.task_id) === 'sending' ? '提交中…' : '仅补充当前任务'
));
formPanel.append(header, renderTaskReplyForm(task));
panel.append(formPanel);
}
return panel;
}
function isTaskPollable(task) {
const localStatuses = [task?.status, task?.result?.status, task?.handoff_status]
.map((value) => String(value || '').toLowerCase());
if (localStatuses.some((status) => MANUAL_RECONCILIATION_STATUSES.has(status))) return false;
const status = taskServerStatus(task);
if (TERMINAL_TASK_STATUSES.has(status) || MANUAL_RECONCILIATION_STATUSES.has(status)) return false;
return Boolean(
task?.confirmed_at
&& (
['queued', 'accepted', 'running'].includes(status)
|| ['accepted', 'running'].includes(task?.handoff_status)
)
);
}
function canDeleteTask(task) {
return Boolean(task);
}
function canStartConfirmedTask(task) {
return Boolean(
task?.operation
&& task?.confirmed_at
&& task?.status === 'confirmed'
&& task?.handoff_status === 'awaiting_handoff'
);
}
function canReconcileTask(task) {
const result = task?.result && typeof task.result === 'object' ? task.result : {};
const failedWithReconciliation = taskCanonicalStatus(task) === 'failed'
&& result.reconciliation_required === true
&& ['reconciliation_pending', 'execution_uncertain', 'saved_unverified'].includes(String(result.status || ''));
return Boolean(
task?.operation
&& task?.confirmed_at
&& (taskCanonicalStatus(task) === 'reconciliation_pending' || failedWithReconciliation)
&& isLifecycleOperationView(selectedOperation(task))
);
}
function canResumePrewriteTask(task) {
const result = task?.result && typeof task.result === 'object' ? task.result : {};
const stage = String(result.stage || task?.stage || '');
return Boolean(
task?.operation
&& task?.confirmed_at
&& taskCanonicalStatus(task) === 'running'
&& ['auto_executor', 'erp_keepalive_degraded'].includes(stage)
&& result.no_erp_write === true
&& result.write_attempted !== true
);
}
function isAutomaticTask(task) {
return task?.confirmation_mode === 'automatic';
}
async function autoDispatchReadyTasks({ force = false } = {}) {
if (!authUser || autoHandoffInProgress) return;
autoHandoffInProgress = true;
try {
let candidates = taskStore;
if (organizationAutomationEnabled) {
const result = await apiRequest('/api/tasks?status=confirmed&limit=200&include_total=false');
const byId = new Map(taskStore.map((task) => [task.task_id, task]));
for (const task of Array.isArray(result.tasks) ? result.tasks : []) {
byId.set(task.task_id, task);
cacheRuntimeTask(task);
}
candidates = [...byId.values()];
}
const orderedCandidates = [...candidates].sort((left, right) => {
const leftTime = Date.parse(left?.created_at || '') || 0;
const rightTime = Date.parse(right?.created_at || '') || 0;
return leftTime - rightTime || String(left?.task_id || '').localeCompare(String(right?.task_id || ''));
});
for (const task of orderedCandidates) {
if (!isAutomaticTask(task) || !task?.confirmed_at || task?.status !== 'confirmed'
|| task?.handoff_status !== 'awaiting_handoff') continue;
const detailedTask = await ensureTaskDetails(task.task_id);
if (!canStartConfirmedTask(detailedTask)) continue;
const retryAt = autoHandoffRetryAt.get(task.task_id) || 0;
if (!force && retryAt > Date.now()) continue;
try {
const handoff = await handoffTaskToExtension(detailedTask);
if (handoff?.status === 'paused') {
autoHandoffRetryAt.set(task.task_id, Date.now() + AUTO_HANDOFF_RETRY_MS);
break;
} else if (handoff?.status === 'queued') {
autoHandoffRetryAt.set(task.task_id, Date.now() + AUTO_HANDOFF_RETRY_MS);
break;
} else {
autoHandoffRetryAt.delete(task.task_id);
break;
}
} catch (error) {
autoHandoffRetryAt.set(task.task_id, Date.now() + AUTO_HANDOFF_RETRY_MS);
// Keep the server-authoritative confirmed/awaiting_handoff state in
// the local snapshot. Replacing it with waiting_extension would make
// the task fail canStartConfirmedTask() and prevent a later
// BRIDGE_READY event from retrying the pre-claim handoff.
updateLocalTask(task.task_id, {
stage: 'bridge',
message: `全自动化等待插件:${error.message || String(error)}`,
error: error.message || String(error)
});
break;
}
}
} finally {
autoHandoffInProgress = false;
}
}
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_needs_input') return '等待用户补充';
if (status === 'agent_parse_passed') return '外部解析完成';
if (status === 'create_error') return '创建失败';
return taskStatusText(task);
}
function selectedOperation(task) {
return task?.operation || task?.result?.operation || null;
}
const LIFECYCLE_ACTIONS = new Set([
'passenger_list_import',
'arrangement_guide',
'arrangement_vehicle',
'arrangement_hotel',
'arrangement_transport',
'arrangement_other',
'order_update_independent',
'order_update_shared_plan',
'order_update_shared_child',
'order_cancel',
'order_restore',
'order_delete'
]);
function isLifecycleOperationView(operation) {
return LIFECYCLE_ACTIONS.has(String(operation?.action || ''));
}
function hasLifecycleTestContextView(operation) {
const context = operation?.source?.test_context;
return isLifecycleOperationView(operation)
&& Boolean(context && typeof context === 'object' && !Array.isArray(context) && Object.keys(context).length);
}
function lifecycleReviewIdentifier(refs = {}) {
return String(
refs.identifier
|| refs.order_no
|| refs.group_no
|| refs.child_order_no
|| refs.parent_group_no
|| refs.parent_plan_no
|| refs.plan_no
|| refs.ddid
|| refs.tid
|| ''
).trim();
}
function operationReviewDetail(operation = {}) {
const data = operation.data || {};
if (data.arrangement) return `${operation.action} / ${data.arrangement.mode || '未声明模式'}`;
if (Array.isArray(data.updates?.actions)) {
return data.updates.actions.map((item) => `${item.target || '未知字段'}:${item.operation || '未知动作'}`).join(',');
}
if (data.transition) return `${data.transition.mode || '未知路径'}:${data.transition.from_status || '未知'} → ${data.transition.to_status || '未知'}`;
if (Array.isArray(data.export_types)) return `文件类型:${data.export_types.join('、')}(保存为后台附件)`;
if (data.passenger_list) return `${data.passenger_list.operation || '名单操作'} / ${data.passenger_list.row_count || 0} 行`;
return data.order_number?.suffix ? `显式 suffix:${data.order_number.suffix}` : '按标准 operation 执行';
}
function renderOperationReview(task) {
const operation = selectedOperation(task);
if (!operation || operation.action === 'confirmation_export') return null;
const data = operation.data || {};
const refs = data.existing_refs || {};
const context = operation.source?.test_context || {};
const lifecycleTest = hasLifecycleTestContextView(operation);
const businessLookup = Boolean(data.customer?.name && Array.isArray(data.departure_dates) && data.departure_dates.length === 1);
const panel = el('section', 'task-output-panel task-operation-review-panel');
const heading = el('div', 'task-output-heading');
heading.append(el('div', 'task-output-label', lifecycleTest ? '生命周期测试写入审核' : '标准 operation 审核'));
panel.append(heading);
const card = el('div', `task-operation-review-card ${lifecycleTest ? 'state-warn' : ''}`);
const rows = [
['action', operation.action || '未识别'],
['目标对象', refs.kind
? `${refs.kind} / ${lifecycleReviewIdentifier(refs) || (businessLookup ? '按业务条件检索' : '缺少编号')}`
: businessLookup ? '按业务条件检索已有对象' : '新建对象'],
['目标日期', refs.departure_date || (Array.isArray(data.departure_dates) ? data.departure_dates.join('、') : '未提供')],
['业务选择', [data.customer?.name, data.product?.name, data.leader?.name].filter(Boolean).join(' / ') || '由当前对象确定'],
['操作明细', operationReviewDetail(operation)]
];
if (lifecycleTest) {
rows.push(
['测试运行', context.run_id || '缺失'],
['账号 / 标记', `${context.account || '缺失'} / ${context.marker || '缺失'}`],
['创建引用', `${Array.isArray(context.created_refs) ? context.created_refs.length : 0} 个(服务端确认时精确复核当前对象)`],
['写入授权', context.allow_live_write === true ? '控制平面已在人工确认后授权' : '尚未授权;点击确认后由控制平面校验并注入']
);
}
for (const [label, value] of rows) {
const row = el('div', 'task-operation-review-row');
row.append(el('span', 'task-operation-review-key', label));
row.append(el('span', 'task-operation-review-value', String(value || '未提供')));
card.append(row);
}
if (lifecycleTest) {
card.append(el('p', 'task-operation-review-note', '仅允许当前运行、AI 测试账号、2026 年 9 月且精确命中 created_refs 的对象;开启全自动后,AgentBus 任务不因生命周期类型转为人工确认。'));
}
panel.append(card);
return panel;
}
function renderTaskArtifacts(task) {
const report = task?.result?.report && typeof task.result.report === 'object'
? task.result.report
: {};
const reportArtifacts = Array.isArray(report.artifacts) ? report.artifacts : [];
const receipt = task?.success_receipt?.receipt && typeof task.success_receipt.receipt === 'object'
? task.success_receipt.receipt
: {};
const receiptAttachments = [
...(Array.isArray(receipt.reply_attachments) ? receipt.reply_attachments : []),
...(Array.isArray(receipt.attachments) ? receipt.attachments : []),
...(Array.isArray(task?.important_message?.attachments) ? task.important_message.attachments : [])
];
const fallbackByName = new Map(
receiptAttachments
.filter((item) => item && typeof item === 'object')
.map((item) => [
String(item.file_name || item.name || item.fileName || '').trim().toLowerCase(),
item
])
.filter(([name]) => name)
);
const artifacts = reportArtifacts.length
? reportArtifacts.map((artifact) => {
const fallback = fallbackByName.get(
String(artifact?.file_name || artifact?.name || artifact?.fileName || '').trim().toLowerCase()
) || {};
return {
...fallback,
...artifact,
file_name: artifact?.file_name || artifact?.name || artifact?.fileName
|| fallback.file_name || fallback.name || '团队文件',
download_url: artifact?.download_url || artifact?.public_url || artifact?.url
|| fallback.download_url || fallback.public_url || fallback.url || ''
};
})
: receiptAttachments;
if (!artifacts.length) return null;
const panel = el('section', 'task-output-panel task-artifact-panel');
const heading = el('div', 'task-output-heading');
heading.append(el('div', 'task-output-label', '后台附件'));
panel.append(heading);
const list = el('div', 'task-artifact-list');
if (reportArtifacts.length && report.backend_stored !== true) {
list.append(el('p', 'task-artifact-pending', 'ERP 文件已读取,正在保存到平台后台…'));
panel.append(list);
return panel;
}
for (const artifact of artifacts) {
const row = el('div', 'task-artifact-row');
const meta = el('div', 'task-artifact-meta');
meta.append(el('strong', 'task-artifact-name', artifact.file_name || '团队文件'));
row.append(meta);
const downloadUrl = artifact.download_url || artifact.public_url || artifact.url
|| (artifact.artifact_id && task?.task_id
? `/api/tasks/${encodeURIComponent(task.task_id)}/artifacts/${encodeURIComponent(artifact.artifact_id)}`
: '');
if (downloadUrl) {
const link = el('a', 'secondary-button task-artifact-download', '下载文件');
link.href = downloadUrl;
link.setAttribute('download', artifact.file_name || 'team-file.bin');
link.rel = 'noopener';
row.append(link);
} else {
row.append(el('span', 'task-artifact-unavailable', '附件链接不可用'));
}
list.append(row);
}
panel.append(list);
return panel;
}
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 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 explicit = task?.success_receipt?.receipt;
if (explicit) return explicit;
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 displayValues(value) {
if (Array.isArray(value)) return value.map((item) => String(item || '').trim()).filter(Boolean);
if (value === undefined || value === null || value === '') return [];
return [String(value).trim()].filter(Boolean);
}
function taskStageSnapshot(task) {
const emptyStage = () => ({
mode: 'pending',
current: null,
currentLabel: ''
});
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 parseNeedsInput = ['awaiting_user_input', 'agent_parse_needs_input'].includes(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: '已失败'
}
: parseNeedsInput
? {
mode: 'needs',
current: 'needs_input',
currentLabel: '等待补充'
}
: agentSucceeded
? {
mode: 'complete',
current: 'success',
currentLabel: '已成功'
}
: status === 'parse_running' || (status === 'running' && stage === 'parse')
? {
mode: 'processing',
current: 'submitted',
currentLabel: '处理中'
}
: {
mode: 'needs',
current: 'submitted',
currentLabel: '已提交'
};
let business = emptyStage();
if (agentSucceeded) {
if (status === 'completed' || status === 'batch_fallback_completed') {
business = {
mode: 'complete',
current: 'completed',
currentLabel: '已完成'
};
} else if (status === 'reconciliation_pending' || status === 'execution_uncertain' || status === 'saved_unverified') {
business = {
mode: 'needs',
current: 'processing',
currentLabel: '处理中'
};
} else if (['blocked', 'failed', 'cancelled', 'extension_error', 'batch_fallback_incomplete', 'live_submit_blocked', 'preflight_blocked'].includes(status)) {
business = {
mode: 'failed',
current: 'failed',
currentLabel: '已失败'
};
} else if (status === 'accepted' || status === 'queued') {
business = {
mode: 'processing',
current: 'accepted',
currentLabel: '已接单'
};
} else if (status === 'running' || ['browser_execution', 'open_order_form', 'wait_order_form', 'preflight', 'live_submit', 'verification', 'batch_fallback'].includes(stage)) {
business = {
mode: 'processing',
current: 'processing',
currentLabel: '处理中'
};
} else if (['confirmed', 'waiting_extension', 'paused'].includes(status)) {
business = {
mode: 'needs',
current: null,
currentLabel: '需处理'
};
} else {
business = {
mode: 'pending',
current: null,
currentLabel: '待开始'
};
}
}
let receiptStage;
if (receipt) {
receiptStage = {
mode: 'complete',
current: 'received',
currentLabel: '已获取回执'
};
} else if (status === 'reconciliation_pending' || status === 'execution_uncertain' || status === 'saved_unverified') {
receiptStage = {
mode: 'needs',
current: 'pending',
currentLabel: '待获取'
};
} else if (business.mode === 'failed') {
receiptStage = {
mode: 'pending',
current: null,
currentLabel: '待获取'
};
} else if (business.mode === 'complete') {
receiptStage = {
mode: 'needs',
current: 'pending',
currentLabel: '待获取'
};
} else {
receiptStage = emptyStage();
}
return { agent, business, receipt: receiptStage };
}
function renderTaskStages(task) {
const container = $('#taskStages');
if (!container) return;
container.innerHTML = '';
const snapshot = taskStageSnapshot(task);
const stages = [
{
key: 'agent',
title: 'AI处理阶段',
options: [
{ key: 'submitted', label: '已提交' },
{ key: 'needs_input', label: '等待补充' },
{ key: 'success', label: '已成功' },
{ key: 'failed', label: '已失败' }
]
},
{
key: 'business',
title: '系统写入处理',
options: [
{ key: 'accepted', label: '已接单' },
{ key: 'processing', label: '处理中' },
{ key: 'completed', label: '已完成' },
{ key: 'failed', label: '已失败' }
]
},
{
key: 'receipt',
title: '获取回执',
options: [
{ key: 'pending', label: '待获取' },
{ key: 'received', label: '已获取回执' },
{ key: 'failed', 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('strong', 'task-stage-title', stage.title));
card.append(heading);
const currentOption = stage.options.find((option) => option.key === state.current);
const statusItem = el(
'span',
'task-stage-status is-current',
currentOption?.label || state.currentLabel || '待开始'
);
statusItem.setAttribute('role', 'status');
card.append(statusItem);
flow.append(card);
}
container.append(flow);
}
function renderTaskDetail() {
const container = $('#taskDetail');
if (!container) return;
const previousLifecycle = $('#taskLifecycle');
if (previousLifecycle?.dataset.taskId && Number.isFinite(previousLifecycle.scrollTop)) {
lifecycleScrollPositions.set(previousLifecycle.dataset.taskId, previousLifecycle.scrollTop);
}
container.innerHTML = '';
const task = selectedTask();
const taskState = $('#taskState');
const confirmButton = $('#confirmTaskButton');
const deleteButton = $('#deleteTaskButton');
const status = task?.status || task?.result?.status || 'created';
const importantMessagePanel = renderTaskImportantMessage(task);
renderTaskStages(task);
if (confirmButton) {
const canConfirm = Boolean(task && requiresManualConfirmation(task));
const canStart = Boolean(task && !isAutomaticTask(task) && canStartConfirmedTask(task));
const canResume = Boolean(task && canResumePrewriteTask(task));
const canReconcile = Boolean(task && canReconcileTask(task));
const reconciliationBusy = Boolean(task && taskReconciliationStates.get(task.task_id) === 'running');
confirmButton.hidden = !canConfirm && !canStart && !canResume && !canReconcile;
confirmButton.disabled = (!canConfirm && !canStart && !canResume && !canReconcile) || reconciliationBusy;
const lifecycleReview = hasLifecycleTestContextView(selectedOperation(task));
confirmButton.textContent = reconciliationBusy
? '只读回查中…'
: canReconcile
? RECONCILE_LABEL
: canResume
? '恢复保存前执行'
: canStart
? RETRY_HANDOFF_LABEL
: lifecycleReview ? '审核并授权本次测试写入' : MANUAL_HANDOFF_LABEL;
}
if (deleteButton) {
const deleteState = task ? taskDeleteStates.get(task.task_id) : '';
deleteButton.disabled = !canDeleteTask(task) || deleteState === 'deleting';
if (deleteState === 'deleting') deleteButton.textContent = '删除中…';
else if (deleteState === 'failed') deleteButton.textContent = '重试删除';
else deleteButton.textContent = '删除任务';
}
if (taskState) {
taskState.textContent = task ? taskStatusLabel(task) : '未创建';
taskState.className = `state ${task ? taskStateClass(status) : 'state-warn'}`;
}
const lifecyclePanel = el('section', 'task-output-panel task-lifecycle-panel');
const lifecycleHeading = el('div', 'task-output-heading');
lifecycleHeading.append(el('div', 'task-output-label', '任务生命周期'));
const copyButton = el('button', 'secondary-button task-copy-button', '一键复制');
copyButton.type = 'button';
copyButton.dataset.copyLifecycle = 'true';
copyButton.title = '复制黑框中的全部内容';
copyButton.setAttribute('aria-label', '复制任务生命周期');
lifecycleHeading.append(copyButton);
lifecyclePanel.append(lifecycleHeading);
const lifecycle = renderTaskLifecycle(task);
lifecycle.id = 'taskLifecycle';
lifecycle.dataset.taskId = task?.task_id || '';
lifecycle.setAttribute('role', 'log');
lifecycle.setAttribute('aria-live', 'polite');
lifecyclePanel.append(lifecycle);
const panels = [];
if (importantMessagePanel) panels.push(importantMessagePanel);
const operationReviewPanel = renderOperationReview(task);
if (operationReviewPanel) panels.push(operationReviewPanel);
const artifactPanel = renderTaskArtifacts(task);
if (artifactPanel) panels.push(artifactPanel);
panels.push(lifecyclePanel);
container.append(...panels);
const rememberedScrollTop = task?.task_id ? lifecycleScrollPositions.get(task.task_id) : undefined;
if (Number.isFinite(rememberedScrollTop)) lifecycle.scrollTop = rememberedScrollTop;
loadSelectedTaskDetails(task);
}
const EXTERNAL_TRACE_MESSAGES = {
not_started: '解析请求已创建,等待调用外部 Agent。',
creating_session: '正在创建外部 Agent 会话。',
session_created: '外部 Agent 会话已创建。',
stream_connecting: '正在接收外部 Agent 的 SSE 响应。',
stream_completed: '外部 Agent SSE 响应已完成。',
result_received: '已收到 Agent 返回结果。',
contract_validating: '正在校验 Agent 返回的标准 operation 契约。',
contract_repairing: '首次结果未通过契约校验,正在发起一次同会话修复。',
recovering_session: '外部 Agent 会话异常,正在恢复。',
session_recreated: '外部 Agent 会话已重建。',
completed_without_json: 'Agent 响应已结束,但未返回可识别的 JSON。',
failed: '外部 Agent 请求失败。',
not_configured: '外部解析服务未配置。'
};
function externalTraceMessage(item) {
return EXTERNAL_TRACE_MESSAGES[item?.stage]
|| `外部解析阶段:${item?.stage || 'unknown'}。`;
}
function taskLifecycleEntries(task) {
const entries = [];
let order = 0;
for (const event of Array.isArray(task?.events) ? task.events : []) {
entries.push({
...event,
source: 'task_event',
group: event.stage || 'system',
order: order++
});
}
const trace = task?.parse_response?.external_request;
for (const item of Array.isArray(trace?.timeline) ? trace.timeline : []) {
entries.push({
id: `agent-trace-${item.sequence || order}`,
status: 'info',
stage: item.stage || 'external_parse',
message: externalTraceMessage(item),
created_at: item.at,
payload: item,
source: 'agent_trace',
group: 'parse',
order: order++
});
}
const error = task?.error_summary;
const failure = taskFailureInfo(task);
const rawErrorText = error?.message || failure?.failure_message || '';
const errorText = taskFailureText(task) || rawErrorText;
if (errorText) {
const hasErrorEntry = entries.some((entry) => {
const values = [entry.message, entry.payload?.failure_message, entry.payload?.message]
.filter(Boolean)
.map((value) => String(value));
return values.some((value) => value.includes(errorText) || (rawErrorText && value.includes(rawErrorText)));
});
if (!hasErrorEntry) {
const errorStage = error?.stage || error?.failure_stage || failure?.failure_stage || 'task';
entries.push({
id: `task-error-summary-${error?.recorded_at || task?.updated_at || 'latest'}`,
status: 'error',
stage: errorStage,
message: errorText.startsWith('失败:')
? errorText
: `失败:${failureStageLabel(errorStage)} · ${errorText}`,
created_at: error?.recorded_at || task?.updated_at || '',
payload: {
error_code: error?.error_code || failure?.error_code || 'task_failed',
failure_stage: error?.failure_stage || errorStage,
failure_source: error?.failure_source || failure?.failure_source || '',
failure_message: rawErrorText || errorText
},
source: 'task_error_summary',
group: errorStage,
order: order++
});
}
}
return entries.sort((left, right) => {
const leftTime = Date.parse(left.created_at || '') || 0;
const rightTime = Date.parse(right.created_at || '') || 0;
return leftTime - rightTime || left.order - right.order;
});
}
function lifecycleLevel(entry) {
const source = `${entry.status || ''} ${entry.message || ''} ${entry.payload?.failure_stage || ''}`;
if (/failed|blocked|error|uncertain|失败|阻断|错误|不可用/i.test(source)) return 'error';
if (/completed|received|accepted|confirmed|success|完成|收到|接单|确认|成功/i.test(source)) return 'ok';
if (/repair|recover|waiting|pending|queued|等待|恢复|修复/i.test(source)) return 'warn';
return 'info';
}
function lifecycleGroupLabel(entry) {
if (entry.source === 'agent_trace' || entry.group === 'parse') return 'AI处理阶段';
return taskLogStageLabel(entry.group || entry.stage);
}
function taskLifecycleDetails(entry, task) {
const details = {
event_id: entry.id,
source: entry.source,
status: entry.status || '',
stage: entry.stage || '',
created_at: entry.created_at || '',
payload: entry.payload || {}
};
if (entry.source === 'task_event' && entry.stage === 'parse'
&& ['awaiting_confirmation', 'awaiting_user_input', 'parse_failed'].includes(entry.status)) {
details.parse_response = task?.parse_response || null;
details.operation = task?.operation || null;
details.failure = taskFailureInfo(task);
}
if (entry.source === 'task_event'
&& ['execution', 'browser_execution', 'preflight', 'live_submit', 'receipt', 'verification', 'reconciliation'].includes(entry.stage)) {
details.executor_response = task?.result || null;
details.failure = taskFailureInfo(task);
}
return details;
}
async function writeClipboardText(text) {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return;
} catch (error) {
// Fall through to the DOM fallback when clipboard permissions are unavailable.
}
}
const input = el('textarea', 'clipboard-fallback-input', text);
input.setAttribute('readonly', '');
input.style.position = 'fixed';
input.style.top = '-9999px';
input.style.left = '-9999px';
input.style.opacity = '0';
document.body.append(input);
input.focus();
input.select();
let copied = false;
try {
copied = document.execCommand('copy');
} finally {
input.remove();
}
if (!copied) throw new Error('当前浏览器不支持复制。');
}
async function copyTaskLifecycle(button) {
const lifecycle = $('#taskLifecycle');
const text = lifecycle?.textContent || '';
if (!lifecycle || !text.trim()) return;
button.disabled = true;
button.classList.remove('is-copied', 'is-error');
try {
await writeClipboardText(text);
button.textContent = '已复制';
button.classList.add('is-copied');
} catch (error) {
button.textContent = '复制失败';
button.classList.add('is-error');
} finally {
window.setTimeout(() => {
if (!button.isConnected) return;
button.disabled = false;
button.textContent = '一键复制';
button.classList.remove('is-copied', 'is-error');
}, 1600);
}
}
function renderTaskLifecycle(task) {
const entries = taskLifecycleEntries(task);
if (!entries.length) return el('pre', 'task-lifecycle-console', '暂无任务生命周期记录。');
const lines = [];
for (const entry of entries) {
const level = lifecycleLevel(entry);
const levelText = { error: 'ERROR', ok: 'OK', warn: 'WARN', info: 'INFO' }[level] || 'INFO';
const time = new Date(entry.created_at || Date.now());
const timeText = Number.isNaN(time.getTime())
? '--:--:--'
: time.toLocaleTimeString('zh-CN', { hour12: false });
const status = entry.status ? `[${entry.status}]` : '[event]';
lines.push(`${timeText} ${levelText.padEnd(5, ' ')} [${lifecycleGroupLabel(entry)}] ${status} ${entry.message || '状态已更新。'}`);
const details = taskLifecycleDetails(entry, task);
const hasTechnicalDetails = Object.keys(details.payload || {}).length > 0
|| details.parse_response !== undefined
|| details.executor_response !== undefined;
if (hasTechnicalDetails) {
const technical = JSON.stringify(redactJsonForDisplay(details), null, 2)
.split('\n')
.map((line) => ` ${line}`)
.join('\n');
lines.push(` 技术详情:\n${technical}`);
}
}
return el('pre', 'task-lifecycle-console', lines.join('\n'));
}
function taskLogStageLabel(stage) {
const normalized = String(stage || '').toLowerCase();
if (normalized === 'parse') return 'AI处理阶段';
if (['receipt', 'verification', 'reconciliation'].includes(normalized)) return '获取回执';
return '系统写入处理';
}
function taskSourceInfo(task) {
const source = task?.source === 'agentbus' ? 'agentbus' : 'manual';
return source === 'agentbus'
? { className: 'agentbus', label: 'AgentBus' }
: { className: 'manual', label: '人工输入' };
}
function configurePageMode() {
document.body.classList.toggle('history-page', IS_HISTORY_PAGE);
document.body.classList.toggle('channels-page-active', IS_CHANNELS_PAGE);
const instructionPanel = document.querySelector('.instruction-panel');
const historyFilters = $('#historyFilters');
const channelsPage = $('#channelsPage');
const title = $('#taskListTitle');
const hint = $('#taskListHint');
const historyLink = $('#historyLink');
const workbenchNav = $('#workbenchNav');
const historyNav = $('#historyNav');
const channelsNav = $('#channelsNav');
if (instructionPanel) instructionPanel.hidden = IS_HISTORY_PAGE;
if (channelsPage) channelsPage.hidden = !IS_CHANNELS_PAGE;
if (historyFilters) historyFilters.hidden = !IS_HISTORY_PAGE;
if (title) title.textContent = IS_HISTORY_PAGE ? '历史任务目录' : '任务卡片';
if (hint) hint.textContent = IS_HISTORY_PAGE ? '全部未删除任务' : '最近任务';
if (historyLink) {
historyLink.href = IS_HISTORY_PAGE ? '/' : '/history';
historyLink.textContent = IS_HISTORY_PAGE ? '返回 AI 操作台' : '查看更多';
historyLink.hidden = !IS_HISTORY_PAGE && taskListMeta.total <= HOME_TASK_LIMIT;
}
if (workbenchNav) {
const workbenchActive = !IS_HISTORY_PAGE && !IS_CHANNELS_PAGE;
workbenchNav.classList.toggle('is-active', workbenchActive);
if (!workbenchActive) workbenchNav.removeAttribute('aria-current');
else workbenchNav.setAttribute('aria-current', 'page');
}
if (historyNav) {
historyNav.classList.toggle('is-active', IS_HISTORY_PAGE);
if (IS_HISTORY_PAGE) historyNav.setAttribute('aria-current', 'page');
else historyNav.removeAttribute('aria-current');
}
if (channelsNav) {
channelsNav.classList.toggle('is-active', IS_CHANNELS_PAGE);
if (IS_CHANNELS_PAGE) channelsNav.setAttribute('aria-current', 'page');
else channelsNav.removeAttribute('aria-current');
}
if (IS_CHANNELS_PAGE) document.title = 'AgentBus 渠道 · LianSyn-platform';
else if (IS_HISTORY_PAGE) document.title = '历史任务 · LianSyn-platform';
const searchInput = $('#historySearchInput');
const statusInput = $('#historyStatusInput');
if (searchInput && searchInput.value !== historySearch) searchInput.value = historySearch;
if (statusInput && statusInput.value !== historyStatus) statusInput.value = historyStatus;
}
function renderTaskListHeader() {
const total = Math.max(taskListMeta.total, taskStore.length);
const count = $('#taskCount');
if (count) {
if (IS_HISTORY_PAGE) {
count.textContent = `${total} 个任务`;
} else if (total > HOME_TASK_LIMIT) {
count.textContent = `最近 ${Math.min(taskStore.length, HOME_TASK_LIMIT)} 条 / 共 ${total} 个任务`;
} else {
count.textContent = `${Math.min(taskStore.length, HOME_TASK_LIMIT)} 个任务`;
}
}
const historyLink = $('#historyLink');
if (historyLink) historyLink.hidden = !IS_HISTORY_PAGE && total <= HOME_TASK_LIMIT;
}
function renderHistoryPagination() {
const pagination = $('#historyPagination');
if (!pagination) return;
pagination.hidden = !IS_HISTORY_PAGE;
if (!IS_HISTORY_PAGE) return;
const total = taskListMeta.total;
const start = total ? taskListMeta.offset + 1 : 0;
const end = Math.min(taskListMeta.offset + taskStore.length, total);
const summary = $('#historyPageSummary');
if (summary) summary.textContent = total ? `显示 ${start}-${end} 条,共 ${total} 条` : '暂无符合条件的任务';
const previous = $('#historyPreviousButton');
const next = $('#historyNextButton');
if (previous) previous.disabled = taskListMeta.offset <= 0;
if (next) next.disabled = !taskListMeta.has_more;
}
function renderTaskCards() {
const container = $('#taskCards');
if (!container) return;
renderTaskListHeader();
renderHistoryPagination();
container.innerHTML = '';
const visibleTasks = IS_HISTORY_PAGE ? taskStore : taskStore.slice(0, HOME_TASK_LIMIT);
if (!visibleTasks.length) {
const empty = document.createElement('div');
empty.className = 'task-empty';
empty.textContent = IS_HISTORY_PAGE && (historySearch || historyStatus)
? '暂无符合筛选条件的任务。'
: '暂无任务。';
container.appendChild(empty);
renderTaskDetail();
return;
}
for (const task of visibleTasks) {
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';
const source = taskSourceInfo(task);
card.innerHTML = `