import { apiClient } from '/static/portal/common/api-client.js?v=20260920'; 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'; const FINANCIAL_NL2SQL_AGENT_TYPE = 'financial_nl2sql'; if (requireOperator()) { mountShell({ active: 'operator-nl2sql', mode: 'operator' }); const context = getAuthContext(); const operatorId = String(context?.userId || context?.username || ''); const pageParams = new URLSearchParams(location.search); const state = { offsiteTask: pageParams.get('task_id') || '', returnMailId: pageParams.get('mail_id') || '', returnPage: pageParams.get('page') || '1', 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: '执行中', queued: '排队中', processing: '处理中', succeeded: '查询成功', failed: '查询失败', cancelled: '已取消', }; 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 generalResultData(run) { const result = run?.result; return result?.data || result?.result?.data || null; } function generalGeneratedSql(run) { return run?.result?.sql || run?.result?.generated_sql || ''; } function generalResultRows(run) { const data = generalResultData(run); return Array.isArray(data?.rows) ? data.rows : []; } function renderGeneralResultDetail(run) { const rows = generalResultRows(run); if (rows.length) { const columns = [...new Set(rows.flatMap((row) => Object.keys(row || {})))]; return `
${columns.map((column) => ``).join('')}${rows.map((row) => `${columns.map((column) => ``).join('')}`).join('')}
${escapeHtml(column)}
${escapeHtml(String(row?.[column] ?? '--'))}
`; } const data = generalResultData(run); const emptyMessage = data?.total !== undefined ? `查询返回 ${data.total} 条记录` : '本次查询没有返回可展示的字段值'; return `
${escapeHtml(emptyMessage)}
`; } function renderGeneralSql(run) { const sql = generalGeneratedSql(run); return sql ? `
${escapeHtml(sql)}
` : '
本次运行未生成可执行 SQL。
'; } 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/?${new URLSearchParams({ task_id: state.offsiteTask, mail_id: state.returnMailId, page: state.returnPage }).toString()}` : '/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; generalResult.innerHTML = run ? `
状态
${escapeHtml(statusLabel(run.status))}

最终查询结果

${renderGeneralResultDetail(run)}

AI 生成的 SQL

${renderGeneralSql(run)}
` : '
提交问题后,前端会自动轮询运行状态。
'; } 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 = `portal-${crypto.randomUUID()}`; const response = await apiClient.post('AGENT_RUN_CREATE', { agent_type: FINANCIAL_NL2SQL_AGENT_TYPE, message, session_id: sessionId, idempotency_key: crypto.randomUUID().replaceAll('-', '') }); state.generalRun = response.data; renderGeneral(); showToast('查询已提交,正在获取结果'); 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(); }