137 lines
8.7 KiB
JavaScript
137 lines
8.7 KiB
JavaScript
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 `<span class="tag">${escapeHtml(status || '等待执行')}</span>`; }
|
|
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 = `<span class="tag" data-offsite-status>${escapeHtml(statusLabel(result?.status))}</span>`;
|
|
offsiteResult.innerHTML = result ? `<dl class="operator-kv"><div><dt>单据</dt><dd>${escapeHtml(result.task_id || state.offsiteTask)}</dd></div><div><dt>执行状态</dt><dd>${statusTag(statusLabel(result.status))}</dd></div><div><dt>查询数量</dt><dd>${escapeHtml(String(result.queries?.length || 0))}</dd></div><div><dt>返回说明</dt><dd>${escapeHtml(result.message || '已完成服务端核对')}</dd></div></dl><div class="operator-table-wrap"><table class="operator-table"><thead><tr><th>核对规则</th><th>状态</th><th>返回行数</th><th>错误信息</th></tr></thead><tbody>${(result.queries || []).map((item) => `<tr><td>${escapeHtml(ruleLabel(item))}<small class="operator-inline-note">${escapeHtml(item.rule_code || '')}</small></td><td>${escapeHtml(statusLabel(item.status))}</td><td>${escapeHtml(String(item.row_count ?? 0))}</td><td>${escapeHtml(item.error_message || '--')}</td></tr>`).join('') || '<tr><td colspan="4">暂无查询记录。</td></tr>'}</tbody></table></div><pre class="operator-json">${escapeHtml(JSON.stringify(result, null, 2))}</pre>` : '<div class="operations-empty">输入 task_id 后执行一次只读核对。</div>';
|
|
}
|
|
function renderGeneral() {
|
|
const run = state.generalRun;
|
|
document.querySelector('[data-general-status]').textContent = run?.status || '等待执行';
|
|
generalResult.innerHTML = run ? `<dl class="operator-kv"><div><dt>运行编号</dt><dd>${escapeHtml(run.run_id)}</dd></div><div><dt>Agent 类型</dt><dd>${escapeHtml(run.agent_type || '--')}</dd></div><div><dt>状态</dt><dd>${escapeHtml(run.status)}</dd></div><div><dt>错误码</dt><dd>${escapeHtml(run.error_code || '--')}</dd></div></dl><pre class="operator-json">${escapeHtml(JSON.stringify(run.result || run, null, 2))}</pre>` : '<div class="operations-empty">提交问题后,前端会自动轮询运行状态。</div>';
|
|
}
|
|
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();
|
|
}
|