import { apiClient } from '/static/portal/common/api-client.js?v=20260913'; import { getAuthContext, requireOperator } from '/static/portal/common/auth.js'; import { escapeHtml } from '/static/portal/common/formatters.js'; import { mountShell } from '/static/portal/common/layout/app-shell.js'; import { showToast } from '/static/portal/common/notifications.js'; if (requireOperator()) { mountShell({ active: 'operator-nl2sql', mode: 'operator' }); const context = getAuthContext(); const operatorId = String(context?.userId || context?.username || ''); const state = { offsiteTask: new URLSearchParams(location.search).get('task_id') || '', offsiteNaturalLanguage: '', offsiteResult: null, generalRun: null, timer: null, }; const offsiteTask = document.querySelector('[data-offsite-task]'); const offsiteNaturalLanguage = document.querySelector('[data-offsite-natural-language]'); const offsiteResult = document.querySelector('[data-offsite-result]'); const generalResult = document.querySelector('[data-general-result]'); offsiteTask.value = state.offsiteTask; document.querySelector('[data-operator-name]').textContent = context?.username || '运营人员'; document.querySelector('[data-operator-scope]').textContent = `数据范围:${context?.dataScope || 'assigned'}`; const backLink = document.querySelector('[data-offsite-back]'); const STATUS_LABELS = { planned: '核对完成', query_failed: '查询失败', success: '查询成功', error: '查询失败', pending: '待执行', running: '执行中', }; const RULE_LABELS = { subscription_holding_ratio: '申购后单一投资者持有比例', subscription_single_share_limit: '申购单笔份额上限', redemption_large_ratio: '赎回巨额比例', redemption_available_quantity: '账户可用份额', }; function statusLabel(status) { return STATUS_LABELS[String(status || '').toLowerCase()] || status || '等待执行'; } function ruleLabel(item) { return item.rule_name || RULE_LABELS[item.rule_code] || item.rule_code || '查询规则'; } function statusTag(status) { return `${escapeHtml(status || '等待执行')}`; } function buildNaturalLanguage(fields) { const fundCode = String(fields?.fund_code || '').trim(); if (!fundCode) throw new Error('当前单据缺少基金代码,无法生成自然语言'); let base = `基金代码为${fundCode}`; const accountIdentifier = String(fields?.account_identifier || '').trim(); if (accountIdentifier) base += `,账户标识为${accountIdentifier}`; if (fields.document_type === 'subscription') { return [ `${base},查询基金最新总份额、最新净值和申请前持有份额`, `${base},查询基金最新总份额和最新净值`, ].join('\n'); } return [ `${base},查询产品最新总份额`, `${base},查询账户当前最新可用份额`, ].join('\n'); } function renderOffsite() { const result = state.offsiteResult; offsiteNaturalLanguage.value = state.offsiteNaturalLanguage; if (backLink) { backLink.href = state.offsiteTask ? `/portal/employee-operations/offsite/?task_id=${encodeURIComponent(state.offsiteTask)}` : '/portal/employee-operations/offsite/'; } document.querySelector('[data-offsite-status]').outerHTML = `${escapeHtml(statusLabel(result?.status))}`; offsiteResult.innerHTML = result ? `
单据
${escapeHtml(result.task_id || state.offsiteTask)}
执行状态
${statusTag(statusLabel(result.status))}
查询数量
${escapeHtml(String(result.queries?.length || 0))}
返回说明
${escapeHtml(result.message || '已完成服务端核对')}
${(result.queries || []).map((item) => ``).join('') || ''}
核对规则状态返回行数错误信息
${escapeHtml(ruleLabel(item))}${escapeHtml(item.rule_code || '')}${escapeHtml(statusLabel(item.status))}${escapeHtml(String(item.row_count ?? 0))}${escapeHtml(item.error_message || '--')}
暂无查询记录。
${escapeHtml(JSON.stringify(result, null, 2))}
` : '
输入 task_id 后执行一次只读核对。
'; } function renderGeneral() { const run = state.generalRun; document.querySelector('[data-general-status]').textContent = run?.status || '等待执行'; generalResult.innerHTML = run ? `
运行编号
${escapeHtml(run.run_id)}
Agent 类型
${escapeHtml(run.agent_type || '--')}
状态
${escapeHtml(run.status)}
错误码
${escapeHtml(run.error_code || '--')}
${escapeHtml(JSON.stringify(run.result || run, null, 2))}
` : '
提交问题后,前端会自动轮询运行状态。
'; } async function poll(runId) { if (state.timer) clearTimeout(state.timer); const response = await apiClient.get('AGENT_RUN', { pathParams: { runId } }); state.generalRun = response.data; renderGeneral(); if (['queued', 'running', 'processing', 'pending'].includes(String(state.generalRun.status).toLowerCase())) { state.timer = window.setTimeout(() => poll(runId).catch((error) => showToast(error.message, 'error')), 1200); } else showToast(state.generalRun.status === 'succeeded' ? '通用查询已完成' : `查询状态:${state.generalRun.status}`, state.generalRun.status === 'succeeded' ? 'success' : 'error'); } async function run(action) { try { if (action === 'generate-offsite') { const taskId = offsiteTask.value.trim(); if (!taskId) throw new Error('请输入单据 task_id'); const response = await apiClient.get('OFFSITE_NL2SQL_FIELDS', { pathParams: { taskId } }); state.offsiteTask = taskId; state.offsiteNaturalLanguage = buildNaturalLanguage(response.data); renderOffsite(); showToast('自然语言已生成,可人工修改'); return; } if (action === 'run-offsite') { const taskId = offsiteTask.value.trim(); if (!taskId) throw new Error('请输入单据 task_id'); if (!document.querySelector('[data-manual-confirmed]').checked) throw new Error('请先确认原始文件内容'); const response = await apiClient.post('OFFSITE_TRIGGER_NL2SQL', { operator_id: operatorId, manual_confirmed: true }, { pathParams: { taskId } }); state.offsiteResult = response.data; renderOffsite(); showToast(response.data?.status === 'query_failed' ? '核对完成,但存在查询失败' : '只读核对已完成'); return; } if (action === 'run-general') { const message = document.querySelector('[data-query-text]').value.trim(); if (!message) throw new Error('请输入业务问题'); const sessionId = document.querySelector('[data-session-id]').value.trim() || `portal-${Date.now()}`; document.querySelector('[data-session-id]').value = sessionId; const response = await apiClient.post('AGENT_RUN_CREATE', { agent_type: document.querySelector('[data-agent-type]').value, message, session_id: sessionId, idempotency_key: crypto.randomUUID().replaceAll('-', '') }); state.generalRun = response.data; renderGeneral(); showToast(`查询已提交:${response.data.run_id}`); poll(response.data.run_id); return; } } catch (error) { apiClient.reportError(error); showToast(error.message || '操作失败', 'error'); } } document.querySelectorAll('[data-tab]').forEach((tab) => tab.addEventListener('click', () => { document.querySelectorAll('[data-tab]').forEach((item) => item.setAttribute('aria-selected', String(item === tab))); document.querySelectorAll('[data-view]').forEach((view) => { view.hidden = view.dataset.view !== tab.dataset.tab; }); })); document.querySelector('main').addEventListener('click', (event) => { const action = event.target.closest('[data-action]')?.dataset.action; if (action) run(action); }); offsiteTask.addEventListener('input', () => { state.offsiteTask = offsiteTask.value; state.offsiteNaturalLanguage = ''; offsiteNaturalLanguage.value = ''; }); offsiteNaturalLanguage.addEventListener('input', () => { state.offsiteNaturalLanguage = offsiteNaturalLanguage.value; }); renderOffsite(); renderGeneral(); }