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) => `| ${escapeHtml(ruleLabel(item))}${escapeHtml(item.rule_code || '')} | ${escapeHtml(statusLabel(item.status))} | ${escapeHtml(String(item.row_count ?? 0))} | ${escapeHtml(item.error_message || '--')} |
`).join('') || '| 暂无查询记录。 |
'}
${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();
}