Files
LWLT-AI/mock-business-system/app.js
2026-07-13 19:57:46 +08:00

1672 lines
55 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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
用房:
SGL0
TWN0
TRP0
DBL2
HNM0
TL0
单价:
成人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(/<br\s*\/?>/gi, '\n')
.replace(/<\/(p|div|li|tr)>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/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 = `
<div class="task-card-head">
<div class="task-card-title-wrap">
<strong class="task-card-title"></strong>
<span class="task-id"></span>
</div>
<div class="task-card-status">
<span class="task-pill ${statusClassName(status)}"></span>
</div>
</div>
`;
card.querySelector('.task-card-title').textContent = summary.operation_label || '输入解析任务';
card.querySelector('.task-id').textContent = task.task_id;
card.querySelector('.task-card-status .task-pill').textContent = taskStatusText(task);
container.appendChild(card);
}
renderTaskDetail();
}
function toNumber(value) {
const normalized = String(value ?? '').replace(/[,]/g, '').trim();
const num = Number(normalized);
return Number.isFinite(num) ? num : 0;
}
function setBridgeState(text, type) {
const el = $('#bridgeState');
if (!el) return;
const value = el.querySelector('.status-icon-value');
if (value) value.textContent = text;
else el.textContent = text;
el.className = `status-icon ${type || 'state-warn'}`;
}
function setAiState(text, type) {
const el = $('#aiState');
if (!el) return;
const value = el.querySelector('.status-icon-value');
if (value) value.textContent = text;
else el.textContent = text;
el.className = `status-icon ${type || 'state-warn'}`;
}
async function pingAi() {
try {
const response = await fetch('/api/status', { cache: 'no-store' });
const result = await response.json();
if (!response.ok || result.ai_connected !== true) {
setAiState('未连接', 'state-bad');
return false;
}
setAiState('已连接', 'state-ok');
return true;
} catch (error) {
setAiState('未连接', 'state-bad');
return false;
}
}
function applyBridgePayload(payload = {}) {
bridgeConnected = Boolean(payload.ok);
if (!bridgeConnected) {
erpAutomationEnabled = false;
extensionCompatible = false;
setBridgeState('未连接', 'state-bad');
return false;
}
const parseVersion = (value) => String(value || '').split('.').map((part) => Number(part) || 0);
const current = parseVersion(payload.version);
const required = parseVersion(REQUIRED_EXTENSION_VERSION);
let versionComparison = 0;
for (let index = 0; index < 3; index += 1) {
const currentPart = current[index] || 0;
const requiredPart = required[index] || 0;
if (currentPart === requiredPart) continue;
versionComparison = currentPart > requiredPart ? 1 : -1;
break;
}
extensionCompatible = versionComparison >= 0;
if (!extensionCompatible) {
erpAutomationEnabled = false;
setBridgeState(`需更新到 ${REQUIRED_EXTENSION_VERSION}`, 'state-bad');
return true;
}
erpAutomationEnabled = payload.erp_automation_enabled !== false;
if (!erpAutomationEnabled) {
setBridgeState('未连接', 'state-bad');
return true;
}
setBridgeState('已连接', 'state-ok');
return true;
}
function setTaskState(text) {
const node = $('#taskState');
if (!node) return;
node.textContent = text;
const normalized = String(text || '');
const type = /失败|阻断|错误|不可用/.test(normalized)
? 'state-bad'
: /完成|收到|确认|成功|已连接/.test(normalized)
? 'state-ok'
: 'state-warn';
node.className = `state ${type}`;
}
function taskLogMessage(output) {
if (typeof output === 'string') return output;
if (!output || typeof output !== 'object') return '';
const blockers = Array.isArray(output.blockers)
? output.blockers
: Array.isArray(output.result?.blockers)
? output.result.blockers
: [];
if (blockers.length) return `阻断:${blockers.join('')}`;
return output.message
|| output.next_step
|| output.error
|| (output.status ? `状态更新:${output.status}` : '');
}
function setOutput(output) {
const task = selectedTask();
if (task && output !== undefined) {
const status = typeof output === 'object' ? output.status || task.status : task.status;
appendTaskLogEntry(task, taskLogMessage(output), { status });
saveTaskStore();
}
renderTaskDetail();
}
function makeRequestId() {
if (crypto.randomUUID) return crypto.randomUUID();
return `REQ-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function sendToExtension(type, payload = {}, timeoutMs = 2500) {
const requestId = makeRequestId();
const promise = new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(requestId);
reject(new Error('插件桥接未响应,请确认扩展已安装并刷新本页。'));
}, timeoutMs);
pending.set(requestId, { resolve, reject, timer });
});
window.postMessage({
source: BUSINESS_SOURCE,
requestId,
type,
payload
}, window.location.origin);
return promise;
}
window.addEventListener('message', (event) => {
if (event.source !== window) return;
const message = event.data || {};
if (message.source !== EXTENSION_SOURCE) return;
if (message.type === 'BRIDGE_READY') {
applyBridgePayload(message.payload || { ok: true });
return;
}
const waiter = pending.get(message.requestId);
if (!waiter) return;
clearTimeout(waiter.timer);
pending.delete(message.requestId);
if (message.type === 'ERROR' || message.payload?.ok === false) {
waiter.reject(new Error(message.payload?.message || '插件返回错误。'));
return;
}
waiter.resolve(message.payload);
});
async function parseRawInstruction(rawText, taskId) {
const receivedAt = new Date().toISOString();
const requestStartedAt = new Date().toISOString();
const pendingRequest = {
service: 'external_parse_api',
transport: 'sse',
stage: 'sending',
session_id_present: false,
event_count: 0,
updated_at: requestStartedAt
};
updateLocalTask(taskId, {
status: 'running',
stage: 'parse',
message: '正在发送到外部解析服务……',
error: ''
});
setTaskState('正在发送到外部解析服务');
setOutput({
status: 'parse_sending',
task_id: taskId,
message: '业务系统已发起解析请求,正在创建独立 session。',
external_request: pendingRequest
});
let response;
try {
response = await fetch('/api/parse', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
raw_text: rawText,
task_id: taskId,
received_at: receivedAt
})
});
} catch (error) {
const result = {
status: 'parse_blocked',
blockers: [`无法连接本地解析服务:${error.message}`],
operation: null,
external_request: {
...pendingRequest,
stage: 'local_request_failed',
error_code: 'local_parse_api_unreachable',
updated_at: new Date().toISOString()
}
};
updateLocalTask(taskId, {
status: 'parse_failed',
stage: 'parse',
message: '无法连接本地解析服务。',
error: result.blockers.join(''),
result,
parse_response: result
});
setTaskState('解析服务连接失败');
setOutput(result);
return result;
}
let result;
try {
result = await response.json();
} catch (error) {
result = {
status: 'parse_blocked',
blockers: [`解析服务返回了无效响应:${error.message}`],
operation: null,
external_request: {
...pendingRequest,
stage: 'invalid_response',
error_code: 'local_parse_api_invalid_response',
updated_at: new Date().toISOString()
}
};
}
if (!response.ok) {
result = {
status: 'parse_blocked',
blockers: [result.message || result.error || `解析服务调用失败HTTP ${response.status}`],
operation: null,
external_request: result.external_request || {
...pendingRequest,
stage: 'local_api_error',
http_status: response.status,
updated_at: new Date().toISOString()
}
};
}
const hasBlockers = Array.isArray(result.blockers) && result.blockers.length > 0;
updateLocalTask(taskId, {
status: hasBlockers ? 'parse_failed' : 'running',
stage: 'parse',
message: hasBlockers ? '外部解析服务已返回阻断结果。' : '外部解析服务已返回结果,正在整理业务字段。',
error: hasBlockers ? result.blockers.join('') : '',
parse_response: result
});
setTaskState(hasBlockers ? '外部解析服务已返回阻断' : '已收到外部解析结果');
setOutput({
status: hasBlockers ? 'parse_response_blocked' : 'parse_response_received',
task_id: taskId,
external_request: result.external_request || {
...pendingRequest,
stage: 'response_received',
updated_at: new Date().toISOString()
},
result
});
return result;
}
async function handoffTaskToExtension(task) {
if (!task?.confirmed_at) {
throw new Error('任务尚未人工确认,不能提交执行。');
}
if (task.status !== 'confirmed' || task.handoff_status !== 'awaiting_handoff') {
throw new Error('任务已领取或已进入执行流程,系统不会再次下发。');
}
if (!bridgeConnected || !extensionCompatible) {
updateLocalTask(task.task_id, {
status: 'waiting_extension',
stage: 'bridge',
message: `任务已确认,但插件未连接或版本低于 ${REQUIRED_EXTENSION_VERSION};不会自动提交。`,
error: '插件不可用。',
handoff_status: 'waiting_extension'
});
throw new Error('插件未连接或版本过低,任务已保留且不会自动补交。');
}
if (!erpAutomationEnabled) {
updateLocalTask(task.task_id, {
status: 'paused',
stage: 'automation_disabled',
message: '任务已确认,但插件 ERP 操作开关已关闭;不会自动提交。',
handoff_status: 'paused',
error: ''
});
return {
ok: true,
task_id: task.task_id,
status: 'paused',
erp_automation_enabled: false,
message: '插件 ERP 操作开关已关闭,未交给 ERP 自动执行器,也不会自动补交。'
};
}
updateLocalTask(task.task_id, {
status: 'queued',
stage: 'handoff',
message: '正在向服务端申请唯一 ERP 执行权。',
handoff_status: 'syncing'
});
let claim;
try {
claim = await apiRequest(`/api/tasks/${encodeURIComponent(task.task_id)}/claim`, {
method: 'POST',
body: { connection_id: BROWSER_CONNECTION_ID }
});
if (claim.task) upsertLocalTask(claim.task);
if (!claim.claimed) {
startPolling();
return {
ok: true,
task_id: task.task_id,
status: 'already_claimed',
dispatched: false,
message: '服务端已存在执行记录,本次没有再次发送给插件。'
};
}
const executionId = claim.execution_id;
if (!executionId) throw new Error('服务端没有返回唯一执行编号,已停止下发。');
const result = await sendToExtension('CREATE_TASK', {
task: claim.task || task,
execution_id: executionId
}, 4000);
if (result?.accepted === false || result?.status === 'duplicate_blocked') {
throw new Error(result.message || '插件检测到重复任务,已阻止再次执行。');
}
currentTaskId = result.task_id || task.task_id;
sessionStorage.setItem('ltjt_mock_current_task_id', currentTaskId);
updateLocalTask(task.task_id, {
status: 'running',
stage: 'auto_executor',
message: '插件已接单,后台会自动打开 ERP 表单、预检、保存并回查。',
handoff_status: 'running',
handoff_response: { ...result, execution_id: executionId }
});
startPolling();
return { ...result, execution_id: executionId };
} catch (error) {
if (claim?.claimed && claim.execution_id) {
const uncertainResult = {
task_id: task.task_id,
execution_id: claim.execution_id,
status: 'execution_uncertain',
stage: 'dispatch',
execution_phase: 'dispatch_uncertain',
write_attempted: true,
message: `任务领取后插件响应不确定:${error.message}。已停止自动重试,请回查 ERP。`
};
try {
const persisted = await apiRequest(`/api/tasks/${encodeURIComponent(task.task_id)}/result`, {
method: 'POST',
body: {
connection_id: BROWSER_CONNECTION_ID,
execution_id: claim.execution_id,
result: uncertainResult
}
});
if (persisted.task) upsertLocalTask(persisted.task);
} catch (persistError) {
updateLocalTask(task.task_id, {
status: 'reconciliation_pending',
stage: 'reconciliation',
handoff_status: 'reconciliation_pending',
message: uncertainResult.message,
error: persistError.message
});
}
}
throw error;
}
}
async function confirmTask(task) {
if (!requiresManualConfirmation(task)) return;
const bridgeReady = await pingBridge();
if (!bridgeReady || !extensionCompatible || !erpAutomationEnabled) {
throw new Error(`插件未连接、版本低于 ${REQUIRED_EXTENSION_VERSION} 或 ERP 操作开关未开启;任务尚未确认,也不会自动补交。`);
}
const response = await apiRequest(`/api/tasks/${encodeURIComponent(task.task_id)}/confirm`, { method: 'POST', body: {} });
const confirmedTask = response.task;
upsertLocalTask(confirmedTask);
try {
const handoff = await handoffTaskToExtension(confirmedTask);
if (handoff?.status === 'paused') {
setTaskState('ERP 操作已关闭');
setOutput({
status: 'task_confirmed_paused',
task_id: task.task_id,
message: handoff.message
});
} else {
setOutput({ status: 'task_confirmed', task_id: task.task_id, result: handoff });
}
} catch (error) {
setTaskState('已确认,等待插件连接');
setOutput({
status: 'task_confirmed_waiting_extension',
task_id: task.task_id,
message: error.message
});
}
}
async function pingBridge() {
try {
const result = await sendToExtension('PING', {}, 1200);
applyBridgePayload(result);
if (authUser && result?.ok) {
await apiRequest('/api/connections/heartbeat', {
method: 'POST',
body: {
connection_id: `administrator-browser:${location.origin}`,
extension_version: result.version || '',
metadata: { bridge_installed_at: result.bridge_installed_at || '' }
}
});
}
return bridgeConnected;
} catch (error) {
bridgeConnected = false;
erpAutomationEnabled = false;
setBridgeState('未连接', 'state-bad');
return false;
}
}
async function createTask() {
if (!authUser) {
showLoginPanel('请先登录。');
return;
}
const rawText = $('#rawInstruction').value;
const task = await apiRequest('/api/tasks', {
method: 'POST',
body: {
raw_text: rawText,
idempotency_key: `create:${makeRequestId()}`
}
});
const remoteTask = task.task;
currentTaskId = remoteTask.task_id;
sessionStorage.setItem('ltjt_mock_current_task_id', currentTaskId);
upsertLocalTask(remoteTask);
setTaskState('任务已入队');
setOutput({
status: 'task_created',
task_id: currentTaskId,
message: '任务已写入平台数据库,等待服务端解析。'
});
startRemoteEventStream();
}
async function pollResult() {
if (!currentTaskId) {
setOutput('还没有任务。');
return;
}
const result = await pollTaskResult(currentTaskId);
setTaskState(result ? `${result.stage} / ${result.status}` : `等待插件反馈:${currentTaskId}`);
setOutput(result || {
status: 'waiting',
task_id: currentTaskId
});
}
async function deleteSelectedTask() {
const task = selectedTask();
if (!task) return;
const taskId = task.task_id;
const deleteButton = $('#deleteTaskButton');
if (deleteButton) deleteButton.disabled = true;
appendTaskLogEntry(task, '正在请求取消任务并清理本地记录。', {
status: 'cancelling',
level: 'INFO'
});
saveTaskStore();
renderTaskDetail();
if (deleteButton) deleteButton.disabled = true;
try {
const needsPluginCancellation = Boolean(
task.confirmed_at
|| ['accepted', 'syncing', 'waiting_extension', 'paused'].includes(task.handoff_status)
);
if (needsPluginCancellation) {
const cancellation = await sendToExtension('DELETE_TASK', { task_id: taskId }, 4000);
if (cancellation?.ok === false) {
throw new Error(cancellation.message || '插件未确认任务已取消。');
}
}
const cancelled = await apiRequest(`/api/tasks/${encodeURIComponent(taskId)}/cancel`, { method: 'POST', body: {} });
if (cancelled.task) updateLocalTask(taskId, cancelled.task);
taskStore = taskStore.filter((item) => item.task_id !== taskId);
const nextTask = taskStore[0] || null;
currentTaskId = nextTask?.task_id || '';
if (currentTaskId) sessionStorage.setItem('ltjt_mock_current_task_id', currentTaskId);
else sessionStorage.removeItem('ltjt_mock_current_task_id');
saveTaskStore();
if (!taskStore.some(isTaskPollable)) {
if (pollTimer) clearInterval(pollTimer);
pollTimer = null;
}
renderTaskCards();
} catch (error) {
setTaskState('删除失败');
setOutput({
status: 'delete_error',
task_id: taskId,
message: `未删除任务:${error.message || String(error)}`
});
if (deleteButton) deleteButton.disabled = false;
}
}
async function pollTaskResult(taskId) {
const result = await sendToExtension('GET_TASK_RESULT', { task_id: taskId }, 3000);
if (result.result) {
try {
const executionId = result.result.execution_id;
if (!executionId) {
throw new Error(`插件版本过低或执行记录缺少 execution_id请更新到 ${REQUIRED_EXTENSION_VERSION};系统不会重复下发。`);
}
const persisted = await apiRequest(`/api/tasks/${encodeURIComponent(taskId)}/result`, {
method: 'POST',
body: {
connection_id: BROWSER_CONNECTION_ID,
execution_id: executionId,
result: result.result
}
});
if (persisted.task) upsertLocalTask(persisted.task);
} catch (error) {
updateLocalTask(taskId, {
result: result.result,
status: result.result.status || 'running',
stage: result.result.stage || '',
message: `插件已返回结果,但服务端持久化失败:${error.message}`,
error: error.message
});
}
}
return result.result || null;
}
async function pollAllTaskResults() {
if (!bridgeConnected) return;
const activeTasks = taskStore.filter(isTaskPollable);
for (const task of activeTasks) {
try {
await pollTaskResult(task.task_id);
} catch (error) {
updateLocalTask(task.task_id, {
message: `查询插件反馈失败:${error.message}`,
error: error.message
});
}
}
if (!taskStore.some(isTaskPollable) && pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function startPolling() {
if (pollTimer) clearInterval(pollTimer);
pollAllTaskResults().catch((error) => {
setTaskState('轮询失败');
setOutput({ status: 'poll_error', message: error.message });
});
pollTimer = setInterval(() => {
pollAllTaskResults().catch((error) => {
setTaskState('轮询失败');
setOutput({ status: 'poll_error', message: error.message });
});
}, 2500);
}
async function clearTasks() {
try {
await sendToExtension('CLEAR_TASKS', {}, 3000);
} catch (error) {
// Local business-system cards can still be cleared when the extension is disconnected.
}
currentTaskId = '';
taskStore = [];
saveTaskStore();
sessionStorage.removeItem('ltjt_mock_current_task_id');
if (pollTimer) clearInterval(pollTimer);
setTaskState('已清空');
setOutput('暂无任务。');
renderTaskCards();
}
async function initializeSession() {
try {
const me = await apiRequest('/api/auth/me');
await refreshCsrfToken();
showAuthenticatedApp(me.user);
await syncRemoteTasks();
startRemoteEventStream();
return true;
} catch (error) {
showLoginPanel('请登录后继续。');
return false;
}
}
document.addEventListener('DOMContentLoaded', async () => {
// Remove credentials/configuration left by the retired browser-side parser.
localStorage.removeItem('ltjt_mock_agent_config');
sessionStorage.removeItem('ltjt_mock_agent_api_key');
$('#rawInstruction').value = sessionStorage.getItem('ltjt_mock_raw_instruction') || SAMPLE_RAW;
$('#rawInstruction').addEventListener('input', () => {
sessionStorage.setItem('ltjt_mock_raw_instruction', $('#rawInstruction').value);
});
showLoginPanel();
$('#loginForm').addEventListener('submit', async (event) => {
event.preventDefault();
const errorNode = $('#loginError');
if (errorNode) errorNode.textContent = '';
const submitButton = $('#loginForm button[type="submit"]');
if (submitButton) submitButton.disabled = true;
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({
username: $('#loginUsername').value,
password: $('#loginPassword').value
})
});
const result = await response.json();
if (!response.ok) throw new Error(result.message || '登录失败。');
csrfToken = result.csrf_token || '';
showAuthenticatedApp(result.user);
$('#loginPassword').value = '';
await syncRemoteTasks();
startRemoteEventStream();
await pingAi();
await pingBridge();
} catch (error) {
if (errorNode) errorNode.textContent = error.message || String(error);
} finally {
if (submitButton) submitButton.disabled = false;
}
});
$('#logoutButton').addEventListener('click', async () => {
try {
await apiRequest('/api/auth/logout', { method: 'POST', body: {} });
} catch (error) {
// The server may already have expired the session; clear the local view regardless.
}
taskStore = [];
currentTaskId = '';
sessionStorage.removeItem('ltjt_mock_current_task_id');
showLoginPanel('已退出登录。');
renderTaskCards();
});
$('#createTaskButton').addEventListener('click', () => {
createTask().catch((error) => {
const result = {
status: 'create_error',
task_id: currentTaskId,
message: error.message
};
updateLocalTask(currentTaskId, {
status: 'parse_failed',
stage: 'parse',
message: '创建任务时发生未处理错误。',
error: error.message,
result
});
setTaskState('创建失败');
setOutput(result);
});
});
$('#aiState').addEventListener('click', () => {
pingAi().catch(() => {});
});
$('#bridgeState').addEventListener('click', () => {
pingBridge().catch(() => {});
});
$('#confirmTaskButton').addEventListener('click', () => {
const task = selectedTask();
if (!task) return;
const action = requiresManualConfirmation(task)
? confirmTask(task)
: canStartConfirmedTask(task)
? handoffTaskToExtension(task)
: null;
if (!action) return;
action.catch((error) => setOutput({
status: 'confirmation_error',
task_id: task.task_id,
message: error.message
}));
});
$('#deleteTaskButton').addEventListener('click', () => {
const task = selectedTask();
if (!task) return;
if (!window.confirm(`确认删除任务 ${task.task_id}?已提交的插件任务也会请求取消。`)) return;
deleteSelectedTask().catch((error) => setOutput({
status: 'delete_error',
task_id: task.task_id,
message: error.message || String(error)
}));
});
$('#taskCards').addEventListener('click', (event) => {
const button = event.target.closest('button[data-task-action]');
const card = event.target.closest('.task-card[data-task-id]');
const taskId = button?.dataset.taskId || card?.dataset.taskId;
if (!taskId) return;
const task = taskStore.find((item) => item.task_id === taskId);
if (!task) return;
currentTaskId = taskId;
sessionStorage.setItem('ltjt_mock_current_task_id', currentTaskId);
renderTaskCards();
});
if (await initializeSession()) {
renderTaskCards();
pingAi().catch(() => {});
pingBridge().catch(() => {});
// The control plane caches this health probe; keep the browser refresh
// interval conservative so status checks cannot pressure the Agent API.
setInterval(() => {
pingAi().catch(() => {});
pingBridge().catch(() => {});
syncRemoteTasks().catch(() => {});
}, 30_000);
if (currentTaskId) renderTaskDetail();
if (taskStore.some(isTaskPollable)) startPolling();
}
});