Files
Auto-Finance/src/web/static/js/agent.js
T
wandering 9043886b6f refactor: 架构重组 — doc/bot → core/infra,新增 Agent 调度模块
- src/doc/ 拆分为 src/core/extraction/, matching/, validation/(核心业务逻辑)
- src/bot/ 重命名为 src/infra/browser/(浏览器自动化基础设施)
- fill_consumable_doc.py → src/infra/documents/consumable.py
- 新增 Agent 调度模块:coordinator.py, events.py, session.py,重构 orchestrator.py
- 更新 AGENTS.md、README.md 及所有子目录 README
2026-07-02 18:36:19 +08:00

291 lines
7.7 KiB
JavaScript

/**
* Agent 交互模块
*/
import { App } from './state.js';
import { escapeHtml } from './utils.js';
import { addChatMessage, showStatus } from './chat.js';
import { createSSEConnection, handleDoneResult } from './sse.js';
// ================================================================
// Agent 事件处理
// ================================================================
/**
* 处理 Agent SSE 事件
*
* @param {Object} msg - SSE 传来的 agent 事件对象
*/
export function handleAgentEvent(msg) {
switch (msg.type) {
case 'agent_state_change':
_handleAgentStateChange(msg);
break;
case 'agent_request_supplement':
_handleAgentRequestSupplement(msg);
break;
case 'agent_ready':
_handleAgentReady(msg);
break;
case 'agent_error':
_handleAgentError(msg);
break;
case 'agent_supplement_received':
_handleSupplementReceived(msg);
break;
case 'agent_force_submit':
_handleForceSubmit(msg);
break;
case 'agent_max_rounds':
_handleAgentError(msg);
break;
case 'agent_extract_status':
_handleAgentStateChange(msg);
break;
}
}
/**
* 状态变更通知 — 使用瞬态消息,状态变化时更新而非追加
*/
function _handleAgentStateChange(msg) {
const stateLabels = {
'extracting': '正在分析文件...',
'validating': '正在校验信息完整性...',
'awaiting_supplement': '等待补充材料',
'ready': '信息完整,可以提交',
'submitting': '正在提交...',
};
const label = stateLabels[msg.state] || msg.message || '处理中...';
if (msg.state === 'ready') {
showStatus(label, 'done');
} else {
showStatus(label);
}
}
/**
* 请求补充材料 — 将 suggestion 以消息气泡形式单独展示
*/
export function _handleAgentRequestSupplement(msg) {
const suggestion = msg.suggestion || '请补充上传相关材料';
showStatus('信息不完整,请补充材料');
addChatMessage(suggestion, 'system');
}
/**
* 信息完整 — 在消息气泡中提醒用户,最终结果由 done 事件处理
*/
function _handleAgentReady(msg) {
addChatMessage(msg.message || '信息完整,正在提交到财务系统', 'done');
}
/**
* Agent 错误
*/
function _handleAgentError(msg) {
addChatMessage(msg.message || 'Agent 处理出错', 'error');
App.isProcessing = false;
App.processState = 'done';
}
/**
* 收到补充文件
*/
function _handleSupplementReceived(msg) {
const fileNames = (msg.files || []).join('、');
addChatMessage(`已收到补充文件:${fileNames}`, 'done');
}
/**
* 强制提交
*/
function _handleForceSubmit(msg) {
showStatus('已跳过校验,开始提交到财务系统');
}
// ================================================================
// Agent 请求面板
// ================================================================
/**
* 显示 Agent 请求补充材料的 UI
*/
export function showAgentRequest(data) {
const existing = document.getElementById('agent-request-panel');
if (existing) existing.remove();
const chatMessages = document.getElementById('chat-messages');
if (!chatMessages) return;
const panel = document.createElement('div');
panel.id = 'agent-request-panel';
panel.className = 'agent-request-panel';
let materialsHtml = '';
if (data.missing_materials && data.missing_materials.length > 0) {
materialsHtml = `
<div class="agent-request-materials">
<strong>需要补充:</strong>
${data.missing_materials.map(m => `<span class="material-tag">${escapeHtml(m)}</span>`).join(' ')}
</div>
`;
}
panel.innerHTML = `
<div class="agent-request-header">
<span class="agent-request-icon"><img src="/static/icon/file.svg" alt=""></span>
<span>第 ${data.round || 1} 轮分析 - 信息不完整</span>
</div>
${materialsHtml}
<div class="agent-request-actions">
<button class="btn btn-sm btn-primary" onclick="document.getElementById('file-input').click()">
上传补充材料
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="handleForceSubmit()">
直接提交
</button>
</div>
`;
chatMessages.appendChild(panel);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
/**
* 移除 Agent 请求面板
*/
export function hideAgentRequest() {
const panel = document.getElementById('agent-request-panel');
if (panel) panel.remove();
}
// ================================================================
// 用户文字补充
// ================================================================
/**
* 处理用户通过输入框补充的文字信息
*
* @param {string} text - 用户输入的文字
*/
export async function handleUserSupplement(text) {
if (!App.sessionId) return;
try {
showStatus('正在解析补充信息...');
const response = await fetch(`/api/agent/user-supplement/${App.sessionId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text }),
});
const result = await response.json();
if (result.status === 'started') {
showStatus('补充信息已收到,正在重新分析...');
App.isProcessing = true;
createSSEConnection(App.sessionId, {
onDone: (result) => {
handleDoneResult(result);
},
handleFileProgress: false,
handleLLMStream: false,
});
} else if (result.error) {
addChatMessage(`处理失败:${result.error}`, 'error');
}
} catch (e) {
addChatMessage(`请求失败:${e.message}`, 'error');
}
}
// ================================================================
// 用户输入处理
// ================================================================
/**
* 处理强制提交
*/
export async function handleForceSubmit() {
if (!App.sessionId) {
addChatMessage('请先上传文件并处理', 'error');
return;
}
if (App.agentEventSource) {
App.agentEventSource.close();
App.agentEventSource = null;
}
App.forceSubmitting = true;
try {
showStatus('正在跳过校验并提交...');
hideAgentRequest();
const response = await fetch(`/api/agent/force-submit/${App.sessionId}`, {
method: 'POST',
});
const result = await response.json();
if (result.status === 'started') {
showStatus('已跳过校验,开始提交到财务系统');
App.isProcessing = true;
App.processState = 'submitting';
createSSEConnection(App.sessionId, {
onDone: (result) => {
handleDoneResult(result);
},
handleFileProgress: false,
handleLLMStream: false,
});
}
} catch (e) {
App.forceSubmitting = false;
addChatMessage(`请求失败:${e.message}`, 'error');
}
}
// ================================================================
// 补充文件处理
// ================================================================
/**
* 处理补充文件上传完成后的重新分析
*
* @param {Array<string>} filenames - 新上传的文件名列表
*/
export async function handleSupplementUpload(filenames) {
if (!App.sessionId) return;
try {
showStatus('收到补充文件,正在重新分析...');
hideAgentRequest();
const response = await fetch(`/api/agent/supplement/${App.sessionId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ files: filenames }),
});
const result = await response.json();
if (result.status === 'started') {
App.isProcessing = true;
createSSEConnection(App.sessionId, {
onDone: (result) => {
handleDoneResult(result);
},
});
}
} catch (e) {
addChatMessage(`请求失败:${e.message}`, 'error');
}
}